Improvement support # Please enter a commit message to explain why this merge is necessary,
This commit is contained in:
Nicolas MAINIL 2025-03-03 17:38:30 +01:00
commit f315321c24
21 changed files with 630 additions and 301 deletions

View file

@ -122,6 +122,10 @@ void setup_can_shunt();
#include "SANTA-FE-PHEV-BATTERY.h"
#endif
#ifdef SIMPBMS_BATTERY
#include "SIMPBMS-BATTERY.h"
#endif
#if defined(TESLA_MODEL_SX_BATTERY) || defined(TESLA_MODEL_3Y_BATTERY)
#define TESLA_BATTERY
#include "TESLA-BATTERY.h"

View file

@ -216,7 +216,7 @@ static uint32_t battery_BEV_available_power_shortterm_charge = 0;
static uint32_t battery_BEV_available_power_shortterm_discharge = 0;
static uint32_t battery_BEV_available_power_longterm_charge = 0;
static uint32_t battery_BEV_available_power_longterm_discharge = 0;
static uint16_t battery_energy_content_maximum_kWh = 0;
static uint16_t battery_energy_content_maximum_Wh = 0;
static uint16_t battery_display_SOC = 0;
static uint16_t battery_volts = 0;
static uint16_t battery_HVBatt_SOC = 0;
@ -284,7 +284,7 @@ static uint32_t battery2_BEV_available_power_shortterm_charge = 0;
static uint32_t battery2_BEV_available_power_shortterm_discharge = 0;
static uint32_t battery2_BEV_available_power_longterm_charge = 0;
static uint32_t battery2_BEV_available_power_longterm_discharge = 0;
static uint16_t battery2_energy_content_maximum_kWh = 0;
static uint16_t battery2_energy_content_maximum_Wh = 0;
static uint16_t battery2_display_SOC = 0;
static uint16_t battery2_volts = 0;
static uint16_t battery2_HVBatt_SOC = 0;
@ -373,7 +373,7 @@ void update_values_battery2() { //This function maps all the values fetched via
datalayer.battery2.status.current_dA = battery2_current;
datalayer.battery2.info.total_capacity_Wh = (battery2_energy_content_maximum_kWh * 1000); // Convert kWh to Wh
datalayer.battery2.info.total_capacity_Wh = battery2_energy_content_maximum_Wh;
datalayer.battery2.status.remaining_capacity_Wh = battery2_predicted_energy_charge_condition;
@ -443,7 +443,7 @@ void update_values_battery() { //This function maps all the values fetched via
datalayer.battery.status.current_dA = battery_current;
datalayer.battery.info.total_capacity_Wh = (battery_energy_content_maximum_kWh * 1000); // Convert kWh to Wh
datalayer.battery.info.total_capacity_Wh = battery_energy_content_maximum_Wh;
datalayer.battery.status.remaining_capacity_Wh = battery_predicted_energy_charge_condition;
@ -611,10 +611,10 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
battery_request_abort_charging = (rx_frame.data.u8[0] & 0x30) >> 4;
battery_prediction_duration_charging_minutes = (rx_frame.data.u8[3] << 8 | rx_frame.data.u8[2]);
battery_prediction_time_end_of_charging_minutes = rx_frame.data.u8[4];
battery_energy_content_maximum_kWh = (((rx_frame.data.u8[6] & 0x0F) << 8 | rx_frame.data.u8[5])) / 50;
if (battery_energy_content_maximum_kWh > 33) {
battery_energy_content_maximum_Wh = (((rx_frame.data.u8[6] & 0x0F) << 8) | rx_frame.data.u8[5]) * 20;
if (battery_energy_content_maximum_Wh > 33000) {
detectedBattery = BATTERY_120AH;
} else if (battery_energy_content_maximum_kWh > 20) {
} else if (battery_energy_content_maximum_Wh > 20000) {
detectedBattery = BATTERY_94AH;
} else {
detectedBattery = BATTERY_60AH;
@ -797,10 +797,10 @@ void handle_incoming_can_frame_battery2(CAN_frame rx_frame) {
battery2_request_abort_charging = (rx_frame.data.u8[0] & 0x30) >> 4;
battery2_prediction_duration_charging_minutes = (rx_frame.data.u8[3] << 8 | rx_frame.data.u8[2]);
battery2_prediction_time_end_of_charging_minutes = rx_frame.data.u8[4];
battery2_energy_content_maximum_kWh = (((rx_frame.data.u8[6] & 0x0F) << 8 | rx_frame.data.u8[5])) / 50;
if (battery2_energy_content_maximum_kWh > 33) {
battery2_energy_content_maximum_Wh = (((rx_frame.data.u8[6] & 0x0F) << 8) | rx_frame.data.u8[5]) * 20;
if (battery2_energy_content_maximum_Wh > 33000) {
detectedBattery2 = BATTERY_120AH;
} else if (battery2_energy_content_maximum_kWh > 20) {
} else if (battery2_energy_content_maximum_Wh > 20000) {
detectedBattery2 = BATTERY_94AH;
} else {
detectedBattery2 = BATTERY_60AH;

View file

@ -36,6 +36,9 @@ static int16_t BMS_highest_cell_temperature = 0;
static int16_t BMS_average_cell_temperature = 0;
static uint16_t BMS_lowest_cell_voltage_mV = 3300;
static uint16_t BMS_highest_cell_voltage_mV = 3300;
static uint8_t battery_frame_index = 0;
#define NOF_CELLS 126
static uint16_t battery_cellvoltages[NOF_CELLS] = {0};
#ifdef DOUBLE_BATTERY
static int16_t battery2_temperature_ambient = 0;
static int16_t battery2_daughterboard_temperatures[10];
@ -52,6 +55,8 @@ static int16_t BMS2_highest_cell_temperature = 0;
static int16_t BMS2_average_cell_temperature = 0;
static uint16_t BMS2_lowest_cell_voltage_mV = 3300;
static uint16_t BMS2_highest_cell_voltage_mV = 3300;
static uint8_t battery2_frame_index = 0;
static uint16_t battery2_cellvoltages[NOF_CELLS] = {0};
#endif //DOUBLE_BATTERY
#define POLL_FOR_BATTERY_SOC 0x05
#define POLL_FOR_BATTERY_VOLTAGE 0x08
@ -135,6 +140,9 @@ void update_values_battery() { //This function maps all the values fetched via
datalayer.battery.status.cell_min_voltage_mV = BMS_lowest_cell_voltage_mV;
//Map all cell voltages to the global array
memcpy(datalayer.battery.status.cell_voltages_mV, battery_cellvoltages, NOF_CELLS * sizeof(uint16_t));
#ifdef SKIP_TEMPERATURE_SENSOR_NUMBER
// Initialize min and max variables for temperature calculation
battery_calc_min_temperature = battery_daughterboard_temperatures[0];
@ -253,6 +261,15 @@ void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
break;
case 0x43D:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
battery_frame_index = rx_frame.data.u8[0];
if (battery_frame_index < (NOF_CELLS / 3)) {
uint8_t base_index = battery_frame_index * 3;
for (uint8_t i = 0; i < 3; i++) {
battery_cellvoltages[base_index + i] =
(((rx_frame.data.u8[2 * (i + 1)] & 0x0F) << 8) | rx_frame.data.u8[2 * i + 1]);
}
}
break;
case 0x444:
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
@ -479,6 +496,9 @@ void update_values_battery2() { //This function maps all the values fetched via
datalayer.battery2.status.temperature_min_dC = BMS2_lowest_cell_temperature * 10; // Add decimals
datalayer.battery2.status.temperature_max_dC = BMS2_highest_cell_temperature * 10;
//Map all cell voltages to the global array
memcpy(datalayer.battery2.status.cell_voltages_mV, battery2_cellvoltages, NOF_CELLS * sizeof(uint16_t));
}
void handle_incoming_can_frame_battery2(CAN_frame rx_frame) {
@ -547,6 +567,14 @@ void handle_incoming_can_frame_battery2(CAN_frame rx_frame) {
break;
case 0x43D:
datalayer.battery2.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
battery2_frame_index = rx_frame.data.u8[0];
if (battery2_frame_index < (NOF_CELLS / 3)) {
uint8_t base2_index = battery2_frame_index * 3;
for (uint8_t i = 0; i < 3; i++) {
battery2_cellvoltages[base2_index + i] =
(((rx_frame.data.u8[2 * (i + 1)] & 0x0F) << 8) | rx_frame.data.u8[2 * i + 1]);
}
}
break;
case 0x444:
datalayer.battery2.status.CAN_battery_still_alive = CAN_STILL_ALIVE;

View file

@ -1075,7 +1075,7 @@ void transmit_can_battery() {
unsigned long currentMillis = millis();
if (datalayer.system.status.BMS_reset_in_progress) {
if (datalayer.system.status.BMS_reset_in_progress || datalayer.system.status.BMS_startup_in_progress) {
// Transmitting towards battery is halted while BMS is being reset
// Reset sending counters to avoid overrun messages when reset is over
previousMillis10 = currentMillis;

View file

@ -0,0 +1,131 @@
#include "../include.h"
#ifdef SIMPBMS_BATTERY
#include "../datalayer/datalayer.h"
#include "../devboard/utils/events.h"
#include "SIMPBMS-BATTERY.h"
#define SIMPBMS_MAX_CELLS 128
/* Do not change code below unless you are sure what you are doing */
static unsigned long previousMillis1000 = 0; // will store last time a 1s CAN Message was sent
//Actual content messages
static int16_t celltemperature_max_dC = 0;
static int16_t celltemperature_min_dC = 0;
static int16_t current_dA = 0;
static uint16_t voltage_dV = 0;
static uint16_t cellvoltage_max_mV = 3700;
static uint16_t cellvoltage_min_mV = 3700;
static uint16_t charge_cutoff_voltage = 0;
static uint16_t discharge_cutoff_voltage = 0;
static int16_t max_charge_current = 0;
static int16_t max_discharge_current = 0;
static uint8_t ensemble_info_ack = 0;
static uint8_t cells_in_series = 0;
static uint8_t voltage_level = 0;
static uint8_t ah_total = 0;
static uint8_t SOC = 0;
static uint8_t SOH = 99;
static uint8_t charge_forbidden = 0;
static uint8_t discharge_forbidden = 0;
static uint16_t cellvoltages_mV[SIMPBMS_MAX_CELLS] = {0};
void update_values_battery() {
datalayer.battery.status.real_soc = (SOC * 100); //increase SOC range from 0-100 -> 100.00
datalayer.battery.status.soh_pptt = (SOH * 100); //Increase decimals from 100% -> 100.00%
datalayer.battery.status.voltage_dV = voltage_dV; //value is *10 (3700 = 370.0)
datalayer.battery.status.current_dA = current_dA; //value is *10 (150 = 15.0) , invert the sign
datalayer.battery.status.max_charge_power_W = (max_charge_current * (voltage_dV / 10));
datalayer.battery.status.max_discharge_power_W = (max_discharge_current * (voltage_dV / 10));
datalayer.battery.info.total_capacity_Wh = ah_total * (voltage_dV / 10);
datalayer.battery.status.remaining_capacity_Wh = static_cast<uint32_t>(
(static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh);
datalayer.battery.status.cell_max_voltage_mV = cellvoltage_max_mV;
datalayer.battery.status.cell_min_voltage_mV = cellvoltage_min_mV;
datalayer.battery.status.temperature_min_dC = celltemperature_min_dC;
datalayer.battery.status.temperature_max_dC = celltemperature_max_dC;
datalayer.battery.info.max_design_voltage_dV = charge_cutoff_voltage;
datalayer.battery.info.min_design_voltage_dV = discharge_cutoff_voltage;
memcpy(datalayer.battery.status.cell_voltages_mV, cellvoltages_mV, SIMPBMS_MAX_CELLS * sizeof(uint16_t));
datalayer.battery.info.number_of_cells = cells_in_series;
}
void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
switch (rx_frame.ID) {
case 0x355:
SOC = (rx_frame.data.u8[1] << 8) + rx_frame.data.u8[0];
SOH = (rx_frame.data.u8[3] << 8) + rx_frame.data.u8[2];
break;
case 0x351:
//discharge and charge limits
charge_cutoff_voltage = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]);
discharge_cutoff_voltage = ((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]);
max_charge_current = (((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2])) / 10;
max_discharge_current = (((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4])) / 10;
break;
case 0x356:
//current status
current_dA = ((rx_frame.data.u8[3] << 8) + rx_frame.data.u8[2]) / 1000;
voltage_dV = ((rx_frame.data.u8[1] << 8) + rx_frame.data.u8[0]) / 10;
break;
case 0x373:
//min/max values
cellvoltage_max_mV = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]);
cellvoltage_min_mV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]);
celltemperature_max_dC = (((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]) - 273.15) * 10;
celltemperature_min_dC = (((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]) - 273.15) * 10;
break;
case 0x372:
//cells_in_series = rx_frame.data.u8[0];
if (rx_frame.data.u8[3] > 0 && rx_frame.data.u8[3] <= SIMPBMS_MAX_CELLS) {
uint8_t cellnumber = rx_frame.data.u8[3];
if (cellnumber > cells_in_series) {
cells_in_series = cellnumber;
}
cellvoltages_mV[cellnumber - 1] = ((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]);
}
break;
case 0x379:
ah_total = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]);
break;
default:
break;
}
}
void transmit_can_battery() {}
void setup_battery(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, "SIMPBMS battery", 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;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV;
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV;
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV;
datalayer.system.status.battery_allows_contactor_closing = true;
}
#endif

View file

@ -0,0 +1,19 @@
#ifndef SIMPBMS_BATTERY_H
#define SIMPBMS_BATTERY_H
#include <Arduino.h>
#include "../include.h"
#define BATTERY_SELECTED
/* DEFAULT VALUES BMS will send configured */
#define MAX_PACK_VOLTAGE_DV 5000 //5000 = 500.0V
#define MIN_PACK_VOLTAGE_DV 1500
#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value
#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value
#define MAX_CELL_DEVIATION_MV 500
#define CELL_COUNT 96
void setup_battery(void);
void transmit_can_frame(CAN_frame* tx_frame, int interface);
#endif

View file

@ -41,8 +41,10 @@ unsigned long timeSpentInFaultedMode = 0;
#endif
unsigned long currentTime = 0;
unsigned long lastPowerRemovalTime = 0;
unsigned long bmsPowerOnTime = 0;
const unsigned long powerRemovalInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const unsigned long powerRemovalDuration = 30000; // 30 seconds in milliseconds
const unsigned long bmsWarmupDuration = 3000;
void set(uint8_t pin, bool direction, uint32_t pwm_freq = 0xFFFF) {
#ifdef PWM_CONTACTOR_CONTROL
@ -249,23 +251,29 @@ void handle_BMSpower() {
#ifdef PERIODIC_BMS_RESET
// Check if 24 hours have passed since the last power removal
if (currentTime - lastPowerRemovalTime >= powerRemovalInterval) {
if ((currentTime + bmsResetTimeOffset) - lastPowerRemovalTime >= powerRemovalInterval) {
start_bms_reset();
}
#endif //PERIODIC_BMS_RESET
// If power has been removed for 30 seconds, restore the power and resume the emulator
// If power has been removed for 30 seconds, restore the power
if (datalayer.system.status.BMS_reset_in_progress && currentTime - lastPowerRemovalTime >= powerRemovalDuration) {
// Reapply power to the BMS
digitalWrite(BMS_POWER, HIGH);
#ifdef BMS_2_POWER
digitalWrite(BMS_2_POWER, HIGH); // Same for battery 2
#endif
bmsPowerOnTime = currentTime;
datalayer.system.status.BMS_reset_in_progress = false; // Reset the power removal flag
datalayer.system.status.BMS_startup_in_progress = true; // Set the BMS warmup flag
}
//if power has been restored we need to wait a couple of seconds to unpause the battery
if (datalayer.system.status.BMS_startup_in_progress && currentTime - bmsPowerOnTime >= bmsWarmupDuration) {
//Resume from the power pause
setBatteryPause(false, false, false, false);
datalayer.system.status.BMS_reset_in_progress = false; // Reset the power removal flag
datalayer.system.status.BMS_startup_in_progress = false; // Reset the BMS warmup removal flag
set_event(EVENT_PERIODIC_BMS_RESET, 0);
}
#endif //defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
}
@ -274,9 +282,10 @@ void start_bms_reset() {
#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
if (!datalayer.system.status.BMS_reset_in_progress) {
lastPowerRemovalTime = currentTime; // Record the time when BMS reset was started
// we are now resetting at the correct time. We don't need to offset anymore
bmsResetTimeOffset = 0;
// Set a flag to let the rest of the system know we are cutting power to the BMS.
// The battery CAN sending routine will then know not to try to send anything towards battery while active
// The battery CAN sending routine will then know not to try guto send anything towards battery while active
datalayer.system.status.BMS_reset_in_progress = true;
// Set emulator state to paused (Max Charge/Discharge = 0 & CAN = stop)

View file

@ -97,6 +97,10 @@ typedef struct {
/** The current battery status, which for now has the name real_bms_status */
real_bms_status_enum real_bms_status = BMS_DISCONNECTED;
/** LED mode, customizable by user */
led_mode_enum led_mode = LED_MODE;
} DATALAYER_BATTERY_STATUS_TYPE;
typedef struct {
@ -288,6 +292,8 @@ typedef struct {
#endif
/** True if the BMS is being reset, by cutting power towards it */
bool BMS_reset_in_progress = false;
/** True if the BMS is starting up */
bool BMS_startup_in_progress = false;
#ifdef PRECHARGE_CONTROL
/** State of automatic precharge sequence */
PrechargeState precharge_status = AUTO_PRECHARGE_IDLE;

View file

@ -219,6 +219,9 @@ void init_events(void) {
events.entries[EVENT_MQTT_DISCONNECT].level = EVENT_LEVEL_INFO;
events.entries[EVENT_EQUIPMENT_STOP].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_SD_INIT_FAILED].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_PERIODIC_BMS_RESET].level = EVENT_LEVEL_INFO;
events.entries[EVENT_PERIODIC_BMS_RESET_AT_INIT_SUCCESS].level = EVENT_LEVEL_INFO;
events.entries[EVENT_PERIODIC_BMS_RESET_AT_INIT_FAILED].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_BATTERY_TEMP_DEVIATION_HIGH].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_EEPROM_WRITE].log = false; // Don't log the logger...
@ -453,6 +456,13 @@ const char* get_event_message_string(EVENTS_ENUM_TYPE event) {
return "EQUIPMENT STOP ACTIVATED!!!";
case EVENT_SD_INIT_FAILED:
return "SD card initialization failed, check hardware. Power must be removed to reset the SD card.";
case EVENT_PERIODIC_BMS_RESET:
return "BMS Reset Event Completed.";
case EVENT_PERIODIC_BMS_RESET_AT_INIT_SUCCESS:
return "Successfully syncronised with the NTP Server. BMS will reset every 24 hours at defined time";
case EVENT_PERIODIC_BMS_RESET_AT_INIT_FAILED:
return "Failed to syncronise with the NTP Server. BMS will reset every 24 hours from when the emulator was "
"powered on";
default:
return "";
}

View file

@ -25,99 +25,102 @@
* - Increment EE_MAGIC_HEADER_VALUE in case you've changed the order
*/
#define EVENTS_ENUM_TYPE(XX) \
XX(EVENT_CANMCP2517FD_INIT_FAILURE) \
XX(EVENT_CANMCP2515_INIT_FAILURE) \
XX(EVENT_CANFD_BUFFER_FULL) \
XX(EVENT_CAN_BUFFER_FULL) \
XX(EVENT_CAN_OVERRUN) \
XX(EVENT_CAN_CORRUPTED_WARNING) \
XX(EVENT_CAN_BATTERY_MISSING) \
XX(EVENT_CAN_BATTERY2_MISSING) \
XX(EVENT_CAN_CHARGER_MISSING) \
XX(EVENT_CAN_INVERTER_MISSING) \
XX(EVENT_CAN_NATIVE_TX_FAILURE) \
XX(EVENT_CHARGE_LIMIT_EXCEEDED) \
XX(EVENT_CONTACTOR_WELDED) \
XX(EVENT_DISCHARGE_LIMIT_EXCEEDED) \
XX(EVENT_WATER_INGRESS) \
XX(EVENT_12V_LOW) \
XX(EVENT_SOC_PLAUSIBILITY_ERROR) \
XX(EVENT_SOC_UNAVAILABLE) \
XX(EVENT_STALE_VALUE) \
XX(EVENT_KWH_PLAUSIBILITY_ERROR) \
XX(EVENT_BALANCING_START) \
XX(EVENT_BALANCING_END) \
XX(EVENT_BATTERY_EMPTY) \
XX(EVENT_BATTERY_FULL) \
XX(EVENT_BATTERY_FUSE) \
XX(EVENT_BATTERY_FROZEN) \
XX(EVENT_BATTERY_CAUTION) \
XX(EVENT_BATTERY_CHG_STOP_REQ) \
XX(EVENT_BATTERY_DISCHG_STOP_REQ) \
XX(EVENT_BATTERY_CHG_DISCHG_STOP_REQ) \
XX(EVENT_BATTERY_OVERHEAT) \
XX(EVENT_BATTERY_OVERVOLTAGE) \
XX(EVENT_BATTERY_UNDERVOLTAGE) \
XX(EVENT_BATTERY_VALUE_UNAVAILABLE) \
XX(EVENT_BATTERY_ISOLATION) \
XX(EVENT_BATTERY_REQUESTS_HEAT) \
XX(EVENT_BATTERY_WARMED_UP) \
XX(EVENT_VOLTAGE_DIFFERENCE) \
XX(EVENT_SOH_DIFFERENCE) \
XX(EVENT_SOH_LOW) \
XX(EVENT_HVIL_FAILURE) \
XX(EVENT_PRECHARGE_FAILURE) \
XX(EVENT_INTERNAL_OPEN_FAULT) \
XX(EVENT_INVERTER_OPEN_CONTACTOR) \
XX(EVENT_INTERFACE_MISSING) \
XX(EVENT_MODBUS_INVERTER_MISSING) \
XX(EVENT_ERROR_OPEN_CONTACTOR) \
XX(EVENT_CELL_CRITICAL_UNDER_VOLTAGE) \
XX(EVENT_CELL_CRITICAL_OVER_VOLTAGE) \
XX(EVENT_CELL_UNDER_VOLTAGE) \
XX(EVENT_CELL_OVER_VOLTAGE) \
XX(EVENT_CELL_DEVIATION_HIGH) \
XX(EVENT_UNKNOWN_EVENT_SET) \
XX(EVENT_OTA_UPDATE) \
XX(EVENT_OTA_UPDATE_TIMEOUT) \
XX(EVENT_DUMMY_INFO) \
XX(EVENT_DUMMY_DEBUG) \
XX(EVENT_DUMMY_WARNING) \
XX(EVENT_DUMMY_ERROR) \
XX(EVENT_PERSISTENT_SAVE_INFO) \
XX(EVENT_SERIAL_RX_WARNING) \
XX(EVENT_SERIAL_RX_FAILURE) \
XX(EVENT_SERIAL_TX_FAILURE) \
XX(EVENT_SERIAL_TRANSMITTER_FAILURE) \
XX(EVENT_EEPROM_WRITE) \
XX(EVENT_RESET_UNKNOWN) \
XX(EVENT_RESET_POWERON) \
XX(EVENT_RESET_EXT) \
XX(EVENT_RESET_SW) \
XX(EVENT_RESET_PANIC) \
XX(EVENT_RESET_INT_WDT) \
XX(EVENT_RESET_TASK_WDT) \
XX(EVENT_RESET_WDT) \
XX(EVENT_RESET_DEEPSLEEP) \
XX(EVENT_RESET_BROWNOUT) \
XX(EVENT_RESET_SDIO) \
XX(EVENT_RESET_USB) \
XX(EVENT_RESET_JTAG) \
XX(EVENT_RESET_EFUSE) \
XX(EVENT_RESET_PWR_GLITCH) \
XX(EVENT_RESET_CPU_LOCKUP) \
XX(EVENT_RJXZS_LOG) \
XX(EVENT_PAUSE_BEGIN) \
XX(EVENT_PAUSE_END) \
XX(EVENT_WIFI_CONNECT) \
XX(EVENT_WIFI_DISCONNECT) \
XX(EVENT_MQTT_CONNECT) \
XX(EVENT_MQTT_DISCONNECT) \
XX(EVENT_EQUIPMENT_STOP) \
XX(EVENT_AUTOMATIC_PRECHARGE_FAILURE) \
XX(EVENT_SD_INIT_FAILED) \
XX(EVENT_BATTERY_TEMP_DEVIATION_HIGH) \
#define EVENTS_ENUM_TYPE(XX) \
XX(EVENT_CANMCP2517FD_INIT_FAILURE) \
XX(EVENT_CANMCP2515_INIT_FAILURE) \
XX(EVENT_CANFD_BUFFER_FULL) \
XX(EVENT_CAN_BUFFER_FULL) \
XX(EVENT_CAN_OVERRUN) \
XX(EVENT_CAN_CORRUPTED_WARNING) \
XX(EVENT_CAN_BATTERY_MISSING) \
XX(EVENT_CAN_BATTERY2_MISSING) \
XX(EVENT_CAN_CHARGER_MISSING) \
XX(EVENT_CAN_INVERTER_MISSING) \
XX(EVENT_CAN_NATIVE_TX_FAILURE) \
XX(EVENT_CHARGE_LIMIT_EXCEEDED) \
XX(EVENT_CONTACTOR_WELDED) \
XX(EVENT_DISCHARGE_LIMIT_EXCEEDED) \
XX(EVENT_WATER_INGRESS) \
XX(EVENT_12V_LOW) \
XX(EVENT_SOC_PLAUSIBILITY_ERROR) \
XX(EVENT_SOC_UNAVAILABLE) \
XX(EVENT_STALE_VALUE) \
XX(EVENT_KWH_PLAUSIBILITY_ERROR) \
XX(EVENT_BALANCING_START) \
XX(EVENT_BALANCING_END) \
XX(EVENT_BATTERY_EMPTY) \
XX(EVENT_BATTERY_FULL) \
XX(EVENT_BATTERY_FUSE) \
XX(EVENT_BATTERY_FROZEN) \
XX(EVENT_BATTERY_CAUTION) \
XX(EVENT_BATTERY_CHG_STOP_REQ) \
XX(EVENT_BATTERY_DISCHG_STOP_REQ) \
XX(EVENT_BATTERY_CHG_DISCHG_STOP_REQ) \
XX(EVENT_BATTERY_OVERHEAT) \
XX(EVENT_BATTERY_OVERVOLTAGE) \
XX(EVENT_BATTERY_UNDERVOLTAGE) \
XX(EVENT_BATTERY_VALUE_UNAVAILABLE) \
XX(EVENT_BATTERY_ISOLATION) \
XX(EVENT_BATTERY_REQUESTS_HEAT) \
XX(EVENT_BATTERY_WARMED_UP) \
XX(EVENT_VOLTAGE_DIFFERENCE) \
XX(EVENT_SOH_DIFFERENCE) \
XX(EVENT_SOH_LOW) \
XX(EVENT_HVIL_FAILURE) \
XX(EVENT_PRECHARGE_FAILURE) \
XX(EVENT_INTERNAL_OPEN_FAULT) \
XX(EVENT_INVERTER_OPEN_CONTACTOR) \
XX(EVENT_INTERFACE_MISSING) \
XX(EVENT_MODBUS_INVERTER_MISSING) \
XX(EVENT_ERROR_OPEN_CONTACTOR) \
XX(EVENT_CELL_CRITICAL_UNDER_VOLTAGE) \
XX(EVENT_CELL_CRITICAL_OVER_VOLTAGE) \
XX(EVENT_CELL_UNDER_VOLTAGE) \
XX(EVENT_CELL_OVER_VOLTAGE) \
XX(EVENT_CELL_DEVIATION_HIGH) \
XX(EVENT_UNKNOWN_EVENT_SET) \
XX(EVENT_OTA_UPDATE) \
XX(EVENT_OTA_UPDATE_TIMEOUT) \
XX(EVENT_DUMMY_INFO) \
XX(EVENT_DUMMY_DEBUG) \
XX(EVENT_DUMMY_WARNING) \
XX(EVENT_DUMMY_ERROR) \
XX(EVENT_PERSISTENT_SAVE_INFO) \
XX(EVENT_SERIAL_RX_WARNING) \
XX(EVENT_SERIAL_RX_FAILURE) \
XX(EVENT_SERIAL_TX_FAILURE) \
XX(EVENT_SERIAL_TRANSMITTER_FAILURE) \
XX(EVENT_EEPROM_WRITE) \
XX(EVENT_RESET_UNKNOWN) \
XX(EVENT_RESET_POWERON) \
XX(EVENT_RESET_EXT) \
XX(EVENT_RESET_SW) \
XX(EVENT_RESET_PANIC) \
XX(EVENT_RESET_INT_WDT) \
XX(EVENT_RESET_TASK_WDT) \
XX(EVENT_RESET_WDT) \
XX(EVENT_RESET_DEEPSLEEP) \
XX(EVENT_RESET_BROWNOUT) \
XX(EVENT_RESET_SDIO) \
XX(EVENT_RESET_USB) \
XX(EVENT_RESET_JTAG) \
XX(EVENT_RESET_EFUSE) \
XX(EVENT_RESET_PWR_GLITCH) \
XX(EVENT_RESET_CPU_LOCKUP) \
XX(EVENT_RJXZS_LOG) \
XX(EVENT_PAUSE_BEGIN) \
XX(EVENT_PAUSE_END) \
XX(EVENT_WIFI_CONNECT) \
XX(EVENT_WIFI_DISCONNECT) \
XX(EVENT_MQTT_CONNECT) \
XX(EVENT_MQTT_DISCONNECT) \
XX(EVENT_EQUIPMENT_STOP) \
XX(EVENT_AUTOMATIC_PRECHARGE_FAILURE) \
XX(EVENT_SD_INIT_FAILED) \
XX(EVENT_PERIODIC_BMS_RESET) \
XX(EVENT_PERIODIC_BMS_RESET_AT_INIT_SUCCESS) \
XX(EVENT_PERIODIC_BMS_RESET_AT_INIT_FAILED) \
XX(EVENT_BATTERY_TEMP_DEVIATION_HIGH) \
XX(EVENT_NOF_EVENTS)
typedef enum { EVENTS_ENUM_TYPE(GENERATE_ENUM) } EVENTS_ENUM_TYPE;

View file

@ -16,7 +16,7 @@ static const float heartbeat_peak1 = 0.80;
static const float heartbeat_peak2 = 0.55;
static const float heartbeat_deviation = 0.05;
static LED led(LED_MODE_DEFAULT);
static LED led(datalayer.battery.status.led_mode);
void led_init(void) {
led.init();
@ -31,14 +31,14 @@ led_color led_get_color() {
void LED::exe(void) {
// Update brightness
switch (mode) {
case led_mode::FLOW:
switch (datalayer.battery.status.led_mode) {
case led_mode_enum::FLOW:
flow_run();
break;
case led_mode::HEARTBEAT:
case led_mode_enum::HEARTBEAT:
heartbeat_run();
break;
case led_mode::CLASSIC:
case led_mode_enum::CLASSIC:
default:
classic_run();
break;

View file

@ -4,8 +4,6 @@
#include "../../include.h"
#include "../../lib/adafruit-Adafruit_NeoPixel/Adafruit_NeoPixel.h"
enum led_mode { CLASSIC, FLOW, HEARTBEAT };
class LED {
public:
led_color color = led_color::GREEN;
@ -14,9 +12,9 @@ class LED {
: pixels(1, LED_PIN, NEO_GRB + NEO_KHZ800),
max_brightness(LED_MAX_BRIGHTNESS),
brightness(LED_MAX_BRIGHTNESS),
mode(led_mode::CLASSIC) {}
mode(led_mode_enum::CLASSIC) {}
LED(led_mode mode)
LED(led_mode_enum mode)
: pixels(1, LED_PIN, NEO_GRB + NEO_KHZ800),
max_brightness(LED_MAX_BRIGHTNESS),
brightness(LED_MAX_BRIGHTNESS),
@ -29,7 +27,7 @@ class LED {
Adafruit_NeoPixel pixels;
uint8_t max_brightness;
uint8_t brightness;
led_mode mode;
led_mode_enum mode;
void classic_run(void);
void flow_run(void);

View file

@ -0,0 +1,63 @@
#include "ntp_time.h"
#include "../../include.h"
#include "time.h"
const unsigned long millisInADay = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
unsigned long long getNtpTimeInMillis() {
configTzTime(time_zone, ntpServer1, ntpServer2);
struct tm timeinfo;
// Wait for time to be set
for (int i = 0; i < 5; i++) {
if (getLocalTime(&timeinfo)) {
logging.println("Time retrieved from NTP Server");
break;
}
logging.println("Waiting for NTP time...");
}
if (!getLocalTime(&timeinfo)) {
logging.println("Failed to obtain time");
return 0;
}
// Convert to milliseconds
time_t epochTime = mktime(&timeinfo);
return static_cast<unsigned long long>(epochTime) * 1000;
}
// Function to calculate the difference in milliseconds to the next target time
unsigned long long millisToNextTargetTime(unsigned long long currentMillis, int targetTime) {
int hour = targetTime / 100;
int minute = targetTime % 100;
time_t currentTime = currentMillis / 1000; // Convert milliseconds to seconds
struct tm* timeinfo = localtime(&currentTime);
// Set timeinfo to the target time on the next day
timeinfo->tm_hour = hour;
timeinfo->tm_min = minute;
timeinfo->tm_sec = 0;
// Increment day if the current time is past the target time
if (mktime(timeinfo) <= currentTime) {
timeinfo->tm_mday += 1;
}
time_t nextTargetTime = mktime(timeinfo);
unsigned long long nextTargetMillis = static_cast<unsigned long long>(nextTargetTime) * 1000;
return nextTargetMillis - currentMillis;
}
unsigned long long getTimeOffsetfromNowUntil(int targetTime) {
logging.println("Getting time offset from now until " + String(targetTime));
unsigned long long timeinMillis = getNtpTimeInMillis();
if (timeinMillis != 0) {
logging.println("Time in millis: " + String(timeinMillis));
unsigned long long timeOffsetUntilTargetTime = millisInADay - (millisToNextTargetTime(timeinMillis, targetTime));
logging.println("Time offset until target time: " + String(timeOffsetUntilTargetTime));
return timeOffsetUntilTargetTime;
} else
return 0;
}

View file

@ -0,0 +1,12 @@
#ifndef __NTPTIME_H__
#define __NTPTIME_H__
extern const char* ntpServer1;
extern const char* ntpServer2;
extern const char* time_zone;
unsigned long long getNtpTimeInMillis();
unsigned long long millisToNextTargetTime(unsigned long long currentMillis, int targetTime);
unsigned long long getTimeOffsetfromNowUntil(int targetTime);
#endif

View file

@ -7,6 +7,7 @@ enum bms_status_enum { STANDBY = 0, INACTIVE = 1, DARKSTART = 2, ACTIVE = 3, FAU
enum real_bms_status_enum { BMS_DISCONNECTED = 0, BMS_STANDBY = 1, BMS_ACTIVE = 2, BMS_FAULT = 3 };
enum battery_chemistry_enum { NCA, NMC, LFP };
enum led_color { GREEN, YELLOW, RED, BLUE };
enum led_mode_enum { CLASSIC, FLOW, HEARTBEAT };
enum PrechargeState {
AUTO_PRECHARGE_IDLE,
AUTO_PRECHARGE_START,

View file

@ -20,6 +20,7 @@ static unsigned long currentMillis;
static unsigned long startupMillis = 0;
static unsigned long contactorMillis = 0;
static uint16_t rx_index = 0;
static boolean RX_allow = false;
union f32b {
@ -27,66 +28,75 @@ union f32b {
byte b[4];
};
// clang-format off
uint8_t BATTERY_INFO[40] = {
0x06, 0xE2, 0xFF, 0x02, 0xFF, 0x29, // Frame header
0x01, 0x08, 0x80, 0x43, // 256.063 Nominal voltage / 5*51.2=256 first byte 0x01 or 0x04
0xE4, 0x70, 0x8A, 0x5C, // These might be Umin & Unax, Uint16
0xB5, 0x02, 0xD3, 0x01, // Battery Serial number? Modbus register 527
0x01, 0x05, 0xC8, 0x41, // 25.0024 ?
0xC2, 0x18, // Battery Firmware, modbus register 586
0x01, 0x03, 0x59, 0x42, // 0x00005942 = 54.25 ??
0x01, 0x01, 0x01, 0x02, 0x05, 0x02, 0xA0, 0x01, 0x01, 0x02,
0x4D, // CRC
0x00}; //
// values in CyclicData will be overwritten at update_modbus_registers_inverter()
uint8_t CyclicData[64] = {
0x00, // First zero byte pointer
0xE2, 0xFF, 0x02, 0xFF, 0x29, // frame Header
0x1D, 0x5A, 0x85, 0x43, // Current Voltage (float) Modbus register 216, Bytes 6-9
0x00, 0x00, 0x8D, 0x43, // Max Voltage (2 byte float), Bytes 12-13
0x00, 0x00, 0xAC, 0x41, // BAttery Temperature (2 byte float) Modbus register 214, Bytes 16-17
0x00, 0x00, 0x00, 0x00, // Peak Current (1s period?), Bytes 18-21
0x00, 0x00, 0x00, 0x00, // Avg current (1s period?), Bytes 22-25
0x00, 0x00, 0x48, 0x42, // Max discharge current (2 byte float), Bit 26-29, // Sunspec: ADisChaMax
0x00, 0x00, 0xC8, 0x41, // Battery gross capacity, Ah (2 byte float) , Bytes 30-33, Modbus 512
0x00, 0x00, 0xA0, 0x41, // Max charge current (2 byte float) Bit 36-37, ZERO WHEN SOC=100 // Sunspec: AChaMax
0xCD, 0xCC, 0xB4, 0x41, // MaxCellTemp (4 byte float) Bit 38-41
0x00, 0x00, 0xA4, 0x41, // MinCellTemp (4 byte float) Bit 42-45
0xA4, 0x70, 0x55, 0x40, // MaxCellVolt (float), Bit 46-49
0x7D, 0x3F, 0x55, 0x40, // MinCellVolt (float), Bit 50-53
0xFE, 0x04, // Cycle count,
0x00, // Byte 56
0x40, // When SOC=100 Byte57=0x40, at startup 0x03 (about 7 times), otherwise 0x02
0x64, // SOC , Bit 58
0x00, // Unknown,
0x00, // Unknown,
0x02, // Unknown, Mostly 0x02. seen also 0x01
0x00, // CRC (inverted sum of bytes 1-62 + 0xC0), Bit 62
0x00, // First zero byte pointer
0xE2, 0xFF, 0x02, 0xFF, 0x29, // Frame header
0x00, 0x00, 0x80, 0x43, // 256.063 Nominal voltage / 5*51.2=256
0xE4, 0x70, 0x8A, 0x5C, // Manufacture date (Epoch time) (BYD: GetBatteryInfo this[0x10ac])
0xB5, 0x00, 0xD3, 0x00, // Battery Serial number? Modbus register 527 - 0x10b0
0x00, 0x00, 0xC8, 0x41, // Nominal Capacity (0x10b4)
0xC2, 0x18, // Battery Firmware, modbus register 586 (0x10b8)
0x00, // Static (BYD: GetBatteryInfo this[0x10ba])
0x00, // ?
0x59, 0x42, // Vendor identifier
// 0x59 0x42 -> 'YB' -> BYD
// 0x59 0x44 -> 'YD' -> Dyness
0x00, 0x00, // Static (BYD: GetBatteryInfo this[0x10be])
0x00, 0x00,
0x05, 0x00, // Number of blocks in series (uint16)
0xA0, 0x00, 0x00, 0x00,
0x4D, // CRC
0x00};
// clang-format on
// values in CYCLIC_DATA will be overwritten at update_modbus_registers_inverter()
// clang-format off
uint8_t CYCLIC_DATA[64] = {
0x00, // First zero byte pointer
0xE2, 0xFF, 0x02, 0xFF, 0x29, // Frame header
0x1D, 0x5A, 0x85, 0x43, // Current Voltage (float32) Bytes 6- 9 Modbus register 216
0x00, 0x00, 0x8D, 0x43, // Max Voltage (float32) Bytes 10-13
0x00, 0x00, 0xAC, 0x41, // Battery Temperature (float32) Bytes 14-17 Modbus register 214
0x00, 0x00, 0x00, 0x00, // Peak Current (1s period?) (float32) Bytes 18-21
0x00, 0x00, 0x00, 0x00, // Avg current (1s period?) (float32) Bytes 22-25
0x00, 0x00, 0x48, 0x42, // Max discharge current (float32) Bytes 26-29 Sunspec: ADisChaMax
0x00, 0x00, 0xC8, 0x41, // Battery gross capacity, Ah (float32) Bytes 30-33 Modbus register 512
0x00, 0x00, 0xA0, 0x41, // Max charge current (float32) Bytes 34-37 0.0f when SoC is 100%, Sunspec: AChaMax
0xCD, 0xCC, 0xB4, 0x41, // MaxCellTemp (float32) Bytes 38-41
0x00, 0x00, 0xA4, 0x41, // MinCellTemp (float32) Bytes 42-45
0xA4, 0x70, 0x55, 0x40, // MaxCellVolt (float32) Bytes 46-49
0x7D, 0x3F, 0x55, 0x40, // MinCellVolt (float32) Bytes 50-53
0xFE, 0x04, // Bytes 54-55, Cycle count (uint16)
0x00, // Byte 56, charge/discharge control, 0=disable, 1=enable
0x00, // Byte 57, When SoC is 100%, seen as 0x40
0x64, // Byte 58, SoC (uint8)
0x00, // Byte 59, Unknown
0x00, // Byte 60, Unknown
0x01, // Byte 61, Unknown, 1 only at first frame, 0 otherwise
0x00, // Byte 62, CRC
0x00};
// clang-format on
// FE 04 01 40 xx 01 01 02 yy (fully charged)
// FE 02 01 02 xx 01 01 02 yy (charging or discharging)
uint8_t frame3[9] = {
0x08, 0xE2, 0xFF, 0x02, 0xFF, 0x29, //header
0x06, //Unknown
uint8_t STATUS_FRAME[9] = {
0x00, 0xE2, 0xFF, 0x02, 0xFF, 0x29, //header
0x06, //Unknown (battery status/error?)
0xEF, //CRC
0x00 //endbyte
};
uint8_t frame4[8] = {0x07, 0xE3, 0xFF, 0x02, 0xFF, 0x29, 0xF4, 0x00};
uint8_t frameB1[10] = {0x07, 0x63, 0xFF, 0x02, 0xFF, 0x29, 0x5E, 0x02, 0x16, 0x00};
uint8_t frameB1b[8] = {0x07, 0xE3, 0xFF, 0x02, 0xFF, 0x29, 0xF4, 0x00};
uint8_t ACK_FRAME[8] = {0x07, 0xE3, 0xFF, 0x02, 0xFF, 0x29, 0xF4, 0x00};
uint8_t RS485_RXFRAME[300];
bool register_content_ok = false;
void float2frame(byte* arr, float value, byte framepointer) {
static void float2frame(byte* arr, float value, byte framepointer) {
f32b g;
g.f = value;
arr[framepointer] = g.b[0];
@ -117,6 +127,18 @@ static void dbg_frame(byte* frame, int len, const char* prefix) {
}
logging.println("");
#endif
#ifdef DEBUG_KOSTAL_RS485_DATA_USB
Serial.print(prefix);
Serial.print(": ");
for (uint8_t i = 0; i < len; i++) {
if (frame[i] < 0x10) {
Serial.print("0");
}
Serial.print(frame[i], HEX);
Serial.print(" ");
}
Serial.println("");
#endif
}
static void dbg_message(const char* msg) {
@ -128,7 +150,7 @@ static void dbg_message(const char* msg) {
/* https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing#Encoding_examples */
void null_stuffer(byte* lfc, int len) {
static void null_stuffer(byte* lfc, int len) {
int last_null_byte = 0;
for (int i = 0; i < len; i++) {
if (lfc[i] == '\0') {
@ -143,7 +165,7 @@ static void send_kostal(byte* frame, int len) {
Serial2.write(frame, len);
}
byte calculate_kostal_crc(byte* lfc, int len) {
static byte calculate_kostal_crc(byte* lfc, int len) {
unsigned int sum = 0;
if (lfc[0] != 0) {
logging.printf("WARNING: first byte should be 0, but is 0x%02x\n", lfc[0]);
@ -154,20 +176,20 @@ byte calculate_kostal_crc(byte* lfc, int len) {
return (byte)(-sum & 0xff);
}
byte calculate_frame1_crc(byte* lfc, int lastbyte) {
static bool check_kostal_frame_crc(int len) {
unsigned int sum = 0;
for (int i = 0; i < lastbyte; ++i) {
sum += lfc[i];
}
return ((byte) ~(sum - 0x28) & 0xff);
}
bool check_kostal_frame_crc() {
unsigned int sum = 0;
for (int i = 1; i < 8; ++i) {
int zeropointer = RS485_RXFRAME[0];
int last_zero = 0;
for (int i = 1; i < len - 2; ++i) {
if (i == zeropointer + last_zero) {
zeropointer = RS485_RXFRAME[i];
last_zero = i;
RS485_RXFRAME[i] = 0x00;
}
sum += RS485_RXFRAME[i];
}
if (((~sum + 1) & 0xff) == (RS485_RXFRAME[8] & 0xff)) {
if ((-sum & 0xff) == (RS485_RXFRAME[len - 2] & 0xff)) {
return (true);
} else {
return (false);
@ -184,11 +206,9 @@ void update_RS485_registers_inverter() {
if (datalayer.system.status.battery_allows_contactor_closing &
datalayer.system.status.inverter_allows_contactor_closing) {
float2frame(CyclicData, (float)datalayer.battery.status.voltage_dV / 10, 6); // Confirmed OK mapping
CyclicData[0] = 0x0A;
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.voltage_dV / 10, 6); // Confirmed OK mapping
} else {
CyclicData[0] = 0x06;
float2frame(CyclicData, 0.0, 6);
float2frame(CYCLIC_DATA, 0.0, 6);
}
// Set nominal voltage to value between min and max voltage set by battery (Example 400 and 300 results in 350V)
nominal_voltage_dV =
@ -196,54 +216,75 @@ void update_RS485_registers_inverter() {
datalayer.battery.info.min_design_voltage_dV);
float2frame(BATTERY_INFO, (float)nominal_voltage_dV / 10, 6);
float2frame(CyclicData, (float)datalayer.battery.info.max_design_voltage_dV / 10, 10);
float2frame(CYCLIC_DATA, (float)datalayer.battery.info.max_design_voltage_dV / 10, 10);
float2frame(CyclicData, (float)average_temperature_dC / 10, 14);
float2frame(CYCLIC_DATA, (float)average_temperature_dC / 10, 14);
// Some current values causes communication error, must be resolved, why.
// float2frame(CyclicData, (float)datalayer.battery.status.current_dA / 10, 18); // Peak discharge? current (2 byte float)
// float2frame(CyclicData, (float)datalayer.battery.status.current_dA / 10, 22);
#ifdef BMW_SBOX
float2frame(CYCLIC_DATA, (float)(datalayer.shunt.measured_amperage_mA / 100) / 10, 18);
float2frame(CYCLIC_DATA, (float)(datalayer.shunt.measured_avg1S_amperage_mA / 100) / 10, 22);
float2frame(CyclicData, (float)datalayer.battery.status.max_discharge_current_dA / 10, 26);
// When SOC = 100%, drop down allowed charge current down.
if ((datalayer.battery.status.reported_soc / 100) < 100) {
float2frame(CyclicData, (float)datalayer.battery.status.max_charge_current_dA / 10, 34);
if (datalayer.shunt.contactors_engaged) {
CYCLIC_DATA[59] = 0;
} else {
float2frame(CyclicData, 0.0, 34);
CYCLIC_DATA[59] = 2;
}
if (datalayer.shunt.precharging || datalayer.shunt.contactors_engaged) {
CYCLIC_DATA[56] = 1;
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.max_discharge_current_dA / 10,
26); // Maximum discharge current
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.max_charge_current_dA / 10, 34); // Maximum charge current
} else {
CYCLIC_DATA[56] = 0;
float2frame(CYCLIC_DATA, 0.0, 26);
float2frame(CYCLIC_DATA, 0.0, 34);
}
#else
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.current_dA / 10, 18); // Last current
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.current_dA / 10, 22); // Should be Avg current(1s)
// On startup, byte 56 seems to be always 0x00 couple of frames,.
if (f2_startup_count < 9) {
CyclicData[56] = 0x00;
CYCLIC_DATA[56] = 0x00;
} else {
CyclicData[56] = 0x01;
CYCLIC_DATA[56] = 0x01;
}
// On startup, byte 59 seems to be always 0x02 couple of frames,.
if (f2_startup_count < 14) {
CyclicData[59] = 0x02;
CYCLIC_DATA[59] = 0x02;
} else {
CyclicData[59] = 0x00;
CYCLIC_DATA[59] = 0x00;
}
#endif
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.max_discharge_current_dA / 10, 26);
// When SoC is 100%, drop down allowed charge current.
if ((datalayer.battery.status.reported_soc / 100) < 100) {
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.max_charge_current_dA / 10, 34);
} else {
float2frame(CYCLIC_DATA, 0.0, 34);
}
if (nominal_voltage_dV > 0) {
float2frame(CyclicData, (float)(datalayer.battery.info.total_capacity_Wh / nominal_voltage_dV * 10),
30); // BAttery capacity Ah
float2frame(CYCLIC_DATA, (float)(datalayer.battery.info.total_capacity_Wh / nominal_voltage_dV * 10),
30); // Battery capacity Ah
}
float2frame(CyclicData, (float)datalayer.battery.status.temperature_max_dC / 10, 38);
float2frame(CyclicData, (float)datalayer.battery.status.temperature_min_dC / 10, 42);
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.temperature_max_dC / 10, 38);
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.temperature_min_dC / 10, 42);
float2frame(CyclicData, (float)datalayer.battery.status.cell_max_voltage_mV / 1000, 46);
float2frame(CyclicData, (float)datalayer.battery.status.cell_min_voltage_mV / 1000, 50);
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.cell_max_voltage_mV / 1000, 46);
float2frame(CYCLIC_DATA, (float)datalayer.battery.status.cell_min_voltage_mV / 1000, 50);
CyclicData[58] = (byte)(datalayer.battery.status.reported_soc / 100); // Confirmed OK mapping
CYCLIC_DATA[58] = (byte)(datalayer.battery.status.reported_soc / 100);
register_content_ok = true;
BATTERY_INFO[38] = calculate_frame1_crc(BATTERY_INFO, 38);
if (incoming_message_counter > 0) {
incoming_message_counter--;
}
@ -255,8 +296,6 @@ void update_RS485_registers_inverter() {
}
}
static uint8_t rx_index = 0;
void receive_RS485() // Runs as fast as possible to handle the serial stream
{
currentMillis = millis();
@ -265,105 +304,95 @@ void receive_RS485() // Runs as fast as possible to handle the serial stream
contactorMillis = currentMillis;
}
if (currentMillis - contactorMillis >= INTERVAL_2_S & !RX_allow) {
RX_allow = true;
dbg_message("RX_allow -> true");
RX_allow = true;
}
if (startupMillis) {
if (((currentMillis - startupMillis) >= INTERVAL_2_S & currentMillis - startupMillis <= 7000) &
datalayer.system.status.inverter_allows_contactor_closing) {
// Disconnect allowed only, when curren zero
if (datalayer.battery.status.current_dA == 0) {
datalayer.system.status.inverter_allows_contactor_closing = false;
dbg_message("inverter_allows_contactor_closing -> false");
}
} else if (((currentMillis - startupMillis) >= 7000) &
datalayer.system.status.inverter_allows_contactor_closing == false) {
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing -> true");
}
}
if (B1_delay) {
if ((currentMillis - B1_last_millis) > INTERVAL_1_S) {
send_kostal(frameB1b, 8);
B1_delay = false;
dbg_message("B1_delay -> false");
}
} else if (Serial2.available()) {
if (Serial2.available()) {
RS485_RXFRAME[rx_index] = Serial2.read();
if (RX_allow) {
rx_index++;
if (RS485_RXFRAME[rx_index - 1] == 0x00) {
if ((rx_index == 10) && (RS485_RXFRAME[0] == 0x09) && register_content_ok) {
if ((rx_index > 9) && register_content_ok) {
dbg_frame(RS485_RXFRAME, 10, "RX");
rx_index = 0;
if (check_kostal_frame_crc()) {
if (check_kostal_frame_crc(rx_index)) {
incoming_message_counter = RS485_HEALTHY;
bool headerA = true;
bool headerB = true;
for (uint8_t i = 0; i < 5; i++) {
if (RS485_RXFRAME[i + 1] != KOSTAL_FRAMEHEADER[i]) {
headerA = false;
}
if (RS485_RXFRAME[i + 1] != KOSTAL_FRAMEHEADER2[i]) {
headerB = false;
}
}
// "frame B1", maybe reset request, seen after battery power on/partial data
if (headerB && (RS485_RXFRAME[6] == 0x5E) && (RS485_RXFRAME[7] == 0xFF)) {
send_kostal(frameB1, 10);
B1_delay = true;
dbg_message("B1_delay -> true");
B1_last_millis = currentMillis;
}
// "frame B1", maybe reset request, seen after battery power on/partial data
if (headerB && (RS485_RXFRAME[6] == 0x5E) && (RS485_RXFRAME[7] == 0x04)) {
send_kostal(frame4, 8);
// This needs more reverse engineering, disabled...
}
if (headerA && (RS485_RXFRAME[6] == 0x4A) && (RS485_RXFRAME[7] == 0x08)) { // "frame 1"
send_kostal(BATTERY_INFO, 40);
if (!startupMillis) {
startupMillis = currentMillis;
if (RS485_RXFRAME[1] == 'c') {
if (RS485_RXFRAME[6] == 0x47) {
// Set time function - Do nothing.
send_kostal(ACK_FRAME, 8); // ACK
}
}
if (headerA && (RS485_RXFRAME[6] == 0x4A) && (RS485_RXFRAME[7] == 0x04)) { // "frame 2"
update_values_battery();
update_RS485_registers_inverter();
if (f2_startup_count < 15) {
f2_startup_count++;
if (RS485_RXFRAME[6] == 0x5E) {
// Set State function
if (RS485_RXFRAME[7] == 0x00) {
// Allow contactor closing
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing -> true");
send_kostal(ACK_FRAME, 8); // ACK
} else if (RS485_RXFRAME[7] == 0x04) {
// INVALID STATE, no ACK sent
} else {
// Battery deep sleep?
send_kostal(ACK_FRAME, 8); // ACK
}
}
} else if (RS485_RXFRAME[1] == 'b') {
if (RS485_RXFRAME[6] == 0x50) {
//Reverse polarity, do nothing
} else {
int code = RS485_RXFRAME[6] + RS485_RXFRAME[7] * 0x100;
if (code == 0x44a) {
//Send cyclic data
update_values_battery();
update_RS485_registers_inverter();
if (f2_startup_count < 15) {
f2_startup_count++;
}
byte tmpframe[64]; //copy values to prevent data manipulation during rewrite/crc calculation
memcpy(tmpframe, CYCLIC_DATA, 64);
tmpframe[62] = calculate_kostal_crc(tmpframe, 62);
null_stuffer(tmpframe, 64);
send_kostal(tmpframe, 64);
CYCLIC_DATA[61] = 0x00;
}
if (code == 0x84a) {
//Send battery info
byte tmpframe[40]; //copy values to prevent data manipulation during rewrite/crc calculation
memcpy(tmpframe, BATTERY_INFO, 40);
tmpframe[38] = calculate_kostal_crc(tmpframe, 38);
null_stuffer(tmpframe, 40);
send_kostal(tmpframe, 40);
if (!startupMillis) {
startupMillis = currentMillis;
}
}
if (code == 0x353) {
//Send battery error/status
byte tmpframe[9]; //copy values to prevent data manipulation during rewrite/crc calculation
memcpy(tmpframe, STATUS_FRAME, 9);
tmpframe[7] = calculate_kostal_crc(tmpframe, 7);
null_stuffer(tmpframe, 9);
send_kostal(tmpframe, 9);
}
}
byte tmpframe[64]; //copy values to prevent data manipulation during rewrite/crc calculation
memcpy(tmpframe, CyclicData, 64);
tmpframe[62] = calculate_kostal_crc(tmpframe, 62);
null_stuffer(tmpframe, 64);
send_kostal(tmpframe, 64);
}
if (headerA && (RS485_RXFRAME[6] == 0x53) && (RS485_RXFRAME[7] == 0x03)) { // "frame 3"
send_kostal(frame3, 9);
}
}
} else {
dbg_frame(RS485_RXFRAME, 10, "RX (dropped)");
rx_index = 0;
}
rx_index = 0;
}
}
if (rx_index >= 10) {
dbg_frame(RS485_RXFRAME, 10, "RX (!RX_allow)");
if (rx_index >= 299) {
rx_index = 0;
}
}
}
void setup_inverter(void) { // Performs one time setup at startup
datalayer.system.status.inverter_allows_contactor_closing = false;
dbg_message("inverter_allows_contactor_closing -> false");
strncpy(datalayer.system.info.inverter_protocol, "BYD battery via Kostal RS485", 63);
datalayer.system.info.inverter_protocol[63] = '\0';
}
#endif

View file

@ -6,6 +6,7 @@
#define RS485_INVERTER_SELECTED
#define RS485_BAUDRATE 57600
//#define DEBUG_KOSTAL_RS485_DATA // Enable this line to get TX / RX printed out via logging
//#define DEBUG_KOSTAL_RS485_DATA_USB // Enable this line to get TX / RX printed out via USB
#if defined(DEBUG_KOSTAL_RS485_DATA) && !defined(DEBUG_LOG)
#error "enable LOG_TO_SD, DEBUG_VIA_USB or DEBUG_VIA_WEB in order to use DEBUG_KOSTAL_RS485_DATA"

View file

@ -37,21 +37,4 @@
*/
#define MAX_AMOUNT_CELLS 192
/** LED
*
* Parameter: LED_MODE_DEFAULT
* Description:
* The default LED mode. Available modes:
* CLASSIC - slow up/down ramp
* FLOW - slow ramp up or down depending on flow of energy
* HEARTBEAT - Heartbeat-like LED pattern that reacts to the system state with color and BPM
*
* Parameter: LED_PERIOD_MS
* Description:
* The period of whatever LED mode is active. If CLASSIC, then a ramp up and ramp down will finish in
* LED_PERIOD_MS milliseconds
*/
#define LED_MODE_DEFAULT FLOW
#define LED_PERIOD_MS 3000
#endif