Merge branch 'main' into mqtt-cleanup

This commit is contained in:
Brett Christensen 2024-02-13 18:57:54 +11:00
commit a74ae258c1
60 changed files with 1667 additions and 800 deletions

30
.github/workflows/unit-tests.yml vendored Normal file
View file

@ -0,0 +1,30 @@
name: Run Unit Tests
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure and build with CMake
run: |
mkdir build
cd build
cmake ..
cmake --build .
- name: Run unit tests
run: |
set -e # Exit immediately on non-zero exit code
cd build/test
dir -s
for test_executable in *; do
if [ -f "$test_executable" ] && [ -x "$test_executable" ]; then
./"$test_executable"
fi
done

7
.gitignore vendored
View file

@ -1,5 +1,8 @@
# Ignore any .vscode folder # Ignore any .vscode folder
*.vscode/ *.vscode/
# Ignore any files in the build folder # Ignore any files in any build folder
Software/build/ *build/
# Ignore .exe (unit tests)
*.exe

10
CMakeLists.txt Normal file
View file

@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.10)
# Set the C++ standard to C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
project(BatteryEmulator)
# add_subdirectory(Software/src/devboard/utils)
add_subdirectory(test)

View file

@ -97,6 +97,7 @@ static uint8_t brightness = 0;
static bool rampUp = true; static bool rampUp = true;
const uint8_t maxBrightness = 100; const uint8_t maxBrightness = 100;
uint8_t LEDcolor = GREEN; uint8_t LEDcolor = GREEN;
bool test_all_colors = false;
// Contactor parameters // Contactor parameters
#ifdef CONTACTOR_CONTROL #ifdef CONTACTOR_CONTROL
@ -131,9 +132,7 @@ void setup() {
init_webserver(); init_webserver();
#endif #endif
#ifdef EVENTLOGGING
init_events(); init_events();
#endif
init_CAN(); init_CAN();
@ -152,6 +151,9 @@ void setup() {
#ifdef BATTERY_HAS_INIT #ifdef BATTERY_HAS_INIT
init_battery(); init_battery();
#endif #endif
// BOOT button at runtime is used as an input for various things
pinMode(0, INPUT_PULLUP);
} }
// Perform main program functions // Perform main program functions
@ -190,7 +192,7 @@ void loop() {
previousMillisUpdateVal = millis(); previousMillisUpdateVal = millis();
update_values(); // Update values heading towards inverter. Prepare for sending on CAN, or write directly to Modbus. update_values(); // Update values heading towards inverter. Prepare for sending on CAN, or write directly to Modbus.
if (DUMMY_EVENT_ENABLED) { if (DUMMY_EVENT_ENABLED) {
set_event(EVENT_DUMMY, (uint8_t)millis()); set_event(EVENT_DUMMY_ERROR, (uint8_t)millis());
} }
} }
@ -199,7 +201,13 @@ void loop() {
#ifdef DUAL_CAN #ifdef DUAL_CAN
send_can2(); send_can2();
#endif #endif
update_event_timestamps(); run_event_handling();
if (digitalRead(0) == HIGH) {
test_all_colors = false;
} else {
test_all_colors = true;
}
} }
// Initialization functions // Initialization functions
@ -560,30 +568,30 @@ void handle_LED_state() {
} else if (!rampUp && brightness == 0) { } else if (!rampUp && brightness == 0) {
rampUp = true; rampUp = true;
} }
switch (LEDcolor) { if (test_all_colors == false) {
case GREEN: switch (get_event_level()) {
pixels.setPixelColor(0, pixels.Color(0, brightness, 0)); // Green pulsing LED case EVENT_LEVEL_INFO:
break; LEDcolor = GREEN;
case YELLOW: pixels.setPixelColor(0, pixels.Color(0, brightness, 0)); // Green pulsing LED
pixels.setPixelColor(0, pixels.Color(brightness, brightness, 0)); // Yellow pulsing LED break;
break; case EVENT_LEVEL_WARNING:
case BLUE: LEDcolor = YELLOW;
pixels.setPixelColor(0, pixels.Color(0, 0, brightness)); // Blue pulsing LED pixels.setPixelColor(0, pixels.Color(brightness, brightness, 0)); // Yellow pulsing LED
break; break;
case RED: case EVENT_LEVEL_DEBUG:
pixels.setPixelColor(0, pixels.Color(150, 0, 0)); // Red LED full brightness case EVENT_LEVEL_UPDATE:
break; LEDcolor = BLUE;
case TEST_ALL_COLORS: pixels.setPixelColor(0, pixels.Color(0, 0, brightness)); // Blue pulsing LED
pixels.setPixelColor(0, pixels.Color(brightness, abs((100 - brightness)), abs((50 - brightness)))); // RGB break;
break; case EVENT_LEVEL_ERROR:
default: LEDcolor = RED;
break; pixels.setPixelColor(0, pixels.Color(150, 0, 0)); // Red LED full brightness
} break;
default:
// BMS in fault state overrides everything break;
if (bms_status == FAULT) { }
LEDcolor = RED; } else {
pixels.setPixelColor(0, pixels.Color(255, 0, 0)); // Red LED full brightness pixels.setPixelColor(0, pixels.Color(brightness, abs((100 - brightness)), abs((50 - brightness)))); // RGB
} }
pixels.show(); // This sends the updated pixel color to the hardware. pixels.show(); // This sends the updated pixel color to the hardware.

View file

@ -16,11 +16,6 @@ volatile uint16_t MAXDISCHARGEAMP =
300; //30.0A , BYD CAN specific setting, Max discharge speed in Amp (Some inverters needs to be artificially limited) 300; //30.0A , BYD CAN specific setting, Max discharge speed in Amp (Some inverters needs to be artificially limited)
/* Charger settings (Optional, when generator charging) */ /* Charger settings (Optional, when generator charging) */
// MQTT
#ifdef MQTT
const char* mqtt_user = "REDACTED";
const char* mqtt_password = "REDACTED";
#endif // USE_MQTT
/* Charger settings */ /* Charger settings */
volatile float CHARGER_SET_HV = 384; // Reasonably appropriate 4.0v per cell charging of a 96s pack volatile float CHARGER_SET_HV = 384; // Reasonably appropriate 4.0v per cell charging of a 96s pack
volatile float CHARGER_MAX_HV = 420; // Max permissible output (VDC) of charger volatile float CHARGER_MAX_HV = 420; // Max permissible output (VDC) of charger
@ -37,4 +32,11 @@ const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Minimum of 8 characters
const char* ssidAP = "Battery Emulator"; // Maximum of 63 characters; const char* ssidAP = "Battery Emulator"; // Maximum of 63 characters;
const char* passwordAP = "123456789"; // Minimum of 8 characters; set to NULL if you want the access point to be open const char* passwordAP = "123456789"; // Minimum of 8 characters; set to NULL if you want the access point to be open
const uint8_t wifi_channel = 0; // set to 0 for automatic channel selection const uint8_t wifi_channel = 0; // set to 0 for automatic channel selection
// MQTT
#ifdef MQTT
const char* mqtt_user = "REDACTED";
const char* mqtt_password = "REDACTED";
#endif // USE_MQTT
#endif #endif

View file

@ -12,7 +12,7 @@
//#define CHADEMO_BATTERY //#define CHADEMO_BATTERY
//#define IMIEV_CZERO_ION_BATTERY //#define IMIEV_CZERO_ION_BATTERY
//#define KIA_HYUNDAI_64_BATTERY //#define KIA_HYUNDAI_64_BATTERY
//#define NISSAN_LEAF_BATTERY // #define NISSAN_LEAF_BATTERY
//#define RENAULT_KANGOO_BATTERY //#define RENAULT_KANGOO_BATTERY
//#define RENAULT_ZOE_BATTERY //#define RENAULT_ZOE_BATTERY
//#define SANTA_FE_PHEV_BATTERY //#define SANTA_FE_PHEV_BATTERY
@ -21,7 +21,7 @@
/* Select inverter communication protocol. See Wiki for which to use with your inverter: https://github.com/dalathegreat/BYD-Battery-Emulator-For-Gen24/wiki */ /* Select inverter communication protocol. See Wiki for which to use with your inverter: https://github.com/dalathegreat/BYD-Battery-Emulator-For-Gen24/wiki */
//#define BYD_CAN //Enable this line to emulate a "BYD Battery-Box Premium HVS" over CAN Bus //#define BYD_CAN //Enable this line to emulate a "BYD Battery-Box Premium HVS" over CAN Bus
//#define BYD_MODBUS //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU // #define BYD_MODBUS //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU
//#define LUNA2000_MODBUS //Enable this line to emulate a "Luna2000 battery" over Modbus RTU //#define LUNA2000_MODBUS //Enable this line to emulate a "Luna2000 battery" over Modbus RTU
//#define PYLON_CAN //Enable this line to emulate a "Pylontech battery" over CAN bus //#define PYLON_CAN //Enable this line to emulate a "Pylontech battery" over CAN bus
//#define SMA_CAN //Enable this line to emulate a "BYD Battery-Box H 8.9kWh, 7 mod" over CAN bus //#define SMA_CAN //Enable this line to emulate a "BYD Battery-Box H 8.9kWh, 7 mod" over CAN bus
@ -37,17 +37,16 @@
//#define SERIAL_LINK_RECEIVER //Enable this line to receive battery data over RS485 pins from another Lilygo (This LilyGo interfaces with inverter) //#define SERIAL_LINK_RECEIVER //Enable this line to receive battery data over RS485 pins from another Lilygo (This LilyGo interfaces with inverter)
//#define SERIAL_LINK_TRANSMITTER //Enable this line to send battery data over RS485 pins to another Lilygo (This LilyGo interfaces with battery) //#define SERIAL_LINK_TRANSMITTER //Enable this line to send battery data over RS485 pins to another Lilygo (This LilyGo interfaces with battery)
#define WEBSERVER //Enable this line to enable WiFi, and to run the webserver. See USER_SETTINGS.cpp for the Wifi settings. #define WEBSERVER //Enable this line to enable WiFi, and to run the webserver. See USER_SETTINGS.cpp for the Wifi settings.
//#define LOAD_SAVED_SETTINGS_ON_BOOT //Enable this line to read settings stored via the webserver on boot // #define LOAD_SAVED_SETTINGS_ON_BOOT //Enable this line to read settings stored via the webserver on boot
/* MQTT options */ /* MQTT options */
//#define MQTT // Enable this line to enable MQTT // #define MQTT // Enable this line to enable MQTT
#define MQTT_SUBSCRIPTIONS \ #define MQTT_SUBSCRIPTIONS \
{ "my/topic/abc", "my/other/topic" } { "my/topic/abc", "my/other/topic" }
#define MQTT_SERVER "192.168.xxx.yyy" #define MQTT_SERVER "192.168.xxx.yyy"
#define MQTT_PORT 1883 #define MQTT_PORT 1883
/* Event options*/ /* Event options*/
#define EVENTLOGGING //Enable this line to log events to the event log
#define DUMMY_EVENT_ENABLED false //Enable this line to have a dummy event that gets logged to test the interface #define DUMMY_EVENT_ENABLED false //Enable this line to have a dummy event that gets logged to test the interface
/* Select charger used (Optional) */ /* Select charger used (Optional) */

View file

@ -55,8 +55,6 @@ static uint16_t DC_link = 0;
static int16_t Battery_Power = 0; static int16_t Battery_Power = 0;
void update_values_i3_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus void update_values_i3_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
bms_status = ACTIVE; //Startout in active mode
//Calculate the SOC% value to send to inverter //Calculate the SOC% value to send to inverter
Calculated_SOC = (Display_SOC * 10); //Increase decimal amount Calculated_SOC = (Display_SOC * 10); //Increase decimal amount
Calculated_SOC = Calculated_SOC =
@ -102,9 +100,7 @@ void update_values_i3_battery() { //This function maps all the values fetched v
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!CANstillAlive) { if (!CANstillAlive) {
bms_status = FAULT; set_event(EVENT_CAN_RX_FAILURE, 0);
Serial.println("No CAN communication detected for 60s. Shutting down battery control.");
set_event(EVENT_CAN_FAILURE, 0);
} else { } else {
CANstillAlive--; CANstillAlive--;
} }

View file

@ -18,7 +18,6 @@ extern uint16_t capacity_Wh;
extern uint16_t remaining_capacity_Wh; extern uint16_t remaining_capacity_Wh;
extern uint16_t max_target_discharge_power; extern uint16_t max_target_discharge_power;
extern uint16_t max_target_charge_power; extern uint16_t max_target_charge_power;
extern uint8_t bms_status;
extern uint8_t bms_char_dis_status; extern uint8_t bms_char_dis_status;
extern uint16_t stat_batt_power; extern uint16_t stat_batt_power;
extern uint16_t temperature_min; extern uint16_t temperature_min;

View file

