Merge from main and fix conflict

This commit is contained in:
Jaakko Haakana 2025-07-11 17:26:09 +03:00
commit 0b3bc738ac
17 changed files with 887 additions and 215 deletions

View file

@ -10,7 +10,7 @@ ci:
repos: repos:
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.5 rev: v20.1.7
hooks: hooks:
- id: clang-format - id: clang-format
args: [-Werror] # change formatting warnings to errors, hook includes -i (Inplace edit) by default args: [-Werror] # change formatting warnings to errors, hook includes -i (Inplace edit) by default

View file

@ -38,7 +38,7 @@
volatile unsigned long long bmsResetTimeOffset = 0; volatile unsigned long long bmsResetTimeOffset = 0;
// The current software version, shown on webserver // The current software version, shown on webserver
const char* version_number = "8.15.dev"; const char* version_number = "8.15.0";
// Interval timers // Interval timers
volatile unsigned long currentMillis = 0; volatile unsigned long currentMillis = 0;

View file

@ -27,6 +27,7 @@
//#define KIA_HYUNDAI_HYBRID_BATTERY //#define KIA_HYUNDAI_HYBRID_BATTERY
//#define MEB_BATTERY //#define MEB_BATTERY
//#define MG_5_BATTERY //#define MG_5_BATTERY
//#define MG_HS_PHEV_BATTERY
//#define NISSAN_LEAF_BATTERY //#define NISSAN_LEAF_BATTERY
//#define ORION_BMS //#define ORION_BMS
//#define PYLON_BATTERY //#define PYLON_BATTERY
@ -118,6 +119,8 @@
/* Connectivity options */ /* Connectivity options */
#define WIFI #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 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 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 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. #define MDNSRESPONDER //Enable this line to enable MDNS, allows battery monitor te be found by .local address. Requires WEBSERVER to be enabled.

View file

@ -33,6 +33,7 @@ void setup_can_shunt();
#include "KIA-HYUNDAI-HYBRID-BATTERY.h" #include "KIA-HYUNDAI-HYBRID-BATTERY.h"
#include "MEB-BATTERY.h" #include "MEB-BATTERY.h"
#include "MG-5-BATTERY.h" #include "MG-5-BATTERY.h"
#include "MG-HS-PHEV-BATTERY.h"
#include "NISSAN-LEAF-BATTERY.h" #include "NISSAN-LEAF-BATTERY.h"
#include "ORION-BMS.h" #include "ORION-BMS.h"
#include "PYLON-BATTERY.h" #include "PYLON-BATTERY.h"

View file

