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:
parent
187f197e32
commit
ad125ea804
@ -147,7 +147,7 @@ class VictronSmartShuntStats : public BatteryStats {
|
||||
void getLiveViewData(JsonVariant& root) const final;
|
||||
void mqttPublish() const final;
|
||||
|
||||
void updateFrom(VeDirectShuntController::veShuntStruct const& shuntData);
|
||||
void updateFrom(VeDirectShuntController::data_t const& shuntData);
|
||||
|
||||
private:
|
||||
float _current;
|
||||
|
||||
@ -21,7 +21,7 @@ public:
|
||||
void forceUpdate();
|
||||
private:
|
||||
void loop();
|
||||
std::map<std::string, VeDirectMpptController::veMpptStruct> _kvFrames;
|
||||
std::map<std::string, VeDirectMpptController::data_t> _kvFrames;
|
||||
|
||||
Task _loopTask;
|
||||
|
||||
@ -34,7 +34,7 @@ private:
|
||||
bool _PublishFull;
|
||||
|
||||
void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData,
|
||||
VeDirectMpptController::veMpptStruct &frame) const;
|
||||
const VeDirectMpptController::data_t &frame) const;
|
||||
};
|
||||
|
||||
extern MqttHandleVedirectClass MqttHandleVedirect;
|
||||
|
||||
224
lib/VeDirectFrameHandler/VeDirectData.cpp
Normal file
224
lib/VeDirectFrameHandler/VeDirectData.cpp
Normal 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);
|
||||
}
|
||||
70
lib/VeDirectFrameHandler/VeDirectData.h
Normal file
70
lib/VeDirectFrameHandler/VeDirectData.h
Normal 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
|
||||
};
|
||||
@ -58,7 +58,8 @@ class Silent : public Print {
|
||||
|
||||
static Silent MessageOutputDummy;
|
||||
|
||||
VeDirectFrameHandler::VeDirectFrameHandler() :
|
||||
template<typename T>
|
||||
VeDirectFrameHandler<T>::VeDirectFrameHandler() :
|
||||
_msgOut(&MessageOutputDummy),
|
||||
_lastUpdate(0),
|
||||
_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->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); }
|
||||
}
|
||||
|
||||
void VeDirectFrameHandler::dumpDebugBuffer() {
|
||||
template<typename T>
|
||||
void VeDirectFrameHandler<T>::dumpDebugBuffer() {
|
||||
_msgOut->printf("%s serial input (%d Bytes):", _logId, _debugIn);
|
||||
for (int i = 0; i < _debugIn; ++i) {
|
||||
if (i % 16 == 0) {
|
||||
@ -96,7 +99,16 @@ void VeDirectFrameHandler::dumpDebugBuffer() {
|
||||
_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()) {
|
||||
rxData(_vedirectSerial->read());
|
||||
@ -109,8 +121,7 @@ void VeDirectFrameHandler::loop()
|
||||
if (IDLE != _state && _lastByteMillis + 500 < millis()) {
|
||||
_msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", _logId, _state);
|
||||
if (_verboseLogging) { dumpDebugBuffer(); }
|
||||
_checksum = 0;
|
||||
_state = IDLE;
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,7 +130,8 @@ void VeDirectFrameHandler::loop()
|
||||
* 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
|
||||
*/
|
||||
void VeDirectFrameHandler::rxData(uint8_t inbyte)
|
||||
template<typename T>
|
||||
void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
|
||||
{
|
||||
if (_verboseLogging) {
|
||||
_debugBuffer[_debugIn] = inbyte;
|
||||
@ -186,7 +198,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
|
||||
case '\n':
|
||||
if ( _textPointer < (_value + sizeof(_value)) ) {
|
||||
*_textPointer = 0; // make zero ended
|
||||
textRxEvent(_name, _value);
|
||||
_textData.push_back({_name, _value});
|
||||
}
|
||||
_state = RECORD_BEGIN;
|
||||
break;
|
||||
@ -201,14 +213,18 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
|
||||
break;
|
||||
case CHECKSUM:
|
||||
{
|
||||
bool valid = _checksum == 0;
|
||||
if (!valid) {
|
||||
_msgOut->printf("%s checksum 0x%02x != 0, invalid frame\r\n", _logId, _checksum);
|
||||
}
|
||||
if (_verboseLogging) { dumpDebugBuffer(); }
|
||||
_checksum = 0;
|
||||
_state = IDLE;
|
||||
if (valid) { frameValidEvent(); }
|
||||
if (_checksum == 0) {
|
||||
for (auto const& event : _textData) {
|
||||
processTextData(event.first, event.second);
|
||||
}
|
||||
_lastUpdate = millis();
|
||||
frameValidEvent();
|
||||
}
|
||||
else {
|
||||
_msgOut->printf("%s checksum 0x%02x != 0x00, invalid frame\r\n", _logId, _checksum);
|
||||
}
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
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.
|
||||
*/
|
||||
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) {
|
||||
_msgOut->printf("%s Text Event %s: Value: %s\r\n",
|
||||
_logId, name, value );
|
||||
_msgOut->printf("%s Text Data '%s' = '%s'\r\n",
|
||||
_logId, name.c_str(), value.c_str());
|
||||
}
|
||||
|
||||
if (strcmp(name, "PID") == 0) {
|
||||
frame.PID = strtol(value, nullptr, 0);
|
||||
return true;
|
||||
if (processTextDataDerived(name, value)) { return; }
|
||||
|
||||
if (name == "PID") {
|
||||
_tmpFrame.PID = strtol(value.c_str(), nullptr, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "SER") == 0) {
|
||||
strcpy(frame.SER, value);
|
||||
return true;
|
||||
if (name == "SER") {
|
||||
strcpy(_tmpFrame.SER, value.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "FW") == 0) {
|
||||
strcpy(frame.FW, value);
|
||||
return true;
|
||||
if (name == "FW") {
|
||||
strcpy(_tmpFrame.FW, value.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "V") == 0) {
|
||||
frame.V = round(atof(value) / 10.0) / 100.0;
|
||||
return true;
|
||||
if (name == "V") {
|
||||
_tmpFrame.V = round(atof(value.c_str()) / 10.0) / 100.0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "I") == 0) {
|
||||
frame.I = round(atof(value) / 10.0) / 100.0;
|
||||
return true;
|
||||
if (name == "I") {
|
||||
_tmpFrame.I = round(atof(value.c_str()) / 10.0) / 100.0;
|
||||
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
|
||||
* 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
|
||||
|
||||
switch (inbyte) {
|
||||
@ -282,138 +303,14 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const {
|
||||
return strlen(frame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000);
|
||||
template<typename T>
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
@ -13,17 +13,17 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <array>
|
||||
#include <frozen/string.h>
|
||||
#include <frozen/map.h>
|
||||
#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
|
||||
#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer
|
||||
|
||||
template<typename T>
|
||||
class VeDirectFrameHandler {
|
||||
public:
|
||||
void loop(); // main loop to read ve.direct data
|
||||
uint32_t getLastUpdate() const; // timestamp of last successful frame read
|
||||
bool isDataValid() const; // return true if data valid and not outdated
|
||||
|
||||
protected:
|
||||
VeDirectFrameHandler();
|
||||
@ -33,36 +33,14 @@ protected:
|
||||
Print* _msgOut;
|
||||
uint32_t _lastUpdate;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
T _tmpFrame;
|
||||
|
||||
private:
|
||||
void setLastUpdate(); // set timestampt after successful frame read
|
||||
void reset();
|
||||
void dumpDebugBuffer();
|
||||
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;
|
||||
int hexRxEvent(uint8_t);
|
||||
|
||||
@ -78,4 +56,18 @@ private:
|
||||
unsigned _debugIn;
|
||||
uint32_t _lastByteMillis;
|
||||
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>;
|
||||
|
||||
@ -7,58 +7,62 @@ void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verb
|
||||
_spData = std::make_shared<veMpptStruct>();
|
||||
}
|
||||
|
||||
bool VeDirectMpptController::isDataValid() const {
|
||||
return VeDirectFrameHandler::isDataValid(*_spData);
|
||||
}
|
||||
|
||||
void VeDirectMpptController::textRxEvent(char* name, char* value)
|
||||
bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value)
|
||||
{
|
||||
if (VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame)) {
|
||||
return;
|
||||
if (name == "LOAD") {
|
||||
_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) {
|
||||
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);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -68,108 +72,14 @@ void VeDirectMpptController::textRxEvent(char* name, char* value)
|
||||
void VeDirectMpptController::frameValidEvent() {
|
||||
_tmpFrame.P = _tmpFrame.V * _tmpFrame.I;
|
||||
|
||||
_tmpFrame.IPV = 0;
|
||||
if (_tmpFrame.VPV > 0) {
|
||||
_tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV;
|
||||
}
|
||||
|
||||
_tmpFrame.E = 0;
|
||||
if (_tmpFrame.PPV > 0) {
|
||||
_efficiency.addNumber(static_cast<double>(_tmpFrame.P * 100) / _tmpFrame.PPV);
|
||||
_tmpFrame.E = _efficiency.getAverage();
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "VeDirectData.h"
|
||||
#include "VeDirectFrameHandler.h"
|
||||
|
||||
template<typename T, size_t WINDOW_SIZE>
|
||||
@ -35,43 +36,19 @@ private:
|
||||
size_t _count;
|
||||
};
|
||||
|
||||
class VeDirectMpptController : public VeDirectFrameHandler {
|
||||
class VeDirectMpptController : public VeDirectFrameHandler<veMpptStruct> {
|
||||
public:
|
||||
VeDirectMpptController() = default;
|
||||
|
||||
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 {
|
||||
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
|
||||
};
|
||||
|
||||
using spData_t = std::shared_ptr<veMpptStruct const>;
|
||||
using data_t = veMpptStruct;
|
||||
using spData_t = std::shared_ptr<data_t const>;
|
||||
spData_t getData() const { return _spData; }
|
||||
|
||||
private:
|
||||
void textRxEvent(char* name, char* value) final;
|
||||
bool processTextDataDerived(std::string const& name, std::string const& value) final;
|
||||
void frameValidEvent() final;
|
||||
spData_t _spData = nullptr;
|
||||
veMpptStruct _tmpFrame{}; // private struct for received name and value pairs
|
||||
MovingAverage<double, 5> _efficiency;
|
||||
};
|
||||
|
||||
@ -3,94 +3,112 @@
|
||||
|
||||
VeDirectShuntController VeDirectShunt;
|
||||
|
||||
VeDirectShuntController::VeDirectShuntController()
|
||||
{
|
||||
}
|
||||
|
||||
void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
|
||||
{
|
||||
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)) {
|
||||
return;
|
||||
if (name == "T") {
|
||||
_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) {
|
||||
_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);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -98,12 +116,5 @@ void VeDirectShuntController::textRxEvent(char* name, char* value)
|
||||
* This function is called at the end of the received frame.
|
||||
*/
|
||||
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;
|
||||
_tmpFrame = {};
|
||||
_lastUpdate = millis();
|
||||
}
|
||||
|
||||
@ -1,49 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "VeDirectData.h"
|
||||
#include "VeDirectFrameHandler.h"
|
||||
|
||||
class VeDirectShuntController : public VeDirectFrameHandler {
|
||||
class VeDirectShuntController : public VeDirectFrameHandler<veShuntStruct> {
|
||||
public:
|
||||
VeDirectShuntController();
|
||||
VeDirectShuntController() = default;
|
||||
|
||||
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging);
|
||||
|
||||
struct veShuntStruct : veStruct {
|
||||
int32_t T; // Battery temperature
|
||||
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{};
|
||||
using data_t = veShuntStruct;
|
||||
data_t veFrame{};
|
||||
|
||||
private:
|
||||
void textRxEvent(char * name, char * value) final;
|
||||
bool processTextDataDerived(std::string const& name, std::string const& value) final;
|
||||
void frameValidEvent() final;
|
||||
veShuntStruct _tmpFrame{}; // private struct for received name and value pairs
|
||||
};
|
||||
|
||||
extern VeDirectShuntController VeDirectShunt;
|
||||
|
||||
@ -373,7 +373,7 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
|
||||
_lastUpdate = millis();
|
||||
}
|
||||
|
||||
void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct const& shuntData) {
|
||||
void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) {
|
||||
BatteryStats::setVoltage(shuntData.V, millis());
|
||||
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ void MqttHandleVedirectClass::loop()
|
||||
|
||||
VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value();
|
||||
|
||||
VeDirectMpptController::veMpptStruct _kvFrame = _kvFrames[spMpptData->SER];
|
||||
VeDirectMpptController::data_t _kvFrame = _kvFrames[spMpptData->SER];
|
||||
publish_mppt_data(spMpptData, _kvFrame);
|
||||
if (!_PublishFull) {
|
||||
_kvFrames[spMpptData->SER] = *spMpptData;
|
||||
@ -105,7 +105,7 @@ void MqttHandleVedirectClass::loop()
|
||||
}
|
||||
|
||||
void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData,
|
||||
VeDirectMpptController::veMpptStruct &frame) const {
|
||||
const VeDirectMpptController::data_t &frame) const {
|
||||
String value;
|
||||
String topic = "victron/";
|
||||
topic.concat(spMpptData->SER);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user