Browse Source

Merge branch 'development03' into ethW5500

pull/1512/head
lumapu 1 year ago
parent
commit
b2e6223efb
  1. 59
      .github/workflows/compile_development.yml
  2. 166
      .github/workflows/compile_release.yml
  3. 8
      README.md
  4. 55
      manual/User_Manual.md
  5. 35
      patches/RF24.patch
  6. 6
      scripts/applyPatches.py
  7. 22
      scripts/buildManifest.py
  8. 128
      src/CHANGES.md
  9. 39
      src/app.cpp
  10. 118
      src/app.h
  11. 8
      src/appInterface.h
  12. 2
      src/config/config.h
  13. 57
      src/config/settings.h
  14. 6
      src/defines.h
  15. 2
      src/eth/ahoyeth.h
  16. 12
      src/hm/CommQueue.h
  17. 353
      src/hm/Communication.h
  18. 80
      src/hm/Heuristic.h
  19. 26
      src/hm/HeuristicInv.h
  20. 20
      src/hm/hmDefines.h
  21. 350
      src/hm/hmInverter.h
  22. 114
      src/hm/hmRadio.h
  23. 29
      src/hm/hmSystem.h
  24. 2
      src/hm/nrfHal.h
  25. 39
      src/hm/radio.h
  26. 6
      src/hm/simulator.h
  27. 274
      src/hms/cmt2300a.h
  28. 2
      src/hms/cmtHal.h
  29. 7
      src/hms/esp32_3wSpi.h
  30. 81
      src/hms/hmsRadio.h
  31. 6
      src/platformio.ini
  32. 31
      src/plugins/Display/Display.h
  33. 12
      src/plugins/Display/Display_Mono.h
  34. 6
      src/plugins/Display/Display_Mono_128X32.h
  35. 39
      src/plugins/Display/Display_Mono_128X64.h
  36. 6
      src/plugins/Display/Display_Mono_64X48.h
  37. 27
      src/plugins/Display/Display_Mono_84X48.h
  38. 17
      src/plugins/Display/Display_ePaper.cpp
  39. 49
      src/plugins/history.h
  40. 193
      src/publisher/pubMqtt.h
  41. 137
      src/publisher/pubMqttIvData.h
  42. 12
      src/publisher/pubSerial.h
  43. 6
      src/utils/helper.cpp
  44. 18
      src/utils/improv.h
  45. 20
      src/utils/scheduler.h
  46. 4
      src/utils/spiPatcher.cpp
  47. 6
      src/utils/spiPatcher.h
  48. 4
      src/utils/syslog.cpp
  49. 5
      src/utils/syslog.h
  50. 6
      src/utils/timemonitor.h
  51. 7
      src/web/Protection.cpp
  52. 122
      src/web/Protection.h
  53. 185
      src/web/RestApi.h
  54. 6
      src/web/html/colorBright.css
  55. 16
      src/web/html/colorDark.css
  56. 24
      src/web/html/history.html
  57. 6
      src/web/html/includes/nav.html
  58. 27
      src/web/html/index.html
  59. 18
      src/web/html/serial.html
  60. 147
      src/web/html/setup.html
  61. 23
      src/web/html/style.css
  62. 31
      src/web/html/visualization.html
  63. 16
      src/web/lang.h
  64. 123
      src/web/lang.json
  65. 115
      src/web/web.h
  66. 2
      src/wifi/ahoywifi.cpp
  67. 21
      src/wifi/ahoywifi.h

59
.github/workflows/compile_development.yml

@ -1,4 +1,4 @@
name: Ahoy Dev-Build for ESP8266/ESP32 name: Ahoy Development
on: on:
push: push:
@ -8,13 +8,15 @@ on:
jobs: jobs:
check: check:
name: Check Repository
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'lumapu/ahoy' && github.ref_name == 'development03' if: github.repository == 'lumapu/ahoy' && github.ref_name == 'development03'
continue-on-error: true continue-on-error: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
build-en: build-en:
name: Build (EN)
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
@ -32,14 +34,14 @@ jobs:
- opendtufusion - opendtufusion
- opendtufusion-ethernet - opendtufusion-ethernet
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v2.1 - uses: benjlevesque/short-sha@v3.0
id: short-sha id: short-sha
with: with:
length: 7 length: 7
- name: Cache Pip - name: Cache Pip
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@ -47,13 +49,13 @@ jobs:
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Cache PlatformIO - name: Cache PlatformIO
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.platformio path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
@ -75,6 +77,7 @@ jobs:
path: firmware/* path: firmware/*
build-de: build-de:
name: Build (DE)
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
@ -92,14 +95,14 @@ jobs:
- opendtufusion-de - opendtufusion-de
- opendtufusion-ethernet-de - opendtufusion-ethernet-de
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v2.1 - uses: benjlevesque/short-sha@v3.0
id: short-sha id: short-sha
with: with:
length: 7 length: 7
- name: Cache Pip - name: Cache Pip
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@ -107,13 +110,13 @@ jobs:
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Cache PlatformIO - name: Cache PlatformIO
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.platformio path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
@ -135,10 +138,12 @@ jobs:
path: firmware/* path: firmware/*
deploy: deploy:
name: Update Artifacts / Deploy
needs: [build-en, build-de] needs: [build-en, build-de]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: false
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
#- name: Copy boot_app0.bin #- name: Copy boot_app0.bin
# run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin # run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
@ -155,13 +160,39 @@ jobs:
- name: Set Version - name: Set Version
uses: cschleiden/replace-tokens@v1 uses: cschleiden/replace-tokens@v1
with: with:
files: tools/esp8266/User_Manual.md files: manual/User_Manual.md
env: env:
VERSION: ${{ steps.version_name.outputs.name }} VERSION: ${{ steps.version_name.outputs.name }}
- name: Create ESP Web Tools Manifest
working-directory: src
run: python ../scripts/buildManifest.py
- name: Copy install html
run: mv scripts/gh-action-dev-build-flash.html firmware/install.html
- name: Copy Changes.md
run: mv src/CHANGES.md firmware/CHANGES.md
- name: Rename firmware directory - name: Rename firmware directory
run: mv firmware ${{ steps.version_name.outputs.name }} run: mv firmware ${{ steps.version_name.outputs.name }}
- name: delete environment Artifacts
uses: geekyeggo/delete-artifact@v4
with:
name: dev-*
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: dev-${{ steps.version_name.outputs.name }}
path: |
${{ steps.version_name.outputs.name }}/*
manual/User_Manual.md
manual/Getting_Started.md
- name: Deploy - name: Deploy
uses: nogsantos/scp-deploy@master uses: nogsantos/scp-deploy@master
with: with:

166
.github/workflows/compile_release.yml

@ -1,29 +1,49 @@
name: Ahoy Release for ESP8266/ESP32 name: Ahoy Release
on: on:
push: push:
branches: main branches: main
paths: paths-ignore:
- 'src/**' # build only when changes occur here - '**.md' # Do no build on *.md changes
- '.github/workflows/compile_release.yml'
- '!README.md'
- '!CHANGES.md'
- '!User_Manual.md'
jobs: jobs:
build: build:
name: Build Environments
runs-on: ubuntu-latest 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: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: - uses: benjlevesque/short-sha@v3.0
ref: main
- uses: benjlevesque/short-sha@v2.1
id: short-sha id: short-sha
with: with:
length: 7 length: 7
- name: Cache Pip - name: Cache Pip
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@ -31,13 +51,13 @@ jobs:
${{ runner.os }}-pip- ${{ runner.os }}-pip-
- name: Cache PlatformIO - name: Cache PlatformIO
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.platformio path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
@ -47,56 +67,108 @@ jobs:
pip install --upgrade platformio pip install --upgrade platformio
- name: Run 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 - name: Create Artifact
run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin 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 release:
id: create-release name: Create Release
uses: actions/create-release@v1 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: with:
draft: false merge-multiple: true
prerelease: false path: firmware
release_name: ${{ steps.rename-binary-files.outputs.name }}
tag_name: ${{ steps.rename-binary-files.outputs.name }} - name: Get Version from code
body_path: src/CHANGES.md id: version_name
env: run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
GITHUB_TOKEN: ${{ github.token }}
- 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 - name: Set Version
uses: cschleiden/replace-tokens@v1 uses: cschleiden/replace-tokens@v1
with: with:
files: User_Manual.md files: manual/User_Manual.md
env: env:
VERSION: ${{ steps.rename-binary-files.outputs.name }} VERSION: ${{ steps.version_name.outputs.name }}
- name: Create Artifact - name: Rename firmware directory
run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip src/firmware/* User_Manual.md run: mv firmware ${{ steps.version_name.outputs.name }}
- name: Upload Release - name: Rename firmware directory
id: upload-release uses: vimtor/action-zip@v1.2
uses: actions/upload-release-asset@v1 with:
env: files: ${{ steps.version_name.outputs.name }} manual/User_Manual.md manual/Getting_Started.md
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} dest: '${{ steps.version_name.outputs.name }}.zip'
- name: Publish Release
uses: ncipollo/release-action@v1
with: with:
upload_url: ${{ steps.create-release.outputs.upload_url }} artifactErrorsFailBuild: true
asset_path: ./${{ steps.rename-binary-files.outputs.name }}.zip skipIfReleaseExists: true
asset_name: ${{ steps.rename-binary-files.outputs.name }}.zip bodyFile: src/CHANGES.md
asset_content_type: application/zip 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 - 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 - name: Deploy
uses: nogsantos/scp-deploy@master uses: nogsantos/scp-deploy@master
with: with:
src: src/${{ steps.rename-binary-files.outputs.name }}/ src: ${{ steps.version_name.outputs.name }}/
host: ${{ secrets.FW_SSH_HOST }} host: ${{ secrets.FW_SSH_HOST }}
remote: ${{ secrets.FW_SSH_DIR }}/release remote: ${{ secrets.FW_SSH_DIR }}/release
port: ${{ secrets.FW_SSH_PORT }} port: ${{ secrets.FW_SSH_PORT }}

8
README.md

@ -31,7 +31,7 @@ Table of approaches:
| Board | MI | HM | HMS/HMT | comment | HowTo start | | 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/) | ❌ | ✔️ | ❌ | | | [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | | | [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | |
| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | | [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
@ -39,11 +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** ⚠️ **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 ## Getting Started
1. [Guide how to start with a ESP module](Getting_Started.md) 1. [Guide how to start with a ESP module](manual/Getting_Started.md)
2. [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 ](ahoy_config.md) 3. [Ahoy Configuration ](manual/ahoy_config.md)
## Our Website ## Our Website
[https://ahoydtu.de](https://ahoydtu.de) [https://ahoydtu.de](https://ahoydtu.de)
@ -64,4 +64,4 @@ If you encounter any problems, use the issue tracker on Github. Provide a detail
- [OpenDTU](https://github.com/tbnobody/OpenDTU) - [OpenDTU](https://github.com/tbnobody/OpenDTU)
<- Our sister project for Hoymiles HM- and HMS-/HMT-series (for ESP32 only!) <- Our sister project for Hoymiles HM- and HMS-/HMT-series (for ESP32 only!)
- [hms-mqtt-publisher](https://github.com/DennisOSRM/hms-mqtt-publisher) - [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

55
manual/User_Manual.md

@ -166,6 +166,8 @@ inverter/ctrl/limit/0 600W
### Power Limit persistent ### 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. 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 ## Control via REST API
### Generic Information ### 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. 👆 `<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) ### Inverter Power (On / Off)
```json ```json
@ -245,19 +287,6 @@ The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]`
The `VALUE` represents watts in a range of `[1.0 .. 6553.5]` 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) ## 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) * 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)
* You can check the inverter set point for the power limit control on the topic `<TOPIC>/<INVERTER_NAME_FROM_SETUP>/ch0/PowerLimit` 👆 This value is ALWAYS in percent of the maximum power limit of the inverter. In regular cases this value will be updated within approx. 15 seconds. (depends on request intervall) * You can check the inverter set point for the power limit control on the topic `<TOPIC>/<INVERTER_NAME_FROM_SETUP>/ch0/PowerLimit` 👆 This value is ALWAYS in percent of the maximum power limit of the inverter. In regular cases this value will be updated within approx. 15 seconds. (depends on request intervall)

35
patches/RF24.patch

@ -1,35 +0,0 @@
diff --git a/RF24.cpp b/RF24.cpp
index 9e5b4a8..a4de63c 100644
--- a/RF24.cpp
+++ b/RF24.cpp
@@ -1871,6 +1871,11 @@ uint8_t RF24::getARC(void)
return read_register(OBSERVE_TX) & 0x0F;
}
+uint8_t RF24::getPLOS(void)
+{
+ return read_register(OBSERVE_TX) & 0x0F;
+}
+
/****************************************************************************/
bool RF24::setDataRate(rf24_datarate_e speed)
diff --git a/RF24.h b/RF24.h
index dbd32ae..a3d6b52 100644
--- a/RF24.h
+++ b/RF24.h
@@ -1644,6 +1644,7 @@ public:
* @return Returns values from 0 to 15.
*/
uint8_t getARC(void);
+ uint8_t getPLOS(void);
/**
* Set the transmission @ref Datarate
@@ -2415,4 +2416,4 @@ private:
* Use `ctrl+c` to quit at any time.
*/
-#endif // __RF24_H__
\ No newline at end of file
+#endif // __RF24_H__

6
scripts/applyPatches.py

@ -12,11 +12,11 @@ def applyPatch(libName, patchFile):
os.chdir('.pio/libdeps/' + env['PIOENV'] + '/' + libName) 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): if (process.returncode == 0):
print('\'' + patchFile + '\' already applied') print('\'' + patchFile + '\' already applied')
else: else:
process = subprocess.run(['git', 'apply', '../../../../' + patchFile]) process = subprocess.run(['git', 'apply', '--ignore-whitespace', '../../../../' + patchFile])
if (process.returncode == 0): if (process.returncode == 0):
print('\'' + patchFile + '\' applied') print('\'' + patchFile + '\' applied')
else: else:
@ -32,5 +32,3 @@ if env['PIOENV'][:22] != "opendtufusion-ethernet":
if env['PIOENV'][:13] == "opendtufusion": if env['PIOENV'][:13] == "opendtufusion":
applyPatch("GxEPD2", "../patches/GxEPD2_SW_SPI.patch") applyPatch("GxEPD2", "../patches/GxEPD2_SW_SPI.patch")
applyPatch("RF24", "../patches/RF24_Hal.patch") applyPatch("RF24", "../patches/RF24_Hal.patch")
else:
applyPatch("RF24", "../patches/RF24.patch")

22
scripts/buildManifest.py

