Merge pull request #1307 from dalathegreat/feature/solxpow-protocol

Feature: Solxpow CAN protocol 
This commit is contained in:
Daniel Öster 2025-07-21 23:33:58 +03:00 committed by GitHub
commit 85df91e443
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 535 additions and 0 deletions

View file

@ -68,6 +68,7 @@
//#define SMA_TRIPOWER_CAN //Enable this line to emulate a "SMA Home Storage battery" over CAN bus //#define SMA_TRIPOWER_CAN //Enable this line to emulate a "SMA Home Storage battery" over CAN bus
//#define SOFAR_CAN //Enable this line to emulate a "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame)" over CAN bus //#define SOFAR_CAN //Enable this line to emulate a "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame)" over CAN bus
//#define SOLAX_CAN //Enable this line to emulate a "SolaX Triple Power LFP" over CAN bus //#define SOLAX_CAN //Enable this line to emulate a "SolaX Triple Power LFP" over CAN bus
//#define SOLXPOW_CAN //Enable this line to emulate a "Solxpow compatible battery" over CAN bus
//#define SUNGROW_CAN //Enable this line to emulate a "Sungrow SBR064" over CAN bus //#define SUNGROW_CAN //Enable this line to emulate a "Sungrow SBR064" over CAN bus
/* Select hardware used for Battery-Emulator */ /* Select hardware used for Battery-Emulator */

View file

@ -70,6 +70,9 @@ extern const char* name_for_inverter_type(InverterProtocolType type) {
case InverterProtocolType::Solax: case InverterProtocolType::Solax:
return SolaxInverter::Name; return SolaxInverter::Name;
case InverterProtocolType::Solxpow:
return SolxpowInverter::Name;
case InverterProtocolType::Sungrow: case InverterProtocolType::Sungrow:
return SungrowInverter::Name; return SungrowInverter::Name;
} }
@ -155,6 +158,10 @@ void setup_inverter() {
inverter = new SolaxInverter(); inverter = new SolaxInverter();
break; break;
case InverterProtocolType::Solxpow:
inverter = new SolxpowInverter();
break;
case InverterProtocolType::Sungrow: case InverterProtocolType::Sungrow:
inverter = new SungrowInverter(); inverter = new SungrowInverter();
break; break;

View file

@ -28,6 +28,7 @@ extern InverterProtocol* inverter;
#include "SMA-TRIPOWER-CAN.h" #include "SMA-TRIPOWER-CAN.h"
#include "SOFAR-CAN.h" #include "SOFAR-CAN.h"
#include "SOLAX-CAN.h" #include "SOLAX-CAN.h"
#include "SOLXPOW-CAN.h"
#include "SUNGROW-CAN.h" #include "SUNGROW-CAN.h"
// Call to initialize the build-time selected inverter. Safe to call even though inverter was not selected. // Call to initialize the build-time selected inverter. Safe to call even though inverter was not selected.

View file

@ -20,6 +20,7 @@ enum class InverterProtocolType {
SmaTripower, SmaTripower,
Sofar, Sofar,
Solax, Solax,
Solxpow,
Sungrow, Sungrow,
Highest Highest
}; };

View file

