Skip to content

Commit

Permalink
VE.Direct: process more values and refactor variable names
Browse files Browse the repository at this point in the history
* process "IL", "AR" and "MON"
* discard "BMV" and (unsolicited) History Data
* simplify isDataValid()
* veMpptStruct, veStruct: new, verbose variable names, including units,
  and replace floats (save values with original integer precision)
* comment on rollover situation in isDataValid()
  • Loading branch information
SW-Niko authored and schlimmchen committed Apr 7, 2024
1 parent 3934906 commit b9ad1e3
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 115 deletions.
10 changes: 5 additions & 5 deletions lib/VeDirectFrameHandler/VeDirectData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ frozen::string const& veStruct::getPidAsString() const
{ 0xA3F0, "Smart BuckBoost 12V/12V-50A" },
};

return getAsString(values, PID);
return getAsString(values, productID_PID);
}

/*
Expand All @@ -154,7 +154,7 @@ frozen::string const& veMpptStruct::getCsAsString() const
{ 252, "External Control" }
};

return getAsString(values, CS);
return getAsString(values, currentState_CS);
}

/*
Expand All @@ -168,7 +168,7 @@ frozen::string const& veMpptStruct::getMpptAsString() const
{ 2, "MPP Tracker active" }
};

return getAsString(values, MPPT);
return getAsString(values, stateOfTracker_MPPT);
}

/*
Expand Down Expand Up @@ -199,7 +199,7 @@ frozen::string const& veMpptStruct::getErrAsString() const
{ 118, "User settings invalid" }
};

return getAsString(values, ERR);
return getAsString(values, errorCode_ERR);
}

/*
Expand All @@ -220,7 +220,7 @@ frozen::string const& veMpptStruct::getOrAsString() const
{ 0x00000100, "Analysing input voltage" }
};

return getAsString(values, OR);
return getAsString(values, offReason_OR);
}

frozen::string const& VeDirectHexData::getResponseAsString() const
Expand Down
50 changes: 27 additions & 23 deletions lib/VeDirectFrameHandler/VeDirectData.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,33 @@
#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
float V = 0; // battery voltage in V
float I = 0; // battery current in A
float E = 0; // efficiency in percent (calculated, moving average)
uint16_t productID_PID = 0; // product id
char serialNr_SER[VE_MAX_VALUE_LEN]; // serial number
char firmwareNr_FW[VE_MAX_VALUE_LEN]; // firmware release number
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)

frozen::string const& getPidAsString() const; // product ID as string
} veStruct;

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)
float VPV; // panel voltage in V
float 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)
uint8_t CS; // current state of operation e.g. OFF or Bulk
uint8_t ERR; // error code
uint32_t OR; // off reason
uint32_t HSDS; // day sequence number 1...365
float H19; // yield total kWh
float H20; // yield today kWh
int32_t H21; // maximum power today W
float H22; // yield yesterday kWh
int32_t H23; // maximum power yesterday W
uint8_t stateOfTracker_MPPT; // state of MPP tracker
uint16_t panelPower_PPV_W; // panel power in W
uint32_t panelVoltage_VPV_mV; // panel voltage in mV
uint32_t panelCurrent_mA; // panel current in mA (calculated)
int16_t batteryOutputPower_W; // battery output power in W (calculated, can be negative if load output is used)
uint32_t loadCurrent_IL_mA; // Load current in mA (Available only for models with a load output)
bool loadOutputState_LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
uint8_t currentState_CS; // current state of operation e.g. OFF or Bulk
uint8_t errorCode_ERR; // error code
uint32_t offReason_OR; // off reason
uint16_t daySequenceNr_HSDS; // day sequence number 1...365
uint32_t yieldTotal_H19_Wh; // yield total resetable Wh
uint32_t yieldToday_H20_Wh; // yield today Wh
uint16_t maxPowerToday_H21_W; // maximum power today W
uint32_t yieldYesterday_H22_Wh; // yield yesterday Wh
uint16_t maxPowerYesterday_H23_W; // maximum power yesterday W

// these are values communicated through the HEX protocol. the pair's first
// value is the timestamp the respective info was last received. if it is
Expand All @@ -59,7 +60,7 @@ struct veShuntStruct : veStruct {
int32_t SOC; // State-of-charge
uint32_t TTG; // Time-to-go
bool ALARM; // Alarm condition active
uint32_t AR; // Alarm Reason
uint16_t alarmReason_AR; // Alarm Reason
int32_t H1; // Depth of the deepest discharge
int32_t H2; // Depth of the last discharge
int32_t H3; // Depth of the average discharge
Expand All @@ -78,6 +79,7 @@ struct veShuntStruct : veStruct {
int32_t H16; // Maximum auxiliary (battery) voltage
int32_t H17; // Amount of discharged energy
int32_t H18; // Amount of charged energy
int8_t dcMonitorMode_MON; // DC monitor mode
};

enum class VeDirectHexCommand : uint8_t {
Expand Down Expand Up @@ -120,7 +122,9 @@ enum class VeDirectHexRegister : uint16_t {
SmartBatterySenseTemperature = 0xEDEC,
NetworkInfo = 0x200D,
NetworkMode = 0x200E,
NetworkStatus = 0x200F
NetworkStatus = 0x200F,
HistoryTotal = 0x104F,
HistoryMPPTD30 = 0x10BE
};

struct VeDirectHexData {
Expand Down
21 changes: 12 additions & 9 deletions lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ void VeDirectFrameHandler<T>::loop()
_lastByteMillis = millis();
}

// there will never be a large gap between two bytes of the same frame.
// there will never be a large gap between two bytes.
// if such a large gap is observed, reset the state machine so it tries
// to decode a new frame once more data arrives.
if (State::IDLE != _state && (millis() - _lastByteMillis) > 500) {
// to decode a new frame / hex messages once more data arrives.
if ((State::IDLE != _state) && ((millis() - _lastByteMillis) > 500)) {
_msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n",
_logId, static_cast<unsigned>(_state));
if (_verboseLogging) { dumpDebugBuffer(); }
Expand Down Expand Up @@ -236,27 +236,27 @@ void VeDirectFrameHandler<T>::processTextData(std::string const& name, std::stri
if (processTextDataDerived(name, value)) { return; }

if (name == "PID") {
_tmpFrame.PID = strtol(value.c_str(), nullptr, 0);
_tmpFrame.productID_PID = strtol(value.c_str(), nullptr, 0);
return;
}

if (name == "SER") {
strcpy(_tmpFrame.SER, value.c_str());
strcpy(_tmpFrame.serialNr_SER, value.c_str());
return;
}

if (name == "FW") {
strcpy(_tmpFrame.FW, value.c_str());
strcpy(_tmpFrame.firmwareNr_FW, value.c_str());
return;
}

if (name == "V") {
_tmpFrame.V = round(atof(value.c_str()) / 10.0) / 100.0;
_tmpFrame.batteryVoltage_V_mV = atol(value.c_str());
return;
}

if (name == "I") {
_tmpFrame.I = round(atof(value.c_str()) / 10.0) / 100.0;
_tmpFrame.batteryCurrent_I_mA = atol(value.c_str());
return;
}

Expand Down Expand Up @@ -307,7 +307,10 @@ typename VeDirectFrameHandler<T>::State VeDirectFrameHandler<T>::hexRxEvent(uint
template<typename T>
bool VeDirectFrameHandler<T>::isDataValid() const
{
return strlen(_tmpFrame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000);
// VE.Direct text frame data is valid if we receive a device serialnumber and
// the data is not older as 10 seconds
// we accept a glitch where the data is valid for ten seconds when serialNr_SER != "" and (millis() - _lastUpdate) overflows
return strlen(_tmpFrame.serialNr_SER) > 0 && (millis() - _lastUpdate) < (10 * 1000);
}

template<typename T>
Expand Down
2 changes: 1 addition & 1 deletion lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class VeDirectFrameHandler {
char _value[VE_MAX_VALUE_LEN]; // buffer for the field value
std::array<uint8_t, 512> _debugBuffer;
unsigned _debugIn;
uint32_t _lastByteMillis;
uint32_t _lastByteMillis; // time of last parsed byte

/**
* not every frame contains every value the device is communicating, i.e.,
Expand Down
6 changes: 6 additions & 0 deletions lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
case Response::ASYNC:
data.addr = static_cast<VeDirectHexRegister>(AsciiHexLE2Int(buffer+2, 4));

// future option: Up to now we do not use historical data
if ((data.addr >= VeDirectHexRegister::HistoryTotal) && (data.addr <= VeDirectHexRegister::HistoryMPPTD30)) {
state = true;
break;
}

// future option: to analyse the flags here?
data.flags = AsciiHexLE2Int(buffer+6, 2);

Expand Down
44 changes: 24 additions & 20 deletions lib/VeDirectFrameHandler/VeDirectMpptController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,60 @@ void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verb

bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value)
{
if (name == "IL") {
_tmpFrame.loadCurrent_IL_mA = atol(value.c_str());
return true;
}
if (name == "LOAD") {
_tmpFrame.LOAD = (value == "ON");
_tmpFrame.loadOutputState_LOAD = (value == "ON");
return true;
}
if (name == "CS") {
_tmpFrame.CS = atoi(value.c_str());
_tmpFrame.currentState_CS = atoi(value.c_str());
return true;
}
if (name == "ERR") {
_tmpFrame.ERR = atoi(value.c_str());
_tmpFrame.errorCode_ERR = atoi(value.c_str());
return true;
}
if (name == "OR") {
_tmpFrame.OR = strtol(value.c_str(), nullptr, 0);
_tmpFrame.offReason_OR = strtol(value.c_str(), nullptr, 0);
return true;
}
if (name == "MPPT") {
_tmpFrame.MPPT = atoi(value.c_str());
_tmpFrame.stateOfTracker_MPPT = atoi(value.c_str());
return true;
}
if (name == "HSDS") {
_tmpFrame.HSDS = atoi(value.c_str());
_tmpFrame.daySequenceNr_HSDS = atoi(value.c_str());
return true;
}
if (name == "VPV") {
_tmpFrame.VPV = round(atof(value.c_str()) / 10.0) / 100.0;
_tmpFrame.panelVoltage_VPV_mV = atol(value.c_str());
return true;
}
if (name == "PPV") {
_tmpFrame.PPV = atoi(value.c_str());
_tmpFrame.panelPower_PPV_W = atoi(value.c_str());
return true;
}
if (name == "H19") {
_tmpFrame.H19 = atof(value.c_str()) / 100.0;
_tmpFrame.yieldTotal_H19_Wh = atol(value.c_str()) * 10;
return true;
}
if (name == "H20") {
_tmpFrame.H20 = atof(value.c_str()) / 100.0;
_tmpFrame.yieldToday_H20_Wh = atol(value.c_str()) * 10;
return true;
}
if (name == "H21") {
_tmpFrame.H21 = atoi(value.c_str());
_tmpFrame.maxPowerToday_H21_W = atoi(value.c_str());
return true;
}
if (name == "H22") {
_tmpFrame.H22 = atof(value.c_str()) / 100.0;
_tmpFrame.yieldYesterday_H22_Wh = atol(value.c_str()) * 10;
return true;
}
if (name == "H23") {
_tmpFrame.H23 = atoi(value.c_str());
_tmpFrame.maxPowerYesterday_H23_W = atoi(value.c_str());
return true;
}

Expand All @@ -80,15 +84,15 @@ bool VeDirectMpptController::processTextDataDerived(std::string const& name, std
* This function is called at the end of the received frame.
*/
void VeDirectMpptController::frameValidEvent() {
_tmpFrame.P = _tmpFrame.V * _tmpFrame.I;
_tmpFrame.batteryOutputPower_W = static_cast<int16_t>(_tmpFrame.batteryVoltage_V_mV * _tmpFrame.batteryCurrent_I_mA / 1000000);

if (_tmpFrame.VPV > 0) {
_tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV;
if ((_tmpFrame.panelVoltage_VPV_mV > 0) && (_tmpFrame.panelPower_PPV_W >= 1)) {
_tmpFrame.panelCurrent_mA = static_cast<uint32_t>(_tmpFrame.panelPower_PPV_W * 1000000) / _tmpFrame.panelVoltage_VPV_mV;
}

if (_tmpFrame.PPV > 0) {
_efficiency.addNumber(static_cast<float>(_tmpFrame.P * 100) / _tmpFrame.PPV);
_tmpFrame.E = _efficiency.getAverage();
if (_tmpFrame.panelPower_PPV_W > 0) {
_efficiency.addNumber(static_cast<float>(_tmpFrame.batteryOutputPower_W * 100) / _tmpFrame.panelPower_PPV_W);
_tmpFrame.mpptEfficiency_Percent = _efficiency.getAverage();
}

if (!_canSend) { return; }
Expand All @@ -98,7 +102,7 @@ void VeDirectMpptController::frameValidEvent() {
// 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) { return; }
if (atoi(_tmpFrame.firmwareNr_FW) < 153) { return; }

using Command = VeDirectHexCommand;
using Register = VeDirectHexRegister;
Expand Down
14 changes: 13 additions & 1 deletion lib/VeDirectFrameHandler/VeDirectShuntController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ bool VeDirectShuntController::processTextDataDerived(std::string const& name, st
_tmpFrame.ALARM = (value == "ON");
return true;
}
if (name == "AR") {
_tmpFrame.alarmReason_AR = atoi(value.c_str());
return true;
}
if (name == "H1") {
_tmpFrame.H1 = atoi(value.c_str());
return true;
Expand Down Expand Up @@ -107,6 +111,14 @@ bool VeDirectShuntController::processTextDataDerived(std::string const& name, st
_tmpFrame.H18 = atoi(value.c_str());
return true;
}

if (name == "BMV") {
// This field contains a textual description of the BMV model,
// for example 602S or 702. It is deprecated, refer to the field PID instead.
return true;
}
if (name == "MON") {
_tmpFrame.dcMonitorMode_MON = static_cast<int8_t>(atoi(value.c_str()));
return true;
}
return false;
}
14 changes: 7 additions & 7 deletions src/BatteryStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -374,10 +374,10 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
}

void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) {
BatteryStats::setVoltage(shuntData.V, millis());
BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis());
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());

_current = shuntData.I;
_current = static_cast<float>(shuntData.batteryCurrent_I_mA) / 1000;
_modelName = shuntData.getPidAsString().data();
_chargeCycles = shuntData.H4;
_timeToGo = shuntData.TTG / 60;
Expand All @@ -390,11 +390,11 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& s
_consumedAmpHours = static_cast<float>(shuntData.CE) / 1000;
_lastFullCharge = shuntData.H9 / 60;
// shuntData.AR is a bitfield, so we need to check each bit individually
_alarmLowVoltage = shuntData.AR & 1;
_alarmHighVoltage = shuntData.AR & 2;
_alarmLowSOC = shuntData.AR & 4;
_alarmLowTemperature = shuntData.AR & 32;
_alarmHighTemperature = shuntData.AR & 64;
_alarmLowVoltage = shuntData.alarmReason_AR & 1;
_alarmHighVoltage = shuntData.alarmReason_AR & 2;
_alarmLowSOC = shuntData.alarmReason_AR & 4;
_alarmLowTemperature = shuntData.alarmReason_AR & 32;
_alarmHighTemperature = shuntData.alarmReason_AR & 64;

_lastUpdate = VeDirectShunt.getLastUpdate();
}
Expand Down
6 changes: 3 additions & 3 deletions src/MqttHandlVedirectHass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char
const char *unitOfMeasurement,
const VeDirectMpptController::data_t &mpptData)
{
String serial = mpptData.SER;
String serial = mpptData.serialNr_SER;

String sensorId = caption;
sensorId.replace(" ", "_");
Expand Down Expand Up @@ -153,7 +153,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const
const char *payload_on, const char *payload_off,
const VeDirectMpptController::data_t &mpptData)
{
String serial = mpptData.SER;
String serial = mpptData.serialNr_SER;

String sensorId = caption;
sensorId.replace(" ", "_");
Expand Down Expand Up @@ -198,7 +198,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object,
const VeDirectMpptController::data_t &mpptData)
{
String serial = mpptData.SER;
String serial = mpptData.serialNr_SER;
object["name"] = "Victron(" + serial + ")";
object["ids"] = serial;
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
Expand Down
Loading

0 comments on commit b9ad1e3

Please sign in to comment.