@ -90,7 +90,6 @@ uint8_t HighCurrentControlStatus = 0;
uint8_t HighVoltageControlStatus = 0; uint8_t HighVoltageControlStatus = 0;
void update_values_chademo_battery() { //This function maps all the values fetched via CAN to the correct parameters used for the inverter void update_values_chademo_battery() { //This function maps all the values fetched via CAN to the correct parameters used for the inverter
bms_status = ACTIVE; //Startout in active mode
SOC = ChargingRate; SOC = ChargingRate;
@ -105,10 +104,8 @@ void update_values_chademo_battery() { //This function maps all the values fetc
/* Check if the Vehicle is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the Vehicle is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!CANstillAlive) { if (!CANstillAlive) {
bms_status = FAULT;
errorCode = 7; errorCode = 7;
Serial.println("No CAN communication detected for 60s. Shutting down battery control."); set_event(EVENT_CAN_RX_FAILURE, 0);
set_event(EVENT_CAN_FAILURE, 0);
} else { } else {
CANstillAlive--; CANstillAlive--;
} }

View file

@ -41,8 +41,6 @@ static double max_temp_cel = 20.00;
static double min_temp_cel = 19.00; static double min_temp_cel = 19.00;
void update_values_imiev_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus void update_values_imiev_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
bms_status = ACTIVE; //Startout in active mode
SOC = (uint16_t)(BMU_SOC * 100); //increase BMU_SOC range from 0-100 -> 100.00 SOC = (uint16_t)(BMU_SOC * 100); //increase BMU_SOC range from 0-100 -> 100.00
battery_voltage = (uint16_t)(BMU_PackVoltage * 10); // Multiply by 10 and cast to uint16_t battery_voltage = (uint16_t)(BMU_PackVoltage * 10); // Multiply by 10 and cast to uint16_t
@ -108,9 +106,7 @@ void update_values_imiev_battery() { //This function maps all the values fetche
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!CANstillAlive) { if (!CANstillAlive) {
bms_status = FAULT; set_event(EVENT_CAN_RX_FAILURE, 0);
Serial.println("No CAN communication detected for 60s. Shutting down battery control.");
set_event(EVENT_CAN_FAILURE, 0);
} else { } else {
CANstillAlive--; CANstillAlive--;
} }

View file

@ -18,7 +18,6 @@ extern uint16_t capacity_Wh;
extern uint16_t remaining_capacity_Wh; extern uint16_t remaining_capacity_Wh;
extern uint16_t max_target_discharge_power; extern uint16_t max_target_discharge_power;
extern uint16_t max_target_charge_power; extern uint16_t max_target_charge_power;
extern uint8_t bms_status;
extern uint8_t bms_char_dis_status; extern uint8_t bms_char_dis_status;
extern uint16_t stat_batt_power; extern uint16_t stat_batt_power;
extern uint16_t temperature_min; extern uint16_t temperature_min;
@ -28,7 +27,6 @@ extern uint16_t cell_max_voltage;
extern uint16_t cell_min_voltage; extern uint16_t cell_min_voltage;
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false
extern uint8_t LEDcolor;
void update_values_imiev_battery(); void update_values_imiev_battery();
void receive_can_imiev_battery(CAN_frame_t rx_frame); void receive_can_imiev_battery(CAN_frame_t rx_frame);

View file

@ -195,26 +195,18 @@ void update_values_kiaHyundai_64_battery() { //This function maps all the value
cell_min_voltage = CellVoltMin_mV; cell_min_voltage = CellVoltMin_mV;
bms_status = ACTIVE; //Startout in active mode. Then check safeties
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!CANstillAlive) { if (!CANstillAlive) {
bms_status = FAULT; set_event(EVENT_CAN_RX_FAILURE, 0);
Serial.println("No CAN communication detected for 60s. Shutting down battery control.");
set_event(EVENT_CAN_FAILURE, 0);
} else { } else {
CANstillAlive--; CANstillAlive--;
} }
if (waterleakageSensor == 0) { if (waterleakageSensor == 0) {
Serial.println("Water leakage inside battery detected. Operation halted. Inspect battery!");
bms_status = FAULT;
set_event(EVENT_WATER_INGRESS, 0); set_event(EVENT_WATER_INGRESS, 0);
} }
if (leadAcidBatteryVoltage < 110) { if (leadAcidBatteryVoltage < 110) {
Serial.println("12V battery source below required voltage to safely close contactors. Inspect the supply/battery!");
LEDcolor = YELLOW;
set_event(EVENT_12V_LOW, leadAcidBatteryVoltage); set_event(EVENT_12V_LOW, leadAcidBatteryVoltage);
} }
@ -222,18 +214,12 @@ void update_values_kiaHyundai_64_battery() { //This function maps all the value
cell_deviation_mV = (cell_max_voltage - cell_min_voltage); cell_deviation_mV = (cell_max_voltage - cell_min_voltage);
if (cell_max_voltage >= MAX_CELL_VOLTAGE) { if (cell_max_voltage >= MAX_CELL_VOLTAGE) {
bms_status = FAULT;
Serial.println("ERROR: CELL OVERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_OVER_VOLTAGE, 0); set_event(EVENT_CELL_OVER_VOLTAGE, 0);
} }
if (cell_min_voltage <= MIN_CELL_VOLTAGE) { if (cell_min_voltage <= MIN_CELL_VOLTAGE) {
bms_status = FAULT;
Serial.println("ERROR: CELL UNDERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_UNDER_VOLTAGE, 0); set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
} }
if (cell_deviation_mV > MAX_CELL_DEVIATION) { if (cell_deviation_mV > MAX_CELL_DEVIATION) {
LEDcolor = YELLOW;
Serial.println("ERROR: HIGH CELL DEVIATION!!! Inspect battery!");
set_event(EVENT_CELL_DEVIATION_HIGH, 0); set_event(EVENT_CELL_DEVIATION_HIGH, 0);
} }
@ -346,10 +332,15 @@ void receive_can_kiaHyundai_64_battery(CAN_frame_t rx_frame) {
ESP32Can.CANWriteFrame(&KIA64_7E4_id1); ESP32Can.CANWriteFrame(&KIA64_7E4_id1);
} else if (poll_data_pid == 2) { } else if (poll_data_pid == 2) {
ESP32Can.CANWriteFrame(&KIA64_7E4_id2); ESP32Can.CANWriteFrame(&KIA64_7E4_id2);
} else if (poll_data_pid == 3) {
ESP32Can.CANWriteFrame(&KIA64_7E4_id3);
} else if (poll_data_pid == 4) {
ESP32Can.CANWriteFrame(&KIA64_7E4_id4);
} else if (poll_data_pid == 5) { } else if (poll_data_pid == 5) {
ESP32Can.CANWriteFrame(&KIA64_7E4_id5); ESP32Can.CANWriteFrame(&KIA64_7E4_id5);
} else if (poll_data_pid == 6) { } else if (poll_data_pid == 6) {
ESP32Can.CANWriteFrame(&KIA64_7E4_id6); ESP32Can.CANWriteFrame(&KIA64_7E4_id6);
} else if (poll_data_pid == 7) {
} else if (poll_data_pid == 8) { } else if (poll_data_pid == 8) {
} else if (poll_data_pid == 9) { } else if (poll_data_pid == 9) {
} else if (poll_data_pid == 10) { } else if (poll_data_pid == 10) {
@ -367,10 +358,55 @@ void receive_can_kiaHyundai_64_battery(CAN_frame_t rx_frame) {
allowedChargePower = ((rx_frame.data.u8[3] << 8) + rx_frame.data.u8[4]); allowedChargePower = ((rx_frame.data.u8[3] << 8) + rx_frame.data.u8[4]);
allowedDischargePower = ((rx_frame.data.u8[5] << 8) + rx_frame.data.u8[6]); allowedDischargePower = ((rx_frame.data.u8[5] << 8) + rx_frame.data.u8[6]);
batteryRelay = rx_frame.data.u8[7]; batteryRelay = rx_frame.data.u8[7];
} else if (poll_data_pid == 2) {
cellvoltages[0] = (rx_frame.data.u8[2] * 20);
cellvoltages[1] = (rx_frame.data.u8[3] * 20);
cellvoltages[2] = (rx_frame.data.u8[4] * 20);
cellvoltages[3] = (rx_frame.data.u8[5] * 20);
cellvoltages[4] = (rx_frame.data.u8[6] * 20);
cellvoltages[5] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) {
cellvoltages[32] = (rx_frame.data.u8[2] * 20);
cellvoltages[33] = (rx_frame.data.u8[3] * 20);
cellvoltages[34] = (rx_frame.data.u8[4] * 20);
cellvoltages[35] = (rx_frame.data.u8[5] * 20);
cellvoltages[36] = (rx_frame.data.u8[6] * 20);
cellvoltages[37] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) {
cellvoltages[64] = (rx_frame.data.u8[2] * 20);
cellvoltages[65] = (rx_frame.data.u8[3] * 20);
cellvoltages[66] = (rx_frame.data.u8[4] * 20);
cellvoltages[67] = (rx_frame.data.u8[5] * 20);
cellvoltages[68] = (rx_frame.data.u8[6] * 20);
cellvoltages[69] = (rx_frame.data.u8[7] * 20);
} }
break; break;
case 0x22: //Second datarow in PID group case 0x22: //Second datarow in PID group
if (poll_data_pid == 6) { if (poll_data_pid == 2) {
cellvoltages[6] = (rx_frame.data.u8[1] * 20);
cellvoltages[7] = (rx_frame.data.u8[2] * 20);
cellvoltages[8] = (rx_frame.data.u8[3] * 20);
cellvoltages[9] = (rx_frame.data.u8[4] * 20);
cellvoltages[10] = (rx_frame.data.u8[5] * 20);
cellvoltages[11] = (rx_frame.data.u8[6] * 20);
cellvoltages[12] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) {
cellvoltages[38] = (rx_frame.data.u8[1] * 20);
cellvoltages[39] = (rx_frame.data.u8[2] * 20);
cellvoltages[40] = (rx_frame.data.u8[3] * 20);
cellvoltages[41] = (rx_frame.data.u8[4] * 20);
cellvoltages[42] = (rx_frame.data.u8[5] * 20);
cellvoltages[43] = (rx_frame.data.u8[6] * 20);
cellvoltages[44] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) {
cellvoltages[70] = (rx_frame.data.u8[1] * 20);
cellvoltages[71] = (rx_frame.data.u8[2] * 20);
cellvoltages[72] = (rx_frame.data.u8[3] * 20);
cellvoltages[73] = (rx_frame.data.u8[4] * 20);
cellvoltages[74] = (rx_frame.data.u8[5] * 20);
cellvoltages[75] = (rx_frame.data.u8[6] * 20);
cellvoltages[76] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 6) {
batteryManagementMode = rx_frame.data.u8[5]; batteryManagementMode = rx_frame.data.u8[5];
} }
break; break;
@ -378,8 +414,31 @@ void receive_can_kiaHyundai_64_battery(CAN_frame_t rx_frame) {
if (poll_data_pid == 1) { if (poll_data_pid == 1) {
temperature_water_inlet = rx_frame.data.u8[6]; temperature_water_inlet = rx_frame.data.u8[6];
CellVoltMax_mV = (rx_frame.data.u8[7] * 20); //(volts *50) *20 =mV CellVoltMax_mV = (rx_frame.data.u8[7] * 20); //(volts *50) *20 =mV
} } else if (poll_data_pid == 2) {
if (poll_data_pid == 5) { cellvoltages[13] = (rx_frame.data.u8[1] * 20);
cellvoltages[14] = (rx_frame.data.u8[2] * 20);
cellvoltages[15] = (rx_frame.data.u8[3] * 20);
cellvoltages[16] = (rx_frame.data.u8[4] * 20);
cellvoltages[17] = (rx_frame.data.u8[5] * 20);
cellvoltages[18] = (rx_frame.data.u8[6] * 20);
cellvoltages[19] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) {
cellvoltages[45] = (rx_frame.data.u8[1] * 20);
cellvoltages[46] = (rx_frame.data.u8[2] * 20);
cellvoltages[47] = (rx_frame.data.u8[3] * 20);
cellvoltages[48] = (rx_frame.data.u8[4] * 20);
cellvoltages[49] = (rx_frame.data.u8[5] * 20);
cellvoltages[50] = (rx_frame.data.u8[6] * 20);
cellvoltages[51] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) {
cellvoltages[77] = (rx_frame.data.u8[1] * 20);
cellvoltages[78] = (rx_frame.data.u8[2] * 20);
cellvoltages[79] = (rx_frame.data.u8[3] * 20);
cellvoltages[80] = (rx_frame.data.u8[4] * 20);
cellvoltages[81] = (rx_frame.data.u8[5] * 20);
cellvoltages[82] = (rx_frame.data.u8[6] * 20);
cellvoltages[83] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 5) {
heatertemp = rx_frame.data.u8[7]; heatertemp = rx_frame.data.u8[7];
} }
break; break;
@ -388,11 +447,58 @@ void receive_can_kiaHyundai_64_battery(CAN_frame_t rx_frame) {
CellVmaxNo = rx_frame.data.u8[1]; CellVmaxNo = rx_frame.data.u8[1];
CellVminNo = rx_frame.data.u8[3]; CellVminNo = rx_frame.data.u8[3];
CellVoltMin_mV = (rx_frame.data.u8[2] * 20); //(volts *50) *20 =mV CellVoltMin_mV = (rx_frame.data.u8[2] * 20); //(volts *50) *20 =mV
} else if (poll_data_pid == 2) {
cellvoltages[20] = (rx_frame.data.u8[1] * 20);
cellvoltages[21] = (rx_frame.data.u8[2] * 20);
cellvoltages[22] = (rx_frame.data.u8[3] * 20);
cellvoltages[23] = (rx_frame.data.u8[4] * 20);
cellvoltages[24] = (rx_frame.data.u8[5] * 20);
cellvoltages[25] = (rx_frame.data.u8[6] * 20);
cellvoltages[26] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 3) {
cellvoltages[52] = (rx_frame.data.u8[1] * 20);
cellvoltages[53] = (rx_frame.data.u8[2] * 20);
cellvoltages[54] = (rx_frame.data.u8[3] * 20);
cellvoltages[55] = (rx_frame.data.u8[4] * 20);
cellvoltages[56] = (rx_frame.data.u8[5] * 20);
cellvoltages[57] = (rx_frame.data.u8[6] * 20);
cellvoltages[58] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 4) {
cellvoltages[84] = (rx_frame.data.u8[1] * 20);
cellvoltages[85] = (rx_frame.data.u8[2] * 20);
cellvoltages[86] = (rx_frame.data.u8[3] * 20);
cellvoltages[87] = (rx_frame.data.u8[4] * 20);
cellvoltages[88] = (rx_frame.data.u8[5] * 20);
cellvoltages[89] = (rx_frame.data.u8[6] * 20);
cellvoltages[90] = (rx_frame.data.u8[7] * 20);
} else if (poll_data_pid == 5) { } else if (poll_data_pid == 5) {
batterySOH = ((rx_frame.data.u8[2] << 8) + rx_frame.data.u8[3]); batterySOH = ((rx_frame.data.u8[2] << 8) + rx_frame.data.u8[3]);
} }
break; break;
case 0x25: //Fifth datarow in PID group case 0x25: //Fifth datarow in PID group
if (poll_data_pid == 2) {
cellvoltages[27] = (rx_frame.data.u8[1] * 20);
cellvoltages[28] = (rx_frame.data.u8[2] * 20);
cellvoltages[29] = (rx_frame.data.u8[3] * 20);
cellvoltages[30] = (rx_frame.data.u8[4] * 20);
cellvoltages[31] = (rx_frame.data.u8[5] * 20);
} else if (poll_data_pid == 3) {
cellvoltages[59] = (rx_frame.data.u8[1] * 20);
cellvoltages[60] = (rx_frame.data.u8[2] * 20);
cellvoltages[61] = (rx_frame.data.u8[3] * 20);
cellvoltages[62] = (rx_frame.data.u8[4] * 20);
cellvoltages[63] = (rx_frame.data.u8[5] * 20);
} else if (poll_data_pid == 4) {
cellvoltages[91] = (rx_frame.data.u8[1] * 20);
cellvoltages[92] = (rx_frame.data.u8[2] * 20);
cellvoltages[93] = (rx_frame.data.u8[3] * 20);
cellvoltages[94] = (rx_frame.data.u8[4] * 20);
cellvoltages[95] = (rx_frame.data.u8[5] * 20);
} else if (poll_data_pid == 5) {
cellvoltages[96] = (rx_frame.data.u8[4] * 20);
cellvoltages[97] = (rx_frame.data.u8[5] * 20);
nof_cellvoltages = 98;
}
break; break;
case 0x26: //Sixth datarow in PID group case 0x26: //Sixth datarow in PID group
break; break;

View file

@ -20,14 +20,14 @@ extern uint16_t capacity_Wh; //Wh, 0-60000
extern uint16_t remaining_capacity_Wh; //Wh, 0-60000 extern uint16_t remaining_capacity_Wh; //Wh, 0-60000
extern uint16_t max_target_discharge_power; //W, 0-60000 extern uint16_t max_target_discharge_power; //W, 0-60000
extern uint16_t max_target_charge_power; //W, 0-60000 extern uint16_t max_target_charge_power; //W, 0-60000
extern uint8_t bms_status; //Enum, 0-5
extern uint8_t bms_char_dis_status; //Enum, 0-2 extern uint8_t bms_char_dis_status; //Enum, 0-2
extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530) extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530)
extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t cell_max_voltage; //mV, 0-4350 extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350 extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint8_t LEDcolor; //Enum, 0-10 extern uint16_t cellvoltages[120]; //mV 0-4350 per cell
extern uint8_t nof_cellvoltages; // Total number of cell voltages, set by each battery.
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false

View file

@ -246,8 +246,6 @@ void update_values_leaf_battery() { /* This function maps all the values fetched
cellvoltages[i] = cell_voltages[i]; cellvoltages[i] = cell_voltages[i];
} }
bms_status = ACTIVE; //Startout in active mode
/*Extra safety functions below*/ /*Extra safety functions below*/
if (LB_GIDS < 10) //800Wh left in battery if (LB_GIDS < 10) //800Wh left in battery
{ //Battery is running abnormally low, some discharge logic might have failed. Zero it all out. { //Battery is running abnormally low, some discharge logic might have failed. Zero it all out.
@ -259,11 +257,9 @@ void update_values_leaf_battery() { /* This function maps all the values fetched
if (battery_voltage > if (battery_voltage >
(ABSOLUTE_MAX_VOLTAGE - 100)) { // When pack voltage is close to max, and SOC% is still low, raise FAULT (ABSOLUTE_MAX_VOLTAGE - 100)) { // When pack voltage is close to max, and SOC% is still low, raise FAULT
if (LB_SOC < 650) { if (LB_SOC < 650) {
bms_status = FAULT; set_event(EVENT_SOC_PLAUSIBILITY_ERROR, LB_SOC / 10); // Set event with the SOC as data
#ifdef DEBUG_VIA_USB } else {
Serial.println("ERROR: SOC% reported by battery not plausible. Restart battery!"); clear_event(EVENT_SOC_PLAUSIBILITY_ERROR);
#endif
set_event(EVENT_SOC_PLAUSIBILITY_ERROR, LB_SOC / 10);
} }
} }
@ -308,30 +304,17 @@ void update_values_leaf_battery() { /* This function maps all the values fetched
break; break;
case (5): case (5):
//Caution Lamp Request & Normal Stop Request //Caution Lamp Request & Normal Stop Request
bms_status = FAULT;
errorCode = 2; errorCode = 2;
#ifdef DEBUG_VIA_USB
Serial.println("ERROR: Battery raised caution indicator AND requested discharge stop. Inspect battery status!");
#endif
set_event(EVENT_BATTERY_DISCHG_STOP_REQ, 0); set_event(EVENT_BATTERY_DISCHG_STOP_REQ, 0);
break; break;
case (6): case (6):
//Caution Lamp Request & Charging Mode Stop Request //Caution Lamp Request & Charging Mode Stop Request
bms_status = FAULT;
errorCode = 3; errorCode = 3;
#ifdef DEBUG_VIA_USB
Serial.println("ERROR: Battery raised caution indicator AND requested charge stop. Inspect battery status!");
#endif
set_event(EVENT_BATTERY_CHG_STOP_REQ, 0); set_event(EVENT_BATTERY_CHG_STOP_REQ, 0);
break; break;
case (7): case (7):
//Caution Lamp Request & Charging Mode Stop Request & Normal Stop Request //Caution Lamp Request & Charging Mode Stop Request & Normal Stop Request
bms_status = FAULT;
errorCode = 4; errorCode = 4;
#ifdef DEBUG_VIA_USB
Serial.println(
"ERROR: Battery raised caution indicator AND requested charge/discharge stop. Inspect battery status!");
#endif
set_event(EVENT_BATTERY_CHG_DISCHG_STOP_REQ, 0); set_event(EVENT_BATTERY_CHG_DISCHG_STOP_REQ, 0);
break; break;
default: default:
@ -341,11 +324,6 @@ void update_values_leaf_battery() { /* This function maps all the values fetched
if (LB_StateOfHealth < 25) { //Battery is extremely degraded, not fit for secondlifestorage. Zero it all out. if (LB_StateOfHealth < 25) { //Battery is extremely degraded, not fit for secondlifestorage. Zero it all out.
if (LB_StateOfHealth != 0) { //Extra check to see that we actually have a SOH Value available if (LB_StateOfHealth != 0) { //Extra check to see that we actually have a SOH Value available
#ifdef DEBUG_VIA_USB
Serial.println(
"ERROR: State of health critically low. Battery internal resistance too high to continue. Recycle battery.");
#endif
bms_status = FAULT;
errorCode = 5; errorCode = 5;
set_event(EVENT_LOW_SOH, LB_StateOfHealth); set_event(EVENT_LOW_SOH, LB_StateOfHealth);
max_target_discharge_power = 0; max_target_discharge_power = 0;
@ -355,12 +333,6 @@ void update_values_leaf_battery() { /* This function maps all the values fetched
#ifdef INTERLOCK_REQUIRED #ifdef INTERLOCK_REQUIRED
if (!LB_Interlock) { if (!LB_Interlock) {
#ifdef DEBUG_VIA_USB
Serial.println(
"ERROR: Battery interlock loop broken. Check that high voltage connectors are seated. Battery will be "
"disabled!");
#endif
bms_status = FAULT;
set_event(EVENT_HVIL_FAILURE, 0); set_event(EVENT_HVIL_FAILURE, 0);
errorCode = 6; errorCode = 6;
SOC = 0; SOC = 0;
@ -371,12 +343,8 @@ void update_values_leaf_battery() { /* This function maps all the values fetched
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!CANstillAlive) { if (!CANstillAlive) {
bms_status = FAULT;
errorCode = 7; errorCode = 7;
#ifdef DEBUG_VIA_USB set_event(EVENT_CAN_RX_FAILURE, 0);
Serial.println("ERROR: No CAN communication detected for 60s. Shutting down battery control.");
#endif
set_event(EVENT_CAN_FAILURE, 0);
} else { } else {
CANstillAlive--; CANstillAlive--;
} }
@ -384,11 +352,7 @@ void update_values_leaf_battery() { /* This function maps all the values fetched
MAX_CAN_FAILURES) //Also check if we have recieved too many malformed CAN messages. If so, signal via LED MAX_CAN_FAILURES) //Also check if we have recieved too many malformed CAN messages. If so, signal via LED
{ {
errorCode = 10; errorCode = 10;
LEDcolor = YELLOW; set_event(EVENT_CAN_RX_WARNING, 0);
#ifdef DEBUG_VIA_USB
Serial.println("ERROR: High amount of corrupted CAN messages detected. Check CAN wire shielding!");
#endif
set_event(EVENT_CAN_WARNING, 0);
} }
/*Finally print out values to serial if configured to do so*/ /*Finally print out values to serial if configured to do so*/
@ -620,27 +584,15 @@ void receive_can_leaf_battery(CAN_frame_t rx_frame) {
cell_min_voltage = min_max_voltage[0]; cell_min_voltage = min_max_voltage[0];
if (cell_deviation_mV > MAX_CELL_DEVIATION) { if (cell_deviation_mV > MAX_CELL_DEVIATION) {
LEDcolor = YELLOW;
#ifdef DEBUG_VIA_USB
Serial.println("HIGH CELL DEVIATION!!! Inspect battery!");
#endif
set_event(EVENT_CELL_DEVIATION_HIGH, 0); set_event(EVENT_CELL_DEVIATION_HIGH, 0);
} }
if (min_max_voltage[1] >= MAX_CELL_VOLTAGE) { if (min_max_voltage[1] >= MAX_CELL_VOLTAGE) {
bms_status = FAULT;
errorCode = 8; errorCode = 8;
#ifdef DEBUG_VIA_USB
Serial.println("CELL OVERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
#endif
set_event(EVENT_CELL_OVER_VOLTAGE, 0); set_event(EVENT_CELL_OVER_VOLTAGE, 0);
} }
if (min_max_voltage[0] <= MIN_CELL_VOLTAGE) { if (min_max_voltage[0] <= MIN_CELL_VOLTAGE) {
bms_status = FAULT;
errorCode = 9; errorCode = 9;
#ifdef DEBUG_VIA_USB
Serial.println("CELL UNDERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
#endif
set_event(EVENT_CELL_UNDER_VOLTAGE, 0); set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
} }
break; break;

View file

@ -18,14 +18,12 @@ extern uint16_t capacity_Wh; //Wh, 0-60000
extern uint16_t remaining_capacity_Wh; //Wh, 0-60000 extern uint16_t remaining_capacity_Wh; //Wh, 0-60000
extern uint16_t max_target_discharge_power; //W, 0-60000 extern uint16_t max_target_discharge_power; //W, 0-60000
extern uint16_t max_target_charge_power; //W, 0-60000 extern uint16_t max_target_charge_power; //W, 0-60000
extern uint8_t bms_status; //Enum, 0-5
extern uint8_t bms_char_dis_status; //Enum, 0-2 extern uint8_t bms_char_dis_status; //Enum, 0-2
extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530) extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530)
extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t cell_max_voltage; //mV, 0-4350 extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350 extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint8_t LEDcolor; //Enum, 0-10
extern uint16_t cellvoltages[120]; //mV 0-4350 per cell extern uint16_t cellvoltages[120]; //mV 0-4350 per cell
extern uint8_t nof_cellvoltages; // Total number of cell voltages, set by each battery. extern uint8_t nof_cellvoltages; // Total number of cell voltages, set by each battery.
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false

View file

@ -58,8 +58,6 @@ static const int interval100 = 100; // interval (ms) at which send CAN Messag
static const int interval1000 = 1000; // interval (ms) at which send CAN Messages static const int interval1000 = 1000; // interval (ms) at which send CAN Messages
void update_values_kangoo_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus void update_values_kangoo_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
bms_status = ACTIVE; //Startout in active mode
StateOfHealth = (LB_SOH * 100); //Increase range from 99% -> 99.00% StateOfHealth = (LB_SOH * 100); //Increase range from 99% -> 99.00%
//Calculate the SOC% value to send to Fronius //Calculate the SOC% value to send to Fronius
@ -125,26 +123,18 @@ void update_values_kangoo_battery() { //This function maps all the values fetch
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!CANstillAlive) { if (!CANstillAlive) {
bms_status = FAULT; set_event(EVENT_CAN_RX_FAILURE, 0);
Serial.println("No CAN communication detected for 60s. Shutting down battery control.");
set_event(EVENT_CAN_FAILURE, 0);
} else { } else {
CANstillAlive--; CANstillAlive--;
} }
if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) { if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) {
bms_status = FAULT;
Serial.println("ERROR: CELL OVERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_OVER_VOLTAGE, 0); set_event(EVENT_CELL_OVER_VOLTAGE, 0);
} }
if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) { if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) {
bms_status = FAULT;
Serial.println("ERROR: CELL UNDERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_UNDER_VOLTAGE, 0); set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
} }
if (cell_deviation_mV > MAX_CELL_DEVIATION_MV) { if (cell_deviation_mV > MAX_CELL_DEVIATION_MV) {
LEDcolor = YELLOW;
Serial.println("ERROR: HIGH CELL mV DEVIATION!!! Inspect battery!");
set_event(EVENT_CELL_DEVIATION_HIGH, 0); set_event(EVENT_CELL_DEVIATION_HIGH, 0);
} }

View file

@ -23,7 +23,6 @@ extern uint16_t capacity_Wh; //Wh, 0-60000
extern uint16_t remaining_capacity_Wh; //Wh, 0-60000 extern uint16_t remaining_capacity_Wh; //Wh, 0-60000
extern uint16_t max_target_discharge_power; //W, 0-60000 extern uint16_t max_target_discharge_power; //W, 0-60000
extern uint16_t max_target_charge_power; //W, 0-60000 extern uint16_t max_target_charge_power; //W, 0-60000
extern uint8_t bms_status; //Enum, 0-5
extern uint8_t bms_char_dis_status; //Enum, 0-2 extern uint8_t bms_char_dis_status; //Enum, 0-2
extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530) extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530)
extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
@ -31,7 +30,6 @@ extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 funct
extern uint16_t cell_max_voltage; //mV, 0-4350 extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350 extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint16_t CANerror; extern uint16_t CANerror;
extern uint8_t LEDcolor; //Enum, 0-10
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false

View file

@ -43,8 +43,6 @@ static const int interval100 = 100; // interval (ms) at which send CAN Messag
static const int interval1000 = 1000; // interval (ms) at which send CAN Messages static const int interval1000 = 1000; // interval (ms) at which send CAN Messages
void update_values_zoe_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus void update_values_zoe_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
bms_status = ACTIVE; //Startout in active mode
StateOfHealth = (LB_SOH * 100); //Increase range from 99% -> 99.00% StateOfHealth = (LB_SOH * 100); //Increase range from 99% -> 99.00%
//Calculate the SOC% value to send to Fronius //Calculate the SOC% value to send to Fronius
@ -86,26 +84,18 @@ void update_values_zoe_battery() { //This function maps all the values fetched
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!CANstillAlive) { if (!CANstillAlive) {
bms_status = FAULT; set_event(EVENT_CAN_RX_FAILURE, 0);
Serial.println("No CAN communication detected for 60s. Shutting down battery control.");
set_event(EVENT_CAN_FAILURE, 0);
} else { } else {
CANstillAlive--; CANstillAlive--;
} }
if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) { if (LB_Cell_Max_Voltage >= ABSOLUTE_CELL_MAX_VOLTAGE) {
bms_status = FAULT;
Serial.println("ERROR: CELL OVERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_OVER_VOLTAGE, 0); set_event(EVENT_CELL_OVER_VOLTAGE, 0);
} }
if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) { if (LB_Cell_Min_Voltage <= ABSOLUTE_CELL_MIN_VOLTAGE) {
bms_status = FAULT;
Serial.println("ERROR: CELL UNDERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_UNDER_VOLTAGE, 0); set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
} }
if (cell_deviation_mV > MAX_CELL_DEVIATION_MV) { if (cell_deviation_mV > MAX_CELL_DEVIATION_MV) {
LEDcolor = YELLOW;
Serial.println("ERROR: HIGH CELL mV DEVIATION!!! Inspect battery!");
set_event(EVENT_CELL_DEVIATION_HIGH, 0); set_event(EVENT_CELL_DEVIATION_HIGH, 0);
} }

View file

@ -23,7 +23,6 @@ extern uint16_t capacity_Wh; //Wh, 0-60000
extern uint16_t remaining_capacity_Wh; //Wh, 0-60000 extern uint16_t remaining_capacity_Wh; //Wh, 0-60000
extern uint16_t max_target_discharge_power; //W, 0-60000 extern uint16_t max_target_discharge_power; //W, 0-60000
extern uint16_t max_target_charge_power; //W, 0-60000 extern uint16_t max_target_charge_power; //W, 0-60000
extern uint8_t bms_status; //Enum, 0-5
extern uint8_t bms_char_dis_status; //Enum, 0-2 extern uint8_t bms_char_dis_status; //Enum, 0-2
extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530) extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530)
extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
@ -31,7 +30,6 @@ extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 funct
extern uint16_t cell_max_voltage; //mV, 0-4350 extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350 extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint16_t CANerror; extern uint16_t CANerror;
extern uint8_t LEDcolor; //Enum, 0-10
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false

