diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ab929dc3..e7a31eb0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ ci: repos: - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v20.1.5 + rev: v20.1.7 hooks: - id: clang-format args: [-Werror] # change formatting warnings to errors, hook includes -i (Inplace edit) by default diff --git a/Software/Software.ino b/Software/Software.ino index 655e88ba..b0aa495e 100644 --- a/Software/Software.ino +++ b/Software/Software.ino @@ -38,7 +38,7 @@ volatile unsigned long long bmsResetTimeOffset = 0; // The current software version, shown on webserver -const char* version_number = "8.15.dev"; +const char* version_number = "8.15.0"; // Interval timers volatile unsigned long currentMillis = 0; diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index cb455009..83cbf777 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -27,6 +27,7 @@ //#define KIA_HYUNDAI_HYBRID_BATTERY //#define MEB_BATTERY //#define MG_5_BATTERY +//#define MG_HS_PHEV_BATTERY //#define NISSAN_LEAF_BATTERY //#define ORION_BMS //#define PYLON_BATTERY @@ -118,6 +119,8 @@ /* Connectivity options */ #define WIFI //#define WIFICONFIG //Enable this line to set a static IP address / gateway /subnet mask for the device. see USER_SETTINGS.cpp for the settings +//#define CUSTOM_HOSTNAME \ + "battery-emulator" //Enable this line to use a custom hostname for the device, if disabled the default naming format 'esp32-XXXXXX' will be used. #define WEBSERVER //Enable this line to enable WiFi, and to run the webserver. See USER_SETTINGS.cpp for the Wifi settings. #define WIFIAP //When enabled, the emulator will broadcast its own access point Wifi. Can be used at the same time as a normal Wifi connection to a router. #define MDNSRESPONDER //Enable this line to enable MDNS, allows battery monitor te be found by .local address. Requires WEBSERVER to be enabled. diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index 17e9dff1..0b9e4346 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -33,6 +33,7 @@ void setup_can_shunt(); #include "KIA-HYUNDAI-HYBRID-BATTERY.h" #include "MEB-BATTERY.h" #include "MG-5-BATTERY.h" +#include "MG-HS-PHEV-BATTERY.h" #include "NISSAN-LEAF-BATTERY.h" #include "ORION-BMS.h" #include "PYLON-BATTERY.h" diff --git a/Software/src/battery/BMW-IX-BATTERY.cpp b/Software/src/battery/BMW-IX-BATTERY.cpp index 74797509..434dc3d6 100644 --- a/Software/src/battery/BMW-IX-BATTERY.cpp +++ b/Software/src/battery/BMW-IX-BATTERY.cpp @@ -101,36 +101,6 @@ void BmwIXBattery::update_values() { //This function maps all the values fetche datalayer.battery.info.number_of_cells = detected_number_of_cells; - datalayer_extended.bmwix.min_cell_voltage_data_age = (millis() - min_cell_voltage_lastchanged); - - datalayer_extended.bmwix.max_cell_voltage_data_age = (millis() - max_cell_voltage_lastchanged); - - datalayer_extended.bmwix.T30_Voltage = terminal30_12v_voltage; - - datalayer_extended.bmwix.hvil_status = hvil_status; - - datalayer_extended.bmwix.bms_uptime = sme_uptime; - - datalayer_extended.bmwix.pyro_status_pss1 = pyro_status_pss1; - - datalayer_extended.bmwix.pyro_status_pss4 = pyro_status_pss4; - - datalayer_extended.bmwix.pyro_status_pss6 = pyro_status_pss6; - - datalayer_extended.bmwix.iso_safety_positive = iso_safety_positive; - - datalayer_extended.bmwix.iso_safety_negative = iso_safety_negative; - - datalayer_extended.bmwix.iso_safety_parallel = iso_safety_parallel; - - datalayer_extended.bmwix.allowable_charge_amps = allowable_charge_amps; - - datalayer_extended.bmwix.allowable_discharge_amps = allowable_discharge_amps; - - datalayer_extended.bmwix.balancing_status = balancing_status; - - datalayer_extended.bmwix.battery_voltage_after_contactor = battery_voltage_after_contactor; - if (battery_info_available) { // If we have data from battery - override the defaults to suit datalayer.battery.info.max_design_voltage_dV = max_design_voltage; @@ -493,30 +463,26 @@ void BmwIXBattery::HandleIncomingUserRequest(void) { // Debug user request to open or close the contactors #ifdef DEBUG_LOG logging.print("User request: contactor close: "); - logging.print(datalayer_extended.bmwix.UserRequestContactorClose); + logging.print(userRequestContactorClose); logging.print(" User request: contactor open: "); - logging.println(datalayer_extended.bmwix.UserRequestContactorOpen); + logging.println(userRequestContactorOpen); #endif // DEBUG_LOG - if ((datalayer_extended.bmwix.UserRequestContactorClose == false) && - (datalayer_extended.bmwix.UserRequestContactorOpen == false)) { + if ((userRequestContactorClose == false) && (userRequestContactorOpen == false)) { // do nothing - } else if ((datalayer_extended.bmwix.UserRequestContactorClose == true) && - (datalayer_extended.bmwix.UserRequestContactorOpen == false)) { + } else if ((userRequestContactorClose == true) && (userRequestContactorOpen == false)) { BmwIxCloseContactors(); // set user request to false - datalayer_extended.bmwix.UserRequestContactorClose = false; - } else if ((datalayer_extended.bmwix.UserRequestContactorClose == false) && - (datalayer_extended.bmwix.UserRequestContactorOpen == true)) { + userRequestContactorClose = false; + } else if ((userRequestContactorClose == false) && (userRequestContactorOpen == true)) { BmwIxOpenContactors(); // set user request to false - datalayer_extended.bmwix.UserRequestContactorOpen = false; - } else if ((datalayer_extended.bmwix.UserRequestContactorClose == true) && - (datalayer_extended.bmwix.UserRequestContactorOpen == true)) { + userRequestContactorOpen = false; + } else if ((userRequestContactorClose == true) && (userRequestContactorOpen == true)) { // these flasgs should not be true at the same time, therefore open contactors, as that is the safest state BmwIxOpenContactors(); // set user request to false - datalayer_extended.bmwix.UserRequestContactorClose = false; - datalayer_extended.bmwix.UserRequestContactorOpen = false; + userRequestContactorClose = false; + userRequestContactorOpen = false; // print error, as both these flags shall not be true at the same time #ifdef DEBUG_LOG logging.println( @@ -695,3 +661,50 @@ void BmwIXBattery::HandleBmwIxOpenContactorsRequest(uint16_t counter_10ms) { } } } + +// Getter implementations for HTML renderer +int BmwIXBattery::get_battery_voltage_after_contactor() const { + return battery_voltage_after_contactor; +} +unsigned long BmwIXBattery::get_min_cell_voltage_data_age() const { + return millis() - min_cell_voltage_lastchanged; +} +unsigned long BmwIXBattery::get_max_cell_voltage_data_age() const { + return millis() - max_cell_voltage_lastchanged; +} +int BmwIXBattery::get_T30_Voltage() const { + return terminal30_12v_voltage; +} +int BmwIXBattery::get_balancing_status() const { + return balancing_status; +} +int BmwIXBattery::get_hvil_status() const { + return hvil_status; +} +unsigned long BmwIXBattery::get_bms_uptime() const { + return sme_uptime; +} +int BmwIXBattery::get_allowable_charge_amps() const { + return allowable_charge_amps; +} +int BmwIXBattery::get_allowable_discharge_amps() const { + return allowable_discharge_amps; +} +int BmwIXBattery::get_iso_safety_positive() const { + return iso_safety_positive; +} +int BmwIXBattery::get_iso_safety_negative() const { + return iso_safety_negative; +} +int BmwIXBattery::get_iso_safety_parallel() const { + return iso_safety_parallel; +} +int BmwIXBattery::get_pyro_status_pss1() const { + return pyro_status_pss1; +} +int BmwIXBattery::get_pyro_status_pss4() const { + return pyro_status_pss4; +} +int BmwIXBattery::get_pyro_status_pss6() const { + return pyro_status_pss6; +} diff --git a/Software/src/battery/BMW-IX-BATTERY.h b/Software/src/battery/BMW-IX-BATTERY.h index f7ef924a..af11a73e 100644 --- a/Software/src/battery/BMW-IX-BATTERY.h +++ b/Software/src/battery/BMW-IX-BATTERY.h @@ -11,6 +11,8 @@ class BmwIXBattery : public CanBattery { public: + BmwIXBattery() : renderer(*this) {} + virtual void setup(void); virtual void handle_incoming_can_frame(CAN_frame rx_frame); virtual void update_values(); @@ -19,12 +21,32 @@ class BmwIXBattery : public CanBattery { bool supports_contactor_close() { return true; } - void request_open_contactors() { datalayer_extended.bmwix.UserRequestContactorOpen = true; } - void request_close_contactors() { datalayer_extended.bmwix.UserRequestContactorClose = true; } + void request_open_contactors() { userRequestContactorOpen = true; } + void request_close_contactors() { userRequestContactorClose = true; } static constexpr const char* Name = "BMW iX and i4-7 platform"; + // Getter methods for HTML renderer + int get_battery_voltage_after_contactor() const; + unsigned long get_min_cell_voltage_data_age() const; + unsigned long get_max_cell_voltage_data_age() const; + int get_T30_Voltage() const; + int get_balancing_status() const; + int get_hvil_status() const; + unsigned long get_bms_uptime() const; + int get_allowable_charge_amps() const; + int get_allowable_discharge_amps() const; + int get_iso_safety_positive() const; + int get_iso_safety_negative() const; + int get_iso_safety_parallel() const; + int get_pyro_status_pss1() const; + int get_pyro_status_pss4() const; + int get_pyro_status_pss6() const; + private: + bool userRequestContactorClose = false; + bool userRequestContactorOpen = false; + BmwIXHtmlRenderer renderer; static const int MAX_PACK_VOLTAGE_DV = 4650; //4650 = 465.0V static const int MIN_PACK_VOLTAGE_DV = 3000; diff --git a/Software/src/battery/BMW-IX-HTML.cpp b/Software/src/battery/BMW-IX-HTML.cpp new file mode 100644 index 00000000..60802c49 --- /dev/null +++ b/Software/src/battery/BMW-IX-HTML.cpp @@ -0,0 +1,120 @@ +#include "BMW-IX-HTML.h" +#include "../include.h" +#include "BMW-IX-BATTERY.h" + +String BmwIXHtmlRenderer::get_status_html() { + String content; + + content += "

