Merge pull request #261 from dalathegreat/feature/double-battery

Feature: Double battery 🔋 🔋 🥈
This commit is contained in:
Daniel Öster 2024-07-30 14:14:01 +03:00 committed by GitHub
commit bcb3abdf1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 2310 additions and 536 deletions

View file

@ -226,6 +226,9 @@ void core_loop(void* task_time_us) {
led_exe();
#ifdef CONTACTOR_CONTROL
handle_contactors(); // Take care of startup precharge/contactor closing
#endif
#ifdef DOUBLE_BATTERY
check_interconnect_available();
#endif
}
END_TIME_MEASUREMENT_MAX(time_10ms, datalayer.system.status.time_10ms_us);
@ -235,7 +238,10 @@ void core_loop(void* task_time_us) {
{
previousMillisUpdateVal = millis(); // Order matters on the update_loop!
update_values_battery(); // Fetch battery values
update_SOC(); // Check if real or calculated SOC% value should be sent
#ifdef DOUBLE_BATTERY
update_values_battery2();
#endif
update_SOC(); // Check if real or calculated SOC% value should be sent
#ifndef SERIAL_LINK_RECEIVER
update_machineryprotection(); // Check safeties (Not on serial link reciever board)
#endif
@ -559,7 +565,7 @@ void send_can() {
}
#ifdef DUAL_CAN
void receive_can2() { // This function is similar to receive_can, but just takes care of inverters in the 2nd bus.
void receive_can2() { // This function is similar to receive_can, but just takes care of inverters in the 2nd bus OR double battery
// Depending on which inverter is selected, we forward this to their respective CAN routines
CAN_frame_t rx_frame_can2; // Struct with ESP32Can library format, compatible with the rest of the program
CANMessage MCP2515Frame; // Struct with ACAN2515 library format, needed to use thw MCP2515 library
@ -578,6 +584,9 @@ void receive_can2() { // This function is similar to receive_can, but just take
#ifdef CAN_INVERTER_SELECTED
receive_can_inverter(rx_frame_can2);
#endif
#ifdef DOUBLE_BATTERY
receive_can_battery2(rx_frame_can2);
#endif //DOUBLE_BATTERY
}
}
@ -589,6 +598,24 @@ void send_can2() {
}
#endif
#ifdef DOUBLE_BATTERY
void check_interconnect_available() {
if (datalayer.battery.status.voltage_dV == 0 || datalayer.battery2.status.voltage_dV == 0) {
return; // Both voltage values need to be available to start check
}
if (abs(datalayer.battery.status.voltage_dV - datalayer.battery2.status.voltage_dV) < 30) { // If we are within 3.0V
clear_event(EVENT_VOLTAGE_DIFFERENCE);
if (datalayer.battery.status.bms_status != FAULT) { // Only proceed if we are not in faulted state
datalayer.system.status.battery2_allows_contactor_closing = true;
}
} else { //We are over 3.0V diff
set_event(EVENT_VOLTAGE_DIFFERENCE,
(uint8_t)(abs(datalayer.battery.status.voltage_dV - datalayer.battery2.status.voltage_dV) / 10));
}
}
#endif //DOUBLE_BATTERY
#ifdef CONTACTOR_CONTROL
void handle_contactors() {
// First check if we have any active errors, incase we do, turn off the battery
@ -710,6 +737,22 @@ void update_SOC() {
} else { // No SOC window wanted. Set scaled to same as real.
datalayer.battery.status.reported_soc = datalayer.battery.status.real_soc;
}
#ifdef DOUBLE_BATTERY
// Perform extra SOC sanity checks on double battery setups
if (datalayer.battery.status.real_soc < 100) { //If this battery is under 1.00%, use this as SOC instead of average
datalayer.battery.status.reported_soc = datalayer.battery.status.real_soc;
}
if (datalayer.battery2.status.real_soc < 100) { //If this battery is under 1.00%, use this as SOC instead of average
datalayer.battery.status.reported_soc = datalayer.battery2.status.real_soc;
}
if (datalayer.battery.status.real_soc > 9900) { //If this battery is over 99.00%, use this as SOC instead of average
datalayer.battery.status.reported_soc = datalayer.battery.status.real_soc;
}
if (datalayer.battery2.status.real_soc > 9900) { //If this battery is over 99.00%, use this as SOC instead of average
datalayer.battery.status.reported_soc = datalayer.battery2.status.real_soc;
}
#endif //DOUBLE_BATTERY
}
void update_values_inverter() {

View file

@ -13,7 +13,7 @@ volatile float CHARGER_END_A = 1.0; // Current at which charging is consid
#ifdef WEBSERVER
volatile uint8_t AccessPointEnabled =
true; //Set to either true or false incase you want the board to enable a direct wifi access point
true; //Set to either true/false incase you want to enable direct wifi access point
std::string ssid = "REPLACE_WITH_YOUR_SSID"; // Maximum of 63 characters;
std::string password = "REPLACE_WITH_YOUR_PASSWORD"; // Minimum of 8 characters;
const char* ssidAP = "Battery Emulator"; // Maximum of 63 characters;

View file

@ -26,10 +26,11 @@
//#define TESLA_MODEL_3_BATTERY
//#define VOLVO_SPA_BATTERY
//#define TEST_FAKE_BATTERY
//#define DOUBLE_BATTERY //Enable this line if you use two identical batteries at the same time (requires DUAL_CAN setup)
/* 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_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 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
@ -47,7 +48,7 @@
//#define INTERLOCK_REQUIRED //Nissan LEAF specific setting, if enabled requires both high voltage conenctors to be seated before starting
//#define CONTACTOR_CONTROL //Enable this line to have pins 25,32,33 handle automatic precharge/contactor+/contactor- closing sequence
//#define PWM_CONTACTOR_CONTROL //Enable this line to use PWM logic for contactors, which lower power consumption and heat generation
//#define DUAL_CAN //Enable this line to activate an isolated secondary CAN Bus using add-on MCP2515 controller (Needed for FoxESS inverters)
//#define DUAL_CAN //Enable this line to activate an isolated secondary CAN Bus using add-on MCP2515 controller (Needed for some inverters / double battery)
//#define CAN_FD //Enable this line to activate an isolated secondary CAN-FD bus using add-on MCP2517FD controller (Needed for some batteries)
//#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)

View file

@ -93,4 +93,9 @@ void update_values_battery();
void send_can_battery();
void setup_battery(void);
#ifdef DOUBLE_BATTERY
void update_values_battery2();
void receive_can_battery2(CAN_frame_t rx_frame);
#endif
#endif

View file

@ -15,18 +15,12 @@ static unsigned long previousMillis640 = 0; // will store last time a 600ms C
static unsigned long previousMillis1000 = 0; // will store last time a 1000ms CAN Message was send
static unsigned long previousMillis5000 = 0; // will store last time a 5000ms CAN Message was send
static unsigned long previousMillis10000 = 0; // will store last time a 10000ms CAN Message was send
#define ALIVE_MAX_VALUE 14 // BMW CAN messages contain alive counter, goes from 0...14
#define ALIVE_MAX_VALUE 14 // BMW CAN messages contain alive counter, goes from 0...14
enum BatterySize { BATTERY_60AH, BATTERY_94AH, BATTERY_120AH };
static BatterySize detectedBattery = BATTERY_60AH;
static const uint16_t WUPonDuration = 477; // in milliseconds how long WUP should be ON after poweron
static const uint16_t WUPoffDuration = 105; // in milliseconds how long WUP should be OFF after on pulse
unsigned long lastChangeTime; // Variables to store timestamps
unsigned long turnOnTime; // Variables to store timestamps
enum State { POWERON, STATE_ON, STATE_OFF };
static State WUPState = POWERON;
enum CmdState { SOH, CELL_VOLTAGE, SOC, CELL_VOLTAGE_AVG };
static CmdState cmdState = SOH;
@ -324,6 +318,7 @@ static uint8_t BMW_13E_counter = 0;
static uint8_t BMW_380_counter = 0;
static uint32_t BMW_328_counter = 0;
static bool battery_awake = false;
static bool battery2_awake = false;
static bool battery_info_available = false;
static uint32_t battery_serial_number = 0;
@ -395,6 +390,75 @@ static uint8_t battery_ID2 = 0;
static uint8_t battery_cellvoltage_mux = 0;
static uint8_t battery_soh = 99;
static uint32_t battery2_serial_number = 0;
static uint32_t battery2_available_power_shortterm_charge = 0;
static uint32_t battery2_available_power_shortterm_discharge = 0;
static uint32_t battery2_available_power_longterm_charge = 0;
static uint32_t battery2_available_power_longterm_discharge = 0;
static uint32_t battery2_BEV_available_power_shortterm_charge = 0;
static uint32_t battery2_BEV_available_power_shortterm_discharge = 0;
static uint32_t battery2_BEV_available_power_longterm_charge = 0;
static uint32_t battery2_BEV_available_power_longterm_discharge = 0;
static uint16_t battery2_energy_content_maximum_kWh = 0;
static uint16_t battery2_display_SOC = 0;
static uint16_t battery2_volts = 0;
static uint16_t battery2_HVBatt_SOC = 0;
static uint16_t battery2_DC_link_voltage = 0;
static uint16_t battery2_max_charge_voltage = 0;
static uint16_t battery2_min_discharge_voltage = 0;
static uint16_t battery2_predicted_energy_charge_condition = 0;
static uint16_t battery2_predicted_energy_charging_target = 0;
static uint16_t battery2_actual_value_power_heating = 0; //0 - 4094 W
static uint16_t battery2_prediction_voltage_shortterm_charge = 0;
static uint16_t battery2_prediction_voltage_shortterm_discharge = 0;
static uint16_t battery2_prediction_voltage_longterm_charge = 0;
static uint16_t battery2_prediction_voltage_longterm_discharge = 0;
static uint16_t battery2_prediction_duration_charging_minutes = 0;
static uint16_t battery2_target_voltage_in_CV_mode = 0;
static uint16_t battery2_soc = 0;
static uint16_t battery2_soc_hvmax = 0;
static uint16_t battery2_soc_hvmin = 0;
static uint16_t battery2_capacity_cah = 0;
static int16_t battery2_temperature_HV = 0;
static int16_t battery2_temperature_heat_exchanger = 0;
static int16_t battery2_temperature_max = 0;
static int16_t battery2_temperature_min = 0;
static int16_t battery2_max_charge_amperage = 0;
static int16_t battery2_max_discharge_amperage = 0;
static int16_t battery2_power = 0;
static int16_t battery2_current = 0;
static uint8_t battery2_status_error_isolation_external_Bordnetz = 0;
static uint8_t battery2_status_error_isolation_internal_Bordnetz = 0;
static uint8_t battery2_request_cooling = 0;
static uint8_t battery2_status_valve_cooling = 0;
static uint8_t battery2_status_error_locking = 0;
static uint8_t battery2_status_precharge_locked = 0;
static uint8_t battery2_status_disconnecting_switch = 0;
static uint8_t battery2_status_emergency_mode = 0;
static uint8_t battery2_request_service = 0;
static uint8_t battery2_error_emergency_mode = 0;
static uint8_t battery2_status_error_disconnecting_switch = 0;
static uint8_t battery2_status_warning_isolation = 0;
static uint8_t battery2_status_cold_shutoff_valve = 0;
static uint8_t battery2_request_open_contactors = 0;
static uint8_t battery2_request_open_contactors_instantly = 0;
static uint8_t battery2_request_open_contactors_fast = 0;
static uint8_t battery2_charging_condition_delta = 0;
static uint8_t battery2_status_service_disconnection_plug = 0;
static uint8_t battery2_status_measurement_isolation = 0;
static uint8_t battery2_request_abort_charging = 0;
static uint8_t battery2_prediction_time_end_of_charging_minutes = 0;
static uint8_t battery2_request_operating_mode = 0;
static uint8_t battery2_request_charging_condition_minimum = 0;
static uint8_t battery2_request_charging_condition_maximum = 0;
static uint8_t battery2_status_cooling_HV = 0; //1 works, 2 does not start
static uint8_t battery2_status_diagnostics_HV = 0; // 0 all OK, 1 HV protection function error, 2 diag not yet expired
static uint8_t battery2_status_diagnosis_powertrain_maximum_multiplexer = 0;
static uint8_t battery2_status_diagnosis_powertrain_immediate_multiplexer = 0;
static uint8_t battery2_ID2 = 0;
static uint8_t battery2_cellvoltage_mux = 0;
static uint8_t battery2_soh = 99;
static uint8_t message_data[50];
static uint8_t next_data = 0;
@ -414,7 +478,56 @@ static uint8_t increment_alive_counter(uint8_t counter) {
return counter;
}
void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for modbus
void CAN_WriteFrame(CAN_frame_t* tx_frame) {
CANMessage MCP2515Frame; //Struct with ACAN2515 library format, needed to use the MCP2515 library for CAN2
MCP2515Frame.id = tx_frame->MsgID;
//MCP2515Frame.ext = tx_frame->FIR.B.FF;
MCP2515Frame.len = tx_frame->FIR.B.DLC;
for (uint8_t i = 0; i < MCP2515Frame.len; i++) {
MCP2515Frame.data[i] = tx_frame->data.u8[i];
}
can.tryToSend(MCP2515Frame);
}
void update_values_battery2() { //This function maps all the values fetched via CAN2 to the battery2 datalayer
if (!battery2_awake) {
return;
}
datalayer.battery2.status.real_soc = (battery2_HVBatt_SOC * 10);
datalayer.battery2.status.voltage_dV = battery2_volts; //Unit V+1 (5000 = 500.0V)
datalayer.battery2.status.current_dA = battery2_current;
datalayer.battery2.status.remaining_capacity_Wh = (battery2_energy_content_maximum_kWh * 1000); // Convert kWh to Wh
datalayer.battery2.status.soh_pptt = battery2_soh * 100;
if (battery2_BEV_available_power_longterm_discharge > 65000) {
datalayer.battery2.status.max_discharge_power_W = 65000;
} else {
datalayer.battery2.status.max_discharge_power_W = battery2_BEV_available_power_longterm_discharge;
}
if (battery2_BEV_available_power_longterm_charge > 65000) {
datalayer.battery2.status.max_charge_power_W = 65000;
} else {
datalayer.battery2.status.max_charge_power_W = battery2_BEV_available_power_longterm_charge;
}
battery2_power = (datalayer.battery2.status.current_dA * (datalayer.battery2.status.voltage_dV / 100));
datalayer.battery2.status.active_power_W = battery2_power;
datalayer.battery2.status.temperature_min_dC = battery2_temperature_min * 10; // Add a decimal
datalayer.battery2.status.temperature_max_dC = battery2_temperature_max * 10; // Add a decimal
datalayer.battery2.status.cell_min_voltage_mV = datalayer.battery2.status.cell_voltages_mV[0];
datalayer.battery2.status.cell_max_voltage_mV = datalayer.battery2.status.cell_voltages_mV[1];
}
void update_values_battery() { //This function maps all the values fetched via CAN to the battery datalayer
if (!battery_awake) {
return;
}
@ -524,6 +637,7 @@ void receive_can_battery(CAN_frame_t rx_frame) {
CAN_STILL_ALIVE; //This message is only sent if 30C (Wakeup pin on battery) is energized with 12V
battery_current = (rx_frame.data.u8[1] << 8 | rx_frame.data.u8[0]) - 8192; //deciAmps (-819.2 to 819.0A)
battery_volts = (rx_frame.data.u8[3] << 8 | rx_frame.data.u8[2]); //500.0 V
datalayer.battery.status.voltage_dV = battery_volts; // Update the datalayer as soon as possible with this info
battery_HVBatt_SOC = ((rx_frame.data.u8[5] & 0x0F) << 8 | rx_frame.data.u8[4]);
battery_request_open_contactors = (rx_frame.data.u8[5] & 0xC0) >> 6;
battery_request_open_contactors_instantly = (rx_frame.data.u8[6] & 0x03);
@ -678,6 +792,174 @@ void receive_can_battery(CAN_frame_t rx_frame) {
break;
}
}
void receive_can_battery2(CAN_frame_t rx_frame) {
switch (rx_frame.MsgID) {
case 0x112: //BMS [10ms] Status Of High-Voltage Battery - 2
battery2_awake = true;
datalayer.battery2.status.CAN_battery_still_alive =
CAN_STILL_ALIVE; //This message is only sent if 30C (Wakeup pin on battery) is energized with 12V
battery2_current = (rx_frame.data.u8[1] << 8 | rx_frame.data.u8[0]) - 8192; //deciAmps (-819.2 to 819.0A)
battery2_volts = (rx_frame.data.u8[3] << 8 | rx_frame.data.u8[2]); //500.0 V
datalayer.battery2.status.voltage_dV =
battery2_volts; // Update the datalayer as soon as possible with this info, needed for contactor control
battery2_HVBatt_SOC = ((rx_frame.data.u8[5] & 0x0F) << 8 | rx_frame.data.u8[4]);
battery2_request_open_contactors = (rx_frame.data.u8[5] & 0xC0) >> 6;
battery2_request_open_contactors_instantly = (rx_frame.data.u8[6] & 0x03);
battery2_request_open_contactors_fast = (rx_frame.data.u8[6] & 0x0C) >> 2;
battery2_charging_condition_delta = (rx_frame.data.u8[6] & 0xF0) >> 4;
battery2_DC_link_voltage = rx_frame.data.u8[7];
break;
case 0x1FA: //BMS [1000ms] Status Of High-Voltage Battery - 1
battery2_status_error_isolation_external_Bordnetz = (rx_frame.data.u8[0] & 0x03);
battery2_status_error_isolation_internal_Bordnetz = (rx_frame.data.u8[0] & 0x0C) >> 2;
battery2_request_cooling = (rx_frame.data.u8[0] & 0x30) >> 4;
battery2_status_valve_cooling = (rx_frame.data.u8[0] & 0xC0) >> 6;
battery2_status_error_locking = (rx_frame.data.u8[1] & 0x03);
battery2_status_precharge_locked = (rx_frame.data.u8[1] & 0x0C) >> 2;
battery2_status_disconnecting_switch = (rx_frame.data.u8[1] & 0x30) >> 4;
battery2_status_emergency_mode = (rx_frame.data.u8[1] & 0xC0) >> 6;
battery2_request_service = (rx_frame.data.u8[2] & 0x03);
battery2_error_emergency_mode = (rx_frame.data.u8[2] & 0x0C) >> 2;
battery2_status_error_disconnecting_switch = (rx_frame.data.u8[2] & 0x30) >> 4;
battery2_status_warning_isolation = (rx_frame.data.u8[2] & 0xC0) >> 6;
battery2_status_cold_shutoff_valve = (rx_frame.data.u8[3] & 0x0F);
battery2_temperature_HV = (rx_frame.data.u8[4] - 50);
battery2_temperature_heat_exchanger = (rx_frame.data.u8[5] - 50);
battery2_temperature_max = (rx_frame.data.u8[6] - 50);
battery2_temperature_min = (rx_frame.data.u8[7] - 50);
break;
case 0x239: //BMS [200ms]
battery2_predicted_energy_charge_condition = (rx_frame.data.u8[2] << 8 | rx_frame.data.u8[1]); //Wh
battery2_predicted_energy_charging_target = ((rx_frame.data.u8[4] << 8 | rx_frame.data.u8[3]) * 0.02); //kWh
break;
case 0x2BD: //BMS [100ms] Status diagnosis high voltage - 1
battery2_awake = true;
if (calculateCRC(rx_frame, rx_frame.FIR.B.DLC, 0x15) != rx_frame.data.u8[0]) {
//If calculated CRC does not match transmitted CRC, increase CANerror counter
datalayer.battery2.status.CAN_error_counter++;
break;
}
battery2_status_diagnostics_HV = (rx_frame.data.u8[2] & 0x0F);
break;
case 0x2F5: //BMS [100ms] High-Voltage Battery Charge/Discharge Limitations
battery2_max_charge_voltage = (rx_frame.data.u8[1] << 8 | rx_frame.data.u8[0]);
battery2_max_charge_amperage = (((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 819.2);
battery2_min_discharge_voltage = (rx_frame.data.u8[5] << 8 | rx_frame.data.u8[4]);
battery2_max_discharge_amperage = (((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]) - 819.2);
break;
case 0x2FF: //BMS [100ms] Status Heating High-Voltage Battery
battery2_awake = true;
battery2_actual_value_power_heating = (rx_frame.data.u8[1] << 4 | rx_frame.data.u8[0] >> 4);
break;
case 0x363: //BMS [1s] Identification High-Voltage Battery
battery2_serial_number =
(rx_frame.data.u8[3] << 24 | rx_frame.data.u8[2] << 16 | rx_frame.data.u8[1] << 8 | rx_frame.data.u8[0]);
break;
case 0x3C2: //BMS (94AH exclusive) - Status diagnostics OBD 2 powertrain
battery2_status_diagnosis_powertrain_maximum_multiplexer =
((rx_frame.data.u8[1] & 0x03) << 4 | rx_frame.data.u8[0] >> 4);
battery2_status_diagnosis_powertrain_immediate_multiplexer = (rx_frame.data.u8[0] & 0xFC) >> 2;
break;
case 0x3EB: //BMS [1s] Status of charging high-voltage storage - 3
battery2_available_power_shortterm_charge = (rx_frame.data.u8[1] << 8 | rx_frame.data.u8[0]) * 3;
battery2_available_power_shortterm_discharge = (rx_frame.data.u8[3] << 8 | rx_frame.data.u8[2]) * 3;
battery2_available_power_longterm_charge = (rx_frame.data.u8[5] << 8 | rx_frame.data.u8[4]) * 3;
battery2_available_power_longterm_discharge = (rx_frame.data.u8[7] << 8 | rx_frame.data.u8[6]) * 3;
break;
case 0x40D: //BMS [1s] Charging status of high-voltage storage - 1
battery2_BEV_available_power_shortterm_charge = (rx_frame.data.u8[1] << 8 | rx_frame.data.u8[0]) * 3;
battery2_BEV_available_power_shortterm_discharge = (rx_frame.data.u8[3] << 8 | rx_frame.data.u8[2]) * 3;
battery2_BEV_available_power_longterm_charge = (rx_frame.data.u8[5] << 8 | rx_frame.data.u8[4]) * 3;
battery2_BEV_available_power_longterm_discharge = (rx_frame.data.u8[7] << 8 | rx_frame.data.u8[6]) * 3;
break;
case 0x41C: //BMS [1s] Operating Mode Status Of Hybrid - 2
battery2_status_cooling_HV = (rx_frame.data.u8[1] & 0x03);
break;
case 0x426: // TODO: Figure out how to trigger sending of this. Does the SME require some CAN command?
battery2_cellvoltage_mux = rx_frame.data.u8[0];
if (battery2_cellvoltage_mux == 0) {
datalayer.battery2.status.cell_voltages_mV[0] = ((rx_frame.data.u8[1] * 10) + 1800);
datalayer.battery2.status.cell_voltages_mV[1] = ((rx_frame.data.u8[2] * 10) + 1800);
datalayer.battery2.status.cell_voltages_mV[2] = ((rx_frame.data.u8[3] * 10) + 1800);
datalayer.battery2.status.cell_voltages_mV[3] = ((rx_frame.data.u8[4] * 10) + 1800);
datalayer.battery2.status.cell_voltages_mV[4] = ((rx_frame.data.u8[5] * 10) + 1800);
datalayer.battery2.status.cell_voltages_mV[5] = ((rx_frame.data.u8[6] * 10) + 1800);
datalayer.battery2.status.cell_voltages_mV[5] = ((rx_frame.data.u8[7] * 10) + 1800);
}
break;
case 0x430: //BMS [1s] - Charging status of high-voltage battery - 2
battery2_prediction_voltage_shortterm_charge = (rx_frame.data.u8[1] << 8 | rx_frame.data.u8[0]);
battery2_prediction_voltage_shortterm_discharge = (rx_frame.data.u8[3] << 8 | rx_frame.data.u8[2]);
battery2_prediction_voltage_longterm_charge = (rx_frame.data.u8[5] << 8 | rx_frame.data.u8[4]);
battery2_prediction_voltage_longterm_discharge = (rx_frame.data.u8[7] << 8 | rx_frame.data.u8[6]);
break;
case 0x431: //BMS [200ms] Data High-Voltage Battery Unit
battery2_status_service_disconnection_plug = (rx_frame.data.u8[0] & 0x0F);
battery2_status_measurement_isolation = (rx_frame.data.u8[0] & 0x0C) >> 2;
battery2_request_abort_charging = (rx_frame.data.u8[0] & 0x30) >> 4;
battery2_prediction_duration_charging_minutes = (rx_frame.data.u8[3] << 8 | rx_frame.data.u8[2]);
battery2_prediction_time_end_of_charging_minutes = rx_frame.data.u8[4];
battery2_energy_content_maximum_kWh = (((rx_frame.data.u8[6] & 0x0F) << 8 | rx_frame.data.u8[5])) / 50;
break;
case 0x432: //BMS [200ms] SOC% info
battery2_request_operating_mode = (rx_frame.data.u8[0] & 0x03);
battery2_target_voltage_in_CV_mode = ((rx_frame.data.u8[1] << 4 | rx_frame.data.u8[0] >> 4)) / 10;
battery2_request_charging_condition_minimum = (rx_frame.data.u8[2] / 2);
battery2_request_charging_condition_maximum = (rx_frame.data.u8[3] / 2);
battery2_display_SOC = (rx_frame.data.u8[4] / 2);
break;
case 0x507: //BMS [640ms] Network Management - 2 - This message is sent on the bus for sleep coordination purposes
break;
case 0x587: //BMS [5s] Services
battery2_ID2 = rx_frame.data.u8[0];
break;
case 0x607: //BMS - responses to message requests on 0x615
if (rx_frame.FIR.B.DLC > 6 && next_data == 0 && rx_frame.data.u8[0] == 0xf1) {
uint8_t count2 = 6;
while (count2 < rx_frame.FIR.B.DLC && next_data < 49) {
message_data[next_data++] = rx_frame.data.u8[count2++];
}
//ESP32Can.CANWriteFrame(&BMW_6F1_CONTINUE); // tell battery to send additional messages TODO: Make this send to Can2 instead of CAN1
} else if (rx_frame.FIR.B.DLC > 3 && next_data > 0 && rx_frame.data.u8[0] == 0xf1 &&
((rx_frame.data.u8[1] & 0xF0) == 0x20)) {
uint8_t count2 = 2;
while (count2 < rx_frame.FIR.B.DLC && next_data < 49) {
message_data[next_data++] = rx_frame.data.u8[count2++];
}
switch (cmdState) {
case CELL_VOLTAGE:
if (next_data >= 4) {
datalayer.battery2.status.cell_voltages_mV[0] = (message_data[0] << 8 | message_data[1]);
datalayer.battery2.status.cell_voltages_mV[2] = (message_data[2] << 8 | message_data[3]);
}
break;
case CELL_VOLTAGE_AVG:
if (next_data >= 30) {
datalayer.battery2.status.cell_voltages_mV[1] = (message_data[10] << 8 | message_data[11]) / 10;
battery2_capacity_cah = (message_data[4] << 8 | message_data[5]);
}
break;
case SOH:
if (next_data >= 4) {
battery2_soh = message_data[3];
}
break;
case SOC:
if (next_data >= 6) {
battery2_soc = (message_data[0] << 8 | message_data[1]);
battery2_soc_hvmax = (message_data[2] << 8 | message_data[3]);
battery2_soc_hvmin = (message_data[4] << 8 | message_data[5]);
}
break;
}
}
break;
default:
break;
}
}
void send_can_battery() {
unsigned long currentMillis = millis();
@ -698,10 +980,6 @@ void send_can_battery() {
BMW_10B.data.u8[1] = 0x10; // Close contactors
}
if (datalayer.battery.status.bms_status == FAULT) {
BMW_10B.data.u8[1] = 0x00; // Open contactors (TODO: test if this works)
}
BMW_10B.data.u8[1] = ((BMW_10B.data.u8[1] & 0xF0) + alive_counter_20ms);
BMW_10B.data.u8[0] = calculateCRC(BMW_10B, 3, 0x3F);
@ -710,7 +988,17 @@ void send_can_battery() {
BMW_13E_counter++;
BMW_13E.data.u8[4] = BMW_13E_counter;
ESP32Can.CANWriteFrame(&BMW_10B);
if (datalayer.battery.status.bms_status == FAULT) {
} //If battery is not in Fault mode, allow contactor to close by sending 10B
else {
ESP32Can.CANWriteFrame(&BMW_10B);
}
#ifdef DOUBLE_BATTERY //If second battery is allowed to join in, also send 10B
if (datalayer.system.status.battery2_allows_contactor_closing == true) {
CAN_WriteFrame(&BMW_10B);
}
#endif
}
// Send 100ms CAN Message
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
@ -722,6 +1010,9 @@ void send_can_battery() {
alive_counter_100ms = increment_alive_counter(alive_counter_100ms);
ESP32Can.CANWriteFrame(&BMW_12F);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_12F);
#endif
}
// Send 200ms CAN Message
if (currentMillis - previousMillis200 >= INTERVAL_200_MS) {
@ -733,6 +1024,9 @@ void send_can_battery() {
alive_counter_200ms = increment_alive_counter(alive_counter_200ms);
ESP32Can.CANWriteFrame(&BMW_19B);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_19B);
#endif
}
// Send 500ms CAN Message
if (currentMillis - previousMillis500 >= INTERVAL_500_MS) {
@ -744,6 +1038,9 @@ void send_can_battery() {
alive_counter_500ms = increment_alive_counter(alive_counter_500ms);
ESP32Can.CANWriteFrame(&BMW_30B);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_30B);
#endif
}
// Send 640ms CAN Message
if (currentMillis - previousMillis640 >= INTERVAL_640_MS) {
@ -751,6 +1048,10 @@ void send_can_battery() {
ESP32Can.CANWriteFrame(&BMW_512); // Keep BMS alive
ESP32Can.CANWriteFrame(&BMW_5F8);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_512);
CAN_WriteFrame(&BMW_5F8);
#endif
}
// Send 1000ms CAN Message
if (currentMillis - previousMillis1000 >= INTERVAL_1_S) {
@ -792,6 +1093,24 @@ void send_can_battery() {
ESP32Can.CANWriteFrame(&BMW_192);
ESP32Can.CANWriteFrame(&BMW_13E);
ESP32Can.CANWriteFrame(&BMW_433);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_3E8);
CAN_WriteFrame(&BMW_328);
CAN_WriteFrame(&BMW_3F9);
CAN_WriteFrame(&BMW_2E2);
CAN_WriteFrame(&BMW_41D);
CAN_WriteFrame(&BMW_3D0);
CAN_WriteFrame(&BMW_3CA);
CAN_WriteFrame(&BMW_3A7);
CAN_WriteFrame(&BMW_2CA);
CAN_WriteFrame(&BMW_3FB);
CAN_WriteFrame(&BMW_418);
CAN_WriteFrame(&BMW_1D0);
CAN_WriteFrame(&BMW_3EC);
CAN_WriteFrame(&BMW_192);
CAN_WriteFrame(&BMW_13E);
CAN_WriteFrame(&BMW_433);
#endif
BMW_433.data.u8[1] = 0x01; // First 433 message byte1 we send is unique, once we sent initial value send this
BMW_3E8.data.u8[0] = 0xF1; // First 3E8 message byte0 we send is unique, once we sent initial value send this
@ -800,18 +1119,30 @@ void send_can_battery() {
switch (cmdState) {
case SOC:
ESP32Can.CANWriteFrame(&BMW_6F1_CELL);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_6F1_CELL);
#endif
cmdState = CELL_VOLTAGE;
break;
case CELL_VOLTAGE:
ESP32Can.CANWriteFrame(&BMW_6F1_SOH);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_6F1_SOH);
#endif
cmdState = SOH;
break;
case SOH:
ESP32Can.CANWriteFrame(&BMW_6F1_CELL_VOLTAGE_AVG);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_6F1_CELL_VOLTAGE_AVG);
#endif
cmdState = CELL_VOLTAGE_AVG;
break;
case CELL_VOLTAGE_AVG:
ESP32Can.CANWriteFrame(&BMW_6F1_SOC);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_6F1_SOC);
#endif
cmdState = SOC;
break;
}
@ -828,11 +1159,21 @@ void send_can_battery() {
ESP32Can.CANWriteFrame(&BMW_3A0);
ESP32Can.CANWriteFrame(&BMW_592_0);
ESP32Can.CANWriteFrame(&BMW_592_1);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_3FC);
CAN_WriteFrame(&BMW_3C5);
CAN_WriteFrame(&BMW_3A0);
CAN_WriteFrame(&BMW_592_0);
CAN_WriteFrame(&BMW_592_1);
#endif
alive_counter_5000ms = increment_alive_counter(alive_counter_5000ms);
if (BMW_380_counter < 3) {
ESP32Can.CANWriteFrame(&BMW_380); // This message stops after 3 times on startup
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_380);
#endif
BMW_380_counter++;
}
}
@ -843,6 +1184,11 @@ void send_can_battery() {
ESP32Can.CANWriteFrame(&BMW_3E5); //Order comes from CAN logs
ESP32Can.CANWriteFrame(&BMW_3E4);
ESP32Can.CANWriteFrame(&BMW_37B);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_3E5);
CAN_WriteFrame(&BMW_3E4);
CAN_WriteFrame(&BMW_37B);
#endif
BMW_3E5.data.u8[0] = 0xFD; // First 3E5 message byte0 we send is unique, once we sent initial value send this
}
@ -867,6 +1213,16 @@ void setup_battery(void) { // Performs one time setup at startup
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_60AH;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_60AH;
datalayer.system.status.battery_allows_contactor_closing = true;
#ifdef DOUBLE_BATTERY
Serial.println("Another BMW i3 battery also selected!");
datalayer.battery2.info.max_design_voltage_dV = datalayer.battery.info.max_design_voltage_dV;
datalayer.battery2.info.min_design_voltage_dV = datalayer.battery.info.min_design_voltage_dV;
datalayer.battery2.status.voltage_dV =
0; //Init voltage to 0 to allow contactor check to operate without fear of default values colliding
#endif
digitalWrite(WUP_PIN, HIGH); // Wake up the battery
}

View file

@ -3,6 +3,9 @@
#include <Arduino.h>
#include "../include.h"
#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
#include "../lib/pierremolinaro-acan2515/ACAN2515.h"
extern ACAN2515 can;
#define BATTERY_SELECTED

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,9 @@
#include "../include.h"
#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
#include "../lib/pierremolinaro-acan2515/ACAN2515.h"
extern ACAN2515 can;
#define BATTERY_SELECTED
#define MAX_CELL_DEVIATION_MV 500

File diff suppressed because it is too large Load diff

View file

@ -24,5 +24,10 @@ void printDebugIfActive(uint8_t symbol, const char* message);
void print_int_with_units(char* header, int value, char* units);
void print_SOC(char* header, int SOC);
void setup_battery(void);
#ifdef DOUBLE_BATTERY
#include "../lib/pierremolinaro-acan2515/ACAN2515.h"
extern ACAN2515 can;
void printFaultCodesIfActive_battery2();
#endif //DOUBLE_BATTERY
#endif

View file

@ -161,6 +161,8 @@ typedef struct {
#endif
/** True if the battery allows for the contactors to close */
bool battery_allows_contactor_closing = false;
/** True if the second battery allows for the contactors to close */
bool battery2_allows_contactor_closing = false;
/** True if the inverter allows for the contactors to close */
bool inverter_allows_contactor_closing = true;
} DATALAYER_SYSTEM_STATUS_TYPE;
@ -177,6 +179,7 @@ typedef struct {
class DataLayer {
public:
DATALAYER_BATTERY_TYPE battery;
DATALAYER_BATTERY_TYPE battery2;
DATALAYER_SHUNT_TYPE shunt;
DATALAYER_SYSTEM_TYPE system;
};

View file

@ -7,6 +7,8 @@ static uint8_t discharge_limit_failures = 0;
static bool battery_full_event_fired = false;
static bool battery_empty_event_fired = false;
#define MAX_SOH_DEVIATION_PPTT 2500
void update_machineryprotection() {
// Start checking that the battery is within reason. Incase we see any funny business, raise an event!
@ -67,9 +69,9 @@ void update_machineryprotection() {
// Battery is extremely degraded, not fit for secondlifestorage!
if (datalayer.battery.status.soh_pptt < 2500) {
set_event(EVENT_LOW_SOH, datalayer.battery.status.soh_pptt);
set_event(EVENT_SOH_LOW, datalayer.battery.status.soh_pptt);
} else {
clear_event(EVENT_LOW_SOH);
clear_event(EVENT_SOH_LOW);
}
// Check if SOC% is plausible
@ -129,8 +131,43 @@ void update_machineryprotection() {
// Too many malformed CAN messages recieved!
if (datalayer.battery.status.CAN_error_counter > MAX_CAN_FAILURES) {
set_event(EVENT_CAN_RX_WARNING, 0);
set_event(EVENT_CAN_RX_WARNING, 1);
} else {
clear_event(EVENT_CAN_RX_WARNING);
}
#ifdef DOUBLE_BATTERY // Additional Double-Battery safeties are checked here
// Check if the Battery 2 BMS is still sending CAN messages. If we go 60s without messages we raise an error
if (!datalayer.battery2.status.CAN_battery_still_alive) {
set_event(EVENT_CAN2_RX_FAILURE, 0);
} else {
datalayer.battery2.status.CAN_battery_still_alive--;
clear_event(EVENT_CAN2_RX_FAILURE);
}
// Too many malformed CAN messages recieved!
if (datalayer.battery2.status.CAN_error_counter > MAX_CAN_FAILURES) {
set_event(EVENT_CAN_RX_WARNING, 2);
} else {
clear_event(EVENT_CAN_RX_WARNING);
}
// Check if SOH% between the packs is too large
if ((datalayer.battery.status.soh_pptt != 9900) && (datalayer.battery2.status.soh_pptt != 9900)) {
// Both values available, check diff
uint16_t soh_diff_pptt;
if (datalayer.battery.status.soh_pptt > datalayer.battery2.status.soh_pptt) {
soh_diff_pptt = datalayer.battery.status.soh_pptt - datalayer.battery2.status.soh_pptt;
} else {
soh_diff_pptt = datalayer.battery2.status.soh_pptt - datalayer.battery.status.soh_pptt;
}
if (soh_diff_pptt > MAX_SOH_DEVIATION_PPTT) {
set_event(EVENT_SOH_DIFFERENCE, MAX_SOH_DEVIATION_PPTT);
} else {
clear_event(EVENT_SOH_DIFFERENCE);
}
}
#endif // DOUBLE_BATTERY
}

View file

@ -135,6 +135,7 @@ void init_events(void) {
events.entries[EVENT_CANFD_INIT_FAILURE].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CAN_OVERRUN].level = EVENT_LEVEL_INFO;
events.entries[EVENT_CAN_RX_FAILURE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_CAN2_RX_FAILURE].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_CANFD_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;
@ -155,7 +156,9 @@ void init_events(void) {
events.entries[EVENT_BATTERY_OVERVOLTAGE].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_BATTERY_UNDERVOLTAGE].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_BATTERY_ISOLATION].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_LOW_SOH].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_VOLTAGE_DIFFERENCE].level = EVENT_LEVEL_INFO;
events.entries[EVENT_SOH_DIFFERENCE].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_SOH_LOW].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_HVIL_FAILURE].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_PRECHARGE_FAILURE].level = EVENT_LEVEL_INFO;
events.entries[EVENT_INTERNAL_OPEN_FAULT].level = EVENT_LEVEL_ERROR;
@ -226,6 +229,8 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
return "CAN message failed to send within defined time. Contact developers, CPU load might be too high.";
case EVENT_CAN_RX_FAILURE:
return "No CAN communication detected for 60s. Shutting down battery control.";
case EVENT_CAN2_RX_FAILURE:
return "No CAN communication detected for 60s on CAN2. Shutting down the secondary battery control.";
case EVENT_CANFD_RX_FAILURE:
return "No CANFD communication detected for 60s. Shutting down battery control.";
case EVENT_CAN_RX_WARNING:
@ -270,7 +275,11 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
return "Warning: Battery under minimum design voltage. Charge battery to prevent damage!";
case EVENT_BATTERY_ISOLATION:
return "Warning: Battery reports isolation error. High voltage might be leaking to ground. Check battery!";
case EVENT_LOW_SOH:
case EVENT_VOLTAGE_DIFFERENCE:
return "Info: Too large voltage diff between the batteries. Second battery cannot join the DC-link";
case EVENT_SOH_DIFFERENCE:
return "Warning: Large deviation in State of health between packs. Inspect battery.";
case EVENT_SOH_LOW:
return "ERROR: State of health critically low. Battery internal resistance too high to continue. Recycle "
"battery.";
case EVENT_HVIL_FAILURE:

View file

@ -6,7 +6,7 @@
// #define INCLUDE_EVENTS_TEST // Enable to run an event test loop, see events_test_on_target.cpp
#define EE_MAGIC_HEADER_VALUE 0x0008 // 0x0000 to 0xFFFF
#define EE_MAGIC_HEADER_VALUE 0x0010 // 0x0000 to 0xFFFF
#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,
@ -29,6 +29,7 @@
XX(EVENT_CANFD_INIT_FAILURE) \
XX(EVENT_CAN_OVERRUN) \
XX(EVENT_CAN_RX_FAILURE) \
XX(EVENT_CAN2_RX_FAILURE) \
XX(EVENT_CANFD_RX_FAILURE) \
XX(EVENT_CAN_RX_WARNING) \
XX(EVENT_CAN_TX_FAILURE) \
@ -51,7 +52,9 @@
XX(EVENT_BATTERY_ISOLATION) \
XX(EVENT_BATTERY_REQUESTS_HEAT) \
XX(EVENT_BATTERY_WARMED_UP) \
XX(EVENT_LOW_SOH) \
XX(EVENT_VOLTAGE_DIFFERENCE) \
XX(EVENT_SOH_DIFFERENCE) \
XX(EVENT_SOH_LOW) \
XX(EVENT_HVIL_FAILURE) \
XX(EVENT_PRECHARGE_FAILURE) \
XX(EVENT_INTERNAL_OPEN_FAULT) \

