mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-03 09:49:32 +02:00
428 lines
16 KiB
C++
428 lines
16 KiB
C++
#include "../../datalayer/datalayer.h"
|
|
#include "../utils/events.h"
|
|
|
|
static uint16_t cell_deviation_mV = 0;
|
|
static uint8_t charge_limit_failures = 0;
|
|
static uint8_t discharge_limit_failures = 0;
|
|
static bool battery_full_event_fired = false;
|
|
static bool battery_empty_event_fired = false;
|
|
|
|
#define MAX_SOH_DEVIATION_PPTT 2500
|
|
#define CELL_CRITICAL_MV 100 // If cells go this much outside design voltage, shut battery down!
|
|
|
|
//battery pause status begin
|
|
bool emulator_pause_request_ON = false;
|
|
bool emulator_pause_CAN_send_ON = false;
|
|
bool allowed_to_send_CAN = true;
|
|
|
|
battery_pause_status emulator_pause_status = NORMAL;
|
|
//battery pause status end
|
|
|
|
void update_machineryprotection() {
|
|
/* Check if the ESP32 CPU running the Battery-Emulator is too hot.
|
|
We start with a warning, you can start to see Wifi issues if it becomes too hot
|
|
If the chip starts to approach the design limit, we perform a graceful shutdown */
|
|
if (datalayer.system.info.CPU_temperature > 80.0f) {
|
|
set_event(EVENT_CPU_OVERHEATING, 0);
|
|
} else {
|
|
clear_event(EVENT_CPU_OVERHEATING);
|
|
}
|
|
if (datalayer.system.info.CPU_temperature > 110.0f) {
|
|
set_event(EVENT_CPU_OVERHEATED, 0);
|
|
}
|
|
if (datalayer.system.info.CPU_temperature < 105.0f) {
|
|
clear_event(EVENT_CPU_OVERHEATED); //Hysteresis on the clearing
|
|
}
|
|
|
|
// Check health status of CAN interfaces
|
|
if (datalayer.system.info.can_native_send_fail) {
|
|
set_event(EVENT_CAN_NATIVE_TX_FAILURE, 0);
|
|
datalayer.system.info.can_native_send_fail = false;
|
|
} else {
|
|
clear_event(EVENT_CAN_NATIVE_TX_FAILURE);
|
|
}
|
|
if (datalayer.system.info.can_2515_send_fail) {
|
|
set_event(EVENT_CAN_BUFFER_FULL, 0);
|
|
datalayer.system.info.can_2515_send_fail = false;
|
|
} else {
|
|
clear_event(EVENT_CAN_BUFFER_FULL);
|
|
}
|
|
if (datalayer.system.info.can_2518_send_fail) {
|
|
set_event(EVENT_CANFD_BUFFER_FULL, 0);
|
|
datalayer.system.info.can_2518_send_fail = false;
|
|
} else {
|
|
clear_event(EVENT_CANFD_BUFFER_FULL);
|
|
}
|
|
|
|
// Start checking that the battery is within reason. Incase we see any funny business, raise an event!
|
|
|
|
// 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;
|
|
}
|
|
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;
|
|
}
|
|
datalayer.battery.status.max_discharge_power_W = 0;
|
|
} else {
|
|
clear_event(EVENT_BATTERY_EMPTY);
|
|
battery_empty_event_fired = false;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
#ifdef NISSAN_LEAF_BATTERY
|
|
// Check if SOC% is plausible
|
|
if (datalayer.battery.status.voltage_dV >
|
|
(datalayer.battery.info.max_design_voltage_dV -
|
|
100)) { // When pack voltage is close to max, and SOC% is still low, raise event
|
|
if (datalayer.battery.status.real_soc < 6500) { // 65.00%
|
|
set_event(EVENT_SOC_PLAUSIBILITY_ERROR, datalayer.battery.status.real_soc);
|
|
} else {
|
|
clear_event(EVENT_SOC_PLAUSIBILITY_ERROR);
|
|
}
|
|
}
|
|
#endif //NISSAN_LEAF_BATTERY
|
|
|
|
// 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);
|
|
}
|
|
|
|
#ifdef CAN_INVERTER_SELECTED
|
|
// Check if the inverter is still sending CAN messages. If we go 60s without messages we raise a warning
|
|
if (!datalayer.system.status.CAN_inverter_still_alive) {
|
|
set_event(EVENT_CAN_INVERTER_MISSING, can_config.inverter);
|
|
} else {
|
|
datalayer.system.status.CAN_inverter_still_alive--;
|
|
clear_event(EVENT_CAN_INVERTER_MISSING);
|
|
}
|
|
#endif //CAN_INVERTER_SELECTED
|
|
|
|
if (charger) {
|
|
// Check if the charger is still sending CAN messages. If we go 60s without messages we raise a warning
|
|
if (!datalayer.charger.CAN_charger_still_alive) {
|
|
set_event(EVENT_CAN_CHARGER_MISSING, can_config.charger);
|
|
} else {
|
|
datalayer.charger.CAN_charger_still_alive--;
|
|
clear_event(EVENT_CAN_CHARGER_MISSING);
|
|
}
|
|
}
|
|
|
|
// Additional Double-Battery safeties are checked here
|
|
if (battery2) {
|
|
// Check if the Battery 2 BMS is still sending CAN messages. If we go 60s without messages we raise a warning
|
|
|
|
// Pause function is on
|
|
if (emulator_pause_request_ON) {
|
|
datalayer.battery2.status.max_discharge_power_W = 0;
|
|
datalayer.battery2.status.max_charge_power_W = 0;
|
|
}
|
|
|
|
if (!datalayer.battery2.status.CAN_battery_still_alive) {
|
|
set_event(EVENT_CAN_BATTERY2_MISSING, can_config.battery_double);
|
|
} else {
|
|
datalayer.battery2.status.CAN_battery_still_alive--;
|
|
clear_event(EVENT_CAN_BATTERY2_MISSING);
|
|
}
|
|
|
|
// Too many malformed CAN messages recieved!
|
|
if (datalayer.battery2.status.CAN_error_counter > MAX_CAN_FAILURES) {
|
|
set_event(EVENT_CAN_CORRUPTED_WARNING, can_config.battery_double);
|
|
} else {
|
|
clear_event(EVENT_CAN_CORRUPTED_WARNING);
|
|
}
|
|
|
|
// Cell overvoltage, critical latching error without automatic reset. Requires user action.
|
|
if (datalayer.battery2.status.cell_max_voltage_mV >= datalayer.battery2.info.max_cell_voltage_mV) {
|
|
set_event(EVENT_CELL_OVER_VOLTAGE, 0);
|
|
}
|
|
// Cell undervoltage, critical latching error without automatic reset. Requires user action.
|
|
if (datalayer.battery2.status.cell_min_voltage_mV <= datalayer.battery2.info.min_cell_voltage_mV) {
|
|
set_event(EVENT_CELL_UNDER_VOLTAGE, 0);
|
|
}
|
|
|
|
// Check diff between highest and lowest cell
|
|
cell_deviation_mV = (datalayer.battery2.status.cell_max_voltage_mV - datalayer.battery2.status.cell_min_voltage_mV);
|
|
if (cell_deviation_mV > datalayer.battery2.info.max_cell_voltage_deviation_mV) {
|
|
set_event(EVENT_CELL_DEVIATION_HIGH, (cell_deviation_mV / 20));
|
|
} else {
|
|
clear_event(EVENT_CELL_DEVIATION_HIGH);
|
|
}
|
|
|
|
// Check if SOH% between the packs is too large
|
|
if ((datalayer.battery.status.soh_pptt != 9900) && (datalayer.battery2.status.soh_pptt != 9900)) {
|
|
// Both values available, check diff
|
|
uint16_t soh_diff_pptt;
|
|
if (datalayer.battery.status.soh_pptt > datalayer.battery2.status.soh_pptt) {
|
|
soh_diff_pptt = datalayer.battery.status.soh_pptt - datalayer.battery2.status.soh_pptt;
|
|
} else {
|
|
soh_diff_pptt = datalayer.battery2.status.soh_pptt - datalayer.battery.status.soh_pptt;
|
|
}
|
|
|
|
if (soh_diff_pptt > MAX_SOH_DEVIATION_PPTT) {
|
|
set_event(EVENT_SOH_DIFFERENCE, (uint8_t)(MAX_SOH_DEVIATION_PPTT / 100));
|
|
} else {
|
|
clear_event(EVENT_SOH_DIFFERENCE);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Safeties verified, Zero charge/discharge ampere values incase any safety wrote the W to 0
|
|
if (datalayer.battery.status.max_discharge_power_W == 0) {
|
|
datalayer.battery.status.max_discharge_current_dA = 0;
|
|
}
|
|
if (datalayer.battery.status.max_charge_power_W == 0) {
|
|
datalayer.battery.status.max_charge_current_dA = 0;
|
|
}
|
|
|
|
//Decrement the forced balancing timer incase user requested it
|
|
if (datalayer.battery.settings.user_requests_balancing) {
|
|
// If this is the start of the balancing period, capture the current time
|
|
if (datalayer.battery.settings.balancing_start_time_ms == 0) {
|
|
datalayer.battery.settings.balancing_start_time_ms = millis();
|
|
set_event(EVENT_BALANCING_START, 0);
|
|
} else {
|
|
clear_event(EVENT_BALANCING_START);
|
|
}
|
|
|
|
// Check if the elapsed time exceeds the balancing time
|
|
if (millis() - datalayer.battery.settings.balancing_start_time_ms >= datalayer.battery.settings.balancing_time_ms) {
|
|
datalayer.battery.settings.user_requests_balancing = false;
|
|
datalayer.battery.settings.balancing_start_time_ms = 0; // Reset the start time
|
|
set_event(EVENT_BALANCING_END, 0);
|
|
} else {
|
|
clear_event(EVENT_BALANCING_END);
|
|
}
|
|
}
|
|
}
|
|
|
|
//battery pause status begin
|
|
void setBatteryPause(bool pause_battery, bool pause_CAN, bool equipment_stop, bool store_settings) {
|
|
|
|
// First handle equipment stop / resume
|
|
if (equipment_stop && !datalayer.system.settings.equipment_stop_active) {
|
|
datalayer.system.settings.equipment_stop_active = true;
|
|
if (store_settings) {
|
|
store_settings_equipment_stop();
|
|
}
|
|
|
|
set_event(EVENT_EQUIPMENT_STOP, 1);
|
|
} else if (!equipment_stop && datalayer.system.settings.equipment_stop_active) {
|
|
datalayer.system.settings.equipment_stop_active = false;
|
|
if (store_settings) {
|
|
store_settings_equipment_stop();
|
|
}
|
|
clear_event(EVENT_EQUIPMENT_STOP);
|
|
}
|
|
|
|
emulator_pause_CAN_send_ON = pause_CAN;
|
|
|
|
if (pause_battery) {
|
|
|
|
set_event(EVENT_PAUSE_BEGIN, 1);
|
|
emulator_pause_request_ON = true;
|
|
emulator_pause_status = PAUSING;
|
|
datalayer.battery.status.max_discharge_power_W = 0;
|
|
datalayer.battery.status.max_charge_power_W = 0;
|
|
if (battery2) {
|
|
datalayer.battery2.status.max_discharge_power_W = 0;
|
|
datalayer.battery2.status.max_charge_power_W = 0;
|
|
}
|
|
|
|
} else {
|
|
clear_event(EVENT_PAUSE_BEGIN);
|
|
set_event(EVENT_PAUSE_END, 0);
|
|
emulator_pause_request_ON = false;
|
|
emulator_pause_CAN_send_ON = false;
|
|
emulator_pause_status = RESUMING;
|
|
clear_event(EVENT_PAUSE_END);
|
|
}
|
|
|
|
//immediate check if we can send CAN messages
|
|
update_pause_state();
|
|
}
|
|
|
|
/// @brief handle emulator pause status and CAN sending allowed
|
|
void update_pause_state() {
|
|
bool previous_allowed_to_send_CAN = allowed_to_send_CAN;
|
|
|
|
if (emulator_pause_status == NORMAL) {
|
|
allowed_to_send_CAN = true;
|
|
}
|
|
|
|
// in some inverters this values are not accurate, so we need to check if we are consider 1.8 amps as the limit
|
|
if (emulator_pause_request_ON && emulator_pause_status == PAUSING && datalayer.battery.status.current_dA < 18 &&
|
|
datalayer.battery.status.current_dA > -18) {
|
|
emulator_pause_status = PAUSED;
|
|
}
|
|
|
|
if (!emulator_pause_request_ON && emulator_pause_status == RESUMING) {
|
|
emulator_pause_status = NORMAL;
|
|
allowed_to_send_CAN = true;
|
|
}
|
|
|
|
allowed_to_send_CAN = (!emulator_pause_CAN_send_ON || emulator_pause_status == NORMAL);
|
|
|
|
if (previous_allowed_to_send_CAN && !allowed_to_send_CAN) {
|
|
#ifdef DEBUG_LOG
|
|
logging.printf("Safety: Pausing CAN sending\n");
|
|
#endif
|
|
//completely force stop the CAN communication
|
|
ESP32Can.CANStop(); //Note: This only stops the NATIVE_CAN port, it will no longer ACK messages
|
|
} else if (!previous_allowed_to_send_CAN && allowed_to_send_CAN) {
|
|
//resume CAN communication
|
|
#ifdef DEBUG_LOG
|
|
logging.printf("Safety: Resuming CAN sending\n");
|
|
#endif
|
|
ESP32Can.CANInit(); //Note: This only resumes the NATIVE_CAN port
|
|
}
|
|
}
|
|
|
|
std::string get_emulator_pause_status() {
|
|
switch (emulator_pause_status) {
|
|
case NORMAL:
|
|
return "RUNNING";
|
|
case PAUSING:
|
|
return "PAUSING";
|
|
case PAUSED:
|
|
return "PAUSED";
|
|
case RESUMING:
|
|
return "RESUMING";
|
|
default:
|
|
return "UNKNOWN";
|
|
}
|
|
}
|
|
//battery pause status
|