Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/battery/victronsmartshunt/Stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Stats : public ::Batteries::Stats {
bool _alarmLowSOC;
bool _alarmLowTemperature;
bool _alarmHighTemperature;
float _transmitErrors;
};

} // namespace Batteries::VictronSmartShunt
18 changes: 18 additions & 0 deletions lib/VeDirectFrameHandler/VeDirectData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,21 @@ frozen::string const& VeDirectHexData::getRegisterAsString() const

return getAsString(values, addr);
}

/*
* returns the transmission error as readable text.
*/
frozen::string const& veStruct::getTransmitErrorAsString(veStruct::Error error) const
{
static constexpr frozen::map<veStruct::Error, frozen::string, 7> values = {
{ 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::NON_VALID_CHAR, "Invalid Char" }
};

return getAsString(values, error);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
4 changes: 4 additions & 0 deletions lib/VeDirectFrameHandler/VeDirectData.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ 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 transmitErrors_Day = 0.0f; // transmit errors per day

enum class Error { TIMEOUT, TEXT_CHECKSUM, HEX_CHECKSUM, HEX_BUFFER, NESTED_HEX, DEBUG_BUFFER, NON_VALID_CHAR, LAST };

frozen::string const& getPidAsString() const; // product ID as string
frozen::string const& getTransmitErrorAsString(Error error) const;
uint32_t getFwVersionAsInteger() const;
String getFwVersionFormatted() const;
} veStruct;
Expand Down
84 changes: 82 additions & 2 deletions lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 transmit error counters
*/

#include <Arduino.h>
Expand All @@ -48,6 +49,7 @@ template<typename T>
VeDirectFrameHandler<T>::VeDirectFrameHandler() :
_lastUpdate(0),
_state(State::IDLE),
_prevState(State::IDLE),
_checksum(0),
_textPointer(0),
_hexSize(0),
Expand All @@ -73,6 +75,7 @@ void VeDirectFrameHandler<T>::init(char const* who, gpio_num_t rx, gpio_num_t tx
_startUpPassed = false; // to obtain a complete dataset after a new start or restart
_dataValid = false; // data is not valid on start or restart
snprintf(_logId, sizeof(_logId), "%s %d/%d", who, rx, tx);

DTU_LOGI("init complete");
}

Expand Down Expand Up @@ -109,10 +112,25 @@ void VeDirectFrameHandler<T>::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<unsigned>(_state));
dumpDebugBuffer();
reset();
}
}

if ((millis() - _lastErrorCalcAndPrint) > 60*1000) {
_lastErrorCalcAndPrint = millis();

// calculate the average transmit errors per day
_tmpFrame.transmitErrors_Day = static_cast<float>(_errorSumSinceStartup);
float errorDays = esp_timer_get_time() / (24.0f*60*60*1000*1000); // 24h, use float to avoid int overflow
if (errorDays > 1.0f) { _tmpFrame.transmitErrors_Day /= errorDays; }

// no need to print the errors if log level is not at least INFO or if we do not have any
if (DTU_LOG_IS_INFO && (_errorSumSinceStartup != 0)) {
printErrorCounter();
}
}
}

static bool isValidChar(uint8_t inbyte)
Expand Down Expand Up @@ -144,17 +162,29 @@ void VeDirectFrameHandler<T>::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

// hex frame can interrupt text frame, but hex frame
// never interrupt hex frame, if we reach this line with
// _state == State::RECORD_HEX we had a transmit fault
if (_state == State::RECORD_HEX) {
setErrorCounter(veStruct::Error::NESTED_HEX);
} else {
// We just store the current state if we come from a text frame state
_prevState = _state;
}

_state = State::RECORD_HEX;
_hexSize = 0;
}
Expand Down Expand Up @@ -250,6 +280,7 @@ void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
frameValidEvent();
}
else {
setErrorCounter(veStruct::Error::TEXT_CHECKSUM);
DTU_LOGW("checksum 0x%02x != 0x00, invalid frame", _checksum);
}
reset();
Expand Down Expand Up @@ -338,6 +369,7 @@ typename VeDirectFrameHandler<T>::State VeDirectFrameHandler<T>::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;
Expand All @@ -354,3 +386,51 @@ uint32_t VeDirectFrameHandler<T>::getLastUpdate() const
{
return _lastUpdate;
}