@ -36,9 +36,27 @@ def buildManifest(path, infile, outfile):
esp32["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096}) esp32["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096})
esp32["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768}) esp32["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768})
esp32["parts"].append({"path": "ESP32/ota.bin", "offset": 57344}) esp32["parts"].append({"path": "ESP32/ota.bin", "offset": 57344})
esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32.bin", "offset": 65536}) esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32-wroom32.bin", "offset": 65536})
data["builds"].append(esp32) data["builds"].append(esp32)
esp32s2 = {}
esp32s2["chipFamily"] = "ESP32-S2"
esp32s2["parts"] = []
esp32s2["parts"].append({"path": "ESP32-S2/bootloader.bin", "offset": 4096})
esp32s2["parts"].append({"path": "ESP32-S2/partitions.bin", "offset": 32768})
esp32s2["parts"].append({"path": "ESP32-S2/ota.bin", "offset": 57344})
esp32s2["parts"].append({"path": "ESP32-S2/" + version[1] + "_" + sha + "_esp32-s2-mini.bin", "offset": 65536})
data["builds"].append(esp32s2)
esp32s3 = {}
esp32s3["chipFamily"] = "ESP32-S3"
esp32s3["parts"] = []
esp32s3["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096})
esp32s3["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768})
esp32s3["parts"].append({"path": "ESP32/ota.bin", "offset": 57344})
esp32s3["parts"].append({"path": "ESP32-S3/" + version[1] + "_" + sha + "_opendtufusion.bin", "offset": 65536})
data["builds"].append(esp32s3)
esp8266 = {} esp8266 = {}
esp8266["chipFamily"] = "ESP8266" esp8266["chipFamily"] = "ESP8266"
esp8266["parts"] = [] esp8266["parts"] = []
@ -47,7 +65,7 @@ def buildManifest(path, infile, outfile):
jsonString = json.dumps(data, indent=2) jsonString = json.dumps(data, indent=2)
fp = open(path + "firmware/" + outfile, "w") fp = open(path + "../firmware/" + outfile, "w")
fp.write(jsonString) fp.write(jsonString)
fp.close() fp.close()

128
src/CHANGES.md

@ -1,5 +1,127 @@
# Development Changes # Development Changes
## 0.8.89 - 2024-03-02
* merge PR: Collection of small fixes #1465
* fix: show esp type on `/history` #1463
* improved HMS-400-1T support (serial number 1125...) #1460
## 0.8.88 - 2024-02-28
* fix MqTT statistic data overflow #1458
* add HMS-400-1T support (serial number 1125...) #1460
* removed `yield efficiency` because the inverter already calculates correct #1243
* merge PR: Remove hint to INV_RESET_MIDNIGHT resp. INV_PAUSE_DURING_NIGHT #1431
## 0.8.87 - 2024-02-25
* fix translations #1455 #1442
## 0.8.86 - 2024-02-23
* RestAPI check for parent element to be JsonObject #1449
* fix translation #1448 #1442
* fix reset values when inverter status is 'not available' #1035 #1437
## 0.8.85 - 2024-02-22
* possible fix of MqTT fix "total values are sent to often" #1421
* fix translation #1442
* availability check only related to live data #1035 #1437
## 0.8.84 - 2024-02-19
* fix homeassistant autodiscovery #1432
* merge PR: more gracefull handling of complete retransmits #1433
# RELEASE 0.8.83 - 2024-02-16
## 0.8.82 - 2024-02-15
* fixed crash once firmware version was read and sent via MqTT #1428
* possible fix: reset yield offset on midnight #1429
## 0.8.81 - 2024-02-13
* fixed authentication with empty token #1415
* added new setting for future function to send log via MqTT
* combined firmware and hardware version to JSON topics (MqTT) #1212
## 0.8.80 - 2024-02-12
* optimize API authentication, Error-Codes #1415
* breaking change: authentication API command changed #1415
* breaking change: limit has to be send als `float`, `0.0 .. 100.0` #1415
* updated documentation #1415
* fix don't send control command twice #1426
## 0.8.79 - 2024-02-11
* fix `opendtufusion` build (started only once USB-console was connected)
* code quality improvments
## 0.8.78 - 2024-02-10
* finalized API token access #1415
* possible fix of MqTT fix "total values are sent to often" #1421
* removed `switchCycle` from `hmsRadio.h` #1412
* merge PR: Add hint to INV_RESET_MIDNIGHT resp. INV_PAUSE_DURING_NIGHT #1418
* merge PR: simplify rxOffset logic #1417
* code quality improvments
## 0.8.77 - 2024-02-08
* merge PR: BugFix: ACK #1414
* fix suspicious if condition #1416
* prepared API token for access, not functional #1415
## 0.8.76 - 2024-02-07
* revert changes from yesterday regarding snprintf and its size #1410, #1411
* reduced cppcheck linter warnings significantly
* try to improve ePaper (ghosting) #1107
## 0.8.75 - 2024-02-06
* fix active power control value #1406, #1409
* update Mqtt lib to version `1.6.0`
* take care of null terminator of chars #1410, #1411
## 0.8.74 - 2024-02-05
* reduced cppcheck linter warnings significantly
## 0.8.73 - 2024-02-03
* fix nullpointer during communication #1401
* added `max_power` to MqTT total values #1375
## 0.8.72 - 2024-02-03
* fixed translation #1403
* fixed sending commands to inverters which are soft turned off #1397
* reduce switchChannel command for HMS (only each 5th cycle it will be send now)
## 0.8.71 - 2024-02-03
* fix heuristics reset
* fix CMT missing frames problem
* removed inverter gap setting
* removed add to total (MqTT) inverter setting
* fixed sending commands to inverters which are soft turned off
* save settings before they are exported #1395
* fix autologin bug if no password is set
* translated `/serial`
* removed "yield day" history
## 0.8.70 - 2024-02-01
* prevent sending commands to inverter which isn't active #1387
* protect commands from popup in `/live` if password is set #1199
## 0.8.69 - 2024-01-31
* merge PR: Dynamic retries, pendular first rx chan #1394
## 0.8.68 - 2024-01-29
* fix HMS / HMT startup
* added `flush_rx` to NRF on TX
* start with heuristics set to `0`
* added warning for WiFi channel 12-14 (ESP8266 only) #1381
## 0.8.67 - 2024-01-29
* fix HMS frequency
* fix display of inverter id in serial log (was displayed twice)
## 0.8.66 - 2024-01-28
* added support for other regions - untested #1271
* fix generation of DTU-ID; was computed twice without reset if two radios are enabled
## 0.8.65 - 2024-01-24
* removed patch for NRF `PLOS`
* fix lang issues #1388
* fix build on Windows of `opendtufusion` environments (git: trailing whitespaces)
## 0.8.64 - 2024-01-22 ## 0.8.64 - 2024-01-22
* add `ARC` to log (NRF24 Debug) * add `ARC` to log (NRF24 Debug)
* merge PR: ETH NTP update bugfix #1385 * merge PR: ETH NTP update bugfix #1385
@ -148,7 +270,7 @@
## 0.8.39 - 2024-01-01 ## 0.8.39 - 2024-01-01
* fix MqTT dis_night_comm in the morning #1309 #1286 * fix MqTT dis_night_comm in the morning #1309 #1286
* seperated offset for sunrise and sunset #1308 * separated offset for sunrise and sunset #1308
* powerlimit (active power control) now has one decimal place (MqTT / API) #1199 * powerlimit (active power control) now has one decimal place (MqTT / API) #1199
* merge Prometheus metrics fix #1310 * merge Prometheus metrics fix #1310
* merge MI grid profile request #1306 * merge MI grid profile request #1306
@ -160,7 +282,7 @@
## 0.8.37 - 2023-12-30 ## 0.8.37 - 2023-12-30
* added grid profiles * added grid profiles
* format version of grid profile * format version of grid profile
# RELEASE 0.8.36 - 2023-12-30 # RELEASE 0.8.36 - 2023-12-30
@ -361,7 +483,7 @@
## 0.7.61 - 2023-10-01 ## 0.7.61 - 2023-10-01
* merged `hmPayload` and `hmsPayload` into single class * merged `hmPayload` and `hmsPayload` into single class
* merged generic radio functions into new parent class `radio.h` * merged generic radio functions into new parent class `radio.h`
* moved radio statistics into the inverter - each inverter has now seperate statistics which can be accessed by click on the footer in `/live` * moved radio statistics into the inverter - each inverter has now separate statistics which can be accessed by click on the footer in `/live`
* fix compiler warnings #1191 * fix compiler warnings #1191
* fix ePaper logo during night time #1151 * fix ePaper logo during night time #1151

39
src/app.cpp

@ -13,7 +13,10 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
app::app() : ah::Scheduler {} {} app::app() : ah::Scheduler {} {
memset(mVersion, 0, sizeof(char) * 12);
memset(mVersionModules, 0, sizeof(char) * 12);
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -47,7 +50,7 @@ void app::setup() {
} }
#if defined(ESP32) #if defined(ESP32)
if(mConfig->cmt.enabled) { 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); 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 #endif
#ifdef ETHERNET #ifdef ETHERNET
@ -64,7 +67,7 @@ void app::setup() {
esp_task_wdt_reset(); esp_task_wdt_reset();
mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs); 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)); mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); }); mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); });
@ -97,9 +100,8 @@ void app::setup() {
esp_task_wdt_reset(); esp_task_wdt_reset();
mWeb.setup(this, &mSys, mConfig); mWeb.setup(this, &mSys, mConfig);
mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig); mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig);
mProtection = Protection::getInstance(mConfig->sys.adminPwd);
#ifdef ENABLE_SYSLOG #ifdef ENABLE_SYSLOG
mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback) mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback)
@ -182,6 +184,8 @@ void app::onNetwork(bool gotIp) {
void app::regularTickers(void) { void app::regularTickers(void) {
DPRINTLN(DBG_DEBUG, F("regularTickers")); DPRINTLN(DBG_DEBUG, F("regularTickers"));
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
everySec([this]() { mProtection->tickSecond(); }, "prot");
// Plugins // Plugins
#if defined(PLUGIN_DISPLAY) #if defined(PLUGIN_DISPLAY)
if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type) if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
@ -227,7 +231,6 @@ void app::updateNtp(void) {
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
if (mConfig->sys.schedReboot) { if (mConfig->sys.schedReboot) {
uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght
if (rebootTrig <= mTimestamp) { //necessary for times other than midnight to prevent reboot loop if (rebootTrig <= mTimestamp) { //necessary for times other than midnight to prevent reboot loop
rebootTrig += 86400; rebootTrig += 86400;
@ -236,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; mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
tickCalcSunrise(); tickCalcSunrise();
} }
@ -261,11 +264,10 @@ void app::tickNtpUpdate(void) {
#endif #endif
if (isOK) { if (isOK) {
this->updateNtp(); this->updateNtp();
nxtTrig = mConfig->ntp.interval * 60; // check again in 12h
nxtTrig = isOK ? (mConfig->ntp.interval * 60) : 60; // depending on NTP update success check again in 12h (depends on setting) or in 1 min
// immediately start communicating // immediately start communicating
if (isOK && mSendFirst) { if (mSendFirst) {
mSendFirst = false; mSendFirst = false;
once(std::bind(&app::tickSend, this), 1, "senOn"); once(std::bind(&app::tickSend, this), 1, "senOn");
} }
@ -300,9 +302,8 @@ void app::tickIVCommunication(void) {
bool zeroValues = false; bool zeroValues = false;
uint32_t nxtTrig = 0; uint32_t nxtTrig = 0;
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys.getInverterByPos(i); Inverter<> *iv = mSys.getInverterByPos(i);
if(NULL == iv) if(NULL == iv)
continue; continue;
@ -389,10 +390,9 @@ void app::tickMidnight(void) {
// clear max values // clear max values
if(mConfig->inst.rstMaxValsMidNight) { if(mConfig->inst.rstMaxValsMidNight) {
uint8_t pos;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t i = 0; i <= iv->channels; i++) { 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); iv->setValue(pos, rec, 0.0f);
} }
} }
@ -466,14 +466,13 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
continue; // skip to next inverter continue; // skip to next inverter
if (!iv->config->enabled) if (!iv->config->enabled)
continue; // skip to next inverter continue; // skip to next inverter
if (iv->commEnabled)
continue; // skip to next inverter
if (checkAvail) { if (checkAvail) {
if (!iv->isAvailable()) if (iv->isAvailable())
continue; continue;
} }
changed = true;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t ch = 0; ch <= iv->channels; ch++) { for(uint8_t ch = 0; ch <= iv->channels; ch++) {
uint8_t pos = 0; uint8_t pos = 0;
@ -495,10 +494,9 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
pos = iv->getPosByChFld(ch, FLD_MP, rec); pos = iv->getPosByChFld(ch, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f); iv->setValue(pos, rec, 0.0f);
} }
iv->resetAlarms();
iv->doCalculations(); iv->doCalculations();
} }
changed = true;
} }
if(changed) if(changed)
@ -591,9 +589,8 @@ void app::updateLed(void) {
uint8_t led_on = (mConfig->led.high_active) ? (mConfig->led.luminance) : (255-mConfig->led.luminance); uint8_t led_on = (mConfig->led.high_active) ? (mConfig->led.luminance) : (255-mConfig->led.luminance);
if (mConfig->led.led[0] != DEF_PIN_OFF) { if (mConfig->led.led[0] != DEF_PIN_OFF) {
Inverter<> *iv;
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id); Inverter<> *iv = mSys.getInverterByPos(id);
if (NULL != iv) { if (NULL != iv) {
if (iv->isProducing()) { if (iv->isProducing()) {
// turn on when at least one inverter is producing // turn on when at least one inverter is producing

118
src/app.h

@ -30,6 +30,7 @@
#include "utils/scheduler.h" #include "utils/scheduler.h"
#include "utils/syslog.h" #include "utils/syslog.h"
#include "web/RestApi.h" #include "web/RestApi.h"
#include "web/Protection.h"
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
#include "plugins/history.h" #include "plugins/history.h"
#endif /*ENABLE_HISTORY*/ #endif /*ENABLE_HISTORY*/
@ -89,7 +90,7 @@ class app : public IApp, public ah::Scheduler {
void handleIntr(void) { void handleIntr(void) {
mNrfRadio.handleIntr(); mNrfRadio.handleIntr();
} }
void* getRadioObj(bool nrf) { void* getRadioObj(bool nrf) override {
if(nrf) if(nrf)
return (void*)&mNrfRadio; return (void*)&mNrfRadio;
else { else {
@ -107,19 +108,19 @@ class app : public IApp, public ah::Scheduler {
} }
#endif #endif
uint32_t getUptime() { uint32_t getUptime() override {
return Scheduler::getUptime(); return Scheduler::getUptime();
} }
uint32_t getTimestamp() { uint32_t getTimestamp() override {
return Scheduler::mTimestamp; return Scheduler::mTimestamp;
} }
uint64_t getTimestampMs() { uint64_t getTimestampMs() override {
return ((uint64_t)Scheduler::mTimestamp * 1000) + ((uint64_t)millis() - (uint64_t)Scheduler::mTsMillis) % 1000; 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 mShowRebootRequest = true; // only message on index, no reboot
mSavePending = true; mSavePending = true;
mSaveReboot = reboot; mSaveReboot = reboot;
@ -130,7 +131,7 @@ class app : public IApp, public ah::Scheduler {
return true; return true;
} }
void initInverter(uint8_t id) { void initInverter(uint8_t id) override {
mSys.addInverter(id, [this](Inverter<> *iv) { mSys.addInverter(id, [this](Inverter<> *iv) {
if((IV_MI == iv->ivGen) || (IV_HM == iv->ivGen)) if((IV_MI == iv->ivGen) || (IV_HM == iv->ivGen))
iv->radio = &mNrfRadio; iv->radio = &mNrfRadio;
@ -141,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); return mSettings.readSettings(path);
} }
@ -149,76 +150,80 @@ class app : public IApp, public ah::Scheduler {
return mSettings.eraseSettings(eraseWifi); return mSettings.eraseSettings(eraseWifi);
} }
bool getSavePending() { bool getSavePending() override {
return mSavePending; return mSavePending;
} }
bool getLastSaveSucceed() { bool getLastSaveSucceed() override {
return mSettings.getLastSaveSucceed(); return mSettings.getLastSaveSucceed();
} }
bool getShouldReboot() { bool getShouldReboot() override {
return mSaveReboot; return mSaveReboot;
} }
#if !defined(ETHERNET) #if !defined(ETHERNET)
void scanAvailNetworks() { void scanAvailNetworks() override {
mWifi.scanAvailNetworks(); mWifi.scanAvailNetworks();
} }
bool getAvailNetworks(JsonObject obj) { bool getAvailNetworks(JsonObject obj) override {
return mWifi.getAvailNetworks(obj); return mWifi.getAvailNetworks(obj);
} }
void setupStation(void) { void setupStation(void) override {
mWifi.setupStation(); mWifi.setupStation();
} }
void setStopApAllowedMode(bool allowed) { void setStopApAllowedMode(bool allowed) override {
mWifi.setStopApAllowedMode(allowed); mWifi.setStopApAllowedMode(allowed);
} }
String getStationIp(void) { String getStationIp(void) override {
return mWifi.getStationIp(); return mWifi.getStationIp();
} }
bool getWasInCh12to14(void) const override {
return mWifi.getWasInCh12to14();
}
#endif /* !defined(ETHERNET) */ #endif /* !defined(ETHERNET) */
void setRebootFlag() { void setRebootFlag() override {
once(std::bind(&app::tickReboot, this), 3, "rboot"); once(std::bind(&app::tickReboot, this), 3, "rboot");
} }
const char *getVersion() { const char *getVersion() override {
return mVersion; return mVersion;
} }
const char *getVersionModules() { const char *getVersionModules() override {
return mVersionModules; return mVersionModules;
} }
uint32_t getSunrise() { uint32_t getSunrise() override {
return mSunrise; return mSunrise;
} }
uint32_t getSunset() { uint32_t getSunset() override {
return mSunset; return mSunset;
} }
bool getSettingsValid() { bool getSettingsValid() override {
return mSettings.getValid(); return mSettings.getValid();
} }
bool getRebootRequestState() { bool getRebootRequestState() override {
return mShowRebootRequest; return mShowRebootRequest;
} }
void setMqttDiscoveryFlag() { void setMqttDiscoveryFlag() override {
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf"); once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf");
#endif #endif
} }
bool getMqttIsConnected() { bool getMqttIsConnected() override {
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
return mMqtt.isConnected(); return mMqtt.isConnected();
#else #else
@ -226,7 +231,7 @@ class app : public IApp, public ah::Scheduler {
#endif #endif
} }
uint32_t getMqttTxCnt() { uint32_t getMqttTxCnt() override {
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
return mMqtt.getTxCnt(); return mMqtt.getTxCnt();
#else #else
@ -234,7 +239,7 @@ class app : public IApp, public ah::Scheduler {
#endif #endif
} }
uint32_t getMqttRxCnt() { uint32_t getMqttRxCnt() override {
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
return mMqtt.getRxCnt(); return mMqtt.getRxCnt();
#else #else
@ -242,15 +247,27 @@ class app : public IApp, public ah::Scheduler {
#endif #endif
} }
bool getProtection(AsyncWebServerRequest *request) { void lock(bool fromWeb) override {
return mWeb.isProtected(request); mProtection->lock(fromWeb);
}
char *unlock(const char *clientIp, bool loginFromWeb) override {
return mProtection->unlock(clientIp, loginFromWeb);
}
void resetLockTimeout(void) override {
mProtection->resetLockTimeout();
}
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; return mConfig->nrf.enabled;
} }
bool getCmtEnabled(void) { bool getCmtEnabled(void) override {
return mConfig->cmt.enabled; return mConfig->cmt.enabled;
} }
@ -262,19 +279,19 @@ class app : public IApp, public ah::Scheduler {
return mConfig->cmt.pinIrq; return mConfig->cmt.pinIrq;
} }
uint32_t getTimezoneOffset() { uint32_t getTimezoneOffset() override {
return mApi.getTimezoneOffset(); return mApi.getTimezoneOffset();
} }
void getSchedulerInfo(uint8_t *max) { void getSchedulerInfo(uint8_t *max) override {
getStat(max); getStat(max);
} }
void getSchedulerNames(void) { void getSchedulerNames(void) override {
printSchedulers(); printSchedulers();
} }
void setTimestamp(uint32_t newTime) { void setTimestamp(uint32_t newTime) override {
DPRINT(DBG_DEBUG, F("setTimestamp: ")); DPRINT(DBG_DEBUG, F("setTimestamp: "));
DBGPRINTLN(String(newTime)); DBGPRINTLN(String(newTime));
if(0 == newTime) if(0 == newTime)
@ -289,7 +306,7 @@ class app : public IApp, public ah::Scheduler {
Scheduler::setTimestamp(newTime); Scheduler::setTimestamp(newTime);
} }
uint16_t getHistoryValue(uint8_t type, uint16_t i) { uint16_t getHistoryValue(uint8_t type, uint16_t i) override {
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
return mHistory.valueAt((HistoryStorageType)type, i); return mHistory.valueAt((HistoryStorageType)type, i);
#else #else
@ -297,7 +314,7 @@ class app : public IApp, public ah::Scheduler {
#endif #endif
} }
uint16_t getHistoryMaxDay() { uint16_t getHistoryMaxDay() override {
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
return mHistory.getMaximumDay(); return mHistory.getMaximumDay();
#else #else
@ -351,11 +368,11 @@ class app : public IApp, public ah::Scheduler {
void tickNtpUpdate(void); void tickNtpUpdate(void);
#if defined(ETHERNET) #if defined(ETHERNET)
void onNtpUpdate(bool gotTime); void onNtpUpdate(bool gotTime);
bool mNtpReceived; bool mNtpReceived = false;
#endif /* defined(ETHERNET) */ #endif /* defined(ETHERNET) */
void updateNtp(void); void updateNtp(void);
void triggerTickSend() { void triggerTickSend() override {
once(std::bind(&app::tickSend, this), 0, "tSend"); once(std::bind(&app::tickSend, this), 0, "tSend");
} }
@ -374,7 +391,7 @@ class app : public IApp, public ah::Scheduler {
HmRadio<> mNrfRadio; HmRadio<> mNrfRadio;
Communication mCommunication; Communication mCommunication;
bool mShowRebootRequest; bool mShowRebootRequest = false;
#if defined(ETHERNET) #if defined(ETHERNET)
ahoyeth mEth; ahoyeth mEth;
@ -383,6 +400,7 @@ class app : public IApp, public ah::Scheduler {
#endif /* defined(ETHERNET) */ #endif /* defined(ETHERNET) */
WebType mWeb; WebType mWeb;
RestApiType mApi; RestApiType mApi;
Protection *mProtection = nullptr;
#ifdef ENABLE_SYSLOG #ifdef ENABLE_SYSLOG
DbgSyslog mDbgSyslog; DbgSyslog mDbgSyslog;
#endif #endif
@ -399,26 +417,26 @@ class app : public IApp, public ah::Scheduler {
char mVersion[12]; char mVersion[12];
char mVersionModules[12]; char mVersionModules[12];
settings mSettings; settings mSettings;
settings_t *mConfig; settings_t *mConfig = nullptr;
bool mSavePending; bool mSavePending = false;
bool mSaveReboot; bool mSaveReboot = false;
uint8_t mSendLastIvId; uint8_t mSendLastIvId = 0;
bool mSendFirst; bool mSendFirst = false;
bool mAllIvNotAvail; bool mAllIvNotAvail = false;
bool mNetworkConnected; bool mNetworkConnected = false;
// mqtt // mqtt
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
PubMqttType mMqtt; PubMqttType mMqtt;
#endif /*ENABLE_MQTT*/ #endif /*ENABLE_MQTT*/
bool mMqttReconnect; bool mMqttReconnect = false;
bool mMqttEnabled; bool mMqttEnabled = false;
// sun // sun
int32_t mCalculatedTimezoneOffset; int32_t mCalculatedTimezoneOffset = 0;
uint32_t mSunrise, mSunset; uint32_t mSunrise = 0, mSunset = 0;
// plugins // plugins
#if defined(PLUGIN_DISPLAY) #if defined(PLUGIN_DISPLAY)

8
src/appInterface.h

@ -14,7 +14,7 @@
class IApp { class IApp {
public: public:
virtual ~IApp() {} virtual ~IApp() {}
virtual bool saveSettings(bool stopFs) = 0; virtual bool saveSettings(bool reboot) = 0;
virtual void initInverter(uint8_t id) = 0; virtual void initInverter(uint8_t id) = 0;
virtual bool readSettings(const char *path) = 0; virtual bool readSettings(const char *path) = 0;
virtual bool eraseSettings(bool eraseWifi) = 0; virtual bool eraseSettings(bool eraseWifi) = 0;
@ -31,6 +31,7 @@ class IApp {
virtual void setupStation(void) = 0; virtual void setupStation(void) = 0;
virtual void setStopApAllowedMode(bool allowed) = 0; virtual void setStopApAllowedMode(bool allowed) = 0;
virtual String getStationIp(void) = 0; virtual String getStationIp(void) = 0;
virtual bool getWasInCh12to14(void) const = 0;
#endif /* defined(ETHERNET) */ #endif /* defined(ETHERNET) */
virtual uint32_t getUptime() = 0; virtual uint32_t getUptime() = 0;
@ -56,7 +57,10 @@ class IApp {
virtual uint32_t getMqttRxCnt() = 0; virtual uint32_t getMqttRxCnt() = 0;
virtual uint32_t getMqttTxCnt() = 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 uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0; virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0;
virtual uint16_t getHistoryMaxDay() = 0; virtual uint16_t getHistoryMaxDay() = 0;

2
src/config/config.h

@ -215,7 +215,7 @@
#define INVERTER_OFF_THRES_SEC 15*60 #define INVERTER_OFF_THRES_SEC 15*60
// threshold of minimum power on which the inverter is marked as inactive // threshold of minimum power on which the inverter is marked as inactive
#define INACT_PWR_THRESH 1 #define INACT_PWR_THRESH 0
// Timezone // Timezone
#define TIMEZONE 1 #define TIMEZONE 1

57
src/config/settings.h

@ -13,6 +13,7 @@
#include <Arduino.h> #include <Arduino.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <algorithm>
#include <LittleFS.h> #include <LittleFS.h>
#include "../defines.h" #include "../defines.h"
@ -30,7 +31,7 @@
* https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout
* */ * */
#define CONFIG_VERSION 9 #define CONFIG_VERSION 11
#define PROT_MASK_INDEX 0x0001 #define PROT_MASK_INDEX 0x0001
@ -68,6 +69,8 @@ typedef struct {
uint16_t protectionMask; uint16_t protectionMask;
bool darkMode; bool darkMode;
bool schedReboot; bool schedReboot;
uint8_t region;
int8_t timezone;
#if !defined(ETHERNET) #if !defined(ETHERNET)
// wifi // wifi
@ -117,6 +120,7 @@ typedef struct {
bool debug; bool debug;
bool privacyLog; bool privacyLog;
bool printWholeTrace; bool printWholeTrace;
bool log2mqtt;
} cfgSerial_t; } cfgSerial_t;
typedef struct { typedef struct {
@ -145,11 +149,10 @@ typedef struct {
uint8_t frequency; uint8_t frequency;
uint8_t powerLevel; uint8_t powerLevel;
bool disNightCom; // disable night communication bool disNightCom; // disable night communication
bool add2Total; // add values to total values - useful if one inverter is on battery to turn off
} cfgIv_t; } cfgIv_t;
typedef struct { typedef struct {
bool enabled; // bool enabled;
cfgIv_t iv[MAX_NUM_INVERTERS]; cfgIv_t iv[MAX_NUM_INVERTERS];
uint16_t sendInterval; uint16_t sendInterval;
@ -158,8 +161,6 @@ typedef struct {
bool rstValsCommStop; bool rstValsCommStop;
bool rstMaxValsMidNight; bool rstMaxValsMidNight;
bool startWithoutTime; bool startWithoutTime;
float yieldEffiency;
uint16_t gapMs;
bool readGrid; bool readGrid;
} cfgInst_t; } cfgInst_t;
@ -206,7 +207,7 @@ typedef struct {
class settings { class settings {
public: public:
settings() { settings() {
mLastSaveSucceed = false; std::fill(reinterpret_cast<char*>(&mCfg), reinterpret_cast<char*>(&mCfg) + sizeof(mCfg), 0);
} }
void setup() { void setup() {
@ -377,7 +378,7 @@ class settings {
memcpy(&tmp, &mCfg.sys, sizeof(cfgSys_t)); memcpy(&tmp, &mCfg.sys, sizeof(cfgSys_t));
} }
// erase all settings and reset to default // 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 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_HISTORY; | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY;
mCfg.sys.darkMode = false; mCfg.sys.darkMode = false;
@ -395,6 +396,8 @@ class settings {
#endif /* !defined(ETHERNET) */ #endif /* !defined(ETHERNET) */
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME);
mCfg.sys.region = 0; // Europe
mCfg.sys.timezone = 1;
mCfg.nrf.pinCs = DEF_NRF_CS_PIN; mCfg.nrf.pinCs = DEF_NRF_CS_PIN;
mCfg.nrf.pinCe = DEF_NRF_CE_PIN; mCfg.nrf.pinCe = DEF_NRF_CE_PIN;
@ -433,6 +436,7 @@ class settings {
mCfg.serial.debug = false; mCfg.serial.debug = false;
mCfg.serial.privacyLog = true; mCfg.serial.privacyLog = true;
mCfg.serial.printWholeTrace = false; mCfg.serial.printWholeTrace = false;
mCfg.serial.log2mqtt = false;
mCfg.mqtt.port = DEF_MQTT_PORT; mCfg.mqtt.port = DEF_MQTT_PORT;
snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER); snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER);
@ -447,15 +451,12 @@ class settings {
mCfg.inst.rstValsCommStop = false; mCfg.inst.rstValsCommStop = false;
mCfg.inst.startWithoutTime = false; mCfg.inst.startWithoutTime = false;
mCfg.inst.rstMaxValsMidNight = false; mCfg.inst.rstMaxValsMidNight = false;
mCfg.inst.yieldEffiency = 1.0f;
mCfg.inst.gapMs = 1;
mCfg.inst.readGrid = true; mCfg.inst.readGrid = true;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value
mCfg.inst.iv[i].frequency = 0x12; // 863MHz (minimum allowed frequency) mCfg.inst.iv[i].frequency = 0x12; // 863MHz (minimum allowed frequency)
mCfg.inst.iv[i].disNightCom = false; mCfg.inst.iv[i].disNightCom = false;
mCfg.inst.iv[i].add2Total = true;
} }
mCfg.led.led[0] = DEF_LED0; mCfg.led.led[0] = DEF_LED0;
@ -487,20 +488,15 @@ class settings {
} }
if(mCfg.configVersion < 2) { if(mCfg.configVersion < 2) {
mCfg.inst.iv[i].disNightCom = false; mCfg.inst.iv[i].disNightCom = false;
mCfg.inst.iv[i].add2Total = true;
} }
if(mCfg.configVersion < 3) { if(mCfg.configVersion < 3) {
mCfg.serial.printWholeTrace = false; mCfg.serial.printWholeTrace = false;
} }
if(mCfg.configVersion < 4) {
mCfg.inst.gapMs = 500;
}
if(mCfg.configVersion < 5) { if(mCfg.configVersion < 5) {
mCfg.inst.sendInterval = SEND_INTERVAL; mCfg.inst.sendInterval = SEND_INTERVAL;
mCfg.serial.printWholeTrace = false; mCfg.serial.printWholeTrace = false;
} }
if(mCfg.configVersion < 6) { if(mCfg.configVersion < 6) {
mCfg.inst.gapMs = 500;
mCfg.inst.readGrid = true; mCfg.inst.readGrid = true;
} }
if(mCfg.configVersion < 7) { if(mCfg.configVersion < 7) {
@ -509,8 +505,12 @@ class settings {
if(mCfg.configVersion < 8) { if(mCfg.configVersion < 8) {
mCfg.sun.offsetSecEvening = mCfg.sun.offsetSecMorning; mCfg.sun.offsetSecEvening = mCfg.sun.offsetSecMorning;
} }
if(mCfg.configVersion < 9) { if(mCfg.configVersion < 10) {
mCfg.inst.gapMs = 1; mCfg.sys.region = 0; // Europe
mCfg.sys.timezone = 1;
}
if(mCfg.configVersion < 11) {
mCfg.serial.log2mqtt = false;
} }
} }
} }
@ -537,6 +537,8 @@ class settings {
obj[F("prot_mask")] = mCfg.sys.protectionMask; obj[F("prot_mask")] = mCfg.sys.protectionMask;
obj[F("dark")] = mCfg.sys.darkMode; obj[F("dark")] = mCfg.sys.darkMode;
obj[F("reb")] = (bool) mCfg.sys.schedReboot; 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.ip, buf); obj[F("ip")] = String(buf);
ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = 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); ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
@ -554,6 +556,8 @@ class settings {
getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask); getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask);
getVal<bool>(obj, F("dark"), &mCfg.sys.darkMode); getVal<bool>(obj, F("dark"), &mCfg.sys.darkMode);
getVal<bool>(obj, F("reb"), &mCfg.sys.schedReboot); 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("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("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*>()); if(obj.containsKey(F("dns1"))) ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>());
@ -657,11 +661,13 @@ class settings {
obj[F("debug")] = mCfg.serial.debug; obj[F("debug")] = mCfg.serial.debug;
obj[F("prv")] = (bool) mCfg.serial.privacyLog; obj[F("prv")] = (bool) mCfg.serial.privacyLog;
obj[F("trc")] = (bool) mCfg.serial.printWholeTrace; obj[F("trc")] = (bool) mCfg.serial.printWholeTrace;
obj[F("mqtt")] = (bool) mCfg.serial.log2mqtt;
} else { } else {
getVal<bool>(obj, F("show"), &mCfg.serial.showIv); getVal<bool>(obj, F("show"), &mCfg.serial.showIv);
getVal<bool>(obj, F("debug"), &mCfg.serial.debug); getVal<bool>(obj, F("debug"), &mCfg.serial.debug);
getVal<bool>(obj, F("prv"), &mCfg.serial.privacyLog); getVal<bool>(obj, F("prv"), &mCfg.serial.privacyLog);
getVal<bool>(obj, F("trc"), &mCfg.serial.printWholeTrace); getVal<bool>(obj, F("trc"), &mCfg.serial.printWholeTrace);
getVal<bool>(obj, F("mqtt"), &mCfg.serial.log2mqtt);
} }
} }
@ -749,32 +755,23 @@ class settings {
void jsonInst(JsonObject obj, bool set = false) { void jsonInst(JsonObject obj, bool set = false) {
if(set) { if(set) {
obj[F("intvl")] = mCfg.inst.sendInterval; obj[F("intvl")] = mCfg.inst.sendInterval;
obj[F("en")] = (bool)mCfg.inst.enabled; // obj[F("en")] = (bool)mCfg.inst.enabled;
obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight;
obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop;
obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime; obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime;
obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstMaxValsMidNight; obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstMaxValsMidNight;
obj[F("yldEff")] = mCfg.inst.yieldEffiency;
obj[F("gap")] = mCfg.inst.gapMs;
obj[F("rdGrid")] = (bool)mCfg.inst.readGrid; obj[F("rdGrid")] = (bool)mCfg.inst.readGrid;
} }
else { else {
getVal<uint16_t>(obj, F("intvl"), &mCfg.inst.sendInterval); getVal<uint16_t>(obj, F("intvl"), &mCfg.inst.sendInterval);
getVal<bool>(obj, F("en"), &mCfg.inst.enabled); // getVal<bool>(obj, F("en"), &mCfg.inst.enabled);
getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight); getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight);
getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail); getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail);
getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop); getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop);
getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime); getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime);
getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstMaxValsMidNight); getVal<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); getVal<bool>(obj, F("rdGrid"), &mCfg.inst.readGrid);
if(mCfg.inst.yieldEffiency < 0.5)
mCfg.inst.yieldEffiency = 1.0f;
else if(mCfg.inst.yieldEffiency > 1.0f)
mCfg.inst.yieldEffiency = 1.0f;
} }
JsonArray ivArr; JsonArray ivArr;
@ -797,7 +794,6 @@ class settings {
obj[F("freq")] = cfg->frequency; obj[F("freq")] = cfg->frequency;
obj[F("pa")] = cfg->powerLevel; obj[F("pa")] = cfg->powerLevel;
obj[F("dis")] = cfg->disNightCom; obj[F("dis")] = cfg->disNightCom;
obj[F("add")] = cfg->add2Total;
for(uint8_t i = 0; i < 6; i++) { for(uint8_t i = 0; i < 6; i++) {
obj[F("yield")][i] = cfg->yieldCor[i]; obj[F("yield")][i] = cfg->yieldCor[i];
obj[F("pwr")][i] = cfg->chMaxPwr[i]; obj[F("pwr")][i] = cfg->chMaxPwr[i];
@ -810,7 +806,6 @@ class settings {
getVal<uint8_t>(obj, F("freq"), &cfg->frequency); getVal<uint8_t>(obj, F("freq"), &cfg->frequency);
getVal<uint8_t>(obj, F("pa"), &cfg->powerLevel); getVal<uint8_t>(obj, F("pa"), &cfg->powerLevel);
getVal<bool>(obj, F("dis"), &cfg->disNightCom); getVal<bool>(obj, F("dis"), &cfg->disNightCom);
getVal<bool>(obj, F("add"), &cfg->add2Total);
uint8_t size = 4; uint8_t size = 4;
if(obj.containsKey(F("pwr"))) if(obj.containsKey(F("pwr")))
size = obj[F("pwr")].size(); size = obj[F("pwr")].size();
@ -851,7 +846,7 @@ class settings {
#endif #endif
settings_t mCfg; settings_t mCfg;
bool mLastSaveSucceed; bool mLastSaveSucceed = 0;
}; };
#endif /*__SETTINGS_H__*/ #endif /*__SETTINGS_H__*/

6
src/defines.h

@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 8 #define VERSION_MINOR 8
#define VERSION_PATCH 64 #define VERSION_PATCH 89
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {
@ -22,8 +22,6 @@ typedef struct {
int8_t rssi; int8_t rssi;
uint8_t packet[MAX_RF_PAYLOAD_SIZE]; uint8_t packet[MAX_RF_PAYLOAD_SIZE];
uint16_t millis; uint16_t millis;
uint8_t arc;
uint8_t plos;
} packet_t; } packet_t;
typedef enum { typedef enum {
@ -111,7 +109,7 @@ enum {
typedef struct { typedef struct {
uint32_t rxFail; uint32_t rxFail;
uint32_t rxFailNoAnser; uint32_t rxFailNoAnswer;
uint32_t rxSuccess; uint32_t rxSuccess;
uint32_t frmCnt; uint32_t frmCnt;
uint32_t txCnt; uint32_t txCnt;

2
src/eth/ahoyeth.h

@ -49,7 +49,7 @@ class ahoyeth {
#if defined(CONFIG_IDF_TARGET_ESP32S3) #if defined(CONFIG_IDF_TARGET_ESP32S3)
EthSpi mEthSpi; EthSpi mEthSpi;
#endif #endif
settings_t *mConfig = NULL; settings_t *mConfig = nullptr;
uint32_t *mUtcTimestamp; uint32_t *mUtcTimestamp;
AsyncUDP mUdp; // for time server AsyncUDP mUdp; // for time server

12
src/hm/CommQueue.h

@ -12,14 +12,12 @@
#include "../utils/dbg.h" #include "../utils/dbg.h"
#define DEFAULT_ATTEMPS 5 #define DEFAULT_ATTEMPS 5
#define MORE_ATTEMPS_ALARMDATA 8 #define MORE_ATTEMPS_ALARMDATA 3 // 8
#define MORE_ATTEMPS_GRIDONPROFILEPARA 5 #define MORE_ATTEMPS_GRIDONPROFILEPARA 0 // 5
template <uint8_t N=100> template <uint8_t N=100>
class CommQueue { class CommQueue {
public: public:
CommQueue() {}
void addImportant(Inverter<> *iv, uint8_t cmd) { void addImportant(Inverter<> *iv, uint8_t cmd) {
dec(&mRdPtr); dec(&mRdPtr);
mQueue[mRdPtr] = queue_s(iv, cmd, true); mQueue[mRdPtr] = queue_s(iv, cmd, true);
@ -34,12 +32,12 @@ class CommQueue {
mQueue[mWrPtr] = queue_s(iv, cmd, false); 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)); //DPRINTLN(DBG_INFO, "wr: " + String(mWrPtr) + ", rd: " + String(mRdPtr));
return abs(mRdPtr - mWrPtr); return abs(mRdPtr - mWrPtr);
} }
uint8_t getMaxFill(void) { uint8_t getMaxFill(void) const {
return N; return N;
} }
@ -93,7 +91,7 @@ class CommQueue {
inc(&mRdPtr); inc(&mRdPtr);
} }
void setTs(uint32_t *ts) { void setTs(const uint32_t *ts) {
mQueue[mRdPtr].ts = *ts; mQueue[mRdPtr].ts = *ts;
} }

353
src/hm/Communication.h

@ -6,13 +6,14 @@
#ifndef __COMMUNICATION_H__ #ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__ #define __COMMUNICATION_H__
#include <array>
#include "CommQueue.h" #include "CommQueue.h"
#include <Arduino.h> #include <Arduino.h>
#include "../utils/crc.h" #include "../utils/crc.h"
#include "../utils/timemonitor.h" #include "../utils/timemonitor.h"
#include "Heuristic.h" #include "Heuristic.h"
#define MAX_BUFFER 250 #define MAX_BUFFER 200
typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType; typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType;
typedef std::function<void(Inverter<> *)> powerLimitAckListenerType; typedef std::function<void(Inverter<> *)> powerLimitAckListenerType;
@ -20,12 +21,11 @@ typedef std::function<void(Inverter<> *)> alarmListenerType;
class Communication : public CommQueue<> { class Communication : public CommQueue<> {
public: public:
void setup(uint32_t *timestamp, bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint16_t *inverterGap) { void setup(uint32_t *timestamp, bool *serialDebug, bool *privacyMode, bool *printWholeTrace) {
mTimestamp = timestamp; mTimestamp = timestamp;
mPrivacyMode = privacyMode; mPrivacyMode = privacyMode;
mSerialDebug = serialDebug; mSerialDebug = serialDebug;
mPrintWholeTrace = printWholeTrace; mPrintWholeTrace = printWholeTrace;
mInverterGap = inverterGap;
} }
void addImportant(Inverter<> *iv, uint8_t cmd) { void addImportant(Inverter<> *iv, uint8_t cmd) {
@ -83,14 +83,17 @@ class Communication : public CommQueue<> {
q->iv->mGotFragment = false; q->iv->mGotFragment = false;
q->iv->mGotLastMsg = false; q->iv->mGotLastMsg = false;
q->iv->curFrmCnt = 0; q->iv->curFrmCnt = 0;
q->iv->radioStatistics.txCnt++;
mIsRetransmit = false; mIsRetransmit = false;
if(NULL == q->iv->radio) if(NULL == q->iv->radio)
cmdDone(false); // can't communicate while radio is not defined! cmdDone(false); // can't communicate while radio is not defined!
mFirstTry = q->iv->isAvailable(); mFirstTry = (INV_RADIO_TYPE_NRF == q->iv->ivRadioType) && (q->iv->isAvailable());
q->iv->mCmd = q->cmd; q->iv->mCmd = q->cmd;
q->iv->mIsSingleframeReq = false; q->iv->mIsSingleframeReq = false;
mFramesExpected = getFramesExpected(q); // function to get expected frame count. mFramesExpected = getFramesExpected(q); // function to get expected frame count.
mTimeout = DURATION_TXFRAME + mFramesExpected*DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]; mTimeout = DURATION_TXFRAME + mFramesExpected*DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType];
if((q->iv->ivGen == IV_MI) && ((q->cmd == MI_REQ_CH1) || (q->cmd == MI_REQ_4CH)))
incrAttempt(q->iv->channels); // 2 more attempts for 2ch, 4 more for 4ch
mState = States::START; mState = States::START;
break; break;
@ -112,13 +115,13 @@ class Communication : public CommQueue<> {
} else } else
q->iv->radio->prepareDevInformCmd(q->iv, q->cmd, q->ts, q->iv->alarmLastId, false); q->iv->radio->prepareDevInformCmd(q->iv, q->cmd, q->ts, q->iv->alarmLastId, false);
q->iv->radioStatistics.txCnt++; //q->iv->radioStatistics.txCnt++;
q->iv->radio->mRadioWaitTime.startTimeMonitor(mTimeout); q->iv->radio->mRadioWaitTime.startTimeMonitor(mTimeout);
if((!mIsRetransmit && (q->cmd == AlarmData)) || (q->cmd == GridOnProFilePara))
incrAttempt((q->cmd == AlarmData)? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA);
mIsRetransmit = false; mIsRetransmit = false;
setAttempt(); setAttempt();
if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara))
incrAttempt(q->cmd == AlarmData? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA);
mState = States::WAIT; mState = States::WAIT;
break; break;
@ -129,37 +132,38 @@ class Communication : public CommQueue<> {
break; break;
case States::CHECK_FRAMES: { case States::CHECK_FRAMES: {
if((q->iv->radio->mBufCtrl.empty() && !mIsRetransmit) || (0 == q->attempts)) { // radio buffer empty or no more answers if((q->iv->radio->mBufCtrl.empty() && !mIsRetransmit) ) { // || (0 == q->attempts)) { // radio buffer empty. No more answers will be checked later
if(*mSerialDebug) { if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id); DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("request timeout: ")); DBGPRINT(F("request timeout: "));
DBGPRINT(String(q->iv->radio->mRadioWaitTime.getRunTime())); DBGPRINT(String(q->iv->radio->mRadioWaitTime.getRunTime()));
DBGPRINT(F("ms")); DBGPRINTLN(F("ms"));
if(INV_RADIO_TYPE_NRF == q->iv->ivRadioType) {
DBGPRINT(F(", ARC "));
DBGPRINT(String(q->iv->radio->getARC()));
DBGPRINT(F(", PLOS "));
DBGPRINTLN(String(q->iv->radio->getPLOS()));
} else
DBGPRINTLN("");
} }
if(!q->iv->mGotFragment) { if(!q->iv->mGotFragment) {
if(INV_RADIO_TYPE_CMT == q->iv->ivRadioType) { if(INV_RADIO_TYPE_CMT == q->iv->ivRadioType) {
q->iv->radio->switchFrequency(q->iv, HOY_BOOT_FREQ_KHZ, (q->iv->config->frequency*FREQ_STEP_KHZ + HOY_BASE_FREQ_KHZ)); #if defined(ESP32)
if(!q->iv->radio->switchFrequency(q->iv, q->iv->radio->getBootFreqMhz() * 1000, (q->iv->config->frequency*FREQ_STEP_KHZ + q->iv->radio->getBaseFreqMhz() * 1000))) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINTLN(F("switch frequency failed!"));
}
mWaitTime.startTimeMonitor(1000); mWaitTime.startTimeMonitor(1000);
#endif
} else { } else {
mHeu.setIvRetriesBad(q->iv);
if(IV_MI == q->iv->ivGen) if(IV_MI == q->iv->ivGen)
q->iv->mIvTxCnt++; q->iv->mIvTxCnt++;
if(mFirstTry) { if(mFirstTry) {
mFirstTry = false; if(q->attempts < 3 || !q->iv->isProducing())
setAttempt(); mFirstTry = false;
mHeu.evalTxChQuality(q->iv, false, 0, 0); mHeu.evalTxChQuality(q->iv, false, 0, 0);
q->iv->radioStatistics.rxFailNoAnser++; mHeu.getTxCh(q->iv);
//q->iv->radioStatistics.rxFailNoAnser++; // should only be one of fail or retransmit.
//q->iv->radioStatistics.txCnt--;
q->iv->radioStatistics.retransmits++; q->iv->radioStatistics.retransmits++;
q->iv->radio->mRadioWaitTime.stopTimeMonitor(); q->iv->radio->mRadioWaitTime.stopTimeMonitor();
mState = States::START; mState = States::START;
return; return;
} }
} }
@ -180,8 +184,11 @@ class Communication : public CommQueue<> {
q->iv->mDtuRxCnt++; q->iv->mDtuRxCnt++;
if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
if(parseFrame(p)) if(parseFrame(p)) {
q->iv->curFrmCnt++; q->iv->curFrmCnt++;
if(!mIsRetransmit && ((p->packet[9] == 0x02) || (p->packet[9] == 0x82)) && (p->millis < LIMIT_FAST_IV))
mHeu.setIvRetriesGood(q->iv,p->millis < LIMIT_VERYFAST_IV);
}
} else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
if(parseDevCtrl(p, q)) if(parseDevCtrl(p, q))
closeRequest(q, true); closeRequest(q, true);
@ -190,8 +197,8 @@ class Communication : public CommQueue<> {
q->iv->radio->mBufCtrl.pop(); q->iv->radio->mBufCtrl.pop();
return; // don't wait for empty buffer return; // don't wait for empty buffer
} else if(IV_MI == q->iv->ivGen) { } else if(IV_MI == q->iv->ivGen) {
if(parseMiFrame(p, q)) parseMiFrame(p, q);
q->iv->curFrmCnt++; q->iv->curFrmCnt++;
} }
} //else -> serial does not match } //else -> serial does not match
@ -202,10 +209,9 @@ class Communication : public CommQueue<> {
if(q->iv->ivGen != IV_MI) { if(q->iv->ivGen != IV_MI) {
mState = States::CHECK_PACKAGE; mState = States::CHECK_PACKAGE;
} else { } else {
bool fastNext = true;
if(q->iv->miMultiParts < 6) { if(q->iv->miMultiParts < 6) {
mState = States::WAIT; mState = States::WAIT;
if((q->iv->radio->mRadioWaitTime.isTimeout() && mIsRetransmit) || !mIsRetransmit) { if(q->iv->radio->mRadioWaitTime.isTimeout() && q->attempts) {
miRepeatRequest(q); miRepeatRequest(q);
return; return;
} }
@ -215,12 +221,12 @@ class Communication : public CommQueue<> {
|| ((q->cmd == MI_REQ_CH2) && (q->iv->type == INV_TYPE_2CH)) || ((q->cmd == MI_REQ_CH2) && (q->iv->type == INV_TYPE_2CH))
|| ((q->cmd == MI_REQ_CH1) && (q->iv->type == INV_TYPE_1CH))) { || ((q->cmd == MI_REQ_CH1) && (q->iv->type == INV_TYPE_1CH))) {
miComplete(q->iv); miComplete(q->iv);
fastNext = false;
} }
if(fastNext) if(*mSerialDebug) {
miNextRequest(q->iv->type == INV_TYPE_4CH ? MI_REQ_4CH : MI_REQ_CH1, q); DPRINT_IVID(DBG_INFO, q->iv->id);
else DBGPRINTLN(F("Payload (MI got all)"));
closeRequest(q, true); }
closeRequest(q, true);
} }
} }
@ -252,10 +258,33 @@ class Communication : public CommQueue<> {
if(framnr) { if(framnr) {
if(0 == q->attempts) { if(0 == q->attempts) {
DPRINT_IVID(DBG_INFO, q->iv->id); DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("no attempts left")); DBGPRINTLN(F("timeout, no attempts left"));
closeRequest(q, false); closeRequest(q, false);
return; return;
} }
//count missing frames
if(!q->iv->mIsSingleframeReq && (q->iv->ivRadioType == INV_RADIO_TYPE_NRF)) { // already checked?
uint8_t missedFrames = 0;
for(uint8_t i = 0; i < q->iv->radio->mFramesExpected; i++) {
if(mLocalBuf[i].len == 0)
missedFrames++;
}
if(missedFrames > 3 || (q->cmd == RealTimeRunData_Debug && missedFrames > 1) || ((missedFrames > 1) && ((missedFrames + 2) > q->attempts))) {
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(String(missedFrames));
DBGPRINT(F(" frames missing "));
DBGPRINTLN(F("-> complete retransmit"));
}
mHeu.evalTxChQuality(q->iv, false, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt, true);
q->iv->radioStatistics.txCnt--;
q->iv->radioStatistics.retransmits++;
mCompleteRetry = true;
mState = States::RESET;
return;
}
}
setAttempt(); setAttempt();
if(*mSerialDebug) { if(*mSerialDebug) {
@ -273,12 +302,14 @@ class Communication : public CommQueue<> {
return; return;
} }
compilePayload(q); if(compilePayload(q)) {
if((NULL != mCbPayload) && (GridOnProFilePara != q->cmd) && (GetLossRate != q->cmd))
(mCbPayload)(q->cmd, q->iv);
if((NULL != mCbPayload) && (GridOnProFilePara != q->cmd) && (GetLossRate != q->cmd)) closeRequest(q, true);
(mCbPayload)(q->cmd, q->iv); } else
closeRequest(q, false);
closeRequest(q, true);
break; break;
} }
} }
@ -291,11 +322,6 @@ class Communication : public CommQueue<> {
DBGPRINT(String(p->millis)); DBGPRINT(String(p->millis));
DBGPRINT(F("ms | ")); DBGPRINT(F("ms | "));
DBGPRINT(String(p->len)); DBGPRINT(String(p->len));
DBGPRINT(F(", ARC "));
DBGPRINT(String(p->arc));
DBGPRINT(F(", PLOS "));
DBGPRINT(String(p->plos));
DBGPRINT(F(" |"));
if(INV_RADIO_TYPE_NRF == q->iv->ivRadioType) { if(INV_RADIO_TYPE_NRF == q->iv->ivRadioType) {
DBGPRINT(F(" CH")); DBGPRINT(F(" CH"));
if(3 == p->ch) if(3 == p->ch)
@ -366,7 +392,7 @@ class Communication : public CommQueue<> {
} }
} }
inline bool validateIvSerial(uint8_t buf[], Inverter<> *iv) { inline bool validateIvSerial(const uint8_t buf[], Inverter<> *iv) {
uint8_t tmp[4]; uint8_t tmp[4];
CP_U32_BigEndian(tmp, iv->radioId.u64 >> 8); CP_U32_BigEndian(tmp, iv->radioId.u64 >> 8);
for(uint8_t i = 0; i < 4; i++) { for(uint8_t i = 0; i < 4; i++) {
@ -416,14 +442,15 @@ class Communication : public CommQueue<> {
return true; return true;
} }
inline bool parseMiFrame(packet_t *p, const queue_s *q) { inline void parseMiFrame(packet_t *p, const queue_s *q) {
if((!mIsRetransmit && p->packet[9] == 0x00) && (p->millis < LIMIT_FAST_IV_MI)) //first frame is fast?
mHeu.setIvRetriesGood(q->iv,p->millis < LIMIT_VERYFAST_IV_MI);
if ((p->packet[0] == MI_REQ_CH1 + ALL_FRAMES) if ((p->packet[0] == MI_REQ_CH1 + ALL_FRAMES)
|| (p->packet[0] == MI_REQ_CH2 + ALL_FRAMES) || (p->packet[0] == MI_REQ_CH2 + ALL_FRAMES)
|| ((p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES)) || ((p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES))
&& (p->packet[0] < (0x39 + SINGLE_FRAME)) && (p->packet[0] < (0x39 + SINGLE_FRAME))
)) { //&& (p->packet[0] != (0x0f + ALL_FRAMES)))) { )) {
// small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39 // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39
//mPayload[iv->id].txId = p->packet[0];
miDataDecode(p, q); miDataDecode(p, q);
} else if (p->packet[0] == (0x0f + ALL_FRAMES)) { } else if (p->packet[0] == (0x0f + ALL_FRAMES)) {
miHwDecode(p, q); miHwDecode(p, q);
@ -436,13 +463,10 @@ class Communication : public CommQueue<> {
record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure
rec->ts = q->ts; rec->ts = q->ts;
miStsConsolidate(q, ((p->packet[0] == 0x88) ? 1 : 2), rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]); miStsConsolidate(q, ((p->packet[0] == 0x88) ? 1 : 2), rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]);
//mHeu.setGotFragment(q->iv); only do this when we are through the cycle?
} }
return true;
} }
inline bool parseDevCtrl(packet_t *p, const queue_s *q) { inline bool parseDevCtrl(const packet_t *p, const queue_s *q) {
switch(p->packet[12]) { switch(p->packet[12]) {
case ActivePowerContr: case ActivePowerContr:
if(p->packet[13] != 0x00) if(p->packet[13] != 0x00)
@ -480,7 +504,7 @@ class Communication : public CommQueue<> {
return accepted; return accepted;
} }
inline void compilePayload(const queue_s *q) { inline bool compilePayload(const queue_s *q) {
uint16_t crc = 0xffff, crcRcv = 0x0000; uint16_t crc = 0xffff, crcRcv = 0x0000;
for(uint8_t i = 0; i < mMaxFrameId; i++) { for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(i == (mMaxFrameId - 1)) { if(i == (mMaxFrameId - 1)) {
@ -496,27 +520,22 @@ class Communication : public CommQueue<> {
DBGPRINT(F("CRC Error ")); DBGPRINT(F("CRC Error "));
if(q->attempts == 0) { if(q->attempts == 0) {
DBGPRINTLN(F("-> Fail")); DBGPRINTLN(F("-> Fail"));
closeRequest(q, false);
} else } else
DBGPRINTLN(F("-> complete retransmit")); DBGPRINTLN(F("-> complete retransmit"));
mCompleteRetry = true;
mState = States::RESET; mState = States::RESET;
return; return false;
} }
/*DPRINT_IVID(DBG_INFO, q->iv->id); mPayload.fill(0);
DBGPRINT(F("procPyld: cmd: 0x"));
DBGHEXLN(q->cmd);*/
memset(mPayload, 0, MAX_BUFFER);
int8_t rssi = -127; int8_t rssi = -127;
uint8_t len = 0; uint8_t len = 0;
DPRINT_IVID(DBG_INFO, q->iv->id);
for(uint8_t i = 0; i < mMaxFrameId; i++) { for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(mLocalBuf[i].len + len > MAX_BUFFER) { if(mLocalBuf[i].len + len > MAX_BUFFER) {
DPRINTLN(DBG_ERROR, F("payload buffer to small!")); DPRINTLN(DBG_ERROR, F("payload buffer to small!"));
return; return true;
} }
memcpy(&mPayload[len], mLocalBuf[i].buf, mLocalBuf[i].len); memcpy(&mPayload[len], mLocalBuf[i].buf, mLocalBuf[i].len);
len += mLocalBuf[i].len; len += mLocalBuf[i].len;
@ -527,30 +546,31 @@ class Communication : public CommQueue<> {
len -= 2; len -= 2;
DPRINT_IVID(DBG_INFO, q->iv->id); if(*mSerialDebug) {
DBGPRINT(F("Payload (")); DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(String(len)); DBGPRINT(F("Payload ("));
if(*mPrintWholeTrace) { DBGPRINT(String(len));
DBGPRINT(F("): ")); if(*mPrintWholeTrace) {
ah::dumpBuf(mPayload, len); DBGPRINT(F("): "));
} else ah::dumpBuf(mPayload.data(), len);
DBGPRINTLN(F(")")); } else
DBGPRINTLN(F(")"));
}
if(GridOnProFilePara == q->cmd) { if(GridOnProFilePara == q->cmd) {
q->iv->addGridProfile(mPayload, len); q->iv->addGridProfile(mPayload.data(), len);
return; return true;
} }
record_t<> *rec = q->iv->getRecordStruct(q->cmd); record_t<> *rec = q->iv->getRecordStruct(q->cmd);
if(NULL == rec) { if(NULL == rec) {
if(GetLossRate == q->cmd) { if(GetLossRate == q->cmd) {
q->iv->parseGetLossRate(mPayload, len); q->iv->parseGetLossRate(mPayload.data(), len);
return; return true;
} else { } else
DPRINTLN(DBG_ERROR, F("record is NULL!")); DPRINTLN(DBG_ERROR, F("record is NULL!"));
closeRequest(q, false);
} return false;
return;
} }
if((rec->pyldLen != len) && (0 != rec->pyldLen)) { if((rec->pyldLen != len) && (0 != rec->pyldLen)) {
if(*mSerialDebug) { if(*mSerialDebug) {
@ -558,15 +578,13 @@ class Communication : public CommQueue<> {
DBGPRINT(String(rec->pyldLen)); DBGPRINT(String(rec->pyldLen));
DBGPRINTLN(F(" bytes")); DBGPRINTLN(F(" bytes"));
} }
/*q->iv->radioStatistics.rxFail++;*/
closeRequest(q, false);
return; return false;
} }
rec->ts = q->ts; rec->ts = q->ts;
for (uint8_t i = 0; i < rec->length; i++) { for (uint8_t i = 0; i < rec->length; i++) {
q->iv->addValue(i, mPayload, rec); q->iv->addValue(i, mPayload.data(), rec);
} }
rec->mqttSentStatus = MqttSentStatus::NEW_DATA; rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
@ -576,13 +594,14 @@ class Communication : public CommQueue<> {
if(AlarmData == q->cmd) { if(AlarmData == q->cmd) {
uint8_t i = 0; uint8_t i = 0;
while(1) { while(1) {
if(0 == q->iv->parseAlarmLog(i++, mPayload, len)) if(0 == q->iv->parseAlarmLog(i++, mPayload.data(), len))
break; break;
if (NULL != mCbAlarm) if (NULL != mCbAlarm)
(mCbAlarm)(q->iv); (mCbAlarm)(q->iv);
yield(); yield();
} }
} }
return true;
} }
void sendRetransmit(const queue_s *q, uint8_t i) { void sendRetransmit(const queue_s *q, uint8_t i) {
@ -600,11 +619,11 @@ class Communication : public CommQueue<> {
mHeu.evalTxChQuality(q->iv, crcPass, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt); mHeu.evalTxChQuality(q->iv, crcPass, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
if(crcPass) if(crcPass)
q->iv->radioStatistics.rxSuccess++; q->iv->radioStatistics.rxSuccess++;
else if(q->iv->mGotFragment) else if(q->iv->mGotFragment || mCompleteRetry)
q->iv->radioStatistics.rxFail++; // got no complete payload q->iv->radioStatistics.rxFail++; // got no complete payload
else else
q->iv->radioStatistics.rxFailNoAnser++; // got nothing q->iv->radioStatistics.rxFailNoAnswer++; // got nothing
mWaitTime.startTimeMonitor(*mInverterGap); mWaitTime.startTimeMonitor(1); // maybe remove, side effects unknown
bool keep = false; bool keep = false;
if(q->isDevControl) if(q->isDevControl)
@ -615,6 +634,7 @@ class Communication : public CommQueue<> {
q->iv->mGotLastMsg = false; q->iv->mGotLastMsg = false;
q->iv->miMultiParts = 0; q->iv->miMultiParts = 0;
mIsRetransmit = false; mIsRetransmit = false;
mCompleteRetry = false;
mState = States::RESET; mState = States::RESET;
DBGPRINTLN(F("-----")); DBGPRINTLN(F("-----"));
} }
@ -656,18 +676,17 @@ class Communication : public CommQueue<> {
}; };
*/ */
if ( p->packet[9] == 0x00 ) {//first frame if ( p->packet[9] == 0x00 ) { //first frame
//FLD_FW_VERSION //FLD_FW_VERSION
for (uint8_t i = 0; i < 5; i++) { for (uint8_t i = 0; i < 5; i++) {
q->iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1); q->iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1);
} }
q->iv->isConnected = true;
if(*mSerialDebug) { if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id); DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("HW_VER is ")); DBGPRINT(F("HW_VER is "));
DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25])); DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25]));
} }
record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec->ts = q->ts; rec->ts = q->ts;
q->iv->setValue(1, rec, (uint32_t) ((p->packet[24] << 8) + p->packet[25])/1); q->iv->setValue(1, rec, (uint32_t) ((p->packet[24] << 8) + p->packet[25])/1);
q->iv->miMultiParts +=4; q->iv->miMultiParts +=4;
@ -686,7 +705,7 @@ class Communication : public CommQueue<> {
byte[23] to byte[26] Matching_APPFW_PN*/ byte[23] to byte[26] Matching_APPFW_PN*/
DPRINT(DBG_INFO,F("HW_PartNo ")); DPRINT(DBG_INFO,F("HW_PartNo "));
DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])); DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13]));
record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec->ts = q->ts; rec->ts = q->ts;
q->iv->setValue(0, rec, (uint32_t) ((((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])/1); q->iv->setValue(0, rec, (uint32_t) ((((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])/1);
rec->mqttSentStatus = MqttSentStatus::NEW_DATA; rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
@ -801,35 +820,22 @@ class Communication : public CommQueue<> {
miStsConsolidate(q, datachan, rec, p->packet[23], p->packet[24]); miStsConsolidate(q, datachan, rec, p->packet[23], p->packet[24]);
if (p->packet[0] < (0x39 + ALL_FRAMES) ) { if (p->packet[0] < (0x39 + ALL_FRAMES) ) {
mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), 1);
miNextRequest((p->packet[0] - ALL_FRAMES + 1), q); miNextRequest((p->packet[0] - ALL_FRAMES + 1), q);
} else { } else {
q->iv->miMultiParts = 7; // indicate we are ready q->iv->miMultiParts = 7; // indicate we are ready
} }
} else if((p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES)) && (q->iv->type == INV_TYPE_2CH)) { } else if((p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES)) && (q->iv->type == INV_TYPE_2CH)) {
//addImportant(q->iv, MI_REQ_CH2);
miNextRequest(MI_REQ_CH2, q); miNextRequest(MI_REQ_CH2, q);
mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt); q->iv->mIvRxCnt++; // statistics workaround...
q->iv->mIvRxCnt++; // statistics workaround...
} else { // first data msg for 1ch, 2nd for 2ch } else // first data msg for 1ch, 2nd for 2ch
q->iv->miMultiParts += 6; // indicate we are ready q->iv->miMultiParts += 6; // indicate we are ready
}
} }
void miNextRequest(uint8_t cmd, const queue_s *q) { void miNextRequest(uint8_t cmd, const queue_s *q) {
incrAttempt(); // if function is called, we got something, and we necessarily need more transmissions for MI types... mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
if(*mSerialDebug) { mHeu.getTxCh(q->iv);
DPRINT_IVID(DBG_WARN, q->iv->id); q->iv->radioStatistics.ivSent++;
DBGPRINT(F("next request ("));
DBGPRINT(String(q->attempts));
DBGPRINT(F(" attempts left): 0x"));
DBGHEXLN(cmd);
}
if(q->iv->miMultiParts == 7)
q->iv->radioStatistics.rxSuccess++;
mFramesExpected = getFramesExpected(q); mFramesExpected = getFramesExpected(q);
q->iv->radio->setExpectedFrames(mFramesExpected); q->iv->radio->setExpectedFrames(mFramesExpected);
@ -838,6 +844,13 @@ class Communication : public CommQueue<> {
q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]); q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
q->iv->miMultiParts = 0; q->iv->miMultiParts = 0;
q->iv->mGotFragment = 0; q->iv->mGotFragment = 0;
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("next: ("));
DBGPRINT(String(q->attempts));
DBGPRINT(F(" attempts left): 0x"));
DBGHEXLN(cmd);
}
mIsRetransmit = true; mIsRetransmit = true;
chgCmd(cmd); chgCmd(cmd);
//mState = States::WAIT; //mState = States::WAIT;
@ -845,18 +858,17 @@ class Communication : public CommQueue<> {
void miRepeatRequest(const queue_s *q) { void miRepeatRequest(const queue_s *q) {
setAttempt(); // if function is called, we got something, and we necessarily need more transmissions for MI types... setAttempt(); // if function is called, we got something, and we necessarily need more transmissions for MI types...
q->iv->radio->sendCmdPacket(q->iv, q->cmd, 0x00, true);
q->iv->radioStatistics.retransmits++;
q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
if(*mSerialDebug) { if(*mSerialDebug) {
DPRINT_IVID(DBG_WARN, q->iv->id); DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("resend request (")); DBGPRINT(F("resend request ("));
DBGPRINT(String(q->attempts)); DBGPRINT(String(q->attempts));
DBGPRINT(F(" attempts left): 0x")); DBGPRINT(F(" attempts left): 0x"));
DBGHEXLN(q->cmd); DBGHEXLN(q->cmd);
} }
//mIsRetransmit = false;
q->iv->radio->sendCmdPacket(q->iv, q->cmd, 0x00, true);
q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
mIsRetransmit = false;
} }
void miStsConsolidate(const queue_s *q, uint8_t stschan, record_t<> *rec, uint8_t uState, uint8_t uEnum, uint8_t lState = 0, uint8_t lEnum = 0) { void miStsConsolidate(const queue_s *q, uint8_t stschan, record_t<> *rec, uint8_t uState, uint8_t uEnum, uint8_t lState = 0, uint8_t lEnum = 0) {
@ -879,50 +891,62 @@ class Communication : public CommQueue<> {
statusMi = 8310; //trick? statusMi = 8310; //trick?
} }
uint16_t prntsts = statusMi == 3 ? 1 : statusMi; uint16_t prntsts = (statusMi == 3) ? 1 : statusMi;
bool stsok = true; bool stsok = true;
if ( prntsts != rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)] ) { //sth.'s changed? bool changedStatus = false; //if true, raise alarms and send via mqtt (might affect single channel only)
q->iv->alarmCnt = 1; // minimum... uint8_t oldState = rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)];
if ( prntsts != oldState ) { // sth.'s changed?
stsok = false; stsok = false;
//sth is or was wrong? if(!oldState) { // initial zero value? => just write this channel to main state and raise changed flags
if ( (q->iv->type != INV_TYPE_1CH) && ( (statusMi != 3) changedStatus = true;
|| ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1))) q->iv->alarmCnt = 1; // minimum...
) { } else {
q->iv->lastAlarm[stschan+q->iv->type==INV_TYPE_2CH ? 2: 4] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts); //sth is or was wrong?
q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0); if (q->iv->type == INV_TYPE_1CH) {
q->iv->alarmCnt = q->iv->type == INV_TYPE_2CH ? 3 : 5; changedStatus = true;
} else if ( (q->iv->type == INV_TYPE_1CH) && ( (statusMi != 3) if(q->iv->alarmCnt == 2) // we had sth. other than "producing" in the past
|| ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1))) q->iv->lastAlarm[1].end = q->ts;
) { else { // copy old state and mark as ended
q->iv->lastAlarm[stschan] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts); q->iv->lastAlarm[1] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts);
} else if (q->iv->type == INV_TYPE_1CH) q->iv->alarmCnt = 2;
stsok = true; }
} else if((prntsts != 1) || (q->iv->alarmCnt > 1) ) { // we had sth. other than "producing" in the past in at least one channel (2 and 4 ch types)
q->iv->alarmLastId = prntsts; //iv->alarmMesIndex; if (q->iv->alarmCnt == 1)
q->iv->alarmCnt = (q->iv->type == INV_TYPE_2CH) ? 5 : 9;
if (q->iv->alarmCnt > 1) { //more than one channel if(q->iv->lastAlarm[stschan].code != prntsts) { // changed?
for (uint8_t ch = 0; ch < (q->iv->alarmCnt); ++ch) { //start with 1 changedStatus = true;
if (q->iv->lastAlarm[ch].code == 1) { if(q->iv->lastAlarm[stschan].code) // copy old data and mark as ended (if any)
stsok = true; q->iv->lastAlarm[(stschan + (q->iv->type==INV_TYPE_2CH ? 2 : 4))] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts);
break; q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0);
}
if(changedStatus) {
for (uint8_t i = 1; i <= q->iv->channels; i++) { //start with 1
if (q->iv->lastAlarm[i].code == 1) {
stsok = true;
break;
}
}
} }
} }
} }
if(*mSerialDebug) {
DPRINT(DBG_WARN, F("New state on CH"));
DBGPRINT(String(stschan)); DBGPRINT(F(" ("));
DBGPRINT(String(prntsts)); DBGPRINT(F("): "));
DBGPRINTLN(q->iv->getAlarmStr(prntsts));
}
if(!q->iv->miMultiParts)
q->iv->miMultiParts = 1; // indicate we got status info (1+2 ch types)
} }
if (!stsok) { if (!stsok) {
q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts); q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts);
q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0); q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0);
}
if (changedStatus || !stsok) {
rec->ts = q->ts; rec->ts = q->ts;
rec->mqttSentStatus = MqttSentStatus::NEW_DATA; rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
q->iv->alarmLastId = prntsts; //iv->alarmMesIndex;
if (NULL != mCbAlarm)
(mCbAlarm)(q->iv);
if(*mSerialDebug) {
DPRINT(DBG_WARN, F("New state on CH"));
DBGPRINT(String(stschan)); DBGPRINT(F(" ("));
DBGPRINT(String(prntsts)); DBGPRINT(F("): "));
DBGPRINTLN(q->iv->getAlarmStr(prntsts));
}
} }
if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) { if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) {
@ -933,27 +957,30 @@ class Communication : public CommQueue<> {
DBGPRINTLN(String(q->iv->alarmMesIndex)); DBGPRINTLN(String(q->iv->alarmMesIndex));
} }
} }
if(!q->iv->miMultiParts)
q->iv->miMultiParts = 1; // indicate we got status info (1+2 ch types)
} }
void miComplete(Inverter<> *iv) { void miComplete(Inverter<> *iv) {
if (*mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("got all data msgs"));
}
if (iv->mGetLossInterval >= AHOY_GET_LOSS_INTERVAL) { // initially mIvRxCnt = mIvTxCnt = 0 if (iv->mGetLossInterval >= AHOY_GET_LOSS_INTERVAL) { // initially mIvRxCnt = mIvTxCnt = 0
iv->mGetLossInterval = 1; iv->mGetLossInterval = 1;
iv->radioStatistics.ivSent = iv->mIvRxCnt + iv->mDtuTxCnt; // iv->mIvRxCnt is the nr. of additional answer frames, default we expect one frame per request iv->radioStatistics.ivSent = iv->mIvRxCnt + iv->mDtuTxCnt; // iv->mIvRxCnt is the nr. of additional answer frames, default we expect one frame per request
iv->radioStatistics.ivLoss = iv->radioStatistics.ivSent - iv->mDtuRxCnt; // this is what we didn't receive iv->radioStatistics.ivLoss = iv->radioStatistics.ivSent - iv->mDtuRxCnt; // this is what we didn't receive
iv->radioStatistics.dtuLoss = iv->mIvTxCnt; // this is somehow the requests w/o answers in that periode iv->radioStatistics.dtuLoss = iv->mIvTxCnt; // this is somehow the requests w/o answers in that periode
iv->radioStatistics.dtuSent = iv->mDtuTxCnt; iv->radioStatistics.dtuSent = iv->mDtuTxCnt;
if (mSerialDebug) { if (*mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN("DTU loss: " + DBGPRINT(F("DTU loss: ") +
String (iv->radioStatistics.ivLoss) + "/" + String (iv->radioStatistics.ivLoss) + F("/") +
String (iv->radioStatistics.ivSent) + " frames for " + String (iv->radioStatistics.ivSent) + F(" frames for ") +
String (iv->radioStatistics.dtuSent) + " requests"); String (iv->radioStatistics.dtuSent) + F(" requests"));
if(iv->mAckCount) {
DBGPRINT(F(". ACKs: "));
DBGPRINTLN(String(iv->mAckCount));
iv->mAckCount = 0;
} else
DBGPRINTLN(F(""));
} }
iv->mIvRxCnt = 0; // start new interval, iVRxCnt is abused to collect additional possible frames iv->mIvRxCnt = 0; // start new interval, iVRxCnt is abused to collect additional possible frames
iv->mIvTxCnt = 0; // start new interval, iVTxCnt is abused to collect nr. of unanswered requests iv->mIvTxCnt = 0; // start new interval, iVTxCnt is abused to collect nr. of unanswered requests
@ -971,10 +998,8 @@ class Communication : public CommQueue<> {
ac_pow += iv->getValue(iv->getPosByChFld(1, FLD_PDC, rec), rec); ac_pow += iv->getValue(iv->getPosByChFld(1, FLD_PDC, rec), rec);
} else { } else {
for(uint8_t i = 1; i <= iv->channels; i++) { for(uint8_t i = 1; i <= iv->channels; i++) {
if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1)) { if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1))
uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); ac_pow += iv->getValue(iv->getPosByChFld(i, FLD_PDC, rec), rec);
ac_pow += iv->getValue(pos, rec);
}
} }
} }
ac_pow = (int) (ac_pow*9.5); ac_pow = (int) (ac_pow*9.5);
@ -1002,17 +1027,17 @@ class Communication : public CommQueue<> {
private: private:
States mState = States::RESET; States mState = States::RESET;
uint32_t *mTimestamp; uint32_t *mTimestamp = nullptr;
bool *mPrivacyMode, *mSerialDebug, *mPrintWholeTrace; bool *mPrivacyMode = nullptr, *mSerialDebug = nullptr, *mPrintWholeTrace = nullptr;
uint16_t *mInverterGap;
TimeMonitor mWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state) TimeMonitor mWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state)
std::array<frame_t, MAX_PAYLOAD_ENTRIES> mLocalBuf; std::array<frame_t, MAX_PAYLOAD_ENTRIES> mLocalBuf;
bool mFirstTry = false; // see, if we should do a second try bool mFirstTry = false; // see, if we should do a second try
bool mIsRetransmit = false; // we already had waited one complete cycle bool mCompleteRetry = false; // remember if we did request a complete retransmission
uint8_t mMaxFrameId; bool mIsRetransmit = false; // we already had waited one complete cycle
uint8_t mMaxFrameId = 0;
uint8_t mFramesExpected = 12; // 0x8c was highest last frame for alarm data uint8_t mFramesExpected = 12; // 0x8c was highest last frame for alarm data
uint16_t mTimeout = 0; // calculating that once should be ok uint16_t mTimeout = 0; // calculating that once should be ok
uint8_t mPayload[MAX_BUFFER]; std::array<uint8_t, MAX_BUFFER> mPayload;
payloadListenerType mCbPayload = NULL; payloadListenerType mCbPayload = NULL;
powerLimitAckListenerType mCbPwrAck = NULL; powerLimitAckListenerType mCbPwrAck = NULL;
alarmListenerType mCbAlarm = NULL; alarmListenerType mCbAlarm = NULL;

