From 3c22bd400e68537ad803bfde8cc4e86a5875abdb Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:41:10 +0100 Subject: [PATCH 01/26] Add files via upload --- Software/src/battery/BATTERIES.cpp | 4 + Software/src/battery/BATTERIES.h | 1 + Software/src/battery/Battery.h | 1 + Software/src/battery/EMUS-BMS.cpp | 209 +++++++++++++++++++++++++++++ Software/src/battery/EMUS-BMS.h | 107 +++++++++++++++ 5 files changed, 322 insertions(+) create mode 100644 Software/src/battery/EMUS-BMS.cpp create mode 100644 Software/src/battery/EMUS-BMS.h diff --git a/Software/src/battery/BATTERIES.cpp b/Software/src/battery/BATTERIES.cpp index 210754abe..e17007b45 100644 --- a/Software/src/battery/BATTERIES.cpp +++ b/Software/src/battery/BATTERIES.cpp @@ -104,6 +104,8 @@ const char* name_for_battery_type(BatteryType type) { return PylonBattery::Name; case BatteryType::DalyBms: return DalyBms::Name; + case BatteryType::EmusBms: + return EmusBms::Name; case BatteryType::RjxzsBms: return RjxzsBms::Name; case BatteryType::RangeRoverPhev: @@ -215,6 +217,8 @@ Battery* create_battery(BatteryType type) { return new PylonBattery(); case BatteryType::DalyBms: return new DalyBms(); + case BatteryType::EmusBms: + return new EmusBms(); case BatteryType::RjxzsBms: return new RjxzsBms(); case BatteryType::RangeRoverPhev: diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index e6e28c654..7f037baa1 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -25,6 +25,7 @@ void setup_shunt(); #include "CMP-SMART-CAR-BATTERY.h" #include "DALY-BMS.h" #include "ECMP-BATTERY.h" +#include "EMUS-BMS.h" #include "FORD-MACH-E-BATTERY.h" #include "FOXESS-BATTERY.h" #include "GEELY-GEOMETRY-C-BATTERY.h" diff --git a/Software/src/battery/Battery.h b/Software/src/battery/Battery.h index c83687064..8bacf96c5 100644 --- a/Software/src/battery/Battery.h +++ b/Software/src/battery/Battery.h @@ -53,6 +53,7 @@ enum class BatteryType { CmpSmartCar = 45, MaxusEV80 = 46, ThinkCity = 47, + EmusBms = 48, Highest }; diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp new file mode 100644 index 000000000..4d22b046b --- /dev/null +++ b/Software/src/battery/EMUS-BMS.cpp @@ -0,0 +1,209 @@ +#include "EMUS-BMS.h" +#include "../battery/BATTERIES.h" +#include "../communication/can/comm_can.h" +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" + +void EmusBms::update_values() { + + 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->status.remaining_capacity_Wh = static_cast( + (static_cast(datalayer_battery->status.real_soc) / 10000) * datalayer_battery->info.total_capacity_Wh); + + // Update cell count if we've received individual cell data + if (actual_cell_count > 0) { + datalayer_battery->info.number_of_cells = actual_cell_count; + + // Calculate min/max from individual cells when available + uint16_t min_voltage = 5000; + uint16_t max_voltage = 0; + for (uint16_t i = 0; i < actual_cell_count; i++) { + uint16_t voltage = datalayer_battery->status.cell_voltages_mV[i]; + if (voltage > 0) { // Valid cell voltage + if (voltage < min_voltage) min_voltage = voltage; + if (voltage > max_voltage) max_voltage = voltage; + } + } + datalayer_battery->status.cell_max_voltage_mV = max_voltage; + datalayer_battery->status.cell_min_voltage_mV = min_voltage; + } else { + // Fall back to Pylon protocol min/max when individual cell data not available + datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; + datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; + + datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; + datalayer_battery->status.cell_voltages_mV[1] = 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; +} + +void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { + // Handle EMUS extended ID frames first + if (rx_frame.ID == 0x19B50000) { + // EMUS configuration frame containing cell count + // Byte 0-1: Unknown (00 05) + // Byte 2-3: Unknown (00 03) + // Byte 4-5: Unknown (00 01) + // Byte 6-7: Number of cells (00 77 = 0x77 = 119 decimal) + actual_cell_count = rx_frame.data.u8[7]; // Just use byte 7 (0x77 = 119) + datalayer_battery->info.number_of_cells = actual_cell_count; + return; + } + + switch (rx_frame.ID) { + case 0x7310: + case 0x7311: + ensemble_info_ack = true; + // This message contains software/hardware version info. No interest to us + break; + case 0x7320: + case 0x7321: + ensemble_info_ack = true; + battery_module_quantity = rx_frame.data.u8[0]; + battery_modules_in_series = rx_frame.data.u8[2]; + cell_quantity_in_module = rx_frame.data.u8[3]; + voltage_level = rx_frame.data.u8[4]; + ah_number = rx_frame.data.u8[6]; + break; + case 0x4210: + case 0x4211: + datalayer_battery->status.CAN_battery_still_alive = CAN_STILL_ALIVE; + voltage_dV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + current_dA = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 30000; + SOC = rx_frame.data.u8[6]; + SOH = rx_frame.data.u8[7]; + break; + case 0x4220: + case 0x4221: + charge_cutoff_voltage = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + discharge_cutoff_voltage = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); + max_charge_current = (((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]) * 0.1) - 3000; + max_discharge_current = (((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]) * 0.1) - 3000; + break; + case 0x4230: + case 0x4231: + cellvoltage_max_mV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + cellvoltage_min_mV = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); + break; + case 0x4240: + case 0x4241: + celltemperature_max_dC = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]) - 1000; + celltemperature_min_dC = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 1000; + break; + case 0x4250: + case 0x4251: + //Byte0 Basic Status + //Byte1-2 Cycle Period + //Byte3 Error + //Byte4-5 Alarm + //Byte6-7 Protection + break; + case 0x4260: + case 0x4261: + //Byte0-1 Module Max Voltage + //Byte2-3 Module Min Voltage + //Byte4-5 Module Max. Voltage Number + //Byte6-7 Module Min. Voltage Number + break; + case 0x4270: + case 0x4271: + //Byte0-1 Module Max. Temperature + //Byte2-3 Module Min. Temperature + //Byte4-5 Module Max. Temperature Number + //Byte6-7 Module Min. Temperature Number + break; + case 0x4280: + case 0x4281: + charge_forbidden = rx_frame.data.u8[0]; + discharge_forbidden = rx_frame.data.u8[1]; + break; + case 0x4290: + case 0x4291: + break; + default: + // Handle EMUS individual cell voltage messages (0x19B50100-0x19B5011F) + // Each message contains 8 cells (1 byte per cell) + if (rx_frame.ID >= CELL_VOLTAGE_BASE_ID && rx_frame.ID < (CELL_VOLTAGE_BASE_ID + 32)) { + uint8_t group = rx_frame.ID - CELL_VOLTAGE_BASE_ID; + uint8_t cell_start = group * 8; // 8 cells per message + + for (uint8_t i = 0; i < 8; i++) { + uint8_t cell_index = cell_start + i; + // Only process cells up to the actual cell count (if known) + if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { + // Cell voltage: 2000mV base + (byte value × 10mV) + // e.g., 0x7B (123) = 2000 + 123 × 10 = 3230mV + uint16_t cell_voltage = 2000 + (rx_frame.data.u8[i] * 10); + datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; + } + } + } + // Handle EMUS individual cell balancing status messages (0x19B50300-0x19B5031F) + // Each message contains 8 cells (1 byte per cell) + else if (rx_frame.ID >= CELL_BALANCING_BASE_ID && rx_frame.ID < (CELL_BALANCING_BASE_ID + 32)) { + uint8_t group = rx_frame.ID - CELL_BALANCING_BASE_ID; + uint8_t cell_start = group * 8; // 8 cells per message + + for (uint8_t i = 0; i < 8; i++) { + uint8_t cell_index = cell_start + i; + if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { + // Balancing status: non-zero value = balancing active + datalayer_battery->status.cell_balancing_status[cell_index] = (rx_frame.data.u8[i] > 0); + } + } + } + break; + } +} + +void EmusBms::transmit_can(unsigned long currentMillis) { + // Send 1s CAN Message + if (currentMillis - previousMillis1000 >= INTERVAL_1_S) { + previousMillis1000 = currentMillis; + + transmit_can_frame(&PYLON_3010); // Heartbeat + transmit_can_frame(&PYLON_4200); // Ensemble OR System equipment info, depends on frame0 + transmit_can_frame(&PYLON_8200); // Control device quit sleep status + transmit_can_frame(&PYLON_8210); // Charge command + + if (ensemble_info_ack) { + PYLON_4200.data.u8[0] = 0x00; //Request system equipment info + } + } + + // Note: EMUS BMS broadcasts cell data automatically on 0x6B0-0x6B7 (voltages) + // and 0x6B8-0x6BF (balancing status). No requests needed - just listen for the broadcasts. +} + +void EmusBms::setup(void) { // Performs one time setup at startup + strncpy(datalayer.system.info.battery_protocol, "EMUS BMS (Pylon 250k)", 63); + datalayer.system.info.battery_protocol[63] = '\0'; + datalayer_battery->info.number_of_cells = 2; // Will be updated dynamically based on received data + datalayer_battery->info.max_design_voltage_dV = user_selected_max_pack_voltage_dV; + datalayer_battery->info.min_design_voltage_dV = user_selected_min_pack_voltage_dV; + datalayer_battery->info.max_cell_voltage_mV = user_selected_max_cell_voltage_mV; + datalayer_battery->info.min_cell_voltage_mV = user_selected_min_cell_voltage_mV; + datalayer_battery->info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV; + + if (allows_contactor_closing) { + *allows_contactor_closing = true; + } +} diff --git a/Software/src/battery/EMUS-BMS.h b/Software/src/battery/EMUS-BMS.h new file mode 100644 index 000000000..ee448767b --- /dev/null +++ b/Software/src/battery/EMUS-BMS.h @@ -0,0 +1,107 @@ +#ifndef EMUS_BMS_H +#define EMUS_BMS_H + +#include "../datalayer/datalayer.h" +#include "CanBattery.h" + +class EmusBms : public CanBattery { + public: + // Use this constructor for the second battery. + EmusBms(DATALAYER_BATTERY_TYPE* datalayer_ptr, bool* contactor_closing_allowed_ptr, CAN_Interface targetCan) + : CanBattery(targetCan, CAN_Speed::CAN_SPEED_250KBPS) { + datalayer_battery = datalayer_ptr; + contactor_closing_allowed = contactor_closing_allowed_ptr; + allows_contactor_closing = nullptr; + } + + // Use the default constructor to create the first or single battery. + EmusBms() : CanBattery(CAN_Speed::CAN_SPEED_250KBPS) { + datalayer_battery = &datalayer.battery; + allows_contactor_closing = &datalayer.system.status.battery_allows_contactor_closing; + contactor_closing_allowed = nullptr; + } + + virtual ~EmusBms() = default; + + 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 = "EMUS BMS compatible battery"; + + private: + static const int MAX_CELL_DEVIATION_MV = 150; + static const int MAX_CELLS = 192; // Maximum cells supported + static const uint32_t EMUS_BASE_ID = 0x19B50000; // EMUS extended ID base + static const uint32_t CELL_VOLTAGE_BASE_ID = 0x19B50100; // Base CAN ID for cell voltages + static const uint32_t CELL_BALANCING_BASE_ID = 0x19B50300; // Base CAN ID for balancing status + + DATALAYER_BATTERY_TYPE* datalayer_battery; + + // If not null, this battery decides when the contactor can be closed and writes the value here. + bool* allows_contactor_closing; + + // If not null, this battery listens to this boolean to determine whether contactor closing is allowed + bool* contactor_closing_allowed; + + unsigned long previousMillis1000 = 0; // will store last time a 1s CAN Message was sent + unsigned long previousMillis5000 = 0; // will store last time a 5s CAN Message was sent + + //Actual content messages + CAN_frame PYLON_3010 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x3010, + .data = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_8200 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x8200, + .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_8210 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x8210, + .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_4200 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4200, + .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + // EMUS request for individual cell voltages (group 0 = all cells) + CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50200, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + // EMUS request for individual cell balancing status (group 0 = all cells) + CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50100, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + + int16_t celltemperature_max_dC; + int16_t celltemperature_min_dC; + int16_t current_dA; + uint16_t voltage_dV = 0; + uint16_t cellvoltage_max_mV = 3300; + uint16_t cellvoltage_min_mV = 3300; + uint16_t charge_cutoff_voltage = 0; + uint16_t discharge_cutoff_voltage = 0; + int16_t max_charge_current = 0; + int16_t max_discharge_current = 0; + uint8_t ensemble_info_ack = 0; + uint8_t battery_module_quantity = 0; + uint8_t battery_modules_in_series = 0; + uint8_t cell_quantity_in_module = 0; + uint8_t voltage_level = 0; + uint8_t ah_number = 0; + uint8_t SOC = 50; + uint8_t SOH = 100; + uint8_t charge_forbidden = 0; + uint8_t discharge_forbidden = 0; + uint8_t actual_cell_count = 0; // Actual number of cells detected +}; + +#endif From 73b195bbf3be87c569b19984bab4f00cb0b7d8bc Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:59:53 +0100 Subject: [PATCH 02/26] Add files via upload --- Software/src/battery/EMUS-BMS.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp index 4d22b046b..63cabe312 100644 --- a/Software/src/battery/EMUS-BMS.cpp +++ b/Software/src/battery/EMUS-BMS.cpp @@ -152,7 +152,10 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { // Cell voltage: 2000mV base + (byte value × 10mV) // e.g., 0x7B (123) = 2000 + 123 × 10 = 3230mV uint16_t cell_voltage = 2000 + (rx_frame.data.u8[i] * 10); - datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; + // Only update if valid (non-zero byte value, as 0x00 would give exactly 2000mV) + if (rx_frame.data.u8[i] > 0) { + datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; + } } } } From 1b6a7f5d5f768f6ed35dcac24fdc986f7945e8f1 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:28:32 +0100 Subject: [PATCH 03/26] Add files via upload --- Software/src/battery/EMUS-BMS.cpp | 54 +++++++++++++++++-------------- Software/src/battery/EMUS-BMS.h | 7 ++-- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp index 63cabe312..dd2b004af 100644 --- a/Software/src/battery/EMUS-BMS.cpp +++ b/Software/src/battery/EMUS-BMS.cpp @@ -24,25 +24,16 @@ void EmusBms::update_values() { // Update cell count if we've received individual cell data if (actual_cell_count > 0) { datalayer_battery->info.number_of_cells = actual_cell_count; - - // Calculate min/max from individual cells when available - uint16_t min_voltage = 5000; - uint16_t max_voltage = 0; - for (uint16_t i = 0; i < actual_cell_count; i++) { - uint16_t voltage = datalayer_battery->status.cell_voltages_mV[i]; - if (voltage > 0) { // Valid cell voltage - if (voltage < min_voltage) min_voltage = voltage; - if (voltage > max_voltage) max_voltage = voltage; - } - } - datalayer_battery->status.cell_max_voltage_mV = max_voltage; - datalayer_battery->status.cell_min_voltage_mV = min_voltage; - } else { - // Fall back to Pylon protocol min/max when individual cell data not available - datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; + } + + // Use Pylon protocol min/max for alarms (more stable than individual cell data) + // Individual cell voltages from 0x19B5 frames are still available in cell_voltages_mV[] for display + datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; + datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; + + // Also populate first two cells for systems that only check those + if (actual_cell_count == 0) { datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; - - datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; datalayer_battery->status.cell_voltages_mV[1] = cellvoltage_min_mV; } @@ -63,8 +54,12 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { // Byte 2-3: Unknown (00 03) // Byte 4-5: Unknown (00 01) // Byte 6-7: Number of cells (00 77 = 0x77 = 119 decimal) - actual_cell_count = rx_frame.data.u8[7]; // Just use byte 7 (0x77 = 119) - datalayer_battery->info.number_of_cells = actual_cell_count; + uint8_t cell_count = rx_frame.data.u8[7]; // Just use byte 7 (0x77 = 119) + // Only update if we got a valid non-zero count + if (cell_count > 0 && cell_count <= MAX_CELLS) { + actual_cell_count = cell_count; + datalayer_battery->info.number_of_cells = actual_cell_count; + } return; } @@ -152,9 +147,14 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { // Cell voltage: 2000mV base + (byte value × 10mV) // e.g., 0x7B (123) = 2000 + 123 × 10 = 3230mV uint16_t cell_voltage = 2000 + (rx_frame.data.u8[i] * 10); - // Only update if valid (non-zero byte value, as 0x00 would give exactly 2000mV) - if (rx_frame.data.u8[i] > 0) { - datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; + // Only update if voltage is in valid LiFePO4 range (2500-4200mV) + if (cell_voltage >= 2500 && cell_voltage <= 4200) { + uint16_t current_voltage = datalayer_battery->status.cell_voltages_mV[cell_index]; + // Reject sudden large changes (>1000mV) as likely data corruption + // Using 1000mV threshold since EMUS updates every 5-6 seconds + if (current_voltage == 0 || abs((int)cell_voltage - (int)current_voltage) <= 1000) { + datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; + } } } } @@ -192,8 +192,7 @@ void EmusBms::transmit_can(unsigned long currentMillis) { } } - // Note: EMUS BMS broadcasts cell data automatically on 0x6B0-0x6B7 (voltages) - // and 0x6B8-0x6BF (balancing status). No requests needed - just listen for the broadcasts. + // EMUS BMS auto-broadcasts cell data - no polling needed } void EmusBms::setup(void) { // Performs one time setup at startup @@ -206,6 +205,11 @@ void EmusBms::setup(void) { // Performs one time setup at startup datalayer_battery->info.min_cell_voltage_mV = user_selected_min_cell_voltage_mV; datalayer_battery->info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV; + // Initialize all cell voltages to a safe mid-range value to prevent false low voltage alarms + for (uint16_t i = 0; i < MAX_CELLS; i++) { + datalayer_battery->status.cell_voltages_mV[i] = 3300; // Safe default value + } + if (allows_contactor_closing) { *allows_contactor_closing = true; } diff --git a/Software/src/battery/EMUS-BMS.h b/Software/src/battery/EMUS-BMS.h index ee448767b..52379b450 100644 --- a/Software/src/battery/EMUS-BMS.h +++ b/Software/src/battery/EMUS-BMS.h @@ -72,13 +72,13 @@ class EmusBms : public CanBattery { CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, .ext_ID = true, .DLC = 0, - .ID = 0x19B50200, + .ID = 0x19B50100, .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; // EMUS request for individual cell balancing status (group 0 = all cells) CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, .ext_ID = true, .DLC = 0, - .ID = 0x19B50100, + .ID = 0x19B50300, .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; int16_t celltemperature_max_dC; @@ -102,6 +102,9 @@ class EmusBms : public CanBattery { uint8_t charge_forbidden = 0; uint8_t discharge_forbidden = 0; uint8_t actual_cell_count = 0; // Actual number of cells detected + uint8_t stable_data_cycles = 0; // Counter for stable voltage data + uint16_t last_min_voltage = 3300; // Track previous min for stability check + uint16_t last_max_voltage = 3300; // Track previous max for stability check }; #endif From 0019c8fdaec17349e254ffe05273f12275259c57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:29:49 +0000 Subject: [PATCH 04/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Software/src/battery/EMUS-BMS.cpp | 12 ++++++------ Software/src/battery/EMUS-BMS.h | 26 +++++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp index dd2b004af..ed0dc9866 100644 --- a/Software/src/battery/EMUS-BMS.cpp +++ b/Software/src/battery/EMUS-BMS.cpp @@ -25,12 +25,12 @@ void EmusBms::update_values() { if (actual_cell_count > 0) { datalayer_battery->info.number_of_cells = actual_cell_count; } - + // Use Pylon protocol min/max for alarms (more stable than individual cell data) // Individual cell voltages from 0x19B5 frames are still available in cell_voltages_mV[] for display datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; - + // Also populate first two cells for systems that only check those if (actual_cell_count == 0) { datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; @@ -62,7 +62,7 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { } return; } - + switch (rx_frame.ID) { case 0x7310: case 0x7311: @@ -139,7 +139,7 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { if (rx_frame.ID >= CELL_VOLTAGE_BASE_ID && rx_frame.ID < (CELL_VOLTAGE_BASE_ID + 32)) { uint8_t group = rx_frame.ID - CELL_VOLTAGE_BASE_ID; uint8_t cell_start = group * 8; // 8 cells per message - + for (uint8_t i = 0; i < 8; i++) { uint8_t cell_index = cell_start + i; // Only process cells up to the actual cell count (if known) @@ -164,7 +164,7 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { else if (rx_frame.ID >= CELL_BALANCING_BASE_ID && rx_frame.ID < (CELL_BALANCING_BASE_ID + 32)) { uint8_t group = rx_frame.ID - CELL_BALANCING_BASE_ID; uint8_t cell_start = group * 8; // 8 cells per message - + for (uint8_t i = 0; i < 8; i++) { uint8_t cell_index = cell_start + i; if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { @@ -191,7 +191,7 @@ void EmusBms::transmit_can(unsigned long currentMillis) { PYLON_4200.data.u8[0] = 0x00; //Request system equipment info } } - + // EMUS BMS auto-broadcasts cell data - no polling needed } diff --git a/Software/src/battery/EMUS-BMS.h b/Software/src/battery/EMUS-BMS.h index 52379b450..12b7d7446 100644 --- a/Software/src/battery/EMUS-BMS.h +++ b/Software/src/battery/EMUS-BMS.h @@ -31,9 +31,9 @@ class EmusBms : public CanBattery { private: static const int MAX_CELL_DEVIATION_MV = 150; - static const int MAX_CELLS = 192; // Maximum cells supported - static const uint32_t EMUS_BASE_ID = 0x19B50000; // EMUS extended ID base - static const uint32_t CELL_VOLTAGE_BASE_ID = 0x19B50100; // Base CAN ID for cell voltages + static const int MAX_CELLS = 192; // Maximum cells supported + static const uint32_t EMUS_BASE_ID = 0x19B50000; // EMUS extended ID base + static const uint32_t CELL_VOLTAGE_BASE_ID = 0x19B50100; // Base CAN ID for cell voltages static const uint32_t CELL_BALANCING_BASE_ID = 0x19B50300; // Base CAN ID for balancing status DATALAYER_BATTERY_TYPE* datalayer_battery; @@ -70,16 +70,16 @@ class EmusBms : public CanBattery { .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; // EMUS request for individual cell voltages (group 0 = all cells) CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50100, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50100, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; // EMUS request for individual cell balancing status (group 0 = all cells) CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50300, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50300, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; int16_t celltemperature_max_dC; int16_t celltemperature_min_dC; @@ -101,8 +101,8 @@ class EmusBms : public CanBattery { uint8_t SOH = 100; uint8_t charge_forbidden = 0; uint8_t discharge_forbidden = 0; - uint8_t actual_cell_count = 0; // Actual number of cells detected - uint8_t stable_data_cycles = 0; // Counter for stable voltage data + uint8_t actual_cell_count = 0; // Actual number of cells detected + uint8_t stable_data_cycles = 0; // Counter for stable voltage data uint16_t last_min_voltage = 3300; // Track previous min for stability check uint16_t last_max_voltage = 3300; // Track previous max for stability check }; From 9ba7d297b5f034c125d06fa887f4f86b1d448acc Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 00:03:02 +0100 Subject: [PATCH 05/26] Refactor EMUS-BMS.cpp for improved structure --- Software/src/battery/EMUS-BMS.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp index ed0dc9866..125c78ba3 100644 --- a/Software/src/battery/EMUS-BMS.cpp +++ b/Software/src/battery/EMUS-BMS.cpp @@ -4,6 +4,8 @@ #include "../datalayer/datalayer.h" #include "../devboard/utils/events.h" +EmusBms::~EmusBms() {} + void EmusBms::update_values() { datalayer_battery->status.real_soc = (SOC * 100); //increase SOC range from 0-100 -> 100.00 @@ -214,3 +216,4 @@ void EmusBms::setup(void) { // Performs one time setup at startup *allows_contactor_closing = true; } } + From e8241f317414d65944bc13b38fb0242de429b4bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:03:09 +0000 Subject: [PATCH 06/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Software/src/battery/EMUS-BMS.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp index 125c78ba3..53fa34042 100644 --- a/Software/src/battery/EMUS-BMS.cpp +++ b/Software/src/battery/EMUS-BMS.cpp @@ -216,4 +216,3 @@ void EmusBms::setup(void) { // Performs one time setup at startup *allows_contactor_closing = true; } } - From 9a1279912ccac701757d25ae3527a662228fba68 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 00:14:53 +0100 Subject: [PATCH 07/26] Add EMUS-BMS.cpp to CMakeLists.txt --- test/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 57512f946..881f3475b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -107,6 +107,7 @@ add_executable(tests ../Software/src/battery/CMP-SMART-CAR-BATTERY.cpp ../Software/src/battery/DALY-BMS.cpp ../Software/src/battery/ECMP-BATTERY.cpp + ../Software/src/battery/EMUS-BMS.cpp ../Software/src/battery/FORD-MACH-E-BATTERY.cpp ../Software/src/battery/FOXESS-BATTERY.cpp ../Software/src/battery/GEELY-GEOMETRY-C-BATTERY.cpp From 37b272e17bf1ff104cdecc397f96ceec3932a5af Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 00:25:23 +0100 Subject: [PATCH 08/26] Update EMUS-BMS.cpp --- Software/src/battery/EMUS-BMS.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp index 53fa34042..af9cea06d 100644 --- a/Software/src/battery/EMUS-BMS.cpp +++ b/Software/src/battery/EMUS-BMS.cpp @@ -4,8 +4,6 @@ #include "../datalayer/datalayer.h" #include "../devboard/utils/events.h" -EmusBms::~EmusBms() {} - void EmusBms::update_values() { datalayer_battery->status.real_soc = (SOC * 100); //increase SOC range from 0-100 -> 100.00 @@ -216,3 +214,4 @@ void EmusBms::setup(void) { // Performs one time setup at startup *allows_contactor_closing = true; } } + From 06679cca6c9b4b25722f9bb981332a8f1e21a3e6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:25:36 +0000 Subject: [PATCH 09/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Software/src/battery/EMUS-BMS.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp index af9cea06d..ed0dc9866 100644 --- a/Software/src/battery/EMUS-BMS.cpp +++ b/Software/src/battery/EMUS-BMS.cpp @@ -214,4 +214,3 @@ void EmusBms::setup(void) { // Performs one time setup at startup *allows_contactor_closing = true; } } - From 84367ec5d4eb084b796a5a50abd6069ffa2a46a3 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:45:12 +0100 Subject: [PATCH 10/26] Add files via upload --- Software/src/inverter/EMUS-BMS.cpp | 266 +++++++++++++++++++++++++++++ Software/src/inverter/EMUS-BMS.h | 122 +++++++++++++ 2 files changed, 388 insertions(+) create mode 100644 Software/src/inverter/EMUS-BMS.cpp create mode 100644 Software/src/inverter/EMUS-BMS.h diff --git a/Software/src/inverter/EMUS-BMS.cpp b/Software/src/inverter/EMUS-BMS.cpp new file mode 100644 index 000000000..24a43f81e --- /dev/null +++ b/Software/src/inverter/EMUS-BMS.cpp @@ -0,0 +1,266 @@ +#include "EMUS-BMS.h" +#include "../battery/BATTERIES.h" +#include "../communication/can/comm_can.h" +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" + +void EmusBms::update_values() { + // SOC from EMUS (0-100). real_soc is kept for diagnostics; reported_soc is what we want the inverter to use. + 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)); + + // Option A: Use EMUS estimated remaining charge (Ah) to compute remaining Wh, then derive reported SOC. + // This keeps the inverter-facing (total, remaining, SOC) internally consistent. + const uint32_t total_Wh = datalayer_battery->info.total_capacity_Wh; + const unsigned long now_ms = millis(); + const bool est_charge_fresh = est_charge_valid && ((now_ms - est_charge_last_ms) <= EST_CHARGE_TIMEOUT_MS); + + if (est_charge_fresh && (voltage_dV > 10) && (total_Wh > 0)) { + // remWh = (est_charge_0p1Ah/10) * (voltage_dV/10) = est_charge_0p1Ah * voltage_dV / 100 + uint32_t remWh = (uint32_t)((uint64_t)est_charge_0p1Ah * (uint64_t)voltage_dV / 100ULL); + if (remWh > total_Wh) { + remWh = total_Wh; // Clamp in case EMUS estimate exceeds configured total + } + datalayer_battery->status.remaining_capacity_Wh = remWh; + + uint32_t soc_x100 = (uint32_t)((uint64_t)remWh * 10000ULL / (uint64_t)total_Wh); + if (soc_x100 > 10000) soc_x100 = 10000; + datalayer_battery->status.reported_soc = soc_x100; + } else { + // Fallback: + // - If total capacity is known, derive remaining from EMUS SOC. + // - If total capacity is unknown, keep inverter SOC aligned with EMUS SOC and report 0Wh remaining. + if (total_Wh > 0) { + datalayer_battery->status.remaining_capacity_Wh = (uint32_t)( + (static_cast(datalayer_battery->status.real_soc) / 10000.0) * (double)total_Wh); + } else { + datalayer_battery->status.remaining_capacity_Wh = 0; + } + datalayer_battery->status.reported_soc = datalayer_battery->status.real_soc; + } + + // Update cell count if we've received individual cell data + if (actual_cell_count > 0) { + datalayer_battery->info.number_of_cells = actual_cell_count; + } + + // Use Pylon protocol min/max for alarms (more stable than individual cell data) + // Individual cell voltages from 0x10B5 frames are still available in cell_voltages_mV[] for display + datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; + datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; + + // Also populate first two cells for systems that only check those + if (actual_cell_count == 0) { + datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; + datalayer_battery->status.cell_voltages_mV[1] = 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; +} + +void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { + // Handle EMUS extended ID frames first + if (rx_frame.ID == 0x10B50000) { + // EMUS configuration frame containing cell count + // Byte 0-1: Unknown (00 05) + // Byte 2-3: Unknown (00 03) + // Byte 4-5: Unknown (00 01) + // Byte 6-7: Number of cells (00 77 = 0x77 = 119 decimal) + uint8_t cell_count = rx_frame.data.u8[7]; // Just use byte 7 (0x77 = 119) + // Only update if we got a valid non-zero count + if (cell_count > 0 && cell_count <= MAX_CELLS) { + actual_cell_count = cell_count; + datalayer_battery->info.number_of_cells = actual_cell_count; + } + return; + } + + if (rx_frame.ID == EMUS_SOC_PARAMS_ID) { + // EMUS Base+0x05: State of Charge parameters + // Data0-1: current (0.1A, signed) + // Data2-3: estimated remaining charge (0.1Ah) + // Data6: SOC (%) + // Data7: SOH (%) + current_dA = (int16_t)((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]); + est_charge_0p1Ah = (uint16_t)((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]); + est_charge_valid = (est_charge_0p1Ah != 0xFFFF) && (est_charge_0p1Ah > 0); + est_charge_last_ms = millis(); + SOC = rx_frame.data.u8[6]; + SOH = rx_frame.data.u8[7]; + return; + } + + if (rx_frame.ID == EMUS_ENERGY_PARAMS_ID) { + // EMUS Base+0x06: Energy parameters + // Data2-3: estimated energy left (10Wh) + est_energy_10Wh = (uint16_t)((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]); + est_energy_valid = (est_energy_10Wh != 0xFFFF) && (est_energy_10Wh > 0); + return; + } + + switch (rx_frame.ID) { + case 0x7310: + case 0x7311: + ensemble_info_ack = true; + // This message contains software/hardware version info. No interest to us + break; + case 0x7320: + case 0x7321: + ensemble_info_ack = true; + battery_module_quantity = rx_frame.data.u8[0]; + battery_modules_in_series = rx_frame.data.u8[2]; + cell_quantity_in_module = rx_frame.data.u8[3]; + voltage_level = rx_frame.data.u8[4]; + ah_number = rx_frame.data.u8[6]; + break; + case 0x4210: + case 0x4211: + datalayer_battery->status.CAN_battery_still_alive = CAN_STILL_ALIVE; + voltage_dV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + current_dA = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 30000; + SOC = rx_frame.data.u8[6]; + SOH = rx_frame.data.u8[7]; + break; + case 0x4220: + case 0x4221: + charge_cutoff_voltage = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + discharge_cutoff_voltage = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); + max_charge_current = (((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]) * 0.1) - 3000; + max_discharge_current = (((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]) * 0.1) - 3000; + break; + case 0x4230: + case 0x4231: + cellvoltage_max_mV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + cellvoltage_min_mV = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); + break; + case 0x4240: + case 0x4241: + celltemperature_max_dC = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]) - 1000; + celltemperature_min_dC = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 1000; + break; + case 0x4250: + case 0x4251: + //Byte0 Basic Status + //Byte1-2 Cycle Period + //Byte3 Error + //Byte4-5 Alarm + //Byte6-7 Protection + break; + case 0x4260: + case 0x4261: + //Byte0-1 Module Max Voltage + //Byte2-3 Module Min Voltage + //Byte4-5 Module Max. Voltage Number + //Byte6-7 Module Min. Voltage Number + break; + case 0x4270: + case 0x4271: + //Byte0-1 Module Max. Temperature + //Byte2-3 Module Min. Temperature + //Byte4-5 Module Max. Temperature Number + //Byte6-7 Module Min. Temperature Number + break; + case 0x4280: + case 0x4281: + charge_forbidden = rx_frame.data.u8[0]; + discharge_forbidden = rx_frame.data.u8[1]; + break; + case 0x4290: + case 0x4291: + break; + default: + // Handle EMUS individual cell voltage messages (0x10B50100-0x10B5011F) + // Each message contains 8 cells (1 byte per cell) + if (rx_frame.ID >= CELL_VOLTAGE_BASE_ID && rx_frame.ID < (CELL_VOLTAGE_BASE_ID + 32)) { + uint8_t group = rx_frame.ID - CELL_VOLTAGE_BASE_ID; + uint8_t cell_start = group * 8; // 8 cells per message + + for (uint8_t i = 0; i < 8; i++) { + uint8_t cell_index = cell_start + i; + // Only process cells up to the actual cell count (if known) + if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { + // Cell voltage: 2000mV base + (byte value × 10mV) + // e.g., 0x7B (123) = 2000 + 123 × 10 = 3230mV + uint16_t cell_voltage = 2000 + (rx_frame.data.u8[i] * 10); + // Only update if voltage is in valid LiFePO4 range (2500-4200mV) + if (cell_voltage >= 2500 && cell_voltage <= 4200) { + uint16_t current_voltage = datalayer_battery->status.cell_voltages_mV[cell_index]; + // Reject sudden large changes (>1000mV) as likely data corruption + // Using 1000mV threshold since EMUS updates every 5-6 seconds + if (current_voltage == 0 || abs((int)cell_voltage - (int)current_voltage) <= 1000) { + datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; + } + } + } + } + } + // Handle EMUS individual cell balancing status messages (0x10B50300-0x10B5031F) + // Each message contains 8 cells (1 byte per cell) + else if (rx_frame.ID >= CELL_BALANCING_BASE_ID && rx_frame.ID < (CELL_BALANCING_BASE_ID + 32)) { + uint8_t group = rx_frame.ID - CELL_BALANCING_BASE_ID; + uint8_t cell_start = group * 8; // 8 cells per message + + for (uint8_t i = 0; i < 8; i++) { + uint8_t cell_index = cell_start + i; + if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { + // Balancing status: non-zero value = balancing active + datalayer_battery->status.cell_balancing_status[cell_index] = (rx_frame.data.u8[i] > 0); + } + } + } + break; + } +} + +void EmusBms::transmit_can(unsigned long currentMillis) { + // Send 1s CAN Message + if (currentMillis - previousMillis1000 >= INTERVAL_1_S) { + previousMillis1000 = currentMillis; + + transmit_can_frame(&PYLON_3010); // Heartbeat + transmit_can_frame(&PYLON_4200); // Ensemble OR System equipment info, depends on frame0 + transmit_can_frame(&PYLON_8200); // Control device quit sleep status + transmit_can_frame(&PYLON_8210); // Charge command + + if (ensemble_info_ack) { + PYLON_4200.data.u8[0] = 0x00; //Request system equipment info + } + } + + // EMUS BMS auto-broadcasts cell data - no polling needed +} + +void EmusBms::setup(void) { // Performs one time setup at startup + strncpy(datalayer.system.info.battery_protocol, "EMUS BMS (Pylon 250k)", 63); + datalayer.system.info.battery_protocol[63] = '\0'; + datalayer_battery->info.number_of_cells = 2; // Will be updated dynamically based on received data + datalayer_battery->info.max_design_voltage_dV = user_selected_max_pack_voltage_dV; + datalayer_battery->info.min_design_voltage_dV = user_selected_min_pack_voltage_dV; + datalayer_battery->info.max_cell_voltage_mV = user_selected_max_cell_voltage_mV; + datalayer_battery->info.min_cell_voltage_mV = user_selected_min_cell_voltage_mV; + datalayer_battery->info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV; + + // Initialize all cell voltages to a safe mid-range value to prevent false low voltage alarms + for (uint16_t i = 0; i < MAX_CELLS; i++) { + datalayer_battery->status.cell_voltages_mV[i] = 3300; // Safe default value + } + + if (allows_contactor_closing) { + *allows_contactor_closing = true; + } +} diff --git a/Software/src/inverter/EMUS-BMS.h b/Software/src/inverter/EMUS-BMS.h new file mode 100644 index 000000000..ae6a4a4fe --- /dev/null +++ b/Software/src/inverter/EMUS-BMS.h @@ -0,0 +1,122 @@ +#ifndef EMUS_BMS_H +#define EMUS_BMS_H + +#include "../datalayer/datalayer.h" +#include "CanBattery.h" + +class EmusBms : public CanBattery { + public: + // Use this constructor for the second battery. + EmusBms(DATALAYER_BATTERY_TYPE* datalayer_ptr, bool* contactor_closing_allowed_ptr, CAN_Interface targetCan) + : CanBattery(targetCan, CAN_Speed::CAN_SPEED_250KBPS) { + datalayer_battery = datalayer_ptr; + contactor_closing_allowed = contactor_closing_allowed_ptr; + allows_contactor_closing = nullptr; + } + + // Use the default constructor to create the first or single battery. + EmusBms() : CanBattery(CAN_Speed::CAN_SPEED_250KBPS) { + datalayer_battery = &datalayer.battery; + allows_contactor_closing = &datalayer.system.status.battery_allows_contactor_closing; + contactor_closing_allowed = nullptr; + } + + virtual ~EmusBms() = default; + + 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 = "EMUS BMS compatible battery"; + + private: + static const int MAX_CELL_DEVIATION_MV = 150; + static const int MAX_CELLS = 192; // Maximum cells supported + static const uint32_t EMUS_BASE_ID = 0x10B50000; // EMUS extended ID base + static const uint32_t EMUS_SOC_PARAMS_ID = 0x10B50500; // Base + 0x05 (State of Charge parameters) + static const uint32_t EMUS_ENERGY_PARAMS_ID = 0x10B50600; // Base + 0x06 (Energy parameters) + static const uint32_t CELL_VOLTAGE_BASE_ID = 0x10B50100; // Base CAN ID for cell voltages + static const uint32_t CELL_BALANCING_BASE_ID = 0x10B50300; // Base CAN ID for balancing status + + DATALAYER_BATTERY_TYPE* datalayer_battery; + + // If not null, this battery decides when the contactor can be closed and writes the value here. + bool* allows_contactor_closing; + + // If not null, this battery listens to this boolean to determine whether contactor closing is allowed + bool* contactor_closing_allowed; + + unsigned long previousMillis1000 = 0; // will store last time a 1s CAN Message was sent + unsigned long previousMillis5000 = 0; // will store last time a 5s CAN Message was sent + + //Actual content messages + CAN_frame PYLON_3010 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x3010, + .data = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_8200 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x8200, + .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_8210 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x8210, + .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_4200 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4200, + .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + // EMUS request for individual cell voltages (group 0 = all cells) + CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50100, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + // EMUS request for individual cell balancing status (group 0 = all cells) + CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50300, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + + int16_t celltemperature_max_dC; + int16_t celltemperature_min_dC; + int16_t current_dA; + uint16_t voltage_dV = 0; + uint16_t cellvoltage_max_mV = 3300; + uint16_t cellvoltage_min_mV = 3300; + uint16_t charge_cutoff_voltage = 0; + uint16_t discharge_cutoff_voltage = 0; + int16_t max_charge_current = 0; + int16_t max_discharge_current = 0; + uint8_t ensemble_info_ack = 0; + uint8_t battery_module_quantity = 0; + uint8_t battery_modules_in_series = 0; + uint8_t cell_quantity_in_module = 0; + uint8_t voltage_level = 0; + uint8_t ah_number = 0; + uint8_t SOC = 50; + uint8_t SOH = 100; + uint8_t charge_forbidden = 0; + uint8_t discharge_forbidden = 0; + uint8_t actual_cell_count = 0; // Actual number of cells detected + uint8_t stable_data_cycles = 0; // Counter for stable voltage data + uint16_t last_min_voltage = 3300; // Track previous min for stability check + uint16_t last_max_voltage = 3300; // Track previous max for stability check + + // EMUS estimated remaining charge (0.1Ah units) from Base+0x05. + uint16_t est_charge_0p1Ah = 0; + bool est_charge_valid = false; + unsigned long est_charge_last_ms = 0; + static const unsigned long EST_CHARGE_TIMEOUT_MS = 10000; // Treat estimate as stale after 10s + + // EMUS estimated remaining energy (10Wh units) from Base+0x06 (optional diagnostics). + uint16_t est_energy_10Wh = 0; + bool est_energy_valid = false; +}; + +#endif From 71b8b6f1e11d8b1041604c891ba3afce02c60f61 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:51:02 +0000 Subject: [PATCH 11/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Software/src/inverter/EMUS-BMS.cpp | 19 ++++++++++--------- Software/src/inverter/EMUS-BMS.h | 30 +++++++++++++++--------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Software/src/inverter/EMUS-BMS.cpp b/Software/src/inverter/EMUS-BMS.cpp index 24a43f81e..69e0ad548 100644 --- a/Software/src/inverter/EMUS-BMS.cpp +++ b/Software/src/inverter/EMUS-BMS.cpp @@ -33,15 +33,16 @@ void EmusBms::update_values() { datalayer_battery->status.remaining_capacity_Wh = remWh; uint32_t soc_x100 = (uint32_t)((uint64_t)remWh * 10000ULL / (uint64_t)total_Wh); - if (soc_x100 > 10000) soc_x100 = 10000; + if (soc_x100 > 10000) + soc_x100 = 10000; datalayer_battery->status.reported_soc = soc_x100; } else { // Fallback: // - If total capacity is known, derive remaining from EMUS SOC. // - If total capacity is unknown, keep inverter SOC aligned with EMUS SOC and report 0Wh remaining. if (total_Wh > 0) { - datalayer_battery->status.remaining_capacity_Wh = (uint32_t)( - (static_cast(datalayer_battery->status.real_soc) / 10000.0) * (double)total_Wh); + datalayer_battery->status.remaining_capacity_Wh = + (uint32_t)((static_cast(datalayer_battery->status.real_soc) / 10000.0) * (double)total_Wh); } else { datalayer_battery->status.remaining_capacity_Wh = 0; } @@ -52,12 +53,12 @@ void EmusBms::update_values() { if (actual_cell_count > 0) { datalayer_battery->info.number_of_cells = actual_cell_count; } - + // Use Pylon protocol min/max for alarms (more stable than individual cell data) // Individual cell voltages from 0x10B5 frames are still available in cell_voltages_mV[] for display datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; - + // Also populate first two cells for systems that only check those if (actual_cell_count == 0) { datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; @@ -112,7 +113,7 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { est_energy_valid = (est_energy_10Wh != 0xFFFF) && (est_energy_10Wh > 0); return; } - + switch (rx_frame.ID) { case 0x7310: case 0x7311: @@ -189,7 +190,7 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { if (rx_frame.ID >= CELL_VOLTAGE_BASE_ID && rx_frame.ID < (CELL_VOLTAGE_BASE_ID + 32)) { uint8_t group = rx_frame.ID - CELL_VOLTAGE_BASE_ID; uint8_t cell_start = group * 8; // 8 cells per message - + for (uint8_t i = 0; i < 8; i++) { uint8_t cell_index = cell_start + i; // Only process cells up to the actual cell count (if known) @@ -214,7 +215,7 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { else if (rx_frame.ID >= CELL_BALANCING_BASE_ID && rx_frame.ID < (CELL_BALANCING_BASE_ID + 32)) { uint8_t group = rx_frame.ID - CELL_BALANCING_BASE_ID; uint8_t cell_start = group * 8; // 8 cells per message - + for (uint8_t i = 0; i < 8; i++) { uint8_t cell_index = cell_start + i; if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { @@ -241,7 +242,7 @@ void EmusBms::transmit_can(unsigned long currentMillis) { PYLON_4200.data.u8[0] = 0x00; //Request system equipment info } } - + // EMUS BMS auto-broadcasts cell data - no polling needed } diff --git a/Software/src/inverter/EMUS-BMS.h b/Software/src/inverter/EMUS-BMS.h index ae6a4a4fe..8afc44914 100644 --- a/Software/src/inverter/EMUS-BMS.h +++ b/Software/src/inverter/EMUS-BMS.h @@ -31,11 +31,11 @@ class EmusBms : public CanBattery { private: static const int MAX_CELL_DEVIATION_MV = 150; - static const int MAX_CELLS = 192; // Maximum cells supported - static const uint32_t EMUS_BASE_ID = 0x10B50000; // EMUS extended ID base - static const uint32_t EMUS_SOC_PARAMS_ID = 0x10B50500; // Base + 0x05 (State of Charge parameters) - static const uint32_t EMUS_ENERGY_PARAMS_ID = 0x10B50600; // Base + 0x06 (Energy parameters) - static const uint32_t CELL_VOLTAGE_BASE_ID = 0x10B50100; // Base CAN ID for cell voltages + static const int MAX_CELLS = 192; // Maximum cells supported + static const uint32_t EMUS_BASE_ID = 0x10B50000; // EMUS extended ID base + static const uint32_t EMUS_SOC_PARAMS_ID = 0x10B50500; // Base + 0x05 (State of Charge parameters) + static const uint32_t EMUS_ENERGY_PARAMS_ID = 0x10B50600; // Base + 0x06 (Energy parameters) + static const uint32_t CELL_VOLTAGE_BASE_ID = 0x10B50100; // Base CAN ID for cell voltages static const uint32_t CELL_BALANCING_BASE_ID = 0x10B50300; // Base CAN ID for balancing status DATALAYER_BATTERY_TYPE* datalayer_battery; @@ -72,16 +72,16 @@ class EmusBms : public CanBattery { .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; // EMUS request for individual cell voltages (group 0 = all cells) CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50100, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50100, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; // EMUS request for individual cell balancing status (group 0 = all cells) CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50300, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50300, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; int16_t celltemperature_max_dC; int16_t celltemperature_min_dC; @@ -103,8 +103,8 @@ class EmusBms : public CanBattery { uint8_t SOH = 100; uint8_t charge_forbidden = 0; uint8_t discharge_forbidden = 0; - uint8_t actual_cell_count = 0; // Actual number of cells detected - uint8_t stable_data_cycles = 0; // Counter for stable voltage data + uint8_t actual_cell_count = 0; // Actual number of cells detected + uint8_t stable_data_cycles = 0; // Counter for stable voltage data uint16_t last_min_voltage = 3300; // Track previous min for stability check uint16_t last_max_voltage = 3300; // Track previous max for stability check From 159054234c3923fb3fbcc6b6d7e48eca31c5d8ab Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:15:00 +0100 Subject: [PATCH 12/26] Add files via upload --- Software/src/battery/EMUS-BMS.cpp | 482 +++++++++++++++++------------- Software/src/battery/EMUS-BMS.h | 232 +++++++------- 2 files changed, 388 insertions(+), 326 deletions(-) diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp index ed0dc9866..24a43f81e 100644 --- a/Software/src/battery/EMUS-BMS.cpp +++ b/Software/src/battery/EMUS-BMS.cpp @@ -1,216 +1,266 @@ -#include "EMUS-BMS.h" -#include "../battery/BATTERIES.h" -#include "../communication/can/comm_can.h" -#include "../datalayer/datalayer.h" -#include "../devboard/utils/events.h" - -void EmusBms::update_values() { - - 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->status.remaining_capacity_Wh = static_cast( - (static_cast(datalayer_battery->status.real_soc) / 10000) * datalayer_battery->info.total_capacity_Wh); - - // Update cell count if we've received individual cell data - if (actual_cell_count > 0) { - datalayer_battery->info.number_of_cells = actual_cell_count; - } - - // Use Pylon protocol min/max for alarms (more stable than individual cell data) - // Individual cell voltages from 0x19B5 frames are still available in cell_voltages_mV[] for display - datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; - datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; - - // Also populate first two cells for systems that only check those - if (actual_cell_count == 0) { - datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; - datalayer_battery->status.cell_voltages_mV[1] = 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; -} - -void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { - // Handle EMUS extended ID frames first - if (rx_frame.ID == 0x19B50000) { - // EMUS configuration frame containing cell count - // Byte 0-1: Unknown (00 05) - // Byte 2-3: Unknown (00 03) - // Byte 4-5: Unknown (00 01) - // Byte 6-7: Number of cells (00 77 = 0x77 = 119 decimal) - uint8_t cell_count = rx_frame.data.u8[7]; // Just use byte 7 (0x77 = 119) - // Only update if we got a valid non-zero count - if (cell_count > 0 && cell_count <= MAX_CELLS) { - actual_cell_count = cell_count; - datalayer_battery->info.number_of_cells = actual_cell_count; - } - return; - } - - switch (rx_frame.ID) { - case 0x7310: - case 0x7311: - ensemble_info_ack = true; - // This message contains software/hardware version info. No interest to us - break; - case 0x7320: - case 0x7321: - ensemble_info_ack = true; - battery_module_quantity = rx_frame.data.u8[0]; - battery_modules_in_series = rx_frame.data.u8[2]; - cell_quantity_in_module = rx_frame.data.u8[3]; - voltage_level = rx_frame.data.u8[4]; - ah_number = rx_frame.data.u8[6]; - break; - case 0x4210: - case 0x4211: - datalayer_battery->status.CAN_battery_still_alive = CAN_STILL_ALIVE; - voltage_dV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); - current_dA = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 30000; - SOC = rx_frame.data.u8[6]; - SOH = rx_frame.data.u8[7]; - break; - case 0x4220: - case 0x4221: - charge_cutoff_voltage = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); - discharge_cutoff_voltage = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); - max_charge_current = (((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]) * 0.1) - 3000; - max_discharge_current = (((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]) * 0.1) - 3000; - break; - case 0x4230: - case 0x4231: - cellvoltage_max_mV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); - cellvoltage_min_mV = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); - break; - case 0x4240: - case 0x4241: - celltemperature_max_dC = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]) - 1000; - celltemperature_min_dC = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 1000; - break; - case 0x4250: - case 0x4251: - //Byte0 Basic Status - //Byte1-2 Cycle Period - //Byte3 Error - //Byte4-5 Alarm - //Byte6-7 Protection - break; - case 0x4260: - case 0x4261: - //Byte0-1 Module Max Voltage - //Byte2-3 Module Min Voltage - //Byte4-5 Module Max. Voltage Number - //Byte6-7 Module Min. Voltage Number - break; - case 0x4270: - case 0x4271: - //Byte0-1 Module Max. Temperature - //Byte2-3 Module Min. Temperature - //Byte4-5 Module Max. Temperature Number - //Byte6-7 Module Min. Temperature Number - break; - case 0x4280: - case 0x4281: - charge_forbidden = rx_frame.data.u8[0]; - discharge_forbidden = rx_frame.data.u8[1]; - break; - case 0x4290: - case 0x4291: - break; - default: - // Handle EMUS individual cell voltage messages (0x19B50100-0x19B5011F) - // Each message contains 8 cells (1 byte per cell) - if (rx_frame.ID >= CELL_VOLTAGE_BASE_ID && rx_frame.ID < (CELL_VOLTAGE_BASE_ID + 32)) { - uint8_t group = rx_frame.ID - CELL_VOLTAGE_BASE_ID; - uint8_t cell_start = group * 8; // 8 cells per message - - for (uint8_t i = 0; i < 8; i++) { - uint8_t cell_index = cell_start + i; - // Only process cells up to the actual cell count (if known) - if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { - // Cell voltage: 2000mV base + (byte value × 10mV) - // e.g., 0x7B (123) = 2000 + 123 × 10 = 3230mV - uint16_t cell_voltage = 2000 + (rx_frame.data.u8[i] * 10); - // Only update if voltage is in valid LiFePO4 range (2500-4200mV) - if (cell_voltage >= 2500 && cell_voltage <= 4200) { - uint16_t current_voltage = datalayer_battery->status.cell_voltages_mV[cell_index]; - // Reject sudden large changes (>1000mV) as likely data corruption - // Using 1000mV threshold since EMUS updates every 5-6 seconds - if (current_voltage == 0 || abs((int)cell_voltage - (int)current_voltage) <= 1000) { - datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; - } - } - } - } - } - // Handle EMUS individual cell balancing status messages (0x19B50300-0x19B5031F) - // Each message contains 8 cells (1 byte per cell) - else if (rx_frame.ID >= CELL_BALANCING_BASE_ID && rx_frame.ID < (CELL_BALANCING_BASE_ID + 32)) { - uint8_t group = rx_frame.ID - CELL_BALANCING_BASE_ID; - uint8_t cell_start = group * 8; // 8 cells per message - - for (uint8_t i = 0; i < 8; i++) { - uint8_t cell_index = cell_start + i; - if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { - // Balancing status: non-zero value = balancing active - datalayer_battery->status.cell_balancing_status[cell_index] = (rx_frame.data.u8[i] > 0); - } - } - } - break; - } -} - -void EmusBms::transmit_can(unsigned long currentMillis) { - // Send 1s CAN Message - if (currentMillis - previousMillis1000 >= INTERVAL_1_S) { - previousMillis1000 = currentMillis; - - transmit_can_frame(&PYLON_3010); // Heartbeat - transmit_can_frame(&PYLON_4200); // Ensemble OR System equipment info, depends on frame0 - transmit_can_frame(&PYLON_8200); // Control device quit sleep status - transmit_can_frame(&PYLON_8210); // Charge command - - if (ensemble_info_ack) { - PYLON_4200.data.u8[0] = 0x00; //Request system equipment info - } - } - - // EMUS BMS auto-broadcasts cell data - no polling needed -} - -void EmusBms::setup(void) { // Performs one time setup at startup - strncpy(datalayer.system.info.battery_protocol, "EMUS BMS (Pylon 250k)", 63); - datalayer.system.info.battery_protocol[63] = '\0'; - datalayer_battery->info.number_of_cells = 2; // Will be updated dynamically based on received data - datalayer_battery->info.max_design_voltage_dV = user_selected_max_pack_voltage_dV; - datalayer_battery->info.min_design_voltage_dV = user_selected_min_pack_voltage_dV; - datalayer_battery->info.max_cell_voltage_mV = user_selected_max_cell_voltage_mV; - datalayer_battery->info.min_cell_voltage_mV = user_selected_min_cell_voltage_mV; - datalayer_battery->info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV; - - // Initialize all cell voltages to a safe mid-range value to prevent false low voltage alarms - for (uint16_t i = 0; i < MAX_CELLS; i++) { - datalayer_battery->status.cell_voltages_mV[i] = 3300; // Safe default value - } - - if (allows_contactor_closing) { - *allows_contactor_closing = true; - } -} +#include "EMUS-BMS.h" +#include "../battery/BATTERIES.h" +#include "../communication/can/comm_can.h" +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" + +void EmusBms::update_values() { + // SOC from EMUS (0-100). real_soc is kept for diagnostics; reported_soc is what we want the inverter to use. + 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)); + + // Option A: Use EMUS estimated remaining charge (Ah) to compute remaining Wh, then derive reported SOC. + // This keeps the inverter-facing (total, remaining, SOC) internally consistent. + const uint32_t total_Wh = datalayer_battery->info.total_capacity_Wh; + const unsigned long now_ms = millis(); + const bool est_charge_fresh = est_charge_valid && ((now_ms - est_charge_last_ms) <= EST_CHARGE_TIMEOUT_MS); + + if (est_charge_fresh && (voltage_dV > 10) && (total_Wh > 0)) { + // remWh = (est_charge_0p1Ah/10) * (voltage_dV/10) = est_charge_0p1Ah * voltage_dV / 100 + uint32_t remWh = (uint32_t)((uint64_t)est_charge_0p1Ah * (uint64_t)voltage_dV / 100ULL); + if (remWh > total_Wh) { + remWh = total_Wh; // Clamp in case EMUS estimate exceeds configured total + } + datalayer_battery->status.remaining_capacity_Wh = remWh; + + uint32_t soc_x100 = (uint32_t)((uint64_t)remWh * 10000ULL / (uint64_t)total_Wh); + if (soc_x100 > 10000) soc_x100 = 10000; + datalayer_battery->status.reported_soc = soc_x100; + } else { + // Fallback: + // - If total capacity is known, derive remaining from EMUS SOC. + // - If total capacity is unknown, keep inverter SOC aligned with EMUS SOC and report 0Wh remaining. + if (total_Wh > 0) { + datalayer_battery->status.remaining_capacity_Wh = (uint32_t)( + (static_cast(datalayer_battery->status.real_soc) / 10000.0) * (double)total_Wh); + } else { + datalayer_battery->status.remaining_capacity_Wh = 0; + } + datalayer_battery->status.reported_soc = datalayer_battery->status.real_soc; + } + + // Update cell count if we've received individual cell data + if (actual_cell_count > 0) { + datalayer_battery->info.number_of_cells = actual_cell_count; + } + + // Use Pylon protocol min/max for alarms (more stable than individual cell data) + // Individual cell voltages from 0x10B5 frames are still available in cell_voltages_mV[] for display + datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; + datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; + + // Also populate first two cells for systems that only check those + if (actual_cell_count == 0) { + datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; + datalayer_battery->status.cell_voltages_mV[1] = 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; +} + +void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { + // Handle EMUS extended ID frames first + if (rx_frame.ID == 0x10B50000) { + // EMUS configuration frame containing cell count + // Byte 0-1: Unknown (00 05) + // Byte 2-3: Unknown (00 03) + // Byte 4-5: Unknown (00 01) + // Byte 6-7: Number of cells (00 77 = 0x77 = 119 decimal) + uint8_t cell_count = rx_frame.data.u8[7]; // Just use byte 7 (0x77 = 119) + // Only update if we got a valid non-zero count + if (cell_count > 0 && cell_count <= MAX_CELLS) { + actual_cell_count = cell_count; + datalayer_battery->info.number_of_cells = actual_cell_count; + } + return; + } + + if (rx_frame.ID == EMUS_SOC_PARAMS_ID) { + // EMUS Base+0x05: State of Charge parameters + // Data0-1: current (0.1A, signed) + // Data2-3: estimated remaining charge (0.1Ah) + // Data6: SOC (%) + // Data7: SOH (%) + current_dA = (int16_t)((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]); + est_charge_0p1Ah = (uint16_t)((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]); + est_charge_valid = (est_charge_0p1Ah != 0xFFFF) && (est_charge_0p1Ah > 0); + est_charge_last_ms = millis(); + SOC = rx_frame.data.u8[6]; + SOH = rx_frame.data.u8[7]; + return; + } + + if (rx_frame.ID == EMUS_ENERGY_PARAMS_ID) { + // EMUS Base+0x06: Energy parameters + // Data2-3: estimated energy left (10Wh) + est_energy_10Wh = (uint16_t)((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]); + est_energy_valid = (est_energy_10Wh != 0xFFFF) && (est_energy_10Wh > 0); + return; + } + + switch (rx_frame.ID) { + case 0x7310: + case 0x7311: + ensemble_info_ack = true; + // This message contains software/hardware version info. No interest to us + break; + case 0x7320: + case 0x7321: + ensemble_info_ack = true; + battery_module_quantity = rx_frame.data.u8[0]; + battery_modules_in_series = rx_frame.data.u8[2]; + cell_quantity_in_module = rx_frame.data.u8[3]; + voltage_level = rx_frame.data.u8[4]; + ah_number = rx_frame.data.u8[6]; + break; + case 0x4210: + case 0x4211: + datalayer_battery->status.CAN_battery_still_alive = CAN_STILL_ALIVE; + voltage_dV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + current_dA = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 30000; + SOC = rx_frame.data.u8[6]; + SOH = rx_frame.data.u8[7]; + break; + case 0x4220: + case 0x4221: + charge_cutoff_voltage = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + discharge_cutoff_voltage = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); + max_charge_current = (((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]) * 0.1) - 3000; + max_discharge_current = (((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]) * 0.1) - 3000; + break; + case 0x4230: + case 0x4231: + cellvoltage_max_mV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); + cellvoltage_min_mV = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); + break; + case 0x4240: + case 0x4241: + celltemperature_max_dC = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]) - 1000; + celltemperature_min_dC = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 1000; + break; + case 0x4250: + case 0x4251: + //Byte0 Basic Status + //Byte1-2 Cycle Period + //Byte3 Error + //Byte4-5 Alarm + //Byte6-7 Protection + break; + case 0x4260: + case 0x4261: + //Byte0-1 Module Max Voltage + //Byte2-3 Module Min Voltage + //Byte4-5 Module Max. Voltage Number + //Byte6-7 Module Min. Voltage Number + break; + case 0x4270: + case 0x4271: + //Byte0-1 Module Max. Temperature + //Byte2-3 Module Min. Temperature + //Byte4-5 Module Max. Temperature Number + //Byte6-7 Module Min. Temperature Number + break; + case 0x4280: + case 0x4281: + charge_forbidden = rx_frame.data.u8[0]; + discharge_forbidden = rx_frame.data.u8[1]; + break; + case 0x4290: + case 0x4291: + break; + default: + // Handle EMUS individual cell voltage messages (0x10B50100-0x10B5011F) + // Each message contains 8 cells (1 byte per cell) + if (rx_frame.ID >= CELL_VOLTAGE_BASE_ID && rx_frame.ID < (CELL_VOLTAGE_BASE_ID + 32)) { + uint8_t group = rx_frame.ID - CELL_VOLTAGE_BASE_ID; + uint8_t cell_start = group * 8; // 8 cells per message + + for (uint8_t i = 0; i < 8; i++) { + uint8_t cell_index = cell_start + i; + // Only process cells up to the actual cell count (if known) + if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { + // Cell voltage: 2000mV base + (byte value × 10mV) + // e.g., 0x7B (123) = 2000 + 123 × 10 = 3230mV + uint16_t cell_voltage = 2000 + (rx_frame.data.u8[i] * 10); + // Only update if voltage is in valid LiFePO4 range (2500-4200mV) + if (cell_voltage >= 2500 && cell_voltage <= 4200) { + uint16_t current_voltage = datalayer_battery->status.cell_voltages_mV[cell_index]; + // Reject sudden large changes (>1000mV) as likely data corruption + // Using 1000mV threshold since EMUS updates every 5-6 seconds + if (current_voltage == 0 || abs((int)cell_voltage - (int)current_voltage) <= 1000) { + datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; + } + } + } + } + } + // Handle EMUS individual cell balancing status messages (0x10B50300-0x10B5031F) + // Each message contains 8 cells (1 byte per cell) + else if (rx_frame.ID >= CELL_BALANCING_BASE_ID && rx_frame.ID < (CELL_BALANCING_BASE_ID + 32)) { + uint8_t group = rx_frame.ID - CELL_BALANCING_BASE_ID; + uint8_t cell_start = group * 8; // 8 cells per message + + for (uint8_t i = 0; i < 8; i++) { + uint8_t cell_index = cell_start + i; + if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { + // Balancing status: non-zero value = balancing active + datalayer_battery->status.cell_balancing_status[cell_index] = (rx_frame.data.u8[i] > 0); + } + } + } + break; + } +} + +void EmusBms::transmit_can(unsigned long currentMillis) { + // Send 1s CAN Message + if (currentMillis - previousMillis1000 >= INTERVAL_1_S) { + previousMillis1000 = currentMillis; + + transmit_can_frame(&PYLON_3010); // Heartbeat + transmit_can_frame(&PYLON_4200); // Ensemble OR System equipment info, depends on frame0 + transmit_can_frame(&PYLON_8200); // Control device quit sleep status + transmit_can_frame(&PYLON_8210); // Charge command + + if (ensemble_info_ack) { + PYLON_4200.data.u8[0] = 0x00; //Request system equipment info + } + } + + // EMUS BMS auto-broadcasts cell data - no polling needed +} + +void EmusBms::setup(void) { // Performs one time setup at startup + strncpy(datalayer.system.info.battery_protocol, "EMUS BMS (Pylon 250k)", 63); + datalayer.system.info.battery_protocol[63] = '\0'; + datalayer_battery->info.number_of_cells = 2; // Will be updated dynamically based on received data + datalayer_battery->info.max_design_voltage_dV = user_selected_max_pack_voltage_dV; + datalayer_battery->info.min_design_voltage_dV = user_selected_min_pack_voltage_dV; + datalayer_battery->info.max_cell_voltage_mV = user_selected_max_cell_voltage_mV; + datalayer_battery->info.min_cell_voltage_mV = user_selected_min_cell_voltage_mV; + datalayer_battery->info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV; + + // Initialize all cell voltages to a safe mid-range value to prevent false low voltage alarms + for (uint16_t i = 0; i < MAX_CELLS; i++) { + datalayer_battery->status.cell_voltages_mV[i] = 3300; // Safe default value + } + + if (allows_contactor_closing) { + *allows_contactor_closing = true; + } +} diff --git a/Software/src/battery/EMUS-BMS.h b/Software/src/battery/EMUS-BMS.h index 12b7d7446..ae6a4a4fe 100644 --- a/Software/src/battery/EMUS-BMS.h +++ b/Software/src/battery/EMUS-BMS.h @@ -1,110 +1,122 @@ -#ifndef EMUS_BMS_H -#define EMUS_BMS_H - -#include "../datalayer/datalayer.h" -#include "CanBattery.h" - -class EmusBms : public CanBattery { - public: - // Use this constructor for the second battery. - EmusBms(DATALAYER_BATTERY_TYPE* datalayer_ptr, bool* contactor_closing_allowed_ptr, CAN_Interface targetCan) - : CanBattery(targetCan, CAN_Speed::CAN_SPEED_250KBPS) { - datalayer_battery = datalayer_ptr; - contactor_closing_allowed = contactor_closing_allowed_ptr; - allows_contactor_closing = nullptr; - } - - // Use the default constructor to create the first or single battery. - EmusBms() : CanBattery(CAN_Speed::CAN_SPEED_250KBPS) { - datalayer_battery = &datalayer.battery; - allows_contactor_closing = &datalayer.system.status.battery_allows_contactor_closing; - contactor_closing_allowed = nullptr; - } - - virtual ~EmusBms() = default; - - 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 = "EMUS BMS compatible battery"; - - private: - static const int MAX_CELL_DEVIATION_MV = 150; - static const int MAX_CELLS = 192; // Maximum cells supported - static const uint32_t EMUS_BASE_ID = 0x19B50000; // EMUS extended ID base - static const uint32_t CELL_VOLTAGE_BASE_ID = 0x19B50100; // Base CAN ID for cell voltages - static const uint32_t CELL_BALANCING_BASE_ID = 0x19B50300; // Base CAN ID for balancing status - - DATALAYER_BATTERY_TYPE* datalayer_battery; - - // If not null, this battery decides when the contactor can be closed and writes the value here. - bool* allows_contactor_closing; - - // If not null, this battery listens to this boolean to determine whether contactor closing is allowed - bool* contactor_closing_allowed; - - unsigned long previousMillis1000 = 0; // will store last time a 1s CAN Message was sent - unsigned long previousMillis5000 = 0; // will store last time a 5s CAN Message was sent - - //Actual content messages - CAN_frame PYLON_3010 = {.FD = false, - .ext_ID = true, - .DLC = 8, - .ID = 0x3010, - .data = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - CAN_frame PYLON_8200 = {.FD = false, - .ext_ID = true, - .DLC = 8, - .ID = 0x8200, - .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - CAN_frame PYLON_8210 = {.FD = false, - .ext_ID = true, - .DLC = 8, - .ID = 0x8210, - .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - CAN_frame PYLON_4200 = {.FD = false, - .ext_ID = true, - .DLC = 8, - .ID = 0x4200, - .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - // EMUS request for individual cell voltages (group 0 = all cells) - CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50100, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - // EMUS request for individual cell balancing status (group 0 = all cells) - CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50300, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - - int16_t celltemperature_max_dC; - int16_t celltemperature_min_dC; - int16_t current_dA; - uint16_t voltage_dV = 0; - uint16_t cellvoltage_max_mV = 3300; - uint16_t cellvoltage_min_mV = 3300; - uint16_t charge_cutoff_voltage = 0; - uint16_t discharge_cutoff_voltage = 0; - int16_t max_charge_current = 0; - int16_t max_discharge_current = 0; - uint8_t ensemble_info_ack = 0; - uint8_t battery_module_quantity = 0; - uint8_t battery_modules_in_series = 0; - uint8_t cell_quantity_in_module = 0; - uint8_t voltage_level = 0; - uint8_t ah_number = 0; - uint8_t SOC = 50; - uint8_t SOH = 100; - uint8_t charge_forbidden = 0; - uint8_t discharge_forbidden = 0; - uint8_t actual_cell_count = 0; // Actual number of cells detected - uint8_t stable_data_cycles = 0; // Counter for stable voltage data - uint16_t last_min_voltage = 3300; // Track previous min for stability check - uint16_t last_max_voltage = 3300; // Track previous max for stability check -}; - -#endif +#ifndef EMUS_BMS_H +#define EMUS_BMS_H + +#include "../datalayer/datalayer.h" +#include "CanBattery.h" + +class EmusBms : public CanBattery { + public: + // Use this constructor for the second battery. + EmusBms(DATALAYER_BATTERY_TYPE* datalayer_ptr, bool* contactor_closing_allowed_ptr, CAN_Interface targetCan) + : CanBattery(targetCan, CAN_Speed::CAN_SPEED_250KBPS) { + datalayer_battery = datalayer_ptr; + contactor_closing_allowed = contactor_closing_allowed_ptr; + allows_contactor_closing = nullptr; + } + + // Use the default constructor to create the first or single battery. + EmusBms() : CanBattery(CAN_Speed::CAN_SPEED_250KBPS) { + datalayer_battery = &datalayer.battery; + allows_contactor_closing = &datalayer.system.status.battery_allows_contactor_closing; + contactor_closing_allowed = nullptr; + } + + virtual ~EmusBms() = default; + + 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 = "EMUS BMS compatible battery"; + + private: + static const int MAX_CELL_DEVIATION_MV = 150; + static const int MAX_CELLS = 192; // Maximum cells supported + static const uint32_t EMUS_BASE_ID = 0x10B50000; // EMUS extended ID base + static const uint32_t EMUS_SOC_PARAMS_ID = 0x10B50500; // Base + 0x05 (State of Charge parameters) + static const uint32_t EMUS_ENERGY_PARAMS_ID = 0x10B50600; // Base + 0x06 (Energy parameters) + static const uint32_t CELL_VOLTAGE_BASE_ID = 0x10B50100; // Base CAN ID for cell voltages + static const uint32_t CELL_BALANCING_BASE_ID = 0x10B50300; // Base CAN ID for balancing status + + DATALAYER_BATTERY_TYPE* datalayer_battery; + + // If not null, this battery decides when the contactor can be closed and writes the value here. + bool* allows_contactor_closing; + + // If not null, this battery listens to this boolean to determine whether contactor closing is allowed + bool* contactor_closing_allowed; + + unsigned long previousMillis1000 = 0; // will store last time a 1s CAN Message was sent + unsigned long previousMillis5000 = 0; // will store last time a 5s CAN Message was sent + + //Actual content messages + CAN_frame PYLON_3010 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x3010, + .data = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_8200 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x8200, + .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_8210 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x8210, + .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + CAN_frame PYLON_4200 = {.FD = false, + .ext_ID = true, + .DLC = 8, + .ID = 0x4200, + .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + // EMUS request for individual cell voltages (group 0 = all cells) + CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50100, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + // EMUS request for individual cell balancing status (group 0 = all cells) + CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50300, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + + int16_t celltemperature_max_dC; + int16_t celltemperature_min_dC; + int16_t current_dA; + uint16_t voltage_dV = 0; + uint16_t cellvoltage_max_mV = 3300; + uint16_t cellvoltage_min_mV = 3300; + uint16_t charge_cutoff_voltage = 0; + uint16_t discharge_cutoff_voltage = 0; + int16_t max_charge_current = 0; + int16_t max_discharge_current = 0; + uint8_t ensemble_info_ack = 0; + uint8_t battery_module_quantity = 0; + uint8_t battery_modules_in_series = 0; + uint8_t cell_quantity_in_module = 0; + uint8_t voltage_level = 0; + uint8_t ah_number = 0; + uint8_t SOC = 50; + uint8_t SOH = 100; + uint8_t charge_forbidden = 0; + uint8_t discharge_forbidden = 0; + uint8_t actual_cell_count = 0; // Actual number of cells detected + uint8_t stable_data_cycles = 0; // Counter for stable voltage data + uint16_t last_min_voltage = 3300; // Track previous min for stability check + uint16_t last_max_voltage = 3300; // Track previous max for stability check + + // EMUS estimated remaining charge (0.1Ah units) from Base+0x05. + uint16_t est_charge_0p1Ah = 0; + bool est_charge_valid = false; + unsigned long est_charge_last_ms = 0; + static const unsigned long EST_CHARGE_TIMEOUT_MS = 10000; // Treat estimate as stale after 10s + + // EMUS estimated remaining energy (10Wh units) from Base+0x06 (optional diagnostics). + uint16_t est_energy_10Wh = 0; + bool est_energy_valid = false; +}; + +#endif From 621e42d92e5f2ed68a3a5cf7c25eb8fe36f1ecd3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:15:07 +0000 Subject: [PATCH 13/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Software/src/battery/EMUS-BMS.cpp | 19 ++++++++++--------- Software/src/battery/EMUS-BMS.h | 30 +++++++++++++++--------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Software/src/battery/EMUS-BMS.cpp b/Software/src/battery/EMUS-BMS.cpp index 24a43f81e..69e0ad548 100644 --- a/Software/src/battery/EMUS-BMS.cpp +++ b/Software/src/battery/EMUS-BMS.cpp @@ -33,15 +33,16 @@ void EmusBms::update_values() { datalayer_battery->status.remaining_capacity_Wh = remWh; uint32_t soc_x100 = (uint32_t)((uint64_t)remWh * 10000ULL / (uint64_t)total_Wh); - if (soc_x100 > 10000) soc_x100 = 10000; + if (soc_x100 > 10000) + soc_x100 = 10000; datalayer_battery->status.reported_soc = soc_x100; } else { // Fallback: // - If total capacity is known, derive remaining from EMUS SOC. // - If total capacity is unknown, keep inverter SOC aligned with EMUS SOC and report 0Wh remaining. if (total_Wh > 0) { - datalayer_battery->status.remaining_capacity_Wh = (uint32_t)( - (static_cast(datalayer_battery->status.real_soc) / 10000.0) * (double)total_Wh); + datalayer_battery->status.remaining_capacity_Wh = + (uint32_t)((static_cast(datalayer_battery->status.real_soc) / 10000.0) * (double)total_Wh); } else { datalayer_battery->status.remaining_capacity_Wh = 0; } @@ -52,12 +53,12 @@ void EmusBms::update_values() { if (actual_cell_count > 0) { datalayer_battery->info.number_of_cells = actual_cell_count; } - + // Use Pylon protocol min/max for alarms (more stable than individual cell data) // Individual cell voltages from 0x10B5 frames are still available in cell_voltages_mV[] for display datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; - + // Also populate first two cells for systems that only check those if (actual_cell_count == 0) { datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; @@ -112,7 +113,7 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { est_energy_valid = (est_energy_10Wh != 0xFFFF) && (est_energy_10Wh > 0); return; } - + switch (rx_frame.ID) { case 0x7310: case 0x7311: @@ -189,7 +190,7 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { if (rx_frame.ID >= CELL_VOLTAGE_BASE_ID && rx_frame.ID < (CELL_VOLTAGE_BASE_ID + 32)) { uint8_t group = rx_frame.ID - CELL_VOLTAGE_BASE_ID; uint8_t cell_start = group * 8; // 8 cells per message - + for (uint8_t i = 0; i < 8; i++) { uint8_t cell_index = cell_start + i; // Only process cells up to the actual cell count (if known) @@ -214,7 +215,7 @@ void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { else if (rx_frame.ID >= CELL_BALANCING_BASE_ID && rx_frame.ID < (CELL_BALANCING_BASE_ID + 32)) { uint8_t group = rx_frame.ID - CELL_BALANCING_BASE_ID; uint8_t cell_start = group * 8; // 8 cells per message - + for (uint8_t i = 0; i < 8; i++) { uint8_t cell_index = cell_start + i; if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { @@ -241,7 +242,7 @@ void EmusBms::transmit_can(unsigned long currentMillis) { PYLON_4200.data.u8[0] = 0x00; //Request system equipment info } } - + // EMUS BMS auto-broadcasts cell data - no polling needed } diff --git a/Software/src/battery/EMUS-BMS.h b/Software/src/battery/EMUS-BMS.h index ae6a4a4fe..8afc44914 100644 --- a/Software/src/battery/EMUS-BMS.h +++ b/Software/src/battery/EMUS-BMS.h @@ -31,11 +31,11 @@ class EmusBms : public CanBattery { private: static const int MAX_CELL_DEVIATION_MV = 150; - static const int MAX_CELLS = 192; // Maximum cells supported - static const uint32_t EMUS_BASE_ID = 0x10B50000; // EMUS extended ID base - static const uint32_t EMUS_SOC_PARAMS_ID = 0x10B50500; // Base + 0x05 (State of Charge parameters) - static const uint32_t EMUS_ENERGY_PARAMS_ID = 0x10B50600; // Base + 0x06 (Energy parameters) - static const uint32_t CELL_VOLTAGE_BASE_ID = 0x10B50100; // Base CAN ID for cell voltages + static const int MAX_CELLS = 192; // Maximum cells supported + static const uint32_t EMUS_BASE_ID = 0x10B50000; // EMUS extended ID base + static const uint32_t EMUS_SOC_PARAMS_ID = 0x10B50500; // Base + 0x05 (State of Charge parameters) + static const uint32_t EMUS_ENERGY_PARAMS_ID = 0x10B50600; // Base + 0x06 (Energy parameters) + static const uint32_t CELL_VOLTAGE_BASE_ID = 0x10B50100; // Base CAN ID for cell voltages static const uint32_t CELL_BALANCING_BASE_ID = 0x10B50300; // Base CAN ID for balancing status DATALAYER_BATTERY_TYPE* datalayer_battery; @@ -72,16 +72,16 @@ class EmusBms : public CanBattery { .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; // EMUS request for individual cell voltages (group 0 = all cells) CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50100, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50100, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; // EMUS request for individual cell balancing status (group 0 = all cells) CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50300, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + .ext_ID = true, + .DLC = 0, + .ID = 0x19B50300, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; int16_t celltemperature_max_dC; int16_t celltemperature_min_dC; @@ -103,8 +103,8 @@ class EmusBms : public CanBattery { uint8_t SOH = 100; uint8_t charge_forbidden = 0; uint8_t discharge_forbidden = 0; - uint8_t actual_cell_count = 0; // Actual number of cells detected - uint8_t stable_data_cycles = 0; // Counter for stable voltage data + uint8_t actual_cell_count = 0; // Actual number of cells detected + uint8_t stable_data_cycles = 0; // Counter for stable voltage data uint16_t last_min_voltage = 3300; // Track previous min for stability check uint16_t last_max_voltage = 3300; // Track previous max for stability check From 677ba4ecec8ac8d75ef69a10fd80a7e076a09327 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:24:10 +0100 Subject: [PATCH 14/26] Delete Software/src/inverter/EMUS-BMS.cpp --- Software/src/inverter/EMUS-BMS.cpp | 267 ----------------------------- 1 file changed, 267 deletions(-) delete mode 100644 Software/src/inverter/EMUS-BMS.cpp diff --git a/Software/src/inverter/EMUS-BMS.cpp b/Software/src/inverter/EMUS-BMS.cpp deleted file mode 100644 index 69e0ad548..000000000 --- a/Software/src/inverter/EMUS-BMS.cpp +++ /dev/null @@ -1,267 +0,0 @@ -#include "EMUS-BMS.h" -#include "../battery/BATTERIES.h" -#include "../communication/can/comm_can.h" -#include "../datalayer/datalayer.h" -#include "../devboard/utils/events.h" - -void EmusBms::update_values() { - // SOC from EMUS (0-100). real_soc is kept for diagnostics; reported_soc is what we want the inverter to use. - 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)); - - // Option A: Use EMUS estimated remaining charge (Ah) to compute remaining Wh, then derive reported SOC. - // This keeps the inverter-facing (total, remaining, SOC) internally consistent. - const uint32_t total_Wh = datalayer_battery->info.total_capacity_Wh; - const unsigned long now_ms = millis(); - const bool est_charge_fresh = est_charge_valid && ((now_ms - est_charge_last_ms) <= EST_CHARGE_TIMEOUT_MS); - - if (est_charge_fresh && (voltage_dV > 10) && (total_Wh > 0)) { - // remWh = (est_charge_0p1Ah/10) * (voltage_dV/10) = est_charge_0p1Ah * voltage_dV / 100 - uint32_t remWh = (uint32_t)((uint64_t)est_charge_0p1Ah * (uint64_t)voltage_dV / 100ULL); - if (remWh > total_Wh) { - remWh = total_Wh; // Clamp in case EMUS estimate exceeds configured total - } - datalayer_battery->status.remaining_capacity_Wh = remWh; - - uint32_t soc_x100 = (uint32_t)((uint64_t)remWh * 10000ULL / (uint64_t)total_Wh); - if (soc_x100 > 10000) - soc_x100 = 10000; - datalayer_battery->status.reported_soc = soc_x100; - } else { - // Fallback: - // - If total capacity is known, derive remaining from EMUS SOC. - // - If total capacity is unknown, keep inverter SOC aligned with EMUS SOC and report 0Wh remaining. - if (total_Wh > 0) { - datalayer_battery->status.remaining_capacity_Wh = - (uint32_t)((static_cast(datalayer_battery->status.real_soc) / 10000.0) * (double)total_Wh); - } else { - datalayer_battery->status.remaining_capacity_Wh = 0; - } - datalayer_battery->status.reported_soc = datalayer_battery->status.real_soc; - } - - // Update cell count if we've received individual cell data - if (actual_cell_count > 0) { - datalayer_battery->info.number_of_cells = actual_cell_count; - } - - // Use Pylon protocol min/max for alarms (more stable than individual cell data) - // Individual cell voltages from 0x10B5 frames are still available in cell_voltages_mV[] for display - datalayer_battery->status.cell_max_voltage_mV = cellvoltage_max_mV; - datalayer_battery->status.cell_min_voltage_mV = cellvoltage_min_mV; - - // Also populate first two cells for systems that only check those - if (actual_cell_count == 0) { - datalayer_battery->status.cell_voltages_mV[0] = cellvoltage_max_mV; - datalayer_battery->status.cell_voltages_mV[1] = 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; -} - -void EmusBms::handle_incoming_can_frame(CAN_frame rx_frame) { - // Handle EMUS extended ID frames first - if (rx_frame.ID == 0x10B50000) { - // EMUS configuration frame containing cell count - // Byte 0-1: Unknown (00 05) - // Byte 2-3: Unknown (00 03) - // Byte 4-5: Unknown (00 01) - // Byte 6-7: Number of cells (00 77 = 0x77 = 119 decimal) - uint8_t cell_count = rx_frame.data.u8[7]; // Just use byte 7 (0x77 = 119) - // Only update if we got a valid non-zero count - if (cell_count > 0 && cell_count <= MAX_CELLS) { - actual_cell_count = cell_count; - datalayer_battery->info.number_of_cells = actual_cell_count; - } - return; - } - - if (rx_frame.ID == EMUS_SOC_PARAMS_ID) { - // EMUS Base+0x05: State of Charge parameters - // Data0-1: current (0.1A, signed) - // Data2-3: estimated remaining charge (0.1Ah) - // Data6: SOC (%) - // Data7: SOH (%) - current_dA = (int16_t)((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]); - est_charge_0p1Ah = (uint16_t)((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]); - est_charge_valid = (est_charge_0p1Ah != 0xFFFF) && (est_charge_0p1Ah > 0); - est_charge_last_ms = millis(); - SOC = rx_frame.data.u8[6]; - SOH = rx_frame.data.u8[7]; - return; - } - - if (rx_frame.ID == EMUS_ENERGY_PARAMS_ID) { - // EMUS Base+0x06: Energy parameters - // Data2-3: estimated energy left (10Wh) - est_energy_10Wh = (uint16_t)((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]); - est_energy_valid = (est_energy_10Wh != 0xFFFF) && (est_energy_10Wh > 0); - return; - } - - switch (rx_frame.ID) { - case 0x7310: - case 0x7311: - ensemble_info_ack = true; - // This message contains software/hardware version info. No interest to us - break; - case 0x7320: - case 0x7321: - ensemble_info_ack = true; - battery_module_quantity = rx_frame.data.u8[0]; - battery_modules_in_series = rx_frame.data.u8[2]; - cell_quantity_in_module = rx_frame.data.u8[3]; - voltage_level = rx_frame.data.u8[4]; - ah_number = rx_frame.data.u8[6]; - break; - case 0x4210: - case 0x4211: - datalayer_battery->status.CAN_battery_still_alive = CAN_STILL_ALIVE; - voltage_dV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); - current_dA = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 30000; - SOC = rx_frame.data.u8[6]; - SOH = rx_frame.data.u8[7]; - break; - case 0x4220: - case 0x4221: - charge_cutoff_voltage = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); - discharge_cutoff_voltage = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); - max_charge_current = (((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]) * 0.1) - 3000; - max_discharge_current = (((rx_frame.data.u8[7] << 8) | rx_frame.data.u8[6]) * 0.1) - 3000; - break; - case 0x4230: - case 0x4231: - cellvoltage_max_mV = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]); - cellvoltage_min_mV = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]); - break; - case 0x4240: - case 0x4241: - celltemperature_max_dC = ((rx_frame.data.u8[1] << 8) | rx_frame.data.u8[0]) - 1000; - celltemperature_min_dC = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]) - 1000; - break; - case 0x4250: - case 0x4251: - //Byte0 Basic Status - //Byte1-2 Cycle Period - //Byte3 Error - //Byte4-5 Alarm - //Byte6-7 Protection - break; - case 0x4260: - case 0x4261: - //Byte0-1 Module Max Voltage - //Byte2-3 Module Min Voltage - //Byte4-5 Module Max. Voltage Number - //Byte6-7 Module Min. Voltage Number - break; - case 0x4270: - case 0x4271: - //Byte0-1 Module Max. Temperature - //Byte2-3 Module Min. Temperature - //Byte4-5 Module Max. Temperature Number - //Byte6-7 Module Min. Temperature Number - break; - case 0x4280: - case 0x4281: - charge_forbidden = rx_frame.data.u8[0]; - discharge_forbidden = rx_frame.data.u8[1]; - break; - case 0x4290: - case 0x4291: - break; - default: - // Handle EMUS individual cell voltage messages (0x10B50100-0x10B5011F) - // Each message contains 8 cells (1 byte per cell) - if (rx_frame.ID >= CELL_VOLTAGE_BASE_ID && rx_frame.ID < (CELL_VOLTAGE_BASE_ID + 32)) { - uint8_t group = rx_frame.ID - CELL_VOLTAGE_BASE_ID; - uint8_t cell_start = group * 8; // 8 cells per message - - for (uint8_t i = 0; i < 8; i++) { - uint8_t cell_index = cell_start + i; - // Only process cells up to the actual cell count (if known) - if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { - // Cell voltage: 2000mV base + (byte value × 10mV) - // e.g., 0x7B (123) = 2000 + 123 × 10 = 3230mV - uint16_t cell_voltage = 2000 + (rx_frame.data.u8[i] * 10); - // Only update if voltage is in valid LiFePO4 range (2500-4200mV) - if (cell_voltage >= 2500 && cell_voltage <= 4200) { - uint16_t current_voltage = datalayer_battery->status.cell_voltages_mV[cell_index]; - // Reject sudden large changes (>1000mV) as likely data corruption - // Using 1000mV threshold since EMUS updates every 5-6 seconds - if (current_voltage == 0 || abs((int)cell_voltage - (int)current_voltage) <= 1000) { - datalayer_battery->status.cell_voltages_mV[cell_index] = cell_voltage; - } - } - } - } - } - // Handle EMUS individual cell balancing status messages (0x10B50300-0x10B5031F) - // Each message contains 8 cells (1 byte per cell) - else if (rx_frame.ID >= CELL_BALANCING_BASE_ID && rx_frame.ID < (CELL_BALANCING_BASE_ID + 32)) { - uint8_t group = rx_frame.ID - CELL_BALANCING_BASE_ID; - uint8_t cell_start = group * 8; // 8 cells per message - - for (uint8_t i = 0; i < 8; i++) { - uint8_t cell_index = cell_start + i; - if (cell_index < MAX_CELLS && (actual_cell_count == 0 || cell_index < actual_cell_count)) { - // Balancing status: non-zero value = balancing active - datalayer_battery->status.cell_balancing_status[cell_index] = (rx_frame.data.u8[i] > 0); - } - } - } - break; - } -} - -void EmusBms::transmit_can(unsigned long currentMillis) { - // Send 1s CAN Message - if (currentMillis - previousMillis1000 >= INTERVAL_1_S) { - previousMillis1000 = currentMillis; - - transmit_can_frame(&PYLON_3010); // Heartbeat - transmit_can_frame(&PYLON_4200); // Ensemble OR System equipment info, depends on frame0 - transmit_can_frame(&PYLON_8200); // Control device quit sleep status - transmit_can_frame(&PYLON_8210); // Charge command - - if (ensemble_info_ack) { - PYLON_4200.data.u8[0] = 0x00; //Request system equipment info - } - } - - // EMUS BMS auto-broadcasts cell data - no polling needed -} - -void EmusBms::setup(void) { // Performs one time setup at startup - strncpy(datalayer.system.info.battery_protocol, "EMUS BMS (Pylon 250k)", 63); - datalayer.system.info.battery_protocol[63] = '\0'; - datalayer_battery->info.number_of_cells = 2; // Will be updated dynamically based on received data - datalayer_battery->info.max_design_voltage_dV = user_selected_max_pack_voltage_dV; - datalayer_battery->info.min_design_voltage_dV = user_selected_min_pack_voltage_dV; - datalayer_battery->info.max_cell_voltage_mV = user_selected_max_cell_voltage_mV; - datalayer_battery->info.min_cell_voltage_mV = user_selected_min_cell_voltage_mV; - datalayer_battery->info.max_cell_voltage_deviation_mV = MAX_CELL_DEVIATION_MV; - - // Initialize all cell voltages to a safe mid-range value to prevent false low voltage alarms - for (uint16_t i = 0; i < MAX_CELLS; i++) { - datalayer_battery->status.cell_voltages_mV[i] = 3300; // Safe default value - } - - if (allows_contactor_closing) { - *allows_contactor_closing = true; - } -} From 832b10fab999eadba102fed68df932266ce912c4 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:24:26 +0100 Subject: [PATCH 15/26] Delete Software/src/inverter/EMUS-BMS.h --- Software/src/inverter/EMUS-BMS.h | 122 ------------------------------- 1 file changed, 122 deletions(-) delete mode 100644 Software/src/inverter/EMUS-BMS.h diff --git a/Software/src/inverter/EMUS-BMS.h b/Software/src/inverter/EMUS-BMS.h deleted file mode 100644 index 8afc44914..000000000 --- a/Software/src/inverter/EMUS-BMS.h +++ /dev/null @@ -1,122 +0,0 @@ -#ifndef EMUS_BMS_H -#define EMUS_BMS_H - -#include "../datalayer/datalayer.h" -#include "CanBattery.h" - -class EmusBms : public CanBattery { - public: - // Use this constructor for the second battery. - EmusBms(DATALAYER_BATTERY_TYPE* datalayer_ptr, bool* contactor_closing_allowed_ptr, CAN_Interface targetCan) - : CanBattery(targetCan, CAN_Speed::CAN_SPEED_250KBPS) { - datalayer_battery = datalayer_ptr; - contactor_closing_allowed = contactor_closing_allowed_ptr; - allows_contactor_closing = nullptr; - } - - // Use the default constructor to create the first or single battery. - EmusBms() : CanBattery(CAN_Speed::CAN_SPEED_250KBPS) { - datalayer_battery = &datalayer.battery; - allows_contactor_closing = &datalayer.system.status.battery_allows_contactor_closing; - contactor_closing_allowed = nullptr; - } - - virtual ~EmusBms() = default; - - 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 = "EMUS BMS compatible battery"; - - private: - static const int MAX_CELL_DEVIATION_MV = 150; - static const int MAX_CELLS = 192; // Maximum cells supported - static const uint32_t EMUS_BASE_ID = 0x10B50000; // EMUS extended ID base - static const uint32_t EMUS_SOC_PARAMS_ID = 0x10B50500; // Base + 0x05 (State of Charge parameters) - static const uint32_t EMUS_ENERGY_PARAMS_ID = 0x10B50600; // Base + 0x06 (Energy parameters) - static const uint32_t CELL_VOLTAGE_BASE_ID = 0x10B50100; // Base CAN ID for cell voltages - static const uint32_t CELL_BALANCING_BASE_ID = 0x10B50300; // Base CAN ID for balancing status - - DATALAYER_BATTERY_TYPE* datalayer_battery; - - // If not null, this battery decides when the contactor can be closed and writes the value here. - bool* allows_contactor_closing; - - // If not null, this battery listens to this boolean to determine whether contactor closing is allowed - bool* contactor_closing_allowed; - - unsigned long previousMillis1000 = 0; // will store last time a 1s CAN Message was sent - unsigned long previousMillis5000 = 0; // will store last time a 5s CAN Message was sent - - //Actual content messages - CAN_frame PYLON_3010 = {.FD = false, - .ext_ID = true, - .DLC = 8, - .ID = 0x3010, - .data = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - CAN_frame PYLON_8200 = {.FD = false, - .ext_ID = true, - .DLC = 8, - .ID = 0x8200, - .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - CAN_frame PYLON_8210 = {.FD = false, - .ext_ID = true, - .DLC = 8, - .ID = 0x8210, - .data = {0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - CAN_frame PYLON_4200 = {.FD = false, - .ext_ID = true, - .DLC = 8, - .ID = 0x4200, - .data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - // EMUS request for individual cell voltages (group 0 = all cells) - CAN_frame EMUS_CELL_VOLTAGE_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50100, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - // EMUS request for individual cell balancing status (group 0 = all cells) - CAN_frame EMUS_CELL_BALANCING_REQUEST = {.FD = false, - .ext_ID = true, - .DLC = 0, - .ID = 0x19B50300, - .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; - - int16_t celltemperature_max_dC; - int16_t celltemperature_min_dC; - int16_t current_dA; - uint16_t voltage_dV = 0; - uint16_t cellvoltage_max_mV = 3300; - uint16_t cellvoltage_min_mV = 3300; - uint16_t charge_cutoff_voltage = 0; - uint16_t discharge_cutoff_voltage = 0; - int16_t max_charge_current = 0; - int16_t max_discharge_current = 0; - uint8_t ensemble_info_ack = 0; - uint8_t battery_module_quantity = 0; - uint8_t battery_modules_in_series = 0; - uint8_t cell_quantity_in_module = 0; - uint8_t voltage_level = 0; - uint8_t ah_number = 0; - uint8_t SOC = 50; - uint8_t SOH = 100; - uint8_t charge_forbidden = 0; - uint8_t discharge_forbidden = 0; - uint8_t actual_cell_count = 0; // Actual number of cells detected - uint8_t stable_data_cycles = 0; // Counter for stable voltage data - uint16_t last_min_voltage = 3300; // Track previous min for stability check - uint16_t last_max_voltage = 3300; // Track previous max for stability check - - // EMUS estimated remaining charge (0.1Ah units) from Base+0x05. - uint16_t est_charge_0p1Ah = 0; - bool est_charge_valid = false; - unsigned long est_charge_last_ms = 0; - static const unsigned long EST_CHARGE_TIMEOUT_MS = 10000; // Treat estimate as stale after 10s - - // EMUS estimated remaining energy (10Wh units) from Base+0x06 (optional diagnostics). - uint16_t est_energy_10Wh = 0; - bool est_energy_valid = false; -}; - -#endif From 8c990ef4030968fefdcb46e0dc35cd6dd9f8b921 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:49:01 +0100 Subject: [PATCH 16/26] Update GROWATT-HV-CAN.cpp with new content --- Software/src/inverter/GROWATT-HV-CAN.cpp | 338 ++++++++++++----------- 1 file changed, 172 insertions(+), 166 deletions(-) diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp index 6e33160a9..9d4a89ca5 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -3,245 +3,252 @@ #include "../datalayer/datalayer.h" /* TODO: -This protocol has not been tested with any inverter. Proceed with extreme caution. -Search the file for "TODO" to see all the places that might require work - -Growatt BMS CAN-Bus-protocol High Voltage V1.10 2023-11-06 -29-bit identifier -500kBit/sec -Big-endian - -Terms and abbreviations: -PCS - Power conversion system (the Storage Inverter) -Cell - A single battery cell -Module - A battery module composed of 16 strings of cells -Pack - A battery pack composed of the BMS and battery modules connected in parallel and series, which can work independently -FCC - Full charge capacity -RM - Remaining capacity -BMS - Battery Information Collector */ - -void GrowattHvInverter:: - update_values() { //This function maps all the values fetched from battery CAN to the correct CAN messages + This protocol has not been tested with any inverter. Proceed with extreme caution. + Search the file for "TODO" to see all the places that might require work + + Growatt BMS CAN-Bus-protocol High Voltage V1.10 2023-11-06 + 29-bit identifier + 500kBit/sec + Big-endian + + Terms and abbreviations: + PCS - Power conversion system (the Storage Inverter) + Cell - A single battery cell + Module - A battery module composed of 16 strings of cells + Pack - A battery pack composed of the BMS and battery modules connected in parallel and series, which can work independently + FCC - Full charge capacity + RM - Remaining capacity + BMS - Battery Information Collector */ + +void GrowattHvInverter::update_values() { + // This function maps all the values fetched from battery CAN to the correct CAN messages if (datalayer.battery.status.voltage_dV > 10) { // Only update value when we have voltage available to avoid div0 ampere_hours_remaining = - ((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) * - 100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah) - ampere_hours_full = ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * - 100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah) + ((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); + // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) + ampere_hours_full = + ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); + // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) } - //Map values to CAN messages - //Battery operating parameters and status information - if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage - //User specified charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V) + // Map values to CAN messages + // Battery operating parameters and status information + if (datalayer.battery.settings.user_set_voltage_limits_active) { // If user is requesting a specific voltage + // User specified charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V) GROWATT_3110.data.u8[0] = (datalayer.battery.settings.max_user_set_charge_voltage_dV >> 8); GROWATT_3110.data.u8[1] = (datalayer.battery.settings.max_user_set_charge_voltage_dV & 0x00FF); } else { - //Battery max voltage used as charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V) + // Battery max voltage used as charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V) GROWATT_3110.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV >> 8); GROWATT_3110.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF); } - //Charge limited current, 125 =12.5A (0.1, A) (Min 0, Max 300A) + + // Charge limited current, 125 =12.5A (0.1, A) (Min 0, Max 300A) GROWATT_3110.data.u8[2] = (datalayer.battery.status.max_charge_current_dA >> 8); GROWATT_3110.data.u8[3] = (datalayer.battery.status.max_charge_current_dA & 0x00FF); - //Discharge limited current, 500 = 50A, (0.1, A) + // Discharge limited current, 500 = 50A, (0.1, A) GROWATT_3110.data.u8[4] = (datalayer.battery.status.max_discharge_current_dA >> 8); GROWATT_3110.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); - //Status bits (see documentation for all bits, most important are mapped - GROWATT_3110.data.u8[7] = 0x00; // Clear all bits + + // Status bits (see documentation for all bits, most important are mapped) + GROWATT_3110.data.u8[7] = 0x00; // Clear all bits if (datalayer.battery.status.active_power_W < -1) { // Discharging - GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000011); + GROWATT_3110.data.u8[7] |= 0b00000011; } else if (datalayer.battery.status.active_power_W > 1) { // Charging - GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000010); - } else { //Idle - GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000001); + GROWATT_3110.data.u8[7] |= 0b00000010; + } else { // Idle + GROWATT_3110.data.u8[7] |= 0b00000001; } - if ((datalayer.battery.status.max_charge_current_dA == 0) || (datalayer.battery.status.reported_soc == 10000) || - (datalayer.battery.status.bms_status == FAULT)) { - GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b01000000); // No Charge - } else { //continue using battery - GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000000); // Charge allowed + + if ((datalayer.battery.status.max_charge_current_dA == 0) || + (datalayer.battery.status.reported_soc == 10000) || (datalayer.battery.status.bms_status == FAULT)) { + GROWATT_3110.data.u8[7] |= 0b01000000; // No Charge + } else { + GROWATT_3110.data.u8[7] |= 0b00000000; // Charge allowed } + if ((datalayer.battery.status.max_discharge_current_dA == 0) || (datalayer.battery.status.reported_soc == 0) || (datalayer.battery.status.bms_status == FAULT)) { - GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00100000); // No Discharge - } else { //continue using battery - GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000000); // Discharge allowed + GROWATT_3110.data.u8[7] |= 0b00100000; // No Discharge + } else { + GROWATT_3110.data.u8[7] |= 0b00000000; // Discharge allowed } - GROWATT_3110.data.u8[6] = (GROWATT_3110.data.u8[6] | 0b00100000); // ISO Detection status: Detected - GROWATT_3110.data.u8[6] = (GROWATT_3110.data.u8[6] | 0b00010000); // Battery status: Normal - - //Battery protection and alarm information - //Fault and warning status bits. TODO, map these according to documentation. - //GROWATT_3120.data.u8[0] = - //GROWATT_3120.data.u8[1] = - //GROWATT_3120.data.u8[2] = - //GROWATT_3120.data.u8[3] = - //GROWATT_3120.data.u8[4] = - //GROWATT_3120.data.u8[5] = - //GROWATT_3120.data.u8[6] = - //GROWATT_3120.data.u8[7] = - - //Battery operation information - //Voltage of the pack (0.1V) [0-1000V] + + GROWATT_3110.data.u8[6] |= 0b00100000; // ISO Detection status: Detected + GROWATT_3110.data.u8[6] |= 0b00010000; // Battery status: Normal + + // Battery protection and alarm information + // Fault and warning status bits. TODO, map these according to documentation. + // GROWATT_3120.data.u8[0] = + // GROWATT_3120.data.u8[1] = + // GROWATT_3120.data.u8[2] = + // GROWATT_3120.data.u8[3] = + // GROWATT_3120.data.u8[4] = + // GROWATT_3120.data.u8[5] = + // GROWATT_3120.data.u8[6] = + // GROWATT_3120.data.u8[7] = + + // Battery operation information + // Voltage of the pack (0.1V) [0-1000V] GROWATT_3130.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8); GROWATT_3130.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF); - //Total current (0.1A -300 to 300A) + // Total current (0.1A -300 to 300A) GROWATT_3130.data.u8[2] = (datalayer.battery.status.reported_current_dA >> 8); GROWATT_3130.data.u8[3] = (datalayer.battery.status.reported_current_dA & 0x00FF); - //Cell max temperature (0.1C) [-40 to 120*C] + // Cell max temperature (0.1C) [-40 to 120*C] GROWATT_3130.data.u8[4] = (datalayer.battery.status.temperature_max_dC >> 8); GROWATT_3130.data.u8[5] = (datalayer.battery.status.temperature_max_dC & 0x00FF); - //SOC (%) [0-100] + // SOC (%) [0-100] GROWATT_3130.data.u8[6] = (datalayer.battery.status.reported_soc / 100); - //SOH (%) (Bit 0~ Bit6 SOH Counters) Bit7 SOH flag (Indicates that battery is in unsafe use) + // SOH (%) (Bit 0~ Bit6 SOH Counters) Bit7 SOH flag (Indicates that battery is in unsafe use) GROWATT_3130.data.u8[7] = (datalayer.battery.status.soh_pptt / 100); - //Battery capacity information - //Remaining capacity (10 mAh) [0.0 ~ 500000.0 mAH] + // Battery capacity information + // Remaining capacity (10 mAh) [0.0 ~ 500000.0 mAH] GROWATT_3140.data.u8[0] = ((ampere_hours_remaining * 100) >> 8); GROWATT_3140.data.u8[1] = ((ampere_hours_remaining * 100) & 0x00FF); - //Fully charged capacity (10 mAh) [0.0 ~ 500000.0 mAH] + // Fully charged capacity (10 mAh) [0.0 ~ 500000.0 mAH] GROWATT_3140.data.u8[2] = ((ampere_hours_full * 100) >> 8); GROWATT_3140.data.u8[3] = ((ampere_hours_full * 100) & 0x00FF); - //Manufacturer code + // Manufacturer code GROWATT_3140.data.u8[4] = MANUFACTURER_ASCII_0; GROWATT_3140.data.u8[5] = MANUFACTURER_ASCII_1; - //Cycle count (h) + // Cycle count (h) GROWATT_3140.data.u8[6] = 0; GROWATT_3140.data.u8[7] = 0; - //Battery working parameters and module number information - if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage - //Use user specified voltage as Discharge cutoff voltage (0.1V) [0-1000V] + // Battery working parameters and module number information + if (datalayer.battery.settings.user_set_voltage_limits_active) { // If user is requesting a specific voltage + // Use user specified voltage as Discharge cutoff voltage (0.1V) [0-1000V] GROWATT_3150.data.u8[0] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV >> 8); GROWATT_3150.data.u8[1] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV & 0x00FF); } else { - //Use battery min design voltage as Discharge cutoff voltage (0.1V) [0-1000V] + // Use battery min design voltage as Discharge cutoff voltage (0.1V) [0-1000V] GROWATT_3150.data.u8[0] = (datalayer.battery.info.min_design_voltage_dV >> 8); GROWATT_3150.data.u8[1] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF); } - //Main control unit temperature (0.1C) [-40 to 120*C] + + // Main control unit temperature (0.1C) [-40 to 120*C] GROWATT_3150.data.u8[2] = (datalayer.battery.status.temperature_max_dC >> 8); GROWATT_3150.data.u8[3] = (datalayer.battery.status.temperature_max_dC & 0x00FF); - //Total number of cells + // Total number of cells GROWATT_3150.data.u8[4] = (TOTAL_NUMBER_OF_CELLS >> 8); GROWATT_3150.data.u8[5] = (TOTAL_NUMBER_OF_CELLS & 0x00FF); - //Number of modules in series + // Number of modules in series GROWATT_3150.data.u8[6] = (NUMBER_OF_MODULES_IN_SERIES >> 8); GROWATT_3150.data.u8[7] = (NUMBER_OF_MODULES_IN_SERIES & 0x00FF); - //Battery fault and voltage number information - //Fault flag bit - GROWATT_3160.data.u8[0] = 0; //TODO: Map according to documentation - //Fault extension flag bit - GROWATT_3160.data.u8[1] = 0; //TODO: Map according to documentation - //Number of module with the maximum cell voltage (1-32) + // Battery fault and voltage number information + // Fault flag bit + GROWATT_3160.data.u8[0] = 0; // TODO: Map according to documentation + // Fault extension flag bit + GROWATT_3160.data.u8[1] = 0; // TODO: Map according to documentation + // Number of module with the maximum cell voltage (1-32) GROWATT_3160.data.u8[2] = 1; - //Number of cell with the maximum cell voltage (1-128) + // Number of cell with the maximum cell voltage (1-128) GROWATT_3160.data.u8[3] = 1; - //Number of module with the minimum cell voltage (1-32) + // Number of module with the minimum cell voltage (1-32) GROWATT_3160.data.u8[4] = 1; - //Number of cell with the minimum cell voltage (1-128) + // Number of cell with the minimum cell voltage (1-128) GROWATT_3160.data.u8[5] = 2; - //Minimum cell temperature (0.1C) [-40 to 120*C] + // Minimum cell temperature (0.1C) [-40 to 120*C] GROWATT_3160.data.u8[6] = (datalayer.battery.status.temperature_min_dC >> 8); GROWATT_3160.data.u8[7] = (datalayer.battery.status.temperature_min_dC & 0x00FF); - //Software version and temperature number information - //Number of Module with the maximum cell temperature (1-32) + // Software version and temperature number information + // Number of Module with the maximum cell temperature (1-32) GROWATT_3170.data.u8[0] = 1; - //Number of cell with the maximum cell temperature (1-128) + // Number of cell with the maximum cell temperature (1-128) GROWATT_3170.data.u8[1] = 1; - //Number of module with the minimum cell temperature (1-32) + // Number of module with the minimum cell temperature (1-32) GROWATT_3170.data.u8[2] = 1; - //Number of cell with the minimum cell temperature (1-128) + // Number of cell with the minimum cell temperature (1-128) GROWATT_3170.data.u8[3] = 2; - //Battery actial capacity (0-100) TODO, what is unit? + // Battery actual capacity (0-100) TODO, what is unit? GROWATT_3170.data.u8[4] = 50; - //Battery correction status display value (0-255) + // Battery correction status display value (0-255) GROWATT_3170.data.u8[5] = 0; // Remaining balancing time (0-255) GROWATT_3170.data.u8[6] = 0; - //Balancing state, bit0-3(range 0-15) , Internal short circuit state, bit4-7 (range 0-15) + // Balancing state, bit0-3(range 0-15) , Internal short circuit state, bit4-7 (range 0-15) GROWATT_3170.data.u8[7] = 0; - //Battery Code and quantity information - //Manufacturer code + // Battery Code and quantity information + // Manufacturer code GROWATT_3180.data.u8[0] = MANUFACTURER_ASCII_0; GROWATT_3180.data.u8[1] = MANUFACTURER_ASCII_1; - //Number of Packs in parallel (1-65536) + // Number of Packs in parallel (1-65536) GROWATT_3180.data.u8[2] = (NUMBER_OF_PACKS_IN_PARALLEL >> 8); GROWATT_3180.data.u8[3] = (NUMBER_OF_PACKS_IN_PARALLEL & 0x00FF); - //Total number of cells (1-65536) + // Total number of cells (1-65536) GROWATT_3180.data.u8[4] = (TOTAL_NUMBER_OF_CELLS >> 8); GROWATT_3180.data.u8[5] = (TOTAL_NUMBER_OF_CELLS & 0x00FF); - //Pack number + BIC forward/reverse encoding number + // Pack number + BIC forward/reverse encoding number // Bits 0-3: Pack number - // Bits 4-9: Max. number of BIC in forward BIC encoding in daisy- chain communication - // Bits 10-15: Max. number of BIC in reverse BIC encoding in daisy- chain communication - GROWATT_3180.data.u8[6] = 0; //TODO, this OK? - GROWATT_3180.data.u8[7] = 0; //TODO, this OK? - - //Cell voltage and status information - //Battery status - GROWATT_3190.data.u8[0] = 0; //LFP, no forced charge - //Maximum cell voltage (mV) + // Bits 4-9: Max. number of BIC in forward BIC encoding in daisy-chain communication + // Bits 10-15: Max. number of BIC in reverse BIC encoding in daisy-chain communication + GROWATT_3180.data.u8[6] = 0; // TODO, this OK? + GROWATT_3180.data.u8[7] = 0; // TODO, this OK? + + // Cell voltage and status information + // Battery status + GROWATT_3190.data.u8[0] = 0; // LFP, no forced charge + // Maximum cell voltage (mV) GROWATT_3190.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8); GROWATT_3190.data.u8[2] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF); // Min cell voltage (mV) GROWATT_3190.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV >> 8); GROWATT_3190.data.u8[4] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF); - //Reserved + // Reserved GROWATT_3190.data.u8[5] = 0; // Faulty battery pack number (1-16) GROWATT_3190.data.u8[6] = 0; - //Faulty battery module number (1-16) + // Faulty battery module number (1-16) GROWATT_3190.data.u8[7] = 0; - //Manufacturer name and version information + // Manufacturer name and version information // Manufacturer name (ASCII) Battery manufacturer abbreviation in capital letters GROWATT_3200.data.u8[0] = MANUFACTURER_ASCII_0; GROWATT_3200.data.u8[1] = MANUFACTURER_ASCII_1; // Hardware revision (0 null, 1 verA, 2verB) GROWATT_3200.data.u8[2] = 0x01; // Reserved - GROWATT_3200.data.u8[3] = 0; //Reserved - //Circulating current value (0.1A), Range 0-20A + GROWATT_3200.data.u8[3] = 0; // Reserved + // Circulating current value (0.1A), Range 0-20A GROWATT_3200.data.u8[4] = 0; GROWATT_3200.data.u8[5] = 0; - //Cell charge cutoff voltage + // Cell charge cutoff voltage GROWATT_3200.data.u8[6] = (datalayer.battery.info.max_cell_voltage_mV >> 8); GROWATT_3200.data.u8[7] = (datalayer.battery.info.max_cell_voltage_mV & 0x00FF); - //Upgrade information - //Message 0x3210 is update status. All blank is OK - //GROWATT_3210.data.u8[0] = 0; - - //De-rating and fault information (reserved) - //Power reduction sign - GROWATT_3220.data.u8[0] = 0; //Bits set to high incase we need to derate - GROWATT_3220.data.u8[1] = 0; //Bits set to high incase we need to derate - //System fault status - GROWATT_3220.data.u8[2] = 0; //All normal - GROWATT_3220.data.u8[3] = 0; //All normal - //Forced discharge mark - GROWATT_3220.data.u8[4] = 0; //When you want to force charge battery, send 0xAA here - //Battery rated energy information (Unit 0.1 kWh ) 30kWh = 300 , so 30000Wh needs to be div by 100 + // Upgrade information + // Message 0x3210 is update status. All blank is OK + // GROWATT_3210.data.u8[0] = 0; + + // De-rating and fault information (reserved) + // Power reduction sign + GROWATT_3220.data.u8[0] = 0; // Bits set to high incase we need to derate + GROWATT_3220.data.u8[1] = 0; // Bits set to high incase we need to derate + // System fault status + GROWATT_3220.data.u8[2] = 0; // All normal + GROWATT_3220.data.u8[3] = 0; // All normal + // Forced discharge mark + GROWATT_3220.data.u8[4] = 0; // When you want to force charge battery, send 0xAA here + // Battery rated energy information (Unit 0.1 kWh ) 30kWh = 300 , so 30000Wh needs to be div by 100 GROWATT_3220.data.u8[5] = ((datalayer.battery.info.total_capacity_Wh / 100) >> 8); GROWATT_3220.data.u8[6] = ((datalayer.battery.info.total_capacity_Wh / 100) & 0x00FF); - //Software subversion number + // Software subversion number GROWATT_3220.data.u8[7] = 0; - //Serial number - //Frame number + // Serial number + // Frame number GROWATT_3230.data.u8[0] = serial_number_counter; - //Serial number content - //The serial number includes the PACK number (1byte: range [1, 11]) and serial number (16bytes). - //(Reserved and filled with 0x00) Explanation: Byte 1 (Battery ID) = 0:Invalid. - //When Byte 1 (Battery ID) = 1, it represents the SN (Serial Number) of the - //high-voltage controller. When BYTE1 (Battery ID) = 2~11, it represents the SN of PACK 1~10. + // Serial number content + // The serial number includes the PACK number (1byte: range [1, 11]) and serial number (16bytes). + // (Reserved and filled with 0x00) Explanation: Byte 1 (Battery ID) = 0:Invalid. + // When Byte 1 (Battery ID) = 1, it represents the SN (Serial Number) of the + // high-voltage controller. When BYTE1 (Battery ID) = 2~11, it represents the SN of PACK 1~10. switch (serial_number_counter) { case 0: GROWATT_3230.data.u8[1] = 0; // BATTERY ID @@ -275,21 +282,21 @@ void GrowattHvInverter:: } serial_number_counter = (serial_number_counter + 1) % 3; // cycles between 0-1-2-0-1... - //Total charge/discharge energy - //Pack number (1-16) + // Total charge/discharge energy + // Pack number (1-16) GROWATT_3240.data.u8[0] = 1; - //Total lifetime discharge energy Unit: 0.1kWh Range [0.0~10000000.0kWh] + // Total lifetime discharge energy Unit: 0.1kWh Range [0.0~10000000.0kWh] GROWATT_3240.data.u8[1] = 0; GROWATT_3240.data.u8[2] = 0; GROWATT_3240.data.u8[3] = 0; - //Pack number (1-16) + // Pack number (1-16) GROWATT_3240.data.u8[4] = 1; - //Total lifetime charge energy Unit: 0.1kWh Range [0.0~10000000.0kWh] + // Total lifetime charge energy Unit: 0.1kWh Range [0.0~10000000.0kWh] GROWATT_3240.data.u8[5] = 0; GROWATT_3240.data.u8[6] = 0; GROWATT_3240.data.u8[7] = 0; - //Fault history + // Fault history // Not applicable for our use. All values at 0 indicates no fault GROWATT_3250.data.u8[0] = 0; GROWATT_3250.data.u8[1] = 0; @@ -300,7 +307,7 @@ void GrowattHvInverter:: GROWATT_3250.data.u8[6] = 0; GROWATT_3250.data.u8[7] = 0; - //Battery internal debugging fault message + // Battery internal debugging fault message // Not applicable for our use. All values at 0 indicates no fault GROWATT_3260.data.u8[0] = 0; GROWATT_3260.data.u8[1] = 0; @@ -311,7 +318,7 @@ void GrowattHvInverter:: GROWATT_3260.data.u8[6] = 0; GROWATT_3260.data.u8[7] = 0; - //Battery internal debugging fault message + // Battery internal debugging fault message // Not applicable for our use. All values at 0 indicates no fault GROWATT_3270.data.u8[0] = 0; GROWATT_3270.data.u8[1] = 0; @@ -322,21 +329,21 @@ void GrowattHvInverter:: GROWATT_3270.data.u8[6] = 0; GROWATT_3270.data.u8[7] = 0; - //Product Version Information - GROWATT_3280.data.u8[0] = 0; //Reserved - //Product version number (1-5) - GROWATT_3280.data.u8[1] = 0; //No software version (1 indicates PRODUCT, 2 indicates COMMUNICATION version) - //For example, the version code for the main control unit (product) of a high-voltage battery is QBAA, - // and the code formonitoring (communication)is ZEAA - //TODO, is this needed? - GROWATT_3280.data.u8[2] = 0; //ASCII - GROWATT_3280.data.u8[3] = 0; //ASCII - GROWATT_3280.data.u8[4] = 0; //ASCII - GROWATT_3280.data.u8[5] = 0; //ASCII - GROWATT_3280.data.u8[6] = 0; //Software version information - - //Battery series information - GROWATT_3290.data.u8[0] = 0; // DTC, range Range [0,65536], Default 12041 (TODO, shall we send that?) + // Product Version Information + GROWATT_3280.data.u8[0] = 0; // Reserved + // Product version number (1-5) + GROWATT_3280.data.u8[1] = 0; // No software version (1 indicates PRODUCT, 2 indicates COMMUNICATION version) + // For example, the version code for the main control unit (product) of a high-voltage battery is QBAA, + // and the code for monitoring (communication) is ZEAA + // TODO, is this needed? + GROWATT_3280.data.u8[2] = 0; // ASCII + GROWATT_3280.data.u8[3] = 0; // ASCII + GROWATT_3280.data.u8[4] = 0; // ASCII + GROWATT_3280.data.u8[5] = 0; // ASCII + GROWATT_3280.data.u8[6] = 0; // Software version information + + // Battery series information + GROWATT_3290.data.u8[0] = 0; // DTC, range [0,65536], Default 12041 (TODO, shall we send that?) GROWATT_3290.data.u8[1] = 0; // RESERVED GROWATT_3290.data.u8[2] = 0; // RESERVED GROWATT_3290.data.u8[3] = 0; // RESERVED @@ -345,8 +352,8 @@ void GrowattHvInverter:: GROWATT_3290.data.u8[6] = 0; // RESERVED GROWATT_3290.data.u8[7] = 0; // RESERVED - //Internal alarm information - //Battery internal debugging fault message + // Internal alarm information + // Battery internal debugging fault message GROWATT_3F00.data.u8[0] = 0; // RESERVED GROWATT_3F00.data.u8[1] = 0; // RESERVED GROWATT_3F00.data.u8[2] = 0; // RESERVED @@ -355,7 +362,7 @@ void GrowattHvInverter:: GROWATT_3F00.data.u8[5] = 0; // RESERVED GROWATT_3F00.data.u8[6] = 0; // RESERVED GROWATT_3F00.data.u8[7] = 0; // RESERVED -} +} void GrowattHvInverter::map_can_frame_to_variable(CAN_frame rx_frame) { switch (rx_frame.ID) { @@ -388,12 +395,11 @@ void GrowattHvInverter::map_can_frame_to_variable(CAN_frame rx_frame) { } void GrowattHvInverter::transmit_can(unsigned long currentMillis) { - if (!inverter_alive) { - return; //Dont send messages towards inverter until it has started + return; // Don't send messages towards inverter until it has started } - //Check if 1 second has passed, then we start sending! + // Check if 1 second has passed, then we start sending! if (currentMillis - previousMillis1s >= INTERVAL_1_S) { previousMillis1s = currentMillis; time_to_send_1s_data = true; @@ -447,4 +453,4 @@ void GrowattHvInverter::transmit_can(unsigned long currentMillis) { can_message_batch_index = 0; } } -} +} \ No newline at end of file From 2e3585ee120df84cd6a5f8622ae5eb9362f4a1ec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:49:09 +0000 Subject: [PATCH 17/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Software/src/inverter/GROWATT-HV-CAN.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp index 9d4a89ca5..f30e93ab4 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -27,8 +27,7 @@ void GrowattHvInverter::update_values() { ampere_hours_remaining = ((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) - ampere_hours_full = - ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); + ampere_hours_full = ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) } @@ -52,7 +51,7 @@ void GrowattHvInverter::update_values() { GROWATT_3110.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); // Status bits (see documentation for all bits, most important are mapped) - GROWATT_3110.data.u8[7] = 0x00; // Clear all bits + GROWATT_3110.data.u8[7] = 0x00; // Clear all bits if (datalayer.battery.status.active_power_W < -1) { // Discharging GROWATT_3110.data.u8[7] |= 0b00000011; } else if (datalayer.battery.status.active_power_W > 1) { // Charging @@ -61,8 +60,8 @@ void GrowattHvInverter::update_values() { GROWATT_3110.data.u8[7] |= 0b00000001; } - if ((datalayer.battery.status.max_charge_current_dA == 0) || - (datalayer.battery.status.reported_soc == 10000) || (datalayer.battery.status.bms_status == FAULT)) { + if ((datalayer.battery.status.max_charge_current_dA == 0) || (datalayer.battery.status.reported_soc == 10000) || + (datalayer.battery.status.bms_status == FAULT)) { GROWATT_3110.data.u8[7] |= 0b01000000; // No Charge } else { GROWATT_3110.data.u8[7] |= 0b00000000; // Charge allowed @@ -362,7 +361,7 @@ void GrowattHvInverter::update_values() { GROWATT_3F00.data.u8[5] = 0; // RESERVED GROWATT_3F00.data.u8[6] = 0; // RESERVED GROWATT_3F00.data.u8[7] = 0; // RESERVED -} +} void GrowattHvInverter::map_can_frame_to_variable(CAN_frame rx_frame) { switch (rx_frame.ID) { @@ -453,4 +452,4 @@ void GrowattHvInverter::transmit_can(unsigned long currentMillis) { can_message_batch_index = 0; } } -} \ No newline at end of file +} From 291795b4fc491cedd8dd92b90b80cb407f321203 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:09:11 +0100 Subject: [PATCH 18/26] Update GROWATT-HV-CAN.cpp with new version --- Software/src/inverter/GROWATT-HV-CAN.cpp | 338 +---------------------- 1 file changed, 6 insertions(+), 332 deletions(-) diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp index f30e93ab4..ac4559412 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -27,7 +27,8 @@ void GrowattHvInverter::update_values() { ampere_hours_remaining = ((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) - ampere_hours_full = ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); + ampere_hours_full = + ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) } @@ -51,7 +52,7 @@ void GrowattHvInverter::update_values() { GROWATT_3110.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); // Status bits (see documentation for all bits, most important are mapped) - GROWATT_3110.data.u8[7] = 0x00; // Clear all bits + GROWATT_3110.data.u8[7] = 0x00; // Clear all bits if (datalayer.battery.status.active_power_W < -1) { // Discharging GROWATT_3110.data.u8[7] |= 0b00000011; } else if (datalayer.battery.status.active_power_W > 1) { // Charging @@ -60,8 +61,8 @@ void GrowattHvInverter::update_values() { GROWATT_3110.data.u8[7] |= 0b00000001; } - if ((datalayer.battery.status.max_charge_current_dA == 0) || (datalayer.battery.status.reported_soc == 10000) || - (datalayer.battery.status.bms_status == FAULT)) { + if ((datalayer.battery.status.max_charge_current_dA == 0) || + (datalayer.battery.status.reported_soc == 10000) || (datalayer.battery.status.bms_status == FAULT)) { GROWATT_3110.data.u8[7] |= 0b01000000; // No Charge } else { GROWATT_3110.data.u8[7] |= 0b00000000; // Charge allowed @@ -125,331 +126,4 @@ void GrowattHvInverter::update_values() { } else { // Use battery min design voltage as Discharge cutoff voltage (0.1V) [0-1000V] GROWATT_3150.data.u8[0] = (datalayer.battery.info.min_design_voltage_dV >> 8); - GROWATT_3150.data.u8[1] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF); - } - - // Main control unit temperature (0.1C) [-40 to 120*C] - GROWATT_3150.data.u8[2] = (datalayer.battery.status.temperature_max_dC >> 8); - GROWATT_3150.data.u8[3] = (datalayer.battery.status.temperature_max_dC & 0x00FF); - // Total number of cells - GROWATT_3150.data.u8[4] = (TOTAL_NUMBER_OF_CELLS >> 8); - GROWATT_3150.data.u8[5] = (TOTAL_NUMBER_OF_CELLS & 0x00FF); - // Number of modules in series - GROWATT_3150.data.u8[6] = (NUMBER_OF_MODULES_IN_SERIES >> 8); - GROWATT_3150.data.u8[7] = (NUMBER_OF_MODULES_IN_SERIES & 0x00FF); - - // Battery fault and voltage number information - // Fault flag bit - GROWATT_3160.data.u8[0] = 0; // TODO: Map according to documentation - // Fault extension flag bit - GROWATT_3160.data.u8[1] = 0; // TODO: Map according to documentation - // Number of module with the maximum cell voltage (1-32) - GROWATT_3160.data.u8[2] = 1; - // Number of cell with the maximum cell voltage (1-128) - GROWATT_3160.data.u8[3] = 1; - // Number of module with the minimum cell voltage (1-32) - GROWATT_3160.data.u8[4] = 1; - // Number of cell with the minimum cell voltage (1-128) - GROWATT_3160.data.u8[5] = 2; - // Minimum cell temperature (0.1C) [-40 to 120*C] - GROWATT_3160.data.u8[6] = (datalayer.battery.status.temperature_min_dC >> 8); - GROWATT_3160.data.u8[7] = (datalayer.battery.status.temperature_min_dC & 0x00FF); - - // Software version and temperature number information - // Number of Module with the maximum cell temperature (1-32) - GROWATT_3170.data.u8[0] = 1; - // Number of cell with the maximum cell temperature (1-128) - GROWATT_3170.data.u8[1] = 1; - // Number of module with the minimum cell temperature (1-32) - GROWATT_3170.data.u8[2] = 1; - // Number of cell with the minimum cell temperature (1-128) - GROWATT_3170.data.u8[3] = 2; - // Battery actual capacity (0-100) TODO, what is unit? - GROWATT_3170.data.u8[4] = 50; - // Battery correction status display value (0-255) - GROWATT_3170.data.u8[5] = 0; - // Remaining balancing time (0-255) - GROWATT_3170.data.u8[6] = 0; - // Balancing state, bit0-3(range 0-15) , Internal short circuit state, bit4-7 (range 0-15) - GROWATT_3170.data.u8[7] = 0; - - // Battery Code and quantity information - // Manufacturer code - GROWATT_3180.data.u8[0] = MANUFACTURER_ASCII_0; - GROWATT_3180.data.u8[1] = MANUFACTURER_ASCII_1; - // Number of Packs in parallel (1-65536) - GROWATT_3180.data.u8[2] = (NUMBER_OF_PACKS_IN_PARALLEL >> 8); - GROWATT_3180.data.u8[3] = (NUMBER_OF_PACKS_IN_PARALLEL & 0x00FF); - // Total number of cells (1-65536) - GROWATT_3180.data.u8[4] = (TOTAL_NUMBER_OF_CELLS >> 8); - GROWATT_3180.data.u8[5] = (TOTAL_NUMBER_OF_CELLS & 0x00FF); - // Pack number + BIC forward/reverse encoding number - // Bits 0-3: Pack number - // Bits 4-9: Max. number of BIC in forward BIC encoding in daisy-chain communication - // Bits 10-15: Max. number of BIC in reverse BIC encoding in daisy-chain communication - GROWATT_3180.data.u8[6] = 0; // TODO, this OK? - GROWATT_3180.data.u8[7] = 0; // TODO, this OK? - - // Cell voltage and status information - // Battery status - GROWATT_3190.data.u8[0] = 0; // LFP, no forced charge - // Maximum cell voltage (mV) - GROWATT_3190.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8); - GROWATT_3190.data.u8[2] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF); - // Min cell voltage (mV) - GROWATT_3190.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV >> 8); - GROWATT_3190.data.u8[4] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF); - // Reserved - GROWATT_3190.data.u8[5] = 0; - // Faulty battery pack number (1-16) - GROWATT_3190.data.u8[6] = 0; - // Faulty battery module number (1-16) - GROWATT_3190.data.u8[7] = 0; - - // Manufacturer name and version information - // Manufacturer name (ASCII) Battery manufacturer abbreviation in capital letters - GROWATT_3200.data.u8[0] = MANUFACTURER_ASCII_0; - GROWATT_3200.data.u8[1] = MANUFACTURER_ASCII_1; - // Hardware revision (0 null, 1 verA, 2verB) - GROWATT_3200.data.u8[2] = 0x01; - // Reserved - GROWATT_3200.data.u8[3] = 0; // Reserved - // Circulating current value (0.1A), Range 0-20A - GROWATT_3200.data.u8[4] = 0; - GROWATT_3200.data.u8[5] = 0; - // Cell charge cutoff voltage - GROWATT_3200.data.u8[6] = (datalayer.battery.info.max_cell_voltage_mV >> 8); - GROWATT_3200.data.u8[7] = (datalayer.battery.info.max_cell_voltage_mV & 0x00FF); - - // Upgrade information - // Message 0x3210 is update status. All blank is OK - // GROWATT_3210.data.u8[0] = 0; - - // De-rating and fault information (reserved) - // Power reduction sign - GROWATT_3220.data.u8[0] = 0; // Bits set to high incase we need to derate - GROWATT_3220.data.u8[1] = 0; // Bits set to high incase we need to derate - // System fault status - GROWATT_3220.data.u8[2] = 0; // All normal - GROWATT_3220.data.u8[3] = 0; // All normal - // Forced discharge mark - GROWATT_3220.data.u8[4] = 0; // When you want to force charge battery, send 0xAA here - // Battery rated energy information (Unit 0.1 kWh ) 30kWh = 300 , so 30000Wh needs to be div by 100 - GROWATT_3220.data.u8[5] = ((datalayer.battery.info.total_capacity_Wh / 100) >> 8); - GROWATT_3220.data.u8[6] = ((datalayer.battery.info.total_capacity_Wh / 100) & 0x00FF); - // Software subversion number - GROWATT_3220.data.u8[7] = 0; - - // Serial number - // Frame number - GROWATT_3230.data.u8[0] = serial_number_counter; - // Serial number content - // The serial number includes the PACK number (1byte: range [1, 11]) and serial number (16bytes). - // (Reserved and filled with 0x00) Explanation: Byte 1 (Battery ID) = 0:Invalid. - // When Byte 1 (Battery ID) = 1, it represents the SN (Serial Number) of the - // high-voltage controller. When BYTE1 (Battery ID) = 2~11, it represents the SN of PACK 1~10. - switch (serial_number_counter) { - case 0: - GROWATT_3230.data.u8[1] = 0; // BATTERY ID - GROWATT_3230.data.u8[2] = 0; // SN0 //TODO, is this needed? - GROWATT_3230.data.u8[3] = 0; // SN1 - GROWATT_3230.data.u8[4] = 0; // SN2 - GROWATT_3230.data.u8[5] = 0; // SN3 - GROWATT_3230.data.u8[6] = 0; // SN4 - GROWATT_3230.data.u8[7] = 0; // SN5 - break; - case 1: - GROWATT_3230.data.u8[1] = 0; // SN6 - GROWATT_3230.data.u8[2] = 0; // SN7 - GROWATT_3230.data.u8[3] = 0; // SN8 - GROWATT_3230.data.u8[4] = 0; // SN9 - GROWATT_3230.data.u8[5] = 0; // SN10 - GROWATT_3230.data.u8[6] = 0; // SN11 - GROWATT_3230.data.u8[7] = 0; // SN12 - break; - case 2: - GROWATT_3230.data.u8[1] = 0; // SN13 - GROWATT_3230.data.u8[2] = 0; // SN14 - GROWATT_3230.data.u8[3] = 0; // SN15 - GROWATT_3230.data.u8[4] = 0; // RESERVED - GROWATT_3230.data.u8[5] = 0; // RESERVED - GROWATT_3230.data.u8[6] = 0; // RESERVED - GROWATT_3230.data.u8[7] = 0; // RESERVED - break; - default: - break; - } - serial_number_counter = (serial_number_counter + 1) % 3; // cycles between 0-1-2-0-1... - - // Total charge/discharge energy - // Pack number (1-16) - GROWATT_3240.data.u8[0] = 1; - // Total lifetime discharge energy Unit: 0.1kWh Range [0.0~10000000.0kWh] - GROWATT_3240.data.u8[1] = 0; - GROWATT_3240.data.u8[2] = 0; - GROWATT_3240.data.u8[3] = 0; - // Pack number (1-16) - GROWATT_3240.data.u8[4] = 1; - // Total lifetime charge energy Unit: 0.1kWh Range [0.0~10000000.0kWh] - GROWATT_3240.data.u8[5] = 0; - GROWATT_3240.data.u8[6] = 0; - GROWATT_3240.data.u8[7] = 0; - - // Fault history - // Not applicable for our use. All values at 0 indicates no fault - GROWATT_3250.data.u8[0] = 0; - GROWATT_3250.data.u8[1] = 0; - GROWATT_3250.data.u8[2] = 0; - GROWATT_3250.data.u8[3] = 0; - GROWATT_3250.data.u8[4] = 0; - GROWATT_3250.data.u8[5] = 0; - GROWATT_3250.data.u8[6] = 0; - GROWATT_3250.data.u8[7] = 0; - - // Battery internal debugging fault message - // Not applicable for our use. All values at 0 indicates no fault - GROWATT_3260.data.u8[0] = 0; - GROWATT_3260.data.u8[1] = 0; - GROWATT_3260.data.u8[2] = 0; - GROWATT_3260.data.u8[3] = 0; - GROWATT_3260.data.u8[4] = 0; - GROWATT_3260.data.u8[5] = 0; - GROWATT_3260.data.u8[6] = 0; - GROWATT_3260.data.u8[7] = 0; - - // Battery internal debugging fault message - // Not applicable for our use. All values at 0 indicates no fault - GROWATT_3270.data.u8[0] = 0; - GROWATT_3270.data.u8[1] = 0; - GROWATT_3270.data.u8[2] = 0; - GROWATT_3270.data.u8[3] = 0; - GROWATT_3270.data.u8[4] = 0; - GROWATT_3270.data.u8[5] = 0; - GROWATT_3270.data.u8[6] = 0; - GROWATT_3270.data.u8[7] = 0; - - // Product Version Information - GROWATT_3280.data.u8[0] = 0; // Reserved - // Product version number (1-5) - GROWATT_3280.data.u8[1] = 0; // No software version (1 indicates PRODUCT, 2 indicates COMMUNICATION version) - // For example, the version code for the main control unit (product) of a high-voltage battery is QBAA, - // and the code for monitoring (communication) is ZEAA - // TODO, is this needed? - GROWATT_3280.data.u8[2] = 0; // ASCII - GROWATT_3280.data.u8[3] = 0; // ASCII - GROWATT_3280.data.u8[4] = 0; // ASCII - GROWATT_3280.data.u8[5] = 0; // ASCII - GROWATT_3280.data.u8[6] = 0; // Software version information - - // Battery series information - GROWATT_3290.data.u8[0] = 0; // DTC, range [0,65536], Default 12041 (TODO, shall we send that?) - GROWATT_3290.data.u8[1] = 0; // RESERVED - GROWATT_3290.data.u8[2] = 0; // RESERVED - GROWATT_3290.data.u8[3] = 0; // RESERVED - GROWATT_3290.data.u8[4] = 0; // RESERVED - GROWATT_3290.data.u8[5] = 0; // RESERVED - GROWATT_3290.data.u8[6] = 0; // RESERVED - GROWATT_3290.data.u8[7] = 0; // RESERVED - - // Internal alarm information - // Battery internal debugging fault message - GROWATT_3F00.data.u8[0] = 0; // RESERVED - GROWATT_3F00.data.u8[1] = 0; // RESERVED - GROWATT_3F00.data.u8[2] = 0; // RESERVED - GROWATT_3F00.data.u8[3] = 0; // RESERVED - GROWATT_3F00.data.u8[4] = 0; // RESERVED - GROWATT_3F00.data.u8[5] = 0; // RESERVED - GROWATT_3F00.data.u8[6] = 0; // RESERVED - GROWATT_3F00.data.u8[7] = 0; // RESERVED -} - -void GrowattHvInverter::map_can_frame_to_variable(CAN_frame rx_frame) { - switch (rx_frame.ID) { - case 0x3010: // Heartbeat command, 1000ms - datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; - inverter_alive = true; - send_times = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]); - safety_specification = rx_frame.data.u8[2]; - break; - case 0x3020: // Control command, 1000ms - datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; - inverter_alive = true; - charging_command = rx_frame.data.u8[0]; - discharging_command = rx_frame.data.u8[1]; - shielding_external_communication_failure = rx_frame.data.u8[2]; - clearing_battery_fault = rx_frame.data.u8[3]; - ISO_detection_command = rx_frame.data.u8[4]; - sleep_wakeup_control = rx_frame.data.u8[7]; - break; - case 0x3030: // Time command, 1000ms - datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; - inverter_alive = true; - unix_time = ((rx_frame.data.u8[0] << 24) | (rx_frame.data.u8[1] << 16) | (rx_frame.data.u8[2] << 8) | - rx_frame.data.u8[3]); - PCS_working_status = rx_frame.data.u8[7]; - break; - default: - break; - } -} - -void GrowattHvInverter::transmit_can(unsigned long currentMillis) { - if (!inverter_alive) { - return; // Don't send messages towards inverter until it has started - } - - // Check if 1 second has passed, then we start sending! - if (currentMillis - previousMillis1s >= INTERVAL_1_S) { - previousMillis1s = currentMillis; - time_to_send_1s_data = true; - } - - // Check if enough time has passed since the last batch - if (currentMillis - previousMillisBatchSend >= delay_between_batches_ms) { - previousMillisBatchSend = currentMillis; // Update the time of the last message batch - - // Send a subset of messages per iteration to avoid overloading the CAN bus / transmit buffer - switch (can_message_batch_index) { - case 0: - transmit_can_frame(&GROWATT_3110); - transmit_can_frame(&GROWATT_3120); - transmit_can_frame(&GROWATT_3130); - transmit_can_frame(&GROWATT_3140); - break; - case 1: - transmit_can_frame(&GROWATT_3150); - transmit_can_frame(&GROWATT_3160); - transmit_can_frame(&GROWATT_3170); - transmit_can_frame(&GROWATT_3180); - break; - case 2: - transmit_can_frame(&GROWATT_3190); - transmit_can_frame(&GROWATT_3200); - transmit_can_frame(&GROWATT_3210); - transmit_can_frame(&GROWATT_3220); - break; - case 3: - transmit_can_frame(&GROWATT_3230); - transmit_can_frame(&GROWATT_3240); - transmit_can_frame(&GROWATT_3250); - transmit_can_frame(&GROWATT_3260); - break; - case 4: - transmit_can_frame(&GROWATT_3270); - transmit_can_frame(&GROWATT_3280); - transmit_can_frame(&GROWATT_3290); - transmit_can_frame(&GROWATT_3F00); - time_to_send_1s_data = false; - break; - default: - break; - } - - // Increment message index and wrap around if needed - can_message_batch_index++; - - if (time_to_send_1s_data == false) { - can_message_batch_index = 0; - } - } -} + GROUND_ATTENTION_ERROR \ No newline at end of file From 3c15990b03e8c9efd3356635db0f22f1ee8e0427 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:09:18 +0000 Subject: [PATCH 19/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Software/src/inverter/GROWATT-HV-CAN.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp index ac4559412..ac3f68c48 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -27,8 +27,7 @@ void GrowattHvInverter::update_values() { ampere_hours_remaining = ((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) - ampere_hours_full = - ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); + ampere_hours_full = ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) } @@ -52,7 +51,7 @@ void GrowattHvInverter::update_values() { GROWATT_3110.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); // Status bits (see documentation for all bits, most important are mapped) - GROWATT_3110.data.u8[7] = 0x00; // Clear all bits + GROWATT_3110.data.u8[7] = 0x00; // Clear all bits if (datalayer.battery.status.active_power_W < -1) { // Discharging GROWATT_3110.data.u8[7] |= 0b00000011; } else if (datalayer.battery.status.active_power_W > 1) { // Charging @@ -61,8 +60,8 @@ void GrowattHvInverter::update_values() { GROWATT_3110.data.u8[7] |= 0b00000001; } - if ((datalayer.battery.status.max_charge_current_dA == 0) || - (datalayer.battery.status.reported_soc == 10000) || (datalayer.battery.status.bms_status == FAULT)) { + if ((datalayer.battery.status.max_charge_current_dA == 0) || (datalayer.battery.status.reported_soc == 10000) || + (datalayer.battery.status.bms_status == FAULT)) { GROWATT_3110.data.u8[7] |= 0b01000000; // No Charge } else { GROWATT_3110.data.u8[7] |= 0b00000000; // Charge allowed @@ -126,4 +125,4 @@ void GrowattHvInverter::update_values() { } else { // Use battery min design voltage as Discharge cutoff voltage (0.1V) [0-1000V] GROWATT_3150.data.u8[0] = (datalayer.battery.info.min_design_voltage_dV >> 8); - GROUND_ATTENTION_ERROR \ No newline at end of file + GROUND_ATTENTION_ERROR From 8a9f6144b61fa0c3caee00939a00ff3c6a9827b5 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:18:41 +0100 Subject: [PATCH 20/26] Remove GROUND_ATTENTION_ERROR from GROWATT-HV-CAN.cpp --- Software/src/inverter/GROWATT-HV-CAN.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp index ac3f68c48..72d4badad 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -125,4 +125,4 @@ void GrowattHvInverter::update_values() { } else { // Use battery min design voltage as Discharge cutoff voltage (0.1V) [0-1000V] GROWATT_3150.data.u8[0] = (datalayer.battery.info.min_design_voltage_dV >> 8); - GROUND_ATTENTION_ERROR + From 823325e01c6a95491bf8341a6429ecc8bb4a8dd1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:18:48 +0000 Subject: [PATCH 21/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Software/src/inverter/GROWATT-HV-CAN.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp index 72d4badad..54992eb96 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -125,4 +125,3 @@ void GrowattHvInverter::update_values() { } else { // Use battery min design voltage as Discharge cutoff voltage (0.1V) [0-1000V] GROWATT_3150.data.u8[0] = (datalayer.battery.info.min_design_voltage_dV >> 8); - From a823f8f50fb24ba5efa53d0591a8303060ee2be9 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:21:46 +0100 Subject: [PATCH 22/26] Delete Software/src/inverter/GROWATT-HV-CAN.cpp --- Software/src/inverter/GROWATT-HV-CAN.cpp | 127 ----------------------- 1 file changed, 127 deletions(-) delete mode 100644 Software/src/inverter/GROWATT-HV-CAN.cpp diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp deleted file mode 100644 index 54992eb96..000000000 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ /dev/null @@ -1,127 +0,0 @@ -#include "GROWATT-HV-CAN.h" -#include "../communication/can/comm_can.h" -#include "../datalayer/datalayer.h" - -/* TODO: - This protocol has not been tested with any inverter. Proceed with extreme caution. - Search the file for "TODO" to see all the places that might require work - - Growatt BMS CAN-Bus-protocol High Voltage V1.10 2023-11-06 - 29-bit identifier - 500kBit/sec - Big-endian - - Terms and abbreviations: - PCS - Power conversion system (the Storage Inverter) - Cell - A single battery cell - Module - A battery module composed of 16 strings of cells - Pack - A battery pack composed of the BMS and battery modules connected in parallel and series, which can work independently - FCC - Full charge capacity - RM - Remaining capacity - BMS - Battery Information Collector */ - -void GrowattHvInverter::update_values() { - // This function maps all the values fetched from battery CAN to the correct CAN messages - - if (datalayer.battery.status.voltage_dV > 10) { // Only update value when we have voltage available to avoid div0 - ampere_hours_remaining = - ((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); - // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) - ampere_hours_full = ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * 100); - // (WH[10000] * V+1[3600])*100 = 270 (27.0Ah) - } - - // Map values to CAN messages - // Battery operating parameters and status information - if (datalayer.battery.settings.user_set_voltage_limits_active) { // If user is requesting a specific voltage - // User specified charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V) - GROWATT_3110.data.u8[0] = (datalayer.battery.settings.max_user_set_charge_voltage_dV >> 8); - GROWATT_3110.data.u8[1] = (datalayer.battery.settings.max_user_set_charge_voltage_dV & 0x00FF); - } else { - // Battery max voltage used as charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V) - GROWATT_3110.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV >> 8); - GROWATT_3110.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF); - } - - // Charge limited current, 125 =12.5A (0.1, A) (Min 0, Max 300A) - GROWATT_3110.data.u8[2] = (datalayer.battery.status.max_charge_current_dA >> 8); - GROWATT_3110.data.u8[3] = (datalayer.battery.status.max_charge_current_dA & 0x00FF); - // Discharge limited current, 500 = 50A, (0.1, A) - GROWATT_3110.data.u8[4] = (datalayer.battery.status.max_discharge_current_dA >> 8); - GROWATT_3110.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); - - // Status bits (see documentation for all bits, most important are mapped) - GROWATT_3110.data.u8[7] = 0x00; // Clear all bits - if (datalayer.battery.status.active_power_W < -1) { // Discharging - GROWATT_3110.data.u8[7] |= 0b00000011; - } else if (datalayer.battery.status.active_power_W > 1) { // Charging - GROWATT_3110.data.u8[7] |= 0b00000010; - } else { // Idle - GROWATT_3110.data.u8[7] |= 0b00000001; - } - - if ((datalayer.battery.status.max_charge_current_dA == 0) || (datalayer.battery.status.reported_soc == 10000) || - (datalayer.battery.status.bms_status == FAULT)) { - GROWATT_3110.data.u8[7] |= 0b01000000; // No Charge - } else { - GROWATT_3110.data.u8[7] |= 0b00000000; // Charge allowed - } - - if ((datalayer.battery.status.max_discharge_current_dA == 0) || (datalayer.battery.status.reported_soc == 0) || - (datalayer.battery.status.bms_status == FAULT)) { - GROWATT_3110.data.u8[7] |= 0b00100000; // No Discharge - } else { - GROWATT_3110.data.u8[7] |= 0b00000000; // Discharge allowed - } - - GROWATT_3110.data.u8[6] |= 0b00100000; // ISO Detection status: Detected - GROWATT_3110.data.u8[6] |= 0b00010000; // Battery status: Normal - - // Battery protection and alarm information - // Fault and warning status bits. TODO, map these according to documentation. - // GROWATT_3120.data.u8[0] = - // GROWATT_3120.data.u8[1] = - // GROWATT_3120.data.u8[2] = - // GROWATT_3120.data.u8[3] = - // GROWATT_3120.data.u8[4] = - // GROWATT_3120.data.u8[5] = - // GROWATT_3120.data.u8[6] = - // GROWATT_3120.data.u8[7] = - - // Battery operation information - // Voltage of the pack (0.1V) [0-1000V] - GROWATT_3130.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8); - GROWATT_3130.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF); - // Total current (0.1A -300 to 300A) - GROWATT_3130.data.u8[2] = (datalayer.battery.status.reported_current_dA >> 8); - GROWATT_3130.data.u8[3] = (datalayer.battery.status.reported_current_dA & 0x00FF); - // Cell max temperature (0.1C) [-40 to 120*C] - GROWATT_3130.data.u8[4] = (datalayer.battery.status.temperature_max_dC >> 8); - GROWATT_3130.data.u8[5] = (datalayer.battery.status.temperature_max_dC & 0x00FF); - // SOC (%) [0-100] - GROWATT_3130.data.u8[6] = (datalayer.battery.status.reported_soc / 100); - // SOH (%) (Bit 0~ Bit6 SOH Counters) Bit7 SOH flag (Indicates that battery is in unsafe use) - GROWATT_3130.data.u8[7] = (datalayer.battery.status.soh_pptt / 100); - - // Battery capacity information - // Remaining capacity (10 mAh) [0.0 ~ 500000.0 mAH] - GROWATT_3140.data.u8[0] = ((ampere_hours_remaining * 100) >> 8); - GROWATT_3140.data.u8[1] = ((ampere_hours_remaining * 100) & 0x00FF); - // Fully charged capacity (10 mAh) [0.0 ~ 500000.0 mAH] - GROWATT_3140.data.u8[2] = ((ampere_hours_full * 100) >> 8); - GROWATT_3140.data.u8[3] = ((ampere_hours_full * 100) & 0x00FF); - // Manufacturer code - GROWATT_3140.data.u8[4] = MANUFACTURER_ASCII_0; - GROWATT_3140.data.u8[5] = MANUFACTURER_ASCII_1; - // Cycle count (h) - GROWATT_3140.data.u8[6] = 0; - GROWATT_3140.data.u8[7] = 0; - - // Battery working parameters and module number information - if (datalayer.battery.settings.user_set_voltage_limits_active) { // If user is requesting a specific voltage - // Use user specified voltage as Discharge cutoff voltage (0.1V) [0-1000V] - GROWATT_3150.data.u8[0] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV >> 8); - GROWATT_3150.data.u8[1] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV & 0x00FF); - } else { - // Use battery min design voltage as Discharge cutoff voltage (0.1V) [0-1000V] - GROWATT_3150.data.u8[0] = (datalayer.battery.info.min_design_voltage_dV >> 8); From d440676e3fd8fce7adc60259925273f903ed7cd4 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:22:05 +0100 Subject: [PATCH 23/26] Add files via upload --- Software/src/inverter/GROWATT-HV-CAN.cpp | 482 +++++++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 Software/src/inverter/GROWATT-HV-CAN.cpp diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp new file mode 100644 index 000000000..7d92e1bf0 --- /dev/null +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -0,0 +1,482 @@ +#include "GROWATT-HV-CAN.h" +#include "../communication/can/comm_can.h" +#include "../datalayer/datalayer.h" + +/* TODO: +This protocol has not been tested with any inverter. Proceed with extreme caution. +Search the file for "TODO" to see all the places that might require work + +Growatt BMS CAN-Bus-protocol High Voltage V1.10 2023-11-06 +29-bit identifier +500kBit/sec +Big-endian + +Terms and abbreviations: +PCS - Power conversion system (the Storage Inverter) +Cell - A single battery cell +Module - A battery module composed of 16 strings of cells +Pack - A battery pack composed of the BMS and battery modules connected in parallel and series, which can work independently +FCC - Full charge capacity +RM - Remaining capacity +BMS - Battery Information Collector */ + +void GrowattHvInverter:: + update_values() { //This function maps all the values fetched from battery CAN to the correct CAN messages + + // ---- Cell/module topology (Growatt frames 0x3150 and 0x3180) ---- + // Previous revisions hard-coded TOTAL_NUMBER_OF_CELLS=300 and NUMBER_OF_MODULES_IN_SERIES=20. + // EMUS provides the actual series cell count in datalayer.battery.info.number_of_cells (e.g. 119). + // Use that when it is sane. Keep the hard-coded values as a fallback for integrations that do not populate + // number_of_cells (e.g. some Pylon paths set it to a placeholder value). + uint16_t total_cells = TOTAL_NUMBER_OF_CELLS; + if (datalayer.battery.info.number_of_cells >= 10 && datalayer.battery.info.number_of_cells <= 512) { + total_cells = (uint16_t)datalayer.battery.info.number_of_cells; + } + // If we are using a dynamic cell count, a safe default for "modules in series" is 1 unless your integration + // provides a more accurate module topology. + uint16_t modules_in_series = NUMBER_OF_MODULES_IN_SERIES; + if (total_cells != TOTAL_NUMBER_OF_CELLS) { + modules_in_series = 1; + } + + if (datalayer.battery.status.voltage_dV > 10) { // Only update value when we have voltage available to avoid div0 + // 0x3140 expects capacity in 10mAh units. + // capacity_10mAh = Wh * 1000 / dV (because V = dV/10, and 10mAh units = Ah*100) + const uint16_t v_dV = datalayer.battery.status.voltage_dV; + + uint32_t full_10mAh = + (uint32_t)((uint64_t)datalayer.battery.info.total_capacity_Wh * 1000ULL / v_dV); + + uint32_t rem_10mAh = 0; + if (datalayer.battery.status.remaining_capacity_Wh > 0) { + rem_10mAh = (uint32_t)((uint64_t)datalayer.battery.status.remaining_capacity_Wh * 1000ULL / v_dV); + } else { + // Fallback: derive remaining capacity from SOC if remaining Wh is not available. + const uint32_t soc_pct = (uint32_t)(datalayer.battery.status.reported_soc / 100); // 0..100 + rem_10mAh = (uint32_t)((uint64_t)full_10mAh * soc_pct / 100ULL); + } + + if (full_10mAh > 0xFFFF) full_10mAh = 0xFFFF; + if (rem_10mAh > 0xFFFF) rem_10mAh = 0xFFFF; + + capacity_full_10mAh = (uint16_t)full_10mAh; + capacity_remaining_10mAh = (uint16_t)rem_10mAh; + } + + //Map values to CAN messages + //Battery operating parameters and status information + if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage + //User specified charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V) + GROWATT_3110.data.u8[0] = (datalayer.battery.settings.max_user_set_charge_voltage_dV >> 8); + GROWATT_3110.data.u8[1] = (datalayer.battery.settings.max_user_set_charge_voltage_dV & 0x00FF); + } else { + //Battery max voltage used as charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 0, MAX 1000V) + GROWATT_3110.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV >> 8); + GROWATT_3110.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF); + } + //Charge limited current, 125 =12.5A (0.1, A) (Min 0, Max 300A) + GROWATT_3110.data.u8[2] = (datalayer.battery.status.max_charge_current_dA >> 8); + GROWATT_3110.data.u8[3] = (datalayer.battery.status.max_charge_current_dA & 0x00FF); + //Discharge limited current, 500 = 50A, (0.1, A) + GROWATT_3110.data.u8[4] = (datalayer.battery.status.max_discharge_current_dA >> 8); + GROWATT_3110.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); + //Status bits (see documentation for all bits, most important are mapped + GROWATT_3110.data.u8[7] = 0x00; // Clear all bits + if (datalayer.battery.status.active_power_W < -1) { // Discharging + GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000011); + } else if (datalayer.battery.status.active_power_W > 1) { // Charging + GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000010); + } else { //Idle + GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000001); + } + if ((datalayer.battery.status.max_charge_current_dA == 0) || (datalayer.battery.status.reported_soc == 10000) || + (datalayer.battery.status.bms_status == FAULT)) { + GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b01000000); // No Charge + } else { //continue using battery + GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000000); // Charge allowed + } + if ((datalayer.battery.status.max_discharge_current_dA == 0) || (datalayer.battery.status.reported_soc == 0) || + (datalayer.battery.status.bms_status == FAULT)) { + GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00100000); // No Discharge + } else { //continue using battery + GROWATT_3110.data.u8[7] = (GROWATT_3110.data.u8[7] | 0b00000000); // Discharge allowed + } + GROWATT_3110.data.u8[6] = (GROWATT_3110.data.u8[6] | 0b00100000); // ISO Detection status: Detected + GROWATT_3110.data.u8[6] = (GROWATT_3110.data.u8[6] | 0b00010000); // Battery status: Normal + + //Battery protection and alarm information + //Fault and warning status bits. TODO, map these according to documentation. + //GROWATT_3120.data.u8[0] = + //GROWATT_3120.data.u8[1] = + //GROWATT_3120.data.u8[2] = + //GROWATT_3120.data.u8[3] = + //GROWATT_3120.data.u8[4] = + //GROWATT_3120.data.u8[5] = + //GROWATT_3120.data.u8[6] = + //GROWATT_3120.data.u8[7] = + + //Battery operation information + //Voltage of the pack (0.1V) [0-1000V] + GROWATT_3130.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8); + GROWATT_3130.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF); + //Total current (0.1A -300 to 300A) + GROWATT_3130.data.u8[2] = (datalayer.battery.status.reported_current_dA >> 8); + GROWATT_3130.data.u8[3] = (datalayer.battery.status.reported_current_dA & 0x00FF); + //Cell max temperature (0.1C) [-40 to 120*C] + GROWATT_3130.data.u8[4] = (datalayer.battery.status.temperature_max_dC >> 8); + GROWATT_3130.data.u8[5] = (datalayer.battery.status.temperature_max_dC & 0x00FF); + //SOC (%) [0-100] + GROWATT_3130.data.u8[6] = (datalayer.battery.status.reported_soc / 100); + //SOH (%) (Bit 0~ Bit6 SOH Counters) Bit7 SOH flag (Indicates that battery is in unsafe use) + GROWATT_3130.data.u8[7] = (datalayer.battery.status.soh_pptt / 100); + + //Battery capacity information + //Remaining capacity (10 mAh) [0.0 ~ 500000.0 mAH] + GROWATT_3140.data.u8[0] = (capacity_remaining_10mAh >> 8); + GROWATT_3140.data.u8[1] = (capacity_remaining_10mAh & 0x00FF); + //Fully charged capacity (10 mAh) [0.0 ~ 500000.0 mAH] + GROWATT_3140.data.u8[2] = (capacity_full_10mAh >> 8); + GROWATT_3140.data.u8[3] = (capacity_full_10mAh & 0x00FF); + //Manufacturer code + GROWATT_3140.data.u8[4] = MANUFACTURER_ASCII_0; + GROWATT_3140.data.u8[5] = MANUFACTURER_ASCII_1; + //Cycle count (h) + GROWATT_3140.data.u8[6] = 0; + GROWATT_3140.data.u8[7] = 0; + + //Battery working parameters and module number information + if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage + //Use user specified voltage as Discharge cutoff voltage (0.1V) [0-1000V] + GROWATT_3150.data.u8[0] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV >> 8); + GROWATT_3150.data.u8[1] = (datalayer.battery.settings.max_user_set_discharge_voltage_dV & 0x00FF); + } else { + //Use battery min design voltage as Discharge cutoff voltage (0.1V) [0-1000V] + GROWATT_3150.data.u8[0] = (datalayer.battery.info.min_design_voltage_dV >> 8); + GROWATT_3150.data.u8[1] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF); + } + //Main control unit temperature (0.1C) [-40 to 120*C] + GROWATT_3150.data.u8[2] = (datalayer.battery.status.temperature_max_dC >> 8); + GROWATT_3150.data.u8[3] = (datalayer.battery.status.temperature_max_dC & 0x00FF); + //Total number of cells + GROWATT_3150.data.u8[4] = (total_cells >> 8); + GROWATT_3150.data.u8[5] = (total_cells & 0x00FF); + //Number of modules in series + GROWATT_3150.data.u8[6] = (modules_in_series >> 8); + GROWATT_3150.data.u8[7] = (modules_in_series & 0x00FF); + + //Battery fault and voltage number information + //Fault flag bit + GROWATT_3160.data.u8[0] = 0; //TODO: Map according to documentation + //Fault extension flag bit + GROWATT_3160.data.u8[1] = 0; //TODO: Map according to documentation + //Number of module with the maximum cell voltage (1-32) + GROWATT_3160.data.u8[2] = 1; + //Number of cell with the maximum cell voltage (1-128) + GROWATT_3160.data.u8[3] = 1; + //Number of module with the minimum cell voltage (1-32) + GROWATT_3160.data.u8[4] = 1; + //Number of cell with the minimum cell voltage (1-128) + GROWATT_3160.data.u8[5] = 2; + //Minimum cell temperature (0.1C) [-40 to 120*C] + GROWATT_3160.data.u8[6] = (datalayer.battery.status.temperature_min_dC >> 8); + GROWATT_3160.data.u8[7] = (datalayer.battery.status.temperature_min_dC & 0x00FF); + + //Software version and temperature number information + //Number of Module with the maximum cell temperature (1-32) + GROWATT_3170.data.u8[0] = 1; + //Number of cell with the maximum cell temperature (1-128) + GROWATT_3170.data.u8[1] = 1; + //Number of module with the minimum cell temperature (1-32) + GROWATT_3170.data.u8[2] = 1; + //Number of cell with the minimum cell temperature (1-128) + GROWATT_3170.data.u8[3] = 2; + //Battery actial capacity (0-100) TODO, what is unit? + GROWATT_3170.data.u8[4] = 50; + //Battery correction status display value (0-255) + GROWATT_3170.data.u8[5] = 0; + // Remaining balancing time (0-255) + GROWATT_3170.data.u8[6] = 0; + //Balancing state, bit0-3(range 0-15) , Internal short circuit state, bit4-7 (range 0-15) + GROWATT_3170.data.u8[7] = 0; + + //Battery Code and quantity information + //Manufacturer code + GROWATT_3180.data.u8[0] = MANUFACTURER_ASCII_0; + GROWATT_3180.data.u8[1] = MANUFACTURER_ASCII_1; + //Number of Packs in parallel (1-65536) + GROWATT_3180.data.u8[2] = (NUMBER_OF_PACKS_IN_PARALLEL >> 8); + GROWATT_3180.data.u8[3] = (NUMBER_OF_PACKS_IN_PARALLEL & 0x00FF); + //Total number of cells (1-65536) + GROWATT_3180.data.u8[4] = (total_cells >> 8); + GROWATT_3180.data.u8[5] = (total_cells & 0x00FF); + //Pack number + BIC forward/reverse encoding number + // Bits 0-3: Pack number + // Bits 4-9: Max. number of BIC in forward BIC encoding in daisy- chain communication + // Bits 10-15: Max. number of BIC in reverse BIC encoding in daisy- chain communication + GROWATT_3180.data.u8[6] = 0; //TODO, this OK? + GROWATT_3180.data.u8[7] = 0; //TODO, this OK? + + //Cell voltage and status information + //Battery status + GROWATT_3190.data.u8[0] = 0; //LFP, no forced charge + //Maximum cell voltage (mV) + GROWATT_3190.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8); + GROWATT_3190.data.u8[2] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF); + // Min cell voltage (mV) + GROWATT_3190.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV >> 8); + GROWATT_3190.data.u8[4] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF); + //Reserved + GROWATT_3190.data.u8[5] = 0; + // Faulty battery pack number (1-16) + GROWATT_3190.data.u8[6] = 0; + //Faulty battery module number (1-16) + GROWATT_3190.data.u8[7] = 0; + + //Manufacturer name and version information + // Manufacturer name (ASCII) Battery manufacturer abbreviation in capital letters + GROWATT_3200.data.u8[0] = MANUFACTURER_ASCII_0; + GROWATT_3200.data.u8[1] = MANUFACTURER_ASCII_1; + // Hardware revision (0 null, 1 verA, 2verB) + GROWATT_3200.data.u8[2] = 0x01; + // Reserved + GROWATT_3200.data.u8[3] = 0; //Reserved + //Circulating current value (0.1A), Range 0-20A + GROWATT_3200.data.u8[4] = 0; + GROWATT_3200.data.u8[5] = 0; + //Cell charge cutoff voltage + GROWATT_3200.data.u8[6] = (datalayer.battery.info.max_cell_voltage_mV >> 8); + GROWATT_3200.data.u8[7] = (datalayer.battery.info.max_cell_voltage_mV & 0x00FF); + + //Upgrade information + //Message 0x3210 is update status. All blank is OK + //GROWATT_3210.data.u8[0] = 0; + + //De-rating and fault information (reserved) + //Power reduction sign + GROWATT_3220.data.u8[0] = 0; //Bits set to high incase we need to derate + GROWATT_3220.data.u8[1] = 0; //Bits set to high incase we need to derate + //System fault status + GROWATT_3220.data.u8[2] = 0; //All normal + GROWATT_3220.data.u8[3] = 0; //All normal + //Forced discharge mark + GROWATT_3220.data.u8[4] = 0; //When you want to force charge battery, send 0xAA here + //Battery rated energy information (Unit 0.1 kWh ) 30kWh = 300 , so 30000Wh needs to be div by 100 + GROWATT_3220.data.u8[5] = ((datalayer.battery.info.total_capacity_Wh / 100) >> 8); + GROWATT_3220.data.u8[6] = ((datalayer.battery.info.total_capacity_Wh / 100) & 0x00FF); + //Software subversion number + GROWATT_3220.data.u8[7] = 0; + + //Serial number + //Frame number + GROWATT_3230.data.u8[0] = serial_number_counter; + //Serial number content + //The serial number includes the PACK number (1byte: range [1, 11]) and serial number (16bytes). + //(Reserved and filled with 0x00) Explanation: Byte 1 (Battery ID) = 0:Invalid. + //When Byte 1 (Battery ID) = 1, it represents the SN (Serial Number) of the + //high-voltage controller. When BYTE1 (Battery ID) = 2~11, it represents the SN of PACK 1~10. + switch (serial_number_counter) { + case 0: + GROWATT_3230.data.u8[1] = 0; // BATTERY ID + GROWATT_3230.data.u8[2] = 0; // SN0 //TODO, is this needed? + GROWATT_3230.data.u8[3] = 0; // SN1 + GROWATT_3230.data.u8[4] = 0; // SN2 + GROWATT_3230.data.u8[5] = 0; // SN3 + GROWATT_3230.data.u8[6] = 0; // SN4 + GROWATT_3230.data.u8[7] = 0; // SN5 + break; + case 1: + GROWATT_3230.data.u8[1] = 0; // SN6 + GROWATT_3230.data.u8[2] = 0; // SN7 + GROWATT_3230.data.u8[3] = 0; // SN8 + GROWATT_3230.data.u8[4] = 0; // SN9 + GROWATT_3230.data.u8[5] = 0; // SN10 + GROWATT_3230.data.u8[6] = 0; // SN11 + GROWATT_3230.data.u8[7] = 0; // SN12 + break; + case 2: + GROWATT_3230.data.u8[1] = 0; // SN13 + GROWATT_3230.data.u8[2] = 0; // SN14 + GROWATT_3230.data.u8[3] = 0; // SN15 + GROWATT_3230.data.u8[4] = 0; // RESERVED + GROWATT_3230.data.u8[5] = 0; // RESERVED + GROWATT_3230.data.u8[6] = 0; // RESERVED + GROWATT_3230.data.u8[7] = 0; // RESERVED + break; + default: + break; + } + serial_number_counter = (serial_number_counter + 1) % 3; // cycles between 0-1-2-0-1... + + //Total charge/discharge energy + //Pack number (1-16) + GROWATT_3240.data.u8[0] = 1; + //Total lifetime discharge energy Unit: 0.1kWh Range [0.0~10000000.0kWh] + GROWATT_3240.data.u8[1] = 0; + GROWATT_3240.data.u8[2] = 0; + GROWATT_3240.data.u8[3] = 0; + //Pack number (1-16) + GROWATT_3240.data.u8[4] = 1; + //Total lifetime charge energy Unit: 0.1kWh Range [0.0~10000000.0kWh] + GROWATT_3240.data.u8[5] = 0; + GROWATT_3240.data.u8[6] = 0; + GROWATT_3240.data.u8[7] = 0; + + //Fault history + // Not applicable for our use. All values at 0 indicates no fault + GROWATT_3250.data.u8[0] = 0; + GROWATT_3250.data.u8[1] = 0; + GROWATT_3250.data.u8[2] = 0; + GROWATT_3250.data.u8[3] = 0; + GROWATT_3250.data.u8[4] = 0; + GROWATT_3250.data.u8[5] = 0; + GROWATT_3250.data.u8[6] = 0; + GROWATT_3250.data.u8[7] = 0; + + //Battery internal debugging fault message + // Not applicable for our use. All values at 0 indicates no fault + GROWATT_3260.data.u8[0] = 0; + GROWATT_3260.data.u8[1] = 0; + GROWATT_3260.data.u8[2] = 0; + GROWATT_3260.data.u8[3] = 0; + GROWATT_3260.data.u8[4] = 0; + GROWATT_3260.data.u8[5] = 0; + GROWATT_3260.data.u8[6] = 0; + GROWATT_3260.data.u8[7] = 0; + + //Battery internal debugging fault message + // Not applicable for our use. All values at 0 indicates no fault + GROWATT_3270.data.u8[0] = 0; + GROWATT_3270.data.u8[1] = 0; + GROWATT_3270.data.u8[2] = 0; + GROWATT_3270.data.u8[3] = 0; + GROWATT_3270.data.u8[4] = 0; + GROWATT_3270.data.u8[5] = 0; + GROWATT_3270.data.u8[6] = 0; + GROWATT_3270.data.u8[7] = 0; + + //Product Version Information + GROWATT_3280.data.u8[0] = 0; //Reserved + //Product version number (1-5) + GROWATT_3280.data.u8[1] = 0; //No software version (1 indicates PRODUCT, 2 indicates COMMUNICATION version) + //For example, the version code for the main control unit (product) of a high-voltage battery is QBAA, + // and the code formonitoring (communication)is ZEAA + //TODO, is this needed? + GROWATT_3280.data.u8[2] = 0; //ASCII + GROWATT_3280.data.u8[3] = 0; //ASCII + GROWATT_3280.data.u8[4] = 0; //ASCII + GROWATT_3280.data.u8[5] = 0; //ASCII + GROWATT_3280.data.u8[6] = 0; //Software version information + + //Battery series information + GROWATT_3290.data.u8[0] = 0; // DTC, range Range [0,65536], Default 12041 (TODO, shall we send that?) + GROWATT_3290.data.u8[1] = 0; // RESERVED + GROWATT_3290.data.u8[2] = 0; // RESERVED + GROWATT_3290.data.u8[3] = 0; // RESERVED + GROWATT_3290.data.u8[4] = 0; // RESERVED + GROWATT_3290.data.u8[5] = 0; // RESERVED + GROWATT_3290.data.u8[6] = 0; // RESERVED + GROWATT_3290.data.u8[7] = 0; // RESERVED + + //Internal alarm information + //Battery internal debugging fault message + GROWATT_3F00.data.u8[0] = 0; // RESERVED + GROWATT_3F00.data.u8[1] = 0; // RESERVED + GROWATT_3F00.data.u8[2] = 0; // RESERVED + GROWATT_3F00.data.u8[3] = 0; // RESERVED + GROWATT_3F00.data.u8[4] = 0; // RESERVED + GROWATT_3F00.data.u8[5] = 0; // RESERVED + GROWATT_3F00.data.u8[6] = 0; // RESERVED + GROWATT_3F00.data.u8[7] = 0; // RESERVED +} + +void GrowattHvInverter::map_can_frame_to_variable(CAN_frame rx_frame) { + switch (rx_frame.ID) { + case 0x3010: // Heartbeat command, 1000ms + datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; + inverter_alive = true; + send_times = ((rx_frame.data.u8[0] << 8) | rx_frame.data.u8[1]); + safety_specification = rx_frame.data.u8[2]; + break; + case 0x3020: // Control command, 1000ms + datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; + inverter_alive = true; + charging_command = rx_frame.data.u8[0]; + discharging_command = rx_frame.data.u8[1]; + shielding_external_communication_failure = rx_frame.data.u8[2]; + clearing_battery_fault = rx_frame.data.u8[3]; + ISO_detection_command = rx_frame.data.u8[4]; + sleep_wakeup_control = rx_frame.data.u8[7]; + break; + case 0x3030: // Time command, 1000ms + datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; + inverter_alive = true; + unix_time = ((rx_frame.data.u8[0] << 24) | (rx_frame.data.u8[1] << 16) | (rx_frame.data.u8[2] << 8) | + rx_frame.data.u8[3]); + PCS_working_status = rx_frame.data.u8[7]; + break; + default: + break; + } +} + +void GrowattHvInverter::transmit_can(unsigned long currentMillis) { + + if (!inverter_alive) { + return; //Dont send messages towards inverter until it has started + } + + //Check if 1 second has passed, then we start sending! + if (currentMillis - previousMillis1s >= INTERVAL_1_S) { + previousMillis1s = currentMillis; + time_to_send_1s_data = true; + } + + // Check if enough time has passed since the last batch + if (currentMillis - previousMillisBatchSend >= delay_between_batches_ms) { + previousMillisBatchSend = currentMillis; // Update the time of the last message batch + + // Send a subset of messages per iteration to avoid overloading the CAN bus / transmit buffer + switch (can_message_batch_index) { + case 0: + transmit_can_frame(&GROWATT_3110); + transmit_can_frame(&GROWATT_3120); + transmit_can_frame(&GROWATT_3130); + transmit_can_frame(&GROWATT_3140); + break; + case 1: + transmit_can_frame(&GROWATT_3150); + transmit_can_frame(&GROWATT_3160); + transmit_can_frame(&GROWATT_3170); + transmit_can_frame(&GROWATT_3180); + break; + case 2: + transmit_can_frame(&GROWATT_3190); + transmit_can_frame(&GROWATT_3200); + transmit_can_frame(&GROWATT_3210); + transmit_can_frame(&GROWATT_3220); + break; + case 3: + transmit_can_frame(&GROWATT_3230); + transmit_can_frame(&GROWATT_3240); + transmit_can_frame(&GROWATT_3250); + transmit_can_frame(&GROWATT_3260); + break; + case 4: + transmit_can_frame(&GROWATT_3270); + transmit_can_frame(&GROWATT_3280); + transmit_can_frame(&GROWATT_3290); + transmit_can_frame(&GROWATT_3F00); + time_to_send_1s_data = false; + break; + default: + break; + } + + // Increment message index and wrap around if needed + can_message_batch_index++; + + if (time_to_send_1s_data == false) { + can_message_batch_index = 0; + } + } +} From 499c4b21956f19a1517a03f388b7945ab97a907a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:22:12 +0000 Subject: [PATCH 24/26] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Software/src/inverter/GROWATT-HV-CAN.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Software/src/inverter/GROWATT-HV-CAN.cpp b/Software/src/inverter/GROWATT-HV-CAN.cpp index 7d92e1bf0..cad13559a 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.cpp +++ b/Software/src/inverter/GROWATT-HV-CAN.cpp @@ -44,8 +44,7 @@ void GrowattHvInverter:: // capacity_10mAh = Wh * 1000 / dV (because V = dV/10, and 10mAh units = Ah*100) const uint16_t v_dV = datalayer.battery.status.voltage_dV; - uint32_t full_10mAh = - (uint32_t)((uint64_t)datalayer.battery.info.total_capacity_Wh * 1000ULL / v_dV); + uint32_t full_10mAh = (uint32_t)((uint64_t)datalayer.battery.info.total_capacity_Wh * 1000ULL / v_dV); uint32_t rem_10mAh = 0; if (datalayer.battery.status.remaining_capacity_Wh > 0) { @@ -56,8 +55,10 @@ void GrowattHvInverter:: rem_10mAh = (uint32_t)((uint64_t)full_10mAh * soc_pct / 100ULL); } - if (full_10mAh > 0xFFFF) full_10mAh = 0xFFFF; - if (rem_10mAh > 0xFFFF) rem_10mAh = 0xFFFF; + if (full_10mAh > 0xFFFF) + full_10mAh = 0xFFFF; + if (rem_10mAh > 0xFFFF) + rem_10mAh = 0xFFFF; capacity_full_10mAh = (uint16_t)full_10mAh; capacity_remaining_10mAh = (uint16_t)rem_10mAh; From dab421b88e034a6b0f2ded8dd9bb260c1f40305b Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:23:18 +0100 Subject: [PATCH 25/26] Add files via upload --- Software/src/inverter/GROWATT-HV-CAN.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Software/src/inverter/GROWATT-HV-CAN.h b/Software/src/inverter/GROWATT-HV-CAN.h index 44b867456..0f896d529 100644 --- a/Software/src/inverter/GROWATT-HV-CAN.h +++ b/Software/src/inverter/GROWATT-HV-CAN.h @@ -130,8 +130,9 @@ class GrowattHvInverter : public CanInverterProtocol { unsigned long previousMillis1s = 0; // will store last time a 1s CAN Message was send unsigned long previousMillisBatchSend = 0; uint32_t unix_time = 0; - uint16_t ampere_hours_remaining = 0; - uint16_t ampere_hours_full = 0; + // 0x3140 expects capacity in 10 mAh units. + uint16_t capacity_remaining_10mAh = 0; + uint16_t capacity_full_10mAh = 0; uint16_t send_times = 0; // Overflows every 18hours. Cumulative number, plus 1 for each transmission uint8_t safety_specification = 0; uint8_t charging_command = 0; From d4d1fdf751c69bb54775c22bfd7cb93711c72284 Mon Sep 17 00:00:00 2001 From: ErikssonPer <127212101+ErikssonPer@users.noreply.github.com> Date: Mon, 19 Jan 2026 23:29:13 +0100 Subject: [PATCH 26/26] Add C/C++ CI workflow configuration --- .github/workflows/c-cpp.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/c-cpp.yml diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml new file mode 100644 index 000000000..6a9c312e6 --- /dev/null +++ b/.github/workflows/c-cpp.yml @@ -0,0 +1,23 @@ +name: C/C++ CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: configure + run: ./configure + - name: make + run: make + - name: make check + run: make check + - name: make distcheck + run: make distcheck