Fix: properly handle fragmented VE.Direct messages

queue every text event until the frame was checked by it checksum. then
process the data directly into the buffer struct. do not clear the
buffer struct, so it will always include the most recent value of a
particular data point.
This commit is contained in:
Bernhard Kirchen 2024-03-29 20:01:14 +01:00 committed by Bernhard Kirchen
parent 187f197e32
commit ad125ea804
12 changed files with 549 additions and 496 deletions

View File

@ -147,7 +147,7 @@ class VictronSmartShuntStats : public BatteryStats {
void getLiveViewData(JsonVariant& root) const final; void getLiveViewData(JsonVariant& root) const final;
void mqttPublish() const final; void mqttPublish() const final;
void updateFrom(VeDirectShuntController::veShuntStruct const& shuntData); void updateFrom(VeDirectShuntController::data_t const& shuntData);
private: private:
float _current; float _current;

View File

@ -21,7 +21,7 @@ public:
void forceUpdate(); void forceUpdate();
private: private:
void loop(); void loop();
std::map<std::string, VeDirectMpptController::veMpptStruct> _kvFrames; std::map<std::string, VeDirectMpptController::data_t> _kvFrames;
Task _loopTask; Task _loopTask;
@ -34,7 +34,7 @@ private:
bool _PublishFull; bool _PublishFull;
void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData,
VeDirectMpptController::veMpptStruct &frame) const; const VeDirectMpptController::data_t &frame) const;
}; };
extern MqttHandleVedirectClass MqttHandleVedirect; extern MqttHandleVedirectClass MqttHandleVedirect;

View File