80
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 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -23,8 +23,8 @@
class Heuristic { class Heuristic {
public: public:
uint8_t getTxCh(Inverter<> *iv) { uint8_t getTxCh(Inverter<> *iv) {
if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen)) if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
return 0; // not used for these inverter types return 0; // not used for other than nRF inverter types
HeuristicInv *ih = &iv->heuristics; HeuristicInv *ih = &iv->heuristics;
@ -38,6 +38,8 @@ class Heuristic {
ih->txRfChId = curId; ih->txRfChId = curId;
curId = (curId + 1) % RF_MAX_CHANNEL_ID; curId = (curId + 1) % RF_MAX_CHANNEL_ID;
} }
if(ih->txRfQuality[ih->txRfChId] == RF_MIN_QUALTIY) // all channels are bad, reset...
ih->clear();
if(ih->testPeriodSendCnt < 0xff) if(ih->testPeriodSendCnt < 0xff)
ih->testPeriodSendCnt++; ih->testPeriodSendCnt++;
@ -66,10 +68,12 @@ class Heuristic {
ih->testPeriodFailCnt = 0; ih->testPeriodFailCnt = 0;
} }
iv->radio->mTxRetriesNext = getIvRetries(iv);
return id2Ch(ih->txRfChId); return id2Ch(ih->txRfChId);
} }
void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments) { void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments, bool quotaMissed = false) {
HeuristicInv *ih = &iv->heuristics; HeuristicInv *ih = &iv->heuristics;
#if (DBG_DEBUG == DEBUG_LEVEL) #if (DBG_DEBUG == DEBUG_LEVEL)
@ -82,8 +86,10 @@ class Heuristic {
DBGPRINT(", "); DBGPRINT(", ");
DBGPRINTLN(String(ih->lastRxFragments)); DBGPRINTLN(String(ih->lastRxFragments));
#endif #endif
if(quotaMissed) // we got not enough frames on this attempt, but iv was answering
updateQuality(ih, (rxFragments > 3 ? RF_TX_CHAN_QUALITY_GOOD : (rxFragments > 1 ? RF_TX_CHAN_QUALITY_OK : RF_TX_CHAN_QUALITY_LOW)));
if(ih->lastRxFragments == rxFragments) { else if(ih->lastRxFragments == rxFragments) {
if(crcPass) if(crcPass)
updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD); updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD);
else if(!retransmits || isNewTxCh(ih)) { // nothing received: send probably lost else if(!retransmits || isNewTxCh(ih)) { // nothing received: send probably lost
@ -130,7 +136,7 @@ class Heuristic {
ih->lastRxFragments = rxFragments; ih->lastRxFragments = rxFragments;
} }
void printStatus(Inverter<> *iv) { void printStatus(const Inverter<> *iv) {
DPRINT_IVID(DBG_INFO, iv->id); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("Radio infos:")); DBGPRINT(F("Radio infos:"));
if((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) { if((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) {
@ -147,7 +153,7 @@ class Heuristic {
DBGPRINT(F(", f: ")); DBGPRINT(F(", f: "));
DBGPRINT(String(iv->radioStatistics.rxFail)); DBGPRINT(String(iv->radioStatistics.rxFail));
DBGPRINT(F(", n: ")); DBGPRINT(F(", n: "));
DBGPRINT(String(iv->radioStatistics.rxFailNoAnser)); DBGPRINT(String(iv->radioStatistics.rxFailNoAnswer));
DBGPRINT(F(" | p: ")); // better debugging for helpers... DBGPRINT(F(" | p: ")); // better debugging for helpers...
if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen)) if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
DBGPRINTLN(String(iv->config->powerLevel-10)); DBGPRINTLN(String(iv->config->powerLevel-10));
@ -155,8 +161,50 @@ class Heuristic {
DBGPRINTLN(String(iv->config->powerLevel)); 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: private:
bool isNewTxCh(HeuristicInv *ih) { bool isNewTxCh(const HeuristicInv *ih) const {
return ih->txRfChId != ih->lastBestTxChId; return ih->txRfChId != ih->lastBestTxChId;
} }
@ -169,18 +217,12 @@ class Heuristic {
} }
inline uint8_t id2Ch(uint8_t id) { inline uint8_t id2Ch(uint8_t id) {
switch(id) { if (id < RF_MAX_CHANNEL_ID)
case 0: return 3; return mChList[id];
case 1: return 23; else
case 2: return 40; return 3; // standard
case 3: return 61;
case 4: return 75;
}
return 3; // standard
} }
uint8_t mChList[RF_MAX_CHANNEL_ID] = {03, 23, 40, 61, 75};
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 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -14,7 +14,27 @@
class HeuristicInv { class HeuristicInv {
public: public:
HeuristicInv() { 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: public:
@ -27,6 +47,8 @@ class HeuristicInv {
uint8_t testChId = 0; uint8_t testChId = 0;
int8_t saveOldTestQuality = -6; int8_t saveOldTestQuality = -6;
uint8_t lastRxFragments = 0; 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__*/ #endif /*__HEURISTIC_INV_H__*/

20
src/hm/hmDefines.h

@ -76,23 +76,23 @@ enum {CMD_CALC = 0xffff};
enum {CH0 = 0, CH1, CH2, CH3, CH4, CH5, CH6}; enum {CH0 = 0, CH1, CH2, CH3, CH4, CH5, CH6};
enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH}; enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH};
enum {INV_RADIO_TYPE_NRF = 0, INV_RADIO_TYPE_CMT}; enum {INV_RADIO_TYPE_UNKNOWN = 0, INV_RADIO_TYPE_NRF, INV_RADIO_TYPE_CMT};
#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
#define DURATION_ONEFRAME 50 // timeout parameter for each expected frame (ms) #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_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_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_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) #define DURATION_PAUSE_LASTFR 45 // how long to pause after last frame (ms)
const uint8_t duration_reserve[2] = {115,115}; 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 { typedef struct {
uint8_t fieldId; // field id uint8_t fieldId; // field id

350
src/hm/hmInverter.h

@ -81,12 +81,12 @@ enum class InverterStatus : uint8_t {
template<class T=float> template<class T=float>
struct record_t { struct record_t {
byteAssign_t* assign; // assignment of bytes in payload byteAssign_t* assign = nullptr; // assignment of bytes in payload
uint8_t length; // length of the assignment list uint8_t length = 0; // length of the assignment list
T *record; // data pointer T *record = nullptr; // data pointer
uint32_t ts; // timestamp of last received payload uint32_t ts = 0; // timestamp of last received payload
uint8_t pyldLen; // expected payload length for plausibility check uint8_t pyldLen = 0; // expected payload length for plausibility check
MqttSentStatus mqttSentStatus; // indicates the current MqTT sent status MqttSentStatus mqttSentStatus = MqttSentStatus:: NEW_DATA; // indicates the current MqTT sent status
}; };
struct alarm_t { struct alarm_t {
@ -113,124 +113,108 @@ const calcFunc_t<T> calcFunctions[] = {
template <class REC_TYP> template <class REC_TYP>
class Inverter { class Inverter {
public: public:
uint8_t ivGen; // generation of inverter (HM / MI) uint8_t ivGen = IV_UNKNOWN; // generation of inverter (HM / MI)
uint8_t ivRadioType; // refers to used radio (nRF24 / CMT) uint8_t ivRadioType = INV_RADIO_TYPE_UNKNOWN; // refers to used radio (nRF24 / CMT)
cfgIv_t *config; // stored settings cfgIv_t *config = nullptr; // stored settings
uint8_t id; // unique id uint8_t id = 0; // unique id
uint8_t type; // integer which refers to inverter type uint8_t type = INV_TYPE_1CH; // integer which refers to inverter type
uint16_t alarmMesIndex; // Last recorded Alarm Message Index uint16_t alarmMesIndex = 0; // Last recorded Alarm Message Index
uint16_t powerLimit[2]; // limit power output (multiplied by 10) uint16_t powerLimit[2] = {0xffff, AbsolutNonPersistent}; // limit power output (multiplied by 10)
float actPowerLimit; // actual power limit uint16_t actPowerLimit = 0xffff; // actual power limit
bool powerLimitAck; // acknowledged power limit (default: false) bool powerLimitAck = false; // acknowledged power limit
uint8_t devControlCmd; // carries the requested cmd uint8_t devControlCmd = InitDataState; // carries the requested cmd
serial_u radioId; // id converted to modbus serial_u radioId; // id converted to modbus
uint8_t channels; // number of PV channels (1-4) uint8_t channels = 1; // number of PV channels (1-4)
record_t<REC_TYP> recordMeas; // structure for measured values record_t<REC_TYP> recordMeas; // structure for measured values
record_t<REC_TYP> recordInfo; // structure for info values record_t<REC_TYP> recordInfo; // structure for info values
record_t<REC_TYP> recordHwInfo; // structure for simple (hardware) info values record_t<REC_TYP> recordHwInfo; // structure for simple (hardware) info values
record_t<REC_TYP> recordConfig; // structure for system config values record_t<REC_TYP> recordConfig; // structure for system config values
record_t<REC_TYP> recordAlarm; // structure for alarm values record_t<REC_TYP> recordAlarm; // structure for alarm values
bool isConnected; // shows if inverter was successfully identified (fw version and hardware info) InverterStatus status = InverterStatus::OFF; // indicates the current inverter status
InverterStatus status; // indicates the current inverter status std::array<alarm_t, 10> lastAlarm; // holds last 10 alarms
std::array<alarm_t, 10> lastAlarm; // holds last 10 alarms int8_t rssi = 0; // RSSI
int8_t rssi; // RSSI uint16_t alarmCnt = 0; // counts the total number of occurred alarms
uint16_t alarmCnt; // counts the total number of occurred alarms uint16_t alarmLastId = 0; // lastId which was received
uint16_t alarmLastId; // lastId which was received uint8_t mCmd = InitDataState; // holds the command to send
uint8_t mCmd; // holds the command to send bool mGotFragment = false; // shows if inverter has sent at least one fragment
bool mGotFragment; // shows if inverter has sent at least one fragment uint8_t miMultiParts = 0; // helper info for MI multiframe msgs
uint8_t miMultiParts; // helper info for MI multiframe msgs uint8_t outstandingFrames = 0; // helper info to count difference between expected and received frames
uint8_t outstandingFrames; // helper info to count difference between expected and received frames uint8_t curFrmCnt = 0; // count received frames in current loop
uint8_t curFrmCnt; // count received frames in current loop bool mGotLastMsg = false; // shows if inverter has already finished transmission cycle
bool mGotLastMsg; // shows if inverter has already finished transmission cycle bool mIsSingleframeReq = false; // indicates this is a missing single frame request
bool mIsSingleframeReq; // indicates this is a missing single frame request Radio *radio = nullptr; // pointer to associated radio class
Radio *radio; // pointer to associated radio class statistics_t radioStatistics; // information about transmitted, failed, ... packets
statistics_t radioStatistics; // information about transmitted, failed, ... packets HeuristicInv heuristics; // heuristic information / logic
HeuristicInv heuristics; // heuristic information / logic uint8_t curCmtFreq = 0; // current used CMT frequency, used to check if freq. was changed during runtime
uint8_t curCmtFreq; // 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; // 'pause night communication' sets this field to false bool commEnabled = true; // 'pause night communication' sets this field to false
uint32_t tsMaxAcPower; // holds the timestamp when the MaxAC power was seen
static uint32_t *timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup
//static IApp *app; // pointer to app interface
public: public:
Inverter() { Inverter() {
ivGen = IV_HM;
powerLimit[0] = 0xffff; // 6553.5 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;
alarmCnt = 0;
alarmLastId = 0;
rssi = -127;
miMultiParts = 0;
mGotLastMsg = false;
mCmd = InitDataState;
mIsSingleframeReq = false;
radio = NULL;
commEnabled = true;
tsMaxAcPower = 0;
memset(&radioStatistics, 0, sizeof(statistics_t)); memset(&radioStatistics, 0, sizeof(statistics_t));
memset(heuristics.txRfQuality, -6, 5);
memset(mOffYD, 0, sizeof(float) * 6); memset(mOffYD, 0, sizeof(float) * 6);
memset(mLastYD, 0, sizeof(float) * 6); memset(mLastYD, 0, sizeof(float) * 6);
mGridProfile.fill(0);
} }
void tickSend(std::function<void(uint8_t cmd, bool isDevControl)> cb) { void tickSend(std::function<void(uint8_t cmd, bool isDevControl)> cb) {
if(mDevControlRequest) { 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; mDevControlRequest = false;
} else if (IV_MI != ivGen) { // HM / HMS / HMT } else if (IV_MI != ivGen) { // HM / HMS / HMT
mGetLossInterval++; mGetLossInterval++;
if(mNextLive) if(INV_RADIO_TYPE_NRF == ivRadioType) {
cb(RealTimeRunData_Debug, false); // get live data // get live data until quality reaches maximum
else { if(!heuristics.isTxAtMax()) {
if(actPowerLimit == 0xffff) cb(RealTimeRunData_Debug, false); // get live data
cb(SystemConfigPara, false); // power limit info return;
else if(InitDataState != devControlCmd) { }
cb(devControlCmd, false); // custom command which was received by API
devControlCmd = InitDataState;
mGetLossInterval = 1;
} 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
} }
if(actPowerLimit == 0xffff) {
cb(SystemConfigPara, false); // power limit info
} else if(InitDataState != devControlCmd) {
cb(devControlCmd, false); // custom command which was received by API
devControlCmd = InitDataState;
mGetLossInterval = 1;
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);
return;
}
cb(RealTimeRunData_Debug, false); // get live data
} else { // MI } else { // MI
if(0 == getFwVersion()) { cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false);
mIvRxCnt +=2; mGetLossInterval++;
cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number if (type != INV_TYPE_4CH)
} else { mIvRxCnt++; // statistics workaround...
record_t<> *rec = getRecordStruct(InverterDevInform_Simple); if(isAvailable()) {
if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) { if(0 == getFwVersion()) {
cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
mIvRxCnt +=2; mIvRxCnt +=2;
} else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number
cb(0x10, false); // legacy GPF command } else {
else { record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false); if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) {
mGetLossInterval++; cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
if (type != INV_TYPE_4CH) mIvRxCnt +=2;
mIvRxCnt++; // statistics workaround... } else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile
cb(0x10, false); // legacy GPF command
} }
} }
} }
@ -249,15 +233,14 @@ class Inverter {
uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) { uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld"));
uint8_t pos = 0;
if(NULL != rec) { if(NULL != rec) {
uint8_t pos = 0;
for(; pos < rec->length; pos++) { for(; pos < rec->length; pos++) {
if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId)) if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
break; break;
} }
return (pos >= rec->length) ? 0xff : pos; return (pos >= rec->length) ? 0xff : pos;
} } else
else
return 0xff; return 0xff;
} }
@ -266,78 +249,73 @@ class Inverter {
} }
const char *getFieldName(uint8_t pos, record_t<> *rec) { const char *getFieldName(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getFieldName"));
if(NULL != rec) if(NULL != rec)
return fields[rec->assign[pos].fieldId]; return fields[rec->assign[pos].fieldId];
return notAvail; return notAvail;
} }
const char *getUnit(uint8_t pos, record_t<> *rec) { const char *getUnit(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getUnit"));
if(NULL != rec) if(NULL != rec)
return units[rec->assign[pos].unitId]; return units[rec->assign[pos].unitId];
return notAvail; return notAvail;
} }
uint8_t getChannel(uint8_t pos, record_t<> *rec) { uint8_t getChannel(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getChannel"));
if(NULL != rec) if(NULL != rec)
return rec->assign[pos].ch; return rec->assign[pos].ch;
return 0; return 0;
} }
bool setDevControlRequest(uint8_t cmd) { bool setDevControlRequest(uint8_t cmd) {
if(isConnected) { if(InverterStatus::OFF != status) {
mDevControlRequest = true; mDevControlRequest = true;
devControlCmd = cmd; devControlCmd = cmd;
//app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)" //app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)"
} }
return isConnected; return (InverterStatus::OFF != status);
} }
bool setDevCommand(uint8_t cmd) { bool setDevCommand(uint8_t cmd) {
if(isConnected) if(InverterStatus::OFF != status)
devControlCmd = cmd; 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")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:addValue"));
if(NULL != rec) { if(NULL != rec) {
uint8_t ptr = rec->assign[pos].start; uint8_t ptr = rec->assign[pos].start;
uint8_t end = ptr + rec->assign[pos].num; uint8_t end = ptr + rec->assign[pos].num;
uint16_t div = rec->assign[pos].div; uint16_t div = rec->assign[pos].div;
if(NULL != rec) { if(CMD_CALC != div) {
if(CMD_CALC != div) { uint32_t val = 0;
uint32_t val = 0; do {
do { val <<= 8;
val <<= 8; val |= buf[ptr];
val |= buf[ptr]; } while(++ptr != end);
} while(++ptr != end);
if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) { 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 // temperature, Qvar, and power factor are a signed values
rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div); rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div);
} else if (FLD_YT == rec->assign[pos].fieldId) { } else if (FLD_YT == rec->assign[pos].fieldId) {
rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]);
} else if (FLD_YD == rec->assign[pos].fieldId) { } else if (FLD_YD == rec->assign[pos].fieldId) {
float actYD = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency; float actYD = (REC_TYP)(val) / (REC_TYP)(div);
uint8_t idx = rec->assign[pos].ch - 1; uint8_t idx = rec->assign[pos].ch - 1;
if (mLastYD[idx] > actYD) if (mLastYD[idx] > actYD)
mOffYD[idx] += mLastYD[idx]; mOffYD[idx] += mLastYD[idx];
mLastYD[idx] = actYD; mLastYD[idx] = actYD;
rec->record[pos] = mOffYD[idx] + actYD; rec->record[pos] = mOffYD[idx] + actYD;
} else { } else {
if ((REC_TYP)(div) > 1) if ((REC_TYP)(div) > 1)
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
else else
rec->record[pos] = (REC_TYP)(val); rec->record[pos] = (REC_TYP)(val);
}
} }
} }
if(rec == &recordMeas) { if(rec == &recordMeas) {
mNextLive = false; // live data received
DPRINTLN(DBG_VERBOSE, "add real time"); DPRINTLN(DBG_VERBOSE, "add real time");
// get last alarm message index and save it in the inverter object // get last alarm message index and save it in the inverter object
if (getPosByChFld(0, FLD_EVT, rec) == pos) { if (getPosByChFld(0, FLD_EVT, rec) == pos) {
@ -348,13 +326,10 @@ class Inverter {
DBGPRINTLN(String(alarmMesIndex)); DBGPRINTLN(String(alarmMesIndex));
} }
} }
} } else {
else {
mNextLive = true;
if (rec->assign == InfoAssignment) { if (rec->assign == InfoAssignment) {
DPRINTLN(DBG_DEBUG, "add info"); DPRINTLN(DBG_DEBUG, "add info");
// eg. fw version ... // eg. fw version ...
isConnected = true;
} else if (rec->assign == SimpleInfoAssignment) { } else if (rec->assign == SimpleInfoAssignment) {
DPRINTLN(DBG_DEBUG, "add simple info"); DPRINTLN(DBG_DEBUG, "add simple info");
// eg. hw version ... // eg. hw version ...
@ -370,8 +345,7 @@ class Inverter {
} else } else
DPRINTLN(DBG_WARN, F("add with unknown assignment")); DPRINTLN(DBG_WARN, F("add with unknown assignment"));
} }
} } else
else
DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x")); DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));
// update status state-machine // update status state-machine
@ -389,18 +363,18 @@ class Inverter {
} }
REC_TYP getChannelFieldValue(uint8_t channel, uint8_t fieldId, record_t<> *rec) { REC_TYP getChannelFieldValue(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
uint8_t pos = 0;
if(NULL != rec) { if(NULL != rec) {
uint8_t pos = 0;
for(; pos < rec->length; pos++) { for(; pos < rec->length; pos++) {
if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId)) if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
break; break;
} }
if(pos >= rec->length) if(pos >= rec->length)
return 0; return 0;
return rec->record[pos]; return rec->record[pos];
} } else
else
return 0; return 0;
} }
@ -431,28 +405,25 @@ class Inverter {
bool isAvailable() { bool isAvailable() {
bool avail = false; bool avail = false;
if((recordMeas.ts == 0) && (recordInfo.ts == 0) && (recordConfig.ts == 0) && (recordAlarm.ts == 0)) if(recordMeas.ts == 0)
return false; return false;
if((*timestamp - recordMeas.ts) < INVERTER_INACT_THRES_SEC) if(((*timestamp) - recordMeas.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if((*timestamp - recordConfig.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if((*timestamp - recordAlarm.ts) < INVERTER_INACT_THRES_SEC)
avail = true; avail = true;
if(avail) { if(avail) {
if(status < InverterStatus::PRODUCING) if(status < InverterStatus::PRODUCING)
status = InverterStatus::STARTING; status = InverterStatus::STARTING;
} else { } else {
if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) { if(((*timestamp) - recordMeas.ts) > INVERTER_OFF_THRES_SEC) {
status = InverterStatus::OFF; if(status != InverterStatus::OFF) {
actPowerLimit = 0xffff; // power limit will be read once inverter becomes available status = InverterStatus::OFF;
alarmMesIndex = 0; actPowerLimit = 0xffff; // power limit will be read once inverter becomes available
} alarmMesIndex = 0;
else if(INV_RADIO_TYPE_NRF == ivRadioType)
heuristics.clear();
}
} else
status = InverterStatus::WAS_ON; status = InverterStatus::WAS_ON;
} }
@ -470,6 +441,7 @@ class Inverter {
else if(InverterStatus::PRODUCING == status) else if(InverterStatus::PRODUCING == status)
status = InverterStatus::WAS_PRODUCING; status = InverterStatus::WAS_PRODUCING;
} }
return producing; return producing;
} }
@ -527,11 +499,11 @@ class Inverter {
if (INV_TYPE_1CH == type) { if (INV_TYPE_1CH == type) {
if((IV_HM == ivGen) || (IV_MI == ivGen)) { if((IV_HM == ivGen) || (IV_MI == ivGen)) {
rec->length = (uint8_t)(HM1CH_LIST_LEN); 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; rec->pyldLen = HM1CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) { } else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS1CH_LIST_LEN); 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; rec->pyldLen = HMS1CH_PAYLOAD_LEN;
} }
channels = 1; channels = 1;
@ -539,11 +511,11 @@ class Inverter {
else if (INV_TYPE_2CH == type) { else if (INV_TYPE_2CH == type) {
if((IV_HM == ivGen) || (IV_MI == ivGen)) { if((IV_HM == ivGen) || (IV_MI == ivGen)) {
rec->length = (uint8_t)(HM2CH_LIST_LEN); 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; rec->pyldLen = HM2CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) { } else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS2CH_LIST_LEN); 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; rec->pyldLen = HMS2CH_PAYLOAD_LEN;
} }
channels = 2; channels = 2;
@ -551,18 +523,18 @@ class Inverter {
else if (INV_TYPE_4CH == type) { else if (INV_TYPE_4CH == type) {
if((IV_HM == ivGen) || (IV_MI == ivGen)) { if((IV_HM == ivGen) || (IV_MI == ivGen)) {
rec->length = (uint8_t)(HM4CH_LIST_LEN); 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; rec->pyldLen = HM4CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) { } else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS4CH_LIST_LEN); 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; rec->pyldLen = HMS4CH_PAYLOAD_LEN;
} }
channels = 4; channels = 4;
} }
else if (INV_TYPE_6CH == type) { else if (INV_TYPE_6CH == type) {
rec->length = (uint8_t)(HMT6CH_LIST_LEN); 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; rec->pyldLen = HMT6CH_PAYLOAD_LEN;
channels = 6; channels = 6;
} }
@ -575,22 +547,22 @@ class Inverter {
break; break;
case InverterDevInform_All: case InverterDevInform_All:
rec->length = (uint8_t)(HMINFO_LIST_LEN); 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; rec->pyldLen = HMINFO_PAYLOAD_LEN;
break; break;
case InverterDevInform_Simple: case InverterDevInform_Simple:
rec->length = (uint8_t)(HMSIMPLE_INFO_LIST_LEN); 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; rec->pyldLen = HMSIMPLE_INFO_PAYLOAD_LEN;
break; break;
case SystemConfigPara: case SystemConfigPara:
rec->length = (uint8_t)(HMSYSTEM_LIST_LEN); rec->length = (uint8_t)(HMSYSTEM_LIST_LEN);
rec->assign = (byteAssign_t *)SystemConfigParaAssignment; rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(SystemConfigParaAssignment));
rec->pyldLen = HMSYSTEM_PAYLOAD_LEN; rec->pyldLen = HMSYSTEM_PAYLOAD_LEN;
break; break;
case AlarmData: case AlarmData:
rec->length = (uint8_t)(HMALARMDATA_LIST_LEN); 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; rec->pyldLen = HMALARMDATA_PAYLOAD_LEN;
break; break;
default: default:
@ -614,7 +586,7 @@ class Inverter {
memset(mLastYD, 0, sizeof(float) * 6); 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) { if (len == HMGETLOSSRATE_PAYLOAD_LEN) {
uint16_t rxCnt = (pyld[0] << 8) + pyld[1]; uint16_t rxCnt = (pyld[0] << 8) + pyld[1];
uint16_t txCnt = (pyld[2] << 8) + pyld[3]; uint16_t txCnt = (pyld[2] << 8) + pyld[3];
@ -813,7 +785,7 @@ class Inverter {
void addGridProfile(uint8_t buf[], uint8_t length) { void addGridProfile(uint8_t buf[], uint8_t length) {
mGridLen = (length > MAX_GRID_LENGTH) ? MAX_GRID_LENGTH : 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) { String getGridProfile(void) {
@ -843,21 +815,23 @@ class Inverter {
radioId.b[0] = 0x01; radioId.b[0] = 0x01;
} }
private:
float mOffYD[6], mLastYD[6];
bool mDevControlRequest; // true if change needed
uint8_t mGridLen = 0;
uint8_t mGridProfile[MAX_GRID_LENGTH];
uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer)
bool mNextLive = true; // first read live data after booting up then version etc.
public: public:
static uint32_t *timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup
uint16_t mDtuRxCnt = 0; uint16_t mDtuRxCnt = 0;
uint16_t mDtuTxCnt = 0; uint16_t mDtuTxCnt = 0;
uint8_t mGetLossInterval = 0; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug uint8_t mGetLossInterval = 0; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug
uint16_t mIvRxCnt = 0; uint16_t mIvRxCnt = 0;
uint16_t mIvTxCnt = 0; uint16_t mIvTxCnt = 0;
uint16_t mAckCount = 0; uint16_t mAckCount = 0;
private:
float mOffYD[6], mLastYD[6];
bool mDevControlRequest = false; // true if change needed
uint8_t mGridLen = 0;
std::array<uint8_t, MAX_GRID_LENGTH> mGridProfile;
uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer)
}; };
template <class REC_TYP> template <class REC_TYP>

114
src/hm/hmRadio.h

@ -15,7 +15,6 @@
#endif #endif
#define SPI_SPEED 1000000 #define SPI_SPEED 1000000
#define RF_CHANNELS 5 #define RF_CHANNELS 5
const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
@ -53,7 +52,7 @@ class HmRadio : public Radio {
mPrintWholeTrace = printWholeTrace; mPrintWholeTrace = printWholeTrace;
generateDtuSn(); 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 #ifdef ESP32
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
@ -86,7 +85,7 @@ class HmRadio : public Radio {
mNrf24->enableDynamicPayloads(); mNrf24->enableDynamicPayloads();
mNrf24->setCRCLength(RF24_CRC_16); mNrf24->setCRCLength(RF24_CRC_16);
mNrf24->setAddressWidth(5); mNrf24->setAddressWidth(5);
mNrf24->openReadingPipe(1, reinterpret_cast<uint8_t*>(&DTU_RADIO_ID)); mNrf24->openReadingPipe(1, reinterpret_cast<uint8_t*>(&mDtuRadioId));
mNrf24->maskIRQ(false, false, false); // enable all receiving interrupts mNrf24->maskIRQ(false, false, false); // enable all receiving interrupts
mNrf24->setPALevel(1); // low is default mNrf24->setPALevel(1); // low is default
@ -100,7 +99,7 @@ class HmRadio : public Radio {
} }
// returns true if communication is active // returns true if communication is active
bool loop(void) { bool loop(void) override {
if (!mIrqRcvd && !mNRFisInRX) if (!mIrqRcvd && !mNRFisInRX)
return false; // first quick check => nothing to do at all here return false; // first quick check => nothing to do at all here
@ -113,22 +112,22 @@ class HmRadio : public Radio {
if (mRadioWaitTime.isTimeout()) { // timeout reached! if (mRadioWaitTime.isTimeout()) { // timeout reached!
mNRFisInRX = false; mNRFisInRX = false;
rx_ready = false;
return false; return false;
} }
// otherwise switch to next RX channel // otherwise switch to next RX channel
mTimeslotStart = millis(); mTimeslotStart = millis();
if(!mNRFloopChannels && ((mTimeslotStart - mLastIrqTime) > (DURATION_TXFRAME+DURATION_ONEFRAME))) if(!mNRFloopChannels && ((mTimeslotStart - mLastIrqTime) > (DURATION_TXFRAME))) //(DURATION_TXFRAME+DURATION_ONEFRAME)))
mNRFloopChannels = true; mNRFloopChannels = true;
rxPendular = !rxPendular; mRxPendular = !mRxPendular;
//innerLoopTimeout = (rxPendular ? 1 : 2)*DURATION_LISTEN_MIN;
innerLoopTimeout = DURATION_LISTEN_MIN; innerLoopTimeout = DURATION_LISTEN_MIN;
if(mNRFloopChannels) if(mNRFloopChannels)
tempRxChIdx = (tempRxChIdx + 4) % RF_CHANNELS; tempRxChIdx = (tempRxChIdx + 4) % RF_CHANNELS;
else else
tempRxChIdx = (mRxChIdx + rxPendular*4) % RF_CHANNELS; tempRxChIdx = (mRxChIdx + mRxPendular*4) % RF_CHANNELS;
mNrf24->setChannel(mRfChLst[tempRxChIdx]); mNrf24->setChannel(mRfChLst[tempRxChIdx]);
isRxInit = false; isRxInit = false;
@ -142,7 +141,7 @@ class HmRadio : public Radio {
if(tx_ok || tx_fail) { // tx related interrupt, basically we should start listening if(tx_ok || tx_fail) { // tx related interrupt, basically we should start listening
mNrf24->flush_tx(); // empty TX FIFO mNrf24->flush_tx(); // empty TX FIFO
mTxSetupTime = millis() - mMillis; //mTxSetupTime = millis() - mMillis;
if(mNRFisInRX) { if(mNRFisInRX) {
DPRINTLN(DBG_WARN, F("unexpected tx irq!")); DPRINTLN(DBG_WARN, F("unexpected tx irq!"));
@ -153,19 +152,19 @@ class HmRadio : public Radio {
if(tx_ok) if(tx_ok)
mLastIv->mAckCount++; mLastIv->mAckCount++;
mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS; 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->setChannel(mRfChLst[mRxChIdx]);
mNrf24->startListening(); mNrf24->startListening();
mTimeslotStart = millis(); mTimeslotStart = millis();
tempRxChIdx = mRxChIdx; tempRxChIdx = mRxChIdx; // might be better to start off with one channel less?
rxPendular = false; mRxPendular = false;
mNRFloopChannels = (mLastIv->ivGen == IV_MI); mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1 || mLastIv->mCmd == MI_REQ_CH2);
innerLoopTimeout = DURATION_LISTEN_MIN;
innerLoopTimeout = DURATION_TXFRAME;
} }
if(rx_ready) { if(rx_ready) {
if (getReceived()) { // check what we got, returns true for last package if (getReceived()) { // check what we got, returns true for last package or success for single frame request
mNRFisInRX = false; mNRFisInRX = false;
mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first end his transmissions mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first end his transmissions
mNrf24->stopListening(); mNrf24->stopListening();
@ -173,7 +172,6 @@ class HmRadio : public Radio {
innerLoopTimeout = DURATION_LISTEN_MIN; innerLoopTimeout = DURATION_LISTEN_MIN;
mTimeslotStart = millis(); mTimeslotStart = millis();
if (!mNRFloopChannels) { if (!mNRFloopChannels) {
//rxPendular = true; // stay longer on the next rx channel
if (isRxInit) { if (isRxInit) {
isRxInit = false; isRxInit = false;
tempRxChIdx = (mRxChIdx + 4) % RF_CHANNELS; tempRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
@ -182,22 +180,19 @@ class HmRadio : public Radio {
mRxChIdx = tempRxChIdx; mRxChIdx = tempRxChIdx;
} }
} }
rx_ready = false; // reset
return mNRFisInRX; return mNRFisInRX;
} /*else if(tx_fail) { }
mNRFisInRX = false;
return false;
}*/
} }
return false; return false;
} }
bool isChipConnected(void) { bool isChipConnected(void) const override {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected"));
return mNrf24->isChipConnected(); 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); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("sendControlPacket cmd: ")); DBGPRINT(F("sendControlPacket cmd: "));
DBGHEXLN(cmd); DBGHEXLN(cmd);
@ -283,27 +278,20 @@ class HmRadio : public Radio {
sendPacket(iv, cnt, isRetransmit, (IV_MI != iv->ivGen)); sendPacket(iv, cnt, isRetransmit, (IV_MI != iv->ivGen));
} }
uint8_t getDataRate(void) { uint8_t getDataRate(void) const {
if(!mNrf24->isChipConnected()) if(!mNrf24->isChipConnected())
return 3; // unknown return 3; // unknown
return mNrf24->getDataRate(); return mNrf24->getDataRate();
} }
bool isPVariant(void) { bool isPVariant(void) const {
return mNrf24->isPVariant(); return mNrf24->isPVariant();
} }
uint8_t getARC(void) {
return mNrf24->getARC();
}
uint8_t getPLOS(void) {
return mNrf24->getPLOS();
}
private: private:
inline bool getReceived(void) { inline bool getReceived(void) {
bool isLastPackage = false; bool isLastPackage = false;
bool isRetransmitAnswer = false;
rx_ready = false; // reset for ACK case rx_ready = false; // reset for ACK case
while(mNrf24->available()) { while(mNrf24->available()) {
@ -315,21 +303,27 @@ class HmRadio : public Radio {
p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len; p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len;
p.rssi = mNrf24->testRPD() ? -64 : -75; p.rssi = mNrf24->testRPD() ? -64 : -75;
p.millis = millis() - mMillis; p.millis = millis() - mMillis;
p.arc = mNrf24->getARC();
p.plos = mNrf24->getPLOS();
mNrf24->read(p.packet, p.len); mNrf24->read(p.packet, p.len);
if (p.packet[0] != 0x00) { if (p.packet[0] != 0x00) {
if(!checkIvSerial(p.packet, mLastIv)) { if(!checkIvSerial(p.packet, mLastIv)) {
DPRINT(DBG_WARN, "RX other inverter "); DPRINT(DBG_WARN, F("RX other inverter "));
if(!*mPrivacyMode) if(!*mPrivacyMode)
ah::dumpBuf(p.packet, p.len); ah::dumpBuf(p.packet, p.len);
else
DBGPRINTLN(F(""));
} else { } else {
mLastIv->mGotFragment = true; mLastIv->mGotFragment = true;
mBufCtrl.push(p); mBufCtrl.push(p);
if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received
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(IV_MI == mLastIv->ivGen) {
if (p.packet[0] == (0x0f + ALL_FRAMES)) // response from MI get information command if (p.packet[0] == (0x0f + ALL_FRAMES)) // response from MI get information command
@ -345,7 +339,7 @@ class HmRadio : public Radio {
} }
if(isLastPackage) if(isLastPackage)
mLastIv->mGotLastMsg = true; mLastIv->mGotLastMsg = true;
return isLastPackage; return isLastPackage || isRetransmitAnswer;
} }
void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) { void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) {
@ -356,23 +350,27 @@ class HmRadio : public Radio {
mTxChIdx = iv->heuristics.txRfChId; mTxChIdx = iv->heuristics.txRfChId;
if(*mSerialDebug) { if(*mSerialDebug) {
if(!isRetransmit) { /*if(!isRetransmit) {
DPRINT(DBG_INFO, "last tx setup: "); DPRINT(DBG_INFO, "last tx setup: ");
DBGPRINT(String(mTxSetupTime)); DBGPRINT(String(mTxSetupTime));
DBGPRINTLN("ms"); DBGPRINTLN("ms");
} }*/
DPRINT_IVID(DBG_INFO, iv->id); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("TX ")); DBGPRINT(F("TX "));
DBGPRINT(String(len)); DBGPRINT(String(len));
DBGPRINT(" CH"); DBGPRINT(" CH");
if(mTxChIdx == 0)
DBGPRINT("0");
DBGPRINT(String(mRfChLst[mTxChIdx])); DBGPRINT(String(mRfChLst[mTxChIdx]));
DBGPRINT(F(" | ")); DBGPRINT(F(", "));
DBGPRINT(String(mTxRetriesNext));
DBGPRINT(F(" ret. | "));
if(*mPrintWholeTrace) { if(*mPrintWholeTrace) {
if(*mPrivacyMode) if(*mPrivacyMode)
ah::dumpBuf(mTxBuf, len, 1, 4); ah::dumpBuf(mTxBuf.data(), len, 1, 4);
else else
ah::dumpBuf(mTxBuf, len); ah::dumpBuf(mTxBuf.data(), len);
} else { } else {
DHEX(mTxBuf[0]); DHEX(mTxBuf[0]);
DBGPRINT(F(" ")); DBGPRINT(F(" "));
@ -383,9 +381,14 @@ class HmRadio : public Radio {
} }
mNrf24->stopListening(); mNrf24->stopListening();
mNrf24->flush_rx();
if(!isRetransmit && (mTxRetries != mTxRetriesNext)) {
mNrf24->setRetries(3, mTxRetriesNext);
mTxRetries = mTxRetriesNext;
}
mNrf24->setChannel(mRfChLst[mTxChIdx]); mNrf24->setChannel(mRfChLst[mTxChIdx]);
mNrf24->openWritingPipe(reinterpret_cast<uint8_t*>(&iv->radioId.u64)); 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(); mMillis = millis();
mLastIv = iv; mLastIv = iv;
@ -393,15 +396,15 @@ class HmRadio : public Radio {
mNRFisInRX = false; mNRFisInRX = false;
} }
uint64_t getIvId(Inverter<> *iv) { uint64_t getIvId(Inverter<> *iv) const override {
return iv->radioId.u64; return iv->radioId.u64;
} }
uint8_t getIvGen(Inverter<> *iv) { uint8_t getIvGen(Inverter<> *iv) const override {
return iv->ivGen; 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++) { for(uint8_t i = 1; i < 5; i++) {
if(buf[i] != iv->radioId.b[i]) if(buf[i] != iv->radioId.b[i])
return false; return false;
@ -409,22 +412,23 @@ class HmRadio : public Radio {
return true; return true;
} }
uint64_t DTU_RADIO_ID; uint64_t mDtuRadioId = 0ULL;
uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz const uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz
uint8_t mTxChIdx = 0; uint8_t mTxChIdx = 0;
uint8_t mRxChIdx = 0; uint8_t mRxChIdx = 0;
uint8_t tempRxChIdx = mRxChIdx; uint8_t tempRxChIdx = 0;
bool mGotLastMsg = false; bool mGotLastMsg = false;
uint32_t mMillis; uint32_t mMillis = 0;
bool tx_ok, tx_fail, rx_ready = false; bool tx_ok = false, tx_fail = false, rx_ready = false;
unsigned long mTimeslotStart = 0; unsigned long mTimeslotStart = 0;
unsigned long mLastIrqTime = 0; unsigned long mLastIrqTime = 0;
bool mNRFloopChannels = false; bool mNRFloopChannels = false;
bool mNRFisInRX = false; bool mNRFisInRX = false;
bool isRxInit = true; bool isRxInit = true;
bool rxPendular = false; bool mRxPendular = false;
uint32_t innerLoopTimeout = DURATION_LISTEN_MIN; uint32_t innerLoopTimeout = DURATION_LISTEN_MIN;
uint8_t mTxSetupTime = 0; 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<SPIClass> mSpi;
std::unique_ptr<RF24> mNrf24; std::unique_ptr<RF24> mNrf24;

29
src/hm/hmSystem.h

@ -16,8 +16,8 @@ class HmSystem {
HmSystem() {} HmSystem() {}
void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) { void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) {
mInverter[0].timestamp = timestamp; INVERTERTYPE::timestamp = timestamp;
mInverter[0].generalConfig = config; INVERTERTYPE::generalConfig = config;
//mInverter[0].app = app; //mInverter[0].app = app;
} }
@ -35,6 +35,8 @@ class HmSystem {
case 0x21: iv->type = INV_TYPE_1CH; case 0x21: iv->type = INV_TYPE_1CH;
break; break;
case 0x25: // HMS-400 - 1 channel but payload like 2ch
case 0x44: // HMS-1000 case 0x44: // HMS-1000
case 0x42: case 0x42:
case 0x41: iv->type = INV_TYPE_2CH; case 0x41: iv->type = INV_TYPE_2CH;
@ -51,15 +53,14 @@ class HmSystem {
} }
if(iv->config->serial.b[5] == 0x11) { if(iv->config->serial.b[5] == 0x11) {
if((iv->config->serial.b[4] & 0x0f) == 0x04) { if(((iv->config->serial.b[4] & 0x0f) == 0x04) || ((iv->config->serial.b[4] & 0x0f) == 0x05)) {
iv->ivGen = IV_HMS; iv->ivGen = IV_HMS;
iv->ivRadioType = INV_RADIO_TYPE_CMT; iv->ivRadioType = INV_RADIO_TYPE_CMT;
} else { } else {
iv->ivGen = IV_HM; iv->ivGen = IV_HM;
iv->ivRadioType = INV_RADIO_TYPE_NRF; 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; iv->ivGen = IV_HM;
iv->ivRadioType = INV_RADIO_TYPE_NRF; iv->ivRadioType = INV_RADIO_TYPE_NRF;
} else { // MI 2nd Gen } else { // MI 2nd Gen
@ -69,6 +70,7 @@ class HmSystem {
} else if(iv->config->serial.b[5] == 0x13) { } else if(iv->config->serial.b[5] == 0x13) {
iv->ivGen = IV_HMT; iv->ivGen = IV_HMT;
iv->type = INV_TYPE_6CH; iv->type = INV_TYPE_6CH;
iv->ivRadioType = INV_RADIO_TYPE_CMT;
} else if(iv->config->serial.u64 != 0ULL) { } else if(iv->config->serial.u64 != 0ULL) {
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); DPRINTLN(DBG_ERROR, F("inverter type can't be detected!"));
return; return;
@ -81,7 +83,7 @@ class HmSystem {
DPRINT(DBG_INFO, "added inverter "); DPRINT(DBG_INFO, "added inverter ");
if(iv->config->serial.b[5] == 0x11) { if(iv->config->serial.b[5] == 0x11) {
if((iv->config->serial.b[4] & 0x0f) == 0x04) if(((iv->config->serial.b[4] & 0x0f) == 0x04) || ((iv->config->serial.b[4] & 0x0f) == 0x05))
DBGPRINT("HMS"); DBGPRINT("HMS");
else else
DBGPRINT("HM"); DBGPRINT("HM");
@ -92,34 +94,31 @@ class HmSystem {
DBGPRINTLN(String(iv->config->serial.u64, HEX)); DBGPRINTLN(String(iv->config->serial.u64, HEX));
if((iv->config->serial.b[5] == 0x10) && ((iv->config->serial.b[4] & 0x03) == 0x01)) if(IV_MI == iv->ivGen)
DPRINTLN(DBG_WARN, F("MI Inverter, has some restrictions!")); DPRINTLN(DBG_WARN, F("MI Inverter, has some restrictions!"));
cb(iv); cb(iv);
} }
INVERTERTYPE *findInverter(uint8_t buf[]) { INVERTERTYPE *findInverter(const uint8_t buf[]) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter"));
INVERTERTYPE *p;
for(uint8_t i = 0; i < MAX_INVERTER; i++) { for(uint8_t i = 0; i < MAX_INVERTER; i++) {
p = &mInverter[i]; INVERTERTYPE *p = &mInverter[i];
if((p->config->serial.b[3] == buf[0]) if((p->config->serial.b[3] == buf[0])
&& (p->config->serial.b[2] == buf[1]) && (p->config->serial.b[2] == buf[1])
&& (p->config->serial.b[1] == buf[2]) && (p->config->serial.b[1] == buf[2])
&& (p->config->serial.b[0] == buf[3])) && (p->config->serial.b[0] == buf[3]))
return p; return p;
} }
return NULL; return nullptr;
} }
INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) { INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos"));
if(pos >= MAX_INVERTER) if(pos >= MAX_INVERTER)
return NULL; return nullptr;
else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check)) else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check))
return &mInverter[pos]; return &mInverter[pos];
else else
return NULL; return nullptr;
} }
uint8_t getNumInverters(void) { uint8_t getNumInverters(void) {

2
src/hm/nrfHal.h

@ -142,7 +142,7 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
} }
uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t len) override { 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; data[0] = cmd;
if(len > NRF_MAX_TRANSFER_SZ) if(len > NRF_MAX_TRANSFER_SZ)
len = NRF_MAX_TRANSFER_SZ; len = NRF_MAX_TRANSFER_SZ;

39
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 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -11,6 +11,8 @@
#define ALL_FRAMES 0x80 #define ALL_FRAMES 0x80
#define SINGLE_FRAME 0x81 #define SINGLE_FRAME 0x81
#include <array>
#include <atomic>
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include "../utils/crc.h" #include "../utils/crc.h"
#include "../utils/timemonitor.h" #include "../utils/timemonitor.h"
@ -27,10 +29,13 @@ class Radio {
virtual void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) = 0; 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 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 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 bool loop(void) = 0;
virtual uint8_t getARC(void) { return 0xff; }
virtual uint8_t getPLOS(void) { return 0xff; } Radio() : mTxBuf{} {}
void handleIntr(void) { void handleIntr(void) {
mIrqRcvd = true; mIrqRcvd = true;
@ -66,7 +71,7 @@ class Radio {
sendPacket(iv, 24, isRetransmit); sendPacket(iv, 24, isRetransmit);
} }
uint32_t getDTUSn(void) { uint32_t getDTUSn(void) const {
return mDtuSn; return mDtuSn;
} }
@ -78,11 +83,13 @@ class Radio {
std::queue<packet_t> mBufCtrl; std::queue<packet_t> mBufCtrl;
uint8_t mIrqOk = IRQ_UNKNOWN; uint8_t mIrqOk = IRQ_UNKNOWN;
TimeMonitor mRadioWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state) 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: protected:
virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0; virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0;
virtual uint64_t getIvId(Inverter<> *iv) = 0; virtual uint64_t getIvId(Inverter<> *iv) const = 0;
virtual uint8_t getIvGen(Inverter<> *iv) = 0; virtual uint8_t getIvGen(Inverter<> *iv) const = 0;
void initPacket(uint64_t ivId, uint8_t mid, uint8_t pid) { void initPacket(uint64_t ivId, uint8_t mid, uint8_t pid) {
mTxBuf[0] = mid; mTxBuf[0] = mid;
@ -103,7 +110,7 @@ class Radio {
mTxBuf[(*len)++] = (crc ) & 0xff; mTxBuf[(*len)++] = (crc ) & 0xff;
} }
// crc over all // crc over all
mTxBuf[*len] = ah::crc8(mTxBuf, *len); mTxBuf[*len] = ah::crc8(mTxBuf.data(), *len);
(*len)++; (*len)++;
} }
@ -115,21 +122,21 @@ class Radio {
chipID = ESP.getChipId(); chipID = ESP.getChipId();
#endif #endif
uint8_t t; mDtuSn = 0;
for(int i = 0; i < (7 << 2); i += 4) { for(int i = 0; i < (7 << 2); i += 4) {
t = (chipID >> i) & 0x0f; uint8_t t = (chipID >> i) & 0x0f;
if(t > 0x09) if(t > 0x09)
t -= 6; t -= 6;
mDtuSn |= (t << i); 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 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; protected:
volatile bool mIrqRcvd; uint32_t mDtuSn = 0;
bool *mSerialDebug, *mPrivacyMode, *mPrintWholeTrace; std::atomic<bool> mIrqRcvd = false;
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE]; bool *mSerialDebug = nullptr, *mPrivacyMode = nullptr, *mPrintWholeTrace = nullptr;
uint8_t mFramesExpected = 0x0c; std::array<uint8_t, MAX_RF_PAYLOAD_SIZE> mTxBuf;
}; };
#endif /*__RADIO_H__*/ #endif /*__RADIO_H__*/

6
src/hm/simulator.h

@ -118,9 +118,9 @@ class Simulator {
} }
private: private:
HMSYSTEM *mSys; HMSYSTEM *mSys = nullptr;
uint8_t mIvId; uint8_t mIvId = 0;
uint32_t *mTimestamp; uint32_t *mTimestamp = nullptr;
payloadListenerType mCbPayload = nullptr; payloadListenerType mCbPayload = nullptr;
uint8_t payloadCtrl = 0; uint8_t payloadCtrl = 0;

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 // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -12,8 +12,23 @@
#include "esp32_3wSpi.h" #include "esp32_3wSpi.h"
#endif #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_CFG_RETAIN 0x10
#define CMT2300A_MASK_RSTN_IN_EN 0x20 #define CMT2300A_MASK_RSTN_IN_EN 0x20
#define CMT2300A_MASK_LOCKING_EN 0x20 #define CMT2300A_MASK_LOCKING_EN 0x20
@ -152,67 +167,6 @@
#define CMT2300A_MASK_TX_DONE_FLG 0x08 #define CMT2300A_MASK_TX_DONE_FLG 0x08
#define CMT2300A_MASK_PKT_OK_FLG 0x01 #define CMT2300A_MASK_PKT_OK_FLG 0x01
// 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 { class Cmt2300a {
public: public:
Cmt2300a() {} Cmt2300a() {}
@ -234,12 +188,12 @@ class Cmt2300a {
} }
} }
uint8_t goRx(void) { CmtStatus goRx(void) {
if(mTxPending) if(mTxPending)
return CMT_ERR_TX_PENDING; return CmtStatus::ERR_TX_PENDING;
if(mInRxMode) if(mInRxMode)
return CMT_SUCCESS; return CmtStatus::SUCCESS;
mSpi.readReg(CMT2300A_CUS_INT1_CTL); mSpi.readReg(CMT2300A_CUS_INT1_CTL);
mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE); 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 mSpi.writeReg(0x16, 0x0C); // [4:3]: RSSI_DET_SEL, [2:0]: RSSI_AVG_MODE
if(!cmtSwitchStatus(CMT2300A_GO_RX, CMT2300A_STA_RX)) if(!cmtSwitchStatus(CMT2300A_GO_RX, CMT2300A_STA_RX))
return CMT_ERR_SWITCH_STATE; return CmtStatus::ERR_SWITCH_STATE;
mInRxMode = true; 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) if(mTxPending)
return CMT_ERR_TX_PENDING; return CmtStatus::ERR_TX_PENDING;
if(0x1b != (mSpi.readReg(CMT2300A_CUS_INT_FLAG) & 0x1b)) if(0x1b != (mSpi.readReg(CMT2300A_CUS_INT_FLAG) & 0x1b))
return CMT_FIFO_EMPTY; return CmtStatus::FIFO_EMPTY;
// receive ok (pream, sync, node, crc) // receive ok (pream, sync, node, crc)
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return CMT_ERR_SWITCH_STATE; return CmtStatus::ERR_SWITCH_STATE;
mSpi.readFifo(buf, rxLen, maxlen); mSpi.readFifo(buf, rxLen, maxlen);
*rssi = mSpi.readReg(CMT2300A_CUS_RSSI_DBM) - 128; *rssi = mSpi.readReg(CMT2300A_CUS_RSSI_DBM) - 128;
if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP)) if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP))
return CMT_ERR_SWITCH_STATE; return CmtStatus::ERR_SWITCH_STATE;
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
return CMT_ERR_SWITCH_STATE; return CmtStatus::ERR_SWITCH_STATE;
mInRxMode = false; mInRxMode = false;
mCusIntFlag = mSpi.readReg(CMT2300A_CUS_INT_FLAG); 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) if(mTxPending)
return CMT_ERR_TX_PENDING; return CmtStatus::ERR_TX_PENDING;
if(mInRxMode) { if(mInRxMode) {
mInRxMode = false; mInRxMode = false;
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY)) 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); 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)) if(!cmtSwitchStatus(CMT2300A_GO_TX, CMT2300A_STA_TX))
return CMT_ERR_SWITCH_STATE; return CmtStatus::ERR_SWITCH_STATE;
// wait for tx done // wait for tx done
mTxPending = true; mTxPending = true;
return CMT_SUCCESS; return CmtStatus::SUCCESS;
} }
// initialize CMT2300A, returns true on success // initialize CMT2300A, returns true on success
bool reset(void) { bool reset(RegionCfg region) {
mRegionCfg = region;
mSpi.writeReg(0x7f, 0xff); // soft reset mSpi.writeReg(0x7f, 0xff); // soft reset
delay(30); delay(30);
@ -346,9 +301,18 @@ class Cmt2300a {
if(mSpi.readReg(0x62) != 0x20) if(mSpi.readReg(0x62) != 0x20)
return false; // not connected! 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]); 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 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) { inline uint8_t freq2Chan(const uint32_t freqKhz) {
if((freqKhz % FREQ_STEP_KHZ) != 0) { if((freqKhz % FREQ_STEP_KHZ) != 0)
DPRINT(DBG_WARN, F("switch frequency to "));
DBGPRINT(String(freqKhz));
DBGPRINT(F("kHz not possible!"));
return 0xff; // error 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 return 0xff; // error
if((freqKhz < FREQ_WARN_MIN_KHZ) || (freqKhz > FREQ_WARN_MAX_KHZ)) return (freqKhz - (getBaseFreqMhz() * 1000)) / FREQ_STEP_KHZ;
DPRINTLN(DBG_WARN, F("Desired frequency is out of EU legal range! (863 - 870MHz)"));
return (freqKhz - HOY_BASE_FREQ_KHZ) / FREQ_STEP_KHZ;
} }
inline void switchChannel(uint8_t ch) { inline void switchChannel(uint8_t ch) {
@ -414,9 +369,9 @@ class Cmt2300a {
inline uint32_t getFreqKhz(void) { inline uint32_t getFreqKhz(void) {
if(0xff != mRqstCh) if(0xff != mRqstCh)
return HOY_BASE_FREQ_KHZ + (mRqstCh * FREQ_STEP_KHZ); return getBaseFreqMhz() * 1000 + (mRqstCh * FREQ_STEP_KHZ);
else else
return HOY_BASE_FREQ_KHZ + (mCurCh * FREQ_STEP_KHZ); return getBaseFreqMhz() * 1000 + (mCurCh * FREQ_STEP_KHZ);
} }
uint8_t getCurrentChannel(void) { uint8_t getCurrentChannel(void) {
@ -443,6 +398,114 @@ class Cmt2300a {
mSpi.writeReg(CMT2300A_CUS_TX9, paLevelList[level][1]); 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: private:
void init() { void init() {
mTxPending = false; mTxPending = false;
@ -466,7 +529,8 @@ class Cmt2300a {
return false; 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); uint8_t toCh = freq2Chan(freqKhz);
if(0xff == toCh) if(0xff == toCh)
return false; return false;
@ -474,22 +538,24 @@ class Cmt2300a {
switchChannel(toCh); switchChannel(toCh);
return true; return true;
} }*/
inline uint8_t getChipStatus(void) { inline uint8_t getChipStatus(void) {
return mSpi.readReg(CMT2300A_CUS_MODE_STA) & CMT2300A_MASK_CHIP_MODE_STA; return mSpi.readReg(CMT2300A_CUS_MODE_STA) & CMT2300A_MASK_CHIP_MODE_STA;
} }
private:
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
cmtHal mSpi; cmtHal mSpi;
#else #else
esp32_3wSpi mSpi; esp32_3wSpi mSpi;
#endif #endif
uint8_t mCnt; uint8_t mCnt = 0;
bool mTxPending; bool mTxPending = false;
bool mInRxMode; bool mInRxMode = false;
uint8_t mCusIntFlag; uint8_t mCusIntFlag = 0;
uint8_t mRqstCh, mCurCh; uint8_t mRqstCh = 0, mCurCh = 0;
RegionCfg mRegionCfg = RegionCfg::EUROPE;
}; };
#endif /*__CMT2300A_H__*/ #endif /*__CMT2300A_H__*/

2
src/hms/cmtHal.h

@ -89,7 +89,7 @@ class cmtHal : public SpiPatcherHandle {
} }
uint8_t readReg(uint8_t addr) { uint8_t readReg(uint8_t addr) {
uint8_t data; uint8_t data = 0;
request_spi(); request_spi();

7
src/hms/esp32_3wSpi.h

@ -10,6 +10,7 @@
#if defined(ESP32) #if defined(ESP32)
#include "driver/spi_master.h" #include "driver/spi_master.h"
#include "esp_rom_gpio.h" // for esp_rom_gpio_connect_out_signal #include "esp_rom_gpio.h" // for esp_rom_gpio_connect_out_signal
#include "../config/config.h"
#define SPI_CLK 1 * 1000 * 1000 // 1MHz #define SPI_CLK 1 * 1000 * 1000 // 1MHz
@ -104,7 +105,7 @@ class esp32_3wSpi {
if(!mInitialized) if(!mInitialized)
return 0; return 0;
uint8_t rx_data; uint8_t rx_data = 0;
spi_transaction_t t = { spi_transaction_t t = {
.cmd = 0, .cmd = 0,
.addr = (uint64_t)(~addr), .addr = (uint64_t)(~addr),
@ -121,7 +122,7 @@ class esp32_3wSpi {
return rx_data; return rx_data;
} }
void writeFifo(uint8_t buf[], uint8_t len) { void writeFifo(const uint8_t buf[], uint8_t len) {
if(!mInitialized) if(!mInitialized)
return; return;
uint8_t tx_data; uint8_t tx_data;
@ -144,7 +145,7 @@ class esp32_3wSpi {
void readFifo(uint8_t buf[], uint8_t *len, uint8_t maxlen) { void readFifo(uint8_t buf[], uint8_t *len, uint8_t maxlen) {
if(!mInitialized) if(!mInitialized)
return; return;
uint8_t rx_data; uint8_t rx_data = 0;
spi_transaction_t t = { spi_transaction_t t = {
.length = 8, .length = 8,

81
src/hms/hmsRadio.h

@ -9,40 +9,38 @@
#include "cmt2300a.h" #include "cmt2300a.h"
#include "../hm/radio.h" #include "../hm/radio.h"
//#define CMT_SWITCH_CHANNEL_CYCLE 5
template<uint32_t DTU_SN = 0x81001765> template<uint32_t DTU_SN = 0x81001765>
class CmtRadio : public Radio { class CmtRadio : public Radio {
typedef Cmt2300a CmtType; typedef Cmt2300a CmtType;
public: public:
CmtRadio() { 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) {
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) {
mCmt.setup(pinSclk, pinSdio, pinCsb, pinFcsb); mCmt.setup(pinSclk, pinSdio, pinCsb, pinFcsb);
reset(genDtuSn); reset(genDtuSn, static_cast<RegionCfg>(region));
mPrivacyMode = privacyMode; mPrivacyMode = privacyMode;
mSerialDebug = serialDebug; mSerialDebug = serialDebug;
mPrintWholeTrace = printWholeTrace; mPrintWholeTrace = printWholeTrace;
mTxBuf.fill(0);
} }
bool loop() { bool loop() override {
mCmt.loop(); mCmt.loop();
if((!mIrqRcvd) && (!mRqstGetRx)) if((!mIrqRcvd) && (!mRqstGetRx))
return false; return false;
getRx(); getRx();
if(CMT_SUCCESS == mCmt.goRx()) { if(CmtStatus::SUCCESS == mCmt.goRx()) {
mIrqRcvd = false; mIrqRcvd = false;
mRqstGetRx = false; mRqstGetRx = false;
} }
return false; return false;
} }
bool isChipConnected(void) { bool isChipConnected(void) const override {
return mCmtAvail; 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: ")); DPRINT(DBG_INFO, F("sendControlPacket cmd: "));
DBGHEXLN(cmd); DBGHEXLN(cmd);
initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME); initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME);
@ -60,14 +58,14 @@ class CmtRadio : public Radio {
sendPacket(iv, cnt, isRetransmit); 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 fromCh = mCmt.freq2Chan(fromkHz);
uint8_t toCh = mCmt.freq2Chan(tokHz); uint8_t toCh = mCmt.freq2Chan(tokHz);
return switchFrequencyCh(iv, fromCh, toCh); 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)) if((0xff == fromCh) || (0xff == toCh))
return false; return false;
@ -77,9 +75,21 @@ class CmtRadio : public Radio {
return true; 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: 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 // inverters have maybe different settings regarding frequency
if(mCmt.getCurrentChannel() != iv->config->frequency) if(mCmt.getCurrentChannel() != iv->config->frequency)
mCmt.switchChannel(iv->config->frequency); mCmt.switchChannel(iv->config->frequency);
@ -93,9 +103,9 @@ class CmtRadio : public Radio {
DBGPRINT(F("Mhz | ")); DBGPRINT(F("Mhz | "));
if(*mPrintWholeTrace) { if(*mPrintWholeTrace) {
if(*mPrivacyMode) if(*mPrivacyMode)
ah::dumpBuf(mTxBuf, len, 1, 4); ah::dumpBuf(mTxBuf.data(), len, 1, 4);
else else
ah::dumpBuf(mTxBuf, len); ah::dumpBuf(mTxBuf.data(), len);
} else { } else {
DHEX(mTxBuf[0]); DHEX(mTxBuf[0]);
DBGPRINT(F(" ")); DBGPRINT(F(" "));
@ -105,29 +115,29 @@ class CmtRadio : public Radio {
} }
} }
uint8_t status = mCmt.tx(mTxBuf, len); CmtStatus status = mCmt.tx(mTxBuf.data(), len);
mMillis = millis(); mMillis = millis();
if(CMT_SUCCESS != status) { if(CmtStatus::SUCCESS != status) {
DPRINT(DBG_WARN, F("CMT TX failed, code: ")); DPRINT(DBG_WARN, F("CMT TX failed, code: "));
DBGPRINTLN(String(status)); DBGPRINTLN(String(static_cast<uint8_t>(status)));
if(CMT_ERR_RX_IN_FIFO == status) if(CmtStatus::ERR_RX_IN_FIFO == status)
mIrqRcvd = true; mIrqRcvd = true;
} }
iv->mDtuTxCnt++; iv->mDtuTxCnt++;
} }
uint64_t getIvId(Inverter<> *iv) { uint64_t getIvId(Inverter<> *iv) const override {
return iv->radioId.u64; return iv->radioId.u64;
} }
uint8_t getIvGen(Inverter<> *iv) { uint8_t getIvGen(Inverter<> *iv) const override {
return iv->ivGen; return iv->ivGen;
} }
inline void reset(bool genDtuSn) { inline void reset(bool genDtuSn, RegionCfg region) {
if(genDtuSn) if(genDtuSn)
generateDtuSn(); generateDtuSn();
if(!mCmt.reset()) { if(!mCmt.reset(region)) {
mCmtAvail = false; mCmtAvail = false;
DPRINTLN(DBG_WARN, F("Initializing CMT2300A failed!")); DPRINTLN(DBG_WARN, F("Initializing CMT2300A failed!"));
} else { } else {
@ -140,6 +150,10 @@ class CmtRadio : public Radio {
} }
inline void sendSwitchChCmd(Inverter<> *iv, uint8_t ch) { inline void sendSwitchChCmd(Inverter<> *iv, uint8_t ch) {
//if(CMT_SWITCH_CHANNEL_CYCLE > ++mSwitchCycle)
// return;
//mSwitchCycle = 0;
/** ch: /** ch:
* 0x00: 860.00 MHz * 0x00: 860.00 MHz
* 0x01: 860.25 MHz * 0x01: 860.25 MHz
@ -161,20 +175,23 @@ class CmtRadio : public Radio {
inline void getRx(void) { inline void getRx(void) {
packet_t p; packet_t p;
p.millis = millis() - mMillis; p.millis = millis() - mMillis;
uint8_t status = mCmt.getRx(p.packet, &p.len, 28, &p.rssi); if(CmtStatus::SUCCESS == mCmt.getRx(p.packet, &p.len, 28, &p.rssi)) {
if(CMT_SUCCESS == status) //mSwitchCycle = 0;
p.ch = 0; // not used for CMT inverters
mBufCtrl.push(p); mBufCtrl.push(p);
}
// this code completly stops communication! if(p.packet[9] > ALL_FRAMES) { // indicates last frame
//if(p.packet[9] > ALL_FRAMES) // indicates last frame setExpectedFrames(p.packet[9] - ALL_FRAMES);
// mRadioWaitTime.stopTimeMonitor(); // we got everything we expected and can exit rx mode... mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first get back to rx mode?
//optionally instead: mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first get back to rx mode? }
} }
CmtType mCmt; CmtType mCmt;
bool mCmtAvail; bool mCmtAvail = false;
bool mRqstGetRx = false; bool mRqstGetRx = false;
uint32_t mMillis; uint32_t mMillis = 0;
//uint8_t mSwitchCycle = 0;
}; };
#endif /*__HMS_RADIO_H__*/ #endif /*__HMS_RADIO_H__*/

6
src/platformio.ini

@ -28,7 +28,7 @@ lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer https://github.com/yubox-node-org/ESPAsyncWebServer
https://github.com/nRF24/RF24 @ 1.4.8 https://github.com/nRF24/RF24 @ 1.4.8
paulstoffregen/Time @ ^1.6.1 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 bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4 https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9 olikraus/U8g2 @ ^2.35.9
@ -394,7 +394,7 @@ lib_deps =
khoih-prog/AsyncUDP_ESP32_W5500 khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8 https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1 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 bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4 https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9 olikraus/U8g2 @ ^2.35.9
@ -439,7 +439,7 @@ lib_deps =
khoih-prog/AsyncUDP_ESP32_W5500 khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8 https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1 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 bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4 https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9 olikraus/U8g2 @ ^2.35.9

31
src/plugins/Display/Display.h

@ -192,15 +192,14 @@ class Display {
if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF)) { if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF)) {
#if defined(ESP8266) #if defined(ESP8266)
if (mCfg->pirPin == A0) if (mCfg->pirPin == A0)
return((analogRead(A0) >= 512)); return (analogRead(A0) >= 512);
else else
return(digitalRead(mCfg->pirPin)); return digitalRead(mCfg->pirPin);
#elif defined(ESP32) #elif defined(ESP32)
return(digitalRead(mCfg->pirPin)); return digitalRead(mCfg->pirPin);
#endif #endif
} } else
else return false;
return(false);
} }
// approximate RSSI in dB by invQuality levels from heuristic function (very unscientific but better than nothing :-) ) // approximate RSSI in dB by invQuality levels from heuristic function (very unscientific but better than nothing :-) )
@ -224,21 +223,21 @@ class Display {
} }
// private member variables // private member variables
IApp *mApp; IApp *mApp = nullptr;
DisplayData mDisplayData; DisplayData mDisplayData;
bool mNewPayload; bool mNewPayload = false;
uint8_t mLoopCnt; uint8_t mLoopCnt = 0;
uint32_t *mUtcTs; uint32_t *mUtcTs = nullptr;
display_t *mCfg; display_t *mCfg = nullptr;
HMSYSTEM *mSys; HMSYSTEM *mSys = nullptr;
RADIO *mHmRadio; RADIO *mHmRadio = nullptr;
RADIO *mHmsRadio; RADIO *mHmsRadio = nullptr;
uint16_t mRefreshCycle; uint16_t mRefreshCycle = 0;
#if defined(ESP32) && !defined(ETHERNET) #if defined(ESP32) && !defined(ETHERNET)
DisplayEPaper mEpaper; DisplayEPaper mEpaper;
#endif #endif
DisplayMono *mMono; DisplayMono *mMono = nullptr;
}; };
#endif /*PLUGIN_DISPLAY*/ #endif /*PLUGIN_DISPLAY*/

12
src/plugins/Display/Display_Mono.h

@ -24,8 +24,6 @@
class DisplayMono { class DisplayMono {
public: public:
DisplayMono() {};
virtual void init(DisplayData *displayData) = 0; virtual void init(DisplayData *displayData) = 0;
virtual void config(display_t *cfg) = 0; virtual void config(display_t *cfg) = 0;
virtual void disp(void) = 0; virtual void disp(void) = 0;
@ -289,11 +287,11 @@ class DisplayMono {
DispSwitchState mDispSwitchState = DispSwitchState::TEXT; DispSwitchState mDispSwitchState = DispSwitchState::TEXT;
uint16_t mDispWidth; uint16_t mDispWidth;
uint8_t mExtra; uint8_t mExtra = 0;
int8_t mPixelshift=0; int8_t mPixelshift=0;
char mFmtText[DISP_FMT_TEXT_LEN]; char mFmtText[DISP_FMT_TEXT_LEN];
uint8_t mLineXOffsets[5] = {}; uint8_t mLineXOffsets[5] = {0, 0, 0, 0, 0};
uint8_t mLineYOffsets[5] = {}; uint8_t mLineYOffsets[5] = {0, 0, 0, 0, 0};
uint8_t mPgWidth = 0; uint8_t mPgWidth = 0;
@ -308,8 +306,8 @@ class DisplayMono {
uint32_t mPgLastTime = 0; uint32_t mPgLastTime = 0;
PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC; PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC;
uint16_t mDispHeight; uint16_t mDispHeight = 0;
uint8_t mLuminance; uint8_t mLuminance = 0;
TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true); TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true);
TimeMonitor mDispSwitchTime = TimeMonitor(); TimeMonitor mDispSwitchTime = TimeMonitor();

6
src/plugins/Display/Display_Mono_128X32.h

@ -12,11 +12,11 @@ class DisplayMono128X32 : public DisplayMono {
mExtra = 0; mExtra = 0;
} }
void config(display_t *cfg) { void config(display_t *cfg) override {
mCfg = cfg; mCfg = cfg;
} }
void init(DisplayData *displayData) { void init(DisplayData *displayData) override {
u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0); 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); monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
calcLinePositions(); calcLinePositions();
@ -26,7 +26,7 @@ class DisplayMono128X32 : public DisplayMono {
mDisplay->sendBuffer(); mDisplay->sendBuffer();
} }
void disp(void) { void disp(void) override {
mDisplay->clearBuffer(); mDisplay->clearBuffer();
// calculate current pixelshift for pixelshift screensaver // calculate current pixelshift for pixelshift screensaver

39
src/plugins/Display/Display_Mono_128X64.h

@ -13,11 +13,11 @@ class DisplayMono128X64 : public DisplayMono {
mExtra = 0; mExtra = 0;
} }
void config(display_t *cfg) { void config(display_t *cfg) override {
mCfg = cfg; mCfg = cfg;
} }
void init(DisplayData *displayData) { void init(DisplayData *displayData) override {
u8g2_cb_t *rot = (u8g2_cb_t *)(( mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0); u8g2_cb_t *rot = (u8g2_cb_t *)(( mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
switch (mCfg->type) { switch (mCfg->type) {
case DISP_TYPE_T1_SSD1306_128X64: case DISP_TYPE_T1_SSD1306_128X64:
@ -68,9 +68,7 @@ class DisplayMono128X64 : public DisplayMono {
mDisplay->sendBuffer(); mDisplay->sendBuffer();
} }
void disp(void) { void disp(void) override {
uint8_t pos, sun_pos, moon_pos;
mDisplay->clearBuffer(); mDisplay->clearBuffer();
// Layout-Test // Layout-Test
@ -106,8 +104,8 @@ class DisplayMono128X64 : public DisplayMono {
} }
// print status of inverters // print status of inverters
else { else {
sun_pos = -1; int8_t sun_pos = -1;
moon_pos = -1; int8_t moon_pos = -1;
setLineFont(l_Status); setLineFont(l_Status);
if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
@ -128,11 +126,11 @@ class DisplayMono128X64 : public DisplayMono {
} }
printText(mFmtText, l_Status, 0xff); printText(mFmtText, l_Status, 0xff);
pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2; uint8_t pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2;
mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy); mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy);
if (sun_pos!=-1) if (sun_pos != -1)
mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol
if (moon_pos!=-1) if (moon_pos != -1)
mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol
} }
} }
@ -181,12 +179,11 @@ class DisplayMono128X64 : public DisplayMono {
// draw dynamic RSSI bars // draw dynamic RSSI bars
int rssi_bar_height = 9; int rssi_bar_height = 9;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
int radio_rssi_threshold = -60 - i * 10; int rssi_threshold = -60 - i * 10;
int wifi_rssi_threshold = -60 - i * 10;
uint8_t barwidth = std::min(4 - i, 3); uint8_t barwidth = std::min(4 - i, 3);
if (mDisplayData->RadioRSSI > radio_rssi_threshold) if (mDisplayData->RadioRSSI > rssi_threshold)
mDisplay->drawBox(widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); mDisplay->drawBox(widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
if (mDisplayData->WifiRSSI > wifi_rssi_threshold) if (mDisplayData->WifiRSSI > rssi_threshold)
mDisplay->drawBox(mDispWidth - barwidth - widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); mDisplay->drawBox(mDispWidth - barwidth - widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
} }
// draw dynamic antenna and WiFi symbols // draw dynamic antenna and WiFi symbols
@ -223,23 +220,22 @@ class DisplayMono128X64 : public DisplayMono {
l_MAX_LINES = 5, l_MAX_LINES = 5,
}; };
uint8_t graph_first_line; uint8_t graph_first_line = 0;
uint8_t graph_last_line; uint8_t graph_last_line = 0;
const uint8_t pixelShiftRange = 11; // number of pixels to shift from left to right (centered -> must be odd!) const uint8_t pixelShiftRange = 11; // number of pixels to shift from left to right (centered -> must be odd!)
uint8_t widthShrink; uint8_t widthShrink = 0;
void calcLinePositions() { void calcLinePositions() {
uint8_t yOff = 0; uint8_t yOff = 0;
uint8_t i = 0; uint8_t i = 0;
uint8_t asc, dsc;
do { do {
setLineFont(i); setLineFont(i);
asc = mDisplay->getAscent(); uint8_t asc = mDisplay->getAscent();
yOff += asc; yOff += asc;
mLineYOffsets[i] = yOff; mLineYOffsets[i] = yOff;
dsc = mDisplay->getDescent(); uint8_t dsc = mDisplay->getDescent();
yOff -= dsc; yOff -= dsc;
if (l_Time == i) // prevent time and status line to touch if (l_Time == i) // prevent time and status line to touch
yOff++; // -> one pixels space yOff++; // -> one pixels space
@ -248,8 +244,7 @@ class DisplayMono128X64 : public DisplayMono {
} }
inline void setLineFont(uint8_t line) { inline void setLineFont(uint8_t line) {
if ((line == l_TotalPower) || if (line == l_TotalPower) // || (line == l_Ahoy) -> l_TotalPower == l_Ahoy == 2
(line == l_Ahoy))
mDisplay->setFont(u8g2_font_ncenB14_tr); mDisplay->setFont(u8g2_font_ncenB14_tr);
else if ((line == l_YieldDay) || else if ((line == l_YieldDay) ||
(line == l_YieldTotal)) (line == l_YieldTotal))

6
src/plugins/Display/Display_Mono_64X48.h

@ -12,11 +12,11 @@ class DisplayMono64X48 : public DisplayMono {
mExtra = 0; mExtra = 0;
} }
void config(display_t *cfg) { void config(display_t *cfg) override {
mCfg = cfg; mCfg = cfg;
} }
void init(DisplayData *displayData) { void init(DisplayData *displayData) override {
u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0); 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 // Wemos OLed Shield is not defined in u8 lib -> use nearest compatible
monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData); monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
@ -28,7 +28,7 @@ class DisplayMono64X48 : public DisplayMono {
mDisplay->sendBuffer(); mDisplay->sendBuffer();
} }
void disp(void) { void disp(void) override {
mDisplay->clearBuffer(); mDisplay->clearBuffer();
// calculate current pixelshift for pixelshift screensaver // calculate current pixelshift for pixelshift screensaver

27
src/plugins/Display/Display_Mono_84X48.h

@ -12,11 +12,11 @@ class DisplayMono84X48 : public DisplayMono {
mExtra = 0; mExtra = 0;
} }
void config(display_t *cfg) { void config(display_t *cfg) override {
mCfg = cfg; mCfg = cfg;
} }
void init(DisplayData *displayData) { void init(DisplayData *displayData) override {
u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0); 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); monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, mCfg->disp_clk, mCfg->disp_data, mCfg->disp_cs, mCfg->disp_dc, 0xff), displayData);
@ -55,7 +55,7 @@ class DisplayMono84X48 : public DisplayMono {
mDisplay->sendBuffer(); mDisplay->sendBuffer();
} }
void disp(void) { void disp(void) override {
mDisplay->clearBuffer(); mDisplay->clearBuffer();
// Layout-Test // Layout-Test
@ -143,12 +143,11 @@ class DisplayMono84X48 : public DisplayMono {
// draw dynamic RSSI bars // draw dynamic RSSI bars
int rssi_bar_height = 7; int rssi_bar_height = 7;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
int radio_rssi_threshold = -60 - i * 10; int rssi_threshold = -60 - i * 10;
int wifi_rssi_threshold = -60 - i * 10;
uint8_t barwidth = std::min(4 - i, 3); uint8_t barwidth = std::min(4 - i, 3);
if (mDisplayData->RadioRSSI > radio_rssi_threshold) if (mDisplayData->RadioRSSI > rssi_threshold)
mDisplay->drawBox(0, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); mDisplay->drawBox(0, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
if (mDisplayData->WifiRSSI > wifi_rssi_threshold) if (mDisplayData->WifiRSSI > rssi_threshold)
mDisplay->drawBox(mDispWidth - barwidth, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height); mDisplay->drawBox(mDispWidth - barwidth, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
} }
@ -184,30 +183,28 @@ class DisplayMono84X48 : public DisplayMono {
l_MAX_LINES = 5, l_MAX_LINES = 5,
}; };
uint8_t graph_first_line; uint8_t graph_first_line = 0;
uint8_t graph_last_line; uint8_t graph_last_line = 0;
void calcLinePositions() { void calcLinePositions() {
uint8_t yOff = 0; uint8_t yOff = 0;
uint8_t i = 0; uint8_t i = 0;
uint8_t asc, dsc;
do { do {
setLineFont(i); setLineFont(i);
asc = mDisplay->getAscent(); uint8_t asc = mDisplay->getAscent();
yOff += asc; yOff += asc;
mLineYOffsets[i] = yOff; mLineYOffsets[i] = yOff;
dsc = mDisplay->getDescent(); uint8_t dsc = mDisplay->getDescent();
if (l_TotalPower != i) // power line needs no descent spacing if (l_TotalPower != i) // power line needs no descent spacing
yOff -= dsc; yOff -= dsc;
yOff++; // instead lets spend one pixel space between all lines yOff++; // instead lets spend one pixel space between all lines
i++; i++;
} while(l_MAX_LINES>i); } while(l_MAX_LINES > i);
} }
inline void setLineFont(uint8_t line) { inline void setLineFont(uint8_t line) {
if ((line == l_TotalPower) || if (line == l_TotalPower) // || (line == l_Ahoy) -> l_TotalPower == l_Ahoy == 2
(line == l_Ahoy))
mDisplay->setFont(u8g2_font_logisoso16_tr); mDisplay->setFont(u8g2_font_logisoso16_tr);
else else
mDisplay->setFont(u8g2_font_5x8_symbols_ahoy); mDisplay->setFont(u8g2_font_5x8_symbols_ahoy);

17
src/plugins/Display/Display_ePaper.cpp

@ -67,7 +67,7 @@ void DisplayEPaper::refreshLoop() {
case RefreshStatus::LOGO: case RefreshStatus::LOGO:
_display->fillScreen(GxEPD_BLACK); _display->fillScreen(GxEPD_BLACK);
_display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
_display->display(false); // full update mSecondCnt = 2;
mNextRefreshState = RefreshStatus::PARTITIALS; mNextRefreshState = RefreshStatus::PARTITIALS;
mRefreshState = RefreshStatus::WAIT; mRefreshState = RefreshStatus::WAIT;
break; break;
@ -79,11 +79,11 @@ void DisplayEPaper::refreshLoop() {
break; break;
case RefreshStatus::WHITE: case RefreshStatus::WHITE:
if(mSecondCnt == 0) { if(0 != mSecondCnt)
_display->fillScreen(GxEPD_WHITE); break;
mNextRefreshState = RefreshStatus::PARTITIALS; _display->fillScreen(GxEPD_WHITE);
mRefreshState = RefreshStatus::WAIT; mNextRefreshState = RefreshStatus::PARTITIALS;
} mRefreshState = RefreshStatus::WAIT;
break; break;
case RefreshStatus::WAIT: case RefreshStatus::WAIT:
@ -92,10 +92,13 @@ void DisplayEPaper::refreshLoop() {
break; break;
case RefreshStatus::PARTITIALS: case RefreshStatus::PARTITIALS:
if(0 != mSecondCnt)
break;
headlineIP(); headlineIP();
versionFooter(); versionFooter();
mSecondCnt = 4; // display Logo time during boot up mSecondCnt = 4; // display Logo time during boot up
mRefreshState = RefreshStatus::DONE; mNextRefreshState = RefreshStatus::DONE;
mRefreshState = RefreshStatus::WAIT;
break; break;
default: // RefreshStatus::DONE default: // RefreshStatus::DONE

49
src/plugins/history.h

@ -24,23 +24,15 @@ template<class HMSYSTEM>
class HistoryData { class HistoryData {
private: private:
struct storage_t { struct storage_t {
uint16_t refreshCycle; uint16_t refreshCycle = 0;
uint16_t loopCnt; uint16_t loopCnt = 0;
uint16_t listIdx; // index for next Element to write into WattArr uint16_t listIdx = 0; // index for next Element to write into WattArr
uint16_t dispIdx; // index for 1st Element to display from WattArr uint16_t dispIdx = 0; // index for 1st Element to display from WattArr
bool wrapped; bool wrapped = false;
// ring buffer for watt history // ring buffer for watt history
std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data; std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data;
void reset() { storage_t() { data.fill(0); }
loopCnt = 0;
listIdx = 0;
dispIdx = 0;
wrapped = false;
for(uint16_t i = 0; i < (HISTORY_DATA_ARR_LENGTH + 1); i++) {
data[i] = 0;
}
}
}; };
public: public:
@ -50,21 +42,18 @@ class HistoryData {
mConfig = config; mConfig = config;
mTs = ts; mTs = ts;
mCurPwr.reset();
mCurPwr.refreshCycle = mConfig->inst.sendInterval; mCurPwr.refreshCycle = mConfig->inst.sendInterval;
mYieldDay.reset(); //mYieldDay.refreshCycle = 60;
mYieldDay.refreshCycle = 60;
} }
void tickerSecond() { void tickerSecond() {
Inverter<> *iv; ;
record_t<> *rec;
float curPwr = 0; float curPwr = 0;
float maxPwr = 0; float maxPwr = 0;
float yldDay = -0.1; float yldDay = -0.1;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
iv = mSys->getInverterByPos(i); Inverter<> *iv = mSys->getInverterByPos(i);
rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL) if (iv == NULL)
continue; continue;
curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec); curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
@ -80,7 +69,7 @@ class HistoryData {
mMaximumDay = roundf(maxPwr); mMaximumDay = roundf(maxPwr);
} }
if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { /*if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) {
if (*mTs > mApp->getSunset()) { if (*mTs > mApp->getSunset()) {
if ((!mDayStored) && (yldDay > 0)) { if ((!mDayStored) && (yldDay > 0)) {
addValue(&mYieldDay, roundf(yldDay)); addValue(&mYieldDay, roundf(yldDay));
@ -88,11 +77,12 @@ class HistoryData {
} }
} else if (*mTs > mApp->getSunrise()) } else if (*mTs > mApp->getSunrise())
mDayStored = false; mDayStored = false;
} }*/
} }
uint16_t valueAt(HistoryStorageType type, uint16_t i) { uint16_t valueAt(HistoryStorageType type, uint16_t i) {
storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; //storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay;
storage_t *s = &mCurPwr;
uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH; uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH;
return s->data[idx]; return s->data[idx];
} }
@ -112,14 +102,13 @@ class HistoryData {
} }
private: private:
IApp *mApp; IApp *mApp = nullptr;
HMSYSTEM *mSys; HMSYSTEM *mSys = nullptr;
settings *mSettings; settings *mSettings = nullptr;
settings_t *mConfig; settings_t *mConfig = nullptr;
uint32_t *mTs; uint32_t *mTs = nullptr;
storage_t mCurPwr; storage_t mCurPwr;
storage_t mYieldDay;
bool mDayStored = false; bool mDayStored = false;
uint16_t mMaximumDay = 0; uint16_t mMaximumDay = 0;
}; };

193
src/publisher/pubMqtt.h

@ -15,6 +15,7 @@
#include <WiFi.h> #include <WiFi.h>
#endif #endif
#include <array>
#if defined(ETHERNET) #if defined(ETHERNET)
#include "../eth/ahoyeth.h" #include "../eth/ahoyeth.h"
#endif #endif
@ -41,13 +42,16 @@ typedef struct {
template<class HMSYSTEM> template<class HMSYSTEM>
class PubMqtt { class PubMqtt {
public: public:
PubMqtt() { PubMqtt() : SendIvData() {
mRxCnt = 0; mLastIvState.fill(InverterStatus::OFF);
mTxCnt = 0; mIvLastRTRpub.fill(0);
mSubscriptionCb = NULL;
memset(mLastIvState, (uint8_t)InverterStatus::OFF, MAX_NUM_INVERTERS); mVal.fill(0);
memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4); mTopic.fill(0);
mLastAnyAvail = false; mSubTopic.fill(0);
mClientId.fill(0);
mLwtTopic.fill(0);
mSendAlarm.fill(false);
} }
~PubMqtt() { } ~PubMqtt() { }
@ -61,22 +65,22 @@ class PubMqtt {
mUptime = uptime; mUptime = uptime;
mIntervalTimeout = 1; mIntervalTimeout = 1;
mSendIvData.setup(sys, utcTs, &mSendList); SendIvData.setup(sys, utcTs, &mSendList);
mSendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained, uint8_t qos) { SendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained, uint8_t qos) {
publish(subTopic, payload, retained, true, qos); publish(subTopic, payload, retained, true, qos);
}); });
mDiscovery.running = false; 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)) if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0))
mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd); mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd);
if(strlen(mCfgMqtt->clientId) > 0) if(strlen(mCfgMqtt->clientId) > 0)
snprintf(mClientId, 23, "%s", mCfgMqtt->clientId); snprintf(mClientId.data(), mClientId.size(), "%s", mCfgMqtt->clientId);
else { else {
snprintf(mClientId, 23, "%s-", mDevName); snprintf(mClientId.data(), mClientId.size(), "%s-", mDevName);
uint8_t pos = strlen(mClientId); uint8_t pos = strlen(mClientId.data());
mClientId[pos++] = WiFi.macAddress().substring( 9, 10).c_str()[0]; 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(10, 11).c_str()[0];
mClientId[pos++] = WiFi.macAddress().substring(12, 13).c_str()[0]; mClientId[pos++] = WiFi.macAddress().substring(12, 13).c_str()[0];
@ -86,16 +90,16 @@ class PubMqtt {
mClientId[pos++] = '\0'; mClientId[pos++] = '\0';
} }
mClient.setClientId(mClientId); mClient.setClientId(mClientId.data());
mClient.setServer(mCfgMqtt->broker, mCfgMqtt->port); 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.onConnect(std::bind(&PubMqtt::onConnect, this, std::placeholders::_1));
mClient.onDisconnect(std::bind(&PubMqtt::onDisconnect, 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)); 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() { void loop() {
mSendIvData.loop(); SendIvData.loop();
#if defined(ESP8266) #if defined(ESP8266)
mClient.loop(); mClient.loop();
@ -129,8 +133,8 @@ class PubMqtt {
} }
void tickerMinute() { void tickerMinute() {
snprintf(mVal, 40, "%d", (*mUptime)); snprintf(mVal.data(), mVal.size(), "%u", (*mUptime));
publish(subtopics[MQTT_UPTIME], mVal); publish(subtopics[MQTT_UPTIME], mVal.data());
publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str()); publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str());
publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str()); publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str());
#ifndef ESP32 #ifndef ESP32
@ -147,25 +151,24 @@ class PubMqtt {
publish(subtopics[MQTT_COMM_START], String(sunrise + offsM).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); publish(subtopics[MQTT_COMM_STOP], String(sunset + offsE).c_str(), true);
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = mSys->getInverterByPos(i); Inverter<> *iv = mSys->getInverterByPos(i);
if(NULL == iv) if(NULL == iv)
continue; continue;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/dis_night_comm", iv->config->name); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/dis_night_comm", iv->config->name);
publish(mSubTopic, ((iv->commEnabled) ? dict[STR_TRUE] : dict[STR_FALSE]), true); publish(mSubTopic.data(), ((iv->commEnabled) ? dict[STR_TRUE] : dict[STR_FALSE]), true);
} }
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "comm_disabled"); snprintf(mSubTopic.data(), mSubTopic.size(), "comm_disabled");
publish(mSubTopic, (((*mUtcTimestamp > (sunset + offsE)) || (*mUtcTimestamp < (sunrise + offsM))) ? dict[STR_TRUE] : dict[STR_FALSE]), true); publish(mSubTopic.data(), (((*mUtcTimestamp > (sunset + offsE)) || (*mUtcTimestamp < (sunrise + offsM))) ? dict[STR_TRUE] : dict[STR_FALSE]), true);
return true; return true;
} }
void notAvailChanged(bool allNotAvail) { void notAvailChanged(bool allNotAvail) {
if(!allNotAvail) if(!allNotAvail)
mSendIvData.resetYieldDay(); SendIvData.resetYieldDay();
} }
bool tickerComm(bool disabled) { bool tickerComm(bool disabled) {
@ -180,9 +183,9 @@ class PubMqtt {
void tickerMidnight() { void tickerMidnight() {
// set Total YieldDay to zero // set Total YieldDay to zero
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[FLD_YD]); snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[FLD_YD]);
snprintf(mVal, 2, "0"); snprintf(mVal.data(), mVal.size(), "0");
publish(mSubTopic, mVal, true); publish(mSubTopic.data(), mVal.data(), true);
} }
void payloadEventListener(uint8_t cmd, Inverter<> *iv) { void payloadEventListener(uint8_t cmd, Inverter<> *iv) {
@ -201,11 +204,11 @@ class PubMqtt {
return; return;
if(addTopic) 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 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(); yield();
mTxCnt++; mTxCnt++;
} }
@ -242,8 +245,8 @@ class PubMqtt {
void setPowerLimitAck(Inverter<> *iv) { void setPowerLimitAck(Inverter<> *iv) {
if (NULL != iv) { if (NULL != iv) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]);
publish(mSubTopic, "true", true, true, QOS_2); publish(mSubTopic.data(), "true", true, true, QOS_2);
} }
} }
@ -259,15 +262,15 @@ class PubMqtt {
publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true); publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true);
#endif #endif
tickerMinute(); 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++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
snprintf(mVal, 20, "ctrl/limit/%d", i); snprintf(mVal.data(), mVal.size(), "ctrl/limit/%d", i);
subscribe(mVal, QOS_2); subscribe(mVal.data(), QOS_2);
snprintf(mVal, 20, "ctrl/restart/%d", i); snprintf(mVal.data(), mVal.size(), "ctrl/restart/%d", i);
subscribe(mVal); subscribe(mVal.data());
snprintf(mVal, 20, "ctrl/power/%d", i); snprintf(mVal.data(), mVal.size(), "ctrl/power/%d", i);
subscribe(mVal); subscribe(mVal.data());
} }
subscribe(subscr[MQTT_SUBS_SET_TIME]); subscribe(subscr[MQTT_SUBS_SET_TIME]);
} }
@ -317,7 +320,7 @@ class PubMqtt {
if(NULL == strstr(topic, "limit")) if(NULL == strstr(topic, "limit"))
root[F("val")] = atoi(pyld); root[F("val")] = atoi(pyld);
else else
root[F("val")] = (int)(atof(pyld) * 10.0f); root[F("val")] = atof(pyld);
if(pyld[len-1] == 'W') if(pyld[len-1] == 'W')
limitAbs = true; limitAbs = true;
@ -363,11 +366,9 @@ class PubMqtt {
} }
void discoveryConfigLoop(void) { void discoveryConfigLoop(void) {
char topic[64], name[32], uniq_id[32], buf[350];
DynamicJsonDocument doc(256); DynamicJsonDocument doc(256);
uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC}; constexpr static uint8_t fldTotal[] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC};
const char* unitTotal[4] = {"W", "kWh", "Wh", "W"};
String node_id = String(mDevName) + "_TOTAL"; String node_id = String(mDevName) + "_TOTAL";
bool total = (mDiscovery.lastIvId == MAX_NUM_INVERTERS); bool total = (mDiscovery.lastIvId == MAX_NUM_INVERTERS);
@ -396,32 +397,41 @@ class PubMqtt {
doc[F("mf")] = F("Hoymiles"); doc[F("mf")] = F("Hoymiles");
JsonObject deviceObj = doc.as<JsonObject>(); // deviceObj is only pointer!? 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;
topic.fill(0);
name.fill(0);
uniq_id.fill(0);
buf.fill(0);
const char *devCls, *stateCls; const char *devCls, *stateCls;
if (!total) { if (!total) {
if (rec->assign[mDiscovery.sub].ch == CH0) 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 else
snprintf(name, 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, 64, "/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, 32, "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); devCls = getFieldDeviceClass(rec->assign[mDiscovery.sub].fieldId);
stateCls = getFieldStateClass(rec->assign[mDiscovery.sub].fieldId); stateCls = getFieldStateClass(rec->assign[mDiscovery.sub].fieldId);
} }
else { // total values else { // total values
snprintf(name, 32, "Total %s", fields[fldTotal[mDiscovery.sub]]); snprintf(name.data(), name.size(), "Total %s", fields[fldTotal[mDiscovery.sub]]);
snprintf(topic, 64, "/%s", fields[fldTotal[mDiscovery.sub]]); snprintf(topic.data(), topic.size(), "/%s", fields[fldTotal[mDiscovery.sub]]);
snprintf(uniq_id, 32, "total_%s", fields[fldTotal[mDiscovery.sub]]); snprintf(uniq_id.data(), uniq_id.size(), "total_%s", fields[fldTotal[mDiscovery.sub]]);
devCls = getFieldDeviceClass(fldTotal[mDiscovery.sub]); devCls = getFieldDeviceClass(fldTotal[mDiscovery.sub]);
stateCls = getFieldStateClass(fldTotal[mDiscovery.sub]); stateCls = getFieldStateClass(fldTotal[mDiscovery.sub]);
} }
DynamicJsonDocument doc2(512); DynamicJsonDocument doc2(512);
doc2[F("name")] = name; constexpr static const char* unitTotal[] = {"W", "kWh", "Wh", "W"};
doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic); doc2[F("name")] = String(name.data());
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("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; doc2[F("dev")] = deviceObj;
if (!(String(stateCls) == String("total_increasing"))) 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!? doc2[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!?
@ -431,13 +441,12 @@ class PubMqtt {
doc2[F("stat_cla")] = String(stateCls); doc2[F("stat_cla")] = String(stateCls);
if (!total) 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 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; size_t size = measureJson(doc2) + 1;
memset(buf, 0, size); serializeJson(doc2, buf.data(), size);
serializeJson(doc2, buf, size); publish(topic.data(), buf.data(), true, false);
publish(topic, buf, true, false);
if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) { if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) {
mDiscovery.sub = 0; mDiscovery.sub = 0;
@ -507,15 +516,15 @@ class PubMqtt {
mLastIvState[id] = status; mLastIvState[id] = status;
changed = true; changed = true;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/available", iv->config->name);
snprintf(mVal, 40, "%d", (uint8_t)status); snprintf(mVal.data(), mVal.size(), "%d", (uint8_t)status);
publish(mSubTopic, mVal, true); publish(mSubTopic.data(), mVal.data(), true);
} }
} }
if(changed) { if(changed) {
snprintf(mVal, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE))); snprintf(mVal.data(), mVal.size(), "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE)));
publish("status", mVal, true); publish("status", mVal.data(), true);
} }
return anyAvail; return anyAvail;
@ -537,19 +546,19 @@ class PubMqtt {
mSendAlarm[i] = false; mSendAlarm[i] = false;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/cnt", iv->config->name); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/alarm/cnt", iv->config->name);
snprintf(mVal, 40, "%d", iv->alarmCnt); snprintf(mVal.data(), mVal.size(), "%d", iv->alarmCnt);
publish(mSubTopic, mVal, false); publish(mSubTopic.data(), mVal.data(), false);
for(uint8_t j = 0; j < 10; j++) { for(uint8_t j = 0; j < 10; j++) {
if(0 != iv->lastAlarm[j].code) { if(0 != iv->lastAlarm[j].code) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/alarm/%d", iv->config->name, j); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/alarm/%d", iv->config->name, j);
snprintf(mVal, 100, "{\"code\":%d,\"str\":\"%s\",\"start\":%d,\"end\":%d}", snprintf(mVal.data(), mVal.size(), "{\"code\":%d,\"str\":\"%s\",\"start\":%d,\"end\":%d}",
iv->lastAlarm[j].code, iv->lastAlarm[j].code,
iv->getAlarmStr(iv->lastAlarm[j].code).c_str(), iv->getAlarmStr(iv->lastAlarm[j].code).c_str(),
iv->lastAlarm[j].start + lastMidnight, iv->lastAlarm[j].start + lastMidnight,
iv->lastAlarm[j].end + lastMidnight); iv->lastAlarm[j].end + lastMidnight);
publish(mSubTopic, mVal, false); publish(mSubTopic.data(), mVal.data(), false);
yield(); yield();
} }
} }
@ -579,9 +588,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(mSubTopic.data(), mSubTopic.size(), "%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))); snprintf(mVal.data(), mVal.size(), "%g", ah::round3(iv->getValue(i, rec)));
publish(mSubTopic, mVal, retained); publish(mSubTopic.data(), mVal.data(), retained);
yield(); yield();
} }
@ -596,38 +605,38 @@ class PubMqtt {
if(mSendList.empty()) if(mSendList.empty())
return; return;
mSendIvData.start(); SendIvData.start();
mLastAnyAvail = anyAvail; mLastAnyAvail = anyAvail;
} }
espMqttClient mClient; espMqttClient mClient;
cfgMqtt_t *mCfgMqtt; cfgMqtt_t *mCfgMqtt = nullptr;
#if defined(ESP8266) #if defined(ESP8266)
WiFiEventHandler mHWifiCon, mHWifiDiscon; WiFiEventHandler mHWifiCon, mHWifiDiscon;
#endif #endif
HMSYSTEM *mSys; HMSYSTEM *mSys = nullptr;
PubMqttIvData<HMSYSTEM> mSendIvData; PubMqttIvData<HMSYSTEM> SendIvData;
uint32_t *mUtcTimestamp, *mUptime; uint32_t *mUtcTimestamp = nullptr, *mUptime = nullptr;
uint32_t mRxCnt, mTxCnt; uint32_t mRxCnt = 0, mTxCnt = 0;
std::queue<sendListCmdIv> mSendList; std::queue<sendListCmdIv> mSendList;
std::array<bool, MAX_NUM_INVERTERS> mSendAlarm{}; std::array<bool, MAX_NUM_INVERTERS> mSendAlarm;
subscriptionCb mSubscriptionCb; subscriptionCb mSubscriptionCb = nullptr;
bool mLastAnyAvail; bool mLastAnyAvail = false;
InverterStatus mLastIvState[MAX_NUM_INVERTERS]; std::array<InverterStatus, MAX_NUM_INVERTERS> mLastIvState;
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS]; std::array<uint32_t, MAX_NUM_INVERTERS> mIvLastRTRpub;
uint16_t mIntervalTimeout; uint16_t mIntervalTimeout = 0;
// last will topic and payload must be available through lifetime of 'espMqttClient' // last will topic and payload must be available through lifetime of 'espMqttClient'
char mLwtTopic[MQTT_TOPIC_LEN+5]; std::array<char, (MQTT_TOPIC_LEN + 5)> mLwtTopic;
const char *mDevName, *mVersion; const char *mDevName = nullptr, *mVersion = nullptr;
char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT 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. // global buffer for mqtt topic. Used when publishing mqtt messages.
char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1]; std::array<char, (MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1)> mTopic;
char mSubTopic[32 + MAX_NAME_LENGTH + 1]; std::array<char, (32 + MAX_NAME_LENGTH + 1)> mSubTopic;
char mVal[100]; std::array<char, 100> mVal;
discovery_t mDiscovery; discovery_t mDiscovery = {true, 0, 0, 0};
}; };
#endif /*ENABLE_MQTT*/ #endif /*ENABLE_MQTT*/

