Merge branch 'main' into feature/lilygo_t_2can

This commit is contained in:
Daniel Öster 2025-08-31 11:22:04 +03:00 committed by GitHub
commit e7cf83e387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 471 additions and 85 deletions

View file

@ -41,7 +41,7 @@
volatile unsigned long long bmsResetTimeOffset = 0;
// The current software version, shown on webserver
const char* version_number = "9.0.RC3";
const char* version_number = "9.0.RC3experimental";
// Interval timers
volatile unsigned long currentMillis = 0;

View file

@ -36,6 +36,7 @@
//#define DALY_BMS
//#define RJXZS_BMS
//#define RANGE_ROVER_PHEV_BATTERY
//#define RELION_BATTERY
//#define RENAULT_KANGOO_BATTERY
//#define RENAULT_TWIZY_BATTERY
//#define RENAULT_ZOE_GEN1_BATTERY

View file

@ -110,6 +110,8 @@ const char* name_for_battery_type(BatteryType type) {
return RjxzsBms::Name;
case BatteryType::RangeRoverPhev:
return RangeRoverPhevBattery::Name;
case BatteryType::RelionBattery:
return RelionBattery::Name;
case BatteryType::RenaultKangoo:
return RenaultKangooBattery::Name;
case BatteryType::RenaultTwizy:
@ -213,6 +215,8 @@ Battery* create_battery(BatteryType type) {
return new RjxzsBms();
case BatteryType::RangeRoverPhev:
return new RangeRoverPhevBattery();
case BatteryType::RelionBattery:
return new RelionBattery();
case BatteryType::RenaultKangoo:
return new RenaultKangooBattery();
case BatteryType::RenaultTwizy:
@ -317,6 +321,14 @@ void setup_battery() {
}
#endif
/* User-selected Tesla settings */
bool user_selected_tesla_digital_HVIL = false;
uint16_t user_selected_tesla_GTW_country = 17477;
bool user_selected_tesla_GTW_rightHandDrive = true;
uint16_t user_selected_tesla_GTW_mapRegion = 2;
uint16_t user_selected_tesla_GTW_chassisType = 2;
uint16_t user_selected_tesla_GTW_packEnergy = 1;
/* User-selected voltages used for custom-BMS batteries (RJXZS etc.) */
#if defined(MAX_CUSTOM_PACK_VOLTAGE_DV) && defined(MIN_CUSTOM_PACK_VOLTAGE_DV) && \
defined(MAX_CUSTOM_CELL_VOLTAGE_MV) && defined(MIN_CUSTOM_CELL_VOLTAGE_MV)

View file

@ -40,6 +40,7 @@ void setup_can_shunt();
#include "ORION-BMS.h"
#include "PYLON-BATTERY.h"
#include "RANGE-ROVER-PHEV-BATTERY.h"
#include "RELION-LV-BATTERY.h"
#include "RENAULT-KANGOO-BATTERY.h"
#include "RENAULT-TWIZY.h"
#include "RENAULT-ZOE-GEN1-BATTERY.h"
@ -61,4 +62,11 @@ extern uint16_t user_selected_min_pack_voltage_dV;
extern uint16_t user_selected_max_cell_voltage_mV;
extern uint16_t user_selected_min_cell_voltage_mV;
extern bool user_selected_tesla_digital_HVIL;
extern uint16_t user_selected_tesla_GTW_country;
extern bool user_selected_tesla_GTW_rightHandDrive;
extern uint16_t user_selected_tesla_GTW_mapRegion;
extern uint16_t user_selected_tesla_GTW_chassisType;
extern uint16_t user_selected_tesla_GTW_packEnergy;
#endif

View file

@ -46,6 +46,7 @@ enum class BatteryType {
SamsungSdiLv = 38,
HyundaiIoniq28 = 39,
Kia64FD = 40,
RelionBattery = 41,
Highest
};
@ -74,6 +75,7 @@ class Battery {
virtual bool supports_clear_isolation() { return false; }
virtual bool supports_reset_BMS() { return false; }
virtual bool supports_reset_SOC() { return false; }
virtual bool supports_reset_crash() { return false; }
virtual bool supports_reset_NVROL() { return false; }
virtual bool supports_reset_DTC() { return false; }
@ -92,6 +94,7 @@ class Battery {
virtual void clear_isolation() {}
virtual void reset_BMS() {}
virtual void reset_SOC() {}
virtual void reset_crash() {}
virtual void reset_contactor() {}
virtual void reset_NVROL() {}

View file

@ -0,0 +1,155 @@
#include "RELION-LV-BATTERY.h"
#include "../battery/BATTERIES.h"
#include "../communication/can/comm_can.h"
#include "../datalayer/datalayer.h"
#include "../devboard/utils/events.h"
/*CAN Type:CAN2.0(Extended)
BPS:250kbps
Data Length: 8
Data Encoded Format:Motorola*/
void RelionBattery::update_values() {
datalayer.battery.status.real_soc = battery_soc * 100;
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.soh_pptt = battery_soh * 100;
datalayer.battery.status.voltage_dV = battery_total_voltage;
datalayer.battery.status.current_dA = battery_total_current; //Charging negative, discharge positive
datalayer.battery.status.max_charge_power_W =
((battery_total_voltage / 10) * charge_current_A); //90A recommended charge current
datalayer.battery.status.max_discharge_power_W =
((battery_total_voltage / 10) * discharge_current_A); //150A max continous discharge current
datalayer.battery.status.temperature_min_dC = max_cell_temperature * 10;
datalayer.battery.status.temperature_max_dC = max_cell_temperature * 10;
datalayer.battery.status.cell_max_voltage_mV = max_cell_voltage;
datalayer.battery.status.cell_min_voltage_mV = min_cell_voltage;
}
void RelionBattery::handle_incoming_can_frame(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x02018100: //ID1 (Example frame 10 08 01 F0 00 00 00 00)
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
battery_total_voltage = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
break;
case 0x02028100: //ID2 (Example frame 00 00 00 63 64 10 00 00)
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
battery_total_current = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
system_state = rx_frame.data.u8[2];
battery_soc = rx_frame.data.u8[3];
battery_soh = rx_frame.data.u8[4];
most_serious_fault = rx_frame.data.u8[5];
break;
case 0x02038100: //ID3 (Example frame 0C F9 01 04 0C A7 01 0F)
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
max_cell_voltage = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
min_cell_voltage = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
break;
case 0x02648100: //Charging limitis
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
charge_current_A = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]) - 800;
regen_charge_current_A = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]) - 800;
discharge_current_A = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]) - 800;
break;
case 0x02048100: ///Temperatures min/max 2048100 [8] 47 01 01 47 01 01 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
max_cell_temperature = rx_frame.data.u8[0] - 50;
min_cell_temperature = rx_frame.data.u8[2] - 50;
break;
case 0x02468100: ///Raw temperatures 2468100 [8] 47 47 47 47 47 47 47 47
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02478100: ///? 2478100 [8] 32 32 32 32 32 32 32 32
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
//ID6 = 0x02108100 ~ 0x023F8100****** Cell Voltage 1~192******
case 0x02108100: ///Cellvoltages 1 2108100 [8] 0C F9 0C F8 0C F8 0C F9
datalayer.battery.status.cell_voltages_mV[0] = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
datalayer.battery.status.cell_voltages_mV[1] = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
datalayer.battery.status.cell_voltages_mV[2] = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
datalayer.battery.status.cell_voltages_mV[3] = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02118100: ///Cellvoltages 2 2118100 [8] 0C F8 0C F8 0C F9 0C F8
datalayer.battery.status.cell_voltages_mV[4] = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
datalayer.battery.status.cell_voltages_mV[5] = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
datalayer.battery.status.cell_voltages_mV[6] = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
datalayer.battery.status.cell_voltages_mV[7] = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02128100: ///Cellvoltages 3 2128100 [8] 0C F8 0C F8 0C F9 0C F8
datalayer.battery.status.cell_voltages_mV[8] = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
datalayer.battery.status.cell_voltages_mV[9] = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
datalayer.battery.status.cell_voltages_mV[10] = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
datalayer.battery.status.cell_voltages_mV[11] = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]);
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02138100: ///Cellvoltages 4 2138100 [8] 0C F9 0C CD 0C A7 00 00
datalayer.battery.status.cell_voltages_mV[12] = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]);
datalayer.battery.status.cell_voltages_mV[13] = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]);
datalayer.battery.status.cell_voltages_mV[14] = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]);
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02058100: ///? 2058100 [8] 00 0C 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02068100: ///? 2068100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02148100: ///? 2148100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02508100: ///? 2508100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02518100: ///? 2518100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02528100: ///? 2528100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02548100: ///? 2548100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x024A8100: ///? 24A8100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02558100: ///? 2558100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02538100: ///? 2538100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
case 0x02568100: ///? 2568100 [8] 00 00 00 00 00 00 00 00
datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE;
break;
default:
break;
}
}
void RelionBattery::transmit_can(unsigned long currentMillis) {
// No periodic sending for this protocol
}
void RelionBattery::setup(void) { // Performs one time setup at startup
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';
datalayer.battery.info.chemistry = LFP;
datalayer.battery.info.number_of_cells = 16;
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;
}

