From bc07bb24d358dc6bd0eafc359450c1b181e61918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Tue, 1 Jul 2025 00:18:31 +0300 Subject: [PATCH 1/5] Make SOFAR CAN operational --- Software/src/inverter/SOFAR-CAN.cpp | 75 ++++++++++++++++++++++++++--- Software/src/inverter/SOFAR-CAN.h | 3 ++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/Software/src/inverter/SOFAR-CAN.cpp b/Software/src/inverter/SOFAR-CAN.cpp index a4c6c1aa..eeb5f715 100644 --- a/Software/src/inverter/SOFAR-CAN.cpp +++ b/Software/src/inverter/SOFAR-CAN.cpp @@ -32,19 +32,56 @@ void SofarInverter:: SOFAR_356.data.u8[3] = (datalayer.battery.status.current_dA >> 8); SOFAR_356.data.u8[4] = (datalayer.battery.status.temperature_max_dC & 0x00FF); SOFAR_356.data.u8[5] = (datalayer.battery.status.temperature_max_dC >> 8); + + // Charge and discharge consent dependent on SoC with hysteresis at 99% soc + //SoC deception only to CAN (we do not touch datalayer) + uint16_t spoofed_soc = datalayer.battery.status.reported_soc; + if (spoofed_soc >= 10000) { + spoofed_soc = 9900; // limit to 99% + } + + // Frame 0x355 – SoC and SoH + SOFAR_355.data.u8[0] = spoofed_soc / 100; + SOFAR_355.data.u8[2] = datalayer.battery.status.soh_pptt / 100; + + // Set charge and discharge consent flags + uint8_t soc_percent = spoofed_soc / 100; + uint8_t enable_flags = 0x00; + + if (soc_percent <= 1) { + enable_flags = 0x02; // Only charging allowed + } else if (soc_percent >= 100) { + enable_flags = 0x01; // Only discharge allowed + } else { + enable_flags = 0x03; // Both charge and discharge allowed + } + + // Ramka 0x30F – operation mode + SOFAR_30F.data.u8[0] = 0x00; // Normal mode + SOFAR_30F.data.u8[1] = enable_flags; } void SofarInverter::map_can_frame_to_variable(CAN_frame rx_frame) { - switch (rx_frame.ID) { //In here we need to respond to the inverter. TODO: make logic + switch (rx_frame.ID) { case 0x605: - datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; - //frame1_605 = rx_frame.data.u8[1]; - //frame3_605 = rx_frame.data.u8[3]; - break; case 0x705: datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; - //frame1_705 = rx_frame.data.u8[1]; - //frame3_705 = rx_frame.data.u8[3]; + switch (rx_frame.data.u8[0]) { + case 0x00: + transmit_can_frame(&SOFAR_683, can_config.inverter); + break; + case 0x01: + transmit_can_frame(&SOFAR_684, can_config.inverter); + break; + case 0x02: + transmit_can_frame(&SOFAR_685, can_config.inverter); + break; + case 0x03: + transmit_can_frame(&SOFAR_690, can_config.inverter); + break; + default: + break; + } break; default: break; @@ -68,6 +105,30 @@ void SofarInverter::transmit_can(unsigned long currentMillis) { } void SofarInverter::setup(void) { // Performs one time setup at startup over CAN bus + // Dymanically set CAN ID according to which battery index we are on + uint16_t base_offset = battery_index << 12; + auto init_frame = [&](CAN_frame& frame, uint16_t base_id) { + frame.FD = false; + frame.ext_ID = true; + frame.DLC = 8; + frame.ID = base_id + base_offset; + memset(frame.data.u8, 0, 8); + }; + + init_frame(SOFAR_351, 0x351); + init_frame(SOFAR_355, 0x355); + init_frame(SOFAR_356, 0x356); + init_frame(SOFAR_30F, 0x30F); + init_frame(SOFAR_359, 0x359); + init_frame(SOFAR_35E, 0x35E); + init_frame(SOFAR_35F, 0x35F); + init_frame(SOFAR_35A, 0x35A); + + init_frame(SOFAR_683, 0x683); + init_frame(SOFAR_684, 0x684); + init_frame(SOFAR_685, 0x685); + init_frame(SOFAR_690, 0x690); + strncpy(datalayer.system.info.inverter_protocol, Name, 63); datalayer.system.info.inverter_protocol[63] = '\0'; } diff --git a/Software/src/inverter/SOFAR-CAN.h b/Software/src/inverter/SOFAR-CAN.h index 678c79f0..b5720528 100644 --- a/Software/src/inverter/SOFAR-CAN.h +++ b/Software/src/inverter/SOFAR-CAN.h @@ -18,6 +18,9 @@ class SofarInverter : public CanInverterProtocol { private: unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send + uint8_t battery_index = 0; // Predefined battery ID (0–15) + uint16_t BatteryCapacity_Ah = 180; + const char* BatteryType = "BAT-EMU"; //Actual content messages //Note that these are technically extended frames. If more batteries are put in parallel,the first battery sends 0x351 the next battery sends 0x1351 etc. 16 batteries in parallel supported From 25c60cb42b2a26b33c82dab554af310d8bab856a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Thu, 3 Jul 2025 18:29:18 +0300 Subject: [PATCH 2/5] Add name and capacity reporting --- Software/src/inverter/SOFAR-CAN.cpp | 13 ++++++++++++- Software/src/inverter/SOFAR-CAN.h | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Software/src/inverter/SOFAR-CAN.cpp b/Software/src/inverter/SOFAR-CAN.cpp index eeb5f715..c97390aa 100644 --- a/Software/src/inverter/SOFAR-CAN.cpp +++ b/Software/src/inverter/SOFAR-CAN.cpp @@ -33,6 +33,17 @@ void SofarInverter:: SOFAR_356.data.u8[4] = (datalayer.battery.status.temperature_max_dC & 0x00FF); SOFAR_356.data.u8[5] = (datalayer.battery.status.temperature_max_dC >> 8); + // frame 0x35E – Manufacturer Name ASCII + memset(SOFAR_35E.data.u8, 0, 8); + strncpy((char*)SOFAR_35E.data.u8, BatteryType, 8); + + //Gets automatically rescaled with SOC scaling. Calculated with max design voltage, better would be to calculate with nominal voltage + calculated_capacity_AH = + (datalayer.battery.info.reported_total_capacity_Wh / (datalayer.battery.info.max_design_voltage_dV * 0.1)); + //Battery Nominal Capacity + SOFAR_35F.data.u8[4] = calculated_capacity_AH & 0x00FF; + SOFAR_35F.data.u8[5] = (calculated_capacity_AH >> 8); + // Charge and discharge consent dependent on SoC with hysteresis at 99% soc //SoC deception only to CAN (we do not touch datalayer) uint16_t spoofed_soc = datalayer.battery.status.reported_soc; @@ -56,7 +67,7 @@ void SofarInverter:: enable_flags = 0x03; // Both charge and discharge allowed } - // Ramka 0x30F – operation mode + // Frame 0x30F – operation mode SOFAR_30F.data.u8[0] = 0x00; // Normal mode SOFAR_30F.data.u8[1] = enable_flags; } diff --git a/Software/src/inverter/SOFAR-CAN.h b/Software/src/inverter/SOFAR-CAN.h index 71db46a9..d779bb71 100644 --- a/Software/src/inverter/SOFAR-CAN.h +++ b/Software/src/inverter/SOFAR-CAN.h @@ -19,8 +19,8 @@ class SofarInverter : public CanInverterProtocol { private: unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send uint8_t battery_index = 0; // Predefined battery ID (0–15) - uint16_t BatteryCapacity_Ah = 180; - const char* BatteryType = "BAT-EMU"; + uint16_t calculated_capacity_AH = 0; // Pack Capacity in AH (Updates based on battery stats) + const char* BatteryType = "BATxEMU"; // Manufacturer name in ASCII //Actual content messages //Note that these are technically extended frames. If more batteries are put in parallel,the first battery sends 0x351 the next battery sends 0x1351 etc. 16 batteries in parallel supported From 920cd835611c5e21a6a4a39c1c96179302521044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Thu, 3 Jul 2025 20:06:35 +0300 Subject: [PATCH 3/5] Add configurable Sofar ID via Webserver --- Software/src/communication/nvm/comm_nvm.cpp | 7 +++++++ Software/src/datalayer/datalayer.h | 3 +++ Software/src/devboard/webserver/settings_html.cpp | 10 ++++++++++ Software/src/devboard/webserver/webserver.cpp | 14 ++++++++++++++ Software/src/inverter/SOFAR-CAN.cpp | 5 ++++- Software/src/inverter/SOFAR-CAN.h | 3 +-- 6 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Software/src/communication/nvm/comm_nvm.cpp b/Software/src/communication/nvm/comm_nvm.cpp index a4a9b9d1..105a1a85 100644 --- a/Software/src/communication/nvm/comm_nvm.cpp +++ b/Software/src/communication/nvm/comm_nvm.cpp @@ -70,6 +70,10 @@ void init_stored_settings() { datalayer.battery.settings.max_user_set_discharge_voltage_dV = temp; } datalayer.battery.settings.user_set_voltage_limits_active = settings.getBool("USEVOLTLIMITS", false); + temp = settings.getUInt("SOFAR_ID", false); + if (temp < 16) { + datalayer.battery.settings.sofar_user_specified_battery_id = temp; + } #ifdef COMMON_IMAGE user_selected_battery_type = (BatteryType)settings.getUInt("BATTTYPE", (int)BatteryType::None); @@ -135,6 +139,9 @@ void store_settings() { if (!settings.putUInt("TARGETDISCHVOLT", datalayer.battery.settings.max_user_set_discharge_voltage_dV)) { set_event(EVENT_PERSISTENT_SAVE_INFO, 11); } + if (!settings.putUInt("SOFAR_ID", datalayer.battery.settings.sofar_user_specified_battery_id)) { + set_event(EVENT_PERSISTENT_SAVE_INFO, 12); + } settings.end(); // Close preferences handle } diff --git a/Software/src/datalayer/datalayer.h b/Software/src/datalayer/datalayer.h index bf818dec..f78a8659 100644 --- a/Software/src/datalayer/datalayer.h +++ b/Software/src/datalayer/datalayer.h @@ -162,6 +162,9 @@ typedef struct { /* Maximum voltage for entire battery pack during forced balancing */ uint16_t balancing_max_pack_voltage_dV = 3940; + /** Sofar CAN Battery ID (0-15) used to parallel multiple packs */ + uint8_t sofar_user_specified_battery_id = 0; + } DATALAYER_BATTERY_SETTINGS_TYPE; typedef struct { diff --git a/Software/src/devboard/webserver/settings_html.cpp b/Software/src/devboard/webserver/settings_html.cpp index 6dedcb97..8f2b80a7 100644 --- a/Software/src/devboard/webserver/settings_html.cpp +++ b/Software/src/devboard/webserver/settings_html.cpp @@ -149,6 +149,9 @@ String settings_processor(const String& var) { if (inverter) { content += "