@ -0,0 +1,361 @@
#include "SOLXPOW-CAN.h"
#include "../communication/can/comm_can.h"
#include "../datalayer/datalayer.h"
#include "../include.h"
#define SEND_0 //If defined, the messages will have ID ending with 0 (useful for some inverters)
//#define SEND_1 //If defined, the messages will have ID ending with 1 (useful for some inverters)
#define INVERT_LOW_HIGH_BYTES //If defined, certain frames will have inverted low/high bytes \
//useful for some inverters like Sofar that report the voltages incorrect otherwise
//#define SET_30K_OFFSET //If defined, current values are sent with a 30k offest (useful for ferroamp)
void SolxpowInverter::
update_values() { //This function maps all the values fetched from battery CAN to the correct CAN messages
//Check what discharge and charge cutoff voltages to send
if (datalayer.battery.settings.user_set_voltage_limits_active) { //If user is requesting a specific voltage
discharge_cutoff_voltage_dV = datalayer.battery.settings.max_user_set_discharge_voltage_dV;
charge_cutoff_voltage_dV = datalayer.battery.settings.max_user_set_charge_voltage_dV;
} else {
discharge_cutoff_voltage_dV = (datalayer.battery.info.min_design_voltage_dV + VOLTAGE_OFFSET_DV);
charge_cutoff_voltage_dV = (datalayer.battery.info.max_design_voltage_dV - VOLTAGE_OFFSET_DV);
}
//There are more mappings that could be added, but this should be enough to use as a starting point
// Note we map both 0 and 1 messages
//Charge / Discharge allowed flags
if (datalayer.battery.status.max_charge_current_dA == 0) {
SOLXPOW_4280.data.u8[0] = 0xAA; //Charge forbidden
SOLXPOW_4281.data.u8[0] = 0xAA;
} else {
SOLXPOW_4280.data.u8[0] = 0; //Charge allowed
SOLXPOW_4281.data.u8[0] = 0;
}
if (datalayer.battery.status.max_discharge_current_dA == 0) {
SOLXPOW_4280.data.u8[1] = 0xAA; //Discharge forbidden
SOLXPOW_4281.data.u8[1] = 0xAA;
} else {
SOLXPOW_4280.data.u8[1] = 0; //Discharge allowed
SOLXPOW_4281.data.u8[1] = 0;
}
//In case run into a FAULT state, let inverter know to stop any charge/discharge
if (datalayer.battery.status.bms_status == FAULT) {
SOLXPOW_4280.data.u8[0] = 0xAA; //Charge forbidden
SOLXPOW_4280.data.u8[1] = 0xAA; //Discharge forbidden
SOLXPOW_4281.data.u8[0] = 0xAA; //Charge forbidden
SOLXPOW_4281.data.u8[1] = 0xAA; //Discharge forbidden
}
//Voltage (370.0)
SOLXPOW_4210.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8);
SOLXPOW_4210.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF);
SOLXPOW_4211.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8);
SOLXPOW_4211.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF);
//Current (15.0)
SOLXPOW_4210.data.u8[2] = (datalayer.battery.status.current_dA >> 8);
SOLXPOW_4210.data.u8[3] = (datalayer.battery.status.current_dA & 0x00FF);
SOLXPOW_4211.data.u8[2] = (datalayer.battery.status.current_dA >> 8);
SOLXPOW_4211.data.u8[3] = (datalayer.battery.status.current_dA & 0x00FF);
// BMS Temperature (We dont have BMS temp, send max cell voltage instead)
#ifdef INVERT_LOW_HIGH_BYTES //Useful for Sofar inverters
SOLXPOW_4210.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
SOLXPOW_4210.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
SOLXPOW_4211.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
SOLXPOW_4211.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
#else // Not INVERT_LOW_HIGH_BYTES
SOLXPOW_4210.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
SOLXPOW_4210.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
SOLXPOW_4211.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
SOLXPOW_4211.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
#endif // INVERT_LOW_HIGH_BYTES
//SOC (100.00%)
SOLXPOW_4210.data.u8[6] = (datalayer.battery.status.reported_soc / 100); //Remove decimals
SOLXPOW_4211.data.u8[6] = (datalayer.battery.status.reported_soc / 100); //Remove decimals
//StateOfHealth (100.00%)
SOLXPOW_4210.data.u8[7] = (datalayer.battery.status.soh_pptt / 100);
SOLXPOW_4211.data.u8[7] = (datalayer.battery.status.soh_pptt / 100);
// Status=Bit 0,1,2= 0:Sleep, 1:Charge, 2:Discharge 3:Idle. Bit3 ForceChargeReq. Bit4 Balance charge Request
if (datalayer.battery.status.bms_status == FAULT) {
SOLXPOW_4250.data.u8[0] = (0x00); // Sleep
SOLXPOW_4251.data.u8[0] = (0x00); // Sleep
} else if (datalayer.battery.status.current_dA < 0) {
SOLXPOW_4250.data.u8[0] = (0x01); // Charge
SOLXPOW_4251.data.u8[0] = (0x01); // Charge
} else if (datalayer.battery.status.current_dA > 0) {
SOLXPOW_4250.data.u8[0] = (0x02); // Discharge
SOLXPOW_4251.data.u8[0] = (0x02); // Discharge
} else if (datalayer.battery.status.current_dA == 0) {
SOLXPOW_4250.data.u8[0] = (0x03); // Idle
SOLXPOW_4251.data.u8[0] = (0x03); // Idle
}
#ifdef INVERT_LOW_HIGH_BYTES //Useful for Sofar inverters
//Voltage (370.0)
SOLXPOW_4210.data.u8[0] = (datalayer.battery.status.voltage_dV & 0x00FF);
SOLXPOW_4210.data.u8[1] = (datalayer.battery.status.voltage_dV >> 8);
SOLXPOW_4211.data.u8[0] = (datalayer.battery.status.voltage_dV & 0x00FF);
SOLXPOW_4211.data.u8[1] = (datalayer.battery.status.voltage_dV >> 8);
#ifdef SET_30K_OFFSET
//Current (15.0)
SOLXPOW_4210.data.u8[2] = ((datalayer.battery.status.current_dA + 30000) & 0x00FF);
SOLXPOW_4210.data.u8[3] = ((datalayer.battery.status.current_dA + 30000) >> 8);
SOLXPOW_4211.data.u8[2] = ((datalayer.battery.status.current_dA + 30000) & 0x00FF);
SOLXPOW_4211.data.u8[3] = ((datalayer.battery.status.current_dA + 30000) >> 8);
#else // Not SET_30K_OFFSET
SOLXPOW_4210.data.u8[2] = (datalayer.battery.status.current_dA & 0x00FF);
SOLXPOW_4210.data.u8[3] = (datalayer.battery.status.current_dA >> 8);
SOLXPOW_4211.data.u8[2] = (datalayer.battery.status.current_dA & 0x00FF);
SOLXPOW_4211.data.u8[3] = (datalayer.battery.status.current_dA >> 8);
#endif //SET_30K_OFFSET
// BMS Temperature (We dont have BMS temp, send max cell voltage instead)
SOLXPOW_4210.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
SOLXPOW_4210.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
SOLXPOW_4211.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
SOLXPOW_4211.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
//Maxvoltage (eg 400.0V = 4000 , 16bits long) Charge Cutoff Voltage
SOLXPOW_4220.data.u8[0] = (charge_cutoff_voltage_dV & 0x00FF);
SOLXPOW_4220.data.u8[1] = (charge_cutoff_voltage_dV >> 8);
SOLXPOW_4221.data.u8[0] = (charge_cutoff_voltage_dV & 0x00FF);
SOLXPOW_4221.data.u8[1] = (charge_cutoff_voltage_dV >> 8);
//Minvoltage (eg 300.0V = 3000 , 16bits long) Discharge Cutoff Voltage
SOLXPOW_4220.data.u8[2] = (discharge_cutoff_voltage_dV & 0x00FF);
SOLXPOW_4220.data.u8[3] = (discharge_cutoff_voltage_dV >> 8);
SOLXPOW_4221.data.u8[2] = (discharge_cutoff_voltage_dV & 0x00FF);
SOLXPOW_4221.data.u8[3] = (discharge_cutoff_voltage_dV >> 8);
#ifdef SET_30K_OFFSET
//Max ChargeCurrent
SOLXPOW_4220.data.u8[4] = ((datalayer.battery.status.max_charge_current_dA + 30000) & 0x00FF);
SOLXPOW_4220.data.u8[5] = ((datalayer.battery.status.max_charge_current_dA + 30000) >> 8);
SOLXPOW_4221.data.u8[4] = ((datalayer.battery.status.max_charge_current_dA + 30000) & 0x00FF);
SOLXPOW_4221.data.u8[5] = ((datalayer.battery.status.max_charge_current_dA + 30000) >> 8);
//Max DischargeCurrent
SOLXPOW_4220.data.u8[6] = ((30000 - datalayer.battery.status.max_discharge_current_dA) & 0x00FF);
SOLXPOW_4220.data.u8[7] = ((30000 - datalayer.battery.status.max_discharge_current_dA) >> 8);
SOLXPOW_4221.data.u8[6] = ((30000 - datalayer.battery.status.max_discharge_current_dA) & 0x00FF);
SOLXPOW_4221.data.u8[7] = ((30000 - datalayer.battery.status.max_discharge_current_dA) >> 8);
#else // Not SET_30K_OFFSET
//Max ChargeCurrent
SOLXPOW_4220.data.u8[4] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
SOLXPOW_4220.data.u8[5] = (datalayer.battery.status.max_charge_current_dA >> 8);
SOLXPOW_4221.data.u8[4] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
SOLXPOW_4221.data.u8[5] = (datalayer.battery.status.max_charge_current_dA >> 8);
//Max DishargeCurrent
SOLXPOW_4220.data.u8[6] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
SOLXPOW_4220.data.u8[7] = (datalayer.battery.status.max_discharge_current_dA >> 8);
SOLXPOW_4221.data.u8[6] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
SOLXPOW_4221.data.u8[7] = (datalayer.battery.status.max_discharge_current_dA >> 8);
#endif // SET_30K_OFFSET
//Max cell voltage
SOLXPOW_4230.data.u8[0] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SOLXPOW_4230.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
SOLXPOW_4231.data.u8[0] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SOLXPOW_4231.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
//Min cell voltage
SOLXPOW_4230.data.u8[2] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF);
SOLXPOW_4230.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV >> 8);
SOLXPOW_4231.data.u8[2] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF);
SOLXPOW_4231.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV >> 8);
//Max temperature per cell
SOLXPOW_4240.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SOLXPOW_4240.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8);
SOLXPOW_4241.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SOLXPOW_4241.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8);
//Max/Min temperature per cell
SOLXPOW_4240.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
SOLXPOW_4240.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
SOLXPOW_4241.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
SOLXPOW_4241.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
//Max temperature per module
SOLXPOW_4270.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SOLXPOW_4270.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8);
SOLXPOW_4271.data.u8[0] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SOLXPOW_4271.data.u8[1] = (datalayer.battery.status.temperature_max_dC >> 8);
//Min temperature per module
SOLXPOW_4270.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
SOLXPOW_4270.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
SOLXPOW_4271.data.u8[2] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
SOLXPOW_4271.data.u8[3] = (datalayer.battery.status.temperature_min_dC >> 8);
#else // Not INVERT_LOW_HIGH_BYTES
//Voltage (370.0)
SOLXPOW_4210.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8);
SOLXPOW_4210.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF);
SOLXPOW_4211.data.u8[0] = (datalayer.battery.status.voltage_dV >> 8);
SOLXPOW_4211.data.u8[1] = (datalayer.battery.status.voltage_dV & 0x00FF);
#ifdef SET_30K_OFFSET
//Current (15.0)
SOLXPOW_4210.data.u8[2] = ((datalayer.battery.status.current_dA + 30000) >> 8);
SOLXPOW_4210.data.u8[3] = ((datalayer.battery.status.current_dA + 30000) & 0x00FF);
SOLXPOW_4211.data.u8[2] = ((datalayer.battery.status.current_dA + 30000) >> 8);
SOLXPOW_4211.data.u8[3] = ((datalayer.battery.status.current_dA + 30000) & 0x00FF);
#else // Not SET_30K_OFFSET
SOLXPOW_4210.data.u8[2] = (datalayer.battery.status.current_dA >> 8);
SOLXPOW_4210.data.u8[3] = (datalayer.battery.status.current_dA & 0x00FF);
SOLXPOW_4211.data.u8[2] = (datalayer.battery.status.current_dA >> 8);
SOLXPOW_4211.data.u8[3] = (datalayer.battery.status.current_dA & 0x00FF);
#endif //SET_30K_OFFSET
// BMS Temperature (We dont have BMS temp, send max cell voltage instead)
SOLXPOW_4210.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
SOLXPOW_4210.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
SOLXPOW_4211.data.u8[4] = ((datalayer.battery.status.temperature_max_dC + 1000) >> 8);
SOLXPOW_4211.data.u8[5] = ((datalayer.battery.status.temperature_max_dC + 1000) & 0x00FF);
//Maxvoltage (eg 400.0V = 4000 , 16bits long) Charge Cutoff Voltage
SOLXPOW_4220.data.u8[0] = (charge_cutoff_voltage_dV >> 8);
SOLXPOW_4220.data.u8[1] = (charge_cutoff_voltage_dV & 0x00FF);
SOLXPOW_4221.data.u8[0] = (charge_cutoff_voltage_dV >> 8);
SOLXPOW_4221.data.u8[1] = (charge_cutoff_voltage_dV & 0x00FF);
//Minvoltage (eg 300.0V = 3000 , 16bits long) Discharge Cutoff Voltage
SOLXPOW_4220.data.u8[2] = (discharge_cutoff_voltage_dV >> 8);
SOLXPOW_4220.data.u8[3] = (discharge_cutoff_voltage_dV & 0x00FF);
SOLXPOW_4221.data.u8[2] = (discharge_cutoff_voltage_dV >> 8);
SOLXPOW_4221.data.u8[3] = (discharge_cutoff_voltage_dV & 0x00FF);
#ifdef SET_30K_OFFSET
//Max ChargeCurrent
SOLXPOW_4220.data.u8[4] = ((datalayer.battery.status.max_charge_current_dA + 30000) >> 8);
SOLXPOW_4220.data.u8[5] = ((datalayer.battery.status.max_charge_current_dA + 30000) & 0x00FF);
SOLXPOW_4221.data.u8[4] = ((datalayer.battery.status.max_charge_current_dA + 30000) >> 8);
SOLXPOW_4221.data.u8[5] = ((datalayer.battery.status.max_charge_current_dA + 30000) & 0x00FF);
//Max DischargeCurrent
SOLXPOW_4220.data.u8[6] = ((30000 - datalayer.battery.status.max_discharge_current_dA) >> 8);
SOLXPOW_4220.data.u8[7] = ((30000 - datalayer.battery.status.max_discharge_current_dA) & 0x00FF);
SOLXPOW_4221.data.u8[6] = ((30000 - datalayer.battery.status.max_discharge_current_dA) >> 8);
SOLXPOW_4221.data.u8[7] = ((30000 - datalayer.battery.status.max_discharge_current_dA) & 0x00FF);
#else // Not SET_30K_OFFSET
//Max ChargeCurrent
SOLXPOW_4220.data.u8[4] = (datalayer.battery.status.max_charge_current_dA >> 8);
SOLXPOW_4220.data.u8[5] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
SOLXPOW_4221.data.u8[4] = (datalayer.battery.status.max_charge_current_dA >> 8);
SOLXPOW_4221.data.u8[5] = (datalayer.battery.status.max_charge_current_dA & 0x00FF);
//Max DishargeCurrent
SOLXPOW_4220.data.u8[6] = (datalayer.battery.status.max_discharge_current_dA >> 8);
SOLXPOW_4220.data.u8[7] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
SOLXPOW_4221.data.u8[6] = (datalayer.battery.status.max_discharge_current >> 8);
SOLXPOW_4221.data.u8[7] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF);
#endif //SET_30K_OFFSET
//Max cell voltage
SOLXPOW_4230.data.u8[0] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
SOLXPOW_4230.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
SOLXPOW_4231.data.u8[0] = (datalayer.battery.status.cell_max_voltage_mV >> 8);
SOLXPOW_4231.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF);
//Min cell voltage
SOLXPOW_4230.data.u8[2] = (datalayer.battery.status.cell_min_voltage_mV >> 8);
SOLXPOW_4230.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF);
SOLXPOW_4231.data.u8[2] = (datalayer.battery.status.cell_min_voltage_mV >> 8);
SOLXPOW_4231.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF);
//Max temperature per cell
SOLXPOW_4240.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SOLXPOW_4240.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SOLXPOW_4241.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SOLXPOW_4241.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//Max/Min temperature per cell
SOLXPOW_4240.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SOLXPOW_4240.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
SOLXPOW_4241.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SOLXPOW_4241.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
//Max temperature per module
SOLXPOW_4270.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SOLXPOW_4270.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
SOLXPOW_4271.data.u8[0] = (datalayer.battery.status.temperature_max_dC >> 8);
SOLXPOW_4271.data.u8[1] = (datalayer.battery.status.temperature_max_dC & 0x00FF);
//Min temperature per module
SOLXPOW_4270.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SOLXPOW_4270.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
SOLXPOW_4271.data.u8[2] = (datalayer.battery.status.temperature_min_dC >> 8);
SOLXPOW_4271.data.u8[3] = (datalayer.battery.status.temperature_min_dC & 0x00FF);
#endif // Not INVERT_LOW_HIGH_BYTES
}
void SolxpowInverter::map_can_frame_to_variable(CAN_frame rx_frame) {
switch (rx_frame.ID) {
case 0x4200: //Message originating from inverter. Depending on which data is required, act accordingly
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
if (rx_frame.data.u8[0] == 0x02) {
send_setup_info();
}
if (rx_frame.data.u8[0] == 0x00) {
send_system_data();
}
break;
default:
break;
}
}
void SolxpowInverter::transmit_can(unsigned long currentMillis) {
// No periodic sending, we only react on received can messages
}
void SolxpowInverter::send_setup_info() { //Ensemble information
#ifdef SEND_0
transmit_can_frame(&SOLXPOW_7310, can_config.inverter);
transmit_can_frame(&SOLXPOW_7320, can_config.inverter);
transmit_can_frame(&SOLXPOW_7330, can_config.inverter);
transmit_can_frame(&SOLXPOW_7340, can_config.inverter);
#endif
#ifdef SEND_1
transmit_can_frame(&SOLXPOW_7311, can_config.inverter);
transmit_can_frame(&SOLXPOW_7321, can_config.inverter);
#endif
}
void SolxpowInverter::send_system_data() { //System equipment information
#ifdef SEND_0
transmit_can_frame(&SOLXPOW_4210, can_config.inverter);
transmit_can_frame(&SOLXPOW_4220, can_config.inverter);
transmit_can_frame(&SOLXPOW_4230, can_config.inverter);
transmit_can_frame(&SOLXPOW_4240, can_config.inverter);
transmit_can_frame(&SOLXPOW_4250, can_config.inverter);
transmit_can_frame(&SOLXPOW_4260, can_config.inverter);
transmit_can_frame(&SOLXPOW_4270, can_config.inverter);
transmit_can_frame(&SOLXPOW_4280, can_config.inverter);
transmit_can_frame(&SOLXPOW_4290, can_config.inverter);
#endif
#ifdef SEND_1
transmit_can_frame(&SOLXPOW_4211, can_config.inverter);
transmit_can_frame(&SOLXPOW_4221, can_config.inverter);
transmit_can_frame(&SOLXPOW_4231, can_config.inverter);
transmit_can_frame(&SOLXPOW_4241, can_config.inverter);
transmit_can_frame(&SOLXPOW_4251, can_config.inverter);
transmit_can_frame(&SOLXPOW_4261, can_config.inverter);
transmit_can_frame(&SOLXPOW_4271, can_config.inverter);
transmit_can_frame(&SOLXPOW_4281, can_config.inverter);
transmit_can_frame(&SOLXPOW_4291, can_config.inverter);
#endif
}
void SolxpowInverter::setup(void) { // Performs one time setup at startup over CAN bus
strncpy(datalayer.system.info.inverter_protocol, Name, 63);
datalayer.system.info.inverter_protocol[63] = '\0';
}

