diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 847495eb..6eba4879 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -39,18 +39,14 @@ jobs: - name: Install PlatformIO run: | - python -m pip install --upgrade pip + python -m pip install setuptools --upgrade pip pip install --upgrade platformio - - name: Convert HTML files - working-directory: src/web/html - run: python convert.py - - 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 - 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 id: rename-binary-files @@ -77,3 +73,15 @@ jobs: src/User_Manual.md 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 }} diff --git a/.github/workflows/compile_release.yml b/.github/workflows/compile_release.yml index 89f6a4bd..a7e3511d 100644 --- a/.github/workflows/compile_release.yml +++ b/.github/workflows/compile_release.yml @@ -43,18 +43,14 @@ jobs: - name: Install PlatformIO run: | - python -m pip install --upgrade pip + python -m pip install setuptools --upgrade pip pip install --upgrade platformio - - name: Convert HTML files - working-directory: src/web/html - run: python convert.py - - 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 - 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 id: rename-binary-files @@ -81,7 +77,7 @@ jobs: VERSION: ${{ steps.rename-binary-files.outputs.name }} - name: Create Artifact - run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip src/firmware/* User_Manual.md + run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip src/firmware/* User_Manual.md - name: Upload Release id: upload-release @@ -93,3 +89,16 @@ jobs: asset_path: ./${{ steps.rename-binary-files.outputs.name }}.zip asset_name: ${{ steps.rename-binary-files.outputs.name }}.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 }} diff --git a/.gitignore b/.gitignore index 2ee4b679..21ae2a57 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ src/web/html/tmp/* *.suo *.ipch src/output.map + +/.venv diff --git a/README.md b/README.md index 07556d2f..02ed130d 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,12 @@ This work is licensed under a THIS IS A FORK OF: - - # 🖐 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. - This fork adds the following features: + ### Added chart to Display128x64 ### Added a History menu @@ -39,4 +35,4 @@ For me the original reconnect did not work. I always lost connection after some Now it is more stable. ### Reduced frequency channels -Removed frequencies 2461, 2475MHz. Now the transfers are much more stable \ No newline at end of file +Removed frequencies 2461, 2475MHz. Now the transfers are much more stable diff --git a/User_Manual.md b/User_Manual.md index 5e1111b0..b4ef8666 100644 --- a/User_Manual.md +++ b/User_Manual.md @@ -124,6 +124,8 @@ The AhoyDTU subscribes on following topics: 👆 `` 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 ```mqtt @@ -265,31 +267,32 @@ 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. 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 | | | -| ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------- | --------- | -| DanielR92 | HM-1500 | | 1.0.16 | 2021 | 10-12 | 100 | | | -| isdor | HM-300 | | 1.0.14 | 2021 | 12-09 | 102 | | | -| aschiffler | HM-1500 | | 1.0.12 | 2020 | 06-24 | 100 | | | -| klahus1 | HM-300 | | 1.0.10 | 2020 | 07-07 | 102 | | | -| roku133 | HM-400 | | 1.0.10 | 2020 | 07-07 | 102 | | | -| eeprom23 | HM-1200 | 0.1.0 | 1.0.18 | 2021 | 12-24 | 269619201 | 18:21:00 | HWRev 256 | -| eeprom23 | HM-1200 2t | 0.1.0 | 1.0.16 | 2021 | 10-12 | 269619207 | 17:06:00 | 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 | | | -| lumapu | HM-1200 | 0.1.0 | 1.0.12 | 2020 | 06-24 | | | | -| chehrlic | HM-600 | | 1.0.10 | 2021 | 11-01 | 104 | | | -| chehrlic | TSOL-M800de | | 1.0.10 | 2021 | 11-01 | 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 | | | -| dragricola | HM-1200 | | 1.0.16 | 2021 | 10-12 | 100 | | | -| dragricola | MI-300 | | 230 | 2017 | 08-08 | 1 | | | -| | | | | | | | | | +| Name | Inverter Typ | Bootloader V. | FWVersion | FWBuild [YYYY] | FWBuild [MM-DD] | HWPartId | FWBuild [HH:MM:SS] | | +| ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------------------- | --------- | +| lumapu | HM-1200 | 0.1.0 | 1.0.12 | 2020 | 06-24 | | | | +| 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 | | | +| 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 | | | +| isdor | HM-300 | | 1.0.14 | 2021 | 12-09 | 102 | | | +| roku133 | HM-400 | | 1.0.10 | 2020 | 07-07 | 102 | | HWRev 256 | +| setje | HM-600 | | 1.0.08 | 2020 | 07-10 | 104 | | HWRev 256 | +| madmartin | HM-600 | 0.1.4 | 1.0.10 | 2021 | 11-01 | 104 | | | +| chehrlic | HM-600 | | 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 | | | +| rejoe2 | MI-1500 | | 1.0.12 | 2020 | 06-24 | 100 | | | +| 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 After reboot or startup the ahoy firmware it will enque three commands in the following sequence: diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md index 755fd1e4..711299dd 100644 --- a/doc/prometheus_ep_description.md +++ b/doc/prometheus_ep_description.md @@ -15,37 +15,40 @@ Prometheus metrics provided at `/metrics`. | channel | Channel (Module) name from setup. Label only available if max power level of module is set to non-zero. Be sure to have a cannel name set in configuration. | ## Exported Metrics -| Metric name | Type | Description | Labels | -|----------------------------------------|---------|--------------------------------------------------------|--------------| -| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename | -| `ahoy_solar_uptime` | Counter | Seconds since boot of the AhoyDTU device | devicename | -| `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename | -| `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial | -| `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter | -| `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter | -| `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter | -| `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter | -| `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter | -| `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter | -| `ahoy_solar_Q_AC_var` | Gauge | AC reactive power[var] | inverter | -| `ahoy_solar_F_AC_hertz` | Gauge | AC frequency [Hz] | inverter | -| `ahoy_solar_PF_AC` | Gauge | AC Power factor | inverter | -| `ahoy_solar_Temp_celsius` | Gauge | Temperature of inverter | inverter | -| `ahoy_solar_ALARM_MES_ID` | Gauge | Alarm message index of inverter | inverter | -| `ahoy_solar_LastAlarmCode` | Gauge | Last alarm code from inverter | inverter | -| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter | -| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter | -| `ahoy_solar_P_DC_watt` | Gauge | DC power of inverter [W] | inverter | -| `ahoy_solar_Efficiency_ratio` | Gauge | ration AC Power over DC Power [%] | inverter | -| `ahoy_solar_U_DC_volt` | Gauge | DC voltage of channel [V] | inverter, channel | -| `ahoy_solar_I_DC_ampere` | Gauge | DC current of channel [A] | inverter, channel | -| `ahoy_solar_P_DC_watt` | Gauge | DC power of channel [P] | inverter, channel | -| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter, channel | -| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter, channel | -| `ahoy_solar_Irradiation_ratio` | Gauge | ratio DC Power over set maximum power per channel [%] | inverter, channel | -| `ahoy_solar_radio_rx_success` | Gauge | NRF24 statistic | | -| `ahoy_solar_radio_rx_fail` | Gauge | NRF24 statistic | | -| `ahoy_solar_radio_rx_fail_answer` | Gauge | NRF24 statistic | | -| `ahoy_solar_radio_frame_cnt` | Gauge | NRF24 statistic | | -| `ahoy_solar_radio_tx_cnt` | Gauge | NRF24 statistic | | +| Metric name | Type | Description | Labels | +|----------------------------------------------|---------|----------------------------------------------------------|--------------| +| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename | +| `ahoy_solar_uptime` | Counter | Seconds since boot of the AhoyDTU device | devicename | +| `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename | +| `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial | +| `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter | +| `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter | +| `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter | +| `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter | +| `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter | +| `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter | +| `ahoy_solar_Q_AC_var` | Gauge | AC reactive power[var] | inverter | +| `ahoy_solar_F_AC_hertz` | Gauge | AC frequency [Hz] | inverter | +| `ahoy_solar_PF_AC` | Gauge | AC Power factor | inverter | +| `ahoy_solar_Temp_celsius` | Gauge | Temperature of inverter | inverter | +| `ahoy_solar_ALARM_MES_ID` | Gauge | Alarm message index of inverter | inverter | +| `ahoy_solar_LastAlarmCode` | Gauge | Last alarm code from inverter | inverter | +| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter,channel | +| `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_Efficiency_ratio` | Gauge | ration AC Power over DC Power [%] | inverter | +| `ahoy_solar_U_DC_volt` | Gauge | DC voltage of channel [V] | inverter, channel | +| `ahoy_solar_I_DC_ampere` | Gauge | DC current of channel [A] | inverter, channel | +| `ahoy_solar_P_DC_watt` | Gauge | DC power of channel [P] | inverter, channel | +| `ahoy_solar_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_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter, channel | +| `ahoy_solar_Irradiation_ratio` | Gauge | ratio DC Power over set maximum power per channel [%] | inverter, channel | +| `ahoy_solar_radio_rx_success` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_rx_fail` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_rx_fail_answer` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_frame_cnt` | Gauge | NRF24 statistic | | +| `ahoy_solar_radio_tx_cnt` | Gauge | NRF24 statistic | | diff --git a/patches/AsyncWeb_Prometheus.patch b/patches/AsyncWeb_Prometheus.patch index 7fb9209a..21fe22cd 100644 --- a/patches/AsyncWeb_Prometheus.patch +++ b/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 index 22a549f..e0b36b3 100644 --- a/src/WebResponses.cpp diff --git a/patches/GxEPD2_SW_SPI.patch b/patches/GxEPD2_SW_SPI.patch new file mode 100644 index 00000000..9697eec8 --- /dev/null +++ b/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; + }; + diff --git a/patches/RF24_Hal.patch b/patches/RF24_Hal.patch new file mode 100644 index 00000000..1a4f159e --- /dev/null +++ b/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(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(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(spi_txbuff), reinterpret_cast(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(spi_txbuff), reinterpret_cast(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(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(spi_txbuff), reinterpret_cast(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(spi_txbuff), reinterpret_cast(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(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(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(spi_txbuff), reinterpret_cast(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(data_len + blank_len + 1); // Add register value to transmit buffer +- +- *ptx++ = R_RX_PAYLOAD; +- while (--size) { +- *ptx++ = RF24_NOP; +- } +- +- size = static_cast(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(spi_txbuff), reinterpret_cast(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(csn_pin % 10); +- uint8_t bus_numb = static_cast((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(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(csn_pin % 10); +- uint8_t bus_numb = static_cast((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(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(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(spi_speed / 1000000), getChannel(), ++ debugging_information, format_string, getChannel(), + static_cast(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((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 +-#endif ++#include "RF24_hal.h" + + /** + * @defgroup PALevel Power Amplifier level +@@ -115,29 +110,8 @@ typedef enum + class RF24 + { + private: +-#ifdef SOFTSPI +- SoftSPI 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; ++}; diff --git a/pics/PXL_20230824_204200660.jpg b/pics/PXL_20230824_204200660.jpg new file mode 100644 index 00000000..cfcb4668 Binary files /dev/null and b/pics/PXL_20230824_204200660.jpg differ diff --git a/pics/PXL_20230901_061927908.jpg b/pics/PXL_20230901_061927908.jpg new file mode 100644 index 00000000..dd8527f2 Binary files /dev/null and b/pics/PXL_20230901_061927908.jpg differ diff --git a/scripts/applyPatches.py b/scripts/applyPatches.py index 7250f5cb..3ba30a5f 100644 --- a/scripts/applyPatches.py +++ b/scripts/applyPatches.py @@ -3,7 +3,11 @@ import subprocess Import("env") def applyPatch(libName, patchFile): + # save current wd + start = os.getcwd() + if os.path.exists('.pio/libdeps/' + env['PIOENV'] + '/' + libName) == False: + print("path '" + '.pio/libdeps/' + env['PIOENV'] + '/' + libName + "' does not exist") return os.chdir('.pio/libdeps/' + env['PIOENV'] + '/' + libName) @@ -18,6 +22,13 @@ def applyPatch(libName, patchFile): else: print('applying \'' + patchFile + '\' failed') + os.chdir(start) + # 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") diff --git a/scripts/buildManifest.py b/scripts/buildManifest.py index db29b352..2664a39f 100644 --- a/scripts/buildManifest.py +++ b/scripts/buildManifest.py @@ -33,16 +33,16 @@ def buildManifest(path, infile, outfile): esp32 = {} esp32["chipFamily"] = "ESP32" esp32["parts"] = [] - esp32["parts"].append({"path": "bootloader.bin", "offset": 4096}) - esp32["parts"].append({"path": "partitions.bin", "offset": 32768}) - esp32["parts"].append({"path": "ota.bin", "offset": 57344}) - esp32["parts"].append({"path": version[1] + "_" + sha + "_esp32.bin", "offset": 65536}) + esp32["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096}) + esp32["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768}) + esp32["parts"].append({"path": "ESP32/ota.bin", "offset": 57344}) + esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32.bin", "offset": 65536}) data["builds"].append(esp32) esp8266 = {} esp8266["chipFamily"] = "ESP8266" 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) jsonString = json.dumps(data, indent=2) diff --git a/src/web/html/convert.py b/scripts/convertHtml.py similarity index 79% rename from src/web/html/convert.py rename to scripts/convertHtml.py index 00f398ac..6eaa92a3 100644 --- a/src/web/html/convert.py +++ b/scripts/convertHtml.py @@ -6,6 +6,7 @@ import shutil from datetime import date from pathlib import Path import subprocess +Import("env") def get_git_sha(): @@ -60,13 +61,41 @@ def htmlParts(file, header, nav, footer, version): link = 'GIT SHA: ' + get_git_sha() + ' :: ' + version + '' p = p.replace("{#VERSION}", version) p = p.replace("{#VERSION_GIT}", link) + + # remove if - endif ESP32 + p = checkIf(p) + f = open("tmp/" + file, "w") f.write(p); f.close(); return p +def checkIf(data): + if (env['PIOENV'][0:5] == "esp32") or env['PIOENV'][0:4] == "open": + data = data.replace("", "") + data = data.replace("", "") + data = data.replace("/*IF_ESP32*/", "") + data = data.replace("/*ENDIF_ESP32*/", "") + else: + while 1: + start = data.find("") + end = data.find("")+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): - fileType = inFile.split(".")[1] + fileType = inFile.split(".")[1] define = inFile.split(".")[0].upper() define2 = inFile.split(".")[1].upper() inFileVarName = inFile.replace(".", "_") @@ -118,9 +147,7 @@ def convert2Header(inFile, version): f.close() # delete all files in the 'h' dir -wd = 'h' -if os.getcwd()[-4:] != "html": - wd = "web/html/" + wd +wd = 'web/html/h' if os.path.exists(wd): for f in os.listdir(wd): @@ -131,9 +158,8 @@ if os.path.exists(wd): os.remove(os.path.join(wd, f)) # grab all files with following extensions -if os.getcwd()[-4:] != "html": - os.chdir('./web/html') -types = ('*.html', '*.css', '*.js', '*.ico') # the tuple of file types +os.chdir('./web/html') +types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types files_grabbed = [] for files in types: files_grabbed.extend(glob.glob(files)) diff --git a/scripts/getVersion.py b/scripts/getVersion.py index 11a76f62..cdb39ae8 100644 --- a/scripts/getVersion.py +++ b/scripts/getVersion.py @@ -50,58 +50,100 @@ def readVersion(path, infile): versionnumber += line[p+13:].rstrip() + "." 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") versionout = version[:-1] + "_" + sha + "_esp8266.bin" - src = path + ".pio/build/esp8266-release/firmware.bin" - dst = path + "firmware/" + versionout + src = path + ".pio/build/esp8266/firmware.bin" + dst = path + "firmware/ESP8266/" + versionout os.rename(src, dst) versionout = version[:-1] + "_" + sha + "_esp8266_prometheus.bin" - src = path + ".pio/build/esp8266-release-prometheus/firmware.bin" - dst = path + "firmware/" + versionout + src = path + ".pio/build/esp8266-prometheus/firmware.bin" + dst = path + "firmware/ESP8266/" + versionout os.rename(src, dst) versionout = version[:-1] + "_" + sha + "_esp8285.bin" - src = path + ".pio/build/esp8285-release/firmware.bin" - dst = path + "firmware/" + versionout + src = path + ".pio/build/esp8285/firmware.bin" + dst = path + "firmware/ESP8285/" + versionout os.rename(src, dst) gzip_bin(dst, dst + ".gz") versionout = version[:-1] + "_" + sha + "_esp32.bin" - src = path + ".pio/build/esp32-wroom32-release/firmware.bin" - dst = path + "firmware/" + versionout + src = path + ".pio/build/esp32-wroom32/firmware.bin" + dst = path + "firmware/ESP32/" + versionout os.rename(src, dst) versionout = version[:-1] + "_" + sha + "_esp32_prometheus.bin" - src = path + ".pio/build/esp32-wroom32-release-prometheus/firmware.bin" - dst = path + "firmware/" + versionout + src = path + ".pio/build/esp32-wroom32-prometheus/firmware.bin" + dst = path + "firmware/ESP32/" + versionout os.rename(src, dst) versionout = version[:-1] + "_" + sha + "_esp32_ethernet.bin" - src = path + ".pio/build/esp32-wroom32-ethernet-release/firmware.bin" - dst = path + "firmware/" + versionout + src = path + ".pio/build/esp32-wroom32-ethernet/firmware.bin" + 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) versionout = version[:-1] + "_" + sha + "_esp32s3.bin" - src = path + ".pio/build/opendtufusionv1-release/firmware.bin" - dst = path + "firmware/s3/" + versionout + src = path + ".pio/build/opendtufusion/firmware.bin" + 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) # other ESP32 bin files - src = path + ".pio/build/esp32-wroom32-release/" - dst = path + "firmware/" + src = path + ".pio/build/esp32-wroom32/" + 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 + "partitions.bin", dst + "partitions.bin") - genOtaBin(path + "firmware/") + genOtaBin(dst) - # other ESP32S3 bin files - src = path + ".pio/build/opendtufusionv1-release/" - dst = path + "firmware/s3/" + # other ESP32-S3-Eth bin files + src = path + ".pio/build/opendtufusion-ethernet/" + dst = path + "firmware/ESP32-S3-ETH/" os.rename(src + "bootloader.bin", dst + "bootloader.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") diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json index e61851fb..fa166086 100644 --- a/src/.vscode/settings.json +++ b/src/.vscode/settings.json @@ -84,4 +84,5 @@ }, "cmake.configureOnOpen": false, "editor.formatOnSave": false, + "cmake.sourceDirectory": "C:/lpusch/github/ahoy/src/.pio/libdeps/esp32-wroom32-release-prometheus/Adafruit BusIO", } \ No newline at end of file diff --git a/src/CHANGES.md b/src/CHANGES.md index dacf21c8..b7d75822 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -5,17 +5,16 @@ Changelog VArt (based on v0.7.36) Changelog v0.7.36 +======= +Changelog v0.8.36 -* added Ethernet variant -* fix configuration of ePaper -* fix MI inverter support -* endpoints `/api/record/live`, `/api/record/alarm`, `/api/record/config`, `/api/record/info` are obsolete -* added `/api/inverter/alarm/[ID]` to read inverter alarms -* added Alarms in Live View as modal window -* added MqTT transmission of last 10 alarms -* updated documentation -* 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 -* added information about maximum power (AC and DC) -* updated documentation -* several small fixes +* added dim option for LEDS +* changed reload time for opendtufusion after update to 5s +* fix default interval and gap for communication +* fix serial number in exported json (was decimal, now correct as hexdecimal number) +* beautified factory reset +* added second stage for erase settings +* increased maximal number of inverters to 32 for opendtufusion board (ESP32-S3) +* fixed crash if CMT inverter is enabled, but CMT isn't configured + +full version log: [Development Log](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md) diff --git a/src/app.cpp b/src/app.cpp index cb5e17ef..7e2a7640 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4,18 +4,13 @@ //----------------------------------------------------------------------------- #include - #include "app.h" - #include "utils/sun.h" #include "plugins/history.h" //----------------------------------------------------------------------------- -app::app() - : ah::Scheduler {}, - mInnerLoopCb {nullptr} { -} +app::app() : ah::Scheduler {} {} //----------------------------------------------------------------------------- @@ -24,7 +19,6 @@ void app::setup() { while (!Serial) yield(); - resetSystem(); mSettings.setup(); @@ -38,32 +32,18 @@ void app::setup() { DBGPRINTLN(F("false")); 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.enableDebug(); + 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); } #if defined(ESP32) if(mConfig->cmt.enabled) { - mCmtRadio.setup(mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, false); - mCmtRadio.enableDebug(); + mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, false); } #endif #ifdef ETHERNET 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); }); - DBGPRINTLN(F("done...")); - DSERIAL.flush(); #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) mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onNetwork, this, std::placeholders::_1)); #if !defined(AP_ONLY) @@ -71,24 +51,13 @@ void app::setup() { #endif #endif /* defined(ETHERNET) */ - mSys.setup(&mTimestamp); - mSys.addInverters(&mConfig->inst); - if (mConfig->nrf.enabled) { - mPayload.setup(this, &mSys, &mNrfRadio, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp); - mPayload.enableSerialDebug(mConfig->serial.debug); - 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)); + mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs); + mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); + mSys.setup(&mTimestamp, &mConfig->inst); + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + initInverter(i); } - #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 (!mNrfRadio.isChipConnected()) DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); @@ -100,11 +69,7 @@ void app::setup() { if (mMqttEnabled) { mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime); mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); - mPayload.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 + mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); }); } #endif setupLed(); @@ -112,11 +77,20 @@ void app::setup() { mWeb.setup(this, &mSys, mConfig); 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 + #if defined(PLUGIN_DISPLAY) 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->setup(this, &mSys, mConfig); @@ -134,115 +108,37 @@ void app::setup() { //----------------------------------------------------------------------------- void app::loop(void) { - if (mInnerLoopCb) - mInnerLoopCb(); - #if !defined(ETHERNET) + if(mConfig->nrf.enabled) + mNrfRadio.loop(); + #if defined(ESP32) + if(mConfig->cmt.enabled) + mCmtRadio.loop(); #endif -} -//----------------------------------------------------------------------------- -void app::loopStandard(void) { ah::Scheduler::loop(); + mCommunication.loop(); - if (mNrfRadio.loop() && mConfig->nrf.enabled) { - 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) + if (mMqttEnabled && mNetworkConnected) mMqtt.loop(); } -#if !defined(ETHERNET) -//----------------------------------------------------------------------------- -void app::loopWifi(void) { - ah::Scheduler::loop(); - yield(); -} -#endif /* !defined(ETHERNET) */ - //----------------------------------------------------------------------------- void app::onNetwork(bool gotIp) { DPRINTLN(DBG_DEBUG, F("onNetwork")); + mNetworkConnected = gotIp; ah::Scheduler::resetTicker(); - regularTickers(); // reinstall regular tickers - if (gotIp) { - 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; - mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! - once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); - #if !defined(ETHERNET) - if (WIFI_AP == WiFi.getMode()) { - 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"); - #endif /* defined(ETHERNET) */ + regularTickers(); //reinstall regular tickers + every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend"); + mMqttReconnect = true; + mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! + once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); + //tickNtpUpdate(); + #if !defined(ETHERNET) + if (WIFI_AP == WiFi.getMode()) { + mMqttEnabled = false; } + everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); + #endif /* !defined(ETHERNET) */ } //----------------------------------------------------------------------------- @@ -250,9 +146,11 @@ void app::regularTickers(void) { DPRINTLN(DBG_DEBUG, F("regularTickers")); everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); // Plugins + #if defined(PLUGIN_DISPLAY) if (mConfig->plugin.display.type != 0) 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) //everySec([this]() { mImprov.tickSerial(); }, "impro"); #endif @@ -284,11 +182,11 @@ void app::updateNtp(void) { if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed if (mConfig->inst.rstValsNotAvail) everyMin(std::bind(&app::tickMinute, this), "tMin"); - if (mConfig->inst.rstYieldMidNight) { - uint32_t localTime = gTimezone.toLocal(mTimestamp); - uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time - onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); - } + + uint32_t localTime = gTimezone.toLocal(mTimestamp); + uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); + if (mConfig->sys.schedReboot) { uint32_t localTime = gTimezone.toLocal(mTimestamp); 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) { uint32_t nxtTrig = 5; // default: check again in 5 sec #if defined(ETHERNET) - bool isOK = mEth.updateNtpTime(); + bool isOK = (mTimestamp != 0); + mEth.updateNtpTime(); #else bool isOK = mWifi.getNtpTime(); #endif @@ -323,7 +222,7 @@ void app::tickNtpUpdate(void) { // immediately start communicating if (isOK && mSendFirst) { mSendFirst = false; - once(std::bind(&app::tickSend, this), 2, "senOn"); + once(std::bind(&app::tickSend, this), 1, "senOn"); } mMqttReconnect = false; @@ -349,43 +248,50 @@ void app::tickCalcSunrise(void) { //----------------------------------------------------------------------------- void app::tickIVCommunication(void) { - mIVCommunicationOn = !mConfig->sun.disNightCom; // if sun.disNightCom is false, communication is always on - if (!mIVCommunicationOn) { // inverter communication only during the day - uint32_t nxtTrig; - if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start - nxtTrig = mSunrise - mConfig->sun.offsetSec; - } else { - if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise - nxtTrig = 0; - } else { // current time lies within communication start/stop time, set next trigger to communication stop - mIVCommunicationOn = true; - nxtTrig = mSunset + mConfig->sun.offsetSec; + bool restartTick = false; + bool zeroValues = false; + 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 + nxtTrig = mSunrise - mConfig->sun.offsetSec; + } else { + if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise + nxtTrig = 0; + } else { // current time lies within communication start/stop time, set next trigger to communication stop + iv->commEnabled = true; + nxtTrig = mSunset + mConfig->sun.offsetSec; + } } + if (nxtTrig != 0) + restartTick = true; } - if (nxtTrig != 0) - onceAt(std::bind(&app::tickIVCommunication, this), nxtTrig, "ivCom"); + + 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) { // 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 } -//----------------------------------------------------------------------------- -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) { zeroIvValues(!CHECK_AVAIL, SKIP_YIELD_DAY); @@ -400,69 +306,74 @@ void app::tickMinute(void) { //----------------------------------------------------------------------------- void app::tickMidnight(void) { - // only triggered if 'reset values at midnight is enabled' uint32_t localTime = gTimezone.toLocal(mTimestamp); uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); - zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY); + Inverter<> *iv; + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL == iv) + continue; // skip to next inverter - if (mMqttEnabled) - mMqtt.tickerMidnight(); + // 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); + + if (mMqttEnabled) + mMqtt.tickerMidnight(); + } } //----------------------------------------------------------------------------- void app::tickSend(void) { - if(mConfig->nrf.enabled) { - if(!mNrfRadio.isChipConnected()) { - DPRINTLN(DBG_WARN, F("NRF24 not connected!")); - return; - } + uint8_t fill = mCommunication.getFillState(); + uint8_t max = mCommunication.getMaxFill(); + if((max-MAX_NUM_INVERTERS) <= fill) { + DPRINT(DBG_WARN, F("send queue almost full, consider to increase interval, ")); + DBGPRINT(String(fill)); + DBGPRINT(F(" of ")); + DBGPRINT(String(max)); + DBGPRINTLN(F(" entries used")); } - if (mIVCommunicationOn) { - if (!mNrfRadio.mBufCtrl.empty()) { - if (mConfig->serial.debug) { - DPRINT(DBG_DEBUG, F("recbuf not empty! #")); - DBGPRINTLN(String(mNrfRadio.mBufCtrl.size())); - } - } - #if defined(ESP32) - if (!mCmtRadio.mBufCtrl.empty()) { - if (mConfig->serial.debug) { - DPRINT(DBG_INFO, F("recbuf not empty! #")); - DBGPRINTLN(String(mCmtRadio.mBufCtrl.size())); - } - } - #endif - int8_t maxLoop = MAX_NUM_INVERTERS; - Inverter<> *iv = mSys.getInverterByPos(mSendLastIvId); - do { - mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1; - iv = mSys.getInverterByPos(mSendLastIvId); - } while ((NULL == iv) && ((maxLoop--) > 0)); - - if (NULL != iv) { - if (iv->config->enabled) { - if(mConfig->nrf.enabled) { - if (iv->ivGen == IV_HM) - mPayload.ivSend(iv); - 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 + for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + Inverter<> *iv = mSys.getInverterByPos(i); + if(NULL == iv) + continue; + + if(iv->config->enabled) { + if(!iv->commEnabled) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("no communication to the inverter (night time)")); + continue; } + + if(!iv->radio->isChipConnected()) + continue; + + iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) { + if(isDevControl) + mCommunication.addImportant(iv, cmd); + else + mCommunication.add(iv, cmd); + }); } - } 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(); } @@ -478,6 +389,8 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { continue; // skip to next inverter if (!iv->config->enabled) continue; // skip to next inverter + if (iv->commEnabled) + continue; // skip to next inverter if (checkAvail) { if (!iv->isAvailable()) @@ -535,11 +448,10 @@ void app::resetSystem(void) { mSendLastIvId = 0; mShowRebootRequest = false; - mIVCommunicationOn = true; mSavePending = false; mSaveReboot = false; - memset(&mStat, 0, sizeof(statistics_t)); + mNetworkConnected = false; } //----------------------------------------------------------------------------- @@ -549,38 +461,44 @@ void app::mqttSubRxCb(JsonObject obj) { //----------------------------------------------------------------------------- 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); - 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); - digitalWrite(mConfig->led.led1, led_off); + analogWrite(mConfig->led.led1, led_off); } } //----------------------------------------------------------------------------- void app::updateLed(void) { - uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH; - uint8_t led_on = (mConfig->led.led_high_active) ? HIGH : LOW; - - if (mConfig->led.led0 != 0xff) { - Inverter<> *iv = mSys.getInverterByPos(0); - if (NULL != iv) { - if (iv->isProducing()) - digitalWrite(mConfig->led.led0, led_on); - else - digitalWrite(mConfig->led.led0, led_off); + uint8_t led_off = (mConfig->led.high_active) ? 0 : 255; + uint8_t led_on = (mConfig->led.high_active) ? (mConfig->led.luminance) : (255-mConfig->led.luminance); + + if (mConfig->led.led0 != DEF_PIN_OFF) { + Inverter<> *iv; + for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { + iv = mSys.getInverterByPos(id); + if (NULL != iv) { + if (iv->isProducing()) { + // turn on when at least one inverter is producing + analogWrite(mConfig->led.led0, led_on); + 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()) { - digitalWrite(mConfig->led.led1, led_on); + analogWrite(mConfig->led.led1, led_on); } else { - digitalWrite(mConfig->led.led1, led_off); + analogWrite(mConfig->led.led1, led_off); } } } diff --git a/src/app.h b/src/app.h index 5613770c..11536191 100644 --- a/src/app.h +++ b/src/app.h @@ -9,23 +9,23 @@ #include #include -#include "appInterface.h" #include "config/settings.h" #include "defines.h" -#include "hm/hmPayload.h" +#include "appInterface.h" #include "hm/hmSystem.h" #include "hm/hmRadio.h" +#if defined(ESP32) #include "hms/hmsRadio.h" -#include "hms/hmsPayload.h" -#include "hm/hmPayload.h" -#include "hm/miPayload.h" +#endif #include "publisher/pubMqtt.h" #include "publisher/pubSerial.h" #include "utils/crc.h" #include "utils/dbg.h" #include "utils/scheduler.h" +#include "utils/syslog.h" #include "web/RestApi.h" #include "web/web.h" +#include "hm/Communication.h" #if defined(ETHERNET) #include "eth/ahoyeth.h" #else /* defined(ETHERNET) */ @@ -42,22 +42,22 @@ #define ACOS(x) (degrees(acos(x))) typedef HmSystem HmSystemType; -typedef HmPayload> PayloadType; -typedef MiPayload> MiPayloadType; #ifdef ESP32 -typedef CmtRadio> CmtRadioType; -typedef HmsPayload HmsPayloadType; #endif typedef Web WebType; -typedef RestApi> RestApiType; +typedef RestApi RestApiType; typedef PubMqtt PubMqttType; typedef PubSerial PubSerialType; // PLUGINS +#if defined(PLUGIN_DISPLAY) #include "plugins/Display/Display.h" -#include "plugins/history.h" +#include "plugins/Display/Display_data.h" -typedef Display DisplayType; +//typedef Display DisplayType; +typedef Display DisplayType; +#endif +#include "plugins/history.h" class app : public IApp, public ah::Scheduler { public: @@ -66,16 +66,23 @@ class app : public IApp, public ah::Scheduler { void setup(void); void loop(void); - void loopStandard(void); -#if !defined(ETHERNET) - void loopWifi(void); -#endif /* !defined(ETHERNET) */ void onNetwork(bool gotIp); void regularTickers(void); void handleIntr(void) { mNrfRadio.handleIntr(); } + void* getRadioObj(bool nrf) { + if(nrf) + return (void*)&mNrfRadio; + else { + #ifdef ESP32 + return (void*)&mCmtRadio; + #else + return NULL; + #endif + } + } #ifdef ESP32 void handleHmsIntr(void) { @@ -88,7 +95,11 @@ class app : public IApp, public ah::Scheduler { } 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) { @@ -96,13 +107,23 @@ class app : public IApp, public ah::Scheduler { mSavePending = true; mSaveReboot = reboot; if(reboot) { - onNetwork(false); ah::Scheduler::resetTicker(); } once(std::bind(&app::tickSave, this), 3, "save"); 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) { return mSettings.readSettings(path); } @@ -123,10 +144,6 @@ class app : public IApp, public ah::Scheduler { return mSaveReboot; } - statistics_t *getStatistics() { - return &mStat; - } - #if !defined(ETHERNET) void scanAvailNetworks() { mWifi.scanAvailNetworks(); @@ -136,9 +153,6 @@ class app : public IApp, public ah::Scheduler { return mWifi.getAvailNetworks(obj); } - void setOnUpdate() { - onNetwork(false); - } #endif /* !defined(ETHERNET) */ void setRebootFlag() { @@ -173,19 +187,6 @@ class app : public IApp, public ah::Scheduler { 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() { return mMqtt.isConnected(); } @@ -202,11 +203,6 @@ class app : public IApp, public ah::Scheduler { return mWeb.isProtected(request); } - void getNrfRadioCounters(uint32_t *sendCnt, uint32_t *retransmits) { - *sendCnt = mNrfRadio.mSendCnt; - *retransmits = mNrfRadio.mRetransmits; - } - bool getNrfEnabled(void) { return mConfig->nrf.enabled; } @@ -223,15 +219,6 @@ class app : public IApp, public ah::Scheduler { 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() { return mApi.getTimezoneOffset(); } @@ -266,8 +253,6 @@ class app : public IApp, public ah::Scheduler { #define CHECK_AVAIL true #define SKIP_YIELD_DAY true - typedef std::function innerLoopCb; - void resetSystem(void); void zeroIvValues(bool checkAvail = false, bool skipYieldDay = true); @@ -276,8 +261,11 @@ class app : public IApp, public ah::Scheduler { if (mMqttEnabled) mMqtt.payloadEventListener(cmd, iv); #endif + #if defined(PLUGIN_DISPLAY) if(mConfig->plugin.display.type != 0) mDisplay.payloadEventListener(cmd); + #endif + updateLed(); } void mqttSubRxCb(JsonObject obj); @@ -287,7 +275,6 @@ class app : public IApp, public ah::Scheduler { void tickReboot(void) { DPRINTLN(DBG_INFO, F("Rebooting...")); - onNetwork(false); ah::Scheduler::resetTicker(); WiFi.disconnect(); delay(200); @@ -318,13 +305,11 @@ class app : public IApp, public ah::Scheduler { void tickZeroValues(void); void tickMidnight(void); - innerLoopCb mInnerLoopCb; - HmSystemType mSys; HmRadio<> mNrfRadio; + Communication mCommunication; bool mShowRebootRequest; - bool mIVCommunicationOn; #if defined(ETHERNET) ahoyeth mEth; @@ -333,15 +318,17 @@ class app : public IApp, public ah::Scheduler { #endif /* defined(ETHERNET) */ WebType mWeb; RestApiType mApi; - PayloadType mPayload; - MiPayloadType mMiPayload; + #ifdef ENABLE_SYSLOG + DbgSyslog mDbgSyslog; + #endif + //PayloadType mPayload; + //MiPayloadType mMiPayload; PubSerialType mPubSerial; #if !defined(ETHERNET) //Improv mImprov; #endif #ifdef ESP32 - CmtRadioType mCmtRadio; - HmsPayloadType mHmsPayload; + CmtRadio<> mCmtRadio; #endif char mVersion[12]; @@ -353,7 +340,7 @@ class app : public IApp, public ah::Scheduler { uint8_t mSendLastIvId; bool mSendFirst; - statistics_t mStat; + bool mNetworkConnected; // mqtt PubMqttType mMqtt; @@ -365,9 +352,13 @@ class app : public IApp, public ah::Scheduler { uint32_t mSunrise, mSunset; // plugins + #if defined(PLUGIN_DISPLAY) DisplayType mDisplay; + DisplayData mDispData; + #endif TotalPowerHistory *mTotalPowerHistory; YieldDayHistory *mYieldDayHistory; + }; #endif /*__APP_H__*/ diff --git a/src/appInterface.h b/src/appInterface.h index 91a3c2ef..630d647e 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -17,23 +17,25 @@ class TotalPowerHistory; class YieldDayHistory; +//#include "hms/hmsRadio.h" +#if defined(ESP32) +//typedef CmtRadio> CmtRadioType; +#endif + // abstract interface to App. Make members of App accessible from child class // like web or API without forward declaration class IApp { public: virtual ~IApp() {} virtual bool saveSettings(bool stopFs) = 0; + virtual void initInverter(uint8_t id) = 0; virtual bool readSettings(const char *path) = 0; virtual bool eraseSettings(bool eraseWifi) = 0; virtual bool getSavePending() = 0; virtual bool getLastSaveSucceed() = 0; virtual bool getShouldReboot() = 0; - #if !defined(ETHERNET) - virtual void setOnUpdate() = 0; - #endif /* defined(ETHERNET) */ virtual void setRebootFlag() = 0; virtual const char *getVersion() = 0; - virtual statistics_t *getStatistics() = 0; #if !defined(ETHERNET) virtual void scanAvailNetworks() = 0; @@ -42,10 +44,10 @@ class IApp { virtual uint32_t getUptime() = 0; virtual uint32_t getTimestamp() = 0; + virtual uint64_t getTimestampMs() = 0; virtual uint32_t getSunrise() = 0; virtual uint32_t getSunset() = 0; virtual void setTimestamp(uint32_t newTime) = 0; - virtual String getTimeStr(uint32_t offset) = 0; virtual uint32_t getTimezoneOffset() = 0; virtual void getSchedulerInfo(uint8_t *max) = 0; virtual void getSchedulerNames() = 0; @@ -54,10 +56,11 @@ class IApp { virtual bool getSettingsValid() = 0; virtual void setMqttDiscoveryFlag() = 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 getMqttTxCnt() = 0; @@ -68,6 +71,9 @@ class IApp { virtual TotalPowerHistory *getTotalPowerHistoryPtr() = 0; virtual YieldDayHistory *getYieldDayHistoryPtr() = 0; + + virtual void* getRadioObj(bool nrf) = 0; + }; #endif /*__IAPP_H__*/ diff --git a/src/config/config.h b/src/config/config.h index 0537195c..393c78e3 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -7,6 +7,10 @@ #define __CONFIG_H__ +// globally used +#define DEF_PIN_OFF 255 + + //------------------------------------- // WIFI CONFIGURATION //------------------------------------- @@ -40,11 +44,21 @@ #if defined(ETHERNET) #define ETH_SPI_HOST SPI2_HOST #define ETH_SPI_CLOCK_MHZ 25 - #define ETH_INT_GPIO 4 - #define ETH_MISO_GPIO 12 - #define ETH_MOSI_GPIO 13 - #define ETH_SCK_GPIO 14 - #define ETH_CS_PIN 15 + #ifndef DEF_ETH_IRQ_PIN + #define DEF_ETH_IRQ_PIN 4 + #endif + #ifndef DEF_ETH_MISO_PIN + #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) */ // time in seconds how long the station info (ssid + pwd) will be tried #define WIFI_TRY_CONNECT_TIME 30 @@ -63,41 +77,96 @@ // for the ESP32-S3 there is no sane 'default', as it has full flexibility // to map its two HW SPIs anywhere and PCBs differ materially, // so it has to be selected in the Web UI - #define DEF_CS_PIN 5 - #define DEF_CE_PIN 17 - #define DEF_IRQ_PIN 16 - #define DEF_MISO_PIN 19 - #define DEF_MOSI_PIN 23 - #define DEF_SCLK_PIN 18 + #ifndef DEF_NRF_CS_PIN + #define DEF_NRF_CS_PIN 5 + #endif + #ifndef DEF_NRF_CE_PIN + #define DEF_NRF_CE_PIN 4 + #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 - #define DEF_CS_PIN 15 - #define DEF_CE_PIN 0 - #define DEF_IRQ_PIN 2 + #ifndef DEF_NRF_CS_PIN + #define DEF_NRF_CS_PIN 15 + #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 // they cannot actually be moved for ESP82xx models - #define DEF_MISO_PIN 12 - #define DEF_MOSI_PIN 13 - #define DEF_SCLK_PIN 14 + #ifndef DEF_NRF_MISO_PIN + #define DEF_NRF_MISO_PIN 12 + #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 - -// default NRF24 power, possible values (0 - 3) -#define DEF_AMPLIFIERPOWER 1 // number of packets hold in buffer #define PACKET_BUFFER_SIZE 30 // number of configurable inverters #if defined(ESP32) - #define MAX_NUM_INVERTERS 16 + #if defined(CONFIG_IDF_TARGET_ESP32S3) + #define MAX_NUM_INVERTERS 32 + #else + #define MAX_NUM_INVERTERS 16 + #endif #else #define MAX_NUM_INVERTERS 4 #endif -// default serial interval -#define SERIAL_INTERVAL 5 - // default send interval -#define SEND_INTERVAL 30 +#define SEND_INTERVAL 15 // maximum human readable inverter name length #define MAX_NAME_LENGTH 16 @@ -106,10 +175,7 @@ #define MAX_RF_PAYLOAD_SIZE 32 // maximum total payload buffers (must be greater than the number of received frame fragments) -#define MAX_PAYLOAD_ENTRIES 10 - -// maximum requests for retransmits per payload (per inverter) -#define DEF_MAX_RETRANS_PER_PYLD 5 +#define MAX_PAYLOAD_ENTRIES 20 // number of seconds since last successful response, before inverter is marked inactive #define INVERTER_INACT_THRES_SEC 5*60 @@ -156,13 +222,6 @@ // reconnect delay #define MQTT_RECONNECT_DELAY 5000 -// Offset for midnight Ticker -// relative to UTC -// may be negative for later in the next day or positive for earlier in previous day -// may contain variable like mCalculatedTimezoneOffset -// must be in parentheses -#define MIDNIGHTTICKER_OFFSET (-1) - #if __has_include("config_override.h") #include "config_override.h" #endif diff --git a/src/config/config_override_example.h b/src/config/config_override_example.h index e7c06b77..b90bbdbd 100644 --- a/src/config/config_override_example.h +++ b/src/config/config_override_example.h @@ -9,11 +9,11 @@ // override fallback WiFi info #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 #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 #define FB_WIFI_PWD "MY_WIFI_KEY" @@ -31,12 +31,17 @@ #undef DEF_SCLK_PIN #define DEF_SCLK_PIN 36 -// Offset for midnight Ticker Example: 1 second before midnight (local time) -#undef MIDNIGHTTICKER_OFFSET -#define MIDNIGHTTICKER_OFFSET (mCalculatedTimezoneOffset + 1) - // To enable the endpoint for prometheus to scrape data from at /metrics // #define ENABLE_PROMETHEUS_EP +// to enable the syslog logging (will disable web-serial) +//#define ENABLE_SYSLOG +#ifdef ENABLE_SYSLOG +#define SYSLOG_HOST "" +#define SYSLOG_APP "ahoy" +#define SYSLOG_FACILITY FAC_USER +#define SYSLOG_PORT 514 +#endif + #endif /*__CONFIG_OVERRIDE_H__*/ diff --git a/src/config/settings.h b/src/config/settings.h index 51843ab9..ec61d733 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -29,7 +29,8 @@ * More info: * 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 @@ -67,35 +68,31 @@ typedef struct { bool darkMode; bool schedReboot; -#if defined(ETHERNET) - // ethernet - -#else /* defined(ETHERNET) */ +#if !defined(ETHERNET) // wifi char stationSsid[SSID_LEN]; char stationPwd[PWD_LEN]; char apPwd[PWD_LEN]; bool isHidden; -#endif /* defined(ETHERNET) */ +#endif /* !defined(ETHERNET) */ cfgIp_t ip; } cfgSys_t; typedef struct { bool enabled; - uint16_t sendInterval; - uint8_t maxRetransPerPyld; uint8_t pinCs; uint8_t pinCe; uint8_t pinIrq; uint8_t pinMiso; uint8_t pinMosi; uint8_t pinSclk; - uint8_t amplifierPower; } cfgNrf24_t; typedef struct { bool enabled; + uint8_t pinSclk; + uint8_t pinSdio; uint8_t pinCsb; uint8_t pinFcsb; uint8_t pinIrq; @@ -110,20 +107,21 @@ typedef struct { typedef struct { float lat; float lon; - bool disNightCom; // disable night communication uint16_t offsetSec; } cfgSun_t; typedef struct { - uint16_t interval; bool showIv; bool debug; + bool privacyLog; + bool printWholeTrace; } cfgSerial_t; typedef struct { - uint8_t led0; // first LED pin - uint8_t led1; // second LED pin - bool led_high_active; // determines if LEDs are high or low active + uint8_t led0; // first LED pin + uint8_t led1; // second LED pin + bool high_active; // determines if LEDs are high or low active + uint8_t luminance; // luminance of LED } cfgLed_t; typedef struct { @@ -143,23 +141,31 @@ typedef struct { uint16_t chMaxPwr[6]; double yieldCor[6]; // YieldTotal correction value 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; typedef struct { bool enabled; cfgIv_t iv[MAX_NUM_INVERTERS]; + uint16_t sendInterval; bool rstYieldMidNight; bool rstValsNotAvail; bool rstValsCommStop; + bool rstMaxValsMidNight; bool startWithoutTime; float yieldEffiency; + uint16_t gapMs; + bool readGrid; } cfgInst_t; typedef struct { uint8_t type; bool pwrSaveAtIvOffline; - bool pxShift; + uint8_t screenSaver; uint8_t rot; //uint16_t wakeUp; //uint16_t sleepAt; @@ -170,6 +176,7 @@ typedef struct { uint8_t disp_reset; uint8_t disp_busy; uint8_t disp_dc; + uint8_t pirPin; } display_t; typedef struct { @@ -188,6 +195,7 @@ typedef struct { cfgInst_t inst; plugins_t plugin; bool valid; + uint16_t configVersion; } settings_t; class settings { @@ -284,6 +292,7 @@ class settings { if(root.containsKey(F("led"))) jsonLed(root[F("led")]); if(root.containsKey(F("plugin"))) jsonPlugin(root[F("plugin")]); if(root.containsKey(F("inst"))) jsonInst(root[F("inst")]); + getConfigVersion(root.as()); } else { Serial.println(F("failed to parse json, using default config")); @@ -299,6 +308,7 @@ class settings { DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE); JsonObject root = json.to(); + json[F("version")] = CONFIG_VERSION; jsonNetwork(root.createNestedObject(F("wifi")), true); jsonNrf(root.createNestedObject(F("nrf")), true); #if defined(ESP32) @@ -368,36 +378,41 @@ class settings { mCfg.sys.darkMode = false; mCfg.sys.schedReboot = false; // restore temp settings - #if defined(ETHERNET) - memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); - #else /* defined(ETHERNET) */ if(keepWifi) memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); + #if !defined(ETHERNET) else { snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD); mCfg.sys.isHidden = false; } - #endif /* defined(ETHERNET) */ + #endif /* !defined(ETHERNET) */ snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); - mCfg.nrf.sendInterval = SEND_INTERVAL; - mCfg.nrf.maxRetransPerPyld = DEF_MAX_RETRANS_PER_PYLD; - mCfg.nrf.pinCs = DEF_CS_PIN; - mCfg.nrf.pinCe = DEF_CE_PIN; - mCfg.nrf.pinIrq = DEF_IRQ_PIN; - mCfg.nrf.pinMiso = DEF_MISO_PIN; - mCfg.nrf.pinMosi = DEF_MOSI_PIN; - mCfg.nrf.pinSclk = DEF_SCLK_PIN; + mCfg.nrf.pinCs = DEF_NRF_CS_PIN; + mCfg.nrf.pinCe = DEF_NRF_CE_PIN; + mCfg.nrf.pinIrq = DEF_NRF_IRQ_PIN; + mCfg.nrf.pinMiso = DEF_NRF_MISO_PIN; + mCfg.nrf.pinMosi = DEF_NRF_MOSI_PIN; + mCfg.nrf.pinSclk = DEF_NRF_SCLK_PIN; - mCfg.nrf.amplifierPower = DEF_AMPLIFIERPOWER & 0x03; 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.pinFcsb = DEF_PIN_OFF; mCfg.cmt.pinIrq = DEF_PIN_OFF; + #endif mCfg.cmt.enabled = false; 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.lon = 0.0; - mCfg.sun.disNightCom = false; mCfg.sun.offsetSec = 0; - mCfg.serial.interval = SERIAL_INTERVAL; mCfg.serial.showIv = false; mCfg.serial.debug = false; + mCfg.serial.privacyLog = true; + mCfg.serial.printWholeTrace = false; mCfg.mqtt.port = DEF_MQTT_PORT; snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER); @@ -420,29 +435,78 @@ class settings { snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); mCfg.mqtt.interval = 0; // off + mCfg.inst.sendInterval = SEND_INTERVAL; mCfg.inst.rstYieldMidNight = false; mCfg.inst.rstValsNotAvail = false; mCfg.inst.rstValsCommStop = 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; - mCfg.led.led1 = DEF_PIN_OFF; - mCfg.led.led_high_active = false; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value + 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.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.disp_data = DEF_PIN_OFF; // SDA - mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL + mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA + mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL mCfg.plugin.display.disp_cs = DEF_PIN_OFF; mCfg.plugin.display.disp_reset = DEF_PIN_OFF; mCfg.plugin.display.disp_busy = DEF_PIN_OFF; mCfg.plugin.display.disp_dc = DEF_PIN_OFF; - } + 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(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) { if(set) { @@ -489,55 +553,59 @@ class settings { void jsonNrf(JsonObject obj, bool set = false) { if(set) { - obj[F("intvl")] = mCfg.nrf.sendInterval; - obj[F("maxRetry")] = mCfg.nrf.maxRetransPerPyld; obj[F("cs")] = mCfg.nrf.pinCs; obj[F("ce")] = mCfg.nrf.pinCe; obj[F("irq")] = mCfg.nrf.pinIrq; obj[F("sclk")] = mCfg.nrf.pinSclk; obj[F("mosi")] = mCfg.nrf.pinMosi; obj[F("miso")] = mCfg.nrf.pinMiso; - obj[F("pwr")] = mCfg.nrf.amplifierPower; obj[F("en")] = (bool) mCfg.nrf.enabled; } else { - getVal(obj, F("intvl"), &mCfg.nrf.sendInterval); - getVal(obj, F("maxRetry"), &mCfg.nrf.maxRetransPerPyld); getVal(obj, F("cs"), &mCfg.nrf.pinCs); getVal(obj, F("ce"), &mCfg.nrf.pinCe); getVal(obj, F("irq"), &mCfg.nrf.pinIrq); getVal(obj, F("sclk"), &mCfg.nrf.pinSclk); getVal(obj, F("mosi"), &mCfg.nrf.pinMosi); getVal(obj, F("miso"), &mCfg.nrf.pinMiso); - getVal(obj, F("pwr"), &mCfg.nrf.amplifierPower); #if !defined(ESP32) mCfg.nrf.enabled = true; // ESP8266, read always as enabled #else mCfg.nrf.enabled = (bool) obj[F("en")]; #endif if((obj[F("cs")] == obj[F("ce")])) { - mCfg.nrf.pinCs = DEF_CS_PIN; - mCfg.nrf.pinCe = DEF_CE_PIN; - mCfg.nrf.pinIrq = DEF_IRQ_PIN; - mCfg.nrf.pinSclk = DEF_SCLK_PIN; - mCfg.nrf.pinMosi = DEF_MOSI_PIN; - mCfg.nrf.pinMiso = DEF_MISO_PIN; + mCfg.nrf.pinCs = DEF_NRF_CS_PIN; + mCfg.nrf.pinCe = DEF_NRF_CE_PIN; + mCfg.nrf.pinIrq = DEF_NRF_IRQ_PIN; + mCfg.nrf.pinSclk = DEF_NRF_SCLK_PIN; + mCfg.nrf.pinMosi = DEF_NRF_MOSI_PIN; + mCfg.nrf.pinMiso = DEF_NRF_MISO_PIN; } } } + #if defined(ESP32) void jsonCmt(JsonObject obj, bool set = false) { if(set) { obj[F("csb")] = mCfg.cmt.pinCsb; obj[F("fcsb")] = mCfg.cmt.pinFcsb; 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; } else { mCfg.cmt.pinCsb = obj[F("csb")]; mCfg.cmt.pinFcsb = obj[F("fcsb")]; 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")]; + 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) { if(set) { @@ -558,42 +626,44 @@ class settings { if(set) { obj[F("lat")] = mCfg.sun.lat; obj[F("lon")] = mCfg.sun.lon; - obj[F("dis")] = mCfg.sun.disNightCom; obj[F("offs")] = mCfg.sun.offsetSec; } else { getVal(obj, F("lat"), &mCfg.sun.lat); getVal(obj, F("lon"), &mCfg.sun.lon); - getVal(obj, F("dis"), &mCfg.sun.disNightCom); getVal(obj, F("offs"), &mCfg.sun.offsetSec); } } void jsonSerial(JsonObject obj, bool set = false) { if(set) { - obj[F("intvl")] = mCfg.serial.interval; obj[F("show")] = mCfg.serial.showIv; obj[F("debug")] = mCfg.serial.debug; + obj[F("prv")] = (bool) mCfg.serial.privacyLog; + obj[F("trc")] = (bool) mCfg.serial.printWholeTrace; } else { - getVal(obj, F("intvl"), &mCfg.serial.interval); getVal(obj, F("show"), &mCfg.serial.showIv); getVal(obj, F("debug"), &mCfg.serial.debug); + getVal(obj, F("prv"), &mCfg.serial.privacyLog); + getVal(obj, F("trc"), &mCfg.serial.printWholeTrace); } } void jsonMqtt(JsonObject obj, bool set = false) { if(set) { - obj[F("broker")] = mCfg.mqtt.broker; - obj[F("port")] = mCfg.mqtt.port; - obj[F("user")] = mCfg.mqtt.user; - obj[F("pwd")] = mCfg.mqtt.pwd; - obj[F("topic")] = mCfg.mqtt.topic; - obj[F("intvl")] = mCfg.mqtt.interval; + obj[F("broker")] = mCfg.mqtt.broker; + obj[F("port")] = mCfg.mqtt.port; + obj[F("clientId")] = mCfg.mqtt.clientId; + obj[F("user")] = mCfg.mqtt.user; + obj[F("pwd")] = mCfg.mqtt.pwd; + obj[F("topic")] = mCfg.mqtt.topic; + obj[F("intvl")] = mCfg.mqtt.interval; } else { getVal(obj, F("port"), &mCfg.mqtt.port); getVal(obj, F("intvl"), &mCfg.mqtt.interval); getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_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("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN); } @@ -601,13 +671,15 @@ class settings { void jsonLed(JsonObject obj, bool set = false) { if(set) { - obj[F("0")] = mCfg.led.led0; - obj[F("1")] = mCfg.led.led1; - obj[F("act_high")] = mCfg.led.led_high_active; + obj[F("0")] = mCfg.led.led0; + obj[F("1")] = mCfg.led.led1; + obj[F("act_high")] = mCfg.led.high_active; + obj[F("lum")] = mCfg.led.luminance; } else { getVal(obj, F("0"), &mCfg.led.led0); getVal(obj, F("1"), &mCfg.led.led1); - getVal(obj, F("act_high"), &mCfg.led.led_high_active); + getVal(obj, F("act_high"), &mCfg.led.high_active); + getVal(obj, F("lum"), &mCfg.led.luminance); } } @@ -616,7 +688,7 @@ class settings { JsonObject disp = obj.createNestedObject("disp"); disp[F("type")] = mCfg.plugin.display.type; disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; - disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift; + disp[F("screenSaver")] = mCfg.plugin.display.screenSaver; disp[F("rotation")] = mCfg.plugin.display.rot; //disp[F("wake")] = mCfg.plugin.display.wakeUp; //disp[F("sleep")] = mCfg.plugin.display.sleepAt; @@ -627,11 +699,12 @@ class settings { disp[F("reset")] = mCfg.plugin.display.disp_reset; disp[F("busy")] = mCfg.plugin.display.disp_busy; disp[F("dc")] = mCfg.plugin.display.disp_dc; + disp[F("pirPin")] = mCfg.plugin.display.pirPin; } else { JsonObject disp = obj["disp"]; getVal(disp, F("type"), &mCfg.plugin.display.type); getVal(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline); - getVal(disp, F("pxShift"), &mCfg.plugin.display.pxShift); + getVal(disp, F("screenSaver"), &mCfg.plugin.display.screenSaver); getVal(disp, F("rotation"), &mCfg.plugin.display.rot); //mCfg.plugin.display.wakeUp = disp[F("wake")]; //mCfg.plugin.display.sleepAt = disp[F("sleep")]; @@ -642,25 +715,34 @@ class settings { getVal(disp, F("reset"), &mCfg.plugin.display.disp_reset); getVal(disp, F("busy"), &mCfg.plugin.display.disp_busy); getVal(disp, F("dc"), &mCfg.plugin.display.disp_dc); + getVal(disp, F("pirPin"), &mCfg.plugin.display.pirPin); } } void jsonInst(JsonObject obj, bool set = false) { if(set) { + obj[F("intvl")] = mCfg.inst.sendInterval; obj[F("en")] = (bool)mCfg.inst.enabled; - obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; - obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; - obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; - obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime; - obj[F("yldEff")] = mCfg.inst.yieldEffiency; + obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; + obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; + obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; + obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime; + obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstMaxValsMidNight; + obj[F("yldEff")] = mCfg.inst.yieldEffiency; + obj[F("gap")] = mCfg.inst.gapMs; + obj[F("rdGrid")] = (bool)mCfg.inst.readGrid; } else { + getVal(obj, F("intvl"), &mCfg.inst.sendInterval); getVal(obj, F("en"), &mCfg.inst.enabled); getVal(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight); getVal(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail); getVal(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop); getVal(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime); + getVal(obj, F("rstMaxMidNight"), &mCfg.inst.rstMaxValsMidNight); getVal(obj, F("yldEff"), &mCfg.inst.yieldEffiency); + getVal(obj, F("gap"), &mCfg.inst.gapMs); + getVal(obj, F("rdGrid"), &mCfg.inst.readGrid); if(mCfg.inst.yieldEffiency < 0.5) mCfg.inst.yieldEffiency = 1.0f; @@ -685,6 +767,10 @@ class settings { obj[F("en")] = (bool)cfg->enabled; obj[F("name")] = cfg->name; 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++) { obj[F("yield")][i] = cfg->yieldCor[i]; obj[F("pwr")][i] = cfg->chMaxPwr[i]; @@ -694,6 +780,10 @@ class settings { getVal(obj, F("en"), &cfg->enabled); getChar(obj, F("name"), cfg->name, MAX_NAME_LENGTH); getVal(obj, F("sn"), &cfg->serial.u64); + getVal(obj, F("freq"), &cfg->frequency); + getVal(obj, F("pa"), &cfg->powerLevel); + getVal(obj, F("dis"), &cfg->disNightCom); + getVal(obj, F("add"), &cfg->add2Total); uint8_t size = 4; if(obj.containsKey(F("pwr"))) size = obj[F("pwr")].size(); diff --git a/src/defines.h b/src/defines.h index 8b6c0dab..23919899 100644 --- a/src/defines.h +++ b/src/defines.h @@ -12,14 +12,16 @@ // VERSION //------------------------------------- #define VERSION_MAJOR 0 -#define VERSION_MINOR 7 +#define VERSION_MINOR 8 #define VERSION_PATCH 36 //------------------------------------- typedef struct { uint8_t ch; uint8_t len; + int8_t rssi; uint8_t packet[MAX_RF_PAYLOAD_SIZE]; + uint16_t millis; } packet_t; typedef enum { @@ -92,11 +94,15 @@ enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE}; #define MQTT_MAX_PACKET_SIZE 384 +#define PLUGIN_DISPLAY + typedef struct { uint32_t rxFail; uint32_t rxFailNoAnser; uint32_t rxSuccess; uint32_t frmCnt; + uint32_t txCnt; + uint32_t retransmits; } statistics_t; #endif /*__DEFINES_H__*/ diff --git a/src/eth/ahoyeth.cpp b/src/eth/ahoyeth.cpp index cb63a950..2226fce6 100644 --- a/src/eth/ahoyeth.cpp +++ b/src/eth/ahoyeth.cpp @@ -10,7 +10,7 @@ #define F(sl) (sl) #endif #include "ahoyeth.h" - +#include //----------------------------------------------------------------------------- ahoyeth::ahoyeth() @@ -26,12 +26,16 @@ void ahoyeth::setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNe mOnNetworkCB = onNetworkCB; mOnTimeCB = onTimeCB; - DPRINTLN(DBG_INFO, F("[ETH]: Register for events...")); Serial.flush(); WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void { this->onEthernetEvent(event, info); }); - DPRINTLN(DBG_INFO, F("[ETH]: begin...")); 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) { IPAddress ip(mConfig->sys.ip.ip); 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)) 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; 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(); IPAddress timeServer; if (!WiFi.hostByName(mConfig->ntp.addr, timeServer)) @@ -68,8 +69,7 @@ bool ahoyeth::updateNtpTime(void) { return false; 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(); this->handleNTPPacket(packet); }); @@ -129,11 +129,9 @@ void ahoyeth::welcome(String ip, String mode) { 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...")); - switch (event) - { + switch (event) { #if ( ( defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2) ) && ( ARDUINO_ESP32_GIT_VER != 0x46d5afb1 ) ) // For breaking core v2.0.0 // 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; 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()); + #endif - if (ETH.fullDuplex()) - { - AWS_LOG0(F("FULL_DUPLEX, ")); - } - else - { + if (ETH.fullDuplex()) { + AWS_LOG0(F("FULL_DUPLEX, ")); + } else { 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; 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; case ARDUINO_EVENT_ETH_DISCONNECTED: @@ -208,16 +213,12 @@ void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info) break; 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()); - if (ETH.fullDuplex()) - { + if (ETH.fullDuplex()) { AWS_LOG0(F("FULL_DUPLEX, ")); - } - else - { + } else { 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; 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; case SYSTEM_EVENT_ETH_DISCONNECTED: diff --git a/src/eth/ahoyeth.h b/src/eth/ahoyeth.h index 9f5123fb..157a9c76 100644 --- a/src/eth/ahoyeth.h +++ b/src/eth/ahoyeth.h @@ -13,6 +13,7 @@ #include #include +#include "ethSpi.h" #include "../utils/dbg.h" #include "../config/config.h" #include "../config/settings.h" @@ -45,6 +46,9 @@ class ahoyeth { void onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info); private: + #if defined(CONFIG_IDF_TARGET_ESP32S3) + EthSpi mEthSpi; + #endif settings_t *mConfig; uint32_t *mUtcTimestamp; @@ -57,4 +61,4 @@ class ahoyeth { }; #endif /*__AHOYETH_H__*/ -#endif /* defined(ETHERNET) */ \ No newline at end of file +#endif /* defined(ETHERNET) */ diff --git a/src/eth/ethSpi.h b/src/eth/ethSpi.h new file mode 100644 index 00000000..d0ef9487 --- /dev/null +++ b/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 +#include +#include +#include + +// 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(pin_rst)); + gpio_set_direction(static_cast(pin_rst), GPIO_MODE_OUTPUT); + gpio_set_level(static_cast(pin_rst), 0); + + gpio_reset_pin(static_cast(pin_sclk)); + gpio_reset_pin(static_cast(pin_mosi)); + gpio_reset_pin(static_cast(pin_miso)); + gpio_reset_pin(static_cast(pin_cs)); + gpio_set_pull_mode(static_cast(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(pin_int)); + gpio_set_pull_mode(static_cast(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(pin_rst), 1); + delayMicroseconds(1000); + + // Arduino function to start networking stack if not already started + tcpipInit(); + + ESP_ERROR_CHECK(tcpip_adapter_set_default_eth_handlers()); // ? + + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi); + w5500_config.int_gpio_num = pin_int; + + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + mac_config.rx_task_stack_size = 4096; + esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.reset_gpio_num = -1; + esp_eth_phy_t *phy = esp_eth_phy_new_w5500(&phy_config); + + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + ESP_ERROR_CHECK(esp_eth_driver_install(ð_config, ð_handle)); + + // Configure MAC address + uint8_t mac_addr[6]; + ESP_ERROR_CHECK(esp_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*/ diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h new file mode 100644 index 00000000..bb815a7e --- /dev/null +++ b/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 +#include +#include "hmInverter.h" +#include "../utils/dbg.h" + +template +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 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 mQueue; + uint8_t mWrPtr = 0; + uint8_t mRdPtr = 0; +}; + + +#endif /*__COMM_QUEUE_H__*/ diff --git a/src/hm/Communication.h b/src/hm/Communication.h new file mode 100644 index 00000000..9f96533d --- /dev/null +++ b/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 +#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 *)> payloadListenerType; +typedef std::function *)> 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 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__*/ diff --git a/src/hm/Heuristic.h b/src/hm/Heuristic.h new file mode 100644 index 00000000..64c78cc8 --- /dev/null +++ b/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__*/ diff --git a/src/hm/HeuristicInv.h b/src/hm/HeuristicInv.h new file mode 100644 index 00000000..e7ad6edd --- /dev/null +++ b/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__*/ diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h index 7c814857..55259289 100644 --- a/src/hm/hmDefines.h +++ b/src/hm/hmDefines.h @@ -10,7 +10,8 @@ #include // 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 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_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_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_HW_ID, - FLD_ACT_ACTIVE_PWR_LIMIT, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP}; - + FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_BOOTLOADER_VER, + 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", "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", - "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","HWPartId", - "active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower"}; + "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","BootloaderVersion", + "active_PowerLimit", "HWPartNumber", "HWVersion", "GridProfileCode", + "GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower"}; const char* const notAvail = "n/a"; const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH, UNIT_V, UNIT_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_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 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}; +#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 { 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_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 } + { FLD_BOOTLOADER_VER, UNIT_NONE, CH0, 8, 2, 1 } }; #define HMINFO_LIST_LEN (sizeof(InfoAssignment) / sizeof(byteAssign_t)) #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[] = { { FLD_ACT_ACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 2, 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 ALARM_LOG_ENTRY_SIZE 12 +#define HMGETLOSSRATE_PAYLOAD_LEN 4 +#define AHOY_GET_LOSS_INTERVAL 10 //------------------------------------- // HM300, HM350, HM400 @@ -242,4 +265,91 @@ const byteAssign_t hm4chAssignment[] = { #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__*/ diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 594f4211..776b9c6f 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -11,12 +11,17 @@ #define F(sl) (sl) #endif +#define MAX_GRID_LENGTH 150 + #include "hmDefines.h" +#include "HeuristicInv.h" #include "../hms/hmsDefines.h" #include #include +#include #include "../config/settings.h" +#include "radio.h" /** * For values which are of interest and not transmitted by the inverter can be * calculated automatically. @@ -24,10 +29,6 @@ * automatically. Their result does not differ from original read values. */ -// forward declaration of class -template -class Inverter; - // prototypes template @@ -65,7 +66,7 @@ struct calcFunc_t { template 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 T *record; // data pointer uint32_t ts; // timestamp of last received payload @@ -80,31 +81,6 @@ struct alarm_t { 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 template const calcFunc_t calcFunctions[] = { @@ -142,16 +118,34 @@ class Inverter { uint8_t channels; // number of PV channels (1-4) record_t recordMeas; // structure for measured values record_t recordInfo; // structure for info values + record_t recordHwInfo; // structure for simple (hardware) info values record_t recordConfig; // structure for system config values record_t recordAlarm; // structure for alarm values - //String lastAlarmMsg; - bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) bool isConnected; // shows if inverter was successfully identified (fw version and hardware info) InverterStatus status; // indicates the current inverter status std::array lastAlarm; // holds last 10 alarms 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 cfgInst_t *generalConfig; // general inverter configuration from setup @@ -164,73 +158,78 @@ class Inverter { actPowerLimit = 0xffff; // init feedback from inverter to -1 mDevControlRequest = false; devControlCmd = InitDataState; - initialized = false; - //lastAlarmMsg = "nothing"; alarmMesIndex = 0; isConnected = false; status = InverterStatus::OFF; alarmNxtWrPos = 0; alarmCnt = 0; - } - - ~Inverter() { - // TODO: cleanup - } - - template - void enqueCommand(uint8_t cmd) { - _commandQueue.push(std::make_shared(cmd)); - DPRINT_IVID(DBG_INFO, id); - DBGPRINT(F("enqueCommand: 0x")); - DBGHEXLN(cmd); - } - - void setQueuedCmdFinished() { - if (!_commandQueue.empty()) { - // Will destroy CommandAbstract Class Object (?) - _commandQueue.pop(); - } - } - - void clearCmdQueue() { - DPRINTLN(DBG_INFO, F("clearCmdQueue")); - while (!_commandQueue.empty()) { - // Will destroy CommandAbstract Class Object (?) - _commandQueue.pop(); - } - } - - uint8_t getQueuedCmd() { - if (_commandQueue.empty()) { - if (ivGen != IV_MI) { - if (getFwVersion() == 0) - enqueCommand(InverterDevInform_All); // firmware version - enqueCommand(RealTimeRunData_Debug); // live data - } else if (ivGen == IV_MI){ - if (getFwVersion() == 0) - enqueCommand(InverterDevInform_All); // firmware version; might not work, esp. for 1/2 ch hardware - if (type == INV_TYPE_4CH) { - enqueCommand(0x36); - } else { - enqueCommand(0x09); - } + alarmLastId = 0; + rssi = -127; + miMultiParts = 0; + mGotLastMsg = false; + mCmd = InitDataState; + mIsSingleframeReq = false; + radio = NULL; + commEnabled = true; + mIvRxCnt = 0; + mIvTxCnt = 0; + mDtuRxCnt = 0; + mDtuTxCnt = 0; + + memset(&radioStatistics, 0, sizeof(statistics_t)); + memset(heuristics.txRfQuality, -6, 5); + + memset(mOffYD, 0, sizeof(float) * 6); + memset(mLastYD, 0, sizeof(float) * 6); + } + + void tickSend(std::function cb) { + if(mDevControlRequest) { + cb(devControlCmd, true); + mDevControlRequest = false; + } else if (IV_MI != ivGen) { + mGetLossInterval++; + if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) + cb(AlarmData, false); // get last alarms + else if(0 == getFwVersion()) + cb(InverterDevInform_All, false); // get firmware version + else if(0 == getHwVersion()) + cb(InverterDevInform_Simple, false); // get hardware version + else if(actPowerLimit == 0xffff) + cb(SystemConfigPara, false); // power limit info + else if(InitDataState != devControlCmd) { + cb(devControlCmd, false); // custom command which was received by API + devControlCmd = InitDataState; + mGetLossInterval = 1; + } else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile + 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 { + 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(SystemConfigPara); // power limit info } - return _commandQueue.front().get()->getCmd(); } - void init(void) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:init")); initAssignment(&recordMeas, RealTimeRunData_Debug); initAssignment(&recordInfo, InverterDevInform_All); + initAssignment(&recordHwInfo, InverterDevInform_Simple); initAssignment(&recordConfig, SystemConfigPara); initAssignment(&recordAlarm, AlarmData); 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) { @@ -280,12 +279,10 @@ class Inverter { return isConnected; } - void clearDevControlRequest() { - mDevControlRequest = false; - } - - inline bool getDevControlRequest() { - return mDevControlRequest; + bool setDevCommand(uint8_t cmd) { + if(isConnected) + devControlCmd = cmd; + return isConnected; } 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) { 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) { - 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 { if ((REC_TYP)(div) > 1) rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); @@ -325,11 +327,9 @@ class Inverter { if (getPosByChFld(0, FLD_EVT, rec) == pos) { if (alarmMesIndex < rec->record[pos]) { alarmMesIndex = rec->record[pos]; - //enqueCommand(AlarmUpdate); // What is the function of AlarmUpdate? DPRINT(DBG_INFO, "alarm ID incremented to "); DBGPRINTLN(String(alarmMesIndex)); - enqueCommand(AlarmData); } } } @@ -338,6 +338,10 @@ class Inverter { // eg. fw version ... isConnected = true; } + else if (rec->assign == SimpleInfoAssignment) { + DPRINTLN(DBG_DEBUG, "add simple info"); + // eg. hw version ... + } else if (rec->assign == SystemConfigParaAssignment) { DPRINTLN(DBG_DEBUG, "add config"); if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ @@ -348,12 +352,9 @@ class Inverter { } else if (rec->assign == AlarmDataAssignment) { DPRINTLN(DBG_DEBUG, "add alarm"); - //if (getPosByChFld(0, FLD_LAST_ALARM_CODE, rec) == pos){ - // lastAlarmMsg = getAlarmStr(rec->record[pos]); - //} } else - DPRINTLN(DBG_WARN, F("add with unknown assginment")); + DPRINTLN(DBG_WARN, F("add with unknown assignment")); } else DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x")); @@ -388,6 +389,10 @@ class Inverter { 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) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue")); if(NULL == rec) @@ -410,6 +415,10 @@ class Inverter { bool isAvailable() { 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) avail = true; if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC) @@ -426,6 +435,7 @@ class Inverter { if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) { status = InverterStatus::OFF; actPowerLimit = 0xffff; // power limit will be read once inverter becomes available + alarmMesIndex = 0; } else status = InverterStatus::WAS_ON; @@ -448,10 +458,31 @@ class Inverter { return producing; } + InverterStatus getStatus(){ + isProducing(); // recalculate status + return status; + } + uint16_t getFwVersion() { record_t<> *rec = getRecordStruct(InverterDevInform_All); - uint8_t pos = getPosByChFld(CH0, FLD_FW_VERSION, rec); - return getValue(pos, rec); + return getChannelFieldValue(CH0, FLD_FW_VERSION, 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) { @@ -461,11 +492,12 @@ class Inverter { record_t<> *getRecordStruct(uint8_t cmd) { switch (cmd) { - case RealTimeRunData_Debug: return &recordMeas; // 11 = 0x0b - case InverterDevInform_All: return &recordInfo; // 1 = 0x01 - case SystemConfigPara: return &recordConfig; // 5 = 0x05 - case AlarmData: return &recordAlarm; // 17 = 0x11 - default: break; + case RealTimeRunData_Debug: return &recordMeas; // 11 = 0x0b + case InverterDevInform_Simple: return &recordHwInfo; // 0 = 0x00 + case InverterDevInform_All: return &recordInfo; // 1 = 0x01 + case SystemConfigPara: return &recordConfig; // 5 = 0x05 + case AlarmData: return &recordAlarm; // 17 = 0x11 + default: break; } return NULL; } @@ -485,10 +517,7 @@ class Inverter { rec->length = (uint8_t)(HMS1CH_LIST_LEN); rec->assign = (byteAssign_t *)hms1chAssignment; rec->pyldLen = HMS1CH_PAYLOAD_LEN; - } /*else if(IV_MI == ivGen) { - rec->length = (uint8_t)(HM1CH_LIST_LEN); - rec->assign = (byteAssign_t *)hm1chAssignment; - }*/ + } channels = 1; } else if (INV_TYPE_2CH == type) { @@ -533,6 +562,11 @@ class Inverter { rec->assign = (byteAssign_t *)InfoAssignment; rec->pyldLen = HMINFO_PAYLOAD_LEN; 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: rec->length = (uint8_t)(HMSYSTEM_LIST_LEN); 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) { uint8_t startOff = 2 + id * ALARM_LOG_ENTRY_SIZE; if((startOff + ALARM_LOG_ENTRY_SIZE) > len) @@ -563,9 +630,9 @@ class Inverter { uint32_t startTimeOffset = 0, endTimeOffset = 0; uint32_t start, endTime; - 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 + // check if is AM or PM + startTimeOffset = ((wCode >> 13) & 0x01) * 12 * 60 * 60; + if (((wCode >> 12) & 0x03) != 0) endTimeOffset = 12 * 60 * 60; 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); alarmCnt++; + alarmLastId = alarmMesIndex; return pyld[startOff+1]; } @@ -582,59 +650,102 @@ class Inverter { static String getAlarmStr(uint16_t alarmCode) { switch (alarmCode) { // breaks are intentionally missing! case 1: return String(F("Inverter start")); - case 2: return String(F("DTU command failed")); + 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 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 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 128: return String(F("Software error code 128")); - case 129: return String(F("Software error code 129")); + case 128: return String(F("Hardware configuration error")); + case 129: return String(F("Abnormal bias")); case 130: return String(F("Offline")); - case 141: return String(F("Grid overvoltage")); - case 142: return String(F("Average grid overvoltage")); - case 143: return String(F("Grid undervoltage")); - case 144: return String(F("Grid overfrequency")); - case 145: return String(F("Grid underfrequency")); - case 146: return String(F("Rapid grid frequency change")); - case 147: return String(F("Power grid outage")); - case 148: return String(F("Grid disconnection")); - case 149: return String(F("Island detected")); - case 205: return String(F("Input port 1 & 2 overvoltage")); - case 206: return String(F("Input port 3 & 4 overvoltage")); - case 207: return String(F("Input port 1 & 2 undervoltage")); - case 208: return String(F("Input port 3 & 4 undervoltage")); - case 209: return String(F("Port 1 no input")); - case 210: return String(F("Port 2 no input")); - case 211: return String(F("Port 3 no input")); - case 212: return String(F("Port 4 no input")); - case 213: return String(F("PV-1 & PV-2 abnormal wiring")); - case 214: return String(F("PV-3 & PV-4 abnormal wiring")); - case 215: return String(F("PV-1 Input overvoltage")); - case 216: return String(F("PV-1 Input undervoltage")); - case 217: return String(F("PV-2 Input overvoltage")); - case 218: return String(F("PV-2 Input undervoltage")); - case 219: return String(F("PV-3 Input overvoltage")); - case 220: return String(F("PV-3 Input undervoltage")); - case 221: return String(F("PV-4 Input overvoltage")); - case 222: return String(F("PV-4 Input undervoltage")); - case 301: return String(F("Hardware error code 301")); - case 302: return String(F("Hardware error code 302")); - case 303: return String(F("Hardware error code 303")); - case 304: return String(F("Hardware error code 304")); - case 305: return String(F("Hardware error code 305")); - case 306: return String(F("Hardware error code 306")); - case 307: return String(F("Hardware error code 307")); - case 308: return String(F("Hardware error code 308")); + case 141: return String(F("Grid: Grid overvoltage")); + case 142: return String(F("Grid: 10 min value grid overvoltage")); + case 143: return String(F("Grid: Grid undervoltage")); + case 144: return String(F("Grid: Grid overfrequency")); + case 145: return String(F("Grid: Grid underfrequency")); + case 146: return String(F("Grid: Rapid grid frequency change rate")); + case 147: return String(F("Grid: Power grid outage")); + case 148: return String(F("Grid: Grid disconnection")); + case 149: return String(F("Grid: Island detected")); + + case 150: return String(F("DCI exceeded")); + + case 171: return String(F("Grid: Abnormal phase difference between phase to phase")); + case 181: return String(F("Abnormal insulation impedance")); + case 182: return String(F("Abnormal grounding")); + case 205: return String(F("MPPT-A: Input overvoltage")); + case 206: return String(F("MPPT-B: Input overvoltage")); + case 207: return String(F("MPPT-A: Input undervoltage")); + case 208: return String(F("MPPT-B: Input undervoltage")); + case 209: return String(F("PV-1: No input")); + case 210: return String(F("PV-2: No input")); + case 211: return String(F("PV-3: No input")); + case 212: return String(F("PV-4: No input")); + case 213: return String(F("MPPT-A: PV-1 & PV-2 abnormal wiring")); + case 214: return String(F("MPPT-B: PV-3 & PV-4 abnormal wiring")); + case 215: return String(F("MPPT-C: Input overvoltage")); + case 216: return String(F("PV-1: Input undervoltage")); + case 217: return String(F("PV-2: Input overvoltage")); + case 218: return String(F("PV-2: Input undervoltage")); + case 219: return String(F("PV-3: Input overvoltage")); + case 220: return String(F("PV-3: Input undervoltage")); + case 221: return String(F("PV-4: Input overvoltage")); + case 222: return String(F("PV-4: Input undervoltage")); + + 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 310: return String(F("Hardware error code 310")); case 311: return String(F("Hardware error code 311")); case 312: return String(F("Hardware error code 312")); case 313: return String(F("Hardware error code 313")); 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 5043: return String(F("Error code-04 Port 3")); - case 5044: return String(F("Error code-04 Port 4")); + + case 5011: return String(F("PV-1: MOSFET overcurrent (II)")); + case 5012: return String(F("PV-2: MOSFET overcurrent (II)")); + 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 5052: return String(F("PV Input 2 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 5090: return String(F("Grid Overfrequency/Underfrequency")); case 5100: return String(F("Island detected")); + case 5110: return String(F("GFDI")); 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 5160: return String(F("Grid transient fluctuation")); 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")); 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: inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) { lastAlarm[alarmNxtWrPos] = alarm_t(code, start, end); @@ -670,8 +804,11 @@ class Inverter { radioId.b[0] = 0x01; } - std::queue> _commandQueue; - bool mDevControlRequest; // true if change needed + private: + float mOffYD[6], mLastYD[6]; + bool mDevControlRequest; // true if change needed + uint8_t mGridLen = 0; + uint8_t mGridProfile[MAX_GRID_LENGTH]; }; template diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h deleted file mode 100644 index 441981cf..00000000 --- a/src/hm/hmPayload.h +++ /dev/null @@ -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 - -typedef struct { - uint8_t txCmd; - uint8_t txId; - uint8_t invId; - uint32_t ts; - uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; - uint8_t len[MAX_PAYLOAD_ENTRIES]; - bool complete; - uint8_t maxPackId; - bool lastFound; - uint8_t retransmits; - bool requested; - bool gotFragment; -} invPayload_t; - - -typedef std::function *)> payloadListenerType; -typedef std::function *)> alarmListenerType; - - -template -class HmPayload { - public: - HmPayload() {} - - void setup(IApp *app, HMSYSTEM *sys, 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(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(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__*/ diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h index a9f24776..762e3adf 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/hmRadio.h @@ -3,180 +3,177 @@ // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- -#ifndef __RADIO_H__ -#define __RADIO_H__ +#ifndef __HM_RADIO_H__ +#define __HM_RADIO_H__ -#include "../utils/dbg.h" #include -#include "../utils/crc.h" -#include "../config/config.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 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"}; +#define TX_REQ_DREDCONTROL 0x50 +#define DRED_A5 0xa5 +#define DRED_5A 0x5a +#define DRED_AA 0xaa +#define DRED_55 0x55 //----------------------------------------------------------------------------- // HM Radio class //----------------------------------------------------------------------------- -template -class HmRadio { +template +class HmRadio : public Radio { public: - HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { - if(mSerialDebug) { - DPRINT(DBG_VERBOSE, F("hmRadio.h : HmRadio():mNrf24(CE_PIN: ")); - 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; + HmRadio() { + mDtuSn = DTU_SN; + 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() {} - 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")); + pinMode(irq, INPUT_PULLUP); - uint32_t dtuSn = 0x87654321; - uint32_t chipID = 0; // will be filled with last 3 bytes of MAC - #ifdef ESP32 - uint64_t MAC = ESP.getEfuseMac(); - chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF); - #else - 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; + mSerialDebug = serialDebug; + mPrivacyMode = privacyMode; + mPrintWholeTrace = printWholeTrace; + + generateDtuSn(); + DTU_RADIO_ID = ((uint64_t)(((mDtuSn >> 24) & 0xFF) | ((mDtuSn >> 8) & 0xFF00) | ((mDtuSn << 8) & 0xFF0000) | ((mDtuSn << 24) & 0xFF000000)) << 8) | 0x01; #ifdef ESP32 - #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 - mSpi = new SPIClass(HSPI); + #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) + mNrfHal.init(mosi, miso, sclk, cs, ce, SPI_SPEED); + mNrf24.reset(new RF24(&mNrfHal)); #else - mSpi = new SPIClass(VSPI); + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 + mSpi.reset(new SPIClass(HSPI)); + #else + mSpi.reset(new SPIClass(VSPI)); + #endif + mSpi->begin(sclk, miso, mosi, cs); #endif - mSpi->begin(sclk, miso, mosi, cs); #else //the old ESP82xx cannot freely place their SPI pins - mSpi = new SPIClass(); + mSpi.reset(new SPIClass()); mSpi->begin(); #endif - mNrf24.begin(mSpi, ce, cs); - mNrf24.setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms - - mNrf24.setChannel(mRfChLst[mRxChIdx]); - mNrf24.startListening(); - mNrf24.setDataRate(RF24_250KBPS); - mNrf24.setAutoAck(true); - mNrf24.enableDynamicPayloads(); - mNrf24.setCRCLength(RF24_CRC_16); - mNrf24.setAddressWidth(5); - mNrf24.openReadingPipe(1, reinterpret_cast(&DTU_RADIO_ID)); + + #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) + mNrf24->begin(); + #else + mNrf24->begin(mSpi.get(), ce, cs); + #endif + mNrf24->setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms + + mNrf24->setChannel(mRfChLst[mRxChIdx]); + mNrf24->startListening(); + mNrf24->setDataRate(RF24_250KBPS); + mNrf24->setAutoAck(true); + mNrf24->enableDynamicAck(); + mNrf24->enableDynamicPayloads(); + mNrf24->setCRCLength(RF24_CRC_16); + mNrf24->setAddressWidth(5); + mNrf24->openReadingPipe(1, reinterpret_cast(&DTU_RADIO_ID)); // enable all receiving interrupts - mNrf24.maskIRQ(false, false, false); + mNrf24->maskIRQ(false, false, false); - DPRINT(DBG_INFO, F("RF24 Amp Pwr: RF24_PA_")); - DPRINTLN(DBG_INFO, String(rf24AmpPowerNames[ampPwr])); - mNrf24.setPALevel(ampPwr & 0x03); + mNrf24->setPALevel(1); // low is default - if(mNrf24.isChipConnected()) { + if(mNrf24->isChipConnected()) { DPRINTLN(DBG_INFO, F("Radio Config:")); - mNrf24.printPrettyDetails(); - } - else + mNrf24->printPrettyDetails(); + DPRINT(DBG_INFO, F("DTU_SN: ")); + DBGPRINTLN(String(mDtuSn, HEX)); + } else DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); } - bool loop(void) { + void loop(void) { if (!mIrqRcvd) - return false; // nothing to do + return; // nothing to do mIrqRcvd = false; bool tx_ok, tx_fail, rx_ready; - mNrf24.whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH - mNrf24.flush_tx(); // empty TX FIFO + mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH + mNrf24->flush_tx(); // empty TX FIFO // start listening - mNrf24.setChannel(mRfChLst[mRxChIdx]); - mNrf24.startListening(); + uint8_t chOffset = 2; + mRxChIdx = (mTxChIdx + chOffset) % RF_CHANNELS; + mNrf24->setChannel(mRfChLst[mRxChIdx]); + mNrf24->startListening(); - uint32_t loopMillis = millis(); - while (millis()-loopMillis < 400) { + if(NULL == mLastIv) // prevent reading on NULL object! + return; + + uint32_t innerLoopTimeout = 55000; + uint32_t loopMillis = millis(); + 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(); - while (micros()-startMicros < 5111) { // listen (4088us or?) 5110us to each channel + while ((micros() - startMicros) < innerLoopTimeout) { // listen (4088us or?) 5110us to each channel if (mIrqRcvd) { mIrqRcvd = false; - if (getReceived()) { // everything received - return true; + + if (getReceived()) { // everything received + 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(); } // switch to next RX channel - if(++mRxChIdx >= RF_CHANNELS) - mRxChIdx = 0; - mNrf24.setChannel(mRfChLst[mRxChIdx]); - yield(); + mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS; + mNrf24->setChannel(mRfChLst[mRxChIdx]); + innerLoopTimeout = 4088; + isRxInit = false; } // not finished but time is over - return true; - } - void handleIntr(void) { - mIrqRcvd = true; + return; } bool isChipConnected(void) { //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected")); - return mNrf24.isChipConnected(); - } - void enableDebug() { - mSerialDebug = true; + return mNrf24->isChipConnected(); } - void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) { - DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); + void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("sendControlPacket cmd: ")); DBGHEXLN(cmd); - initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); + initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME); 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++] = 0x00; if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet @@ -186,160 +183,194 @@ class HmRadio { mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling } } else { //MI 2nd gen. specific + uint16_t powerMax = ((iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower()); switch (cmd) { + case Restart: case TurnOn: - //mTxBuf[0] = 0x50; - mTxBuf[9] = 0x55; - mTxBuf[10] = 0xaa; + mTxBuf[9] = DRED_55; + mTxBuf[10] = DRED_AA; break; case TurnOff: - mTxBuf[9] = 0xaa; - mTxBuf[10] = 0x55; + mTxBuf[9] = DRED_AA; + mTxBuf[10] = DRED_55; break; case ActivePowerContr: - cnt++; - mTxBuf[9] = 0x5a; - mTxBuf[10] = 0x5a; - mTxBuf[11] = data[0]; // power limit + if (data[1]<256) { // non persistent + mTxBuf[9] = DRED_5A; + mTxBuf[10] = DRED_5A; + //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; default: return; } cnt++; } - sendPacket(invId, cnt, isRetransmit, isNoMI); - } - - 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); + sendPacket(iv, cnt, isRetransmit, (IV_MI != iv->ivGen)); } uint8_t getDataRate(void) { - if(!mNrf24.isChipConnected()) - return 3; // unkown - return mNrf24.getDataRate(); + if(!mNrf24->isChipConnected()) + return 3; // unknown + return mNrf24->getDataRate(); } bool isPVariant(void) { - return mNrf24.isPVariant(); + return mNrf24->isPVariant(); } - std::queue mBufCtrl; - - uint32_t mSendCnt; - uint32_t mRetransmits; - - bool mSerialDebug; - private: - bool getReceived(void) { + inline bool getReceived(void) { 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; - while(mNrf24.available()) { + while(mNrf24->available()) { 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) { packet_t p; p.ch = mRfChLst[mRxChIdx]; - p.len = len; - mNrf24.read(p.packet, len); + p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len; + p.rssi = mNrf24->testRPD() ? -64 : -75; + p.millis = millis() - mMillis; + mNrf24->read(p.packet, p.len); if (p.packet[0] != 0x00) { - mBufCtrl.push(p); - if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command + 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); + 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 - 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 - else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore fragment number zero and MI status messages //#0 was p.packet[0] != 0x00 && - isLastPackage = true; // response from dev control command + 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 } } - yield(); - } - return isLastPackage; - } - - void initPacket(uint64_t invId, uint8_t mid, uint8_t pid) { - if(mSerialDebug) { - DPRINT(DBG_VERBOSE, F("initPacket, mid: ")); - DPRINT(DBG_VERBOSE, String(mid, HEX)); - DPRINT(DBG_VERBOSE,F(" pid: ")); - DPRINTLN(DBG_VERBOSE,String(pid, HEX)); + yield(); } - memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); - mTxBuf[0] = mid; // message id - CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); - CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8)); - mTxBuf[9] = pid; + if(isLastPackage) + mLastIv->mGotLastMsg = true; + return isLastPackage; } - 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++; + void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) { + mNrf24->setPALevel(iv->config->powerLevel & 0x03); + updateCrcs(&len, appendCrc16); // set TX and RX channels - mTxChIdx = (mTxChIdx + 1) % RF_CHANNELS; - mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS; + mTxChIdx = iv->heuristics.txRfChId; - if(mSerialDebug) { - DPRINT(DBG_INFO, F("TX ")); + if(*mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("TX ")); DBGPRINT(String(len)); - DBGPRINT("B Ch"); + DBGPRINT(" CH"); DBGPRINT(String(mRfChLst[mTxChIdx])); DBGPRINT(F(" | ")); - ah::dumpBuf(mTxBuf, len); + if(*mPrintWholeTrace) { + if(*mPrivacyMode) + ah::dumpBuf(mTxBuf, len, 1, 4); + else + ah::dumpBuf(mTxBuf, len); + } else { + DHEX(mTxBuf[0]); + DBGPRINT(F(" ")); + DHEX(mTxBuf[10]); + DBGPRINT(F(" ")); + DBGHEXLN(mTxBuf[9]); + } } - mNrf24.stopListening(); - mNrf24.setChannel(mRfChLst[mTxChIdx]); - mNrf24.openWritingPipe(reinterpret_cast(&invId)); - mNrf24.startWrite(mTxBuf, len, false); // false = request ACK response + mNrf24->stopListening(); + mNrf24->setChannel(mRfChLst[mTxChIdx]); + mNrf24->openWritingPipe(reinterpret_cast(&iv->radioId.u64)); + mNrf24->startWrite(mTxBuf, len, false); // false = request ACK response + mMillis = millis(); - if(isRetransmit) - mRetransmits++; - else - mSendCnt++; + mLastIv = iv; + iv->mDtuTxCnt++; } - volatile bool mIrqRcvd; - uint64_t DTU_RADIO_ID; + uint64_t getIvId(Inverter<> *iv) { + return iv->radioId.u64; + } - uint8_t mRfChLst[RF_CHANNELS]; - uint8_t mTxChIdx; - uint8_t mRxChIdx; + uint8_t getIvGen(Inverter<> *iv) { + return iv->ivGen; + } - SPIClass* mSpi; - RF24 mNrf24; - uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; + inline bool checkIvSerial(uint8_t buf[], Inverter<> *iv) { + for(uint8_t i = 1; i < 5; i++) { + if(buf[i] != iv->radioId.b[i]) + return false; + } + return true; + } + + uint64_t DTU_RADIO_ID; + uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz + uint8_t mTxChIdx = 0; + uint8_t mRxChIdx = 0; + bool mGotLastMsg = false; + uint32_t mMillis; + + std::unique_ptr mSpi; + std::unique_ptr mNrf24; + #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) + nrfHal mNrfHal; + #endif + Inverter<> *mLastIv = NULL; }; -#endif /*__RADIO_H__*/ +#endif /*__HM_RADIO_H__*/ diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h index 25d6dd11..b86f8d08 100644 --- a/src/hm/hmSystem.h +++ b/src/hm/hmSystem.h @@ -7,70 +7,40 @@ #define __HM_SYSTEM_H__ #include "hmInverter.h" +#include template > class HmSystem { public: HmSystem() {} - void setup(uint32_t *timestamp) { + void setup(uint32_t *timestamp, cfgInst_t *config) { mInverter[0].timestamp = timestamp; - mNumInv = 0; - } - - void addInverters(cfgInst_t *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 *iv)> cb) { DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter")); - if(MAX_INVERTER <= mNumInv) { - DPRINT(DBG_WARN, F("max number of inverters reached!")); - return NULL; - } - INVERTERTYPE *p = &mInverter[mNumInv]; - p->id = mNumInv; - p->config = config; - DPRINT(DBG_VERBOSE, "SERIAL: " + String(p->config->serial.b[5], HEX)); - DPRINTLN(DBG_VERBOSE, " " + String(p->config->serial.b[4], HEX)); - if((p->config->serial.b[5] == 0x11) || (p->config->serial.b[5] == 0x10)) { - switch(p->config->serial.b[4]) { + INVERTERTYPE *iv = &mInverter[id]; + iv->id = id; + iv->config = &mInverter[0].generalConfig->iv[id]; + DPRINT(DBG_VERBOSE, "SERIAL: " + String(iv->config->serial.b[5], HEX)); + DPRINTLN(DBG_VERBOSE, " " + String(iv->config->serial.b[4], HEX)); + if((iv->config->serial.b[5] == 0x11) || (iv->config->serial.b[5] == 0x10)) { + switch(iv->config->serial.b[4]) { case 0x24: // HMS-500 case 0x22: - case 0x21: p->type = INV_TYPE_1CH; + case 0x21: iv->type = INV_TYPE_1CH; break; case 0x44: // HMS-1000 case 0x42: - case 0x41: p->type = INV_TYPE_2CH; + case 0x41: iv->type = INV_TYPE_2CH; break; case 0x64: // HMS-2000 case 0x62: - case 0x61: p->type = INV_TYPE_4CH; + case 0x61: iv->type = INV_TYPE_4CH; break; default: @@ -78,32 +48,52 @@ class HmSystem { break; } - if(p->config->serial.b[5] == 0x11) { - if((p->config->serial.b[4] & 0x0f) == 0x04) - p->ivGen = IV_HMS; + if(iv->config->serial.b[5] == 0x11) { + if((iv->config->serial.b[4] & 0x0f) == 0x04) + iv->ivGen = IV_HMS; else - p->ivGen = IV_HM; + iv->ivGen = IV_HM; } - else if((p->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM - p->ivGen = IV_HM; + else if((iv->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM + iv->ivGen = IV_HM; else // MI 2nd Gen - p->ivGen = IV_MI; - } else if(p->config->serial.b[5] == 0x13) { - p->ivGen = IV_HMT; - p->type = INV_TYPE_6CH; - } else if(p->config->serial.u64 != 0ULL) + iv->ivGen = IV_MI; + } else if(iv->config->serial.b[5] == 0x13) { + iv->ivGen = IV_HMT; + iv->type = INV_TYPE_6CH; + } else if(iv->config->serial.u64 != 0ULL) { DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); + return; + } else + iv->ivGen = IV_UNKNOWN; + + iv->init(); + if(IV_UNKNOWN == iv->ivGen) + return; // serial is 0 + + 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)); - p->init(); + 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!!!")); - mNumInv ++; - return p; + cb(iv); } INVERTERTYPE *findInverter(uint8_t buf[]) { DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter")); INVERTERTYPE *p; - for(uint8_t i = 0; i < mNumInv; i++) { + for(uint8_t i = 0; i < MAX_INVERTER; i++) { p = &mInverter[i]; if((p->config->serial.b[3] == buf[0]) && (p->config->serial.b[2] == buf[1]) @@ -118,7 +108,7 @@ class HmSystem { DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); if(pos >= MAX_INVERTER) 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]; else return NULL; @@ -138,7 +128,6 @@ class HmSystem { private: INVERTERTYPE mInverter[MAX_INVERTER]; - uint8_t mNumInv; }; #endif /*__HM_SYSTEM_H__*/ diff --git a/src/hm/miPayload.h b/src/hm/miPayload.h deleted file mode 100644 index fb909663..00000000 --- a/src/hm/miPayload.h +++ /dev/null @@ -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 - -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 *)> miPayloadListenerType; - - -template -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(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(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__*/ diff --git a/src/hm/nrfHal.h b/src/hm/nrfHal.h new file mode 100644 index 00000000..c9fbcdc7 --- /dev/null +++ b/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 +#include + +#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(mosi); + mPinMiso = static_cast(miso); + mPinClk = static_cast(sclk); + mPinCs = static_cast(cs); + mPinEn = static_cast(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(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(data_len) + static_cast(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(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(data_len) + static_cast(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__*/ diff --git a/src/hm/radio.h b/src/hm/radio.h new file mode 100644 index 00000000..2fe4f640 --- /dev/null +++ b/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 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 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__*/ diff --git a/src/hms/cmt2300a.h b/src/hms/cmt2300a.h index 95d62191..1ff112e2 100644 --- a/src/hms/cmt2300a.h +++ b/src/hms/cmt2300a.h @@ -6,16 +6,11 @@ #ifndef __CMT2300A_H__ #define __CMT2300A_H__ +#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) +#include "cmtHal.h" +#else #include "esp32_3wSpi.h" - -#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 +#endif // 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_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 // [6] go_tx @@ -153,7 +152,43 @@ #define CMT2300A_MASK_TX_DONE_FLG 0x08 #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 { // 0x00 - 0x0f -- RSSI offset +- 0 and 13dBm 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, 0x12, 0x1E, 0x00, 0xAA, 0x06, 0x00, 0x00, 0x00, // 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, // 0x50 - 0x5f 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}; -template class Cmt2300a { - typedef SPI SpiType; public: Cmt2300a() {} - void setup(uint8_t pinCsb, uint8_t pinFcsb) { - mSpi.setup(pinCsb, pinFcsb); - init(); - } - - void setup() { - mSpi.setup(); + void setup(uint8_t pinSclk, uint8_t pinSdio, uint8_t pinCsb, uint8_t pinFcsb) { + mSpi.init(pinSdio, pinSclk, pinCsb, pinFcsb); init(); } @@ -239,7 +267,7 @@ class Cmt2300a { 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) return CMT_ERR_TX_PENDING; @@ -250,7 +278,7 @@ class Cmt2300a { if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) return CMT_ERR_SWITCH_STATE; - mSpi.readFifo(buf, len); + mSpi.readFifo(buf, rxLen, maxlen); *rssi = mSpi.readReg(CMT2300A_CUS_RSSI_DBM) - 128; if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP)) @@ -315,6 +343,8 @@ class Cmt2300a { mSpi.writeReg(CMT2300A_CUS_MODE_STA, 0x52); mSpi.writeReg(0x62, 0x20); + if(mSpi.readReg(0x62) != 0x20) + return false; // not connected! for(uint8_t i = 0; i < 0x60; i++) { mSpi.writeReg(i, cmtConfig[i]); @@ -360,7 +390,7 @@ class Cmt2300a { inline uint8_t freq2Chan(const uint32_t freqKhz) { if((freqKhz % FREQ_STEP_KHZ) != 0) { - DPRINT(DBG_WARN, F("swtich frequency to ")); + DPRINT(DBG_WARN, F("switch frequency to ")); DBGPRINT(String(freqKhz)); DBGPRINT(F("kHz not possible!")); return 0xff; // error @@ -373,7 +403,7 @@ class Cmt2300a { return 0xff; // error 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; } @@ -389,6 +419,30 @@ class Cmt2300a { 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: void init() { mTxPending = false; @@ -426,7 +480,11 @@ class Cmt2300a { 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; bool mTxPending; bool mInRxMode; diff --git a/src/hms/cmtHal.h b/src/hms/cmtHal.h new file mode 100644 index 00000000..d5e31ea0 --- /dev/null +++ b/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 + +#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(sdio); + mPinClk = static_cast(clk); + mPinCs = static_cast(cs); + mPinFcs = static_cast(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(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__*/ diff --git a/src/hms/esp32_3wSpi.h b/src/hms/esp32_3wSpi.h index 080e2251..20c42632 100644 --- a/src/hms/esp32_3wSpi.h +++ b/src/hms/esp32_3wSpi.h @@ -11,14 +11,6 @@ #include "driver/spi_master.h" #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_PARAM_LOCK() \ @@ -31,19 +23,18 @@ // it is simply the first externally usable hardware SPI master controller #define SPI_CMT SPI2_HOST -template //, uint8_t GPIO3_PIN=15> class esp32_3wSpi { public: esp32_3wSpi() { 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(); spi_bus_config_t buscfg = { - .mosi_io_num = MOSI_PIN, + .mosi_io_num = pinSdio, .miso_io_num = -1, // single wire MOSI/MISO - .sclk_io_num = CLK_PIN, + .sclk_io_num = pinSclk, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 32, @@ -83,7 +74,7 @@ class esp32_3wSpi { }; 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); //pinMode(pinGpio3, INPUT); @@ -150,7 +141,7 @@ class esp32_3wSpi { SPI_PARAM_UNLOCK(); } - void readFifo(uint8_t buf[], uint8_t len) { + void readFifo(uint8_t buf[], uint8_t *len, uint8_t maxlen) { if(!mInitialized) return; uint8_t rx_data; @@ -162,13 +153,16 @@ class esp32_3wSpi { .rx_buffer = &rx_data }; - SPI_PARAM_LOCK(); - for(uint8_t i = 0; i < len; i++) { + SPI_PARAM_LOCK(); + for(uint8_t i = 0; i < maxlen; i++) { ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t)); 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(); } private: diff --git a/src/hms/hmsPayload.h b/src/hms/hmsPayload.h deleted file mode 100644 index c167fd43..00000000 --- a/src/hms/hmsPayload.h +++ /dev/null @@ -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 - -#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 *)> payloadListenerType; -typedef std::function *)> alarmListenerType; - - -template -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(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(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__*/ diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h index dc79afcb..d2779012 100644 --- a/src/hms/hmsRadio.h +++ b/src/hms/hmsRadio.h @@ -6,67 +6,45 @@ #ifndef __HMS_RADIO_H__ #define __HMS_RADIO_H__ -#include "../utils/dbg.h" #include "cmt2300a.h" +#include "../hm/radio.h" -typedef struct { - int8_t rssi; - uint8_t data[28]; -} 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 CmtRadio { - typedef SPI SpiType; - typedef Cmt2300a CmtType; +template +class CmtRadio : public Radio { + typedef Cmt2300a CmtType; public: CmtRadio() { - mDtuSn = DTU_SN; + mDtuSn = DTU_SN; + mCmtAvail = false; } - void setup(uint8_t pinCsb, uint8_t pinFcsb, bool genDtuSn = true) { - mCmt.setup(pinCsb, pinFcsb); + 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(pinSclk, pinSdio, pinCsb, pinFcsb); reset(genDtuSn); + mPrivacyMode = privacyMode; + mSerialDebug = serialDebug; + mPrintWholeTrace = printWholeTrace; } - void setup(bool genDtuSn = true) { - mCmt.setup(); - reset(genDtuSn); - } - - bool loop() { + void loop() { mCmt.loop(); - if((!mIrqRcvd) && (!mRqstGetRx)) - return false; + return; getRx(); if(CMT_SUCCESS == mCmt.goRx()) { mIrqRcvd = false; mRqstGetRx = false; - return true; - } else - return false; - } - - void tickSecond() { - } - - void handleIntr(void) { - mIrqRcvd = true; + } } - void enableDebug() { - mSerialDebug = true; + bool isChipConnected(void) { + return mCmtAvail; } - void sendControlPacket(const uint64_t *ivId, uint8_t cmd, uint16_t *data, bool isRetransmit) { - DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); + void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) { + DPRINT(DBG_INFO, F("sendControlPacket cmd: ")); DBGHEXLN(cmd); - initPacket(ivId, TX_REQ_DEVCONTROL, SINGLE_FRAME); + initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME); uint8_t cnt = 10; 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 } - 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 toCh = mCmt.freq2Chan(tokHz); + return switchFrequencyCh(iv, fromCh, toCh); + } + + bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) { if((0xff == fromCh) || (0xff == toCh)) return false; mCmt.switchChannel(fromCh); - sendSwitchChCmd(ivId, toCh); + sendSwitchChCmd(iv, toCh); mCmt.switchChannel(toCh); 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. - 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); - } + private: - void sendPacket(uint8_t len, bool isRetransmit) { - if (len > 14) { - uint16_t crc = ah::crc16(&mTxBuf[10], len - 10); - mTxBuf[len++] = (crc >> 8) & 0xff; - mTxBuf[len++] = (crc ) & 0xff; - } - mTxBuf[len] = ah::crc8(mTxBuf, len); - len++; + void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) { + // inverters have maybe different settings regarding frequency + if(mCmt.getCurrentChannel() != iv->config->frequency) + mCmt.switchChannel(iv->config->frequency); - if(mSerialDebug) { - DPRINT(DBG_INFO, F("TX ")); + updateCrcs(&len, appendCrc16); + + if(*mSerialDebug) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINT(F("TX ")); DBGPRINT(String(mCmt.getFreqKhz()/1000.0f)); DBGPRINT(F("Mhz | ")); - ah::dumpBuf(mTxBuf, len); + if(*mPrintWholeTrace) { + if(*mPrivacyMode) + ah::dumpBuf(mTxBuf, len, 1, 4); + else + 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); + mMillis = millis(); if(CMT_SUCCESS != status) { DPRINT(DBG_WARN, F("CMT TX failed, code: ")); DBGPRINTLN(String(status)); if(CMT_ERR_RX_IN_FIFO == status) mIrqRcvd = true; } + iv->mDtuTxCnt++; + } - if(isRetransmit) - mRetransmits++; - else - mSendCnt++; + uint64_t getIvId(Inverter<> *iv) { + return iv->radioId.u64; } - uint32_t mSendCnt; - uint32_t mRetransmits; - std::queue mBufCtrl; + uint8_t getIvGen(Inverter<> *iv) { + return iv->ivGen; + } - private: inline void reset(bool genDtuSn) { if(genDtuSn) generateDtuSn(); - if(!mCmt.reset()) + if(!mCmt.reset()) { + mCmtAvail = false; DPRINTLN(DBG_WARN, F("Initializing CMT2300A failed!")); - else + } else { + mCmtAvail = true; mCmt.goRx(); + } - mSendCnt = 0; - mRetransmits = 0; - mSerialDebug = false; mIrqRcvd = false; mRqstGetRx = false; } - inline void sendSwitchChCmd(const uint64_t *ivId, uint8_t ch) { + inline void sendSwitchChCmd(Inverter<> *iv, uint8_t ch) { /** ch: * 0x00: 860.00 MHz * 0x01: 860.25 MHz @@ -165,49 +148,27 @@ class CmtRadio { * ... * 0x28: 870.00 MHz * */ - initPacket(ivId, 0x56, 0x02); + initPacket(iv->radioId.u64, 0x56, 0x02); mTxBuf[10] = 0x15; mTxBuf[11] = 0x21; mTxBuf[12] = ch; mTxBuf[13] = 0x14; - sendPacket(14, false); + sendPacket(iv, 14, false); 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) { - hmsPacket_t p; - uint8_t status = mCmt.getRx(p.data, 28, &p.rssi); + packet_t p; + p.millis = millis() - mMillis; + uint8_t status = mCmt.getRx(p.packet, &p.len, 28, &p.rssi); if(CMT_SUCCESS == status) mBufCtrl.push(p); } CmtType mCmt; - uint32_t mDtuSn; - uint8_t mTxBuf[27]; - bool mSerialDebug; - bool mIrqRcvd; bool mRqstGetRx; + bool mCmtAvail; + uint32_t mMillis; }; #endif /*__HMS_RADIO_H__*/ diff --git a/src/platformio.ini b/src/platformio.ini index 28c47783..2b4b25cf 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -20,168 +20,220 @@ monitor_speed = 115200 extra_scripts = pre:../scripts/auto_firmware_version.py - pre:web/html/convert.py + pre:../scripts/convertHtml.py pre:../scripts/applyPatches.py lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer - nrf24/RF24 @ 1.4.7 + nrf24/RF24 @ 1.4.8 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 https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.34.17 - zinggjm/GxEPD2 @ ^1.5.2 + olikraus/U8g2 @ ^2.35.7 + 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 board = esp12e board_build.f_cpu = 80000000L -build_flags = - -D RELEASE - -std=gnu++17 +build_flags = ${env.build_flags} -DEMC_MIN_FREE_MEMORY=4096 ;-Wl,-Map,output.map monitor_filters = esp8266_exception_decoder -[env:esp8266-release-prometheus] +[env:esp8266-prometheus] platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L -build_flags = - -D RELEASE - -std=gnu++17 +build_flags = ${env.build_flags} -DENABLE_PROMETHEUS_EP -DEMC_MIN_FREE_MEMORY=4096 monitor_filters = esp8266_exception_decoder -[env:esp8266-debug] -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] +[env:esp8285] platform = espressif8266 board = esp8285 board_build.ldscript = eagle.flash.1m64.ld board_build.f_cpu = 80000000L -build_flags = - -DDEBUG_LEVEL=DBG_DEBUG +build_flags = ${env.build_flags} -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 = - time ; Add timestamp with milliseconds for each new line - log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory + esp8266_exception_decoder -[env:esp32-wroom32-release] -platform = espressif32@6.1.0 +[env:esp32-wroom32] +platform = espressif32@6.4.0 board = lolin_d32 -build_flags = - -D RELEASE - -std=gnu++17 -build_unflags = -std=gnu++11 +build_flags = ${env.build_flags} + -DUSE_HSPI_FOR_EPD monitor_filters = esp32_exception_decoder -[env:esp32-wroom32-release-prometheus] -platform = espressif32@6.1.0 +[env:esp32-wroom32-prometheus] +platform = espressif32@6.4.0 board = lolin_d32 -build_flags = - -D RELEASE - -std=gnu++17 +build_flags = ${env.build_flags} + -DUSE_HSPI_FOR_EPD -DENABLE_PROMETHEUS_EP -build_unflags = -std=gnu++11 monitor_filters = esp32_exception_decoder -[env:esp32-wroom32-debug] -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] +[env:esp32-wroom32-ethernet] platform = espressif32 board = esp32dev lib_deps = khoih-prog/AsyncWebServer_ESP32_W5500 khoih-prog/AsyncUDP_ESP32_W5500 - nrf24/RF24 @ ^1.4.7 + nrf24/RF24 @ ^1.4.8 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 https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.34.17 + olikraus/U8g2 @ ^2.35.7 zinggjm/GxEPD2 @ ^1.5.2 -build_flags = +build_flags = ${env.build_flags} -D ETHERNET -DRELEASE + -DUSE_HSPI_FOR_EPD -DLOG_LOCAL_LEVEL=ESP_LOG_INFO -DDEBUG_LEVEL=DBG_INFO - -std=gnu++17 -build_unflags = -std=gnu++11 monitor_filters = esp32_exception_decoder -[env:opendtufusionv1-release] -platform = espressif32@6.1.0 +[env:esp32-s2-mini] +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 upload_protocol = esp-builtin -debug_tool = esp-builtin -debug_speed = 12000 -build_flags = - -D RELEASE - -std=gnu++17 -build_unflags = -std=gnu++11 +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 +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 = - time ; Add timestamp with milliseconds for each new line + esp32_exception_decoder, colorize diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 9c2dd3d7..467f7d48 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -5,6 +5,7 @@ #include #include "../../hm/hmSystem.h" +#include "../../hm/hmRadio.h" #include "../../utils/helper.h" #include "Display_Mono.h" #include "Display_Mono_128X32.h" @@ -12,8 +13,10 @@ #include "Display_Mono_84X48.h" #include "Display_Mono_64X48.h" #include "Display_ePaper.h" +#include "Display_data.h" -template + +template class Display { public: @@ -21,37 +24,52 @@ class Display { 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; mSys = sys; mUtcTs = utcTs; mNewPayload = false; mLoopCnt = 0; - mVersion = version; + + mDisplayData.version = app->getVersion(); // version never changes, so only set once switch (mCfg->type) { - case 0: mMono = NULL; break; - case 1: // fall-through - case 2: mMono = new DisplayMono128X64(); break; - case 3: mMono = new DisplayMono84X48(); break; - case 4: mMono = new DisplayMono128X32(); break; - case 5: mMono = new DisplayMono64X48(); break; - -#if defined(ESP32) + case 0: mMono = NULL; break; // None + case 1: mMono = new DisplayMono128X64(); break; // SSD1306_128X64 (0.96", 1.54") + case 2: mMono = new DisplayMono128X64(); break; // SH1106_128X64 (1.3") + case 3: mMono = new DisplayMono84X48(); break; // PCD8544_84X48 (1.6" - Nokia 5110) + case 4: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91") + 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) && !defined(ETHERNET) case 10: mMono = NULL; // ePaper does not use this mRefreshCycle = 0; 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; #endif default: mMono = NULL; break; } if(mMono) { - mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); - mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + 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, &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) { @@ -60,56 +78,104 @@ class Display { void tickerSecond() { if (mMono != NULL) - mMono->loop(); + mMono->loop(mCfg->contrast, motionSensorActive()); - if (mNewPayload || ((++mLoopCnt % 10) == 0)) { + if (mNewPayload || (((++mLoopCnt) % 5) == 0)) { + DataScreen(); mNewPayload = false; mLoopCnt = 0; - DataScreen(); } + #if defined(ESP32) && !defined(ETHERNET) + mEpaper.tickerSecond(); + #endif } - private: + private: void DataScreen() { if (mCfg->type == 0) return; - if (*mUtcTs == 0) - return; - float totalPower = 0; - float totalYieldDay = 0; - float totalYieldTotal = 0; + float totalPower = 0.0; + float totalYieldDay = 0.0; + float totalYieldTotal = 0.0; - uint8_t isprod = 0; + uint8_t nrprod = 0; + uint8_t nrsleep = 0; + int8_t minQAllInv = 4; Inverter<> *iv; 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); - rec = iv->getRecordStruct(RealTimeRunData_Debug); if (iv == NULL) continue; - if (iv->isProducing()) - isprod++; + if (iv->isProducing()) // also updates inverter state engine + 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); 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 ) { - mMono->disp(totalPower, totalYieldDay, totalYieldTotal, isprod); + mMono->disp(); } -#if defined(ESP32) +#if defined(ESP32) && !defined(ETHERNET) else if (mCfg->type == 10) { - - mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); + mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod); mRefreshCycle++; } -#endif -#if defined(ESP32) if (mRefreshCycle > 480) { mEpaper.fullRefresh(); mRefreshCycle = 0; @@ -117,16 +183,54 @@ class Display { #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 + IApp *mApp; + DisplayData mDisplayData; bool mNewPayload; uint8_t mLoopCnt; uint32_t *mUtcTs; - const char *mVersion; display_t *mCfg; HMSYSTEM *mSys; + RADIO *mHmRadio; + RADIO *mHmsRadio; uint16_t mRefreshCycle; -#if defined(ESP32) +#if defined(ESP32) && !defined(ETHERNET) DisplayEPaper mEpaper; #endif DisplayMono *mMono; diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index cbaea75b..e9e0d416 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -10,6 +10,7 @@ #define DISP_FMT_TEXT_LEN 32 #define BOTTOM_MARGIN 5 +#include "defines.h" #ifdef ESP8266 #include @@ -17,33 +18,68 @@ #include #endif #include "../../utils/helper.h" +#include "Display_data.h" +#include "../../utils/dbg.h" +#include "../../utils/timemonitor.h" class DisplayMono { public: 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 config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) = 0; - virtual void loop(void) = 0; - virtual void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) = 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 enPowerSave, uint8_t screenSaver, uint8_t lum) = 0; + virtual void disp(void) = 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: U8G2* mDisplay; + DisplayData *mDisplayData; 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 mLoopCnt; - uint32_t* mUtcTs; uint8_t mLineXOffsets[5] = {}; uint8_t mLineYOffsets[5] = {}; - uint16_t mDispY; - uint8_t mExtra; - uint16_t mTimeout; - char mFmtText[DISP_FMT_TEXT_LEN]; #ifdef DISPLAY_CHART #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 void drawPowerChart(); #endif -}; \ No newline at end of file + + 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"; diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h index e9e09d28..fa0cacdf 100644 --- a/src/plugins/Display/Display_Mono_128X32.h +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -9,95 +9,65 @@ class DisplayMono128X32 : public DisplayMono { public: DisplayMono128X32() : DisplayMono() { - mEnPowerSafe = true; - mEnScreenSaver = true; - mLuminance = 60; 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); - mType = type; - mDisplay = new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data); - - mUtcTs = utcTs; - - mDisplay->begin(); - + monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data), type, displayData); calcLinePositions(); - - mDisplay->clearBuffer(); - mDisplay->setContrast(mLuminance); - printText("AHOY!", 0); + printText("Ahoy!", 0); printText("ahoydtu.de", 2); - printText(version, 3); + printText(mDisplayData->version, 3); mDisplay->sendBuffer(); } - void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { - 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) { + void disp(void) { mDisplay->clearBuffer(); - // set Contrast of the Display to raise the lifetime - if (3 != mType) - mDisplay->setContrast(mLuminance); + // calculate current pixelshift for pixelshift screensaver + calcPixelShift(pixelShiftRange); - if ((totalPower > 0) && (isprod > 0)) { - mTimeout = DISP_DEFAULT_TIMEOUT; - mDisplay->setPowerSave(false); - if (totalPower > 999) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + if ((mDisplayData->totalPower > 0) && (mDisplayData->nrProducing > 0)) { + if (mDisplayData->totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (mDisplayData->totalPower / 1000)); 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); } else { 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); - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", mDisplayData->totalYieldTotal); printText(mFmtText, 2); IPAddress ip = WiFi.localIP(); if (!(mExtra % 10) && (ip)) printText(ip.toString().c_str(), 3); 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); - } else if (NULL != mUtcTs) - printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + } else if (0 != mDisplayData->utcTs) + printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3); mDisplay->sendBuffer(); - mDispY = 0; mExtra++; } private: + const uint8_t pixelShiftRange = 7; // number of pixels to shift from left to right (centered -> must be odd!) + void calcLinePositions() { uint8_t yOff[] = {0, 0}; for (uint8_t i = 0; i < 4; i++) { @@ -114,13 +84,13 @@ class DisplayMono128X32 : public DisplayMono { inline void setFont(uint8_t line) { switch (line) { case 0: - mDisplay->setFont(u8g2_font_9x15_tf); + mDisplay->setFont(u8g2_font_9x15_tr); break; case 3: - mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); + mDisplay->setFont(u8g2_font_tom_thumb_4x6_tr); break; default: - mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); + mDisplay->setFont(u8g2_font_tom_thumb_4x6_tr); break; } } @@ -139,7 +109,7 @@ class DisplayMono128X32 : public DisplayMono { void printText(const char *text, uint8_t line) { setFont(line); - uint8_t dispX = mLineXOffsets[line] + ((mEnScreenSaver) ? (mExtra % 7) : 0); + uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift; if (isTwoRowLine(line)) { String stringText = String(text); diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index 60bf835b..22310b82 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -17,55 +17,44 @@ class DisplayMono128X64 : public DisplayMono { mUtcTs = NULL; mType = 0; #ifdef DISPLAY_CHART - for (int i = 0; i < DISP_WATT_ARR_LENGTH; i++) - m_wattArr[i] = 0.0; - m_wattListIdx = 0; - m_wattDispIdx = 1; + for (int i = 0; i < DISP_WATT_ARR_LENGTH; i++) + m_wattArr[i] = 0.0; + m_wattListIdx = 0; + m_wattDispIdx = 1; #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); - mType = type; - switch (type) { 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; - default: 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; } - - mUtcTs = utcTs; - - mDisplay->begin(); calcLinePositions(); - mDisplay->clearBuffer(); - mDisplay->setContrast(mLuminance); - printText("AHOY!", 0, 35); - printText("ahoydtu.de", 2, 20); - printText(version, 3, 46); + printText("Ahoy!", l_Ahoy, 0xff); + printText("ahoydtu.de", l_Website, 0xff); + printText(mDisplayData->version, l_Version, 0xff); mDisplay->sendBuffer(); } - void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { - 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) { + void disp(void) { + uint8_t pos, sun_pos, moon_pos; #ifdef DISPLAY_CHART bool drawChart = true; // should come from settings static unsigned int dbgCnt = 0; @@ -76,28 +65,35 @@ class DisplayMono128X64 : public DisplayMono { #endif mDisplay->clearBuffer(); - // set Contrast of the Display to raise the lifetime - mDisplay->setContrast(mLuminance); + // Layout-Test + /* + 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)) { - mTimeout = DISP_DEFAULT_TIMEOUT; - mDisplay->setPowerSave(false); + // calculate current pixelshift for pixelshift screensaver + calcPixelShift(pixelShiftRange); - if (totalPower > 999) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + // 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, "%3.0f W", totalPower); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - printText(mFmtText, 0); + printText(mFmtText, l_TotalPower, 0xff); } else { - printText("offline", 0, 25); - // check if it's time to enter power saving mode - if (mTimeout == 0) - mDisplay->setPowerSave(mEnPowerSafe); + printText("offline", l_TotalPower, 0xff); } - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); - printText(mFmtText, 1); + #ifdef DISPLAY_CHART if (drawChart) @@ -123,48 +119,169 @@ class DisplayMono128X64 : public DisplayMono { } else if (NULL != mUtcTs) printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); #endif - } - mDisplay->sendBuffer(); - mDispY = 0; - mExtra++; + // 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(); + + mDispY = 0; + mExtra++; } 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() { uint8_t yOff = 0; - for (uint8_t i = 0; i < 4; i++) { - setFont(i); - yOff += (mDisplay->getMaxCharHeight()); + uint8_t i = 0; + uint8_t asc, dsc; + + do { + setLineFont(i); + asc = mDisplay->getAscent(); + yOff += asc; 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) { - case 0: + + inline void setLineFont(uint8_t line) { + if ((line == l_TotalPower) || + (line == l_Ahoy)) #ifdef DISPLAY_CHART - mDisplay->setFont(u8g2_font_6x12_tr); + mDisplay->setFont(u8g2_font_6x12_tr); #else - mDisplay->setFont(u8g2_font_ncenB14_tr); + mDisplay->setFont(u8g2_font_ncenB14_tr); #endif - break; - case 3: - mDisplay->setFont(u8g2_font_5x8_tr); - break; - default: + else if ((line == l_YieldDay) || + (line == l_YieldTotal)) + // mDisplay->setFont(u8g2_font_5x8_symbols_ahoy); #ifdef DISPLAY_CHART - mDisplay->setFont(u8g2_font_5x8_tr); + mDisplay->setFont(u8g2_font_5x8_tr); #else - mDisplay->setFont(u8g2_font_ncenB10_tr); + mDisplay->setFont(u8g2_font_ncenB10_tr); #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); } diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h index 8c355322..68cac96f 100644 --- a/src/plugins/Display/Display_Mono_64X48.h +++ b/src/plugins/Display/Display_Mono_64X48.h @@ -9,88 +9,58 @@ class DisplayMono64X48 : public DisplayMono { public: DisplayMono64X48() : DisplayMono() { - mEnPowerSafe = true; - mEnScreenSaver = false; - mLuminance = 20; 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); - mType = type; - // 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); - - mUtcTs = utcTs; + monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, reset, clock, data), type, displayData); - mDisplay->begin(); calcLinePositions(); - - mDisplay->clearBuffer(); - mDisplay->setContrast(mLuminance); - - printText("AHOY!", 0); + printText("Ahoy!", 0); printText("ahoydtu.de", 1); - printText(version, 2); + printText(mDisplayData->version, 2); mDisplay->sendBuffer(); } - void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { - 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) { + void disp(void) { mDisplay->clearBuffer(); - // set Contrast of the Display to raise the lifetime - mDisplay->setContrast(mLuminance); - - if ((totalPower > 0) && (isprod > 0)) { - mTimeout = DISP_DEFAULT_TIMEOUT; - mDisplay->setPowerSave(false); + // calculate current pixelshift for pixelshift screensaver + calcPixelShift(pixelShiftRange); - if (totalPower > 999) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + if ((mDisplayData->totalPower > 0) && (mDisplayData->nrProducing > 0)) { + if (mDisplayData->totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (mDisplayData->totalPower / 1000)); 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); } else { 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); - 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); IPAddress ip = WiFi.localIP(); if (!(mExtra % 10) && (ip)) printText(ip.toString().c_str(), 3); 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); - } else if (NULL != mUtcTs) - printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + } else if (0 != mDisplayData->utcTs) + printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3); mDisplay->sendBuffer(); @@ -98,6 +68,8 @@ class DisplayMono64X48 : public DisplayMono { } private: + const uint8_t pixelShiftRange = 4; // number of pixels to shift from left to right + void calcLinePositions() { uint8_t yOff = 0; for (uint8_t i = 0; i < 4; i++) { @@ -110,11 +82,11 @@ class DisplayMono64X48 : public DisplayMono { inline void setFont(uint8_t line) { switch (line) { case 0: - mDisplay->setFont(u8g2_font_fur11_tf); + mDisplay->setFont(u8g2_font_fur11_tr); break; case 1: case 2: - mDisplay->setFont(u8g2_font_6x10_tf); + mDisplay->setFont(u8g2_font_6x10_tr); break; case 3: mDisplay->setFont(u8g2_font_4x6_tr); @@ -126,8 +98,8 @@ class DisplayMono64X48 : public DisplayMono { } void printText(const char *text, uint8_t line) { - uint8_t dispX = 0; //small display, use all we have - dispX += (mEnScreenSaver) ? (mExtra % 4) : 0; + uint8_t dispX = mLineXOffsets[line] + pixelShiftRange/2 + mPixelshift; + setFont(line); mDisplay->drawStr(dispX, mLineYOffsets[line], text); } diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index aefe1372..7e5c157f 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -5,93 +5,116 @@ #pragma once #include "Display_Mono.h" +#include "../../utils/dbg.h" class DisplayMono84X48 : public DisplayMono { public: DisplayMono84X48() : DisplayMono() { - mEnPowerSafe = true; - mEnScreenSaver = true; - mLuminance = 60; 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); - mType = type; - mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); - - mUtcTs = utcTs; - - mDisplay->begin(); - mDispWidth = mDisplay->getDisplayWidth(); + monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset), type, displayData); calcLinePositions(); - - mDisplay->clearBuffer(); - mDisplay->setContrast(mLuminance); - - printText("AHOY!", l_Ahoy); - printText("ahoydtu.de", l_Website); - printText(version, l_Version); + printText("Ahoy!", l_Ahoy, 0xff); + printText("ahoydtu.de", l_Website, 0xff); + printText(mDisplayData->version, l_Version, 0xff); mDisplay->sendBuffer(); } - void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { - 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) { + void disp(void) { mDisplay->clearBuffer(); - // 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) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (totalPower / 1000)); + // Layout-Test + /* + mDisplayData->nrSleeping = 10; + 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", totalPower); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); - printText(mFmtText, l_TotalPower); + printText(mFmtText, l_TotalPower, 0xff); } else { - printText("offline", l_TotalPower); - // check if it's time to enter power saving mode - if (mTimeout == 0) - mDisplay->setPowerSave(mEnPowerSafe); + printText("offline", l_TotalPower, 0xff); } - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "Today: %4.0f Wh", totalYieldDay); - printText(mFmtText, l_YieldDay); + // print Date and time + if (0 != mDisplayData->utcTs) + printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff); - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "Total: %.1f kWh", totalYieldTotal); - printText(mFmtText, l_YieldTotal); + // 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); + } - if (NULL != mUtcTs) - printText(ah::getDateTimeStrShort(gTimezone.toLocal(*mUtcTs)).c_str(), l_Time); + // print yields + printText("\x88", l_YieldDay, 10); // day symbol + printText("\x83", l_YieldTotal, 10); // total symbol - IPAddress ip = WiFi.localIP(); - if (!(mExtra % 5) && (ip)) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", ip.toString().c_str()); + if (mDisplayData->totalYieldDay > 9999.0) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0); else - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "Inv.On: %d", isprod); - printText(mFmtText, l_Status); + 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 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); + } + + // draw dynamic antenna and WiFi symbols + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%c", mDisplayData->RadioSymbol?'\x80':'\x84'); // NRF + printText(mFmtText, l_RSSI); + if (mDisplayData->MQTTSymbol) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x89"); // MQTT + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%c", mDisplayData->WifiSymbol?'\x81':'\x85'); // Wifi connected + printText(mFmtText, l_RSSI, mDispWidth - 6); mDisplay->sendBuffer(); @@ -99,7 +122,6 @@ class DisplayMono84X48 : public DisplayMono { } private: - uint16_t mDispWidth; enum _dispLine { // start page l_Website = 0, @@ -111,6 +133,8 @@ class DisplayMono84X48 : public DisplayMono { l_TotalPower = 2, l_YieldDay = 3, l_YieldTotal = 4, + // run page - rssi bar symbols + l_RSSI = 4, // End l_MAX_LINES = 5, }; @@ -121,7 +145,7 @@ class DisplayMono84X48 : public DisplayMono { uint8_t asc, dsc; do { - setFont(i); + setLineFont(i); asc = mDisplay->getAscent(); yOff += asc; mLineYOffsets[i] = yOff; @@ -133,18 +157,23 @@ class DisplayMono84X48 : public DisplayMono { } while(l_MAX_LINES>i); } - inline void setFont(uint8_t line) { + inline void setLineFont(uint8_t line) { if ((line == l_TotalPower) || (line == l_Ahoy)) mDisplay->setFont(u8g2_font_logisoso16_tr); 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; - setFont(line); - dispX = (mDispWidth - mDisplay->getStrWidth(text)) / 2; // center text - dispX += (mEnScreenSaver) ? (mExtra % 7) : 0; + + setLineFont(line); + if (0xff == col) + dispX = (mDispWidth - mDisplay->getStrWidth(text)) / 2; // center text + else + dispX = col; mDisplay->drawStr(dispX, mLineYOffsets[line], text); } }; + + diff --git a/src/plugins/Display/Display_data.h b/src/plugins/Display/Display_data.h new file mode 100644 index 00000000..a400377d --- /dev/null +++ b/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__*/ diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp index 74000180..d12da365 100644 --- a/src/plugins/Display/Display_ePaper.cpp +++ b/src/plugins/Display/Display_ePaper.cpp @@ -1,9 +1,9 @@ #include "Display_ePaper.h" #ifdef ESP8266 - #include +#include #elif defined(ESP32) - #include +#include #endif #include "../../utils/helper.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) { mUtcTs = utcTs; + mRefreshState = RefreshStatus::LOGO; + mSecondCnt = 0; + if (type == 10) { Serial.begin(115200); _display = new GxEPD2_BW(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); - hspi.begin(_SCK, _BUSY, _MOSI, _CS); #if defined(ESP32) && defined(USE_HSPI_FOR_EPD) + hspi.begin(_SCK, _BUSY, _MOSI, _CS); _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0)); +#elif defined(ESP32) + _display->epd2.init(_SCK, _MOSI, 115200, true, 20, false); #endif - _display->init(115200, true, 2, false); + _display->init(115200, true, 20, false); _display->setRotation(mDisplayRotation); _display->setFullWindow(); - - // Logo - _display->fillScreen(GxEPD_BLACK); - _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); - while (_display->nextPage()) - ; - - // clean the screen - delay(2000); - _display->fillScreen(GxEPD_WHITE); - while (_display->nextPage()) - ; - - headlineIP(); - - // call the PowerPage to change the PV Power Values - actualPowerPaged(0, 0, 0, 0); + _version = version; } } -void DisplayEPaper::config(uint8_t rotation, bool enPowerSafe) { +void DisplayEPaper::config(uint8_t rotation, bool enPowerSave) { mDisplayRotation = rotation; - mEnPowerSafe = enPowerSafe; + mEnPowerSave = enPowerSave; } //*************************************************************************** void DisplayEPaper::fullRefresh() { - // screen complete black - _display->fillScreen(GxEPD_BLACK); - while (_display->nextPage()) - ; - delay(2000); - // screen complete white - _display->fillScreen(GxEPD_WHITE); - while (_display->nextPage()) - ; + if(RefreshStatus::DONE != mRefreshState) + return; + mSecondCnt = 2; + mRefreshState = RefreshStatus::BLACK; +} + +//*************************************************************************** +void DisplayEPaper::refreshLoop() { + switch(mRefreshState) { + case RefreshStatus::LOGO: + _display->fillScreen(GxEPD_BLACK); + _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); + mNextRefreshState = RefreshStatus::PARTITIALS; + mRefreshState = RefreshStatus::WAIT; + 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); + 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() { @@ -121,6 +146,26 @@ void DisplayEPaper::lastUpdatePaged() { } 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() { int16_t tbx, tby; 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->fillScreen(GxEPD_WHITE); + do { + // actual Production if (totalPower > 9999) { - snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 10000)); + snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 1000)); _changed = true; } else if ((totalPower > 0) && (totalPower <= 9999)) { snprintf(_fmtText, sizeof(_fmtText), "%.0f W", totalPower); _changed = true; - } else { + } else snprintf(_fmtText, sizeof(_fmtText), "offline"); - } - if (totalPower == 0){ - _display->fillRect(0, mHeadFootPadding, 200,200, GxEPD_BLACK); + + if ((totalPower == 0) && (mEnPowerSave)) { + _display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK); _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); } else { _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->print(_fmtText); + 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; + _display->setCursor(5, y); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + _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->print(_fmtText); + _display->setCursor(_display->width() - ((kwh) ? 50 : 38), y); + _display->setFont(&FreeSans12pt7b); + _display->println((kwh) ? "kWh" : "Wh"); + y = y + tbh + 15; + + + // 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->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + _display->drawInvertedBitmap(5, y - tbh, mySigma, 30, 30, GxEPD_BLACK); + x = ((_display->width() - tbw - 20) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - ((mwh) ? 59 : 50), y); + _display->setFont(&FreeSans12pt7b); + _display->println((mwh) ? "MWh" : "kWh"); + } + + // Inverter online _display->setFont(&FreeSans12pt7b); - y = _display->height() / 2; - _display->setCursor(5, y); - _display->print("today:"); - snprintf(_fmtText, _display->width(), "%.0f", totalYieldDay); + y = _display->height() - (mHeadFootPadding + 10); + snprintf(_fmtText, sizeof(_fmtText), " %d online", isprod); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); - x = ((_display->width() - tbw) / 2) - tbx; - _display->setCursor(x, y); - _display->print(_fmtText); - _display->setCursor(_display->width() - 38, y); - _display->println("Wh"); - - y = y + tbh + 7; - _display->setCursor(5, y); - _display->print("total:"); - snprintf(_fmtText, _display->width(), "%.1f", totalYieldTotal); - _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); - x = ((_display->width() - tbw) / 2) - tbx; + _display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK); + x = ((_display->width() - tbw - 20) / 2) - tbx; _display->setCursor(x, y); - _display->print(_fmtText); - _display->setCursor(_display->width() - 50, y); - _display->println("kWh"); - - _display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); - snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", isprod); _display->println(_fmtText); - } + yield(); } while (_display->nextPage()); } //*************************************************************************** void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + if(RefreshStatus::DONE != mRefreshState) + return; + // check if the IP has changed - if (_settedIP != WiFi.localIP().toString().c_str()) { - // save the new IP and call the Headline Funktion to adapt the Headline - _settedIP = WiFi.localIP().toString().c_str(); + if (_settedIP != WiFi.localIP().toString()) { + // save the new IP and call the Headline Function to adapt the Headline + _settedIP = WiFi.localIP().toString(); headlineIP(); } @@ -217,10 +291,16 @@ void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYield if ((isprod > 0) && (_changed)) { _changed = false; lastUpdatePaged(); - } else if((0 == totalPower) && (mEnPowerSafe)) + } else if ((0 == totalPower) && (mEnPowerSave)) offlineFooter(); _display->powerOff(); } + //*************************************************************************** -#endif // ESP32 +void DisplayEPaper::tickerSecond() { + if(mSecondCnt != 0) + mSecondCnt--; + refreshLoop(); +} +#endif // ESP32 diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h index ad422b26..c9a0fbf5 100644 --- a/src/plugins/Display/Display_ePaper.h +++ b/src/plugins/Display/Display_ePaper.h @@ -3,9 +3,6 @@ #if defined(ESP32) -// uncomment next line to use HSPI for EPD (and VSPI for SD), e.g. with Waveshare ESP32 Driver Board -#define USE_HSPI_FOR_EPD - /// uncomment next line to use class GFX of library GFX_Root instead of Adafruit_GFX, to use less code and ram // #include // base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code @@ -27,28 +24,42 @@ // GDEH0154D67 1.54" b/w 200x200 class DisplayEPaper { - public: - DisplayEPaper(); - void fullRefresh(); - void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char* version); - void config(uint8_t rotation, bool enPowerSafe); - void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); - - - private: - void headlineIP(); - void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); - void lastUpdatePaged(); - void offlineFooter(); - - uint8_t mDisplayRotation; - bool _changed = false; - char _fmtText[35]; - const char* _settedIP; - uint8_t mHeadFootPadding; - GxEPD2_GFX* _display; - uint32_t *mUtcTs; - bool mEnPowerSafe; + public: + DisplayEPaper(); + void fullRefresh(); + void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t* utcTs, const char* version); + void config(uint8_t rotation, bool enPowerSave); + void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); + void refreshLoop(); + void tickerSecond(); + + private: + void headlineIP(); + void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); + void lastUpdatePaged(); + void offlineFooter(); + void versionFooter(); + + enum class RefreshStatus : uint8_t { + DONE, + BLACK, + WHITE, + WAIT, + PARTITIALS, + LOGO + }; + + uint8_t mDisplayRotation; + bool _changed = false; + char _fmtText[35]; + String _settedIP; + uint8_t mHeadFootPadding; + GxEPD2_GFX* _display; + uint32_t* mUtcTs; + bool mEnPowerSave; + const char* _version; + RefreshStatus mRefreshState, mNextRefreshState; + uint8_t mSecondCnt; }; -#endif // ESP32 +#endif // ESP32 diff --git a/src/plugins/Display/imagedata.h b/src/plugins/Display/imagedata.h index baaddec8..b136193b 100644 --- a/src/plugins/Display/imagedata.h +++ b/src/plugins/Display/imagedata.h @@ -9,6 +9,47 @@ #include #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 const unsigned char logo[] PROGMEM = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 095d6c19..e062439c 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -134,7 +134,7 @@ class PubMqtt { #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()) return false; @@ -142,7 +142,20 @@ class PubMqtt { publish(subtopics[MQTT_SUNSET], String(sunset).c_str(), true); publish(subtopics[MQTT_COMM_START], String(sunrise - offs).c_str(), true); publish(subtopics[MQTT_COMM_STOP], String(sunset + offs).c_str(), true); - publish(subtopics[MQTT_DIS_NIGHT_COMM], ((disNightCom) ? dict[STR_TRUE] : dict[STR_FALSE]), true); + + 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; } @@ -236,7 +249,11 @@ class PubMqtt { publish(subtopics[MQTT_VERSION], mVersion, 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); + #endif tickerMinute(); publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false); @@ -470,22 +487,22 @@ class PubMqtt { continue; // skip to next inverter // inverter status - iv->isProducing(); // recalculate status - if (InverterStatus::OFF < iv->status) + InverterStatus status = iv->getStatus(); + if (InverterStatus::OFF < status) anyAvail = true; else // inverter is enabled but not available allAvail = false; - if(mLastIvState[id] != iv->status) { + if(mLastIvState[id] != status) { // if status changed from producing to not producing send last data immediately if (InverterStatus::WAS_PRODUCING == mLastIvState[id]) sendData(iv, RealTimeRunData_Debug); - mLastIvState[id] = iv->status; + mLastIvState[id] = status; changed = true; 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); } } @@ -516,25 +533,17 @@ class PubMqtt { snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/cnt", iv->config->name); snprintf(mVal, 40, "%d", iv->alarmCnt); - publish(mSubTopic, mVal, true); + publish(mSubTopic, mVal, false); for(uint8_t j = 0; j < 10; j++) { if(0 != iv->lastAlarm[j].code) { - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/code", iv->config->name, j); - snprintf(mVal, 40, "%d", iv->lastAlarm[j].code); - publish(mSubTopic, mVal, true); - - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d/str", iv->config->name, j); - snprintf(mVal, 40, "%s", iv->getAlarmStr(iv->lastAlarm[j].code).c_str()); - publish(mSubTopic, mVal, true); - - 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); + snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d", iv->config->name, j); + snprintf(mVal, 100, "{\"code\":%d,\"str\":\"%s\",\"start\":%d,\"end\":%d}", + iv->lastAlarm[j].code, + iv->getAlarmStr(iv->lastAlarm[j].code).c_str(), + iv->lastAlarm[j].start + lastMidnight, + iv->lastAlarm[j].end + lastMidnight); + publish(mSubTopic, mVal, false); yield(); } } @@ -606,14 +615,14 @@ class PubMqtt { uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS]; 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]; const char *mDevName, *mVersion; char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT // global buffer for mqtt topic. Used when publishing mqtt messages. char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1]; char mSubTopic[32 + MAX_NAME_LENGTH + 1]; - char mVal[40]; + char mVal[100]; discovery_t mDiscovery; }; diff --git a/src/publisher/pubMqttDefs.h b/src/publisher/pubMqttDefs.h index ca8bf01e..c97daa78 100644 --- a/src/publisher/pubMqttDefs.h +++ b/src/publisher/pubMqttDefs.h @@ -48,7 +48,6 @@ enum { MQTT_SUNSET, MQTT_COMM_START, MQTT_COMM_STOP, - MQTT_DIS_NIGHT_COMM, MQTT_COMM_DISABLED, MQTT_COMM_DIS_TS, MQTT_VERSION, @@ -69,7 +68,6 @@ const char* const subtopics[] PROGMEM = { "sunset", "comm_start", "comm_stop", - "dis_night_comm", "comm_disabled", "comm_dis_ts", "version", diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 57bf5609..9a364ec9 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -28,14 +28,14 @@ class PubMqttIvData { mState = IDLE; mZeroValues = false; - memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4); + memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * sizeof(uint32_t)); mRTRDataHasBeenSent = false; - mTable[IDLE] = &PubMqttIvData::stateIdle; - mTable[START] = &PubMqttIvData::stateStart; - mTable[FIND_NXT_IV] = &PubMqttIvData::stateFindNxtIv; - mTable[SEND_DATA] = &PubMqttIvData::stateSend; - mTable[SEND_TOTALS] = &PubMqttIvData::stateSendTotals; + mTable[IDLE] = &PubMqttIvData::stateIdle; + mTable[START] = &PubMqttIvData::stateStart; + mTable[FIND_NXT_IV] = &PubMqttIvData::stateFindNxtIv; + mTable[SEND_DATA] = &PubMqttIvData::stateSend; + mTable[SEND_TOTALS] = &PubMqttIvData::stateSendTotals; } void loop() { @@ -102,9 +102,17 @@ class PubMqttIvData { mPos = 0; if(found) { record_t<> *rec = mIv->getRecordStruct(mCmd); - snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", mIv->config->name); - snprintf(mVal, 40, "%d", mIv->getLastTs(rec)); - mPublish(mSubTopic, mVal, true, QOS_0); + 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(mVal, 40, "%d", mIv->getLastTs(rec)); + 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 mState = SEND_DATA; @@ -119,6 +127,11 @@ class PubMqttIvData { void stateSend() { 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); bool pubData = (lastTs > 0); if (mCmd == RealTimeRunData_Debug) @@ -133,26 +146,28 @@ class PubMqttIvData { // calculate total values for RealTimeRunData_Debug if (CH0 == rec->assign[mPos].ch) { - if(mIv->status > InverterStatus::STARTING) { - mTotalFound = true; - switch (rec->assign[mPos].fieldId) { - case FLD_PAC: - mTotal[0] += mIv->getValue(mPos, rec); - break; - case FLD_YT: - mTotal[1] += mIv->getValue(mPos, rec); - break; - case FLD_YD: { - float val = mIv->getValue(mPos, rec); - if(0 == val) // inverter restarted during day - mSendTotalYd = false; - else - mTotal[2] += val; - break; + if(mIv->getStatus() > InverterStatus::STARTING) { + if(mIv->config->add2Total) { + mTotalFound = true; + switch (rec->assign[mPos].fieldId) { + case FLD_PAC: + mTotal[0] += mIv->getValue(mPos, rec); + break; + case FLD_YT: + mTotal[1] += mIv->getValue(mPos, rec); + break; + case FLD_YD: { + float val = mIv->getValue(mPos, rec); + if(0 == val) // inverter restarted during day + mSendTotalYd = false; + else + mTotal[2] += val; + break; + } + case FLD_PDC: + mTotal[3] += mIv->getValue(mPos, rec); + break; } - case FLD_PDC: - mTotal[3] += mIv->getValue(mPos, rec); - break; } } else mAllTotalFound = false; @@ -170,12 +185,25 @@ class PubMqttIvData { mPublish(mSubTopic, mVal, retained, qos); } mPos++; - } else + } else { + sendRadioStat(rec->length); mState = FIND_NXT_IV; + } } else 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() { uint8_t fieldId; mRTRDataHasBeenSent = true; @@ -213,7 +241,8 @@ class PubMqttIvData { } else { mSendList->pop(); mZeroValues = false; - mState = START; + mPos = 0; + mState = IDLE; } } @@ -234,7 +263,7 @@ class PubMqttIvData { bool mRTRDataHasBeenSent; 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 std::queue *mSendList; diff --git a/src/utils/dbg.h b/src/utils/dbg.h index 6c861329..9e754ba6 100644 --- a/src/utils/dbg.h +++ b/src/utils/dbg.h @@ -148,7 +148,7 @@ #define PVERBLN(str) #endif -#define DPRINT(level, str) ({\ +#define DPRINT(level, str) do {\ switch(level) {\ case DBG_ERROR: PERR(str); break; \ case DBG_WARN: PWARN(str); break; \ @@ -156,13 +156,13 @@ case DBG_DEBUG: PDBG(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(") "));\ -}) +} while (0) -#define DPRINTLN(level, str) ({\ +#define DPRINTLN(level, str) do {\ switch(level) {\ case DBG_ERROR: PERRLN(str); break; \ case DBG_WARN: PWARNLN(str); break; \ @@ -170,7 +170,7 @@ case DBG_DEBUG: PDBGLN(str); break; \ default: PVERBLN(str); break; \ }\ -}) +} while (0) /*class ahoyLog { diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp index 4050c999..9aea1054 100644 --- a/src/utils/helper.cpp +++ b/src/utils/helper.cpp @@ -70,6 +70,17 @@ namespace ah { 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) { char tmp[3]; uint64_t ret = 0ULL; @@ -86,9 +97,12 @@ namespace ah { 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++) { - DHEX(buf[i]); + if((i < firstRepl) || (i > lastRepl) || (0 == firstRepl)) + DHEX(buf[i]); + else + DBGPRINT(F(" *")); DBGPRINT(" "); } DBGPRINTLN(""); diff --git a/src/utils/helper.h b/src/utils/helper.h index e1702877..1dbba3d9 100644 --- a/src/utils/helper.h +++ b/src/utils/helper.h @@ -20,21 +20,21 @@ static Timezone gTimezone(CEST, CET); #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; \ b[0] = ((v >> 24) & 0xff); \ b[1] = ((v >> 16) & 0xff); \ b[2] = ((v >> 8) & 0xff); \ b[3] = ((v ) & 0xff); \ -}) +} while (0) -#define CP_U32_BigEndian(buf, v) ({ \ +#define CP_U32_BigEndian(buf, v) do { \ uint8_t *b = buf; \ b[3] = ((v >> 24) & 0xff); \ b[2] = ((v >> 16) & 0xff); \ b[1] = ((v >> 8) & 0xff); \ b[0] = ((v ) & 0xff); \ -}) +} while (0) namespace ah { void ip2Arr(uint8_t ip[], const char *ipStr); @@ -44,8 +44,9 @@ namespace ah { String getDateTimeStrShort(time_t t); String getDateTimeStrFile(time_t t); String getTimeStr(time_t t); + String getTimeStrMs(uint64_t t); 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__*/ diff --git a/src/utils/improv.h b/src/utils/improv.h index 23850ba9..02165239 100644 --- a/src/utils/improv.h +++ b/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) { parsePayload(type, buf, len); })) { - DBGPRINTLN(F("check paket failed")); + DBGPRINTLN(F("check packet failed")); } dumpBuf(buf, len); } @@ -100,7 +100,7 @@ class Improv { if(0 != strncmp((char*)buf, "IMPROV", 6)) return false; - // verison check (only version 1 is supported!) + // version check (only version 1 is supported!) if(0x01 != buf[6]) return false; @@ -124,7 +124,7 @@ class Improv { void sendDevInfo(void) { uint8_t buf[50]; 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; // firmware name p += char2Improv("AhoyDTU", &buf[p]); @@ -140,7 +140,7 @@ class Improv { p += char2Improv(mDevName, &buf[p]); buf[10] = p - 11; // sub length - buf[8] = p - 9; // paket length + buf[8] = p - 9; // packet length sendPaket(buf, p); } @@ -157,7 +157,7 @@ class Improv { uint8_t buf[50]; 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; JsonArray arr = obj[F("networks")]; @@ -170,7 +170,7 @@ class Improv { p += char2Improv(String(arr[i][F("rssi")]).c_str(), &buf[p]); buf[10] = p - 11; // sub length - buf[8] = p - 9; // paket length + buf[8] = p - 9; // packet length sendPaket(buf, p); } diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h index 954ae18a..23af29f8 100644 --- a/src/utils/scheduler.h +++ b/src/utils/scheduler.h @@ -34,6 +34,7 @@ namespace ah { void setup(bool directStart) { mUptime = 0; mTimestamp = (directStart) ? 1 : 0; + mTsMillis = 0; mMax = 0; mPrevMillis = millis(); resetTicker(); @@ -59,8 +60,10 @@ namespace ah { } mUptime += mDiffSeconds; - if(0 != mTimestamp) + if(0 != mTimestamp) { mTimestamp += mDiffSeconds; + mTsMillis = mMillis % 1000; + } checkTicker(); } @@ -77,6 +80,7 @@ namespace ah { virtual void setTimestamp(uint32_t ts) { mTimestamp = ts; + mTsMillis = millis() % 1000; } bool resetEveryById(uint8_t id) { @@ -90,10 +94,6 @@ namespace ah { return mUptime; } - uint32_t getTimestamp(void) { - return mTimestamp; - } - inline void resetTicker(void) { for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) mTickerInUse[i] = false; @@ -118,6 +118,7 @@ namespace ah { protected: uint32_t mTimestamp; uint32_t mUptime; + uint16_t mTsMillis; private: inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) { diff --git a/src/utils/spiPatcher.cpp b/src/utils/spiPatcher.cpp new file mode 100644 index 00000000..0470b476 --- /dev/null +++ b/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 diff --git a/src/utils/spiPatcher.h b/src/utils/spiPatcher.h new file mode 100644 index 00000000..14cb138c --- /dev/null +++ b/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 +#include + +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__*/ diff --git a/src/utils/spiPatcherHandle.h b/src/utils/spiPatcherHandle.h new file mode 100644 index 00000000..69aa14df --- /dev/null +++ b/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__*/ diff --git a/src/utils/syslog.cpp b/src/utils/syslog.cpp new file mode 100644 index 00000000..5e73f287 --- /dev/null +++ b/src/utils/syslog.cpp @@ -0,0 +1,99 @@ +#include +#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 \ No newline at end of file diff --git a/src/utils/syslog.h b/src/utils/syslog.h new file mode 100644 index 00000000..37bdc524 --- /dev/null +++ b/src/utils/syslog.h @@ -0,0 +1,54 @@ + +#ifndef __SYSLOG_H__ +#define __SYSLOG_H__ + +#ifdef ESP8266 + #include +#elif defined(ESP32) + #include +#endif +#include +#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__*/ \ No newline at end of file diff --git a/src/utils/timemonitor.h b/src/utils/timemonitor.h new file mode 100644 index 00000000..798077d5 --- /dev/null +++ b/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 + +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 \ No newline at end of file diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 6e48c401..94a09c71 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -33,26 +33,28 @@ 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 dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP}; -template +template class RestApi { public: RestApi() { mTimezoneOffset = 0; - mHeapFree = 0; - mHeapFreeBlk = 0; - mHeapFrag = 0; - nr = 0; - } - - void setup(IApp *app, HMSYSTEM *sys, HMRADIO *radio, AsyncWebServer *srv, settings_t *config) { - mApp = app; - mSrv = srv; - mSys = sys; - mRadio = radio; - mConfig = config; - mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); + mHeapFree = 0; + mHeapFreeBlk = 0; + mHeapFrag = 0; + } + + void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) { + mApp = app; + mSrv = srv; + mSys = sys; + mRadioNrf = (HmRadio<>*)mApp->getRadioObj(true); + #if defined(ESP32) + mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false); + #endif + mConfig = config; 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)); + 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)); } @@ -62,9 +64,6 @@ class RestApi { } void ctrlRequest(JsonObject obj) { - /*char out[128]; - serializeJson(obj, out, 128); - DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/ DynamicJsonDocument json(128); JsonObject dummy = json.as(); if(obj[F("path")] == "ctrl") @@ -75,6 +74,8 @@ class RestApi { private: void onApi(AsyncWebServerRequest *request) { + DPRINTLN(DBG_VERBOSE, String("onApi: ") + String((uint16_t)request->method())); // 1 == Get, 3 == POST + mHeapFree = ESP.getFreeHeap(); #ifndef ESP32 mHeapFreeBlk = ESP.getMaxFreeBlockSize(); @@ -89,10 +90,13 @@ class RestApi { else if(path == "html/logout") getHtmlLogout(request, root); else if(path == "html/reboot") getHtmlReboot(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 == "generic") getGeneric(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 == "index") getIndex(request, root); else if(path == "setup") getSetup(request, root); @@ -111,6 +115,14 @@ class RestApi { getInverter(root, request->url().substring(17).toInt()); else if(path.substring(0, 15) == "inverter/alarm/") 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 getNotFound(root, F("http://") + request->host() + F("/api/")); } @@ -124,16 +136,36 @@ class RestApi { void onApiPost(AsyncWebServerRequest *request) { 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) { 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(); + + AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); + JsonObject root = response->getRoot(); root[F("success")] = (err) ? false : true; if(!err) { String path = request->url().substring(5); @@ -145,18 +177,20 @@ class RestApi { root[F("success")] = false; root[F("error")] = "Path not found: " + path; } - } - else { + } else { switch (err.code()) { case DeserializationError::Ok: break; - case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; - case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; - default: root[F("error")] = F("Deserialization failed"); break; + case DeserializationError::IncompleteInput: root[F("error")] = F("Incomplete input"); break; + case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break; + case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break; + default: root[F("error")] = F("Deserialization failed"); break; } } response->setLength(); request->send(response); + delete[] mTmpBuf; + mTmpBuf = NULL; } void getNotFound(JsonObject obj, String url) { @@ -164,7 +198,7 @@ class RestApi { ep[F("inverter/list")] = url + F("inverter/list"); ep[F("inverter/id/0")] = url + F("inverter/id/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("index")] = url + F("index"); ep[F("setup")] = url + F("setup"); @@ -193,6 +227,16 @@ class RestApi { 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); } @@ -231,7 +275,7 @@ class RestApi { #endif /* !defined(ETHERNET) */ obj[F("device_name")] = mConfig->sys.deviceName; obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; - obj[F("sched_reboot")] = (bool)mConfig->sys.schedReboot; + obj[F("sched_reboot")] = (bool)mConfig->sys.schedReboot; obj[F("mac")] = WiFi.macAddress(); obj[F("hostname")] = mConfig->sys.deviceName; @@ -245,14 +289,17 @@ class RestApi { obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb getGeneric(request, obj); - getRadioNrf(obj.createNestedObject(F("radio"))); - getStatistics(obj.createNestedObject(F("statistics"))); + getRadioNrf(obj.createNestedObject(F("radioNrf"))); + #if defined(ESP32) + getRadioCmtInfo(obj.createNestedObject(F("radioCmt"))); + #endif + getMqttInfo(obj.createNestedObject(F("mqtt"))); #if defined(ESP32) - obj[F("heap_total")] = ESP.getHeapSize(); obj[F("chip_revision")] = ESP.getChipRevision(); obj[F("chip_model")] = ESP.getChipModel(); obj[F("chip_cores")] = ESP.getChipCores(); + obj[F("heap_total")] = ESP.getHeapSize(); //obj[F("core_version")] = F("n/a"); //obj[F("flash_size")] = 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_model")] = 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("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(); #endif //obj[F("littlefs_total")] = LittleFS.totalBytes(); @@ -280,28 +327,71 @@ class RestApi { void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) { getSysInfo(request, obj.createNestedObject(F("system"))); getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("html")] = F("Factory Reset