View file

@ -0,0 +1,43 @@
#ifndef RELION_BATTERY_H
#define RELION_BATTERY_H
#include "../system_settings.h"
#include "CanBattery.h"
#ifdef RELION_BATTERY
#define SELECTED_BATTERY_CLASS RelionBattery
#endif
class RelionBattery : public CanBattery {
public:
RelionBattery() : CanBattery(CAN_Speed::CAN_SPEED_250KBPS) {}
virtual void setup(void);
virtual void handle_incoming_can_frame(CAN_frame rx_frame);
virtual void update_values();
virtual void transmit_can(unsigned long currentMillis);
static constexpr const char* Name = "Relion LV protocol via 250kbps CAN";
private:
static const int MAX_PACK_VOLTAGE_DV = 584; //58.4V recommended charge voltage. BMS protection steps in at 60.8V
static const int MIN_PACK_VOLTAGE_DV = 440; //44.0V Recommended LV disconnect. BMS protection steps in at 40.0V
static const int MAX_CELL_DEVIATION_MV = 300;
static const int MAX_CELL_VOLTAGE_MV = 3800; //Battery is put into emergency stop if one cell goes over this value
static const int MIN_CELL_VOLTAGE_MV = 2700; //Battery is put into emergency stop if one cell goes below this value
uint16_t battery_total_voltage = 500;
int16_t battery_total_current = 0;
uint8_t system_state = 0;
uint8_t battery_soc = 50;
uint8_t battery_soh = 99;
uint8_t most_serious_fault = 0;
uint16_t max_cell_voltage = 3300;
uint16_t min_cell_voltage = 3300;
int16_t max_cell_temperature = 0;
int16_t min_cell_temperature = 0;
int16_t charge_current_A = 0;
int16_t regen_charge_current_A = 0;
int16_t discharge_current_A = 0;
};
#endif

