diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5e73566e..8f357930 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -47,7 +47,8 @@ body: label: Install Method description: How did you install OpenDTU? options: - - Pre-Compiled binary from GitHub + - Pre-Compiled binary from GitHub releases + - Pre-Compiled binary from GitHub actions/pull-request - Self-Compiled validations: required: true @@ -59,6 +60,14 @@ body: placeholder: "e.g. 359d513" validations: required: true + - type: input + id: environment + attributes: + label: What firmware variant (PIO Environment) are you using? + description: You can find this in by going to Info -> System + placeholder: "generic_esp32s3_usb" + validations: + required: true - type: textarea id: logs attributes: @@ -84,5 +93,5 @@ body: required: true - label: I have updated the title field above with a concise description. required: true - - label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported + - label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported. required: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32bbad64..29a30d9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: environments: ${{ steps.envs.outputs.environments }} build: - name: Build Enviornments + name: Build Environments runs-on: ubuntu-latest needs: get_default_envs strategy: @@ -79,18 +79,27 @@ jobs: python -m pip install --upgrade pip pip install --upgrade platformio setuptools + - name: Enable Corepack + run: | + cd webapp + corepack enable + - name: Setup Node.js and yarn uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" cache: "yarn" cache-dependency-path: "webapp/yarn.lock" - name: Install WebApp dependencies - run: yarn --cwd webapp install --frozen-lockfile + run: | + cd webapp + yarn install --frozen-lockfile - name: Build WebApp - run: yarn --cwd webapp build + run: | + cd webapp + yarn build - name: Build firmware run: pio run -e ${{ matrix.environment }} diff --git a/.github/workflows/config/release-notes-config.json b/.github/workflows/config/release-notes-config.json index 3fa9e3bd..8699820e 100644 --- a/.github/workflows/config/release-notes-config.json +++ b/.github/workflows/config/release-notes-config.json @@ -18,6 +18,12 @@ "fix" ] }, + { + "title": "## 🌎 Web Application", + "labels": [ + "webapp" + ] + }, { "title": "## 📚 Documentation", "labels": [ diff --git a/.github/workflows/cpplint.yml b/.github/workflows/cpplint.yml index 4ee4b4a8..84d9fd06 100644 --- a/.github/workflows/cpplint.yml +++ b/.github/workflows/cpplint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/yarnlint.yml b/.github/workflows/yarnlint.yml index f1c912c9..dd438a05 100644 --- a/.github/workflows/yarnlint.yml +++ b/.github/workflows/yarnlint.yml @@ -6,17 +6,23 @@ jobs: build: runs-on: ubuntu-latest + defaults: + run: + working-directory: webapp + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Enable Corepack + run: corepack enable - name: Setup Node.js and yarn - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: "18" + node-version: "22" cache: "yarn" cache-dependency-path: "webapp/yarn.lock" - name: Install WebApp dependencies - run: yarn --cwd webapp install --frozen-lockfile + run: yarn install --frozen-lockfile - name: Linting - run: yarn --cwd webapp lint \ No newline at end of file + run: yarn lint diff --git a/.github/workflows/yarnprettier.yml b/.github/workflows/yarnprettier.yml new file mode 100644 index 00000000..c521f88d --- /dev/null +++ b/.github/workflows/yarnprettier.yml @@ -0,0 +1,28 @@ +name: Yarn Prettier + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: webapp + + steps: + - uses: actions/checkout@v4 + - name: Enable Corepack + run: corepack enable + - name: Setup Node.js and yarn + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "yarn" + cache-dependency-path: "webapp/yarn.lock" + + - name: Install WebApp dependencies + run: yarn install --frozen-lockfile + + - name: Check Formatting + run: yarn prettier --check src/ diff --git a/README.md b/README.md index cb9f4ef4..c61c8e60 100644 --- a/README.md +++ b/README.md @@ -40,41 +40,4 @@ Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | gre ## Currently supported Inverters -| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases | -| ---------------------| ------------------ | --------- | ----------- | --------- | -| Hoymiles HM-300-1T | NRF24L01+ | 1 | 1 | 1 | -| Hoymiles HM-350-1T | NRF24L01+ | 1 | 1 | 1 | -| Hoymiles HM-400-1T | NRF24L01+ | 1 | 1 | 1 | -| Hoymiles HM-600-2T | NRF24L01+ | 2 | 2 | 1 | -| Hoymiles HM-700-2T | NRF24L01+ | 2 | 2 | 1 | -| Hoymiles HM-800-2T | NRF24L01+ | 2 | 2 | 1 | -| Hoymiles HM-1000-4T | NRF24L01+ | 4 | 2 | 1 | -| Hoymiles HM-1200-4T | NRF24L01+ | 4 | 2 | 1 | -| Hoymiles HM-1500-4T | NRF24L01+ | 4 | 2 | 1 | -| Hoymiles HMS-300-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-350-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-400-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-450-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-500-1T | CMT2300A | 1 | 1 | 1 | -| Hoymiles HMS-600-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-700-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-800-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-900-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-1000-2T | CMT2300A | 2 | 2 | 1 | -| Hoymiles HMS-1600-4T | CMT2300A | 4 | 4 | 1 | -| Hoymiles HMS-1800-4T | CMT2300A | 4 | 4 | 1 | -| Hoymiles HMS-2000-4T | CMT2300A | 4 | 4 | 1 | -| Hoymiles HMT-1600-4T | CMT2300A | 4 | 2 | 3 | -| Hoymiles HMT-1800-4T | CMT2300A | 4 | 2 | 3 | -| Hoymiles HMT-2000-4T | CMT2300A | 4 | 2 | 3 | -| Hoymiles HMT-1800-6T | CMT2300A | 6 | 3 | 3 | -| Hoymiles HMT-2250-6T | CMT2300A | 6 | 3 | 3 | -| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | 1 | -| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | 1 | -| Solenso SOL-H800 | NRF24L01+ | 2 | 2 | 1 | -| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 | -| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 | -| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 | -| E-Star HERF-800 | NRF24L01+ | 2 | 2 | 1 | -| E-Star HERF-1600 | NRF24L01+ | 4 | 2 | 1 | -| E-Star HERF-1800 | NRF24L01+ | 4 | 2 | 1 | +A list of all currently supported inverters can be found [here](https://www.opendtu.solar/hardware/inverter_overview/) diff --git a/docs/DeviceProfiles/opendtu_fusion.json b/docs/DeviceProfiles/opendtu_fusion.json index f33dc47d..990f4c46 100644 --- a/docs/DeviceProfiles/opendtu_fusion.json +++ b/docs/DeviceProfiles/opendtu_fusion.json @@ -1,6 +1,9 @@ [ { "name": "OpenDTU Fusion v1", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -25,6 +28,9 @@ }, { "name": "OpenDTU Fusion v1 with SSD1306 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -54,6 +60,9 @@ }, { "name": "OpenDTU Fusion v1 with SH1106 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -83,6 +92,9 @@ }, { "name": "OpenDTU Fusion v2 with CMT2300A and NRF24", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -115,6 +127,9 @@ }, { "name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SH1106 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -152,6 +167,9 @@ }, { "name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SSD1306 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], "nrf24": { "miso": 48, "mosi": 35, @@ -186,5 +204,122 @@ "data": 2, "clk": 1 } + }, + { + "name": "OpenDTU Fusion v2 PoE", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], + "nrf24": { + "miso": 48, + "mosi": 35, + "clk": 36, + "irq": 47, + "en": 38, + "cs": 37 + }, + "cmt": { + "clk": 6, + "cs": 4, + "fcs": 21, + "sdio": 5, + "gpio2": 3, + "gpio3": 8 + }, + "w5500": { + "mosi": 40, + "miso": 41, + "sclk": 39, + "cs": 42, + "int": 44, + "rst": 43 + }, + "led": { + "led0": 17, + "led1": 18 + }, + "display": { + "type": 0, + "data": 2, + "clk": 1 + } + }, + { + "name": "OpenDTU Fusion v2 PoE with SH1106 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], + "nrf24": { + "miso": 48, + "mosi": 35, + "clk": 36, + "irq": 47, + "en": 38, + "cs": 37 + }, + "cmt": { + "clk": 6, + "cs": 4, + "fcs": 21, + "sdio": 5, + "gpio2": 3, + "gpio3": 8 + }, + "w5500": { + "mosi": 40, + "miso": 41, + "sclk": 39, + "cs": 42, + "int": 44, + "rst": 43 + }, + "led": { + "led0": 17, + "led1": 18 + }, + "display": { + "type": 3, + "data": 2, + "clk": 1 + } + }, + { + "name": "OpenDTU Fusion v2 PoE with SSD1306 Display", + "links": [ + {"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"} + ], + "nrf24": { + "miso": 48, + "mosi": 35, + "clk": 36, + "irq": 47, + "en": 38, + "cs": 37 + }, + "cmt": { + "clk": 6, + "cs": 4, + "fcs": 21, + "sdio": 5, + "gpio2": 3, + "gpio3": 8 + }, + "w5500": { + "mosi": 40, + "miso": 41, + "sclk": 39, + "cs": 42, + "int": 44, + "rst": 43 + }, + "led": { + "led0": 17, + "led1": 18 + }, + "display": { + "type": 2, + "data": 2, + "clk": 1 + } } -] \ No newline at end of file +] diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index 9ab746a3..c0a22c70 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -6,29 +6,42 @@ #include // mqtt discovery device classes -enum { +enum DeviceClassType { DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, - DEVICE_CLS_TEMP, DEVICE_CLS_POWER_FACTOR, - DEVICE_CLS_REACTIVE_POWER + DEVICE_CLS_REACTIVE_POWER, + DEVICE_CLS_CONNECTIVITY, + DEVICE_CLS_DURATION, + DEVICE_CLS_SIGNAL_STRENGTH, + DEVICE_CLS_TEMPERATURE, + DEVICE_CLS_RESTART }; -const char* const deviceClasses[] = { 0, "current", "energy", "power", "voltage", "frequency", "temperature", "power_factor", "reactive_power" }; -enum { +const char* const deviceClass_name[] = { 0, "current", "energy", "power", "voltage", "frequency", "power_factor", "reactive_power", "connectivity", "duration", "signal_strength", "temperature", "restart" }; + +enum StateClassType { STATE_CLS_NONE = 0, STATE_CLS_MEASUREMENT, STATE_CLS_TOTAL_INCREASING }; -const char* const stateClasses[] = { 0, "measurement", "total_increasing" }; +const char* const stateClass_name[] = { 0, "measurement", "total_increasing" }; + +enum CategoryType { + CATEGORY_NONE = 0, + CATEGORY_CONFIG, + CATEGORY_DIAGNOSTIC +}; +const char* const category_name[] = { 0, "config", "diagnostic" }; + typedef struct { FieldId_t fieldId; // field id - uint8_t deviceClsId; // device class - uint8_t stateClsId; // state class + DeviceClassType deviceClsId; // device class + StateClassType stateClsId; // state class } byteAssign_fieldDeviceClass_t; const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { @@ -41,7 +54,7 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = { { FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT }, { FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT }, { FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT }, - { FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT }, + { FLD_T, DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT }, { FLD_PF, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT }, { FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE }, { FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE }, @@ -58,13 +71,24 @@ public: private: void loop(); - void publish(const String& subtopic, const String& payload); - void publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic); - void publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic = ""); - void publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false); - void publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload); - void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100, float step = 1.0); - void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); + static void publish(const String& subtopic, const String& payload); + static void publish(const String& subtopic, const JsonDocument& doc); + + static void addCommonMetadata(JsonDocument& doc, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + + // Binary Sensor + static void publishBinarySensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishDtuBinarySensor(const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishInverterBinarySensor(std::shared_ptr inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + + // Sensor + static void publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishInverterSensor(std::shared_ptr inv, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + + static void publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false); + static void publishInverterButton(std::shared_ptr inv, const String& name, const String& state_topic, const String& payload, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category); + static void publishInverterNumber(std::shared_ptr inv, const String& name, const String& state_topic, const String& command_topic, const int16_t min, const int16_t max, float step, const String& unit_of_measure, const String& icon, const StateClassType state_class, const CategoryType category); static void createInverterInfo(JsonDocument& doc, std::shared_ptr inv); static void createDtuInfo(JsonDocument& doc); diff --git a/include/MqttHandleInverter.h b/include/MqttHandleInverter.h index 7c86a809..ea3a6e38 100644 --- a/include/MqttHandleInverter.h +++ b/include/MqttHandleInverter.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include class MqttHandleInverterClass { public: @@ -19,7 +21,6 @@ public: private: void loop(); void publishField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total); Task _loopTask; @@ -41,6 +42,29 @@ private: FLD_IRR, FLD_Q }; + + enum class Topic : unsigned { + LimitPersistentRelative, + LimitPersistentAbsolute, + LimitNonPersistentRelative, + LimitNonPersistentAbsolute, + Power, + Restart, + ResetRfStats, + }; + + static constexpr frozen::string _cmdtopic = "+/cmd/"; + static constexpr frozen::map _subscriptions = { + { "limit_persistent_relative", Topic::LimitPersistentRelative }, + { "limit_persistent_absolute", Topic::LimitPersistentAbsolute }, + { "limit_nonpersistent_relative", Topic::LimitNonPersistentRelative }, + { "limit_nonpersistent_absolute", Topic::LimitNonPersistentAbsolute }, + { "power", Topic::Power }, + { "restart", Topic::Restart }, + { "reset_rf_stats", Topic::ResetRfStats }, + }; + + void onMqttMessage(Topic t, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total); }; extern MqttHandleInverterClass MqttHandleInverter; diff --git a/include/NetworkSettings.h b/include/NetworkSettings.h index 40ddc914..90d3962b 100644 --- a/include/NetworkSettings.h +++ b/include/NetworkSettings.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "W5500.h" #include #include #include @@ -23,18 +24,18 @@ enum class network_event { NETWORK_EVENT_MAX }; -typedef std::function NetworkEventCb; +typedef std::function DtuNetworkEventCb; -typedef struct NetworkEventCbList { - NetworkEventCb cb; +typedef struct DtuNetworkEventCbList { + DtuNetworkEventCb cb; network_event event; - NetworkEventCbList() + DtuNetworkEventCbList() : cb(nullptr) , event(network_event::NETWORK_UNKNOWN) { } -} NetworkEventCbList_t; +} DtuNetworkEventCbList_t; class NetworkSettingsClass { public: @@ -53,7 +54,7 @@ public: bool isConnected() const; network_mode NetworkMode() const; - bool onEvent(NetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX); + bool onEvent(DtuNetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX); void raiseEvent(const network_event event); private: @@ -62,7 +63,7 @@ private: void setStaticIp(); void handleMDNS(); void setupMode(); - void NetworkEvent(const WiFiEvent_t event); + void NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info); Task _loopTask; @@ -81,8 +82,9 @@ private: bool _dnsServerStatus = false; network_mode _networkMode = network_mode::Undefined; bool _ethConnected = false; - std::vector _cbEventList; + std::vector _cbEventList; bool _lastMdnsEnabled = false; + std::unique_ptr _w5500; }; -extern NetworkSettingsClass NetworkSettings; \ No newline at end of file +extern NetworkSettingsClass NetworkSettings; diff --git a/include/PinMapping.h b/include/PinMapping.h index e0db88b6..21723067 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -12,6 +12,7 @@ struct PinMapping_t { char name[MAPPING_NAME_STRLEN + 1]; + int8_t nrf24_miso; int8_t nrf24_mosi; int8_t nrf24_clk; @@ -26,6 +27,14 @@ struct PinMapping_t { int8_t cmt_gpio3; int8_t cmt_sdio; + int8_t w5500_mosi; + int8_t w5500_miso; + int8_t w5500_sclk; + int8_t w5500_cs; + int8_t w5500_int; + int8_t w5500_rst; + +#if CONFIG_ETH_USE_ESP32_EMAC int8_t eth_phy_addr; bool eth_enabled; int eth_power; @@ -33,11 +42,14 @@ struct PinMapping_t { int eth_mdio; eth_phy_type_t eth_type; eth_clock_mode_t eth_clk_mode; +#endif + uint8_t display_type; uint8_t display_data; uint8_t display_clk; uint8_t display_cs; uint8_t display_reset; + int8_t led[PINMAPPING_LED_COUNT]; }; @@ -49,10 +61,13 @@ public: bool isValidNrf24Config() const; bool isValidCmt2300Config() const; + bool isValidW5500Config() const; +#if CONFIG_ETH_USE_ESP32_EMAC bool isValidEthConfig() const; +#endif private: PinMapping_t _pinMapping; }; -extern PinMappingClass PinMapping; \ No newline at end of file +extern PinMappingClass PinMapping; diff --git a/include/RestartHelper.h b/include/RestartHelper.h new file mode 100644 index 00000000..80f5f675 --- /dev/null +++ b/include/RestartHelper.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class RestartHelperClass { +public: + RestartHelperClass(); + void init(Scheduler& scheduler); + void triggerRestart(); + +private: + void loop(); + + Task _rebootTask; +}; + +extern RestartHelperClass RestartHelper; diff --git a/include/Utils.h b/include/Utils.h index f81e7318..6645f497 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -9,7 +9,6 @@ public: static uint32_t getChipId(); static uint64_t generateDtuSerial(); static int getTimezoneOffset(); - static void restartDtu(); static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static void removeAllFiles(); }; diff --git a/include/W5500.h b/include/W5500.h new file mode 100644 index 00000000..d85cb016 --- /dev/null +++ b/include/W5500.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include // required for esp_eth_handle_t +#include + +#include + +class W5500 { +private: + explicit W5500(spi_device_handle_t spi, gpio_num_t pin_int); + +public: + W5500(const W5500&) = delete; + W5500& operator=(const W5500&) = delete; + ~W5500(); + + static std::unique_ptr setup(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst); + String macAddress(); + +private: + static bool connection_check_spi(spi_device_handle_t spi); + static bool connection_check_interrupt(gpio_num_t pin_int); + + esp_eth_handle_t eth_handle; + esp_netif_t* eth_netif; +}; diff --git a/include/WebApi.h b/include/WebApi.h index b6fdbd08..2932f015 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -30,6 +30,7 @@ class WebApiClass { public: WebApiClass(); void init(Scheduler& scheduler); + void reload(); static bool checkCredentials(AsyncWebServerRequest* request); static bool checkCredentialsReadonly(AsyncWebServerRequest* request); diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index 0da8d3d9..c4cdaf21 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -32,6 +32,7 @@ enum WebApiError { InverterChanged, InverterDeleted, InverterOrdered, + InverterStatsResetted, LimitBase = 5000, LimitSerialZero, diff --git a/include/WebApi_inverter.h b/include/WebApi_inverter.h index c316622e..6ba6c5e8 100644 --- a/include/WebApi_inverter.h +++ b/include/WebApi_inverter.h @@ -14,4 +14,5 @@ private: void onInverterEdit(AsyncWebServerRequest* request); void onInverterDelete(AsyncWebServerRequest* request); void onInverterOrder(AsyncWebServerRequest* request); + void onInverterStatReset(AsyncWebServerRequest* request); }; diff --git a/include/WebApi_ws_console.h b/include/WebApi_ws_console.h index cf7beecc..b3194319 100644 --- a/include/WebApi_ws_console.h +++ b/include/WebApi_ws_console.h @@ -8,9 +8,11 @@ class WebApiWsConsoleClass { public: WebApiWsConsoleClass(); void init(AsyncWebServer& server, Scheduler& scheduler); + void reload(); private: AsyncWebSocket _ws; + AuthenticationMiddleware _simpleDigestAuth; Task _wsCleanupTask; void wsCleanupTaskCb(); diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 05f8ab8f..e16372e9 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -11,6 +11,7 @@ class WebApiWsLiveClass { public: WebApiWsLiveClass(); void init(AsyncWebServer& server, Scheduler& scheduler); + void reload(); private: static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv); @@ -24,6 +25,7 @@ private: void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); AsyncWebSocket _ws; + AuthenticationMiddleware _simpleDigestAuth; uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; diff --git a/include/__compiled_constants.h b/include/__compiled_constants.h index ac8991e9..a00caf6d 100644 --- a/include/__compiled_constants.h +++ b/include/__compiled_constants.h @@ -5,4 +5,5 @@ extern const char *__COMPILED_GIT_HASH__; +extern const char *__COMPILED_GIT_BRANCH__; // extern const char *__COMPILED_DATE_TIME_UTC_STR__; diff --git a/lib/CMT2300a/cmt_spi3.c b/lib/CMT2300a/cmt_spi3.c deleted file mode 100644 index 59aad36f..00000000 --- a/lib/CMT2300a/cmt_spi3.c +++ /dev/null @@ -1,142 +0,0 @@ -#include "cmt_spi3.h" -#include -#include -#include // for esp_rom_gpio_connect_out_signal - -SemaphoreHandle_t paramLock = NULL; -#define SPI_PARAM_LOCK() \ - do { \ - } while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS) -#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock) - -// for ESP32 this is the so-called HSPI -// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore, -// it is simply the first externally usable hardware SPI master controller -#define SPI_CMT SPI2_HOST - -spi_device_handle_t spi_reg, spi_fifo; - -void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed) -{ - paramLock = xSemaphoreCreateMutex(); - - spi_bus_config_t buscfg = { - .mosi_io_num = pin_sdio, - .miso_io_num = -1, // single wire MOSI/MISO - .sclk_io_num = pin_clk, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .max_transfer_sz = 32, - }; - spi_device_interface_config_t devcfg = { - .command_bits = 1, - .address_bits = 7, - .dummy_bits = 0, - .mode = 0, // SPI mode 0 - .cs_ena_pretrans = 1, - .cs_ena_posttrans = 1, - .clock_speed_hz = spi_speed, - .spics_io_num = pin_cs, - .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, - .queue_size = 1, - .pre_cb = NULL, - .post_cb = NULL, - }; - - ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED)); - ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg)); - - // FiFo - spi_device_interface_config_t devcfg2 = { - .command_bits = 0, - .address_bits = 0, - .dummy_bits = 0, - .mode = 0, // SPI mode 0 - .cs_ena_pretrans = 2, - .cs_ena_posttrans = (uint8_t)(1 / (spi_speed * 10e6 * 2) + 2), // >2 us - .clock_speed_hz = spi_speed, - .spics_io_num = pin_fcs, - .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, - .queue_size = 1, - .pre_cb = NULL, - .post_cb = NULL, - }; - ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo)); - - esp_rom_gpio_connect_out_signal(pin_sdio, spi_periph_signal[SPI_CMT].spid_out, true, false); - delay(100); -} - -void cmt_spi3_write(const uint8_t addr, const uint8_t dat) -{ - uint8_t tx_data; - tx_data = ~dat; - spi_transaction_t t = { - .cmd = 1, - .addr = ~addr, - .length = 8, - .tx_buffer = &tx_data, - .rx_buffer = NULL - }; - SPI_PARAM_LOCK(); - ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); - SPI_PARAM_UNLOCK(); - delayMicroseconds(100); -} - -uint8_t cmt_spi3_read(const uint8_t addr) -{ - uint8_t rx_data; - spi_transaction_t t = { - .cmd = 0, - .addr = ~addr, - .length = 8, - .rxlength = 8, - .tx_buffer = NULL, - .rx_buffer = &rx_data - }; - SPI_PARAM_LOCK(); - ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t)); - SPI_PARAM_UNLOCK(); - delayMicroseconds(100); - return rx_data; -} - -void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len) -{ - uint8_t tx_data; - - spi_transaction_t t = { - .length = 8, - .tx_buffer = &tx_data, // reference to write data - .rx_buffer = NULL - }; - - SPI_PARAM_LOCK(); - for (uint8_t i = 0; i < len; i++) { - tx_data = ~buf[i]; // negate buffer contents - ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); - delayMicroseconds(4); // > 4 us - } - SPI_PARAM_UNLOCK(); -} - -void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len) -{ - uint8_t rx_data; - - spi_transaction_t t = { - .length = 8, - .rxlength = 8, - .tx_buffer = NULL, - .rx_buffer = &rx_data - }; - - SPI_PARAM_LOCK(); - for (uint8_t i = 0; i < len; i++) { - ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); - delayMicroseconds(4); // > 4 us - buf[i] = rx_data; - } - SPI_PARAM_UNLOCK(); -} diff --git a/lib/CMT2300a/cmt_spi3.cpp b/lib/CMT2300a/cmt_spi3.cpp new file mode 100644 index 00000000..28fdc8ae --- /dev/null +++ b/lib/CMT2300a/cmt_spi3.cpp @@ -0,0 +1,155 @@ +#include "cmt_spi3.h" +#include +#include +#include + +SemaphoreHandle_t paramLock = NULL; +#define SPI_PARAM_LOCK() \ + do { \ + } while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS) +#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock) + +static void IRAM_ATTR pre_cb(spi_transaction_t *trans) { + gpio_set_level(*reinterpret_cast(trans->user), 0); +} + +static void IRAM_ATTR post_cb(spi_transaction_t *trans) { + gpio_set_level(*reinterpret_cast(trans->user), 1); +} + +spi_device_handle_t spi; +gpio_num_t cs_reg, cs_fifo; + +void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int32_t spi_speed) +{ + paramLock = xSemaphoreCreateMutex(); + + auto bus_config = std::make_shared( + static_cast(pin_sdio), + GPIO_NUM_NC, + static_cast(pin_clk) + ); + + spi_device_interface_config_t device_config { + .command_bits = 0, // set by transactions individually + .address_bits = 0, // set by transactions individually + .dummy_bits = 0, + .mode = 0, // SPI mode 0 + .duty_cycle_pos = 0, + .cs_ena_pretrans = 2, // only 1 pre and post cycle would be required for register access + .cs_ena_posttrans = static_cast(2 * spi_speed / 1000000), // >2 us + .clock_speed_hz = spi_speed, + .input_delay_ns = 0, + .spics_io_num = -1, // CS handled by callbacks + .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, + .queue_size = 1, + .pre_cb = pre_cb, + .post_cb = post_cb, + }; + + spi = SpiManagerInst.alloc_device("", bus_config, device_config); + if (!spi) + ESP_ERROR_CHECK(ESP_FAIL); + + cs_reg = static_cast(pin_cs); + ESP_ERROR_CHECK(gpio_reset_pin(cs_reg)); + ESP_ERROR_CHECK(gpio_set_level(cs_reg, 1)); + ESP_ERROR_CHECK(gpio_set_direction(cs_reg, GPIO_MODE_OUTPUT)); + + cs_fifo = static_cast(pin_fcs); + ESP_ERROR_CHECK(gpio_reset_pin(cs_fifo)); + ESP_ERROR_CHECK(gpio_set_level(cs_fifo, 1)); + ESP_ERROR_CHECK(gpio_set_direction(cs_fifo, GPIO_MODE_OUTPUT)); +} + +void cmt_spi3_write(const uint8_t addr, const uint8_t data) +{ + spi_transaction_ext_t trans { + .base { + .flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR, + .cmd = 0, + .addr = addr, + .length = 8, + .rxlength = 0, + .user = &cs_reg, // CS for register access + .tx_buffer = &data, + .rx_buffer = nullptr, + }, + .command_bits = 1, + .address_bits = 7, + .dummy_bits = 0, + }; + SPI_PARAM_LOCK(); + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, reinterpret_cast(&trans))); + SPI_PARAM_UNLOCK(); +} + +uint8_t cmt_spi3_read(const uint8_t addr) +{ + uint8_t data; + spi_transaction_ext_t trans { + .base { + .flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR, + .cmd = 1, + .addr = addr, + .length = 0, + .rxlength = 8, + .user = &cs_reg, // CS for register access + .tx_buffer = nullptr, + .rx_buffer = &data, + }, + .command_bits = 1, + .address_bits = 7, + .dummy_bits = 0, + }; + SPI_PARAM_LOCK(); + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, reinterpret_cast(&trans))); + SPI_PARAM_UNLOCK(); + return data; +} + +void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len) +{ + spi_transaction_t trans { + .flags = 0, + .cmd = 0, + .addr = 0, + .length = 8, + .rxlength = 0, + .user = &cs_fifo, // CS for FIFO access + .tx_buffer = nullptr, + .rx_buffer = nullptr, + }; + + SPI_PARAM_LOCK(); + spi_device_acquire_bus(spi, portMAX_DELAY); + for (uint8_t i = 0; i < len; i++) { + trans.tx_buffer = buf + i; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans)); + } + spi_device_release_bus(spi); + SPI_PARAM_UNLOCK(); +} + +void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len) +{ + spi_transaction_t trans { + .flags = 0, + .cmd = 0, + .addr = 0, + .length = 0, + .rxlength = 8, + .user = &cs_fifo, // CS for FIFO access + .tx_buffer = nullptr, + .rx_buffer = nullptr, + }; + + SPI_PARAM_LOCK(); + spi_device_acquire_bus(spi, portMAX_DELAY); + for (uint8_t i = 0; i < len; i++) { + trans.rx_buffer = buf + i; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans)); + } + spi_device_release_bus(spi); + SPI_PARAM_UNLOCK(); +} diff --git a/lib/CMT2300a/cmt_spi3.h b/lib/CMT2300a/cmt_spi3.h index 6d3a67b6..16655dba 100644 --- a/lib/CMT2300a/cmt_spi3.h +++ b/lib/CMT2300a/cmt_spi3.h @@ -3,7 +3,11 @@ #include -void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed); +#ifdef __cplusplus +extern "C" { +#endif + +void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int32_t spi_speed); void cmt_spi3_write(const uint8_t addr, const uint8_t dat); uint8_t cmt_spi3_read(const uint8_t addr); @@ -11,4 +15,8 @@ uint8_t cmt_spi3_read(const uint8_t addr); void cmt_spi3_write_fifo(const uint8_t* p_buf, const uint16_t len); void cmt_spi3_read_fifo(uint8_t* p_buf, const uint16_t len); +#ifdef __cplusplus +} +#endif + #endif diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 1416a73a..7f1a3c1f 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -4,6 +4,7 @@ */ #include "Hoymiles.h" #include "Utils.h" +#include "inverters/HERF_1CH.h" #include "inverters/HERF_2CH.h" #include "inverters/HERF_4CH.h" #include "inverters/HMS_1CH.h" @@ -135,15 +136,7 @@ void HoymilesClass::loop() if (currentWeekDay != lastWeekDay) { for (auto& inv : _inverters) { - // Have to reset the offets first, otherwise it will - // Substract the offset from zero which leads to a high value - inv->Statistics()->resetYieldDayCorrection(); - if (inv->getZeroYieldDayOnMidnight()) { - inv->Statistics()->zeroDailyData(); - } - if (inv->getClearEventlogOnMidnight()) { - inv->EventLog()->clearBuffer(); - } + inv->performDailyTask(); } lastWeekDay = currentWeekDay; @@ -173,6 +166,8 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, c i = std::make_shared(_radioNrf.get(), serial); } else if (HM_1CH::isValidSerial(serial)) { i = std::make_shared(_radioNrf.get(), serial); + } else if (HERF_1CH::isValidSerial(serial)) { + i = std::make_shared(_radioNrf.get(), serial); } else if (HERF_2CH::isValidSerial(serial)) { i = std::make_shared(_radioNrf.get(), serial); } else if (HERF_4CH::isValidSerial(serial)) { @@ -200,9 +195,9 @@ std::shared_ptr HoymilesClass::getInverterByPos(const uint8_t std::shared_ptr HoymilesClass::getInverterBySerial(const uint64_t serial) { - for (uint8_t i = 0; i < _inverters.size(); i++) { - if (_inverters[i]->serial() == serial) { - return _inverters[i]; + for (auto& inv : _inverters) { + if (inv->serial() == serial) { + return inv; } } return nullptr; @@ -214,9 +209,7 @@ std::shared_ptr HoymilesClass::getInverterByFragment(const fra return nullptr; } - std::shared_ptr inv; - for (uint8_t i = 0; i < _inverters.size(); i++) { - inv = _inverters[i]; + for (auto& inv : _inverters) { serial_u p; p.u64 = inv->serial(); diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 9d288554..55281ab0 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -66,16 +66,31 @@ void HoymilesRadio::handleReceivedPackage() } else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) { Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded"); + // Statistics: Count RX Fail No Answer + if (inv->RadioStats.TxRequestData > 0) { + inv->RadioStats.RxFailNoAnswer++; + } + _commandQueue.pop(); _busyFlag = false; } else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) { Hoymiles.getMessageOutput()->println("Retransmit timeout"); + // Statistics: Count RX Fail Partial Answer + if (inv->RadioStats.TxRequestData > 0) { + inv->RadioStats.RxFailPartialAnswer++; + } + _commandQueue.pop(); _busyFlag = false; } else if (verifyResult == FRAGMENT_HANDLE_ERROR) { Hoymiles.getMessageOutput()->println("Packet handling error"); + // Statistics: Count RX Fail Corrupt Data + if (inv->RadioStats.TxRequestData > 0) { + inv->RadioStats.RxFailCorruptData++; + } + _commandQueue.pop(); _busyFlag = false; @@ -83,17 +98,26 @@ void HoymilesRadio::handleReceivedPackage() // Perform Retransmit Hoymiles.getMessageOutput()->print("Request retransmit: "); Hoymiles.getMessageOutput()->println(verifyResult); + // Statistics: Count TX Re-Request Fragment + inv->RadioStats.TxReRequestFragment++; + sendRetransmitPacket(verifyResult); } else { // Successful received all packages Hoymiles.getMessageOutput()->println("Success"); + // Statistics: Count RX Success + if (inv->RadioStats.TxRequestData > 0) { + inv->RadioStats.RxSuccess++; + } + _commandQueue.pop(); _busyFlag = false; } } else { // If inverter was not found, assume the command is invalid Hoymiles.getMessageOutput()->println("RX: Invalid inverter found"); + // Statistics: Count RX Fail Unknown Data _commandQueue.pop(); _busyFlag = false; } @@ -105,6 +129,9 @@ void HoymilesRadio::handleReceivedPackage() auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); if (nullptr != inv) { inv->clearRxFragmentBuffer(); + // Statistics: TX Requests + inv->RadioStats.TxRequestData++; + sendEsbPacket(*cmd); } else { Hoymiles.getMessageOutput()->println("TX: Invalid inverter found"); diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index bbd31f21..e58221cd 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -34,7 +34,7 @@ uint32_t HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) const uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) const { if ((frequency % getChannelWidth()) != 0) { - Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %d kHz!\r\n", frequency / 1000000.0, getChannelWidth()); + Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %" PRId32 " kHz!\r\n", frequency / 1000000.0, getChannelWidth()); return 0xFF; // ERROR } if (frequency < getMinFrequency() || frequency > getMaxFrequency()) { @@ -43,7 +43,7 @@ uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) con return 0xFF; // ERROR } if (frequency < countryDefinition.at(_countryMode).Freq_Legal_Min || frequency > countryDefinition.at(_countryMode).Freq_Legal_Max) { - Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%d - %d MHz)\r\n", + Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%" PRId32 " - %" PRId32 " MHz)\r\n", frequency / 1000000.0, static_cast(countryDefinition.at(_countryMode).Freq_Legal_Min / 1e6), static_cast(countryDefinition.at(_countryMode).Freq_Legal_Max / 1e6)); @@ -167,9 +167,9 @@ void HoymilesRadio_CMT::loop() // Save packet in inverter rx buffer Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0); dumpBuf(f.fragment, f.len, false); - Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); + Hoymiles.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi); - inv->addRxFragment(f.fragment, f.len); + inv->addRxFragment(f.fragment, f.len, f.rssi); } else { Hoymiles.getMessageOutput()->println("Inverter Not found!"); } @@ -194,9 +194,9 @@ void HoymilesRadio_CMT::setPALevel(const int8_t paLevel) } if (_radio->setPALevel(paLevel)) { - Hoymiles.getMessageOutput()->printf("CMT TX power set to %d dBm\r\n", paLevel); + Hoymiles.getMessageOutput()->printf("CMT TX power set to %" PRId8 " dBm\r\n", paLevel); } else { - Hoymiles.getMessageOutput()->printf("CMT TX power %d dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel); + Hoymiles.getMessageOutput()->printf("CMT TX power %" PRId8 " dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel); } } diff --git a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp index 4bf104ad..0019a4bd 100644 --- a/lib/Hoymiles/src/HoymilesRadio_NRF.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_NRF.cpp @@ -76,11 +76,11 @@ void HoymilesRadio_NRF::loop() if (nullptr != inv) { // Save packet in inverter rx buffer - Hoymiles.getMessageOutput()->printf("RX Channel: %d --> ", f.channel); + Hoymiles.getMessageOutput()->printf("RX Channel: %" PRId8 " --> ", f.channel); dumpBuf(f.fragment, f.len, false); - Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); + Hoymiles.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi); - inv->addRxFragment(f.fragment, f.len); + inv->addRxFragment(f.fragment, f.len, f.rssi); } else { Hoymiles.getMessageOutput()->println("Inverter Not found!"); } @@ -183,7 +183,7 @@ void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract& cmd) openWritingPipe(s); _radio->setRetries(3, 15); - Hoymiles.getMessageOutput()->printf("TX %s Channel: %d --> ", + Hoymiles.getMessageOutput()->printf("TX %s Channel: %" PRId8 " --> ", cmd.getCommandName().c_str(), _radio->getChannel()); cmd.dumpDataPayload(Hoymiles.getMessageOutput()); _radio->write(cmd.getDataPayload(), cmd.getDataSize()); diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp index b1396a4d..9f5563bf 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp @@ -48,7 +48,7 @@ bool RealTimeRunDataCommand::handleResponse(const fragment_t fragment[], const u const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { - Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", + Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); return false; diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp index 0c142afc..70dcffa9 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp @@ -48,7 +48,7 @@ bool SystemConfigParaCommand::handleResponse(const fragment_t fragment[], const const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { - Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", + Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); return false; diff --git a/lib/Hoymiles/src/inverters/HERF_1CH.cpp b/lib/Hoymiles/src/inverters/HERF_1CH.cpp new file mode 100644 index 00000000..49531d99 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_1CH.cpp @@ -0,0 +1,55 @@ + +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "HERF_1CH.h" + +static const byteAssign_t byteAssignment[] = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 40, 2, 10, false, 1 }, // to be verified + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, // to be verified + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, // to be verified + + { TYPE_INV, CH0, FLD_YD, UNIT_WH, CALC_TOTAL_YD, 0, CMD_CALC, false, 0 }, + { TYPE_INV, CH0, FLD_YT, UNIT_KWH, CALC_TOTAL_YT, 0, CMD_CALC, false, 3 }, + { TYPE_INV, CH0, FLD_PDC, UNIT_W, CALC_TOTAL_PDC, 0, CMD_CALC, false, 1 }, + { TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 } +}; + +HERF_1CH::HERF_1CH(HoymilesRadio* radio, const uint64_t serial) + : HM_Abstract(radio, serial) {}; + +bool HERF_1CH::isValidSerial(const uint64_t serial) +{ + // serial >= 0x284100000000 && serial <= 0x2841ffffffff + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x2841; +} + +String HERF_1CH::typeName() const +{ + return "HERF-300-1T"; +} + +const byteAssign_t* HERF_1CH::getByteAssignment() const +{ + return byteAssignment; +} + +uint8_t HERF_1CH::getByteAssignmentSize() const +{ + return sizeof(byteAssignment) / sizeof(byteAssignment[0]); +} diff --git a/lib/Hoymiles/src/inverters/HERF_1CH.h b/lib/Hoymiles/src/inverters/HERF_1CH.h new file mode 100644 index 00000000..8220272e --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_1CH.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_Abstract.h" + +class HERF_1CH : public HM_Abstract { +public: + explicit HERF_1CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; +}; diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index 4ad0157f..4cbc686c 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -42,7 +42,7 @@ bool HMS_2CH::isValidSerial(const uint64_t serial) { // serial >= 0x114400000000 && serial <= 0x1144ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; - return preSerial == 0x1144 || preSerial == 0x1143; + return preSerial == 0x1144 || preSerial == 0x1143 || preSerial == 0x1410; } String HMS_2CH::typeName() const diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 68d61183..5d52a380 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -137,6 +137,11 @@ bool InverterAbstract::getClearEventlogOnMidnight() const return _clearEventlogOnMidnight; } +int8_t InverterAbstract::getLastRssi() const +{ + return _lastRssi; +} + bool InverterAbstract::sendChangeChannelRequest() { return false; @@ -185,8 +190,10 @@ void InverterAbstract::clearRxFragmentBuffer() _rxFragmentRetransmitCnt = 0; } -void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len) +void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi) { + _lastRssi = rssi; + if (len < 11) { Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__); return; @@ -208,7 +215,7 @@ void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len } if (fragmentId >= MAX_RF_FRAGMENT_COUNT) { - Hoymiles.getMessageOutput()->printf("ERROR: fragment id %d is too large for buffer and ignored\r\n", fragmentId); + Hoymiles.getMessageOutput()->printf("ERROR: fragment id %" PRId8 " is too large for buffer and ignored\r\n", fragmentId); return; } @@ -272,3 +279,22 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd) return FRAGMENT_OK; } + +void InverterAbstract::performDailyTask() +{ + // Have to reset the offets first, otherwise it will + // Substract the offset from zero which leads to a high value + Statistics()->resetYieldDayCorrection(); + if (getZeroYieldDayOnMidnight()) { + Statistics()->zeroDailyData(); + } + if (getClearEventlogOnMidnight()) { + EventLog()->clearBuffer(); + } + resetRadioStats(); +} + +void InverterAbstract::resetRadioStats() +{ + RadioStats = {}; +} diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 2a51079b..29fba12f 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -61,10 +61,36 @@ public: void setClearEventlogOnMidnight(const bool enabled); bool getClearEventlogOnMidnight() const; + int8_t getLastRssi() const; + void clearRxFragmentBuffer(); - void addRxFragment(const uint8_t fragment[], const uint8_t len); + void addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi); uint8_t verifyAllFragments(CommandAbstract& cmd); + void performDailyTask(); + + void resetRadioStats(); + + struct { + // TX Request Data + uint32_t TxRequestData; + + // TX Re-Request Fragment + uint32_t TxReRequestFragment; + + // RX Success + uint32_t RxSuccess; + + // RX Fail Partial Answer + uint32_t RxFailPartialAnswer; + + // RX Fail No Answer + uint32_t RxFailNoAnswer; + + // RX Fail Corrupt Data + uint32_t RxFailCorruptData; + } RadioStats = {}; + virtual bool sendStatsRequest() = 0; virtual bool sendAlarmLogRequest(const bool force = false) = 0; virtual bool sendDevInfoRequest() = 0; @@ -107,6 +133,8 @@ private: bool _zeroYieldDayOnMidnight = false; bool _clearEventlogOnMidnight = false; + int8_t _lastRssi = -127; + std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; std::unique_ptr _gridProfileParser; diff --git a/lib/Hoymiles/src/inverters/README.md b/lib/Hoymiles/src/inverters/README.md index 8d913deb..b5544532 100644 --- a/lib/Hoymiles/src/inverters/README.md +++ b/lib/Hoymiles/src/inverters/README.md @@ -1,15 +1,16 @@ # Class overview -| Class | Models | Serial range | -| --------------| --------------------------- | ------------ | -| HM_1CH | HM-300/350/400-1T | 1121 | -| HM_2CH | HM-600/700/800-2T | 1141 | -| HM_4CH | HM-1000/1200/1500-4T | 1161 | -| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 | -| HMS_1CHv2 | HMS-500-1T v2 | 1125 | -| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144 | -| HMS_4CH | HMS-1600/1800/2000-4T | 1164 | -| HMT_4CH | HMT-1600/1800/2000-4T | 1361 | -| HMT_6CH | HMT-1800/2250-6T | 1382 | -| HERF_2CH | HERF 800 | 2821 | -| HERF_4CH | HERF 1800 | 2801 | +| Class | Models | Serial range | +| --------------| --------------------------- | ------------- -- | +| HM_1CH | HM-300/350/400-1T | 1121 | +| HM_2CH | HM-600/700/800-2T | 1141 | +| HM_4CH | HM-1000/1200/1500-4T | 1161 | +| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 | +| HMS_1CHv2 | HMS-500-1T v2 | 1125 | +| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144, 1410 | +| HMS_4CH | HMS-1600/1800/2000-4T | 1164 | +| HMT_4CH | HMT-1600/1800/2000-4T | 1361 | +| HMT_6CH | HMT-1800/2250-6T | 1382 | +| HERF_1CH | HERF 300 | 2841 | +| HERF_2CH | HERF 800 | 2821 | +| HERF_4CH | HERF 1800 | 2801 | diff --git a/lib/SpiManager/library.json b/lib/SpiManager/library.json new file mode 100644 index 00000000..22e5ddc9 --- /dev/null +++ b/lib/SpiManager/library.json @@ -0,0 +1,13 @@ +{ + "name": "SpiManager", + "keywords": "spi", + "description": "Library for managing the allocation of dedicated or shared SPI buses on the ESP32.", + "authors": { + "name": "Lennart Ferlemann" + }, + "version": "0.0.1", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} diff --git a/lib/SpiManager/src/SpiBus.cpp b/lib/SpiManager/src/SpiBus.cpp new file mode 100644 index 00000000..0dcb5e4f --- /dev/null +++ b/lib/SpiManager/src/SpiBus.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SpiBus.h" +#include "SpiBusConfig.h" +#include "SpiCallback.h" + +SpiBus::SpiBus(const std::string& _id, spi_host_device_t _host_device) + : id(_id) + , host_device(_host_device) + , cur_config(nullptr) +{ + spi_bus_config_t bus_config { + .mosi_io_num = -1, + .miso_io_num = -1, + .sclk_io_num = -1, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .data4_io_num = -1, + .data5_io_num = -1, + .data6_io_num = -1, + .data7_io_num = -1, + .max_transfer_sz = SPI_MAX_DMA_LEN, + .flags = 0, + .intr_flags = 0 + }; + ESP_ERROR_CHECK(spi_bus_initialize(host_device, &bus_config, SPI_DMA_CH_AUTO)); +} + +SpiBus::~SpiBus() +{ + ESP_ERROR_CHECK(spi_bus_free(host_device)); +} + +spi_device_handle_t SpiBus::add_device(const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config) +{ + if (!SpiCallback::patch(shared_from_this(), bus_config, device_config)) + return nullptr; + + spi_device_handle_t device; + ESP_ERROR_CHECK(spi_bus_add_device(host_device, &device_config, &device)); + return device; +} + +// TODO: add remove_device (with spi_device_acquire_bus) + +void SpiBus::apply_config(SpiBusConfig* config) +{ + if (cur_config) + cur_config->unpatch(host_device); + cur_config = config; + if (cur_config) + cur_config->patch(host_device); +} diff --git a/lib/SpiManager/src/SpiBus.h b/lib/SpiManager/src/SpiBus.h new file mode 100644 index 00000000..1ca79c7c --- /dev/null +++ b/lib/SpiManager/src/SpiBus.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +class SpiBusConfig; + +class SpiBus : public std::enable_shared_from_this { +public: + explicit SpiBus(const std::string& id, spi_host_device_t host_device); + SpiBus(const SpiBus&) = delete; + SpiBus& operator=(const SpiBus&) = delete; + ~SpiBus(); + + inline __attribute__((always_inline)) void require_config(SpiBusConfig* config) + { + if (config == cur_config) + return; + apply_config(config); + } + + inline __attribute__((always_inline)) void free_config(SpiBusConfig* config) + { + if (config != cur_config) + return; + apply_config(nullptr); + } + + inline const std::string& get_id() const + { + return id; + } + + inline spi_host_device_t get_host_device() const + { + return host_device; + } + + spi_device_handle_t add_device(const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config); + +private: + void apply_config(SpiBusConfig* config); + + std::string id; + spi_host_device_t host_device; + SpiBusConfig* cur_config; +}; diff --git a/lib/SpiManager/src/SpiBusConfig.cpp b/lib/SpiManager/src/SpiBusConfig.cpp new file mode 100644 index 00000000..64234d65 --- /dev/null +++ b/lib/SpiManager/src/SpiBusConfig.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SpiBusConfig.h" + +#include +#include +#include + +SpiBusConfig::SpiBusConfig(gpio_num_t _pin_mosi, gpio_num_t _pin_miso, gpio_num_t _pin_sclk) + : pin_mosi(_pin_mosi) + , pin_miso(_pin_miso) + , pin_sclk(_pin_sclk) +{ + if (pin_mosi != GPIO_NUM_NC) { + ESP_ERROR_CHECK(gpio_reset_pin(pin_mosi)); + ESP_ERROR_CHECK(gpio_set_direction(pin_mosi, GPIO_MODE_INPUT_OUTPUT)); + } + + if (pin_miso != GPIO_NUM_NC) { + ESP_ERROR_CHECK(gpio_reset_pin(pin_miso)); + ESP_ERROR_CHECK(gpio_set_direction(pin_miso, GPIO_MODE_INPUT)); + } + + if (pin_sclk != GPIO_NUM_NC) { + ESP_ERROR_CHECK(gpio_reset_pin(pin_sclk)); + ESP_ERROR_CHECK(gpio_set_direction(pin_sclk, GPIO_MODE_INPUT_OUTPUT)); + } +} + +SpiBusConfig::~SpiBusConfig() +{ + if (pin_mosi != GPIO_NUM_NC) + ESP_ERROR_CHECK(gpio_reset_pin(pin_mosi)); + + if (pin_miso != GPIO_NUM_NC) + ESP_ERROR_CHECK(gpio_reset_pin(pin_miso)); + + if (pin_sclk != GPIO_NUM_NC) + ESP_ERROR_CHECK(gpio_reset_pin(pin_sclk)); +} + +void SpiBusConfig::patch(spi_host_device_t host_device) +{ + if (pin_mosi != GPIO_NUM_NC) { + esp_rom_gpio_connect_out_signal(pin_mosi, spi_periph_signal[host_device].spid_out, false, false); + esp_rom_gpio_connect_in_signal(pin_mosi, spi_periph_signal[host_device].spid_in, false); + } + + if (pin_miso != GPIO_NUM_NC) + esp_rom_gpio_connect_in_signal(pin_miso, spi_periph_signal[host_device].spiq_in, false); + + if (pin_sclk != GPIO_NUM_NC) { + esp_rom_gpio_connect_out_signal(pin_sclk, spi_periph_signal[host_device].spiclk_out, false, false); + esp_rom_gpio_connect_in_signal(pin_sclk, spi_periph_signal[host_device].spiclk_in, false); + } +} + +void SpiBusConfig::unpatch(spi_host_device_t host_device) +{ + if (pin_mosi != GPIO_NUM_NC) { + esp_rom_gpio_connect_out_signal(pin_mosi, SIG_GPIO_OUT_IDX, false, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spid_in, false); + } + + if (pin_miso != GPIO_NUM_NC) + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spiq_in, false); + + if (pin_sclk != GPIO_NUM_NC) { + esp_rom_gpio_connect_out_signal(pin_sclk, SIG_GPIO_OUT_IDX, false, false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spiclk_in, false); + } +} diff --git a/lib/SpiManager/src/SpiBusConfig.h b/lib/SpiManager/src/SpiBusConfig.h new file mode 100644 index 00000000..736b8951 --- /dev/null +++ b/lib/SpiManager/src/SpiBusConfig.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class SpiBusConfig { +public: + explicit SpiBusConfig(gpio_num_t pin_mosi, gpio_num_t pin_miso, gpio_num_t pin_sclk); + SpiBusConfig(const SpiBusConfig&) = delete; + SpiBusConfig& operator=(const SpiBusConfig&) = delete; + ~SpiBusConfig(); + + void patch(spi_host_device_t host_device); + void unpatch(spi_host_device_t host_device); + +private: + gpio_num_t pin_mosi; + gpio_num_t pin_miso; + gpio_num_t pin_sclk; +}; diff --git a/lib/SpiManager/src/SpiCallback.cpp b/lib/SpiManager/src/SpiCallback.cpp new file mode 100644 index 00000000..e353d04b --- /dev/null +++ b/lib/SpiManager/src/SpiCallback.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SpiCallback.h" + +#include "SpiBus.h" +#include +#include + +namespace SpiCallback { +namespace { + struct CallbackData { + std::shared_ptr bus; + std::shared_ptr config; + transaction_cb_t inner_pre_cb; + transaction_cb_t inner_post_cb; + }; + + std::array, SPI_MANAGER_CALLBACK_COUNT> instances; + + template + void IRAM_ATTR fn_pre_cb(spi_transaction_t* trans) + { + instances[N]->bus->require_config(instances[N]->config.get()); + if (instances[N]->inner_pre_cb) + instances[N]->inner_pre_cb(trans); + } + + template + void IRAM_ATTR fn_post_cb(spi_transaction_t* trans) + { + if (instances[N]->inner_post_cb) + instances[N]->inner_post_cb(trans); + } + + template + inline __attribute__((always_inline)) bool alloc(CallbackData*& instance, transaction_cb_t& pre_cb, transaction_cb_t& post_cb) + { + if constexpr (N > 0) { + if (alloc(instance, pre_cb, post_cb)) + return true; + if (!instances[N - 1]) { + instances[N - 1].emplace(); + instance = &*instances[N - 1]; + pre_cb = fn_pre_cb; + post_cb = fn_post_cb; + return true; + } + } + return false; + } +} + +bool patch(const std::shared_ptr& bus, const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config) +{ + CallbackData* instance; + transaction_cb_t pre_cb; + transaction_cb_t post_cb; + if (!alloc(instance, pre_cb, post_cb)) + return false; + + instance->bus = bus; + instance->config = bus_config; + instance->inner_pre_cb = device_config.pre_cb; + instance->inner_post_cb = device_config.post_cb; + device_config.pre_cb = pre_cb; + device_config.post_cb = post_cb; + + return true; +} +} diff --git a/lib/SpiManager/src/SpiCallback.h b/lib/SpiManager/src/SpiCallback.h new file mode 100644 index 00000000..98222b1a --- /dev/null +++ b/lib/SpiManager/src/SpiCallback.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +// Pre and post callbacks for 2 buses with 3 devices each +#define SPI_MANAGER_CALLBACK_COUNT 6 + +class SpiBus; +class SpiBusConfig; + +namespace SpiCallback { +bool patch(const std::shared_ptr& bus, const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config); +} diff --git a/lib/SpiManager/src/SpiManager.cpp b/lib/SpiManager/src/SpiManager.cpp new file mode 100644 index 00000000..d727a96e --- /dev/null +++ b/lib/SpiManager/src/SpiManager.cpp @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SpiManager.h" + +#ifdef ARDUINO +#include +#endif + +SpiManager::SpiManager() +{ +} + +#ifdef ARDUINO + +std::optional SpiManager::to_arduino(spi_host_device_t host_device) +{ + switch (host_device) { +#if CONFIG_IDF_TARGET_ESP32 + case SPI1_HOST: + return FSPI; + case SPI2_HOST: + return HSPI; + case SPI3_HOST: + return VSPI; +#elif CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 + case SPI2_HOST: + return FSPI; + case SPI3_HOST: + return HSPI; +#elif CONFIG_IDF_TARGET_ESP32C3 + case SPI2_HOST: + return FSPI; +#endif + default: + return std::nullopt; + } +} + +#endif + +bool SpiManager::register_bus(spi_host_device_t host_device) +{ + for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) { + if (available_buses[i]) + continue; + + available_buses[i] = host_device; + return true; + } + + return false; +} + +bool SpiManager::claim_bus(spi_host_device_t& host_device) +{ + for (int i = SPI_MANAGER_NUM_BUSES - 1; i >= 0; --i) { + if (!available_buses[i]) + continue; + + host_device = *available_buses[i]; + available_buses[i].reset(); + return true; + } + + return false; +} + +#ifdef ARDUINO + +std::optional SpiManager::claim_bus_arduino() +{ + spi_host_device_t host_device; + if (!claim_bus(host_device)) + return std::nullopt; + return to_arduino(host_device); +} + +#endif + +spi_device_handle_t SpiManager::alloc_device(const std::string& bus_id, const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config) +{ + std::shared_ptr shared_bus = get_shared_bus(bus_id); + if (!shared_bus) + return nullptr; + + return shared_bus->add_device(bus_config, device_config); +} + +std::shared_ptr SpiManager::get_shared_bus(const std::string& bus_id) +{ + // look for existing shared bus + for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) { + if (!shared_buses[i]) + continue; + if (shared_buses[i]->get_id() == bus_id) + return shared_buses[i]; + } + + // create new shared bus + for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) { + if (shared_buses[i]) + continue; + + spi_host_device_t host_device; + if (!claim_bus(host_device)) + return nullptr; + + shared_buses[i] = std::make_shared(bus_id, host_device); + return shared_buses[i]; + } + + return nullptr; +} + +SpiManager SpiManagerInst; diff --git a/lib/SpiManager/src/SpiManager.h b/lib/SpiManager/src/SpiManager.h new file mode 100644 index 00000000..1e8f6e1b --- /dev/null +++ b/lib/SpiManager/src/SpiManager.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "SpiBus.h" +#include "SpiBusConfig.h" + +#include + +#include +#include +#include +#include + +#define SPI_MANAGER_NUM_BUSES SOC_SPI_PERIPH_NUM + +class SpiManager { +public: + explicit SpiManager(); + SpiManager(const SpiManager&) = delete; + SpiManager& operator=(const SpiManager&) = delete; + +#ifdef ARDUINO + static std::optional to_arduino(spi_host_device_t host_device); +#endif + + bool register_bus(spi_host_device_t host_device); + bool claim_bus(spi_host_device_t& host_device); +#ifdef ARDUINO + std::optional claim_bus_arduino(); +#endif + + spi_device_handle_t alloc_device(const std::string& bus_id, const std::shared_ptr& bus_config, spi_device_interface_config_t& device_config); + +private: + std::shared_ptr get_shared_bus(const std::string& bus_id); + + std::array, SPI_MANAGER_NUM_BUSES> available_buses; + std::array, SPI_MANAGER_NUM_BUSES> shared_buses; +}; + +extern SpiManager SpiManagerInst; diff --git a/pio-scripts/auto_firmware_version.py b/pio-scripts/auto_firmware_version.py index 26e1bd65..c1fa9771 100644 --- a/pio-scripts/auto_firmware_version.py +++ b/pio-scripts/auto_firmware_version.py @@ -36,9 +36,20 @@ def get_build_version(): return build_version +def get_build_branch(): + try: + branch_name = porcelain.active_branch('.').decode('utf-8') # '.' refers to the repository root dir + except Exception as err: + branch_name = "master" + print("Firmware Branch: " + branch_name) + return branch_name + + def get_firmware_specifier_build_flag(): build_version = get_build_version() build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\"" + build_branch = get_build_branch() + build_flag += " -D AUTO_GIT_BRANCH=\\\"" + branch_name + "\\\"" return (build_flag) @@ -64,6 +75,8 @@ def do_main(): if 1: # Add the description of the current git revision lines += 'const char *__COMPILED_GIT_HASH__ = "%s";\n' % (get_build_version()) + # ... and git branch + lines += 'const char *__COMPILED_GIT_BRANCH__ = "%s";\n' % (get_build_branch()) updateFileIfChanged(targetfile, bytes(lines, "utf-8")) diff --git a/pio-scripts/create_factory_bin.py b/pio-scripts/create_factory_bin.py index d394998b..c0ef4a8c 100644 --- a/pio-scripts/create_factory_bin.py +++ b/pio-scripts/create_factory_bin.py @@ -18,20 +18,64 @@ Import("env") +env = DefaultEnvironment() platform = env.PioPlatform() import sys -from os.path import join, getsize +import csv +import subprocess +import shutil +from os.path import join, getsize, exists, isdir +from os import listdir sys.path.append(join(platform.get_package_dir("tool-esptoolpy"))) import esptool +def esp32_build_filesystem(fs_name, fs_size): + filesystem_dir = env.subst("$PROJECT_DATA_DIR") + print("Creating %dKiB filesystem with content:" % (int(fs_size, 0)/1024) ) + if not isdir(filesystem_dir) or not listdir(filesystem_dir): + print("No files added -> will NOT create littlefs.bin and NOT overwrite fs partition!") + return False + # this does not work on GitHub, results in 'mklittlefs: No such file or directory' + tool = shutil.which(env.subst(env["MKFSTOOL"])) + if tool is None or not exists(tool): + print("Using fallback mklittlefs") + tool = "~/.platformio/packages/tool-mklittlefs/mklittlefs" + + cmd = (tool, "-c", filesystem_dir, "-s", fs_size, fs_name) + returncode = subprocess.call(cmd, shell=False) + print("Return Code:", returncode) + return True + def esp32_create_combined_bin(source, target, env): print("Generating combined binary for serial flashing") # The offset from begin of the file where the app0 partition starts # This is defined in the partition .csv file app_offset = 0x10000 + fs_offset = -1 + fs_name = env.subst("$BUILD_DIR/littlefs.bin") + + with open(env.BoardConfig().get("build.partitions")) as csv_file: + print("Read partitions from ", env.BoardConfig().get("build.partitions")) + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + for row in csv_reader: + if line_count == 0: + print(f'{", ".join(row)}') + line_count += 1 + else: + if (len(row) < 4): + continue + print(f'{row[0]} {row[1]} {row[2]} {row[3]} {row[4]}') + line_count += 1 + if(row[0] == 'app0'): + app_offset = int(row[3], base=16) + elif(row[0] == 'spiffs'): + partition_size = row[4] + if esp32_build_filesystem(fs_name, partition_size): + fs_offset = int(row[3], base=16) new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) @@ -77,9 +121,13 @@ def esp32_create_combined_bin(source, target, env): print(f" - {hex(app_offset)} | {firmware_name}") cmd += [hex(app_offset), firmware_name] + if fs_offset != -1: + print(f" - {hex(fs_offset)} | {fs_name}") + cmd += [hex(fs_offset), fs_name] + print('Using esptool.py arguments: %s' % ' '.join(cmd)) esptool.main(cmd) -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) \ No newline at end of file +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) diff --git a/platformio.ini b/platformio.ini index 2d86e9de..6ab55a22 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,7 +19,9 @@ extra_configs = custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb framework = arduino -platform = espressif32@6.8.1 +platform = espressif32@6.9.0 +platform_packages = + platformio/tool-mklittlefs build_flags = -DPIOENV=\"$PIOENV\" @@ -39,13 +41,13 @@ build_unflags = -std=gnu++11 lib_deps = - mathieucarbou/ESPAsyncWebServer @ 3.1.2 - bblanchon/ArduinoJson @ 7.1.0 + mathieucarbou/ESPAsyncWebServer @ 3.3.12 + bblanchon/ArduinoJson @ 7.2.0 https://github.com/bertmelis/espMqttClient.git#v1.7.0 nrf24/RF24 @ 1.4.9 - olikraus/U8g2 @ 2.35.19 + olikraus/U8g2 @ 2.35.30 buelowp/sunset @ 1.1.7 - https://github.com/arkhipenko/TaskScheduler#testing + arkhipenko/TaskScheduler @ 3.8.5 extra_scripts = pre:pio-scripts/auto_firmware_version.py @@ -227,6 +229,7 @@ build_flags = ${env.build_flags} -DLED0=17 -DLED1=18 -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 [env:opendtufusionv2] board = esp32-s3-devkitc-1 @@ -250,3 +253,32 @@ build_flags = ${env.build_flags} -DCMT_SDIO=5 -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 + +[env:opendtufusionv2_poe] +board = esp32-s3-devkitc-1 +upload_protocol = esp-builtin +debug_tool = esp-builtin +debug_speed = 12000 +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=48 + -DHOYMILES_PIN_MOSI=35 + -DHOYMILES_PIN_SCLK=36 + -DHOYMILES_PIN_IRQ=47 + -DHOYMILES_PIN_CE=38 + -DHOYMILES_PIN_CS=37 + -DLED0=17 + -DLED1=18 + -DCMT_CLK=6 + -DCMT_CS=4 + -DCMT_FCS=21 + -DCMT_GPIO2=3 + -DCMT_GPIO3=8 + -DCMT_SDIO=5 + -DW5500_MOSI=40 + -DW5500_MISO=41 + -DW5500_SCLK=39 + -DW5500_CS=42 + -DW5500_INT=44 + -DW5500_RST=43 + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/src/Display_Graphic_Diagram.cpp b/src/Display_Graphic_Diagram.cpp index b5296882..fb0b68fe 100644 --- a/src/Display_Graphic_Diagram.cpp +++ b/src/Display_Graphic_Diagram.cpp @@ -87,7 +87,7 @@ void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX, uint8_t xPos if (maxWatts > 999) { snprintf(fmtText, sizeof(fmtText), "%2.1fkW", maxWatts / 1000); } else { - snprintf(fmtText, sizeof(fmtText), "%dW", static_cast(maxWatts)); + snprintf(fmtText, sizeof(fmtText), "%" PRId16 "W", static_cast(maxWatts)); } if (isFullscreen) { diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 0e903187..7e7a5f86 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -8,20 +8,7 @@ #include "PinMapping.h" #include "SunPosition.h" #include - -// the NRF shall use the second externally usable HW SPI controller -// for ESP32 that is the so-called VSPI, for ESP32-S2/S3 it is now called implicitly -// HSPI, as it has shifted places for these chip generations -// for all generations, this is equivalent to SPI3_HOST in the lower level driver -// For ESP32-C2, the only externally usable HW SPI controller is SPI2, its signal names -// being prefixed with FSPI. -#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 -#define SPI_NRF HSPI -#elif CONFIG_IDF_TARGET_ESP32C3 -#define SPI_NRF FSPI -#else -#define SPI_NRF VSPI -#endif +#include InverterSettingsClass InverterSettings; @@ -44,7 +31,10 @@ void InverterSettingsClass::init(Scheduler& scheduler) if (PinMapping.isValidNrf24Config() || PinMapping.isValidCmt2300Config()) { if (PinMapping.isValidNrf24Config()) { - SPIClass* spiClass = new SPIClass(SPI_NRF); + auto spi_bus = SpiManagerInst.claim_bus_arduino(); + ESP_ERROR_CHECK(spi_bus ? ESP_OK : ESP_FAIL); + + SPIClass* spiClass = new SPIClass(*spi_bus); spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); Hoymiles.initNRF(spiClass, pin.nrf24_en, pin.nrf24_irq); } diff --git a/src/MqttHandleDtu.cpp b/src/MqttHandleDtu.cpp index e8192b2e..df025f12 100644 --- a/src/MqttHandleDtu.cpp +++ b/src/MqttHandleDtu.cpp @@ -7,6 +7,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include +#include MqttHandleDtuClass MqttHandleDtu; @@ -34,8 +35,17 @@ void MqttHandleDtuClass::loop() MqttSettings.publish("dtu/uptime", String(millis() / 1000)); MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); + MqttSettings.publish("dtu/heap/size", String(ESP.getHeapSize())); + MqttSettings.publish("dtu/heap/free", String(ESP.getFreeHeap())); + MqttSettings.publish("dtu/heap/minfree", String(ESP.getMinFreeHeap())); + MqttSettings.publish("dtu/heap/maxalloc", String(ESP.getMaxAllocHeap())); if (NetworkSettings.NetworkMode() == network_mode::WiFi) { MqttSettings.publish("dtu/rssi", String(WiFi.RSSI())); MqttSettings.publish("dtu/bssid", WiFi.BSSIDstr()); } + + float temperature = CpuTemperature.read(); + if (!std::isnan(temperature)) { + MqttSettings.publish("dtu/temperature", String(temperature)); + } } diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 67d264b2..6491c9ba 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -7,8 +7,8 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "Utils.h" -#include "defaults.h" #include "__compiled_constants.h" +#include "defaults.h" MqttHandleHassClass MqttHandleHass; @@ -58,29 +58,46 @@ void MqttHandleHassClass::publishConfig() const CONFIG_T& config = Configuration.get(); // publish DTU sensors - publishDtuSensor("IP", "", "diagnostic", "mdi:network-outline", "", ""); - publishDtuSensor("WiFi Signal", "signal_strength", "diagnostic", "", "dBm", "rssi"); - publishDtuSensor("Uptime", "duration", "diagnostic", "", "s", ""); - publishDtuBinarySensor("Status", "connectivity", "diagnostic", config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, config.Mqtt.Lwt.Topic); + publishDtuSensor("IP", "dtu/ip", "", "mdi:network-outline", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("WiFi Signal", "dtu/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Uptime", "dtu/uptime", "s", "", DEVICE_CLS_DURATION, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Temperature", "dtu/temperature", "°C", "", DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Heap Size", "dtu/heap/size", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Heap Free", "dtu/heap/free", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Largest Free Heap Block", "dtu/heap/maxalloc", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Lifetime Minimum Free Heap", "dtu/heap/minfree", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); - yield(); + publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE); + publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE); + publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE); + + publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); - publishInverterButton(inv, "Turn Inverter Off", "mdi:power-plug-off", "config", "", "cmd/power", "0"); - publishInverterButton(inv, "Turn Inverter On", "mdi:power-plug", "config", "", "cmd/power", "1"); - publishInverterButton(inv, "Restart Inverter", "", "config", "restart", "cmd/restart", "1"); + publishInverterButton(inv, "Turn Inverter Off", "cmd/power", "0", "mdi:power-plug-off", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterButton(inv, "Turn Inverter On", "cmd/power", "1", "mdi:power-plug", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterButton(inv, "Restart Inverter", "cmd/restart", "1", "", DEVICE_CLS_RESTART, STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterButton(inv, "Reset Radio Statistics", "cmd/reset_rf_stats", "1", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG); - publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100, 0.1); - publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100, 0.1); + publishInverterNumber(inv, "Limit NonPersistent Relative", "status/limit_relative", "cmd/limit_nonpersistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterNumber(inv, "Limit Persistent Relative", "status/limit_relative", "cmd/limit_persistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG); - publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); - publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); + publishInverterNumber(inv, "Limit NonPersistent Absolute", "status/limit_absolute", "cmd/limit_nonpersistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG); + publishInverterNumber(inv, "Limit Persistent Absolute", "status/limit_absolute", "cmd/limit_persistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG); - publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0"); - publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0"); + publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0", DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_NONE); + + publishInverterSensor(inv, "TX Requests", "radio/tx_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Success", "radio/rx_success", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Fail Receive Nothing", "radio/rx_fail_nothing", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Fail Receive Partial", "radio/rx_fail_partial", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Fail Receive Corrupt", "radio/rx_fail_corrupt", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "TX Re-Request Fragment", "radio/tx_re_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RSSI", "radio/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); // Loop all channels for (auto& t : inv->Statistics()->getChannelTypes()) { @@ -94,8 +111,6 @@ void MqttHandleHassClass::publishConfig() } } } - - yield(); } } @@ -128,8 +143,6 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr if (!clear) { const String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId); - const char* devCls = deviceClasses[fieldType.deviceClsId]; - const char* stateCls = stateClasses[fieldType.stateClsId]; String name; if (type != TYPE_DC) { @@ -138,46 +151,34 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr name = "CH" + chanNum + " " + fieldName; } + String unit_of_measure = inv->Statistics()->getChannelFieldUnit(type, channel, fieldType.fieldId); + JsonDocument root; + createInverterInfo(root, inv); + addCommonMetadata(root, unit_of_measure, "", fieldType.deviceClsId, fieldType.stateClsId, CATEGORY_NONE); root["name"] = name; root["stat_t"] = stateTopic; root["uniq_id"] = serial + "_ch" + chanNum + "_" + fieldName; - String unit_of_measure = inv->Statistics()->getChannelFieldUnit(type, channel, fieldType.fieldId); - if (unit_of_measure != "") { - root["unit_of_meas"] = unit_of_measure; - } - - createInverterInfo(root, inv); - if (Configuration.get().Mqtt.Hass.Expire) { root["exp_aft"] = Hoymiles.getNumInverters() * max(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold(); } - if (devCls != 0) { - root["dev_cla"] = devCls; - } - if (stateCls != 0) { - root["stat_cla"] = stateCls; - } - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - serializeJson(root, buffer); - publish(configTopic, buffer); + publish(configTopic, root); } else { publish(configTopic, ""); } } -void MqttHandleHassClass::publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload) +void MqttHandleHassClass::publishInverterButton( + std::shared_ptr inv, const String& name, const String& state_topic, const String& payload, + const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) { const String serial = inv->serialString(); - String buttonId = caption; + String buttonId = name; buttonId.replace(" ", "_"); buttonId.toLowerCase(); @@ -185,41 +186,30 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, - const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, - const int16_t min, const int16_t max, float step) + std::shared_ptr inv, const String& name, + const String& stateTopic, const String& command_topic, + const int16_t min, const int16_t max, float step, + const String& unit_of_measure, const String& icon, + const StateClassType state_class, const CategoryType category) { const String serial = inv->serialString(); - String buttonId = caption; + String buttonId = name; buttonId.replace(" ", "_"); buttonId.toLowerCase(); @@ -227,150 +217,22 @@ void MqttHandleHassClass::publishInverterNumber( + "/" + buttonId + "/config"; - const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic; + const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + command_topic; const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; JsonDocument root; + createInverterInfo(root, inv); + addCommonMetadata(root, unit_of_measure, icon, DEVICE_CLS_NONE, state_class, category); - root["name"] = caption; + root["name"] = name; root["uniq_id"] = serial + "_" + buttonId; - if (strcmp(icon, "")) { - root["ic"] = icon; - } - root["ent_cat"] = category; root["cmd_t"] = cmdTopic; root["stat_t"] = statTopic; - root["unit_of_meas"] = unitOfMeasure; root["min"] = min; root["max"] = max; root["step"] = step; - createInverterInfo(root, inv); - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - serializeJson(root, buffer); - publish(configTopic, buffer); -} - -void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off) -{ - const String serial = inv->serialString(); - - String sensorId = caption; - sensorId.replace(" ", "_"); - sensorId.toLowerCase(); - - const String configTopic = "binary_sensor/dtu_" + serial - + "/" + sensorId - + "/config"; - - const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; - - JsonDocument root; - - root["name"] = caption; - root["uniq_id"] = serial + "_" + sensorId; - root["stat_t"] = statTopic; - root["pl_on"] = payload_on; - root["pl_off"] = payload_off; - - createInverterInfo(root, inv); - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - serializeJson(root, buffer); - publish(configTopic, buffer); -} - -void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic) -{ - String id = name; - id.toLowerCase(); - id.replace(" ", "_"); - String topic = subTopic; - if (topic == "") { - topic = id; - } - - JsonDocument root; - - root["name"] = name; - root["uniq_id"] = getDtuUniqueId() + "_" + id; - if (strcmp(device_class, "")) { - root["dev_cla"] = device_class; - } - if (strcmp(category, "")) { - root["ent_cat"] = category; - } - if (strcmp(icon, "")) { - root["ic"] = icon; - } - if (strcmp(unit_of_measure, "")) { - root["unit_of_meas"] = unit_of_measure; - } - root["stat_t"] = MqttSettings.getPrefix() + "dtu" + "/" + topic; - - root["avty_t"] = MqttSettings.getPrefix() + Configuration.get().Mqtt.Lwt.Topic; - - const CONFIG_T& config = Configuration.get(); - root["pl_avail"] = config.Mqtt.Lwt.Value_Online; - root["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline; - - createDtuInfo(root); - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config"; - serializeJson(root, buffer); - publish(configTopic, buffer); -} - -void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic) -{ - String id = name; - id.toLowerCase(); - id.replace(" ", "_"); - - String topic = subTopic; - if (!strcmp(subTopic, "")) { - topic = String("dtu/") + "/" + id; - } - - JsonDocument root; - - root["name"] = name; - root["uniq_id"] = getDtuUniqueId() + "_" + id; - root["stat_t"] = MqttSettings.getPrefix() + topic; - root["pl_on"] = payload_on; - root["pl_off"] = payload_off; - - if (strcmp(device_class, "")) { - root["dev_cla"] = device_class; - } - if (strcmp(category, "")) { - root["ent_cat"] = category; - } - - createDtuInfo(root); - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - String buffer; - const String configTopic = "binary_sensor/" + getDtuUniqueId() + "/" + id + "/config"; - serializeJson(root, buffer); - publish(configTopic, buffer); + publish(configTopic, root); } void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr inv) @@ -433,4 +295,129 @@ void MqttHandleHassClass::publish(const String& subtopic, const String& payload) String topic = Configuration.get().Mqtt.Hass.Topic; topic += subtopic; MqttSettings.publishGeneric(topic, payload, Configuration.get().Mqtt.Hass.Retain); + yield(); +} + +void MqttHandleHassClass::publish(const String& subtopic, const JsonDocument& doc) +{ + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return; + } + String buffer; + serializeJson(doc, buffer); + publish(subtopic, buffer); +} + +void MqttHandleHassClass::addCommonMetadata( + JsonDocument& doc, + const String& unit_of_measure, const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + if (unit_of_measure != "") { + doc["unit_of_meas"] = unit_of_measure; + } + if (icon != "") { + doc["ic"] = icon; + } + if (device_class != DEVICE_CLS_NONE) { + doc["dev_cla"] = deviceClass_name[device_class]; + } + if (state_class != STATE_CLS_NONE) { + doc["stat_cla"] = stateClass_name[state_class];; + } + if (category != CATEGORY_NONE) { + doc["ent_cat"] = category_name[category]; + } +} + +void MqttHandleHassClass::publishBinarySensor( + JsonDocument& doc, + const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + String sensor_id = name; + sensor_id.toLowerCase(); + sensor_id.replace(" ", "_"); + + doc["name"] = name; + doc["uniq_id"] = unique_id_prefix + "_" + sensor_id; + doc["stat_t"] = MqttSettings.getPrefix() + state_topic; + doc["pl_on"] = payload_on; + doc["pl_off"] = payload_off; + + addCommonMetadata(doc, "", "", device_class, state_class, category); + + const String configTopic = "binary_sensor/" + root_device + "/" + sensor_id + "/config"; + publish(configTopic, doc); +} + +void MqttHandleHassClass::publishDtuBinarySensor( + const String& name, const String& state_topic, const String& payload_on, const String& payload_off, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + const String dtuId = getDtuUniqueId(); + + JsonDocument root; + createDtuInfo(root); + publishBinarySensor(root, dtuId, dtuId, name, state_topic, payload_on, payload_off, device_class, state_class, category); +} + +void MqttHandleHassClass::publishInverterBinarySensor( + std::shared_ptr inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + const String serial = inv->serialString(); + + JsonDocument root; + createInverterInfo(root, inv); + publishBinarySensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, payload_on, payload_off, device_class, state_class, category); +} + +void MqttHandleHassClass::publishSensor( + JsonDocument& doc, + const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, + const String& unit_of_measure, const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + String sensor_id = name; + sensor_id.toLowerCase(); + sensor_id.replace(" ", "_"); + + doc["name"] = name; + doc["uniq_id"] = unique_id_prefix + "_" + sensor_id; + doc["stat_t"] = MqttSettings.getPrefix() + state_topic; + + addCommonMetadata(doc, unit_of_measure, icon, device_class, state_class, category); + + const CONFIG_T& config = Configuration.get(); + doc["avty_t"] = MqttSettings.getPrefix() + config.Mqtt.Lwt.Topic; + doc["pl_avail"] = config.Mqtt.Lwt.Value_Online; + doc["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline; + + const String configTopic = "sensor/" + root_device + "/" + sensor_id + "/config"; + publish(configTopic, doc); +} + +void MqttHandleHassClass::publishDtuSensor( + const String& name, const String& state_topic, + const String& unit_of_measure, const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + const String dtuId = getDtuUniqueId(); + + JsonDocument root; + createDtuInfo(root); + publishSensor(root, dtuId, dtuId, name, state_topic, unit_of_measure, icon, device_class, state_class, category); +} + +void MqttHandleHassClass::publishInverterSensor( + std::shared_ptr inv, const String& name, const String& state_topic, + const String& unit_of_measure, const String& icon, + const DeviceClassType device_class, const StateClassType state_class, const CategoryType category) +{ + const String serial = inv->serialString(); + + JsonDocument root; + createInverterInfo(root, inv); + publishSensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, unit_of_measure, icon, device_class, state_class, category); } diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index d099b444..70a7222d 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -7,13 +7,6 @@ #include "MqttSettings.h" #include -#define TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE "limit_persistent_relative" -#define TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE "limit_persistent_absolute" -#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative" -#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute" -#define TOPIC_SUB_POWER "power" -#define TOPIC_SUB_RESTART "restart" - #define PUBLISH_MAX_INTERVAL 60000 MqttHandleInverterClass MqttHandleInverter; @@ -50,6 +43,15 @@ void MqttHandleInverterClass::loop() // Name MqttSettings.publish(subtopic + "/name", inv->name()); + // Radio Statistics + MqttSettings.publish(subtopic + "/radio/tx_request", String(inv->RadioStats.TxRequestData)); + MqttSettings.publish(subtopic + "/radio/tx_re_request", String(inv->RadioStats.TxReRequestFragment)); + MqttSettings.publish(subtopic + "/radio/rx_success", String(inv->RadioStats.RxSuccess)); + MqttSettings.publish(subtopic + "/radio/rx_fail_nothing", String(inv->RadioStats.RxFailNoAnswer)); + MqttSettings.publish(subtopic + "/radio/rx_fail_partial", String(inv->RadioStats.RxFailPartialAnswer)); + MqttSettings.publish(subtopic + "/radio/rx_fail_corrupt", String(inv->RadioStats.RxFailCorruptData)); + MqttSettings.publish(subtopic + "/radio/rssi", String(inv->getLastRssi())); + if (inv->DevInfo()->getLastUpdate() > 0) { // Bootloader Version MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); @@ -146,7 +148,7 @@ String MqttHandleInverterClass::getTopic(std::shared_ptr inv, return inv->serialString() + "/" + chanNum + "/" + chanName; } -void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total) +void MqttHandleInverterClass::onMqttMessage(Topic t, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total) { const CONFIG_T& config = Configuration.get(); @@ -154,15 +156,11 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* char* serial_str; - char* subtopic; - char* setting; char* rest = &token_topic[strlen(config.Mqtt.Topic)]; serial_str = strtok_r(rest, "/", &rest); - subtopic = strtok_r(rest, "/", &rest); - setting = strtok_r(rest, "/", &rest); - if (serial_str == NULL || subtopic == NULL || setting == NULL) { + if (serial_str == NULL) { return; } @@ -175,33 +173,30 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro return; } - // check if subtopic is unequal cmd - if (strcmp(subtopic, "cmd")) { + std::string strValue(reinterpret_cast(payload), len); + float payload_val = -1; + try { + payload_val = std::stof(strValue); + } catch (std::invalid_argument const& e) { + MessageOutput.printf("MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n", + topic, strValue.c_str()); return; } - char* strlimit = new char[len + 1]; - memcpy(strlimit, payload, len); - strlimit[len] = '\0'; - const float payload_val = strtof(strlimit, NULL); - delete[] strlimit; - - if (payload_val < 0) { - MessageOutput.printf("MQTT payload < 0 received --> ignoring\r\n"); - return; - } - - if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) { + switch (t) { + case Topic::LimitPersistentRelative: // Set inverter limit relative persistent MessageOutput.printf("Limit Persistent: %.1f %%\r\n", payload_val); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativPersistent); + break; - } else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) { + case Topic::LimitPersistentAbsolute: // Set inverter limit absolute persistent MessageOutput.printf("Limit Persistent: %.1f W\r\n", payload_val); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutPersistent); + break; - } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { + case Topic::LimitNonPersistentRelative: // Set inverter limit relative non persistent MessageOutput.printf("Limit Non-Persistent: %.1f %%\r\n", payload_val); if (!properties.retain) { @@ -209,8 +204,9 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else { MessageOutput.println("Ignored because retained"); } + break; - } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) { + case Topic::LimitNonPersistentAbsolute: // Set inverter limit absolute non persistent MessageOutput.printf("Limit Non-Persistent: %.1f W\r\n", payload_val); if (!properties.retain) { @@ -218,13 +214,15 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else { MessageOutput.println("Ignored because retained"); } + break; - } else if (!strcmp(setting, TOPIC_SUB_POWER)) { + case Topic::Power: // Turn inverter on or off - MessageOutput.printf("Set inverter power to: %d\r\n", static_cast(payload_val)); + MessageOutput.printf("Set inverter power to: %" PRId32 "\r\n", static_cast(payload_val)); inv->sendPowerControlRequest(static_cast(payload_val) > 0); + break; - } else if (!strcmp(setting, TOPIC_SUB_RESTART)) { + case Topic::Restart: // Restart inverter MessageOutput.printf("Restart inverter\r\n"); if (!properties.retain && payload_val == 1) { @@ -232,34 +230,41 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else { MessageOutput.println("Ignored because retained or numeric value not '1'"); } + break; + + case Topic::ResetRfStats: + // Reset RF Stats + MessageOutput.printf("Reset RF stats\r\n"); + if (!properties.retain && payload_val == 1) { + inv->resetRadioStats(); + } else { + MessageOutput.println("Ignored because retained or numeric value not '1'"); + } } } void MqttHandleInverterClass::subscribeTopics() { - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; + String const& prefix = MqttSettings.getPrefix(); - const String topic = MqttSettings.getPrefix(); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + auto subscribe = [&prefix, this](char const* subTopic, Topic t) { + String fullTopic(prefix + _cmdtopic.data() + subTopic); + MqttSettings.subscribe(fullTopic.c_str(), 0, + std::bind(&MqttHandleInverterClass::onMqttMessage, this, t, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6)); + }; + + for (auto const& s : _subscriptions) { + subscribe(s.first.data(), s.second); + } } void MqttHandleInverterClass::unsubscribeTopics() { - const String topic = MqttSettings.getPrefix(); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER)); - MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART)); + String const& prefix = MqttSettings.getPrefix() + _cmdtopic.data(); + for (auto const& s : _subscriptions) { + MqttSettings.unsubscribe(prefix + s.first.data()); + } } diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index 55ea428e..b50fa986 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -7,10 +7,10 @@ #include "MessageOutput.h" #include "PinMapping.h" #include "Utils.h" +#include "__compiled_constants.h" #include "defaults.h" #include #include -#include "__compiled_constants.h" NetworkSettingsClass::NetworkSettingsClass() : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&NetworkSettingsClass::loop, this)) @@ -23,20 +23,41 @@ NetworkSettingsClass::NetworkSettingsClass() void NetworkSettingsClass::init(Scheduler& scheduler) { using std::placeholders::_1; + using std::placeholders::_2; WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); WiFi.disconnect(true, true); - WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1)); + WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1, _2)); + + if (PinMapping.isValidW5500Config()) { + PinMapping_t& pin = PinMapping.get(); + _w5500 = W5500::setup(pin.w5500_mosi, pin.w5500_miso, pin.w5500_sclk, pin.w5500_cs, pin.w5500_int, pin.w5500_rst); + if (_w5500) + MessageOutput.println("W5500: Connection successful"); + else + MessageOutput.println("W5500: Connection error!!"); + } +#if CONFIG_ETH_USE_ESP32_EMAC + else if (PinMapping.isValidEthConfig()) { + PinMapping_t& pin = PinMapping.get(); +#if ESP_ARDUINO_VERSION_MAJOR < 3 + ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode); +#else + ETH.begin(pin.eth_type, pin.eth_phy_addr, pin.eth_mdc, pin.eth_mdio, pin.eth_power, pin.eth_clk_mode); +#endif + } +#endif + setupMode(); scheduler.addTask(_loopTask); _loopTask.enable(); } -void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) +void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info) { switch (event) { case ARDUINO_EVENT_ETH_START: @@ -76,7 +97,8 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) } break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - MessageOutput.println("WiFi disconnected"); + // Reason codes can be found here: https://github.com/espressif/esp-idf/blob/5454d37d496a8c58542eb450467471404c606501/components/esp_wifi/include/esp_wifi_types_generic.h#L79-L141 + MessageOutput.printf("WiFi disconnected: %" PRId8 "\r\n", info.wifi_sta_disconnected.reason); if (_networkMode == network_mode::WiFi) { MessageOutput.println("Try reconnecting"); WiFi.disconnect(true, false); @@ -95,12 +117,12 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) } } -bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event event) +bool NetworkSettingsClass::onEvent(DtuNetworkEventCb cbEvent, const network_event event) { if (!cbEvent) { return pdFALSE; } - NetworkEventCbList_t newEventHandler; + DtuNetworkEventCbList_t newEventHandler; newEventHandler.cb = cbEvent; newEventHandler.event = event; _cbEventList.push_back(newEventHandler); @@ -109,8 +131,7 @@ bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event e void NetworkSettingsClass::raiseEvent(const network_event event) { - for (uint32_t i = 0; i < _cbEventList.size(); i++) { - const NetworkEventCbList_t entry = _cbEventList[i]; + for (auto& entry : _cbEventList) { if (entry.cb) { if (entry.event == event || entry.event == network_event::NETWORK_EVENT_MAX) { entry.cb(event); @@ -167,11 +188,6 @@ void NetworkSettingsClass::setupMode() WiFi.mode(WIFI_MODE_NULL); } } - - if (PinMapping.isValidEthConfig()) { - PinMapping_t& pin = PinMapping.get(); - ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode); - } } void NetworkSettingsClass::enableAdminMode() @@ -210,7 +226,7 @@ void NetworkSettingsClass::loop() if (_adminEnabled && _adminTimeoutCounterMax > 0) { _adminTimeoutCounter++; if (_adminTimeoutCounter % 10 == 0) { - MessageOutput.printf("Admin AP remaining seconds: %d / %d\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax); + MessageOutput.printf("Admin AP remaining seconds: %" PRId32 " / %" PRId32 "\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax); } } _connectTimeoutTimer++; @@ -399,6 +415,9 @@ String NetworkSettingsClass::macAddress() const { switch (_networkMode) { case network_mode::Ethernet: + if (_w5500) { + return _w5500->macAddress(); + } return ETH.macAddress(); break; case network_mode::WiFi: diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 74f28285..bd5ea995 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -84,6 +84,58 @@ #define CMT_SDIO -1 #endif +#ifndef W5500_MOSI +#define W5500_MOSI -1 +#endif + +#ifndef W5500_MISO +#define W5500_MISO -1 +#endif + +#ifndef W5500_SCLK +#define W5500_SCLK -1 +#endif + +#ifndef W5500_CS +#define W5500_CS -1 +#endif + +#ifndef W5500_INT +#define W5500_INT -1 +#endif + +#ifndef W5500_RST +#define W5500_RST -1 +#endif + +#if CONFIG_ETH_USE_ESP32_EMAC + +#ifndef ETH_PHY_ADDR +#define ETH_PHY_ADDR -1 +#endif + +#ifndef ETH_PHY_POWER +#define ETH_PHY_POWER -1 +#endif + +#ifndef ETH_PHY_MDC +#define ETH_PHY_MDC -1 +#endif + +#ifndef ETH_PHY_MDIO +#define ETH_PHY_MDIO -1 +#endif + +#ifndef ETH_PHY_TYPE +#define ETH_PHY_TYPE ETH_PHY_LAN8720 +#endif + +#ifndef ETH_CLK_MODE +#define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN +#endif + +#endif + PinMappingClass PinMapping; PinMappingClass::PinMappingClass() @@ -103,18 +155,26 @@ PinMappingClass::PinMappingClass() _pinMapping.cmt_gpio3 = CMT_GPIO3; _pinMapping.cmt_sdio = CMT_SDIO; + _pinMapping.w5500_mosi = W5500_MOSI; + _pinMapping.w5500_miso = W5500_MISO; + _pinMapping.w5500_sclk = W5500_SCLK; + _pinMapping.w5500_cs = W5500_CS; + _pinMapping.w5500_int = W5500_INT; + _pinMapping.w5500_rst = W5500_RST; + +#if CONFIG_ETH_USE_ESP32_EMAC #ifdef OPENDTU_ETHERNET _pinMapping.eth_enabled = true; #else _pinMapping.eth_enabled = false; #endif - _pinMapping.eth_phy_addr = ETH_PHY_ADDR; _pinMapping.eth_power = ETH_PHY_POWER; _pinMapping.eth_mdc = ETH_PHY_MDC; _pinMapping.eth_mdio = ETH_PHY_MDIO; _pinMapping.eth_type = ETH_PHY_TYPE; _pinMapping.eth_clk_mode = ETH_CLK_MODE; +#endif _pinMapping.display_type = DISPLAY_TYPE; _pinMapping.display_data = DISPLAY_DATA; @@ -164,18 +224,26 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.cmt_gpio3 = doc[i]["cmt"]["gpio3"] | CMT_GPIO3; _pinMapping.cmt_sdio = doc[i]["cmt"]["sdio"] | CMT_SDIO; + _pinMapping.w5500_mosi = doc[i]["w5500"]["mosi"] | W5500_MOSI; + _pinMapping.w5500_miso = doc[i]["w5500"]["miso"] | W5500_MISO; + _pinMapping.w5500_sclk = doc[i]["w5500"]["sclk"] | W5500_SCLK; + _pinMapping.w5500_cs = doc[i]["w5500"]["cs"] | W5500_CS; + _pinMapping.w5500_int = doc[i]["w5500"]["int"] | W5500_INT; + _pinMapping.w5500_rst = doc[i]["w5500"]["rst"] | W5500_RST; + +#if CONFIG_ETH_USE_ESP32_EMAC #ifdef OPENDTU_ETHERNET _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true; #else _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | false; #endif - _pinMapping.eth_phy_addr = doc[i]["eth"]["phy_addr"] | ETH_PHY_ADDR; _pinMapping.eth_power = doc[i]["eth"]["power"] | ETH_PHY_POWER; _pinMapping.eth_mdc = doc[i]["eth"]["mdc"] | ETH_PHY_MDC; _pinMapping.eth_mdio = doc[i]["eth"]["mdio"] | ETH_PHY_MDIO; _pinMapping.eth_type = doc[i]["eth"]["type"] | ETH_PHY_TYPE; _pinMapping.eth_clk_mode = doc[i]["eth"]["clk_mode"] | ETH_CLK_MODE; +#endif _pinMapping.display_type = doc[i]["display"]["type"] | DISPLAY_TYPE; _pinMapping.display_data = doc[i]["display"]["data"] | DISPLAY_DATA; @@ -211,7 +279,21 @@ bool PinMappingClass::isValidCmt2300Config() const && _pinMapping.cmt_sdio >= 0; } +bool PinMappingClass::isValidW5500Config() const +{ + return _pinMapping.w5500_mosi >= 0 + && _pinMapping.w5500_miso >= 0 + && _pinMapping.w5500_sclk >= 0 + && _pinMapping.w5500_cs >= 0 + && _pinMapping.w5500_int >= 0 + && _pinMapping.w5500_rst >= 0; +} + +#if CONFIG_ETH_USE_ESP32_EMAC bool PinMappingClass::isValidEthConfig() const { - return _pinMapping.eth_enabled; + return _pinMapping.eth_enabled + && _pinMapping.eth_mdc >= 0 + && _pinMapping.eth_mdio >= 0; } +#endif diff --git a/src/RestartHelper.cpp b/src/RestartHelper.cpp new file mode 100644 index 00000000..ab385ef6 --- /dev/null +++ b/src/RestartHelper.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ +#include "RestartHelper.h" +#include "Display_Graphic.h" +#include "Led_Single.h" +#include + +RestartHelperClass RestartHelper; + +RestartHelperClass::RestartHelperClass() + : _rebootTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&RestartHelperClass::loop, this)) +{ +} + +void RestartHelperClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_rebootTask); +} + +void RestartHelperClass::triggerRestart() +{ + _rebootTask.enable(); + _rebootTask.restart(); +} + +void RestartHelperClass::loop() +{ + if (_rebootTask.isFirstIteration()) { + LedSingle.turnAllOff(); + Display.setStatus(false); + } else { + ESP.restart(); + } +} diff --git a/src/Utils.cpp b/src/Utils.cpp index 6abe4dd1..16b63985 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -4,10 +4,8 @@ */ #include "Utils.h" -#include "Display_Graphic.h" -#include "Led_Single.h" #include "MessageOutput.h" -#include +#include "PinMapping.h" #include uint32_t Utils::getChipId() @@ -59,20 +57,10 @@ int Utils::getTimezoneOffset() return static_cast(difftime(rawtime, gmt)); } -void Utils::restartDtu() -{ - LedSingle.turnAllOff(); - Display.setStatus(false); - yield(); - delay(1000); - yield(); - ESP.restart(); -} - bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line) { if (doc.overflowed()) { - MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line); + MessageOutput.printf("Alloc failed: %s, %" PRId16 "\r\n", function, line); return false; } diff --git a/src/W5500.cpp b/src/W5500.cpp new file mode 100644 index 00000000..bf539434 --- /dev/null +++ b/src/W5500.cpp @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ + +#include "W5500.h" + +#include +#include + +// Internal Arduino functions from WiFiGeneric +void tcpipInit(); +void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif); + +W5500::W5500(spi_device_handle_t spi, gpio_num_t pin_int) + : eth_handle(nullptr) + , eth_netif(nullptr) +{ + // Arduino function to start networking stack if not already started + tcpipInit(); + + ESP_ERROR_CHECK(tcpip_adapter_set_default_eth_handlers()); + + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi); + w5500_config.int_gpio_num = pin_int; + + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + mac_config.rx_task_stack_size = 4096; + esp_eth_mac_t* mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.reset_gpio_num = -1; + esp_eth_phy_t* phy = esp_eth_phy_new_w5500(&phy_config); + + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + ESP_ERROR_CHECK(esp_eth_driver_install(ð_config, ð_handle)); + + // Configure MAC address + uint8_t mac_addr[6]; + ESP_ERROR_CHECK(esp_read_mac(mac_addr, ESP_MAC_ETH)); + ESP_ERROR_CHECK(esp_eth_ioctl(eth_handle, ETH_CMD_S_MAC_ADDR, mac_addr)); + + esp_netif_config_t netif_config = ESP_NETIF_DEFAULT_ETH(); + eth_netif = esp_netif_new(&netif_config); + + ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle))); + + // Add to Arduino + add_esp_interface_netif(ESP_IF_ETH, eth_netif); + + ESP_ERROR_CHECK(esp_eth_start(eth_handle)); +} + +W5500::~W5500() +{ + // TODO(LennartF22): support cleanup at some point? +} + +std::unique_ptr W5500::setup(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) +{ + gpio_reset_pin(static_cast(pin_rst)); + gpio_set_level(static_cast(pin_rst), 0); + gpio_set_direction(static_cast(pin_rst), GPIO_MODE_OUTPUT); + + gpio_reset_pin(static_cast(pin_cs)); + gpio_reset_pin(static_cast(pin_int)); + + auto bus_config = std::make_shared( + static_cast(pin_mosi), + static_cast(pin_miso), + static_cast(pin_sclk)); + + spi_device_interface_config_t device_config { + .command_bits = 16, // actually address phase + .address_bits = 8, // actually command phase + .dummy_bits = 0, + .mode = 0, + .duty_cycle_pos = 0, + .cs_ena_pretrans = 0, // only 0 supported + .cs_ena_posttrans = 0, // only 0 supported + .clock_speed_hz = 20000000, // stable with OpenDTU Fusion shield + .input_delay_ns = 0, + .spics_io_num = pin_cs, + .flags = 0, + .queue_size = 20, + .pre_cb = nullptr, + .post_cb = nullptr, + }; + + spi_device_handle_t spi = SpiManagerInst.alloc_device("", bus_config, device_config); + if (!spi) + return nullptr; + + // Reset sequence + delayMicroseconds(500); + gpio_set_level(static_cast(pin_rst), 1); + delayMicroseconds(1000); + + if (!connection_check_spi(spi)) + return nullptr; + if (!connection_check_interrupt(static_cast(pin_int))) + return nullptr; + + // Use Arduino functions to temporarily attach interrupt to enable the GPIO ISR service + // (if we used ESP-IDF functions, a warning would be printed the first time anyone uses attachInterrupt) + attachInterrupt(pin_int, nullptr, FALLING); + detachInterrupt(pin_int); + + // Return to default state once again after connection check and temporary interrupt registration + gpio_reset_pin(static_cast(pin_int)); + + return std::unique_ptr(new W5500(spi, static_cast(pin_int))); +} + +String W5500::macAddress() +{ + uint8_t mac_addr[6] = {}; + esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr); + + char mac_addr_str[18]; + snprintf( + mac_addr_str, sizeof(mac_addr_str), "%02X:%02X:%02X:%02X:%02X:%02X", + mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); + return String(mac_addr_str); +} + +bool W5500::connection_check_spi(spi_device_handle_t spi) +{ + spi_transaction_t trans = { + .flags = SPI_TRANS_USE_RXDATA, + .cmd = 0x0039, // actually address (VERSIONR) + .addr = (0b00000 << 3) | (0 << 2) | (0b00 < 0), // actually command (common register, read, VDM) + .length = 8, + .rxlength = 8, + .user = nullptr, + .tx_buffer = nullptr, + .rx_data = {}, + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans)); + + // Version number (VERSIONR) is always 0x04 + return *reinterpret_cast(&trans.rx_data) == 0x04; +} + +bool W5500::connection_check_interrupt(gpio_num_t pin_int) +{ + gpio_set_direction(pin_int, GPIO_MODE_INPUT); + gpio_set_pull_mode(pin_int, GPIO_PULLDOWN_ONLY); + int level = gpio_get_level(pin_int); + + // Interrupt line must be high + return level == 1; +} diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 1a5b2870..b3255c41 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -39,6 +39,12 @@ void WebApiClass::init(Scheduler& scheduler) _server.begin(); } +void WebApiClass::reload() +{ + _webApiWsConsole.reload(); + _webApiWsLive.reload(); +} + bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) { CONFIG_T& config = Configuration.get(); @@ -131,7 +137,7 @@ bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResp root["code"] = WebApiError::GenericInternalServerError; root["type"] = "danger"; response->setCode(500); - MessageOutput.printf("WebResponse failed: %s, %d\r\n", function, line); + MessageOutput.printf("WebResponse failed: %s, %" PRId16 "\r\n", function, line); ret_val = false; } diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 759b6b24..51a7aab1 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -4,6 +4,7 @@ */ #include "WebApi_config.h" #include "Configuration.h" +#include "RestartHelper.h" #include "Utils.h" #include "WebApi.h" #include "WebApi_errors.h" @@ -61,7 +62,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("delete"))) { + if (!(root["delete"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -82,7 +83,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); Utils::removeAllFiles(); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) @@ -124,7 +125,7 @@ void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) response->addHeader("Connection", "close"); response->addHeader("Access-Control-Allow-Origin", "*"); request->send(response); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 078d5b4a..29686fe0 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -6,7 +6,7 @@ #include "Configuration.h" #include "Display_Graphic.h" #include "PinMapping.h" -#include "Utils.h" +#include "RestartHelper.h" #include "WebApi.h" #include "WebApi_errors.h" #include "helper.h" @@ -50,6 +50,15 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) cmtPinObj["gpio2"] = pin.cmt_gpio2; cmtPinObj["gpio3"] = pin.cmt_gpio3; + auto w5500PinObj = curPin["w5500"].to(); + w5500PinObj["sclk"] = pin.w5500_sclk; + w5500PinObj["mosi"] = pin.w5500_mosi; + w5500PinObj["miso"] = pin.w5500_miso; + w5500PinObj["cs"] = pin.w5500_cs; + w5500PinObj["int"] = pin.w5500_int; + w5500PinObj["rst"] = pin.w5500_rst; + +#if CONFIG_ETH_USE_ESP32_EMAC auto ethPinObj = curPin["eth"].to(); ethPinObj["enabled"] = pin.eth_enabled; ethPinObj["phy_addr"] = pin.eth_phy_addr; @@ -58,6 +67,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) ethPinObj["mdio"] = pin.eth_mdio; ethPinObj["type"] = pin.eth_type; ethPinObj["clk_mode"] = pin.eth_clk_mode; +#endif auto displayPinObj = curPin["display"].to(); displayPinObj["type"] = pin.display_type; @@ -103,8 +113,8 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("curPin") - || root.containsKey("display"))) { + if (!(root["curPin"].is() + || root["display"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -149,6 +159,6 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); if (performRestart) { - Utils::restartDtu(); + RestartHelper.triggerRestart(); } } diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 9b67ec39..7c6c3f73 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -49,7 +49,7 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) // DTU Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0x%08x", + snprintf(buffer, sizeof(buffer), "%0" PRIx32 "%08" PRIx32, ((uint32_t)((config.Dtu.Serial >> 32) & 0xFFFFFFFF)), ((uint32_t)(config.Dtu.Serial & 0xFFFFFFFF))); root["serial"] = buffer; @@ -90,12 +90,12 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && root.containsKey("pollinterval") - && root.containsKey("nrf_palevel") - && root.containsKey("cmt_palevel") - && root.containsKey("cmt_frequency") - && root.containsKey("cmt_country"))) { + if (!(root["serial"].is() + && root["pollinterval"].is() + && root["nrf_palevel"].is() + && root["cmt_palevel"].is() + && root["cmt_frequency"].is() + && root["cmt_country"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 9491f935..b179eb10 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -4,6 +4,7 @@ */ #include "WebApi_firmware.h" #include "Configuration.h" +#include "RestartHelper.h" #include "Update.h" #include "Utils.h" #include "WebApi.h" @@ -37,7 +38,7 @@ void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) response->addHeader("Connection", "close"); response->addHeader("Access-Control-Allow-Origin", "*"); request->send(response); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 5a8585f7..ef353158 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -21,6 +21,7 @@ void WebApiInverterClass::init(AsyncWebServer& server, Scheduler& scheduler) server.on("/api/inverter/edit", HTTP_POST, std::bind(&WebApiInverterClass::onInverterEdit, this, _1)); server.on("/api/inverter/del", HTTP_POST, std::bind(&WebApiInverterClass::onInverterDelete, this, _1)); server.on("/api/inverter/order", HTTP_POST, std::bind(&WebApiInverterClass::onInverterOrder, this, _1)); + server.on("/api/inverter/stats_reset", HTTP_GET, std::bind(&WebApiInverterClass::onInverterStatReset, this, _1)); } void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) @@ -44,7 +45,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) // Inverter Serial is read as HEX char buffer[sizeof(uint64_t) * 8 + 1]; - snprintf(buffer, sizeof(buffer), "%0x%08x", + snprintf(buffer, sizeof(buffer), "%0" PRIx32 "%08" PRIx32, ((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)), ((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF))); obj["serial"] = buffer; @@ -95,8 +96,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && root.containsKey("name"))) { + if (!(root["serial"].is() + && root["name"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -165,7 +166,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) { + if (!(root["id"].is() + && root["serial"].is() + && root["name"].is() + && root["channel"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -281,7 +285,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("id"))) { + if (!(root["id"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -323,7 +327,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("order"))) { + if (!(root["order"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -346,3 +350,24 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } + +void WebApiInverterClass::onInverterStatReset(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto retMsg = response->getRoot(); + auto serial = WebApi.parseSerialFromRequest(request); + auto inv = Hoymiles.getInverterBySerial(serial); + + if (inv != nullptr) { + inv->resetRadioStats(); + retMsg["type"] = "success"; + retMsg["message"] = "Stats resetted"; + retMsg["code"] = WebApiError::InverterStatsResetted; + } + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 6a6c90ca..e3f53ae0 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -64,9 +64,9 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && root.containsKey("limit_value") - && root.containsKey("limit_type"))) { + if (!(root["serial"].is() + && root["limit_value"].is() + && root["limit_type"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index 1504f9d7..1835138f 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -4,7 +4,7 @@ */ #include "WebApi_maintenance.h" -#include "Utils.h" +#include "RestartHelper.h" #include "WebApi.h" #include "WebApi_errors.h" #include @@ -30,7 +30,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("reboot"))) { + if (!(root["reboot"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -43,7 +43,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::MaintenanceRebootTriggered; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - Utils::restartDtu(); + RestartHelper.triggerRestart(); } else { retMsg["message"] = "Reboot cancled!"; retMsg["code"] = WebApiError::MaintenanceRebootCancled; diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index af40643e..0c8ee5c6 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -107,29 +107,29 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("mqtt_enabled") - && root.containsKey("mqtt_hostname") - && root.containsKey("mqtt_port") - && root.containsKey("mqtt_clientid") - && root.containsKey("mqtt_username") - && root.containsKey("mqtt_password") - && root.containsKey("mqtt_topic") - && root.containsKey("mqtt_retain") - && root.containsKey("mqtt_tls") - && root.containsKey("mqtt_tls_cert_login") - && root.containsKey("mqtt_client_cert") - && root.containsKey("mqtt_client_key") - && root.containsKey("mqtt_lwt_topic") - && root.containsKey("mqtt_lwt_online") - && root.containsKey("mqtt_lwt_offline") - && root.containsKey("mqtt_lwt_qos") - && root.containsKey("mqtt_publish_interval") - && root.containsKey("mqtt_clean_session") - && root.containsKey("mqtt_hass_enabled") - && root.containsKey("mqtt_hass_expire") - && root.containsKey("mqtt_hass_retain") - && root.containsKey("mqtt_hass_topic") - && root.containsKey("mqtt_hass_individualpanels"))) { + if (!(root["mqtt_enabled"].is() + && root["mqtt_hostname"].is() + && root["mqtt_port"].is() + && root["mqtt_clientid"].is() + && root["mqtt_username"].is() + && root["mqtt_password"].is() + && root["mqtt_topic"].is() + && root["mqtt_retain"].is() + && root["mqtt_tls"].is() + && root["mqtt_tls_cert_login"].is() + && root["mqtt_client_cert"].is() + && root["mqtt_client_key"].is() + && root["mqtt_lwt_topic"].is() + && root["mqtt_lwt_online"].is() + && root["mqtt_lwt_offline"].is() + && root["mqtt_lwt_qos"].is() + && root["mqtt_publish_interval"].is() + && root["mqtt_clean_session"].is() + && root["mqtt_hass_enabled"].is() + && root["mqtt_hass_expire"].is() + && root["mqtt_hass_retain"].is() + && root["mqtt_hass_topic"].is() + && root["mqtt_hass_individualpanels"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 7fec44b2..75275755 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -88,16 +88,16 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("ssid") - && root.containsKey("password") - && root.containsKey("hostname") - && root.containsKey("dhcp") - && root.containsKey("ipaddress") - && root.containsKey("netmask") - && root.containsKey("gateway") - && root.containsKey("dns1") - && root.containsKey("dns2") - && root.containsKey("aptimeout"))) { + if (!(root["ssid"].is() + && root["password"].is() + && root["hostname"].is() + && root["dhcp"].is() + && root["ipaddress"].is() + && root["netmask"].is() + && root["gateway"].is() + && root["dns1"].is() + && root["dns2"].is() + && root["aptimeout"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index d50e0f02..5dc874b5 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -100,11 +100,11 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("ntp_server") - && root.containsKey("ntp_timezone") - && root.containsKey("longitude") - && root.containsKey("latitude") - && root.containsKey("sunsettype"))) { + if (!(root["ntp_server"].is() + && root["ntp_timezone"].is() + && root["longitude"].is() + && root["latitude"].is() + && root["sunsettype"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -193,12 +193,12 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("year") - && root.containsKey("month") - && root.containsKey("day") - && root.containsKey("hour") - && root.containsKey("minute") - && root.containsKey("second"))) { + if (!(root["year"].is() + && root["month"].is() + && root["day"].is() + && root["hour"].is() + && root["minute"].is() + && root["second"].is())) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index b2b2ce42..83e7fac6 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -57,9 +57,9 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") - && (root.containsKey("power") - || root.containsKey("restart")))) { + if (!(root["serial"].is() + && (root["power"].is() + || root["restart"].is()))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -84,8 +84,8 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) return; } - if (root.containsKey("power")) { - uint16_t power = root["power"].as(); + if (root["power"].is()) { + bool power = root["power"].as(); inv->sendPowerControlRequest(power); } else { if (root["restart"].as()) { diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index ad95aacb..9a79466a 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -42,23 +42,23 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->print("# HELP opendtu_heap_size System memory size\n"); stream->print("# TYPE opendtu_heap_size gauge\n"); - stream->printf("opendtu_heap_size %zu\n", ESP.getHeapSize()); + stream->printf("opendtu_heap_size %" PRId32 "\n", ESP.getHeapSize()); stream->print("# HELP opendtu_free_heap_size System free memory\n"); stream->print("# TYPE opendtu_free_heap_size gauge\n"); - stream->printf("opendtu_free_heap_size %zu\n", ESP.getFreeHeap()); + stream->printf("opendtu_free_heap_size %" PRId32 "\n", ESP.getFreeHeap()); stream->print("# HELP opendtu_biggest_heap_block Biggest free heap block\n"); stream->print("# TYPE opendtu_biggest_heap_block gauge\n"); - stream->printf("opendtu_biggest_heap_block %zu\n", ESP.getMaxAllocHeap()); + stream->printf("opendtu_biggest_heap_block %" PRId32 "\n", ESP.getMaxAllocHeap()); stream->print("# HELP opendtu_heap_min_free Minimum free memory since boot\n"); stream->print("# TYPE opendtu_heap_min_free gauge\n"); - stream->printf("opendtu_heap_min_free %zu\n", ESP.getMinFreeHeap()); + stream->printf("opendtu_heap_min_free %" PRId32 "\n", ESP.getMinFreeHeap()); stream->print("# HELP wifi_rssi WiFi RSSI\n"); stream->print("# TYPE wifi_rssi gauge\n"); - stream->printf("wifi_rssi %d\n", WiFi.RSSI()); + stream->printf("wifi_rssi %" PRId8 "\n", WiFi.RSSI()); stream->print("# HELP wifi_station WiFi Station info\n"); stream->print("# TYPE wifi_station gauge\n"); @@ -73,14 +73,14 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->print("# HELP opendtu_last_update last update from inverter in s\n"); stream->print("# TYPE opendtu_last_update gauge\n"); } - stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\n", + stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %" PRId32 "\n", serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000); if (i == 0) { stream->print("# HELP opendtu_inverter_limit_relative current relative limit of the inverter\n"); stream->print("# TYPE opendtu_inverter_limit_relative gauge\n"); } - stream->printf("opendtu_inverter_limit_relative{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n", + stream->printf("opendtu_inverter_limit_relative{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %f\n", serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() / 100.0); if (inv->DevInfo()->getMaxPower() > 0) { @@ -88,7 +88,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->print("# HELP opendtu_inverter_limit_absolute current relative limit of the inverter\n"); stream->print("# TYPE opendtu_inverter_limit_absolute gauge\n"); } - stream->printf("opendtu_inverter_limit_absolute{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n", + stream->printf("opendtu_inverter_limit_absolute{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %f\n", serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0); } @@ -126,7 +126,7 @@ void WebApiPrometheusClass::addField(AsyncResponseStream* stream, const String& stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); stream->printf("# TYPE opendtu_%s %s\n", chanName, metricName); } - stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n", + stream->printf("opendtu_%s{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n", chanName, serial.c_str(), idx, @@ -150,7 +150,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri stream->print("# HELP opendtu_PanelInfo panel information\n"); stream->print("# TYPE opendtu_PanelInfo gauge\n"); } - stream->printf("opendtu_PanelInfo{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\",panelname=\"%s\"} 1\n", + stream->printf("opendtu_PanelInfo{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%d\",panelname=\"%s\"} 1\n", serial.c_str(), idx, inv->name(), @@ -161,7 +161,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri stream->print("# HELP opendtu_MaxPower panel maximum output power\n"); stream->print("# TYPE opendtu_MaxPower gauge\n"); } - stream->printf("opendtu_MaxPower{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %d\n", + stream->printf("opendtu_MaxPower{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%d\"} %d\n", serial.c_str(), idx, inv->name(), @@ -172,7 +172,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri stream->print("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n"); stream->print("# TYPE opendtu_YieldTotalOffset gauge\n"); } - stream->printf("opendtu_YieldTotalOffset{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %f\n", + stream->printf("opendtu_YieldTotalOffset{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%" PRId16 "\"} %f\n", serial.c_str(), idx, inv->name(), diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index eb0f27d2..6be21ca6 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -48,8 +48,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!root.containsKey("password") - && root.containsKey("allow_readonly")) { + if (!root["password"].is() + && root["allow_readonly"].is()) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); @@ -71,6 +71,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + + WebApi.reload(); } void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) diff --git a/src/WebApi_ws_console.cpp b/src/WebApi_ws_console.cpp index 1f1efcb2..51035f6f 100644 --- a/src/WebApi_ws_console.cpp +++ b/src/WebApi_ws_console.cpp @@ -21,16 +21,30 @@ void WebApiWsConsoleClass::init(AsyncWebServer& server, Scheduler& scheduler) scheduler.addTask(_wsCleanupTask); _wsCleanupTask.enable(); + + _simpleDigestAuth.setUsername(AUTH_USERNAME); + _simpleDigestAuth.setRealm("console websocket"); + + reload(); +} + +void WebApiWsConsoleClass::reload() +{ + _ws.removeMiddleware(&_simpleDigestAuth); + + auto const& config = Configuration.get(); + + if (config.Security.AllowReadonly) { return; } + + _ws.enable(false); + _simpleDigestAuth.setPassword(config.Security.Password); + _ws.addMiddleware(&_simpleDigestAuth); + _ws.closeAll(); + _ws.enable(true); } void WebApiWsConsoleClass::wsCleanupTaskCb() { // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients _ws.cleanupClients(); - - if (Configuration.get().Security.AllowReadonly) { - _ws.setAuthentication("", ""); - } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); - } } diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index eb1a83bf..a50a792a 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -36,18 +36,31 @@ void WebApiWsLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) scheduler.addTask(_sendDataTask); _sendDataTask.enable(); + _simpleDigestAuth.setUsername(AUTH_USERNAME); + _simpleDigestAuth.setRealm("live websocket"); + + reload(); +} + +void WebApiWsLiveClass::reload() +{ + _ws.removeMiddleware(&_simpleDigestAuth); + + auto const& config = Configuration.get(); + + if (config.Security.AllowReadonly) { return; } + + _ws.enable(false); + _simpleDigestAuth.setPassword(config.Security.Password); + _ws.addMiddleware(&_simpleDigestAuth); + _ws.closeAll(); + _ws.enable(true); } void WebApiWsLiveClass::wsCleanupTaskCb() { // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients _ws.cleanupClients(); - - if (Configuration.get().Security.AllowReadonly) { - _ws.setAuthentication("", ""); - } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); - } } void WebApiWsLiveClass::sendDataTaskCb() @@ -134,6 +147,13 @@ void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std } else { root["limit_absolute"] = -1; } + root["radio_stats"]["tx_request"] = inv->RadioStats.TxRequestData; + root["radio_stats"]["tx_re_request"] = inv->RadioStats.TxReRequestFragment; + root["radio_stats"]["rx_success"] = inv->RadioStats.RxSuccess; + root["radio_stats"]["rx_fail_nothing"] = inv->RadioStats.RxFailNoAnswer; + root["radio_stats"]["rx_fail_partial"] = inv->RadioStats.RxFailPartialAnswer; + root["radio_stats"]["rx_fail_corrupt"] = inv->RadioStats.RxFailCorruptData; + root["radio_stats"]["rssi"] = inv->getLastRssi(); } void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv) diff --git a/src/main.cpp b/src/main.cpp index 433619e1..073d27bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,7 @@ #include "NetworkSettings.h" #include "NtpSettings.h" #include "PinMapping.h" +#include "RestartHelper.h" #include "Scheduler.h" #include "SunPosition.h" #include "Utils.h" @@ -25,12 +26,21 @@ #include #include #include +#include + +#include void setup() { // Move all dynamic allocations >512byte to psram (if available) heap_caps_malloc_extmem_enable(512); + // Initialize SpiManager + SpiManagerInst.register_bus(SPI2_HOST); +#if SOC_SPI_PERIPH_NUM > 2 + SpiManagerInst.register_bus(SPI3_HOST); +#endif + // Initialize serial output Serial.begin(SERIAL_BAUDRATE); #if ARDUINO_USB_CDC_ON_BOOT @@ -143,7 +153,7 @@ void setup() if (config.Dtu.Serial == DTU_SERIAL) { MessageOutput.print("generate serial based on ESP chip id: "); const uint64_t dtuId = Utils::generateDtuSerial(); - MessageOutput.printf("%0x%08x... ", + MessageOutput.printf("%0" PRIx32 "%08" PRIx32 "... ", ((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)), ((uint32_t)(dtuId & 0xFFFFFFFF))); config.Dtu.Serial = dtuId; @@ -154,6 +164,7 @@ void setup() InverterSettings.init(scheduler); Datastore.init(scheduler); + RestartHelper.init(scheduler); } void loop() diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js index 9a2aaecb..94f27454 100644 --- a/webapp/eslint.config.js +++ b/webapp/eslint.config.js @@ -1,22 +1,12 @@ /* eslint-env node */ -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { FlatCompat } from "@eslint/eslintrc"; import js from "@eslint/js"; import pluginVue from 'eslint-plugin-vue' - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, -}); +import vueTsEslintConfig from '@vue/eslint-config-typescript' export default [ js.configs.recommended, ...pluginVue.configs['flat/essential'], - ...compat.extends("@vue/eslint-config-typescript/recommended"), + ...vueTsEslintConfig(), { files: [ "**/*.vue", diff --git a/webapp/package.json b/webapp/package.json index a427ce2c..dc6f8b89 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -17,34 +17,35 @@ "bootstrap": "^5.3.3", "bootstrap-icons-vue": "^1.11.3", "mitt": "^3.0.1", - "sortablejs": "^1.15.2", + "sortablejs": "^1.15.3", "spark-md5": "^3.0.2", - "vue": "^3.4.35", - "vue-i18n": "^9.13.1", - "vue-router": "^4.4.2" + "vue": "^3.5.11", + "vue-i18n": "9.13.1", + "vue-router": "^4.4.5" }, "devDependencies": { "@intlify/unplugin-vue-i18n": "^4.0.0", - "@tsconfig/node18": "^18.2.4", + "@tsconfig/node22": "^22.0.0", "@types/bootstrap": "^5.2.10", - "@types/node": "^22.1.0", + "@types/node": "^22.7.4", "@types/pulltorefreshjs": "^0.1.7", "@types/sortablejs": "^1.15.8", "@types/spark-md5": "^3.0.4", - "@vitejs/plugin-vue": "^5.1.2", - "@vue/eslint-config-typescript": "^13.0.0", + "@vitejs/plugin-vue": "^5.1.4", + "@vue/eslint-config-typescript": "^14.0.0", "@vue/tsconfig": "^0.5.1", - "eslint": "^9.8.0", - "eslint-plugin-vue": "^9.27.0", + "eslint": "^9.12.0", + "eslint-plugin-vue": "^9.28.0", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", "pulltorefreshjs": "^0.1.22", "sass": "^1.77.6", - "terser": "^5.31.3", - "typescript": "^5.5.4", - "vite": "^5.3.5", + "terser": "^5.34.1", + "typescript": "^5.6.2", + "vite": "^5.4.8", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^3.5.1", - "vue-tsc": "^2.0.29" - } + "vite-plugin-css-injected-by-js": "^3.5.2", + "vue-tsc": "^2.1.6" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/webapp/src/components/EventLog.vue b/webapp/src/components/EventLog.vue index 9011467d..f3c243a1 100644 --- a/webapp/src/components/EventLog.vue +++ b/webapp/src/components/EventLog.vue @@ -1,10 +1,12 @@