View file

@ -79,13 +79,9 @@ void update_values_santafe_phev_battery() { //This function maps all the values
temperature_max; temperature_max;
bms_status = ACTIVE; //Startout in active mode, then check safeties
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!CANstillAlive) { if (!CANstillAlive) {
bms_status = FAULT; set_event(EVENT_CAN_RX_FAILURE, 0);
Serial.println("No CAN communication detected for 60s. Shutting down battery control.");
set_event(EVENT_CAN_FAILURE, 0);
} else { } else {
CANstillAlive--; CANstillAlive--;
} }

View file

@ -18,7 +18,6 @@ extern uint16_t capacity_Wh;
extern uint16_t remaining_capacity_Wh; extern uint16_t remaining_capacity_Wh;
extern uint16_t max_target_discharge_power; extern uint16_t max_target_discharge_power;
extern uint16_t max_target_charge_power; extern uint16_t max_target_charge_power;
extern uint8_t bms_status;
extern uint8_t bms_char_dis_status; extern uint8_t bms_char_dis_status;
extern uint16_t stat_batt_power; extern uint16_t stat_batt_power;
extern uint16_t temperature_min; extern uint16_t temperature_min;

View file

@ -1,6 +1,8 @@
// SERIAL-LINK-RECEIVER-FROM-BATTERY.cpp // SERIAL-LINK-RECEIVER-FROM-BATTERY.cpp
#include "SERIAL-LINK-RECEIVER-FROM-BATTERY.h" #include "SERIAL-LINK-RECEIVER-FROM-BATTERY.h"
#include <Arduino.h>
#include "../devboard/utils/events.h"
#define INVERTER_SEND_NUM_VARIABLES 1 #define INVERTER_SEND_NUM_VARIABLES 1
#define INVERTER_RECV_NUM_VARIABLES 16 #define INVERTER_RECV_NUM_VARIABLES 16
@ -35,7 +37,6 @@ void __getData() {
max_target_discharge_power = (uint16_t)dataLinkReceive.getReceivedData(6); max_target_discharge_power = (uint16_t)dataLinkReceive.getReceivedData(6);
max_target_charge_power = (uint16_t)dataLinkReceive.getReceivedData(7); max_target_charge_power = (uint16_t)dataLinkReceive.getReceivedData(7);
uint16_t _bms_status = (uint16_t)dataLinkReceive.getReceivedData(8); uint16_t _bms_status = (uint16_t)dataLinkReceive.getReceivedData(8);
bms_status = _bms_status;
bms_char_dis_status = (uint16_t)dataLinkReceive.getReceivedData(9); bms_char_dis_status = (uint16_t)dataLinkReceive.getReceivedData(9);
stat_batt_power = (uint16_t)dataLinkReceive.getReceivedData(10); stat_batt_power = (uint16_t)dataLinkReceive.getReceivedData(10);
temperature_min = (uint16_t)dataLinkReceive.getReceivedData(11); temperature_min = (uint16_t)dataLinkReceive.getReceivedData(11);
@ -46,8 +47,10 @@ void __getData() {
batteryAllowsContactorClosing = (uint16_t)dataLinkReceive.getReceivedData(16); batteryAllowsContactorClosing = (uint16_t)dataLinkReceive.getReceivedData(16);
batteryFault = false; batteryFault = false;
if (_bms_status == FAULT) if (_bms_status == FAULT) {
batteryFault = true; batteryFault = true;
set_event(EVENT_SERIAL_TRANSMITTER_FAILURE, 0);
}
} }
void updateData() { void updateData() {
@ -116,12 +119,12 @@ void manageSerialLinkReceiver() {
if (minutesLost < 4) { if (minutesLost < 4) {
max_target_charge_power = (lastGoodMaxCharge * (4 - minutesLost)) / 4; max_target_charge_power = (lastGoodMaxCharge * (4 - minutesLost)) / 4;
max_target_discharge_power = (lastGoodMaxDischarge * (4 - minutesLost)) / 4; max_target_discharge_power = (lastGoodMaxDischarge * (4 - minutesLost)) / 4;
set_event(EVENT_SERIAL_RX_WARNING, minutesLost);
} else { } else {
// Times Up - // Times Up -
max_target_charge_power = 0; max_target_charge_power = 0;
max_target_discharge_power = 0; max_target_discharge_power = 0;
bms_status = 4; //Fault state set_event(EVENT_SERIAL_RX_FAILURE, uint8_t(min(minutesLost, 255uL)));
LEDcolor = RED;
//----- Throw Error //----- Throw Error
} }
// report Lost data & Max charge / Discharge reductions // report Lost data & Max charge / Discharge reductions

View file

@ -30,7 +30,6 @@ extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 funct
extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t cell_max_voltage; //mV, 0-4350 extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350 extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint8_t LEDcolor; //Enum, 0-10
extern bool LFP_Chemistry; extern bool LFP_Chemistry;
extern uint16_t CANerror; extern uint16_t CANerror;

View file

@ -226,20 +226,14 @@ void update_values_tesla_model_3_battery() { //This function maps all the value
/* Value mapping is completed. Start to check all safeties */ /* Value mapping is completed. Start to check all safeties */
bms_status = ACTIVE; //Startout in active mode before checking if we have any faults
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!stillAliveCAN) { if (!stillAliveCAN) {
bms_status = FAULT; set_event(EVENT_CAN_RX_FAILURE, 0);
Serial.println("ERROR: No CAN communication detected for 60s. Shutting down battery control.");
set_event(EVENT_CAN_FAILURE, 0);
} else { } else {
stillAliveCAN--; stillAliveCAN--;
} }
if (hvil_status == 3) { //INTERNAL_OPEN_FAULT - Someone disconnected a high voltage cable while battery was in use if (hvil_status == 3) { //INTERNAL_OPEN_FAULT - Someone disconnected a high voltage cable while battery was in use
bms_status = FAULT;
Serial.println("ERROR: High voltage cable removed while battery running. Opening contactors!");
set_event(EVENT_INTERNAL_OPEN_FAULT, 0); set_event(EVENT_INTERNAL_OPEN_FAULT, 0);
} }
@ -259,8 +253,6 @@ void update_values_tesla_model_3_battery() { //This function maps all the value
if (battery_voltage > if (battery_voltage >
(ABSOLUTE_MAX_VOLTAGE - 100)) { // When pack voltage is close to max, and SOC% is still low, raise FAULT (ABSOLUTE_MAX_VOLTAGE - 100)) { // When pack voltage is close to max, and SOC% is still low, raise FAULT
if (SOC < 6500) { //When SOC is less than 65.00% when approaching max voltage if (SOC < 6500) { //When SOC is less than 65.00% when approaching max voltage
bms_status = FAULT;
Serial.println("ERROR: SOC% reported by battery not plausible. Restart battery!");
set_event(EVENT_SOC_PLAUSIBILITY_ERROR, SOC / 100); set_event(EVENT_SOC_PLAUSIBILITY_ERROR, SOC / 100);
} }
} }
@ -270,7 +262,6 @@ void update_values_tesla_model_3_battery() { //This function maps all the value
Serial.println("Warning: kWh remaining " + String(nominal_full_pack_energy) + Serial.println("Warning: kWh remaining " + String(nominal_full_pack_energy) +
" reported by battery not plausible. Battery needs cycling."); " reported by battery not plausible. Battery needs cycling.");
set_event(EVENT_KWH_PLAUSIBILITY_ERROR, nominal_full_pack_energy); set_event(EVENT_KWH_PLAUSIBILITY_ERROR, nominal_full_pack_energy);
LEDcolor = YELLOW;
} else if (nominal_full_pack_energy <= 1) { } else if (nominal_full_pack_energy <= 1) {
Serial.println("Info: kWh remaining battery is not reporting kWh remaining."); Serial.println("Info: kWh remaining battery is not reporting kWh remaining.");
set_event(EVENT_KWH_PLAUSIBILITY_ERROR, nominal_full_pack_energy); set_event(EVENT_KWH_PLAUSIBILITY_ERROR, nominal_full_pack_energy);
@ -278,34 +269,22 @@ void update_values_tesla_model_3_battery() { //This function maps all the value
if (LFP_Chemistry) { //LFP limits used for voltage safeties if (LFP_Chemistry) { //LFP limits used for voltage safeties
if (cell_max_v >= MAX_CELL_VOLTAGE_LFP) { if (cell_max_v >= MAX_CELL_VOLTAGE_LFP) {
bms_status = FAULT;
Serial.println("ERROR: CELL OVERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_OVER_VOLTAGE, 0); set_event(EVENT_CELL_OVER_VOLTAGE, 0);
} }
if (cell_min_v <= MIN_CELL_VOLTAGE_LFP) { if (cell_min_v <= MIN_CELL_VOLTAGE_LFP) {
bms_status = FAULT;
Serial.println("ERROR: CELL UNDERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_UNDER_VOLTAGE, 0); set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
} }
if (cell_deviation_mV > MAX_CELL_DEVIATION_LFP) { if (cell_deviation_mV > MAX_CELL_DEVIATION_LFP) {
LEDcolor = YELLOW;
Serial.println("ERROR: HIGH CELL DEVIATION!!! Inspect battery!");
set_event(EVENT_CELL_DEVIATION_HIGH, 0); set_event(EVENT_CELL_DEVIATION_HIGH, 0);
} }
} else { //NCA/NCM limits used } else { //NCA/NCM limits used
if (cell_max_v >= MAX_CELL_VOLTAGE_NCA_NCM) { if (cell_max_v >= MAX_CELL_VOLTAGE_NCA_NCM) {
bms_status = FAULT;
Serial.println("ERROR: CELL OVERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_OVER_VOLTAGE, 0); set_event(EVENT_CELL_OVER_VOLTAGE, 0);
} }
if (cell_min_v <= MIN_CELL_VOLTAGE_NCA_NCM) { if (cell_min_v <= MIN_CELL_VOLTAGE_NCA_NCM) {
bms_status = FAULT;
Serial.println("ERROR: CELL UNDERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!");
set_event(EVENT_CELL_UNDER_VOLTAGE, 0); set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
} }
if (cell_deviation_mV > MAX_CELL_DEVIATION_NCA_NCM) { if (cell_deviation_mV > MAX_CELL_DEVIATION_NCA_NCM) {
LEDcolor = YELLOW;
Serial.println("ERROR: HIGH CELL DEVIATION!!! Inspect battery!");
set_event(EVENT_CELL_DEVIATION_HIGH, 0); set_event(EVENT_CELL_DEVIATION_HIGH, 0);
} }
} }

View file

@ -21,7 +21,6 @@ extern uint16_t capacity_Wh; //Wh, 0-60000
extern uint16_t remaining_capacity_Wh; //Wh, 0-60000 extern uint16_t remaining_capacity_Wh; //Wh, 0-60000
extern uint16_t max_target_discharge_power; //W, 0-60000 extern uint16_t max_target_discharge_power; //W, 0-60000
extern uint16_t max_target_charge_power; //W, 0-60000 extern uint16_t max_target_charge_power; //W, 0-60000
extern uint8_t bms_status; //Enum, 0-5
extern uint8_t bms_char_dis_status; //Enum, 0-2 extern uint8_t bms_char_dis_status; //Enum, 0-2
extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530) extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530)
extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
@ -30,7 +29,6 @@ extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350 extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint16_t cellvoltages[120]; //mV 0-4350 per cell extern uint16_t cellvoltages[120]; //mV 0-4350 per cell
extern uint8_t nof_cellvoltages; // Total number of cell voltages, set by each battery. extern uint8_t nof_cellvoltages; // Total number of cell voltages, set by each battery.
extern uint8_t LEDcolor; //Enum, 0-10
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool LFP_Chemistry; extern bool LFP_Chemistry;

View file

@ -17,11 +17,7 @@ void print_units(char* header, int value, char* units) {
} }
void update_values_test_battery() { /* This function puts fake values onto the parameters sent towards the inverter */ void update_values_test_battery() { /* This function puts fake values onto the parameters sent towards the inverter */
bms_status = ACTIVE; //Always be in Active mode SOC = 5000; // 50.00%
LEDcolor = TEST_ALL_COLORS; // Cycle the LED thru all available colors
SOC = 5000; // 50.00%
StateOfHealth = 9900; // 99.00% StateOfHealth = 9900; // 99.00%

View file

@ -28,7 +28,6 @@ extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint16_t cellvoltages[120]; //mV 0-5000 per cell extern uint16_t cellvoltages[120]; //mV 0-5000 per cell
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false
extern uint8_t LEDcolor; //Enum, 0-10
void update_values_test_battery(); void update_values_test_battery();
void receive_can_test_battery(CAN_frame_t rx_frame); void receive_can_test_battery(CAN_frame_t rx_frame);

View file

@ -32,11 +32,11 @@
#define SD_CS_PIN 13 #define SD_CS_PIN 13
#define WS2812_PIN 4 #define WS2812_PIN 4
// LED definitions for the board // LED definitions for the board, in order of "priority", DONT CHANGE!
#define GREEN 0 #define GREEN 0
#define YELLOW 1 #define YELLOW 1
#define RED 2 #define BLUE 2
#define BLUE 3 #define RED 3
#define TEST_ALL_COLORS 10 #define TEST_ALL_COLORS 10
// Inverter definitions // Inverter definitions

View file

@ -67,7 +67,8 @@ static void publish_cell_voltages(void) {
} }
doc.clear(); // clear after sending autoconfig doc.clear(); // clear after sending autoconfig
} else { } else {
if (cellvoltages[0] == 0u) { // If cell voltages haven't been populated...
if (nof_cellvoltages == 0u) {
return; return;
} }

View file

@ -1,84 +1,170 @@
#include "events.h" #include "events.h"
#ifndef UNIT_TEST
#include <EEPROM.h>
#endif
#include "../../../USER_SETTINGS.h" #include "../../../USER_SETTINGS.h"
#include "../config.h" #include "../config.h"
#include "timer.h"
unsigned long previous_millis = 0; #define EE_MAGIC_HEADER_VALUE 0xAA55
uint32_t time_seconds = 0; #define EE_NOF_EVENT_ENTRIES 30
static uint8_t total_led_color = GREEN; #define EE_EVENT_ENTRY_SIZE sizeof(EVENT_LOG_ENTRY_TYPE)
static char event_message[256]; #define EE_WRITE_PERIOD_MINUTES 10
EVENTS_STRUCT_TYPE entries[EVENT_NOF_EVENTS];
/** EVENT LOG STRUCTURE
*
* The event log is stored in a simple header-block structure. The
* header contains a magic number to identify it as an event log,
* a head index and a tail index. The head index points to the last
* recorded event, the tail index points to the "oldest" event in the
* log. The event log is set up like a circular buffer, so we only
* store the set amount of events. The head continuously overwrites
* the oldest events, and both the head and tail indices wrap around
* to 0 at the end of the event log:
*
* [ HEADER ]
* [ MAGIC NUMBER ][ HEAD INDEX ][ TAIL INDEX ][ EVENT BLOCK 0 ][ EVENT BLOCK 1]...
* [ 2 bytes ][ 2 bytes ][ 2 bytes ][ 6 bytes ][ 6 bytes ]
*
* 1024 bytes are allocated to the event log in flash emulated EEPROM,
* giving room for (1024 - (2 + 2 + 2)) / 6 ~= 169 events
*
* For now, we store 30 to make it easier to handle initial debugging.
*/
#define EE_EVENT_LOG_START_ADDRESS 0
#define EE_EVENT_LOG_HEAD_INDEX_ADDRESS EE_EVENT_LOG_START_ADDRESS + 2
#define EE_EVENT_LOG_TAIL_INDEX_ADDRESS EE_EVENT_LOG_HEAD_INDEX_ADDRESS + 2
#define EE_EVENT_ENTRY_START_ADDRESS EE_EVENT_LOG_TAIL_INDEX_ADDRESS + 2
typedef struct {
EVENTS_ENUM_TYPE event;
uint32_t timestamp;
uint8_t data;
} EVENT_LOG_ENTRY_TYPE;
typedef struct {
EVENTS_STRUCT_TYPE entries[EVENT_NOF_EVENTS];
uint32_t time_seconds;
MyTimer second_timer;
MyTimer ee_timer;
EVENTS_LEVEL_TYPE level;
uint16_t event_log_head_index;
uint16_t event_log_tail_index;
} EVENT_TYPE;
/* Local variables */
static EVENT_TYPE events;
static const char* EVENTS_ENUM_TYPE_STRING[] = {EVENTS_ENUM_TYPE(GENERATE_STRING)};
static const char* EVENTS_LEVEL_TYPE_STRING[] = {EVENTS_LEVEL_TYPE(GENERATE_STRING)};
/* Local function prototypes */ /* Local function prototypes */
static void set_event_message(EVENTS_ENUM_TYPE event); static void update_event_time(void);
static void update_led_color(EVENTS_ENUM_TYPE event); static void set_event(EVENTS_ENUM_TYPE event, uint8_t data, bool latched);
static void update_event_level(void);
static void update_bms_status(void);
static void log_event(EVENTS_ENUM_TYPE event, uint8_t data);
static void print_event_log(void);
static void check_ee_write(void);
/* Exported functions */ /* Exported functions */
/* Main execution function, should handle various continuous functionality */
void run_event_handling(void) {
update_event_time();
run_sequence_on_target();
check_ee_write();
}
/* Initialization function */
void init_events(void) { void init_events(void) {
for (uint8_t i = 0; i < EVENT_NOF_EVENTS; i++) {
entries[i].timestamp = 0; EEPROM.begin(1024);
entries[i].data = 0;
entries[i].occurences = 0; uint16_t header = EEPROM.readUShort(EE_EVENT_LOG_START_ADDRESS);
entries[i].led_color = RED; // Most events are RED if (header != EE_MAGIC_HEADER_VALUE) {
EEPROM.writeUShort(EE_EVENT_LOG_START_ADDRESS, EE_MAGIC_HEADER_VALUE);
EEPROM.writeUShort(EE_EVENT_LOG_HEAD_INDEX_ADDRESS, 0);
EEPROM.writeUShort(EE_EVENT_LOG_TAIL_INDEX_ADDRESS, 0);
EEPROM.commit();
Serial.println("EEPROM wasn't ready");
} else {
events.event_log_head_index = EEPROM.readUShort(EE_EVENT_LOG_HEAD_INDEX_ADDRESS);
events.event_log_tail_index = EEPROM.readUShort(EE_EVENT_LOG_TAIL_INDEX_ADDRESS);
Serial.println("EEPROM was initialized for event logging");
Serial.println("head: " + String(events.event_log_head_index) + ", tail: " + String(events.event_log_tail_index));
print_event_log();
} }
// YELLOW events below for (uint16_t i = 0; i < EVENT_NOF_EVENTS; i++) {
entries[EVENT_12V_LOW].led_color = YELLOW; events.entries[i].data = 0;
entries[EVENT_CAN_WARNING].led_color = YELLOW; events.entries[i].timestamp = 0;
entries[EVENT_CELL_DEVIATION_HIGH].led_color = YELLOW; events.entries[i].occurences = 0;
entries[EVENT_KWH_PLAUSIBILITY_ERROR].led_color = YELLOW; events.entries[i].log = false;
}
events.entries[EVENT_CAN_RX_FAILURE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CAN_RX_WARNING].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CAN_TX_FAILURE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_WATER_INGRESS].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_12V_LOW].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_SOC_PLAUSIBILITY_ERROR].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_KWH_PLAUSIBILITY_ERROR].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_BATTERY_CHG_STOP_REQ].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_BATTERY_DISCHG_STOP_REQ].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_BATTERY_CHG_DISCHG_STOP_REQ].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_LOW_SOH].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_HVIL_FAILURE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_INTERNAL_OPEN_FAULT].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CELL_UNDER_VOLTAGE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CELL_OVER_VOLTAGE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CELL_DEVIATION_HIGH].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_UNKNOWN_EVENT_SET].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_OTA_UPDATE].level = EVENT_LEVEL_UPDATE;
events.entries[EVENT_DUMMY_INFO].level = EVENT_LEVEL_INFO;
events.entries[EVENT_DUMMY_DEBUG].level = EVENT_LEVEL_DEBUG;
events.entries[EVENT_DUMMY_WARNING].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_DUMMY_ERROR].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_SERIAL_RX_WARNING].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_SERIAL_RX_FAILURE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_SERIAL_TX_FAILURE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_SERIAL_TRANSMITTER_FAILURE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_DUMMY_INFO].log = true;
events.entries[EVENT_DUMMY_DEBUG].log = true;
events.entries[EVENT_DUMMY_WARNING].log = true;
events.entries[EVENT_DUMMY_ERROR].log = true;
events.second_timer.set_interval(1000);
events.ee_timer.set_interval(EE_WRITE_PERIOD_MINUTES * 60 * 1000); // Write to EEPROM every X minutes
} }
void set_event(EVENTS_ENUM_TYPE event, uint8_t data) { void set_event(EVENTS_ENUM_TYPE event, uint8_t data) {
#ifdef EVENTLOGGING set_event(event, data, false);
if (event >= EVENT_NOF_EVENTS) {
event = EVENT_UNKNOWN_EVENT_SET;
}
entries[event].timestamp = time_seconds;
entries[event].data = data;
++entries[event].occurences;
set_event_message(event);
#ifdef DEBUG_VIA_USB
Serial.println("Set event: " + String(get_event_enum_string(event)) + ". Has occured " +
String(entries[event].occurences) + " times");
#endif
#endif
} }
void update_event_timestamps(void) { void set_event_latched(EVENTS_ENUM_TYPE event, uint8_t data) {
unsigned long new_millis = millis(); set_event(event, data, true);
if (new_millis - previous_millis >= 1000) { }
time_seconds++;
previous_millis = new_millis; void clear_event(EVENTS_ENUM_TYPE event) {
if (events.entries[event].state == EVENT_STATE_ACTIVE) {
events.entries[event].state = EVENT_STATE_INACTIVE;
update_event_level();
update_bms_status();
} }
} }
/* Local functions */ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
static void update_led_color(EVENTS_ENUM_TYPE event) {
total_led_color = (total_led_color == RED) ? RED : entries[event].led_color;
}
const char* get_led_color_display_text(u_int8_t led_color) {
switch (led_color) {
case RED:
return "RED";
case YELLOW:
return "YELLOW";
case GREEN:
return "GREEN";
case BLUE:
return "BLUE";
default:
return "UNKNOWN";
}
}
const char* get_event_message(EVENTS_ENUM_TYPE event) {
switch (event) { switch (event) {
case EVENT_CAN_FAILURE: case EVENT_CAN_RX_FAILURE:
return "No CAN communication detected for 60s. Shutting down battery control."; return "No CAN communication detected for 60s. Shutting down battery control.";
case EVENT_CAN_WARNING: case EVENT_CAN_RX_WARNING:
return "ERROR: High amount of corrupted CAN messages detected. Check CAN wire shielding!"; return "ERROR: High amount of corrupted CAN messages detected. Check CAN wire shielding!";
case EVENT_CAN_TX_FAILURE:
return "ERROR: CAN messages failed to transmit, or no one on the bus to ACK the message!";
case EVENT_WATER_INGRESS: case EVENT_WATER_INGRESS:
return "Water leakage inside battery detected. Operation halted. Inspect battery!"; return "Water leakage inside battery detected. Operation halted. Inspect battery!";
case EVENT_12V_LOW: case EVENT_12V_LOW:
@ -109,22 +195,167 @@ const char* get_event_message(EVENTS_ENUM_TYPE event) {
return "ERROR: HIGH CELL DEVIATION!!! Inspect battery!"; return "ERROR: HIGH CELL DEVIATION!!! Inspect battery!";
case EVENT_UNKNOWN_EVENT_SET: case EVENT_UNKNOWN_EVENT_SET:
return "An unknown event was set! Review your code!"; return "An unknown event was set! Review your code!";
case EVENT_DUMMY: case EVENT_DUMMY_INFO:
return "The dummy event was set!"; // Don't change this event message! return "The dummy info event was set!"; // Don't change this event message!
case EVENT_DUMMY_DEBUG:
return "The dummy debug event was set!"; // Don't change this event message!
case EVENT_DUMMY_WARNING:
return "The dummy warning event was set!"; // Don't change this event message!
case EVENT_DUMMY_ERROR:
return "The dummy error event was set!"; // Don't change this event message!
case EVENT_SERIAL_RX_WARNING:
return "Error in serial function: No data received for some time, see data for minutes";
case EVENT_SERIAL_RX_FAILURE:
return "Error in serial function: No data for a long time!";
case EVENT_SERIAL_TX_FAILURE:
return "Error in serial function: No ACK from receiver!";
case EVENT_SERIAL_TRANSMITTER_FAILURE:
return "Error in serial function: Some ERROR level fault in transmitter, received by receiver";
case EVENT_OTA_UPDATE:
return "OTA update started!";
default: default:
return ""; return "";
} }
} }
const char* get_event_enum_string(EVENTS_ENUM_TYPE event) { const char* get_event_enum_string(EVENTS_ENUM_TYPE event) {
const char* fullString = EVENTS_ENUM_TYPE_STRING[event]; // Return the event name but skip "EVENT_" that should always be first
if (strncmp(fullString, "EVENT_", 6) == 0) { return EVENTS_ENUM_TYPE_STRING[event] + 6;
return fullString + 6; // Skip the first 6 characters
}
return fullString;
} }
static void set_event_message(EVENTS_ENUM_TYPE event) { const char* get_event_level_string(EVENTS_ENUM_TYPE event) {
const char* message = get_event_message(event); // Return the event level but skip "EVENT_LEVEL_" that should always be first
snprintf(event_message, sizeof(event_message), "%s", message); return EVENTS_LEVEL_TYPE_STRING[events.entries[event].level] + 12;
}
const EVENTS_STRUCT_TYPE* get_event_pointer(EVENTS_ENUM_TYPE event) {
return &events.entries[event];
}
EVENTS_LEVEL_TYPE get_event_level(void) {
return events.level;
}
/* Local functions */
static void set_event(EVENTS_ENUM_TYPE event, uint8_t data, bool latched) {
// Just some defensive stuff if someone sets an unknown event
if (event >= EVENT_NOF_EVENTS) {
event = EVENT_UNKNOWN_EVENT_SET;
}
// If the event is already set, no reason to continue
if ((events.entries[event].state != EVENT_STATE_ACTIVE) &&
(events.entries[event].state != EVENT_STATE_ACTIVE_LATCHED)) {
events.entries[event].occurences++;
if (events.entries[event].log) {
log_event(event, data);
}
}
// We should set the event, update event info
events.entries[event].timestamp = events.time_seconds;
events.entries[event].data = data;
// Check if the event is latching
events.entries[event].state = latched ? EVENT_STATE_ACTIVE_LATCHED : EVENT_STATE_ACTIVE;
update_event_level();
update_bms_status();
#ifdef DEBUG_VIA_USB
Serial.println(get_event_message_string(event));
#endif
}
static void update_bms_status(void) {
switch (events.level) {
case EVENT_LEVEL_INFO:
case EVENT_LEVEL_WARNING:
case EVENT_LEVEL_DEBUG:
bms_status = ACTIVE;
break;
case EVENT_LEVEL_UPDATE:
bms_status = UPDATING;
break;
case EVENT_LEVEL_ERROR:
bms_status = FAULT;
break;
default:
break;
}
}
static void update_event_level(void) {
events.level = EVENT_LEVEL_INFO;
for (uint8_t i = 0u; i < EVENT_NOF_EVENTS; i++) {
if ((events.entries[i].state == EVENT_STATE_ACTIVE) || (events.entries[i].state == EVENT_STATE_ACTIVE_LATCHED)) {
events.level = max(events.entries[i].level, events.level);
}
}
}
static void update_event_time(void) {
if (events.second_timer.elapsed() == true) {
events.time_seconds++;
}
}
static void log_event(EVENTS_ENUM_TYPE event, uint8_t data) {
// Update head with wrap to 0
if (++events.event_log_head_index == EE_NOF_EVENT_ENTRIES) {
events.event_log_head_index = 0;
}
// If the head now points to the tail, move the tail, with wrap to 0
if (events.event_log_head_index == events.event_log_tail_index) {
if (++events.event_log_tail_index == EE_NOF_EVENT_ENTRIES) {
events.event_log_tail_index = 0;
}
}
// The head now holds the index to the oldest event, the one we want to overwrite,
// so calculate the absolute address
int entry_address = EE_EVENT_ENTRY_START_ADDRESS + EE_EVENT_ENTRY_SIZE * events.event_log_head_index;
// Prepare an event block to write
EVENT_LOG_ENTRY_TYPE entry = {.event = event, .timestamp = events.time_seconds, .data = data};
// Put the event in (what I guess is) the RAM EEPROM mirror, or write buffer
EEPROM.put(entry_address, entry);
// Store the new indices
EEPROM.writeUShort(EE_EVENT_LOG_HEAD_INDEX_ADDRESS, events.event_log_head_index);
EEPROM.writeUShort(EE_EVENT_LOG_TAIL_INDEX_ADDRESS, events.event_log_tail_index);
//Serial.println("Wrote event " + String(event) + " to " + String(entry_address));
//Serial.println("head: " + String(events.event_log_head_index) + ", tail: " + String(events.event_log_tail_index));
}
static void print_event_log(void) {
// If the head actually points to the tail, the log is probably blank
if (events.event_log_head_index == events.event_log_tail_index) {
Serial.println("No events in log");
return;
}
EVENT_LOG_ENTRY_TYPE entry;
for (int i = 0; i < EE_NOF_EVENT_ENTRIES; i++) {
// Start at the oldest event, work through the log all the way the the head
int index = ((events.event_log_tail_index + i) % EE_NOF_EVENT_ENTRIES);
int address = EE_EVENT_ENTRY_START_ADDRESS + EE_EVENT_ENTRY_SIZE * index;
EEPROM.get(address, entry);
Serial.println("Event: " + String(get_event_enum_string(entry.event)) + ", data: " + String(entry.data) +
", time: " + String(entry.timestamp));
if (index == events.event_log_head_index) {
break;
}
}
}
static void check_ee_write(void) {
// Only actually write to flash emulated EEPROM every EE_WRITE_PERIOD_MINUTES minutes
if (events.ee_timer.elapsed()) {
EEPROM.commit();
}
} }