View file

@ -12,13 +12,25 @@ String cellmonitor_processor(const String& var) {
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
#ifdef DOUBLE_BATTERY
content +=
"#graph, #graph2 {display: flex;align-items: flex-end;height: 200px;border: 1px solid #ccc;position: "
"relative;}";
#else
content += "#graph {display: flex;align-items: flex-end;height: 200px;border: 1px solid #ccc;position: relative;}";
#endif
content +=
".bar {margin: 0 0px;background-color: blue;display: inline-block;position: relative;cursor: pointer;border: "
"1px solid white; /* Add this line */}";
#ifdef DOUBLE_BATTERY
content += "#valueDisplay, #valueDisplay2 {text-align: left;font-weight: bold;margin-top: 10px;}";
#else
content += "#valueDisplay {text-align: left;font-weight: bold;margin-top: 10px;}";
#endif
content += "</style>";
content += "<button onclick='home()'>Back to main page</button>";
// Start a new block with a specific background color
content += "<div style='background-color: #303E47; padding: 10px; margin-bottom: 10px; border-radius: 50px'>";
@ -33,7 +45,26 @@ String cellmonitor_processor(const String& var) {
// Close the block
content += "</div>";
#ifdef DOUBLE_BATTERY
// Start a new block with a specific background color
content += "<div style='background-color: #303E41; padding: 10px; margin-bottom: 10px; border-radius: 50px'>";
// Display max, min, and deviation voltage values
content += "<div id='voltageValues2' class='voltage-values'></div>";
// Display cells
content += "<div id='cellContainer2' class='container'></div>";
// Display bars
content += "<div id='graph2'></div>";
// Display single hovered value
content += "<div id='valueDisplay2'>Value: ...</div>";
// Close the block
content += "</div>";
content += "<button onclick='home()'>Back to main page</button>";
#endif // DOUBLE_BATTERY
content += "<script>";
// Populate cell data
content += "const data = [";
@ -149,8 +180,123 @@ String cellmonitor_processor(const String& var) {
"available';";
content += "}";
#ifdef DOUBLE_BATTERY
// Populate cell data
content += "const data2 = [";
for (uint8_t i = 0u; i < datalayer.battery2.info.number_of_cells; i++) {
if (datalayer.battery2.status.cell_voltages_mV[i] == 0) {
continue;
}
content += String(datalayer.battery2.status.cell_voltages_mV[i]) + ",";
}
content += "];";
content += "const min_mv2 = Math.min(...data2) - 20;";
content += "const max_mv2 = Math.max(...data2) + 20;";
content += "const min_index2 = data2.indexOf(Math.min(...data2));";
content += "const max_index2 = data2.indexOf(Math.max(...data2));";
content += "const graphContainer2 = document.getElementById('graph2');";
content += "const valueDisplay2 = document.getElementById('valueDisplay2');";
content += "const cellContainer2 = document.getElementById('cellContainer2');";
// Arduino-style map() function
content +=
"function map2(value, fromLow, fromHigh, toLow, toHigh) {return (value - fromLow) * (toHigh - toLow) / "
"(fromHigh - fromLow) + toLow;}";
// Mark cell and bar with highest/lowest values
content +=
"function checkMinMax2(cell2, bar2, index2) {if ((index2 == min_index2) || (index2 == max_index2)) "
"{cell2.style.borderColor = 'red';bar2.style.borderColor = 'red';}}";
// Bar function. Basically get the mV, scale the height and add a bar div to its container
content +=
"function createBars2(data2) {"
"data2.forEach((mV, index2) => {"
"const bar2 = document.createElement('div');"
"const mV_limited2 = map2(mV, min_mv2, max_mv2, 20, 200);"
"bar2.className = 'bar';"
"bar2.id = `barIndex2${index2}`;"
"bar2.style.height = `${mV_limited2}px`;"
"bar2.style.width = `${750/data2.length}px`;"
"const cell2 = document.getElementById(`cellIndex2${index2}`);"
"checkMinMax2(cell2, bar2, index2);"
"bar2.addEventListener('mouseenter', () => {"
"valueDisplay2.textContent = `Value: ${mV}`;"
"bar2.style.backgroundColor = `lightblue`;"
"cell2.style.backgroundColor = `blue`;"
"});"
"bar2.addEventListener('mouseleave', () => {"
"valueDisplay2.textContent = 'Value: ...';"
"bar2.style.backgroundColor = `blue`;"
"cell2.style.removeProperty('background-color');"
"});"
"graphContainer2.appendChild(bar2);"
"});"
"}";
// Cell population function. For each value, add a cell block with its value
content +=
"function createCells2(data2) {"
"data2.forEach((mV, index2) => {"
"const cell2 = document.createElement('div');"
"cell2.className = 'cell';"
"cell2.id = `cellIndex2${index2}`;"
"let cellContent2 = `Cell ${index2 + 1}<br>${mV} mV`;"
"if (mV < 3000) {"
"cellContent2 = `<span class='low-voltage'>${cellContent2}</span>`;"
"}"
"cell2.innerHTML = cellContent2;"
"cell2.addEventListener('mouseenter', () => {"
"let bar2 = document.getElementById(`barIndex2${index2}`);"
"valueDisplay2.textContent = `Value: ${mV}`;"
"bar2.style.backgroundColor = `lightblue`;"
"cell2.style.backgroundColor = `blue`;"
"});"
"cell2.addEventListener('mouseleave', () => {"
"let bar2 = document.getElementById(`barIndex2${index2}`);"
"bar2.style.backgroundColor = `blue`;"
"cell2.style.removeProperty('background-color');"
"});"
"cellContainer2.appendChild(cell2);"
"});"
"}";
// On fetch, update the header of max/min/deviation client-side for consistency
content +=
"function updateVoltageValues2(data2) {"
"const min_mv2 = Math.min(...data2);"
"const max_mv2 = Math.max(...data2);"
"const cell_dev2 = max_mv2 - min_mv2;"
"const voltVal2 = document.getElementById('voltageValues2');"
"voltVal2.innerHTML = `Max Voltage : ${max_mv2} mV<br>Min Voltage: ${min_mv2} mV<br>Voltage Deviation: "
"${cell_dev2} mV`"
"}";
// If we have values, do the thing. Otherwise, display friendly message and wait
content += "if (data2.length != 0) {";
content += "createCells2(data2);";
content += "createBars2(data2);";
content += "updateVoltageValues2(data2);";
content += "}";
content += "else {";
content +=
"document.getElementById('voltageValues2').textContent = 'Cell information not yet fetched, or information not "
"available';";
content += "}";
#endif //DOUBLE_BATTERY
// Automatic refresh is nice
content += "setTimeout(function(){ location.reload(true); }, 10000);";
content += "setTimeout(function(){ location.reload(true); }, 20000);";
content += "</script>";
return content;

View file

@ -531,6 +531,9 @@ String processor(const String& var) {
#endif
#ifdef TEST_FAKE_BATTERY
content += "Fake battery for testing purposes";
#endif
#ifdef DOUBLE_BATTERY
content += " (Double battery)";
#endif
content += "</h4>";
@ -548,8 +551,15 @@ String processor(const String& var) {
// Close the block
content += "</div>";
#ifdef DOUBLE_BATTERY
// Start a new block with a specific background color. Color changes depending on BMS status
content += "<div style='display: flex; width: 100%;'>";
content += "<div style='flex: 1; background-color: ";
#else
// Start a new block with a specific background color. Color changes depending on system status
content += "<div style='background-color: ";
#endif
switch (led_get_color()) {
case led_color::GREEN:
content += "#2D3F2F;";
@ -634,6 +644,83 @@ String processor(const String& var) {
// Close the block
content += "</div>";
#ifdef DOUBLE_BATTERY
content += "<div style='flex: 1; background-color: ";
switch (datalayer.battery.status.bms_status) {
case ACTIVE:
content += "#2D3F2F;";
break;
case FAULT:
content += "#A70107;";
break;
default:
content += "#2D3F2F;";
break;
}
// Add the common style properties
content += "padding: 10px; margin-bottom: 10px; border-radius: 50px;'>";
// Display battery statistics within this block
socRealFloat =
static_cast<float>(datalayer.battery2.status.real_soc) / 100.0; // Convert to float and divide by 100
//socScaledFloat; // Same value used for bat2
sohFloat = static_cast<float>(datalayer.battery2.status.soh_pptt) / 100.0; // Convert to float and divide by 100
voltageFloat =
static_cast<float>(datalayer.battery2.status.voltage_dV) / 10.0; // Convert to float and divide by 10
currentFloat =
static_cast<float>(datalayer.battery2.status.current_dA) / 10.0; // Convert to float and divide by 10
powerFloat = static_cast<float>(datalayer.battery2.status.active_power_W); // Convert to float
tempMaxFloat = static_cast<float>(datalayer.battery2.status.temperature_max_dC) / 10.0; // Convert to float
tempMinFloat = static_cast<float>(datalayer.battery2.status.temperature_min_dC) / 10.0; // Convert to float
content += "<h4 style='color: white;'>Real SOC: " + String(socRealFloat, 2) + "</h4>";
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) + "</h4>";
content += "<h4 style='color: white;'>SOH: " + String(sohFloat, 2) + "</h4>";
content += "<h4 style='color: white;'>Voltage: " + String(voltageFloat, 1) + " V</h4>";
content += "<h4 style='color: white;'>Current: " + String(currentFloat, 1) + " A</h4>";
content += formatPowerValue("Power", powerFloat, "", 1);
content += formatPowerValue("Total capacity", datalayer.battery2.info.total_capacity_Wh, "h", 0);
content += formatPowerValue("Remaining capacity", datalayer.battery2.status.remaining_capacity_Wh, "h", 1);
content += formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1);
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1);
content += "<h4>Cell max: " + String(datalayer.battery2.status.cell_max_voltage_mV) + " mV</h4>";
content += "<h4>Cell min: " + String(datalayer.battery2.status.cell_min_voltage_mV) + " mV</h4>";
content += "<h4>Temperature max: " + String(tempMaxFloat, 1) + " C</h4>";
content += "<h4>Temperature min: " + String(tempMinFloat, 1) + " C</h4>";
if (datalayer.battery.status.bms_status == ACTIVE) {
content += "<h4>System status: OK </h4>";
} else if (datalayer.battery.status.bms_status == UPDATING) {
content += "<h4>System status: UPDATING </h4>";
} else {
content += "<h4>System status: FAULT </h4>";
}
if (datalayer.battery2.status.current_dA == 0) {
content += "<h4>Battery idle</h4>";
} else if (datalayer.battery2.status.current_dA < 0) {
content += "<h4>Battery discharging!</h4>";
} else { // > 0
content += "<h4>Battery charging!</h4>";
}
content += "<h4>Automatic contactor closing allowed:</h4>";
content += "<h4>Battery: ";
if (datalayer.system.status.battery2_allows_contactor_closing == true) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Inverter: ";
if (datalayer.system.status.inverter_allows_contactor_closing == true) {
content += "<span>&#10003;</span></h4>";
} else {
content += "<span style='color: red;'>&#10005;</span></h4>";
}
content += "</div>";
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'>";