diff --git a/include/battery/victronsmartshunt/Stats.h b/include/battery/victronsmartshunt/Stats.h index 9dfd49923..65a33d2d3 100644 --- a/include/battery/victronsmartshunt/Stats.h +++ b/include/battery/victronsmartshunt/Stats.h @@ -31,6 +31,7 @@ class Stats : public ::Batteries::Stats { bool _alarmLowSOC; bool _alarmLowTemperature; bool _alarmHighTemperature; + float _transmissionErrors; }; } // namespace Batteries::VictronSmartShunt diff --git a/lib/VeDirectFrameHandler/VeDirectData.cpp b/lib/VeDirectFrameHandler/VeDirectData.cpp index dc4f9ce67..f1b1e2f3a 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.cpp +++ b/lib/VeDirectFrameHandler/VeDirectData.cpp @@ -360,3 +360,23 @@ frozen::string const& VeDirectHexData::getRegisterAsString() const return getAsString(values, addr); } + +/* + * This function returns the transmission error as readable text. + */ +frozen::string const& veStruct::getTransmissionErrorAsString(veStruct::Error error) const +{ + static constexpr frozen::map values = { + { Error::SUM, "Total Sum" }, + { Error::TIMEOUT, "Frame Timeout" }, + { Error::TEXT_CHECKSUM, "Text Checksum" }, + { Error::HEX_CHECKSUM, "Hex Checksum" }, + { Error::HEX_BUFFER, "Hex Buffer" }, + { Error::NESTED_HEX, "Nested Hex" }, + { Error::DEBUG_BUFFER, "Debug Buffer" }, + { Error::UNKNOWN_TEXT_DATA, "Unknown Data" }, + { Error::NON_VALID_CHAR, "Invalid Char" } + }; + + return getAsString(values, error); +} diff --git a/lib/VeDirectFrameHandler/VeDirectData.h b/lib/VeDirectFrameHandler/VeDirectData.h index 4cda91274..2c124ab48 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.h +++ b/lib/VeDirectFrameHandler/VeDirectData.h @@ -16,8 +16,13 @@ typedef struct { uint32_t batteryVoltage_V_mV = 0; // battery voltage in mV int32_t batteryCurrent_I_mA = 0; // battery current in mA (can be negative) float mpptEfficiency_Percent = 0; // efficiency in percent (calculated, moving average) + float transmissionErrors_Day = 0; // transmissions errors per day + + enum class Error { SUM, TIMEOUT, TEXT_CHECKSUM, HEX_CHECKSUM, HEX_BUFFER, NESTED_HEX, DEBUG_BUFFER, + UNKNOWN_TEXT_DATA, NON_VALID_CHAR, LAST }; frozen::string const& getPidAsString() const; // product ID as string + frozen::string const& getTransmissionErrorAsString(Error error) const; uint32_t getFwVersionAsInteger() const; String getFwVersionFormatted() const; } veStruct; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index bb24a2f18..ddf3f1ec0 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -31,6 +31,7 @@ * 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 + * 2025.03.29 - 0.5 - add of transmission error counters */ #include @@ -109,10 +110,23 @@ void VeDirectFrameHandler::loop() // if such a large gap is observed, reset the state machine so it tries // to decode a new frame / hex messages once more data arrives. if ((State::IDLE != _state) && ((millis() - _lastByteMillis) > 500)) { + setErrorCounter(veStruct::Error::TIMEOUT); DTU_LOGW("Resetting state machine (was %d) after timeout", static_cast(_state)); dumpDebugBuffer(); reset(); - } + } + + if ((millis() - _lastErrorPrint) > 60*1000) { + _lastErrorPrint = millis(); + + // calculate the average transmission errors per day + _tmpFrame.transmissionErrors_Day = _errorCounter.at(static_cast(veStruct::Error::SUM)); + float errorDays = esp_timer_get_time() / (24*60*60*1000*1000.0f); // 24h, use float to avoid int overflow + if (errorDays > 1.0f) { _tmpFrame.transmissionErrors_Day /= errorDays; } + + // no need to print the errors if we do not have any + if (_errorCounter.at(static_cast(veStruct::Error::SUM)) != 0) { printErrorCounter(); } + } } static bool isValidChar(uint8_t inbyte) @@ -144,17 +158,26 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) _debugBuffer[_debugIn] = inbyte; _debugIn = (_debugIn + 1) % _debugBuffer.size(); if (0 == _debugIn) { + setErrorCounter(veStruct::Error::DEBUG_BUFFER); DTU_LOGE("debug buffer overrun!"); } } if (_state != State::CHECKSUM && !isValidChar(inbyte)) { + setErrorCounter(veStruct::Error::NON_VALID_CHAR); DTU_LOGW("non-ASCII character 0x%02x, invalid frame", inbyte); reset(); return; } if ( (inbyte == ':') && (_state != State::CHECKSUM) ) { - _prevState = _state; //hex frame can interrupt TEXT + + if (_prevState == State::RECORD_HEX) { setErrorCounter(veStruct::Error::NESTED_HEX); } + + // Hex frame can interrupt text frame but hex frame + // never interrupt hex frame, in that case we had a transmission fault + // We just store the state if we come from a text frame state + if (_state != State::RECORD_HEX) { _prevState = _state; } + _state = State::RECORD_HEX; _hexSize = 0; } @@ -250,6 +273,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) frameValidEvent(); } else { + setErrorCounter(veStruct::Error::TEXT_CHECKSUM); DTU_LOGW("checksum 0x%02x != 0x00, invalid frame", _checksum); } reset(); @@ -304,6 +328,7 @@ void VeDirectFrameHandler::processTextData(std::string const& name, std::stri return; } + setErrorCounter(veStruct::Error::UNKNOWN_TEXT_DATA); DTU_LOGI("Unknown text data '%s' (value '%s')", name.c_str(), value.c_str()); } @@ -338,6 +363,7 @@ typename VeDirectFrameHandler::State VeDirectFrameHandler::hexRxEvent(uint _hexBuffer[_hexSize++]=inbyte; if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort + setErrorCounter(veStruct::Error::HEX_BUFFER); DTU_LOGE("hexRx buffer overflow - aborting read"); _hexSize=0; ret = State::IDLE; @@ -354,3 +380,44 @@ uint32_t VeDirectFrameHandler::getLastUpdate() const { return _lastUpdate; } + +/* + * Counts the transmission errors + */ +template +void VeDirectFrameHandler::setErrorCounter(veStruct::Error type) +{ + // Start-up can be in the middle of a VE.Direct transmission. + // That errors must be ignored. We wait until the startup condition is passed + if (_startUpPassed) { + + // increment the error counters but do not overflow + _errorCounter.at(static_cast(veStruct::Error::SUM))++; + _errorCounter.at(static_cast(type))++; + if (_errorCounter.at(static_cast(veStruct::Error::SUM)) > 50000) { _errorCounter.fill(0); } + } +} + +/* + * Prints the specific error counters every 60 seconds + */ +template +void VeDirectFrameHandler::printErrorCounter(void) +{ + DTU_LOGI("Average transmission errors per day: %0.1f 1/d", _tmpFrame.transmissionErrors_Day); + + auto constexpr maxPerLine = 3; // maximum number of errors per line + std::string sBuffer; + for(auto idx = 0; idx < _errorCounter.size(); ++idx) { + sBuffer.append(_tmpFrame.getTransmissionErrorAsString(static_cast(idx)).data()); + sBuffer.append(": "); + sBuffer.append(std::to_string(_errorCounter.at(static_cast(idx)))); + + if (((idx > 0) && (idx % maxPerLine) == 0) || (idx == _errorCounter.size() - 1)) { + DTU_LOGI("%s", sBuffer.c_str()); // print the buffer if it is full or if we are at the end + sBuffer.clear(); + } else { + sBuffer.append(", "); // add a comma to separate the errors + } + } +} diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 15ed05c4b..ddf3c67c6 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -89,6 +89,13 @@ class VeDirectFrameHandler { * queue, which is fine as we know the source frame was valid. */ std::deque> _textData; + + + void setErrorCounter(veStruct::Error type); + void printErrorCounter(void); + + std::array(veStruct::Error::LAST)> _errorCounter; + uint32_t _lastErrorPrint; // timestamp of the last logging print }; template class VeDirectFrameHandler; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp index 0768e7e4d..7274a0893 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp @@ -125,6 +125,8 @@ bool VeDirectFrameHandler::disassembleHexData(VeDirectHexData &data) { default: break; // something went wrong } + } else { + setErrorCounter(veStruct::Error::HEX_CHECKSUM); } if (!state) { diff --git a/src/battery/victronsmartshunt/Stats.cpp b/src/battery/victronsmartshunt/Stats.cpp index 4802937de..9ddfaa96d 100644 --- a/src/battery/victronsmartshunt/Stats.cpp +++ b/src/battery/victronsmartshunt/Stats.cpp @@ -29,6 +29,7 @@ void Stats::updateFrom(VeDirectShuntController::data_t const& shuntData) { _alarmLowSOC = shuntData.alarmReason_AR & 4; _alarmLowTemperature = shuntData.alarmReason_AR & 32; _alarmHighTemperature = shuntData.alarmReason_AR & 64; + _transmissionErrors = shuntData.transmissionErrors_Day; } void Stats::getLiveViewData(JsonVariant& root) const { @@ -43,6 +44,7 @@ void Stats::getLiveViewData(JsonVariant& root) const { addLiveViewValue(root, "midpointVoltage", _midpointVoltage, "V", 2); addLiveViewValue(root, "midpointDeviation", _midpointDeviation, "%", 1); addLiveViewValue(root, "lastFullCharge", _lastFullCharge, "min", 0); + addLiveViewValue(root, "transmitError", _transmissionErrors, "1/d", 1); if (_tempPresent) { addLiveViewValue(root, "temperature", _temperature, "°C", 0); } diff --git a/src/solarcharger/victron/Stats.cpp b/src/solarcharger/victron/Stats.cpp index 3ad95f431..3d64c8436 100644 --- a/src/solarcharger/victron/Stats.cpp +++ b/src/solarcharger/victron/Stats.cpp @@ -251,6 +251,9 @@ void Stats::populateJsonWithInstanceStats(const JsonObject &root, const VeDirect device["MpptTemperature"]["u"] = "°C"; device["MpptTemperature"]["d"] = "1"; } + device["MpptTransmitError"]["v"] = mpptData.transmissionErrors_Day; + device["MpptTransmitError"]["u"] = "1/d"; + device["MpptTransmitError"]["d"] = "1"; const JsonObject output = values["output"].to(); output["P"]["v"] = mpptData.batteryOutputPower_W; diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index b6680e011..29d8f72c8 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -200,7 +200,8 @@ "RELAY": "Status Fehlerrelais", "ERR": "Fehlerbeschreibung", "HSDS": "Anzahl der Tage (0..364)", - "MpptTemperature": "Ladereglertemperatur" + "MpptTemperature": "Ladereglertemperatur", + "MpptTransmitError": "Übertragungsfehler" }, "section_output": "Ausgang (Batterie)", "output": { @@ -1268,6 +1269,7 @@ "full-access": "@:batteryadmin.zendure.controlModes.Full", "write-once": "Einmalig Schreiben", "read-only": "@:batteryadmin.zendure.controlModes.ReadOnly", + "transmitError": "Übertragungsfehler", "zendure": { "chargeThroughState": "Status Vollladezyklus", "chargeThroughStates": { diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 44de8cc57..f3b8d3e76 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -200,7 +200,8 @@ "RELAY": "Error relay state", "ERR": "Error code", "HSDS": "Day sequence number (0..364)", - "MpptTemperature": "Charge controller temperature" + "MpptTemperature": "Charge controller temperature", + "MpptTransmitError": "Transmit errors" }, "section_output": "Output (Battery)", "output": { @@ -1270,6 +1271,7 @@ "full-access": "@:batteryadmin.zendure.controlModes.Full", "write-once": "Write Once", "read-only": "@:batteryadmin.zendure.controlModes.ReadOnly", + "transmitError": "Transmit errors", "zendure": { "chargeThroughState": "Charge through state", "chargeThroughStates": {