View file

@ -4,13 +4,26 @@
#ifndef UNIT_TEST #ifndef UNIT_TEST
#include <Arduino.h> #include <Arduino.h>
extern unsigned long previous_millis;
extern uint32_t time_seconds;
#endif #endif
// #define INCLUDE_EVENTS_TEST // Enable to run an event test loop, see events_test_on_target.cpp
#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,
/** EVENT ENUMERATION
*
* Do not change the order!
* When adding events, add them RIGHT BEFORE the EVENT_NOF_EVENTS enum.
* In addition, the event name must start with "EVENT_"
*
* After adding an event, assign the proper event level in events.cpp:init_events()
*/
#define EVENTS_ENUM_TYPE(XX) \ #define EVENTS_ENUM_TYPE(XX) \
XX(EVENT_CAN_FAILURE) \ XX(EVENT_CAN_RX_FAILURE) \
XX(EVENT_CAN_WARNING) \ XX(EVENT_CAN_RX_WARNING) \
XX(EVENT_CAN_TX_FAILURE) \
XX(EVENT_WATER_INGRESS) \ XX(EVENT_WATER_INGRESS) \
XX(EVENT_12V_LOW) \ XX(EVENT_12V_LOW) \
XX(EVENT_SOC_PLAUSIBILITY_ERROR) \ XX(EVENT_SOC_PLAUSIBILITY_ERROR) \
@ -25,31 +38,63 @@ extern uint32_t time_seconds;
XX(EVENT_CELL_OVER_VOLTAGE) \ XX(EVENT_CELL_OVER_VOLTAGE) \
XX(EVENT_CELL_DEVIATION_HIGH) \ XX(EVENT_CELL_DEVIATION_HIGH) \
XX(EVENT_UNKNOWN_EVENT_SET) \ XX(EVENT_UNKNOWN_EVENT_SET) \
XX(EVENT_DUMMY) \ XX(EVENT_OTA_UPDATE) \
XX(EVENT_DUMMY_INFO) \
XX(EVENT_DUMMY_DEBUG) \
XX(EVENT_DUMMY_WARNING) \
XX(EVENT_DUMMY_ERROR) \
XX(EVENT_SERIAL_RX_WARNING) \
XX(EVENT_SERIAL_RX_FAILURE) \
XX(EVENT_SERIAL_TX_FAILURE) \
XX(EVENT_SERIAL_TRANSMITTER_FAILURE) \
XX(EVENT_NOF_EVENTS) XX(EVENT_NOF_EVENTS)
#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,
typedef enum { EVENTS_ENUM_TYPE(GENERATE_ENUM) } EVENTS_ENUM_TYPE; typedef enum { EVENTS_ENUM_TYPE(GENERATE_ENUM) } EVENTS_ENUM_TYPE;
static const char* EVENTS_ENUM_TYPE_STRING[] = {EVENTS_ENUM_TYPE(GENERATE_STRING)}; /* Event type enumeration, keep in order of priority! */
#define EVENTS_LEVEL_TYPE(XX) \
XX(EVENT_LEVEL_INFO) \
XX(EVENT_LEVEL_DEBUG) \
XX(EVENT_LEVEL_WARNING) \
XX(EVENT_LEVEL_ERROR) \
XX(EVENT_LEVEL_UPDATE)
typedef enum { EVENTS_LEVEL_TYPE(GENERATE_ENUM) } EVENTS_LEVEL_TYPE;
typedef enum {
EVENT_STATE_PENDING = 0,
EVENT_STATE_INACTIVE,
EVENT_STATE_ACTIVE,
EVENT_STATE_ACTIVE_LATCHED
} EVENTS_STATE_TYPE;
typedef struct {
uint32_t timestamp; // Time in seconds since startup when the event occurred
uint8_t data; // Custom data passed when setting the event, for example cell number for under voltage
uint8_t occurences; // Number of occurrences since startup
EVENTS_LEVEL_TYPE level; // Event level, i.e. ERROR/WARNING...
EVENTS_STATE_TYPE state; // Event state, i.e. ACTIVE/INACTIVE...
bool log;
} EVENTS_STRUCT_TYPE;
const char* get_event_enum_string(EVENTS_ENUM_TYPE event); const char* get_event_enum_string(EVENTS_ENUM_TYPE event);
const char* get_event_message_string(EVENTS_ENUM_TYPE event);
const char* get_event_level_string(EVENTS_ENUM_TYPE event);
const char* get_event_type(EVENTS_ENUM_TYPE event);
const char* get_event_message(EVENTS_ENUM_TYPE event); EVENTS_LEVEL_TYPE get_event_level(void);
const char* get_led_color_display_text(u_int8_t led_color);
void init_events(void); void init_events(void);
void set_event_latched(EVENTS_ENUM_TYPE event, uint8_t data);
void set_event(EVENTS_ENUM_TYPE event, uint8_t data); void set_event(EVENTS_ENUM_TYPE event, uint8_t data);
void update_event_timestamps(void); void clear_event(EVENTS_ENUM_TYPE event);
typedef struct {
uint32_t timestamp; // Time in seconds since startup when the event occurred const EVENTS_STRUCT_TYPE* get_event_pointer(EVENTS_ENUM_TYPE event);
uint8_t data; // Custom data passed when setting the event, for example cell number for under voltage
uint8_t occurences; // Number of occurrences since startup void run_event_handling(void);
uint8_t led_color; // Weirdly indented comment
} EVENTS_STRUCT_TYPE; void run_sequence_on_target(void);
extern EVENTS_STRUCT_TYPE entries[EVENT_NOF_EVENTS];
extern uint8_t bms_status; //Enum, 0-5
#endif // __MYTIMER_H__ #endif // __MYTIMER_H__

