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);
}
}