Browse Source

Merge branch 'development03' into ethW5500

pull/1512/head
lumapu 11 months 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:
push:
@ -8,13 +8,15 @@ on:
jobs:
check:
name: Check Repository
runs-on: ubuntu-latest
if: github.repository == 'lumapu/ahoy' && github.ref_name == 'development03'
continue-on-error: true
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
build-en:
name: Build (EN)
needs: check
runs-on: ubuntu-latest
continue-on-error: true
@ -32,14 +34,14 @@ jobs:
- opendtufusion
- opendtufusion-ethernet
steps:
- uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- name: Cache Pip
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@ -47,13 +49,13 @@ jobs:
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
uses: actions/setup-python@v4.3.0
uses: actions/setup-python@v5
with:
python-version: "3.x"
@ -75,6 +77,7 @@ jobs:
path: firmware/*
build-de:
name: Build (DE)
needs: check
runs-on: ubuntu-latest
continue-on-error: true
@ -92,14 +95,14 @@ jobs:
- opendtufusion-de
- opendtufusion-ethernet-de
steps:
- uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- name: Cache Pip
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@ -107,13 +110,13 @@ jobs:
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
uses: actions/setup-python@v4.3.0
uses: actions/setup-python@v5
with:
python-version: "3.x"
@ -135,10 +138,12 @@ jobs:
path: firmware/*
deploy:
name: Update Artifacts / Deploy
needs: [build-en, build-de]
runs-on: ubuntu-latest
continue-on-error: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
#- name: Copy boot_app0.bin
# run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
@ -155,13 +160,39 @@ jobs:
- name: Set Version
uses: cschleiden/replace-tokens@v1
with:
files: tools/esp8266/User_Manual.md
files: manual/User_Manual.md
env:
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
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
uses: nogsantos/scp-deploy@master
with:

166
.github/workflows/compile_release.yml

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

8
README.md

@ -31,7 +31,7 @@ Table of approaches:
| Board | MI | HM | HMS/HMT | comment | HowTo start |
| ------ | -- | -- | ------- | ------- | ---------- |
| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
| [ESP8266/ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | |
| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
@ -39,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**
## 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)
3. [Ahoy Configuration ](ahoy_config.md)
3. [Ahoy Configuration ](manual/ahoy_config.md)
## Our Website
[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)
<- Our sister project for Hoymiles HM- and HMS-/HMT-series (for ESP32 only!)
- [hms-mqtt-publisher](https://github.com/DennisOSRM/hms-mqtt-publisher)
<- a project which can handle WiFi inverters like HMS-XXXXW-2T
<- a project which can handle WiFi inverters like HMS-XXXXW-2T

55
manual/User_Manual.md

@ -166,6 +166,8 @@ inverter/ctrl/limit/0 600W
### Power Limit persistent
This feature was removed. The persisten limit should not be modified cyclic by a script because of potential wearout of the flash inside the inverter.
## Control via REST API
### Generic Information
@ -174,6 +176,46 @@ The rest API works with *JSON* POST requests. All the following instructions mus
👆 `<INVERTER_ID>` is the number of the specific inverter in the setup page.
### Authentication (new for versions > `0.8.79`)
The authentication is only needed if a password was set.
To authenticate from API you have to add the following `JSON` to your request:
```json
{
"auth": <PASSOWRD>
}
```
`<PASSWORD>` is your DTU password in plain text.
As Response you get the following `JSON` if successful:
```json
{
"success": true,
"token": "<TOKEN>"
}
```
Where `<TOKEN>` is a random token with a length of 16 characters.
For all following commands you have only to include the token into your `JSON`:
```json
{
"token": "<TOKEN>"
}
```
ℹ️ Do not pass the plain text password with each command. Authenticate once and then use the token for all following commands. The token expires once the token wasn't sent for 20 minutes.
If the authentication fails or the token is expired you will receive the following `JSON`:
```json
{
"success": false,
"error": "ERR_PROTECTED"
}
```
### Inverter Power (On / Off)
```json
@ -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]`
### Developer Information REST API (obsolete)
In the same approach as for MQTT any other SubCmd and also MainCmd can be applied and the response payload can be observed in the serial logs. Eg. request the Alarm-Data from the Alarm-Index 5 from inverter 0 will look like this:
```json
{
"inverter":0,
"tx_request": 21,
"cmd": 17,
"payload": 5,
"payload2": 0
}
```
## Zero Export Control (needs rework)
* You can use the mqtt topic `<TOPIC>/devcontrol/<INVERTER_ID>/11` with a number as payload (eg. 300 -> 300 Watt) to set the power limit to the published number in Watt. (In regular cases the inverter will use the new set point within one intervall period; to verify this see next bullet)
* 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)
process = subprocess.run(['git', 'apply', '--reverse', '--check', '../../../../' + patchFile], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
process = subprocess.run(['git', 'apply', '--ignore-whitespace', '--reverse', '--check', '../../../../' + patchFile], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if (process.returncode == 0):
print('\'' + patchFile + '\' already applied')
else:
process = subprocess.run(['git', 'apply', '../../../../' + patchFile])
process = subprocess.run(['git', 'apply', '--ignore-whitespace', '../../../../' + patchFile])
if (process.returncode == 0):
print('\'' + patchFile + '\' applied')
else:
@ -32,5 +32,3 @@ if env['PIOENV'][:22] != "opendtufusion-ethernet":
if env['PIOENV'][:13] == "opendtufusion":
applyPatch("GxEPD2", "../patches/GxEPD2_SW_SPI.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/partitions.bin", "offset": 32768})
esp32["parts"].append({"path": "ESP32/ota.bin", "offset": 57344})
esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32.bin", "offset": 65536})
esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32-wroom32.bin", "offset": 65536})
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["chipFamily"] = "ESP8266"
esp8266["parts"] = []
@ -47,7 +65,7 @@ def buildManifest(path, infile, outfile):
jsonString = json.dumps(data, indent=2)
fp = open(path + "firmware/" + outfile, "w")
fp = open(path + "../firmware/" + outfile, "w")
fp.write(jsonString)
fp.close()

128
src/CHANGES.md

@ -1,5 +1,127 @@
# 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
* add `ARC` to log (NRF24 Debug)
* merge PR: ETH NTP update bugfix #1385
@ -148,7 +270,7 @@
## 0.8.39 - 2024-01-01
* 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
* merge Prometheus metrics fix #1310
* merge MI grid profile request #1306
@ -160,7 +282,7 @@
## 0.8.37 - 2023-12-30
* added grid profiles
* format version of grid profile
* format version of grid profile
# RELEASE 0.8.36 - 2023-12-30
@ -361,7 +483,7 @@
## 0.7.61 - 2023-10-01
* merged `hmPayload` and `hmsPayload` into single class
* 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 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(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
#ifdef ETHERNET
@ -64,7 +67,7 @@ void app::setup() {
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));
#if defined(ENABLE_MQTT)
mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); });
@ -97,9 +100,8 @@ void app::setup() {
esp_task_wdt_reset();
mWeb.setup(this, &mSys, mConfig);
mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig);
mProtection = Protection::getInstance(mConfig->sys.adminPwd);
#ifdef ENABLE_SYSLOG
mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback)
@ -182,6 +184,8 @@ void app::onNetwork(bool gotIp) {
void app::regularTickers(void) {
DPRINTLN(DBG_DEBUG, F("regularTickers"));
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
everySec([this]() { mProtection->tickSecond(); }, "prot");
// Plugins
#if defined(PLUGIN_DISPLAY)
if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
@ -227,7 +231,6 @@ void app::updateNtp(void) {
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
if (mConfig->sys.schedReboot) {
uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght
if (rebootTrig <= mTimestamp) { //necessary for times other than midnight to prevent reboot loop
rebootTrig += 86400;
@ -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;
tickCalcSunrise();
}
@ -261,11 +264,10 @@ void app::tickNtpUpdate(void) {
#endif
if (isOK) {
this->updateNtp();
nxtTrig = isOK ? (mConfig->ntp.interval * 60) : 60; // depending on NTP update success check again in 12h (depends on setting) or in 1 min
nxtTrig = mConfig->ntp.interval * 60; // check again in 12h
// immediately start communicating
if (isOK && mSendFirst) {
if (mSendFirst) {
mSendFirst = false;
once(std::bind(&app::tickSend, this), 1, "senOn");
}
@ -300,9 +302,8 @@ void app::tickIVCommunication(void) {
bool zeroValues = false;
uint32_t nxtTrig = 0;
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys.getInverterByPos(i);
Inverter<> *iv = mSys.getInverterByPos(i);
if(NULL == iv)
continue;
@ -389,10 +390,9 @@ void app::tickMidnight(void) {
// clear max values
if(mConfig->inst.rstMaxValsMidNight) {
uint8_t pos;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t i = 0; i <= iv->channels; i++) {
pos = iv->getPosByChFld(i, FLD_MP, rec);
uint8_t pos = iv->getPosByChFld(i, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f);
}
}
@ -466,14 +466,13 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
continue; // skip to next inverter
if (!iv->config->enabled)
continue; // skip to next inverter
if (iv->commEnabled)
continue; // skip to next inverter
if (checkAvail) {
if (!iv->isAvailable())
if (iv->isAvailable())
continue;
}
changed = true;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t ch = 0; ch <= iv->channels; ch++) {
uint8_t pos = 0;
@ -495,10 +494,9 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
pos = iv->getPosByChFld(ch, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f);
}
iv->resetAlarms();
iv->doCalculations();
}
changed = true;
}
if(changed)
@ -591,9 +589,8 @@ void app::updateLed(void) {
uint8_t led_on = (mConfig->led.high_active) ? (mConfig->led.luminance) : (255-mConfig->led.luminance);
if (mConfig->led.led[0] != DEF_PIN_OFF) {
Inverter<> *iv;
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
Inverter<> *iv = mSys.getInverterByPos(id);
if (NULL != iv) {
if (iv->isProducing()) {
// turn on when at least one inverter is producing

118
src/app.h

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

8
src/appInterface.h

@ -14,7 +14,7 @@
class IApp {
public:
virtual ~IApp() {}
virtual bool saveSettings(bool stopFs) = 0;
virtual bool saveSettings(bool reboot) = 0;
virtual void initInverter(uint8_t id) = 0;
virtual bool readSettings(const char *path) = 0;
virtual bool eraseSettings(bool eraseWifi) = 0;
@ -31,6 +31,7 @@ class IApp {
virtual void setupStation(void) = 0;
virtual void setStopApAllowedMode(bool allowed) = 0;
virtual String getStationIp(void) = 0;
virtual bool getWasInCh12to14(void) const = 0;
#endif /* defined(ETHERNET) */
virtual uint32_t getUptime() = 0;
@ -56,7 +57,10 @@ class IApp {
virtual uint32_t getMqttRxCnt() = 0;
virtual uint32_t getMqttTxCnt() = 0;
virtual bool getProtection(AsyncWebServerRequest *request) = 0;
virtual void lock(bool fromWeb) = 0;
virtual char *unlock(const char *clientIp, bool loginFromWeb) = 0;
virtual void resetLockTimeout(void) = 0;
virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0;
virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0;
virtual uint16_t getHistoryMaxDay() = 0;

2
src/config/config.h

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

57
src/config/settings.h

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

6
src/defines.h

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

2
src/eth/ahoyeth.h

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

12
src/hm/CommQueue.h

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

353
src/hm/Communication.h

@ -6,13 +6,14 @@
#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__
#include <array>
#include "CommQueue.h"
#include <Arduino.h>
#include "../utils/crc.h"
#include "../utils/timemonitor.h"
#include "Heuristic.h"
#define MAX_BUFFER 250
#define MAX_BUFFER 200
typedef std::function<void(uint8_t, Inverter<> *)> payloadListenerType;
typedef std::function<void(Inverter<> *)> powerLimitAckListenerType;
@ -20,12 +21,11 @@ typedef std::function<void(Inverter<> *)> alarmListenerType;
class Communication : public CommQueue<> {
public:
void setup(uint32_t *timestamp, bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint16_t *inverterGap) {
void setup(uint32_t *timestamp, bool *serialDebug, bool *privacyMode, bool *printWholeTrace) {
mTimestamp = timestamp;
mPrivacyMode = privacyMode;
mSerialDebug = serialDebug;
mPrintWholeTrace = printWholeTrace;
mInverterGap = inverterGap;
}
void addImportant(Inverter<> *iv, uint8_t cmd) {
@ -83,14 +83,17 @@ class Communication : public CommQueue<> {
q->iv->mGotFragment = false;
q->iv->mGotLastMsg = false;
q->iv->curFrmCnt = 0;
q->iv->radioStatistics.txCnt++;
mIsRetransmit = false;
if(NULL == q->iv->radio)
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->mIsSingleframeReq = false;
mFramesExpected = getFramesExpected(q); // function to get expected frame count.
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;
break;
@ -112,13 +115,13 @@ class Communication : public CommQueue<> {
} else
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);
if((!mIsRetransmit && (q->cmd == AlarmData)) || (q->cmd == GridOnProFilePara))
incrAttempt((q->cmd == AlarmData)? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA);
mIsRetransmit = false;
setAttempt();
if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara))
incrAttempt(q->cmd == AlarmData? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA);
mState = States::WAIT;
break;
@ -129,37 +132,38 @@ class Communication : public CommQueue<> {
break;
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) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("request timeout: "));
DBGPRINT(String(q->iv->radio->mRadioWaitTime.getRunTime()));
DBGPRINT(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("");
DBGPRINTLN(F("ms"));
}
if(!q->iv->mGotFragment) {
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);
#endif
} else {
mHeu.setIvRetriesBad(q->iv);
if(IV_MI == q->iv->ivGen)
q->iv->mIvTxCnt++;
if(mFirstTry) {
mFirstTry = false;
setAttempt();
if(q->attempts < 3 || !q->iv->isProducing())
mFirstTry = false;
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->radio->mRadioWaitTime.stopTimeMonitor();
mState = States::START;
return;
}
}
@ -180,8 +184,11 @@ class Communication : public CommQueue<> {
q->iv->mDtuRxCnt++;
if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
if(parseFrame(p))
if(parseFrame(p)) {
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
if(parseDevCtrl(p, q))
closeRequest(q, true);
@ -190,8 +197,8 @@ class Communication : public CommQueue<> {
q->iv->radio->mBufCtrl.pop();
return; // don't wait for empty buffer
} else if(IV_MI == q->iv->ivGen) {
if(parseMiFrame(p, q))
q->iv->curFrmCnt++;
parseMiFrame(p, q);
q->iv->curFrmCnt++;
}
} //else -> serial does not match
@ -202,10 +209,9 @@ class Communication : public CommQueue<> {
if(q->iv->ivGen != IV_MI) {
mState = States::CHECK_PACKAGE;
} else {
bool fastNext = true;
if(q->iv->miMultiParts < 6) {
mState = States::WAIT;
if((q->iv->radio->mRadioWaitTime.isTimeout() && mIsRetransmit) || !mIsRetransmit) {
if(q->iv->radio->mRadioWaitTime.isTimeout() && q->attempts) {
miRepeatRequest(q);
return;
}
@ -215,12 +221,12 @@ class Communication : public CommQueue<> {
|| ((q->cmd == MI_REQ_CH2) && (q->iv->type == INV_TYPE_2CH))
|| ((q->cmd == MI_REQ_CH1) && (q->iv->type == INV_TYPE_1CH))) {
miComplete(q->iv);
fastNext = false;
}
if(fastNext)
miNextRequest(q->iv->type == INV_TYPE_4CH ? MI_REQ_4CH : MI_REQ_CH1, q);
else
closeRequest(q, true);
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINTLN(F("Payload (MI got all)"));
}
closeRequest(q, true);
}
}
@ -252,10 +258,33 @@ class Communication : public CommQueue<> {
if(framnr) {
if(0 == q->attempts) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("no attempts left"));
DBGPRINTLN(F("timeout, no attempts left"));
closeRequest(q, false);
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();
if(*mSerialDebug) {
@ -273,12 +302,14 @@ class Communication : public CommQueue<> {
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))
(mCbPayload)(q->cmd, q->iv);
closeRequest(q, true);
} else
closeRequest(q, false);
closeRequest(q, true);
break;
}
}
@ -291,11 +322,6 @@ class Communication : public CommQueue<> {
DBGPRINT(String(p->millis));
DBGPRINT(F("ms | "));
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) {
DBGPRINT(F(" 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];
CP_U32_BigEndian(tmp, iv->radioId.u64 >> 8);
for(uint8_t i = 0; i < 4; i++) {
@ -416,14 +442,15 @@ class Communication : public CommQueue<> {
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)
|| (p->packet[0] == MI_REQ_CH2 + ALL_FRAMES)
|| ((p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES))
&& (p->packet[0] < (0x39 + SINGLE_FRAME))
)) { //&& (p->packet[0] != (0x0f + ALL_FRAMES)))) {
)) {
// small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39
//mPayload[iv->id].txId = p->packet[0];
miDataDecode(p, q);
} else if (p->packet[0] == (0x0f + ALL_FRAMES)) {
miHwDecode(p, q);
@ -436,13 +463,10 @@ class Communication : public CommQueue<> {
record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure
rec->ts = q->ts;
miStsConsolidate(q, ((p->packet[0] == 0x88) ? 1 : 2), rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]);
//mHeu.setGotFragment(q->iv); only do this when we are through the cycle?
}
return true;
}
inline bool parseDevCtrl(packet_t *p, const queue_s *q) {
inline bool parseDevCtrl(const packet_t *p, const queue_s *q) {
switch(p->packet[12]) {
case ActivePowerContr:
if(p->packet[13] != 0x00)
@ -480,7 +504,7 @@ class Communication : public CommQueue<> {
return accepted;
}
inline void compilePayload(const queue_s *q) {
inline bool compilePayload(const queue_s *q) {
uint16_t crc = 0xffff, crcRcv = 0x0000;
for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(i == (mMaxFrameId - 1)) {
@ -496,27 +520,22 @@ class Communication : public CommQueue<> {
DBGPRINT(F("CRC Error "));
if(q->attempts == 0) {
DBGPRINTLN(F("-> Fail"));
closeRequest(q, false);
} else
DBGPRINTLN(F("-> complete retransmit"));
mCompleteRetry = true;
mState = States::RESET;
return;
return false;
}
/*DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("procPyld: cmd: 0x"));
DBGHEXLN(q->cmd);*/
memset(mPayload, 0, MAX_BUFFER);
mPayload.fill(0);
int8_t rssi = -127;
uint8_t len = 0;
DPRINT_IVID(DBG_INFO, q->iv->id);
for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(mLocalBuf[i].len + len > MAX_BUFFER) {
DPRINTLN(DBG_ERROR, F("payload buffer to small!"));
return;
return true;
}
memcpy(&mPayload[len], mLocalBuf[i].buf, mLocalBuf[i].len);
len += mLocalBuf[i].len;
@ -527,30 +546,31 @@ class Communication : public CommQueue<> {
len -= 2;
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("Payload ("));
DBGPRINT(String(len));
if(*mPrintWholeTrace) {
DBGPRINT(F("): "));
ah::dumpBuf(mPayload, len);
} else
DBGPRINTLN(F(")"));
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("Payload ("));
DBGPRINT(String(len));
if(*mPrintWholeTrace) {
DBGPRINT(F("): "));
ah::dumpBuf(mPayload.data(), len);
} else
DBGPRINTLN(F(")"));
}
if(GridOnProFilePara == q->cmd) {
q->iv->addGridProfile(mPayload, len);
return;
q->iv->addGridProfile(mPayload.data(), len);
return true;
}
record_t<> *rec = q->iv->getRecordStruct(q->cmd);
if(NULL == rec) {
if(GetLossRate == q->cmd) {
q->iv->parseGetLossRate(mPayload, len);
return;
} else {
q->iv->parseGetLossRate(mPayload.data(), len);
return true;
} else
DPRINTLN(DBG_ERROR, F("record is NULL!"));
closeRequest(q, false);
}
return;
return false;
}
if((rec->pyldLen != len) && (0 != rec->pyldLen)) {
if(*mSerialDebug) {
@ -558,15 +578,13 @@ class Communication : public CommQueue<> {
DBGPRINT(String(rec->pyldLen));
DBGPRINTLN(F(" bytes"));
}
/*q->iv->radioStatistics.rxFail++;*/
closeRequest(q, false);
return;
return false;
}
rec->ts = q->ts;
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;
@ -576,13 +594,14 @@ class Communication : public CommQueue<> {
if(AlarmData == q->cmd) {
uint8_t i = 0;
while(1) {
if(0 == q->iv->parseAlarmLog(i++, mPayload, len))
if(0 == q->iv->parseAlarmLog(i++, mPayload.data(), len))
break;
if (NULL != mCbAlarm)
(mCbAlarm)(q->iv);
yield();
}
}
return true;
}
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);
if(crcPass)
q->iv->radioStatistics.rxSuccess++;
else if(q->iv->mGotFragment)
else if(q->iv->mGotFragment || mCompleteRetry)
q->iv->radioStatistics.rxFail++; // got no complete payload
else
q->iv->radioStatistics.rxFailNoAnser++; // got nothing
mWaitTime.startTimeMonitor(*mInverterGap);
q->iv->radioStatistics.rxFailNoAnswer++; // got nothing
mWaitTime.startTimeMonitor(1); // maybe remove, side effects unknown
bool keep = false;
if(q->isDevControl)
@ -615,6 +634,7 @@ class Communication : public CommQueue<> {
q->iv->mGotLastMsg = false;
q->iv->miMultiParts = 0;
mIsRetransmit = false;
mCompleteRetry = false;
mState = States::RESET;
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
for (uint8_t i = 0; i < 5; i++) {
q->iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1);
}
q->iv->isConnected = true;
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("HW_VER is "));
DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25]));
}
record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec->ts = q->ts;
q->iv->setValue(1, rec, (uint32_t) ((p->packet[24] << 8) + p->packet[25])/1);
q->iv->miMultiParts +=4;
@ -686,7 +705,7 @@ class Communication : public CommQueue<> {
byte[23] to byte[26] Matching_APPFW_PN*/
DPRINT(DBG_INFO,F("HW_PartNo "));
DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13]));
record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec->ts = q->ts;
q->iv->setValue(0, rec, (uint32_t) ((((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])/1);
rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
@ -801,35 +820,22 @@ class Communication : public CommQueue<> {
miStsConsolidate(q, datachan, rec, p->packet[23], p->packet[24]);
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);
} else {
q->iv->miMultiParts = 7; // indicate we are ready
}
} else if((p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES)) && (q->iv->type == INV_TYPE_2CH)) {
//addImportant(q->iv, MI_REQ_CH2);
miNextRequest(MI_REQ_CH2, q);
mHeu.evalTxChQuality(q->iv, true, (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
}
}
void miNextRequest(uint8_t cmd, const queue_s *q) {
incrAttempt(); // if function is called, we got something, and we necessarily need more transmissions for MI types...
if(*mSerialDebug) {
DPRINT_IVID(DBG_WARN, q->iv->id);
DBGPRINT(F("next request ("));
DBGPRINT(String(q->attempts));
DBGPRINT(F(" attempts left): 0x"));
DBGHEXLN(cmd);
}
if(q->iv->miMultiParts == 7)
q->iv->radioStatistics.rxSuccess++;
mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
mHeu.getTxCh(q->iv);
q->iv->radioStatistics.ivSent++;
mFramesExpected = getFramesExpected(q);
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->miMultiParts = 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;
chgCmd(cmd);
//mState = States::WAIT;
@ -845,18 +858,17 @@ class Communication : public CommQueue<> {
void miRepeatRequest(const queue_s *q) {
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) {
DPRINT_IVID(DBG_WARN, q->iv->id);
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("resend request ("));
DBGPRINT(String(q->attempts));
DBGPRINT(F(" attempts left): 0x"));
DBGHEXLN(q->cmd);
}
q->iv->radio->sendCmdPacket(q->iv, q->cmd, 0x00, true);
q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
mIsRetransmit = false;
//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) {
@ -879,50 +891,62 @@ class Communication : public CommQueue<> {
statusMi = 8310; //trick?
}
uint16_t prntsts = statusMi == 3 ? 1 : statusMi;
uint16_t prntsts = (statusMi == 3) ? 1 : statusMi;
bool stsok = true;
if ( prntsts != rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)] ) { //sth.'s changed?
q->iv->alarmCnt = 1; // minimum...
bool changedStatus = false; //if true, raise alarms and send via mqtt (might affect single channel only)
uint8_t oldState = rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)];
if ( prntsts != oldState ) { // sth.'s changed?
stsok = false;
//sth is or was wrong?
if ( (q->iv->type != INV_TYPE_1CH) && ( (statusMi != 3)
|| ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1)))
) {
q->iv->lastAlarm[stschan+q->iv->type==INV_TYPE_2CH ? 2: 4] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts);
q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0);
q->iv->alarmCnt = q->iv->type == INV_TYPE_2CH ? 3 : 5;
} else if ( (q->iv->type == INV_TYPE_1CH) && ( (statusMi != 3)
|| ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1)))
) {
q->iv->lastAlarm[stschan] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts);
} else if (q->iv->type == INV_TYPE_1CH)
stsok = true;
q->iv->alarmLastId = prntsts; //iv->alarmMesIndex;
if (q->iv->alarmCnt > 1) { //more than one channel
for (uint8_t ch = 0; ch < (q->iv->alarmCnt); ++ch) { //start with 1
if (q->iv->lastAlarm[ch].code == 1) {
stsok = true;
break;
if(!oldState) { // initial zero value? => just write this channel to main state and raise changed flags
changedStatus = true;
q->iv->alarmCnt = 1; // minimum...
} else {
//sth is or was wrong?
if (q->iv->type == INV_TYPE_1CH) {
changedStatus = true;
if(q->iv->alarmCnt == 2) // we had sth. other than "producing" in the past
q->iv->lastAlarm[1].end = q->ts;
else { // copy old state and mark as ended
q->iv->lastAlarm[1] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts);
q->iv->alarmCnt = 2;
}
} 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)
if (q->iv->alarmCnt == 1)
q->iv->alarmCnt = (q->iv->type == INV_TYPE_2CH) ? 5 : 9;
if(q->iv->lastAlarm[stschan].code != prntsts) { // changed?
changedStatus = true;
if(q->iv->lastAlarm[stschan].code) // copy old data and mark as ended (if any)
q->iv->lastAlarm[(stschan + (q->iv->type==INV_TYPE_2CH ? 2 : 4))] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts);
q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0);
}
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) {
q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts);
q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0);
}
if (changedStatus || !stsok) {
rec->ts = q->ts;
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)]) {
@ -933,27 +957,30 @@ class Communication : public CommQueue<> {
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) {
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
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.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.dtuSent = iv->mDtuTxCnt;
if (mSerialDebug) {
if (*mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN("DTU loss: " +
String (iv->radioStatistics.ivLoss) + "/" +
String (iv->radioStatistics.ivSent) + " frames for " +
String (iv->radioStatistics.dtuSent) + " requests");
DBGPRINT(F("DTU loss: ") +
String (iv->radioStatistics.ivLoss) + F("/") +
String (iv->radioStatistics.ivSent) + F(" frames for ") +
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->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);
} else {
for(uint8_t i = 1; i <= iv->channels; i++) {
if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1)) {
uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec);
ac_pow += iv->getValue(pos, rec);
}
if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1))
ac_pow += iv->getValue(iv->getPosByChFld(i, FLD_PDC, rec), rec);
}
}
ac_pow = (int) (ac_pow*9.5);
@ -1002,17 +1027,17 @@ class Communication : public CommQueue<> {
private:
States mState = States::RESET;
uint32_t *mTimestamp;
bool *mPrivacyMode, *mSerialDebug, *mPrintWholeTrace;
uint16_t *mInverterGap;
uint32_t *mTimestamp = nullptr;
bool *mPrivacyMode = nullptr, *mSerialDebug = nullptr, *mPrintWholeTrace = nullptr;
TimeMonitor mWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state)
std::array<frame_t, MAX_PAYLOAD_ENTRIES> mLocalBuf;
bool mFirstTry = false; // see, if we should do a second try
bool mIsRetransmit = false; // we already had waited one complete cycle
uint8_t mMaxFrameId;
bool mFirstTry = false; // see, if we should do a second try
bool mCompleteRetry = false; // remember if we did request a complete retransmission
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
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;
powerLimitAckListenerType mCbPwrAck = 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
//-----------------------------------------------------------------------------
@ -23,8 +23,8 @@
class Heuristic {
public:
uint8_t getTxCh(Inverter<> *iv) {
if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
return 0; // not used for these inverter types
if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
return 0; // not used for other than nRF inverter types
HeuristicInv *ih = &iv->heuristics;
@ -38,6 +38,8 @@ class Heuristic {
ih->txRfChId = curId;
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)
ih->testPeriodSendCnt++;
@ -66,10 +68,12 @@ class Heuristic {
ih->testPeriodFailCnt = 0;
}
iv->radio->mTxRetriesNext = getIvRetries(iv);
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;
#if (DBG_DEBUG == DEBUG_LEVEL)
@ -82,8 +86,10 @@ class Heuristic {
DBGPRINT(", ");
DBGPRINTLN(String(ih->lastRxFragments));
#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)
updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD);
else if(!retransmits || isNewTxCh(ih)) { // nothing received: send probably lost
@ -130,7 +136,7 @@ class Heuristic {
ih->lastRxFragments = rxFragments;
}
void printStatus(Inverter<> *iv) {
void printStatus(const Inverter<> *iv) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("Radio infos:"));
if((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) {
@ -147,7 +153,7 @@ class Heuristic {
DBGPRINT(F(", f: "));
DBGPRINT(String(iv->radioStatistics.rxFail));
DBGPRINT(F(", n: "));
DBGPRINT(String(iv->radioStatistics.rxFailNoAnser));
DBGPRINT(String(iv->radioStatistics.rxFailNoAnswer));
DBGPRINT(F(" | p: ")); // better debugging for helpers...
if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
DBGPRINTLN(String(iv->config->powerLevel-10));
@ -155,8 +161,50 @@ class Heuristic {
DBGPRINTLN(String(iv->config->powerLevel));
}
uint8_t getIvRetries(const Inverter<> *iv) const {
if(iv->heuristics.rxSpeeds[0])
return RETRIES_VERYFAST_IV;
if(iv->heuristics.rxSpeeds[1])
return RETRIES_FAST_IV;
return 15;
}
void setIvRetriesGood(Inverter<> *iv, bool veryGood) {
if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
return; // not used for other than nRF inverter types
if(iv->heuristics.rxSpeedCnt[veryGood] > 9)
return;
iv->heuristics.rxSpeedCnt[veryGood]++;
iv->heuristics.rxSpeeds[veryGood] = true;
}
void setIvRetriesBad(Inverter<> *iv) {
if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
return; // not used for other than nRF inverter types
if(iv->heuristics.rxSpeedCnt[0]) {
iv->heuristics.rxSpeedCnt[0]--;
return;
}
if(iv->heuristics.rxSpeeds[0]) {
iv->heuristics.rxSpeeds[0] = false;
return;
}
if(iv->heuristics.rxSpeedCnt[1]) {
iv->heuristics.rxSpeedCnt[1]--;
return;
}
if(iv->heuristics.rxSpeeds[1]) {
iv->heuristics.rxSpeeds[1] = false;
return;
}
return;
}
private:
bool isNewTxCh(HeuristicInv *ih) {
bool isNewTxCh(const HeuristicInv *ih) const {
return ih->txRfChId != ih->lastBestTxChId;
}
@ -169,18 +217,12 @@ class Heuristic {
}
inline uint8_t id2Ch(uint8_t id) {
switch(id) {
case 0: return 3;
case 1: return 23;
case 2: return 40;
case 3: return 61;
case 4: return 75;
}
return 3; // standard
if (id < RF_MAX_CHANNEL_ID)
return mChList[id];
else
return 3; // standard
}
private:
uint8_t mChList[5] = {03, 23, 40, 61, 75};
uint8_t mChList[RF_MAX_CHANNEL_ID] = {03, 23, 40, 61, 75};
};

26
src/hm/HeuristicInv.h

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

20
src/hm/hmDefines.h

@ -76,23 +76,23 @@ enum {CMD_CALC = 0xffff};
enum {CH0 = 0, CH1, CH2, CH3, CH4, CH5, CH6};
enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH};
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_RESERVE {90,120} // timeout parameter to still wait after last expected frame (ms)
#define DURATION_TXFRAME 85 // timeout parameter for first transmission and first expected frame (time to first channel switch from tx start!) (ms)
#define DURATION_LISTEN_MIN 5 // time to stay at least on a listening channel (ms)
#define DURATION_PAUSE_LASTFR 45 // how long to pause after last frame (ms)
const uint8_t duration_reserve[2] = {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 {
uint8_t fieldId; // field id

350
src/hm/hmInverter.h

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

114
src/hm/hmRadio.h

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

29
src/hm/hmSystem.h

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

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 data[NRF_MAX_TRANSFER_SZ];
uint8_t data[NRF_MAX_TRANSFER_SZ + 1];
data[0] = cmd;
if(len > NRF_MAX_TRANSFER_SZ)
len = NRF_MAX_TRANSFER_SZ;

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
//-----------------------------------------------------------------------------
@ -11,6 +11,8 @@
#define ALL_FRAMES 0x80
#define SINGLE_FRAME 0x81
#include <array>
#include <atomic>
#include "../utils/dbg.h"
#include "../utils/crc.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 bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) { return true; }
virtual bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) { return true; }
virtual bool isChipConnected(void) { return false; }
virtual bool isChipConnected(void) const { return false; }
virtual uint16_t getBaseFreqMhz() { return 0; }
virtual uint16_t getBootFreqMhz() { return 0; }
virtual std::pair<uint16_t,uint16_t> getFreqRangeMhz(void) { return std::make_pair(0, 0); }
virtual bool loop(void) = 0;
virtual uint8_t getARC(void) { return 0xff; }
virtual uint8_t getPLOS(void) { return 0xff; }
Radio() : mTxBuf{} {}
void handleIntr(void) {
mIrqRcvd = true;
@ -66,7 +71,7 @@ class Radio {
sendPacket(iv, 24, isRetransmit);
}
uint32_t getDTUSn(void) {
uint32_t getDTUSn(void) const {
return mDtuSn;
}
@ -78,11 +83,13 @@ class Radio {
std::queue<packet_t> mBufCtrl;
uint8_t mIrqOk = IRQ_UNKNOWN;
TimeMonitor mRadioWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state)
uint8_t mTxRetriesNext = 15; // let heuristics tell us the next reties count (for nRF type radios only)
uint8_t mFramesExpected = 0x0c;
protected:
virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0;
virtual uint64_t getIvId(Inverter<> *iv) = 0;
virtual uint8_t getIvGen(Inverter<> *iv) = 0;
virtual uint64_t getIvId(Inverter<> *iv) const = 0;
virtual uint8_t getIvGen(Inverter<> *iv) const = 0;
void initPacket(uint64_t ivId, uint8_t mid, uint8_t pid) {
mTxBuf[0] = mid;
@ -103,7 +110,7 @@ class Radio {
mTxBuf[(*len)++] = (crc ) & 0xff;
}
// crc over all
mTxBuf[*len] = ah::crc8(mTxBuf, *len);
mTxBuf[*len] = ah::crc8(mTxBuf.data(), *len);
(*len)++;
}
@ -115,21 +122,21 @@ class Radio {
chipID = ESP.getChipId();
#endif
uint8_t t;
mDtuSn = 0;
for(int i = 0; i < (7 << 2); i += 4) {
t = (chipID >> i) & 0x0f;
uint8_t t = (chipID >> i) & 0x0f;
if(t > 0x09)
t -= 6;
mDtuSn |= (t << i);
}
mDtuSn |= 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
}
}
uint32_t mDtuSn;
volatile bool mIrqRcvd;
bool *mSerialDebug, *mPrivacyMode, *mPrintWholeTrace;
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
uint8_t mFramesExpected = 0x0c;
protected:
uint32_t mDtuSn = 0;
std::atomic<bool> mIrqRcvd = false;
bool *mSerialDebug = nullptr, *mPrivacyMode = nullptr, *mPrintWholeTrace = nullptr;
std::array<uint8_t, MAX_RF_PAYLOAD_SIZE> mTxBuf;
};
#endif /*__RADIO_H__*/

6
src/hm/simulator.h

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

2
src/hms/cmtHal.h

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

7
src/hms/esp32_3wSpi.h

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

81
src/hms/hmsRadio.h

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

6
src/platformio.ini

@ -28,7 +28,7 @@ lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
https://github.com/nRF24/RF24 @ 1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.5.0
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9
@ -394,7 +394,7 @@ lib_deps =
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.5.0
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9
@ -439,7 +439,7 @@ lib_deps =
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.5.0
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9

31
src/plugins/Display/Display.h

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

12
src/plugins/Display/Display_Mono.h

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

6
src/plugins/Display/Display_Mono_128X32.h

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

39
src/plugins/Display/Display_Mono_128X64.h

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

6
src/plugins/Display/Display_Mono_64X48.h

@ -12,11 +12,11 @@ class DisplayMono64X48 : public DisplayMono {
mExtra = 0;
}
void config(display_t *cfg) {
void config(display_t *cfg) override {
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);
// 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);
@ -28,7 +28,7 @@ class DisplayMono64X48 : public DisplayMono {
mDisplay->sendBuffer();
}
void disp(void) {
void disp(void) override {
mDisplay->clearBuffer();
// calculate current pixelshift for pixelshift screensaver

27
src/plugins/Display/Display_Mono_84X48.h

@ -12,11 +12,11 @@ class DisplayMono84X48 : public DisplayMono {
mExtra = 0;
}
void config(display_t *cfg) {
void config(display_t *cfg) override {
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);
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();
}
void disp(void) {
void disp(void) override {
mDisplay->clearBuffer();
// Layout-Test
@ -143,12 +143,11 @@ class DisplayMono84X48 : public DisplayMono {
// draw dynamic RSSI bars
int rssi_bar_height = 7;
for (int i = 0; i < 4; i++) {
int radio_rssi_threshold = -60 - i * 10;
int wifi_rssi_threshold = -60 - i * 10;
int rssi_threshold = -60 - i * 10;
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);
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);
}
@ -184,30 +183,28 @@ class DisplayMono84X48 : public DisplayMono {
l_MAX_LINES = 5,
};
uint8_t graph_first_line;
uint8_t graph_last_line;
uint8_t graph_first_line = 0;
uint8_t graph_last_line = 0;
void calcLinePositions() {
uint8_t yOff = 0;
uint8_t i = 0;
uint8_t asc, dsc;
do {
setLineFont(i);
asc = mDisplay->getAscent();
uint8_t asc = mDisplay->getAscent();
yOff += asc;
mLineYOffsets[i] = yOff;
dsc = mDisplay->getDescent();
uint8_t dsc = mDisplay->getDescent();
if (l_TotalPower != i) // power line needs no descent spacing
yOff -= dsc;
yOff++; // instead lets spend one pixel space between all lines
i++;
} while(l_MAX_LINES>i);
} while(l_MAX_LINES > i);
}
inline void setLineFont(uint8_t line) {
if ((line == l_TotalPower) ||
(line == l_Ahoy))
if (line == l_TotalPower) // || (line == l_Ahoy) -> l_TotalPower == l_Ahoy == 2
mDisplay->setFont(u8g2_font_logisoso16_tr);
else
mDisplay->setFont(u8g2_font_5x8_symbols_ahoy);

17
src/plugins/Display/Display_ePaper.cpp

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

49
src/plugins/history.h

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

193
src/publisher/pubMqtt.h

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

137
src/publisher/pubMqttIvData.h

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

12
src/publisher/pubSerial.h

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

6
src/utils/helper.cpp

@ -72,11 +72,10 @@ namespace ah {
String getTimeStrMs(uint64_t t) {
char str[13];
uint16_t m;
if(0 == t)
sprintf(str, "n/a");
else {
m = t % 1000;
uint16_t m = t % 1000;
t = t / 1000;
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) {
char tmp[3];
uint64_t ret = 0ULL;
uint64_t u64;
memset(tmp, 0, 3);
for(uint8_t i = 0; i < 6; i++) {
tmp[0] = val[i*2];
tmp[1] = val[i*2 + 1];
if((tmp[0] == '\0') || (tmp[1] == '\0'))
break;
u64 = strtol(tmp, NULL, 16);
uint64_t u64 = strtol(tmp, NULL, 16);
ret |= (u64 << ((5-i) << 3));
}
return ret;

18
src/utils/improv.h

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

20
src/utils/scheduler.h

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

4
src/utils/spiPatcher.cpp

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

6
src/utils/spiPatcher.h

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

4
src/utils/syslog.cpp

@ -10,6 +10,7 @@
#define SYSLOG_MAX_PACKET_SIZE 256
DbgSyslog::DbgSyslog() : mSyslogBuffer{} {}
//-----------------------------------------------------------------------------
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
int packetStart = 0;
int packetSize = 122; // syslog payload depends also on hostname and app
char saveChar;
if (isEolFound) {
mSyslogBuffer[mSyslogBufFill-2]=0; // skip \r\n
}
while(packetStart < mSyslogBufFill) {
saveChar = mSyslogBuffer[packetStart+packetSize];
char saveChar = mSyslogBuffer[packetStart+packetSize];
mSyslogBuffer[packetStart+packetSize] = 0;
log(mConfig->sys.deviceName,SYSLOG_FACILITY, mSyslogSeverity, &mSyslogBuffer[packetStart]);
mSyslogBuffer[packetStart+packetSize] = saveChar;

5
src/utils/syslog.h

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

6
src/utils/timemonitor.h

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

6
src/web/html/colorBright.css

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

16
src/web/html/colorDark.css

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

24
src/web/html/history.html

@ -20,14 +20,6 @@
{#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS}
</p>
</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>
{#HTML_FOOTER}
@ -87,6 +79,8 @@
function parsePowerHistory(obj){
if (null != obj) {
parseNav(obj.generic);
parseESP(obj.generic);
parseHistory(obj,"pwr", pwrExeOnce)
document.getElementById("pwrLast").innerHTML = mLastValue
document.getElementById("pwrMaxDay").innerHTML = obj.maxDay
@ -94,20 +88,6 @@
if (pwrExeOnce) {
pwrExeOnce = false;
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="nav4" class="hide" href="/serial?v={#VERSION}">{#NAV_WEBSERIAL}</a>
<a id="nav5" class="hide" href="/setup?v={#VERSION}">{#NAV_SETTINGS}</a>
<span class="seperator"></span>
<span class="separator"></span>
<a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a>
<a id="nav7" class="hide" href="/system?v={#VERSION}">System</a>
<span class="seperator"></span>
<span class="separator"></span>
<a id="nav8" href="/api" target="_blank">REST API</a>
<a id="nav9" href="https://ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</a>
<a id="nav10" href="/about?v={#VERSION}">{#NAV_ABOUT}</a>
<a id="nav12" href="#" class="hide" target="_blank">Custom Link</a>
<span class="seperator"></span>
<span class="separator"></span>
<a id="nav0" class="hide" href="/login">Login</a>
<a id="nav1" class="hide" href="/logout">Logout</a>
</div>

27
src/web/html/index.html

@ -41,27 +41,24 @@
var release = null;
function apiCb(obj) {
var e = document.getElementById("apiResult");
var e = document.getElementById("apiResult")
if(obj.success) {
e.innerHTML = " {#COMMAND_EXE}";
getAjax("/api/index", parse);
}
else
e.innerHTML = " {#ERROR}: " + obj.error;
e.innerHTML = " {#COMMAND_EXE}"
getAjax("/api/index", parse)
} else
e.innerHTML = " {#ERROR}: " + obj.error
}
function setTime() {
var date = new Date();
var obj = new Object();
obj.cmd = "set_time";
obj.val = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj));
var date = new Date()
var obj = {cmd: "set_time", token: "*", val: parseInt(date.getTime() / 1000)}
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj))
}
function parseGeneric(obj) {
if(exeOnce)
parseESP(obj);
parseRssi(obj);
parseESP(obj)
parseRssi(obj)
}
function parseSys(obj) {
@ -73,9 +70,9 @@
var min = parseInt(up / 60) % 60;
var sec = up % 60;
var e = document.getElementById("uptime");
e.innerHTML = days + " Day";
e.innerHTML = days + " {#DAY}";
if(1 != days)
e.innerHTML += "s";
e.innerHTML += "{#S}";
e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":"
+ ("0"+min).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>
</div>
<div class="row my-3">
<div class="col-3">console active: <span class="dot" id="active"></span></div>
<div class="col-3 col-sm-4 my-3">Uptime: <span id="uptime"></span></div>
<div class="col-3">{#CONSOLE_ACTIVE}: <span class="dot" id="active"></span></div>
<div class="col-3 col-sm-4 my-3">{#UPTIME}: <span id="uptime"></span></div>
<div class="col-6 col-sm-4 a-r">
<input type="button" value="clear" class="btn" id="clear"/>
<input type="button" value="autoscroll" class="btn" id="scroll"/>
<input type="button" value="copy" class="btn" id="copy"/>
<input type="button" value="{#BTN_CLEAR}" class="btn" id="clear"/>
<input type="button" value="{#BTN_AUTOSCROLL}" class="btn" id="scroll"/>
<input type="button" value="{#BTN_COPY}" class="btn" id="copy"/>
</div>
</div>
</div>
@ -35,7 +35,7 @@
var hrs = parseInt(up / 3600) % 24;
var min = parseInt(up / 60) % 60;
var sec = up % 60;
document.getElementById("uptime").innerHTML = days + " Days, "
document.getElementById("uptime").innerHTML = days + " {#DAYS}, "
+ ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2);
@ -65,7 +65,7 @@
});
document.getElementById("scroll").addEventListener("click", function() {
mAutoScroll = !mAutoScroll;
this.value = (mAutoScroll) ? "autoscroll" : "manual scroll";
this.value = (mAutoScroll) ? "{#BTN_AUTOSCROLL}" : "{#BTN_MANUALSCROLL}";
});
document.getElementById("copy").addEventListener("click", function() {
con.value = version + " - " + build + "\n---------------\n" + con.value;
@ -80,10 +80,10 @@
try {
return document.execCommand("copy"); // Security exception may be thrown by some browsers.
} catch (ex) {
alert("Copy to clipboard failed" + ex);
alert("{#CLIPBOARD_FAILED} " + ex);
} finally {
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-12">{#DARK_MODE_NOTE}</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="col-8 col-sm-3">{#CUSTOM_LINK}</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>
</fieldset>
<fieldset class="mb-4">
<fieldset class="mb-4" id="serialCb">
<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>
</div>
@ -141,10 +133,6 @@
<div class="col-8 my-2">{#INTERVAL} [s]</div>
<div class="col-4"><input type="number" name="invInterval" title="Invalid input"/></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="col-8 mb-2">{#INV_RESET_MIDNIGHT}</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-4"><input type="checkbox" name="rdGrid"/></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>
</div>
@ -499,9 +483,6 @@
for(var i = 0; i < 31; i++) {
esp32cmtPa.push([i, String(i-10) + " dBm"]);
}
for(var i = 12; i < 41; i++) {
esp32cmtFreq.push([i, freqFmt.format(860 + i*0.25) + " MHz"]);
}
/*ENDIF_ESP32*/
var led_high_active = [
[0, "{#PIN_LOW_ACTIVE}"],
@ -558,31 +539,26 @@
}
function setTime() {
var date = new Date();
var obj = new Object();
obj.cmd = "set_time";
obj.val = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj));
setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000);
var date = new Date()
var obj = {cmd: "set_time", token: "*", val: parseInt(date.getTime() / 1000)}
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj))
setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000)
}
function scan() {
var obj = new Object();
obj.cmd = "scan_wifi";
var obj = {cmd: "scan_wifi", token: "*"}
getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj));
setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 5000);
}
function syncTime() {
var obj = new Object();
obj.cmd = "sync_ntp";
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj));
setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000);
var obj = {cmd: "sync_ntp", token: "*"}
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj))
setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000)
}
function sendDiscoveryConfig() {
var obj = new Object();
obj.cmd = "discovery_cfg";
var obj = {cmd: "discovery_cfg", token: "*"}
getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj));
}
@ -625,7 +601,7 @@
}
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]];
for(var i of ["Mid", "ComStop", "NotAvail", "MaxMid"])
document.getElementsByName("invRst"+i)[0].checked = obj["rst" + i];
@ -650,6 +626,14 @@
el.push(mlCb("protMask" + i, a[i], chk))
}
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) {
@ -702,7 +686,6 @@
add.ch_yield_cor = [];
add.freq = 12;
add.pa = 30;
add.add2total = true;
var e = document.getElementById("inverter");
e.innerHTML = ""; // remove all childs
@ -713,6 +696,13 @@
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) {
var lines = [];
lines.push(ml("tr", {}, [
@ -733,62 +723,36 @@
var cbEn = ml("input", {name: "enable", type: "checkbox"}, null);
var cbDisNightCom = ml("input", {name: "disnightcom", type: "checkbox"}, null);
var cbAddTotal = ml("input", {name: "add2total", type: "checkbox"}, null);
cbEn.checked = (obj.enabled);
cbDisNightCom.checked = (obj.disnightcom);
cbAddTotal.checked = (obj.add2total);
var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null);
var html = ml("div", {}, [
tabs(["{#TAB_GENERAL}", "{#TAB_INPUTS}", "{#TAB_RADIO}", "{#TAB_ADVANCED}"]),
ml("div", {id: "div{#TAB_GENERAL}", class: "tab-content"}, [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-2"}, "{#INV_ENABLE}"),
ml("div", {class: "col-10"}, cbEn)
]),
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))
])
divRow("{#INV_ENABLE}", cbEn),
divRow("{#INV_SERIAL}", ser),
divRow("Name", ml("input", {name: "name", class: "text", type: "text", value: obj.name}, null))
]),
ml("div", {id: "div{#TAB_INPUTS}", class: "tab-content hide"}, [
ml("div", {class: "row mb-3"},
ml("table", {class: "table"},
ml("tbody", {}, lines)
)
ml("table", {class: "table"}, ml("tbody", {}, lines))
)
]),
ml("div", {id: "div{#TAB_RADIO}", class: "tab-content hide"}, [
ml("input", {type: "hidden", name: "isnrf"}, null),
ml("div", {id: "setcmt"}, [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-3 mt-2"}, "{#INV_FREQUENCY}"),
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))
]),
divRow("{#INV_FREQUENCY}", sel("freq", esp32cmtFreq, obj.freq)),
divRow("{#INV_POWER_LEVEL}", sel("cmtpa", esp32cmtPa, obj.pa))
]),
ml("div", {id: "setnrf"},
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-3 mt-2"}, "{#INV_POWER_LEVEL}"),
ml("div", {class: "col-9"}, sel("nrfpa", nrfPa, obj.pa))
]),
divRow("{#INV_POWER_LEVEL}", sel("nrfpa", nrfPa, obj.pa))
),
]),
ml("div", {id: "div{#TAB_ADVANCED}", class: "tab-content hide"}, [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-10"}, "{#INV_PAUSE_DURING_NIGHT}"),
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"}, [
@ -818,7 +782,8 @@
case 0x1000: nrf = true; break;
case 0x1100:
switch(sn & 0x000f) {
case 0x0004: nrf = false; break;
case 0x0004:
case 0x0005: nrf = false; break;
default: nrf = true; break;
}
break;
@ -835,8 +800,9 @@
function ivSave() {
var o = new Object();
o.cmd = "save_iv";
o.id = obj.id;
o.cmd = "save_iv"
o.token = "*"
o.id = obj.id
o.ser = parseInt(document.getElementsByName("ser")[0].value, 16);
o.name = document.getElementsByName("name")[0].value;
o.en = document.getElementsByName("enable")[0].checked;
@ -854,7 +820,6 @@
o.pa = document.getElementsByName("cmtpa")[0].value;
o.freq = document.getElementsByName("freq")[0].value;
o.disnightcom = document.getElementsByName("disnightcom")[0].checked;
o.add2total = document.getElementsByName("add2total")[0].checked;
getAjax("/api/setup", cb, "POST", JSON.stringify(o));
}
@ -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*/
function parseSerial(obj) {
for(var i of [["serEn", "show_live_data"], ["serDbg", "debug"], ["priv", "priv"], ["wholeTrace", "wholeTrace"]])
document.getElementsByName(i[0])[0].checked = obj[i[1]];
var e = document.getElementById("serialCb")
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) {

23
src/web/html/style.css

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

31
src/web/html/visualization.html

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

16
src/web/lang.h

@ -19,21 +19,9 @@
#endif
#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*/
#define INV_INDEX_INVALID "inverter index invalid: "
#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"
#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

123
src/web/lang.json

@ -148,6 +148,16 @@
"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)"
},
{
"token": "REGION",
"en": "Region",
"de": "Region"
},
{
"token": "TIMEZONE",
"en": "Timezone",
"de": "Zeitzone"
},
{
"token": "CUSTOM_LINK",
"en": "Custom link (leave empty to hide element in navigation)",
@ -193,6 +203,11 @@
"en": "Print whole traces in Log",
"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",
"en": "Network",
@ -278,11 +293,6 @@
"en": "Interval",
"de": "Intervall"
},
{
"token": "INV_GAP",
"en": "Communication Gap",
"de": "Kommunikationsl&uuml;cke"
},
{
"token": "INV_RESET_MIDNIGHT",
"en": "Reset values and YieldDay at midnight",
@ -313,11 +323,6 @@
"en": "Read Grid Profile",
"de": "Grid-Profil auslesen"
},
{
"token": "INV_YIELD_EFF",
"en": "Yield Efficiency (default 1.0)",
"de": "Ertragseffizienz (Standard 1.0)"
},
{
"token": "NTP_INTERVAL",
"en": "NTP Interval (in minutes, min. 5 minutes)",
@ -653,11 +658,6 @@
"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"
},
{
"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",
"en": "save",
@ -670,7 +670,7 @@
},
{
"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?"
},
{
@ -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",
"list": [
@ -938,6 +988,16 @@
"en": "Error",
"de": "Fehler"
},
{
"token": "DAY",
"en": "day",
"de": "Tag"
},
{
"token": "S",
"en": "s",
"de": "e"
},
{
"token": "NTP_UNREACH",
"en": "NTP timeserver unreachable",
@ -951,7 +1011,7 @@
{
"token": "NIGHT_TIME",
"en": "Night time, inverter polling disabled",
"de": "Wechselrichterabfrage deaktivert (Nacht)"
"de": "Wechselrichterabfrage deaktiviert (Nacht)"
},
{
"token": "PAUSED_AT",
@ -1070,7 +1130,7 @@
},
{
"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!"
},
{
@ -1386,7 +1446,7 @@
{
"token": "CMD_RECEIVED_WAIT_ACK",
"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",
@ -1402,6 +1462,31 @@
"token": "INV_ACK",
"en": "inverter acknowledged active power control command",
"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 {
public:
Web(void) : mWeb(80), mEvts("/events") {
mProtected = true;
mLogoutTimeout = 0;
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
mSerialAddTime = true;
mSerialClientConnnected = false;
}
void setup(IApp *app, HMSYSTEM *sys, settings_t *config) {
@ -106,16 +100,6 @@ class Web {
}
void tickSecond() {
if (0 != mLogoutTimeout) {
mLogoutTimeout -= 1;
if (0 == mLogoutTimeout) {
if (strlen(mConfig->sys.adminPwd) > 0)
mProtected = true;
}
DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout));
}
if (mSerialClientConnnected) {
if (mSerialBufFill > 0) {
mEvts.send(mSerialBuf, "serial", millis());
@ -129,27 +113,6 @@ class Web {
return &mWeb;
}
void setProtection(bool protect) {
mProtected = protect;
}
bool isProtected(AsyncWebServerRequest *request) {
bool prot;
prot = mProtected;
if(!prot) {
if(strlen(mConfig->sys.adminPwd) > 0) {
uint8_t ip[4];
ah::ip2Arr(ip, request->client()->remoteIP().toString().c_str());
for(uint8_t i = 0; i < 4; i++) {
if(mLoginIp[i] != ip[i])
prot = true;
}
}
}
return prot;
}
void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!index) {
Serial.printf("Update Start: %s\n", filename.c_str());
@ -260,7 +223,7 @@ class Web {
}
void checkProtection(AsyncWebServerRequest *request) {
if(isProtected(request)) {
if(mApp->isProtected(request->client()->remoteIP().toString().c_str(), "", true)) {
checkRedirect(request);
return;
}
@ -347,8 +310,7 @@ class Web {
if (request->args() > 0) {
if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) {
mProtected = false;
ah::ip2Arr(mLoginIp, request->client()->remoteIP().toString().c_str());
mApp->unlock(request->client()->remoteIP().toString().c_str(), true);
request->redirect("/");
}
}
@ -362,8 +324,7 @@ class Web {
DPRINTLN(DBG_VERBOSE, F("onLogout"));
checkProtection(request);
mProtected = true;
mApp->lock(true);
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
@ -386,7 +347,6 @@ class Web {
void onCss(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onCss"));
mLogoutTimeout = LOGOUT_TIMEOUT;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len);
response->addHeader(F("Content-Encoding"), "gzip");
if(request->hasParam("v")) {
@ -477,7 +437,8 @@ class Web {
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
mConfig->sys.darkMode = (request->arg("darkMode") == "on");
mConfig->sys.schedReboot = (request->arg("schedReboot") == "on");
mConfig->sys.region = (request->arg("region")).toInt();
mConfig->sys.timezone = (request->arg("timezone")).toInt() - 12;
if (request->arg("cstLnk") != "") {
request->arg("cstLnk").toCharArray(mConfig->plugin.customLink, MAX_CUSTOM_LINK_LEN);
@ -490,7 +451,7 @@ class Web {
// protection
if (request->arg("adminpwd") != "{PWD}") {
request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN);
mProtected = (strlen(mConfig->sys.adminPwd) > 0);
mApp->lock(false);
}
mConfig->sys.protectionMask = 0x0000;
for (uint8_t i = 0; i < 7; i++) {
@ -518,14 +479,11 @@ class Web {
mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on");
mConfig->inst.readGrid = (request->arg("rdGrid") == "on");
mConfig->inst.rstMaxValsMidNight = (request->arg("invRstMaxMid") == "on");
mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat();
mConfig->inst.gapMs = (request->arg("invGap")).toInt();
// pinout
uint8_t pin;
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) {
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;
@ -589,6 +547,7 @@ class Web {
mConfig->serial.privacyLog = (request->arg("priv") == "on");
mConfig->serial.printWholeTrace = (request->arg("wholeTrace") == "on");
mConfig->serial.showIv = (request->arg("serEn") == "on");
mConfig->serial.log2mqtt = (request->arg("log2mqtt") == "on");
// display
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on");
@ -666,8 +625,8 @@ class Web {
// NOTE: Grouping for fields with channels and totals is currently not working
// TODO: Handle grouping and sorting for independant from channel number
// NOTE: Check packetsize for MAX_NUM_INVERTERS. Successfully Tested with 4 Inverters (each with 4 channels)
const char * metricConstPrefix = "ahoy_solar_";
const char * metricConstInverterFormat = " {inverter=\"%s\"} %d\n";
const char* metricConstPrefix = "ahoy_solar_";
const char* metricConstInverterFormat = " {inverter=\"%s\"} %d\n";
typedef enum {
metricsStateInverterInfo=0, metricsStateInverterEnabled=1, metricsStateInverterAvailable=2,
metricsStateInverterProducing=3, metricsStateInverterPowerLimitRead=4, metricsStateInverterPowerLimitAck=5,
@ -681,7 +640,7 @@ class Web {
metricsStateStart,
metricsStateEnd
} MetricStep_t;
MetricStep_t metricsStep;
MetricStep_t metricsStep = metricsStateInverterInfo;
typedef struct {
const char *topic;
const char *type;
@ -693,12 +652,12 @@ class Web {
{ "is_enabled", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->config->enabled;} },
{ "is_available", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isAvailable();} },
{ "is_producing", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->isProducing();} },
{ "power_limit_read", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return (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;} },
{ "max_power", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} },
{ "radio_rx_success", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} },
{ "radio_rx_fail", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} },
{ "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} },
{ "radio_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_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} },
{ "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_sent_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.dtuSent;} }
};
int metricsInverterId;
uint8_t metricsFieldId;
bool metricDeclared, metricTotalDeclard;
void showMetrics(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
@ -724,7 +680,6 @@ class Web {
char type[60], topic[100], val[25];
size_t len = 0;
int alarmChannelId;
int metricsChannelId;
// Perform grouping on metrics according to format specification
// Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops.
@ -748,7 +703,7 @@ class Web {
snprintf(topic,sizeof(topic),"%swifi_rssi_db{devicename=\"%s\"} %d\n",metricConstPrefix, mConfig->sys.deviceName, WiFi.RSSI());
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
metricsStep = metricsStateInverterInfo;
break;
@ -776,7 +731,7 @@ class Web {
(String("ahoy_solar_inverter_") + inverterMetrics[metricsStep].topic +
inverterMetrics[metricsStep].format).c_str(),
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
metricsStep = static_cast<MetricStep_t>( static_cast<int>(metricsStep) + 1);
// Prepare Realtime Field loop, which may be startet next
@ -796,7 +751,7 @@ class Web {
metrics = "# Info: all realtime fields processed\n";
metricsStep = metricsStateAlarmData;
}
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
len = snprintf(reinterpret_cast<char *>(buffer), maxLen, "%s", metrics.c_str());
break;
case metricStateRealtimeInverterId: // Iterate over all inverters for this field
@ -806,7 +761,7 @@ class Web {
iv = mSys->getInverterByPos(metricsInverterId);
if (NULL != iv) {
rec = iv->getRecordStruct(RealTimeRunData_Debug);
for (metricsChannelId=0; metricsChannelId < rec->length;metricsChannelId++) {
for (int metricsChannelId=0; metricsChannelId < rec->length;metricsChannelId++) {
uint8_t channel = rec->assign[metricsChannelId].ch;
// Try inverter channel (channel 0) or any channel with maxPwr > 0
@ -823,10 +778,10 @@ class Web {
// report value
if (0 == channel) {
// Report a _total value if also channel values were reported. Otherwise report without _total
char total[7];
char total[7] = {0};
if (metricDeclared) {
// 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) {
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
metricsStep = metricStateRealtimeFieldId;
}
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
len = snprintf(reinterpret_cast<char *>(buffer), maxLen, "%s", metrics.c_str());
break;
case metricsStateAlarmData: // Alarm Info loop : fit to one packet
@ -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;
break;
@ -913,10 +868,9 @@ class Web {
// Traverse all inverters and collect the metric via valueFunc
String inverterMetric(char *buffer, size_t len, const char *format, std::function<uint64_t(Inverter<> *iv)> valueFunc) {
Inverter<> *iv;
String metric = "";
for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) {
iv = mSys->getInverterByPos(metricsInverterId);
for (int id = 0; id < mSys->getNumInverters();id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) {
snprintf(buffer,len,format,iv->config->name, valueFunc(iv));
metric += String(buffer);
@ -937,24 +891,27 @@ class Web {
if(shortUnit == "Hz") return {"_hertz", "gauge"};
return {"", "gauge"};
}
private:
int metricsInverterId = 0;
uint8_t metricsFieldId = 0;
bool metricDeclared = false, metricTotalDeclard = false;
#endif
private:
AsyncWebServer mWeb;
AsyncEventSource mEvts;
bool mProtected;
uint32_t mLogoutTimeout;
uint8_t mLoginIp[4];
IApp *mApp;
HMSYSTEM *mSys;
IApp *mApp = nullptr;
HMSYSTEM *mSys = nullptr;
settings_t *mConfig;
settings_t *mConfig = nullptr;
bool mSerialAddTime;
bool mSerialAddTime = true;
char mSerialBuf[WEB_SERIAL_BUF_SIZE];
uint16_t mSerialBufFill;
bool mSerialClientConnnected;
uint16_t mSerialBufFill = 0;
bool mSerialClientConnnected = false;
File mUploadFp;
bool mUploadFail;
bool mUploadFail = false;
};
#endif /*__WEB_H__*/

2
src/wifi/ahoywifi.cpp

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

21
src/wifi/ahoywifi.h

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

Loading…
Cancel
Save