View file

@ -0,0 +1,142 @@
#include "events.h"
#include "timer.h"
typedef enum {
ETOT_INIT,
ETOT_FIRST_WAIT,
ETOT_INFO,
ETOT_INFO_CLEAR,
ETOT_DEBUG,
ETOT_DEBUG_CLEAR,
ETOT_WARNING,
ETOT_WARNING_CLEAR,
ETOT_ERROR,
ETOT_ERROR_CLEAR,
ETOT_ERROR_LATCHED,
ETOT_DONE
} ETOT_TYPE;
MyTimer timer(5000);
ETOT_TYPE events_test_state = ETOT_INIT;
void run_sequence_on_target(void) {
#ifdef INCLUDE_EVENTS_TEST
switch (events_test_state) {
case ETOT_INIT:
timer.set_interval(10000);
events_test_state = ETOT_FIRST_WAIT;
Serial.println("Events test: initialized");
Serial.print("bms_status: ");
Serial.println(bms_status);
break;
case ETOT_FIRST_WAIT:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_INFO;
set_event(EVENT_DUMMY_INFO, 123);
set_event(EVENT_DUMMY_INFO, 234); // 234 should show, occurrence 1
Serial.println("Events test: info event set, data: 234");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_INFO:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_INFO);
events_test_state = ETOT_INFO_CLEAR;
Serial.println("Events test : info event cleared");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_INFO_CLEAR:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_DEBUG;
set_event(EVENT_DUMMY_DEBUG, 111);
set_event(EVENT_DUMMY_DEBUG, 222); // 222 should show, occurrence 1
Serial.println("Events test : debug event set, data: 222");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_DEBUG:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_DEBUG);
events_test_state = ETOT_DEBUG_CLEAR;
Serial.println("Events test : info event cleared");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_DEBUG_CLEAR:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_WARNING;
set_event(EVENT_DUMMY_WARNING, 234);
set_event(EVENT_DUMMY_WARNING, 121); // 121 should show, occurrence 1
Serial.println("Events test : warning event set, data: 121");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_WARNING:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_WARNING);
events_test_state = ETOT_WARNING_CLEAR;
Serial.println("Events test : warning event cleared");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_WARNING_CLEAR:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_ERROR;
set_event(EVENT_DUMMY_ERROR, 221);
set_event(EVENT_DUMMY_ERROR, 133); // 133 should show, occurrence 1
Serial.println("Events test : error event set, data: 133");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_ERROR:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_ERROR);
events_test_state = ETOT_ERROR_CLEAR;
Serial.println("Events test : error event cleared");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_ERROR_CLEAR:
if (timer.elapsed()) {
timer.set_interval(8000);
events_test_state = ETOT_ERROR_LATCHED;
set_event_latched(EVENT_DUMMY_ERROR, 221);
set_event_latched(EVENT_DUMMY_ERROR, 133); // 133 should show, occurrence 1
Serial.println("Events test : latched error event set, data: 133");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_ERROR_LATCHED:
if (timer.elapsed()) {
timer.set_interval(8000);
clear_event(EVENT_DUMMY_ERROR);
events_test_state = ETOT_DONE;
Serial.println("Events test : latched error event cleared?");
Serial.print("bms_status: ");
Serial.println(bms_status);
}
break;
case ETOT_DONE:
default:
break;
}
#endif
}

View file