Reboot"); + obj[F("html")] = F("AhoyFactory Reset

Reboot"); } void getHtmlLogout(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); obj[F("refresh")] = 3; obj[F("refresh_url")] = "/"; - obj[F("html")] = F("succesfully logged out"); + obj[F("html")] = F("successfully logged out"); } void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); + #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) + obj[F("refresh")] = 5; + #else obj[F("refresh")] = 20; + #endif obj[F("refresh_url")] = "/"; obj[F("html")] = F("rebooting ..."); } void getHtmlSave(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); - obj["pending"] = (bool)mApp->getSavePending(); - obj["success"] = (bool)mApp->getLastSaveSucceed(); - obj["reboot"] = (bool)mApp->getShouldReboot(); + obj[F("pending")] = (bool)mApp->getSavePending(); + obj[F("success")] = (bool)mApp->getLastSaveSucceed(); + 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)? yes no"); + } + + 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? yes no"); + } + + 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) { @@ -311,14 +401,28 @@ class RestApi { obj[F("html")] = F("reboot. Autoreload after 10 seconds"); } - void getStatistics(JsonObject obj) { - statistics_t *stat = mApp->getStatistics(); - obj[F("rx_success")] = stat->rxSuccess; - obj[F("rx_fail")] = stat->rxFail; - obj[F("rx_fail_answer")] = stat->rxFailNoAnser; - obj[F("frame_cnt")] = stat->frmCnt; - obj[F("tx_cnt")] = mRadio->mSendCnt; - obj[F("retransmits")] = mRadio->mRetransmits; + void getIvStatistis(JsonObject obj, uint8_t id) { + Inverter<> *iv = mSys->getInverterByPos(id); + if(NULL == iv) { + obj[F("error")] = F("inverter not found!"); + return; + } + obj[F("name")] = String(iv->config->name); + 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) { @@ -327,30 +431,42 @@ class RestApi { Inverter<> *iv; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { iv = mSys->getInverterByPos(i); - if(NULL != iv) { - JsonObject obj2 = invArr.createNestedObject(); - obj2[F("enabled")] = (bool)iv->config->enabled; - obj2[F("id")] = i; - obj2[F("name")] = String(iv->config->name); - obj2[F("serial")] = String(iv->config->serial.u64, HEX); - obj2[F("channels")] = iv->channels; - obj2[F("version")] = String(iv->getFwVersion()); - - for(uint8_t j = 0; j < iv->channels; j ++) { - obj2[F("ch_yield_cor")][j] = (double)iv->config->yieldCor[j]; - obj2[F("ch_name")][j] = iv->config->chName[j]; - obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j]; - } + if(NULL == iv) + continue; + + JsonObject obj2 = invArr.createNestedObject(); + obj2[F("enabled")] = (bool)iv->config->enabled; + obj2[F("id")] = i; + obj2[F("name")] = String(iv->config->name); + obj2[F("serial")] = String(iv->config->serial.u64, HEX); + obj2[F("channels")] = iv->channels; + 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 ++) { + obj2[F("ch_yield_cor")][j] = (double)iv->config->yieldCor[j]; + obj2[F("ch_name")][j] = iv->config->chName[j]; + obj2[F("ch_max_pwr")][j] = iv->config->chMaxPwr[j]; } } - obj[F("interval")] = String(mConfig->nrf.sendInterval); - obj[F("retries")] = String(mConfig->nrf.maxRetransPerPyld); + obj[F("interval")] = String(mConfig->inst.sendInterval); obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; 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("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("gap")] = mConfig->inst.gapMs; } void getInverter(JsonObject obj, uint8_t id) { @@ -368,10 +484,12 @@ class RestApi { obj[F("version")] = String(iv->getFwVersion()); obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); obj[F("power_limit_ack")] = iv->powerLimitAck; + obj[F("max_pwr")] = iv->getMaxPower(); obj[F("ts_last_success")] = rec->ts; 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("rssi")] = iv->rssi; JsonArray ch = obj.createNestedArray("ch"); @@ -385,10 +503,10 @@ class RestApi { ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; } } else { - for (uint8_t fld = 0; fld < sizeof(acList); fld++) { - pos = (iv->getPosByChFld(CH0, acList[fld], rec)); - ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; - } + for (uint8_t fld = 0; fld < sizeof(acList); fld++) { + pos = (iv->getPosByChFld(CH0, acList[fld], rec)); + ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0; + } } // DC @@ -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) { Inverter<> *iv = mSys->getInverterByPos(id); if(NULL == iv) { @@ -413,6 +541,7 @@ class RestApi { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); obj[F("iv_id")] = id; + obj[F("iv_name")] = String(iv->config->name); obj[F("cnt")] = iv->alarmCnt; 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) { obj[F("broker")] = String(mConfig->mqtt.broker); obj[F("clientId")] = String(mConfig->mqtt.clientId); @@ -444,7 +609,6 @@ class RestApi { void getSun(JsonObject obj) { obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : ""; obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : ""; - obj[F("disnightcom")] = mConfig->sun.disNightCom; obj[F("offs")] = mConfig->sun.offsetSec; } @@ -457,28 +621,43 @@ class RestApi { obj[F("miso")] = mConfig->nrf.pinMiso; obj[F("led0")] = mConfig->led.led0; 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) { - obj[F("csb")] = mConfig->cmt.pinCsb; - obj[F("fcsb")] = mConfig->cmt.pinFcsb; - obj[F("irq")] = mConfig->cmt.pinIrq; - obj[F("en")] = (bool) mConfig->cmt.enabled; + obj[F("sclk")] = mConfig->cmt.pinSclk; + obj[F("sdio")] = mConfig->cmt.pinSdio; + obj[F("csb")] = mConfig->cmt.pinCsb; + obj[F("fcsb")] = mConfig->cmt.pinFcsb; + obj[F("gpio3")] = mConfig->cmt.pinIrq; + 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) { - 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) { - obj[F("interval")] = (uint16_t)mConfig->serial.interval; obj[F("show_live_data")] = mConfig->serial.showIv; obj[F("debug")] = mConfig->serial.debug; + obj[F("priv")] = mConfig->serial.privacyLog; + obj[F("wholeTrace")] = mConfig->serial.printWholeTrace; } void getStaticIp(JsonObject obj) { @@ -493,7 +672,7 @@ class RestApi { void getDisplay(JsonObject obj) { obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type; obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; - obj[F("disp_pxshift")] = (bool)mConfig->plugin.display.pxShift; + obj[F("disp_screensaver")] = (uint8_t)mConfig->plugin.display.screenSaver; obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot; obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; obj[F("disp_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk; @@ -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_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("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) { @@ -510,47 +698,38 @@ class RestApi { obj[F("ts_sunrise")] = mApp->getSunrise(); obj[F("ts_sunset")] = mApp->getSunset(); obj[F("ts_offset")] = mConfig->sun.offsetSec; - obj[F("disNightComm")] = mConfig->sun.disNightCom; JsonArray inv = obj.createNestedArray(F("inverter")); Inverter<> *iv; + bool disNightCom = false; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { iv = mSys->getInverterByPos(i); - if(NULL != iv) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - JsonObject invObj = inv.createNestedObject(); - invObj[F("enabled")] = (bool)iv->config->enabled; - invObj[F("id")] = i; - invObj[F("name")] = String(iv->config->name); - invObj[F("version")] = String(iv->getFwVersion()); - invObj[F("is_avail")] = iv->isAvailable(); - invObj[F("is_producing")] = iv->isProducing(); - invObj[F("ts_last_success")] = iv->getLastTs(rec); - } + if(NULL == iv) + continue; + + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + JsonObject invObj = inv.createNestedObject(); + invObj[F("enabled")] = (bool)iv->config->enabled; + invObj[F("id")] = i; + invObj[F("name")] = String(iv->config->name); + invObj[F("cur_pwr")] = ah::round3(iv->getChannelFieldValue(CH0, FLD_PAC, rec)); + invObj[F("is_avail")] = iv->isAvailable(); + invObj[F("is_producing")] = iv->isProducing(); + invObj[F("ts_last_success")] = iv->getLastTs(rec); + if(iv->config->disNightCom) + disNightCom = true; } + obj[F("disNightComm")] = disNightCom; 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")); - else if(!mRadio->isPVariant() && mConfig->nrf.enabled) - warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible")); if(!mApp->getSettingsValid()) warn.add(F("your settings are invalid")); if(mApp->getRebootRequestState()) warn.add(F("reboot your ESP to apply all your configuration changes")); if(0 == mApp->getTimestamp()) 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) { @@ -561,7 +740,9 @@ class RestApi { getNtp(obj.createNestedObject(F("ntp"))); getSun(obj.createNestedObject(F("sun"))); getPinout(obj.createNestedObject(F("pinout"))); + #if defined(ESP32) getRadioCmt(obj.createNestedObject(F("radioCmt"))); + #endif getRadioNrf(obj.createNestedObject(F("radioNrf"))); getSerial(obj.createNestedObject(F("serial"))); getStaticIp(obj.createNestedObject(F("static_ip"))); @@ -576,7 +757,7 @@ class RestApi { void getLive(AsyncWebServerRequest *request, JsonObject obj) { 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++) { obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); @@ -630,28 +811,6 @@ class RestApi { 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) { Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); bool accepted = true; @@ -659,6 +818,7 @@ class RestApi { jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as(); return false; } + jsonOut[F("id")] = jsonIn[F("id")]; if(F("power") == jsonIn[F("cmd")]) accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); @@ -676,12 +836,10 @@ class RestApi { iv->powerLimit[1] = AbsolutNonPersistent; accepted = iv->setDevControlRequest(ActivePowerContr); - } - else if(F("dev") == jsonIn[F("cmd")]) { + } else if(F("dev") == jsonIn[F("cmd")]) { DPRINTLN(DBG_INFO, F("dev cmd")); - iv->enqueCommand(jsonIn[F("val")].as()); - } - else { + iv->setDevCommand(jsonIn[F("val")].as()); + } else { jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as() + "'"; return false; } @@ -689,8 +847,7 @@ class RestApi { if(!accepted) { jsonOut[F("error")] = F("inverter does not accept dev control request at this moment"); return false; - } else - mApp->ivSendHighPrio(iv); + } return true; } @@ -707,8 +864,26 @@ class RestApi { mApp->setTimestamp(0); // 0: update ntp flag else if(F("serial_utc_offset") == jsonIn[F("cmd")]) mTimezoneOffset = jsonIn[F("val")]; - else if(F("discovery_cfg") == jsonIn[F("cmd")]) { + else if(F("discovery_cfg") == jsonIn[F("cmd")]) 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()); + + 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()); + } + + 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 { jsonOut[F("error")] = F("unknown cmd"); return false; @@ -719,14 +894,18 @@ class RestApi { IApp *mApp; HMSYSTEM *mSys; - HMRADIO *mRadio; + HmRadio<> *mRadioNrf; + #if defined(ESP32) + CmtRadio<> *mRadioCmt; + #endif AsyncWebServer *mSrv; settings_t *mConfig; uint32_t mTimezoneOffset; uint32_t mHeapFree, mHeapFreeBlk; uint8_t mHeapFrag; - uint16_t nr; + uint8_t *mTmpBuf = NULL; + uint32_t mTmpSize; }; #endif /*__WEB_API_H__*/ diff --git a/src/web/html/api.js b/src/web/html/api.js index 9dbe56c7..3b8b266d 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -1,6 +1,4 @@ -/** - * SVG ICONS - */ +/* SVG ICONS - https://icons.getbootstrap.com */ 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" @@ -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" ]; +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 */ @@ -119,24 +126,25 @@ function parseRssi(obj) { icon = iconWifi1; else if(obj["wifi_rssi"] <= -70) 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) { return new Date(d.getTime() + (d.getTimezoneOffset() * -60000)).toISOString().substring(0, 19).replace('T', ', '); } -function toIsoTimeStr(d) { - return new Date(d.getTime() + (d.getTimezoneOffset() * -60000)).toISOString().substring(11, 19).replace('T', ', '); +function toIsoTimeStr(d) { // UTC! + return new Date(d.getTime()).toISOString().substring(11, 19).replace('T', ', '); } function setHide(id, hide) { var elm = document.getElementById(id); + if(null == elm) + return; if(hide) { if(!elm.classList.contains("hide")) elm.classList.add("hide"); - } - else + } else 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 */ +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) { e = document.createElement('p'); 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) { - e = document.createElement('select'); - e.name = name; + var o = []; for(it of options) { - o = opt(it[0], it[1], (it[0] == selId)); - e.appendChild(o); + o.push(opt(it[0], it[1], (it[0] == selId))); } - return e; + return ml("select", {name: name}, o); } function selDelAllOpt(sel) { @@ -219,9 +287,7 @@ function selDelAllOpt(sel) { } function opt(val, html, sel=false) { - o = document.createElement('option'); - o.value = val; - o.innerHTML = html; + var o = ml("option", {value: val}, html); if(sel) o.selected = true; return o; @@ -280,7 +346,7 @@ function svg(data=null, w=24, h=24, cl=null, tooltip=null) { function modal(title, body) { if(null == document.getElementById("modal")) { 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", {class: "modal-content"}, [ ml("div", {class: "modal-header"}, [ diff --git a/src/web/html/colorBright.css b/src/web/html/colorBright.css index 81d57326..47382daa 100644 --- a/src/web/html/colorBright.css +++ b/src/web/html/colorBright.css @@ -8,6 +8,7 @@ --success: #009900; --input-bg: #eee; + --table-border: #ccc; --nav-bg: #333; --primary: #006ec0; @@ -17,6 +18,8 @@ --footer-bg: #282828; --modal-bg: #fff; + --invalid-bg: #f99; + --total-head-title: #8e5903; --total-bg: #b06e04; --iv-head-title: #1c6800; diff --git a/src/web/html/colorDark.css b/src/web/html/colorDark.css index 549158b9..65100721 100644 --- a/src/web/html/colorDark.css +++ b/src/web/html/colorDark.css @@ -8,6 +8,7 @@ --success: #00bb00; --input-bg: #333; + --table-border: #333; --nav-bg: #333; --primary: #004d87; @@ -15,7 +16,9 @@ --secondary: #0072c8; --nav-active: #555; --footer-bg: #282828; - --modal-bg: #666; + --modal-bg: #282828; + + --invalid-bg: #400; --total-head-title: #555511; --total-bg: #666622; diff --git a/src/web/html/grid_info.json b/src/web/html/grid_info.json new file mode 100644 index 00000000..5dcdda65 --- /dev/null +++ b/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 + } + ] + } + ] +} diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html index 9c81944c..2f13c2e8 100644 --- a/src/web/html/includes/nav.html +++ b/src/web/html/includes/nav.html @@ -8,7 +8,7 @@
Live History - Serial / Control + Webserial Settings Update diff --git a/src/web/html/index.html b/src/web/html/index.html index 3e3abc5a..baa70742 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -19,7 +19,6 @@