Battery Voltage after Contactor: " + String(batt.get_battery_voltage_after_contactor()) + " dV

"; + content += "

Max Design Voltage: " + String(datalayer.battery.info.max_design_voltage_dV) + " dV

"; + content += "

Min Design Voltage: " + String(datalayer.battery.info.min_design_voltage_dV) + " dV

"; + content += "

Max Cell Design Voltage: " + String(datalayer.battery.info.max_cell_voltage_mV) + " mV

"; + content += "

Min Cell Design Voltage: " + String(datalayer.battery.info.min_cell_voltage_mV) + " mV

"; + content += "

Min Cell Voltage Data Age: " + String(batt.get_min_cell_voltage_data_age()) + " ms

"; + content += "

Max Cell Voltage Data Age: " + String(batt.get_max_cell_voltage_data_age()) + " ms

"; + content += "

Allowed Discharge Power: " + String(datalayer.battery.status.max_discharge_power_W) + " W

"; + content += "

Allowed Charge Power: " + String(datalayer.battery.status.max_charge_power_W) + " W

"; + content += "

T30 Terminal Voltage: " + String(batt.get_T30_Voltage()) + " mV

"; + content += "

Detected Cell Count: " + String(datalayer.battery.info.number_of_cells) + "

"; + content += "

Balancing: "; + switch (batt.get_balancing_status()) { + case 0: + content += "0 No balancing mode active

"; + break; + case 1: + content += "1 Voltage-Controlled Balancing Mode"; + break; + case 2: + content += "2 Time-Controlled Balancing Mode with Demand Calculation at End of Charging"; + break; + case 3: + content += "3 Time-Controlled Balancing Mode with Demand Calculation at Resting Voltage"; + break; + case 4: + content += "4 No balancing mode active, qualifier invalid"; + break; + default: + content += "Unknown"; + } + content += "

HVIL Status: "; + switch (batt.get_hvil_status()) { + case 0: + content += "Error (Loop Open)

"; + break; + case 1: + content += "OK (Loop Closed)"; + break; + default: + content += "Unknown"; + } + content += "

BMS Uptime: " + String(batt.get_bms_uptime()) + " seconds

"; + content += "

BMS Allowed Charge Amps: " + String(batt.get_allowable_charge_amps()) + " A

"; + content += "

BMS Allowed Disharge Amps: " + String(batt.get_allowable_discharge_amps()) + " A

"; + content += "
"; + content += "

HV Isolation (2147483647kOhm = maximum/invalid)

"; + content += "

Isolation Positive: " + String(batt.get_iso_safety_positive()) + " kOhm

"; + content += "

Isolation Negative: " + String(batt.get_iso_safety_negative()) + " kOhm

"; + content += "

Isolation Parallel: " + String(batt.get_iso_safety_parallel()) + " kOhm

"; + content += "

Pyro Status PSS1: "; + switch (batt.get_pyro_status_pss1()) { + case 0: + content += "0 Value Invalid

"; + break; + case 1: + content += "1 Successfully Blown"; + break; + case 2: + content += "2 Disconnected"; + break; + case 3: + content += "3 Not Activated - Pyro Intact"; + break; + case 4: + content += "4 Unknown"; + break; + default: + content += "Unknown"; + } + content += "

