Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 180 additions & 1 deletion Software/src/battery/TESLA-BATTERY.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,10 @@ void TeslaBattery::
datalayer.battery.settings.balancing_max_deviation_cell_voltage_mV;
datalayer.battery.status.max_charge_power_W = datalayer.battery.settings.balancing_float_power_W;
}
// Auto-balance charging phase: further cap to the dedicated auto-balance charge power
if (datalayer.battery.settings.user_requests_auto_balancing && datalayer.battery.settings.user_requests_balancing) {
datalayer.battery.status.max_charge_power_W = datalayer.battery.settings.auto_balance_charge_power_W;
}
}

// Check if user requests some action
Expand Down Expand Up @@ -607,6 +611,180 @@ void TeslaBattery::
}
}

// Auto-balance state machine: automates Tesla bleed (contactors open) + charge cycles
// Phases: 0=INIT, 1=WAIT_OPEN, 2=BLEEDING, 3=ISO_CLEAR, 4=WAIT_ISO_CLEAR,
// 5=BMS_RESET, 6=WAIT_BMS_RESET, 7=WAIT_BMS_BOOT, 8=CLOSE_CONTACTORS,
// 9=WAIT_CLOSED, 10=CHARGING, 11=RETRY_WAIT_OPEN, 0xFF=IDLE
if (datalayer.battery.settings.user_requests_auto_balancing && stateMachineAutoBalance == 0xFF) {
stateMachineAutoBalance = 0;
autoBalance_cycle_count = 0;
logging.println("INFO: Auto-balance: starting automated Tesla bleed+charge cycle");
}
if (!datalayer.battery.settings.user_requests_auto_balancing && stateMachineAutoBalance != 0xFF) {
logging.println("INFO: Auto-balance: cancelled, restoring normal operation");
autoBalance_hold_contactors_open = false;
datalayer.battery.settings.user_requests_balancing = false;
stateMachineAutoBalance = 0xFF;
autoBalance_cycle_count = 0;
}
if (stateMachineAutoBalance != 0xFF) {
uint16_t cell_deviation_mV = cellvoltagesRead ? (battery_cell_max_v - battery_cell_min_v) : 9999;
uint32_t currentTime = (uint32_t)millis64();
bool contactors_closed = (battery_contactor == 4) ||
(battery_packContPositiveState == 4 && battery_packContNegativeState == 4) ||
(battery_packContPositiveState == 6 && battery_packContNegativeState == 6);
switch (stateMachineAutoBalance) {
case 0: // INIT: force contactors open to begin bleed phase
logging.println("INFO: Auto-balance: opening contactors for bleed phase");
set_event(EVENT_AUTO_BALANCE_START, 0);
autoBalance_hold_contactors_open = true;
datalayer.battery.settings.user_requests_balancing = false;
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 1;
break;
case 1: // WAIT_CONTACTORS_OPEN
if (battery_contactor == 1) { // BMS reports OPEN
logging.printf("INFO: Auto-balance: contactors open, bleed phase %d started\n", autoBalance_cycle_count);
set_event(EVENT_AUTO_BALANCE_CONTACTORS_OPEN, autoBalance_cycle_count);
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 2;
} else if (currentTime - autoBalance_phase_start_ms > 60000UL) {
logging.println("ERROR: Auto-balance: timeout waiting for contactors to open, aborting");
set_event(EVENT_AUTO_BALANCE_ERROR, 1);
datalayer.battery.settings.user_requests_auto_balancing = false;
autoBalance_hold_contactors_open = false;
stateMachineAutoBalance = 0xFF;
}
break;
case 2: // BLEEDING: Tesla passively balances cells via internal bleed resistors
// Exit once deviation drops below the done threshold - cells close enough to recharge
if (cellvoltagesRead && cell_deviation_mV <= datalayer.battery.settings.auto_balance_done_deviation_mV) {
logging.printf("INFO: Auto-balance: bleed complete, deviation %d mV <= %d mV, preparing to recharge\n",
cell_deviation_mV, datalayer.battery.settings.auto_balance_done_deviation_mV);
stateMachineAutoBalance = 3;
}
break;
case 3: // ISO_CLEAR: clear isolation faults before closing contactors
logging.println("INFO: Auto-balance: clearing isolation faults");
datalayer.battery.settings.user_requests_tesla_isolation_clear = true;
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 4;
break;
case 4: // WAIT_ISO_CLEAR
if (stateMachineClearIsolationFault == 0xFF) {
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 5;
} else if (currentTime - autoBalance_phase_start_ms > 5000UL) {
logging.println("WARN: Auto-balance: isolation clear timeout, proceeding");
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 5;
}
break;
case 5: // BMS_RESET: reset BMS to clear faults (requires contactors open)
if (battery_contactor == 1 && !BMS_a180_SW_ECU_reset_blocked) {
logging.println("INFO: Auto-balance: resetting BMS");
datalayer.battery.settings.user_requests_tesla_bms_reset = true;
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 6;
} else if (currentTime - autoBalance_phase_start_ms > 10000UL) {
logging.println("WARN: Auto-balance: BMS reset not possible, proceeding without");
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 7;
}
break;
case 6: // WAIT_BMS_RESET
if (stateMachineBMSReset == 0xFF) {
logging.println("INFO: Auto-balance: BMS reset done, waiting for BMS to boot");
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 7;
} else if (currentTime - autoBalance_phase_start_ms > 15000UL) {
logging.println("WARN: Auto-balance: BMS reset timeout, proceeding");
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 7;
}
break;
case 7: // WAIT_BMS_BOOT: give BMS time to come back up after reset (20s)
if (currentTime - autoBalance_phase_start_ms >= 20000UL) {
logging.println("INFO: Auto-balance: BMS boot wait done, closing contactors");
stateMachineAutoBalance = 8;
}
break;
case 8: // CLOSE_CONTACTORS: allow closing, enable overcharge balancing mode
autoBalance_close_retry_count = 0;
autoBalance_hold_contactors_open = false;
datalayer.battery.settings.user_requests_balancing = true;
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 9;
break;
case 9: // WAIT_CONTACTORS_CLOSED: check CLOSED or ECONOMIZED state
if (contactors_closed) {
autoBalance_cycle_count++;
logging.printf("INFO: Auto-balance: contactors closed, charging phase %d started\n", autoBalance_cycle_count);
set_event(EVENT_AUTO_BALANCE_CONTACTORS_CLOSED, autoBalance_cycle_count);
stateMachineAutoBalance = 10;
} else if (currentTime - autoBalance_phase_start_ms > 30000UL) {
autoBalance_close_retry_count++;
if (autoBalance_close_retry_count <= 5) {
logging.printf("WARN: Auto-balance: close attempt %d/5 failed, retrying isolation clear + BMS reset\n",
autoBalance_close_retry_count);
set_event(EVENT_AUTO_BALANCE_RETRY, autoBalance_close_retry_count);
autoBalance_hold_contactors_open = true;
datalayer.battery.settings.user_requests_balancing = false;
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 11; // Wait for open, then redo iso clear + BMS reset
} else {
logging.println("ERROR: Auto-balance: failed to close contactors after 5 attempts, aborting");
set_event(EVENT_AUTO_BALANCE_ERROR, 2);
datalayer.battery.settings.user_requests_auto_balancing = false;
autoBalance_hold_contactors_open = false;
datalayer.battery.settings.user_requests_balancing = false;
stateMachineAutoBalance = 0xFF;
}
}
break;
case 10: // CHARGING: charge until ceiling hit or deviation too high
if (cellvoltagesRead) {
if (battery_cell_max_v >= datalayer.battery.settings.auto_balance_max_cell_mV) {
// Any cell hit the voltage ceiling - stop the whole procedure
logging.printf("INFO: Auto-balance: max cell %d mV reached ceiling %d mV, stopping\n", battery_cell_max_v,
datalayer.battery.settings.auto_balance_max_cell_mV);
set_event(EVENT_AUTO_BALANCE_STOP, 0);
datalayer.battery.settings.user_requests_auto_balancing = false;
autoBalance_hold_contactors_open = false;
datalayer.battery.settings.user_requests_balancing = false;
stateMachineAutoBalance = 0xFF;
} else if (cell_deviation_mV > datalayer.battery.settings.auto_balance_charge_deviation_stop_mV) {
// Deviation too high during charging - open contactors and re-bleed
logging.printf("INFO: Auto-balance: deviation %d mV exceeds %d mV, re-entering bleed phase\n",
cell_deviation_mV, datalayer.battery.settings.auto_balance_charge_deviation_stop_mV);
autoBalance_hold_contactors_open = true;
datalayer.battery.settings.user_requests_balancing = false;
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 1;
}
}
break;
case 11: // RETRY_WAIT_OPEN: wait for contactors to open before retrying iso clear + BMS reset
if (battery_contactor == 1) {
autoBalance_phase_start_ms = currentTime;
stateMachineAutoBalance = 3; // Retry iso clear, then BMS reset, then close
} else if (currentTime - autoBalance_phase_start_ms > 60000UL) {
logging.println("ERROR: Auto-balance: failed to open contactors for retry, aborting");
set_event(EVENT_AUTO_BALANCE_ERROR, 1);
datalayer.battery.settings.user_requests_auto_balancing = false;
autoBalance_hold_contactors_open = false;
datalayer.battery.settings.user_requests_balancing = false;
stateMachineAutoBalance = 0xFF;
}
break;
default:
autoBalance_hold_contactors_open = false;
datalayer.battery.settings.user_requests_balancing = false;
stateMachineAutoBalance = 0xFF;
break;
}
}

