From d23b991f5cf6ea8a376c9ba7772ef41ea96f3ed5 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 19 Oct 2023 16:15:29 +0200 Subject: [PATCH] VE.Direct: Fix design issues and prepare support for multiple instances (#505) * introduce VictronMpptClass this solves a design issue where the loop() method of a static instance of VeDirectMpptController, which is part of library code, is called as part of the main loop() implementation. that is a problem because the call to this loop() must be handled differently from all other calls: the lib does not know whether or not the feature is enabled at all. also, the instance would not be initialized when enabling the feature during normal operation. that would even lead to a nullptr exception since the pointer to the serial implementation is still uninitialized. this new intermediate class is implemented with the support for multiple Victron charge controllers in mind. adding support for more charge controllers should be more viable than ever. fixes #481. related to #397 #129. * VE.Direct: move get.*AsString methods to respective structs those structs, which hold the data to be translated into strings, know best how to translate them. this change also simplifies access to those translation, as no parameter must be handed to the respective methods: they now act upon the data of the instance they are called for. adds constness to those methods. * VE.Direct: simplify and clean up get.*AsString methods use a map, which is much easier to maintain and which reads much easier. move the strings to flash memory to save RAM. * DPL: use VictronMpptClass::getPowerOutputWatts method remove redundant calculation of output power from DPL. consider separation of concern: VictronMpptClass will provide the total solar output power. the DPL shall not concern itself about how that value is calculated and it certainly should be unaware about how many MPPT charge controllers there actually are. * VE.Direct: avoid shadowing struct member "P" P was part of the base struct for both MPPT and SmartShunt controller. however, P was also part of the SmartShunt controller data struct, shadowing the member in the base struct. since P has slightly different meaning in MPPT versus SmartShunt, and since P is calculated for MPPT controllers but read from SmartShunts, P now lives in both derived structs, but not in the base struct. * VE.Direct: isDataValid(): avoid copying data structs pass a const reference to the base class implementation of isDataValid() rather than a copy of the whole struct. * VE.Direct: unify logging of text events * VE.Direct: stop processing text event if handled by base in case the base class processed a text event, do not try to match it against values that are only valid in the derived class -- none will match. * VE.Direct MPPT: manage data in a shared_ptr instead of handing out a reference to a struct which is part of a class instance that may disappear, e.g., on a config change, we now manage the lifetime of said data structure using a shared_ptr and hand out copies of that shared_ptr. this makes sure that users have a valid copy of the data as long as they hold the shared_ptr. * VE.Direct MPPT: implement getDataAgeMillis() this works even if millis() wraps around. * VE.Direct: process frame end event only for valid frames save a parameters, save a level of indention, save a function call for invalid frames. --- include/PinMapping.h | 1 - include/VictronMppt.h | 39 ++ include/WebApi_ws_vedirect_live.h | 3 +- .../VeDirectFrameHandler.cpp | 447 +++++------------- .../VeDirectFrameHandler.h | 38 +- .../VeDirectMpptController.cpp | 216 ++++----- .../VeDirectMpptController.h | 23 +- .../VeDirectShuntController.cpp | 24 +- .../VeDirectShuntController.h | 2 +- src/BatteryStats.cpp | 2 +- src/MqttHandlVedirectHass.cpp | 18 +- src/MqttHandleVedirect.cpp | 93 ++-- src/PinMapping.cpp | 5 - src/PowerLimiter.cpp | 15 +- src/VictronMppt.cpp | 96 ++++ src/WebApi_vedirect.cpp | 4 +- src/WebApi_ws_live.cpp | 12 +- src/WebApi_ws_vedirect_live.cpp | 69 ++- src/main.cpp | 21 +- 19 files changed, 506 insertions(+), 622 deletions(-) create mode 100644 include/VictronMppt.h create mode 100644 src/VictronMppt.cpp diff --git a/include/PinMapping.h b/include/PinMapping.h index 5e4c7fa04..c9541c4ba 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -62,7 +62,6 @@ class PinMappingClass { bool isValidNrf24Config(); bool isValidCmt2300Config(); bool isValidEthConfig(); - bool isValidVictronConfig(); bool isValidHuaweiConfig(); private: diff --git a/include/VictronMppt.h b/include/VictronMppt.h new file mode 100644 index 000000000..6718ef330 --- /dev/null +++ b/include/VictronMppt.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +#include "VeDirectMpptController.h" + +class VictronMpptClass { +public: + VictronMpptClass() = default; + ~VictronMpptClass() = default; + + void init(); + void loop(); + + bool isDataValid() const; + + // returns the data age of all controllers, + // i.e, the youngest data's age is returned. + uint32_t getDataAgeMillis() const; + + VeDirectMpptController::spData_t getData(size_t idx = 0) const; + + // total output of all MPPT charge controllers in Watts + int32_t getPowerOutputWatts() const; + +private: + VictronMpptClass(VictronMpptClass const& other) = delete; + VictronMpptClass(VictronMpptClass&& other) = delete; + VictronMpptClass& operator=(VictronMpptClass const& other) = delete; + VictronMpptClass& operator=(VictronMpptClass&& other) = delete; + + mutable std::mutex _mutex; + using controller_t = std::unique_ptr; + std::vector _controllers; +}; + +extern VictronMpptClass VictronMppt; diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index 13b27d9fa..d084e74ee 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -20,8 +20,7 @@ class WebApiWsVedirectLiveClass { AsyncWebSocket _ws; uint32_t _lastWsPublish = 0; - uint32_t _lastVedirectUpdateCheck = 0; uint32_t _lastWsCleanup = 0; - uint32_t _newestVedirectTimestamp = 0; + uint32_t _dataAgeMillis = 0; static constexpr uint16_t _responseSize = 1024 + 128; }; \ No newline at end of file diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 39ce4fabf..0c0c544e6 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -72,19 +72,14 @@ VeDirectFrameHandler::VeDirectFrameHandler() : { } -void VeDirectFrameHandler::setVerboseLogging(bool verboseLogging) -{ - _verboseLogging = verboseLogging; - if (!_verboseLogging) { _debugIn = 0; } -} - void VeDirectFrameHandler::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { _vedirectSerial = std::make_unique(hwSerialPort); _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx); _vedirectSerial->flush(); _msgOut = msgOut; - setVerboseLogging(verboseLogging); + _verboseLogging = verboseLogging; + _debugIn = 0; } void VeDirectFrameHandler::dumpDebugBuffer() { @@ -211,7 +206,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) if (_verboseLogging) { dumpDebugBuffer(); } _checksum = 0; _state = IDLE; - frameEndEvent(valid); + if (valid) { frameValidEvent(); } break; } case RECORD_HEX: @@ -224,22 +219,38 @@ 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. */ -void VeDirectFrameHandler::textRxEvent(char * name, char * value, veStruct& frame) { +bool VeDirectFrameHandler::textRxEvent(std::string const& who, char* name, char* value, veStruct& frame) { + if (_verboseLogging) { + _msgOut->printf("[Victron %s] Text Event %s: Value: %s\r\n", + who.c_str(), name, value ); + } + if (strcmp(name, "PID") == 0) { frame.PID = strtol(value, nullptr, 0); + return true; } - else if (strcmp(name, "SER") == 0) { + + if (strcmp(name, "SER") == 0) { strcpy(frame.SER, value); + return true; } - else if (strcmp(name, "FW") == 0) { + + if (strcmp(name, "FW") == 0) { strcpy(frame.FW, value); + return true; } - else if (strcmp(name, "V") == 0) { + + if (strcmp(name, "V") == 0) { frame.V = round(atof(value) / 10.0) / 100.0; + return true; } - else if (strcmp(name, "I") == 0) { + + if (strcmp(name, "I") == 0) { frame.I = round(atof(value) / 10.0) / 100.0; + return true; } + + return false; } @@ -269,7 +280,7 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { return ret; } -bool VeDirectFrameHandler::isDataValid(veStruct frame) { +bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const { if (_lastUpdate == 0) { return false; } @@ -279,330 +290,112 @@ bool VeDirectFrameHandler::isDataValid(veStruct frame) { return true; } -unsigned long VeDirectFrameHandler::getLastUpdate() +uint32_t VeDirectFrameHandler::getLastUpdate() const { return _lastUpdate; } -/* - * getPidAsString - * This function returns the product id (PID) as readable text. - */ -String VeDirectFrameHandler::getPidAsString(uint16_t pid) +template +String const& VeDirectFrameHandler::getAsString(std::map const& values, T val) { - String strPID =""; - - switch(pid) { - case 0x0300: - strPID = "BlueSolar MPPT 70|15"; - break; - case 0xA040: - strPID = "BlueSolar MPPT 75|50"; - break; - case 0xA041: - strPID = "BlueSolar MPPT 150|35"; - break; - case 0xA042: - strPID = "BlueSolar MPPT 75|15"; - break; - case 0xA043: - strPID = "BlueSolar MPPT 100|15"; - break; - case 0xA044: - strPID = "BlueSolar MPPT 100|30"; - break; - case 0xA045: - strPID = "BlueSolar MPPT 100|50"; - break; - case 0xA046: - strPID = "BlueSolar MPPT 100|70"; - break; - case 0xA047: - strPID = "BlueSolar MPPT 150|100"; - break; - case 0xA049: - strPID = "BlueSolar MPPT 100|50 rev2"; - break; - case 0xA04A: - strPID = "BlueSolar MPPT 100|30 rev2"; - break; - case 0xA04B: - strPID = "BlueSolar MPPT 150|35 rev2"; - break; - case 0XA04C: - strPID = "BlueSolar MPPT 75|10"; - break; - case 0XA04D: - strPID = "BlueSolar MPPT 150|45"; - break; - case 0XA04E: - strPID = "BlueSolar MPPT 150|60"; - break; - case 0XA04F: - strPID = "BlueSolar MPPT 150|85"; - break; - case 0XA050: - strPID = "SmartSolar MPPT 250|100"; - break; - case 0XA051: - strPID = "SmartSolar MPPT 150|100"; - break; - case 0XA052: - strPID = "SmartSolar MPPT 150|85"; - break; - case 0XA053: - strPID = "SmartSolar MPPT 75|15"; - break; - case 0XA054: - strPID = "SmartSolar MPPT 75|10"; - break; - case 0XA055: - strPID = "SmartSolar MPPT 100|15"; - break; - case 0XA056: - strPID = "SmartSolar MPPT 100|30"; - break; - case 0XA057: - strPID = "SmartSolar MPPT 100|50"; - break; - case 0XA058: - strPID = "SmartSolar MPPT 150|35"; - break; - case 0XA059: - strPID = "SmartSolar MPPT 150|10 rev2"; - break; - case 0XA05A: - strPID = "SmartSolar MPPT 150|85 rev2"; - break; - case 0XA05B: - strPID = "SmartSolar MPPT 250|70"; - break; - case 0XA05C: - strPID = "SmartSolar MPPT 250|85"; - break; - case 0XA05D: - strPID = "SmartSolar MPPT 250|60"; - break; - case 0XA05E: - strPID = "SmartSolar MPPT 250|45"; - break; - case 0XA05F: - strPID = "SmartSolar MPPT 100|20"; - break; - case 0XA060: - strPID = "SmartSolar MPPT 100|20 48V"; - break; - case 0XA061: - strPID = "SmartSolar MPPT 150|45"; - break; - case 0XA062: - strPID = "SmartSolar MPPT 150|60"; - break; - case 0XA063: - strPID = "SmartSolar MPPT 150|70"; - break; - case 0XA064: - strPID = "SmartSolar MPPT 250|85 rev2"; - break; - case 0XA065: - strPID = "SmartSolar MPPT 250|100 rev2"; - break; - case 0XA066: - strPID = "BlueSolar MPPT 100|20"; - break; - case 0XA067: - strPID = "BlueSolar MPPT 100|20 48V"; - break; - case 0XA068: - strPID = "SmartSolar MPPT 250|60 rev2"; - break; - case 0XA069: - strPID = "SmartSolar MPPT 250|70 rev2"; - break; - case 0XA06A: - strPID = "SmartSolar MPPT 150|45 rev2"; - break; - case 0XA06B: - strPID = "SmartSolar MPPT 150|60 rev2"; - break; - case 0XA06C: - strPID = "SmartSolar MPPT 150|70 rev2"; - break; - case 0XA06D: - strPID = "SmartSolar MPPT 150|85 rev3"; - break; - case 0XA06E: - strPID = "SmartSolar MPPT 150|100 rev3"; - break; - case 0XA06F: - strPID = "BlueSolar MPPT 150|45 rev2"; - break; - case 0XA070: - strPID = "BlueSolar MPPT 150|60 rev2"; - break; - case 0XA071: - strPID = "BlueSolar MPPT 150|70 rev2"; - break; - case 0XA102: - strPID = "SmartSolar MPPT VE.Can 150|70"; - break; - case 0XA103: - strPID = "SmartSolar MPPT VE.Can 150|45"; - break; - case 0XA104: - strPID = "SmartSolar MPPT VE.Can 150|60"; - break; - case 0XA105: - strPID = "SmartSolar MPPT VE.Can 150|85"; - break; - case 0XA106: - strPID = "SmartSolar MPPT VE.Can 150|100"; - break; - case 0XA107: - strPID = "SmartSolar MPPT VE.Can 250|45"; - break; - case 0XA108: - strPID = "SmartSolar MPPT VE.Can 250|60"; - break; - case 0XA109: - strPID = "SmartSolar MPPT VE.Can 250|80"; - break; - case 0XA10A: - strPID = "SmartSolar MPPT VE.Can 250|85"; - break; - case 0XA10B: - strPID = "SmartSolar MPPT VE.Can 250|100"; - break; - case 0XA10C: - strPID = "SmartSolar MPPT VE.Can 150|70 rev2"; - break; - case 0XA10D: - strPID = "SmartSolar MPPT VE.Can 150|85 rev2"; - break; - case 0XA10E: - strPID = "SmartSolar MPPT VE.Can 150|100 rev2"; - break; - case 0XA10F: - strPID = "BlueSolar MPPT VE.Can 150|100"; - break; - case 0XA110: - strPID = "SmartSolar MPPT RS 450|100"; - break; - case 0XA112: - strPID = "BlueSolar MPPT VE.Can 250|70"; - break; - case 0XA113: - strPID = "BlueSolar MPPT VE.Can 250|100"; - break; - case 0XA114: - strPID = "SmartSolar MPPT VE.Can 250|70 rev2"; - break; - case 0XA115: - strPID = "SmartSolar MPPT VE.Can 250|100 rev2"; - break; - case 0XA116: - strPID = "SmartSolar MPPT VE.Can 250|85 rev2"; - break; - case 0xA381: - strPID = "BMV-712 Smart"; - break; - case 0xA382: - strPID = "BMV-710H Smart"; - break; - case 0xA383: - strPID = "BMV-712 Smart Rev2"; - break; - case 0xA389: - strPID = "SmartShunt 500A/50mV"; - break; - case 0xA38A: - strPID = "SmartShunt 1000A/50mV"; - break; - case 0xA38B: - strPID = "SmartShunt 2000A/50mV"; - break; - case 0xA3F0: - strPID = "SmartShunt 2000A/50mV" ; - break; - default: - strPID = pid; + auto pos = values.find(val); + if (pos == values.end()) { + static String dummy; + dummy = val; + return dummy; } - return strPID; + return pos->second; } - +template String const& VeDirectFrameHandler::getAsString(std::map const& values, uint8_t val); +template String const& VeDirectFrameHandler::getAsString(std::map const& values, uint16_t val); +template String const& VeDirectFrameHandler::getAsString(std::map const& values, uint32_t val); /* - * getErrAsString - * This function returns error state (ERR) as readable text. + * getPidAsString + * This function returns the product id (PID) as readable text. */ -String VeDirectFrameHandler::getErrAsString(uint8_t err) +String VeDirectFrameHandler::veStruct::getPidAsString() const { - String strERR =""; + static const std::map values = { + { 0x0300, F("BlueSolar MPPT 70|15") }, + { 0xA040, F("BlueSolar MPPT 75|50") }, + { 0xA041, F("BlueSolar MPPT 150|35") }, + { 0xA042, F("BlueSolar MPPT 75|15") }, + { 0xA043, F("BlueSolar MPPT 100|15") }, + { 0xA044, F("BlueSolar MPPT 100|30") }, + { 0xA045, F("BlueSolar MPPT 100|50") }, + { 0xA046, F("BlueSolar MPPT 100|70") }, + { 0xA047, F("BlueSolar MPPT 150|100") }, + { 0xA049, F("BlueSolar MPPT 100|50 rev2") }, + { 0xA04A, F("BlueSolar MPPT 100|30 rev2") }, + { 0xA04B, F("BlueSolar MPPT 150|35 rev2") }, + { 0xA04C, F("BlueSolar MPPT 75|10") }, + { 0xA04D, F("BlueSolar MPPT 150|45") }, + { 0xA04E, F("BlueSolar MPPT 150|60") }, + { 0xA04F, F("BlueSolar MPPT 150|85") }, + { 0xA050, F("SmartSolar MPPT 250|100") }, + { 0xA051, F("SmartSolar MPPT 150|100") }, + { 0xA052, F("SmartSolar MPPT 150|85") }, + { 0xA053, F("SmartSolar MPPT 75|15") }, + { 0xA054, F("SmartSolar MPPT 75|10") }, + { 0xA055, F("SmartSolar MPPT 100|15") }, + { 0xA056, F("SmartSolar MPPT 100|30") }, + { 0xA057, F("SmartSolar MPPT 100|50") }, + { 0xA058, F("SmartSolar MPPT 150|35") }, + { 0xA059, F("SmartSolar MPPT 150|10 rev2") }, + { 0xA05A, F("SmartSolar MPPT 150|85 rev2") }, + { 0xA05B, F("SmartSolar MPPT 250|70") }, + { 0xA05C, F("SmartSolar MPPT 250|85") }, + { 0xA05D, F("SmartSolar MPPT 250|60") }, + { 0xA05E, F("SmartSolar MPPT 250|45") }, + { 0xA05F, F("SmartSolar MPPT 100|20") }, + { 0xA060, F("SmartSolar MPPT 100|20 48V") }, + { 0xA061, F("SmartSolar MPPT 150|45") }, + { 0xA062, F("SmartSolar MPPT 150|60") }, + { 0xA063, F("SmartSolar MPPT 150|70") }, + { 0xA064, F("SmartSolar MPPT 250|85 rev2") }, + { 0xA065, F("SmartSolar MPPT 250|100 rev2") }, + { 0xA066, F("BlueSolar MPPT 100|20") }, + { 0xA067, F("BlueSolar MPPT 100|20 48V") }, + { 0xA068, F("SmartSolar MPPT 250|60 rev2") }, + { 0xA069, F("SmartSolar MPPT 250|70 rev2") }, + { 0xA06A, F("SmartSolar MPPT 150|45 rev2") }, + { 0xA06B, F("SmartSolar MPPT 150|60 rev2") }, + { 0xA06C, F("SmartSolar MPPT 150|70 rev2") }, + { 0xA06D, F("SmartSolar MPPT 150|85 rev3") }, + { 0xA06E, F("SmartSolar MPPT 150|100 rev3") }, + { 0xA06F, F("BlueSolar MPPT 150|45 rev2") }, + { 0xA070, F("BlueSolar MPPT 150|60 rev2") }, + { 0xA071, F("BlueSolar MPPT 150|70 rev2") }, + { 0xA102, F("SmartSolar MPPT VE.Can 150|70") }, + { 0xA103, F("SmartSolar MPPT VE.Can 150|45") }, + { 0xA104, F("SmartSolar MPPT VE.Can 150|60") }, + { 0xA105, F("SmartSolar MPPT VE.Can 150|85") }, + { 0xA106, F("SmartSolar MPPT VE.Can 150|100") }, + { 0xA107, F("SmartSolar MPPT VE.Can 250|45") }, + { 0xA108, F("SmartSolar MPPT VE.Can 250|60") }, + { 0xA109, F("SmartSolar MPPT VE.Can 250|80") }, + { 0xA10A, F("SmartSolar MPPT VE.Can 250|85") }, + { 0xA10B, F("SmartSolar MPPT VE.Can 250|100") }, + { 0xA10C, F("SmartSolar MPPT VE.Can 150|70 rev2") }, + { 0xA10D, F("SmartSolar MPPT VE.Can 150|85 rev2") }, + { 0xA10E, F("SmartSolar MPPT VE.Can 150|100 rev2") }, + { 0xA10F, F("BlueSolar MPPT VE.Can 150|100") }, + { 0xA110, F("SmartSolar MPPT RS 450|100") }, + { 0xA112, F("BlueSolar MPPT VE.Can 250|70") }, + { 0xA113, F("BlueSolar MPPT VE.Can 250|100") }, + { 0xA114, F("SmartSolar MPPT VE.Can 250|70 rev2") }, + { 0xA115, F("SmartSolar MPPT VE.Can 250|100 rev2") }, + { 0xA116, F("SmartSolar MPPT VE.Can 250|85 rev2") }, + { 0xA381, F("BMV-712 Smart") }, + { 0xA382, F("BMV-710H Smart") }, + { 0xA383, F("BMV-712 Smart Rev2") }, + { 0xA389, F("SmartShunt 500A/50mV") }, + { 0xA38A, F("SmartShunt 1000A/50mV") }, + { 0xA38B, F("SmartShunt 2000A/50mV") }, + { 0xA3F0, F("SmartShunt 2000A/50mV" ) } + }; - switch(err) { - case 0: - strERR = "No error"; - break; - case 2: - strERR = "Battery voltage too high"; - break; - case 17: - strERR = "Charger temperature too high"; - break; - case 18: - strERR = "Charger over current"; - break; - case 19: - strERR = "Charger current reversed"; - break; - case 20: - strERR = "Bulk time limit exceeded"; - break; - case 21: - strERR = "Current sensor issue(sensor bias/sensor broken)"; - break; - case 26: - strERR = "Terminals overheated"; - break; - case 28: - strERR = "Converter issue (dual converter models only)"; - break; - case 33: - strERR = "Input voltage too high (solar panel)"; - break; - case 34: - strERR = "Input current too high (solar panel)"; - break; - case 38: - strERR = "Input shutdown (due to excessive battery voltage)"; - break; - case 39: - strERR = "Input shutdown (due to current flow during off mode)"; - break; - case 40: - strERR = "Input"; - break; - case 65: - strERR = "Lost communication with one of devices"; - break; - case 67: - strERR = "Synchronisedcharging device configuration issue"; - break; - case 68: - strERR = "BMS connection lost"; - break; - case 116: - strERR = "Factory calibration data lost"; - break; - case 117: - strERR = "Invalid/incompatible firmware"; - break; - case 118: - strERR = "User settings invalid"; - break; - default: - strERR = err; - } - return strERR; + return getAsString(values, PID); } diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 2cb554875..bc6678b53 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -13,45 +13,47 @@ #include #include +#include #include #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 - int32_t P = 0; // battery output power in W (calculated) - double V = 0; // battery voltage in V - double I = 0; // battery current in A - double E = 0; // efficiency in percent (calculated, moving average) -} veStruct; - class VeDirectFrameHandler { public: VeDirectFrameHandler(); - void setVerboseLogging(bool verboseLogging); virtual void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); void loop(); // main loop to read ve.direct data - unsigned long getLastUpdate(); // timestamp of last successful frame read - bool isDataValid(veStruct frame); // return true if data valid and not outdated - String getPidAsString(uint16_t pid); // product id as string - String getErrAsString(uint8_t err); // errer state as string + uint32_t getLastUpdate() const; // timestamp of last successful frame read protected: - void textRxEvent(char *, char *, veStruct& ); - bool _verboseLogging; 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) + + String getPidAsString() const; // product id as string + } veStruct; + + bool textRxEvent(std::string const& who, char* name, char* value, veStruct& frame); + bool isDataValid(veStruct const& frame) const; // return true if data valid and not outdated + + template + static String const& getAsString(std::map const& values, T val); + private: void setLastUpdate(); // set timestampt after successful frame read void dumpDebugBuffer(); void rxData(uint8_t inbyte); // byte of serial data virtual void textRxEvent(char *, char *) = 0; - virtual void frameEndEvent(bool) = 0; // copy temp struct to public struct + virtual void frameValidEvent() = 0; int hexRxEvent(uint8_t); std::unique_ptr _vedirectSerial; diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 0f8246d7e..5635cd45f 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -1,25 +1,24 @@ #include +#include #include "VeDirectMpptController.h" -VeDirectMpptController VeDirectMppt; - -VeDirectMpptController::VeDirectMpptController() -{ -} - void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) { VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 1); + _spData = std::make_shared(); if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); } } -bool VeDirectMpptController::isDataValid() { - return VeDirectFrameHandler::isDataValid(veFrame); +bool VeDirectMpptController::isDataValid() const { + return VeDirectFrameHandler::isDataValid(*_spData); } -void VeDirectMpptController::textRxEvent(char * name, char * value) { - if (_verboseLogging) { _msgOut->printf("[Victron MPPT] Received Text Event %s: Value: %s\r\n", name, value ); } - VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame); +void VeDirectMpptController::textRxEvent(char* name, char* value) +{ + if (VeDirectFrameHandler::textRxEvent("MPPT", name, value, _tmpFrame)) { + return; + } + if (strcmp(name, "LOAD") == 0) { if (strcmp(value, "ON") == 0) _tmpFrame.LOAD = true; @@ -65,139 +64,114 @@ void VeDirectMpptController::textRxEvent(char * name, char * value) { } /* - * frameEndEvent - * This function is called at the end of the received frame. If the checksum is valid, the temp buffer is read line by line. - * If the name exists in the public buffer, the new value is copied to the public buffer. If not, a new name/value entry - * is created in the public buffer. + * frameValidEvent + * This function is called at the end of the received frame. */ -void VeDirectMpptController::frameEndEvent(bool valid) { - if (valid) { - _tmpFrame.P = _tmpFrame.V * _tmpFrame.I; - - _tmpFrame.IPV = 0; - if (_tmpFrame.VPV > 0) { - _tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV; - } +void VeDirectMpptController::frameValidEvent() { + _tmpFrame.P = _tmpFrame.V * _tmpFrame.I; - _tmpFrame.E = 0; - if ( _tmpFrame.PPV > 0) { - _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); - _tmpFrame.E = _efficiency.getAverage(); - } + _tmpFrame.IPV = 0; + if (_tmpFrame.VPV > 0) { + _tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV; + } - veFrame = _tmpFrame; - _tmpFrame = {}; - _lastUpdate = millis(); + _tmpFrame.E = 0; + if ( _tmpFrame.PPV > 0) { + _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); + _tmpFrame.E = _efficiency.getAverage(); } + + _spData = std::make_shared(_tmpFrame); + _tmpFrame = {}; + _lastUpdate = millis(); } /* * getCsAsString * This function returns the state of operations (CS) as readable text. */ -String VeDirectMpptController::getCsAsString(uint8_t cs) +String VeDirectMpptController::veMpptStruct::getCsAsString() const { - String strCS =""; - - switch(cs) { - case 0: - strCS = "OFF"; - break; - case 2: - strCS = "Fault"; - break; - case 3: - strCS = "Bulk"; - break; - case 4: - strCS = "Absorbtion"; - break; - case 5: - strCS = "Float"; - break; - case 7: - strCS = "Equalize (manual)"; - break; - case 245: - strCS = "Starting-up"; - break; - case 247: - strCS = "Auto equalize / Recondition"; - break; - case 252: - strCS = "External Control"; - break; - default: - strCS = cs; - } - return strCS; + static const std::map values = { + { 0, F("OFF") }, + { 2, F("Fault") }, + { 3, F("Bulk") }, + { 4, F("Absorbtion") }, + { 5, F("Float") }, + { 7, F("Equalize (manual)") }, + { 245, F("Starting-up") }, + { 247, F("Auto equalize / Recondition") }, + { 252, F("External Control") } + }; + + return getAsString(values, CS); } /* * getMpptAsString * This function returns the state of MPPT (MPPT) as readable text. */ -String VeDirectMpptController::getMpptAsString(uint8_t mppt) +String VeDirectMpptController::veMpptStruct::getMpptAsString() const { - String strMPPT =""; - - switch(mppt) { - case 0: - strMPPT = "OFF"; - break; - case 1: - strMPPT = "Voltage or current limited"; - break; - case 2: - strMPPT = "MPP Tracker active"; - break; - default: - strMPPT = mppt; - } - return strMPPT; + static const std::map values = { + { 0, F("OFF") }, + { 1, F("Voltage or current limited") }, + { 2, F("MPP Tracker active") } + }; + + return getAsString(values, MPPT); +} + +/* + * getErrAsString + * This function returns error state (ERR) as readable text. + */ +String VeDirectMpptController::veMpptStruct::getErrAsString() const +{ + static const std::map values = { + { 0, F("No error") }, + { 2, F("Battery voltage too high") }, + { 17, F("Charger temperature too high") }, + { 18, F("Charger over current") }, + { 19, F("Charger current reversed") }, + { 20, F("Bulk time limit exceeded") }, + { 21, F("Current sensor issue(sensor bias/sensor broken)") }, + { 26, F("Terminals overheated") }, + { 28, F("Converter issue (dual converter models only)") }, + { 33, F("Input voltage too high (solar panel)") }, + { 34, F("Input current too high (solar panel)") }, + { 38, F("Input shutdown (due to excessive battery voltage)") }, + { 39, F("Input shutdown (due to current flow during off mode)") }, + { 40, F("Input") }, + { 65, F("Lost communication with one of devices") }, + { 67, F("Synchronisedcharging device configuration issue") }, + { 68, F("BMS connection lost") }, + { 116, F("Factory calibration data lost") }, + { 117, F("Invalid/incompatible firmware") }, + { 118, F("User settings invalid") } + }; + + return getAsString(values, ERR); } /* * getOrAsString * This function returns the off reason (OR) as readable text. */ -String VeDirectMpptController::getOrAsString(uint32_t offReason) +String VeDirectMpptController::veMpptStruct::getOrAsString() const { - String strOR =""; - - switch(offReason) { - case 0x00000000: - strOR = "Not off"; - break; - case 0x00000001: - strOR = "No input power"; - break; - case 0x00000002: - strOR = "Switched off (power switch)"; - break; - case 0x00000004: - strOR = "Switched off (device moderegister)"; - break; - case 0x00000008: - strOR = "Remote input"; - break; - case 0x00000010: - strOR = "Protection active"; - break; - case 0x00000020: - strOR = "Paygo"; - break; - case 0x00000040: - strOR = "BMS"; - break; - case 0x00000080: - strOR = "Engine shutdown detection"; - break; - case 0x00000100: - strOR = "Analysing input voltage"; - break; - default: - strOR = offReason; - } - return strOR; + static const std::map values = { + { 0x00000000, F("Not off") }, + { 0x00000001, F("No input power") }, + { 0x00000002, F("Switched off (power switch)") }, + { 0x00000004, F("Switched off (device moderegister)") }, + { 0x00000008, F("Remote input") }, + { 0x00000010, F("Protection active") }, + { 0x00000020, F("Paygo") }, + { 0x00000040, F("BMS") }, + { 0x00000080, F("Engine shutdown detection") }, + { 0x00000100, F("Analysing input voltage") } + }; + + return getAsString(values, OR); } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index 789454298..04e0d8ca4 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -37,17 +37,15 @@ class MovingAverage { class VeDirectMpptController : public VeDirectFrameHandler { public: - VeDirectMpptController(); + VeDirectMpptController() = default; void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging); - String getMpptAsString(uint8_t mppt); // state of mppt as string - String getCsAsString(uint8_t cs); // current state as string - String getOrAsString(uint32_t offReason); // off reason as string - bool isDataValid(); // return true if data valid and not outdated + 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) @@ -60,15 +58,20 @@ class VeDirectMpptController : public VeDirectFrameHandler { int32_t H21; // maximum power today W double H22; // yield yesterday kWh int32_t H23; // maximum power yesterday W + + String getMpptAsString() const; // state of mppt as string + String getCsAsString() const; // current state as string + String getErrAsString() const; // error state as string + String getOrAsString() const; // off reason as string }; - veMpptStruct veFrame{}; + using spData_t = std::shared_ptr; + spData_t getData() const { return _spData; } private: - void textRxEvent(char * name, char * value) final; - void frameEndEvent(bool) final; // copy temp struct to public struct + void textRxEvent(char* name, char* value) final; + void frameValidEvent() final; + spData_t _spData = nullptr; veMpptStruct _tmpFrame{}; // private struct for received name and value pairs MovingAverage _efficiency; }; - -extern VeDirectMpptController VeDirectMppt; \ No newline at end of file diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp index 249472c54..7a1fa59a5 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -17,10 +17,10 @@ void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool ver void VeDirectShuntController::textRxEvent(char* name, char* value) { - VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame); - if (_verboseLogging) { - _msgOut->printf("[Victron SmartShunt] Received Text Event %s: Value: %s\r\n", name, value ); + if (VeDirectFrameHandler::textRxEvent("SmartShunt", name, value, _tmpFrame)) { + return; } + if (strcmp(name, "T") == 0) { _tmpFrame.T = atoi(value); } @@ -96,18 +96,16 @@ void VeDirectShuntController::textRxEvent(char* name, char* value) } /* - * frameEndEvent - * This function is called at the end of the received frame. If the checksum is valid, the temp buffer is read line by line. - * If the name exists in the public buffer, the new value is copied to the public buffer. If not, a new name/value entry - * is created in the public buffer. + * frameValidEvent + * This function is called at the end of the received frame. */ -void VeDirectShuntController::frameEndEvent(bool valid) { +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 (valid && _tmpFrame.PID != 0) { - veFrame = _tmpFrame; - _tmpFrame = {}; - _lastUpdate = millis(); - } + if (_tmpFrame.PID == 0) { return; } + + veFrame = _tmpFrame; + _tmpFrame = {}; + _lastUpdate = millis(); } diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.h b/lib/VeDirectFrameHandler/VeDirectShuntController.h index 28ffd0718..115af35b6 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.h +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.h @@ -41,7 +41,7 @@ class VeDirectShuntController : public VeDirectFrameHandler { private: void textRxEvent(char * name, char * value) final; - void frameEndEvent(bool) final; // copy temp struct to public struct + void frameValidEvent() final; veShuntStruct _tmpFrame{}; // private struct for received name and value pairs }; diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 67b77ba99..b1f616b1b 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -208,7 +208,7 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct c _SoC = shuntData.SOC / 10; _voltage = shuntData.V; _current = shuntData.I; - _modelName = VeDirectShunt.getPidAsString(shuntData.PID); + _modelName = shuntData.getPidAsString(); _chargeCycles = shuntData.H4; _timeToGo = shuntData.TTG / 60; _chargedEnergy = shuntData.H18 / 100; diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index 85e392b86..e5a9be5c2 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -7,6 +7,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "MessageOutput.h" +#include "VictronMppt.h" MqttHandleVedirectHassClass MqttHandleVedirectHass; @@ -50,7 +51,7 @@ void MqttHandleVedirectHassClass::publishConfig() return; } // ensure data is revieved from victron - if (!VeDirectMppt.isDataValid()) { + if (!VictronMppt.isDataValid()) { return; } @@ -82,7 +83,7 @@ void MqttHandleVedirectHassClass::publishConfig() void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement ) { - String serial = VeDirectMppt.veFrame.SER; + String serial = VictronMppt.getData()->SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -94,9 +95,9 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* String configTopic = "sensor/dtu_victron_" + serial + "/" + sensorId + "/config"; - + String statTopic = MqttSettings.getPrefix() + "victron/"; - statTopic.concat(VeDirectMppt.veFrame.SER); + statTopic.concat(serial); statTopic.concat("/"); statTopic.concat(subTopic); @@ -133,7 +134,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* } void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off) { - String serial = VeDirectMppt.veFrame.SER; + String serial = VictronMppt.getData()->SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -147,7 +148,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const + "/config"; String statTopic = MqttSettings.getPrefix() + "victron/"; - statTopic.concat(VeDirectMppt.veFrame.SER); + statTopic.concat(serial); statTopic.concat("/"); statTopic.concat(subTopic); @@ -172,12 +173,13 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object) { - String serial = VeDirectMppt.veFrame.SER; + auto spMpptData = VictronMppt.getData(); + String serial = spMpptData->SER; object[F("name")] = "Victron(" + serial + ")"; object[F("ids")] = serial; object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString(); object[F("mf")] = F("OpenDTU"); - object[F("mdl")] = VeDirectMppt.getPidAsString(VeDirectMppt.veFrame.PID); + object[F("mdl")] = spMpptData->getPidAsString(); object[F("sw")] = AUTO_GIT_HASH; } diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index 6013fb243..7466fc1a6 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2022 Helge Erbe and others */ -#include "VeDirectMpptController.h" +#include "VictronMppt.h" #include "MqttHandleVedirect.h" #include "MqttSettings.h" #include "MessageOutput.h" @@ -29,7 +29,7 @@ void MqttHandleVedirectClass::loop() return; } - if (!VeDirectMppt.isDataValid()) { + if (!VictronMppt.isDataValid()) { return; } @@ -50,81 +50,82 @@ void MqttHandleVedirectClass::loop() } #endif + auto spMpptData = VictronMppt.getData(); String value; String topic = "victron/"; - topic.concat(VeDirectMppt.veFrame.SER); + topic.concat(spMpptData->SER); topic.concat("/"); - if (_PublishFull || VeDirectMppt.veFrame.PID != _kvFrame.PID) - MqttSettings.publish(topic + "PID", VeDirectMppt.getPidAsString(VeDirectMppt.veFrame.PID)); - if (_PublishFull || strcmp(VeDirectMppt.veFrame.SER, _kvFrame.SER) != 0) - MqttSettings.publish(topic + "SER", VeDirectMppt.veFrame.SER ); - if (_PublishFull || strcmp(VeDirectMppt.veFrame.FW, _kvFrame.FW) != 0) - MqttSettings.publish(topic + "FW", VeDirectMppt.veFrame.FW); - if (_PublishFull || VeDirectMppt.veFrame.LOAD != _kvFrame.LOAD) - MqttSettings.publish(topic + "LOAD", VeDirectMppt.veFrame.LOAD == true ? "ON": "OFF"); - if (_PublishFull || VeDirectMppt.veFrame.CS != _kvFrame.CS) - MqttSettings.publish(topic + "CS", VeDirectMppt.getCsAsString(VeDirectMppt.veFrame.CS)); - if (_PublishFull || VeDirectMppt.veFrame.ERR != _kvFrame.ERR) - MqttSettings.publish(topic + "ERR", VeDirectMppt.getErrAsString(VeDirectMppt.veFrame.ERR)); - if (_PublishFull || VeDirectMppt.veFrame.OR != _kvFrame.OR) - MqttSettings.publish(topic + "OR", VeDirectMppt.getOrAsString(VeDirectMppt.veFrame.OR)); - if (_PublishFull || VeDirectMppt.veFrame.MPPT != _kvFrame.MPPT) - MqttSettings.publish(topic + "MPPT", VeDirectMppt.getMpptAsString(VeDirectMppt.veFrame.MPPT)); - if (_PublishFull || VeDirectMppt.veFrame.HSDS != _kvFrame.HSDS) { - value = VeDirectMppt.veFrame.HSDS; + if (_PublishFull || spMpptData->PID != _kvFrame.PID) + MqttSettings.publish(topic + "PID", spMpptData->getPidAsString()); + if (_PublishFull || strcmp(spMpptData->SER, _kvFrame.SER) != 0) + MqttSettings.publish(topic + "SER", spMpptData->SER ); + if (_PublishFull || strcmp(spMpptData->FW, _kvFrame.FW) != 0) + MqttSettings.publish(topic + "FW", spMpptData->FW); + if (_PublishFull || spMpptData->LOAD != _kvFrame.LOAD) + MqttSettings.publish(topic + "LOAD", spMpptData->LOAD == true ? "ON": "OFF"); + if (_PublishFull || spMpptData->CS != _kvFrame.CS) + MqttSettings.publish(topic + "CS", spMpptData->getCsAsString()); + if (_PublishFull || spMpptData->ERR != _kvFrame.ERR) + MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString()); + if (_PublishFull || spMpptData->OR != _kvFrame.OR) + MqttSettings.publish(topic + "OR", spMpptData->getOrAsString()); + if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT) + MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString()); + if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) { + value = spMpptData->HSDS; MqttSettings.publish(topic + "HSDS", value); } - if (_PublishFull || VeDirectMppt.veFrame.V != _kvFrame.V) { - value = VeDirectMppt.veFrame.V; + if (_PublishFull || spMpptData->V != _kvFrame.V) { + value = spMpptData->V; MqttSettings.publish(topic + "V", value); } - if (_PublishFull || VeDirectMppt.veFrame.I != _kvFrame.I) { - value = VeDirectMppt.veFrame.I; + if (_PublishFull || spMpptData->I != _kvFrame.I) { + value = spMpptData->I; MqttSettings.publish(topic + "I", value); } - if (_PublishFull || VeDirectMppt.veFrame.P != _kvFrame.P) { - value = VeDirectMppt.veFrame.P; + if (_PublishFull || spMpptData->P != _kvFrame.P) { + value = spMpptData->P; MqttSettings.publish(topic + "P", value); } - if (_PublishFull || VeDirectMppt.veFrame.VPV != _kvFrame.VPV) { - value = VeDirectMppt.veFrame.VPV; + if (_PublishFull || spMpptData->VPV != _kvFrame.VPV) { + value = spMpptData->VPV; MqttSettings.publish(topic + "VPV", value); } - if (_PublishFull || VeDirectMppt.veFrame.IPV != _kvFrame.IPV) { - value = VeDirectMppt.veFrame.IPV; + if (_PublishFull || spMpptData->IPV != _kvFrame.IPV) { + value = spMpptData->IPV; MqttSettings.publish(topic + "IPV", value); } - if (_PublishFull || VeDirectMppt.veFrame.PPV != _kvFrame.PPV) { - value = VeDirectMppt.veFrame.PPV; + if (_PublishFull || spMpptData->PPV != _kvFrame.PPV) { + value = spMpptData->PPV; MqttSettings.publish(topic + "PPV", value); } - if (_PublishFull || VeDirectMppt.veFrame.E != _kvFrame.E) { - value = VeDirectMppt.veFrame.E; + if (_PublishFull || spMpptData->E != _kvFrame.E) { + value = spMpptData->E; MqttSettings.publish(topic + "E", value); } - if (_PublishFull || VeDirectMppt.veFrame.H19 != _kvFrame.H19) { - value = VeDirectMppt.veFrame.H19; + if (_PublishFull || spMpptData->H19 != _kvFrame.H19) { + value = spMpptData->H19; MqttSettings.publish(topic + "H19", value); } - if (_PublishFull || VeDirectMppt.veFrame.H20 != _kvFrame.H20) { - value = VeDirectMppt.veFrame.H20; + if (_PublishFull || spMpptData->H20 != _kvFrame.H20) { + value = spMpptData->H20; MqttSettings.publish(topic + "H20", value); } - if (_PublishFull || VeDirectMppt.veFrame.H21 != _kvFrame.H21) { - value = VeDirectMppt.veFrame.H21; + if (_PublishFull || spMpptData->H21 != _kvFrame.H21) { + value = spMpptData->H21; MqttSettings.publish(topic + "H21", value); } - if (_PublishFull || VeDirectMppt.veFrame.H22 != _kvFrame.H22) { - value = VeDirectMppt.veFrame.H22; + if (_PublishFull || spMpptData->H22 != _kvFrame.H22) { + value = spMpptData->H22; MqttSettings.publish(topic + "H22", value); } - if (_PublishFull || VeDirectMppt.veFrame.H23 != _kvFrame.H23) { - value = VeDirectMppt.veFrame.H23; + if (_PublishFull || spMpptData->H23 != _kvFrame.H23) { + value = spMpptData->H23; MqttSettings.publish(topic + "H23", value); } if (!_PublishFull) { - _kvFrame= VeDirectMppt.veFrame; + _kvFrame = *spMpptData; } // now calculate next points of time to publish diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index b680edb3e..2cc48906d 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -305,11 +305,6 @@ bool PinMappingClass::isValidEthConfig() return _pinMapping.eth_enabled; } -bool PinMappingClass::isValidVictronConfig() -{ - return _pinMapping.victron_rx >= 0; -} - bool PinMappingClass::isValidHuaweiConfig() { return _pinMapping.huawei_miso >= 0 diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index c5c3dc868..feb41dd80 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -10,7 +10,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "Huawei_can.h" -#include +#include #include "MessageOutput.h" #include #include @@ -364,14 +364,12 @@ int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr */ void PowerLimiterClass::unconditionalSolarPassthrough(std::shared_ptr inverter) { - CONFIG_T& config = Configuration.get(); - - if (!config.Vedirect_Enabled || !VeDirectMppt.isDataValid()) { + if (!VictronMppt.isDataValid()) { shutdown(Status::NoVeDirect); return; } - int32_t solarPower = VeDirectMppt.veFrame.V * VeDirectMppt.veFrame.I; + int32_t solarPower = VictronMppt.getPowerOutputWatts(); setNewPowerLimit(inverter, inverterPowerDcToAc(inverter, solarPower)); announceStatus(Status::UnconditionalSolarPassthrough); } @@ -406,12 +404,11 @@ bool PowerLimiterClass::canUseDirectSolarPower() if (!config.PowerLimiter_SolarPassThroughEnabled || isBelowStopThreshold() - || !config.Vedirect_Enabled - || !VeDirectMppt.isDataValid()) { + || !VictronMppt.isDataValid()) { return false; } - return VeDirectMppt.veFrame.PPV >= 20; // enough power? + return VictronMppt.getPowerOutputWatts() >= 20; // enough power? } @@ -576,7 +573,7 @@ int32_t PowerLimiterClass::getSolarChargePower() return 0; } - return VeDirectMppt.veFrame.V * VeDirectMppt.veFrame.I; + return VictronMppt.getPowerOutputWatts(); } float PowerLimiterClass::getLoadCorrectedVoltage() diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp new file mode 100644 index 000000000..b857e0ec0 --- /dev/null +++ b/src/VictronMppt.cpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "VictronMppt.h" +#include "Configuration.h" +#include "PinMapping.h" +#include "MessageOutput.h" + +VictronMpptClass VictronMppt; + +void VictronMpptClass::init() +{ + std::lock_guard lock(_mutex); + + _controllers.clear(); + + CONFIG_T& config = Configuration.get(); + if (!config.Vedirect_Enabled) { return; } + + const PinMapping_t& pin = PinMapping.get(); + int8_t rx = pin.victron_rx; + int8_t tx = pin.victron_tx; + + MessageOutput.printf("[VictronMppt] rx = %d, tx = %d\r\n", rx, tx); + + if (rx < 0) { + MessageOutput.println(F("[VictronMppt] invalid pin config")); + return; + } + + auto upController = std::make_unique(); + upController->init(rx, tx, &MessageOutput, config.Vedirect_VerboseLogging); + _controllers.push_back(std::move(upController)); +} + +void VictronMpptClass::loop() +{ + std::lock_guard lock(_mutex); + + for (auto const& upController : _controllers) { + upController->loop(); + } +} + +bool VictronMpptClass::isDataValid() const +{ + std::lock_guard lock(_mutex); + + for (auto const& upController : _controllers) { + if (!upController->isDataValid()) { return false; } + } + + return !_controllers.empty(); +} + +uint32_t VictronMpptClass::getDataAgeMillis() const +{ + std::lock_guard lock(_mutex); + + if (_controllers.empty()) { return 0; } + + auto now = millis(); + + auto iter = _controllers.cbegin(); + uint32_t age = now - (*iter)->getLastUpdate(); + ++iter; + + while (iter != _controllers.end()) { + age = std::min(age, now - (*iter)->getLastUpdate()); + ++iter; + } + + return age; +} + +VeDirectMpptController::spData_t VictronMpptClass::getData(size_t idx) const +{ + std::lock_guard lock(_mutex); + + if (_controllers.empty() || idx >= _controllers.size()) { + MessageOutput.printf("ERROR: MPPT controller index %d is out of bounds (%d controllers)\r\n", + idx, _controllers.size()); + return VeDirectMpptController::spData_t{}; + } + + return _controllers[idx]->getData(); +} + +int32_t VictronMpptClass::getPowerOutputWatts() const +{ + int32_t sum = 0; + + for (const auto& upController : _controllers) { + sum += upController->getData()->P; + } + + return sum; +} diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index d92fda0a0..1b8842655 100644 --- a/src/WebApi_vedirect.cpp +++ b/src/WebApi_vedirect.cpp @@ -3,7 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "WebApi_vedirect.h" -#include "VeDirectMpptController.h" +#include "VictronMppt.h" #include "ArduinoJson.h" #include "AsyncJson.h" #include "Configuration.h" @@ -117,7 +117,7 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) config.Vedirect_UpdatesOnly = root[F("vedirect_updatesonly")].as(); Configuration.write(); - VeDirectMppt.setVerboseLogging(config.Vedirect_VerboseLogging); + VictronMppt.init(); retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 463f629c6..fdca8baae 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -10,7 +10,7 @@ #include "Battery.h" #include "Huawei_can.h" #include "PowerMeter.h" -#include "VeDirectMpptController.h" +#include "VictronMppt.h" #include "defaults.h" #include @@ -191,10 +191,12 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) JsonObject vedirectObj = root.createNestedObject("vedirect"); vedirectObj[F("enabled")] = Configuration.get().Vedirect_Enabled; JsonObject totalVeObj = vedirectObj.createNestedObject("total"); - addTotalField(totalVeObj, "Power", VeDirectMppt.veFrame.PPV, "W", 1); - addTotalField(totalVeObj, "YieldDay", VeDirectMppt.veFrame.H20 * 1000, "Wh", 0); - addTotalField(totalVeObj, "YieldTotal", VeDirectMppt.veFrame.H19, "kWh", 2); - + + auto spMpptData = VictronMppt.getData(); + addTotalField(totalVeObj, "Power", spMpptData->PPV, "W", 1); + addTotalField(totalVeObj, "YieldDay", spMpptData->H20 * 1000, "Wh", 0); + addTotalField(totalVeObj, "YieldTotal", spMpptData->H19, "kWh", 2); + JsonObject huaweiObj = root.createNestedObject("huawei"); huaweiObj[F("enabled")] = Configuration.get().Huawei_Enabled; const RectifierParameters_t * rp = HuaweiCan.get(); diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 5f26cf348..53c81d122 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -9,6 +9,7 @@ #include "WebApi.h" #include "defaults.h" #include "PowerLimiter.h" +#include "VictronMppt.h" WebApiWsVedirectLiveClass::WebApiWsVedirectLiveClass() : _ws("/vedirectlivedata") @@ -44,18 +45,14 @@ void WebApiWsVedirectLiveClass::loop() return; } - if (millis() - _lastVedirectUpdateCheck < 1000) { - return; - } - _lastVedirectUpdateCheck = millis(); - - uint32_t maxTimeStamp = 0; - if (VeDirectMppt.getLastUpdate() > maxTimeStamp) { - maxTimeStamp = VeDirectMppt.getLastUpdate(); - } + // we assume this loop to be running at least twice for every + // update from a VE.Direct MPPT data producer, so _dataAgeMillis + // acutally grows in between updates. + auto lastDataAgeMillis = _dataAgeMillis; + _dataAgeMillis = VictronMppt.getDataAgeMillis(); // Update on ve.direct change or at least after 10 seconds - if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestVedirectTimestamp)) { + if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) { try { String buffer; @@ -87,57 +84,59 @@ void WebApiWsVedirectLiveClass::loop() void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) { + auto spMpptData = VictronMppt.getData(); + // device info - root["device"]["data_age"] = (millis() - VeDirectMppt.getLastUpdate() ) / 1000; - root["device"]["age_critical"] = !VeDirectMppt.isDataValid(); - root["device"]["PID"] = VeDirectMppt.getPidAsString(VeDirectMppt.veFrame.PID); - root["device"]["SER"] = VeDirectMppt.veFrame.SER; - root["device"]["FW"] = VeDirectMppt.veFrame.FW; - root["device"]["LOAD"] = VeDirectMppt.veFrame.LOAD == true ? "ON" : "OFF"; - root["device"]["CS"] = VeDirectMppt.getCsAsString(VeDirectMppt.veFrame.CS); - root["device"]["ERR"] = VeDirectMppt.getErrAsString(VeDirectMppt.veFrame.ERR); - root["device"]["OR"] = VeDirectMppt.getOrAsString(VeDirectMppt.veFrame.OR); - root["device"]["MPPT"] = VeDirectMppt.getMpptAsString(VeDirectMppt.veFrame.MPPT); - root["device"]["HSDS"]["v"] = VeDirectMppt.veFrame.HSDS; + root["device"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000; + root["device"]["age_critical"] = !VictronMppt.isDataValid(); + root["device"]["PID"] = spMpptData->getPidAsString(); + root["device"]["SER"] = spMpptData->SER; + root["device"]["FW"] = spMpptData->FW; + root["device"]["LOAD"] = spMpptData->LOAD == true ? "ON" : "OFF"; + root["device"]["CS"] = spMpptData->getCsAsString(); + root["device"]["ERR"] = spMpptData->getErrAsString(); + root["device"]["OR"] = spMpptData->getOrAsString(); + root["device"]["MPPT"] = spMpptData->getMpptAsString(); + root["device"]["HSDS"]["v"] = spMpptData->HSDS; root["device"]["HSDS"]["u"] = "d"; // battery info - root["output"]["P"]["v"] = VeDirectMppt.veFrame.P; + root["output"]["P"]["v"] = spMpptData->P; root["output"]["P"]["u"] = "W"; root["output"]["P"]["d"] = 0; - root["output"]["V"]["v"] = VeDirectMppt.veFrame.V; + root["output"]["V"]["v"] = spMpptData->V; root["output"]["V"]["u"] = "V"; root["output"]["V"]["d"] = 2; - root["output"]["I"]["v"] = VeDirectMppt.veFrame.I; + root["output"]["I"]["v"] = spMpptData->I; root["output"]["I"]["u"] = "A"; root["output"]["I"]["d"] = 2; - root["output"]["E"]["v"] = VeDirectMppt.veFrame.E; + root["output"]["E"]["v"] = spMpptData->E; root["output"]["E"]["u"] = "%"; root["output"]["E"]["d"] = 1; // panel info - root["input"]["PPV"]["v"] = VeDirectMppt.veFrame.PPV; + root["input"]["PPV"]["v"] = spMpptData->PPV; root["input"]["PPV"]["u"] = "W"; root["input"]["PPV"]["d"] = 0; - root["input"]["VPV"]["v"] = VeDirectMppt.veFrame.VPV; + root["input"]["VPV"]["v"] = spMpptData->VPV; root["input"]["VPV"]["u"] = "V"; root["input"]["VPV"]["d"] = 2; - root["input"]["IPV"]["v"] = VeDirectMppt.veFrame.IPV; + root["input"]["IPV"]["v"] = spMpptData->IPV; root["input"]["IPV"]["u"] = "A"; root["input"]["IPV"]["d"] = 2; - root["input"]["YieldToday"]["v"] = VeDirectMppt.veFrame.H20; + root["input"]["YieldToday"]["v"] = spMpptData->H20; root["input"]["YieldToday"]["u"] = "kWh"; root["input"]["YieldToday"]["d"] = 3; - root["input"]["YieldYesterday"]["v"] = VeDirectMppt.veFrame.H22; + root["input"]["YieldYesterday"]["v"] = spMpptData->H22; root["input"]["YieldYesterday"]["u"] = "kWh"; root["input"]["YieldYesterday"]["d"] = 3; - root["input"]["YieldTotal"]["v"] = VeDirectMppt.veFrame.H19; + root["input"]["YieldTotal"]["v"] = spMpptData->H19; root["input"]["YieldTotal"]["u"] = "kWh"; root["input"]["YieldTotal"]["d"] = 3; - root["input"]["MaximumPowerToday"]["v"] = VeDirectMppt.veFrame.H21; + root["input"]["MaximumPowerToday"]["v"] = spMpptData->H21; root["input"]["MaximumPowerToday"]["u"] = "W"; root["input"]["MaximumPowerToday"]["d"] = 0; - root["input"]["MaximumPowerYesterday"]["v"] = VeDirectMppt.veFrame.H23; + root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23; root["input"]["MaximumPowerYesterday"]["u"] = "W"; root["input"]["MaximumPowerYesterday"]["d"] = 0; @@ -146,10 +145,6 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) if (Configuration.get().PowerLimiter_Enabled) root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); - - if (VeDirectMppt.getLastUpdate() > _newestVedirectTimestamp) { - _newestVedirectTimestamp = VeDirectMppt.getLastUpdate(); - } } void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) diff --git a/src/main.cpp b/src/main.cpp index 3851b1274..b39845ca0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,7 +8,7 @@ #include "InverterSettings.h" #include "Led_Single.h" #include "MessageOutput.h" -#include "VeDirectMpptController.h" +#include "VictronMppt.h" #include "Battery.h" #include "Huawei_can.h" #include "MqttHandleDtu.h" @@ -161,16 +161,8 @@ void setup() Datastore.init(); - // Initialize ve.direct communication - MessageOutput.println(F("Initialize ve.direct interface... ")); - if (PinMapping.isValidVictronConfig()) { - MessageOutput.printf("ve.direct rx = %d, tx = %d\r\n", pin.victron_rx, pin.victron_tx); - VeDirectMppt.init(pin.victron_rx, pin.victron_tx, - &MessageOutput, config.Vedirect_VerboseLogging); - MessageOutput.println(F("done")); - } else { - MessageOutput.println(F("Invalid pin config")); - } + VictronMppt.init(); + // Power meter PowerMeter.init(); @@ -202,11 +194,8 @@ void loop() yield(); Datastore.loop(); yield(); - // Vedirect_Enabled is unknown to lib. Therefor check has to be done here - if (Configuration.get().Vedirect_Enabled) { - VeDirectMppt.loop(); - yield(); - } + VictronMppt.loop(); + yield(); MqttSettings.loop(); yield(); MqttHandleDtu.loop();