@ -4,7 +4,7 @@ MyTimer::MyTimer(unsigned long interval) : interval(interval) {
previous_millis = millis(); previous_millis = millis();
} }
bool MyTimer::elapsed() { bool MyTimer::elapsed(void) {
unsigned long current_millis = millis(); unsigned long current_millis = millis();
if (current_millis - previous_millis >= interval) { if (current_millis - previous_millis >= interval) {
previous_millis = current_millis; previous_millis = current_millis;
@ -12,3 +12,12 @@ bool MyTimer::elapsed() {
} }
return false; return false;
} }
void MyTimer::reset(void) {
previous_millis = millis();
}
void MyTimer::set_interval(unsigned long interval) {
this->interval = interval;
reset();
}

View file

@ -7,14 +7,20 @@
class MyTimer { class MyTimer {
public: public:
/** Default constructor */
MyTimer() : interval(0), previous_millis(0) {}
/** interval in ms */ /** interval in ms */
MyTimer(unsigned long interval); MyTimer(unsigned long interval);
/** Returns true and resets the timer if it has elapsed */ /** Returns true and resets the timer if it has elapsed */
bool elapsed(); bool elapsed(void);
void reset(void);
void set_interval(unsigned long interval);
private:
unsigned long interval; unsigned long interval;
unsigned long previous_millis; unsigned long previous_millis;
private:
}; };
#endif // __MYTIMER_H__ #endif // __MYTIMER_H__

View file

@ -24,10 +24,6 @@ This section lists a number of features that can be implemented as part of the w
- TODO: list all available ssids: scan WiFi Networks https://randomnerdtutorials.com/esp32-useful-wi-fi-functions-arduino/ - TODO: list all available ssids: scan WiFi Networks https://randomnerdtutorials.com/esp32-useful-wi-fi-functions-arduino/
- TODO: add option to add/change ssid and password and save, connect to the new ssid (Option: save ssid and password using Preferences.h library https://randomnerdtutorials.com/esp32-save-data-permanently-preferences/) - TODO: add option to add/change ssid and password and save, connect to the new ssid (Option: save ssid and password using Preferences.h library https://randomnerdtutorials.com/esp32-save-data-permanently-preferences/)
- TODO: display WiFi connection strength (https://randomnerdtutorials.com/esp32-useful-wi-fi-functions-arduino/)
- TODO: display CAN state (indicate if there is a communication error)
- TODO: display battery errors in battery diagnosis tab
- TODO: display inverter errors in battery diagnosis tab
- TODO: add functionality to turn WiFi AP off - TODO: add functionality to turn WiFi AP off
- TODO: fix IP address on home network (https://randomnerdtutorials.com/esp32-static-fixed-ip-address-arduino-ide/) - TODO: fix IP address on home network (https://randomnerdtutorials.com/esp32-static-fixed-ip-address-arduino-ide/)
- TODO: set hostname (https://randomnerdtutorials.com/esp32-set-custom-hostname-arduino/) - TODO: set hostname (https://randomnerdtutorials.com/esp32-set-custom-hostname-arduino/)

View file

@ -0,0 +1,56 @@
#include "cellmonitor_html.h"
#include <Arduino.h>
String cellmonitor_processor(const String& var) {
if (var == "PLACEHOLDER") {
String content = "";
// Page format
content += "<style>";
content += "body { background-color: black; color: white; }";
content += ".container { display: flex; flex-wrap: wrap; justify-content: space-around; }";
content += ".cell { width: 48%; margin: 1%; padding: 10px; border: 1px solid white; text-align: center; }";
content += ".low-voltage { color: red; }"; // Style for low voltage text
content += ".voltage-values { margin-bottom: 10px; }"; // Style for voltage values section
content += "</style>";
// Start a new block with a specific background color
content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px; border-radius: 50px'>";
// Display max, min, and deviation voltage values
content += "<div class='voltage-values'>";
content += "Max Voltage: " + String(cell_max_voltage) + " mV<br>";
content += "Min Voltage: " + String(cell_min_voltage) + " mV<br>";
int deviation = cell_max_voltage - cell_min_voltage;
content += "Voltage Deviation: " + String(deviation) + " mV";
content += "</div>";
// Visualize the populated cells in forward order using flexbox with conditional text color
content += "<div class='container'>";
for (int i = 0; i < 120; ++i) {
// Skip empty values
if (cellvoltages[i] == 0) {
continue;
}
String cellContent = "Cell " + String(i + 1) + "<br>" + String(cellvoltages[i]) + " mV";
// Check if the cell voltage is below 3000, apply red color
if (cellvoltages[i] < 3000) {
cellContent = "<span class='low-voltage'>" + cellContent + "</span>";
}
content += "<div class='cell'>" + cellContent + "</div>";
}
content += "</div>";
// Close the block
content += "</div>";
content += "<button onclick='goToMainPage()'>Back to main page</button>";
content += "<script>";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
return content;
}
return String();
}

View file

@ -0,0 +1,20 @@
#ifndef CELLMONITOR_H
#define CELLMONITOR_H
#include <Arduino.h>
#include <stdint.h>
extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint16_t cellvoltages[120]; //mV 0-4350 per cell
/**
* @brief Replaces placeholder with content section in web page
*
* @param[in] var
*
* @return String
*/
String cellmonitor_processor(const String& var);
#endif

View file

@ -0,0 +1,80 @@
#include "events_html.h"
#include <Arduino.h>
#include "../utils/events.h"
const char EVENTS_HTML_START[] = R"=====(
<style>body{background-color:#000;color:#fff}.event-log{display:flex;flex-direction:column}.event{display:flex;flex-wrap:wrap;border:1px solid #fff;padding:10px}.event>div{flex:1;min-width:100px;max-width:90%;word-break:break-word}</style><div style="background-color:#303e47;padding:10px;margin-bottom:10px;border-radius:25px"><div class="event-log"><div class="event" style="background-color:#1e2c33;font-weight:700"><div>Event Type</div><div>Severity</div><div>Last Event</div><div>Count</div><div>Data</div><div>Message</div></div>
)=====";
const char EVENTS_HTML_END[] = R"=====(
</div></div>
<button onclick='goToMainPage()'>Back to main page</button>
<style>.event:nth-child(even){background-color:#455a64}.event:nth-child(odd){background-color:#394b52}</style>
<script>function displayEventLog(){document.querySelector(".event-log");var i=(new Date).getTime()/1e3;document.querySelectorAll(".event").forEach(function(e){var n=e.querySelector(".last-event-seconds-ago"),t=e.querySelector(".timestamp");if(n&&t){var o=parseInt(n.innerText,10),a=parseFloat(t.innerText),r=new Date(1e3*(i-a+o)).toLocaleString();n.innerText=r}})}function goToMainPage(){window.location.href="/"}window.onload=function(){displayEventLog()}</script>
)=====";
String events_processor(const String& var) {
if (var == "PLACEHOLDER") {
String content = "";
content.reserve(5000);
// Page format
content.concat(FPSTR(EVENTS_HTML_START));
const EVENTS_STRUCT_TYPE* event_pointer;
for (int i = 0; i < EVENT_NOF_EVENTS; i++) {
event_pointer = get_event_pointer((EVENTS_ENUM_TYPE)i);
EVENTS_ENUM_TYPE event_handle = static_cast<EVENTS_ENUM_TYPE>(i);
Serial.println("Event: " + String(get_event_enum_string(event_handle)) +
" count: " + String(event_pointer->occurences) + " seconds: " + String(event_pointer->timestamp) +
" data: " + String(event_pointer->data) +
" level: " + String(get_event_level_string(event_handle)));
if (event_pointer->occurences > 0) {
content.concat("<div class='event'>");
content.concat("<div>" + String(get_event_enum_string(event_handle)) + "</div>");
content.concat("<div>" + String(get_event_level_string(event_handle)) + "</div>");
content.concat("<div class='last-event-seconds-ago'>" + String(event_pointer->timestamp) + "</div>");
content.concat("<div>" + String(event_pointer->occurences) + "</div>");
content.concat("<div>" + String(event_pointer->data) + "</div>");
content.concat("<div>" + String(get_event_message_string(event_handle)) + "</div>");
content.concat("<div class='timestamp' style='display:none;'>" + String(millis() / 1000) + "</div>");
content.concat("</div>"); // End of event row
}
}
content.concat(FPSTR(EVENTS_HTML_END));
return content;
}
return String();
}
/* Script for displaying event log before it gets minified
<script>
function displayEventLog() {
var eventLogElement = document.querySelector('.event-log');
// Get the current time on the client side
var currentTime = new Date().getTime() / 1000; // Convert milliseconds to seconds
// Loop through the events and update the "Last Event" column
var events = document.querySelectorAll('.event');
events.forEach(function(event) {
var secondsAgoElement = event.querySelector('.last-event-seconds-ago');
var timestampElement = event.querySelector('.timestamp');
if (secondsAgoElement && timestampElement) {
var secondsAgo = parseInt(secondsAgoElement.innerText, 10);
var uptimeTimestamp = parseFloat(timestampElement.innerText); // Parse as float to handle seconds with decimal parts
// Calculate the actual system time based on the client-side current time
var actualTime = new Date((currentTime - uptimeTimestamp + secondsAgo) * 1000);
// Format the date and time
var formattedTime = actualTime.toLocaleString();
// Update the "Last Event" column with the formatted time
secondsAgoElement.innerText = formattedTime;
}
});
}
// Call the displayEventLog function when the page is loaded
window.onload = function() {
displayEventLog();
};
function goToMainPage() {
window.location.href = '/';
}
</script>
*/

View file

@ -0,0 +1,15 @@
#ifndef EVENTS_H
#define EVENTS_H
#include <Arduino.h>
/**
* @brief Replaces placeholder with content section in web page
*
* @param[in] var
*
* @return String
*/
String events_processor(const String& var);
#endif

View file

@ -0,0 +1,29 @@
const char index_html[] = R"rawliteral(
<!DOCTYPE HTML><html><head><title>Battery Emulator</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>html{font-family:Arial;display:inline-block;text-align:center}h2{font-size:3rem}p{font-size:3rem}body{max-width:800px;margin:0 auto;padding-bottom:25px}.switch{position:relative;display:inline-block;width:120px;height:68px}.switch input{display:none}.slider{position:absolute;top:0;left:0;right:0;bottom:0;background-color:#ccc;border-radius:6px}.slider:before{position:absolute;content:"";height:52px;width:52px;left:8px;bottom:8px;background-color:#fff;-webkit-transition:.4s;transition:.4s;border-radius:3px}input:checked+.slider{background-color:#b30000}input:checked+.slider:before{-webkit-transform:translateX(52px);-ms-transform:translateX(52px);transform:translateX(52px)}</style></head><body><h2>Battery Emulator</h2>%PLACEHOLDER%</body></html>
)rawliteral";
/* The above code is minified to increase performance. Here is the full HTML function:
<!DOCTYPE HTML><html>
<head>
<title>Battery Emulator</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html {font-family: Arial; display: inline-block; text-align: center;}
h2 {font-size: 3.0rem;}
p {font-size: 3.0rem;}
body {max-width: 800px; margin:0px auto; padding-bottom: 25px;}
.switch {position: relative; display: inline-block; width: 120px; height: 68px}
.switch input {display: none}
.slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 6px}
.slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 3px}
input:checked+.slider {background-color: #b30000}
input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
</style>
</head>
<body>
<h2>Battery Emulator</h2>
%PLACEHOLDER%
</script>
</body>
</html>
*/

View file

@ -0,0 +1,238 @@
#include "settings_html.h"
#include <Arduino.h>
String settings_processor(const String& var) {
if (var == "PLACEHOLDER") {
String content = "";
//Page format
content += "<style>";
content += "body { background-color: black; color: white; }";
content += "</style>";
// Start a new block with a specific background color
content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px;border-radius: 50px'>";
// Show current settings with edit buttons and input fields
content += "<h4 style='color: white;'>Battery capacity: <span id='BATTERY_WH_MAX'>" + String(BATTERY_WH_MAX) +
" Wh </span> <button onclick='editWh()'>Edit</button></h4>";
content += "<h4 style='color: white;'>SOC max percentage: " + String(MAXPERCENTAGE / 10.0, 1) +
" </span> <button onclick='editSocMax()'>Edit</button></h4>";
content += "<h4 style='color: white;'>SOC min percentage: " + String(MINPERCENTAGE / 10.0, 1) +
" </span> <button onclick='editSocMin()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Max charge speed: " + String(MAXCHARGEAMP / 10.0, 1) +
" A </span> <button onclick='editMaxChargeA()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Max discharge speed: " + String(MAXDISCHARGEAMP / 10.0, 1) +
" A </span> <button onclick='editMaxDischargeA()'>Edit</button></h4>";
// Close the block
content += "</div>";
#ifdef TEST_FAKE_BATTERY
// Start a new block with blue background color
content += "<div style='background-color: #2E37AD; padding: 10px; margin-bottom: 10px;border-radius: 50px'>";
float voltageFloat = static_cast<float>(battery_voltage) / 10.0; // Convert to float and divide by 10
content += "<h4 style='color: white;'>Fake battery voltage: " + String(voltageFloat, 1) +
" V </span> <button onclick='editFakeBatteryVoltage()'>Edit</button></h4>";
// Close the block
content += "</div>";
#endif
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
// Start a new block with orange background color
content += "<div style='background-color: #FF6E00; padding: 10px; margin-bottom: 10px;border-radius: 50px'>";
content += "<h4 style='color: white;'>Charger HVDC Enabled: ";
if (charger_HV_enabled) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " <button onclick='editChargerHVDCEnabled()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Charger Aux12VDC Enabled: ";
if (charger_aux12V_enabled) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " <button onclick='editChargerAux12vEnabled()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Charger Voltage Setpoint: " + String(charger_setpoint_HV_VDC, 1) +
" V </span> <button onclick='editChargerSetpointVDC()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Charger Current Setpoint: " + String(charger_setpoint_HV_IDC, 1) +
" A </span> <button onclick='editChargerSetpointIDC()'>Edit</button></h4>";
// Close the block
content += "</div>";
#endif
content += "<script>";
content += "function editWh() {";
content += "var value = prompt('How much energy the battery can store. Enter new Wh value (1-65000):');";
content += "if (value !== null) {";
content += " if (value >= 1 && value <= 65000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateBatterySize?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 1 and 65000.');";
content += " }";
content += "}";
content += "}";
content += "function editSocMax() {";
content +=
"var value = prompt('Inverter will see fully charged (100pct)SOC when this value is reached. Enter new maximum "
"SOC value that battery will charge to (50.0-100.0):');";
content += "if (value !== null) {";
content += " if (value >= 50 && value <= 100) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateSocMax?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 50.0 and 100.0');";
content += " }";
content += "}";
content += "}";
content += "function editSocMin() {";
content +=
"var value = prompt('Inverter will see completely discharged (0pct)SOC when this value is reached. Enter new "
"minimum SOC value that battery will discharge to (0-50.0):');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 50) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateSocMin?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 50.0');";
content += " }";
content += "}";
content += "}";
content += "function editMaxChargeA() {";
content +=
"var value = prompt('BYD CAN specific setting, some inverters needs to be artificially limited. Enter new "
"maximum charge current in A (0-1000.0):');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateMaxChargeA?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 1000.0');";
content += " }";
content += "}";
content += "}";
content += "function editMaxDischargeA() {";
content +=
"var value = prompt('BYD CAN specific setting, some inverters needs to be artificially limited. Enter new "
"maximum discharge current in A (0-1000.0):');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateMaxDischargeA?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 1000.0');";
content += " }";
content += "}";
content += "}";
#ifdef TEST_FAKE_BATTERY
content += "function editFakeBatteryVoltage() {";
content += " var value = prompt('Enter new fake battery voltage');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 5000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateFakeBatteryVoltage?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 1000');";
content += " }";
content += "}";
content += "}";
#endif
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
content += "function editChargerHVDCEnabled() {";
content += " var value = prompt('Enable or disable HV DC output. Enter 1 for enabled, 0 for disabled');";
content += " if (value !== null) {";
content += " if (value == 0 || value == 1) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargerHvEnabled?value=' + value, true);";
content += " xhr.send();";
content += " }";
content += " } else {";
content += " alert('Invalid value. Please enter 1 or 0');";
content += " }";
content += "}";
content += "function editChargerAux12vEnabled() {";
content +=
"var value = prompt('Enable or disable low voltage 12v auxiliary DC output. Enter 1 for enabled, 0 for "
"disabled');";
content += "if (value !== null) {";
content += " if (value == 0 || value == 1) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargerAux12vEnabled?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter 1 or 0');";
content += " }";
content += "}";
content += "}";
content += "function editChargerSetpointVDC() {";
content +=
"var value = prompt('Set charging voltage. Input will be validated against inverter and/or charger "
"configuration parameters, but use sensible values like 200 to 420.');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargeSetpointV?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 1000');";
content += " }";
content += "}";
content += "}";
content += "function editChargerSetpointIDC() {";
content +=
"var value = prompt('Set charging amperage. Input will be validated against inverter and/or charger "
"configuration parameters, but use sensible values like 6 to 48.');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargeSetpointA?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 100');";
content += " }";
content += "}";
content += "}";
content += "function editChargerSetpointEndI() {";
content +=
"var value = prompt('Set amperage that terminates charge as being sufficiently complete. Input will be "
"validated against inverter and/or charger configuration parameters, but use sensible values like 1-5.');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargeEndA?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 100');";
content += " }";
content += "}";
content += "}";
#endif
content += "</script>";
content += "<button onclick='goToMainPage()'>Back to main page</button>";
content += "<script>";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
return content;
}
return String();
}

View file

@ -0,0 +1,18 @@
#ifndef SETTINGS_H
#define SETTINGS_H
#include <Arduino.h>
#include "../../../USER_SETTINGS.h" // Needed for WiFi ssid and password
extern uint16_t battery_voltage; //V+1, 0-500.0 (0-5000)
/**
* @brief Replaces placeholder with content section in web page
*
* @param[in] var
*
* @return String
*/
String settings_processor(const String& var);
#endif

View file

@ -1,7 +1,6 @@
#include "webserver.h" #include "webserver.h"
#include <Preferences.h> #include <Preferences.h>
#include "../utils/events.h"
Preferences preferences3;
// Create AsyncWebServer object on port 80 // Create AsyncWebServer object on port 80
AsyncWebServer server(80); AsyncWebServer server(80);
@ -9,32 +8,10 @@ AsyncWebServer server(80);
// Measure OTA progress // Measure OTA progress
unsigned long ota_progress_millis = 0; unsigned long ota_progress_millis = 0;
const char index_html[] PROGMEM = R"rawliteral( #include "cellmonitor_html.h"
<!DOCTYPE HTML><html> #include "events_html.h"
<head> #include "index_html.cpp"
<title>Battery Emulator</title> #include "settings_html.h"
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="favicon.png">
<style>
html {font-family: Arial; display: inline-block; text-align: center;}
h2 {font-size: 3.0rem;}
p {font-size: 3.0rem;}
body {max-width: 600px; margin:0px auto; padding-bottom: 25px;}
.switch {position: relative; display: inline-block; width: 120px; height: 68px}
.switch input {display: none}
.slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 6px}
.slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 3px}
input:checked+.slider {background-color: #b30000}
input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
</style>
</head>
<body>
<h2>Battery Emulator</h2>
%PLACEHOLDER%
</script>
</body>
</html>
)rawliteral";
enum WifiState { enum WifiState {
INIT, //before connecting first time INIT, //before connecting first time
@ -62,6 +39,8 @@ void init_webserver() {
} }
init_WiFi_STA(ssid, password, wifi_channel); init_WiFi_STA(ssid, password, wifi_channel);
String content = index_html;
// Route for root / web page // Route for root / web page
server.on("/", HTTP_GET, server.on("/", HTTP_GET,
[](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, processor); }); [](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, processor); });
@ -75,10 +54,9 @@ void init_webserver() {
request->send_P(200, "text/html", index_html, cellmonitor_processor); request->send_P(200, "text/html", index_html, cellmonitor_processor);
}); });
#ifdef EVENTLOGGING // Route for going to event log web page
server.on("/events", HTTP_GET, server.on("/events", HTTP_GET,
[](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, events_processor); }); [](AsyncWebServerRequest* request) { request->send_P(200, "text/html", index_html, events_processor); });
#endif
// Route for editing Wh // Route for editing Wh
server.on("/updateBatterySize", HTTP_GET, [](AsyncWebServerRequest* request) { server.on("/updateBatterySize", HTTP_GET, [](AsyncWebServerRequest* request) {
@ -530,8 +508,10 @@ String processor(const String& var) {
content += "<h4>Cell min: " + String(cell_min_voltage) + " mV</h4>"; content += "<h4>Cell min: " + String(cell_min_voltage) + " mV</h4>";
content += "<h4>Temperature max: " + String(tempMaxFloat, 1) + " C</h4>"; content += "<h4>Temperature max: " + String(tempMaxFloat, 1) + " C</h4>";
content += "<h4>Temperature min: " + String(tempMinFloat, 1) + " C</h4>"; content += "<h4>Temperature min: " + String(tempMinFloat, 1) + " C</h4>";
if (bms_status == 3) { if (bms_status == ACTIVE) {
content += "<h4>BMS Status: OK </h4>"; content += "<h4>BMS Status: OK </h4>";
} else if (bms_status == UPDATING) {
content += "<h4>BMS Status: UPDATING </h4>";
} else { } else {
content += "<h4>BMS Status: FAULT </h4>"; content += "<h4>BMS Status: FAULT </h4>";
} }
@ -629,9 +609,7 @@ String processor(const String& var) {
content += "function goToUpdatePage() { window.location.href = '/update'; }"; content += "function goToUpdatePage() { window.location.href = '/update'; }";
content += "function goToCellmonitorPage() { window.location.href = '/cellmonitor'; }"; content += "function goToCellmonitorPage() { window.location.href = '/cellmonitor'; }";
content += "function goToSettingsPage() { window.location.href = '/settings'; }"; content += "function goToSettingsPage() { window.location.href = '/settings'; }";
#ifdef EVENTLOGGING
content += "function goToEventsPage() { window.location.href = '/events'; }"; content += "function goToEventsPage() { window.location.href = '/events'; }";
#endif
content += content +=
"function promptToReboot() { if (window.confirm('Are you sure you want to reboot the emulator? NOTE: If " "function promptToReboot() { if (window.confirm('Are you sure you want to reboot the emulator? NOTE: If "
"emulator is handling contactors, they will open during reboot!')) { " "emulator is handling contactors, they will open during reboot!')) { "
@ -653,359 +631,13 @@ String processor(const String& var) {
return String(); return String();
} }
String settings_processor(const String& var) {
if (var == "PLACEHOLDER") {
String content = "";
//Page format
content += "<style>";
content += "body { background-color: black; color: white; }";
content += "</style>";
// Start a new block with a specific background color
content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px;border-radius: 50px'>";
// Show current settings with edit buttons and input fields
content += "<h4 style='color: white;'>Battery capacity: <span id='BATTERY_WH_MAX'>" + String(BATTERY_WH_MAX) +
" Wh </span> <button onclick='editWh()'>Edit</button></h4>";
content += "<h4 style='color: white;'>SOC max percentage: " + String(MAXPERCENTAGE / 10.0, 1) +
" </span> <button onclick='editSocMax()'>Edit</button></h4>";
content += "<h4 style='color: white;'>SOC min percentage: " + String(MINPERCENTAGE / 10.0, 1) +
" </span> <button onclick='editSocMin()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Max charge speed: " + String(MAXCHARGEAMP / 10.0, 1) +
" A </span> <button onclick='editMaxChargeA()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Max discharge speed: " + String(MAXDISCHARGEAMP / 10.0, 1) +
" A </span> <button onclick='editMaxDischargeA()'>Edit</button></h4>";
// Close the block
content += "</div>";
#ifdef TEST_FAKE_BATTERY
// Start a new block with blue background color
content += "<div style='background-color: #2E37AD; padding: 10px; margin-bottom: 10px;border-radius: 50px'>";
float voltageFloat = static_cast<float>(battery_voltage) / 10.0; // Convert to float and divide by 10
content += "<h4 style='color: white;'>Fake battery voltage: " + String(voltageFloat, 1) +
" V </span> <button onclick='editFakeBatteryVoltage()'>Edit</button></h4>";
// Close the block
content += "</div>";
#endif
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
// Start a new block with orange background color
content += "<div style='background-color: #FF6E00; padding: 10px; margin-bottom: 10px;border-radius: 50px'>";
content += "<h4 style='color: white;'>Charger HVDC Enabled: ";
if (charger_HV_enabled) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " <button onclick='editChargerHVDCEnabled()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Charger Aux12VDC Enabled: ";
if (charger_aux12V_enabled) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " <button onclick='editChargerAux12vEnabled()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Charger Voltage Setpoint: " + String(charger_setpoint_HV_VDC, 1) +
" V </span> <button onclick='editChargerSetpointVDC()'>Edit</button></h4>";
content += "<h4 style='color: white;'>Charger Current Setpoint: " + String(charger_setpoint_HV_IDC, 1) +
" A </span> <button onclick='editChargerSetpointIDC()'>Edit</button></h4>";
// Close the block
content += "</div>";
#endif
content += "<script>";
content += "function editWh() {";
content += "var value = prompt('How much energy the battery can store. Enter new Wh value (1-65000):');";
content += "if (value !== null) {";
content += " if (value >= 1 && value <= 65000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateBatterySize?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 1 and 65000.');";
content += " }";
content += "}";
content += "}";
content += "function editSocMax() {";
content +=
"var value = prompt('Inverter will see fully charged (100pct)SOC when this value is reached. Enter new maximum "
"SOC value that battery will charge to (50.0-100.0):');";
content += "if (value !== null) {";
content += " if (value >= 50 && value <= 100) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateSocMax?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 50.0 and 100.0');";
content += " }";
content += "}";
content += "}";
content += "function editSocMin() {";
content +=
"var value = prompt('Inverter will see completely discharged (0pct)SOC when this value is reached. Enter new "
"minimum SOC value that battery will discharge to (0-50.0):');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 50) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateSocMin?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 50.0');";
content += " }";
content += "}";
content += "}";
content += "function editMaxChargeA() {";
content +=
"var value = prompt('BYD CAN specific setting, some inverters needs to be artificially limited. Enter new "
"maximum charge current in A (0-1000.0):');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateMaxChargeA?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 1000.0');";
content += " }";
content += "}";
content += "}";
content += "function editMaxDischargeA() {";
content +=
"var value = prompt('BYD CAN specific setting, some inverters needs to be artificially limited. Enter new "
"maximum discharge current in A (0-1000.0):');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateMaxDischargeA?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 1000.0');";
content += " }";
content += "}";
content += "}";
#ifdef TEST_FAKE_BATTERY
content += "function editFakeBatteryVoltage() {";
content += " var value = prompt('Enter new fake battery voltage');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 5000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateFakeBatteryVoltage?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 1000');";
content += " }";
content += "}";
content += "}";
#endif
#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
content += "function editChargerHVDCEnabled() {";
content += " var value = prompt('Enable or disable HV DC output. Enter 1 for enabled, 0 for disabled');";
content += " if (value !== null) {";
content += " if (value == 0 || value == 1) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargerHvEnabled?value=' + value, true);";
content += " xhr.send();";
content += " }";
content += " } else {";
content += " alert('Invalid value. Please enter 1 or 0');";
content += " }";
content += "}";
content += "function editChargerAux12vEnabled() {";
content +=
"var value = prompt('Enable or disable low voltage 12v auxiliary DC output. Enter 1 for enabled, 0 for "
"disabled');";
content += "if (value !== null) {";
content += " if (value == 0 || value == 1) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargerAux12vEnabled?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter 1 or 0');";
content += " }";
content += "}";
content += "}";
content += "function editChargerSetpointVDC() {";
content +=
"var value = prompt('Set charging voltage. Input will be validated against inverter and/or charger "
"configuration parameters, but use sensible values like 200 to 420.');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargeSetpointV?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 1000');";
content += " }";
content += "}";
content += "}";
content += "function editChargerSetpointIDC() {";
content +=
"var value = prompt('Set charging amperage. Input will be validated against inverter and/or charger "
"configuration parameters, but use sensible values like 6 to 48.');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargeSetpointA?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 100');";
content += " }";
content += "}";
content += "}";
content += "function editChargerSetpointEndI() {";
content +=
"var value = prompt('Set amperage that terminates charge as being sufficiently complete. Input will be "
"validated against inverter and/or charger configuration parameters, but use sensible values like 1-5.');";
content += "if (value !== null) {";
content += " if (value >= 0 && value <= 1000) {";
content += " var xhr = new XMLHttpRequest();";
content += " xhr.open('GET', '/updateChargeEndA?value=' + value, true);";
content += " xhr.send();";
content += " } else {";
content += " alert('Invalid value. Please enter a value between 0 and 100');";
content += " }";
content += "}";
content += "}";
#endif
content += "</script>";
content += "<button onclick='goToMainPage()'>Back to main page</button>";
content += "<script>";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
return content;
}
return String();
}
String cellmonitor_processor(const String& var) {
if (var == "PLACEHOLDER") {
String content = "";
// Page format
content += "<style>";
content += "body { background-color: black; color: white; }";
content += ".container { display: flex; flex-wrap: wrap; justify-content: space-around; }";
content += ".cell { width: 48%; margin: 1%; padding: 10px; border: 1px solid white; text-align: center; }";
content += ".low-voltage { color: red; }"; // Style for low voltage text
content += ".voltage-values { margin-bottom: 10px; }"; // Style for voltage values section
content += "</style>";
// Start a new block with a specific background color
content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px; border-radius: 50px'>";
// Display max, min, and deviation voltage values
content += "<div class='voltage-values'>";
content += "Max Voltage: " + String(cell_max_voltage) + " mV<br>";
content += "Min Voltage: " + String(cell_min_voltage) + " mV<br>";
int deviation = cell_max_voltage - cell_min_voltage;
content += "Voltage Deviation: " + String(deviation) + " mV";
content += "</div>";
// Visualize the populated cells in forward order using flexbox with conditional text color
content += "<div class='container'>";
for (int i = 0; i < 120; ++i) {
// Skip empty values
if (cellvoltages[i] == 0) {
continue;
}
String cellContent = "Cell " + String(i + 1) + "<br>" + String(cellvoltages[i]) + " mV";
// Check if the cell voltage is below 3000, apply red color
if (cellvoltages[i] < 3000) {
cellContent = "<span class='low-voltage'>" + cellContent + "</span>";
}
content += "<div class='cell'>" + cellContent + "</div>";
}
content += "</div>";
// Close the block
content += "</div>";
content += "<button onclick='goToMainPage()'>Back to main page</button>";
content += "<script>";
content += "function goToMainPage() { window.location.href = '/'; }";
content += "</script>";
return content;
}
return String();
}
#ifdef EVENTLOGGING
const char EVENTS_HTML_START[] PROGMEM = R"=====(
<style>
body { background-color: black; color: white; }
.event-log { display: flex; flex-direction: column; }
.event { display: flex; flex-wrap: wrap; border: 1px solid white; padding: 10px; }
.event > div { flex: 1; min-width: 100px; max-width: 90%; word-break: break-word; }
</style>
<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px;border-radius: 50px'>
<h4 style='color: white;'>Event log:</h4>
<div class="event-log">
<div class="event">
<div>Event Type</div><div>LED Color</div><div>Last Event (seconds ago)</div><div>Count</div><div>Data</div><div>Message</div>
</div>
)=====";
const char EVENTS_HTML_END[] PROGMEM = R"=====(
</div></div>
<button onclick='goToMainPage()'>Back to main page</button>
<script>
function goToMainPage() {
window.location.href = '/';
}
</script>
)=====";
String events_processor(const String& var) {
if (var == "PLACEHOLDER") {
String content = "";
content.reserve(5000);
// Page format
content.concat(FPSTR(EVENTS_HTML_START));
for (int i = 0; i < EVENT_NOF_EVENTS; i++) {
Serial.println("Event: " + String(get_event_enum_string(static_cast<EVENTS_ENUM_TYPE>(i))) +
" count: " + String(entries[i].occurences) + " seconds: " + String(entries[i].timestamp) +
" data: " + String(entries[i].data));
if (entries[i].occurences > 0) {
content.concat("<div class='event'>");
content.concat("<div>" + String(get_event_enum_string(static_cast<EVENTS_ENUM_TYPE>(i))) + "</div>");
content.concat("<div>" + String(get_led_color_display_text(entries[i].led_color)) + "</div>");
content.concat("<div>" + String((millis() / 1000) - entries[i].timestamp) + "</div>");
content.concat("<div>" + String(entries[i].occurences) + "</div>");
content.concat("<div>" + String(entries[i].data) + "</div>");
content.concat("<div>" + String(get_event_message(static_cast<EVENTS_ENUM_TYPE>(i))) + "</div>");
content.concat("</div>"); // End of event row
}
}
content.concat(FPSTR(EVENTS_HTML_END));
return content;
}
return String();
}
#endif
void onOTAStart() { void onOTAStart() {
// Log when OTA has started // Log when OTA has started
Serial.println("OTA update started!");
ESP32Can.CANStop(); ESP32Can.CANStop();
bms_status = UPDATING; //Inform inverter that we are updating set_event(EVENT_OTA_UPDATE, 0);
LEDcolor = BLUE;
} }
void onOTAProgress(size_t current, size_t final) { void onOTAProgress(size_t current, size_t final) {
bms_status = UPDATING; //Inform inverter that we are updating
LEDcolor = BLUE;
// Log every 1 second // Log every 1 second
if (millis() - ota_progress_millis > 1000) { if (millis() - ota_progress_millis > 1000) {
ota_progress_millis = millis(); ota_progress_millis = millis();
@ -1020,8 +652,7 @@ void onOTAEnd(bool success) {
} else { } else {
Serial.println("There was an error during OTA update!"); Serial.println("There was an error during OTA update!");
} }
bms_status = UPDATING; //Inform inverter that we are updating clear_event(EVENT_OTA_UPDATE);
LEDcolor = BLUE;
} }
template <typename T> // This function makes power values appear as W when under 1000, and kW when over template <typename T> // This function makes power values appear as W when under 1000, and kW when over

View file

@ -12,7 +12,6 @@
#include "../../lib/me-no-dev-ESPAsyncWebServer/src/ESPAsyncWebServer.h" #include "../../lib/me-no-dev-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
#include "../../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" #include "../../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
#include "../config.h" // Needed for LED defines #include "../config.h" // Needed for LED defines
#include "../utils/events.h"
#ifdef MQTT #ifdef MQTT
#include "../mqtt/mqtt.h" #include "../mqtt/mqtt.h"
#endif #endif
@ -37,7 +36,6 @@ extern uint16_t cellvoltages[120]; //mV 0-4350 per cell
extern uint8_t LEDcolor; //Enum, 0-10 extern uint8_t LEDcolor; //Enum, 0-10
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false
extern EVENTS_STRUCT_TYPE entries[EVENT_NOF_EVENTS];
extern const char* ssid; extern const char* ssid;
extern const char* password; extern const char* password;
@ -120,33 +118,6 @@ void init_ElegantOTA();
*/ */
String processor(const String& var); String processor(const String& var);
/**
* @brief Replaces placeholder with content section in web page
*
* @param[in] var
*
* @return String
*/
String settings_processor(const String& var);
/**
* @brief Replaces placeholder with content section in web page
*
* @param[in] var
*
* @return String
*/
String cellmonitor_processor(const String& var);
/**
* @brief Replaces placeholder with content section in web page
*
* @param[in] var
*
* @return String
*/
String events_processor(const String& var);
/** /**
* @brief Executes on OTA start * @brief Executes on OTA start
* *

View file

@ -13,7 +13,6 @@ extern uint16_t capacity_Wh;
extern uint16_t remaining_capacity_Wh; extern uint16_t remaining_capacity_Wh;
extern uint16_t max_target_discharge_power; extern uint16_t max_target_discharge_power;
extern uint16_t max_target_charge_power; extern uint16_t max_target_charge_power;
extern uint8_t bms_status;
extern uint8_t bms_char_dis_status; extern uint8_t bms_char_dis_status;
extern uint16_t stat_batt_power; extern uint16_t stat_batt_power;
extern uint16_t temperature_min; extern uint16_t temperature_min;

View file

@ -14,7 +14,6 @@ extern uint16_t capacity_Wh;
extern uint16_t remaining_capacity_Wh; extern uint16_t remaining_capacity_Wh;
extern uint16_t max_target_discharge_power; extern uint16_t max_target_discharge_power;
extern uint16_t max_target_charge_power; extern uint16_t max_target_charge_power;
extern uint8_t bms_status;
extern uint8_t bms_char_dis_status; extern uint8_t bms_char_dis_status;
extern uint16_t stat_batt_power; extern uint16_t stat_batt_power;
extern uint16_t temperature_min; extern uint16_t temperature_min;

View file

@ -1,6 +1,7 @@
//SERIAL-LINK-TRANSMITTER-INVERTER.cpp //SERIAL-LINK-TRANSMITTER-INVERTER.cpp
#include "SERIAL-LINK-TRANSMITTER-INVERTER.h" #include "SERIAL-LINK-TRANSMITTER-INVERTER.h"
#include "../devboard/utils/events.h"
/* /*
* SerialDataLink * SerialDataLink
@ -107,10 +108,9 @@ void manageSerialLinkTransmitter() {
Serial.println("SerialDataLink : max_target_discharge_power = 0"); Serial.println("SerialDataLink : max_target_discharge_power = 0");
Serial.println("SerialDataLink : max_target_charge_power = 0"); Serial.println("SerialDataLink : max_target_charge_power = 0");
bms_status = 4; //FAULT
max_target_discharge_power = 0; max_target_discharge_power = 0;
max_target_charge_power = 0; max_target_charge_power = 0;
LEDcolor = RED; set_event(EVENT_SERIAL_TX_FAILURE, 0);
// throw error // throw error
} }
/* /*

View file

@ -24,7 +24,6 @@ extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 funct
extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t cell_max_voltage; //mV, 0-4350 extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350 extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint8_t LEDcolor; //Enum, 0-10
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool LFP_Chemistry; extern bool LFP_Chemistry;

View file

@ -2,33 +2,9 @@
#include "../lib/miwagner-ESP32-Arduino-CAN/CAN_config.h" #include "../lib/miwagner-ESP32-Arduino-CAN/CAN_config.h"
#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" #include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
//TODO: change CAN sending routine once confirmed that 500ms interval is OK for this battery type
/* Do not change code below unless you are sure what you are doing */ /* Do not change code below unless you are sure what you are doing */
static unsigned long previousMillis1s = 0; // will store last time a Xs CAN Message was send static unsigned long previousMillis100ms = 0; // will store last time a 100ms CAN Message was send
static unsigned long previousMillis2s = 0; // will store last time a Xs CAN Message was send static const int interval100ms = 100; // interval (ms) at which send CAN Messages
static unsigned long previousMillis3s = 0; // will store last time a Xs CAN Message was send
static unsigned long previousMillis4s = 0; // will store last time a Xs CAN Message was send
static unsigned long previousMillis5s = 0; // will store last time a Xs CAN Message was send
static unsigned long previousMillis6s = 0; // will store last time a Xs CAN Message was send
static unsigned long previousMillis7s = 0; // will store last time a Xs CAN Message was send
static unsigned long previousMillis8s = 0; // will store last time a Xs CAN Message was send
static unsigned long previousMillis9s = 0; // will store last time a Xs CAN Message was send
static unsigned long previousMillis10s = 0; // will store last time a Xs CAN Message was send
static unsigned long previousMillis11s = 0; // will store last time a Xs CAN Message was send
static unsigned long previousMillis12s = 0; // will store last time a Xs CAN Message was send
static const int interval1s = 100; // interval (ms) at which send CAN Messages
static const int interval2s = 102; // interval (ms) at which send CAN Messages
static const int interval3s = 104; // interval (ms) at which send CAN Messages
static const int interval4s = 106; // interval (ms) at which send CAN Messages
static const int interval5s = 108; // interval (ms) at which send CAN Messages
static const int interval6s = 110; // interval (ms) at which send CAN Messages
static const int interval7s = 112; // interval (ms) at which send CAN Messages
static const int interval8s = 114; // interval (ms) at which send CAN Messages
static const int interval9s = 116; // interval (ms) at which send CAN Messages
static const int interval10s = 118; // interval (ms) at which send CAN Messages
static const int interval11s = 120; // interval (ms) at which send CAN Messages
static const int interval12s = 122; // interval (ms) at which send CAN Messages
//Actual content messages //Actual content messages
static const CAN_frame_t SMA_558 = { static const CAN_frame_t SMA_558 = {
@ -176,11 +152,18 @@ void update_values_can_sma() { //This function maps all the values fetched from
//Error bits //Error bits
//SMA_158.data.u8[0] = //bit12 Fault high temperature, bit34Battery cellundervoltage, bit56 Battery cell overvoltage, bit78 batterysystemdefect //SMA_158.data.u8[0] = //bit12 Fault high temperature, bit34Battery cellundervoltage, bit56 Battery cell overvoltage, bit78 batterysystemdefect
//TODO: add all error bits //TODO: add all error bits. Sending message with all 0xAA until that.
} }
void receive_can_sma(CAN_frame_t rx_frame) { void receive_can_sma(CAN_frame_t rx_frame) {
switch (rx_frame.MsgID) { switch (rx_frame.MsgID) {
case 0x360: //Message originating from SMA inverter - Voltage and current
//Frame0-1 Voltage
//Frame2-3 Current
break;
case 0x420: //Message originating from SMA inverter - Timestamp
//Frame0-3 Timestamp
break;
case 0x660: //Message originating from SMA inverter case 0x660: //Message originating from SMA inverter
break; break;
case 0x5E0: //Message originating from SMA inverter case 0x5E0: //Message originating from SMA inverter
@ -195,65 +178,21 @@ void receive_can_sma(CAN_frame_t rx_frame) {
void send_can_sma() { void send_can_sma() {
unsigned long currentMillis = millis(); unsigned long currentMillis = millis();
// Send CAN Message every X ms, 1000 for testing // Send CAN Message every 100ms
if (currentMillis - previousMillis1s >= interval1s) { if (currentMillis - previousMillis100ms >= interval100ms) {
previousMillis1s = currentMillis; previousMillis100ms = currentMillis;
ESP32Can.CANWriteFrame(&SMA_558); ESP32Can.CANWriteFrame(&SMA_558);
}
if (currentMillis - previousMillis2s >= interval2s) {
previousMillis2s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_598); ESP32Can.CANWriteFrame(&SMA_598);
}
if (currentMillis - previousMillis3s >= interval3s) {
previousMillis3s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_5D8); ESP32Can.CANWriteFrame(&SMA_5D8);
}
if (currentMillis - previousMillis4s >= interval4s) {
previousMillis4s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_618_1); ESP32Can.CANWriteFrame(&SMA_618_1);
}
if (currentMillis - previousMillis5s >= interval5s) {
previousMillis5s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_618_2); ESP32Can.CANWriteFrame(&SMA_618_2);
}
if (currentMillis - previousMillis6s >= interval6s) {
previousMillis6s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_618_3); ESP32Can.CANWriteFrame(&SMA_618_3);
}
if (currentMillis - previousMillis7s >= interval7s) {
previousMillis7s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_358); ESP32Can.CANWriteFrame(&SMA_358);
}
if (currentMillis - previousMillis8s >= interval8s) {
previousMillis8s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_3D8); ESP32Can.CANWriteFrame(&SMA_3D8);
}
if (currentMillis - previousMillis9s >= interval9s) {
previousMillis9s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_458); ESP32Can.CANWriteFrame(&SMA_458);
}
if (currentMillis - previousMillis10s >= interval10s) {
previousMillis10s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_518); ESP32Can.CANWriteFrame(&SMA_518);
}
if (currentMillis - previousMillis11s >= interval11s) {
previousMillis11s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_4D8); ESP32Can.CANWriteFrame(&SMA_4D8);
}
if (currentMillis - previousMillis12s >= interval12s) {
previousMillis12s = currentMillis;
ESP32Can.CANWriteFrame(&SMA_158); ESP32Can.CANWriteFrame(&SMA_158);
} }
} }

