More batteries to common image

This commit is contained in:
Jaakko Haakana 2025-06-17 23:19:26 +03:00
parent 57f8d2618e
commit 02917dcbea
64 changed files with 885 additions and 722 deletions

View file

@ -19,6 +19,8 @@ std::vector<BatteryType> supported_battery_types() {
extern const char* name_for_type(BatteryType type) {
switch (type) {
case BatteryType::None:
return "None";
case BatteryType::BmwI3:
return BmwI3Battery::Name;
case BatteryType::BmwIx:
@ -31,19 +33,67 @@ extern const char* name_for_type(BatteryType type) {
return CellPowerBms::Name;
case BatteryType::Chademo:
return ChademoBattery::Name;
case BatteryType::CmfaEv:
return CmfaEvBattery::Name;
case BatteryType::Foxess:
return FoxessBattery::Name;
case BatteryType::GeelyGeometryC:
return GeelyGeometryCBattery::Name;
case BatteryType::OrionBms:
return OrionBms::Name;
case BatteryType::Sono:
return SonoBattery::Name;
case BatteryType::StellantisEcmp:
return EcmpBattery::Name;
case BatteryType::ImievCZeroIon:
return ImievCZeroIonBattery::Name;
case BatteryType::JaguarIpace:
return JaguarIpaceBattery::Name;
case BatteryType::KiaEGmp:
return KiaEGmpBattery::Name;
case BatteryType::KiaHyundai64:
return KiaHyundai64Battery::Name;
case BatteryType::KiaHyundaiHybrid:
return KiaHyundaiHybridBattery::Name;
case BatteryType::Meb:
return MebBattery::Name;
case BatteryType::Mg5:
return Mg5Battery::Name;
case BatteryType::NissanLeaf:
return NissanLeafBattery::Name;
case BatteryType::TestFake:
return TestFakeBattery::Name;
case BatteryType::Pylon:
return PylonBattery::Name;
case BatteryType::DalyBms:
return DalyBms::Name;
case BatteryType::RjxzsBms:
return RjxzsBms::Name;
case BatteryType::RangeRoverPhev:
return RangeRoverPhevBattery::Name;
case BatteryType::RenaultKangoo:
return RenaultKangooBattery::Name;
case BatteryType::RenaultTwizy:
return RenaultTwizyBattery::Name;
case BatteryType::RenaultZoe1:
return RenaultZoeGen1Battery::Name;
case BatteryType::RenaultZoe2:
return RenaultZoeGen2Battery::Name;
case BatteryType::SantaFePhev:
return SantaFePhevBattery::Name;
case BatteryType::SimpBms:
return SimpBmsBattery::Name;
case BatteryType::TeslaModel3Y:
return TeslaModel3YBattery::Name;
case BatteryType::TeslaModelSX:
return TeslaModelSXBattery::Name;
case BatteryType::None:
return "None";
case BatteryType::TestFake:
return TestFakeBattery::Name;
case BatteryType::VolvoSpa:
return VolvoSpaBattery::Name;
case BatteryType::VolvoSpaHybrid:
return VolvoSpaHybridBattery::Name;
default:
return nullptr;
}
return nullptr;
}
#ifdef COMMON_IMAGE
@ -54,51 +104,92 @@ extern const char* name_for_type(BatteryType type) {
BatteryType user_selected_battery_type = BatteryType::NissanLeaf;
bool user_selected_second_battery = false;
void setup_battery() {
Battery* create_battery(BatteryType type) {
switch (type) {
case BatteryType::None:
return nullptr;
case BatteryType::BmwI3:
return new BmwI3Battery();
case BatteryType::BmwIx:
return new BmwIXBattery();
case BatteryType::BoltAmpera:
return new BoltAmperaBattery();
case BatteryType::BydAtto3:
return new BydAttoBattery();
case BatteryType::CellPowerBms:
return new CellPowerBms();
case BatteryType::Chademo:
return new ChademoBattery();
case BatteryType::CmfaEv:
return new CmfaEvBattery();
case BatteryType::Foxess:
return new FoxessBattery();
case BatteryType::GeelyGeometryC:
return new GeelyGeometryCBattery();
case BatteryType::OrionBms:
return new OrionBms();
case BatteryType::Sono:
return new SonoBattery();
case BatteryType::StellantisEcmp:
return new EcmpBattery();
case BatteryType::ImievCZeroIon:
return new ImievCZeroIonBattery();
case BatteryType::JaguarIpace:
return new JaguarIpaceBattery();
case BatteryType::KiaEGmp:
return new KiaEGmpBattery();
case BatteryType::KiaHyundai64:
return new KiaHyundai64Battery();
case BatteryType::KiaHyundaiHybrid:
return new KiaHyundaiHybridBattery();
case BatteryType::Meb:
return new MebBattery();
case BatteryType::Mg5:
return new Mg5Battery();
case BatteryType::NissanLeaf:
return new NissanLeafBattery();
case BatteryType::Pylon:
return new PylonBattery();
case BatteryType::DalyBms:
return new DalyBms();
case BatteryType::RjxzsBms:
return new RjxzsBms();
case BatteryType::RangeRoverPhev:
return new RangeRoverPhevBattery();
case BatteryType::RenaultKangoo:
return new RenaultKangooBattery();
case BatteryType::RenaultTwizy:
return new RenaultTwizyBattery();
case BatteryType::RenaultZoe1:
return new RenaultZoeGen1Battery();
case BatteryType::RenaultZoe2:
return new RenaultZoeGen2Battery();
case BatteryType::SantaFePhev:
return new SantaFePhevBattery();
case BatteryType::SimpBms:
return new SimpBmsBattery();
case BatteryType::TeslaModel3Y:
return new TeslaModel3YBattery();
case BatteryType::TeslaModelSX:
return new TeslaModelSXBattery();
case BatteryType::TestFake:
return new TestFakeBattery();
case BatteryType::VolvoSpa:
return new VolvoSpaBattery();
case BatteryType::VolvoSpaHybrid:
return new VolvoSpaHybridBattery();
default:
return nullptr;
}
}
void setup_battery() {
if (battery) {
// Let's not create the battery again.
return;
}
switch (user_selected_battery_type) {
case BatteryType::BmwI3:
battery = new BmwI3Battery();
break;
case BatteryType::BmwIx:
battery = new BmwIXBattery();
break;
case BatteryType::BoltAmpera:
battery = new BoltAmperaBattery();
break;
case BatteryType::BydAtto3:
battery = new BydAttoBattery();
break;
case BatteryType::CellPowerBms:
battery = new CellPowerBms();
break;
case BatteryType::Chademo:
battery = new ChademoBattery();
break;
case BatteryType::CmfaEv:
battery = new CmfaEvBattery();
break;
case BatteryType::DalyBms:
battery = new DalyBms();
break;
case BatteryType::NissanLeaf:
battery = new NissanLeafBattery();
break;
case BatteryType::TeslaModel3Y:
battery = new TeslaModel3YBattery();
break;
case BatteryType::TeslaModelSX:
battery = new TeslaModelSXBattery();
break;
case BatteryType::TestFake:
battery = new TestFakeBattery();
break;
}
battery = create_battery(user_selected_battery_type);
if (battery) {
battery->setup();

View file

@ -178,6 +178,6 @@ void BmwSbox::transmit_can(unsigned long currentMillis) {
}
void BmwSbox::setup() {
strncpy(datalayer.system.info.shunt_protocol, "BMW SBOX", 63);
strncpy(datalayer.system.info.shunt_protocol, Name, 63);
datalayer.system.info.shunt_protocol[63] = '\0';
}

View file

@ -13,6 +13,7 @@ class BmwSbox : public CanShunt {
void setup();
void transmit_can(unsigned long currentMillis);
void handle_incoming_can_frame(CAN_frame rx_frame);
static constexpr char* Name = "BMW SBOX";
private:
/** Minimum input voltage required to enable relay control **/

View file

@ -0,0 +1,6 @@
#include "Battery.h"
#include "../datalayer/datalayer.h"
float Battery::get_voltage() {
return static_cast<float>(datalayer.battery.status.voltage_dV) / 10.0;
}

View file

@ -1,47 +1,47 @@
#ifndef BATTERY_H
#define BATTERY_H
#include "../datalayer/datalayer.h"
#include <vector>
#include "src/devboard/webserver/BatteryHtmlRenderer.h"
enum class BatteryType {
None = 0,
BmwSbox,
BmwI3,
BmwIx,
BoltAmpera,
BydAtto3,
CellPowerBms,
Chademo,
CmfaEv,
Foxess,
GeelyGeometryC,
OrionBms,
Sono,
StellantisEcmp,
ImievCZeroIon,
JaguarIpace,
KiaEGmp,
KiaHyundai64,
KiaHyundaiHybrid,
Meb,
Mg5,
NissanLeaf,
Pylon,
DalyBms,
RjxzsBms,
RangeRoverPhev,
RenaultKangoo,
RenaultTwizy,
RenaultZoe1,
RenaultZoe2,
SantaFePhev,
SimpBms,
TeslaModel3Y,
TeslaModelSX,
TestFake,
VolvoSpa,
VolvoSpaHybrid,
BmwSbox = 1,
BmwI3 = 2,
BmwIx = 3,
BoltAmpera = 4,
BydAtto3 = 5,
CellPowerBms = 6,
Chademo = 7,
CmfaEv = 8,
Foxess = 9,
GeelyGeometryC = 10,
OrionBms = 11,
Sono = 12,
StellantisEcmp = 13,
ImievCZeroIon = 14,
JaguarIpace = 15,
KiaEGmp = 16,
KiaHyundai64 = 17,
KiaHyundaiHybrid = 18,
Meb = 19,
Mg5 = 20,
NissanLeaf = 21,
Pylon = 22,
DalyBms = 23,
RjxzsBms = 24,
RangeRoverPhev = 25,
RenaultKangoo = 26,
RenaultTwizy = 27,
RenaultZoe1 = 28,
RenaultZoe2 = 29,
SantaFePhev = 30,
SimpBms = 31,
TeslaModel3Y = 32,
TeslaModelSX = 33,
TestFake = 34,
VolvoSpa = 35,
VolvoSpaHybrid = 36,
Highest
};
@ -95,7 +95,7 @@ class Battery {
virtual void set_factory_mode() {}
virtual void set_fake_voltage(float v) {}
virtual float get_voltage() { return static_cast<float>(datalayer.battery.status.voltage_dV) / 10.0; }
virtual float get_voltage();
// This allows for battery specific SOC plausibility calculations to be performed.
virtual bool soc_plausible() { return true; }

View file

@ -1,6 +1,9 @@
#ifndef CHADEMO_SHUNTS_H
#define CHADEMO_SHUNTS_H
#include <stdint.h>
#include "../devboard/utils/types.h"
uint16_t get_measured_voltage();
uint16_t get_measured_current();
void ISA_handleFrame(CAN_frame* frame);

View file

@ -940,7 +940,7 @@ void CmfaEvBattery::transmit_can(unsigned long currentMillis) {
}
void CmfaEvBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "CMFA platform, 27 kWh battery", 63);
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.number_of_cells = 72;

View file

@ -15,6 +15,7 @@ class CmfaEvBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "CMFA platform, 27 kWh battery";
BatteryHtmlRenderer& get_status_renderer() { return renderer; }

View file

@ -0,0 +1,8 @@
#include "CanBattery.h"
#include "../../src/include.h"
CanBattery::CanBattery() {
can_interface = can_config.battery;
register_transmitter(this);
register_can_receiver(this, can_interface);
}

View file

@ -22,11 +22,7 @@ class CanBattery : public Battery, Transmitter, CanReceiver {
protected:
CAN_Interface can_interface;
CanBattery() {
can_interface = can_config.battery;
register_transmitter(this);
register_can_receiver(this, can_interface);
}
CanBattery();
CanBattery(CAN_Interface interface) {
can_interface = interface;

View file

@ -60,7 +60,7 @@ void DalyBms::update_values() {
}
void DalyBms::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "DALY RS485", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = CELL_COUNT;
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;

View file

@ -13,6 +13,7 @@ class DalyBms : public RS485Battery {
void update_values();
void transmit_rs485(unsigned long currentMillis);
void receive();
static constexpr char* Name = "DALY RS485";
private:
/* Tweak these according to your battery build */

View file

@ -1516,7 +1516,7 @@ void EcmpBattery::transmit_can(unsigned long currentMillis) {
}
void EcmpBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Stellantis ECMP battery", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = 108;
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;

View file

@ -16,6 +16,7 @@ class EcmpBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Stellantis ECMP battery";
bool supports_clear_isolation() { return true; }
void clear_isolation() { datalayer_extended.stellantisECMP.UserRequestIsolationReset = true; }
@ -38,8 +39,8 @@ class EcmpBattery : public CanBattery {
static const int MAX_CELL_DEVIATION_MV = 100;
static const int MAX_CELL_VOLTAGE_MV = 4250;
static const int MIN_CELL_VOLTAGE_MV = 2700;
#define NOT_SAMPLED_YET 255
#define COMPLETED_STATE 0
static const int NOT_SAMPLED_YET = 255;
static const int COMPLETED_STATE = 0;
bool battery_RelayOpenRequest = false;
bool battery_InterlockOpen = false;
uint8_t ContactorResetStatemachine = 0;
@ -143,77 +144,77 @@ class EcmpBattery : public CanBattery {
unsigned long previousMillis500 = 0; // will store last time a 500ms CAN Message was sent
unsigned long previousMillis1000 = 0; // will store last time a 1000ms CAN Message was sent
unsigned long previousMillis5000 = 0; // will store last time a 1000ms CAN Message was sent
#define PID_WELD_CHECK 0xD814
#define PID_CONT_REASON_OPEN 0xD812
#define PID_CONTACTOR_STATUS 0xD813
#define PID_NEG_CONT_CONTROL 0xD44F
#define PID_NEG_CONT_STATUS 0xD453
#define PID_POS_CONT_CONTROL 0xD44E
#define PID_POS_CONT_STATUS 0xD452
#define PID_CONTACTOR_NEGATIVE 0xD44C
#define PID_CONTACTOR_POSITIVE 0xD44D
#define PID_PRECHARGE_RELAY_CONTROL 0xD44B
#define PID_PRECHARGE_RELAY_STATUS 0xD451
#define PID_RECHARGE_STATUS 0xD864
#define PID_DELTA_TEMPERATURE 0xD878
#define PID_COLDEST_MODULE 0xD446
#define PID_LOWEST_TEMPERATURE 0xD87D
#define PID_AVERAGE_TEMPERATURE 0xD877
#define PID_HIGHEST_TEMPERATURE 0xD817
#define PID_HOTTEST_MODULE 0xD445
#define PID_AVG_CELL_VOLTAGE 0xD43D
#define PID_CURRENT 0xD816
#define PID_INSULATION_NEG 0xD87C
#define PID_INSULATION_POS 0xD87B
#define PID_MAX_CURRENT_10S 0xD876
#define PID_MAX_DISCHARGE_10S 0xD873
#define PID_MAX_DISCHARGE_30S 0xD874
#define PID_MAX_CHARGE_10S 0xD871
#define PID_MAX_CHARGE_30S 0xD872
#define PID_ENERGY_CAPACITY 0xD860
#define PID_HIGH_CELL_NUM 0xD43B
#define PID_LOW_CELL_NUM 0xD43C
#define PID_SUM_OF_CELLS 0xD438
#define PID_CELL_MIN_CAPACITY 0xD413
#define PID_CELL_VOLTAGE_MEAS_STATUS 0xD48A
#define PID_INSULATION_RES 0xD47A
#define PID_PACK_VOLTAGE 0xD815
#define PID_HIGH_CELL_VOLTAGE 0xD870
#define PID_ALL_CELL_VOLTAGES 0xD440 //Multi-frame
#define PID_LOW_CELL_VOLTAGE 0xD86F
#define PID_BATTERY_ENERGY 0xD865
#define PID_BATTERY_ENERGY 0xD865
#define PID_CELLBALANCE_STATUS 0xD46F //Multi-frame?
#define PID_CELLBALANCE_HWERR_MASK 0xD470 //Multi-frame
#define PID_CRASH_COUNTER 0xD42F
#define PID_WIRE_CRASH 0xD87F
#define PID_CAN_CRASH 0xD48D
#define PID_HISTORY_DATA 0xD465
#define PID_LOWSOC_COUNTER 0xD492 //Not supported on all batteris
#define PID_LAST_CAN_FAILURE_DETAIL 0xD89E //Not supported on all batteris
#define PID_HW_VERSION_NUM 0xF193 //Not supported on all batteris
#define PID_SW_VERSION_NUM 0xF195 //Not supported on all batteris
#define PID_FACTORY_MODE_CONTROL 0xD900
#define PID_BATTERY_SERIAL 0xD901
#define PID_ALL_CELL_SOH 0xD4B5 //Very long message reply, too much data for this integration
#define PID_AUX_FUSE_STATE 0xD86C
#define PID_BATTERY_STATE 0xD811
#define PID_PRECHARGE_SHORT_CIRCUIT 0xD4D8
#define PID_ESERVICE_PLUG_STATE 0xD86A
#define PID_MAINFUSE_STATE 0xD86B
#define PID_MOST_CRITICAL_FAULT 0xD481
#define PID_CURRENT_TIME 0xD47F
#define PID_TIME_SENT_BY_CAR 0xD4CA
#define PID_12V 0xD822
#define PID_12V_ABNORMAL 0xD42B
#define PID_HVIL_IN_VOLTAGE 0xD46B
#define PID_HVIL_OUT_VOLTAGE 0xD46A
#define PID_HVIL_STATE 0xD869
#define PID_BMS_STATE 0xD45A
#define PID_VEHICLE_SPEED 0xD802
#define PID_TIME_SPENT_OVER_55C 0xE082
#define PID_CONTACTOR_CLOSING_COUNTER 0xD416
#define PID_DATE_OF_MANUFACTURE 0xF18B
static const uint16_t PID_WELD_CHECK = 0xD814;
static const uint16_t PID_CONT_REASON_OPEN = 0xD812;
static const uint16_t PID_CONTACTOR_STATUS = 0xD813;
static const uint16_t PID_NEG_CONT_CONTROL = 0xD44F;
static const uint16_t PID_NEG_CONT_STATUS = 0xD453;
static const uint16_t PID_POS_CONT_CONTROL = 0xD44E;
static const uint16_t PID_POS_CONT_STATUS = 0xD452;
static const uint16_t PID_CONTACTOR_NEGATIVE = 0xD44C;
static const uint16_t PID_CONTACTOR_POSITIVE = 0xD44D;
static const uint16_t PID_PRECHARGE_RELAY_CONTROL = 0xD44B;
static const uint16_t PID_PRECHARGE_RELAY_STATUS = 0xD451;
static const uint16_t PID_RECHARGE_STATUS = 0xD864;
static const uint16_t PID_DELTA_TEMPERATURE = 0xD878;
static const uint16_t PID_COLDEST_MODULE = 0xD446;
static const uint16_t PID_LOWEST_TEMPERATURE = 0xD87D;
static const uint16_t PID_AVERAGE_TEMPERATURE = 0xD877;
static const uint16_t PID_HIGHEST_TEMPERATURE = 0xD817;
static const uint16_t PID_HOTTEST_MODULE = 0xD445;
static const uint16_t PID_AVG_CELL_VOLTAGE = 0xD43D;
static const uint16_t PID_CURRENT = 0xD816;
static const uint16_t PID_INSULATION_NEG = 0xD87C;
static const uint16_t PID_INSULATION_POS = 0xD87B;
static const uint16_t PID_MAX_CURRENT_10S = 0xD876;
static const uint16_t PID_MAX_DISCHARGE_10S = 0xD873;
static const uint16_t PID_MAX_DISCHARGE_30S = 0xD874;
static const uint16_t PID_MAX_CHARGE_10S = 0xD871;
static const uint16_t PID_MAX_CHARGE_30S = 0xD872;
static const uint16_t PID_ENERGY_CAPACITY = 0xD860;
static const uint16_t PID_HIGH_CELL_NUM = 0xD43B;
static const uint16_t PID_LOW_CELL_NUM = 0xD43C;
static const uint16_t PID_SUM_OF_CELLS = 0xD438;
static const uint16_t PID_CELL_MIN_CAPACITY = 0xD413;
static const uint16_t PID_CELL_VOLTAGE_MEAS_STATUS = 0xD48A;
static const uint16_t PID_INSULATION_RES = 0xD47A;
static const uint16_t PID_PACK_VOLTAGE = 0xD815;
static const uint16_t PID_HIGH_CELL_VOLTAGE = 0xD870;
static const uint16_t PID_ALL_CELL_VOLTAGES = 0xD440; //Multi-frame
static const uint16_t PID_LOW_CELL_VOLTAGE = 0xD86F;
static const uint16_t PID_BATTERY_ENERGY = 0xD865;
static const uint16_t PID_CELLBALANCE_STATUS = 0xD46F; //Multi-frame?
static const uint16_t PID_CELLBALANCE_HWERR_MASK = 0xD470; //Multi-frame
static const uint16_t PID_CRASH_COUNTER = 0xD42F;
static const uint16_t PID_WIRE_CRASH = 0xD87F;
static const uint16_t PID_CAN_CRASH = 0xD48D;
static const uint16_t PID_HISTORY_DATA = 0xD465;
static const uint16_t PID_LOWSOC_COUNTER = 0xD492; //Not supported on all batteris
static const uint16_t PID_LAST_CAN_FAILURE_DETAIL = 0xD89E; //Not supported on all batteris
static const uint16_t PID_HW_VERSION_NUM = 0xF193; //Not supported on all batteris
static const uint16_t PID_SW_VERSION_NUM = 0xF195; //Not supported on all batteris
static const uint16_t PID_FACTORY_MODE_CONTROL = 0xD900;
static const uint16_t PID_BATTERY_SERIAL = 0xD901;
static const uint16_t PID_ALL_CELL_SOH = 0xD4B5; //Very long message reply, too much data for this integration
static const uint16_t PID_AUX_FUSE_STATE = 0xD86C;
static const uint16_t PID_BATTERY_STATE = 0xD811;
static const uint16_t PID_PRECHARGE_SHORT_CIRCUIT = 0xD4D8;
static const uint16_t PID_ESERVICE_PLUG_STATE = 0xD86A;
static const uint16_t PID_MAINFUSE_STATE = 0xD86B;
static const uint16_t PID_MOST_CRITICAL_FAULT = 0xD481;
static const uint16_t PID_CURRENT_TIME = 0xD47F;
static const uint16_t PID_TIME_SENT_BY_CAR = 0xD4CA;
static const uint16_t PID_12V = 0xD822;
static const uint16_t PID_12V_ABNORMAL = 0xD42B;
static const uint16_t PID_HVIL_IN_VOLTAGE = 0xD46B;
static const uint16_t PID_HVIL_OUT_VOLTAGE = 0xD46A;
static const uint16_t PID_HVIL_STATE = 0xD869;
static const uint16_t PID_BMS_STATE = 0xD45A;
static const uint16_t PID_VEHICLE_SPEED = 0xD802;
static const uint16_t PID_TIME_SPENT_OVER_55C = 0xE082;
static const uint16_t PID_CONTACTOR_CLOSING_COUNTER = 0xD416;
static const uint16_t PID_DATE_OF_MANUFACTURE = 0xF18B;
uint16_t poll_state = PID_WELD_CHECK;
uint16_t incoming_poll = 0;

View file

@ -572,7 +572,7 @@ void FoxessBattery::transmit_can(unsigned long currentMillis) {
}
void FoxessBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "FoxESS HV2600/ECS4100 OEM battery", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = 0; //Startup with no cells, populates later when we know packsize
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;

View file

@ -15,6 +15,7 @@ class FoxessBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "FoxESS HV2600/ECS4100 OEM battery";
private:
static const int MAX_PACK_VOLTAGE_DV = 4672; //467.2V for HS20.8 (used during startup, refined later)

View file

@ -660,7 +660,7 @@ void GeelyGeometryCBattery::transmit_can(unsigned long currentMillis) {
}
void GeelyGeometryCBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Geely Geometry C", 63);
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.number_of_cells = 102; //70kWh pack has 102S, startup in this mode

View file

@ -16,6 +16,7 @@ class GeelyGeometryCBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Geely Geometry C";
BatteryHtmlRenderer& get_status_renderer() { return renderer; }

View file

@ -190,7 +190,7 @@ void ImievCZeroIonBattery::transmit_can(unsigned long currentMillis) {
}
void ImievCZeroIonBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "I-Miev / C-Zero / Ion Triplet", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;

View file

@ -15,6 +15,7 @@ class ImievCZeroIonBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "I-Miev / C-Zero / Ion Triplet";
private:
static const int MAX_PACK_VOLTAGE_DV = 3696; //5000 = 500.0V

View file

@ -13,6 +13,7 @@ class JaguarIpaceBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Jaguar I-PACE";
private:
static const int MAX_PACK_VOLTAGE_DV = 4546; //5000 = 500.0V

View file

@ -1090,7 +1090,7 @@ void KiaEGmpBattery::transmit_can(unsigned long currentMillis) {
}
void KiaEGmpBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Kia/Hyundai EGMP platform", 63);
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.number_of_cells = 192; // TODO: will vary depending on battery

View file

@ -19,6 +19,7 @@ class KiaEGmpBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Kia/Hyundai EGMP platform";
private:
uint16_t estimateSOC(uint16_t packVoltage, uint16_t cellCount, int16_t currentAmps);

View file

@ -464,7 +464,7 @@ void KiaHyundai64Battery::transmit_can(unsigned long currentMillis) {
}
void KiaHyundai64Battery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Kia/Hyundai 64/40kWh battery", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer_battery->info.max_design_voltage_dV = MAX_PACK_VOLTAGE_98S_DV; //Start with 98S value. Precised later
datalayer_battery->info.min_design_voltage_dV = MIN_PACK_VOLTAGE_90S_DV; //Start with 90S value. Precised later

View file

@ -35,6 +35,7 @@ class KiaHyundai64Battery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Kia/Hyundai 64/40kWh battery";
BatteryHtmlRenderer& get_status_renderer() { return renderer; }

View file

@ -214,7 +214,7 @@ void KiaHyundaiHybridBattery::transmit_can(unsigned long currentMillis) {
}
void KiaHyundaiHybridBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Kia/Hyundai Hybrid", 63);
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.number_of_cells = 56; // HEV , TODO: Make dynamic according to HEV/PHEV

View file

@ -15,6 +15,7 @@ class KiaHyundaiHybridBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Kia/Hyundai Hybrid";
private:
static const int MAX_PACK_VOLTAGE_DV = 2550; //5000 = 500.0V

View file

@ -2035,7 +2035,7 @@ void MebBattery::transmit_can(unsigned long currentMillis) {
}
void MebBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Volkswagen Group MEB platform via CAN-FD", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = 108; //Startup in 108S mode. We figure out the actual count later.
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_108S_DV; //Defined later to correct pack size

View file

@ -17,6 +17,7 @@ class MebBattery : public CanBattery {
virtual void transmit_can(unsigned long currentMillis);
bool supports_real_BMS_status() { return true; }
bool supports_charged_energy() { return true; }
static constexpr char* Name = "Volkswagen Group MEB platform via CAN-FD";
BatteryHtmlRenderer& get_status_renderer() { return renderer; }

View file

@ -15,6 +15,7 @@ class Mg5Battery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "MG 5 battery";
private:
static const int MAX_PACK_VOLTAGE_DV = 4040; //5000 = 500.0V

View file

@ -114,7 +114,7 @@ void OrionBms::transmit_can(unsigned long currentMillis) {
}
void OrionBms::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "DIY battery with Orion BMS (Victron setting)", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = NUMBER_OF_CELLS;
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;

View file

@ -15,6 +15,7 @@ class OrionBms : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "DIY battery with Orion BMS (Victron setting)";
private:
/* Change the following to suit your battery */

View file

@ -31,6 +31,7 @@ class PylonBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Pylon compatible battery";
private:
/* Change the following to suit your battery */

View file

@ -207,7 +207,7 @@ void RangeRoverPhevBattery::transmit_can(unsigned long currentMillis) {
}
void RangeRoverPhevBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Range Rover 13kWh PHEV battery (L494/L405)", 63);
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;

View file

@ -15,6 +15,7 @@ class RangeRoverPhevBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Range Rover 13kWh PHEV battery (L494/L405)";
private:
/* Change the following to suit your battery */

View file

@ -181,7 +181,7 @@ void RenaultKangooBattery::transmit_can(unsigned long currentMillis) {
}
void RenaultKangooBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Renault Kangoo", 63);
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;

View file

@ -15,6 +15,7 @@ class RenaultKangooBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Renault Kangoo";
private:
static const int MAX_PACK_VOLTAGE_DV = 4150; //5000 = 500.0V

View file

@ -118,7 +118,7 @@ void RenaultTwizyBattery::transmit_can(unsigned long currentMillis) {
}
void RenaultTwizyBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Renault Twizy", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = 14;
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;

View file

@ -13,6 +13,7 @@ class RenaultTwizyBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Renault Twizy";
private:
static const int MAX_PACK_VOLTAGE_DV = 579; // 57.9V at 100% SOC (with 70% SOH, new one might be higher)

View file

@ -513,7 +513,7 @@ void RenaultZoeGen1Battery::transmit_can(unsigned long currentMillis) {
}
void RenaultZoeGen1Battery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Renault Zoe Gen1 22/40kWh", 63);
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.number_of_cells = 96;

View file

@ -31,6 +31,7 @@ class RenaultZoeGen1Battery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Renault Zoe Gen1 22/40kWh";
BatteryHtmlRenderer& get_status_renderer() { return renderer; }

View file

@ -704,7 +704,7 @@ void RenaultZoeGen2Battery::transmit_can(unsigned long currentMillis) {
}
void RenaultZoeGen2Battery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Renault Zoe Gen2 50kWh", 63);
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.number_of_cells = 96;

View file

@ -15,6 +15,7 @@ class RenaultZoeGen2Battery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Renault Zoe Gen2 50kWh";
bool supports_reset_NVROL() { return true; }

View file

@ -516,7 +516,7 @@ void RjxzsBms::transmit_can(unsigned long currentMillis) {
}
void RjxzsBms::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "RJXZS BMS, DIY battery", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;

View file

@ -15,6 +15,7 @@ class RjxzsBms : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "RJXZS BMS, DIY battery";
private:
/* Tweak these according to your battery build */

View file

@ -319,7 +319,7 @@ void SantaFePhevBattery::transmit_can(unsigned long currentMillis) {
}
void SantaFePhevBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Santa Fe PHEV", 63);
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.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;

View file

@ -27,6 +27,7 @@ class SantaFePhevBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Santa Fe PHEV";
private:
DATALAYER_BATTERY_TYPE* datalayer_battery;

View file

@ -92,7 +92,7 @@ void SimpBmsBattery::transmit_can(unsigned long currentMillis) {
}
void SimpBmsBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "SIMPBMS battery", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = CELL_COUNT;
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;

View file

@ -15,6 +15,7 @@ class SimpBmsBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "SIMPBMS battery";
private:
/* DEFAULT VALUES BMS will send configured */

View file

@ -140,7 +140,7 @@ void SonoBattery::transmit_can(unsigned long currentMillis) {
}
void SonoBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Sono Motors Sion 64kWh LFP ", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = 96;
datalayer.system.status.battery_allows_contactor_closing = true;

View file

@ -15,6 +15,7 @@ class SonoBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Sono Motors Sion 64kWh LFP ";
private:
static const int MAX_PACK_VOLTAGE_DV = 5000; //5000 = 500.0V

View file

@ -377,7 +377,7 @@ void VolvoSpaBattery::transmit_can(unsigned long currentMillis) {
}
void VolvoSpaBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Volvo / Polestar 69/78kWh SPA battery", 63);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = 0; // Initializes when all cells have been read
datalayer.battery.info.total_capacity_Wh = 78200; //Startout in 78kWh mode (This value used for SOC calc)

View file

@ -16,6 +16,7 @@ class VolvoSpaBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Volvo / Polestar 69/78kWh SPA battery";
bool supports_reset_DTC() { return true; }
void reset_DTC() { datalayer_extended.VolvoPolestar.UserRequestDTCreset = true; }

View file

@ -552,8 +552,8 @@ void VolvoSpaHybridBattery::transmit_can(unsigned long currentMillis) {
}
}
void VolvoSpaHybridBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "Volvo PHEV battery", 63); //changed
void VolvoSpaHybridBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, Name, 63); //changed
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.number_of_cells = 102; //was 108, changed
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV;

View file

@ -16,6 +16,7 @@ class VolvoSpaHybridBattery : public CanBattery {
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr char* Name = "Volvo PHEV battery";
bool supports_reset_DTC() { return true; }
void reset_DTC() { datalayer_extended.VolvoHybrid.UserRequestDTCreset = true; }

View file

@ -2,7 +2,6 @@
#define _CANRECEIVER_H
#include "src/devboard/utils/types.h"
#include "src/include.h"
class CanReceiver {
public:

View file

@ -29,6 +29,13 @@ const bool remote_bms_reset_default = true;
#endif
bool remote_bms_reset = remote_bms_reset_default;
#ifdef CONTACTOR_CONTROL_DOUBLE_BATTERY
const bool contactor_control_enabled_double_battery_default = true;
#else
const bool contactor_control_enabled_double_battery_default = false;
#endif
bool contactor_control_enabled_double_battery = contactor_control_enabled_double_battery_default;
// TODO: Ensure valid values at run-time
// Parameters
@ -105,12 +112,15 @@ void init_contactors() {
set(PRECHARGE_PIN, OFF);
}
#ifdef CONTACTOR_CONTROL_DOUBLE_BATTERY
pinMode(SECOND_POSITIVE_CONTACTOR_PIN, OUTPUT);
set(SECOND_POSITIVE_CONTACTOR_PIN, OFF);
pinMode(SECOND_NEGATIVE_CONTACTOR_PIN, OUTPUT);
set(SECOND_NEGATIVE_CONTACTOR_PIN, OFF);
#endif // CONTACTOR_CONTROL_DOUBLE_BATTERY
#if defined(SECOND_POSITIVE_CONTACTOR_PIN) && defined(SECOND_NEGATIVE_CONTACTOR_PIN)
if (contactor_control_enabled_double_battery) {
pinMode(SECOND_POSITIVE_CONTACTOR_PIN, OUTPUT);
set(SECOND_POSITIVE_CONTACTOR_PIN, OFF);
pinMode(SECOND_NEGATIVE_CONTACTOR_PIN, OUTPUT);
set(SECOND_NEGATIVE_CONTACTOR_PIN, OFF);
}
#endif
// Init BMS contactor
#if defined HW_STARK || defined HW_3LB // This hardware has dedicated pin, always enable on start
pinMode(BMS_POWER, OUTPUT); //LilyGo is omitted from this, only enabled if user selects PERIODIC_BMS_RESET

View file

@ -8,6 +8,7 @@
// Settings that can be changed at run-time
extern bool contactor_control_enabled;
extern bool contactor_control_enabled_double_battery;
extern bool pwm_contactor_control;
extern bool periodic_bms_reset;
extern bool remote_bms_reset;

View file

@ -81,6 +81,7 @@ void init_stored_settings() {
user_selected_charger_type = (ChargerType)settings.getUInt("CHGTYPE", (int)ChargerType::None);
user_selected_second_battery = settings.getBool("DBLBTR", false);
contactor_control_enabled = settings.getBool("CNTCTRL", false);
contactor_control_enabled_double_battery = settings.getBool("CNTCTRLDBL", false);
pwm_contactor_control = settings.getBool("PWMCNTCTRL", false);
periodic_bms_reset = settings.getBool("PERBMSRESET", false);
remote_bms_reset = settings.getBool("REMBMSRESET", false);

View file

@ -34,21 +34,25 @@ void store_settings_equipment_stop();
*/
void store_settings();
// Wraps the Preferences object begin/end calls, so that the scope of this object
// runs them automatically (via constructor/destructor).
class BatteryEmulatorSettingsStore {
public:
BatteryEmulatorSettingsStore() { settings.begin("batterySettings", false); }
BatteryEmulatorSettingsStore(bool readOnly = false) {
if (!settings.begin("batterySettings", readOnly)) {
set_event(EVENT_PERSISTENT_SAVE_INFO, 0);
}
}
~BatteryEmulatorSettingsStore() { settings.end(); }
BatteryType get_batterytype() { return (BatteryType)settings.getUInt("BATTTYPE", (int)BatteryType::None); }
uint32_t getUInt(const char* name, uint32_t defaultValue) { return settings.getUInt(name, defaultValue); }
void set_batterytype(BatteryType type) { settings.putUInt("BATTTYPE", (int)type); }
void saveUInt(const char* name, uint32_t value) { settings.putUInt(name, value); }
InverterProtocolType get_invertertype() {
return (InverterProtocolType)settings.getUInt("INVTYPE", (int)InverterProtocolType::None);
}
bool getBool(const char* name) { return settings.getBool(name, false); }
void set_invertertype(InverterProtocolType type) { settings.putUInt("INVTYPE", (int)type); }
void saveBool(const char* name, bool value) { settings.putBool(name, value); }
private:
Preferences settings;

View file

@ -55,161 +55,166 @@ void update_machineryprotection() {
}
// Start checking that the battery is within reason. Incase we see any funny business, raise an event!
// Don't check any battery issues if battery is not configured
if (battery) {
// Pause function is on OR we have a critical fault event active
if (emulator_pause_request_ON || (datalayer.battery.status.bms_status == FAULT)) {
datalayer.battery.status.max_discharge_power_W = 0;
datalayer.battery.status.max_charge_power_W = 0;
}
// Battery is overheated!
if (datalayer.battery.status.temperature_max_dC > BATTERY_MAXTEMPERATURE) {
set_event(EVENT_BATTERY_OVERHEAT, datalayer.battery.status.temperature_max_dC);
} else {
clear_event(EVENT_BATTERY_OVERHEAT);
}
// Battery is frozen!
if (datalayer.battery.status.temperature_min_dC < BATTERY_MINTEMPERATURE) {
set_event(EVENT_BATTERY_FROZEN, datalayer.battery.status.temperature_min_dC);
} else {
clear_event(EVENT_BATTERY_FROZEN);
}
if (labs(datalayer.battery.status.temperature_max_dC - datalayer.battery.status.temperature_min_dC) >
BATTERY_MAX_TEMPERATURE_DEVIATION) {
set_event_latched(EVENT_BATTERY_TEMP_DEVIATION_HIGH,
datalayer.battery.status.temperature_max_dC - datalayer.battery.status.temperature_min_dC);
} else {
clear_event(EVENT_BATTERY_TEMP_DEVIATION_HIGH);
}
// Battery voltage is over designed max voltage!
if (datalayer.battery.status.voltage_dV > datalayer.battery.info.max_design_voltage_dV) {
set_event(EVENT_BATTERY_OVERVOLTAGE, datalayer.battery.status.voltage_dV);
datalayer.battery.status.max_charge_power_W = 0;
} else {
clear_event(EVENT_BATTERY_OVERVOLTAGE);
}
// Battery voltage is under designed min voltage!
if (datalayer.battery.status.voltage_dV < datalayer.battery.info.min_design_voltage_dV) {
set_event(EVENT_BATTERY_UNDERVOLTAGE, datalayer.battery.status.voltage_dV);
datalayer.battery.status.max_discharge_power_W = 0;
} else {
clear_event(EVENT_BATTERY_UNDERVOLTAGE);
}
// Cell overvoltage, further charging not possible. Battery might be imbalanced.
if (datalayer.battery.status.cell_max_voltage_mV >= datalayer.battery.info.max_cell_voltage_mV) {
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
datalayer.battery.status.max_charge_power_W = 0;
}
// Cell CRITICAL overvoltage, critical latching error without automatic reset. Requires user action to inspect battery.
if (datalayer.battery.status.cell_max_voltage_mV >= (datalayer.battery.info.max_cell_voltage_mV + CELL_CRITICAL_MV)) {
set_event(EVENT_CELL_CRITICAL_OVER_VOLTAGE, 0);
}
// Cell undervoltage. Further discharge not possible. Battery might be imbalanced.
if (datalayer.battery.status.cell_min_voltage_mV <= datalayer.battery.info.min_cell_voltage_mV) {
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
datalayer.battery.status.max_discharge_power_W = 0;
}
//Cell CRITICAL undervoltage. critical latching error without automatic reset. Requires user action to inspect battery.
if (datalayer.battery.status.cell_min_voltage_mV <= (datalayer.battery.info.min_cell_voltage_mV - CELL_CRITICAL_MV)) {
set_event(EVENT_CELL_CRITICAL_UNDER_VOLTAGE, 0);
}
// Battery is fully charged. Dont allow any more power into it
// Normally the BMS will send 0W allowed, but this acts as an additional layer of safety
if (datalayer.battery.status.reported_soc == 10000) //Scaled SOC% value is 100.00%
{
if (!battery_full_event_fired) {
set_event(EVENT_BATTERY_FULL, 0);
battery_full_event_fired = true;
// Pause function is on OR we have a critical fault event active
if (emulator_pause_request_ON || (datalayer.battery.status.bms_status == FAULT)) {
datalayer.battery.status.max_discharge_power_W = 0;
datalayer.battery.status.max_charge_power_W = 0;
}
datalayer.battery.status.max_charge_power_W = 0;
} else {
clear_event(EVENT_BATTERY_FULL);
battery_full_event_fired = false;
}
// Battery is empty. Do not allow further discharge.
// Normally the BMS will send 0W allowed, but this acts as an additional layer of safety
if (datalayer.battery.status.bms_status == ACTIVE) {
if (datalayer.battery.status.reported_soc == 0) { //Scaled SOC% value is 0.00%
if (!battery_empty_event_fired) {
set_event(EVENT_BATTERY_EMPTY, 0);
battery_empty_event_fired = true;
}
// Battery is overheated!
if (datalayer.battery.status.temperature_max_dC > BATTERY_MAXTEMPERATURE) {
set_event(EVENT_BATTERY_OVERHEAT, datalayer.battery.status.temperature_max_dC);
} else {
clear_event(EVENT_BATTERY_OVERHEAT);
}
// Battery is frozen!
if (datalayer.battery.status.temperature_min_dC < BATTERY_MINTEMPERATURE) {
set_event(EVENT_BATTERY_FROZEN, datalayer.battery.status.temperature_min_dC);
} else {
clear_event(EVENT_BATTERY_FROZEN);
}
if (labs(datalayer.battery.status.temperature_max_dC - datalayer.battery.status.temperature_min_dC) >
BATTERY_MAX_TEMPERATURE_DEVIATION) {
set_event_latched(EVENT_BATTERY_TEMP_DEVIATION_HIGH,
datalayer.battery.status.temperature_max_dC - datalayer.battery.status.temperature_min_dC);
} else {
clear_event(EVENT_BATTERY_TEMP_DEVIATION_HIGH);
}
// Battery voltage is over designed max voltage!
if (datalayer.battery.status.voltage_dV > datalayer.battery.info.max_design_voltage_dV) {
set_event(EVENT_BATTERY_OVERVOLTAGE, datalayer.battery.status.voltage_dV);
datalayer.battery.status.max_charge_power_W = 0;
} else {
clear_event(EVENT_BATTERY_OVERVOLTAGE);
}
// Battery voltage is under designed min voltage!
if (datalayer.battery.status.voltage_dV < datalayer.battery.info.min_design_voltage_dV) {
set_event(EVENT_BATTERY_UNDERVOLTAGE, datalayer.battery.status.voltage_dV);
datalayer.battery.status.max_discharge_power_W = 0;
} else {
clear_event(EVENT_BATTERY_EMPTY);
battery_empty_event_fired = false;
clear_event(EVENT_BATTERY_UNDERVOLTAGE);
}
}
// Battery is extremely degraded, not fit for secondlifestorage!
if (datalayer.battery.status.soh_pptt < 2500) {
set_event(EVENT_SOH_LOW, datalayer.battery.status.soh_pptt);
} else {
clear_event(EVENT_SOH_LOW);
}
// Cell overvoltage, further charging not possible. Battery might be imbalanced.
if (datalayer.battery.status.cell_max_voltage_mV >= datalayer.battery.info.max_cell_voltage_mV) {
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
datalayer.battery.status.max_charge_power_W = 0;
}
// Cell CRITICAL overvoltage, critical latching error without automatic reset. Requires user action to inspect battery.
if (datalayer.battery.status.cell_max_voltage_mV >=
(datalayer.battery.info.max_cell_voltage_mV + CELL_CRITICAL_MV)) {
set_event(EVENT_CELL_CRITICAL_OVER_VOLTAGE, 0);
}
if (battery && !battery->soc_plausible()) {
set_event(EVENT_SOC_PLAUSIBILITY_ERROR, datalayer.battery.status.real_soc);
}
// Cell undervoltage. Further discharge not possible. Battery might be imbalanced.
if (datalayer.battery.status.cell_min_voltage_mV <= datalayer.battery.info.min_cell_voltage_mV) {
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
datalayer.battery.status.max_discharge_power_W = 0;
}
//Cell CRITICAL undervoltage. critical latching error without automatic reset. Requires user action to inspect battery.
if (datalayer.battery.status.cell_min_voltage_mV <=
(datalayer.battery.info.min_cell_voltage_mV - CELL_CRITICAL_MV)) {
set_event(EVENT_CELL_CRITICAL_UNDER_VOLTAGE, 0);
}
// Check diff between highest and lowest cell
cell_deviation_mV =
std::abs(datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV);
if (cell_deviation_mV > datalayer.battery.info.max_cell_voltage_deviation_mV) {
set_event(EVENT_CELL_DEVIATION_HIGH, (cell_deviation_mV / 20));
} else {
clear_event(EVENT_CELL_DEVIATION_HIGH);
}
// Inverter is charging with more power than battery wants!
if (datalayer.battery.status.active_power_W > 0) { // Charging
if (datalayer.battery.status.active_power_W > (datalayer.battery.status.max_charge_power_W + 2000)) {
if (charge_limit_failures > MAX_CHARGE_DISCHARGE_LIMIT_FAILURES) {
set_event(EVENT_CHARGE_LIMIT_EXCEEDED, 0); // Alert when 2kW over requested max
} else {
charge_limit_failures++;
// Battery is fully charged. Dont allow any more power into it
// Normally the BMS will send 0W allowed, but this acts as an additional layer of safety
if (datalayer.battery.status.reported_soc == 10000) //Scaled SOC% value is 100.00%
{
if (!battery_full_event_fired) {
set_event(EVENT_BATTERY_FULL, 0);
battery_full_event_fired = true;
}
datalayer.battery.status.max_charge_power_W = 0;
} else {
clear_event(EVENT_CHARGE_LIMIT_EXCEEDED);
charge_limit_failures = 0;
clear_event(EVENT_BATTERY_FULL);
battery_full_event_fired = false;
}
}
// Inverter is pulling too much power from battery!
if (datalayer.battery.status.active_power_W < 0) { // Discharging
if (-datalayer.battery.status.active_power_W > (datalayer.battery.status.max_discharge_power_W + 2000)) {
if (discharge_limit_failures > MAX_CHARGE_DISCHARGE_LIMIT_FAILURES) {
set_event(EVENT_DISCHARGE_LIMIT_EXCEEDED, 0); // Alert when 2kW over requested max
// Battery is empty. Do not allow further discharge.
// Normally the BMS will send 0W allowed, but this acts as an additional layer of safety
if (datalayer.battery.status.bms_status == ACTIVE) {
if (datalayer.battery.status.reported_soc == 0) { //Scaled SOC% value is 0.00%
if (!battery_empty_event_fired) {
set_event(EVENT_BATTERY_EMPTY, 0);
battery_empty_event_fired = true;
}
datalayer.battery.status.max_discharge_power_W = 0;
} else {
discharge_limit_failures++;
clear_event(EVENT_BATTERY_EMPTY);
battery_empty_event_fired = false;
}
} else {
clear_event(EVENT_DISCHARGE_LIMIT_EXCEEDED);
discharge_limit_failures = 0;
}
}
// Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error
if (!datalayer.battery.status.CAN_battery_still_alive) {
set_event(EVENT_CAN_BATTERY_MISSING, can_config.battery);
} else {
datalayer.battery.status.CAN_battery_still_alive--;
clear_event(EVENT_CAN_BATTERY_MISSING);
}
// Battery is extremely degraded, not fit for secondlifestorage!
if (datalayer.battery.status.soh_pptt < 2500) {
set_event(EVENT_SOH_LOW, datalayer.battery.status.soh_pptt);
} else {
clear_event(EVENT_SOH_LOW);
}
// Too many malformed CAN messages recieved!
if (datalayer.battery.status.CAN_error_counter > MAX_CAN_FAILURES) {
set_event(EVENT_CAN_CORRUPTED_WARNING, can_config.battery);
} else {
clear_event(EVENT_CAN_CORRUPTED_WARNING);
if (battery && !battery->soc_plausible()) {
set_event(EVENT_SOC_PLAUSIBILITY_ERROR, datalayer.battery.status.real_soc);
}
// Check diff between highest and lowest cell
cell_deviation_mV =
std::abs(datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV);
if (cell_deviation_mV > datalayer.battery.info.max_cell_voltage_deviation_mV) {
set_event(EVENT_CELL_DEVIATION_HIGH, (cell_deviation_mV / 20));
} else {
clear_event(EVENT_CELL_DEVIATION_HIGH);
}
// Inverter is charging with more power than battery wants!
if (datalayer.battery.status.active_power_W > 0) { // Charging
if (datalayer.battery.status.active_power_W > (datalayer.battery.status.max_charge_power_W + 2000)) {
if (charge_limit_failures > MAX_CHARGE_DISCHARGE_LIMIT_FAILURES) {
set_event(EVENT_CHARGE_LIMIT_EXCEEDED, 0); // Alert when 2kW over requested max
} else {
charge_limit_failures++;
}
} else {
clear_event(EVENT_CHARGE_LIMIT_EXCEEDED);
charge_limit_failures = 0;
}
}
// Inverter is pulling too much power from battery!
if (datalayer.battery.status.active_power_W < 0) { // Discharging
if (-datalayer.battery.status.active_power_W > (datalayer.battery.status.max_discharge_power_W + 2000)) {
if (discharge_limit_failures > MAX_CHARGE_DISCHARGE_LIMIT_FAILURES) {
set_event(EVENT_DISCHARGE_LIMIT_EXCEEDED, 0); // Alert when 2kW over requested max
} else {
discharge_limit_failures++;
}
} else {
clear_event(EVENT_DISCHARGE_LIMIT_EXCEEDED);
discharge_limit_failures = 0;
}
}
// Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error
if (!datalayer.battery.status.CAN_battery_still_alive) {
set_event(EVENT_CAN_BATTERY_MISSING, can_config.battery);
} else {
datalayer.battery.status.CAN_battery_still_alive--;
clear_event(EVENT_CAN_BATTERY_MISSING);
}
// Too many malformed CAN messages recieved!
if (datalayer.battery.status.CAN_error_counter > MAX_CAN_FAILURES) {
set_event(EVENT_CAN_CORRUPTED_WARNING, can_config.battery);
} else {
clear_event(EVENT_CAN_CORRUPTED_WARNING);
}
}
if (inverter && inverter->interface_type() == InverterInterfaceType::Can) {

View file

@ -42,6 +42,9 @@ enum PrechargeState {
#define CAN_STILL_ALIVE 60
// Set by battery each time we get a CAN message. Decrements every second. When reaching 0, sets event
typedef enum { CAN_NATIVE = 0, CANFD_NATIVE = 1, CAN_ADDON_MCP2515 = 2, CANFD_ADDON_MCP2518 = 3 } CAN_Interface;
extern const char* getCANInterfaceName(CAN_Interface interface);
/* CAN Frame structure */
typedef struct {
bool FD;

View file

@ -107,29 +107,24 @@ String settings_processor(const String& var) {
"<form action='saveSettings' method='post' style='display: grid; grid-template-columns: 1fr 2fr; gap: 10px; "
"align-items: center;'>";
content += "<label>Battery: </label><select style='max-width: 250px;' name='battery'>";
content += battery_options(settings.get_batterytype());
content += battery_options((BatteryType)settings.getUInt("BATTTYPE", (int)BatteryType::None));
content += "</select>";
content += "<label>Inverter protocol: </label><select style='max-width: 250px;' name='inverter'>";
content += inverter_options(settings.get_invertertype());
content += inverter_options((InverterProtocolType)settings.getUInt("INVTYPE", (int)InverterProtocolType::None));
content += "</select>";
content += "<label>Charger: </label><select style='max-width: 250px;' name='charger'>";
content += charger_options((ChargerType)("CHGTYPE", 0));
content += charger_options((ChargerType)settings.getUInt("CHGTYPE", 0));
content += "</select>";
//content += "<label>Double battery:</label>";
// TODO: Generalize settings: define settings in one place and use the definitions to render
// UI and handle load/save
render_checkbox(content, "Double battery", settings.get_doublebattery(), "dblbtr");
render_checkbox(content, "Contactor control", get_bool("CNTCTRL"), "contctrl");
render_checkbox(content, "PWM contactor control", get_bool("PWMCNTCTRL"), "pwmcontctrl");
render_checkbox(content, "Periodic BMS reset", get_bool("PERBMSRESET"), "PERBMSRESET");
render_checkbox(content, "Remote BMS reset", get_bool("REMBMSRESET"), "REMBMSRESET");
render_checkbox(content, "Double battery", settings.getBool("DBLBTR"), "dblbtr");
render_checkbox(content, "Contactor control", settings.getBool("CNTCTRL"), "contctrl");
render_checkbox(content, "Contactor control double battery", settings.getBool("CNTCTRLDBL"), "contctrldbl");
render_checkbox(content, "PWM contactor control", settings.getBool("PWMCNTCTRL"), "pwmcontctrl");
render_checkbox(content, "Periodic BMS reset", settings.getBool("PERBMSRESET"), "PERBMSRESET");
render_checkbox(content, "Remote BMS reset", settings.getBool("REMBMSRESET"), "REMBMSRESET");
/* content +=
"<div style=\"display: flex; justify-content: flex-start;\"><input id='dblbtr' name='dblbtr' type='checkbox' "
"style=\"margin-left: 0;\"";
content += (user_selected_second_battery ? " checked" : "");
content += " value='on'/></div>";*/
content +=
"<div style='grid-column: span 2; text-align: center; padding-top: 10px;'><button "
"type='submit'>Save</button></div>";

View file

@ -4,6 +4,7 @@
#include "../../../USER_SECRETS.h"
#include "../../battery/BATTERIES.h"
#include "../../battery/Battery.h"
#include "../../communication/contactorcontrol/comm_contactorcontrol.h"
#include "../../communication/nvm/comm_nvm.h"
#include "../../datalayer/datalayer.h"
#include "../../datalayer/datalayer_extended.h"
@ -39,7 +40,7 @@ const char get_firmware_info_html[] = R"rawliteral(%X%)rawliteral";
String importedLogs = ""; // Store the uploaded logfile contents in RAM
bool isReplayRunning = false; // Global flag to track replay state
// True when user has updated settings and a reboot is needed.
// True when user has updated settings that need a reboot to be effective.
bool settingsUpdated = false;
CAN_frame currentFrame = {.FD = true, .ext_ID = false, .DLC = 64, .ID = 0x12F, .data = {0}};
@ -389,30 +390,32 @@ void init_webserver() {
#ifdef COMMON_IMAGE
// Handles the form POST from UI to save certain settings: battery/inverter type and double battery on/off
server.on("/saveSettings", HTTP_POST, [](AsyncWebServerRequest* request) {
BatteryEmulatorSettingsStore settings;
int params = request->params();
// dblbtr not present in form content if not checked.
bool secondBattery = false;
for (int i = 0; i < params; i++) {
auto p = request->getParam(i);
if (p->name() == "inverter") {
auto type = static_cast<InverterProtocolType>(atoi(p->value().c_str()));
store_uint("INVTYPE", (int)type);
settings.saveUInt("INVTYPE", (int)type);
} else if (p->name() == "battery") {
auto type = static_cast<BatteryType>(atoi(p->value().c_str()));
store_uint("BATTTYPE", (int)type);
settings.saveUInt("BATTTYPE", (int)type);
} else if (p->name() == "charger") {
auto type = static_cast<ChargerType>(atoi(p->value().c_str()));
store_uint("CHGTYPE", (int)type);
settings.saveUInt("CHGTYPE", (int)type);
} else if (p->name() == "dblbtr") {
store_bool("DBLBTR", p->value() == "on");
settings.saveBool("DBLBTR", p->value() == "on");
} else if (p->name() == "contctrl") {
store_bool("CNTCTRL", p->value() == "on");
settings.saveBool("CNTCTRL", p->value() == "on");
} else if (p->name() == "contctrldbl") {
settings.saveBool("CNTCTRLDBL", p->value() == "on");
} else if (p->name() == "pwmcontctrl") {
store_bool("PWMCNTCTRL", p->value() == "on");
settings.saveBool("PWMCNTCTRL", p->value() == "on");
} else if (p->name() == "PERBMSRESET") {
store_bool("PERBMSRESET", p->value() == "on");
settings.saveBool("PERBMSRESET", p->value() == "on");
} else if (p->name() == "REMBMSRESET") {
store_bool("REMBMSRESET", p->value() == "on");
settings.saveBool("REMBMSRESET", p->value() == "on");
}
}
@ -937,6 +940,10 @@ String processor(const String& var) {
// Show version number
content += "<h4>Software: " + String(version_number);
#ifdef COMMON_IMAGE
content += " (Common image) ";
#endif
// Show hardware used:
#ifdef HW_LILYGO
content += " Hardware: LilyGo T-CAN485";
@ -980,315 +987,98 @@ String processor(const String& var) {
// Close the block
content += "</div>";
// Start a new block with a specific background color
content += "<div style='background-color: #333; padding: 10px; margin-bottom: 10px; border-radius: 50px'>";
if (inverter || battery || shunt || charger) {
// Start a new block with a specific background color
content += "<div style='background-color: #333; padding: 10px; margin-bottom: 10px; border-radius: 50px'>";
// Display which components are used
if (inverter) {
content += "<h4 style='color: white;'>Inverter protocol: ";
content += datalayer.system.info.inverter_protocol;
content += " ";
content += datalayer.system.info.inverter_brand;
content += "</h4>";
// Display which components are used
if (inverter) {
content += "<h4 style='color: white;'>Inverter protocol: ";
content += datalayer.system.info.inverter_protocol;
content += " ";
content += datalayer.system.info.inverter_brand;
content += "</h4>";
}
if (battery) {
content += "<h4 style='color: white;'>Battery protocol: ";
content += datalayer.system.info.battery_protocol;
if (battery2) {
content += " (Double battery)";
}
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) {
content += " (LFP)";
}
content += "</h4>";
}
if (shunt) {
content += "<h4 style='color: white;'>Shunt protocol: ";
content += datalayer.system.info.shunt_protocol;
content += "</h4>";
}
if (charger) {
content += "<h4 style='color: white;'>Charger protocol: ";
content += charger->name();
content += "</h4>";
}
// Close the block
content += "</div>";
}
if (battery) {
content += "<h4 style='color: white;'>Battery protocol: ";
content += datalayer.system.info.battery_protocol;
if (battery2) {
content += " (Double battery)";
}
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) {
content += " (LFP)";
}
content += "</h4>";
}
if (shunt) {
content += "<h4 style='color: white;'>Shunt protocol: ";
content += datalayer.system.info.shunt_protocol;
content += "</h4>";
}
if (charger) {
content += "<h4 style='color: white;'>Charger protocol: ";
content += charger->name();
content += "</h4>";
}
// Close the block
content += "</div>";
if (battery2) {
// Start a new block with a specific background color. Color changes depending on BMS status
content += "<div style='display: flex; width: 100%;'>";
content += "<div style='flex: 1; background-color: ";
} else {
// Start a new block with a specific background color. Color changes depending on system status
content += "<div style='background-color: ";
}
switch (led_get_color()) {
case led_color::GREEN:
content += "#2D3F2F;";
break;
case led_color::YELLOW:
content += "#F5CC00;";
break;
case led_color::BLUE:
content += "#2B35AF;"; // Blue in test mode
break;
case led_color::RED:
content += "#A70107;";
break;
default: // Some new color, make background green
content += "#2D3F2F;";
break;
}
// Add the common style properties
content += "padding: 10px; margin-bottom: 10px; border-radius: 50px;'>";
// Display battery statistics within this block
float socRealFloat =
static_cast<float>(datalayer.battery.status.real_soc) / 100.0; // Convert to float and divide by 100
float socScaledFloat =
static_cast<float>(datalayer.battery.status.reported_soc) / 100.0; // Convert to float and divide by 100
float sohFloat =
static_cast<float>(datalayer.battery.status.soh_pptt) / 100.0; // Convert to float and divide by 100
float voltageFloat =
static_cast<float>(datalayer.battery.status.voltage_dV) / 10.0; // Convert to float and divide by 10
float currentFloat =
static_cast<float>(datalayer.battery.status.current_dA) / 10.0; // Convert to float and divide by 10
float powerFloat = static_cast<float>(datalayer.battery.status.active_power_W); // Convert to float
float tempMaxFloat = static_cast<float>(datalayer.battery.status.temperature_max_dC) / 10.0; // Convert to float
float tempMinFloat = static_cast<float>(datalayer.battery.status.temperature_min_dC) / 10.0; // Convert to float
float maxCurrentChargeFloat =
static_cast<float>(datalayer.battery.status.max_charge_current_dA) / 10.0; // Convert to float
float maxCurrentDischargeFloat =
static_cast<float>(datalayer.battery.status.max_discharge_current_dA) / 10.0; // Convert to float
uint16_t cell_delta_mv =
datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV;
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) +
"&percnt; (real: " + String(socRealFloat, 2) + "&percnt;)</h4>";
else
content += "<h4 style='color: white;'>SOC: " + String(socRealFloat, 2) + "&percnt;</h4>";
content += "<h4 style='color: white;'>SOH: " + String(sohFloat, 2) + "&percnt;</h4>";
content += "<h4 style='color: white;'>Voltage: " + String(voltageFloat, 1) +
" V &nbsp; Current: " + String(currentFloat, 1) + " A</h4>";
content += formatPowerValue("Power", powerFloat, "", 1);
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled total capacity: " +
formatPowerValue(datalayer.battery.info.reported_total_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery.info.total_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Total capacity", datalayer.battery.info.total_capacity_Wh, "h", 1);
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled remaining capacity: " +
formatPowerValue(datalayer.battery.status.reported_remaining_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery.status.remaining_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Remaining capacity", datalayer.battery.status.remaining_capacity_Wh, "h", 1);
if (datalayer.system.settings.equipment_stop_active) {
content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1, "red");
content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1, "red");
content += "<h4 style='color: red;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A</h4>";
content += "<h4 style='color: red;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A</h4>";
} else {
content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1);
content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1);
content += "<h4 style='color: white;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A";
if (datalayer.battery.settings.user_settings_limit_discharge) {
content += " (Manual)</h4>";
// Start a new block with a specific background color. Color changes depending on BMS status
content += "<div style='display: flex; width: 100%;'>";
content += "<div style='flex: 1; background-color: ";
} else {
content += " (BMS)</h4>";
// Start a new block with a specific background color. Color changes depending on system status
content += "<div style='background-color: ";
}
content += "<h4 style='color: white;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A";
if (datalayer.battery.settings.user_settings_limit_charge) {
content += " (Manual)</h4>";
} else {
content += " (BMS)</h4>";
}
}
content += "<h4>Cell min/max: " + String(datalayer.battery.status.cell_min_voltage_mV) + " mV / " +
String(datalayer.battery.status.cell_max_voltage_mV) + " mV</h4>";
if (cell_delta_mv > datalayer.battery.info.max_cell_voltage_deviation_mV) {
content += "<h4 style='color: red;'>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
} else {
content += "<h4>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
}
content +=
"<h4>Temperature min/max: " + String(tempMinFloat, 1) + " &deg;C / " + String(tempMaxFloat, 1) + " &deg;C</h4>";
content += "<h4>System status: ";
switch (datalayer.battery.status.bms_status) {
case ACTIVE:
content += String("OK");
break;
case UPDATING:
content += String("UPDATING");
break;
case FAULT:
content += String("FAULT");
break;
case INACTIVE:
content += String("INACTIVE");
break;
case STANDBY:
content += String("STANDBY");
break;
default:
content += String("??");
break;
}
content += "</h4>";
if (battery && battery->supports_real_BMS_status()) {
content += "<h4>Battery BMS status: ";
switch (datalayer.battery.status.real_bms_status) {
case BMS_ACTIVE:
content += String("OK");
break;
case BMS_FAULT:
content += String("FAULT");
break;
case BMS_DISCONNECTED:
content += String("DISCONNECTED");
break;
case BMS_STANDBY:
content += String("STANDBY");
break;
default:
content += String("??");
break;
}
content += "</h4>";
}
if (datalayer.battery.status.current_dA == 0) {
content += "<h4>Battery idle</h4>";
} else if (datalayer.battery.status.current_dA < 0) {
content += "<h4>Battery discharging!";
if (datalayer.battery.settings.inverter_limits_discharge) {
content += " (Inverter limiting)</h4>";
} else {
if (datalayer.battery.settings.user_settings_limit_discharge) {
content += " (Settings limiting)</h4>";
} else {
content += " (Battery limiting)</h4>";
}
}
content += "</h4>";
} else { // > 0 , positive current
content += "<h4>Battery charging!";
if (datalayer.battery.settings.inverter_limits_charge) {
content += " (Inverter limiting)</h4>";
} else {
if (datalayer.battery.settings.user_settings_limit_charge) {
content += " (Settings limiting)</h4>";
} else {
content += " (Battery limiting)</h4>";
}
}
}
content += "<h4>Battery allows contactor closing: ";
if (datalayer.system.status.battery_allows_contactor_closing == true) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Inverter allows contactor closing: ";
if (datalayer.system.status.inverter_allows_contactor_closing == true) {
content += "<span>&#10003;</span></h4>";
} else {
content += "<span style='color: red;'>&#10005;</span></h4>";
}
if (emulator_pause_status == NORMAL)
content += "<h4>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
else
content += "<h4 style='color: red;'>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
#ifdef CONTACTOR_CONTROL
content += "<h4>Contactors controlled by emulator, state: ";
if (datalayer.system.status.contactors_engaged) {
content += "<span style='color: green;'>ON</span>";
} else {
content += "<span style='color: red;'>OFF</span>";
}
content += "</h4>";
content += "<h4>Precharge: (";
content += PRECHARGE_TIME_MS;
content += " ms) Cont. Neg.: ";
#ifdef PWM_CONTACTOR_CONTROL
if (datalayer.system.status.contactors_engaged) {
content += "<span style='color: green;'>Economized</span>";
content += " Cont. Pos.: ";
content += "<span style='color: green;'>Economized</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
content += " Cont. Pos.: ";
content += "<span style='color: red;'>&#10005;</span>";
}
#else // No PWM_CONTACTOR_CONTROL , we can read the pin and see feedback. Helpful if channel overloaded
if (digitalRead(NEGATIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Pos.: ";
if (digitalRead(POSITIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
#endif //no PWM_CONTACTOR_CONTROL
content += "</h4>";
#endif
// Close the block
content += "</div>";
if (battery2) {
content += "<div style='flex: 1; background-color: ";
switch (datalayer.battery.status.bms_status) {
case ACTIVE:
switch (led_get_color()) {
case led_color::GREEN:
content += "#2D3F2F;";
break;
case FAULT:
case led_color::YELLOW:
content += "#F5CC00;";
break;
case led_color::BLUE:
content += "#2B35AF;"; // Blue in test mode
break;
case led_color::RED:
content += "#A70107;";
break;
default:
default: // Some new color, make background green
content += "#2D3F2F;";
break;
}
// Add the common style properties
content += "padding: 10px; margin-bottom: 10px; border-radius: 50px;'>";
// Display battery statistics within this block
socRealFloat =
static_cast<float>(datalayer.battery2.status.real_soc) / 100.0; // Convert to float and divide by 100
//socScaledFloat; // Same value used for bat2
sohFloat = static_cast<float>(datalayer.battery2.status.soh_pptt) / 100.0; // Convert to float and divide by 100
voltageFloat =
static_cast<float>(datalayer.battery2.status.voltage_dV) / 10.0; // Convert to float and divide by 10
currentFloat =
static_cast<float>(datalayer.battery2.status.current_dA) / 10.0; // Convert to float and divide by 10
powerFloat = static_cast<float>(datalayer.battery2.status.active_power_W); // Convert to float
tempMaxFloat = static_cast<float>(datalayer.battery2.status.temperature_max_dC) / 10.0; // Convert to float
tempMinFloat = static_cast<float>(datalayer.battery2.status.temperature_min_dC) / 10.0; // Convert to float
cell_delta_mv = datalayer.battery2.status.cell_max_voltage_mV - datalayer.battery2.status.cell_min_voltage_mV;
float socRealFloat =
static_cast<float>(datalayer.battery.status.real_soc) / 100.0; // Convert to float and divide by 100
float socScaledFloat =
static_cast<float>(datalayer.battery.status.reported_soc) / 100.0; // Convert to float and divide by 100
float sohFloat =
static_cast<float>(datalayer.battery.status.soh_pptt) / 100.0; // Convert to float and divide by 100
float voltageFloat =
static_cast<float>(datalayer.battery.status.voltage_dV) / 10.0; // Convert to float and divide by 10
float currentFloat =
static_cast<float>(datalayer.battery.status.current_dA) / 10.0; // Convert to float and divide by 10
float powerFloat = static_cast<float>(datalayer.battery.status.active_power_W); // Convert to float
float tempMaxFloat = static_cast<float>(datalayer.battery.status.temperature_max_dC) / 10.0; // Convert to float
float tempMinFloat = static_cast<float>(datalayer.battery.status.temperature_min_dC) / 10.0; // Convert to float
float maxCurrentChargeFloat =
static_cast<float>(datalayer.battery.status.max_charge_current_dA) / 10.0; // Convert to float
float maxCurrentDischargeFloat =
static_cast<float>(datalayer.battery.status.max_discharge_current_dA) / 10.0; // Convert to float
uint16_t cell_delta_mv =
datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV;
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) +
@ -1303,117 +1093,342 @@ String processor(const String& var) {
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled total capacity: " +
formatPowerValue(datalayer.battery2.info.reported_total_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery2.info.total_capacity_Wh, "h", 1) + ")</h4>";
formatPowerValue(datalayer.battery.info.reported_total_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery.info.total_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Total capacity", datalayer.battery2.info.total_capacity_Wh, "h", 1);
content += formatPowerValue("Total capacity", datalayer.battery.info.total_capacity_Wh, "h", 1);
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled remaining capacity: " +
formatPowerValue(datalayer.battery2.status.reported_remaining_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery2.status.remaining_capacity_Wh, "h", 1) + ")</h4>";
formatPowerValue(datalayer.battery.status.reported_remaining_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery.status.remaining_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Remaining capacity", datalayer.battery2.status.remaining_capacity_Wh, "h", 1);
content += formatPowerValue("Remaining capacity", datalayer.battery.status.remaining_capacity_Wh, "h", 1);
if (datalayer.system.settings.equipment_stop_active) {
content +=
formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1, "red");
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1, "red");
formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1, "red");
content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1, "red");
content += "<h4 style='color: red;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A</h4>";
content += "<h4 style='color: red;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A</h4>";
} else {
content += formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1);
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1);
content +=
"<h4 style='color: white;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A</h4>";
content += "<h4 style='color: white;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A</h4>";
content += formatPowerValue("Max discharge power", datalayer.battery.status.max_discharge_power_W, "", 1);
content += formatPowerValue("Max charge power", datalayer.battery.status.max_charge_power_W, "", 1);
content += "<h4 style='color: white;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A";
if (datalayer.battery.settings.user_settings_limit_discharge) {
content += " (Manual)</h4>";
} else {
content += " (BMS)</h4>";
}
content += "<h4 style='color: white;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A";
if (datalayer.battery.settings.user_settings_limit_charge) {
content += " (Manual)</h4>";
} else {
content += " (BMS)</h4>";
}
}
content += "<h4>Cell min/max: " + String(datalayer.battery2.status.cell_min_voltage_mV) + " mV / " +
String(datalayer.battery2.status.cell_max_voltage_mV) + " mV</h4>";
if (cell_delta_mv > datalayer.battery2.info.max_cell_voltage_deviation_mV) {
content += "<h4>Cell min/max: " + String(datalayer.battery.status.cell_min_voltage_mV) + " mV / " +
String(datalayer.battery.status.cell_max_voltage_mV) + " mV</h4>";
if (cell_delta_mv > datalayer.battery.info.max_cell_voltage_deviation_mV) {
content += "<h4 style='color: red;'>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
} else {
content += "<h4>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
}
content += "<h4>Temperature min/max: " + String(tempMinFloat, 1) + " &deg;C / " + String(tempMaxFloat, 1) +
" &deg;C</h4>";
if (datalayer.battery.status.bms_status == ACTIVE) {
content += "<h4>System status: OK </h4>";
} else if (datalayer.battery.status.bms_status == UPDATING) {
content += "<h4>System status: UPDATING </h4>";
} else {
content += "<h4>System status: FAULT </h4>";
content += "<h4>System status: ";
switch (datalayer.battery.status.bms_status) {
case ACTIVE:
content += String("OK");
break;
case UPDATING:
content += String("UPDATING");
break;
case FAULT:
content += String("FAULT");
break;
case INACTIVE:
content += String("INACTIVE");
break;
case STANDBY:
content += String("STANDBY");
break;
default:
content += String("??");
break;
}
if (datalayer.battery2.status.current_dA == 0) {
content += "<h4>Battery idle</h4>";
} else if (datalayer.battery2.status.current_dA < 0) {
content += "<h4>Battery discharging!</h4>";
} else { // > 0
content += "<h4>Battery charging!</h4>";
content += "</h4>";
if (battery && battery->supports_real_BMS_status()) {
content += "<h4>Battery BMS status: ";
switch (datalayer.battery.status.real_bms_status) {
case BMS_ACTIVE:
content += String("OK");
break;
case BMS_FAULT:
content += String("FAULT");
break;
case BMS_DISCONNECTED:
content += String("DISCONNECTED");
break;
case BMS_STANDBY:
content += String("STANDBY");
break;
default:
content += String("??");
break;
}
content += "</h4>";
}
content += "<h4>Automatic contactor closing allowed:</h4>";
content += "<h4>Battery: ";
if (datalayer.system.status.battery2_allowed_contactor_closing == true) {
if (datalayer.battery.status.current_dA == 0) {
content += "<h4>Battery idle</h4>";
} else if (datalayer.battery.status.current_dA < 0) {
content += "<h4>Battery discharging!";
if (datalayer.battery.settings.inverter_limits_discharge) {
content += " (Inverter limiting)</h4>";
} else {
if (datalayer.battery.settings.user_settings_limit_discharge) {
content += " (Settings limiting)</h4>";
} else {
content += " (Battery limiting)</h4>";
}
}
content += "</h4>";
} else { // > 0 , positive current
content += "<h4>Battery charging!";
if (datalayer.battery.settings.inverter_limits_charge) {
content += " (Inverter limiting)</h4>";
} else {
if (datalayer.battery.settings.user_settings_limit_charge) {
content += " (Settings limiting)</h4>";
} else {
content += " (Battery limiting)</h4>";
}
}
}
content += "<h4>Battery allows contactor closing: ";
if (datalayer.system.status.battery_allows_contactor_closing == true) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Inverter: ";
content += " Inverter allows contactor closing: ";
if (datalayer.system.status.inverter_allows_contactor_closing == true) {
content += "<span>&#10003;</span></h4>";
} else {
content += "<span style='color: red;'>&#10005;</span></h4>";
}
if (emulator_pause_status == NORMAL)
content += "<h4>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
else
content += "<h4 style='color: red;'>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
#ifdef CONTACTOR_CONTROL
content += "<h4>Contactors controlled by emulator, state: ";
if (datalayer.system.status.contactors_battery2_engaged) {
content += "<span style='color: green;'>ON</span>";
} else {
content += "<span style='color: red;'>OFF</span>";
}
content += "</h4>";
#ifdef CONTACTOR_CONTROL_DOUBLE_BATTERY
content += "<h4>Cont. Neg.: ";
#ifdef PWM_CONTACTOR_CONTROL
if (datalayer.system.status.contactors_battery2_engaged) {
content += "<span style='color: green;'>Economized</span>";
content += " Cont. Pos.: ";
content += "<span style='color: green;'>Economized</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
content += " Cont. Pos.: ";
content += "<span style='color: red;'>&#10005;</span>";
if (contactor_control_enabled) {
content += "<h4>Contactors controlled by emulator, state: ";
if (datalayer.system.status.contactors_engaged) {
content += "<span style='color: green;'>ON</span>";
} else {
content += "<span style='color: red;'>OFF</span>";
}
content += "</h4>";
content += "<h4>Precharge: (";
content += PRECHARGE_TIME_MS;
content += " ms) Cont. Neg.: ";
if (pwm_contactor_control) {
if (datalayer.system.status.contactors_engaged) {
content += "<span style='color: green;'>Economized</span>";
content += " Cont. Pos.: ";
content += "<span style='color: green;'>Economized</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
content += " Cont. Pos.: ";
content += "<span style='color: red;'>&#10005;</span>";
}
} else { // No PWM_CONTACTOR_CONTROL , we can read the pin and see feedback. Helpful if channel overloaded
if (digitalRead(NEGATIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Pos.: ";
if (digitalRead(POSITIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
}
content += "</h4>";
}
#else // No PWM_CONTACTOR_CONTROL , we can read the pin and see feedback. Helpful if channel overloaded
if (digitalRead(SECOND_NEGATIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Pos.: ";
if (digitalRead(SECOND_POSITIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
#endif //no PWM_CONTACTOR_CONTROL
content += "</h4>";
#endif // CONTACTOR_CONTROL_DOUBLE_BATTERY
#endif // CONTACTOR_CONTROL
content += "</div>";
// Close the block
content += "</div>";
if (battery2) {
content += "<div style='flex: 1; background-color: ";
switch (datalayer.battery.status.bms_status) {
case ACTIVE:
content += "#2D3F2F;";
break;
case FAULT:
content += "#A70107;";
break;
default:
content += "#2D3F2F;";
break;
}
// Add the common style properties
content += "padding: 10px; margin-bottom: 10px; border-radius: 50px;'>";
// Display battery statistics within this block
socRealFloat =
static_cast<float>(datalayer.battery2.status.real_soc) / 100.0; // Convert to float and divide by 100
//socScaledFloat; // Same value used for bat2
sohFloat =
static_cast<float>(datalayer.battery2.status.soh_pptt) / 100.0; // Convert to float and divide by 100
voltageFloat =
static_cast<float>(datalayer.battery2.status.voltage_dV) / 10.0; // Convert to float and divide by 10
currentFloat =
static_cast<float>(datalayer.battery2.status.current_dA) / 10.0; // Convert to float and divide by 10
powerFloat = static_cast<float>(datalayer.battery2.status.active_power_W); // Convert to float
tempMaxFloat = static_cast<float>(datalayer.battery2.status.temperature_max_dC) / 10.0; // Convert to float
tempMinFloat = static_cast<float>(datalayer.battery2.status.temperature_min_dC) / 10.0; // Convert to float
cell_delta_mv = datalayer.battery2.status.cell_max_voltage_mV - datalayer.battery2.status.cell_min_voltage_mV;
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled SOC: " + String(socScaledFloat, 2) +
"&percnt; (real: " + String(socRealFloat, 2) + "&percnt;)</h4>";
else
content += "<h4 style='color: white;'>SOC: " + String(socRealFloat, 2) + "&percnt;</h4>";
content += "<h4 style='color: white;'>SOH: " + String(sohFloat, 2) + "&percnt;</h4>";
content += "<h4 style='color: white;'>Voltage: " + String(voltageFloat, 1) +
" V &nbsp; Current: " + String(currentFloat, 1) + " A</h4>";
content += formatPowerValue("Power", powerFloat, "", 1);
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled total capacity: " +
formatPowerValue(datalayer.battery2.info.reported_total_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery2.info.total_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Total capacity", datalayer.battery2.info.total_capacity_Wh, "h", 1);
if (datalayer.battery.settings.soc_scaling_active)
content += "<h4 style='color: white;'>Scaled remaining capacity: " +
formatPowerValue(datalayer.battery2.status.reported_remaining_capacity_Wh, "h", 1) +
" (real: " + formatPowerValue(datalayer.battery2.status.remaining_capacity_Wh, "h", 1) + ")</h4>";
else
content += formatPowerValue("Remaining capacity", datalayer.battery2.status.remaining_capacity_Wh, "h", 1);
if (datalayer.system.settings.equipment_stop_active) {
content +=
formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1, "red");
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1, "red");
content +=
"<h4 style='color: red;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A</h4>";
content += "<h4 style='color: red;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A</h4>";
} else {
content += formatPowerValue("Max discharge power", datalayer.battery2.status.max_discharge_power_W, "", 1);
content += formatPowerValue("Max charge power", datalayer.battery2.status.max_charge_power_W, "", 1);
content +=
"<h4 style='color: white;'>Max discharge current: " + String(maxCurrentDischargeFloat, 1) + " A</h4>";
content += "<h4 style='color: white;'>Max charge current: " + String(maxCurrentChargeFloat, 1) + " A</h4>";
}
content += "<h4>Cell min/max: " + String(datalayer.battery2.status.cell_min_voltage_mV) + " mV / " +
String(datalayer.battery2.status.cell_max_voltage_mV) + " mV</h4>";
if (cell_delta_mv > datalayer.battery2.info.max_cell_voltage_deviation_mV) {
content += "<h4 style='color: red;'>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
} else {
content += "<h4>Cell delta: " + String(cell_delta_mv) + " mV</h4>";
}
content += "<h4>Temperature min/max: " + String(tempMinFloat, 1) + " &deg;C / " + String(tempMaxFloat, 1) +
" &deg;C</h4>";
if (datalayer.battery.status.bms_status == ACTIVE) {
content += "<h4>System status: OK </h4>";
} else if (datalayer.battery.status.bms_status == UPDATING) {
content += "<h4>System status: UPDATING </h4>";
} else {
content += "<h4>System status: FAULT </h4>";
}
if (datalayer.battery2.status.current_dA == 0) {
content += "<h4>Battery idle</h4>";
} else if (datalayer.battery2.status.current_dA < 0) {
content += "<h4>Battery discharging!</h4>";
} else { // > 0
content += "<h4>Battery charging!</h4>";
}
content += "<h4>Automatic contactor closing allowed:</h4>";
content += "<h4>Battery: ";
if (datalayer.system.status.battery2_allowed_contactor_closing == true) {
content += "<span>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Inverter: ";
if (datalayer.system.status.inverter_allows_contactor_closing == true) {
content += "<span>&#10003;</span></h4>";
} else {
content += "<span style='color: red;'>&#10005;</span></h4>";
}
if (emulator_pause_status == NORMAL)
content += "<h4>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
else
content += "<h4 style='color: red;'>Power status: " + String(get_emulator_pause_status().c_str()) + " </h4>";
if (contactor_control_enabled) {
content += "<h4>Contactors controlled by emulator, state: ";
if (datalayer.system.status.contactors_battery2_engaged) {
content += "<span style='color: green;'>ON</span>";
} else {
content += "<span style='color: red;'>OFF</span>";
}
content += "</h4>";
if (contactor_control_enabled_double_battery) {
content += "<h4>Cont. Neg.: ";
if (pwm_contactor_control) {
if (datalayer.system.status.contactors_battery2_engaged) {
content += "<span style='color: green;'>Economized</span>";
content += " Cont. Pos.: ";
content += "<span style='color: green;'>Economized</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
content += " Cont. Pos.: ";
content += "<span style='color: red;'>&#10005;</span>";
}
} else { // No PWM_CONTACTOR_CONTROL , we can read the pin and see feedback. Helpful if channel overloaded
#if defined(SECOND_POSITIVE_CONTACTOR_PIN) && defined(SECOND_NEGATIVE_CONTACTOR_PIN)
if (digitalRead(SECOND_NEGATIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
content += " Cont. Pos.: ";
if (digitalRead(SECOND_POSITIVE_CONTACTOR_PIN) == HIGH) {
content += "<span style='color: green;'>&#10003;</span>";
} else {
content += "<span style='color: red;'>&#10005;</span>";
}
#endif
}
content += "</h4>";
}
}
content += "</div>";
content += "</div>";
}
}
if (charger) {