/*
* Counts the transmit errors
*/
template<typename T>
void VeDirectFrameHandler<T>::setErrorCounter(veStruct::Error type)
{
// Start-up can be in the middle of a VE.Direct transmit.
// That errors must be ignored. We wait until the startup condition is passed
if (_startUpPassed) {

// increment the error counters
_errorCounter.at(static_cast<size_t>(type))++;
_errorSumSinceStartup++; // with 1 error per second we would overflow after 136 years
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// to avoid overflow, we divide all counters by 2 if one of them exceeds 50k
for (size_t idx = 0; idx < _errorCounter.size(); ++idx) {
if (_errorCounter.at(idx) > 50000) {
for (auto& counter : _errorCounter) { counter /= 2; }
break;
}
}
}
}
Comment thread
SW-Niko marked this conversation as resolved.

/*
* Prints the specific error counters every 60 seconds
*/
template<typename T>
void VeDirectFrameHandler<T>::printErrorCounter(void)
{
DTU_LOGI("Average transmit errors per day: %0.1f 1/d", _tmpFrame.transmitErrors_Day);

auto constexpr maxPerLine = 3; // maximum number of errors per line
std::string sBuffer;
for(size_t idx = 0; idx < _errorCounter.size(); ++idx) {
sBuffer.append(_tmpFrame.getTransmitErrorAsString(static_cast<veStruct::Error>(idx)).data());
sBuffer.append(": ");
sBuffer.append(std::to_string(_errorCounter.at(static_cast<size_t>(idx))));

if (((idx + 1) % maxPerLine == 0) || (idx == _errorCounter.size() - 1)) {
DTU_LOGI("%s", sBuffer.c_str()); // print the buffer
sBuffer.clear();
} else {
sBuffer.append(", "); // add a comma to separate the errors
}
}
}
8 changes: 8 additions & 0 deletions lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ class VeDirectFrameHandler {
* queue, which is fine as we know the source frame was valid.
*/
std::deque<std::pair<std::string, std::string>> _textData;


void setErrorCounter(veStruct::Error type);
void printErrorCounter(void);

std::array<uint16_t, static_cast<size_t>(veStruct::Error::LAST)> _errorCounter = {0}; // error counters for each error type
uint32_t _errorSumSinceStartup = 0; // sum of all errors since startup
uint32_t _lastErrorCalcAndPrint = 0; // timestamp of the last logging print
};

template class VeDirectFrameHandler<veMpptStruct>;
Expand Down
6 changes: 5 additions & 1 deletion lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
// reset hex data first
data = {};

if ((len > 3) && (calcHexFrameCheckSum(buffer, len) == 0x00)) {
if (len <= 3) {
setErrorCounter(veStruct::Error::HEX_BUFFER);
} else if (calcHexFrameCheckSum(buffer, len) != 0x00) {
setErrorCounter(veStruct::Error::HEX_CHECKSUM);
} else {
data.rsp = static_cast<VeDirectHexResponse>(AsciiHexLE2Int(buffer+1, 1));

using Response = VeDirectHexResponse;
Expand Down
2 changes: 2 additions & 0 deletions src/battery/victronsmartshunt/Stats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ void Stats::updateFrom(VeDirectShuntController::data_t const& shuntData) {
_alarmLowSOC = shuntData.alarmReason_AR & 4;
_alarmLowTemperature = shuntData.alarmReason_AR & 32;
_alarmHighTemperature = shuntData.alarmReason_AR & 64;
_transmitErrors = shuntData.transmitErrors_Day;
}

void Stats::getLiveViewData(JsonVariant& root) const {
Expand All @@ -46,6 +47,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", _transmitErrors, "1/d", 1);

auto oTemperature = getTemperature();
if (oTemperature) {
Expand Down
3 changes: 3 additions & 0 deletions src/solarcharger/victron/Stats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ void Stats::populateJsonWithInstanceStats(const JsonObject &root, const VeDirect
device["MpptTemperature"]["u"] = "°C";
device["MpptTemperature"]["d"] = "1";
}
device["MpptTransmitError"]["v"] = mpptData.transmitErrors_Day;
device["MpptTransmitError"]["u"] = "1/d";
device["MpptTransmitError"]["d"] = "1";

const JsonObject output = values["output"].to<JsonObject>();
output["P"]["v"] = mpptData.batteryOutputPower_W;
Expand Down
4 changes: 3 additions & 1 deletion webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -1296,6 +1297,7 @@
"full-access": "@:batteryadmin.zendure.controlModes.Full",
"write-once": "Einmalig Schreiben",
"read-only": "@:batteryadmin.zendure.controlModes.ReadOnly",
"transmitError": "Übertragungsfehler",
"zendure": {
"chargeThroughState": "Status Vollladezyklus",
"chargeThroughStates": {
Expand Down
4 changes: 3 additions & 1 deletion webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -1300,6 +1301,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": {
Expand Down