Inverter interface: " + String(inverter->interface_name()) + "

"; + content += "

Sofar battery ID: " + + String(datalayer.battery.settings.sofar_user_specified_battery_id) + + "

"; } if (shunt) { @@ -284,6 +287,13 @@ String settings_processor(const String& var) { "function editPassword(){var value=prompt('Enter new password:');if(value!==null){var xhr=new " "XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/" "updatePassword?value='+encodeURIComponent(value),true);xhr.send();}}"; + content += + "function editSofarID(){var value=prompt('For double battery setups. Which battery ID should this emulator " + "send? Enter new value between " + "(0-15):');if(value!==null){if(value>=0&&value<=15){var xhr=new " + "XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/" + "updateSofarID?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value between 0 " + "and 15.');}}}"; content += "function editWh(){var value=prompt('How much energy the battery can store. Enter new Wh value " "(1-120000):');if(value!==null){if(value>=1&&value<=120000){var xhr=new " diff --git a/Software/src/devboard/webserver/webserver.cpp b/Software/src/devboard/webserver/webserver.cpp index 09b6db98..4316b4b3 100644 --- a/Software/src/devboard/webserver/webserver.cpp +++ b/Software/src/devboard/webserver/webserver.cpp @@ -487,6 +487,20 @@ void init_webserver() { } }); + // Route for editing Sofar ID + server.on("/updateSofarID", HTTP_GET, [](AsyncWebServerRequest* request) { + if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) + return request->requestAuthentication(); + if (request->hasParam("value")) { + String value = request->getParam("value")->value(); + datalayer.battery.settings.sofar_user_specified_battery_id = value.toInt(); + store_settings(); + request->send(200, "text/plain", "Updated successfully"); + } else { + request->send(400, "text/plain", "Bad Request"); + } + }); + // Route for editing Wh server.on("/updateBatterySize", HTTP_GET, [](AsyncWebServerRequest* request) { if (WEBSERVER_AUTH_REQUIRED && !request->authenticate(http_username, http_password)) diff --git a/Software/src/inverter/SOFAR-CAN.cpp b/Software/src/inverter/SOFAR-CAN.cpp index c97390aa..004e5375 100644 --- a/Software/src/inverter/SOFAR-CAN.cpp +++ b/Software/src/inverter/SOFAR-CAN.cpp @@ -117,7 +117,7 @@ void SofarInverter::transmit_can(unsigned long currentMillis) { void SofarInverter::setup(void) { // Performs one time setup at startup over CAN bus // Dymanically set CAN ID according to which battery index we are on - uint16_t base_offset = battery_index << 12; + uint16_t base_offset = (datalayer.battery.settings.sofar_user_specified_battery_id << 12); auto init_frame = [&](CAN_frame& frame, uint16_t base_id) { frame.FD = false; frame.ext_ID = true; @@ -142,4 +142,7 @@ void SofarInverter::setup(void) { // Performs one time setup at startup over CA strncpy(datalayer.system.info.inverter_protocol, Name, 63); datalayer.system.info.inverter_protocol[63] = '\0'; + String tempStr(datalayer.battery.settings.sofar_user_specified_battery_id); + strncpy(datalayer.system.info.inverter_brand, tempStr.c_str(), 7); + datalayer.system.info.inverter_brand[7] = '\0'; } diff --git a/Software/src/inverter/SOFAR-CAN.h b/Software/src/inverter/SOFAR-CAN.h index d779bb71..803f2e75 100644 --- a/Software/src/inverter/SOFAR-CAN.h +++ b/Software/src/inverter/SOFAR-CAN.h @@ -14,11 +14,10 @@ class SofarInverter : public CanInverterProtocol { void update_values(); void transmit_can(unsigned long currentMillis); void map_can_frame_to_variable(CAN_frame rx_frame); - static constexpr const char* Name = "Sofar BMS (Extended Frame) over CAN bus"; + static constexpr const char* Name = "Sofar BMS (Extended) via CAN, Battery ID"; private: unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send - uint8_t battery_index = 0; // Predefined battery ID (0–15) uint16_t calculated_capacity_AH = 0; // Pack Capacity in AH (Updates based on battery stats) const char* BatteryType = "BATxEMU"; // Manufacturer name in ASCII From dc443e580b2416ea9ae15c26510d6da26d3971a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Thu, 3 Jul 2025 20:07:59 +0300 Subject: [PATCH 4/5] Update comment on setting Sofar --- Software/src/devboard/webserver/settings_html.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Software/src/devboard/webserver/settings_html.cpp b/Software/src/devboard/webserver/settings_html.cpp index 8f2b80a7..786dac91 100644 --- a/Software/src/devboard/webserver/settings_html.cpp +++ b/Software/src/devboard/webserver/settings_html.cpp @@ -289,7 +289,7 @@ String settings_processor(const String& var) { "updatePassword?value='+encodeURIComponent(value),true);xhr.send();}}"; content += "function editSofarID(){var value=prompt('For double battery setups. Which battery ID should this emulator " - "send? Enter new value between " + "send? Remember to reboot after configuring this! Enter new value between " "(0-15):');if(value!==null){if(value>=0&&value<=15){var xhr=new " "XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/" "updateSofarID?value='+value,true);xhr.send();}else{alert('Invalid value. Please enter a value between 0 " From 3c0928d7fc2adf3bded1f21b0a16725346576e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20=C3=96ster?= Date: Tue, 15 Jul 2025 21:18:28 +0300 Subject: [PATCH 5/5] MEB: Fix contactors incorrectly opening after 49 days. (#1308) Fixed rollover millis bug (#1308) --- Software/src/battery/MEB-BATTERY.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Software/src/battery/MEB-BATTERY.cpp b/Software/src/battery/MEB-BATTERY.cpp index 0ac01e26..4db2b339 100644 --- a/Software/src/battery/MEB-BATTERY.cpp +++ b/Software/src/battery/MEB-BATTERY.cpp @@ -1290,7 +1290,7 @@ void MebBattery::handle_incoming_can_frame(CAN_frame rx_frame) { void MebBattery::transmit_can(unsigned long currentMillis) { - if (currentMillis > last_can_msg_timestamp + 500) { + if (currentMillis - last_can_msg_timestamp > 500) { #ifdef DEBUG_LOG if (first_can_msg) logging.printf("MEB: No CAN msg received for 500ms\n");