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"); diff --git a/Software/src/communication/nvm/comm_nvm.cpp b/Software/src/communication/nvm/comm_nvm.cpp index 23d47a72..d2adb904 100644 --- a/Software/src/communication/nvm/comm_nvm.cpp +++ b/Software/src/communication/nvm/comm_nvm.cpp @@ -72,6 +72,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); @@ -177,6 +181,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 275f195d..08352dce 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 a240991a..ed562679 100644 --- a/Software/src/devboard/webserver/settings_html.cpp +++ b/Software/src/devboard/webserver/settings_html.cpp @@ -135,6 +135,18 @@ String settings_processor(const String& var, BatteryEmulatorSettingsStore& setti } } + if (var == "INVBIDCLASS") { + if (!inverter || !inverter->supports_battery_id()) { + return "hidden"; + } + } + + if (var == "INVBID") { + if (inverter && inverter->supports_battery_id()) { + return String(datalayer.battery.settings.sofar_user_specified_battery_id); + } + } + if (var == "INVINTF") { if (inverter) { return inverter->interface_name(); @@ -493,7 +505,11 @@ const char* getCANInterfaceName(CAN_Interface interface) { 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();}} - + + function editSofarID(){var value=prompt('For double battery setups. Which battery ID should this emulator 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 and 15.');}}} + 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 XMLHttpRequest();xhr.onload=editComplete;xhr.onerror=editError;xhr.open('GET','/updateBatterySize?value='+value,true);xhr.send();}else{ @@ -746,6 +762,8 @@ const char* getCANInterfaceName(CAN_Interface interface) {

Inverter interface: %INVINTF%

+

Battery ID: %INVBID%

+

Shunt interface: %SHUNTINTF%

diff --git a/Software/src/devboard/webserver/webserver.cpp b/Software/src/devboard/webserver/webserver.cpp index 84630d4d..54dd5725 100644 --- a/Software/src/devboard/webserver/webserver.cpp +++ b/Software/src/devboard/webserver/webserver.cpp @@ -566,6 +566,10 @@ void init_webserver() { update_string_setting(route, [setter](String value) { setter(value.toInt()); }); }; + // Route for editing Sofar ID + update_int_setting("/updateSofarID", + [](int value) { datalayer.battery.settings.sofar_user_specified_battery_id = value; }); + // Route for editing Wh update_int_setting("/updateBatterySize", [](int value) { datalayer.battery.info.total_capacity_Wh = value; }); diff --git a/Software/src/inverter/InverterProtocol.h b/Software/src/inverter/InverterProtocol.h index 9aa053bf..9e1a07df 100644 --- a/Software/src/inverter/InverterProtocol.h +++ b/Software/src/inverter/InverterProtocol.h @@ -46,6 +46,8 @@ class InverterProtocol { virtual bool controls_contactor() { return false; } virtual bool allows_contactor_closing() { return false; } + + virtual bool supports_battery_id() { return false; } }; extern InverterProtocol* inverter; diff --git a/Software/src/inverter/SOFAR-CAN.cpp b/Software/src/inverter/SOFAR-CAN.cpp index d9c0d96f..a5af6fbb 100644 --- a/Software/src/inverter/SOFAR-CAN.cpp +++ b/Software/src/inverter/SOFAR-CAN.cpp @@ -32,19 +32,67 @@ 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); + + // 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; + 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 + } + + // Frame 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; @@ -66,3 +114,35 @@ void SofarInverter::transmit_can(unsigned long currentMillis) { transmit_can_frame(&SOFAR_35A, can_config.inverter); } } + +bool SofarInverter::setup() { // 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 = (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; + 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); + + 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'; + + return true; +} diff --git a/Software/src/inverter/SOFAR-CAN.h b/Software/src/inverter/SOFAR-CAN.h index af3e80cc..3b8fa635 100644 --- a/Software/src/inverter/SOFAR-CAN.h +++ b/Software/src/inverter/SOFAR-CAN.h @@ -10,14 +10,18 @@ class SofarInverter : public CanInverterProtocol { public: + bool setup() override; const char* name() override { return Name; } 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"; + bool supports_battery_id() { return true; } private: unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send + 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