Browse Source

Merge branch 'main' into V0.8.36

pull/1321/head
VArt67 2 years ago
committed by GitHub
parent
commit
274e76834b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      .github/workflows/compile_development.yml
  2. 23
      .github/workflows/compile_release.yml
  3. 2
      .gitignore
  4. 8
      README.md
  5. 35
      User_Manual.md
  6. 9
      doc/prometheus_ep_description.md
  7. 13
      patches/AsyncWeb_Prometheus.patch
  8. 360
      patches/GxEPD2_SW_SPI.patch
  9. 981
      patches/RF24_Hal.patch
  10. BIN
      pics/PXL_20230824_204200660.jpg
  11. BIN
      pics/PXL_20230901_061927908.jpg
  12. 13
      scripts/applyPatches.py
  13. 10
      scripts/buildManifest.py
  14. 38
      scripts/convertHtml.py
  15. 86
      scripts/getVersion.py
  16. 1
      src/.vscode/settings.json
  17. 25
      src/CHANGES.md
  18. 364
      src/app.cpp
  19. 117
      src/app.h
  20. 20
      src/appInterface.h
  21. 129
      src/config/config.h
  22. 17
      src/config/config_override_example.h
  23. 198
      src/config/settings.h
  24. 8
      src/defines.h
  25. 62
      src/eth/ahoyeth.cpp
  26. 4
      src/eth/ahoyeth.h
  27. 141
      src/eth/ethSpi.h
  28. 119
      src/hm/CommQueue.h
  29. 918
      src/hm/Communication.h
  30. 187
      src/hm/Heuristic.h
  31. 32
      src/hm/HeuristicInv.h
  32. 124
      src/hm/hmDefines.h
  33. 439
      src/hm/hmInverter.h
  34. 418
      src/hm/hmPayload.h
  35. 443
      src/hm/hmRadio.h
  36. 109
      src/hm/hmSystem.h
  37. 775
      src/hm/miPayload.h
  38. 218
      src/hm/nrfHal.h
  39. 117
      src/hm/radio.h
  40. 110
      src/hms/cmt2300a.h
  41. 196
      src/hms/cmtHal.h
  42. 26
      src/hms/esp32_3wSpi.h
  43. 406
      src/hms/hmsPayload.h
  44. 171
      src/hms/hmsRadio.h
  45. 260
      src/platformio.ini
  46. 176
      src/plugins/Display/Display.h
  47. 144
      src/plugins/Display/Display_Mono.h
  48. 84
      src/plugins/Display/Display_Mono_128X32.h
  49. 239
      src/plugins/Display/Display_Mono_128X64.h
  50. 82
      src/plugins/Display/Display_Mono_64X48.h
  51. 167
      src/plugins/Display/Display_Mono_84X48.h
  52. 22
      src/plugins/Display/Display_data.h
  53. 188
      src/plugins/Display/Display_ePaper.cpp
  54. 29
      src/plugins/Display/Display_ePaper.h
  55. 41
      src/plugins/Display/imagedata.h
  56. 59
      src/publisher/pubMqtt.h
  57. 2
      src/publisher/pubMqttDefs.h
  58. 39
      src/publisher/pubMqttIvData.h
  59. 12
      src/utils/dbg.h
  60. 16
      src/utils/helper.cpp
  61. 11
      src/utils/helper.h
  62. 12
      src/utils/improv.h
  63. 11
      src/utils/scheduler.h
  64. 9
      src/utils/spiPatcher.cpp
  65. 88
      src/utils/spiPatcher.h
  66. 17
      src/utils/spiPatcherHandle.h
  67. 99
      src/utils/syslog.cpp
  68. 54
      src/utils/syslog.h
  69. 126
      src/utils/timemonitor.h
  70. 381
      src/web/RestApi.h
  71. 100
      src/web/html/api.js
  72. 3
      src/web/html/colorBright.css
  73. 5
      src/web/html/colorDark.css
  74. 765
      src/web/html/grid_info.json
  75. 2
      src/web/html/includes/nav.html
  76. 32
      src/web/html/index.html
  77. 6
      src/web/html/save.html
  78. 144
      src/web/html/serial.html
  79. 634
      src/web/html/setup.html
  80. 118
      src/web/html/style.css
  81. 140
      src/web/html/system.html
  82. 5
      src/web/html/update.html
  83. 274
      src/web/html/visualization.html
  84. 218
      src/web/web.h
  85. 188
      src/wifi/ahoywifi.cpp
  86. 12
      src/wifi/ahoywifi.h
  87. 6
      tools/fonts/fontconv.bat
  88. 11
      tools/fonts/u8g2 font-sources.txt
  89. 1327
      tools/fonts/u8g2_font_5x8_symbols_ahoy.bdf
  90. 40
      tools/fonts/u8g2_font_5x8_symbols_ahoy.c_
  91. BIN
      tools/fonts/u8g2_font_5x8_symbols_ahoy.fon
  92. 166
      tools/fonts/u8g2_font_ncenB08_symbols8_ahoy.bdf
  93. 13
      tools/fonts/u8g2_font_ncenB08_symbols8_ahoy.c_
  94. BIN
      tools/fonts/u8g2_font_ncenB08_symbols8_ahoy.fon
  95. 196
      tools/fonts/u8g2_font_ncenB10_symbols10_ahoy.bdf
  96. 14
      tools/fonts/u8g2_font_ncenB10_symbols10_ahoy.c_
  97. BIN
      tools/fonts/u8g2_font_ncenB10_symbols10_ahoy.fon
  98. 16
      tools/fonts/used_fonts.txt
  99. 13
      tools/rpi/hoymiles/__init__.py
  100. 9
      tools/rpi/hoymiles/__main__.py

22
.github/workflows/compile_development.yml

@ -39,18 +39,14 @@ jobs:
- name: Install PlatformIO - name: Install PlatformIO
run: | run: |
python -m pip install --upgrade pip python -m pip install setuptools --upgrade pip
pip install --upgrade platformio pip install --upgrade platformio
- name: Convert HTML files
working-directory: src/web/html
run: python convert.py
- name: Run PlatformIO - name: Run PlatformIO
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 esp32-wroom32-ethernet-release --environment opendtufusionv1-release run: pio run -d src --environment esp8266 --environment esp8266-prometheus --environment esp8285 --environment esp32-wroom32 --environment esp32-wroom32-prometheus --environment esp32-wroom32-ethernet --environment esp32-s2-mini --environment esp32-c3-mini --environment opendtufusion --environment opendtufusion-ethernet
- name: Copy boot_app0.bin - name: Copy boot_app0.bin
run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusionv1-release/ota.bin run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
- name: Rename Binary files - name: Rename Binary files
id: rename-binary-files id: rename-binary-files
@ -77,3 +73,15 @@ jobs:
src/User_Manual.md src/User_Manual.md
src/install.html src/install.html
- name: Rename firmware directory
run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }}
- name: Deploy
uses: nogsantos/scp-deploy@master
with:
src: src/${{ steps.rename-binary-files.outputs.name }}/
host: ${{ secrets.FW_SSH_HOST }}
remote: ${{ secrets.FW_SSH_DIR }}/dev
port: ${{ secrets.FW_SSH_PORT }}
user: ${{ secrets.FW_SSH_USER }}
key: ${{ secrets.FW_SSH_KEY }}

23
.github/workflows/compile_release.yml

@ -43,18 +43,14 @@ jobs:
- name: Install PlatformIO - name: Install PlatformIO
run: | run: |
python -m pip install --upgrade pip python -m pip install setuptools --upgrade pip
pip install --upgrade platformio pip install --upgrade platformio
- name: Convert HTML files
working-directory: src/web/html
run: python convert.py
- name: Run PlatformIO - name: Run PlatformIO
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 esp32-wroom32-ethernet-release --environment opendtufusionv1-release run: pio run -d src --environment esp8266 --environment esp8266-prometheus --environment esp8285 --environment esp32-wroom32 --environment esp32-wroom32-prometheus --environment esp32-wroom32-ethernet --environment esp32-s2-mini --environment esp32-c3-mini --environment opendtufusion --environment opendtufusion-ethernet
- name: Copy boot_app0.bin - name: Copy boot_app0.bin
run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusionv1-release/ota.bin run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
- name: Rename Binary files - name: Rename Binary files
id: rename-binary-files id: rename-binary-files
@ -93,3 +89,16 @@ jobs:
asset_path: ./${{ steps.rename-binary-files.outputs.name }}.zip asset_path: ./${{ steps.rename-binary-files.outputs.name }}.zip
asset_name: ${{ steps.rename-binary-files.outputs.name }}.zip asset_name: ${{ steps.rename-binary-files.outputs.name }}.zip
asset_content_type: application/zip asset_content_type: application/zip
- name: Rename firmware directory
run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }}
- name: Deploy
uses: nogsantos/scp-deploy@master
with:
src: src/${{ steps.rename-binary-files.outputs.name }}/
host: ${{ secrets.FW_SSH_HOST }}
remote: ${{ secrets.FW_SSH_DIR }}/release
port: ${{ secrets.FW_SSH_PORT }}
user: ${{ secrets.FW_SSH_USER }}
key: ${{ secrets.FW_SSH_KEY }}

2
.gitignore

@ -13,3 +13,5 @@ src/web/html/tmp/*
*.suo *.suo
*.ipch *.ipch
src/output.map src/output.map
/.venv

8
README.md

@ -16,16 +16,12 @@ This work is licensed under a
THIS IS A FORK OF: THIS IS A FORK OF:
# 🖐 Ahoy! # 🖐 Ahoy!
![Logo](https://github.com/grindylow/ahoy/blob/main/doc/logo1_small.png?raw=true) ![Logo](https://github.com/lumapu/ahoy/blob/main/doc/logo1_small.png?raw=true)
For details on the base project visit the Ahoy site. For details on the base project visit the Ahoy site.
This fork adds the following features: This fork adds the following features:
### Added chart to Display128x64 ### Added chart to Display128x64
### Added a History menu ### Added a History menu

35
User_Manual.md

@ -124,6 +124,8 @@ The AhoyDTU subscribes on following topics:
👆 `<INVERTER_ID>` is the number of the specific inverter in the setup page. 👆 `<INVERTER_ID>` is the number of the specific inverter in the setup page.
**NOTE:** Some users reported that a limit below 20W results in 0W output of the inverter. To reenable the inverter a reboot command was need to be sent, because a new limit with 100% didn't work.
### Inverter restart ### Inverter restart
```mqtt ```mqtt
@ -265,30 +267,31 @@ In the same approach as for MQTT any other SubCmd and also MainCmd can be applie
Gather user inverter information here to understand what differs between some inverters. Gather user inverter information here to understand what differs between some inverters.
To get the information open the URL `/api/record/info` on your AhoyDTU. The information will only be present once the AhoyDTU was able to communicate with an inverter. To get the information open the URL `/api/record/info` on your AhoyDTU. The information will only be present once the AhoyDTU was able to communicate with an inverter.
| Name | Inverter Typ | Bootloader V. | FWVersion | FWBuild [YYYY] | FWBuild [MM-DD] | HWPartId | | | | Name | Inverter Typ | Bootloader V. | FWVersion | FWBuild [YYYY] | FWBuild [MM-DD] | HWPartId | FWBuild [HH:MM:SS] | |
| ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------- | --------- | | ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------------------- | --------- |
| DanielR92 | HM-1500 | | 1.0.16 | 2021 | 10-12 | 100 | | | | lumapu | HM-1200 | 0.1.0 | 1.0.12 | 2020 | 06-24 | | | |
| isdor | HM-300 | | 1.0.14 | 2021 | 12-09 | 102 | | | | dragricola | HM-1200 | | 1.0.16 | 2021 | 10-12 | 100 | | |
| eeprom23 | HM-1200 | 0.1.0 | 1.0.18 | 2021 | 12-24 | 269619201 | 18:21:00 | |
| eeprom23 | HM-1200 2t | 0.1.0 | 1.0.16 | 2021 | 10-12 | 269619207 | 17:06:00 | |
| aschiffler | HM-1500 | | 1.0.12 | 2020 | 06-24 | 100 | | | | aschiffler | HM-1500 | | 1.0.12 | 2020 | 06-24 | 100 | | |
| DanielR92 | HM-1500 | | 1.0.16 | 2021 | 10-12 | 100 | | |
| Copro | HM-300 | | 1.0.10 | 2020 | 07-07 | 102 | 14:12:00 | |
| klahus1 | HM-300 | | 1.0.10 | 2020 | 07-07 | 102 | | | | klahus1 | HM-300 | | 1.0.10 | 2020 | 07-07 | 102 | | |
| roku133 | HM-400 | | 1.0.10 | 2020 | 07-07 | 102 | | | | isdor | HM-300 | | 1.0.14 | 2021 | 12-09 | 102 | | |
| eeprom23 | HM-1200 | 0.1.0 | 1.0.18 | 2021 | 12-24 | 269619201 | 18:21:00 | HWRev 256 | | roku133 | HM-400 | | 1.0.10 | 2020 | 07-07 | 102 | | HWRev 256 |
| eeprom23 | HM-1200 2t | 0.1.0 | 1.0.16 | 2021 | 10-12 | 269619207 | 17:06:00 | HWRev 256 | | setje | HM-600 | | 1.0.08 | 2020 | 07-10 | 104 | | HWRev 256 |
| fila612 | HM-700 | | 1.0.10 | 2021 | 11-01 | 104 | | |
| tfhcm | TSUN-350 | | 1.0.14 | 2021 | 12-09 | 102 | | |
| Groobi | TSOL-M400 | | 1.0.14 | 2021 | 12-09 | 102 | | |
| setje | HM-600 | | 1.0.08 | 2020 | 07-10 | 104 | | |
| madmartin | HM-600 | 0.1.4 | 1.0.10 | 2021 | 11-01 | 104 | | | | madmartin | HM-600 | 0.1.4 | 1.0.10 | 2021 | 11-01 | 104 | | |
| lumapu | HM-1200 | 0.1.0 | 1.0.12 | 2020 | 06-24 | | | |
| chehrlic | HM-600 | | 1.0.10 | 2021 | 11-01 | 104 | | | | chehrlic | HM-600 | | 1.0.10 | 2021 | 11-01 | 104 | | |
| chehrlic | TSOL-M800de | | 1.0.10 | 2021 | 11-01 | 104 | | | | fila612 | HM-700 | | 1.0.10 | 2021 | 11-01 | 104 | | |
| B5r1oJ0A9G | HM-800 | | 1.0.10 | 2021 | | 104 | | | | B5r1oJ0A9G | HM-800 | | 1.0.10 | 2021 | | 104 | | |
| 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 | | | | 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 | | | | dragricola | MI-300 | | 230 | 2017 | 08-08 | 1 | | |
| rejoe2 | MI-600 | | 236 | 2018 | 11-27 | 17 | | |
| tomquist | TSOL-M1600 | | 1.0.12 | 2020 | 06-24 | 100 | | |
| Groobi | TSOL-M400 | | 1.0.14 | 2021 | 12-09 | 102 | | |
| chehrlic | TSOL-M800de | | 1.0.10 | 2021 | 11-01 | 104 | | |
| tfhcm | TSUN-350 | | 1.0.14 | 2021 | 12-09 | 102 | | |
| | | | | | | | | | | | | | | | | | | |
## Developer Information about Command Queue ## Developer Information about Command Queue

9
doc/prometheus_ep_description.md

@ -16,7 +16,7 @@ Prometheus metrics provided at `/metrics`.
## Exported Metrics ## Exported Metrics
| Metric name | Type | Description | Labels | | Metric name | Type | Description | Labels |
|----------------------------------------|---------|--------------------------------------------------------|--------------| |----------------------------------------------|---------|----------------------------------------------------------|--------------|
| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename | | `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_uptime` | Counter | Seconds since boot of the AhoyDTU device | devicename |
| `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename | | `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename |
@ -33,13 +33,16 @@ Prometheus metrics provided at `/metrics`.
| `ahoy_solar_Temp_celsius` | Gauge | Temperature of inverter | inverter | | `ahoy_solar_Temp_celsius` | Gauge | Temperature of inverter | inverter |
| `ahoy_solar_ALARM_MES_ID` | Gauge | Alarm message index 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_LastAlarmCode` | Gauge | Last alarm code from inverter | inverter |
| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter | | `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 | | `ahoy_solar_YieldDay_wattHours_total` | Counter | Energy converted to AC per day [Wh] for all moduls | inverter |
| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter,channel |
| `ahoy_solar_YieldTotal_kilowattHours_total` | Counter | Energy converted to AC since reset [kWh] for all modules | inverter |
| `ahoy_solar_P_DC_watt` | Gauge | DC power of inverter [W] | 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_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_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_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_P_DC_watt` | Gauge | DC power of channel [P] | inverter, channel |
| `ahoy_solar_P_DC_watt_total` | Gauge | DC power of inverter [P] | inverter |
| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | 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_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_Irradiation_ratio` | Gauge | ratio DC Power over set maximum power per channel [%] | inverter, channel |

13
patches/AsyncWeb_Prometheus.patch

@ -1,3 +1,16 @@
diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp
index 12be5f8..cffeed7 100644
--- a/src/AsyncWebSocket.cpp
+++ b/src/AsyncWebSocket.cpp
@@ -737,7 +737,7 @@ void AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len)
IPAddress AsyncWebSocketClient::remoteIP() const
{
if (!_client)
- return IPAddress(0U);
+ return IPAddress();
return _client->remoteIP();
}
diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp
index 22a549f..e0b36b3 100644 index 22a549f..e0b36b3 100644
--- a/src/WebResponses.cpp --- a/src/WebResponses.cpp

360
patches/GxEPD2_SW_SPI.patch

@ -0,0 +1,360 @@
diff --git a/src/GxEPD2_EPD.cpp b/src/GxEPD2_EPD.cpp
index 1588444..592869b 100644
--- a/src/GxEPD2_EPD.cpp
+++ b/src/GxEPD2_EPD.cpp
@@ -19,9 +19,9 @@
GxEPD2_EPD::GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout,
uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu) :
- WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu),
+ WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu), _sck(-1), _mosi(-1),
_cs(cs), _dc(dc), _rst(rst), _busy(busy), _busy_level(busy_level), _busy_timeout(busy_timeout), _diag_enabled(false),
- _pSPIx(&SPI), _spi_settings(4000000, MSBFIRST, SPI_MODE0)
+ _spi_settings(4000000, MSBFIRST, SPI_MODE0)
{
_initial_write = true;
_initial_refresh = true;
@@ -71,27 +71,30 @@ void GxEPD2_EPD::init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset
{
pinMode(_busy, INPUT);
}
- _pSPIx->begin();
- if (_busy == MISO) // may be overridden
- {
- pinMode(_busy, INPUT);
- }
- if (_dc == MISO) // may be overridden, TTGO T5 V2.66
- {
- pinMode(_dc, OUTPUT);
- }
- if (_cs == MISO) // may be overridden
+ if (_sck < 0) SPI.begin();
+}
+
+void GxEPD2_EPD::init(int16_t sck, int16_t mosi, uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration, bool pulldown_rst_mode)
+{
+ if ((sck >= 0) && (mosi >= 0))
{
- pinMode(_cs, INPUT);
- }
+ _sck = sck;
+ _mosi = mosi;
+ digitalWrite(_sck, LOW);
+ digitalWrite(_mosi, LOW);
+ pinMode(_sck, OUTPUT);
+ pinMode(_mosi, OUTPUT);
+ } else _sck = -1;
+ init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode);
}
void GxEPD2_EPD::end()
{
- _pSPIx->end();
if (_cs >= 0) pinMode(_cs, INPUT);
if (_dc >= 0) pinMode(_dc, INPUT);
if (_rst >= 0) pinMode(_rst, INPUT);
+ if (_sck >= 0) pinMode(_sck, INPUT);
+ if (_mosi >= 0) pinMode(_mosi, INPUT);
}
void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter)
@@ -100,12 +103,6 @@ void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void*
_busy_callback_parameter = busy_callback_parameter;
}
-void GxEPD2_EPD::selectSPI(SPIClass& spi, SPISettings spi_settings)
-{
- _pSPIx = &spi;
- _spi_settings = spi_settings;
-}
-
void GxEPD2_EPD::_reset()
{
if (_rst >= 0)
@@ -174,115 +169,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
void GxEPD2_EPD::_writeCommand(uint8_t c)
{
- _pSPIx->beginTransaction(_spi_settings);
+ _beginTransaction(_spi_settings);
if (_dc >= 0) digitalWrite(_dc, LOW);
if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(c);
+ _spi_write(c);
if (_cs >= 0) digitalWrite(_cs, HIGH);
if (_dc >= 0) digitalWrite(_dc, HIGH);
- _pSPIx->endTransaction();
+ _endTransaction();
}
void GxEPD2_EPD::_writeData(uint8_t d)
{
- _pSPIx->beginTransaction(_spi_settings);
+ _beginTransaction(_spi_settings);
if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(d);
+ _spi_write(d);
if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _endTransaction();
}
void GxEPD2_EPD::_writeData(const uint8_t* data, uint16_t n)
{
- _pSPIx->beginTransaction(_spi_settings);
+ _beginTransaction(_spi_settings);
if (_cs >= 0) digitalWrite(_cs, LOW);
- for (uint16_t i = 0; i < n; i++)
+ for (uint8_t i = 0; i < n; i++)
{
- _pSPIx->transfer(*data++);
+ _spi_write(*data++);
}
if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _endTransaction();
}
void GxEPD2_EPD::_writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes)
{
- _pSPIx->beginTransaction(_spi_settings);
+ _beginTransaction(_spi_settings);
if (_cs >= 0) digitalWrite(_cs, LOW);
- for (uint16_t i = 0; i < n; i++)
+ for (uint8_t i = 0; i < n; i++)
{
- _pSPIx->transfer(pgm_read_byte(&*data++));
+ _spi_write(pgm_read_byte(&*data++));
}
while (fill_with_zeroes > 0)
{
- _pSPIx->transfer(0x00);
+ _spi_write(0x00);
fill_with_zeroes--;
}
if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _endTransaction();
}
void GxEPD2_EPD::_writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes)
{
- _pSPIx->beginTransaction(_spi_settings);
+ _beginTransaction(_spi_settings);
for (uint8_t i = 0; i < n; i++)
{
if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(pgm_read_byte(&*data++));
+ _spi_write(pgm_read_byte(&*data++));
if (_cs >= 0) digitalWrite(_cs, HIGH);
}
while (fill_with_zeroes > 0)
{
if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(0x00);
+ _spi_write(0x00);
fill_with_zeroes--;
if (_cs >= 0) digitalWrite(_cs, HIGH);
}
- _pSPIx->endTransaction();
+ _endTransaction();
}
void GxEPD2_EPD::_writeCommandData(const uint8_t* pCommandData, uint8_t datalen)
{
- _pSPIx->beginTransaction(_spi_settings);
+ _beginTransaction(_spi_settings);
if (_dc >= 0) digitalWrite(_dc, LOW);
if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(*pCommandData++);
+ _spi_write(*pCommandData++);
if (_dc >= 0) digitalWrite(_dc, HIGH);
for (uint8_t i = 0; i < datalen - 1; i++) // sub the command
{
- _pSPIx->transfer(*pCommandData++);
+ _spi_write(*pCommandData++);
}
if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _endTransaction();
}
void GxEPD2_EPD::_writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen)
{
- _pSPIx->beginTransaction(_spi_settings);
+ _beginTransaction(_spi_settings);
if (_dc >= 0) digitalWrite(_dc, LOW);
if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(pgm_read_byte(&*pCommandData++));
+ _spi_write(pgm_read_byte(&*pCommandData++));
if (_dc >= 0) digitalWrite(_dc, HIGH);
for (uint8_t i = 0; i < datalen - 1; i++) // sub the command
{
- _pSPIx->transfer(pgm_read_byte(&*pCommandData++));
+ _spi_write(pgm_read_byte(&*pCommandData++));
}
if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _endTransaction();
}
void GxEPD2_EPD::_startTransfer()
{
- _pSPIx->beginTransaction(_spi_settings);
+ _beginTransaction(_spi_settings);
if (_cs >= 0) digitalWrite(_cs, LOW);
}
void GxEPD2_EPD::_transfer(uint8_t value)
{
- _pSPIx->transfer(value);
+ _spi_write(value);
}
void GxEPD2_EPD::_endTransfer()
{
if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _endTransaction();
+}
+
+void GxEPD2_EPD::_beginTransaction(const SPISettings& settings)
+{
+ if (_sck < 0) SPI.beginTransaction(settings);
+}
+
+void GxEPD2_EPD::_spi_write(uint8_t data)
+{
+ if (_sck < 0) SPI.transfer(data);
+ else
+ {
+#if defined (ESP8266)
+ yield();
+#endif
+ for (int i = 0; i < 8; i++)
+ {
+ digitalWrite(_mosi, (data & 0x80) ? HIGH : LOW);
+ data <<= 1;
+ digitalWrite(_sck, HIGH);
+ digitalWrite(_sck, LOW);
+ }
+ }
+}
+
+void GxEPD2_EPD::_endTransaction()
+{
+ if (_sck < 0) SPI.endTransaction();
+}
+
+uint8_t GxEPD2_EPD::_readData()
+{
+ uint8_t data = 0;
+ _beginTransaction(_spi_settings);
+ if (_cs >= 0) digitalWrite(_cs, LOW);
+ if (_sck < 0)
+ {
+ data = SPI.transfer(0);
+ }
+ else
+ {
+ pinMode(_mosi, INPUT);
+ for (int i = 0; i < 8; i++)
+ {
+ data <<= 1;
+ digitalWrite(_sck, HIGH);
+ data |= digitalRead(_mosi);
+ digitalWrite(_sck, LOW);
+ }
+ pinMode(_mosi, OUTPUT);
+ }
+ if (_cs >= 0) digitalWrite(_cs, HIGH);
+ _endTransaction();
+ return data;
+}
+
+void GxEPD2_EPD::_readData(uint8_t* data, uint16_t n)
+{
+ _beginTransaction(_spi_settings);
+ if (_cs >= 0) digitalWrite(_cs, LOW);
+ if (_sck < 0)
+ {
+ for (uint8_t i = 0; i < n; i++)
+ {
+ *data++ = SPI.transfer(0);
+ }
+ }
+ else
+ {
+ pinMode(_mosi, INPUT);
+ for (uint8_t i = 0; i < n; i++)
+ {
+ *data = 0;
+ for (int i = 0; i < 8; i++)
+ {
+ *data <<= 1;
+ digitalWrite(_sck, HIGH);
+ *data |= digitalRead(_mosi);
+ digitalWrite(_sck, LOW);
+ }
+ data++;
+ }
+ pinMode(_mosi, OUTPUT);
+ }
+ if (_cs >= 0) digitalWrite(_cs, HIGH);
+ _endTransaction();
}
diff --git a/src/GxEPD2_EPD.h b/src/GxEPD2_EPD.h
index ef2318f..50aa961 100644
--- a/src/GxEPD2_EPD.h
+++ b/src/GxEPD2_EPD.h
@@ -8,6 +8,10 @@
// Version: see library.properties
//
// Library: https://github.com/ZinggJM/GxEPD2
+// To use SW SPI with GxEPD2:
+// add the special call to the added init method BEFORE the normal init method:
+// display.epd2.init(SW_SCK, SW_MOSI, 115200, true, 20, false); // define or replace SW_SCK, SW_MOSI
+// display.init(115200); // needed to init upper level
#ifndef _GxEPD2_EPD_H_
#define _GxEPD2_EPD_H_
@@ -35,6 +39,7 @@ class GxEPD2_EPD
uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu);
virtual void init(uint32_t serial_diag_bitrate = 0); // serial_diag_bitrate = 0 : disabled
virtual void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 10, bool pulldown_rst_mode = false);
+ virtual void init(int16_t sck, int16_t mosi, uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false);
virtual void end(); // release SPI and control pins
// Support for Bitmaps (Sprites) to Controller Buffer and to Screen
virtual void clearScreen(uint8_t value) = 0; // init controller memory and screen (default white)
@@ -97,7 +102,6 @@ class GxEPD2_EPD
{
return (a > b ? a : b);
};
- void selectSPI(SPIClass& spi, SPISettings spi_settings);
protected:
void _reset();
void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000);
@@ -111,9 +115,14 @@ class GxEPD2_EPD
void _startTransfer();
void _transfer(uint8_t value);
void _endTransfer();
+ void _beginTransaction(const SPISettings& settings);
+ void _spi_write(uint8_t data);
+ void _endTransaction();
+ public:
+ uint8_t _readData();
+ void _readData(uint8_t* data, uint16_t n);
protected:
- int16_t _cs, _dc, _rst, _busy, _busy_level;
+ int16_t _cs, _dc, _rst, _busy, _busy_level, _sck, _mosi;;
uint32_t _busy_timeout;
bool _diag_enabled, _pulldown_rst_mode;
- SPIClass* _pSPIx;
SPISettings _spi_settings;
@@ -123,5 +124,5 @@ class GxEPD2_EPD
uint16_t _reset_duration;
- void (*_busy_callback)(const void*);
+ void (*_busy_callback)(const void*);
const void* _busy_callback_parameter;
};

981
patches/RF24_Hal.patch

@ -0,0 +1,981 @@
diff --git a/RF24.cpp b/RF24.cpp
index c0cc732..b6708d9 100644
--- a/RF24.cpp
+++ b/RF24.cpp
@@ -12,228 +12,24 @@
/****************************************************************************/
-void RF24::csn(bool mode)
-{
-#if defined(RF24_TINY)
- if (ce_pin != csn_pin) {
- digitalWrite(csn_pin, mode);
- }
- else {
- if (mode == HIGH) {
- PORTB |= (1 << PINB2); // SCK->CSN HIGH
- delayMicroseconds(RF24_CSN_SETTLE_HIGH_DELAY); // allow csn to settle.
- }
- else {
- PORTB &= ~(1 << PINB2); // SCK->CSN LOW
- delayMicroseconds(RF24_CSN_SETTLE_LOW_DELAY); // allow csn to settle
- }
- }
- // Return, CSN toggle complete
- return;
-
-#elif defined(ARDUINO) && !defined(RF24_SPI_TRANSACTIONS)
- // Minimum ideal SPI bus speed is 2x data rate
- // If we assume 2Mbs data rate and 16Mhz clock, a
- // divider of 4 is the minimum we want.
- // CLK:BUS 8Mhz:2Mhz, 16Mhz:4Mhz, or 20Mhz:5Mhz
-
- #if !defined(SOFTSPI)
- // applies to SPI_UART and inherent hardware SPI
- #if defined(RF24_SPI_PTR)
- _spi->setBitOrder(MSBFIRST);
- _spi->setDataMode(SPI_MODE0);
-
- #if !defined(F_CPU) || F_CPU < 20000000
- _spi->setClockDivider(SPI_CLOCK_DIV2);
- #elif F_CPU < 40000000
- _spi->setClockDivider(SPI_CLOCK_DIV4);
- #elif F_CPU < 80000000
- _spi->setClockDivider(SPI_CLOCK_DIV8);
- #elif F_CPU < 160000000
- _spi->setClockDivider(SPI_CLOCK_DIV16);
- #elif F_CPU < 320000000
- _spi->setClockDivider(SPI_CLOCK_DIV32);
- #elif F_CPU < 640000000
- _spi->setClockDivider(SPI_CLOCK_DIV64);
- #elif F_CPU < 1280000000
- _spi->setClockDivider(SPI_CLOCK_DIV128);
- #else // F_CPU >= 1280000000
- #error "Unsupported CPU frequency. Please set correct SPI divider."
- #endif // F_CPU to SPI_CLOCK_DIV translation
-
- #else // !defined(RF24_SPI_PTR)
- _SPI.setBitOrder(MSBFIRST);
- _SPI.setDataMode(SPI_MODE0);
-
- #if !defined(F_CPU) || F_CPU < 20000000
- _SPI.setClockDivider(SPI_CLOCK_DIV2);
- #elif F_CPU < 40000000
- _SPI.setClockDivider(SPI_CLOCK_DIV4);
- #elif F_CPU < 80000000
- _SPI.setClockDivider(SPI_CLOCK_DIV8);
- #elif F_CPU < 160000000
- _SPI.setClockDivider(SPI_CLOCK_DIV16);
- #elif F_CPU < 320000000
- _SPI.setClockDivider(SPI_CLOCK_DIV32);
- #elif F_CPU < 640000000
- _SPI.setClockDivider(SPI_CLOCK_DIV64);
- #elif F_CPU < 1280000000
- _SPI.setClockDivider(SPI_CLOCK_DIV128);
- #else // F_CPU >= 1280000000
- #error "Unsupported CPU frequency. Please set correct SPI divider."
- #endif // F_CPU to SPI_CLOCK_DIV translation
- #endif // !defined(RF24_SPI_PTR)
- #endif // !defined(SOFTSPI)
-
-#elif defined(RF24_RPi)
- if (!mode)
- _SPI.chipSelect(csn_pin);
-#endif // defined(RF24_RPi)
-
-#if !defined(RF24_LINUX)
- digitalWrite(csn_pin, mode);
- delayMicroseconds(csDelay);
-#else
- static_cast<void>(mode); // ignore -Wunused-parameter
-#endif // !defined(RF24_LINUX)
-}
-
-/****************************************************************************/
-
void RF24::ce(bool level)
{
-#ifndef RF24_LINUX
- //Allow for 3-pin use on ATTiny
- if (ce_pin != csn_pin) {
-#endif
- digitalWrite(ce_pin, level);
-#ifndef RF24_LINUX
- }
-#endif
-}
-
-/****************************************************************************/
-
-inline void RF24::beginTransaction()
-{
-#if defined(RF24_SPI_TRANSACTIONS)
- #if defined(RF24_SPI_PTR)
- #if defined(RF24_RP2)
- _spi->beginTransaction(spi_speed);
- #else // ! defined (RF24_RP2)
- _spi->beginTransaction(SPISettings(spi_speed, MSBFIRST, SPI_MODE0));
- #endif // ! defined (RF24_RP2)
- #else // !defined(RF24_SPI_PTR)
- _SPI.beginTransaction(SPISettings(spi_speed, MSBFIRST, SPI_MODE0));
- #endif // !defined(RF24_SPI_PTR)
-#endif // defined (RF24_SPI_TRANSACTIONS)
- csn(LOW);
-}
-
-/****************************************************************************/
-
-inline void RF24::endTransaction()
-{
- csn(HIGH);
-#if defined(RF24_SPI_TRANSACTIONS)
- #if defined(RF24_SPI_PTR)
- _spi->endTransaction();
- #else // !defined(RF24_SPI_PTR)
- _SPI.endTransaction();
- #endif // !defined(RF24_SPI_PTR)
-#endif // defined (RF24_SPI_TRANSACTIONS)
+ hal->ce(level);
}
/****************************************************************************/
void RF24::read_register(uint8_t reg, uint8_t* buf, uint8_t len)
{
-#if defined(RF24_LINUX) || defined(RF24_RP2)
- beginTransaction(); //configures the spi settings for RPi, locks mutex and setting csn low
- uint8_t* prx = spi_rxbuff;
- uint8_t* ptx = spi_txbuff;
- uint8_t size = static_cast<uint8_t>(len + 1); // Add register value to transmit buffer
-
- *ptx++ = (R_REGISTER | reg);
-
- while (len--) {
- *ptx++ = RF24_NOP; // Dummy operation, just for reading
- }
-
- #if defined(RF24_RP2)
- _spi->transfernb((const uint8_t*)spi_txbuff, spi_rxbuff, size);
- #else // !defined (RF24_RP2)
- _SPI.transfernb(reinterpret_cast<char*>(spi_txbuff), reinterpret_cast<char*>(spi_rxbuff), size);
- #endif // !defined (RF24_RP2)
-
- status = *prx++; // status is 1st byte of receive buffer
-
- // decrement before to skip status byte
- while (--size) {
- *buf++ = *prx++;
- }
-
- endTransaction(); // unlocks mutex and setting csn high
-
-#else // !defined(RF24_LINUX) && !defined(RF24_RP2)
-
- beginTransaction();
- #if defined(RF24_SPI_PTR)
- status = _spi->transfer(R_REGISTER | reg);
- while (len--) {
- *buf++ = _spi->transfer(0xFF);
- }
-
- #else // !defined(RF24_SPI_PTR)
- status = _SPI.transfer(R_REGISTER | reg);
- while (len--) {
- *buf++ = _SPI.transfer(0xFF);
- }
-
- #endif // !defined(RF24_SPI_PTR)
- endTransaction();
-#endif // !defined(RF24_LINUX) && !defined(RF24_RP2)
+ status = hal->read(R_REGISTER | reg, buf, len);
}
/****************************************************************************/
uint8_t RF24::read_register(uint8_t reg)
{
- uint8_t result;
-
-#if defined(RF24_LINUX) || defined(RF24_RP2)
- beginTransaction();
-
- uint8_t* prx = spi_rxbuff;
- uint8_t* ptx = spi_txbuff;
- *ptx++ = (R_REGISTER | reg);
- *ptx++ = RF24_NOP; // Dummy operation, just for reading
-
- #if defined(RF24_RP2)
- _spi->transfernb((const uint8_t*)spi_txbuff, spi_rxbuff, 2);
- #else // !defined(RF24_RP2)
- _SPI.transfernb(reinterpret_cast<char*>(spi_txbuff), reinterpret_cast<char*>(spi_rxbuff), 2);
- #endif // !defined(RF24_RP2)
-
- status = *prx; // status is 1st byte of receive buffer
- result = *++prx; // result is 2nd byte of receive buffer
-
- endTransaction();
-#else // !defined(RF24_LINUX) && !defined(RF24_RP2)
-
- beginTransaction();
- #if defined(RF24_SPI_PTR)
- status = _spi->transfer(R_REGISTER | reg);
- result = _spi->transfer(0xff);
-
- #else // !defined(RF24_SPI_PTR)
- status = _SPI.transfer(R_REGISTER | reg);
- result = _SPI.transfer(0xff);
-
- #endif // !defined(RF24_SPI_PTR)
- endTransaction();
-#endif // !defined(RF24_LINUX) && !defined(RF24_RP2)
-
+ uint8_t result = 0xff;
+ status = hal->read(R_REGISTER | reg, &result, sizeof(result));
return result;
}
@@ -241,43 +37,7 @@ uint8_t RF24::read_register(uint8_t reg)
void RF24::write_register(uint8_t reg, const uint8_t* buf, uint8_t len)
{
-#if defined(RF24_LINUX) || defined(RF24_RP2)
- beginTransaction();
- uint8_t* prx = spi_rxbuff;
- uint8_t* ptx = spi_txbuff;
- uint8_t size = static_cast<uint8_t>(len + 1); // Add register value to transmit buffer
-
- *ptx++ = (W_REGISTER | (REGISTER_MASK & reg));
- while (len--) {
- *ptx++ = *buf++;
- }
-
- #if defined(RF24_RP2)
- _spi->transfernb((const uint8_t*)spi_txbuff, spi_rxbuff, size);
- #else // !defined(RF24_RP2)
- _SPI.transfernb(reinterpret_cast<char*>(spi_txbuff), reinterpret_cast<char*>(spi_rxbuff), size);
- #endif // !defined(RF24_RP2)
-
- status = *prx; // status is 1st byte of receive buffer
- endTransaction();
-#else // !defined(RF24_LINUX) && !defined(RF24_RP2)
-
- beginTransaction();
- #if defined(RF24_SPI_PTR)
- status = _spi->transfer(W_REGISTER | reg);
- while (len--) {
- _spi->transfer(*buf++);
- }
-
- #else // !defined(RF24_SPI_PTR)
- status = _SPI.transfer(W_REGISTER | reg);
- while (len--) {
- _SPI.transfer(*buf++);
- }
-
- #endif // !defined(RF24_SPI_PTR)
- endTransaction();
-#endif // !defined(RF24_LINUX) && !defined(RF24_RP2)
+ status = hal->write(W_REGISTER | reg, buf, len);
}
/****************************************************************************/
@@ -288,47 +48,11 @@ void RF24::write_register(uint8_t reg, uint8_t value, bool is_cmd_only)
if (reg != RF24_NOP) { // don't print the get_status() operation
IF_SERIAL_DEBUG(printf_P(PSTR("write_register(%02x)\r\n"), reg));
}
- beginTransaction();
-#if defined(RF24_LINUX)
- status = _SPI.transfer(W_REGISTER | reg);
-#else // !defined(RF24_LINUX) || defined (RF24_RP2)
- #if defined(RF24_SPI_PTR)
- status = _spi->transfer(W_REGISTER | reg);
- #else // !defined (RF24_SPI_PTR)
- status = _SPI.transfer(W_REGISTER | reg);
- #endif // !defined (RF24_SPI_PTR)
-#endif // !defined(RF24_LINUX) || defined(RF24_RP2)
- endTransaction();
+ status = hal->write(W_REGISTER | reg, nullptr, 0);
}
else {
IF_SERIAL_DEBUG(printf_P(PSTR("write_register(%02x,%02x)\r\n"), reg, value));
-#if defined(RF24_LINUX) || defined(RF24_RP2)
- beginTransaction();
- uint8_t* prx = spi_rxbuff;
- uint8_t* ptx = spi_txbuff;
- *ptx++ = (W_REGISTER | reg);
- *ptx = value;
-
- #if defined(RF24_RP2)
- _spi->transfernb((const uint8_t*)spi_txbuff, spi_rxbuff, 2);
- #else // !defined(RF24_RP2)
- _SPI.transfernb(reinterpret_cast<char*>(spi_txbuff), reinterpret_cast<char*>(spi_rxbuff), 2);
- #endif // !defined(RF24_RP2)
-
- status = *prx++; // status is 1st byte of receive buffer
- endTransaction();
-#else // !defined(RF24_LINUX) && !defined(RF24_RP2)
-
- beginTransaction();
- #if defined(RF24_SPI_PTR)
- status = _spi->transfer(W_REGISTER | reg);
- _spi->transfer(value);
- #else // !defined(RF24_SPI_PTR)
- status = _SPI.transfer(W_REGISTER | reg);
- _SPI.transfer(value);
- #endif // !defined(RF24_SPI_PTR)
- endTransaction();
-#endif // !defined(RF24_LINUX) && !defined(RF24_RP2)
+ status = hal->write(W_REGISTER | reg, &value, sizeof(value));
}
}
@@ -347,60 +71,8 @@ void RF24::write_payload(const void* buf, uint8_t data_len, const uint8_t writeT
data_len = rf24_min(data_len, static_cast<uint8_t>(32));
}
- //printf("[Writing %u bytes %u blanks]",data_len,blank_len);
IF_SERIAL_DEBUG(printf("[Writing %u bytes %u blanks]\n", data_len, blank_len););
-
-#if defined(RF24_LINUX) || defined(RF24_RP2)
- beginTransaction();
- uint8_t* prx = spi_rxbuff;
- uint8_t* ptx = spi_txbuff;
- uint8_t size;
- size = static_cast<uint8_t>(data_len + blank_len + 1); // Add register value to transmit buffer
-
- *ptx++ = writeType;
- while (data_len--) {
- *ptx++ = *current++;
- }
-
- while (blank_len--) {
- *ptx++ = 0;
- }
-
- #if defined(RF24_RP2)
- _spi->transfernb((const uint8_t*)spi_txbuff, spi_rxbuff, size);
- #else // !defined(RF24_RP2)
- _SPI.transfernb(reinterpret_cast<char*>(spi_txbuff), reinterpret_cast<char*>(spi_rxbuff), size);
- #endif // !defined(RF24_RP2)
-
- status = *prx; // status is 1st byte of receive buffer
- endTransaction();
-
-#else // !defined(RF24_LINUX) && !defined(RF24_RP2)
-
- beginTransaction();
- #if defined(RF24_SPI_PTR)
- status = _spi->transfer(writeType);
- while (data_len--) {
- _spi->transfer(*current++);
- }
-
- while (blank_len--) {
- _spi->transfer(0);
- }
-
- #else // !defined(RF24_SPI_PTR)
- status = _SPI.transfer(writeType);
- while (data_len--) {
- _SPI.transfer(*current++);
- }
-
- while (blank_len--) {
- _SPI.transfer(0);
- }
-
- #endif // !defined(RF24_SPI_PTR)
- endTransaction();
-#endif // !defined(RF24_LINUX) && !defined(RF24_RP2)
+ status = hal->write(writeType, current, data_len, blank_len);
}
/****************************************************************************/
@@ -421,65 +93,7 @@ void RF24::read_payload(void* buf, uint8_t data_len)
//printf("[Reading %u bytes %u blanks]",data_len,blank_len);
IF_SERIAL_DEBUG(printf("[Reading %u bytes %u blanks]\n", data_len, blank_len););
-
-#if defined(RF24_LINUX) || defined(RF24_RP2)
- beginTransaction();
- uint8_t* prx = spi_rxbuff;
- uint8_t* ptx = spi_txbuff;
- uint8_t size;
- size = static_cast<uint8_t>(data_len + blank_len + 1); // Add register value to transmit buffer
-
- *ptx++ = R_RX_PAYLOAD;
- while (--size) {
- *ptx++ = RF24_NOP;
- }
-
- size = static_cast<uint8_t>(data_len + blank_len + 1); // Size has been lost during while, re affect
-
- #if defined(RF24_RP2)
- _spi->transfernb((const uint8_t*)spi_txbuff, spi_rxbuff, size);
- #else // !defined(RF24_RP2)
- _SPI.transfernb(reinterpret_cast<char*>(spi_txbuff), reinterpret_cast<char*>(spi_rxbuff), size);
- #endif // !defined(RF24_RP2)
-
- status = *prx++; // 1st byte is status
-
- if (data_len > 0) {
- // Decrement before to skip 1st status byte
- while (--data_len) {
- *current++ = *prx++;
- }
-
- *current = *prx;
- }
- endTransaction();
-#else // !defined(RF24_LINUX) && !defined(RF24_RP2)
-
- beginTransaction();
- #if defined(RF24_SPI_PTR)
- status = _spi->transfer(R_RX_PAYLOAD);
- while (data_len--) {
- *current++ = _spi->transfer(0xFF);
- }
-
- while (blank_len--) {
- _spi->transfer(0xFF);
- }
-
- #else // !defined(RF24_SPI_PTR)
- status = _SPI.transfer(R_RX_PAYLOAD);
- while (data_len--) {
- *current++ = _SPI.transfer(0xFF);
- }
-
- while (blank_len--) {
- _SPI.transfer(0xff);
- }
-
- #endif // !defined(RF24_SPI_PTR)
- endTransaction();
-
-#endif // !defined(RF24_LINUX) && !defined(RF24_RP2)
+ status = hal->read(R_RX_PAYLOAD, current, data_len, blank_len);
}
/****************************************************************************/
@@ -577,16 +191,16 @@ uint8_t RF24::sprintf_address_register(char* out_buffer, uint8_t reg, uint8_t qt
/****************************************************************************/
-RF24::RF24(rf24_gpio_pin_t _cepin, rf24_gpio_pin_t _cspin, uint32_t _spi_speed)
- : ce_pin(_cepin), csn_pin(_cspin), spi_speed(_spi_speed), payload_size(32), _is_p_variant(false), _is_p0_rx(false), addr_width(5), dynamic_payloads_enabled(true), csDelay(5)
+RF24::RF24(RF24_hal* _hal)
+ : hal(_hal), payload_size(32), _is_p_variant(false), _is_p0_rx(false), addr_width(5), dynamic_payloads_enabled(true), csDelay(5)
{
_init_obj();
}
/****************************************************************************/
-RF24::RF24(uint32_t _spi_speed)
- : ce_pin(RF24_PIN_INVALID), csn_pin(RF24_PIN_INVALID), spi_speed(_spi_speed), payload_size(32), _is_p_variant(false), _is_p0_rx(false), addr_width(5), dynamic_payloads_enabled(true), csDelay(5)
+RF24::RF24()
+ : hal(nullptr), payload_size(32), _is_p_variant(false), _is_p0_rx(false), addr_width(5), dynamic_payloads_enabled(true), csDelay(5)
{
_init_obj();
}
@@ -595,16 +209,7 @@ RF24::RF24(uint32_t _spi_speed)
void RF24::_init_obj()
{
- // Use a pointer on the Arduino platform
-
-#if defined(RF24_SPI_PTR) && !defined(RF24_RP2)
- _spi = &SPI;
-#endif // defined (RF24_SPI_PTR)
-
pipe0_reading_address[0] = 0;
- if (spi_speed <= 35000) { //Handle old BCM2835 speed constants, default to RF24_SPI_SPEED
- spi_speed = RF24_SPI_SPEED;
- }
}
/****************************************************************************/
@@ -677,19 +282,6 @@ static const PROGMEM char* const rf24_pa_dbm_e_str_P[] = {
rf24_pa_dbm_e_str_3,
};
- #if defined(RF24_LINUX)
-static const char rf24_csn_e_str_0[] = "CE0 (PI Hardware Driven)";
-static const char rf24_csn_e_str_1[] = "CE1 (PI Hardware Driven)";
-static const char rf24_csn_e_str_2[] = "CE2 (PI Hardware Driven)";
-static const char rf24_csn_e_str_3[] = "Custom GPIO Software Driven";
-static const char* const rf24_csn_e_str_P[] = {
- rf24_csn_e_str_0,
- rf24_csn_e_str_1,
- rf24_csn_e_str_2,
- rf24_csn_e_str_3,
-};
- #endif // defined(RF24_LINUX)
-
static const PROGMEM char rf24_feature_e_str_on[] = "= Enabled";
static const PROGMEM char rf24_feature_e_str_allowed[] = "= Allowed";
static const PROGMEM char rf24_feature_e_str_open[] = " open ";
@@ -704,19 +296,6 @@ static const PROGMEM char* const rf24_feature_e_str_P[] = {
void RF24::printDetails(void)
{
-
- #if defined(RF24_LINUX)
- printf("================ SPI Configuration ================\n");
- uint8_t bus_ce = static_cast<uint8_t>(csn_pin % 10);
- uint8_t bus_numb = static_cast<uint8_t>((csn_pin - bus_ce) / 10);
- printf("CSN Pin\t\t= /dev/spidev%d.%d\n", bus_numb, bus_ce);
- printf("CE Pin\t\t= Custom GPIO%d\n", ce_pin);
- #endif
- printf_P(PSTR("SPI Speedz\t= %d Mhz\n"), static_cast<uint8_t>(spi_speed / 1000000)); //Print the SPI speed on non-Linux devices
- #if defined(RF24_LINUX)
- printf("================ NRF Configuration ================\n");
- #endif // defined(RF24_LINUX)
-
print_status(get_status());
print_address_register(PSTR("RX_ADDR_P0-1"), RX_ADDR_P0, 2);
@@ -748,19 +327,6 @@ void RF24::printDetails(void)
void RF24::printPrettyDetails(void)
{
-
- #if defined(RF24_LINUX)
- printf("================ SPI Configuration ================\n");
- uint8_t bus_ce = static_cast<uint8_t>(csn_pin % 10);
- uint8_t bus_numb = static_cast<uint8_t>((csn_pin - bus_ce) / 10);
- printf("CSN Pin\t\t\t= /dev/spidev%d.%d\n", bus_numb, bus_ce);
- printf("CE Pin\t\t\t= Custom GPIO%d\n", ce_pin);
- #endif
- printf_P(PSTR("SPI Frequency\t\t= %d Mhz\n"), static_cast<uint8_t>(spi_speed / 1000000)); //Print the SPI speed on non-Linux devices
- #if defined(RF24_LINUX)
- printf("================ NRF Configuration ================\n");
- #endif // defined(RF24_LINUX)
-
uint8_t channel = getChannel();
uint16_t frequency = static_cast<uint16_t>(channel + 2400);
printf_P(PSTR("Channel\t\t\t= %u (~ %u MHz)\r\n"), channel, frequency);
@@ -846,11 +412,6 @@ void RF24::printPrettyDetails(void)
uint16_t RF24::sprintfPrettyDetails(char* debugging_information)
{
const char* format_string = PSTR(
- "================ SPI Configuration ================\n"
- "CSN Pin\t\t\t= %d\n"
- "CE Pin\t\t\t= %d\n"
- "SPI Frequency\t\t= %d Mhz\n"
- "================ NRF Configuration ================\n"
"Channel\t\t\t= %u (~ %u MHz)\n"
"RF Data Rate\t\t" PRIPSTR "\n"
"RF Power Amplifier\t" PRIPSTR "\n"
@@ -870,8 +431,7 @@ uint16_t RF24::sprintfPrettyDetails(char* debugging_information)
const char* format_str3 = PSTR("\nPipe %d (" PRIPSTR ") bound\t= 0x");
uint16_t offset = sprintf_P(
- debugging_information, format_string, csn_pin, ce_pin,
- static_cast<uint8_t>(spi_speed / 1000000), getChannel(),
+ debugging_information, format_string, getChannel(),
static_cast<uint16_t>(getChannel() + 2400),
(char*)(pgm_read_ptr(&rf24_datarate_e_str_P[getDataRate()])),
(char*)(pgm_read_ptr(&rf24_pa_dbm_e_str_P[getPALevel()])),
@@ -940,87 +500,26 @@ void RF24::encodeRadioDetails(uint8_t* encoded_details)
*encoded_details++ = read_register(i);
}
}
- *encoded_details++ = ce_pin >> 4;
- *encoded_details++ = ce_pin & 0xFF;
- *encoded_details++ = csn_pin >> 4;
- *encoded_details++ = csn_pin & 0xFF;
- *encoded_details = static_cast<uint8_t>((spi_speed / 1000000) | _BV(_is_p_variant * 4));
}
#endif // !defined(MINIMAL)
/****************************************************************************/
-#if defined(RF24_SPI_PTR) || defined(DOXYGEN_FORCED)
-// does not apply to RF24_LINUX
-bool RF24::begin(_SPI* spiBus)
+bool RF24::begin(void)
{
- _spi = spiBus;
return _init_pins() && _init_radio();
}
/****************************************************************************/
-bool RF24::begin(_SPI* spiBus, rf24_gpio_pin_t _cepin, rf24_gpio_pin_t _cspin)
+bool RF24::begin(RF24_hal* _hal)
{
- ce_pin = _cepin;
- csn_pin = _cspin;
- return begin(spiBus);
-}
-
-#endif // defined (RF24_SPI_PTR) || defined (DOXYGEN_FORCED)
-
-/****************************************************************************/
-
-bool RF24::begin(rf24_gpio_pin_t _cepin, rf24_gpio_pin_t _cspin)
-{
- ce_pin = _cepin;
- csn_pin = _cspin;
+ hal = _hal;
return begin();
}
/****************************************************************************/
-bool RF24::begin(void)
-{
-#if defined(RF24_LINUX)
- #if defined(RF24_RPi)
- switch (csn_pin) { // Ensure valid hardware CS pin
- case 0: break;
- case 1: break;
- // Allow BCM2835 enums for RPi
- case 8: csn_pin = 0; break;
- case 7: csn_pin = 1; break;
- case 18: csn_pin = 10; break; // to make it work on SPI1
- case 17: csn_pin = 11; break;
- case 16: csn_pin = 12; break;
- default: csn_pin = 0; break;
- }
- #endif // RF24_RPi
-
- _SPI.begin(csn_pin, spi_speed);
-
-#elif defined(XMEGA_D3)
- _spi->begin(csn_pin);
-
-#elif defined(RF24_RP2)
- _spi = new SPI();
- _spi->begin(PICO_DEFAULT_SPI ? spi1 : spi0);
-
-#else // using an Arduino platform || defined (LITTLEWIRE)
-
- #if defined(RF24_SPI_PTR)
- _spi->begin();
- #else // !defined(RF24_SPI_PTR)
- _SPI.begin();
- #endif // !defined(RF24_SPI_PTR)
-
-#endif // !defined(XMEGA_D3) && !defined(RF24_LINUX)
-
- return _init_pins() && _init_radio();
-}
-
-/****************************************************************************/
-
bool RF24::_init_pins()
{
if (!isValid()) {
@@ -1028,46 +527,7 @@ bool RF24::_init_pins()
return false;
}
-#if defined(RF24_LINUX)
-
- #if defined(MRAA)
- GPIO();
- gpio.begin(ce_pin, csn_pin);
- #endif
-
- pinMode(ce_pin, OUTPUT);
- ce(LOW);
- delay(100);
-
-#elif defined(LITTLEWIRE)
- pinMode(csn_pin, OUTPUT);
- csn(HIGH);
-
-#elif defined(XMEGA_D3)
- if (ce_pin != csn_pin) {
- pinMode(ce_pin, OUTPUT);
- };
- ce(LOW);
- csn(HIGH);
- delay(200);
-
-#else // using an Arduino platform
-
- // Initialize pins
- if (ce_pin != csn_pin) {
- pinMode(ce_pin, OUTPUT);
- pinMode(csn_pin, OUTPUT);
- }
-
- ce(LOW);
- csn(HIGH);
-
- #if defined(__ARDUINO_X86__)
- delay(100);
- #endif
-#endif // !defined(XMEGA_D3) && !defined(LITTLEWIRE) && !defined(RF24_LINUX)
-
- return true; // assuming pins are connected properly
+ return hal->begin();
}
/****************************************************************************/
@@ -1151,7 +611,7 @@ bool RF24::isChipConnected()
bool RF24::isValid()
{
- return ce_pin != RF24_PIN_INVALID && csn_pin != RF24_PIN_INVALID;
+ return hal != nullptr;
}
/****************************************************************************/
@@ -1676,15 +1136,8 @@ void RF24::closeReadingPipe(uint8_t pipe)
void RF24::toggle_features(void)
{
- beginTransaction();
-#if defined(RF24_SPI_PTR)
- status = _spi->transfer(ACTIVATE);
- _spi->transfer(0x73);
-#else
- status = _SPI.transfer(ACTIVATE);
- _SPI.transfer(0x73);
-#endif
- endTransaction();
+ uint8_t value = 0x73;
+ status = hal->write(ACTIVATE, &value, sizeof(value));
}
/****************************************************************************/
diff --git a/RF24.h b/RF24.h
index dbd32ae..f774bba 100644
--- a/RF24.h
+++ b/RF24.h
@@ -16,12 +16,7 @@
#define __RF24_H__
#include "RF24_config.h"
-
-#if defined(RF24_LINUX) || defined(LITTLEWIRE)
- #include "utility/includes.h"
-#elif defined SOFTSPI
- #include <DigitalIO.h>
-#endif
+#include "RF24_hal.h"
/**
* @defgroup PALevel Power Amplifier level
@@ -115,29 +110,8 @@ typedef enum
class RF24
{
private:
-#ifdef SOFTSPI
- SoftSPI<SOFT_SPI_MISO_PIN, SOFT_SPI_MOSI_PIN, SOFT_SPI_SCK_PIN, SPI_MODE> spi;
-#elif defined(SPI_UART)
- SPIUARTClass uspi;
-#endif
-
-#if defined(RF24_LINUX) || defined(XMEGA_D3) /* XMEGA can use SPI class */
- SPI spi;
-#endif // defined (RF24_LINUX) || defined (XMEGA_D3)
-#if defined(RF24_SPI_PTR)
- _SPI* _spi;
-#endif // defined (RF24_SPI_PTR)
-#if defined(MRAA)
- GPIO gpio;
-#endif
+ RF24_hal *hal;
- rf24_gpio_pin_t ce_pin; /* "Chip Enable" pin, activates the RX or TX role */
- rf24_gpio_pin_t csn_pin; /* SPI Chip select */
- uint32_t spi_speed; /* SPI Bus Speed */
-#if defined(RF24_LINUX) || defined(XMEGA_D3) || defined(RF24_RP2)
- uint8_t spi_rxbuff[32 + 1]; //SPI receive buffer (payload max 32 bytes)
- uint8_t spi_txbuff[32 + 1]; //SPI transmit buffer (payload max 32 bytes + 1 byte for the command)
-#endif
uint8_t status; /* The status byte returned from every SPI transaction */
uint8_t payload_size; /* Fixed size of payloads */
uint8_t pipe0_reading_address[5]; /* Last address set on pipe 0 for reading. */
@@ -146,16 +120,6 @@ private:
bool _is_p0_rx; /* For keeping track of pipe 0's usage in user-triggered RX mode. */
protected:
- /**
- * SPI transactions
- *
- * Common code for SPI transactions including CSN toggle
- *
- */
- inline void beginTransaction();
-
- inline void endTransaction();
-
/** Whether ack payloads are enabled. */
bool ack_payloads_enabled;
/** The address width to use (3, 4 or 5 bytes). */
@@ -198,30 +162,15 @@ public:
*
* See [Related Pages](pages.html) for device specific information
*
- * @param _cepin The pin attached to Chip Enable on the RF module
- * @param _cspin The pin attached to Chip Select (often labeled CSN) on the radio module.
- * - For the Arduino Due board, the [Arduino Due extended SPI feature](https://www.arduino.cc/en/Reference/DueExtendedSPI)
- * is not supported. This means that the Due's pins 4, 10, or 52 are not mandated options (can use any digital output pin) for
- * the radio's CSN pin.
- * @param _spi_speed The SPI speed in Hz ie: 1000000 == 1Mhz
- * - Users can specify default SPI speed by modifying @ref RF24_SPI_SPEED in @ref RF24_config.h
- * - For Arduino, the default SPI speed will only be properly configured this way on devices supporting SPI TRANSACTIONS
- * - Older/Unsupported Arduino devices will use a default clock divider & settings configuration
- * - For Linux: The old way of setting SPI speeds using BCM2835 driver enums has been removed as of v1.3.7
+ * @param _hal A pointer to the device specific hardware abstraction layer
*/
- RF24(rf24_gpio_pin_t _cepin, rf24_gpio_pin_t _cspin, uint32_t _spi_speed = RF24_SPI_SPEED);
+ RF24(RF24_hal *_hal);
/**
* A constructor for initializing the radio's hardware dynamically
- * @warning You MUST use begin(rf24_gpio_pin_t, rf24_gpio_pin_t) or begin(_SPI*, rf24_gpio_pin_t, rf24_gpio_pin_t) to pass both the
- * digital output pin numbers connected to the radio's CE and CSN pins.
- * @param _spi_speed The SPI speed in Hz ie: 1000000 == 1Mhz
- * - Users can specify default SPI speed by modifying @ref RF24_SPI_SPEED in @ref RF24_config.h
- * - For Arduino, the default SPI speed will only be properly configured this way on devices supporting SPI TRANSACTIONS
- * - Older/Unsupported Arduino devices will use a default clock divider & settings configuration
- * - For Linux: The old way of setting SPI speeds using BCM2835 driver enums has been removed as of v1.3.7
+ * @warning You MUST use begin(RF24_hal*)
*/
- RF24(uint32_t _spi_speed = RF24_SPI_SPEED);
+ RF24(void);
#if defined(RF24_LINUX)
virtual ~RF24() {};
@@ -243,58 +192,16 @@ public:
*/
bool begin(void);
-#if defined(RF24_SPI_PTR) || defined(DOXYGEN_FORCED)
/**
* Same as begin(), but allows specifying a non-default SPI bus to use.
*
- * @note This function assumes the `SPI::begin()` method was called before to
- * calling this function.
- *
- * @warning This function is for the Arduino platforms only
- *
- * @param spiBus A pointer or reference to an instantiated SPI bus object.
- * The `_SPI` datatype is a "wrapped" definition that will represent
- * various SPI implementations based on the specified platform.
- * @see Review the [Arduino support page](md_docs_arduino.html).
- *
- * @return same result as begin()
- */
- bool begin(_SPI* spiBus);
-
- /**
- * Same as begin(), but allows dynamically specifying a SPI bus, CE pin,
- * and CSN pin to use.
- *
- * @note This function assumes the `SPI::begin()` method was called before to
- * calling this function.
- *
* @warning This function is for the Arduino platforms only
*
- * @param spiBus A pointer or reference to an instantiated SPI bus object.
- * The `_SPI` datatype is a "wrapped" definition that will represent
- * various SPI implementations based on the specified platform.
- * @param _cepin The pin attached to Chip Enable on the RF module
- * @param _cspin The pin attached to Chip Select (often labeled CSN) on the radio module.
- * - For the Arduino Due board, the [Arduino Due extended SPI feature](https://www.arduino.cc/en/Reference/DueExtendedSPI)
- * is not supported. This means that the Due's pins 4, 10, or 52 are not mandated options (can use any digital output pin) for the radio's CSN pin.
+ * @param _hal A pointer to the device specific hardware abstraction layer
*
- * @see Review the [Arduino support page](md_docs_arduino.html).
- *
- * @return same result as begin()
- */
- bool begin(_SPI* spiBus, rf24_gpio_pin_t _cepin, rf24_gpio_pin_t _cspin);
-#endif // defined (RF24_SPI_PTR) || defined (DOXYGEN_FORCED)
-
- /**
- * Same as begin(), but allows dynamically specifying a CE pin
- * and CSN pin to use.
- * @param _cepin The pin attached to Chip Enable on the RF module
- * @param _cspin The pin attached to Chip Select (often labeled CSN) on the radio module.
- * - For the Arduino Due board, the [Arduino Due extended SPI feature](https://www.arduino.cc/en/Reference/DueExtendedSPI)
- * is not supported. This means that the Due's pins 4, 10, or 52 are not mandated options (can use any digital output pin) for the radio's CSN pin.
* @return same result as begin()
*/
- bool begin(rf24_gpio_pin_t _cepin, rf24_gpio_pin_t _cspin);
+ bool begin(RF24_hal* _hal);
/**
* Checks if the chip is connected to the SPI bus
@@ -667,12 +574,12 @@ public:
* This function uses much less ram than other `*print*Details()` methods.
*
* @code
- * uint8_t encoded_details[43] = {0};
+ * uint8_t encoded_details[38] = {0};
* radio.encodeRadioDetails(encoded_details);
* @endcode
*
* @param encoded_status The uint8_t array that RF24 radio details are
- * encoded into. This array must be at least 43 bytes in length; any less would surely
+ * encoded into. This array must be at least 38 bytes in length; any less would surely
* cause undefined behavior.
*
* Registers names and/or data corresponding to the index of the `encoded_details` array:
@@ -704,9 +611,6 @@ public:
* | 35 | FIFO_STATUS |
* | 36 | DYNPD |
* | 37 | FEATURE |
- * | 38-39 | ce_pin |
- * | 40-41 | csn_pin |
- * | 42 | SPI speed (in MHz) or'd with (isPlusVariant << 4) |
*/
void encodeRadioDetails(uint8_t* encoded_status);
@@ -1896,18 +1800,6 @@ private:
*/
bool _init_pins();
- /**
- * Set chip select pin
- *
- * Running SPI bus at PI_CLOCK_DIV2 so we don't waste time transferring data
- * and best of all, we make use of the radio's FIFO buffers. A lower speed
- * means we're less likely to effectively leverage our FIFOs and pay a higher
- * AVR runtime cost as toll.
- *
- * @param mode HIGH to take this unit off the SPI bus, LOW to put it on
- */
- void csn(bool mode);
-
/**
* Set chip enable
*
diff --git a/RF24_hal.cpp b/RF24_hal.cpp
new file mode 100644
index 0000000..3cc78e4
--- /dev/null
+++ b/RF24_hal.cpp
@@ -0,0 +1 @@
+#include "RF24_hal.h"
diff --git a/RF24_hal.h b/RF24_hal.h
new file mode 100644
index 0000000..baceab3
--- /dev/null
+++ b/RF24_hal.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "RF24_config.h"
+
+class RF24_hal
+{
+public:
+ virtual void ce(bool level) = 0;
+ virtual uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t len) = 0;
+ virtual uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t data_len, uint8_t blank_len) = 0;
+ virtual uint8_t write(uint8_t cmd, const uint8_t* buf, uint8_t len) = 0;
+ virtual uint8_t write(uint8_t cmd, const uint8_t* buf, uint8_t len, uint8_t blank_len) = 0;
+ virtual bool begin() = 0;
+ virtual void end() = 0;
+};

BIN
pics/PXL_20230824_204200660.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
pics/PXL_20230901_061927908.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

13
scripts/applyPatches.py

@ -3,7 +3,11 @@ import subprocess
Import("env") Import("env")
def applyPatch(libName, patchFile): def applyPatch(libName, patchFile):
# save current wd
start = os.getcwd()
if os.path.exists('.pio/libdeps/' + env['PIOENV'] + '/' + libName) == False: if os.path.exists('.pio/libdeps/' + env['PIOENV'] + '/' + libName) == False:
print("path '" + '.pio/libdeps/' + env['PIOENV'] + '/' + libName + "' does not exist")
return return
os.chdir('.pio/libdeps/' + env['PIOENV'] + '/' + libName) os.chdir('.pio/libdeps/' + env['PIOENV'] + '/' + libName)
@ -18,6 +22,13 @@ def applyPatch(libName, patchFile):
else: else:
print('applying \'' + patchFile + '\' failed') print('applying \'' + patchFile + '\' failed')
os.chdir(start)
# list of patches to apply (relative to /src) # list of patches to apply (relative to /src)
applyPatch("ESP Async WebServer", "../patches/AsyncWeb_Prometheus.patch") if env['PIOENV'][:22] != "opendtufusion-ethernet":
applyPatch("ESP Async WebServer", "../patches/AsyncWeb_Prometheus.patch")
if env['PIOENV'][:13] == "opendtufusion":
applyPatch("GxEPD2", "../patches/GxEPD2_SW_SPI.patch")
applyPatch("RF24", "../patches/RF24_Hal.patch")

10
scripts/buildManifest.py

@ -33,16 +33,16 @@ def buildManifest(path, infile, outfile):
esp32 = {} esp32 = {}
esp32["chipFamily"] = "ESP32" esp32["chipFamily"] = "ESP32"
esp32["parts"] = [] esp32["parts"] = []
esp32["parts"].append({"path": "bootloader.bin", "offset": 4096}) esp32["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096})
esp32["parts"].append({"path": "partitions.bin", "offset": 32768}) esp32["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768})
esp32["parts"].append({"path": "ota.bin", "offset": 57344}) esp32["parts"].append({"path": "ESP32/ota.bin", "offset": 57344})
esp32["parts"].append({"path": version[1] + "_" + sha + "_esp32.bin", "offset": 65536}) esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32.bin", "offset": 65536})
data["builds"].append(esp32) data["builds"].append(esp32)
esp8266 = {} esp8266 = {}
esp8266["chipFamily"] = "ESP8266" esp8266["chipFamily"] = "ESP8266"
esp8266["parts"] = [] esp8266["parts"] = []
esp8266["parts"].append({"path": version[1] + "_" + sha + "_esp8266.bin", "offset": 0}) esp8266["parts"].append({"path": "ESP8266/" + version[1] + "_" + sha + "_esp8266.bin", "offset": 0})
data["builds"].append(esp8266) data["builds"].append(esp8266)
jsonString = json.dumps(data, indent=2) jsonString = json.dumps(data, indent=2)

38
src/web/html/convert.py → scripts/convertHtml.py

@ -6,6 +6,7 @@ import shutil
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
import subprocess import subprocess
Import("env")
def get_git_sha(): def get_git_sha():
@ -60,11 +61,39 @@ def htmlParts(file, header, nav, footer, version):
link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>' link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>'
p = p.replace("{#VERSION}", version) p = p.replace("{#VERSION}", version)
p = p.replace("{#VERSION_GIT}", link) p = p.replace("{#VERSION_GIT}", link)
# remove if - endif ESP32
p = checkIf(p)
f = open("tmp/" + file, "w") f = open("tmp/" + file, "w")
f.write(p); f.write(p);
f.close(); f.close();
return p return p
def checkIf(data):
if (env['PIOENV'][0:5] == "esp32") or env['PIOENV'][0:4] == "open":
data = data.replace("<!--IF_ESP32-->", "")
data = data.replace("<!--ENDIF_ESP32-->", "")
data = data.replace("/*IF_ESP32*/", "")
data = data.replace("/*ENDIF_ESP32*/", "")
else:
while 1:
start = data.find("<!--IF_ESP32-->")
end = data.find("<!--ENDIF_ESP32-->")+18
if -1 == start:
break
else:
data = data[0:start] + data[end:]
while 1:
start = data.find("/*IF_ESP32*/")
end = data.find("/*ENDIF_ESP32*/")+15
if -1 == start:
break
else:
data = data[0:start] + data[end:]
return data
def convert2Header(inFile, version): def convert2Header(inFile, version):
fileType = inFile.split(".")[1] fileType = inFile.split(".")[1]
define = inFile.split(".")[0].upper() define = inFile.split(".")[0].upper()
@ -118,9 +147,7 @@ def convert2Header(inFile, version):
f.close() f.close()
# delete all files in the 'h' dir # delete all files in the 'h' dir
wd = 'h' wd = 'web/html/h'
if os.getcwd()[-4:] != "html":
wd = "web/html/" + wd
if os.path.exists(wd): if os.path.exists(wd):
for f in os.listdir(wd): for f in os.listdir(wd):
@ -131,9 +158,8 @@ if os.path.exists(wd):
os.remove(os.path.join(wd, f)) os.remove(os.path.join(wd, f))
# grab all files with following extensions # grab all files with following extensions
if os.getcwd()[-4:] != "html": os.chdir('./web/html')
os.chdir('./web/html') types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types
types = ('*.html', '*.css', '*.js', '*.ico') # the tuple of file types
files_grabbed = [] files_grabbed = []
for files in types: for files in types:
files_grabbed.extend(glob.glob(files)) files_grabbed.extend(glob.glob(files))

86
scripts/getVersion.py

@ -50,58 +50,100 @@ def readVersion(path, infile):
versionnumber += line[p+13:].rstrip() + "." versionnumber += line[p+13:].rstrip() + "."
os.mkdir(path + "firmware/") os.mkdir(path + "firmware/")
os.mkdir(path + "firmware/s3/") os.mkdir(path + "firmware/ESP8266/")
os.mkdir(path + "firmware/ESP8285/")
os.mkdir(path + "firmware/ESP32/")
os.mkdir(path + "firmware/ESP32-S2/")
os.mkdir(path + "firmware/ESP32-S3/")
os.mkdir(path + "firmware/ESP32-C3/")
os.mkdir(path + "firmware/ESP32-S3-ETH/")
sha = os.getenv("SHA",default="sha") sha = os.getenv("SHA",default="sha")
versionout = version[:-1] + "_" + sha + "_esp8266.bin" versionout = version[:-1] + "_" + sha + "_esp8266.bin"
src = path + ".pio/build/esp8266-release/firmware.bin" src = path + ".pio/build/esp8266/firmware.bin"
dst = path + "firmware/" + versionout dst = path + "firmware/ESP8266/" + versionout
os.rename(src, dst) os.rename(src, dst)
versionout = version[:-1] + "_" + sha + "_esp8266_prometheus.bin" versionout = version[:-1] + "_" + sha + "_esp8266_prometheus.bin"
src = path + ".pio/build/esp8266-release-prometheus/firmware.bin" src = path + ".pio/build/esp8266-prometheus/firmware.bin"
dst = path + "firmware/" + versionout dst = path + "firmware/ESP8266/" + versionout
os.rename(src, dst) os.rename(src, dst)
versionout = version[:-1] + "_" + sha + "_esp8285.bin" versionout = version[:-1] + "_" + sha + "_esp8285.bin"
src = path + ".pio/build/esp8285-release/firmware.bin" src = path + ".pio/build/esp8285/firmware.bin"
dst = path + "firmware/" + versionout dst = path + "firmware/ESP8285/" + versionout
os.rename(src, dst) os.rename(src, dst)
gzip_bin(dst, dst + ".gz") gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_" + sha + "_esp32.bin" versionout = version[:-1] + "_" + sha + "_esp32.bin"
src = path + ".pio/build/esp32-wroom32-release/firmware.bin" src = path + ".pio/build/esp32-wroom32/firmware.bin"
dst = path + "firmware/" + versionout dst = path + "firmware/ESP32/" + versionout
os.rename(src, dst) os.rename(src, dst)
versionout = version[:-1] + "_" + sha + "_esp32_prometheus.bin" versionout = version[:-1] + "_" + sha + "_esp32_prometheus.bin"
src = path + ".pio/build/esp32-wroom32-release-prometheus/firmware.bin" src = path + ".pio/build/esp32-wroom32-prometheus/firmware.bin"
dst = path + "firmware/" + versionout dst = path + "firmware/ESP32/" + versionout
os.rename(src, dst) os.rename(src, dst)
versionout = version[:-1] + "_" + sha + "_esp32_ethernet.bin" versionout = version[:-1] + "_" + sha + "_esp32_ethernet.bin"
src = path + ".pio/build/esp32-wroom32-ethernet-release/firmware.bin" src = path + ".pio/build/esp32-wroom32-ethernet/firmware.bin"
dst = path + "firmware/" + versionout dst = path + "firmware/ESP32/" + versionout
os.rename(src, dst)
versionout = version[:-1] + "_" + sha + "_esp32s2-mini.bin"
src = path + ".pio/build/esp32-s2-mini/firmware.bin"
dst = path + "firmware/ESP32-S2/" + versionout
os.rename(src, dst)
versionout = version[:-1] + "_" + sha + "_esp32c3-mini.bin"
src = path + ".pio/build/esp32-c3-mini/firmware.bin"
dst = path + "firmware/ESP32-C3/" + versionout
os.rename(src, dst) os.rename(src, dst)
versionout = version[:-1] + "_" + sha + "_esp32s3.bin" versionout = version[:-1] + "_" + sha + "_esp32s3.bin"
src = path + ".pio/build/opendtufusionv1-release/firmware.bin" src = path + ".pio/build/opendtufusion/firmware.bin"
dst = path + "firmware/s3/" + versionout dst = path + "firmware/ESP32-S3/" + versionout
os.rename(src, dst)
versionout = version[:-1] + "_" + sha + "_esp32s3_ethernet.bin"
src = path + ".pio/build/opendtufusion-ethernet/firmware.bin"
dst = path + "firmware/ESP32-S3-ETH/" + versionout
os.rename(src, dst) os.rename(src, dst)
# other ESP32 bin files # other ESP32 bin files
src = path + ".pio/build/esp32-wroom32-release/" src = path + ".pio/build/esp32-wroom32/"
dst = path + "firmware/" dst = path + "firmware/ESP32/"
os.rename(src + "bootloader.bin", dst + "bootloader.bin")
os.rename(src + "partitions.bin", dst + "partitions.bin")
genOtaBin(dst)
# other ESP32-S2 bin files
src = path + ".pio/build/esp32-s2-mini/"
dst = path + "firmware/ESP32-S2/"
os.rename(src + "bootloader.bin", dst + "bootloader.bin")
os.rename(src + "partitions.bin", dst + "partitions.bin")
genOtaBin(dst)
# other ESP32-C3 bin files
src = path + ".pio/build/esp32-c3-mini/"
dst = path + "firmware/ESP32-C3/"
os.rename(src + "bootloader.bin", dst + "bootloader.bin")
os.rename(src + "partitions.bin", dst + "partitions.bin")
genOtaBin(dst)
# other ESP32-S3 bin files
src = path + ".pio/build/opendtufusion/"
dst = path + "firmware/ESP32-S3/"
os.rename(src + "bootloader.bin", dst + "bootloader.bin") os.rename(src + "bootloader.bin", dst + "bootloader.bin")
os.rename(src + "partitions.bin", dst + "partitions.bin") os.rename(src + "partitions.bin", dst + "partitions.bin")
genOtaBin(path + "firmware/") genOtaBin(dst)
# other ESP32S3 bin files # other ESP32-S3-Eth bin files
src = path + ".pio/build/opendtufusionv1-release/" src = path + ".pio/build/opendtufusion-ethernet/"
dst = path + "firmware/s3/" dst = path + "firmware/ESP32-S3-ETH/"
os.rename(src + "bootloader.bin", dst + "bootloader.bin") os.rename(src + "bootloader.bin", dst + "bootloader.bin")
os.rename(src + "partitions.bin", dst + "partitions.bin") os.rename(src + "partitions.bin", dst + "partitions.bin")
os.rename(src + "ota.bin", dst + "ota.bin") genOtaBin(dst)
os.rename("../scripts/gh-action-dev-build-flash.html", path + "install.html") os.rename("../scripts/gh-action-dev-build-flash.html", path + "install.html")

1
src/.vscode/settings.json

@ -84,4 +84,5 @@
}, },
"cmake.configureOnOpen": false, "cmake.configureOnOpen": false,
"editor.formatOnSave": false, "editor.formatOnSave": false,
"cmake.sourceDirectory": "C:/lpusch/github/ahoy/src/.pio/libdeps/esp32-wroom32-release-prometheus/Adafruit BusIO",
} }

25
src/CHANGES.md

@ -5,17 +5,16 @@ Changelog VArt (based on v0.7.36)
Changelog v0.7.36 Changelog v0.7.36
=======
Changelog v0.8.36
* added Ethernet variant * added dim option for LEDS
* fix configuration of ePaper * changed reload time for opendtufusion after update to 5s
* fix MI inverter support * fix default interval and gap for communication
* endpoints `/api/record/live`, `/api/record/alarm`, `/api/record/config`, `/api/record/info` are obsolete * fix serial number in exported json (was decimal, now correct as hexdecimal number)
* added `/api/inverter/alarm/[ID]` to read inverter alarms * beautified factory reset
* added Alarms in Live View as modal window * added second stage for erase settings
* added MqTT transmission of last 10 alarms * increased maximal number of inverters to 32 for opendtufusion board (ESP32-S3)
* updated documentation * fixed crash if CMT inverter is enabled, but CMT isn't configured
* changed `ESP8266` default NRF24 pin assignments (`D3` = `CE` and `D4` = `IRQ`)
* changed live view to gray once inverter isn't available -> fast identify if inverters are online full version log: [Development Log](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md)
* added information about maximum power (AC and DC)
* updated documentation
* several small fixes

364
src/app.cpp

@ -4,18 +4,13 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "app.h" #include "app.h"
#include "utils/sun.h" #include "utils/sun.h"
#include "plugins/history.h" #include "plugins/history.h"
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
app::app() app::app() : ah::Scheduler {} {}
: ah::Scheduler {},
mInnerLoopCb {nullptr} {
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -24,7 +19,6 @@ void app::setup() {
while (!Serial) while (!Serial)
yield(); yield();
resetSystem(); resetSystem();
mSettings.setup(); mSettings.setup();
@ -38,32 +32,18 @@ void app::setup() {
DBGPRINTLN(F("false")); DBGPRINTLN(F("false"));
if(mConfig->nrf.enabled) { if(mConfig->nrf.enabled) {
mNrfRadio.setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); mNrfRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso);
mNrfRadio.enableDebug();
} }
#if defined(ESP32) #if defined(ESP32)
if(mConfig->cmt.enabled) { if(mConfig->cmt.enabled) {
mCmtRadio.setup(mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, false); mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, false);
mCmtRadio.enableDebug();
} }
#endif #endif
#ifdef ETHERNET #ifdef ETHERNET
delay(1000); delay(1000);
DPRINT(DBG_INFO, F("mEth setup..."));
DSERIAL.flush();
mEth.setup(mConfig, &mTimestamp, [this](bool gotIp) { this->onNetwork(gotIp); }, [this](bool gotTime) { this->onNtpUpdate(gotTime); }); mEth.setup(mConfig, &mTimestamp, [this](bool gotIp) { this->onNetwork(gotIp); }, [this](bool gotTime) { this->onNtpUpdate(gotTime); });
DBGPRINTLN(F("done..."));
DSERIAL.flush();
#endif // ETHERNET #endif // ETHERNET
#if !defined(ETHERNET)
#if defined(AP_ONLY)
mInnerLoopCb = std::bind(&app::loopStandard, this);
#else
mInnerLoopCb = std::bind(&app::loopWifi, this);
#endif
#endif /* !defined(ETHERNET) */
#if !defined(ETHERNET) #if !defined(ETHERNET)
mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onNetwork, this, std::placeholders::_1)); mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onNetwork, this, std::placeholders::_1));
#if !defined(AP_ONLY) #if !defined(AP_ONLY)
@ -71,24 +51,13 @@ void app::setup() {
#endif #endif
#endif /* defined(ETHERNET) */ #endif /* defined(ETHERNET) */
mSys.setup(&mTimestamp); mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs);
mSys.addInverters(&mConfig->inst); mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
if (mConfig->nrf.enabled) { mSys.setup(&mTimestamp, &mConfig->inst);
mPayload.setup(this, &mSys, &mNrfRadio, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
mPayload.enableSerialDebug(mConfig->serial.debug); initInverter(i);
mPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
mMiPayload.setup(this, &mSys, &mNrfRadio, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp);
mMiPayload.enableSerialDebug(mConfig->serial.debug);
mMiPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
} }
#if defined(ESP32)
mHmsPayload.setup(this, &mSys, &mCmtRadio, &mStat, 5, &mTimestamp);
mHmsPayload.enableSerialDebug(mConfig->serial.debug);
mHmsPayload.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
#endif
if(mConfig->nrf.enabled) { if(mConfig->nrf.enabled) {
if (!mNrfRadio.isChipConnected()) if (!mNrfRadio.isChipConnected())
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
@ -100,11 +69,7 @@ void app::setup() {
if (mMqttEnabled) { if (mMqttEnabled) {
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime); mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime);
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
mPayload.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); }); mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
mMiPayload.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
#if defined(ESP32)
mHmsPayload.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
#endif
} }
#endif #endif
setupLed(); setupLed();
@ -112,11 +77,20 @@ void app::setup() {
mWeb.setup(this, &mSys, mConfig); mWeb.setup(this, &mSys, mConfig);
mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0); mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
mApi.setup(this, &mSys, &mNrfRadio, mWeb.getWebSrvPtr(), mConfig); mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig);
#ifdef ENABLE_SYSLOG
mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback)
#endif
// Plugins // Plugins
#if defined(PLUGIN_DISPLAY)
if (mConfig->plugin.display.type != 0) if (mConfig->plugin.display.type != 0)
mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, mVersion); #if defined(ESP32)
mDisplay.setup(this, &mConfig->plugin.display, &mSys, &mNrfRadio, &mCmtRadio, &mTimestamp);
#else
mDisplay.setup(this, &mConfig->plugin.display, &mSys, &mNrfRadio, NULL, &mTimestamp);
#endif
#endif
mTotalPowerHistory = new TotalPowerHistory(); mTotalPowerHistory = new TotalPowerHistory();
mTotalPowerHistory->setup(this, &mSys, mConfig); mTotalPowerHistory->setup(this, &mSys, mConfig);
@ -134,115 +108,37 @@ void app::setup() {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::loop(void) { void app::loop(void) {
if (mInnerLoopCb) if(mConfig->nrf.enabled)
mInnerLoopCb(); mNrfRadio.loop();
#if !defined(ETHERNET) #if defined(ESP32)
if(mConfig->cmt.enabled)
mCmtRadio.loop();
#endif #endif
}
//-----------------------------------------------------------------------------
void app::loopStandard(void) {
ah::Scheduler::loop(); ah::Scheduler::loop();
mCommunication.loop();
if (mNrfRadio.loop() && mConfig->nrf.enabled) { if (mMqttEnabled && mNetworkConnected)
while (!mNrfRadio.mBufCtrl.empty()) {
packet_t *p = &mNrfRadio.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(" | "));
ah::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);
}
mNrfRadio.mBufCtrl.pop();
yield();
}
mPayload.process(true);
mMiPayload.process(true);
}
#if defined(ESP32)
if (mCmtRadio.loop() && mConfig->cmt.enabled) {
while (!mCmtRadio.mBufCtrl.empty()) {
hmsPacket_t *p = &mCmtRadio.mBufCtrl.front();
if (mConfig->serial.debug) {
DPRINT(DBG_INFO, F("RX "));
DBGPRINT(String(p->data[0]));
DBGPRINT(F(" RSSI "));
DBGPRINT(String(p->rssi));
DBGPRINT(F("dBm | "));
ah::dumpBuf(&p->data[1], p->data[0]);
}
mStat.frmCnt++;
Inverter<> *iv = mSys.findInverter(&p->data[2]);
if(NULL != iv) {
if((iv->ivGen == IV_HMS) || (iv->ivGen == IV_HMT))
mHmsPayload.add(iv, p);
}
mCmtRadio.mBufCtrl.pop();
yield();
}
mHmsPayload.process(false); //true
}
#endif
mPayload.loop();
mMiPayload.loop();
#if defined(ESP32)
mHmsPayload.loop();
#endif
if (mMqttEnabled)
mMqtt.loop(); mMqtt.loop();
} }
#if !defined(ETHERNET)
//-----------------------------------------------------------------------------
void app::loopWifi(void) {
ah::Scheduler::loop();
yield();
}
#endif /* !defined(ETHERNET) */
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::onNetwork(bool gotIp) { void app::onNetwork(bool gotIp) {
DPRINTLN(DBG_DEBUG, F("onNetwork")); DPRINTLN(DBG_DEBUG, F("onNetwork"));
mNetworkConnected = gotIp;
ah::Scheduler::resetTicker(); ah::Scheduler::resetTicker();
regularTickers(); // reinstall regular tickers regularTickers(); //reinstall regular tickers
if (gotIp) { every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend");
every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend");
#if defined(ESP32)
if(mConfig->cmt.enabled)
everySec(std::bind(&CmtRadioType::tickSecond, &mCmtRadio), "tsCmt");
#endif
mMqttReconnect = true; mMqttReconnect = true;
mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers!
once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2");
//tickNtpUpdate();
#if !defined(ETHERNET) #if !defined(ETHERNET)
if (WIFI_AP == WiFi.getMode()) { if (WIFI_AP == WiFi.getMode()) {
mMqttEnabled = false; mMqttEnabled = false;
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
} }
#endif /* !defined(ETHERNET) */
mInnerLoopCb = [this]() { this->loopStandard(); };
} else {
#if defined(ETHERNET)
mInnerLoopCb = nullptr;
#else /* defined(ETHERNET) */
mInnerLoopCb = [this]() { this->loopWifi(); };
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
#endif /* defined(ETHERNET) */ #endif /* !defined(ETHERNET) */
}
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -250,9 +146,11 @@ void app::regularTickers(void) {
DPRINTLN(DBG_DEBUG, F("regularTickers")); DPRINTLN(DBG_DEBUG, F("regularTickers"));
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
// Plugins // Plugins
#if defined(PLUGIN_DISPLAY)
if (mConfig->plugin.display.type != 0) if (mConfig->plugin.display.type != 0)
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); #endif
every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart");
#if !defined(ETHERNET) #if !defined(ETHERNET)
//everySec([this]() { mImprov.tickSerial(); }, "impro"); //everySec([this]() { mImprov.tickSerial(); }, "impro");
#endif #endif
@ -284,11 +182,11 @@ void app::updateNtp(void) {
if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed
if (mConfig->inst.rstValsNotAvail) if (mConfig->inst.rstValsNotAvail)
everyMin(std::bind(&app::tickMinute, this), "tMin"); everyMin(std::bind(&app::tickMinute, this), "tMin");
if (mConfig->inst.rstYieldMidNight) {
uint32_t localTime = gTimezone.toLocal(mTimestamp); uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
}
if (mConfig->sys.schedReboot) { if (mConfig->sys.schedReboot) {
uint32_t localTime = gTimezone.toLocal(mTimestamp); uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght
@ -311,7 +209,8 @@ void app::updateNtp(void) {
void app::tickNtpUpdate(void) { void app::tickNtpUpdate(void) {
uint32_t nxtTrig = 5; // default: check again in 5 sec uint32_t nxtTrig = 5; // default: check again in 5 sec
#if defined(ETHERNET) #if defined(ETHERNET)
bool isOK = mEth.updateNtpTime(); bool isOK = (mTimestamp != 0);
mEth.updateNtpTime();
#else #else
bool isOK = mWifi.getNtpTime(); bool isOK = mWifi.getNtpTime();
#endif #endif
@ -323,7 +222,7 @@ void app::tickNtpUpdate(void) {
// immediately start communicating // immediately start communicating
if (isOK && mSendFirst) { if (isOK && mSendFirst) {
mSendFirst = false; mSendFirst = false;
once(std::bind(&app::tickSend, this), 2, "senOn"); once(std::bind(&app::tickSend, this), 1, "senOn");
} }
mMqttReconnect = false; mMqttReconnect = false;
@ -349,43 +248,50 @@ void app::tickCalcSunrise(void) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::tickIVCommunication(void) { void app::tickIVCommunication(void) {
mIVCommunicationOn = !mConfig->sun.disNightCom; // if sun.disNightCom is false, communication is always on bool restartTick = false;
if (!mIVCommunicationOn) { // inverter communication only during the day bool zeroValues = false;
uint32_t nxtTrig; uint32_t nxtTrig = 0;
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys.getInverterByPos(i);
if(NULL == iv)
continue;
iv->commEnabled = !iv->config->disNightCom; // if sun.disNightCom is false, communication is always on
if (!iv->commEnabled) { // inverter communication only during the day
if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start
nxtTrig = mSunrise - mConfig->sun.offsetSec; nxtTrig = mSunrise - mConfig->sun.offsetSec;
} else { } 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 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; nxtTrig = 0;
} else { // current time lies within communication start/stop time, set next trigger to communication stop } else { // current time lies within communication start/stop time, set next trigger to communication stop
mIVCommunicationOn = true; iv->commEnabled = true;
nxtTrig = mSunset + mConfig->sun.offsetSec; nxtTrig = mSunset + mConfig->sun.offsetSec;
} }
} }
if (nxtTrig != 0) if (nxtTrig != 0)
onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom"); restartTick = true;
}
if ((!iv->commEnabled) && (mConfig->inst.rstValsCommStop))
zeroValues = true;
} }
tickComm();
if(restartTick) // at least one inverter
onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom");
if (zeroValues) // at least one inverter
once(std::bind(&app::tickZeroValues, this), mConfig->inst.sendInterval, "tZero");
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::tickSun(void) { void app::tickSun(void) {
// only used and enabled by MQTT (see setup()) // only used and enabled by MQTT (see setup())
if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom)) if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec))
once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry 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) { void app::tickZeroValues(void) {
zeroIvValues(!CHECK_AVAIL, SKIP_YIELD_DAY); zeroIvValues(!CHECK_AVAIL, SKIP_YIELD_DAY);
@ -400,69 +306,74 @@ void app::tickMinute(void) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::tickMidnight(void) { void app::tickMidnight(void) {
// only triggered if 'reset values at midnight is enabled'
uint32_t localTime = gTimezone.toLocal(mTimestamp); uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2");
Inverter<> *iv;
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
// reset alarms
if(InverterStatus::OFF == iv->getStatus())
iv->resetAlarms();
// clear max values
if(mConfig->inst.rstMaxValsMidNight) {
uint8_t pos;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t i = 0; i <= iv->channels; i++) {
pos = iv->getPosByChFld(i, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f);
}
}
}
if (mConfig->inst.rstYieldMidNight) {
zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY); zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY);
if (mMqttEnabled) if (mMqttEnabled)
mMqtt.tickerMidnight(); mMqtt.tickerMidnight();
}
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::tickSend(void) { void app::tickSend(void) {
if(mConfig->nrf.enabled) { uint8_t fill = mCommunication.getFillState();
if(!mNrfRadio.isChipConnected()) { uint8_t max = mCommunication.getMaxFill();
DPRINTLN(DBG_WARN, F("NRF24 not connected!")); if((max-MAX_NUM_INVERTERS) <= fill) {
return; DPRINT(DBG_WARN, F("send queue almost full, consider to increase interval, "));
} DBGPRINT(String(fill));
} DBGPRINT(F(" of "));
if (mIVCommunicationOn) { DBGPRINT(String(max));
if (!mNrfRadio.mBufCtrl.empty()) { DBGPRINTLN(F(" entries used"));
if (mConfig->serial.debug) { }
DPRINT(DBG_DEBUG, F("recbuf not empty! #"));
DBGPRINTLN(String(mNrfRadio.mBufCtrl.size())); for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
} Inverter<> *iv = mSys.getInverterByPos(i);
} if(NULL == iv)
#if defined(ESP32) continue;
if (!mCmtRadio.mBufCtrl.empty()) {
if (mConfig->serial.debug) { if(iv->config->enabled) {
DPRINT(DBG_INFO, F("recbuf not empty! #")); if(!iv->commEnabled) {
DBGPRINTLN(String(mCmtRadio.mBufCtrl.size())); DPRINT_IVID(DBG_INFO, iv->id);
} DBGPRINTLN(F("no communication to the inverter (night time)"));
continue;
} }
#endif
int8_t maxLoop = MAX_NUM_INVERTERS; if(!iv->radio->isChipConnected())
Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId); continue;
do {
mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1;
iv = mSys.getInverterByPos(mSendLastIvId);
} while ((NULL == iv) && ((maxLoop--) > 0));
if (NULL != iv) { iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) {
if (iv->config->enabled) { if(isDevControl)
if(mConfig->nrf.enabled) { mCommunication.addImportant(iv, cmd);
if (iv->ivGen == IV_HM) else
mPayload.ivSend(iv); mCommunication.add(iv, cmd);
else if(iv->ivGen == IV_MI) });
mMiPayload.ivSend(iv);
}
#if defined(ESP32)
if(mConfig->cmt.enabled) {
if((iv->ivGen == IV_HMS) || (iv->ivGen == IV_HMT))
mHmsPayload.ivSend(iv);
}
#endif
}
} }
} else {
if (mConfig->serial.debug)
DPRINTLN(DBG_WARN, F("Time not set or it is night time, therefore no communication to the inverter!"));
} }
yield();
updateLed(); updateLed();
} }
@ -478,6 +389,8 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
continue; // skip to next inverter continue; // skip to next inverter
if (!iv->config->enabled) if (!iv->config->enabled)
continue; // skip to next inverter continue; // skip to next inverter
if (iv->commEnabled)
continue; // skip to next inverter
if (checkAvail) { if (checkAvail) {
if (!iv->isAvailable()) if (!iv->isAvailable())
@ -535,11 +448,10 @@ void app::resetSystem(void) {
mSendLastIvId = 0; mSendLastIvId = 0;
mShowRebootRequest = false; mShowRebootRequest = false;
mIVCommunicationOn = true;
mSavePending = false; mSavePending = false;
mSaveReboot = false; mSaveReboot = false;
memset(&mStat, 0, sizeof(statistics_t)); mNetworkConnected = false;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -549,38 +461,44 @@ void app::mqttSubRxCb(JsonObject obj) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::setupLed(void) { void app::setupLed(void) {
uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH; uint8_t led_off = (mConfig->led.high_active) ? 0 : 255;
if (mConfig->led.led0 != 0xff) { if (mConfig->led.led0 != DEF_PIN_OFF) {
pinMode(mConfig->led.led0, OUTPUT); pinMode(mConfig->led.led0, OUTPUT);
digitalWrite(mConfig->led.led0, led_off); analogWrite(mConfig->led.led0, led_off);
} }
if (mConfig->led.led1 != 0xff) { if (mConfig->led.led1 != DEF_PIN_OFF) {
pinMode(mConfig->led.led1, OUTPUT); pinMode(mConfig->led.led1, OUTPUT);
digitalWrite(mConfig->led.led1, led_off); analogWrite(mConfig->led.led1, led_off);
} }
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::updateLed(void) { void app::updateLed(void) {
uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH; uint8_t led_off = (mConfig->led.high_active) ? 0 : 255;
uint8_t led_on = (mConfig->led.led_high_active) ? HIGH : LOW; uint8_t led_on = (mConfig->led.high_active) ? (mConfig->led.luminance) : (255-mConfig->led.luminance);
if (mConfig->led.led0 != 0xff) { if (mConfig->led.led0 != DEF_PIN_OFF) {
Inverter<> *iv = mSys.getInverterByPos(0); Inverter<> *iv;
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL != iv) { if (NULL != iv) {
if (iv->isProducing()) if (iv->isProducing()) {
digitalWrite(mConfig->led.led0, led_on); // turn on when at least one inverter is producing
else analogWrite(mConfig->led.led0, led_on);
digitalWrite(mConfig->led.led0, led_off); break;
}
else if(iv->config->enabled)
analogWrite(mConfig->led.led0, led_off);
}
} }
} }
if (mConfig->led.led1 != 0xff) { if (mConfig->led.led1 != DEF_PIN_OFF) {
if (getMqttIsConnected()) { if (getMqttIsConnected()) {
digitalWrite(mConfig->led.led1, led_on); analogWrite(mConfig->led.led1, led_on);
} else { } else {
digitalWrite(mConfig->led.led1, led_off); analogWrite(mConfig->led.led1, led_off);
} }
} }
} }

117
src/app.h

@ -9,23 +9,23 @@
#include <Arduino.h> #include <Arduino.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "appInterface.h"
#include "config/settings.h" #include "config/settings.h"
#include "defines.h" #include "defines.h"
#include "hm/hmPayload.h" #include "appInterface.h"
#include "hm/hmSystem.h" #include "hm/hmSystem.h"
#include "hm/hmRadio.h" #include "hm/hmRadio.h"
#if defined(ESP32)
#include "hms/hmsRadio.h" #include "hms/hmsRadio.h"
#include "hms/hmsPayload.h" #endif
#include "hm/hmPayload.h"
#include "hm/miPayload.h"
#include "publisher/pubMqtt.h" #include "publisher/pubMqtt.h"
#include "publisher/pubSerial.h" #include "publisher/pubSerial.h"
#include "utils/crc.h" #include "utils/crc.h"
#include "utils/dbg.h" #include "utils/dbg.h"
#include "utils/scheduler.h" #include "utils/scheduler.h"
#include "utils/syslog.h"
#include "web/RestApi.h" #include "web/RestApi.h"
#include "web/web.h" #include "web/web.h"
#include "hm/Communication.h"
#if defined(ETHERNET) #if defined(ETHERNET)
#include "eth/ahoyeth.h" #include "eth/ahoyeth.h"
#else /* defined(ETHERNET) */ #else /* defined(ETHERNET) */
@ -42,22 +42,22 @@
#define ACOS(x) (degrees(acos(x))) #define ACOS(x) (degrees(acos(x)))
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType; typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType;
typedef HmPayload<HmSystemType, HmRadio<>> PayloadType;
typedef MiPayload<HmSystemType, HmRadio<>> MiPayloadType;
#ifdef ESP32 #ifdef ESP32
typedef CmtRadio<esp32_3wSpi<>> CmtRadioType;
typedef HmsPayload<HmSystemType, CmtRadioType> HmsPayloadType;
#endif #endif
typedef Web<HmSystemType> WebType; typedef Web<HmSystemType> WebType;
typedef RestApi<HmSystemType, HmRadio<>> RestApiType; typedef RestApi<HmSystemType> RestApiType;
typedef PubMqtt<HmSystemType> PubMqttType; typedef PubMqtt<HmSystemType> PubMqttType;
typedef PubSerial<HmSystemType> PubSerialType; typedef PubSerial<HmSystemType> PubSerialType;
// PLUGINS // PLUGINS
#if defined(PLUGIN_DISPLAY)
#include "plugins/Display/Display.h" #include "plugins/Display/Display.h"
#include "plugins/history.h" #include "plugins/Display/Display_data.h"
typedef Display<HmSystemType> DisplayType; //typedef Display<HmSystemType> DisplayType;
typedef Display<HmSystemType, Radio> DisplayType;
#endif
#include "plugins/history.h"
class app : public IApp, public ah::Scheduler { class app : public IApp, public ah::Scheduler {
public: public:
@ -66,16 +66,23 @@ class app : public IApp, public ah::Scheduler {
void setup(void); void setup(void);
void loop(void); void loop(void);
void loopStandard(void);
#if !defined(ETHERNET)
void loopWifi(void);
#endif /* !defined(ETHERNET) */
void onNetwork(bool gotIp); void onNetwork(bool gotIp);
void regularTickers(void); void regularTickers(void);
void handleIntr(void) { void handleIntr(void) {
mNrfRadio.handleIntr(); mNrfRadio.handleIntr();
} }
void* getRadioObj(bool nrf) {
if(nrf)
return (void*)&mNrfRadio;
else {
#ifdef ESP32
return (void*)&mCmtRadio;
#else
return NULL;
#endif
}
}
#ifdef ESP32 #ifdef ESP32
void handleHmsIntr(void) { void handleHmsIntr(void) {
@ -88,7 +95,11 @@ class app : public IApp, public ah::Scheduler {
} }
uint32_t getTimestamp() { uint32_t getTimestamp() {
return Scheduler::getTimestamp(); return Scheduler::mTimestamp;
}
uint64_t getTimestampMs() {
return ((uint64_t)Scheduler::mTimestamp * 1000) + (uint64_t)Scheduler::mTsMillis;
} }
bool saveSettings(bool reboot) { bool saveSettings(bool reboot) {
@ -96,13 +107,23 @@ class app : public IApp, public ah::Scheduler {
mSavePending = true; mSavePending = true;
mSaveReboot = reboot; mSaveReboot = reboot;
if(reboot) { if(reboot) {
onNetwork(false);
ah::Scheduler::resetTicker(); ah::Scheduler::resetTicker();
} }
once(std::bind(&app::tickSave, this), 3, "save"); once(std::bind(&app::tickSave, this), 3, "save");
return true; return true;
} }
void initInverter(uint8_t id) {
mSys.addInverter(id, [this](Inverter<> *iv) {
if((IV_MI == iv->ivGen) || (IV_HM == iv->ivGen))
iv->radio = &mNrfRadio;
#if defined(ESP32)
else if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
iv->radio = &mCmtRadio;
#endif
});
}
bool readSettings(const char *path) { bool readSettings(const char *path) {
return mSettings.readSettings(path); return mSettings.readSettings(path);
} }
@ -123,10 +144,6 @@ class app : public IApp, public ah::Scheduler {
return mSaveReboot; return mSaveReboot;
} }
statistics_t *getStatistics() {
return &mStat;
}
#if !defined(ETHERNET) #if !defined(ETHERNET)
void scanAvailNetworks() { void scanAvailNetworks() {
mWifi.scanAvailNetworks(); mWifi.scanAvailNetworks();
@ -136,9 +153,6 @@ class app : public IApp, public ah::Scheduler {
return mWifi.getAvailNetworks(obj); return mWifi.getAvailNetworks(obj);
} }
void setOnUpdate() {
onNetwork(false);
}
#endif /* !defined(ETHERNET) */ #endif /* !defined(ETHERNET) */
void setRebootFlag() { void setRebootFlag() {
@ -173,19 +187,6 @@ class app : public IApp, public ah::Scheduler {
mMqtt.setPowerLimitAck(iv); mMqtt.setPowerLimitAck(iv);
} }
void ivSendHighPrio(Inverter<> *iv) {
if(mIVCommunicationOn) { // only send commands if communcation is enabled
if (iv->ivGen == IV_HM)
mPayload.ivSendHighPrio(iv);
else if (iv->ivGen == IV_MI)
mMiPayload.ivSendHighPrio(iv);
#if defined(ESP32)
else if((iv->ivGen == IV_HMS) || (iv->ivGen == IV_HMT))
mHmsPayload.ivSendHighPrio(iv);
#endif
}
}
bool getMqttIsConnected() { bool getMqttIsConnected() {
return mMqtt.isConnected(); return mMqtt.isConnected();
} }
@ -202,11 +203,6 @@ class app : public IApp, public ah::Scheduler {
return mWeb.isProtected(request); return mWeb.isProtected(request);
} }
void getNrfRadioCounters(uint32_t *sendCnt, uint32_t *retransmits) {
*sendCnt = mNrfRadio.mSendCnt;
*retransmits = mNrfRadio.mRetransmits;
}
bool getNrfEnabled(void) { bool getNrfEnabled(void) {
return mConfig->nrf.enabled; return mConfig->nrf.enabled;
} }
@ -223,15 +219,6 @@ class app : public IApp, public ah::Scheduler {
return mConfig->cmt.pinIrq; return mConfig->cmt.pinIrq;
} }
String getTimeStr(uint32_t offset = 0) {
char str[10];
if(0 == mTimestamp)
sprintf(str, "n/a");
else
sprintf(str, "%02d:%02d:%02d ", hour(mTimestamp + offset), minute(mTimestamp + offset), second(mTimestamp + offset));
return String(str);
}
uint32_t getTimezoneOffset() { uint32_t getTimezoneOffset() {
return mApi.getTimezoneOffset(); return mApi.getTimezoneOffset();
} }
@ -266,8 +253,6 @@ class app : public IApp, public ah::Scheduler {
#define CHECK_AVAIL true #define CHECK_AVAIL true
#define SKIP_YIELD_DAY true #define SKIP_YIELD_DAY true
typedef std::function<void()> innerLoopCb;
void resetSystem(void); void resetSystem(void);
void zeroIvValues(bool checkAvail = false, bool skipYieldDay = true); void zeroIvValues(bool checkAvail = false, bool skipYieldDay = true);
@ -276,8 +261,11 @@ class app : public IApp, public ah::Scheduler {
if (mMqttEnabled) if (mMqttEnabled)
mMqtt.payloadEventListener(cmd, iv); mMqtt.payloadEventListener(cmd, iv);
#endif #endif
#if defined(PLUGIN_DISPLAY)
if(mConfig->plugin.display.type != 0) if(mConfig->plugin.display.type != 0)
mDisplay.payloadEventListener(cmd); mDisplay.payloadEventListener(cmd);
#endif
updateLed();
} }
void mqttSubRxCb(JsonObject obj); void mqttSubRxCb(JsonObject obj);
@ -287,7 +275,6 @@ class app : public IApp, public ah::Scheduler {
void tickReboot(void) { void tickReboot(void) {
DPRINTLN(DBG_INFO, F("Rebooting...")); DPRINTLN(DBG_INFO, F("Rebooting..."));
onNetwork(false);
ah::Scheduler::resetTicker(); ah::Scheduler::resetTicker();
WiFi.disconnect(); WiFi.disconnect();
delay(200); delay(200);
@ -318,13 +305,11 @@ class app : public IApp, public ah::Scheduler {
void tickZeroValues(void); void tickZeroValues(void);
void tickMidnight(void); void tickMidnight(void);
innerLoopCb mInnerLoopCb;
HmSystemType mSys; HmSystemType mSys;
HmRadio<> mNrfRadio; HmRadio<> mNrfRadio;
Communication mCommunication;
bool mShowRebootRequest; bool mShowRebootRequest;
bool mIVCommunicationOn;
#if defined(ETHERNET) #if defined(ETHERNET)
ahoyeth mEth; ahoyeth mEth;
@ -333,15 +318,17 @@ class app : public IApp, public ah::Scheduler {
#endif /* defined(ETHERNET) */ #endif /* defined(ETHERNET) */
WebType mWeb; WebType mWeb;
RestApiType mApi; RestApiType mApi;
PayloadType mPayload; #ifdef ENABLE_SYSLOG
MiPayloadType mMiPayload; DbgSyslog mDbgSyslog;
#endif
//PayloadType mPayload;
//MiPayloadType mMiPayload;
PubSerialType mPubSerial; PubSerialType mPubSerial;
#if !defined(ETHERNET) #if !defined(ETHERNET)
//Improv mImprov; //Improv mImprov;
#endif #endif
#ifdef ESP32 #ifdef ESP32
CmtRadioType mCmtRadio; CmtRadio<> mCmtRadio;
HmsPayloadType mHmsPayload;
#endif #endif
char mVersion[12]; char mVersion[12];
@ -353,7 +340,7 @@ class app : public IApp, public ah::Scheduler {
uint8_t mSendLastIvId; uint8_t mSendLastIvId;
bool mSendFirst; bool mSendFirst;
statistics_t mStat; bool mNetworkConnected;
// mqtt // mqtt
PubMqttType mMqtt; PubMqttType mMqtt;
@ -365,9 +352,13 @@ class app : public IApp, public ah::Scheduler {
uint32_t mSunrise, mSunset; uint32_t mSunrise, mSunset;
// plugins // plugins
#if defined(PLUGIN_DISPLAY)
DisplayType mDisplay; DisplayType mDisplay;
DisplayData mDispData;
#endif
TotalPowerHistory *mTotalPowerHistory; TotalPowerHistory *mTotalPowerHistory;
YieldDayHistory *mYieldDayHistory; YieldDayHistory *mYieldDayHistory;
}; };
#endif /*__APP_H__*/ #endif /*__APP_H__*/

20
src/appInterface.h

@ -17,23 +17,25 @@
class TotalPowerHistory; class TotalPowerHistory;
class YieldDayHistory; class YieldDayHistory;
//#include "hms/hmsRadio.h"
#if defined(ESP32)
//typedef CmtRadio<esp32_3wSpi<>> CmtRadioType;
#endif
// abstract interface to App. Make members of App accessible from child class // abstract interface to App. Make members of App accessible from child class
// like web or API without forward declaration // like web or API without forward declaration
class IApp { class IApp {
public: public:
virtual ~IApp() {} virtual ~IApp() {}
virtual bool saveSettings(bool stopFs) = 0; virtual bool saveSettings(bool stopFs) = 0;
virtual void initInverter(uint8_t id) = 0;
virtual bool readSettings(const char *path) = 0; virtual bool readSettings(const char *path) = 0;
virtual bool eraseSettings(bool eraseWifi) = 0; virtual bool eraseSettings(bool eraseWifi) = 0;
virtual bool getSavePending() = 0; virtual bool getSavePending() = 0;
virtual bool getLastSaveSucceed() = 0; virtual bool getLastSaveSucceed() = 0;
virtual bool getShouldReboot() = 0; virtual bool getShouldReboot() = 0;
#if !defined(ETHERNET)
virtual void setOnUpdate() = 0;
#endif /* defined(ETHERNET) */
virtual void setRebootFlag() = 0; virtual void setRebootFlag() = 0;
virtual const char *getVersion() = 0; virtual const char *getVersion() = 0;
virtual statistics_t *getStatistics() = 0;
#if !defined(ETHERNET) #if !defined(ETHERNET)
virtual void scanAvailNetworks() = 0; virtual void scanAvailNetworks() = 0;
@ -42,10 +44,10 @@ class IApp {
virtual uint32_t getUptime() = 0; virtual uint32_t getUptime() = 0;
virtual uint32_t getTimestamp() = 0; virtual uint32_t getTimestamp() = 0;
virtual uint64_t getTimestampMs() = 0;
virtual uint32_t getSunrise() = 0; virtual uint32_t getSunrise() = 0;
virtual uint32_t getSunset() = 0; virtual uint32_t getSunset() = 0;
virtual void setTimestamp(uint32_t newTime) = 0; virtual void setTimestamp(uint32_t newTime) = 0;
virtual String getTimeStr(uint32_t offset) = 0;
virtual uint32_t getTimezoneOffset() = 0; virtual uint32_t getTimezoneOffset() = 0;
virtual void getSchedulerInfo(uint8_t *max) = 0; virtual void getSchedulerInfo(uint8_t *max) = 0;
virtual void getSchedulerNames() = 0; virtual void getSchedulerNames() = 0;
@ -54,10 +56,11 @@ class IApp {
virtual bool getSettingsValid() = 0; virtual bool getSettingsValid() = 0;
virtual void setMqttDiscoveryFlag() = 0; virtual void setMqttDiscoveryFlag() = 0;
virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0; virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0;
virtual bool getMqttIsConnected() = 0;
virtual void ivSendHighPrio(Inverter<> *iv) = 0; virtual bool getNrfEnabled() = 0;
virtual bool getCmtEnabled() = 0;
virtual bool getMqttIsConnected() = 0;
virtual uint32_t getMqttRxCnt() = 0; virtual uint32_t getMqttRxCnt() = 0;
virtual uint32_t getMqttTxCnt() = 0; virtual uint32_t getMqttTxCnt() = 0;
@ -68,6 +71,9 @@ class IApp {
virtual TotalPowerHistory *getTotalPowerHistoryPtr() = 0; virtual TotalPowerHistory *getTotalPowerHistoryPtr() = 0;
virtual YieldDayHistory *getYieldDayHistoryPtr() = 0; virtual YieldDayHistory *getYieldDayHistoryPtr() = 0;
virtual void* getRadioObj(bool nrf) = 0;
}; };
#endif /*__IAPP_H__*/ #endif /*__IAPP_H__*/

129
src/config/config.h

@ -7,6 +7,10 @@
#define __CONFIG_H__ #define __CONFIG_H__
// globally used
#define DEF_PIN_OFF 255
//------------------------------------- //-------------------------------------
// WIFI CONFIGURATION // WIFI CONFIGURATION
//------------------------------------- //-------------------------------------
@ -40,11 +44,21 @@
#if defined(ETHERNET) #if defined(ETHERNET)
#define ETH_SPI_HOST SPI2_HOST #define ETH_SPI_HOST SPI2_HOST
#define ETH_SPI_CLOCK_MHZ 25 #define ETH_SPI_CLOCK_MHZ 25
#define ETH_INT_GPIO 4 #ifndef DEF_ETH_IRQ_PIN
#define ETH_MISO_GPIO 12 #define DEF_ETH_IRQ_PIN 4
#define ETH_MOSI_GPIO 13 #endif
#define ETH_SCK_GPIO 14 #ifndef DEF_ETH_MISO_PIN
#define ETH_CS_PIN 15 #define DEF_ETH_MISO_PIN 12
#endif
#ifndef DEF_ETH_MOSI_PIN
#define DEF_ETH_MOSI_PIN 13
#endif
#ifndef DEF_ETH_SCK_PIN
#define DEF_ETH_SCK_PIN 14
#endif
#ifndef DEF_ETH_CS_PIN
#define DEF_ETH_CS_PIN 15
#endif
#else /* defined(ETHERNET) */ #else /* defined(ETHERNET) */
// time in seconds how long the station info (ssid + pwd) will be tried // time in seconds how long the station info (ssid + pwd) will be tried
#define WIFI_TRY_CONNECT_TIME 30 #define WIFI_TRY_CONNECT_TIME 30
@ -63,41 +77,96 @@
// for the ESP32-S3 there is no sane 'default', as it has full flexibility // 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, // to map its two HW SPIs anywhere and PCBs differ materially,
// so it has to be selected in the Web UI // so it has to be selected in the Web UI
#define DEF_CS_PIN 5 #ifndef DEF_NRF_CS_PIN
#define DEF_CE_PIN 17 #define DEF_NRF_CS_PIN 5
#define DEF_IRQ_PIN 16 #endif
#define DEF_MISO_PIN 19 #ifndef DEF_NRF_CE_PIN
#define DEF_MOSI_PIN 23 #define DEF_NRF_CE_PIN 4
#define DEF_SCLK_PIN 18 #endif
#ifndef DEF_NRF_IRQ_PIN
#define DEF_NRF_IRQ_PIN 16
#endif
#ifndef DEF_NRF_MISO_PIN
#define DEF_NRF_MISO_PIN 19
#endif
#ifndef DEF_NRF_MOSI_PIN
#define DEF_NRF_MOSI_PIN 23
#endif
#ifndef DEF_NRF_SCLK_PIN
#define DEF_NRF_SCLK_PIN 18
#endif
#ifndef DEF_CMT_SCLK
#define DEF_CMT_SCLK 12
#endif
#ifndef DEF_CMT_SDIO
#define DEF_CMT_SDIO 14
#endif
#ifndef DEF_CMT_CSB
#define DEF_CMT_CSB 27
#endif
#ifndef DEF_CMT_FCSB
#define DEF_CMT_FCSB 26
#endif
#ifndef DEF_CMT_IRQ
#define DEF_CMT_IRQ 34
#endif
#ifndef DEF_MOTION_SENSOR_PIN
#define DEF_MOTION_SENSOR_PIN DEF_PIN_OFF
#endif
#else #else
#define DEF_CS_PIN 15 #ifndef DEF_NRF_CS_PIN
#define DEF_CE_PIN 0 #define DEF_NRF_CS_PIN 15
#define DEF_IRQ_PIN 2 #endif
#ifndef DEF_NRF_CE_PIN
#define DEF_NRF_CE_PIN 0
#endif
#ifndef DEF_NRF_IRQ_PIN
#define DEF_NRF_IRQ_PIN 2
#endif
// these are given to relay the correct values via API // these are given to relay the correct values via API
// they cannot actually be moved for ESP82xx models // they cannot actually be moved for ESP82xx models
#define DEF_MISO_PIN 12 #ifndef DEF_NRF_MISO_PIN
#define DEF_MOSI_PIN 13 #define DEF_NRF_MISO_PIN 12
#define DEF_SCLK_PIN 14 #endif
#ifndef DEF_NRF_MOSI_PIN
#define DEF_NRF_MOSI_PIN 13
#endif
#ifndef DEF_NRF_SCLK_PIN
#define DEF_NRF_SCLK_PIN 14
#endif
#ifndef DEF_MOTION_SENSOR_PIN
#define DEF_MOTION_SENSOR_PIN A0
#endif
#endif
#ifndef DEF_LED0
#define DEF_LED0 DEF_PIN_OFF
#endif
#ifndef DEF_LED1
#define DEF_LED1 DEF_PIN_OFF
#endif
#ifdef LED_ACTIVE_HIGH
#define LED_HIGH_ACTIVE true
#else
#define LED_HIGH_ACTIVE false
#endif #endif
// default NRF24 power, possible values (0 - 3)
#define DEF_AMPLIFIERPOWER 1
// number of packets hold in buffer // number of packets hold in buffer
#define PACKET_BUFFER_SIZE 30 #define PACKET_BUFFER_SIZE 30
// number of configurable inverters // number of configurable inverters
#if defined(ESP32) #if defined(ESP32)
#if defined(CONFIG_IDF_TARGET_ESP32S3)
#define MAX_NUM_INVERTERS 32
#else
#define MAX_NUM_INVERTERS 16 #define MAX_NUM_INVERTERS 16
#endif
#else #else
#define MAX_NUM_INVERTERS 4 #define MAX_NUM_INVERTERS 4
#endif #endif
// default serial interval
#define SERIAL_INTERVAL 5
// default send interval // default send interval
#define SEND_INTERVAL 30 #define SEND_INTERVAL 15
// maximum human readable inverter name length // maximum human readable inverter name length
#define MAX_NAME_LENGTH 16 #define MAX_NAME_LENGTH 16
@ -106,10 +175,7 @@
#define MAX_RF_PAYLOAD_SIZE 32 #define MAX_RF_PAYLOAD_SIZE 32
// maximum total payload buffers (must be greater than the number of received frame fragments) // maximum total payload buffers (must be greater than the number of received frame fragments)
#define MAX_PAYLOAD_ENTRIES 10 #define MAX_PAYLOAD_ENTRIES 20
// maximum requests for retransmits per payload (per inverter)
#define DEF_MAX_RETRANS_PER_PYLD 5
// number of seconds since last successful response, before inverter is marked inactive // number of seconds since last successful response, before inverter is marked inactive
#define INVERTER_INACT_THRES_SEC 5*60 #define INVERTER_INACT_THRES_SEC 5*60
@ -156,13 +222,6 @@
// reconnect delay // reconnect delay
#define MQTT_RECONNECT_DELAY 5000 #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") #if __has_include("config_override.h")
#include "config_override.h" #include "config_override.h"
#endif #endif

17
src/config/config_override_example.h

@ -9,11 +9,11 @@
// override fallback WiFi info // override fallback WiFi info
#define FB_WIFI_OVERRIDDEN #define FB_WIFI_OVERRIDDEN
// each ovveride must be preceeded with an #undef statement // each override must be preceded with an #undef statement
#undef FB_WIFI_SSID #undef FB_WIFI_SSID
#define FB_WIFI_SSID "MY_SSID" #define FB_WIFI_SSID "MY_SSID"
// each ovveride must be preceeded with an #undef statement // each override must be preceded with an #undef statement
#undef FB_WIFI_PWD #undef FB_WIFI_PWD
#define FB_WIFI_PWD "MY_WIFI_KEY" #define FB_WIFI_PWD "MY_WIFI_KEY"
@ -31,12 +31,17 @@
#undef DEF_SCLK_PIN #undef DEF_SCLK_PIN
#define DEF_SCLK_PIN 36 #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 // To enable the endpoint for prometheus to scrape data from at /metrics
// #define ENABLE_PROMETHEUS_EP // #define ENABLE_PROMETHEUS_EP
// to enable the syslog logging (will disable web-serial)
//#define ENABLE_SYSLOG
#ifdef ENABLE_SYSLOG
#define SYSLOG_HOST "<hostname-or-ip-address-of-syslog-server>"
#define SYSLOG_APP "ahoy"
#define SYSLOG_FACILITY FAC_USER
#define SYSLOG_PORT 514
#endif
#endif /*__CONFIG_OVERRIDE_H__*/ #endif /*__CONFIG_OVERRIDE_H__*/

198
src/config/settings.h

@ -29,7 +29,8 @@
* More info: * More info:
* https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout
* */ * */
#define DEF_PIN_OFF 255
#define CONFIG_VERSION 7
#define PROT_MASK_INDEX 0x0001 #define PROT_MASK_INDEX 0x0001
@ -67,35 +68,31 @@ typedef struct {
bool darkMode; bool darkMode;
bool schedReboot; bool schedReboot;
#if defined(ETHERNET) #if !defined(ETHERNET)
// ethernet
#else /* defined(ETHERNET) */
// wifi // wifi
char stationSsid[SSID_LEN]; char stationSsid[SSID_LEN];
char stationPwd[PWD_LEN]; char stationPwd[PWD_LEN];
char apPwd[PWD_LEN]; char apPwd[PWD_LEN];
bool isHidden; bool isHidden;
#endif /* defined(ETHERNET) */ #endif /* !defined(ETHERNET) */
cfgIp_t ip; cfgIp_t ip;
} cfgSys_t; } cfgSys_t;
typedef struct { typedef struct {
bool enabled; bool enabled;
uint16_t sendInterval;
uint8_t maxRetransPerPyld;
uint8_t pinCs; uint8_t pinCs;
uint8_t pinCe; uint8_t pinCe;
uint8_t pinIrq; uint8_t pinIrq;
uint8_t pinMiso; uint8_t pinMiso;
uint8_t pinMosi; uint8_t pinMosi;
uint8_t pinSclk; uint8_t pinSclk;
uint8_t amplifierPower;
} cfgNrf24_t; } cfgNrf24_t;
typedef struct { typedef struct {
bool enabled; bool enabled;
uint8_t pinSclk;
uint8_t pinSdio;
uint8_t pinCsb; uint8_t pinCsb;
uint8_t pinFcsb; uint8_t pinFcsb;
uint8_t pinIrq; uint8_t pinIrq;
@ -110,20 +107,21 @@ typedef struct {
typedef struct { typedef struct {
float lat; float lat;
float lon; float lon;
bool disNightCom; // disable night communication
uint16_t offsetSec; uint16_t offsetSec;
} cfgSun_t; } cfgSun_t;
typedef struct { typedef struct {
uint16_t interval;
bool showIv; bool showIv;
bool debug; bool debug;
bool privacyLog;
bool printWholeTrace;
} cfgSerial_t; } cfgSerial_t;
typedef struct { typedef struct {
uint8_t led0; // first LED pin uint8_t led0; // first LED pin
uint8_t led1; // second LED pin uint8_t led1; // second LED pin
bool led_high_active; // determines if LEDs are high or low active bool high_active; // determines if LEDs are high or low active
uint8_t luminance; // luminance of LED
} cfgLed_t; } cfgLed_t;
typedef struct { typedef struct {
@ -143,23 +141,31 @@ typedef struct {
uint16_t chMaxPwr[6]; uint16_t chMaxPwr[6];
double yieldCor[6]; // YieldTotal correction value double yieldCor[6]; // YieldTotal correction value
char chName[6][MAX_NAME_LENGTH]; char chName[6][MAX_NAME_LENGTH];
uint8_t frequency;
uint8_t powerLevel;
bool disNightCom; // disable night communication
bool add2Total; // add values to total values - useful if one inverter is on battery to turn off
} cfgIv_t; } cfgIv_t;
typedef struct { typedef struct {
bool enabled; bool enabled;
cfgIv_t iv[MAX_NUM_INVERTERS]; cfgIv_t iv[MAX_NUM_INVERTERS];
uint16_t sendInterval;
bool rstYieldMidNight; bool rstYieldMidNight;
bool rstValsNotAvail; bool rstValsNotAvail;
bool rstValsCommStop; bool rstValsCommStop;
bool rstMaxValsMidNight;
bool startWithoutTime; bool startWithoutTime;
float yieldEffiency; float yieldEffiency;
uint16_t gapMs;
bool readGrid;
} cfgInst_t; } cfgInst_t;
typedef struct { typedef struct {
uint8_t type; uint8_t type;
bool pwrSaveAtIvOffline; bool pwrSaveAtIvOffline;
bool pxShift; uint8_t screenSaver;
uint8_t rot; uint8_t rot;
//uint16_t wakeUp; //uint16_t wakeUp;
//uint16_t sleepAt; //uint16_t sleepAt;
@ -170,6 +176,7 @@ typedef struct {
uint8_t disp_reset; uint8_t disp_reset;
uint8_t disp_busy; uint8_t disp_busy;
uint8_t disp_dc; uint8_t disp_dc;
uint8_t pirPin;
} display_t; } display_t;
typedef struct { typedef struct {
@ -188,6 +195,7 @@ typedef struct {
cfgInst_t inst; cfgInst_t inst;
plugins_t plugin; plugins_t plugin;
bool valid; bool valid;
uint16_t configVersion;
} settings_t; } settings_t;
class settings { class settings {
@ -284,6 +292,7 @@ class settings {
if(root.containsKey(F("led"))) jsonLed(root[F("led")]); if(root.containsKey(F("led"))) jsonLed(root[F("led")]);
if(root.containsKey(F("plugin"))) jsonPlugin(root[F("plugin")]); if(root.containsKey(F("plugin"))) jsonPlugin(root[F("plugin")]);
if(root.containsKey(F("inst"))) jsonInst(root[F("inst")]); if(root.containsKey(F("inst"))) jsonInst(root[F("inst")]);
getConfigVersion(root.as<JsonObject>());
} }
else { else {
Serial.println(F("failed to parse json, using default config")); Serial.println(F("failed to parse json, using default config"));
@ -299,6 +308,7 @@ class settings {
DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE); DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE);
JsonObject root = json.to<JsonObject>(); JsonObject root = json.to<JsonObject>();
json[F("version")] = CONFIG_VERSION;
jsonNetwork(root.createNestedObject(F("wifi")), true); jsonNetwork(root.createNestedObject(F("wifi")), true);
jsonNrf(root.createNestedObject(F("nrf")), true); jsonNrf(root.createNestedObject(F("nrf")), true);
#if defined(ESP32) #if defined(ESP32)
@ -368,36 +378,41 @@ class settings {
mCfg.sys.darkMode = false; mCfg.sys.darkMode = false;
mCfg.sys.schedReboot = false; mCfg.sys.schedReboot = false;
// restore temp settings // restore temp settings
#if defined(ETHERNET)
memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t));
#else /* defined(ETHERNET) */
if(keepWifi) if(keepWifi)
memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t));
#if !defined(ETHERNET)
else { else {
snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID);
snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD);
snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD); snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD);
mCfg.sys.isHidden = false; mCfg.sys.isHidden = false;
} }
#endif /* defined(ETHERNET) */ #endif /* !defined(ETHERNET) */
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME);
mCfg.nrf.sendInterval = SEND_INTERVAL; mCfg.nrf.pinCs = DEF_NRF_CS_PIN;
mCfg.nrf.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; mCfg.nrf.pinCe = DEF_NRF_CE_PIN;
mCfg.nrf.pinCs = DEF_CS_PIN; mCfg.nrf.pinIrq = DEF_NRF_IRQ_PIN;
mCfg.nrf.pinCe = DEF_CE_PIN; mCfg.nrf.pinMiso = DEF_NRF_MISO_PIN;
mCfg.nrf.pinIrq = DEF_IRQ_PIN; mCfg.nrf.pinMosi = DEF_NRF_MOSI_PIN;
mCfg.nrf.pinMiso = DEF_MISO_PIN; mCfg.nrf.pinSclk = DEF_NRF_SCLK_PIN;
mCfg.nrf.pinMosi = DEF_MOSI_PIN;
mCfg.nrf.pinSclk = DEF_SCLK_PIN;
mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03;
mCfg.nrf.enabled = true; mCfg.nrf.enabled = true;
#if defined(ESP32)
mCfg.cmt.pinSclk = DEF_CMT_SCLK;
mCfg.cmt.pinSdio = DEF_CMT_SDIO;
mCfg.cmt.pinCsb = DEF_CMT_CSB;
mCfg.cmt.pinFcsb = DEF_CMT_FCSB;
mCfg.cmt.pinIrq = DEF_CMT_IRQ;
#else
mCfg.cmt.pinSclk = DEF_PIN_OFF;
mCfg.cmt.pinSdio = DEF_PIN_OFF;
mCfg.cmt.pinCsb = DEF_PIN_OFF; mCfg.cmt.pinCsb = DEF_PIN_OFF;
mCfg.cmt.pinFcsb = DEF_PIN_OFF; mCfg.cmt.pinFcsb = DEF_PIN_OFF;
mCfg.cmt.pinIrq = DEF_PIN_OFF; mCfg.cmt.pinIrq = DEF_PIN_OFF;
#endif
mCfg.cmt.enabled = false; mCfg.cmt.enabled = false;
snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME); snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", DEF_NTP_SERVER_NAME);
@ -406,12 +421,12 @@ class settings {
mCfg.sun.lat = 0.0; mCfg.sun.lat = 0.0;
mCfg.sun.lon = 0.0; mCfg.sun.lon = 0.0;
mCfg.sun.disNightCom = false;
mCfg.sun.offsetSec = 0; mCfg.sun.offsetSec = 0;
mCfg.serial.interval = SERIAL_INTERVAL;
mCfg.serial.showIv = false; mCfg.serial.showIv = false;
mCfg.serial.debug = false; mCfg.serial.debug = false;
mCfg.serial.privacyLog = true;
mCfg.serial.printWholeTrace = false;
mCfg.mqtt.port = DEF_MQTT_PORT; mCfg.mqtt.port = DEF_MQTT_PORT;
snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER); snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER);
@ -420,21 +435,31 @@ class settings {
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC);
mCfg.mqtt.interval = 0; // off mCfg.mqtt.interval = 0; // off
mCfg.inst.sendInterval = SEND_INTERVAL;
mCfg.inst.rstYieldMidNight = false; mCfg.inst.rstYieldMidNight = false;
mCfg.inst.rstValsNotAvail = false; mCfg.inst.rstValsNotAvail = false;
mCfg.inst.rstValsCommStop = false; mCfg.inst.rstValsCommStop = false;
mCfg.inst.startWithoutTime = false; mCfg.inst.startWithoutTime = false;
mCfg.inst.yieldEffiency = 0.955f; mCfg.inst.rstMaxValsMidNight = false;
mCfg.inst.yieldEffiency = 1.0f;
mCfg.inst.gapMs = 500;
mCfg.inst.readGrid = true;
mCfg.led.led0 = DEF_PIN_OFF; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
mCfg.led.led1 = DEF_PIN_OFF; mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value
mCfg.led.led_high_active = false; mCfg.inst.iv[i].frequency = 0x12; // 863MHz (minimum allowed frequency)
mCfg.inst.iv[i].disNightCom = false;
mCfg.inst.iv[i].add2Total = true;
}
memset(&mCfg.inst, 0, sizeof(cfgInst_t)); mCfg.led.led0 = DEF_LED0;
mCfg.led.led1 = DEF_LED1;
mCfg.led.high_active = LED_HIGH_ACTIVE;
mCfg.led.luminance = 255;
mCfg.plugin.display.pwrSaveAtIvOffline = false; mCfg.plugin.display.pwrSaveAtIvOffline = false;
mCfg.plugin.display.contrast = 60; mCfg.plugin.display.contrast = 60;
mCfg.plugin.display.pxShift = true; mCfg.plugin.display.screenSaver = 1; // default: 1 .. pixelshift for OLED for downward compatibility
mCfg.plugin.display.rot = 0; mCfg.plugin.display.rot = 0;
mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA
mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL
@ -442,6 +467,45 @@ class settings {
mCfg.plugin.display.disp_reset = DEF_PIN_OFF; mCfg.plugin.display.disp_reset = DEF_PIN_OFF;
mCfg.plugin.display.disp_busy = DEF_PIN_OFF; mCfg.plugin.display.disp_busy = DEF_PIN_OFF;
mCfg.plugin.display.disp_dc = DEF_PIN_OFF; mCfg.plugin.display.disp_dc = DEF_PIN_OFF;
mCfg.plugin.display.pirPin = DEF_MOTION_SENSOR_PIN;
}
void loadAddedDefaults() {
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
if(mCfg.configVersion < 1) {
mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value
mCfg.inst.iv[i].frequency = 0x0; // 860MHz (backward compatibility)
}
if(mCfg.configVersion < 2) {
mCfg.inst.iv[i].disNightCom = false;
mCfg.inst.iv[i].add2Total = true;
}
if(mCfg.configVersion < 3) {
mCfg.serial.printWholeTrace = false;
}
if(mCfg.configVersion < 4) {
mCfg.inst.gapMs = 500;
}
if(mCfg.configVersion < 5) {
mCfg.inst.sendInterval = SEND_INTERVAL;
mCfg.serial.printWholeTrace = false;
}
if(mCfg.configVersion < 6) {
mCfg.inst.gapMs = 500;
mCfg.inst.readGrid = true;
}
if(mCfg.configVersion < 7) {
mCfg.led.luminance = 255;
}
}
}
void getConfigVersion(JsonObject obj) {
getVal<uint16_t>(obj, F("version"), &mCfg.configVersion);
DPRINT(DBG_INFO, F("Config Version: "));
DBGPRINTLN(String(mCfg.configVersion));
if(CONFIG_VERSION != mCfg.configVersion)
loadAddedDefaults();
} }
void jsonNetwork(JsonObject obj, bool set = false) { void jsonNetwork(JsonObject obj, bool set = false) {
@ -489,55 +553,59 @@ class settings {
void jsonNrf(JsonObject obj, bool set = false) { void jsonNrf(JsonObject obj, bool set = false) {
if(set) { if(set) {
obj[F("intvl")] = mCfg.nrf.sendInterval;
obj[F("maxRetry")] = mCfg.nrf.maxRetransPerPyld;
obj[F("cs")] = mCfg.nrf.pinCs; obj[F("cs")] = mCfg.nrf.pinCs;
obj[F("ce")] = mCfg.nrf.pinCe; obj[F("ce")] = mCfg.nrf.pinCe;
obj[F("irq")] = mCfg.nrf.pinIrq; obj[F("irq")] = mCfg.nrf.pinIrq;
obj[F("sclk")] = mCfg.nrf.pinSclk; obj[F("sclk")] = mCfg.nrf.pinSclk;
obj[F("mosi")] = mCfg.nrf.pinMosi; obj[F("mosi")] = mCfg.nrf.pinMosi;
obj[F("miso")] = mCfg.nrf.pinMiso; obj[F("miso")] = mCfg.nrf.pinMiso;
obj[F("pwr")] = mCfg.nrf.amplifierPower;
obj[F("en")] = (bool) mCfg.nrf.enabled; obj[F("en")] = (bool) mCfg.nrf.enabled;
} else { } else {
getVal<uint16_t>(obj, F("intvl"), &mCfg.nrf.sendInterval);
getVal<uint8_t>(obj, F("maxRetry"), &mCfg.nrf.maxRetransPerPyld);
getVal<uint8_t>(obj, F("cs"), &mCfg.nrf.pinCs); getVal<uint8_t>(obj, F("cs"), &mCfg.nrf.pinCs);
getVal<uint8_t>(obj, F("ce"), &mCfg.nrf.pinCe); getVal<uint8_t>(obj, F("ce"), &mCfg.nrf.pinCe);
getVal<uint8_t>(obj, F("irq"), &mCfg.nrf.pinIrq); getVal<uint8_t>(obj, F("irq"), &mCfg.nrf.pinIrq);
getVal<uint8_t>(obj, F("sclk"), &mCfg.nrf.pinSclk); getVal<uint8_t>(obj, F("sclk"), &mCfg.nrf.pinSclk);
getVal<uint8_t>(obj, F("mosi"), &mCfg.nrf.pinMosi); getVal<uint8_t>(obj, F("mosi"), &mCfg.nrf.pinMosi);
getVal<uint8_t>(obj, F("miso"), &mCfg.nrf.pinMiso); getVal<uint8_t>(obj, F("miso"), &mCfg.nrf.pinMiso);
getVal<uint8_t>(obj, F("pwr"), &mCfg.nrf.amplifierPower);
#if !defined(ESP32) #if !defined(ESP32)
mCfg.nrf.enabled = true; // ESP8266, read always as enabled mCfg.nrf.enabled = true; // ESP8266, read always as enabled
#else #else
mCfg.nrf.enabled = (bool) obj[F("en")]; mCfg.nrf.enabled = (bool) obj[F("en")];
#endif #endif
if((obj[F("cs")] == obj[F("ce")])) { if((obj[F("cs")] == obj[F("ce")])) {
mCfg.nrf.pinCs = DEF_CS_PIN; mCfg.nrf.pinCs = DEF_NRF_CS_PIN;
mCfg.nrf.pinCe = DEF_CE_PIN; mCfg.nrf.pinCe = DEF_NRF_CE_PIN;
mCfg.nrf.pinIrq = DEF_IRQ_PIN; mCfg.nrf.pinIrq = DEF_NRF_IRQ_PIN;
mCfg.nrf.pinSclk = DEF_SCLK_PIN; mCfg.nrf.pinSclk = DEF_NRF_SCLK_PIN;
mCfg.nrf.pinMosi = DEF_MOSI_PIN; mCfg.nrf.pinMosi = DEF_NRF_MOSI_PIN;
mCfg.nrf.pinMiso = DEF_MISO_PIN; mCfg.nrf.pinMiso = DEF_NRF_MISO_PIN;
} }
} }
} }
#if defined(ESP32)
void jsonCmt(JsonObject obj, bool set = false) { void jsonCmt(JsonObject obj, bool set = false) {
if(set) { if(set) {
obj[F("csb")] = mCfg.cmt.pinCsb; obj[F("csb")] = mCfg.cmt.pinCsb;
obj[F("fcsb")] = mCfg.cmt.pinFcsb; obj[F("fcsb")] = mCfg.cmt.pinFcsb;
obj[F("irq")] = mCfg.cmt.pinIrq; obj[F("irq")] = mCfg.cmt.pinIrq;
obj[F("dio")] = mCfg.cmt.pinSdio;
obj[F("clk")] = mCfg.cmt.pinSclk;
obj[F("en")] = (bool) mCfg.cmt.enabled; obj[F("en")] = (bool) mCfg.cmt.enabled;
} else { } else {
mCfg.cmt.pinCsb = obj[F("csb")]; mCfg.cmt.pinCsb = obj[F("csb")];
mCfg.cmt.pinFcsb = obj[F("fcsb")]; mCfg.cmt.pinFcsb = obj[F("fcsb")];
mCfg.cmt.pinIrq = obj[F("irq")]; mCfg.cmt.pinIrq = obj[F("irq")];
mCfg.cmt.pinSdio = obj[F("dio")];
mCfg.cmt.pinSclk = obj[F("clk")];
mCfg.cmt.enabled = (bool) obj[F("en")]; mCfg.cmt.enabled = (bool) obj[F("en")];
if(0 == mCfg.cmt.pinSclk)
mCfg.cmt.pinSclk = DEF_CMT_SCLK;
if(0 == mCfg.cmt.pinSdio)
mCfg.cmt.pinSdio = DEF_CMT_SDIO;
} }
} }
#endif
void jsonNtp(JsonObject obj, bool set = false) { void jsonNtp(JsonObject obj, bool set = false) {
if(set) { if(set) {
@ -558,25 +626,25 @@ class settings {
if(set) { if(set) {
obj[F("lat")] = mCfg.sun.lat; obj[F("lat")] = mCfg.sun.lat;
obj[F("lon")] = mCfg.sun.lon; obj[F("lon")] = mCfg.sun.lon;
obj[F("dis")] = mCfg.sun.disNightCom;
obj[F("offs")] = mCfg.sun.offsetSec; obj[F("offs")] = mCfg.sun.offsetSec;
} else { } else {
getVal<float>(obj, F("lat"), &mCfg.sun.lat); getVal<float>(obj, F("lat"), &mCfg.sun.lat);
getVal<float>(obj, F("lon"), &mCfg.sun.lon); getVal<float>(obj, F("lon"), &mCfg.sun.lon);
getVal<bool>(obj, F("dis"), &mCfg.sun.disNightCom);
getVal<uint16_t>(obj, F("offs"), &mCfg.sun.offsetSec); getVal<uint16_t>(obj, F("offs"), &mCfg.sun.offsetSec);
} }
} }
void jsonSerial(JsonObject obj, bool set = false) { void jsonSerial(JsonObject obj, bool set = false) {
if(set) { if(set) {
obj[F("intvl")] = mCfg.serial.interval;
obj[F("show")] = mCfg.serial.showIv; obj[F("show")] = mCfg.serial.showIv;
obj[F("debug")] = mCfg.serial.debug; obj[F("debug")] = mCfg.serial.debug;
obj[F("prv")] = (bool) mCfg.serial.privacyLog;
obj[F("trc")] = (bool) mCfg.serial.printWholeTrace;
} else { } else {
getVal<uint16_t>(obj, F("intvl"), &mCfg.serial.interval);
getVal<bool>(obj, F("show"), &mCfg.serial.showIv); getVal<bool>(obj, F("show"), &mCfg.serial.showIv);
getVal<bool>(obj, F("debug"), &mCfg.serial.debug); getVal<bool>(obj, F("debug"), &mCfg.serial.debug);
getVal<bool>(obj, F("prv"), &mCfg.serial.privacyLog);
getVal<bool>(obj, F("trc"), &mCfg.serial.printWholeTrace);
} }
} }
@ -584,6 +652,7 @@ class settings {
if(set) { if(set) {
obj[F("broker")] = mCfg.mqtt.broker; obj[F("broker")] = mCfg.mqtt.broker;
obj[F("port")] = mCfg.mqtt.port; obj[F("port")] = mCfg.mqtt.port;
obj[F("clientId")] = mCfg.mqtt.clientId;
obj[F("user")] = mCfg.mqtt.user; obj[F("user")] = mCfg.mqtt.user;
obj[F("pwd")] = mCfg.mqtt.pwd; obj[F("pwd")] = mCfg.mqtt.pwd;
obj[F("topic")] = mCfg.mqtt.topic; obj[F("topic")] = mCfg.mqtt.topic;
@ -594,6 +663,7 @@ class settings {
getVal<uint16_t>(obj, F("intvl"), &mCfg.mqtt.interval); getVal<uint16_t>(obj, F("intvl"), &mCfg.mqtt.interval);
getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_LEN); getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_LEN);
getChar(obj, F("user"), mCfg.mqtt.user, MQTT_USER_LEN); getChar(obj, F("user"), mCfg.mqtt.user, MQTT_USER_LEN);
getChar(obj, F("clientId"), mCfg.mqtt.clientId, MQTT_CLIENTID_LEN);
getChar(obj, F("pwd"), mCfg.mqtt.pwd, MQTT_PWD_LEN); getChar(obj, F("pwd"), mCfg.mqtt.pwd, MQTT_PWD_LEN);
getChar(obj, F("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN); getChar(obj, F("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN);
} }
@ -603,11 +673,13 @@ class settings {
if(set) { if(set) {
obj[F("0")] = mCfg.led.led0; obj[F("0")] = mCfg.led.led0;
obj[F("1")] = mCfg.led.led1; obj[F("1")] = mCfg.led.led1;
obj[F("act_high")] = mCfg.led.led_high_active; obj[F("act_high")] = mCfg.led.high_active;
obj[F("lum")] = mCfg.led.luminance;
} else { } else {
getVal<uint8_t>(obj, F("0"), &mCfg.led.led0); getVal<uint8_t>(obj, F("0"), &mCfg.led.led0);
getVal<uint8_t>(obj, F("1"), &mCfg.led.led1); getVal<uint8_t>(obj, F("1"), &mCfg.led.led1);
getVal<bool>(obj, F("act_high"), &mCfg.led.led_high_active); getVal<bool>(obj, F("act_high"), &mCfg.led.high_active);
getVal<uint8_t>(obj, F("lum"), &mCfg.led.luminance);
} }
} }
@ -616,7 +688,7 @@ class settings {
JsonObject disp = obj.createNestedObject("disp"); JsonObject disp = obj.createNestedObject("disp");
disp[F("type")] = mCfg.plugin.display.type; disp[F("type")] = mCfg.plugin.display.type;
disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline;
disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift; disp[F("screenSaver")] = mCfg.plugin.display.screenSaver;
disp[F("rotation")] = mCfg.plugin.display.rot; disp[F("rotation")] = mCfg.plugin.display.rot;
//disp[F("wake")] = mCfg.plugin.display.wakeUp; //disp[F("wake")] = mCfg.plugin.display.wakeUp;
//disp[F("sleep")] = mCfg.plugin.display.sleepAt; //disp[F("sleep")] = mCfg.plugin.display.sleepAt;
@ -627,11 +699,12 @@ class settings {
disp[F("reset")] = mCfg.plugin.display.disp_reset; disp[F("reset")] = mCfg.plugin.display.disp_reset;
disp[F("busy")] = mCfg.plugin.display.disp_busy; disp[F("busy")] = mCfg.plugin.display.disp_busy;
disp[F("dc")] = mCfg.plugin.display.disp_dc; disp[F("dc")] = mCfg.plugin.display.disp_dc;
disp[F("pirPin")] = mCfg.plugin.display.pirPin;
} else { } else {
JsonObject disp = obj["disp"]; JsonObject disp = obj["disp"];
getVal<uint8_t>(disp, F("type"), &mCfg.plugin.display.type); getVal<uint8_t>(disp, F("type"), &mCfg.plugin.display.type);
getVal<bool>(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline); getVal<bool>(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline);
getVal<bool>(disp, F("pxShift"), &mCfg.plugin.display.pxShift); getVal<uint8_t>(disp, F("screenSaver"), &mCfg.plugin.display.screenSaver);
getVal<uint8_t>(disp, F("rotation"), &mCfg.plugin.display.rot); getVal<uint8_t>(disp, F("rotation"), &mCfg.plugin.display.rot);
//mCfg.plugin.display.wakeUp = disp[F("wake")]; //mCfg.plugin.display.wakeUp = disp[F("wake")];
//mCfg.plugin.display.sleepAt = disp[F("sleep")]; //mCfg.plugin.display.sleepAt = disp[F("sleep")];
@ -642,25 +715,34 @@ class settings {
getVal<uint8_t>(disp, F("reset"), &mCfg.plugin.display.disp_reset); getVal<uint8_t>(disp, F("reset"), &mCfg.plugin.display.disp_reset);
getVal<uint8_t>(disp, F("busy"), &mCfg.plugin.display.disp_busy); getVal<uint8_t>(disp, F("busy"), &mCfg.plugin.display.disp_busy);
getVal<uint8_t>(disp, F("dc"), &mCfg.plugin.display.disp_dc); getVal<uint8_t>(disp, F("dc"), &mCfg.plugin.display.disp_dc);
getVal<uint8_t>(disp, F("pirPin"), &mCfg.plugin.display.pirPin);
} }
} }
void jsonInst(JsonObject obj, bool set = false) { void jsonInst(JsonObject obj, bool set = false) {
if(set) { if(set) {
obj[F("intvl")] = mCfg.inst.sendInterval;
obj[F("en")] = (bool)mCfg.inst.enabled; obj[F("en")] = (bool)mCfg.inst.enabled;
obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight;
obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop;
obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime; obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime;
obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstMaxValsMidNight;
obj[F("yldEff")] = mCfg.inst.yieldEffiency; obj[F("yldEff")] = mCfg.inst.yieldEffiency;
obj[F("gap")] = mCfg.inst.gapMs;
obj[F("rdGrid")] = (bool)mCfg.inst.readGrid;
} }
else { else {
getVal<uint16_t>(obj, F("intvl"), &mCfg.inst.sendInterval);
getVal<bool>(obj, F("en"), &mCfg.inst.enabled); getVal<bool>(obj, F("en"), &mCfg.inst.enabled);
getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight); getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight);
getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail); getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail);
getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop); getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop);
getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime); getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime);
getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstMaxValsMidNight);
getVal<float>(obj, F("yldEff"), &mCfg.inst.yieldEffiency); getVal<float>(obj, F("yldEff"), &mCfg.inst.yieldEffiency);
getVal<uint16_t>(obj, F("gap"), &mCfg.inst.gapMs);
getVal<bool>(obj, F("rdGrid"), &mCfg.inst.readGrid);
if(mCfg.inst.yieldEffiency < 0.5) if(mCfg.inst.yieldEffiency < 0.5)
mCfg.inst.yieldEffiency = 1.0f; mCfg.inst.yieldEffiency = 1.0f;
@ -685,6 +767,10 @@ class settings {
obj[F("en")] = (bool)cfg->enabled; obj[F("en")] = (bool)cfg->enabled;
obj[F("name")] = cfg->name; obj[F("name")] = cfg->name;
obj[F("sn")] = cfg->serial.u64; obj[F("sn")] = cfg->serial.u64;
obj[F("freq")] = cfg->frequency;
obj[F("pa")] = cfg->powerLevel;
obj[F("dis")] = cfg->disNightCom;
obj[F("add")] = cfg->add2Total;
for(uint8_t i = 0; i < 6; i++) { for(uint8_t i = 0; i < 6; i++) {
obj[F("yield")][i] = cfg->yieldCor[i]; obj[F("yield")][i] = cfg->yieldCor[i];
obj[F("pwr")][i] = cfg->chMaxPwr[i]; obj[F("pwr")][i] = cfg->chMaxPwr[i];
@ -694,6 +780,10 @@ class settings {
getVal<bool>(obj, F("en"), &cfg->enabled); getVal<bool>(obj, F("en"), &cfg->enabled);
getChar(obj, F("name"), cfg->name, MAX_NAME_LENGTH); getChar(obj, F("name"), cfg->name, MAX_NAME_LENGTH);
getVal<uint64_t>(obj, F("sn"), &cfg->serial.u64); getVal<uint64_t>(obj, F("sn"), &cfg->serial.u64);
getVal<uint8_t>(obj, F("freq"), &cfg->frequency);
getVal<uint8_t>(obj, F("pa"), &cfg->powerLevel);
getVal<bool>(obj, F("dis"), &cfg->disNightCom);
getVal<bool>(obj, F("add"), &cfg->add2Total);
uint8_t size = 4; uint8_t size = 4;
if(obj.containsKey(F("pwr"))) if(obj.containsKey(F("pwr")))
size = obj[F("pwr")].size(); size = obj[F("pwr")].size();

8
src/defines.h

@ -12,14 +12,16 @@
// VERSION // VERSION
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 7 #define VERSION_MINOR 8
#define VERSION_PATCH 36 #define VERSION_PATCH 36
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {
uint8_t ch; uint8_t ch;
uint8_t len; uint8_t len;
int8_t rssi;
uint8_t packet[MAX_RF_PAYLOAD_SIZE]; uint8_t packet[MAX_RF_PAYLOAD_SIZE];
uint16_t millis;
} packet_t; } packet_t;
typedef enum { typedef enum {
@ -92,11 +94,15 @@ enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
#define MQTT_MAX_PACKET_SIZE 384 #define MQTT_MAX_PACKET_SIZE 384
#define PLUGIN_DISPLAY
typedef struct { typedef struct {
uint32_t rxFail; uint32_t rxFail;
uint32_t rxFailNoAnser; uint32_t rxFailNoAnser;
uint32_t rxSuccess; uint32_t rxSuccess;
uint32_t frmCnt; uint32_t frmCnt;
uint32_t txCnt;
uint32_t retransmits;
} statistics_t; } statistics_t;
#endif /*__DEFINES_H__*/ #endif /*__DEFINES_H__*/

62
src/eth/ahoyeth.cpp

@ -10,7 +10,7 @@
#define F(sl) (sl) #define F(sl) (sl)
#endif #endif
#include "ahoyeth.h" #include "ahoyeth.h"
#include <ESPmDNS.h>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
ahoyeth::ahoyeth() ahoyeth::ahoyeth()
@ -26,12 +26,16 @@ void ahoyeth::setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNe
mOnNetworkCB = onNetworkCB; mOnNetworkCB = onNetworkCB;
mOnTimeCB = onTimeCB; mOnTimeCB = onTimeCB;
DPRINTLN(DBG_INFO, F("[ETH]: Register for events..."));
Serial.flush(); Serial.flush();
WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void { this->onEthernetEvent(event, info); }); WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void { this->onEthernetEvent(event, info); });
DPRINTLN(DBG_INFO, F("[ETH]: begin..."));
Serial.flush(); Serial.flush();
#if defined(CONFIG_IDF_TARGET_ESP32S3)
mEthSpi.begin(DEF_ETH_MISO_PIN, DEF_ETH_MOSI_PIN, DEF_ETH_SCK_PIN, DEF_ETH_CS_PIN, DEF_ETH_IRQ_PIN, DEF_ETH_RST_PIN);
#else
ETH.begin(DEF_ETH_MISO_PIN, DEF_ETH_MOSI_PIN, DEF_ETH_SCK_PIN, DEF_ETH_CS_PIN, DEF_ETH_IRQ_PIN, ETH_SPI_CLOCK_MHZ, ETH_SPI_HOST);
#endif
if(mConfig->sys.ip.ip[0] != 0) { if(mConfig->sys.ip.ip[0] != 0) {
IPAddress ip(mConfig->sys.ip.ip); IPAddress ip(mConfig->sys.ip.ip);
IPAddress mask(mConfig->sys.ip.mask); IPAddress mask(mConfig->sys.ip.mask);
@ -41,8 +45,6 @@ void ahoyeth::setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNe
if(!ETH.config(ip, gateway, mask, dns1, dns2)) if(!ETH.config(ip, gateway, mask, dns1, dns2))
DPRINTLN(DBG_ERROR, F("failed to set static IP!")); DPRINTLN(DBG_ERROR, F("failed to set static IP!"));
} }
ETH.begin(ETH_MISO_GPIO, ETH_MOSI_GPIO, ETH_SCK_GPIO, ETH_CS_PIN, ETH_INT_GPIO, ETH_SPI_CLOCK_MHZ, ETH_SPI_HOST);
} }
@ -57,8 +59,7 @@ bool ahoyeth::updateNtpTime(void) {
return false; return false;
DPRINTLN(DBG_DEBUG, F("updateNtpTime: checking udp \"connection\"...")); Serial.flush(); DPRINTLN(DBG_DEBUG, F("updateNtpTime: checking udp \"connection\"...")); Serial.flush();
if (!mUdp.connected()) if (!mUdp.connected()) {
{
DPRINTLN(DBG_DEBUG, F("updateNtpTime: About to (re)connect...")); Serial.flush(); DPRINTLN(DBG_DEBUG, F("updateNtpTime: About to (re)connect...")); Serial.flush();
IPAddress timeServer; IPAddress timeServer;
if (!WiFi.hostByName(mConfig->ntp.addr, timeServer)) if (!WiFi.hostByName(mConfig->ntp.addr, timeServer))
@ -68,8 +69,7 @@ bool ahoyeth::updateNtpTime(void) {
return false; return false;
DPRINTLN(DBG_DEBUG, F("updateNtpTime: Connected...")); Serial.flush(); DPRINTLN(DBG_DEBUG, F("updateNtpTime: Connected...")); Serial.flush();
mUdp.onPacket([this](AsyncUDPPacket packet) mUdp.onPacket([this](AsyncUDPPacket packet) {
{
DPRINTLN(DBG_DEBUG, F("updateNtpTime: about to handle ntp packet...")); Serial.flush(); DPRINTLN(DBG_DEBUG, F("updateNtpTime: about to handle ntp packet...")); Serial.flush();
this->handleNTPPacket(packet); this->handleNTPPacket(packet);
}); });
@ -129,11 +129,9 @@ void ahoyeth::welcome(String ip, String mode) {
DBGPRINTLN(F("--------------------------------\n")); DBGPRINTLN(F("--------------------------------\n"));
} }
void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info) void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info) {
{
AWS_LOG(F("[ETH]: Got event...")); AWS_LOG(F("[ETH]: Got event..."));
switch (event) switch (event) {
{
#if ( ( defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2) ) && ( ARDUINO_ESP32_GIT_VER != 0x46d5afb1 ) ) #if ( ( defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2) ) && ( ARDUINO_ESP32_GIT_VER != 0x46d5afb1 ) )
// For breaking core v2.0.0 // For breaking core v2.0.0
// Why so strange to define a breaking enum arduino_event_id_t in WiFiGeneric.h // Why so strange to define a breaking enum arduino_event_id_t in WiFiGeneric.h
@ -153,16 +151,16 @@ void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info)
break; break;
case ARDUINO_EVENT_ETH_GOT_IP: case ARDUINO_EVENT_ETH_GOT_IP:
if (!ESP32_W5500_eth_connected) if (!ESP32_W5500_eth_connected) {
{ #if defined (CONFIG_IDF_TARGET_ESP32S3)
AWS_LOG3(F("ETH MAC: "), mEthSpi.macAddress(), F(", IPv4: "), ETH.localIP());
#else
AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP()); AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP());
#endif
if (ETH.fullDuplex()) if (ETH.fullDuplex()) {
{
AWS_LOG0(F("FULL_DUPLEX, ")); AWS_LOG0(F("FULL_DUPLEX, "));
} } else {
else
{
AWS_LOG0(F("HALF_DUPLEX, ")); AWS_LOG0(F("HALF_DUPLEX, "));
} }
@ -171,6 +169,13 @@ void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info)
ESP32_W5500_eth_connected = true; ESP32_W5500_eth_connected = true;
mOnNetworkCB(true); mOnNetworkCB(true);
} }
if (!MDNS.begin(mConfig->sys.deviceName)) {
DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!"));
} else {
DBGPRINT(F("[WiFi] mDNS established: "));
DBGPRINT(mConfig->sys.deviceName);
DBGPRINTLN(F(".local"));
}
break; break;
case ARDUINO_EVENT_ETH_DISCONNECTED: case ARDUINO_EVENT_ETH_DISCONNECTED:
@ -208,16 +213,12 @@ void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info)
break; break;
case SYSTEM_EVENT_ETH_GOT_IP: case SYSTEM_EVENT_ETH_GOT_IP:
if (!ESP32_W5500_eth_connected) if (!ESP32_W5500_eth_connected) {
{
AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP()); AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP());
if (ETH.fullDuplex()) if (ETH.fullDuplex()) {
{
AWS_LOG0(F("FULL_DUPLEX, ")); AWS_LOG0(F("FULL_DUPLEX, "));
} } else {
else
{
AWS_LOG0(F("HALF_DUPLEX, ")); AWS_LOG0(F("HALF_DUPLEX, "));
} }
@ -226,6 +227,13 @@ void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info)
ESP32_W5500_eth_connected = true; ESP32_W5500_eth_connected = true;
mOnNetworkCB(true); mOnNetworkCB(true);
} }
if (!MDNS.begin(mConfig->sys.deviceName)) {
DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!"));
} else {
DBGPRINT(F("[WiFi] mDNS established: "));
DBGPRINT(mConfig->sys.deviceName);
DBGPRINTLN(F(".local"));
}
break; break;
case SYSTEM_EVENT_ETH_DISCONNECTED: case SYSTEM_EVENT_ETH_DISCONNECTED:

4
src/eth/ahoyeth.h

@ -13,6 +13,7 @@
#include <AsyncUDP.h> #include <AsyncUDP.h>
#include <DNSServer.h> #include <DNSServer.h>
#include "ethSpi.h"
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include "../config/config.h" #include "../config/config.h"
#include "../config/settings.h" #include "../config/settings.h"
@ -45,6 +46,9 @@ class ahoyeth {
void onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info); void onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info);
private: private:
#if defined(CONFIG_IDF_TARGET_ESP32S3)
EthSpi mEthSpi;
#endif
settings_t *mConfig; settings_t *mConfig;
uint32_t *mUtcTimestamp; uint32_t *mUtcTimestamp;

141
src/eth/ethSpi.h

@ -0,0 +1,141 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#if defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(ETHERNET)
#ifndef __ETH_SPI_H__
#define __ETH_SPI_H__
#pragma once
#include <Arduino.h>
#include <esp_netif.h>
#include <WiFiGeneric.h>
#include <driver/spi_master.h>
// Functions from WiFiGeneric
void tcpipInit();
void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif);
class EthSpi {
public:
EthSpi() :
eth_handle(nullptr),
eth_netif(nullptr) {}
void begin(int8_t pin_miso, int8_t pin_mosi, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) {
gpio_reset_pin(static_cast<gpio_num_t>(pin_rst));
gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0);
gpio_reset_pin(static_cast<gpio_num_t>(pin_sclk));
gpio_reset_pin(static_cast<gpio_num_t>(pin_mosi));
gpio_reset_pin(static_cast<gpio_num_t>(pin_miso));
gpio_reset_pin(static_cast<gpio_num_t>(pin_cs));
gpio_set_pull_mode(static_cast<gpio_num_t>(pin_miso), GPIO_PULLUP_ONLY);
// Workaround, because calling gpio_install_isr_service directly causes issues with attachInterrupt later
attachInterrupt(digitalPinToInterrupt(pin_int), nullptr, CHANGE);
detachInterrupt(digitalPinToInterrupt(pin_int));
gpio_reset_pin(static_cast<gpio_num_t>(pin_int));
gpio_set_pull_mode(static_cast<gpio_num_t>(pin_int), GPIO_PULLUP_ONLY);
spi_bus_config_t buscfg = {
.mosi_io_num = pin_mosi,
.miso_io_num = pin_miso,
.sclk_io_num = pin_sclk,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0, // uses default value internally
.flags = 0,
.intr_flags = 0
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO));
spi_device_interface_config_t devcfg = {
.command_bits = 16, // actually address phase
.address_bits = 8, // actually command phase
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0, // only 0 supported
.cs_ena_posttrans = 0, // only 0 supported
.clock_speed_hz = 20000000, // stable with on OpenDTU Fusion Shield
.input_delay_ns = 0,
.spics_io_num = pin_cs,
.flags = 0,
.queue_size = 20,
.pre_cb = nullptr,
.post_cb = nullptr
};
spi_device_handle_t spi;
ESP_ERROR_CHECK(spi_bus_add_device(SPI3_HOST, &devcfg, &spi));
// Reset sequence
delayMicroseconds(500);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
delayMicroseconds(1000);
// Arduino function to start networking stack if not already started
tcpipInit();
ESP_ERROR_CHECK(tcpip_adapter_set_default_eth_handlers()); // ?
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi);
w5500_config.int_gpio_num = pin_int;
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
mac_config.rx_task_stack_size = 4096;
esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
phy_config.reset_gpio_num = -1;
esp_eth_phy_t *phy = esp_eth_phy_new_w5500(&phy_config);
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy);
ESP_ERROR_CHECK(esp_eth_driver_install(&eth_config, &eth_handle));
// Configure MAC address
uint8_t mac_addr[6];
ESP_ERROR_CHECK(esp_efuse_mac_get_default(mac_addr));
mac_addr[5] |= 0x03; // derive ethernet MAC address from base MAC address
ESP_ERROR_CHECK(esp_eth_ioctl(eth_handle, ETH_CMD_S_MAC_ADDR, mac_addr));
esp_netif_config_t netif_config = ESP_NETIF_DEFAULT_ETH();
eth_netif = esp_netif_new(&netif_config);
ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle)));
// Add to Arduino
add_esp_interface_netif(ESP_IF_ETH, eth_netif);
ESP_ERROR_CHECK(esp_eth_start(eth_handle));
}
String macAddress() {
uint8_t mac_addr[6] = {0, 0, 0, 0, 0, 0};
esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr);
char mac_addr_str[24];
snprintf(mac_addr_str, sizeof(mac_addr_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return String(mac_addr_str);
}
private:
esp_eth_handle_t eth_handle;
esp_netif_t *eth_netif;
};
#endif /*__ETH_SPI_H__*/
#endif /*ETHERNET*/
#endif /*CONFIG_IDF_TARGET_ESP32S3*/

119
src/hm/CommQueue.h

@ -0,0 +1,119 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __COMM_QUEUE_H__
#define __COMM_QUEUE_H__
#include <array>
#include <functional>
#include "hmInverter.h"
#include "../utils/dbg.h"
template <uint8_t N=100>
class CommQueue {
public:
CommQueue() {}
void addImportant(Inverter<> *iv, uint8_t cmd) {
dec(&mRdPtr);
mQueue[mRdPtr] = queue_s(iv, cmd, true);
}
void add(Inverter<> *iv, uint8_t cmd) {
mQueue[mWrPtr] = queue_s(iv, cmd, false);
inc(&mWrPtr);
}
void chgCmd(Inverter<> *iv, uint8_t cmd) {
mQueue[mWrPtr] = queue_s(iv, cmd, false);
}
uint8_t getFillState(void) {
//DPRINTLN(DBG_INFO, "wr: " + String(mWrPtr) + ", rd: " + String(mRdPtr));
return abs(mRdPtr - mWrPtr);
}
uint8_t getMaxFill(void) {
return N;
}
protected:
struct queue_s {
Inverter<> *iv;
uint8_t cmd;
uint8_t attempts;
uint32_t ts;
bool isDevControl;
queue_s() {}
queue_s(Inverter<> *i, uint8_t c, bool dev) :
iv(i), cmd(c), attempts(5), ts(0), isDevControl(dev) {}
};
protected:
void add(queue_s q) {
mQueue[mWrPtr] = q;
inc(&mWrPtr);
}
void add(const queue_s *q, bool rstAttempts = false) {
mQueue[mWrPtr] = *q;
if(rstAttempts)
mQueue[mWrPtr].attempts = 5;
inc(&mWrPtr);
}
void chgCmd(uint8_t cmd) {
mQueue[mRdPtr].cmd = cmd;
mQueue[mRdPtr].isDevControl = false;
}
void get(std::function<void(bool valid, const queue_s *q)> cb) {
if(mRdPtr == mWrPtr) {
cb(false, &mQueue[mRdPtr]); // empty
return;
}
cb(true, &mQueue[mRdPtr]);
}
void cmdDone(bool keep = false) {
if(keep) {
mQueue[mRdPtr].attempts = 5;
add(mQueue[mRdPtr]); // add to the end again
}
inc(&mRdPtr);
}
void setTs(uint32_t *ts) {
mQueue[mRdPtr].ts = *ts;
}
void setAttempt(void) {
if(mQueue[mRdPtr].attempts)
mQueue[mRdPtr].attempts--;
}
void incrAttempt(uint8_t attempts = 1) {
mQueue[mRdPtr].attempts += attempts;
}
void inc(uint8_t *ptr) {
if(++(*ptr) >= N)
*ptr = 0;
}
void dec(uint8_t *ptr) {
if((*ptr) == 0)
*ptr = N-1;
else
--(*ptr);
}
protected:
std::array<queue_s, N> mQueue;
uint8_t mWrPtr = 0;
uint8_t mRdPtr = 0;
};
#endif /*__COMM_QUEUE_H__*/

918
src/hm/Communication.h

@ -0,0 +1,918 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__
#include "CommQueue.h"
#include <Arduino.h>
#include "../utils/crc.h"
#include "../utils/timemonitor.h"
#include "Heuristic.h"
#define MI_TIMEOUT 250 // timeout for MI type requests
#define FRSTMSG_TIMEOUT 150 // how long to wait for first msg to be received
#define DEFAULT_TIMEOUT 500 // timeout for regular requests
#define SINGLEFR_TIMEOUT 100 // timeout for single frame requests
#define MAX_BUFFER 250
typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType;
typedef std::function<void(Inverter<> *)> alarmListenerType;
class Communication : public CommQueue<> {
public:
void setup(uint32_t *timestamp, bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint16_t *inverterGap) {
mTimestamp = timestamp;
mPrivacyMode = privacyMode;
mSerialDebug = serialDebug;
mPrintWholeTrace = printWholeTrace;
mInverterGap = inverterGap;
}
void addImportant(Inverter<> *iv, uint8_t cmd) {
mState = States::RESET; // cancel current operation
CommQueue::addImportant(iv, cmd);
}
void addPayloadListener(payloadListenerType cb) {
mCbPayload = cb;
}
void addAlarmListener(alarmListenerType cb) {
mCbAlarm = cb;
}
void loop() {
get([this](bool valid, const queue_s *q) {
if(!valid) {
if(mPrintSequenceDuration) {
mPrintSequenceDuration = false;
DPRINT(DBG_INFO, F("com loop duration: "));
DBGPRINT(String(millis() - mLastEmptyQueueMillis));
DBGPRINTLN(F("ms"));
DBGPRINTLN(F("-----"));
}
return; // empty
}
if(!mPrintSequenceDuration) // entry was added to the queue
mLastEmptyQueueMillis = millis();
mPrintSequenceDuration = true;
uint16_t timeout = (q->iv->ivGen == IV_MI) ? MI_TIMEOUT : (((q->iv->mGotFragment && q->iv->mGotLastMsg) || mIsRetransmit) ? SINGLEFR_TIMEOUT : ((q->cmd != AlarmData) && (q->cmd != GridOnProFilePara) ? DEFAULT_TIMEOUT : (1.5 * DEFAULT_TIMEOUT)));
/*if(mDebugState != mState) {
DPRINT(DBG_INFO, F("State: "));
DBGHEXLN((uint8_t)(mState));
mDebugState = mState;
}*/
switch(mState) {
case States::RESET:
if (!mWaitTime.isTimeout())
return;
mMaxFrameId = 0;
for(uint8_t i = 0; i < MAX_PAYLOAD_ENTRIES; i++) {
mLocalBuf[i].len = 0;
}
if(*mSerialDebug)
mHeu.printStatus(q->iv);
mHeu.getTxCh(q->iv);
q->iv->mGotFragment = false;
q->iv->mGotLastMsg = false;
q->iv->curFrmCnt = 0;
mIsRetransmit = false;
if(NULL == q->iv->radio)
cmdDone(false); // can't communicate while radio is not defined!
q->iv->mCmd = q->cmd;
q->iv->mIsSingleframeReq = false;
mState = States::START;
break;
case States::START:
setTs(mTimestamp);
if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) {
// frequency was changed during runtime
if(q->iv->curCmtFreq != q->iv->config->frequency) {
if(q->iv->radio->switchFrequencyCh(q->iv, q->iv->curCmtFreq, q->iv->config->frequency))
q->iv->curCmtFreq = q->iv->config->frequency;
}
}
if(q->isDevControl) {
if(ActivePowerContr == q->cmd)
q->iv->powerLimitAck = false;
q->iv->radio->sendControlPacket(q->iv, q->cmd, q->iv->powerLimit, false);
} else
q->iv->radio->prepareDevInformCmd(q->iv, q->cmd, q->ts, q->iv->alarmLastId, false);
q->iv->radioStatistics.txCnt++;
mWaitTime.startTimeMonitor(timeout);
mIsRetransmit = false;
setAttempt();
if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara))
incrAttempt(q->cmd == AlarmData? 5 : 3);
mState = States::WAIT;
break;
case States::WAIT:
if (!mWaitTime.isTimeout())
return;
mState = States::CHECK_FRAMES;
break;
case States::CHECK_FRAMES: {
if((q->iv->radio->mBufCtrl.empty() && !mIsRetransmit) || (0 == q->attempts)) { // radio buffer empty or no more answers
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("request timeout: "));
DBGPRINT(String(mWaitTime.getRunTime()));
DBGPRINTLN(F("ms"));
}
if(!q->iv->mGotFragment) {
if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) {
q->iv->radio->switchFrequency(q->iv, HOY_BOOT_FREQ_KHZ, (q->iv->config->frequency*FREQ_STEP_KHZ + HOY_BASE_FREQ_KHZ));
mWaitTime.startTimeMonitor(1000);
}
}
closeRequest(q, false);
break;
}
mFirstTry = false; // for correct reset
if((IV_MI != q->iv->ivGen) || (0 == q->attempts))
mIsRetransmit = false;
while(!q->iv->radio->mBufCtrl.empty()) {
packet_t *p = &q->iv->radio->mBufCtrl.front();
printRxInfo(q, p);
if(validateIvSerial(&p->packet[1], q->iv)) {
q->iv->radioStatistics.frmCnt++;
q->iv->mDtuRxCnt++;
if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
if(parseFrame(p))
q->iv->curFrmCnt++;
} else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
if(parseDevCtrl(p, q))
closeRequest(q, true);
else
closeRequest(q, false);
q->iv->radio->mBufCtrl.pop();
return; // don't wait for empty buffer
} else if(IV_MI == q->iv->ivGen) {
if(parseMiFrame(p, q))
q->iv->curFrmCnt++;
}
} //else -> serial does not match
q->iv->radio->mBufCtrl.pop();
yield();
}
if(0 == q->attempts) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("no attempts left"));
closeRequest(q, false);
} else {
if(q->iv->ivGen != IV_MI) {
mState = States::CHECK_PACKAGE;
} else {
bool fastNext = true;
if(q->iv->miMultiParts < 6) {
mState = States::WAIT;
if((mWaitTime.isTimeout() && mIsRetransmit) || !mIsRetransmit) {
miRepeatRequest(q);
return;
}
} else {
mHeu.evalTxChQuality(q->iv, true, (4 - q->attempts), q->iv->curFrmCnt);
if(((q->cmd == 0x39) && (q->iv->type == INV_TYPE_4CH))
|| ((q->cmd == MI_REQ_CH2) && (q->iv->type == INV_TYPE_2CH))
|| ((q->cmd == MI_REQ_CH1) && (q->iv->type == INV_TYPE_1CH))) {
miComplete(q->iv);
fastNext = false;
}
if(fastNext)
miNextRequest(q->iv->type == INV_TYPE_4CH ? MI_REQ_4CH : MI_REQ_CH1, q);
else
closeRequest(q, true);
}
}
}
}
break;
case States::CHECK_PACKAGE:
uint8_t framnr = 0;
if(0 == mMaxFrameId) {
uint8_t i = 0;
while(i < MAX_PAYLOAD_ENTRIES) {
if(mLocalBuf[i].len == 0) {
framnr = i+1;
break;
}
i++;
}
}
if(!framnr) {
for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(mLocalBuf[i].len == 0) {
framnr = i+1;
break;
}
}
}
if(framnr) {
setAttempt();
if(*mSerialDebug) {
DPRINT_IVID(DBG_WARN, q->iv->id);
DBGPRINT(F("frame "));
DBGPRINT(String(framnr));
DBGPRINT(F(" missing: request retransmit ("));
DBGPRINT(String(q->attempts));
DBGPRINTLN(F(" attempts left)"));
}
if (!mIsRetransmit)
q->iv->mIsSingleframeReq = true;
sendRetransmit(q, (framnr-1));
mIsRetransmit = true;
return;
}
compilePayload(q);
if((NULL != mCbPayload) && (GridOnProFilePara != q->cmd) && (GetLossRate != q->cmd))
(mCbPayload)(q->cmd, q->iv);
closeRequest(q, true);
break;
}
});
}
private:
inline void printRxInfo(const queue_s *q, packet_t *p) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("RX "));
if(p->millis < 100)
DBGPRINT(F(" "));
DBGPRINT(String(p->millis));
DBGPRINT(F("ms | "));
DBGPRINT(String(p->len));
if((IV_HM == q->iv->ivGen) || (IV_MI == q->iv->ivGen)) {
DBGPRINT(F(" CH"));
if(3 == p->ch)
DBGPRINT(F("0"));
DBGPRINT(String(p->ch));
DBGPRINT(F(" "));
} else {
DBGPRINT(F(" "));
DBGPRINT(String(p->rssi));
DBGPRINT(F("dBm "));
}
if(*mPrintWholeTrace) {
DBGPRINT(F("| "));
if(*mPrivacyMode)
ah::dumpBuf(p->packet, p->len, 1, 8);
else
ah::dumpBuf(p->packet, p->len);
} else {
DBGPRINT(F("| "));
DHEX(p->packet[0]);
DBGPRINT(F(" "));
DBGHEXLN(p->packet[9]);
}
}
inline bool validateIvSerial(uint8_t buf[], Inverter<> *iv) {
uint8_t tmp[4];
CP_U32_BigEndian(tmp, iv->radioId.u64 >> 8);
for(uint8_t i = 0; i < 4; i++) {
if(tmp[i] != buf[i]) {
DPRINT(DBG_WARN, F("Inverter serial does not match, got: 0x"));
DHEX(buf[0]);DHEX(buf[1]);DHEX(buf[2]);DHEX(buf[3]);
DBGPRINT(F(", expected: 0x"));
DHEX(tmp[0]);DHEX(tmp[1]);DHEX(tmp[2]);DHEX(tmp[3]);
DBGPRINTLN("");
return false;
}
}
return true;
}
inline bool checkFrameCrc(uint8_t buf[], uint8_t len) {
return (ah::crc8(buf, len - 1) == buf[len-1]);
}
inline bool parseFrame(packet_t *p) {
uint8_t *frameId = &p->packet[9];
if(0x00 == *frameId) {
DPRINTLN(DBG_WARN, F("invalid frameId 0x00"));
return false; // skip current packet
}
if((*frameId & 0x7f) > MAX_PAYLOAD_ENTRIES) {
DPRINTLN(DBG_WARN, F("local buffer to small for payload fragments"));
return false; // local storage is to small for id
}
if(!checkFrameCrc(p->packet, p->len)) {
DPRINTLN(DBG_WARN, F("frame CRC is wrong"));
return false; // CRC8 is wrong, frame invalid
}
if((*frameId & ALL_FRAMES) == ALL_FRAMES) {
mMaxFrameId = (*frameId & 0x7f);
if(mMaxFrameId > 8) // large payloads, e.g. AlarmData
incrAttempt(mMaxFrameId - 6);
}
frame_t *f = &mLocalBuf[(*frameId & 0x7f) - 1];
memcpy(f->buf, &p->packet[10], p->len-11);
f->len = p->len - 11;
f->rssi = p->rssi;
return true;
}
inline bool parseMiFrame(packet_t *p, const queue_s *q) {
if ((p->packet[0] == MI_REQ_CH1 + ALL_FRAMES)
|| (p->packet[0] == MI_REQ_CH2 + ALL_FRAMES)
|| ((p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES))
&& (p->packet[0] < (0x39 + SINGLE_FRAME))
)) { //&& (p->packet[0] != (0x0f + ALL_FRAMES)))) {
// small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39
//mPayload[iv->id].txId = p->packet[0];
miDataDecode(p, q);
} else if (p->packet[0] == (0x0f + ALL_FRAMES))
miHwDecode(p, q);
else if ((p->packet[0] == 0x88) || (p->packet[0] == 0x92)) {
record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure
rec->ts = q->ts;
miStsConsolidate(q, ((p->packet[0] == 0x88) ? 1 : 2), rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]);
//mHeu.setGotFragment(q->iv); only do this when we are through the cycle?
}
return true;
}
inline bool parseDevCtrl(packet_t *p, const queue_s *q) {
switch(p->packet[12]) {
case ActivePowerContr:
if(p->packet[13] != 0x00)
return false;
break;
case TurnOn: [[fallthrough]];
case TurnOff: [[fallthrough]];
case Restart:
return true;
break;
default:
DPRINT(DBG_WARN, F("unknown dev ctrl: "));
DBGHEXLN(p->packet[12]);
break;
}
bool accepted = true;
if((p->packet[10] == 0x00) && (p->packet[11] == 0x00))
q->iv->powerLimitAck = true;
else
accepted = false;
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("has "));
if(!accepted) DBGPRINT(F("not "));
DBGPRINT(F("accepted power limit set point "));
DBGPRINT(String(q->iv->powerLimit[0]));
DBGPRINT(F(" with PowerLimitControl "));
DBGPRINTLN(String(q->iv->powerLimit[1]));
q->iv->actPowerLimit = 0xffff; // unknown, readback current value
return accepted;
}
inline void compilePayload(const queue_s *q) {
uint16_t crc = 0xffff, crcRcv = 0x0000;
for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(i == (mMaxFrameId - 1)) {
crc = ah::crc16(mLocalBuf[i].buf, mLocalBuf[i].len - 2, crc);
crcRcv = (mLocalBuf[i].buf[mLocalBuf[i].len-2] << 8);
crcRcv |= mLocalBuf[i].buf[mLocalBuf[i].len-1];
} else
crc = ah::crc16(mLocalBuf[i].buf, mLocalBuf[i].len, crc);
}
if(crc != crcRcv) {
DPRINT_IVID(DBG_WARN, q->iv->id);
DBGPRINT(F("CRC Error "));
if(q->attempts == 0) {
DBGPRINTLN(F("-> Fail"));
closeRequest(q, false);
} else
DBGPRINTLN(F("-> complete retransmit"));
mState = States::RESET;
return;
}
/*DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("procPyld: cmd: 0x"));
DBGHEXLN(q->cmd);*/
memset(mPayload, 0, MAX_BUFFER);
int8_t rssi = -127;
uint8_t len = 0;
for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(mLocalBuf[i].len + len > MAX_BUFFER) {
DPRINTLN(DBG_ERROR, F("payload buffer to small!"));
return;
}
memcpy(&mPayload[len], mLocalBuf[i].buf, mLocalBuf[i].len);
len += mLocalBuf[i].len;
// get worst RSSI (high value is better)
if(mLocalBuf[i].rssi > rssi)
rssi = mLocalBuf[i].rssi;
}
len -= 2;
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("Payload ("));
DBGPRINT(String(len));
if(*mPrintWholeTrace) {
DBGPRINT(F("): "));
ah::dumpBuf(mPayload, len);
} else
DBGPRINTLN(F(")"));
if(GridOnProFilePara == q->cmd) {
q->iv->addGridProfile(mPayload, len);
return;
}
record_t<> *rec = q->iv->getRecordStruct(q->cmd);
if(NULL == rec) {
if(GetLossRate == q->cmd) {
q->iv->parseGetLossRate(mPayload, len);
return;
} else {
DPRINTLN(DBG_ERROR, F("record is NULL!"));
closeRequest(q, false);
}
return;
}
if((rec->pyldLen != len) && (0 != rec->pyldLen)) {
if(*mSerialDebug) {
DPRINT(DBG_ERROR, F("plausibility check failed, expected "));
DBGPRINT(String(rec->pyldLen));
DBGPRINTLN(F(" bytes"));
}
/*q->iv->radioStatistics.rxFail++;*/
closeRequest(q, false);
return;
}
rec->ts = q->ts;
for (uint8_t i = 0; i < rec->length; i++) {
q->iv->addValue(i, mPayload, rec);
}
q->iv->rssi = rssi;
q->iv->doCalculations();
if(AlarmData == q->cmd) {
uint8_t i = 0;
while(1) {
if(0 == q->iv->parseAlarmLog(i++, mPayload, len))
break;
if (NULL != mCbAlarm)
(mCbAlarm)(q->iv);
yield();
}
}
}
void sendRetransmit(const queue_s *q, uint8_t i) {
if(q->attempts) {
q->iv->radio->sendCmdPacket(q->iv, TX_REQ_INFO, (SINGLE_FRAME + i), true);
q->iv->radioStatistics.retransmits++;
mWaitTime.startTimeMonitor(SINGLEFR_TIMEOUT); // timeout
mState = States::WAIT;
} else {
//add(q, true);
closeRequest(q, false);
}
}
private:
void closeRequest(const queue_s *q, bool crcPass) {
mHeu.evalTxChQuality(q->iv, crcPass, (4 - q->attempts), q->iv->curFrmCnt);
if(crcPass)
q->iv->radioStatistics.rxSuccess++;
else if(q->iv->mGotFragment)
q->iv->radioStatistics.rxFail++; // got no complete payload
else
q->iv->radioStatistics.rxFailNoAnser++; // got nothing
mWaitTime.startTimeMonitor(*mInverterGap);
bool keep = false;
if(q->isDevControl)
keep = !crcPass;
cmdDone(keep);
q->iv->mGotFragment = false;
q->iv->mGotLastMsg = false;
q->iv->miMultiParts = 0;
mIsRetransmit = false;
mFirstTry = false; // for correct reset
mState = States::RESET;
DBGPRINTLN(F("-----"));
}
inline void miHwDecode(packet_t *p, const queue_s *q) {
record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_All); // choose the record structure
rec->ts = q->ts;
/*
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_BOOTLOADER_VER, UNIT_NONE, CH0, 8, 2, 1 }
};
*/
if ( p->packet[9] == 0x00 ) {//first frame
//FLD_FW_VERSION
for (uint8_t i = 0; i < 5; i++) {
q->iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1);
}
q->iv->isConnected = true;
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("HW_VER is "));
DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25]));
}
record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec->ts = q->ts;
q->iv->setValue(1, rec, (uint32_t) ((p->packet[24] << 8) + p->packet[25])/1);
q->iv->miMultiParts +=4;
} else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10
DPRINT_IVID(DBG_INFO, q->iv->id);
if ( p->packet[9] == 0x01 ) {
DBGPRINTLN(F("got 2nd frame (hw info)"));
/* according to xlsx (different start byte -1!)
byte[11] to byte[14] HW_PN
byte[15] byte[16] HW_FB_TLmValue
byte[17] byte[18] HW_FB_ReSPRT
byte[19] byte[20] HW_GridSamp_ResValule
byte[21] byte[22] HW_ECapValue
byte[23] to byte[26] Matching_APPFW_PN*/
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]));
record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec->ts = q->ts;
q->iv->setValue(0, rec, (uint32_t) ((((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])/1);
if(*mSerialDebug) {
DPRINT(DBG_INFO,F("HW_FB_TLmValue "));
DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15]));
DBGPRINT(F("HW_FB_ReSPRT "));
DBGPRINTLN(String((p->packet[16] << 8) + p->packet[17]));
DBGPRINT(F("HW_GridSamp_ResValule "));
DBGPRINTLN(String((p->packet[18] << 8) + p->packet[19]));
DBGPRINT(F("HW_ECapValue "));
DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21]));
DBGPRINT(F("Matching_APPFW_PN "));
DBGPRINTLN(String((uint32_t) (((p->packet[22] << 8) | p->packet[23]) << 8 | p->packet[24]) << 8 | p->packet[25]));
}
if(NULL != mCbPayload)
(mCbPayload)(InverterDevInform_All, q->iv);
q->iv->miMultiParts +=2;
} else {
DBGPRINTLN(F("3rd gen. inverter!"));
}
} else if ( p->packet[9] == 0x12 ) {//3rd frame
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINTLN(F("got 3rd frame (hw info)"));
/* according to xlsx (different start byte -1!)
byte[11] byte[12] APPFW_MINVER
byte[13] byte[14] HWInfoAddr
byte[15] byte[16] PNInfoCRC_gusv
byte[15] byte[16] PNInfoCRC_gusv (this really is double mentionned in xlsx...)
*/
if(*mSerialDebug) {
DPRINT(DBG_INFO,F("APPFW_MINVER "));
DBGPRINTLN(String((p->packet[10] << 8) + p->packet[11]));
DBGPRINT(F("HWInfoAddr "));
DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13]));
DBGPRINT(F("PNInfoCRC_gusv "));
DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15]));
}
if(NULL != mCbPayload)
(mCbPayload)(InverterDevInform_Simple, q->iv);
q->iv->miMultiParts++;
}
//if(q->iv->miMultiParts > 5)
//closeRequest(q->iv, true);
//else
//if(q->iv->miMultiParts < 6)
// mState = States::WAIT;
/*if (mPayload[iv->id].multi_parts > 5) {
iv->setQueuedCmdFinished();
mPayload[iv->id].complete = true;
mPayload[iv->id].rxTmo = true;
mPayload[iv->id].requested= false;
iv->radioStatistics.rxSuccess++;
}
if (mHighPrioIv == NULL)
mHighPrioIv = iv;
*/
}
inline void miDataDecode(packet_t *p, const queue_s *q) {
record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser
rec->ts = q->ts;
//mState = States::RESET;
if(q->iv->miMultiParts < 6)
q->iv->miMultiParts += 6;
uint8_t datachan = ( p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES) || p->packet[0] == (MI_REQ_4CH + ALL_FRAMES) ) ? CH1 :
( p->packet[0] == (MI_REQ_CH2 + ALL_FRAMES) || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 :
p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 :
CH4;
// count in RF_communication_protocol.xlsx is with offset = -1
q->iv->setValue(q->iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[9] << 8) + p->packet[10])/10);
q->iv->setValue(q->iv->getPosByChFld(datachan, FLD_IDC, rec), rec, (float)((p->packet[11] << 8) + p->packet[12])/10);
q->iv->setValue(q->iv->getPosByChFld(0, FLD_UAC, rec), rec, (float)((p->packet[13] << 8) + p->packet[14])/10);
q->iv->setValue(q->iv->getPosByChFld(0, FLD_F, rec), rec, (float) ((p->packet[15] << 8) + p->packet[16])/100);
q->iv->setValue(q->iv->getPosByChFld(datachan, FLD_PDC, rec), rec, (float)((p->packet[17] << 8) + p->packet[18])/10);
q->iv->setValue(q->iv->getPosByChFld(datachan, FLD_YD, rec), rec, (float)((p->packet[19] << 8) + p->packet[20])/1);
q->iv->setValue(q->iv->getPosByChFld(0, FLD_T, rec), rec, (float) ((int16_t)(p->packet[21] << 8) + p->packet[22])/10);
q->iv->setValue(q->iv->getPosByChFld(0, FLD_IRR, rec), rec, (float) (calcIrradiation(q->iv, datachan)));
if (datachan == 1)
q->iv->rssi = p->rssi;
else if(q->iv->rssi > p->rssi)
q->iv->rssi = p->rssi;
if (p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES) ) {
/*For MI1500:
if (MI1500) {
STAT = (uint8_t)(p->packet[25] );
FCNT = (uint8_t)(p->packet[26]);
FCODE = (uint8_t)(p->packet[27]);
}*/
miStsConsolidate(q, datachan, rec, p->packet[23], p->packet[24]);
if (p->packet[0] < (0x39 + ALL_FRAMES) ) {
mHeu.evalTxChQuality(q->iv, true, (4 - q->attempts), 1);
miNextRequest((p->packet[0] - ALL_FRAMES + 1), q);
} else {
q->iv->miMultiParts = 7; // indicate we are ready
//miComplete(q->iv);
}
} else if((p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES)) && (q->iv->type == INV_TYPE_2CH)) {
//addImportant(q->iv, MI_REQ_CH2);
miNextRequest(MI_REQ_CH2, q);
mHeu.evalTxChQuality(q->iv, true, (4 - q->attempts), q->iv->curFrmCnt);
//use also miMultiParts here for better statistics?
//mHeu.setGotFragment(q->iv);
} else { // first data msg for 1ch, 2nd for 2ch
q->iv->miMultiParts += 6; // indicate we are ready
//miComplete(q->iv);
}
}
void miNextRequest(uint8_t cmd, const queue_s *q) {
incrAttempt(); // if function is called, we got something, and we necessarily need more transmissions for MI types...
if(*mSerialDebug) {
DPRINT_IVID(DBG_WARN, q->iv->id);
DBGPRINT(F("next request ("));
DBGPRINT(String(q->attempts));
DBGPRINT(F(" attempts left): 0x"));
DBGHEXLN(cmd);
}
if(q->iv->miMultiParts == 7) {
//mHeu.setGotAll(q->iv);
q->iv->radioStatistics.rxSuccess++;
} else
//mHeu.setGotFragment(q->iv);
/*iv->radioStatistics.rxFail++; // got no complete payload*/
//q->iv->radioStatistics.retransmits++;
q->iv->radio->sendCmdPacket(q->iv, cmd, 0x00, true);
mWaitTime.startTimeMonitor(MI_TIMEOUT);
q->iv->miMultiParts = 0;
q->iv->mGotFragment = 0;
mIsRetransmit = true;
chgCmd(cmd);
//mState = States::WAIT;
}
void miRepeatRequest(const queue_s *q) {
setAttempt(); // if function is called, we got something, and we necessarily need more transmissions for MI types...
if(*mSerialDebug) {
DPRINT_IVID(DBG_WARN, q->iv->id);
DBGPRINT(F("resend request ("));
DBGPRINT(String(q->attempts));
DBGPRINT(F(" attempts left): 0x"));
DBGHEXLN(q->cmd);
}
q->iv->radio->sendCmdPacket(q->iv, q->cmd, 0x00, true);
mWaitTime.startTimeMonitor(MI_TIMEOUT);
//mState = States::WAIT;
mIsRetransmit = false;
}
void miStsConsolidate(const queue_s *q, 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 statusMi = 3; // regular status for MI, change to 1 later?
if ( uState == 2 ) {
statusMi = 5050 + stschan; //first approach, needs review!
if (lState)
statusMi += lState*10;
} else if ( uState > 3 ) {
statusMi = uState*1000 + uEnum*10;
if (lState)
statusMi += lState*100; //needs review, esp. for 4ch-8310 state!
//if (lEnum)
statusMi += lEnum;
if (uEnum < 6) {
statusMi += stschan;
}
if (statusMi == 8000)
statusMi = 8310; //trick?
}
uint16_t prntsts = statusMi == 3 ? 1 : statusMi;
bool stsok = true;
if ( prntsts != rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)] ) { //sth.'s changed?
q->iv->alarmCnt = 1; // minimum...
stsok = false;
//sth is or was wrong?
if ( (q->iv->type != INV_TYPE_1CH) && ( (statusMi != 3)
|| ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1)))
) {
q->iv->lastAlarm[stschan+q->iv->type==INV_TYPE_2CH ? 2: 4] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts);
q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0);
q->iv->alarmCnt = q->iv->type == INV_TYPE_2CH ? 3 : 5;
} else if ( (q->iv->type == INV_TYPE_1CH) && ( (statusMi != 3)
|| ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1)))
) {
q->iv->lastAlarm[stschan] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts);
} else if (q->iv->type == INV_TYPE_1CH)
stsok = true;
q->iv->alarmLastId = prntsts; //iv->alarmMesIndex;
if (q->iv->alarmCnt > 1) { //more than one channel
for (uint8_t ch = 0; ch < (q->iv->alarmCnt); ++ch) { //start with 1
if (q->iv->lastAlarm[ch].code == 1) {
stsok = true;
break;
}
}
}
if(*mSerialDebug) {
DPRINT(DBG_WARN, F("New state on CH"));
DBGPRINT(String(stschan)); DBGPRINT(F(" ("));
DBGPRINT(String(prntsts)); DBGPRINT(F("): "));
DBGPRINTLN(q->iv->getAlarmStr(prntsts));
}
if(!q->iv->miMultiParts)
q->iv->miMultiParts = 1; // indicate we got status info (1+2 ch types)
}
if (!stsok) {
q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts);
q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0);
}
if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) {
q->iv->alarmMesIndex = rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!?
if (*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("alarm ID incremented to "));
DBGPRINTLN(String(q->iv->alarmMesIndex));
}
}
}
void miComplete(Inverter<> *iv) {
if (*mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("got all data 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;
if (iv->type == INV_TYPE_1CH) {
if ((!iv->lastAlarm[0].code) || (iv->lastAlarm[0].code == 1))
ac_pow += iv->getValue(iv->getPosByChFld(1, FLD_PDC, rec), rec);
} else {
for(uint8_t i = 1; i <= iv->channels; i++) {
if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1)) {
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();
// update status state-machine,
if (ac_pow)
iv->isProducing();
//closeRequest(iv, iv->miMultiParts > 5);
//mHeu.setGotAll(iv);
//cmdDone(false);
if(NULL != mCbPayload)
(mCbPayload)(RealTimeRunData_Debug, iv);
//mState = States::RESET; // everything ok, next request
}
private:
enum class States : uint8_t {
RESET, START, WAIT, CHECK_FRAMES, CHECK_PACKAGE
};
typedef struct {
uint8_t buf[MAX_RF_PAYLOAD_SIZE];
uint8_t len;
int8_t rssi;
} frame_t;
private:
States mState = States::RESET;
uint32_t *mTimestamp;
bool *mPrivacyMode, *mSerialDebug, *mPrintWholeTrace;
uint16_t *mInverterGap;
TimeMonitor mWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state)
std::array<frame_t, MAX_PAYLOAD_ENTRIES> mLocalBuf;
bool mFirstTry = false; // see, if we should do a second try
bool mIsRetransmit = false; // we already had waited one complete cycle
uint8_t mMaxFrameId;
uint8_t mPayload[MAX_BUFFER];
payloadListenerType mCbPayload = NULL;
alarmListenerType mCbAlarm = NULL;
Heuristic mHeu;
uint32_t mLastEmptyQueueMillis = 0;
bool mPrintSequenceDuration = false;
//States mDebugState = States::START;
};
#endif /*__COMMUNICATION_H__*/

187
src/hm/Heuristic.h

@ -0,0 +1,187 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HEURISTIC_H__
#define __HEURISTIC_H__
#include "../utils/dbg.h"
#include "hmInverter.h"
#include "HeuristicInv.h"
#define RF_TEST_PERIOD_MAX_SEND_CNT 50
#define RF_TEST_PERIOD_MAX_FAIL_CNT 5
#define RF_TX_TEST_CHAN_1ST_USE 0xff
#define RF_TX_CHAN_QUALITY_GOOD 2
#define RF_TX_CHAN_QUALITY_OK 1
#define RF_TX_CHAN_QUALITY_LOW -1
#define RF_TX_CHAN_QUALITY_BAD -2
class Heuristic {
public:
uint8_t getTxCh(Inverter<> *iv) {
if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
return 0; // not used for these inverter types
HeuristicInv *ih = &iv->heuristics;
// start with the next index: round robbin in case of same 'best' quality
uint8_t curId = (ih->txRfChId + 1) % RF_MAX_CHANNEL_ID;
ih->lastBestTxChId = ih->txRfChId;
ih->txRfChId = curId;
curId = (curId + 1) % RF_MAX_CHANNEL_ID;
for(uint8_t i = 1; i < RF_MAX_CHANNEL_ID; i++) {
if(ih->txRfQuality[curId] > ih->txRfQuality[ih->txRfChId])
ih->txRfChId = curId;
curId = (curId + 1) % RF_MAX_CHANNEL_ID;
}
if(ih->testPeriodSendCnt < 0xff)
ih->testPeriodSendCnt++;
if((ih->txRfChId == ih->lastBestTxChId) && (ih->testPeriodSendCnt >= RF_TEST_PERIOD_MAX_SEND_CNT)) {
if(ih->testPeriodFailCnt > RF_TEST_PERIOD_MAX_FAIL_CNT) {
// try round robbin another chan and see if it works even better
ih->testChId = (ih->testChId + 1) % RF_MAX_CHANNEL_ID;
if(ih->testChId == ih->txRfChId)
ih->testChId = (ih->testChId + 1) % RF_MAX_CHANNEL_ID;
// give it a fair chance but remember old status in case of immediate fail
ih->saveOldTestQuality = ih->txRfQuality[ih->testChId];
ih->txRfQuality[ih->testChId] = ih->txRfQuality[ih->txRfChId];
ih->txRfChId = ih->testChId;
ih->testChId = RF_TX_TEST_CHAN_1ST_USE; // mark the chan as a test and as 1st use during new test period
DPRINTLN(DBG_INFO, F("Test CH ") + String(id2Ch(ih->txRfChId)));
}
// start new test period
ih->testPeriodSendCnt = 0;
ih->testPeriodFailCnt = 0;
} else if(ih->txRfChId != ih->lastBestTxChId) {
// start new test period
ih->testPeriodSendCnt = 0;
ih->testPeriodFailCnt = 0;
}
return id2Ch(ih->txRfChId);
}
void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments) {
HeuristicInv *ih = &iv->heuristics;
#if (DBG_DEBUG == DEBUG_LEVEL)
DPRINT(DBG_DEBUG, "eval ");
DBGPRINT(String(crcPass));
DBGPRINT(", ");
DBGPRINT(String(retransmits));
DBGPRINT(", ");
DBGPRINT(String(rxFragments));
DBGPRINT(", ");
DBGPRINTLN(String(ih->lastRxFragments));
#endif
if(ih->lastRxFragments == rxFragments) {
if(crcPass)
updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD);
else if(!retransmits || isNewTxCh(ih)) { // nothing received: send probably lost
if(RF_TX_TEST_CHAN_1ST_USE == ih->testChId) {
// switch back to original quality
DPRINTLN(DBG_INFO, F("Test failed (-2)"));
ih->txRfQuality[ih->txRfChId] = ih->saveOldTestQuality;
}
updateQuality(ih, RF_TX_CHAN_QUALITY_BAD);
if(ih->testPeriodFailCnt < 0xff)
ih->testPeriodFailCnt++;
}
} else if(!ih->lastRxFragments && crcPass) {
if(!retransmits || isNewTxCh(ih)) {
// every fragment received successfull immediately
updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD);
} else {
// every fragment received successfully
updateQuality(ih, RF_TX_CHAN_QUALITY_OK);
}
} else if(crcPass) {
if(isNewTxCh(ih)) {
// last Fragment successfully received on new send channel
updateQuality(ih, RF_TX_CHAN_QUALITY_OK);
}
} else if(!retransmits || isNewTxCh(ih)) {
// no complete receive for this send channel
if((rxFragments - ih->lastRxFragments) > 2) {
// graceful evaluation for big inverters that have to send 4 answer packets
updateQuality(ih, RF_TX_CHAN_QUALITY_OK);
} else if((rxFragments - ih->lastRxFragments) < 2) {
if(RF_TX_TEST_CHAN_1ST_USE == ih->testChId) {
// switch back to original quality
DPRINTLN(DBG_INFO, F("Test failed (-1)"));
ih->txRfQuality[ih->txRfChId] = ih->saveOldTestQuality;
}
updateQuality(ih, RF_TX_CHAN_QUALITY_LOW);
if(ih->testPeriodFailCnt < 0xff)
ih->testPeriodFailCnt++;
} // else: _QUALITY_NEUTRAL, keep any test channel
} // else: dont overestimate burst distortion
ih->testChId = ih->txRfChId; // reset to best
ih->lastRxFragments = rxFragments;
}
void printStatus(Inverter<> *iv) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("Radio infos:"));
if((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) {
for(uint8_t i = 0; i < RF_MAX_CHANNEL_ID; i++) {
DBGPRINT(F(" "));
DBGPRINT(String(iv->heuristics.txRfQuality[i]));
}
DBGPRINT(F(" |"));
}
DBGPRINT(F(" t: "));
DBGPRINT(String(iv->radioStatistics.txCnt));
DBGPRINT(F(", s: "));
DBGPRINT(String(iv->radioStatistics.rxSuccess));
DBGPRINT(F(", f: "));
DBGPRINT(String(iv->radioStatistics.rxFail));
DBGPRINT(F(", n: "));
DBGPRINT(String(iv->radioStatistics.rxFailNoAnser));
DBGPRINT(F(" | p: ")); // better debugging for helpers...
if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
DBGPRINTLN(String(iv->config->powerLevel-10));
else
DBGPRINTLN(String(iv->config->powerLevel));
}
private:
bool isNewTxCh(HeuristicInv *ih) {
return ih->txRfChId != ih->lastBestTxChId;
}
void updateQuality(HeuristicInv *ih, uint8_t quality) {
ih->txRfQuality[ih->txRfChId] += quality;
if(ih->txRfQuality[ih->txRfChId] > RF_MAX_QUALITY)
ih->txRfQuality[ih->txRfChId] = RF_MAX_QUALITY;
else if(ih->txRfQuality[ih->txRfChId] < RF_MIN_QUALTIY)
ih->txRfQuality[ih->txRfChId] = RF_MIN_QUALTIY;
}
inline uint8_t id2Ch(uint8_t id) {
switch(id) {
case 0: return 3;
case 1: return 23;
case 2: return 40;
case 3: return 61;
case 4: return 75;
}
return 3; // standard
}
private:
uint8_t mChList[5] = {03, 23, 40, 61, 75};
};
#endif /*__HEURISTIC_H__*/

32
src/hm/HeuristicInv.h

@ -0,0 +1,32 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HEURISTIC_INV_H__
#define __HEURISTIC_INV_H__
#define RF_MAX_CHANNEL_ID 5
#define RF_MAX_QUALITY 4
#define RF_MIN_QUALTIY -6
#define RF_NA -99
class HeuristicInv {
public:
HeuristicInv() {
memset(txRfQuality, -6, RF_MAX_CHANNEL_ID);
}
public:
int8_t txRfQuality[RF_MAX_CHANNEL_ID]; // heuristics tx quality (check 'Heuristics.h')
uint8_t txRfChId = 0; // RF TX channel id
uint8_t lastBestTxChId = 0;
uint8_t testPeriodSendCnt = 0;
uint8_t testPeriodFailCnt = 0;
uint8_t testChId = 0;
int8_t saveOldTestQuality = -6;
uint8_t lastRxFragments = 0;
};
#endif /*__HEURISTIC_INV_H__*/

124
src/hm/hmDefines.h

@ -10,7 +10,8 @@
#include <cstdint> #include <cstdint>
// inverter generations // inverter generations
enum {IV_HM = 0, IV_MI, IV_HMS, IV_HMT}; enum {IV_MI = 0, IV_HM, IV_HMS, IV_HMT, IV_UNKNOWN};
const char* const generationNames[] = {"MI", "HM", "HMS", "HMT", "UNKNOWN"};
// units // units
enum {UNIT_V = 0, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_HZ, UNIT_C, UNIT_PCT, UNIT_VAR, UNIT_NONE}; enum {UNIT_V = 0, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_HZ, UNIT_C, UNIT_PCT, UNIT_VAR, UNIT_NONE};
@ -21,20 +22,22 @@ enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT,
FLD_UAC, FLD_UAC_1N, FLD_UAC_2N, FLD_UAC_3N, FLD_UAC_12, FLD_UAC_23, FLD_UAC_31, FLD_IAC, FLD_UAC, FLD_UAC_1N, FLD_UAC_2N, FLD_UAC_3N, FLD_UAC_12, FLD_UAC_23, FLD_UAC_31, FLD_IAC,
FLD_IAC_1, FLD_IAC_2, FLD_IAC_3, FLD_PAC, FLD_F, FLD_T, FLD_PF, FLD_EFF, FLD_IAC_1, FLD_IAC_2, FLD_IAC_3, FLD_PAC, FLD_F, FLD_T, FLD_PF, FLD_EFF,
FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR, FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR,
FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_HW_ID, FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_BOOTLOADER_VER,
FLD_ACT_ACTIVE_PWR_LIMIT, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP}; FLD_ACT_ACTIVE_PWR_LIMIT, FLD_PART_NUM, FLD_HW_VERSION, FLD_GRID_PROFILE_CODE,
FLD_GRID_PROFILE_VERSION, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP};
const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal",
"U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "UAC_12", "UAC_23", "UAC_31", "I_AC", "U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "UAC_12", "UAC_23", "UAC_31", "I_AC",
"IAC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC", "IAC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC",
"ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","HWPartId", "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","BootloaderVersion",
"active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower"}; "active_PowerLimit", "HWPartNumber", "HWVersion", "GridProfileCode",
"GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower"};
const char* const notAvail = "n/a"; const char* const notAvail = "n/a";
const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH, const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH,
UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_A, UNIT_A, UNIT_A, UNIT_A, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_A, UNIT_A, UNIT_A, UNIT_A,
UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR, 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, UNIT_W}; UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_W};
// mqtt discovery device classes // 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}; enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP};
@ -74,6 +77,15 @@ enum {CH0 = 0, CH1, CH2, CH3, CH4, CH5, CH6};
enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH}; enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH};
#define WORK_FREQ_KHZ 865000 // desired work frequency between DTU and
// inverter in kHz
#define HOY_BASE_FREQ_KHZ 860000 // in kHz
#define HOY_MAX_FREQ_KHZ 923500 // 0xFE * 250kHz + Base_freq
#define HOY_BOOT_FREQ_KHZ 868000 // Hoymiles boot/init frequency after power up inverter
#define FREQ_STEP_KHZ 250 // channel step size in kHz
#define FREQ_WARN_MIN_KHZ 863000 // for EU 863 - 870 MHz is allowed
#define FREQ_WARN_MAX_KHZ 870000 // for EU 863 - 870 MHz is allowed
typedef struct { typedef struct {
uint8_t fieldId; // field id uint8_t fieldId; // field id
@ -98,11 +110,20 @@ const byteAssign_t InfoAssignment[] = {
{ FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 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_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 },
{ FLD_FW_BUILD_HOUR_MINUTE, UNIT_NONE, CH0, 6, 2, 1 }, { FLD_FW_BUILD_HOUR_MINUTE, UNIT_NONE, CH0, 6, 2, 1 },
{ FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } { FLD_BOOTLOADER_VER, UNIT_NONE, CH0, 8, 2, 1 }
}; };
#define HMINFO_LIST_LEN (sizeof(InfoAssignment) / sizeof(byteAssign_t)) #define HMINFO_LIST_LEN (sizeof(InfoAssignment) / sizeof(byteAssign_t))
#define HMINFO_PAYLOAD_LEN 14 #define HMINFO_PAYLOAD_LEN 14
const byteAssign_t SimpleInfoAssignment[] = {
{ FLD_PART_NUM, UNIT_NONE, CH0, 2, 4, 1 },
{ FLD_HW_VERSION, UNIT_NONE, CH0, 6, 2, 1 },
{ FLD_GRID_PROFILE_CODE, UNIT_NONE, CH0, 8, 2, 1 },
{ FLD_GRID_PROFILE_VERSION, UNIT_NONE, CH0, 10, 2, 1 }
};
#define HMSIMPLE_INFO_LIST_LEN (sizeof(SimpleInfoAssignment) / sizeof(byteAssign_t))
#define HMSIMPLE_INFO_PAYLOAD_LEN 14
const byteAssign_t SystemConfigParaAssignment[] = { const byteAssign_t SystemConfigParaAssignment[] = {
{ FLD_ACT_ACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 2, 2, 10 }/*, { FLD_ACT_ACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 2, 2, 10 }/*,
{ FLD_ACT_REACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 4, 2, 10 }, { FLD_ACT_REACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 4, 2, 10 },
@ -118,6 +139,8 @@ const byteAssign_t AlarmDataAssignment[] = {
#define HMALARMDATA_PAYLOAD_LEN 0 // 0: means check is off #define HMALARMDATA_PAYLOAD_LEN 0 // 0: means check is off
#define ALARM_LOG_ENTRY_SIZE 12 #define ALARM_LOG_ENTRY_SIZE 12
#define HMGETLOSSRATE_PAYLOAD_LEN 4
#define AHOY_GET_LOSS_INTERVAL 10
//------------------------------------- //-------------------------------------
// HM300, HM350, HM400 // HM300, HM350, HM400
@ -242,4 +265,91 @@ const byteAssign_t hm4chAssignment[] = {
#define HM4CH_PAYLOAD_LEN 62 #define HM4CH_PAYLOAD_LEN 62
typedef struct {
uint32_t hwPart;
uint16_t maxPower;
} devInfo_t;
// https://github.com/lumapu/ahoy/issues/1111
// Hardware number:
// 0xAABBCCDD
// ^^ ------- always 10 (for MI, HM, HMS, HMT)
// ^ ------ 0 = MI
// 1 = HM
// 1, 2 = HMS (version)
// 3 = HMT
// ^ ----- 0 = 1 Input
// 1 = 2 Inputs
// 2 = 4 Inputs
// 3 = 6 Inputs
// ^ ---- 0 = smallest with x inputs
// 7 = biggest with x inputs
const devInfo_t devInfo[] = {
// MI 2nd gen; only 0x001311 is tested,
// others (starting with MI-250) according to https://github.com/lumapu/ahoy/issues/1111#issuecomment-1698100571
{ 0x000111, 250 },
{ 0x000311, 300 },
{ 0x000411, 350 },
{ 0x001111, 500 },
{ 0x001311, 600 },
{ 0x001321, 600 },
{ 0x001421, 700 },
{ 0x001411, 700 },
{ 0x002111, 1000 },
{ 0x002311, 1200 },
{ 0x002511, 1500 },
{ 0x002411, 1500 },
// MI 3rd gen
{ 0x100000, 250 },
{ 0x100010, 300 },
{ 0x100020, 350 },
{ 0x100030, 400 },
{ 0x100100, 500 },
{ 0x100110, 600 },
{ 0x100120, 700 },
{ 0x100130, 800 },
{ 0x100200, 1000 },
{ 0x100210, 1200 },
{ 0x100230, 1500 },
// HM
{ 0x101010, 300 },
{ 0x101020, 350 },
{ 0x101030, 400 },
{ 0x101040, 400 },
{ 0x101110, 600 }, // [TSOL800(DE) ..20, HWv=2.66], [HM-600 ..20, HWv=2.66]
{ 0x101120, 700 },
{ 0x101130, 800 },
{ 0x101140, 800 },
{ 0x101200, 1000 },
{ 0x101210, 1200 },
{ 0x101230, 1500 },
// HMS
{ 0x102021, 350 },
{ 0x102041, 400 },
{ 0x101051, 450 },
{ 0x101071, 500 },
{ 0x102111, 600 },
{ 0x101120, 700 },
{ 0x102141, 800 },
{ 0x101151, 900 },
{ 0x102171, 1000 },
{ 0x102241, 1600 },
{ 0x101251, 1800 },
{ 0x102251, 1800 },
{ 0x101271, 2000 }, // v1 grey backplane, 14A
{ 0x102271, 2000 }, // v2 black backplane, 16A
// HMT
{ 0x103311, 1800 },
{ 0x103331, 2250 }
};
#define MI_REQ_CH1 0x09
#define MI_REQ_CH2 0x11
#define MI_REQ_4CH 0x36
#endif /*__HM_DEFINES_H__*/ #endif /*__HM_DEFINES_H__*/

439
src/hm/hmInverter.h

@ -11,12 +11,17 @@
#define F(sl) (sl) #define F(sl) (sl)
#endif #endif
#define MAX_GRID_LENGTH 150
#include "hmDefines.h" #include "hmDefines.h"
#include "HeuristicInv.h"
#include "../hms/hmsDefines.h" #include "../hms/hmsDefines.h"
#include <memory> #include <memory>
#include <queue> #include <queue>
#include <functional>
#include "../config/settings.h" #include "../config/settings.h"
#include "radio.h"
/** /**
* For values which are of interest and not transmitted by the inverter can be * For values which are of interest and not transmitted by the inverter can be
* calculated automatically. * calculated automatically.
@ -24,10 +29,6 @@
* automatically. Their result does not differ from original read values. * automatically. Their result does not differ from original read values.
*/ */
// forward declaration of class
template <class REC_TYP=float>
class Inverter;
// prototypes // prototypes
template<class T=float> template<class T=float>
@ -65,7 +66,7 @@ struct calcFunc_t {
template<class T=float> template<class T=float>
struct record_t { struct record_t {
byteAssign_t* assign; // assigment of bytes in payload byteAssign_t* assign; // assignment of bytes in payload
uint8_t length; // length of the assignment list uint8_t length; // length of the assignment list
T *record; // data pointer T *record; // data pointer
uint32_t ts; // timestamp of last received payload uint32_t ts; // timestamp of last received payload
@ -80,31 +81,6 @@ struct alarm_t {
alarm_t() : code(0), start(0), end(0) {} alarm_t() : code(0), start(0), end(0) {}
}; };
class CommandAbstract {
public:
CommandAbstract(uint8_t txType = 0, uint8_t cmd = 0) {
_TxType = txType;
_Cmd = cmd;
};
virtual ~CommandAbstract() {};
const uint8_t getCmd() {
return _Cmd;
}
protected:
uint8_t _TxType;
uint8_t _Cmd;
};
class InfoCommand : public CommandAbstract {
public:
InfoCommand(uint8_t cmd){
_TxType = 0x15;
_Cmd = cmd;
}
};
// list of all available functions, mapped in hmDefines.h // list of all available functions, mapped in hmDefines.h
template<class T=float> template<class T=float>
const calcFunc_t<T> calcFunctions[] = { const calcFunc_t<T> calcFunctions[] = {
@ -142,16 +118,34 @@ class Inverter {
uint8_t channels; // number of PV channels (1-4) uint8_t channels; // number of PV channels (1-4)
record_t<REC_TYP> recordMeas; // structure for measured values record_t<REC_TYP> recordMeas; // structure for measured values
record_t<REC_TYP> recordInfo; // structure for info values record_t<REC_TYP> recordInfo; // structure for info values
record_t<REC_TYP> recordHwInfo; // structure for simple (hardware) info values
record_t<REC_TYP> recordConfig; // structure for system config values record_t<REC_TYP> recordConfig; // structure for system config values
record_t<REC_TYP> recordAlarm; // structure for alarm values record_t<REC_TYP> 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) bool isConnected; // shows if inverter was successfully identified (fw version and hardware info)
InverterStatus status; // indicates the current inverter status InverterStatus status; // indicates the current inverter status
std::array<alarm_t, 10> lastAlarm; // holds last 10 alarms std::array<alarm_t, 10> lastAlarm; // holds last 10 alarms
uint8_t alarmNxtWrPos; // indicates the position in array (rolling buffer) uint8_t alarmNxtWrPos; // indicates the position in array (rolling buffer)
uint16_t alarmCnt; // counts the total number of occured alarms uint16_t alarmCnt; // counts the total number of occurred alarms
uint16_t alarmLastId; // lastId which was received
int8_t rssi; // RSSI
uint8_t miMultiParts; // helper info for MI multiframe msgs
uint8_t outstandingFrames; // helper info to count difference between expected and received frames
bool mGotFragment; // shows if inverter has sent at least one fragment
uint8_t curFrmCnt; // count received frames in current loop
bool mGotLastMsg; // shows if inverter has already finished transmission cycle
uint8_t mCmd; // holds the command to send
bool mIsSingleframeReq; // indicates this is a missing single frame request
Radio *radio; // pointer to associated radio class
statistics_t radioStatistics; // information about transmitted, failed, ... packets
HeuristicInv heuristics; // heuristic information / logic
uint8_t curCmtFreq; // current used CMT frequency, used to check if freq. was changed during runtime
bool commEnabled; // 'pause night communication' sets this field to false
uint16_t mIvRxCnt; // last iv rx frames (from GetLossRate)
uint16_t mIvTxCnt; // last iv tx frames (from GetLossRate)
uint16_t mDtuRxCnt; // cur dtu rx frames (since last GetLossRate)
uint16_t mDtuTxCnt; // cur dtu tx frames (since last getLoassRate)
uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debu
static uint32_t *timestamp; // system timestamp static uint32_t *timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup static cfgInst_t *generalConfig; // general inverter configuration from setup
@ -164,73 +158,78 @@ class Inverter {
actPowerLimit = 0xffff; // init feedback from inverter to -1 actPowerLimit = 0xffff; // init feedback from inverter to -1
mDevControlRequest = false; mDevControlRequest = false;
devControlCmd = InitDataState; devControlCmd = InitDataState;
initialized = false;
//lastAlarmMsg = "nothing";
alarmMesIndex = 0; alarmMesIndex = 0;
isConnected = false; isConnected = false;
status = InverterStatus::OFF; status = InverterStatus::OFF;
alarmNxtWrPos = 0; alarmNxtWrPos = 0;
alarmCnt = 0; alarmCnt = 0;
} alarmLastId = 0;
rssi = -127;
~Inverter() { miMultiParts = 0;
// TODO: cleanup mGotLastMsg = false;
} mCmd = InitDataState;
mIsSingleframeReq = false;
template <typename T> radio = NULL;
void enqueCommand(uint8_t cmd) { commEnabled = true;
_commandQueue.push(std::make_shared<T>(cmd)); mIvRxCnt = 0;
DPRINT_IVID(DBG_INFO, id); mIvTxCnt = 0;
DBGPRINT(F("enqueCommand: 0x")); mDtuRxCnt = 0;
DBGHEXLN(cmd); mDtuTxCnt = 0;
}
memset(&radioStatistics, 0, sizeof(statistics_t));
void setQueuedCmdFinished() { memset(heuristics.txRfQuality, -6, 5);
if (!_commandQueue.empty()) {
// Will destroy CommandAbstract Class Object (?) memset(mOffYD, 0, sizeof(float) * 6);
_commandQueue.pop(); memset(mLastYD, 0, sizeof(float) * 6);
} }
}
void tickSend(std::function<void(uint8_t cmd, bool isDevControl)> cb) {
void clearCmdQueue() { if(mDevControlRequest) {
DPRINTLN(DBG_INFO, F("clearCmdQueue")); cb(devControlCmd, true);
while (!_commandQueue.empty()) { mDevControlRequest = false;
// Will destroy CommandAbstract Class Object (?) } else if (IV_MI != ivGen) {
_commandQueue.pop(); mGetLossInterval++;
} if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0))
} cb(AlarmData, false); // get last alarms
else if(0 == getFwVersion())
uint8_t getQueuedCmd() { cb(InverterDevInform_All, false); // get firmware version
if (_commandQueue.empty()) { else if(0 == getHwVersion())
if (ivGen != IV_MI) { cb(InverterDevInform_Simple, false); // get hardware version
if (getFwVersion() == 0) else if(actPowerLimit == 0xffff)
enqueCommand<InfoCommand>(InverterDevInform_All); // firmware version cb(SystemConfigPara, false); // power limit info
enqueCommand<InfoCommand>(RealTimeRunData_Debug); // live data else if(InitDataState != devControlCmd) {
} else if (ivGen == IV_MI){ cb(devControlCmd, false); // custom command which was received by API
if (getFwVersion() == 0) devControlCmd = InitDataState;
enqueCommand<InfoCommand>(InverterDevInform_All); // firmware version; might not work, esp. for 1/2 ch hardware mGetLossInterval = 1;
if (type == INV_TYPE_4CH) { } else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile
enqueCommand<InfoCommand>(0x36); cb(GridOnProFilePara, false);
} else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate
mGetLossInterval = 1;
cb(GetLossRate, false);
} else
cb(RealTimeRunData_Debug, false); // get live data
} else { } else {
enqueCommand<InfoCommand>(0x09); if(0 == getFwVersion())
} cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number
else {
record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0)
cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
else
cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false);
} }
if ((actPowerLimit == 0xffff) && isConnected)
enqueCommand<InfoCommand>(SystemConfigPara); // power limit info
} }
return _commandQueue.front().get()->getCmd();
} }
void init(void) { void init(void) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:init")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:init"));
initAssignment(&recordMeas, RealTimeRunData_Debug); initAssignment(&recordMeas, RealTimeRunData_Debug);
initAssignment(&recordInfo, InverterDevInform_All); initAssignment(&recordInfo, InverterDevInform_All);
initAssignment(&recordHwInfo, InverterDevInform_Simple);
initAssignment(&recordConfig, SystemConfigPara); initAssignment(&recordConfig, SystemConfigPara);
initAssignment(&recordAlarm, AlarmData); initAssignment(&recordAlarm, AlarmData);
toRadioId(); toRadioId();
initialized = true; curCmtFreq = this->config->frequency; // update to frequency read from settings
} }
uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) { uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
@ -280,12 +279,10 @@ class Inverter {
return isConnected; return isConnected;
} }
void clearDevControlRequest() { bool setDevCommand(uint8_t cmd) {
mDevControlRequest = false; if(isConnected)
} devControlCmd = cmd;
return isConnected;
inline bool getDevControlRequest() {
return mDevControlRequest;
} }
void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) { void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) {
@ -308,7 +305,12 @@ class Inverter {
} else if (FLD_YT == rec->assign[pos].fieldId) { } else if (FLD_YT == rec->assign[pos].fieldId) {
rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]);
} else if (FLD_YD == rec->assign[pos].fieldId) { } else if (FLD_YD == rec->assign[pos].fieldId) {
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency; float actYD = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency;
uint8_t idx = rec->assign[pos].ch - 1;
if (mLastYD[idx] > actYD)
mOffYD[idx] += mLastYD[idx];
mLastYD[idx] = actYD;
rec->record[pos] = mOffYD[idx] + actYD;
} else { } else {
if ((REC_TYP)(div) > 1) if ((REC_TYP)(div) > 1)
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
@ -325,11 +327,9 @@ class Inverter {
if (getPosByChFld(0, FLD_EVT, rec) == pos) { if (getPosByChFld(0, FLD_EVT, rec) == pos) {
if (alarmMesIndex < rec->record[pos]) { if (alarmMesIndex < rec->record[pos]) {
alarmMesIndex = rec->record[pos]; alarmMesIndex = rec->record[pos];
//enqueCommand<InfoCommand>(AlarmUpdate); // What is the function of AlarmUpdate?
DPRINT(DBG_INFO, "alarm ID incremented to "); DPRINT(DBG_INFO, "alarm ID incremented to ");
DBGPRINTLN(String(alarmMesIndex)); DBGPRINTLN(String(alarmMesIndex));
enqueCommand<InfoCommand>(AlarmData);
} }
} }
} }
@ -338,6 +338,10 @@ class Inverter {
// eg. fw version ... // eg. fw version ...
isConnected = true; isConnected = true;
} }
else if (rec->assign == SimpleInfoAssignment) {
DPRINTLN(DBG_DEBUG, "add simple info");
// eg. hw version ...
}
else if (rec->assign == SystemConfigParaAssignment) { else if (rec->assign == SystemConfigParaAssignment) {
DPRINTLN(DBG_DEBUG, "add config"); DPRINTLN(DBG_DEBUG, "add config");
if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){
@ -348,12 +352,9 @@ class Inverter {
} }
else if (rec->assign == AlarmDataAssignment) { else if (rec->assign == AlarmDataAssignment) {
DPRINTLN(DBG_DEBUG, "add alarm"); DPRINTLN(DBG_DEBUG, "add alarm");
//if (getPosByChFld(0, FLD_LAST_ALARM_CODE, rec) == pos){
// lastAlarmMsg = getAlarmStr(rec->record[pos]);
//}
} }
else else
DPRINTLN(DBG_WARN, F("add with unknown assginment")); DPRINTLN(DBG_WARN, F("add with unknown assignment"));
} }
else else
DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x")); DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));
@ -388,6 +389,10 @@ class Inverter {
return 0; return 0;
} }
uint32_t getChannelFieldValueInt(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
return (uint32_t) getChannelFieldValue(channel, fieldId, rec);
}
REC_TYP getValue(uint8_t pos, record_t<> *rec) { REC_TYP getValue(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue"));
if(NULL == rec) if(NULL == rec)
@ -410,6 +415,10 @@ class Inverter {
bool isAvailable() { bool isAvailable() {
bool avail = false; bool avail = false;
if((recordMeas.ts == 0) && (recordInfo.ts == 0) && (recordConfig.ts == 0) && (recordAlarm.ts == 0))
return false;
if((*timestamp - recordMeas.ts) < INVERTER_INACT_THRES_SEC) if((*timestamp - recordMeas.ts) < INVERTER_INACT_THRES_SEC)
avail = true; avail = true;
if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC) if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC)
@ -426,6 +435,7 @@ class Inverter {
if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) { if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) {
status = InverterStatus::OFF; status = InverterStatus::OFF;
actPowerLimit = 0xffff; // power limit will be read once inverter becomes available actPowerLimit = 0xffff; // power limit will be read once inverter becomes available
alarmMesIndex = 0;
} }
else else
status = InverterStatus::WAS_ON; status = InverterStatus::WAS_ON;
@ -448,10 +458,31 @@ class Inverter {
return producing; return producing;
} }
InverterStatus getStatus(){
isProducing(); // recalculate status
return status;
}
uint16_t getFwVersion() { uint16_t getFwVersion() {
record_t<> *rec = getRecordStruct(InverterDevInform_All); record_t<> *rec = getRecordStruct(InverterDevInform_All);
uint8_t pos = getPosByChFld(CH0, FLD_FW_VERSION, rec); return getChannelFieldValue(CH0, FLD_FW_VERSION, rec);
return getValue(pos, rec); }
uint16_t getHwVersion() {
record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
return getChannelFieldValue(CH0, FLD_HW_VERSION, rec);
}
uint16_t getMaxPower() {
record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
if(0 == getChannelFieldValue(CH0, FLD_HW_VERSION, rec))
return 0;
for(uint8_t i = 0; i < sizeof(devInfo) / sizeof(devInfo_t); i++) {
if(devInfo[i].hwPart == (getChannelFieldValueInt(CH0, FLD_PART_NUM, rec) >> 8))
return devInfo[i].maxPower;
}
return 0;
} }
uint32_t getLastTs(record_t<> *rec) { uint32_t getLastTs(record_t<> *rec) {
@ -462,6 +493,7 @@ class Inverter {
record_t<> *getRecordStruct(uint8_t cmd) { record_t<> *getRecordStruct(uint8_t cmd) {
switch (cmd) { switch (cmd) {
case RealTimeRunData_Debug: return &recordMeas; // 11 = 0x0b case RealTimeRunData_Debug: return &recordMeas; // 11 = 0x0b
case InverterDevInform_Simple: return &recordHwInfo; // 0 = 0x00
case InverterDevInform_All: return &recordInfo; // 1 = 0x01 case InverterDevInform_All: return &recordInfo; // 1 = 0x01
case SystemConfigPara: return &recordConfig; // 5 = 0x05 case SystemConfigPara: return &recordConfig; // 5 = 0x05
case AlarmData: return &recordAlarm; // 17 = 0x11 case AlarmData: return &recordAlarm; // 17 = 0x11
@ -485,10 +517,7 @@ class Inverter {
rec->length = (uint8_t)(HMS1CH_LIST_LEN); rec->length = (uint8_t)(HMS1CH_LIST_LEN);
rec->assign = (byteAssign_t *)hms1chAssignment; rec->assign = (byteAssign_t *)hms1chAssignment;
rec->pyldLen = HMS1CH_PAYLOAD_LEN; rec->pyldLen = HMS1CH_PAYLOAD_LEN;
} /*else if(IV_MI == ivGen) { }
rec->length = (uint8_t)(HM1CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm1chAssignment;
}*/
channels = 1; channels = 1;
} }
else if (INV_TYPE_2CH == type) { else if (INV_TYPE_2CH == type) {
@ -533,6 +562,11 @@ class Inverter {
rec->assign = (byteAssign_t *)InfoAssignment; rec->assign = (byteAssign_t *)InfoAssignment;
rec->pyldLen = HMINFO_PAYLOAD_LEN; rec->pyldLen = HMINFO_PAYLOAD_LEN;
break; break;
case InverterDevInform_Simple:
rec->length = (uint8_t)(HMSIMPLE_INFO_LIST_LEN);
rec->assign = (byteAssign_t *)SimpleInfoAssignment;
rec->pyldLen = HMSIMPLE_INFO_PAYLOAD_LEN;
break;
case SystemConfigPara: case SystemConfigPara:
rec->length = (uint8_t)(HMSYSTEM_LIST_LEN); rec->length = (uint8_t)(HMSYSTEM_LIST_LEN);
rec->assign = (byteAssign_t *)SystemConfigParaAssignment; rec->assign = (byteAssign_t *)SystemConfigParaAssignment;
@ -554,6 +588,39 @@ class Inverter {
} }
} }
void resetAlarms() {
lastAlarm.fill({0, 0, 0});
alarmNxtWrPos = 0;
alarmCnt = 0;
alarmLastId = 0;
memset(mOffYD, 0, sizeof(float) * 6);
memset(mLastYD, 0, sizeof(float) * 6);
}
bool parseGetLossRate(uint8_t pyld[], uint8_t len) {
if (len == HMGETLOSSRATE_PAYLOAD_LEN) {
uint16_t rxCnt = (pyld[0] << 8) + pyld[1];
uint16_t txCnt = (pyld[2] << 8) + pyld[3];
if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past
DPRINT_IVID(DBG_INFO, id);
DBGPRINTLN("Inv loss: " +
String (mDtuTxCnt - (rxCnt - mIvRxCnt)) + " of " +
String (mDtuTxCnt) + ", DTU loss: " +
String (txCnt - mIvTxCnt - mDtuRxCnt) + " of " +
String (txCnt - mIvTxCnt));
}
mIvRxCnt = rxCnt;
mIvTxCnt = txCnt;
mDtuRxCnt = 0; // start new interval
mDtuTxCnt = 0; // start new interval
return true;
}
return false;
}
uint16_t parseAlarmLog(uint8_t id, uint8_t pyld[], uint8_t len) { uint16_t parseAlarmLog(uint8_t id, uint8_t pyld[], uint8_t len) {
uint8_t startOff = 2 + id * ALARM_LOG_ENTRY_SIZE; uint8_t startOff = 2 + id * ALARM_LOG_ENTRY_SIZE;
if((startOff + ALARM_LOG_ENTRY_SIZE) > len) if((startOff + ALARM_LOG_ENTRY_SIZE) > len)
@ -563,9 +630,9 @@ class Inverter {
uint32_t startTimeOffset = 0, endTimeOffset = 0; uint32_t startTimeOffset = 0, endTimeOffset = 0;
uint32_t start, endTime; uint32_t start, endTime;
if (((wCode >> 13) & 0x01) == 1) // check if is AM or PM // check if is AM or PM
startTimeOffset = 12 * 60 * 60; startTimeOffset = ((wCode >> 13) & 0x01) * 12 * 60 * 60;
if (((wCode >> 12) & 0x01) == 1) // check if is AM or PM if (((wCode >> 12) & 0x03) != 0)
endTimeOffset = 12 * 60 * 60; endTimeOffset = 12 * 60 * 60;
start = (((uint16_t)pyld[startOff + 4] << 8) | ((uint16_t)pyld[startOff + 5])) + startTimeOffset; start = (((uint16_t)pyld[startOff + 4] << 8) | ((uint16_t)pyld[startOff + 5])) + startTimeOffset;
@ -575,6 +642,7 @@ class Inverter {
addAlarm(pyld[startOff+1], start, endTime); addAlarm(pyld[startOff+1], start, endTime);
alarmCnt++; alarmCnt++;
alarmLastId = alarmMesIndex;
return pyld[startOff+1]; return pyld[startOff+1];
} }
@ -582,59 +650,102 @@ class Inverter {
static String getAlarmStr(uint16_t alarmCode) { static String getAlarmStr(uint16_t alarmCode) {
switch (alarmCode) { // breaks are intentionally missing! switch (alarmCode) { // breaks are intentionally missing!
case 1: return String(F("Inverter start")); case 1: return String(F("Inverter start"));
case 2: return String(F("DTU command failed")); case 2: return String(F("Time calibration"));
case 3: return String(F("EEPROM reading and writing error during operation"));
case 4: return String(F("Offline"));
case 11: return String(F("Grid voltage surge"));
case 12: return String(F("Grid voltage sharp drop"));
case 13: return String(F("Grid frequency mutation"));
case 14: return String(F("Grid phase mutation"));
case 15: return String(F("Grid transient fluctuation"));
case 36: return String(F("INV overvoltage or overcurrent"));
case 46: return String(F("FB overvoltage"));
case 47: return String(F("FB overcurrent"));
case 48: return String(F("FB clamp overvoltage"));
case 49: return String(F("FB clamp overvoltage"));
case 61: return String(F("Calibration parameter error"));
case 62: return String(F("System configuration parameter error"));
case 63: return String(F("Abnormal power generation data"));
case 71: return String(F("Grid overvoltage load reduction (VW) function enable"));
case 72: return String(F("Power grid over-frequency load reduction (FW) function enable"));
case 73: return String(F("Over-temperature load reduction (TW) function enable"));
case 95: return String(F("PV-1: Module in suspected shadow"));
case 96: return String(F("PV-2: Module in suspected shadow"));
case 97: return String(F("PV-3: Module in suspected shadow"));
case 98: return String(F("PV-4: Module in suspected shadow"));
case 121: return String(F("Over temperature protection")); case 121: return String(F("Over temperature protection"));
case 122: return String(F("Microinverter is suspected of being stolen"));
case 123: return String(F("Locked by remote control"));
case 124: return String(F("Shut down by remote control"));
case 125: return String(F("Grid configuration parameter error")); case 125: return String(F("Grid configuration parameter error"));
case 126: return String(F("Software error code 126")); case 126: return String(F("EEPROM reading and writing error"));
case 127: return String(F("Firmware error")); case 127: return String(F("Firmware error"));
case 128: return String(F("Software error code 128")); case 128: return String(F("Hardware configuration error"));
case 129: return String(F("Software error code 129")); case 129: return String(F("Abnormal bias"));
case 130: return String(F("Offline")); case 130: return String(F("Offline"));
case 141: return String(F("Grid overvoltage")); case 141: return String(F("Grid: Grid overvoltage"));
case 142: return String(F("Average grid overvoltage")); case 142: return String(F("Grid: 10 min value grid overvoltage"));
case 143: return String(F("Grid undervoltage")); case 143: return String(F("Grid: Grid undervoltage"));
case 144: return String(F("Grid overfrequency")); case 144: return String(F("Grid: Grid overfrequency"));
case 145: return String(F("Grid underfrequency")); case 145: return String(F("Grid: Grid underfrequency"));
case 146: return String(F("Rapid grid frequency change")); case 146: return String(F("Grid: Rapid grid frequency change rate"));
case 147: return String(F("Power grid outage")); case 147: return String(F("Grid: Power grid outage"));
case 148: return String(F("Grid disconnection")); case 148: return String(F("Grid: Grid disconnection"));
case 149: return String(F("Island detected")); case 149: return String(F("Grid: Island detected"));
case 205: return String(F("Input port 1 & 2 overvoltage"));
case 206: return String(F("Input port 3 & 4 overvoltage")); case 150: return String(F("DCI exceeded"));
case 207: return String(F("Input port 1 & 2 undervoltage"));
case 208: return String(F("Input port 3 & 4 undervoltage")); case 171: return String(F("Grid: Abnormal phase difference between phase to phase"));
case 209: return String(F("Port 1 no input")); case 181: return String(F("Abnormal insulation impedance"));
case 210: return String(F("Port 2 no input")); case 182: return String(F("Abnormal grounding"));
case 211: return String(F("Port 3 no input")); case 205: return String(F("MPPT-A: Input overvoltage"));
case 212: return String(F("Port 4 no input")); case 206: return String(F("MPPT-B: Input overvoltage"));
case 213: return String(F("PV-1 & PV-2 abnormal wiring")); case 207: return String(F("MPPT-A: Input undervoltage"));
case 214: return String(F("PV-3 & PV-4 abnormal wiring")); case 208: return String(F("MPPT-B: Input undervoltage"));
case 215: return String(F("PV-1 Input overvoltage")); case 209: return String(F("PV-1: No input"));
case 216: return String(F("PV-1 Input undervoltage")); case 210: return String(F("PV-2: No input"));
case 217: return String(F("PV-2 Input overvoltage")); case 211: return String(F("PV-3: No input"));
case 218: return String(F("PV-2 Input undervoltage")); case 212: return String(F("PV-4: No input"));
case 219: return String(F("PV-3 Input overvoltage")); case 213: return String(F("MPPT-A: PV-1 & PV-2 abnormal wiring"));
case 220: return String(F("PV-3 Input undervoltage")); case 214: return String(F("MPPT-B: PV-3 & PV-4 abnormal wiring"));
case 221: return String(F("PV-4 Input overvoltage")); case 215: return String(F("MPPT-C: Input overvoltage"));
case 222: return String(F("PV-4 Input undervoltage")); case 216: return String(F("PV-1: Input undervoltage"));
case 301: return String(F("Hardware error code 301")); case 217: return String(F("PV-2: Input overvoltage"));
case 302: return String(F("Hardware error code 302")); case 218: return String(F("PV-2: Input undervoltage"));
case 303: return String(F("Hardware error code 303")); case 219: return String(F("PV-3: Input overvoltage"));
case 304: return String(F("Hardware error code 304")); case 220: return String(F("PV-3: Input undervoltage"));
case 305: return String(F("Hardware error code 305")); case 221: return String(F("PV-4: Input overvoltage"));
case 306: return String(F("Hardware error code 306")); case 222: return String(F("PV-4: Input undervoltage"));
case 307: return String(F("Hardware error code 307"));
case 308: return String(F("Hardware error code 308")); case 301: return String(F("FB short circuit failure"));
case 302: return String(F("FB short circuit failure"));
case 303: return String(F("FB overcurrent protection failure"));
case 304: return String(F("FB overcurrent protection failure"));
case 305: return String(F("FB clamp circuit failure"));
case 306: return String(F("FB clamp circuit failure"));
case 307: return String(F("INV power device failure"));
case 308: return String(F("INV overcurrent or overvoltage protection failure"));
case 309: return String(F("Hardware error code 309")); case 309: return String(F("Hardware error code 309"));
case 310: return String(F("Hardware error code 310")); case 310: return String(F("Hardware error code 310"));
case 311: return String(F("Hardware error code 311")); case 311: return String(F("Hardware error code 311"));
case 312: return String(F("Hardware error code 312")); case 312: return String(F("Hardware error code 312"));
case 313: return String(F("Hardware error code 313")); case 313: return String(F("Hardware error code 313"));
case 314: return String(F("Hardware error code 314")); case 314: return String(F("Hardware error code 314"));
case 5041: return String(F("Error code-04 Port 1"));
case 5042: return String(F("Error code-04 Port 2")); case 5011: return String(F("PV-1: MOSFET overcurrent (II)"));
case 5043: return String(F("Error code-04 Port 3")); case 5012: return String(F("PV-2: MOSFET overcurrent (II)"));
case 5044: return String(F("Error code-04 Port 4")); case 5013: return String(F("PV-3: MOSFET overcurrent (II)"));
case 5014: return String(F("PV-4: MOSFET overcurrent (II)"));
case 5020: return String(F("H-bridge MOSFET overcurrent or H-bridge overvoltage"));
case 5041: return String(F("PV-1: current overcurrent (II)"));
case 5042: return String(F("PV-2: current overcurrent (II)"));
case 5043: return String(F("PV-3: current overcurrent (II)"));
case 5044: return String(F("PV-4: current overcurrent (II)"));
case 5051: return String(F("PV Input 1 Overvoltage/Undervoltage")); case 5051: return String(F("PV Input 1 Overvoltage/Undervoltage"));
case 5052: return String(F("PV Input 2 Overvoltage/Undervoltage")); case 5052: return String(F("PV Input 2 Overvoltage/Undervoltage"));
case 5053: return String(F("PV Input 3 Overvoltage/Undervoltage")); case 5053: return String(F("PV Input 3 Overvoltage/Undervoltage"));
@ -644,15 +755,38 @@ class Inverter {
case 5080: return String(F("Grid Overvoltage/Undervoltage")); case 5080: return String(F("Grid Overvoltage/Undervoltage"));
case 5090: return String(F("Grid Overfrequency/Underfrequency")); case 5090: return String(F("Grid Overfrequency/Underfrequency"));
case 5100: return String(F("Island detected")); case 5100: return String(F("Island detected"));
case 5110: return String(F("GFDI"));
case 5120: return String(F("EEPROM reading and writing error")); case 5120: return String(F("EEPROM reading and writing error"));
case 5141:
case 5142:
case 5143:
case 5144:
return String(F("FB clamp overvoltage"));
case 5150: return String(F("10 min value grid overvoltage")); case 5150: return String(F("10 min value grid overvoltage"));
case 5160: return String(F("Grid transient fluctuation"));
case 5200: return String(F("Firmware error")); case 5200: return String(F("Firmware error"));
case 8310: return String(F("Shut down")); case 8310: return String(F("Shut down by remote control"));
case 8320: return String(F("Locked by remote control"));
case 9000: return String(F("Microinverter is suspected of being stolen")); case 9000: return String(F("Microinverter is suspected of being stolen"));
default: return String(F("Unknown")); default: return String(F("Unknown"));
} }
} }
void addGridProfile(uint8_t buf[], uint8_t length) {
mGridLen = (length > MAX_GRID_LENGTH) ? MAX_GRID_LENGTH : length;
std::copy(buf, &buf[mGridLen], mGridProfile);
}
String getGridProfile(void) {
char buf[MAX_GRID_LENGTH * 3];
memset(buf, 0, MAX_GRID_LENGTH);
for(uint8_t i = 0; i < mGridLen; i++) {
snprintf(&buf[i*3], 4, "%02X ", mGridProfile[i]);
}
buf[mGridLen*3] = 0;
return String(buf);
}
private: private:
inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) { inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) {
lastAlarm[alarmNxtWrPos] = alarm_t(code, start, end); lastAlarm[alarmNxtWrPos] = alarm_t(code, start, end);
@ -670,8 +804,11 @@ class Inverter {
radioId.b[0] = 0x01; radioId.b[0] = 0x01;
} }
std::queue<std::shared_ptr<CommandAbstract>> _commandQueue; private:
float mOffYD[6], mLastYD[6];
bool mDevControlRequest; // true if change needed bool mDevControlRequest; // true if change needed
uint8_t mGridLen = 0;
uint8_t mGridProfile[MAX_GRID_LENGTH];
}; };
template <class REC_TYP> template <class REC_TYP>

418
src/hm/hmPayload.h

@ -1,418 +0,0 @@
//-----------------------------------------------------------------------------
// 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 "hmRadio.h"
#include <Arduino.h>
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<void(uint8_t, Inverter<> *)> payloadListenerType;
typedef std::function<void(Inverter<> *)> alarmListenerType;
template<class HMSYSTEM, class HMRADIO>
class HmPayload {
public:
HmPayload() {}
void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) {
mApp = app;
mSys = sys;
mRadio = radio;
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 simulation() {
uint8_t pay[] = {
0x00, 0x01, 0x01, 0x24, 0x02, 0x28, 0x02, 0x33,
0x06, 0x49, 0x06, 0x6a, 0x00, 0x05, 0x5f, 0x1b,
0x00, 0x06, 0x66, 0x9a, 0x03, 0xfd, 0x04, 0x0b,
0x01, 0x23, 0x02, 0x28, 0x02, 0x28, 0x06, 0x41,
0x06, 0x43, 0x00, 0x05, 0xdc, 0x2c, 0x00, 0x06,
0x2e, 0x3f, 0x04, 0x01, 0x03, 0xfb, 0x09, 0x78,
0x13, 0x86, 0x18, 0x15, 0x00, 0xcf, 0x00, 0xfe,
0x03, 0xe7, 0x01, 0x42, 0x00, 0x03
};
Inverter<> *iv = mSys->getInverterByPos(0);
record_t<> *rec = iv->getRecordStruct(0x0b);
rec->ts = *mTimestamp;
for (uint8_t i = 0; i < rec->length; i++) {
iv->addValue(i, pay, rec);
yield();
}
iv->doCalculations();
notify(0x0b, iv);
}*/
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 (mSerialDebug)
DPRINT_IVID(DBG_INFO, iv->id);
if (MAX_PAYLOAD_ENTRIES == mPayload[iv->id].maxPackId) {
mStat->rxFailNoAnser++; // got nothing
if (mSerialDebug)
DBGPRINTLN(F("enqueued cmd failed/timeout"));
} else {
mStat->rxFail++; // got fragments but not complete response
if (mSerialDebug) {
DBGPRINT(F("no complete Payload received! (retransmits: "));
DBGPRINT(String(mPayload[iv->id].retransmits));
DBGPRINTLN(F(")"));
}
}
iv->setQueuedCmdFinished(); // command failed
}
}
}
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]));
}
iv->powerLimitAck = false;
mRadio->sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false);
mPayload[iv->id].txCmd = iv->devControlCmd;
//iv->clearCmdQueue();
//iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
} else {
uint8_t cmd = iv->getQueuedCmd();
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("prepareDevInformCmd 0x"));
DBGHEXLN(cmd);
mRadio->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);
iv->powerLimitAck = true;
} 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<InfoCommand>(SystemConfigPara); // read back power limit
if(mHighPrioIv == NULL) // do it immediately if possible
mHighPrioIv = iv;
}
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_HM != 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"));
mRadio->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));
mRadio->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"));
mRadio->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);
mRadio->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[150];
uint8_t payloadLen = 0;
memset(payload, 0, 150);
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) {
if((mPayload[iv->id].len[i] + payloadLen) > 150) {
DPRINTLN(DBG_ERROR, F("payload buffer to small!"));
break;
}
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("): "));
ah::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, iv);
if(AlarmData == mPayload[iv->id].txCmd) {
uint8_t i = 0;
while(1) {
if(0 == iv->parseAlarmLog(i++, payload, payloadLen))
break;
if (NULL != mCbAlarm)
(mCbAlarm)(iv);
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, Inverter<> *iv) {
if(NULL != mCbPayload)
(mCbPayload)(val, iv);
}
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_IVID(DBG_INFO, id);
DBGPRINTLN(F("resetPayload"));
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;
HMRADIO *mRadio;
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__*/

443
src/hm/hmRadio.h

@ -3,180 +3,177 @@
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __RADIO_H__ #ifndef __HM_RADIO_H__
#define __RADIO_H__ #define __HM_RADIO_H__
#include "../utils/dbg.h"
#include <RF24.h> #include <RF24.h>
#include "../utils/crc.h"
#include "../config/config.h"
#include "SPI.h" #include "SPI.h"
#include "radio.h"
#include "../config/config.h"
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
#include "nrfHal.h"
#endif
#define SPI_SPEED 1000000 #define SPI_SPEED 1000000
#define RF_CHANNELS 5 #define RF_CHANNELS 5
#define TX_REQ_INFO 0x15
#define TX_REQ_DEVCONTROL 0x51
#define ALL_FRAMES 0x80
#define SINGLE_FRAME 0x81
const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
#define TX_REQ_DREDCONTROL 0x50
#define DRED_A5 0xa5
#define DRED_5A 0x5a
#define DRED_AA 0xaa
#define DRED_55 0x55
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// HM Radio class // HM Radio class
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
template <uint8_t IRQ_PIN = DEF_IRQ_PIN, uint8_t CE_PIN = DEF_CE_PIN, uint8_t CS_PIN = DEF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW, uint8_t SCLK_PIN = DEF_SCLK_PIN, uint8_t MOSI_PIN = DEF_MOSI_PIN, uint8_t MISO_PIN = DEF_MISO_PIN> template <uint8_t IRQ_PIN = DEF_NRF_IRQ_PIN, uint8_t CE_PIN = DEF_NRF_CE_PIN, uint8_t CS_PIN = DEF_NRF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW, uint8_t SCLK_PIN = DEF_NRF_SCLK_PIN, uint8_t MOSI_PIN = DEF_NRF_MOSI_PIN, uint8_t MISO_PIN = DEF_NRF_MISO_PIN, uint32_t DTU_SN = 0x81001765>
class HmRadio { class HmRadio : public Radio {
public: public:
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) {
if(mSerialDebug) { HmRadio() {
DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); mDtuSn = DTU_SN;
DBGPRINT(String(CE_PIN));
DBGPRINT(F(", CS_PIN: "));
DBGPRINT(String(CS_PIN));
DBGPRINT(F(", SPI_SPEED: "));
DBGPRINT(String(SPI_SPEED));
DBGPRINTLN(F(")"));
}
// Depending on the program, the module can work on 2403, 2423, 2440, 2461 or 2475MHz.
// Channel List 2403, 2423, 2440, 2461, 2475MHz
mRfChLst[0] = 03;
mRfChLst[1] = 23;
mRfChLst[2] = 40;
mRfChLst[3] = 61;
mRfChLst[4] = 75;
// VArt: Overwriting 61/75 so I got much more stable responses
mRfChLst[3] = 23;
mRfChLst[4] = 40;
// default channels
mTxChIdx = 2; // Start TX with 40
mRxChIdx = 0; // Start RX with 03
mSendCnt = 0;
mRetransmits = 0;
mSerialDebug = false;
mIrqRcvd = false; mIrqRcvd = false;
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
//mNrf24.reset(new RF24());
#else
mNrf24.reset(new RF24(CE_PIN, CS_PIN, SPI_SPEED));
#endif
} }
~HmRadio() {} ~HmRadio() {}
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) { void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, 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")); DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup"));
pinMode(irq, INPUT_PULLUP); pinMode(irq, INPUT_PULLUP);
uint32_t dtuSn = 0x87654321; mSerialDebug = serialDebug;
uint32_t chipID = 0; // will be filled with last 3 bytes of MAC mPrivacyMode = privacyMode;
#ifdef ESP32 mPrintWholeTrace = printWholeTrace;
uint64_t MAC = ESP.getEfuseMac();
chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); generateDtuSn();
#else DTU_RADIO_ID = ((uint64_t)(((mDtuSn >> 24) & 0xFF) | ((mDtuSn >> 8) & 0xFF00) | ((mDtuSn << 8) & 0xFF0000) | ((mDtuSn << 24) & 0xFF000000)) << 8) | 0x01;
chipID = ESP.getChipId();
#endif
if(chipID) {
dtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
for(int i = 0; i < 7; i++) {
dtuSn |= (chipID % 10) << (i * 4);
chipID /= 10;
}
}
// 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;
#ifdef ESP32 #ifdef ESP32
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
mNrfHal.init(mosi, miso, sclk, cs, ce, SPI_SPEED);
mNrf24.reset(new RF24(&mNrfHal));
#else
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
mSpi = new SPIClass(HSPI); mSpi.reset(new SPIClass(HSPI));
#else #else
mSpi = new SPIClass(VSPI); mSpi.reset(new SPIClass(VSPI));
#endif #endif
mSpi->begin(sclk, miso, mosi, cs); mSpi->begin(sclk, miso, mosi, cs);
#endif
#else #else
//the old ESP82xx cannot freely place their SPI pins //the old ESP82xx cannot freely place their SPI pins
mSpi = new SPIClass(); mSpi.reset(new SPIClass());
mSpi->begin(); mSpi->begin();
#endif #endif
mNrf24.begin(mSpi, ce, cs);
mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
mNrf24->begin();
mNrf24.setChannel(mRfChLst[mRxChIdx]); #else
mNrf24.startListening(); mNrf24->begin(mSpi.get(), ce, cs);
mNrf24.setDataRate(RF24_250KBPS); #endif
mNrf24.setAutoAck(true); mNrf24->setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms
mNrf24.enableDynamicPayloads();
mNrf24.setCRCLength(RF24_CRC_16); mNrf24->setChannel(mRfChLst[mRxChIdx]);
mNrf24.setAddressWidth(5); mNrf24->startListening();
mNrf24.openReadingPipe(1, reinterpret_cast<uint8_t*>(&DTU_RADIO_ID)); mNrf24->setDataRate(RF24_250KBPS);
mNrf24->setAutoAck(true);
mNrf24->enableDynamicAck();
mNrf24->enableDynamicPayloads();
mNrf24->setCRCLength(RF24_CRC_16);
mNrf24->setAddressWidth(5);
mNrf24->openReadingPipe(1, reinterpret_cast<uint8_t*>(&DTU_RADIO_ID));
// enable all receiving interrupts // enable all receiving interrupts
mNrf24.maskIRQ(false, false, false); mNrf24->maskIRQ(false, false, false);
DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); mNrf24->setPALevel(1); // low is default
DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr]));
mNrf24.setPALevel(ampPwr & 0x03);
if(mNrf24.isChipConnected()) { if(mNrf24->isChipConnected()) {
DPRINTLN(DBG_INFO, F("Radio Config:")); DPRINTLN(DBG_INFO, F("Radio Config:"));
mNrf24.printPrettyDetails(); mNrf24->printPrettyDetails();
} DPRINT(DBG_INFO, F("DTU_SN: "));
else DBGPRINTLN(String(mDtuSn, HEX));
} else
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
} }
bool loop(void) { void loop(void) {
if (!mIrqRcvd) if (!mIrqRcvd)
return false; // nothing to do return; // nothing to do
mIrqRcvd = false; mIrqRcvd = false;
bool tx_ok, tx_fail, rx_ready; bool tx_ok, tx_fail, rx_ready;
mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
mNrf24.flush_tx(); // empty TX FIFO mNrf24->flush_tx(); // empty TX FIFO
// start listening // start listening
mNrf24.setChannel(mRfChLst[mRxChIdx]); uint8_t chOffset = 2;
mNrf24.startListening(); mRxChIdx = (mTxChIdx + chOffset) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
mNrf24->startListening();
if(NULL == mLastIv) // prevent reading on NULL object!
return;
uint32_t innerLoopTimeout = 55000;
uint32_t loopMillis = millis(); uint32_t loopMillis = millis();
while (millis()-loopMillis < 400) { uint32_t outerLoopTimeout = (mLastIv->mIsSingleframeReq) ? 100 : ((mLastIv->mCmd != AlarmData) && (mLastIv->mCmd != GridOnProFilePara)) ? 400 : 600;
bool isRxInit = true;
while ((millis() - loopMillis) < outerLoopTimeout) {
uint32_t startMicros = micros(); uint32_t startMicros = micros();
while (micros()-startMicros < 5111) { // listen (4088us or?) 5110us to each channel while ((micros() - startMicros) < innerLoopTimeout) { // listen (4088us or?) 5110us to each channel
if (mIrqRcvd) { if (mIrqRcvd) {
mIrqRcvd = false; mIrqRcvd = false;
if (getReceived()) { // everything received if (getReceived()) { // everything received
return true; return;
}
innerLoopTimeout = 4088*5;
if (isRxInit) {
isRxInit = false;
if (micros() - startMicros < 42000) {
innerLoopTimeout = 4088*12;
mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
}
} }
startMicros = micros();
} }
yield(); yield();
} }
// switch to next RX channel // switch to next RX channel
if(++mRxChIdx >= RF_CHANNELS) mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
mRxChIdx = 0; mNrf24->setChannel(mRfChLst[mRxChIdx]);
mNrf24.setChannel(mRfChLst[mRxChIdx]); innerLoopTimeout = 4088;
yield(); isRxInit = false;
} }
// not finished but time is over // not finished but time is over
return true;
}
void handleIntr(void) { return;
mIrqRcvd = true;
} }
bool isChipConnected(void) { bool isChipConnected(void) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected"));
return mNrf24.isChipConnected(); return mNrf24->isChipConnected();
}
void enableDebug() {
mSerialDebug = true;
} }
void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) { void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) {
DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("sendControlPacket cmd: "));
DBGHEXLN(cmd); DBGHEXLN(cmd);
initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME);
uint8_t cnt = 10; uint8_t cnt = 10;
if (isNoMI) { if (IV_MI != iv->ivGen) {
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[cnt++] = 0x00; mTxBuf[cnt++] = 0x00;
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
@ -186,160 +183,194 @@ class HmRadio {
mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
} }
} else { //MI 2nd gen. specific } else { //MI 2nd gen. specific
uint16_t powerMax = ((iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower());
switch (cmd) { switch (cmd) {
case Restart:
case TurnOn: case TurnOn:
//mTxBuf[0] = 0x50; mTxBuf[9] = DRED_55;
mTxBuf[9] = 0x55; mTxBuf[10] = DRED_AA;
mTxBuf[10] = 0xaa;
break; break;
case TurnOff: case TurnOff:
mTxBuf[9] = 0xaa; mTxBuf[9] = DRED_AA;
mTxBuf[10] = 0x55; mTxBuf[10] = DRED_55;
break; break;
case ActivePowerContr: case ActivePowerContr:
cnt++; if (data[1]<256) { // non persistent
mTxBuf[9] = 0x5a; mTxBuf[9] = DRED_5A;
mTxBuf[10] = 0x5a; mTxBuf[10] = DRED_5A;
mTxBuf[11] = data[0]; // power limit //Testing only! Original NRF24_DTUMIesp.ino code #L612-L613:
//UsrData[0]=0x5A;UsrData[1]=0x5A;UsrData[2]=100;//0x0a;// 10% limit
//UsrData[3]=((Limit*10) >> 8) & 0xFF; UsrData[4]= (Limit*10) & 0xFF; //WR needs 1 dec= zB 100.1 W
if (!data[1]) { // AbsolutNonPersistent
mTxBuf[++cnt] = 100; //10% limit, seems to be necessary to send sth. at all, but for MI-1500 this has no effect
//works (if ever!) only for absulute power limits!
mTxBuf[++cnt] = ((data[0] * 10) >> 8) & 0xff; // power limit in W
mTxBuf[++cnt] = ((data[0] * 10) ) & 0xff; // power limit in W
} else if (powerMax) { //relative, but 4ch-MI (if ever) only accepts absolute values
mTxBuf[++cnt] = data[0]; // simple power limit in %, might be necessary to multiply by 10?
mTxBuf[++cnt] = ((data[0] * 10 * powerMax) >> 8) & 0xff; // power limit
mTxBuf[++cnt] = ((data[0] * 10 * powerMax) ) & 0xff; // power limit
} else { // might work for 1/2ch MI (if ever)
mTxBuf[++cnt] = data[0]; // simple power limit in %, might be necessary to multiply by 10?
}
} else { // persistent power limit needs to be translated in DRED command (?)
/* DRED instruction
Order Function
0x55AA Boot without DRM restrictions
0xA5A5 DRM0 shutdown
0x5A5A DRM5 power limit 0%
0xAA55 DRM6 power limit 50%
0x5A55 DRM8 unlimited power operation
*/
mTxBuf[0] = TX_REQ_DREDCONTROL;
if (data[1] == 256UL) { // AbsolutPersistent
if (data[0] == 0 && !powerMax) {
mTxBuf[9] = DRED_A5;
mTxBuf[10] = DRED_A5;
} else if (data[0] == 0 || !powerMax || data[0] < powerMax/4 ) {
mTxBuf[9] = DRED_5A;
mTxBuf[10] = DRED_5A;
} else if (data[0] <= powerMax/4*3) {
mTxBuf[9] = DRED_AA;
mTxBuf[10] = DRED_55;
} else if (data[0] <= powerMax) {
mTxBuf[9] = DRED_5A;
mTxBuf[10] = DRED_55;
} else if (data[0] > powerMax*2) {
mTxBuf[9] = DRED_55;
mTxBuf[10] = DRED_AA;
}
}
}
break; break;
default: default:
return; return;
} }
cnt++; cnt++;
} }
sendPacket(invId, cnt, isRetransmit, isNoMI); sendPacket(iv, cnt, isRetransmit, (IV_MI != iv->ivGen));
}
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.
if(mSerialDebug) {
DPRINT(DBG_DEBUG, F("prepareDevInformCmd 0x"));
DPRINTLN(DBG_DEBUG,String(cmd, HEX));
}
initPacket(invId, reqfld, ALL_FRAMES);
mTxBuf[10] = cmd; // cid
mTxBuf[11] = 0x00;
CP_U32_LittleEndian(&mTxBuf[12], ts);
/*if (cmd == AlarmData ) { //cmd == RealTimeRunData_Debug ||
mTxBuf[18] = (alarmMesId >> 8) & 0xff;
mTxBuf[19] = (alarmMesId ) & 0xff;
}*/
sendPacket(invId, 24, isRetransmit);
}
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) {
initPacket(invId, mid, pid);
sendPacket(invId, 10, isRetransmit, appendCrc16);
} }
uint8_t getDataRate(void) { uint8_t getDataRate(void) {
if(!mNrf24.isChipConnected()) if(!mNrf24->isChipConnected())
return 3; // unkown return 3; // unknown
return mNrf24.getDataRate(); return mNrf24->getDataRate();
} }
bool isPVariant(void) { bool isPVariant(void) {
return mNrf24.isPVariant(); return mNrf24->isPVariant();
} }
std::queue<packet_t> mBufCtrl;
uint32_t mSendCnt;
uint32_t mRetransmits;
bool mSerialDebug;
private: private:
bool getReceived(void) { inline bool getReceived(void) {
bool tx_ok, tx_fail, rx_ready; bool tx_ok, tx_fail, rx_ready;
mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
bool isLastPackage = false; bool isLastPackage = false;
while(mNrf24.available()) { while(mNrf24->available()) {
uint8_t len; uint8_t len;
len = mNrf24.getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed len = mNrf24->getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed
if (len > 0) { if (len > 0) {
packet_t p; packet_t p;
p.ch = mRfChLst[mRxChIdx]; p.ch = mRfChLst[mRxChIdx];
p.len = len; p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len;
mNrf24.read(p.packet, len); p.rssi = mNrf24->testRPD() ? -64 : -75;
p.millis = millis() - mMillis;
mNrf24->read(p.packet, p.len);
if (p.packet[0] != 0x00) { if (p.packet[0] != 0x00) {
if(!checkIvSerial(p.packet, mLastIv)) {
DPRINT(DBG_WARN, "RX other inverter ");
if(*mPrivacyMode)
ah::dumpBuf(p.packet, p.len, 1, 4);
else
ah::dumpBuf(p.packet, p.len);
return false;
}
mLastIv->mGotFragment = true;
mBufCtrl.push(p); mBufCtrl.push(p);
if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received
else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command
isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received
else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore fragment number zero and MI status messages //#0 was p.packet[0] != 0x00 && else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore MI status messages //#0 was p.packet[0] != 0x00 &&
isLastPackage = true; // response from dev control command isLastPackage = true; // response from dev control command
} }
} }
yield(); yield();
} }
if(isLastPackage)
mLastIv->mGotLastMsg = true;
return isLastPackage; return isLastPackage;
} }
void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) { void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) {
if(mSerialDebug) { mNrf24->setPALevel(iv->config->powerLevel & 0x03);
DPRINT(DBG_VERBOSE, F("initPacket, mid: ")); updateCrcs(&len, appendCrc16);
DPRINT(DBG_VERBOSE, String(mid, HEX));
DPRINT(DBG_VERBOSE,F(" pid: "));
DPRINTLN(DBG_VERBOSE,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;
}
void sendPacket(uint64_t invId, uint8_t len, bool isRetransmit, bool appendCrc16=true) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendPacket"));
//DPRINTLN(DBG_VERBOSE, "sent packet: #" + String(mSendCnt));
// append crc's
if (appendCrc16 && (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++;
// set TX and RX channels // set TX and RX channels
mTxChIdx = (mTxChIdx + 1) % RF_CHANNELS; mTxChIdx = iv->heuristics.txRfChId;
mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS;
if(mSerialDebug) { if(*mSerialDebug) {
DPRINT(DBG_INFO, F("TX ")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("TX "));
DBGPRINT(String(len)); DBGPRINT(String(len));
DBGPRINT("B Ch"); DBGPRINT(" CH");
DBGPRINT(String(mRfChLst[mTxChIdx])); DBGPRINT(String(mRfChLst[mTxChIdx]));
DBGPRINT(F(" | ")); DBGPRINT(F(" | "));
if(*mPrintWholeTrace) {
if(*mPrivacyMode)
ah::dumpBuf(mTxBuf, len, 1, 4);
else
ah::dumpBuf(mTxBuf, len); ah::dumpBuf(mTxBuf, len);
} else {
DHEX(mTxBuf[0]);
DBGPRINT(F(" "));
DHEX(mTxBuf[10]);
DBGPRINT(F(" "));
DBGHEXLN(mTxBuf[9]);
}
} }
mNrf24.stopListening(); mNrf24->stopListening();
mNrf24.setChannel(mRfChLst[mTxChIdx]); mNrf24->setChannel(mRfChLst[mTxChIdx]);
mNrf24.openWritingPipe(reinterpret_cast<uint8_t*>(&invId)); mNrf24->openWritingPipe(reinterpret_cast<uint8_t*>(&iv->radioId.u64));
mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response mNrf24->startWrite(mTxBuf, len, false); // false = request ACK response
mMillis = millis();
if(isRetransmit) mLastIv = iv;
mRetransmits++; iv->mDtuTxCnt++;
else
mSendCnt++;
} }
volatile bool mIrqRcvd; uint64_t getIvId(Inverter<> *iv) {
uint64_t DTU_RADIO_ID; return iv->radioId.u64;
}
uint8_t getIvGen(Inverter<> *iv) {
return iv->ivGen;
}
uint8_t mRfChLst[RF_CHANNELS]; inline bool checkIvSerial(uint8_t buf[], Inverter<> *iv) {
uint8_t mTxChIdx; for(uint8_t i = 1; i < 5; i++) {
uint8_t mRxChIdx; if(buf[i] != iv->radioId.b[i])
return false;
}
return true;
}
SPIClass* mSpi; uint64_t DTU_RADIO_ID;
RF24 mNrf24; uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; uint8_t mTxChIdx = 0;
uint8_t mRxChIdx = 0;
bool mGotLastMsg = false;
uint32_t mMillis;
std::unique_ptr<SPIClass> mSpi;
std::unique_ptr<RF24> mNrf24;
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
nrfHal mNrfHal;
#endif
Inverter<> *mLastIv = NULL;
}; };
#endif /*__RADIO_H__*/ #endif /*__HM_RADIO_H__*/

109
src/hm/hmSystem.h

@ -7,70 +7,40 @@
#define __HM_SYSTEM_H__ #define __HM_SYSTEM_H__
#include "hmInverter.h" #include "hmInverter.h"
#include <functional>
template <uint8_t MAX_INVERTER=3, class INVERTERTYPE=Inverter<float>> template <uint8_t MAX_INVERTER=3, class INVERTERTYPE=Inverter<float>>
class HmSystem { class HmSystem {
public: public:
HmSystem() {} HmSystem() {}
void setup(uint32_t *timestamp) { void setup(uint32_t *timestamp, cfgInst_t *config) {
mInverter[0].timestamp = timestamp; mInverter[0].timestamp = timestamp;
mNumInv = 0;
}
void addInverters(cfgInst_t *config) {
mInverter[0].generalConfig = config; mInverter[0].generalConfig = config;
Inverter<> *iv;
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) {
DPRINT(DBG_INFO, "added inverter ");
if(iv->config->serial.b[5] == 0x11) {
if((iv->config->serial.b[4] & 0x0f) == 0x04)
DBGPRINT("HMS");
else
DBGPRINT("HM");
} else if(iv->config->serial.b[5] == 0x13)
DBGPRINT("HMT");
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!!!"));
}
}
}
} }
INVERTERTYPE *addInverter(cfgIv_t *config) { void addInverter(uint8_t id, std::function<void(Inverter<> *iv)> cb) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter")); DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter"));
if(MAX_INVERTER <= mNumInv) { INVERTERTYPE *iv = &mInverter[id];
DPRINT(DBG_WARN, F("max number of inverters reached!")); iv->id = id;
return NULL; iv->config = &mInverter[0].generalConfig->iv[id];
} DPRINT(DBG_VERBOSE, "SERIAL: " + String(iv->config->serial.b[5], HEX));
INVERTERTYPE *p = &mInverter[mNumInv]; DPRINTLN(DBG_VERBOSE, " " + String(iv->config->serial.b[4], HEX));
p->id = mNumInv; if((iv->config->serial.b[5] == 0x11) || (iv->config->serial.b[5] == 0x10)) {
p->config = config; switch(iv->config->serial.b[4]) {
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) || (p->config->serial.b[5] == 0x10)) {
switch(p->config->serial.b[4]) {
case 0x24: // HMS-500 case 0x24: // HMS-500
case 0x22: case 0x22:
case 0x21: p->type = INV_TYPE_1CH; case 0x21: iv->type = INV_TYPE_1CH;
break; break;
case 0x44: // HMS-1000 case 0x44: // HMS-1000
case 0x42: case 0x42:
case 0x41: p->type = INV_TYPE_2CH; case 0x41: iv->type = INV_TYPE_2CH;
break; break;
case 0x64: // HMS-2000 case 0x64: // HMS-2000
case 0x62: case 0x62:
case 0x61: p->type = INV_TYPE_4CH; case 0x61: iv->type = INV_TYPE_4CH;
break; break;
default: default:
@ -78,32 +48,52 @@ class HmSystem {
break; break;
} }
if(p->config->serial.b[5] == 0x11) { if(iv->config->serial.b[5] == 0x11) {
if((p->config->serial.b[4] & 0x0f) == 0x04) if((iv->config->serial.b[4] & 0x0f) == 0x04)
p->ivGen = IV_HMS; iv->ivGen = IV_HMS;
else else
p->ivGen = IV_HM; iv->ivGen = IV_HM;
} }
else if((p->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM else if((iv->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM
p->ivGen = IV_HM; iv->ivGen = IV_HM;
else // MI 2nd Gen else // MI 2nd Gen
p->ivGen = IV_MI; iv->ivGen = IV_MI;
} else if(p->config->serial.b[5] == 0x13) { } else if(iv->config->serial.b[5] == 0x13) {
p->ivGen = IV_HMT; iv->ivGen = IV_HMT;
p->type = INV_TYPE_6CH; iv->type = INV_TYPE_6CH;
} else if(p->config->serial.u64 != 0ULL) } else if(iv->config->serial.u64 != 0ULL) {
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); DPRINTLN(DBG_ERROR, F("inverter type can't be detected!"));
return;
} else
iv->ivGen = IV_UNKNOWN;
p->init(); iv->init();
if(IV_UNKNOWN == iv->ivGen)
return; // serial is 0
mNumInv ++; DPRINT(DBG_INFO, "added inverter ");
return p; if(iv->config->serial.b[5] == 0x11) {
if((iv->config->serial.b[4] & 0x0f) == 0x04)
DBGPRINT("HMS");
else
DBGPRINT("HM");
} else if(iv->config->serial.b[5] == 0x13)
DBGPRINT("HMT");
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!!!"));
cb(iv);
} }
INVERTERTYPE *findInverter(uint8_t buf[]) { INVERTERTYPE *findInverter(uint8_t buf[]) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter")); DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter"));
INVERTERTYPE *p; INVERTERTYPE *p;
for(uint8_t i = 0; i < mNumInv; i++) { for(uint8_t i = 0; i < MAX_INVERTER; i++) {
p = &mInverter[i]; p = &mInverter[i];
if((p->config->serial.b[3] == buf[0]) if((p->config->serial.b[3] == buf[0])
&& (p->config->serial.b[2] == buf[1]) && (p->config->serial.b[2] == buf[1])
@ -118,7 +108,7 @@ class HmSystem {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos"));
if(pos >= MAX_INVERTER) if(pos >= MAX_INVERTER)
return NULL; return NULL;
else if((mInverter[pos].initialized && mInverter[pos].config->serial.u64 != 0ULL) || false == check) else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check))
return &mInverter[pos]; return &mInverter[pos];
else else
return NULL; return NULL;
@ -138,7 +128,6 @@ class HmSystem {
private: private:
INVERTERTYPE mInverter[MAX_INVERTER]; INVERTERTYPE mInverter[MAX_INVERTER];
uint8_t mNumInv;
}; };
#endif /*__HM_SYSTEM_H__*/ #endif /*__HM_SYSTEM_H__*/

775
src/hm/miPayload.h

@ -1,775 +0,0 @@
//-----------------------------------------------------------------------------
// 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 <Arduino.h>
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;
bool gotFragment;
/*
uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE];
uint8_t maxPackId;
bool lastFound;*/
} miPayload_t;
typedef std::function<void(uint8_t, Inverter<> *)> miPayloadListenerType;
template<class HMSYSTEM, class HMRADIO>
class MiPayload {
public:
MiPayload() {}
void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) {
mApp = app;
mSys = sys;
mRadio = radio;
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) {
mCbAlarm = cb;
}
void loop() {
if (NULL != mHighPrioIv) {
ivSend(mHighPrioIv, true); // for e.g. 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 (mSerialDebug)
DPRINT_IVID(DBG_INFO, iv->id);
if (!mPayload[iv->id].gotFragment) {
mStat->rxFailNoAnser++; // got nothing
if (mSerialDebug)
DBGPRINTLN(F("enqueued cmd failed/timeout"));
} else {
mStat->rxFail++; // got "fragments" (part of the required messages)
// but no complete set of responses
if (mSerialDebug) {
DBGPRINT(F("no complete Payload received! (retransmits: "));
DBGPRINT(String(mPayload[iv->id].retransmits));
DBGPRINTLN(F(")"));
}
}
iv->setQueuedCmdFinished(); // command failed
}
}
}
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]));
}
iv->powerLimitAck = false;
mRadio->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<InfoCommand>(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?
mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false);
} else {
mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd2, false, false);
};
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;
mPayload[iv->id].gotFragment = 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]));
}
} 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)"));
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]));
mPayload[iv->id].gotFragment = true;
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_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]));
DPRINT(DBG_INFO,F("HW_ECapValue "));
DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21]));
}
} else {
DBGPRINTLN(F("3rd gen. inverter!")); // see table in OpenDTU code, DevInfoParser.cpp devInfo[]
}
} else if ( p->packet[9] == 0x12 ) {//3rd frame
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("got 3rd frame (hw info)"));
iv->setQueuedCmdFinished();
mPayload[iv->id].complete = true;
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);
iv->powerLimitAck = true;
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<InfoCommand>(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 ("));
DBGPRINT(String(payloadLen));
DBGPRINT("): ");
ah::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, iv);
if(AlarmData == mPayload[iv->id].txCmd) {
uint8_t i = 0;
while(1) {
if(0 == iv->parseAlarmLog(i++, payload, payloadLen))
break;
if (NULL != mCbAlarm)
(mCbAlarm)(iv);
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"));
mRadio->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
mRadio->sendCmdPacket(iv->radioId.u64, 0x0f, 0x00, true, false);
//iv->setQueuedCmdFinished();
//cmd = iv->getQueuedCmd();
} else {
bool change = false;
if ( cmd >= 0x36 && cmd < 0x39 ) { // MI-1500 Data command
if (cmd > 0x36 && mPayload[iv->id].retransmits==1) // first request for the upper channels
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);
mRadio->sendCmdPacket(iv->radioId.u64, cmd, cmd, true, false);
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);
mRadio->sendCmdPacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].txCmd, false, false);
}
}
}
yield();
}
}
private:
void notify(uint8_t val, Inverter<> *iv) {
if(NULL != mCbMiPayload)
(mCbMiPayload)(val, iv);
}
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 statusMi = 3; // regular status for MI, change to 1 later?
if ( uState == 2 ) {
statusMi = 5050 + stschan; //first approach, needs review!
if (lState)
statusMi += lState*10;
} else if ( uState > 3 ) {
statusMi = uState*1000 + uEnum*10;
if (lState)
statusMi += lState*100; //needs review, esp. for 4ch-8310 state!
//if (lEnum)
statusMi += lEnum;
if (uEnum < 6) {
statusMi += stschan;
}
if (statusMi == 8000)
statusMi = 8310; //trick?
}
uint16_t prntsts = statusMi == 3 ? 1 : statusMi;
if ( statusMi != mPayload[iv->id].sts[stschan] ) { //sth.'s changed?
mPayload[iv->id].sts[stschan] = statusMi;
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;
// 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)));
if ( datachan < 3 ) {
mPayload[iv->id].dataAB[datachan] = true;
}
if ( !mPayload[iv->id].dataAB[CH0] && mPayload[iv->id].dataAB[CH1] && 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) ) {
mPayload[iv->id].txCmd++;
mPayload[iv->id].retransmits = 0; // reserve retransmissions for each response
mPayload[iv->id].complete = false;
}
}
/*
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)
(mCbAl { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
}*/
//if ( mPayload[iv->id].complete || //4ch device
if ( p->packet[0] == (0x39 + ALL_FRAMES) || //4ch device - last message
(iv->type != INV_TYPE_4CH //other devices
&& mPayload[iv->id].dataAB[CH0]
&& mPayload[iv->id].stsAB[CH0])) {
miComplete(iv);
}
}
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;
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();
// update status state-machine,
iv->isProducing();
iv->setQueuedCmdFinished();
mStat->rxSuccess++;
yield();
notify(RealTimeRunData_Debug, iv);
}
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;
}
/* uint16_t mParseAlarmLog(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];
}
*/
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;
HMRADIO *mRadio;
statistics_t *mStat;
uint8_t mMaxRetrans;
uint32_t *mTimestamp;
miPayload_t mPayload[MAX_NUM_INVERTERS];
bool mSerialDebug;
Inverter<> *mHighPrioIv;
alarmListenerType mCbAlarm;
payloadListenerType mCbMiPayload;
};
#endif /*__MI_PAYLOAD_H__*/

218
src/hm/nrfHal.h

@ -0,0 +1,218 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __NRF_HAL_H__
#define __NRF_HAL_H__
#pragma once
#include "../utils/spiPatcher.h"
#include <esp_rom_gpio.h>
#include <RF24_hal.h>
#define NRF_MAX_TRANSFER_SZ 64
#define NRF_DEFAULT_SPI_SPEED 10000000 // 10 MHz
class nrfHal: public RF24_hal, public SpiPatcherHandle {
public:
nrfHal() {
mSpiPatcher = SpiPatcher::getInstance(SPI2_HOST);
}
void patch() override {
esp_rom_gpio_connect_out_signal(mPinMosi, spi_periph_signal[mHostDevice].spid_out, false, false);
esp_rom_gpio_connect_in_signal(mPinMiso, spi_periph_signal[mHostDevice].spiq_in, false);
esp_rom_gpio_connect_out_signal(mPinClk, spi_periph_signal[mHostDevice].spiclk_out, false, false);
}
void unpatch() override {
esp_rom_gpio_connect_out_signal(mPinMosi, SIG_GPIO_OUT_IDX, false, false);
esp_rom_gpio_connect_in_signal(mPinMiso, GPIO_MATRIX_CONST_ZERO_INPUT, false);
esp_rom_gpio_connect_out_signal(mPinClk, SIG_GPIO_OUT_IDX, false, false);
}
void init(int8_t mosi, int8_t miso, int8_t sclk, int8_t cs, int8_t en, int32_t speed = NRF_DEFAULT_SPI_SPEED) {
mPinMosi = static_cast<gpio_num_t>(mosi);
mPinMiso = static_cast<gpio_num_t>(miso);
mPinClk = static_cast<gpio_num_t>(sclk);
mPinCs = static_cast<gpio_num_t>(cs);
mPinEn = static_cast<gpio_num_t>(en);
mSpiSpeed = speed;
mHostDevice = mSpiPatcher->getDevice();
gpio_reset_pin(mPinMosi);
gpio_set_direction(mPinMosi, GPIO_MODE_OUTPUT);
gpio_set_level(mPinMosi, 1);
gpio_reset_pin(mPinMiso);
gpio_set_direction(mPinMiso, GPIO_MODE_INPUT);
gpio_reset_pin(mPinClk);
gpio_set_direction(mPinClk, GPIO_MODE_OUTPUT);
gpio_set_level(mPinClk, 0);
gpio_reset_pin(mPinCs);
spi_device_interface_config_t devcfg = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.clock_speed_hz = mSpiSpeed,
.input_delay_ns = 0,
.spics_io_num = mPinCs,
.flags = 0,
.queue_size = 1,
.pre_cb = nullptr,
.post_cb = nullptr
};
ESP_ERROR_CHECK(spi_bus_add_device(mHostDevice, &devcfg, &spi));
gpio_reset_pin(mPinEn);
gpio_set_direction(mPinEn, GPIO_MODE_OUTPUT);
gpio_set_level(mPinEn, 0);
}
bool begin() override {
return true;
}
void end() override {}
void ce(bool level) override {
gpio_set_level(mPinEn, level);
}
uint8_t write(uint8_t cmd, const uint8_t* buf, uint8_t len) override {
uint8_t data[NRF_MAX_TRANSFER_SZ];
data[0] = cmd;
std::copy(&buf[0], &buf[len], &data[1]);
request_spi();
size_t spiLen = (static_cast<size_t>(len) + 1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
release_spi();
return data[0]; // status
}
uint8_t write(uint8_t cmd, const uint8_t* buf, uint8_t data_len, uint8_t blank_len) override {
uint8_t data[NRF_MAX_TRANSFER_SZ];
data[0] = cmd;
memset(&data[1], 0, (NRF_MAX_TRANSFER_SZ-1));
std::copy(&buf[0], &buf[data_len], &data[1]);
request_spi();
size_t spiLen = (static_cast<size_t>(data_len) + static_cast<size_t>(blank_len) + 1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
release_spi();
return data[0]; // status
}
uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t len) override {
uint8_t data[NRF_MAX_TRANSFER_SZ];
data[0] = cmd;
memset(&data[1], 0xff, len);
request_spi();
size_t spiLen = (static_cast<size_t>(len) + 1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
release_spi();
std::copy(&data[1], &data[len+1], buf);
return data[0]; // status
}
uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t data_len, uint8_t blank_len) override {
uint8_t data[NRF_MAX_TRANSFER_SZ];
data[0] = cmd;
memset(&data[1], 0xff, (data_len + blank_len));
request_spi();
size_t spiLen = (static_cast<size_t>(data_len) + static_cast<size_t>(blank_len) + 1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
release_spi();
std::copy(&data[1], &data[data_len+1], buf);
return data[0]; // status
}
private:
inline void request_spi() {
mSpiPatcher->request(this);
}
inline void release_spi() {
mSpiPatcher->release();
}
private:
gpio_num_t mPinMosi = GPIO_NUM_NC;
gpio_num_t mPinMiso = GPIO_NUM_NC;
gpio_num_t mPinClk = GPIO_NUM_NC;
gpio_num_t mPinCs = GPIO_NUM_NC;
gpio_num_t mPinEn = GPIO_NUM_NC;
int32_t mSpiSpeed = NRF_DEFAULT_SPI_SPEED;
spi_host_device_t mHostDevice;
spi_device_handle_t spi;
SpiPatcher *mSpiPatcher;
};
#endif /*__NRF_HAL_H__*/

117
src/hm/radio.h

@ -0,0 +1,117 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __RADIO_H__
#define __RADIO_H__
#define TX_REQ_INFO 0x15
#define TX_REQ_DEVCONTROL 0x51
#define ALL_FRAMES 0x80
#define SINGLE_FRAME 0x81
#include "../utils/dbg.h"
#include "../utils/crc.h"
// forward declaration of class
template <class REC_TYP=float>
class Inverter;
// abstract radio interface
class Radio {
public:
virtual void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) = 0;
virtual bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) { return true; }
virtual bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) { return true; }
virtual bool isChipConnected(void) { return false; }
virtual void loop(void) {};
void handleIntr(void) {
mIrqRcvd = true;
}
void sendCmdPacket(Inverter<> *iv, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) {
initPacket(getIvId(iv), mid, pid);
sendPacket(iv, 10, isRetransmit, appendCrc16);
}
void prepareDevInformCmd(Inverter<> *iv, 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.
if(IV_MI == getIvGen(iv)) {
DPRINT(DBG_DEBUG, F("legacy cmd 0x"));
DPRINTLN(DBG_DEBUG,String(cmd, HEX));
sendCmdPacket(iv, cmd, cmd, false, false);
return;
}
if(*mSerialDebug) {
DPRINT(DBG_DEBUG, F("prepareDevInformCmd 0x"));
DPRINTLN(DBG_DEBUG,String(cmd, HEX));
}
initPacket(getIvId(iv), reqfld, ALL_FRAMES);
mTxBuf[10] = cmd;
CP_U32_LittleEndian(&mTxBuf[12], ts);
if (cmd == AlarmData ) { //cmd == RealTimeRunData_Debug ||
mTxBuf[18] = (alarmMesId >> 8) & 0xff;
mTxBuf[19] = (alarmMesId ) & 0xff;
}
sendPacket(iv, 24, isRetransmit);
}
uint32_t getDTUSn(void) {
return mDtuSn;
}
public:
std::queue<packet_t> mBufCtrl;
protected:
virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0;
virtual uint64_t getIvId(Inverter<> *iv) = 0;
virtual uint8_t getIvGen(Inverter<> *iv) = 0;
void initPacket(uint64_t ivId, uint8_t mid, uint8_t pid) {
mTxBuf[0] = mid;
CP_U32_BigEndian(&mTxBuf[1], ivId >> 8);
CP_U32_LittleEndian(&mTxBuf[5], mDtuSn);
mTxBuf[9] = pid;
memset(&mTxBuf[10], 0x00, (MAX_RF_PAYLOAD_SIZE-10));
}
void updateCrcs(uint8_t *len, bool appendCrc16=true) {
// append crc's
if (appendCrc16 && ((*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)++;
}
void generateDtuSn(void) {
uint32_t chipID = 0;
#ifdef ESP32
uint64_t MAC = ESP.getEfuseMac();
chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF);
#else
chipID = ESP.getChipId();
#endif
mDtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
for(int i = 0; i < 7; i++) {
mDtuSn |= (chipID % 10) << (i * 4);
chipID /= 10;
}
}
uint32_t mDtuSn;
volatile bool mIrqRcvd;
bool *mSerialDebug, *mPrivacyMode, *mPrintWholeTrace;
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
};
#endif /*__RADIO_H__*/

110
src/hms/cmt2300a.h

@ -6,16 +6,11 @@
#ifndef __CMT2300A_H__ #ifndef __CMT2300A_H__
#define __CMT2300A_H__ #define __CMT2300A_H__
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
#include "cmtHal.h"
#else
#include "esp32_3wSpi.h" #include "esp32_3wSpi.h"
#endif
#define WORK_FREQ_KHZ 865000 // disired work frequency between DTU and
// inverter in kHz
#define HOY_BASE_FREQ_KHZ 860000 // in kHz
#define HOY_MAX_FREQ_KHZ 923500 // 0xFE * 250kHz + Base_freq
#define HOY_BOOT_FREQ_KHZ 868000 // Hoymiles boot/init frequency after power up inverter
#define FREQ_STEP_KHZ 250 // channel step size in kHz
#define FREQ_WARN_MIN_KHZ 863000 // for EU 863 - 870 MHz is allowed
#define FREQ_WARN_MAX_KHZ 870000 // for EU 863 - 870 MHz is allowed
// detailed register infos from AN142_CMT2300AW_Quick_Start_Guide-Rev0.8.pdf // detailed register infos from AN142_CMT2300AW_Quick_Start_Guide-Rev0.8.pdf
@ -25,6 +20,10 @@
#define CMT2300A_MASK_CHIP_MODE_STA 0x0F #define CMT2300A_MASK_CHIP_MODE_STA 0x0F
#define CMT2300A_CUS_CMT10 0x09 #define CMT2300A_CUS_CMT10 0x09
#define CMT2300A_CUS_TX5 0x59
#define CMT2300A_CUS_TX8 0x5C
#define CMT2300A_CUS_TX9 0x5D
#define CMT2300A_CUS_TX10 0x5E
#define CMT2300A_CUS_MODE_CTL 0x60 // [7] go_switch #define CMT2300A_CUS_MODE_CTL 0x60 // [7] go_switch
// [6] go_tx // [6] go_tx
@ -153,7 +152,43 @@
#define CMT2300A_MASK_TX_DONE_FLG 0x08 #define CMT2300A_MASK_TX_DONE_FLG 0x08
#define CMT2300A_MASK_PKT_OK_FLG 0x01 #define CMT2300A_MASK_PKT_OK_FLG 0x01
// default CMT paramters // this list and the TX5, TX10 registers were compiled from the output of
// HopeRF RFPDK Tool v1.54
static uint8_t paLevelList[31][2] PROGMEM = {
{0x17, 0x01}, // -10dBm
{0x1a, 0x01}, // -09dBm
{0x1d, 0x01}, // -08dBm
{0x21, 0x01}, // -07dBm
{0x25, 0x01}, // -06dBm
{0x29, 0x01}, // -05dBm
{0x2d, 0x01}, // -04dBm
{0x33, 0x01}, // -03dBm
{0x39, 0x02}, // -02dBm
{0x41, 0x02}, // -01dBm
{0x4b, 0x02}, // 00dBm
{0x56, 0x03}, // 01dBm
{0x63, 0x03}, // 02dBm
{0x71, 0x04}, // 03dBm
{0x80, 0x04}, // 04dBm
{0x22, 0x01}, // 05dBm
{0x27, 0x04}, // 06dBm
{0x2c, 0x05}, // 07dBm
{0x31, 0x06}, // 08dBm
{0x38, 0x06}, // 09dBm
{0x3f, 0x07}, // 10dBm
{0x48, 0x08}, // 11dBm
{0x52, 0x09}, // 12dBm
{0x5d, 0x0b}, // 13dBm
{0x6a, 0x0c}, // 14dBm
{0x79, 0x0d}, // 15dBm
{0x46, 0x10}, // 16dBm
{0x51, 0x10}, // 17dBm
{0x60, 0x12}, // 18dBm
{0x71, 0x14}, // 19dBm
{0x8c, 0x1c} // 20dBm
};
// default CMT parameters
static uint8_t cmtConfig[0x60] PROGMEM { static uint8_t cmtConfig[0x60] PROGMEM {
// 0x00 - 0x0f -- RSSI offset +- 0 and 13dBm // 0x00 - 0x0f -- RSSI offset +- 0 and 13dBm
0x00, 0x66, 0xEC, 0x1C, 0x70, 0x80, 0x14, 0x08, 0x00, 0x66, 0xEC, 0x1C, 0x70, 0x80, 0x14, 0x08,
@ -168,29 +203,22 @@ static uint8_t cmtConfig[0x60] PROGMEM {
0x10, 0x00, 0xB4, 0x00, 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0xB4, 0x00, 0x00, 0x01, 0x00, 0x00,
0x12, 0x1E, 0x00, 0xAA, 0x06, 0x00, 0x00, 0x00, 0x12, 0x1E, 0x00, 0xAA, 0x06, 0x00, 0x00, 0x00,
// 0x40 - 0x4f // 0x40 - 0x4f
0x00, 0x48, 0x5A, 0x48, 0x4D, 0x01, 0x1D, 0x00, 0x00, 0x48, 0x5A, 0x48, 0x4D, 0x01, 0x1F, 0x00,
0x00, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x60,
// 0x50 - 0x5f // 0x50 - 0x5f
0xFF, 0x00, 0x00, 0x1F, 0x10, 0x70, 0x4D, 0x06, 0xFF, 0x00, 0x00, 0x1F, 0x10, 0x70, 0x4D, 0x06,
0x00, 0x07, 0x50, 0x00, 0x42, 0x0C, 0x3F, 0x7F // - TX 13dBm 0x00, 0x07, 0x50, 0x00, 0x5D, 0x0B, 0x3F, 0x7F // - TX 13dBm
}; };
enum {CMT_SUCCESS = 0, CMT_ERR_SWITCH_STATE, CMT_ERR_TX_PENDING, CMT_FIFO_EMPTY, CMT_ERR_RX_IN_FIFO}; enum {CMT_SUCCESS = 0, CMT_ERR_SWITCH_STATE, CMT_ERR_TX_PENDING, CMT_FIFO_EMPTY, CMT_ERR_RX_IN_FIFO};
template<class SPI>
class Cmt2300a { class Cmt2300a {
typedef SPI SpiType;
public: public:
Cmt2300a() {} Cmt2300a() {}
void setup(uint8_t pinCsb, uint8_t pinFcsb) { void setup(uint8_t pinSclk, uint8_t pinSdio, uint8_t pinCsb, uint8_t pinFcsb) {
mSpi.setup(pinCsb, pinFcsb); mSpi.init(pinSdio, pinSclk, pinCsb, pinFcsb);
init();
}
void setup() {
mSpi.setup();
init(); init();
} }
@ -239,7 +267,7 @@ class Cmt2300a {
return CMT_SUCCESS; return CMT_SUCCESS;
} }
uint8_t getRx(uint8_t buf[], uint8_t len, int8_t *rssi) { uint8_t getRx(uint8_t buf[], uint8_t *rxLen, uint8_t maxlen, int8_t *rssi) {
if(mTxPending) if(mTxPending)
return CMT_ERR_TX_PENDING; return CMT_ERR_TX_PENDING;
@ -250,7 +278,7 @@ class Cmt2300a {
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return CMT_ERR_SWITCH_STATE; return CMT_ERR_SWITCH_STATE;
mSpi.readFifo(buf, len); mSpi.readFifo(buf, rxLen, maxlen);
*rssi = mSpi.readReg(CMT2300A_CUS_RSSI_DBM) - 128; *rssi = mSpi.readReg(CMT2300A_CUS_RSSI_DBM) - 128;
if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP)) if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP))
@ -315,6 +343,8 @@ class Cmt2300a {
mSpi.writeReg(CMT2300A_CUS_MODE_STA, 0x52); mSpi.writeReg(CMT2300A_CUS_MODE_STA, 0x52);
mSpi.writeReg(0x62, 0x20); mSpi.writeReg(0x62, 0x20);
if(mSpi.readReg(0x62) != 0x20)
return false; // not connected!
for(uint8_t i = 0; i < 0x60; i++) { for(uint8_t i = 0; i < 0x60; i++) {
mSpi.writeReg(i, cmtConfig[i]); mSpi.writeReg(i, cmtConfig[i]);
@ -360,7 +390,7 @@ class Cmt2300a {
inline uint8_t freq2Chan(const uint32_t freqKhz) { inline uint8_t freq2Chan(const uint32_t freqKhz) {
if((freqKhz % FREQ_STEP_KHZ) != 0) { if((freqKhz % FREQ_STEP_KHZ) != 0) {
DPRINT(DBG_WARN, F("swtich frequency to ")); DPRINT(DBG_WARN, F("switch frequency to "));
DBGPRINT(String(freqKhz)); DBGPRINT(String(freqKhz));
DBGPRINT(F("kHz not possible!")); DBGPRINT(F("kHz not possible!"));
return 0xff; // error return 0xff; // error
@ -373,7 +403,7 @@ class Cmt2300a {
return 0xff; // error return 0xff; // error
if((freqKhz < FREQ_WARN_MIN_KHZ) || (freqKhz > FREQ_WARN_MAX_KHZ)) if((freqKhz < FREQ_WARN_MIN_KHZ) || (freqKhz > FREQ_WARN_MAX_KHZ))
DPRINTLN(DBG_WARN, F("Disired frequency is out of EU legal range! (863 - 870MHz)")); DPRINTLN(DBG_WARN, F("Desired frequency is out of EU legal range! (863 - 870MHz)"));
return (freqKhz - HOY_BASE_FREQ_KHZ) / FREQ_STEP_KHZ; return (freqKhz - HOY_BASE_FREQ_KHZ) / FREQ_STEP_KHZ;
} }
@ -389,6 +419,30 @@ class Cmt2300a {
return HOY_BASE_FREQ_KHZ + (mCurCh * FREQ_STEP_KHZ); return HOY_BASE_FREQ_KHZ + (mCurCh * FREQ_STEP_KHZ);
} }
uint8_t getCurrentChannel(void) {
return mCurCh;
}
void setPaLevel(int8_t level) {
if(level < -10)
level = -10;
if(level > 20)
level = 20;
level += 10; // unsigned value
if(level >= 15) {
mSpi.writeReg(CMT2300A_CUS_TX5, 0x07);
mSpi.writeReg(CMT2300A_CUS_TX10, 0x3f);
} else {
mSpi.writeReg(CMT2300A_CUS_TX5, 0x13);
mSpi.writeReg(CMT2300A_CUS_TX10, 0x18);
}
mSpi.writeReg(CMT2300A_CUS_TX8, paLevelList[level][0]);
mSpi.writeReg(CMT2300A_CUS_TX9, paLevelList[level][1]);
}
private: private:
void init() { void init() {
mTxPending = false; mTxPending = false;
@ -426,7 +480,11 @@ class Cmt2300a {
return mSpi.readReg(CMT2300A_CUS_MODE_STA) & CMT2300A_MASK_CHIP_MODE_STA; return mSpi.readReg(CMT2300A_CUS_MODE_STA) & CMT2300A_MASK_CHIP_MODE_STA;
} }
SpiType mSpi; #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
cmtHal mSpi;
#else
esp32_3wSpi mSpi;
#endif
uint8_t mCnt; uint8_t mCnt;
bool mTxPending; bool mTxPending;
bool mInRxMode; bool mInRxMode;

196
src/hms/cmtHal.h

@ -0,0 +1,196 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __CMT_HAL_H__
#define __CMT_HAL_H__
#pragma once
#include "../utils/spiPatcher.h"
#include <driver/gpio.h>
#define CMT_DEFAULT_SPI_SPEED 4000000 // 4 MHz
class cmtHal : public SpiPatcherHandle {
public:
cmtHal() {
mSpiPatcher = SpiPatcher::getInstance(SPI2_HOST);
}
void patch() override {
esp_rom_gpio_connect_out_signal(mPinSdio, spi_periph_signal[mHostDevice].spid_out, false, false);
esp_rom_gpio_connect_in_signal(mPinSdio, spi_periph_signal[mHostDevice].spid_in, false);
esp_rom_gpio_connect_out_signal(mPinClk, spi_periph_signal[mHostDevice].spiclk_out, false, false);
}
void unpatch() override {
esp_rom_gpio_connect_out_signal(mPinSdio, SIG_GPIO_OUT_IDX, false, false);
esp_rom_gpio_connect_in_signal(mPinSdio, GPIO_MATRIX_CONST_ZERO_INPUT, false);
esp_rom_gpio_connect_out_signal(mPinClk, SIG_GPIO_OUT_IDX, false, false);
}
void init(int8_t sdio, int8_t clk, int8_t cs, int8_t fcs, int32_t speed = CMT_DEFAULT_SPI_SPEED) {
mPinSdio = static_cast<gpio_num_t>(sdio);
mPinClk = static_cast<gpio_num_t>(clk);
mPinCs = static_cast<gpio_num_t>(cs);
mPinFcs = static_cast<gpio_num_t>(fcs);
mSpiSpeed = speed;
mHostDevice = mSpiPatcher->getDevice();
gpio_reset_pin(mPinSdio);
gpio_set_direction(mPinSdio, GPIO_MODE_INPUT_OUTPUT);
gpio_set_level(mPinSdio, 1);
gpio_reset_pin(mPinClk);
gpio_set_direction(mPinClk, GPIO_MODE_OUTPUT);
gpio_set_level(mPinClk, 0);
gpio_reset_pin(mPinCs);
spi_device_interface_config_t devcfg_reg = {
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 1,
.cs_ena_posttrans = 1,
.clock_speed_hz = mSpiSpeed,
.input_delay_ns = 0,
.spics_io_num = mPinCs,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = nullptr,
.post_cb = nullptr
};
ESP_ERROR_CHECK(spi_bus_add_device(mHostDevice, &devcfg_reg, &spi_reg));
gpio_reset_pin(mPinFcs);
spi_device_interface_config_t devcfg_fifo = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 2,
.cs_ena_posttrans = static_cast<uint8_t>(2 * mSpiSpeed / 1000000), // >2 us
.clock_speed_hz = mSpiSpeed,
.input_delay_ns = 0,
.spics_io_num = mPinFcs,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = nullptr,
.post_cb = nullptr
};
ESP_ERROR_CHECK(spi_bus_add_device(mHostDevice, &devcfg_fifo, &spi_fifo));
}
uint8_t readReg(uint8_t addr) {
uint8_t data;
request_spi();
spi_transaction_t t = {
.flags = 0,
.cmd = 1,
.addr = addr,
.length = 0,
.rxlength = 8,
.user = NULL,
.tx_buffer = NULL,
.rx_buffer = &data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
release_spi();
return data;
}
void writeReg(uint8_t addr, uint8_t data) {
request_spi();
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = addr,
.length = 8,
.rxlength = 0,
.user = NULL,
.tx_buffer = &data,
.rx_buffer = NULL
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
release_spi();
}
void readFifo(uint8_t buf[], uint8_t *len, uint8_t maxlen) {
request_spi();
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = 0,
.rxlength = 8,
.user = NULL,
.tx_buffer = NULL,
.rx_buffer = NULL
};
for (uint8_t i = 0; i < maxlen; i++) {
if(0 == i)
t.rx_buffer = len;
else
t.rx_buffer = buf + i - 1;
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t));
}
release_spi();
}
void writeFifo(const uint8_t buf[], uint16_t len) {
request_spi();
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = 8,
.rxlength = 0,
.user = NULL,
.tx_buffer = NULL,
.rx_buffer = NULL
};
for (uint16_t i = 0; i < len; i++) {
t.tx_buffer = buf + i;
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t));
}
release_spi();
}
private:
inline void request_spi() {
mSpiPatcher->request(this);
}
inline void release_spi() {
mSpiPatcher->release();
}
private:
gpio_num_t mPinSdio = GPIO_NUM_NC;
gpio_num_t mPinClk = GPIO_NUM_NC;
gpio_num_t mPinCs = GPIO_NUM_NC;
gpio_num_t mPinFcs = GPIO_NUM_NC;
int32_t mSpiSpeed = CMT_DEFAULT_SPI_SPEED;
spi_host_device_t mHostDevice;
spi_device_handle_t spi_reg, spi_fifo;
SpiPatcher *mSpiPatcher;
};
#endif /*__CMT_HAL_H__*/

26
src/hms/esp32_3wSpi.h

@ -11,14 +11,6 @@
#include "driver/spi_master.h" #include "driver/spi_master.h"
#include "esp_rom_gpio.h" // for esp_rom_gpio_connect_out_signal #include "esp_rom_gpio.h" // for esp_rom_gpio_connect_out_signal
#if CONFIG_IDF_TARGET_ESP32S3
#define CLK_PIN 6
#define MOSI_PIN 5
#else
#define CLK_PIN 18
#define MOSI_PIN 23
#endif
#define SPI_CLK 1 * 1000 * 1000 // 1MHz #define SPI_CLK 1 * 1000 * 1000 // 1MHz
#define SPI_PARAM_LOCK() \ #define SPI_PARAM_LOCK() \
@ -31,19 +23,18 @@
// it is simply the first externally usable hardware SPI master controller // it is simply the first externally usable hardware SPI master controller
#define SPI_CMT SPI2_HOST #define SPI_CMT SPI2_HOST
template<uint8_t CSB_PIN=5, uint8_t FCSB_PIN=4> //, uint8_t GPIO3_PIN=15>
class esp32_3wSpi { class esp32_3wSpi {
public: public:
esp32_3wSpi() { esp32_3wSpi() {
mInitialized = false; mInitialized = false;
} }
void setup(uint8_t pinCsb = CSB_PIN, uint8_t pinFcsb = FCSB_PIN) { //, uint8_t pinGpio3 = GPIO3_PIN) { void init(uint8_t pinSdio = DEF_CMT_SDIO, uint8_t pinSclk = DEF_CMT_SCLK, uint8_t pinCsb = DEF_CMT_CSB, uint8_t pinFcsb = DEF_CMT_FCSB) {
paramLock = xSemaphoreCreateMutex(); paramLock = xSemaphoreCreateMutex();
spi_bus_config_t buscfg = { spi_bus_config_t buscfg = {
.mosi_io_num = MOSI_PIN, .mosi_io_num = pinSdio,
.miso_io_num = -1, // single wire MOSI/MISO .miso_io_num = -1, // single wire MOSI/MISO
.sclk_io_num = CLK_PIN, .sclk_io_num = pinSclk,
.quadwp_io_num = -1, .quadwp_io_num = -1,
.quadhd_io_num = -1, .quadhd_io_num = -1,
.max_transfer_sz = 32, .max_transfer_sz = 32,
@ -83,7 +74,7 @@ class esp32_3wSpi {
}; };
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo)); ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo));
esp_rom_gpio_connect_out_signal(MOSI_PIN, spi_periph_signal[SPI_CMT].spid_out, true, false); esp_rom_gpio_connect_out_signal(pinSdio, spi_periph_signal[SPI_CMT].spid_out, true, false);
delay(100); delay(100);
//pinMode(pinGpio3, INPUT); //pinMode(pinGpio3, INPUT);
@ -150,7 +141,7 @@ class esp32_3wSpi {
SPI_PARAM_UNLOCK(); SPI_PARAM_UNLOCK();
} }
void readFifo(uint8_t buf[], uint8_t len) { void readFifo(uint8_t buf[], uint8_t *len, uint8_t maxlen) {
if(!mInitialized) if(!mInitialized)
return; return;
uint8_t rx_data; uint8_t rx_data;
@ -163,10 +154,13 @@ class esp32_3wSpi {
}; };
SPI_PARAM_LOCK(); SPI_PARAM_LOCK();
for(uint8_t i = 0; i < len; i++) { for(uint8_t i = 0; i < maxlen; i++) {
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t));
delayMicroseconds(4); // > 4 us delayMicroseconds(4); // > 4 us
buf[i] = rx_data; if(0 == i)
*len = rx_data;
else
buf[i-1] = rx_data;
} }
SPI_PARAM_UNLOCK(); SPI_PARAM_UNLOCK();
} }

406
src/hms/hmsPayload.h

@ -1,406 +0,0 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HMS_PAYLOAD_H__
#define __HMS_PAYLOAD_H__
#include "../utils/dbg.h"
#include "../utils/crc.h"
#include "../config/config.h"
#include <Arduino.h>
#define HMS_TIMEOUT_SEC 30 // 30s * 1000
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;
} hmsPayload_t;
typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType;
typedef std::function<void(Inverter<> *)> alarmListenerType;
template<class HMSYSTEM, class RADIO>
class HmsPayload {
public:
HmsPayload() {}
void setup(IApp *app, HMSYSTEM *sys, RADIO *radio, statistics_t *stat, uint8_t maxRetransmits, uint32_t *timestamp) {
mApp = app;
mSys = sys;
mRadio = radio;
mStat = stat;
mMaxRetrans = maxRetransmits;
mTimestamp = timestamp;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
reset(i);
mIvCmd56Cnt[i] = 0;
}
mSerialDebug = false;
mHighPrioIv = NULL;
mCbAlarm = NULL;
mCbPayload = NULL;
//mLastRx = 0;
}
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 ivSendHighPrio(Inverter<> *iv) {
mHighPrioIv = iv;
}
void ivSend(Inverter<> *iv, bool highPrio = false) {
if ((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) // only process HMS inverters
return;
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"));
/*if (mSerialDebug) {
DPRINT(DBG_INFO, F("(#"));
DBGPRINT(String(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));
}
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
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]));
}
iv->powerLimitAck = false;
mRadio->sendControlPacket(&iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false);
mPayload[iv->id].txCmd = iv->devControlCmd;
//iv->clearCmdQueue();
//iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
} else if(((rec->ts + HMS_TIMEOUT_SEC) < *mTimestamp) && (mIvCmd56Cnt[iv->id] < 3)) {
mRadio->switchFrequency(&iv->radioId.u64, HOY_BOOT_FREQ_KHZ, WORK_FREQ_KHZ);
mIvCmd56Cnt[iv->id]++;
} else {
if(++mIvCmd56Cnt[iv->id] == 10)
mIvCmd56Cnt[iv->id] = 0;
uint8_t cmd = iv->getQueuedCmd();
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("prepareDevInformCmd 0x"));
DBGHEXLN(cmd);
mRadio->prepareDevInformCmd(&iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false);
mPayload[iv->id].txCmd = cmd;
}
}
void add(Inverter<> *iv, hmsPacket_t *p) {
if (p->data[1] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
mPayload[iv->id].txId = p->data[1];
DPRINTLN(DBG_DEBUG, F("Response from info request received"));
uint8_t *pid = &p->data[10];
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) < MAX_PAYLOAD_ENTRIES) {
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->data[11], p->data[0] - 11);
mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->data[0] -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->data[1] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received"));
mPayload[iv->id].txId = p->data[1];
iv->clearDevControlRequest();
if ((p->data[13] == ActivePowerContr) && (p->data[14] == 0x00)) {
bool ok = true;
if((p->data[11] == 0x00) && (p->data[12] == 0x00)) {
mApp->setMqttPowerLimitAck(iv);
iv->powerLimitAck = true;
} 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<InfoCommand>(SystemConfigPara); // read back power limit
if(mHighPrioIv == NULL) // do it immediately if possible
mHighPrioIv = iv;
}
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_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) // only process HMS 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) {
DPRINTLN(DBG_INFO, F("retransmit power limit"));
mRadio->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));
//mRadio->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(DBG_WARN, F("Frame "));
DBGPRINT(String(i + 1));
DBGPRINTLN(F(" missing: Request Retransmit"));
//mRadio->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(DBG_INFO, F("(#"));
DBGPRINT(String(iv->id));
DBGPRINT(F(") prepareDevInformCmd 0x"));
DBGPRINTLN(String(mPayload[iv->id].txCmd, HEX));
mRadio->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"));
DBGPRINTLN(String(mPayload[iv->id].txCmd, HEX));
DPRINT(DBG_INFO, F("procPyld: txid: 0x"));
DBGPRINTLN(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[150];
uint8_t payloadLen = 0;
memset(payload, 0, 150);
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId); i++) {
if((mPayload[iv->id].len[i] + payloadLen) > 150) {
DPRINTLN(DBG_ERROR, F("payload buffer to small!"));
break;
}
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("): "));
ah::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, iv);
if(AlarmData == mPayload[iv->id].txCmd) {
uint8_t i = 0;
uint32_t start, end;
while(1) {
if(0 == iv->parseAlarmLog(i++, payload, payloadLen))
break;
if (NULL != mCbAlarm)
(mCbAlarm)(iv);
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, Inverter<> *iv) {
if(NULL != mCbPayload)
(mCbPayload)(val, iv);
}
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] - 1, 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], 0, sizeof(hmsPayload_t));
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;
RADIO *mRadio;
statistics_t *mStat;
uint8_t mMaxRetrans;
uint32_t *mTimestamp;
//uint32_t mLastRx;
hmsPayload_t mPayload[MAX_NUM_INVERTERS];
uint8_t mIvCmd56Cnt[MAX_NUM_INVERTERS];
bool mSerialDebug;
Inverter<> *mHighPrioIv;
alarmListenerType mCbAlarm;
payloadListenerType mCbPayload;
};
#endif /*__HMS_PAYLOAD_H__*/

171
src/hms/hmsRadio.h

@ -6,67 +6,45 @@
#ifndef __HMS_RADIO_H__ #ifndef __HMS_RADIO_H__
#define __HMS_RADIO_H__ #define __HMS_RADIO_H__
#include "../utils/dbg.h"
#include "cmt2300a.h" #include "cmt2300a.h"
#include "../hm/radio.h"
typedef struct { template<uint32_t DTU_SN = 0x81001765>
int8_t rssi; class CmtRadio : public Radio {
uint8_t data[28]; typedef Cmt2300a CmtType;
} hmsPacket_t;
#define U32_B3(val) ((uint8_t)((val >> 24) & 0xff))
#define U32_B2(val) ((uint8_t)((val >> 16) & 0xff))
#define U32_B1(val) ((uint8_t)((val >> 8) & 0xff))
#define U32_B0(val) ((uint8_t)((val ) & 0xff))
template<class SPI, uint32_t DTU_SN = 0x81001765>
class CmtRadio {
typedef SPI SpiType;
typedef Cmt2300a<SpiType> CmtType;
public: public:
CmtRadio() { CmtRadio() {
mDtuSn = DTU_SN; mDtuSn = DTU_SN;
mCmtAvail = false;
} }
void setup(uint8_t pinCsb, uint8_t pinFcsb, bool genDtuSn = true) { void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t pinSclk, uint8_t pinSdio, uint8_t pinCsb, uint8_t pinFcsb, bool genDtuSn = true) {
mCmt.setup(pinCsb, pinFcsb); mCmt.setup(pinSclk, pinSdio, pinCsb, pinFcsb);
reset(genDtuSn);
}
void setup(bool genDtuSn = true) {
mCmt.setup();
reset(genDtuSn); reset(genDtuSn);
mPrivacyMode = privacyMode;
mSerialDebug = serialDebug;
mPrintWholeTrace = printWholeTrace;
} }
bool loop() { void loop() {
mCmt.loop(); mCmt.loop();
if((!mIrqRcvd) && (!mRqstGetRx)) if((!mIrqRcvd) && (!mRqstGetRx))
return false; return;
getRx(); getRx();
if(CMT_SUCCESS == mCmt.goRx()) { if(CMT_SUCCESS == mCmt.goRx()) {
mIrqRcvd = false; mIrqRcvd = false;
mRqstGetRx = false; mRqstGetRx = false;
return true;
} else
return false;
}
void tickSecond() {
} }
void handleIntr(void) {
mIrqRcvd = true;
} }
void enableDebug() { bool isChipConnected(void) {
mSerialDebug = true; return mCmtAvail;
} }
void sendControlPacket(const uint64_t *ivId, uint8_t cmd, uint16_t *data, bool isRetransmit) { void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) {
DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); DPRINT(DBG_INFO, F("sendControlPacket cmd: "));
DBGHEXLN(cmd); DBGHEXLN(cmd);
initPacket(ivId, TX_REQ_DEVCONTROL, SINGLE_FRAME); initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME);
uint8_t cnt = 10; uint8_t cnt = 10;
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
@ -78,84 +56,89 @@ class CmtRadio {
mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
} }
sendPacket(cnt, isRetransmit); sendPacket(iv, cnt, isRetransmit);
} }
bool switchFrequency(const uint64_t *ivId, uint32_t fromkHz, uint32_t tokHz) { bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) {
uint8_t fromCh = mCmt.freq2Chan(fromkHz); uint8_t fromCh = mCmt.freq2Chan(fromkHz);
uint8_t toCh = mCmt.freq2Chan(tokHz); uint8_t toCh = mCmt.freq2Chan(tokHz);
return switchFrequencyCh(iv, fromCh, toCh);
}
bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) {
if((0xff == fromCh) || (0xff == toCh)) if((0xff == fromCh) || (0xff == toCh))
return false; return false;
mCmt.switchChannel(fromCh); mCmt.switchChannel(fromCh);
sendSwitchChCmd(ivId, toCh); sendSwitchChCmd(iv, toCh);
mCmt.switchChannel(toCh); mCmt.switchChannel(toCh);
return true; return true;
} }
void prepareDevInformCmd(const uint64_t *ivId, 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. private:
initPacket(ivId, reqfld, ALL_FRAMES);
mTxBuf[10] = cmd;
CP_U32_LittleEndian(&mTxBuf[12], ts);
/*if (cmd == AlarmData ) { //cmd == RealTimeRunData_Debug ||
mTxBuf[18] = (alarmMesId >> 8) & 0xff;
mTxBuf[19] = (alarmMesId ) & 0xff;
}*/
sendPacket(24, isRetransmit);
}
void sendPacket(uint8_t len, bool isRetransmit) { void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) {
if (len > 14) { // inverters have maybe different settings regarding frequency
uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); if(mCmt.getCurrentChannel() != iv->config->frequency)
mTxBuf[len++] = (crc >> 8) & 0xff; mCmt.switchChannel(iv->config->frequency);
mTxBuf[len++] = (crc ) & 0xff;
}
mTxBuf[len] = ah::crc8(mTxBuf, len);
len++;
if(mSerialDebug) { updateCrcs(&len, appendCrc16);
DPRINT(DBG_INFO, F("TX "));
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("TX "));
DBGPRINT(String(mCmt.getFreqKhz()/1000.0f)); DBGPRINT(String(mCmt.getFreqKhz()/1000.0f));
DBGPRINT(F("Mhz | ")); DBGPRINT(F("Mhz | "));
if(*mPrintWholeTrace) {
if(*mPrivacyMode)
ah::dumpBuf(mTxBuf, len, 1, 4);
else
ah::dumpBuf(mTxBuf, len); ah::dumpBuf(mTxBuf, len);
} else {
DHEX(mTxBuf[0]);
DBGPRINT(F(" "));
DHEX(mTxBuf[10]);
DBGPRINT(F(" "));
DBGHEXLN(mTxBuf[9]);
}
} }
uint8_t status = mCmt.tx(mTxBuf, len); uint8_t status = mCmt.tx(mTxBuf, len);
mMillis = millis();
if(CMT_SUCCESS != status) { if(CMT_SUCCESS != status) {
DPRINT(DBG_WARN, F("CMT TX failed, code: ")); DPRINT(DBG_WARN, F("CMT TX failed, code: "));
DBGPRINTLN(String(status)); DBGPRINTLN(String(status));
if(CMT_ERR_RX_IN_FIFO == status) if(CMT_ERR_RX_IN_FIFO == status)
mIrqRcvd = true; mIrqRcvd = true;
} }
iv->mDtuTxCnt++;
}
if(isRetransmit) uint64_t getIvId(Inverter<> *iv) {
mRetransmits++; return iv->radioId.u64;
else
mSendCnt++;
} }
uint32_t mSendCnt; uint8_t getIvGen(Inverter<> *iv) {
uint32_t mRetransmits; return iv->ivGen;
std::queue<hmsPacket_t> mBufCtrl; }
private:
inline void reset(bool genDtuSn) { inline void reset(bool genDtuSn) {
if(genDtuSn) if(genDtuSn)
generateDtuSn(); generateDtuSn();
if(!mCmt.reset()) if(!mCmt.reset()) {
mCmtAvail = false;
DPRINTLN(DBG_WARN, F("Initializing CMT2300A failed!")); DPRINTLN(DBG_WARN, F("Initializing CMT2300A failed!"));
else } else {
mCmtAvail = true;
mCmt.goRx(); mCmt.goRx();
}
mSendCnt = 0;
mRetransmits = 0;
mSerialDebug = false;
mIrqRcvd = false; mIrqRcvd = false;
mRqstGetRx = false; mRqstGetRx = false;
} }
inline void sendSwitchChCmd(const uint64_t *ivId, uint8_t ch) { inline void sendSwitchChCmd(Inverter<> *iv, uint8_t ch) {
/** ch: /** ch:
* 0x00: 860.00 MHz * 0x00: 860.00 MHz
* 0x01: 860.25 MHz * 0x01: 860.25 MHz
@ -165,49 +148,27 @@ class CmtRadio {
* ... * ...
* 0x28: 870.00 MHz * 0x28: 870.00 MHz
* */ * */
initPacket(ivId, 0x56, 0x02); initPacket(iv->radioId.u64, 0x56, 0x02);
mTxBuf[10] = 0x15; mTxBuf[10] = 0x15;
mTxBuf[11] = 0x21; mTxBuf[11] = 0x21;
mTxBuf[12] = ch; mTxBuf[12] = ch;
mTxBuf[13] = 0x14; mTxBuf[13] = 0x14;
sendPacket(14, false); sendPacket(iv, 14, false);
mRqstGetRx = true; mRqstGetRx = true;
} }
void initPacket(const uint64_t *ivId, uint8_t mid, uint8_t pid) {
mTxBuf[0] = mid;
CP_U32_BigEndian(&mTxBuf[1], (*ivId) >> 8);
CP_U32_LittleEndian(&mTxBuf[5], mDtuSn);
mTxBuf[9] = pid;
memset(&mTxBuf[10], 0x00, 17);
}
inline void generateDtuSn(void) {
uint32_t chipID = 0;
#ifdef ESP32
uint64_t MAC = ESP.getEfuseMac();
chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF);
#endif
mDtuSn = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
for(int i = 0; i < 7; i++) {
mDtuSn |= (chipID % 10) << (i * 4);
chipID /= 10;
}
}
inline void getRx(void) { inline void getRx(void) {
hmsPacket_t p; packet_t p;
uint8_t status = mCmt.getRx(p.data, 28, &p.rssi); p.millis = millis() - mMillis;
uint8_t status = mCmt.getRx(p.packet, &p.len, 28, &p.rssi);
if(CMT_SUCCESS == status) if(CMT_SUCCESS == status)
mBufCtrl.push(p); mBufCtrl.push(p);
} }
CmtType mCmt; CmtType mCmt;
uint32_t mDtuSn;
uint8_t mTxBuf[27];
bool mSerialDebug;
bool mIrqRcvd;
bool mRqstGetRx; bool mRqstGetRx;
bool mCmtAvail;
uint32_t mMillis;
}; };
#endif /*__HMS_RADIO_H__*/ #endif /*__HMS_RADIO_H__*/

260
src/platformio.ini

@ -20,168 +20,220 @@ monitor_speed = 115200
extra_scripts = extra_scripts =
pre:../scripts/auto_firmware_version.py pre:../scripts/auto_firmware_version.py
pre:web/html/convert.py pre:../scripts/convertHtml.py
pre:../scripts/applyPatches.py pre:../scripts/applyPatches.py
lib_deps = lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24 @ 1.4.7 nrf24/RF24 @ 1.4.8
paulstoffregen/Time @ ^1.6.1 paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.4.4 https://github.com/bertmelis/espMqttClient#v1.5.0
bblanchon/ArduinoJson @ ^6.21.3 bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4 https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.34.17 olikraus/U8g2 @ ^2.35.7
zinggjm/GxEPD2 @ ^1.5.2 https://github.com/zinggjm/GxEPD2 @ ^1.5.2
build_flags =
-std=c++17
-std=gnu++17
build_unflags =
-std=gnu++11
[env:esp8266-release] [env:esp8266]
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = build_flags = ${env.build_flags}
-D RELEASE
-std=gnu++17
-DEMC_MIN_FREE_MEMORY=4096 -DEMC_MIN_FREE_MEMORY=4096
;-Wl,-Map,output.map ;-Wl,-Map,output.map
monitor_filters = monitor_filters =
esp8266_exception_decoder esp8266_exception_decoder
[env:esp8266-release-prometheus] [env:esp8266-prometheus]
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = build_flags = ${env.build_flags}
-D RELEASE
-std=gnu++17
-DENABLE_PROMETHEUS_EP -DENABLE_PROMETHEUS_EP
-DEMC_MIN_FREE_MEMORY=4096 -DEMC_MIN_FREE_MEMORY=4096
monitor_filters = monitor_filters =
esp8266_exception_decoder esp8266_exception_decoder
[env:esp8266-debug] [env:esp8285]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags =
-DDEBUG_LEVEL=DBG_DEBUG
-std=gnu++17
-DEMC_MIN_FREE_MEMORY=4096
-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 =
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]
platform = espressif8266
board = esp8285
board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L
build_flags =
-D RELEASE
-std=gnu++17
-DEMC_MIN_FREE_MEMORY=4096
monitor_filters =
time ; Add timestamp with milliseconds for each new line
[env:esp8285-debug]
platform = espressif8266 platform = espressif8266
board = esp8285 board = esp8285
board_build.ldscript = eagle.flash.1m64.ld board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = build_flags = ${env.build_flags}
-DDEBUG_LEVEL=DBG_DEBUG
-DEMC_MIN_FREE_MEMORY=4096 -DEMC_MIN_FREE_MEMORY=4096
-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 = monitor_filters =
time ; Add timestamp with milliseconds for each new line esp8266_exception_decoder
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
[env:esp32-wroom32-release] [env:esp32-wroom32]
platform = espressif32@6.1.0 platform = espressif32@6.4.0
board = lolin_d32 board = lolin_d32
build_flags = build_flags = ${env.build_flags}
-D RELEASE -DUSE_HSPI_FOR_EPD
-std=gnu++17
build_unflags = -std=gnu++11
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-wroom32-release-prometheus] [env:esp32-wroom32-prometheus]
platform = espressif32@6.1.0 platform = espressif32@6.4.0
board = lolin_d32 board = lolin_d32
build_flags = build_flags = ${env.build_flags}
-D RELEASE -DUSE_HSPI_FOR_EPD
-std=gnu++17
-DENABLE_PROMETHEUS_EP -DENABLE_PROMETHEUS_EP
build_unflags = -std=gnu++11
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-wroom32-debug] [env:esp32-wroom32-ethernet]
platform = espressif32@6.1.0
board = lolin_d32
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
-std=gnu++17
build_unflags = -std=gnu++11
build_type = debug
monitor_filters =
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-ethernet-release]
platform = espressif32 platform = espressif32
board = esp32dev board = esp32dev
lib_deps = lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500 khoih-prog/AsyncWebServer_ESP32_W5500
khoih-prog/AsyncUDP_ESP32_W5500 khoih-prog/AsyncUDP_ESP32_W5500
nrf24/RF24 @ ^1.4.7 nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1 paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.4.4 https://github.com/bertmelis/espMqttClient#v1.5.0
bblanchon/ArduinoJson @ ^6.21.3 bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4 https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.34.17 olikraus/U8g2 @ ^2.35.7
zinggjm/GxEPD2 @ ^1.5.2 zinggjm/GxEPD2 @ ^1.5.2
build_flags = build_flags = ${env.build_flags}
-D ETHERNET -D ETHERNET
-DRELEASE -DRELEASE
-DUSE_HSPI_FOR_EPD
-DLOG_LOCAL_LEVEL=ESP_LOG_INFO -DLOG_LOCAL_LEVEL=ESP_LOG_INFO
-DDEBUG_LEVEL=DBG_INFO -DDEBUG_LEVEL=DBG_INFO
-std=gnu++17
build_unflags = -std=gnu++11
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:opendtufusionv1-release] [env:esp32-s2-mini]
platform = espressif32@6.1.0 platform = espressif32@6.4.0
board = lolin_s2_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DDEF_NRF_CS_PIN=12
-DDEF_NRF_CE_PIN=3
-DDEF_NRF_IRQ_PIN=5
-DDEF_NRF_MISO_PIN=9
-DDEF_NRF_MOSI_PIN=11
-DDEF_NRF_SCLK_PIN=7
monitor_filters =
esp32_exception_decoder
[env:esp32-c3-mini]
platform = espressif32@6.4.0
board = lolin_c3_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DDEF_NRF_CS_PIN=5
-DDEF_NRF_CE_PIN=0
-DDEF_NRF_IRQ_PIN=1
-DDEF_NRF_MISO_PIN=3
-DDEF_NRF_MOSI_PIN=4
-DDEF_NRF_SCLK_PIN=2
monitor_filters =
esp32_exception_decoder
[env:opendtufusion]
platform = espressif32@6.4.0
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
upload_protocol = esp-builtin upload_protocol = esp-builtin
debug_tool = esp-builtin build_flags = ${env.build_flags}
debug_speed = 12000 -DDEF_NRF_CS_PIN=37
build_flags = -DDEF_NRF_CE_PIN=38
-D RELEASE -DDEF_NRF_IRQ_PIN=47
-std=gnu++17 -DDEF_NRF_MISO_PIN=48
build_unflags = -std=gnu++11 -DDEF_NRF_MOSI_PIN=35
-DDEF_NRF_SCLK_PIN=36
-DDEF_CMT_CSB=4
-DDEF_CMT_FCSB=21
-DDEF_CMT_IRQ=8
-DDEF_CMT_SDIO=5
-DDEF_CMT_SCLK=6
-DDEF_LED0=18
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-ethernet]
platform = espressif32@6.4.0
board = esp32-s3-devkitc-1
lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.5.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.7
zinggjm/GxEPD2 @ ^1.5.2
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DETHERNET
-DSPI_HAL
-DUSE_HSPI_FOR_EPD
-DDEF_ETH_CS_PIN=42
-DDEF_ETH_SCK_PIN=39
-DDEF_ETH_MISO_PIN=41
-DDEF_ETH_MOSI_PIN=40
-DDEF_ETH_IRQ_PIN=44
-DDEF_ETH_RST_PIN=43
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
-DDEF_NRF_MISO_PIN=48
-DDEF_NRF_MOSI_PIN=35
-DDEF_NRF_SCLK_PIN=36
-DDEF_CMT_CSB=4
-DDEF_CMT_FCSB=21
-DDEF_CMT_IRQ=8
-DDEF_CMT_SDIO=5
-DDEF_CMT_SCLK=6
-DDEF_LED0=18
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
#-DARDUINO_USB_CDC_ON_BOOT=1
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-dev]
platform = espressif32@6.4.0
board = esp32-s3-devkitc-1
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.5.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.7
https://github.com/zinggjm/GxEPD2 @ ^1.5.2
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
-DDEF_NRF_MISO_PIN=48
-DDEF_NRF_MOSI_PIN=35
-DDEF_NRF_SCLK_PIN=36
-DDEF_CMT_CSB=4
-DDEF_CMT_FCSB=21
-DDEF_CMT_IRQ=8
-DDEF_CMT_SDIO=5
-DDEF_CMT_SCLK=6
-DDEF_LED0=18
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DSPI_HAL
monitor_filters = monitor_filters =
time ; Add timestamp with milliseconds for each new line esp32_exception_decoder, colorize

176
src/plugins/Display/Display.h

@ -5,6 +5,7 @@
#include <U8g2lib.h> #include <U8g2lib.h>
#include "../../hm/hmSystem.h" #include "../../hm/hmSystem.h"
#include "../../hm/hmRadio.h"
#include "../../utils/helper.h" #include "../../utils/helper.h"
#include "Display_Mono.h" #include "Display_Mono.h"
#include "Display_Mono_128X32.h" #include "Display_Mono_128X32.h"
@ -12,8 +13,10 @@
#include "Display_Mono_84X48.h" #include "Display_Mono_84X48.h"
#include "Display_Mono_64X48.h" #include "Display_Mono_64X48.h"
#include "Display_ePaper.h" #include "Display_ePaper.h"
#include "Display_data.h"
template <class HMSYSTEM>
template <class HMSYSTEM, class RADIO>
class Display { class Display {
public: public:
@ -21,37 +24,52 @@ class Display {
mMono = NULL; mMono = NULL;
} }
void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *hmradio, RADIO *hmsradio, uint32_t *utcTs) {
mApp = app;
mHmRadio = hmradio;
mHmsRadio = hmsradio;
mCfg = cfg; mCfg = cfg;
mSys = sys; mSys = sys;
mUtcTs = utcTs; mUtcTs = utcTs;
mNewPayload = false; mNewPayload = false;
mLoopCnt = 0; mLoopCnt = 0;
mVersion = version;
mDisplayData.version = app->getVersion(); // version never changes, so only set once
switch (mCfg->type) { switch (mCfg->type) {
case 0: mMono = NULL; break; case 0: mMono = NULL; break; // None
case 1: // fall-through case 1: mMono = new DisplayMono128X64(); break; // SSD1306_128X64 (0.96", 1.54")
case 2: mMono = new DisplayMono128X64(); break; case 2: mMono = new DisplayMono128X64(); break; // SH1106_128X64 (1.3")
case 3: mMono = new DisplayMono84X48(); break; case 3: mMono = new DisplayMono84X48(); break; // PCD8544_84X48 (1.6" - Nokia 5110)
case 4: mMono = new DisplayMono128X32(); break; case 4: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91")
case 5: mMono = new DisplayMono64X48(); break; case 5: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield)
case 6: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42")
#if defined(ESP32) #if defined(ESP32) && !defined(ETHERNET)
case 10: case 10:
mMono = NULL; // ePaper does not use this mMono = NULL; // ePaper does not use this
mRefreshCycle = 0; mRefreshCycle = 0;
mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline); mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline);
mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mDisplayData.version);
break; break;
#endif #endif
default: mMono = NULL; break; default: mMono = NULL; break;
} }
if(mMono) { if(mMono) {
mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->screenSaver, mCfg->contrast);
mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, &mDisplayData);
} }
// setup PIR pin for motion sensor
#ifdef ESP32
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF))
pinMode(mCfg->pirPin, INPUT);
#endif
#ifdef ESP8266
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF) && (mCfg->pirPin != A0))
pinMode(mCfg->pirPin, INPUT);
#endif
} }
void payloadEventListener(uint8_t cmd) { void payloadEventListener(uint8_t cmd) {
@ -60,56 +78,104 @@ class Display {
void tickerSecond() { void tickerSecond() {
if (mMono != NULL) if (mMono != NULL)
mMono->loop(); mMono->loop(mCfg->contrast, motionSensorActive());
if (mNewPayload || ((++mLoopCnt % 10) == 0)) { if (mNewPayload || (((++mLoopCnt) % 5) == 0)) {
DataScreen();
mNewPayload = false; mNewPayload = false;
mLoopCnt = 0; mLoopCnt = 0;
DataScreen();
} }
#if defined(ESP32) && !defined(ETHERNET)
mEpaper.tickerSecond();
#endif
} }
private: private:
void DataScreen() { void DataScreen() {
if (mCfg->type == 0) if (mCfg->type == 0)
return; return;
if (*mUtcTs == 0)
return;
float totalPower = 0; float totalPower = 0.0;
float totalYieldDay = 0; float totalYieldDay = 0.0;
float totalYieldTotal = 0; float totalYieldTotal = 0.0;
uint8_t isprod = 0; uint8_t nrprod = 0;
uint8_t nrsleep = 0;
int8_t minQAllInv = 4;
Inverter<> *iv; Inverter<> *iv;
record_t<> *rec; record_t<> *rec;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { bool allOff = true;
uint8_t nInv = mSys->getNumInverters();
for (uint8_t i = 0; i < nInv; i++) {
iv = mSys->getInverterByPos(i); iv = mSys->getInverterByPos(i);
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL) if (iv == NULL)
continue; continue;
if (iv->isProducing()) if (iv->isProducing()) // also updates inverter state engine
isprod++; nrprod++;
else
nrsleep++;
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv->isAvailable()) { // consider only radio quality of inverters still communicating
int8_t maxQInv = -6;
for(uint8_t ch = 0; ch < RF_MAX_CHANNEL_ID; ch++) {
int8_t q = iv->heuristics.txRfQuality[ch];
if (q > maxQInv)
maxQInv = q;
}
if (maxQInv < minQAllInv)
minQAllInv = maxQInv;
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); // add only FLD_PAC from inverters still communicating
allOff = false;
}
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
} }
if (allOff)
minQAllInv = -6;
// prepare display data
mDisplayData.nrProducing = nrprod;
mDisplayData.nrSleeping = nrsleep;
mDisplayData.totalPower = totalPower;
mDisplayData.totalYieldDay = totalYieldDay;
mDisplayData.totalYieldTotal = totalYieldTotal;
bool nrf_en = mApp->getNrfEnabled();
bool nrf_ok = nrf_en && mHmRadio->isChipConnected();
#if defined(ESP32)
bool cmt_en = mApp->getCmtEnabled();
bool cmt_ok = cmt_en && mHmsRadio->isChipConnected();
#else
bool cmt_en = false;
bool cmt_ok = false;
#endif
mDisplayData.RadioSymbol = (nrf_ok && !cmt_en) || (cmt_ok && !nrf_en) || (nrf_ok && cmt_ok);
mDisplayData.WifiSymbol = (WiFi.status() == WL_CONNECTED);
mDisplayData.MQTTSymbol = mApp->getMqttIsConnected();
mDisplayData.RadioRSSI = ivQuality2RadioRSSI(minQAllInv); // Workaround as NRF24 has no RSSI. Approximation by quality levels from heuristic function
mDisplayData.WifiRSSI = (WiFi.status() == WL_CONNECTED) ? WiFi.RSSI() : SCHAR_MIN;
mDisplayData.ipAddress = WiFi.localIP();
time_t utc= mApp->getTimestamp();
if (year(utc) > 2020)
mDisplayData.utcTs = utc;
else
mDisplayData.utcTs = 0;
if (mMono ) { if (mMono ) {
mMono->disp(totalPower, totalYieldDay, totalYieldTotal, isprod); mMono->disp();
} }
#if defined(ESP32) #if defined(ESP32) && !defined(ETHERNET)
else if (mCfg->type == 10) { else if (mCfg->type == 10) {
mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod);
mRefreshCycle++; mRefreshCycle++;
} }
#endif
#if defined(ESP32)
if (mRefreshCycle > 480) { if (mRefreshCycle > 480) {
mEpaper.fullRefresh(); mEpaper.fullRefresh();
mRefreshCycle = 0; mRefreshCycle = 0;
@ -117,16 +183,54 @@ class Display {
#endif #endif
} }
bool motionSensorActive() {
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF)) {
#if defined(ESP8266)
if (mCfg->pirPin == A0)
return((analogRead(A0) >= 512));
else
return(digitalRead(mCfg->pirPin));
#elif defined(ESP32)
return(digitalRead(mCfg->pirPin));
#endif
}
else
return(false);
}
// approximate RSSI in dB by invQuality levels from heuristic function (very unscientific but better than nothing :-) )
int8_t ivQuality2RadioRSSI(int8_t invQuality) {
int8_t pseudoRSSIdB;
switch(invQuality) {
case 4: pseudoRSSIdB = -55; break;
case 3:
case 2:
case 1: pseudoRSSIdB = -65; break;
case 0:
case -1:
case -2: pseudoRSSIdB = -75; break;
case -3:
case -4:
case -5: pseudoRSSIdB = -85; break;
case -6:
default: pseudoRSSIdB = -95; break;
}
return (pseudoRSSIdB);
}
// private member variables // private member variables
IApp *mApp;
DisplayData mDisplayData;
bool mNewPayload; bool mNewPayload;
uint8_t mLoopCnt; uint8_t mLoopCnt;
uint32_t *mUtcTs; uint32_t *mUtcTs;
const char *mVersion;
display_t *mCfg; display_t *mCfg;
HMSYSTEM *mSys; HMSYSTEM *mSys;
RADIO *mHmRadio;
RADIO *mHmsRadio;
uint16_t mRefreshCycle; uint16_t mRefreshCycle;
#if defined(ESP32) #if defined(ESP32) && !defined(ETHERNET)
DisplayEPaper mEpaper; DisplayEPaper mEpaper;
#endif #endif
DisplayMono *mMono; DisplayMono *mMono;

144
src/plugins/Display/Display_Mono.h

@ -10,6 +10,7 @@
#define DISP_FMT_TEXT_LEN 32 #define DISP_FMT_TEXT_LEN 32
#define BOTTOM_MARGIN 5 #define BOTTOM_MARGIN 5
#include "defines.h"
#ifdef ESP8266 #ifdef ESP8266
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
@ -17,33 +18,68 @@
#include <WiFi.h> #include <WiFi.h>
#endif #endif
#include "../../utils/helper.h" #include "../../utils/helper.h"
#include "Display_data.h"
#include "../../utils/dbg.h"
#include "../../utils/timemonitor.h"
class DisplayMono { class DisplayMono {
public: public:
DisplayMono() {}; DisplayMono() {};
virtual 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) = 0; virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) = 0;
virtual void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) = 0; virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) = 0;
virtual void loop(void) = 0; virtual void disp(void) = 0;
virtual void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) = 0;
// Common loop function, manages display on/off functions for powersave and screensaver with motionsensor
// can be overridden by subclasses
virtual void loop(uint8_t lum, bool motion) {
bool dispConditions = (!mEnPowerSave || (mDisplayData->nrProducing > 0)) &&
((mScreenSaver != 2) || motion); // screensaver 2 .. motionsensor
if (mDisplayActive) {
if (!dispConditions) {
if (mDisplayTime.isTimeout()) { // switch display off after timeout
mDisplayActive = false;
mDisplay->setPowerSave(true);
DBGPRINTLN("**** Display off ****");
}
}
else
mDisplayTime.reStartTimeMonitor(); // keep display on
}
else {
if (dispConditions) {
mDisplayActive = true;
mDisplayTime.reStartTimeMonitor(); // switch display on
mDisplay->setPowerSave(false);
DBGPRINTLN("**** Display on ****");
}
}
if(mLuminance != lum) {
mLuminance = lum;
mDisplay->setContrast(mLuminance);
}
}
protected: protected:
U8G2* mDisplay; U8G2* mDisplay;
DisplayData *mDisplayData;
uint8_t mType; uint8_t mType;
bool mEnPowerSafe, mEnScreenSaver; uint16_t mDispWidth;
uint16_t mDispHeight;
bool mEnPowerSave;
uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor
uint8_t mLuminance; uint8_t mLuminance;
uint8_t mLoopCnt; uint8_t mLoopCnt;
uint32_t* mUtcTs;
uint8_t mLineXOffsets[5] = {}; uint8_t mLineXOffsets[5] = {};
uint8_t mLineYOffsets[5] = {}; uint8_t mLineYOffsets[5] = {};
uint16_t mDispY;
uint8_t mExtra; uint8_t mExtra;
uint16_t mTimeout;
char mFmtText[DISP_FMT_TEXT_LEN];
#ifdef DISPLAY_CHART #ifdef DISPLAY_CHART
#define DISP_WATT_ARR_LENGTH 128 // Number of WATT history values #define DISP_WATT_ARR_LENGTH 128 // Number of WATT history values
@ -52,4 +88,92 @@ class DisplayMono {
int m_wattDispIdx; // index for 1st Element to display from WattArr int m_wattDispIdx; // index for 1st Element to display from WattArr
void drawPowerChart(); void drawPowerChart();
#endif #endif
int8_t mPixelshift=0;
TimeMonitor mDisplayTime = TimeMonitor(1000 * 15, true);
bool mDisplayActive = true; // always start with display on
char mFmtText[DISP_FMT_TEXT_LEN];
// Common initialization function to be called by subclasses
void monoInit(U8G2* display, uint8_t type, DisplayData *displayData) {
mDisplay = display;
mType = type;
mDisplayData = displayData;
mDisplay->begin();
mDisplay->setPowerSave(false); // always start with display on
mDisplay->setContrast(mLuminance);
mDisplay->clearBuffer();
mDispWidth = mDisplay->getDisplayWidth();
mDispHeight = mDisplay->getDisplayHeight();
}
void calcPixelShift(int range) {
int8_t mod = (millis() / 10000) % ((range >> 1) << 2);
mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0;
}
}; };
/* adapted 5x8 Font for low-res displays with symbols
Symbols:
\x80 ... antenna
\x81 ... WiFi
\x82 ... suncurve
\x83 ... sum/sigma
\x84 ... antenna crossed
\x85 ... WiFi crossed
\x86 ... sun
\x87 ... moon
\x88 ... calendar/day
\x89 ... MQTT */
const uint8_t u8g2_font_5x8_symbols_ahoy[1049] U8G2_FONT_SECTION("u8g2_font_5x8_symbols_ahoy") =
"j\0\3\2\4\4\3\4\5\10\10\0\377\6\377\6\0\1\61\2b\4\0 \5\0\304\11!\7a\306"
"\212!\11\42\7\63\335\212\304\22#\16u\304\232R\222\14JePJI\2$\14u\304\252l\251m"
"I\262E\0%\10S\315\212(\351\24&\13t\304\232(i\252\64%\1'\6\61\336\212\1(\7b"
"\305\32\245))\11b\305\212(\251(\0*\13T\304\212(Q\206D\211\2+\12U\304\252\60\32\244"
"\60\2,\7\63\275\32\245\4-\6\24\324\212!.\6\42\305\212!/\10d\304\272R[\6\60\14d"
"\304\32%R\206DJ\24\0\61\10c\305\232Dj\31\62\13d\304\32%\312\22%\33\2\63\13d\304"
"\212!\212D)Q\0\64\13d\304\252H\251\14Q\226\0\65\12d\304\212A\33\245D\1\66\13d\304"
"\32%[\42)Q\0\67\13d\304\212!\213\262(\213\0\70\14d\304\32%J\224HJ\24\0\71\13"
"d\304\32%\222\222-Q\0:\10R\305\212!\32\2;\10c\275\32\243R\2<\10c\305\252\244\224"
"\25=\10\64\314\212!\34\2>\11c\305\212\254\224\224\0?\11c\305\232\246$M\0@\15\205\274*"
")\222\226DI\244\252\2A\12d\304\32%\222\206I\12B\14d\304\212%\32\222H\32\22\0C\12"
"d\304\32%\322J\211\2D\12d\304\212%r\32\22\0E\12d\304\212A[\262l\10F\12d\304"
"\212A[\262\32\0G\13d\304\32%\322\222)Q\0H\12d\304\212H\32&S\0I\10c\305\212"
"%j\31J\12d\304\232)\253\224\42\0K\13d\304\212HI\244\244S\0L\10d\304\212\254\333\20"
"M\12d\304\212h\70D\246\0N\12d\304\212h\31\226I\12O\12d\304\32%rJ\24\0P\13"
"d\304\212%\222\206$\313\0Q\12t\274\32%\222\26\307\0R\13d\304\212%\222\206$\222\2S\14"
"d\304\32%J\302$J\24\0T\11e\304\212A\12;\1U\11d\304\212\310S\242\0V\12d\304"
"\212\310)\221\24\0W\12d\304\212\310\64\34\242\0X\13d\304\212HJ$%\222\2Y\12e\304\212"
"LKja\11Z\12d\304\212!\213\332\206\0[\10c\305\212!j\32\134\10d\304\212,l\13]"
"\10c\305\212\251i\10^\6#\345\232\6_\6\24\274\212!`\6\42\345\212(a\11D\304\232!\222"
"\222\1b\13d\304\212,[\42iH\0c\7C\305\232)\23d\12d\304\272\312\20I\311\0e\11"
"D\304\32%\31\262\1f\12d\304\252Ji\312\42\0g\12T\274\32%J\266D\1h\12d\304\212"
",[\42S\0i\10c\305\232P\252\14j\12s\275\252\64\212\224\12\0k\12d\304\212\254\64$\221"
"\24l\10c\305\12\251\313\0m\12E\304\12\245EI\224\2n\10D\304\212%\62\5o\11D\304\32"
"%\222\22\5p\12T\274\212%\32\222,\3q\11T\274\232!J\266\2r\11D\304\212$\261e\0"
"s\10C\305\232![\0t\13d\304\232,\232\262$J\0u\10D\304\212\310\224\14v\10C\305\212"
"\304R\1w\12E\304\212LI\224.\0x\11D\304\212(\221\224(y\13T\274\212HJ\206(Q"
"\0z\11D\304\212!*\15\1{\12t\304*%L\304(\24|\6a\306\212\3}\13t\304\12\61"
"\12\225\60\221\0~\10$\344\232DI\0\5\0\304\12\200\13u\274\212K\242T\266\260\4\201\14f"
"D\233!\11#-\312!\11\202\15hD<\65\12\243,\214\302$\16\203\15w<\214C\22F\71\220"
"\26\207A\204\13u\274\212\244\232t\313\241\10\205\17\206<\213\60\31\22\311\66D\245!\11\3\206\20\210"
"<\254\342\20]\302(L\246C\30E\0\207\15wD\334X\25\267\341\20\15\21\0\210\16w<\214\203"
"RQ\25I\212\324a\20\211\15f\304\213)\213\244,\222\222\245\0\0\0\0";
const uint8_t u8g2_font_ncenB08_symbols8_ahoy[173] U8G2_FONT_SECTION("u8g2_font_ncenB08_symbols8_ahoy") =
"\13\0\3\2\4\4\1\2\5\10\11\0\0\10\0\10\0\0\0\0\0\0\224A\14\207\305\70H\321\222H"
"k\334\6B\20\230\305\32\262\60\211\244\266\60T\243\34\326\0C\20\210\305S\243\60\312\302(\214\302("
"L\342\0D\16\210\315\70(i\224#\71\20W\207\3E\15\207\305xI\206\323\232nIS\1F\25"
"\230\305H\206\244\230$C\22\15Y\242\204j\224\205I$\5G\17\210\305*\16\321%\214\302d:\204"
"Q\4H\14w\307\215Uq\33\16\321\20\1I\21\227\305\311\222aP\245H\221\244H\212\324a\20J"
"\5\0\275\0K\5\0\315\0\0\0\0";
const uint8_t u8g2_font_ncenB10_symbols10_ahoy[217] U8G2_FONT_SECTION("u8g2_font_ncenB10_symbols10_ahoy") =
"\13\0\3\2\4\4\2\2\5\13\13\0\0\13\0\13\0\0\0\0\0\0\300A\15\267\212q\220\42\251\322"
"\266\306\275\1B\20\230\236\65da\22Ima\250F\71\254\1C\23\272\272\251\3Q\32\366Q\212\243"
"\70\212\243\70\311\221\0D\20\271\252\361\242F:\242#: {\36\16\1E\17\267\212\221\264\3Q\35"
"\332\321\34\316\341\14F\25\250\233\221\14I\61I\206$\252%J\250Fa\224%J\71G\30\273\312W"
"\316r`T\262DJ\303\64L#%K\304\35\310\342,\3H\27\272\272\217\344P\16\351\210\16\354\300"
"<\244C\70,\303 \16!\0I\24\271\252\241\34\336\1-\223\64-\323\62-\323\62\35x\10J\22"
"\210\232\61Hi\64Di\64DI\226$KeiK\5\0\212\1\0\0\0";

84
src/plugins/Display/Display_Mono_128X32.h

@ -9,95 +9,65 @@
class DisplayMono128X32 : public DisplayMono { class DisplayMono128X32 : public DisplayMono {
public: public:
DisplayMono128X32() : DisplayMono() { DisplayMono128X32() : DisplayMono() {
mEnPowerSafe = true;
mEnScreenSaver = true;
mLuminance = 60;
mExtra = 0; mExtra = 0;
mDispY = 0;
mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds)
mUtcTs = NULL;
mType = 0;
} }
void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
}
void 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) { void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) {
u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
mType = type; monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data), type, displayData);
mDisplay = new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data);
mUtcTs = utcTs;
mDisplay->begin();
calcLinePositions(); calcLinePositions();
printText("Ahoy!", 0);
mDisplay->clearBuffer();
mDisplay->setContrast(mLuminance);
printText("AHOY!", 0);
printText("ahoydtu.de", 2); printText("ahoydtu.de", 2);
printText(version, 3); printText(mDisplayData->version, 3);
mDisplay->sendBuffer(); mDisplay->sendBuffer();
} }
void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { void disp(void) {
mEnPowerSafe = enPowerSafe;
mEnScreenSaver = enScreenSaver;
mLuminance = lum;
}
void loop(void) {
if (mEnPowerSafe) {
if (mTimeout != 0)
mTimeout--;
}
}
void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) {
mDisplay->clearBuffer(); mDisplay->clearBuffer();
// set Contrast of the Display to raise the lifetime // calculate current pixelshift for pixelshift screensaver
if (3 != mType) calcPixelShift(pixelShiftRange);
mDisplay->setContrast(mLuminance);
if ((totalPower > 0) && (isprod > 0)) { if ((mDisplayData->totalPower > 0) && (mDisplayData->nrProducing > 0)) {
mTimeout = DISP_DEFAULT_TIMEOUT; if (mDisplayData->totalPower > 999)
mDisplay->setPowerSave(false); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (mDisplayData->totalPower / 1000));
if (totalPower > 999)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000));
else else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", mDisplayData->totalPower);
printText(mFmtText, 0); printText(mFmtText, 0);
} else { } else {
printText("offline", 0); printText("offline", 0);
// check if it's time to enter power saving mode
if (mTimeout == 0)
mDisplay->setPowerSave(mEnPowerSafe);
} }
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", mDisplayData->totalYieldDay);
printText(mFmtText, 1); printText(mFmtText, 1);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", mDisplayData->totalYieldTotal);
printText(mFmtText, 2); printText(mFmtText, 2);
IPAddress ip = WiFi.localIP(); IPAddress ip = WiFi.localIP();
if (!(mExtra % 10) && (ip)) if (!(mExtra % 10) && (ip))
printText(ip.toString().c_str(), 3); printText(ip.toString().c_str(), 3);
else if (!(mExtra % 5)) { else if (!(mExtra % 5)) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", mDisplayData->nrProducing);
printText(mFmtText, 3); printText(mFmtText, 3);
} else if (NULL != mUtcTs) } else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3);
mDisplay->sendBuffer(); mDisplay->sendBuffer();
mDispY = 0;
mExtra++; mExtra++;
} }
private: private:
const uint8_t pixelShiftRange = 7; // number of pixels to shift from left to right (centered -> must be odd!)
void calcLinePositions() { void calcLinePositions() {
uint8_t yOff[] = {0, 0}; uint8_t yOff[] = {0, 0};
for (uint8_t i = 0; i < 4; i++) { for (uint8_t i = 0; i < 4; i++) {
@ -114,13 +84,13 @@ class DisplayMono128X32 : public DisplayMono {
inline void setFont(uint8_t line) { inline void setFont(uint8_t line) {
switch (line) { switch (line) {
case 0: case 0:
mDisplay->setFont(u8g2_font_9x15_tf); mDisplay->setFont(u8g2_font_9x15_tr);
break; break;
case 3: case 3:
mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); mDisplay->setFont(u8g2_font_tom_thumb_4x6_tr);
break; break;
default: default:
mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); mDisplay->setFont(u8g2_font_tom_thumb_4x6_tr);
break; break;
} }
} }
@ -139,7 +109,7 @@ class DisplayMono128X32 : public DisplayMono {
void printText(const char *text, uint8_t line) { void printText(const char *text, uint8_t line) {
setFont(line); setFont(line);
uint8_t dispX = mLineXOffsets[line] + ((mEnScreenSaver) ? (mExtra % 7) : 0); uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift;
if (isTwoRowLine(line)) { if (isTwoRowLine(line)) {
String stringText = String(text); String stringText = String(text);

239
src/plugins/Display/Display_Mono_128X64.h

@ -22,50 +22,39 @@ class DisplayMono128X64 : public DisplayMono {
m_wattListIdx = 0; m_wattListIdx = 0;
m_wattDispIdx = 1; m_wattDispIdx = 1;
#endif #endif
mExtra = 0;
} }
void 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) { void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
}
void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) {
u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
mType = type;
switch (type) { switch (type) {
case 1: case 1:
mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); monoInit(new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData);
break; break;
default:
case 2: case 2:
mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); monoInit(new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData);
break;
case 6:
default:
monoInit(new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(rot, reset, clock, data), type, displayData);
break; break;
} }
mUtcTs = utcTs;
mDisplay->begin();
calcLinePositions(); calcLinePositions();
mDisplay->clearBuffer(); printText("Ahoy!", l_Ahoy, 0xff);
mDisplay->setContrast(mLuminance); printText("ahoydtu.de", l_Website, 0xff);
printText("AHOY!", 0, 35); printText(mDisplayData->version, l_Version, 0xff);
printText("ahoydtu.de", 2, 20);
printText(version, 3, 46);
mDisplay->sendBuffer(); mDisplay->sendBuffer();
} }
void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { void disp(void) {
mEnPowerSafe = enPowerSafe; uint8_t pos, sun_pos, moon_pos;
mEnScreenSaver = enScreenSaver;
mLuminance = lum;
}
void loop(void) {
if (mEnPowerSafe) {
if (mTimeout != 0)
mTimeout--;
}
}
void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) {
#ifdef DISPLAY_CHART #ifdef DISPLAY_CHART
bool drawChart = true; // should come from settings bool drawChart = true; // should come from settings
static unsigned int dbgCnt = 0; static unsigned int dbgCnt = 0;
@ -76,28 +65,35 @@ class DisplayMono128X64 : public DisplayMono {
#endif #endif
mDisplay->clearBuffer(); mDisplay->clearBuffer();
// set Contrast of the Display to raise the lifetime // Layout-Test
mDisplay->setContrast(mLuminance); /*
mDisplayData->nrSleeping = 10;
mDisplayData->nrProducing = 10;
mDisplayData->totalPower = 15432.9; // W
mDisplayData->totalYieldDay = 14321.9; // Wh
mDisplayData->totalYieldTotal = 15432.9; // kWh
mDisplay->drawPixel(0, 0);
mDisplay->drawPixel(mDispWidth-1, 0);
mDisplay->drawPixel(0, mDispHeight-1);
mDisplay->drawPixel(mDispWidth-1, mDispHeight-1);
*/
if ((totalPower > 0) && (isprod > 0)) { // calculate current pixelshift for pixelshift screensaver
mTimeout = DISP_DEFAULT_TIMEOUT; calcPixelShift(pixelShiftRange);
mDisplay->setPowerSave(false);
if (totalPower > 999) // print total power
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); if (mDisplayData->nrProducing > 0) {
if (mDisplayData->totalPower > 9999.0)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
else else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
printText(mFmtText, 0); printText(mFmtText, l_TotalPower, 0xff);
} else { } else {
printText("offline", 0, 25); printText("offline", l_TotalPower, 0xff);
// check if it's time to enter power saving mode
if (mTimeout == 0)
mDisplay->setPowerSave(mEnPowerSafe);
} }
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay);
printText(mFmtText, 1);
#ifdef DISPLAY_CHART #ifdef DISPLAY_CHART
if (drawChart) if (drawChart)
@ -123,6 +119,95 @@ class DisplayMono128X64 : public DisplayMono {
} else if (NULL != mUtcTs) } else if (NULL != mUtcTs)
printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3);
#endif #endif
// print Date and time
if (0 != mDisplayData->utcTs)
printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff);
// dynamic status bar, alternatively:
// print ip address
if (!(mExtra % 5) && (mDisplayData->ipAddress)) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str());
printText(mFmtText, l_Status, 0xff);
}
// print status of inverters
else {
sun_pos = -1;
moon_pos = -1;
setLineFont(l_Status);
if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
else if (0 == mDisplayData->nrSleeping) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, " ");
sun_pos = 0;
}
else if (0 == mDisplayData->nrProducing) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, " ");
moon_pos = 0;
}
else {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing);
sun_pos = mDisplay->getStrWidth(mFmtText) + 1;
snprintf(mFmtText+2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping);
moon_pos = mDisplay->getStrWidth(mFmtText) + 1;
snprintf(mFmtText+7, DISP_FMT_TEXT_LEN, " ");
}
printText(mFmtText, l_Status, 0xff);
pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2;
mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy);
if (sun_pos!=-1)
mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol
if (moon_pos!=-1)
mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol
}
// print yields
mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy);
mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol
mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol
if (mDisplayData->totalYieldDay > 9999.0)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0);
else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay);
printText(mFmtText, l_YieldDay, 0xff);
if (mDisplayData->totalYieldTotal > 9999.0)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0);
else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal);
printText(mFmtText, l_YieldTotal, 0xff);
// draw dynamic RSSI bars
int xoffs;
if (mScreenSaver == 1) // shrink screenwidth for pixelshift screensaver
xoffs = pixelShiftRange/2;
else
xoffs = 0;
int rssi_bar_height = 9;
for (int i = 0; i < 4; i++) {
int radio_rssi_threshold = -60 - i * 10;
int wifi_rssi_threshold = -60 - i * 10;
if (mDisplayData->RadioRSSI > radio_rssi_threshold)
mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height);
if (mDisplayData->WifiRSSI > wifi_rssi_threshold)
mDisplay->drawBox(mDispWidth - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height);
}
// draw dynamic antenna and WiFi symbols
mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy);
char sym[]=" ";
sym[0] = mDisplayData->RadioSymbol?'A':'E'; // NRF
mDisplay->drawStr(xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym);
if (mDisplayData->MQTTSymbol)
sym[0] = 'J'; // MQTT
else
sym[0] = mDisplayData->WifiSymbol?'B':'F'; // Wifi
mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym);
mDisplay->sendBuffer();
} }
mDisplay->sendBuffer(); mDisplay->sendBuffer();
@ -131,40 +216,72 @@ class DisplayMono128X64 : public DisplayMono {
} }
private: private:
enum _dispLine {
// start page
l_Website = 0,
l_Ahoy = 2,
l_Version = 4,
// run page
l_Time = 0,
l_Status = 1,
l_TotalPower = 2,
l_YieldDay = 3,
l_YieldTotal = 4,
// run page - rssi bar symbols
l_RSSI = 4,
// End
l_MAX_LINES = 5,
};
const uint8_t pixelShiftRange = 11; // number of pixels to shift from left to right (centered -> must be odd!)
void calcLinePositions() { void calcLinePositions() {
uint8_t yOff = 0; uint8_t yOff = 0;
for (uint8_t i = 0; i < 4; i++) { uint8_t i = 0;
setFont(i); uint8_t asc, dsc;
yOff += (mDisplay->getMaxCharHeight());
do {
setLineFont(i);
asc = mDisplay->getAscent();
yOff += asc;
mLineYOffsets[i] = yOff; mLineYOffsets[i] = yOff;
} dsc = mDisplay->getDescent();
yOff -= dsc;
if (l_Time==i) // prevent time and status line to touch
yOff+=1; // -> one pixels space
i++;
} while(l_MAX_LINES>i);
} }
inline void setFont(uint8_t line) {
switch (line) { inline void setLineFont(uint8_t line) {
case 0: if ((line == l_TotalPower) ||
(line == l_Ahoy))
#ifdef DISPLAY_CHART #ifdef DISPLAY_CHART
mDisplay->setFont(u8g2_font_6x12_tr); mDisplay->setFont(u8g2_font_6x12_tr);
#else #else
mDisplay->setFont(u8g2_font_ncenB14_tr); mDisplay->setFont(u8g2_font_ncenB14_tr);
#endif #endif
break; else if ((line == l_YieldDay) ||
case 3: (line == l_YieldTotal))
mDisplay->setFont(u8g2_font_5x8_tr); // mDisplay->setFont(u8g2_font_5x8_symbols_ahoy);
break;
default:
#ifdef DISPLAY_CHART #ifdef DISPLAY_CHART
mDisplay->setFont(u8g2_font_5x8_tr); mDisplay->setFont(u8g2_font_5x8_tr);
#else #else
mDisplay->setFont(u8g2_font_ncenB10_tr); mDisplay->setFont(u8g2_font_ncenB10_tr);
#endif #endif
break; else
} mDisplay->setFont(u8g2_font_ncenB08_tr);
} }
void printText(const char *text, uint8_t line, uint8_t dispX = 5) {
setFont(line);
dispX += (mEnScreenSaver) ? (mExtra % 7) : 0; void printText(const char *text, uint8_t line, uint8_t col=0) {
uint8_t dispX;
setLineFont(line);
if (0xff == col)
dispX = (mDispWidth - mDisplay->getStrWidth(text)) / 2; // center text
else
dispX = col;
dispX += mPixelshift;
mDisplay->drawStr(dispX, mLineYOffsets[line], text); mDisplay->drawStr(dispX, mLineYOffsets[line], text);
} }

82
src/plugins/Display/Display_Mono_64X48.h

@ -9,88 +9,58 @@
class DisplayMono64X48 : public DisplayMono { class DisplayMono64X48 : public DisplayMono {
public: public:
DisplayMono64X48() : DisplayMono() { DisplayMono64X48() : DisplayMono() {
mEnPowerSafe = true;
mEnScreenSaver = false;
mLuminance = 20;
mExtra = 0; mExtra = 0;
mDispY = 0;
mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds)
mUtcTs = NULL;
mType = 0;
} }
void 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) { void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
}
void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) {
u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
mType = type;
// Wemos OLed Shield is not defined in u8 lib -> use nearest compatible // Wemos OLed Shield is not defined in u8 lib -> use nearest compatible
mDisplay = new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, reset, clock, data); monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, reset, clock, data), type, displayData);
mUtcTs = utcTs;
mDisplay->begin();
calcLinePositions(); calcLinePositions();
printText("Ahoy!", 0);
mDisplay->clearBuffer();
mDisplay->setContrast(mLuminance);
printText("AHOY!", 0);
printText("ahoydtu.de", 1); printText("ahoydtu.de", 1);
printText(version, 2); printText(mDisplayData->version, 2);
mDisplay->sendBuffer(); mDisplay->sendBuffer();
} }
void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { void disp(void) {
mEnPowerSafe = enPowerSafe;
mEnScreenSaver = enScreenSaver;
mLuminance = lum;
}
void loop(void) {
if (mEnPowerSafe) {
if (mTimeout != 0)
mTimeout--;
}
}
void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) {
mDisplay->clearBuffer(); mDisplay->clearBuffer();
// set Contrast of the Display to raise the lifetime // calculate current pixelshift for pixelshift screensaver
mDisplay->setContrast(mLuminance); calcPixelShift(pixelShiftRange);
if ((totalPower > 0) && (isprod > 0)) {
mTimeout = DISP_DEFAULT_TIMEOUT;
mDisplay->setPowerSave(false);
if (totalPower > 999) if ((mDisplayData->totalPower > 0) && (mDisplayData->nrProducing > 0)) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); if (mDisplayData->totalPower > 999)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (mDisplayData->totalPower / 1000));
else else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", mDisplayData->totalPower);
printText(mFmtText, 0); printText(mFmtText, 0);
} else { } else {
printText("offline", 0); printText("offline", 0);
// check if it's time to enter power saving mode
if (mTimeout == 0)
mDisplay->setPowerSave(mEnPowerSafe);
} }
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", totalYieldDay); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", mDisplayData->totalYieldDay);
printText(mFmtText, 1); printText(mFmtText, 1);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "T: %4.0f kWh", totalYieldTotal); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "T: %4.0f kWh", mDisplayData->totalYieldTotal);
printText(mFmtText, 2); printText(mFmtText, 2);
IPAddress ip = WiFi.localIP(); IPAddress ip = WiFi.localIP();
if (!(mExtra % 10) && (ip)) if (!(mExtra % 10) && (ip))
printText(ip.toString().c_str(), 3); printText(ip.toString().c_str(), 3);
else if (!(mExtra % 5)) { else if (!(mExtra % 5)) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", isprod); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", mDisplayData->nrProducing);
printText(mFmtText, 3); printText(mFmtText, 3);
} else if (NULL != mUtcTs) } else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3);
mDisplay->sendBuffer(); mDisplay->sendBuffer();
@ -98,6 +68,8 @@ class DisplayMono64X48 : public DisplayMono {
} }
private: private:
const uint8_t pixelShiftRange = 4; // number of pixels to shift from left to right
void calcLinePositions() { void calcLinePositions() {
uint8_t yOff = 0; uint8_t yOff = 0;
for (uint8_t i = 0; i < 4; i++) { for (uint8_t i = 0; i < 4; i++) {
@ -110,11 +82,11 @@ class DisplayMono64X48 : public DisplayMono {
inline void setFont(uint8_t line) { inline void setFont(uint8_t line) {
switch (line) { switch (line) {
case 0: case 0:
mDisplay->setFont(u8g2_font_fur11_tf); mDisplay->setFont(u8g2_font_fur11_tr);
break; break;
case 1: case 1:
case 2: case 2:
mDisplay->setFont(u8g2_font_6x10_tf); mDisplay->setFont(u8g2_font_6x10_tr);
break; break;
case 3: case 3:
mDisplay->setFont(u8g2_font_4x6_tr); mDisplay->setFont(u8g2_font_4x6_tr);
@ -126,8 +98,8 @@ class DisplayMono64X48 : public DisplayMono {
} }
void printText(const char *text, uint8_t line) { void printText(const char *text, uint8_t line) {
uint8_t dispX = 0; //small display, use all we have uint8_t dispX = mLineXOffsets[line] + pixelShiftRange/2 + mPixelshift;
dispX += (mEnScreenSaver) ? (mExtra % 4) : 0;
setFont(line); setFont(line);
mDisplay->drawStr(dispX, mLineYOffsets[line], text); mDisplay->drawStr(dispX, mLineYOffsets[line], text);
} }

167
src/plugins/Display/Display_Mono_84X48.h

@ -5,93 +5,116 @@
#pragma once #pragma once
#include "Display_Mono.h" #include "Display_Mono.h"
#include "../../utils/dbg.h"
class DisplayMono84X48 : public DisplayMono { class DisplayMono84X48 : public DisplayMono {
public: public:
DisplayMono84X48() : DisplayMono() { DisplayMono84X48() : DisplayMono() {
mEnPowerSafe = true;
mEnScreenSaver = true;
mLuminance = 60;
mExtra = 0; mExtra = 0;
mDispY = 0;
mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds)
mUtcTs = NULL;
mType = 0;
mDispWidth = 0;
} }
void 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) { void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
}
void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) {
u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
mType = type; monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset), type, displayData);
mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset);
mUtcTs = utcTs;
mDisplay->begin();
mDispWidth = mDisplay->getDisplayWidth();
calcLinePositions(); calcLinePositions();
printText("Ahoy!", l_Ahoy, 0xff);
printText("ahoydtu.de", l_Website, 0xff);
printText(mDisplayData->version, l_Version, 0xff);
mDisplay->sendBuffer();
}
void disp(void) {
mDisplay->clearBuffer(); mDisplay->clearBuffer();
mDisplay->setContrast(mLuminance);
printText("AHOY!", l_Ahoy); // Layout-Test
printText("ahoydtu.de", l_Website); /*
printText(version, l_Version); mDisplayData->nrSleeping = 10;
mDisplay->sendBuffer(); mDisplayData->nrProducing = 10;
} mDisplayData->totalPower = 19897.6; // W
mDisplayData->totalYieldDay = 15521.9; // Wh
mDisplayData->totalYieldTotal = 654321.9; // kWh
mDisplay->drawPixel(0, 0);
mDisplay->drawPixel(mDispWidth-1, 0);
mDisplay->drawPixel(0, mDispHeight-1);
mDisplay->drawPixel(mDispWidth-1, mDispHeight-1);
*/
// print total power
if (mDisplayData->nrProducing > 0) {
if (mDisplayData->totalPower > 9999.0)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { printText(mFmtText, l_TotalPower, 0xff);
mEnPowerSafe = enPowerSafe; } else {
mEnScreenSaver = enScreenSaver; printText("offline", l_TotalPower, 0xff);
mLuminance = lum;
} }
void loop(void) { // print Date and time
if (mEnPowerSafe) { if (0 != mDisplayData->utcTs)
if (mTimeout != 0) printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff);
mTimeout--;
// alternatively:
// print ip address
if (!(mExtra % 5) && (mDisplayData->ipAddress)) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str());
printText(mFmtText, l_Status, 0xff);
} }
// print status of inverters
else {
if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
else if (0 == mDisplayData->nrSleeping)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol
else if (0 == mDisplayData->nrProducing)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x87"); // moon symbol
else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d\x86 %d\x87", mDisplayData->nrProducing, mDisplayData->nrSleeping);
printText(mFmtText, l_Status, 0xff);
} }
void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { // print yields
mDisplay->clearBuffer(); printText("\x88", l_YieldDay, 10); // day symbol
printText("\x83", l_YieldTotal, 10); // total symbol
// set Contrast of the Display to raise the lifetime
mDisplay->setContrast(mLuminance);
if ((totalPower > 0) && (isprod > 0)) {
mTimeout = DISP_DEFAULT_TIMEOUT;
mDisplay->setPowerSave(false);
if (totalPower > 999) if (mDisplayData->totalYieldDay > 9999.0)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (totalPower / 1000)); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0);
else else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", totalPower); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay);
printText(mFmtText, l_YieldDay, 0xff);
printText(mFmtText, l_TotalPower); if (mDisplayData->totalYieldTotal > 9999.0)
} else { snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0);
printText("offline", l_TotalPower); else
// check if it's time to enter power saving mode snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal);
if (mTimeout == 0) printText(mFmtText, l_YieldTotal, 0xff);
mDisplay->setPowerSave(mEnPowerSafe);
// draw dynamic Nokia RSSI bars
int rssi_bar_height = 7;
for (int i=0; i<4;i++) {
int radio_rssi_threshold = -60 - i*10; // radio rssi not yet tested in reality!
int wifi_rssi_threshold = -60 - i*10;
if (mDisplayData->RadioRSSI > radio_rssi_threshold)
mDisplay->drawBox(0, 8+(rssi_bar_height+1)*i, 4-i,rssi_bar_height);
if (mDisplayData->WifiRSSI > wifi_rssi_threshold)
mDisplay->drawBox(mDispWidth-4+i, 8+(rssi_bar_height+1)*i, 4-i,rssi_bar_height);
} }
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "Today: %4.0f Wh", totalYieldDay); // draw dynamic antenna and WiFi symbols
printText(mFmtText, l_YieldDay); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%c", mDisplayData->RadioSymbol?'\x80':'\x84'); // NRF
printText(mFmtText, l_RSSI);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "Total: %.1f kWh", totalYieldTotal); if (mDisplayData->MQTTSymbol)
printText(mFmtText, l_YieldTotal); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x89"); // MQTT
if (NULL != mUtcTs)
printText(ah::getDateTimeStrShort(gTimezone.toLocal(*mUtcTs)).c_str(), l_Time);
IPAddress ip = WiFi.localIP();
if (!(mExtra % 5) && (ip))
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", ip.toString().c_str());
else else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "Inv.On: %d", isprod); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%c", mDisplayData->WifiSymbol?'\x81':'\x85'); // Wifi connected
printText(mFmtText, l_Status); printText(mFmtText, l_RSSI, mDispWidth - 6);
mDisplay->sendBuffer(); mDisplay->sendBuffer();
@ -99,7 +122,6 @@ class DisplayMono84X48 : public DisplayMono {
} }
private: private:
uint16_t mDispWidth;
enum _dispLine { enum _dispLine {
// start page // start page
l_Website = 0, l_Website = 0,
@ -111,6 +133,8 @@ class DisplayMono84X48 : public DisplayMono {
l_TotalPower = 2, l_TotalPower = 2,
l_YieldDay = 3, l_YieldDay = 3,
l_YieldTotal = 4, l_YieldTotal = 4,
// run page - rssi bar symbols
l_RSSI = 4,
// End // End
l_MAX_LINES = 5, l_MAX_LINES = 5,
}; };
@ -121,7 +145,7 @@ class DisplayMono84X48 : public DisplayMono {
uint8_t asc, dsc; uint8_t asc, dsc;
do { do {
setFont(i); setLineFont(i);
asc = mDisplay->getAscent(); asc = mDisplay->getAscent();
yOff += asc; yOff += asc;
mLineYOffsets[i] = yOff; mLineYOffsets[i] = yOff;
@ -133,18 +157,23 @@ class DisplayMono84X48 : public DisplayMono {
} while(l_MAX_LINES>i); } while(l_MAX_LINES>i);
} }
inline void setFont(uint8_t line) { inline void setLineFont(uint8_t line) {
if ((line == l_TotalPower) || (line == l_Ahoy)) if ((line == l_TotalPower) || (line == l_Ahoy))
mDisplay->setFont(u8g2_font_logisoso16_tr); mDisplay->setFont(u8g2_font_logisoso16_tr);
else else
mDisplay->setFont(u8g2_font_5x8_tr); mDisplay->setFont(u8g2_font_5x8_symbols_ahoy);
} }
void printText(const char *text, uint8_t line) { void printText(const char *text, uint8_t line, uint8_t col=0) {
uint8_t dispX; uint8_t dispX;
setFont(line);
setLineFont(line);
if (0xff == col)
dispX = (mDispWidth - mDisplay->getStrWidth(text)) / 2; // center text dispX = (mDispWidth - mDisplay->getStrWidth(text)) / 2; // center text
dispX += (mEnScreenSaver) ? (mExtra % 7) : 0; else
dispX = col;
mDisplay->drawStr(dispX, mLineYOffsets[line], text); mDisplay->drawStr(dispX, mLineYOffsets[line], text);
} }
}; };

22
src/plugins/Display/Display_data.h

@ -0,0 +1,22 @@
#include "../../utils/helper.h"
#ifndef __DISPLAY_DATA__
#define __DISPLAY_DATA__
struct DisplayData {
const char *version=nullptr;
float totalPower=0.0f; // indicate current power (W)
float totalYieldDay=0.0f; // indicate day yield (Wh)
float totalYieldTotal=0.0f; // indicate total yield (kWh)
uint32_t utcTs=0; // indicate absolute timestamp (utc unix time). 0 = time is not synchonized
uint8_t nrProducing=0; // indicate number of producing inverters
uint8_t nrSleeping=0; // indicate number of sleeping inverters
bool WifiSymbol = false; // indicate if WiFi is connected
bool RadioSymbol = false; // indicate if radio module is connecting and working
bool MQTTSymbol = false; // indicate if MQTT is connected
int8_t WifiRSSI=SCHAR_MIN; // indicate RSSI value for WiFi
int8_t RadioRSSI=SCHAR_MIN; // indicate RSSI value for radio
IPAddress ipAddress; // indicate ip adress of ahoy
};
#endif /*__DISPLAY_DATA__*/

188
src/plugins/Display/Display_ePaper.cpp

@ -1,9 +1,9 @@
#include "Display_ePaper.h" #include "Display_ePaper.h"
#ifdef ESP8266 #ifdef ESP8266
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#elif defined(ESP32) #elif defined(ESP32)
#include <WiFi.h> #include <WiFi.h>
#endif #endif
#include "../../utils/helper.h" #include "../../utils/helper.h"
#include "imagedata.h" #include "imagedata.h"
@ -26,53 +26,78 @@ DisplayEPaper::DisplayEPaper() {
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) { 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; mUtcTs = utcTs;
mRefreshState = RefreshStatus::LOGO;
mSecondCnt = 0;
if (type == 10) { if (type == 10) {
Serial.begin(115200); Serial.begin(115200);
_display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); _display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY));
hspi.begin(_SCK, _BUSY, _MOSI, _CS);
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) #if defined(ESP32) && defined(USE_HSPI_FOR_EPD)
hspi.begin(_SCK, _BUSY, _MOSI, _CS);
_display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0)); _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0));
#elif defined(ESP32)
_display->epd2.init(_SCK, _MOSI, 115200, true, 20, false);
#endif #endif
_display->init(115200, true, 2, false); _display->init(115200, true, 20, false);
_display->setRotation(mDisplayRotation); _display->setRotation(mDisplayRotation);
_display->setFullWindow(); _display->setFullWindow();
_version = version;
// 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, bool enPowerSafe) { void DisplayEPaper::config(uint8_t rotation, bool enPowerSave) {
mDisplayRotation = rotation; mDisplayRotation = rotation;
mEnPowerSafe = enPowerSafe; mEnPowerSave = enPowerSave;
} }
//*************************************************************************** //***************************************************************************
void DisplayEPaper::fullRefresh() { void DisplayEPaper::fullRefresh() {
// screen complete black if(RefreshStatus::DONE != mRefreshState)
return;
mSecondCnt = 2;
mRefreshState = RefreshStatus::BLACK;
}
//***************************************************************************
void DisplayEPaper::refreshLoop() {
switch(mRefreshState) {
case RefreshStatus::LOGO:
_display->fillScreen(GxEPD_BLACK); _display->fillScreen(GxEPD_BLACK);
while (_display->nextPage()) _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
; mNextRefreshState = RefreshStatus::PARTITIALS;
delay(2000); mRefreshState = RefreshStatus::WAIT;
// screen complete white break;
case RefreshStatus::BLACK:
_display->fillScreen(GxEPD_BLACK);
mNextRefreshState = RefreshStatus::WHITE;
mRefreshState = RefreshStatus::WAIT;
break;
case RefreshStatus::WHITE:
if(mSecondCnt == 0) {
_display->fillScreen(GxEPD_WHITE); _display->fillScreen(GxEPD_WHITE);
while (_display->nextPage()) mNextRefreshState = RefreshStatus::PARTITIALS;
; mRefreshState = RefreshStatus::WAIT;
}
break;
case RefreshStatus::WAIT:
if(!_display->nextPage())
mRefreshState = mNextRefreshState;
break;
case RefreshStatus::PARTITIALS:
headlineIP();
versionFooter();
mSecondCnt = 4; // display Logo time during boot up
mRefreshState = RefreshStatus::DONE;
break;
default: // RefreshStatus::DONE
break;
}
} }
//*************************************************************************** //***************************************************************************
void DisplayEPaper::headlineIP() { void DisplayEPaper::headlineIP() {
@ -121,6 +146,26 @@ void DisplayEPaper::lastUpdatePaged() {
} while (_display->nextPage()); } while (_display->nextPage());
} }
//*************************************************************************** //***************************************************************************
void DisplayEPaper::versionFooter() {
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 {
snprintf(_fmtText, sizeof(_fmtText), "Version: %s", _version);
_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::offlineFooter() { void DisplayEPaper::offlineFooter() {
int16_t tbx, tby; int16_t tbx, tby;
uint16_t tbw, tbh; uint16_t tbw, tbh;
@ -152,18 +197,20 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
_display->setPartialWindow(0, mHeadFootPadding, _display->width(), _display->height() - (mHeadFootPadding * 2)); _display->setPartialWindow(0, mHeadFootPadding, _display->width(), _display->height() - (mHeadFootPadding * 2));
_display->fillScreen(GxEPD_WHITE); _display->fillScreen(GxEPD_WHITE);
do { do {
// actual Production
if (totalPower > 9999) { if (totalPower > 9999) {
snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 10000)); snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 1000));
_changed = true; _changed = true;
} else if ((totalPower > 0) && (totalPower <= 9999)) { } else if ((totalPower > 0) && (totalPower <= 9999)) {
snprintf(_fmtText, sizeof(_fmtText), "%.0f W", totalPower); snprintf(_fmtText, sizeof(_fmtText), "%.0f W", totalPower);
_changed = true; _changed = true;
} else { } else
snprintf(_fmtText, sizeof(_fmtText), "offline"); snprintf(_fmtText, sizeof(_fmtText), "offline");
}
if (totalPower == 0){ if ((totalPower == 0) && (mEnPowerSave)) {
_display->fillRect(0, mHeadFootPadding, 200,200, GxEPD_BLACK); _display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK);
_display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
} else { } else {
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
@ -171,42 +218,69 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
_display->setCursor(x, mHeadFootPadding + tbh + 10); _display->setCursor(x, mHeadFootPadding + tbh + 10);
_display->print(_fmtText); _display->print(_fmtText);
_display->setFont(&FreeSans12pt7b); if ((totalYieldDay > 0) && (totalYieldTotal > 0)) {
// Today Production
bool kwh = (totalYieldDay > 9999);
if(kwh)
snprintf(_fmtText, _display->width(), "%.1f", (totalYieldDay / 1000));
else
snprintf(_fmtText, _display->width(), "%.0f", (totalYieldDay));
_display->setFont(&FreeSans18pt7b);
y = _display->height() / 2; y = _display->height() / 2;
_display->setCursor(5, y); _display->setCursor(5, y);
_display->print("today:");
snprintf(_fmtText, _display->width(), "%.0f", totalYieldDay);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
x = ((_display->width() - tbw) / 2) - tbx; _display->drawInvertedBitmap(5, ((kwh) ? (y - ((tbh + 30) / 2)) : (y - tbh)), myToday, 30, 30, GxEPD_BLACK);
x = ((_display->width() - tbw - 20) / 2) - tbx;
_display->setCursor(x, y); _display->setCursor(x, y);
_display->print(_fmtText); _display->print(_fmtText);
_display->setCursor(_display->width() - 38, y); _display->setCursor(_display->width() - ((kwh) ? 50 : 38), y);
_display->println("Wh"); _display->setFont(&FreeSans12pt7b);
_display->println((kwh) ? "kWh" : "Wh");
y = y + tbh + 15;
y = y + tbh + 7;
// Total Production
bool mwh = (totalYieldTotal > 9999);
if(mwh)
snprintf(_fmtText, _display->width(), "%.1f", (totalYieldTotal / 1000));
else
snprintf(_fmtText, _display->width(), "%.0f", (totalYieldTotal));
_display->setFont(&FreeSans18pt7b);
_display->setCursor(5, y); _display->setCursor(5, y);
_display->print("total:");
snprintf(_fmtText, _display->width(), "%.1f", totalYieldTotal);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
x = ((_display->width() - tbw) / 2) - tbx; _display->drawInvertedBitmap(5, y - tbh, mySigma, 30, 30, GxEPD_BLACK);
x = ((_display->width() - tbw - 20) / 2) - tbx;
_display->setCursor(x, y); _display->setCursor(x, y);
_display->print(_fmtText); _display->print(_fmtText);
_display->setCursor(_display->width() - 50, y); _display->setCursor(_display->width() - ((mwh) ? 59 : 50), y);
_display->println("kWh"); _display->setFont(&FreeSans12pt7b);
_display->println((mwh) ? "MWh" : "kWh");
}
_display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); // Inverter online
snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", isprod); _display->setFont(&FreeSans12pt7b);
y = _display->height() - (mHeadFootPadding + 10);
snprintf(_fmtText, sizeof(_fmtText), " %d online", isprod);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
_display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK);
x = ((_display->width() - tbw - 20) / 2) - tbx;
_display->setCursor(x, y);
_display->println(_fmtText); _display->println(_fmtText);
} }
yield();
} while (_display->nextPage()); } while (_display->nextPage());
} }
//*************************************************************************** //***************************************************************************
void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) {
if(RefreshStatus::DONE != mRefreshState)
return;
// check if the IP has changed // check if the IP has changed
if (_settedIP != WiFi.localIP().toString().c_str()) { if (_settedIP != WiFi.localIP().toString()) {
// save the new IP and call the Headline Funktion to adapt the Headline // save the new IP and call the Headline Function to adapt the Headline
_settedIP = WiFi.localIP().toString().c_str(); _settedIP = WiFi.localIP().toString();
headlineIP(); headlineIP();
} }
@ -217,10 +291,16 @@ void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYield
if ((isprod > 0) && (_changed)) { if ((isprod > 0) && (_changed)) {
_changed = false; _changed = false;
lastUpdatePaged(); lastUpdatePaged();
} else if((0 == totalPower) && (mEnPowerSafe)) } else if ((0 == totalPower) && (mEnPowerSave))
offlineFooter(); offlineFooter();
_display->powerOff(); _display->powerOff();
} }
//*************************************************************************** //***************************************************************************
void DisplayEPaper::tickerSecond() {
if(mSecondCnt != 0)
mSecondCnt--;
refreshLoop();
}
#endif // ESP32 #endif // ESP32

29
src/plugins/Display/Display_ePaper.h

@ -3,9 +3,6 @@
#if defined(ESP32) #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 /// uncomment next line to use class GFX of library GFX_Root instead of Adafruit_GFX, to use less code and ram
// #include <GFX.h> // #include <GFX.h>
// base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code // base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code
@ -30,25 +27,39 @@ class DisplayEPaper {
public: public:
DisplayEPaper(); DisplayEPaper();
void fullRefresh(); 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 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, bool enPowerSafe); void config(uint8_t rotation, bool enPowerSave);
void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod);
void refreshLoop();
void tickerSecond();
private: private:
void headlineIP(); void headlineIP();
void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod);
void lastUpdatePaged(); void lastUpdatePaged();
void offlineFooter(); void offlineFooter();
void versionFooter();
enum class RefreshStatus : uint8_t {
DONE,
BLACK,
WHITE,
WAIT,
PARTITIALS,
LOGO
};
uint8_t mDisplayRotation; uint8_t mDisplayRotation;
bool _changed = false; bool _changed = false;
char _fmtText[35]; char _fmtText[35];
const char* _settedIP; String _settedIP;
uint8_t mHeadFootPadding; uint8_t mHeadFootPadding;
GxEPD2_GFX* _display; GxEPD2_GFX* _display;
uint32_t *mUtcTs; uint32_t* mUtcTs;
bool mEnPowerSafe; bool mEnPowerSave;
const char* _version;
RefreshStatus mRefreshState, mNextRefreshState;
uint8_t mSecondCnt;
}; };
#endif // ESP32 #endif // ESP32

41
src/plugins/Display/imagedata.h

@ -9,6 +9,47 @@
#include <pgmspace.h> #include <pgmspace.h>
#endif #endif
// 'Sigma', 30x30px
const unsigned char mySigma[] PROGMEM = {
0x80, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xf0,
0x3f, 0x80, 0x07, 0xf0, 0x3f, 0x80, 0x07, 0xf0, 0x3f, 0x9f, 0xe7, 0xf0, 0x3f, 0xcf, 0xe7, 0xf0,
0x3f, 0xcf, 0xff, 0xf0, 0x3f, 0xe7, 0xff, 0xf0, 0x3f, 0xe7, 0xff, 0xf0, 0x3f, 0xf3, 0xff, 0xf0,
0x3f, 0xf3, 0xff, 0xf0, 0x3f, 0xf9, 0xff, 0xf0, 0x3f, 0xf8, 0xff, 0xf0, 0x3f, 0xf8, 0xff, 0xf0,
0x3f, 0xf9, 0xff, 0xf0, 0x3f, 0xf3, 0xff, 0xf0, 0x3f, 0xf3, 0xff, 0xf0, 0x3f, 0xe7, 0xff, 0xf0,
0x3f, 0xe7, 0xff, 0xf0, 0x3f, 0xcf, 0xff, 0xf0, 0x3f, 0xcf, 0xe7, 0xf0, 0x3f, 0x9f, 0xe7, 0xf0,
0x3f, 0x80, 0x07, 0xf0, 0x3f, 0x80, 0x07, 0xf0, 0x3f, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xf0,
0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x04
};
// 'Sun', 30x30px
const unsigned char mySun[] PROGMEM = {
0x80, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xfb, 0xf0,
0x3f, 0xfc, 0xfb, 0xf0, 0x3f, 0xfc, 0xfb, 0xf0, 0x3e, 0xfc, 0xf3, 0xf0, 0x3c, 0x7c, 0xf3, 0xf0,
0x3e, 0x30, 0x33, 0xf0, 0x3f, 0x0f, 0xcb, 0xf0, 0x3f, 0x9f, 0xeb, 0xf0, 0x3f, 0xbf, 0xeb, 0xf0,
0x3f, 0x7f, 0xd8, 0x30, 0x3f, 0x7f, 0xdf, 0xb0, 0x30, 0x7f, 0xdf, 0xb0, 0x30, 0x7f, 0xbf, 0x70,
0x3f, 0x7f, 0xbf, 0x70, 0x3f, 0x7f, 0x83, 0x70, 0x3f, 0xbf, 0xf2, 0xf0, 0x3f, 0x9f, 0xe2, 0xf0,
0x3f, 0x0f, 0xca, 0xf0, 0x3e, 0x30, 0x39, 0xf0, 0x3c, 0x7c, 0xf9, 0xf0, 0x3e, 0xfc, 0xf9, 0xf0,
0x3f, 0xfc, 0xfb, 0xf0, 0x3f, 0xfc, 0xfb, 0xf0, 0x3f, 0xff, 0xfb, 0xf0, 0x3f, 0xff, 0xff, 0xf0,
0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x04
};
// 'Today', 30x30px
const unsigned char myToday[] PROGMEM = {
0xf3, 0xff, 0xff, 0x3c, 0xf3, 0xff, 0xff, 0x3c, 0x80, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
0x33, 0xff, 0xff, 0x30, 0x33, 0xff, 0xff, 0x30, 0x3f, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xf0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xf0,
0x3f, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xfc, 0xf0, 0x3f, 0xff, 0xf8, 0xf0, 0x3f, 0xff, 0xf1, 0xf0,
0x3f, 0xff, 0xe3, 0xf0, 0x3f, 0xff, 0xc7, 0xf0, 0x3f, 0xff, 0x8f, 0xf0, 0x3f, 0xff, 0x1f, 0xf0,
0x3f, 0xfe, 0x3f, 0xf0, 0x3e, 0x7c, 0x7f, 0xf0, 0x3e, 0x38, 0xff, 0xf0, 0x3f, 0x11, 0xff, 0xf0,
0x3f, 0x83, 0xff, 0xf0, 0x3f, 0xc7, 0xff, 0xf0, 0x3f, 0xef, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xf0,
0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x04
};
// 'WR', 20x20px
const unsigned char myWR[] PROGMEM = {
0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x80, 0x3f, 0xff, 0x40, 0x30, 0x7e, 0xc0, 0x3f,
0xfd, 0xc0, 0x30, 0x7b, 0xc0, 0x3f, 0xf7, 0xc0, 0x3f, 0xef, 0xc0, 0x3f, 0xdf, 0xc0, 0x3f, 0xbf,
0xc0, 0x3f, 0x7f, 0xc0, 0x3e, 0xff, 0xc0, 0x3d, 0xf7, 0xc0, 0x3b, 0xea, 0xc0, 0x37, 0xfd, 0xc0,
0x2f, 0xff, 0xc0, 0x1f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x80, 0x00, 0x10
};
// 'Logo', 200x200px // 'Logo', 200x200px
const unsigned char logo[] PROGMEM = { 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,

59
src/publisher/pubMqtt.h

@ -134,7 +134,7 @@ class PubMqtt {
#endif #endif
} }
bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) { bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs) {
if (!mClient.connected()) if (!mClient.connected())
return false; return false;
@ -142,7 +142,20 @@ class PubMqtt {
publish(subtopics[MQTT_SUNSET], String(sunset).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_START], String(sunrise - offs).c_str(), true);
publish(subtopics[MQTT_COMM_STOP], String(sunset + 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);
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = mSys->getInverterByPos(i);
if(NULL == iv)
continue;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/dis_night_comm", iv->config->name);
publish(mSubTopic, ((iv->commEnabled) ? dict[STR_TRUE] : dict[STR_FALSE]), true);
}
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "comm_disabled");
publish(mSubTopic, (((*mUtcTimestamp > (sunset + offs)) || (*mUtcTimestamp < (sunrise - offs))) ? dict[STR_TRUE] : dict[STR_FALSE]), true);
return true; return true;
} }
@ -236,7 +249,11 @@ class PubMqtt {
publish(subtopics[MQTT_VERSION], mVersion, true); publish(subtopics[MQTT_VERSION], mVersion, true);
publish(subtopics[MQTT_DEVICE], mDevName, true); publish(subtopics[MQTT_DEVICE], mDevName, true);
#if defined(ETHERNET)
publish(subtopics[MQTT_IP_ADDR], ETH.localIP().toString().c_str(), true);
#else
publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true); publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true);
#endif
tickerMinute(); tickerMinute();
publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false); publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false);
@ -470,22 +487,22 @@ class PubMqtt {
continue; // skip to next inverter continue; // skip to next inverter
// inverter status // inverter status
iv->isProducing(); // recalculate status InverterStatus status = iv->getStatus();
if (InverterStatus::OFF < iv->status) if (InverterStatus::OFF < status)
anyAvail = true; anyAvail = true;
else // inverter is enabled but not available else // inverter is enabled but not available
allAvail = false; allAvail = false;
if(mLastIvState[id] != iv->status) { if(mLastIvState[id] != status) {
// if status changed from producing to not producing send last data immediately // if status changed from producing to not producing send last data immediately
if (InverterStatus::WAS_PRODUCING == mLastIvState[id]) if (InverterStatus::WAS_PRODUCING == mLastIvState[id])
sendData(iv, RealTimeRunData_Debug); sendData(iv, RealTimeRunData_Debug);
mLastIvState[id] = iv->status; mLastIvState[id] = status;
changed = true; changed = true;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
snprintf(mVal, 40, "%d", (uint8_t)iv->status); snprintf(mVal, 40, "%d", (uint8_t)status);
publish(mSubTopic, mVal, true); publish(mSubTopic, mVal, true);
} }
} }
@ -516,25 +533,17 @@ class PubMqtt {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/cnt", iv->config->name); snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/cnt", iv->config->name);
snprintf(mVal, 40, "%d", iv->alarmCnt); snprintf(mVal, 40, "%d", iv->alarmCnt);
publish(mSubTopic, mVal, true); publish(mSubTopic, mVal, false);
for(uint8_t j = 0; j < 10; j++) { for(uint8_t j = 0; j < 10; j++) {
if(0 != iv->lastAlarm[j].code) { if(0 != iv->lastAlarm[j].code) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/code", iv->config->name, j); snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d", iv->config->name, j);
snprintf(mVal, 40, "%d", iv->lastAlarm[j].code); snprintf(mVal, 100, "{\"code\":%d,\"str\":\"%s\",\"start\":%d,\"end\":%d}",
publish(mSubTopic, mVal, true); iv->lastAlarm[j].code,
iv->getAlarmStr(iv->lastAlarm[j].code).c_str(),
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/str", iv->config->name, j); iv->lastAlarm[j].start + lastMidnight,
snprintf(mVal, 40, "%s", iv->getAlarmStr(iv->lastAlarm[j].code).c_str()); iv->lastAlarm[j].end + lastMidnight);
publish(mSubTopic, mVal, true); publish(mSubTopic, mVal, false);
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/start", iv->config->name, j);
snprintf(mVal, 40, "%d", iv->lastAlarm[j].start + lastMidnight);
publish(mSubTopic, mVal, true);
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/end", iv->config->name, j);
snprintf(mVal, 40, "%d", iv->lastAlarm[j].end + lastMidnight);
publish(mSubTopic, mVal, true);
yield(); yield();
} }
} }
@ -606,14 +615,14 @@ class PubMqtt {
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS]; uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS];
uint16_t mIntervalTimeout; uint16_t mIntervalTimeout;
// last will topic and payload must be available trough lifetime of 'espMqttClient' // last will topic and payload must be available through lifetime of 'espMqttClient'
char mLwtTopic[MQTT_TOPIC_LEN+5]; char mLwtTopic[MQTT_TOPIC_LEN+5];
const char *mDevName, *mVersion; const char *mDevName, *mVersion;
char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT 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. // global buffer for mqtt topic. Used when publishing mqtt messages.
char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1]; char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1];
char mSubTopic[32 + MAX_NAME_LENGTH + 1]; char mSubTopic[32 + MAX_NAME_LENGTH + 1];
char mVal[40]; char mVal[100];
discovery_t mDiscovery; discovery_t mDiscovery;
}; };

2
src/publisher/pubMqttDefs.h

@ -48,7 +48,6 @@ enum {
MQTT_SUNSET, MQTT_SUNSET,
MQTT_COMM_START, MQTT_COMM_START,
MQTT_COMM_STOP, MQTT_COMM_STOP,
MQTT_DIS_NIGHT_COMM,
MQTT_COMM_DISABLED, MQTT_COMM_DISABLED,
MQTT_COMM_DIS_TS, MQTT_COMM_DIS_TS,
MQTT_VERSION, MQTT_VERSION,
@ -69,7 +68,6 @@ const char* const subtopics[] PROGMEM = {
"sunset", "sunset",
"comm_start", "comm_start",
"comm_stop", "comm_stop",
"dis_night_comm",
"comm_disabled", "comm_disabled",
"comm_dis_ts", "comm_dis_ts",
"version", "version",

39
src/publisher/pubMqttIvData.h

@ -28,7 +28,7 @@ class PubMqttIvData {
mState = IDLE; mState = IDLE;
mZeroValues = false; mZeroValues = false;
memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4); memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * sizeof(uint32_t));
mRTRDataHasBeenSent = false; mRTRDataHasBeenSent = false;
mTable[IDLE] = &PubMqttIvData::stateIdle; mTable[IDLE] = &PubMqttIvData::stateIdle;
@ -102,10 +102,18 @@ class PubMqttIvData {
mPos = 0; mPos = 0;
if(found) { if(found) {
record_t<> *rec = mIv->getRecordStruct(mCmd); record_t<> *rec = mIv->getRecordStruct(mCmd);
if((RealTimeRunData_Debug == mCmd) && mIv->getLastTs(rec) != 0 ) { //workaround for startup. Suspect, mCmd might cause to much messages....
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", mIv->config->name); snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", mIv->config->name);
snprintf(mVal, 40, "%d", mIv->getLastTs(rec)); snprintf(mVal, 40, "%d", mIv->getLastTs(rec));
mPublish(mSubTopic, mVal, true, QOS_0); mPublish(mSubTopic, mVal, true, QOS_0);
if((mIv->ivGen == IV_HMS) || (mIv->ivGen == IV_HMT)) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch0/rssi", mIv->config->name);
snprintf(mVal, 40, "%d", mIv->rssi);
mPublish(mSubTopic, mVal, false, QOS_0);
}
}
mIv->isProducing(); // recalculate status mIv->isProducing(); // recalculate status
mState = SEND_DATA; mState = SEND_DATA;
} else if(mSendTotals && mTotalFound) } else if(mSendTotals && mTotalFound)
@ -119,6 +127,11 @@ class PubMqttIvData {
void stateSend() { void stateSend() {
record_t<> *rec = mIv->getRecordStruct(mCmd); record_t<> *rec = mIv->getRecordStruct(mCmd);
if(rec == NULL) {
if (mCmd != GetLossRate)
DPRINT(DBG_WARN, "unknown record to publish!");
return;
}
uint32_t lastTs = mIv->getLastTs(rec); uint32_t lastTs = mIv->getLastTs(rec);
bool pubData = (lastTs > 0); bool pubData = (lastTs > 0);
if (mCmd == RealTimeRunData_Debug) if (mCmd == RealTimeRunData_Debug)
@ -133,7 +146,8 @@ class PubMqttIvData {
// calculate total values for RealTimeRunData_Debug // calculate total values for RealTimeRunData_Debug
if (CH0 == rec->assign[mPos].ch) { if (CH0 == rec->assign[mPos].ch) {
if(mIv->status > InverterStatus::STARTING) { if(mIv->getStatus() > InverterStatus::STARTING) {
if(mIv->config->add2Total) {
mTotalFound = true; mTotalFound = true;
switch (rec->assign[mPos].fieldId) { switch (rec->assign[mPos].fieldId) {
case FLD_PAC: case FLD_PAC:
@ -154,6 +168,7 @@ class PubMqttIvData {
mTotal[3] += mIv->getValue(mPos, rec); mTotal[3] += mIv->getValue(mPos, rec);
break; break;
} }
}
} else } else
mAllTotalFound = false; mAllTotalFound = false;
} }
@ -170,12 +185,25 @@ class PubMqttIvData {
mPublish(mSubTopic, mVal, retained, qos); mPublish(mSubTopic, mVal, retained, qos);
} }
mPos++; mPos++;
} else } else {
sendRadioStat(rec->length);
mState = FIND_NXT_IV; mState = FIND_NXT_IV;
}
} else } else
mState = FIND_NXT_IV; mState = FIND_NXT_IV;
} }
inline void sendRadioStat(uint8_t start) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/radio_stat", mIv->config->name);
snprintf(mVal, 100, "{\"tx\":%d,\"success\":%d,\"fail\":%d,\"no_answer\":%d,\"retransmits\":%d}",
mIv->radioStatistics.txCnt,
mIv->radioStatistics.rxSuccess,
mIv->radioStatistics.rxFail,
mIv->radioStatistics.rxFailNoAnser,
mIv->radioStatistics.retransmits);
mPublish(mSubTopic, mVal, false, QOS_0);
}
void stateSendTotals() { void stateSendTotals() {
uint8_t fieldId; uint8_t fieldId;
mRTRDataHasBeenSent = true; mRTRDataHasBeenSent = true;
@ -213,7 +241,8 @@ class PubMqttIvData {
} else { } else {
mSendList->pop(); mSendList->pop();
mZeroValues = false; mZeroValues = false;
mState = START; mPos = 0;
mState = IDLE;
} }
} }
@ -234,7 +263,7 @@ class PubMqttIvData {
bool mRTRDataHasBeenSent; bool mRTRDataHasBeenSent;
char mSubTopic[32 + MAX_NAME_LENGTH + 1]; char mSubTopic[32 + MAX_NAME_LENGTH + 1];
char mVal[40]; char mVal[100];
bool mZeroValues; // makes sure that yield day is sent even if no inverter is online bool mZeroValues; // makes sure that yield day is sent even if no inverter is online
std::queue<sendListCmdIv> *mSendList; std::queue<sendListCmdIv> *mSendList;

12
src/utils/dbg.h

@ -148,7 +148,7 @@
#define PVERBLN(str) #define PVERBLN(str)
#endif #endif
#define DPRINT(level, str) ({\ #define DPRINT(level, str) do {\
switch(level) {\ switch(level) {\
case DBG_ERROR: PERR(str); break; \ case DBG_ERROR: PERR(str); break; \
case DBG_WARN: PWARN(str); break; \ case DBG_WARN: PWARN(str); break; \
@ -156,13 +156,13 @@
case DBG_DEBUG: PDBG(str); break; \ case DBG_DEBUG: PDBG(str); break; \
default: PVERB(str); break; \ default: PVERB(str); break; \
}\ }\
}) } while (0)
#define DPRINT_IVID(level, id) ({\ #define DPRINT_IVID(level, id) do {\
DPRINT(level, F("(#")); DBGPRINT(String(id)); DBGPRINT(F(") "));\ DPRINT(level, F("(#")); DBGPRINT(String(id)); DBGPRINT(F(") "));\
}) } while (0)
#define DPRINTLN(level, str) ({\ #define DPRINTLN(level, str) do {\
switch(level) {\ switch(level) {\
case DBG_ERROR: PERRLN(str); break; \ case DBG_ERROR: PERRLN(str); break; \
case DBG_WARN: PWARNLN(str); break; \ case DBG_WARN: PWARNLN(str); break; \
@ -170,7 +170,7 @@
case DBG_DEBUG: PDBGLN(str); break; \ case DBG_DEBUG: PDBGLN(str); break; \
default: PVERBLN(str); break; \ default: PVERBLN(str); break; \
}\ }\
}) } while (0)
/*class ahoyLog { /*class ahoyLog {

16
src/utils/helper.cpp

@ -70,6 +70,17 @@ namespace ah {
return String(str); return String(str);
} }
String getTimeStrMs(uint64_t t) {
char str[13];
if(0 == t)
sprintf(str, "n/a");
else {
t = (t + (millis() % 1000)) / 1000;
sprintf(str, "%02d:%02d:%02d.%03d", hour(t), minute(t), second(t), millis() % 1000);
}
return String(str);
}
uint64_t Serial2u64(const char *val) { uint64_t Serial2u64(const char *val) {
char tmp[3]; char tmp[3];
uint64_t ret = 0ULL; uint64_t ret = 0ULL;
@ -86,9 +97,12 @@ namespace ah {
return ret; return ret;
} }
void dumpBuf(uint8_t buf[], uint8_t len) { void dumpBuf(uint8_t buf[], uint8_t len, uint8_t firstRepl, uint8_t lastRepl) {
for(uint8_t i = 0; i < len; i++) { for(uint8_t i = 0; i < len; i++) {
if((i < firstRepl) || (i > lastRepl) || (0 == firstRepl))
DHEX(buf[i]); DHEX(buf[i]);
else
DBGPRINT(F(" *"));
DBGPRINT(" "); DBGPRINT(" ");
} }
DBGPRINTLN(""); DBGPRINTLN("");

11
src/utils/helper.h

@ -20,21 +20,21 @@ static Timezone gTimezone(CEST, CET);
#define CHECK_MASK(a,b) ((a & b) == b) #define CHECK_MASK(a,b) ((a & b) == b)
#define CP_U32_LittleEndian(buf, v) ({ \ #define CP_U32_LittleEndian(buf, v) do { \
uint8_t *b = buf; \ uint8_t *b = buf; \
b[0] = ((v >> 24) & 0xff); \ b[0] = ((v >> 24) & 0xff); \
b[1] = ((v >> 16) & 0xff); \ b[1] = ((v >> 16) & 0xff); \
b[2] = ((v >> 8) & 0xff); \ b[2] = ((v >> 8) & 0xff); \
b[3] = ((v ) & 0xff); \ b[3] = ((v ) & 0xff); \
}) } while (0)
#define CP_U32_BigEndian(buf, v) ({ \ #define CP_U32_BigEndian(buf, v) do { \
uint8_t *b = buf; \ uint8_t *b = buf; \
b[3] = ((v >> 24) & 0xff); \ b[3] = ((v >> 24) & 0xff); \
b[2] = ((v >> 16) & 0xff); \ b[2] = ((v >> 16) & 0xff); \
b[1] = ((v >> 8) & 0xff); \ b[1] = ((v >> 8) & 0xff); \
b[0] = ((v ) & 0xff); \ b[0] = ((v ) & 0xff); \
}) } while (0)
namespace ah { namespace ah {
void ip2Arr(uint8_t ip[], const char *ipStr); void ip2Arr(uint8_t ip[], const char *ipStr);
@ -44,8 +44,9 @@ namespace ah {
String getDateTimeStrShort(time_t t); String getDateTimeStrShort(time_t t);
String getDateTimeStrFile(time_t t); String getDateTimeStrFile(time_t t);
String getTimeStr(time_t t); String getTimeStr(time_t t);
String getTimeStrMs(uint64_t t);
uint64_t Serial2u64(const char *val); uint64_t Serial2u64(const char *val);
void dumpBuf(uint8_t buf[], uint8_t len); void dumpBuf(uint8_t buf[], uint8_t len, uint8_t firstRepl = 0, uint8_t lastRepl = 0);
} }
#endif /*__HELPER_H__*/ #endif /*__HELPER_H__*/

12
src/utils/improv.h

@ -39,7 +39,7 @@ class Improv {
if(!checkPaket(&buf[0], len, [this](uint8_t type, uint8_t buf[], uint8_t len) { if(!checkPaket(&buf[0], len, [this](uint8_t type, uint8_t buf[], uint8_t len) {
parsePayload(type, buf, len); parsePayload(type, buf, len);
})) { })) {
DBGPRINTLN(F("check paket failed")); DBGPRINTLN(F("check packet failed"));
} }
dumpBuf(buf, len); dumpBuf(buf, len);
} }
@ -100,7 +100,7 @@ class Improv {
if(0 != strncmp((char*)buf, "IMPROV", 6)) if(0 != strncmp((char*)buf, "IMPROV", 6))
return false; return false;
// verison check (only version 1 is supported!) // version check (only version 1 is supported!)
if(0x01 != buf[6]) if(0x01 != buf[6])
return false; return false;
@ -124,7 +124,7 @@ class Improv {
void sendDevInfo(void) { void sendDevInfo(void) {
uint8_t buf[50]; uint8_t buf[50];
buf[7] = TYPE_RPC_RESPONSE; buf[7] = TYPE_RPC_RESPONSE;
buf[9] = GET_DEVICE_INFO; // repsonse to cmd buf[9] = GET_DEVICE_INFO; // response to cmd
uint8_t p = 11; uint8_t p = 11;
// firmware name // firmware name
p += char2Improv("AhoyDTU", &buf[p]); p += char2Improv("AhoyDTU", &buf[p]);
@ -140,7 +140,7 @@ class Improv {
p += char2Improv(mDevName, &buf[p]); p += char2Improv(mDevName, &buf[p]);
buf[10] = p - 11; // sub length buf[10] = p - 11; // sub length
buf[8] = p - 9; // paket length buf[8] = p - 9; // packet length
sendPaket(buf, p); sendPaket(buf, p);
} }
@ -157,7 +157,7 @@ class Improv {
uint8_t buf[50]; uint8_t buf[50];
buf[7] = TYPE_RPC_RESPONSE; buf[7] = TYPE_RPC_RESPONSE;
buf[9] = GET_WIFI_NETWORKS; // repsonse to cmd buf[9] = GET_WIFI_NETWORKS; // response to cmd
uint8_t p = 11; uint8_t p = 11;
JsonArray arr = obj[F("networks")]; JsonArray arr = obj[F("networks")];
@ -170,7 +170,7 @@ class Improv {
p += char2Improv(String(arr[i][F("rssi")]).c_str(), &buf[p]); p += char2Improv(String(arr[i][F("rssi")]).c_str(), &buf[p]);
buf[10] = p - 11; // sub length buf[10] = p - 11; // sub length
buf[8] = p - 9; // paket length buf[8] = p - 9; // packet length
sendPaket(buf, p); sendPaket(buf, p);
} }

11
src/utils/scheduler.h

@ -34,6 +34,7 @@ namespace ah {
void setup(bool directStart) { void setup(bool directStart) {
mUptime = 0; mUptime = 0;
mTimestamp = (directStart) ? 1 : 0; mTimestamp = (directStart) ? 1 : 0;
mTsMillis = 0;
mMax = 0; mMax = 0;
mPrevMillis = millis(); mPrevMillis = millis();
resetTicker(); resetTicker();
@ -59,8 +60,10 @@ namespace ah {
} }
mUptime += mDiffSeconds; mUptime += mDiffSeconds;
if(0 != mTimestamp) if(0 != mTimestamp) {
mTimestamp += mDiffSeconds; mTimestamp += mDiffSeconds;
mTsMillis = mMillis % 1000;
}
checkTicker(); checkTicker();
} }
@ -77,6 +80,7 @@ namespace ah {
virtual void setTimestamp(uint32_t ts) { virtual void setTimestamp(uint32_t ts) {
mTimestamp = ts; mTimestamp = ts;
mTsMillis = millis() % 1000;
} }
bool resetEveryById(uint8_t id) { bool resetEveryById(uint8_t id) {
@ -90,10 +94,6 @@ namespace ah {
return mUptime; return mUptime;
} }
uint32_t getTimestamp(void) {
return mTimestamp;
}
inline void resetTicker(void) { inline void resetTicker(void) {
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) for (uint8_t i = 0; i < MAX_NUM_TICKER; i++)
mTickerInUse[i] = false; mTickerInUse[i] = false;
@ -118,6 +118,7 @@ namespace ah {
protected: protected:
uint32_t mTimestamp; uint32_t mTimestamp;
uint32_t mUptime; uint32_t mUptime;
uint16_t mTsMillis;
private: private:
inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) { inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) {

9
src/utils/spiPatcher.cpp

@ -0,0 +1,9 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#if defined(ESP32)
#include "spiPatcher.h"
SpiPatcher *SpiPatcher::mInstance = nullptr;
#endif

88
src/utils/spiPatcher.h

@ -0,0 +1,88 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __SPI_PATCHER_H__
#define __SPI_PATCHER_H__
#pragma once
#if defined(ESP32)
#include "spiPatcherHandle.h"
#include <driver/spi_master.h>
#include <freertos/semphr.h>
class SpiPatcher {
protected:
SpiPatcher(spi_host_device_t dev) :
mHostDevice(dev), mCurHandle(nullptr) {
// Use binary semaphore instead of mutex for performance reasons
mutex = xSemaphoreCreateBinaryStatic(&mutex_buffer);
xSemaphoreGive(mutex);
spi_bus_config_t buscfg = {
.mosi_io_num = -1,
.miso_io_num = -1,
.sclk_io_num = -1,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = SOC_SPI_MAXIMUM_BUFFER_SIZE,
.flags = 0,
.intr_flags = 0
};
ESP_ERROR_CHECK(spi_bus_initialize(mHostDevice, &buscfg, SPI_DMA_DISABLED));
}
public:
SpiPatcher(SpiPatcher &other) = delete;
void operator=(const SpiPatcher &) = delete;
static SpiPatcher* getInstance(spi_host_device_t dev) {
if(nullptr == mInstance)
mInstance = new SpiPatcher(dev);
return mInstance;
}
~SpiPatcher() { vSemaphoreDelete(mutex); }
spi_host_device_t getDevice() {
return mHostDevice;
}
inline void request(SpiPatcherHandle* handle) {
xSemaphoreTake(mutex, portMAX_DELAY);
if (mCurHandle != handle) {
if (mCurHandle) {
mCurHandle->unpatch();
}
mCurHandle = handle;
if (mCurHandle) {
mCurHandle->patch();
}
}
}
inline void release() {
xSemaphoreGive(mutex);
}
protected:
static SpiPatcher *mInstance;
private:
const spi_host_device_t mHostDevice;
SpiPatcherHandle* mCurHandle;
SemaphoreHandle_t mutex;
StaticSemaphore_t mutex_buffer;
};
#endif /*ESP32*/
#endif /*__SPI_PATCHER_H__*/

17
src/utils/spiPatcherHandle.h

@ -0,0 +1,17 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __SPI_PATCHER_HANDLE_H__
#define __SPI_PATCHER_HANDLE_H__
#pragma once
class SpiPatcherHandle {
public:
virtual ~SpiPatcherHandle() {}
virtual void patch() = 0;
virtual void unpatch() = 0;
};
#endif /*__SPI_PATCHER_HANDLE_H__*/

99
src/utils/syslog.cpp

@ -0,0 +1,99 @@
#include <algorithm>
#include "syslog.h"
#ifdef ENABLE_SYSLOG
#define SYSLOG_MAX_PACKET_SIZE 256
//-----------------------------------------------------------------------------
void DbgSyslog::setup(settings_t *config) {
mConfig = config;
// Syslog callback overrides web-serial callback
registerDebugCb(std::bind(&DbgSyslog::syslogCb, this, std::placeholders::_1)); // dbg.h
}
//-----------------------------------------------------------------------------
void DbgSyslog::syslogCb (String msg)
{
if (!mSyslogIP.isSet()) {
// use WiFi.hostByName to DNS lookup for IPAddress of syslog server
if (WiFi.status() == WL_CONNECTED) {
WiFi.hostByName(SYSLOG_HOST,mSyslogIP);
}
}
if (!mSyslogIP.isSet()) {
return;
}
uint16_t msgLength = msg.length();
uint16_t msgPos = 0;
do {
uint16_t charsToCopy = std::min(msgLength-msgPos,SYSLOG_BUF_SIZE - mSyslogBufFill);
while (charsToCopy > 0) {
mSyslogBuffer[mSyslogBufFill] = msg[msgPos];
msgPos++;
mSyslogBufFill++;
charsToCopy--;
}
mSyslogBuffer[mSyslogBufFill] = '\0';
bool isBufferFull = (mSyslogBufFill == SYSLOG_BUF_SIZE);
bool isEolFound = false;
if (mSyslogBufFill >= 2) {
isEolFound = (mSyslogBuffer[mSyslogBufFill-2] == '\r' && mSyslogBuffer[mSyslogBufFill-1] == '\n');
}
// Get severity from input message
if (msgLength >= 2) {
if (':' == msg[1]) {
switch(msg[0]) {
case 'E': mSyslogSeverity = PRI_ERROR; break;
case 'W': mSyslogSeverity = PRI_WARNING; break;
case 'I': mSyslogSeverity = PRI_INFO; break;
case 'D': mSyslogSeverity = PRI_DEBUG; break;
default: mSyslogSeverity = PRI_NOTICE; break;
}
}
}
if (isBufferFull || isEolFound) {
// Send mSyslogBuffer in chunks because mSyslogBuffer is larger than syslog packet size
int packetStart = 0;
int packetSize = 122; // syslog payload depends also on hostname and app
char saveChar;
if (isEolFound) {
mSyslogBuffer[mSyslogBufFill-2]=0; // skip \r\n
}
while(packetStart < mSyslogBufFill) {
saveChar = mSyslogBuffer[packetStart+packetSize];
mSyslogBuffer[packetStart+packetSize] = 0;
log(mConfig->sys.deviceName,SYSLOG_FACILITY, mSyslogSeverity, &mSyslogBuffer[packetStart]);
mSyslogBuffer[packetStart+packetSize] = saveChar;
packetStart += packetSize;
}
mSyslogBufFill = 0;
}
} while (msgPos < msgLength); // Message not completely processed
}
//-----------------------------------------------------------------------------
void DbgSyslog::log(const char *hostname, uint8_t facility, uint8_t severity, char* msg) {
// The PRI value is an integer number which calculates by the following metric:
uint8_t priority = (8 * facility) + severity;
// This is a unit8 instead of a char because that's what udp.write() wants
uint8_t buffer[SYSLOG_MAX_PACKET_SIZE];
int len = snprintf((char*)buffer, SYSLOG_MAX_PACKET_SIZE, "<%d>%s %s: %s", priority, hostname, SYSLOG_APP, msg);
//printf("syslog::log %s\n",mSyslogIP.toString().c_str());
//printf("syslog::log %d %s\n",len,buffer);
// Send the raw UDP packet
mSyslogUdp.beginPacket(mSyslogIP, SYSLOG_PORT);
mSyslogUdp.write(buffer, len);
mSyslogUdp.endPacket();
}
#endif

54
src/utils/syslog.h

@ -0,0 +1,54 @@
#ifndef __SYSLOG_H__
#define __SYSLOG_H__
#ifdef ESP8266
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include <WiFiUdp.h>
#include "../config/config.h"
#include "../config/settings.h"
#ifdef ENABLE_SYSLOG
#define SYSLOG_BUF_SIZE 255
#define PRI_EMERGENCY 0
#define PRI_ALERT 1
#define PRI_CRITICAL 2
#define PRI_ERROR 3
#define PRI_WARNING 4
#define PRI_NOTICE 5
#define PRI_INFO 6
#define PRI_DEBUG 7
#define FAC_USER 1
#define FAC_LOCAL0 16
#define FAC_LOCAL1 17
#define FAC_LOCAL2 18
#define FAC_LOCAL3 19
#define FAC_LOCAL4 20
#define FAC_LOCAL5 21
#define FAC_LOCAL6 22
#define FAC_LOCAL7 23
class DbgSyslog {
public:
void setup (settings_t *config);
void syslogCb(String msg);
void log(const char *hostname, uint8_t facility, uint8_t severity, char* msg);
private:
WiFiUDP mSyslogUdp;
IPAddress mSyslogIP;
settings_t *mConfig;
char mSyslogBuffer[SYSLOG_BUF_SIZE+1];
uint16_t mSyslogBufFill = 0;
int mSyslogSeverity = PRI_NOTICE;
};
#endif /*ENABLE_SYSLOG*/
#endif /*__SYSLOG_H__*/

126
src/utils/timemonitor.h

@ -0,0 +1,126 @@
//-----------------------------------------------------------
// You69Man, 2023
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// version 2 as published by the Free Software Foundation.
//-----------------------------------------------------------
/**
* @file timemonitor.h
*
* Class declaration for TimeMonitor
*/
#ifndef __TIMEMONITOR_H__
#define __TIMEMONITOR_H__
#include <Arduino.h>
class TimeMonitor {
public:
/**
* A constructor for initializing a TimeMonitor
* @note TimeMonitor witch default constructor is stopped
*/
TimeMonitor(void) {}
/**
* A constructor for initializing a TimeMonitor
* @param timeout timeout in ms
* @param start (optional) if true, start TimeMonitor immediately
* @note TimeMonitor witch default constructor is stopped
*/
TimeMonitor(uint32_t timeout, bool start = false) {
if (start)
startTimeMonitor(timeout);
else
configureTimeMonitor(timeout);
}
/**
* Start the TimeMonitor with new timeout configuration
* @param timeout timout in ms
*/
void startTimeMonitor(uint32_t timeout) {
mStartTime = millis();
mTimeout = timeout;
mStarted = true;
}
/**
* Restart the TimeMonitor with already set timeout configuration
* @note returns nothing
*/
void reStartTimeMonitor(void) {
mStartTime = millis();
mStarted = true;
}
/**
* Configure the TimeMonitor to new timeout configuration
* @param timeout timeout in ms
* @note This doesn't restart an already running TimeMonitor.
* If timer is already running and new timeout is longer that current timeout runtime of the running timer is expanded
* If timer is already running and new timeout is shorter that current timeout this can immediately lead to a timeout
*/
void configureTimeMonitor(uint32_t timeout) {
mTimeout = timeout;
}
/**
* Stop the TimeMonitor
*/
void stopTimeMonitor(void) {
mStarted = false;
}
/**
* Get timeout status of the TimeMonitor
* @return bool
* true: TimeMonitor already timed out
* false: TimeMonitor still in time or TimeMonitor was stopped
*/
bool isTimeout(void) {
if ((mStarted) && (millis() - mStartTime >= mTimeout))
return true;
else
return false;
}
/**
* Get timeout configuration of the TimeMonitor
* @return uint32_t timeout value in ms
*/
uint32_t getTimeout(void) const {
return mTimeout;
}
/**
* Get residual time of the TimeMonitor until timeout
* @return uint32_t residual time until timeout in ms
* @note in case of a stopped TimeMonitor residual time is always 0xFFFFFFFFUL
* in case of a timed out TimeMonitor residual time is always 0UL (zero)
*/
uint32_t getResidualTime(void) const {
uint32_t delayed = millis() - mStartTime;
return(mStarted ? (delayed < mTimeout ? mTimeout - delayed : 0UL) : 0xFFFFFFFFUL);
}
/**
* Get overall run time of the TimeMonitor
* @return uint32_t residual time until timeout in ms
* @note in case of a stopped TimeMonitor residual time is always 0xFFFFFFFFUL
* in case of a timed out TimeMonitor residual time is always 0UL (zero)
*/
uint32_t getRunTime(void) const {
return(mStarted ? millis() - mStartTime : 0UL);
}
private:
uint32_t mStartTime = 0UL; // start time of the TimeMonitor
uint32_t mTimeout = 0UL; // timeout configuration of the TimeMonitor
bool mStarted = false; // start/stop state of the TimeMonitor
};
#endif

381
src/web/RestApi.h

@ -33,7 +33,7 @@ const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_Y
const uint8_t acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP}; const uint8_t acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP};
const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP}; const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP};
template<class HMSYSTEM, class HMRADIO> template<class HMSYSTEM>
class RestApi { class RestApi {
public: public:
RestApi() { RestApi() {
@ -41,18 +41,20 @@ class RestApi {
mHeapFree = 0; mHeapFree = 0;
mHeapFreeBlk = 0; mHeapFreeBlk = 0;
mHeapFrag = 0; mHeapFrag = 0;
nr = 0;
} }
void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, AsyncWebServer *srv, settings_t *config) { void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) {
mApp = app; mApp = app;
mSrv = srv; mSrv = srv;
mSys = sys; mSys = sys;
mRadio = radio; mRadioNrf = (HmRadio<>*)mApp->getRadioObj(true);
#if defined(ESP32)
mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false);
#endif
mConfig = config; mConfig = config;
mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1));
mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody(
std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5));
mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1));
mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1));
} }
@ -62,9 +64,6 @@ class RestApi {
} }
void ctrlRequest(JsonObject obj) { void ctrlRequest(JsonObject obj) {
/*char out[128];
serializeJson(obj, out, 128);
DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/
DynamicJsonDocument json(128); DynamicJsonDocument json(128);
JsonObject dummy = json.as<JsonObject>(); JsonObject dummy = json.as<JsonObject>();
if(obj[F("path")] == "ctrl") if(obj[F("path")] == "ctrl")
@ -75,6 +74,8 @@ class RestApi {
private: private:
void onApi(AsyncWebServerRequest *request) { void onApi(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, String("onApi: ") + String((uint16_t)request->method())); // 1 == Get, 3 == POST
mHeapFree = ESP.getFreeHeap(); mHeapFree = ESP.getFreeHeap();
#ifndef ESP32 #ifndef ESP32
mHeapFreeBlk = ESP.getMaxFreeBlockSize(); mHeapFreeBlk = ESP.getMaxFreeBlockSize();
@ -89,10 +90,13 @@ class RestApi {
else if(path == "html/logout") getHtmlLogout(request, root); else if(path == "html/logout") getHtmlLogout(request, root);
else if(path == "html/reboot") getHtmlReboot(request, root); else if(path == "html/reboot") getHtmlReboot(request, root);
else if(path == "html/save") getHtmlSave(request, root); else if(path == "html/save") getHtmlSave(request, root);
else if(path == "html/erase") getHtmlErase(request, root);
else if(path == "html/erasetrue") getHtmlEraseTrue(request, root);
else if(path == "html/factory") getHtmlFactory(request, root);
else if(path == "html/factorytrue") getHtmlFactoryTrue(request, root);
else if(path == "system") getSysInfo(request, root); else if(path == "system") getSysInfo(request, root);
else if(path == "generic") getGeneric(request, root); else if(path == "generic") getGeneric(request, root);
else if(path == "reboot") getReboot(request, root); else if(path == "reboot") getReboot(request, root);
else if(path == "statistics") getStatistics(root);
else if(path == "inverter/list") getInverterList(root); else if(path == "inverter/list") getInverterList(root);
else if(path == "index") getIndex(request, root); else if(path == "index") getIndex(request, root);
else if(path == "setup") getSetup(request, root); else if(path == "setup") getSetup(request, root);
@ -111,6 +115,14 @@ class RestApi {
getInverter(root, request->url().substring(17).toInt()); getInverter(root, request->url().substring(17).toInt());
else if(path.substring(0, 15) == "inverter/alarm/") else if(path.substring(0, 15) == "inverter/alarm/")
getIvAlarms(root, request->url().substring(20).toInt()); getIvAlarms(root, request->url().substring(20).toInt());
else if(path.substring(0, 17) == "inverter/version/")
getIvVersion(root, request->url().substring(22).toInt());
else if(path.substring(0, 19) == "inverter/radiostat/")
getIvStatistis(root, request->url().substring(24).toInt());
else if(path.substring(0, 16) == "inverter/pwrack/")
getIvPowerLimitAck(root, request->url().substring(21).toInt());
else if(path.substring(0, 14) == "inverter/grid/")
getGridProfile(root, request->url().substring(19).toInt());
else else
getNotFound(root, F("http://") + request->host() + F("/api/")); getNotFound(root, F("http://") + request->host() + F("/api/"));
} }
@ -124,16 +136,36 @@ class RestApi {
void onApiPost(AsyncWebServerRequest *request) { void onApiPost(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, "onApiPost"); DPRINTLN(DBG_VERBOSE, "onApiPost");
#if defined(ETHERNET)
// workaround for AsyncWebServer_ESP32_W5500, because it can't distinguish
// between HTTP_GET and HTTP_POST if both are registered
if(request->method() == HTTP_GET)
onApi(request);
#endif
} }
void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
DPRINTLN(DBG_VERBOSE, "onApiPostBody"); DPRINTLN(DBG_VERBOSE, "onApiPostBody");
DynamicJsonDocument json(200);
AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
JsonObject root = response->getRoot();
DeserializationError err = deserializeJson(json, (const char *)data, len); if(0 == index) {
if(NULL != mTmpBuf)
delete[] mTmpBuf;
mTmpBuf = new uint8_t[total+1];
mTmpSize = total;
}
if(mTmpSize >= (len + index))
memcpy(&mTmpBuf[index], data, len);
if((len + index) != total)
return; // not last frame - nothing to do
DynamicJsonDocument json(1000);
DeserializationError err = deserializeJson(json, (const char *)mTmpBuf, mTmpSize);
JsonObject obj = json.as<JsonObject>(); JsonObject obj = json.as<JsonObject>();
AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
JsonObject root = response->getRoot();
root[F("success")] = (err) ? false : true; root[F("success")] = (err) ? false : true;
if(!err) { if(!err) {
String path = request->url().substring(5); String path = request->url().substring(5);
@ -145,10 +177,10 @@ class RestApi {
root[F("success")] = false; root[F("success")] = false;
root[F("error")] = "Path not found: " + path; root[F("error")] = "Path not found: " + path;
} }
} } else {
else {
switch (err.code()) { switch (err.code()) {
case DeserializationError::Ok: break; case DeserializationError::Ok: break;
case DeserializationError::IncompleteInput: root[F("error")] = F("Incomplete input"); break;
case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break;
case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break;
default: root[F("error")] = F("Deserialization failed"); break; default: root[F("error")] = F("Deserialization failed"); break;
@ -157,6 +189,8 @@ class RestApi {
response->setLength(); response->setLength();
request->send(response); request->send(response);
delete[] mTmpBuf;
mTmpBuf = NULL;
} }
void getNotFound(JsonObject obj, String url) { void getNotFound(JsonObject obj, String url) {
@ -164,7 +198,7 @@ class RestApi {
ep[F("inverter/list")] = url + F("inverter/list"); ep[F("inverter/list")] = url + F("inverter/list");
ep[F("inverter/id/0")] = url + F("inverter/id/0"); ep[F("inverter/id/0")] = url + F("inverter/id/0");
ep[F("inverter/alarm/0")] = url + F("inverter/alarm/0"); ep[F("inverter/alarm/0")] = url + F("inverter/alarm/0");
ep[F("statistics")] = url + F("statistics"); ep[F("inverter/version/0")] = url + F("inverter/version/0");
ep[F("generic")] = url + F("generic"); ep[F("generic")] = url + F("generic");
ep[F("index")] = url + F("index"); ep[F("index")] = url + F("index");
ep[F("setup")] = url + F("setup"); ep[F("setup")] = url + F("setup");
@ -193,6 +227,16 @@ class RestApi {
tmp.remove(i, tmp.indexOf("\"", i)-i); tmp.remove(i, tmp.indexOf("\"", i)-i);
} }
} }
i = 0;
// convert all serial numbers to hexadecimal
while (i != -1) {
i = tmp.indexOf("\"sn\":", i);
if(-1 != i) {
i+=5;
String sn = tmp.substring(i, tmp.indexOf("\"", i)-1);
tmp.replace(sn, String(atoll(sn.c_str()), HEX));
}
}
response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp);
} }
@ -245,14 +289,17 @@ class RestApi {
obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb
getGeneric(request, obj); getGeneric(request, obj);
getRadioNrf(obj.createNestedObject(F("radio"))); getRadioNrf(obj.createNestedObject(F("radioNrf")));
getStatistics(obj.createNestedObject(F("statistics"))); #if defined(ESP32)
getRadioCmtInfo(obj.createNestedObject(F("radioCmt")));
#endif
getMqttInfo(obj.createNestedObject(F("mqtt")));
#if defined(ESP32) #if defined(ESP32)
obj[F("heap_total")] = ESP.getHeapSize();
obj[F("chip_revision")] = ESP.getChipRevision(); obj[F("chip_revision")] = ESP.getChipRevision();
obj[F("chip_model")] = ESP.getChipModel(); obj[F("chip_model")] = ESP.getChipModel();
obj[F("chip_cores")] = ESP.getChipCores(); obj[F("chip_cores")] = ESP.getChipCores();
obj[F("heap_total")] = ESP.getHeapSize();
//obj[F("core_version")] = F("n/a"); //obj[F("core_version")] = F("n/a");
//obj[F("flash_size")] = F("n/a"); //obj[F("flash_size")] = F("n/a");
//obj[F("heap_frag")] = F("n/a"); //obj[F("heap_frag")] = F("n/a");
@ -263,10 +310,10 @@ class RestApi {
//obj[F("chip_revision")] = F("n/a"); //obj[F("chip_revision")] = F("n/a");
//obj[F("chip_model")] = F("n/a"); //obj[F("chip_model")] = F("n/a");
//obj[F("chip_cores")] = F("n/a"); //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")] = mHeapFrag; obj[F("heap_frag")] = mHeapFrag;
obj[F("max_free_blk")] = mHeapFreeBlk; obj[F("max_free_blk")] = mHeapFreeBlk;
obj[F("core_version")] = ESP.getCoreVersion();
obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb
obj[F("reboot_reason")] = ESP.getResetReason(); obj[F("reboot_reason")] = ESP.getResetReason();
#endif #endif
//obj[F("littlefs_total")] = LittleFS.totalBytes(); //obj[F("littlefs_total")] = LittleFS.totalBytes();
@ -280,28 +327,71 @@ class RestApi {
void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) { void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) {
getSysInfo(request, obj.createNestedObject(F("system"))); getSysInfo(request, obj.createNestedObject(F("system")));
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">Factory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>"); obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">AhoyFactory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>");
} }
void getHtmlLogout(AsyncWebServerRequest *request, JsonObject obj) { void getHtmlLogout(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("refresh")] = 3; obj[F("refresh")] = 3;
obj[F("refresh_url")] = "/"; obj[F("refresh_url")] = "/";
obj[F("html")] = F("succesfully logged out"); obj[F("html")] = F("successfully logged out");
} }
void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) { void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
obj[F("refresh")] = 5;
#else
obj[F("refresh")] = 20; obj[F("refresh")] = 20;
#endif
obj[F("refresh_url")] = "/"; obj[F("refresh_url")] = "/";
obj[F("html")] = F("rebooting ..."); obj[F("html")] = F("rebooting ...");
} }
void getHtmlSave(AsyncWebServerRequest *request, JsonObject obj) { void getHtmlSave(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
obj["pending"] = (bool)mApp->getSavePending(); obj[F("pending")] = (bool)mApp->getSavePending();
obj["success"] = (bool)mApp->getLastSaveSucceed(); obj[F("success")] = (bool)mApp->getLastSaveSucceed();
obj["reboot"] = (bool)mApp->getShouldReboot(); obj[F("reboot")] = (bool)mApp->getShouldReboot();
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
obj[F("reload")] = 5;
#else
obj[F("reload")] = 20;
#endif
}
void getHtmlErase(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("html")] = F("Erase settings (not WiFi)? <a class=\"btn\" href=\"/erasetrue\">yes</a> <a class=\"btn\" href=\"/\">no</a>");
}
void getHtmlEraseTrue(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic")));
mApp->eraseSettings(false);
mApp->setRebootFlag();
obj[F("html")] = F("Erase settings: success");
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
obj[F("reload")] = 5;
#else
obj[F("reload")] = 20;
#endif
}
void getHtmlFactory(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("html")] = F("Factory reset? <a class=\"btn\" href=\"/factorytrue\">yes</a> <a class=\"btn\" href=\"/\">no</a>");
}
void getHtmlFactoryTrue(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic")));
mApp->eraseSettings(true);
mApp->setRebootFlag();
obj[F("html")] = F("Factory reset: success");
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
obj[F("reload")] = 5;
#else
obj[F("reload")] = 20;
#endif
} }
void getReboot(AsyncWebServerRequest *request, JsonObject obj) { void getReboot(AsyncWebServerRequest *request, JsonObject obj) {
@ -311,14 +401,28 @@ class RestApi {
obj[F("html")] = F("reboot. Autoreload after 10 seconds"); obj[F("html")] = F("reboot. Autoreload after 10 seconds");
} }
void getStatistics(JsonObject obj) { void getIvStatistis(JsonObject obj, uint8_t id) {
statistics_t *stat = mApp->getStatistics(); Inverter<> *iv = mSys->getInverterByPos(id);
obj[F("rx_success")] = stat->rxSuccess; if(NULL == iv) {
obj[F("rx_fail")] = stat->rxFail; obj[F("error")] = F("inverter not found!");
obj[F("rx_fail_answer")] = stat->rxFailNoAnser; return;
obj[F("frame_cnt")] = stat->frmCnt; }
obj[F("tx_cnt")] = mRadio->mSendCnt; obj[F("name")] = String(iv->config->name);
obj[F("retransmits")] = mRadio->mRetransmits; obj[F("rx_success")] = iv->radioStatistics.rxSuccess;
obj[F("rx_fail")] = iv->radioStatistics.rxFail;
obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnser;
obj[F("frame_cnt")] = iv->radioStatistics.frmCnt;
obj[F("tx_cnt")] = iv->radioStatistics.txCnt;
obj[F("retransmits")] = iv->radioStatistics.retransmits;
}
void getIvPowerLimitAck(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) {
obj[F("error")] = F("inverter not found!");
return;
}
obj["ack"] = (bool)iv->powerLimitAck;
} }
void getInverterList(JsonObject obj) { void getInverterList(JsonObject obj) {
@ -327,14 +431,25 @@ class RestApi {
Inverter<> *iv; Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i); iv = mSys->getInverterByPos(i);
if(NULL != iv) { if(NULL == iv)
continue;
JsonObject obj2 = invArr.createNestedObject(); JsonObject obj2 = invArr.createNestedObject();
obj2[F("enabled")] = (bool)iv->config->enabled; obj2[F("enabled")] = (bool)iv->config->enabled;
obj2[F("id")] = i; obj2[F("id")] = i;
obj2[F("name")] = String(iv->config->name); obj2[F("name")] = String(iv->config->name);
obj2[F("serial")] = String(iv->config->serial.u64, HEX); obj2[F("serial")] = String(iv->config->serial.u64, HEX);
obj2[F("channels")] = iv->channels; obj2[F("channels")] = iv->channels;
obj2[F("version")] = String(iv->getFwVersion()); obj2[F("freq")] = iv->config->frequency;
obj2[F("disnightcom")] = (bool)iv->config->disNightCom;
obj2[F("add2total")] = (bool)iv->config->add2Total;
if(0xff == iv->config->powerLevel) {
if((IV_HMT == iv->ivGen) || (IV_HMS == iv->ivGen))
obj2[F("pa")] = 30; // 20dBm
else
obj2[F("pa")] = 1; // low
} else
obj2[F("pa")] = iv->config->powerLevel;
for(uint8_t j = 0; j < iv->channels; j ++) { for(uint8_t j = 0; j < iv->channels; j ++) {
obj2[F("ch_yield_cor")][j] = (double)iv->config->yieldCor[j]; obj2[F("ch_yield_cor")][j] = (double)iv->config->yieldCor[j];
@ -342,15 +457,16 @@ class RestApi {
obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j]; obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j];
} }
} }
} obj[F("interval")] = String(mConfig->inst.sendInterval);
obj[F("interval")] = String(mConfig->nrf.sendInterval);
obj[F("retries")] = String(mConfig->nrf.maxRetransPerPyld);
obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; obj[F("max_num_inverters")] = MAX_NUM_INVERTERS;
obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight;
obj[F("rstNAvail")] = (bool)mConfig->inst.rstValsNotAvail; obj[F("rstNotAvail")] = (bool)mConfig->inst.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop;
obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime; obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime;
obj[F("rdGrid")] = (bool)mConfig->inst.readGrid;
obj[F("rstMaxMid")] = (bool)mConfig->inst.rstMaxValsMidNight;
obj[F("yldEff")] = mConfig->inst.yieldEffiency; obj[F("yldEff")] = mConfig->inst.yieldEffiency;
obj[F("gap")] = mConfig->inst.gapMs;
} }
void getInverter(JsonObject obj, uint8_t id) { void getInverter(JsonObject obj, uint8_t id) {
@ -368,10 +484,12 @@ class RestApi {
obj[F("version")] = String(iv->getFwVersion()); obj[F("version")] = String(iv->getFwVersion());
obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit);
obj[F("power_limit_ack")] = iv->powerLimitAck; obj[F("power_limit_ack")] = iv->powerLimitAck;
obj[F("max_pwr")] = iv->getMaxPower();
obj[F("ts_last_success")] = rec->ts; obj[F("ts_last_success")] = rec->ts;
obj[F("generation")] = iv->ivGen; obj[F("generation")] = iv->ivGen;
obj[F("status")] = (uint8_t)iv->status; obj[F("status")] = (uint8_t)iv->getStatus();
obj[F("alarm_cnt")] = iv->alarmCnt; obj[F("alarm_cnt")] = iv->alarmCnt;
obj[F("rssi")] = iv->rssi;
JsonArray ch = obj.createNestedArray("ch"); JsonArray ch = obj.createNestedArray("ch");
@ -403,6 +521,16 @@ class RestApi {
} }
} }
void getGridProfile(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) {
return;
}
obj[F("name")] = String(iv->config->name);
obj[F("grid")] = iv->getGridProfile();
}
void getIvAlarms(JsonObject obj, uint8_t id) { void getIvAlarms(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id); Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) { if(NULL == iv) {
@ -413,6 +541,7 @@ class RestApi {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
obj[F("iv_id")] = id; obj[F("iv_id")] = id;
obj[F("iv_name")] = String(iv->config->name);
obj[F("cnt")] = iv->alarmCnt; obj[F("cnt")] = iv->alarmCnt;
obj[F("last_id")] = iv->getChannelFieldValue(CH0, FLD_EVT, rec); obj[F("last_id")] = iv->getChannelFieldValue(CH0, FLD_EVT, rec);
@ -425,6 +554,42 @@ class RestApi {
} }
} }
void getIvVersion(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) {
obj[F("error")] = F("inverter not found!");
return;
}
record_t<> *rec = iv->getRecordStruct(InverterDevInform_Simple);
obj[F("id")] = id;
obj[F("name")] = String(iv->config->name);
obj[F("serial")] = String(iv->config->serial.u64, HEX);
obj[F("generation")] = iv->ivGen;
obj[F("max_pwr")] = iv->getMaxPower();
obj[F("part_num")] = iv->getChannelFieldValueInt(CH0, FLD_PART_NUM, rec);
obj[F("hw_ver")] = iv->getChannelFieldValueInt(CH0, FLD_HW_VERSION, rec);
obj[F("prod_cw")] = ((iv->config->serial.b[3] & 0x0f) * 10 + (((iv->config->serial.b[2] >> 4) & 0x0f)));
obj[F("prod_year")] = ((iv->config->serial.b[3] >> 4) & 0x0f) + 2014;
rec = iv->getRecordStruct(InverterDevInform_All);
char buf[10];
uint16_t val;
val = iv->getChannelFieldValueInt(CH0, FLD_FW_BUILD_MONTH_DAY, rec);
snprintf(buf, 10, "-%02d-%02d", (val / 100), (val % 100));
obj[F("fw_date")] = String(iv->getChannelFieldValueInt(CH0, FLD_FW_BUILD_YEAR, rec)) + String(buf);
val = iv->getChannelFieldValueInt(CH0, FLD_FW_BUILD_HOUR_MINUTE, rec);
snprintf(buf, 10, "%02d:%02d", (val / 100), (val % 100));
obj[F("fw_time")] = String(buf);
val = iv->getChannelFieldValueInt(CH0, FLD_FW_VERSION, rec);
snprintf(buf, 10, "%d.%02d.%02d", (val / 10000), ((val % 10000) / 100), (val % 100));
obj[F("fw_ver")] = String(buf);
obj[F("boot_ver")] = iv->getChannelFieldValueInt(CH0, FLD_BOOTLOADER_VER, rec);
}
void getMqtt(JsonObject obj) { void getMqtt(JsonObject obj) {
obj[F("broker")] = String(mConfig->mqtt.broker); obj[F("broker")] = String(mConfig->mqtt.broker);
obj[F("clientId")] = String(mConfig->mqtt.clientId); obj[F("clientId")] = String(mConfig->mqtt.clientId);
@ -444,7 +609,6 @@ class RestApi {
void getSun(JsonObject obj) { void getSun(JsonObject obj) {
obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : "";
obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : "";
obj[F("disnightcom")] = mConfig->sun.disNightCom;
obj[F("offs")] = mConfig->sun.offsetSec; obj[F("offs")] = mConfig->sun.offsetSec;
} }
@ -457,28 +621,43 @@ class RestApi {
obj[F("miso")] = mConfig->nrf.pinMiso; obj[F("miso")] = mConfig->nrf.pinMiso;
obj[F("led0")] = mConfig->led.led0; obj[F("led0")] = mConfig->led.led0;
obj[F("led1")] = mConfig->led.led1; obj[F("led1")] = mConfig->led.led1;
obj[F("led_high_active")] = mConfig->led.led_high_active; obj[F("led_high_active")] = mConfig->led.high_active;
obj[F("led_lum")] = mConfig->led.luminance;
} }
#if defined(ESP32)
void getRadioCmt(JsonObject obj) { void getRadioCmt(JsonObject obj) {
obj[F("sclk")] = mConfig->cmt.pinSclk;
obj[F("sdio")] = mConfig->cmt.pinSdio;
obj[F("csb")] = mConfig->cmt.pinCsb; obj[F("csb")] = mConfig->cmt.pinCsb;
obj[F("fcsb")] = mConfig->cmt.pinFcsb; obj[F("fcsb")] = mConfig->cmt.pinFcsb;
obj[F("irq")] = mConfig->cmt.pinIrq; obj[F("gpio3")] = mConfig->cmt.pinIrq;
obj[F("en")] = (bool) mConfig->cmt.enabled; obj[F("en")] = (bool) mConfig->cmt.enabled;
} }
void getRadioCmtInfo(JsonObject obj) {
obj[F("en")] = (bool) mConfig->cmt.enabled;
if(mConfig->cmt.enabled) {
obj[F("isconnected")] = mRadioCmt->isChipConnected();
obj[F("sn")] = String(mRadioCmt->getDTUSn(), HEX);
}
}
#endif
void getRadioNrf(JsonObject obj) { void getRadioNrf(JsonObject obj) {
obj[F("power_level")] = mConfig->nrf.amplifierPower;
obj[F("isconnected")] = mRadio->isChipConnected();
obj[F("DataRate")] = mRadio->getDataRate();
obj[F("isPVariant")] = mRadio->isPVariant();
obj[F("en")] = (bool) mConfig->nrf.enabled; obj[F("en")] = (bool) mConfig->nrf.enabled;
if(mConfig->nrf.enabled) {
obj[F("isconnected")] = mRadioNrf->isChipConnected();
obj[F("dataRate")] = mRadioNrf->getDataRate();
obj[F("sn")] = String(mRadioNrf->getDTUSn(), HEX);
}
} }
void getSerial(JsonObject obj) { void getSerial(JsonObject obj) {
obj[F("interval")] = (uint16_t)mConfig->serial.interval;
obj[F("show_live_data")] = mConfig->serial.showIv; obj[F("show_live_data")] = mConfig->serial.showIv;
obj[F("debug")] = mConfig->serial.debug; obj[F("debug")] = mConfig->serial.debug;
obj[F("priv")] = mConfig->serial.privacyLog;
obj[F("wholeTrace")] = mConfig->serial.printWholeTrace;
} }
void getStaticIp(JsonObject obj) { void getStaticIp(JsonObject obj) {
@ -493,7 +672,7 @@ class RestApi {
void getDisplay(JsonObject obj) { void getDisplay(JsonObject obj) {
obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type; obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type;
obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline;
obj[F("disp_pxshift")] = (bool)mConfig->plugin.display.pxShift; obj[F("disp_screensaver")] = (uint8_t)mConfig->plugin.display.screenSaver;
obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot; obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot;
obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; 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_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk;
@ -502,6 +681,15 @@ class RestApi {
obj[F("disp_dc")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc; 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_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; obj[F("disp_bsy")] = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy;
obj[F("pir_pin")] = mConfig->plugin.display.pirPin;
}
void getMqttInfo(JsonObject obj) {
obj[F("enabled")] = (mConfig->mqtt.broker[0] != '\0');
obj[F("connected")] = mApp->getMqttIsConnected();
obj[F("tx_cnt")] = mApp->getMqttTxCnt();
obj[F("rx_cnt")] = mApp->getMqttRxCnt();
obj[F("interval")] = mConfig->mqtt.interval;
} }
void getIndex(AsyncWebServerRequest *request, JsonObject obj) { void getIndex(AsyncWebServerRequest *request, JsonObject obj) {
@ -510,47 +698,38 @@ class RestApi {
obj[F("ts_sunrise")] = mApp->getSunrise(); obj[F("ts_sunrise")] = mApp->getSunrise();
obj[F("ts_sunset")] = mApp->getSunset(); obj[F("ts_sunset")] = mApp->getSunset();
obj[F("ts_offset")] = mConfig->sun.offsetSec; obj[F("ts_offset")] = mConfig->sun.offsetSec;
obj[F("disNightComm")] = mConfig->sun.disNightCom;
JsonArray inv = obj.createNestedArray(F("inverter")); JsonArray inv = obj.createNestedArray(F("inverter"));
Inverter<> *iv; Inverter<> *iv;
bool disNightCom = false;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i); iv = mSys->getInverterByPos(i);
if(NULL != iv) { if(NULL == iv)
continue;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
JsonObject invObj = inv.createNestedObject(); JsonObject invObj = inv.createNestedObject();
invObj[F("enabled")] = (bool)iv->config->enabled; invObj[F("enabled")] = (bool)iv->config->enabled;
invObj[F("id")] = i; invObj[F("id")] = i;
invObj[F("name")] = String(iv->config->name); invObj[F("name")] = String(iv->config->name);
invObj[F("version")] = String(iv->getFwVersion()); invObj[F("cur_pwr")] = ah::round3(iv->getChannelFieldValue(CH0, FLD_PAC, rec));
invObj[F("is_avail")] = iv->isAvailable(); invObj[F("is_avail")] = iv->isAvailable();
invObj[F("is_producing")] = iv->isProducing(); invObj[F("is_producing")] = iv->isProducing();
invObj[F("ts_last_success")] = iv->getLastTs(rec); invObj[F("ts_last_success")] = iv->getLastTs(rec);
if(iv->config->disNightCom)
disNightCom = true;
} }
} obj[F("disNightComm")] = disNightCom;
JsonArray warn = obj.createNestedArray(F("warnings")); JsonArray warn = obj.createNestedArray(F("warnings"));
if(!mRadio->isChipConnected() && mConfig->nrf.enabled) if(!mRadioNrf->isChipConnected() && mConfig->nrf.enabled)
warn.add(F("your NRF24 module can't be reached, check the wiring, pinout and enable")); warn.add(F("your NRF24 module can't be reached, check the wiring, pinout and enable"));
else if(!mRadio->isPVariant() && mConfig->nrf.enabled)
warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible"));
if(!mApp->getSettingsValid()) if(!mApp->getSettingsValid())
warn.add(F("your settings are invalid")); warn.add(F("your settings are invalid"));
if(mApp->getRebootRequestState()) if(mApp->getRebootRequestState())
warn.add(F("reboot your ESP to apply all your configuration changes")); warn.add(F("reboot your ESP to apply all your configuration changes"));
if(0 == mApp->getTimestamp()) if(0 == mApp->getTimestamp())
warn.add(F("time not set. No communication to inverter possible")); warn.add(F("time not set. No communication to inverter possible"));
/*if(0 == mSys->getNumInverters())
warn.add(F("no inverter configured"));*/
if((!mApp->getMqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0))
warn.add(F("MQTT is not connected"));
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(AsyncWebServerRequest *request, JsonObject obj) { void getSetup(AsyncWebServerRequest *request, JsonObject obj) {
@ -561,7 +740,9 @@ class RestApi {
getNtp(obj.createNestedObject(F("ntp"))); getNtp(obj.createNestedObject(F("ntp")));
getSun(obj.createNestedObject(F("sun"))); getSun(obj.createNestedObject(F("sun")));
getPinout(obj.createNestedObject(F("pinout"))); getPinout(obj.createNestedObject(F("pinout")));
#if defined(ESP32)
getRadioCmt(obj.createNestedObject(F("radioCmt"))); getRadioCmt(obj.createNestedObject(F("radioCmt")));
#endif
getRadioNrf(obj.createNestedObject(F("radioNrf"))); getRadioNrf(obj.createNestedObject(F("radioNrf")));
getSerial(obj.createNestedObject(F("serial"))); getSerial(obj.createNestedObject(F("serial")));
getStaticIp(obj.createNestedObject(F("static_ip"))); getStaticIp(obj.createNestedObject(F("static_ip")));
@ -576,7 +757,7 @@ class RestApi {
void getLive(AsyncWebServerRequest *request, JsonObject obj) { void getLive(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("refresh")] = mConfig->nrf.sendInterval; obj[F("refresh")] = mConfig->inst.sendInterval;
for (uint8_t fld = 0; fld < sizeof(acList); fld++) { for (uint8_t fld = 0; fld < sizeof(acList); fld++) {
obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]);
@ -630,28 +811,6 @@ class RestApi {
obj[F("dispIndex")] = p->getDisplIdx(); obj[F("dispIndex")] = p->getDisplIdx();
} }
/*void getRecord(JsonObject obj, uint8_t recType) {
JsonArray invArr = obj.createNestedArray(F("inverter"));
Inverter<> *iv;
record_t<> *rec;
uint8_t pos;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i);
if(NULL != iv) {
rec = iv->getRecordStruct(recType);
JsonArray obj2 = invArr.createNestedArray();
for(uint8_t j = 0; j < rec->length; j++) {
byteAssign_t *assign = iv->getByteAssign(j, rec);
pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec));
obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail;
}
}
}
}*/
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) { bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) {
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]);
bool accepted = true; bool accepted = true;
@ -659,6 +818,7 @@ class RestApi {
jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>(); jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>();
return false; return false;
} }
jsonOut[F("id")] = jsonIn[F("id")];
if(F("power") == jsonIn[F("cmd")]) if(F("power") == jsonIn[F("cmd")])
accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff);
@ -676,12 +836,10 @@ class RestApi {
iv->powerLimit[1] = AbsolutNonPersistent; iv->powerLimit[1] = AbsolutNonPersistent;
accepted = iv->setDevControlRequest(ActivePowerContr); accepted = iv->setDevControlRequest(ActivePowerContr);
} } else if(F("dev") == jsonIn[F("cmd")]) {
else if(F("dev") == jsonIn[F("cmd")]) {
DPRINTLN(DBG_INFO, F("dev cmd")); DPRINTLN(DBG_INFO, F("dev cmd"));
iv->enqueCommand<InfoCommand>(jsonIn[F("val")].as<int>()); iv->setDevCommand(jsonIn[F("val")].as<int>());
} } else {
else {
jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as<String>() + "'"; jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as<String>() + "'";
return false; return false;
} }
@ -689,8 +847,7 @@ class RestApi {
if(!accepted) { if(!accepted) {
jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); jsonOut[F("error")] = F("inverter does not accept dev control request at this moment");
return false; return false;
} else }
mApp->ivSendHighPrio(iv);
return true; return true;
} }
@ -707,8 +864,26 @@ class RestApi {
mApp->setTimestamp(0); // 0: update ntp flag mApp->setTimestamp(0); // 0: update ntp flag
else if(F("serial_utc_offset") == jsonIn[F("cmd")]) else if(F("serial_utc_offset") == jsonIn[F("cmd")])
mTimezoneOffset = jsonIn[F("val")]; mTimezoneOffset = jsonIn[F("val")];
else if(F("discovery_cfg") == jsonIn[F("cmd")]) { else if(F("discovery_cfg") == jsonIn[F("cmd")])
mApp->setMqttDiscoveryFlag(); // for homeassistant mApp->setMqttDiscoveryFlag(); // for homeassistant
else if(F("save_iv") == jsonIn[F("cmd")]) {
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")], false);
iv->config->enabled = jsonIn[F("en")];
iv->config->serial.u64 = jsonIn[F("ser")];
snprintf(iv->config->name, MAX_NAME_LENGTH, "%s", jsonIn[F("name")].as<const char*>());
for(uint8_t i = 0; i < 6; i++) {
iv->config->chMaxPwr[i] = jsonIn[F("ch")][i][F("pwr")];
iv->config->yieldCor[i] = jsonIn[F("ch")][i][F("yld")];
snprintf(iv->config->chName[i], MAX_NAME_LENGTH, "%s", jsonIn[F("ch")][i][F("name")].as<const char*>());
}
mApp->initInverter(jsonIn[F("id")]);
iv->config->frequency = jsonIn[F("freq")];
iv->config->powerLevel = jsonIn[F("pa")];
iv->config->disNightCom = jsonIn[F("disnightcom")];
iv->config->add2Total = jsonIn[F("add2total")];
mApp->saveSettings(false); // without reboot
} else { } else {
jsonOut[F("error")] = F("unknown cmd"); jsonOut[F("error")] = F("unknown cmd");
return false; return false;
@ -719,14 +894,18 @@ class RestApi {
IApp *mApp; IApp *mApp;
HMSYSTEM *mSys; HMSYSTEM *mSys;
HMRADIO *mRadio; HmRadio<> *mRadioNrf;
#if defined(ESP32)
CmtRadio<> *mRadioCmt;
#endif
AsyncWebServer *mSrv; AsyncWebServer *mSrv;
settings_t *mConfig; settings_t *mConfig;
uint32_t mTimezoneOffset; uint32_t mTimezoneOffset;
uint32_t mHeapFree, mHeapFreeBlk; uint32_t mHeapFree, mHeapFreeBlk;
uint8_t mHeapFrag; uint8_t mHeapFrag;
uint16_t nr; uint8_t *mTmpBuf = NULL;
uint32_t mTmpSize;
}; };
#endif /*__WEB_API_H__*/ #endif /*__WEB_API_H__*/

100
src/web/html/api.js

@ -1,6 +1,4 @@
/** /* SVG ICONS - https://icons.getbootstrap.com */
* SVG ICONS
*/
iconWifi1 = [ iconWifi1 = [
"M11.046 10.454c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.708-.707z" "M11.046 10.454c.226-.226.185-.605-.1-.75A6.473 6.473 0 0 0 8 9c-1.06 0-2.062.254-2.946.704-.285.145-.326.524-.1.75l.015.015c.16.16.407.19.611.09A5.478 5.478 0 0 1 8 10c.868 0 1.69.201 2.42.56.203.1.45.07.611-.091l.015-.015zM9.06 12.44c.196-.196.198-.52-.04-.66A1.99 1.99 0 0 0 8 11.5a1.99 1.99 0 0 0-1.02.28c-.238.14-.236.464-.04.66l.706.706a.5.5 0 0 0 .707 0l.708-.707z"
@ -34,6 +32,15 @@ iconSuccessFull = [
"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" "M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"
]; ];
iconGear = [
"M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"
];
iconDel = [
"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z",
"M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"
];
/** /**
* GENERIC FUNCTIONS * GENERIC FUNCTIONS
*/ */
@ -119,24 +126,25 @@ function parseRssi(obj) {
icon = iconWifi1; icon = iconWifi1;
else if(obj["wifi_rssi"] <= -70) else if(obj["wifi_rssi"] <= -70)
icon = iconWifi2; icon = iconWifi2;
document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"])); document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "icon-fg2", obj["wifi_rssi"]));
} }
function toIsoDateStr(d) { function toIsoDateStr(d) {
return new Date(d.getTime() + (d.getTimezoneOffset() * -60000)).toISOString().substring(0, 19).replace('T', ', '); return new Date(d.getTime() + (d.getTimezoneOffset() * -60000)).toISOString().substring(0, 19).replace('T', ', ');
} }
function toIsoTimeStr(d) { function toIsoTimeStr(d) { // UTC!
return new Date(d.getTime() + (d.getTimezoneOffset() * -60000)).toISOString().substring(11, 19).replace('T', ', '); return new Date(d.getTime()).toISOString().substring(11, 19).replace('T', ', ');
} }
function setHide(id, hide) { function setHide(id, hide) {
var elm = document.getElementById(id); var elm = document.getElementById(id);
if(null == elm)
return;
if(hide) { if(hide) {
if(!elm.classList.contains("hide")) if(!elm.classList.contains("hide"))
elm.classList.add("hide"); elm.classList.add("hide");
} } else
else
elm.classList.remove('hide'); elm.classList.remove('hide');
} }
@ -167,10 +175,72 @@ function getAjax(url, ptr, method="GET", json=null) {
} }
} }
const getJSON = async url => {
const re = await fetch(url);
if(!re.ok)
throw new Error(re.statusText);
const data = re.json();
return data;
}
/** /**
* CREATE DOM FUNCTIONS * CREATE DOM FUNCTIONS
*/ */
function tr(val1, val2) {
if(typeof val2 == "number")
val2 = String(val2);
return ml("tr", {}, [
ml("th", {style: "width: 50%"}, val1),
ml("td", {}, val2)
]);
}
function tr2(cols) {
var t = [];
for(val of cols) {
if(typeof val == "number")
val = String(val);
if(t.length == 0)
t.push(ml("th", {}, val));
else
t.push(ml("td", {}, val));
}
return ml("tr", {}, t);
}
function badge(success, text, second="error") {
return ml("span", {class: "badge badge-" + ((success) ? "success" : second)}, text);
}
function tabChange(id) {
var els = document.getElementsByClassName("nav-link");
[].forEach.call(els, function(e) {
if(e.id != id)
e.classList.remove('active');
else
e.classList.add('active');
});
els = document.getElementsByClassName("tab-content");
[].forEach.call(els, function(e) {
if(e.id == ("div"+id.substring(3)))
e.classList.remove('hide');
else
e.classList.add('hide');
});
}
function tabs(items) {
var li = [];
var cl = " active";
for(it of items) {
li.push(ml("li", {class: "nav-item"},ml("a", {id: "tab"+it, class: "nav-link" + cl, href: "#", onclick: function(){tabChange(this.id)}}, it)))
cl = "";
}
return ml("ul", {class: "nav nav-tabs mb-4"}, li);
}
function des(val) { function des(val) {
e = document.createElement('p'); e = document.createElement('p');
e.classList.add("subdes"); e.classList.add("subdes");
@ -202,13 +272,11 @@ function inp(name, val, max=32, cl=["text"], id=null, type=null, pattern=null, t
} }
function sel(name, options, selId) { function sel(name, options, selId) {
e = document.createElement('select'); var o = [];
e.name = name;
for(it of options) { for(it of options) {
o = opt(it[0], it[1], (it[0] == selId)); o.push(opt(it[0], it[1], (it[0] == selId)));
e.appendChild(o);
} }
return e; return ml("select", {name: name}, o);
} }
function selDelAllOpt(sel) { function selDelAllOpt(sel) {
@ -219,9 +287,7 @@ function selDelAllOpt(sel) {
} }
function opt(val, html, sel=false) { function opt(val, html, sel=false) {
o = document.createElement('option'); var o = ml("option", {value: val}, html);
o.value = val;
o.innerHTML = html;
if(sel) if(sel)
o.selected = true; o.selected = true;
return o; return o;
@ -280,7 +346,7 @@ function svg(data=null, w=24, h=24, cl=null, tooltip=null) {
function modal(title, body) { function modal(title, body) {
if(null == document.getElementById("modal")) { if(null == document.getElementById("modal")) {
document.getElementById("wrapper").append( document.getElementById("wrapper").append(
ml("div", {id: "modal-wrapper", class: "modal", onclick: modalClose}), ml("div", {id: "modal-wrapper", onclick: modalClose}),
ml("div", {id: "modal", class: "modal"}, ml("div", {id: "modal", class: "modal"},
ml("div", {class: "modal-content"}, [ ml("div", {class: "modal-content"}, [
ml("div", {class: "modal-header"}, [ ml("div", {class: "modal-header"}, [

3
src/web/html/colorBright.css

@ -8,6 +8,7 @@
--success: #009900; --success: #009900;
--input-bg: #eee; --input-bg: #eee;
--table-border: #ccc;
--nav-bg: #333; --nav-bg: #333;
--primary: #006ec0; --primary: #006ec0;
@ -17,6 +18,8 @@
--footer-bg: #282828; --footer-bg: #282828;
--modal-bg: #fff; --modal-bg: #fff;
--invalid-bg: #f99;
--total-head-title: #8e5903; --total-head-title: #8e5903;
--total-bg: #b06e04; --total-bg: #b06e04;
--iv-head-title: #1c6800; --iv-head-title: #1c6800;

5
src/web/html/colorDark.css

@ -8,6 +8,7 @@
--success: #00bb00; --success: #00bb00;
--input-bg: #333; --input-bg: #333;
--table-border: #333;
--nav-bg: #333; --nav-bg: #333;
--primary: #004d87; --primary: #004d87;
@ -15,7 +16,9 @@
--secondary: #0072c8; --secondary: #0072c8;
--nav-active: #555; --nav-active: #555;
--footer-bg: #282828; --footer-bg: #282828;
--modal-bg: #666; --modal-bg: #282828;
--invalid-bg: #400;
--total-head-title: #555511; --total-head-title: #555511;
--total-bg: #666622; --total-bg: #666622;

765
src/web/html/grid_info.json

@ -0,0 +1,765 @@
{
"type": [
{"0x0300": "DE_VDE4105_2018"},
{"0x0a00": "DE NF_EN_50549-1:2019"},
{"0x0c00": "AT_TOR_Erzeuger_default"},
{"0x0d04": "France NF_EN_50549-1:2019"},
{"0x1200": "2.0.4 (EU_EN50438)"},
{"0x3700": "2.0.0 (CH_NA EEA-NE7–CH2020)"}
],
"grp_codes": [
{"0x00": "Voltage H/LVRT"},
{"0x10": "Frequency H/LFRT"},
{"0x20": "Islanding Detection"},
{"0x30": "Reconnection"},
{"0x40": "Ramp Rates"},
{"0x50": "Frequency Watt"},
{"0x60": "Volt Watt"},
{"0x70": "Active Power Control"},
{"0x80": "Volt Var"},
{"0x90": "Specified Power Factor"},
{"0xa0": "Reactive Power Control"},
{"0xb0": "Watt Power Factor"}
],
"group": [
{
"0x0003": [
{
"name": "Nominal Voltage",
"div": 10,
"def": 230,
"unit": "V"
},
{
"name": "Low Voltage 1",
"div": 10,
"min": 180,
"max": 207,
"def": 184,
"unit": "V"
},
{
"name": "LV1 Maximum Trip Time",
"div": 10,
"def": 1.5,
"unit": "s"
},
{
"name": "High Voltage 1",
"div": 10,
"min": 250,
"max": 270,
"def": 253,
"unit": "V"
},
{
"name": "HV1 Maximum Trip Time",
"div": 10,
"min": 0.1,
"max": 100,
"def": 0.1,
"unit": "s"
},
{
"name": "Low Voltage 2",
"div": 10,
"min": 80,
"max": 161,
"def": 104,
"unit": "V"
},
{
"name": "LV2 Maximum Trip Time",
"div": 100,
"min": 0.1,
"max": 5,
"def": 0.3,
"unit": "s"
},
{
"name": "High Voltage 2",
"div": 10,
"min": 230,
"max": 299,
"def": 276,
"unit": "V"
},
{
"name": "HV2 Maximum Trip Time",
"div": 100,
"min": 0,
"max": 5,
"def": 0.05,
"unit": "s"
}
]
},
{
"0x000a": [
{
"name": "Nominal Voltage",
"div": 10,
"def": 230,
"unit": "V"
},
{
"name": "Low Voltage 1",
"div": 10,
"min": 160,
"max": 195.5,
"def": 184,
"unit": "V"
},
{
"name": "LV1 Maximum Trip Time",
"div": 10,
"def": 3,
"unit": "s"
},
{
"name": "High Voltage 1",
"div": 10,
"min": 270,
"max": 287.5,
"def": 287.5,
"unit": "V"
},
{
"name": "HV1 Maximum Trip Time",
"div": 10,
"def": 0.1,
"unit": "s"
},
{
"name": "Low Voltage 2",
"div": 10,
"max": 150,
"min": 100,
"def": 103.5,
"unit": "V"
},
{
"name": "LV2 Maximum Trip Time",
"div": 100,
"def": 0.3,
"unit": "s"
},
{
"name": "10 mins Average High Voltage",
"div": 10,
"min": 250,
"max": 270,
"def": 253,
"unit": "V"
}
]
},
{
"0x000c": [
{
"name": "Nominal Voltage",
"div": 10,
"def": 230,
"unit": "V"
},
{
"name": "Low Voltage 1",
"div": 10,
"min": 180,
"max": 207,
"def": 184,
"unit": "V"
},
{
"name": "LV1 Maximum Trip Time",
"div": 10,
"min": 0.1,
"max": 5,
"def": 1.5,
"unit": "s"
},
{
"name": "High Voltage 1",
"div": 10,
"min": 250,
"max": 270,
"def": 253,
"unit": "V"
},
{
"name": "HV1 Maximum Trip Time",
"div": 10,
"min": 0.1,
"max": 100,
"def": 3,
"unit": "s"
},
{
"name": "Low Voltage 2",
"div": 10,
"min": 80,
"max": 161,
"def": 161,
"unit": "V"
},
{
"name": "LV2 Maximum Trip Time",
"div": 100,
"min": 0.1,
"max": 5,
"def": 0.2,
"unit": "s"
},
{
"name": "High Voltage 2",
"div": 10,
"min": 230,
"max": 299,
"def": 264.5,
"unit": "V"
},
{
"name": "HV2 Maximum Trip Time",
"div": 100,
"min": 0.1,
"max": 5,
"def": 0.2,
"unit": "s"
},
{
"name": "High Voltage 3",
"div": 10,
"def": 276,
"unit": "V"
},
{
"name": "HV3 Maximum Trip Time",
"div": 100,
"min": 0.1,
"max": 10,
"def": 0.1,
"unit": "s"
},
{
"name": "10 mins Average High Voltage",
"div": 10,
"def": 253,
"unit": "V"
}
]
},
{
"0x1000": [
{
"name": "Nominal Frequency",
"div": 100,
"def": 50,
"unit": "Hz"
},
{
"name": "Low Frequency 1",
"div": 100,
"min": 47.5,
"max": 49.9,
"def": 47.5,
"unit": "Hz"
},
{
"name": "LF1 Maximum Trip Time",
"div": 10,
"def": 0.1,
"unit": "s"
},
{
"name": "High Frequency 1",
"div": 100,
"min": 50.1,
"max": 51.5,
"def": 51.5,
"unit": "Hz"
},
{
"name": "HF1 Maximum Trip Time",
"div": 10,
"def": 0.1,
"unit": "s"
}
]
},
{
"0x1003": [
{
"name": "Nominal Frequency",
"div": 100,
"def": 50,
"unit": "Hz"
},
{
"name": "Low Frequency 1",
"div": 100,
"min": 45,
"max": 49.9,
"def": 48,
"unit": "Hz"
},
{
"name": "LF1 Maximum Trip Time",
"div": 100,
"min": 0.1,
"max": 20,
"def": 2,
"unit": "s"
},
{
"name": "High Frequency 1",
"div": 100,
"min": 50,
"max": 53,
"def": 51,
"unit": "Hz"
},
{
"name": "HF1 Maximum Trip time",
"div": 10,
"min": 0.1,
"max": 20,
"def": 2,
"unit": "s"
},
{
"name": "Low Frequency 2",
"div": 100,
"min": 45,
"max": 50,
"def": 47.5,
"unit": "Hz"
},
{
"name": "LF2 Maximum Trip Time",
"div": 10,
"min": 0.1,
"max": 5,
"def": 0.5,
"unit": "s"
},
{
"name": "High Frequency 2",
"div": 100,
"min": 50,
"max": 52,
"def": 52,
"unit": "Hz"
},
{
"name": "HF2 Maximum Trip time",
"div": 10,
"min": 0.1,
"max": 5,
"def": 0.5,
"unit": "s"
}
]
},
{
"0x2000": [
{
"name": "Island Detection Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 1
}
]
},
{
"0x3003": [
{
"name": "Reconnect Time",
"div": 10,
"min": 10,
"max": 300,
"def": 60,
"unit": "s"
},
{
"name": "Reconnect High Voltage",
"div": 10,
"min": 240,
"max": 276,
"def": 253,
"unit": "V"
},
{
"name": "Reconnect Low Voltage",
"div": 10,
"min": 195.5,
"max": 210,
"def": 195.5,
"unit": "V"
},
{
"name": "Reconnect High Frequency",
"div": 100,
"max": 50.9,
"min": 50.1,
"def": 50.2,
"unit": "Hz"
},
{
"name": "Reconnect Low Frequency",
"div": 100,
"min": 47.5,
"max": 49.9,
"def": 49.5,
"unit": "Hz"
}
]
},
{
"0x4000": [
{
"name": "Normal Ramp up Rate",
"div": 100,
"min": 0.1,
"max": 100,
"def": 20,
"unit": "Rated%/s"
},
{
"name": "Soft Start Ramp up Rate ",
"div": 100,
"min": 0.1,
"max": 10,
"def": 0.16,
"unit": "Rated%/s"
}
]
},
{
"0x5001": [
{
"name": "FW Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 1
},
{
"name": "Start of Frequency Watt Droop",
"div": 100,
"min": 50.2,
"max": 53,
"def": 50.2,
"unit": "Hz"
},
{
"name": "FW Droop Slope",
"div": 10,
"min": 16.7,
"max": 100,
"def": 40,
"unit": "Pn%/Hz"
},
{
"name": "Recovery Ramp Rate",
"div": 100,
"min": 0.1,
"max": 100,
"def": 0.16,
"unit": "Pn%/s"
},
{
"name": "FW Setting Time",
"div": 10,
"min": 0,
"max": 2,
"def": 0,
"unit": "s"
}
]
},
{
"0x5008": [
{
"name": "FW Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 1
},
{
"name": "Start of Frequency Watt Droop",
"div": 100,
"min": 50.2,
"max": 52,
"def": 50.2,
"unit": "Hz"
},
{
"name": "FW Droop Slope",
"div": 10,
"min": 16.7,
"max": 100,
"def": 40,
"unit": "Pn%/Hz"
},
{
"name": "Recovery Ramp Rate",
"div": 100,
"min": 0.1,
"max": 50,
"def": 0.16,
"unit": "Pn%/s"
},
{
"name": "Recovery High Frequency",
"div": 10,
"min": 50.1,
"max": 52,
"def": 50.2,
"unit": "Hz"
},
{
"name": "Recovery Low Frequency",
"div": 100,
"min": 49,
"max": 49.9,
"def": 49.8,
"unit": "Hz"
}
]
},
{
"0x6000": [
{
"name": "VW Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 1
},
{
"name": "Start of Voltage Watt Droop",
"div": 10,
"def": 253,
"unit": "V"
},
{
"name": "End of Voltage Watt Droop",
"div": 10,
"min": 258,
"max": 270,
"def": 265,
"unit": "V"
},
{
"name": "VW Droop Slope",
"div": 100,
"min": 5,
"max": 18,
"def": 5.33,
"unit": "Pn%/V"
}
]
},
{
"0x7002": [
{
"name": "APC Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 1
},
{
"name": "Power Ramp Rate",
"div": 100,
"min": 0.33,
"max": 100,
"def": 100,
"unit": "Pn%/s"
}
]
},
{
"0x8000": [
{
"name": "VV Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 0
},
{
"name": "Voltage Set Point V1",
"div": 10,
"min": 184,
"max": 230,
"def": 213.9,
"unit": "V"
},
{
"name": "Reactive Set Point Q1",
"div": 10,
"min": 0,
"max": 100,
"def": 30,
"unit": "%Pn"
},
{
"name": "Voltage Set Point V2",
"div": 10,
"min": 210,
"max": 240,
"def": 223.1,
"unit": "V"
},
{
"name": "Voltage Set Point V3",
"div": 10,
"min": 220,
"max": 240,
"def": 236.9,
"unit": "V"
},
{
"name": "Voltage Set Point V4",
"div": 10,
"min": 230,
"max": 253,
"def": 246.1,
"unit": "V"
},
{
"name": "Reactive Set Point Q4",
"div": 10,
"min": 0,
"max": 100,
"def": 30,
"unit": "%Pn"
}
]
},
{
"0x8001": [
{
"name": "VV Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 0
},
{
"name": "Voltage Set Point V1",
"div": 10,
"def": 213.9,
"unit": "V"
},
{
"name": "Reactive Set Point Q1",
"div": 10,
"min": 0,
"max": 100,
"def": 30,
"unit": "%Pn"
},
{
"name": "Voltage Set Point V2",
"div": 10,
"def": 223.1,
"unit": "V"
},
{
"name": "Voltage Set Point V3",
"div": 10,
"def": 236.9,
"unit": "V"
},
{
"name": "Voltage Set Point V4",
"div": 10,
"def": 246.1,
"unit": "V"
},
{
"name": "Reactive Set Point Q4",
"div": 10,
"min": 0,
"max": 100,
"def": 30,
"unit": "%Pn"
},
{
"name": "VV Setting Time",
"div": 10,
"min": 0,
"max": 60,
"def": 10,
"unit": "s"
}
]
},
{
"0x9000": [
{
"name": "Specified Power Factor Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 0
},
{
"name": "Power Factor",
"div": 100,
"min": 0.9,
"max": 1,
"def": 0.95
}
]
},
{
"0xa002": [
{
"name": "RPC Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 0
},
{
"name": "Reactive Power",
"div": 100,
"min": 0,
"max": 50,
"def": 0,
"unit": "%Sn"
}
]
},
{
"0xb000": [
{
"name": "WPF Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 0
},
{
"name": "Start of Power of WPF",
"div": 10,
"def": 50,
"unit": "%Pn"
},
{
"name": "Power Factor ar Rated Power",
"div": 100,
"min": 0.8,
"max": 1,
"def": 0.95
}
]
}
]
}

2
src/web/html/includes/nav.html

@ -8,7 +8,7 @@
<div id="topnav" class="mobile"> <div id="topnav" class="mobile">
<a id="nav3" class="hide" href="/live?v={#VERSION}">Live</a> <a id="nav3" class="hide" href="/live?v={#VERSION}">Live</a>
<a id="nav11" class="acitve" href="/history?v={#VERSION}">History</a> <a id="nav11" class="acitve" href="/history?v={#VERSION}">History</a>
<a id="nav4" class="hide" href="/serial?v={#VERSION}">Serial / Control</a> <a id="nav4" class="hide" href="/serial?v={#VERSION}">Webserial</a>
<a id="nav5" class="hide" href="/setup?v={#VERSION}">Settings</a> <a id="nav5" class="hide" href="/setup?v={#VERSION}">Settings</a>
<span class="seperator"></span> <span class="seperator"></span>
<a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a> <a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a>

32
src/web/html/index.html

@ -19,7 +19,6 @@
<div id="warn_info"></div> <div id="warn_info"></div>
</p> </p>
<div class="hr"></div>
<div id="note"> <div id="note">
<h3>Support this project:</h3> <h3>Support this project:</h3>
<ul> <ul>
@ -47,7 +46,7 @@
function apiCb(obj) { function apiCb(obj) {
var e = document.getElementById("apiResult"); var e = document.getElementById("apiResult");
if(obj["success"]) { if(obj["success"]) {
e.innerHTML = " command excuted"; e.innerHTML = " command executed";
getAjax("/api/index", parse); getAjax("/api/index", parse);
} }
else else
@ -116,7 +115,7 @@
} }
} }
function parseIv(obj) { function parseIv(obj, ts) {
var p = div(["none"]); var p = div(["none"]);
for(var i of obj) { for(var i of obj) {
var icon = iconSuccess; var icon = iconSuccess;
@ -126,27 +125,25 @@
icon = iconWarn; icon = iconWarn;
cl = "icon-warn"; cl = "icon-warn";
avail = "disabled"; avail = "disabled";
} } else if((false == i["is_avail"]) || (0 == ts)) {
else if(false == i["is_avail"]) {
icon = iconInfo; icon = iconInfo;
cl = "icon-info"; cl = "icon-info";
avail = "not yet available"; avail = "not yet available";
} } else if(0 == i["ts_last_success"]) {
else if(0 == i["ts_last_success"]) {
avail = "available but no data was received until now"; avail = "available but no data was received until now";
} } else {
else {
avail = "available and is "; avail = "available and is ";
if(false == i["is_producing"]) if(false == i["is_producing"])
avail += "not "; avail += "not producing";
else else {
icon = iconSuccessFull; icon = iconSuccessFull;
avail += "producing"; avail += "producing " + i.cur_pwr + "W";
}
} }
p.append( p.append(
svg(icon, 30, 30, "icon " + cl), svg(icon, 30, 30, "icon " + cl),
span("Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is " + avail), span("Inverter #" + i["id"] + ": " + i["name"] + " is " + avail),
br() br()
); );
@ -160,14 +157,11 @@
document.getElementById("iv").replaceChildren(p); document.getElementById("iv").replaceChildren(p);
} }
function parseWarnInfo(warn, success) { function parseWarn(warn) {
var p = div(["none"]); var p = div(["none"]);
for(var w of warn) { for(var w of warn) {
p.append(svg(iconWarn, 30, 30, "icon icon-warn"), span(w), br()); p.append(svg(iconWarn, 30, 30, "icon icon-warn"), span(w), br());
} }
for(var i of success) {
p.append(svg(iconSuccess, 30, 30, "icon icon-success"), span(i), br());
}
if(commInfo.length > 0) if(commInfo.length > 0)
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span(commInfo), br()); p.append(svg(iconInfo, 30, 30, "icon icon-info"), span(commInfo), br());
@ -199,8 +193,8 @@
parseNav(obj["generic"]); parseNav(obj["generic"]);
parseGeneric(obj["generic"]); parseGeneric(obj["generic"]);
parseSys(obj); parseSys(obj);
parseIv(obj["inverter"]); parseIv(obj["inverter"], obj.ts_now);
parseWarnInfo(obj["warnings"], obj["infos"]); parseWarn(obj["warnings"]);
if(exeOnce) { if(exeOnce) {
window.setInterval("tick()", 1000); window.setInterval("tick()", 1000);
exeOnce = false; exeOnce = false;

6
src/web/html/save.html

@ -34,8 +34,8 @@
html = "Settings successfully saved. Automatic page reload in 3 seconds."; html = "Settings successfully saved. Automatic page reload in 3 seconds.";
meta.content = 3; meta.content = 3;
} else { } else {
html = "Settings successfully saved. Rebooting. Automatic redirect in 20 seconds."; html = "Settings successfully saved. Rebooting. Automatic redirect in " + obj.reload + " seconds.";
meta.content = 20 + "; URL=/"; meta.content = obj.reload + "; URL=/";
} }
document.getElementsByTagName('head')[0].appendChild(meta); document.getElementsByTagName('head')[0].appendChild(meta);
} else { } else {
@ -51,7 +51,9 @@
parseHtml(obj); parseHtml(obj);
} }
} }
intervalId = window.setInterval("getAjax('/api/html/save', parse)", 2500); intervalId = window.setInterval("getAjax('/api/html/save', parse)", 2500);
getAjax("/api/generic", parseGeneric);
</script> </script>
</body> </body>
</html> </html>

144
src/web/html/serial.html

@ -7,58 +7,19 @@
<body> <body>
{#HTML_NAV} {#HTML_NAV}
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content" style="max-width: 100% !important;">
<div class="row"> <div class="row">
<textarea id="serial" class="mt-3" cols="80" rows="20" readonly></textarea> <textarea id="serial" class="mt-3" cols="80" rows="40" readonly></textarea>
</div> </div>
<div class="row my-3"> <div class="row my-3">
<div class="col-3">connected: <span class="dot" id="connected"></span></div> <div class="col-3">console active: <span class="dot" id="active"></span></div>
<div class="col-3 col-sm-4 my-3">Uptime: <span id="uptime"></span></div> <div class="col-3 col-sm-4 my-3">Uptime: <span id="uptime"></span></div>
<div class="col-6 col-sm-4"> <div class="col-6 col-sm-4 a-r">
<input type="button" value="clear" class="btn" id="clear"/> <input type="button" value="clear" class="btn" id="clear"/>
<input type="button" value="autoscroll" class="btn" id="scroll"/> <input type="button" value="autoscroll" class="btn" id="scroll"/>
<input type="button" value="copy" class="btn" id="copy"/>
</div> </div>
</div> </div>
<div class="hr my-3"></div>
<div class="row mb-3">
<h3>Commands</h3>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Select Inverter</div>
<div class="col-12 col-sm-9"><select name="iv" id="InvID"></select></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Power Limit Command</div>
<div class="col-12 col-sm-9">
<select name="pwrlimctrl">
<option value="" selected disabled hidden>select the unit and persistence</option>
<option value="limit_nonpersistent_absolute">absolute non persistent [W]</option>
<option value="limit_nonpersistent_relative">relative non persistent [%]</option>
<option value="limit_persistent_absolute">absolute persistent [W]</option>
<option value="limit_persistent_relative">relative persistent [%]</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Power Limit Value</div>
<div class="col-12 col-sm-9"><input type="number" name="pwrlimval" maxlength="4"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3"></div>
<div class="col-12 col-sm-9"><input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Control Inverter</div>
<div class="col-12 col-sm-9" id="power">
<input type="button" value="Restart" class="btn" id="restart"/>
<input type="button" value="Turn Off" class="btn" id="power_off"/>
<input type="button" value="Turn On" class="btn" id="power_on"/>
</div>
</div>
<div class="row mb-5">
<div class="col-3 my-2">Ctrl result</div>
<div class="col-9 my-2"><span id="result">n/a</span></div>
</div>
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}
@ -66,6 +27,7 @@
var mAutoScroll = true; var mAutoScroll = true;
var con = document.getElementById("serial"); var con = document.getElementById("serial");
var exeOnce = true; var exeOnce = true;
var version, build;
function parseGeneric(obj) { function parseGeneric(obj) {
var up = obj["ts_uptime"]; var up = obj["ts_uptime"];
@ -82,25 +44,15 @@
if(true == exeOnce) { if(true == exeOnce) {
parseNav(obj); parseNav(obj);
parseESP(obj); parseESP(obj);
window.setInterval("getAjax('/api/generic', parseGeneric)", 10000); window.setInterval("getAjax('/api/generic', parseGeneric)", 5000);
exeOnce = false; exeOnce = false;
getAjax("/api/inverter/list", parse); setTimeOffset();
} }
version = obj.version;
build = obj.build;
} }
function parse(root) { function setTimeOffset() {
select = document.getElementById('InvID');
if(null == root) return;
root = root.inverter;
for(var i = 0; i < root.length; i++) {
inv = root[i];
var opt = document.createElement('option');
opt.value = inv.id;
opt.innerHTML = inv.name;
select.appendChild(opt);
}
// set time offset for serial console // set time offset for serial console
var obj = new Object(); var obj = new Object();
obj.cmd = "serial_utc_offset"; obj.cmd = "serial_utc_offset";
@ -115,16 +67,36 @@
mAutoScroll = !mAutoScroll; mAutoScroll = !mAutoScroll;
this.value = (mAutoScroll) ? "autoscroll" : "manual scroll"; this.value = (mAutoScroll) ? "autoscroll" : "manual scroll";
}); });
document.getElementById("copy").addEventListener("click", function() {
con.value = version + " - " + build + "\n---------------\n" + con.value;
if (window.clipboardData && window.clipboardData.setData) {
return window.clipboardData.setData("Text", text);
} else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
var ta = document.createElement("textarea");
ta.textContent = con.value;
ta.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
document.body.appendChild(ta);
ta.select();
try {
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
} catch (ex) {
alert("Copy to clipboard failed" + ex);
} finally {
document.body.removeChild(ta);
alert("Copied to clipboard");
}
}
});
if (!!window.EventSource) { if (!!window.EventSource) {
var source = new EventSource('/events'); var source = new EventSource('/events');
source.addEventListener('open', function(e) { source.addEventListener('open', function(e) {
document.getElementById("connected").style.backgroundColor = "#0c0"; document.getElementById("active").style.backgroundColor = "#0c0";
}, false); }, false);
source.addEventListener('error', function(e) { source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) { if (e.target.readyState != EventSource.OPEN) {
document.getElementById("connected").style.backgroundColor = "#f00"; document.getElementById("active").style.backgroundColor = "#f00";
} }
}, false); }, false);
@ -135,56 +107,6 @@
}, false); }, false);
} }
function ctrlCb(obj) {
var e = document.getElementById("result");
if(obj["success"])
e.innerHTML = "ok";
else
e.innerHTML = "Error: " + obj["error"];
}
function get_selected_iv() {
var e = document.getElementById("InvID");
return parseInt(e.value);
}
const wrapper = document.getElementById('power');
wrapper.addEventListener('click', (event) => {
var obj = new Object();
obj.id = get_selected_iv();
obj.cmd = "power";
switch (event.target.value) {
default:
case "Turn On":
obj.val = 1;
break;
case "Turn Off":
obj.val = 0;
break;
}
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
});
document.getElementById("sendpwrlim").addEventListener("click", function() {
var val = parseInt(document.getElementsByName('pwrlimval')[0].value);
var cmd = document.getElementsByName('pwrlimctrl')[0].value;
if(isNaN(val)) {
document.getElementById("result").textContent = "value is missing";
return;
}
var obj = new Object();
obj.id = get_selected_iv();
obj.cmd = cmd;
obj.val = val;
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
});
getAjax("/api/generic", parseGeneric); getAjax("/api/generic", parseGeneric);
</script> </script>
</body> </body>

634
src/web/html/setup.html

@ -30,15 +30,15 @@
</fieldset> </fieldset>
<fieldset class="mb-4"> <fieldset class="mb-4">
<legend class="des">System Config</legend> <legend class="des">System Config</legend>
<p class="des">Pinout</p> <p class="des">Status LEDs</p>
<div id="pinout"></div> <div id="pinout"></div>
<p class="des">Radio (NRF24L01+)</p> <p class="des">Radio (NRF24L01+)</p>
<div id="rf24"></div> <div id="rf24"></div>
<!--IF_ESP32-->
<p class="des">Radio (CMT2300A)</p> <p class="des">Radio (CMT2300A)</p>
<div id="cmt"><div class="col-12">(ESP32 only)</div></div> <div id="cmt"><div class="col-12">(ESP32 only)</div></div>
<!--ENDIF_ESP32-->
<p class="des">Serial Console</p> <p class="des">Serial Console</p>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3">print inverter data</div> <div class="col-8 col-sm-3">print inverter data</div>
@ -49,8 +49,12 @@
<div class="col-4 col-sm-9"><input type="checkbox" name="serDbg"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="serDbg"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Interval [s]</div> <div class="col-8 col-sm-3">Privacy Mode</div>
<div class="col-12 col-sm-9"><input type="text" name="serIntvl" pattern="[0-9]+" title="Invalid input"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="priv"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Print whole traces in Log</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="wholeTrace"/></div>
</div> </div>
</fieldset> </fieldset>
</div> </div>
@ -65,7 +69,7 @@
<div class="col-12 col-sm-9"><input type="text" name="ap_pwd" minlength="8" /></div> <div class="col-12 col-sm-9"><input type="text" name="ap_pwd" minlength="8" /></div>
</div> </div>
<p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p> <p>Enter the credentials to your preferred WiFi station. After rebooting the device tries to connect with this information.</p>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Search Networks</div> <div class="col-12 col-sm-3 my-2">Search Networks</div>
@ -140,45 +144,41 @@
<fieldset class="mb-4"> <fieldset class="mb-4">
<legend class="des">Inverter</legend> <legend class="des">Inverter</legend>
<div id="inverter"></div> <div id="inverter"></div>
<div class="row mb-2"> <div class="row mb-3">
<div class="col-12 col-sm-3"></div> <div class="col-8 my-2">Interval [s]</div>
<div class="col-12 col-sm-9"><input type="button" id="btnAdd" class="btn" value="Add Inverter"/></div> <div class="col-4"><input type="number" name="invInterval" title="Invalid input"/></div>
</div>
<div class="row mb-2">
<div class="col-12 col-sm-3"><p class="subdes">Note</p></div>
<div class="col-12 col-sm-9"><p>A 'max module power' value of '0' disables the channel in 'live' view</p></div>
</div> </div>
<div class="row mb-2"> <div class="row mb-3">
<div class="col-12 col-sm-3"><p class="subdes">General</p></div> <div class="col-8 my-2">Inverter Gap [ms]</div>
<div class="col-12 col-sm-9"></div> <div class="col-4"><input type="number" name="invGap" title="Invalid input"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Interval [s]</div> <div class="col-8 mb-2">Reset values and YieldDay at midnight</div>
<div class="col-12 col-sm-9"><input type="number" name="invInterval" title="Invalid input"/></div> <div class="col-4"><input type="checkbox" name="invRstMid"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Max retries per Payload</div> <div class="col-8 mb-2">Reset values when inverter polling pauses at sunset</div>
<div class="col-12 col-sm-9"><input type="number" name="invRetry"/></div> <div class="col-4"><input type="checkbox" name="invRstComStop"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3 mb-2">Reset values and YieldDay at midnight</div> <div class="col-8">Reset values when inverter status is 'not available'</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstMid"/></div> <div class="col-4"><input type="checkbox" name="invRstNotAvail"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3 mb-2">Reset values when inverter polling pauses at sunset</div> <div class="col-8">Reset 'max' values at midnight</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstComStop"/></div> <div class="col-4"><input type="checkbox" name="invRstMaxMid"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3">Reset values when inverter status is 'not available'</div> <div class="col-8">Start without time sync (useful in AP-Only-Mode)</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstNotAvail"/></div> <div class="col-4"><input type="checkbox" name="strtWthtTm"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3">Start without time sync (useful in AP-Only-Mode)</div> <div class="col-8">Read Grid Profile</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="strtWthtTm"/></div> <div class="col-4"><input type="checkbox" name="rdGrid"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3">Yield Effiency (should be between 0.95 and 0.96)</div> <div class="col-8">Yield Efficiency (Standard 1.0)</div>
<div class="col-4 col-sm-9"><input type="number" name="yldEff" step="any"/></div> <div class="col-4"><input type="number" name="yldEff" step="any"/></div>
</div> </div>
</fieldset> </fieldset>
</div> </div>
@ -196,7 +196,7 @@
<div class="col-12 col-sm-9"><input type="number" name="ntpPort"/></div> <div class="col-12 col-sm-9"><input type="number" name="ntpPort"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">NTP Intervall (in Minutes, min. 5 Minutes)</div> <div class="col-12 col-sm-3 my-2">NTP Interval (in Minutes, min. 5 Minutes)</div>
<div class="col-12 col-sm-9"><input type="number" name="ntpIntvl"/></div> <div class="col-12 col-sm-9"><input type="number" name="ntpIntvl"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
@ -230,10 +230,6 @@
<div class="col-12 col-sm-3 my-2">Offset (pre sunrise, post sunset)</div> <div class="col-12 col-sm-3 my-2">Offset (pre sunrise, post sunset)</div>
<div class="col-12 col-sm-9"><select name="sunOffs"></select></div> <div class="col-12 col-sm-9"><select name="sunOffs"></select></div>
</div> </div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Pause polling inverters during night</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="sunDisNightCom"/></div>
</div>
</fieldset> </fieldset>
</div> </div>
@ -263,7 +259,7 @@
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Topic</div> <div class="col-12 col-sm-3 my-2">Topic</div>
<div class="col-12 col-sm-9"><input type="text" name="mqttTopic" pattern="[A-Za-z0-9./#$%&=+_-]+" title="Invalid input" /></div> <div class="col-12 col-sm-9"><input type="text" name="mqttTopic" pattern="[\-\+A-Za-z0-9\.\/#\$%&=_]+" title="Invalid input" /></div>
</div> </div>
<p class="des">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)</p> <p class="des">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)</p>
<div class="row mb-3"> <div class="row mb-3">
@ -290,16 +286,14 @@
<div class="col-8 col-sm-3">Turn off while inverters are offline</div> <div class="col-8 col-sm-3">Turn off while inverters are offline</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="disp_pwr"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="disp_pwr"/></div>
</div> </div>
<div class="row mb-3"> <div id="screenSaver"></div>
<div class="col-8 col-sm-3">Enable Screensaver (pixel shifting, OLED only)</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="disp_pxshift"/></div>
</div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Luminance</div> <div class="col-12 col-sm-3 my-2">Luminance</div>
<div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="255"></select></div> <div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="255"></select></div>
</div> </div>
<p class="des">Pinout</p> <p class="des">Pinout</p>
<div id="dispPins"></div> <div id="dispPins"></div>
<div id="pirPin"></div>
</fieldset> </fieldset>
</div> </div>
@ -339,7 +333,6 @@
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}
<script type="text/javascript"> <script type="text/javascript">
var highestId = 0;
var maxInv = 0; var maxInv = 0;
var ts = 0; var ts = 0;
@ -363,6 +356,8 @@
[15, "D8 (GPIO15)"], [15, "D8 (GPIO15)"],
[16, "D0 (GPIO16 - no IRQ!)"] [16, "D0 (GPIO16 - no IRQ!)"]
]; ];
/*IF_ESP32*/
var esp32pins = [ var esp32pins = [
[255, "off / default"], [255, "off / default"],
[0, "GPIO0"], [0, "GPIO0"],
@ -371,26 +366,26 @@
[3, "RX (GPIO3)"], [3, "RX (GPIO3)"],
[4, "GPIO4"], [4, "GPIO4"],
[5, "GPIO5"], [5, "GPIO5"],
[12, "GPIO12"], [12, "GPIO12 (HSPI MISO)"],
[13, "GPIO13"], [13, "GPIO13 (HSPI MOSI)"],
[14, "GPIO14"], [14, "GPIO14 (HSPI SCLK)"],
[15, "GPIO15"], [15, "GPIO15"],
[16, "GPIO16"], [16, "GPIO16"],
[17, "GPIO17"], [17, "GPIO17"],
[18, "GPIO18"], [18, "GPIO18 (VSPI SCLK)"],
[19, "GPIO19"], [19, "GPIO19 (VSPI MISO)"],
[21, "GPIO21 (SDA)"], [21, "GPIO21 (SDA)"],
[22, "GPIO22 (SCL)"], [22, "GPIO22 (SCL)"],
[23, "GPIO23"], [23, "GPIO23 (VSPI MOSI)"],
[25, "GPIO25"], [25, "GPIO25"],
[26, "GPIO26"], [26, "GPIO26"],
[27, "GPIO27"], [27, "GPIO27"],
[32, "GPIO32"], [32, "GPIO32"],
[33, "GPIO33"], [33, "GPIO33"],
[34, "GPIO34"], [34, "GPIO34 (in only)"],
[35, "GPIO35"], [35, "GPIO35 (in only)"],
[36, "VP (GPIO36)"], [36, "VP (GPIO36, in only)"],
[39, "VN (GPIO39)"] [39, "VN (GPIO39, in only)"]
]; ];
var esp32s3pins = [ var esp32s3pins = [
[255, "off / default"], [255, "off / default"],
@ -440,13 +435,58 @@
[47, "GPIO47"], [47, "GPIO47"],
[48, "GPIO48"], [48, "GPIO48"],
]; ];
var esp32c3pins = [
[255, "off / default"],
[0, "GPIO0"],
[1, "GPIO1"],
[2, "GPIO2"],
[3, "GPIO3"],
[4, "GPIO4"],
[5, "GPIO5"],
[6, "GPIO6"],
[7, "GPIO7"],
[8, "GPIO8"],
[9, "GPIO9"],
[10, "GPIO10"],
[11, "GPIO11"],
[12, "GPIO12 (PSRAM/FLASH)"],
[13, "GPIO13 (PSRAM/FLASH)"],
[14, "GPIO14 (PSRAM/FLASH)"],
[15, "GPIO15 (PSRAM/FLASH)"],
[16, "GPIO16 (PSRAM/FLASH)"],
[17, "GPIO17 (PSRAM/FLASH)"],
[18, "GPIO18 (DONT USE - USB-)"],
[19, "GPIO19 (DONT USE - USB+)"],
[20, "GPIO20 (RX)"],
[21, "GPIO21 (TX)"],
];
/*ENDIF_ESP32*/
var nrfPa = [
[0, "MIN (recommended)"],
[1, "LOW"],
[2, "HIGH"],
[3, "MAX (experimental)"]
];
var esp32cmtPa = [];
var esp32cmtFreq = [];
/*IF_ESP32*/
var freqFmt = new Intl.NumberFormat('en-US', {
minimumIntegerDigits: 3,
minimumFractionDigits: 2
});
for(var i = 0; i < 31; i++) {
esp32cmtPa.push([i, String(i-10) + " dBm"]);
}
for(var i = 12; i < 41; i++) {
esp32cmtFreq.push([i, freqFmt.format(860 + i*0.25) + " MHz"]);
}
/*ENDIF_ESP32*/
var led_high_active = [ var led_high_active = [
[0, "low active"], [0, "low active"],
[1, "high active"], [1, "high active"],
]; ];
const re = /1[1,3][2,4,6,8][1,2,4].*/;
window.onload = function() { window.onload = function() {
for(it of document.getElementsByClassName("s_collapsible")) { for(it of document.getElementsByClassName("s_collapsible")) {
it.addEventListener("click", function() { it.addEventListener("click", function() {
@ -465,12 +505,6 @@
}); });
} }
document.getElementById("btnAdd").addEventListener("click", function() {
if(highestId <= (maxInv-1)) {
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":6,"ch_max_pwr":[0,0,0,0,0,0],"ch_name":["","","","","",""],"ch_yield_cor":[0,0,0,0,0,0]}'), highestId);
}
});
function apiCbWifi(obj) { function apiCbWifi(obj) {
var e = document.getElementById("networks"); var e = document.getElementById("networks");
selDelAllOpt(e); selDelAllOpt(e);
@ -483,7 +517,7 @@
function apiCbNtp(obj) { function apiCbNtp(obj) {
var e = document.getElementById("apiResultNtp"); var e = document.getElementById("apiResultNtp");
if(obj["success"]) if(obj["success"])
e.innerHTML = "command excuted, set new time ..."; e.innerHTML = "command executed, set new time ...";
else else
e.innerHTML = "Error: " + obj["error"]; e.innerHTML = "Error: " + obj["error"];
} }
@ -497,7 +531,7 @@
function apiCbMqtt(obj) { function apiCbMqtt(obj) {
var e = document.getElementById("apiResultMqtt"); var e = document.getElementById("apiResultMqtt");
if(obj["success"]) if(obj["success"])
e.innerHTML = "command excuted"; e.innerHTML = "command executed";
else else
e.innerHTML = "Error: " + obj["error"]; e.innerHTML = "Error: " + obj["error"];
} }
@ -538,12 +572,8 @@
} }
function delIv() { function delIv() {
var id = this.id.substring(0,4); var id = this.id.substring(0, this.id.length-3);
var e = document.getElementsByName(id + "Addr")[0]; document.getElementById(id).remove();
e.value = "";
e.dispatchEvent(new Event("keyup"));
e.dispatchEvent(new Event("change"));
document.getElementsByName(id + "Name")[0].value = "";
} }
function mlCb(id, des, chk=false) { function mlCb(id, des, chk=false) {
@ -563,99 +593,23 @@
]); ]);
} }
function ivHtml(obj, id) { function getFreeId() {
highestId = id + 1; var id = 0;
if(highestId == maxInv) while(id < maxInv) {
setHide("btnAdd", true); if(null == document.getElementById("inv" + id))
return id;
var iv = document.getElementById("inverter"); id++;
iv.appendChild(des("Inverter " + id));
id = "inv" + id;
var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input");
iv.append(
mlCb(id + "Enable", "Communication Enable", obj["enabled"]),
mlE("Serial Number (12 digits)*", addr)
);
['keyup', 'change'].forEach(function(evt) {
addr.addEventListener(evt, (e) => {
var serial = addr.value.substring(0,4);
var max = 0;
for(var i=0;i<6;i++) {
setHide(id+"ModPwr"+i, true);
setHide(id+"ModName"+i, true);
setHide(id+"YieldCor"+i, true);
}
setHide("row"+id+"ModPwr", true);
setHide("row"+id+"ModName", true);
setHide("row"+id+"YieldCor", true);
if(serial.charAt(0) == 1) {
if((serial.charAt(1) == 0) || (serial.charAt(1) == 1) || (serial.charAt(1) == 3)) {
if((serial.charAt(3) == 1) || (serial.charAt(3) == 2) || (serial.charAt(3) == 4)) {
switch(serial.charAt(2)) {
case "2": max = 1; break;
case "4": max = 2; break;
case "6": max = 4; break;
case "8": max = 6; break;
}
}
}
}
if(max != 0) {
for(var i=0;i<max;i++) {
setHide(id+"ModPwr"+i, false);
setHide(id+"ModName"+i, false);
setHide(id+"YieldCor"+i, false);
}
setHide("row"+id+"ModPwr", false);
setHide("row"+id+"ModName", false);
setHide("row"+id+"YieldCor", false);
}
})
});
iv.append(mlE("Name*", inp(id + "Name", obj["name"], 16, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input")));
for(var j of [
["ModPwr", "ch_max_pwr", "Max Module Power (Wp)", 4, "[0-9]+"],
["ModName", "ch_name", "Module Name", 15, null],
["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 8, "[0-9-\.]+"]]) {
var cl = (re.test(obj["serial"])) ? "" : " hide";
i = 0;
arrIn = [];
for(it of obj[j[1]]) {
arrIn.push(ml("div", {class: "col-3 "},
inp(id + j[0] + i, it, j[3], [], id + j[0] + i, "text", j[4], "Invalid input")
));
i++;
}
iv.append(
ml("div", {class: "row mb-2 mb-sm-3" + cl, id: "row" + id + j[0]}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, j[2]),
ml("div", {class: "col-12 col-sm-9"},
ml("div", {class: "row"}, arrIn)
)
])
);
} }
return null;
var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button");
del.addEventListener("click", delIv);
iv.append(mlE("Delete", del));
} }
function ivGlob(obj) { function ivGlob(obj) {
for(var i of [["invInterval", "interval"], ["invRetry", "retries"], ["yldEff", "yldEff"]]) for(var i of [["invInterval", "interval"], ["yldEff", "yldEff"], ["invGap", "gap"]])
document.getElementsByName(i[0])[0].value = obj[i[1]]; document.getElementsByName(i[0])[0].value = obj[i[1]];
for(var i of [["Mid", "rstMid"], ["ComStop", "rstComStop"], ["NotAvail", "rstNAvail"]]) for(var i of ["Mid", "ComStop", "NotAvail", "MaxMid"])
document.getElementsByName("invRst"+i[0])[0].checked = obj[i[1]]; document.getElementsByName("invRst"+i)[0].checked = obj["rst" + i];
document.getElementsByName("strtWthtTm")[0].checked = obj["strtWthtTm"]; document.getElementsByName("strtWthtTm")[0].checked = obj["strtWthtTm"];
document.getElementsByName("rdGrid")[0].checked = obj["rdGrid"];
} }
function parseSys(obj) { function parseSys(obj) {
@ -668,7 +622,7 @@
if(!obj["pwd_set"]) if(!obj["pwd_set"])
e.value = ""; e.value = "";
var d = document.getElementById("prot_mask"); var d = document.getElementById("prot_mask");
var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"]; var a = ["Index", "Live", "Webserial", "Settings", "Update", "System"];
var el = []; var el = [];
for(var i = 0; i < 6; i++) { for(var i = 0; i < 6; i++) {
var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i)); var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i));
@ -693,10 +647,233 @@
} }
function parseIv(obj) { function parseIv(obj) {
for(var i = 0; i < obj.inverter.length; i++)
ivHtml(obj.inverter[i], i);
ivGlob(obj);
maxInv = obj["max_num_inverters"]; maxInv = obj["max_num_inverters"];
var lines = [];
lines.push(ml("tr", {}, [
ml("th", {style: "width: 10%; text-align: center;"}, ""),
ml("th", {}, "Name"),
ml("th", {}, "Serial"),
ml("th", {style: "width: 10%; text-align: center;"}, "Edit"),
ml("th", {style: "width: 10%; text-align: center;"}, "Delete")
]));
for(let i = 0; i < obj.inverter.length; i++) {
lines.push(ml("tr", {}, [
ml("td", {}, badge(obj.inverter[i].enabled, (obj.inverter[i].enabled) ? "enabled" : "disabled")),
ml("td", {}, obj.inverter[i].name),
ml("td", {}, String(obj.inverter[i].serial)),
ml("td", {style: "text-align: center;", onclick: function() {ivModal(obj.inverter[i]);}}, svg(iconGear, 25, 25, "icon icon-fg pointer")),
ml("td", {style: "text-align: center; ", onclick: function() {ivDel(obj.inverter[i]);}}, svg(iconDel, 25, 25, "icon icon-fg pointer"))
]));
}
var add = new Object();
add.id = obj.inverter.length;
add.name = "";
add.enabled = true;
add.ch_max_pwr = [400,400,400,400,400,400];
add.ch_name = [];
add.ch_yield_cor = [];
add.freq = 12;
add.pa = 30;
add.add2total = true;
var e = document.getElementById("inverter");
e.innerHTML = ""; // remove all childs
e.append(ml("table", {class: "table"}, ml("tbody", {}, lines)));
if(obj.max_num_inverters > obj.inverter.length)
e.append(ml("div", {class: "row my-3"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "add Inverter", class: "btn", onclick: function() { ivModal(add); }}, null))));
ivGlob(obj);
}
function ivModal(obj) {
var lines = [];
lines.push(ml("tr", {}, [
ml("th", {style: "width: 10%;"}, "Input"),
ml("th", {}, "Max Module Power [Wp]"),
ml("th", {}, "Name (optional)"),
ml("th", {}, "Yield Correction [kWh] (optional)")
]));
for(let i = 0; i < 6; i++) {
lines.push(ml("tr", {id: "ch"+i}, [
ml("td", {}, String(i+1)),
ml("td", {}, ml("input", {name: "ch_p"+i, class: "text", type: "number", max: 999, value: obj.ch_max_pwr[i]}, null)),
ml("td", {}, ml("input", {name: "ch_n"+i, class: "text", type: "text", maxlength: 15, value: (undefined === obj.ch_name[i]) ? "" : obj.ch_name[i]}, null)),
ml("td", {}, ml("input", {name: "yld_c"+i, class: "text", type: "number", max: 999999, value: obj.ch_yield_cor[i], step: "0.001"}, null))
]));
}
var cbEn = ml("input", {name: "enable", type: "checkbox"}, null);
var cbDisNightCom = ml("input", {name: "disnightcom", type: "checkbox"}, null);
var cbAddTotal = ml("input", {name: "add2total", type: "checkbox"}, null);
cbEn.checked = (obj.enabled);
cbDisNightCom.checked = (obj.disnightcom);
cbAddTotal.checked = (obj.add2total);
var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null);
var html = ml("div", {}, [
tabs(["General", "Inputs", "Radio", "Advanced"]),
ml("div", {id: "divGeneral", class: "tab-content"}, [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-2"}, "Enable"),
ml("div", {class: "col-10"}, cbEn)
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-2 mt-2"}, "Serial"),
ml("div", {class: "col-10"}, ser)
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-2 mt-2"}, "Name"),
ml("div", {class: "col-10"}, ml("input", {name: "name", class: "text", type: "text", value: obj.name}, null))
])
]),
ml("div", {id: "divInputs", class: "tab-content hide"}, [
ml("div", {class: "row mb-3"},
ml("table", {class: "table"},
ml("tbody", {}, lines)
)
)
]),
ml("div", {id: "divRadio", class: "tab-content hide"}, [
ml("input", {type: "hidden", name: "isnrf"}, null),
ml("div", {id: "setcmt"}, [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-3 mt-2"}, "Frequency"),
ml("div", {class: "col-9"}, sel("freq", esp32cmtFreq, obj.freq))
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-3 mt-2"}, "Power Level"),
ml("div", {class: "col-9"}, sel("cmtpa", esp32cmtPa, obj.pa))
]),
]),
ml("div", {id: "setnrf"},
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-3 mt-2"}, "Power Level"),
ml("div", {class: "col-9"}, sel("nrfpa", nrfPa, obj.pa))
]),
),
]),
ml("div", {id: "divAdvanced", class: "tab-content hide"}, [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-10"}, "Pause communication during night (lat. and lon. need to be set)"),
ml("div", {class: "col-2"}, cbDisNightCom)
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-10"}, "Include inverter to sum of total (should be checked by default)"),
ml("div", {class: "col-2"}, cbAddTotal)
])
]),
ml("div", {class: "row mt-5"}, [
ml("div", {class: "col-8", id: "res"}, ""),
ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "save", class: "btn", onclick: function() { ivSave(); }}, null))
])
]);
['keyup', 'change'].forEach(function(evt) {
ser.addEventListener(evt, (e) => {
var sn = parseInt(ser.value, 16);
sn = Math.floor(sn / Math.pow(2, 32));
var max = 1;
switch(sn & 0x00f0) {
case 0x0010: max = 1; break;
case 0x0040: max = 2; break;
case 0x0060: max = 4; break;
case 0x0080: max = 6; break;
}
for(var i = 0; i < 6; i++) {
setHide("ch"+i, (i >= max));
}
var nrf = true;
switch(sn & 0xff00) {
case 0x1000: nrf = true; break;
case 0x1100:
switch(sn & 0x000f) {
case 0x0004: nrf = false; break;
default: nrf = true; break;
}
break;
case 0x1300: nrf = false; break;
}
setHide("setcmt", nrf);
setHide("setnrf", !nrf);
document.getElementsByName("isnrf")[0].value = nrf;
})
});
modal("Edit inverter " + obj.name, html);
ser.dispatchEvent(new Event('change'));
function ivSave() {
var o = new Object();
o.cmd = "save_iv";
o.id = obj.id;
o.ser = parseInt(document.getElementsByName("ser")[0].value, 16);
o.name = document.getElementsByName("name")[0].value;
o.en = document.getElementsByName("enable")[0].checked;
o.ch = [];
for(let i = 0; i < 6; i++) {
var q = new Object();
q.pwr = document.getElementsByName("ch_p"+i)[0].value;
q.name = document.getElementsByName("ch_n"+i)[0].value;
q.yld = document.getElementsByName("yld_c"+i)[0].value;
o.ch.push(q);
}
if("true" == document.getElementsByName("isnrf")[0].value)
o.pa = document.getElementsByName("nrfpa")[0].value;
else
o.pa = document.getElementsByName("cmtpa")[0].value;
o.freq = document.getElementsByName("freq")[0].value;
o.disnightcom = document.getElementsByName("disnightcom")[0].checked;
o.add2total = document.getElementsByName("add2total")[0].checked;
getAjax("/api/setup", cb, "POST", JSON.stringify(o));
}
function cb(obj2) {
var e = document.getElementById("res");
if(!obj2.success)
e.innerHTML = "error: " + obj2.error;
else {
modalClose();
getAjax("/api/inverter/list", parseIv);
}
}
}
function ivDel(obj) {
var html = ml("div", {class: "row"}, [
ml("div", {class: "col-9"}, "do you realy want to delete inverter " + obj.name + "?"),
ml("div", {class: "col-3 a-r"}, ml("div", {class: "col-4 a-r"}, ml("input", {type: "button", value: "yes", class: "btn", onclick: function() { del(); }}, null)))
]);
modal("Delete inverter " + obj.name, html);
function del() {
var o = new Object();
o.cmd = "save_iv";
o.id = obj.id;
o.ser = 0;
o.name = "";
o.en = false;
o.ch = [];
for(let i = 0; i < 6; i++) {
var q = new Object();
q.pwr = 0;
q.name = "";
q.yld = 0;
o.ch.push(q);
}
getAjax("/api/setup", cb, "POST", JSON.stringify(o));
}
function cb(obj) {
if(obj.success) {
modalClose();
getAjax("/api/inverter/list", parseIv);
}
}
} }
function parseMqtt(obj) { function parseMqtt(obj) {
@ -712,7 +889,6 @@
function parseSun(obj) { function parseSun(obj) {
document.getElementsByName("sunLat")[0].value = obj["lat"]; document.getElementsByName("sunLat")[0].value = obj["lat"];
document.getElementsByName("sunLon")[0].value = obj["lon"]; document.getElementsByName("sunLon")[0].value = obj["lon"];
document.getElementsByName("sunDisNightCom")[0].checked = obj["disnightcom"];
const sel = document.getElementsByName("sunOffs")[0]; const sel = document.getElementsByName("sunOffs")[0];
for(var i = 0; i <= 60; i++) { for(var i = 0; i <= 60; i++) {
sel.appendChild(opt(i, i + " minutes", (i == (obj["offs"] / 60)))); sel.appendChild(opt(i, i + " minutes", (i == (obj["offs"] / 60))));
@ -721,13 +897,19 @@
function parsePinout(obj, type, system) { function parsePinout(obj, type, system) {
var e = document.getElementById("pinout"); var e = document.getElementById("pinout");
pins = [['led0', 'pinLed0'], ['led1', 'pinLed1']]; var pinList = esp8266pins;
/*IF_ESP32*/
var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins;
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
/*ENDIF_ESP32*/
pins = [['led0', 'pinLed0', 'At least one inverter is producing'], ['led1', 'pinLed1', 'MqTT connected']];
for(p of pins) { for(p of pins) {
e.append( e.append(
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), ml("div", {class: "col-12 col-sm-3 my-2"}, p[2]),
ml("div", {class: "col-12 col-sm-9"}, ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, obj[p[0]]) sel(p[1], pinList, obj[p[0]])
) )
]) ])
); );
@ -736,8 +918,12 @@
ml("div", { class: "row mb-3" }, [ ml("div", { class: "row mb-3" }, [
ml("div", { class: "col-12 col-sm-3 my-2" }, "LED polarity"), ml("div", { class: "col-12 col-sm-3 my-2" }, "LED polarity"),
ml("div", { class: "col-12 col-sm-9" }, ml("div", { class: "col-12 col-sm-9" },
sel('pinLedHighActive', led_high_active, obj['led_high_active']) sel('pinLedHighActive', led_high_active, obj.led_high_active)
) )
]),
ml("div", { class: "row mb-3" }, [
ml("div", { class: "col-12 col-sm-3 my-2" }, "LED luminance (0-255)"),
ml("div", { class: "col-12 col-sm-9" }, ml("input", {class: "text", type: "number", name: "pinLedLum", value: obj.led_lum, min: 0, max: 255}, null))
]) ])
) )
} }
@ -747,6 +933,13 @@
var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox"); var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox");
en.checked = obj["en"]; en.checked = obj["en"];
var pinList = esp8266pins;
/*IF_ESP32*/
var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins;
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
/*ENDIF_ESP32*/
e.replaceChildren ( e.replaceChildren (
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-3 my-2"}, "NRF24 Enable"), ml("div", {class: "col-8 col-sm-3 my-2"}, "NRF24 Enable"),
@ -764,29 +957,21 @@
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"}, ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, objPin[p[0]]) sel(p[1], pinList, objPin[p[0]])
) )
]) ])
); );
} }
e.append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, "Power Level"),
ml("div", {class: "col-12 col-sm-9"},
sel("rf24Power", [
[0, "MIN"],
[1, "LOW"],
[2, "HIGH"],
[3, "MAX"]
], obj["power_level"])
)
])
);
} }
/*IF_ESP32*/
function parseCmtRadio(obj, type, system) { function parseCmtRadio(obj, type, system) {
var e = document.getElementById("cmt"); var e = document.getElementById("cmt");
var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox"); var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox");
var pinList = esp32pins;
if ("ESP32-S3" == system["chip_model"]) pinList = esp32s3pins;
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
en.checked = obj["en"]; en.checked = obj["en"];
e.replaceChildren ( e.replaceChildren (
@ -795,65 +980,75 @@
ml("div", {class: "col-4 col-sm-9"}, en) ml("div", {class: "col-4 col-sm-9"}, en)
]) ])
); );
pins = [['csb', 'pinCsb'], ['fcsb', 'pinFcsb'], ['irq', 'pinGpio3']]; pins = [['sclk', 'pinCmtSclk'], ['sdio', 'pinSdio'], ['csb', 'pinCsb'], ['fcsb', 'pinFcsb'], ['gpio3', 'pinGpio3']];
for(p of pins) { for(p of pins) {
e.append( e.append(
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"}, ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, obj[p[0]]) sel(p[1], pinList, obj[p[0]])
) )
]) ])
); );
} }
} }
/*ENDIF_ESP32*/
function parseSerial(obj) { function parseSerial(obj) {
for(var i of [["serEn", "show_live_data"], ["serDbg", "debug"]]) for(var i of [["serEn", "show_live_data"], ["serDbg", "debug"], ["priv", "priv"], ["wholeTrace", "wholeTrace"]])
document.getElementsByName(i[0])[0].checked = obj[i[1]]; document.getElementsByName(i[0])[0].checked = obj[i[1]];
document.getElementsByName("serIntvl")[0].value = obj["interval"];
} }
function parseDisplay(obj, type, system) { function parseDisplay(obj, type, system) {
for(var i of ["disp_pwr", "disp_pxshift"]) var pinList = esp8266pins;
/*IF_ESP32*/
var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model) pinList = esp32s3pins;
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
/*ENDIF_ESP32*/
for(var i of ["disp_pwr"])
document.getElementsByName(i)[0].checked = obj[i]; document.getElementsByName(i)[0].checked = obj[i];
var e = document.getElementById("dispPins"); var dpins_elem = document.getElementById("dispPins");
//KEEP this order !!! //KEEP this order !!!
var pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst']]; var pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst']];
if("ESP32" == type) /*IF_ESP32*/
pins.push(['busy', 'disp_bsy']); pins.push(['busy', 'disp_bsy']);
/*ENDIF_ESP32*/
for(p of pins) { for(p of pins) {
e.append( dpins_elem.append(
ml("div", {class: "row mb-3", id: "row_" + p[1]}, [ ml("div", {class: "row mb-3", id: "row_" + p[1]}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"}, ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : ("ESP32-S3" == system["chip_model"]) ? esp32s3pins : esp32pins, obj[p[1]]) sel(p[1], pinList, obj[p[1]])
) )
]) ])
); );
} }
// keep display types grouped // keep display types grouped
var opts = [[0, "None"], [2, "SH1106 1.3\" 128X64"], [5, "SSD1306 0.66\" 64X48 (Wemos OLED Shield)"], [4, "SSD1306 0.91\" 128X32"], [1, "SSD1306 0.96\" 128X64"], [3, "Nokia5110"]]; var opts = [[0, "None"],
if("ESP32" == type) [2, "SH1106 128x64 (1.3\")"],
[5, "SSD1306 64x48 (0.66\" Wemos OLED Shield)"],
[4, "SSD1306 128x32 (0.91\")"],
[1, "SSD1306 128x64 (0.96\", 1.54\")"],
[6, "SSD1309 128X64 (2.42\")"],
[3, "PCD8544 84X48 (1.6\" Nokia 5110)"]];
/*IF_ESP32*/
opts.push([10, "ePaper"]); opts.push([10, "ePaper"]);
var dispType = sel("disp_typ", opts, obj["disp_typ"]); /*ENDIF_ESP32*/
var dtype_sel = sel("disp_typ", opts, obj["disp_typ"]);
document.getElementById("dispType").append( document.getElementById("dispType").append(
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"), ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"),
ml("div", {class: "col-12 col-sm-9"}, dispType) ml("div", {class: "col-12 col-sm-9"}, dtype_sel)
]) ])
); );
dispType.addEventListener('change', (e) => {
hideDispPins(pins, parseInt(e.target.value))
});
opts = [[0, "0&deg;"], [2, "180&deg;"]]; opts = [[0, "0&deg;"], [2, "180&deg;"]];
if("ESP32" == type) { /*IF_ESP32*/
opts.push([1, "90&deg;"]); opts.push([1, "90&deg;"]);
opts.push([3, "270&deg;"]); opts.push([3, "270&deg;"]);
} /*ENDIF_ESP32*/
document.getElementById("dispRot").append( document.getElementById("dispRot").append(
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, "Rotation"), ml("div", {class: "col-12 col-sm-3 my-2"}, "Rotation"),
@ -861,7 +1056,35 @@
]) ])
); );
var opts1 = [[0, "off"], [1, "pixel shift"], [2, "motion sensor"]];
var screensaver_sel = sel("disp_screensaver", opts1, obj["disp_screensaver"]);
screensaver_sel.id = 'disp_screensaver';
document.getElementById("screenSaver").append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, "Screensaver (OLED only)"),
ml("div", {class: "col-12 col-sm-9"}, screensaver_sel)
])
);
var esp8266pirpins = [[255, "off / default"],
[17, "A0"]];
document.getElementById("pirPin").append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, "PIR sensor"),
ml("div", {class: "col-12 col-sm-9"}, sel("pir_pin", ("ESP8266" == type) ? esp8266pirpins : pinList, obj["pir_pin"]))
])
);
document.getElementsByName("disp_cont")[0].value = obj["disp_cont"]; document.getElementsByName("disp_cont")[0].value = obj["disp_cont"];
dtype_sel.addEventListener('change', function(e) {
hideDispPins(pins, parseInt(e.target.value))
});
document.getElementById("disp_screensaver").addEventListener('change', function(e) {
hideDispPins(pins, parseInt(dtype_sel.value))
});
hideDispPins(pins, obj.disp_typ); hideDispPins(pins, obj.disp_typ);
} }
@ -875,7 +1098,8 @@
[2, [1,1,0,0,0,0]], //SH1106_128X64 [2, [1,1,0,0,0,0]], //SH1106_128X64
[3, [1,1,1,1,0,0]], //PCD8544_84X48 /nokia5110 [3, [1,1,1,1,0,0]], //PCD8544_84X48 /nokia5110
[4, [1,1,0,0,0,0]], //SSD1306_128X32 [4, [1,1,0,0,0,0]], //SSD1306_128X32
[5, [1,1,0,0,0,0]], //SSD1306_64X48 [5, [1,1,0,0,0,0]], //SSD1306_128x64
[6, [1,1,0,0,0,0]], //SSD1309_128x64
[10, [1,1,1,1,1,1]] //ePaper [10, [1,1,1,1,1,1]] //ePaper
]) ])
for(var i = 0; i < pins.length; i++) { for(var i = 0; i < pins.length; i++) {
@ -887,6 +1111,15 @@
cl.add("hide"); cl.add("hide");
} }
} }
var screenSaver = document.getElementById("disp_screensaver").value;
if (2==screenSaver) { // show pir pin only for motion screensaver
setHide("pirPin", false);
}
else { // no pir pin for all others
setHide("pirPin", true);
}
} }
function tick() { function tick() {
@ -895,16 +1128,17 @@
function parse(root) { function parse(root) {
if(null != root) { if(null != root) {
parseSys(root["system"]);
parseGeneric(root["generic"]); parseGeneric(root["generic"]);
parseSys(root["system"]);
parseStaticIp(root["static_ip"]); parseStaticIp(root["static_ip"]);
parseMqtt(root["mqtt"]); parseMqtt(root["mqtt"]);
parseNtp(root["ntp"]); parseNtp(root["ntp"]);
parseSun(root["sun"]); parseSun(root["sun"]);
parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]); parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]);
parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]); parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]);
if(root["generic"]["esp_type"] == "ESP32") /*IF_ESP32*/
parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]); parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]);
/*ENDIF_ESP32*/
parseSerial(root["serial"]); parseSerial(root["serial"]);
parseDisplay(root["display"], root["system"]["esp_type"], root["system"]); parseDisplay(root["display"], root["system"]["esp_type"], root["system"]);
getAjax("/api/inverter/list", parseIv); getAjax("/api/inverter/list", parseIv);

118
src/web/html/style.css

@ -20,6 +20,15 @@ fieldset, input[type=submit], .btn {
border-radius: 4px; border-radius: 4px;
} }
input[type=file] {
width: 100%;
}
textarea {
color: var(--fg);
background-color: var(--bg);
}
#live span { #live span {
color: var(--fg2); color: var(--fg2);
} }
@ -105,7 +114,11 @@ svg.icon {
fill: var(--success); fill: var(--success);
} }
.wifi { .icon-fg {
fill: var(--fg);
}
.icon-fg2 {
fill: var(--fg2); fill: var(--fg2);
} }
@ -282,6 +295,9 @@ p {
.a-r { text-align: right; } .a-r { text-align: right; }
.a-c { text-align: center; } .a-c { text-align: center; }
.d-none { display: none !important; }
.d-block { display: block !important; }
.row > * { .row > * {
padding-left: 0.5rem; padding-left: 0.5rem;
padding-right: 0.5rem; padding-right: 0.5rem;
@ -320,6 +336,9 @@ p {
.fs-sm-6 { font-size: 1.5rem; } .fs-sm-6 { font-size: 1.5rem; }
.fs-sm-7 { font-size: 1.25rem; } .fs-sm-7 { font-size: 1.25rem; }
.fs-sm-8 { font-size: 1rem; } .fs-sm-8 { font-size: 1rem; }
.d-sm-block { display: block !important;}
.d-sm-none { display: none !important; }
} }
/* md */ /* md */
@ -338,6 +357,20 @@ p {
.col-md-12 { width: 100%; } .col-md-12 { width: 100%; }
} }
table {
border-collapse: collapse;
width: 100%;
}
th {
text-align: inherit;
}
.table td, .table th {
padding: .75rem;
border-bottom: 1px solid var(--table-border);
}
#wrapper { #wrapper {
min-height: 100%; min-height: 100%;
} }
@ -507,7 +540,7 @@ input[type=text], input[type=password], select, input[type=number] {
input:invalid { input:invalid {
border: 2px solid #f00 !important; border: 2px solid #f00 !important;
background-color: #400 !important; background-color: var(--invalid-bg) !important;
} }
input.sh { input.sh {
@ -677,7 +710,7 @@ div.hr {
margin: 1.75rem auto; margin: 1.75rem auto;
} }
.modal { .modal, #modal-wrapper {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
@ -686,6 +719,10 @@ div.hr {
display: block; display: block;
} }
.modal {
height: calc(100% - 3.5rem);
}
#modal-wrapper { #modal-wrapper {
background-color: #000; background-color: #000;
opacity: 0.5; opacity: 0.5;
@ -699,8 +736,10 @@ div.hr {
width: 100%; width: 100%;
background-color: var(--modal-bg); background-color: var(--modal-bg);
background-clip: padding-box; background-clip: padding-box;
border: 1px solid rgba(0,0,0,.2); border: 1px solid var(--fg);
flex-direction: column; flex-direction: column;
max-height: 100%;
overflow: hidden;
} }
.modal-header { .modal-header {
@ -708,7 +747,7 @@ div.hr {
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
padding: 1rem; padding: 1rem;
border-bottom: 1px solid #e9ecef; border-bottom: 1px solid var(--table-border);
} }
.modal-header .close { .modal-header .close {
@ -717,7 +756,8 @@ div.hr {
} }
.modal-body { .modal-body {
padding: 1rem 1rem 2rem 1rem; padding: 1rem;
overflow-y: auto;
} }
.close { .close {
@ -743,3 +783,69 @@ h5 {
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
.badge-success {
color: #fff;
background-color: #28a745;
}
.badge-warning {
color: #212529;
background-color: #ffc107;
}
.badge-error {
color: #fff;
background-color: #dc3545;
}
.badge {
display: inline-block;
padding: .25em .4em;
font-size: 85%;
font-weight: 700;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: .25rem;
}
ul {
margin-top: 0;
}
.nav {
display: flex;
flex-wrap: wrap;
padding-left: 0;
list-style: none;
}
.nav-tabs {
border-bottom: 1px solid var(--fg);
}
.nav-tabs .nav-link {
margin-bottom: -1px;
border: 1px solid transparent;
border-top-left-radius: .25rem;
border-top-right-radius: .25rem;
}
.nav-link {
display: block;
padding: .5rem 1rem;
text-decoration: none;
color: var(--fg);
}
.nav-tabs .nav-link.active {
border-color: var(--primary) var(--fg) var(--bg);
border-top-width: 4px;
}
.nav-link:hover, .nav-link:visited {
background-color: var(--input-bg);
color: var(--fg);
}

140
src/web/html/system.html

@ -8,10 +8,7 @@
{#HTML_NAV} {#HTML_NAV}
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<pre id="stat"></pre>
<div id="info" class="col-sm-12 col-md-6 mt-3"></div> <div id="info" class="col-sm-12 col-md-6 mt-3"></div>
<div id="radio" class="col-sm-12 col-md-6 mt-3"></div>
<div id="sun" class="col-sm-12 col-md-6 mt-3"></div>
<div id="html" class="mt-3 mb-3"></div> <div id="html" class="mt-3 mb-3"></div>
</div> </div>
</div> </div>
@ -23,94 +20,119 @@
parseRssi(obj); parseRssi(obj);
} }
function genTabRow(key, value) {
var r = div(["row", "p-1"]);
r.appendChild(div(["col"], key));
r.appendChild(div(["col"], value));
return r;
}
function parseSysInfo(obj) { function parseSysInfo(obj) {
const data = ["sdk", "cpu_freq", "chip_revision", const data = ["sdk", "cpu_freq", "chip_revision",
"chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime", "chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime",
"flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag", "flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag",
"max_free_blk", "version", "core_version", "reboot_reason"]; "max_free_blk", "version", "core_version", "reboot_reason"];
var main = document.getElementById("info"); lines = [];
var h = div(["head", "p-2"]);
var r = div(["row"]);
r.appendChild(div(["col", "a-c"], "System Information"));
h.appendChild(r);
main.appendChild(h);
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if(!data.includes(key) || (typeof value == 'undefined')) continue; if(!data.includes(key) || (typeof value == 'undefined')) continue;
main.appendChild(genTabRow(key, value)); lines.push(tr(key.replace('_', ' '), value));
} }
document.getElementById("info").append(
headline("System Information"),
ml("table", {class: "table"},
ml("tbody", {}, lines)
)
);
} }
function parseRadio(obj, stat) { function headline(text) {
const pa = ["MIN", "LOW", "HIGH", "MAX"]; return ml("div", {class: "head p-2 mt-3"}, ml("div", {class: "row"}, ml("div", {class: "col a-c"}, text)))
const datarate = ["1 MBps", "2 MBps", "250 kbps"]; }
var main = document.getElementById("radio"); function parseRadio(obj) {
var h = div(["head", "p-2"]); const dr = ["1 M", "2 M", "250 k"]
var r = div(["row"]);
r.appendChild(div(["col", "a-c"], "Radio"));
h.appendChild(r);
main.appendChild(h);
main.appendChild(genTabRow("nrf24l01" + (obj["isPVariant"] ? "+ " : ""), (obj["isconnected"] ? "is connected " : "is not connected "))); if(obj.radioNrf.en) {
lines = [
tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "not ") + "connected")),
tr("NRF24 Data Rate", dr[obj.radioNrf.dataRate] + "bps"),
tr("DTU Radio ID", obj.radioNrf.sn)
];
} else
lines = [tr("NRF24L01", badge(false, "not enabled"))];
if(obj["isconnected"]) { document.getElementById("info").append(
main.appendChild(genTabRow("Datarate", datarate[obj["DataRate"]])); headline("Radio NRF"),
main.appendChild(genTabRow("Power Level", pa[obj["power_level"]])); ml("table", {class: "table"},
ml("tbody", {}, lines)
)
);
/*IF_ESP32*/
if(obj.radioCmt.en) {
cmt = [
tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "not ") + "connected")),
tr("DTU Radio ID", obj.radioCmt.sn)
];
} else
cmt = [tr("CMT2300A", badge(false, "not enabled"))];
document.getElementById("info").append(
headline("Radio CMT"),
ml("table", {class: "table"},
ml("tbody", {}, cmt)
)
);
/*ENDIF_ESP32*/
} }
main.append( function parseMqtt(obj) {
genTabRow("TX count", stat["tx_cnt"]), if(obj.enabled) {
genTabRow("RX success", stat["rx_success"]), lines = [
genTabRow("RX fail", stat["rx_fail"]), tr("connected", badge(obj.connected, ((obj.connected) ? "true" : "false"))),
genTabRow("RX no answer", stat["rx_fail_answer"]), tr("#TX", obj.tx_cnt),
genTabRow("RX fragments", stat["frame_cnt"]), tr("#RX", obj.rx_cnt)
genTabRow("TX retransmits", stat["retransmits"]) ];
} else
lines = tr("enabled", badge(false, "false"));
document.getElementById("info").append(
headline("MqTT"),
ml("table", {class: "table"},
ml("tbody", {}, lines)
)
); );
} }
function parseIndex(obj) { function parseIndex(obj) {
if(obj["ts_sunrise"] > 0) { if(obj.ts_sunrise > 0) {
var h = div(["head", "p-2"]); document.getElementById("info").append(
var r = div(["row"]); headline("Sun"),
r.appendChild(div(["col", "a-c"], "Sun")); ml("table", {class: "table"},
h.appendChild(r); ml("tbody", {}, [
tr("Sunrise", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')),
document.getElementById("sun").append ( tr("Sunset", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')),
h, tr("Communication start", new Date((obj.ts_sunrise - obj.ts_offset) * 1000).toLocaleString('de-DE')),
genTabRow("Sunrise", new Date(obj["ts_sunrise"] * 1000).toLocaleString('de-DE')), tr("Communication stop", new Date((obj.ts_sunset + obj.ts_offset) * 1000).toLocaleString('de-DE')),
genTabRow("Sunset", new Date(obj["ts_sunset"] * 1000).toLocaleString('de-DE')), tr("Night behaviour", badge(obj.disNightComm, ((obj.disNightComm) ? "not" : "") + " communicating", "warning"))
genTabRow("Communication start", new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE')), ])
genTabRow("Communication stop", new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')), )
genTabRow("Night Communication", ((obj["disNightComm"]) ? "disabled" : "enabled"))
); );
} }
} }
function parse(obj) { function parse(obj) {
if(null != obj) { if(null != obj) {
parseGeneric(obj["generic"]); parseGeneric(obj.generic);
if(null != obj["refresh"]) { if(null != obj.refresh) {
var meta = document.createElement('meta'); var meta = document.createElement('meta');
meta.httpEquiv = "refresh" meta.httpEquiv = "refresh"
meta.content = obj["refresh"] + "; URL=" + obj["refresh_url"]; meta.content = obj.refresh + "; URL=" + obj.refresh_url;
document.getElementsByTagName('head')[0].appendChild(meta); document.getElementsByTagName('head')[0].appendChild(meta);
} } else if(null != obj.system) {
else { parseRadio(obj.system);
parseSysInfo(obj["system"]); parseMqtt(obj.system.mqtt);
parseRadio(obj["system"]["radio"], obj["system"]["statistics"]); parseSysInfo(obj.system);
getAjax('/api/index', parseIndex); getAjax('/api/index', parseIndex);
} }
document.getElementById("html").innerHTML = obj["html"]; document.getElementById("html").innerHTML = obj.html;
} }
} }

5
src/web/html/update.html

@ -12,9 +12,12 @@
<legend class="des">Select firmware file (*.bin)</legend> <legend class="des">Select firmware file (*.bin)</legend>
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8"> <form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update"> <input type="file" name="update">
<input type="button" class="btn" value="Update" onclick="hide()"> <input type="button" class="btn my-4" value="Update" onclick="hide()">
</form> </form>
</fieldset> </fieldset>
<div class="row mt-4">
<a href="https://fw.ahoydtu.de" target="_blank">Download latest Release and Development versions<a/>
</div>
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}

274
src/web/html/visualization.html

@ -19,7 +19,8 @@
var units, ivEn; var units, ivEn;
var mIvHtml = []; var mIvHtml = [];
var mNum = 0; var mNum = 0;
var total = Array(5).fill(0); var total = Array(6).fill(0);
var tPwrAck;
function parseGeneric(obj) { function parseGeneric(obj) {
if(true == exeOnce){ if(true == exeOnce){
@ -60,7 +61,7 @@
} }
function totals() { function totals() {
for(var i = 0; i < 5; i++) { for(var i = 0; i < 6; i++) {
total[i] = Math.round(total[i] * 100) / 100; total[i] = Math.round(total[i] * 100) / 100;
} }
@ -79,8 +80,9 @@
]), ]),
ml("div", {class: "hr"}), ml("div", {class: "hr"}),
ml("div", {class: "row"}, [ ml("div", {class: "row"}, [
numMid(total[3], "W", "DC Power"), numMid(total[3], "W", "Max Power"),
numMid(total[4], "var", "Reactive Power") numMid(total[4], "W", "DC Power"),
numMid(total[5], "var", "Reactive Power")
]) ])
]) ])
]) ])
@ -89,8 +91,9 @@
function ivHead(obj) { function ivHead(obj) {
if(0 != obj.status) { // only add totals if inverter is online if(0 != obj.status) { // only add totals if inverter is online
total[0] += obj.ch[0][2]; // P_AC total[0] += obj.ch[0][2]; // P_AC
total[3] += obj.ch[0][8]; // P_DC total[3] += obj.ch[0][11]; // MAX P_AC
total[4] += obj.ch[0][10]; // Q_AC total[4] += obj.ch[0][8]; // P_DC
total[5] += obj.ch[0][10]; // Q_AC
} }
total[1] += obj.ch[0][7]; // YieldDay total[1] += obj.ch[0][7]; // YieldDay
total[2] += obj.ch[0][6]; // YieldTotal total[2] += obj.ch[0][6]; // YieldTotal
@ -98,16 +101,28 @@
var t = span("&nbsp;&deg;C"); var t = span("&nbsp;&deg;C");
var clh = (0 == obj.status) ? "iv-h-dis" : "iv-h"; var clh = (0 == obj.status) ? "iv-h-dis" : "iv-h";
var clbg = (0 == obj.status) ? "iv-bg-dis" : "iv-bg"; var clbg = (0 == obj.status) ? "iv-bg-dis" : "iv-bg";
var pwrLimit = "n/a";
if(65535 != obj.power_limit_read) {
pwrLimit = obj.power_limit_read + "&nbsp;%";
if(0 != obj.max_pwr)
pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "W";
}
return ml("div", {class: "row mt-2"}, return ml("div", {class: "row mt-2"},
ml("div", {class: "col"}, [ ml("div", {class: "col"}, [
ml("div", {class: "p-2 " + clh}, ml("div", {class: "p-2 " + clh},
ml("div", {class: "row"}, [ ml("div", {class: "row"}, [
ml("div", {class: "col mx-2 mx-md-1"}, obj.name), ml("div", {class: "col mx-2 mx-md-1"}, ml("span", { class: "pointer", onclick: function() {
ml("div", {class: "col a-c"}, "Power limit " + ((obj.power_limit_read == 65535) ? "n/a" : (obj.power_limit_read + "&nbsp;%"))), getAjax("/api/inverter/version/" + obj.id, parseIvVersion);
}}, obj.name)),
ml("div", {class: "col a-c", onclick: function() {limitModal(obj)}}, [
ml("span", {class: "d-none d-sm-block pointer"}, "Active Power Control: " + pwrLimit),
ml("span", {class: "d-block d-sm-none pointer"}, "APC: " + pwrLimit)
]),
ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() { ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() {
getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm); getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm);
}}, ("Alarms: " + obj.alarm_cnt))), }}, ("Alarms: " + obj.alarm_cnt))),
ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5]) + t.innerText) ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5].toFixed(1)) + t.innerText)
]) ])
), ),
ml("div", {class: "p-2 " + clbg}, [ ml("div", {class: "p-2 " + clbg}, [
@ -166,21 +181,30 @@
]); ]);
} }
function tsInfo(ts) { function tsInfo(obj) {
var ageInfo = "Last received data requested at: "; var ageInfo = "Last received data requested at: ";
if(ts > 0) { if(obj.ts_last_success > 0) {
var date = new Date(ts * 1000); var date = new Date(obj.ts_last_success * 1000);
ageInfo += toIsoDateStr(date); ageInfo += toIsoDateStr(date);
} }
else else
ageInfo += "nothing received"; ageInfo += "nothing received";
if(obj.rssi > -127) {
if(obj.generation < 2)
ageInfo += " (RSSI: " + ((obj.rssi == -64) ? ">=" : "<") + " -64dBm)";
else
ageInfo += " (RSSI: " + obj.rssi + "dBm)";
}
return ml("div", {class: "mb-5"}, [ return ml("div", {class: "mb-5"}, [
ml("div", {class: "row p-1 ts-h mx-2"}, ml("div", {class: "row p-1 ts-h mx-2"},
ml("div", {class: "col"}, "") ml("div", {class: "col"}, "")
), ),
ml("div", {class: "row p-2 ts-bg mx-2"}, ml("div", {class: "row p-2 ts-bg mx-2"},
ml("div", {class: "col mx-2"}, ageInfo) ml("div", { class: "pointer col mx-2", onclick: function() {
getAjax("/api/inverter/radiostat/" + obj.id, parseIvRadioStats);
}}, ageInfo)
) )
]); ]);
} }
@ -200,7 +224,7 @@
ml("div", {}, [ ml("div", {}, [
ivHead(obj), ivHead(obj),
ml("div", {class: "row mb-2"}, chn), ml("div", {class: "row mb-2"}, chn),
tsInfo(obj.ts_last_success) tsInfo(obj)
]) ])
); );
@ -225,7 +249,7 @@
var offs = new Date().getTimezoneOffset() * -60; var offs = new Date().getTimezoneOffset() * -60;
html.push( html.push(
ml("div", {class: "row"}, [ ml("div", {class: "row"}, [
ml("div", {class: "col"}, ml("strong", {}, "String")), ml("div", {class: "col"}, ml("strong", {}, "Event")),
ml("div", {class: "col"}, ml("strong", {}, "ID")), ml("div", {class: "col"}, ml("strong", {}, "ID")),
ml("div", {class: "col"}, ml("strong", {}, "Start")), ml("div", {class: "col"}, ml("strong", {}, "Start")),
ml("div", {class: "col"}, ml("strong", {}, "End")) ml("div", {class: "col"}, ml("strong", {}, "End"))
@ -244,7 +268,225 @@
); );
} }
} }
modal("Alarms of inverter #" + obj.iv_id, ml("div", {}, html)); modal("Alarms of inverter " + obj.iv_name, ml("div", {}, html));
}
function parseIvVersion(obj) {
var model;
switch(obj.generation) {
case 0: model = "MI-"; break;
case 1: model = "HM-"; break;
case 2: model = "HMS-"; break;
case 3: model = "HMT-"; break;
default: model = "???-"; break;
}
model += String(obj.max_pwr) + " (Serial: " + obj.serial + ")";
var html = ml("table", {class: "table"}, [
ml("tbody", {}, [
tr("Model", model),
tr("Firmware Version / Build", String(obj.fw_ver) + " (build: " + String(obj.fw_date) + " " + String(obj.fw_time) + ")"),
tr("Hardware Version / Build", (obj.hw_ver/100).toFixed(2) + " (build: " + String(obj.prod_cw) + "/" + String(obj.prod_year) + ")"),
tr("Hardware Number", obj.part_num.toString(16)),
tr("Bootloader Version", (obj.boot_ver/100).toFixed(2)),
tr("Grid Profile", ml("input", {type: "button", value: "show", class: "btn", onclick: function() {
modalClose();
getAjax("/api/inverter/grid/" + obj.id, showGridProfile);
}}, null))
])
])
modal("Info for inverter " + obj.name, ml("div", {}, html))
}
function getGridValue(g) {
var val = (parseInt(g.grid.substring(g.offs*3, g.offs*3+2), 16) * 256)
+ parseInt(g.grid.substring(g.offs*3+3, g.offs*3+5), 16)
g.offs += 2
return val
}
function getGridIdentifier(g) {
return "0x" + getGridValue(g).toString(16).padStart(4, '0')
}
function getGridType(t, id) {
for(e of t) {
if(undefined !== e[id])
return e[id]
}
return null
}
function parseGridGroup(g) {
var id = getGridIdentifier(g)
var type = getGridType(g.info.grp_codes, id.substring(0, 4))
var content = []
content.push(ml("div", {class: "row"},
ml("div", {class: "col head p-2 mt-3"},
ml("div", {class: "col a-c"}, type + " (Code " + id + ")")
)
))
content.push(ml("div", {class: "row my-2"}, [
ml("div", {class: "col-4"}, ml("b", {}, "Name")),
ml("div", {class: "col-3"}, ml("b", {}, "Value")),
ml("div", {class: "col-3"}, ml("b", {}, "Range")),
ml("div", {class: "col-2"}, ml("b", {}, "Default"))
]))
for(e of g.info.group) {
if(Array.isArray(e[id])) {
for(e of e[id]) {
var v = String(getGridValue(g) / e.div);
var vt = (v !== String(e.def)) ? "b" : "span";
content.push(ml("div", {class: "row mt-2"}, [
ml("div", {class: "col-4"}, e.name),
ml("div", {class: "col-3"}, ml(vt, {}, v + ((undefined !== e.unit) ? " [" + e.unit + "]" : ""))),
ml("div", {class: "col-3"}, (undefined !== e.min) ? (e.min + " - " + e.max) : "n/a"),
ml("div", {class: "col-2"}, String(e.def))
]))
}
}
}
return ml("div", {class: "col"}, [...content])
}
function showGridProfile(obj) {
getJSON("/grid_info.json").then(data => {
var glob = {offs:0, grid:obj.grid, info: data}
var content = [];
var g = getGridType(glob.info.type, getGridIdentifier(glob))
if(null === g) {
if(0 == obj.grid.length) {
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "Profile was not read until now, maybe turned off?"))))
} else {
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "Unknown Profile"))))
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "Please open a new issue at https://github.com/lumapu/ahoy and copy the raw data into it."))))
content.push(ml("div", {class: "row"}, ml("div", {class: "col my-2"}, ml("pre", {}, obj.grid))))
}
} else {
content.push(ml("div", {class: "row"},
ml("div", {class: "col my-3"}, ml("h5", {}, g + " (Version " + getGridValue(glob).toString(16) + ")"))
))
while((glob.offs*3) < glob.grid.length) {
content.push(parseGridGroup(glob))
}
}
modal("Grid Profile for inverter " + obj.name, ml("div", {}, ml("div", {class: "col mb-2"}, [...content])))
})
}
function parseIvRadioStats(obj) {
var html = ml("table", {class: "table"}, [
ml("tbody", {}, [
tr2(["TX count", obj.tx_cnt, ""]),
tr2(["RX success", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + "%"]),
tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + "%"]),
tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + "%"]),
tr2(["RX fragments", obj.frame_cnt, ""]),
tr2(["TX retransmits", obj.retransmits, ""])
])
])
modal("Radio statistics for inverter " + obj.name, ml("div", {}, html))
}
function limitModal(obj) {
var opt = [["pct", "%"], ["watt", "W"]];
var html = ml("div", {}, [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-5 my-2"}, "Limit Value"),
ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number"}, "")),
ml("div", {class: "col-4 col-sm-2"}, sel("type", opt, "pct"))
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-5"}, "Keep limit over inverter restart"),
ml("div", {class: "col-4 col-sm-7"}, ml("input", {type: "checkbox", name: "keep"}))
]),
ml("div", {class: "row my-3"},
ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "Apply", class: "btn", onclick: function() {
applyLimit(obj.id);
}}, null))
),
ml("div", {class: "row my-4"}, [
ml("div", {class: "col-12 col-sm-5 my-2"}, "Control"),
ml("div", {class: "col col-sm-7 a-r"}, [
ml("input", {type: "button", value: "restart", class: "btn", onclick: function() {
applyCtrl(obj.id, "restart");
}}, null),
ml("input", {type: "button", value: "turn off", class: "btn mx-1", onclick: function() {
applyCtrl(obj.id, "power", 0);
}}, null),
ml("input", {type: "button", value: "turn on", class: "btn", onclick: function() {
applyCtrl(obj.id, "power", 1);
}}, null)
])
]),
ml("div", {class: "row mt-1"}, [
ml("div", {class: "col-12 col-sm-5 my-2"}, "Result"),
ml("div", {class: "col-sm-7 my-2"}, ml("span", {name: "pwrres"}, "-"))
])
]);
modal("Active Power Control for inverter " + obj.name, html);
}
function applyLimit(id) {
var cmd = "limit_";
if(!document.getElementsByName("keep")[0].checked)
cmd += "non";
cmd += "persistent_";
if(document.getElementsByName("type")[0].value == "pct")
cmd += "relative";
else
cmd += "absolute";
var val = document.getElementsByName("limit")[0].value;
if(isNaN(val))
val = 100;
var obj = new Object();
obj.id = id;
obj.cmd = cmd;
obj.val = val;
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
}
function applyCtrl(id, cmd, val=0) {
var obj = new Object();
obj.id = id;
obj.cmd = cmd;
obj.val = val;
getAjax("/api/ctrl", ctrlCb2, "POST", JSON.stringify(obj));
}
function ctrlCb(obj) {
var e = document.getElementsByName("pwrres")[0];
if(obj.success) {
e.innerHTML = "received command, waiting for inverter acknowledge ...";
tPwrAck = window.setInterval("getAjax('/api/inverter/pwrack/" + obj.id + "', updatePwrAck)", 1000);
}
else
e.innerHTML = "Error: " + obj["error"];
}
function ctrlCb2(obj) {
var e = document.getElementsByName("pwrres")[0];
if(obj.success)
e.innerHTML = "command received";
else
e.innerHTML = "Error: " + obj["error"];
}
function updatePwrAck(obj) {
if(!obj.ack)
return;
var e = document.getElementsByName("pwrres")[0];
clearInterval(tPwrAck);
if(null == e)
return;
e.innerHTML = "inverter acknowledged active power control command";
} }
function parse(obj) { function parse(obj) {

218
src/web/web.h

@ -26,6 +26,7 @@
#include "html/h/colorBright_css.h" #include "html/h/colorBright_css.h"
#include "html/h/colorDark_css.h" #include "html/h/colorDark_css.h"
#include "html/h/favicon_ico.h" #include "html/h/favicon_ico.h"
#include "html/h/grid_info_json.h"
#include "html/h/index_html.h" #include "html/h/index_html.h"
#include "html/h/login_html.h" #include "html/h/login_html.h"
#include "html/h/save_html.h" #include "html/h/save_html.h"
@ -39,7 +40,8 @@
#define WEB_SERIAL_BUF_SIZE 2048 #define WEB_SERIAL_BUF_SIZE 2048
const char *const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLedHighActive", "pinCsb", "pinFcsb", "pinGpio3"};
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"};
template <class HMSYSTEM> template <class HMSYSTEM>
class Web { class Web {
@ -66,27 +68,28 @@ class Web {
mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1)); mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1));
mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1)); mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1));
mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1)); mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1));
mWeb.on("/grid_info.json", HTTP_GET, std::bind(&Web::onGridInfoJson, this, std::placeholders::_1));
mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1)); mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1));
mWeb.onNotFound ( std::bind(&Web::showNotFound, this, std::placeholders::_1)); mWeb.onNotFound ( std::bind(&Web::showNotFound, this, std::placeholders::_1));
mWeb.on("/reboot", HTTP_ANY, std::bind(&Web::onReboot, this, std::placeholders::_1)); mWeb.on("/reboot", HTTP_ANY, std::bind(&Web::onReboot, this, std::placeholders::_1));
mWeb.on("/system", HTTP_ANY, std::bind(&Web::onSystem, this, std::placeholders::_1)); mWeb.on("/system", HTTP_ANY, std::bind(&Web::onSystem, this, std::placeholders::_1));
mWeb.on("/erase", HTTP_ANY, std::bind(&Web::showErase, this, std::placeholders::_1)); mWeb.on("/erase", HTTP_ANY, std::bind(&Web::showHtml, this, std::placeholders::_1));
mWeb.on("/factory", HTTP_ANY, std::bind(&Web::showFactoryRst, this, std::placeholders::_1)); mWeb.on("/erasetrue", HTTP_ANY, std::bind(&Web::showHtml, this, std::placeholders::_1));
mWeb.on("/factory", HTTP_ANY, std::bind(&Web::showHtml, this, std::placeholders::_1));
mWeb.on("/factorytrue", HTTP_ANY, std::bind(&Web::showHtml, this, std::placeholders::_1));
mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1));
mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1)); mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1));
mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1));
mWeb.on("/history", HTTP_ANY, std::bind(&Web::onHistory, this, std::placeholders::_1)); mWeb.on("/history", HTTP_ANY, std::bind(&Web::onHistory, this, std::placeholders::_1));
// mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1)); mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1));
#ifdef ENABLE_PROMETHEUS_EP #ifdef ENABLE_PROMETHEUS_EP
mWeb.on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1)); mWeb.on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1));
#endif #endif
mWeb.on("/update", HTTP_GET, std::bind(&Web::onUpdate, this, std::placeholders::_1));
mWeb.on("/update", HTTP_POST, std::bind(&Web::showUpdate, this, std::placeholders::_1), mWeb.on("/update", HTTP_POST, std::bind(&Web::showUpdate, this, std::placeholders::_1),
std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
mWeb.on("/update", HTTP_GET, std::bind(&Web::onUpdate, this, std::placeholders::_1));
mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1), mWeb.on("/upload", HTTP_POST, std::bind(&Web::onUpload, this, std::placeholders::_1),
std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); std::bind(&Web::onUpload2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1)); mWeb.on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1));
@ -150,10 +153,6 @@ class Web {
} }
void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
#if !defined(ETHERNET)
mApp->setOnUpdate();
#endif /* !defined(ETHERNET) */
if (!index) { if (!index) {
Serial.printf("Update Start: %s\n", filename.c_str()); Serial.printf("Update Start: %s\n", filename.c_str());
#ifndef ESP32 #ifndef ESP32
@ -189,8 +188,8 @@ class Web {
mUploadFp.write(data, len); mUploadFp.write(data, len);
if (final) { if (final) {
mUploadFp.close(); mUploadFp.close();
char pwd[PWD_LEN];
#if !defined(ETHERNET) #if !defined(ETHERNET)
char pwd[PWD_LEN];
strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD
#endif #endif
if (!mApp->readSettings("/tmp.json")) { if (!mApp->readSettings("/tmp.json")) {
@ -201,6 +200,11 @@ class Web {
#if !defined(ETHERNET) #if !defined(ETHERNET)
strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD
#endif #endif
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
if((mConfig->inst.iv[i].serial.u64 != 0) && (mConfig->inst.iv[i].serial.u64 < 138999999999)) { // hexadecimal
mConfig->inst.iv[i].serial.u64 = ah::Serial2u64(String(mConfig->inst.iv[i].serial.u64).c_str());
}
}
mApp->saveSettings(true); mApp->saveSettings(true);
} }
if (!mUploadFail) if (!mUploadFail)
@ -214,10 +218,11 @@ class Web {
msg.replace("\r\n", "<rn>"); msg.replace("\r\n", "<rn>");
if (mSerialAddTime) { if (mSerialAddTime) {
if ((9 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { if ((13 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
if (mApp->getTimestamp() > 0) { if (mApp->getTimestamp() > 0) {
strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9); strncpy(&mSerialBuf[mSerialBufFill], ah::getTimeStrMs(mApp->getTimestampMs() + mApp->getTimezoneOffset() * 1000).c_str(), 12);
mSerialBufFill += 9; mSerialBuf[mSerialBufFill+12] = ' ';
mSerialBufFill += 13;
} }
} else { } else {
mSerialBufFill = 0; mSerialBufFill = 0;
@ -280,14 +285,27 @@ class Web {
} }
void showUpdate(AsyncWebServerRequest *request) { void showUpdate(AsyncWebServerRequest *request) {
#if defined(ETHERNET)
// workaround for AsyncWebServer_ESP32_W5500, because it can't distinguish
// between HTTP_GET and HTTP_POST if both are registered
if(request->method() == HTTP_GET)
onUpdate(request);
#endif
bool reboot = (!Update.hasError()); bool reboot = (!Update.hasError());
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: "); String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"");
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
html += F("5");
#else
html += F("20");
#endif
html += F("; URL=/\"></head><body>Update: ");
if (reboot) if (reboot)
html += "success"; html += "success";
else else
html += "failed"; html += "failed";
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>"); html += F("<br/><br/>rebooting ...</body></html>");
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html); AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html);
response->addHeader("Connection", "close"); response->addHeader("Connection", "close");
@ -389,6 +407,16 @@ class Web {
request->send(response); request->send(response);
} }
void onGridInfoJson(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onGridInfoJson"));
AsyncWebServerResponse *response = request->beginResponse_P(200, F("application/json; charset=utf-8"), grid_info_json, grid_info_json_len);
response->addHeader(F("Content-Encoding"), "gzip");
if(request->hasParam("v"))
response->addHeader(F("Cache-Control"), F("max-age=604800"));
request->send(response);
}
void onFavicon(AsyncWebServerRequest *request) { void onFavicon(AsyncWebServerRequest *request) {
static const char favicon_type[] PROGMEM = "image/x-icon"; static const char favicon_type[] PROGMEM = "image/x-icon";
AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len); AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len);
@ -408,39 +436,12 @@ class Web {
request->send(response); request->send(response);
} }
void showErase(AsyncWebServerRequest *request) { void showHtml(AsyncWebServerRequest *request) {
checkProtection(request); checkProtection(request);
DPRINTLN(DBG_VERBOSE, F("showErase")); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
mApp->eraseSettings(false); response->addHeader(F("Content-Encoding"), "gzip");
onReboot(request); request->send(response);
}
void showFactoryRst(AsyncWebServerRequest *request) {
checkProtection(request);
DPRINTLN(DBG_VERBOSE, F("showFactoryRst"));
String content = "";
int refresh = 3;
if (request->args() > 0) {
if (request->arg("reset").toInt() == 1) {
refresh = 10;
if (mApp->eraseSettings(true))
content = F("factory reset: success\n\nrebooting ... ");
else
content = F("factory reset: failed\n\nrebooting ... ");
} else {
content = F("factory reset: aborted");
refresh = 3;
}
} else {
content = F("<h1>Factory Reset</h1>"
"<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>");
refresh = 120;
}
request->send(200, F("text/html; charset=UTF-8"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>"));
if (refresh == 10)
onReboot(request);
} }
void onSetup(AsyncWebServerRequest *request) { void onSetup(AsyncWebServerRequest *request) {
@ -495,80 +496,42 @@ class Web {
request->arg("ipGateway").toCharArray(buf, 20); request->arg("ipGateway").toCharArray(buf, 20);
ah::ip2Arr(mConfig->sys.ip.gateway, buf); ah::ip2Arr(mConfig->sys.ip.gateway, buf);
// inverter
Inverter<> *iv;
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = mSys->getInverterByPos(i, false);
// enable communication
iv->config->enabled = (request->arg("inv" + String(i) + "Enable") == "on");
// address
request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20);
if (strlen(buf) == 0)
memset(buf, 0, 20);
iv->config->serial.u64 = ah::Serial2u64(buf);
switch(iv->config->serial.b[4]) {
case 0x24:
case 0x22:
case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break;
case 0x44:
case 0x42:
case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break;
case 0x64:
case 0x62:
case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break;
default: break;
}
// name
request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH);
// max channel power / name
for (uint8_t j = 0; j < 6; j++) {
iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toDouble();
iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff;
request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH);
}
iv->initialized = true;
}
if (request->arg("invInterval") != "") if (request->arg("invInterval") != "")
mConfig->nrf.sendInterval = request->arg("invInterval").toInt(); mConfig->inst.sendInterval = request->arg("invInterval").toInt();
if (request->arg("invRetry") != "")
mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt();
mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on"); mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on");
mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on"); mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on");
mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on"); mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on");
mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on"); mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on");
mConfig->inst.readGrid = (request->arg("rdGrid") == "on");
mConfig->inst.rstMaxValsMidNight = (request->arg("invRstMaxMid") == "on");
mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat(); mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat();
mConfig->inst.gapMs = (request->arg("invGap")).toInt();
// pinout // pinout
uint8_t pin; uint8_t pin;
for (uint8_t i = 0; i < 12; i++) { for (uint8_t i = 0; i < 15; i++) {
pin = request->arg(String(pinArgNames[i])).toInt(); pin = request->arg(String(pinArgNames[i])).toInt();
switch(i) { switch(i) {
case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break; case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN); break;
case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); break; case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_NRF_CE_PIN); break;
case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break; case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_NRF_IRQ_PIN); break;
case 3: mConfig->nrf.pinSclk = ((pin != 0xff) ? pin : DEF_SCLK_PIN); break; case 3: mConfig->nrf.pinSclk = ((pin != 0xff) ? pin : DEF_NRF_SCLK_PIN); break;
case 4: mConfig->nrf.pinMosi = ((pin != 0xff) ? pin : DEF_MOSI_PIN); break; case 4: mConfig->nrf.pinMosi = ((pin != 0xff) ? pin : DEF_NRF_MOSI_PIN); break;
case 5: mConfig->nrf.pinMiso = ((pin != 0xff) ? pin : DEF_MISO_PIN); break; case 5: mConfig->nrf.pinMiso = ((pin != 0xff) ? pin : DEF_NRF_MISO_PIN); break;
case 6: mConfig->led.led0 = pin; break; case 6: mConfig->led.led0 = pin; break;
case 7: mConfig->led.led1 = pin; break; case 7: mConfig->led.led1 = pin; break;
case 8: mConfig->led.led_high_active = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense case 8: mConfig->led.high_active = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense
case 9: mConfig->cmt.pinCsb = pin; break; case 9: mConfig->led.luminance = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense
case 10: mConfig->cmt.pinFcsb = pin; break; case 10: mConfig->cmt.pinSclk = pin; break;
case 11: mConfig->cmt.pinIrq = pin; break; case 11: mConfig->cmt.pinSdio = pin; break;
case 12: mConfig->cmt.pinCsb = pin; break;
case 13: mConfig->cmt.pinFcsb = pin; break;
case 14: mConfig->cmt.pinIrq = pin; break;
} }
} }
// nrf24 amplifier power
mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03;
mConfig->nrf.enabled = (request->arg("nrfEnable") == "on"); mConfig->nrf.enabled = (request->arg("nrfEnable") == "on");
// cmt
mConfig->cmt.enabled = (request->arg("cmtEnable") == "on"); mConfig->cmt.enabled = (request->arg("cmtEnable") == "on");
// ntp // ntp
@ -582,12 +545,10 @@ class Web {
if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) { if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) {
mConfig->sun.lat = 0.0; mConfig->sun.lat = 0.0;
mConfig->sun.lon = 0.0; mConfig->sun.lon = 0.0;
mConfig->sun.disNightCom = false;
mConfig->sun.offsetSec = 0; mConfig->sun.offsetSec = 0;
} else { } else {
mConfig->sun.lat = request->arg("sunLat").toFloat(); mConfig->sun.lat = request->arg("sunLat").toFloat();
mConfig->sun.lon = request->arg("sunLon").toFloat(); mConfig->sun.lon = request->arg("sunLon").toFloat();
mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on");
mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60; mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60;
} }
@ -607,18 +568,14 @@ class Web {
mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); mConfig->mqtt.interval = request->arg("mqttInterval").toInt();
// serial console // serial console
if (request->arg("serIntvl") != "") {
mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff;
mConfig->serial.debug = (request->arg("serDbg") == "on"); mConfig->serial.debug = (request->arg("serDbg") == "on");
mConfig->serial.privacyLog = (request->arg("priv") == "on");
mConfig->serial.printWholeTrace = (request->arg("wholeTrace") == "on");
mConfig->serial.showIv = (request->arg("serEn") == "on"); mConfig->serial.showIv = (request->arg("serEn") == "on");
// Needed to log TX buffers to serial console
// mSys->Radio.mSerialDebug = mConfig->serial.debug;
}
// display // display
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on"); mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on");
mConfig->plugin.display.pxShift = (request->arg("disp_pxshift") == "on"); mConfig->plugin.display.screenSaver = request->arg("disp_screensaver").toInt();
mConfig->plugin.display.rot = request->arg("disp_rot").toInt(); mConfig->plugin.display.rot = request->arg("disp_rot").toInt();
mConfig->plugin.display.type = request->arg("disp_typ").toInt(); mConfig->plugin.display.type = request->arg("disp_typ").toInt();
mConfig->plugin.display.contrast = (mConfig->plugin.display.type == 0) ? 60 : request->arg("disp_cont").toInt(); mConfig->plugin.display.contrast = (mConfig->plugin.display.type == 0) ? 60 : request->arg("disp_cont").toInt();
@ -628,6 +585,7 @@ class Web {
mConfig->plugin.display.disp_reset = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_rst").toInt(); mConfig->plugin.display.disp_reset = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_rst").toInt();
mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_dc").toInt(); mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_dc").toInt();
mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt(); mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt();
mConfig->plugin.display.pirPin = request->arg("pir_pin").toInt();
mApp->saveSettings((request->arg("reboot") == "on")); mApp->saveSettings((request->arg("reboot") == "on"));
@ -673,7 +631,7 @@ class Web {
#ifdef ENABLE_PROMETHEUS_EP #ifdef ENABLE_PROMETHEUS_EP
// Note // Note
// Prometheus exposition format is defined here: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md // Prometheus exposition format is defined here: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md
// TODO: Check packetsize for MAX_NUM_INVERTERS. Successfull Tested with 4 Inverters (each with 4 channels) // TODO: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels)
enum { enum {
metricsStateStart, metricsStateStart,
metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4, metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4,
@ -693,7 +651,6 @@ class Web {
{ {
Inverter<> *iv; Inverter<> *iv;
record_t<> *rec; record_t<> *rec;
statistics_t *stat;
String promUnit, promType; String promUnit, promType;
String metrics; String metrics;
char type[60], topic[100], val[25]; char type[60], topic[100], val[25];
@ -724,16 +681,14 @@ class Web {
metrics += String(type) + String(topic); metrics += String(type) + String(topic);
// NRF Statistics // NRF Statistics
stat = mApp->getStatistics(); // @TODO 2023-10-01: the statistic data is now available per inverter
uint32_t nrfSendCnt; /*stat = mApp->getNrfStatistics();
uint32_t nrfRetransmits;
mApp->getNrfRadioCounters(&nrfSendCnt, &nrfRetransmits);
metrics += radioStatistic(F("rx_success"), stat->rxSuccess); metrics += radioStatistic(F("rx_success"), stat->rxSuccess);
metrics += radioStatistic(F("rx_fail"), stat->rxFail); metrics += radioStatistic(F("rx_fail"), stat->rxFail);
metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser); metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser);
metrics += radioStatistic(F("frame_cnt"), stat->frmCnt); metrics += radioStatistic(F("frame_cnt"), stat->frmCnt);
metrics += radioStatistic(F("tx_cnt"), nrfSendCnt); metrics += radioStatistic(F("tx_cnt"), stat->txCnt);
metrics += radioStatistic(F("retrans_cnt"), nrfRetransmits); metrics += radioStatistic(F("retrans_cnt"), stat->retransmits);*/
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
// Next is Inverter information // Next is Inverter information
@ -807,16 +762,31 @@ class Web {
// This is the correct field to report // This is the correct field to report
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec));
// Declare metric only once // Declare metric only once
if (!metricDeclared) { if (channel != 0 && !metricDeclared) {
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str());
metrics += type; metrics += type;
metricDeclared = true; metricDeclared = true;
} }
// report value // report value
if (0 == channel) { if (0 == channel) {
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); char total[7];
total[0] = 0;
if (metricDeclared) {
// A declaration and value for channels has been delivered. So declare and deliver a _total metric
strncpy(total,"_total",sizeof(total));
}
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str());
metrics += type;
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name);
} else { } else {
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); // Use a fallback channel name (ch0, ch1, ...)if non is given by user
char chName[MAX_NAME_LENGTH];
if (iv->config->chName[channel-1][0] != 0) {
strncpy(chName, iv->config->chName[channel-1], sizeof(chName));
} else {
snprintf(chName,sizeof(chName),"ch%1d",channel);
}
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName);
} }
snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec));
metrics += topic; metrics += topic;

188
src/wifi/ahoywifi.cpp

@ -10,6 +10,12 @@
#endif #endif
#include "ahoywifi.h" #include "ahoywifi.h"
#if defined(ESP32)
#include <ESPmDNS.h>
#else
#include <ESP8266mDNS.h>
#endif
// NTP CONFIG // NTP CONFIG
#define NTP_PACKET_SIZE 48 #define NTP_PACKET_SIZE 48
@ -29,6 +35,7 @@ void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) {
mUtcTimestamp = utcTimestamp; mUtcTimestamp = utcTimestamp;
mAppWifiCb = cb; mAppWifiCb = cb;
mGotDisconnect = false;
mStaConn = DISCONNECTED; mStaConn = DISCONNECTED;
mCnt = 0; mCnt = 0;
mScanActive = false; mScanActive = false;
@ -69,12 +76,36 @@ void ahoywifi::setupWifi(bool startAP = false) {
} }
//-----------------------------------------------------------------------------
void ahoywifi::tickWifiLoop() { void ahoywifi::tickWifiLoop() {
static const uint8_t TIMEOUT = 20;
static const uint8_t SCAN_TIMEOUT = 10;
#if !defined(AP_ONLY) #if !defined(AP_ONLY)
if(mStaConn != GOT_IP) {
if (WiFi.softAPgetStationNum() > 0) { // do not reconnect if any AP connection exists mCnt++;
if(mStaConn != IN_AP_MODE) {
switch (mStaConn) {
case IN_STA_MODE:
// Nothing to do
if (mGotDisconnect) {
mStaConn = RESET;
}
#if !defined(ESP32)
MDNS.update();
#endif
return;
case IN_AP_MODE:
if (WiFi.softAPgetStationNum() == 0) {
mCnt = 0;
mDns.stop();
WiFi.mode(WIFI_AP_STA);
mStaConn = DISCONNECTED;
} else {
mDns.processNextRequest();
return;
}
break;
case DISCONNECTED:
if (WiFi.softAPgetStationNum() > 0) {
mStaConn = IN_AP_MODE; mStaConn = IN_AP_MODE;
// first time switch to AP Mode // first time switch to AP Mode
if (mScanActive) { if (mScanActive) {
@ -86,57 +117,46 @@ void ahoywifi::tickWifiLoop() {
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
mDns.start(53, "*", mApIp); mDns.start(53, "*", mApIp);
mAppWifiCb(true); mAppWifiCb(true);
}
mDns.processNextRequest(); mDns.processNextRequest();
return; return;
} } else if (!mScanActive) {
else if(mStaConn == IN_AP_MODE) {
mCnt = 0;
mDns.stop();
WiFi.mode(WIFI_AP_STA);
mStaConn = DISCONNECTED;
}
mCnt++;
uint8_t timeout = (mStaConn == DISCONNECTED) ? 20 : 30; // seconds
if (mStaConn == CONNECTED) // connected but no ip
timeout = 30;
if(!mScanActive && mBSSIDList.empty() && (mStaConn == DISCONNECTED)) { // start scanning APs with the given SSID
DBGPRINT(F("scanning APs with SSID ")); DBGPRINT(F("scanning APs with SSID "));
DBGPRINTLN(String(mConfig->sys.stationSsid)); DBGPRINTLN(String(mConfig->sys.stationSsid));
mScanCnt = 0; mScanCnt = 0;
mCnt = 0;
mScanActive = true; mScanActive = true;
#if defined(ESP8266) #if defined(ESP8266)
WiFi.scanNetworks(true, true, 0U, ([this] () { WiFi.scanNetworks(true, true, 0U, ([this]() {
if(mConfig->sys.isHidden) if (mConfig->sys.isHidden)
return (uint8_t *)NULL; return (uint8_t*)NULL;
return (uint8_t *)(mConfig->sys.stationSsid); return (uint8_t*)(mConfig->sys.stationSsid);
})()); })());
#else #else
WiFi.scanNetworks(true, true, false, 300U, 0U, ([this] () { WiFi.scanNetworks(true, true, false, 300U, 0U, ([this]() {
if(mConfig->sys.isHidden) if (mConfig->sys.isHidden)
return (char*)NULL; return (char*)NULL;
return (mConfig->sys.stationSsid); return (mConfig->sys.stationSsid);
})()); })());
#endif #endif
return; return;
} else if(getBSSIDs()) {
// Scan ready
mStaConn = SCAN_READY;
} else {
// In case of a timeout, what do we do?
// For now we start scanning again as the original code did.
// Would be better to into PA mode
if (isTimeout(SCAN_TIMEOUT)) {
WiFi.scanDelete();
mScanActive = false;
} }
if(mScanActive) {
getBSSIDs();
if((!mScanActive) && (!mBSSIDList.empty())) // scan completed
if ((mCnt % timeout) < timeout - 2)
mCnt = timeout - 2;
} }
DBGPRINT(F("reconnect in "));
DBGPRINT(String(timeout - mCnt));
DBGPRINTLN(F(" seconds"));
if (mStaConn == DISCONNECTED) {
if ((mCnt % timeout) == 0) { // try to reconnect after x sec without connection
mStaConn = CONNECTING;
WiFi.disconnect();
if(mBSSIDList.size() > 0) { // get first BSSID in list break;
case SCAN_READY:
mStaConn = CONNECTING;
mCnt = 0;
DBGPRINT(F("try to connect to AP with BSSID:")); DBGPRINT(F("try to connect to AP with BSSID:"));
uint8_t bssid[6]; uint8_t bssid[6];
for (int j = 0; j < 6; j++) { for (int j = 0; j < 6; j++) {
@ -145,18 +165,56 @@ void ahoywifi::tickWifiLoop() {
DBGPRINT(" " + String(bssid[j], HEX)); DBGPRINT(" " + String(bssid[j], HEX));
} }
DBGPRINTLN(""); DBGPRINTLN("");
mGotDisconnect = false;
WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]); WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]);
}
else
mStaConn = DISCONNECTED;
mCnt = 0; break;
case CONNECTING:
if (isTimeout(TIMEOUT)) {
WiFi.disconnect();
mStaConn = mBSSIDList.empty() ? DISCONNECTED : SCAN_READY;
} }
break;
case CONNECTED:
// Connection but no IP yet
if (isTimeout(TIMEOUT) || mGotDisconnect) {
mStaConn = RESET;
} }
break;
case GOT_IP:
welcome(WiFi.localIP().toString(), F(" (Station)"));
WiFi.softAPdisconnect();
WiFi.mode(WIFI_STA);
DBGPRINTLN(F("[WiFi] AP disabled"));
delay(100);
mAppWifiCb(true);
mGotDisconnect = false;
mStaConn = IN_STA_MODE;
if (!MDNS.begin(mConfig->sys.deviceName)) {
DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!"));
} else {
DBGPRINT(F("[WiFi] mDNS established: "));
DBGPRINT(mConfig->sys.deviceName);
DBGPRINTLN(F(".local"));
}
break;
case RESET:
mGotDisconnect = false;
mStaConn = DISCONNECTED;
mCnt = 5; // try to reconnect in 5 sec
setupWifi(); // reconnect with AP / Station setup
mAppWifiCb(false);
DPRINTLN(DBG_INFO, "[WiFi] Connection Lost");
break;
default:
DBGPRINTLN(F("Unhandled status"));
break;
} }
#endif
}
#endif
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void ahoywifi::setupAp(void) { void ahoywifi::setupAp(void) {
@ -206,7 +264,6 @@ void ahoywifi::setupStation(void) {
WiFi.hostname(mConfig->sys.deviceName); WiFi.hostname(mConfig->sys.deviceName);
WiFi.mode(WIFI_AP_STA); WiFi.mode(WIFI_AP_STA);
DBGPRINT(F("connect to network '")); DBGPRINT(F("connect to network '"));
DBGPRINT(mConfig->sys.stationSsid); DBGPRINT(mConfig->sys.stationSsid);
DBGPRINTLN(F("' ...")); DBGPRINTLN(F("' ..."));
@ -215,7 +272,7 @@ void ahoywifi::setupStation(void) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
bool ahoywifi::getNtpTime(void) { bool ahoywifi::getNtpTime(void) {
if(GOT_IP != mStaConn) if(IN_STA_MODE != mStaConn)
return false; return false;
IPAddress timeServer; IPAddress timeServer;
@ -316,11 +373,12 @@ bool ahoywifi::getAvailNetworks(JsonObject obj) {
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void ahoywifi::getBSSIDs() { bool ahoywifi::getBSSIDs() {
bool result = false;
int n = WiFi.scanComplete(); int n = WiFi.scanComplete();
if (n < 0) { if (n < 0) {
if (++mScanCnt < 20) if (++mScanCnt < 20)
return; return false;
} }
if(n > 0) { if(n > 0) {
mBSSIDList.clear(); mBSSIDList.clear();
@ -335,9 +393,11 @@ void ahoywifi::getBSSIDs() {
} }
DBGPRINTLN(""); DBGPRINTLN("");
} }
result = true;
} }
mScanActive = false; mScanActive = false;
WiFi.scanDelete(); WiFi.scanDelete();
return result;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -348,40 +408,30 @@ void ahoywifi::connectionEvent(WiFiStatus_t status) {
case CONNECTED: case CONNECTED:
if(mStaConn != CONNECTED) { if(mStaConn != CONNECTED) {
mStaConn = CONNECTED; mStaConn = CONNECTED;
mGotDisconnect = false;
DBGPRINTLN(F("\n[WiFi] Connected")); DBGPRINTLN(F("\n[WiFi] Connected"));
// VArt check!!!
DBGPRINTLN(F("[WiFi] RRSI: ")); DBGPRINTLN(F("[WiFi] RRSI: "));
DBGPRINTLN(String(WiFi.RSSI())); DBGPRINTLN(String(WiFi.RSSI()));
}
mCnt = 0;
WiFi.setAutoReconnect(true); WiFi.setAutoReconnect(true);
WiFi.persistent(true); WiFi.persistent(true);
// <<-- check
}
mCnt = 0;
break; break;
case GOT_IP: case GOT_IP:
mStaConn = GOT_IP; mStaConn = GOT_IP;
if (mScanActive) { // maybe another scan has started // VArt check!!!
WiFi.scanDelete();
mScanActive = false;
}
welcome(WiFi.localIP().toString(), F(" (Station)"));
WiFi.softAPdisconnect();
WiFi.mode(WIFI_STA);
DBGPRINTLN(F("[WiFi] AP disabled"));
delay(100);
mCnt = 0; mCnt = 0;
WiFi.setAutoReconnect(true); WiFi.setAutoReconnect(true);
WiFi.persistent(true); WiFi.persistent(true);
mAppWifiCb(true); // <<-- check
break; break;
case DISCONNECTED: case DISCONNECTED:
if(mStaConn != CONNECTING) { mGotDisconnect = true;
mStaConn = DISCONNECTED;
mCnt = 6; // try to reconnect in timeout - 6 sec
setupWifi(); // reconnect with AP / Station setup
mAppWifiCb(false);
DPRINTLN(DBG_INFO, "[WiFi] Connection Lost"); DPRINTLN(DBG_INFO, "[WiFi] Connection Lost");
}
break; break;
default: default:

12
src/wifi/ahoywifi.h

@ -32,10 +32,13 @@ class ahoywifi {
private: private:
typedef enum WiFiStatus { typedef enum WiFiStatus {
DISCONNECTED = 0, DISCONNECTED = 0,
SCAN_READY,
CONNECTING, CONNECTING,
CONNECTED, CONNECTED,
IN_AP_MODE, IN_AP_MODE,
GOT_IP GOT_IP,
IN_STA_MODE,
RESET
} WiFiStatus_t; } WiFiStatus_t;
void setupWifi(bool startAP); void setupWifi(bool startAP);
@ -43,9 +46,11 @@ class ahoywifi {
void setupStation(void); void setupStation(void);
void sendNTPpacket(IPAddress& address); void sendNTPpacket(IPAddress& address);
void sortRSSI(int *sort, int n); void sortRSSI(int *sort, int n);
void getBSSIDs(void); bool getBSSIDs(void);
void connectionEvent(WiFiStatus_t status); void connectionEvent(WiFiStatus_t status);
#if defined(ESP8266) bool isTimeout(uint8_t timeout) { return (mCnt % timeout) == 0; }
#if defined(ESP8266)
void onConnect(const WiFiEventStationModeConnected& event); void onConnect(const WiFiEventStationModeConnected& event);
void onGotIP(const WiFiEventStationModeGotIP& event); void onGotIP(const WiFiEventStationModeGotIP& event);
void onDisconnect(const WiFiEventStationModeDisconnected& event); void onDisconnect(const WiFiEventStationModeDisconnected& event);
@ -71,6 +76,7 @@ class ahoywifi {
uint8_t mScanCnt; uint8_t mScanCnt;
bool mScanActive; bool mScanActive;
bool mGotDisconnect;
std::list<uint8_t> mBSSIDList; std::list<uint8_t> mBSSIDList;
}; };

6
tools/fonts/fontconv.bat

@ -0,0 +1,6 @@
.\bdfconv\bdfconv_2_22.exe -v -f 1 -m "32-137" u8g2_font_5x8_symbols_ahoy.bdf -o u8g2_font_5x8_symbols_ahoy.c_ -n u8g2_font_5x8_symbols_ahoy
.\bdfconv\bdfconv_2_22.exe -v -f 1 -m "65-75" u8g2_font_ncenB10_symbols10_ahoy.bdf -o u8g2_font_ncenB10_symbols10_ahoy.c_ -n u8g2_font_ncenB10_symbols10_ahoy
.\bdfconv\bdfconv_2_22.exe -v -f 1 -m "65-75" u8g2_font_ncenB08_symbols8_ahoy.bdf -o u8g2_font_ncenB08_symbols8_ahoy.c_ -n u8g2_font_ncenB08_symbols8_ahoy
pause

11
tools/fonts/u8g2 font-sources.txt

@ -0,0 +1,11 @@
Useful sources to edit u8g2 fonts:
bdf font files for u8g2 font library:
https://github.com/olikraus/u8g2/tree/master/tools/font/bdf
Tool to edit bdf files:
https://github.com/olikraus/u8g2/tree/master/tools/font/fony
Tool to convert bdf font files to u8g2 source code:
https://github.com/olikraus/u8g2/tree/master/tools/font/bdfconv

1327
tools/fonts/u8g2_font_5x8_symbols_ahoy.bdf

File diff suppressed because it is too large

40
tools/fonts/u8g2_font_5x8_symbols_ahoy.c_

@ -0,0 +1,40 @@
/*
Fontname: u8g2_font_5x8_symbols_ahoy
Copyright: Public domain font. Share and enjoy.
Glyphs: 106/106
BBX Build Mode: 0
*/
const uint8_t u8g2_font_5x8_symbols_ahoy[1049] U8G2_FONT_SECTION("u8g2_font_5x8_symbols_ahoy") =
"j\0\3\2\4\4\3\4\5\10\10\0\377\6\377\6\0\1\61\2b\4\0 \5\0\304\11!\7a\306"
"\212!\11\42\7\63\335\212\304\22#\16u\304\232R\222\14JePJI\2$\14u\304\252l\251m"
"I\262E\0%\10S\315\212(\351\24&\13t\304\232(i\252\64%\1'\6\61\336\212\1(\7b"
"\305\32\245))\11b\305\212(\251(\0*\13T\304\212(Q\206D\211\2+\12U\304\252\60\32\244"
"\60\2,\7\63\275\32\245\4-\6\24\324\212!.\6\42\305\212!/\10d\304\272R[\6\60\14d"
"\304\32%R\206DJ\24\0\61\10c\305\232Dj\31\62\13d\304\32%\312\22%\33\2\63\13d\304"
"\212!\212D)Q\0\64\13d\304\252H\251\14Q\226\0\65\12d\304\212A\33\245D\1\66\13d\304"
"\32%[\42)Q\0\67\13d\304\212!\213\262(\213\0\70\14d\304\32%J\224HJ\24\0\71\13"
"d\304\32%\222\222-Q\0:\10R\305\212!\32\2;\10c\275\32\243R\2<\10c\305\252\244\224"
"\25=\10\64\314\212!\34\2>\11c\305\212\254\224\224\0?\11c\305\232\246$M\0@\15\205\274*"
")\222\226DI\244\252\2A\12d\304\32%\222\206I\12B\14d\304\212%\32\222H\32\22\0C\12"
"d\304\32%\322J\211\2D\12d\304\212%r\32\22\0E\12d\304\212A[\262l\10F\12d\304"
"\212A[\262\32\0G\13d\304\32%\322\222)Q\0H\12d\304\212H\32&S\0I\10c\305\212"
"%j\31J\12d\304\232)\253\224\42\0K\13d\304\212HI\244\244S\0L\10d\304\212\254\333\20"
"M\12d\304\212h\70D\246\0N\12d\304\212h\31\226I\12O\12d\304\32%rJ\24\0P\13"
"d\304\212%\222\206$\313\0Q\12t\274\32%\222\26\307\0R\13d\304\212%\222\206$\222\2S\14"
"d\304\32%J\302$J\24\0T\11e\304\212A\12;\1U\11d\304\212\310S\242\0V\12d\304"
"\212\310)\221\24\0W\12d\304\212\310\64\34\242\0X\13d\304\212HJ$%\222\2Y\12e\304\212"
"LKja\11Z\12d\304\212!\213\332\206\0[\10c\305\212!j\32\134\10d\304\212,l\13]"
"\10c\305\212\251i\10^\6#\345\232\6_\6\24\274\212!`\6\42\345\212(a\11D\304\232!\222"
"\222\1b\13d\304\212,[\42iH\0c\7C\305\232)\23d\12d\304\272\312\20I\311\0e\11"
"D\304\32%\31\262\1f\12d\304\252Ji\312\42\0g\12T\274\32%J\266D\1h\12d\304\212"
",[\42S\0i\10c\305\232P\252\14j\12s\275\252\64\212\224\12\0k\12d\304\212\254\64$\221"
"\24l\10c\305\12\251\313\0m\12E\304\12\245EI\224\2n\10D\304\212%\62\5o\11D\304\32"
"%\222\22\5p\12T\274\212%\32\222,\3q\11T\274\232!J\266\2r\11D\304\212$\261e\0"
"s\10C\305\232![\0t\13d\304\232,\232\262$J\0u\10D\304\212\310\224\14v\10C\305\212"
"\304R\1w\12E\304\212LI\224.\0x\11D\304\212(\221\224(y\13T\274\212HJ\206(Q"
"\0z\11D\304\212!*\15\1{\12t\304*%L\304(\24|\6a\306\212\3}\13t\304\12\61"
"\12\225\60\221\0~\10$\344\232DI\0\5\0\304\12\200\13u\274\212K\242T\266\260\4\201\14f"
"D\233!\11#-\312!\11\202\15hD<\65\12\243,\214\302$\16\203\15w<\214C\22F\71\220"
"\26\207A\204\13u\274\212\244\232t\313\241\10\205\17\206<\213\60\31\22\311\66D\245!\11\3\206\20\210"
"<\254\342\20]\302(L\246C\30E\0\207\15wD\334X\25\267\341\20\15\21\0\210\16w<\214\203"
"RQ\25I\212\324a\20\211\15f\304\213)\213\244,\222\222\245\0\0\0\0";

BIN
tools/fonts/u8g2_font_5x8_symbols_ahoy.fon

Binary file not shown.

166
tools/fonts/u8g2_font_ncenB08_symbols8_ahoy.bdf

@ -0,0 +1,166 @@
STARTFONT 2.1
COMMENT Exported by Fony v1.4.7
FONT u8g2_font_ncenB08_symbols8_ahoy
SIZE 12 100 100
FONTBOUNDINGBOX 9 11 0 -2
STARTPROPERTIES 6
COPYRIGHT ""
RESOLUTION_X 100
RESOLUTION_Y 100
FONT_ASCENT 10
FONT_DESCENT 2
DEFAULT_CHAR 0
ENDPROPERTIES
CHARS 11
STARTCHAR 065
ENCODING 65
SWIDTH 576 0
DWIDTH 8 0
BBX 7 8 0 0
BITMAP
FE
92
D6
38
10
10
10
10
ENDCHAR
STARTCHAR 066
ENCODING 66
SWIDTH 648 0
DWIDTH 9 0
BBX 8 9 1 0
BITMAP
3C
42
99
24
42
18
24
00
18
ENDCHAR
STARTCHAR 067
ENCODING 67
SWIDTH 576 0
DWIDTH 8 0
BBX 8 8 0 0
BITMAP
18
24
24
42
42
42
42
81
ENDCHAR
STARTCHAR 068
ENCODING 68
SWIDTH 648 0
DWIDTH 9 0
BBX 8 8 0 0
BITMAP
FF
41
20
10
10
20
41
FF
ENDCHAR
STARTCHAR 069
ENCODING 69
SWIDTH 576 0
DWIDTH 8 0
BBX 7 8 0 0
BITMAP
FE
D6
FE
38
10
38
54
92
ENDCHAR
STARTCHAR 070
ENCODING 70
SWIDTH 648 0
DWIDTH 9 0
BBX 8 9 1 0
BITMAP
BD
42
BD
3C
5A
18
24
42
99
ENDCHAR
STARTCHAR 071
ENCODING 71
SWIDTH 648 0
DWIDTH 9 0
BBX 8 8 0 0
BITMAP
24
3C
E7
42
42
E7
3C
24
ENDCHAR
STARTCHAR 072
ENCODING 72
SWIDTH 576 0
DWIDTH 8 0
BBX 7 7 0 1
BITMAP
04
06
06
0E
1E
FC
78
ENDCHAR
STARTCHAR 073
ENCODING 73
SWIDTH 576 0
DWIDTH 8 0
BBX 7 9 0 0
BITMAP
44
FE
82
92
B2
92
92
82
FE
ENDCHAR
STARTCHAR 074
ENCODING 74
SWIDTH 504 0
DWIDTH 7 0
BBX 0 0 0 0
BITMAP
ENDCHAR
STARTCHAR 075
ENCODING 75
SWIDTH 648 0
DWIDTH 9 0
BBX 0 0 0 0
BITMAP
ENDCHAR
ENDFONT

13
tools/fonts/u8g2_font_ncenB08_symbols8_ahoy.c_

@ -0,0 +1,13 @@
/*
Fontname: u8g2_font_ncenB08_symbols8_ahoy
Copyright:
Glyphs: 11/11
BBX Build Mode: 0
*/
const uint8_t u8g2_font_ncenB08_symbols8_ahoy[173] U8G2_FONT_SECTION("u8g2_font_ncenB08_symbols8_ahoy") =
"\13\0\3\2\4\4\2\2\5\11\11\0\0\10\0\10\0\0\0\0\0\0\224A\14\207\212q\220\242%\221"
"\326\270\15B\20\230\233\65da\22Ima\250F\71\254\1C\20\210\212\247Fa\224\205Q\30\205Q"
"\230\304\1D\16\210\232qP\322(Gr \256\16\7E\15\207\212\361\222\14\247\65\335\222\246\2F\25"
"\230\233\221\14I\61I\206$\32\262D\11\325(\13\223H\12G\17\210\232U\34\242K\30\205\311t\10"
"\243\10H\14w\216\33\253\342\66\34\242!\2I\21\227\212\223%\303\240J\221\42I\221\24\251\303 J"
"\5\0z\1K\5\0\232\1\0\0\0";

BIN
tools/fonts/u8g2_font_ncenB08_symbols8_ahoy.fon

Binary file not shown.

196
tools/fonts/u8g2_font_ncenB10_symbols10_ahoy.bdf

@ -0,0 +1,196 @@
STARTFONT 2.1
COMMENT Exported by Fony v1.4.7
FONT u8g2_font_symbols10_ahoy
SIZE 16 100 100
FONTBOUNDINGBOX 12 15 0 -3
STARTPROPERTIES 6
COPYRIGHT ""
RESOLUTION_X 100
RESOLUTION_Y 100
FONT_ASCENT 13
FONT_DESCENT 3
DEFAULT_CHAR 0
ENDPROPERTIES
CHARS 11
STARTCHAR 065
ENCODING 65
SWIDTH 576 0
DWIDTH 8 0
BBX 7 11 0 0
BITMAP
FE
92
92
54
38
10
10
10
10
10
10
ENDCHAR
STARTCHAR 066
ENCODING 66
SWIDTH 648 0
DWIDTH 9 0
BBX 8 9 0 1
BITMAP
3C
42
99
24
42
18
24
00
18
ENDCHAR
STARTCHAR 067
ENCODING 67
SWIDTH 792 0
DWIDTH 11 0
BBX 10 11 0 0
BITMAP
0C00
1200
2100
2100
2100
2180
4080
4080
4080
4080
8040
ENDCHAR
STARTCHAR 068
ENCODING 68
SWIDTH 720 0
DWIDTH 10 0
BBX 9 11 0 0
BITMAP
FF80
6080
3000
1800
0C00
0C00
1800
3000
6000
C080
FF80
ENDCHAR
STARTCHAR 069
ENCODING 69
SWIDTH 576 0
DWIDTH 8 0
BBX 7 11 0 0
BITMAP
AA
00
92
00
38
00
10
00
10
00
10
ENDCHAR
STARTCHAR 070
ENCODING 70
SWIDTH 648 0
DWIDTH 9 0
BBX 8 10 1 0
BITMAP
BD
42
BD
24
5A
18
24
24
5A
81
ENDCHAR
STARTCHAR 071
ENCODING 71
SWIDTH 864 0
DWIDTH 12 0
BBX 11 11 0 0
BITMAP
1100
1100
0E00
D160
2080
2080
2080
D160
0E00
1100
1100
ENDCHAR
STARTCHAR 072
ENCODING 72
SWIDTH 792 0
DWIDTH 11 0
BBX 10 11 0 0
BITMAP
0080
0080
00C0
00C0
01C0
01C0
03C0
0780
1F80
FF00
3C00
ENDCHAR
STARTCHAR 073
ENCODING 73
SWIDTH 720 0
DWIDTH 10 0
BBX 9 11 0 0
BITMAP
DD80
FF80
8080
8880
9880
8880
8880
8880
8880
8080
FF80
ENDCHAR
STARTCHAR 074
ENCODING 74
SWIDTH 648 0
DWIDTH 9 0
BBX 8 8 0 0
BITMAP
F9
04
F2
09
E5
15
D5
D5
ENDCHAR
STARTCHAR 075
ENCODING 75
SWIDTH 576 0
DWIDTH 8 0
BBX 0 0 0 0
BITMAP
ENDCHAR
ENDFONT

14
tools/fonts/u8g2_font_ncenB10_symbols10_ahoy.c_

@ -0,0 +1,14 @@
/*
Fontname: u8g2_font_symbols10_ahoy
Copyright:
Glyphs: 11/11
BBX Build Mode: 0
*/
const uint8_t u8g2_font_ncenB10_symbols10_ahoy[217] U8G2_FONT_SECTION("u8g2_font_ncenB10_symbols10_ahoy") =
"\13\0\3\2\4\4\2\2\5\13\13\0\0\13\0\13\0\0\0\0\0\0\300A\15\267\212q\220\42\251\322"
"\266\306\275\1B\20\230\236\65da\22Ima\250F\71\254\1C\23\272\272\251\3Q\32\366Q\212\243"
"\70\212\243\70\311\221\0D\20\271\252\361\242F:\242#: {\36\16\1E\17\267\212\221\264\3Q\35"
"\332\321\34\316\341\14F\25\250\233\221\14I\61I\206$\252%J\250Fa\224%J\71G\30\273\312W"
"\316r`T\262DJ\303\64L#%K\304\35\310\342,\3H\27\272\272\217\344P\16\351\210\16\354\300"
"<\244C\70,\303 \16!\0I\24\271\252\241\34\336\1-\223\64-\323\62-\323\62\35x\10J\22"
"\210\232\61Hi\64Di\64DI\226$KeiK\5\0\212\1\0\0\0";

BIN
tools/fonts/u8g2_font_ncenB10_symbols10_ahoy.fon

Binary file not shown.

16
tools/fonts/used_fonts.txt

@ -0,0 +1,16 @@
Display_Mono_64x48:
u8g2_font_fur11_tr
u8g2_font_6x10_tf
u8g2_font_4x6_tr
Display_Mono_128x32:
u8g2_font_9x15_tr
u8g2_font_tom_thumb_4x6_tr
Display_Mono_84x48:
u8g2_font_5x8_symbols_ahoy
u8g2_font_logisoso16_tr
Display_Mono_128x64:
u8g2_font_ncenB08_symbols8_ahoy
u8g2_font_ncenB10_symbols10_ahoy

13
tools/rpi/hoymiles/__init__.py

@ -345,6 +345,9 @@ class HoymilesNRF:
if not radio.begin(): if not radio.begin():
raise RuntimeError('Can\'t open radio') raise RuntimeError('Can\'t open radio')
if not radio.isChipConnected():
logging.warning("could not connect to NRF24 radio")
self.txpower = radio_config.get('txpower', 'max') self.txpower = radio_config.get('txpower', 'max')
self.radio = radio self.radio = radio
@ -411,7 +414,7 @@ class HoymilesNRF:
self.radio.startListening() self.radio.startListening()
fragments = [] fragments = []
received_sth=False
# Receive: Loop # Receive: Loop
t_end = time.monotonic_ns()+timeout t_end = time.monotonic_ns()+timeout
while time.monotonic_ns() < t_end: while time.monotonic_ns() < t_end:
@ -431,7 +434,7 @@ class HoymilesNRF:
ch_rx=self.rx_channel, ch_tx=self.tx_channel, ch_rx=self.rx_channel, ch_tx=self.tx_channel,
time_rx=datetime.now() time_rx=datetime.now()
) )
received_sth=True
yield fragment yield fragment
else: else:
@ -447,7 +450,11 @@ class HoymilesNRF:
self.radio.setChannel(self.rx_channel) self.radio.setChannel(self.rx_channel)
self.radio.startListening() self.radio.startListening()
time.sleep(0.004) time.sleep(0.005)
if not received_sth:
raise TimeoutError
def next_rx_channel(self): def next_rx_channel(self):
""" """

9
tools/rpi/hoymiles/__main__.py

@ -103,10 +103,11 @@ class SunsetHandler:
def sun_status2mqtt(self, dtu_ser, dtu_name): def sun_status2mqtt(self, dtu_ser, dtu_name):
if not mqtt_client or not self.suntimes: if not mqtt_client or not self.suntimes:
return return
if self.suntimes:
local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M") local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M")
local_sunset = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M") local_sunset = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M")
local_zone = self.suntimes.setlocal(datetime.now()).tzinfo._key local_zone = self.suntimes.setlocal(datetime.now()).tzinfo.key
if self.suntimes:
mqtt_client.info2mqtt({'topic' : f'{dtu_name}/{dtu_ser}'}, \ mqtt_client.info2mqtt({'topic' : f'{dtu_name}/{dtu_ser}'}, \
{'dis_night_comm' : 'True', \ {'dis_night_comm' : 'True', \
'local_sunrise' : local_sunrise, \ 'local_sunrise' : local_sunrise, \
@ -235,7 +236,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
if isinstance(result, hoymiles.decoders.StatusResponse): if isinstance(result, hoymiles.decoders.StatusResponse):
data = result.__dict__() data = result.__dict__()
if 'event_count' in data: if data is not None and 'event_count' in data:
if event_message_index[inv_str] < data['event_count']: if event_message_index[inv_str] < data['event_count']:
event_message_index[inv_str] = data['event_count'] event_message_index[inv_str] = data['event_count']
command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str])) command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str]))
@ -409,7 +410,7 @@ if __name__ == '__main__':
str(g_inverter_ser), str(g_inverter_ser),
g_inverter.get('mqtt', {}).get('topic', f'hoymiles/{g_inverter_ser}') + '/command' g_inverter.get('mqtt', {}).get('topic', f'hoymiles/{g_inverter_ser}') + '/command'
) )
mqtt_client.subscribe(topic_item[1]) mqtt_client.client.subscribe(topic_item[1])
mqtt_command_topic_subs.append(topic_item) mqtt_command_topic_subs.append(topic_item)
# start main-loop # start main-loop

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save