View file

@ -0,0 +1,164 @@
#ifndef SOLXPOW_CAN_H
#define SOLXPOW_CAN_H
#include "../include.h"
#include "CanInverterProtocol.h"
#ifdef SOLXPOW_CAN
#define SELECTED_INVERTER_CLASS SolxpowInverter
#endif
class SolxpowInverter : public CanInverterProtocol {
public:
void setup();
void update_values();
void transmit_can(unsigned long currentMillis);
void map_can_frame_to_variable(CAN_frame rx_frame);
static constexpr const char* Name = "Solxpow compatible battery";
private:
void send_system_data();
void send_setup_info();
/* Some inverters need to see a specific amount of cells/modules to emulate a specific Pylon battery.
Change the following only if your inverter is generating fault codes about voltage range */
static const int TOTAL_CELL_AMOUNT = 120;
static const int MODULES_IN_SERIES = 4;
static const int CELLS_PER_MODULE = 30;
static const int VOLTAGE_LEVEL = 384;
static const int AH_CAPACITY = 37;
/* Do not change code below unless you are sure what you are doing */
//Actual content messages
CAN_frame SOLXPOW_7330 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x7330,
.data = {0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58}};
CAN_frame SOLXPOW_7340 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x7340,
.data = {0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58, 0x58}};
CAN_frame SOLXPOW_7310 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x7310,
.data = {0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x00, 0x00}};
CAN_frame SOLXPOW_7311 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x7311,
.data = {0x01, 0x00, 0x02, 0x01, 0x01, 0x02, 0x00, 0x00}};
CAN_frame SOLXPOW_7320 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x7320,
.data = {TOTAL_CELL_AMOUNT, (uint8_t)(TOTAL_CELL_AMOUNT >> 8), MODULES_IN_SERIES,
CELLS_PER_MODULE, (uint8_t)(VOLTAGE_LEVEL & 0x00FF), (uint8_t)(VOLTAGE_LEVEL >> 8),
(uint8_t)(AH_CAPACITY & 0x00FF), (uint8_t)(AH_CAPACITY >> 8)}};
CAN_frame SOLXPOW_7321 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x7321,
.data = {TOTAL_CELL_AMOUNT, (uint8_t)(TOTAL_CELL_AMOUNT >> 8), MODULES_IN_SERIES,
CELLS_PER_MODULE, (uint8_t)(VOLTAGE_LEVEL & 0x00FF), (uint8_t)(VOLTAGE_LEVEL >> 8),
(uint8_t)(AH_CAPACITY & 0x00FF), (uint8_t)(AH_CAPACITY >> 8)}};
CAN_frame SOLXPOW_4210 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4210,
.data = {0xA5, 0x09, 0x30, 0x75, 0x9D, 0x04, 0x2E, 0x64}};
CAN_frame SOLXPOW_4220 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4220,
.data = {0x8C, 0x0A, 0xE9, 0x07, 0x4A, 0x79, 0x4A, 0x79}};
CAN_frame SOLXPOW_4230 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4230,
.data = {0xDF, 0x0C, 0xDA, 0x0C, 0x03, 0x00, 0x06, 0x00}};
CAN_frame SOLXPOW_4240 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4240,
.data = {0x7E, 0x04, 0x62, 0x04, 0x11, 0x00, 0x03, 0x00}};
CAN_frame SOLXPOW_4250 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4250,
.data = {0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SOLXPOW_4260 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4260,
.data = {0xAC, 0xC7, 0x74, 0x27, 0x03, 0x00, 0x02, 0x00}};
CAN_frame SOLXPOW_4270 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4270,
.data = {0x7E, 0x04, 0x62, 0x04, 0x05, 0x00, 0x01, 0x00}};
CAN_frame SOLXPOW_4280 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4280,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SOLXPOW_4290 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4290,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SOLXPOW_4211 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4211,
.data = {0xA5, 0x09, 0x30, 0x75, 0x9D, 0x04, 0x2E, 0x64}};
CAN_frame SOLXPOW_4221 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4221,
.data = {0x8C, 0x0A, 0xE9, 0x07, 0x4A, 0x79, 0x4A, 0x79}};
CAN_frame SOLXPOW_4231 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4231,
.data = {0xDF, 0x0C, 0xDA, 0x0C, 0x03, 0x00, 0x06, 0x00}};
CAN_frame SOLXPOW_4241 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4241,
.data = {0x7E, 0x04, 0x62, 0x04, 0x11, 0x00, 0x03, 0x00}};
CAN_frame SOLXPOW_4251 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4251,
.data = {0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SOLXPOW_4261 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4261,
.data = {0xAC, 0xC7, 0x74, 0x27, 0x03, 0x00, 0x02, 0x00}};
CAN_frame SOLXPOW_4271 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4271,
.data = {0x7E, 0x04, 0x62, 0x04, 0x05, 0x00, 0x01, 0x00}};
CAN_frame SOLXPOW_4281 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4281,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
CAN_frame SOLXPOW_4291 = {.FD = false,
.ext_ID = true,
.DLC = 8,
.ID = 0x4291,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
uint16_t discharge_cutoff_voltage_dV = 0;
uint16_t charge_cutoff_voltage_dV = 0;
static const int VOLTAGE_OFFSET_DV = 20; // Small offset voltage to avoid generating voltage events
};
#endif