Pyro Status PSS4: "; + switch (batt.get_pyro_status_pss4()) { + case 0: + content += "0 Value Invalid

"; + break; + case 1: + content += "1 Successfully Blown"; + break; + case 2: + content += "2 Disconnected"; + break; + case 3: + content += "3 Not Activated - Pyro Intact"; + break; + case 4: + content += "4 Unknown"; + break; + default: + content += "Unknown"; + } + content += "

Pyro Status PSS6: "; + switch (batt.get_pyro_status_pss6()) { + case 0: + content += "0 Value Invalid

"; + break; + case 1: + content += "1 Successfully Blown"; + break; + case 2: + content += "2 Disconnected"; + break; + case 3: + content += "3 Not Activated - Pyro Intact"; + break; + case 4: + content += "4 Unknown"; + break; + default: + content += "Unknown"; + } + + return content; +} diff --git a/Software/src/battery/BMW-IX-HTML.h b/Software/src/battery/BMW-IX-HTML.h index 27bd3d56..aada8182 100644 --- a/Software/src/battery/BMW-IX-HTML.h +++ b/Software/src/battery/BMW-IX-HTML.h @@ -2,132 +2,18 @@ #define _BMW_IX_HTML_H #include "../datalayer/datalayer.h" -#include "../datalayer/datalayer_extended.h" #include "src/devboard/webserver/BatteryHtmlRenderer.h" +class BmwIXBattery; + class BmwIXHtmlRenderer : public BatteryHtmlRenderer { + private: + BmwIXBattery& batt; + public: - String get_status_html() { - String content; + BmwIXHtmlRenderer(BmwIXBattery& b) : batt(b) {} - content += - "

Battery Voltage after Contactor: " + String(datalayer_extended.bmwix.battery_voltage_after_contactor) + - " dV

"; - content += "

Max Design Voltage: " + String(datalayer.battery.info.max_design_voltage_dV) + " dV

"; - content += "

Min Design Voltage: " + String(datalayer.battery.info.min_design_voltage_dV) + " dV

"; - content += "

Max Cell Design Voltage: " + String(datalayer.battery.info.max_cell_voltage_mV) + " mV

"; - content += "

Min Cell Design Voltage: " + String(datalayer.battery.info.min_cell_voltage_mV) + " mV

"; - content += - "

Min Cell Voltage Data Age: " + String(datalayer_extended.bmwix.min_cell_voltage_data_age) + " ms

"; - content += - "

Max Cell Voltage Data Age: " + String(datalayer_extended.bmwix.max_cell_voltage_data_age) + " ms

"; - content += "

Allowed Discharge Power: " + String(datalayer.battery.status.max_discharge_power_W) + " W

"; - content += "

Allowed Charge Power: " + String(datalayer.battery.status.max_charge_power_W) + " W

"; - content += "

T30 Terminal Voltage: " + String(datalayer_extended.bmwix.T30_Voltage) + " mV

"; - content += "

Detected Cell Count: " + String(datalayer.battery.info.number_of_cells) + "

"; - content += "

Balancing: "; - switch (datalayer_extended.bmwix.balancing_status) { - case 0: - content += "0 No balancing mode active

"; - break; - case 1: - content += "1 Voltage-Controlled Balancing Mode"; - break; - case 2: - content += "2 Time-Controlled Balancing Mode with Demand Calculation at End of Charging"; - break; - case 3: - content += "3 Time-Controlled Balancing Mode with Demand Calculation at Resting Voltage"; - break; - case 4: - content += "4 No balancing mode active, qualifier invalid"; - break; - default: - content += "Unknown"; - } - content += "

HVIL Status: "; - switch (datalayer_extended.bmwix.hvil_status) { - case 0: - content += "Error (Loop Open)

"; - break; - case 1: - content += "OK (Loop Closed)"; - break; - default: - content += "Unknown"; - } - content += "

BMS Uptime: " + String(datalayer_extended.bmwix.bms_uptime) + " seconds

"; - content += "

BMS Allowed Charge Amps: " + String(datalayer_extended.bmwix.allowable_charge_amps) + " A

"; - content += - "

BMS Allowed Disharge Amps: " + String(datalayer_extended.bmwix.allowable_discharge_amps) + " A

"; - content += "
"; - content += "

HV Isolation (2147483647kOhm = maximum/invalid)

"; - content += "

Isolation Positive: " + String(datalayer_extended.bmwix.iso_safety_positive) + " kOhm

"; - content += "

Isolation Negative: " + String(datalayer_extended.bmwix.iso_safety_negative) + " kOhm

"; - content += "

Isolation Parallel: " + String(datalayer_extended.bmwix.iso_safety_parallel) + " kOhm

"; - content += "

Pyro Status PSS1: "; - switch (datalayer_extended.bmwix.pyro_status_pss1) { - case 0: - content += "0 Value Invalid

"; - break; - case 1: - content += "1 Successfully Blown"; - break; - case 2: - content += "2 Disconnected"; - break; - case 3: - content += "3 Not Activated - Pyro Intact"; - break; - case 4: - content += "4 Unknown"; - break; - default: - content += "Unknown"; - } - content += "

Pyro Status PSS4: "; - switch (datalayer_extended.bmwix.pyro_status_pss4) { - case 0: - content += "0 Value Invalid

"; - break; - case 1: - content += "1 Successfully Blown"; - break; - case 2: - content += "2 Disconnected"; - break; - case 3: - content += "3 Not Activated - Pyro Intact"; - break; - case 4: - content += "4 Unknown"; - break; - default: - content += "Unknown"; - } - content += "

Pyro Status PSS6: "; - switch (datalayer_extended.bmwix.pyro_status_pss6) { - case 0: - content += "0 Value Invalid

