mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-03 09:49:32 +02:00
Merge pull request #1399 from dalathegreat/feature/sol-ark-lv
New inverter protocol ⚡ Sol-Ark LV CAN
This commit is contained in:
commit
91a32e60c2
6 changed files with 177 additions and 0 deletions
|
@ -71,6 +71,7 @@
|
||||||
//#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 SOLXPOW_CAN //Enable this line to emulate a "Solxpow compatible battery" over CAN bus
|
||||||
|
//#define SOL_ARK_LV_CAN //Enable this line to emulate a "Sol-Ark compatible LV 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 */
|
||||||
|
|
|
@ -76,6 +76,9 @@ extern const char* name_for_inverter_type(InverterProtocolType type) {
|
||||||
case InverterProtocolType::Solxpow:
|
case InverterProtocolType::Solxpow:
|
||||||
return SolxpowInverter::Name;
|
return SolxpowInverter::Name;
|
||||||
|
|
||||||
|
case InverterProtocolType::SolArkLv:
|
||||||
|
return SolArkLvInverter::Name;
|
||||||
|
|
||||||
case InverterProtocolType::Sungrow:
|
case InverterProtocolType::Sungrow:
|
||||||
return SungrowInverter::Name;
|
return SungrowInverter::Name;
|
||||||
}
|
}
|
||||||
|
@ -169,6 +172,10 @@ bool setup_inverter() {
|
||||||
inverter = new SolxpowInverter();
|
inverter = new SolxpowInverter();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case InverterProtocolType::SolArkLv:
|
||||||
|
inverter = new SolArkLvInverter();
|
||||||
|
break;
|
||||||
|
|
||||||
case InverterProtocolType::Sungrow:
|
case InverterProtocolType::Sungrow:
|
||||||
inverter = new SungrowInverter();
|
inverter = new SungrowInverter();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -28,6 +28,7 @@ extern InverterProtocol* inverter;
|
||||||
#include "SMA-LV-CAN.h"
|
#include "SMA-LV-CAN.h"
|
||||||
#include "SMA-TRIPOWER-CAN.h"
|
#include "SMA-TRIPOWER-CAN.h"
|
||||||
#include "SOFAR-CAN.h"
|
#include "SOFAR-CAN.h"
|
||||||
|
#include "SOL-ARK-LV-CAN.h"
|
||||||
#include "SOLAX-CAN.h"
|
#include "SOLAX-CAN.h"
|
||||||
#include "SOLXPOW-CAN.h"
|
#include "SOLXPOW-CAN.h"
|
||||||
#include "SUNGROW-CAN.h"
|
#include "SUNGROW-CAN.h"
|
||||||
|
|
|
@ -24,6 +24,7 @@ enum class InverterProtocolType {
|
||||||
Sofar,
|
Sofar,
|
||||||
Solax,
|
Solax,
|
||||||
Solxpow,
|
Solxpow,
|
||||||
|
SolArkLv,
|
||||||
Sungrow,
|
Sungrow,
|
||||||
Highest
|
Highest
|
||||||
};
|
};
|
||||||
|
|
112
Software/src/inverter/SOL-ARK-LV-CAN.cpp
Normal file
112
Software/src/inverter/SOL-ARK-LV-CAN.cpp
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
#include "SOL-ARK-LV-CAN.h"
|
||||||
|
#include "../communication/can/comm_can.h"
|
||||||
|
#include "../datalayer/datalayer.h"
|
||||||
|
|
||||||
|
/* Sol-Ark v1.3 protocol
|
||||||
|
The Sol-Ark inverters only recognize standard CAN Bus frames containing 8 bytes of data.
|
||||||
|
CAN FD with 64 data bytes per frame is NOT supported.
|
||||||
|
Communication Rate: 500 kbps
|
||||||
|
Data Endianness: Little Endian (least significate byte is at left end of multi or 2-byte values)
|
||||||
|
Transmission Cycle Rate: BMS full data set here shall be transmitted to the inverter once every second.
|
||||||
|
BMS sends first message with this full register set. Inverter Heartbeat Response: Each time the inverter
|
||||||
|
correctly receives data, it will respond with CAN ID 0x305 containing “00 00 00 00 00 00 00 00” as data.*/
|
||||||
|
|
||||||
|
void SolArkLvInverter::update_values() {
|
||||||
|
|
||||||
|
// Set "Charge voltage limit" to battery max value OR user supplied value
|
||||||
|
uint16_t charge_voltage_dV = datalayer.battery.info.max_design_voltage_dV;
|
||||||
|
if (datalayer.battery.settings.user_set_voltage_limits_active)
|
||||||
|
charge_voltage_dV = datalayer.battery.settings.max_user_set_charge_voltage_dV;
|
||||||
|
if (charge_voltage_dV > datalayer.battery.info.max_design_voltage_dV)
|
||||||
|
charge_voltage_dV = datalayer.battery.info.max_design_voltage_dV;
|
||||||
|
SOLARK_351.data.u8[0] = charge_voltage_dV & 0xff;
|
||||||
|
SOLARK_351.data.u8[1] = charge_voltage_dV >> 8;
|
||||||
|
//Rest of setpoints in deci-units
|
||||||
|
SOLARK_351.data.u8[2] = datalayer.battery.status.max_charge_current_dA & 0xff;
|
||||||
|
SOLARK_351.data.u8[3] = datalayer.battery.status.max_charge_current_dA >> 8;
|
||||||
|
SOLARK_351.data.u8[4] = datalayer.battery.status.max_discharge_current_dA & 0xff;
|
||||||
|
SOLARK_351.data.u8[5] = datalayer.battery.status.max_discharge_current_dA >> 8;
|
||||||
|
SOLARK_351.data.u8[6] = datalayer.battery.info.min_design_voltage_dV & 0xff;
|
||||||
|
SOLARK_351.data.u8[7] = datalayer.battery.info.min_design_voltage_dV >> 8;
|
||||||
|
|
||||||
|
SOLARK_355.data.u8[0] = (datalayer.battery.status.reported_soc / 100) & 0xff;
|
||||||
|
SOLARK_355.data.u8[1] = (datalayer.battery.status.reported_soc / 100) >> 8;
|
||||||
|
SOLARK_355.data.u8[2] = (datalayer.battery.status.soh_pptt / 100) & 0xff;
|
||||||
|
SOLARK_355.data.u8[3] = (datalayer.battery.status.soh_pptt / 100) >> 8;
|
||||||
|
|
||||||
|
int16_t average_temperature =
|
||||||
|
(datalayer.battery.status.temperature_min_dC + datalayer.battery.status.temperature_max_dC) / 2;
|
||||||
|
SOLARK_356.data.u8[0] = datalayer.battery.status.voltage_dV & 0xff;
|
||||||
|
SOLARK_356.data.u8[1] = datalayer.battery.status.voltage_dV >> 8;
|
||||||
|
SOLARK_356.data.u8[2] = datalayer.battery.status.current_dA & 0xff;
|
||||||
|
SOLARK_356.data.u8[3] = datalayer.battery.status.current_dA >> 8;
|
||||||
|
SOLARK_356.data.u8[4] = average_temperature & 0xff;
|
||||||
|
SOLARK_356.data.u8[5] = average_temperature >> 8;
|
||||||
|
|
||||||
|
// initialize all errors and warnings to 0
|
||||||
|
SOLARK_359.data.u8[0] = 0x00; //Protection byte 1
|
||||||
|
SOLARK_359.data.u8[1] = 0x00; //Protection byte 2
|
||||||
|
SOLARK_359.data.u8[2] = 0x00; // Alarm byte 1
|
||||||
|
SOLARK_359.data.u8[3] = 0x00; // Alarm byte 2
|
||||||
|
SOLARK_359.data.u8[4] = MODULE_NUMBER;
|
||||||
|
SOLARK_359.data.u8[5] = 0x50; //P
|
||||||
|
SOLARK_359.data.u8[6] = 0x4E; //N
|
||||||
|
SOLARK_359.data.u8[7] = 0x00; //Unused, should be 00
|
||||||
|
|
||||||
|
// Protection Byte 1 Bitfield: (If a bit is set, one of these caused batt self-protection mode)
|
||||||
|
if (datalayer.battery.status.current_dA >= (datalayer.battery.status.max_discharge_current_dA + 50))
|
||||||
|
SOLARK_359.data.u8[0] |= 0x80;
|
||||||
|
if (datalayer.battery.status.temperature_min_dC <= BATTERY_MINTEMPERATURE)
|
||||||
|
SOLARK_359.data.u8[0] |= 0x10;
|
||||||
|
if (datalayer.battery.status.temperature_max_dC >= BATTERY_MAXTEMPERATURE)
|
||||||
|
SOLARK_359.data.u8[0] |= 0x0C;
|
||||||
|
if (datalayer.battery.status.voltage_dV <= datalayer.battery.info.min_design_voltage_dV)
|
||||||
|
SOLARK_359.data.u8[0] |= 0x04;
|
||||||
|
if (datalayer.battery.status.bms_status == FAULT)
|
||||||
|
SOLARK_359.data.u8[1] |= 0x80;
|
||||||
|
if (datalayer.battery.status.current_dA <= -1 * datalayer.battery.status.max_charge_current_dA)
|
||||||
|
SOLARK_359.data.u8[1] |= 0x01;
|
||||||
|
|
||||||
|
// WARNINGS (using same rules as errors but reporting earlier)
|
||||||
|
// TODO: Not enabled in this integration yet. See Pylon protocol for example integration
|
||||||
|
|
||||||
|
SOLARK_35C.data.u8[0] = 0xC0; // enable charging and discharging
|
||||||
|
if (datalayer.battery.status.bms_status == FAULT)
|
||||||
|
SOLARK_35C.data.u8[0] = 0x00; // disable all
|
||||||
|
else if (datalayer.battery.settings.user_set_voltage_limits_active &&
|
||||||
|
datalayer.battery.status.voltage_dV > datalayer.battery.settings.max_user_set_charge_voltage_dV)
|
||||||
|
SOLARK_35C.data.u8[0] = 0x40; // only allow discharging
|
||||||
|
else if (datalayer.battery.settings.user_set_voltage_limits_active &&
|
||||||
|
datalayer.battery.status.voltage_dV < datalayer.battery.settings.max_user_set_discharge_voltage_dV)
|
||||||
|
SOLARK_35C.data.u8[0] = 0xA0; // enable charing, set charge immediately
|
||||||
|
else if (datalayer.battery.status.real_soc <= datalayer.battery.settings.min_percentage)
|
||||||
|
SOLARK_35C.data.u8[0] = 0xA0; // enable charing, set charge immediately
|
||||||
|
else if (datalayer.battery.status.real_soc >= datalayer.battery.settings.max_percentage)
|
||||||
|
SOLARK_35C.data.u8[0] = 0x40; // enable discharging only
|
||||||
|
|
||||||
|
// SOLARK_35E is pre-filled with the manufacturer name (BAT-EMU)
|
||||||
|
}
|
||||||
|
|
||||||
|
void SolArkLvInverter::map_can_frame_to_variable(CAN_frame rx_frame) {
|
||||||
|
switch (rx_frame.ID) {
|
||||||
|
case 0x305: //Message originating from inverter, signalling that data rec OK
|
||||||
|
datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SolArkLvInverter::transmit_can(unsigned long currentMillis) {
|
||||||
|
|
||||||
|
if (currentMillis - previousMillis1000ms >= INTERVAL_1_S) {
|
||||||
|
previousMillis1000ms = currentMillis;
|
||||||
|
|
||||||
|
transmit_can_frame(&SOLARK_351);
|
||||||
|
transmit_can_frame(&SOLARK_355);
|
||||||
|
transmit_can_frame(&SOLARK_356);
|
||||||
|
transmit_can_frame(&SOLARK_359);
|
||||||
|
transmit_can_frame(&SOLARK_35C);
|
||||||
|
transmit_can_frame(&SOLARK_35E);
|
||||||
|
}
|
||||||
|
}
|
55
Software/src/inverter/SOL-ARK-LV-CAN.h
Normal file
55
Software/src/inverter/SOL-ARK-LV-CAN.h
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#ifndef SOL_ARK_LV_CAN_H
|
||||||
|
#define SOL_ARK_LV_CAN_H
|
||||||
|
|
||||||
|
#include "CanInverterProtocol.h"
|
||||||
|
|
||||||
|
#ifdef SOL_ARK_LV_CAN
|
||||||
|
#define SELECTED_INVERTER_CLASS SolArkLvInverter
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class SolArkLvInverter : public CanInverterProtocol {
|
||||||
|
public:
|
||||||
|
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 = "Sol-Ark LV protocol over CAN bus";
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned long previousMillis1000ms = 0;
|
||||||
|
|
||||||
|
const uint8_t MODULE_NUMBER = 1; //8-bit integer representing quantity of parallel connected batteries
|
||||||
|
|
||||||
|
CAN_frame SOLARK_359 = {.FD = false,
|
||||||
|
.ext_ID = false,
|
||||||
|
.DLC = 8,
|
||||||
|
.ID = 0x359,
|
||||||
|
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||||
|
CAN_frame SOLARK_351 = {.FD = false,
|
||||||
|
.ext_ID = false,
|
||||||
|
.DLC = 8,
|
||||||
|
.ID = 0x351,
|
||||||
|
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||||
|
CAN_frame SOLARK_355 = {.FD = false,
|
||||||
|
.ext_ID = false,
|
||||||
|
.DLC = 8,
|
||||||
|
.ID = 0x355,
|
||||||
|
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||||
|
CAN_frame SOLARK_356 = {.FD = false,
|
||||||
|
.ext_ID = false,
|
||||||
|
.DLC = 8,
|
||||||
|
.ID = 0x356,
|
||||||
|
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||||
|
CAN_frame SOLARK_35C = {.FD = false,
|
||||||
|
.ext_ID = false,
|
||||||
|
.DLC = 8,
|
||||||
|
.ID = 0x35C,
|
||||||
|
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
|
||||||
|
CAN_frame SOLARK_35E = {.FD = false,
|
||||||
|
.ext_ID = false,
|
||||||
|
.DLC = 8,
|
||||||
|
.ID = 0x35E, //BAT-EMU
|
||||||
|
.data = {0x42, 0x41, 0x54, 0x2D, 0x45, 0x4D, 0x55, 0x20}};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
Loading…
Add table
Add a link
Reference in a new issue