Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Surplus-Power-Mode #1167

Open
wants to merge 11 commits into
base: development
Choose a base branch
from
1 change: 1 addition & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ struct CONFIG_T {
uint32_t FullSolarPassThroughSoc;
float FullSolarPassThroughStartVoltage;
float FullSolarPassThroughStopVoltage;
bool SurplusPowerEnabled;
} PowerLimiter;

struct {
Expand Down
46 changes: 46 additions & 0 deletions include/Statistic.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once


template <typename T>
class WeightedAVG {
public:
WeightedAVG(int16_t factor)
: _countMax(factor)
, _count(0), _countNum(0), _avgV(0), _minV(0), _maxV(0), _lastV(0) {}

void addNumber(const T& num) {
if (_count == 0){
_count++;
_avgV = num;
_minV = num;
_maxV = num;
_countNum = 1;
} else {
if (_count < _countMax)
_count++;
_avgV = (_avgV * (_count - 1) + num) / _count;
if (num < _minV) _minV = num;
if (num > _maxV) _maxV = num;
_countNum++;
}
_lastV = num;
}

void reset(void) { _count = 0; _avgV = 0; _minV = 0; _maxV = 0; _lastV = 0; _countNum = 0; }
void reset(const T& num) { _count = 0; addNumber(num); }
T getAverage() const { return _avgV; }
T getMin() const { return _minV; }
T getMax() const { return _maxV; }
T getLast() const { return _lastV; }
int32_t getCounts() const { return _countNum; }

private:
int16_t _countMax; // weighting factor (10 => 1/10 => 10%)
int16_t _count; // counter (0 - _countMax)
int32_t _countNum; // counts the amount of added numbers
T _avgV; // average value
T _minV; // minimum value
T _maxV; // maximum value
T _lastV; // last value
};

52 changes: 52 additions & 0 deletions include/SurplusPower.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#pragma once

#include <Arduino.h>
#include <frozen/string.h>
#include "Statistic.h"


class SurplusPowerClass {
public:
SurplusPowerClass() = default;
~SurplusPowerClass() = default;

bool useSurplusPower(void) const;
int32_t calcSurplusPower(int32_t const requestedPower);

private:
enum class SurplusState : uint8_t {
IDLE = 0,
TRY_MORE = 1,
REDUCE_POWER = 2,
IN_TARGET = 3,
MAXIMUM_POWER = 4,
};

enum class Text : uint8_t {
Q_NODATA = 0,
Q_EXCELLENT = 1,
Q_GOOD = 2,
Q_BAD = 3,
T_HEAD = 4
};

frozen::string const& getStatusText(SurplusPowerClass::SurplusState state);
frozen::string const& getText(SurplusPowerClass::Text tNr);
void handleQualityCounter(void);

// to handle regulation
SurplusState _surplusState = SurplusState::IDLE; // actual regulation state
float _absorptionVoltage = -1.0f; // from MPPT
float _floatVoltage = -1.0f; // from MPPT
int32_t _powerStep = 50; // power step size in W (default)
int32_t _surplusPower = 0; // actual surplus power
int32_t _inTargetTime = 0; // records the time we hit the target power
WeightedAVG<float> _avgMPPTVoltage {3}; // the average helps to smooth the regulation

// to handle the quality counter
int8_t _qualityCounter = 0; // quality counter
WeightedAVG<float> _qualityAVG {20}; // quality counter average
int32_t _lastAddPower = 0; // last power step
};

extern SurplusPowerClass SurplusPower;
11 changes: 11 additions & 0 deletions include/VictronMppt.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ class VictronMpptClass {
// minimum of all MPPT charge controllers' output voltages in V
float getOutputVoltage() const;

// returns the state of operation from the first available controller
int16_t getStateOfOperation() const;

// the configured value from the first available controller in V
enum class MPPTVoltage : uint8_t {
ABSORPTION = 0,
FLOAT = 1,
BATTERY = 2
};
float getVoltage(MPPTVoltage kindOf) const;

private:
void loop();
VictronMpptClass(VictronMpptClass const& other) = delete;
Expand Down
15 changes: 12 additions & 3 deletions lib/VeDirectFrameHandler/VeDirectData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ frozen::string const& veMpptStruct::getCsAsString() const
{ 0, "OFF" },
{ 2, "Fault" },
{ 3, "Bulk" },
{ 4, "Absorbtion" },
{ 4, "Absorption" },
{ 5, "Float" },
{ 7, "Equalize (manual)" },
{ 245, "Starting-up" },
Expand Down Expand Up @@ -287,18 +287,27 @@ frozen::string const& VeDirectHexData::getResponseAsString() const
frozen::string const& VeDirectHexData::getRegisterAsString() const
{
using Register = VeDirectHexRegister;
static constexpr frozen::map<Register, frozen::string, 11> values = {
static constexpr frozen::map<Register, frozen::string, 20> values = {
{ Register::DeviceMode, "Device Mode" },
{ Register::DeviceState, "Device State" },
{ Register::RemoteControlUsed, "Remote Control Used" },
{ Register::PanelVoltage, "Panel Voltage" },
{ Register::PanelPower, "Panel Power" },
{ Register::ChargerVoltage, "Charger Voltage" },
{ Register::ChargerCurrent, "Charger Current" },
{ Register::NetworkTotalDcInputPower, "Network Total DC Input Power" },
{ Register::ChargeControllerTemperature, "Charger Controller Temperature" },
{ Register::SmartBatterySenseTemperature, "Smart Battery Sense Temperature" },
{ Register::NetworkInfo, "Network Info" },
{ Register::NetworkMode, "Network Mode" },
{ Register::NetworkStatus, "Network Status" }
{ Register::NetworkStatus, "Network Status" },
{ Register::BatteryAbsorptionVoltage, "Battery Absorption Voltage" },
{ Register::BatteryFloatVoltage, "Battery Float Voltage" },
{ Register::TotalChargeCurrent, "Total Charge Current" },
{ Register::ChargeStateElapsedTime, "Charge State Elapsed Time" },
{ Register::BatteryVoltageSense, "Battery Voltage Sense" },
{ Register::LoadCurrent, "Load current" },
{ Register::LoadOutputVoltage, "Load Output Voltage" }
};

return getAsString(values, addr);
Expand Down
13 changes: 12 additions & 1 deletion lib/VeDirectFrameHandler/VeDirectData.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ struct veMpptStruct : veStruct {
std::pair<uint32_t, int32_t> MpptTemperatureMilliCelsius;
std::pair<uint32_t, int32_t> SmartBatterySenseTemperatureMilliCelsius;
std::pair<uint32_t, uint32_t> NetworkTotalDcInputPowerMilliWatts;
std::pair<uint32_t, uint32_t> BatteryAbsorptionMilliVolt;
std::pair<uint32_t, uint32_t> BatteryFloatMilliVolt;
std::pair<uint32_t, uint8_t> NetworkInfo;
std::pair<uint32_t, uint8_t> NetworkMode;
std::pair<uint32_t, uint8_t> NetworkStatus;
Expand Down Expand Up @@ -121,15 +123,24 @@ enum class VeDirectHexRegister : uint16_t {
DeviceState = 0x0201,
RemoteControlUsed = 0x0202,
PanelVoltage = 0xEDBB,
PanelPower = 0xEDBC,
ChargerVoltage = 0xEDD5,
ChargerCurrent = 0xEDD7,
NetworkTotalDcInputPower = 0x2027,
ChargeControllerTemperature = 0xEDDB,
SmartBatterySenseTemperature = 0xEDEC,
NetworkInfo = 0x200D,
NetworkMode = 0x200E,
NetworkStatus = 0x200F,
HistoryTotal = 0x104F,
HistoryMPPTD30 = 0x10BE
HistoryMPPTD30 = 0x10BE,
BatteryAbsorptionVoltage = 0xEDF7,
BatteryFloatVoltage = 0xEDF6,
TotalChargeCurrent = 0x2013,
ChargeStateElapsedTime= 0x2007,
BatteryVoltageSense = 0x2002,
LoadCurrent = 0xEDAD,
LoadOutputVoltage = 0xEDA9
};

struct VeDirectHexData {
Expand Down
3 changes: 2 additions & 1 deletion lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ class VeDirectFrameHandler {
bool isDataValid() const; // return true if data valid and not outdated
T const& getData() const { return _tmpFrame; }
bool sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value = 0, uint8_t valsize = 0);
bool isStateIdle() const { return (_state == State::IDLE); }

protected:
VeDirectFrameHandler();
void init(char const* who, int8_t rx, int8_t tx, Print* msgOut,
bool verboseLogging, uint8_t hwSerialPort);
virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembeled hex response
virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembled hex response

bool _verboseLogging;
Print* _msgOut;
Expand Down
12 changes: 6 additions & 6 deletions lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ HexHandler.cpp
* 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()
* to handle the received hex messages. All hex messages will be forwarded 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
Expand Down Expand Up @@ -63,9 +63,9 @@ static uint32_t AsciiHexLE2Int(const char *ascii, const uint8_t anz) {
* disassembleHexData()
* analysis the hex message and extract: response, address, flags 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
* data: disassembled message
* return: true = successful disassembled, false = hex sum fault or message
* do not align with VE.Direct syntax
*/
template<typename T>
bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
Expand Down Expand Up @@ -164,14 +164,14 @@ static String Int2HexLEString(uint32_t value, uint8_t anz) {
* addr: register address, default 0
* value: value to write into a register, default 0
* valsize: size of the value, 8, 16 or 32 bit, default 0
* return: true = message assembeld and send, false = it was not possible to put the message together
* return: true = message assembled 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"
* On MPPT for example 0xEDE0 - 0xEDFF. Check the Victron doc "BlueSolar-HEX-protocol.pdf"
*/
template<typename T>
bool VeDirectFrameHandler<T>::sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value, uint8_t valsize) {
Expand Down
87 changes: 66 additions & 21 deletions lib/VeDirectFrameHandler/VeDirectMpptController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,35 +104,51 @@ void VeDirectMpptController::frameValidEvent() {
} else {
_tmpFrame.mpptEfficiency_Percent = 0.0f;
}
}

if (!_canSend) { return; }

void VeDirectMpptController::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 (_tmpFrame.getFwVersionAsInteger() < 153) { return; }

using Command = VeDirectHexCommand;
using Register = VeDirectHexRegister;

sendHexCommand(Command::GET, Register::ChargeControllerTemperature);
sendHexCommand(Command::GET, Register::SmartBatterySenseTemperature);
sendHexCommand(Command::GET, Register::NetworkTotalDcInputPower);

#ifdef PROCESS_NETWORK_STATE
sendHexCommand(Command::GET, Register::NetworkInfo);
sendHexCommand(Command::GET, Register::NetworkMode);
sendHexCommand(Command::GET, Register::NetworkStatus);
#endif // PROCESS_NETWORK_STATE
}
// --> We just use hex commands for firmware >= 1.53 to keep text messages alive
// Note: First we send queries (timing improvement)
if (_canSend && (_tmpFrame.getFwVersionAsInteger() >= 153)) {

// It seems some commands get lost if we send to fast the next command.
// maybe we produce an overflow on the MPPT receive buffer or we have to wait for the MPPT answer
// before we can send the next command.
// We only send a new query in VE.Direct idle state and if no query is pending
// In case we do not get an answer we send the next query from the queue after a timeout of 500ms
// Note: _sendTimeout will be set to 0 after receiving an answer, see function hexDataHandler()
auto millisTime = millis();
if (isStateIdle() && ((millisTime - _sendTimeStamp) > _sendTimeout)) {

for (auto idx = 0; idx < _hexQueue.size(); ++idx) {

// we check if it is time to update a value
if ((millisTime - _hexQueue[idx]._lastSendTime) > (_hexQueue[idx]._readPeriod * 1000)) {
sendHexCommand(VeDirectHexCommand::GET, _hexQueue[idx]._hexRegister);
_hexQueue[idx]._lastSendTime = millisTime;
_sendTimeStamp = millisTime;

// we need this information to check if we get an answer, see hexDataHandler()
_sendTimeout = 500;
_sendQueueNr = idx;
break;
}
}

}
}

void VeDirectMpptController::loop()
{
// Second we read the messages
VeDirectFrameHandler::loop();

// Third we check if hex data is outdated
// Note: Room for improvement, longer data valid time for slow changing values
auto resetTimestamp = [this](auto& pair) {
if (pair.first > 0 && (millis() - pair.first) > (10 * 1000)) {
pair.first = 0;
Expand All @@ -142,6 +158,8 @@ void VeDirectMpptController::loop()
resetTimestamp(_tmpFrame.MpptTemperatureMilliCelsius);
resetTimestamp(_tmpFrame.SmartBatterySenseTemperatureMilliCelsius);
resetTimestamp(_tmpFrame.NetworkTotalDcInputPowerMilliWatts);
resetTimestamp(_tmpFrame.BatteryFloatMilliVolt);
resetTimestamp(_tmpFrame.BatteryAbsorptionMilliVolt);

#ifdef PROCESS_NETWORK_STATE
resetTimestamp(_tmpFrame.NetworkInfo);
Expand All @@ -153,15 +171,19 @@ void VeDirectMpptController::loop()

/*
* hexDataHandler()
* analyse the content of VE.Direct hex messages
* Handels the received hex data from the MPPT
* analyze the content of VE.Direct hex messages
* handel's the received hex data from the MPPT
*/
bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
if (data.rsp != VeDirectHexResponse::GET &&
data.rsp != VeDirectHexResponse::ASYNC) { return false; }

auto regLog = static_cast<uint16_t>(data.addr);

// we check if we get we right answer to our query
if ((data.rsp == VeDirectHexResponse::GET) && (data.addr == _hexQueue[_sendQueueNr]._hexRegister))
_sendTimeout = 0;

switch (data.addr) {
case VeDirectHexRegister::ChargeControllerTemperature:
_tmpFrame.MpptTemperatureMilliCelsius =
Expand Down Expand Up @@ -215,6 +237,29 @@ bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
return true;
break;

case VeDirectHexRegister::BatteryAbsorptionVoltage:
_tmpFrame.BatteryAbsorptionMilliVolt =
{ millis(), static_cast<uint32_t>(data.value) * 10 };
if (_verboseLogging) {
_msgOut->printf("%s Hex Data: MPPT Absorption Voltage (0x%04X): %.2fV\r\n",
_logId, regLog,
_tmpFrame.BatteryAbsorptionMilliVolt.second / 1000.0);
}
return true;
break;

case VeDirectHexRegister::BatteryFloatVoltage:
_tmpFrame.BatteryFloatMilliVolt =
{ millis(), static_cast<uint32_t>(data.value) * 10 };

if (_verboseLogging) {
_msgOut->printf("%s Hex Data: MPPT Float Voltage (0x%04X): %.2fV\r\n",
_logId, regLog,
_tmpFrame.BatteryFloatMilliVolt.second / 1000.0);
}
return true;
break;

#ifdef PROCESS_NETWORK_STATE
case VeDirectHexRegister::NetworkInfo:
_tmpFrame.NetworkInfo =
Expand Down
Loading