diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 63db05dd5..c26c97055 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -30,7 +30,7 @@ * 2020.05.05 - 0.2 - initial release * 2020.06.21 - 0.2 - add MIT license, no code changes * 2020.08.20 - 0.3 - corrected #include reference - * + * 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages */ #include @@ -274,8 +274,6 @@ void VeDirectFrameHandler::processTextData(std::string const& name, std::stri _logId, name.c_str(), value.c_str()); } - - /* * hexRxEvent * This function records hex answers or async messages @@ -287,12 +285,19 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) switch (inbyte) { case '\n': + // now we can analyse the hex message + _hexBuffer[_hexSize] = '\0'; + VeDirectHexData data; + if (disassembleHexData(data)) + hexDataHandler(data); + // restore previous state ret=_prevState; break; default: - _hexSize++; + _hexBuffer[_hexSize++]=inbyte; + if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort _msgOut->printf("%s hexRx buffer overflow - aborting read\r\n", _logId); _hexSize=0; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index dedf59726..a2cd28348 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -6,6 +6,7 @@ * 2020.05.05 - 0.2 - initial release * 2021.02.23 - 0.3 - change frameLen to 22 per VE.Direct Protocol version 3.30 * 2022.08.20 - 0.4 - changes for OpenDTU + * 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages * */ @@ -18,17 +19,52 @@ #include #include "VeDirectData.h" +// hex send commands +enum VeDirectHexCommand { + ENTER_BOOT = 0x00, + PING = 0x01, + APP_VERSION = 0x02, + PRODUCT_ID = 0x04, + RESTART = 0x06, + GET = 0x07, + SET = 0x08, + ASYNC = 0x0A, + UNKNOWN = 0x0F +}; + +// hex receive responses +enum VeDirectHexResponse { + R_DONE = 0x01, + R_UNKNOWN = 0x03, + R_ERROR = 0x04, + R_PING = 0x05, + R_GET = 0x07, + R_SET = 0x08, + R_ASYNC = 0x0A, +}; + +// hex response data, contains the disassembeled hex message, forwarded to virtual funktion hexDataHandler() +struct VeDirectHexData { + VeDirectHexResponse rsp; // hex response type + uint16_t id; // register address + uint8_t flag; // flag + uint32_t value; // value from register + char text[VE_MAX_HEX_LEN]; // text/string response +}; + template class VeDirectFrameHandler { public: - void loop(); // main loop to read ve.direct data + virtual 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 T const& getData() const { return _tmpFrame; } + bool sendHexCommand(VeDirectHexCommand cmd, uint16_t id = 0, uint32_t value = 0, uint8_t valunibble = 0); // send hex commands via ve.direct protected: VeDirectFrameHandler(); void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); + virtual void hexDataHandler(VeDirectHexData const &data) { } // handles the disassembeled hex response bool _verboseLogging; Print* _msgOut; @@ -44,6 +80,7 @@ class VeDirectFrameHandler { virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0; virtual void frameValidEvent() { } int hexRxEvent(uint8_t); + bool disassembleHexData(VeDirectHexData &data); //return true if disassembling was possible std::unique_ptr _vedirectSerial; int _state; // current state @@ -51,6 +88,7 @@ class VeDirectFrameHandler { uint8_t _checksum; // checksum value char * _textPointer; // pointer to the private buffer we're writing to, name or value int _hexSize; // length of hex buffer + char _hexBuffer[VE_MAX_HEX_LEN] = { }; // buffer for received hex frames char _name[VE_MAX_VALUE_LEN]; // buffer for the field name char _value[VE_MAX_VALUE_LEN]; // buffer for the field value std::array _debugBuffer; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp new file mode 100644 index 000000000..b8cd0f277 --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp @@ -0,0 +1,227 @@ +/* VeDirectFrame +HexHandler.cpp + * + * Library to read/write from Victron devices using VE.Direct Hex protocol. + * Add on to Victron framehandler reference implementation. + * + * How to use: + * 1. Use sendHexCommand() to send hex messages. Use the Victron documentation to find the parameter. + * 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function + * void VeDirectFrameHandler::hexDataHandler(VeDirectHexData const &data) + * to handle the received hex messages. All hex messages will be forwarted to function hexDataHandler() + * 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits. + * + * 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages + * + */ +#include +#include "VeDirectFrameHandler.h" + + +// support for debugging, 0=without extended logging, 1=with extended logging +constexpr int MODUL_DEBUG = 0; + + +/* + * calcHexFrameCheckSum() + * help function to calculate the hex checksum + */ +#define ascii2hex(v) (v-48-(v>='A'?7:0)) +#define hex2byte(b) (ascii2hex(*(b)))*16+((ascii2hex(*(b+1)))) +static uint8_t calcHexFrameCheckSum(const char* buffer, int size) { + uint8_t checksum=0x55-ascii2hex(buffer[1]); + for (int i=2; i(strtoul(help, nullptr, 16))); +} + + +/* + * disassembleHexData() + * analysis the hex message and extract: response, id, flag and value/text + * buffer: pointer to message (ascii hex little endian format) + * data: disassembeled message + * return: true = successful disassembeld, false = hex sum fault or message + * do not aligin with VE.Diekt syntax + */ +template +bool VeDirectFrameHandler::disassembleHexData(VeDirectHexData &data) { + bool state = false; + char * buffer = _hexBuffer; + auto len = strlen(buffer); + + // reset hex data first + data = {}; + + if ((len > 3) && (calcHexFrameCheckSum(buffer, len) == 0x00)) { + data.rsp = static_cast(AsciiHexLE2Int(buffer+1, 1)); + + switch (data.rsp) { + case R_DONE: + case R_ERROR: + case R_PING: + case R_UNKNOWN: + strncpy(data.text, buffer+2, len-4); + state = true; + break; + case R_GET: + case R_SET: + case R_ASYNC: + data.id = AsciiHexLE2Int(buffer+2, 4); + + // future option: to analyse the flag here? + data.flag = AsciiHexLE2Int(buffer+6, 2); + + if (len == 12) { // 8bit value + data.value = AsciiHexLE2Int(buffer+8, 2); + state = true; + } + + if (len == 14) { // 16bit value + data.value = AsciiHexLE2Int(buffer+8, 4); + state = true; + } + + if (len == 18) { // 32bit value + data.value = AsciiHexLE2Int(buffer+8, 8); + state = true; + } + break; + default: + break; // something went wrong + } + } + + if constexpr(MODUL_DEBUG == 1) { + _msgOut->printf("[VE.Direct] debug: disassembleHexData(), rsp: %i, id: 0x%04X, value: 0x%X, Flag: 0x%02X\r\n", + data.rsp, data.id, data.value, data.flag); + } + + if (_verboseLogging && !state) + _msgOut->printf("[VE.Direct] failed to disassemble the hex message: %s\r\n", buffer); + + return (state); +} + + +/* + * uint2toHexLEString() + * help function to convert up to 32 bits into little endian hex String + * ascii: pointer to Ascii Hex Little Endian data + * anz: 1,2,4 or 8 nibble + */ +static String Int2HexLEString(uint32_t value, uint8_t anz) { + char hexchar[] = "0123456789ABCDEF"; + char help[9] = {}; + + switch (anz) { + case 1: + help[0] = hexchar[(value & 0x0000000F)]; + break; + case 2: + case 4: + case 8: + for (uint8_t i = 0; i < anz; i += 2) { + help[i] = hexchar[(value>>((1+1*i)*4)) & 0x0000000F]; + help[i+1] = hexchar[(value>>((1*i)*4)) & 0x0000000F]; + } + default: + ; + } + return String(help); +} + + +/* + * sendHexCommand() + * send the hex commend after assembling the command string + * cmd: command + * id: id/register, default 0 + * value: value to write into a id/register, default 0 + * valsize: size of the value/id, 8, 16 or 32 bit, default 0 + * return: true = message assembeld and send, false = it was not possible to put the message together + * SAMPLE: ping command: sendHexCommand(PING), + * read total DC input power sendHexCommand(GET, 0xEDEC) + * set Charge current limit 10A sendHexCommand(SET, 0x2015, 64, 16) + * + * WARNING: some values are stored in non-volatile memory. Continuous writing, for example from a control loop, will + * lead to early failure. + * On MPPT for example 0xEDE0 - 0xEDFF. Check the Vivtron doc "BlueSolar-HEX-protocol.pdf" + */ +template +bool VeDirectFrameHandler::sendHexCommand(VeDirectHexCommand cmd, uint16_t id, uint32_t value, uint8_t valsize) { + bool ret = false; + uint8_t flag = 0x00; // always 0x00 + + String txData = ":" + Int2HexLEString(cmd, 1); // add the command nibble + + switch (cmd) { + case PING: + case APP_VERSION: + case PRODUCT_ID: + ret = true; + break; + case GET: + case ASYNC: + txData += Int2HexLEString(id, 4); // add the id/register (4 nibble) + txData += Int2HexLEString(flag, 2); // add the flag (2 nibble) + ret = true; + break; + case SET: + txData += Int2HexLEString(id, 4); // add the id/register (4 nibble) + txData += Int2HexLEString(flag, 2); // add the flag (2 nibble) + if ((valsize == 8) || (valsize == 16) || (valsize == 32)) { + txData += Int2HexLEString(value, valsize/4); // add value (2-8 nibble) + ret = true; + } + break; + default: + ret = false; + break; + } + + if (ret) { + // add the checksum (2 nibble) + txData += Int2HexLEString(calcHexFrameCheckSum(txData.c_str(), txData.length()), 2); + String send = txData + "\n"; // hex command end byte + _vedirectSerial->write(send.c_str(), send.length()); + + if constexpr(MODUL_DEBUG == 1) { + auto blen = _vedirectSerial->availableForWrite(); + _msgOut->printf("[VE.Direct] debug: sendHexCommand(): %s, Free FIFO-Buffer: %u\r\n", txData.c_str(), blen); + } + } + + if (_verboseLogging && !ret) + _msgOut->println("[VE.Direct] send hex command fault:" + txData); + + return (ret); +} diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 63825d47a..9e94356ae 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -1,6 +1,20 @@ +/* VeDirectMpptController.cpp + * + * + * 2020.08.20 - 0.0 - ??? + * 2024.03.18 - 0.1 - add of: - temperature from "Smart Battery Sense" connected over VE.Smart network + * - temperature from internal MPPT sensor + * - "total DC input power" from MPPT's connected over VE.Smart network + */ + #include #include "VeDirectMpptController.h" + +// support for debugging, 0=without extended logging, 1=with extended logging +constexpr int MODUL_DEBUG = 0; + + void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, verboseLogging, hwSerialPort); @@ -80,3 +94,95 @@ void VeDirectMpptController::frameValidEvent() { _tmpFrame.E = _efficiency.getAverage(); } } + + +/* +// loop() +// send hex commands to MPPT every 5 seconds +*/ +void VeDirectMpptController::loop() +{ + VeDirectFrameHandler::loop(); + + // Copy from the "VE.Direct Protocol" documentation + // For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the + // charger periodically sends human readable (TEXT) data to the serial port. For firmware + // versions v1.53 and above, the charger always periodically sends TEXT data to the serial port. + // --> We just use hex commandes for firmware >= 1.53 to keep text messages alive + if (atoi(_tmpFrame.FW) >= 153 ) { + if ((millis() - _lastPingTime) > 5000) { + + sendHexCommand(GET, 0x2027); // MPPT total DC input power + sendHexCommand(GET, 0xEDDB); // MPPT internal temperature + sendHexCommand(GET, 0xEDEC); // "Smart Battery Sense" temperature + sendHexCommand(GET, 0x200F); // Network info + _lastPingTime = millis(); + } + } +} + + +/* + * hexDataHandler() + * analyse the content of VE.Direct hex messages + * Handels the received hex data from the MPPT + */ +void VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) { + bool state = false; + + switch (data.rsp) { + case R_GET: + case R_ASYNC: + + // check if MPPT internal temperature is available + if(data.id == 0xEDDB) { + _ExData.T = static_cast(data.value) * 10; // conversion from unit [0.01°C] to unit [m°C] + _ExData.Tts = millis(); + state = true; + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(), MTTP Temperature: %.2f°C\r\n", _ExData.T/1000.0); + } + + // check if temperature from "Smart Battery Sense" is available + if(data.id == 0xEDEC) { + _ExData.TSBS = static_cast(data.value) * 10 - 272150; // conversion from unit [0.01K] to unit [m°C] + _ExData.TSBSts = millis(); + state = true; + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(), Battery Temperature: %.2f°C\r\n", _ExData.TSBS/1000.0); + } + + // check if "Total DC power" is available + if(data.id == 0x2027) { + _ExData.TDCP = data.value * 10; // conversion from unit [0.01W] to unit [mW] + _ExData.TDCPts = millis(); + state = true; + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(), Total Power: %.2fW\r\n", _ExData.TDCP/1000.0); + } + + // check if connected MPPT is charge instance master + // Hint: not used right now but maybe necessary for future extensions + if(data.id == 0x200F) { + _veMaster = ((data.value & 0x0F) == 0x02) ? true : false; + state = true; + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(), Networkmode: 0x%X\r\n", data.value); + } + break; + default: + break; + } + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(): rsp: %i, id: 0x%04X, value: %i[0x%08X], text: %s\r\n", + data.rsp, data.id, data.value, data.value, data.text); + + if (_verboseLogging && state) + _msgOut->printf("[VE.Direct] MPPT hex message: rsp: %i, id: 0x%04X, value: %i[0x%08X], text: %s\r\n", + data.rsp, data.id, data.value, data.value, data.text); +} diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index eddf8be70..ebee88513 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -44,8 +44,24 @@ class VeDirectMpptController : public VeDirectFrameHandler { using data_t = veMpptStruct; + virtual void loop() final; // main loop to read ve.direct data + + struct veMPPTExStruct { + int32_t T; // temperature [m°C] from internal MPPT sensor + unsigned long Tts; // time of last recieved value + int32_t TSBS; // temperature [m°C] from the "Smart Battery Sense" + unsigned long TSBSts; // time of last recieved value + uint32_t TDCP; // total DC input power [mW] + unsigned long TDCPts; // time of last recieved value + }; + veMPPTExStruct _ExData{}; + veMPPTExStruct const *getExData() const { return &_ExData; } + private: + void hexDataHandler(VeDirectHexData const &data) final; bool processTextDataDerived(std::string const& name, std::string const& value) final; void frameValidEvent() final; MovingAverage _efficiency; + unsigned long _lastPingTime = 0L; // time of last device PING/GET hex command + bool _veMaster = true; // MPPT is instance master };