View file

@ -13,7 +13,6 @@ extern uint16_t capacity_Wh; //Wh, 0-60000
extern uint16_t remaining_capacity_Wh; //Wh, 0-60000 extern uint16_t remaining_capacity_Wh; //Wh, 0-60000
extern uint16_t max_target_discharge_power; //W, 0-60000 extern uint16_t max_target_discharge_power; //W, 0-60000
extern uint16_t max_target_charge_power; //W, 0-60000 extern uint16_t max_target_charge_power; //W, 0-60000
extern uint8_t bms_status; //Enum, 0-5
extern uint8_t bms_char_dis_status; //Enum, 0-2 extern uint8_t bms_char_dis_status; //Enum, 0-2
extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530) extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530)
extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
@ -22,7 +21,6 @@ extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350 extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint16_t min_voltage; extern uint16_t min_voltage;
extern uint16_t max_voltage; extern uint16_t max_voltage;
extern uint8_t LEDcolor; //Enum, 0-10
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false

View file

@ -14,14 +14,12 @@ extern uint16_t capacity_Wh; //Wh, 0-60000
extern uint16_t remaining_capacity_Wh; //Wh, 0-60000 extern uint16_t remaining_capacity_Wh; //Wh, 0-60000
extern uint16_t max_target_discharge_power; //W, 0-60000 extern uint16_t max_target_discharge_power; //W, 0-60000
extern uint16_t max_target_charge_power; //W, 0-60000 extern uint16_t max_target_charge_power; //W, 0-60000
extern uint8_t bms_status; //Enum, 0-5
extern uint8_t bms_char_dis_status; //Enum, 0-2 extern uint8_t bms_char_dis_status; //Enum, 0-2
extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530) extern uint16_t stat_batt_power; //W, Goes thru convert2unsignedint16 function (5W = 5, -5W = 65530)
extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385) extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t cell_max_voltage; //mV, 0-4350 extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350 extern uint16_t cell_min_voltage; //mV, 0-4350
extern uint8_t LEDcolor; //Enum, 0-10
extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false extern bool batteryAllowsContactorClosing; //Bool, 1=true, 0=false
extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false extern bool inverterAllowsContactorClosing; //Bool, 1=true, 0=false

