diff --git a/include/Battery.h b/include/Battery.h index b5f5ace63..2947dc9f6 100644 --- a/include/Battery.h +++ b/include/Battery.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include #include #include #include @@ -9,28 +8,28 @@ #include "BatteryStats.h" class BatteryProvider { - public: - // returns true if the provider is ready for use, false otherwise - virtual bool init(bool verboseLogging) = 0; - - virtual void deinit() = 0; - virtual void loop() = 0; - virtual std::shared_ptr getStats() const = 0; +public: + // returns true if the provider is ready for use, false otherwise + virtual bool init(bool verboseLogging) = 0; + virtual void deinit() = 0; + virtual void loop() = 0; + virtual std::shared_ptr getStats() const = 0; + virtual bool usesHwPort2() = 0; }; class BatteryClass { - public: - void init(Scheduler&); - void updateSettings(); +public: + void init(Scheduler&); + void updateSettings(); - std::shared_ptr getStats() const; - private: - void loop(); + std::shared_ptr getStats() const; - Task _loopTask; +private: + void loop(); - mutable std::mutex _mutex; - std::unique_ptr _upProvider = nullptr; + Task _loopTask; + mutable std::mutex _mutex; + std::unique_ptr _upProvider = nullptr; }; extern BatteryClass Battery; diff --git a/include/Configuration.h b/include/Configuration.h index 14a056670..daa77916d 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -30,6 +30,8 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 +#define VICTRON_MAX_COUNT 2 + #define POWERMETER_MAX_PHASES 3 #define POWERMETER_MAX_HTTP_URL_STRLEN 1024 #define POWERMETER_MAX_USERNAME_STRLEN 64 @@ -198,7 +200,7 @@ struct CONFIG_T { bool HttpIndividualRequests; POWERMETER_HTTP_PHASE_CONFIG_T Http_Phase[POWERMETER_MAX_PHASES]; } PowerMeter; - + struct { bool Enabled; bool VerboseLogging; @@ -225,7 +227,7 @@ struct CONFIG_T { float FullSolarPassThroughStartVoltage; float FullSolarPassThroughStopVoltage; } PowerLimiter; - + struct { bool Enabled; bool VerboseLogging; @@ -243,7 +245,7 @@ struct CONFIG_T { float Auto_Power_Voltage_Limit; float Auto_Power_Enable_Voltage_Limit; float Auto_Power_Lower_Power_Limit; - float Auto_Power_Upper_Power_Limit; + float Auto_Power_Upper_Power_Limit; } Huawei; diff --git a/include/JkBmsController.h b/include/JkBmsController.h index 5399951d4..4221f9f4a 100644 --- a/include/JkBmsController.h +++ b/include/JkBmsController.h @@ -19,6 +19,7 @@ class Controller : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() override; private: enum class Status : unsigned { diff --git a/include/MqttBattery.h b/include/MqttBattery.h index 61df04500..e75e38f3c 100644 --- a/include/MqttBattery.h +++ b/include/MqttBattery.h @@ -5,23 +5,24 @@ #include class MqttBattery : public BatteryProvider { - public: - MqttBattery() = default; +public: + MqttBattery() = default; - bool init(bool verboseLogging) final; - void deinit() final; - void loop() final { return; } // this class is event-driven - std::shared_ptr getStats() const final { return _stats; } + bool init(bool verboseLogging) final; + void deinit() final; + void loop() final { return; } // this class is event-driven + std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() override; - private: - bool _verboseLogging = false; - String _socTopic; - String _voltageTopic; - std::shared_ptr _stats = std::make_shared(); +private: + bool _verboseLogging = false; + String _socTopic; + String _voltageTopic; + std::shared_ptr _stats = std::make_shared(); - std::optional getFloat(std::string const& src, char const* topic); - void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); - void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); + std::optional getFloat(std::string const& src, char const* topic); + void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); + void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); }; diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h index 571ee1e6a..c420d0884 100644 --- a/include/MqttHandleVedirect.h +++ b/include/MqttHandleVedirect.h @@ -4,6 +4,7 @@ #include "VeDirectMpptController.h" #include "Configuration.h" #include +#include #include #ifndef VICTRON_PIN_RX @@ -20,7 +21,7 @@ class MqttHandleVedirectClass { void forceUpdate(); private: void loop(); - VeDirectMpptController::veMpptStruct _kvFrame{}; + std::map _kvFrames; Task _loopTask; @@ -31,6 +32,9 @@ class MqttHandleVedirectClass { uint32_t _nextPublishFull = 1; bool _PublishFull; + + void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, + VeDirectMpptController::veMpptStruct &frame) const; }; -extern MqttHandleVedirectClass MqttHandleVedirect; \ No newline at end of file +extern MqttHandleVedirectClass MqttHandleVedirect; diff --git a/include/MqttHandleVedirectHass.h b/include/MqttHandleVedirectHass.h index 577f08d6a..86d364cda 100644 --- a/include/MqttHandleVedirectHass.h +++ b/include/MqttHandleVedirectHass.h @@ -14,9 +14,15 @@ class MqttHandleVedirectHassClass { private: void loop(); void publish(const String& subtopic, const String& payload); - void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off); - void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL); - void createDeviceInfo(JsonObject& object); + void publishBinarySensor(const char *caption, const char *icon, const char *subTopic, + const char *payload_on, const char *payload_off, + const VeDirectMpptController::spData_t &spMpptData); + void publishSensor(const char *caption, const char *icon, const char *subTopic, + const char *deviceClass, const char *stateClass, + const char *unitOfMeasurement, + const VeDirectMpptController::spData_t &spMpptData); + void createDeviceInfo(JsonObject &object, + const VeDirectMpptController::spData_t &spMpptData); Task _loopTask; @@ -24,4 +30,4 @@ class MqttHandleVedirectHassClass { bool _updateForced = false; }; -extern MqttHandleVedirectHassClass MqttHandleVedirectHass; \ No newline at end of file +extern MqttHandleVedirectHassClass MqttHandleVedirectHass; diff --git a/include/PinMapping.h b/include/PinMapping.h index 4096e8072..6197b5a45 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -40,6 +40,8 @@ struct PinMapping_t { uint8_t display_reset; int8_t victron_tx; int8_t victron_rx; + int8_t victron_tx2; + int8_t victron_rx2; int8_t battery_rx; int8_t battery_rxen; int8_t battery_tx; @@ -63,7 +65,7 @@ class PinMappingClass { bool isValidCmt2300Config() const; bool isValidEthConfig() const; bool isValidHuaweiConfig() const; - + private: PinMapping_t _pinMapping; }; diff --git a/include/PylontechCanReceiver.h b/include/PylontechCanReceiver.h index 2b2b922d9..7b6404253 100644 --- a/include/PylontechCanReceiver.h +++ b/include/PylontechCanReceiver.h @@ -14,6 +14,7 @@ class PylontechCanReceiver : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() override; private: uint16_t readUnsignedInt16(uint8_t *data); diff --git a/include/SerialPortManager.h b/include/SerialPortManager.h new file mode 100644 index 000000000..495e70141 --- /dev/null +++ b/include/SerialPortManager.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class SerialPortManager { +public: + bool allocateMpptPort(int port); + bool allocateBatteryPort(int port); + void invalidateBatteryPort(); + void invalidateMpptPorts(); + +private: + enum Owner { + BATTERY, + MPPT + }; + + std::map allocatedPorts; + + bool allocatePort(uint8_t port, Owner owner); + void invalidate(Owner owner); + + static const char* print(Owner owner); +}; + +extern SerialPortManager PortManager; diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 12d6bdf75..8ce4e8dea 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -5,6 +5,7 @@ #include #include "VeDirectMpptController.h" +#include "Configuration.h" #include class VictronMpptClass { @@ -16,12 +17,13 @@ class VictronMpptClass { void updateSettings(); bool isDataValid() const; + bool isDataValid(size_t idx) const; // returns the data age of all controllers, // i.e, the youngest data's age is returned. uint32_t getDataAgeMillis() const; - VeDirectMpptController::spData_t getData(size_t idx = 0) const; + std::optional getData(size_t idx = 0) const; // total output of all MPPT charge controllers in Watts int32_t getPowerOutputWatts() const; @@ -50,6 +52,8 @@ class VictronMpptClass { mutable std::mutex _mutex; using controller_t = std::unique_ptr; std::vector _controllers; + + bool initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort); }; extern VictronMpptClass VictronMppt; diff --git a/include/VictronSmartShunt.h b/include/VictronSmartShunt.h index ffb91ee5b..0c5c5d89f 100644 --- a/include/VictronSmartShunt.h +++ b/include/VictronSmartShunt.h @@ -9,6 +9,7 @@ class VictronSmartShunt : public BatteryProvider { void deinit() final { } void loop() final; std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() override; private: uint32_t _lastUpdate = 0; diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index 3e0b81aba..3b41aa79f 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -2,6 +2,7 @@ #pragma once #include "ArduinoJson.h" +#include "Configuration.h" #include #include #include @@ -14,6 +15,7 @@ class WebApiWsVedirectLiveClass { private: void generateJsonResponse(JsonVariant& root); + static void populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); @@ -22,7 +24,7 @@ class WebApiWsVedirectLiveClass { uint32_t _lastWsPublish = 0; uint32_t _dataAgeMillis = 0; - static constexpr uint16_t _responseSize = 1024 + 128; + static constexpr uint16_t _responseSize = VICTRON_MAX_COUNT * (1024 + 128); std::mutex _mutex; @@ -31,4 +33,4 @@ class WebApiWsVedirectLiveClass { Task _sendDataTask; void sendDataTaskCb(); -}; \ No newline at end of file +}; diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 5b8d6afd7..4112510f4 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -1,9 +1,9 @@ #include #include "VeDirectMpptController.h" -void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) +void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { - VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 1); + VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, hwSerialPort); _spData = std::make_shared(); if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); } } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index 158772373..08574252d 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -39,7 +39,7 @@ class VeDirectMpptController : public VeDirectFrameHandler { public: VeDirectMpptController() = default; - void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging); + void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); bool isDataValid() const; // return true if data valid and not outdated struct veMpptStruct : veStruct { @@ -49,7 +49,7 @@ class VeDirectMpptController : public VeDirectFrameHandler { double VPV; // panel voltage in V double 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 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 diff --git a/src/Battery.cpp b/src/Battery.cpp index 381fdc952..0ea2b979f 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -5,6 +5,7 @@ #include "JkBmsController.h" #include "VictronSmartShunt.h" #include "MqttBattery.h" +#include "SerialPortManager.h" BatteryClass Battery; @@ -38,6 +39,7 @@ void BatteryClass::updateSettings() _upProvider->deinit(); _upProvider = nullptr; } + PortManager.invalidateBatteryPort(); CONFIG_T& config = Configuration.get(); if (!config.Battery.Enabled) { return; } @@ -47,23 +49,32 @@ void BatteryClass::updateSettings() switch (config.Battery.Provider) { case 0: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; case 1: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; case 2: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; case 3: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; default: MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider); - break; + return; + } + + if(_upProvider->usesHwPort2()) { + if (!PortManager.allocateBatteryPort(2)) { + MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2); + _upProvider = nullptr; + return; + } + } + + if (!_upProvider->init(verboseLogging)) { + PortManager.invalidateBatteryPort(); + _upProvider = nullptr; } } diff --git a/src/JkBmsController.cpp b/src/JkBmsController.cpp index 3f924030f..381b1a8eb 100644 --- a/src/JkBmsController.cpp +++ b/src/JkBmsController.cpp @@ -427,4 +427,8 @@ void Controller::processDataPoints(DataPointContainer const& dataPoints) } } +bool Controller::usesHwPort2() { + return true; +} + } /* namespace JkBms */ diff --git a/src/MqttBattery.cpp b/src/MqttBattery.cpp index 03e141e2f..0ce073fe9 100644 --- a/src/MqttBattery.cpp +++ b/src/MqttBattery.cpp @@ -112,3 +112,7 @@ void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties con *voltage, topic); } } + +bool MqttBattery::usesHwPort2() { + return false; +} diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index b176e9410..e3e0655dc 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -7,7 +7,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "MessageOutput.h" -#include "VictronMppt.h" +#include "VictronMppt.h" #include "Utils.h" MqttHandleVedirectHassClass MqttHandleVedirectHass; @@ -15,7 +15,7 @@ MqttHandleVedirectHassClass MqttHandleVedirectHass; void MqttHandleVedirectHassClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&MqttHandleVedirectHassClass::loop, this)); + _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); } @@ -55,43 +55,56 @@ void MqttHandleVedirectHassClass::publishConfig() if (!MqttSettings.getConnected()) { return; } - // ensure data is revieved from victron - if (!VictronMppt.isDataValid()) { - return; - } // device info - publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF"); - publishSensor("MPPT serial number", "mdi:counter", "SER"); - publishSensor("MPPT firmware number", "mdi:counter", "FW"); - publishSensor("MPPT state of operation", "mdi:wrench", "CS"); - publishSensor("MPPT error code", "mdi:bell", "ERR"); - publishSensor("MPPT off reason", "mdi:wrench", "OR"); - publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT"); - publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d"); - - // battery info - publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V"); - publishSensor("Battery current", NULL, "I", "current", "measurement", "A"); - publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W"); - publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%"); - - // panel info - publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V"); - publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A"); - publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W"); - publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh"); - publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh"); - publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W"); - publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh"); - publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W"); + for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) { + // ensure data is received from victron + if (!VictronMppt.isDataValid(idx)) { + continue; + } + + std::optional spOptMpptData = VictronMppt.getData(idx); + if (!spOptMpptData.has_value()) { + continue; + } + + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); + + publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", spMpptData); + publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", spMpptData); + + // battery info + publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", spMpptData); + publishSensor("Battery current", NULL, "I", "current", "measurement", "A", spMpptData); + publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", spMpptData); + publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", spMpptData); + + // panel info + publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", spMpptData); + publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", spMpptData); + publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", spMpptData); + publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", spMpptData); + publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", spMpptData); + publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", spMpptData); + publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", spMpptData); + publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", spMpptData); + } yield(); } -void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement ) +void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic, + const char *deviceClass, const char *stateClass, + const char *unitOfMeasurement, + const VeDirectMpptController::spData_t &spMpptData) { - String serial = VictronMppt.getData()->SER; + String serial = spMpptData->SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -126,7 +139,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* } JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj); + createDeviceInfo(deviceObj, spMpptData); if (Configuration.get().Mqtt.Hass.Expire) { root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3; @@ -143,9 +156,11 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* publish(configTopic, buffer); } -void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off) +void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic, + const char *payload_on, const char *payload_off, + const VeDirectMpptController::spData_t &spMpptData) { - String serial = VictronMppt.getData()->SER; + String serial = spMpptData->SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -178,16 +193,16 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const } JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj); + createDeviceInfo(deviceObj, spMpptData); char buffer[512]; serializeJson(root, buffer); publish(configTopic, buffer); } -void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object) +void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, + const VeDirectMpptController::spData_t &spMpptData) { - auto spMpptData = VictronMppt.getData(); String serial = spMpptData->SER; object["name"] = "Victron(" + serial + ")"; object["ids"] = serial; diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index af659f0d7..b363493f2 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -17,7 +17,7 @@ MqttHandleVedirectClass MqttHandleVedirect; void MqttHandleVedirectClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&MqttHandleVedirectClass::loop, this)); + _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); @@ -41,10 +41,6 @@ void MqttHandleVedirectClass::loop() return; } - if (!VictronMppt.isDataValid()) { - return; - } - if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) { // determine if this cycle should publish full values or updates only if (_nextPublishFull <= _nextPublishUpdatesOnly) { @@ -62,82 +58,23 @@ void MqttHandleVedirectClass::loop() } #endif - auto spMpptData = VictronMppt.getData(); - String value; - String topic = "victron/"; - topic.concat(spMpptData->SER); - topic.concat("/"); - - if (_PublishFull || spMpptData->PID != _kvFrame.PID) - MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data()); - if (_PublishFull || strcmp(spMpptData->SER, _kvFrame.SER) != 0) - MqttSettings.publish(topic + "SER", spMpptData->SER ); - if (_PublishFull || strcmp(spMpptData->FW, _kvFrame.FW) != 0) - MqttSettings.publish(topic + "FW", spMpptData->FW); - if (_PublishFull || spMpptData->LOAD != _kvFrame.LOAD) - MqttSettings.publish(topic + "LOAD", spMpptData->LOAD == true ? "ON": "OFF"); - if (_PublishFull || spMpptData->CS != _kvFrame.CS) - MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data()); - if (_PublishFull || spMpptData->ERR != _kvFrame.ERR) - MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data()); - if (_PublishFull || spMpptData->OR != _kvFrame.OR) - MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data()); - if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT) - MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data()); - if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) { - value = spMpptData->HSDS; - MqttSettings.publish(topic + "HSDS", value); - } - if (_PublishFull || spMpptData->V != _kvFrame.V) { - value = spMpptData->V; - MqttSettings.publish(topic + "V", value); - } - if (_PublishFull || spMpptData->I != _kvFrame.I) { - value = spMpptData->I; - MqttSettings.publish(topic + "I", value); - } - if (_PublishFull || spMpptData->P != _kvFrame.P) { - value = spMpptData->P; - MqttSettings.publish(topic + "P", value); - } - if (_PublishFull || spMpptData->VPV != _kvFrame.VPV) { - value = spMpptData->VPV; - MqttSettings.publish(topic + "VPV", value); - } - if (_PublishFull || spMpptData->IPV != _kvFrame.IPV) { - value = spMpptData->IPV; - MqttSettings.publish(topic + "IPV", value); - } - if (_PublishFull || spMpptData->PPV != _kvFrame.PPV) { - value = spMpptData->PPV; - MqttSettings.publish(topic + "PPV", value); - } - if (_PublishFull || spMpptData->E != _kvFrame.E) { - value = spMpptData->E; - MqttSettings.publish(topic + "E", value); - } - if (_PublishFull || spMpptData->H19 != _kvFrame.H19) { - value = spMpptData->H19; - MqttSettings.publish(topic + "H19", value); - } - if (_PublishFull || spMpptData->H20 != _kvFrame.H20) { - value = spMpptData->H20; - MqttSettings.publish(topic + "H20", value); - } - if (_PublishFull || spMpptData->H21 != _kvFrame.H21) { - value = spMpptData->H21; - MqttSettings.publish(topic + "H21", value); - } - if (_PublishFull || spMpptData->H22 != _kvFrame.H22) { - value = spMpptData->H22; - MqttSettings.publish(topic + "H22", value); - } - if (_PublishFull || spMpptData->H23 != _kvFrame.H23) { - value = spMpptData->H23; - MqttSettings.publish(topic + "H23", value); - } - if (!_PublishFull) { - _kvFrame = *spMpptData; + for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) { + if (!VictronMppt.isDataValid(idx)) { + continue; + } + + std::optional spOptMpptData = VictronMppt.getData(idx); + if (!spOptMpptData.has_value()) { + continue; + } + + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); + + VeDirectMpptController::veMpptStruct _kvFrame = _kvFrames[spMpptData->SER]; + publish_mppt_data(spMpptData, _kvFrame); + if (!_PublishFull) { + _kvFrames[spMpptData->SER] = *spMpptData; + } } // now calculate next points of time to publish @@ -165,4 +102,81 @@ void MqttHandleVedirectClass::loop() MessageOutput.printf("MqttHandleVedirectClass::loop _nextPublishUpdatesOnly %u _nextPublishFull %u\r\n", _nextPublishUpdatesOnly, _nextPublishFull); #endif } -} \ No newline at end of file +} + +void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, + VeDirectMpptController::veMpptStruct &frame) const { + String value; + String topic = "victron/"; + topic.concat(spMpptData->SER); + topic.concat("/"); + + if (_PublishFull || spMpptData->PID != frame.PID) + MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data()); + if (_PublishFull || strcmp(spMpptData->SER, frame.SER) != 0) + MqttSettings.publish(topic + "SER", spMpptData->SER ); + if (_PublishFull || strcmp(spMpptData->FW, frame.FW) != 0) + MqttSettings.publish(topic + "FW", spMpptData->FW); + if (_PublishFull || spMpptData->LOAD != frame.LOAD) + MqttSettings.publish(topic + "LOAD", spMpptData->LOAD ? "ON" : "OFF"); + if (_PublishFull || spMpptData->CS != frame.CS) + MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data()); + if (_PublishFull || spMpptData->ERR != frame.ERR) + MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data()); + if (_PublishFull || spMpptData->OR != frame.OR) + MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data()); + if (_PublishFull || spMpptData->MPPT != frame.MPPT) + MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data()); + if (_PublishFull || spMpptData->HSDS != frame.HSDS) { + value = spMpptData->HSDS; + MqttSettings.publish(topic + "HSDS", value); + } + if (_PublishFull || spMpptData->V != frame.V) { + value = spMpptData->V; + MqttSettings.publish(topic + "V", value); + } + if (_PublishFull || spMpptData->I != frame.I) { + value = spMpptData->I; + MqttSettings.publish(topic + "I", value); + } + if (_PublishFull || spMpptData->P != frame.P) { + value = spMpptData->P; + MqttSettings.publish(topic + "P", value); + } + if (_PublishFull || spMpptData->VPV != frame.VPV) { + value = spMpptData->VPV; + MqttSettings.publish(topic + "VPV", value); + } + if (_PublishFull || spMpptData->IPV != frame.IPV) { + value = spMpptData->IPV; + MqttSettings.publish(topic + "IPV", value); + } + if (_PublishFull || spMpptData->PPV != frame.PPV) { + value = spMpptData->PPV; + MqttSettings.publish(topic + "PPV", value); + } + if (_PublishFull || spMpptData->E != frame.E) { + value = spMpptData->E; + MqttSettings.publish(topic + "E", value); + } + if (_PublishFull || spMpptData->H19 != frame.H19) { + value = spMpptData->H19; + MqttSettings.publish(topic + "H19", value); + } + if (_PublishFull || spMpptData->H20 != frame.H20) { + value = spMpptData->H20; + MqttSettings.publish(topic + "H20", value); + } + if (_PublishFull || spMpptData->H21 != frame.H21) { + value = spMpptData->H21; + MqttSettings.publish(topic + "H21", value); + } + if (_PublishFull || spMpptData->H22 != frame.H22) { + value = spMpptData->H22; + MqttSettings.publish(topic + "H22", value); + } + if (_PublishFull || spMpptData->H23 != frame.H23) { + value = spMpptData->H23; + MqttSettings.publish(topic + "H23", value); + } +} diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index bf78e4ede..39eeca13b 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -181,9 +181,12 @@ PinMappingClass::PinMappingClass() _pinMapping.display_clk = DISPLAY_CLK; _pinMapping.display_cs = DISPLAY_CS; _pinMapping.display_reset = DISPLAY_RESET; - - _pinMapping.victron_tx = VICTRON_PIN_TX; + _pinMapping.victron_rx = VICTRON_PIN_RX; + _pinMapping.victron_tx = VICTRON_PIN_TX; + + _pinMapping.victron_rx2 = VICTRON_PIN_RX; + _pinMapping.victron_tx2 = VICTRON_PIN_TX; _pinMapping.battery_rx = BATTERY_PIN_RX; _pinMapping.battery_rxen = BATTERY_PIN_RXEN; @@ -259,6 +262,8 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX; _pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX; + _pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX; + _pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX; _pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX; _pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN; diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index e19cff599..6f18fb4d8 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -266,6 +266,10 @@ bool PylontechCanReceiver::getBit(uint8_t value, uint8_t bit) return (value & (1 << bit)) >> bit; } +bool PylontechCanReceiver::usesHwPort2() { + return false; +} + #ifdef PYLONTECH_DUMMY void PylontechCanReceiver::dummyData() { diff --git a/src/SerialPortManager.cpp b/src/SerialPortManager.cpp new file mode 100644 index 000000000..e5db3395f --- /dev/null +++ b/src/SerialPortManager.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SerialPortManager.h" +#include "MessageOutput.h" + +#define MAX_CONTROLLERS 3 + +SerialPortManager PortManager; + +bool SerialPortManager::allocateBatteryPort(int port) +{ + return allocatePort(port, Owner::BATTERY); +} + +bool SerialPortManager::allocateMpptPort(int port) +{ + return allocatePort(port, Owner::MPPT); +} + +bool SerialPortManager::allocatePort(uint8_t port, Owner owner) +{ + if (port >= MAX_CONTROLLERS) { + MessageOutput.printf("[SerialPortManager] Invalid serial port = %d \r\n", port); + return false; + } + + return allocatedPorts.insert({port, owner}).second; +} + +void SerialPortManager::invalidateBatteryPort() +{ + invalidate(Owner::BATTERY); +} + +void SerialPortManager::invalidateMpptPorts() +{ + invalidate(Owner::MPPT); +} + +void SerialPortManager::invalidate(Owner owner) +{ + for (auto it = allocatedPorts.begin(); it != allocatedPorts.end();) { + if (it->second == owner) { + MessageOutput.printf("[SerialPortManager] Removing port = %d, owner = %s \r\n", it->first, print(owner)); + it = allocatedPorts.erase(it); + } else { + ++it; + } + } +} + +const char* SerialPortManager::print(Owner owner) +{ + switch (owner) { + case BATTERY: + return "BATTERY"; + case MPPT: + return "MPPT"; + } +} diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index c4dd0bd5a..fec0f67d3 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -3,13 +3,14 @@ #include "Configuration.h" #include "PinMapping.h" #include "MessageOutput.h" +#include "SerialPortManager.h" VictronMpptClass VictronMppt; void VictronMpptClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&VictronMpptClass::loop, this)); + _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); @@ -21,24 +22,41 @@ void VictronMpptClass::updateSettings() std::lock_guard lock(_mutex); _controllers.clear(); + PortManager.invalidateMpptPorts(); CONFIG_T& config = Configuration.get(); if (!config.Vedirect.Enabled) { return; } const PinMapping_t& pin = PinMapping.get(); - int8_t rx = pin.victron_rx; - int8_t tx = pin.victron_tx; - MessageOutput.printf("[VictronMppt] rx = %d, tx = %d\r\n", rx, tx); + int hwSerialPort = 1; + bool initSuccess = initController(pin.victron_rx, pin.victron_tx, config.Vedirect.VerboseLogging, hwSerialPort); + if (initSuccess) { + hwSerialPort++; + } + + initController(pin.victron_rx2, pin.victron_tx2, config.Vedirect.VerboseLogging, hwSerialPort); +} + +bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort) +{ + MessageOutput.printf("[VictronMppt] rx = %d, tx = %d, hwSerialPort = %d\r\n", rx, tx, hwSerialPort); if (rx < 0) { - MessageOutput.println("[VictronMppt] invalid pin config"); - return; + MessageOutput.printf("[VictronMppt] invalid pin config rx = %d, tx = %d\r\n", rx, tx); + return false; + } + + if (!PortManager.allocateMpptPort(hwSerialPort)) { + MessageOutput.printf("[VictronMppt] Serial port %d already in use. Initialization aborted!\r\n", + hwSerialPort); + return false; } auto upController = std::make_unique(); - upController->init(rx, tx, &MessageOutput, config.Vedirect.VerboseLogging); + upController->init(rx, tx, &MessageOutput, logging, hwSerialPort); _controllers.push_back(std::move(upController)); + return true; } void VictronMpptClass::loop() @@ -54,13 +72,24 @@ bool VictronMpptClass::isDataValid() const { std::lock_guard lock(_mutex); - for (auto const& upController : _controllers) { + for (auto const& upController: _controllers) { if (!upController->isDataValid()) { return false; } } return !_controllers.empty(); } +bool VictronMpptClass::isDataValid(size_t idx) const +{ + std::lock_guard lock(_mutex); + + if (_controllers.empty() || idx >= _controllers.size()) { + return false; + } + + return _controllers[idx]->isDataValid(); +} + uint32_t VictronMpptClass::getDataAgeMillis() const { std::lock_guard lock(_mutex); @@ -81,17 +110,17 @@ uint32_t VictronMpptClass::getDataAgeMillis() const return age; } -VeDirectMpptController::spData_t VictronMpptClass::getData(size_t idx) const +std::optional VictronMpptClass::getData(size_t idx) const { std::lock_guard lock(_mutex); if (_controllers.empty() || idx >= _controllers.size()) { MessageOutput.printf("ERROR: MPPT controller index %d is out of bounds (%d controllers)\r\n", - idx, _controllers.size()); - return std::make_shared(); + idx, _controllers.size()); + return std::nullopt; } - return _controllers[idx]->getData(); + return std::optional{_controllers[idx]->getData()}; } int32_t VictronMpptClass::getPowerOutputWatts() const diff --git a/src/VictronSmartShunt.cpp b/src/VictronSmartShunt.cpp index 7b6da145a..6a3047763 100644 --- a/src/VictronSmartShunt.cpp +++ b/src/VictronSmartShunt.cpp @@ -34,3 +34,7 @@ void VictronSmartShunt::loop() _stats->updateFrom(VeDirectShunt.veFrame); _lastUpdate = VeDirectShunt.getLastUpdate(); } + +bool VictronSmartShunt::usesHwPort2() { + return true; +} diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index cc08dfaab..9ab8d4fa2 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -86,9 +86,11 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) led["brightness"] = config.Led_Single[i].Brightness; } - JsonObject victronPinObj = curPin.createNestedObject("victron"); + auto victronPinObj = curPin.createNestedObject("victron"); victronPinObj["rx"] = pin.victron_rx; victronPinObj["tx"] = pin.victron_tx; + victronPinObj["rx2"] = pin.victron_rx2; + victronPinObj["tx2"] = pin.victron_tx2; JsonObject batteryPinObj = curPin.createNestedObject("battery"); batteryPinObj["rx"] = pin.battery_rx; diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index df59172e8..cf598c88f 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -32,7 +32,7 @@ void WebApiWsVedirectLiveClass::init(AsyncWebServer& server, Scheduler& schedule _server->addHandler(&_ws); _ws.onEvent(std::bind(&WebApiWsVedirectLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); - + scheduler.addTask(_wsCleanupTask); _wsCleanupTask.setCallback(std::bind(&WebApiWsVedirectLiveClass::wsCleanupTaskCb, this)); _wsCleanupTask.setIterations(TASK_FOREVER); @@ -61,13 +61,13 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() // we assume this loop to be running at least twice for every // update from a VE.Direct MPPT data producer, so _dataAgeMillis - // acutally grows in between updates. + // actually grows in between updates. auto lastDataAgeMillis = _dataAgeMillis; _dataAgeMillis = VictronMppt.getDataAgeMillis(); // Update on ve.direct change or at least after 10 seconds if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) { - + try { std::lock_guard lock(_mutex); DynamicJsonDocument root(_responseSize); @@ -77,7 +77,7 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() String buffer; serializeJson(root, buffer); - + if (Configuration.get().Security.AllowReadonly) { _ws.setAuthentication("", ""); } else { @@ -99,15 +99,36 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) { - auto spMpptData = VictronMppt.getData(); + root["vedirect"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000; + const JsonArray &array = root["vedirect"].createNestedArray("devices"); + + for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) { + std::optional spOptMpptData = VictronMppt.getData(idx); + if (!spOptMpptData.has_value()) { + continue; + } + + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); + + const JsonObject &nested = array.createNestedObject(); + nested["age_critical"] = !VictronMppt.isDataValid(idx); + populateJson(nested, spMpptData); + } + + // power limiter state + root["dpl"]["PLSTATE"] = -1; + if (Configuration.get().PowerLimiter.Enabled) + root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); + root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); +} +void +WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) { // device info - root["device"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000; - root["device"]["age_critical"] = !VictronMppt.isDataValid(); root["device"]["PID"] = spMpptData->getPidAsString(); root["device"]["SER"] = spMpptData->SER; root["device"]["FW"] = spMpptData->FW; - root["device"]["LOAD"] = spMpptData->LOAD == true ? "ON" : "OFF"; + root["device"]["LOAD"] = spMpptData->LOAD ? "ON" : "OFF"; root["device"]["CS"] = spMpptData->getCsAsString(); root["device"]["ERR"] = spMpptData->getErrAsString(); root["device"]["OR"] = spMpptData->getOrAsString(); @@ -115,7 +136,7 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) root["device"]["HSDS"]["v"] = spMpptData->HSDS; root["device"]["HSDS"]["u"] = "d"; - // battery info + // battery info root["output"]["P"]["v"] = spMpptData->P; root["output"]["P"]["u"] = "W"; root["output"]["P"]["d"] = 0; @@ -154,12 +175,6 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23; root["input"]["MaximumPowerYesterday"]["u"] = "W"; root["input"]["MaximumPowerYesterday"]["d"] = 0; - - // power limiter state - root["dpl"]["PLSTATE"] = -1; - if (Configuration.get().PowerLimiter.Enabled) - root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); - root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); } void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) @@ -199,4 +214,4 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request) MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what()); WebApi.sendTooManyRequests(request); } -} \ No newline at end of file +} diff --git a/webapp/src/components/VedirectView.vue b/webapp/src/components/VedirectView.vue index 215ce80de..004911558 100644 --- a/webapp/src/components/VedirectView.vue +++ b/webapp/src/components/VedirectView.vue @@ -9,25 +9,25 @@ @@ -178,7 +178,7 @@ \ No newline at end of file + diff --git a/webapp/src/types/VedirectLiveDataStatus.ts b/webapp/src/types/VedirectLiveDataStatus.ts index f6635c4c3..c3117a07b 100644 --- a/webapp/src/types/VedirectLiveDataStatus.ts +++ b/webapp/src/types/VedirectLiveDataStatus.ts @@ -5,12 +5,22 @@ export interface DynamicPowerLimiter { PLLIMIT: number; } +export interface Vedirect { + data_age: 0; + devices: Array; +} + +export interface VedirectDevices { + age_critical: boolean; + device: VedirectDevice; + input: VedirectInput; + output: VedirectOutput; +} + export interface VedirectDevice { SER: string; PID: string; FW: string; - age_critical: boolean; - data_age: 0; LOAD: ValueObject; CS: ValueObject; MPPT: ValueObject;