Minimum viable product for double battery

This commit is contained in:
Daniel 2024-04-20 20:39:59 +03:00
parent 4cdbc50aaf
commit f83bbc35eb
10 changed files with 521 additions and 33 deletions

View file

@ -216,6 +216,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
handle_CAN_contactors();
#endif
}
END_TIME_MEASUREMENT_MAX(time_10ms, datalayer.system.status.time_10ms_us);
@ -590,6 +593,9 @@ void receive_can2() { // This function is similar to receive_can, but just take
if (rx_frame2.FIR.B.FF == CAN_frame_std) { // New standard frame
#ifdef BYD_CAN
receive_can_byd(rx_frame2);
#endif
#ifdef DOUBLE_BATTERY
receive_can_battery2(rx_frame2);
#endif
} else { // New extended frame
#ifdef PYLON_CAN
@ -611,6 +617,14 @@ void send_can2() {
}
#endif
#ifdef DOUBLE_BATTERY
void handle_CAN_contactors() {
if (abs(datalayer.battery.status.voltage_dV - datalayer.battery2.status.voltage_dV) < 50) {
datalayer.system.status.battery2_allows_contactor_closing = true;
} //TODO: Shall we handle opening incase of fault here?
}
#endif
#ifdef CONTACTOR_CONTROL
void handle_contactors() {
// First check if we have any active errors, incase we do, turn off the battery
@ -733,9 +747,17 @@ void update_SOC() {
}
}
void summarize_battery_values() {
// TODO: What needs to be summed?
}
void update_values() {
// Battery
update_values_battery(); // Map the fake values to the correct registers
update_values_battery();
#ifdef DOUBLE_BATTERY
update_values_battery2();
summarize_battery_values();
#endif
// Inverter
#ifdef BYD_CAN
update_values_can_byd();

View file

@ -14,8 +14,8 @@ 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
const char* ssid = "REPLACE_WITH_YOUR_SSID"; // Maximum of 63 characters;
const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Minimum of 8 characters;
const char* ssid = "ZTE_5G_VACCINE"; // Maximum of 63 characters;
const char* password = "secretpassword"; // Minimum of 8 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 uint8_t wifi_channel = 0; // set to 0 for automatic channel selection

View file

@ -8,7 +8,7 @@
/* To edit battery specific limits, see also the USER_SETTINGS.cpp file*/
/* Select battery used */
//#define BMW_I3_BATTERY
#define BMW_I3_BATTERY
//#define CHADEMO_BATTERY
//#define IMIEV_CZERO_ION_BATTERY
//#define KIA_HYUNDAI_64_BATTERY
@ -20,10 +20,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
@ -36,7 +37,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

@ -69,4 +69,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

@ -16,16 +16,18 @@ static unsigned long previousMillis1000 = 0; // will store last time a 1000ms
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
static uint8_t CANstillAlive = 12; // counter for checking if CAN is still alive
static unsigned long previousMillis20_2 = 0; // will store last time a 20ms CAN Message was send
static unsigned long previousMillis100_2 = 0; // will store last time a 100ms CAN Message was send
static unsigned long previousMillis200_2 = 0; // will store last time a 200ms CAN Message was send
static unsigned long previousMillis500_2 = 0; // will store last time a 500ms CAN Message was send
static unsigned long previousMillis640_2 = 0; // will store last time a 600ms CAN Message was send
static unsigned long previousMillis1000_2 = 0; // will store last time a 1000ms CAN Message was send
static unsigned long previousMillis5000_2 = 0; // will store last time a 5000ms CAN Message was send
static unsigned long previousMillis10000_2 = 0; // will store last time a 10000ms CAN Message was send
static uint8_t CAN2stillAlive = 12; // counter for checking if CAN2 is still alive
static uint16_t CANerror = 0; // counter on how many CAN errors encountered
#define ALIVE_MAX_VALUE 14 // BMW CAN messages contain alive counter, goes from 0...14
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;
@ -323,6 +325,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 uint32_t battery_serial_number = 0;
static uint32_t battery_available_power_shortterm_charge = 0;
@ -353,7 +356,6 @@ static uint16_t battery_soc = 0;
static uint16_t battery_soc_hvmax = 0;
static uint16_t battery_soc_hvmin = 0;
static uint16_t battery_capacity_cah = 0;
static int16_t battery_temperature_HV = 0;
static int16_t battery_temperature_heat_exchanger = 0;
static int16_t battery_temperature_max = 0;
@ -394,6 +396,75 @@ static uint8_t battery_ID2 = 0;
static uint8_t battery_cellvoltage_mux = 0;
static uint8_t battery_soh = 0;
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 = 0;
static uint8_t message_data[50];
static uint8_t next_data = 0;
@ -413,7 +484,66 @@ 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
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];
/* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/
if (!CAN2stillAlive) {
set_event(EVENT_CAN2_RX_FAILURE, 2);
datalayer.battery2.status.bms_status = FAULT; //TODO: Refactor handling of event for battery2
} else {
CAN2stillAlive--;
clear_event(EVENT_CAN2_RX_FAILURE);
}
// Check if we have encountered any malformed CAN messages
if (CANerror > MAX_CAN_FAILURES) {
set_event(EVENT_CAN_RX_WARNING, 2);
}
}
void update_values_battery() { //This function maps all the values fetched via CAN to the battery datalayer
datalayer.battery.status.real_soc = (battery_HVBatt_SOC * 10);
@ -492,6 +622,7 @@ void receive_can_battery(CAN_frame_t rx_frame) {
CANstillAlive = 12; //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);
@ -650,6 +781,172 @@ 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;
CAN2stillAlive = 12; //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
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
CANerror++;
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();
@ -668,10 +965,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);
@ -680,8 +973,18 @@ void send_can_battery() {
BMW_13E_counter++;
BMW_13E.data.u8[4] = BMW_13E_counter;
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) {
previousMillis100 = currentMillis;
@ -692,6 +995,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) {
@ -703,6 +1009,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) {
@ -714,6 +1023,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) {
@ -721,6 +1033,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) {
@ -762,6 +1078,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
@ -778,11 +1112,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++;
}
}
@ -793,9 +1137,17 @@ 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
next_data = 0;
ESP32Can.CANWriteFrame(&BMW_6F1_CELL);
#ifdef DOUBLE_BATTERY
CAN_WriteFrame(&BMW_6F1_CELL);
#endif
BMW_3E5.data.u8[0] = 0xFD; // First 3E5 message byte0 we send is unique, once we sent initial value send this
}
@ -811,6 +1163,16 @@ void setup_battery(void) { // Performs one time setup at startup
4040; // 404.4V, over this, charging is not possible (goes into forced discharge)
datalayer.battery.info.min_design_voltage_dV = 2800; // 280.0V under this, discharging further is disabled
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

View file

@ -142,6 +142,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;
@ -158,6 +160,7 @@ typedef struct {
class DataLayer {
public:
DATALAYER_BATTERY_TYPE battery;
DATALAYER_BATTERY_TYPE battery2;
DATALAYER_SYSTEM_TYPE system;
};

View file

@ -129,6 +129,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;
@ -194,6 +195,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:

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 0x0002 // 0x0000 to 0xFFFF
#define EE_MAGIC_HEADER_VALUE 0x0003 // 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) \

View file

@ -466,6 +466,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>";
@ -483,8 +486,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 BMS status
content += "<div style='background-color: ";
#endif
switch (led_get_color()) {
case led_color::GREEN:
content += "#2D3F2F;";
@ -569,6 +579,84 @@ String processor(const String& var) {
// Close the block
content += "</div>";
#ifdef DOUBLE_BATTERY
content += "<div style='flex: 1; background-color: ";
switch (datalayer.battery2.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 =
static_cast<float>(datalayer.battery2.status.reported_soc) / 100.0; // Convert to float and divide by 100
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.battery2.status.bms_status == ACTIVE) {
content += "<h4>BMS Status: OK </h4>";
} else if (datalayer.battery2.status.bms_status == UPDATING) {
content += "<h4>BMS Status: UPDATING </h4>";
} else {
content += "<h4>BMS 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'>";