//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
write_signal_value(&TESLA_333, 16, 10, static_cast<int64_t>(datalayer.battery.settings.max_percentage / 10), false);
Expand Down Expand Up @@ -840,7 +1018,8 @@ void TeslaBattery::

//Safety checks for CAN message sending
if ((datalayer.system.status.inverter_allows_contactor_closing == true) &&
(datalayer.battery.status.bms_status != FAULT) && (!datalayer.system.info.equipment_stop_active)) {
(datalayer.battery.status.bms_status != FAULT) && (!datalayer.system.info.equipment_stop_active) &&
(!autoBalance_hold_contactors_open)) { // Auto-balance: hold contactors open during bleed phase
// Carry on: 0x221 DRIVE state & reset power down timer
vehicleState = CAR_DRIVE;
powerDownSeconds = 9;
Expand Down
9 changes: 9 additions & 0 deletions Software/src/battery/TESLA-BATTERY.h
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,15 @@ class TeslaBattery : public CanBattery {
uint8_t stateMachineBMSReset = 0xFF;
uint8_t stateMachineSOCReset = 0xFF;
uint8_t stateMachineBMSQuery = 0xFF;
// Auto-balance state machine (0xFF = idle, 0-11 = active phases)
// 0=INIT, 1=WAIT_OPEN, 2=BLEEDING, 3=ISO_CLEAR, 4=WAIT_ISO_CLEAR,
// 5=BMS_RESET, 6=WAIT_BMS_RESET, 7=WAIT_BMS_BOOT, 8=CLOSE_CONTACTORS,
// 9=WAIT_CLOSED, 10=CHARGING, 11=RETRY_WAIT_OPEN
uint8_t stateMachineAutoBalance = 0xFF;
uint32_t autoBalance_phase_start_ms = 0;
uint8_t autoBalance_cycle_count = 0;
uint8_t autoBalance_close_retry_count = 0;
bool autoBalance_hold_contactors_open = false;
uint16_t battery_cell_max_v = 3300;
uint16_t battery_cell_min_v = 3300;
bool cellvoltagesRead = false;
Expand Down
6 changes: 6 additions & 0 deletions Software/src/datalayer/datalayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ struct DATALAYER_BATTERY_SETTINGS_TYPE {
bool user_requests_tesla_isolation_clear = false;
bool user_requests_tesla_bms_reset = false;
bool user_requests_tesla_soc_reset = false;
/* Tesla auto-balance: automated bleed (contactors open) + charge cycle */
bool user_requests_auto_balancing = false;
uint16_t auto_balance_max_cell_mV = 3650; // mV: NEVER charge any cell above this; stops procedure
uint16_t auto_balance_charge_power_W = 500; // W: max charge power during auto-balance (~1.4A at 350V)
uint16_t auto_balance_charge_deviation_stop_mV = 50; // mV: re-bleed if deviation exceeds this during charging
uint16_t auto_balance_done_deviation_mV = 20; // mV: stop early if deviation drops below this during charging
};

typedef struct {
Expand Down
61 changes: 59 additions & 2 deletions Software/src/devboard/mqtt/mqtt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "../../battery/BATTERIES.h"
#include "../../communication/contactorcontrol/comm_contactorcontrol.h"
#include "../../datalayer/datalayer.h"
#include "../../datalayer/datalayer_extended.h"
#include "../../devboard/hal/hal.h"
#include "../../devboard/safety/safety.h"
#include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h"
Expand Down Expand Up @@ -138,7 +139,9 @@ SensorConfig batterySensorConfigTemplate[] = {
SensorConfig globalSensorConfigTemplate[] = {{"bms_status", "BMS Status", "", "", "", always},
{"pause_status", "Pause Status", "", "", "", always},
{"event_level", "Event Level", "", "", "", always},
{"emulator_status", "Emulator Status", "", "", "", always}};
{"emulator_status", "Emulator Status", "", "", "", always},
{"negative_contactor", "Negative Contactor", "", "", "", always},
{"positive_contactor", "Positive Contactor", "", "", "", always}};

static std::list<SensorConfig> sensorConfigs;

Expand Down Expand Up @@ -169,7 +172,10 @@ SensorConfig buttonConfigs[] = {{"BMSRESET", "Reset BMS", nullptr, nullptr, null
{"PAUSE", "Pause charge/discharge", nullptr, nullptr, nullptr, nullptr},
{"RESUME", "Resume charge/discharge", nullptr, nullptr, nullptr, nullptr},
{"RESTART", "Restart Battery Emulator", nullptr, nullptr, nullptr, nullptr},
{"STOP", "Open Contactors", nullptr, nullptr, nullptr, nullptr}};
{"STOP", "Open Contactors", nullptr, nullptr, nullptr, nullptr},
{"TESLA_BMS_RESET", "Tesla BMS Reset", nullptr, nullptr, nullptr, nullptr},
{"START_LFP_BALANCING", "Start LFP balancing", nullptr, nullptr, nullptr, nullptr},
{"STOP_LFP_BALANCING", "Stop LFP balancing", nullptr, nullptr, nullptr, nullptr}};

static String generateCommonInfoAutoConfigTopic(const char* object_id) {
return "homeassistant/sensor/" + topic_name + "/" + String(object_id) + "/config";
Expand Down Expand Up @@ -229,6 +235,27 @@ static const char* get_balancing_status_text(balancing_status_enum status) {
}
}

static const char* get_tesla_contactor_state(uint8_t index) {
switch (index) {
case 1:
return "OPEN";
case 2:
return "PRECHARGE";
case 3:
return "BLOCKED";
case 4:
return "PULLED_IN";
case 5:
return "OPENING";
case 6:
return "ECONOMIZED";
case 7:
return "WELDED";
default:
return "Unknown";
}
}

void set_battery_attributes(JsonDocument& doc, const DATALAYER_BATTERY_TYPE& battery, const String& suffix,
bool supports_charged) {
doc["SOC" + suffix] = ((float)battery.status.reported_soc) / 100.0f;
Expand Down Expand Up @@ -328,6 +355,16 @@ static bool publish_common_info(void) {
doc["event_level"] = get_event_level_string(get_event_level());
doc["emulator_status"] = get_emulator_status_string(get_emulator_status());

const char* negative_contactor_state = "Unknown";
const char* positive_contactor_state = "Unknown";
if (user_selected_battery_type == BatteryType::TeslaModel3Y ||
user_selected_battery_type == BatteryType::TeslaModelSX) {
negative_contactor_state = get_tesla_contactor_state(datalayer_extended.tesla.packContNegativeState);
positive_contactor_state = get_tesla_contactor_state(datalayer_extended.tesla.packContPositiveState);
}
doc["negative_contactor"] = negative_contactor_state;
doc["positive_contactor"] = positive_contactor_state;

serializeJson(doc, mqtt_msg);
if (mqtt_publish(state_topic.c_str(), mqtt_msg, false) == false) {
logging.println("Common info MQTT msg could not be sent");
Expand Down Expand Up @@ -598,6 +635,26 @@ void mqtt_message_received(char* topic_raw, int topic_len, char* data, int data_
setBatteryPause(true, false, true);
}

if (strcmp(topic, generateButtonTopic("TESLA_BMS_RESET").c_str()) == 0) {
if (user_selected_battery_type == BatteryType::TeslaModel3Y ||
user_selected_battery_type == BatteryType::TeslaModelSX) {
logging.println("MQTT: Tesla BMS reset requested");
datalayer.battery.settings.user_requests_tesla_bms_reset = true;
} else {
logging.println("MQTT: Tesla BMS reset ignored (non-Tesla battery selected)");
}
}

if (strcmp(topic, generateButtonTopic("START_LFP_BALANCING").c_str()) == 0) {
logging.println("MQTT: Starting LFP balancing");
datalayer.battery.settings.user_requests_balancing = true;
}

if (strcmp(topic, generateButtonTopic("STOP_LFP_BALANCING").c_str()) == 0) {
logging.println("MQTT: Stopping LFP balancing");
datalayer.battery.settings.user_requests_balancing = false;
}

if (strcmp(topic, generateButtonTopic("SET_LIMITS").c_str()) == 0) {
JsonDocument doc;
char* data_str = strndup(data, data_len);
Expand Down
19 changes: 18 additions & 1 deletion Software/src/devboard/utils/events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,12 @@ void init_events(void) {
events.entries[EVENT_BATTERY_TEMP_DEVIATION_HIGH].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_GPIO_CONFLICT].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_GPIO_NOT_DEFINED].level = EVENT_LEVEL_ERROR;
events.entries[EVENT_BATTERY_TEMP_DEVIATION_HIGH].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_AUTO_BALANCE_START].level = EVENT_LEVEL_INFO;
events.entries[EVENT_AUTO_BALANCE_CONTACTORS_OPEN].level = EVENT_LEVEL_INFO;
events.entries[EVENT_AUTO_BALANCE_CONTACTORS_CLOSED].level = EVENT_LEVEL_INFO;
events.entries[EVENT_AUTO_BALANCE_STOP].level = EVENT_LEVEL_INFO;
events.entries[EVENT_AUTO_BALANCE_RETRY].level = EVENT_LEVEL_WARNING;
events.entries[EVENT_AUTO_BALANCE_ERROR].level = EVENT_LEVEL_ERROR;
}

void set_event(EVENTS_ENUM_TYPE event, uint8_t data) {
Expand Down Expand Up @@ -400,6 +405,18 @@ String get_event_message_string(EVENTS_ENUM_TYPE event) {
case EVENT_GPIO_NOT_DEFINED:
return "Missing GPIO Assignment: The component '" + esp32hal->failed_allocator() +
"' requires a GPIO pin that isn't configured. Please define a valid pin number in your settings.";
case EVENT_AUTO_BALANCE_START:
return "Auto-Balance: Activated. Opening contactors for first bleed phase.";
case EVENT_AUTO_BALANCE_CONTACTORS_OPEN:
return "Auto-Balance: Contactors OPEN - bleed phase started.";
case EVENT_AUTO_BALANCE_CONTACTORS_CLOSED:
return "Auto-Balance: Contactors CLOSED - charge phase started.";
case EVENT_AUTO_BALANCE_STOP:
return "Auto-Balance: Stopped. Max cell voltage ceiling reached, normal operation restored.";
case EVENT_AUTO_BALANCE_RETRY:
return "Auto-Balance: Contactor close attempt failed, retrying isolation clear + BMS reset. Data=attempt number.";
case EVENT_AUTO_BALANCE_ERROR:
return "Auto-Balance: ABORTED. Data: 1=contactors failed to open, 2=contactors failed to close after 5 attempts.";
default:
return "";
}
Expand Down
Loading