diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5e73566e..d41f9824 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-Compiles 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 c92d91c3..6e6314bc 100644 --- a/README.md +++ b/README.md @@ -43,41 +43,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..bb1e4108 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,44 @@ "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 + } } -] \ No newline at end of file +] diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index 9ab746a3..1a3acf59 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -6,7 +6,7 @@ #include // mqtt discovery device classes -enum { +enum DeviceClassType { DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, @@ -15,20 +15,34 @@ enum { 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", "temperature", "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[] = { @@ -58,13 +72,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 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 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 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 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 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 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 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 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 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..ea3869cb 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,10 +24,10 @@ enum class network_event { NETWORK_EVENT_MAX }; -typedef std::function NetworkEventCb; +typedef std::function DtuNetworkEventCb; typedef struct NetworkEventCbList { - NetworkEventCb cb; + DtuNetworkEventCb cb; network_event event; NetworkEventCbList() @@ -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; @@ -83,6 +84,7 @@ private: bool _ethConnected = false; 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..de94654c 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -26,6 +26,13 @@ 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; + int8_t eth_phy_addr; bool eth_enabled; int eth_power; @@ -49,10 +56,11 @@ public: bool isValidNrf24Config() const; bool isValidCmt2300Config() const; + bool isValidW5500Config() const; bool isValidEthConfig() const; 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..f62c3312 --- /dev/null +++ b/include/W5500.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include // required for esp_eth_handle_t +#include + +class W5500 { +public: + explicit W5500(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst); + W5500(const W5500&) = delete; + W5500& operator=(const W5500&) = delete; + ~W5500(); + + String macAddress(); + +private: + esp_eth_handle_t eth_handle; + esp_netif_t* eth_netif; +}; 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/__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/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..3e51bbd7 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -272,3 +272,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..f139fab3 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -65,6 +65,30 @@ public: void addRxFragment(const uint8_t fragment[], const uint8_t len); 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; 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..bc81b1a7 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.1 + 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_shield] +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/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..ab4a2ad1 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,45 @@ 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, CATEGORY_DIAGNOSTIC); + publishDtuSensor("WiFi Signal", "dtu/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Uptime", "dtu/uptime", "s", "", DEVICE_CLS_DURATION, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Temperature", "dtu/temperature", "°C", "mdi:thermometer", DEVICE_CLS_TEMPERATURE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Heap Size", "dtu/heap/size", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Heap Free", "dtu/heap/free", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Largest Free Heap Block", "dtu/heap/maxalloc", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishDtuSensor("Lifetime Minimum Free Heap", "dtu/heap/minfree", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); - yield(); + publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, CATEGORY_NONE); + publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, CATEGORY_NONE); + publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, CATEGORY_NONE); + + publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, 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, CATEGORY_CONFIG); + publishInverterButton(inv, "Turn Inverter On", "cmd/power", "1", "mdi:power-plug", DEVICE_CLS_NONE, CATEGORY_CONFIG); + publishInverterButton(inv, "Restart Inverter", "cmd/restart", "1", "", DEVICE_CLS_RESTART, CATEGORY_CONFIG); + publishInverterButton(inv, "Reset Radio Statistics", "cmd/reset_rf_stats", "1", "", DEVICE_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", CATEGORY_CONFIG); + publishInverterNumber(inv, "Limit Persistent Relative", "status/limit_relative", "cmd/limit_persistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", 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", CATEGORY_CONFIG); + publishInverterNumber(inv, "Limit Persistent Absolute", "status/limit_absolute", "cmd/limit_persistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", 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, CATEGORY_DIAGNOSTIC); + publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0", DEVICE_CLS_NONE, CATEGORY_NONE); + + publishInverterSensor(inv, "TX Requests", "radio/tx_request", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Success", "radio/rx_success", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Fail Receive Nothing", "radio/rx_fail_nothing", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Fail Receive Partial", "radio/rx_fail_partial", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "RX Fail Receive Corrupt", "radio/rx_fail_corrupt", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); + publishInverterSensor(inv, "TX Re-Request Fragment", "radio/tx_re_request", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); // Loop all channels for (auto& t : inv->Statistics()->getChannelTypes()) { @@ -94,8 +110,6 @@ void MqttHandleHassClass::publishConfig() } } } - - yield(); } } @@ -128,8 +142,7 @@ 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]; + const char* stateCls = stateClass_name[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, 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 CategoryType category) { const String serial = inv->serialString(); - String buttonId = caption; + String buttonId = name; buttonId.replace(" ", "_"); buttonId.toLowerCase(); @@ -185,41 +186,29 @@ 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 CategoryType category) { const String serial = inv->serialString(); - String buttonId = caption; + String buttonId = name; buttonId.replace(" ", "_"); buttonId.toLowerCase(); @@ -227,150 +216,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, 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 +294,106 @@ 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 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 (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 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, 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 CategoryType category) +{ + const String dtuId = getDtuUniqueId(); + + JsonDocument root; + createDtuInfo(root); + publishBinarySensor(root, dtuId, dtuId, name, state_topic, payload_on, payload_off, device_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 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, 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 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, 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 CategoryType category) +{ + const String dtuId = getDtuUniqueId(); + + JsonDocument root; + createDtuInfo(root); + publishSensor(root, dtuId, dtuId, name, state_topic, unit_of_measure, icon, device_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 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, category); } diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index d099b444..8e720663 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,14 @@ 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)); + if (inv->DevInfo()->getLastUpdate() > 0) { // Bootloader Version MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion())); @@ -146,7 +147,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 +155,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 +172,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 +203,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 +213,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)); 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 +229,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..33de9255 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,34 @@ 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 = std::make_unique(pin.w5500_mosi, pin.w5500_miso, pin.w5500_sclk, pin.w5500_cs, pin.w5500_int, pin.w5500_rst); + } 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 + } + 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 +90,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: %d\r\n", info.wifi_sta_disconnected.reason); if (_networkMode == network_mode::WiFi) { MessageOutput.println("Try reconnecting"); WiFi.disconnect(true, false); @@ -95,7 +110,7 @@ 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; @@ -109,8 +124,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 +181,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() @@ -399,6 +408,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..67b68a15 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -84,6 +84,54 @@ #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 + +#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 + PinMappingClass PinMapping; PinMappingClass::PinMappingClass() @@ -103,6 +151,13 @@ 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; + #ifdef OPENDTU_ETHERNET _pinMapping.eth_enabled = true; #else @@ -164,6 +219,13 @@ 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; + #ifdef OPENDTU_ETHERNET _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true; #else @@ -211,7 +273,19 @@ 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; +} + bool PinMappingClass::isValidEthConfig() const { - return _pinMapping.eth_enabled; + return _pinMapping.eth_enabled + && _pinMapping.eth_mdc >= 0 + && _pinMapping.eth_mdio >= 0; } 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..4d680368 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,16 +57,6 @@ 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()) { diff --git a/src/W5500.cpp b/src/W5500.cpp new file mode 100644 index 00000000..0f5b5739 --- /dev/null +++ b/src/W5500.cpp @@ -0,0 +1,111 @@ +// 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(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) + : eth_handle(nullptr) + , eth_netif(nullptr) +{ + 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)); + + esp_err_t err = gpio_install_isr_service(ARDUINO_ISR_FLAG); + if (err != ESP_ERR_INVALID_STATE) // don't raise an error when ISR service is already installed + ESP_ERROR_CHECK(err); + + 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) + ESP_ERROR_CHECK(ESP_FAIL); + + // Reset sequence + delayMicroseconds(500); + gpio_set_level(static_cast(pin_rst), 1); + delayMicroseconds(1000); + + // 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? +} + +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); +} 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..b4878817 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,14 @@ 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; + auto ethPinObj = curPin["eth"].to(); ethPinObj["enabled"] = pin.eth_enabled; ethPinObj["phy_addr"] = pin.eth_phy_addr; @@ -103,8 +111,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 +157,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..cd8747bf 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -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..79c8a8e9 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) @@ -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_security.cpp b/src/WebApi_security.cpp index eb0f27d2..ddd8bb50 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__); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index eb1a83bf..4fa79837 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -134,6 +134,12 @@ 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; } void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv) diff --git a/src/main.cpp b/src/main.cpp index 433619e1..0377c8b3 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 @@ -154,6 +164,7 @@ void setup() InverterSettings.init(scheduler); Datastore.init(scheduler); + RestartHelper.init(scheduler); } void loop() diff --git a/webapp/package.json b/webapp/package.json index a427ce2c..908058cc 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.10", + "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", + "@vitejs/plugin-vue": "^5.1.4", "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", - "eslint": "^9.8.0", - "eslint-plugin-vue": "^9.27.0", + "eslint": "^9.11.1", + "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.0", + "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 @@