@ -0,0 +1,224 @@
#include "VeDirectData.h"
template<typename T, size_t L>
static frozen::string const& getAsString(frozen::map<T, frozen::string, L> const& values, T val)
{
auto pos = values.find(val);
if (pos == values.end()) {
static constexpr frozen::string dummy("???");
return dummy;
}
return pos->second;
}
/*
* This function returns the product id (PID) as readable text.
*/
frozen::string const& veStruct::getPidAsString() const
{
/**
* this map is rendered from [1], which is more recent than [2]. Phoenix
* inverters are not included in the map. unfortunately, the documents do
* not fully align. PID 0xA07F is only present in [1]. PIDs 0xA048, 0xA110,
* and 0xA111 are only present in [2]. PIDs 0xA06D and 0xA078 are rev3 in
* [1] but rev2 in [2].
*
* [1] https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf
* [2] https://www.victronenergy.com/upload/documents/BlueSolar-HEX-protocol.pdf
*/
static constexpr frozen::map<uint16_t, frozen::string, 105> values = {
{ 0x0203, "BMV-700" },
{ 0x0204, "BMV-702" },
{ 0x0205, "BMV-700H" },
{ 0x0300, "BlueSolar MPPT 70|15" },
{ 0xA040, "BlueSolar MPPT 75|50" },
{ 0xA041, "BlueSolar MPPT 150|35" },
{ 0xA042, "BlueSolar MPPT 75|15" },
{ 0xA043, "BlueSolar MPPT 100|15" },
{ 0xA044, "BlueSolar MPPT 100|30" },
{ 0xA045, "BlueSolar MPPT 100|50" },
{ 0xA046, "BlueSolar MPPT 150|70" },
{ 0xA047, "BlueSolar MPPT 150|100" },
{ 0xA048, "BlueSolar MPPT 75|50 rev2" },
{ 0xA049, "BlueSolar MPPT 100|50 rev2" },
{ 0xA04A, "BlueSolar MPPT 100|30 rev2" },
{ 0xA04B, "BlueSolar MPPT 150|35 rev2" },
{ 0xA04C, "BlueSolar MPPT 75|10" },
{ 0xA04D, "BlueSolar MPPT 150|45" },
{ 0xA04E, "BlueSolar MPPT 150|60" },
{ 0xA04F, "BlueSolar MPPT 150|85" },
{ 0xA050, "SmartSolar MPPT 250|100" },
{ 0xA051, "SmartSolar MPPT 150|100" },
{ 0xA052, "SmartSolar MPPT 150|85" },
{ 0xA053, "SmartSolar MPPT 75|15" },
{ 0xA054, "SmartSolar MPPT 75|10" },
{ 0xA055, "SmartSolar MPPT 100|15" },
{ 0xA056, "SmartSolar MPPT 100|30" },
{ 0xA057, "SmartSolar MPPT 100|50" },
{ 0xA058, "SmartSolar MPPT 150|35" },
{ 0xA059, "SmartSolar MPPT 150|100 rev2" },
{ 0xA05A, "SmartSolar MPPT 150|85 rev2" },
{ 0xA05B, "SmartSolar MPPT 250|70" },
{ 0xA05C, "SmartSolar MPPT 250|85" },
{ 0xA05D, "SmartSolar MPPT 250|60" },
{ 0xA05E, "SmartSolar MPPT 250|45" },
{ 0xA05F, "SmartSolar MPPT 100|20" },
{ 0xA060, "SmartSolar MPPT 100|20 48V" },
{ 0xA061, "SmartSolar MPPT 150|45" },
{ 0xA062, "SmartSolar MPPT 150|60" },
{ 0xA063, "SmartSolar MPPT 150|70" },
{ 0xA064, "SmartSolar MPPT 250|85 rev2" },
{ 0xA065, "SmartSolar MPPT 250|100 rev2" },
{ 0xA066, "BlueSolar MPPT 100|20" },
{ 0xA067, "BlueSolar MPPT 100|20 48V" },
{ 0xA068, "SmartSolar MPPT 250|60 rev2" },
{ 0xA069, "SmartSolar MPPT 250|70 rev2" },
{ 0xA06A, "SmartSolar MPPT 150|45 rev2" },
{ 0xA06B, "SmartSolar MPPT 150|60 rev2" },
{ 0xA06C, "SmartSolar MPPT 150|70 rev2" },
{ 0xA06D, "SmartSolar MPPT 150|85 rev3" },
{ 0xA06E, "SmartSolar MPPT 150|100 rev3" },
{ 0xA06F, "BlueSolar MPPT 150|45 rev2" },
{ 0xA070, "BlueSolar MPPT 150|60 rev2" },
{ 0xA071, "BlueSolar MPPT 150|70 rev2" },
{ 0xA072, "BlueSolar MPPT 150|45 rev3" },
{ 0xA073, "SmartSolar MPPT 150|45 rev3" },
{ 0xA074, "SmartSolar MPPT 75|10 rev2" },
{ 0xA075, "SmartSolar MPPT 75|15 rev2" },
{ 0xA076, "BlueSolar MPPT 100|30 rev3" },
{ 0xA077, "BlueSolar MPPT 100|50 rev3" },
{ 0xA078, "BlueSolar MPPT 150|35 rev3" },
{ 0xA079, "BlueSolar MPPT 75|10 rev2" },
{ 0xA07A, "BlueSolar MPPT 75|15 rev2" },
{ 0xA07B, "BlueSolar MPPT 100|15 rev2" },
{ 0xA07C, "BlueSolar MPPT 75|10 rev3" },
{ 0xA07D, "BlueSolar MPPT 75|15 rev3" },
{ 0xA07E, "SmartSolar MPPT 100|30 12V" },
{ 0xA07F, "All-In-1 SmartSolar MPPT 75|15 12V" },
{ 0xA102, "SmartSolar MPPT VE.Can 150|70" },
{ 0xA103, "SmartSolar MPPT VE.Can 150|45" },
{ 0xA104, "SmartSolar MPPT VE.Can 150|60" },
{ 0xA105, "SmartSolar MPPT VE.Can 150|85" },
{ 0xA106, "SmartSolar MPPT VE.Can 150|100" },
{ 0xA107, "SmartSolar MPPT VE.Can 250|45" },
{ 0xA108, "SmartSolar MPPT VE.Can 250|60" },
{ 0xA109, "SmartSolar MPPT VE.Can 250|70" },
{ 0xA10A, "SmartSolar MPPT VE.Can 250|85" },
{ 0xA10B, "SmartSolar MPPT VE.Can 250|100" },
{ 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" },
{ 0xA10D, "SmartSolar MPPT VE.Can 150|85 rev2" },
{ 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" },
{ 0xA10F, "BlueSolar MPPT VE.Can 150|100" },
{ 0xA110, "SmartSolar MPPT RS 450|100" },
{ 0xA111, "SmartSolar MPPT RS 450|200" },
{ 0xA112, "BlueSolar MPPT VE.Can 250|70" },
{ 0xA113, "BlueSolar MPPT VE.Can 250|100" },
{ 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" },
{ 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" },
{ 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" },
{ 0xA117, "BlueSolar MPPT VE.Can 150|100 rev2" },
{ 0xA340, "Phoenix Smart IP43 Charger 12|50 (1+1)" },
{ 0xA341, "Phoenix Smart IP43 Charger 12|50 (3)" },
{ 0xA342, "Phoenix Smart IP43 Charger 24|25 (1+1)" },
{ 0xA343, "Phoenix Smart IP43 Charger 24|25 (3)" },
{ 0xA344, "Phoenix Smart IP43 Charger 12|30 (1+1)" },
{ 0xA345, "Phoenix Smart IP43 Charger 12|30 (3)" },
{ 0xA346, "Phoenix Smart IP43 Charger 24|16 (1+1)" },
{ 0xA347, "Phoenix Smart IP43 Charger 24|16 (3)" },
{ 0xA381, "BMV-712 Smart" },
{ 0xA382, "BMV-710H Smart" },
{ 0xA383, "BMV-712 Smart Rev2" },
{ 0xA389, "SmartShunt 500A/50mV" },
{ 0xA38A, "SmartShunt 1000A/50mV" },
{ 0xA38B, "SmartShunt 2000A/50mV" },
{ 0xA3F0, "Smart BuckBoost 12V/12V-50A" },
};
return getAsString(values, PID);
}
/*
* This function returns the state of operations (CS) as readable text.
*/
frozen::string const& veMpptStruct::getCsAsString() const
{
static constexpr frozen::map<uint8_t, frozen::string, 9> values = {
{ 0, "OFF" },
{ 2, "Fault" },
{ 3, "Bulk" },
{ 4, "Absorbtion" },
{ 5, "Float" },
{ 7, "Equalize (manual)" },
{ 245, "Starting-up" },
{ 247, "Auto equalize / Recondition" },
{ 252, "External Control" }
};
return getAsString(values, CS);
}
/*
* This function returns the state of MPPT (MPPT) as readable text.
*/
frozen::string const& veMpptStruct::getMpptAsString() const
{
static constexpr frozen::map<uint8_t, frozen::string, 3> values = {
{ 0, "OFF" },
{ 1, "Voltage or current limited" },
{ 2, "MPP Tracker active" }
};
return getAsString(values, MPPT);
}
/*
* This function returns error state (ERR) as readable text.
*/
frozen::string const& veMpptStruct::getErrAsString() const
{
static constexpr frozen::map<uint8_t, frozen::string, 20> values = {
{ 0, "No error" },
{ 2, "Battery voltage too high" },
{ 17, "Charger temperature too high" },
{ 18, "Charger over current" },
{ 19, "Charger current reversed" },
{ 20, "Bulk time limit exceeded" },
{ 21, "Current sensor issue(sensor bias/sensor broken)" },
{ 26, "Terminals overheated" },
{ 28, "Converter issue (dual converter models only)" },
{ 33, "Input voltage too high (solar panel)" },
{ 34, "Input current too high (solar panel)" },
{ 38, "Input shutdown (due to excessive battery voltage)" },
{ 39, "Input shutdown (due to current flow during off mode)" },
{ 40, "Input" },
{ 65, "Lost communication with one of devices" },
{ 67, "Synchronisedcharging device configuration issue" },
{ 68, "BMS connection lost" },
{ 116, "Factory calibration data lost" },
{ 117, "Invalid/incompatible firmware" },
{ 118, "User settings invalid" }
};
return getAsString(values, ERR);
}
/*
* This function returns the off reason (OR) as readable text.
*/
frozen::string const& veMpptStruct::getOrAsString() const
{
static constexpr frozen::map<uint32_t, frozen::string, 10> values = {
{ 0x00000000, "Not off" },
{ 0x00000001, "No input power" },
{ 0x00000002, "Switched off (power switch)" },
{ 0x00000004, "Switched off (device moderegister)" },
{ 0x00000008, "Remote input" },
{ 0x00000010, "Protection active" },
{ 0x00000020, "Paygo" },
{ 0x00000040, "BMS" },
{ 0x00000080, "Engine shutdown detection" },
{ 0x00000100, "Analysing input voltage" }
};
return getAsString(values, OR);
}

View File

@ -0,0 +1,70 @@
#pragma once
#include <frozen/string.h>
#include <frozen/map.h>
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0
#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer
typedef struct {
uint16_t PID = 0; // product id
char SER[VE_MAX_VALUE_LEN]; // serial number
char FW[VE_MAX_VALUE_LEN]; // firmware release number
double V = 0; // battery voltage in V
double I = 0; // battery current in A
double E = 0; // efficiency in percent (calculated, moving average)
frozen::string const& getPidAsString() const; // product ID as string
} veStruct;
struct veMpptStruct : veStruct {
uint8_t MPPT; // state of MPP tracker
int32_t PPV; // panel power in W
int32_t P; // battery output power in W (calculated)
double VPV; // panel voltage in V
double IPV; // panel current in A (calculated)
bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
uint8_t CS; // current state of operation e.g. OFF or Bulk
uint8_t ERR; // error code
uint32_t OR; // off reason
uint32_t HSDS; // day sequence number 1...365
double H19; // yield total kWh
double H20; // yield today kWh
int32_t H21; // maximum power today W
double H22; // yield yesterday kWh
int32_t H23; // maximum power yesterday W
frozen::string const& getMpptAsString() const; // state of mppt as string
frozen::string const& getCsAsString() const; // current state as string
frozen::string const& getErrAsString() const; // error state as string
frozen::string const& getOrAsString() const; // off reason as string
};
struct veShuntStruct : veStruct {
int32_t T; // Battery temperature
bool tempPresent; // Battery temperature sensor is attached to the shunt
int32_t P; // Instantaneous power
int32_t CE; // Consumed Amp Hours
int32_t SOC; // State-of-charge
uint32_t TTG; // Time-to-go
bool ALARM; // Alarm condition active
uint32_t AR; // Alarm Reason
int32_t H1; // Depth of the deepest discharge
int32_t H2; // Depth of the last discharge
int32_t H3; // Depth of the average discharge
int32_t H4; // Number of charge cycles
int32_t H5; // Number of full discharges
int32_t H6; // Cumulative Amp Hours drawn
int32_t H7; // Minimum main (battery) voltage
int32_t H8; // Maximum main (battery) voltage
int32_t H9; // Number of seconds since last full charge
int32_t H10; // Number of automatic synchronizations
int32_t H11; // Number of low main voltage alarms
int32_t H12; // Number of high main voltage alarms
int32_t H13; // Number of low auxiliary voltage alarms
int32_t H14; // Number of high auxiliary voltage alarms
int32_t H15; // Minimum auxiliary (battery) voltage
int32_t H16; // Maximum auxiliary (battery) voltage
int32_t H17; // Amount of discharged energy
int32_t H18; // Amount of charged energy
};

View File

@ -58,7 +58,8 @@ class Silent : public Print {
static Silent MessageOutputDummy; static Silent MessageOutputDummy;
VeDirectFrameHandler::VeDirectFrameHandler() : template<typename T>
VeDirectFrameHandler<T>::VeDirectFrameHandler() :
_msgOut(&MessageOutputDummy), _msgOut(&MessageOutputDummy),
_lastUpdate(0), _lastUpdate(0),
_state(IDLE), _state(IDLE),
@ -72,7 +73,8 @@ VeDirectFrameHandler::VeDirectFrameHandler() :
{ {
} }
void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) template<typename T>
void VeDirectFrameHandler<T>::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
{ {
_vedirectSerial = std::make_unique<HardwareSerial>(hwSerialPort); _vedirectSerial = std::make_unique<HardwareSerial>(hwSerialPort);
_vedirectSerial->begin(19200, SERIAL_8N1, rx, tx); _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx);
@ -84,7 +86,8 @@ void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* ms
if (_verboseLogging) { _msgOut->printf("%s init complete\r\n", _logId); } if (_verboseLogging) { _msgOut->printf("%s init complete\r\n", _logId); }
} }
void VeDirectFrameHandler::dumpDebugBuffer() { template<typename T>
void VeDirectFrameHandler<T>::dumpDebugBuffer() {
_msgOut->printf("%s serial input (%d Bytes):", _logId, _debugIn); _msgOut->printf("%s serial input (%d Bytes):", _logId, _debugIn);
for (int i = 0; i < _debugIn; ++i) { for (int i = 0; i < _debugIn; ++i) {
if (i % 16 == 0) { if (i % 16 == 0) {
@ -96,7 +99,16 @@ void VeDirectFrameHandler::dumpDebugBuffer() {
_debugIn = 0; _debugIn = 0;
} }
void VeDirectFrameHandler::loop() template<typename T>
void VeDirectFrameHandler<T>::reset()
{
_checksum = 0;
_state = IDLE;
_textData.clear();
}
template<typename T>
void VeDirectFrameHandler<T>::loop()
{ {
while ( _vedirectSerial->available()) { while ( _vedirectSerial->available()) {
rxData(_vedirectSerial->read()); rxData(_vedirectSerial->read());
@ -109,8 +121,7 @@ void VeDirectFrameHandler::loop()
if (IDLE != _state && _lastByteMillis + 500 < millis()) { if (IDLE != _state && _lastByteMillis + 500 < millis()) {
_msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", _logId, _state); _msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", _logId, _state);
if (_verboseLogging) { dumpDebugBuffer(); } if (_verboseLogging) { dumpDebugBuffer(); }
_checksum = 0; reset();
_state = IDLE;
} }
} }
@ -119,7 +130,8 @@ void VeDirectFrameHandler::loop()
* This function is called by loop() which passes a byte of serial data * This function is called by loop() which passes a byte of serial data
* Based on Victron's example code. But using String and Map instead of pointer and arrays * Based on Victron's example code. But using String and Map instead of pointer and arrays
*/ */
void VeDirectFrameHandler::rxData(uint8_t inbyte) template<typename T>
void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
{ {
if (_verboseLogging) { if (_verboseLogging) {
_debugBuffer[_debugIn] = inbyte; _debugBuffer[_debugIn] = inbyte;
@ -186,7 +198,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
case '\n': case '\n':
if ( _textPointer < (_value + sizeof(_value)) ) { if ( _textPointer < (_value + sizeof(_value)) ) {
*_textPointer = 0; // make zero ended *_textPointer = 0; // make zero ended
textRxEvent(_name, _value); _textData.push_back({_name, _value});
} }
_state = RECORD_BEGIN; _state = RECORD_BEGIN;
break; break;
@ -201,14 +213,18 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
break; break;
case CHECKSUM: case CHECKSUM:
{ {
bool valid = _checksum == 0;
if (!valid) {
_msgOut->printf("%s checksum 0x%02x != 0, invalid frame\r\n", _logId, _checksum);
}
if (_verboseLogging) { dumpDebugBuffer(); } if (_verboseLogging) { dumpDebugBuffer(); }
_checksum = 0; if (_checksum == 0) {
_state = IDLE; for (auto const& event : _textData) {
if (valid) { frameValidEvent(); } processTextData(event.first, event.second);
}
_lastUpdate = millis();
frameValidEvent();
}
else {
_msgOut->printf("%s checksum 0x%02x != 0x00, invalid frame\r\n", _logId, _checksum);
}
reset();
break; break;
} }
case RECORD_HEX: case RECORD_HEX:
@ -218,41 +234,44 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
} }
/* /*
* textRxEvent
* This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer. * This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer.
*/ */
bool VeDirectFrameHandler::textRxEvent(char* name, char* value, veStruct& frame) { template<typename T>
void VeDirectFrameHandler<T>::processTextData(std::string const& name, std::string const& value) {
if (_verboseLogging) { if (_verboseLogging) {
_msgOut->printf("%s Text Event %s: Value: %s\r\n", _msgOut->printf("%s Text Data '%s' = '%s'\r\n",
_logId, name, value ); _logId, name.c_str(), value.c_str());
} }
if (strcmp(name, "PID") == 0) { if (processTextDataDerived(name, value)) { return; }
frame.PID = strtol(value, nullptr, 0);
return true; if (name == "PID") {
_tmpFrame.PID = strtol(value.c_str(), nullptr, 0);
return;
} }
if (strcmp(name, "SER") == 0) { if (name == "SER") {
strcpy(frame.SER, value); strcpy(_tmpFrame.SER, value.c_str());
return true; return;
} }
if (strcmp(name, "FW") == 0) { if (name == "FW") {
strcpy(frame.FW, value); strcpy(_tmpFrame.FW, value.c_str());
return true; return;
} }
if (strcmp(name, "V") == 0) { if (name == "V") {
frame.V = round(atof(value) / 10.0) / 100.0; _tmpFrame.V = round(atof(value.c_str()) / 10.0) / 100.0;
return true; return;
} }
if (strcmp(name, "I") == 0) { if (name == "I") {
frame.I = round(atof(value) / 10.0) / 100.0; _tmpFrame.I = round(atof(value.c_str()) / 10.0) / 100.0;
return true; return;
} }
return false; _msgOut->printf("%s Unknown text data '%s' (value '%s')\r\n",
_logId, name.c_str(), value.c_str());
} }
@ -261,7 +280,9 @@ bool VeDirectFrameHandler::textRxEvent(char* name, char* value, veStruct& frame)
* hexRxEvent * hexRxEvent
* This function records hex answers or async messages * This function records hex answers or async messages
*/ */
int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { template<typename T>
int VeDirectFrameHandler<T>::hexRxEvent(uint8_t inbyte)
{
int ret=RECORD_HEX; // default - continue recording until end of frame int ret=RECORD_HEX; // default - continue recording until end of frame
switch (inbyte) { switch (inbyte) {
@ -282,138 +303,14 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
return ret; return ret;
} }
bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const { template<typename T>
return strlen(frame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000); bool VeDirectFrameHandler<T>::isDataValid() const
{
return strlen(_tmpFrame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000);
} }
uint32_t VeDirectFrameHandler::getLastUpdate() const template<typename T>
uint32_t VeDirectFrameHandler<T>::getLastUpdate() const
{ {
return _lastUpdate; return _lastUpdate;
} }
/*
* getPidAsString
* This function returns the product id (PID) as readable text.
*/
frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
{
/**
* this map is rendered from [1], which is more recent than [2]. Phoenix
* inverters are not included in the map. unfortunately, the documents do
* not fully align. PID 0xA07F is only present in [1]. PIDs 0xA048, 0xA110,
* and 0xA111 are only present in [2]. PIDs 0xA06D and 0xA078 are rev3 in
* [1] but rev2 in [2].
*
* [1] https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf
* [2] https://www.victronenergy.com/upload/documents/BlueSolar-HEX-protocol.pdf
*/
static constexpr frozen::map<uint16_t, frozen::string, 105> values = {
{ 0x0203, "BMV-700" },
{ 0x0204, "BMV-702" },
{ 0x0205, "BMV-700H" },
{ 0x0300, "BlueSolar MPPT 70|15" },
{ 0xA040, "BlueSolar MPPT 75|50" },
{ 0xA041, "BlueSolar MPPT 150|35" },
{ 0xA042, "BlueSolar MPPT 75|15" },
{ 0xA043, "BlueSolar MPPT 100|15" },
{ 0xA044, "BlueSolar MPPT 100|30" },
{ 0xA045, "BlueSolar MPPT 100|50" },
{ 0xA046, "BlueSolar MPPT 150|70" },
{ 0xA047, "BlueSolar MPPT 150|100" },
{ 0xA048, "BlueSolar MPPT 75|50 rev2" },
{ 0xA049, "BlueSolar MPPT 100|50 rev2" },
{ 0xA04A, "BlueSolar MPPT 100|30 rev2" },
{ 0xA04B, "BlueSolar MPPT 150|35 rev2" },
{ 0xA04C, "BlueSolar MPPT 75|10" },
{ 0xA04D, "BlueSolar MPPT 150|45" },
{ 0xA04E, "BlueSolar MPPT 150|60" },
{ 0xA04F, "BlueSolar MPPT 150|85" },
{ 0xA050, "SmartSolar MPPT 250|100" },
{ 0xA051, "SmartSolar MPPT 150|100" },
{ 0xA052, "SmartSolar MPPT 150|85" },
{ 0xA053, "SmartSolar MPPT 75|15" },
{ 0xA054, "SmartSolar MPPT 75|10" },
{ 0xA055, "SmartSolar MPPT 100|15" },
{ 0xA056, "SmartSolar MPPT 100|30" },
{ 0xA057, "SmartSolar MPPT 100|50" },
{ 0xA058, "SmartSolar MPPT 150|35" },
{ 0xA059, "SmartSolar MPPT 150|100 rev2" },
{ 0xA05A, "SmartSolar MPPT 150|85 rev2" },
{ 0xA05B, "SmartSolar MPPT 250|70" },
{ 0xA05C, "SmartSolar MPPT 250|85" },
{ 0xA05D, "SmartSolar MPPT 250|60" },
{ 0xA05E, "SmartSolar MPPT 250|45" },
{ 0xA05F, "SmartSolar MPPT 100|20" },
{ 0xA060, "SmartSolar MPPT 100|20 48V" },
{ 0xA061, "SmartSolar MPPT 150|45" },
{ 0xA062, "SmartSolar MPPT 150|60" },
{ 0xA063, "SmartSolar MPPT 150|70" },
{ 0xA064, "SmartSolar MPPT 250|85 rev2" },
{ 0xA065, "SmartSolar MPPT 250|100 rev2" },
{ 0xA066, "BlueSolar MPPT 100|20" },
{ 0xA067, "BlueSolar MPPT 100|20 48V" },
{ 0xA068, "SmartSolar MPPT 250|60 rev2" },
{ 0xA069, "SmartSolar MPPT 250|70 rev2" },
{ 0xA06A, "SmartSolar MPPT 150|45 rev2" },
{ 0xA06B, "SmartSolar MPPT 150|60 rev2" },
{ 0xA06C, "SmartSolar MPPT 150|70 rev2" },
{ 0xA06D, "SmartSolar MPPT 150|85 rev3" },
{ 0xA06E, "SmartSolar MPPT 150|100 rev3" },
{ 0xA06F, "BlueSolar MPPT 150|45 rev2" },
{ 0xA070, "BlueSolar MPPT 150|60 rev2" },
{ 0xA071, "BlueSolar MPPT 150|70 rev2" },
{ 0xA072, "BlueSolar MPPT 150|45 rev3" },
{ 0xA073, "SmartSolar MPPT 150|45 rev3" },
{ 0xA074, "SmartSolar MPPT 75|10 rev2" },
{ 0xA075, "SmartSolar MPPT 75|15 rev2" },
{ 0xA076, "BlueSolar MPPT 100|30 rev3" },
{ 0xA077, "BlueSolar MPPT 100|50 rev3" },
{ 0xA078, "BlueSolar MPPT 150|35 rev3" },
{ 0xA079, "BlueSolar MPPT 75|10 rev2" },
{ 0xA07A, "BlueSolar MPPT 75|15 rev2" },
{ 0xA07B, "BlueSolar MPPT 100|15 rev2" },
{ 0xA07C, "BlueSolar MPPT 75|10 rev3" },
{ 0xA07D, "BlueSolar MPPT 75|15 rev3" },
{ 0xA07E, "SmartSolar MPPT 100|30 12V" },
{ 0xA07F, "All-In-1 SmartSolar MPPT 75|15 12V" },
{ 0xA102, "SmartSolar MPPT VE.Can 150|70" },
{ 0xA103, "SmartSolar MPPT VE.Can 150|45" },
{ 0xA104, "SmartSolar MPPT VE.Can 150|60" },
{ 0xA105, "SmartSolar MPPT VE.Can 150|85" },
{ 0xA106, "SmartSolar MPPT VE.Can 150|100" },
{ 0xA107, "SmartSolar MPPT VE.Can 250|45" },
{ 0xA108, "SmartSolar MPPT VE.Can 250|60" },
{ 0xA109, "SmartSolar MPPT VE.Can 250|70" },
{ 0xA10A, "SmartSolar MPPT VE.Can 250|85" },
{ 0xA10B, "SmartSolar MPPT VE.Can 250|100" },
{ 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" },
{ 0xA10D, "SmartSolar MPPT VE.Can 150|85 rev2" },
{ 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" },
{ 0xA10F, "BlueSolar MPPT VE.Can 150|100" },
{ 0xA110, "SmartSolar MPPT RS 450|100" },
{ 0xA111, "SmartSolar MPPT RS 450|200" },
{ 0xA112, "BlueSolar MPPT VE.Can 250|70" },
{ 0xA113, "BlueSolar MPPT VE.Can 250|100" },
{ 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" },
{ 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" },
{ 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" },
{ 0xA117, "BlueSolar MPPT VE.Can 150|100 rev2" },
{ 0xA340, "Phoenix Smart IP43 Charger 12|50 (1+1)" },
{ 0xA341, "Phoenix Smart IP43 Charger 12|50 (3)" },
{ 0xA342, "Phoenix Smart IP43 Charger 24|25 (1+1)" },
{ 0xA343, "Phoenix Smart IP43 Charger 24|25 (3)" },
{ 0xA344, "Phoenix Smart IP43 Charger 12|30 (1+1)" },
{ 0xA345, "Phoenix Smart IP43 Charger 12|30 (3)" },
{ 0xA346, "Phoenix Smart IP43 Charger 24|16 (1+1)" },
{ 0xA347, "Phoenix Smart IP43 Charger 24|16 (3)" },
{ 0xA381, "BMV-712 Smart" },
{ 0xA382, "BMV-710H Smart" },
{ 0xA383, "BMV-712 Smart Rev2" },
{ 0xA389, "SmartShunt 500A/50mV" },
{ 0xA38A, "SmartShunt 1000A/50mV" },
{ 0xA38B, "SmartShunt 2000A/50mV" },
{ 0xA3F0, "Smart BuckBoost 12V/12V-50A" },
};
return getAsString(values, PID);
}

View File

@ -13,17 +13,17 @@
#include <Arduino.h> #include <Arduino.h>
#include <array> #include <array>
#include <frozen/string.h>
#include <frozen/map.h>
#include <memory> #include <memory>
#include <utility>
#include <deque>
#include "VeDirectData.h"
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0 template<typename T>
#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer
class VeDirectFrameHandler { class VeDirectFrameHandler {
public: public:
void loop(); // main loop to read ve.direct data void loop(); // main loop to read ve.direct data
uint32_t getLastUpdate() const; // timestamp of last successful frame read uint32_t getLastUpdate() const; // timestamp of last successful frame read
bool isDataValid() const; // return true if data valid and not outdated
protected: protected:
VeDirectFrameHandler(); VeDirectFrameHandler();
@ -33,36 +33,14 @@ protected:
Print* _msgOut; Print* _msgOut;
uint32_t _lastUpdate; uint32_t _lastUpdate;
typedef struct { T _tmpFrame;
uint16_t PID = 0; // product id
char SER[VE_MAX_VALUE_LEN]; // serial number
char FW[VE_MAX_VALUE_LEN]; // firmware release number
double V = 0; // battery voltage in V
double I = 0; // battery current in A
double E = 0; // efficiency in percent (calculated, moving average)
frozen::string const& getPidAsString() const; // product ID as string
} veStruct;
bool textRxEvent(char* name, char* value, veStruct& frame);
bool isDataValid(veStruct const& frame) const; // return true if data valid and not outdated
template<typename T, size_t L>
static frozen::string const& getAsString(frozen::map<T, frozen::string, L> const& values, T val)
{
auto pos = values.find(val);
if (pos == values.end()) {
static constexpr frozen::string dummy("???");
return dummy;
}
return pos->second;
}
private: private:
void setLastUpdate(); // set timestampt after successful frame read void reset();
void dumpDebugBuffer(); void dumpDebugBuffer();
void rxData(uint8_t inbyte); // byte of serial data void rxData(uint8_t inbyte); // byte of serial data
virtual void textRxEvent(char *, char *) = 0; void processTextData(std::string const& name, std::string const& value);
virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0;
virtual void frameValidEvent() = 0; virtual void frameValidEvent() = 0;
int hexRxEvent(uint8_t); int hexRxEvent(uint8_t);
@ -78,4 +56,18 @@ private:
unsigned _debugIn; unsigned _debugIn;
uint32_t _lastByteMillis; uint32_t _lastByteMillis;
char _logId[32]; char _logId[32];
/**
* not every frame contains every value the device is communicating, i.e.,
* a set of values can be fragmented across multiple frames. frames can be
* invalid. in order to only process data from valid frames, we add data
* to this queue and only process it once the frame was found to be valid.
* this also handles fragmentation nicely, since there is no need to reset
* our data buffer. we simply update the interpreted data from this event
* queue, which is fine as we know the source frame was valid.
*/
std::deque<std::pair<std::string, std::string>> _textData;
}; };
template class VeDirectFrameHandler<veMpptStruct>;
template class VeDirectFrameHandler<veShuntStruct>;

View File

@ -7,58 +7,62 @@ void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verb
_spData = std::make_shared<veMpptStruct>(); _spData = std::make_shared<veMpptStruct>();
} }
bool VeDirectMpptController::isDataValid() const { bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value)
return VeDirectFrameHandler::isDataValid(*_spData);
}
void VeDirectMpptController::textRxEvent(char* name, char* value)
{ {
if (VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame)) { if (name == "LOAD") {
return; _tmpFrame.LOAD = (value == "ON");
return true;
}
if (name == "CS") {
_tmpFrame.CS = atoi(value.c_str());
return true;
}
if (name == "ERR") {
_tmpFrame.ERR = atoi(value.c_str());
return true;
}
if (name == "OR") {
_tmpFrame.OR = strtol(value.c_str(), nullptr, 0);
return true;
}
if (name == "MPPT") {
_tmpFrame.MPPT = atoi(value.c_str());
return true;
}
if (name == "HSDS") {
_tmpFrame.HSDS = atoi(value.c_str());
return true;
}
if (name == "VPV") {
_tmpFrame.VPV = round(atof(value.c_str()) / 10.0) / 100.0;
return true;
}
if (name == "PPV") {
_tmpFrame.PPV = atoi(value.c_str());
return true;
}
if (name == "H19") {
_tmpFrame.H19 = atof(value.c_str()) / 100.0;
return true;
}
if (name == "H20") {
_tmpFrame.H20 = atof(value.c_str()) / 100.0;
return true;
}
if (name == "H21") {
_tmpFrame.H21 = atoi(value.c_str());
return true;
}
if (name == "H22") {
_tmpFrame.H22 = atof(value.c_str()) / 100.0;
return true;
}
if (name == "H23") {
_tmpFrame.H23 = atoi(value.c_str());
return true;
} }
if (strcmp(name, "LOAD") == 0) { return false;
if (strcmp(value, "ON") == 0)
_tmpFrame.LOAD = true;
else
_tmpFrame.LOAD = false;
}
else if (strcmp(name, "CS") == 0) {
_tmpFrame.CS = atoi(value);
}
else if (strcmp(name, "ERR") == 0) {
_tmpFrame.ERR = atoi(value);
}
else if (strcmp(name, "OR") == 0) {
_tmpFrame.OR = strtol(value, nullptr, 0);
}
else if (strcmp(name, "MPPT") == 0) {
_tmpFrame.MPPT = atoi(value);
}
else if (strcmp(name, "HSDS") == 0) {
_tmpFrame.HSDS = atoi(value);
}
else if (strcmp(name, "VPV") == 0) {
_tmpFrame.VPV = round(atof(value) / 10.0) / 100.0;
}
else if (strcmp(name, "PPV") == 0) {
_tmpFrame.PPV = atoi(value);
}
else if (strcmp(name, "H19") == 0) {
_tmpFrame.H19 = atof(value) / 100.0;
}
else if (strcmp(name, "H20") == 0) {
_tmpFrame.H20 = atof(value) / 100.0;
}
else if (strcmp(name, "H21") == 0) {
_tmpFrame.H21 = atoi(value);
}
else if (strcmp(name, "H22") == 0) {
_tmpFrame.H22 = atof(value) / 100.0;
}
else if (strcmp(name, "H23") == 0) {
_tmpFrame.H23 = atoi(value);
}
} }
/* /*
@ -68,108 +72,14 @@ void VeDirectMpptController::textRxEvent(char* name, char* value)
void VeDirectMpptController::frameValidEvent() { void VeDirectMpptController::frameValidEvent() {
_tmpFrame.P = _tmpFrame.V * _tmpFrame.I; _tmpFrame.P = _tmpFrame.V * _tmpFrame.I;
_tmpFrame.IPV = 0;
if (_tmpFrame.VPV > 0) { if (_tmpFrame.VPV > 0) {
_tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV; _tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV;
} }
_tmpFrame.E = 0; if (_tmpFrame.PPV > 0) {
if ( _tmpFrame.PPV > 0) {
_efficiency.addNumber(static_cast<double>(_tmpFrame.P * 100) / _tmpFrame.PPV); _efficiency.addNumber(static_cast<double>(_tmpFrame.P * 100) / _tmpFrame.PPV);
_tmpFrame.E = _efficiency.getAverage(); _tmpFrame.E = _efficiency.getAverage();
} }
_spData = std::make_shared<veMpptStruct>(_tmpFrame); _spData = std::make_shared<veMpptStruct>(_tmpFrame);
_tmpFrame = {};
_lastUpdate = millis();
}
/*
* getCsAsString
* This function returns the state of operations (CS) as readable text.
*/
frozen::string const& VeDirectMpptController::veMpptStruct::getCsAsString() const
{
static constexpr frozen::map<uint8_t, frozen::string, 9> values = {
{ 0, "OFF" },
{ 2, "Fault" },
{ 3, "Bulk" },
{ 4, "Absorbtion" },
{ 5, "Float" },
{ 7, "Equalize (manual)" },
{ 245, "Starting-up" },
{ 247, "Auto equalize / Recondition" },
{ 252, "External Control" }
};
return getAsString(values, CS);
}
/*
* getMpptAsString
* This function returns the state of MPPT (MPPT) as readable text.
*/
frozen::string const& VeDirectMpptController::veMpptStruct::getMpptAsString() const
{
static constexpr frozen::map<uint8_t, frozen::string, 3> values = {
{ 0, "OFF" },
{ 1, "Voltage or current limited" },
{ 2, "MPP Tracker active" }
};
return getAsString(values, MPPT);
}
/*
* getErrAsString
* This function returns error state (ERR) as readable text.
*/
frozen::string const& VeDirectMpptController::veMpptStruct::getErrAsString() const
{
static constexpr frozen::map<uint8_t, frozen::string, 20> values = {
{ 0, "No error" },
{ 2, "Battery voltage too high" },
{ 17, "Charger temperature too high" },
{ 18, "Charger over current" },
{ 19, "Charger current reversed" },
{ 20, "Bulk time limit exceeded" },
{ 21, "Current sensor issue(sensor bias/sensor broken)" },
{ 26, "Terminals overheated" },
{ 28, "Converter issue (dual converter models only)" },
{ 33, "Input voltage too high (solar panel)" },
{ 34, "Input current too high (solar panel)" },
{ 38, "Input shutdown (due to excessive battery voltage)" },
{ 39, "Input shutdown (due to current flow during off mode)" },
{ 40, "Input" },
{ 65, "Lost communication with one of devices" },
{ 67, "Synchronisedcharging device configuration issue" },
{ 68, "BMS connection lost" },
{ 116, "Factory calibration data lost" },
{ 117, "Invalid/incompatible firmware" },
{ 118, "User settings invalid" }
};
return getAsString(values, ERR);
}
/*
* getOrAsString
* This function returns the off reason (OR) as readable text.
*/
frozen::string const& VeDirectMpptController::veMpptStruct::getOrAsString() const
{
static constexpr frozen::map<uint32_t, frozen::string, 10> values = {
{ 0x00000000, "Not off" },
{ 0x00000001, "No input power" },
{ 0x00000002, "Switched off (power switch)" },
{ 0x00000004, "Switched off (device moderegister)" },
{ 0x00000008, "Remote input" },
{ 0x00000010, "Protection active" },
{ 0x00000020, "Paygo" },
{ 0x00000040, "BMS" },
{ 0x00000080, "Engine shutdown detection" },
{ 0x00000100, "Analysing input voltage" }
};
return getAsString(values, OR);
} }

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include "VeDirectData.h"
#include "VeDirectFrameHandler.h" #include "VeDirectFrameHandler.h"
template<typename T, size_t WINDOW_SIZE> template<typename T, size_t WINDOW_SIZE>
@ -35,43 +36,19 @@ private:
size_t _count; size_t _count;
}; };
class VeDirectMpptController : public VeDirectFrameHandler { class VeDirectMpptController : public VeDirectFrameHandler<veMpptStruct> {
public: public:
VeDirectMpptController() = default; VeDirectMpptController() = default;
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
bool isDataValid() const; // return true if data valid and not outdated
struct veMpptStruct : veStruct { using data_t = veMpptStruct;
uint8_t MPPT; // state of MPP tracker using spData_t = std::shared_ptr<data_t const>;
int32_t PPV; // panel power in W
int32_t P; // battery output power in W (calculated)
double VPV; // panel voltage in V
double IPV; // panel current in A (calculated)
bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
uint8_t CS; // current state of operation e.g. OFF or Bulk
uint8_t ERR; // error code
uint32_t OR; // off reason
uint32_t HSDS; // day sequence number 1...365
double H19; // yield total kWh
double H20; // yield today kWh
int32_t H21; // maximum power today W
double H22; // yield yesterday kWh
int32_t H23; // maximum power yesterday W
frozen::string const& getMpptAsString() const; // state of mppt as string
frozen::string const& getCsAsString() const; // current state as string
frozen::string const& getErrAsString() const; // error state as string
frozen::string const& getOrAsString() const; // off reason as string
};
using spData_t = std::shared_ptr<veMpptStruct const>;
spData_t getData() const { return _spData; } spData_t getData() const { return _spData; }
private: private:
void textRxEvent(char* name, char* value) final; bool processTextDataDerived(std::string const& name, std::string const& value) final;
void frameValidEvent() final; void frameValidEvent() final;
spData_t _spData = nullptr; spData_t _spData = nullptr;
veMpptStruct _tmpFrame{}; // private struct for received name and value pairs
MovingAverage<double, 5> _efficiency; MovingAverage<double, 5> _efficiency;
}; };

View File

@ -3,94 +3,112 @@
VeDirectShuntController VeDirectShunt; VeDirectShuntController VeDirectShunt;
VeDirectShuntController::VeDirectShuntController()
{
}
void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
{ {
VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging, 2); VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging, 2);
} }
void VeDirectShuntController::textRxEvent(char* name, char* value) bool VeDirectShuntController::processTextDataDerived(std::string const& name, std::string const& value)
{ {
if (VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame)) { if (name == "T") {
return; _tmpFrame.T = atoi(value.c_str());
_tmpFrame.tempPresent = true;
return true;
}
if (name == "P") {
_tmpFrame.P = atoi(value.c_str());
return true;
}
if (name == "CE") {
_tmpFrame.CE = atoi(value.c_str());
return true;
}
if (name == "SOC") {
_tmpFrame.SOC = atoi(value.c_str());
return true;
}
if (name == "TTG") {
_tmpFrame.TTG = atoi(value.c_str());
return true;
}
if (name == "ALARM") {
_tmpFrame.ALARM = (value == "ON");
return true;
}
if (name == "H1") {
_tmpFrame.H1 = atoi(value.c_str());
return true;
}
if (name == "H2") {
_tmpFrame.H2 = atoi(value.c_str());
return true;
}
if (name == "H3") {
_tmpFrame.H3 = atoi(value.c_str());
return true;
}
if (name == "H4") {
_tmpFrame.H4 = atoi(value.c_str());
return true;
}
if (name == "H5") {
_tmpFrame.H5 = atoi(value.c_str());
return true;
}
if (name == "H6") {
_tmpFrame.H6 = atoi(value.c_str());
return true;
}
if (name == "H7") {
_tmpFrame.H7 = atoi(value.c_str());
return true;
}
if (name == "H8") {
_tmpFrame.H8 = atoi(value.c_str());
return true;
}
if (name == "H9") {
_tmpFrame.H9 = atoi(value.c_str());
return true;
}
if (name == "H10") {
_tmpFrame.H10 = atoi(value.c_str());
return true;
}
if (name == "H11") {
_tmpFrame.H11 = atoi(value.c_str());
return true;
}
if (name == "H12") {
_tmpFrame.H12 = atoi(value.c_str());
return true;
}
if (name == "H13") {
_tmpFrame.H13 = atoi(value.c_str());
return true;
}
if (name == "H14") {
_tmpFrame.H14 = atoi(value.c_str());
return true;
}
if (name == "H15") {
_tmpFrame.H15 = atoi(value.c_str());
return true;
}
if (name == "H16") {
_tmpFrame.H16 = atoi(value.c_str());
return true;
}
if (name == "H17") {
_tmpFrame.H17 = atoi(value.c_str());
return true;
}
if (name == "H18") {
_tmpFrame.H18 = atoi(value.c_str());
return true;
} }
if (strcmp(name, "T") == 0) { return false;
_tmpFrame.T = atoi(value);
_tmpFrame.tempPresent = true;
}
else if (strcmp(name, "P") == 0) {
_tmpFrame.P = atoi(value);
}
else if (strcmp(name, "CE") == 0) {
_tmpFrame.CE = atoi(value);
}
else if (strcmp(name, "SOC") == 0) {
_tmpFrame.SOC = atoi(value);
}
else if (strcmp(name, "TTG") == 0) {
_tmpFrame.TTG = atoi(value);
}
else if (strcmp(name, "ALARM") == 0) {
_tmpFrame.ALARM = (strcmp(value, "ON") == 0);
}
else if (strcmp(name, "H1") == 0) {
_tmpFrame.H1 = atoi(value);
}
else if (strcmp(name, "H2") == 0) {
_tmpFrame.H2 = atoi(value);
}
else if (strcmp(name, "H3") == 0) {
_tmpFrame.H3 = atoi(value);
}
else if (strcmp(name, "H4") == 0) {
_tmpFrame.H4 = atoi(value);
}
else if (strcmp(name, "H5") == 0) {
_tmpFrame.H5 = atoi(value);
}
else if (strcmp(name, "H6") == 0) {
_tmpFrame.H6 = atoi(value);
}
else if (strcmp(name, "H7") == 0) {
_tmpFrame.H7 = atoi(value);
}
else if (strcmp(name, "H8") == 0) {
_tmpFrame.H8 = atoi(value);
}
else if (strcmp(name, "H9") == 0) {
_tmpFrame.H9 = atoi(value);
}
else if (strcmp(name, "H10") == 0) {
_tmpFrame.H10 = atoi(value);
}
else if (strcmp(name, "H11") == 0) {
_tmpFrame.H11 = atoi(value);
}
else if (strcmp(name, "H12") == 0) {
_tmpFrame.H12 = atoi(value);
}
else if (strcmp(name, "H13") == 0) {
_tmpFrame.H13 = atoi(value);
}
else if (strcmp(name, "H14") == 0) {
_tmpFrame.H14 = atoi(value);
}
else if (strcmp(name, "H15") == 0) {
_tmpFrame.H15 = atoi(value);
}
else if (strcmp(name, "H16") == 0) {
_tmpFrame.H16 = atoi(value);
}
else if (strcmp(name, "H17") == 0) {
_tmpFrame.H17 = atoi(value);
}
else if (strcmp(name, "H18") == 0) {
_tmpFrame.H18 = atoi(value);
}
} }
/* /*
@ -98,12 +116,5 @@ void VeDirectShuntController::textRxEvent(char* name, char* value)
* This function is called at the end of the received frame. * This function is called at the end of the received frame.
*/ */
void VeDirectShuntController::frameValidEvent() { void VeDirectShuntController::frameValidEvent() {
// other than in the MPPT controller, the SmartShunt seems to split all data
// into two seperate messagesas. Thus we update veFrame only every second message
// after a value for PID has been received
if (_tmpFrame.PID == 0) { return; }
veFrame = _tmpFrame; veFrame = _tmpFrame;
_tmpFrame = {};
_lastUpdate = millis();
} }

View File

@ -1,49 +1,21 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include "VeDirectData.h"
#include "VeDirectFrameHandler.h" #include "VeDirectFrameHandler.h"
class VeDirectShuntController : public VeDirectFrameHandler { class VeDirectShuntController : public VeDirectFrameHandler<veShuntStruct> {
public: public:
VeDirectShuntController(); VeDirectShuntController() = default;
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging); void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging);
struct veShuntStruct : veStruct { using data_t = veShuntStruct;
int32_t T; // Battery temperature data_t veFrame{};
bool tempPresent = false; // Battery temperature sensor is attached to the shunt
int32_t P; // Instantaneous power
int32_t CE; // Consumed Amp Hours
int32_t SOC; // State-of-charge
uint32_t TTG; // Time-to-go
bool ALARM; // Alarm condition active
uint32_t AR; // Alarm Reason
int32_t H1; // Depth of the deepest discharge
int32_t H2; // Depth of the last discharge
int32_t H3; // Depth of the average discharge
int32_t H4; // Number of charge cycles
int32_t H5; // Number of full discharges
int32_t H6; // Cumulative Amp Hours drawn
int32_t H7; // Minimum main (battery) voltage
int32_t H8; // Maximum main (battery) voltage
int32_t H9; // Number of seconds since last full charge
int32_t H10; // Number of automatic synchronizations
int32_t H11; // Number of low main voltage alarms
int32_t H12; // Number of high main voltage alarms
int32_t H13; // Number of low auxiliary voltage alarms
int32_t H14; // Number of high auxiliary voltage alarms
int32_t H15; // Minimum auxiliary (battery) voltage
int32_t H16; // Maximum auxiliary (battery) voltage
int32_t H17; // Amount of discharged energy
int32_t H18; // Amount of charged energy
};
veShuntStruct veFrame{};
private: private:
void textRxEvent(char * name, char * value) final; bool processTextDataDerived(std::string const& name, std::string const& value) final;
void frameValidEvent() final; void frameValidEvent() final;
veShuntStruct _tmpFrame{}; // private struct for received name and value pairs
}; };
extern VeDirectShuntController VeDirectShunt; extern VeDirectShuntController VeDirectShunt;

View File

@ -373,7 +373,7 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
_lastUpdate = millis(); _lastUpdate = millis();
} }
void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct const& shuntData) { void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) {
BatteryStats::setVoltage(shuntData.V, millis()); BatteryStats::setVoltage(shuntData.V, millis());
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis()); BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());

View File

@ -70,7 +70,7 @@ void MqttHandleVedirectClass::loop()
VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value();
VeDirectMpptController::veMpptStruct _kvFrame = _kvFrames[spMpptData->SER]; VeDirectMpptController::data_t _kvFrame = _kvFrames[spMpptData->SER];
publish_mppt_data(spMpptData, _kvFrame); publish_mppt_data(spMpptData, _kvFrame);
if (!_PublishFull) { if (!_PublishFull) {
_kvFrames[spMpptData->SER] = *spMpptData; _kvFrames[spMpptData->SER] = *spMpptData;
@ -105,7 +105,7 @@ void MqttHandleVedirectClass::loop()
} }
void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData,
VeDirectMpptController::veMpptStruct &frame) const { const VeDirectMpptController::data_t &frame) const {
String value; String value;
String topic = "victron/"; String topic = "victron/";
topic.concat(spMpptData->SER); topic.concat(spMpptData->SER);