diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 8d8ab45e5..ffde2d216 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -47,7 +47,7 @@ jobs: run: python convert.py - name: Run PlatformIO - run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp8266-nokia5110 --environment esp8266-ssd1306 --environment esp32-wroom32-release --environment esp32-wroom32-nokia5110 --environment esp32-wroom32-ssd1306 + run: pio run -d src --environment esp8266-release --environment esp8266-release-prometheus --environment esp8285-release --environment esp32-wroom32-release --environment esp32-wroom32-release-prometheus --environment opendtufusionv1-release - name: Rename Binary files id: rename-binary-files diff --git a/.github/workflows/compile_release.yml b/.github/workflows/compile_release.yml index 20fcef872..84ad51116 100644 --- a/.github/workflows/compile_release.yml +++ b/.github/workflows/compile_release.yml @@ -51,7 +51,7 @@ jobs: run: python convert.py - name: Run PlatformIO - run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp8266-nokia5110 --environment esp8266-ssd1306 --environment esp32-wroom32-release --environment esp32-wroom32-nokia5110 --environment esp32-wroom32-ssd1306 + run: pio run -d src --environment esp8266-release --environment esp8266-release-prometheus --environment esp8285-release --environment esp32-wroom32-release --environment esp32-wroom32-release-prometheus --environment opendtufusionv1-release - name: Rename Binary files id: rename-binary-files diff --git a/.gitignore b/.gitignore index ed5e95383..2ee4b6794 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,10 @@ .vscode/extensions.json src/config/config_override.h src/web/html/h/* +src/web/html/tmp/* /**/Debug /**/v16/* *.db *.suo *.ipch +src/output.map diff --git a/Getting_Started.md b/Getting_Started.md index fd4de83ea..e73277f08 100644 --- a/Getting_Started.md +++ b/Getting_Started.md @@ -1,3 +1,22 @@ +## Overview + +This page describes how the module of a Wemos D1 mini and ESP8266 is wired to the radio module and is flashed with the latest Firmware.
+Further information will help you to communicate to the compatible inverters. + +You find the full [User_Manual here](User_Manual.md) + +## Compatiblity + +For now the following Inverters should work out of the box: + +Hoymiles Inverters + +| Status | Serie | Model | comment | +| ----- | ----- | ------ | ------- | +| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet | +| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | | +| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). | + ## Table of Contents - [Table of Contents](#table-of-contents) @@ -6,9 +25,12 @@ - [Things needed](#things-needed) - [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there) - [Wiring things up](#wiring-things-up) - - [ESP8266 wiring example](#esp8266-wiring-example) + - [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example) - [Schematic](#schematic) - [Symbolic view](#symbolic-view) + - [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2) + - [Schematic](#schematic-2) + - [Symbolic view](#symbolic-view-2) - [ESP32 wiring example](#esp32-wiring-example) - [Schematic](#schematic-1) - [Symbolic view](#symbolic-view-1) @@ -23,40 +45,13 @@ - [HTTP based Pages](#http-based-pages) - [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface) - [Used Libraries](#used-libraries) -- [Contact](#contact) - [ToDo](#todo) *** -## Overview - -This page describes how the module of a Wemos D1 mini and ESP8266 is wired to the radio module and is flashed with the latest Firmware.
-Further information will help you to communicate to the compatible inverters. - -You find the full [User_Manual here](User_Manual.md) - -## Compatiblity +Solenso Inverters: -For now the following Inverters should work out of the box: - -Hoymiles Inverters - -- HM300 -- HM350 -- HM400 -- HM600 -- HM700 -- HM800 -- HM1000? -- HM1200 -- HM1500 - -TSUN Inverters: - -- [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400) -- [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400) -- [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) -- others may work as well (need to be verified). +- SOL-H350 ## Things needed @@ -69,8 +64,9 @@ Make sure the NRF24L01+ module has the "+" in its name as we depend on the 250kb | **Parts** | **Price** | | --- | --- | -| D1 ESP8266 Mini WLAN Board Mikrokontroller | 4,40 Euro | +| D1 ESP8266 Mini WLAN Board Microcontroller | 4,40 Euro | | NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul | 3,45 Euro | +| 100µF / 10V Capacitor Kondensator | 0,15 Euro | | Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro | | **Total costs** | **10,34 Euro** | @@ -80,6 +76,7 @@ To also run our sister project OpenDTU and be upwards compatible for the future | --- | --- | | ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 Euro | | NRF24L01+ PA LNA SMA mit Antenne Long | 4,50 Euro | +| 100µF / 10V Capacitor Kondensator | 0,15 Euro | | Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro | | **Total costs** | **14,89 Euro** | @@ -89,6 +86,18 @@ Watch out, there are some fake NRF24L01+ Modules out there that seem to use rebr An example can be found in [Issue #230](https://github.com/lumapu/ahoy/issues/230).
You are welcome to add more examples of faked chips. We will add that information here.
+Some users reported better connection or longer range through more walls when using the +"E01-ML01DP5" EBYTE 2,4 GHz Wireless Modul nRF24L01 + PA + LNA RF Modul, SMA-K Antenna connector, +which has an eye-catching HF cover. But beware: It comes without the antenna! + +In any case you should stabilize the Vcc power by a capacitor and don't exceed the Amplifier Power Level "LOW". +Users reporting good connection over 10m through walls / ceilings with Amplifier Power Level "MIN". +It is not always the bigger the better... + +Power levels "HIGH" and "MAX" are meant to wirings where the nRF24 is supplied by an extra 3.3 Volt regulator. +The bultin regulator on ESP boards has only low reserves in case WiFi and nRF are sending simultaneously. +If you operate additional interfaces like a display, the reserve is again reduced. + ## Wiring things up The NRF24L01+ radio module is connected to the standard SPI pins: @@ -107,7 +116,7 @@ Additional, there are 3 pins, which can be set individual: *These pins can be changed from the /setup URL.* -#### ESP8266 wiring example +#### ESP8266 wiring example on WEMOS D1 This is an example wiring using a Wemos D1 mini.
@@ -119,6 +128,18 @@ This is an example wiring using a Wemos D1 mini.
![Symbolic](doc/AhoyWemos_Steckplatine.jpg) +#### ESP8266 wiring example on 30pin Lolin NodeMCU v3 + +This is an example wiring using a NodeMCU V3.
+ +##### Schematic + +![Schematic](doc/ESP8266_nRF24L01+_Schaltplan.jpg) + +##### Symbolic view + +![Symbolic](doc/ESP8266_nRF24L01+_bb.png) + #### ESP32 wiring example Example wiring for a 38pin ESP32 module @@ -141,12 +162,27 @@ CE D2 (GPIO4) IRQ D0 (GPIO16 - no IRQ!) ``` +ATTENTION: From development version 108 onwards, also MISO, MOSI and SCLK +are configurable. Their defaults are correct for 'standard' ESP32 boards +and non-settable for ESP8266 (as this chip cannot move them elsewhere). +If you have an existing install though, you might see '0' in the web GUI. + +Set MISO=19, MOSI=23, SCLK=18 in GUI and save for existing installs, this is the old +correct default for most ESP32 boards, for ESP82xx, a simple settings save should suffice. +Reboot afterwards. + + ## Flash the Firmware on your Ahoy DTU Hardware Once your Hardware is ready to run, you need to flash the Ahoy DTU Firmware to your Board. You can either build your own using your own configuration or use one of our pre-compiled generic builds. -#### Compiling your own Version +### Flash from your browser (easy) + +The easiest step for you is to flash online. A browser MS Edge or Google Chrome is required. +[Here you go](https://ahoydtu.de/web_install/) + +### Compiling your own Version This information suits you if you want to configure and build your own firmware. @@ -255,12 +291,6 @@ When everything is wired up and the firmware is flashed, it is time to connect t | `ArduinoJson` | 6.19.4 | MIT | | `ESP Async WebServer` | 4.3.0 | ? | -## Contact - -We run a Discord Server that can be used to get in touch with the Developers and Users. - - - ## ToDo [See this post](https://github.com/lumapu/ahoy/issues/142) diff --git a/README.md b/README.md index 43b31dfed..ab91e1782 100644 --- a/README.md +++ b/README.md @@ -18,23 +18,27 @@ This work is licensed under a **Communicate with Hoymiles inverters via radio**. Get actual values like power, current, daily energy and set parameters like the power limit via web interface or MQTT. In this repository you will find different approaches means Hardware / Software to realize the described functionalities. -List of approaches +Table of approaches: -- [ESP8266/ESP32, C++](Getting_Started.md) 👈 the most effort is spent here -- [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) -- [Raspberry Pi, Python](tools/rpi/) -- [Others, C/C++](tools/nano/NRF24_SendRcv/) +| Board | MI | HM | HMS/HMT | comment | HowTo start | +| ------ | -- | -- | ------- | ------- | ---------- | +| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | coming soon✨ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) | +| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | +| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | | +| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | -## Quick Start with ESP8266 -- [Go here ✨](Getting_Started.md#things-needed) -- [Our Website](https://ahoydtu.de) +## Getting Started +[Guide how to start with a ESP module](Getting_Started.md) +[ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install) +## Our Website +[https://ahoydtu.de](https://ahoydtu.de) ## Success Stories - [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl) ## Support, Feedback, Information and Discussion -- [Discord Server (~ 1200 Users)](https://discord.gg/WzhxEY62mB) +- [Discord Server (~ 3.800 Users)](https://discord.gg/WzhxEY62mB) - [The root of development](https://www.mikrocontroller.net/topic/525778) ### Development @@ -48,4 +52,4 @@ Please try to describe your issues as precise as possible and think about if thi - [OpenDTU](https://github.com/tbnobody/OpenDTU) <- Our sister project ✨ for Hoymiles HM-300, HM-600, HM-1200 (for ESP32 only!) - [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles) - <- Go here ✨ for Hoymiles MI-300, MI-600, MI-1200 Software + <- Go here ✨ for Hoymiles MI-300, MI-600, MI-1200 Software (single inverter only) diff --git a/User_Manual.md b/User_Manual.md index 5ff1339a4..242a2809a 100644 --- a/User_Manual.md +++ b/User_Manual.md @@ -29,6 +29,7 @@ The AhoyDTU will publish on the following topics | `uptime` | 73630 | uptime in seconds | false | | `version` | 0.5.61 | current installed verison of AhoyDTU | true | | `wifi_rssi` | -75 | WiFi signal strength | false | +| `ip_addr` | 192.168.178.25 | WiFi Station IP Address | true | | status code | Remarks | |---|---| @@ -43,6 +44,7 @@ The AhoyDTU will publish on the following topics |---|---|---|---| | `available` | 2 | see table below | true | | `last_success` | 1672155690 | UTC Timestamp | true | +| `ack_pwr_limit` | true | fast information if inverter has accepted power limit | false | | status code | Remarks | |---|---| @@ -89,9 +91,6 @@ The AhoyDTU will publish on the following topics ## Active Power Limit via Serial / Control Page URL: `/serial` -If you leave the field "Active Power Limit" empty during the setup and reboot the ahoy-dtu will set a value of 65535 in the setup. -That is the value you have to fill in case you want to operate the inverter without a active power limit. -If the value is 65535 or -1 after another reboot the value will be set automatically to "100" and in the drop-down menu "relative in percent persistent" will be set. Of course you can do this also by your self. You can change the setting in the following manner. Decide if you want to set @@ -113,24 +112,17 @@ Also an absolute active power limit below approx. 30 Watt seems to be not meanfu ### Generic Information -The AhoyDTU subscribes on three topics `/ctrl/#`, `/setup` and `/status`. +The AhoyDTU subscribes on following topics: + +- `/ctrl/limit/` +- `/ctrl/restart/` +- `/setup/set_time` 👆 `` can be set on setup page, default is `inverter`. 👆 `` is the number of the specific inverter in the setup page. -### Inverter Power (On / Off) -```mqtt -/ctrl/power/ -``` -with payload `1` = `ON` and `0` = `OFF` - -Example: -```mqtt -inverter/ctrl/power/0 1 -``` - ### Inverter restart ```mqtt /ctrl/restart/ @@ -140,50 +132,35 @@ Example: inverter/ctrl/restart/0 ``` -### Power Limit relative persistent [%] +### Power Limit relative (non persistent) [%] ```mqtt -/ctrl/limit_persistent_relative/ +/ctrl/limit/ ``` with a payload `[2 .. 100]` +**NOTE: optional a `%` can be sent as last character** + Example: ```mqtt -inverter/ctrl/limit_persistent_relative/0 70 +inverter/ctrl/limit/0 70 ``` -### Power Limit absolute persistent [Watts] +### Power Limit absolute (non persistent) [Watts] ```mqtt -/ctrl/limit_persistent_absolute/ +/ctrl/limit/ ``` with a payload `[0 .. 65535]` -Example: -```mqtt -inverter/ctrl/limit_persistent_absolute/0 600 -``` - -### Power Limit relative non persistent [%] -```mqtt -/ctrl/limit_nonpersistent_relative/ -``` -with a payload `[2 .. 100]` +**NOTE: the unit `W` is necessary to determine an absolute limit** Example: ```mqtt -inverter/ctrl/limit_nonpersistent_relative/0 70 -``` - -### Power Limit absolute non persistent [Watts] -```mqtt -/ctrl/limit_nonpersistent_absolute/ +inverter/ctrl/limit/0 600W ``` -with a payload `[0 .. 65535]` -Example: -```mqtt -inverter/ctrl/limit_nonpersistent_absolute/0 600 -``` +### Power Limit persistent +This feature was removed. The persisten limit should not be modified cyclic by a script because of potential wearout of the flash inside the inverter. ## Control via REST API @@ -306,6 +283,10 @@ To get the information open the URL `/api/record/info` on your AhoyDTU. The info | B5r1oJ0A9G | HM-800 | | 1.0.10 | 2021 | | 104 | | | | B5r1oJ0A9G | HM-800 | | 1.0.10 | 2021 | | 104 | | | | tomquist | TSOL-M1600 | | 1.0.12 | 2020 | 06-24 | 100 | | | +| rejoe2 | MI-600 | | 236 | 2018 | 11-27 | 17 | | | +| rejoe2 | MI-1500 | | 1.0.12 | 2020 | 06-24 | 100 | | | +| dragricola | HM-1200 | | 1.0.16 | 2021 | 10-12 | 100 | | | +| dragricola | MI-300 | | 230 | 2017 | 08-08 | 1 | | | | | | | | | | | | | ## Developer Information about Command Queue @@ -340,3 +321,11 @@ Send Power Limit: - A persistent limit is only needed if you want to throttle your inverter permanently or you can use it to set a start value on the battery, which is then always the switch-on limit when switching on, otherwise it would ramp up to 100% without regulation, which is continuous load is not healthy. - You can set a new limit in the turn-off state, which is then used for on (switching on again), otherwise the last limit from before the turn-off is used, but of course this only applies if DC voltage is applied the whole time. - If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit. + +## Additional Notes +### MI Inverters +- AhoyDTU supports MI type inverters as well, since dev. version 0.5.70. +- MI inverters are known to be delivered with two different generations of firmwares: inverters with serial numbers 10x2 already use the 3rd generation protocol and behave just like the newer HM models, *the follwoing remarks do not apply to these*. +- Older MI inverters (#sn 10x1) use a different rf protocol and thus do not deliver exactly the same data. E.g. the AC power value will therefore be calculated by AhoyDTU itself, while other values might not be available at all. +- Single and dual channel 2nd gen. devices seem not to accept power limiting commands at all, the lower limit for 4-channel MI is 10% (instead of 2% for newer models) +- 4-channel MI type inverters might work, but code still is untested. diff --git a/doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png b/doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png new file mode 100644 index 000000000..2b37a3b08 Binary files /dev/null and b/doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png differ diff --git a/doc/ESP8266_nRF24L01+_Schaltplan.jpg b/doc/ESP8266_nRF24L01+_Schaltplan.jpg new file mode 100644 index 000000000..749e3fa3d Binary files /dev/null and b/doc/ESP8266_nRF24L01+_Schaltplan.jpg differ diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md new file mode 100644 index 000000000..8fb9e0028 --- /dev/null +++ b/doc/prometheus_ep_description.md @@ -0,0 +1,51 @@ +# Prometheus Endpoint +Metrics available for AhoyDTU device, inverters and channels. + +Prometheus metrics provided at `/metrics`. + +## Labels +| Label name | Description | +|:-------------|:--------------------------------------| +| version | current installed version of AhoyDTU | +| image | currently not used | +| devicename | Device name from setup | +| name | Inverter name from setup | +| serial | Serial number of inverter | +| inverter | Inverter name from setup | +| channel | Channel name from setup | + +## Exported Metrics +| Metric name | Type | Description | Labels | +|----------------------------------------|---------|--------------------------------------------------------|--------------| +| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename | +| `ahoy_solar_uptime` | Counter | Seconds since boot of the AhoyDTU device | devicename | +| `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename | +| `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial | +| `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter | +| `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter | +| `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter | +| `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter | +| `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter | +| `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter | +| `ahoy_solar_Q_AC_var` | Gauge | AC reactive power[var] | inverter | +| `ahoy_solar_F_AC_hertz` | Gauge | AC frequency [Hz] | inverter | +| `ahoy_solar_PF_AC` | Gauge | AC Power factor | inverter | +| `ahoy_solar_Temp_celsius` | Gauge | Temperature of inverter | inverter | +| `ahoy_solar_ALARM_MES_ID` | Gauge | Alarm message index of inverter | inverter | +| `ahoy_solar_LastAlarmCode` | Gauge | Last alarm code from inverter | inverter | +| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter | +| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter | +| `ahoy_solar_P_DC_watt` | Gauge | DC power of inverter [W] | inverter | +| `ahoy_solar_Efficiency_ratio` | Gauge | ration AC Power over DC Power [%] | inverter | +| `ahoy_solar_U_DC_volt` | Gauge | DC voltage of channel [V] | inverter, channel | +| `ahoy_solar_I_DC_ampere` | Gauge | DC current of channel [A] | inverter, channel | +| `ahoy_solar_P_DC_watt` | Gauge | DC power of channel [P] | inverter, channel | +| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter, channel | +| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter, channel | +| `ahoy_solar_Irradiation_ratio` | Gauge | ratio DC Power over set maximum power per channel [%] | inverter, channel | +| `ahoy_solar_radio_rx_success` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_rx_fail` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_rx_fail_answer` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_frame_cnt` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_tx_cnt` | Gauge | NRF24 statistic | | + diff --git a/scripts/buildManifest.py b/scripts/buildManifest.py index 9a5411d57..db29b3527 100644 --- a/scripts/buildManifest.py +++ b/scripts/buildManifest.py @@ -36,13 +36,13 @@ def buildManifest(path, infile, outfile): esp32["parts"].append({"path": "bootloader.bin", "offset": 4096}) esp32["parts"].append({"path": "partitions.bin", "offset": 32768}) esp32["parts"].append({"path": "ota.bin", "offset": 57344}) - esp32["parts"].append({"path": version[1] + "_esp32_" + sha + ".bin", "offset": 65536}) + esp32["parts"].append({"path": version[1] + "_" + sha + "_esp32.bin", "offset": 65536}) data["builds"].append(esp32) esp8266 = {} esp8266["chipFamily"] = "ESP8266" esp8266["parts"] = [] - esp8266["parts"].append({"path": version[1] + "_esp8266_" + sha + ".bin", "offset": 0}) + esp8266["parts"].append({"path": version[1] + "_" + sha + "_esp8266.bin", "offset": 0}) data["builds"].append(esp8266) jsonString = json.dumps(data, indent=2) diff --git a/scripts/getVersion.py b/scripts/getVersion.py index f7c825ced..0ebe1ec21 100644 --- a/scripts/getVersion.py +++ b/scripts/getVersion.py @@ -52,39 +52,34 @@ def readVersion(path, infile): os.mkdir(path + "firmware/") sha = os.getenv("SHA",default="sha") - versionout = version[:-1] + "_esp8266_" + sha + ".bin" + versionout = version[:-1] + "_" + sha + "_esp8266.bin" src = path + ".pio/build/esp8266-release/firmware.bin" dst = path + "firmware/" + versionout os.rename(src, dst) - versionout = version[:-1] + "_esp8266_nokia5110_" + sha + ".bin" - src = path + ".pio/build/esp8266-nokia5110/firmware.bin" - dst = path + "firmware/" + versionout - os.rename(src, dst) - - versionout = version[:-1] + "_esp8266_ssd1306_" + sha + ".bin" - src = path + ".pio/build/esp8266-ssd1306/firmware.bin" + versionout = version[:-1] + "_" + sha + "_esp8266_prometheus.bin" + src = path + ".pio/build/esp8266-release-prometheus/firmware.bin" dst = path + "firmware/" + versionout os.rename(src, dst) - versionout = version[:-1] + "_esp8285_" + sha + ".bin" + versionout = version[:-1] + "_" + sha + "_esp8285.bin" src = path + ".pio/build/esp8285-release/firmware.bin" dst = path + "firmware/" + versionout os.rename(src, dst) gzip_bin(dst, dst + ".gz") - versionout = version[:-1] + "_esp32_" + sha + ".bin" + versionout = version[:-1] + "_" + sha + "_esp32.bin" src = path + ".pio/build/esp32-wroom32-release/firmware.bin" dst = path + "firmware/" + versionout os.rename(src, dst) - versionout = version[:-1] + "_esp32_nokia5110_" + sha + ".bin" - src = path + ".pio/build/esp32-wroom32-nokia5110/firmware.bin" + versionout = version[:-1] + "_" + sha + "_esp32_prometheus.bin" + src = path + ".pio/build/esp32-wroom32-release-prometheus/firmware.bin" dst = path + "firmware/" + versionout os.rename(src, dst) - versionout = version[:-1] + "_esp32_ssd1306_" + sha + ".bin" - src = path + ".pio/build/esp32-wroom32-ssd1306/firmware.bin" + versionout = version[:-1] + "_" + sha + "_esp32s3.bin" + src = path + ".pio/build/opendtufusionv1-release/firmware.bin" dst = path + "firmware/" + versionout os.rename(src, dst) diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json index 85de73f4b..58a2c3c7b 100644 --- a/src/.vscode/settings.json +++ b/src/.vscode/settings.json @@ -4,20 +4,16 @@ "workbench.colorCustomizations": { "editorLineNumber.foreground": "#00ff00" }, - "editor.wordWrap": "off", - "files.eol" : "\n", - "files.trimTrailingWhitespace" : true, - + "files.eol": "\n", + "files.trimTrailingWhitespace": true, "diffEditor.ignoreTrimWhitespace": true, "files.autoSave": "afterDelay", - "editor.tabSize": 4, "editor.insertSpaces": true, // `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents. // Set to false to keep the values you've explicitly set, above. "editor.detectIndentation": false, - // https://clang.llvm.org/docs/ClangFormatStyleOptions.html "C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 0}", "files.associations": { @@ -85,4 +81,6 @@ "stop_token": "cpp", "thread": "cpp" }, + "cmake.configureOnOpen": false, + "editor.formatOnSave": false, } \ No newline at end of file diff --git a/src/CHANGES.md b/src/CHANGES.md index f8a2c5cab..debcf40d6 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,36 +1,33 @@ -# Changelog v0.5.66 +Changelog v0.6.0 -**Note:** Version `0.5.42` to `0.5.65` were development versions. Last release version was `0.5.41` -Detailed change log (development changes): https://github.com/lumapu/ahoy/blob/945a671d27d10d0f7c175ebbf2fbb2806f9cd79a/src/CHANGES.md +## General +* improved night time calculation time to 1 minute after last communication pause #515 +* refactored code for better readability +* improved Hoymiles commuinication (retransmits, immediate power limit transmission, timing at all) +* renamed firmware binaries +* add login / logout to menu +* add display support for `SH1106`, `SSD1306`, `Nokia` and `ePaper 1.54"` (ESP32 only) +* add yield total correction - move your yield to a new inverter or correct an already used inverter +* added import / export feature +* added `Prometheus` endpoints +* improved wifi connection and stability (connect to strongest AP) +* addded Hoymiles alarm IDs to log +* improved `System` information page (eg. radio statitistics) +* improved UI (repsonsive design, (optional) dark mode) +* improved system stability (reduced `heap-fragmentation`, don't break settings on failure) #644, #645 +* added support for 2nd generation of Hoymiles inverters, MI series +* improved JSON API for more stable WebUI +* added option to disable input display in `/live` (`max-power` has to be set to `0`) +* updated documentation +* improved settings on ESP32 devices while setting SPI pins (for `NRF24` radio) - -* updated REST API and MQTT (both of them use the same functionality) -* improved stability -* Regular expressions for input fields which are used for MQTT to be compliant to MQTT -* WiFi optimization (AP Mode and STA in parallel, reconnect if local STA is unavailable) -* improved display of `/system` -* fix Update button protection (prevent double click #527) -* optimized scheduler #515 -* fix of duplicates in API `/api/record/live` (#526) -* added update information to `index.html` (check for update with github.com) -* fix web logout (auto logout) -* switched MQTT library -* removed MQTT `available_text` (can be deducted from `available`) -* enhanced MQTT documentation in `User_Manual.md` -* changed MQTT topic `status` to nummeric value, check documentation in `User_Manual.md` -* added immediate (each minute) report of inverter status MQTT #522 -* increased MQTT user, pwd and topic length to 64 characters + `\0`. (The string end `\0` reduces the available size by one) #516 -* added disable night communication flag to MQTT #505 -* added MQTT /status to show status over all inverters -* added MQTT RX counter to index.html -* added protection mask to select which pages should be protected -* added monochrome display that show values also if nothing changed and in offline mode #498 -* added icons to index.html, added WiFi-strength symbol on each page -* refactored communication offset (adjustable in minutes now) -* factory reset formats entire little fs -* renamed sunrise / sunset on index.html to start / stop communication -* fixed static IP save -* fix NTP with static IP -* all values are displayed on /live even if they are 0 -* added NRF24 info to Systeminfo -* reordered enqueue commands after boot up to prevent same payload length for successive commands +## MqTT +* added `comm_disabled` #529 +* added fixed interval option #542, #523 +* improved communication, only required publishes +* improved retained flags +* added `set_power_limit` acknowledge MQTT publish #553 +* added feature to reset values on midnight, communication pause or if the inverters are not available +* partially added Hoymiles alarm ID +* improved autodiscover (added total values on multi-inverter setup) +* improved `clientID` a part of the MAC address is added to have an unique name diff --git a/src/app.cpp b/src/app.cpp index 29df1e648..d3fba12a4 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,13 +1,8 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- -#if defined(ESP32) && defined(F) -#undef F -#define F(sl) (sl) -#endif - #include "app.h" #include #include "utils/sun.h" @@ -28,124 +23,209 @@ void app::setup() { mSettings.setup(); mSettings.getPtr(mConfig); - DPRINTLN(DBG_INFO, F("Settings valid: ") + String((mSettings.getValid()) ? F("true") : F("false"))); - - mSys = new HmSystemType(); - mSys->enableDebug(); - mSys->setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs); - mPayload.addListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); - - - #if !defined(AP_ONLY) - mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mTimestamp); + DPRINT(DBG_INFO, F("Settings valid: ")); + if (mSettings.getValid()) + DBGPRINTLN(F("true")); + else + DBGPRINTLN(F("false")); + + mSys.enableDebug(); + mSys.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); + +#if defined(AP_ONLY) + mInnerLoopCb = std::bind(&app::loopStandard, this); + #else + mInnerLoopCb = std::bind(&app::loopWifi, this); #endif - mWifi.setup(mConfig, &mTimestamp); + mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onWifi, this, std::placeholders::_1)); #if !defined(AP_ONLY) - everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi)); + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); #endif - mSendTickerId = every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval); - #if !defined(AP_ONLY) - once(std::bind(&app::tickNtpUpdate, this), 2); - #endif + mSys.addInverters(&mConfig->inst); - mSys->addInverters(&mConfig->inst); - mPayload.setup(mSys); + mPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); mPayload.enableSerialDebug(mConfig->serial.debug); + mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1)); + + mMiPayload.setup(this, &mSys, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); + mMiPayload.enableSerialDebug(mConfig->serial.debug); + + // DBGPRINTLN("--- after payload"); + // DBGPRINTLN(String(ESP.getFreeHeap())); + // DBGPRINTLN(String(ESP.getHeapFragmentation())); + // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); - if(!mSys->Radio.isChipConnected()) + if (!mSys.Radio.isChipConnected()) DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); // when WiFi is in client mode, then enable mqtt broker #if !defined(AP_ONLY) - if (mConfig->mqtt.broker[0] > 0) { - everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt)); - everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt)); + mMqttEnabled = (mConfig->mqtt.broker[0] > 0); + if (mMqttEnabled) { + mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp); mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); + mPayload.addAlarmListener(std::bind(&PubMqttType::alarmEventListener, &mMqtt, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } #endif setupLed(); - mWeb.setup(this, mSys, mConfig); + mWeb.setup(this, &mSys, mConfig); mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0); - everySec(std::bind(&WebType::tickSecond, &mWeb)); - mApi.setup(this, mSys, mWeb.getWebSrvPtr(), mConfig); + mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); // Plugins - #if defined(ENA_NOKIA) || defined(ENA_SSD1306) - mMonoDisplay.setup(mSys, &mTimestamp); - everySec(std::bind(&MonoDisplayType::tickerSecond, &mMonoDisplay)); - #endif + if (mConfig->plugin.display.type != 0) + mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, mVersion); + + mPubSerial.setup(mConfig, &mSys, &mTimestamp); + + regularTickers(); - mPubSerial.setup(mConfig, mSys, &mTimestamp); - every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval); - //everySec(std::bind(&app::tickSerial, this)); + + // DBGPRINTLN("--- end setup"); + // DBGPRINTLN(String(ESP.getFreeHeap())); + // DBGPRINTLN(String(ESP.getHeapFragmentation())); + // DBGPRINTLN(String(ESP.getMaxFreeBlockSize())); } //----------------------------------------------------------------------------- void app::loop(void) { - DPRINTLN(DBG_VERBOSE, F("app::loop")); + mInnerLoopCb(); +} +//----------------------------------------------------------------------------- +void app::loopStandard(void) { ah::Scheduler::loop(); - mSys->Radio.loop(); - - yield(); - if (ah::checkTicker(&mRxTicker, 5)) { - bool rxRdy = mSys->Radio.switchRxCh(); + if (mSys.Radio.loop()) { + while (!mSys.Radio.mBufCtrl.empty()) { + packet_t *p = &mSys.Radio.mBufCtrl.front(); + + if (mConfig->serial.debug) { + DPRINT(DBG_INFO, F("RX ")); + DBGPRINT(String(p->len)); + DBGPRINT(F("B Ch")); + DBGPRINT(String(p->ch)); + DBGPRINT(F(" | ")); + mSys.Radio.dumpBuf(p->packet, p->len); + } + mStat.frmCnt++; + + Inverter<> *iv = mSys.findInverter(&p->packet[1]); + if (NULL != iv) { + if (IV_HM == iv->ivGen) + mPayload.add(iv, p); + else + mMiPayload.add(iv, p); + } + mSys.Radio.mBufCtrl.pop(); + yield(); + } + mPayload.process(true); + mMiPayload.process(true); + } + mPayload.loop(); + mMiPayload.loop(); - if (!mSys->BufCtrl.empty()) { - uint8_t len; - packet_t *p = mSys->BufCtrl.getBack(); + if (mMqttEnabled) + mMqtt.loop(); +} - if (mSys->Radio.checkPaketCrc(p->packet, &len, p->rxCh)) { - if (mConfig->serial.debug) { - DPRINT(DBG_INFO, "RX " + String(len) + "B Ch" + String(p->rxCh) + " | "); - mSys->Radio.dumpBuf(NULL, p->packet, len); - } - mStat.frmCnt++; +//----------------------------------------------------------------------------- +void app::loopWifi(void) { + ah::Scheduler::loop(); + yield(); +} - if (0 != len) - mPayload.add(p, len); - } - mSys->BufCtrl.popBack(); +//----------------------------------------------------------------------------- +void app::onWifi(bool gotIp) { + DPRINTLN(DBG_DEBUG, F("onWifi")); + ah::Scheduler::resetTicker(); + regularTickers(); // reinstall regular tickers + if (gotIp) { + mInnerLoopCb = std::bind(&app::loopStandard, this); + every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend"); + mMqttReconnect = true; + mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! + once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); + if (WIFI_AP == WiFi.getMode()) { + mMqttEnabled = false; + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); } - yield(); - - if (rxRdy) - mPayload.process(true, mConfig->nrf.maxRetransPerPyld, &mStat); + } else { + mInnerLoopCb = std::bind(&app::loopWifi, this); + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); } +} -#if !defined(AP_ONLY) - mMqtt.loop(); -#endif +//----------------------------------------------------------------------------- +void app::regularTickers(void) { + DPRINTLN(DBG_DEBUG, F("regularTickers")); + everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); + // Plugins + if (mConfig->plugin.display.type != 0) + everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); + every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); } //----------------------------------------------------------------------------- void app::tickNtpUpdate(void) { uint32_t nxtTrig = 5; // default: check again in 5 sec - if (mWifi.getNtpTime()) { - nxtTrig = 43200; // check again in 12 h - if((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) { + bool isOK = mWifi.getNtpTime(); + if (isOK || mTimestamp != 0) { + if (mMqttReconnect && mMqttEnabled) { + mMqtt.tickerSecond(); + everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); + everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); + } + + // only install schedulers once even if NTP wasn't successful in first loop + if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed + if (mConfig->inst.rstValsNotAvail) + everyMin(std::bind(&app::tickMinute, this), "tMin"); + if (mConfig->inst.rstYieldMidNight) { + uint32_t localTime = gTimezone.toLocal(mTimestamp); + uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); + } + } + + nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min + + if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) { mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600; tickCalcSunrise(); } + + // immediately start communicating + // @TODO: leads to reboot loops? not sure #674 + if (isOK && mSendFirst) { + mSendFirst = false; + once(std::bind(&app::tickSend, this), 2, "senOn"); + } + + mMqttReconnect = false; } - once(std::bind(&app::tickNtpUpdate, this), nxtTrig); + once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); } //----------------------------------------------------------------------------- void app::tickCalcSunrise(void) { - ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); + if (mSunrise == 0) // on boot/reboot calc sun values for current time + ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); + + if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day + ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); + tickIVCommunication(); - uint32_t nxtTrig = mTimestamp - ((mTimestamp + mCalculatedTimezoneOffset - 10) % 86400) + 86400;; // next midnight, -10 for safety that it is certain next day, local timezone - onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig); - if (mConfig->mqtt.broker[0] > 0) { - mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom); - } + uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop + onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri"); + if (mMqttEnabled) + tickSun(); } //----------------------------------------------------------------------------- @@ -156,79 +236,115 @@ void app::tickIVCommunication(void) { if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start nxtTrig = mSunrise - mConfig->sun.offsetSec; } else { - if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise - return; + if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise + nxtTrig = 0; } else { // current time lies within communication start/stop time, set next trigger to communication stop mIVCommunicationOn = true; nxtTrig = mSunset + mConfig->sun.offsetSec; } } - onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig); + if (nxtTrig != 0) + onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom"); + } + tickComm(); +} + +//----------------------------------------------------------------------------- +void app::tickSun(void) { + // only used and enabled by MQTT (see setup()) + if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom)) + once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry +} + +//----------------------------------------------------------------------------- +void app::tickComm(void) { + if ((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop)) + once(std::bind(&app::tickZeroValues, this), mConfig->nrf.sendInterval, "tZero"); + + if (mMqttEnabled) { + if (!mMqtt.tickerComm(!mIVCommunicationOn)) + once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s } } +//----------------------------------------------------------------------------- +void app::tickZeroValues(void) { + Inverter<> *iv; + // set values to zero, except yields + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + mPayload.zeroInverterValues(iv); + } +} + +//----------------------------------------------------------------------------- +void app::tickMinute(void) { + // only triggered if 'reset values on no avail is enabled' + + Inverter<> *iv; + // set values to zero, except yields + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) + mPayload.zeroInverterValues(iv); + } +} + +//----------------------------------------------------------------------------- +void app::tickMidnight(void) { + // only triggered if 'reset values at midnight is enabled' + uint32_t localTime = gTimezone.toLocal(mTimestamp); + uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); + + Inverter<> *iv; + // set values to zero, except yield total + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + mPayload.zeroInverterValues(iv); + mPayload.zeroYieldDay(iv); + } + + if (mMqttEnabled) + mMqtt.tickerMidnight(); +} + //----------------------------------------------------------------------------- void app::tickSend(void) { - if(!mSys->Radio.isChipConnected()) { - DPRINTLN(DBG_WARN, "NRF24 not connected!"); + if (!mSys.Radio.isChipConnected()) { + DPRINTLN(DBG_WARN, F("NRF24 not connected!")); return; } if (mIVCommunicationOn) { - if (!mSys->BufCtrl.empty()) { - if (mConfig->serial.debug) - DPRINTLN(DBG_DEBUG, F("recbuf not empty! #") + String(mSys->BufCtrl.getFill())); + if (!mSys.Radio.mBufCtrl.empty()) { + if (mConfig->serial.debug) { + DPRINT(DBG_DEBUG, F("recbuf not empty! #")); + DBGPRINTLN(String(mSys.Radio.mBufCtrl.size())); + } } int8_t maxLoop = MAX_NUM_INVERTERS; - Inverter<> *iv = mSys->getInverterByPos(mSendLastIvId); + Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId); do { mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1; - iv = mSys->getInverterByPos(mSendLastIvId); + iv = mSys.getInverterByPos(mSendLastIvId); } while ((NULL == iv) && ((maxLoop--) > 0)); if (NULL != iv) { - if(iv->config->enabled) { - if (!mPayload.isComplete(iv)) - mPayload.process(false, mConfig->nrf.maxRetransPerPyld, &mStat); - - if (!mPayload.isComplete(iv)) { - if (0 == mPayload.getMaxPacketId(iv)) - mStat.rxFailNoAnser++; - else - mStat.rxFail++; - - iv->setQueuedCmdFinished(); // command failed - if (mConfig->serial.debug) - DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout")); - if (mConfig->serial.debug) { - DPRINT(DBG_INFO, F("(#") + String(iv->id) + ") "); - DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload.getRetransmits(iv)) + ")"); - } - } - - mPayload.reset(iv, mTimestamp); - mPayload.request(iv); - - yield(); - if (mConfig->serial.debug) { - DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status())); - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Requesting Inv SN ") + String(iv->config->serial.u64, HEX)); - } - - if (iv->devControlRequest) { - if (mConfig->serial.debug) - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0])); - mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit); - mPayload.setTxCmd(iv, iv->devControlCmd); - iv->clearCmdQueue(); - iv->enqueCommand(SystemConfigPara); // read back power limit - } else { - uint8_t cmd = iv->getQueuedCmd(); - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket")); - mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload.getTs(iv), iv->alarmMesIndex); - mPayload.setTxCmd(iv, cmd); - mRxTicker = 0; - } + if (iv->config->enabled) { + if (iv->ivGen == IV_HM) + mPayload.ivSend(iv); + else + mMiPayload.ivSend(iv); } } } else { @@ -240,31 +356,26 @@ void app::tickSend(void) { updateLed(); } -//----------------------------------------------------------------------------- -void app::handleIntr(void) { - DPRINTLN(DBG_VERBOSE, F("app::handleIntr")); - mSys->Radio.handleIntr(); -} - //----------------------------------------------------------------------------- void app::resetSystem(void) { snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); #ifdef AP_ONLY mTimestamp = 1; -#else - mTimestamp = 0; #endif + mSendFirst = true; + mSunrise = 0; mSunset = 0; - mRxTicker = 0; - mSendTickerId = 0xff; // invalid id + mMqttEnabled = false; mSendLastIvId = 0; mShowRebootRequest = false; mIVCommunicationOn = true; + mSavePending = false; + mSaveReboot = false; memset(&mStat, 0, sizeof(statistics_t)); } @@ -281,26 +392,25 @@ void app::setupLed(void) { * PIN ---- |<----- 3.3V * * */ - if(mConfig->led.led0 != 0xff) { + if (mConfig->led.led0 != 0xff) { pinMode(mConfig->led.led0, OUTPUT); - digitalWrite(mConfig->led.led0, HIGH); // LED off + digitalWrite(mConfig->led.led0, HIGH); // LED off } - if(mConfig->led.led1 != 0xff) { + if (mConfig->led.led1 != 0xff) { pinMode(mConfig->led.led1, OUTPUT); - digitalWrite(mConfig->led.led1, HIGH); // LED off + digitalWrite(mConfig->led.led1, HIGH); // LED off } } //----------------------------------------------------------------------------- void app::updateLed(void) { - if(mConfig->led.led0 != 0xff) { - Inverter<> *iv = mSys->getInverterByPos(0); + if (mConfig->led.led0 != 0xff) { + Inverter<> *iv = mSys.getInverterByPos(0); if (NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - if(iv->isProducing(mTimestamp, rec)) - digitalWrite(mConfig->led.led0, LOW); // LED on + if (iv->isProducing(mTimestamp)) + digitalWrite(mConfig->led.led0, LOW); // LED on else - digitalWrite(mConfig->led.led0, HIGH); // LED off + digitalWrite(mConfig->led.led0, HIGH); // LED off } } } diff --git a/src/app.h b/src/app.h index 54dca314c..cbf7e6a1d 100644 --- a/src/app.h +++ b/src/app.h @@ -1,36 +1,30 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __APP_H__ #define __APP_H__ - -#include "utils/dbg.h" #include +#include #include #include -#include #include "appInterface.h" - #include "config/settings.h" #include "defines.h" -#include "utils/crc.h" -#include "utils/ahoyTimer.h" -#include "utils/scheduler.h" - -#include "hm/CircularBuffer.h" +#include "hm/hmPayload.h" #include "hm/hmSystem.h" -#include "hm/payload.h" -#include "wifi/ahoywifi.h" -#include "web/web.h" -#include "web/RestApi.h" - +#include "hm/miPayload.h" #include "publisher/pubMqtt.h" #include "publisher/pubSerial.h" - +#include "utils/crc.h" +#include "utils/dbg.h" +#include "utils/scheduler.h" +#include "web/RestApi.h" +#include "web/web.h" +#include "wifi/ahoywifi.h" // convert degrees and radians for sun calculation #define SIN(x) (sin(radians(x))) @@ -39,31 +33,32 @@ #define ACOS(x) (degrees(acos(x))) typedef HmSystem HmSystemType; -typedef Payload PayloadType; +typedef HmPayload PayloadType; +typedef MiPayload MiPayloadType; typedef Web WebType; typedef RestApi RestApiType; typedef PubMqtt PubMqttType; typedef PubSerial PubSerialType; // PLUGINS -#if defined(ENA_NOKIA) || defined(ENA_SSD1306) - #include "plugins/MonochromeDisplay/MonochromeDisplay.h" - typedef MonochromeDisplay MonoDisplayType; -#endif - +#include "plugins/Display/Display.h" +typedef Display DisplayType; class app : public IApp, public ah::Scheduler { - public: + public: app(); ~app() {} void setup(void); void loop(void); - void handleIntr(void); - void cbMqtt(char* topic, byte* payload, unsigned int length); - void saveValues(void); - void resetPayload(Inverter<>* iv); - bool getWifiApActive(void); + void loopStandard(void); + void loopWifi(void); + void onWifi(bool gotIp); + void regularTickers(void); + + void handleIntr(void) { + mSys.Radio.handleIntr(); + } uint32_t getUptime() { return Scheduler::getUptime(); @@ -73,15 +68,30 @@ class app : public IApp, public ah::Scheduler { return Scheduler::getTimestamp(); } - bool saveSettings() { - mShowRebootRequest = true; - return mSettings.saveSettings(); + bool saveSettings(bool reboot) { + mShowRebootRequest = true; // only message on index, no reboot + mSavePending = true; + mSaveReboot = reboot; + once(std::bind(&app::tickSave, this), 3, "save"); + return true; + } + + bool readSettings(const char *path) { + return mSettings.readSettings(path); } bool eraseSettings(bool eraseWifi = false) { return mSettings.eraseSettings(eraseWifi); } + bool getSavePending() { + return mSavePending; + } + + bool getLastSaveSucceed() { + return mSettings.getLastSaveSucceed(); + } + statistics_t *getStatistics() { return &mStat; } @@ -94,8 +104,12 @@ class app : public IApp, public ah::Scheduler { mWifi.getAvailNetworks(obj); } + void setOnUpdate() { + onWifi(false); + } + void setRebootFlag() { - once(std::bind(&app::tickReboot, this), 1); + once(std::bind(&app::tickReboot, this), 3, "rboot"); } const char *getVersion() { @@ -119,7 +133,16 @@ class app : public IApp, public ah::Scheduler { } void setMqttDiscoveryFlag() { - once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1); + once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); + } + + void setMqttPowerLimitAck(Inverter<> *iv) { + mMqtt.setPowerLimitAck(iv); + } + + void ivSendHighPrio(Inverter<> *iv) { + if(mIVCommunicationOn) // only send commands if communcation is enabled + mPayload.ivSendHighPrio(iv); } bool getMqttIsConnected() { @@ -159,26 +182,33 @@ class app : public IApp, public ah::Scheduler { getStat(max); } + void getSchedulerNames(void) { + printSchedulers(); + } + void setTimestamp(uint32_t newTime) { - DPRINTLN(DBG_DEBUG, F("setTimestamp: ") + String(newTime)); + DPRINT(DBG_DEBUG, F("setTimestamp: ")); + DBGPRINTLN(String(newTime)); if(0 == newTime) mWifi.getNtpTime(); else Scheduler::setTimestamp(newTime); } - HmSystemType *mSys; + HmSystemType mSys; private: + typedef std::function innerLoopCb; + void resetSystem(void); void payloadEventListener(uint8_t cmd) { #if !defined(AP_ONLY) - mMqtt.payloadEventListener(cmd); - #endif - #if defined(ENA_NOKIA) || defined(ENA_SSD1306) - mMonoDisplay.payloadEventListener(cmd); + if (mMqttEnabled) + mMqtt.payloadEventListener(cmd); #endif + if(mConfig->plugin.display.type != 0) + mDisplay.payloadEventListener(cmd); } void mqttSubRxCb(JsonObject obj); @@ -188,13 +218,31 @@ class app : public IApp, public ah::Scheduler { void tickReboot(void) { DPRINTLN(DBG_INFO, F("Rebooting...")); + onWifi(false); + ah::Scheduler::resetTicker(); + WiFi.disconnect(); + delay(200); ESP.restart(); } + void tickSave(void) { + if(!mSettings.saveSettings()) + mSaveReboot = false; + mSavePending = false; + + if(mSaveReboot) + setRebootFlag(); + } + void tickNtpUpdate(void); void tickCalcSunrise(void); void tickIVCommunication(void); + void tickSun(void); + void tickComm(void); void tickSend(void); + void tickMinute(void); + void tickZeroValues(void); + void tickMidnight(void); /*void tickSerial(void) { if(Serial.available() == 0) return; @@ -210,6 +258,8 @@ class app : public IApp, public ah::Scheduler { DBGPRINTLN(""); }*/ + innerLoopCb mInnerLoopCb; + bool mShowRebootRequest; bool mIVCommunicationOn; @@ -217,32 +267,31 @@ class app : public IApp, public ah::Scheduler { WebType mWeb; RestApiType mApi; PayloadType mPayload; + MiPayloadType mMiPayload; PubSerialType mPubSerial; char mVersion[12]; settings mSettings; settings_t *mConfig; + bool mSavePending; + bool mSaveReboot; uint8_t mSendLastIvId; - uint8_t mSendTickerId; + bool mSendFirst; statistics_t mStat; - // timer - uint32_t mRxTicker; - // mqtt PubMqttType mMqtt; - bool mMqttActive; + bool mMqttReconnect; + bool mMqttEnabled; // sun int32_t mCalculatedTimezoneOffset; uint32_t mSunrise, mSunset; // plugins - #if defined(ENA_NOKIA) || defined(ENA_SSD1306) - MonoDisplayType mMonoDisplay; - #endif + DisplayType mDisplay; }; #endif /*__APP_H__*/ diff --git a/src/appInterface.h b/src/appInterface.h index 94f753992..a79dcdb17 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -1,20 +1,25 @@ //----------------------------------------------------------------------------- // 2022 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __IAPP_H__ #define __IAPP_H__ #include "defines.h" +#include "hm/hmSystem.h" // abstract interface to App. Make members of App accessible from child class // like web or API without forward declaration class IApp { public: virtual ~IApp() {} - virtual bool saveSettings() = 0; + virtual bool saveSettings(bool stopFs) = 0; + virtual bool readSettings(const char *path) = 0; virtual bool eraseSettings(bool eraseWifi) = 0; + virtual bool getSavePending() = 0; + virtual bool getLastSaveSucceed() = 0; + virtual void setOnUpdate() = 0; virtual void setRebootFlag() = 0; virtual const char *getVersion() = 0; virtual statistics_t *getStatistics() = 0; @@ -29,10 +34,14 @@ class IApp { virtual String getTimeStr(uint32_t offset) = 0; virtual uint32_t getTimezoneOffset() = 0; virtual void getSchedulerInfo(uint8_t *max) = 0; + virtual void getSchedulerNames() = 0; virtual bool getRebootRequestState() = 0; virtual bool getSettingsValid() = 0; virtual void setMqttDiscoveryFlag() = 0; + virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0; + + virtual void ivSendHighPrio(Inverter<> *iv) = 0; virtual bool getMqttIsConnected() = 0; virtual uint32_t getMqttRxCnt() = 0; diff --git a/src/config/config.h b/src/config/config.h index 2b5a06882..ac28c1a26 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -44,16 +44,26 @@ // default pinout (GPIO Number) #if defined(ESP32) + // this is the default ESP32 (son-S) pinout on the WROOM modules for VSPI, + // for the ESP32-S3 there is no sane 'default', as it has full flexibility + // to map its two HW SPIs anywhere and PCBs differ materially, + // so it has to be selected in the Web UI #define DEF_CS_PIN 5 #define DEF_CE_PIN 4 #define DEF_IRQ_PIN 16 + #define DEF_MISO_PIN 19 + #define DEF_MOSI_PIN 23 + #define DEF_SCLK_PIN 18 #else #define DEF_CS_PIN 15 #define DEF_CE_PIN 2 #define DEF_IRQ_PIN 0 + // these are given to relay the correct values via API + // they cannot actually be moved for ESP82xx models + #define DEF_MISO_PIN 12 + #define DEF_MOSI_PIN 13 + #define DEF_SCLK_PIN 14 #endif -#define DEF_LED0_PIN 255 // off -#define DEF_LED1_PIN 255 // off // default NRF24 power, possible values (0 - 3) #define DEF_AMPLIFIERPOWER 1 @@ -101,7 +111,7 @@ #define NTP_REFRESH_INTERVAL 12 * 3600 * 1000 // default mqtt interval -#define MQTT_INTERVAL 60 +#define MQTT_INTERVAL 90 // default MQTT broker uri #define DEF_MQTT_BROKER "\0" @@ -124,6 +134,13 @@ // reconnect delay #define MQTT_RECONNECT_DELAY 5000 +// Offset for midnight Ticker +// relative to UTC +// may be negative for later in the next day or positive for earlier in previous day +// may contain variable like mCalculatedTimezoneOffset +// must be in parentheses +#define MIDNIGHTTICKER_OFFSET (-1) + #if __has_include("config_override.h") #include "config_override.h" #endif diff --git a/src/config/config_override_example.h b/src/config/config_override_example.h index 443d8969e..e7c06b77b 100644 --- a/src/config/config_override_example.h +++ b/src/config/config_override_example.h @@ -17,17 +17,23 @@ #undef FB_WIFI_PWD #define FB_WIFI_PWD "MY_WIFI_KEY" -// ESP32 default pinout -#undef DEF_RF24_CS_PIN -#define DEF_RF24_CS_PIN 5 -#undef DEF_RF24_CE_PIN -#define DEF_RF24_CE_PIN 4 -#undef DEF_RF24_IRQ_PIN -#define DEF_RF24_IRQ_PIN 16 - - -// To enable the json endpoint at /json -// #define ENABLE_JSON_EP +// ESP32-S3 example pinout +#undef DEF_CS_PIN +#define DEF_CS_PIN 37 +#undef DEF_CE_PIN +#define DEF_CE_PIN 38 +#undef DEF_IRQ_PIN +#define DEF_IRQ_PIN 47 +#undef DEF_MISO_PIN +#define DEF_MISO_PIN 48 +#undef DEF_MOSI_PIN +#define DEF_MOSI_PIN 35 +#undef DEF_SCLK_PIN +#define DEF_SCLK_PIN 36 + +// Offset for midnight Ticker Example: 1 second before midnight (local time) +#undef MIDNIGHTTICKER_OFFSET +#define MIDNIGHTTICKER_OFFSET (mCalculatedTimezoneOffset + 1) // To enable the endpoint for prometheus to scrape data from at /metrics // #define ENABLE_PROMETHEUS_EP diff --git a/src/config/settings.h b/src/config/settings.h index 47c59a8c7..6d58b4061 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -1,22 +1,30 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __SETTINGS_H__ #define __SETTINGS_H__ #include -#include #include +#include + +#include "../defines.h" #include "../utils/dbg.h" #include "../utils/helper.h" -#include "../defines.h" + +#if defined(ESP32) + #define MAX_ALLOWED_BUF_SIZE ESP.getMaxAllocHeap() - 1024 +#else + #define MAX_ALLOWED_BUF_SIZE ESP.getMaxFreeBlockSize() - 1024 +#endif /** * More info: * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout * */ +#define DEF_PIN_OFF 255 #define PROT_MASK_INDEX 0x0001 @@ -50,6 +58,7 @@ typedef struct { char deviceName[DEVNAME_LEN]; char adminPwd[PWD_LEN]; uint16_t protectionMask; + bool darkMode; // wifi char stationSsid[SSID_LEN]; @@ -64,6 +73,9 @@ typedef struct { uint8_t pinCs; uint8_t pinCe; uint8_t pinIrq; + uint8_t pinMiso; + uint8_t pinMosi; + uint8_t pinSclk; uint8_t amplifierPower; } cfgNrf24_t; @@ -75,7 +87,7 @@ typedef struct { typedef struct { float lat; float lon; - bool disNightCom; // disable night communication + bool disNightCom; // disable night communication uint16_t offsetSec; } cfgSun_t; @@ -86,8 +98,8 @@ typedef struct { } cfgSerial_t; typedef struct { - uint8_t led0; // first LED pin - uint8_t led1; // second LED pin + uint8_t led0; // first LED pin + uint8_t led1; // second LED pin } cfgLed_t; typedef struct { @@ -96,6 +108,7 @@ typedef struct { char user[MQTT_USER_LEN]; char pwd[MQTT_PWD_LEN]; char topic[MQTT_TOPIC_LEN]; + uint16_t interval; } cfgMqtt_t; typedef struct { @@ -103,14 +116,39 @@ typedef struct { char name[MAX_NAME_LENGTH]; serial_u serial; uint16_t chMaxPwr[4]; + int32_t yieldCor[4]; // signed YieldTotal correction value char chName[4][MAX_NAME_LENGTH]; } cfgIv_t; typedef struct { bool enabled; cfgIv_t iv[MAX_NUM_INVERTERS]; + + bool rstYieldMidNight; + bool rstValsNotAvail; + bool rstValsCommStop; } cfgInst_t; +typedef struct { + uint8_t type; + bool pwrSaveAtIvOffline; + bool pxShift; + uint8_t rot; + //uint16_t wakeUp; + //uint16_t sleepAt; + uint8_t contrast; + uint8_t disp_data; + uint8_t disp_clk; + uint8_t disp_cs; + uint8_t disp_reset; + uint8_t disp_busy; + uint8_t disp_dc; +} display_t; + +typedef struct { + display_t display; +} plugins_t; + typedef struct { cfgSys_t sys; cfgNrf24_t nrf; @@ -120,12 +158,15 @@ typedef struct { cfgMqtt_t mqtt; cfgLed_t led; cfgInst_t inst; + plugins_t plugin; bool valid; } settings_t; class settings { public: - settings() {} + settings() { + mLastSaveSucceed = false; + } void setup() { DPRINTLN(DBG_INFO, F("Initializing FS ..")); @@ -145,16 +186,17 @@ class settings { if(!LittleFS.begin(LITTLFS_FALSE)) { DPRINTLN(DBG_INFO, F(".. format ..")); LittleFS.format(); - if(LittleFS.begin(LITTLFS_TRUE)) + if(LittleFS.begin(LITTLFS_TRUE)) { DPRINTLN(DBG_INFO, F(".. success")); - else + } else { DPRINTLN(DBG_INFO, F(".. failed")); + } } else DPRINTLN(DBG_INFO, F(" .. done")); - readSettings(); + readSettings("/settings.json"); } // should be used before OTA @@ -171,6 +213,10 @@ class settings { return mCfg.valid; } + inline bool getLastSaveSucceed() { + return mLastSaveSucceed; + } + void getInfo(uint32_t *used, uint32_t *size) { #if !defined(ESP32) FSInfo info; @@ -185,26 +231,28 @@ class settings { #endif } - void readSettings(void) { + bool readSettings(const char* path) { loadDefaults(); - File fp = LittleFS.open("/settings.json", "r"); + File fp = LittleFS.open(path, "r"); if(!fp) DPRINTLN(DBG_WARN, F("failed to load json, using default config")); else { //DPRINTLN(DBG_INFO, fp.readString()); //fp.seek(0, SeekSet); - DynamicJsonDocument root(4096); + DynamicJsonDocument root(MAX_ALLOWED_BUF_SIZE); DeserializationError err = deserializeJson(root, fp); + root.shrinkToFit(); if(!err && (root.size() > 0)) { mCfg.valid = true; - jsonWifi(root["wifi"]); - jsonNrf(root["nrf"]); - jsonNtp(root["ntp"]); - jsonSun(root["sun"]); - jsonSerial(root["serial"]); - jsonMqtt(root["mqtt"]); - jsonLed(root["led"]); - jsonInst(root["inst"]); + jsonWifi(root[F("wifi")]); + jsonNrf(root[F("nrf")]); + jsonNtp(root[F("ntp")]); + jsonSun(root[F("sun")]); + jsonSerial(root[F("serial")]); + jsonMqtt(root[F("mqtt")]); + jsonLed(root[F("led")]); + jsonPlugin(root[F("plugin")]); + jsonInst(root[F("inst")]); } else { Serial.println(F("failed to parse json, using default config")); @@ -212,17 +260,13 @@ class settings { fp.close(); } + return mCfg.valid; } - bool saveSettings(void) { + bool saveSettings() { DPRINTLN(DBG_DEBUG, F("save settings")); - File fp = LittleFS.open("/settings.json", "w"); - if(!fp) { - DPRINTLN(DBG_ERROR, F("can't open settings file!")); - return false; - } - DynamicJsonDocument json(4096); + DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE); JsonObject root = json.to(); jsonWifi(root.createNestedObject(F("wifi")), true); jsonNrf(root.createNestedObject(F("nrf")), true); @@ -231,14 +275,38 @@ class settings { jsonSerial(root.createNestedObject(F("serial")), true); jsonMqtt(root.createNestedObject(F("mqtt")), true); jsonLed(root.createNestedObject(F("led")), true); + jsonPlugin(root.createNestedObject(F("plugin")), true); jsonInst(root.createNestedObject(F("inst")), true); + DPRINT(DBG_INFO, F("memory usage: ")); + DBGPRINTLN(String(json.memoryUsage())); + DPRINT(DBG_INFO, F("capacity: ")); + DBGPRINTLN(String(json.capacity())); + DPRINT(DBG_INFO, F("max alloc: ")); + DBGPRINTLN(String(MAX_ALLOWED_BUF_SIZE)); + + if(json.overflowed()) { + DPRINTLN(DBG_ERROR, F("buffer too small!")); + mLastSaveSucceed = false; + return false; + } + + File fp = LittleFS.open("/settings.json", "w"); + if(!fp) { + DPRINTLN(DBG_ERROR, F("can't open settings file!")); + mLastSaveSucceed = false; + return false; + } + if(0 == serializeJson(root, fp)) { DPRINTLN(DBG_ERROR, F("can't write settings file!")); + mLastSaveSucceed = false; return false; } fp.close(); + DPRINTLN(DBG_INFO, F("settings saved")); + mLastSaveSucceed = true; return true; } @@ -263,6 +331,7 @@ class settings { memset(&mCfg, 0, sizeof(settings_t)); mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; + mCfg.sys.darkMode = false; // restore temp settings if(keepWifi) memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); @@ -278,6 +347,10 @@ class settings { mCfg.nrf.pinCs = DEF_CS_PIN; mCfg.nrf.pinCe = DEF_CE_PIN; mCfg.nrf.pinIrq = DEF_IRQ_PIN; + mCfg.nrf.pinMiso = DEF_MISO_PIN; + mCfg.nrf.pinMosi = DEF_MOSI_PIN; + mCfg.nrf.pinSclk = DEF_SCLK_PIN; + mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME); @@ -297,12 +370,28 @@ class settings { snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", DEF_MQTT_USER); snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); + mCfg.mqtt.interval = 0; // off - mCfg.led.led0 = DEF_LED0_PIN; - mCfg.led.led1 = DEF_LED1_PIN; + mCfg.inst.rstYieldMidNight = false; + mCfg.inst.rstValsNotAvail = false; + mCfg.inst.rstValsCommStop = false; + + mCfg.led.led0 = DEF_PIN_OFF; + mCfg.led.led1 = DEF_PIN_OFF; memset(&mCfg.inst, 0, sizeof(cfgInst_t)); - } + + mCfg.plugin.display.pwrSaveAtIvOffline = false; + mCfg.plugin.display.contrast = 60; + mCfg.plugin.display.pxShift = true; + mCfg.plugin.display.rot = 0; + mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA + mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL + mCfg.plugin.display.disp_cs = DEF_PIN_OFF; + mCfg.plugin.display.disp_reset = DEF_PIN_OFF; + mCfg.plugin.display.disp_busy = DEF_PIN_OFF; + mCfg.plugin.display.disp_dc = DEF_PIN_OFF; + } void jsonWifi(JsonObject obj, bool set = false) { if(set) { @@ -312,6 +401,7 @@ class settings { obj[F("dev")] = mCfg.sys.deviceName; obj[F("adm")] = mCfg.sys.adminPwd; obj[F("prot_mask")] = mCfg.sys.protectionMask; + obj[F("dark")] = mCfg.sys.darkMode; ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf); ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf); ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf); @@ -323,6 +413,7 @@ class settings { snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as()); snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as()); mCfg.sys.protectionMask = obj[F("prot_mask")]; + mCfg.sys.darkMode = obj[F("dark")]; ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as()); ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as()); ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as()); @@ -342,6 +433,9 @@ class settings { obj[F("cs")] = mCfg.nrf.pinCs; obj[F("ce")] = mCfg.nrf.pinCe; obj[F("irq")] = mCfg.nrf.pinIrq; + obj[F("sclk")] = mCfg.nrf.pinSclk; + obj[F("mosi")] = mCfg.nrf.pinMosi; + obj[F("miso")] = mCfg.nrf.pinMiso; obj[F("pwr")] = mCfg.nrf.amplifierPower; } else { mCfg.nrf.sendInterval = obj[F("intvl")]; @@ -349,7 +443,18 @@ class settings { mCfg.nrf.pinCs = obj[F("cs")]; mCfg.nrf.pinCe = obj[F("ce")]; mCfg.nrf.pinIrq = obj[F("irq")]; + mCfg.nrf.pinSclk = obj[F("sclk")]; + mCfg.nrf.pinMosi = obj[F("mosi")]; + mCfg.nrf.pinMiso = obj[F("miso")]; mCfg.nrf.amplifierPower = obj[F("pwr")]; + if((obj[F("cs")] == obj[F("ce")])) { + mCfg.nrf.pinCs = DEF_CS_PIN; + mCfg.nrf.pinCe = DEF_CE_PIN; + mCfg.nrf.pinIrq = DEF_IRQ_PIN; + mCfg.nrf.pinSclk = DEF_SCLK_PIN; + mCfg.nrf.pinMosi = DEF_MOSI_PIN; + mCfg.nrf.pinMiso = DEF_MISO_PIN; + } } } @@ -396,8 +501,11 @@ class settings { obj[F("user")] = mCfg.mqtt.user; obj[F("pwd")] = mCfg.mqtt.pwd; obj[F("topic")] = mCfg.mqtt.topic; + obj[F("intvl")] = mCfg.mqtt.interval; + } else { - mCfg.mqtt.port = obj[F("port")]; + mCfg.mqtt.port = obj[F("port")]; + mCfg.mqtt.interval = obj[F("intvl")]; snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as()); snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as()); snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as()); @@ -415,20 +523,66 @@ class settings { } } + void jsonPlugin(JsonObject obj, bool set = false) { + if(set) { + JsonObject disp = obj.createNestedObject("disp"); + disp[F("type")] = mCfg.plugin.display.type; + disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; + disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift; + disp[F("rotation")] = mCfg.plugin.display.rot; + //disp[F("wake")] = mCfg.plugin.display.wakeUp; + //disp[F("sleep")] = mCfg.plugin.display.sleepAt; + disp[F("contrast")] = mCfg.plugin.display.contrast; + disp[F("data")] = mCfg.plugin.display.disp_data; + disp[F("clock")] = mCfg.plugin.display.disp_clk; + disp[F("cs")] = mCfg.plugin.display.disp_cs; + disp[F("reset")] = mCfg.plugin.display.disp_reset; + disp[F("busy")] = mCfg.plugin.display.disp_busy; + disp[F("dc")] = mCfg.plugin.display.disp_dc; + } else { + JsonObject disp = obj["disp"]; + mCfg.plugin.display.type = disp[F("type")]; + mCfg.plugin.display.pwrSaveAtIvOffline = (bool)disp[F("pwrSafe")]; + mCfg.plugin.display.pxShift = (bool)disp[F("pxShift")]; + mCfg.plugin.display.rot = disp[F("rotation")]; + //mCfg.plugin.display.wakeUp = disp[F("wake")]; + //mCfg.plugin.display.sleepAt = disp[F("sleep")]; + mCfg.plugin.display.contrast = disp[F("contrast")]; + mCfg.plugin.display.disp_data = disp[F("data")]; + mCfg.plugin.display.disp_clk = disp[F("clock")]; + mCfg.plugin.display.disp_cs = disp[F("cs")]; + mCfg.plugin.display.disp_reset = disp[F("reset")]; + mCfg.plugin.display.disp_busy = disp[F("busy")]; + mCfg.plugin.display.disp_dc = disp[F("dc")]; + } + } + void jsonInst(JsonObject obj, bool set = false) { - if(set) + if(set) { obj[F("en")] = (bool)mCfg.inst.enabled; - else + obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; + obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; + } + else { mCfg.inst.enabled = (bool)obj[F("en")]; + mCfg.inst.rstYieldMidNight = (bool)obj["rstMidNight"]; + mCfg.inst.rstValsNotAvail = (bool)obj["rstNotAvail"]; + mCfg.inst.rstValsCommStop = (bool)obj["rstComStop"]; + } JsonArray ivArr; if(set) ivArr = obj.createNestedArray(F("iv")); for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { - if(set) - jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); - else - jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); + if(set) { + if(mCfg.inst.iv[i].serial.u64 != 0ULL) + jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); + } + else { + if(!obj[F("iv")][i].isNull()) + jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); + } } } @@ -438,7 +592,8 @@ class settings { obj[F("name")] = cfg->name; obj[F("sn")] = cfg->serial.u64; for(uint8_t i = 0; i < 4; i++) { - obj[F("pwr")][i] = cfg->chMaxPwr[i]; + obj[F("yield")][i] = cfg->yieldCor[i]; + obj[F("pwr")][i] = cfg->chMaxPwr[i]; obj[F("chName")][i] = cfg->chName[i]; } } else { @@ -446,6 +601,7 @@ class settings { snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as()); cfg->serial.u64 = obj[F("sn")]; for(uint8_t i = 0; i < 4; i++) { + cfg->yieldCor[i] = obj[F("yield")][i]; cfg->chMaxPwr[i] = obj[F("pwr")][i]; snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as()); } @@ -453,6 +609,7 @@ class settings { } settings_t mCfg; + bool mLastSaveSucceed; }; #endif /*__SETTINGS_H__*/ diff --git a/src/defines.h b/src/defines.h index ddfda35e1..1be438ffe 100644 --- a/src/defines.h +++ b/src/defines.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #ifndef __DEFINES_H__ @@ -12,12 +12,13 @@ // VERSION //------------------------------------- #define VERSION_MAJOR 0 -#define VERSION_MINOR 5 -#define VERSION_PATCH 66 +#define VERSION_MINOR 6 +#define VERSION_PATCH 0 //------------------------------------- typedef struct { - uint8_t rxCh; + uint8_t ch; + uint8_t len; uint8_t packet[MAX_RF_PAYLOAD_SIZE]; } packet_t; @@ -68,7 +69,7 @@ union serial_u { uint8_t b[8]; }; -#define MIN_SERIAL_INTERVAL 5 +#define MIN_SERIAL_INTERVAL 2 // 5 #define MIN_SEND_INTERVAL 15 #define MIN_MQTT_INTERVAL 60 diff --git a/src/hm/CircularBuffer.h b/src/hm/CircularBuffer.h deleted file mode 100644 index 65c9e7685..000000000 --- a/src/hm/CircularBuffer.h +++ /dev/null @@ -1,161 +0,0 @@ -/* - CircularBuffer - An Arduino circular buffering library for arbitrary types. - - Created by Ivo Pullens, Emmission, 2014 -- www.emmission.nl - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#ifndef CircularBuffer_h -#define CircularBuffer_h - -#if defined(ESP8266) || defined(ESP32) -#define DISABLE_IRQ noInterrupts() -#define RESTORE_IRQ interrupts() -#else -#define DISABLE_IRQ \ - uint8_t sreg = SREG; \ - cli(); - -#define RESTORE_IRQ \ - SREG = sreg; -#endif - -template -class CircularBuffer { - - typedef BUFFERTYPE BufferType; - BufferType Buffer[BUFFERSIZE]; - - public: - CircularBuffer() : m_buff(Buffer) { - m_size = BUFFERSIZE; - clear(); - } - - /** Clear all entries in the circular buffer. */ - void clear(void) - { - m_front = 0; - m_fill = 0; - } - - /** Test if the circular buffer is empty */ - inline bool empty(void) const - { - return !m_fill; - } - - /** Return the number of records stored in the buffer */ - inline uint8_t available(void) const - { - return m_fill; - } - - /** Test if the circular buffer is full */ - inline bool full(void) const - { - return m_fill == m_size; - } - - inline uint8_t getFill(void) const { - return m_fill; - } - - /** Aquire record on front of the buffer, for writing. - * After filling the record, it has to be pushed to actually - * add it to the buffer. - * @return Pointer to record, or NULL when buffer is full. - */ - BUFFERTYPE* getFront(void) const - { - DISABLE_IRQ; - BUFFERTYPE* f = NULL; - if (!full()) - f = get(m_front); - RESTORE_IRQ; - return f; - } - - /** Push record to front of the buffer - * @param record Record to push. If record was aquired previously (using getFront) its - * data will not be copied as it is already present in the buffer. - * @return True, when record was pushed successfully. - */ - bool pushFront(BUFFERTYPE* record) - { - bool ok = false; - DISABLE_IRQ; - if (!full()) - { - BUFFERTYPE* f = get(m_front); - if (f != record) - *f = *record; - m_front = (m_front+1) % m_size; - m_fill++; - ok = true; - } - RESTORE_IRQ; - return ok; - } - - /** Aquire record on back of the buffer, for reading. - * After reading the record, it has to be pop'ed to actually - * remove it from the buffer. - * @return Pointer to record, or NULL when buffer is empty. - */ - BUFFERTYPE* getBack(void) const - { - BUFFERTYPE* b = NULL; - DISABLE_IRQ; - if (!empty()) - b = get(back()); - RESTORE_IRQ; - return b; - } - - /** Remove record from back of the buffer. - * @return True, when record was pop'ed successfully. - */ - bool popBack(void) - { - bool ok = false; - DISABLE_IRQ; - if (!empty()) - { - m_fill--; - ok = true; - } - RESTORE_IRQ; - return ok; - } - - protected: - inline BUFFERTYPE * get(const uint8_t idx) const - { - return &(m_buff[idx]); - } - inline uint8_t back(void) const - { - return (m_front - m_fill + m_size) % m_size; - } - - uint8_t m_size; // Total number of records that can be stored in the buffer. - BUFFERTYPE* const m_buff; - volatile uint8_t m_front; // Index of front element (not pushed yet). - volatile uint8_t m_fill; // Amount of records currently pushed. -}; - -#endif // CircularBuffer_h diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h index 21af98f0a..bd12f72ba 100644 --- a/src/hm/hmDefines.h +++ b/src/hm/hmDefines.h @@ -9,6 +9,9 @@ #include "../utils/dbg.h" #include +// inverter generations +enum {IV_HM = 0, IV_MI}; + // units enum {UNIT_V = 0, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_HZ, UNIT_C, UNIT_PCT, UNIT_VAR, UNIT_NONE}; const char* const units[] = {"V", "A", "W", "Wh", "kWh", "Hz", "°C", "%", "var", ""}; @@ -23,9 +26,13 @@ enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT, const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", "U_AC", "I_AC", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC", "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","HWPartId", - "active PowerLimit", /*"reactive PowerLimit","Powerfactor",*/ "LastAlarmCode"}; + "active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode"}; const char* const notAvail = "n/a"; +const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH, + UNIT_V, UNIT_A, UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR, + UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE}; + // mqtt discovery device classes enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; const char* const deviceClasses[] = {0, "current", "energy", "power", "voltage", "frequency", "temperature"}; @@ -106,7 +113,7 @@ const byteAssign_t AlarmDataAssignment[] = { }; #define HMALARMDATA_LIST_LEN (sizeof(AlarmDataAssignment) / sizeof(byteAssign_t)) #define HMALARMDATA_PAYLOAD_LEN 0 // 0: means check is off - +#define ALARM_LOG_ENTRY_SIZE 12 //------------------------------------- diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 8c1fccc84..b1390b29f 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 +// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -105,32 +105,35 @@ const calcFunc_t calcFunctions[] = { template class Inverter { public: - cfgIv_t *config; // stored settings - uint8_t id; // unique id - uint8_t type; // integer which refers to inverter type - uint16_t alarmMesIndex; // Last recorded Alarm Message Index - uint16_t powerLimit[2]; // limit power output - float actPowerLimit; // actual power limit - uint8_t devControlCmd; // carries the requested cmd - bool devControlRequest; // true if change needed - serial_u radioId; // id converted to modbus - uint8_t channels; // number of PV channels (1-4) - record_t recordMeas; // structure for measured values - record_t recordInfo; // structure for info values - record_t recordConfig; // structure for system config values - record_t recordAlarm; // structure for alarm values - String lastAlarmMsg; - bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) + uint8_t ivGen; // generation of inverter (HM / MI) + cfgIv_t *config; // stored settings + uint8_t id; // unique id + uint8_t type; // integer which refers to inverter type + uint16_t alarmMesIndex; // Last recorded Alarm Message Index + uint16_t powerLimit[2]; // limit power output + float actPowerLimit; // actual power limit + uint8_t devControlCmd; // carries the requested cmd + serial_u radioId; // id converted to modbus + uint8_t channels; // number of PV channels (1-4) + record_t recordMeas; // structure for measured values + record_t recordInfo; // structure for info values + record_t recordConfig; // structure for system config values + record_t recordAlarm; // structure for alarm values + //String lastAlarmMsg; + bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) + bool isConnected; // shows if inverter was successfully identified (fw version and hardware info) Inverter() { - powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited - powerLimit[1] = AbsolutNonPersistent; // default power limit setting - actPowerLimit = 0xffff; // init feedback from inverter to -1 - devControlRequest = false; - devControlCmd = InitDataState; - initialized = false; - lastAlarmMsg = "nothing"; - alarmMesIndex = 0; + ivGen = IV_HM; + powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited + powerLimit[1] = AbsolutNonPersistent; // default power limit setting + actPowerLimit = 0xffff; // init feedback from inverter to -1 + mDevControlRequest = false; + devControlCmd = InitDataState; + initialized = false; + //lastAlarmMsg = "nothing"; + alarmMesIndex = 0; + isConnected = false; } ~Inverter() { @@ -140,7 +143,9 @@ class Inverter { template void enqueCommand(uint8_t cmd) { _commandQueue.push(std::make_shared(cmd)); - DPRINTLN(DBG_INFO, F("(#") + String(id) + F(") enqueuedCmd: ") + String(cmd)); + DPRINT_IVID(DBG_INFO, id); + DBGPRINT(F("enqueCommand: 0x")); + DBGHEXLN(cmd); } void setQueuedCmdFinished() { @@ -158,13 +163,24 @@ class Inverter { } } - uint8_t getQueuedCmd() { + uint8_t getQueuedCmd() { if (_commandQueue.empty()) { - if (getFwVersion() == 0) - enqueCommand(InverterDevInform_All); - enqueCommand(RealTimeRunData_Debug); - if (actPowerLimit == 0xffff) - enqueCommand(SystemConfigPara); + if (ivGen != IV_MI) { + if (getFwVersion() == 0) + enqueCommand(InverterDevInform_All); // firmware version + enqueCommand(RealTimeRunData_Debug); // live data + } else if (ivGen == IV_MI){ + if (getFwVersion() == 0) + enqueCommand(InverterDevInform_All); // firmware version; might not work, esp. for 1/2 ch hardware + if (type == INV_TYPE_4CH) { + enqueCommand(0x36); + } else { + enqueCommand(0x09); + } + } + + if ((actPowerLimit == 0xffff) && isConnected) + enqueCommand(SystemConfigPara); // power limit info } return _commandQueue.front().get()->getCmd(); } @@ -219,6 +235,22 @@ class Inverter { return 0; } + bool setDevControlRequest(uint8_t cmd) { + if(isConnected) { + mDevControlRequest = true; + devControlCmd = cmd; + } + return isConnected; + } + + void clearDevControlRequest() { + mDevControlRequest = false; + } + + inline bool getDevControlRequest() { + return mDevControlRequest; + } + void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:addValue")); if(NULL != rec) { @@ -233,11 +265,12 @@ class Inverter { val <<= 8; val |= buf[ptr]; } while(++ptr != end); - if(FLD_T == rec->assign[pos].fieldId) { + if (FLD_T == rec->assign[pos].fieldId) { // temperature is a signed value! rec->record[pos] = (REC_TYP)((int16_t)val) / (REC_TYP)(div); - } - else { + } else if (FLD_YT == rec->assign[pos].fieldId) { + rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); + } else { if ((REC_TYP)(div) > 1) rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); else @@ -254,30 +287,31 @@ class Inverter { if (alarmMesIndex < rec->record[pos]){ alarmMesIndex = rec->record[pos]; //enqueCommand(AlarmUpdate); // What is the function of AlarmUpdate? + + DPRINT(DBG_INFO, "alarm ID incremented to "); + DBGPRINTLN(String(alarmMesIndex)); enqueCommand(AlarmData); } - else { - alarmMesIndex = rec->record[pos]; // no change - } } } else if (rec->assign == InfoAssignment) { DPRINTLN(DBG_DEBUG, "add info"); // eg. fw version ... + isConnected = true; } else if (rec->assign == SystemConfigParaAssignment) { DPRINTLN(DBG_DEBUG, "add config"); - // get at least the firmware version and save it to the inverter object if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ actPowerLimit = rec->record[pos]; - DPRINT(DBG_DEBUG, F("Inverter actual power limit: ") + String(actPowerLimit, 1)); + DPRINT(DBG_DEBUG, F("Inverter actual power limit: ")); + DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1)); } } else if (rec->assign == AlarmDataAssignment) { DPRINTLN(DBG_DEBUG, "add alarm"); - if (getPosByChFld(0, FLD_LAST_ALARM_CODE, rec) == pos){ - lastAlarmMsg = getAlarmStr(rec->record[pos]); - } + //if (getPosByChFld(0, FLD_LAST_ALARM_CODE, rec) == pos){ + // lastAlarmMsg = getAlarmStr(rec->record[pos]); + //} } else DPRINTLN(DBG_WARN, F("add with unknown assginment")); @@ -286,6 +320,37 @@ class Inverter { DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x")); } + /*inline REC_TYP getPowerLimit(void) { + record_t<> *rec = getRecordStruct(SystemConfigPara); + return getChannelFieldValue(CH0, FLD_ACT_ACTIVE_PWR_LIMIT, rec); + }*/ + + bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) { + DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue")); + if(NULL == rec) + return false; + if(pos > rec->length) + return false; + rec->record[pos] = val; + return true; + } + + REC_TYP getChannelFieldValue(uint8_t channel, uint8_t fieldId, record_t<> *rec) { + uint8_t pos = 0; + if(NULL != rec) { + for(; pos < rec->length; pos++) { + if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId)) + break; + } + if(pos >= rec->length) + return 0; + + return rec->record[pos]; + } + else + return 0; + } + REC_TYP getValue(uint8_t pos, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue")); if(NULL == rec) @@ -306,16 +371,23 @@ class Inverter { } } - bool isAvailable(uint32_t timestamp, record_t<> *rec) { - DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isAvailable")); - return ((timestamp - rec->ts) < INACT_THRES_SEC); + bool isAvailable(uint32_t timestamp) { + if((timestamp - recordMeas.ts) < INACT_THRES_SEC) + return true; + if((timestamp - recordInfo.ts) < INACT_THRES_SEC) + return true; + if((timestamp - recordConfig.ts) < INACT_THRES_SEC) + return true; + if((timestamp - recordAlarm.ts) < INACT_THRES_SEC) + return true; + return false; } - bool isProducing(uint32_t timestamp, record_t<> *rec) { + bool isProducing(uint32_t timestamp) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing")); - if(isAvailable(timestamp, rec)) { - uint8_t pos = getPosByChFld(CH0, FLD_PAC, rec); - return (getValue(pos, rec) > INACT_PWR_THRESH); + if(isAvailable(timestamp)) { + uint8_t pos = getPosByChFld(CH0, FLD_PAC, &recordMeas); + return (getValue(pos, &recordMeas) > INACT_PWR_THRESH); } return false; } @@ -333,10 +405,10 @@ class Inverter { record_t<> *getRecordStruct(uint8_t cmd) { switch (cmd) { - case RealTimeRunData_Debug: return &recordMeas; - case InverterDevInform_All: return &recordInfo; - case SystemConfigPara: return &recordConfig; - case AlarmData: return &recordAlarm; + case RealTimeRunData_Debug: return &recordMeas; // 11 = 0x0b + case InverterDevInform_All: return &recordInfo; // 1 = 0x01 + case SystemConfigPara: return &recordConfig; // 5 = 0x05 + case AlarmData: return &recordAlarm; // 17 = 0x11 default: break; } return NULL; @@ -399,7 +471,27 @@ class Inverter { } } - String getAlarmStr(u_int16_t alarmCode) { + uint16_t parseAlarmLog(uint8_t id, uint8_t pyld[], uint8_t len, uint32_t *start, uint32_t *endTime) { + uint8_t startOff = 2 + id * ALARM_LOG_ENTRY_SIZE; + if((startOff + ALARM_LOG_ENTRY_SIZE) > len) + return 0; + + uint16_t wCode = ((uint16_t)pyld[startOff]) << 8 | pyld[startOff+1]; + uint32_t startTimeOffset = 0, endTimeOffset = 0; + + if (((wCode >> 13) & 0x01) == 1) // check if is AM or PM + startTimeOffset = 12 * 60 * 60; + if (((wCode >> 12) & 0x01) == 1) // check if is AM or PM + endTimeOffset = 12 * 60 * 60; + + *start = (((uint16_t)pyld[startOff + 4] << 8) | ((uint16_t)pyld[startOff + 5])) + startTimeOffset; + *endTime = (((uint16_t)pyld[startOff + 6] << 8) | ((uint16_t)pyld[startOff + 7])) + endTimeOffset; + + DPRINTLN(DBG_INFO, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(*start) + ", end: " + ah::getTimeStr(*endTime)); + return pyld[startOff+1]; + } + + String getAlarmStr(uint16_t alarmCode) { switch (alarmCode) { // breaks are intentionally missing! case 1: return String(F("Inverter start")); case 2: return String(F("DTU command failed")); @@ -474,7 +566,6 @@ class Inverter { } private: - std::queue> _commandQueue; void toRadioId(void) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:toRadioId")); radioId.u64 = 0ULL; @@ -484,6 +575,9 @@ class Inverter { radioId.b[1] = config->serial.b[3]; radioId.b[0] = 0x01; } + + std::queue> _commandQueue; + bool mDevControlRequest; // true if change needed }; diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h new file mode 100644 index 000000000..0af8f466c --- /dev/null +++ b/src/hm/hmPayload.h @@ -0,0 +1,419 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __HM_PAYLOAD_H__ +#define __HM_PAYLOAD_H__ + +#include "../utils/dbg.h" +#include "../utils/crc.h" +#include "../config/config.h" +#include + +typedef struct { + uint8_t txCmd; + uint8_t txId; + uint8_t invId; + uint32_t ts; + uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; + uint8_t len[MAX_PAYLOAD_ENTRIES]; + bool complete; + uint8_t maxPackId; + bool lastFound; + uint8_t retransmits; + bool requested; + bool gotFragment; +} invPayload_t; + + +typedef std::function payloadListenerType; +typedef std::function alarmListenerType; + + +template +class HmPayload { + public: + HmPayload() {} + + void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { + mApp = app; + mSys = sys; + mStat = stat; + mMaxRetrans = maxRetransmits; + mTimestamp = timestamp; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + reset(i); + } + mSerialDebug = false; + mHighPrioIv = NULL; + mCbAlarm = NULL; + mCbPayload = NULL; + } + + void enableSerialDebug(bool enable) { + mSerialDebug = enable; + } + + void addPayloadListener(payloadListenerType cb) { + mCbPayload = cb; + } + + void addAlarmListener(alarmListenerType cb) { + mCbAlarm = cb; + } + + void loop() { + if(NULL != mHighPrioIv) { + ivSend(mHighPrioIv, true); + mHighPrioIv = NULL; + } + } + + void zeroYieldDay(Inverter<> *iv) { + DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + uint8_t pos; + for(uint8_t ch = 0; ch < iv->channels; ch++) { + pos = iv->getPosByChFld(CH0, FLD_YD, rec); + iv->setValue(pos, rec, 0.0f); + } + } + + void zeroInverterValues(Inverter<> *iv) { + DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + for(uint8_t ch = 0; ch <= iv->channels; ch++) { + uint8_t pos = 0; + for(uint8_t fld = 0; fld < FLD_EVT; fld++) { + switch(fld) { + case FLD_YD: + case FLD_YT: + continue; + } + pos = iv->getPosByChFld(ch, fld, rec); + iv->setValue(pos, rec, 0.0f); + } + } + + notify(RealTimeRunData_Debug); + } + + void ivSendHighPrio(Inverter<> *iv) { + mHighPrioIv = iv; + } + + void ivSend(Inverter<> *iv, bool highPrio = false) { + if(!highPrio) { + if (mPayload[iv->id].requested) { + if (!mPayload[iv->id].complete) + process(false); // no retransmit + + if (!mPayload[iv->id].complete) { + if (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId) + mStat->rxFailNoAnser++; // got nothing + else + mStat->rxFail++; // got fragments but not complete response + + iv->setQueuedCmdFinished(); // command failed + if (mSerialDebug) { + DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout")); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("no Payload received! (retransmits: ")); + DBGPRINT(String(mPayload[iv->id].retransmits)); + DBGPRINTLN(F(")")); + } + } + } + } + + reset(iv->id); + mPayload[iv->id].requested = true; + + yield(); + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Requesting Inv SN ")); + DBGPRINTLN(String(iv->config->serial.u64, HEX)); + } + + if (iv->getDevControlRequest()) { + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Devcontrol request 0x")); + DBGPRINT(String(iv->devControlCmd, HEX)); + DBGPRINT(F(" power limit ")); + DBGPRINTLN(String(iv->powerLimit[0])); + } + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false); + mPayload[iv->id].txCmd = iv->devControlCmd; + //iv->clearCmdQueue(); + //iv->enqueCommand(SystemConfigPara); // read back power limit + } else { + uint8_t cmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(cmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); + mPayload[iv->id].txCmd = cmd; + } + } + + void add(Inverter<> *iv, packet_t *p) { + if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command + mPayload[iv->id].txId = p->packet[0]; + DPRINTLN(DBG_DEBUG, F("Response from info request received")); + uint8_t *pid = &p->packet[9]; + if (*pid == 0x00) { + DPRINTLN(DBG_DEBUG, F("fragment number zero received and ignored")); + } else { + DPRINT(DBG_DEBUG, F("PID: 0x")); + DPRINTLN(DBG_DEBUG, String(*pid, HEX)); + if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { + memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); + mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; + mPayload[iv->id].gotFragment = true; + } + + if ((*pid & ALL_FRAMES) == ALL_FRAMES) { + // Last packet + if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) { + mPayload[iv->id].maxPackId = (*pid & 0x7f); + if (*pid > 0x81) + mPayload[iv->id].lastFound = true; + } + } + } + } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command + DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); + + mPayload[iv->id].txId = p->packet[0]; + iv->clearDevControlRequest(); + + if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { + bool ok = true; + if((p->packet[10] == 0x00) && (p->packet[11] == 0x00)) + mApp->setMqttPowerLimitAck(iv); + else + ok = false; + + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("has ")); + if(!ok) DBGPRINT(F("not ")); + DBGPRINT(F("accepted power limit set point ")); + DBGPRINT(String(iv->powerLimit[0])); + DBGPRINT(F(" with PowerLimitControl ")); + DBGPRINTLN(String(iv->powerLimit[1])); + + iv->clearCmdQueue(); + iv->enqueCommand(SystemConfigPara); // read back power limit + } + iv->devControlCmd = Init; + } + } + + void process(bool retransmit) { + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + if (IV_MI == iv->ivGen) // only process HM inverters + continue; // skip to next inverter + + if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { + // no processing needed if txId is not 0x95 + mPayload[iv->id].complete = true; + continue; // skip to next inverter + } + + if (!mPayload[iv->id].complete) { + bool crcPass, pyldComplete; + crcPass = build(iv->id, &pyldComplete); + if (!crcPass && !pyldComplete) { // payload not complete + if ((mPayload[iv->id].requested) && (retransmit)) { + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { + // This is required to prevent retransmissions without answer. + DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else if(iv->devControlCmd == ActivePowerContr) { + DPRINT_IVID(DBG_INFO, iv->id); + DPRINTLN(DBG_INFO, F("retransmit power limit")); + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); + } else { + if(false == mPayload[iv->id].gotFragment) { + /* + DPRINTLN(DBG_WARN, F("nothing received: Request Complete Retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + */ + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("nothing received")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else { + for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { + if (mPayload[iv->id].len[i] == 0) { + DPRINT_IVID(DBG_WARN, iv->id); + DBGPRINT(F("Frame ")); + DBGPRINT(String(i + 1)); + DBGPRINTLN(F(" missing: Request Retransmit")); + mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); + break; // only request retransmit one frame per loop + } + yield(); + } + } + } + } + } + } else if(!crcPass && pyldComplete) { // crc error on complete Payload + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + } + } else { // payload complete + DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + DPRINT(DBG_INFO, F("procPyld: txid: 0x")); + DBGHEXLN(mPayload[iv->id].txId); + DPRINT(DBG_DEBUG, F("procPyld: max: ")); + DPRINTLN(DBG_DEBUG, String(mPayload[iv->id].maxPackId)); + record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser + mPayload[iv->id].complete = true; + + uint8_t payload[128]; + uint8_t payloadLen = 0; + + memset(payload, 0, 128); + + for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { + memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + payloadLen += (mPayload[iv->id].len[i]); + yield(); + } + payloadLen -= 2; + + if (mSerialDebug) { + DPRINT(DBG_INFO, F("Payload (")); + DBGPRINT(String(payloadLen)); + DBGPRINT(F("): ")); + mSys->Radio.dumpBuf(payload, payloadLen); + } + + if (NULL == rec) { + DPRINTLN(DBG_ERROR, F("record is NULL!")); + } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { + if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) + mStat->rxSuccess++; + + rec->ts = mPayload[iv->id].ts; + for (uint8_t i = 0; i < rec->length; i++) { + iv->addValue(i, payload, rec); + yield(); + } + iv->doCalculations(); + notify(mPayload[iv->id].txCmd); + + if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + } + } else { + DPRINT(DBG_ERROR, F("plausibility check failed, expected ")); + DBGPRINT(String(rec->pyldLen)); + DBGPRINTLN(F(" bytes")); + mStat->rxFail++; + } + + iv->setQueuedCmdFinished(); + } + } + yield(); + } + } + + private: + void notify(uint8_t val) { + if(NULL != mCbPayload) + (mCbPayload)(val); + } + + void notify(uint16_t code, uint32_t start, uint32_t endTime) { + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, endTime); + } + + bool build(uint8_t id, bool *complete) { + DPRINTLN(DBG_VERBOSE, F("build")); + uint16_t crc = 0xffff, crcRcv = 0x0000; + if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + + // check if all fragments are there + *complete = true; + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if(mPayload[id].len[i] == 0) + *complete = false; + } + if(!*complete) + return false; + + for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { + if (mPayload[id].len[i] > 0) { + if (i == (mPayload[id].maxPackId - 1)) { + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); + crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); + } else + crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); + } + yield(); + } + + return (crc == crcRcv) ? true : false; + } + + void reset(uint8_t id) { + DPRINT(DBG_INFO, "resetPayload: id: "); + DBGPRINTLN(String(id)); + memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); + mPayload[id].txCmd = 0; + mPayload[id].gotFragment = false; + mPayload[id].retransmits = 0; + mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + mPayload[id].lastFound = false; + mPayload[id].complete = false; + mPayload[id].requested = false; + mPayload[id].ts = *mTimestamp; + } + + IApp *mApp; + HMSYSTEM *mSys; + statistics_t *mStat; + uint8_t mMaxRetrans; + uint32_t *mTimestamp; + invPayload_t mPayload[MAX_NUM_INVERTERS]; + bool mSerialDebug; + Inverter<> *mHighPrioIv; + + alarmListenerType mCbAlarm; + payloadListenerType mCbPayload; +}; + +#endif /*__HM_PAYLOAD_H__*/ diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index 13508a472..c37ab7d05 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://github.com/lumpapu/ahoy +// 2023 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -9,28 +9,12 @@ #include "../utils/dbg.h" #include #include "../utils/crc.h" -#ifndef DISABLE_IRQ - #if defined(ESP8266) || defined(ESP32) - #define DISABLE_IRQ noInterrupts() - #define RESTORE_IRQ interrupts() - #else - #define DISABLE_IRQ \ - uint8_t sreg = SREG; \ - cli(); +#include "../config/config.h" +#include "SPI.h" - #define RESTORE_IRQ \ - SREG = sreg; - #endif -#endif -//#define CHANNEL_HOP // switch between channels or use static channel to send +#define SPI_SPEED 1000000 -#define DEFAULT_RECV_CHANNEL 3 -#define SPI_SPEED 1000000 - -#define DUMMY_RADIO_ID ((uint64_t)0xDEADBEEF01ULL) - -#define RF_CHANNELS 5 -#define RF_LOOP_CNT 300 +#define RF_CHANNELS 5 #define TX_REQ_INFO 0x15 #define TX_REQ_DEVCONTROL 0x51 @@ -61,11 +45,10 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; #define BIT_CNT(x) ((x)<<3) - //----------------------------------------------------------------------------- // HM Radio class //----------------------------------------------------------------------------- -template +template class HmRadio { public: HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { @@ -84,22 +67,21 @@ class HmRadio { mRfChLst[3] = 61; mRfChLst[4] = 75; + // default channels mTxChIdx = 2; // Start TX with 40 mRxChIdx = 0; // Start RX with 03 - mRxLoopCnt = RF_LOOP_CNT; - mSendCnt = 0; + mSendCnt = 0; + mRetransmits = 0; - mSerialDebug = false; - mIrqRcvd = false; + mSerialDebug = false; + mIrqRcvd = false; } ~HmRadio() {} - void setup(BUFFER *ctrl, uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN) { + void setup(uint8_t ampPwr = RF24_PA_LOW, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) { DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); pinMode(irq, INPUT_PULLUP); - mBufCtrl = ctrl; - uint32_t dtuSn = 0x87654321; uint32_t chipID = 0; // will be filled with last 3 bytes of MAC @@ -119,28 +101,36 @@ class HmRadio { // change the byte order of the DTU serial number and append the required 0x01 at the end DTU_RADIO_ID = ((uint64_t)(((dtuSn >> 24) & 0xFF) | ((dtuSn >> 8) & 0xFF00) | ((dtuSn << 8) & 0xFF0000) | ((dtuSn << 24) & 0xFF000000)) << 8) | 0x01; - mNrf24.begin(ce, cs); - mNrf24.setRetries(0, 0); + #ifdef ESP32 + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + mSpi = new SPIClass(FSPI); + #else + mSpi = new SPIClass(VSPI); + #endif + mSpi->begin(sclk, miso, mosi, cs); + #else + //the old ESP82xx cannot freely place their SPI pins + mSpi = new SPIClass(); + mSpi->begin(); + #endif + mNrf24.begin(mSpi, ce, cs); + mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms - mNrf24.setChannel(DEFAULT_RECV_CHANNEL); + mNrf24.setChannel(mRfChLst[mRxChIdx]); + mNrf24.startListening(); mNrf24.setDataRate(RF24_250KBPS); + mNrf24.setAutoAck(true); + mNrf24.enableDynamicPayloads(); mNrf24.setCRCLength(RF24_CRC_16); - mNrf24.setAutoAck(false); - mNrf24.setPayloadSize(MAX_RF_PAYLOAD_SIZE); mNrf24.setAddressWidth(5); mNrf24.openReadingPipe(1, DTU_RADIO_ID); - mNrf24.enableDynamicPayloads(); - // enable only receiving interrupts - mNrf24.maskIRQ(true, true, false); + // enable all receiving interrupts + mNrf24.maskIRQ(false, false, false); DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr])); mNrf24.setPALevel(ampPwr & 0x03); - mNrf24.startListening(); - - - mTxCh = setDefaultChannels(); if(mNrf24.isChipConnected()) { DPRINTLN(DBG_INFO, F("Radio Config:")); @@ -150,77 +140,95 @@ class HmRadio { DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); } - void loop(void) { - if(mIrqRcvd) { - DISABLE_IRQ; - mIrqRcvd = false; - bool tx_ok, tx_fail, rx_ready; - mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH - RESTORE_IRQ; - uint8_t pipe, len; - packet_t *p; - while(mNrf24.available(&pipe)) { - if(!mBufCtrl->full()) { - p = mBufCtrl->getFront(); - p->rxCh = mRfChLst[mRxChIdx]; - len = mNrf24.getPayloadSize(); - if(len > MAX_RF_PAYLOAD_SIZE) - len = MAX_RF_PAYLOAD_SIZE; - - mNrf24.read(p->packet, len); - mBufCtrl->pushFront(p); - yield(); + bool loop(void) { + if (!mIrqRcvd) + return false; // nothing to do + mIrqRcvd = false; + bool tx_ok, tx_fail, rx_ready; + mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH + mNrf24.flush_tx(); // empty TX FIFO + //DBGPRINTLN("TX whatHappened Ch" + String(mRfChLst[mTxChIdx]) + " " + String(tx_ok) + String(tx_fail) + String(rx_ready)); + + // start listening on the default RX channel + mRxChIdx = 0; + mNrf24.setChannel(mRfChLst[mRxChIdx]); + mNrf24.startListening(); + + //uint32_t debug_ms = millis(); + uint16_t cnt = 300; // that is 60 times 5 channels + while (0 < cnt--) { + uint32_t startMillis = millis(); + while (millis()-startMillis < 4) { // listen 4ms to each channel + if (mIrqRcvd) { + mIrqRcvd = false; + if (getReceived()) { // everything received + //DBGPRINTLN("RX finished Cnt: " + String(300-cnt) + " time used: " + String(millis()-debug_ms)+ " ms"); + return true; + } } - else - break; + yield(); } - mNrf24.flush_rx(); // drop the packet - RESTORE_IRQ; + switchRxCh(); // switch to next RX channel + yield(); } - } - - void enableDebug() { - mSerialDebug = true; + // not finished but time is over + //DBGPRINTLN("RX not finished: 300 time used: " + String(millis()-debug_ms)+ " ms"); + return true; } void handleIntr(void) { mIrqRcvd = true; } - uint8_t setDefaultChannels(void) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setDefaultChannels")); - mTxChIdx = 2; // Start TX with 40 - mRxChIdx = 0; // Start RX with 03 - return mRfChLst[mTxChIdx]; + bool isChipConnected(void) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); + return mNrf24.isChipConnected(); + } + void enableDebug() { + mSerialDebug = true; } - void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) { - DPRINTLN(DBG_INFO, F("sendControlPacket cmd: ") + String(cmd)); - sendCmdPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME, false); - uint8_t cnt = 0; - mTxBuf[10 + cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor - mTxBuf[10 + cnt++] = 0x00; - if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet - mTxBuf[10 + cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit - mTxBuf[10 + cnt++] = ((data[0] * 10) ) & 0xff; // power limit - mTxBuf[10 + cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings - mTxBuf[10 + cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling + void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) { + DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); + DBGHEXLN(cmd); + initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); + uint8_t cnt = 10; + if (isNoMI) { + mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor + mTxBuf[cnt++] = 0x00; + if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet + mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit + mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit + mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings + mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling + } + } else { //MI 2nd gen. specific + switch (cmd) { + case TurnOn: + mTxBuf[9] = 0x55; + mTxBuf[10] = 0xaa; + break; + case TurnOff: + mTxBuf[9] = 0xaa; + mTxBuf[10] = 0x55; + break; + case ActivePowerContr: + cnt++; + mTxBuf[9] = 0x5a; + mTxBuf[10] = 0x5a; + mTxBuf[11] = data[0]; // power limit + break; + default: + return; + } + cnt++; } - - // crc control data - uint16_t crc = ah::crc16(&mTxBuf[10], cnt); - mTxBuf[10 + cnt++] = (crc >> 8) & 0xff; - mTxBuf[10 + cnt++] = (crc ) & 0xff; - - // crc over all - mTxBuf[10 + cnt] = ah::crc8(mTxBuf, 10 + cnt); - - sendPacket(invId, mTxBuf, 10 + cnt + 1, true); + sendPacket(invId, cnt, isRetransmit, true); } - void sendTimePacket(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId) { - DPRINTLN(DBG_INFO, F("sendTimePacket ") + String(cmd, HEX)); - sendCmdPacket(invId, TX_REQ_INFO, ALL_FRAMES, false); + void prepareDevInformCmd(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId, bool isRetransmit, uint8_t reqfld=TX_REQ_INFO) { // might not be necessary to add additional arg. + DPRINTLN(DBG_DEBUG, F("prepareDevInformCmd 0x") + String(cmd, HEX)); + initPacket(invId, reqfld, ALL_FRAMES); mTxBuf[10] = cmd; // cid mTxBuf[11] = 0x00; CP_U32_LittleEndian(&mTxBuf[12], ts); @@ -228,61 +236,16 @@ class HmRadio { mTxBuf[18] = (alarmMesId >> 8) & 0xff; mTxBuf[19] = (alarmMesId ) & 0xff; } - uint16_t crc = ah::crc16(&mTxBuf[10], 14); - mTxBuf[24] = (crc >> 8) & 0xff; - mTxBuf[25] = (crc ) & 0xff; - mTxBuf[26] = ah::crc8(mTxBuf, 26); - - sendPacket(invId, mTxBuf, 27, true); + sendPacket(invId, 24, isRetransmit, true); } - void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool calcCrc = true) { - DPRINTLN(DBG_VERBOSE, F("sendCmdPacket, mid: ") + String(mid, HEX) + F(" pid: ") + String(pid, HEX)); - memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); - mTxBuf[0] = mid; // message id - CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); - CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); - mTxBuf[9] = pid; - if(calcCrc) { - mTxBuf[10] = ah::crc8(mTxBuf, 10); - sendPacket(invId, mTxBuf, 11, false); - } + void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit) { + initPacket(invId, mid, pid); + sendPacket(invId, 10, isRetransmit, false); } - bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t rxCh) { - //DPRINTLN(DBG_INFO, F("hmRadio.h:checkPaketCrc")); - *len = (buf[0] >> 2); - if(*len > (MAX_RF_PAYLOAD_SIZE - 2)) - *len = MAX_RF_PAYLOAD_SIZE - 2; - for(uint8_t i = 1; i < (*len + 1); i++) { - buf[i-1] = (buf[i] << 1) | (buf[i+1] >> 7); - } - - uint8_t crc = ah::crc8(buf, *len-1); - bool valid = (crc == buf[*len-1]); - - return valid; - } - - bool switchRxCh(uint16_t addLoop = 0) { - if(!mNrf24.isChipConnected()) - return true; - mRxLoopCnt += addLoop; - if(mRxLoopCnt != 0) { - mRxLoopCnt--; - DISABLE_IRQ; - mNrf24.stopListening(); - mNrf24.setChannel(getRxNxtChannel()); - mNrf24.startListening(); - RESTORE_IRQ; - } - return (0 == mRxLoopCnt); // receive finished - } - - void dumpBuf(const char *info, uint8_t buf[], uint8_t len) { + void dumpBuf(uint8_t buf[], uint8_t len) { //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:dumpBuf")); - if(NULL != info) - DBGPRINT(String(info)); for(uint8_t i = 0; i < len; i++) { DHEX(buf[i]); DBGPRINT(" "); @@ -290,11 +253,6 @@ class HmRadio { DBGPRINTLN(""); } - bool isChipConnected(void) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); - return mNrf24.isChipConnected(); - } - uint8_t getDataRate(void) { if(!mNrf24.isChipConnected()) return 3; // unkown @@ -305,82 +263,109 @@ class HmRadio { return mNrf24.isPVariant(); } - + std::queue mBufCtrl; uint32_t mSendCnt; + uint32_t mRetransmits; bool mSerialDebug; private: - void sendPacket(uint64_t invId, uint8_t buf[], uint8_t len, bool clear=false) { - //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket")); - //DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt)); - //dumpBuf("SEN ", buf, len); - if(mSerialDebug) { - DPRINT(DBG_INFO, "TX " + String(len) + "B Ch" + String(mRfChLst[mTxChIdx]) + " | "); - dumpBuf(NULL, buf, len); + bool getReceived(void) { + bool tx_ok, tx_fail, rx_ready; + mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH + //DBGPRINTLN("RX whatHappened Ch" + String(mRfChLst[mRxChIdx]) + " " + String(tx_ok) + String(tx_fail) + String(rx_ready)); + + bool isLastPackage = false; + while(mNrf24.available()) { + uint8_t len; + len = mNrf24.getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed + if (len > 0) { + packet_t p; + p.ch = mRfChLst[mRxChIdx]; + p.len = len; + mNrf24.read(p.packet, len); + mBufCtrl.push(p); + if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command + isLastPackage = (p.packet[9] > 0x81); // > 0x81 indicates last packet received + else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command + isLastPackage = (p.packet[9] > 0x11); // > 0x11 indicates last packet received + else if (p.packet[0] != 0x00 && p.packet[0] != 0x88 && p.packet[0] != 0x92) + // ignore fragment number zero and MI status messages + isLastPackage = true; // response from dev control command + yield(); + } } + return isLastPackage; + } - DISABLE_IRQ; + void switchRxCh() { mNrf24.stopListening(); - - if(clear) - mRxLoopCnt = RF_LOOP_CNT; - - mNrf24.setChannel(mRfChLst[mTxChIdx]); - mTxCh = getTxNxtChannel(); // switch channel for next packet - mNrf24.openWritingPipe(invId); // TODO: deprecated - mNrf24.setCRCLength(RF24_CRC_16); - mNrf24.enableDynamicPayloads(); - mNrf24.setAutoAck(true); - mNrf24.setRetries(3, 15); // 3*250us and 15 loops -> 11.25ms - mNrf24.write(buf, len); - - // Try to avoid zero payload acks (has no effect) - mNrf24.openWritingPipe(DUMMY_RADIO_ID); // TODO: why dummy radio id?, deprecated - mRxChIdx = 0; + // get next channel index + if(++mRxChIdx >= RF_CHANNELS) + mRxChIdx = 0; mNrf24.setChannel(mRfChLst[mRxChIdx]); - mNrf24.setAutoAck(false); - mNrf24.setRetries(0, 0); - mNrf24.disableDynamicPayloads(); - mNrf24.setCRCLength(RF24_CRC_DISABLED); mNrf24.startListening(); + } - RESTORE_IRQ; - mSendCnt++; + void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) { + DPRINTLN(DBG_VERBOSE, F("initPacket, mid: ") + String(mid, HEX) + F(" pid: ") + String(pid, HEX)); + memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); + mTxBuf[0] = mid; // message id + CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); + CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); + mTxBuf[9] = pid; } - uint8_t getTxNxtChannel(void) { + void sendPacket(uint64_t invId, uint8_t len, bool isRetransmit, bool clear=false) { + //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket")); + //DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt)); + + // append crc's + if (len > 10) { + // crc control data + uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); + mTxBuf[len++] = (crc >> 8) & 0xff; + mTxBuf[len++] = (crc ) & 0xff; + } + // crc over all + mTxBuf[len] = ah::crc8(mTxBuf, len); + len++; + + if(mSerialDebug) { + DPRINT(DBG_INFO, F("TX ")); + DBGPRINT(String(len)); + DBGPRINT("B Ch"); + DBGPRINT(String(mRfChLst[mTxChIdx])); + DBGPRINT(F(" | ")); + dumpBuf(mTxBuf, len); + } + + mNrf24.stopListening(); + mNrf24.setChannel(mRfChLst[mTxChIdx]); + mNrf24.openWritingPipe(reinterpret_cast(&invId)); + mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response + // switch TX channel for next packet if(++mTxChIdx >= RF_CHANNELS) mTxChIdx = 0; - return mRfChLst[mTxChIdx]; - } - - uint8_t getRxNxtChannel(void) { - if(++mRxChIdx >= RF_CHANNELS) - mRxChIdx = 0; - return mRfChLst[mRxChIdx]; + if(isRetransmit) + mRetransmits++; + else + mSendCnt++; } + volatile bool mIrqRcvd; uint64_t DTU_RADIO_ID; - uint8_t mTxCh; - uint8_t mTxChIdx; - uint8_t mRfChLst[RF_CHANNELS]; - + uint8_t mTxChIdx; uint8_t mRxChIdx; - uint16_t mRxLoopCnt; + SPIClass* mSpi; RF24 mNrf24; - BUFFER *mBufCtrl; uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; - - DevControlCmdType DevControlCmd; - - volatile bool mIrqRcvd; }; #endif /*__RADIO_H__*/ diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h index 85976cbd5..a95d4d24f 100644 --- a/src/hm/hmSystem.h +++ b/src/hm/hmSystem.h @@ -8,33 +8,22 @@ #include "hmInverter.h" #include "hmRadio.h" -#include "CircularBuffer.h" -typedef CircularBuffer BufferType; -typedef HmRadio RadioType; - -template > +template > class HmSystem { public: - typedef RADIO RadioType; - RadioType Radio; - typedef BUFFER BufferType; - BufferType BufCtrl; - //DevControlCmdType DevControlCmd; + HmRadio<> Radio; - HmSystem() { - mNumInv = 0; - } - ~HmSystem() { - // TODO: cleanup - } + HmSystem() {} void setup() { - Radio.setup(&BufCtrl); + mNumInv = 0; + Radio.setup(); } - void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin) { - Radio.setup(&BufCtrl, ampPwr, irqPin, cePin, csPin); + void setup(uint8_t ampPwr, uint8_t irqPin, uint8_t cePin, uint8_t csPin, uint8_t sclkPin, uint8_t mosiPin, uint8_t misoPin) { + mNumInv = 0; + Radio.setup(ampPwr, irqPin, cePin, csPin, sclkPin, mosiPin, misoPin); } void addInverters(cfgInst_t *config) { @@ -42,8 +31,19 @@ class HmSystem { for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { iv = addInverter(&config->iv[i]); if (0ULL != config->iv[i].serial.u64) { - if (NULL != iv) - DPRINTLN(DBG_INFO, "added inverter " + String(iv->config->serial.u64, HEX)); + if (NULL != iv) { + DPRINT(DBG_INFO, "added inverter "); + if(iv->config->serial.b[5] == 0x11) + DBGPRINT("HM"); + else { + DBGPRINT(((iv->config->serial.b[4] & 0x03) == 0x01) ? " (2nd Gen) " : " (3rd Gen) "); + } + + DBGPRINTLN(String(iv->config->serial.u64, HEX)); + + if((iv->config->serial.b[5] == 0x10) && ((iv->config->serial.b[4] & 0x03) == 0x01)) + DPRINTLN(DBG_WARN, F("MI Inverter are not fully supported now!!!")); + } } } } @@ -59,16 +59,25 @@ class HmSystem { p->config = config; DPRINT(DBG_VERBOSE, "SERIAL: " + String(p->config->serial.b[5], HEX)); DPRINTLN(DBG_VERBOSE, " " + String(p->config->serial.b[4], HEX)); - if(p->config->serial.b[5] == 0x11) { + if((p->config->serial.b[5] == 0x11) || (p->config->serial.b[5] == 0x10)) { switch(p->config->serial.b[4]) { + case 0x22: case 0x21: p->type = INV_TYPE_1CH; break; + case 0x42: case 0x41: p->type = INV_TYPE_2CH; break; + case 0x62: case 0x61: p->type = INV_TYPE_4CH; break; default: - DPRINT(DBG_ERROR, F("unknown inverter type: 11")); - DPRINTLN(DBG_ERROR, String(p->config->serial.b[4], HEX)); + DPRINTLN(DBG_ERROR, F("unknown inverter type")); break; } + + if(p->config->serial.b[5] == 0x11) + p->ivGen = IV_HM; + else if((p->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM + p->ivGen = IV_HM; + else // MI 2nd Gen + p->ivGen = IV_MI; } else if(p->config->serial.u64 != 0ULL) DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); diff --git a/src/hm/miPayload.h b/src/hm/miPayload.h new file mode 100644 index 000000000..dbe43c061 --- /dev/null +++ b/src/hm/miPayload.h @@ -0,0 +1,825 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + +#ifndef __MI_PAYLOAD_H__ +#define __MI_PAYLOAD_H__ + +//#include "hmInverter.h" +#include "../utils/dbg.h" +#include "../utils/crc.h" +#include "../config/config.h" +#include + +typedef struct { + uint32_t ts; + bool requested; + bool limitrequested; + uint8_t txCmd; + uint8_t len[MAX_PAYLOAD_ENTRIES]; + bool complete; + bool dataAB[3]; + bool stsAB[3]; + uint16_t sts[6]; + uint8_t txId; + uint8_t invId; + uint8_t retransmits; + //uint8_t skipfirstrepeat; + bool gotFragment; + /* + uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; + uint8_t maxPackId; + bool lastFound;*/ +} miPayload_t; + + +typedef std::function miPayloadListenerType; + + +template +class MiPayload { + public: + MiPayload() {} + + void setup(IApp *app, HMSYSTEM *sys, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) { + mApp = app; + mSys = sys; + mStat = stat; + mMaxRetrans = maxRetransmits; + mTimestamp = timestamp; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + reset(i, true); + mPayload[i].limitrequested = true; + } + mSerialDebug = false; + mHighPrioIv = NULL; + mCbMiPayload = NULL; + } + + void enableSerialDebug(bool enable) { + mSerialDebug = enable; + } + + void addPayloadListener(miPayloadListenerType cb) { + mCbMiPayload = cb; + } + + void addAlarmListener(alarmListenerType cb) { + mCbMiAlarm = cb; + } + + void loop() { + if(NULL != mHighPrioIv) { // && mHighPrioIv->ivGen == IV_MI) { + ivSend(mHighPrioIv, true); // for devcontrol commands? + mHighPrioIv = NULL; + } + } + + void ivSendHighPrio(Inverter<> *iv) { + mHighPrioIv = iv; + } + + void ivSend(Inverter<> *iv, bool highPrio = false) { + if(!highPrio) { + if (mPayload[iv->id].requested) { + if (!mPayload[iv->id].complete) + process(false); // no retransmit + + if (!mPayload[iv->id].complete) { + if (!mPayload[iv->id].gotFragment) + mStat->rxFailNoAnser++; // got nothing + else + mStat->rxFail++; // got fragments but not complete response + + iv->setQueuedCmdFinished(); // command failed + if (mSerialDebug) + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("enqueued cmd failed/timeout")); + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("no Payload received! (retransmits: ")); + DBGPRINT(String(mPayload[iv->id].retransmits)); + DBGPRINTLN(F(")")); + } + } + } + } + + reset(iv->id); + mPayload[iv->id].requested = true; + + yield(); + if (mSerialDebug){ + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Requesting Inv SN ")); + DBGPRINTLN(String(iv->config->serial.u64, HEX)); + } + + if (iv->getDevControlRequest()) { + if (mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("Devcontrol request 0x")); + DHEX(iv->devControlCmd); + DBGPRINT(F(" power limit ")); + DBGPRINTLN(String(iv->powerLimit[0])); + } + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false, false); + mPayload[iv->id].txCmd = iv->devControlCmd; + mPayload[iv->id].limitrequested = true; + + iv->clearCmdQueue(); + iv->enqueCommand(SystemConfigPara); // try to read back power limit + } else { + uint8_t cmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(cmd); + uint8_t cmd2 = cmd; + if ( cmd == SystemConfigPara ) { //0x05 for HM-types + if (!mPayload[iv->id].limitrequested) { // only do once at startup + iv->setQueuedCmdFinished(); + cmd = iv->getQueuedCmd(); + } else { + mPayload[iv->id].limitrequested = false; + } + } + + if (cmd == 0x01 || cmd == SystemConfigPara ) { //0x1 and 0x05 for HM-types + cmd = 0x0f; // for MI, these seem to make part of the Polling the device software and hardware version number command + cmd2 = cmd == SystemConfigPara ? 0x01 : 0x00; //perhaps we can only try to get second frame? + mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd2, false); + } else { + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); + }; + + mPayload[iv->id].txCmd = cmd; + if (iv->type == INV_TYPE_1CH || iv->type == INV_TYPE_2CH) { + mPayload[iv->id].dataAB[CH1] = false; + mPayload[iv->id].stsAB[CH1] = false; + mPayload[iv->id].dataAB[CH0] = false; + mPayload[iv->id].stsAB[CH0] = false; + } + + if (iv->type == INV_TYPE_2CH) { + mPayload[iv->id].dataAB[CH2] = false; + mPayload[iv->id].stsAB[CH2] = false; + } + } + } + + void add(Inverter<> *iv, packet_t *p) { + //DPRINTLN(DBG_INFO, F("MI got data [0]=") + String(p->packet[0], HEX)); + if (p->packet[0] == (0x08 + ALL_FRAMES)) { // 0x88; MI status response to 0x09 + miStsDecode(iv, p); + } + + else if (p->packet[0] == (0x11 + SINGLE_FRAME)) { // 0x92; MI status response to 0x11 + miStsDecode(iv, p, CH2); + } + + else if ( p->packet[0] == 0x09 + ALL_FRAMES || + p->packet[0] == 0x11 + ALL_FRAMES || + ( p->packet[0] >= (0x36 + ALL_FRAMES) && p->packet[0] < (0x39 + SINGLE_FRAME) + && mPayload[iv->id].txCmd != 0x0f) ) { // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39 + mPayload[iv->id].txId = p->packet[0]; + miDataDecode(iv,p); + } + + else if (p->packet[0] == ( 0x0f + ALL_FRAMES)) { + // MI response from get hardware information request + record_t<> *rec = iv->getRecordStruct(InverterDevInform_All); // choose the record structure + rec->ts = mPayload[iv->id].ts; + mPayload[iv->id].gotFragment = true; + +/* + Polling the device software and hardware version number command + start byte Command word routing address target address User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] + 0x7e 0x0f xx xx xx xx YY YY YY YY 0x00 CRC 0x7f + Command Receipt - First Frame + start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] + 0x7e 0x8f YY YY YY YY xx xx xx xx 0x00 USFWBuild_VER APPFWBuild_VER APPFWBuild_YYYY APPFWBuild_MMDD APPFWBuild_HHMM APPFW_PN HW_VER CRC 0x7f + Command Receipt - Second Frame + start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[17] byte[18] byte[19] byte[20] byte[21] byte[22] byte[23] byte[24] byte[25] byte[26] byte[27] byte[28] + 0x7e 0x8f YY YY YY YY xx xx xx xx 0x01 HW_PN HW_FB_TLmValue HW_FB_ReSPRT HW_GridSamp_ResValule HW_ECapValue Matching_APPFW_PN CRC 0x7f + Command receipt - third frame + start byte Command word target address routing address Multi-frame marking User data User data User data User data User data User data User data User data check end byte + byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11] byte[12] byte[13] byte[14] byte[15] byte[16] byte[15] byte[16] byte[17] byte[18] + 0x7e 0x8f YY YY YY YY xx xx xx xx 0x12 APPFW_MINVER HWInfoAddr PNInfoCRC_gusv PNInfoCRC_gusv CRC 0x7f +*/ + +/* +case InverterDevInform_All: + rec->length = (uint8_t)(HMINFO_LIST_LEN); + rec->assign = (byteAssign_t *)InfoAssignment; + rec->pyldLen = HMINFO_PAYLOAD_LEN; + break; +const byteAssign_t InfoAssignment[] = { + { FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, + { FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, + { FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, + { FLD_FW_BUILD_HOUR_MINUTE, UNIT_NONE, CH0, 6, 2, 1 }, + { FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } +}; +*/ + + if ( p->packet[9] == 0x00 ) {//first frame + //FLD_FW_VERSION + for (uint8_t i = 0; i < 5; i++) { + iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1); + } + iv->isConnected = true; + if(mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DPRINT(DBG_INFO,F("HW_VER is ")); + DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25])); + } + /*iv->setQueuedCmdFinished(); + mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x01, false);*/ + } else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10 + DPRINT_IVID(DBG_INFO, iv->id); + if ( p->packet[9] == 0x01 ) { + DBGPRINTLN(F("got 2nd frame (hw info)")); + } else { + DBGPRINTLN(F("3rd gen. inverter!")); // see table in OpenDTU code, DevInfoParser.cpp devInfo[] + } + // xlsx: HW_ECapValue is total energy?!? (data coll. inst. #154) + DPRINT(DBG_INFO,F("HW_PartNo ")); + DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])); + //DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13])); + if ( p->packet[9] == 0x01 ) { + iv->setValue(iv->getPosByChFld(0, FLD_YT, rec), rec, (float) ((p->packet[20] << 8) + p->packet[21])/1); + if(mSerialDebug) { + DPRINT(DBG_INFO,F("HW_ECapValue ")); + DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21])); + + DPRINT(DBG_INFO,F("HW_FB_TLmValue ")); + DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15])); + DPRINT(DBG_INFO,F("HW_FB_ReSPRT ")); + DBGPRINTLN(String((p->packet[16] << 8) + p->packet[17])); + DPRINT(DBG_INFO,F("HW_GridSamp_ResValule ")); + DBGPRINTLN(String((p->packet[18] << 8) + p->packet[19])); + } + } + } else if ( p->packet[9] == 0x12 ) {//3rd frame + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("got 3rd frame (hw info)")); + iv->setQueuedCmdFinished(); + mStat->rxSuccess++; + } + + } else if ( p->packet[0] == (TX_REQ_INFO + ALL_FRAMES) // response from get information command + || (p->packet[0] == 0xB6 && mPayload[iv->id].txCmd != 0x36)) { // strange short response from MI-1500 3rd gen; might be missleading! + // atm, we just do nothing else than print out what we got... + // for decoding see xls- Data collection instructions - #147ff + //mPayload[iv->id].txId = p->packet[0]; + DPRINTLN(DBG_DEBUG, F("Response from info request received")); + uint8_t *pid = &p->packet[9]; + if (*pid == 0x00) { + DPRINT(DBG_DEBUG, F("fragment number zero received")); + iv->setQueuedCmdFinished(); + } else if (p->packet[9] == 0x81) { // might need some additional check, as this is only ment for short answers! + DPRINT_IVID(DBG_WARN, iv->id); + DBGPRINTLN(F("seems to use 3rd gen. protocol - switching ivGen!")); + iv->ivGen = IV_HM; + iv->setQueuedCmdFinished(); + iv->clearCmdQueue(); + //DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX)); + /* (old else-tree) + if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) {^ + memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); + mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; + mPayload[iv->id].gotFragment = true; + } + if ((*pid & ALL_FRAMES) == ALL_FRAMES) { + // Last packet + if (((*pid & 0x7f) > mPayload[iv->id].maxPackId) || (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId)) { + mPayload[iv->id].maxPackId = (*pid & 0x7f); + if (*pid > 0x81) + mPayload[iv->id].lastFound = true; + } + }*/ + } + //} + } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES ) // response from dev control command + || p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES -1)) { // response from DRED instruction + DPRINT_IVID(DBG_DEBUG, iv->id); + DBGPRINTLN(F("Response from devcontrol request received")); + + mPayload[iv->id].txId = p->packet[0]; + iv->clearDevControlRequest(); + + if ((p->packet[9] == 0x5a) && (p->packet[10] == 0x5a)) { + mApp->setMqttPowerLimitAck(iv); + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("has accepted power limit set point ")); + DBGPRINT(String(iv->powerLimit[0])); + DBGPRINT(F(" with PowerLimitControl ")); + DBGPRINTLN(String(iv->powerLimit[1])); + + iv->clearCmdQueue(); + iv->enqueCommand(SystemConfigPara); // read back power limit + } + iv->devControlCmd = Init; + } else { // some other response; copied from hmPayload:process; might not be correct to do that here!!! + DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + DPRINT(DBG_INFO, F("procPyld: txid: 0x")); + DBGHEXLN(mPayload[iv->id].txId); + //DPRINT(DBG_DEBUG, F("procPyld: max: ")); + //DBGPRINTLN(String(mPayload[iv->id].maxPackId)); + record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser + mPayload[iv->id].complete = true; + + uint8_t payload[128]; + uint8_t payloadLen = 0; + + memset(payload, 0, 128); + + /*for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { + memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + payloadLen += (mPayload[iv->id].len[i]); + yield(); + }*/ + payloadLen -= 2; + + if (mSerialDebug) { + DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): "); + mSys->Radio.dumpBuf(payload, payloadLen); + } + + if (NULL == rec) { + DPRINTLN(DBG_ERROR, F("record is NULL!")); + } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { + if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) + mStat->rxSuccess++; + + rec->ts = mPayload[iv->id].ts; + for (uint8_t i = 0; i < rec->length; i++) { + iv->addValue(i, payload, rec); + yield(); + } + iv->doCalculations(); + notify(mPayload[iv->id].txCmd); + + if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbMiAlarm) + (mCbMiAlarm)(code, start, end); + yield(); + } + } + } else { + DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); + mStat->rxFail++; + } + + iv->setQueuedCmdFinished(); + } + } + + void process(bool retransmit) { + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + + if (IV_HM == iv->ivGen) // only process MI inverters + continue; // skip to next inverter + + if ( !mPayload[iv->id].complete && + (mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && + (mPayload[iv->id].txId < (0x36 + ALL_FRAMES)) && + (mPayload[iv->id].txId > (0x39 + ALL_FRAMES)) && + (mPayload[iv->id].txId != (0x09 + ALL_FRAMES)) && + (mPayload[iv->id].txId != (0x11 + ALL_FRAMES)) && + (mPayload[iv->id].txId != (0x88)) && + (mPayload[iv->id].txId != (0x92)) && + (mPayload[iv->id].txId != 0 )) { + // no processing needed if txId is not one of 0x95, 0x88, 0x89, 0x91, 0x92 or resonse to 0x36ff + mPayload[iv->id].complete = true; + continue; // skip to next inverter + } + + //delayed next message? + //mPayload[iv->id].skipfirstrepeat++; + /*if (mPayload[iv->id].skipfirstrepeat) { + mPayload[iv->id].skipfirstrepeat = 0; //reset counter + continue; // skip to next inverter + }*/ + + if (!mPayload[iv->id].complete) { + //DPRINTLN(DBG_INFO, F("Pyld incompl code")); //info for testing only + bool crcPass, pyldComplete; + crcPass = build(iv->id, &pyldComplete); + if (!crcPass && !pyldComplete) { // payload not complete + if ((mPayload[iv->id].requested) && (retransmit)) { + if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { + // This is required to prevent retransmissions without answer. + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else if(iv->devControlCmd == ActivePowerContr) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("retransmit power limit")); + mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true, false); + } else { + uint8_t cmd = mPayload[iv->id].txCmd; + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + if( !mPayload[iv->id].gotFragment ) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("nothing received")); + mPayload[iv->id].retransmits = mMaxRetrans; + } else if ( cmd == 0x0f ) { + //hard/firmware request + mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x00, true); + //iv->setQueuedCmdFinished(); + //cmd = iv->getQueuedCmd(); + } else { + bool change = false; + if ( cmd >= 0x36 && cmd < 0x39 ) { // MI-1500 Data command + //cmd++; // just request the next channel + //change = true; + } else if ( cmd == 0x09 ) {//MI single or dual channel device + if ( mPayload[iv->id].dataAB[CH1] && iv->type == INV_TYPE_2CH ) { + if (!mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].retransmits<2) {} + //first try to get missing sts for first channel a second time + else if (!mPayload[iv->id].stsAB[CH2] || !mPayload[iv->id].dataAB[CH2] ) { + cmd = 0x11; + change = true; + mPayload[iv->id].retransmits = 0; //reset counter + } + } + } else if ( cmd == 0x11) { + if ( mPayload[iv->id].dataAB[CH2] ) { // data + status ch2 are there? + if (mPayload[iv->id].stsAB[CH2] && (!mPayload[iv->id].stsAB[CH1] || !mPayload[iv->id].dataAB[CH1])) { + cmd = 0x09; + change = true; + } + } + } + DPRINT_IVID(DBG_INFO, iv->id); + if (change) { + DBGPRINT(F("next request is")); + //mPayload[iv->id].skipfirstrepeat = 0; + mPayload[iv->id].txCmd = cmd; + } else { + DBGPRINT(F("sth.")); + DBGPRINT(F(" missing: Request Retransmit")); + } + DBGPRINT(F(" 0x")); + DBGHEXLN(cmd); + //mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd, true); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, true, cmd); + yield(); + } + } + } + } + } else if(!crcPass && pyldComplete) { // crc error on complete Payload + if (mPayload[iv->id].retransmits < mMaxRetrans) { + mPayload[iv->id].retransmits++; + DPRINT_IVID(DBG_WARN, iv->id); + DBGPRINTLN(F("CRC Error: Request Complete Retransmit")); + mPayload[iv->id].txCmd = iv->getQueuedCmd(); + DPRINT_IVID(DBG_INFO, iv->id); + + DBGPRINT(F("prepareDevInformCmd 0x")); + DBGHEXLN(mPayload[iv->id].txCmd); + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); + } + } + /*else { // payload complete + //This tree is not really tested, most likely it's not truly complete.... + DPRINTLN(DBG_INFO, F("procPyld: cmd: 0x") + String(mPayload[iv->id].txCmd, HEX)); + DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX)); + //DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId)); + //record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser + //uint8_t payload[128]; + //uint8_t payloadLen = 0; + //memset(payload, 0, 128); + //for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { + // memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); + // payloadLen += (mPayload[iv->id].len[i]); + // yield(); + //} + //payloadLen -= 2; + //if (mSerialDebug) { + // DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): "); + // mSys->Radio.dumpBuf(payload, payloadLen); + //} + //if (NULL == rec) { + // DPRINTLN(DBG_ERROR, F("record is NULL!")); + //} else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { + // if (mPayload[iv->id].txId == (TX_REQ_INFO + ALL_FRAMES)) + // mStat->rxSuccess++; + // rec->ts = mPayload[iv->id].ts; + // for (uint8_t i = 0; i < rec->length; i++) { + // iv->addValue(i, payload, rec); + // yield(); + // } + // iv->doCalculations(); + // notify(mPayload[iv->id].txCmd); + // if(AlarmData == mPayload[iv->id].txCmd) { + // uint8_t i = 0; + // uint16_t code; + // uint32_t start, end; + // while(1) { + // code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + // if(0 == code) + // break; + // if (NULL != mCbAlarm) + // (mCbAlarm)(code, start, end); + // yield(); + // } + // } + //} else { + // DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); + // mStat->rxFail++; + //} + //iv->setQueuedCmdFinished(); + //}*/ + } + yield(); + } + } + + private: + void notify(uint8_t val) { + if(NULL != mCbMiPayload) + (mCbMiPayload)(val); + } + + void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t stschan = CH1) { + //DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") status msg 0x") + String(p->packet[0], HEX)); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure + rec->ts = mPayload[iv->id].ts; + mPayload[iv->id].gotFragment = true; + mPayload[iv->id].txId = p->packet[0]; + miStsConsolidate(iv, stschan, rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]); + mPayload[iv->id].stsAB[stschan] = true; + if (mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].stsAB[CH2]) + mPayload[iv->id].stsAB[CH0] = true; + //mPayload[iv->id].skipfirstrepeat = 1; + if (mPayload[iv->id].stsAB[CH0] && mPayload[iv->id].dataAB[CH0] && !mPayload[iv->id].complete) { + miComplete(iv); + } + } + + void miStsConsolidate(Inverter<> *iv, uint8_t stschan, record_t<> *rec, uint8_t uState, uint8_t uEnum, uint8_t lState = 0, uint8_t lEnum = 0) { + //uint8_t status = (p->packet[11] << 8) + p->packet[12]; + uint16_t status = 3; // regular status for MI, change to 1 later? + if ( uState == 2 ) { + status = 5050 + stschan; //first approach, needs review! + if (lState) + status += lState*10; + } else if ( uState > 3 ) { + status = uState*1000 + uEnum*10; + if (lState) + status += lState*100; //needs review, esp. for 4ch-8310 state! + //if (lEnum) + status += lEnum; + if (uEnum < 6) { + status += stschan; + } + if (status == 8000) + status = 8310; //trick? + } + + uint16_t prntsts = status == 3 ? 1 : status; + if ( status != mPayload[iv->id].sts[stschan] ) { //sth.'s changed? + mPayload[iv->id].sts[stschan] = status; + DPRINT(DBG_WARN, F("Status change for CH")); + DBGPRINT(String(stschan)); DBGPRINT(F(" (")); + DBGPRINT(String(prntsts)); DBGPRINT(F("): ")); + DBGPRINTLN(iv->getAlarmStr(prntsts)); + } + + if ( !mPayload[iv->id].sts[0] || prntsts < mPayload[iv->id].sts[0] ) { + mPayload[iv->id].sts[0] = prntsts; + iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts); + } + + if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ + iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!? + + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("alarm ID incremented to ")); + DBGPRINTLN(String(iv->alarmMesIndex)); + } + /*if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + }*/ + } + + void miDataDecode(Inverter<> *iv, packet_t *p) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser + rec->ts = mPayload[iv->id].ts; + mPayload[iv->id].gotFragment = true; + + uint8_t datachan = ( p->packet[0] == 0x89 || p->packet[0] == (0x36 + ALL_FRAMES) ) ? CH1 : + ( p->packet[0] == 0x91 || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 : + p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 : + CH4; + //DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") data msg 0x") + String(p->packet[0], HEX) + F(" channel ") + datachan); + // count in RF_communication_protocol.xlsx is with offset = -1 + iv->setValue(iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[9] << 8) + p->packet[10])/10); + yield(); + iv->setValue(iv->getPosByChFld(datachan, FLD_IDC, rec), rec, (float)((p->packet[11] << 8) + p->packet[12])/10); + yield(); + iv->setValue(iv->getPosByChFld(0, FLD_UAC, rec), rec, (float)((p->packet[13] << 8) + p->packet[14])/10); + yield(); + iv->setValue(iv->getPosByChFld(0, FLD_F, rec), rec, (float) ((p->packet[15] << 8) + p->packet[16])/100); + iv->setValue(iv->getPosByChFld(datachan, FLD_PDC, rec), rec, (float)((p->packet[17] << 8) + p->packet[18])/10); + yield(); + iv->setValue(iv->getPosByChFld(datachan, FLD_YD, rec), rec, (float)((p->packet[19] << 8) + p->packet[20])/1); + yield(); + iv->setValue(iv->getPosByChFld(0, FLD_T, rec), rec, (float) ((int16_t)(p->packet[21] << 8) + p->packet[22])/10); + iv->setValue(iv->getPosByChFld(0, FLD_IRR, rec), rec, (float) (calcIrradiation(iv, datachan))); + //AC Power is missing; we may have to calculate, as no respective data is in payload + + if ( datachan < 3 ) { + mPayload[iv->id].dataAB[datachan] = true; + } + if ( !mPayload[iv->id].dataAB[CH0] && mPayload[iv->id].dataAB[CH2] && mPayload[iv->id].dataAB[CH2] ) { + mPayload[iv->id].dataAB[CH0] = true; + } + + if (p->packet[0] >= (0x36 + ALL_FRAMES) ) { + + /*For MI1500: + if (MI1500) { + STAT = (uint8_t)(p->packet[25] ); + FCNT = (uint8_t)(p->packet[26]); + FCODE = (uint8_t)(p->packet[27]); + }*/ + + /*uint16_t status = (uint8_t)(p->packet[23]); + mPayload[iv->id].sts[datachan] = status; + if ( !mPayload[iv->id].sts[0] || status < mPayload[iv->id].sts[0]) { + mPayload[iv->id].sts[0] = status; + iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, status); + }*/ + miStsConsolidate(iv, datachan, rec, p->packet[23], p->packet[24]); + + if (p->packet[0] < (0x39 + ALL_FRAMES) ) { + /*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1; + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); + mPayload[iv->id].txCmd = cmd;*/ + mPayload[iv->id].txCmd++; + if (mPayload[iv->id].retransmits) + mPayload[iv->id].retransmits--; // reserve retransmissions for each response + mPayload[iv->id].complete = false; + } + + else if (p->packet[0] == (0x39 + ALL_FRAMES) ) { + /*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1; + mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); + mPayload[iv->id].txCmd = cmd;*/ + mPayload[iv->id].complete = true; + } + + /*if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ + iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; + + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT_TXT(TXT_INCRALM); + DBGPRINTLN(String(iv->alarmMesIndex)); + }*/ + + } + + if ( mPayload[iv->id].complete || //4ch device + (iv->type != INV_TYPE_4CH //other devices + && mPayload[iv->id].dataAB[CH0] + && mPayload[iv->id].stsAB[CH0])) { + miComplete(iv); + } + + + +/* + if(AlarmData == mPayload[iv->id].txCmd) { + uint8_t i = 0; + uint16_t code; + uint32_t start, end; + while(1) { + code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end); + if(0 == code) + break; + if (NULL != mCbMiAlarm) + (mCbAlarm)(code, start, end); + yield(); + } + }*/ + } + + void miComplete(Inverter<> *iv) { + if (mPayload[iv->id].complete) + return; //if we got second message as well in repreated attempt + mPayload[iv->id].complete = true; // For 2 CH devices, this might be too short... + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("got all msgs")); + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); + + //preliminary AC calculation... + float ac_pow = 0; + for(uint8_t i = 1; i <= iv->channels; i++) { + if (mPayload[iv->id].sts[i] == 3) { + uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); + ac_pow += iv->getValue(pos, rec); + } + } + ac_pow = (int) (ac_pow*9.5); + iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) ac_pow/10); + + iv->doCalculations(); + iv->setQueuedCmdFinished(); + mStat->rxSuccess++; + yield(); + notify(mPayload[iv->id].txCmd); + } + + bool build(uint8_t id, bool *complete) { + DPRINTLN(DBG_VERBOSE, F("build")); + // check if all messages are there + + *complete = mPayload[id].complete; + uint8_t txCmd = mPayload[id].txCmd; + + if(!*complete) { + DPRINTLN(DBG_VERBOSE, F("incomlete, txCmd is 0x") + String(txCmd, HEX)); + //DBGHEXLN(txCmd); + if (txCmd == 0x09 || txCmd == 0x11 || (txCmd >= 0x36 && txCmd <= 0x39)) + return false; + } + + return true; + } + + void reset(uint8_t id, bool clrSts = false) { + DPRINT_IVID(DBG_INFO, id); + DBGPRINTLN(F("resetPayload")); + memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); + mPayload[id].gotFragment = false; + /*mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; + mPayload[id].lastFound = false;*/ + mPayload[id].retransmits = 0; + mPayload[id].complete = false; + mPayload[id].dataAB[CH0] = true; //required for 1CH and 2CH devices + mPayload[id].dataAB[CH1] = true; //required for 1CH and 2CH devices + mPayload[id].dataAB[CH2] = true; //only required for 2CH devices + mPayload[id].stsAB[CH0] = true; //required for 1CH and 2CH devices + mPayload[id].stsAB[CH1] = true; //required for 1CH and 2CH devices + mPayload[id].stsAB[CH2] = true; //only required for 2CH devices + mPayload[id].txCmd = 0; + //mPayload[id].skipfirstrepeat = 0; + mPayload[id].requested = false; + mPayload[id].ts = *mTimestamp; + mPayload[id].sts[0] = 0; + if (clrSts) { // only clear channel states at startup + mPayload[id].sts[CH1] = 0; + mPayload[id].sts[CH2] = 0; + mPayload[id].sts[CH3] = 0; + mPayload[id].sts[CH4] = 0; + mPayload[id].sts[5] = 0; //remember last summarized state + } + } + + + + IApp *mApp; + HMSYSTEM *mSys; + statistics_t *mStat; + uint8_t mMaxRetrans; + uint32_t *mTimestamp; + miPayload_t mPayload[MAX_NUM_INVERTERS]; + bool mSerialDebug; + + Inverter<> *mHighPrioIv; + alarmListenerType mCbMiAlarm; + payloadListenerType mCbMiPayload; +}; + +#endif /*__MI_PAYLOAD_H__*/ diff --git a/src/hm/payload.h b/src/hm/payload.h deleted file mode 100644 index ef69c4fee..000000000 --- a/src/hm/payload.h +++ /dev/null @@ -1,251 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __PAYLOAD_H__ -#define __PAYLOAD_H__ - -#include "../utils/dbg.h" -#include "../utils/crc.h" -#include "../utils/handler.h" -#include "../config/config.h" -#include - -typedef struct { - uint8_t txCmd; - uint8_t txId; - uint8_t invId; - uint32_t ts; - uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; - uint8_t len[MAX_PAYLOAD_ENTRIES]; - bool complete; - uint8_t maxPackId; - uint8_t retransmits; - bool requested; -} invPayload_t; - - -typedef std::function payloadListenerType; - - -template -class Payload : public Handler { - public: - Payload() : Handler() {} - - void setup(HMSYSTEM *sys) { - mSys = sys; - memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t))); - mLastPacketId = 0x00; - mSerialDebug = false; - } - - void enableSerialDebug(bool enable) { - mSerialDebug = enable; - } - - bool isComplete(Inverter<> *iv) { - return mPayload[iv->id].complete; - } - - uint8_t getMaxPacketId(Inverter<> *iv) { - return mPayload[iv->id].maxPackId; - } - - uint8_t getRetransmits(Inverter<> *iv) { - return mPayload[iv->id].retransmits; - } - - uint32_t getTs(Inverter<> *iv) { - return mPayload[iv->id].ts; - } - - void request(Inverter<> *iv) { - mPayload[iv->id].requested = true; - } - - void setTxCmd(Inverter<> *iv, uint8_t cmd) { - mPayload[iv->id].txCmd = cmd; - } - - void notify(uint8_t val) { - for(typename std::list::iterator it = mList.begin(); it != mList.end(); ++it) { - (*it)(val); - } - } - - void add(packet_t *p, uint8_t len) { - Inverter<> *iv = mSys->findInverter(&p->packet[1]); - if ((NULL != iv) && (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES))) { // response from get information command - mPayload[iv->id].txId = p->packet[0]; - DPRINTLN(DBG_DEBUG, F("Response from info request received")); - uint8_t *pid = &p->packet[9]; - if (*pid == 0x00) { - DPRINT(DBG_DEBUG, F("fragment number zero received and ignored")); - } else { - DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX)); - if ((*pid & 0x7F) < 5) { - memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], len - 11); - mPayload[iv->id].len[(*pid & 0x7F) - 1] = len - 11; - } - - if ((*pid & ALL_FRAMES) == ALL_FRAMES) { - // Last packet - if ((*pid & 0x7f) > mPayload[iv->id].maxPackId) { - mPayload[iv->id].maxPackId = (*pid & 0x7f); - if (*pid > 0x81) - mLastPacketId = *pid; - } - } - } - } - if ((NULL != iv) && (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES))) { // response from dev control command - DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); - - mPayload[iv->id].txId = p->packet[0]; - iv->devControlRequest = false; - - if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { - String msg = (p->packet[10] == 0x00 && p->packet[11] == 0x00) ? "" : "NOT "; - DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has ") + msg + F("accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1])); - } - iv->devControlCmd = Init; - } - } - - bool build(uint8_t id) { - DPRINTLN(DBG_VERBOSE, F("build")); - uint16_t crc = 0xffff, crcRcv = 0x0000; - if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES) - mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; - - for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) { - if (mPayload[id].len[i] > 0) { - if (i == (mPayload[id].maxPackId - 1)) { - crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); - crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]); - } else - crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); - } - yield(); - } - - return (crc == crcRcv) ? true : false; - } - - void process(bool retransmit, uint8_t maxRetransmits, statistics_t *stat) { - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - if ((mPayload[iv->id].txId != (TX_REQ_INFO + ALL_FRAMES)) && (0 != mPayload[iv->id].txId)) { - // no processing needed if txId is not 0x95 - // DPRINTLN(DBG_INFO, F("processPayload - set complete, txId: ") + String(mPayload[iv->id].txId, HEX)); - mPayload[iv->id].complete = true; - } - - if (!mPayload[iv->id].complete) { - if (!build(iv->id)) { // payload not complete - if ((mPayload[iv->id].requested) && (retransmit)) { - if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { - // This is required to prevent retransmissions without answer. - DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); - mPayload[iv->id].retransmits = maxRetransmits; - } else { - if (mPayload[iv->id].retransmits < maxRetransmits) { - mPayload[iv->id].retransmits++; - if (mPayload[iv->id].maxPackId != 0) { - for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { - if (mPayload[iv->id].len[i] == 0) { - DPRINTLN(DBG_WARN, F("while retrieving data: Frame ") + String(i + 1) + F(" missing: Request Retransmit")); - mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); - break; // only retransmit one frame per loop - } - yield(); - } - } else { - DPRINTLN(DBG_WARN, F("while retrieving data: last frame missing: Request Retransmit")); - if (0x00 != mLastPacketId) - mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, mLastPacketId, true); - else { - mPayload[iv->id].txCmd = iv->getQueuedCmd(); - DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket")); - mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex); - } - } - mSys->Radio.switchRxCh(100); - } - } - } - } else { // payload complete - DPRINTLN(DBG_INFO, F("procPyld: cmd: ") + String(mPayload[iv->id].txCmd)); - DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX)); - DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId)); - record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser - mPayload[iv->id].complete = true; - - uint8_t payload[128]; - uint8_t payloadLen = 0; - - memset(payload, 0, 128); - - for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) { - memcpy(&payload[payloadLen], mPayload[iv->id].data[i], (mPayload[iv->id].len[i])); - payloadLen += (mPayload[iv->id].len[i]); - yield(); - } - payloadLen -= 2; - - if (mSerialDebug) { - DPRINT(DBG_INFO, F("Payload (") + String(payloadLen) + "): "); - mSys->Radio.dumpBuf(NULL, payload, payloadLen); - } - - if (NULL == rec) { - DPRINTLN(DBG_ERROR, F("record is NULL!")); - } else if ((rec->pyldLen == payloadLen) || (0 == rec->pyldLen)) { - if (mPayload[iv->id].txId == (TX_REQ_INFO + 0x80)) - stat->rxSuccess++; - - rec->ts = mPayload[iv->id].ts; - for (uint8_t i = 0; i < rec->length; i++) { - iv->addValue(i, payload, rec); - yield(); - } - iv->doCalculations(); - notify(mPayload[iv->id].txCmd); - } else { - DPRINTLN(DBG_ERROR, F("plausibility check failed, expected ") + String(rec->pyldLen) + F(" bytes")); - stat->rxFail++; - } - - iv->setQueuedCmdFinished(); - } - } - - yield(); - - } - } - - void reset(Inverter<> *iv, uint32_t utcTs) { - DPRINTLN(DBG_INFO, "resetPayload: id: " + String(iv->id)); - memset(mPayload[iv->id].len, 0, MAX_PAYLOAD_ENTRIES); - mPayload[iv->id].txCmd = 0; - mPayload[iv->id].retransmits = 0; - mPayload[iv->id].maxPackId = 0; - mPayload[iv->id].complete = false; - mPayload[iv->id].requested = false; - mPayload[iv->id].ts = utcTs; - } - - private: - HMSYSTEM *mSys; - invPayload_t mPayload[MAX_NUM_INVERTERS]; - uint8_t mLastPacketId; - bool mSerialDebug; -}; - -#endif /*__PAYLOAD_H_*/ diff --git a/src/main.cpp b/src/main.cpp index c585d0f25..af10abceb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- #include "utils/dbg.h" diff --git a/src/platformio.ini b/src/platformio.ini index ba6c8be0b..cae359340 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -15,16 +15,17 @@ include_dir = . [env] framework = arduino board_build.filesystem = littlefs +upload_speed = 921600 ;build_flags = ; ;;;;; Possible Debug options ;;;;;; ; https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level - ;-DDEBUG_ESP_PORT=Serial + ;-DDEBUG_ESP_PORT=Serial ;-DDEBUG_ESP_CORE - ;-DDEBUG_ESP_WIFI - ;-DDEBUG_ESP_HTTP_CLIENT - ;-DDEBUG_ESP_HTTP_SERVER - ;-DDEBUG_ESP_OOM + ;-DDEBUG_ESP_WIFI + ;-DDEBUG_ESP_HTTP_CLIENT + ;-DDEBUG_ESP_HTTP_SERVER + ;-DDEBUG_ESP_OOM monitor_speed = 115200 @@ -34,15 +35,13 @@ extra_scripts = lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer - nrf24/RF24 - paulstoffregen/Time - https://github.com/bertmelis/espMqttClient#v1.3.3 - bblanchon/ArduinoJson - ;esp8266/DNSServer - ;esp8266/EEPROM - ;esp8266/ESP8266WiFi - ;esp8266/SPI - ;esp8266/Ticker + nrf24/RF24 @ ^1.4.5 + paulstoffregen/Time @ ^1.6.1 + https://github.com/bertmelis/espMqttClient#v1.4.2 + bblanchon/ArduinoJson @ ^6.21.0 + https://github.com/JChristensen/Timezone @ ^1.2.4 + olikraus/U8g2 @ ^2.34.16 + zinggjm/GxEPD2 @ ^1.5.0 [env:esp8266-release] @@ -50,20 +49,34 @@ platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L build_flags = -D RELEASE + ;-Wl,-Map,output.map monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line + ;default ; Remove typical terminal control codes from input + ;time ; Add timestamp with milliseconds for each new line + ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + esp8266_exception_decoder + + +[env:esp8266-release-prometheus] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L +build_flags = -D RELEASE -DENABLE_PROMETHEUS_EP +monitor_filters = + ;default ; Remove typical terminal control codes from input + ;time ; Add timestamp with milliseconds for each new line ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + esp8266_exception_decoder [env:esp8266-debug] platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L -build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial +build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 build_type = debug monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line + ;default ; Remove typical terminal control codes from input + time ; Add timestamp with milliseconds for each new line log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory [env:esp8285-release] @@ -73,8 +86,8 @@ board_build.ldscript = eagle.flash.1m64.ld board_build.f_cpu = 80000000L build_flags = -D RELEASE monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line + ;default ; Remove typical terminal control codes from input + time ; Add timestamp with milliseconds for each new line ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory [env:esp8285-debug] @@ -85,55 +98,31 @@ board_build.f_cpu = 80000000L build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial build_type = debug monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line + ;default ; Remove typical terminal control codes from input + time ; Add timestamp with milliseconds for each new line log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory -[env:esp8266-nokia5110] -platform = espressif8266 -board = esp12e -board_build.f_cpu = 80000000L -build_flags = -D RELEASE -DU8X8_NO_HW_I2C -DENA_NOKIA -monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line - ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory -lib_deps = - https://github.com/yubox-node-org/ESPAsyncWebServer - nrf24/RF24 - paulstoffregen/Time - https://github.com/bertmelis/espMqttClient#v1.3.3 - bblanchon/ArduinoJson - olikraus/U8g2 - https://github.com/JChristensen/Timezone - -[env:esp8266-ssd1306] -platform = espressif8266 -board = esp12e -board_build.f_cpu = 80000000L -build_flags = -D RELEASE -DENA_SSD1306 +[env:esp32-wroom32-release] +platform = espressif32 +board = lolin_d32 +build_flags = -D RELEASE -std=gnu++14 +build_unflags = -std=gnu++11 monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line + ;default ; Remove typical terminal control codes from input + ;time ; Add timestamp with milliseconds for each new line ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory -lib_deps = - https://github.com/yubox-node-org/ESPAsyncWebServer - nrf24/RF24 - paulstoffregen/Time - https://github.com/bertmelis/espMqttClient#v1.3.3 - bblanchon/ArduinoJson - https://github.com/ThingPulse/esp8266-oled-ssd1306.git - https://github.com/JChristensen/Timezone + esp32_exception_decoder -[env:esp32-wroom32-release] +[env:esp32-wroom32-release-prometheus] platform = espressif32 board = lolin_d32 -build_flags = -D RELEASE -std=gnu++14 +build_flags = -D RELEASE -std=gnu++14 -DENABLE_PROMETHEUS_EP build_unflags = -std=gnu++11 monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line + ;default ; Remove typical terminal control codes from input + ;time ; Add timestamp with milliseconds for each new line ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + esp32_exception_decoder [env:esp32-wroom32-debug] platform = espressif32 @@ -142,42 +131,16 @@ build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ build_unflags = -std=gnu++11 build_type = debug monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line + ;default ; Remove typical terminal control codes from input + time ; Add timestamp with milliseconds for each new line log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory -[env:esp32-wroom32-nokia5110] -platform = espressif32 -board = lolin_d32 -build_flags = -D RELEASE -std=gnu++14 -DU8X8_NO_HW_I2C -DENA_NOKIA -build_unflags = -std=gnu++11 -monitor_filters = - ;default ; Remove typical terminal control codes from input - time ; Add timestamp with milliseconds for each new line - ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory -lib_deps = - https://github.com/yubox-node-org/ESPAsyncWebServer - nrf24/RF24 - paulstoffregen/Time - https://github.com/bertmelis/espMqttClient#v1.3.3 - bblanchon/ArduinoJson - olikraus/U8g2 - https://github.com/JChristensen/Timezone - -[env:esp32-wroom32-ssd1306] +[env:opendtufusionv1-release] platform = espressif32 -board = lolin_d32 -build_flags = -D RELEASE -std=gnu++14 -DENA_SSD1306 +board = esp32-s3-devkitc-1 +build_flags = -D RELEASE -std=gnu++14 build_unflags = -std=gnu++11 monitor_filters = ;default ; Remove typical terminal control codes from input time ; Add timestamp with milliseconds for each new line ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory -lib_deps = - https://github.com/yubox-node-org/ESPAsyncWebServer - nrf24/RF24 - paulstoffregen/Time - https://github.com/bertmelis/espMqttClient#v1.3.3 - bblanchon/ArduinoJson - https://github.com/ThingPulse/esp8266-oled-ssd1306.git - https://github.com/JChristensen/Timezone diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h new file mode 100644 index 000000000..1a0222b22 --- /dev/null +++ b/src/plugins/Display/Display.h @@ -0,0 +1,114 @@ +#ifndef __DISPLAY__ +#define __DISPLAY__ + +#include +#include + +#include "../../hm/hmSystem.h" +#include "../../utils/helper.h" +#include "Display_Mono.h" +#include "Display_ePaper.h" + +template +class Display { + public: + Display() {} + + void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { + mCfg = cfg; + mSys = sys; + mUtcTs = utcTs; + mNewPayload = false; + mLoopCnt = 0; + mVersion = version; + + if (mCfg->type == 0) + return; + + if ((0 < mCfg->type) && (mCfg->type < 10)) { + mMono.config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); + mMono.init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + } else if (mCfg->type >= 10) { + #if defined(ESP32) + mRefreshCycle = 0; + mEpaper.config(mCfg->rot); + mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + #endif + } + } + + void payloadEventListener(uint8_t cmd) { + mNewPayload = true; + } + + void tickerSecond() { + mMono.loop(); + if (mNewPayload || ((++mLoopCnt % 10) == 0)) { + mNewPayload = false; + mLoopCnt = 0; + DataScreen(); + } + } + + private: + void DataScreen() { + if (mCfg->type == 0) + return; + if (*mUtcTs == 0) + return; + + float totalPower = 0; + float totalYieldDay = 0; + float totalYieldTotal = 0; + + uint8_t isprod = 0; + + Inverter<> *iv; + record_t<> *rec; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + iv = mSys->getInverterByPos(i); + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if (iv == NULL) + continue; + + if (iv->isProducing(*mUtcTs)) + isprod++; + + totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); + totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); + } + + if ((0 < mCfg->type) && (mCfg->type < 10)) { + mMono.disp(totalPower, totalYieldDay, totalYieldTotal, isprod); + } else if (mCfg->type >= 10) { + #if defined(ESP32) + mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); + mRefreshCycle++; + #endif + } + + #if defined(ESP32) + if (mRefreshCycle > 480) { + mEpaper.fullRefresh(); + mRefreshCycle = 0; + } + #endif + } + + // private member variables + bool mNewPayload; + uint8_t mLoopCnt; + uint32_t *mUtcTs; + const char *mVersion; + display_t *mCfg; + HMSYSTEM *mSys; + uint16_t mRefreshCycle; + + #if defined(ESP32) + DisplayEPaper mEpaper; + #endif + DisplayMono mMono; +}; + +#endif /*__DISPLAY__*/ diff --git a/src/plugins/Display/Display_Mono.cpp b/src/plugins/Display/Display_Mono.cpp new file mode 100644 index 000000000..d55b6061e --- /dev/null +++ b/src/plugins/Display/Display_Mono.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "Display_Mono.h" + +#ifdef ESP8266 + #include +#elif defined(ESP32) + #include +#endif +#include "../../utils/helper.h" + +//#ifdef U8X8_HAVE_HW_SPI +//#include +//#endif +//#ifdef U8X8_HAVE_HW_I2C +//#include +//#endif + +DisplayMono::DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + _dispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; +} + + + +void DisplayMono::init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) { + if ((0 < type) && (type < 4)) { + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + switch(type) { + case 1: + mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + default: + case 2: + mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + case 3: + mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); + break; + } + + mUtcTs = utcTs; + + mDisplay->begin(); + + mIsLarge = (mDisplay->getWidth() > 120); + calcLineHeights(); + + mDisplay->clearBuffer(); + if (3 != mType) + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0, 35); + printText("ahoydtu.de", 2, 20); + printText(version, 3, 46); + mDisplay->sendBuffer(); + } +} + +void DisplayMono::config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; +} + +void DisplayMono::loop(void) { + if (mEnPowerSafe) + if(mTimeout != 0) + mTimeout--; +} + +void DisplayMono::disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + + + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + if (totalPower > 999) { + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + } else { + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + } + printText(_fmtText, 0); + } else { + printText("offline", 0, 25); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(_fmtText, 1); + + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(_fmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(_mExtra % 10) && (ip)) { + printText(ip.toString().c_str(), 3); + } else if (!(_mExtra % 5)) { + snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(_fmtText, 3); + } else { + if(mIsLarge && (NULL != mUtcTs)) + printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + else + printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + } + + mDisplay->sendBuffer(); + + _dispY = 0; + _mExtra++; +} + +void DisplayMono::calcLineHeights() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (mDisplay->getMaxCharHeight()); + mLineOffsets[i] = yOff; + } +} + +inline void DisplayMono::setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_logisoso16_tr); + break; + case 3: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + default: + mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); + break; + } +} + +void DisplayMono::printText(const char* text, uint8_t line, uint8_t dispX) { + if (!mIsLarge) { + dispX = (line == 0) ? 10 : 5; + } + setFont(line); + + dispX += (mEnScreenSaver) ? (_mExtra % 7) : 0; + mDisplay->drawStr(dispX, mLineOffsets[line], text); +} diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h new file mode 100644 index 000000000..ad04c9f4f --- /dev/null +++ b/src/plugins/Display/Display_Mono.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#define DISP_DEFAULT_TIMEOUT 60 // in seconds +#define DISP_FMT_TEXT_LEN 32 + +class DisplayMono { + public: + DisplayMono(); + + void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version); + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum); + void loop(void); + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); + + private: + void calcLineHeights(); + void setFont(uint8_t line); + void printText(const char* text, uint8_t line, uint8_t dispX = 5); + + U8G2* mDisplay; + + uint8_t mType; + bool mEnPowerSafe, mEnScreenSaver; + uint8_t mLuminance; + + bool mIsLarge = false; + uint8_t mLoopCnt; + uint32_t* mUtcTs; + uint8_t mLineOffsets[5]; + + uint16_t _dispY; + + uint8_t _mExtra; + uint16_t mTimeout; + char _fmtText[DISP_FMT_TEXT_LEN]; +}; diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp new file mode 100644 index 000000000..99d35ed8c --- /dev/null +++ b/src/plugins/Display/Display_ePaper.cpp @@ -0,0 +1,197 @@ +#include "Display_ePaper.h" + +#ifdef ESP8266 + #include +#elif defined(ESP32) + #include +#endif +#include "../../utils/helper.h" +#include "imagedata.h" + +#if defined(ESP32) + +static const uint32_t spiClk = 4000000; // 4 MHz + +#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) +SPIClass hspi(HSPI); +#endif + +DisplayEPaper::DisplayEPaper() { + mDisplayRotation = 2; + mHeadFootPadding = 16; +} + + +//*************************************************************************** +void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char *version) { + mUtcTs = utcTs; + + if (type > 9) { + Serial.begin(115200); + _display = new GxEPD2_BW(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); + hspi.begin(_SCK, _BUSY, _MOSI, _CS); + +#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) + _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0)); +#endif + _display->init(115200, true, 2, false); + _display->setRotation(mDisplayRotation); + _display->setFullWindow(); + + // Logo + _display->fillScreen(GxEPD_BLACK); + _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); + while (_display->nextPage()) + ; + + // clean the screen + delay(2000); + _display->fillScreen(GxEPD_WHITE); + while (_display->nextPage()) + ; + + headlineIP(); + + // call the PowerPage to change the PV Power Values + actualPowerPaged(0, 0, 0, 0); + } +} + +void DisplayEPaper::config(uint8_t rotation) { + mDisplayRotation = rotation; +} + +//*************************************************************************** +void DisplayEPaper::fullRefresh() { + // screen complete black + _display->fillScreen(GxEPD_BLACK); + while (_display->nextPage()) + ; + delay(2000); + // screen complete white + _display->fillScreen(GxEPD_WHITE); + while (_display->nextPage()) + ; +} +//*************************************************************************** +void DisplayEPaper::headlineIP() { + int16_t tbx, tby; + uint16_t tbw, tbh; + + _display->setFont(&FreeSans9pt7b); + _display->setTextColor(GxEPD_WHITE); + + _display->setPartialWindow(0, 0, _display->width(), mHeadFootPadding); + _display->fillScreen(GxEPD_BLACK); + + do { + if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) { + snprintf(_fmtText, sizeof(_fmtText), "%s", WiFi.localIP().toString().c_str()); + } else { + snprintf(_fmtText, sizeof(_fmtText), "WiFi not connected"); + } + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + uint16_t x = ((_display->width() - tbw) / 2) - tbx; + + _display->setCursor(x, (mHeadFootPadding - 2)); + _display->println(_fmtText); + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaper::lastUpdatePaged() { + int16_t tbx, tby; + uint16_t tbw, tbh; + + _display->setFont(&FreeSans9pt7b); + _display->setTextColor(GxEPD_WHITE); + + _display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding); + _display->fillScreen(GxEPD_BLACK); + do { + if (NULL != mUtcTs) { + snprintf(_fmtText, sizeof(_fmtText), ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str()); + + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + uint16_t x = ((_display->width() - tbw) / 2) - tbx; + + _display->setCursor(x, (_display->height() - 3)); + _display->println(_fmtText); + } + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod) { + int16_t tbx, tby; + uint16_t tbw, tbh, x, y; + + _display->setFont(&FreeSans24pt7b); + _display->setTextColor(GxEPD_BLACK); + + _display->setPartialWindow(0, mHeadFootPadding, _display->width(), _display->height() - (mHeadFootPadding * 2)); + _display->fillScreen(GxEPD_WHITE); + do { + if (_totalPower > 9999) { + snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (_totalPower / 10000)); + _changed = true; + } else if ((_totalPower > 0) && (_totalPower <= 9999)) { + snprintf(_fmtText, sizeof(_fmtText), "%.0f W", _totalPower); + _changed = true; + } else { + snprintf(_fmtText, sizeof(_fmtText), "offline"); + } + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, mHeadFootPadding + tbh + 10); + _display->print(_fmtText); + + _display->setFont(&FreeSans12pt7b); + y = _display->height() / 2; + _display->setCursor(5, y); + _display->print("today:"); + snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 38, y); + _display->println("Wh"); + + y = y + tbh + 7; + _display->setCursor(5, y); + _display->print("total:"); + snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 50, y); + _display->println("kWh"); + + _display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); + snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod); + _display->println(_fmtText); + + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + // check if the IP has changed + if (_settedIP != WiFi.localIP().toString().c_str()) { + // save the new IP and call the Headline Funktion to adapt the Headline + _settedIP = WiFi.localIP().toString().c_str(); + headlineIP(); + } + + // call the PowerPage to change the PV Power Values + actualPowerPaged(totalPower, totalYieldDay, totalYieldTotal, isprod); + + // if there was an change and the Inverter is producing set a new Timestam in the footline + if ((isprod > 0) && (_changed)) { + _changed = false; + lastUpdatePaged(); + } + + _display->powerOff(); +} +//*************************************************************************** +#endif // ESP32 diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h new file mode 100644 index 000000000..b2729f259 --- /dev/null +++ b/src/plugins/Display/Display_ePaper.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#if defined(ESP32) + +// uncomment next line to use HSPI for EPD (and VSPI for SD), e.g. with Waveshare ESP32 Driver Board +#define USE_HSPI_FOR_EPD + +/// uncomment next line to use class GFX of library GFX_Root instead of Adafruit_GFX, to use less code and ram +// #include +// base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code +// enable GxEPD2_GFX base class +#define ENABLE_GxEPD2_GFX 1 + +#include +#include +#include + +#include +// FreeFonts from Adafruit_GFX +#include +#include +#include +#include + +// GDEW027C44 2.7 " b/w/r 176x264, IL91874 +// GDEH0154D67 1.54" b/w 200x200 + +class DisplayEPaper { + public: + DisplayEPaper(); + void fullRefresh(); + void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char* version); + void config(uint8_t rotation); + void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); + + + private: + void headlineIP(); + void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); + void lastUpdatePaged(); + + uint8_t mDisplayRotation; + bool _changed = false; + char _fmtText[35]; + const char* _settedIP; + uint8_t mHeadFootPadding; + GxEPD2_GFX* _display; + uint32_t *mUtcTs; +}; + +#endif // ESP32 diff --git a/src/plugins/Display/imagedata.h b/src/plugins/Display/imagedata.h new file mode 100644 index 000000000..baaddec85 --- /dev/null +++ b/src/plugins/Display/imagedata.h @@ -0,0 +1,329 @@ +// GxEPD2_ESP32_ESP8266_WifiData_V1_und_V2 + +#ifndef __IMAGEDATA_H__ +#define __IMAGEDATA_H__ + +#if defined(__AVR__) || defined(ARDUINO_ARCH_SAMD) +#include +#elif defined(ESP8266) || defined(ESP32) +#include +#endif + +// 'Logo', 200x200px +const unsigned char logo[] PROGMEM = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, + 0x0b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x06, + 0x0f, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x7e, 0x0f, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x03, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x19, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfe, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xe0, 0x70, 0x7f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe0, 0x3f, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfc, 0x0f, 0xe0, 0x3f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xe0, 0x1f, 0x83, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xe0, 0x1f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0, + 0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0, 0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x07, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0xff, 0xc1, 0x07, 0x80, 0x07, 0xfe, 0xff, 0xff, 0xfc, 0x07, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xe1, 0x07, 0xc0, 0x01, 0xe0, 0x0f, + 0xff, 0xfc, 0x0f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0xff, 0xe1, 0x83, 0xc0, 0x01, 0xc0, 0x07, 0xff, 0xf8, 0x0f, 0xfc, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe1, 0x83, 0xc0, 0x00, + 0xc0, 0x07, 0x8f, 0xf8, 0x1f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe0, 0x01, 0xc0, 0x00, 0x81, 0x83, 0x07, 0xf0, 0x3f, 0xf9, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xe0, 0x01, + 0xe0, 0xe0, 0x87, 0xe3, 0x0f, 0xf0, 0x3f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xe0, 0x00, 0xe0, 0xe0, 0x87, 0xe1, 0x0c, 0x60, 0x7f, + 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, + 0xe0, 0x00, 0xe1, 0xf0, 0x87, 0xe1, 0x08, 0x60, 0x7f, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xe0, 0xe0, 0xe0, 0xe0, 0x87, 0xc2, 0x00, + 0x40, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x8f, 0xc0, 0xe0, 0x60, 0xe0, 0xc0, 0x82, 0x00, 0xc0, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xc0, 0xe0, 0x60, 0xe0, 0xc0, + 0x06, 0x01, 0x81, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xcf, 0xe0, 0xe0, 0x20, 0xe0, 0xe0, 0x0c, 0x03, 0x81, 0xff, 0x1f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x30, + 0xe1, 0xf8, 0x18, 0x07, 0xe1, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x7f, 0xff, 0xff, 0xf0, 0x1f, 0xf3, 0xfe, 0x01, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xc0, + 0xfb, 0xff, 0xff, 0xff, 0xe0, 0x3e, 0x1f, 0xfc, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xfc, 0x0f, + 0xf8, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0x33, 0xef, 0xff, 0xff, 0xff, 0xff, 0x81, 0xfc, 0x0f, 0xf1, 0xff, 0x07, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xf1, 0xff, 0xff, 0xa0, 0x00, 0x7f, 0xe3, + 0xfc, 0x0f, 0xf3, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf1, 0xf9, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xfc, 0x0f, 0xe7, 0xff, 0xe0, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf9, 0xff, 0x80, 0x3f, 0xff, + 0xe0, 0x0f, 0xfe, 0x1f, 0xc7, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xcf, 0xf8, 0xf0, 0x07, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0x8f, 0xff, 0xfc, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x70, 0x3f, + 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0x1f, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc, 0x63, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0x3f, + 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfe, + 0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7e, 0x3f, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x0c, 0x7f, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, + 0x7f, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xe1, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf1, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xf8, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x87, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x00, 0x3f, 0xff, 0xf8, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x00, 0x00, 0x01, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x55, 0x00, 0x3f, 0xf8, 0x00, + 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x01, 0xff, 0xff, 0xf8, 0x0f, 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x9f, 0xff, 0xf8, 0x03, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xff, 0x03, + 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe3, 0xf1, 0xff, + 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xe0, 0xfe, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe7, 0xf9, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x7e, 0x06, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xcf, + 0xf8, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0x03, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x1f, 0x23, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, + 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xf3, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xf1, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xf8, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x0f, 0xff, 0x8f, 0xf1, 0xff, 0xff, 0xff, 0xfe, 0xf5, 0x90, 0x07, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x03, 0xff, + 0x1f, 0xe3, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0xfe, 0x31, 0xfe, 0x7f, 0xe7, 0xff, 0x80, 0x00, 0x40, 0x00, + 0x07, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0x3c, + 0xf9, 0xfc, 0xff, 0xe7, 0xfe, 0x3f, 0xc9, 0xff, 0xf1, 0x1f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x3c, 0xf9, 0xf9, 0xff, 0xc7, 0xfc, 0xff, 0x90, + 0x7f, 0xf3, 0x03, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, + 0x3f, 0x39, 0xfd, 0xf3, 0xff, 0xcf, 0xfc, 0xff, 0x90, 0x3f, 0xf3, 0x83, 0xf8, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x39, 0xf9, 0xc7, 0xff, 0xcf, 0xfc, + 0xff, 0x32, 0x7f, 0xe4, 0x77, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0xff, 0xff, 0x7f, 0x33, 0xf9, 0x8f, 0xff, 0xcf, 0xf9, 0xff, 0x00, 0x7f, 0xe0, 0x67, 0xfc, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xb3, 0xf3, 0xbf, 0xff, + 0xcf, 0xf9, 0xff, 0x00, 0xff, 0xfe, 0x47, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xf7, 0xf3, 0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe0, 0xff, 0xfc, 0x0f, + 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xe7, 0xe7, + 0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe1, 0xff, 0xfc, 0x1f, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xe7, 0xef, 0xff, 0xc7, 0xf9, 0xff, 0xc3, 0xff, + 0xfc, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, + 0xef, 0xef, 0xc0, 0xff, 0xe7, 0xf9, 0xff, 0xc3, 0xff, 0xf8, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xcf, 0xf0, 0x01, 0xe7, 0xf1, 0xff, + 0x87, 0xff, 0xf8, 0x7f, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, + 0xff, 0xbf, 0xcf, 0xe7, 0xff, 0xc1, 0xe3, 0xe1, 0xff, 0x8f, 0xff, 0xf0, 0xff, 0xff, 0x9f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x9f, 0xef, 0xe7, 0xff, 0xff, 0xf3, + 0xc1, 0xff, 0x96, 0xaf, 0xf9, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf9, 0xff, 0xff, 0x9f, 0xe7, 0xe3, 0xff, 0xff, 0xf1, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, + 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xcf, 0xe7, 0xf3, 0xff, + 0xff, 0xf8, 0xc0, 0x00, 0x4a, 0x90, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf9, 0xff, 0xff, 0xef, 0xf3, 0xf3, 0x9f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe7, 0xf1, + 0xe7, 0xc7, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf3, 0xf0, 0x07, 0xe3, 0xff, 0xff, 0x81, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, + 0xf8, 0x07, 0x1f, 0xf1, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xfc, 0x1f, 0x9f, 0xf8, 0xff, 0xff, 0xc3, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, + 0xff, 0xff, 0xf8, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf9, 0xff, 0x9f, 0xfe, 0x3f, + 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfd, 0xff, 0xff, 0xf1, 0xff, 0x9f, 0xff, 0x9f, 0xff, 0xf3, 0xff, 0x3f, 0x3f, 0xff, 0xff, + 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe1, 0xff, 0xcf, + 0xff, 0xc7, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xe1, 0xff, 0x8f, 0xff, 0xe7, 0xff, 0xf3, 0xff, 0x3f, 0x9f, + 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xc1, + 0xff, 0xcf, 0xff, 0xf3, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x81, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xf3, 0xff, + 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, + 0xff, 0x91, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0x11, 0xff, 0x9f, 0xff, 0xff, 0xff, + 0xf3, 0xff, 0x1f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0x7f, 0xff, 0x21, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xbf, 0x9f, 0xff, 0xff, 0xfe, + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x20, 0xff, 0x9f, 0xff, + 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x60, 0x7f, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0x64, 0x3f, + 0x1f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0xe7, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, + 0xff, 0x3f, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc, + 0xe7, 0x80, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xf1, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xf8, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, + 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x9f, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xe7, 0xff, 0xfe, 0x7f, 0xff, 0xc3, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf3, 0xf3, 0xff, 0xfc, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xff, 0xf8, 0xff, 0xff, + 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xf9, 0xe7, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xf3, 0xf9, 0xff, 0xe1, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe7, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0x3f, 0x07, + 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf3, 0xe7, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x00, 0x1f, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, + 0xe0, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, + 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf7, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x83, 0xe7, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x13, 0xe7, 0xff, 0xfc, 0x03, + 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xfc, 0x31, 0xe7, 0xff, 0xfc, 0x00, 0x7f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfe, + 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x39, 0xe3, 0xff, + 0xfc, 0x00, 0x1f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x31, 0xf3, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0xc7, 0xff, 0xff, + 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x19, + 0xf3, 0xff, 0xfc, 0x00, 0x07, 0xff, 0x87, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xf3, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07, + 0xff, 0xff, 0xff, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x83, 0xf3, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xf3, 0xff, 0xff, 0xff, 0xff, + 0xf8, 0x07, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xe3, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0xff, 0xff, 0xfe, 0x1f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe1, 0xff, 0xfe, + 0x01, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xe1, 0xff, 0xf0, 0x00, 0x3f, 0x80, 0x07, 0xff, 0xff, 0xf0, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x4c, + 0xff, 0xf0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0c, 0xff, 0xf0, 0x00, 0x00, 0x0b, 0x87, 0xff, + 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x0e, 0x7f, 0xf8, 0x00, 0x3f, 0xff, 0xc7, 0xff, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x86, 0x7f, 0xfe, 0x00, 0xff, 0xff, + 0xc3, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x80, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xf3, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, + 0xff, 0xff, 0xf3, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x07, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, + 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xf3, 0xc0, 0x7f, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xe3, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, 0xe0, + 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x03, 0xff, + 0xfe, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x17, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + +#endif /*__IMAGEDATA_H__*/ diff --git a/src/plugins/MonochromeDisplay/MonochromeDisplay.h b/src/plugins/MonochromeDisplay/MonochromeDisplay.h deleted file mode 100644 index 832369921..000000000 --- a/src/plugins/MonochromeDisplay/MonochromeDisplay.h +++ /dev/null @@ -1,307 +0,0 @@ -#ifndef __MONOCHROME_DISPLAY__ -#define __MONOCHROME_DISPLAY__ - -#if defined(ENA_NOKIA) || defined(ENA_SSD1306) -#ifdef ENA_NOKIA - #include - #define DISP_PROGMEM U8X8_PROGMEM -#else // ENA_SSD1306 - /* esp8266 : SCL = 5, SDA = 4 */ - /* ewsp32 : SCL = 22, SDA = 21 */ - #include - #include - #define DISP_PROGMEM PROGMEM -#endif - -#include - -#include "../../utils/helper.h" -#include "../../hm/hmSystem.h" - -static uint8_t bmp_arrow[] DISP_PROGMEM = { - B00000000, B00011100, B00011100, B00001110, B00001110, B11111110, B01111111, - B01110000, B01110000, B00110000, B00111000, B00011000, B01111111, B00111111, - B00011110, B00001110, B00000110, B00000000, B00000000, B00000000, B00000000}; - -static TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time -static TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Tim - -template -class MonochromeDisplay { - public: - #if defined(ENA_NOKIA) - MonochromeDisplay() : mDisplay(U8G2_R0, 5, 4, 16), mCE(CEST, CET) { - mNewPayload = false; - mExtra = 0; - } - #else // ENA_SSD1306 - MonochromeDisplay() : mDisplay(0x3c, SDA, SCL), mCE(CEST, CET) { - mNewPayload = false; - mExtra = 0; - mRx = 0; - mUp = 1; - } - #endif - - void setup(HMSYSTEM *sys, uint32_t *utcTs) { - mSys = sys; - mUtcTs = utcTs; - memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS ); - memset( mTotal, 0, sizeof(float)*MAX_NUM_INVERTERS ); - mLastHour = 25; - #if defined(ENA_NOKIA) - mDisplay.begin(); - ShowInfoText("booting..."); - #else - mDisplay.init(); - mDisplay.flipScreenVertically(); - mDisplay.setContrast(63); - mDisplay.setBrightness(63); - - mDisplay.clear(); - mDisplay.setFont(ArialMT_Plain_24); - mDisplay.setTextAlignment(TEXT_ALIGN_CENTER_BOTH); - - mDisplay.drawString(64,22,"Starting..."); - mDisplay.display(); - mDisplay.setTextAlignment(TEXT_ALIGN_LEFT); - #endif - } - - void loop(void) { - - } - - void payloadEventListener(uint8_t cmd) { - mNewPayload = true; - } - - void tickerSecond() { - static int cnt=1; - if(mNewPayload || !(cnt % 10)) { - cnt=1; - mNewPayload = false; - DataScreen(); - } - else - cnt++; - } - - private: - #if defined(ENA_NOKIA) - void ShowInfoText(const char *txt) { - /* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */ - mDisplay.clear(); - mDisplay.firstPage(); - do { - const char *e; - const char *p = txt; - int y=10; - mDisplay.setFont(u8g2_font_5x8_tr); - while(1) { - for(e=p+1; (*e && (*e != '\n')); e++); - size_t len=e-p; - mDisplay.setCursor(2,y); - String res=((String)p).substring(0,len); - mDisplay.print(res); - if ( !*e ) - break; - p=e+1; - y+=12; - } - mDisplay.sendBuffer(); - } while( mDisplay.nextPage() ); - } - #endif - - void DataScreen(void) { - String timeStr = ah::getDateTimeStr(mCE.toLocal(*mUtcTs)).substring(2, 22); - int hr = timeStr.substring(9,2).toInt(); - IPAddress ip = WiFi.localIP(); - float totalYield = 0.0, totalYieldToday = 0.0, totalActual = 0.0; - char fmtText[32]; - int ucnt=0, num_inv=0; - unsigned int pow_i[ MAX_NUM_INVERTERS ]; - - memset( pow_i, 0, sizeof(unsigned int)* MAX_NUM_INVERTERS ); - if ( hr < mLastHour ) // next day ? reset today-values - memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS ); - mLastHour = hr; - - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - uint8_t pos; - uint8_t list[] = {FLD_PAC, FLD_YT, FLD_YD}; - - for (uint8_t fld = 0; fld < 3; fld++) { - pos = iv->getPosByChFld(CH0, list[fld],rec); - int isprod = iv->isProducing(*mUtcTs,rec); - - if(fld == 1) - { - if ( isprod ) - mTotal[num_inv] = iv->getValue(pos,rec); - totalYield += mTotal[num_inv]; - } - if(fld == 2) - { - if ( isprod ) - mToday[num_inv] = iv->getValue(pos,rec); - totalYieldToday += mToday[num_inv]; - } - if((fld == 0) && isprod ) - { - pow_i[num_inv] = iv->getValue(pos,rec); - totalActual += iv->getValue(pos,rec); - ucnt++; - } - } - num_inv++; - } - } - /* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */ - mDisplay.clear(); - #if defined(ENA_NOKIA) - mDisplay.firstPage(); - do { - if(ucnt) { - mDisplay.drawXBMP(10,1,8,17,bmp_arrow); - mDisplay.setFont(u8g2_font_logisoso16_tr); - mDisplay.setCursor(25,17); - sprintf(fmtText,"%3.0f",totalActual); - mDisplay.print(String(fmtText)+F(" W")); - } - else - { - mDisplay.setFont(u8g2_font_logisoso16_tr ); - mDisplay.setCursor(10,17); - mDisplay.print(String(F("offline"))); - } - mDisplay.drawHLine(2,20,78); - mDisplay.setFont(u8g2_font_5x8_tr); - mDisplay.setCursor(5,29); - if (( num_inv < 2 ) || !(mExtra%2)) - { - sprintf(fmtText,"%4.0f",totalYieldToday); - mDisplay.print(F("today ")+String(fmtText)+F(" Wh")); - mDisplay.setCursor(5,37); - sprintf(fmtText,"%.1f",totalYield); - mDisplay.print(F("total ")+String(fmtText)+F(" kWh")); - } - else - { - int id1=(mExtra/2)%(num_inv-1); - if( pow_i[id1] ) - mDisplay.print(F("#")+String(id1+1)+F(" ")+String(pow_i[id1])+F(" W")); - else - mDisplay.print(F("#")+String(id1+1)+F(" -----")); - mDisplay.setCursor(5,37); - if( pow_i[id1+1] ) - mDisplay.print(F("#")+String(id1+2)+F(" ")+String(pow_i[id1+1])+F(" W")); - else - mDisplay.print(F("#")+String(id1+2)+F(" -----")); - } - if ( !(mExtra%10) && ip ) { - mDisplay.setCursor(5,47); - mDisplay.print(ip.toString()); - } - else { - mDisplay.setCursor(0,47); - mDisplay.print(timeStr); - } - - mDisplay.sendBuffer(); - } while( mDisplay.nextPage() ); - mExtra++; - #else // ENA_SSD1306 - if(mUp) { - mRx += 2; - if(mRx >= 20) - mUp = 0; - } else { - mRx -= 2; - if(mRx <= 0) - mUp = 1; - } - int ex = 2*( mExtra % 5 ); - - if(ucnt) { - mDisplay.setBrightness(63); - mDisplay.drawXbm(10+ex,5,8,17,bmp_arrow); - mDisplay.setFont(ArialMT_Plain_24); - sprintf(fmtText,"%3.0f",totalActual); - mDisplay.drawString(25+ex,0,String(fmtText)+F(" W")); - } - else - { - mDisplay.setBrightness(1); - mDisplay.setFont(ArialMT_Plain_24); - mDisplay.drawString(25+ex,0,String(F("offline"))); - } - mDisplay.setFont(ArialMT_Plain_16); - - if (( num_inv < 2 ) || !(mExtra%2)) - { - sprintf(fmtText,"%4.0f",totalYieldToday); - mDisplay.drawString(5,22,F("today ")+String(fmtText)+F(" Wh")); - sprintf(fmtText,"%.1f",totalYield); - mDisplay.drawString(5,35,F("total ")+String(fmtText)+F(" kWh")); - } - else - { - int id1=(mExtra/2)%(num_inv-1); - if( pow_i[id1] ) - mDisplay.drawString(15,22,F("#")+String(id1+1)+F(" ")+String(pow_i[id1])+F(" W")); - else - mDisplay.drawString(15,22,F("#")+String(id1+1)+F(" -----")); - if( pow_i[id1+1] ) - mDisplay.drawString(15,35,F("#")+String(id1+2)+F(" ")+String(pow_i[id1+1])+F(" W")); - else - mDisplay.drawString(15,35,F("#")+String(id1+2)+F(" -----")); - } - mDisplay.drawLine(2,23,123,23); - - if ( (!(mExtra%10) && ip )|| (timeStr.length()<16)) - { - mDisplay.drawString(5,49,ip.toString()); - } - else - { - int w=mDisplay.getStringWidth(timeStr.c_str(),timeStr.length(),0); - if ( w>127 ) - { - String tt=timeStr.substring(9,17); - w=mDisplay.getStringWidth(tt.c_str(),tt.length(),0); - mDisplay.drawString(127-w-mRx,49,tt); - } - else - mDisplay.drawString(0,49,timeStr); - } - - mDisplay.display(); - mExtra++; - #endif - } - - // private member variables - #if defined(ENA_NOKIA) - U8G2_PCD8544_84X48_1_4W_HW_SPI mDisplay; - #else // ENA_SSD1306 - SSD1306Wire mDisplay; - int mRx; - char mUp; - #endif - int mExtra; - bool mNewPayload; - float mTotal[ MAX_NUM_INVERTERS ]; - float mToday[ MAX_NUM_INVERTERS ]; - uint32_t *mUtcTs; - int mLastHour; - HMSYSTEM *mSys; - Timezone mCE; -}; -#endif - -#endif /*__MONOCHROME_DISPLAY__*/ diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index bd7a709fd..b008d8d2e 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- // https://bert.emelis.net/espMqttClient/ @@ -15,52 +15,72 @@ #endif #include "../utils/dbg.h" -#include "../utils/ahoyTimer.h" #include "../config/config.h" #include #include #include "../defines.h" #include "../hm/hmSystem.h" +#include "pubMqttDefs.h" + #define QOS_0 0 typedef std::function subscriptionCb; +struct alarm_t { + uint16_t code; + uint32_t start; + uint32_t end; + alarm_t(uint16_t c, uint32_t s, uint32_t e) : code(c), start(s), end(e) {} +}; + +typedef struct { + bool running; + uint8_t lastIvId; + uint8_t sub; + uint8_t foundIvCnt; +} discovery_t; + template class PubMqtt { public: PubMqtt() { mRxCnt = 0; mTxCnt = 0; - mEnReconnect = false; mSubscriptionCb = NULL; - mIvAvail = true; - memset(mLastIvState, 0xff, MAX_NUM_INVERTERS); + memset(mLastIvState, MQTT_STATUS_NOT_AVAIL_NOT_PROD, MAX_NUM_INVERTERS); + mLastAnyAvail = false; } ~PubMqtt() { } void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs) { - mCfgMqtt = cfg_mqtt; - mDevName = devName; - mVersion = version; - mSys = sys; - mUtcTimestamp = utcTs; + mCfgMqtt = cfg_mqtt; + mDevName = devName; + mVersion = version; + mSys = sys; + mUtcTimestamp = utcTs; + mIntervalTimeout = 1; - snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic); + mDiscovery.running = false; - #if defined(ESP8266) - mHWifiCon = WiFi.onStationModeGotIP(std::bind(&PubMqtt::onWifiConnect, this, std::placeholders::_1)); - mHWifiDiscon = WiFi.onStationModeDisconnected(std::bind(&PubMqtt::onWifiDisconnect, this, std::placeholders::_1)); - #else - WiFi.onEvent(std::bind(&PubMqtt::onWiFiEvent, this, std::placeholders::_1)); - #endif + snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic); if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0)) mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd); - mClient.setClientId(mDevName); // TODO: add mac? + snprintf(mClientId, 24, "%s-", mDevName); + uint8_t pos = strlen(mClientId); + mClientId[pos++] = WiFi.macAddress().substring( 9, 10).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(10, 11).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(12, 13).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(13, 14).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(15, 16).c_str()[0]; + mClientId[pos++] = WiFi.macAddress().substring(16, 17).c_str()[0]; + mClientId[pos++] = '\0'; + + mClient.setClientId(mClientId); mClient.setServer(mCfgMqtt->broker, mCfgMqtt->port); - mClient.setWill(mLwtTopic, QOS_0, true, mLwtOffline); + mClient.setWill(mLwtTopic, QOS_0, true, mqttStr[MQTT_STR_LWT_NOT_CONN]); mClient.onConnect(std::bind(&PubMqtt::onConnect, this, std::placeholders::_1)); mClient.onDisconnect(std::bind(&PubMqtt::onDisconnect, this, std::placeholders::_1)); mClient.onMessage(std::bind(&PubMqtt::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); @@ -69,50 +89,108 @@ class PubMqtt { void loop() { #if defined(ESP8266) mClient.loop(); + yield(); #endif + + if(mDiscovery.running) + discoveryConfigLoop(); } + void tickerSecond() { - sendIvData(); + if (mIntervalTimeout > 0) + mIntervalTimeout--; + + if(mClient.disconnected()) { + mClient.connect(); + return; // next try in a second + } + + if(0 == mCfgMqtt->interval) // no fixed interval, publish once new data were received (from inverter) + sendIvData(); + else { // send mqtt data in a fixed interval + if(mIntervalTimeout == 0) { + mIntervalTimeout = mCfgMqtt->interval; + mSendList.push(RealTimeRunData_Debug); + sendIvData(); + } + } } void tickerMinute() { - processIvStatus(); - char val[12]; - snprintf(val, 12, "%ld", millis() / 1000); - publish("uptime", val); - publish("wifi_rssi", String(WiFi.RSSI()).c_str()); - publish("free_heap", String(ESP.getFreeHeap()).c_str()); - - if(!mClient.connected()) { - if(mEnReconnect) - mClient.connect(); - } + snprintf(mVal, 40, "%ld", millis() / 1000); + publish(subtopics[MQTT_UPTIME], mVal); + publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str()); + publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str()); + #ifndef ESP32 + publish(subtopics[MQTT_HEAP_FRAG], String(ESP.getHeapFragmentation()).c_str()); + #endif + } + + bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) { + if (!mClient.connected()) + return false; + + publish(subtopics[MQTT_SUNRISE], String(sunrise).c_str(), true); + publish(subtopics[MQTT_SUNSET], String(sunset).c_str(), true); + publish(subtopics[MQTT_COMM_START], String(sunrise - offs).c_str(), true); + publish(subtopics[MQTT_COMM_STOP], String(sunset + offs).c_str(), true); + publish(subtopics[MQTT_DIS_NIGHT_COMM], ((disNightCom) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + + return true; + } + + bool tickerComm(bool disabled) { + if (!mClient.connected()) + return false; + + publish(subtopics[MQTT_COMM_DISABLED], ((disabled) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + publish(subtopics[MQTT_COMM_DIS_TS], String(*mUtcTimestamp).c_str(), true); + + return true; } - void tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) { - publish("sunrise", String(sunrise).c_str(), true); - publish("sunset", String(sunset).c_str(), true); - publish("comm_start", String(sunrise - offs).c_str(), true); - publish("comm_stop", String(sunset + offs).c_str(), true); - publish("dis_night_comm", ((disNightCom) ? "true" : "false"), true); + void tickerMidnight() { + // set Total YieldDay to zero + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[FLD_YD]); + snprintf(mVal, 2, "0"); + publish(mSubTopic, mVal, true); } void payloadEventListener(uint8_t cmd) { - if(mClient.connected()) // prevent overflow if MQTT broker is not reachable but set - mSendList.push(cmd); + if(mClient.connected()) { // prevent overflow if MQTT broker is not reachable but set + if((0 == mCfgMqtt->interval) || (RealTimeRunData_Debug != cmd)) // no interval or no live data + mSendList.push(cmd); + } + } + + void alarmEventListener(uint16_t code, uint32_t start, uint32_t endTime) { + if(mClient.connected()) { + mAlarmList.push(alarm_t(code, start, endTime)); + } } void publish(const char *subTopic, const char *payload, bool retained = false, bool addTopic = true) { if(!mClient.connected()) return; - char topic[(MQTT_TOPIC_LEN << 1) + 2]; - snprintf(topic, ((MQTT_TOPIC_LEN << 1) + 2), "%s/%s", mCfgMqtt->topic, subTopic); - if(addTopic) - mClient.publish(topic, QOS_0, retained, payload); - else - mClient.publish(subTopic, QOS_0, retained, payload); + if(addTopic){ + snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s/%s", mCfgMqtt->topic, subTopic); + } else { + snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s", subTopic); + } + + do { + if(0 != mClient.publish(mTopic, QOS_0, retained, payload)) + break; + if(!mClient.connected()) + break; + #if defined(ESP8266) + mClient.loop(); + #endif + yield(); + } while(1); + mTxCnt++; } @@ -140,97 +218,36 @@ class PubMqtt { void sendDiscoveryConfig(void) { DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig")); - - char stateTopic[64], discoveryTopic[64], buffer[512], name[32], uniq_id[32]; - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - DynamicJsonDocument deviceDoc(128); - deviceDoc[F("name")] = iv->config->name; - deviceDoc[F("ids")] = String(iv->config->serial.u64, HEX); - deviceDoc[F("cu")] = F("http://") + String(WiFi.localIP().toString()); - deviceDoc[F("mf")] = F("Hoymiles"); - deviceDoc[F("mdl")] = iv->config->name; - JsonObject deviceObj = deviceDoc.as(); - DynamicJsonDocument doc(384); - - for (uint8_t i = 0; i < rec->length; i++) { - if (rec->assign[i].ch == CH0) { - snprintf(name, 32, "%s %s", iv->config->name, iv->getFieldName(i, rec)); - } else { - snprintf(name, 32, "%s CH%d %s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec)); - } - snprintf(stateTopic, 64, "/ch%d/%s", rec->assign[i].ch, iv->getFieldName(i, rec)); - snprintf(discoveryTopic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec)); - snprintf(uniq_id, 32, "ch%d_%s", rec->assign[i].ch, iv->getFieldName(i, rec)); - const char *devCls = getFieldDeviceClass(rec->assign[i].fieldId); - const char *stateCls = getFieldStateClass(rec->assign[i].fieldId); - - doc[F("name")] = name; - doc[F("stat_t")] = String(mCfgMqtt->topic) + "/" + String(iv->config->name) + String(stateTopic); - doc[F("unit_of_meas")] = iv->getUnit(i, rec); - doc[F("uniq_id")] = String(iv->config->serial.u64, HEX) + "_" + uniq_id; - doc[F("dev")] = deviceObj; - doc[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!? - if (devCls != NULL) - doc[F("dev_cla")] = devCls; - if (stateCls != NULL) - doc[F("stat_cla")] = stateCls; - - serializeJson(doc, buffer); - publish(discoveryTopic, buffer, true, false); - doc.clear(); - } - - yield(); - } - } + mDiscovery.running = true; + mDiscovery.lastIvId = 0; + mDiscovery.sub = 0; + mDiscovery.foundIvCnt = 0; } - private: - #if defined(ESP8266) - void onWifiConnect(const WiFiEventStationModeGotIP& event) { - DPRINTLN(DBG_VERBOSE, F("MQTT connecting")); - mClient.connect(); - mEnReconnect = true; - } - - void onWifiDisconnect(const WiFiEventStationModeDisconnected& event) { - mEnReconnect = false; - } - - #else - void onWiFiEvent(WiFiEvent_t event) { - switch(event) { - case SYSTEM_EVENT_STA_GOT_IP: - DPRINTLN(DBG_VERBOSE, F("MQTT connecting")); - mClient.connect(); - mEnReconnect = true; - break; - - case SYSTEM_EVENT_STA_DISCONNECTED: - mEnReconnect = false; - break; - - default: - break; + void setPowerLimitAck(Inverter<> *iv) { + if (NULL != iv) { + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]); + publish(mSubTopic, "true", true); } } - #endif + private: void onConnect(bool sessionPreset) { DPRINTLN(DBG_INFO, F("MQTT connected")); - mEnReconnect = true; - publish("version", mVersion, true); - publish("device", mDevName, true); + publish(subtopics[MQTT_VERSION], mVersion, true); + publish(subtopics[MQTT_DEVICE], mDevName, true); + publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true); tickerMinute(); - publish(mLwtTopic, mLwtOnline, true, false); + publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false); - subscribe("ctrl/#"); - subscribe("setup/#"); - //subscribe("status/#"); + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + snprintf(mVal, 20, "ctrl/limit/%d", i); + subscribe(mVal); + snprintf(mVal, 20, "ctrl/restart/%d", i); + subscribe(mVal); + } + subscribe(subscr[MQTT_SUBS_SET_TIME]); } void onDisconnect(espMqttClientTypes::DisconnectReason reason) { @@ -260,62 +277,165 @@ class PubMqtt { } void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { - DPRINTLN(DBG_INFO, F("MQTT got topic: ") + String(topic)); + if(len == 0) + return; + DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]); + DBGPRINTLN(String(topic)); if(NULL == mSubscriptionCb) return; - char *tpc = new char[strlen(topic) + 1]; - uint8_t cnt = 0; DynamicJsonDocument json(128); JsonObject root = json.to(); - strncpy(tpc, topic, strlen(topic) + 1); + bool limitAbs = false; if(len > 0) { char *pyld = new char[len + 1]; strncpy(pyld, (const char*)payload, len); pyld[len] = '\0'; - root["val"] = atoi(pyld); + root[F("val")] = atoi(pyld); + if(pyld[len-1] == 'W') + limitAbs = true; delete[] pyld; } - char *p = strtok(tpc, "/"); - p = strtok(NULL, "/"); // remove mCfgMqtt->topic - while(NULL != p) { - if(0 == cnt) { - if(0 == strncmp(p, "ctrl", 4)) { - if(NULL != (p = strtok(NULL, "/"))) { - root[F("path")] = F("ctrl"); - root[F("cmd")] = p; - } - } else if(0 == strncmp(p, "setup", 5)) { - if(NULL != (p = strtok(NULL, "/"))) { - root[F("path")] = F("setup"); - root[F("cmd")] = p; - } - } else if(0 == strncmp(p, "status", 6)) { - if(NULL != (p = strtok(NULL, "/"))) { - root[F("path")] = F("status"); - root[F("cmd")] = p; - } + const char *p = topic; + uint8_t pos = 0; + uint8_t elm = 0; + char tmp[30]; + + while(1) { + if(('/' == p[pos]) || ('\0' == p[pos])) { + strncpy(tmp, p, pos); + tmp[pos] = '\0'; + switch(elm++) { + case 1: root[F("path")] = String(tmp); break; + case 2: + if(strncmp("limit", tmp, 5) == 0) { + if(limitAbs) + root[F("cmd")] = F("limit_nonpersistent_absolute"); + else + root[F("cmd")] = F("limit_nonpersistent_relative"); + } + else + root[F("cmd")] = String(tmp); + break; + case 3: root[F("id")] = atoi(tmp); break; + default: break; } + if('\0' == p[pos]) + break; + p = p + pos + 1; + pos = 0; } - else if(1 == cnt) { - root[F("id")] = atoi(p); - } - p = strtok(NULL, "/"); - cnt++; + pos++; } - delete[] tpc; /*char out[128]; serializeJson(root, out, 128); DPRINTLN(DBG_INFO, "json: " + String(out));*/ - if(NULL != mSubscriptionCb) - (mSubscriptionCb)(root); + (mSubscriptionCb)(root); mRxCnt++; } + void discoveryConfigLoop(void) { + char topic[64], name[32], uniq_id[32], buf[350]; + DynamicJsonDocument doc(256); + + uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC}; + const char* unitTotal[4] = {"W", "kWh", "Wh", "W"}; + + String node_id = String(mDevName) + "_TOTAL"; + bool total = (mDiscovery.lastIvId == MAX_NUM_INVERTERS); + + Inverter<> *iv = mSys->getInverterByPos(mDiscovery.lastIvId); + record_t<> *rec = NULL; + if (NULL != iv) { + rec = iv->getRecordStruct(RealTimeRunData_Debug); + if(0 == mDiscovery.sub) + mDiscovery.foundIvCnt++; + } + + if ((NULL != iv) || total) { + if (!total) { + doc[F("name")] = iv->config->name; + doc[F("ids")] = String(iv->config->serial.u64, HEX); + doc[F("mdl")] = iv->config->name; + } + else { + doc[F("name")] = node_id; + doc[F("ids")] = node_id; + doc[F("mdl")] = node_id; + } + + doc[F("cu")] = F("http://") + String(WiFi.localIP().toString()); + doc[F("mf")] = F("Hoymiles"); + JsonObject deviceObj = doc.as(); // deviceObj is only pointer!? + + const char *devCls, *stateCls; + if (!total) { + if (rec->assign[mDiscovery.sub].ch == CH0) + snprintf(name, 32, "%s %s", iv->config->name, iv->getFieldName(mDiscovery.sub, rec)); + else + snprintf(name, 32, "%s CH%d %s", iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); + snprintf(topic, 64, "/ch%d/%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); + snprintf(uniq_id, 32, "ch%d_%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); + + devCls = getFieldDeviceClass(rec->assign[mDiscovery.sub].fieldId); + stateCls = getFieldStateClass(rec->assign[mDiscovery.sub].fieldId); + } + + else { // total values + snprintf(name, 32, "Total %s", fields[fldTotal[mDiscovery.sub]]); + snprintf(topic, 64, "/%s", fields[fldTotal[mDiscovery.sub]]); + snprintf(uniq_id, 32, "total_%s", fields[fldTotal[mDiscovery.sub]]); + devCls = getFieldDeviceClass(fldTotal[mDiscovery.sub]); + stateCls = getFieldStateClass(fldTotal[mDiscovery.sub]); + } + + DynamicJsonDocument doc2(512); + doc2[F("name")] = name; + doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic); + doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub])); + doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id; + doc2[F("dev")] = deviceObj; + if (!(String(stateCls) == String("total_increasing"))) + doc2[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!? + if (devCls != NULL) + doc2[F("dev_cla")] = String(devCls); + if (stateCls != NULL) + doc2[F("stat_cla")] = String(stateCls); + + if (!total) + snprintf(topic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec)); + else // total values + snprintf(topic, 64, "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(), fields[fldTotal[mDiscovery.sub]]); + size_t size = measureJson(doc2) + 1; + memset(buf, 0, size); + serializeJson(doc2, buf, size); + publish(topic, buf, true, false); + + if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) { + mDiscovery.sub = 0; + checkDiscoveryEnd(); + } + } else { + mDiscovery.sub = 0; + checkDiscoveryEnd(); + } + + yield(); + } + + void checkDiscoveryEnd(void) { + if(++mDiscovery.lastIvId == MAX_NUM_INVERTERS) { + // check if only one inverter was found, then don't create 'total' sensor + if(mDiscovery.foundIvCnt == 1) + mDiscovery.running = false; + } else if(mDiscovery.lastIvId == (MAX_NUM_INVERTERS + 1)) + mDiscovery.running = false; + } + const char *getFieldDeviceClass(uint8_t fieldId) { uint8_t pos = 0; for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { @@ -334,150 +454,189 @@ class PubMqtt { return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : stateClasses[deviceFieldAssignment[pos].stateClsId]; } - bool processIvStatus() { - // returns true if all inverters are available - bool allAvail = true; - bool first = true; + bool processIvStatus() { + // returns true if any inverter is available + bool allAvail = true; // shows if all enabled inverters are available + bool anyAvail = false; // shows if at least one enabled inverter is available bool changed = false; - char topic[7 + MQTT_TOPIC_LEN], val[40]; Inverter<> *iv; record_t<> *rec; - bool totalComplete = true; for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { iv = mSys->getInverterByPos(id); if (NULL == iv) continue; // skip to next inverter + if (!iv->config->enabled) + continue; // skip to next inverter rec = iv->getRecordStruct(RealTimeRunData_Debug); - if(first) - mIvAvail = false; - first = false; // inverter status - uint8_t status = MQTT_STATUS_AVAIL_PROD; - if ((!iv->isAvailable(*mUtcTimestamp, rec)) || (!iv->config->enabled)) { - status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; - if(iv->config->enabled) { // only change all-avail if inverter is enabled! - totalComplete = false; - allAvail = false; - } - } - else if (!iv->isProducing(*mUtcTimestamp, rec)) { - mIvAvail = true; - if (MQTT_STATUS_AVAIL_PROD == status) - status = MQTT_STATUS_AVAIL_NOT_PROD; + uint8_t status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; + if (iv->isAvailable(*mUtcTimestamp)) { + anyAvail = true; + status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD; } - else - mIvAvail = true; + else // inverter is enabled but not available + allAvail = false; if(mLastIvState[id] != status) { + // if status changed from producing to not producing send last data immediately + if (MQTT_STATUS_AVAIL_PROD == mLastIvState[id]) + sendData(iv, RealTimeRunData_Debug); + mLastIvState[id] = status; changed = true; - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); - snprintf(val, 40, "%d", status); - publish(topic, val, true); + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); + snprintf(mVal, 40, "%d", status); + publish(mSubTopic, mVal, true); - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name); - snprintf(val, 40, "%d", iv->getLastTs(rec)); - publish(topic, val, true); + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name); + snprintf(mVal, 40, "%d", iv->getLastTs(rec)); + publish(mSubTopic, mVal, true); } } if(changed) { - snprintf(val, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((mIvAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE))); - publish("status", val, true); + snprintf(mVal, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE))); + publish("status", mVal, true); } - return totalComplete; + return anyAvail; } - void sendIvData(void) { + void sendAlarmData() { + if(mAlarmList.empty()) + return; + Inverter<> *iv = mSys->getInverterByPos(0, false); + while(!mAlarmList.empty()) { + alarm_t alarm = mAlarmList.front(); + publish(subtopics[MQTT_ALARM], iv->getAlarmStr(alarm.code).c_str()); + publish(subtopics[MQTT_ALARM_START], String(alarm.start).c_str()); + publish(subtopics[MQTT_ALARM_END], String(alarm.end).c_str()); + mAlarmList.pop(); + } + } + + void sendData(Inverter<> *iv, uint8_t curInfoCmd) { + record_t<> *rec = iv->getRecordStruct(curInfoCmd); + + if (iv->getLastTs(rec) > 0) { + for (uint8_t i = 0; i < rec->length; i++) { + bool retained = false; + if (curInfoCmd == RealTimeRunData_Debug) { + switch (rec->assign[i].fieldId) { + case FLD_YT: + case FLD_YD: + if ((rec->assign[i].ch == CH0) && (!iv->isProducing(*mUtcTimestamp))) // avoids returns to 0 on restart + continue; + retained = true; + break; + } + } + + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]); + snprintf(mVal, 40, "%g", ah::round3(iv->getValue(i, rec))); + publish(mSubTopic, mVal, retained); + + yield(); + } + } + } + + void sendIvData() { + bool anyAvail = processIvStatus(); + if (mLastAnyAvail != anyAvail) + mSendList.push(RealTimeRunData_Debug); // makes shure that total values are calculated + if(mSendList.empty()) return; - char topic[7 + MQTT_TOPIC_LEN], val[40]; float total[4]; - bool sendTotal = false; + bool RTRDataHasBeenSent = false; while(!mSendList.empty()) { memset(total, 0, sizeof(float) * 4); - for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { - Inverter<> *iv = mSys->getInverterByPos(id); - if (NULL == iv) - continue; // skip to next inverter - - record_t<> *rec = iv->getRecordStruct(mSendList.front()); - - // data - if(iv->isAvailable(*mUtcTimestamp, rec)) { - for (uint8_t i = 0; i < rec->length; i++) { - bool retained = false; - if (mSendList.front() == RealTimeRunData_Debug) { - switch (rec->assign[i].fieldId) { - case FLD_YT: - case FLD_YD: - retained = true; - break; - } - } - - snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]); - snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec))); - publish(topic, val, retained); - - // calculate total values for RealTimeRunData_Debug - if (mSendList.front() == RealTimeRunData_Debug) { - if (CH0 == rec->assign[i].ch) { - switch (rec->assign[i].fieldId) { - case FLD_PAC: - total[0] += iv->getValue(i, rec); - break; - case FLD_YT: - total[1] += iv->getValue(i, rec); - break; - case FLD_YD: - total[2] += iv->getValue(i, rec); - break; - case FLD_PDC: - total[3] += iv->getValue(i, rec); - break; + uint8_t curInfoCmd = mSendList.front(); + + if ((curInfoCmd != RealTimeRunData_Debug) || !RTRDataHasBeenSent) { // send RTR Data only once + bool sendTotals = (curInfoCmd == RealTimeRunData_Debug); + + for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { + Inverter<> *iv = mSys->getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter + if (!iv->config->enabled) + continue; // skip to next inverter + + // send RTR Data only if status is available + if ((curInfoCmd != RealTimeRunData_Debug) || (MQTT_STATUS_NOT_AVAIL_NOT_PROD != mLastIvState[id])) + sendData(iv, curInfoCmd); + + // calculate total values for RealTimeRunData_Debug + if (sendTotals) { + record_t<> *rec = iv->getRecordStruct(curInfoCmd); + + sendTotals &= (iv->getLastTs(rec) > 0); + if (sendTotals) { + for (uint8_t i = 0; i < rec->length; i++) { + if (CH0 == rec->assign[i].ch) { + switch (rec->assign[i].fieldId) { + case FLD_PAC: + total[0] += iv->getValue(i, rec); + break; + case FLD_YT: + total[1] += iv->getValue(i, rec); + break; + case FLD_YD: + total[2] += iv->getValue(i, rec); + break; + case FLD_PDC: + total[3] += iv->getValue(i, rec); + break; + } } } - sendTotal = true; } - yield(); } + yield(); } - } - mSendList.pop(); // remove from list once all inverters were processed - - if ((true == sendTotal) && processIvStatus()) { - uint8_t fieldId; - for (uint8_t i = 0; i < 4; i++) { - switch (i) { - default: - case 0: - fieldId = FLD_PAC; - break; - case 1: - fieldId = FLD_YT; - break; - case 2: - fieldId = FLD_YD; - break; - case 3: - fieldId = FLD_PDC; - break; + if (sendTotals) { + uint8_t fieldId; + for (uint8_t i = 0; i < 4; i++) { + bool retained = true; + switch (i) { + default: + case 0: + fieldId = FLD_PAC; + retained = false; + break; + case 1: + fieldId = FLD_YT; + break; + case 2: + fieldId = FLD_YD; + break; + case 3: + fieldId = FLD_PDC; + retained = false; + break; + } + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]); + snprintf(mVal, 40, "%g", ah::round3(total[i])); + publish(mSubTopic, mVal, retained); } - snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]); - snprintf(val, 40, "%g", ah::round3(total[i])); - publish(topic, val, true); + RTRDataHasBeenSent = true; + yield(); } } + + mSendList.pop(); // remove from list once all inverters were processed } + + mLastAnyAvail = anyAvail; } espMqttClient mClient; @@ -490,16 +649,21 @@ class PubMqtt { uint32_t *mUtcTimestamp; uint32_t mRxCnt, mTxCnt; std::queue mSendList; - bool mEnReconnect; + std::queue mAlarmList; subscriptionCb mSubscriptionCb; - bool mIvAvail; // shows if at least one inverter is available + bool mLastAnyAvail; uint8_t mLastIvState[MAX_NUM_INVERTERS]; + uint16_t mIntervalTimeout; // last will topic and payload must be available trough lifetime of 'espMqttClient' char mLwtTopic[MQTT_TOPIC_LEN+5]; - const char* mLwtOnline = "connected"; - const char* mLwtOffline = "not connected"; const char *mDevName, *mVersion; + char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT + // global buffer for mqtt topic. Used when publishing mqtt messages. + char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1]; + char mSubTopic[32 + MAX_NAME_LENGTH + 1]; + char mVal[40]; + discovery_t mDiscovery; }; #endif /*__PUB_MQTT_H__*/ diff --git a/src/publisher/pubMqttDefs.h b/src/publisher/pubMqttDefs.h new file mode 100644 index 000000000..088023b7a --- /dev/null +++ b/src/publisher/pubMqttDefs.h @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __PUB_MQTT_DEFS_H__ +#define __PUB_MQTT_DEFS_H__ + +#include + +enum { + STR_TRUE, + STR_FALSE +}; + +const char* const dict[] PROGMEM = { + "true", + "false" +}; + +enum { + MQTT_STR_LWT_CONN, + MQTT_STR_LWT_NOT_CONN, + MQTT_STR_AVAILABLE, + MQTT_STR_LAST_SUCCESS, + MQTT_STR_TOTAL, + MQTT_STR_GOT_TOPIC +}; + +const char* const mqttStr[] PROGMEM = { + "connected", + "not connected", + "available", + "last_success", + "total", + "MQTT got topic: " +}; + + +enum { + MQTT_UPTIME = 0, + MQTT_RSSI, + MQTT_FREE_HEAP, + MQTT_HEAP_FRAG, + MQTT_SUNRISE, + MQTT_SUNSET, + MQTT_COMM_START, + MQTT_COMM_STOP, + MQTT_DIS_NIGHT_COMM, + MQTT_COMM_DISABLED, + MQTT_COMM_DIS_TS, + MQTT_VERSION, + MQTT_DEVICE, + MQTT_IP_ADDR, + MQTT_STATUS, + MQTT_ALARM, + MQTT_ALARM_START, + MQTT_ALARM_END, + MQTT_LWT_ONLINE, + MQTT_LWT_OFFLINE, + MQTT_ACK_PWR_LMT +}; + +const char* const subtopics[] PROGMEM = { + "uptime", + "wifi_rssi", + "free_heap", + "heap_frag", + "sunrise", + "sunset", + "comm_start", + "comm_stop", + "dis_night_comm", + "comm_disabled", + "comm_dis_ts", + "version", + "device", + "ip_addr", + "status", + "alarm", + "alarm_start", + "alarm_end", + "connected", + "not_connected", + "ack_pwr_limit" +}; + +enum { + MQTT_SUBS_SET_TIME +}; + +const char* const subscr[] PROGMEM = { + "setup/set_time" +}; + +#endif /*__PUB_MQTT_DEFS_H__*/ diff --git a/src/publisher/pubSerial.h b/src/publisher/pubSerial.h index d1cc55aa4..522a227d3 100644 --- a/src/publisher/pubSerial.h +++ b/src/publisher/pubSerial.h @@ -28,8 +28,8 @@ class PubSerial { Inverter<> *iv = mSys->getInverterByPos(id); if (NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (iv->isAvailable(*mUtcTimestamp, rec)) { - DPRINTLN(DBG_INFO, F("Inverter: ") + String(id)); + if (iv->isAvailable(*mUtcTimestamp)) { + DPRINTLN(DBG_INFO, "Iv: " + String(id)); for (uint8_t i = 0; i < rec->length; i++) { if (0.0f != iv->getValue(i, rec)) { snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec)); diff --git a/src/utils/ahoyTimer.h b/src/utils/ahoyTimer.h deleted file mode 100644 index 5c960a346..000000000 --- a/src/utils/ahoyTimer.h +++ /dev/null @@ -1,27 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __AHOY_TIMER_H__ -#define __AHOY_TIMER_H__ - -#include - -namespace ah { - inline bool checkTicker(uint32_t *ticker, uint32_t interval) { - uint32_t mil = millis(); - if(mil >= *ticker) { - *ticker = mil + interval; - return true; - } - else if(mil < (*ticker - interval)) { - *ticker = mil + interval; - return true; - } - - return false; - } -} - -#endif /*__AHOY_TIMER_H__*/ diff --git a/src/utils/dbg.h b/src/utils/dbg.h index 17b334c95..4716d7aeb 100644 --- a/src/utils/dbg.h +++ b/src/utils/dbg.h @@ -1,11 +1,11 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 +// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- #ifndef __DBG_H__ #define __DBG_H__ -#if defined(ESP32) && defined(F) +#if defined(F) && defined(ESP32) #undef F #define F(sl) (sl) #endif @@ -19,6 +19,8 @@ #define DBG_DEBUG 4 #define DBG_VERBOSE 5 +//#define LOG_MAX_MSG_LEN 100 + //----------------------------------------------------------------------------- // globally used level @@ -58,7 +60,12 @@ mCb(String(b, HEX)); } } - inline void DHEX(uint16_t b) { + + inline void DBGHEXLN(uint8_t b) { + DHEX(b); + DBGPRINT(F("\r\n")); + } + /*inline void DHEX(uint16_t b) { if( b<0x10 ) DSERIAL.print(F("000")); else if( b<0x100 ) DSERIAL.print(F("00")); else if( b<0x1000 ) DSERIAL.print(F("0")); @@ -89,7 +96,7 @@ else if( b<0x10000000 ) mCb(F("0")); mCb(String(b, HEX)); } - } + }*/ #endif #endif @@ -144,6 +151,10 @@ }\ }) +#define DPRINT_IVID(level, id) ({\ + DPRINT(level, F("(#")); DBGPRINT(String(id)); DBGPRINT(F(") "));\ +}) + #define DPRINTLN(level, str) ({\ switch(level) {\ case DBG_ERROR: PERRLN(str); break; \ @@ -154,4 +165,53 @@ }\ }) + +/*class ahoyLog { + public: + ahoyLog() {} + + inline void logMsg(uint8_t lvl, bool newLine, const char *fmt, va_list args) { + snprintf(mLogBuf, LOG_MAX_MSG_LEN, fmt, args); + DSERIAL.print(mLogBuf); + if(NULL != mCb) + mCb(mLogBuf); + if(newLine) { + DSERIAL.print(F("\r\n")); + if(NULL != mCb) + mCb(F("\r\n")); + } + } + + inline void logError(const char *fmt, ...) { + #if DEBUG_LEVEL >= DBG_ERROR + va_list args; + va_start(args, fmt); + logMsg(DBG_ERROR, true, fmt, args); + va_end(args); + #endif + } + + inline void logWarn(const char *fmt, ...) { + #if DEBUG_LEVEL >= DBG_WARN + va_list args; + va_start(args, fmt); + logMsg(DBG_ERROR, true, fmt, args); + va_end(args); + #endif + } + + inline void logInfo(const char *fmt, ...) { + #if DEBUG_LEVEL >= DBG_INFO + va_list args; + va_start(args, fmt); + logMsg(DBG_ERROR, true, fmt, args); + va_end(args); + #endif + } + + private: + char mLogBuf[LOG_MAX_MSG_LEN]; + DBG_CB mCb = NULL; +};*/ + #endif /*__DBG_H__*/ diff --git a/src/utils/handler.h b/src/utils/handler.h deleted file mode 100644 index 51d64c0d2..000000000 --- a/src/utils/handler.h +++ /dev/null @@ -1,33 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de -// Lukas Pusch, lukas@lpusch.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#ifndef __HANDLER_H__ -#define __HANDLER_H__ - -#include -#include -#include - -template -class Handler { - public: - Handler() {} - - void addListener(TYPE f) { - mList.push_back(f); - } - - /*virtual void notify(void) { - for(typename std::list::iterator it = mList.begin(); it != mList.end(); ++it) { - (*it)(); - } - }*/ - - protected: - std::list mList; -}; - -#endif /*__HANDLER_H__*/ diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp index 6854a7e8f..97d7418bf 100644 --- a/src/utils/helper.cpp +++ b/src/utils/helper.cpp @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://github.com/lumpapu/ahoy +// 2023 Ahoy, https://github.com/lumpapu/ahoy // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -7,15 +7,15 @@ namespace ah { void ip2Arr(uint8_t ip[], const char *ipStr) { - char tmp[16]; + uint8_t p = 1; memset(ip, 0, 4); - memset(tmp, 0, 16); - snprintf(tmp, 16, ipStr); - char *p = strtok(tmp, "."); - uint8_t i = 0; - while(NULL != p) { - ip[i++] = atoi(p); - p = strtok(NULL, "."); + for(uint8_t i = 0; i < 16; i++) { + if(ipStr[i] == 0) + return; + if(0 == i) + ip[0] = atoi(ipStr); + else if(ipStr[i] == '.') + ip[p++] = atoi(&ipStr[i+1]); } } @@ -40,6 +40,15 @@ namespace ah { return String(str); } + String getTimeStr(time_t t) { + char str[9]; + if(0 == t) + sprintf(str, "n/a"); + else + sprintf(str, "%02d:%02d:%02d", hour(t), minute(t), second(t)); + return String(str); + } + uint64_t Serial2u64(const char *val) { char tmp[3]; uint64_t ret = 0ULL; diff --git a/src/utils/helper.h b/src/utils/helper.h index 7e9086246..efd056d23 100644 --- a/src/utils/helper.h +++ b/src/utils/helper.h @@ -11,7 +11,11 @@ #include #include #include -#include +#include + +static TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time +static TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time +static Timezone gTimezone(CEST, CET); #define CHECK_MASK(a,b) ((a & b) == b) @@ -21,6 +25,7 @@ namespace ah { void ip2Char(uint8_t ip[], char *str); double round3(double value); String getDateTimeStr(time_t t); + String getTimeStr(time_t t); uint64_t Serial2u64(const char *val); } diff --git a/src/utils/llist.h b/src/utils/llist.h deleted file mode 100644 index 69750f19e..000000000 --- a/src/utils/llist.h +++ /dev/null @@ -1,110 +0,0 @@ -//----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de -// Lukas Pusch, lukas@lpusch.de -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- -#ifndef __LIST_H__ -#define __LIST_H__ - -template -struct node_s { - typedef T dT; - node_s *pre; - node_s *nxt; - uint8_t id; - dT d; - node_s() : pre(NULL), nxt(NULL), d() {} - node_s(Args... args) : id(0), pre(NULL), nxt(NULL), d(args...) {} -}; - -template -class llist { - typedef node_s elmType; - typedef T dataType; - public: - llist() : root(mPool) { - root = NULL; - elmType *p = mPool; - for(uint32_t i = 0; i < MAX_NUM; i++) { - p->id = i; - p++; - } - mFill = mMax = 0; - } - - elmType *add(Args... args) { - elmType *p = root, *t; - if(NULL == (t = getFreeNode())) - return NULL; - if(++mFill > mMax) - mMax = mFill; - - if(NULL == root) { - p = root = t; - p->pre = p; - p->nxt = p; - } - else { - p = root->pre; - t->pre = p; - p->nxt->pre = t; - t->nxt = p->nxt; - p->nxt = t; - } - t->d = dataType(args...); - return p; - } - - elmType *getFront() { - return root; - } - - elmType *get(elmType *p) { - p = p->nxt; - return (p == root) ? NULL : p; - } - - elmType *rem(elmType *p) { - if(NULL == p) - return NULL; - elmType *t = p->nxt; - p->nxt->pre = p->pre; - p->pre->nxt = p->nxt; - if((root == p) && (p->nxt == p)) - root = NULL; - else - root = p->nxt; - p->nxt = NULL; - p->pre = NULL; - p = NULL; - mFill--; - return (NULL == root) ? NULL : ((t == root) ? NULL : t); - } - - uint16_t getFill(void) { - return mFill; - } - - uint16_t getMaxFill(void) { - return mMax; - } - - protected: - elmType *root; - - private: - elmType *getFreeNode(void) { - elmType *n = mPool; - for(uint32_t i = 0; i < MAX_NUM; i++) { - if(NULL == n->nxt) - return n; - n++; - } - return NULL; - } - - elmType mPool[MAX_NUM]; - uint16_t mFill, mMax; -}; - -#endif /*__LIST_H__*/ diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h index 36dcdaaef..ca250a3e3 100644 --- a/src/utils/scheduler.h +++ b/src/utils/scheduler.h @@ -1,5 +1,5 @@ //----------------------------------------------------------------------------- -// 2022 Ahoy, https://ahoydtu.de +// 2023 Ahoy, https://ahoydtu.de // Lukas Pusch, lukas@lpusch.de // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- @@ -20,8 +20,9 @@ namespace ah { uint32_t timeout; uint32_t reload; bool isTimestamp; - sP() : c(NULL), timeout(0), reload(0), isTimestamp(false) {} - sP(scdCb a, uint32_t tmt, uint32_t rl, bool its) : c(a), timeout(tmt), reload(rl), isTimestamp(its) {} + char name[6]; + sP() : c(NULL), timeout(0), reload(0), isTimestamp(false), name("\n") {} + sP(scdCb a, uint32_t tmt, uint32_t rl, bool its) : c(a), timeout(tmt), reload(rl), isTimestamp(its), name("\n") {} }; #define MAX_NUM_TICKER 30 @@ -35,8 +36,7 @@ namespace ah { mTimestamp = 0; mMax = 0; mPrevMillis = millis(); - for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) - mTickerInUse[i] = false; + resetTicker(); } void loop(void) { @@ -65,15 +65,15 @@ namespace ah { } - void once(scdCb c, uint32_t timeout) { addTicker(c, timeout, 0, false); } - void onceAt(scdCb c, uint32_t timestamp) { addTicker(c, timestamp, 0, true); } - uint8_t every(scdCb c, uint32_t interval){ return addTicker(c, interval, interval, false); } + void once(scdCb c, uint32_t timeout, const char *name) { addTicker(c, timeout, 0, false, name); } + void onceAt(scdCb c, uint32_t timestamp, const char *name) { addTicker(c, timestamp, 0, true, name); } + uint8_t every(scdCb c, uint32_t interval, const char *name){ return addTicker(c, interval, interval, false, name); } - void everySec(scdCb c) { every(c, SCD_SEC); } - void everyMin(scdCb c) { every(c, SCD_MIN); } - void everyHour(scdCb c) { every(c, SCD_HOUR); } - void every12h(scdCb c) { every(c, SCD_12H); } - void everyDay(scdCb c) { every(c, SCD_DAY); } + void everySec(scdCb c, const char *name) { every(c, SCD_SEC, name); } + void everyMin(scdCb c, const char *name) { every(c, SCD_MIN, name); } + void everyHour(scdCb c, const char *name) { every(c, SCD_HOUR, name); } + void every12h(scdCb c, const char *name) { every(c, SCD_12H, name); } + void everyDay(scdCb c, const char *name) { every(c, SCD_DAY, name); } virtual void setTimestamp(uint32_t ts) { mTimestamp = ts; @@ -94,15 +94,32 @@ namespace ah { return mTimestamp; } + inline void resetTicker(void) { + for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) + mTickerInUse[i] = false; + } + void getStat(uint8_t *max) { *max = mMax; } + void printSchedulers() { + for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) { + if (mTickerInUse[i]) { + DPRINT(DBG_INFO, String(mTicker[i].name)); + DBGPRINT(", tmt: "); + DBGPRINT(String(mTicker[i].timeout)); + DBGPRINT(", rel: "); + DBGPRINTLN(String(mTicker[i].reload)); + } + } + } + protected: uint32_t mTimestamp; private: - inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp) { + inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) { for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) { if (!mTickerInUse[i]) { mTickerInUse[i] = true; @@ -110,6 +127,8 @@ namespace ah { mTicker[i].timeout = timeout; mTicker[i].reload = reload; mTicker[i].isTimestamp = isTimestamp; + memset(mTicker[i].name, 0, 6); + strncpy(mTicker[i].name, name, (strlen(name) < 6) ? strlen(name) : 5); if(mMax == i) mMax = i + 1; return i; @@ -129,6 +148,7 @@ namespace ah { mTickerInUse[i] = false; else mTicker[i].timeout = mTicker[i].reload; + //DPRINTLN(DBG_INFO, "checkTick " + String(i) + " reload: " + String(mTicker[i].reload) + ", timeout: " + String(mTicker[i].timeout)); (mTicker[i].c)(); yield(); } diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 8ad55e26d..b224f239d 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -1,25 +1,40 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +//----------------------------------------------------------------------------- + #ifndef __WEB_API_H__ #define __WEB_API_H__ #include "../utils/dbg.h" #ifdef ESP32 - #include "AsyncTCP.h" +#include "AsyncTCP.h" #else - #include "ESPAsyncTCP.h" +#include "ESPAsyncTCP.h" #endif -#include "ESPAsyncWebServer.h" -#include "AsyncJson.h" +#include "../appInterface.h" #include "../hm/hmSystem.h" #include "../utils/helper.h" +#include "AsyncJson.h" +#include "ESPAsyncWebServer.h" -#include "../appInterface.h" +#if defined(F) && defined(ESP32) +#undef F +#define F(sl) (sl) +#endif + +const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; +const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR}; -template +template class RestApi { public: RestApi() { mTimezoneOffset = 0; - mFreeHeap = 0; + mHeapFree = 0; + mHeapFreeBlk = 0; + mHeapFrag = 0; + nr = 0; } void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { @@ -43,7 +58,7 @@ class RestApi { serializeJson(obj, out, 128); DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/ DynamicJsonDocument json(128); - JsonObject dummy = json.to(); + JsonObject dummy = json.as(); if(obj[F("path")] == "ctrl") setCtrl(obj, dummy); else if(obj[F("path")] == "setup") @@ -52,21 +67,25 @@ class RestApi { private: void onApi(AsyncWebServerRequest *request) { - mFreeHeap = ESP.getFreeHeap(); + mHeapFree = ESP.getFreeHeap(); + #ifndef ESP32 + mHeapFreeBlk = ESP.getMaxFreeBlockSize(); + mHeapFrag = ESP.getHeapFragmentation(); + #endif - AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000); JsonObject root = response->getRoot(); String path = request->url().substring(5); if(path == "html/system") getHtmlSystem(root); else if(path == "html/logout") getHtmlLogout(root); + else if(path == "html/reboot") getHtmlReboot(root); else if(path == "html/save") getHtmlSave(root); else if(path == "system") getSysInfo(root); else if(path == "generic") getGeneric(root); else if(path == "reboot") getReboot(root); else if(path == "statistics") getStatistics(root); else if(path == "inverter/list") getInverterList(root); - else if(path == "menu") getMenu(root); else if(path == "index") getIndex(root); else if(path == "setup") getSetup(root); else if(path == "setup/networks") getNetworks(root); @@ -75,9 +94,14 @@ class RestApi { else if(path == "record/alarm") getRecord(root, AlarmData); else if(path == "record/config") getRecord(root, SystemConfigPara); else if(path == "record/live") getRecord(root, RealTimeRunData_Debug); - else - getNotFound(root, F("http://") + request->host() + F("/api/")); + else { + if(path.substring(0, 12) == "inverter/id/") + getInverter(root, request->url().substring(17).toInt()); + else + getNotFound(root, F("http://") + request->host() + F("/api/")); + } + //DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage())); response->addHeader("Access-Control-Allow-Origin", "*"); response->addHeader("Access-Control-Allow-Headers", "content-type"); response->setLength(); @@ -134,24 +158,43 @@ class RestApi { ep[F("record/config")] = url + F("record/config"); ep[F("record/live")] = url + F("record/live"); } + + void onDwnldSetup(AsyncWebServerRequest *request) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); - JsonObject root = response->getRoot(); + AsyncWebServerResponse *response; - getSetup(root); + File fp = LittleFS.open("/settings.json", "r"); + if(!fp) { + DPRINTLN(DBG_ERROR, F("failed to load settings")); + response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}"); + } + else { + String tmp = fp.readString(); + int i = 0; + // remove all passwords + while (i != -1) { + i = tmp.indexOf("\"pwd\":", i); + if(-1 != i) { + i+=7; + tmp.remove(i, tmp.indexOf("\"", i)-i); + } + } + response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); + } - response->setLength(); response->addHeader("Content-Type", "application/octet-stream"); response->addHeader("Content-Description", "File Transfer"); response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json"); request->send(response); + fp.close(); } void getGeneric(JsonObject obj) { - obj[F("version")] = String(mApp->getVersion()); - obj[F("build")] = String(AUTO_GIT_HASH); - obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); - obj[F("ts_uptime")] = mApp->getUptime(); + obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); + obj[F("ts_uptime")] = mApp->getUptime(); + obj[F("menu_prot")] = mApp->getProtection(); + obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); + obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0); #if defined(ESP32) obj[F("esp_type")] = F("ESP32"); @@ -163,15 +206,16 @@ class RestApi { void getSysInfo(JsonObject obj) { obj[F("ssid")] = mConfig->sys.stationSsid; obj[F("device_name")] = mConfig->sys.deviceName; + obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; obj[F("mac")] = WiFi.macAddress(); - obj[F("hostname")] = WiFi.getHostname(); + obj[F("hostname")] = mConfig->sys.deviceName; obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); obj[F("prot_mask")] = mConfig->sys.protectionMask; obj[F("sdk")] = ESP.getSdkVersion(); obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); - obj[F("heap_free")] = mFreeHeap; + obj[F("heap_free")] = mHeapFree; obj[F("sketch_total")] = ESP.getFreeSketchSpace(); obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb getGeneric(obj); @@ -196,8 +240,8 @@ class RestApi { //obj[F("chip_cores")] = F("n/a"); obj[F("core_version")] = ESP.getCoreVersion(); obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb - obj[F("heap_frag")] = ESP.getHeapFragmentation(); - obj[F("max_free_blk")] = ESP.getMaxFreeBlockSize(); + obj[F("heap_frag")] = mHeapFrag; + obj[F("max_free_blk")] = mHeapFreeBlk; obj[F("reboot_reason")] = ESP.getResetReason(); #endif //obj[F("littlefs_total")] = LittleFS.totalBytes(); @@ -209,31 +253,32 @@ class RestApi { } void getHtmlSystem(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); getSysInfo(obj.createNestedObject(F("system"))); getGeneric(obj.createNestedObject(F("generic"))); obj[F("html")] = F("Factory Reset

Reboot"); - } void getHtmlLogout(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); getGeneric(obj.createNestedObject(F("generic"))); obj[F("refresh")] = 3; obj[F("refresh_url")] = "/"; obj[F("html")] = F("succesfully logged out"); } + void getHtmlReboot(JsonObject obj) { + getGeneric(obj.createNestedObject(F("generic"))); + obj[F("refresh")] = 20; + obj[F("refresh_url")] = "/"; + obj[F("html")] = F("rebooting ..."); + } + void getHtmlSave(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); getGeneric(obj.createNestedObject(F("generic"))); - obj[F("refresh")] = 2; - obj[F("refresh_url")] = "/setup"; - obj[F("html")] = F("settings succesfully save"); + obj["pending"] = (bool)mApp->getSavePending(); + obj["success"] = (bool)mApp->getLastSaveSucceed(); } void getReboot(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); getGeneric(obj.createNestedObject(F("generic"))); obj[F("refresh")] = 10; obj[F("refresh_url")] = "/"; @@ -247,6 +292,7 @@ class RestApi { obj[F("rx_fail_answer")] = stat->rxFailNoAnser; obj[F("frame_cnt")] = stat->frmCnt; obj[F("tx_cnt")] = mSys->Radio.mSendCnt; + obj[F("retransmits")] = mSys->Radio.mRetransmits; } void getInverterList(JsonObject obj) { @@ -265,22 +311,63 @@ class RestApi { obj2[F("version")] = String(iv->getFwVersion()); for(uint8_t j = 0; j < iv->channels; j ++) { - obj2[F("ch_max_power")][j] = iv->config->chMaxPwr[j]; - obj2[F("ch_name")][j] = iv->config->chName[j]; + obj2[F("ch_yield_cor")][j] = iv->config->yieldCor[j]; + obj2[F("ch_name")][j] = iv->config->chName[j]; + obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j]; } } } obj[F("interval")] = String(mConfig->nrf.sendInterval); obj[F("retries")] = String(mConfig->nrf.maxRetransPerPyld); obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; + obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; + obj[F("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; + } + + void getInverter(JsonObject obj, uint8_t id) { + Inverter<> *iv = mSys->getInverterByPos(id); + if(NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + obj[F("id")] = id; + obj[F("enabled")] = (bool)iv->config->enabled; + obj[F("name")] = String(iv->config->name); + obj[F("serial")] = String(iv->config->serial.u64, HEX); + obj[F("version")] = String(iv->getFwVersion()); + obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); + obj[F("ts_last_success")] = rec->ts; + + JsonArray ch = obj.createNestedArray("ch"); + + // AC + uint8_t pos; + obj[F("ch_name")][0] = "AC"; + JsonArray ch0 = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + pos = (iv->getPosByChFld(CH0, acList[fld], rec)); + ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } + + // DC + for(uint8_t j = 0; j < iv->channels; j ++) { + obj[F("ch_name")][j+1] = iv->config->chName[j]; + obj[F("ch_max_pwr")][j+1] = iv->config->chMaxPwr[j]; + JsonArray cur = ch.createNestedArray(); + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + pos = (iv->getPosByChFld((j+1), dcList[fld], rec)); + cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } + } + } } void getMqtt(JsonObject obj) { - obj[F("broker")] = String(mConfig->mqtt.broker); - obj[F("port")] = String(mConfig->mqtt.port); - obj[F("user")] = String(mConfig->mqtt.user); - obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); - obj[F("topic")] = String(mConfig->mqtt.topic); + obj[F("broker")] = String(mConfig->mqtt.broker); + obj[F("port")] = String(mConfig->mqtt.port); + obj[F("user")] = String(mConfig->mqtt.user); + obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); + obj[F("topic")] = String(mConfig->mqtt.topic); + obj[F("interval")] = String(mConfig->mqtt.interval); } void getNtp(JsonObject obj) { @@ -299,6 +386,9 @@ class RestApi { obj[F("cs")] = mConfig->nrf.pinCs; obj[F("ce")] = mConfig->nrf.pinCe; obj[F("irq")] = mConfig->nrf.pinIrq; + obj[F("sclk")] = mConfig->nrf.pinSclk; + obj[F("mosi")] = mConfig->nrf.pinMosi; + obj[F("miso")] = mConfig->nrf.pinMiso; obj[F("led0")] = mConfig->led.led0; obj[F("led1")] = mConfig->led.led1; } @@ -325,49 +415,22 @@ class RestApi { ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf); } - void getMenu(JsonObject obj) { - uint8_t i = 0; - uint16_t mask = (mApp->getProtection()) ? mConfig->sys.protectionMask : 0; - if(!CHECK_MASK(mask, PROT_MASK_LIVE)) { - obj[F("name")][i] = "Live"; - obj[F("link")][i++] = "/live"; - } - if(!CHECK_MASK(mask, PROT_MASK_SERIAL)) { - obj[F("name")][i] = "Serial / Control"; - obj[F("link")][i++] = "/serial"; - } - if(!CHECK_MASK(mask, PROT_MASK_SETUP)) { - obj[F("name")][i] = "Settings"; - obj[F("link")][i++] = "/setup"; - } - obj[F("name")][i++] = "-"; - obj[F("name")][i] = "REST API"; - obj[F("link")][i] = "/api"; - obj[F("trgt")][i++] = "_blank"; - obj[F("name")][i++] = "-"; - if(!CHECK_MASK(mask, PROT_MASK_UPDATE)) { - obj[F("name")][i] = "Update"; - obj[F("link")][i++] = "/update"; - } - if(!CHECK_MASK(mask, PROT_MASK_SYSTEM)) { - obj[F("name")][i] = "System"; - obj[F("link")][i++] = "/system"; - } - obj[F("name")][i++] = "-"; - obj[F("name")][i] = "Documentation"; - obj[F("link")][i] = "https://ahoydtu.de"; - obj[F("trgt")][i++] = "_blank"; - if((strlen(mConfig->sys.adminPwd) > 0) && !mApp->getProtection()) { - obj[F("name")][i++] = "-"; - obj[F("name")][i] = "Logout"; - obj[F("link")][i++] = "/logout"; - } + void getDisplay(JsonObject obj) { + obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type; + obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; + obj[F("disp_pxshift")] = (bool)mConfig->plugin.display.pxShift; + obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot; + obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; + obj[F("disp_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk; + obj[F("disp_data")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_data; + obj[F("disp_cs")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs; + obj[F("disp_dc")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc; + obj[F("disp_rst")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset; + obj[F("disp_bsy")] = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy; } void getIndex(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); getGeneric(obj.createNestedObject(F("generic"))); - obj[F("ts_now")] = mApp->getTimestamp(); obj[F("ts_sunrise")] = mApp->getSunrise(); obj[F("ts_sunset")] = mApp->getSunset(); @@ -385,8 +448,8 @@ class RestApi { invObj[F("id")] = i; invObj[F("name")] = String(iv->config->name); invObj[F("version")] = String(iv->getFwVersion()); - invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp(), rec); - invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp(), rec); + invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp()); + invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp()); invObj[F("ts_last_success")] = iv->getLastTs(rec); } } @@ -411,13 +474,14 @@ class RestApi { JsonArray info = obj.createNestedArray(F("infos")); if(mApp->getMqttIsConnected()) info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received")); + if(mConfig->mqtt.interval > 0) + info.add(F("MQTT publishes in a fixed interval of ") + String(mConfig->mqtt.interval) + F(" seconds")); } void getSetup(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); getGeneric(obj.createNestedObject(F("generic"))); getSysInfo(obj.createNestedObject(F("system"))); - getInverterList(obj.createNestedObject(F("inverter"))); + //getInverterList(obj.createNestedObject(F("inverter"))); getMqtt(obj.createNestedObject(F("mqtt"))); getNtp(obj.createNestedObject(F("ntp"))); getSun(obj.createNestedObject(F("sun"))); @@ -425,6 +489,7 @@ class RestApi { getRadio(obj.createNestedObject(F("radio"))); getSerial(obj.createNestedObject(F("serial"))); getStaticIp(obj.createNestedObject(F("static_ip"))); + getDisplay(obj.createNestedObject(F("display"))); } void getNetworks(JsonObject obj) { @@ -432,57 +497,25 @@ class RestApi { } void getLive(JsonObject obj) { - getMenu(obj.createNestedObject(F("menu"))); getGeneric(obj.createNestedObject(F("generic"))); - JsonArray invArr = obj.createNestedArray(F("inverter")); - obj["refresh_interval"] = mConfig->nrf.sendInterval; + obj[F("refresh")] = mConfig->nrf.sendInterval; - uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); + obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]); + } + for (uint8_t fld = 0; fld < sizeof(dcList); fld++) { + obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]); + obj[F("fld_names")][fld] = String(fields[dcList[fld]]); + } Inverter<> *iv; - uint8_t pos; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { iv = mSys->getInverterByPos(i); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - JsonObject obj2 = invArr.createNestedObject(); - obj2[F("enabled")] = (bool)iv->config->enabled; - obj2[F("name")] = String(iv->config->name); - obj2[F("channels")] = iv->channels; - obj2[F("power_limit_read")] = ah::round3(iv->actPowerLimit); - obj2[F("last_alarm")] = String(iv->lastAlarmMsg); - obj2[F("ts_last_success")] = rec->ts; - - JsonArray ch = obj2.createNestedArray("ch"); - JsonArray ch0 = ch.createNestedArray(); - obj2[F("ch_names")][0] = "AC"; - for (uint8_t fld = 0; fld < sizeof(list); fld++) { - pos = (iv->getPosByChFld(CH0, list[fld], rec)); - ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - } - - for(uint8_t j = 1; j <= iv->channels; j ++) { - obj2[F("ch_names")][j] = String(iv->config->chName[j-1]); - JsonArray cur = ch.createNestedArray(); - for (uint8_t k = 0; k < 6; k++) { - switch(k) { - default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break; - case 1: pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break; - case 2: pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break; - case 3: pos = (iv->getPosByChFld(j, FLD_YD, rec)); break; - case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break; - case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break; - } - cur[k] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - if(1 == j) { - obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; - obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; - } - } - } - } + bool parse = false; + if(NULL != iv) + parse = iv->config->enabled; + obj[F("iv")][i] = parse; } } @@ -510,18 +543,16 @@ class RestApi { bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); + bool accepted = true; if(NULL == iv) { jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as(); return false; } - if(F("power") == jsonIn[F("cmd")]) { - iv->devControlCmd = (jsonIn[F("val")] == 1) ? TurnOn : TurnOff; - iv->devControlRequest = true; - } else if(F("restart") == jsonIn[F("restart")]) { - iv->devControlCmd = Restart; - iv->devControlRequest = true; - } + if(F("power") == jsonIn[F("cmd")]) + accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); + else if(F("restart") == jsonIn[F("restart")]) + accepted = iv->setDevControlRequest(Restart); else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { iv->powerLimit[0] = jsonIn["val"]; if(F("limit_persistent_relative") == jsonIn[F("cmd")]) @@ -532,8 +563,8 @@ class RestApi { iv->powerLimit[1] = RelativNonPersistent; else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")]) iv->powerLimit[1] = AbsolutNonPersistent; - iv->devControlCmd = ActivePowerContr; - iv->devControlRequest = true; + + accepted = iv->setDevControlRequest(ActivePowerContr); } else if(F("dev") == jsonIn[F("cmd")]) { DPRINTLN(DBG_INFO, F("dev cmd")); @@ -544,13 +575,18 @@ class RestApi { return false; } + if(!accepted) { + jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); + return false; + } else + mApp->ivSendHighPrio(iv); + return true; } bool setSetup(JsonObject jsonIn, JsonObject jsonOut) { - if(F("scan_wifi") == jsonIn[F("cmd")]) { + if(F("scan_wifi") == jsonIn[F("cmd")]) mApp->scanAvailNetworks(); - } else if(F("set_time") == jsonIn[F("cmd")]) mApp->setTimestamp(jsonIn[F("val")]); else if(F("sync_ntp") == jsonIn[F("cmd")]) @@ -559,8 +595,7 @@ class RestApi { mTimezoneOffset = jsonIn[F("val")]; else if(F("discovery_cfg") == jsonIn[F("cmd")]) { mApp->setMqttDiscoveryFlag(); // for homeassistant - } - else { + } else { jsonOut[F("error")] = F("unknown cmd"); return false; } @@ -574,7 +609,9 @@ class RestApi { settings_t *mConfig; uint32_t mTimezoneOffset; - uint32_t mFreeHeap; + uint32_t mHeapFree, mHeapFreeBlk; + uint8_t mHeapFrag; + uint16_t nr; }; #endif /*__WEB_API_H__*/ diff --git a/src/web/html/about.html b/src/web/html/about.html new file mode 100644 index 000000000..c0eb8c5ea --- /dev/null +++ b/src/web/html/about.html @@ -0,0 +1,57 @@ + + + + About + {#HTML_HEADER} + + + {#HTML_NAV} +
+
+

About AhoyDTU

+
+
+
Used Libraries
+
+ + + + + + + + +
+
Contact Information
+
+
+
Github Repository
+ +
+
+
Discord Chat
+ +
+
+
E-Mail
+ +
+
+
+
+ {#HTML_FOOTER} + + + diff --git a/src/web/html/api.js b/src/web/html/api.js index 2e1ffb2bc..5ccb0e158 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -33,23 +33,66 @@ iconSuccess = [ /** * GENERIC FUNCTIONS */ +function ml(tagName, ...args) { + var el = document.createElement(tagName); + if(args[0]) { + for(var name in args[0]) { + if(name.indexOf("on") === 0) { + el.addEventListener(name.substr(2).toLowerCase(), args[0][name], false) + } else { + el.setAttribute(name, args[0][name]); + } + } + } + if (!args[1]) { + return el; + } + return nester(el, args[1]) +} -function topnav() { - toggle("topnav"); -} - -function parseMenu(obj) { - var e = document.getElementById("topnav"); - e.innerHTML = ""; - for(var i = 0; i < obj["name"].length; i ++) { - if(obj["name"][i] == "-") - e.appendChild(span("", ["seperator"])); - else { - var l = link(obj["link"][i], obj["name"][i], obj["trgt"][i]); - if(obj["link"][i] == window.location.pathname) - l.classList.add("active"); - e.appendChild(l); +function nester(el, n) { + if (typeof n === "string") { + var t = document.createTextNode(n); + el.appendChild(t); + } else if (n instanceof Array) { + for(var i = 0; i < n.length; i++) { + if (typeof n[i] === "string") { + var t = document.createTextNode(n[i]); + el.appendChild(t); + } else if (n[i] instanceof Node){ + el.appendChild(n[i]); + } } + } else if (n instanceof Node){ + el.appendChild(n) + } + return el; +} + +function topnav() { + toggle("topnav", "mobile"); +} + +function parseNav(obj) { + for(i = 0; i < 11; i++) { + if(i == 2) + continue; + var l = document.getElementById("nav"+i); + if(window.location.pathname == "/" + l.href.split('/').pop()) + l.classList.add("active"); + + if(obj["menu_protEn"]) { + if(obj["menu_prot"]) { + if(0 == i) + l.classList.remove("hide"); + else if(i > 2) { + if(((obj["menu_mask"] >> (i-2)) & 0x01) == 0x00) + l.classList.remove("hide"); + } + } else if(0 != i) + l.classList.remove("hide"); + } else if(i > 1) + l.classList.remove("hide"); } } @@ -60,7 +103,9 @@ function parseVersion(obj) { } function parseESP(obj) { - document.getElementById("esp_type").innerHTML="Board: " + obj["esp_type"]; + document.getElementById("esp_type").append( + document.createTextNode("Board: " + obj["esp_type"]) + ); } function parseRssi(obj) { @@ -69,7 +114,7 @@ function parseRssi(obj) { icon = iconWifi1; else if(obj["wifi_rssi"] <= -70) icon = iconWifi2; - document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "#fff", null, obj["wifi_rssi"])); + document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"])); } function setHide(id, hide) { @@ -82,12 +127,12 @@ function setHide(id, hide) { elm.classList.remove('hide'); } -function toggle(id) { +function toggle(id, cl="hide") { var e = document.getElementById(id); - if(!e.classList.contains("hide")) - e.classList.add("hide"); + if(!e.classList.contains(cl)) + e.classList.add(cl); else - e.classList.remove('hide'); + e.classList.remove(cl); } function getAjax(url, ptr, method="GET", json=null) { @@ -198,11 +243,10 @@ function link(dst, text, target=null) { return a; } -function svg(data=null, w=24, h=24, color="#000", cl=null, tooltip=null) { +function svg(data=null, w=24, h=24, cl=null, tooltip=null) { var s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); s.setAttribute('width', w); s.setAttribute('height', h); - s.setAttribute('fill', color); s.setAttribute('viewBox', '0 0 16 16'); if(null != cl) s.setAttribute('class', cl); if(null != data) { diff --git a/src/web/html/colorBright.css b/src/web/html/colorBright.css new file mode 100644 index 000000000..929828caa --- /dev/null +++ b/src/web/html/colorBright.css @@ -0,0 +1,27 @@ +:root { + --bg: #fff; + --fg: #000; + --fg2: #fff; + + --info: #0000dd; + --warn: #ff7700; + --success: #009900; + + --input-bg: #eee; + + --nav-bg: #333; + --primary: #006ec0; + --primary-hover: #044e86; + --secondary: #0072c8; + --nav-active: #555; + --footer-bg: #282828; + + --total-head-title: #8e5903; + --total-bg: #b06e04; + --iv-head-title: #1c6800; + --iv-head-bg: #32b004; + --ch-head-title: #003c80; + --ch-head-bg: #006ec0; + --ts-head: #333; + --ts-bg: #555; +} diff --git a/src/web/html/colorDark.css b/src/web/html/colorDark.css new file mode 100644 index 000000000..aa98c8620 --- /dev/null +++ b/src/web/html/colorDark.css @@ -0,0 +1,27 @@ +:root { + --bg: #222; + --fg: #ccc; + --fg2: #fff; + + --info: #0072c8; + --warn: #ffaa00; + --success: #00bb00; + + --input-bg: #333; + + --nav-bg: #333; + --primary: #004d87; + --primary-hover: #023155; + --secondary: #0072c8; + --nav-active: #555; + --footer-bg: #282828; + + --total-head-title: #555511; + --total-bg: #666622; + --iv-head-title: #115511; + --iv-head-bg: #226622; + --ch-head-title: #112255; + --ch-head-bg: #223366; + --ts-head: #333; + --ts-bg: #555; +} diff --git a/src/web/html/convert.py b/src/web/html/convert.py index 4a8f1f324..00f398acd 100644 --- a/src/web/html/convert.py +++ b/src/web/html/convert.py @@ -2,10 +2,70 @@ import os import gzip import glob - +import shutil +from datetime import date from pathlib import Path +import subprocess + + +def get_git_sha(): + try: + return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip() + except: + return "0000000" + +def readVersion(path): + f = open(path, "r") + lines = f.readlines() + f.close() + + today = date.today() + search = ["_MAJOR", "_MINOR", "_PATCH"] + version = today.strftime("%y%m%d") + "_ahoy_" + ver = "" + for line in lines: + if(line.find("VERSION_") != -1): + for s in search: + p = line.find(s) + if(p != -1): + version += line[p+13:].rstrip() + "." + ver += line[p+13:].rstrip() + "." + return ver[:-1] + +def htmlParts(file, header, nav, footer, version): + p = ""; + f = open(file, "r") + lines = f.readlines() + f.close(); + + f = open(header, "r") + h = f.read().strip() + f.close() -def convert2Header(inFile): + f = open(nav, "r") + n = f.read().strip() + f.close() + + f = open(footer, "r") + fo = f.read().strip() + f.close() + + for line in lines: + line = line.replace("{#HTML_HEADER}", h) + line = line.replace("{#HTML_NAV}", n) + line = line.replace("{#HTML_FOOTER}", fo) + p += line + + #placeholders + link = 'GIT SHA: ' + get_git_sha() + ' :: ' + version + '' + p = p.replace("{#VERSION}", version) + p = p.replace("{#VERSION_GIT}", link) + f = open("tmp/" + file, "w") + f.write(p); + f.close(); + return p + +def convert2Header(inFile, version): fileType = inFile.split(".")[1] define = inFile.split(".")[0].upper() define2 = inFile.split(".")[1].upper() @@ -17,14 +77,19 @@ def convert2Header(inFile): Path("html/h").mkdir(exist_ok=True) else: outName = "h/" + inFileVarName + ".h" - Path("h").mkdir(exist_ok=True) + data = "" if fileType == "ico": f = open(inFile, "rb") + data = f.read() + f.close() else: - f = open(inFile, "r") - data = f.read() - f.close() + if fileType == "html": + data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version) + else: + f = open(inFile, "r") + data = f.read() + f.close() if fileType == "css": data = data.replace('\n', '') @@ -53,13 +118,17 @@ def convert2Header(inFile): f.close() # delete all files in the 'h' dir -dir = 'h' +wd = 'h' if os.getcwd()[-4:] != "html": - dir = "web/html/" + dir + wd = "web/html/" + wd -if os.path.exists(dir): - for f in os.listdir(dir): - os.remove(os.path.join(dir, f)) +if os.path.exists(wd): + for f in os.listdir(wd): + os.remove(os.path.join(wd, f)) +wd += "/tmp" +if os.path.exists(wd): + for f in os.listdir(wd): + os.remove(os.path.join(wd, f)) # grab all files with following extensions if os.getcwd()[-4:] != "html": @@ -69,6 +138,11 @@ def convert2Header(inFile): for files in types: files_grabbed.extend(glob.glob(files)) +Path("h").mkdir(exist_ok=True) +Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements +shutil.copyfile("style.css", "tmp/style.css") +version = readVersion("../../defines.h") + # go throw the array for val in files_grabbed: - convert2Header(val) + convert2Header(val, version) diff --git a/src/web/html/includes/footer.html b/src/web/html/includes/footer.html new file mode 100644 index 000000000..bbc4c6ce7 --- /dev/null +++ b/src/web/html/includes/footer.html @@ -0,0 +1,16 @@ + diff --git a/src/web/html/includes/header.html b/src/web/html/includes/header.html new file mode 100644 index 000000000..f38a30f74 --- /dev/null +++ b/src/web/html/includes/header.html @@ -0,0 +1,5 @@ + + + + + diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html new file mode 100644 index 000000000..3dac1590e --- /dev/null +++ b/src/web/html/includes/nav.html @@ -0,0 +1,24 @@ + diff --git a/src/web/html/index.html b/src/web/html/index.html index 0cd7f430d..72537e5e4 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -2,36 +2,12 @@ Index - - - + {#HTML_HEADER} -
- AhoyDTU - - - - - -
-
-
+ {#HTML_NAV}
-

Uptime:
ESP-Time: @@ -51,7 +27,7 @@

Support this project:

  • Discuss with us on Discord
  • Report issues
  • Contribute to documentation
  • -
  • Download & Test development firmware, Changelog
  • +
  • Download & Test development firmware, Development Changelog
  • make a donation
  • @@ -60,22 +36,7 @@

    Support this project:

    - + {#HTML_FOOTER} + {#HTML_HEADER}
    -
    +
    -

    AhoyDTU

    - - +

    AhoyDTU

    +
    +
    +
    +
    - - + {#HTML_FOOTER} diff --git a/src/web/html/save.html b/src/web/html/save.html new file mode 100644 index 000000000..54d43d7f9 --- /dev/null +++ b/src/web/html/save.html @@ -0,0 +1,51 @@ + + + + Save + {#HTML_HEADER} + + + {#HTML_NAV} +
    +
    +
    +
    +
    + {#HTML_FOOTER} + + + diff --git a/src/web/html/serial.html b/src/web/html/serial.html index 8b2d292cd..da9d28167 100644 --- a/src/web/html/serial.html +++ b/src/web/html/serial.html @@ -2,73 +2,66 @@ Serial Console - - - + {#HTML_HEADER} -
    - AhoyDTU - - - - - -
    -
    -
    + {#HTML_NAV}
    -
    -
    - connected: - Uptime: - - -
    +
    + +
    +
    +
    connected:
    +
    Uptime:
    +
    + + +
    +
    +
    +

    Commands

    - - - - - - -
    - -
    -
    +
    +
    +
    Select Inverter
    +
    +
    +
    +
    Power Limit Command
    +
    + +
    +
    +
    +
    Power Limit Value
    +
    +
    +
    +
    +
    +
    +
    +
    Control Inverter
    +
    -
    -

    Ctrl result: n/a

    +
    +
    +
    Ctrl result
    +
    n/a
    - + {#HTML_FOOTER} + {#HTML_HEADER} -
    - AhoyDTU - - - - - -
    -
    -
    + {#HTML_NAV}
    - ERASE SETTINGS (not WiFi) -
    -
    + +
    +
    Device Host Name - - +
    +
    Device Name
    +
    +
    +
    +
    Dark Mode
    +
    +
    +
    + System Config +

    Pinout

    +
    + +

    Radio (NRF24L01+)

    +
    + +

    Serial Console

    +
    +
    print inverter data
    +
    +
    +
    +
    Serial Debug
    +
    +
    +
    +
    Interval [s]
    +
    +
    +
    +
    -
    +
    WiFi

    Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.

    - -
    - - - - - - + +
    +
    Search Networks
    +
    +
    + +
    +
    Avail Networks
    +
    + +
    +
    +
    +
    SSID
    +
    +
    +
    +
    Password
    +
    +
    -
    +
    Static IP (optional)

    Leave fields blank for DHCP
    - The following fields are parsed in this format: 192.168.1.1 + The following fields are parsed in this format: 192.168.4.1

    - - - - - - - - - - +
    +
    IP Address
    +
    +
    +
    +
    Submask
    +
    +
    +
    +
    DNS 1
    +
    +
    +
    +
    DNS 2
    +
    +
    +
    +
    Gateway
    +
    +
    -
    - Protection - - - +
    + Protection +
    +
    Admin Password
    +
    +

    Select pages which should be protected by password

    @@ -88,129 +129,295 @@
    -
    +
    Inverter -

    - -

    General

    - - - - +
    +
    +
    +
    +
    +
    +

    Note

    +

    A 'max module power' value of '0' disables the channel in 'live' view

    +
    +
    +

    General

    +
    +
    +
    +
    Interval [s]
    +
    +
    +
    +
    Max retries per Payload
    +
    +
    +
    +
    Reset values and YieldDay at midnight
    +
    +
    +
    +
    Reset values when inverter polling pauses at sunset
    +
    +
    +
    +
    Reset values when inverter status is 'not available'
    +
    +
    -
    - NTP Server - - - - - - - - +
    + NTP Server +
    +
    NTP Server / IP
    +
    +
    +
    +
    NTP Port
    +
    +
    +
    +
    set system time
    +
    + + + +
    +
    -
    - Sunrise & Sunset -

    - Latitude and Longitude must be set to be stored! decimal separator: '.' (dot) -

    - - - - - - -
    - -
    +
    + Sunrise & Sunset +

    Use a decimal separator: '.' (dot) for Latitude and Longitude

    + +
    +
    Latitude (decimal)
    +
    +
    +
    +
    Longitude (decimal)
    +
    +
    +
    +
    Offset (pre sunrise, post sunset)
    +
    +
    +
    +
    Pause polling inverters during night
    +
    +
    -
    - MQTT - - - - - - - - - - - - - +
    + MQTT +
    +
    Broker / Server IP
    +
    +
    +
    +
    Port
    +
    +
    +
    +
    Username (optional)
    +
    +
    +
    +
    Password (optional)
    +
    +
    +
    +
    Topic
    +
    +
    +

    Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)

    +
    +
    Interval [s]
    +
    +
    +
    +
    Discovery Config (homeassistant)
    +
    + + +
    +
    - +
    -
    - System Config -

    Pinout (Wemos)

    -
    - -

    Radio (NRF24L01+)

    -
    - -

    Serial Console

    - -
    - -
    - - +
    + Display Config +
    +
    +
    +
    Turn off while inverters are offline
    +
    +
    +
    +
    Enable Screensaver (pixel shifting, OLED only)
    +
    +
    +
    +
    Luminance
    +
    +
    +

    Pinout

    +
    -
    - - - -
    -
    -
    - Download your settings (JSON file) (only saved values) +
    +
    Reboot device after successful save
    +
    + + +
    +
    +
    + ERASE SETTINGS (not WiFi) +
    + Import / Export JSON Settings +
    +
    Import
    +
    +
    + + +
    +
    +
    +
    +
    Export
    +
    + Export settings (JSON file) (only values, passwords will be removed!) +
    +
    +
    +
    - + {#HTML_FOOTER} diff --git a/src/web/html/style.css b/src/web/html/style.css index 8f4b473bc..ca4b0c9a6 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -4,26 +4,39 @@ html, body { padding: 0; height: 100%; min-height: 100%; + background-color: var(--bg); + color: var(--fg); } h2 { padding-left: 10px; } +span, li, h3, label, fieldset { + color: var(--fg); +} + +fieldset, input[type=submit], .btn { + border-radius: 4px; +} + +#live span { + color: var(--fg2); +} + .topnav { - background-color: #333; + background-color: var(--nav-bg); position: fixed; top: 0; width: 100%; } .topnav a { - color: #fff; + color: var(--fg2); padding: 14px 14px; text-decoration: none; font-size: 17px; display: block; - height: 20px; } #topnav a { @@ -33,23 +46,26 @@ h2 { .topnav a.icon { top: 0; left: 0; - background: #333; + background: var(--nav-bg); display: block; position: absolute; } .topnav a:hover { - background-color: #044e86 !important; - color: #000; + background-color: var(--primary-hover) !important; } .topnav .info { - color: #fff; + color: var(--fg2); position: absolute; right: 24px; top: 5px; } +.topnav .mobile { + display: none; +} + svg.icon { vertical-align: middle; display: inline-block; @@ -57,8 +73,24 @@ svg.icon { padding: 5px 7px 5px 0px; } +.icon-info { + fill: var(--info); +} + +.icon-warn { + fill: var(--warn); +} + +.icon-success { + fill: var(--success); +} + +.wifi { + fill: var(--fg2); +} + .title { - background-color: #006ec0; + background-color: var(--primary); color: #fff !important; padding-left: 80px !important } @@ -74,7 +106,7 @@ svg.icon { } .topnav .active { - background-color: #555; + background-color: var(--nav-active); } span.seperator { @@ -85,6 +117,197 @@ span.seperator { display: block; } +#content { + max-width: 1140px; +} + +.total-h { + background-color: var(--total-head-title); + color: var(--fg2); +} + +.total-bg { + background-color: var(--total-bg); + color: var(--fg2); +} + +.iv-h { + background-color: var(--iv-head-title); + color: var(--fg2); +} + +.iv-bg { + background-color: var(--iv-head-bg); + color: var(--fg2); +} + +.ch-h { + background-color: var(--ch-head-title); + color: var(--fg2); +} + +.ch-bg { + background-color: var(--ch-head-bg); + color: var(--fg2); +} + +.ts-h { + background-color: var(--ts-head); + color: var(--fg2); +} + +.ts-bg { + background-color: var(--ts-bg); + color: var(--fg2); +} + +.hr { + border-top: 1px solid var(--iv-head-title); + margin: 1rem 0 1rem; +} + +p { + text-align: justify; + font-size: 13pt; + color: var(--fg); +} + +#footer { + background-color: var(--footer-bg); +} + +.row { display: flex; max-width: 100%; flex-wrap: wrap; } +.col { flex: 1 0 0%; } + +.col-1, .col-2, .col-3, .col-4, +.col-5, .col-6, .col-7, .col-8, +.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; } + +.col-1 { width: 8.333333333%; } +.col-2 { width: 16.66666667%; } +.col-3 { width: 25%; } +.col-4 { width: 33.33333333%; } +.col-5 { width: 41.66666667%; } +.col-6 { width: 50%; } +.col-7 { width: 58.33333333%; } +.col-8 { width: 66.66666667%; } +.col-9 { width: 75%; } +.col-10 { width: 83.33333333%; } +.col-11 { width: 91.66666667%; } +.col-12 { width: 100%; } + +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-3 { padding: 1rem; } +.p-4 { padding: 1.5rem; } +.p-5 { padding: 3rem; } + +.px-1 { padding: 0 0.25rem 0 0.25rem; } +.px-2 { padding: 0 0.5rem 0 0.5rem; } +.px-3 { padding: 0 1rem 0 1rem; } +.px-4 { padding: 0 1.5rem 0 1.5rem; } +.px-5 { padding: 0 3rem 0 3rem; } + +.py-1 { padding: 0.25rem 0 0.25rem; } +.py-2 { padding: 0.5rem 0 0.5rem; } +.py-3 { padding: 1rem 0 1rem; } +.py-4 { padding: 1.5rem 0 1.5rem; } +.py-5 { padding: 3rem 0 3rem; } + +.mx-1 { margin: 0 0.25rem 0 0.25rem; } +.mx-2 { margin: 0 0.5rem 0 0.5rem; } +.mx-3 { margin: 0 1rem 0 1rem; } +.mx-4 { margin: 0 1.5rem 0 1.5rem; } +.mx-5 { margin: 0 3rem 0 3rem; } + +.my-1 { margin: 0.25rem 0 0.25rem; } +.my-2 { margin: 0.5rem 0 0.5rem; } +.my-3 { margin: 1rem 0 1rem; } +.my-4 { margin: 1.5rem 0 1.5rem; } +.my-5 { margin: 3rem 0 3rem; } + +.mt-1 { margin-top: 0.25rem } +.mt-2 { margin-top: 0.5rem } +.mt-3 { margin-top: 1rem } +.mt-4 { margin-top: 1.5rem } +.mt-5 { margin-top: 3rem } + +.mb-1 { margin-bottom: 0.25rem } +.mb-2 { margin-bottom: 0.5rem } +.mb-3 { margin-bottom: 1rem } +.mb-4 { margin-bottom: 1.5rem } +.mb-5 { margin-bottom: 3rem } + +.fs-1 { font-size: 3.5rem; } +.fs-2 { font-size: 3rem; } +.fs-3 { font-size: 2.5rem; } +.fs-4 { font-size: 2rem; } +.fs-5 { font-size: 1.75rem; } +.fs-6 { font-size: 1.5rem; } +.fs-7 { font-size: 1.25rem; } +.fs-8 { font-size: 1rem; } +.fs-9 { font-size: 0.75rem; } +.fs-10 { font-size: 0.5rem; } + +.a-r { text-align: right; } +.a-c { text-align: center; } + +.row > * { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +*, ::after, ::before { + box-sizing: border-box; +} + +/* sm */ +@media(min-width: 768px) { + .col-sm-1 { width: 8.333333333%; } + .col-sm-2 { width: 16.66666667%; } + .col-sm-3 { width: 25%; } + .col-sm-4 { width: 33.33333333%; } + .col-sm-5 { width: 41.66666667%; } + .col-sm-6 { width: 50%; } + .col-sm-7 { width: 58.33333333%; } + .col-sm-8 { width: 66.66666667%; } + .col-sm-9 { width: 75%; } + .col-sm-10 { width: 83.33333333%; } + .col-sm-11 { width: 91.66666667%; } + .col-sm-12 { width: 100%; } + + .mb-sm-1 { margin-bottom: 0.25rem } + .mb-sm-2 { margin-bottom: 0.5rem } + .mb-sm-3 { margin-bottom: 1rem } + .mb-sm-4 { margin-bottom: 1.5rem } + .mb-sm-5 { margin-bottom: 3rem } + + .fs-sm-1 { font-size: 3.5rem; } + .fs-sm-2 { font-size: 3rem; } + .fs-sm-3 { font-size: 2.5rem; } + .fs-sm-4 { font-size: 2rem; } + .fs-sm-5 { font-size: 1.75rem; } + .fs-sm-6 { font-size: 1.5rem; } + .fs-sm-7 { font-size: 1.25rem; } + .fs-sm-8 { font-size: 1rem; } +} + +/* md */ +@media(min-width: 992px) { + .col-md-1 { width: 8.333333333%; } + .col-md-2 { width: 16.66666667%; } + .col-md-3 { width: 25%; } + .col-md-4 { width: 33.33333333%; } + .col-md-5 { width: 41.66666667%; } + .col-md-6 { width: 50%; } + .col-md-7 { width: 58.33333333%; } + .col-md-8 { width: 66.66666667%; } + .col-md-9 { width: 75%; } + .col-md-10 { width: 83.33333333%; } + .col-md-11 { width: 91.66666667%; } + .col-md-12 { width: 100%; } +} + #wrapper { min-height: 100%; } @@ -97,7 +320,6 @@ span.seperator { #footer { height: 121px; margin-top: -121px; - background-color: #555; width: 100%; font-size: 13px; } @@ -131,7 +353,7 @@ span.seperator { } .hide { - display: none; + display: none !important; } @media only screen and (min-width: 992px) { @@ -152,7 +374,7 @@ span.seperator { padding-left: 24px !important; } - .topnav .hide { + .topnav .mobile { display: block; } @@ -172,13 +394,6 @@ span.seperator { } } -/** old CSS below **/ - -p { - text-align: justify; - font-size: 13pt; -} - p.lic, p.lic a { font-size: 8pt; color: #999; @@ -187,11 +402,11 @@ p.lic, p.lic a { .des { margin-top: 20px; font-size: 13pt; - color: #006ec0; + color: var(--secondary); } .s_active, .s_collapsible:hover { - background-color: #044e86; + background-color: var(--primary-hover); color: #fff; } @@ -201,34 +416,34 @@ p.lic, p.lic a { } .s_collapsible { - background-color: #006ec0; + background-color: var(--primary); color: white; cursor: pointer; - padding: 18px; + padding: 12px; width: 100%; border: none; text-align: left; outline: none; font-size: 15px; - margin-bottom: 4px; + margin-bottom: 5px; } .subdes { font-size: 12pt; - color: #006ec0; + color: var(--secondary); margin-left: 7px; } .subsubdes { font-size:12pt; - color:#006ec0; + color:var(--secondary); margin: 0 0 7px 12px; } a:link, a:visited { text-decoration: none; font-size: 13pt; - color: #006ec0; + color: var(--secondary); } a:hover, a:focus { @@ -236,14 +451,14 @@ a:hover, a:focus { } a.btn { - background-color: #006ec0; + background-color: var(--primary); color: #fff; padding: 7px 15px 7px 15px; display: inline-block; } a.btn:hover { - background-color: #044e86 !important; + background-color: var(--primary-hover) !important; } input, select { @@ -251,11 +466,13 @@ input, select { font-size: 13pt; } -input.text, select { - width: 70%; +input[type=text], input[type=password], select, input[type=number] { + width: 100%; box-sizing: border-box; - margin-bottom: 10px; border: 1px solid #ccc; + border-radius: 4px; + background-color: var(--input-bg); + color: var(--fg); } input.sh { @@ -268,7 +485,7 @@ input.btnDel { } input.btn { - background-color: #006ec0; + background-color: var(--primary); color: #fff; border: 0px; padding: 7px 20px 7px 20px; @@ -282,7 +499,8 @@ input.btn:hover { } input.cb { - margin-bottom: 20px; + margin-bottom: 15px; + margin-top: 10px; } label { @@ -298,10 +516,6 @@ pre { white-space: pre-wrap; } -fieldset { - margin-bottom: 15px; -} - .left { float: left; } @@ -310,89 +524,12 @@ fieldset { float: right; } -div.ch-iv { - width: 100%; - background-color: #32b004; - display: inline-block; - margin-bottom: 15px; - padding-bottom: 20px; - overflow: auto; -} - -div.ch { - width: 220px; - min-height: 350px; - background-color: #006ec0; - display: inline-block; - margin: 0 20px 10px 0px; - overflow: auto; - padding-bottom: 20px; -} - -div.ch-all { - width: 100%; - background-color: #b06e04; - display: inline-block; - margin-bottom: 15px; - padding-bottom: 20px; - overflow: auto; -} - -div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head, div.ch-all .value, div.ch-all .info, div.ch-all .head { - color: #fff; - display: block; - width: 100%; - text-align: center; -} - .subgrp { float: left; width: 220px; } -div.ch .unit, div.ch-iv .unit, div.ch-all .unit { - font-size: 19px; - margin-left: 10px; -} - -div.ch .value, div.ch-iv .value, div.ch-all .value { - margin-top: 20px; - font-size: 24px; -} - -div.ch .info, div.ch-iv .info, div.ch-all .info { - margin-top: 3px; - font-size: 10px; -} - -div.ch .head { - background-color: #003c80; - padding: 10px 0 10px 0; -} - -div.ch-all .head { - background-color: #8e5903; - padding: 10px 0 10px 0; -} - -div.ch-iv .head { - background-color: #1c6800; - padding: 10px 0 10px 0; -} - -div.iv { - max-width: 960px; - margin-bottom: 40px; -} - -div.ts { - font-size: 13px; - background-color: #ddd; - border-top: 7px solid #999; - padding: 7px; -} - -div.ModPwr, div.ModName { +div.ModPwr, div.ModName, div.YieldCor { width:70%; display: inline-block; } @@ -442,104 +579,55 @@ div.hr { } #login { - width: 300px; + width: 450px; height: 200px; border: 1px solid #ccc; - background-color: #eee; + background-color: var(--input-bg); position: absolute; top: 50%; left: 50%; margin-top: -160px; - margin-left: -150px; -} - -#login .pad { - padding: 20px; -} - -#login .pad input { - width: 100%; - padding: 7px 0 7px 0; - border: 0px; - margin-bottom: 10px; + margin-left: -225px; } .head { - background-color: #006ec0; + background-color: var(--primary); color: #fff; } -.row { display: flex; max-width: 100%; flex-wrap: wrap; } -.col { flex: 1 0 0%; } -.col-1, .col-2, .col-3, .col-4, -.col-5, .col-6, .col-7, .col-8, -.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; } - - -.col-1 { width: 8.333333333%; } -.col-2 { width: 16.66666667%; } -.col-3 { width: 25%; } -.col-4 { width: 33.33333333%; } -.col-5 { width: 41.66666667%; } -.col-6 { width: 50%; } -.col-7 { width: 58.33333333%; } -.col-8 { width: 66.66666667%; } -.col-9 { width: 75%; } -.col-10 { width: 83.33333333%; } -.col-11 { width: 91.66666667%; } -.col-12 { width: 100%; } - -.p-1 { padding: 0.25rem; } -.p-2 { padding: 0.5rem; } -.p-3 { padding: 1rem; } -.p-4 { padding: 1.5rem; } -.p-5 { padding: 3rem; } - -.mt-1 { margin-top: 0.25rem } -.mt-2 { margin-top: 0.5rem } -.mt-3 { margin-top: 1rem } -.mt-4 { margin-top: 1.5rem } -.mt-5 { margin-top: 3rem } - -.mb-1 { margin-bottom: 0.25rem } -.mb-2 { margin-bottom: 0.5rem } -.mb-3 { margin-bottom: 1rem } -.mb-4 { margin-bottom: 1.5rem } -.mb-5 { margin-bottom: 3rem } - -.a-r { text-align: right; } -.a-c { text-align: center; } - -/* sm */ -@media(min-width: 768px) { - .col-sm-1 { width: 8.333333333%; } - .col-sm-2 { width: 16.66666667%; } - .col-sm-3 { width: 25%; } - .col-sm-4 { width: 33.33333333%; } - .col-sm-5 { width: 41.66666667%; } - .col-sm-6 { width: 50%; } - .col-sm-7 { width: 58.33333333%; } - .col-sm-8 { width: 66.66666667%; } - .col-sm-9 { width: 75%; } - .col-sm-10 { width: 83.33333333%; } - .col-sm-11 { width: 91.66666667%; } - .col-sm-12 { width: 100%; } +.css-tooltip{ + position: relative; } - -/* md */ -@media(min-width: 992px) { - .col-md-1 { width: 8.333333333%; } - .col-md-2 { width: 16.66666667%; } - .col-md-3 { width: 25%; } - .col-md-4 { width: 33.33333333%; } - .col-md-5 { width: 41.66666667%; } - .col-md-6 { width: 50%; } - .col-md-7 { width: 58.33333333%; } - .col-md-8 { width: 66.66666667%; } - .col-md-9 { width: 75%; } - .col-md-10 { width: 83.33333333%; } - .col-md-11 { width: 91.66666667%; } - .col-md-12 { width: 100%; } +.css-tooltip:hover:after{ + content:attr(data-tooltip); + background:#000; + padding:5px; + border-radius:3px; + display: inline-block; + position: absolute; + transform: translate(-50%,-100%); + margin:0 auto; + color:#FFF; + min-width:100px; + min-width:150px; + top:-5px; + left: 50%; + text-align:center; +} +.css-tooltip:hover:before { + top:-5px; + left: 50%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-color: rgba(0, 0, 0, 0); + border-top-color: #000; + border-width: 5px; + margin-left: -5px; + transform: translate(0,0px); } diff --git a/src/web/html/system.html b/src/web/html/system.html index 5419d7ba4..0f1df3194 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -2,21 +2,10 @@ System - - - + {#HTML_HEADER} -
    - AhoyDTU - - - - - -
    -
    -
    + {#HTML_NAV}
    
    @@ -26,25 +15,10 @@
                     
    - + {#HTML_FOOTER} + {#HTML_HEADER} -
    - AhoyDTU - - - - - -
    -
    -
    + {#HTML_NAV}
    -
    - - -
    - -
    -
    - + {#HTML_FOOTER} diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index b53258895..de5b0069b 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -2,144 +2,229 @@ Live - - + {#HTML_HEADER} - -
    - AhoyDTU - - - - - -
    -
    -
    + {#HTML_NAV}

    Every seconds the values are updated

    - + {#HTML_FOOTER}