"; - break; - case 1: - content += "1 Successfully Blown"; - break; - case 2: - content += "2 Disconnected"; - break; - case 3: - content += "3 Not Activated - Pyro Intact"; - break; - case 4: - content += "4 Unknown"; - break; - default: - content += "Unknown"; - } - - return content; - } + String get_status_html(); }; #endif diff --git a/Software/src/battery/BOLT-AMPERA-BATTERY.cpp b/Software/src/battery/BOLT-AMPERA-BATTERY.cpp index 2cb13d95..cc44ff4d 100644 --- a/Software/src/battery/BOLT-AMPERA-BATTERY.cpp +++ b/Software/src/battery/BOLT-AMPERA-BATTERY.cpp @@ -11,12 +11,12 @@ TODOs left for this implementation - Current implementation only seems to get the 7E7 polls working. - We might need to poll on 7E6 also? -- The values missing for a working implementation is: -- SOC% missing! This is absolutely mandatory to fix before starting to use this! -- Capacity (kWh) (can be estimated) -- Charge max power (can be estimated) -- Discharge max power (can be estimated) -- SOH% (low prio)) +- The values missing for a fully working implementation is: +- SOC% missing! (now estimated based on voltage) +- Capacity (kWh) (now estimated) +- Charge max power (now estimated) +- Discharge max power (now estimated) +- SOH% (now hardcoded to 99%) */ /*TODO, messages we might need to send towards the battery to keep it happy and close contactors @@ -39,24 +39,85 @@ TODOs left for this implementation 0x460 Energy Storage System Temp HV (Who sends this? Battery?) */ +// Define the data points for %SOC depending on pack voltage +const uint8_t numEntries = 28; +const uint16_t SOC[28] = {10000, 9985, 9970, 9730, 9490, 8980, 8470, 8110, 7750, 7270, 6790, 6145, 5500, 5200, + 4900, 4405, 3910, 3455, 3000, 2640, 2280, 1940, 1600, 1040, 480, 240, 120, 0}; + +const uint16_t voltage_lookup[28] = { //403 V fully charged, 335V empty + 4032, 4005, 3978, 3951, 3924, 3897, 3870, 3843, 3816, 3789, 3762, 3735, 3708, 3681, + 3654, 3627, 3600, 3573, 3546, 3519, 3492, 3465, 3438, 3411, 3384, 3357, 3350, 3350}; + +static uint16_t estimateSOC(uint16_t packVoltage) { // Linear interpolation function + if (packVoltage >= voltage_lookup[0]) { + return SOC[0]; + } + if (packVoltage <= voltage_lookup[numEntries - 1]) { + return SOC[numEntries - 1]; + } + + for (int i = 1; i < numEntries; ++i) { + if (packVoltage >= voltage_lookup[i]) { + double t = (packVoltage - voltage_lookup[i]) / (voltage_lookup[i - 1] - voltage_lookup[i]); + return SOC[i] + t * (SOC[i - 1] - SOC[i]); + } + } + return 0; // Default return for safety, should never reach here +} + +void findMinMaxCells(const uint16_t arr[], size_t size, uint16_t& Minimum_Cell_Voltage, + uint16_t& Maximum_Cell_Voltage) { + Minimum_Cell_Voltage = std::numeric_limits::max(); + Maximum_Cell_Voltage = 0; + bool foundValidValue = false; + + for (size_t i = 0; i < size; ++i) { + if (arr[i] != 0) { // Skip zero values + if (arr[i] < Minimum_Cell_Voltage) + Minimum_Cell_Voltage = arr[i]; + if (arr[i] > Maximum_Cell_Voltage) + Maximum_Cell_Voltage = arr[i]; + foundValidValue = true; + } + } + + // If all values were zero, set min and max to 3700 + if (!foundValidValue) { + Minimum_Cell_Voltage = 3700; + Maximum_Cell_Voltage = 3700; + } +} + void BoltAmperaBattery::update_values() { //This function maps all the values fetched via CAN to the battery datalayer - datalayer.battery.status.real_soc = battery_SOC_display; + //datalayer.battery.status.real_soc = battery_SOC_display; //TODO: this poll does not work + + datalayer.battery.status.real_soc = estimateSOC(((battery_voltage_periodic / 8) * 10)); //datalayer.battery.status.voltage_dV = battery_voltage * 0.52; - datalayer.battery.status.voltage_dV = (battery_voltage_periodic / 8) * 10; + datalayer.battery.status.voltage_dV = ((battery_voltage_periodic / 8) * 10); - datalayer.battery.status.current_dA = battery_current_7E7; + datalayer.battery.status.current_dA = battery_current_7E7 / 2; - datalayer.battery.info.total_capacity_Wh; + datalayer.battery.status.remaining_capacity_Wh = static_cast( + (static_cast(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh); - datalayer.battery.status.remaining_capacity_Wh; + datalayer.battery.status.soh_pptt = 9900; //TODO: Fix me when real SOH% value has been found - datalayer.battery.status.soh_pptt; + // Charge power is set in .h file (TODO: Remove this estimation when real value has been found) + if (datalayer.battery.status.real_soc > 9900) { + datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_WHEN_TOPBALANCING_W; + } else if (datalayer.battery.status.real_soc > RAMPDOWN_SOC) { + // When real SOC is between RAMPDOWN_SOC-99%, ramp the value between Max<->0 + datalayer.battery.status.max_charge_power_W = + MAX_CHARGE_POWER_ALLOWED_W * + (1 - (datalayer.battery.status.real_soc - RAMPDOWN_SOC) / (10000.0 - RAMPDOWN_SOC)); + } else { // No limits, max charging power allowed + datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_ALLOWED_W; + } - datalayer.battery.status.max_discharge_power_W; - - datalayer.battery.status.max_charge_power_W; + // Discharge power is also set in .h file (TODO: Remove this estimation when real value has been found) + datalayer.battery.status.max_discharge_power_W = MAX_DISCHARGE_POWER_ALLOWED_W; // Store temperatures in an array int16_t temperatures[] = {temperature_1, temperature_2, temperature_3, temperature_4, temperature_5, temperature_6}; @@ -82,6 +143,14 @@ void BoltAmperaBattery::update_values() { //This function maps all the values f //Map all cell voltages to the global array memcpy(datalayer.battery.status.cell_voltages_mV, battery_cell_voltages, 96 * sizeof(uint16_t)); + //Find min and max cellvoltage from the array + findMinMaxCells(battery_cell_voltages, datalayer.battery.info.number_of_cells, Minimum_Cell_Voltage, + Maximum_Cell_Voltage); + + datalayer.battery.status.cell_max_voltage_mV = Maximum_Cell_Voltage; + + datalayer.battery.status.cell_min_voltage_mV = Minimum_Cell_Voltage; + // Update webserver datalayer datalayer_extended.boltampera.battery_5V_ref = battery_5V_ref; datalayer_extended.boltampera.battery_module_temp_1 = battery_module_temp_1; @@ -647,6 +716,7 @@ void BoltAmperaBattery::setup(void) { // Performs one time setup at startup strncpy(datalayer.system.info.battery_protocol, Name, 63); datalayer.system.info.battery_protocol[63] = '\0'; datalayer.battery.info.number_of_cells = 96; + datalayer.battery.info.total_capacity_Wh = 64000; datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV; datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV; datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV; diff --git a/Software/src/battery/BOLT-AMPERA-BATTERY.h b/Software/src/battery/BOLT-AMPERA-BATTERY.h index e15b291c..5211abe3 100644 --- a/Software/src/battery/BOLT-AMPERA-BATTERY.h +++ b/Software/src/battery/BOLT-AMPERA-BATTERY.h @@ -23,6 +23,14 @@ class BoltAmperaBattery : public CanBattery { private: BoltAmperaHtmlRenderer renderer; + uint16_t Maximum_Cell_Voltage = 3700; + uint16_t Minimum_Cell_Voltage = 3700; + static const int MAX_DISCHARGE_POWER_ALLOWED_W = 5000; + static const int MAX_CHARGE_POWER_ALLOWED_W = 5000; + static const int MAX_CHARGE_POWER_WHEN_TOPBALANCING_W = 500; + static const int RAMPDOWN_SOC = + 9000; // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00% + static const int MAX_PACK_VOLTAGE_DV = 4150; //5000 = 500.0V static const int MIN_PACK_VOLTAGE_DV = 2500; static const int MAX_CELL_DEVIATION_MV = 150; diff --git a/Software/src/battery/Battery.h b/Software/src/battery/Battery.h index d41277f8..aa8958ac 100644 --- a/Software/src/battery/Battery.h +++ b/Software/src/battery/Battery.h @@ -43,6 +43,7 @@ enum class BatteryType { TestFake = 34, VolvoSpa = 35, VolvoSpaHybrid = 36, + MgHsPhev = 37, Highest }; diff --git a/Software/src/battery/MG-HS-PHEV-BATTERY.cpp b/Software/src/battery/MG-HS-PHEV-BATTERY.cpp new file mode 100644 index 00000000..6f05e49b --- /dev/null +++ b/Software/src/battery/MG-HS-PHEV-BATTERY.cpp @@ -0,0 +1,363 @@ +#include "../include.h" +#ifdef MG_HS_PHEV_BATTERY_H +#include "../communication/can/comm_can.h" +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" +#include "MG-HS-PHEV-BATTERY.h" + +/* TODO: +- Get contactor closing working +- Figure out which CAN messages need to be sent towards the battery to keep it alive +- Map all values from battery CAN messages +- Note: Charge power/discharge power is estimated for now + +# row3 pin2 needs strobing to 12V (via a 1k) to wake up the BMU +# but contactor won't come on until deasserted +# BMU goes to sleep after after ~18s of no CAN +*/ + +void MgHsPHEVBattery:: + update_values() { //This function maps all the values fetched via CAN to the correct parameters used for modbus + + datalayer.battery.status.real_soc; + + datalayer.battery.status.voltage_dV; + + datalayer.battery.status.current_dA; + + datalayer.battery.info.total_capacity_Wh; + + datalayer.battery.status.remaining_capacity_Wh; + + datalayer.battery.status.max_discharge_power_W; + + datalayer.battery.status.max_charge_power_W; + + datalayer.battery.status.temperature_min_dC; + + datalayer.battery.status.temperature_max_dC; +} + +void MgHsPHEVBattery::handle_incoming_can_frame(CAN_frame rx_frame) { + switch (rx_frame.ID) { + case 0x171: //Following messages were detected on a MG5 battery BMS + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x172: + break; + case 0x173: + break; + case 0x293: + break; + case 0x295: + break; + case 0x297: + break; + case 0x29B: //This ID is on the MG HS RX WITHOUT ANY TX PRESENT + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x29C: + break; + case 0x2A0: + break; + case 0x2A2: //This ID is on the MG HS RX WITHOUT ANY TX PRESENT + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x322: + break; + case 0x334: + break; + case 0x33F: + break; + case 0x391: //This ID is on the MG HS RX WITHOUT ANY TX PRESENT + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x393: + break; + case 0x3AB: //This ID is on the MG HS RX WITHOUT ANY TX PRESENT + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x3AC: //This ID is on the MG HS RX WITHOUT ANY TX PRESENT + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x3B8: + break; + case 0x3BA: + break; + case 0x3BC: + break; + case 0x3BE: + break; + case 0x3C0: + break; + case 0x3C2: + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x400: + break; + case 0x402: + break; + case 0x418: + break; + case 0x44C: //This ID is on the MG HS RX WITHOUT ANY TX PRESENT + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x620: + break; //This is the last on the list in the MG5 template. + case 0x3a8: //This ID is on the MG HS RX WITHOUT ANY TX PRESENT + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x508: //This ID is a new one MG HS RX WHEN TRANSMITTING 03 22 B0 41 00 00 00 00. Rx data is 00 00 00 00 00 00 00 00 + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + break; + case 0x7ED: //This ID is the battery BMS + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + //Process rx data for incoming responses to "Read data by ID" Tx + if (rx_frame.data.u8[1] == 0x62) { + if (rx_frame.data.u8[2] == 0xB0) { //Battery information + if (rx_frame.data.u8[3] == 0x41 && rx_frame.data.u8[0] == 0x05) { // Battery bus voltage + // Serial.print ("Battery Bus voltage frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + // datalayer.battery.status.PARAMETER = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]) * 2.5; + // Serial.print ("Battery Bus Voltage = "); + // Serial.println (datalayer.battery.status.PARAMETER); + } + if (rx_frame.data.u8[3] == 0x42 && rx_frame.data.u8[0] == 0x05) { // Battery voltage + // Serial.print ("Battery voltage frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + datalayer.battery.status.voltage_dV = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]) * 2.5; + // Serial.print ("Battery voltage = "); + // Serial.println (datalayer.battery.status.voltage_dV); + } + if (rx_frame.data.u8[3] == 0x43 && rx_frame.data.u8[0] == 0x05) { // Battery current + // Serial.print ("Battery current frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + datalayer.battery.status.current_dA = ((rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]) - 40000) / -4; + // Serial.print ("Battery current = "); + // Serial.println (datalayer.battery.status.current_dA); + } + + if (rx_frame.data.u8[3] == 0x45 && rx_frame.data.u8[0] == 0x05) { // Battery resistance + // Serial.print ("Battery resistance frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + // datalayer.battery.status.PARAMETER = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]); + // Serial.print ("Battery resistance = "); + // Serial.println (datalayer.battery.status.PARAMETER); + } + if (rx_frame.data.u8[3] == 0x46 && rx_frame.data.u8[0] == 0x05) { // Battery SoC + // Serial.print ("Battery SoC frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + datalayer.battery.status.real_soc = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]) * 10; + // Serial.print ("Battery SoC = "); + // Serial.println (datalayer.battery.status.real_soc); + RealSoC = datalayer.battery.status.real_soc / 100; // For calculation of charge and discharge rates + } + + if (rx_frame.data.u8[3] == 0x47) { // && rx_frame.data.u8[0] == 0x05) { // BMS error code + // Serial.print ("BMS error code frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + // datalayer.battery.status.PARAMETER = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]); // HOLD: What to do with this data + // Serial.print ("Battery error = "); + // Serial.println (datalayer.battery.status.PARAMETER); + } + + if (rx_frame.data.u8[3] == 0x48) { // && rx_frame.data.u8[0] == 0x05) { // BMS status code + // Serial.print ("BMS status code frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + // datalayer.battery.status.PARAMETER = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]); // HOLD: What to do with this data + // Serial.print ("Battery Status = "); + // Serial.println (datalayer.battery.status.PARAMETER); + } + + if (rx_frame.data.u8[3] == 0x49) { // && rx_frame.data.u8[0] == 0x05) { // System main relay B status + // Serial.print ("System main relay B status frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + // datalayer.battery.status.PARAMETER = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]); // HOLD: What to do with this data + // Serial.print ("Main relay B status = "); + // Serial.println (datalayer.battery.status.PARAMETER); + } + + if (rx_frame.data.u8[3] == 0x4A) { // && rx_frame.data.u8[0] == 0x05) { // System main relay G status + // Serial.print ("System main relay G status frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + // datalayer.battery.status.PARAMETER = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]); // HOLD: What to do with this data + // Serial.print ("Main relay G status = "); + // Serial.println (datalayer.battery.status.PARAMETER); + } + + if (rx_frame.data.u8[3] == 0x52) { // && rx_frame.data.u8[0] == 0x05) { // System main relay P status + // Serial.print ("System main relay P status frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + // datalayer.battery.status.PARAMETER = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]); // HOLD: What to do with this data + // Serial.print ("BMain relay P status = "); + // Serial.println (datalayer.battery.status.PARAMETER); + } + + if (rx_frame.data.u8[3] == 0x56 && rx_frame.data.u8[0] == 0x05) { // Max cell temperature + // Serial.print ("Max cell temperature frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + datalayer.battery.status.temperature_max_dC = + (((rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]) / 500) - 40) * 10; + // Serial.print ("Max cell temperature = "); + // Serial.println (datalayer.battery.status.temperature_max_dC); + } + + if (rx_frame.data.u8[3] == 0x57 && rx_frame.data.u8[0] == 0x05) { // Min cell temperature + // Serial.print ("Min cell temperature frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + datalayer.battery.status.temperature_min_dC = + (((rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]) / 500) - 40) * 10; + // Serial.print ("Min cell temperature = "); + // Serial.println (datalayer.battery.status.temperature_min_dC); + } + + if (rx_frame.data.u8[3] == 0x58 && rx_frame.data.u8[0] == 0x06) { // Max cell voltage + // Serial.print ("Max cell voltage frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + datalayer.battery.status.cell_max_voltage_mV = + (rx_frame.data.u8[4] << 16 | rx_frame.data.u8[5] << 8 | rx_frame.data.u8[6]) / 250; + // Serial.print ("Max cell voltage = "); + // Serial.println (datalayer.battery.status.cell_max_voltage_mV); + } + + if (rx_frame.data.u8[3] == 0x59 && rx_frame.data.u8[0] == 0x06) { // Min cell voltage + // Serial.print ("Min cell voltage frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + datalayer.battery.status.cell_min_voltage_mV = + (rx_frame.data.u8[4] << 16 | rx_frame.data.u8[5] << 8 | rx_frame.data.u8[6]) / 250; + // Serial.print ("Min cell voltage = "); + // Serial.println (datalayer.battery.status.cell_min_voltage_mV); + } + + if (rx_frame.data.u8[3] == 0x61 && rx_frame.data.u8[0] == 0x05) { // Battery SoH + // Serial.print ("Battery SoH frame = "); + // print_can_frame_MG5(rx_frame, frameDirection(MSG_RX)); + datalayer.battery.status.soh_pptt = (rx_frame.data.u8[4] << 8 | rx_frame.data.u8[5]); + // Serial.print ("Battery SoH = "); + // Serial.println (datalayer.battery.status.soh_pptt); + } + + } // data.u8[2]=0xB0 + } // data.u8[1] = 0x62) + + //Set calculated and derived parameters + + // Calculate the remaining capacity. + tempfloat = datalayer.battery.info.total_capacity_Wh * (RealSoC - MinSoC) / 100; + // Serial.print ("Remaining capacity calculated = "); + // Serial.println (tempfloat); + if (tempfloat > 0) { + datalayer.battery.status.remaining_capacity_Wh = tempfloat; + } else { + datalayer.battery.status.remaining_capacity_Wh = 0; + } + + // Calculate the maximum charge power. Taper the charge power between 90% and 100% SoC, as 100% SoC is approached + if (RealSoC < StartChargeTaper) { + datalayer.battery.status.max_charge_power_W = MaxChargePower; + } else if (RealSoC >= 100) { + datalayer.battery.status.max_charge_power_W = TricklePower; + } else { + //Taper the charge to the Trickle value. The shape and start point of the taper is set by the constants + datalayer.battery.status.max_charge_power_W = + (MaxChargePower * pow(((100 - RealSoC) / (100 - StartChargeTaper)), ChargeTaperExponent)) + TricklePower; + } + + // Calculate the maximum discharge power. Taper the discharge power between 35% and Min% SoC, as Min% SoC is approached + if (RealSoC > StartDischargeTaper) { + datalayer.battery.status.max_discharge_power_W = MaxDischargePower; + } else if (RealSoC < MinSoC) { + datalayer.battery.status.max_discharge_power_W = TricklePower; + } else { + //Taper the charge to the Trickle value. The shape and start point of the taper is set by the constants + datalayer.battery.status.max_discharge_power_W = + (MaxDischargePower * pow(((RealSoC - MinSoC) / (StartDischargeTaper - MinSoC)), DischargeTaperExponent)) + + TricklePower; + } + + break; + default: + break; + } +} +void MgHsPHEVBattery::transmit_can(unsigned long currentMillis) { + // Send 70ms CAN Message + if (currentMillis - previousMillis70 >= INTERVAL_70_MS) { + previousMillis70 = currentMillis; + + if (datalayer.battery.status.bms_status == FAULT) { + //Open contactors! + MG_HS_8A.data.u8[5] = 0x00; + } else { // Not in faulted mode, Close contactors! + MG_HS_8A.data.u8[5] = 0x02; + } + + transmit_can_frame(&MG_HS_8A, can_config.battery); + transmit_can_frame(&MG_HS_1F1, can_config.battery); + } + // Send 200ms CAN Message + if (currentMillis - previousMillis200 >= INTERVAL_200_MS) { + previousMillis200 = currentMillis; + + switch (messageindex) { + case 1: + transmit_can_frame(&MG_HS_7E5_B0_42, can_config.battery); //Battery voltage + break; + case 2: + transmit_can_frame(&MG_HS_7E5_B0_43, can_config.battery); //Battery current + break; + case 3: + transmit_can_frame(&MG_HS_7E5_B0_46, can_config.battery); //Battery SoC + break; + case 4: + transmit_can_frame(&MG_HS_7E5_B0_47, can_config.battery); // Get BMS error code + break; + case 5: + transmit_can_frame(&MG_HS_7E5_B0_48, can_config.battery); // Get BMS status + break; + case 6: + transmit_can_frame(&MG_HS_7E5_B0_49, can_config.battery); // Get System main relay B status + break; + case 7: + transmit_can_frame(&MG_HS_7E5_B0_4A, can_config.battery); // Get System main relay G status + break; + case 8: + transmit_can_frame(&MG_HS_7E5_B0_52, can_config.battery); // Get System main relay P status + break; + case 9: + transmit_can_frame(&MG_HS_7E5_B0_56, can_config.battery); //Max cell temperature + break; + case 10: + transmit_can_frame(&MG_HS_7E5_B0_57, can_config.battery); //Min cell temperature + break; + case 11: + transmit_can_frame(&MG_HS_7E5_B0_58, can_config.battery); //Max cell voltage + break; + case 12: + transmit_can_frame(&MG_HS_7E5_B0_59, can_config.battery); //Min cell voltage + break; + case 13: + transmit_can_frame(&MG_HS_7E5_B0_61, can_config.battery); //Battery SoH + messageindex = 0; //Return to the first message index. This goes in the last message entry + break; + default: + break; + + } //switch + + messageindex++; //Increment the message index + + } //endif +} + +void MgHsPHEVBattery::setup(void) { // Performs one time setup at startup + strncpy(datalayer.system.info.battery_protocol, Name, 63); + datalayer.system.info.battery_protocol[63] = '\0'; + datalayer.system.status.battery_allows_contactor_closing = true; + datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV; + datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV; + datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV; + datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV; +} + +#endif diff --git a/Software/src/battery/MG-HS-PHEV-BATTERY.h b/Software/src/battery/MG-HS-PHEV-BATTERY.h new file mode 100644 index 00000000..ba9591c5 --- /dev/null +++ b/Software/src/battery/MG-HS-PHEV-BATTERY.h @@ -0,0 +1,140 @@ +#ifndef MG_HS_PHEV_BATTERY_H +#define MG_HS_PHEV_BATTERY_H +#include +#include "../include.h" + +#include "CanBattery.h" + +#ifdef MG_HS_PHEV_BATTERY +#define SELECTED_BATTERY_CLASS MgHsPHEVBattery +#endif + +class MgHsPHEVBattery : public CanBattery { + public: + virtual void setup(void); + virtual void handle_incoming_can_frame(CAN_frame rx_frame); + virtual void update_values(); + virtual void transmit_can(unsigned long currentMillis); + + static constexpr const char* Name = "MG HS PHEV 16.6kWh battery"; + + private: + static const int MAX_PACK_VOLTAGE_DV = 4040; //5000 = 500.0V + static const int MIN_PACK_VOLTAGE_DV = 3100; + static const int MAX_CELL_DEVIATION_MV = 150; + static const int MAX_CELL_VOLTAGE_MV = 4250; //Battery is put into emergency stop if one cell goes over this value + static const int MIN_CELL_VOLTAGE_MV = 2700; //Battery is put into emergency stop if one cell goes below this value + + unsigned long previousMillis70 = 0; // will store last time a 70ms CAN Message was send + unsigned long previousMillis200 = 0; // will store last time a 200ms CAN Message was send + + int BMS_SOC = 0; + // For calculating charge and discharge power + float RealVoltage; + float RealSoC; + float tempfloat; + + uint8_t messageindex = 0; //For polling switchcase + + const int MaxChargePower = 3000; // Maximum allowable charge power, excluding the taper + const int StartChargeTaper = 90; // Battery percentage above which the charge power will taper to zero + const float ChargeTaperExponent = + 1; // Shape of charge power taper to zero. 1 is linear. >1 reduces quickly and is small at nearly full. + const int TricklePower = 20; // Minimimum trickle charge or discharge power (W) + + const int MaxDischargePower = 4000; // Maximum allowable discharge power, excluding the taper + const int MinSoC = 20; // Minimum SoC allowed + const int StartDischargeTaper = 30; // Battery percentage below which the discharge power will taper to zero + const float DischargeTaperExponent = + 1; // Shape of discharge power taper to zero. 1 is linear. >1 reduces quickly and is small at nearly full. + + CAN_frame MG_HS_8A = {.FD = false, + .ext_ID = false, + .DLC = 8, + .ID = 0x08A, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x36, 0xB0}}; + CAN_frame MG_HS_1F1 = {.FD = false, + .ext_ID = false, + .DLC = 8, + .ID = 0x1F1, + .data = {0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame MG_HS_7E5_B0_42 = {.FD = false, // Get Battery voltage + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x42, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_43 = {.FD = false, // Get Battery current + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x43, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_46 = {.FD = false, // Get Battery SoC + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x46, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_47 = {.FD = false, // Get BMS error code + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x47, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_48 = {.FD = false, // Get BMS status + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x48, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_49 = {.FD = false, // Get System main relay B status + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x49, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_4A = {.FD = false, // Get System main relay G status + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x4A, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_52 = {.FD = false, // Get System main relay P status + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x52, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_56 = {.FD = false, // Get Max cell temperature + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x56, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_57 = {.FD = false, // Get Min call temperature + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x57, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_58 = {.FD = false, // Get Max cell voltage + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x58, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_59 = {.FD = false, // Get Min call voltage + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x59, 0x00, 0x00, 0x00, 0x00}}; + + CAN_frame MG_HS_7E5_B0_61 = {.FD = false, // Get Battery SoH + .ext_ID = false, + .DLC = 8, + .ID = 0x7E5, + .data = {0x03, 0x22, 0xB0, 0x61, 0x00, 0x00, 0x00, 0x00}}; +}; + +#endif diff --git a/Software/src/datalayer/datalayer_extended.h b/Software/src/datalayer/datalayer_extended.h index a78911c0..a758d400 100644 --- a/Software/src/datalayer/datalayer_extended.h +++ b/Software/src/datalayer/datalayer_extended.h @@ -40,34 +40,6 @@ typedef struct { int16_t battery_current_7E4 = 0; } DATALAYER_INFO_BOLTAMPERA; -typedef struct { - /** User requesting contactor open or close via WebUI*/ - bool UserRequestContactorClose = false; - bool UserRequestContactorOpen = false; - /** uint16_t */ - /** Terminal 30 - 12V SME Supply Voltage */ - uint16_t T30_Voltage = 0; - /** Status HVIL, 1 HVIL OK, 0 HVIL disconnected*/ - uint8_t hvil_status = 0; - /** Min/Max Cell SOH*/ - uint16_t min_soh_state = 0; - uint16_t max_soh_state = 0; - uint32_t bms_uptime = 0; - uint8_t pyro_status_pss1 = 0; - uint8_t pyro_status_pss4 = 0; - uint8_t pyro_status_pss6 = 0; - int32_t iso_safety_positive = 0; - int32_t iso_safety_negative = 0; - int32_t iso_safety_parallel = 0; - int32_t allowable_charge_amps = 0; - int32_t allowable_discharge_amps = 0; - int16_t balancing_status = 0; - int16_t battery_voltage_after_contactor = 0; - unsigned long min_cell_voltage_data_age = 0; - unsigned long max_cell_voltage_data_age = 0; - -} DATALAYER_INFO_BMWIX; - typedef struct { /** uint8_t */ /** Status isolation external, 0 not evaluated, 1 OK, 2 error active, 3 Invalid signal*/ @@ -851,7 +823,6 @@ typedef struct { class DataLayerExtended { public: DATALAYER_INFO_BOLTAMPERA boltampera; - DATALAYER_INFO_BMWIX bmwix; DATALAYER_INFO_BMWPHEV bmwphev; DATALAYER_INFO_BYDATTO3 bydAtto3; DATALAYER_INFO_CELLPOWER cellpower; diff --git a/Software/src/devboard/mqtt/mqtt.cpp b/Software/src/devboard/mqtt/mqtt.cpp index 1c88ed7d..b9b783e7 100644 --- a/Software/src/devboard/mqtt/mqtt.cpp +++ b/Software/src/devboard/mqtt/mqtt.cpp @@ -64,6 +64,7 @@ static String device_id = ""; static bool publish_common_info(void); static bool publish_cell_voltages(void); +static bool publish_cell_balancing(void); static bool publish_events(void); /** Publish global values and call callbacks for specific modules */ @@ -86,6 +87,12 @@ static void publish_values(void) { return; } #endif + +#ifdef MQTT_PUBLISH_CELL_VOLTAGES + if (publish_cell_balancing() == false) { + return; + } +#endif } static bool ha_common_info_published = false; @@ -129,7 +136,8 @@ SensorConfig batterySensorConfigTemplate[] = { {"max_discharge_power", "Battery Max Discharge Power", "", "W", "power", always}, {"max_charge_power", "Battery Max Charge Power", "", "W", "power", always}, {"charged_energy", "Battery Charged Energy", "", "Wh", "energy", supports_charged}, - {"discharged_energy", "Battery Discharged Energy", "", "Wh", "energy", supports_charged}}; + {"discharged_energy", "Battery Discharged Energy", "", "Wh", "energy", supports_charged}, + {"balancing_active_cells", "Balancing Active Cells", "", "", "", always}}; SensorConfig globalSensorConfigTemplate[] = {{"bms_status", "BMS Status", "", "", "", always}, {"pause_status", "Pause Status", "", "", "", always}}; @@ -238,6 +246,17 @@ void set_battery_attributes(JsonDocument& doc, const DATALAYER_BATTERY_TYPE& bat doc["discharged_energy" + suffix] = ((float)datalayer.battery.status.total_discharged_battery_Wh); } } + + // Add balancing data + uint16_t active_cells = 0; + if (battery.info.number_of_cells != 0u) { + for (size_t i = 0; i < battery.info.number_of_cells; ++i) { + if (battery.status.cell_balancing_status[i]) { + active_cells++; + } + } + } + doc["balancing_active_cells" + suffix] = active_cells; } static std::vector order_events; @@ -398,6 +417,53 @@ static bool publish_cell_voltages(void) { return true; } +static bool publish_cell_balancing(void) { + static JsonDocument doc; + static String state_topic = topic_name + "/balancing_data"; + static String state_topic_2 = topic_name + "/balancing_data_2"; + + // If cell balancing data is available... + if (datalayer.battery.info.number_of_cells != 0u) { + + JsonArray cell_balancing = doc["cell_balancing"].to(); + for (size_t i = 0; i < datalayer.battery.info.number_of_cells; ++i) { + cell_balancing.add(datalayer.battery.status.cell_balancing_status[i]); + } + + serializeJson(doc, mqtt_msg, sizeof(mqtt_msg)); + + if (!mqtt_publish(state_topic.c_str(), mqtt_msg, false)) { +#ifdef DEBUG_LOG + logging.println("Cell balancing MQTT msg could not be sent"); +#endif // DEBUG_LOG + return false; + } + doc.clear(); + } + + // Handle second battery if available + if (battery2) { + if (datalayer.battery2.info.number_of_cells != 0u) { + + JsonArray cell_balancing = doc["cell_balancing"].to(); + for (size_t i = 0; i < datalayer.battery2.info.number_of_cells; ++i) { + cell_balancing.add(datalayer.battery2.status.cell_balancing_status[i]); + } + + serializeJson(doc, mqtt_msg, sizeof(mqtt_msg)); + + if (!mqtt_publish(state_topic_2.c_str(), mqtt_msg, false)) { +#ifdef DEBUG_LOG + logging.println("Cell balancing MQTT msg could not be sent"); +#endif // DEBUG_LOG + return false; + } + doc.clear(); + } + } + return true; +} + bool publish_events() { static JsonDocument doc; static String state_topic = topic_name + "/events"; diff --git a/Software/src/devboard/webserver/webserver.cpp b/Software/src/devboard/webserver/webserver.cpp index 5f7b862f..33b50596 100644 --- a/Software/src/devboard/webserver/webserver.cpp +++ b/Software/src/devboard/webserver/webserver.cpp @@ -844,6 +844,7 @@ String processor(const String& var) { } content += ""; if (status == WL_CONNECTED) { + content += "