@ -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.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 (battery_info_available) {
// If we have data from battery - override the defaults to suit // If we have data from battery - override the defaults to suit
datalayer.battery.info.max_design_voltage_dV = max_design_voltage; 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 // Debug user request to open or close the contactors
#ifdef DEBUG_LOG #ifdef DEBUG_LOG
logging.print("User request: contactor close: "); logging.print("User request: contactor close: ");
logging.print(datalayer_extended.bmwix.UserRequestContactorClose); logging.print(userRequestContactorClose);
logging.print(" User request: contactor open: "); logging.print(" User request: contactor open: ");
logging.println(datalayer_extended.bmwix.UserRequestContactorOpen); logging.println(userRequestContactorOpen);
#endif // DEBUG_LOG #endif // DEBUG_LOG
if ((datalayer_extended.bmwix.UserRequestContactorClose == false) && if ((userRequestContactorClose == false) && (userRequestContactorOpen == false)) {
(datalayer_extended.bmwix.UserRequestContactorOpen == false)) {
// do nothing // do nothing
} else if ((datalayer_extended.bmwix.UserRequestContactorClose == true) && } else if ((userRequestContactorClose == true) && (userRequestContactorOpen == false)) {
(datalayer_extended.bmwix.UserRequestContactorOpen == false)) {
BmwIxCloseContactors(); BmwIxCloseContactors();
// set user request to false // set user request to false
datalayer_extended.bmwix.UserRequestContactorClose = false; userRequestContactorClose = false;
} else if ((datalayer_extended.bmwix.UserRequestContactorClose == false) && } else if ((userRequestContactorClose == false) && (userRequestContactorOpen == true)) {
(datalayer_extended.bmwix.UserRequestContactorOpen == true)) {
BmwIxOpenContactors(); BmwIxOpenContactors();
// set user request to false // set user request to false
datalayer_extended.bmwix.UserRequestContactorOpen = false; userRequestContactorOpen = false;
} else if ((datalayer_extended.bmwix.UserRequestContactorClose == true) && } else if ((userRequestContactorClose == true) && (userRequestContactorOpen == true)) {
(datalayer_extended.bmwix.UserRequestContactorOpen == true)) {
// these flasgs should not be true at the same time, therefore open contactors, as that is the safest state // these flasgs should not be true at the same time, therefore open contactors, as that is the safest state
BmwIxOpenContactors(); BmwIxOpenContactors();
// set user request to false // set user request to false
datalayer_extended.bmwix.UserRequestContactorClose = false; userRequestContactorClose = false;
datalayer_extended.bmwix.UserRequestContactorOpen = false; userRequestContactorOpen = false;
// print error, as both these flags shall not be true at the same time // print error, as both these flags shall not be true at the same time
#ifdef DEBUG_LOG #ifdef DEBUG_LOG
logging.println( 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;
}

View file

@ -11,6 +11,8 @@
class BmwIXBattery : public CanBattery { class BmwIXBattery : public CanBattery {
public: public:
BmwIXBattery() : renderer(*this) {}
virtual void setup(void); virtual void setup(void);
virtual void handle_incoming_can_frame(CAN_frame rx_frame); virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values(); virtual void update_values();
@ -19,12 +21,32 @@ class BmwIXBattery : public CanBattery {
bool supports_contactor_close() { return true; } bool supports_contactor_close() { return true; }
void request_open_contactors() { datalayer_extended.bmwix.UserRequestContactorOpen = true; } void request_open_contactors() { userRequestContactorOpen = true; }
void request_close_contactors() { datalayer_extended.bmwix.UserRequestContactorClose = true; } void request_close_contactors() { userRequestContactorClose = true; }
static constexpr const char* Name = "BMW iX and i4-7 platform"; 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: private:
bool userRequestContactorClose = false;
bool userRequestContactorOpen = false;
BmwIXHtmlRenderer renderer; BmwIXHtmlRenderer renderer;
static const int MAX_PACK_VOLTAGE_DV = 4650; //4650 = 465.0V static const int MAX_PACK_VOLTAGE_DV = 4650; //4650 = 465.0V
static const int MIN_PACK_VOLTAGE_DV = 3000; static const int MIN_PACK_VOLTAGE_DV = 3000;

View file

@ -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 += "<h4>Battery Voltage after Contactor: " + String(batt.get_battery_voltage_after_contactor()) + " dV</h4>";
content += "<h4>Max Design Voltage: " + String(datalayer.battery.info.max_design_voltage_dV) + " dV</h4>";
content += "<h4>Min Design Voltage: " + String(datalayer.battery.info.min_design_voltage_dV) + " dV</h4>";
content += "<h4>Max Cell Design Voltage: " + String(datalayer.battery.info.max_cell_voltage_mV) + " mV</h4>";
content += "<h4>Min Cell Design Voltage: " + String(datalayer.battery.info.min_cell_voltage_mV) + " mV</h4>";
content += "<h4>Min Cell Voltage Data Age: " + String(batt.get_min_cell_voltage_data_age()) + " ms</h4>";
content += "<h4>Max Cell Voltage Data Age: " + String(batt.get_max_cell_voltage_data_age()) + " ms</h4>";
content += "<h4>Allowed Discharge Power: " + String(datalayer.battery.status.max_discharge_power_W) + " W</h4>";
content += "<h4>Allowed Charge Power: " + String(datalayer.battery.status.max_charge_power_W) + " W</h4>";
content += "<h4>T30 Terminal Voltage: " + String(batt.get_T30_Voltage()) + " mV</h4>";
content += "<h4>Detected Cell Count: " + String(datalayer.battery.info.number_of_cells) + "</h4>";
content += "<h4>Balancing: ";
switch (batt.get_balancing_status()) {
case 0:
content += "0 No balancing mode active</h4>";
break;
case 1:
content += "1 Voltage-Controlled Balancing Mode</h4>";
break;
case 2:
content += "2 Time-Controlled Balancing Mode with Demand Calculation at End of Charging</h4>";
break;
case 3:
content += "3 Time-Controlled Balancing Mode with Demand Calculation at Resting Voltage</h4>";
break;
case 4:
content += "4 No balancing mode active, qualifier invalid</h4>";
break;
default:
content += "Unknown</h4>";
}
content += "<h4>HVIL Status: ";
switch (batt.get_hvil_status()) {
case 0:
content += "Error (Loop Open)</h4>";
break;
case 1:
content += "OK (Loop Closed)</h4>";
break;
default:
content += "Unknown</h4>";
}
content += "<h4>BMS Uptime: " + String(batt.get_bms_uptime()) + " seconds</h4>";
content += "<h4>BMS Allowed Charge Amps: " + String(batt.get_allowable_charge_amps()) + " A</h4>";
content += "<h4>BMS Allowed Disharge Amps: " + String(batt.get_allowable_discharge_amps()) + " A</h4>";
content += "<br>";
content += "<h3>HV Isolation (2147483647kOhm = maximum/invalid)</h3>";
content += "<h4>Isolation Positive: " + String(batt.get_iso_safety_positive()) + " kOhm</h4>";
content += "<h4>Isolation Negative: " + String(batt.get_iso_safety_negative()) + " kOhm</h4>";
content += "<h4>Isolation Parallel: " + String(batt.get_iso_safety_parallel()) + " kOhm</h4>";
content += "<h4>Pyro Status PSS1: ";
switch (batt.get_pyro_status_pss1()) {
case 0:
content += "0 Value Invalid</h4>";
break;
case 1:
content += "1 Successfully Blown</h4>";
break;
case 2:
content += "2 Disconnected</h4>";
break;
case 3:
content += "3 Not Activated - Pyro Intact</h4>";
break;
case 4:
content += "4 Unknown</h4>";
break;
default:
content += "Unknown</h4>";
}
content += "<h4>Pyro Status PSS4: ";
switch (batt.get_pyro_status_pss4()) {
case 0:
content += "0 Value Invalid</h4>";
break;
case 1:
content += "1 Successfully Blown</h4>";
break;
case 2:
content += "2 Disconnected</h4>";
break;
case 3:
content += "3 Not Activated - Pyro Intact</h4>";
break;
case 4:
content += "4 Unknown</h4>";
break;
default:
content += "Unknown</h4>";
}
content += "<h4>Pyro Status PSS6: ";
switch (batt.get_pyro_status_pss6()) {
case 0:
content += "0 Value Invalid</h4>";
break;
case 1:
content += "1 Successfully Blown</h4>";
break;
case 2:
content += "2 Disconnected</h4>";
break;
case 3:
content += "3 Not Activated - Pyro Intact</h4>";
break;
case 4:
content += "4 Unknown</h4>";
break;
default:
content += "Unknown</h4>";
}
return content;
}

View file

@ -2,132 +2,18 @@
#define _BMW_IX_HTML_H #define _BMW_IX_HTML_H
#include "../datalayer/datalayer.h" #include "../datalayer/datalayer.h"
#include "../datalayer/datalayer_extended.h"
#include "src/devboard/webserver/BatteryHtmlRenderer.h" #include "src/devboard/webserver/BatteryHtmlRenderer.h"
class BmwIXBattery;
class BmwIXHtmlRenderer : public BatteryHtmlRenderer { class BmwIXHtmlRenderer : public BatteryHtmlRenderer {
private:
BmwIXBattery& batt;
public: public:
String get_status_html() { BmwIXHtmlRenderer(BmwIXBattery& b) : batt(b) {}
String content;
content += String get_status_html();
"<h4>Battery Voltage after Contactor: " + String(datalayer_extended.bmwix.battery_voltage_after_contactor) +
" dV</h4>";
content += "<h4>Max Design Voltage: " + String(datalayer.battery.info.max_design_voltage_dV) + " dV</h4>";
content += "<h4>Min Design Voltage: " + String(datalayer.battery.info.min_design_voltage_dV) + " dV</h4>";
content += "<h4>Max Cell Design Voltage: " + String(datalayer.battery.info.max_cell_voltage_mV) + " mV</h4>";
content += "<h4>Min Cell Design Voltage: " + String(datalayer.battery.info.min_cell_voltage_mV) + " mV</h4>";
content +=
"<h4>Min Cell Voltage Data Age: " + String(datalayer_extended.bmwix.min_cell_voltage_data_age) + " ms</h4>";
content +=
"<h4>Max Cell Voltage Data Age: " + String(datalayer_extended.bmwix.max_cell_voltage_data_age) + " ms</h4>";
content += "<h4>Allowed Discharge Power: " + String(datalayer.battery.status.max_discharge_power_W) + " W</h4>";
content += "<h4>Allowed Charge Power: " + String(datalayer.battery.status.max_charge_power_W) + " W</h4>";
content += "<h4>T30 Terminal Voltage: " + String(datalayer_extended.bmwix.T30_Voltage) + " mV</h4>";
content += "<h4>Detected Cell Count: " + String(datalayer.battery.info.number_of_cells) + "</h4>";
content += "<h4>Balancing: ";
switch (datalayer_extended.bmwix.balancing_status) {
case 0:
content += "0 No balancing mode active</h4>";
break;
case 1:
content += "1 Voltage-Controlled Balancing Mode</h4>";
break;
case 2:
content += "2 Time-Controlled Balancing Mode with Demand Calculation at End of Charging</h4>";
break;
case 3:
content += "3 Time-Controlled Balancing Mode with Demand Calculation at Resting Voltage</h4>";
break;
case 4:
content += "4 No balancing mode active, qualifier invalid</h4>";
break;
default:
content += "Unknown</h4>";
}
content += "<h4>HVIL Status: ";
switch (datalayer_extended.bmwix.hvil_status) {
case 0:
content += "Error (Loop Open)</h4>";
break;
case 1:
content += "OK (Loop Closed)</h4>";
break;
default:
content += "Unknown</h4>";
}
content += "<h4>BMS Uptime: " + String(datalayer_extended.bmwix.bms_uptime) + " seconds</h4>";
content += "<h4>BMS Allowed Charge Amps: " + String(datalayer_extended.bmwix.allowable_charge_amps) + " A</h4>";
content +=
"<h4>BMS Allowed Disharge Amps: " + String(datalayer_extended.bmwix.allowable_discharge_amps) + " A</h4>";
content += "<br>";
content += "<h3>HV Isolation (2147483647kOhm = maximum/invalid)</h3>";
content += "<h4>Isolation Positive: " + String(datalayer_extended.bmwix.iso_safety_positive) + " kOhm</h4>";
content += "<h4>Isolation Negative: " + String(datalayer_extended.bmwix.iso_safety_negative) + " kOhm</h4>";
content += "<h4>Isolation Parallel: " + String(datalayer_extended.bmwix.iso_safety_parallel) + " kOhm</h4>";
content += "<h4>Pyro Status PSS1: ";
switch (datalayer_extended.bmwix.pyro_status_pss1) {
case 0:
content += "0 Value Invalid</h4>";
break;
case 1:
content += "1 Successfully Blown</h4>";
break;
case 2:
content += "2 Disconnected</h4>";
break;
case 3:
content += "3 Not Activated - Pyro Intact</h4>";
break;
case 4:
content += "4 Unknown</h4>";
break;
default:
content += "Unknown</h4>";
}
content += "<h4>Pyro Status PSS4: ";
switch (datalayer_extended.bmwix.pyro_status_pss4) {
case 0:
content += "0 Value Invalid</h4>";
break;
case 1:
content += "1 Successfully Blown</h4>";
break;
case 2:
content += "2 Disconnected</h4>";
break;
case 3:
content += "3 Not Activated - Pyro Intact</h4>";
break;
case 4:
content += "4 Unknown</h4>";
break;
default:
content += "Unknown</h4>";
}
content += "<h4>Pyro Status PSS6: ";
switch (datalayer_extended.bmwix.pyro_status_pss6) {
case 0:
content += "0 Value Invalid</h4>";
break;
case 1:
content += "1 Successfully Blown</h4>";
break;
case 2:
content += "2 Disconnected</h4>";
break;
case 3:
content += "3 Not Activated - Pyro Intact</h4>";
break;
case 4:
content += "4 Unknown</h4>";
break;
default:
content += "Unknown</h4>";
}
return content;
}
}; };
#endif #endif

View file

@ -11,12 +11,12 @@ TODOs left for this implementation
- Current implementation only seems to get the 7E7 polls working. - Current implementation only seems to get the 7E7 polls working.
- We might need to poll on 7E6 also? - We might need to poll on 7E6 also?
- The values missing for a working implementation is: - The values missing for a fully working implementation is:
- SOC% missing! This is absolutely mandatory to fix before starting to use this! - SOC% missing! (now estimated based on voltage)
- Capacity (kWh) (can be estimated) - Capacity (kWh) (now estimated)
- Charge max power (can be estimated) - Charge max power (now estimated)
- Discharge max power (can be estimated) - Discharge max power (now estimated)
- SOH% (low prio)) - SOH% (now hardcoded to 99%)
*/ */
/*TODO, messages we might need to send towards the battery to keep it happy and close contactors /*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?) 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<uint16_t>::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 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 * 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<uint32_t>(
(static_cast<double>(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; // 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;
datalayer.battery.status.max_charge_power_W;
// Store temperatures in an array // Store temperatures in an array
int16_t temperatures[] = {temperature_1, temperature_2, temperature_3, temperature_4, temperature_5, temperature_6}; 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 //Map all cell voltages to the global array
memcpy(datalayer.battery.status.cell_voltages_mV, battery_cell_voltages, 96 * sizeof(uint16_t)); 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 // Update webserver datalayer
datalayer_extended.boltampera.battery_5V_ref = battery_5V_ref; datalayer_extended.boltampera.battery_5V_ref = battery_5V_ref;
datalayer_extended.boltampera.battery_module_temp_1 = battery_module_temp_1; 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); strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0'; datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = 96; 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.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
datalayer.battery.info.min_design_voltage_dV = MIN_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.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;

View file

@ -23,6 +23,14 @@ class BoltAmperaBattery : public CanBattery {
private: private:
BoltAmperaHtmlRenderer renderer; 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 MAX_PACK_VOLTAGE_DV = 4150; //5000 = 500.0V
static const int MIN_PACK_VOLTAGE_DV = 2500; static const int MIN_PACK_VOLTAGE_DV = 2500;
static const int MAX_CELL_DEVIATION_MV = 150; static const int MAX_CELL_DEVIATION_MV = 150;

View file

@ -43,6 +43,7 @@ enum class BatteryType {
TestFake = 34, TestFake = 34,
VolvoSpa = 35, VolvoSpa = 35,
VolvoSpaHybrid = 36, VolvoSpaHybrid = 36,
MgHsPhev = 37,
Highest Highest
}; };

View file

@ -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

View file

@ -0,0 +1,140 @@
#ifndef MG_HS_PHEV_BATTERY_H
#define MG_HS_PHEV_BATTERY_H
#include <Arduino.h>
#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

View file

@ -40,34 +40,6 @@ typedef struct {
int16_t battery_current_7E4 = 0; int16_t battery_current_7E4 = 0;
} DATALAYER_INFO_BOLTAMPERA; } 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 { typedef struct {
/** uint8_t */ /** uint8_t */
/** Status isolation external, 0 not evaluated, 1 OK, 2 error active, 3 Invalid signal*/ /** Status isolation external, 0 not evaluated, 1 OK, 2 error active, 3 Invalid signal*/
@ -851,7 +823,6 @@ typedef struct {
class DataLayerExtended { class DataLayerExtended {
public: public:
DATALAYER_INFO_BOLTAMPERA boltampera; DATALAYER_INFO_BOLTAMPERA boltampera;
DATALAYER_INFO_BMWIX bmwix;
DATALAYER_INFO_BMWPHEV bmwphev; DATALAYER_INFO_BMWPHEV bmwphev;
DATALAYER_INFO_BYDATTO3 bydAtto3; DATALAYER_INFO_BYDATTO3 bydAtto3;
DATALAYER_INFO_CELLPOWER cellpower; DATALAYER_INFO_CELLPOWER cellpower;

View file

@ -64,6 +64,7 @@ static String device_id = "";
static bool publish_common_info(void); static bool publish_common_info(void);
static bool publish_cell_voltages(void); static bool publish_cell_voltages(void);
static bool publish_cell_balancing(void);
static bool publish_events(void); static bool publish_events(void);
/** Publish global values and call callbacks for specific modules */ /** Publish global values and call callbacks for specific modules */
@ -86,6 +87,12 @@ static void publish_values(void) {
return; return;
} }
#endif #endif
#ifdef MQTT_PUBLISH_CELL_VOLTAGES
if (publish_cell_balancing() == false) {
return;
}
#endif
} }
static bool ha_common_info_published = false; static bool ha_common_info_published = false;
@ -129,7 +136,8 @@ SensorConfig batterySensorConfigTemplate[] = {
{"max_discharge_power", "Battery Max Discharge Power", "", "W", "power", always}, {"max_discharge_power", "Battery Max Discharge Power", "", "W", "power", always},
{"max_charge_power", "Battery Max Charge Power", "", "W", "power", always}, {"max_charge_power", "Battery Max Charge Power", "", "W", "power", always},
{"charged_energy", "Battery Charged Energy", "", "Wh", "energy", supports_charged}, {"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}, SensorConfig globalSensorConfigTemplate[] = {{"bms_status", "BMS Status", "", "", "", always},
{"pause_status", "Pause 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); 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<EventData> order_events; static std::vector<EventData> order_events;
@ -398,6 +417,53 @@ static bool publish_cell_voltages(void) {
return true; 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<JsonArray>();
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<JsonArray>();
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() { bool publish_events() {
static JsonDocument doc; static JsonDocument doc;
static String state_topic = topic_name + "/events"; static String state_topic = topic_name + "/events";

View file

@ -844,6 +844,7 @@ String processor(const String& var) {
} }
content += "</h4>"; content += "</h4>";
if (status == WL_CONNECTED) { if (status == WL_CONNECTED) {
content += "<h4>Hostname: " + String(WiFi.getHostname()) + "</h4>";
content += "<h4>IP: " + WiFi.localIP().toString() + "</h4>"; content += "<h4>IP: " + WiFi.localIP().toString() + "</h4>";
} else { } else {
content += "<h4>Wifi state: " + getConnectResultString(status) + "</h4>"; content += "<h4>Wifi state: " + getConnectResultString(status) + "</h4>";

View file

@ -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(), DEBUG_PRINTF("init_Wifi enabled=%d, apå=%d, ssid=%s, password=%s\n", wifi_enabled, wifiap_enabled, ssid.c_str(),
password.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) { if (wifiap_enabled) {
WiFi.mode(WIFI_AP_STA); // Simultaneous WiFi AP and Router connection WiFi.mode(WIFI_AP_STA); // Simultaneous WiFi AP and Router connection
init_WiFi_AP(); 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 // e.g batteryemulator8C.local where the mac address is 08:F9:E0:D1:06:8C
String mac = WiFi.macAddress(); String mac = WiFi.macAddress();
String mdnsHost = "batteryemulator" + mac.substring(mac.length() - 2); 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 // Initialize mDNS .local resolution
if (!MDNS.begin(mdnsHost)) { if (!MDNS.begin(mdnsHost)) {
@ -240,7 +247,7 @@ void init_mDNS() {
#endif #endif
} else { } else {
// Advertise via bonjour the service so we can auto discover these battery emulators on the local network. // 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);
} }
} }