-

Support this project:

    @@ -47,7 +46,7 @@ function apiCb(obj) { var e = document.getElementById("apiResult"); if(obj["success"]) { - e.innerHTML = " command excuted"; + e.innerHTML = " command executed"; getAjax("/api/index", parse); } else @@ -116,7 +115,7 @@ } } - function parseIv(obj) { + function parseIv(obj, ts) { var p = div(["none"]); for(var i of obj) { var icon = iconSuccess; @@ -126,27 +125,25 @@ icon = iconWarn; cl = "icon-warn"; avail = "disabled"; - } - else if(false == i["is_avail"]) { + } else if((false == i["is_avail"]) || (0 == ts)) { icon = iconInfo; cl = "icon-info"; 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"; - } - else { + } else { avail = "available and is "; if(false == i["is_producing"]) - avail += "not "; - else + avail += "not producing"; + else { icon = iconSuccessFull; - avail += "producing"; + avail += "producing " + i.cur_pwr + "W"; + } } p.append( 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() ); @@ -160,14 +157,11 @@ document.getElementById("iv").replaceChildren(p); } - function parseWarnInfo(warn, success) { + function parseWarn(warn) { var p = div(["none"]); for(var w of warn) { 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) p.append(svg(iconInfo, 30, 30, "icon icon-info"), span(commInfo), br()); @@ -199,8 +193,8 @@ parseNav(obj["generic"]); parseGeneric(obj["generic"]); parseSys(obj); - parseIv(obj["inverter"]); - parseWarnInfo(obj["warnings"], obj["infos"]); + parseIv(obj["inverter"], obj.ts_now); + parseWarn(obj["warnings"]); if(exeOnce) { window.setInterval("tick()", 1000); exeOnce = false; diff --git a/src/web/html/save.html b/src/web/html/save.html index 4c924c40..9b5b4864 100644 --- a/src/web/html/save.html +++ b/src/web/html/save.html @@ -34,8 +34,8 @@ html = "Settings successfully saved. Automatic page reload in 3 seconds."; meta.content = 3; } else { - html = "Settings successfully saved. Rebooting. Automatic redirect in 20 seconds."; - meta.content = 20 + "; URL=/"; + html = "Settings successfully saved. Rebooting. Automatic redirect in " + obj.reload + " seconds."; + meta.content = obj.reload + "; URL=/"; } document.getElementsByTagName('head')[0].appendChild(meta); } else { @@ -51,7 +51,9 @@ parseHtml(obj); } } - intervalId = window.setInterval("getAjax('/api/html/save', parse)", 2500); + + intervalId = window.setInterval("getAjax('/api/html/save', parse)", 2500); + getAjax("/api/generic", parseGeneric); diff --git a/src/web/html/serial.html b/src/web/html/serial.html index 442ba24a..ef7aa4c3 100644 --- a/src/web/html/serial.html +++ b/src/web/html/serial.html @@ -7,58 +7,19 @@ {#HTML_NAV}
    -
    +
    - +
    -
    connected:
    +
    console active:
    Uptime:
    -
    +
    +
    -
    -
    -

    Commands

    -
    -
    -
    Select Inverter
    -
    -
    -
    -
    Power Limit Command
    -
    - -
    -
    -
    -
    Power Limit Value
    -
    -
    -
    -
    -
    -
    -
    -
    Control Inverter
    -
    - - - -
    -
    -
    -
    Ctrl result
    -
    n/a
    -
    {#HTML_FOOTER} @@ -66,6 +27,7 @@ var mAutoScroll = true; var con = document.getElementById("serial"); var exeOnce = true; + var version, build; function parseGeneric(obj) { var up = obj["ts_uptime"]; @@ -82,25 +44,15 @@ if(true == exeOnce) { parseNav(obj); parseESP(obj); - window.setInterval("getAjax('/api/generic', parseGeneric)", 10000); + window.setInterval("getAjax('/api/generic', parseGeneric)", 5000); exeOnce = false; - getAjax("/api/inverter/list", parse); + setTimeOffset(); } + version = obj.version; + build = obj.build; } - function parse(root) { - 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); - } - + function setTimeOffset() { // set time offset for serial console var obj = new Object(); obj.cmd = "serial_utc_offset"; @@ -115,16 +67,36 @@ mAutoScroll = !mAutoScroll; 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) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { - document.getElementById("connected").style.backgroundColor = "#0c0"; + document.getElementById("active").style.backgroundColor = "#0c0"; }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { - document.getElementById("connected").style.backgroundColor = "#f00"; + document.getElementById("active").style.backgroundColor = "#f00"; } }, false); @@ -135,56 +107,6 @@ }, 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); diff --git a/src/web/html/setup.html b/src/web/html/setup.html index fc855055..f172f925 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -30,15 +30,15 @@
    System Config -

    Pinout

    +

    Status LEDs

    Radio (NRF24L01+)

    - +

    Radio (CMT2300A)

    (ESP32 only)
    - +

    Serial Console

    print inverter data
    @@ -49,8 +49,12 @@
    -
    Interval [s]
    -
    +
    Privacy Mode
    +
    +
    +
    +
    Print whole traces in Log
    +
    @@ -65,7 +69,7 @@
    -

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

    +

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

    Search Networks
    @@ -140,45 +144,41 @@
    Inverter
    -
    -
    -
    -
    -
    -

    Note

    -

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

    +
    +
    Interval [s]
    +
    -
    -

    General

    -
    +
    +
    Inverter Gap [ms]
    +
    -
    Interval [s]
    -
    +
    Reset values and YieldDay at midnight
    +
    -
    Max retries per Payload
    -
    +
    Reset values when inverter polling pauses at sunset
    +
    -
    Reset values and YieldDay at midnight
    -
    +
    Reset values when inverter status is 'not available'
    +
    -
    Reset values when inverter polling pauses at sunset
    -
    +
    Reset 'max' values at midnight
    +
    -
    Reset values when inverter status is 'not available'
    -
    +
    Start without time sync (useful in AP-Only-Mode)
    +
    -
    Start without time sync (useful in AP-Only-Mode)
    -
    +
    Read Grid Profile
    +
    -
    Yield Effiency (should be between 0.95 and 0.96)
    -
    +
    Yield Efficiency (Standard 1.0)
    +
    @@ -196,7 +196,7 @@
-
NTP Intervall (in Minutes, min. 5 Minutes)
+
NTP Interval (in Minutes, min. 5 Minutes)
@@ -230,10 +230,6 @@
Offset (pre sunrise, post sunset)
-
-
Pause polling inverters during night
-
-
@@ -263,7 +259,7 @@
Topic
-
+

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

@@ -290,16 +286,14 @@
Turn off while inverters are offline
-
-
Enable Screensaver (pixel shifting, OLED only)
-
-
+
Luminance

Pinout

+
@@ -339,7 +333,6 @@ {#HTML_FOOTER}