diff --git a/Software/Software.ino b/Software/Software.ino index 2c4654d5..9ccf534f 100644 --- a/Software/Software.ino +++ b/Software/Software.ino @@ -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(); diff --git a/Software/USER_SETTINGS.cpp b/Software/USER_SETTINGS.cpp index c35fee69..7975ecac 100644 --- a/Software/USER_SETTINGS.cpp +++ b/Software/USER_SETTINGS.cpp @@ -14,9 +14,9 @@ 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* ssidAP = "Battery Emulator"; // Maximum of 63 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 diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index 5537a05c..83324670 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -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) diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index ef35110c..c29feb08 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -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 diff --git a/Software/src/battery/BMW-I3-BATTERY.cpp b/Software/src/battery/BMW-I3-BATTERY.cpp index 54172cee..604284dd 100644 --- a/Software/src/battery/BMW-I3-BATTERY.cpp +++ b/Software/src/battery/BMW-I3-BATTERY.cpp @@ -7,24 +7,26 @@ #include "BMW-I3-BATTERY.h" /* Do not change code below unless you are sure what you are doing */ -static unsigned long previousMillis20 = 0; // will store last time a 20ms CAN Message was send -static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send -static unsigned long previousMillis200 = 0; // will store last time a 200ms CAN Message was send -static unsigned long previousMillis500 = 0; // will store last time a 500ms CAN Message was send -static unsigned long previousMillis640 = 0; // will store last time a 600ms CAN Message was send -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 -static uint8_t CANstillAlive = 12; // counter for checking if CAN 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; +static unsigned long previousMillis20 = 0; // will store last time a 20ms CAN Message was send +static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send +static unsigned long previousMillis200 = 0; // will store last time a 200ms CAN Message was send +static unsigned long previousMillis500 = 0; // will store last time a 500ms CAN Message was send +static unsigned long previousMillis640 = 0; // will store last time a 600ms CAN Message was send +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 +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 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,7 +973,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) { @@ -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 } diff --git a/Software/src/battery/BMW-I3-BATTERY.h b/Software/src/battery/BMW-I3-BATTERY.h index 73690635..7a7feb81 100644 --- a/Software/src/battery/BMW-I3-BATTERY.h +++ b/Software/src/battery/BMW-I3-BATTERY.h @@ -3,6 +3,9 @@ #include #include "../include.h" #include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" +#include "../lib/pierremolinaro-acan2515/ACAN2515.h" + +extern ACAN2515 can; #define BATTERY_SELECTED diff --git a/Software/src/datalayer/datalayer.h b/Software/src/datalayer/datalayer.h index 4c44ccfc..938bb431 100644 --- a/Software/src/datalayer/datalayer.h +++ b/Software/src/datalayer/datalayer.h @@ -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; }; diff --git a/Software/src/devboard/utils/events.cpp b/Software/src/devboard/utils/events.cpp index 0ff59ae1..6dda684f 100644 --- a/Software/src/devboard/utils/events.cpp +++ b/Software/src/devboard/utils/events.cpp @@ -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: diff --git a/Software/src/devboard/utils/events.h b/Software/src/devboard/utils/events.h index 5058e4a9..954169fc 100644 --- a/Software/src/devboard/utils/events.h +++ b/Software/src/devboard/utils/events.h @@ -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) \ diff --git a/Software/src/devboard/webserver/webserver.cpp b/Software/src/devboard/webserver/webserver.cpp index 4c2f5a75..13555f08 100644 --- a/Software/src/devboard/webserver/webserver.cpp +++ b/Software/src/devboard/webserver/webserver.cpp @@ -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 += ""; @@ -483,8 +486,15 @@ String processor(const String& var) { // Close the block content += ""; +#ifdef DOUBLE_BATTERY + // Start a new block with a specific background color. Color changes depending on BMS status + content += "
"; + content += "
(datalayer.battery2.status.real_soc) / 100.0; // Convert to float and divide by 100 + socScaledFloat = + static_cast(datalayer.battery2.status.reported_soc) / 100.0; // Convert to float and divide by 100 + sohFloat = static_cast(datalayer.battery2.status.soh_pptt) / 100.0; // Convert to float and divide by 100 + voltageFloat = + static_cast(datalayer.battery2.status.voltage_dV) / 10.0; // Convert to float and divide by 10 + currentFloat = + static_cast(datalayer.battery2.status.current_dA) / 10.0; // Convert to float and divide by 10 + powerFloat = static_cast(datalayer.battery2.status.active_power_W); // Convert to float + tempMaxFloat = static_cast(datalayer.battery2.status.temperature_max_dC) / 10.0; // Convert to float + tempMinFloat = static_cast(datalayer.battery2.status.temperature_min_dC) / 10.0; // Convert to float + + content += "

Real SOC: " + String(socRealFloat, 2) + "

"; + content += "

Scaled SOC: " + String(socScaledFloat, 2) + "

"; + content += "

SOH: " + String(sohFloat, 2) + "

"; + content += "

Voltage: " + String(voltageFloat, 1) + " V

"; + content += "

Current: " + String(currentFloat, 1) + " A

"; + 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 += "

Cell max: " + String(datalayer.battery2.status.cell_max_voltage_mV) + " mV

"; + content += "

Cell min: " + String(datalayer.battery2.status.cell_min_voltage_mV) + " mV

"; + content += "

Temperature max: " + String(tempMaxFloat, 1) + " C

"; + content += "

Temperature min: " + String(tempMinFloat, 1) + " C

"; + if (datalayer.battery2.status.bms_status == ACTIVE) { + content += "

BMS Status: OK

"; + } else if (datalayer.battery2.status.bms_status == UPDATING) { + content += "

BMS Status: UPDATING

"; + } else { + content += "

BMS Status: FAULT

"; + } + if (datalayer.battery2.status.current_dA == 0) { + content += "

Battery idle

"; + } else if (datalayer.battery2.status.current_dA < 0) { + content += "

Battery discharging!

"; + } else { // > 0 + content += "

Battery charging!

"; + } + + content += "

Automatic contactor closing allowed:

"; + content += "

Battery: "; + if (datalayer.system.status.battery2_allows_contactor_closing == true) { + content += ""; + } else { + content += ""; + } + + content += " Inverter: "; + if (datalayer.system.status.inverter_allows_contactor_closing == true) { + content += "

"; + } else { + content += ""; + } + + content += "
"; + content += "
"; +#endif + #if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER // Start a new block with orange background color content += "
";