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 @@ -31,6 +31,7 @@ class Stats : public ::Batteries::Stats {
bool _alarmLowSOC;
bool _alarmLowTemperature;
bool _alarmHighTemperature;
float _transmissionErrors;
};

} // namespace Batteries::VictronSmartShunt
20 changes: 20 additions & 0 deletions lib/VeDirectFrameHandler/VeDirectData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<veStruct::Error, frozen::string, 9> 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);
}
5 changes: 5 additions & 0 deletions lib/VeDirectFrameHandler/VeDirectData.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
71 changes: 69 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 transmission error counters
*/

#include <Arduino.h>
Expand Down Expand Up @@ -109,10 +110,23 @@ 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() - _lastErrorPrint) > 60*1000) {
_lastErrorPrint = millis();

// calculate the average transmission errors per day
_tmpFrame.transmissionErrors_Day = _errorCounter.at(static_cast<size_t>(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<size_t>(veStruct::Error::SUM)) != 0) { printErrorCounter(); }
}
}

static bool isValidChar(uint8_t inbyte)
Expand Down Expand Up @@ -144,17 +158,26 @@ 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

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;
}
Expand Down Expand Up @@ -250,6 +273,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 @@ -304,6 +328,7 @@ void VeDirectFrameHandler<T>::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());
}

Expand Down Expand Up @@ -338,6 +363,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 +380,44 @@ uint32_t VeDirectFrameHandler<T>::getLastUpdate() const
{
return _lastUpdate;
}

/*
* Counts the transmission errors
*/
template<typename T>
void VeDirectFrameHandler<T>::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<size_t>(veStruct::Error::SUM))++;
_errorCounter.at(static_cast<size_t>(type))++;
if (_errorCounter.at(static_cast<size_t>(veStruct::Error::SUM)) > 50000) { _errorCounter.fill(0); }
}
}

/*
* Prints the specific error counters every 60 seconds
*/
template<typename T>
void VeDirectFrameHandler<T>::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<veStruct::Error>(idx)).data());
sBuffer.append(": ");
sBuffer.append(std::to_string(_errorCounter.at(static_cast<size_t>(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
}
}
}
7 changes: 7 additions & 0 deletions lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ 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;
uint32_t _lastErrorPrint; // timestamp of the last logging print
};

template class VeDirectFrameHandler<veMpptStruct>;
Expand Down
2 changes: 2 additions & 0 deletions lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
default:
break; // something went wrong
}
} else {
setErrorCounter(veStruct::Error::HEX_CHECKSUM);
}

if (!state) {
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 @@ -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 {
Expand All @@ -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);
}
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.transmissionErrors_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 @@ -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": {
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 @@ -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": {
Expand Down