mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-03 09:49:32 +02:00
Merge from main and fix conflict
This commit is contained in:
commit
0b3bc738ac
17 changed files with 887 additions and 215 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
120
Software/src/battery/BMW-IX-HTML.cpp
Normal file
120
Software/src/battery/BMW-IX-HTML.cpp
Normal 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;
|
||||
}
|
|
@ -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 +=
|
||||
"<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;
|
||||
}
|
||||
String get_status_html();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -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<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
|
||||
|
||||
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<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;
|
||||
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -43,6 +43,7 @@ enum class BatteryType {
|
|||
TestFake = 34,
|
||||
VolvoSpa = 35,
|
||||
VolvoSpaHybrid = 36,
|
||||
MgHsPhev = 37,
|
||||
Highest
|
||||
};
|
||||
|
||||
|
|
363
Software/src/battery/MG-HS-PHEV-BATTERY.cpp
Normal file
363
Software/src/battery/MG-HS-PHEV-BATTERY.cpp
Normal 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
|
140
Software/src/battery/MG-HS-PHEV-BATTERY.h
Normal file
140
Software/src/battery/MG-HS-PHEV-BATTERY.h
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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<EventData> 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<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() {
|
||||
static JsonDocument doc;
|
||||
static String state_topic = topic_name + "/events";
|
||||
|
|
|
@ -844,6 +844,7 @@ String processor(const String& var) {
|
|||
}
|
||||
content += "</h4>";
|
||||
if (status == WL_CONNECTED) {
|
||||
content += "<h4>Hostname: " + String(WiFi.getHostname()) + "</h4>";
|
||||
content += "<h4>IP: " + WiFi.localIP().toString() + "</h4>";
|
||||
} else {
|
||||
content += "<h4>Wifi state: " + getConnectResultString(status) + "</h4>";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue