Browse Source

Merge branch 'lumapu:main' into main

pull/1392/head
Knuti_in_Päse 1 year ago
committed by GitHub
parent
commit
fe92b2af31
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 155
      .github/workflows/compile_development.yml
  2. 166
      .github/workflows/compile_release.yml
  3. 302
      Getting_Started.md
  4. 16
      README.md
  5. 25
      doc/prometheus_ep_description.md
  6. BIN
      doc/screenshots/inverterSettings.png
  7. BIN
      doc/screenshots/settings.png
  8. 301
      manual/Getting_Started.md
  9. 74
      manual/User_Manual.md
  10. 71
      manual/ahoy_config.md
  11. 12
      patches/GxEPD2_SW_SPI.patch
  12. 28
      patches/RF24_Hal.patch
  13. 4
      scripts/applyPatches.py
  14. 3
      scripts/auto_firmware_version.py
  15. 61
      scripts/convertHtml.py
  16. 133
      scripts/getVersion.py
  17. 41
      scripts/reduceGxEPD2.py
  18. 4
      src/.vscode/settings.json
  19. 47
      src/CHANGES.md
  20. 246
      src/app.cpp
  21. 206
      src/app.h
  22. 27
      src/appInterface.h
  23. 53
      src/config/config.h
  24. 8
      src/config/config_override_example.h
  25. 132
      src/config/settings.h
  26. 21
      src/defines.h
  27. 6
      src/eth/ahoyeth.h
  28. 26
      src/hm/CommQueue.h
  29. 721
      src/hm/Communication.h
  30. 57
      src/hm/Heuristic.h
  31. 26
      src/hm/HeuristicInv.h
  32. 29
      src/hm/hmDefines.h
  33. 391
      src/hm/hmInverter.h
  34. 263
      src/hm/hmRadio.h
  35. 42
      src/hm/hmSystem.h
  36. 17
      src/hm/nrfHal.h
  37. 65
      src/hm/radio.h
  38. 175
      src/hm/simulator.h
  39. 274
      src/hms/cmt2300a.h
  40. 4
      src/hms/cmtHal.h
  41. 17
      src/hms/esp32_3wSpi.h
  42. 97
      src/hms/hmsRadio.h
  43. 315
      src/platformio.ini
  44. 400
      src/plugins/Display/Display.h
  45. 379
      src/plugins/Display/Display_Mono.h
  46. 20
      src/plugins/Display/Display_Mono_128X32.h
  47. 236
      src/plugins/Display/Display_Mono_128X64.h
  48. 18
      src/plugins/Display/Display_Mono_64X48.h
  49. 176
      src/plugins/Display/Display_Mono_84X48.h
  50. 28
      src/plugins/Display/Display_data.h
  51. 38
      src/plugins/Display/Display_ePaper.cpp
  52. 6
      src/plugins/Display/Display_ePaper.h
  53. 117
      src/plugins/history.h
  54. 225
      src/publisher/pubMqtt.h
  55. 236
      src/publisher/pubMqttIvData.h
  56. 12
      src/publisher/pubSerial.h
  57. 6
      src/utils/crc.cpp
  58. 8
      src/utils/helper.cpp
  59. 26
      src/utils/improv.h
  60. 30
      src/utils/scheduler.h
  61. 4
      src/utils/spiPatcher.cpp
  62. 6
      src/utils/spiPatcher.h
  63. 13
      src/utils/syslog.cpp
  64. 5
      src/utils/syslog.h
  65. 21
      src/utils/timemonitor.h
  66. 7
      src/web/Protection.cpp
  67. 122
      src/web/Protection.h
  68. 254
      src/web/RestApi.h
  69. 28
      src/web/html/api.js
  70. 6
      src/web/html/colorBright.css
  71. 16
      src/web/html/colorDark.css
  72. 224
      src/web/html/grid_info.json
  73. 96
      src/web/html/history.html
  74. 2
      src/web/html/includes/footer.html
  75. 3
      src/web/html/includes/header.html
  76. 12
      src/web/html/includes/nav.html
  77. 102
      src/web/html/index.html
  78. 14
      src/web/html/save.html
  79. 12
      src/web/html/serial.html
  80. 765
      src/web/html/setup.html
  81. 87
      src/web/html/style.css
  82. 42
      src/web/html/system.html
  83. 37
      src/web/html/update.html
  84. 188
      src/web/html/visualization.html
  85. 87
      src/web/html/wizard.html
  86. 75
      src/web/lang.h
  87. 1539
      src/web/lang.json
  88. 387
      src/web/web.h
  89. 21
      src/wifi/ahoywifi.cpp
  90. 35
      src/wifi/ahoywifi.h

155
.github/workflows/compile_development.yml

@ -1,25 +1,47 @@
name: Ahoy Dev-Build for ESP8266/ESP32
name: Ahoy Development
on:
push:
branches: development*
paths-ignore:
- '**.md' # Do no build on *.md changes
jobs:
build:
check:
name: Check Repository
runs-on: ubuntu-latest
if: github.repository == 'lumapu/ahoy' && github.ref_name == 'development03'
continue-on-error: true
steps:
- uses: actions/checkout@v4
build-en:
name: Build Environments (English)
needs: check
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
variant:
- esp8266
- esp8266-prometheus
- esp8285
- esp32-wroom32
- esp32-wroom32-prometheus
- esp32-wroom32-ethernet
- esp32-s2-mini
- esp32-c3-mini
- opendtufusion
- opendtufusion-ethernet
steps:
- uses: actions/checkout@v3
with:
ref: development03
- uses: benjlevesque/short-sha@v2.1
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- name: Cache Pip
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@ -27,13 +49,13 @@ jobs:
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
uses: actions/setup-python@v4.3.0
uses: actions/setup-python@v5
with:
python-version: "3.x"
@ -43,43 +65,112 @@ jobs:
pip install --upgrade platformio
- name: Run PlatformIO
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
run: pio run -d src -e ${{ matrix.variant }}
- name: Copy boot_app0.bin
run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
- name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Rename Binary files
id: rename-binary-files
working-directory: src
run: python ../scripts/getVersion.py >> $GITHUB_OUTPUT
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: dev-${{ matrix.variant }}
path: firmware/*
- name: Set Version
uses: cschleiden/replace-tokens@v1
build-de:
name: Build Environments (German)
needs: check
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
variant:
- esp8266-de
- esp8266-prometheus-de
- esp8285-de
- esp32-wroom32-de
- esp32-wroom32-prometheus-de
- esp32-wroom32-ethernet-de
- esp32-s2-mini-de
- esp32-c3-mini-de
- opendtufusion-de
- opendtufusion-ethernet-de
steps:
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
files: tools/esp8266/User_Manual.md
env:
VERSION: ${{ steps.rename-binary-files.outputs.name }}
length: 7
- name: Cache Pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Create Manifest
working-directory: src
run: python ../scripts/buildManifest.py
- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install PlatformIO
run: |
python -m pip install setuptools --upgrade pip
pip install --upgrade platformio
- name: Run PlatformIO
run: pio run -d src -e ${{ matrix.variant }}
- name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Create Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ahoydtu_dev
path: |
src/firmware/*
src/User_Manual.md
src/install.html
name: dev-${{ matrix.variant }}
path: firmware/*
deploy:
name: Deploy Environments
needs: [build-en, build-de]
runs-on: ubuntu-latest
continue-on-error: false
steps:
- uses: actions/checkout@v4
#- name: Copy boot_app0.bin
# run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
- name: Get Artifacts
uses: actions/download-artifact@v4
with:
merge-multiple: true
path: firmware
- name: Get Version from code
id: version_name
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Set Version
uses: cschleiden/replace-tokens@v1
with:
files: manual/User_Manual.md
env:
VERSION: ${{ steps.version_name.outputs.name }}
- name: Rename firmware directory
run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }}
run: mv firmware ${{ steps.version_name.outputs.name }}
- name: Deploy
uses: nogsantos/scp-deploy@master
with:
src: src/${{ steps.rename-binary-files.outputs.name }}/
src: ${{ steps.version_name.outputs.name }}/
host: ${{ secrets.FW_SSH_HOST }}
remote: ${{ secrets.FW_SSH_DIR }}/dev
port: ${{ secrets.FW_SSH_PORT }}

166
.github/workflows/compile_release.yml

@ -1,29 +1,49 @@
name: Ahoy Release for ESP8266/ESP32
name: Ahoy Release
on:
push:
branches: main
paths:
- 'src/**' # build only when changes occur here
- '.github/workflows/compile_release.yml'
- '!README.md'
- '!CHANGES.md'
- '!User_Manual.md'
paths-ignore:
- '**.md' # Do no build on *.md changes
jobs:
build:
name: Build Environments
runs-on: ubuntu-latest
if: github.repository == 'lumapu/ahoy' && github.ref_name == 'main'
continue-on-error: false
strategy:
matrix:
variant:
- esp8266
- esp8266-prometheus
- esp8285
- esp32-wroom32
- esp32-wroom32-prometheus
- esp32-wroom32-ethernet
- esp32-s2-mini
- esp32-c3-mini
- opendtufusion
- opendtufusion-ethernet
- esp8266-de
- esp8266-prometheus-de
- esp8285-de
- esp32-wroom32-de
- esp32-wroom32-prometheus-de
- esp32-wroom32-ethernet-de
- esp32-s2-mini-de
- esp32-c3-mini-de
- opendtufusion-de
- opendtufusion-ethernet-de
steps:
- uses: actions/checkout@v3
with:
ref: main
- uses: benjlevesque/short-sha@v2.1
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- name: Cache Pip
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@ -31,13 +51,13 @@ jobs:
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
uses: actions/setup-python@v4.3.0
uses: actions/setup-python@v5
with:
python-version: "3.x"
@ -47,56 +67,108 @@ jobs:
pip install --upgrade platformio
- name: Run PlatformIO
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
run: pio run -d src -e ${{ matrix.variant }}
- name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Copy boot_app0.bin
run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.variant }}
path: firmware/*
- name: Rename Binary files
id: rename-binary-files
working-directory: src
run: python ../scripts/getVersion.py >> $GITHUB_OUTPUT
- name: Create Release
id: create-release
uses: actions/create-release@v1
release:
name: Create Release
runs-on: ubuntu-latest
needs: [build]
continue-on-error: false
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Get Artifacts
uses: actions/download-artifact@v4
with:
draft: false
prerelease: false
release_name: ${{ steps.rename-binary-files.outputs.name }}
tag_name: ${{ steps.rename-binary-files.outputs.name }}
body_path: src/CHANGES.md
env:
GITHUB_TOKEN: ${{ github.token }}
merge-multiple: true
path: firmware
- name: Get Version from code
id: version_name
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Create tag
uses: actions/github-script@v7
with:
script: |
github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/tags/${{ steps.version_name.outputs.name }}',
sha: context.sha
})
- name: Set Version
uses: cschleiden/replace-tokens@v1
with:
files: User_Manual.md
files: manual/User_Manual.md
env:
VERSION: ${{ steps.rename-binary-files.outputs.name }}
VERSION: ${{ steps.version_name.outputs.name }}
- name: Create Artifact
run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip src/firmware/* User_Manual.md
- name: Rename firmware directory
run: mv firmware ${{ steps.version_name.outputs.name }}
- name: Upload Release
id: upload-release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Rename firmware directory
uses: vimtor/action-zip@v1.2
with:
files: ${{ steps.version_name.outputs.name }} manual/User_Manual.md manual/Getting_Started.md
dest: '${{ steps.version_name.outputs.name }}.zip'
- name: Publish Release
uses: ncipollo/release-action@v1
with:
upload_url: ${{ steps.create-release.outputs.upload_url }}
asset_path: ./${{ steps.rename-binary-files.outputs.name }}.zip
asset_name: ${{ steps.rename-binary-files.outputs.name }}.zip
asset_content_type: application/zip
artifactErrorsFailBuild: true
skipIfReleaseExists: true
bodyFile: src/CHANGES.md
artifacts: '${{ steps.version_name.outputs.name }}.zip'
tag: ${{ steps.version_name.outputs.name }}
name: ${{ steps.version_name.outputs.name }}
token: ${{ secrets.GITHUB_TOKEN }}
deploy:
name: Deploy Environments to fw.ahoydtu.de
needs: [build, release]
runs-on: ubuntu-latest
continue-on-error: false
steps:
- uses: actions/checkout@v4
- name: Get Artifacts
uses: actions/download-artifact@v4
with:
merge-multiple: true
path: firmware
- name: Get Version from code
id: version_name
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Set Version
uses: cschleiden/replace-tokens@v1
with:
files: manual/User_Manual.md
env:
VERSION: ${{ steps.version_name.outputs.name }}
- name: Rename firmware directory
run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }}
run: mv firmware ${{ steps.version_name.outputs.name }}
- name: Deploy
uses: nogsantos/scp-deploy@master
with:
src: src/${{ steps.rename-binary-files.outputs.name }}/
src: ${{ steps.version_name.outputs.name }}/
host: ${{ secrets.FW_SSH_HOST }}
remote: ${{ secrets.FW_SSH_DIR }}/release
port: ${{ secrets.FW_SSH_PORT }}

302
Getting_Started.md

@ -1,302 +0,0 @@
## Overview
On this page, you'll find detailed instructions on how to wire the module of a Wemos D1 mini or ESP32 to the radio module, as well as how to flash it with the latest firmware. This information will enable you to communicate with compatible inverters.
You find the full [User_Manual here](User_Manual.md)
## Compatiblity
The following inverters are currently supported out of the box:
Hoymiles Inverters
| Status | Serie | Model | comment |
| ----- | ----- | ------ | ------- |
| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet |
| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | |
| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
| ✔️ | HMT | 1600, 1800, 2250 | |
| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
## Table of Contents
- [Table of Contents](#table-of-contents)
- [Overview](#overview)
- [Compatiblity](#compatiblity)
- [Things needed](#things-needed)
- [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
- [Wiring things up](#wiring-things-up)
- [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
- [Schematic](#schematic)
- [Symbolic view](#symbolic-view)
- [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
- [Schematic](#schematic-2)
- [Symbolic view](#symbolic-view-2)
- [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1)
- [ESP32 GPIO settings](#esp32-gpio-settings)
- [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware)
- [Compiling your own Version](#compiling-your-own-version)
- [Optional Configuration before compilation](#optional-configuration-before-compilation)
- [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher)
- [Connect to your Ahoy DTU](#connect-to-your-ahoy-dtu)
- [Your Ahoy DTU is very verbose using the Serial Console](#your-ahoy-dtu-is-very-verbose-using-the-serial-console)
- [Connect to the Ahoy DTU Webinterface using your Browser](#connect-to-the-ahoy-dtu-webinterface-using-your-browser)
- [HTTP based Pages](#http-based-pages)
- [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface)
- [Used Libraries](#used-libraries)
- [ToDo](#todo)
***
Solenso Inverters:
- SOL-H350
## Things needed
If you're interested in building your own AhoyDTU, you'll need a few things to get started. While we've provided a list of recommended boards below, keep in mind that the maker community is constantly developing new and innovative options that we may not have covered in this readme..
For optimal performance, we recommend using a Wemos D1 mini or ESP32 along with a NRF24L01+ breakout board as a bare minimum. However, if you have experience working with other ESP boards, any board with at least 4MBytes of ROM may be suitable, depending on your skills.
Just be sure that the NRF24L01+ module you choose includes the "+" in its name, as we rely on the 250kbps features that are only provided by the plus-variant.
| **Parts** | **Price** |
| --- | --- |
| D1 ESP8266 Mini WLAN Board Microcontroller | 4,40 Euro |
| NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul | 3,45 Euro |
| 100µF / 10V Capacitor Kondensator | 0,15 Euro |
| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
| **Total costs** | **10,34 Euro** |
If you're interested in using our sister project OpenDTU or you want to future-proof your setup, we recommend investing in an ESP32 board that features two CPU cores. As Radio you can also use a NRF24L01+ module with an external antenna. While this option may cost a bit more, it will provide superior performance and ensure compatibility with upcoming developments.
| **Parts** | **Price** |
| --- | --- |
| ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 Euro |
| NRF24L01+ PA LNA SMA mit Antenne Long | 4,50 Euro |
| 100µF / 10V Capacitor Kondensator | 0,15 Euro |
| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
| **Total costs** | **14,89 Euro** |
#### There are fake NRF24L01+ Modules out there
Watch out, there are some fake NRF24L01+ Modules out there that seem to use rebranded NRF24L01 Chips (without the +).<br/>
An example can be found in [Issue #230](https://github.com/lumapu/ahoy/issues/230).<br/>
You are welcome to add more examples of faked chips. We will add that information here.<br/>
Some users reported better connection or longer range through more walls when using the
"E01-ML01DP5" EBYTE 2,4 GHz Wireless Modul nRF24L01 + PA + LNA RF Modul, SMA-K Antenna connector,
which has an eye-catching HF cover. But beware: It comes without the antenna!
In any case you should stabilize the Vcc power by a capacitor and don't exceed the Amplifier Power Level "LOW".
Users reporting good connection over 10m through walls / ceilings with Amplifier Power Level "MIN".
It is not always the bigger the better...
Power levels "HIGH" and "MAX" are meant to wirings where the nRF24 is supplied by an extra 3.3 Volt regulator.
The bultin regulator on ESP boards has only low reserves in case WiFi and nRF are sending simultaneously.
If you operate additional interfaces like a display, the reserve is again reduced.
## Wiring things up
The NRF24L01+ radio module is connected to the standard SPI pins:
- SCLK (Signal Clock),
- MISO (Master In Slave Out) and
- MOSI (Master Out Slave In)
*These pins need to be configured in the config.h.*
Additional, there are 3 pins, which can be set individual:
- CS (Chip Select),
- CE (Chip Enable) and
- IRQ (Interrupt)
*These pins can be changed from the /setup URL.*
#### ESP8266 wiring example on WEMOS D1
This is an example wiring using a Wemos D1 mini.<br>
##### Schematic
![Schematic](https://ahoydtu.de/img/fritzing/esp8266_nrf_sch.png)
##### Symbolic view
![Symbolic](https://ahoydtu.de/img/fritzing/esp8266_nrf.png)
#### ESP8266 wiring example on 30pin Lolin NodeMCU v3
This is an example wiring using a NodeMCU V3.<br>
##### Schematic
![Schematic](doc/ESP8266_nRF24L01+_Schaltplan.jpg)
##### Symbolic view
![Symbolic](doc/ESP8266_nRF24L01+_bb.png)
#### ESP32 wiring example
Example wiring for a 38pin ESP32 module
##### Schematic
![Schematic](https://ahoydtu.de/img/fritzing/esp32-38_nrf_sch.png)
##### Symbolic view
![Symbolic](https://ahoydtu.de/img/fritzing/esp32-38_nrf.png)
##### ESP32 GPIO settings
CS, CE, IRQ must be set according to how they are wired up. For the diagram above, set the 3 individual GPIOs under the /setup URL as follows:
```
CS D1 (GPIO5)
CE D2 (GPIO4)
IRQ D0 (GPIO16 - no IRQ!)
```
IMPORTANT: From development version 108/release 0.6.0 onwards, also MISO, MOSI, and SCLK
are configurable. On new installations, their defaults are correct for most ESP32 boards.
These pins cannot be configured for ESP82xx boards, as this chip cannot move them elsewhere.
If you are upgrading an existing install though, you might see that these pins are set to '0' in the web GUI.
Communication with the NRF module wont work. For upgrading an existing installations, set MISO=19, MOSI=23, SCLK=18 in the settings.
This is the correct default for most ESP32 boards. On ESP82xx, simply saving the settings without changes should suffice.
Save and reboot.
## Flash the Firmware on your Ahoy DTU Hardware
Once your Hardware is ready to run, you need to flash the Ahoy DTU Firmware to your Board.
You can either build your own using your own configuration or use one of our pre-compiled generic builds.
### Flash from your browser (easy)
The easiest step for you is to flash online. A browser MS Edge or Google Chrome is required.
[Here you go](https://ahoydtu.de/web_install/)
### Compiling your own Version
This information suits you if you want to configure and build your own firmware.
This code comes to you as a **PlatformIO** project and can be compiled using the **PlatformIO** Addon.<br/>
Visual Studio Code, AtomIDE and other IDE's support the PlatformIO Addon.<br/>
If you do not want to compile your own build, you can use one of our ready-to-flash binaries.
##### Optional Configuration before compilation
- number of supported inverters (set to 3 by default) `config.h`
- DTU radio id `config.h` (default = 1234567801)
- unformatted list in webbrowser `/livedata` `config.h`, `LIVEDATA_VISUALIZED`
Alternativly, instead of modifying `config.h`, `config_override_example.h` can be copied to `config_override.h` and customized.
config_override.h is excluded from version control and stays local.
#### Using a ready-to-flash binary using nodemcu-pyflasher
This information suits you if you just want to use an easy way.
1. download the flash-tool [nodemcu-pyflasher](https://github.com/marcelstoer/nodemcu-pyflasher)
2. download latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
3. open flash-tool and connect the target device to your computer.
4. Set the correct serial port and select the correct *.bin file
5. click on "Flash NodeMCU"
6. flash the ESP with the compiled firmware using the UART pins or
7. repower the ESP
8. the ESP will start as access point (AP) if there is no network config stored in its eeprom
9. connect to the AP (password: `esp_8266`), you will be forwarded to the setup page
10. configure your WiFi settings, save, repower
11. check your router or serial console for the IP address of the module. You can try ping the configured device name as well.
Once your Ahoy DTU is running, you can use the Over The Air (OTA) capabilities to update your firmware.
! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data!
#### Flashing on Linux with `esptool.py` (ESP32)
1. install [esptool.py](https://docs.espressif.com/projects/esptool/en/latest/esp32/) if you haven't already.
2. download and extract the latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
3. `cd ahoy_v<XXX> && cp *esp32.bin esp32.bin`
4. Perhaps you need to replace `/dev/ttyUSB0` to match your acual device in the following command. Execute it afterwards: `esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset write_flash --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 esp32.bin`
5. Unplug and replug your device.
6. Open a serial monitor (e.g. Putty) @ 115200 Baud. You should see some messages regarding wifi.
## Connect to your Ahoy DTU
When everything is wired up and the firmware is flashed, it is time to connect to your Ahoy DTU.
#### Your Ahoy DTU is very verbose using the Serial Console
When connected to your computer, you can open a Serial Console to obtain additional information.<br/>
This might be useful in case of any troubles that might occur as well as to simply<br/>
obtain information about the converted values which were read out of the inverter(s).
#### Connect to the Ahoy DTU Webinterface using your Browser
After you have sucessfully flashed and powered your Ahoy DTU, you can access it via your Browser.<br/>
If your Ahoy DTU was able to log into the configured WiFi Network, it will try to obtain an IP-Address<br/>
from your local DHCP Server (in most cases thats your Router).<br/><br/>
In case it could not connect to your configured Network, it will provide its own WiFi Network that you can<br/>
connect to for furter configuration.<br/>
The WiFi SSID *(the WiFi Name)* and Passwort is configured in the config.h and defaults to the SSID "`AHOY-DTU`" with the Passwort "`esp_8266`".<br/>
The Ahoy DTU will keep that Network open for a certain amount of time (also configurable in the config.h and defaults to 60secs).<br/>
If nothing connects to it and that time runs up, it will retry to connect to the configured network an so on.<br/>
<br/>
If connected to your local Network, you just have to find out the used IP Address or try the default name [http://ahoy-dtu/](http://ahoy-dtu/). In most cases your Router will give you a hint.<br/>
If you connect to the WiFi the Ahoy DTU opens in case it could not connect to any other Network, the IP-Address of your Ahoy DTU is [http://192.168.4.1/](http://192.168.4.1/).<br/>
Just open the IP-Address in your browser.<br/>
<br/>
The webinterface has the following abilities:
- OTA Update (Over The Air Update)
- Configuration (Wifi, inverter(s), NTP Server, Pinout, MQTT, Amplifier Power Level, Debug)
- visual display of the connected inverters / modules
- some statistics about communication (debug)
##### HTTP based Pages
To take control of your Ahoy DTU, you can directly call one of the following sub-pages (e.g. [http://ahoy-dtu/setup](http://ahoy-dtu/setup) or [http://192.168.4.1/setup](http://192.168.4.1/setup) ).<br/>
| page | use | output | default availability |
| ---- | ------ | ------ | ------ |
| /uptime | displays the uptime uf your Ahoy DTU | 0 Days, 01:37:34; now: 2022-08-21 11:13:53 | yes |
| /reboot | reboots the Ahoy DTU | | yes |
| /erase | erases the EEPROM | | yes |
| /factory | resets to the factory defaults configured in config.h | | yes |
| /setup | opens the setup page | | yes |
| /save | | | yes |
| /cmdstat | show stat from the home page | | yes |
| /visualization | displays the information from your converter | | yes |
| /livedata | displays the live data | | yes |
| /metrics | gets live-data for prometheus | prometheus metrics from the livedata | no - enable via config_override.h |
| /api | gets configuration and live-data in JSON format | json output from the configuration or livedata | yes |
## MQTT command to set the DTU without webinterface
[Read here](User_Manual.md)
## Used Libraries
| Name | version | License |
| --------------------- | ------- | -------- |
| `ESP8266WiFi` | 1.0 | LGPL-2.1 |
| `DNSServer` | 1.1.1 | LGPL-2.1 |
| `SPI` | 1.0 | LGPL-2.1 |
| `Hash` | 1.0 | LGPL-2.1 |
| `EEPROM` | 1.0 | LGPL-2.1 |
| `ESP Async WebServer` | 1.2.3 | LGPL-3.0 |
| `ESPAsyncTCP` | 1.2.2 | LGPL-3.0 |
| `Time` | 1.6.1 | LGPL-2.1 |
| `RF24` | 1.4.7 | GPL-2.0 |
| `espMqttClient` | 1.4.4 | MIT |
| `ArduinoJson` | 6.21.3 | MIT |
## ToDo
[See this post](https://github.com/lumapu/ahoy/issues/142)

16
README.md

@ -20,7 +20,7 @@ This work is licensed under a
# 🖐 Ahoy!
![Logo](https://github.com/lumapu/ahoy/blob/main/doc/logo1_small.png?raw=true)
This repository offers hardware and software solutions for communicating with Hoymiles inverters via radio. With our system, you can easily obtain real-time values such as power, current, and daily energy. Additionally, you can set parameters like the power limit of your inverter to achieve zero export. You can access these functionalities through our user-friendly web interface, MQTT, or JSON. Whether you're monitoring your solar panel system's performance or fine-tuning its settings, our solutions make it easy to achieve your goals.
This repository provides hardware and software solutions for communicating with Hoymiles inverters via radio. Our system allows you to easily obtain real-time values, such as power, current, and daily energy, as well as set parameters like the power limit of your inverter to achieve zero export. You can access these functionalities through our user-friendly web interface, MQTT, or JSON. Our solutions simplify the process of monitoring and fine-tuning your solar panel system to help you achieve your goals.
## Changelog
[latest Release](https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md)
@ -31,7 +31,7 @@ Table of approaches:
| Board | MI | HM | HMS/HMT | comment | HowTo start |
| ------ | -- | -- | ------- | ------- | ---------- |
| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
| [ESP8266/ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | |
| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
@ -39,9 +39,11 @@ Table of approaches:
⚠️ **Warning: HMS-XXXXW-2T WiFi inverters are not supported. They have a 'W' in their name and a DTU serial number on its sticker**
## Getting Started
[Guide how to start with a ESP module](Getting_Started.md)
1. [Guide how to start with a ESP module](manual/Getting_Started.md)
[ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install)
2. [ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install)
3. [Ahoy Configuration ](manual/ahoy_config.md)
## Our Website
[https://ahoydtu.de](https://ahoydtu.de)
@ -50,11 +52,11 @@ Table of approaches:
- [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl)
## Support, Feedback, Information and Discussion
- [Discord Server (~ 3.800 Users)](https://discord.gg/WzhxEY62mB)
- [Discord Server (~ 7.300 Users)](https://discord.gg/WzhxEY62mB)
- [The root of development](https://www.mikrocontroller.net/topic/525778)
### Development
If you run into any issues, please feel free to use the issue tracker here on Github. When describing your issue, please be as detailed and precise as possible, and take a moment to consider whether the issue is related to our software. This will help us to provide more effective solutions to your problem.
If you encounter any problems, use the issue tracker on Github. Provide a detailed description of the issue and consider if it is related to our software. This will help us provide effective solutions.
**Contributors are always welcome!**
@ -62,4 +64,4 @@ If you run into any issues, please feel free to use the issue tracker here on Gi
- [OpenDTU](https://github.com/tbnobody/OpenDTU)
<- Our sister project for Hoymiles HM- and HMS-/HMT-series (for ESP32 only!)
- [hms-mqtt-publisher](https://github.com/DennisOSRM/hms-mqtt-publisher)
<- a project which can handle WiFi inverters like HMS-XXXXW-2T
<- a project which can handle WiFi inverters like HMS-XXXXW-2T

25
doc/prometheus_ep_description.md

@ -1,10 +1,10 @@
# Prometheus Endpoint
Metrics available for AhoyDTU device, inverters and channels.
Prometheus metrics provided at `/metrics`.
Prometheus metrics provided at `/metrics`.
## Labels
| Label name | Description |
| Label name | Description |
|:-------------|:--------------------------------------|
| version | current installed version of AhoyDTU |
| image | currently not used |
@ -19,11 +19,25 @@ Prometheus metrics provided at `/metrics`.
|----------------------------------------------|---------|----------------------------------------------------------|--------------|
| `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_freeheap` | Gauge | free heap memory of the AhoyDTU device | devicename |
| `ahoy_solar_wifi_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_inverter_power_limit_read` | Gauge | Power Limit read from inverter. Defaults to 65535 | inverter |
| `ahoy_solar_inverter_power_limit_ack` | Gauge | Power Limit acknowledged by inverter | inverter |
| `ahoy_solar_inverter_max_power` | Gauge | Max Power of inverter | inverter |
| `ahoy_solar_inverter_radio_rx_success` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_rx_fail` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_rx_fail_answer` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_frame_cnt` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_tx_cnt` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_retransmits` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_iv_loss_cnt` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_iv_sent_cnt` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_dtu_loss_cnt` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_inverter_radio_dtu_sent_cnt` | Counter | NRF24 statistic of inverter | 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 |
@ -46,9 +60,4 @@ Prometheus metrics provided at `/metrics`.
| `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 | |

BIN
doc/screenshots/inverterSettings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
doc/screenshots/settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

301
manual/Getting_Started.md

@ -0,0 +1,301 @@
## Overview
This page contains detailed instructions on building a module and flashing it with the latest firmware. Following these instructions will allow you to communicate with compatible inverters.
You find the full [User_Manual here](User_Manual.md)
## Compatiblity
Currently, the following inverters are supported:
Hoymiles Inverters
| Status | Serie | Model | comment |
| ----- | ----- | ------ | ------- |
| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet |
| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | |
| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
| ✔️ | HMT | 1600, 1800, 2250 | |
| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
## Table of Contents
- [Overview](#overview)
- [Compatiblity](#compatiblity)
- [Things needed](#things-needed)
- [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
- [Wiring things up](#wiring-things-up)
- [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
- [Schematic](#schematic)
- [Symbolic view](#symbolic-view)
- [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
- [Schematic](#schematic-2)
- [Symbolic view](#symbolic-view-2)
- [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1)
- [ESP32 GPIO settings](#esp32-gpio-settings)
- [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware)
- [Compiling your own Version](#compiling-your-own-version)
- [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher)
- [Connect to your Ahoy DTU](#connect-to-your-ahoy-dtu)
- [Your Ahoy DTU is very verbose using the Serial Console](#your-ahoy-dtu-is-very-verbose-using-the-serial-console)
- [Connect to the Ahoy DTU Webinterface using your Browser](#connect-to-the-ahoy-dtu-webinterface-using-your-browser)
- [HTTP based Pages](#http-based-pages)
- [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface)
- [Used Libraries](#used-libraries)
- [ToDo](#todo)
***
Solenso Inverters:
- SOL-H350
## Things needed
To build your own AhoyDTU, you only need a few things. Remember that the maker community is always developing new and innovative options that we may not have covered in this readme.
Start with an ESP8266 or ESP32, and combine it with an NRF24L01+ breakout board. Other ESP boards with at least 4MBytes of ROM may also be suitable.
Make sure to choose an NRF24L01+ module that includes the '+' in its name. This is important because we need the 250kbps features that are only available in the plus-variant.
**Attention**: The NRF24L01+ can only communicate with the MI/HM/TSUN inverter. For the HMS/HMT it is needed to use a CMT2300A!
| **Parts** | **Price** |
| --- | --- |
| D1 ESP8266 Mini WLAN Board Microcontroller | 4,40 €|
| *NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul (not for HMS/HMT)* | *3,45 €*|
| *CMT2300A 868/915MHz (E49-900M20S)* | *4,59 €* |
| 100µF / 10V Capacitor Kondensator | 0,15 €|
| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 €|
| **Total costs** | **10,34 € / 11,48 €** |
To future-proof your setup and use our sister project OpenDTU, we recommend investing in an ESP32 board with two CPU cores. Additionally, you can use a NRF24L01+ module with an external antenna as a radio for superior performance and compatibility with upcoming developments.
| **Parts** | **Price** |
| --- | --- |
| ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 €|
| *NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul (not for HMS/HMT)* | *3,45 €*|
| *CMT2300A 868/915MHz (E49-900M20S)* | *4,59 €* |
| 100µF / 10V Capacitor Kondensator | 0,15 €|
| Jumper Wire breadboard female-female | 2,49 €|
| **Total costs** | **13,99 € / 15,13 €** |
#### There are fake NRF24L01+ Modules out there
Beware of fake NRF24L01+ modules that use rebranded NRF24L01 chips (without the +).
An example of this can be found in Issue #230 (https://github.com/lumapu/ahoy/issues/230).
If you have any additional examples of fake chips, please share them with us and we will add the information here.
#### NRF24L01+ improvements
Users have reported improved connections and longer range through walls when using these modules.
The "E01-ML01DP5" module is a 2.4 GHz wireless module that utilizes the nRF24L01+PA+LNA RF module and features an SMA-K antenna connector.
**The product includes an HF cover, but please note that it does not come with an antenna.**
To achieve the best results, stabilize the Vcc power by using a capacitor and do not exceed the 'LOW' Amplifier Power Level.
Users have reported good connections over 10m through walls and ceilings when using the Amplifier Power Level 'MIN'.
It's important to remember that bigger is not always better.
If you are using the NRF24 directly on the ESP board, make sure to set the transmission power to the lowest possible level (this can be adjusted later in the web interface). Using a high transmission power can potentially cause problems.
The ESP board's built-in controller has limited reserves in case both WiFi and nRF are transmitting simultaneously.
If you are using additional interfaces, such as a display, the reserves will be further reduced.
## Wiring things up
The NRF24L01+ radio module is connected to the standard SPI pins:
- SCLK (Signal Clock),
- MISO (Master In Slave Out) and
- MOSI (Master Out Slave In)
*These pins need to be configured in the config.h.*
Additional, there are 3 pins, which can be set individual:
- CS (Chip Select),
- CE (Chip Enable) and
- IRQ (Interrupt)
*These pins can be changed from the /setup URL.*
#### ESP8266 wiring example on WEMOS D1
This is an example wiring using a Wemos D1 mini.
##### Schematic
![Schematic](https://ahoydtu.de/img/fritzing/esp8266_nrf_sch.png)
##### Symbolic view
![Symbolic](https://ahoydtu.de/img/fritzing/esp8266_nrf.png)
#### ESP8266 wiring example on 30pin Lolin NodeMCU v3
This is an example wiring using a NodeMCU V3.<br>
##### Schematic
![Schematic](doc/ESP8266_nRF24L01+_Schaltplan.jpg)
##### Symbolic view
![Symbolic](doc/ESP8266_nRF24L01+_bb.png)
#### ESP32 wiring example
Example wiring for a 38pin ESP32 module
##### Schematic
![Schematic](https://ahoydtu.de/img/fritzing/esp32-38_nrf_sch.png)
##### Symbolic view
![Symbolic](https://ahoydtu.de/img/fritzing/esp32-38_nrf.png)
##### ESP32 GPIO settings
CS, CE, IRQ must be set according to how they are wired up. For the diagram above, set the 3 individual GPIOs under the /setup URL as follows:
```
CS D1 (GPIO5)
CE D2 (GPIO4)
IRQ D0 (GPIO16 - no IRQ!)
```
**IMPORTANT**: Starting from development version 108/release 0.6.0, MISO, MOSI, and SCLK are also included.
For most ESP32 boards, the default settings are correct on new installations.
However, it is not possible to configure these pins for ESP82xx boards, as they cannot be moved elsewhere.
If you are upgrading an existing installation, you may notice that the pins are set to '0' in the web GUI, which will prevent communication with the NRF module.
To resolve this, set MISO=19, MOSI=23, SCLK=18 in the settings.
This is the correct default for most ESP32 boards. For ESP82xx, simply saving the settings without changes should suffice.
Save and reboot.
## Flash the Firmware on your Ahoy DTU Hardware
After preparing your hardware, you must flash the Ahoy DTU Firmware to your board.
You can either create your own firmware using your configuration or use one of our pre-compiled generic builds.
Are you ready to flash? Then go to next Step here.
### Flash from your browser (easy)
The easiest step for you is to flash online. A browser MS Edge or Google Chrome is required.
[Here you go](https://ahoydtu.de/web_install/)
### Compiling your own Version (expert)
This information is for those who wish to configure and build their own firmware.
The code is provided as a PlatformIO project and can be compiled using the PlatformIO Addon.
The PlatformIO Addon is supported by Visual Studio Code, AtomIDE, and other IDEs.
If you do not wish to compile your own build, you can use one of our pre-compiled binaries.
#### Using a ready-to-flash binary using nodemcu-pyflasher
This information suits you if you just want to use an easy way.
1. download the flash-tool [nodemcu-pyflasher](https://github.com/marcelstoer/nodemcu-pyflasher)
2. download latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
3. open flash-tool and connect the target device to your computer.
4. Set the correct serial port and select the correct *.bin file
5. click on "Flash NodeMCU"
6. flash the ESP with the compiled firmware using the UART pins or
7. repower the ESP
8. the ESP will start as access point (AP) if there is no network config stored in its eeprom
9. connect to the AP (password: `esp_8266`), you will be forwarded to the setup page
10. configure your WiFi settings, save, repower
11. check your router or serial console for the IP address of the module. You can try ping the configured device name as well.
Once your Ahoy DTU is running, you can use the Over The Air (OTA) capabilities to update your firmware.
**! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data!**
#### Flashing on Linux with `esptool.py` (ESP32)
1. install [esptool.py](https://docs.espressif.com/projects/esptool/en/latest/esp32/) if you haven't already.
2. download and extract the latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
3. `cd ahoy_v<XXX> && cp *esp32.bin esp32.bin`
4. Perhaps you need to replace `/dev/ttyUSB0` to match your acual device in the following command. Execute it afterwards: `esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset write_flash --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 esp32.bin`
5. Unplug and replug your device.
6. Open a serial monitor (e.g. Putty) @ 115200 Baud. You should see some messages regarding wifi.
## Connect to your Ahoy DTU
Once everything is wired and the firmware is flashed, it is time to connect to your Ahoy DTU.
#### Your Ahoy DTU is very verbose using the Serial Console
Once connected to your computer, you can open a serial console to get additional information.
This can be useful for troubleshooting, as well as simply to get
information about the converted values read from the inverter(s).
#### Connect to the Ahoy DTU Webinterface using your Browser
After you have successfully flashed and powered up your Ahoy DTU, you can access it from your browser.<br/>
If your Ahoy DTU was able to log on to the configured WiFi network, it will try to obtain an IP address from your local DHCP server (in most cases this is your router).
If it cannot connect to your configured network, it will provide its own WiFi network that you can
to for further configuration.
The WiFi SSID *(the WiFi name)* and password are pre-configured and are set to SSID "`AHOY-DTU`" and password "`esp_8266`" by default.
The Ahoy DTU will keep this network open for a certain amount of time (default is 60sec).
If nothing connects to it and the time expires, it will retry to connect to the configured network, and so on.
If you are connected to your local network, just find out the IP address used or try the default name [http://ahoy-dtu/](http://ahoy-dtu/).
In most cases, your router will give you a hint.
If you connect to the WiFi the Ahoy DTU opens in case it could not connect to any other Network, the IP-Address of your Ahoy DTU is [http://192.168.4.1/](http://192.168.4.1/).
Just open the IP-Address in your browser.
The web interface has the following capabilities:
- Live data (values updated every 5 seconds)
Click on the title/name/alarm for more actions.
- Webserial (Debug)
- Settings (System Config, Network, Protection, Inverter, NTP Server, Sunrise/Sunset, MQTT, Display Config)
- Update (Over The Air Update)
- System (status about the modules)
##### HTTP based Pages
To take control of your Ahoy DTU, you can directly call one of the following sub-pages (e.g. [http://ahoy-dtu/setup](http://ahoy-dtu/setup) or [http://192.168.4.1/setup](http://192.168.4.1/setup) ).
| page | use | output | default availability |
| ---- | ------ | ------ | ------ |
| /logout| logout the user from webinterface | | yes |
| /reboot | reboots the Ahoy DTU | | yes |
| /system| show system inforamtion | | yes |
| /live | displays the live data | | yes |
| /save | | | yes |
| /erase | erases the EEPROM | | yes |
| /factory | resets to the factory defaults configured in config.h | | yes |
| /setup | opens the setup page | | yes |
| /metrics | gets live-data for prometheus | prometheus metrics from the livedata | no - enable via config_override.h |
| /api | gets configuration and live-data in JSON format | json output from the configuration or livedata | yes |
## MQTT command to set the DTU without webinterface
[Read here](User_Manual.md)
## Used Libraries
| Name | version | License |
| --------------------- | ------- | -------- |
| `ESP8266WiFi` | 1.0 | LGPL-2.1 |
| `DNSServer` | 1.1.1 | LGPL-2.1 |
| `SPI` | 1.0 | LGPL-2.1 |
| `Hash` | 1.0 | LGPL-2.1 |
| `EEPROM` | 1.0 | LGPL-2.1 |
| `ESPAsyncWebServer` | 1.2.3 | LGPL-3.0 |
| [ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) | 1.2.2 | [LGPL-3.0 license](https://github.com/me-no-dev/ESPAsyncTCP#LGPL-3.0-1-ov-file) |
| [Time](https://github.com/PaulStoffregen/Time) | 1.6.1 | ? |
| [RF24](https://github.com/nRF24/RF24) | 1.4.8 | [GPL-2.0 license](https://github.com/nRF24/RF24#GPL-2.0-1-ov-file) |
| [espMqttClient](https://github.com/bertmelis/espMqttClient) | ? | [MIT license](https://github.com/bertmelis/espMqttClient#MIT-1-ov-file) |
| [ArduinoJson](https://github.com/bblanchon/ArduinoJson) | 6.21.3 | [MIT license](https://github.com/bblanchon/ArduinoJson#MIT-1-ov-file)|
| [GxEPD2](https://github.com/ZinggJM/GxEPD2) | 1.5.2 | [GPL-3.0 license](https://github.com/ZinggJM/GxEPD2#GPL-3.0-1-ov-file)|
| [U8g2_Arduino](https://registry.platformio.org/libraries/olikraus/U8g2) | [2.35.9](https://registry.platformio.org/libraries/olikraus/U8g2/versions) | [BSD-2-Clause](https://spdx.org/licenses/BSD-2-Clause.html) |
## ToDo
[See this post](https://github.com/lumapu/ahoy/issues/142)

74
User_Manual.md → manual/User_Manual.md

@ -166,6 +166,8 @@ inverter/ctrl/limit/0 600W
### Power Limit persistent
This feature was removed. The persisten limit should not be modified cyclic by a script because of potential wearout of the flash inside the inverter.
## Control via REST API
### Generic Information
@ -174,6 +176,46 @@ The rest API works with *JSON* POST requests. All the following instructions mus
👆 `<INVERTER_ID>` is the number of the specific inverter in the setup page.
### Authentication (new for versions > `0.8.79`)
The authentication is only needed if a password was set.
To authenticate from API you have to add the following `JSON` to your request:
```json
{
"auth": <PASSOWRD>
}
```
`<PASSWORD>` is your DTU password in plain text.
As Response you get the following `JSON` if successful:
```json
{
"success": true,
"token": "<TOKEN>"
}
```
Where `<TOKEN>` is a random token with a length of 16 characters.
For all following commands you have only to include the token into your `JSON`:
```json
{
"token": "<TOKEN>"
}
```
ℹ️ Do not pass the plain text password with each command. Authenticate once and then use the token for all following commands. The token expires once the token wasn't sent for 20 minutes.
If the authentication fails or the token is expired you will receive the following `JSON`:
```json
{
"success": false,
"error": "ERR_PROTECTED"
}
```
### Inverter Power (On / Off)
```json
@ -195,8 +237,9 @@ The `<VALUE>` should be set to `1` = `ON` and `0` = `OFF`
}
```
**beginning from verson `0.8.39` the wattage and percentage has one decimal place!**
### Power Limit relative persistent [%]
### Power Limit (active power control) relative persistent [%]
```json
{
@ -205,10 +248,10 @@ The `<VALUE>` should be set to `1` = `ON` and `0` = `OFF`
"val": <VALUE>
}
```
The `VALUE` represents a percent number in a range of `[2 .. 100]`
The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]`
### Power Limit absolute persistent [Watts]
### Power Limit (active power control) absolute persistent [Watts]
```json
{
@ -217,10 +260,10 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]`
"val": <VALUE>
}
```
The `VALUE` represents watts in a range of `[0 .. 65535]`
The `VALUE` represents watts in a range of `[1.0 .. 6553.5]`
### Power Limit relative non persistent [%]
### Power Limit (active power control) relative non persistent [%]
```json
{
@ -229,10 +272,10 @@ The `VALUE` represents watts in a range of `[0 .. 65535]`
"val": <VALUE>
}
```
The `VALUE` represents a percent number in a range of `[2 .. 100]`
The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]`
### Power Limit absolute non persistent [Watts]
### Power Limit (active power control) absolute non persistent [Watts]
```json
{
@ -241,21 +284,8 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]`
"val": <VALUE>
}
```
The `VALUE` represents watts in a range of `[0 .. 65535]`
The `VALUE` represents watts in a range of `[1.0 .. 6553.5]`
### Developer Information REST API (obsolete)
In the same approach as for MQTT any other SubCmd and also MainCmd can be applied and the response payload can be observed in the serial logs. Eg. request the Alarm-Data from the Alarm-Index 5 from inverter 0 will look like this:
```json
{
"inverter":0,
"tx_request": 21,
"cmd": 17,
"payload": 5,
"payload2": 0
}
```
## Zero Export Control (needs rework)
* You can use the mqtt topic `<TOPIC>/devcontrol/<INVERTER_ID>/11` with a number as payload (eg. 300 -> 300 Watt) to set the power limit to the published number in Watt. (In regular cases the inverter will use the new set point within one intervall period; to verify this see next bullet)
@ -328,7 +358,7 @@ Send Power Limit:
- If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit.
### Update your AHOY-DTU Firmware
To update your AHOY-DTU, you have to download the latest firmware package.
Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip) available for download.
Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://fw.ahoydtu.de/dev) available for download.
As soon as you have downloaded the firmware package, unzip it. On the WebUI, navigate to Update and press on select firmware file.
From the unzipped files, select the right .bin file for your hardware and needs.
- If you use an ESP8266, select the file ending with esp8266.bin

71
manual/ahoy_config.md

@ -0,0 +1,71 @@
# Ahoy configuration
## Prerequists
You have build your own hardware (or purchased one). The firmware is already loaded on the ESP and the WebUI is accessible from your browser.
## Start
But how do I get my data from the inverter?
The following steps are required:
1. Set the pinning to communicate with the radio module.
2. Check if Ahoy has a current time
3. Configure the inverter data (e.g. serialnumber)
### 1.) Set the pinning
Once you are in the web interface, you will find the "System Config" sub-item in the Setup area.
This is where you tell the ESP how you connected the radio module.
Note the schematics you saw earlier. - If you haven't noticed them yet, here's another table of connections.
#### OpenDTU Fusion (ESP32-S3)
| NRF24 Pin | ESP Pin|
|---------| --------|
| CS (4) | GPIO37
| CE (3)| GPIO38
| IRQ (8) | GPIO47
| SCLK (5)| GPIO36
| MOSI (6)| GPIO35
| MISO (7)| GPIO48
| CMT2300A | Pin |
|---------| --------|
| CMT| Enabled |
| SCLK| GPIO6
| SDIO| GPIO5
| CSB| GPIO4
| FCSB| GPIO21
| GPIO3| GPIO8
### 2.) Set current time (standard: skip this step)
Ahoy needs a current date and time to talk to the inverter.
It works without, but it is recommended to include a time. This allows you to analyze information from the inverter in more detail.
Normally, a date/time should be automatically retrieved from the NTP server. However, it may happen that the firewall of some routers does not allow this.
In the section "Settings -> NTP Server" you can also get the time from your own computer. Or set up your own NTP server.
### 3.) Set inverter data
#### add new inverter
Now it's time to place the inverter. This is necessary because it is not the inverter that speaks first, but the DTU (Ahoy).
Each inverter has its own S.Nr. This also serves as an identity for communication between the DTU and the inverter.
The S.Nr is a 12-digit number. Check [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information.
#### set pv-modules (not necessary)
Click on "Add Inverter" and enter the S.No. and a name. Please keep the name short!
![grafik](https://github.com/lumapu/ahoy/doc/screenshots/settings.png)
![grafik](https://github.com/lumapu/ahoy/doc/screenshots/inverterSettings.png)
In the upper tab "Inputs" you can enter the data of the solar modules. These are only used directly in Ahoy for calculation and have no influence on the inverter.
#### set radio parameter (not necessary, only for EU)
In the next tab "Radio" you can adjust the power and other parameters if necessary. However, these should be left as default (EU only).
#### advanced options (not necessary to be changed)
In the "Advanced" section, you can customize more settings.
Save and reboot.
## ✅ Done - Now check the live site

12
patches/GxEPD2_SW_SPI.patch

@ -1,5 +1,5 @@
diff --git a/src/GxEPD2_EPD.cpp b/src/GxEPD2_EPD.cpp
index 1588444..592869b 100644
index 8df8bef..91d7f49 100644
--- a/src/GxEPD2_EPD.cpp
+++ b/src/GxEPD2_EPD.cpp
@@ -19,9 +19,9 @@
@ -71,7 +71,7 @@ index 1588444..592869b 100644
void GxEPD2_EPD::_reset()
{
if (_rst >= 0)
@@ -174,115 +169,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
@@ -174,115 +171,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
void GxEPD2_EPD::_writeCommand(uint8_t c)
{
@ -304,7 +304,7 @@ index 1588444..592869b 100644
+ _endTransaction();
}
diff --git a/src/GxEPD2_EPD.h b/src/GxEPD2_EPD.h
index ef2318f..50aa961 100644
index 34c1145..c480b7d 100644
--- a/src/GxEPD2_EPD.h
+++ b/src/GxEPD2_EPD.h
@@ -8,6 +8,10 @@
@ -334,7 +334,7 @@ index ef2318f..50aa961 100644
protected:
void _reset();
void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000);
@@ -111,9 +115,14 @@ class GxEPD2_EPD
@@ -111,17 +115,22 @@ class GxEPD2_EPD
void _startTransfer();
void _transfer(uint8_t value);
void _endTransfer();
@ -351,7 +351,9 @@ index ef2318f..50aa961 100644
bool _diag_enabled, _pulldown_rst_mode;
- SPIClass* _pSPIx;
SPISettings _spi_settings;
@@ -123,5 +124,5 @@ class GxEPD2_EPD
bool _initial_write, _initial_refresh;
bool _power_is_on, _using_partial_mode, _hibernating;
bool _init_display_done;
uint16_t _reset_duration;
- void (*_busy_callback)(const void*);
+ void (*_busy_callback)(const void*);

28
patches/RF24_Hal.patch

@ -1,5 +1,5 @@
diff --git a/RF24.cpp b/RF24.cpp
index c0cc732..b6708d9 100644
index 9e5b4a8..af00758 100644
--- a/RF24.cpp
+++ b/RF24.cpp
@@ -12,228 +12,24 @@
@ -727,7 +727,7 @@ index c0cc732..b6708d9 100644
}
/****************************************************************************/
@@ -1676,15 +1136,8 @@ void RF24::closeReadingPipe(uint8_t pipe)
@@ -1675,15 +1135,8 @@ void RF24::closeReadingPipe(uint8_t pipe)
void RF24::toggle_features(void)
{
@ -745,8 +745,20 @@ index c0cc732..b6708d9 100644
}
/****************************************************************************/
@@ -1871,6 +1324,11 @@ uint8_t RF24::getARC(void)
return read_register(OBSERVE_TX) & 0x0F;
}
+uint8_t RF24::getPLOS(void)
+{
+ return (read_register(OBSERVE_TX) >> 4) & 0x0F;
+}
+
/****************************************************************************/
bool RF24::setDataRate(rf24_datarate_e speed)
diff --git a/RF24.h b/RF24.h
index dbd32ae..f774bba 100644
index dbd32ae..74ae35d 100644
--- a/RF24.h
+++ b/RF24.h
@@ -16,12 +16,7 @@
@ -932,7 +944,15 @@ index dbd32ae..f774bba 100644
*/
void encodeRadioDetails(uint8_t* encoded_status);
@@ -1896,18 +1800,6 @@ private:
@@ -1644,6 +1548,7 @@ public:
* @return Returns values from 0 to 15.
*/
uint8_t getARC(void);
+ uint8_t getPLOS(void);
/**
* Set the transmission @ref Datarate
@@ -1896,18 +1801,6 @@ private:
*/
bool _init_pins();

4
scripts/applyPatches.py

@ -12,11 +12,11 @@ def applyPatch(libName, patchFile):
os.chdir('.pio/libdeps/' + env['PIOENV'] + '/' + libName)
process = subprocess.run(['git', 'apply', '--reverse', '--check', '../../../../' + patchFile], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
process = subprocess.run(['git', 'apply', '--ignore-whitespace', '--reverse', '--check', '../../../../' + patchFile], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if (process.returncode == 0):
print('\'' + patchFile + '\' already applied')
else:
process = subprocess.run(['git', 'apply', '../../../../' + patchFile])
process = subprocess.run(['git', 'apply', '--ignore-whitespace', '../../../../' + patchFile])
if (process.returncode == 0):
print('\'' + patchFile + '\' applied')
else:

3
scripts/auto_firmware_version.py

@ -21,7 +21,8 @@ def get_firmware_specifier_build_flag():
except:
build_version = "g0000000"
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version[1:] + "\\\""
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version[1:] + "\\\" "
build_flag += "-DENV_NAME=\\\"" + env["PIOENV"] + "\\\" ";
print ("Firmware Revision: " + build_version)
return (build_flag)

61
scripts/convertHtml.py

@ -3,6 +3,7 @@ import os
import gzip
import glob
import shutil
import json
from datetime import date
from pathlib import Path
import subprocess
@ -22,18 +23,33 @@ def readVersion(path):
today = date.today()
search = ["_MAJOR", "_MINOR", "_PATCH"]
version = today.strftime("%y%m%d") + "_ahoy_"
ver = ""
for line in lines:
if(line.find("VERSION_") != -1):
for s in search:
p = line.find(s)
if(p != -1):
version += line[p+13:].rstrip() + "."
ver += line[p+13:].rstrip() + "."
return ver[:-1]
def htmlParts(file, header, nav, footer, version):
def readVersionFull(path):
f = open(path, "r")
lines = f.readlines()
f.close()
today = date.today()
search = ["_MAJOR", "_MINOR", "_PATCH"]
version = today.strftime("%y%m%d") + "_ahoy_"
for line in lines:
if(line.find("VERSION_") != -1):
for s in search:
p = line.find(s)
if(p != -1):
version += line[p+13:].rstrip() + "."
version = version[:-1] + "_" + get_git_sha()
return version
def htmlParts(file, header, nav, footer, versionPath, lang):
p = "";
f = open(file, "r")
lines = f.readlines()
@ -58,12 +74,16 @@ def htmlParts(file, header, nav, footer, version):
p += line
#placeholders
version = readVersion(versionPath);
link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>'
p = p.replace("{#VERSION}", version)
p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath))
p = p.replace("{#VERSION_GIT}", link)
# remove if - endif ESP32
p = checkIf(p)
p = translate(file, p, lang)
p = translate("general", p, lang) # menu / header / footer
f = open("tmp/" + file, "w")
f.write(p);
@ -94,7 +114,30 @@ def checkIf(data):
return data
def convert2Header(inFile, version):
def findLang(file):
with open('../lang.json') as j:
lang = json.load(j)
for l in lang["files"]:
if l["name"] == file:
return l
return None
def translate(file, data, lang="de"):
json = findLang(file)
if None != json:
matches = re.findall(r'\{\#([A-Z0-9_]+)\}', data)
for x in matches:
for e in json["list"]:
if x == e["token"]:
#print("replace " + "{#" + x + "}" + " with " + e[lang])
data = data.replace("{#" + x + "}", e[lang])
return data
def convert2Header(inFile, versionPath, lang):
fileType = inFile.split(".")[1]
define = inFile.split(".")[0].upper()
define2 = inFile.split(".")[1].upper()
@ -114,7 +157,7 @@ def convert2Header(inFile, version):
f.close()
else:
if fileType == "html":
data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version)
data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", versionPath, lang)
else:
f = open(inFile, "r")
data = f.read()
@ -167,8 +210,12 @@ for files in types:
Path("h").mkdir(exist_ok=True)
Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements
shutil.copyfile("style.css", "tmp/style.css")
version = readVersion("../../defines.h")
# get language from environment
lang = "en"
if env['PIOENV'][-3:] == "-de":
lang = "de"
# go throw the array
for val in files_grabbed:
convert2Header(val, version)
convert2Header(val, "../../defines.h", lang)

133
scripts/getVersion.py

@ -2,6 +2,7 @@ import os
import shutil
import gzip
from datetime import date
import sys
def genOtaBin(path):
arr = []
@ -32,8 +33,8 @@ def gzip_bin(bin_file, gzip_file):
with gzip.open(gzip_file, "wb", compresslevel = 9) as f:
shutil.copyfileobj(fp, f)
def readVersion(path, infile):
f = open(path + infile, "r")
def getVersion(path_define):
f = open(path_define, "r")
lines = f.readlines()
f.close()
@ -48,106 +49,44 @@ def readVersion(path, infile):
if(p != -1):
version += line[p+13:].rstrip() + "."
versionnumber += line[p+13:].rstrip() + "."
os.mkdir(path + "firmware/")
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/firmware.bin"
dst = path + "firmware/ESP8266/" + versionout
os.rename(src, dst)
return [version, versionnumber]
versionout = version[:-1] + "_" + sha + "_esp8266_prometheus.bin"
src = path + ".pio/build/esp8266-prometheus/firmware.bin"
dst = path + "firmware/ESP8266/" + versionout
os.rename(src, dst)
def renameFw(path_define, env):
version = getVersion(path_define)[0]
versionout = version[:-1] + "_" + sha + "_esp8285.bin"
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/firmware.bin"
dst = path + "firmware/ESP32/" + versionout
os.rename(src, dst)
versionout = version[:-1] + "_" + sha + "_esp32_prometheus.bin"
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/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/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/"
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)
os.mkdir("firmware/")
fwDir = ""
if env[:7] == "esp8266":
fwDir = "ESP8266/"
elif env[:7] == "esp8285":
fwDir = "ESP8285/"
elif env[:7] == "esp32-w":
fwDir = "ESP32/"
elif env[:8] == "esp32-s2":
fwDir = "ESP32-S2/"
elif env[:4] == "open":
fwDir = "ESP32-S3/"
elif env[:8] == "esp32-c3":
fwDir = "ESP32-C3/"
os.mkdir("firmware/" + fwDir)
sha = os.getenv("SHA",default="sha")
# 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)
dst = "firmware/" + fwDir
fname = version[:-1] + "_" + sha + "_" + env + ".bin"
# 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(dst)
os.rename("src/.pio/build/" + env + "/firmware.bin", dst + fname)
# 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")
genOtaBin(dst)
if env[:5] == "esp32":
os.rename("src/.pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin")
os.rename("src/.pio/build/" + env + "/partitions.bin", dst + "partitions.bin")
genOtaBin(dst)
os.rename("../scripts/gh-action-dev-build-flash.html", path + "install.html")
if env[:7] == "esp8285":
gzip_bin(dst + fname, dst + fname[:-4] + ".gz")
print("name=" + versionnumber[:-1] )
readVersion("", "defines.h")
if len(sys.argv) == 1:
print("name=" + getVersion("src/defines.h")[1][:-1])
else:
# arg1: environment
renameFw("src/defines.h", sys.argv[1])

41
scripts/reduceGxEPD2.py

@ -0,0 +1,41 @@
import os
import subprocess
import glob
Import("env")
def rmDirWithFiles(path):
if os.path.isdir(path):
for f in glob.glob(path + "/*"):
os.remove(f)
os.rmdir(path)
def clean(libName):
# 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)
os.chdir('src/')
types = ('epd/*.h', 'epd/*.cpp') # the tuple of file types
files = []
for t in types:
files.extend(glob.glob(t))
for f in files:
if f.count('GxEPD2_150_BN') == 0:
os.remove(f)
rmDirWithFiles("epd3c")
rmDirWithFiles("epd4c")
rmDirWithFiles("epd7c")
rmDirWithFiles("gdeq")
rmDirWithFiles("gdey")
rmDirWithFiles("it8951")
os.chdir(start)
clean("GxEPD2")

4
src/.vscode/settings.json

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

47
src/CHANGES.md

@ -1,12 +1,41 @@
Changelog v0.8.36
Changelog v0.8.83
* 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
* added German translations for all variants
* added reading grid profile
* added decimal place for active power control (APC aka power limit)
* added information about working IRQ for NRF24 and CMT2300A to `/system`
* added loss rate to `/visualization` in the statistics window and MqTT
* added optional output to display whether it's night time or not. Can be reused as output to control battery system or mapped to a LED
* added timestamp for `max ac power` as tooltip
* added wizard for initial WiFi connection
* added history graph (still under development)
* added simulator (must be activated before compile, standard: off)
* added minimal version (without: MqTT, Display, History), WebUI is not changed! (not compiled automatically)
* added info about installed binary to `/update`
* added protection to prevent update to wrong firmware (environment check)
* added optional custom link to the menu
* added support for other regions (USA, Indonesia)
* added warning for WiFi channel 12-14 (ESP8266 only)
* added `max_power` to MqTT total values
* added API-Token authentification for external scripts
* improved MqTT by marking sent data and improved `last_success` resends
* improved communication for HM and MI inverters
* improved reading live data from inverter
* improved sending active power control command faster
* improved `/settings`: pinout has an own subgroup
* improved export by saving settings before they are exported (to have everything in JSON)
* improved code quality (cppcheck)
* seperated sunrise and sunset offset to two fields
* fix MqTT night communication
* fix missing favicon to html header
* fix build on Windows of `opendtufusion` environments (git: trailing whitespaces)
* fix generation of DTU-ID
* fix: protect commands from popup in `/live` if password is set
* fix: prevent sending commands to inverter which isn't active
* combined firmware and hardware version to JSON topics (MqTT)
* updated Prometheus with latest changes
* upgraded most libraries to newer versions
* beautified typography, added spaces between value and unit for `/visualization`
* removed add to total (MqTT) inverter setting
full version log: [Development Log](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md)

246
src/app.cpp

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -7,9 +7,16 @@
#include "app.h"
#include "utils/sun.h"
#if !defined(ESP32)
void esp_task_wdt_reset() {}
#endif
//-----------------------------------------------------------------------------
app::app() : ah::Scheduler {} {}
app::app() : ah::Scheduler {} {
memset(mVersion, 0, sizeof(char) * 12);
memset(mVersionModules, 0, sizeof(char) * 12);
}
//-----------------------------------------------------------------------------
@ -18,7 +25,13 @@ void app::setup() {
while (!Serial)
yield();
#if defined(ESP32)
esp_task_wdt_init(WDT_TIMEOUT_SECONDS, true);
esp_task_wdt_add(NULL);
#endif
resetSystem();
esp_task_wdt_reset();
mSettings.setup();
mSettings.getPtr(mConfig);
@ -30,12 +43,14 @@ void app::setup() {
else
DBGPRINTLN(F("false"));
esp_task_wdt_reset();
if(mConfig->nrf.enabled) {
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->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, false);
mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, mConfig->sys.region);
}
#endif
#ifdef ETHERNET
@ -50,9 +65,14 @@ void app::setup() {
#endif
#endif /* defined(ETHERNET) */
mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs);
esp_task_wdt_reset();
mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace);
mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
mSys.setup(&mTimestamp, &mConfig->inst);
#if defined(ENABLE_MQTT)
mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); });
#endif
mSys.setup(&mTimestamp, &mConfig->inst, this);
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
initInverter(i);
}
@ -62,8 +82,11 @@ void app::setup() {
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
esp_task_wdt_reset();
// when WiFi is in client mode, then enable mqtt broker
#if !defined(AP_ONLY)
#if defined(ENABLE_MQTT)
mMqttEnabled = (mConfig->mqtt.broker[0] > 0);
if (mMqttEnabled) {
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime);
@ -71,19 +94,21 @@ void app::setup() {
mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
}
#endif
#endif
setupLed();
mWeb.setup(this, &mSys, mConfig);
mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
esp_task_wdt_reset();
mWeb.setup(this, &mSys, mConfig);
mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig);
mProtection = Protection::getInstance(mConfig->sys.adminPwd);
#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)
if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
#if defined(ESP32)
mDisplay.setup(this, &mConfig->plugin.display, &mSys, &mNrfRadio, &mCmtRadio, &mTimestamp);
#else
@ -91,19 +116,37 @@ void app::setup() {
#endif
#endif
esp_task_wdt_reset();
#if defined(ENABLE_HISTORY)
mHistory.setup(this, &mSys, mConfig, &mTimestamp);
#endif /*ENABLE_HISTORY*/
mPubSerial.setup(mConfig, &mSys, &mTimestamp);
#if !defined(ETHERNET)
//mImprov.setup(this, mConfig->sys.deviceName, mVersion);
#endif
#if defined(ENABLE_SIMULATOR)
mSimulator.setup(&mSys, &mTimestamp, 0);
mSimulator.addPayloadListener([this](uint8_t cmd, Inverter<> *iv) {
payloadEventListener(cmd, iv);
});
#endif /*ENABLE_SIMULATOR*/
esp_task_wdt_reset();
regularTickers();
}
//-----------------------------------------------------------------------------
void app::loop(void) {
esp_task_wdt_reset();
if(mConfig->nrf.enabled)
mNrfRadio.loop();
#if defined(ESP32)
if(mConfig->cmt.enabled)
mCmtRadio.loop();
@ -112,8 +155,11 @@ void app::loop(void) {
ah::Scheduler::loop();
mCommunication.loop();
#if defined(ENABLE_MQTT)
if (mMqttEnabled && mNetworkConnected)
mMqtt.loop();
#endif
yield();
}
//-----------------------------------------------------------------------------
@ -126,7 +172,6 @@ void app::onNetwork(bool gotIp) {
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;
@ -139,35 +184,42 @@ void app::onNetwork(bool gotIp) {
void app::regularTickers(void) {
DPRINTLN(DBG_DEBUG, F("regularTickers"));
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
everySec([this]() { mProtection->tickSecond(); }, "prot");
// Plugins
#if defined(PLUGIN_DISPLAY)
if (mConfig->plugin.display.type != 0)
if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
#endif
every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart");
#if !defined(ETHERNET)
//everySec([this]() { mImprov.tickSerial(); }, "impro");
#endif
#if defined(ENABLE_HISTORY)
everySec(std::bind(&HistoryType::tickerSecond, &mHistory), "hist");
#endif /*ENABLE_HISTORY*/
#if defined(ENABLE_SIMULATOR)
every(std::bind(&SimulatorType::tick, &mSimulator), 5, "sim");
#endif /*ENABLE_SIMULATOR*/
}
#if defined(ETHERNET)
void app::onNtpUpdate(bool gotTime) {
uint32_t nxtTrig = 5; // default: check again in 5 sec
if (gotTime || mTimestamp != 0) {
this->updateNtp();
nxtTrig = gotTime ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min
}
once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp");
mNtpReceived = true;
}
#endif /* defined(ETHERNET) */
//-----------------------------------------------------------------------------
void app::updateNtp(void) {
#if defined(ENABLE_MQTT)
if (mMqttReconnect && mMqttEnabled) {
mMqtt.tickerSecond();
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
}
#endif /*ENABLE_MQTT*/
// only install schedulers once even if NTP wasn't successful in first loop
if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed
@ -179,7 +231,6 @@ void app::updateNtp(void) {
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
if (rebootTrig <= mTimestamp) { //necessary for times other than midnight to prevent reboot loop
rebootTrig += 86400;
@ -188,7 +239,7 @@ void app::updateNtp(void) {
}
}
if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) {
if ((0 == mSunrise) && (0.0 != mConfig->sun.lat) && (0.0 != mConfig->sun.lon)) {
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
tickCalcSunrise();
}
@ -199,19 +250,24 @@ void app::updateNtp(void) {
//-----------------------------------------------------------------------------
void app::tickNtpUpdate(void) {
uint32_t nxtTrig = 5; // default: check again in 5 sec
bool isOK = false;
#if defined(ETHERNET)
bool isOK = (mTimestamp != 0);
mEth.updateNtpTime();
if (!mNtpReceived)
mEth.updateNtpTime();
else {
mNtpReceived = false;
isOK = true;
}
#else
bool isOK = mWifi.getNtpTime();
isOK = mWifi.getNtpTime();
#endif
if (isOK || mTimestamp != 0) {
if (isOK) {
this->updateNtp();
nxtTrig = isOK ? (mConfig->ntp.interval * 60) : 60; // depending on NTP update success check again in 12h (depends on setting) or in 1 min
nxtTrig = mConfig->ntp.interval * 60; // check again in 12h
// immediately start communicating
if (isOK && mSendFirst) {
if (mSendFirst) {
mSendFirst = false;
once(std::bind(&app::tickSend, this), 1, "senOn");
}
@ -226,15 +282,18 @@ void app::tickCalcSunrise(void) {
if (mSunrise == 0) // on boot/reboot calc sun values for current time
ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day
if (mTimestamp > (mSunset + mConfig->sun.offsetSecEvening)) // current time is past communication stop, calc sun values for next day
ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
tickIVCommunication();
uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop
uint32_t nxtTrig = mSunset + mConfig->sun.offsetSecEvening + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop
onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri");
if (mMqttEnabled)
if (mMqttEnabled) {
tickSun();
nxtTrig = mSunrise + mConfig->sun.offsetSecMorning + 1; // one second safety to trigger correctly
onceAt(std::bind(&app::tickSunrise, this), nxtTrig, "mqSr"); // trigger on sunrise to update 'dis_night_comm'
}
}
//-----------------------------------------------------------------------------
@ -243,22 +302,21 @@ void app::tickIVCommunication(void) {
bool zeroValues = false;
uint32_t nxtTrig = 0;
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys.getInverterByPos(i);
Inverter<> *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;
if (mTimestamp < (mSunrise + mConfig->sun.offsetSecMorning)) { // current time is before communication start, set next trigger to communication start
nxtTrig = mSunrise + mConfig->sun.offsetSecMorning;
} else {
if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
if (mTimestamp >= (mSunset + mConfig->sun.offsetSecEvening)) { // 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;
nxtTrig = mSunset + mConfig->sun.offsetSecEvening;
}
}
if (nxtTrig != 0)
@ -279,8 +337,27 @@ void app::tickIVCommunication(void) {
//-----------------------------------------------------------------------------
void app::tickSun(void) {
// only used and enabled by MQTT (see setup())
if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec))
#if defined(ENABLE_MQTT)
if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening))
once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry
#endif
}
//-----------------------------------------------------------------------------
void app::tickSunrise(void) {
// only used and enabled by MQTT (see setup())
#if defined(ENABLE_MQTT)
if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening, true))
once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry
#endif
}
//-----------------------------------------------------------------------------
void app::notAvailChanged(void) {
#if defined(ENABLE_MQTT)
if (mMqttEnabled)
mMqtt.notAvailChanged(mAllIvNotAvail);
#endif
}
//-----------------------------------------------------------------------------
@ -313,10 +390,9 @@ void app::tickMidnight(void) {
// 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);
uint8_t pos = iv->getPosByChFld(i, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f);
}
}
@ -325,13 +401,16 @@ void app::tickMidnight(void) {
if (mConfig->inst.rstYieldMidNight) {
zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY);
#if defined(ENABLE_MQTT)
if (mMqttEnabled)
mMqtt.tickerMidnight();
#endif
}
}
//-----------------------------------------------------------------------------
void app::tickSend(void) {
bool notAvail = true;
uint8_t fill = mCommunication.getFillState();
uint8_t max = mCommunication.getMaxFill();
if((max-MAX_NUM_INVERTERS) <= fill) {
@ -357,6 +436,9 @@ void app::tickSend(void) {
if(!iv->radio->isChipConnected())
continue;
if(InverterStatus::OFF != iv->status)
notAvail = false;
iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) {
if(isDevControl)
mCommunication.addImportant(iv, cmd);
@ -366,6 +448,10 @@ void app::tickSend(void) {
}
}
if(mAllIvNotAvail != notAvail)
once(std::bind(&app::notAvailChanged, this), 1, "avail");
mAllIvNotAvail = notAvail;
updateLed();
}
@ -380,8 +466,6 @@ 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())
@ -409,28 +493,63 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
pos = iv->getPosByChFld(ch, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f);
}
iv->resetAlarms();
iv->doCalculations();
}
changed = true;
}
if(changed) {
if(mMqttEnabled && !skipYieldDay)
mMqtt.setZeroValuesEnable();
if(changed)
payloadEventListener(RealTimeRunData_Debug, NULL);
}
}
//-----------------------------------------------------------------------------
void app::resetSystem(void) {
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
snprintf(mVersion, sizeof(mVersion), "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
snprintf(mVersionModules, sizeof(mVersionModules), "%s",
#ifdef ENABLE_PROMETHEUS_EP
"P"
#endif
#ifdef ENABLE_MQTT
"M"
#endif
#ifdef PLUGIN_DISPLAY
"D"
#endif
#ifdef ENABLE_HISTORY
"H"
#endif
#ifdef AP_ONLY
"A"
#endif
#ifdef ENABLE_SYSLOG
"Y"
#endif
#ifdef ENABLE_SIMULATOR
"S"
#endif
"-"
#ifdef LANG_DE
"de"
#else
"en"
#endif
);
#ifdef AP_ONLY
mTimestamp = 1;
#endif
mSendFirst = true;
mAllIvNotAvail = true;
mSunrise = 0;
mSunset = 0;
@ -443,6 +562,10 @@ void app::resetSystem(void) {
mSaveReboot = false;
mNetworkConnected = false;
#if defined(ETHERNET)
mNtpReceived = false;
#endif
}
//-----------------------------------------------------------------------------
@ -453,14 +576,11 @@ void app::mqttSubRxCb(JsonObject obj) {
//-----------------------------------------------------------------------------
void app::setupLed(void) {
uint8_t led_off = (mConfig->led.high_active) ? 0 : 255;
if (mConfig->led.led0 != DEF_PIN_OFF) {
pinMode(mConfig->led.led0, OUTPUT);
analogWrite(mConfig->led.led0, led_off);
}
if (mConfig->led.led1 != DEF_PIN_OFF) {
pinMode(mConfig->led.led1, OUTPUT);
analogWrite(mConfig->led.led1, led_off);
for(uint8_t i = 0; i < 3; i ++) {
if (mConfig->led.led[i] != DEF_PIN_OFF) {
pinMode(mConfig->led.led[i], OUTPUT);
analogWrite(mConfig->led.led[i], led_off);
}
}
}
@ -469,27 +589,33 @@ void app::updateLed(void) {
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;
if (mConfig->led.led[0] != DEF_PIN_OFF) {
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
Inverter<> *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);
analogWrite(mConfig->led.led[0], led_on);
break;
}
else if(iv->config->enabled)
analogWrite(mConfig->led.led0, led_off);
analogWrite(mConfig->led.led[0], led_off);
}
}
}
if (mConfig->led.led1 != DEF_PIN_OFF) {
if (mConfig->led.led[1] != DEF_PIN_OFF) {
if (getMqttIsConnected()) {
analogWrite(mConfig->led.led1, led_on);
analogWrite(mConfig->led.led[1], led_on);
} else {
analogWrite(mConfig->led.led1, led_off);
analogWrite(mConfig->led.led[1], led_off);
}
}
if (mConfig->led.led[2] != DEF_PIN_OFF) {
if((mTimestamp > (mSunset + mConfig->sun.offsetSecEvening)) || (mTimestamp < (mSunrise + mConfig->sun.offsetSecMorning)))
analogWrite(mConfig->led.led[2], led_on);
else
analogWrite(mConfig->led.led[2], led_off);
}
}

206
src/app.h

@ -8,6 +8,10 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#if defined(ESP32)
#include <esp_task_wdt.h>
#define WDT_TIMEOUT_SECONDS 8 // Watchdog Timeout 8s
#endif
#include "config/settings.h"
#include "defines.h"
@ -17,13 +21,19 @@
#if defined(ESP32)
#include "hms/hmsRadio.h"
#endif
#if defined(ENABLE_MQTT)
#include "publisher/pubMqtt.h"
#endif /*ENABLE_MQTT*/
#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/Protection.h"
#if defined(ENABLE_HISTORY)
#include "plugins/history.h"
#endif /*ENABLE_HISTORY*/
#include "web/web.h"
#include "hm/Communication.h"
#if defined(ETHERNET)
@ -33,8 +43,13 @@
#include "utils/improv.h"
#endif /* defined(ETHERNET) */
#if defined(ENABLE_SIMULATOR)
#include "hm/simulator.h"
#endif /*ENABLE_SIMULATOR*/
#include <RF24.h> // position is relevant since version 1.4.7 of this library
// convert degrees and radians for sun calculation
#define SIN(x) (sin(radians(x)))
#define COS(x) (cos(radians(x)))
@ -42,12 +57,18 @@
#define ACOS(x) (degrees(acos(x)))
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType;
#ifdef ESP32
#endif
typedef Web<HmSystemType> WebType;
typedef RestApi<HmSystemType> RestApiType;
#if defined(ENABLE_MQTT)
typedef PubMqtt<HmSystemType> PubMqttType;
#endif /*ENABLE_MQTT*/
typedef PubSerial<HmSystemType> PubSerialType;
#if defined(ENABLE_HISTORY)
typedef HistoryData<HmSystemType> HistoryType;
#endif /*ENABLE_HISTORY*/
#if defined (ENABLE_SIMULATOR)
typedef Simulator<HmSystemType> SimulatorType;
#endif /*ENABLE_SIMULATOR*/
// PLUGINS
#if defined(PLUGIN_DISPLAY)
@ -62,14 +83,14 @@ class app : public IApp, public ah::Scheduler {
~app() {}
void setup(void);
void loop(void);
void loop(void) override;
void onNetwork(bool gotIp);
void regularTickers(void);
void handleIntr(void) {
mNrfRadio.handleIntr();
}
void* getRadioObj(bool nrf) {
void* getRadioObj(bool nrf) override {
if(nrf)
return (void*)&mNrfRadio;
else {
@ -87,19 +108,19 @@ class app : public IApp, public ah::Scheduler {
}
#endif
uint32_t getUptime() {
uint32_t getUptime() override {
return Scheduler::getUptime();
}
uint32_t getTimestamp() {
uint32_t getTimestamp() override {
return Scheduler::mTimestamp;
}
uint64_t getTimestampMs() {
return ((uint64_t)Scheduler::mTimestamp * 1000) + (uint64_t)Scheduler::mTsMillis;
uint64_t getTimestampMs() override {
return ((uint64_t)Scheduler::mTimestamp * 1000) + ((uint64_t)millis() - (uint64_t)Scheduler::mTsMillis) % 1000;
}
bool saveSettings(bool reboot) {
bool saveSettings(bool reboot) override {
mShowRebootRequest = true; // only message on index, no reboot
mSavePending = true;
mSaveReboot = reboot;
@ -110,7 +131,7 @@ class app : public IApp, public ah::Scheduler {
return true;
}
void initInverter(uint8_t id) {
void initInverter(uint8_t id) override {
mSys.addInverter(id, [this](Inverter<> *iv) {
if((IV_MI == iv->ivGen) || (IV_HM == iv->ivGen))
iv->radio = &mNrfRadio;
@ -121,7 +142,7 @@ class app : public IApp, public ah::Scheduler {
});
}
bool readSettings(const char *path) {
bool readSettings(const char *path) override {
return mSettings.readSettings(path);
}
@ -129,82 +150,124 @@ class app : public IApp, public ah::Scheduler {
return mSettings.eraseSettings(eraseWifi);
}
bool getSavePending() {
bool getSavePending() override {
return mSavePending;
}
bool getLastSaveSucceed() {
bool getLastSaveSucceed() override {
return mSettings.getLastSaveSucceed();
}
bool getShouldReboot() {
bool getShouldReboot() override {
return mSaveReboot;
}
#if !defined(ETHERNET)
void scanAvailNetworks() {
void scanAvailNetworks() override {
mWifi.scanAvailNetworks();
}
bool getAvailNetworks(JsonObject obj) {
bool getAvailNetworks(JsonObject obj) override {
return mWifi.getAvailNetworks(obj);
}
void setupStation(void) override {
mWifi.setupStation();
}
void setStopApAllowedMode(bool allowed) override {
mWifi.setStopApAllowedMode(allowed);
}
String getStationIp(void) override {
return mWifi.getStationIp();
}
bool getWasInCh12to14(void) const override {
return mWifi.getWasInCh12to14();
}
#endif /* !defined(ETHERNET) */
void setRebootFlag() {
void setRebootFlag() override {
once(std::bind(&app::tickReboot, this), 3, "rboot");
}
const char *getVersion() {
const char *getVersion() override {
return mVersion;
}
uint32_t getSunrise() {
const char *getVersionModules() override {
return mVersionModules;
}
uint32_t getSunrise() override {
return mSunrise;
}
uint32_t getSunset() {
uint32_t getSunset() override {
return mSunset;
}
bool getSettingsValid() {
bool getSettingsValid() override {
return mSettings.getValid();
}
bool getRebootRequestState() {
bool getRebootRequestState() override {
return mShowRebootRequest;
}
void setMqttDiscoveryFlag() {
void setMqttDiscoveryFlag() override {
#if defined(ENABLE_MQTT)
once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf");
#endif
}
void setMqttPowerLimitAck(Inverter<> *iv) {
mMqtt.setPowerLimitAck(iv);
bool getMqttIsConnected() override {
#if defined(ENABLE_MQTT)
return mMqtt.isConnected();
#else
return false;
#endif
}
uint32_t getMqttTxCnt() override {
#if defined(ENABLE_MQTT)
return mMqtt.getTxCnt();
#else
return 0;
#endif
}
bool getMqttIsConnected() {
return mMqtt.isConnected();
uint32_t getMqttRxCnt() override {
#if defined(ENABLE_MQTT)
return mMqtt.getRxCnt();
#else
return 0;
#endif
}
void lock(bool fromWeb) override {
mProtection->lock(fromWeb);
}
uint32_t getMqttTxCnt() {
return mMqtt.getTxCnt();
char *unlock(const char *clientIp, bool loginFromWeb) override {
return mProtection->unlock(clientIp, loginFromWeb);
}
uint32_t getMqttRxCnt() {
return mMqtt.getRxCnt();
void resetLockTimeout(void) override {
mProtection->resetLockTimeout();
}
bool getProtection(AsyncWebServerRequest *request) {
return mWeb.isProtected(request);
bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const override {
return mProtection->isProtected(clientIp, token, askedFromWeb);
}
bool getNrfEnabled(void) {
bool getNrfEnabled(void) override {
return mConfig->nrf.enabled;
}
bool getCmtEnabled(void) {
bool getCmtEnabled(void) override {
return mConfig->cmt.enabled;
}
@ -216,19 +279,19 @@ class app : public IApp, public ah::Scheduler {
return mConfig->cmt.pinIrq;
}
uint32_t getTimezoneOffset() {
uint32_t getTimezoneOffset() override {
return mApi.getTimezoneOffset();
}
void getSchedulerInfo(uint8_t *max) {
void getSchedulerInfo(uint8_t *max) override {
getStat(max);
}
void getSchedulerNames(void) {
void getSchedulerNames(void) override {
printSchedulers();
}
void setTimestamp(uint32_t newTime) {
void setTimestamp(uint32_t newTime) override {
DPRINT(DBG_DEBUG, F("setTimestamp: "));
DBGPRINTLN(String(newTime));
if(0 == newTime)
@ -243,6 +306,22 @@ class app : public IApp, public ah::Scheduler {
Scheduler::setTimestamp(newTime);
}
uint16_t getHistoryValue(uint8_t type, uint16_t i) override {
#if defined(ENABLE_HISTORY)
return mHistory.valueAt((HistoryStorageType)type, i);
#else
return 0;
#endif
}
uint16_t getHistoryMaxDay() override {
#if defined(ENABLE_HISTORY)
return mHistory.getMaximumDay();
#else
return 0;
#endif
}
private:
#define CHECK_AVAIL true
#define SKIP_YIELD_DAY true
@ -252,11 +331,13 @@ class app : public IApp, public ah::Scheduler {
void payloadEventListener(uint8_t cmd, Inverter<> *iv) {
#if !defined(AP_ONLY)
if (mMqttEnabled)
mMqtt.payloadEventListener(cmd, iv);
#if defined(ENABLE_MQTT)
if (mMqttEnabled)
mMqtt.payloadEventListener(cmd, iv);
#endif /*ENABLE_MQTT*/
#endif
#if defined(PLUGIN_DISPLAY)
if(mConfig->plugin.display.type != 0)
if(DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
mDisplay.payloadEventListener(cmd);
#endif
updateLed();
@ -287,23 +368,30 @@ class app : public IApp, public ah::Scheduler {
void tickNtpUpdate(void);
#if defined(ETHERNET)
void onNtpUpdate(bool gotTime);
bool mNtpReceived = false;
#endif /* defined(ETHERNET) */
void updateNtp(void);
void triggerTickSend() override {
once(std::bind(&app::tickSend, this), 0, "tSend");
}
void tickCalcSunrise(void);
void tickIVCommunication(void);
void tickSun(void);
void tickSunrise(void);
void tickComm(void);
void tickSend(void);
void tickMinute(void);
void tickZeroValues(void);
void tickMidnight(void);
void notAvailChanged(void);
HmSystemType mSys;
HmRadio<> mNrfRadio;
Communication mCommunication;
bool mShowRebootRequest;
bool mShowRebootRequest = false;
#if defined(ETHERNET)
ahoyeth mEth;
@ -312,6 +400,7 @@ class app : public IApp, public ah::Scheduler {
#endif /* defined(ETHERNET) */
WebType mWeb;
RestApiType mApi;
Protection *mProtection = nullptr;
#ifdef ENABLE_SYSLOG
DbgSyslog mDbgSyslog;
#endif
@ -326,30 +415,41 @@ class app : public IApp, public ah::Scheduler {
#endif
char mVersion[12];
char mVersionModules[12];
settings mSettings;
settings_t *mConfig;
bool mSavePending;
bool mSaveReboot;
settings_t *mConfig = nullptr;
bool mSavePending = false;
bool mSaveReboot = false;
uint8_t mSendLastIvId;
bool mSendFirst;
uint8_t mSendLastIvId = 0;
bool mSendFirst = false;
bool mAllIvNotAvail = false;
bool mNetworkConnected;
bool mNetworkConnected = false;
// mqtt
#if defined(ENABLE_MQTT)
PubMqttType mMqtt;
bool mMqttReconnect;
bool mMqttEnabled;
#endif /*ENABLE_MQTT*/
bool mMqttReconnect = false;
bool mMqttEnabled = false;
// sun
int32_t mCalculatedTimezoneOffset;
uint32_t mSunrise, mSunset;
int32_t mCalculatedTimezoneOffset = 0;
uint32_t mSunrise = 0, mSunset = 0;
// plugins
#if defined(PLUGIN_DISPLAY)
DisplayType mDisplay;
DisplayData mDispData;
#endif
#if defined(ENABLE_HISTORY)
HistoryType mHistory;
#endif /*ENABLE_HISTORY*/
#if defined(ENABLE_SIMULATOR)
SimulatorType mSimulator;
#endif /*ENABLE_SIMULATOR*/
};
#endif /*__APP_H__*/

27
src/appInterface.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://ahoydtu.de
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -7,24 +7,18 @@
#define __IAPP_H__
#include "defines.h"
#include "hm/hmSystem.h"
#if defined(ETHERNET)
#include "AsyncWebServer_ESP32_W5500.h"
#else
#include "ESPAsyncWebServer.h"
#endif
//#include "hms/hmsRadio.h"
#if defined(ESP32)
//typedef CmtRadio<esp32_3wSpi<>> CmtRadioType;
#endif
// abstract interface to App. Make members of App accessible from child class
// like web or API without forward declaration
class IApp {
public:
virtual ~IApp() {}
virtual bool saveSettings(bool stopFs) = 0;
virtual bool saveSettings(bool reboot) = 0;
virtual void initInverter(uint8_t id) = 0;
virtual bool readSettings(const char *path) = 0;
virtual bool eraseSettings(bool eraseWifi) = 0;
@ -33,10 +27,15 @@ class IApp {
virtual bool getShouldReboot() = 0;
virtual void setRebootFlag() = 0;
virtual const char *getVersion() = 0;
virtual const char *getVersionModules() = 0;
#if !defined(ETHERNET)
virtual void scanAvailNetworks() = 0;
virtual bool getAvailNetworks(JsonObject obj) = 0;
virtual void setupStation(void) = 0;
virtual void setStopApAllowedMode(bool allowed) = 0;
virtual String getStationIp(void) = 0;
virtual bool getWasInCh12to14(void) const = 0;
#endif /* defined(ETHERNET) */
virtual uint32_t getUptime() = 0;
@ -49,10 +48,11 @@ class IApp {
virtual void getSchedulerInfo(uint8_t *max) = 0;
virtual void getSchedulerNames() = 0;
virtual void triggerTickSend() = 0;
virtual bool getRebootRequestState() = 0;
virtual bool getSettingsValid() = 0;
virtual void setMqttDiscoveryFlag() = 0;
virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0;
virtual bool getMqttIsConnected() = 0;
virtual bool getNrfEnabled() = 0;
@ -61,10 +61,15 @@ class IApp {
virtual uint32_t getMqttRxCnt() = 0;
virtual uint32_t getMqttTxCnt() = 0;
virtual bool getProtection(AsyncWebServerRequest *request) = 0;
virtual void lock(bool fromWeb) = 0;
virtual char *unlock(const char *clientIp, bool loginFromWeb) = 0;
virtual void resetLockTimeout(void) = 0;
virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0;
virtual void* getRadioObj(bool nrf) = 0;
virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0;
virtual uint16_t getHistoryMaxDay() = 0;
virtual void* getRadioObj(bool nrf) = 0;
};
#endif /*__IAPP_H__*/

53
src/config/config.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __CONFIG_H__
@ -32,12 +32,33 @@
// timeout for automatic logoff (20 minutes)
#define LOGOUT_TIMEOUT (20 * 60)
//-------------------------------------
// MODULE SELECTOR - done by platform.ini
//-------------------------------------
// MqTT connection
//#define ENABLE_MQTT
// display plugin
//#define PLUGIN_DISPLAY
// history graph (WebUI)
//#define ENABLE_HISTORY
// inverter simulation
//#define ENABLE_SIMULATOR
// to enable the syslog logging (will disable web-serial)
//#define ENABLE_SYSLOG
//-------------------------------------
// CONFIGURATION - COMPILE TIME
//-------------------------------------
// ethernet
#if defined(ETHERNET)
#define ETH_SPI_HOST SPI2_HOST
#define ETH_SPI_CLOCK_MHZ 25
@ -93,6 +114,16 @@
#define DEF_NRF_SCLK_PIN 18
#endif
#if defined(ETHERNET) && !defined(SPI_HAL)
#ifndef DEF_CMT_SPI_HOST
#define DEF_CMT_SPI_HOST SPI3_HOST
#endif
#else
#ifndef DEF_CMT_SPI_HOST
#define DEF_CMT_SPI_HOST SPI2_HOST
#endif
#endif /* defined(ETHERNET) */
#ifndef DEF_CMT_SCLK
#define DEF_CMT_SCLK 12
#endif
@ -142,6 +173,9 @@
#ifndef DEF_LED1
#define DEF_LED1 DEF_PIN_OFF
#endif
#ifndef DEF_LED2
#define DEF_LED2 DEF_PIN_OFF
#endif
#ifdef LED_ACTIVE_HIGH
#define LED_HIGH_ACTIVE true
#else
@ -181,7 +215,7 @@
#define INVERTER_OFF_THRES_SEC 15*60
// threshold of minimum power on which the inverter is marked as inactive
#define INACT_PWR_THRESH 3
#define INACT_PWR_THRESH 0
// Timezone
#define TIMEZONE 1
@ -219,6 +253,17 @@
// reconnect delay
#define MQTT_RECONNECT_DELAY 5000
// maximum custom link length
#define MAX_CUSTOM_LINK_LEN 100
#define MAX_CUSTOM_LINK_TEXT_LEN 32
// syslog settings
#ifdef ENABLE_SYSLOG
#define SYSLOG_HOST "<hostname-or-ip-address-of-syslog-server>"
#define SYSLOG_APP "ahoy"
#define SYSLOG_FACILITY FAC_USER
#define SYSLOG_PORT 514
#endif
#if __has_include("config_override.h")
#include "config_override.h"
#endif

8
src/config/config_override_example.h

@ -35,13 +35,5 @@
// #define ENABLE_PROMETHEUS_EP
// to enable the syslog logging (will disable web-serial)
//#define ENABLE_SYSLOG
#ifdef ENABLE_SYSLOG
#define SYSLOG_HOST "<hostname-or-ip-address-of-syslog-server>"
#define SYSLOG_APP "ahoy"
#define SYSLOG_FACILITY FAC_USER
#define SYSLOG_PORT 514
#endif
#endif /*__CONFIG_OVERRIDE_H__*/

132
src/config/settings.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -13,6 +13,7 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <algorithm>
#include <LittleFS.h>
#include "../defines.h"
@ -30,7 +31,7 @@
* https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout
* */
#define CONFIG_VERSION 7
#define CONFIG_VERSION 11
#define PROT_MASK_INDEX 0x0001
@ -39,8 +40,9 @@
#define PROT_MASK_SETUP 0x0008
#define PROT_MASK_UPDATE 0x0010
#define PROT_MASK_SYSTEM 0x0020
#define PROT_MASK_API 0x0040
#define PROT_MASK_MQTT 0x0080
#define PROT_MASK_HISTORY 0x0040
#define PROT_MASK_API 0x0080
#define PROT_MASK_MQTT 0x0100
#define DEF_PROT_INDEX 0x0001
#define DEF_PROT_LIVE 0x0000
@ -48,6 +50,7 @@
#define DEF_PROT_SETUP 0x0008
#define DEF_PROT_UPDATE 0x0010
#define DEF_PROT_SYSTEM 0x0020
#define DEF_PROT_HISTORY 0x0000
#define DEF_PROT_API 0x0000
#define DEF_PROT_MQTT 0x0000
@ -66,6 +69,8 @@ typedef struct {
uint16_t protectionMask;
bool darkMode;
bool schedReboot;
uint8_t region;
int8_t timezone;
#if !defined(ETHERNET)
// wifi
@ -106,7 +111,8 @@ typedef struct {
typedef struct {
float lat;
float lon;
uint16_t offsetSec;
int16_t offsetSecMorning;
int16_t offsetSecEvening;
} cfgSun_t;
typedef struct {
@ -114,11 +120,11 @@ typedef struct {
bool debug;
bool privacyLog;
bool printWholeTrace;
bool log2mqtt;
} cfgSerial_t;
typedef struct {
uint8_t led0; // first LED pin
uint8_t led1; // second LED pin
uint8_t led[3]; // LED pins
bool high_active; // determines if LEDs are high or low active
uint8_t luminance; // luminance of LED
} cfgLed_t;
@ -143,7 +149,6 @@ typedef struct {
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 {
@ -157,7 +162,6 @@ typedef struct {
bool rstMaxValsMidNight;
bool startWithoutTime;
float yieldEffiency;
uint16_t gapMs;
bool readGrid;
} cfgInst_t;
@ -165,6 +169,8 @@ typedef struct {
uint8_t type;
bool pwrSaveAtIvOffline;
uint8_t screenSaver;
uint8_t graph_ratio;
uint8_t graph_size;
uint8_t rot;
//uint16_t wakeUp;
//uint16_t sleepAt;
@ -180,6 +186,8 @@ typedef struct {
typedef struct {
display_t display;
char customLink[MAX_CUSTOM_LINK_LEN];
char customLinkText[MAX_CUSTOM_LINK_TEXT_LEN];
} plugins_t;
typedef struct {
@ -200,7 +208,7 @@ typedef struct {
class settings {
public:
settings() {
mLastSaveSucceed = false;
std::fill(reinterpret_cast<char*>(&mCfg), reinterpret_cast<char*>(&mCfg) + sizeof(mCfg), 0);
}
void setup() {
@ -308,18 +316,18 @@ class settings {
DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE);
JsonObject root = json.to<JsonObject>();
json[F("version")] = CONFIG_VERSION;
jsonNetwork(root.createNestedObject(F("wifi")), true);
jsonNrf(root.createNestedObject(F("nrf")), true);
jsonNetwork(root[F("wifi")].to<JsonObject>(), true);
jsonNrf(root[F("nrf")].to<JsonObject>(), true);
#if defined(ESP32)
jsonCmt(root.createNestedObject(F("cmt")), true);
jsonCmt(root[F("cmt")].to<JsonObject>(), true);
#endif
jsonNtp(root.createNestedObject(F("ntp")), true);
jsonSun(root.createNestedObject(F("sun")), true);
jsonSerial(root.createNestedObject(F("serial")), true);
jsonMqtt(root.createNestedObject(F("mqtt")), true);
jsonLed(root.createNestedObject(F("led")), true);
jsonPlugin(root.createNestedObject(F("plugin")), true);
jsonInst(root.createNestedObject(F("inst")), true);
jsonNtp(root[F("ntp")].to<JsonObject>(), true);
jsonSun(root[F("sun")].to<JsonObject>(), true);
jsonSerial(root[F("serial")].to<JsonObject>(), true);
jsonMqtt(root[F("mqtt")].to<JsonObject>(), true);
jsonLed(root[F("led")].to<JsonObject>(), true);
jsonPlugin(root[F("plugin")].to<JsonObject>(), true);
jsonInst(root[F("inst")].to<JsonObject>(), true);
DPRINT(DBG_INFO, F("memory usage: "));
DBGPRINTLN(String(json.memoryUsage()));
@ -371,9 +379,9 @@ class settings {
memcpy(&tmp, &mCfg.sys, sizeof(cfgSys_t));
}
// erase all settings and reset to default
memset(&mCfg, 0, sizeof(settings_t));
std::fill(reinterpret_cast<char*>(&mCfg), reinterpret_cast<char*>(&mCfg) + sizeof(mCfg), 0);
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT;
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY;
mCfg.sys.darkMode = false;
mCfg.sys.schedReboot = false;
// restore temp settings
@ -389,6 +397,8 @@ class settings {
#endif /* !defined(ETHERNET) */
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME);
mCfg.sys.region = 0; // Europe
mCfg.sys.timezone = 1;
mCfg.nrf.pinCs = DEF_NRF_CS_PIN;
mCfg.nrf.pinCe = DEF_NRF_CE_PIN;
@ -420,12 +430,14 @@ class settings {
mCfg.sun.lat = 0.0;
mCfg.sun.lon = 0.0;
mCfg.sun.offsetSec = 0;
mCfg.sun.offsetSecMorning = 0;
mCfg.sun.offsetSecEvening = 0;
mCfg.serial.showIv = false;
mCfg.serial.debug = false;
mCfg.serial.privacyLog = true;
mCfg.serial.printWholeTrace = false;
mCfg.serial.log2mqtt = false;
mCfg.mqtt.port = DEF_MQTT_PORT;
snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER);
@ -441,24 +453,25 @@ class settings {
mCfg.inst.startWithoutTime = false;
mCfg.inst.rstMaxValsMidNight = false;
mCfg.inst.yieldEffiency = 1.0f;
mCfg.inst.gapMs = 500;
mCfg.inst.readGrid = true;
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;
}
mCfg.led.led0 = DEF_LED0;
mCfg.led.led1 = DEF_LED1;
mCfg.led.led[0] = DEF_LED0;
mCfg.led.led[1] = DEF_LED1;
mCfg.led.led[2] = DEF_LED2;
mCfg.led.high_active = LED_HIGH_ACTIVE;
mCfg.led.luminance = 255;
mCfg.plugin.display.pwrSaveAtIvOffline = false;
mCfg.plugin.display.contrast = 60;
mCfg.plugin.display.contrast = 140;
mCfg.plugin.display.screenSaver = 1; // default: 1 .. pixelshift for OLED for downward compatibility
mCfg.plugin.display.graph_ratio = 0;
mCfg.plugin.display.graph_size = 2;
mCfg.plugin.display.rot = 0;
mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA
mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL
@ -466,7 +479,7 @@ class settings {
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;
mCfg.plugin.display.pirPin = DEF_PIN_OFF;
}
void loadAddedDefaults() {
@ -477,25 +490,30 @@ class settings {
}
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;
}
if(mCfg.configVersion < 8) {
mCfg.sun.offsetSecEvening = mCfg.sun.offsetSecMorning;
}
if(mCfg.configVersion < 10) {
mCfg.sys.region = 0; // Europe
mCfg.sys.timezone = 1;
}
if(mCfg.configVersion < 11) {
mCfg.serial.log2mqtt = false;
}
}
}
@ -521,6 +539,8 @@ class settings {
obj[F("prot_mask")] = mCfg.sys.protectionMask;
obj[F("dark")] = mCfg.sys.darkMode;
obj[F("reb")] = (bool) mCfg.sys.schedReboot;
obj[F("region")] = mCfg.sys.region;
obj[F("timezone")] = mCfg.sys.timezone;
ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf);
ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf);
ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
@ -538,6 +558,8 @@ class settings {
getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask);
getVal<bool>(obj, F("dark"), &mCfg.sys.darkMode);
getVal<bool>(obj, F("reb"), &mCfg.sys.schedReboot);
getVal<uint8_t>(obj, F("region"), &mCfg.sys.region);
getVal<int8_t>(obj, F("timezone"), &mCfg.sys.timezone);
if(obj.containsKey(F("ip"))) ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>());
if(obj.containsKey(F("mask"))) ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>());
if(obj.containsKey(F("dns1"))) ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>());
@ -546,7 +568,7 @@ class settings {
if(mCfg.sys.protectionMask == 0)
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT;
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY;
}
}
@ -625,11 +647,13 @@ class settings {
if(set) {
obj[F("lat")] = mCfg.sun.lat;
obj[F("lon")] = mCfg.sun.lon;
obj[F("offs")] = mCfg.sun.offsetSec;
obj[F("offs")] = mCfg.sun.offsetSecMorning;
obj[F("offsEve")] = mCfg.sun.offsetSecEvening;
} else {
getVal<float>(obj, F("lat"), &mCfg.sun.lat);
getVal<float>(obj, F("lon"), &mCfg.sun.lon);
getVal<uint16_t>(obj, F("offs"), &mCfg.sun.offsetSec);
getVal<int16_t>(obj, F("offs"), &mCfg.sun.offsetSecMorning);
getVal<int16_t>(obj, F("offsEve"), &mCfg.sun.offsetSecEvening);
}
}
@ -639,11 +663,13 @@ class settings {
obj[F("debug")] = mCfg.serial.debug;
obj[F("prv")] = (bool) mCfg.serial.privacyLog;
obj[F("trc")] = (bool) mCfg.serial.printWholeTrace;
obj[F("mqtt")] = (bool) mCfg.serial.log2mqtt;
} else {
getVal<bool>(obj, F("show"), &mCfg.serial.showIv);
getVal<bool>(obj, F("debug"), &mCfg.serial.debug);
getVal<bool>(obj, F("prv"), &mCfg.serial.privacyLog);
getVal<bool>(obj, F("trc"), &mCfg.serial.printWholeTrace);
getVal<bool>(obj, F("mqtt"), &mCfg.serial.log2mqtt);
}
}
@ -670,13 +696,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("0")] = mCfg.led.led[0];
obj[F("1")] = mCfg.led.led[1];
obj[F("2")] = mCfg.led.led[2];
obj[F("act_high")] = mCfg.led.high_active;
obj[F("lum")] = mCfg.led.luminance;
} else {
getVal<uint8_t>(obj, F("0"), &mCfg.led.led0);
getVal<uint8_t>(obj, F("1"), &mCfg.led.led1);
getVal<uint8_t>(obj, F("0"), &mCfg.led.led[0]);
getVal<uint8_t>(obj, F("1"), &mCfg.led.led[1]);
getVal<uint8_t>(obj, F("2"), &mCfg.led.led[2]);
getVal<bool>(obj, F("act_high"), &mCfg.led.high_active);
getVal<uint8_t>(obj, F("lum"), &mCfg.led.luminance);
}
@ -688,6 +716,8 @@ class settings {
disp[F("type")] = mCfg.plugin.display.type;
disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline;
disp[F("screenSaver")] = mCfg.plugin.display.screenSaver;
disp[F("graph_ratio")] = mCfg.plugin.display.graph_ratio;
disp[F("graph_size")] = mCfg.plugin.display.graph_size;
disp[F("rotation")] = mCfg.plugin.display.rot;
//disp[F("wake")] = mCfg.plugin.display.wakeUp;
//disp[F("sleep")] = mCfg.plugin.display.sleepAt;
@ -699,11 +729,15 @@ class settings {
disp[F("busy")] = mCfg.plugin.display.disp_busy;
disp[F("dc")] = mCfg.plugin.display.disp_dc;
disp[F("pirPin")] = mCfg.plugin.display.pirPin;
obj[F("cst_lnk")] = mCfg.plugin.customLink;
obj[F("cst_lnk_txt")] = mCfg.plugin.customLinkText;
} else {
JsonObject disp = obj["disp"];
getVal<uint8_t>(disp, F("type"), &mCfg.plugin.display.type);
getVal<bool>(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline);
getVal<uint8_t>(disp, F("screenSaver"), &mCfg.plugin.display.screenSaver);
getVal<uint8_t>(disp, F("graph_ratio"), &mCfg.plugin.display.graph_ratio);
getVal<uint8_t>(disp, F("graph_size"), &mCfg.plugin.display.graph_size);
getVal<uint8_t>(disp, F("rotation"), &mCfg.plugin.display.rot);
//mCfg.plugin.display.wakeUp = disp[F("wake")];
//mCfg.plugin.display.sleepAt = disp[F("sleep")];
@ -715,6 +749,8 @@ class settings {
getVal<uint8_t>(disp, F("busy"), &mCfg.plugin.display.disp_busy);
getVal<uint8_t>(disp, F("dc"), &mCfg.plugin.display.disp_dc);
getVal<uint8_t>(disp, F("pirPin"), &mCfg.plugin.display.pirPin);
getChar(obj, F("cst_lnk"), mCfg.plugin.customLink, MAX_CUSTOM_LINK_LEN);
getChar(obj, F("cst_lnk_txt"), mCfg.plugin.customLinkText, MAX_CUSTOM_LINK_TEXT_LEN);
}
}
@ -728,7 +764,6 @@ class settings {
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 {
@ -740,7 +775,6 @@ class settings {
getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime);
getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstMaxValsMidNight);
getVal<float>(obj, F("yldEff"), &mCfg.inst.yieldEffiency);
getVal<uint16_t>(obj, F("gap"), &mCfg.inst.gapMs);
getVal<bool>(obj, F("rdGrid"), &mCfg.inst.readGrid);
if(mCfg.inst.yieldEffiency < 0.5)
@ -769,7 +803,6 @@ class settings {
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];
@ -782,7 +815,6 @@ class settings {
getVal<uint8_t>(obj, F("freq"), &cfg->frequency);
getVal<uint8_t>(obj, F("pa"), &cfg->powerLevel);
getVal<bool>(obj, F("dis"), &cfg->disNightCom);
getVal<bool>(obj, F("add"), &cfg->add2Total);
uint8_t size = 4;
if(obj.containsKey(F("pwr")))
size = obj[F("pwr")].size();
@ -796,8 +828,10 @@ class settings {
#if defined(ESP32)
void getChar(JsonObject obj, const char *key, char *dst, int maxLen) {
if(obj.containsKey(key))
if(obj.containsKey(key)) {
snprintf(dst, maxLen, "%s", obj[key].as<const char*>());
dst[maxLen-1] = '\0';
}
}
template<typename T=uint8_t>
@ -807,8 +841,10 @@ class settings {
}
#else
void getChar(JsonObject obj, const __FlashStringHelper *key, char *dst, int maxLen) {
if(obj.containsKey(key))
if(obj.containsKey(key)) {
snprintf(dst, maxLen, "%s", obj[key].as<const char*>());
dst[maxLen-1] = '\0';
}
}
template<typename T=uint8_t>
@ -819,7 +855,7 @@ class settings {
#endif
settings_t mCfg;
bool mLastSaveSucceed;
bool mLastSaveSucceed = 0;
};
#endif /*__SETTINGS_H__*/

21
src/defines.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -13,7 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 8
#define VERSION_PATCH 36
#define VERSION_PATCH 83
//-------------------------------------
typedef struct {
@ -78,6 +78,18 @@ union serial_u {
enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
enum {
DISP_TYPE_T0_NONE = 0,
DISP_TYPE_T1_SSD1306_128X64 = 1,
DISP_TYPE_T2_SH1106_128X64 = 2,
DISP_TYPE_T3_PCD8544_84X48 = 3,
DISP_TYPE_T4_SSD1306_128X32 = 4,
DISP_TYPE_T5_SSD1306_64X48 = 5,
DISP_TYPE_T6_SSD1309_128X64 = 6,
DISP_TYPE_T10_EPAPER = 10
};
//-------------------------------------
// EEPROM
//-------------------------------------
@ -94,7 +106,6 @@ 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;
@ -103,6 +114,10 @@ typedef struct {
uint32_t frmCnt;
uint32_t txCnt;
uint32_t retransmits;
uint16_t ivLoss; // lost frames (from GetLossRate)
uint16_t ivSent; // sent frames (from GetLossRate)
uint16_t dtuLoss; // current DTU lost frames (since last GetLossRate)
uint16_t dtuSent; // current DTU sent frames (since last GetLossRate)
} statistics_t;
#endif /*__DEFINES_H__*/

6
src/eth/ahoyeth.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#if defined(ETHERNET)
@ -49,7 +49,7 @@ class ahoyeth {
#if defined(CONFIG_IDF_TARGET_ESP32S3)
EthSpi mEthSpi;
#endif
settings_t *mConfig;
settings_t *mConfig = nullptr;
uint32_t *mUtcTimestamp;
AsyncUDP mUdp; // for time server

26
src/hm/CommQueue.h

@ -11,11 +11,13 @@
#include "hmInverter.h"
#include "../utils/dbg.h"
#define DEFAULT_ATTEMPS 5
#define MORE_ATTEMPS_ALARMDATA 3 // 8
#define MORE_ATTEMPS_GRIDONPROFILEPARA 0 // 5
template <uint8_t N=100>
class CommQueue {
public:
CommQueue() {}
void addImportant(Inverter<> *iv, uint8_t cmd) {
dec(&mRdPtr);
mQueue[mRdPtr] = queue_s(iv, cmd, true);
@ -30,12 +32,12 @@ class CommQueue {
mQueue[mWrPtr] = queue_s(iv, cmd, false);
}
uint8_t getFillState(void) {
uint8_t getFillState(void) const {
//DPRINTLN(DBG_INFO, "wr: " + String(mWrPtr) + ", rd: " + String(mRdPtr));
return abs(mRdPtr - mWrPtr);
}
uint8_t getMaxFill(void) {
uint8_t getMaxFill(void) const {
return N;
}
@ -44,11 +46,12 @@ class CommQueue {
Inverter<> *iv;
uint8_t cmd;
uint8_t attempts;
uint8_t attemptsMax;
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) {}
iv(i), cmd(c), attempts(DEFAULT_ATTEMPS), attemptsMax(DEFAULT_ATTEMPS), ts(0), isDevControl(dev) {}
};
protected:
@ -59,8 +62,10 @@ class CommQueue {
void add(const queue_s *q, bool rstAttempts = false) {
mQueue[mWrPtr] = *q;
if(rstAttempts)
mQueue[mWrPtr].attempts = 5;
if(rstAttempts) {
mQueue[mWrPtr].attempts = DEFAULT_ATTEMPS;
mQueue[mWrPtr].attemptsMax = DEFAULT_ATTEMPS;
}
inc(&mWrPtr);
}
@ -79,13 +84,14 @@ class CommQueue {
void cmdDone(bool keep = false) {
if(keep) {
mQueue[mRdPtr].attempts = 5;
mQueue[mRdPtr].attempts = DEFAULT_ATTEMPS;
mQueue[mRdPtr].attemptsMax = DEFAULT_ATTEMPS;
add(mQueue[mRdPtr]); // add to the end again
}
inc(&mRdPtr);
}
void setTs(uint32_t *ts) {
void setTs(const uint32_t *ts) {
mQueue[mRdPtr].ts = *ts;
}
@ -96,6 +102,8 @@ class CommQueue {
void incrAttempt(uint8_t attempts = 1) {
mQueue[mRdPtr].attempts += attempts;
if (mQueue[mRdPtr].attempts > mQueue[mRdPtr].attemptsMax)
mQueue[mRdPtr].attemptsMax = mQueue[mRdPtr].attempts;
}
void inc(uint8_t *ptr) {

721
src/hm/Communication.h

File diff suppressed because it is too large

57
src/hm/Heuristic.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -23,8 +23,8 @@
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
if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
return 0; // not used for other than nRF inverter types
HeuristicInv *ih = &iv->heuristics;
@ -66,6 +66,8 @@ class Heuristic {
ih->testPeriodFailCnt = 0;
}
iv->radio->mTxRetriesNext = getIvRetries(iv);
return id2Ch(ih->txRfChId);
}
@ -130,7 +132,7 @@ class Heuristic {
ih->lastRxFragments = rxFragments;
}
void printStatus(Inverter<> *iv) {
void printStatus(const Inverter<> *iv) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("Radio infos:"));
if((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) {
@ -155,8 +157,50 @@ class Heuristic {
DBGPRINTLN(String(iv->config->powerLevel));
}
uint8_t getIvRetries(const Inverter<> *iv) const {
if(iv->heuristics.rxSpeeds[0])
return RETRIES_VERYFAST_IV;
if(iv->heuristics.rxSpeeds[1])
return RETRIES_FAST_IV;
return 15;
}
void setIvRetriesGood(Inverter<> *iv, bool veryGood) {
if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
return; // not used for other than nRF inverter types
if(iv->heuristics.rxSpeedCnt[veryGood] > 9)
return;
iv->heuristics.rxSpeedCnt[veryGood]++;
iv->heuristics.rxSpeeds[veryGood] = true;
}
void setIvRetriesBad(Inverter<> *iv) {
if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
return; // not used for other than nRF inverter types
if(iv->heuristics.rxSpeedCnt[0]) {
iv->heuristics.rxSpeedCnt[0]--;
return;
}
if(iv->heuristics.rxSpeeds[0]) {
iv->heuristics.rxSpeeds[0] = false;
return;
}
if(iv->heuristics.rxSpeedCnt[1]) {
iv->heuristics.rxSpeedCnt[1]--;
return;
}
if(iv->heuristics.rxSpeeds[1]) {
iv->heuristics.rxSpeeds[1] = false;
return;
}
return;
}
private:
bool isNewTxCh(HeuristicInv *ih) {
bool isNewTxCh(const HeuristicInv *ih) const {
return ih->txRfChId != ih->lastBestTxChId;
}
@ -178,9 +222,6 @@ class Heuristic {
}
return 3; // standard
}
private:
uint8_t mChList[5] = {03, 23, 40, 61, 75};
};

26
src/hm/HeuristicInv.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -14,7 +14,27 @@
class HeuristicInv {
public:
HeuristicInv() {
memset(txRfQuality, -6, RF_MAX_CHANNEL_ID);
clear();
}
void clear() {
memset(txRfQuality, 0, RF_MAX_CHANNEL_ID);
txRfChId = 0;
lastBestTxChId = 0;
testPeriodSendCnt = 0;
testPeriodFailCnt = 0;
testChId = 0;
saveOldTestQuality = -6;
lastRxFragments = 0;
rxSpeeds[0] = false;
rxSpeeds[1] = false;
rxSpeedCnt[0] = 0;
rxSpeedCnt[1] = 0;
}
bool isTxAtMax(void) const {
return (RF_MAX_QUALITY == txRfQuality[txRfChId]);
}
public:
@ -27,6 +47,8 @@ class HeuristicInv {
uint8_t testChId = 0;
int8_t saveOldTestQuality = -6;
uint8_t lastRxFragments = 0;
bool rxSpeeds[2] = {false, false}; // is inverter responding very fast respective fast?
uint8_t rxSpeedCnt[2] = {0, 0}; // count how many messages had been received very fast respective fast (10 max)
};
#endif /*__HEURISTIC_INV_H__*/

29
src/hm/hmDefines.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HM_DEFINES_H__
@ -76,15 +76,22 @@ enum {CMD_CALC = 0xffff};
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
enum {INV_RADIO_TYPE_UNKNOWN = 0, INV_RADIO_TYPE_NRF, INV_RADIO_TYPE_CMT};
#define DURATION_ONEFRAME 50 // timeout parameter for each expected frame (ms)
//#define DURATION_RESERVE {90,120} // timeout parameter to still wait after last expected frame (ms)
#define DURATION_TXFRAME 85 // timeout parameter for first transmission and first expected frame (time to first channel switch from tx start!) (ms)
#define DURATION_LISTEN_MIN 5 // time to stay at least on a listening channel (ms)
#define DURATION_PAUSE_LASTFR 45 // how long to pause after last frame (ms)
const uint8_t duration_reserve[2] = {65, 115};
#define LIMIT_FAST_IV 85 // time limit to qualify an inverter as very fast answering inverter
#define LIMIT_VERYFAST_IV 70 // time limit to qualify an inverter as very fast answering inverter
#define LIMIT_FAST_IV_MI 35 // time limit to qualify a MI type inverter as fast answering inverter
#define LIMIT_VERYFAST_IV_MI 25 // time limit to qualify a MI type inverter as very fast answering inverter
#define RETRIES_FAST_IV 12 // how often shall a message be automatically retransmitted by the nRF (fast answering inverter)
#define RETRIES_VERYFAST_IV 9 // how often shall a message be automatically retransmitted by the nRF (very fast answering inverter)
typedef struct {

391
src/hm/hmInverter.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HM_INVERTER_H__
@ -14,6 +14,7 @@
#define MAX_GRID_LENGTH 150
#include "hmDefines.h"
#include "../appInterface.h"
#include "HeuristicInv.h"
#include "../hms/hmsDefines.h"
#include <memory>
@ -64,13 +65,28 @@ struct calcFunc_t {
func_t<T>* func; // function pointer
};
enum class MqttSentStatus : uint8_t {
NEW_DATA,
LAST_SUCCESS_SENT,
DATA_SENT
};
enum class InverterStatus : uint8_t {
OFF,
STARTING,
PRODUCING,
WAS_PRODUCING,
WAS_ON
};
template<class T=float>
struct record_t {
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
uint8_t pyldLen; // expected payload length for plausibility check
byteAssign_t* assign = nullptr; // assignment of bytes in payload
uint8_t length = 0; // length of the assignment list
T *record = nullptr; // data pointer
uint32_t ts = 0; // timestamp of last received payload
uint8_t pyldLen = 0; // expected payload length for plausibility check
MqttSentStatus mqttSentStatus = MqttSentStatus:: NEW_DATA; // indicates the current MqTT sent status
};
struct alarm_t {
@ -94,129 +110,112 @@ const calcFunc_t<T> calcFunctions[] = {
{ CALC_MPDC_CH, &calcMaxPowerDc }
};
enum class InverterStatus : uint8_t {
OFF,
STARTING,
PRODUCING,
WAS_PRODUCING,
WAS_ON
};
template <class REC_TYP>
class Inverter {
public:
uint8_t ivGen; // generation of inverter (HM / MI)
cfgIv_t *config; // stored settings
uint8_t id; // unique id
uint8_t type; // integer which refers to inverter type
uint16_t alarmMesIndex; // Last recorded Alarm Message Index
uint16_t powerLimit[2]; // limit power output
float actPowerLimit; // actual power limit
bool powerLimitAck; // acknowledged power limit (default: false)
uint8_t devControlCmd; // carries the requested cmd
serial_u radioId; // id converted to modbus
uint8_t channels; // number of PV channels (1-4)
record_t<REC_TYP> recordMeas; // structure for measured values
record_t<REC_TYP> recordInfo; // structure for info values
record_t<REC_TYP> recordHwInfo; // structure for simple (hardware) info values
record_t<REC_TYP> recordConfig; // structure for system config values
record_t<REC_TYP> recordAlarm; // structure for alarm values
bool isConnected; // shows if inverter was successfully identified (fw version and hardware info)
InverterStatus status; // indicates the current inverter status
std::array<alarm_t, 10> lastAlarm; // holds last 10 alarms
uint8_t alarmNxtWrPos; // indicates the position in array (rolling buffer)
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
uint8_t ivGen = IV_UNKNOWN; // generation of inverter (HM / MI)
uint8_t ivRadioType = INV_RADIO_TYPE_UNKNOWN; // refers to used radio (nRF24 / CMT)
cfgIv_t *config = nullptr; // stored settings
uint8_t id = 0; // unique id
uint8_t type = INV_TYPE_1CH; // integer which refers to inverter type
uint16_t alarmMesIndex = 0; // Last recorded Alarm Message Index
uint16_t powerLimit[2] = {0xffff, AbsolutNonPersistent}; // limit power output (multiplied by 10)
uint16_t actPowerLimit = 0xffff; // actual power limit
bool powerLimitAck = false; // acknowledged power limit
uint8_t devControlCmd = InitDataState; // carries the requested cmd
serial_u radioId; // id converted to modbus
uint8_t channels = 1; // number of PV channels (1-4)
record_t<REC_TYP> recordMeas; // structure for measured values
record_t<REC_TYP> recordInfo; // structure for info values
record_t<REC_TYP> recordHwInfo; // structure for simple (hardware) info values
record_t<REC_TYP> recordConfig; // structure for system config values
record_t<REC_TYP> recordAlarm; // structure for alarm values
InverterStatus status = InverterStatus::OFF; // indicates the current inverter status
std::array<alarm_t, 10> lastAlarm; // holds last 10 alarms
int8_t rssi = 0; // RSSI
uint16_t alarmCnt = 0; // counts the total number of occurred alarms
uint16_t alarmLastId = 0; // lastId which was received
uint8_t mCmd = InitDataState; // holds the command to send
bool mGotFragment = false; // shows if inverter has sent at least one fragment
uint8_t miMultiParts = 0; // helper info for MI multiframe msgs
uint8_t outstandingFrames = 0; // helper info to count difference between expected and received frames
uint8_t curFrmCnt = 0; // count received frames in current loop
bool mGotLastMsg = false; // shows if inverter has already finished transmission cycle
bool mIsSingleframeReq = false; // indicates this is a missing single frame request
Radio *radio = nullptr; // pointer to associated radio class
statistics_t radioStatistics; // information about transmitted, failed, ... packets
HeuristicInv heuristics; // heuristic information / logic
uint8_t curCmtFreq = 0; // current used CMT frequency, used to check if freq. was changed during runtime
uint32_t tsMaxAcPower = 0; // holds the timestamp when the MaxAC power was seen
bool commEnabled = true; // 'pause night communication' sets this field to false
Inverter() {
ivGen = IV_HM;
powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited
powerLimit[1] = AbsolutNonPersistent; // default power limit setting
powerLimitAck = false;
actPowerLimit = 0xffff; // init feedback from inverter to -1
mDevControlRequest = false;
devControlCmd = InitDataState;
alarmMesIndex = 0;
isConnected = false;
status = InverterStatus::OFF;
alarmNxtWrPos = 0;
alarmCnt = 0;
alarmLastId = 0;
rssi = -127;
miMultiParts = 0;
mGotLastMsg = false;
mCmd = InitDataState;
mIsSingleframeReq = false;
radio = NULL;
commEnabled = true;
mIvRxCnt = 0;
mIvTxCnt = 0;
mDtuRxCnt = 0;
mDtuTxCnt = 0;
public:
Inverter() {
memset(&radioStatistics, 0, sizeof(statistics_t));
memset(heuristics.txRfQuality, -6, 5);
memset(mOffYD, 0, sizeof(float) * 6);
memset(mLastYD, 0, sizeof(float) * 6);
mGridProfile.fill(0);
}
void tickSend(std::function<void(uint8_t cmd, bool isDevControl)> cb) {
if(mDevControlRequest) {
cb(devControlCmd, true);
if(InverterStatus::OFF != status) {
cb(devControlCmd, true);
devControlCmd = InitDataState;
} else
DPRINTLN(DBG_WARN, F("Inverter is not avail"));
mDevControlRequest = false;
} else if (IV_MI != ivGen) {
} else if (IV_MI != ivGen) { // HM / HMS / HMT
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)
if(INV_RADIO_TYPE_NRF == ivRadioType) {
// get live data until quality reaches maximum
if(!heuristics.isTxAtMax()) {
cb(RealTimeRunData_Debug, false); // get live data
return;
}
}
if(actPowerLimit == 0xffff) {
cb(SystemConfigPara, false); // power limit info
else if(InitDataState != devControlCmd) {
} else if(InitDataState != devControlCmd) {
cb(devControlCmd, false); // custom command which was received by API
devControlCmd = InitDataState;
mGetLossInterval = 1;
return;
} else if(0 == getFwVersion()) {
cb(InverterDevInform_All, false); // get firmware version
} else if(0 == getHwVersion()) {
cb(InverterDevInform_Simple, false); // get hardware version
} else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) {
cb(AlarmData, false); // get last alarms
} 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(RealTimeRunData_Debug, false); // get live data
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);
return;
}
cb(RealTimeRunData_Debug, false); // get live data
} else { // MI
cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false);
mGetLossInterval++;
if (type != INV_TYPE_4CH)
mIvRxCnt++; // statistics workaround...
if(isAvailable()) {
if(0 == getFwVersion()) {
mIvRxCnt +=2;
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
mIvRxCnt +=2;
} else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile
cb(0x10, false); // legacy GPF command
}
}
}
}
@ -234,15 +233,14 @@ class Inverter {
uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld"));
uint8_t pos = 0;
if(NULL != rec) {
uint8_t pos = 0;
for(; pos < rec->length; pos++) {
if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
break;
}
return (pos >= rec->length) ? 0xff : pos;
}
else
} else
return 0xff;
}
@ -251,41 +249,39 @@ class Inverter {
}
const char *getFieldName(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getFieldName"));
if(NULL != rec)
return fields[rec->assign[pos].fieldId];
return notAvail;
}
const char *getUnit(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getUnit"));
if(NULL != rec)
return units[rec->assign[pos].unitId];
return notAvail;
}
uint8_t getChannel(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getChannel"));
if(NULL != rec)
return rec->assign[pos].ch;
return 0;
}
bool setDevControlRequest(uint8_t cmd) {
if(isConnected) {
if(InverterStatus::OFF != status) {
mDevControlRequest = true;
devControlCmd = cmd;
//app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)"
}
return isConnected;
return (InverterStatus::OFF != status);
}
bool setDevCommand(uint8_t cmd) {
if(isConnected)
if(InverterStatus::OFF != status)
devControlCmd = cmd;
return isConnected;
return (InverterStatus::OFF != status);
}
void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) {
void addValue(uint8_t pos, const uint8_t buf[], record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:addValue"));
if(NULL != rec) {
uint8_t ptr = rec->assign[pos].start;
@ -299,6 +295,7 @@ class Inverter {
val <<= 8;
val |= buf[ptr];
} while(++ptr != end);
if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) {
// temperature, Qvar, and power factor are a signed values
rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div);
@ -322,7 +319,6 @@ class Inverter {
if(rec == &recordMeas) {
DPRINTLN(DBG_VERBOSE, "add real time");
// get last alarm message index and save it in the inverter object
if (getPosByChFld(0, FLD_EVT, rec) == pos) {
if (alarmMesIndex < rec->record[pos]) {
@ -332,31 +328,26 @@ class Inverter {
DBGPRINTLN(String(alarmMesIndex));
}
}
} else {
if (rec->assign == InfoAssignment) {
DPRINTLN(DBG_DEBUG, "add info");
// eg. fw version ...
} 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){
actPowerLimit = rec->record[pos];
DPRINT(DBG_DEBUG, F("Inverter actual power limit: "));
DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1));
}
} else if (rec->assign == AlarmDataAssignment) {
DPRINTLN(DBG_DEBUG, "add alarm");
} else
DPRINTLN(DBG_WARN, F("add with unknown assignment"));
}
else if (rec->assign == InfoAssignment) {
DPRINTLN(DBG_DEBUG, "add info");
// 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){
actPowerLimit = rec->record[pos];
DPRINT(DBG_DEBUG, F("Inverter actual power limit: "));
DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1));
}
}
else if (rec->assign == AlarmDataAssignment) {
DPRINTLN(DBG_DEBUG, "add alarm");
}
else
DPRINTLN(DBG_WARN, F("add with unknown assignment"));
}
else
} else
DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));
// update status state-machine
@ -374,18 +365,18 @@ class Inverter {
}
REC_TYP getChannelFieldValue(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
uint8_t pos = 0;
if(NULL != rec) {
uint8_t pos = 0;
for(; pos < rec->length; pos++) {
if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
break;
}
if(pos >= rec->length)
return 0;
return rec->record[pos];
}
else
} else
return 0;
}
@ -433,11 +424,14 @@ class Inverter {
status = InverterStatus::STARTING;
} else {
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
if(status != InverterStatus::OFF) {
status = InverterStatus::OFF;
actPowerLimit = 0xffff; // power limit will be read once inverter becomes available
alarmMesIndex = 0;
if(INV_RADIO_TYPE_NRF == ivRadioType)
heuristics.clear();
}
} else
status = InverterStatus::WAS_ON;
}
@ -455,6 +449,7 @@ class Inverter {
else if(InverterStatus::PRODUCING == status)
status = InverterStatus::WAS_PRODUCING;
}
return producing;
}
@ -506,16 +501,17 @@ class Inverter {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:initAssignment"));
rec->ts = 0;
rec->length = 0;
rec->mqttSentStatus = MqttSentStatus::DATA_SENT; // nothing new to transmit
switch (cmd) {
case RealTimeRunData_Debug:
if (INV_TYPE_1CH == type) {
if((IV_HM == ivGen) || (IV_MI == ivGen)) {
rec->length = (uint8_t)(HM1CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm1chAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hm1chAssignment));
rec->pyldLen = HM1CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS1CH_LIST_LEN);
rec->assign = (byteAssign_t *)hms1chAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hms1chAssignment));
rec->pyldLen = HMS1CH_PAYLOAD_LEN;
}
channels = 1;
@ -523,11 +519,11 @@ class Inverter {
else if (INV_TYPE_2CH == type) {
if((IV_HM == ivGen) || (IV_MI == ivGen)) {
rec->length = (uint8_t)(HM2CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm2chAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hm2chAssignment));
rec->pyldLen = HM2CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS2CH_LIST_LEN);
rec->assign = (byteAssign_t *)hms2chAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hms2chAssignment));
rec->pyldLen = HMS2CH_PAYLOAD_LEN;
}
channels = 2;
@ -535,18 +531,18 @@ class Inverter {
else if (INV_TYPE_4CH == type) {
if((IV_HM == ivGen) || (IV_MI == ivGen)) {
rec->length = (uint8_t)(HM4CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm4chAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hm4chAssignment));
rec->pyldLen = HM4CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS4CH_LIST_LEN);
rec->assign = (byteAssign_t *)hms4chAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hms4chAssignment));
rec->pyldLen = HMS4CH_PAYLOAD_LEN;
}
channels = 4;
}
else if (INV_TYPE_6CH == type) {
rec->length = (uint8_t)(HMT6CH_LIST_LEN);
rec->assign = (byteAssign_t *)hmt6chAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hmt6chAssignment));
rec->pyldLen = HMT6CH_PAYLOAD_LEN;
channels = 6;
}
@ -559,22 +555,22 @@ class Inverter {
break;
case InverterDevInform_All:
rec->length = (uint8_t)(HMINFO_LIST_LEN);
rec->assign = (byteAssign_t *)InfoAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<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->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(SimpleInfoAssignment));
rec->pyldLen = HMSIMPLE_INFO_PAYLOAD_LEN;
break;
case SystemConfigPara:
rec->length = (uint8_t)(HMSYSTEM_LIST_LEN);
rec->assign = (byteAssign_t *)SystemConfigParaAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(SystemConfigParaAssignment));
rec->pyldLen = HMSYSTEM_PAYLOAD_LEN;
break;
case AlarmData:
rec->length = (uint8_t)(HMALARMDATA_LIST_LEN);
rec->assign = (byteAssign_t *)AlarmDataAssignment;
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(AlarmDataAssignment));
rec->pyldLen = HMALARMDATA_PAYLOAD_LEN;
break;
default:
@ -590,7 +586,7 @@ class Inverter {
void resetAlarms() {
lastAlarm.fill({0, 0, 0});
alarmNxtWrPos = 0;
mAlarmNxtWrPos = 0;
alarmCnt = 0;
alarmLastId = 0;
@ -598,26 +594,49 @@ class Inverter {
memset(mLastYD, 0, sizeof(float) * 6);
}
bool parseGetLossRate(uint8_t pyld[], uint8_t len) {
bool parseGetLossRate(const 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
if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past
radioStatistics.ivSent = mDtuTxCnt;
if (rxCnt < mIvRxCnt) // overflow
radioStatistics.ivLoss = radioStatistics.ivSent - (rxCnt + ((uint16_t)65535 - mIvRxCnt) + 1);
else
radioStatistics.ivLoss = radioStatistics.ivSent - (rxCnt - mIvRxCnt);
if (txCnt < mIvTxCnt) // overflow
radioStatistics.dtuSent = txCnt + ((uint16_t)65535 - mIvTxCnt) + 1;
else
radioStatistics.dtuSent = txCnt - mIvTxCnt;
radioStatistics.dtuLoss = radioStatistics.dtuSent - mDtuRxCnt;
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));
DBGPRINT(F("Inv loss: "));
DBGPRINT(String(radioStatistics.ivLoss));
DBGPRINT(F(" of "));
DBGPRINT(String(radioStatistics.ivSent));
DBGPRINT(F(", DTU loss: "));
DBGPRINT(String(radioStatistics.dtuLoss));
DBGPRINT(F(" of "));
if(mAckCount) {
DBGPRINT(String(radioStatistics.dtuSent));
DBGPRINT(F(". ACKs: "));
DBGPRINTLN(String(mAckCount));
mAckCount = 0;
} else
DBGPRINTLN(String(radioStatistics.dtuSent));
}
mIvRxCnt = rxCnt;
mIvTxCnt = txCnt;
mIvRxCnt = rxCnt;
mIvTxCnt = txCnt;
mDtuRxCnt = 0; // start new interval
mDtuTxCnt = 0; // start new interval
return true;
}
return false;
}
@ -774,7 +793,7 @@ class Inverter {
void addGridProfile(uint8_t buf[], uint8_t length) {
mGridLen = (length > MAX_GRID_LENGTH) ? MAX_GRID_LENGTH : length;
std::copy(buf, &buf[mGridLen], mGridProfile);
std::copy(buf, &buf[mGridLen], mGridProfile.data());
}
String getGridProfile(void) {
@ -789,9 +808,9 @@ class Inverter {
private:
inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) {
lastAlarm[alarmNxtWrPos] = alarm_t(code, start, end);
if(++alarmNxtWrPos >= 10) // rolling buffer
alarmNxtWrPos = 0;
lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end);
if(++mAlarmNxtWrPos >= 10) // rolling buffer
mAlarmNxtWrPos = 0;
}
void toRadioId(void) {
@ -804,11 +823,23 @@ class Inverter {
radioId.b[0] = 0x01;
}
public:
static uint32_t *timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup
uint16_t mDtuRxCnt = 0;
uint16_t mDtuTxCnt = 0;
uint8_t mGetLossInterval = 0; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug
uint16_t mIvRxCnt = 0;
uint16_t mIvTxCnt = 0;
uint16_t mAckCount = 0;
private:
float mOffYD[6], mLastYD[6];
bool mDevControlRequest; // true if change needed
bool mDevControlRequest = false; // true if change needed
uint8_t mGridLen = 0;
uint8_t mGridProfile[MAX_GRID_LENGTH];
std::array<uint8_t, MAX_GRID_LENGTH> mGridProfile;
uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer)
};
template <class REC_TYP>
@ -920,8 +951,10 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
acMaxPower = iv->getValue(i, rec);
}
}
if(acPower > acMaxPower)
if(acPower > acMaxPower) {
iv->tsMaxAcPower = *iv->timestamp;
return acPower;
}
}
return acMaxPower;
}

263
src/hm/hmRadio.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -15,7 +15,6 @@
#endif
#define SPI_SPEED 1000000
#define RF_CHANNELS 5
const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
@ -48,12 +47,12 @@ class HmRadio : public Radio {
pinMode(irq, INPUT_PULLUP);
mSerialDebug = serialDebug;
mPrivacyMode = privacyMode;
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;
mDtuRadioId = ((uint64_t)(((mDtuSn >> 24) & 0xFF) | ((mDtuSn >> 8) & 0xFF00) | ((mDtuSn << 8) & 0xFF0000) | ((mDtuSn << 24) & 0xFF000000)) << 8) | 0x01;
#ifdef ESP32
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
@ -78,21 +77,16 @@ class HmRadio : public Radio {
#else
mNrf24->begin(mSpi.get(), ce, cs);
#endif
mNrf24->setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms
mNrf24->setRetries(3, 9); // wait 3*250 = 750us, 16 * 250us -> 4000us = 4ms
mNrf24->setChannel(mRfChLst[mRxChIdx]);
mNrf24->startListening();
mNrf24->setDataRate(RF24_250KBPS);
mNrf24->setAutoAck(true);
mNrf24->enableDynamicAck();
//mNrf24->setAutoAck(true); // enabled by default
//mNrf24->enableDynamicAck();
mNrf24->enableDynamicPayloads();
mNrf24->setCRCLength(RF24_CRC_16);
mNrf24->setAddressWidth(5);
mNrf24->openReadingPipe(1, reinterpret_cast<uint8_t*>(&DTU_RADIO_ID));
// enable all receiving interrupts
mNrf24->maskIRQ(false, false, false);
mNrf24->openReadingPipe(1, reinterpret_cast<uint8_t*>(&mDtuRadioId));
mNrf24->maskIRQ(false, false, false); // enable all receiving interrupts
mNrf24->setPALevel(1); // low is default
if(mNrf24->isChipConnected()) {
@ -104,70 +98,101 @@ class HmRadio : public Radio {
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
void loop(void) {
if (!mIrqRcvd)
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
// start listening
uint8_t chOffset = 2;
mRxChIdx = (mTxChIdx + chOffset) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
mNrf24->startListening();
// returns true if communication is active
bool loop(void) override {
if (!mIrqRcvd && !mNRFisInRX)
return false; // first quick check => nothing to do at all here
if(NULL == mLastIv) // prevent reading on NULL object!
return;
return false;
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;
if(!mIrqRcvd) { // no news from nRF, check timers
if ((millis() - mTimeslotStart) < innerLoopTimeout)
return true; // nothing to do, still waiting
if (mRadioWaitTime.isTimeout()) { // timeout reached!
mNRFisInRX = false;
rx_ready = false;
return false;
}
while ((millis() - loopMillis) < outerLoopTimeout) {
uint32_t startMicros = micros();
while ((micros() - startMicros) < innerLoopTimeout) { // listen (4088us or?) 5110us to each channel
if (mIrqRcvd) {
mIrqRcvd = false;
// otherwise switch to next RX channel
mTimeslotStart = millis();
if(!mNRFloopChannels && ((mTimeslotStart - mLastIrqTime) > (DURATION_TXFRAME))) //(DURATION_TXFRAME+DURATION_ONEFRAME)))
mNRFloopChannels = true;
if (getReceived()) { // everything received
return;
}
mRxPendular = !mRxPendular;
innerLoopTimeout = DURATION_LISTEN_MIN;
innerLoopTimeout = 4088*5;
if (isRxInit) {
isRxInit = false;
if (micros() - startMicros < 42000) {
innerLoopTimeout = 4088*12;
mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
}
}
if(mNRFloopChannels)
tempRxChIdx = (tempRxChIdx + 4) % RF_CHANNELS;
else
tempRxChIdx = (mRxChIdx + mRxPendular*4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[tempRxChIdx]);
isRxInit = false;
startMicros = micros();
return true; // communicating, but changed RX channel
} else {
// here we got news from the nRF
mIrqRcvd = false;
mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
mLastIrqTime = millis();
if(tx_ok || tx_fail) { // tx related interrupt, basically we should start listening
mNrf24->flush_tx(); // empty TX FIFO
//mTxSetupTime = millis() - mMillis;
if(mNRFisInRX) {
DPRINTLN(DBG_WARN, F("unexpected tx irq!"));
return false;
}
yield();
mNRFisInRX = true;
if(tx_ok)
mLastIv->mAckCount++;
rxOffset = mLastIv->ivGen == IV_HM ? 3 : 2; // holds the default channel offset between tx and rx channel (nRF only)
mRxChIdx = (mTxChIdx + rxOffset) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
mNrf24->startListening();
mTimeslotStart = millis();
tempRxChIdx = mRxChIdx; // might be better to start off with one channel less?
mRxPendular = false;
mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1);
innerLoopTimeout = DURATION_LISTEN_MIN;
}
if(rx_ready) {
if (getReceived()) { // check what we got, returns true for last package or success for single frame request
mNRFisInRX = false;
mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first end his transmissions
mNrf24->stopListening();
} else {
innerLoopTimeout = DURATION_LISTEN_MIN;
mTimeslotStart = millis();
if (!mNRFloopChannels) {
if (isRxInit) {
isRxInit = false;
tempRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[tempRxChIdx]);
} else
mRxChIdx = tempRxChIdx;
}
}
rx_ready = false; // reset
return mNRFisInRX;
}
// switch to next RX channel
mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[mRxChIdx]);
innerLoopTimeout = 4088;
isRxInit = false;
}
// not finished but time is over
return;
return false;
}
bool isChipConnected(void) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected"));
bool isChipConnected(void) const override {
return mNrf24->isChipConnected();
}
void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) {
void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("sendControlPacket cmd: "));
DBGHEXLN(cmd);
@ -177,10 +202,10 @@ class HmRadio : public Radio {
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[cnt++] = 0x00;
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit
mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit
mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction)
mTxBuf[cnt++] = (data[0] ) & 0xff; // power limit
mTxBuf[cnt++] = (data[1] >> 8) & 0xff; // setting for persistens handlings
mTxBuf[cnt++] = (data[1] ) & 0xff; // setting for persistens handling
}
} else { //MI 2nd gen. specific
uint16_t powerMax = ((iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower());
@ -249,59 +274,72 @@ class HmRadio : public Radio {
}
cnt++;
}
sendPacket(iv, cnt, isRetransmit, (IV_MI != iv->ivGen));
}
uint8_t getDataRate(void) {
uint8_t getDataRate(void) const {
if(!mNrf24->isChipConnected())
return 3; // unknown
return mNrf24->getDataRate();
}
bool isPVariant(void) {
bool isPVariant(void) const {
return mNrf24->isPVariant();
}
private:
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
bool isLastPackage = false;
bool isRetransmitAnswer = false;
rx_ready = false; // reset for ACK case
while(mNrf24->available()) {
uint8_t len;
len = mNrf24->getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed
uint8_t len = mNrf24->getDynamicPayloadSize(); // payload size > 32 -> corrupt payload
if (len > 0) {
packet_t p;
p.ch = mRfChLst[mRxChIdx];
p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len;
p.ch = mRfChLst[tempRxChIdx];
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) {
if(!checkIvSerial(p.packet, mLastIv)) {
DPRINT(DBG_WARN, "RX other inverter ");
if(*mPrivacyMode)
ah::dumpBuf(p.packet, p.len, 1, 4);
else
DPRINT(DBG_WARN, F("RX other inverter "));
if(!*mPrivacyMode)
ah::dumpBuf(p.packet, p.len);
return false;
else
DBGPRINTLN(F(""));
} else {
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
if(mLastIv->mIsSingleframeReq) // we only expect one frame here...
isRetransmitAnswer = true;
if(isLastPackage)
setExpectedFrames(p.packet[9] - ALL_FRAMES);
}
if(IV_MI == mLastIv->ivGen) {
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 MI status messages //#0 was p.packet[0] != 0x00 &&
isLastPackage = true; // response from dev control command
}
rx_ready = true; //reset in case we first read messages from other inverter or ACK zero payloads
}
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
isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received
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();
}
if(isLastPackage)
mLastIv->mGotLastMsg = true;
return isLastPackage;
return isLastPackage || isRetransmitAnswer;
}
void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) {
@ -312,17 +350,27 @@ class HmRadio : public Radio {
mTxChIdx = iv->heuristics.txRfChId;
if(*mSerialDebug) {
/*if(!isRetransmit) {
DPRINT(DBG_INFO, "last tx setup: ");
DBGPRINT(String(mTxSetupTime));
DBGPRINTLN("ms");
}*/
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("TX "));
DBGPRINT(String(len));
DBGPRINT(" CH");
if(mTxChIdx == 0)
DBGPRINT("0");
DBGPRINT(String(mRfChLst[mTxChIdx]));
DBGPRINT(F(" | "));
DBGPRINT(F(", "));
DBGPRINT(String(mTxRetriesNext));
DBGPRINT(F(" ret. | "));
if(*mPrintWholeTrace) {
if(*mPrivacyMode)
ah::dumpBuf(mTxBuf, len, 1, 4);
ah::dumpBuf(mTxBuf.data(), len, 1, 4);
else
ah::dumpBuf(mTxBuf, len);
ah::dumpBuf(mTxBuf.data(), len);
} else {
DHEX(mTxBuf[0]);
DBGPRINT(F(" "));
@ -333,24 +381,30 @@ class HmRadio : public Radio {
}
mNrf24->stopListening();
mNrf24->flush_rx();
if(!isRetransmit && (mTxRetries != mTxRetriesNext)) {
mNrf24->setRetries(3, mTxRetriesNext);
mTxRetries = mTxRetriesNext;
}
mNrf24->setChannel(mRfChLst[mTxChIdx]);
mNrf24->openWritingPipe(reinterpret_cast<uint8_t*>(&iv->radioId.u64));
mNrf24->startWrite(mTxBuf, len, false); // false = request ACK response
mNrf24->startFastWrite(mTxBuf.data(), len, false, true); // false (3) = request ACK response; true (4) reset CE to high after transmission
mMillis = millis();
mLastIv = iv;
iv->mDtuTxCnt++;
mNRFisInRX = false;
}
uint64_t getIvId(Inverter<> *iv) {
uint64_t getIvId(Inverter<> *iv) const override {
return iv->radioId.u64;
}
uint8_t getIvGen(Inverter<> *iv) {
uint8_t getIvGen(Inverter<> *iv) const override {
return iv->ivGen;
}
inline bool checkIvSerial(uint8_t buf[], Inverter<> *iv) {
inline bool checkIvSerial(const uint8_t buf[], Inverter<> *iv) {
for(uint8_t i = 1; i < 5; i++) {
if(buf[i] != iv->radioId.b[i])
return false;
@ -358,12 +412,23 @@ class HmRadio : public Radio {
return true;
}
uint64_t DTU_RADIO_ID;
uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz
uint64_t mDtuRadioId = 0ULL;
const 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;
uint8_t tempRxChIdx = 0;
bool mGotLastMsg = false;
uint32_t mMillis;
uint32_t mMillis = 0;
bool tx_ok = false, tx_fail = false, rx_ready = false;
unsigned long mTimeslotStart = 0;
unsigned long mLastIrqTime = 0;
bool mNRFloopChannels = false;
bool mNRFisInRX = false;
bool isRxInit = true;
bool mRxPendular = false;
uint32_t innerLoopTimeout = DURATION_LISTEN_MIN;
uint8_t mTxRetries = 15; // memorize last setting for mNrf24->setRetries(3, 15);
uint8_t rxOffset = 3; // holds the channel offset between tx and rx channel used for actual inverter
std::unique_ptr<SPIClass> mSpi;
std::unique_ptr<RF24> mNrf24;

42
src/hm/hmSystem.h

@ -1,11 +1,12 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HM_SYSTEM_H__
#define __HM_SYSTEM_H__
#include "../appInterface.h"
#include "hmInverter.h"
#include <functional>
@ -14,9 +15,10 @@ class HmSystem {
public:
HmSystem() {}
void setup(uint32_t *timestamp, cfgInst_t *config) {
mInverter[0].timestamp = timestamp;
void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) {
mInverter[0].timestamp = timestamp;
mInverter[0].generalConfig = config;
//mInverter[0].app = app;
}
void addInverter(uint8_t id, std::function<void(Inverter<> *iv)> cb) {
@ -49,18 +51,25 @@ class HmSystem {
}
if(iv->config->serial.b[5] == 0x11) {
if((iv->config->serial.b[4] & 0x0f) == 0x04)
if((iv->config->serial.b[4] & 0x0f) == 0x04) {
iv->ivGen = IV_HMS;
else
iv->ivRadioType = INV_RADIO_TYPE_CMT;
} else {
iv->ivGen = IV_HM;
iv->ivRadioType = INV_RADIO_TYPE_NRF;
}
}
else if((iv->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM
else if((iv->config->serial.b[4] & 0x03) == 0x02) { // MI 3rd Gen -> same as HM
iv->ivGen = IV_HM;
else // MI 2nd Gen
iv->ivRadioType = INV_RADIO_TYPE_NRF;
} else { // MI 2nd Gen
iv->ivGen = IV_MI;
iv->ivRadioType = INV_RADIO_TYPE_NRF;
}
} else if(iv->config->serial.b[5] == 0x13) {
iv->ivGen = IV_HMT;
iv->type = INV_TYPE_6CH;
iv->ivRadioType = INV_RADIO_TYPE_CMT;
} else if(iv->config->serial.u64 != 0ULL) {
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!"));
return;
@ -84,34 +93,31 @@ class HmSystem {
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!!!"));
if(IV_MI == iv->ivGen)
DPRINTLN(DBG_WARN, F("MI Inverter, has some restrictions!"));
cb(iv);
}
INVERTERTYPE *findInverter(uint8_t buf[]) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter"));
INVERTERTYPE *p;
INVERTERTYPE *findInverter(const uint8_t buf[]) {
for(uint8_t i = 0; i < MAX_INVERTER; i++) {
p = &mInverter[i];
INVERTERTYPE *p = &mInverter[i];
if((p->config->serial.b[3] == buf[0])
&& (p->config->serial.b[2] == buf[1])
&& (p->config->serial.b[1] == buf[2])
&& (p->config->serial.b[0] == buf[3]))
return p;
}
return NULL;
return nullptr;
}
INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos"));
if(pos >= MAX_INVERTER)
return NULL;
return nullptr;
else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check))
return &mInverter[pos];
else
return NULL;
return nullptr;
}
uint8_t getNumInverters(void) {

17
src/hm/nrfHal.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __NRF_HAL_H__
@ -142,8 +142,10 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
}
uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t len) override {
uint8_t data[NRF_MAX_TRANSFER_SZ];
uint8_t data[NRF_MAX_TRANSFER_SZ + 1];
data[0] = cmd;
if(len > NRF_MAX_TRANSFER_SZ)
len = NRF_MAX_TRANSFER_SZ;
memset(&data[1], 0xff, len);
request_spi();
@ -168,13 +170,16 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
}
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];
uint8_t data[NRF_MAX_TRANSFER_SZ + 1];
uint8_t len = data_len + blank_len;
data[0] = cmd;
memset(&data[1], 0xff, (data_len + blank_len));
if(len > (NRF_MAX_TRANSFER_SZ + 1))
len = (NRF_MAX_TRANSFER_SZ + 1);
memset(&data[1], 0xff, len);
request_spi();
size_t spiLen = (static_cast<size_t>(data_len) + static_cast<size_t>(blank_len) + 1u) << 3;
size_t spiLen = (static_cast<size_t>(len) + 1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,

65
src/hm/radio.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -11,8 +11,13 @@
#define ALL_FRAMES 0x80
#define SINGLE_FRAME 0x81
#include <array>
#include <atomic>
#include "../utils/dbg.h"
#include "../utils/crc.h"
#include "../utils/timemonitor.h"
enum { IRQ_UNKNOWN = 0, IRQ_OK, IRQ_ERROR };
// forward declaration of class
template <class REC_TYP=float>
@ -24,12 +29,17 @@ class Radio {
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 bool isChipConnected(void) const { return false; }
virtual uint16_t getBaseFreqMhz() { return 0; }
virtual uint16_t getBootFreqMhz() { return 0; }
virtual std::pair<uint16_t,uint16_t> getFreqRangeMhz(void) { return std::make_pair(0, 0); }
virtual bool loop(void) = 0;
virtual void loop(void) {};
Radio() : mTxBuf{} {}
void handleIntr(void) {
mIrqRcvd = true;
mIrqOk = IRQ_OK;
}
void sendCmdPacket(Inverter<> *iv, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) {
@ -39,8 +49,10 @@ class Radio {
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));
if(*mSerialDebug) {
DPRINT(DBG_DEBUG, F("legacy cmd 0x"));
DPRINTLN(DBG_DEBUG,String(cmd, HEX));
}
sendCmdPacket(iv, cmd, cmd, false, false);
return;
}
@ -59,17 +71,25 @@ class Radio {
sendPacket(iv, 24, isRetransmit);
}
uint32_t getDTUSn(void) {
uint32_t getDTUSn(void) const {
return mDtuSn;
}
void setExpectedFrames(uint8_t framesExpected) {
mFramesExpected = framesExpected;
}
public:
std::queue<packet_t> mBufCtrl;
uint8_t mIrqOk = IRQ_UNKNOWN;
TimeMonitor mRadioWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state)
uint8_t mTxRetriesNext = 15; // let heuristics tell us the next reties count (for nRF type radios only)
uint8_t mFramesExpected = 0x0c;
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;
virtual uint64_t getIvId(Inverter<> *iv) const = 0;
virtual uint8_t getIvGen(Inverter<> *iv) const = 0;
void initPacket(uint64_t ivId, uint8_t mid, uint8_t pid) {
mTxBuf[0] = mid;
@ -77,6 +97,8 @@ class Radio {
CP_U32_LittleEndian(&mTxBuf[5], mDtuSn);
mTxBuf[9] = pid;
memset(&mTxBuf[10], 0x00, (MAX_RF_PAYLOAD_SIZE-10));
if(IRQ_UNKNOWN == mIrqOk)
mIrqOk = IRQ_ERROR;
}
void updateCrcs(uint8_t *len, bool appendCrc16=true) {
@ -88,30 +110,33 @@ class Radio {
mTxBuf[(*len)++] = (crc ) & 0xff;
}
// crc over all
mTxBuf[*len] = ah::crc8(mTxBuf, *len);
mTxBuf[*len] = ah::crc8(mTxBuf.data(), *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);
chipID = (ESP.getEfuseMac() & 0xffffffff);
#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;
mDtuSn = 0;
for(int i = 0; i < (7 << 2); i += 4) {
uint8_t t = (chipID >> i) & 0x0f;
if(t > 0x09)
t -= 6;
mDtuSn |= (t << i);
}
mDtuSn |= 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
}
uint32_t mDtuSn;
volatile bool mIrqRcvd;
bool *mSerialDebug, *mPrivacyMode, *mPrintWholeTrace;
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
protected:
uint32_t mDtuSn = 0;
std::atomic<bool> mIrqRcvd = false;
bool *mSerialDebug = nullptr, *mPrivacyMode = nullptr, *mPrintWholeTrace = nullptr;
std::array<uint8_t, MAX_RF_PAYLOAD_SIZE> mTxBuf;
};
#endif /*__RADIO_H__*/

175
src/hm/simulator.h

@ -0,0 +1,175 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __SIMULATOR_H__
#define __SIMULATOR_H__
#if defined(ENABLE_SIMULATOR)
#include "../defines.h"
#include "../utils/dbg.h"
#include "../utils/helper.h"
#include "hmSystem.h"
#include "hmInverter.h"
#include "Communication.h"
template<class HMSYSTEM>
class Simulator {
public:
void setup(HMSYSTEM *sys, uint32_t *ts, uint8_t ivId = 0) {
mTimestamp = ts;
mSys = sys;
mIvId = ivId;
}
void addPayloadListener(payloadListenerType cb) {
mCbPayload = cb;
}
void tick() {
uint8_t cmd, len;
uint8_t *payload;
getPayload(&cmd, &payload, &len);
Inverter<> *iv = mSys->getInverterByPos(mIvId);
if (NULL == iv)
return;
DPRINT(DBG_INFO, F("add payload with cmd: 0x"));
DBGHEXLN(cmd);
if(GridOnProFilePara == cmd) {
iv->addGridProfile(payload, len);
return;
}
record_t<> *rec = iv->getRecordStruct(cmd);
rec->ts = *mTimestamp;
for (uint8_t i = 0; i < rec->length; i++) {
iv->addValue(i, payload, rec);
yield();
}
iv->doCalculations();
if((nullptr != mCbPayload) && (GridOnProFilePara != cmd))
(mCbPayload)(cmd, iv);
}
private:
inline void getPayload(uint8_t *cmd, uint8_t *payload[], uint8_t *len) {
switch(payloadCtrl) {
default: *cmd = RealTimeRunData_Debug; break;
case 1: *cmd = SystemConfigPara; break;
case 3: *cmd = InverterDevInform_All; break;
case 5: *cmd = InverterDevInform_Simple; break;
case 7: *cmd = GridOnProFilePara; break;
}
if(payloadCtrl < 8)
payloadCtrl++;
switch(*cmd) {
default:
case RealTimeRunData_Debug:
*payload = plRealtime;
modifyAcPwr();
*len = 62;
break;
case InverterDevInform_All:
*payload = plFirmware;
*len = 14;
break;
case InverterDevInform_Simple:
*payload = plPart;
*len = 14;
break;
case SystemConfigPara:
*payload = plLimit;
*len = 14;
break;
case AlarmData:
*payload = plAlarm;
*len = 26;
break;
case GridOnProFilePara:
*payload = plGrid;
*len = 70;
break;
}
}
inline void modifyAcPwr() {
uint16_t cur = (plRealtime[50] << 8) | plRealtime[51];
uint16_t change = cur ^ 0xa332;
if(0 == change)
change = 140;
else if(change > 200)
change = (change % 200) + 1;
if(cur > 7000)
cur -= change;
else
cur += change;
plRealtime[50] = (cur >> 8) & 0xff;
plRealtime[51] = (cur ) & 0xff;
}
private:
HMSYSTEM *mSys = nullptr;
uint8_t mIvId = 0;
uint32_t *mTimestamp = nullptr;
payloadListenerType mCbPayload = nullptr;
uint8_t payloadCtrl = 0;
private:
uint8_t plRealtime[62] = {
0x00, 0x01, 0x01, 0x24, 0x00, 0x22, 0x00, 0x23,
0x00, 0x63, 0x00, 0x65, 0x00, 0x08, 0x5c, 0xbb,
0x00, 0x09, 0x6f, 0x08, 0x00, 0x0c, 0x00, 0x0c,
0x01, 0x1e, 0x00, 0x22, 0x00, 0x21, 0x00, 0x60,
0x00, 0x5f, 0x00, 0x08, 0xdd, 0x84, 0x00, 0x09,
0x13, 0x6f, 0x00, 0x0b, 0x00, 0x0b, 0x09, 0x27,
0x13, 0x8c, 0x01, 0x75, 0x00, 0xc2, 0x00, 0x10,
0x03, 0x77, 0x00, 0x61, 0x00, 0x02
};
uint8_t plPart[14] = {
0x27, 0x1c, 0x10, 0x12, 0x10, 0x01, 0x01, 0x00,
0x0a, 0x00, 0x20, 0x01, 0x00, 0x00
};
uint8_t plFirmware[14] = {
0x00, 0x01, 0x80, 0x01, 0x00, 0x01, 0x60, 0x42,
0x60, 0x42, 0x00, 0x00, 0x00, 0x00
};
uint8_t plLimit[14] = {
0x00, 0x01, 0x03, 0xe8, 0x00, 0x00, 0x03, 0xe8,
0xff, 0xff, 0xff, 0xff, 0x01, 0x68
};
uint8_t plGrid[70] = {
0x0D, 0x00, 0x20, 0x00, 0x00, 0x08, 0x08, 0xFC,
0x07, 0x30, 0x00, 0x01, 0x0A, 0x55, 0x00, 0x01,
0x09, 0xE2, 0x10, 0x00, 0x13, 0x88, 0x12, 0x8E,
0x00, 0x01, 0x14, 0x1E, 0x00, 0x01, 0x20, 0x00,
0x00, 0x01, 0x30, 0x07, 0x01, 0x2C, 0x0A, 0x55,
0x07, 0x30, 0x14, 0x1E, 0x12, 0x8E, 0x00, 0x32,
0x00, 0x1E, 0x40, 0x00, 0x07, 0xD0, 0x00, 0x10,
0x50, 0x00, 0x00, 0x01, 0x13, 0x9C, 0x01, 0x90,
0x00, 0x10, 0x70, 0x00, 0x00, 0x01
};
uint8_t plAlarm[26] = {
0x00, 0x01, 0x80, 0x01, 0x00, 0x01, 0x51, 0xc7,
0x51, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x80, 0x02,
0x00, 0x02, 0xa6, 0xc9, 0xa6, 0xc9, 0x65, 0x3e,
0x47, 0x21
};
};
#endif /*ENABLE_SIMULATOR*/
#endif /*__SIMULATOR_H__*/

274
src/hms/cmt2300a.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -12,8 +12,23 @@
#include "esp32_3wSpi.h"
#endif
// detailed register infos from AN142_CMT2300AW_Quick_Start_Guide-Rev0.8.pdf
#include <utility>
enum class RegionCfg : uint8_t {
EUROPE, USA, BRAZIL, NUM
};
enum class CmtStatus : uint8_t {
SUCCESS = 0,
ERR_SWITCH_STATE,
ERR_TX_PENDING,
FIFO_EMPTY,
ERR_RX_IN_FIFO
};
#define FREQ_STEP_KHZ 250 // channel step size in kHz
// detailed register infos from AN142_CMT2300AW_Quick_Start_Guide-Rev0.8.pdf
#define CMT2300A_MASK_CFG_RETAIN 0x10
#define CMT2300A_MASK_RSTN_IN_EN 0x20
#define CMT2300A_MASK_LOCKING_EN 0x20
@ -152,67 +167,6 @@
#define CMT2300A_MASK_TX_DONE_FLG 0x08
#define CMT2300A_MASK_PKT_OK_FLG 0x01
// 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,
0x11, 0x02, 0x02, 0x00, 0xAE, 0xE0, 0x35, 0x00,
// 0x10 - 0x1f
0x00, 0xF4, 0x10, 0xE2, 0x42, 0x20, 0x0C, 0x81,
0x42, 0x32, 0xCF, 0x82, 0x42, 0x27, 0x76, 0x12, // 860MHz as default
// 0x20 - 0x2f
0xA6, 0xC9, 0x20, 0x20, 0xD2, 0x35, 0x0C, 0x0A,
0x9F, 0x4B, 0x29, 0x29, 0xC0, 0x14, 0x05, 0x53,
// 0x30 - 0x3f
0x10, 0x00, 0xB4, 0x00, 0x00, 0x01, 0x00, 0x00,
0x12, 0x1E, 0x00, 0xAA, 0x06, 0x00, 0x00, 0x00,
// 0x40 - 0x4f
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, 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};
class Cmt2300a {
public:
Cmt2300a() {}
@ -234,12 +188,12 @@ class Cmt2300a {
}
}
uint8_t goRx(void) {
CmtStatus goRx(void) {
if(mTxPending)
return CMT_ERR_TX_PENDING;
return CmtStatus::ERR_TX_PENDING;
if(mInRxMode)
return CMT_SUCCESS;
return CmtStatus::SUCCESS;
mSpi.readReg(CMT2300A_CUS_INT1_CTL);
mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE);
@ -260,47 +214,47 @@ class Cmt2300a {
mSpi.writeReg(0x16, 0x0C); // [4:3]: RSSI_DET_SEL, [2:0]: RSSI_AVG_MODE
if(!cmtSwitchStatus(CMT2300A_GO_RX, CMT2300A_STA_RX))
return CMT_ERR_SWITCH_STATE;
return CmtStatus::ERR_SWITCH_STATE;
mInRxMode = true;
return CMT_SUCCESS;
return CmtStatus::SUCCESS;
}
uint8_t getRx(uint8_t buf[], uint8_t *rxLen, uint8_t maxlen, int8_t *rssi) {
CmtStatus getRx(uint8_t buf[], uint8_t *rxLen, uint8_t maxlen, int8_t *rssi) {
if(mTxPending)
return CMT_ERR_TX_PENDING;
return CmtStatus::ERR_TX_PENDING;
if(0x1b != (mSpi.readReg(CMT2300A_CUS_INT_FLAG) & 0x1b))
return CMT_FIFO_EMPTY;
return CmtStatus::FIFO_EMPTY;
// receive ok (pream, sync, node, crc)
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return CMT_ERR_SWITCH_STATE;
return CmtStatus::ERR_SWITCH_STATE;
mSpi.readFifo(buf, rxLen, maxlen);
*rssi = mSpi.readReg(CMT2300A_CUS_RSSI_DBM) - 128;
if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP))
return CMT_ERR_SWITCH_STATE;
return CmtStatus::ERR_SWITCH_STATE;
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return CMT_ERR_SWITCH_STATE;
return CmtStatus::ERR_SWITCH_STATE;
mInRxMode = false;
mCusIntFlag = mSpi.readReg(CMT2300A_CUS_INT_FLAG);
return CMT_SUCCESS;
return CmtStatus::SUCCESS;
}
uint8_t tx(uint8_t buf[], uint8_t len) {
CmtStatus tx(uint8_t buf[], uint8_t len) {
if(mTxPending)
return CMT_ERR_TX_PENDING;
return CmtStatus::ERR_TX_PENDING;
if(mInRxMode) {
mInRxMode = false;
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return CMT_ERR_SWITCH_STATE;
return CmtStatus::ERR_SWITCH_STATE;
}
mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE);
@ -325,16 +279,17 @@ class Cmt2300a {
}
if(!cmtSwitchStatus(CMT2300A_GO_TX, CMT2300A_STA_TX))
return CMT_ERR_SWITCH_STATE;
return CmtStatus::ERR_SWITCH_STATE;
// wait for tx done
mTxPending = true;
return CMT_SUCCESS;
return CmtStatus::SUCCESS;
}
// initialize CMT2300A, returns true on success
bool reset(void) {
bool reset(RegionCfg region) {
mRegionCfg = region;
mSpi.writeReg(0x7f, 0xff); // soft reset
delay(30);
@ -346,9 +301,18 @@ class Cmt2300a {
if(mSpi.readReg(0x62) != 0x20)
return false; // not connected!
for(uint8_t i = 0; i < 0x60; i++) {
for(uint8_t i = 0; i < 0x18; i++) {
mSpi.writeReg(i, cmtConfig[i]);
}
for(uint8_t i = 0; i < 8; i++) {
mSpi.writeReg(0x18 + i, mBaseFreqCfg[static_cast<uint8_t>(region)][i]);
}
for(uint8_t i = 0x20; i < 0x60; i++) {
mSpi.writeReg(i, cmtConfig[i]);
}
if(RegionCfg::EUROPE != region)
mSpi.writeReg(0x27, 0x0B);
mSpi.writeReg(CMT2300A_CUS_IO_SEL, 0x20); // -> GPIO3_SEL[1:0] = 0x02
@ -389,23 +353,14 @@ class Cmt2300a {
}
inline uint8_t freq2Chan(const uint32_t freqKhz) {
if((freqKhz % FREQ_STEP_KHZ) != 0) {
DPRINT(DBG_WARN, F("switch frequency to "));
DBGPRINT(String(freqKhz));
DBGPRINT(F("kHz not possible!"));
if((freqKhz % FREQ_STEP_KHZ) != 0)
return 0xff; // error
// apply the nearest frequency
//freqKhz = (freqKhz + FREQ_STEP_KHZ/2) / FREQ_STEP_KHZ;
//freqKhz *= FREQ_STEP_KHZ;
}
if((freqKhz < HOY_BASE_FREQ_KHZ) || (freqKhz > HOY_MAX_FREQ_KHZ))
std::pair<uint16_t, uint16_t> range = getFreqRangeMhz();
if((freqKhz < (range.first * 1000)) || (freqKhz > (range.second * 1000)))
return 0xff; // error
if((freqKhz < FREQ_WARN_MIN_KHZ) || (freqKhz > FREQ_WARN_MAX_KHZ))
DPRINTLN(DBG_WARN, F("Desired frequency is out of EU legal range! (863 - 870MHz)"));
return (freqKhz - HOY_BASE_FREQ_KHZ) / FREQ_STEP_KHZ;
return (freqKhz - (getBaseFreqMhz() * 1000)) / FREQ_STEP_KHZ;
}
inline void switchChannel(uint8_t ch) {
@ -414,9 +369,9 @@ class Cmt2300a {
inline uint32_t getFreqKhz(void) {
if(0xff != mRqstCh)
return HOY_BASE_FREQ_KHZ + (mRqstCh * FREQ_STEP_KHZ);
return getBaseFreqMhz() * 1000 + (mRqstCh * FREQ_STEP_KHZ);
else
return HOY_BASE_FREQ_KHZ + (mCurCh * FREQ_STEP_KHZ);
return getBaseFreqMhz() * 1000 + (mCurCh * FREQ_STEP_KHZ);
}
uint8_t getCurrentChannel(void) {
@ -443,6 +398,114 @@ class Cmt2300a {
mSpi.writeReg(CMT2300A_CUS_TX9, paLevelList[level][1]);
}
public:
uint16_t getBaseFreqMhz(void) {
switch(mRegionCfg) {
default:
[[fallthrough]];
case RegionCfg::EUROPE:
break;
case RegionCfg::USA:
return 905;
case RegionCfg::BRAZIL:
return 915;
}
return 860;
}
uint16_t getBootFreqMhz(void) {
switch(mRegionCfg) {
default:
[[fallthrough]];
case RegionCfg::EUROPE:
break;
case RegionCfg::USA:
return 915;
case RegionCfg::BRAZIL:
return 915;
}
return 868;
}
std::pair<uint16_t,uint16_t> getFreqRangeMhz(void) {
switch(mRegionCfg) {
default:
[[fallthrough]];
case RegionCfg::EUROPE:
break;
case RegionCfg::USA:
return std::make_pair(905, 925);
case RegionCfg::BRAZIL:
return std::make_pair(915, 928);
}
return std::make_pair(860, 870); // Europe
}
private:
// this list and the TX5, TX10 registers were compiled from the output of
// HopeRF RFPDK Tool v1.54
constexpr 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
constexpr static uint8_t cmtConfig[0x60] PROGMEM {
// 0x00 - 0x0f -- RSSI offset +- 0 and 13dBm
0x00, 0x66, 0xEC, 0x1C, 0x70, 0x80, 0x14, 0x08,
0x11, 0x02, 0x02, 0x00, 0xAE, 0xE0, 0x35, 0x00,
// 0x10 - 0x1f
0x00, 0xF4, 0x10, 0xE2, 0x42, 0x20, 0x0C, 0x81,
0x42, 0x32, 0xCF, 0x82, 0x42, 0x27, 0x76, 0x12, // 860MHz as default
// 0x20 - 0x2f
0xA6, 0xC9, 0x20, 0x20, 0xD2, 0x35, 0x0C, 0x0A,
0x9F, 0x4B, 0x29, 0x29, 0xC0, 0x14, 0x05, 0x53,
// 0x30 - 0x3f
0x10, 0x00, 0xB4, 0x00, 0x00, 0x01, 0x00, 0x00,
0x12, 0x1E, 0x00, 0xAA, 0x06, 0x00, 0x00, 0x00,
// 0x40 - 0x4f
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, 0x5D, 0x0B, 0x3F, 0x7F // TX 13dBm
};
constexpr static uint8_t mBaseFreqCfg[static_cast<uint8_t>(RegionCfg::NUM)][8] {
{0x42, 0x32, 0xCF, 0x82, 0x42, 0x27, 0x76, 0x12}, // 860MHz
{0x45, 0xA8, 0x31, 0x8A, 0x45, 0x9D, 0xD8, 0x19}, // 905MHz (USA, Indonesia)
{0x46, 0x6D, 0x80, 0x86, 0x46, 0x62, 0x27, 0x16} // 915MHz (Brazil)
};
private:
void init() {
mTxPending = false;
@ -466,7 +529,8 @@ class Cmt2300a {
return false;
}
inline bool switchDtuFreq(const uint32_t freqKhz) {
// maybe used in future
/*inline bool switchDtuFreq(const uint32_t freqKhz) {
uint8_t toCh = freq2Chan(freqKhz);
if(0xff == toCh)
return false;
@ -474,22 +538,24 @@ class Cmt2300a {
switchChannel(toCh);
return true;
}
}*/
inline uint8_t getChipStatus(void) {
return mSpi.readReg(CMT2300A_CUS_MODE_STA) & CMT2300A_MASK_CHIP_MODE_STA;
}
private:
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
cmtHal mSpi;
#else
esp32_3wSpi mSpi;
#endif
uint8_t mCnt;
bool mTxPending;
bool mInRxMode;
uint8_t mCusIntFlag;
uint8_t mRqstCh, mCurCh;
uint8_t mCnt = 0;
bool mTxPending = false;
bool mInRxMode = false;
uint8_t mCusIntFlag = 0;
uint8_t mRqstCh = 0, mCurCh = 0;
RegionCfg mRegionCfg = RegionCfg::EUROPE;
};
#endif /*__CMT2300A_H__*/

4
src/hms/cmtHal.h

@ -17,7 +17,7 @@
class cmtHal : public SpiPatcherHandle {
public:
cmtHal() {
mSpiPatcher = SpiPatcher::getInstance(SPI2_HOST);
mSpiPatcher = SpiPatcher::getInstance(DEF_CMT_SPI_HOST);
}
void patch() override {
@ -89,7 +89,7 @@ class cmtHal : public SpiPatcherHandle {
}
uint8_t readReg(uint8_t addr) {
uint8_t data;
uint8_t data = 0;
request_spi();

17
src/hms/esp32_3wSpi.h

@ -10,6 +10,7 @@
#if defined(ESP32)
#include "driver/spi_master.h"
#include "esp_rom_gpio.h" // for esp_rom_gpio_connect_out_signal
#include "../config/config.h"
#define SPI_CLK 1 * 1000 * 1000 // 1MHz
@ -21,7 +22,7 @@
// for ESP32 this is the so-called HSPI
// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore,
// it is simply the first externally usable hardware SPI master controller
#define SPI_CMT SPI2_HOST
//#define SPI_CMT SPI2_HOST
class esp32_3wSpi {
public:
@ -54,8 +55,8 @@ class esp32_3wSpi {
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED));
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg));
ESP_ERROR_CHECK(spi_bus_initialize(DEF_CMT_SPI_HOST, &buscfg, SPI_DMA_DISABLED));
ESP_ERROR_CHECK(spi_bus_add_device(DEF_CMT_SPI_HOST, &devcfg, &spi_reg));
// FiFo
spi_device_interface_config_t devcfg2 = {
@ -72,9 +73,9 @@ class esp32_3wSpi {
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo));
ESP_ERROR_CHECK(spi_bus_add_device(DEF_CMT_SPI_HOST, &devcfg2, &spi_fifo));
esp_rom_gpio_connect_out_signal(pinSdio, spi_periph_signal[SPI_CMT].spid_out, true, false);
esp_rom_gpio_connect_out_signal(pinSdio, spi_periph_signal[DEF_CMT_SPI_HOST].spid_out, true, false);
delay(100);
//pinMode(pinGpio3, INPUT);
@ -104,7 +105,7 @@ class esp32_3wSpi {
if(!mInitialized)
return 0;
uint8_t rx_data;
uint8_t rx_data = 0;
spi_transaction_t t = {
.cmd = 0,
.addr = (uint64_t)(~addr),
@ -121,7 +122,7 @@ class esp32_3wSpi {
return rx_data;
}
void writeFifo(uint8_t buf[], uint8_t len) {
void writeFifo(const uint8_t buf[], uint8_t len) {
if(!mInitialized)
return;
uint8_t tx_data;
@ -144,7 +145,7 @@ class esp32_3wSpi {
void readFifo(uint8_t buf[], uint8_t *len, uint8_t maxlen) {
if(!mInitialized)
return;
uint8_t rx_data;
uint8_t rx_data = 0;
spi_transaction_t t = {
.length = 8,

97
src/hms/hmsRadio.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -9,39 +9,38 @@
#include "cmt2300a.h"
#include "../hm/radio.h"
//#define CMT_SWITCH_CHANNEL_CYCLE 5
template<uint32_t DTU_SN = 0x81001765>
class CmtRadio : public Radio {
typedef Cmt2300a CmtType;
public:
CmtRadio() {
mDtuSn = DTU_SN;
mCmtAvail = false;
}
void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t pinSclk, uint8_t pinSdio, uint8_t pinCsb, uint8_t pinFcsb, bool genDtuSn = true) {
void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t pinSclk, uint8_t pinSdio, uint8_t pinCsb, uint8_t pinFcsb, uint8_t region = 0, bool genDtuSn = true) {
mCmt.setup(pinSclk, pinSdio, pinCsb, pinFcsb);
reset(genDtuSn);
reset(genDtuSn, static_cast<RegionCfg>(region));
mPrivacyMode = privacyMode;
mSerialDebug = serialDebug;
mPrintWholeTrace = printWholeTrace;
mTxBuf.fill(0);
}
void loop() {
bool loop() override {
mCmt.loop();
if((!mIrqRcvd) && (!mRqstGetRx))
return;
return false;
getRx();
if(CMT_SUCCESS == mCmt.goRx()) {
if(CmtStatus::SUCCESS == mCmt.goRx()) {
mIrqRcvd = false;
mRqstGetRx = false;
}
return false;
}
bool isChipConnected(void) {
bool isChipConnected(void) const override {
return mCmtAvail;
}
void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) {
void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override {
DPRINT(DBG_INFO, F("sendControlPacket cmd: "));
DBGHEXLN(cmd);
initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME);
@ -50,23 +49,23 @@ class CmtRadio : public Radio {
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[cnt++] = 0x00;
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit
mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit
mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction)
mTxBuf[cnt++] = (data[0] ) & 0xff; // power limit
mTxBuf[cnt++] = (data[1] >> 8) & 0xff; // setting for persistens handlings
mTxBuf[cnt++] = (data[1] ) & 0xff; // setting for persistens handling
}
sendPacket(iv, cnt, isRetransmit);
}
bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) {
bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) override {
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) {
bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) override {
if((0xff == fromCh) || (0xff == toCh))
return false;
@ -76,9 +75,21 @@ class CmtRadio : public Radio {
return true;
}
uint16_t getBaseFreqMhz(void) override {
return mCmt.getBaseFreqMhz();
}
uint16_t getBootFreqMhz(void) override {
return mCmt.getBootFreqMhz();
}
std::pair<uint16_t,uint16_t> getFreqRangeMhz(void) override {
return mCmt.getFreqRangeMhz();
}
private:
void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) {
void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) override {
// inverters have maybe different settings regarding frequency
if(mCmt.getCurrentChannel() != iv->config->frequency)
mCmt.switchChannel(iv->config->frequency);
@ -92,9 +103,9 @@ class CmtRadio : public Radio {
DBGPRINT(F("Mhz | "));
if(*mPrintWholeTrace) {
if(*mPrivacyMode)
ah::dumpBuf(mTxBuf, len, 1, 4);
ah::dumpBuf(mTxBuf.data(), len, 1, 4);
else
ah::dumpBuf(mTxBuf, len);
ah::dumpBuf(mTxBuf.data(), len);
} else {
DHEX(mTxBuf[0]);
DBGPRINT(F(" "));
@ -104,29 +115,29 @@ class CmtRadio : public Radio {
}
}
uint8_t status = mCmt.tx(mTxBuf, len);
CmtStatus status = mCmt.tx(mTxBuf.data(), len);
mMillis = millis();
if(CMT_SUCCESS != status) {
if(CmtStatus::SUCCESS != status) {
DPRINT(DBG_WARN, F("CMT TX failed, code: "));
DBGPRINTLN(String(status));
if(CMT_ERR_RX_IN_FIFO == status)
DBGPRINTLN(String(static_cast<uint8_t>(status)));
if(CmtStatus::ERR_RX_IN_FIFO == status)
mIrqRcvd = true;
}
iv->mDtuTxCnt++;
}
uint64_t getIvId(Inverter<> *iv) {
uint64_t getIvId(Inverter<> *iv) const override {
return iv->radioId.u64;
}
uint8_t getIvGen(Inverter<> *iv) {
uint8_t getIvGen(Inverter<> *iv) const override {
return iv->ivGen;
}
inline void reset(bool genDtuSn) {
inline void reset(bool genDtuSn, RegionCfg region) {
if(genDtuSn)
generateDtuSn();
if(!mCmt.reset()) {
if(!mCmt.reset(region)) {
mCmtAvail = false;
DPRINTLN(DBG_WARN, F("Initializing CMT2300A failed!"));
} else {
@ -134,11 +145,15 @@ class CmtRadio : public Radio {
mCmt.goRx();
}
mIrqRcvd = false;
mRqstGetRx = false;
mIrqRcvd = false;
mRqstGetRx = false;
}
inline void sendSwitchChCmd(Inverter<> *iv, uint8_t ch) {
//if(CMT_SWITCH_CHANNEL_CYCLE > ++mSwitchCycle)
// return;
//mSwitchCycle = 0;
/** ch:
* 0x00: 860.00 MHz
* 0x01: 860.25 MHz
@ -160,15 +175,23 @@ class CmtRadio : public Radio {
inline void getRx(void) {
packet_t p;
p.millis = millis() - mMillis;
uint8_t status = mCmt.getRx(p.packet, &p.len, 28, &p.rssi);
if(CMT_SUCCESS == status)
if(CmtStatus::SUCCESS == mCmt.getRx(p.packet, &p.len, 28, &p.rssi)) {
//mSwitchCycle = 0;
p.ch = 0; // not used for CMT inverters
mBufCtrl.push(p);
}
if(p.packet[9] > ALL_FRAMES) { // indicates last frame
setExpectedFrames(p.packet[9] - ALL_FRAMES);
mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first get back to rx mode?
}
}
CmtType mCmt;
bool mRqstGetRx;
bool mCmtAvail;
uint32_t mMillis;
bool mCmtAvail = false;
bool mRqstGetRx = false;
uint32_t mMillis = 0;
//uint8_t mSwitchCycle = 0;
};
#endif /*__HMS_RADIO_H__*/

315
src/platformio.ini

@ -22,16 +22,17 @@ extra_scripts =
pre:../scripts/auto_firmware_version.py
pre:../scripts/convertHtml.py
pre:../scripts/applyPatches.py
pre:../scripts/reduceGxEPD2.py
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24 @ 1.4.8
https://github.com/nRF24/RF24 @ 1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.5.0
https://github.com/bertmelis/espMqttClient#v1.6.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
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
build_flags =
-std=c++17
-std=gnu++17
@ -45,21 +46,66 @@ board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
;-Wl,-Map,output.map
monitor_filters =
esp8266_exception_decoder
[env:esp8266-de]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
;-Wl,-Map,output.map
monitor_filters =
esp8266_exception_decoder
[env:esp8266-prometheus]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
[env:esp8266-prometheus-de]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_PROMETHEUS_EP
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
[env:esp8266-minimal]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
;-Wl,-Map,output.map
monitor_filters =
esp8266_exception_decoder
[env:esp8285]
platform = espressif8266
board = esp8285
@ -67,23 +113,79 @@ board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
[env:esp8285-de]
platform = espressif8266
board = esp8285
board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
[env:esp32-wroom32]
platform = espressif32@6.4.0
platform = espressif32@6.5.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-minimal]
platform = espressif32@6.5.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-de]
platform = espressif32@6.5.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DLANG_DE
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-prometheus]
platform = espressif32@6.4.0
platform = espressif32@6.5.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-prometheus-de]
platform = espressif32@6.5.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DLANG_DE
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
@ -93,56 +195,199 @@ board = esp32dev
lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500
khoih-prog/AsyncUDP_ESP32_W5500
nrf24/RF24 @ ^1.4.8
https://github.com/nRF24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
build_flags = ${env.build_flags}
-D ETHERNET
-DRELEASE
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-ethernet-de]
platform = espressif32
board = esp32dev
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
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.7
zinggjm/GxEPD2 @ ^1.5.2
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
build_flags = ${env.build_flags}
-D ETHERNET
-DRELEASE
-DUSE_HSPI_FOR_EPD
-DLOG_LOCAL_LEVEL=ESP_LOG_INFO
-DDEBUG_LEVEL=DBG_INFO
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[env:esp32-s2-mini]
platform = espressif32@6.4.0
platform = espressif32@6.5.0
board = lolin_s2_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-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
-DDEF_CMT_CSB=16
-DDEF_CMT_FCSB=18
-DDEF_CMT_IRQ=33
-DDEF_CMT_SDIO=35
-DDEF_CMT_SCLK=37
monitor_filters =
esp32_exception_decoder
[env:esp32-s2-mini-de]
platform = espressif32@6.5.0
board = lolin_s2_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-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
-DDEF_CMT_CSB=16
-DDEF_CMT_FCSB=18
-DDEF_CMT_IRQ=33
-DDEF_CMT_SDIO=35
-DDEF_CMT_SCLK=37
-DLANG_DE
monitor_filters =
esp32_exception_decoder
[env:esp32-c3-mini]
platform = espressif32@6.4.0
platform = espressif32@6.5.0
board = lolin_c3_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-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
-DDEF_CMT_CSB=255
-DDEF_CMT_FCSB=255
-DDEF_CMT_IRQ=255
-DDEF_CMT_SDIO=255
-DDEF_CMT_SCLK=255
monitor_filters =
esp32_exception_decoder
[env:esp32-c3-mini-de]
platform = espressif32@6.5.0
board = lolin_c3_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-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
-DDEF_CMT_CSB=255
-DDEF_CMT_FCSB=255
-DDEF_CMT_IRQ=255
-DDEF_CMT_SDIO=255
-DDEF_CMT_SCLK=255
-DLANG_DE
monitor_filters =
esp32_exception_decoder
[env:opendtufusion]
platform = espressif32@6.4.0
platform = espressif32@6.5.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DSPI_HAL
-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-de]
platform = espressif32@6.5.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DSPI_HAL
-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-minimal]
platform = espressif32@6.5.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DSPI_HAL
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
@ -162,23 +407,25 @@ monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-ethernet]
platform = espressif32@6.4.0
platform = espressif32@6.5.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
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.7
zinggjm/GxEPD2 @ ^1.5.2
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DETHERNET
-DSPI_HAL
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DDEF_ETH_CS_PIN=42
-DDEF_ETH_SCK_PIN=39
-DDEF_ETH_MISO_PIN=41
@ -204,20 +451,33 @@ build_flags = ${env.build_flags}
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-dev]
platform = espressif32@6.4.0
[env:opendtufusion-ethernet-de]
platform = espressif32@6.5.0
board = esp32-s3-devkitc-1
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
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
https://github.com/bertmelis/espMqttClient#v1.6.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
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DETHERNET
-DSPI_HAL
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-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
@ -233,7 +493,6 @@ build_flags = ${env.build_flags}
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DSPI_HAL
#-DARDUINO_USB_CDC_ON_BOOT=1
monitor_filters =
esp32_exception_decoder, colorize

400
src/plugins/Display/Display.h

@ -1,6 +1,8 @@
#ifndef __DISPLAY__
#define __DISPLAY__
#if defined(PLUGIN_DISPLAY)
#include <Timezone.h>
#include <U8g2lib.h>
@ -17,221 +19,227 @@
template <class HMSYSTEM, class RADIO>
class Display {
public:
Display() {
mMono = NULL;
}
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;
mDisplayData.version = app->getVersion(); // version never changes, so only set once
switch (mCfg->type) {
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, mDisplayData.version);
break;
#endif
default: mMono = NULL; break;
}
if(mMono) {
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);
public:
Display() {
mMono = NULL;
}
// 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 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;
}
mDisplayData.version = app->getVersion(); // version never changes, so only set once
switch (mCfg->type) {
case DISP_TYPE_T0_NONE: mMono = NULL; break; // None
case DISP_TYPE_T1_SSD1306_128X64: mMono = new DisplayMono128X64(); break; // SSD1306_128X64 (0.96", 1.54")
case DISP_TYPE_T2_SH1106_128X64: mMono = new DisplayMono128X64(); break; // SH1106_128X64 (1.3")
case DISP_TYPE_T3_PCD8544_84X48: mMono = new DisplayMono84X48(); break; // PCD8544_84X48 (1.6" - Nokia 5110)
case DISP_TYPE_T4_SSD1306_128X32: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91")
case DISP_TYPE_T5_SSD1306_64X48: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield)
case DISP_TYPE_T6_SSD1309_128X64: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42")
#if defined(ESP32) && !defined(ETHERNET)
case DISP_TYPE_T10_EPAPER:
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, mDisplayData.version);
break;
#endif
default: mMono = NULL; break;
}
if(mMono) {
mMono->config(mCfg);
mMono->init(&mDisplayData);
}
void payloadEventListener(uint8_t cmd) {
mNewPayload = true;
}
// 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 tickerSecond() {
if (mMono != NULL)
mMono->loop(mCfg->contrast, motionSensorActive());
}
if (mNewPayload || (((++mLoopCnt) % 5) == 0)) {
DataScreen();
mNewPayload = false;
mLoopCnt = 0;
void payloadEventListener(uint8_t cmd) {
mNewPayload = true;
}
#if defined(ESP32) && !defined(ETHERNET)
mEpaper.tickerSecond();
#endif
}
private:
void DataScreen() {
if (mCfg->type == 0)
return;
float totalPower = 0.0;
float totalYieldDay = 0.0;
float totalYieldTotal = 0.0;
uint8_t nrprod = 0;
uint8_t nrsleep = 0;
int8_t minQAllInv = 4;
Inverter<> *iv;
record_t<> *rec;
bool allOff = true;
uint8_t nInv = mSys->getNumInverters();
for (uint8_t i = 0; i < nInv; i++) {
iv = mSys->getInverterByPos(i);
if (iv == NULL)
continue;
if (iv->isProducing()) // also updates inverter state engine
nrprod++;
else
nrsleep++;
void tickerSecond() {
bool request_refresh = false;
if (mMono != NULL)
request_refresh = mMono->loop(motionSensorActive());
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) {
DataScreen();
mNewPayload = false;
mLoopCnt = 0;
}
#if defined(ESP32) && !defined(ETHERNET)
mEpaper.tickerSecond();
#endif
}
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;
private:
void DataScreen() {
if (DISP_TYPE_T0_NONE == mCfg->type)
return;
float totalPower = 0.0;
float totalYieldDay = 0.0;
float totalYieldTotal = 0.0;
uint8_t nrprod = 0;
uint8_t nrsleep = 0;
int8_t minQAllInv = 4;
Inverter<> *iv;
record_t<> *rec;
bool allOff = true;
uint8_t nInv = mSys->getNumInverters();
for (uint8_t i = 0; i < nInv; i++) {
iv = mSys->getInverterByPos(i);
if (iv == NULL)
continue;
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;
}
if (maxQInv < minQAllInv)
minQAllInv = maxQInv;
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); // add only FLD_PAC from inverters still communicating
allOff = false;
totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, 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();
// provide localized times to display classes
time_t utc= mApp->getTimestamp();
if (year(utc) > 2020)
mDisplayData.utcTs = gTimezone.toLocal(utc);
else
mDisplayData.utcTs = 0;
mDisplayData.pGraphStartTime = gTimezone.toLocal(mApp->getSunrise());
mDisplayData.pGraphEndTime = gTimezone.toLocal(mApp->getSunset());
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();
}
#if defined(ESP32) && !defined(ETHERNET)
else if (mCfg->type == 10) {
mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
mRefreshCycle++;
}
if (mMono ) {
mMono->disp();
}
#if defined(ESP32) && !defined(ETHERNET)
else if (DISP_TYPE_T10_EPAPER == mCfg->type) {
mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
mRefreshCycle++;
}
if (mRefreshCycle > 480) {
mEpaper.fullRefresh();
mRefreshCycle = 0;
if (mRefreshCycle > 480) {
mEpaper.fullRefresh();
mRefreshCycle = 0;
}
#endif
}
#endif
}
bool motionSensorActive() {
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF)) {
#if defined(ESP8266)
if (mCfg->pirPin == A0)
return((analogRead(A0) >= 512));
else
return(digitalRead(mCfg->pirPin));
#elif defined(ESP32)
return(digitalRead(mCfg->pirPin));
#endif
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;
}
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;
// 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);
}
return (pseudoRSSIdB);
}
// private member variables
IApp *mApp;
DisplayData mDisplayData;
bool mNewPayload;
uint8_t mLoopCnt;
uint32_t *mUtcTs;
display_t *mCfg;
HMSYSTEM *mSys;
RADIO *mHmRadio;
RADIO *mHmsRadio;
uint16_t mRefreshCycle;
#if defined(ESP32) && !defined(ETHERNET)
DisplayEPaper mEpaper;
#endif
DisplayMono *mMono;
// private member variables
IApp *mApp = nullptr;
DisplayData mDisplayData;
bool mNewPayload = false;
uint8_t mLoopCnt = 0;
uint32_t *mUtcTs = nullptr;
display_t *mCfg = nullptr;
HMSYSTEM *mSys = nullptr;
RADIO *mHmRadio = nullptr;
RADIO *mHmsRadio = nullptr;
uint16_t mRefreshCycle = 0;
#if defined(ESP32) && !defined(ETHERNET)
DisplayEPaper mEpaper;
#endif
DisplayMono *mMono = nullptr;
};
#endif /*PLUGIN_DISPLAY*/
#endif /*__DISPLAY__*/

379
src/plugins/Display/Display_Mono.h

@ -20,101 +20,312 @@
#include "Display_data.h"
#include "../../utils/dbg.h"
#include "../../utils/timemonitor.h"
#include "../../config/settings.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, 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 ****");
public:
virtual void init(DisplayData *displayData) = 0;
virtual void config(display_t *cfg) = 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 bool loop(bool motion) {
bool dispConditions = (!mCfg->pwrSaveAtIvOffline || (mDisplayData->nrProducing > 0)) &&
((mCfg->screenSaver != 2) || motion); // screensaver 2 .. motionsensor
if (mDisplayActive) {
if (!dispConditions) {
if (mDisplayTime.isTimeout()) { // switch display off after timeout
mDisplayActive = false;
mDisplay->setPowerSave(true);
}
}
else
mDisplayTime.reStartTimeMonitor(); // keep display on
}
else
mDisplayTime.reStartTimeMonitor(); // keep display on
}
else {
if (dispConditions) {
mDisplayActive = true;
mDisplayTime.reStartTimeMonitor(); // switch display on
mDisplay->setPowerSave(false);
DBGPRINTLN("**** Display on ****");
else {
if (dispConditions) {
mDisplayActive = true;
mDisplayTime.reStartTimeMonitor(); // switch display on
mDisplay->setPowerSave(false);
}
}
if(mLuminance != mCfg->contrast) {
mLuminance = mCfg->contrast;
mDisplay->setContrast(mLuminance);
}
}
if(mLuminance != lum) {
mLuminance = lum;
return(monoMaintainDispSwitchState()); // return flag, if display content should be updated immediately
}
protected:
enum class DispSwitchState {
TEXT,
GRAPH
};
protected:
// Common initialization function to be called by subclasses
void monoInit(U8G2* display, DisplayData *displayData) {
mDisplay = display;
mDisplayData = displayData;
mDisplay->begin();
mDisplay->setPowerSave(false); // always start with display on
mDisplay->setContrast(mLuminance);
}
}
protected:
U8G2* mDisplay;
DisplayData *mDisplayData;
uint8_t mType;
uint16_t mDispWidth;
uint16_t mDispHeight;
bool mEnPowerSave;
uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor
uint8_t mLuminance;
uint8_t mLoopCnt;
uint8_t mLineXOffsets[5] = {};
uint8_t mLineYOffsets[5] = {};
uint8_t mExtra;
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;
}
mDisplay->clearBuffer();
mDispWidth = mDisplay->getDisplayWidth();
mDispHeight = mDisplay->getDisplayHeight();
mDispSwitchTime.stopTimeMonitor();
if (100 == mCfg->graph_ratio) // if graph ratio is 100% start in graph mode
mDispSwitchState = DispSwitchState::GRAPH;
else if (mCfg->graph_ratio != 0)
mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio)); // start display mode change only if ratio is neither 0 nor 100
}
// pixelshift screensaver with wipe effect
void calcPixelShift(int range) {
int8_t mod = (millis() / 10000) % ((range >> 1) << 2);
mPixelshift = (1 == mCfg->screenSaver) ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0;
}
protected:
enum class PowerGraphState {
NO_TIME_SYNC,
IN_PERIOD,
WAIT_4_NEW_PERIOD,
WAIT_4_RESTART
};
// initialize power graph and allocate data buffer based on pixel width
void initPowerGraph(uint8_t width, uint8_t height) {
DBGPRINTLN(F("---- Init Power Graph ----"));
mPgWidth = width;
mPgHeight = height;
mPgData = new float[mPgWidth];
mPgState = PowerGraphState::NO_TIME_SYNC;
resetPowerGraph();
}
// add new value to power graph and maintain state engine for period times
void addPowerGraphEntry(float val) {
if (nullptr == mPgData) // power graph not initialized
return;
bool storeStartEndTimes = false;
bool store_entry = false;
switch(mPgState) {
case PowerGraphState::NO_TIME_SYNC:
if ((mDisplayData->pGraphStartTime > 0)
&& (mDisplayData->pGraphEndTime > 0) // wait until period data is available ...
&& (mDisplayData->utcTs >= mDisplayData->pGraphStartTime)
&& (mDisplayData->utcTs < mDisplayData->pGraphEndTime)) // and current time is in period
{
storeStartEndTimes = true; // period was received -> store
store_entry = true;
mPgState = PowerGraphState::IN_PERIOD;
}
break;
case PowerGraphState::IN_PERIOD:
if (mDisplayData->utcTs > mPgEndTime) // check if end of day is reached ...
mPgState = PowerGraphState::WAIT_4_NEW_PERIOD; // then wait for new period setting
else
store_entry = true;
break;
case PowerGraphState::WAIT_4_NEW_PERIOD:
if ((mPgStartTime != mDisplayData->pGraphStartTime) || (mPgEndTime != mDisplayData->pGraphEndTime)) { // wait until new time period was received ...
storeStartEndTimes = true; // and store it for next period
mPgState = PowerGraphState::WAIT_4_RESTART;
}
break;
case PowerGraphState::WAIT_4_RESTART:
if ((mDisplayData->utcTs >= mPgStartTime) && (mDisplayData->utcTs < mPgEndTime)) { // wait until current time is in period again ...
resetPowerGraph(); // then reset power graph data
store_entry = true;
mPgState = PowerGraphState::IN_PERIOD;
}
break;
}
// store start and end times of current time period and calculate period length
if (storeStartEndTimes) {
mPgStartTime = mDisplayData->pGraphStartTime;
mPgEndTime = mDisplayData->pGraphEndTime;
mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // time period of power graph in sec for scaling of x-axis
}
// store new value to mPgData
if (store_entry) {
mPgLastTime = mDisplayData->utcTs; // time of last datapoint
mPgLastPos = std::min((uint8_t) sss2PgPos(mPgLastTime - mPgStartTime), (uint8_t) (mPgWidth - 1)); // last datapoint based on seconds since start
mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); // update current datapoint to maximum of all seen values (= envelope curve)
mPgMaxPwr = std::max(mPgMaxPwr, val); // update max value of stored data for scaling of y-axis
}
}
// plot power graph to given display offset
void plotPowerGraph(uint8_t xoff, uint8_t yoff) {
if (nullptr == mPgData) // power graph not initialized
return;
// draw axes
mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis
mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis
// do not draw as long as time is not set correctly and no data was received
if ((0 == mPgStartTime) || (0 == mPgEndTime) || (0 == mPgLastTime) || (0 == mPgLastPos) || (mPgMaxPwr < 1))
return;
// draw X scale
tmElements_t tm;
breakTime(mPgEndTime, tm);
uint8_t endHourPg = tm.Hour; // absolute last hour in diagram
breakTime(mPgLastTime, tm);
uint8_t endHour = std::min(endHourPg, tm.Hour); // last hour of current data point in scaled diagram
breakTime(mPgStartTime, tm);
tm.Hour += 1;
tm.Minute = 0;
tm.Second = 0;
for (; tm.Hour <= endHour; tm.Hour++) {
uint8_t x_pos_screen = getPowerGraphXpos(sss2PgPos((uint32_t) makeTime(tm) - mPgStartTime)); // scale horizontal axis
if (12 == tm.Hour) {
mDisplay->drawLine(xoff + x_pos_screen, yoff, xoff + x_pos_screen, yoff - 2); // mark noon
mDisplay->drawLine(xoff + x_pos_screen - 1, yoff - 1, xoff + x_pos_screen + 1, yoff - 1);
}
else
mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1);
}
// draw Y scale
uint16_t scale_y = 10;
uint32_t maxpwr_int = static_cast<uint32_t>(std::round(mPgMaxPwr));
if (maxpwr_int > 100)
scale_y = 100;
for (uint32_t i = scale_y; i <= maxpwr_int; i += scale_y) {
uint8_t ypos = yoff - static_cast<uint8_t>(std::round(i * (float) mPgHeight / mPgMaxPwr)); // scale vertical axis
mDisplay->drawPixel(xoff + 1, ypos);
}
// draw curve
for (uint8_t i = 1; i <= mPgLastPos; i++) {
mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphValueYpos(i - 1),
xoff + getPowerGraphXpos(i), yoff - getPowerGraphValueYpos(i));
}
// print max power value
mDisplay->setFont(u8g2_font_4x6_tr);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast<uint16_t>(std::round(mPgMaxPwr)));
mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText);
}
private:
bool monoMaintainDispSwitchState(void) {
bool change = false;
switch(mDispSwitchState) {
case DispSwitchState::TEXT:
if (mDispSwitchTime.isTimeout()) {
mDispSwitchState = DispSwitchState::GRAPH;
mDispSwitchTime.startTimeMonitor(150 * mCfg->graph_ratio); // graph_ratio: 0-100 Gesamtperiode 15000 ms
change = true;
}
break;
case DispSwitchState::GRAPH:
if (mDispSwitchTime.isTimeout()) {
mDispSwitchState = DispSwitchState::TEXT;
mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio));
change = true;
}
break;
}
return change;
}
// reset power graph
void resetPowerGraph() {
if (mPgData != nullptr) {
mPgMaxPwr = 0.0;
mPgLastPos = 0;
mPgLastTime = 0;
for (uint8_t i = 0; i < mPgWidth; i++) {
mPgData[i] = 0.0;
}
}
}
// get power graph datapoint index, scaled to current time period, by seconds since start
uint8_t sss2PgPos(uint seconds_since_start) {
if(mPgPeriod)
return (seconds_since_start * (mPgWidth - 1) / mPgPeriod);
else
return 0;
}
// get X-position of power graph, scaled to lastpos, by according data point index
uint8_t getPowerGraphXpos(uint8_t p) {
if ((p <= mPgLastPos) && (mPgLastPos > 0))
return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis
else
return 0;
}
// get Y-position of power graph, scaled to maximum value, by according datapoint index
uint8_t getPowerGraphValueYpos(uint8_t p) {
if ((p < mPgWidth) && (mPgMaxPwr > 0))
return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height
else
return 0;
}
protected:
display_t *mCfg;
U8G2 *mDisplay;
DisplayData *mDisplayData;
DispSwitchState mDispSwitchState = DispSwitchState::TEXT;
uint16_t mDispWidth;
uint8_t mExtra = 0;
int8_t mPixelshift=0;
char mFmtText[DISP_FMT_TEXT_LEN];
uint8_t mLineXOffsets[5] = {0, 0, 0, 0, 0};
uint8_t mLineYOffsets[5] = {0, 0, 0, 0, 0};
uint8_t mPgWidth = 0;
private:
float *mPgData = nullptr;
uint8_t mPgHeight = 0;
float mPgMaxPwr = 0.0;
uint32_t mPgStartTime = 0;
uint32_t mPgEndTime = 0;
uint32_t mPgPeriod = 0; // seconds
uint8_t mPgLastPos = 0;
uint32_t mPgLastTime = 0;
PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC;
uint16_t mDispHeight = 0;
uint8_t mLuminance = 0;
TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true);
TimeMonitor mDispSwitchTime = TimeMonitor();
bool mDisplayActive = true; // always start with display on
};
/* 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 */
\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"

20
src/plugins/Display/Display_Mono_128X32.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -12,15 +12,13 @@ class DisplayMono128X32 : public DisplayMono {
mExtra = 0;
}
void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
void config(display_t *cfg) override {
mCfg = cfg;
}
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);
monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data), type, displayData);
void init(DisplayData *displayData) override {
u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
calcLinePositions();
printText("Ahoy!", 0);
printText("ahoydtu.de", 2);
@ -28,7 +26,7 @@ class DisplayMono128X32 : public DisplayMono {
mDisplay->sendBuffer();
}
void disp(void) {
void disp(void) override {
mDisplay->clearBuffer();
// calculate current pixelshift for pixelshift screensaver
@ -58,7 +56,7 @@ class DisplayMono128X32 : public DisplayMono {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", mDisplayData->nrProducing);
printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3);
printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);
mDisplay->sendBuffer();
@ -109,7 +107,7 @@ class DisplayMono128X32 : public DisplayMono {
void printText(const char *text, uint8_t line) {
setFont(line);
uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift;
uint8_t dispX = mLineXOffsets[line] + (pixelShiftRange / 2) + mPixelshift;
if (isTwoRowLine(line)) {
String stringText = String(text);

236
src/plugins/Display/Display_Mono_128X64.h

@ -4,6 +4,7 @@
//-----------------------------------------------------------------------------
#pragma once
#include "Display.h"
#include "Display_Mono.h"
class DisplayMono128X64 : public DisplayMono {
@ -12,27 +13,54 @@ class DisplayMono128X64 : public DisplayMono {
mExtra = 0;
}
void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
void config(display_t *cfg) override {
mCfg = cfg;
}
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);
switch (type) {
void init(DisplayData *displayData) override {
u8g2_cb_t *rot = (u8g2_cb_t *)(( mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
switch (mCfg->type) {
case DISP_TYPE_T1_SSD1306_128X64:
monoInit(new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
break;
case DISP_TYPE_T2_SH1106_128X64:
monoInit(new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
break;
case DISP_TYPE_T6_SSD1309_128X64:
default:
monoInit(new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
break;
}
calcLinePositions();
switch(mCfg->graph_size) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]];
case 0:
graph_first_line = 1;
graph_last_line = 2;
break;
case 1:
monoInit(new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData);
graph_first_line = 2;
graph_last_line = 3;
break;
case 2:
monoInit(new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData);
graph_first_line = 1;
graph_last_line = 3;
break;
case 6:
case 3:
graph_first_line = 2;
graph_last_line = 4;
break;
case 4:
default:
monoInit(new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(rot, reset, clock, data), type, displayData);
graph_first_line = 1;
graph_last_line = 4;
break;
}
calcLinePositions();
widthShrink = (mCfg->screenSaver == 1) ? pixelShiftRange : 0; // shrink graphwidth for pixelshift screensaver
if (mCfg->graph_ratio > 0)
initPowerGraph(mDispWidth - 22 - widthShrink, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2);
printText("Ahoy!", l_Ahoy, 0xff);
printText("ahoydtu.de", l_Website, 0xff);
@ -40,9 +68,7 @@ class DisplayMono128X64 : public DisplayMono {
mDisplay->sendBuffer();
}
void disp(void) {
uint8_t pos, sun_pos, moon_pos;
void disp(void) override {
mDisplay->clearBuffer();
// Layout-Test
@ -61,106 +87,116 @@ class DisplayMono128X64 : public DisplayMono {
// calculate current pixelshift for pixelshift screensaver
calcPixelShift(pixelShiftRange);
// print total power
if (mDisplayData->nrProducing > 0) {
if (mDisplayData->totalPower > 9999.0)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
printText(mFmtText, l_TotalPower, 0xff);
} else {
printText("offline", l_TotalPower, 0xff);
}
// add new power data to power graph
if (mDisplayData->nrProducing > 0)
addPowerGraphEntry(mDisplayData->totalPower);
// print Date and time
if (0 != mDisplayData->utcTs)
printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff);
printText(ah::getDateTimeStrShort(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;
if (showLine(l_Status)) {
// 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 {
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, " ");
int8_t sun_pos = -1;
int8_t 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);
uint8_t 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
}
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 (showLine(l_TotalPower)) {
// print total power
if (mDisplayData->nrProducing > 0) {
if (mDisplayData->totalPower > 9999.0)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
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);
printText(mFmtText, l_TotalPower, 0xff);
} else {
printText("offline", l_TotalPower, 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);
if (showLine(l_YieldDay)) {
// print day yield
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 (showLine(l_YieldTotal)) {
// print total yield
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);
}
if ((mCfg->graph_ratio > 0) && (mDispSwitchState == DispSwitchState::GRAPH)) {
// plot power graph
plotPowerGraph((mDispWidth - mPgWidth) / 2 + mPixelshift, mLineYOffsets[graph_last_line] - 1);
}
// 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);
int rssi_threshold = -60 - i * 10;
uint8_t barwidth = std::min(4 - i, 3);
if (mDisplayData->RadioRSSI > rssi_threshold)
mDisplay->drawBox(widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
if (mDisplayData->WifiRSSI > rssi_threshold)
mDisplay->drawBox(mDispWidth - barwidth - widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, 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);
mDisplay->drawStr((widthShrink / 2) + 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->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - (widthShrink / 2) + mPixelshift, mLineYOffsets[l_RSSI], sym);
mDisplay->sendBuffer();
mExtra++;
@ -184,29 +220,31 @@ class DisplayMono128X64 : public DisplayMono {
l_MAX_LINES = 5,
};
uint8_t graph_first_line = 0;
uint8_t graph_last_line = 0;
const uint8_t pixelShiftRange = 11; // number of pixels to shift from left to right (centered -> must be odd!)
uint8_t widthShrink = 0;
void calcLinePositions() {
uint8_t yOff = 0;
uint8_t i = 0;
uint8_t asc, dsc;
do {
setLineFont(i);
asc = mDisplay->getAscent();
uint8_t asc = mDisplay->getAscent();
yOff += asc;
mLineYOffsets[i] = yOff;
dsc = mDisplay->getDescent();
uint8_t dsc = mDisplay->getDescent();
yOff -= dsc;
if (l_Time==i) // prevent time and status line to touch
yOff+=1; // -> one pixels space
if (l_Time == i) // prevent time and status line to touch
yOff++; // -> one pixels space
i++;
} while(l_MAX_LINES>i);
}
inline void setLineFont(uint8_t line) {
if ((line == l_TotalPower) ||
(line == l_Ahoy))
if (line == l_TotalPower) // || (line == l_Ahoy) -> l_TotalPower == l_Ahoy == 2
mDisplay->setFont(u8g2_font_ncenB14_tr);
else if ((line == l_YieldDay) ||
(line == l_YieldTotal))
@ -226,4 +264,8 @@ class DisplayMono128X64 : public DisplayMono {
dispX += mPixelshift;
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
}
bool showLine(uint8_t line) {
return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line)));
}
};

18
src/plugins/Display/Display_Mono_64X48.h

@ -12,16 +12,14 @@ class DisplayMono64X48 : public DisplayMono {
mExtra = 0;
}
void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
void config(display_t *cfg) override {
mCfg = cfg;
}
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);
void init(DisplayData *displayData) override {
u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
// Wemos OLed Shield is not defined in u8 lib -> use nearest compatible
monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, reset, clock, data), type, displayData);
monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
calcLinePositions();
printText("Ahoy!", 0);
@ -30,7 +28,7 @@ class DisplayMono64X48 : public DisplayMono {
mDisplay->sendBuffer();
}
void disp(void) {
void disp(void) override {
mDisplay->clearBuffer();
// calculate current pixelshift for pixelshift screensaver
@ -60,7 +58,7 @@ class DisplayMono64X48 : public DisplayMono {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", mDisplayData->nrProducing);
printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3);
printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);
mDisplay->sendBuffer();
@ -98,7 +96,7 @@ class DisplayMono64X48 : public DisplayMono {
}
void printText(const char *text, uint8_t line) {
uint8_t dispX = mLineXOffsets[line] + pixelShiftRange/2 + mPixelshift;
uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift;
setFont(line);
mDisplay->drawStr(dispX, mLineYOffsets[line], text);

176
src/plugins/Display/Display_Mono_84X48.h

@ -1,11 +1,10 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#pragma once
#include "Display_Mono.h"
#include "../../utils/dbg.h"
class DisplayMono84X48 : public DisplayMono {
public:
@ -13,23 +12,50 @@ class DisplayMono84X48 : public DisplayMono {
mExtra = 0;
}
void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
mEnPowerSave = enPowerSave;
mScreenSaver = screenSaver;
mLuminance = lum;
void config(display_t *cfg) override {
mCfg = cfg;
}
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);
monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset), type, displayData);
void init(DisplayData *displayData) override {
u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, mCfg->disp_clk, mCfg->disp_data, mCfg->disp_cs, mCfg->disp_dc, 0xff), displayData);
calcLinePositions();
switch(mCfg->graph_size) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]];
case 0:
graph_first_line = 1;
graph_last_line = 2;
break;
case 1:
graph_first_line = 2;
graph_last_line = 3;
break;
case 2:
graph_first_line = 1;
graph_last_line = 3;
break;
case 3:
graph_first_line = 2;
graph_last_line = 4;
break;
case 4:
default:
graph_first_line = 1;
graph_last_line = 4;
break;
}
if (mCfg->graph_ratio > 0)
initPowerGraph(mDispWidth - 16, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2);
printText("Ahoy!", l_Ahoy, 0xff);
printText("ahoydtu.de", l_Website, 0xff);
printText(mDisplayData->version, l_Version, 0xff);
mDisplay->sendBuffer();
}
void disp(void) {
void disp(void) override {
mDisplay->clearBuffer();
// Layout-Test
@ -45,66 +71,84 @@ class DisplayMono84X48 : public DisplayMono {
mDisplay->drawPixel(mDispWidth-1, mDispHeight-1);
*/
// print total power
// add new power data to power graph
if (mDisplayData->nrProducing > 0) {
if (mDisplayData->totalPower > 9999.0)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
printText(mFmtText, l_TotalPower, 0xff);
} else {
printText("offline", l_TotalPower, 0xff);
addPowerGraphEntry(mDisplayData->totalPower);
}
// print Date and time
if (0 != mDisplayData->utcTs)
printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff);
printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff);
if (showLine(l_Status)) {
// 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);
}
}
// 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);
if (showLine(l_TotalPower)) {
// print total power
if (mDisplayData->nrProducing > 0) {
if (mDisplayData->totalPower > 9999.0)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
else
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
printText(mFmtText, l_TotalPower, 0xff);
} else {
printText("offline", l_TotalPower, 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
if (showLine(l_YieldDay)) {
// print day yield
printText("\x88", l_YieldDay, 10); // day 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, "%d\x86 %d\x87", mDisplayData->nrProducing, mDisplayData->nrSleeping);
printText(mFmtText, l_Status, 0xff);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay);
printText(mFmtText, l_YieldDay, 0xff);
}
// print yields
printText("\x88", l_YieldDay, 10); // day symbol
printText("\x83", l_YieldTotal, 10); // 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 (showLine(l_YieldTotal)) {
// print total yield
printText("\x83", l_YieldTotal, 10); // total symbol
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);
}
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);
if ((mCfg->graph_ratio > 0) && (mDispSwitchState == DispSwitchState::GRAPH)) {
// plot power graph
plotPowerGraph(8, mLineYOffsets[graph_last_line] - 1);
}
// draw dynamic Nokia RSSI bars
// draw dynamic 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);
for (int i = 0; i < 4; i++) {
int rssi_threshold = -60 - i * 10;
uint8_t barwidth = std::min(4 - i, 3);
if (mDisplayData->RadioRSSI > rssi_threshold)
mDisplay->drawBox(0, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
if (mDisplayData->WifiRSSI > rssi_threshold)
mDisplay->drawBox(mDispWidth - barwidth, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
}
// draw dynamic antenna and WiFi symbols
@ -139,26 +183,28 @@ class DisplayMono84X48 : public DisplayMono {
l_MAX_LINES = 5,
};
uint8_t graph_first_line = 0;
uint8_t graph_last_line = 0;
void calcLinePositions() {
uint8_t yOff = 0;
uint8_t i = 0;
uint8_t asc, dsc;
do {
setLineFont(i);
asc = mDisplay->getAscent();
uint8_t asc = mDisplay->getAscent();
yOff += asc;
mLineYOffsets[i] = yOff;
dsc = mDisplay->getDescent();
if (l_TotalPower!=i) // power line needs no descent spacing
uint8_t dsc = mDisplay->getDescent();
if (l_TotalPower != i) // power line needs no descent spacing
yOff -= dsc;
yOff++; // instead lets spend one pixel space between all lines
i++;
} while(l_MAX_LINES>i);
} while(l_MAX_LINES > i);
}
inline void setLineFont(uint8_t line) {
if ((line == l_TotalPower) || (line == l_Ahoy))
if (line == l_TotalPower) // || (line == l_Ahoy) -> l_TotalPower == l_Ahoy == 2
mDisplay->setFont(u8g2_font_logisoso16_tr);
else
mDisplay->setFont(u8g2_font_5x8_symbols_ahoy);
@ -174,6 +220,10 @@ class DisplayMono84X48 : public DisplayMono {
dispX = col;
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
}
bool showLine(uint8_t line) {
return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line)));
}
};

28
src/plugins/Display/Display_data.h

@ -4,19 +4,21 @@
#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
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 (localized utc unix time). 0 = time is not synchonized
uint32_t pGraphStartTime=0; // localized starttime for power graph (e.g. sunRise)
uint32_t pGraphEndTime=0; // localized endttime for power graph (e.g. sunSet)
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__*/

38
src/plugins/Display/Display_ePaper.cpp

@ -7,6 +7,7 @@
#endif
#include "../../utils/helper.h"
#include "imagedata.h"
#include "defines.h"
#if defined(ESP32)
@ -19,6 +20,7 @@ SPIClass hspi(HSPI);
DisplayEPaper::DisplayEPaper() {
mDisplayRotation = 2;
mHeadFootPadding = 16;
memset(_fmtText, 0, EPAPER_MAX_TEXT_LEN);
}
@ -29,7 +31,7 @@ void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, u
mRefreshState = RefreshStatus::LOGO;
mSecondCnt = 0;
if (type == 10) {
if (DISP_TYPE_T10_EPAPER == type) {
Serial.begin(115200);
_display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY));
@ -65,6 +67,7 @@ void DisplayEPaper::refreshLoop() {
case RefreshStatus::LOGO:
_display->fillScreen(GxEPD_BLACK);
_display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
mSecondCnt = 2;
mNextRefreshState = RefreshStatus::PARTITIALS;
mRefreshState = RefreshStatus::WAIT;
break;
@ -76,11 +79,11 @@ void DisplayEPaper::refreshLoop() {
break;
case RefreshStatus::WHITE:
if(mSecondCnt == 0) {
_display->fillScreen(GxEPD_WHITE);
mNextRefreshState = RefreshStatus::PARTITIALS;
mRefreshState = RefreshStatus::WAIT;
}
if(0 != mSecondCnt)
break;
_display->fillScreen(GxEPD_WHITE);
mNextRefreshState = RefreshStatus::PARTITIALS;
mRefreshState = RefreshStatus::WAIT;
break;
case RefreshStatus::WAIT:
@ -89,10 +92,13 @@ void DisplayEPaper::refreshLoop() {
break;
case RefreshStatus::PARTITIALS:
if(0 != mSecondCnt)
break;
headlineIP();
versionFooter();
mSecondCnt = 4; // display Logo time during boot up
mRefreshState = RefreshStatus::DONE;
mNextRefreshState = RefreshStatus::DONE;
mRefreshState = RefreshStatus::WAIT;
break;
default: // RefreshStatus::DONE
@ -112,9 +118,9 @@ void DisplayEPaper::headlineIP() {
do {
if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) {
snprintf(_fmtText, sizeof(_fmtText), "%s", WiFi.localIP().toString().c_str());
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str());
} else {
snprintf(_fmtText, sizeof(_fmtText), "WiFi not connected");
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "WiFi not connected");
}
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -135,7 +141,7 @@ void DisplayEPaper::lastUpdatePaged() {
_display->fillScreen(GxEPD_BLACK);
do {
if (NULL != mUtcTs) {
snprintf(_fmtText, sizeof(_fmtText), ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str());
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str());
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -156,7 +162,7 @@ void DisplayEPaper::versionFooter() {
_display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding);
_display->fillScreen(GxEPD_BLACK);
do {
snprintf(_fmtText, sizeof(_fmtText), "Version: %s", _version);
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "Version: %s", _version);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -177,7 +183,7 @@ void DisplayEPaper::offlineFooter() {
_display->fillScreen(GxEPD_BLACK);
do {
if (NULL != mUtcTs) {
snprintf(_fmtText, sizeof(_fmtText), "offline");
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline");
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -201,13 +207,13 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
do {
// actual Production
if (totalPower > 9999) {
snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 1000));
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.1f kW", (totalPower / 1000));
_changed = true;
} else if ((totalPower > 0) && (totalPower <= 9999)) {
snprintf(_fmtText, sizeof(_fmtText), "%.0f W", totalPower);
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.0f W", totalPower);
_changed = true;
} else
snprintf(_fmtText, sizeof(_fmtText), "offline");
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline");
if ((totalPower == 0) && (mEnPowerSave)) {
_display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK);
@ -262,7 +268,7 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
// Inverter online
_display->setFont(&FreeSans12pt7b);
y = _display->height() - (mHeadFootPadding + 10);
snprintf(_fmtText, sizeof(_fmtText), " %d online", isprod);
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d online", isprod);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
_display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK);
x = ((_display->width() - tbw - 20) / 2) - tbx;

6
src/plugins/Display/Display_ePaper.h

@ -9,11 +9,11 @@
// enable GxEPD2_GFX base class
#define ENABLE_GxEPD2_GFX 1
#include <GxEPD2_3C.h>
#define EPAPER_MAX_TEXT_LEN 35
#include <GxEPD2_BW.h>
#include <SPI.h>
#include <map>
// FreeFonts from Adafruit_GFX
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
@ -51,7 +51,7 @@ class DisplayEPaper {
uint8_t mDisplayRotation;
bool _changed = false;
char _fmtText[35];
char _fmtText[EPAPER_MAX_TEXT_LEN];
String _settedIP;
uint8_t mHeadFootPadding;
GxEPD2_GFX* _display;

117
src/plugins/history.h

@ -0,0 +1,117 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HISTORY_DATA_H__
#define __HISTORY_DATA_H__
#if defined(ENABLE_HISTORY)
#include <array>
#include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/helper.h"
#define HISTORY_DATA_ARR_LENGTH 256
enum class HistoryStorageType : uint8_t {
POWER,
YIELD
};
template<class HMSYSTEM>
class HistoryData {
private:
struct storage_t {
uint16_t refreshCycle = 0;
uint16_t loopCnt = 0;
uint16_t listIdx = 0; // index for next Element to write into WattArr
uint16_t dispIdx = 0; // index for 1st Element to display from WattArr
bool wrapped = false;
// ring buffer for watt history
std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data;
storage_t() { data.fill(0); }
};
public:
void setup(IApp *app, HMSYSTEM *sys, settings_t *config, uint32_t *ts) {
mApp = app;
mSys = sys;
mConfig = config;
mTs = ts;
mCurPwr.refreshCycle = mConfig->inst.sendInterval;
//mYieldDay.refreshCycle = 60;
}
void tickerSecond() {
;
float curPwr = 0;
float maxPwr = 0;
float yldDay = -0.1;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
Inverter<> *iv = mSys->getInverterByPos(i);
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL)
continue;
curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec);
yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
}
if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) {
mCurPwr.loopCnt = 0;
if (curPwr > 0)
addValue(&mCurPwr, roundf(curPwr));
if (maxPwr > 0)
mMaximumDay = roundf(maxPwr);
}
/*if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) {
if (*mTs > mApp->getSunset()) {
if ((!mDayStored) && (yldDay > 0)) {
addValue(&mYieldDay, roundf(yldDay));
mDayStored = true;
}
} else if (*mTs > mApp->getSunrise())
mDayStored = false;
}*/
}
uint16_t valueAt(HistoryStorageType type, uint16_t i) {
//storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay;
storage_t *s = &mCurPwr;
uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH;
return s->data[idx];
}
uint16_t getMaximumDay() {
return mMaximumDay;
}
private:
void addValue(storage_t *s, uint16_t value) {
if (s->wrapped) // after 1st time array wrap we have to increase the display index
s->dispIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH);
s->data[s->listIdx] = value;
s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH);
if (s->listIdx == 0)
s->wrapped = true;
}
private:
IApp *mApp = nullptr;
HMSYSTEM *mSys = nullptr;
settings *mSettings = nullptr;
settings_t *mConfig = nullptr;
uint32_t *mTs = nullptr;
storage_t mCurPwr;
bool mDayStored = false;
uint16_t mMaximumDay = 0;
};
#endif /*ENABLE_HISTORY*/
#endif /*__HISTORY_DATA_H__*/

225
src/publisher/pubMqtt.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -8,12 +8,14 @@
#ifndef __PUB_MQTT_H__
#define __PUB_MQTT_H__
#if defined(ENABLE_MQTT)
#ifdef ESP8266
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include <array>
#include "../utils/dbg.h"
#include "../config/config.h"
#include <espMqttClient.h>
@ -36,14 +38,16 @@ typedef struct {
template<class HMSYSTEM>
class PubMqtt {
public:
PubMqtt() {
mRxCnt = 0;
mTxCnt = 0;
mSubscriptionCb = NULL;
memset(mLastIvState, (uint8_t)InverterStatus::OFF, MAX_NUM_INVERTERS);
memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4);
mLastAnyAvail = false;
mZeroValues = false;
PubMqtt() : SendIvData() {
mLastIvState.fill(InverterStatus::OFF);
mIvLastRTRpub.fill(0);
mVal.fill(0);
mTopic.fill(0);
mSubTopic.fill(0);
mClientId.fill(0);
mLwtTopic.fill(0);
mSendAlarm.fill(false);
}
~PubMqtt() { }
@ -57,22 +61,22 @@ class PubMqtt {
mUptime = uptime;
mIntervalTimeout = 1;
mSendIvData.setup(sys, utcTs, &mSendList);
mSendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained, uint8_t qos) {
SendIvData.setup(sys, utcTs, &mSendList);
SendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained, uint8_t qos) {
publish(subTopic, payload, retained, true, qos);
});
mDiscovery.running = false;
snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic);
snprintf(mLwtTopic.data(), mLwtTopic.size(), "%s/mqtt", mCfgMqtt->topic);
if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0))
mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd);
if(strlen(mCfgMqtt->clientId) > 0)
snprintf(mClientId, 23, "%s", mCfgMqtt->clientId);
else{
snprintf(mClientId, 24, "%s-", mDevName);
uint8_t pos = strlen(mClientId);
snprintf(mClientId.data(), mClientId.size(), "%s", mCfgMqtt->clientId);
else {
snprintf(mClientId.data(), mClientId.size(), "%s-", mDevName);
uint8_t pos = strlen(mClientId.data());
mClientId[pos++] = WiFi.macAddress().substring( 9, 10).c_str()[0];
mClientId[pos++] = WiFi.macAddress().substring(10, 11).c_str()[0];
mClientId[pos++] = WiFi.macAddress().substring(12, 13).c_str()[0];
@ -82,16 +86,16 @@ class PubMqtt {
mClientId[pos++] = '\0';
}
mClient.setClientId(mClientId);
mClient.setClientId(mClientId.data());
mClient.setServer(mCfgMqtt->broker, mCfgMqtt->port);
mClient.setWill(mLwtTopic, QOS_0, true, mqttStr[MQTT_STR_LWT_NOT_CONN]);
mClient.setWill(mLwtTopic.data(), QOS_0, true, mqttStr[MQTT_STR_LWT_NOT_CONN]);
mClient.onConnect(std::bind(&PubMqtt::onConnect, this, std::placeholders::_1));
mClient.onDisconnect(std::bind(&PubMqtt::onDisconnect, this, std::placeholders::_1));
mClient.onMessage(std::bind(&PubMqtt::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
}
void loop() {
mSendIvData.loop();
SendIvData.loop();
#if defined(ESP8266)
mClient.loop();
@ -125,8 +129,8 @@ class PubMqtt {
}
void tickerMinute() {
snprintf(mVal, 40, "%d", (*mUptime));
publish(subtopics[MQTT_UPTIME], mVal);
snprintf(mVal.data(), mVal.size(), "%u", (*mUptime));
publish(subtopics[MQTT_UPTIME], mVal.data());
publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str());
publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str());
#ifndef ESP32
@ -134,32 +138,35 @@ class PubMqtt {
#endif
}
bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs) {
bool tickerSun(uint32_t sunrise, uint32_t sunset, int16_t offsM, int16_t offsE, bool isSunrise = false) {
if (!mClient.connected())
return false;
publish(subtopics[MQTT_SUNRISE], String(sunrise).c_str(), true);
publish(subtopics[MQTT_SUNSET], String(sunset).c_str(), true);
publish(subtopics[MQTT_COMM_START], String(sunrise - offs).c_str(), true);
publish(subtopics[MQTT_COMM_STOP], String(sunset + offs).c_str(), true);
publish(subtopics[MQTT_COMM_START], String(sunrise + offsM).c_str(), true);
publish(subtopics[MQTT_COMM_STOP], String(sunset + offsE).c_str(), true);
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = mSys->getInverterByPos(i);
Inverter<> *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.data(), mSubTopic.size(), "%s/dis_night_comm", iv->config->name);
publish(mSubTopic.data(), ((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);
snprintf(mSubTopic.data(), mSubTopic.size(), "comm_disabled");
publish(mSubTopic.data(), (((*mUtcTimestamp > (sunset + offsE)) || (*mUtcTimestamp < (sunrise + offsM))) ? dict[STR_TRUE] : dict[STR_FALSE]), true);
return true;
}
void notAvailChanged(bool allNotAvail) {
if(!allNotAvail)
SendIvData.resetYieldDay();
}
bool tickerComm(bool disabled) {
if (!mClient.connected())
return false;
@ -172,9 +179,9 @@ class PubMqtt {
void tickerMidnight() {
// set Total YieldDay to zero
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[FLD_YD]);
snprintf(mVal, 2, "0");
publish(mSubTopic, mVal, true);
snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[FLD_YD]);
snprintf(mVal.data(), mVal.size(), "0");
publish(mSubTopic.data(), mVal.data(), true);
}
void payloadEventListener(uint8_t cmd, Inverter<> *iv) {
@ -193,11 +200,11 @@ class PubMqtt {
return;
if(addTopic)
snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s/%s", mCfgMqtt->topic, subTopic);
snprintf(mTopic.data(), mTopic.size(), "%s/%s", mCfgMqtt->topic, subTopic);
else
snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s", subTopic);
snprintf(mTopic.data(), mTopic.size(), "%s", subTopic);
mClient.publish(mTopic, qos, retained, payload);
mClient.publish(mTopic.data(), qos, retained, payload);
yield();
mTxCnt++;
}
@ -234,15 +241,11 @@ class PubMqtt {
void setPowerLimitAck(Inverter<> *iv) {
if (NULL != iv) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]);
publish(mSubTopic, "true", true, true, QOS_2);
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]);
publish(mSubTopic.data(), "true", true, true, QOS_2);
}
}
void setZeroValuesEnable(void) {
mZeroValues = true;
}
private:
void onConnect(bool sessionPreset) {
DPRINTLN(DBG_INFO, F("MQTT connected"));
@ -255,15 +258,15 @@ class PubMqtt {
publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true);
#endif
tickerMinute();
publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false);
publish(mLwtTopic.data(), mqttStr[MQTT_STR_LWT_CONN], true, false);
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
snprintf(mVal, 20, "ctrl/limit/%d", i);
subscribe(mVal, QOS_2);
snprintf(mVal, 20, "ctrl/restart/%d", i);
subscribe(mVal);
snprintf(mVal, 20, "ctrl/power/%d", i);
subscribe(mVal);
snprintf(mVal.data(), mVal.size(), "ctrl/limit/%d", i);
subscribe(mVal.data(), QOS_2);
snprintf(mVal.data(), mVal.size(), "ctrl/restart/%d", i);
subscribe(mVal.data());
snprintf(mVal.data(), mVal.size(), "ctrl/power/%d", i);
subscribe(mVal.data());
}
subscribe(subscr[MQTT_SUBS_SET_TIME]);
}
@ -308,22 +311,25 @@ class PubMqtt {
bool limitAbs = false;
if(len > 0) {
char *pyld = new char[len + 1];
strncpy(pyld, (const char*)payload, len);
memcpy(pyld, payload, len);
pyld[len] = '\0';
root[F("val")] = atoi(pyld);
if(NULL == strstr(topic, "limit"))
root[F("val")] = atoi(pyld);
else
root[F("val")] = atof(pyld);
if(pyld[len-1] == 'W')
limitAbs = true;
delete[] pyld;
}
const char *p = topic + strlen(mCfgMqtt->topic);
uint8_t pos = 0;
uint8_t elm = 0;
uint8_t pos = 0, elm = 0;
char tmp[30];
while(1) {
if(('/' == p[pos]) || ('\0' == p[pos])) {
strncpy(tmp, p, pos);
memcpy(tmp, p, pos);
tmp[pos] = '\0';
switch(elm++) {
case 1: root[F("path")] = String(tmp); break;
@ -333,8 +339,7 @@ class PubMqtt {
root[F("cmd")] = F("limit_nonpersistent_absolute");
else
root[F("cmd")] = F("limit_nonpersistent_relative");
}
else
} else
root[F("cmd")] = String(tmp);
break;
case 3: root[F("id")] = atoi(tmp); break;
@ -357,11 +362,9 @@ class PubMqtt {
}
void discoveryConfigLoop(void) {
char topic[64], name[32], uniq_id[32], buf[350];
DynamicJsonDocument doc(256);
uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC};
const char* unitTotal[4] = {"W", "kWh", "Wh", "W"};
constexpr static uint8_t fldTotal[] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC};
String node_id = String(mDevName) + "_TOTAL";
bool total = (mDiscovery.lastIvId == MAX_NUM_INVERTERS);
@ -390,32 +393,37 @@ class PubMqtt {
doc[F("mf")] = F("Hoymiles");
JsonObject deviceObj = doc.as<JsonObject>(); // deviceObj is only pointer!?
std::array<char, 64> topic;
std::array<char, 32> name;
std::array<char, 32> uniq_id;
std::array<char, 350> buf;
const char *devCls, *stateCls;
if (!total) {
if (rec->assign[mDiscovery.sub].ch == CH0)
snprintf(name, 32, "%s", iv->getFieldName(mDiscovery.sub, rec));
snprintf(name.data(), name.size(), "%s", iv->getFieldName(mDiscovery.sub, rec));
else
snprintf(name, 32, "CH%d_%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
snprintf(topic, 64, "/ch%d/%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
snprintf(uniq_id, 32, "ch%d_%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
snprintf(name.data(), name.size(), "CH%d_%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
snprintf(topic.data(), name.size(), "/ch%d/%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
snprintf(uniq_id.data(), uniq_id.size(), "ch%d_%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
devCls = getFieldDeviceClass(rec->assign[mDiscovery.sub].fieldId);
stateCls = getFieldStateClass(rec->assign[mDiscovery.sub].fieldId);
}
else { // total values
snprintf(name, 32, "Total %s", fields[fldTotal[mDiscovery.sub]]);
snprintf(topic, 64, "/%s", fields[fldTotal[mDiscovery.sub]]);
snprintf(uniq_id, 32, "total_%s", fields[fldTotal[mDiscovery.sub]]);
snprintf(name.data(), name.size(), "Total %s", fields[fldTotal[mDiscovery.sub]]);
snprintf(topic.data(), topic.size(), "/%s", fields[fldTotal[mDiscovery.sub]]);
snprintf(uniq_id.data(), uniq_id.size(), "total_%s", fields[fldTotal[mDiscovery.sub]]);
devCls = getFieldDeviceClass(fldTotal[mDiscovery.sub]);
stateCls = getFieldStateClass(fldTotal[mDiscovery.sub]);
}
DynamicJsonDocument doc2(512);
constexpr static char* unitTotal[] = {"W", "kWh", "Wh", "W"};
doc2[F("name")] = name;
doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic);
doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic.data());
doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub]));
doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id;
doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id.data();
doc2[F("dev")] = deviceObj;
if (!(String(stateCls) == String("total_increasing")))
doc2[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!?
@ -425,13 +433,13 @@ class PubMqtt {
doc2[F("stat_cla")] = String(stateCls);
if (!total)
snprintf(topic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
snprintf(topic.data(), topic.size(), "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
else // total values
snprintf(topic, 64, "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(), fields[fldTotal[mDiscovery.sub]]);
snprintf(topic.data(), topic.size(), "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(), fields[fldTotal[mDiscovery.sub]]);
size_t size = measureJson(doc2) + 1;
memset(buf, 0, size);
serializeJson(doc2, buf, size);
publish(topic, buf, true, false);
buf.fill(0);
serializeJson(doc2, buf.data(), size);
publish(topic.data(), buf.data(), true, false);
if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) {
mDiscovery.sub = 0;
@ -501,15 +509,15 @@ class PubMqtt {
mLastIvState[id] = status;
changed = true;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
snprintf(mVal, 40, "%d", (uint8_t)status);
publish(mSubTopic, mVal, true);
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/available", iv->config->name);
snprintf(mVal.data(), mVal.size(), "%d", (uint8_t)status);
publish(mSubTopic.data(), mVal.data(), true);
}
}
if(changed) {
snprintf(mVal, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE)));
publish("status", mVal, true);
snprintf(mVal.data(), mVal.size(), "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE)));
publish("status", mVal.data(), true);
}
return anyAvail;
@ -531,19 +539,19 @@ class PubMqtt {
mSendAlarm[i] = false;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/cnt", iv->config->name);
snprintf(mVal, 40, "%d", iv->alarmCnt);
publish(mSubTopic, mVal, false);
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/alarm/cnt", iv->config->name);
snprintf(mVal.data(), mVal.size(), "%d", iv->alarmCnt);
publish(mSubTopic.data(), mVal.data(), false);
for(uint8_t j = 0; j < 10; j++) {
if(0 != iv->lastAlarm[j].code) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d", iv->config->name, j);
snprintf(mVal, 100, "{\"code\":%d,\"str\":\"%s\",\"start\":%d,\"end\":%d}",
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/alarm/%d", iv->config->name, j);
snprintf(mVal.data(), mVal.size(), "{\"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);
publish(mSubTopic.data(), mVal.data(), false);
yield();
}
}
@ -573,9 +581,9 @@ class PubMqtt {
}
}
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(mVal, 40, "%g", ah::round3(iv->getValue(i, rec)));
publish(mSubTopic, mVal, retained);
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(mVal.data(), mVal.size(), "%g", ah::round3(iv->getValue(i, rec)));
publish(mSubTopic.data(), mVal.data(), retained);
yield();
}
@ -590,40 +598,39 @@ class PubMqtt {
if(mSendList.empty())
return;
mSendIvData.start(mZeroValues);
mZeroValues = false;
SendIvData.start();
mLastAnyAvail = anyAvail;
}
espMqttClient mClient;
cfgMqtt_t *mCfgMqtt;
cfgMqtt_t *mCfgMqtt = nullptr;
#if defined(ESP8266)
WiFiEventHandler mHWifiCon, mHWifiDiscon;
#endif
HMSYSTEM *mSys;
PubMqttIvData<HMSYSTEM> mSendIvData;
HMSYSTEM *mSys = nullptr;
PubMqttIvData<HMSYSTEM> SendIvData;
uint32_t *mUtcTimestamp, *mUptime;
uint32_t mRxCnt, mTxCnt;
uint32_t *mUtcTimestamp = nullptr, *mUptime = nullptr;
uint32_t mRxCnt = 0, mTxCnt = 0;
std::queue<sendListCmdIv> mSendList;
std::array<bool, MAX_NUM_INVERTERS> mSendAlarm{};
subscriptionCb mSubscriptionCb;
bool mLastAnyAvail;
bool mZeroValues;
InverterStatus mLastIvState[MAX_NUM_INVERTERS];
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS];
uint16_t mIntervalTimeout;
std::array<bool, MAX_NUM_INVERTERS> mSendAlarm;
subscriptionCb mSubscriptionCb = nullptr;
bool mLastAnyAvail = false;
std::array<InverterStatus, MAX_NUM_INVERTERS> mLastIvState;
std::array<uint32_t, MAX_NUM_INVERTERS> mIvLastRTRpub;
uint16_t mIntervalTimeout = 0;
// 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
std::array<char, (MQTT_TOPIC_LEN + 5)> mLwtTopic;
const char *mDevName = nullptr, *mVersion = nullptr;
std::array<char, 24> mClientId; // 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[100];
discovery_t mDiscovery;
std::array<char, (MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1)> mTopic;
std::array<char, (32 + MAX_NAME_LENGTH + 1)> mSubTopic;
std::array<char, 100> mVal;
discovery_t mDiscovery = {true, 0, 0, 0};
};
#endif /*ENABLE_MQTT*/
#endif /*__PUB_MQTT_H__*/

236
src/publisher/pubMqttIvData.h

@ -1,11 +1,12 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __PUB_MQTT_IV_DATA_H__
#define __PUB_MQTT_IV_DATA_H__
#include <array>
#include "../utils/dbg.h"
#include "../hm/hmSystem.h"
#include "pubMqttDefs.h"
@ -21,21 +22,22 @@ struct sendListCmdIv {
template<class HMSYSTEM>
class PubMqttIvData {
public:
PubMqttIvData() : mTotal{}, mSubTopic{}, mVal{} {}
void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) {
mSys = sys;
mUtcTimestamp = utcTs;
mSendList = sendList;
mState = IDLE;
mZeroValues = false;
mSys = sys;
mUtcTimestamp = utcTs;
mSendList = sendList;
mState = IDLE;
mYldTotalStore = 0;
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() {
@ -43,11 +45,14 @@ class PubMqttIvData {
yield();
}
bool start(bool zeroValues = false) {
void resetYieldDay() {
mYldTotalStore = 0;
}
bool start() {
if(IDLE != mState)
return false;
mZeroValues = zeroValues;
mRTRDataHasBeenSent = false;
mState = START;
return true;
@ -76,7 +81,7 @@ class PubMqttIvData {
if((RealTimeRunData_Debug != mCmd) || !mRTRDataHasBeenSent) { // send RealTimeRunData only once
mSendTotals = (RealTimeRunData_Debug == mCmd);
memset(mTotal, 0, sizeof(float) * 4);
memset(mTotal, 0, sizeof(float) * 5);
mState = FIND_NXT_IV;
} else
mSendList->pop();
@ -102,25 +107,30 @@ class PubMqttIvData {
mPos = 0;
if(found) {
record_t<> *rec = mIv->getRecordStruct(mCmd);
if((RealTimeRunData_Debug == mCmd) && mIv->getLastTs(rec) != 0 ) { //workaround for startup. Suspect, mCmd might cause to much messages....
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", mIv->config->name);
snprintf(mVal, 40, "%d", mIv->getLastTs(rec));
mPublish(mSubTopic, mVal, true, QOS_0);
if(MqttSentStatus::NEW_DATA == rec->mqttSentStatus) {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/last_success", mIv->config->name);
snprintf(mVal.data(), mVal.size(), "%d", mIv->getLastTs(rec));
mPublish(mSubTopic.data(), mVal.data(), 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);
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch0/rssi", mIv->config->name);
snprintf(mVal.data(), mVal.size(), "%d", mIv->rssi);
mPublish(mSubTopic.data(), mVal.data(), false, QOS_0);
}
rec->mqttSentStatus = MqttSentStatus::LAST_SUCCESS_SENT;
}
mIv->isProducing(); // recalculate status
mState = SEND_DATA;
} else if(mSendTotals && mTotalFound)
} else if(mSendTotals && mTotalFound) {
if(mYldTotalStore > mTotal[2])
mSendTotalYd = false; // don't send yield total if last value was greater
else
mYldTotalStore = mTotal[2];
mState = SEND_TOTALS;
else {
} else {
mSendList->pop();
mZeroValues = false;
mState = START;
}
}
@ -132,82 +142,97 @@ class PubMqttIvData {
DPRINT(DBG_WARN, "unknown record to publish!");
return;
}
uint32_t lastTs = mIv->getLastTs(rec);
bool pubData = (lastTs > 0);
if (mCmd == RealTimeRunData_Debug)
pubData &= (lastTs != mIvLastRTRpub[mIv->id]);
if (pubData) {
if(mPos < rec->length) {
bool retained = false;
if (mCmd == RealTimeRunData_Debug) {
if((FLD_YT == rec->assign[mPos].fieldId) || (FLD_YD == rec->assign[mPos].fieldId))
retained = true;
// calculate total values for RealTimeRunData_Debug
if (CH0 == rec->assign[mPos].ch) {
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;
}
}
} else
mAllTotalFound = false;
}
} else
mIvLastRTRpub[mIv->id] = lastTs;
uint8_t qos = QOS_0;
if(FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId)
qos = QOS_2;
if(mPos < rec->length) {
bool retained = false;
if (RealTimeRunData_Debug == mCmd) {
if((FLD_YT == rec->assign[mPos].fieldId) || (FLD_YD == rec->assign[mPos].fieldId))
retained = true;
// calculate total values for RealTimeRunData_Debug
if (CH0 == rec->assign[mPos].ch) {
if(mIv->getStatus() != InverterStatus::OFF) {
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: {
mTotal[2] += mIv->getValue(mPos, rec);
break;
}
case FLD_PDC:
mTotal[3] += mIv->getValue(mPos, rec);
break;
case FLD_MP:
mTotal[4] += mIv->getValue(mPos, rec);
break;
}
} else
mAllTotalFound = false;
}
}
if((mIvSend == mIv) || (NULL == mIvSend)) { // send only updated values, or all if the inverter is NULL
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]);
snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec)));
mPublish(mSubTopic, mVal, retained, qos);
if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) {
if(InverterDevInform_All == mCmd) {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/firmware", mIv->config->name);
snprintf(mVal.data(), mVal.size(), "{\"version\":%d,\"build_year\":\"%d\",\"build_month_day\":%d,\"build_hour_min\":%d,\"bootloader\":%d}",
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_FW_VERSION, rec)),
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_FW_BUILD_YEAR, rec)),
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_FW_BUILD_MONTH_DAY, rec)),
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_FW_BUILD_HOUR_MINUTE, rec)),
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_BOOTLOADER_VER, rec)));
retained = true;
} else if(InverterDevInform_Simple == mCmd) {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/hardware", mIv->config->name);
snprintf(mVal.data(), mVal.size(), "{\"part\":%d,\"version\":\"%d\",\"grid_profile_code\":%d,\"grid_profile_version\":%d}",
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_PART_NUM, rec)),
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_HW_VERSION, rec)),
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec)),
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_GRID_PROFILE_VERSION, rec)));
retained = true;
} else {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]);
snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec)));
}
mPos++;
} else {
uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0;
if((FLD_EVT != rec->assign[mPos].fieldId)
&& (FLD_LAST_ALARM_CODE != rec->assign[mPos].fieldId))
mPublish(mSubTopic.data(), mVal.data(), retained, qos);
}
mPos++;
} else {
if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) {
sendRadioStat(rec->length);
mState = FIND_NXT_IV;
rec->mqttSentStatus = MqttSentStatus::DATA_SENT;
}
} 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}",
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/radio_stat", mIv->config->name);
snprintf(mVal.data(), mVal.size(), "{\"tx\":%d,\"success\":%d,\"fail\":%d,\"no_answer\":%d,\"retransmits\":%d,\"lossIvRx\":%d,\"lossIvTx\":%d,\"lossDtuRx\":%d,\"lossDtuTx\":%d}",
mIv->radioStatistics.txCnt,
mIv->radioStatistics.rxSuccess,
mIv->radioStatistics.rxFail,
mIv->radioStatistics.rxFailNoAnser,
mIv->radioStatistics.retransmits);
mPublish(mSubTopic, mVal, false, QOS_0);
mIv->radioStatistics.retransmits,
mIv->radioStatistics.ivLoss,
mIv->radioStatistics.ivSent,
mIv->radioStatistics.dtuLoss,
mIv->radioStatistics.dtuSent);
mPublish(mSubTopic.data(), mVal.data(), false, QOS_0);
}
void stateSendTotals() {
uint8_t fieldId;
mRTRDataHasBeenSent = true;
if(mPos < 4) {
if(mPos < 5) {
uint8_t fieldId;
bool retained = true;
switch (mPos) {
default:
@ -233,40 +258,41 @@ class PubMqttIvData {
fieldId = FLD_PDC;
retained = false;
break;
case 4:
fieldId = FLD_MP;
retained = false;
break;
}
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
snprintf(mVal, 40, "%g", ah::round3(mTotal[mPos]));
mPublish(mSubTopic, mVal, retained, QOS_0);
snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]);
snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos]));
mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0);
mPos++;
} else {
mSendList->pop();
mZeroValues = false;
mPos = 0;
mSendTotals = false;
mState = IDLE;
}
}
HMSYSTEM *mSys;
uint32_t *mUtcTimestamp;
HMSYSTEM *mSys = nullptr;
uint32_t *mUtcTimestamp = nullptr;
pubMqttPublisherType mPublish;
State mState;
State mState = IDLE;
StateFunction mTable[NUM_STATES];
uint8_t mCmd;
uint8_t mLastIvId;
bool mSendTotals, mTotalFound, mAllTotalFound, mSendTotalYd;
float mTotal[4];
uint8_t mCmd = 0;
uint8_t mLastIvId = 0;
bool mSendTotals = false, mTotalFound = false, mAllTotalFound = false, mSendTotalYd = false;
float mTotal[5], mYldTotalStore = 0;
Inverter<> *mIv, *mIvSend;
uint8_t mPos;
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS];
bool mRTRDataHasBeenSent;
Inverter<> *mIv = nullptr, *mIvSend = nullptr;
uint8_t mPos = 0;
bool mRTRDataHasBeenSent = false;
char mSubTopic[32 + MAX_NAME_LENGTH + 1];
char mVal[100];
bool mZeroValues; // makes sure that yield day is sent even if no inverter is online
std::array<char, (32 + MAX_NAME_LENGTH + 1)> mSubTopic;
std::array<char, 140> mVal;
std::queue<sendListCmdIv> *mSendList;
std::queue<sendListCmdIv> *mSendList = nullptr;
};
#endif /*__PUB_MQTT_IV_DATA_H__*/

12
src/publisher/pubSerial.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __PUB_SERIAL_H__
@ -13,8 +13,6 @@
template<class HMSYSTEM>
class PubSerial {
public:
PubSerial() {}
void setup(settings_t *cfg, HMSYSTEM *sys, uint32_t *utcTs) {
mCfg = cfg;
mSys = sys;
@ -46,9 +44,9 @@ class PubSerial {
}
private:
settings_t *mCfg;
HMSYSTEM *mSys;
uint32_t *mUtcTimestamp;
settings_t *mCfg = nullptr;
HMSYSTEM *mSys = nullptr;
uint32_t *mUtcTimestamp = nullptr;
};

6
src/utils/crc.cpp

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#include "crc.h"
@ -19,7 +19,7 @@ namespace ah {
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start) {
uint16_t crc = start;
uint8_t shift = 0;
uint8_t shift;
for(uint8_t i = 0; i < len; i ++) {
crc = crc ^ buf[i];

8
src/utils/helper.cpp

@ -75,8 +75,9 @@ namespace ah {
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);
uint16_t m = t % 1000;
t = t / 1000;
sprintf(str, "%02d:%02d:%02d.%03d", hour(t), minute(t), second(t), m);
}
return String(str);
}
@ -84,14 +85,13 @@ namespace ah {
uint64_t Serial2u64(const char *val) {
char tmp[3];
uint64_t ret = 0ULL;
uint64_t u64;
memset(tmp, 0, 3);
for(uint8_t i = 0; i < 6; i++) {
tmp[0] = val[i*2];
tmp[1] = val[i*2 + 1];
if((tmp[0] == '\0') || (tmp[1] == '\0'))
break;
u64 = strtol(tmp, NULL, 16);
uint64_t u64 = strtol(tmp, NULL, 16);
ret |= (u64 << ((5-i) << 3));
}
return ret;

26
src/utils/improv.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://github.com/lumpapu/ahoy
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -10,6 +10,7 @@
#include <functional>
#include "dbg.h"
#include "AsyncJson.h"
#include "../appInterface.h"
// https://www.improv-wifi.com/serial/
// https://github.com/jnthas/improv-wifi-demo/blob/main/src/esp32-wifiimprov/esp32-wifiimprov.ino
@ -70,15 +71,15 @@ class Improv {
TYPE_RPC_RESPONSE = 0x04
};
void dumpBuf(uint8_t buf[], uint8_t len) {
void dumpBuf(const uint8_t buf[], uint8_t len) {
for(uint8_t i = 0; i < len; i++) {
DHEX(buf[i], false);
DBGPRINT(" ", false);
DHEX(buf[i]);
DBGPRINT(F(" "));
}
DBGPRINTLN("", false);
DBGPRINTLN("");
}
inline uint8_t buildChecksum(uint8_t buf[], uint8_t len) {
inline uint8_t buildChecksum(const uint8_t buf[], uint8_t len) {
uint8_t calc = 0;
for(uint8_t i = 0; i < len; i++) {
calc += buf[i];
@ -86,7 +87,7 @@ class Improv {
return calc;
}
inline bool checkChecksum(uint8_t buf[], uint8_t len) {
inline bool checkChecksum(const uint8_t buf[], uint8_t len) {
/*DHEX(buf[len], false);
DBGPRINT(F(" == "), false);
DBGHEXLN(buildChecksum(buf, len), false);*/
@ -97,7 +98,7 @@ class Improv {
if(len < 11)
return false;
if(0 != strncmp((char*)buf, "IMPROV", 6))
if(0 != strncmp(reinterpret_cast<char*>(buf), "IMPROV", 6))
return false;
// version check (only version 1 is supported!)
@ -199,7 +200,7 @@ class Improv {
dumpBuf(buf, len);
}
void parsePayload(uint8_t type, uint8_t buf[], uint8_t len) {
void parsePayload(uint8_t type, const uint8_t buf[], uint8_t len) {
if(TYPE_RPC == type) {
if(GET_CURRENT_STATE == buf[0]) {
setDebugEn(false);
@ -212,9 +213,10 @@ class Improv {
}
}
IApp *mApp;
const char *mDevName, *mVersion;
bool mScanRunning;
IApp *mApp = nullptr;
const char *mDevName = nullptr;
const char *mVersion = nullptr;
bool mScanRunning = false;
};
#endif

30
src/utils/scheduler.h

@ -1,13 +1,13 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// Lukas Pusch, lukas@lpusch.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __SCHEDULER_H__
#define __SCHEDULER_H__
#include <functional>
#include <array>
#include "dbg.h"
namespace ah {
@ -29,18 +29,16 @@ namespace ah {
class Scheduler {
public:
Scheduler() {}
void setup(bool directStart) {
mUptime = 0;
mTimestamp = (directStart) ? 1 : 0;
mTsMillis = 0;
mMax = 0;
mPrevMillis = millis();
mTsMillis = mPrevMillis % 1000;
resetTicker();
}
void loop(void) {
virtual void loop(void) {
mMillis = millis();
mDiff = mMillis - mPrevMillis;
if (mDiff < 1000)
@ -62,7 +60,7 @@ namespace ah {
mUptime += mDiffSeconds;
if(0 != mTimestamp) {
mTimestamp += mDiffSeconds;
mTsMillis = mMillis % 1000;
mTsMillis = mPrevMillis % 1000;
}
checkTicker();
@ -80,7 +78,6 @@ namespace ah {
virtual void setTimestamp(uint32_t ts) {
mTimestamp = ts;
mTsMillis = millis() % 1000;
}
bool resetEveryById(uint8_t id) {
@ -95,8 +92,7 @@ namespace ah {
}
inline void resetTicker(void) {
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++)
mTickerInUse[i] = false;
mTickerInUse.fill(false);
}
void getStat(uint8_t *max) {
@ -121,7 +117,7 @@ namespace ah {
uint16_t mTsMillis;
private:
inline uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) {
uint8_t addTicker(scdCb c, uint32_t timeout, uint32_t reload, bool isTimestamp, const char *name) {
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) {
if (!mTickerInUse[i]) {
mTickerInUse[i] = true;
@ -161,11 +157,11 @@ namespace ah {
}
}
sP mTicker[MAX_NUM_TICKER];
bool mTickerInUse[MAX_NUM_TICKER];
uint32_t mMillis, mPrevMillis, mDiff;
uint8_t mDiffSeconds;
uint8_t mMax;
std::array<sP, MAX_NUM_TICKER> mTicker;
std::array<bool, MAX_NUM_TICKER> mTickerInUse;
uint32_t mMillis = 0, mPrevMillis = 0, mDiff = 0;
uint8_t mDiffSeconds = 0;
uint8_t mMax = 0;
};
}

4
src/utils/spiPatcher.cpp

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#if defined(ESP32)

6
src/utils/spiPatcher.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __SPI_PATCHER_H__
@ -16,7 +16,7 @@
class SpiPatcher {
protected:
SpiPatcher(spi_host_device_t dev) :
explicit SpiPatcher(spi_host_device_t dev) :
mHostDevice(dev), mCurHandle(nullptr) {
// Use binary semaphore instead of mutex for performance reasons
mutex = xSemaphoreCreateBinaryStatic(&mutex_buffer);

13
src/utils/syslog.cpp

@ -1,3 +1,8 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#include <algorithm>
#include "syslog.h"
@ -5,6 +10,7 @@
#define SYSLOG_MAX_PACKET_SIZE 256
DbgSyslog::DbgSyslog() : mSyslogBuffer{} {}
//-----------------------------------------------------------------------------
void DbgSyslog::setup(settings_t *config) {
@ -62,12 +68,11 @@ void DbgSyslog::syslogCb (String msg)
// 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];
char saveChar = mSyslogBuffer[packetStart+packetSize];
mSyslogBuffer[packetStart+packetSize] = 0;
log(mConfig->sys.deviceName,SYSLOG_FACILITY, mSyslogSeverity, &mSyslogBuffer[packetStart]);
mSyslogBuffer[packetStart+packetSize] = saveChar;
@ -87,7 +92,7 @@ void DbgSyslog::log(const char *hostname, uint8_t facility, uint8_t severity, ch
// 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);
int len = snprintf(static_cast<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
@ -96,4 +101,4 @@ void DbgSyslog::log(const char *hostname, uint8_t facility, uint8_t severity, ch
mSyslogUdp.endPacket();
}
#endif
#endif

5
src/utils/syslog.h

@ -36,6 +36,7 @@
class DbgSyslog {
public:
DbgSyslog();
void setup (settings_t *config);
void syslogCb(String msg);
void log(const char *hostname, uint8_t facility, uint8_t severity, char* msg);
@ -43,7 +44,7 @@ class DbgSyslog {
private:
WiFiUDP mSyslogUdp;
IPAddress mSyslogIP;
settings_t *mConfig;
settings_t *mConfig = nullptr;
char mSyslogBuffer[SYSLOG_BUF_SIZE+1];
uint16_t mSyslogBufFill = 0;
int mSyslogSeverity = PRI_NOTICE;
@ -51,4 +52,4 @@ class DbgSyslog {
#endif /*ENABLE_SYSLOG*/
#endif /*__SYSLOG_H__*/
#endif /*__SYSLOG_H__*/

21
src/utils/timemonitor.h

@ -20,18 +20,16 @@
class TimeMonitor {
public:
/**
* A constructor for initializing a TimeMonitor
* @note TimeMonitor witch default constructor is stopped
* A constructor for creating a TimeMonitor object
*/
TimeMonitor(void) {}
TimeMonitor() {}
/**
* A constructor for initializing a TimeMonitor
* A constructor for initializing a TimeMonitor object
* @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) {
explicit TimeMonitor(uint32_t timeout, bool start = false) {
if (start)
startTimeMonitor(timeout);
else
@ -50,7 +48,8 @@ class TimeMonitor {
/**
* Restart the TimeMonitor with already set timeout configuration
* @note returns nothing
* @note a timeout has to be set before, no need to call
* 'startTimeMonitor' before
*/
void reStartTimeMonitor(void) {
mStartTime = millis();
@ -81,8 +80,8 @@ class TimeMonitor {
* true: TimeMonitor already timed out
* false: TimeMonitor still in time or TimeMonitor was stopped
*/
bool isTimeout(void) {
if ((mStarted) && (millis() - mStartTime >= mTimeout))
bool isTimeout(void) const {
if ((mStarted) && ((millis() - mStartTime) >= mTimeout))
return true;
else
return false;
@ -104,7 +103,7 @@ class TimeMonitor {
*/
uint32_t getResidualTime(void) const {
uint32_t delayed = millis() - mStartTime;
return(mStarted ? (delayed < mTimeout ? mTimeout - delayed : 0UL) : 0xFFFFFFFFUL);
return(mStarted ? (delayed < mTimeout ? (mTimeout - delayed) : 0UL) : 0xFFFFFFFFUL);
}
/**
@ -123,4 +122,4 @@ class TimeMonitor {
bool mStarted = false; // start/stop state of the TimeMonitor
};
#endif
#endif

7
src/web/Protection.cpp

@ -0,0 +1,7 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#include "Protection.h"
Protection *Protection::mInstance = nullptr;

122
src/web/Protection.h

@ -0,0 +1,122 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __PROTECTION_H__
#define __PROTECTION_H__
#pragma once
#include <array>
#include <cstdint>
#include "../config/config.h"
#include "../utils/helper.h"
class Protection {
protected:
explicit Protection(const char *pwd) {
mPwd = pwd;
mLogoutTimeout = 0;
mWebIp.fill(0);
mApiIp.fill(0);
mToken.fill(0);
}
public:
Protection(Protection &other) = delete;
void operator=(const Protection &) = delete;
static Protection* getInstance(const char *pwd) {
if(nullptr == mInstance)
mInstance = new Protection(pwd);
return mInstance;
}
void tickSecond() { // auto logout
if(0 != mLogoutTimeout) {
if (0 == --mLogoutTimeout) {
if(mPwd[0] != '\0')
lock(false);
}
}
}
void lock(bool fromWeb) {
mWebIp.fill(0);
if(fromWeb)
return;
mApiIp.fill(0);
mToken.fill(0);
}
char *unlock(const char *clientIp, bool loginFromWeb) {
mLogoutTimeout = LOGOUT_TIMEOUT;
if(loginFromWeb)
ah::ip2Arr(static_cast<uint8_t*>(mWebIp.data()), clientIp);
else {
ah::ip2Arr(static_cast<uint8_t*>(mApiIp.data()), clientIp);
genToken();
}
return reinterpret_cast<char*>(mToken.data());
}
void resetLockTimeout(void) {
if(0 != mLogoutTimeout)
mLogoutTimeout = LOGOUT_TIMEOUT;
}
bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const {
if(mPwd[0] == '\0') // no password set
return false;
if(askedFromWeb)
return !isIdentical(clientIp, mWebIp);
if(nullptr == token)
return true;
if('*' == token[0]) // call from WebUI
return !isIdentical(clientIp, mWebIp);
if(isIdentical(clientIp, mApiIp))
return (0 != strncmp(token, mToken.data(), 16));
return true;
}
private:
void genToken() {
mToken.fill(0);
for(uint8_t i = 0; i < 16; i++) {
mToken[i] = random(1, 35);
// convert to ascii number 1-9 (zero isn't allowed) or upper
// case character A-Z
mToken[i] += (mToken[i] < 10) ? 0x30 : 0x37;
}
}
bool isIdentical(const char *clientIp, const std::array<uint8_t, 4> cmp) const {
std::array<uint8_t, 4> ip;
ah::ip2Arr(static_cast<uint8_t*>(ip.data()), clientIp);
for(uint8_t i = 0; i < 4; i++) {
if(cmp[i] != ip[i])
return false;
}
return true;
}
protected:
static Protection *mInstance;
private:
const char *mPwd;
uint16_t mLogoutTimeout = 0;
std::array<uint8_t, 4> mWebIp, mApiIp;
std::array<char, 17> mToken;
};
#endif /*__PROTECTION_H__*/

254
src/web/RestApi.h

@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __WEB_API_H__
@ -15,6 +15,7 @@
#include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/helper.h"
#include "lang.h"
#include "AsyncJson.h"
#if defined(ETHERNET)
#include "AsyncWebServer_ESP32_W5500.h"
@ -22,15 +23,13 @@
#include "ESPAsyncWebServer.h"
#endif
#include "plugins/history.h"
#if defined(F) && defined(ESP32)
#undef F
#define F(sl) (sl)
#endif
const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP};
const uint8_t acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP};
const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP};
template<class HMSYSTEM>
class RestApi {
public:
@ -65,9 +64,9 @@ class RestApi {
DynamicJsonDocument json(128);
JsonObject dummy = json.as<JsonObject>();
if(obj[F("path")] == "ctrl")
setCtrl(obj, dummy);
setCtrl(obj, dummy, "*");
else if(obj[F("path")] == "setup")
setSetup(obj, dummy);
setSetup(obj, dummy, "*");
}
private:
@ -100,8 +99,10 @@ class RestApi {
else if(path == "setup") getSetup(request, root);
#if !defined(ETHERNET)
else if(path == "setup/networks") getNetworks(root);
else if(path == "setup/getip") getWifiIp(root);
#endif /* !defined(ETHERNET) */
else if(path == "live") getLive(request,root);
else if (path == "powerHistory") getPowerHistory(request, root);
else {
if(path.substring(0, 12) == "inverter/id/")
getInverter(root, request->url().substring(17).toInt());
@ -136,7 +137,7 @@ class RestApi {
#endif
}
void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
void onApiPostBody(AsyncWebServerRequest *request, const uint8_t *data, size_t len, size_t index, size_t total) {
DPRINTLN(DBG_VERBOSE, "onApiPostBody");
if(0 == index) {
@ -153,7 +154,7 @@ class RestApi {
DynamicJsonDocument json(1000);
DeserializationError err = deserializeJson(json, (const char *)mTmpBuf, mTmpSize);
DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize);
JsonObject obj = json.as<JsonObject>();
AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
@ -162,20 +163,20 @@ class RestApi {
if(!err) {
String path = request->url().substring(5);
if(path == "ctrl")
root[F("success")] = setCtrl(obj, root);
root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str());
else if(path == "setup")
root[F("success")] = setSetup(obj, root);
root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str());
else {
root[F("success")] = false;
root[F("error")] = "Path not found: " + path;
root[F("error")] = F(PATH_NOT_FOUND) + path;
}
} else {
switch (err.code()) {
case DeserializationError::Ok: break;
case DeserializationError::IncompleteInput: root[F("error")] = F("Incomplete input"); break;
case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break;
case DeserializationError::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_MEM); break;
default: root[F("error")] = F(DESER_FAILED); break;
}
}
@ -194,14 +195,24 @@ class RestApi {
ep[F("generic")] = url + F("generic");
ep[F("index")] = url + F("index");
ep[F("setup")] = url + F("setup");
#if !defined(ETHERNET)
ep[F("setup/networks")] = url + F("setup/networks");
ep[F("setup/getip")] = url + F("setup/getip");
#endif /* !defined(ETHERNET) */
ep[F("system")] = url + F("system");
ep[F("live")] = url + F("live");
#if defined(ENABLE_HISTORY)
ep[F("powerHistory")] = url + F("powerHistory");
#endif
}
void onDwnldSetup(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response;
// save settings to have latest firmware changes in export
mApp->saveSettings(false);
File fp = LittleFS.open("/settings.json", "r");
if(!fp) {
DPRINTLN(DBG_ERROR, F("failed to load settings"));
@ -242,14 +253,22 @@ class RestApi {
}
void getGeneric(AsyncWebServerRequest *request, JsonObject obj) {
mApp->resetLockTimeout();
obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI();
obj[F("ts_uptime")] = mApp->getUptime();
obj[F("ts_now")] = mApp->getTimestamp();
obj[F("version")] = String(mApp->getVersion());
obj[F("modules")] = String(mApp->getVersionModules());
obj[F("build")] = String(AUTO_GIT_HASH);
obj[F("menu_prot")] = mApp->getProtection(request);
obj[F("env")] = String(ENV_NAME);
obj[F("menu_prot")] = mApp->isProtected(request->client()->remoteIP().toString().c_str(), "", true);
obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask );
obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0);
obj[F("menu_protEn")] = (bool) (mConfig->sys.adminPwd[0] != '\0');
obj[F("cst_lnk")] = String(mConfig->plugin.customLink);
obj[F("cst_lnk_txt")] = String(mConfig->plugin.customLinkText);
obj[F("region")] = mConfig->sys.region;
obj[F("timezone")] = mConfig->sys.timezone;
#if defined(ESP32)
obj[F("esp_type")] = F("ESP32");
@ -318,7 +337,9 @@ class RestApi {
void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) {
getSysInfo(request, obj.createNestedObject(F("system")));
getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">AhoyFactory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>");
char tmp[200];
snprintf(tmp, 200, "<a href=\"/factory\" class=\"btn\">%s</a><br/><br/><a href=\"/reboot\" class=\"btn\">%s</a>", FACTORY_RESET, BTN_REBOOT);
obj[F("html")] = String(tmp);
}
void getHtmlLogout(AsyncWebServerRequest *request, JsonObject obj) {
@ -395,7 +416,7 @@ class RestApi {
void getIvStatistis(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) {
obj[F("error")] = F("inverter not found!");
obj[F("error")] = F(INV_NOT_FOUND);
return;
}
obj[F("name")] = String(iv->config->name);
@ -405,12 +426,16 @@ class RestApi {
obj[F("frame_cnt")] = iv->radioStatistics.frmCnt;
obj[F("tx_cnt")] = iv->radioStatistics.txCnt;
obj[F("retransmits")] = iv->radioStatistics.retransmits;
obj[F("ivLoss")] = iv->radioStatistics.ivLoss;
obj[F("ivSent")] = iv->radioStatistics.ivSent;
obj[F("dtuLoss")] = iv->radioStatistics.dtuLoss;
obj[F("dtuSent")] = iv->radioStatistics.dtuSent;
}
void getIvPowerLimitAck(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) {
obj[F("error")] = F("inverter not found!");
obj[F("error")] = F(INV_NOT_FOUND);
return;
}
obj["ack"] = (bool)iv->powerLimitAck;
@ -433,7 +458,6 @@ class RestApi {
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
@ -457,13 +481,12 @@ class RestApi {
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) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) {
obj[F("error")] = F("inverter not found!");
obj[F("error")] = F(INV_NOT_FOUND);
return;
}
@ -473,7 +496,7 @@ class RestApi {
obj[F("name")] = String(iv->config->name);
obj[F("serial")] = String(iv->config->serial.u64, HEX);
obj[F("version")] = String(iv->getFwVersion());
obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit);
obj[F("power_limit_read")] = iv->actPowerLimit;
obj[F("power_limit_ack")] = iv->powerLimitAck;
obj[F("max_pwr")] = iv->getMaxPower();
obj[F("ts_last_success")] = rec->ts;
@ -481,6 +504,7 @@ class RestApi {
obj[F("status")] = (uint8_t)iv->getStatus();
obj[F("alarm_cnt")] = iv->alarmCnt;
obj[F("rssi")] = iv->rssi;
obj[F("ts_max_ac_pwr")] = iv->tsMaxAcPower;
JsonArray ch = obj.createNestedArray("ch");
@ -525,7 +549,7 @@ class RestApi {
void getIvAlarms(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) {
obj[F("error")] = F("inverter not found!");
obj[F("error")] = F(INV_NOT_FOUND);
return;
}
@ -548,7 +572,7 @@ class RestApi {
void getIvVersion(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL == iv) {
obj[F("error")] = F("inverter not found!");
obj[F("error")] = F(INV_NOT_FOUND);
return;
}
@ -600,7 +624,8 @@ 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("offs")] = mConfig->sun.offsetSec;
obj[F("offsSr")] = mConfig->sun.offsetSecMorning;
obj[F("offsSs")] = mConfig->sun.offsetSecEvening;
}
void getPinout(JsonObject obj) {
@ -610,8 +635,9 @@ class RestApi {
obj[F("sclk")] = mConfig->nrf.pinSclk;
obj[F("mosi")] = mConfig->nrf.pinMosi;
obj[F("miso")] = mConfig->nrf.pinMiso;
obj[F("led0")] = mConfig->led.led0;
obj[F("led1")] = mConfig->led.led1;
obj[F("led0")] = mConfig->led.led[0];
obj[F("led1")] = mConfig->led.led[1];
obj[F("led2")] = mConfig->led.led[2];
obj[F("led_high_active")] = mConfig->led.high_active;
obj[F("led_lum")] = mConfig->led.luminance;
}
@ -624,6 +650,9 @@ class RestApi {
obj[F("fcsb")] = mConfig->cmt.pinFcsb;
obj[F("gpio3")] = mConfig->cmt.pinIrq;
obj[F("en")] = (bool) mConfig->cmt.enabled;
std::pair<uint16_t, uint16_t> range = mRadioCmt->getFreqRangeMhz();
obj[F("freq_min")] = range.first;
obj[F("freq_max")] = range.second;
}
void getRadioCmtInfo(JsonObject obj) {
@ -631,6 +660,7 @@ class RestApi {
if(mConfig->cmt.enabled) {
obj[F("isconnected")] = mRadioCmt->isChipConnected();
obj[F("sn")] = String(mRadioCmt->getDTUSn(), HEX);
obj[F("irqOk")] = mRadioCmt->mIrqOk;
}
}
#endif
@ -641,6 +671,7 @@ class RestApi {
obj[F("isconnected")] = mRadioNrf->isChipConnected();
obj[F("dataRate")] = mRadioNrf->getDataRate();
obj[F("sn")] = String(mRadioNrf->getDTUSn(), HEX);
obj[F("irqOk")] = mRadioNrf->mIrqOk;
}
}
@ -649,6 +680,7 @@ class RestApi {
obj[F("debug")] = mConfig->serial.debug;
obj[F("priv")] = mConfig->serial.privacyLog;
obj[F("wholeTrace")] = mConfig->serial.printWholeTrace;
obj[F("log2mqtt")] = mConfig->serial.log2mqtt;
}
void getStaticIp(JsonObject obj) {
@ -661,18 +693,20 @@ 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_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;
obj[F("disp_data")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_data;
obj[F("disp_cs")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs;
obj[F("disp_dc")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc;
obj[F("disp_rst")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset;
obj[F("disp_bsy")] = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy;
obj[F("pir_pin")] = mConfig->plugin.display.pirPin;
obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type;
obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline;
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_graph_ratio")] = (uint8_t)mConfig->plugin.display.graph_ratio;
obj[F("disp_graph_size")] = (uint8_t)mConfig->plugin.display.graph_size;
obj[F("disp_clk")] = mConfig->plugin.display.disp_clk;
obj[F("disp_data")] = mConfig->plugin.display.disp_data;
obj[F("disp_cs")] = mConfig->plugin.display.disp_cs;
obj[F("disp_dc")] = mConfig->plugin.display.disp_dc;
obj[F("disp_rst")] = mConfig->plugin.display.disp_reset;
obj[F("disp_bsy")] = mConfig->plugin.display.disp_busy;
obj[F("pir_pin")] = mConfig->plugin.display.pirPin;
}
void getMqttInfo(JsonObject obj) {
@ -685,10 +719,11 @@ class RestApi {
void getIndex(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("ts_now")] = mApp->getTimestamp();
obj[F("ts_sunrise")] = mApp->getSunrise();
obj[F("ts_sunset")] = mApp->getSunset();
obj[F("ts_offset")] = mConfig->sun.offsetSec;
obj[F("ts_now")] = mApp->getTimestamp();
obj[F("ts_sunrise")] = mApp->getSunrise();
obj[F("ts_sunset")] = mApp->getSunset();
obj[F("ts_offsSr")] = mConfig->sun.offsetSecMorning;
obj[F("ts_offsSs")] = mConfig->sun.offsetSecEvening;
JsonArray inv = obj.createNestedArray(F("inverter"));
Inverter<> *iv;
@ -713,14 +748,16 @@ class RestApi {
obj[F("disNightComm")] = disNightCom;
JsonArray warn = obj.createNestedArray(F("warnings"));
if(!mRadioNrf->isChipConnected() && mConfig->nrf.enabled)
warn.add(F("your NRF24 module can't be reached, check the wiring, pinout and enable"));
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"));
warn.add(F(REBOOT_ESP_APPLY_CHANGES));
if(0 == mApp->getTimestamp())
warn.add(F("time not set. No communication to inverter possible"));
warn.add(F(TIME_NOT_SET));
#if !defined(ETHERNET)
#if !defined(ESP32)
if(mApp->getWasInCh12to14())
warn.add(F(WAS_IN_CH_12_TO_14));
#endif
#endif
}
void getSetup(AsyncWebServerRequest *request, JsonObject obj) {
@ -744,6 +781,9 @@ class RestApi {
void getNetworks(JsonObject obj) {
mApp->getAvailNetworks(obj);
}
void getWifiIp(JsonObject obj) {
obj[F("ip")] = mApp->getStationIp();
}
#endif /* !defined(ETHERNET) */
void getLive(AsyncWebServerRequest *request, JsonObject obj) {
@ -769,11 +809,44 @@ class RestApi {
}
}
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) {
void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ENABLE_HISTORY)
obj[F("refresh")] = mConfig->inst.sendInterval;
uint16_t max = 0;
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld);
obj[F("value")][fld] = value;
if (value > max)
max = value;
}
obj[F("max")] = max;
obj[F("maxDay")] = mApp->getHistoryMaxDay();
#else
obj[F("refresh")] = 86400; // 1 day;
#endif /*ENABLE_HISTORY*/
}
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) {
if(jsonIn.containsKey(F("auth"))) {
if(String(jsonIn[F("auth")]) == String(mConfig->sys.adminPwd)) {
jsonOut[F("token")] = mApp->unlock(clientIP, false);
jsonIn[F("token")] = jsonOut[F("token")];
} else {
jsonOut[F("error")] = F("ERR_AUTH");
return false;
}
if(!jsonIn.containsKey(F("cmd")))
return true;
}
if(isProtected(jsonIn, jsonOut, clientIP))
return false;
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]);
bool accepted = true;
if(NULL == iv) {
jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>();
jsonOut[F("error")] = F("ERR_INDEX");
return false;
}
jsonOut[F("id")] = jsonIn[F("id")];
@ -783,7 +856,7 @@ class RestApi {
else if(F("restart") == jsonIn[F("cmd")])
accepted = iv->setDevControlRequest(Restart);
else if(0 == strncmp("limit_", jsonIn[F("cmd")].as<const char*>(), 6)) {
iv->powerLimit[0] = jsonIn["val"];
iv->powerLimit[0] = static_cast<uint16_t>(jsonIn["val"].as<float>() * 10.0);
if(F("limit_persistent_relative") == jsonIn[F("cmd")])
iv->powerLimit[1] = RelativPersistent;
else if(F("limit_persistent_absolute") == jsonIn[F("cmd")])
@ -794,23 +867,28 @@ class RestApi {
iv->powerLimit[1] = AbsolutNonPersistent;
accepted = iv->setDevControlRequest(ActivePowerContr);
if(accepted)
mApp->triggerTickSend();
} else if(F("dev") == jsonIn[F("cmd")]) {
DPRINTLN(DBG_INFO, F("dev cmd"));
iv->setDevCommand(jsonIn[F("val")].as<int>());
} else {
jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as<String>() + "'";
jsonOut[F("error")] = F("ERR_UNKNOWN_CMD");
return false;
}
if(!accepted) {
jsonOut[F("error")] = F("inverter does not accept dev control request at this moment");
jsonOut[F("error")] = F("ERR_LIMIT_NOT_ACCEPT");
return false;
}
return true;
}
bool setSetup(JsonObject jsonIn, JsonObject jsonOut) {
bool setSetup(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) {
if(isProtected(jsonIn, jsonOut, clientIP))
return false;
#if !defined(ETHERNET)
if(F("scan_wifi") == jsonIn[F("cmd")])
mApp->scanAvailNetworks();
@ -824,6 +902,15 @@ class RestApi {
mTimezoneOffset = jsonIn[F("val")];
else if(F("discovery_cfg") == jsonIn[F("cmd")])
mApp->setMqttDiscoveryFlag(); // for homeassistant
#if !defined(ETHERNET)
else if(F("save_wifi") == jsonIn[F("cmd")]) {
snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", jsonIn[F("ssid")].as<const char*>());
snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", jsonIn[F("pwd")].as<const char*>());
mApp->saveSettings(false); // without reboot
mApp->setStopApAllowedMode(false);
mApp->setupStation();
}
#endif /* !defined(ETHERNET */
else if(F("save_iv") == jsonIn[F("cmd")]) {
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")], false);
iv->config->enabled = jsonIn[F("en")];
@ -840,30 +927,55 @@ class RestApi {
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");
jsonOut[F("error")] = F("ERR_UNKNOWN_CMD");
return false;
}
return true;
}
IApp *mApp;
HMSYSTEM *mSys;
HmRadio<> *mRadioNrf;
bool isProtected(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) {
if(mConfig->sys.adminPwd[0] != '\0') { // check if admin password is set
if(strncmp("*", clientIP, 1) != 0) { // no call from MqTT
const char* token = nullptr;
if(jsonIn.containsKey(F("token")))
token = jsonIn["token"];
if(!mApp->isProtected(clientIP, token, false))
return false;
jsonOut[F("error")] = F("ERR_PROTECTED");
return true;
}
}
return false;
}
private:
constexpr static uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT,
FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP};
constexpr static 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};
constexpr static uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP};
private:
IApp *mApp = nullptr;
HMSYSTEM *mSys = nullptr;
HmRadio<> *mRadioNrf = nullptr;
#if defined(ESP32)
CmtRadio<> *mRadioCmt;
CmtRadio<> *mRadioCmt = nullptr;
#endif
AsyncWebServer *mSrv;
settings_t *mConfig;
AsyncWebServer *mSrv = nullptr;
settings_t *mConfig = nullptr;
uint32_t mTimezoneOffset;
uint32_t mHeapFree, mHeapFreeBlk;
uint8_t mHeapFrag;
uint32_t mTimezoneOffset = 0;
uint32_t mHeapFree = 0, mHeapFreeBlk = 0;
uint8_t mHeapFrag = 0;
uint8_t *mTmpBuf = NULL;
uint32_t mTmpSize;
uint32_t mTmpSize = 0;
};
#endif /*__WEB_API_H__*/

28
src/web/html/api.js

@ -1,42 +1,42 @@
/* SVG ICONS - https://icons.getbootstrap.com */
iconWifi1 = [
var 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"
];
iconWifi2 = [
var iconWifi2 = [
"M13.229 8.271c.216-.216.194-.578-.063-.745A9.456 9.456 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.577 1.336c.205.132.48.108.652-.065zm-2.183 2.183c.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.408.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 .708 0l.707-.707z"
];
iconWifi3 = [
var iconWifi3 = [
"M15.384 6.115a.485.485 0 0 0-.047-.736A12.444 12.444 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.518.518 0 0 0 .668.05A11.448 11.448 0 0 1 8 4c2.507 0 4.827.802 6.716 2.164.205.148.49.13.668-.049z",
"M13.229 8.271a.482.482 0 0 0-.063-.745A9.455 9.455 0 0 0 8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065A8.46 8.46 0 0 1 8 7a8.46 8.46 0 0 1 4.576 1.336c.206.132.48.108.653-.065zm-2.183 2.183c.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.61-.091l.016-.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.707-.707z"
];
iconWarn = [
var iconWarn = [
"M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z",
"M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z"
];
iconInfo = [
var iconInfo = [
"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",
"m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"
];
iconSuccess = [
var iconSuccess = [
"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",
"M10.97 4.97a.235.235 0 0 0-.02.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-1.071-1.05z"
];
iconSuccessFull = [
var 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 = [
var 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 = [
var 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"
];
@ -84,10 +84,18 @@ function topnav() {
}
function parseNav(obj) {
for(i = 0; i < 11; i++) {
for(i = 0; i < 13; i++) {
if(i == 2)
continue;
var l = document.getElementById("nav"+i);
if(12 == i) {
if(obj.cst_lnk.length > 0) {
l.href = obj.cst_lnk
l.innerHTML = obj.cst_lnk_txt
l.classList.remove("hide");
}
continue;
}
if(window.location.pathname == "/" + l.href.substring(0, l.href.indexOf("?")).split('/').pop()) {
if((i != 8 )&& (i != 9))
l.classList.add("active");

6
src/web/html/colorBright.css

@ -3,9 +3,9 @@
--fg: #000;
--fg2: #fff;
--info: #0000dd;
--warn: #ff7700;
--success: #009900;
--info: #00d;
--warn: #f70;
--success: #090;
--input-bg: #eee;
--table-border: #ccc;

16
src/web/html/colorDark.css

@ -4,8 +4,8 @@
--fg2: #fff;
--info: #0072c8;
--warn: #ffaa00;
--success: #00bb00;
--warn: #fa0;
--success: #0b0;
--input-bg: #333;
--table-border: #333;
@ -20,14 +20,14 @@
--invalid-bg: #400;
--total-head-title: #555511;
--total-bg: #666622;
--iv-head-title: #115511;
--iv-head-bg: #226622;
--total-head-title: #551;
--total-bg: #662;
--iv-head-title: #151;
--iv-head-bg: #262;
--iv-dis-title: #333;
--iv-dis: #444;
--ch-head-title: #112255;
--ch-head-bg: #223366;
--ch-head-title: #125;
--ch-head-bg: #236;
--ts-head: #333;
--ts-bg: #555;
}

224
src/web/html/grid_info.json

@ -1,11 +1,24 @@
{
"type": [
{"0x0100": "CN_NBT32004_2018"},
{"0x0200": "US_Rule21_240V"},
{"0x0300": "DE_VDE4105_2018"},
{"0x0301": "DE_VDE4105_2011"},
{"0x0604": "Germany_VDE4105"},
{"0x0800": "IT_CEI0-21"},
{"0x0807": "Netherland_EN50438"},
{"0x0908": "France_VFR2014"},
{"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)"}
{"0x0d00": "FR_VFR2019"},
{"0x0d04": "NF_EN_50549-1:2019"},
{"0x1000": "ES_RD1699"},
{"0x1200": "EU_EN50438"},
{"0x2600": "BE_C10_26"},
{"0x2900": "NL_NEN-EN50549-1_2019"},
{"0x2a00": "PL_PN-EN 50549-1:2019"},
{"0x3700": "CH_NA EEA-NE7–CH2020"},
{"0xe100": "LN_50Hz"}
],
"grp_codes": [
{"0x00": "Voltage H/LVRT"},
@ -94,6 +107,52 @@
}
]
},
{
"0x0008": [
{
"name": "Nominal Voltage",
"div": 10,
"def": 230,
"unit": "V"
},
{
"name": "Low Voltage 1",
"div": 10,
"min": 170,
"max": 184,
"def": 184,
"unit": "V"
},
{
"name": "LV1 Maximum Trip Time",
"div": 10,
"def": 0.1,
"unit": "s"
},
{
"name": "High Voltage 1",
"div": 10,
"min": 264.5,
"max": 270,
"def": 264.5,
"unit": "V"
},
{
"name": "HV1 Maximum Trip Time",
"div": 10,
"def": 0.1,
"unit": "s"
},
{
"name": "10 mins Average High Voltage",
"div": 10,
"min": 253,
"max": 260,
"def": 253,
"unit": "V"
}
]
},
{
"0x000a": [
{
@ -286,6 +345,58 @@
}
]
},
{
"0x1002": [
{
"name": "Nominal Frequency",
"div": 100,
"def": 50,
"unit": "Hz"
},
{
"name": "Low Frequency 1",
"div": 100,
"min": 49.5,
"max": 49.9,
"def": 49.5,
"unit": "Hz"
},
{
"name": "LF1 Maximum Trip Time",
"div": 10,
"def": 700,
"unit": "s"
},
{
"name": "High Frequency 1",
"div": 100,
"max": 51,
"min": 50.1,
"def": 50.2,
"unit": "Hz"
},
{
"name": "HF1 Maximum Trip time",
"div": 10,
"def": 0.1,
"unit": "s"
},
{
"name": "Low Frequency 2",
"div": 100,
"max": 49,
"min": 47.5,
"def": 47.5,
"unit": "Hz"
},
{
"name": "LF2 Maximum Trip Time",
"div": 100,
"def": 0.1,
"unit": "s"
}
]
},
{
"0x1003": [
{
@ -415,6 +526,62 @@
}
]
},
{
"0x3007": [
{
"name": "Reconnect Time",
"div": 10,
"min": 10,
"max": 300,
"def": 65,
"unit": "s"
},
{
"name": "Reconnect High Voltage",
"div": 10,
"min": 253,
"max": 264.5,
"def": 264.5,
"unit": "V"
},
{
"name": "Reconnect Low Voltage",
"div": 10,
"min": 184,
"max": 210,
"def": 184,
"unit": "V"
},
{
"name": "Reconnect High Frequency",
"div": 100,
"max": 52,
"min": 50.5,
"def": 51.5,
"unit": "Hz"
},
{
"name": "Reconnect Low Frequency",
"div": 100,
"min": 47,
"max": 49.9,
"def": 47.5,
"unit": "Hz"
},
{
"name": "Short Interruption Reconnect Time",
"div": 10,
"def": 8,
"unit": "s"
},
{
"name": "Short Interruption Time",
"div": 10,
"def": 3,
"unit": "s"
}
]
},
{
"0x4000": [
{
@ -435,6 +602,42 @@
}
]
},
{
"0x5000": [
{
"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.5,
"unit": "Pn%/s"
}
]
},
{
"0x5001": [
{
@ -514,7 +717,7 @@
},
{
"name": "Recovery High Frequency",
"div": 10,
"div": 100,
"min": 50.1,
"max": 52,
"def": 50.2,
@ -563,6 +766,17 @@
}
]
},
{
"0x7000": [
{
"name": "APC Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 1
}
]
},
{
"0x7002": [
{
@ -753,7 +967,7 @@
"unit": "%Pn"
},
{
"name": "Power Factor ar Rated Power",
"name": "Power Factor of Rated Power",
"div": 100,
"min": 0.8,
"max": 1,

96
src/web/html/history.html

@ -0,0 +1,96 @@
<!doctype html>
<html>
<head>
<title>{#NAV_HISTORY}</title>
{#HTML_HEADER}
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="format-detection" content="telephone=no">
</head>
<body>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<h3>{#TOTAL_POWER}</h3>
<div>
<div class="chartDiv" id="pwrChart"> </div>
<p>
{#MAX_DAY}: <span id="pwrMaxDay"></span> W. {#LAST_VALUE}: <span id="pwrLast"></span> W.<br />
{#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS}
</p>
</div>
</div>
</div>
{#HTML_FOOTER}
<script type="text/javascript">
const svgns = "http://www.w3.org/2000/svg";
var pwrExeOnce = true;
var ydExeOnce = true;
// make a simple rectangle
var mRefresh = 60;
var mLastValue = 0;
const mChartHeight = 250;
function parseHistory(obj, namePrefix, execOnce) {
mRefresh = obj.refresh
var data = Object.assign({}, obj.value)
numDataPts = Object.keys(data).length
if (true == execOnce) {
let s = document.createElementNS(svgns, "svg");
s.setAttribute("class", "chart");
s.setAttribute("width", (numDataPts + 2) * 2);
s.setAttribute("height", mChartHeight);
s.setAttribute("role", "img");
let g = document.createElementNS(svgns, "g");
s.appendChild(g);
for (var i = 0; i < numDataPts; i++) {
val = data[i];
let rect = document.createElementNS(svgns, "rect");
rect.setAttribute("id", namePrefix+"Rect" + i);
rect.setAttribute("x", i * 2);
rect.setAttribute("width", 2);
g.appendChild(rect);
}
document.getElementById(namePrefix+"Chart").appendChild(s);
}
// normalize data to chart
let divider = obj.max / mChartHeight;
if (divider == 0)
divider = 1;
for (var i = 0; i < numDataPts; i++) {
val = data[i];
if (val > 0)
mLastValue = val
val = val / divider
rect = document.getElementById(namePrefix+"Rect" + i);
rect.setAttribute("height", val);
rect.setAttribute("y", mChartHeight - val);
}
document.getElementById(namePrefix + "Max").innerHTML = obj.max;
if (mRefresh < 5)
mRefresh = 5;
document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh;
}
function parsePowerHistory(obj){
if (null != obj) {
parseNav(obj.generic);
parseHistory(obj,"pwr", pwrExeOnce)
document.getElementById("pwrLast").innerHTML = mLastValue
document.getElementById("pwrMaxDay").innerHTML = obj.maxDay
}
if (pwrExeOnce) {
pwrExeOnce = false;
window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000);
}
}
getAjax("/api/powerHistory", parsePowerHistory);
</script>
</body>
</html>

2
src/web/html/includes/footer.html

@ -1,6 +1,6 @@
<div id="footer">
<div class="left">
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a>
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2024</a>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>

3
src/web/html/includes/header.html

@ -1,6 +1,7 @@
<link rel="stylesheet" type="text/css" href="style.css?v={#VERSION}"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<meta charset="utf-8">
<script type="text/javascript" src="api.js?v={#VERSION}"></script>
<link rel="stylesheet" type="text/css" href="colors.css?v={#VERSION}"/>
<meta name="robots" content="noindex, nofollow" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">

12
src/web/html/includes/nav.html

@ -6,16 +6,18 @@
<span></span>
</a>
<div id="topnav" class="mobile">
<a id="nav3" class="hide" href="/live?v={#VERSION}">Live</a>
<a id="nav4" class="hide" href="/serial?v={#VERSION}">Webserial</a>
<a id="nav5" class="hide" href="/setup?v={#VERSION}">Settings</a>
<a id="nav3" class="hide" href="/live?v={#VERSION}">{#NAV_LIVE}</a>
<a id="nav11" class="acitve" href="/history?v={#VERSION}">{#NAV_HISTORY}</a>
<a id="nav4" class="hide" href="/serial?v={#VERSION}">{#NAV_WEBSERIAL}</a>
<a id="nav5" class="hide" href="/setup?v={#VERSION}">{#NAV_SETTINGS}</a>
<span class="seperator"></span>
<a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a>
<a id="nav7" class="hide" href="/system?v={#VERSION}">System</a>
<span class="seperator"></span>
<a id="nav8" href="/api" target="_blank">REST API</a>
<a id="nav9" href="https://ahoydtu.de" target="_blank">Documentation</a>
<a id="nav10" href="/about?v={#VERSION}">About</a>
<a id="nav9" href="https://ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</a>
<a id="nav10" href="/about?v={#VERSION}">{#NAV_ABOUT}</a>
<a id="nav12" href="#" class="hide" target="_blank">Custom Link</a>
<span class="seperator"></span>
<a id="nav0" class="hide" href="/login">Login</a>
<a id="nav1" class="hide" href="/logout">Logout</a>

102
src/web/html/index.html

@ -20,18 +20,15 @@
</p>
<div id="note">
<h3>Support this project:</h3>
<h3>{#SUPPORT}:</h3>
<ul>
<li><a href="https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md" target="_blank">Changelog</a></li>
<li>Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a></li>
<li>Report <a href="https://github.com/lumapu/ahoy/issues" target="_blank">issues</a></li>
<li>Contribute to <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">documentation</a></li>
<li><a href="https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip" target="_blank">Download</a> & Test development firmware, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">Development Changelog</a></li>
<li>make a <a href="https://paypal.me/lupusch" target="_blank">donation</a></li>
<li><a href="https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md" target="_blank">{#CHANGELOG}</a></li>
<li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB">Discord</a></li>
<li>{#REPORT} <a href="https://github.com/lumapu/ahoy/issues" target="_blank">{#ISSUES}</a></li>
<li>{#CONTRIBUTE} <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">{#DOCUMENTATION}</a></li>
<li><a href="https://fw.ahoydtu.de/fw/dev/" target="_blank">Download</a> & Test {#DEV_FIRMWARE}, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">{#DEV_CHANGELOG}</a></li>
<li>{#DON_MAKE} <a href="https://paypal.me/lupusch" target="_blank">{#DONATION}</a></li>
</ul>
<p class="lic">
This project was started from <a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion. (Mikrocontroller.net)</a>
</p>
</div>
</div>
</div>
@ -44,33 +41,30 @@
var release = null;
function apiCb(obj) {
var e = document.getElementById("apiResult");
if(obj["success"]) {
e.innerHTML = " command executed";
getAjax("/api/index", parse);
}
else
e.innerHTML = " Error: " + obj["error"];
var e = document.getElementById("apiResult")
if(obj.success) {
e.innerHTML = " {#COMMAND_EXE}"
getAjax("/api/index", parse)
} else
e.innerHTML = " {#ERROR}: " + obj.error
}
function setTime() {
var date = new Date();
var obj = new Object();
obj.cmd = "set_time";
obj.val = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj));
var date = new Date()
var obj = {cmd: "set_time", token: "*", val: parseInt(date.getTime() / 1000)}
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj))
}
function parseGeneric(obj) {
if(exeOnce)
parseESP(obj);
parseRssi(obj);
parseESP(obj)
parseRssi(obj)
}
function parseSys(obj) {
ts = obj["ts_now"];
var date = new Date(obj["ts_now"] * 1000);
var up = obj["generic"]["ts_uptime"];
ts = obj.ts_now;
var date = new Date(obj.ts_now * 1000);
var up = obj.generic["ts_uptime"];
var days = parseInt(up / 86400) % 365;
var hrs = parseInt(up / 3600) % 24;
var min = parseInt(up / 60) % 60;
@ -83,8 +77,8 @@
+ ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2);
var dSpan = document.getElementById("date");
if(0 != obj["ts_now"]) {
if(obj["ts_now"] < 1680000000)
if(0 != obj.ts_now) {
if(obj.ts_now < 1680000000)
setTime();
else
dSpan.innerHTML = toIsoDateStr(date);
@ -92,24 +86,24 @@
else {
dSpan.innerHTML = "";
var e = inp("set", "sync from browser", 0, ["btn"], "set", "button");
dSpan.appendChild(span("NTP timeserver unreachable. "));
dSpan.appendChild(span("{#NTP_UNREACH}. "));
dSpan.appendChild(e);
dSpan.appendChild(span("", ["span"], "apiResult"));
e.addEventListener("click", setTime);
}
if(obj["disNightComm"]) {
if(((obj["ts_sunrise"] - obj["ts_offset"]) < obj["ts_now"])
&& ((obj["ts_sunset"] + obj["ts_offset"]) > obj["ts_now"])) {
commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE'));
if(obj.disNightComm) {
if(((obj.ts_sunrise + obj.ts_offsSr) < obj.ts_now)
&& ((obj.ts_sunset + obj.ts_offsSs) > obj.ts_now)) {
commInfo = "{#POLLING_STOP} " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE'));
}
else {
commInfo = "Night time, inverter polling disabled, ";
if(obj["ts_now"] > (obj["ts_sunrise"] - obj["ts_offset"])) {
commInfo += "paused at " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE'));
commInfo = "{#NIGHT_TIME}, ";
if(obj.ts_now > (obj.ts_sunrise + obj.ts_offsSr)) {
commInfo += "{#PAUSED_AT} " + (new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE'));
}
else {
commInfo += "will start polling at " + (new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE'));
commInfo += "{#START_AT} " + (new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE'));
}
}
}
@ -124,33 +118,33 @@
if(false == i["enabled"]) {
icon = iconWarn;
cl = "icon-warn";
avail = "disabled";
avail = "{#DISABLED}";
} else if((false == i["is_avail"]) || (0 == ts)) {
icon = iconInfo;
cl = "icon-info";
avail = "not yet available";
avail = "{#NOT_YET_AVAIL}";
} else if(0 == i["ts_last_success"]) {
avail = "available but no data was received until now";
avail = "{#AVAIL_NO_DATA}";
} else {
avail = "available and is ";
avail = "{#AVAIL} ";
if(false == i["is_producing"])
avail += "not producing";
avail += "{#NOT_PRODUCING}";
else {
icon = iconSuccessFull;
avail += "producing " + i.cur_pwr + "W";
avail += "{#PRODUCING} " + i.cur_pwr + "W";
}
}
p.append(
svg(icon, 30, 30, "icon " + cl),
span("Inverter #" + i["id"] + ": " + i["name"] + " is " + avail),
span("{#INVERTER} #" + i["id"] + ": " + i["name"] + " {#IS} " + avail),
br()
);
if(false == i["is_avail"]) {
if(i["ts_last_success"] > 0) {
var date = new Date(i["ts_last_success"] * 1000);
p.append(span("-> last successful transmission: " + toIsoDateStr(date)), br());
p.append(span("-> {#LAST_SUCCESS}: " + toIsoDateStr(date)), br());
}
}
}
@ -168,11 +162,11 @@
if(null != release) {
if(getVerInt("{#VERSION}") < getVerInt(release))
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Update available, current released version: " + release), br());
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#UPDATE_AVAIL}: " + release), br());
else if(getVerInt("{#VERSION}") > getVerInt(release))
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using development version {#VERSION}. In case of issues you may want to try the current stable release: " + release), br());
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#USING_DEV_VERSION} {#VERSION}. {#DEV_ISSUE_RELEASE_VERSION}: " + release), br());
else
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using the current stable release: " + release), br());
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#RELEASE_INSTALLED}: " + release), br());
}
document.getElementById("warn_info").replaceChildren(p);
@ -190,11 +184,11 @@
function parse(obj) {
if(null != obj) {
if(exeOnce)
parseNav(obj["generic"]);
parseGeneric(obj["generic"]);
parseNav(obj.generic);
parseGeneric(obj.generic);
parseSys(obj);
parseIv(obj["inverter"], obj.ts_now);
parseWarn(obj["warnings"]);
parseIv(obj.inverter, obj.ts_now);
parseWarn(obj.warnings);
if(exeOnce) {
window.setInterval("tick()", 1000);
exeOnce = false;
@ -210,7 +204,7 @@
}
function parseRelease(obj) {
release = obj["name"].substring(6);
release = obj.name.substring(6);
getAjax("/api/index", parse);
}

14
src/web/html/save.html

@ -1,14 +1,14 @@
<!doctype html>
<html>
<head>
<title>Save</title>
<title>{#NAV_SAVE}</title>
{#HTML_HEADER}
</head>
<body>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<div id="html" class="mt-3 mb-3">Saving settings...</div>
<div id="html" class="mt-3 mb-3">{#SAVE_SETTINGS}</div>
</div>
</div>
{#HTML_FOOTER}
@ -31,15 +31,15 @@
var meta = document.createElement('meta');
meta.httpEquiv = "refresh"
if(!obj.reboot) {
html = "Settings successfully saved. Automatic page reload in 3 seconds.";
meta.content = 3;
html = "{#SUCCESS_SAVED_RELOAD}";
meta.content = "2; URL=/setup"
} else {
html = "Settings successfully saved. Rebooting. Automatic redirect in " + obj.reload + " seconds.";
meta.content = obj.reload + "; URL=/";
html = "{#SUCCESS_SAVED_REBOOT} " + obj.reload + " {#SECONDS}.";
meta.content = obj.reload + "; URL=/"
}
document.getElementsByTagName('head')[0].appendChild(meta);
} else {
html = "Failed saving settings.";
html = "{#FAILED_SAVE}.";
}
}
document.getElementById("html").innerHTML = html;

12
src/web/html/serial.html

@ -1,7 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<title>Serial Console</title>
<title>{#NAV_WEBSERIAL}</title>
{#HTML_HEADER}
</head>
<body>
@ -12,12 +12,12 @@
<textarea id="serial" class="mt-3" cols="80" rows="40" readonly></textarea>
</div>
<div class="row my-3">
<div class="col-3">console active: <span class="dot" id="active"></span></div>
<div class="col-3 col-sm-4 my-3">Uptime: <span id="uptime"></span></div>
<div class="col-3">{#CONSOLE_ACTIVE}: <span class="dot" id="active"></span></div>
<div class="col-3 col-sm-4 my-3">{#UPTIME}: <span id="uptime"></span></div>
<div class="col-6 col-sm-4 a-r">
<input type="button" value="clear" class="btn" id="clear"/>
<input type="button" value="autoscroll" class="btn" id="scroll"/>
<input type="button" value="copy" class="btn" id="copy"/>
<input type="button" value="{#BTN_CLEAR}" class="btn" id="clear"/>
<input type="button" value="{#BTN_AUTOSCROLL}" class="btn" id="scroll"/>
<input type="button" value="{#BTN_COPY}" class="btn" id="copy"/>
</div>
</div>
</div>

765
src/web/html/setup.html

File diff suppressed because it is too large

87
src/web/html/style.css

@ -16,11 +16,11 @@ span, li, h3, label, fieldset {
color: var(--fg);
}
fieldset, input[type=submit], .btn {
fieldset, input[type="submit"], .btn {
border-radius: 4px;
}
input[type=file] {
input[type="file"] {
width: 100%;
}
@ -33,6 +33,23 @@ textarea {
color: var(--fg2);
}
svg rect {fill: #00A;}
svg.chart {
background: #f2f2f2;
border: 2px solid gray;
padding: 1px;
}
div.chartDivContainer {
padding: 1px;
margin: 1px;
}
div.chartdivContainer span {
color: var(--fg2);
}
.topnav {
background-color: var(--nav-bg);
position: fixed;
@ -79,7 +96,7 @@ svg.icon {
vertical-align: middle;
display: inline-block;
margin-top:-4x;
padding: 5px 7px 5px 0px;
padding: 5px 7px 5px 0;
}
.icon-info {
@ -125,7 +142,7 @@ svg.icon {
span.seperator {
width: 100%;
height: 1px;
margin: 5px 0px 5px;
margin: 5px 0 5px;
background-color: #494949;
display: block;
}
@ -375,7 +392,7 @@ th {
#footer .left {
color: #bbb;
margin: 23px 0px 0px 25px;
margin: 23px 0 0 25px;
}
#footer ul {
@ -509,7 +526,7 @@ input, select {
font-size: 13pt;
}
input[type=text], input[type=password], select, input[type=number] {
input[type="text"], input[type="password"], select, input[type="number"] {
width: 100%;
box-sizing: border-box;
border: 1px solid #ccc;
@ -535,7 +552,7 @@ input.btnDel {
input.btn {
background-color: var(--primary);
color: #fff;
border: 0px;
border: 0;
padding: 7px 20px 7px 20px;
margin-bottom: 10px;
text-transform: uppercase;
@ -556,7 +573,7 @@ label {
display: inline-block;
font-size: 12pt;
padding-right: 10px;
margin: 10px 0px 0px 15px;
margin: 10px 0 0 15px;
vertical-align: top;
}
@ -585,7 +602,7 @@ div.ModPwr, div.ModName, div.YieldCor {
div.hr {
height: 1px;
border-top: 1px solid #ccc;
margin: 10px 0px 10px;
margin: 10px 0 10px;
}
#note {
@ -650,39 +667,24 @@ div.hr {
}
.css-tooltip{
.tooltip:hover {
position: relative;
}
.css-tooltip:hover:after{
content:attr(data-tooltip);
background:#000;
padding:5px;
border-radius:3px;
.tooltip:hover:after {
content: attr(data);
background: var(--nav-active);
padding: 5px;
border-radius: 3px;
display: inline-block;
position: absolute;
transform: translate(-50%,-100%);
margin:0 auto;
color:#FFF;
min-width:100px;
min-width:150px;
top:-5px;
color: var(--fg2);
min-width: 100px;
top: -5px;
left: 50%;
text-align:center;
}
.css-tooltip:hover:before {
top:-5px;
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(0, 0, 0, 0);
border-top-color: #000;
border-width: 5px;
margin-left: -5px;
transform: translate(0,0px);
font-size: 1rem;
}
#modal {
@ -829,3 +831,20 @@ ul {
background-color: var(--input-bg);
color: var(--fg);
}
.d-flex {
display: flex !important;
}
.jc {
justify-content: center !important;
}
.aic {
align-items: center !important;
}
.container {
height: 100%;
overflow: auto;
}

42
src/web/html/system.html

@ -24,7 +24,7 @@
const data = ["sdk", "cpu_freq", "chip_revision",
"chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime",
"flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag",
"max_free_blk", "version", "core_version", "reboot_reason"];
"max_free_blk", "version", "modules", "env", "core_version", "reboot_reason"];
lines = [];
for (const [key, value] of Object.entries(obj)) {
@ -44,20 +44,29 @@
return ml("div", {class: "head p-2 mt-3"}, ml("div", {class: "row"}, ml("div", {class: "col a-c"}, text)))
}
function irqBadge(state) {
switch(state) {
case 0: return badge(false, "{#UNKNOWN}", "warning"); break;
case 1: return badge(true, "{#TRUE}"); break;
default: return badge(false, "{#FALSE}"); break;
}
}
function parseRadio(obj) {
const dr = ["1 M", "2 M", "250 k"]
if(obj.radioNrf.en) {
lines = [
tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "not ") + "connected")),
tr("NRF24 Data Rate", dr[obj.radioNrf.dataRate] + "bps"),
tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")),
tr("{#IRQ_WORKING}", irqBadge(obj.radioNrf.irqOk)),
tr("{#NRF24_DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"),
tr("DTU Radio ID", obj.radioNrf.sn)
];
} else
lines = [tr("NRF24L01", badge(false, "not enabled"))];
lines = [tr("NRF24L01", badge(false, "{#NOT_ENABLED}"))];
document.getElementById("info").append(
headline("Radio NRF"),
headline("{#NRF24_RADIO}"),
ml("table", {class: "table"},
ml("tbody", {}, lines)
)
@ -66,14 +75,15 @@
/*IF_ESP32*/
if(obj.radioCmt.en) {
cmt = [
tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "not ") + "connected")),
tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")),
tr("{#IRQ_WORKING}", irqBadge(obj.radioCmt.irqOk)),
tr("DTU Radio ID", obj.radioCmt.sn)
];
} else
cmt = [tr("CMT2300A", badge(false, "not enabled"))];
cmt = [tr("CMT2300A", badge(false, "{#NOT_ENABLED}"))];
document.getElementById("info").append(
headline("Radio CMT"),
headline("{#CMT_RADIO}"),
ml("table", {class: "table"},
ml("tbody", {}, cmt)
)
@ -84,13 +94,13 @@
function parseMqtt(obj) {
if(obj.enabled) {
lines = [
tr("connected", badge(obj.connected, ((obj.connected) ? "true" : "false"))),
tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "{#TRUE}" : "{#FALSE}"))),
tr("#TX", obj.tx_cnt),
tr("#RX", obj.rx_cnt)
];
} else
lines = tr("enabled", badge(false, "false"));
lines = tr("{#ENABLED}", badge(false, "{#FALSE}"));
document.getElementById("info").append(
headline("MqTT"),
@ -103,14 +113,14 @@
function parseIndex(obj) {
if(obj.ts_sunrise > 0) {
document.getElementById("info").append(
headline("Sun"),
headline("{#SUN}"),
ml("table", {class: "table"},
ml("tbody", {}, [
tr("Sunrise", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')),
tr("Sunset", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')),
tr("Communication start", new Date((obj.ts_sunrise - obj.ts_offset) * 1000).toLocaleString('de-DE')),
tr("Communication stop", new Date((obj.ts_sunset + obj.ts_offset) * 1000).toLocaleString('de-DE')),
tr("Night behaviour", badge(obj.disNightComm, ((obj.disNightComm) ? "not" : "") + " communicating", "warning"))
tr("{#SUNRISE}", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')),
tr("{#SUNSET}", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')),
tr("{#COMMUNICATION_START}", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')),
tr("{#COMMUNICATION_STOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')),
tr("{#NIGHT_BEHAVE}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning"))
])
)
);

37
src/web/html/update.html

@ -9,29 +9,48 @@
<div id="wrapper">
<div id="content">
<fieldset>
<legend class="des">Select firmware file (*.bin)</legend>
<legend class="des">{#SELECT_FILE} (*.bin)</legend>
<p>{#INSTALLED_VERSION}:<br/><span id="version" style="background-color: var(--input-bg); padding: 7px; display: block; margin: 3px;"></span></p>
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update">
<input type="button" class="btn my-4" value="Update" onclick="hide()">
<input type="button" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()">
</form>
</fieldset>
<div class="row mt-4">
<a href="https://fw.ahoydtu.de" target="_blank">Download latest Release and Development versions<a/>
<a href="https://fw.ahoydtu.de" target="_blank">{#DOWNLOADS}<a/>
</div>
</div>
</div>
{#HTML_FOOTER}
<script type="text/javascript">
var env;
function parseGeneric(obj) {
parseNav(obj);
parseESP(obj);
parseRssi(obj);
parseNav(obj)
parseESP(obj)
parseRssi(obj)
env = obj.env
document.getElementById("version").innerHTML = "{#VERSION_FULL}_" + obj.env + ".bin"
}
function hide() {
document.getElementById("form").submit();
var e = document.getElementById("content");
e.replaceChildren(span("update started"));
var bin = document.getElementsByName("update")[0].value.slice(-env.length-4, -4)
if (bin !== env) {
var html = ml("div", {class: "row"}, [
ml("div", {class: "row my-3"}, "{#WARN_DIFF_ENV}"),
ml("div", {class: "row"}, [
ml("div", {class: "col-6"}, ml("input", {type: "button", class: "btn", value: "{#CANCEL}", onclick: function() { modalClose(); }}, null)),
ml("div", {class: "col-6"}, ml("input", {type: "button", class: "btn", value: "{#CONTIUE}", onclick: function() { start(); modalClose(); }}, null))
])
])
modal("{#UPDATE_MODAL}", html)
} else
start()
}
function start() {
document.getElementById("form").submit()
var e = document.getElementById("content")
e.replaceChildren(span("{#UPDATE_STARTED}"))
}
getAjax("/api/generic", parseGeneric);

188
src/web/html/visualization.html

@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
<title>Live</title>
<title>{#NAV_LIVE}</title>
{#HTML_HEADER}
<meta name="apple-mobile-web-app-capable" content="yes">
</head>
@ -10,7 +10,7 @@
<div id="wrapper">
<div id="content">
<div id="live"></div>
<p>Every <span id="refresh"></span> seconds the values are updated</p>
<p>{#EVERY} <span id="refresh"></span> {#UPDATE_SECS}</p>
</div>
</div>
{#HTML_FOOTER}
@ -22,6 +22,15 @@
var total = Array(6).fill(0);
var tPwrAck;
function getErrStr(code) {
if("ERR_AUTH") return "{#ERR_AUTH}"
if("ERR_INDEX") return "{#ERR_INDEX}"
if("ERR_UNKNOWN_CMD") return "{#ERR_UNKNOWN_CMD}"
if("ERR_LIMIT_NOT_ACCEPT") return "{#ERR_LIMIT_NOT_ACCEPT}"
if("ERR_UNKNOWN_CMD") return "{#ERR_AUTH}"
return "n/a"
}
function parseGeneric(obj) {
if(true == exeOnce){
parseNav(obj);
@ -45,13 +54,14 @@
]);
}
function numMid(val, unit, des) {
function numMid(val, unit, des, opt={class: "fs-6"}) {
return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [
ml("div", {class: "row"},
ml("div", {class: "col"}, [
ml("span", {class: "fs-6"}, String(Math.round(val * 100) / 100)),
ml("span", opt, String(Math.round(val * 100) / 100)),
ml("span", {class: "fs-8 mx-1"}, unit)
])),
])
),
ml("div", {class: "row"},
ml("div", {class: "col"},
ml("span", {class: "fs-9"}, des)
@ -69,20 +79,20 @@
ml("div", {class: "col"}, [
ml("div", {class: "p-2 total-h"},
ml("div", {class: "row"},
ml("div", {class: "col mx-2 mx-md-1"}, "TOTAL")
ml("div", {class: "col mx-2 mx-md-1"}, "{#TOTAL}")
),
),
ml("div", {class: "p-2 total-bg"}, [
ml("div", {class: "row"}, [
numBig(total[0], "W", "AC Power"),
numBig(total[1], "Wh", "Yield Day"),
numBig(total[2], "kWh", "Yield Total")
numBig(total[0], "W", "{#AC_POWER}"),
numBig(total[1], "Wh", "{#YIELD_DAY}"),
numBig(total[2], "kWh", "{#YIELD_TOTAL}")
]),
ml("div", {class: "hr"}),
ml("div", {class: "row"}, [
numMid(total[3], "W", "Max Power"),
numMid(total[4], "W", "DC Power"),
numMid(total[5], "var", "Reactive Power")
numMid(total[3], "W", "{#MAX_POWER}"),
numMid(total[4], "W", "{#DC_POWER}"),
numMid(total[5], "var", "{#REACTIVE_POWER}")
])
])
])
@ -91,10 +101,10 @@
function ivHead(obj) {
if(0 != obj.status) { // only add totals if inverter is online
total[0] += obj.ch[0][2]; // P_AC
total[3] += obj.ch[0][11]; // MAX P_AC
total[4] += obj.ch[0][8]; // P_DC
total[5] += obj.ch[0][10]; // Q_AC
}
total[3] += obj.ch[0][11]; // MAX P_AC
total[1] += obj.ch[0][7]; // YieldDay
total[2] += obj.ch[0][6]; // YieldTotal
@ -106,8 +116,10 @@
if(65535 != obj.power_limit_read) {
pwrLimit = obj.power_limit_read + "&nbsp;%";
if(0 != obj.max_pwr)
pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "W";
pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "&nbsp;W";
}
var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000));
return ml("div", {class: "row mt-2"},
ml("div", {class: "col"}, [
ml("div", {class: "p-2 " + clh},
@ -116,31 +128,31 @@
getAjax("/api/inverter/version/" + obj.id, parseIvVersion);
}}, obj.name)),
ml("div", {class: "col a-c", onclick: function() {limitModal(obj)}}, [
ml("span", {class: "d-none d-sm-block pointer"}, "Active Power Control: " + pwrLimit),
ml("span", {class: "d-block d-sm-none pointer"}, "APC: " + pwrLimit)
ml("span", {class: "d-none d-sm-block pointer"}, "{#ACTIVE_POWER_CONTROL}: " + pwrLimit),
ml("span", {class: "d-block d-sm-none pointer"}, "{#APC}: " + pwrLimit)
]),
ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() {
getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm);
}}, ("Alarms: " + obj.alarm_cnt))),
}}, ("{#ALARMS}: " + obj.alarm_cnt))),
ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5].toFixed(1)) + t.innerText)
])
),
ml("div", {class: "p-2 " + clbg}, [
ml("div", {class: "row"},[
numBig(obj.ch[0][2], "W", "AC Power"),
numBig(obj.ch[0][7], "Wh", "Yield Day"),
numBig(obj.ch[0][6], "kWh", "Yield Total")
numBig(obj.ch[0][2], "W", "{#AC_POWER}"),
numBig(obj.ch[0][7], "Wh", "{#YIELD_DAY}"),
numBig(obj.ch[0][6], "kWh", "{#YIELD_TOTAL}")
]),
ml("div", {class: "hr"}),
ml("div", {class: "row mt-2"},[
numMid(obj.ch[0][11], "W", "Max AC Power"),
numMid(obj.ch[0][8], "W", "DC Power"),
numMid(obj.ch[0][0], "V", "AC Voltage"),
numMid(obj.ch[0][1], "A", "AC Current"),
numMid(obj.ch[0][3], "Hz", "Frequency"),
numMid(obj.ch[0][9], "%", "Efficiency"),
numMid(obj.ch[0][10], "var", "Reactive Power"),
numMid(obj.ch[0][4], "", "Power Factor")
numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "fs-6 tooltip", data: maxAcPwr}),
numMid(obj.ch[0][8], "W", "{#DC_POWER}"),
numMid(obj.ch[0][0], "V", "{#AC_VOLTAGE}"),
numMid(obj.ch[0][1], "A", "{#AC_CURRENT}"),
numMid(obj.ch[0][3], "Hz", "{#FREQUENCY}"),
numMid(obj.ch[0][9], "%", "{#EFFICIENCY}"),
numMid(obj.ch[0][10], "var", "{#REACTIVE_POWER}"),
numMid(obj.ch[0][4], "", "{#POWER_FACTOR}")
])
])
])
@ -169,32 +181,32 @@
ml("div", {class: "p-2 a-c " + clh}, name),
ml("div", {class: "p-2 " + clbg}, [
ml("div", {class: "row"}, [
numCh(vals[2], units[2], "DC Power"),
numCh(vals[6], units[2], "Max Power"),
numCh(vals[5], units[5], "Irradiation"),
numCh(vals[3], units[3], "Yield Day"),
numCh(vals[4], units[4], "Yield Total"),
numCh(vals[0], units[0], "DC Voltage"),
numCh(vals[1], units[1], "DC Current")
numCh(vals[2], units[2], "{#DC_POWER}"),
numCh(vals[6], units[2], "{#MAX_POWER}"),
numCh(vals[5], units[5], "{#IRRADIATION}"),
numCh(vals[3], units[3], "{#YIELD_DAY}"),
numCh(vals[4], units[4], "{#YIELD_TOTAL}"),
numCh(vals[0], units[0], "{#DC_VOLTAGE}"),
numCh(vals[1], units[1], "{#DC_CURRENT}")
])
])
]);
}
function tsInfo(obj) {
var ageInfo = "Last received data requested at: ";
var ageInfo = "{#LAST_RECEIVED}: ";
if(obj.ts_last_success > 0) {
var date = new Date(obj.ts_last_success * 1000);
ageInfo += toIsoDateStr(date);
}
else
ageInfo += "nothing received";
ageInfo += "{#NOTHING_RECEIVED}";
if(obj.rssi > -127) {
if(obj.generation < 2)
ageInfo += " (RSSI: " + ((obj.rssi == -64) ? ">=" : "<") + " -64dBm)";
ageInfo += " (RSSI: " + ((obj.rssi == -64) ? "&gt;=" : "&lt;") + " -64&nbsp;dBm)";
else
ageInfo += " (RSSI: " + obj.rssi + "dBm)";
ageInfo += " (RSSI: " + obj.rssi + "&nbsp;dBm)";
}
return ml("div", {class: "mb-5"}, [
@ -249,10 +261,10 @@
var offs = new Date().getTimezoneOffset() * -60;
html.push(
ml("div", {class: "row"}, [
ml("div", {class: "col"}, ml("strong", {}, "Event")),
ml("div", {class: "col"}, ml("strong", {}, "{#EVENT}")),
ml("div", {class: "col"}, ml("strong", {}, "ID")),
ml("div", {class: "col"}, ml("strong", {}, "Start")),
ml("div", {class: "col"}, ml("strong", {}, "End"))
ml("div", {class: "col"}, ml("strong", {}, "{#END}"))
])
);
@ -268,7 +280,7 @@
);
}
}
modal("Alarms of inverter " + obj.iv_name, ml("div", {}, html));
modal("{#ALARMS_MODAL}: " + obj.iv_name, ml("div", {}, html));
}
function parseIvVersion(obj) {
@ -280,7 +292,7 @@
case 3: model = "HMT-"; break;
default: model = "???-"; break;
}
model += String(obj.max_pwr) + " (Serial: " + obj.serial + ")";
model += String(obj.max_pwr) + " ({#SERIAL}: " + obj.serial + ")";
var html = ml("table", {class: "table"}, [
@ -288,15 +300,15 @@
tr("Model", model),
tr("Firmware Version / Build", String(obj.fw_ver) + " (build: " + String(obj.fw_date) + " " + String(obj.fw_time) + ")"),
tr("Hardware Version / Build", (obj.hw_ver/100).toFixed(2) + " (build: " + String(obj.prod_cw) + "/" + String(obj.prod_year) + ")"),
tr("Hardware Number", obj.part_num.toString(16)),
tr("{#HW_NUMBER}", obj.part_num.toString(16)),
tr("Bootloader Version", (obj.boot_ver/100).toFixed(2)),
tr("Grid Profile", ml("input", {type: "button", value: "show", class: "btn", onclick: function() {
tr("Grid Profile", ml("input", {type: "button", value: "{#BTN_SHOW}", class: "btn", onclick: function() {
modalClose();
getAjax("/api/inverter/grid/" + obj.id, showGridProfile);
}}, null))
])
])
modal("Info for inverter " + obj.name, ml("div", {}, html))
modal("{#INV_INFO}: " + obj.name, ml("div", {}, html))
}
function getGridValue(g) {
@ -329,9 +341,9 @@
))
content.push(ml("div", {class: "row my-2"}, [
ml("div", {class: "col-4"}, ml("b", {}, "Name")),
ml("div", {class: "col-3"}, ml("b", {}, "Value")),
ml("div", {class: "col-3"}, ml("b", {}, "Range")),
ml("div", {class: "col-2"}, ml("b", {}, "Default"))
ml("div", {class: "col-3"}, ml("b", {}, "{#VALUE}")),
ml("div", {class: "col-3"}, ml("b", {}, "{#RANGE}")),
ml("div", {class: "col-2"}, ml("b", {}, "{#DEFAULT}"))
]))
for(e of g.info.group) {
if(Array.isArray(e[id])) {
@ -356,25 +368,27 @@
var glob = {offs:0, grid:obj.grid, info: data}
var content = [];
var g = getGridType(glob.info.type, getGridIdentifier(glob))
var v = getGridValue(glob);
if(null === g) {
if(0 == obj.grid.length) {
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "Profile was not read until now, maybe turned off?"))))
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#PROFILE_NOT_READ}"))))
} else {
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "Unknown Profile"))))
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "Please open a new issue at https://github.com/lumapu/ahoy and copy the raw data into it."))))
content.push(ml("div", {class: "row"}, ml("div", {class: "col my-2"}, ml("pre", {}, obj.grid))))
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("h5", {}, "{#UNKNOWN_PROFILE}"))))
content.push(ml("div", {class: "row"}, ml("div", {class: "col"}, ml("p", {}, "{#OPEN_ISSUE}."))))
}
} else {
content.push(ml("div", {class: "row"},
ml("div", {class: "col my-3"}, ml("h5", {}, g + " (Version " + getGridValue(glob).toString(16) + ")"))
ml("div", {class: "col my-3"}, ml("h5", {}, g + " (Version " + (Math.round(v / 0x1000)) + "." + (Math.round((v & 0x0ff0) / 0x10)) + "." + (v & 0x0F) + ")"))
))
while((glob.offs*3) < glob.grid.length) {
content.push(parseGridGroup(glob))
}
}
if(0 != obj.grid.length)
content.push(ml("div", {class: "row"}, ml("div", {class: "col my-2"}, ml("pre", {}, obj.grid))))
modal("Grid Profile for inverter " + obj.name, ml("div", {}, ml("div", {class: "col mb-2"}, [...content])))
modal("{#PROFILE_MODAL}: " + obj.name, ml("div", {}, ml("div", {class: "col mb-2"}, [...content])))
})
}
@ -382,54 +396,56 @@
function parseIvRadioStats(obj) {
var html = ml("table", {class: "table"}, [
ml("tbody", {}, [
tr2(["TX count", obj.tx_cnt, ""]),
tr2(["RX success", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + "%"]),
tr2(["RX fail", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + "%"]),
tr2(["RX no answer", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + "%"]),
tr2(["RX fragments", obj.frame_cnt, ""]),
tr2(["TX retransmits", obj.retransmits, ""])
tr2(["{#TX_COUNT}", obj.tx_cnt, ""]),
tr2(["{#RX_SUCCESS}", obj.rx_success, String(Math.round(obj.rx_success / obj.tx_cnt * 10000) / 100) + "&nbsp;%"]),
tr2(["{#RX_FAIL}", obj.rx_fail, String(Math.round(obj.rx_fail / obj.tx_cnt * 10000) / 100) + "&nbsp;%"]),
tr2(["{#RX_NO_ANSWER}", obj.rx_fail_answer, String(Math.round(obj.rx_fail_answer / obj.tx_cnt * 10000) / 100) + "&nbsp;%"]),
tr2(["{#RX_FRAGMENTS}", obj.frame_cnt, ""]),
tr2(["{#TX_RETRANSMITS}", obj.retransmits, ""]),
tr2(["{#INV_LOSS_RATE}", "{#LOST_1} " + obj.ivLoss + " {#LOST_2} " + obj.ivSent + " {#LOST_3}", String(Math.round(obj.ivLoss / obj.ivSent * 10000) / 100) + "&nbsp;%"]),
tr2(["{#DTU_LOSS_RATE}", "{#LOST_1} " + obj.dtuLoss + " {#LOST_2} " + obj.dtuSent + " {#LOST_3}", String(Math.round(obj.dtuLoss / obj.dtuSent * 10000) / 100) + "&nbsp;%"])
])
])
modal("Radio statistics for inverter " + obj.name, ml("div", {}, html))
modal("{#RADIO_STAT_MODAL}: " + obj.name, ml("div", {}, html))
}
function limitModal(obj) {
var opt = [["pct", "%"], ["watt", "W"]];
var html = ml("div", {}, [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-5 my-2"}, "Limit Value"),
ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number"}, "")),
ml("div", {class: "col-12 col-sm-5 my-2"}, "{#LIMIT_VALUE}"),
ml("div", {class: "col-8 col-sm-5"}, ml("input", {name: "limit", type: "number", step: "0.1", min: 1}, "")),
ml("div", {class: "col-4 col-sm-2"}, sel("type", opt, "pct"))
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-5"}, "Keep limit over inverter restart"),
ml("div", {class: "col-8 col-sm-5"}, "{#KEEP_LIMIT}"),
ml("div", {class: "col-4 col-sm-7"}, ml("input", {type: "checkbox", name: "keep"}))
]),
ml("div", {class: "row my-3"},
ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "Apply", class: "btn", onclick: function() {
ml("div", {class: "col a-r"}, ml("input", {type: "button", value: "{#BTN_APPLY}", class: "btn", onclick: function() {
applyLimit(obj.id);
}}, null))
),
ml("div", {class: "row my-4"}, [
ml("div", {class: "col-12 col-sm-5 my-2"}, "Control"),
ml("div", {class: "col-12 col-sm-5 my-2"}, "{#CONTROL}"),
ml("div", {class: "col col-sm-7 a-r"}, [
ml("input", {type: "button", value: "restart", class: "btn", onclick: function() {
ml("input", {type: "button", value: "{#RESTART}", class: "btn", onclick: function() {
applyCtrl(obj.id, "restart");
}}, null),
ml("input", {type: "button", value: "turn off", class: "btn mx-1", onclick: function() {
ml("input", {type: "button", value: "{#TURN_OFF}", class: "btn mx-1", onclick: function() {
applyCtrl(obj.id, "power", 0);
}}, null),
ml("input", {type: "button", value: "turn on", class: "btn", onclick: function() {
ml("input", {type: "button", value: "{#TURN_ON}", class: "btn", onclick: function() {
applyCtrl(obj.id, "power", 1);
}}, null)
])
]),
ml("div", {class: "row mt-1"}, [
ml("div", {class: "col-12 col-sm-5 my-2"}, "Result"),
ml("div", {class: "col-12 col-sm-5 my-2"}, "{#RESULT}"),
ml("div", {class: "col-sm-7 my-2"}, ml("span", {name: "pwrres"}, "-"))
])
]);
modal("Active Power Control for inverter " + obj.name, html);
modal("{#POWER_LIMIT_MODAL}: " + obj.name, html);
}
function applyLimit(id) {
@ -447,36 +463,38 @@
val = 100;
var obj = new Object();
obj.id = id;
obj.cmd = cmd;
obj.val = val;
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
obj.id = id
obj.token = "*"
obj.cmd = cmd
obj.val = val
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj))
}
function applyCtrl(id, cmd, val=0) {
var obj = new Object();
obj.id = id;
obj.cmd = cmd;
obj.val = val;
getAjax("/api/ctrl", ctrlCb2, "POST", JSON.stringify(obj));
obj.id = id
obj.token = "*"
obj.cmd = cmd
obj.val = val
getAjax("/api/ctrl", ctrlCb2, "POST", JSON.stringify(obj))
}
function ctrlCb(obj) {
var e = document.getElementsByName("pwrres")[0];
if(obj.success) {
e.innerHTML = "received command, waiting for inverter acknowledge ...";
e.innerHTML = "{#CMD_RECEIVED_WAIT_ACK}";
tPwrAck = window.setInterval("getAjax('/api/inverter/pwrack/" + obj.id + "', updatePwrAck)", 1000);
}
else
e.innerHTML = "Error: " + obj["error"];
e.innerHTML = "{#ERROR}: " + getErrStr(obj.error);
}
function ctrlCb2(obj) {
var e = document.getElementsByName("pwrres")[0];
if(obj.success)
e.innerHTML = "command received";
e.innerHTML = "{#COMMAND_RECEIVED}";
else
e.innerHTML = "Error: " + obj["error"];
e.innerHTML = "{#ERROR}: " + getErrStr(obj.error);
}
function updatePwrAck(obj) {
@ -486,7 +504,7 @@
clearInterval(tPwrAck);
if(null == e)
return;
e.innerHTML = "inverter acknowledged active power control command";
e.innerHTML = "{#INV_ACK}";
}
function parse(obj) {

87
src/web/html/wizard.html

@ -0,0 +1,87 @@
<!doctype html>
<html>
<head>
<title>{#NAV_WIZARD}</title>
{#HTML_HEADER}
</head>
<body>
<div id="wrapper">
<div class="container d-flex aic jc">
<div id="con"></div>
</div>
</div>
<script type="text/javascript">
var v;
var found = false;
var c = document.getElementById("con");
function sect(e1, e2) {
return ml("div", {class: "row"}, [
ml("div", {class: "col-12"}, ml("p", {}, e1)),
ml("div", {class: "col-12"}, e2)
])
}
function wifi() {
return ml("div", {}, [
ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "{#WELCOME}"))),
ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#NETWORK_SETUP}"))),
sect("{#CHOOSE_WIFI}", ml("select", {id: "net", onchange: () => {if(found) clearInterval(v)}}, ml("option", {value: "-1"}, "---"))),
sect("{#WIFI_MANUAL}", ml("input", {id: "man", type: "text"})),
sect("{#WIFI_PASSWORD}", ml("input", {id: "pwd", type: "password"})),
ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_NEXT}", onclick: () => {saveWifi()}}, null))),
ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}")))
])
}
function checkWifi() {
c.replaceChildren(
ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "{#WELCOME}"))),
ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#TEST_CONNECTION}"))),
sect("{#TRY_TO_CONNECT}", ml("span", {id: "state"}, "{#CONNECTING}")),
ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn hide", id: "btn", value: "{#BTN_FINISH}", onclick: () => {redirect()}}, null))),
ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}")))
)
v = setInterval(() => {getAjax('/api/setup/getip', printIp)}, 2500);
}
function redirect() {
window.location.replace("http://192.168.4.1/")
}
function printIp(obj) {
if("0.0.0.0" != obj["ip"]) {
clearInterval(v)
setHide("btn", false)
document.getElementById("state").innerHTML = "{#NETWORK_SUCCESS}" + obj.ip
}
}
function saveWifi() {
var ssid = document.getElementById("net").value;
if(-1 == ssid)
ssid = document.getElementById("man").value;
getAjax("/api/setup", ((o) => {if(!o.error) checkWifi()}), "POST", JSON.stringify({cmd: "save_wifi", ssid: ssid, pwd: document.getElementById("pwd").value}));
}
function nets(obj) {
var e = document.getElementById("net");
if(obj.networks.length > 0) {
var a = []
a.push(ml("option", {value: -1}, obj.networks.length + " {#NUM_NETWORKS_FOUND}"))
for(n of obj.networks) {
a.push(ml("option", {value: n.ssid}, n.ssid + " (" + n.rssi + "dBm)"))
found = true;
}
e.replaceChildren(...a)
}
getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"}));
}
getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"}));
c.append(wifi())
v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 2500);
</script>
</body>
</html>

75
src/web/lang.h

@ -0,0 +1,75 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __LANG_H__
#define __LANG_H__
#ifdef LANG_DE
#define REBOOT_ESP_APPLY_CHANGES "starte AhoyDTU neu, um die Änderungen zu speichern"
#else /*LANG_EN*/
#define REBOOT_ESP_APPLY_CHANGES "reboot AhoyDTU to apply all your configuration changes"
#endif
#ifdef LANG_DE
#define TIME_NOT_SET "keine gültige Zeit, daher keine Kommunikation zum Wechselrichter möglich"
#else /*LANG_EN*/
#define TIME_NOT_SET "time not set. No communication to inverter possible"
#endif
#ifdef LANG_DE
#define WAS_IN_CH_12_TO_14 "Der ESP war in WLAN Kanal 12 bis 14, was uU. zu Abstürzen führt"
#else /*LANG_EN*/
#define WAS_IN_CH_12_TO_14 "Your ESP was in wifi channel 12 to 14. It may cause reboots of your AhoyDTU"
#endif
#ifdef LANG_DE
#define PATH_NOT_FOUND "Pfad nicht gefunden: "
#else /*LANG_EN*/
#define PATH_NOT_FOUND "Path not found: "
#endif
#ifdef LANG_DE
#define INCOMPLETE_INPUT "Unvollständige Eingabe"
#else /*LANG_EN*/
#define INCOMPLETE_INPUT "Incomplete input"
#endif
#ifdef LANG_DE
#define INVALID_INPUT "Ungültige Eingabe"
#else /*LANG_EN*/
#define INVALID_INPUT "Invalid input"
#endif
#ifdef LANG_DE
#define NOT_ENOUGH_MEM "nicht genügend Speicher"
#else /*LANG_EN*/
#define NOT_ENOUGH_MEM "Not enough memory"
#endif
#ifdef LANG_DE
#define DESER_FAILED "Deserialisierung fehlgeschlagen"
#else /*LANG_EN*/
#define DESER_FAILED "Deserialization failed"
#endif
#ifdef LANG_DE
#define INV_NOT_FOUND "Wechselrichter nicht gefunden!"
#else /*LANG_EN*/
#define INV_NOT_FOUND "inverter not found!"
#endif
#ifdef LANG_DE
#define FACTORY_RESET "Ahoy auf Werkseinstellungen zurücksetzen"
#else /*LANG_EN*/
#define FACTORY_RESET "Ahoy Factory Reset"
#endif
#ifdef LANG_DE
#define BTN_REBOOT "Ahoy neustarten"
#else /*LANG_EN*/
#define BTN_REBOOT "Reboot"
#endif
#endif /*__LANG_H__*/

1539
src/web/lang.json

File diff suppressed because it is too large

387
src/web/web.h

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -36,22 +36,18 @@
#include "html/h/update_html.h"
#include "html/h/visualization_html.h"
#include "html/h/about_html.h"
#include "html/h/wizard_html.h"
#include "html/h/history_html.h"
#define WEB_SERIAL_BUF_SIZE 2048
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"};
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLed2", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"};
template <class HMSYSTEM>
class Web {
public:
Web(void) : mWeb(80), mEvts("/events") {
mProtected = true;
mLogoutTimeout = 0;
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
mSerialAddTime = true;
mSerialClientConnnected = false;
}
void setup(IApp *app, HMSYSTEM *sys, settings_t *config) {
@ -77,9 +73,11 @@ class Web {
mWeb.on("/factorytrue", HTTP_ANY, std::bind(&Web::showHtml, this, std::placeholders::_1));
mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1));
mWeb.on("/wizard", HTTP_GET, std::bind(&Web::onWizard, this, std::placeholders::_1));
mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1));
mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1));
mWeb.on("/history", HTTP_ANY, std::bind(&Web::onHistory, this, std::placeholders::_1));
#ifdef ENABLE_PROMETHEUS_EP
mWeb.on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1));
@ -106,16 +104,6 @@ class Web {
}
void tickSecond() {
if (0 != mLogoutTimeout) {
mLogoutTimeout -= 1;
if (0 == mLogoutTimeout) {
if (strlen(mConfig->sys.adminPwd) > 0)
mProtected = true;
}
DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout));
}
if (mSerialClientConnnected) {
if (mSerialBufFill > 0) {
mEvts.send(mSerialBuf, "serial", millis());
@ -129,27 +117,6 @@ class Web {
return &mWeb;
}
void setProtection(bool protect) {
mProtected = protect;
}
bool isProtected(AsyncWebServerRequest *request) {
bool prot;
prot = mProtected;
if(!prot) {
if(strlen(mConfig->sys.adminPwd) > 0) {
uint8_t ip[4];
ah::ip2Arr(ip, request->client()->remoteIP().toString().c_str());
for(uint8_t i = 0; i < 4; i++) {
if(mLoginIp[i] != ip[i])
prot = true;
}
}
}
return prot;
}
void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!index) {
Serial.printf("Update Start: %s\n", filename.c_str());
@ -249,6 +216,8 @@ class Web {
request->redirect(F("/index"));
else if ((mConfig->sys.protectionMask & PROT_MASK_LIVE) != PROT_MASK_LIVE)
request->redirect(F("/live"));
else if ((mConfig->sys.protectionMask & PROT_MASK_HISTORY) != PROT_MASK_HISTORY)
request->redirect(F("/history"));
else if ((mConfig->sys.protectionMask & PROT_MASK_SERIAL) != PROT_MASK_SERIAL)
request->redirect(F("/serial"));
else if ((mConfig->sys.protectionMask & PROT_MASK_SYSTEM) != PROT_MASK_SYSTEM)
@ -258,13 +227,13 @@ class Web {
}
void checkProtection(AsyncWebServerRequest *request) {
if(isProtected(request)) {
if(mApp->isProtected(request->client()->remoteIP().toString().c_str(), "", true)) {
checkRedirect(request);
return;
}
}
void getPage(AsyncWebServerRequest *request, uint8_t mask, const uint8_t *zippedHtml, uint32_t len) {
void getPage(AsyncWebServerRequest *request, uint16_t mask, const uint8_t *zippedHtml, uint32_t len) {
if (CHECK_MASK(mConfig->sys.protectionMask, mask))
checkProtection(request);
@ -345,8 +314,7 @@ class Web {
if (request->args() > 0) {
if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) {
mProtected = false;
ah::ip2Arr(mLoginIp, request->client()->remoteIP().toString().c_str());
mApp->unlock(request->client()->remoteIP().toString().c_str(), true);
request->redirect("/");
}
}
@ -360,8 +328,7 @@ class Web {
DPRINTLN(DBG_VERBOSE, F("onLogout"));
checkProtection(request);
mProtected = true;
mApp->lock(true);
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
@ -384,7 +351,6 @@ class Web {
void onCss(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onCss"));
mLogoutTimeout = LOGOUT_TIMEOUT;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len);
response->addHeader(F("Content-Encoding"), "gzip");
if(request->hasParam("v")) {
@ -422,7 +388,7 @@ class Web {
void showNotFound(AsyncWebServerRequest *request) {
checkProtection(request);
request->redirect("/setup");
request->redirect("/wizard");
}
void onReboot(AsyncWebServerRequest *request) {
@ -444,6 +410,13 @@ class Web {
getPage(request, PROT_MASK_SETUP, setup_html, setup_html_len);
}
void onWizard(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), wizard_html, wizard_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
response->addHeader(F("content-type"), "text/html; charset=UTF-8");
request->send(response);
}
void showSave(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showSave"));
@ -468,14 +441,24 @@ class Web {
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
mConfig->sys.darkMode = (request->arg("darkMode") == "on");
mConfig->sys.schedReboot = (request->arg("schedReboot") == "on");
mConfig->sys.region = (request->arg("region")).toInt();
mConfig->sys.timezone = (request->arg("timezone")).toInt() - 12;
if (request->arg("cstLnk") != "") {
request->arg("cstLnk").toCharArray(mConfig->plugin.customLink, MAX_CUSTOM_LINK_LEN);
request->arg("cstLnkTxt").toCharArray(mConfig->plugin.customLinkText, MAX_CUSTOM_LINK_TEXT_LEN);
} else {
mConfig->plugin.customLink[0] = '\0';
mConfig->plugin.customLinkText[0] = '\0';
}
// protection
if (request->arg("adminpwd") != "{PWD}") {
request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN);
mProtected = (strlen(mConfig->sys.adminPwd) > 0);
mApp->lock(false);
}
mConfig->sys.protectionMask = 0x0000;
for (uint8_t i = 0; i < 6; i++) {
for (uint8_t i = 0; i < 7; i++) {
if (request->arg("protMask" + String(i)) == "on")
mConfig->sys.protectionMask |= (1 << i);
}
@ -501,13 +484,11 @@ class Web {
mConfig->inst.readGrid = (request->arg("rdGrid") == "on");
mConfig->inst.rstMaxValsMidNight = (request->arg("invRstMaxMid") == "on");
mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat();
mConfig->inst.gapMs = (request->arg("invGap")).toInt();
// pinout
uint8_t pin;
for (uint8_t i = 0; i < 15; i++) {
pin = request->arg(String(pinArgNames[i])).toInt();
for (uint8_t i = 0; i < 16; i++) {
uint8_t pin = request->arg(String(pinArgNames[i])).toInt();
switch(i) {
case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN); break;
case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_NRF_CE_PIN); break;
@ -515,15 +496,16 @@ class Web {
case 3: mConfig->nrf.pinSclk = ((pin != 0xff) ? pin : DEF_NRF_SCLK_PIN); break;
case 4: mConfig->nrf.pinMosi = ((pin != 0xff) ? pin : DEF_NRF_MOSI_PIN); break;
case 5: mConfig->nrf.pinMiso = ((pin != 0xff) ? pin : DEF_NRF_MISO_PIN); break;
case 6: mConfig->led.led0 = pin; break;
case 7: mConfig->led.led1 = pin; break;
case 8: mConfig->led.high_active = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense
case 9: mConfig->led.luminance = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense
case 10: mConfig->cmt.pinSclk = pin; break;
case 11: mConfig->cmt.pinSdio = pin; break;
case 12: mConfig->cmt.pinCsb = pin; break;
case 13: mConfig->cmt.pinFcsb = pin; break;
case 14: mConfig->cmt.pinIrq = pin; break;
case 6: mConfig->led.led[0] = pin; break;
case 7: mConfig->led.led[1] = pin; break;
case 8: mConfig->led.led[2] = pin; break;
case 9: mConfig->led.high_active = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense
case 10: mConfig->led.luminance = pin; break; // this is not really a pin but a polarity, but handling it close to here makes sense
case 11: mConfig->cmt.pinSclk = pin; break;
case 12: mConfig->cmt.pinSdio = pin; break;
case 13: mConfig->cmt.pinCsb = pin; break;
case 14: mConfig->cmt.pinFcsb = pin; break;
case 15: mConfig->cmt.pinIrq = pin; break;
}
}
@ -541,11 +523,13 @@ class Web {
if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) {
mConfig->sun.lat = 0.0;
mConfig->sun.lon = 0.0;
mConfig->sun.offsetSec = 0;
mConfig->sun.offsetSecMorning = 0;
mConfig->sun.offsetSecEvening = 0;
} else {
mConfig->sun.lat = request->arg("sunLat").toFloat();
mConfig->sun.lon = request->arg("sunLon").toFloat();
mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60;
mConfig->sun.offsetSecMorning = request->arg("sunOffsSr").toInt() * 60;
mConfig->sun.offsetSecEvening = request->arg("sunOffsSs").toInt() * 60;
}
// mqtt
@ -568,20 +552,36 @@ class Web {
mConfig->serial.privacyLog = (request->arg("priv") == "on");
mConfig->serial.printWholeTrace = (request->arg("wholeTrace") == "on");
mConfig->serial.showIv = (request->arg("serEn") == "on");
mConfig->serial.log2mqtt = (request->arg("log2mqtt") == "on");
// display
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on");
mConfig->plugin.display.screenSaver = request->arg("disp_screensaver").toInt();
mConfig->plugin.display.rot = request->arg("disp_rot").toInt();
mConfig->plugin.display.type = request->arg("disp_typ").toInt();
mConfig->plugin.display.contrast = (mConfig->plugin.display.type == 0) ? 60 : request->arg("disp_cont").toInt();
mConfig->plugin.display.disp_data = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_data").toInt();
mConfig->plugin.display.disp_clk = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_clk").toInt();
mConfig->plugin.display.disp_cs = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_cs").toInt();
mConfig->plugin.display.disp_reset = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_rst").toInt();
mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_dc").toInt();
mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt();
mConfig->plugin.display.pirPin = request->arg("pir_pin").toInt();
mConfig->plugin.display.graph_size = request->arg("disp_graph_size").toInt();
mConfig->plugin.display.rot = request->arg("disp_rot").toInt();
mConfig->plugin.display.type = request->arg("disp_typ").toInt();
mConfig->plugin.display.contrast = (mConfig->plugin.display.type == DISP_TYPE_T0_NONE) || // contrast available only according optionsMap in setup.html, otherwise default value
(mConfig->plugin.display.type == DISP_TYPE_T10_EPAPER) ? 140 : request->arg("disp_cont").toInt();
mConfig->plugin.display.screenSaver = ((mConfig->plugin.display.type == DISP_TYPE_T1_SSD1306_128X64) // screensaver available only according optionsMap in setup.html, otherwise default value
|| (mConfig->plugin.display.type == DISP_TYPE_T2_SH1106_128X64)
|| (mConfig->plugin.display.type == DISP_TYPE_T4_SSD1306_128X32)
|| (mConfig->plugin.display.type == DISP_TYPE_T5_SSD1306_64X48)
|| (mConfig->plugin.display.type == DISP_TYPE_T6_SSD1309_128X64)) ? request->arg("disp_screensaver").toInt() : 0;
mConfig->plugin.display.graph_ratio = ((mConfig->plugin.display.type == DISP_TYPE_T1_SSD1306_128X64) // display graph available only according optionsMap in setup.html, otherwise has to be 0
|| (mConfig->plugin.display.type == DISP_TYPE_T2_SH1106_128X64)
|| (mConfig->plugin.display.type == DISP_TYPE_T3_PCD8544_84X48)
|| (mConfig->plugin.display.type == DISP_TYPE_T6_SSD1309_128X64)) ? request->arg("disp_graph_ratio").toInt() : 0;
// available pins according pinMap in setup.html, otherwise default value
mConfig->plugin.display.disp_data = (mConfig->plugin.display.type == DISP_TYPE_T0_NONE) ? DEF_PIN_OFF : request->arg("disp_data").toInt();
mConfig->plugin.display.disp_clk = (mConfig->plugin.display.type == DISP_TYPE_T0_NONE) ? DEF_PIN_OFF : request->arg("disp_clk").toInt();
mConfig->plugin.display.disp_cs = (mConfig->plugin.display.type != DISP_TYPE_T3_PCD8544_84X48)
&& (mConfig->plugin.display.type != DISP_TYPE_T10_EPAPER) ? DEF_PIN_OFF : request->arg("disp_cs").toInt();
mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type != DISP_TYPE_T3_PCD8544_84X48)
&& (mConfig->plugin.display.type != DISP_TYPE_T10_EPAPER) ? DEF_PIN_OFF : request->arg("disp_dc").toInt();
mConfig->plugin.display.disp_reset = (mConfig->plugin.display.type != DISP_TYPE_T10_EPAPER) ? DEF_PIN_OFF : request->arg("disp_rst").toInt();
mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type != DISP_TYPE_T10_EPAPER) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt();
mConfig->plugin.display.pirPin = (mConfig->plugin.display.screenSaver != DISP_TYPE_T2_SH1106_128X64) ? DEF_PIN_OFF : request->arg("pir_pin").toInt(); // pir pin only for motion screensaver
// otherweise default value
mApp->saveSettings((request->arg("reboot") == "on"));
@ -594,6 +594,10 @@ class Web {
getPage(request, PROT_MASK_LIVE, visualization_html, visualization_html_len);
}
void onHistory(AsyncWebServerRequest *request) {
getPage(request, PROT_MASK_HISTORY, history_html, history_html_len);
}
void onAbout(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), about_html, about_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
@ -623,17 +627,50 @@ class Web {
#ifdef ENABLE_PROMETHEUS_EP
// Note
// Prometheus exposition format is defined here: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md
// TODO: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels)
enum {
metricsStateStart,
metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4,
metricStateRealtimeFieldId, metricStateRealtimeInverterId,
// NOTE: Grouping for fields with channels and totals is currently not working
// TODO: Handle grouping and sorting for independant from channel number
// NOTE: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels)
const char* metricConstPrefix = "ahoy_solar_";
const char* metricConstInverterFormat = " {inverter=\"%s\"} %d\n";
typedef enum {
metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2,
metricsStateInverterProducing=3, metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5,
metricsStateInverterMaxPower=6, metricsStateInverterRxSuccess=7, metricsStateInverterRxFail=8,
metricsStateInverterRxFailAnswer=9, metricsStateInverterFrameCnt=10, metricsStateInverterTxCnt=11,
metricsStateInverterRetransmits=12, metricsStateInverterIvRxCnt=13, metricsStateInverterIvTxCnt=14,
metricsStateInverterDtuRxCnt=15, metricsStateInverterDtuTxCnt=16,
metricStateRealtimeFieldId=metricsStateInverterDtuTxCnt+1, // ensure that this state follows the last per_inverter state
metricStateRealtimeInverterId,
metricsStateAlarmData,
metricsStateStart,
metricsStateEnd
} metricsStep;
int metricsInverterId;
uint8_t metricsFieldId;
bool metricDeclared;
} MetricStep_t;
MetricStep_t metricsStep = metricsStateInverterInfo;
typedef struct {
const char *topic;
const char *type;
const char *format;
const std::function<uint64_t(Inverter<> *iv)> valueFunc;
} InverterMetric_t;
InverterMetric_t inverterMetrics[17] = {
{ "info", "gauge", " {name=\"%s\",serial=\"%12llx\"} 1\n", [](Inverter<> *iv)-> uint64_t {return iv->config->serial.u64;} },
{ "is_enabled", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} },
{ "is_available", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} },
{ "is_producing", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} },
{ "power_limit_read", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->actPowerLimit;} },
{ "power_limit_ack", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return (iv->powerLimitAck)?1:0;} },
{ "max_power", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} },
{ "radio_rx_success", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} },
{ "radio_rx_fail", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} },
{ "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} },
{ "radio_frame_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} },
{ "radio_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} },
{ "radio_retransmits", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} },
{ "radio_iv_loss_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivLoss;} },
{ "radio_iv_sent_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.ivSent;} },
{ "radio_dtu_loss_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuLoss;} },
{ "radio_dtu_sent_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuSent;} }
};
void showMetrics(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
@ -648,85 +685,70 @@ class Web {
char type[60], topic[100], val[25];
size_t len = 0;
int alarmChannelId;
int metricsChannelId;
// Perform grouping on metrics according to format specification
// Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops.
// So several "Info:" blocks are used to keep the transmission going
switch (metricsStep) {
case metricsStateStart: // System Info & NRF Statistics : fit to one packet
snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n");
snprintf(topic,sizeof(topic),"ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",
case metricsStateStart: // System Info : fit to one packet
snprintf(type,sizeof(type),"# TYPE %sinfo gauge\n",metricConstPrefix);
snprintf(topic,sizeof(topic),"%sinfo{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",metricConstPrefix,
mApp->getVersion(), mConfig->sys.deviceName);
metrics = String(type) + String(topic);
snprintf(type,sizeof(type),"# TYPE ahoy_solar_freeheap gauge\n");
snprintf(topic,sizeof(topic),"ahoy_solar_freeheap{devicename=\"%s\"} %u\n",mConfig->sys.deviceName,ESP.getFreeHeap());
snprintf(type,sizeof(type),"# TYPE %sfreeheap gauge\n",metricConstPrefix);
snprintf(topic,sizeof(topic),"%sfreeheap{devicename=\"%s\"} %u\n",metricConstPrefix,mConfig->sys.deviceName,ESP.getFreeHeap());
metrics += String(type) + String(topic);
snprintf(type,sizeof(type),"# TYPE ahoy_solar_uptime counter\n");
snprintf(topic,sizeof(topic),"ahoy_solar_uptime{devicename=\"%s\"} %u\n", mConfig->sys.deviceName, mApp->getUptime());
snprintf(type,sizeof(type),"# TYPE %suptime counter\n",metricConstPrefix);
snprintf(topic,sizeof(topic),"%suptime{devicename=\"%s\"} %u\n",metricConstPrefix, mConfig->sys.deviceName, mApp->getUptime());
metrics += String(type) + String(topic);
snprintf(type,sizeof(type),"# TYPE ahoy_solar_wifi_rssi_db gauge\n");
snprintf(topic,sizeof(topic),"ahoy_solar_wifi_rssi_db{devicename=\"%s\"} %d\n", mConfig->sys.deviceName, WiFi.RSSI());
snprintf(type,sizeof(type),"# TYPE %swifi_rssi_db gauge\n",metricConstPrefix);
snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricConstPrefix, mConfig->sys.deviceName, WiFi.RSSI());
metrics += String(type) + String(topic);
// NRF Statistics
// @TODO 2023-10-01: the statistic data is now available per inverter
/*stat = mApp->getNrfStatistics();
metrics += radioStatistic(F("rx_success"), stat->rxSuccess);
metrics += radioStatistic(F("rx_fail"), stat->rxFail);
metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser);
metrics += radioStatistic(F("frame_cnt"), stat->frmCnt);
metrics += radioStatistic(F("tx_cnt"), stat->txCnt);
metrics += radioStatistic(F("retrans_cnt"), stat->retransmits);*/
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
len = snprintf(reinterpret_cast<char*>(buffer), maxLen,"%s",metrics.c_str());
// Next is Inverter information
metricsInverterId = 0;
metricsStep = metricsStateInverter1;
metricsStep = metricsStateInverterInfo;
break;
case metricsStateInverter1: // Information about all inverters configured : fit to one packet
metrics = "# TYPE ahoy_solar_inverter_info gauge\n";
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n",
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->serial.u64;});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
metricsStep = metricsStateInverter2;
break;
case metricsStateInverter2: // Information about all inverters configured : fit to one packet
metrics += "# TYPE ahoy_solar_inverter_is_enabled gauge\n";
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->enabled;});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
metricsStep = metricsStateInverter3;
break;
case metricsStateInverter3: // Information about all inverters configured : fit to one packet
metrics += "# TYPE ahoy_solar_inverter_is_available gauge\n";
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable();});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
metricsStep = metricsStateInverter4;
break;
case metricsStateInverter4: // Information about all inverters configured : fit to one packet
metrics += "# TYPE ahoy_solar_inverter_is_producing gauge\n";
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isProducing();});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
// Start Realtime Field loop
// Information about all inverters configured : each metric for all inverters must fit to one network packet
case metricsStateInverterInfo:
case metricsStateInverterEnabled:
case metricsStateInverterAvailable:
case metricsStateInverterProducing:
case metricsStateInverterPowerLimitRead:
case metricsStateInverterPowerLimitAck:
case metricsStateInverterMaxPower:
case metricsStateInverterRxSuccess:
case metricsStateInverterRxFail:
case metricsStateInverterRxFailAnswer:
case metricsStateInverterFrameCnt:
case metricsStateInverterTxCnt:
case metricsStateInverterRetransmits:
case metricsStateInverterIvRxCnt:
case metricsStateInverterIvTxCnt:
case metricsStateInverterDtuRxCnt:
case metricsStateInverterDtuTxCnt:
metrics = "# TYPE ahoy_solar_inverter_" + String(inverterMetrics[metricsStep].topic) + " " + String(inverterMetrics[metricsStep].type) + "\n";
metrics += inverterMetric(topic, sizeof(topic),
(String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].topic +
inverterMetrics[metricsStep].format).c_str(),
inverterMetrics[metricsStep].valueFunc);
len = snprintf(reinterpret_cast<char*>(buffer), maxLen, "%s", metrics.c_str());
// ugly hack to increment the enum
metricsStep = static_cast<MetricStep_t>( static_cast<int>(metricsStep) + 1);
// Prepare Realtime Field loop, which may be startet next
metricsFieldId = FLD_UDC;
metricsStep = metricStateRealtimeFieldId;
break;
case metricStateRealtimeFieldId: // Iterate over all defined fields
if (metricsFieldId < FLD_LAST_ALARM_CODE) {
metrics = "# Info: processing realtime field #"+String(metricsFieldId)+"\n";
metricDeclared = false;
metricTotalDeclard = false;
metricsInverterId = 0;
metricsStep = metricStateRealtimeInverterId;
@ -734,18 +756,17 @@ class Web {
metrics = "# Info: all realtime fields processed\n";
metricsStep = metricsStateAlarmData;
}
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
len = snprintf(reinterpret_cast<char *>(buffer), maxLen, "%s", metrics.c_str());
break;
case metricStateRealtimeInverterId: // Iterate over all inverters for this field
metrics = "";
if (metricsInverterId < mSys->getNumInverters()) {
// process all channels of this inverter
iv = mSys->getInverterByPos(metricsInverterId);
if (NULL != iv) {
rec = iv->getRecordStruct(RealTimeRunData_Debug);
for (metricsChannelId=0; metricsChannelId < rec->length;metricsChannelId++) {
for (int metricsChannelId=0; metricsChannelId < rec->length;metricsChannelId++) {
uint8_t channel = rec->assign[metricsChannelId].ch;
// Try inverter channel (channel 0) or any channel with maxPwr > 0
@ -755,22 +776,26 @@ class Web {
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec));
// Declare metric only once
if (channel != 0 && !metricDeclared) {
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str());
snprintf(type, sizeof(type), "# TYPE %s%s%s %s\n",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str());
metrics += type;
metricDeclared = true;
}
// report value
if (0 == channel) {
char total[7];
total[0] = 0;
// Report a _total value if also channel values were reported. Otherwise report without _total
char total[7] = {0};
if (metricDeclared) {
// A declaration and value for channels has been delivered. So declare and deliver a _total metric
strncpy(total,"_total",sizeof(total));
// A declaration and value for channels have been delivered. So declare and deliver a _total metric
snprintf(total, sizeof(total), "_total");
}
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str());
metrics += type;
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name);
if (!metricTotalDeclard) {
snprintf(type, sizeof(type), "# TYPE %s%s%s%s %s\n",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str());
metrics += type;
metricTotalDeclard = true;
}
snprintf(topic, sizeof(topic), "%s%s%s%s{inverter=\"%s\"}",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total,iv->config->name);
} else {
// Report (non zero) channel value
// Use a fallback channel name (ch0, ch1, ...)if non is given by user
char chName[MAX_NAME_LENGTH];
if (iv->config->chName[channel-1][0] != 0) {
@ -778,7 +803,7 @@ class Web {
} else {
snprintf(chName,sizeof(chName),"ch%1d",channel);
}
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName);
snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\",channel=\"%s\"}",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,chName);
}
snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec));
metrics += topic;
@ -787,12 +812,14 @@ class Web {
}
}
if (metrics.length() < 1) {
metrics = "# Info: Field #"+String(metricsFieldId)+" not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n";
metrics = "# Info: Field #"+String(metricsFieldId)+" (" + fields[metricsFieldId] +
") not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n";
metricsFieldId++; // Process next field Id
metricsStep = metricStateRealtimeFieldId;
}
} else {
metrics = "# Info: No data for field #"+String(metricsFieldId)+" of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n";
metrics = "# Info: No data for field #"+String(metricsFieldId)+ " (" + fields[metricsFieldId] +
") of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n";
metricsFieldId++; // Process next field Id
metricsStep = metricStateRealtimeFieldId;
}
@ -803,12 +830,12 @@ class Web {
metricsFieldId++; // Process next field Id
metricsStep = metricStateRealtimeFieldId;
}
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
len = snprintf(reinterpret_cast<char *>(buffer), maxLen, "%s", metrics.c_str());
break;
case metricsStateAlarmData: // Alarm Info loop : fit to one packet
// Perform grouping on metrics according to Prometheus exposition format specification
snprintf(type, sizeof(type),"# TYPE ahoy_solar_%s gauge\n",fields[FLD_LAST_ALARM_CODE]);
snprintf(type, sizeof(type),"# TYPE %s%s gauge\n",metricConstPrefix,fields[FLD_LAST_ALARM_CODE]);
metrics = type;
for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) {
@ -820,22 +847,24 @@ class Web {
alarmChannelId = 0;
if (alarmChannelId < rec->length) {
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec));
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name);
snprintf(topic, sizeof(topic), "%s%s%s{inverter=\"%s\"}",metricConstPrefix, iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name);
snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec));
metrics += topic;
metrics += val;
}
}
}
len = snprintf((char*)buffer,maxLen,"%s",metrics.c_str());
len = snprintf(reinterpret_cast<char*>(buffer), maxLen, "%s", metrics.c_str());
metricsStep = metricsStateEnd;
break;
case metricsStateEnd:
default: // end of transmission
DBGPRINT("E: Prometheus: Bad metricsStep=");
DBGPRINTLN(String(metricsStep));
case metricsStateEnd:
len = 0;
break;
}
} // switch
return len;
});
request->send(response);
@ -843,27 +872,18 @@ class Web {
// Traverse all inverters and collect the metric via valueFunc
String inverterMetric(char *buffer, size_t len, const char *format, std::function<uint64_t(Inverter<> *iv, IApp *mApp)> valueFunc) {
Inverter<> *iv;
String inverterMetric(char *buffer, size_t len, const char *format, std::function<uint64_t(Inverter<> *iv)> valueFunc) {
String metric = "";
for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) {
iv = mSys->getInverterByPos(metricsInverterId);
for (int id = 0; id < mSys->getNumInverters();id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) {
snprintf(buffer,len,format,iv->config->name, valueFunc(iv,mApp));
snprintf(buffer,len,format,iv->config->name, valueFunc(iv));
metric += String(buffer);
}
}
return metric;
}
String radioStatistic(String statistic, uint32_t value) {
char type[60], topic[80], val[25];
snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str());
snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s",statistic.c_str());
snprintf(val, sizeof(val), "%d", value);
return ( String(type) + "\n" + String(topic) + " " + String(val) + "\n");
}
std::pair<String, String> convertToPromUnits(String shortUnit) {
if(shortUnit == "A") return {"_ampere", "gauge"};
if(shortUnit == "V") return {"_volt", "gauge"};
@ -876,24 +896,27 @@ class Web {
if(shortUnit == "Hz") return {"_hertz", "gauge"};
return {"", "gauge"};
}
private:
int metricsInverterId = 0;
uint8_t metricsFieldId = 0;
bool metricDeclared = false, metricTotalDeclard = false;
#endif
private:
AsyncWebServer mWeb;
AsyncEventSource mEvts;
bool mProtected;
uint32_t mLogoutTimeout;
uint8_t mLoginIp[4];
IApp *mApp;
HMSYSTEM *mSys;
IApp *mApp = nullptr;
HMSYSTEM *mSys = nullptr;
settings_t *mConfig;
settings_t *mConfig = nullptr;
bool mSerialAddTime;
bool mSerialAddTime = true;
char mSerialBuf[WEB_SERIAL_BUF_SIZE];
uint16_t mSerialBufFill;
bool mSerialClientConnnected;
uint16_t mSerialBufFill = 0;
bool mSerialClientConnnected = false;
File mUploadFp;
bool mUploadFail;
bool mUploadFail = false;
};
#endif /*__WEB_H__*/

21
src/wifi/ahoywifi.cpp

@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@ -40,6 +40,7 @@ void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) {
mCnt = 0;
mScanActive = false;
mScanCnt = 0;
mStopApAllowed = true;
#if defined(ESP8266)
wifiConnectHandler = WiFi.onStationModeConnected(std::bind(&ahoywifi::onConnect, this, std::placeholders::_1));
@ -91,10 +92,12 @@ void ahoywifi::tickWifiLoop() {
}
#if !defined(ESP32)
MDNS.update();
if(WiFi.channel() > 11)
mWasInCh12to14 = true;
#endif
return;
case IN_AP_MODE:
if (WiFi.softAPgetStationNum() == 0) {
if ((WiFi.softAPgetStationNum() == 0) || (!mStopApAllowed)) {
mCnt = 0;
mDns.stop();
WiFi.mode(WIFI_AP_STA);
@ -105,7 +108,7 @@ void ahoywifi::tickWifiLoop() {
}
break;
case DISCONNECTED:
if (WiFi.softAPgetStationNum() > 0) {
if ((WiFi.softAPgetStationNum() > 0) && (mStopApAllowed)) {
mStaConn = IN_AP_MODE;
// first time switch to AP Mode
if (mScanActive) {
@ -182,10 +185,12 @@ void ahoywifi::tickWifiLoop() {
break;
case GOT_IP:
welcome(WiFi.localIP().toString(), F(" (Station)"));
WiFi.softAPdisconnect();
WiFi.mode(WIFI_STA);
DBGPRINTLN(F("[WiFi] AP disabled"));
delay(100);
if(mStopApAllowed) {
WiFi.softAPdisconnect();
WiFi.mode(WIFI_STA);
DBGPRINTLN(F("[WiFi] AP disabled"));
delay(100);
}
mAppWifiCb(true);
mGotDisconnect = false;
mStaConn = IN_STA_MODE;
@ -290,7 +295,7 @@ bool ahoywifi::getNtpTime(void) {
if(NTP_PACKET_SIZE <= mUdp.parsePacket()) {
uint64_t secsSince1900;
mUdp.read(buf, NTP_PACKET_SIZE);
secsSince1900 = (buf[40] << 24);
secsSince1900 = ((uint64_t)buf[40] << 24);
secsSince1900 |= (buf[41] << 16);
secsSince1900 |= (buf[42] << 8);
secsSince1900 |= (buf[43] );

35
src/wifi/ahoywifi.h

@ -1,7 +1,8 @@
//------------------------------------//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#if !defined(ETHERNET)
#ifndef __AHOYWIFI_H__
#define __AHOYWIFI_H__
@ -28,6 +29,17 @@ class ahoywifi {
bool getNtpTime(void);
void scanAvailNetworks(void);
bool getAvailNetworks(JsonObject obj);
void setStopApAllowedMode(bool allowed) {
mStopApAllowed = allowed;
}
String getStationIp(void) {
return WiFi.localIP().toString();
}
void setupStation(void);
bool getWasInCh12to14() const {
return mWasInCh12to14;
}
private:
typedef enum WiFiStatus {
@ -43,7 +55,6 @@ class ahoywifi {
void setupWifi(bool startAP);
void setupAp(void);
void setupStation(void);
void sendNTPpacket(IPAddress& address);
void sortRSSI(int *sort, int n);
bool getBSSIDs(void);
@ -60,7 +71,7 @@ class ahoywifi {
void welcome(String ip, String mode);
settings_t *mConfig;
settings_t *mConfig = nullptr;
appWifiCb mAppWifiCb;
DNSServer mDns;
@ -70,14 +81,16 @@ class ahoywifi {
WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler;
#endif
WiFiStatus_t mStaConn;
uint8_t mCnt;
uint32_t *mUtcTimestamp;
WiFiStatus_t mStaConn = DISCONNECTED;
uint8_t mCnt = 0;
uint32_t *mUtcTimestamp = nullptr;
uint8_t mScanCnt;
bool mScanActive;
bool mGotDisconnect;
uint8_t mScanCnt = 0;
bool mScanActive = false;
bool mGotDisconnect = false;
std::list<uint8_t> mBSSIDList;
bool mStopApAllowed = false;
bool mWasInCh12to14 = false;
};
#endif /*__AHOYWIFI_H__*/

Loading…
Cancel
Save