137
src/publisher/pubMqttIvData.h

@ -1,4 +1,4 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de // 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -6,6 +6,7 @@
#ifndef __PUB_MQTT_IV_DATA_H__ #ifndef __PUB_MQTT_IV_DATA_H__
#define __PUB_MQTT_IV_DATA_H__ #define __PUB_MQTT_IV_DATA_H__
#include <array>
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include "../hm/hmSystem.h" #include "../hm/hmSystem.h"
#include "pubMqttDefs.h" #include "pubMqttDefs.h"
@ -21,6 +22,8 @@ struct sendListCmdIv {
template<class HMSYSTEM> template<class HMSYSTEM>
class PubMqttIvData { class PubMqttIvData {
public: public:
PubMqttIvData() : mTotal{}, mSubTopic{}, mVal{} {}
void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) { void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) {
mSys = sys; mSys = sys;
mUtcTimestamp = utcTs; mUtcTimestamp = utcTs;
@ -72,13 +75,14 @@ class PubMqttIvData {
mTotalFound = false; mTotalFound = false;
mSendTotalYd = true; mSendTotalYd = true;
mAllTotalFound = true; mAllTotalFound = true;
mAtLeastOneWasntSent = false;
if(!mSendList->empty()) { if(!mSendList->empty()) {
mCmd = mSendList->front().cmd; mCmd = mSendList->front().cmd;
mIvSend = mSendList->front().iv; mIvSend = mSendList->front().iv;
if((RealTimeRunData_Debug != mCmd) || !mRTRDataHasBeenSent) { // send RealTimeRunData only once if((RealTimeRunData_Debug != mCmd) || !mRTRDataHasBeenSent) { // send RealTimeRunData only once
mSendTotals = (RealTimeRunData_Debug == mCmd); mSendTotals = (RealTimeRunData_Debug == mCmd);
memset(mTotal, 0, sizeof(float) * 4); memset(mTotal, 0, sizeof(float) * 5);
mState = FIND_NXT_IV; mState = FIND_NXT_IV;
} else } else
mSendList->pop(); mSendList->pop();
@ -105,21 +109,21 @@ class PubMqttIvData {
if(found) { if(found) {
record_t<> *rec = mIv->getRecordStruct(mCmd); record_t<> *rec = mIv->getRecordStruct(mCmd);
if(MqttSentStatus::NEW_DATA == rec->mqttSentStatus) { if(MqttSentStatus::NEW_DATA == rec->mqttSentStatus) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/last_success", mIv->config->name); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/last_success", mIv->config->name);
snprintf(mVal, 40, "%d", mIv->getLastTs(rec)); snprintf(mVal.data(), mVal.size(), "%d", mIv->getLastTs(rec));
mPublish(mSubTopic, mVal, true, QOS_0); mPublish(mSubTopic.data(), mVal.data(), true, QOS_0);
if((mIv->ivGen == IV_HMS) || (mIv->ivGen == IV_HMT)) { if((mIv->ivGen == IV_HMS) || (mIv->ivGen == IV_HMT)) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch0/rssi", mIv->config->name); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch0/rssi", mIv->config->name);
snprintf(mVal, 40, "%d", mIv->rssi); snprintf(mVal.data(), mVal.size(), "%d", mIv->rssi);
mPublish(mSubTopic, mVal, false, QOS_0); mPublish(mSubTopic.data(), mVal.data(), false, QOS_0);
} }
rec->mqttSentStatus = MqttSentStatus::LAST_SUCCESS_SENT; rec->mqttSentStatus = MqttSentStatus::LAST_SUCCESS_SENT;
} }
mIv->isProducing(); // recalculate status mIv->isProducing(); // recalculate status
mState = SEND_DATA; mState = SEND_DATA;
} else if(mSendTotals && mTotalFound) { } else if(mSendTotals && mTotalFound && mAtLeastOneWasntSent) {
if(mYldTotalStore > mTotal[2]) if(mYldTotalStore > mTotal[2])
mSendTotalYd = false; // don't send yield total if last value was greater mSendTotalYd = false; // don't send yield total if last value was greater
else else
@ -142,30 +146,31 @@ class PubMqttIvData {
if(mPos < rec->length) { if(mPos < rec->length) {
bool retained = false; bool retained = false;
if (mCmd == RealTimeRunData_Debug) { if (RealTimeRunData_Debug == mCmd) {
if((FLD_YT == rec->assign[mPos].fieldId) || (FLD_YD == rec->assign[mPos].fieldId)) if((FLD_YT == rec->assign[mPos].fieldId) || (FLD_YD == rec->assign[mPos].fieldId))
retained = true; retained = true;
// calculate total values for RealTimeRunData_Debug // calculate total values for RealTimeRunData_Debug
if (CH0 == rec->assign[mPos].ch) { if (CH0 == rec->assign[mPos].ch) {
if(mIv->getStatus() != InverterStatus::OFF) { if(mIv->getStatus() != InverterStatus::OFF) {
if(mIv->config->add2Total) { mTotalFound = true;
mTotalFound = true; switch (rec->assign[mPos].fieldId) {
switch (rec->assign[mPos].fieldId) { case FLD_PAC:
case FLD_PAC: mTotal[0] += mIv->getValue(mPos, rec);
mTotal[0] += mIv->getValue(mPos, rec); break;
break; case FLD_YT:
case FLD_YT: mTotal[1] += mIv->getValue(mPos, rec);
mTotal[1] += mIv->getValue(mPos, rec); break;
break; case FLD_YD: {
case FLD_YD: { mTotal[2] += mIv->getValue(mPos, rec);
mTotal[2] += mIv->getValue(mPos, rec); break;
break;
}
case FLD_PDC:
mTotal[3] += 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 } else
mAllTotalFound = false; mAllTotalFound = false;
@ -173,10 +178,33 @@ class PubMqttIvData {
} }
if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) {
mAtLeastOneWasntSent = true;
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)));
}
uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0; uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); if((FLD_EVT != rec->assign[mPos].fieldId)
snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec))); && (FLD_LAST_ALARM_CODE != rec->assign[mPos].fieldId))
mPublish(mSubTopic, mVal, retained, qos); mPublish(mSubTopic.data(), mVal.data(), retained, qos);
} }
mPos++; mPos++;
} else { } else {
@ -189,24 +217,24 @@ class PubMqttIvData {
} }
inline void sendRadioStat(uint8_t start) { inline void sendRadioStat(uint8_t start) {
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/radio_stat", mIv->config->name); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/radio_stat", mIv->config->name);
snprintf(mVal, 140, "{\"tx\":%d,\"success\":%d,\"fail\":%d,\"no_answer\":%d,\"retransmits\":%d,\"lossIvRx\":%d,\"lossIvTx\":%d,\"lossDtuRx\":%d,\"lossDtuTx\":%d}", 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.txCnt,
mIv->radioStatistics.rxSuccess, mIv->radioStatistics.rxSuccess,
mIv->radioStatistics.rxFail, mIv->radioStatistics.rxFail,
mIv->radioStatistics.rxFailNoAnser, mIv->radioStatistics.rxFailNoAnswer,
mIv->radioStatistics.retransmits, mIv->radioStatistics.retransmits,
mIv->radioStatistics.ivLoss, mIv->radioStatistics.ivLoss,
mIv->radioStatistics.ivSent, mIv->radioStatistics.ivSent,
mIv->radioStatistics.dtuLoss, mIv->radioStatistics.dtuLoss,
mIv->radioStatistics.dtuSent); mIv->radioStatistics.dtuSent);
mPublish(mSubTopic, mVal, false, QOS_0); mPublish(mSubTopic.data(), mVal.data(), false, QOS_0);
} }
void stateSendTotals() { void stateSendTotals() {
uint8_t fieldId;
mRTRDataHasBeenSent = true; mRTRDataHasBeenSent = true;
if(mPos < 4) { if(mPos < 5) {
uint8_t fieldId;
bool retained = true; bool retained = true;
switch (mPos) { switch (mPos) {
default: default:
@ -232,37 +260,42 @@ class PubMqttIvData {
fieldId = FLD_PDC; fieldId = FLD_PDC;
retained = false; retained = false;
break; break;
case 4:
fieldId = FLD_MP;
retained = false;
break;
} }
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]); snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]);
snprintf(mVal, 40, "%g", ah::round3(mTotal[mPos])); snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos]));
mPublish(mSubTopic, mVal, retained, QOS_0); mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0);
mPos++; mPos++;
} else { } else {
mSendList->pop(); mSendList->pop();
mPos = 0; mSendTotals = false;
mState = IDLE; mState = IDLE;
} }
} }
HMSYSTEM *mSys; HMSYSTEM *mSys = nullptr;
uint32_t *mUtcTimestamp; uint32_t *mUtcTimestamp = nullptr;
pubMqttPublisherType mPublish; pubMqttPublisherType mPublish;
State mState; State mState = IDLE;
StateFunction mTable[NUM_STATES]; StateFunction mTable[NUM_STATES];
uint8_t mCmd; uint8_t mCmd = 0;
uint8_t mLastIvId; uint8_t mLastIvId = 0;
bool mSendTotals, mTotalFound, mAllTotalFound, mSendTotalYd; bool mSendTotals = false, mTotalFound = false, mAllTotalFound = false;
float mTotal[4], mYldTotalStore; bool mSendTotalYd = false, mAtLeastOneWasntSent = false;
float mTotal[5], mYldTotalStore = 0;
Inverter<> *mIv, *mIvSend; Inverter<> *mIv = nullptr, *mIvSend = nullptr;
uint8_t mPos; uint8_t mPos = 0;
bool mRTRDataHasBeenSent; bool mRTRDataHasBeenSent = false;
char mSubTopic[32 + MAX_NAME_LENGTH + 1]; std::array<char, (32 + MAX_NAME_LENGTH + 1)> mSubTopic;
char mVal[140]; std::array<char, 160> mVal;
std::queue<sendListCmdIv> *mSendList; std::queue<sendListCmdIv> *mSendList = nullptr;
}; };
#endif /*__PUB_MQTT_IV_DATA_H__*/ #endif /*__PUB_MQTT_IV_DATA_H__*/