View file

@ -676,39 +676,39 @@ void TeslaBattery::
clear_event(EVENT_BATTERY_FUSE);
}
#ifdef TESLA_MODEL_3Y_BATTERY
// Autodetect algoritm for chemistry on 3/Y packs.
// NCM/A batteries have 96s, LFP has 102-108s
// Drawback with this check is that it takes 3-5minutes before all cells have been counted!
if (datalayer.battery.info.number_of_cells > 101) {
datalayer.battery.info.chemistry = battery_chemistry_enum::LFP;
}
if (user_selected_tesla_GTW_chassisType > 1) { //{{0, "Model S"}, {1, "Model X"}, {2, "Model 3"}, {3, "Model Y"}};
// Autodetect algoritm for chemistry on 3/Y packs.
// NCM/A batteries have 96s, LFP has 102-108s
// Drawback with this check is that it takes 3-5minutes before all cells have been counted!
if (datalayer.battery.info.number_of_cells > 101) {
datalayer.battery.info.chemistry = battery_chemistry_enum::LFP;
}
//Once cell chemistry is determined, set maximum and minimum total pack voltage safety limits
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) {
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_LFP;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_LFP;
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_LFP;
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_LFP;
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_LFP;
} else { // NCM/A chemistry
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_NCMA;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_NCMA;
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_NCA_NCM;
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
}
//Once cell chemistry is determined, set maximum and minimum total pack voltage safety limits
if (datalayer.battery.info.chemistry == battery_chemistry_enum::LFP) {
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_LFP;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_LFP;
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_LFP;
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_LFP;
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_LFP;
} else { // NCM/A chemistry
datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_3Y_NCMA;
datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_3Y_NCMA;
datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_NCA_NCM;
datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_NCA_NCM;
datalayer.battery.info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_NCA_NCM;
}
// During forced balancing request via webserver, we allow the battery to exceed normal safety parameters
if (datalayer.battery.settings.user_requests_balancing) {
datalayer.battery.status.real_soc = 9900; //Force battery to show up as 99% when balancing
datalayer.battery.info.max_design_voltage_dV = datalayer.battery.settings.balancing_max_pack_voltage_dV;
datalayer.battery.info.max_cell_voltage_mV = datalayer.battery.settings.balancing_max_cell_voltage_mV;
datalayer.battery.info.max_cell_voltage_deviation_mV =
datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV;
datalayer.battery.status.max_charge_power_W = datalayer.battery.settings.balancing_float_power_W;
// During forced balancing request via webserver, we allow the battery to exceed normal safety parameters
if (datalayer.battery.settings.user_requests_balancing) {
datalayer.battery.status.real_soc = 9900; //Force battery to show up as 99% when balancing
datalayer.battery.info.max_design_voltage_dV = datalayer.battery.settings.balancing_max_pack_voltage_dV;
datalayer.battery.info.max_cell_voltage_mV = datalayer.battery.settings.balancing_max_cell_voltage_mV;
datalayer.battery.info.max_cell_voltage_deviation_mV =
datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV;
datalayer.battery.status.max_charge_power_W = datalayer.battery.settings.balancing_float_power_W;
}
}
#endif // TESLA_MODEL_3Y_BATTERY
// Check if user requests some action
if (datalayer.battery.settings.user_requests_tesla_isolation_clear) {
@ -727,10 +727,26 @@ void TeslaBattery::
#ifdef DEBUG_LOG
logging.println("ERROR: BMS reset failed due to contactors not being open, or BMS ECU not allowing it");
#endif //DEBUG_LOG
stateMachineBMSReset = 0;
stateMachineBMSReset = 0xFF;
datalayer.battery.settings.user_requests_tesla_bms_reset = false;
}
}
if (datalayer.battery.settings.user_requests_tesla_soc_reset) {
if (datalayer.battery.status.real_soc < 1500 || datalayer.battery.status.real_soc > 9000) {
//Start the SOC reset statemachine, only if SOC < 15% or > 90%
stateMachineSOCReset = 0;
datalayer.battery.settings.user_requests_tesla_soc_reset = false;
#ifdef DEBUG_LOG
logging.println("SOC reset requested");
#endif //DEBUG_LOG
} else {
#ifdef DEBUG_LOG
logging.println("ERROR: SOC reset failed due to SOC not being less than 15 or greater than 90");
#endif //DEBUG_LOG
stateMachineSOCReset = 0xFF;
datalayer.battery.settings.user_requests_tesla_soc_reset = false;
}
}
//Update 0x333 UI_chargeTerminationPct (bit 16, width 10) value to SOC max value - expose via UI?
//One firmware version this was seen at bit 17 width 11
@ -1999,7 +2015,7 @@ int index_118 = 0;
void TeslaBattery::transmit_can(unsigned long currentMillis) {
if (operate_contactors) { //Special S/X mode
if (user_selected_tesla_digital_HVIL) { //Special S/X? mode for 2024+ batteries
if ((datalayer.system.status.inverter_allows_contactor_closing) && (datalayer.battery.status.bms_status != FAULT)) {
if (currentMillis - lastSend1CF >= 10) {
transmit_can_frame(&can_msg_1CF[index_1CF]);
@ -2312,6 +2328,47 @@ void TeslaBattery::transmit_can(unsigned long currentMillis) {
break;
}
}
if (stateMachineSOCReset != 0xFF) {
//This implementation should be rewritten to actually reply to the UDS responses sent by the BMS
//While this may work, it is not the correct way to implement this
switch (stateMachineSOCReset) {
case 0:
TESLA_602.data = {0x02, 0x27, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00};
transmit_can_frame(&TESLA_602);
stateMachineSOCReset = 1;
break;
case 1:
TESLA_602.data = {0x30, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00};
transmit_can_frame(&TESLA_602);
stateMachineSOCReset = 2;
break;
case 2:
TESLA_602.data = {0x10, 0x12, 0x27, 0x06, 0x35, 0x34, 0x37, 0x36};
transmit_can_frame(&TESLA_602);
stateMachineSOCReset = 3;
break;
case 3:
TESLA_602.data = {0x21, 0x31, 0x30, 0x33, 0x32, 0x3D, 0x3C, 0x3F};
transmit_can_frame(&TESLA_602);
stateMachineSOCReset = 4;
break;
case 4:
TESLA_602.data = {0x22, 0x3E, 0x39, 0x38, 0x3B, 0x3A, 0x00, 0x00};
transmit_can_frame(&TESLA_602);
//Should generate a CAN UDS log message indicating ECU unlocked
stateMachineSOCReset = 5;
break;
case 5:
TESLA_602.data = {0x04, 0x31, 0x01, 0x04, 0x07, 0x00, 0x00, 0x00};
transmit_can_frame(&TESLA_602);
stateMachineSOCReset = 0xFF;
break;
default:
//Something went wrong. Reset all and cancel
stateMachineSOCReset = 0xFF;
break;
}
}
if (stateMachineBMSQuery != 0xFF) {
//This implementation should be rewritten to actually reply to the UDS responses sent by the BMS
//While this may work, it is not the correct way to implement this query logic
@ -2571,12 +2628,12 @@ void TeslaModel3YBattery::setup(void) { // Performs one time setup at startup
//0x7FF GTW CAN frame values
//Mux1
write_signal_value(&TESLA_7FF_Mux1, 16, 16, GTW_country, false);
write_signal_value(&TESLA_7FF_Mux1, 11, 1, GTW_rightHandDrive, false);
write_signal_value(&TESLA_7FF_Mux1, 16, 16, user_selected_tesla_GTW_country, false);
write_signal_value(&TESLA_7FF_Mux1, 11, 1, user_selected_tesla_GTW_country, false);
//Mux3
write_signal_value(&TESLA_7FF_Mux3, 8, 4, GTW_mapRegion, false);
write_signal_value(&TESLA_7FF_Mux3, 18, 3, GTW_chassisType, false);
write_signal_value(&TESLA_7FF_Mux3, 32, 5, GTW_packEnergy, false);
write_signal_value(&TESLA_7FF_Mux3, 8, 4, user_selected_tesla_GTW_mapRegion, false);
write_signal_value(&TESLA_7FF_Mux3, 18, 3, user_selected_tesla_GTW_chassisType, false);
write_signal_value(&TESLA_7FF_Mux3, 32, 5, user_selected_tesla_GTW_packEnergy, false);
strncpy(datalayer.system.info.battery_protocol, Name, 63);
datalayer.system.info.battery_protocol[63] = '\0';

View file

@ -11,12 +11,14 @@
#define SELECTED_BATTERY_CLASS TeslaModelSXBattery
#endif
/*NOTE! IMPORTANT INFORMATION!
Be sure to scroll down in this file and configure all "GTW_" parameters to suit your battery.
Failure to configure these will result in VCFRONT and GTW MIA errors
*/
//#define EXP_TESLA_BMS_DIGITAL_HVIL // Experimental parameter. Forces the transmission of additional CAN frames for experimental purposes, to test potential HVIL issues in 3/Y packs with newer firmware.
// 0x7FF gateway config, "Gen3" vehicles only, not applicable to Gen2 "classic" Model S and Model X
// These are user configurable from the Webserver UI
extern bool user_selected_tesla_digital_HVIL;
extern uint16_t user_selected_tesla_GTW_country;
extern bool user_selected_tesla_GTW_rightHandDrive;
extern uint16_t user_selected_tesla_GTW_mapRegion;
extern uint16_t user_selected_tesla_GTW_chassisType;
extern uint16_t user_selected_tesla_GTW_packEnergy;
class TeslaBattery : public CanBattery {
public:
@ -33,6 +35,9 @@ class TeslaBattery : public CanBattery {
bool supports_reset_BMS() { return true; }
void reset_BMS() { datalayer.battery.settings.user_requests_tesla_bms_reset = true; }
bool supports_reset_SOC() { return true; }
void reset_SOC() { datalayer.battery.settings.user_requests_tesla_soc_reset = true; }
bool supports_charged_energy() { return true; }
BatteryHtmlRenderer& get_status_renderer() { return renderer; }
@ -50,21 +55,6 @@ class TeslaBattery : public CanBattery {
// Set this to true to try to close contactors/full startup even with no inverter defined/connected
bool batteryTestOverride = false;
// 0x7FF gateway config, "Gen3" vehicles only, not applicable to Gen2 "classic" Model S and Model X
//
// ** MANUALLY SET FOR NOW **, TODO: change based on USER_SETTINGS.h or preset
//
static const uint16_t GTW_country =
18242; // "US" (USA): 21843, "CA" (Canada): 17217, "GB" (UK & N Ireland): 18242, "DK" (Denmark): 17483, "DE" (Germany): 17477, "AU" (Australia): 16725 [HVP shows errors if EU/US region mismatch for example]
// GTW_country is ISO 3166-1 Alpha-2 code, each letter converted to binary (8-bit chunks), those 8-bit chunks concatenated and then converted to decimal
static const uint8_t GTW_rightHandDrive =
1; // Left: 0, Right: 1 (not sure this matters but there for consistency in emulating the car - make sure correct for GTW_country, e.g. 0 for USA)
static const uint8_t GTW_mapRegion =
1; // "ME": 8, "NONE": 2, "CN": 3, "TW": 6, "JP": 5, "US": 0, "KR": 7, "AU": 4, "EU": 1 (not sure this matters but there for consistency)
static const uint8_t GTW_chassisType =
2; // "MODEL_3_CHASSIS": 2, "MODEL_Y_CHASSIS": 3 ("MODEL_S_CHASSIS": 0, "MODEL_X_CHASSIS": 1)
static const uint8_t GTW_packEnergy = 1; // "PACK_50_KWH": 0, "PACK_74_KWH": 1, "PACK_62_KWH": 2, "PACK_100_KWH": 3
/* Do not change anything below this line! */
static const int RAMPDOWN_SOC = 900; // 90.0 SOC% to start ramping down from max charge power towards 0 at 100.00%
static const int RAMPDOWNPOWERALLOWED = 10000; // What power we ramp down from towards top balancing
@ -486,6 +476,7 @@ class TeslaBattery : public CanBattery {
uint8_t stateMachineClearIsolationFault = 0xFF;
uint8_t stateMachineBMSReset = 0xFF;
uint8_t stateMachineSOCReset = 0xFF;
uint8_t stateMachineBMSQuery = 0xFF;
uint16_t sendContactorClosingMessagesStill = 300;
uint16_t battery_cell_max_v = 3300;
@ -922,19 +913,14 @@ class TeslaBattery : public CanBattery {
class TeslaModel3YBattery : public TeslaBattery {
public:
TeslaModel3YBattery(battery_chemistry_enum chemistry) {
datalayer.battery.info.chemistry = chemistry;
#ifdef EXP_TESLA_BMS_DIGITAL_HVIL
operate_contactors = true;
#endif
}
TeslaModel3YBattery(battery_chemistry_enum chemistry) { datalayer.battery.info.chemistry = chemistry; }
static constexpr const char* Name = "Tesla Model 3/Y";
virtual void setup(void);
};
class TeslaModelSXBattery : public TeslaBattery {
public:
TeslaModelSXBattery() { operate_contactors = true; }
TeslaModelSXBattery() {}
static constexpr const char* Name = "Tesla Model S/X";
virtual void setup(void);
};

View file

@ -106,6 +106,12 @@ void init_stored_settings() {
user_selected_inverter_battery_type = settings.getUInt("INVBTYPE", 0);
user_selected_inverter_ignore_contactors = settings.getBool("INVICNT", false);
user_selected_can_addon_crystal_frequency_mhz = settings.getUInt("CANFREQ", 8);
user_selected_tesla_digital_HVIL = settings.getBool("DIGITALHVIL", false);
user_selected_tesla_GTW_country = settings.getUInt("GTWCOUNTRY", 0);
user_selected_tesla_GTW_rightHandDrive = settings.getBool("GTWRHD", false);
user_selected_tesla_GTW_mapRegion = settings.getUInt("GTWMAPREG", 0);
user_selected_tesla_GTW_chassisType = settings.getUInt("GTWCHASSIS", 0);
user_selected_tesla_GTW_packEnergy = settings.getUInt("GTWPACK", 0);
auto readIf = [](const char* settingName) {
auto batt1If = (comm_interface)settings.getUInt(settingName, (int)comm_interface::CanNative);

View file

@ -64,6 +64,14 @@ void handle_precharge_control(unsigned long currentMillis) {
auto hia4v1_pin = esp32hal->HIA4V1_PIN();
auto inverter_disconnect_contactor_pin = esp32hal->INVERTER_DISCONNECT_CONTACTOR_PIN();
// If we're in FAILURE state, completely disable any further precharge attempts
if (datalayer.system.status.precharge_status == AUTO_PRECHARGE_FAILURE) {
pinMode(hia4v1_pin, OUTPUT);
digitalWrite(hia4v1_pin, LOW);
digitalWrite(inverter_disconnect_contactor_pin, ON);
return; // Exit immediately - no further processing allowed. Reboot required to recover
}
int32_t target_voltage = datalayer.battery.status.voltage_dV;
int32_t external_voltage = datalayer_extended.meb.BMS_voltage_intermediate_dV;
@ -128,11 +136,13 @@ void handle_precharge_control(unsigned long currentMillis) {
pinMode(hia4v1_pin, OUTPUT);
digitalWrite(hia4v1_pin, LOW);
digitalWrite(inverter_disconnect_contactor_pin, ON);
datalayer.system.status.precharge_status = AUTO_PRECHARGE_OFF;
datalayer.system.status.precharge_status = AUTO_PRECHARGE_FAILURE;
#ifdef DEBUG_LOG
logging.printf("Precharge: Disabled (timeout reached / BMS fault) -> AUTO_PRECHARGE_OFF\n");
logging.printf("Precharge: CRITICAL FAILURE (timeout/BMS fault) -> REQUIRES REBOOT\n");
#endif
set_event(EVENT_AUTOMATIC_PRECHARGE_FAILURE, 0);
// Force stop any further precharge attempts
datalayer.system.settings.start_precharging = false;
// Add event
} else if (datalayer.system.status.battery_allows_contactor_closing) {

View file

@ -152,6 +152,7 @@ struct DATALAYER_BATTERY_SETTINGS_TYPE {
bool user_requests_balancing = false;
bool user_requests_tesla_isolation_clear = false;
bool user_requests_tesla_bms_reset = false;
bool user_requests_tesla_soc_reset = false;
/* Forced balancing max time & start timestamp */
uint32_t balancing_time_ms = 3600000; //1h default, (60min*60sec*1000ms)
uint32_t balancing_start_time_ms = 0; //For keeping track when balancing started

View file

@ -263,7 +263,7 @@ String get_event_message_string(EVENTS_ENUM_TYPE event) {
case EVENT_PRECHARGE_FAILURE:
return "Battery failed to precharge. Check that capacitor is seated on high voltage output.";
case EVENT_AUTOMATIC_PRECHARGE_FAILURE:
return "Automatic precharge failed to reach target voltae.";
return "Automatic precharge FAILURE. Failed to reach target voltage or BMS timeout. Reboot emulator to retry!";
case EVENT_INTERNAL_OPEN_FAULT:
return "High voltage cable removed while battery running. Opening contactors!";
case EVENT_INVERTER_OPEN_CONTACTOR:

View file

@ -27,7 +27,8 @@ enum PrechargeState {
AUTO_PRECHARGE_START,
AUTO_PRECHARGE_PRECHARGING,
AUTO_PRECHARGE_OFF,
AUTO_PRECHARGE_COMPLETED
AUTO_PRECHARGE_COMPLETED,
AUTO_PRECHARGE_FAILURE
};
#define DISCHARGING 1

View file

@ -25,6 +25,10 @@ std::vector<BatteryCommand> battery_commands = {
[](Battery* b) {
b->reset_BMS();
}},
{"resetSOC", "SOC reset", "reset SOC?", [](Battery* b) { return b && b->supports_reset_SOC(); },
[](Battery* b) {
b->reset_SOC();
}},
{"resetCrash", "Unlock crashed BMS",
"reset crash data? Note this will unlock your BMS and enable contactor closing and SOC calculation.",
[](Battery* b) { return b && b->supports_reset_crash(); },

View file

@ -78,6 +78,33 @@ String options_for_enum(TEnum selected, Func name_for_type) {
return options;
}
template <typename TMap>
String options_from_map(int selected, const TMap& value_name_map) {
String options;
for (const auto& [value, name] : value_name_map) {
options += "<option value=\"" + String(value) + "\"";
if (selected == value) {
options += " selected";
}
options += ">";
options += name;
options += "</option>";
}
return options;
}
static const std::map<int, String> tesla_countries = {
{21843, "US (USA)"}, {17217, "CA (Canada)"}, {18242, "GB (UK & N Ireland)"},
{17483, "DK (Denmark)"}, {17477, "DE (Germany)"}, {16725, "AU (Australia)"}};
static const std::map<int, String> tesla_mapregion = {
{8, "ME (Middle East)"}, {2, "NONE"}, {3, "CN (China)"}, {6, "TW (Taiwan)"}, {5, "JP (Japan)"},
{0, "US (USA)"}, {7, "KR (Korea)"}, {4, "AU (Australia)"}, {1, "EU (Europe)"}};
static const std::map<int, String> tesla_chassis = {{0, "Model S"}, {1, "Model X"}, {2, "Model 3"}, {3, "Model Y"}};
static const std::map<int, String> tesla_pack = {{0, "50 kWh"}, {2, "62 kWh"}, {1, "74 kWh"}, {3, "100 kWh"}};
const char* name_for_button_type(STOP_BUTTON_BEHAVIOR behavior) {
switch (behavior) {
case STOP_BUTTON_BEHAVIOR::LATCHING_SWITCH:
@ -504,6 +531,30 @@ String settings_processor(const String& var, BatteryEmulatorSettingsStore& setti
return String(settings.getUInt("CANFREQ", 8));
}
if (var == "DIGITALHVIL") {
return settings.getBool("DIGITALHVIL") ? "checked" : "";
}
if (var == "GTWCOUNTRY") {
return options_from_map(settings.getUInt("GTWCOUNTRY", 0), tesla_countries);
}
if (var == "GTWRHD") {
return settings.getBool("GTWRHD") ? "checked" : "";
}
if (var == "GTWMAPREG") {
return options_from_map(settings.getUInt("GTWMAPREG", 0), tesla_mapregion);
}
if (var == "GTWCHASSIS") {
return options_from_map(settings.getUInt("GTWCHASSIS", 0), tesla_chassis);
}
if (var == "GTWPACK") {
return options_from_map(settings.getUInt("GTWPACK", 0), tesla_pack);
}
return String();
}
@ -690,6 +741,11 @@ const char* getCANInterfaceName(CAN_Interface interface) {
display: contents;
}
form .if-tesla { display: none; }
form[data-battery="32"] .if-tesla, form[data-battery="33"] .if-tesla {
display: contents;
}
form .if-dblbtr { display: none; }
form[data-dblbtr="true"] .if-dblbtr {
display: contents;
@ -739,6 +795,25 @@ const char* getCANInterfaceName(CAN_Interface interface) {
%BATTTYPE%
</select>
<div class="if-tesla">
<label>Digital HVIL (2024+): </label>
<input type='checkbox' name='DIGITALHVIL' value='on' style='margin-left: 0;' %DIGITALHVIL% />
<label>Right hand drive: </label>
<input type='checkbox' name='GTWRHD' value='on' style='margin-left: 0;' %GTWRHD% />
<label for='GTWCOUNTRY'>Country code: </label><select name='GTWCOUNTRY' id='GTWCOUNTRY'>
%GTWCOUNTRY%
</select>
<label for='GTWMAPREG'>Map region: </label><select name='GTWMAPREG' id='GTWMAPREG'>
%GTWMAPREG%
</select>
<label for='GTWCHASSIS'>Chassis type: </label><select name='GTWCHASSIS' id='GTWCHASSIS'>
%GTWCHASSIS%
</select>
<label for='GTWPACK'>Pack type: </label><select name='GTWPACK' id='GTWPACK'>
%GTWPACK%
</select>
</div>
<div class="if-battery">
<label for='BATTCOMM'>Battery comm I/F: </label><select name='BATTCOMM' id='BATTCOMM'>
%BATTCOMM%

View file

@ -413,8 +413,8 @@ void init_webserver() {
};
const char* boolSettingNames[] = {
"DBLBTR", "CNTCTRL", "CNTCTRLDBL", "PWMCNTCTRL", "PERBMSRESET", "REMBMSRESET",
"CANFDASCAN", "WIFIAPENABLED", "MQTTENABLED", "HADISC", "MQTTTOPICS", "INVICNT",
"DBLBTR", "CNTCTRL", "CNTCTRLDBL", "PWMCNTCTRL", "PERBMSRESET", "REMBMSRESET", "CANFDASCAN",
"WIFIAPENABLED", "MQTTENABLED", "HADISC", "MQTTTOPICS", "INVICNT", "GTWRHD", "DIGITALHVIL",
};
// Handles the form POST from UI to save settings of the common image
@ -518,6 +518,18 @@ void init_webserver() {
} else if (p->name() == "CANFREQ") {
auto type = atoi(p->value().c_str());
settings.saveUInt("CANFREQ", type);
} else if (p->name() == "GTWCOUNTRY") {
auto type = atoi(p->value().c_str());
settings.saveUInt("GTWCOUNTRY", type);
} else if (p->name() == "GTWMAPREG") {
auto type = atoi(p->value().c_str());
settings.saveUInt("GTWMAPREG", type);
} else if (p->name() == "GTWCHASSIS") {
auto type = atoi(p->value().c_str());
settings.saveUInt("GTWCHASSIS", type);
} else if (p->name() == "GTWPACK") {
auto type = atoi(p->value().c_str());
settings.saveUInt("GTWPACK", type);
}
for (auto& boolSetting : boolSettings) {

View file

@ -154,24 +154,18 @@ void KostalInverterProtocol::update_values() {
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)
// Close contactors after 7 battery info frames requested
if (f2_startup_count > 7) {
// Close contactors after 20 battery info frames requested
if (f2_startup_count > 20) {
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing -> true");
dbg_message("inverter_allows_contactor_closing -> true (info frame)");
}
// On startup, byte 56 seems to be always 0x00 couple of frames,.
if (f2_startup_count < 9) {
CYCLIC_DATA[56] = 0x00;
} else {
if (datalayer.system.status.inverter_allows_contactor_closing) {
CYCLIC_DATA[56] = 0x01;
}
// On startup, byte 59 seems to be always 0x02 couple of frames,.
if (f2_startup_count < 14) {
CYCLIC_DATA[59] = 0x02;
} else {
CYCLIC_DATA[59] = 0x00;
} else {
CYCLIC_DATA[56] = 0x00;
CYCLIC_DATA[59] = 0x02;
}
#endif
@ -214,6 +208,12 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
{
currentMillis = millis();
// Auto-reset contactor_test_active after 5 seconds
if (contactortestTimerActive && (millis() - contactortestTimerStart >= 5000)) {
datalayer.system.status.inverter_allows_contactor_closing = true;
dbg_message("inverter_allows_contactor_closing -> true (Contactor test ended)");
contactortestTimerActive = false;
}
if (datalayer.system.status.battery_allows_contactor_closing & !contactorMillis) {
contactorMillis = currentMillis;
}
@ -240,9 +240,17 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
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 (5E 02)");
send_kostal(ACK_FRAME, 8); // ACK
} else if (RS485_RXFRAME[7] == 0x04) {
// contactor test STATE, ACK sent
datalayer.system.status.inverter_allows_contactor_closing = false;
dbg_message("inverter_allows_contactor_closing -> false (Contactor test start)");
send_kostal(ACK_FRAME, 8); // ACK
contactortestTimerStart = currentMillis;
contactortestTimerActive = true;
} else if (RS485_RXFRAME[7] == 0xFF) {
// no ACK sent
} else {
@ -279,6 +287,8 @@ void KostalInverterProtocol::receive() // Runs as fast as possible to handle th
tmpframe[38] = calculate_kostal_crc(tmpframe, 38);
null_stuffer(tmpframe, 40);
send_kostal(tmpframe, 40);
datalayer.system.status.inverter_allows_contactor_closing = false;
dbg_message("inverter_allows_contactor_closing -> false (battery info sent)");
info_sent = true;
if (!startupMillis) {
startupMillis = currentMillis;

View file

@ -43,6 +43,8 @@ class KostalInverterProtocol : public Rs485InverterProtocol {
unsigned long currentMillis;
unsigned long startupMillis = 0;
unsigned long contactorMillis = 0;
unsigned long contactortestTimerStart = 0;
bool contactortestTimerActive = false;
uint16_t rx_index = 0;
bool RX_allow = false;