diff --git a/README.md b/README.md index 45677b55..5392e74d 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ -- [OpenDTU-onBattery](#opendtu-onbattery) - - [What is OpenDTU-onBattery](#what-is-opendtu-onbattery) +- [OpenDTU-OnBattery](#opendtu-onbattery) + - [What is OpenDTU-OnBattery](#what-is-opendtu-onbattery) - [History of the project](#history-of-the-project) - - [Highlights of OpenDTU-onBattery](#highlights-of-opendtu-onbattery) + - [Highlights of OpenDTU-OnBattery](#highlights-of-opendtu-onbattery) - [Documentation](#documentation) - [Acknowledgment](#acknowledgment) -# OpenDTU-onBattery +# OpenDTU-OnBattery This is a fork from the Hoymiles project [OpenDTU](https://github.com/tbnobody/OpenDTU). ![GitHub tag (latest SemVer)](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/helgeerbe/68b47cc8c8994d04ab3a4fa9d8aee5e6/raw/openDTUcoreRelease.json) -[![OpenDTU-onBattery Build](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml) +[![OpenDTU-OnBattery Build](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml) [![cpplint](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml) [![Yarn Linting](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml) -## What is OpenDTU-onBattery +## What is OpenDTU-OnBattery -OpenDTU-onBattery is an extension of the original OpenDTU to support battery chargers, battery management systems (BMS) and power meters on a single esp32. With the help of a dynamic power limiter, the power production can be adjusted to the actual consumption. In this way, it is possible to come as close as possible to the goal of zero feed-in. +OpenDTU-OnBattery is an extension of the original OpenDTU to support battery chargers, battery management systems (BMS) and power meters on a single esp32. With the help of a dynamic power limiter, the power production can be adjusted to the actual consumption. In this way, it is possible to come as close as possible to the goal of zero feed-in. ## History of the project @@ -25,7 +25,7 @@ The original OpenDTU project was started from [this](https://www.mikrocontroller Summer 2022 I bought my Victron MPPT battery charger, and didn't like the idea to set up a separate esp32 to recieve the charger data. I decided to fork OpenDTU and extend it with battery charger support and a dynamic power limitter to my own needs. Hoping someone can make use of it. -## Highlights of OpenDTU-onBattery +## Highlights of OpenDTU-OnBattery This project is still under development and adds following features: @@ -40,11 +40,11 @@ This project is still under development and adds following features: ## Documentation -[Full documentation of OpenDTU-onBattery extensions can be found at the project's wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki). +Documentation of OpenDTU-OnBattery extensions can be found in [the project's wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki). -For documentation of openDTU core functionality I refer to the original [repo](https://github.com/tbnobody/OpenDTU) and its [documentation](https://tbnobody.github.io/OpenDTU-docs/). +For documentation of OpenDTU core functionality refer to the original [repo](https://github.com/tbnobody/OpenDTU) and its [documentation](https://opendtu.solar). -Please note that openDTU-onBattery may change significantly during its development. +Please note that OpenDTU-OnBattery may change significantly during its development. Bug reports, comments, feature requests and fixes are most welcome! To find out what's new or improved have a look at the [changelog](https://github.com/helgeerbe/OpenDTU-OnBattery/releases). @@ -53,4 +53,4 @@ To find out what's new or improved have a look at the [changelog](https://github A special Thank to Thomas Basler (tbnobody) the author of the original [OpenDTU](https://github.com/tbnobody/OpenDTU) project. You are doing a great job! -Last but not least, I would like to thank all the contributors. With your ideas and enhancements, you have made OpenDTU-onBattery much more than I originally had in mind. +Last but not least, I would like to thank all the contributors. With your ideas and enhancements, you have made OpenDTU-OnBattery much more than I originally had in mind. diff --git a/README_onBattery.md b/README_onBattery.md index 3f484c5c..e1489af6 100644 --- a/README_onBattery.md +++ b/README_onBattery.md @@ -6,7 +6,7 @@ This is a fork from the Hoymiles project [OpenDTU](https://github.com/tbnobody/O > **Warning** > -> In contrast to the original openDTU, with release 2023.05.23.post1 openDTU-onBattery supports only 5 inverters. Otherwise, there is not enough memory for the liveData view. +> In contrast to the original openDTU, with release 2023.05.23.post1 openDTU-OnBattery supports only 5 inverters. Otherwise, there is not enough memory for the liveData view. ## Features @@ -106,7 +106,7 @@ Some points for consideration are: #### Operation modes -openDTU-onBattery supports three operation modes for the Huawei PSU: +openDTU-OnBattery supports three operation modes for the Huawei PSU: 1. Fully manual - In this mode the PSU needs to be turned on/off externally using MQTT and voltage and current limits need to be provided. See [MQTT Documentation](docs/MQTT_Topics.md) for details on these commands 2. Manual with auto power on / off - In this mode the PSU is turned on when a current limit > 1A is set. If the current limit is < 1A for some time the PSU is turned off. Current and voltage limits need to be provided externally using MQTT. See [MQTT Documentation](docs/MQTT_Topics.md) for details on these commands. 3. Automatic - In this mode the PSU power is controlled by the Power Meter and information provided in the web-interface. If excess power is present the PSU is turned on. The voltage limit is set as per web-interface and the current limit is set so that the maximum PSU output power equals the Power Meter value. Minium and maximum PSU power levels as configured in the web-interface are respected in this process. The PSU is turned off if the output current is limited and the output power drops below the minium power level. This will disable automatic mode until the battery is discharged below the start voltage level (set in the web-interface). This mode can be enabled using the web-interface and MQTT. See [MQTT Documentation](docs/MQTT_Topics.md) @@ -135,7 +135,7 @@ It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with th ### Status -[![OpenDTU-onBattery Build](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml) +[![OpenDTU-OnBattery Build](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/build.yml) [![cpplint](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/cpplint.yml) [![Yarn Linting](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml/badge.svg)](https://github.com/helgeerbe/OpenDTU-OnBattery/actions/workflows/yarnlint.yml) diff --git a/docs/DeviceProfiles.md b/docs/DeviceProfiles.md index 3444acc6..8976b821 100644 --- a/docs/DeviceProfiles.md +++ b/docs/DeviceProfiles.md @@ -1,91 +1,3 @@ # Device Profiles -This documentation will has been moved and can be found here: - -## Structure of the json file for openDTU-onBattery (outdated example) - -```json -[ - { - "name": "Generic NodeMCU 38 pin", - "nrf24": { - "miso": 19, - "mosi": 23, - "clk": 18, - "irq": 16, - "en": 4, - "cs": 5 - }, - "victron": { - "rx": 22, - "tx": 21 - }, - "battery": { - "rx": 27, - "tx": 14 - }, - "huawei": { - "miso": 12, - "mosi": 13, - "clk": 26, - "irq": 25, - "power": 33, - "cs": 15 - }, - "eth": { - "enabled": false, - "phy_addr": -1, - "power": -1, - "mdc": -1, - "mdio": -1, - "type": -1, - "clk_mode": -1 - } - }, - { - "name": "Generic NodeMCU 38 pin with SSD1306", - "nrf24": { - "miso": 19, - "mosi": 23, - "clk": 18, - "irq": 16, - "en": 4, - "cs": 5 - }, - "eth": { - "enabled": false, - "phy_addr": -1, - "power": -1, - "mdc": -1, - "mdio": -1, - "type": -1, - "clk_mode": -1 - }, - "display": { - "type": 2, - "data": 21, - "clk": 22 - } - }, - { - "name": "Olimex ESP32-POE", - "nrf24": { - "miso": 15, - "mosi": 2, - "clk": 14, - "irq": 13, - "en": 16, - "cs": 5 - }, - "eth": { - "enabled": true, - "phy_addr": 0, - "power": 12, - "mdc": 23, - "mdio": 18, - "type": 0, - "clk_mode": 3 - } - } -] -``` \ No newline at end of file +This documentation has been [moved to the wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki/Device-Profiles). \ No newline at end of file diff --git a/docs/hardware_flash.md b/docs/hardware_flash.md index e8aa193c..2d1db186 100644 --- a/docs/hardware_flash.md +++ b/docs/hardware_flash.md @@ -2,15 +2,28 @@ ## Hardware you need +For a highly integrated board with most peripherals already included, have a +look at the [OpenDTU Fusion](https://shop.allianceapps.io/products/opendtu-fusion-community-edition) +board. There are also [OpenDTU-OnBattery-specific addons](https://github.com/markusdd/OpenDTUFusionDocs/blob/main/CANIso.md). + +Otherwise, read on to assemble your own components. + ### ESP32 board -For ease of use, buy a "ESP32 DEVKIT DOIT" or "ESP32 NodeMCU Development Board" with an ESP32-S3 or ESP-WROOM-32 chipset on it. +For ease of use, buy a "ESP32 DEVKIT DOIT" or "ESP32 NodeMCU Development Board" +with an ESP32-S3 chipset on it, **with at least 8 MB of flash memory**. +Preferrably, the ESP32 should also have some embedded PSRAM. Look out for +labels like "N8R2", "N16R8", where the number after the N is the amount of +flash in Megabytes, and the number after the R is the amount of PSRAM in +Megabytes. Sample Picture: ![NodeMCU-ESP32](nodemcu-esp32.png) -Also supported: Board with Ethernet-Connector and Power-over-Ethernet [Olimex ESP32-POE](https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware) +Also supported: Board with Ethernet-Connector and Power-over-Ethernet (PoE) +(select version with 8 MB of flash memory or more) +[Olimex ESP32-POE](https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware) ### NRF24L01+ radio board (See inverter table above for supported inverters) @@ -41,7 +54,10 @@ The CMT2300A uses 3-Wire half duplex SPI communication. Due to this fact it curr ### 3.3V / 5V logic level converter -The logic level converter is used to interface with the Victron MPPT charge controller and the relay board. It converts the 3.3V logic level used by the ESP32 to 5V logic used by the other devices. +The logic level converter is used to interface with the Victron MPPT charge +controller and the relay board. It converts the 3.3V logic level used by the +ESP32 to 5V logic used by the other devices. A commonly used digital isolator +is the ADUM1201. ### SN65HVD230 CAN bus transceiver @@ -49,7 +65,7 @@ The SN65HVD230 CAN bus transceiver is used to interface with the Pylontech batte ### MCP2515 CAN bus module -See [Wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki/Huawei-AC-PSU) for details. +Used to connect to the Huawei PSU (AC charger). See [the Wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki/Huawei-AC-PSU) for details. ### Relay module @@ -71,30 +87,8 @@ Use a power supply with 5 V and 1 A. The USB cable connected to your PC/Notebook ### Change pin assignment -Its possible to change all the pins of the NRF24L01+ module, the Display, the LED etc. -The recommend way to change the pin assignment is by creating a custom [device profile](DeviceProfiles.md). -It is also possible to create a custom environment and compile the source yourself. This can be achieved by copying one of the [env:....] sections from 'platformio.ini' to 'platformio_override.ini' and editing the 'platformio_override.ini' file and add/change one or more of the following lines to the 'build_flags' parameter: - -```makefile --DHOYMILES_PIN_MISO=19 --DHOYMILES_PIN_MOSI=23 --DHOYMILES_PIN_SCLK=18 --DHOYMILES_PIN_IRQ=16 --DHOYMILES_PIN_CE=4 --DHOYMILES_PIN_CS=5 --DVICTRON_PIN_TX=21 --DVICTRON_PIN_RX=22 --DPYLONTECH_PIN_RX=27 --DPYLONTECH_PIN_TX=14 --DHUAWEI_PIN_MISO=12 --DHUAWEI_PIN_MOSI=13 --DHUAWEI_PIN_SCLK=26 --DHUAWEI_PIN_IRQ=25 --DHUAWEI_PIN_CS=15 --DHUAWEI_PIN_POWER=33 -``` - -It is recommended to make all changes only in the 'platformio_override.ini', this is your personal copy. +It is possible to change all the pins of the NRF24L01+ module, the Display, the LED etc. +The way to change the pin assignment is by creating a custom [device profile](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki/Device-Profiles). ## Flashing and starting up diff --git a/include/Battery.h b/include/Battery.h index 700c46c4..5130381c 100644 --- a/include/Battery.h +++ b/include/Battery.h @@ -14,7 +14,6 @@ public: virtual void deinit() = 0; virtual void loop() = 0; virtual std::shared_ptr getStats() const = 0; - virtual bool usesHwPort2() const { return false; } }; class BatteryClass { diff --git a/include/BatteryStats.h b/include/BatteryStats.h index ca669aa8..0f86c7c5 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -58,6 +58,8 @@ class BatteryStats { } String _manufacturer = "unknown"; + String _hwversion = ""; + String _fwversion = ""; uint32_t _lastUpdate = 0; private: @@ -157,8 +159,9 @@ class VictronSmartShuntStats : public BatteryStats { uint32_t _timeToGo; float _chargedEnergy; float _dischargedEnergy; - String _modelName; int32_t _instantaneousPower; + float _midpointVoltage; + float _midpointDeviation; float _consumedAmpHours; int32_t _lastFullCharge; diff --git a/include/JkBmsController.h b/include/JkBmsController.h index bbc5a5ac..7156d760 100644 --- a/include/JkBmsController.h +++ b/include/JkBmsController.h @@ -7,6 +7,8 @@ #include "Battery.h" #include "JkBmsSerialMessage.h" +//#define JKBMS_DUMMY_SERIAL + class DataPointContainer; namespace JkBms { @@ -19,11 +21,16 @@ class Controller : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() const final { - return ARDUINO_USB_CDC_ON_BOOT != 1; - } private: + static char constexpr _serialPortOwner[] = "JK BMS"; + +#ifdef JKBMS_DUMMY_SERIAL + std::unique_ptr _upSerial; +#else + std::unique_ptr _upSerial; +#endif + enum class Status : unsigned { Initializing, Timeout, diff --git a/include/PinMapping.h b/include/PinMapping.h index f6db7a2d..75b27294 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -45,6 +45,8 @@ struct PinMapping_t { int8_t victron_rx; int8_t victron_tx2; int8_t victron_rx2; + int8_t victron_tx3; + int8_t victron_rx3; int8_t battery_rx; int8_t battery_rxen; int8_t battery_tx; diff --git a/include/PowerMeter.h b/include/PowerMeter.h index 0ce38f61..5b3d8f31 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -60,6 +60,8 @@ private: mutable std::mutex _mutex; + static char constexpr _sdmSerialPortOwner[] = "SDM power meter"; + std::unique_ptr _upSdmSerial = nullptr; std::unique_ptr _upSdm = nullptr; std::unique_ptr _upSmlSerial = nullptr; diff --git a/include/SerialPortManager.h b/include/SerialPortManager.h index 4d4c2dab..372b9c50 100644 --- a/include/SerialPortManager.h +++ b/include/SerialPortManager.h @@ -1,27 +1,21 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include +#include +#include +#include class SerialPortManagerClass { public: - bool allocateMpptPort(int port); - bool allocateBatteryPort(int port); - void invalidateBatteryPort(); - void invalidateMpptPorts(); + void init(); + + std::optional allocatePort(std::string const& owner); + void freePort(std::string const& owner); 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); + // the amount of hardare UARTs available on supported ESP32 chips + static size_t constexpr _num_controllers = 3; + std::array _ports = { "" }; }; extern SerialPortManagerClass SerialPortManager; diff --git a/include/VictronMppt.h b/include/VictronMppt.h index a4f1b1ab..963a46cd 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -55,7 +55,9 @@ private: using controller_t = std::unique_ptr; std::vector _controllers; - bool initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort); + std::vector _serialPortOwners; + bool initController(int8_t rx, int8_t tx, bool logging, + uint8_t instance); }; extern VictronMpptClass VictronMppt; diff --git a/include/VictronSmartShunt.h b/include/VictronSmartShunt.h index 97b42132..10bf1c52 100644 --- a/include/VictronSmartShunt.h +++ b/include/VictronSmartShunt.h @@ -6,14 +6,13 @@ class VictronSmartShunt : public BatteryProvider { public: bool init(bool verboseLogging) final; - void deinit() final { } + void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() const final { - return ARDUINO_USB_CDC_ON_BOOT != 1; - } private: + static char constexpr _serialPortOwner[] = "SmartShunt"; + uint32_t _lastUpdate = 0; std::shared_ptr _stats = std::make_shared(); diff --git a/lib/VeDirectFrameHandler/VeDirectData.cpp b/lib/VeDirectFrameHandler/VeDirectData.cpp index d28f9f78..a67175fd 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.cpp +++ b/lib/VeDirectFrameHandler/VeDirectData.cpp @@ -137,6 +137,51 @@ frozen::string const& veStruct::getPidAsString() const return getAsString(values, productID_PID); } +/* + * This function returns the firmware version as an integer, disregarding + * release candidate marks. + */ +uint32_t veStruct::getFwVersionAsInteger() const +{ + char const* strVersion = firmwareVer_FW; + + // VE.Direct protocol manual states that the first char can be a non-digit, + // in which case that char represents a release candidate version + if (strVersion[0] < '0' || strVersion[0] > '9') { ++strVersion; } + + return static_cast(strtoul(strVersion, nullptr, 10)); +} + +/* + * This function returns the firmware version as readable text. + */ +String veStruct::getFwVersionFormatted() const +{ + char const* strVersion = firmwareVer_FW; + char rc = 0; + + // VE.Direct protocol manual states that the first char can be a non-digit, + // in which case that char represents a release candidate version + if (strVersion[0] < '0' || strVersion[0] > '9') { + rc = strVersion[0]; + ++strVersion; + } + + // SmartShunt firmware version is transmitted with leading zero(es) + while (strVersion[0] == '0') { ++strVersion; } + + String res(strVersion[0]); + res += "."; + res += strVersion + 1; + + if (rc != 0) { + res += "-rc-"; + res += rc; + } + + return res; +} + /* * This function returns the state of operations (CS) as readable text. */ diff --git a/lib/VeDirectFrameHandler/VeDirectData.h b/lib/VeDirectFrameHandler/VeDirectData.h index 86be497f..34c6b899 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.h +++ b/lib/VeDirectFrameHandler/VeDirectData.h @@ -2,6 +2,7 @@ #include #include +#include #define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0 #define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer @@ -9,12 +10,14 @@ typedef struct { 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 + char firmwareVer_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 + uint32_t getFwVersionAsInteger() const; + String getFwVersionFormatted() const; } veStruct; struct veMpptStruct : veStruct { @@ -79,6 +82,8 @@ struct veShuntStruct : veStruct { int32_t H16; // Maximum auxiliary (battery) voltage int32_t H17; // Amount of discharged energy int32_t H18; // Amount of charged energy + int32_t VM; // Mid-point voltage of the battery bank + int32_t DM; // Mid-point deviation of the battery bank int8_t dcMonitorMode_MON; // DC monitor mode }; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 44435dac..4b258e75 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -62,7 +62,8 @@ VeDirectFrameHandler::VeDirectFrameHandler() : } template -void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) +void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, + Print* msgOut, bool verboseLogging, uint8_t hwSerialPort) { _vedirectSerial = std::make_unique(hwSerialPort); _vedirectSerial->end(); // make sure the UART will be re-initialized @@ -242,12 +243,12 @@ void VeDirectFrameHandler::processTextData(std::string const& name, std::stri } if (name == "SER") { - strcpy(_tmpFrame.serialNr_SER, value.c_str()); + strncpy(_tmpFrame.serialNr_SER, value.c_str(), sizeof(_tmpFrame.serialNr_SER)); return; } if (name == "FW") { - strcpy(_tmpFrame.firmwareNr_FW, value.c_str()); + strncpy(_tmpFrame.firmwareVer_FW, value.c_str(), sizeof(_tmpFrame.firmwareVer_FW)); return; } diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 1c482920..244caf3f 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -30,7 +30,8 @@ public: protected: VeDirectFrameHandler(); - void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); + 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 bool _verboseLogging; diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index f1bd00de..e0f386db 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -12,9 +12,11 @@ //#define PROCESS_NETWORK_STATE -void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) +void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, + bool verboseLogging, uint8_t hwSerialPort) { - VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, verboseLogging, hwSerialPort); + VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, + verboseLogging, hwSerialPort); } bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value) @@ -110,7 +112,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.firmwareNr_FW) < 153) { return; } + if (_tmpFrame.getFwVersionAsInteger() < 153) { return; } using Command = VeDirectHexCommand; using Register = VeDirectHexRegister; diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index 59598898..b8bd4c72 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -40,7 +40,8 @@ class VeDirectMpptController : public VeDirectFrameHandler { public: VeDirectMpptController() = default; - void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); + void init(int8_t rx, int8_t tx, Print* msgOut, + bool verboseLogging, uint8_t hwSerialPort); using data_t = veMpptStruct; diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp index 2d8b85a7..c16b6a07 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -3,10 +3,11 @@ VeDirectShuntController VeDirectShunt; -void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) +void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, + bool verboseLogging, uint8_t hwSerialPort) { - VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging, - ((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0)); + VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, + verboseLogging, hwSerialPort); } bool VeDirectShuntController::processTextDataDerived(std::string const& name, std::string const& value) @@ -108,6 +109,14 @@ bool VeDirectShuntController::processTextDataDerived(std::string const& name, st _tmpFrame.H17 = atoi(value.c_str()); return true; } + if (name == "VM") { + _tmpFrame.VM = atoi(value.c_str()); + return true; + } + if (name == "DM") { + _tmpFrame.DM = atoi(value.c_str()); + return true; + } if (name == "H18") { _tmpFrame.H18 = atoi(value.c_str()); return true; diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.h b/lib/VeDirectFrameHandler/VeDirectShuntController.h index 03bc96b8..d7bc2513 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.h +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.h @@ -8,7 +8,8 @@ class VeDirectShuntController : public VeDirectFrameHandler { public: VeDirectShuntController() = 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, uint8_t hwSerialPort); using data_t = veShuntStruct; diff --git a/pio-scripts/create_factory_bin.py b/pio-scripts/create_factory_bin.py index 56f71c4b..d394998b 100644 --- a/pio-scripts/create_factory_bin.py +++ b/pio-scripts/create_factory_bin.py @@ -21,7 +21,7 @@ Import("env") platform = env.PioPlatform() import sys -from os.path import join +from os.path import join, getsize sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) import esptool @@ -60,6 +60,14 @@ def esp32_create_combined_bin(source, target, env): flash_size, ] + # platformio estimates the amount of flash used to store the firmware. this + # estimate is not accurate. we perform a final check on the firmware bin + # size by comparing it against the respective partition size. + max_size = env.BoardConfig().get("upload.maximum_size", 1) + fw_size = getsize(firmware_name) + if (fw_size > max_size): + raise Exception("firmware binary too large: %d > %d" % (fw_size, max_size)) + print(" Offset | File") for section in sections: sect_adr, sect_file = section.split(" ", 1) diff --git a/src/Battery.cpp b/src/Battery.cpp index de05b03d..794afa51 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -5,7 +5,6 @@ #include "JkBmsController.h" #include "VictronSmartShunt.h" #include "MqttBattery.h" -#include "SerialPortManager.h" BatteryClass Battery; @@ -39,7 +38,6 @@ void BatteryClass::updateSettings() _upProvider->deinit(); _upProvider = nullptr; } - SerialPortManager.invalidateBatteryPort(); CONFIG_T& config = Configuration.get(); if (!config.Battery.Enabled) { return; } @@ -60,22 +58,11 @@ void BatteryClass::updateSettings() _upProvider = std::make_unique(); break; default: - MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider); + MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider); return; } - if(_upProvider->usesHwPort2()) { - if (!SerialPortManager.allocateBatteryPort(2)) { - MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2); - _upProvider = nullptr; - return; - } - } - - if (!_upProvider->init(verboseLogging)) { - SerialPortManager.invalidateBatteryPort(); - _upProvider = nullptr; - } + if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } } void BatteryClass::loop() diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 8992efab..61f23823 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -62,6 +62,12 @@ bool BatteryStats::updateAvailable(uint32_t since) const void BatteryStats::getLiveViewData(JsonVariant& root) const { root["manufacturer"] = _manufacturer; + if (!_fwversion.isEmpty()) { + root["fwversion"] = _fwversion; + } + if (!_hwversion.isEmpty()) { + root["hwversion"] = _hwversion; + } root["data_age"] = getAgeSeconds(); addLiveViewValue(root, "SoC", _soc, "%", _socPrecision); @@ -375,22 +381,42 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) _cellVoltageTimestamp = millis(); } + auto oVersion = _dataPoints.get(); + if (oVersion.has_value()) { + // raw: "11.XW_S11.262H_" + // => Hardware "V11.XW" (displayed in Android app) + // => Software "V11.262H" (displayed in Android app) + auto first = oVersion->find('_'); + if (first != std::string::npos) { + _hwversion = oVersion->substr(0, first).c_str(); + + auto second = oVersion->find('_', first + 1); + + // the 'S' seems to be merely an indicator for "software"? + if (oVersion->at(first + 1) == 'S') { first++; } + + _fwversion = oVersion->substr(first + 1, second - first - 1).c_str(); + } + } + _lastUpdate = millis(); } void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) { BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis()); BatteryStats::setSoC(static_cast(shuntData.SOC) / 10, 1/*precision*/, millis()); + _fwversion = shuntData.getFwVersionFormatted(); _current = static_cast(shuntData.batteryCurrent_I_mA) / 1000; - _modelName = shuntData.getPidAsString().data(); _chargeCycles = shuntData.H4; _timeToGo = shuntData.TTG / 60; _chargedEnergy = static_cast(shuntData.H18) / 100; _dischargedEnergy = static_cast(shuntData.H17) / 100; - _manufacturer = "Victron " + _modelName; + _manufacturer = String("Victron ") + shuntData.getPidAsString().data(); _temperature = shuntData.T; _tempPresent = shuntData.tempPresent; + _midpointVoltage = static_cast(shuntData.VM) / 1000; + _midpointDeviation = static_cast(shuntData.DM) / 10; _instantaneousPower = shuntData.P; _consumedAmpHours = static_cast(shuntData.CE) / 1000; _lastFullCharge = shuntData.H9 / 60; @@ -414,6 +440,8 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 2); addLiveViewValue(root, "instantaneousPower", _instantaneousPower, "W", 0); addLiveViewValue(root, "consumedAmpHours", _consumedAmpHours, "Ah", 3); + addLiveViewValue(root, "midpointVoltage", _midpointVoltage, "V", 2); + addLiveViewValue(root, "midpointDeviation", _midpointDeviation, "%", 1); addLiveViewValue(root, "lastFullCharge", _lastFullCharge, "min", 0); if (_tempPresent) { addLiveViewValue(root, "temperature", _temperature, "°C", 0); @@ -436,4 +464,6 @@ void VictronSmartShuntStats::mqttPublish() const { MqttSettings.publish("battery/instantaneousPower", String(_instantaneousPower)); MqttSettings.publish("battery/consumedAmpHours", String(_consumedAmpHours)); MqttSettings.publish("battery/lastFullCharge", String(_lastFullCharge)); + MqttSettings.publish("battery/midpointVoltage", String(_midpointVoltage)); + MqttSettings.publish("battery/midpointDeviation", String(_midpointDeviation)); } diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index a288981a..20b01388 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -389,8 +389,10 @@ void HuaweiCanClass::loop() MessageOutput.printf("[HuaweiCanClass::loop] newPowerLimit: %f, output_power: %f \r\n", newPowerLimit, _rp.output_power); } + // Check whether the battery SoC limit setting is enabled if (config.Battery.Enabled && config.Huawei.Auto_Power_BatterySoC_Limits_Enabled) { uint8_t _batterySoC = Battery.getStats()->getSoC(); + // Sets power limit to 0 if the BMS reported SoC reaches or exceeds the user configured value if (_batterySoC >= config.Huawei.Auto_Power_Stop_BatterySoC_Threshold) { newPowerLimit = 0; if (verboseLogging) { @@ -427,7 +429,7 @@ void HuaweiCanClass::loop() float calculatedCurrent = efficiency * (newPowerLimit / _rp.output_voltage); // Limit output current to value requested by BMS - float permissableCurrent = stats->getChargeCurrentLimitation() - (stats->getChargeCurrent() - _rp.output_current); // BMS current limit - current from other sources + float permissableCurrent = stats->getChargeCurrentLimitation() - (stats->getChargeCurrent() - _rp.output_current); // BMS current limit - current from other sources, e.g. Victron MPPT charger float outputCurrent = std::min(calculatedCurrent, permissableCurrent); outputCurrent= outputCurrent > 0 ? outputCurrent : 0; diff --git a/src/JkBmsController.cpp b/src/JkBmsController.cpp index 94f80cb0..c39664e0 100644 --- a/src/JkBmsController.cpp +++ b/src/JkBmsController.cpp @@ -5,9 +5,10 @@ #include "MessageOutput.h" #include "JkBmsDataPoints.h" #include "JkBmsController.h" +#include "SerialPortManager.h" #include -//#define JKBMS_DUMMY_SERIAL +namespace JkBms { #ifdef JKBMS_DUMMY_SERIAL class DummySerial { @@ -196,13 +197,8 @@ class DummySerial { size_t _msg_idx = 0; size_t _byte_idx = 0; }; -DummySerial HwSerial; -#else -HardwareSerial HwSerial((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0); #endif -namespace JkBms { - bool Controller::init(bool verboseLogging) { _verboseLogging = verboseLogging; @@ -220,9 +216,18 @@ bool Controller::init(bool verboseLogging) return false; } - HwSerial.end(); // make sure the UART will be re-initialized - HwSerial.begin(115200, SERIAL_8N1, pin.battery_rx, pin.battery_tx); - HwSerial.flush(); +#ifdef JKBMS_DUMMY_SERIAL + _upSerial = std::make_unique(); +#else + auto oHwSerialPort = SerialPortManager.allocatePort(_serialPortOwner); + if (!oHwSerialPort) { return false; } + + _upSerial = std::make_unique(*oHwSerialPort); +#endif + + _upSerial->end(); // make sure the UART will be re-initialized + _upSerial->begin(115200, SERIAL_8N1, pin.battery_rx, pin.battery_tx); + _upSerial->flush(); if (Interface::Transceiver != getInterface()) { return true; } @@ -242,10 +247,12 @@ bool Controller::init(bool verboseLogging) void Controller::deinit() { - HwSerial.end(); + _upSerial->end(); if (_rxEnablePin > 0) { pinMode(_rxEnablePin, INPUT); } if (_txEnablePin > 0) { pinMode(_txEnablePin, INPUT); } + + SerialPortManager.freePort(_serialPortOwner); } Controller::Interface Controller::getInterface() const @@ -296,7 +303,7 @@ void Controller::sendRequest(uint8_t pollInterval) return announceStatus(Status::WaitingForPollInterval); } - if (!HwSerial.availableForWrite()) { + if (!_upSerial->availableForWrite()) { return announceStatus(Status::HwSerialNotAvailableForWrite); } @@ -307,10 +314,10 @@ void Controller::sendRequest(uint8_t pollInterval) digitalWrite(_txEnablePin, HIGH); // enable transmission } - HwSerial.write(readAll.data(), readAll.size()); + _upSerial->write(readAll.data(), readAll.size()); if (Interface::Transceiver == getInterface()) { - HwSerial.flush(); + _upSerial->flush(); digitalWrite(_rxEnablePin, LOW); // enable reception digitalWrite(_txEnablePin, LOW); // disable transmission (free the bus) } @@ -326,8 +333,8 @@ void Controller::loop() CONFIG_T& config = Configuration.get(); uint8_t pollInterval = config.Battery.JkBmsPollingInterval; - while (HwSerial.available()) { - rxData(HwSerial.read()); + while (_upSerial->available()) { + rxData(_upSerial->read()); } sendRequest(pollInterval); diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index 1b31d5ad..678b6b52 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -92,10 +92,10 @@ void MqttHandleVedirectHassClass::publishConfig() publishSensor("VE.Smart network total DC input power", "mdi:solar-power", "NetworkTotalDcInputPower", "power", "measurement", "W", *optMpptData); } if (optMpptData->MpptTemperatureMilliCelsius.first != 0) { - publishSensor("MPPT temperature", "mdi:temperature-celsius", "MpptTemperature", "temperature", "measurement", "W", *optMpptData); + publishSensor("MPPT temperature", "mdi:temperature-celsius", "MpptTemperature", "temperature", "measurement", "°C", *optMpptData); } if (optMpptData->SmartBatterySenseTemperatureMilliCelsius.first != 0) { - publishSensor("Smart Battery Sense temperature", "mdi:temperature-celsius", "SmartBatterySenseTemperature", "temperature", "measurement", "W", *optMpptData); + publishSensor("Smart Battery Sense temperature", "mdi:temperature-celsius", "SmartBatterySenseTemperature", "temperature", "measurement", "°C", *optMpptData); } } diff --git a/src/MqttHandleBatteryHass.cpp b/src/MqttHandleBatteryHass.cpp index 2659b412..c91b855a 100644 --- a/src/MqttHandleBatteryHass.cpp +++ b/src/MqttHandleBatteryHass.cpp @@ -87,6 +87,8 @@ void MqttHandleBatteryHassClass::loop() publishSensor("Current", "mdi:current-dc", "BatteryCurrentMilliAmps", "current", "measurement", "mA"); publishSensor("BMS Temperature", "mdi:thermometer", "BmsTempCelsius", "temperature", "measurement", "°C"); publishSensor("Cell Voltage Diff", "mdi:battery-alert", "CellDiffMilliVolt", "voltage", "measurement", "mV"); + publishSensor("Battery Temperature 1", "mdi:thermometer", "BatteryTempOneCelsius", "temperature", "measurement", "°C"); + publishSensor("Battery Temperature 2", "mdi:thermometer", "BatteryTempTwoCelsius", "temperature", "measurement", "°C"); publishSensor("Charge Cycles", "mdi:counter", "BatteryCycles"); publishSensor("Cycle Capacity", "mdi:battery-sync", "BatteryCycleCapacity"); @@ -120,6 +122,8 @@ void MqttHandleBatteryHassClass::loop() publishSensor("Charge Cycles", "mdi:counter", "chargeCycles"); publishSensor("Consumed Amp Hours", NULL, "consumedAmpHours", NULL, "measurement", "Ah"); publishSensor("Last Full Charge", "mdi:timelapse", "lastFullCharge", NULL, NULL, "min"); + publishSensor("Midpoint Voltage", NULL, "midpointVoltage", "voltage", "measurement", "V"); + publishSensor("Midpoint Deviation", NULL, "midpointDeviation", "battery", "measurement", "%"); break; } @@ -202,7 +206,7 @@ void MqttHandleBatteryHassClass::publishBinarySensor(const char* caption, const statTopic.concat(subTopic); JsonDocument root; - + root["name"] = caption; root["uniq_id"] = serial + "_" + sensorId; root["stat_t"] = statTopic; diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index 0ed47e87..c244afc6 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -110,7 +110,7 @@ void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::da PUBLISH(productID_PID, "PID", currentData.getPidAsString().data()); PUBLISH(serialNr_SER, "SER", currentData.serialNr_SER); - PUBLISH(firmwareNr_FW, "FW", currentData.firmwareNr_FW); + PUBLISH(firmwareVer_FW, "FW", currentData.firmwareVer_FW); PUBLISH(loadOutputState_LOAD, "LOAD", (currentData.loadOutputState_LOAD ? "ON" : "OFF")); PUBLISH(currentState_CS, "CS", currentData.getCsAsString().data()); PUBLISH(errorCode_ERR, "ERR", currentData.getErrAsString().data()); diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index a3c88886..0c4a0984 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -100,6 +100,14 @@ #define VICTRON_PIN_RX2 -1 #endif +#ifndef VICTRON_PIN_TX3 +#define VICTRON_PIN_TX3 -1 +#endif + +#ifndef VICTRON_PIN_RX3 +#define VICTRON_PIN_RX3 -1 +#endif + #ifndef BATTERY_PIN_RX #define BATTERY_PIN_RX -1 #endif @@ -207,8 +215,11 @@ PinMappingClass::PinMappingClass() _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.victron_rx2 = VICTRON_PIN_RX2; + _pinMapping.victron_tx2 = VICTRON_PIN_TX2; + + _pinMapping.victron_rx3 = VICTRON_PIN_RX3; + _pinMapping.victron_tx3 = VICTRON_PIN_TX3; _pinMapping.battery_rx = BATTERY_PIN_RX; _pinMapping.battery_rxen = BATTERY_PIN_RXEN; @@ -292,6 +303,8 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX; _pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX2; _pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX2; + _pinMapping.victron_rx3 = doc[i]["victron"]["rx3"] | VICTRON_PIN_RX3; + _pinMapping.victron_tx3 = doc[i]["victron"]["tx3"] | VICTRON_PIN_TX3; _pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX; _pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN; diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index 63cce421..56582d00 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -9,6 +9,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "MessageOutput.h" +#include "SerialPortManager.h" #include #include @@ -57,16 +58,22 @@ void PowerMeterClass::init(Scheduler& scheduler) } case Source::SDM1PH: - case Source::SDM3PH: + case Source::SDM3PH: { if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) { MessageOutput.println("[PowerMeter] invalid pin config for SDM power meter (RX and TX pins must be defined)"); return; } - _upSdm = std::make_unique(Serial2, 9600, pin.powermeter_dere, + auto oHwSerialPort = SerialPortManager.allocatePort(_sdmSerialPortOwner); + if (!oHwSerialPort) { return; } + + _upSdmSerial = std::make_unique(*oHwSerialPort); + _upSdmSerial->end(); // make sure the UART will be re-initialized + _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_dere, SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); _upSdm->begin(); break; + } case Source::HTTP: HttpPowerMeter.init(); diff --git a/src/SerialPortManager.cpp b/src/SerialPortManager.cpp index c4eb8514..fbcc7f03 100644 --- a/src/SerialPortManager.cpp +++ b/src/SerialPortManager.cpp @@ -2,59 +2,46 @@ #include "SerialPortManager.h" #include "MessageOutput.h" -#define MAX_CONTROLLERS 3 - SerialPortManagerClass SerialPortManager; -bool SerialPortManagerClass::allocateBatteryPort(int port) +void SerialPortManagerClass::init() { - return allocatePort(port, Owner::BATTERY); -} - -bool SerialPortManagerClass::allocateMpptPort(int port) -{ - return allocatePort(port, Owner::MPPT); -} - -bool SerialPortManagerClass::allocatePort(uint8_t port, Owner owner) -{ - if (port >= MAX_CONTROLLERS) { - MessageOutput.printf("[SerialPortManager] Invalid serial port = %d \r\n", port); - return false; + if (ARDUINO_USB_CDC_ON_BOOT != 1) { + _ports[0] = "Serial Console"; + MessageOutput.println("[SerialPortManager] HW UART port 0 now in use " + "by 'Serial Console'"); } - - return allocatedPorts.insert({port, owner}).second; } -void SerialPortManagerClass::invalidateBatteryPort() +std::optional SerialPortManagerClass::allocatePort(std::string const& owner) { - invalidate(Owner::BATTERY); -} - -void SerialPortManagerClass::invalidateMpptPorts() -{ - invalidate(Owner::MPPT); -} - -void SerialPortManagerClass::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; + for (size_t i = 0; i < _ports.size(); ++i) { + if (_ports[i] != "") { + MessageOutput.printf("[SerialPortManager] HW UART %d already " + "in use by '%s'\r\n", i, _ports[i].c_str()); + continue; } + + _ports[i] = owner; + + MessageOutput.printf("[SerialPortManager] HW UART %d now in use " + "by '%s'\r\n", i, owner.c_str()); + + return i; } + + MessageOutput.printf("[SerialPortManager] Cannot assign another HW " + "UART port to '%s'\r\n", owner.c_str()); + return std::nullopt; } -const char* SerialPortManagerClass::print(Owner owner) +void SerialPortManagerClass::freePort(std::string const& owner) { - switch (owner) { - case BATTERY: - return "BATTERY"; - case MPPT: - return "MPPT"; + for (size_t i = 0; i < _ports.size(); ++i) { + if (_ports[i] != owner) { continue; } + + MessageOutput.printf("[SerialPortManager] Freeing HW UART %d, owner " + "was '%s'\r\n", i, owner.c_str()); + _ports[i] = ""; } - return "unknown"; } diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index 770be014..bc0122d4 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -22,39 +22,46 @@ void VictronMpptClass::updateSettings() std::lock_guard lock(_mutex); _controllers.clear(); - SerialPortManager.invalidateMpptPorts(); + for (auto const& o: _serialPortOwners) { + SerialPortManager.freePort(o.c_str()); + } + _serialPortOwners.clear(); CONFIG_T& config = Configuration.get(); if (!config.Vedirect.Enabled) { return; } const PinMapping_t& pin = PinMapping.get(); - int hwSerialPort = 1; - bool initSuccess = initController(pin.victron_rx, pin.victron_tx, config.Vedirect.VerboseLogging, hwSerialPort); - if (initSuccess) { - hwSerialPort++; - } + initController(pin.victron_rx, pin.victron_tx, + config.Vedirect.VerboseLogging, 1); - initController(pin.victron_rx2, pin.victron_tx2, config.Vedirect.VerboseLogging, hwSerialPort); + initController(pin.victron_rx2, pin.victron_tx2, + config.Vedirect.VerboseLogging, 2); + + initController(pin.victron_rx3, pin.victron_tx3, + config.Vedirect.VerboseLogging, 3); } -bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort) +bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, + uint8_t instance) { - MessageOutput.printf("[VictronMppt] rx = %d, tx = %d, hwSerialPort = %d\r\n", rx, tx, hwSerialPort); + MessageOutput.printf("[VictronMppt Instance %d] rx = %d, tx = %d\r\n", + instance, rx, tx); if (rx < 0) { - MessageOutput.printf("[VictronMppt] invalid pin config rx = %d, tx = %d\r\n", rx, tx); + MessageOutput.printf("[VictronMppt Instance %d] invalid pin config\r\n", instance); return false; } - if (!SerialPortManager.allocateMpptPort(hwSerialPort)) { - MessageOutput.printf("[VictronMppt] Serial port %d already in use. Initialization aborted!\r\n", - hwSerialPort); - return false; - } + String owner("Victron MPPT "); + owner += String(instance); + auto oHwSerialPort = SerialPortManager.allocatePort(owner.c_str()); + if (!oHwSerialPort) { return false; } + + _serialPortOwners.push_back(owner); auto upController = std::make_unique(); - upController->init(rx, tx, &MessageOutput, logging, hwSerialPort); + upController->init(rx, tx, &MessageOutput, logging, *oHwSerialPort); _controllers.push_back(std::move(upController)); return true; } diff --git a/src/VictronSmartShunt.cpp b/src/VictronSmartShunt.cpp index e3da07fa..c64f9a83 100644 --- a/src/VictronSmartShunt.cpp +++ b/src/VictronSmartShunt.cpp @@ -3,7 +3,12 @@ #include "Configuration.h" #include "PinMapping.h" #include "MessageOutput.h" +#include "SerialPortManager.h" +void VictronSmartShunt::deinit() +{ + SerialPortManager.freePort(_serialPortOwner); +} bool VictronSmartShunt::init(bool verboseLogging) { @@ -21,7 +26,10 @@ bool VictronSmartShunt::init(bool verboseLogging) auto tx = static_cast(pin.battery_tx); auto rx = static_cast(pin.battery_rx); - VeDirectShunt.init(rx, tx, &MessageOutput, verboseLogging); + auto oHwSerialPort = SerialPortManager.allocatePort(_serialPortOwner); + if (!oHwSerialPort) { return false; } + + VeDirectShunt.init(rx, tx, &MessageOutput, verboseLogging, *oHwSerialPort); return true; } diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index e6c3170a..9bed5e53 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -91,6 +91,8 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) victronPinObj["tx"] = pin.victron_tx; victronPinObj["rx2"] = pin.victron_rx2; victronPinObj["tx2"] = pin.victron_tx2; + victronPinObj["rx3"] = pin.victron_rx3; + victronPinObj["tx3"] = pin.victron_tx3; auto batteryPinObj = curPin["battery"].to(); batteryPinObj["rx"] = pin.battery_rx; diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index 5e06d0be..6d07f2fb 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -48,6 +48,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["chiprevision"] = ESP.getChipRevision(); root["chipmodel"] = ESP.getChipModel(); root["chipcores"] = ESP.getChipCores(); + root["flashsize"] = ESP.getFlashChipSize(); String reason; reason = ResetReason::get_reset_reason_verbose(0); diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 9a92b363..abf3376e 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -139,7 +139,7 @@ void WebApiWsVedirectLiveClass::generateCommonJsonResponse(JsonVariant& root, bo void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) { root["product_id"] = mpptData.getPidAsString(); - root["firmware_version"] = String(mpptData.firmwareNr_FW); + root["firmware_version"] = mpptData.getFwVersionFormatted(); const JsonObject values = root["values"].to(); diff --git a/src/main.cpp b/src/main.cpp index e0a54c15..c23087aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include "InverterSettings.h" #include "Led_Single.h" #include "MessageOutput.h" +#include "SerialPortManager.h" #include "VictronMppt.h" #include "Battery.h" #include "Huawei_can.h" @@ -96,6 +97,8 @@ void setup() const auto& pin = PinMapping.get(); MessageOutput.println("done"); + SerialPortManager.init(); + // Initialize WiFi MessageOutput.print("Initialize Network... "); NetworkSettings.init(scheduler); diff --git a/webapp/index.html b/webapp/index.html index 47122be1..bbb68b54 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -7,7 +7,7 @@ - OpenDTU-onBattery + OpenDTU-OnBattery