12
src/publisher/pubSerial.h

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

6
src/utils/helper.cpp

@ -72,11 +72,10 @@ namespace ah {
String getTimeStrMs(uint64_t t) { String getTimeStrMs(uint64_t t) {
char str[13]; char str[13];
uint16_t m;
if(0 == t) if(0 == t)
sprintf(str, "n/a"); sprintf(str, "n/a");
else { else {
m = t % 1000; uint16_t m = t % 1000;
t = t / 1000; t = t / 1000;
sprintf(str, "%02d:%02d:%02d.%03d", hour(t), minute(t), second(t), m); sprintf(str, "%02d:%02d:%02d.%03d", hour(t), minute(t), second(t), m);
} }
@ -86,14 +85,13 @@ namespace ah {
uint64_t Serial2u64(const char *val) { uint64_t Serial2u64(const char *val) {
char tmp[3]; char tmp[3];
uint64_t ret = 0ULL; uint64_t ret = 0ULL;
uint64_t u64;
memset(tmp, 0, 3); memset(tmp, 0, 3);
for(uint8_t i = 0; i < 6; i++) { for(uint8_t i = 0; i < 6; i++) {
tmp[0] = val[i*2]; tmp[0] = val[i*2];
tmp[1] = val[i*2 + 1]; tmp[1] = val[i*2 + 1];
if((tmp[0] == '\0') || (tmp[1] == '\0')) if((tmp[0] == '\0') || (tmp[1] == '\0'))
break; break;
u64 = strtol(tmp, NULL, 16); uint64_t u64 = strtol(tmp, NULL, 16);
ret |= (u64 << ((5-i) << 3)); ret |= (u64 << ((5-i) << 3));
} }
return ret; return ret;

18
src/utils/improv.h

@ -10,6 +10,7 @@
#include <functional> #include <functional>
#include "dbg.h" #include "dbg.h"
#include "AsyncJson.h" #include "AsyncJson.h"
#include "../appInterface.h"
// https://www.improv-wifi.com/serial/ // https://www.improv-wifi.com/serial/
// https://github.com/jnthas/improv-wifi-demo/blob/main/src/esp32-wifiimprov/esp32-wifiimprov.ino // https://github.com/jnthas/improv-wifi-demo/blob/main/src/esp32-wifiimprov/esp32-wifiimprov.ino
@ -70,7 +71,7 @@ class Improv {
TYPE_RPC_RESPONSE = 0x04 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++) { for(uint8_t i = 0; i < len; i++) {
DHEX(buf[i]); DHEX(buf[i]);
DBGPRINT(F(" ")); DBGPRINT(F(" "));
@ -78,7 +79,7 @@ class Improv {
DBGPRINTLN(""); 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; uint8_t calc = 0;
for(uint8_t i = 0; i < len; i++) { for(uint8_t i = 0; i < len; i++) {
calc += buf[i]; calc += buf[i];
@ -86,7 +87,7 @@ class Improv {
return calc; 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); /*DHEX(buf[len], false);
DBGPRINT(F(" == "), false); DBGPRINT(F(" == "), false);
DBGHEXLN(buildChecksum(buf, len), false);*/ DBGHEXLN(buildChecksum(buf, len), false);*/
@ -97,7 +98,7 @@ class Improv {
if(len < 11) if(len < 11)
return false; return false;
if(0 != strncmp((char*)buf, "IMPROV", 6)) if(0 != strncmp(reinterpret_cast<char*>(buf), "IMPROV", 6))
return false; return false;
// version check (only version 1 is supported!) // version check (only version 1 is supported!)
@ -199,7 +200,7 @@ class Improv {
dumpBuf(buf, len); 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(TYPE_RPC == type) {
if(GET_CURRENT_STATE == buf[0]) { if(GET_CURRENT_STATE == buf[0]) {
setDebugEn(false); setDebugEn(false);
@ -212,9 +213,10 @@ class Improv {
} }
} }
IApp *mApp; IApp *mApp = nullptr;
const char *mDevName, *mVersion; const char *mDevName = nullptr;
bool mScanRunning; const char *mVersion = nullptr;
bool mScanRunning = false;
}; };
#endif #endif

20
src/utils/scheduler.h

@ -7,6 +7,7 @@
#define __SCHEDULER_H__ #define __SCHEDULER_H__
#include <functional> #include <functional>
#include <array>
#include "dbg.h" #include "dbg.h"
namespace ah { namespace ah {
@ -28,8 +29,6 @@ namespace ah {
class Scheduler { class Scheduler {
public: public:
Scheduler() {}
void setup(bool directStart) { void setup(bool directStart) {
mUptime = 0; mUptime = 0;
mTimestamp = (directStart) ? 1 : 0; mTimestamp = (directStart) ? 1 : 0;
@ -93,8 +92,7 @@ namespace ah {
} }
inline void resetTicker(void) { inline void resetTicker(void) {
for (uint8_t i = 0; i < MAX_NUM_TICKER; i++) mTickerInUse.fill(false);
mTickerInUse[i] = false;
} }
void getStat(uint8_t *max) { void getStat(uint8_t *max) {
@ -127,8 +125,8 @@ namespace ah {
mTicker[i].timeout = timeout; mTicker[i].timeout = timeout;
mTicker[i].reload = reload; mTicker[i].reload = reload;
mTicker[i].isTimestamp = isTimestamp; mTicker[i].isTimestamp = isTimestamp;
memset(mTicker[i].name, 0, 6); strncpy(mTicker[i].name, name, 5);
strncpy(mTicker[i].name, name, (strlen(name) < 6) ? strlen(name) : 5); mTicker[i].name[5]=0;
if(mMax == i) if(mMax == i)
mMax = i + 1; mMax = i + 1;
return i; return i;
@ -159,11 +157,11 @@ namespace ah {
} }
} }
sP mTicker[MAX_NUM_TICKER]; std::array<sP, MAX_NUM_TICKER> mTicker;
bool mTickerInUse[MAX_NUM_TICKER]; std::array<bool, MAX_NUM_TICKER> mTickerInUse;
uint32_t mMillis, mPrevMillis, mDiff; uint32_t mMillis = 0, mPrevMillis = 0, mDiff = 0;
uint8_t mDiffSeconds; uint8_t mDiffSeconds = 0;
uint8_t mMax; uint8_t mMax = 0;
}; };
} }

4
src/utils/spiPatcher.cpp

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

6
src/utils/spiPatcher.h

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

4
src/utils/syslog.cpp

@ -10,6 +10,7 @@
#define SYSLOG_MAX_PACKET_SIZE 256 #define SYSLOG_MAX_PACKET_SIZE 256
DbgSyslog::DbgSyslog() : mSyslogBuffer{} {}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void DbgSyslog::setup(settings_t *config) { void DbgSyslog::setup(settings_t *config) {
@ -67,12 +68,11 @@ void DbgSyslog::syslogCb (String msg)
// Send mSyslogBuffer in chunks because mSyslogBuffer is larger than syslog packet size // Send mSyslogBuffer in chunks because mSyslogBuffer is larger than syslog packet size
int packetStart = 0; int packetStart = 0;
int packetSize = 122; // syslog payload depends also on hostname and app int packetSize = 122; // syslog payload depends also on hostname and app
char saveChar;
if (isEolFound) { if (isEolFound) {
mSyslogBuffer[mSyslogBufFill-2]=0; // skip \r\n mSyslogBuffer[mSyslogBufFill-2]=0; // skip \r\n
} }
while(packetStart < mSyslogBufFill) { while(packetStart < mSyslogBufFill) {
saveChar = mSyslogBuffer[packetStart+packetSize]; char saveChar = mSyslogBuffer[packetStart+packetSize];
mSyslogBuffer[packetStart+packetSize] = 0; mSyslogBuffer[packetStart+packetSize] = 0;
log(mConfig->sys.deviceName,SYSLOG_FACILITY, mSyslogSeverity, &mSyslogBuffer[packetStart]); log(mConfig->sys.deviceName,SYSLOG_FACILITY, mSyslogSeverity, &mSyslogBuffer[packetStart]);
mSyslogBuffer[packetStart+packetSize] = saveChar; mSyslogBuffer[packetStart+packetSize] = saveChar;

5
src/utils/syslog.h

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

6
src/utils/timemonitor.h

@ -22,14 +22,14 @@ class TimeMonitor {
/** /**
* A constructor for creating a TimeMonitor object * A constructor for creating a TimeMonitor object
*/ */
TimeMonitor(void) {} TimeMonitor() {}
/** /**
* A constructor for initializing a TimeMonitor object * A constructor for initializing a TimeMonitor object
* @param timeout timeout in ms * @param timeout timeout in ms
* @param start (optional) if true, start TimeMonitor immediately * @param start (optional) if true, start TimeMonitor immediately
*/ */
TimeMonitor(uint32_t timeout, bool start = false) { explicit TimeMonitor(uint32_t timeout, bool start = false) {
if (start) if (start)
startTimeMonitor(timeout); startTimeMonitor(timeout);
else else
@ -80,7 +80,7 @@ class TimeMonitor {
* true: TimeMonitor already timed out * true: TimeMonitor already timed out
* false: TimeMonitor still in time or TimeMonitor was stopped * false: TimeMonitor still in time or TimeMonitor was stopped
*/ */
bool isTimeout(void) { bool isTimeout(void) const {
if ((mStarted) && ((millis() - mStartTime) >= mTimeout)) if ((mStarted) && ((millis() - mStartTime) >= mTimeout))
return true; return true;
else else

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__*/

185
src/web/RestApi.h

@ -26,10 +26,6 @@
#define F(sl) (sl) #define F(sl) (sl)
#endif #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> template<class HMSYSTEM>
class RestApi { class RestApi {
public: public:
@ -64,9 +60,9 @@ class RestApi {
DynamicJsonDocument json(128); DynamicJsonDocument json(128);
JsonObject dummy = json.as<JsonObject>(); JsonObject dummy = json.as<JsonObject>();
if(obj[F("path")] == "ctrl") if(obj[F("path")] == "ctrl")
setCtrl(obj, dummy); setCtrl(obj, dummy, "*");
else if(obj[F("path")] == "setup") else if(obj[F("path")] == "setup")
setSetup(obj, dummy); setSetup(obj, dummy, "*");
} }
private: private:
@ -103,7 +99,6 @@ class RestApi {
#endif /* !defined(ETHERNET) */ #endif /* !defined(ETHERNET) */
else if(path == "live") getLive(request,root); else if(path == "live") getLive(request,root);
else if (path == "powerHistory") getPowerHistory(request, root); else if (path == "powerHistory") getPowerHistory(request, root);
else if (path == "yieldDayHistory") getYieldDayHistory(request, root);
else { else {
if(path.substring(0, 12) == "inverter/id/") if(path.substring(0, 12) == "inverter/id/")
getInverter(root, request->url().substring(17).toInt()); getInverter(root, request->url().substring(17).toInt());
@ -138,11 +133,11 @@ class RestApi {
#endif #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"); DPRINTLN(DBG_VERBOSE, "onApiPostBody");
if(0 == index) { if(0 == index) {
if(NULL != mTmpBuf) if(nullptr != mTmpBuf)
delete[] mTmpBuf; delete[] mTmpBuf;
mTmpBuf = new uint8_t[total+1]; mTmpBuf = new uint8_t[total+1];
mTmpSize = total; mTmpSize = total;
@ -155,36 +150,40 @@ class RestApi {
DynamicJsonDocument json(1000); DynamicJsonDocument json(1000);
DeserializationError err = deserializeJson(json, (const char *)mTmpBuf, mTmpSize);
JsonObject obj = json.as<JsonObject>();
AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
root[F("success")] = (err) ? false : true; DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize);
if(!err) { if(!json.is<JsonObject>())
String path = request->url().substring(5); root[F("error")] = F(DESER_FAILED);
if(path == "ctrl") else {
root[F("success")] = setCtrl(obj, root); JsonObject obj = json.as<JsonObject>();
else if(path == "setup")
root[F("success")] = setSetup(obj, root); root[F("success")] = (err) ? false : true;
else { if(!err) {
root[F("success")] = false; String path = request->url().substring(5);
root[F("error")] = F(PATH_NOT_FOUND) + path; if(path == "ctrl")
} root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str());
} else { else if(path == "setup")
switch (err.code()) { root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str());
case DeserializationError::Ok: break; else {
case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break; root[F("success")] = false;
case DeserializationError::InvalidInput: root[F("error")] = F(INVALID_INPUT); break; root[F("error")] = F(PATH_NOT_FOUND) + path;
case DeserializationError::NoMemory: root[F("error")] = F(NOT_ENOUGH_MEM); break; }
default: root[F("error")] = F(DESER_FAILED); break; } 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_MEM); break;
default: root[F("error")] = F(DESER_FAILED); break;
}
} }
} }
response->setLength(); response->setLength();
request->send(response); request->send(response);
delete[] mTmpBuf; delete[] mTmpBuf;
mTmpBuf = NULL; mTmpBuf = nullptr;
} }
void getNotFound(JsonObject obj, String url) { void getNotFound(JsonObject obj, String url) {
@ -204,7 +203,6 @@ class RestApi {
ep[F("live")] = url + F("live"); ep[F("live")] = url + F("live");
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
ep[F("powerHistory")] = url + F("powerHistory"); ep[F("powerHistory")] = url + F("powerHistory");
ep[F("yieldDayHistory")] = url + F("yieldDayHistory");
#endif #endif
} }
@ -212,6 +210,9 @@ class RestApi {
void onDwnldSetup(AsyncWebServerRequest *request) { void onDwnldSetup(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response; AsyncWebServerResponse *response;
// save settings to have latest firmware changes in export
mApp->saveSettings(false);
File fp = LittleFS.open("/settings.json", "r"); File fp = LittleFS.open("/settings.json", "r");
if(!fp) { if(!fp) {
DPRINTLN(DBG_ERROR, F("failed to load settings")); DPRINTLN(DBG_ERROR, F("failed to load settings"));
@ -252,6 +253,8 @@ class RestApi {
} }
void getGeneric(AsyncWebServerRequest *request, JsonObject obj) { void getGeneric(AsyncWebServerRequest *request, JsonObject obj) {
mApp->resetLockTimeout();
obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI();
obj[F("ts_uptime")] = mApp->getUptime(); obj[F("ts_uptime")] = mApp->getUptime();
obj[F("ts_now")] = mApp->getTimestamp(); obj[F("ts_now")] = mApp->getTimestamp();
@ -259,11 +262,13 @@ class RestApi {
obj[F("modules")] = String(mApp->getVersionModules()); obj[F("modules")] = String(mApp->getVersionModules());
obj[F("build")] = String(AUTO_GIT_HASH); obj[F("build")] = String(AUTO_GIT_HASH);
obj[F("env")] = String(ENV_NAME); obj[F("env")] = String(ENV_NAME);
obj[F("menu_prot")] = mApp->getProtection(request); 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_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")] = String(mConfig->plugin.customLink);
obj[F("cst_lnk_txt")] = String(mConfig->plugin.customLinkText); obj[F("cst_lnk_txt")] = String(mConfig->plugin.customLinkText);
obj[F("region")] = mConfig->sys.region;
obj[F("timezone")] = mConfig->sys.timezone;
#if defined(ESP32) #if defined(ESP32)
obj[F("esp_type")] = F("ESP32"); obj[F("esp_type")] = F("ESP32");
@ -417,7 +422,7 @@ class RestApi {
obj[F("name")] = String(iv->config->name); obj[F("name")] = String(iv->config->name);
obj[F("rx_success")] = iv->radioStatistics.rxSuccess; obj[F("rx_success")] = iv->radioStatistics.rxSuccess;
obj[F("rx_fail")] = iv->radioStatistics.rxFail; obj[F("rx_fail")] = iv->radioStatistics.rxFail;
obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnser; obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnswer;
obj[F("frame_cnt")] = iv->radioStatistics.frmCnt; obj[F("frame_cnt")] = iv->radioStatistics.frmCnt;
obj[F("tx_cnt")] = iv->radioStatistics.txCnt; obj[F("tx_cnt")] = iv->radioStatistics.txCnt;
obj[F("retransmits")] = iv->radioStatistics.retransmits; obj[F("retransmits")] = iv->radioStatistics.retransmits;
@ -453,7 +458,6 @@ class RestApi {
obj2[F("channels")] = iv->channels; obj2[F("channels")] = iv->channels;
obj2[F("freq")] = iv->config->frequency; obj2[F("freq")] = iv->config->frequency;
obj2[F("disnightcom")] = (bool)iv->config->disNightCom; obj2[F("disnightcom")] = (bool)iv->config->disNightCom;
obj2[F("add2total")] = (bool)iv->config->add2Total;
if(0xff == iv->config->powerLevel) { if(0xff == iv->config->powerLevel) {
if((IV_HMT == iv->ivGen) || (IV_HMS == iv->ivGen)) if((IV_HMT == iv->ivGen) || (IV_HMS == iv->ivGen))
obj2[F("pa")] = 30; // 20dBm obj2[F("pa")] = 30; // 20dBm
@ -476,8 +480,6 @@ class RestApi {
obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime; obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime;
obj[F("rdGrid")] = (bool)mConfig->inst.readGrid; obj[F("rdGrid")] = (bool)mConfig->inst.readGrid;
obj[F("rstMaxMid")] = (bool)mConfig->inst.rstMaxValsMidNight; 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) { void getInverter(JsonObject obj, uint8_t id) {
@ -493,7 +495,7 @@ class RestApi {
obj[F("name")] = String(iv->config->name); obj[F("name")] = String(iv->config->name);
obj[F("serial")] = String(iv->config->serial.u64, HEX); obj[F("serial")] = String(iv->config->serial.u64, HEX);
obj[F("version")] = String(iv->getFwVersion()); obj[F("version")] = String(iv->getFwVersion());
obj[F("power_limit_read")] = ah::round3(iv->actPowerLimit); obj[F("power_limit_read")] = iv->actPowerLimit;
obj[F("power_limit_ack")] = iv->powerLimitAck; obj[F("power_limit_ack")] = iv->powerLimitAck;
obj[F("max_pwr")] = iv->getMaxPower(); obj[F("max_pwr")] = iv->getMaxPower();
obj[F("ts_last_success")] = rec->ts; obj[F("ts_last_success")] = rec->ts;
@ -647,6 +649,9 @@ class RestApi {
obj[F("fcsb")] = mConfig->cmt.pinFcsb; obj[F("fcsb")] = mConfig->cmt.pinFcsb;
obj[F("gpio3")] = mConfig->cmt.pinIrq; obj[F("gpio3")] = mConfig->cmt.pinIrq;
obj[F("en")] = (bool) mConfig->cmt.enabled; 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) { void getRadioCmtInfo(JsonObject obj) {
@ -674,6 +679,7 @@ class RestApi {
obj[F("debug")] = mConfig->serial.debug; obj[F("debug")] = mConfig->serial.debug;
obj[F("priv")] = mConfig->serial.privacyLog; obj[F("priv")] = mConfig->serial.privacyLog;
obj[F("wholeTrace")] = mConfig->serial.printWholeTrace; obj[F("wholeTrace")] = mConfig->serial.printWholeTrace;
obj[F("log2mqtt")] = mConfig->serial.log2mqtt;
} }
void getStaticIp(JsonObject obj) { void getStaticIp(JsonObject obj) {
@ -745,6 +751,12 @@ class RestApi {
warn.add(F(REBOOT_ESP_APPLY_CHANGES)); warn.add(F(REBOOT_ESP_APPLY_CHANGES));
if(0 == mApp->getTimestamp()) if(0 == mApp->getTimestamp())
warn.add(F(TIME_NOT_SET)); 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) { void getSetup(AsyncWebServerRequest *request, JsonObject obj) {
@ -814,29 +826,26 @@ class RestApi {
#endif /*ENABLE_HISTORY*/ #endif /*ENABLE_HISTORY*/
} }
void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) { bool setCtrl(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) {
getGeneric(request, obj.createNestedObject(F("generic"))); if(jsonIn.containsKey(F("auth"))) {
#if defined(ENABLE_HISTORY) if(String(jsonIn[F("auth")]) == String(mConfig->sys.adminPwd)) {
obj[F("refresh")] = 86400; // 1 day jsonOut[F("token")] = mApp->unlock(clientIP, false);
uint16_t max = 0; jsonIn[F("token")] = jsonOut[F("token")];
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { } else {
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld); jsonOut[F("error")] = F("ERR_AUTH");
obj[F("value")][fld] = value; return false;
if (value > max) }
max = value; if(!jsonIn.containsKey(F("cmd")))
return true;
} }
obj[F("max")] = max;
#else
obj[F("refresh")] = 86400; // 1 day
#endif /*ENABLE_HISTORY*/
}
if(isProtected(jsonIn, jsonOut, clientIP))
return false;
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) {
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]);
bool accepted = true; bool accepted = true;
if(NULL == iv) { if(NULL == iv) {
jsonOut[F("error")] = F(INV_INDEX_INVALID) + jsonIn[F("id")].as<String>(); jsonOut[F("error")] = F("ERR_INDEX");
return false; return false;
} }
jsonOut[F("id")] = jsonIn[F("id")]; jsonOut[F("id")] = jsonIn[F("id")];
@ -846,7 +855,7 @@ class RestApi {
else if(F("restart") == jsonIn[F("cmd")]) else if(F("restart") == jsonIn[F("cmd")])
accepted = iv->setDevControlRequest(Restart); accepted = iv->setDevControlRequest(Restart);
else if(0 == strncmp("limit_", jsonIn[F("cmd")].as<const char*>(), 6)) { 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")]) if(F("limit_persistent_relative") == jsonIn[F("cmd")])
iv->powerLimit[1] = RelativPersistent; iv->powerLimit[1] = RelativPersistent;
else if(F("limit_persistent_absolute") == jsonIn[F("cmd")]) else if(F("limit_persistent_absolute") == jsonIn[F("cmd")])
@ -863,19 +872,22 @@ class RestApi {
DPRINTLN(DBG_INFO, F("dev cmd")); DPRINTLN(DBG_INFO, F("dev cmd"));
iv->setDevCommand(jsonIn[F("val")].as<int>()); iv->setDevCommand(jsonIn[F("val")].as<int>());
} else { } else {
jsonOut[F("error")] = F(UNKNOWN_CMD) + jsonIn["cmd"].as<String>() + "'"; jsonOut[F("error")] = F("ERR_UNKNOWN_CMD");
return false; return false;
} }
if(!accepted) { if(!accepted) {
jsonOut[F("error")] = F(INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT); jsonOut[F("error")] = F("ERR_LIMIT_NOT_ACCEPT");
return false; return false;
} }
return true; 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 !defined(ETHERNET)
if(F("scan_wifi") == jsonIn[F("cmd")]) if(F("scan_wifi") == jsonIn[F("cmd")])
mApp->scanAvailNetworks(); mApp->scanAvailNetworks();
@ -914,30 +926,55 @@ class RestApi {
iv->config->frequency = jsonIn[F("freq")]; iv->config->frequency = jsonIn[F("freq")];
iv->config->powerLevel = jsonIn[F("pa")]; iv->config->powerLevel = jsonIn[F("pa")];
iv->config->disNightCom = jsonIn[F("disnightcom")]; iv->config->disNightCom = jsonIn[F("disnightcom")];
iv->config->add2Total = jsonIn[F("add2total")];
mApp->saveSettings(false); // without reboot mApp->saveSettings(false); // without reboot
} else { } else {
jsonOut[F("error")] = F(UNKNOWN_CMD); jsonOut[F("error")] = F("ERR_UNKNOWN_CMD");
return false; return false;
} }
return true; return true;
} }
IApp *mApp; bool isProtected(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) {
HMSYSTEM *mSys; if(mConfig->sys.adminPwd[0] != '\0') { // check if admin password is set
HmRadio<> *mRadioNrf; 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) #if defined(ESP32)
CmtRadio<> *mRadioCmt; CmtRadio<> *mRadioCmt = nullptr;
#endif #endif
AsyncWebServer *mSrv; AsyncWebServer *mSrv = nullptr;
settings_t *mConfig; settings_t *mConfig = nullptr;
uint32_t mTimezoneOffset; uint32_t mTimezoneOffset = 0;
uint32_t mHeapFree, mHeapFreeBlk; uint32_t mHeapFree = 0, mHeapFreeBlk = 0;
uint8_t mHeapFrag; uint8_t mHeapFrag = 0;
uint8_t *mTmpBuf = NULL; uint8_t *mTmpBuf = nullptr;
uint32_t mTmpSize; uint32_t mTmpSize = 0;
}; };
#endif /*__WEB_API_H__*/ #endif /*__WEB_API_H__*/

6
src/web/html/colorBright.css

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

16
src/web/html/colorDark.css

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

24
src/web/html/history.html

@ -20,14 +20,6 @@
{#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS} {#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS}
</p> </p>
</div> </div>
<h3>{#TOTAL_YIELD_PER_DAY}</h3>
<div>
<div class="chartDiv" id="ydChart"> </div>
<p>
{#MAXIMUM}: <span id="ydMax"></span> Wh<br />
{#UPDATED} <span id="ydRefresh"></span> {#SECONDS}
</p>
</div>
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}
@ -87,6 +79,8 @@
function parsePowerHistory(obj){ function parsePowerHistory(obj){
if (null != obj) { if (null != obj) {
parseNav(obj.generic);
parseESP(obj.generic);
parseHistory(obj,"pwr", pwrExeOnce) parseHistory(obj,"pwr", pwrExeOnce)
document.getElementById("pwrLast").innerHTML = mLastValue document.getElementById("pwrLast").innerHTML = mLastValue
document.getElementById("pwrMaxDay").innerHTML = obj.maxDay document.getElementById("pwrMaxDay").innerHTML = obj.maxDay
@ -94,20 +88,6 @@
if (pwrExeOnce) { if (pwrExeOnce) {
pwrExeOnce = false; pwrExeOnce = false;
window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000); window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000);
setTimeout(() => {
getAjax("/api/yieldDayHistory", parseYieldDayHistory);
} , 20);
}
}
function parseYieldDayHistory(obj) {
if (null != obj) {
parseNav(obj.generic);
parseHistory(obj, "yd", ydExeOnce)
}
if (ydExeOnce) {
ydExeOnce = false;
window.setInterval("getAjax('/api/yieldDayHistory', parseYieldDayHistory)", mRefresh * 500);
} }
} }

6
src/web/html/includes/nav.html

@ -10,15 +10,15 @@
<a id="nav11" class="acitve" href="/history?v={#VERSION}">{#NAV_HISTORY}</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="nav4" class="hide" href="/serial?v={#VERSION}">{#NAV_WEBSERIAL}</a>
<a id="nav5" class="hide" href="/setup?v={#VERSION}">{#NAV_SETTINGS}</a> <a id="nav5" class="hide" href="/setup?v={#VERSION}">{#NAV_SETTINGS}</a>
<span class="seperator"></span> <span class="separator"></span>
<a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a> <a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a>
<a id="nav7" class="hide" href="/system?v={#VERSION}">System</a> <a id="nav7" class="hide" href="/system?v={#VERSION}">System</a>
<span class="seperator"></span> <span class="separator"></span>
<a id="nav8" href="/api" target="_blank">REST API</a> <a id="nav8" href="/api" target="_blank">REST API</a>
<a id="nav9" href="https://ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</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="nav10" href="/about?v={#VERSION}">{#NAV_ABOUT}</a>
<a id="nav12" href="#" class="hide" target="_blank">Custom Link</a> <a id="nav12" href="#" class="hide" target="_blank">Custom Link</a>
<span class="seperator"></span> <span class="separator"></span>
<a id="nav0" class="hide" href="/login">Login</a> <a id="nav0" class="hide" href="/login">Login</a>
<a id="nav1" class="hide" href="/logout">Logout</a> <a id="nav1" class="hide" href="/logout">Logout</a>
</div> </div>

27
src/web/html/index.html

@ -41,27 +41,24 @@
var release = null; var release = null;
function apiCb(obj) { function apiCb(obj) {
var e = document.getElementById("apiResult"); var e = document.getElementById("apiResult")
if(obj.success) { if(obj.success) {
e.innerHTML = " {#COMMAND_EXE}"; e.innerHTML = " {#COMMAND_EXE}"
getAjax("/api/index", parse); getAjax("/api/index", parse)
} } else
else e.innerHTML = " {#ERROR}: " + obj.error
e.innerHTML = " {#ERROR}: " + obj.error;
} }
function setTime() { function setTime() {
var date = new Date(); var date = new Date()
var obj = new Object(); var obj = {cmd: "set_time", token: "*", val: parseInt(date.getTime() / 1000)}
obj.cmd = "set_time"; getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj))
obj.val = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj));
} }
function parseGeneric(obj) { function parseGeneric(obj) {
if(exeOnce) if(exeOnce)
parseESP(obj); parseESP(obj)
parseRssi(obj); parseRssi(obj)
} }
function parseSys(obj) { function parseSys(obj) {
@ -73,9 +70,9 @@
var min = parseInt(up / 60) % 60; var min = parseInt(up / 60) % 60;
var sec = up % 60; var sec = up % 60;
var e = document.getElementById("uptime"); var e = document.getElementById("uptime");
e.innerHTML = days + " Day"; e.innerHTML = days + " {#DAY}";
if(1 != days) if(1 != days)
e.innerHTML += "s"; e.innerHTML += "{#S}";
e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":" e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":" + ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2); + ("0"+sec).substr(-2);

18
src/web/html/serial.html

@ -12,12 +12,12 @@
<textarea id="serial" class="mt-3" cols="80" rows="40" readonly></textarea> <textarea id="serial" class="mt-3" cols="80" rows="40" readonly></textarea>
</div> </div>
<div class="row my-3"> <div class="row my-3">
<div class="col-3">console active: <span class="dot" id="active"></span></div> <div class="col-3">{#CONSOLE_ACTIVE}: <span class="dot" id="active"></span></div>
<div class="col-3 col-sm-4 my-3">Uptime: <span id="uptime"></span></div> <div class="col-3 col-sm-4 my-3">{#UPTIME}: <span id="uptime"></span></div>
<div class="col-6 col-sm-4 a-r"> <div class="col-6 col-sm-4 a-r">
<input type="button" value="clear" class="btn" id="clear"/> <input type="button" value="{#BTN_CLEAR}" class="btn" id="clear"/>
<input type="button" value="autoscroll" class="btn" id="scroll"/> <input type="button" value="{#BTN_AUTOSCROLL}" class="btn" id="scroll"/>
<input type="button" value="copy" class="btn" id="copy"/> <input type="button" value="{#BTN_COPY}" class="btn" id="copy"/>
</div> </div>
</div> </div>
</div> </div>
@ -35,7 +35,7 @@
var hrs = parseInt(up / 3600) % 24; var hrs = parseInt(up / 3600) % 24;
var min = parseInt(up / 60) % 60; var min = parseInt(up / 60) % 60;
var sec = up % 60; var sec = up % 60;
document.getElementById("uptime").innerHTML = days + " Days, " document.getElementById("uptime").innerHTML = days + " {#DAYS}, "
+ ("0"+hrs).substr(-2) + ":" + ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":" + ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2); + ("0"+sec).substr(-2);
@ -65,7 +65,7 @@
}); });
document.getElementById("scroll").addEventListener("click", function() { document.getElementById("scroll").addEventListener("click", function() {
mAutoScroll = !mAutoScroll; mAutoScroll = !mAutoScroll;
this.value = (mAutoScroll) ? "autoscroll" : "manual scroll"; this.value = (mAutoScroll) ? "{#BTN_AUTOSCROLL}" : "{#BTN_MANUALSCROLL}";
}); });
document.getElementById("copy").addEventListener("click", function() { document.getElementById("copy").addEventListener("click", function() {
con.value = version + " - " + build + "\n---------------\n" + con.value; con.value = version + " - " + build + "\n---------------\n" + con.value;
@ -80,10 +80,10 @@
try { try {
return document.execCommand("copy"); // Security exception may be thrown by some browsers. return document.execCommand("copy"); // Security exception may be thrown by some browsers.
} catch (ex) { } catch (ex) {
alert("Copy to clipboard failed" + ex); alert("{#CLIPBOARD_FAILED} " + ex);
} finally { } finally {
document.body.removeChild(ta); document.body.removeChild(ta);
alert("Copied to clipboard"); alert("{#COPIED_TO_CLIPBOARD}");
} }
} }
}); });

147
src/web/html/setup.html

@ -26,6 +26,14 @@
<div class="col-4 col-sm-9"><input type="checkbox" name="darkMode"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="darkMode"/></div>
<div class="col-12">{#DARK_MODE_NOTE}</div> <div class="col-12">{#DARK_MODE_NOTE}</div>
</div> </div>
<div class="row mb-3">
<div class="col-8 col-sm-3">{#REGION}</div>
<div class="col-4 col-sm-9" id="region"></div>
</div>
<div class="row mb-5">
<div class="col-8 col-sm-3">{#TIMEZONE}</div>
<div class="col-4 col-sm-9" id="timezone"></div>
</div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3">{#CUSTOM_LINK}</div> <div class="col-8 col-sm-3">{#CUSTOM_LINK}</div>
<div class="col-4 col-sm-9"><input type="text" name="cstLnk"/></div> <div class="col-4 col-sm-9"><input type="text" name="cstLnk"/></div>
@ -35,24 +43,8 @@
<div class="col-4 col-sm-9"><input type="text" name="cstLnkTxt"/></div> <div class="col-4 col-sm-9"><input type="text" name="cstLnkTxt"/></div>
</div> </div>
</fieldset> </fieldset>
<fieldset class="mb-4"> <fieldset class="mb-4" id="serialCb">
<legend class="des">{#SERIAL_CONSOLE}</legend> <legend class="des">{#SERIAL_CONSOLE}</legend>
<div class="row mb-3">
<div class="col-8 col-sm-3">{#LOG_PRINT_INVERTER_DATA}</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="serEn"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">{#LOG_SERIAL_DEBUG}</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="serDbg"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">{#LOG_PRIVACY_MODE}</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="priv"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">{#LOG_PRINT_TRACES}</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="wholeTrace"/></div>
</div>
</fieldset> </fieldset>
</div> </div>
@ -141,10 +133,6 @@
<div class="col-8 my-2">{#INTERVAL} [s]</div> <div class="col-8 my-2">{#INTERVAL} [s]</div>
<div class="col-4"><input type="number" name="invInterval" title="Invalid input"/></div> <div class="col-4"><input type="number" name="invInterval" title="Invalid input"/></div>
</div> </div>
<div class="row mb-3">
<div class="col-8 my-2">{#INV_GAP} [ms]</div>
<div class="col-4"><input type="number" name="invGap" title="Invalid input"/></div>
</div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 mb-2">{#INV_RESET_MIDNIGHT}</div> <div class="col-8 mb-2">{#INV_RESET_MIDNIGHT}</div>
<div class="col-4"><input type="checkbox" name="invRstMid"/></div> <div class="col-4"><input type="checkbox" name="invRstMid"/></div>
@ -169,10 +157,6 @@
<div class="col-8">{#INV_READ_GRID_PROFILE}</div> <div class="col-8">{#INV_READ_GRID_PROFILE}</div>
<div class="col-4"><input type="checkbox" name="rdGrid"/></div> <div class="col-4"><input type="checkbox" name="rdGrid"/></div>
</div> </div>
<div class="row mb-3">
<div class="col-8">{#INV_YIELD_EFF}</div>
<div class="col-4"><input type="number" name="yldEff" step="any"/></div>
</div>
</fieldset> </fieldset>
</div> </div>
@ -499,9 +483,6 @@
for(var i = 0; i < 31; i++) { for(var i = 0; i < 31; i++) {
esp32cmtPa.push([i, String(i-10) + " dBm"]); esp32cmtPa.push([i, String(i-10) + " dBm"]);
} }
for(var i = 12; i < 41; i++) {
esp32cmtFreq.push([i, freqFmt.format(860 + i*0.25) + " MHz"]);
}
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
var led_high_active = [ var led_high_active = [
[0, "{#PIN_LOW_ACTIVE}"], [0, "{#PIN_LOW_ACTIVE}"],
@ -558,31 +539,26 @@
} }
function setTime() { function setTime() {
var date = new Date(); var date = new Date()
var obj = new Object(); var obj = {cmd: "set_time", token: "*", val: parseInt(date.getTime() / 1000)}
obj.cmd = "set_time"; getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj))
obj.val = parseInt(date.getTime() / 1000); setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000)
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj));
setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000);
} }
function scan() { function scan() {
var obj = new Object(); var obj = {cmd: "scan_wifi", token: "*"}
obj.cmd = "scan_wifi";
getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj)); getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj));
setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 5000); setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 5000);
} }
function syncTime() { function syncTime() {
var obj = new Object(); var obj = {cmd: "sync_ntp", token: "*"}
obj.cmd = "sync_ntp"; getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj))
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj)); setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000)
setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000);
} }
function sendDiscoveryConfig() { function sendDiscoveryConfig() {
var obj = new Object(); var obj = {cmd: "discovery_cfg", token: "*"}
obj.cmd = "discovery_cfg";
getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj)); getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj));
} }
@ -625,7 +601,7 @@
} }
function ivGlob(obj) { function ivGlob(obj) {
for(var i of [["invInterval", "interval"], ["yldEff", "yldEff"], ["invGap", "gap"]]) for(var i of [["invInterval", "interval"]])
document.getElementsByName(i[0])[0].value = obj[i[1]]; document.getElementsByName(i[0])[0].value = obj[i[1]];
for(var i of ["Mid", "ComStop", "NotAvail", "MaxMid"]) for(var i of ["Mid", "ComStop", "NotAvail", "MaxMid"])
document.getElementsByName("invRst"+i)[0].checked = obj["rst" + i]; document.getElementsByName("invRst"+i)[0].checked = obj["rst" + i];
@ -650,6 +626,14 @@
el.push(mlCb("protMask" + i, a[i], chk)) el.push(mlCb("protMask" + i, a[i], chk))
} }
d.append(...el); d.append(...el);
var tz = []
for(i = 0; i < 24; i += 0.5)
tz.push([i, ((i-12 > 0) ? "+" : "") + String(i-12)]);
document.getElementById("timezone").append(sel("timezone", tz, obj.timezone + 12))
var region = [[0, "Europe (860 - 870 MHz)"], [1, "USA, Indonesia (905 - 925 MHz)"], [2, "Brazil (915 - 928 MHz)"]]
document.getElementById("region").append(sel("region", region, obj.region))
} }
function parseGeneric(obj) { function parseGeneric(obj) {
@ -702,7 +686,6 @@
add.ch_yield_cor = []; add.ch_yield_cor = [];
add.freq = 12; add.freq = 12;
add.pa = 30; add.pa = 30;
add.add2total = true;
var e = document.getElementById("inverter"); var e = document.getElementById("inverter");
e.innerHTML = ""; // remove all childs e.innerHTML = ""; // remove all childs
@ -713,6 +696,13 @@
ivGlob(obj); ivGlob(obj);
} }
function divRow(item0, item1) {
return ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-3 mt-2"}, item0),
ml("div", {class: "col-9"}, item1)
])
}
function ivModal(obj) { function ivModal(obj) {
var lines = []; var lines = [];
lines.push(ml("tr", {}, [ lines.push(ml("tr", {}, [
@ -733,62 +723,36 @@
var cbEn = ml("input", {name: "enable", type: "checkbox"}, null); var cbEn = ml("input", {name: "enable", type: "checkbox"}, null);
var cbDisNightCom = ml("input", {name: "disnightcom", type: "checkbox"}, null); var cbDisNightCom = ml("input", {name: "disnightcom", type: "checkbox"}, null);
var cbAddTotal = ml("input", {name: "add2total", type: "checkbox"}, null);
cbEn.checked = (obj.enabled); cbEn.checked = (obj.enabled);
cbDisNightCom.checked = (obj.disnightcom); cbDisNightCom.checked = (obj.disnightcom);
cbAddTotal.checked = (obj.add2total);
var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null); var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null);
var html = ml("div", {}, [ var html = ml("div", {}, [
tabs(["{#TAB_GENERAL}", "{#TAB_INPUTS}", "{#TAB_RADIO}", "{#TAB_ADVANCED}"]), tabs(["{#TAB_GENERAL}", "{#TAB_INPUTS}", "{#TAB_RADIO}", "{#TAB_ADVANCED}"]),
ml("div", {id: "div{#TAB_GENERAL}", class: "tab-content"}, [ ml("div", {id: "div{#TAB_GENERAL}", class: "tab-content"}, [
ml("div", {class: "row mb-3"}, [ divRow("{#INV_ENABLE}", cbEn),
ml("div", {class: "col-2"}, "{#INV_ENABLE}"), divRow("{#INV_SERIAL}", ser),
ml("div", {class: "col-10"}, cbEn) divRow("Name", ml("input", {name: "name", class: "text", type: "text", value: obj.name}, null))
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-2 mt-2"}, "{#INV_SERIAL}"),
ml("div", {class: "col-10"}, ser)
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-2 mt-2"}, "Name"),
ml("div", {class: "col-10"}, ml("input", {name: "name", class: "text", type: "text", value: obj.name}, null))
])
]), ]),
ml("div", {id: "div{#TAB_INPUTS}", class: "tab-content hide"}, [ ml("div", {id: "div{#TAB_INPUTS}", class: "tab-content hide"}, [
ml("div", {class: "row mb-3"}, ml("div", {class: "row mb-3"},
ml("table", {class: "table"}, ml("table", {class: "table"}, ml("tbody", {}, lines))
ml("tbody", {}, lines)
)
) )
]), ]),
ml("div", {id: "div{#TAB_RADIO}", class: "tab-content hide"}, [ ml("div", {id: "div{#TAB_RADIO}", class: "tab-content hide"}, [
ml("input", {type: "hidden", name: "isnrf"}, null), ml("input", {type: "hidden", name: "isnrf"}, null),
ml("div", {id: "setcmt"}, [ ml("div", {id: "setcmt"}, [
ml("div", {class: "row mb-3"}, [ divRow("{#INV_FREQUENCY}", sel("freq", esp32cmtFreq, obj.freq)),
ml("div", {class: "col-3 mt-2"}, "{#INV_FREQUENCY}"), divRow("{#INV_POWER_LEVEL}", sel("cmtpa", esp32cmtPa, obj.pa))
ml("div", {class: "col-9"}, sel("freq", esp32cmtFreq, obj.freq))
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-3 mt-2"}, "{#INV_POWER_LEVEL}"),
ml("div", {class: "col-9"}, sel("cmtpa", esp32cmtPa, obj.pa))
]),
]), ]),
ml("div", {id: "setnrf"}, ml("div", {id: "setnrf"},
ml("div", {class: "row mb-3"}, [ divRow("{#INV_POWER_LEVEL}", sel("nrfpa", nrfPa, obj.pa))
ml("div", {class: "col-3 mt-2"}, "{#INV_POWER_LEVEL}"),
ml("div", {class: "col-9"}, sel("nrfpa", nrfPa, obj.pa))
]),
), ),
]), ]),
ml("div", {id: "div{#TAB_ADVANCED}", class: "tab-content hide"}, [ ml("div", {id: "div{#TAB_ADVANCED}", class: "tab-content hide"}, [
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-10"}, "{#INV_PAUSE_DURING_NIGHT}"), ml("div", {class: "col-10"}, "{#INV_PAUSE_DURING_NIGHT}"),
ml("div", {class: "col-2"}, cbDisNightCom) ml("div", {class: "col-2"}, cbDisNightCom)
]),
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-10"}, "{#INV_INCLUDE_MQTT_SUM}"),
ml("div", {class: "col-2"}, cbAddTotal)
]) ])
]), ]),
ml("div", {class: "row mt-5"}, [ ml("div", {class: "row mt-5"}, [
@ -818,7 +782,8 @@
case 0x1000: nrf = true; break; case 0x1000: nrf = true; break;
case 0x1100: case 0x1100:
switch(sn & 0x000f) { switch(sn & 0x000f) {
case 0x0004: nrf = false; break; case 0x0004:
case 0x0005: nrf = false; break;
default: nrf = true; break; default: nrf = true; break;
} }
break; break;
@ -835,8 +800,9 @@
function ivSave() { function ivSave() {
var o = new Object(); var o = new Object();
o.cmd = "save_iv"; o.cmd = "save_iv"
o.id = obj.id; o.token = "*"
o.id = obj.id
o.ser = parseInt(document.getElementsByName("ser")[0].value, 16); o.ser = parseInt(document.getElementsByName("ser")[0].value, 16);
o.name = document.getElementsByName("name")[0].value; o.name = document.getElementsByName("name")[0].value;
o.en = document.getElementsByName("enable")[0].checked; o.en = document.getElementsByName("enable")[0].checked;
@ -854,7 +820,6 @@
o.pa = document.getElementsByName("cmtpa")[0].value; o.pa = document.getElementsByName("cmtpa")[0].value;
o.freq = document.getElementsByName("freq")[0].value; o.freq = document.getElementsByName("freq")[0].value;
o.disnightcom = document.getElementsByName("disnightcom")[0].checked; o.disnightcom = document.getElementsByName("disnightcom")[0].checked;
o.add2total = document.getElementsByName("add2total")[0].checked;
getAjax("/api/setup", cb, "POST", JSON.stringify(o)); getAjax("/api/setup", cb, "POST", JSON.stringify(o));
} }
@ -1019,12 +984,28 @@
]) ])
); );
} }
var i = 0
while(obj.freq_max >= (obj.freq_min + i * 0.25)) {
esp32cmtFreq.push([i, freqFmt.format(obj.freq_min + i * 0.25) + " MHz"])
i++
}
} }
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
function parseSerial(obj) { function parseSerial(obj) {
for(var i of [["serEn", "show_live_data"], ["serDbg", "debug"], ["priv", "priv"], ["wholeTrace", "wholeTrace"]]) var e = document.getElementById("serialCb")
document.getElementsByName(i[0])[0].checked = obj[i[1]]; var l = [["serEn", "show_live_data", "{#LOG_PRINT_INVERTER_DATA}"], ["serDbg", "debug", "{#LOG_SERIAL_DEBUG}"], ["priv", "priv", "{#LOG_PRIVACY_MODE}"], ["wholeTrace", "wholeTrace", "{#LOG_PRINT_TRACES}"], ["log2mqtt", "log2mqtt", "{#LOG_TO_MQTT}"]]
for(var i of l) {
var cb = ml("input", {name: i[0], type: "checkbox"}, null)
cb.checked = obj[i[1]]
e.appendChild(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-3"}, i[2]),
ml("div", {class: "col-4 col-sm-9"}, cb)
])
)
}
} }
function parseDisplay(obj, type, system) { function parseDisplay(obj, type, system) {

23
src/web/html/style.css

@ -16,11 +16,11 @@ span, li, h3, label, fieldset {
color: var(--fg); color: var(--fg);
} }
fieldset, input[type=submit], .btn { fieldset, input[type="submit"], .btn {
border-radius: 4px; border-radius: 4px;
} }
input[type=file] { input[type="file"] {
width: 100%; width: 100%;
} }
@ -33,7 +33,7 @@ textarea {
color: var(--fg2); color: var(--fg2);
} }
svg rect {fill: #0000AA;} svg rect {fill: #00A;}
svg.chart { svg.chart {
background: #f2f2f2; background: #f2f2f2;
border: 2px solid gray; border: 2px solid gray;
@ -44,6 +44,7 @@ div.chartDivContainer {
padding: 1px; padding: 1px;
margin: 1px; margin: 1px;
} }
div.chartdivContainer span { div.chartdivContainer span {
color: var(--fg2); color: var(--fg2);
} }
@ -95,7 +96,7 @@ svg.icon {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
margin-top:-4x; margin-top:-4x;
padding: 5px 7px 5px 0px; padding: 5px 7px 5px 0;
} }
.icon-info { .icon-info {
@ -138,10 +139,10 @@ svg.icon {
background-color: var(--nav-active); background-color: var(--nav-active);
} }
span.seperator { span.separator {
width: 100%; width: 100%;
height: 1px; height: 1px;
margin: 5px 0px 5px; margin: 5px 0 5px;
background-color: #494949; background-color: #494949;
display: block; display: block;
} }
@ -391,7 +392,7 @@ th {
#footer .left { #footer .left {
color: #bbb; color: #bbb;
margin: 23px 0px 0px 25px; margin: 23px 0 0 25px;
} }
#footer ul { #footer ul {
@ -525,7 +526,7 @@ input, select {
font-size: 13pt; 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%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #ccc; border: 1px solid #ccc;
@ -551,7 +552,7 @@ input.btnDel {
input.btn { input.btn {
background-color: var(--primary); background-color: var(--primary);
color: #fff; color: #fff;
border: 0px; border: 0;
padding: 7px 20px 7px 20px; padding: 7px 20px 7px 20px;
margin-bottom: 10px; margin-bottom: 10px;
text-transform: uppercase; text-transform: uppercase;
@ -572,7 +573,7 @@ label {
display: inline-block; display: inline-block;
font-size: 12pt; font-size: 12pt;
padding-right: 10px; padding-right: 10px;
margin: 10px 0px 0px 15px; margin: 10px 0 0 15px;
vertical-align: top; vertical-align: top;
} }
@ -601,7 +602,7 @@ div.ModPwr, div.ModName, div.YieldCor {
div.hr { div.hr {
height: 1px; height: 1px;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
margin: 10px 0px 10px; margin: 10px 0 10px;
} }
#note { #note {

31
src/web/html/visualization.html

@ -22,6 +22,15 @@
var total = Array(6).fill(0); var total = Array(6).fill(0);
var tPwrAck; 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) { function parseGeneric(obj) {
if(true == exeOnce){ if(true == exeOnce){
parseNav(obj); parseNav(obj);
@ -454,18 +463,20 @@
val = 100; val = 100;
var obj = new Object(); var obj = new Object();
obj.id = id; obj.id = id
obj.cmd = cmd; obj.token = "*"
obj.val = Math.round(val*10); obj.cmd = cmd
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj)); obj.val = val
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj))
} }
function applyCtrl(id, cmd, val=0) { function applyCtrl(id, cmd, val=0) {
var obj = new Object(); var obj = new Object();
obj.id = id; obj.id = id
obj.cmd = cmd; obj.token = "*"
obj.val = val; obj.cmd = cmd
getAjax("/api/ctrl", ctrlCb2, "POST", JSON.stringify(obj)); obj.val = val
getAjax("/api/ctrl", ctrlCb2, "POST", JSON.stringify(obj))
} }
function ctrlCb(obj) { function ctrlCb(obj) {
@ -475,7 +486,7 @@
tPwrAck = window.setInterval("getAjax('/api/inverter/pwrack/" + obj.id + "', updatePwrAck)", 1000); tPwrAck = window.setInterval("getAjax('/api/inverter/pwrack/" + obj.id + "', updatePwrAck)", 1000);
} }
else else
e.innerHTML = "{#ERROR}: " + obj["error"]; e.innerHTML = "{#ERROR}: " + getErrStr(obj.error);
} }
function ctrlCb2(obj) { function ctrlCb2(obj) {
@ -483,7 +494,7 @@
if(obj.success) if(obj.success)
e.innerHTML = "{#COMMAND_RECEIVED}"; e.innerHTML = "{#COMMAND_RECEIVED}";
else else
e.innerHTML = "{#ERROR}: " + obj["error"]; e.innerHTML = "{#ERROR}: " + getErrStr(obj.error);
} }
function updatePwrAck(obj) { function updatePwrAck(obj) {

16
src/web/lang.h

@ -19,21 +19,9 @@
#endif #endif
#ifdef LANG_DE #ifdef LANG_DE
#define INV_INDEX_INVALID "Wechselrichterindex ungültig; " #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*/ #else /*LANG_EN*/
#define INV_INDEX_INVALID "inverter index invalid: " #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 UNKNOWN_CMD "unbekanntes Kommando: '"
#else /*LANG_EN*/
#define UNKNOWN_CMD "unknown cmd: '"
#endif
#ifdef LANG_DE
#define INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT "Leistungsbegrenzung / Ansteuerung aktuell nicht möglich"
#else /*LANG_EN*/
#define INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT "inverter does not accept dev control request at this moment"
#endif #endif
#ifdef LANG_DE #ifdef LANG_DE

123
src/web/lang.json

@ -148,6 +148,16 @@
"en": "(empty browser cache or use CTRL + F5 after reboot to apply this setting)", "en": "(empty browser cache or use CTRL + F5 after reboot to apply this setting)",
"de": "(der Browser-Cache muss geleert oder STRG + F5 gedr&uuml;ckt werden, um diese Einstellung zu aktivieren)" "de": "(der Browser-Cache muss geleert oder STRG + F5 gedr&uuml;ckt werden, um diese Einstellung zu aktivieren)"
}, },
{
"token": "REGION",
"en": "Region",
"de": "Region"
},
{
"token": "TIMEZONE",
"en": "Timezone",
"de": "Zeitzone"
},
{ {
"token": "CUSTOM_LINK", "token": "CUSTOM_LINK",
"en": "Custom link (leave empty to hide element in navigation)", "en": "Custom link (leave empty to hide element in navigation)",
@ -193,6 +203,11 @@
"en": "Print whole traces in Log", "en": "Print whole traces in Log",
"de": "alle Informationen in Log schreiben" "de": "alle Informationen in Log schreiben"
}, },
{
"token": "LOG_TO_MQTT",
"en": "Send Serial debug over MqTT",
"de": "sende serielles Log &uuml;ber MqTT"
},
{ {
"token": "NETWORK", "token": "NETWORK",
"en": "Network", "en": "Network",
@ -278,11 +293,6 @@
"en": "Interval", "en": "Interval",
"de": "Intervall" "de": "Intervall"
}, },
{
"token": "INV_GAP",
"en": "Communication Gap",
"de": "Kommunikationsl&uuml;cke"
},
{ {
"token": "INV_RESET_MIDNIGHT", "token": "INV_RESET_MIDNIGHT",
"en": "Reset values and YieldDay at midnight", "en": "Reset values and YieldDay at midnight",
@ -313,11 +323,6 @@
"en": "Read Grid Profile", "en": "Read Grid Profile",
"de": "Grid-Profil auslesen" "de": "Grid-Profil auslesen"
}, },
{
"token": "INV_YIELD_EFF",
"en": "Yield Efficiency (default 1.0)",
"de": "Ertragseffizienz (Standard 1.0)"
},
{ {
"token": "NTP_INTERVAL", "token": "NTP_INTERVAL",
"en": "NTP Interval (in minutes, min. 5 minutes)", "en": "NTP Interval (in minutes, min. 5 minutes)",
@ -653,11 +658,6 @@
"en": "Pause communication during night (lat. and lon. need to be set)", "en": "Pause communication during night (lat. and lon. need to be set)",
"de": "Kommunikation w&auml;hrend der Nacht pausieren (Breiten- und L&auml;ngengrad m&uuml;ssen gesetzt sein" "de": "Kommunikation w&auml;hrend der Nacht pausieren (Breiten- und L&auml;ngengrad m&uuml;ssen gesetzt sein"
}, },
{
"token": "INV_INCLUDE_MQTT_SUM",
"en": "Include inverter to sum of total (should be checked by default, MqTT only)",
"de": "Wechselrichter in Liste der aufzuaddierenden Wechselrichter aufnehmen (nur MqTT)"
},
{ {
"token": "BTN_SAVE", "token": "BTN_SAVE",
"en": "save", "en": "save",
@ -670,7 +670,7 @@
}, },
{ {
"token": "INV_DELETE_SURE", "token": "INV_DELETE_SURE",
"en": "do you realy want to delete inverter?", "en": "do you really want to delete inverter?",
"de": "Willst du den Wechselrichter wirklich l&ouml;schen?" "de": "Willst du den Wechselrichter wirklich l&ouml;schen?"
}, },
{ {
@ -865,6 +865,56 @@
} }
] ]
}, },
{
"name": "serial.html",
"list": [
{
"token": "BTN_CLEAR",
"en": "clear",
"de": "l&ouml;schen"
},
{
"token": "BTN_AUTOSCROLL",
"en": "autoscroll",
"de": "automatisch scrollen"
},
{
"token": "BTN_MANUALSCROLL",
"en": "manual scroll",
"de": "manuell scrollen"
},
{
"token": "BTN_COPY",
"en": "copy",
"de": "kopieren"
},
{
"token": "CONSOLE_ACTIVE",
"en": "console active",
"de": "Konsole aktiv"
},
{
"token": "UPTIME",
"en": "uptime",
"de": "Laufzeit"
},
{
"token": "DAYS",
"en": "days",
"de": "Tage"
},
{
"token": "COPIED_TO_CLIPBOARD",
"en": "Copied to clipboard",
"de": "in die Zwischenablage kopiert"
},
{
"token": "CLIPBOARD_FAILED",
"en": "Copy failed",
"de": "kopieren fehlgeschlagen"
}
]
},
{ {
"name": "index.html", "name": "index.html",
"list": [ "list": [
@ -938,6 +988,16 @@
"en": "Error", "en": "Error",
"de": "Fehler" "de": "Fehler"
}, },
{
"token": "DAY",
"en": "day",
"de": "Tag"
},
{
"token": "S",
"en": "s",
"de": "e"
},
{ {
"token": "NTP_UNREACH", "token": "NTP_UNREACH",
"en": "NTP timeserver unreachable", "en": "NTP timeserver unreachable",
@ -951,7 +1011,7 @@
{ {
"token": "NIGHT_TIME", "token": "NIGHT_TIME",
"en": "Night time, inverter polling disabled", "en": "Night time, inverter polling disabled",
"de": "Wechselrichterabfrage deaktivert (Nacht)" "de": "Wechselrichterabfrage deaktiviert (Nacht)"
}, },
{ {
"token": "PAUSED_AT", "token": "PAUSED_AT",
@ -1070,7 +1130,7 @@
}, },
{ {
"token": "WARN_DIFF_ENV", "token": "WARN_DIFF_ENV",
"en": "your environment does not match the update file!", "en": "your environment may not match the update file!",
"de": "Die ausgew&auml;hlte Firmware passt u.U. nicht zum Chipsatz!" "de": "Die ausgew&auml;hlte Firmware passt u.U. nicht zum Chipsatz!"
}, },
{ {
@ -1386,7 +1446,7 @@
{ {
"token": "CMD_RECEIVED_WAIT_ACK", "token": "CMD_RECEIVED_WAIT_ACK",
"en": "received command, waiting for inverter acknowledge ...", "en": "received command, waiting for inverter acknowledge ...",
"de": "Befehl erhalten, warte auf Best&auml;igung von Wechselrichter ..." "de": "Befehl erhalten, warte auf Best&auml;igung vom Wechselrichter ..."
}, },
{ {
"token": "COMMAND_RECEIVED", "token": "COMMAND_RECEIVED",
@ -1402,6 +1462,31 @@
"token": "INV_ACK", "token": "INV_ACK",
"en": "inverter acknowledged active power control command", "en": "inverter acknowledged active power control command",
"de": "Wechselrichter hat die Leistungsbegrenzung akzeptiert" "de": "Wechselrichter hat die Leistungsbegrenzung akzeptiert"
},
{
"token": "ERR_AUTH",
"en": "authentication error",
"de": "Authentifizierungsfehler"
},
{
"token": "ERR_INDEX",
"en": "inverter index invalid",
"de": "Wechselrichterindex ungültig"
},
{
"token": "ERR_UNKNOWN_CMD",
"en": "unknown cmd",
"de": "unbekanntes Kommando"
},
{
"token": "ERR_LIMIT_NOT_ACCEPT",
"en": "inverter does not accept dev control request at this moment",
"de": "Leistungsbegrenzung / Ansteuerung aktuell nicht m&ouml;glich"
},
{
"token": "ERR_PROTECTED",
"en": "not logged in, command not possible!",
"de": "nicht angemeldet, Kommando nicht m&ouml;glich!"
} }
] ]
}, },

115
src/web/web.h

@ -43,13 +43,7 @@ template <class HMSYSTEM>
class Web { class Web {
public: public:
Web(void) : mWeb(80), mEvts("/events") { Web(void) : mWeb(80), mEvts("/events") {
mProtected = true;
mLogoutTimeout = 0;
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
mSerialAddTime = true;
mSerialClientConnnected = false;
} }
void setup(IApp *app, HMSYSTEM *sys, settings_t *config) { void setup(IApp *app, HMSYSTEM *sys, settings_t *config) {
@ -106,16 +100,6 @@ class Web {
} }
void tickSecond() { 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 (mSerialClientConnnected) {
if (mSerialBufFill > 0) { if (mSerialBufFill > 0) {
mEvts.send(mSerialBuf, "serial", millis()); mEvts.send(mSerialBuf, "serial", millis());
@ -129,27 +113,6 @@ class Web {
return &mWeb; 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) { void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!index) { if (!index) {
Serial.printf("Update Start: %s\n", filename.c_str()); Serial.printf("Update Start: %s\n", filename.c_str());
@ -260,7 +223,7 @@ class Web {
} }
void checkProtection(AsyncWebServerRequest *request) { void checkProtection(AsyncWebServerRequest *request) {
if(isProtected(request)) { if(mApp->isProtected(request->client()->remoteIP().toString().c_str(), "", true)) {
checkRedirect(request); checkRedirect(request);
return; return;
} }
@ -347,8 +310,7 @@ class Web {
if (request->args() > 0) { if (request->args() > 0) {
if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) { if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) {
mProtected = false; mApp->unlock(request->client()->remoteIP().toString().c_str(), true);
ah::ip2Arr(mLoginIp, request->client()->remoteIP().toString().c_str());
request->redirect("/"); request->redirect("/");
} }
} }
@ -362,8 +324,7 @@ class Web {
DPRINTLN(DBG_VERBOSE, F("onLogout")); DPRINTLN(DBG_VERBOSE, F("onLogout"));
checkProtection(request); checkProtection(request);
mApp->lock(true);
mProtected = true;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
@ -386,7 +347,6 @@ class Web {
void onCss(AsyncWebServerRequest *request) { void onCss(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onCss")); DPRINTLN(DBG_VERBOSE, F("onCss"));
mLogoutTimeout = LOGOUT_TIMEOUT;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
if(request->hasParam("v")) { if(request->hasParam("v")) {
@ -477,7 +437,8 @@ class Web {
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
mConfig->sys.darkMode = (request->arg("darkMode") == "on"); mConfig->sys.darkMode = (request->arg("darkMode") == "on");
mConfig->sys.schedReboot = (request->arg("schedReboot") == "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") != "") { if (request->arg("cstLnk") != "") {
request->arg("cstLnk").toCharArray(mConfig->plugin.customLink, MAX_CUSTOM_LINK_LEN); request->arg("cstLnk").toCharArray(mConfig->plugin.customLink, MAX_CUSTOM_LINK_LEN);
@ -490,7 +451,7 @@ class Web {
// protection // protection
if (request->arg("adminpwd") != "{PWD}") { if (request->arg("adminpwd") != "{PWD}") {
request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN); request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN);
mProtected = (strlen(mConfig->sys.adminPwd) > 0); mApp->lock(false);
} }
mConfig->sys.protectionMask = 0x0000; mConfig->sys.protectionMask = 0x0000;
for (uint8_t i = 0; i < 7; i++) { for (uint8_t i = 0; i < 7; i++) {
@ -518,14 +479,11 @@ class Web {
mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on"); mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on");
mConfig->inst.readGrid = (request->arg("rdGrid") == "on"); mConfig->inst.readGrid = (request->arg("rdGrid") == "on");
mConfig->inst.rstMaxValsMidNight = (request->arg("invRstMaxMid") == "on"); mConfig->inst.rstMaxValsMidNight = (request->arg("invRstMaxMid") == "on");
mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat();
mConfig->inst.gapMs = (request->arg("invGap")).toInt();
// pinout // pinout
uint8_t pin;
for (uint8_t i = 0; i < 16; i++) { for (uint8_t i = 0; i < 16; i++) {
pin = request->arg(String(pinArgNames[i])).toInt(); uint8_t pin = request->arg(String(pinArgNames[i])).toInt();
switch(i) { switch(i) {
case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN); break; case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN); break;
case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_NRF_CE_PIN); break; case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_NRF_CE_PIN); break;
@ -589,6 +547,7 @@ class Web {
mConfig->serial.privacyLog = (request->arg("priv") == "on"); mConfig->serial.privacyLog = (request->arg("priv") == "on");
mConfig->serial.printWholeTrace = (request->arg("wholeTrace") == "on"); mConfig->serial.printWholeTrace = (request->arg("wholeTrace") == "on");
mConfig->serial.showIv = (request->arg("serEn") == "on"); mConfig->serial.showIv = (request->arg("serEn") == "on");
mConfig->serial.log2mqtt = (request->arg("log2mqtt") == "on");
// display // display
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on"); mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on");
@ -666,8 +625,8 @@ class Web {
// NOTE: Grouping for fields with channels and totals is currently not working // NOTE: Grouping for fields with channels and totals is currently not working
// TODO: Handle grouping and sorting for independant from channel number // 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) // NOTE: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels)
const char * metricConstPrefix = "ahoy_solar_"; const char* metricConstPrefix = "ahoy_solar_";
const char * metricConstInverterFormat = " {inverter=\"%s\"} %d\n"; const char* metricConstInverterFormat = " {inverter=\"%s\"} %d\n";
typedef enum { typedef enum {
metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2, metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2,
metricsStateInverterProducing=3, metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5, metricsStateInverterProducing=3, metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5,
@ -681,7 +640,7 @@ class Web {
metricsStateStart, metricsStateStart,
metricsStateEnd metricsStateEnd
} MetricStep_t; } MetricStep_t;
MetricStep_t metricsStep; MetricStep_t metricsStep = metricsStateInverterInfo;
typedef struct { typedef struct {
const char *topic; const char *topic;
const char *type; const char *type;
@ -693,12 +652,12 @@ class Web {
{ "is_enabled", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} }, { "is_enabled", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} },
{ "is_available", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} }, { "is_available", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} },
{ "is_producing", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} }, { "is_producing", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} },
{ "power_limit_read", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return (int64_t)ah::round3(iv->actPowerLimit);} }, { "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;} }, { "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();} }, { "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_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", "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_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnswer;} },
{ "radio_frame_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, { "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_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_retransmits", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} },
@ -707,9 +666,6 @@ class Web {
{ "radio_dtu_loss_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuLoss;} }, { "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;} } { "radio_dtu_sent_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuSent;} }
}; };
int metricsInverterId;
uint8_t metricsFieldId;
bool metricDeclared, metricTotalDeclard;
void showMetrics(AsyncWebServerRequest *request) { void showMetrics(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
@ -724,7 +680,6 @@ class Web {
char type[60], topic[100], val[25]; char type[60], topic[100], val[25];
size_t len = 0; size_t len = 0;
int alarmChannelId; int alarmChannelId;
int metricsChannelId;
// Perform grouping on metrics according to format specification // Perform grouping on metrics according to format specification
// Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops. // Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops.
@ -748,7 +703,7 @@ class Web {
snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricConstPrefix, mConfig->sys.deviceName, WiFi.RSSI()); snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricConstPrefix, mConfig->sys.deviceName, WiFi.RSSI());
metrics += String(type) + String(topic); metrics += String(type) + String(topic);
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); len = snprintf(reinterpret_cast<char*>(buffer), maxLen,"%s",metrics.c_str());
// Next is Inverter information // Next is Inverter information
metricsStep = metricsStateInverterInfo; metricsStep = metricsStateInverterInfo;
break; break;
@ -776,7 +731,7 @@ class Web {
(String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].topic + (String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].topic +
inverterMetrics[metricsStep].format).c_str(), inverterMetrics[metricsStep].format).c_str(),
inverterMetrics[metricsStep].valueFunc); inverterMetrics[metricsStep].valueFunc);
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); len = snprintf(reinterpret_cast<char*>(buffer), maxLen, "%s", metrics.c_str());
// ugly hack to increment the enum // ugly hack to increment the enum
metricsStep = static_cast<MetricStep_t>( static_cast<int>(metricsStep) + 1); metricsStep = static_cast<MetricStep_t>( static_cast<int>(metricsStep) + 1);
// Prepare Realtime Field loop, which may be startet next // Prepare Realtime Field loop, which may be startet next
@ -796,7 +751,7 @@ class Web {
metrics = "# Info: all realtime fields processed\n"; metrics = "# Info: all realtime fields processed\n";
metricsStep = metricsStateAlarmData; metricsStep = metricsStateAlarmData;
} }
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); len = snprintf(reinterpret_cast<char *>(buffer), maxLen, "%s", metrics.c_str());
break; break;
case metricStateRealtimeInverterId: // Iterate over all inverters for this field case metricStateRealtimeInverterId: // Iterate over all inverters for this field
@ -806,7 +761,7 @@ class Web {
iv = mSys->getInverterByPos(metricsInverterId); iv = mSys->getInverterByPos(metricsInverterId);
if (NULL != iv) { if (NULL != iv) {
rec = iv->getRecordStruct(RealTimeRunData_Debug); 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; uint8_t channel = rec->assign[metricsChannelId].ch;
// Try inverter channel (channel 0) or any channel with maxPwr > 0 // Try inverter channel (channel 0) or any channel with maxPwr > 0
@ -823,10 +778,10 @@ class Web {
// report value // report value
if (0 == channel) { if (0 == channel) {
// Report a _total value if also channel values were reported. Otherwise report without _total // Report a _total value if also channel values were reported. Otherwise report without _total
char total[7]; char total[7] = {0};
if (metricDeclared) { if (metricDeclared) {
// A declaration and value for channels have been delivered. So declare and deliver a _total metric // A declaration and value for channels have been delivered. So declare and deliver a _total metric
strncpy(total, "_total", 6); snprintf(total, sizeof(total), "_total");
} }
if (!metricTotalDeclard) { 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()); snprintf(type, sizeof(type), "# TYPE %s%s%s%s %s\n",metricConstPrefix, iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), total, promType.c_str());
@ -870,7 +825,7 @@ class Web {
metricsFieldId++; // Process next field Id metricsFieldId++; // Process next field Id
metricsStep = metricStateRealtimeFieldId; metricsStep = metricStateRealtimeFieldId;
} }
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); len = snprintf(reinterpret_cast<char *>(buffer), maxLen, "%s", metrics.c_str());
break; break;
case metricsStateAlarmData: // Alarm Info loop : fit to one packet case metricsStateAlarmData: // Alarm Info loop : fit to one packet
@ -894,7 +849,7 @@ class Web {
} }
} }
} }
len = snprintf((char*)buffer,maxLen,"%s",metrics.c_str()); len = snprintf(reinterpret_cast<char*>(buffer), maxLen, "%s", metrics.c_str());
metricsStep = metricsStateEnd; metricsStep = metricsStateEnd;
break; break;
@ -913,10 +868,9 @@ class Web {
// Traverse all inverters and collect the metric via valueFunc // 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)> valueFunc) { String inverterMetric(char *buffer, size_t len, const char *format, std::function<uint64_t(Inverter<> *iv)> valueFunc) {
Inverter<> *iv;
String metric = ""; String metric = "";
for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { for (int id = 0; id < mSys->getNumInverters();id++) {
iv = mSys->getInverterByPos(metricsInverterId); Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) { if (NULL != iv) {
snprintf(buffer,len,format,iv->config->name, valueFunc(iv)); snprintf(buffer,len,format,iv->config->name, valueFunc(iv));
metric += String(buffer); metric += String(buffer);
@ -937,24 +891,27 @@ class Web {
if(shortUnit == "Hz") return {"_hertz", "gauge"}; if(shortUnit == "Hz") return {"_hertz", "gauge"};
return {"", "gauge"}; return {"", "gauge"};
} }
private:
int metricsInverterId = 0;
uint8_t metricsFieldId = 0;
bool metricDeclared = false, metricTotalDeclard = false;
#endif #endif
private:
AsyncWebServer mWeb; AsyncWebServer mWeb;
AsyncEventSource mEvts; AsyncEventSource mEvts;
bool mProtected; IApp *mApp = nullptr;
uint32_t mLogoutTimeout; HMSYSTEM *mSys = nullptr;
uint8_t mLoginIp[4];
IApp *mApp;
HMSYSTEM *mSys;
settings_t *mConfig; settings_t *mConfig = nullptr;
bool mSerialAddTime; bool mSerialAddTime = true;
char mSerialBuf[WEB_SERIAL_BUF_SIZE]; char mSerialBuf[WEB_SERIAL_BUF_SIZE];
uint16_t mSerialBufFill; uint16_t mSerialBufFill = 0;
bool mSerialClientConnnected; bool mSerialClientConnnected = false;
File mUploadFp; File mUploadFp;
bool mUploadFail; bool mUploadFail = false;
}; };
#endif /*__WEB_H__*/ #endif /*__WEB_H__*/

2
src/wifi/ahoywifi.cpp

@ -92,6 +92,8 @@ void ahoywifi::tickWifiLoop() {
} }
#if !defined(ESP32) #if !defined(ESP32)
MDNS.update(); MDNS.update();
if(WiFi.channel() > 11)
mWasInCh12to14 = true;
#endif #endif
return; return;
case IN_AP_MODE: case IN_AP_MODE:

21
src/wifi/ahoywifi.h

@ -37,6 +37,10 @@ class ahoywifi {
} }
void setupStation(void); void setupStation(void);
bool getWasInCh12to14() const {
return mWasInCh12to14;
}
private: private:
typedef enum WiFiStatus { typedef enum WiFiStatus {
DISCONNECTED = 0, DISCONNECTED = 0,
@ -67,7 +71,7 @@ class ahoywifi {
void welcome(String ip, String mode); void welcome(String ip, String mode);
settings_t *mConfig = NULL; settings_t *mConfig = nullptr;
appWifiCb mAppWifiCb; appWifiCb mAppWifiCb;
DNSServer mDns; DNSServer mDns;
@ -77,15 +81,16 @@ class ahoywifi {
WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler; WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler;
#endif #endif
WiFiStatus_t mStaConn; WiFiStatus_t mStaConn = DISCONNECTED;
uint8_t mCnt; uint8_t mCnt = 0;
uint32_t *mUtcTimestamp; uint32_t *mUtcTimestamp = nullptr;
uint8_t mScanCnt; uint8_t mScanCnt = 0;
bool mScanActive; bool mScanActive = false;
bool mGotDisconnect; bool mGotDisconnect = false;
std::list<uint8_t> mBSSIDList; std::list<uint8_t> mBSSIDList;
bool mStopApAllowed; bool mStopApAllowed = false;
bool mWasInCh12to14 = false;
}; };
#endif /*__AHOYWIFI_H__*/ #endif /*__AHOYWIFI_H__*/

Loading…
Cancel
Save