Merge pull request #1408 from dalathegreat/bugfix/sofar-fix

Sofar CAN: Reduced CAN sending rate, improved inverter response
This commit is contained in:
Daniel Öster 2025-08-15 20:32:26 +03:00 committed by GitHub
commit 6c0675c85a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 214 additions and 61 deletions

View file

@ -1,32 +1,35 @@
#include "SOFAR-CAN.h" #include "SOFAR-CAN.h"
#include <Arduino.h>
#include <string.h>
#include "../communication/can/comm_can.h" #include "../communication/can/comm_can.h"
#include "../datalayer/datalayer.h" #include "../datalayer/datalayer.h"
// for memcmp/memcpy used in event-driven 0x35A
/* This implementation of the SOFAR can protocol is halfway done. What's missing is implementing the inverter replies, all the CAN messages are listed, but the can sending is missing. */ #include <cstring>
void SofarInverter:: void SofarInverter::
update_values() { //This function maps all the values fetched from battery CAN to the correct CAN messages update_values() { // This function maps all the values fetched from battery CAN to the correct CAN messages
//Maxvoltage (eg 400.0V = 4000 , 16bits long) Charge Cutoff Voltage // ----- Frame 0x351 limits/voltages -----
// Maxvoltage (eg 400.0V = 4000 , 16bits long) Charge Cutoff Voltage
SOFAR_351.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF); SOFAR_351.data.u8[0] = (datalayer.battery.info.max_design_voltage_dV & 0x00FF);
SOFAR_351.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV >> 8); SOFAR_351.data.u8[1] = (datalayer.battery.info.max_design_voltage_dV >> 8);
SOFAR_351.data.u8[2] = (datalayer.battery.status.max_charge_current_dA & 0x00FF); SOFAR_351.data.u8[2] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
SOFAR_351.data.u8[3] = (datalayer.battery.status.max_charge_current_dA >> 8); SOFAR_351.data.u8[3] = (datalayer.battery.status.max_charge_current_dA >> 8);
SOFAR_351.data.u8[4] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); SOFAR_351.data.u8[4] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
SOFAR_351.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA >> 8); SOFAR_351.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA >> 8);
//Minvoltage (eg 300.0V = 3000 , 16bits long) Discharge Cutoff Voltage // Minvoltage (eg 300.0V = 3000 , 16bits long) Discharge Cutoff Voltage
SOFAR_351.data.u8[6] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF); SOFAR_351.data.u8[6] = (datalayer.battery.info.min_design_voltage_dV & 0x00FF);
SOFAR_351.data.u8[7] = (datalayer.battery.info.min_design_voltage_dV >> 8); SOFAR_351.data.u8[7] = (datalayer.battery.info.min_design_voltage_dV >> 8);
//SOC // ----- Frame 0x355 SoC / SoH -----
SOFAR_355.data.u8[0] = (datalayer.battery.status.reported_soc / 100); // SoC deception only to CAN (we do not touch datalayer)
SOFAR_355.data.u8[2] = (datalayer.battery.status.soh_pptt / 100); uint16_t spoofed_soc = datalayer.battery.status.reported_soc; // 0..10000 pptt
//SOFAR_355.data.u8[6] = (AH_remaining & 0x00FF); if (spoofed_soc >= 10000) {
//SOFAR_355.data.u8[7] = (AH_remaining >> 8); spoofed_soc = 9900; // limit to 99%
}
SOFAR_355.data.u8[0] = spoofed_soc / 100; // %
SOFAR_355.data.u8[2] = datalayer.battery.status.soh_pptt / 100; // %
//Voltage (370.0) // ----- Frame 0x356 pack voltage/current/temp -----
// Voltage (e.g. 370.0V -> 3700 dV), Current in dA, Temperature in dC
SOFAR_356.data.u8[0] = (datalayer.battery.status.voltage_dV & 0x00FF); SOFAR_356.data.u8[0] = (datalayer.battery.status.voltage_dV & 0x00FF);
SOFAR_356.data.u8[1] = (datalayer.battery.status.voltage_dV >> 8); SOFAR_356.data.u8[1] = (datalayer.battery.status.voltage_dV >> 8);
SOFAR_356.data.u8[2] = (datalayer.battery.status.current_dA & 0x00FF); SOFAR_356.data.u8[2] = (datalayer.battery.status.current_dA & 0x00FF);
@ -34,32 +37,44 @@ void SofarInverter::
SOFAR_356.data.u8[4] = (datalayer.battery.status.temperature_max_dC & 0x00FF); SOFAR_356.data.u8[4] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SOFAR_356.data.u8[5] = (datalayer.battery.status.temperature_max_dC >> 8); SOFAR_356.data.u8[5] = (datalayer.battery.status.temperature_max_dC >> 8);
// frame 0x35E Manufacturer Name ASCII // ===== Frame 0x359 Battery Pack Config 1 (cyclic, 1 Hz) =====
// Byte0: number of parallel packs; Byte1: number of modules in series (packs in series);
// Byte2: CAN version; Byte3: number of cells in series; Byte4..7: reserved (0)
memset(SOFAR_359.data.u8, 0, 8);
SOFAR_359.data.u8[0] = 0x02; // Two parallel packs in system
SOFAR_359.data.u8[1] = 0x01; // One module in series (single HV pack)
SOFAR_359.data.u8[2] = 0x01; // CAN protocol version = 1
SOFAR_359.data.u8[3] = 79; // 79S string
// ===== Frame 0x35E Manufacturer Name ASCII (cykliczna, 1 Hz) =====
memset(SOFAR_35E.data.u8, 0, 8); memset(SOFAR_35E.data.u8, 0, 8);
// BatteryType should be max 8 chars, e.g. "AMASS"
strncpy((char*)SOFAR_35E.data.u8, BatteryType, 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 // ===== Frame 0x35F Battery Type & Capacity (cykliczna, 1 Hz) =====
// Byte0: Battery type (0x01 = Li-ion), Byte1..3: BMS version (vendor-defined),
// Byte4..5: Nominal capacity (Ah, uint16), Byte6..7: Manufacturer ID (optional)
// Capacity calculation (approx): Wh / (Vmax * 0.1)
if (datalayer.battery.info.max_design_voltage_dV > 20) { //div0 protection
calculated_capacity_AH = calculated_capacity_AH =
(datalayer.battery.info.reported_total_capacity_Wh / (datalayer.battery.info.max_design_voltage_dV * 0.1)); (datalayer.battery.info.reported_total_capacity_Wh / (datalayer.battery.info.max_design_voltage_dV * 0.1));
//Battery Nominal Capacity }
// Set type + a simple version triplet 1.0.0 (can be adjusted)
SOFAR_35F.data.u8[0] = 0x01; // Li-ion
SOFAR_35F.data.u8[1] = 0x01; // BMS ver major
SOFAR_35F.data.u8[2] = 0x00; // BMS ver minor
SOFAR_35F.data.u8[3] = 0x00; // BMS ver patch
// Nominal capacity (Ah)
SOFAR_35F.data.u8[4] = calculated_capacity_AH & 0x00FF; SOFAR_35F.data.u8[4] = calculated_capacity_AH & 0x00FF;
SOFAR_35F.data.u8[5] = (calculated_capacity_AH >> 8); SOFAR_35F.data.u8[5] = (calculated_capacity_AH >> 8);
// Optional manufacturer ID left at 0
// SOFAR_35F.data.u8[6] = 0x00;
// SOFAR_35F.data.u8[7] = 0x00;
// ===== Frame 0x30F Remote command / enable (event/keep-alive) =====
// Charge and discharge consent dependent on SoC with hysteresis at 99% soc // 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 soc_percent = spoofed_soc / 100;
uint8_t enable_flags = 0x00; uint8_t enable_flags = 0x00;
if (soc_percent <= 1) { if (soc_percent <= 1) {
enable_flags = 0x02; // Only charging allowed enable_flags = 0x02; // Only charging allowed
} else if (soc_percent >= 100) { } else if (soc_percent >= 100) {
@ -67,58 +82,171 @@ void SofarInverter::
} else { } else {
enable_flags = 0x03; // Both charge and discharge allowed enable_flags = 0x03; // Both charge and discharge allowed
} }
// 0x30F byte0: remote command (0=normal), byte1: enable flags (bit0 discharge, bit1 charge)
// Frame 0x30F operation mode SOFAR_30F.data.u8[0] = 0x00; // Normal mode (no forced modes)
SOFAR_30F.data.u8[0] = 0x00; // Normal mode
SOFAR_30F.data.u8[1] = enable_flags; SOFAR_30F.data.u8[1] = enable_flags;
} }
void SofarInverter::map_can_frame_to_variable(CAN_frame rx_frame) { void SofarInverter::map_can_frame_to_variable(CAN_frame rx_frame) {
switch (rx_frame.ID) { switch (rx_frame.ID) {
case 0x605: case 0x605:
case 0x705: case 0x705: {
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
switch (rx_frame.data.u8[0]) {
// 0x605 format: Byte0=Target(m), Byte1=Inquiry-ID, Byte2=Sub-target(k)
// 0x705 format: Byte0=Target(m), Byte1=Inquiry-ID, Byte2=Record(n), Byte3=Sub-target(k)
const uint8_t target_m = rx_frame.data.u8[0];
const uint8_t inquiry = rx_frame.data.u8[1];
const bool is705 = (rx_frame.ID == 0x705);
const uint8_t record_n = is705 ? rx_frame.data.u8[2] : 0;
const uint8_t sub_k = is705 ? rx_frame.data.u8[3] : rx_frame.data.u8[2];
// Respond ONLY if addressed to my PACK ID
if (target_m != datalayer.battery.settings.sofar_user_specified_battery_id) {
break; // not my address → no reply
}
// Helper to stamp identifiers into payload (m/k/n) easy to replace with real fields later
auto stamp_ids = [&](CAN_frame& f) {
memset(f.data.u8, 0, 8);
f.data.u8[0] = datalayer.battery.settings.sofar_user_specified_battery_id; // PACK ID
f.data.u8[1] = sub_k; // Module/Sub-target (k) if applies
f.data.u8[2] = record_n; // Record Num (n) only used for 0x705
};
if (rx_frame.ID == 0x605) {
// Realtime/status inquiries → replies 0x670..0x6C0 depending on Inquiry-ID
switch (inquiry) {
case 0x00: case 0x00:
transmit_can_frame(&SOFAR_683); stamp_ids(SOFAR_670);
break; transmit_can_frame(&SOFAR_670);
break; // 0x670
case 0x01: case 0x01:
transmit_can_frame(&SOFAR_684); stamp_ids(SOFAR_671);
break; transmit_can_frame(&SOFAR_671);
break; // 0x671
case 0x02: case 0x02:
transmit_can_frame(&SOFAR_685); stamp_ids(SOFAR_680);
break; transmit_can_frame(&SOFAR_680);
break; // 0x680
case 0x03: case 0x03:
stamp_ids(SOFAR_681);
transmit_can_frame(&SOFAR_681);
break; // 0x681
case 0x0A:
stamp_ids(SOFAR_690);
transmit_can_frame(&SOFAR_690); transmit_can_frame(&SOFAR_690);
break; break; // 0x690
default: case 0x0B:
stamp_ids(SOFAR_691);
transmit_can_frame(&SOFAR_691);
break; // 0x691
case 0x0C:
stamp_ids(SOFAR_6A0);
transmit_can_frame(&SOFAR_6A0);
break; // 0x6A0
case 0x0D:
stamp_ids(SOFAR_6B0);
transmit_can_frame(&SOFAR_6B0);
break; // 0x6B0
case 0x0E:
stamp_ids(SOFAR_6C0);
transmit_can_frame(&SOFAR_6C0);
break; // 0x6C0
default: /* unsupported inquiry → no reply */
break; break;
} }
} else {
// 0x705: history/fault queries → replies 0x770..0x773 and 0x780..0x784
switch (inquiry) {
case 0x00:
stamp_ids(SOFAR_770);
transmit_can_frame(&SOFAR_770);
break; // 0x770
case 0x01:
stamp_ids(SOFAR_771);
transmit_can_frame(&SOFAR_771);
break; // 0x771
case 0x02:
stamp_ids(SOFAR_772);
transmit_can_frame(&SOFAR_772);
break; // 0x772
case 0x03:
stamp_ids(SOFAR_773);
transmit_can_frame(&SOFAR_773);
break; // 0x773
case 0x04:
stamp_ids(SOFAR_780);
transmit_can_frame(&SOFAR_780);
break; // 0x780
case 0x05:
stamp_ids(SOFAR_781);
transmit_can_frame(&SOFAR_781);
break; // 0x781
case 0x06:
stamp_ids(SOFAR_782);
transmit_can_frame(&SOFAR_782);
break; // 0x782
case 0x07:
stamp_ids(SOFAR_783);
transmit_can_frame(&SOFAR_783);
break; // 0x783
case 0x08:
stamp_ids(SOFAR_784);
transmit_can_frame(&SOFAR_784);
break; // 0x784
default: /* unsupported inquiry → no reply */
break; break;
}
}
break;
}
default: default:
break; break;
} }
} }
void SofarInverter::transmit_can(unsigned long currentMillis) { void SofarInverter::transmit_can(unsigned long currentMillis) {
// Send 100ms CAN Message
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) { static uint8_t last_35A_payload[8];
previousMillis100 = currentMillis; static bool have_last_35A = false;
//Frames actively reported by BMS
if ((unsigned long)(currentMillis - previousMillis1s) >= INTERVAL_1_S) {
previousMillis1s = currentMillis;
//Runtime frames 1000 ms
transmit_can_frame(&SOFAR_351); transmit_can_frame(&SOFAR_351);
transmit_can_frame(&SOFAR_355); transmit_can_frame(&SOFAR_355);
transmit_can_frame(&SOFAR_356); transmit_can_frame(&SOFAR_356);
transmit_can_frame(&SOFAR_30F); //Config/identity frames
transmit_can_frame(&SOFAR_359); transmit_can_frame(&SOFAR_359);
transmit_can_frame(&SOFAR_35E); transmit_can_frame(&SOFAR_35E);
transmit_can_frame(&SOFAR_35F); transmit_can_frame(&SOFAR_35F);
}
// 0x30F only when active, keep-alive ~1 Hz
bool remote_cmd_active = (SOFAR_30F.data.u8[0] != 0) || (SOFAR_30F.data.u8[1] != 0) || (SOFAR_30F.data.u8[2] != 0) ||
(SOFAR_30F.data.u8[3] != 0);
if (remote_cmd_active && (unsigned long)(currentMillis - last_command_millis) >= INTERVAL_1_S) {
last_command_millis = currentMillis;
transmit_can_frame(&SOFAR_30F);
}
// 0x35A ONLY when content changes (edge-triggered), with light debounce
if (!have_last_35A || (memcmp(last_35A_payload, SOFAR_35A.data.u8, 8) != 0)) {
if ((unsigned long)(currentMillis - last_35A_sent_millis) >= INTERVAL_200_MS) {
memcpy(last_35A_payload, SOFAR_35A.data.u8, 8);
have_last_35A = true;
last_35A_sent_millis = currentMillis;
transmit_can_frame(&SOFAR_35A); transmit_can_frame(&SOFAR_35A);
} }
}
} }
bool SofarInverter::setup() { // Performs one time setup at startup over CAN bus bool SofarInverter::setup() { // Performs one time setup at startup over CAN bus
// Dymanically set CAN ID according to which battery index we are on // Dynamically set CAN ID according to which battery index we are on
uint16_t base_offset = (datalayer.battery.settings.sofar_user_specified_battery_id << 12); uint16_t base_offset = (datalayer.battery.settings.sofar_user_specified_battery_id << 12);
auto init_frame = [&](CAN_frame& frame, uint16_t base_id) { auto init_frame = [&](CAN_frame& frame, uint16_t base_id) {
frame.FD = false; frame.FD = false;
frame.ext_ID = true; frame.ext_ID = true;
@ -127,6 +255,7 @@ bool SofarInverter::setup() { // Performs one time setup at startup over CAN bu
memset(frame.data.u8, 0, 8); memset(frame.data.u8, 0, 8);
}; };
// Cyclic BMS→PCS frames
init_frame(SOFAR_351, 0x351); init_frame(SOFAR_351, 0x351);
init_frame(SOFAR_355, 0x355); init_frame(SOFAR_355, 0x355);
init_frame(SOFAR_356, 0x356); init_frame(SOFAR_356, 0x356);
@ -136,11 +265,33 @@ bool SofarInverter::setup() { // Performs one time setup at startup over CAN bu
init_frame(SOFAR_35F, 0x35F); init_frame(SOFAR_35F, 0x35F);
init_frame(SOFAR_35A, 0x35A); init_frame(SOFAR_35A, 0x35A);
// Example legacy replies (already present)
init_frame(SOFAR_683, 0x683); init_frame(SOFAR_683, 0x683);
init_frame(SOFAR_684, 0x684); init_frame(SOFAR_684, 0x684);
init_frame(SOFAR_685, 0x685); init_frame(SOFAR_685, 0x685);
init_frame(SOFAR_690, 0x690); init_frame(SOFAR_690, 0x690);
// Replies for 0x605 (realtime queries)
init_frame(SOFAR_670, 0x670);
init_frame(SOFAR_671, 0x671);
init_frame(SOFAR_680, 0x680);
init_frame(SOFAR_681, 0x681);
init_frame(SOFAR_691, 0x691);
init_frame(SOFAR_6A0, 0x6A0);
init_frame(SOFAR_6B0, 0x6B0);
init_frame(SOFAR_6C0, 0x6C0);
// Replies for 0x705 (history/fault queries)
init_frame(SOFAR_770, 0x770);
init_frame(SOFAR_771, 0x771);
init_frame(SOFAR_772, 0x772);
init_frame(SOFAR_773, 0x773);
init_frame(SOFAR_780, 0x780);
init_frame(SOFAR_781, 0x781);
init_frame(SOFAR_782, 0x782);
init_frame(SOFAR_783, 0x783);
init_frame(SOFAR_784, 0x784);
String tempStr(datalayer.battery.settings.sofar_user_specified_battery_id); String tempStr(datalayer.battery.settings.sofar_user_specified_battery_id);
strncpy(datalayer.system.info.inverter_brand, tempStr.c_str(), 7); strncpy(datalayer.system.info.inverter_brand, tempStr.c_str(), 7);
datalayer.system.info.inverter_brand[7] = '\0'; datalayer.system.info.inverter_brand[7] = '\0';

View file

@ -1,6 +1,5 @@
#ifndef SOFAR_CAN_H #ifndef SOFAR_CAN_H
#define SOFAR_CAN_H #define SOFAR_CAN_H
#include "CanInverterProtocol.h" #include "CanInverterProtocol.h"
#ifdef SOFAR_CAN #ifdef SOFAR_CAN
@ -18,9 +17,13 @@ class SofarInverter : public CanInverterProtocol {
bool supports_battery_id() { return true; } bool supports_battery_id() { return true; }
private: private:
unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send // State
uint16_t calculated_capacity_AH = 0; // Pack Capacity in AH (Updates based on battery stats) unsigned long previousMillis1s = 0;
const char* BatteryType = "BATxEMU"; // Manufacturer name in ASCII unsigned long previousMillis100 = 0;
unsigned long last_command_millis = 0;
unsigned long last_35A_sent_millis = 0;
uint16_t calculated_capacity_AH = 0;
const char* BatteryType = "BATxEMU";
//Actual content messages //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 //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
@ -64,7 +67,6 @@ class SofarInverter : public CanInverterProtocol {
.DLC = 8, .DLC = 8,
.ID = 0x35A, .ID = 0x35A,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SOFAR_670 = {.FD = false, CAN_frame SOFAR_670 = {.FD = false,
.ext_ID = true, .ext_ID = true,
.DLC = 8, .DLC = 8,