Hostname: " + String(WiFi.getHostname()) + "

"; content += "

IP: " + WiFi.localIP().toString() + "

"; } else { content += "

Wifi state: " + getConnectResultString(status) + "

"; diff --git a/Software/src/devboard/wifi/wifi.cpp b/Software/src/devboard/wifi/wifi.cpp index 57d2dbc1..9899e8c2 100644 --- a/Software/src/devboard/wifi/wifi.cpp +++ b/Software/src/devboard/wifi/wifi.cpp @@ -60,6 +60,10 @@ void init_WiFi() { DEBUG_PRINTF("init_Wifi enabled=%d, apÄ=%d, ssid=%s, password=%s\n", wifi_enabled, wifiap_enabled, ssid.c_str(), password.c_str()); +#ifdef CUSTOM_HOSTNAME + WiFi.setHostname(CUSTOM_HOSTNAME); // Set custom hostname if defined in USER_SETTINGS.h +#endif + if (wifiap_enabled) { WiFi.mode(WIFI_AP_STA); // Simultaneous WiFi AP and Router connection init_WiFi_AP(); @@ -232,6 +236,9 @@ void init_mDNS() { // e.g batteryemulator8C.local where the mac address is 08:F9:E0:D1:06:8C String mac = WiFi.macAddress(); String mdnsHost = "batteryemulator" + mac.substring(mac.length() - 2); +#ifdef CUSTOM_HOSTNAME // If CUSTOM_HOSTNAME is defined, use the same hostname also for mDNS + mdnsHost = CUSTOM_HOSTNAME; +#endif // Initialize mDNS .local resolution if (!MDNS.begin(mdnsHost)) { @@ -240,7 +247,7 @@ void init_mDNS() { #endif } else { // Advertise via bonjour the service so we can auto discover these battery emulators on the local network. - MDNS.addService("battery_emulator", "tcp", 80); + MDNS.addService(mdnsHost, "tcp", 80); } }