View file

@ -16,7 +16,6 @@ extern uint16_t capacity_Wh;
extern uint16_t remaining_capacity_Wh; extern uint16_t remaining_capacity_Wh;
extern uint16_t max_target_discharge_power; extern uint16_t max_target_discharge_power;
extern uint16_t max_target_charge_power; extern uint16_t max_target_charge_power;
extern uint8_t bms_status;
extern uint8_t bms_char_dis_status; extern uint8_t bms_char_dis_status;
extern uint16_t stat_batt_power; extern uint16_t stat_batt_power;
extern uint16_t temperature_min; extern uint16_t temperature_min;

View file

@ -1,5 +1,6 @@
#include "ESP32CAN.h" #include "ESP32CAN.h"
#include <Arduino.h> #include <Arduino.h>
#include "../../devboard/utils/events.h"
int ESP32CAN::CANInit() { int ESP32CAN::CANInit() {
return CAN_init(); return CAN_init();
@ -12,13 +13,14 @@ int ESP32CAN::CANWriteFrame(const CAN_frame_t* p_frame) {
tx_ok = (result == 0) ? true : false; tx_ok = (result == 0) ? true : false;
if (tx_ok == false) { if (tx_ok == false) {
Serial.println("CAN failure! Check wires"); Serial.println("CAN failure! Check wires");
LEDcolor = 3; set_event(EVENT_CAN_TX_FAILURE, 0);
start_time = millis(); start_time = millis();
} else {
clear_event(EVENT_CAN_TX_FAILURE);
} }
} else { } else {
if ((millis() - start_time) >= 2000) { if ((millis() - start_time) >= 2000) {
tx_ok = true; tx_ok = true;
LEDcolor = 0;
} }
} }
return result; return result;

View file

@ -3,7 +3,6 @@
#include "../../lib/miwagner-ESP32-Arduino-CAN/CAN.h" #include "../../lib/miwagner-ESP32-Arduino-CAN/CAN.h"
#include "../../lib/miwagner-ESP32-Arduino-CAN/CAN_config.h" #include "../../lib/miwagner-ESP32-Arduino-CAN/CAN_config.h"
extern uint8_t LEDcolor;
class ESP32CAN { class ESP32CAN {
public: public:

15
cmake_clean.bat Normal file
View file

@ -0,0 +1,15 @@
@echo off
echo Cleaning up
rmdir /Q/S build
echo Creating new CMake build folder
mkdir build
cd build
echo Building CMake project
call cmake ..
call cmake --build .
echo Executing tests
for %%i in ("test\Debug\*.exe") do (
echo Running %%i
%%i
)
cd..

18
test/CMakeLists.txt Normal file
View file

@ -0,0 +1,18 @@
# Include the directory with your source files
include_directories(${CMAKE_SOURCE_DIR}/Software/src/devboard ${CMAKE_SOURCE_DIR}/Software/src/devboard/utils . )
# Create a variable to store the list of test files
file(GLOB TEST_SOURCES utils/*.cpp)
# Loop through each test source file and create an executable
foreach(TEST_SOURCE ${TEST_SOURCES})
# Extract the test name without extension
get_filename_component(TEST_NAME ${TEST_SOURCE} NAME_WE)
# Create an executable for the test
add_executable(${TEST_NAME} ${TEST_SOURCE} test_lib.cpp)
# Apply the target_compile_definitions for the test
target_compile_definitions(${TEST_NAME} PRIVATE UNIT_TEST)
endforeach()

209
test/microtest.h Normal file
View file

@ -0,0 +1,209 @@
//
// microtest.h
//
// URL: https://github.com/torpedro/microtest.h
// Author: Pedro Flemming (http://torpedro.com/)
// License: MIT License (https://github.com/torpedro/microtest.h/blob/master/LICENSE)
// Copyright (c) 2017 Pedro Flemming
//
// This is a small header-only C++ unit testing framework.
// It allows to define small unit tests with set of assertions available.
//
#ifndef __MICROTEST_H__
#define __MICROTEST_H__
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
////////////////
// Assertions //
////////////////
#define ASSERT(cond) ASSERT_TRUE(cond);
#define ASSERT_TRUE(cond) \
if (!(cond)) \
throw mt::AssertFailedException(#cond, __FILE__, __LINE__);
#define ASSERT_FALSE(cond) \
if (cond) \
throw mt::AssertFailedException(#cond, __FILE__, __LINE__);
#define ASSERT_NULL(value) ASSERT_TRUE(value == NULL);
#define ASSERT_NOTNULL(value) ASSERT_TRUE(value != NULL);
#define ASSERT_STREQ(a, b) \
if (std::string(a).compare(std::string(b)) != 0) { \
printf("%s{ info} %s", mt::yellow(), mt::def()); \
std::cout << "Actual values: " << a << " != " << b << std::endl; \
throw mt::AssertFailedException(#a " == " #b, __FILE__, __LINE__); \
}
#define ASSERT_STRNEQ(a, b) \
if (std::string(a).compare(std::string(b)) != = 0) { \
printf("%s{ info} %s", mt::yellow(), mt::def()); \
std::cout << "Actual values: " << a << " == " << b << std::endl; \
throw mt::AssertFailedException(#a " != " #b, __FILE__, __LINE__); \
}
#define ASSERT_EQ(a, b) \
if (a != b) { \
printf("%s{ info} %s", mt::yellow(), mt::def()); \
std::cout << "Actual values: " << a << " != " << b << std::endl; \
} \
ASSERT(a == b);
#define ASSERT_NEQ(a, b) \
if (a == b) { \
printf("%s{ info} %s", mt::yellow(), mt::def()); \
std::cout << "Actual values: " << a << " == " << b << std::endl; \
} \
ASSERT(a != b);
////////////////
// Unit Tests //
////////////////
#define TEST(name) \
void name(); \
namespace { \
bool __##name = mt::TestsManager::AddTest(name, #name); \
} \
void name()
///////////////
// Framework //
///////////////
namespace mt {
inline const char* red() {
return "\033[1;31m";
}
inline const char* green() {
return "\033[0;32m";
}
inline const char* yellow() {
return "\033[0;33m";
}
inline const char* def() {
return "\033[0m";
}
inline void printRunning(const char* message, FILE* file = stdout) {
fprintf(file, "%s{ running}%s %s\n", green(), def(), message);
}
inline void printOk(const char* message, FILE* file = stdout) {
fprintf(file, "%s{ ok}%s %s\n", green(), def(), message);
}
inline void printFailed(const char* message, FILE* file = stdout) {
fprintf(file, "%s{ failed} %s%s\n", red(), message, def());
}
// Exception that is thrown when an assertion fails.
class AssertFailedException : public std::exception {
public:
AssertFailedException(std::string description, std::string filepath, int line)
: std::exception(), description_(description), filepath_(filepath), line_(line){};
virtual const char* what() const throw() { return description_.c_str(); }
inline const char* getFilepath() { return filepath_.c_str(); }
inline int getLine() { return line_; }
protected:
std::string description_;
std::string filepath_;
int line_;
};
class TestsManager {
// Note: static initialization fiasco
// http://www.parashift.com/c++-faq-lite/static-init-order.html
// http://www.parashift.com/c++-faq-lite/static-init-order-on-first-use.html
public:
struct Test {
const char* name;
void (*fn)(void);
};
static std::vector<Test>& tests() {
static std::vector<Test> tests_;
return tests_;
}
// Adds a new test to the current set of tests.
// Returns false if a test with the same name already exists.
inline static bool AddTest(void (*fn)(void), const char* name) {
tests().push_back({name, fn});
return true;
}
// Run all tests that are registered.
// Returns the number of tests that failed.
inline static size_t RunAllTests(FILE* file = stdout) {
size_t num_failed = 0;
for (const Test& test : tests()) {
// Run the test.
// If an AsserFailedException is thrown, the test has failed.
try {
printRunning(test.name, file);
(*test.fn)();
printOk(test.name, file);
} catch (AssertFailedException& e) {
printFailed(test.name, file);
fprintf(file, " %sAssertion failed: %s%s\n", red(), e.what(), def());
fprintf(file, " %s%s:%d%s\n", red(), e.getFilepath(), e.getLine(), def());
++num_failed;
}
}
int return_code = (num_failed > 0) ? 1 : 0;
return return_code;
}
};
// Class that will capture the arguments passed to the program.
class Runtime {
public:
static const std::vector<std::string>& args(int argc = -1, char** argv = NULL) {
static std::vector<std::string> args_;
if (argc >= 0) {
for (int i = 0; i < argc; ++i) {
args_.push_back(argv[i]);
}
}
return args_;
}
};
} // namespace mt
#define TEST_MAIN() \
int main(int argc, char* argv[]) { \
mt::Runtime::args(argc, argv); \
\
size_t num_failed = mt::TestsManager::RunAllTests(stdout); \
if (num_failed == 0) { \
fprintf(stdout, "%s{ summary} All tests succeeded!%s\n", mt::green(), mt::def()); \
return 0; \
} else { \
double percentage = 100.0 * num_failed / mt::TestsManager::tests().size(); \
fprintf(stderr, "%s{ summary} %zu tests failed (%.2f%%)%s\n", mt::red(), num_failed, percentage, mt::def()); \
return -1; \
} \
}
#endif // __MICROTEST_H__

13
test/test_lib.cpp Normal file
View file

@ -0,0 +1,13 @@
#include "test_lib.h"
#include <cstdint>
#include "../Software/src/devboard/config.h"
MySerial Serial;
unsigned long testlib_millis = 0;
uint8_t bms_status = ACTIVE;
uint8_t LEDcolor = GREEN;

48
test/test_lib.h Normal file
View file

@ -0,0 +1,48 @@
#ifndef __TEST_LIB_H__
#define __TEST_LIB_H__
#include <stdint.h>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include "config.h"
#include "microtest.h"
using namespace std;
class MySerial;
extern unsigned long testlib_millis;
extern MySerial Serial;
extern uint8_t bms_status;
extern uint8_t LEDcolor;
/* Mock millis() */
static inline unsigned long millis(void) {
return testlib_millis;
}
/* Mock Serial class */
class MySerial {
public:
size_t println(const char* s) {
return print(s, true); // Call print with newline argument true
}
size_t print(const char* s) {
return print(s, false); // Call print with newline argument false
}
private:
size_t print(const char* s, bool newline) {
size_t length = printf("%s", s); // Print the string without newline
if (newline) {
printf("\n"); // Add a newline if specified
length++; // Increment length to account for the added newline character
}
return length; // Return the total length printed
}
};
#endif

105
test/utils/events_test.cpp_ Normal file
View file

@ -0,0 +1,105 @@
// The test library must be included first!
#include "../test_lib.h"
#include "../../Software/src/devboard/config.h"
#include "../../Software/src/devboard/utils/timer.cpp"
class EEPROMClass {
public:
void begin(int s) {}
void writeUShort(int a, uint16_t d) {}
void commit(void) {}
uint16_t readUShort(int a) {}
template<typename T>
void get(int address, T &t) {}
template<typename T>
void put(int address, const T &t) {}
};
EEPROMClass EEPROM;
#include "../../Software/src/devboard/utils/events.cpp"
/* Local test variables */
bool elapsed = false;
/* Stubs */
void run_sequence_on_target(void) {}
/* Helper functions */
/* Test functions */
TEST(init_events_test) {
init_events();
for (uint8_t i = 0; i < EVENT_NOF_EVENTS; i++) {
ASSERT_EQ(events.entries[i].occurences, 0);
ASSERT_EQ(events.entries[i].data, 0);
ASSERT_EQ(events.entries[i].timestamp, 0);
}
}
TEST(update_event_time_test) {
// Reset
testlib_millis = 0;
events.time_seconds = 0;
init_events();
// No delta, so time shouldn't increase
update_event_time();
ASSERT_EQ(events.time_seconds, 0);
// Almost time to bump the seconds
testlib_millis = 999;
update_event_time();
ASSERT_EQ(events.time_seconds, 0);
// millis == 1000, so we should add a second
testlib_millis = 1000;
update_event_time();
ASSERT_EQ(events.time_seconds, 1);
// We shouldn't add more seconds until 2000 now
testlib_millis = 1999;
update_event_time();
ASSERT_EQ(events.time_seconds, 1);
testlib_millis = 2000;
update_event_time();
ASSERT_EQ(events.time_seconds, 2);
}
TEST(set_event_test) {
// Reset
init_events();
events.time_seconds = 0;
// Initially, the event should not have any data or occurences
ASSERT_EQ(events.entries[EVENT_CELL_OVER_VOLTAGE].data, 0);
ASSERT_EQ(events.entries[EVENT_CELL_OVER_VOLTAGE].occurences, 0);
ASSERT_EQ(events.entries[EVENT_CELL_OVER_VOLTAGE].timestamp, 0);
// Set current time and overvoltage event for cell 23 (RED color, bms_status == FAULT)
events.time_seconds = 345;
set_event(EVENT_CELL_OVER_VOLTAGE, 123);
// Ensure proper event data
ASSERT_EQ(events.entries[EVENT_CELL_OVER_VOLTAGE].data, 123);
ASSERT_EQ(events.entries[EVENT_CELL_OVER_VOLTAGE].occurences, 1);
ASSERT_EQ(events.entries[EVENT_CELL_OVER_VOLTAGE].timestamp, 345);
ASSERT_EQ(bms_status, FAULT);
}
TEST(events_message_test) {
set_event(EVENT_DUMMY_ERROR, 0); // Set dummy event with no data
ASSERT_STREQ("The dummy error event was set!", get_event_message_string(EVENT_DUMMY_ERROR));
}
TEST(events_level_test) {
init_events();
set_event(EVENT_DUMMY_ERROR, 0); // Set dummy event with no data
ASSERT_STREQ("ERROR", get_event_level_string(EVENT_DUMMY_ERROR));
}
TEST_MAIN();