Browse Source

Merge branch 'lumapu:main' into main

pull/1392/head
Knuti_in_Päse 6 months ago
committed by GitHub
parent
commit
a1a069cd6d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      .gitattributes
  2. 15
      .github/ISSUE_TEMPLATE/report.yaml
  3. 61
      .github/workflows/compile_development.yml
  4. 93
      .github/workflows/compile_release.yml
  5. 2
      .gitignore
  6. 10
      README.md
  7. 14
      manual/Getting_Started.md
  8. 56
      manual/factory_firmware.md
  9. 22
      patches/AsyncWeb_Prometheus.patch
  10. 392
      patches/GxEPD2_HAL.patch
  11. 362
      patches/GxEPD2_SW_SPI.patch
  12. 79
      scripts/add_littlefs_binary.py
  13. 9
      scripts/applyPatches.py
  14. 22
      scripts/buildManifest.py
  15. 172
      scripts/convertHtml.py
  16. 3
      scripts/getVersion.py
  17. 40
      scripts/htmlPreprocessorDefines.py
  18. 2
      src/.gitignore
  19. 89
      src/CHANGES.md
  20. 255
      src/app.cpp
  21. 131
      src/app.h
  22. 28
      src/appInterface.h
  23. 15
      src/config/config.h
  24. 3
      src/config/config_override_example.h
  25. 168
      src/config/settings.h
  26. 65
      src/defines.h
  27. 261
      src/eth/ahoyeth.cpp
  28. 64
      src/eth/ahoyeth.h
  29. 27
      src/hm/CommQueue.h
  30. 92
      src/hm/Communication.h
  31. 23
      src/hm/Heuristic.h
  32. 77
      src/hm/NrfRadio.h
  33. 2
      src/hm/Radio.h
  34. 63
      src/hm/hmDefines.h
  35. 220
      src/hm/hmInverter.h
  36. 27
      src/hm/hmSystem.h
  37. 20
      src/hm/nrfHal.h
  38. 34
      src/hms/CmtRadio.h
  39. 4
      src/hms/cmt2300a.h
  40. 16
      src/hms/cmtHal.h
  41. 73
      src/hms/hmsDefines.h
  42. 151
      src/network/AhoyEthernet.h
  43. 67
      src/network/AhoyEthernetSpi.h
  44. 254
      src/network/AhoyNetwork.h
  45. 20
      src/network/AhoyNetworkHelper.cpp
  46. 39
      src/network/AhoyNetworkHelper.h
  47. 76
      src/network/AhoyWifiAp.h
  48. 103
      src/network/AhoyWifiEsp32.h
  49. 170
      src/network/AhoyWifiEsp8266.h
  50. 375
      src/platformio.ini
  51. 58
      src/plugins/Display/Display.h
  52. 8
      src/plugins/Display/Display_Mono_128X32.h
  53. 6
      src/plugins/Display/Display_Mono_128X64.h
  54. 4
      src/plugins/Display/Display_Mono_64X48.h
  55. 9
      src/plugins/Display/Display_Mono_84X48.h
  56. 103
      src/plugins/Display/Display_ePaper.cpp
  57. 15
      src/plugins/Display/Display_ePaper.h
  58. 304
      src/plugins/Display/epdHal.h
  59. 67
      src/plugins/MaxPower.h
  60. 223
      src/plugins/history.h
  61. 44
      src/plugins/plugin_lang.h
  62. 149
      src/publisher/pubMqtt.h
  63. 96
      src/publisher/pubMqttIvData.h
  64. 2
      src/utils/dbg.h
  65. 35
      src/utils/helper.cpp
  66. 2
      src/utils/helper.h
  67. 8
      src/utils/improv.h
  68. 16
      src/utils/scheduler.h
  69. 3
      src/utils/spiPatcher.cpp
  70. 78
      src/utils/spiPatcher.h
  71. 2
      src/web/Protection.h
  72. 534
      src/web/RestApi.h
  73. 2
      src/web/html/about.html
  74. 33
      src/web/html/api.js
  75. 5
      src/web/html/colorBright.css
  76. 5
      src/web/html/colorDark.css
  77. 142
      src/web/html/grid_info.json
  78. 216
      src/web/html/history.html
  79. 2
      src/web/html/includes/footer.html
  80. 11
      src/web/html/includes/nav.html
  81. 44
      src/web/html/index.html
  82. 15
      src/web/html/serial.html
  83. 369
      src/web/html/setup.html
  84. 40
      src/web/html/style.css
  85. 120
      src/web/html/system.html
  86. 23
      src/web/html/update.html
  87. 41
      src/web/html/visualization.html
  88. 255
      src/web/html/wizard.html
  89. 24
      src/web/lang.h
  90. 312
      src/web/lang.json
  91. 100
      src/web/web.h
  92. 488
      src/wifi/ahoywifi.cpp
  93. 97
      src/wifi/ahoywifi.h
  94. 466
      tools/NodeRED/flows-mqtt-json-example.json

1
.gitattributes

@ -0,0 +1 @@
patches/GxEPD2_HAL.patch eol=lf

15
.github/ISSUE_TEMPLATE/report.yaml

@ -1,9 +1,6 @@
name: "AhoyDTU bug" name: "issue report"
description: "File a bug report" description: "issue report"
title: "[Bug]" labels: ["new", "needs-triage"]
labels: ["bug", "needs-triage"]
assignees:
- lumapu
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -12,7 +9,7 @@ body:
Wir lesen auch gerne Deutsch, bitte fülle die u.a. Fragen aus damit wir Dir bestmöglich helfen können Danke! Wir lesen auch gerne Deutsch, bitte fülle die u.a. Fragen aus damit wir Dir bestmöglich helfen können Danke!
Bitte unser FAQ als Hilfestellung prüfen: https://ahoydtu.de/faq Bitte unser FAQ als Hilfestellung prüfen: https://ahoydtu.de/faq
Please read, copy & fill in the template from our Posting Guide lines into your Support Forum post. Please read, then copy & fill in the template from our Posting Guide lines into your Support Forum post.
We do enjoy the english language, but we need a couple of things to best support you in your goal, please fill in all / most of the details given below. Thanks! We do enjoy the english language, but we need a couple of things to best support you in your goal, please fill in all / most of the details given below. Thanks!
Check our FAQ: https://ahoydtu.de/faq Check our FAQ: https://ahoydtu.de/faq
- type: markdown - type: markdown
@ -35,7 +32,7 @@ body:
label: Assembly label: Assembly
description: description:
options: options:
- I did the assebly by myself - I did the assembly by myself
- the DTU was already assembled - the DTU was already assembled
validations: validations:
required: true required: true
@ -84,7 +81,7 @@ body:
label: Connection picture label: Connection picture
description: description:
options: options:
- label: I will attach/upload an Image of my wiring - label: I will attach/upload an image of my wiring
validations: validations:
required: true required: true
- type: markdown - type: markdown

61
.github/workflows/compile_development.yml

@ -16,23 +16,25 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
build-en: build-en:
name: Build Environments (English) name: Build (EN)
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
strategy: strategy:
matrix: matrix:
variant: variant:
- opendtufusion
- opendtufusion-16MB
- esp8266 - esp8266
- esp8266-all
- esp8266-minimal
- esp8266-prometheus - esp8266-prometheus
- esp8285 - esp8285
- esp32-wroom32 - esp32-wroom32
- esp32-wroom32-minimal
- esp32-wroom32-prometheus - esp32-wroom32-prometheus
- esp32-wroom32-ethernet
- esp32-s2-mini - esp32-s2-mini
- esp32-c3-mini - esp32-c3-mini
- opendtufusion
- opendtufusion-ethernet
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0 - uses: benjlevesque/short-sha@v3.0
@ -67,6 +69,11 @@ jobs:
- name: Run PlatformIO - name: Run PlatformIO
run: pio run -d src -e ${{ matrix.variant }} run: pio run -d src -e ${{ matrix.variant }}
- name: Compress .elf
uses: edgarrc/action-7z@v1
with:
args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf
- name: Rename Firmware - name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
@ -77,23 +84,23 @@ jobs:
path: firmware/* path: firmware/*
build-de: build-de:
name: Build Environments (German) name: Build (DE)
needs: check needs: check
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
strategy: strategy:
matrix: matrix:
variant: variant:
- opendtufusion-de
#- opendtufusion-16MB-de #not needed, only the partions.bin is different and can be used from english build
- esp8266-de - esp8266-de
- esp8266-all-de
- esp8266-prometheus-de - esp8266-prometheus-de
- esp8285-de - esp8285-de
- esp32-wroom32-de - esp32-wroom32-de
- esp32-wroom32-prometheus-de - esp32-wroom32-prometheus-de
- esp32-wroom32-ethernet-de
- esp32-s2-mini-de - esp32-s2-mini-de
- esp32-c3-mini-de - esp32-c3-mini-de
- opendtufusion-de
- opendtufusion-ethernet-de
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0 - uses: benjlevesque/short-sha@v3.0
@ -128,6 +135,11 @@ jobs:
- name: Run PlatformIO - name: Run PlatformIO
run: pio run -d src -e ${{ matrix.variant }} run: pio run -d src -e ${{ matrix.variant }}
- name: Compress .elf
uses: edgarrc/action-7z@v1
with:
args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf
- name: Rename Firmware - name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
@ -138,7 +150,7 @@ jobs:
path: firmware/* path: firmware/*
deploy: deploy:
name: Deploy Environments name: Update Artifacts / Deploy
needs: [build-en, build-de] needs: [build-en, build-de]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: false continue-on-error: false
@ -164,9 +176,26 @@ jobs:
env: env:
VERSION: ${{ steps.version_name.outputs.name }} VERSION: ${{ steps.version_name.outputs.name }}
- name: Create ESP Web Tools Manifest
working-directory: src
run: python ../scripts/buildManifest.py
- name: Copy install html
run: mv scripts/gh-action-dev-build-flash.html firmware/install.html
- name: Copy Changes.md
run: mv src/CHANGES.md firmware/CHANGES.md
- name: Rename firmware directory - name: Rename firmware directory
run: mv firmware ${{ steps.version_name.outputs.name }} run: mv firmware ${{ steps.version_name.outputs.name }}
- name: delete environment Artifacts
uses: geekyeggo/delete-artifact@v4
with:
name: dev-*
- name: Deploy - name: Deploy
uses: nogsantos/scp-deploy@master uses: nogsantos/scp-deploy@master
with: with:
@ -176,3 +205,17 @@ jobs:
port: ${{ secrets.FW_SSH_PORT }} port: ${{ secrets.FW_SSH_PORT }}
user: ${{ secrets.FW_SSH_USER }} user: ${{ secrets.FW_SSH_USER }}
key: ${{ secrets.FW_SSH_KEY }} key: ${{ secrets.FW_SSH_KEY }}
- name: Clean elf files (7z compressed) for Artifact
run: |
rm -f \
${{ steps.version_name.outputs.name }}/*/*.elf.7z
- 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

93
.github/workflows/compile_release.yml

@ -5,36 +5,103 @@ on:
branches: main branches: main
paths-ignore: paths-ignore:
- '**.md' # Do no build on *.md changes - '**.md' # Do no build on *.md changes
- '**.yaml' # e.g. issue report
jobs: jobs:
build: check:
name: Build Environments name: Check Repository
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'lumapu/ahoy' && github.ref_name == 'main' if: github.repository == 'lumapu/ahoy' && github.ref_name == 'main'
continue-on-error: false continue-on-error: false
steps:
- uses: actions/checkout@v4
build-en:
name: Build (EN)
needs: check
runs-on: ubuntu-latest
continue-on-error: false
strategy: strategy:
matrix: matrix:
variant: variant:
- opendtufusion
- opendtufusion-16MB
- esp8266 - esp8266
- esp8266-all
- esp8266-minimal
- esp8266-prometheus - esp8266-prometheus
- esp8285 - esp8285
- esp32-wroom32 - esp32-wroom32
- esp32-wroom32-minimal
- esp32-wroom32-prometheus - esp32-wroom32-prometheus
- esp32-wroom32-ethernet
- esp32-s2-mini - esp32-s2-mini
- esp32-c3-mini - esp32-c3-mini
- opendtufusion steps:
- opendtufusion-ethernet - uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- name: Cache Pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install PlatformIO
run: |
python -m pip install setuptools --upgrade pip
pip install --upgrade platformio
- name: Run PlatformIO
run: pio run -d src -e ${{ matrix.variant }}
- name: Compress .elf
uses: edgarrc/action-7z@v1
with:
args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf
- name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.variant }}
path: firmware/*
build-de:
name: Build (DE)
needs: check
runs-on: ubuntu-latest
continue-on-error: false
strategy:
matrix:
variant:
- opendtufusion-de
#- opendtufusion-16MB-de #not needed, only the partions.bin is different and can be used from english build
- esp8266-de - esp8266-de
- esp8266-all-de
- esp8266-prometheus-de - esp8266-prometheus-de
- esp8285-de - esp8285-de
- esp32-wroom32-de - esp32-wroom32-de
- esp32-wroom32-prometheus-de - esp32-wroom32-prometheus-de
- esp32-wroom32-ethernet-de
- esp32-s2-mini-de - esp32-s2-mini-de
- esp32-c3-mini-de - esp32-c3-mini-de
- opendtufusion-de
- opendtufusion-ethernet-de
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0 - uses: benjlevesque/short-sha@v3.0
@ -69,6 +136,11 @@ jobs:
- name: Run PlatformIO - name: Run PlatformIO
run: pio run -d src -e ${{ matrix.variant }} run: pio run -d src -e ${{ matrix.variant }}
- name: Compress .elf
uses: edgarrc/action-7z@v1
with:
args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf
- name: Rename Firmware - name: Rename Firmware
run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
@ -78,11 +150,10 @@ jobs:
name: ${{ matrix.variant }} name: ${{ matrix.variant }}
path: firmware/* path: firmware/*
release: release:
name: Create Release name: Create Release
needs: [build-en, build-de]
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build]
continue-on-error: false continue-on-error: false
permissions: permissions:
contents: write contents: write
@ -140,7 +211,7 @@ jobs:
deploy: deploy:
name: Deploy Environments to fw.ahoydtu.de name: Deploy Environments to fw.ahoydtu.de
needs: [build, release] needs: [build-en, build-de, release]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: false continue-on-error: false
steps: steps:

2
.gitignore

@ -7,6 +7,7 @@
src/config/config_override.h src/config/config_override.h
src/web/html/h/* src/web/html/h/*
src/web/html/tmp/* src/web/html/tmp/*
src/data/*
/**/Debug /**/Debug
/**/v16/* /**/v16/*
*.db *.db
@ -15,3 +16,4 @@ src/web/html/tmp/*
src/output.map src/output.map
/.venv /.venv
/scripts/__pycache__/*

10
README.md

@ -24,6 +24,7 @@ This repository provides hardware and software solutions for communicating with
## Changelog ## Changelog
[latest Release](https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md) [latest Release](https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md)
[Development Version](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md) [Development Version](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md)
@ -31,10 +32,9 @@ Table of approaches:
| Board | MI | HM | HMS/HMT | comment | HowTo start | | Board | MI | HM | HMS/HMT | comment | HowTo start |
| ------ | -- | -- | ------- | ------- | ---------- | | ------ | -- | -- | ------- | ------- | ---------- |
| [ESP8266/ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) | | [ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | [create your own DTU](https://ahoydtu.de/getting_started/) |
| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | | ESP8266, C++ | ✔️ | ✔️ | ❌ | ⚠️ not recommended for new DTU |
| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | |
| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
⚠️ **Warning: HMS-XXXXW-2T WiFi inverters are not supported. They have a 'W' in their name and a DTU serial number on its sticker** ⚠️ **Warning: HMS-XXXXW-2T WiFi inverters are not supported. They have a 'W' in their name and a DTU serial number on its sticker**
@ -48,6 +48,8 @@ Table of approaches:
## Our Website ## Our Website
[https://ahoydtu.de](https://ahoydtu.de) [https://ahoydtu.de](https://ahoydtu.de)
[Firmware Archive https://fw.ahoydtu.de](https://fw.ahoydtu.de)
## Success Stories ## Success Stories
- [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl) - [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl)

14
manual/Getting_Started.md

@ -17,6 +17,7 @@ Hoymiles Inverters
| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | | | ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
| ✔️ | HMT | 1600, 1800, 2250 | | | ✔️ | HMT | 1600, 1800, 2250 | |
| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). | | ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
| 🟡 | HERF | (supported) | |
## Table of Contents ## Table of Contents
@ -25,16 +26,16 @@ Hoymiles Inverters
- [Things needed](#things-needed) - [Things needed](#things-needed)
- [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there) - [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
- [Wiring things up](#wiring-things-up) - [Wiring things up](#wiring-things-up)
- [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1)
- [ESP32 GPIO settings](#esp32-gpio-settings)
- [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example) - [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
- [Schematic](#schematic) - [Schematic](#schematic)
- [Symbolic view](#symbolic-view) - [Symbolic view](#symbolic-view)
- [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2) - [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
- [Schematic](#schematic-2) - [Schematic](#schematic-2)
- [Symbolic view](#symbolic-view-2) - [Symbolic view](#symbolic-view-2)
- [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1)
- [ESP32 GPIO settings](#esp32-gpio-settings)
- [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware) - [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware)
- [Compiling your own Version](#compiling-your-own-version) - [Compiling your own Version](#compiling-your-own-version)
- [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher) - [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher)
@ -56,9 +57,10 @@ Solenso Inverters:
To build your own AhoyDTU, you only need a few things. Remember that the maker community is always developing new and innovative options that we may not have covered in this readme. To build your own AhoyDTU, you only need a few things. Remember that the maker community is always developing new and innovative options that we may not have covered in this readme.
Start with an ESP8266 or ESP32, and combine it with an NRF24L01+ breakout board. Other ESP boards with at least 4MBytes of ROM may also be suitable. Start with an ESP32 or ESP8266 (not recommended), and combine it with an NRF24L01+ breakout board for HM-series inverters. To communicate with a HMS or HMT inverter you need to use a CMT2300A radio module.
Other ESP boards with at least 4MBytes of ROM may also be suitable.
Make sure to choose an NRF24L01+ module that includes the '+' in its name. This is important because we need the 250kbps features that are only available in the plus-variant. Note for NRF24 radio module: Make sure to choose an NRF24L01+ module that includes the '+' in its name. This is important because we need the 250kbps features that are only available in the plus-variant.
**Attention**: The NRF24L01+ can only communicate with the MI/HM/TSUN inverter. For the HMS/HMT it is needed to use a CMT2300A! **Attention**: The NRF24L01+ can only communicate with the MI/HM/TSUN inverter. For the HMS/HMT it is needed to use a CMT2300A!

56
manual/factory_firmware.md

@ -0,0 +1,56 @@
# Generate factory firmware (ESP32)
If the firmware should already contain predefined settings this guide will help you to compile these into a single binary file.
## Generate default settings
First install on the requested platform the standard firmware and configure everything to your needs. Once you did all changes store them and export them to a `json` file.
## Further prepare default settings
First create a directory `data` inside the following project path: `src/`.
As the export removes all your passwords you need to add them again to the `json` file. Open the `json` file with a text editor and search for all the `"pwd":""` sections. Between the second bunch of quotation marks you have to place the password.
*Note: It's recommended to keep all information in one line to save space on the ESP littlefs partition*
Next rename your export file to `settings.json` and move it to the new created directory. It should be look similar to this:
```
ahoy
|-- src
|-- data
|-- settings.json
|-- config
|-- network
...
```
## build firmware
Choose your prefered environment and build firmware as usual. Once the process is finished you should find along with the standard `firmware.bin` an additional file called `firmware.factory.bin`. Both files are located here: `src/.pio/build/[ENVIRONMENT]/`
## Upload to device
Navigate to the firmware output directory `src/.pio/build/[ENVIRONMENT]/` and open a terminal or vice versa.
Python:
`esptool.py -b 921600 write_flash --flash_mode dio --flash_size detect 0x0 firmware.factory.bin`
Windows:
`esptool.exe -b 921600 write_flash --flash_mode dio --flash_size detect 0x0 firmware.factory.bin`
The upload should be finished within one minute.
## Testing
Reboot your ESP an check if all your settings are present.
## Get updated with 'Mainline'
From time to time a new version of AhoyDTU will be published. To get the changes into your already prepared factory binary generation environment you have to do only a few steps:
1. pull new changes from remote: `git pull`
2. check if the `data` folder is still there and contains the `settings.json`
3. build and upload
4. enjoy

22
patches/AsyncWeb_Prometheus.patch

@ -1,26 +1,26 @@
diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp
index 12be5f8..cffeed7 100644 index 6e88da9..09359c3 100644
--- a/src/AsyncWebSocket.cpp --- a/src/AsyncWebSocket.cpp
+++ b/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp
@@ -737,7 +737,7 @@ void AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len) @@ -827,7 +827,7 @@ void AsyncWebSocketClient::binary(AsyncWebSocketMessageBuffer * buffer)
IPAddress AsyncWebSocketClient::remoteIP() const
{
if (!_client)
- return IPAddress(0U);
+ return IPAddress();
IPAddress AsyncWebSocketClient::remoteIP() {
if(!_client) {
- return IPAddress((uint32_t)0);
+ return IPAddress();
}
return _client->remoteIP(); return _client->remoteIP();
} }
diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp
index 22a549f..e0b36b3 100644 index a22e991..babef18 100644
--- a/src/WebResponses.cpp --- a/src/WebResponses.cpp
+++ b/src/WebResponses.cpp +++ b/src/WebResponses.cpp
@@ -318,7 +318,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u @@ -317,7 +317,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
free(buf); free(buf);
return 0; return 0;
} }
- outLen = sprintf_P((char*)buf+headLen, PSTR("%x"), readLen) + headLen; - outLen = sprintf((char*)buf+headLen, "%x", readLen) + headLen;
+ outLen = sprintf_P((char*)buf+headLen, PSTR("%04x"), readLen) + headLen; + outLen = sprintf((char*)buf+headLen, "%04x", readLen) + headLen;
while(outLen < headLen + 4) buf[outLen++] = ' '; while(outLen < headLen + 4) buf[outLen++] = ' ';
buf[outLen++] = '\r'; buf[outLen++] = '\r';
buf[outLen++] = '\n'; buf[outLen++] = '\n';

392
patches/GxEPD2_HAL.patch

@ -0,0 +1,392 @@
diff --git a/src/GxEPD2_EPD.cpp b/src/GxEPD2_EPD.cpp
index 8df8bef..e9dfb19 100644
--- a/src/GxEPD2_EPD.cpp
+++ b/src/GxEPD2_EPD.cpp
@@ -17,11 +17,10 @@
#include <avr/pgmspace.h>
#endif
-GxEPD2_EPD::GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout,
+GxEPD2_EPD::GxEPD2_EPD(GxEPD2_HalInterface *hal, int16_t busy_level, uint32_t busy_timeout,
uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu) :
WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu),
- _cs(cs), _dc(dc), _rst(rst), _busy(busy), _busy_level(busy_level), _busy_timeout(busy_timeout), _diag_enabled(false),
- _pSPIx(&SPI), _spi_settings(4000000, MSBFIRST, SPI_MODE0)
+ _hal(hal), _busy_level(busy_level), _busy_timeout(busy_timeout), _diag_enabled(false)
{
_initial_write = true;
_initial_refresh = true;
@@ -54,44 +53,10 @@ void GxEPD2_EPD::init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset
Serial.begin(serial_diag_bitrate);
_diag_enabled = true;
}
- if (_cs >= 0)
- {
- digitalWrite(_cs, HIGH); // preset (less glitch for any analyzer)
- pinMode(_cs, OUTPUT);
- digitalWrite(_cs, HIGH); // set (needed e.g. for RP2040)
- }
- if (_dc >= 0)
- {
- digitalWrite(_dc, HIGH); // preset (less glitch for any analyzer)
- pinMode(_dc, OUTPUT);
- digitalWrite(_dc, HIGH); // set (needed e.g. for RP2040)
- }
- _reset();
- if (_busy >= 0)
- {
- pinMode(_busy, INPUT);
- }
- _pSPIx->begin();
- if (_busy == MISO) // may be overridden
- {
- pinMode(_busy, INPUT);
- }
- if (_dc == MISO) // may be overridden, TTGO T5 V2.66
- {
- pinMode(_dc, OUTPUT);
- }
- if (_cs == MISO) // may be overridden
- {
- pinMode(_cs, INPUT);
- }
}
void GxEPD2_EPD::end()
{
- _pSPIx->end();
- if (_cs >= 0) pinMode(_cs, INPUT);
- if (_dc >= 0) pinMode(_dc, INPUT);
- if (_rst >= 0) pinMode(_rst, INPUT);
}
void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter)
@@ -100,34 +65,27 @@ void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void*
_busy_callback_parameter = busy_callback_parameter;
}
-void GxEPD2_EPD::selectSPI(SPIClass& spi, SPISettings spi_settings)
-{
- _pSPIx = &spi;
- _spi_settings = spi_settings;
-}
-
void GxEPD2_EPD::_reset()
{
- if (_rst >= 0)
{
if (_pulldown_rst_mode)
{
- digitalWrite(_rst, LOW);
- pinMode(_rst, OUTPUT);
- digitalWrite(_rst, LOW);
+ _hal->rst(LOW);
+ _hal->rstMode(OUTPUT);
+ _hal->rst(LOW);
delay(_reset_duration);
- pinMode(_rst, INPUT_PULLUP);
+ _hal->rstMode(INPUT_PULLUP);
delay(_reset_duration > 10 ? _reset_duration : 10);
}
else
{
- digitalWrite(_rst, HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, preset (less glitch for any analyzer)
- pinMode(_rst, OUTPUT);
- digitalWrite(_rst, HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, set (needed e.g. for RP2040)
+ _hal->rst(HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, preset (less glitch for any analyzer)
+ _hal->rstMode(OUTPUT);
+ _hal->rst(HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, set (needed e.g. for RP2040)
delay(10); // NEEDED for Waveshare "clever" reset circuit, at least delay(2);
- digitalWrite(_rst, LOW);
+ _hal->rst(LOW);
delay(_reset_duration);
- digitalWrite(_rst, HIGH);
+ _hal->rst(HIGH);
delay(_reset_duration > 10 ? _reset_duration : 10);
}
_hibernating = false;
@@ -136,16 +94,15 @@ void GxEPD2_EPD::_reset()
void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
{
- if (_busy >= 0)
{
delay(1); // add some margin to become active
unsigned long start = micros();
while (1)
{
- if (digitalRead(_busy) != _busy_level) break;
+ if (_hal->getBusy() != _busy_level) break;
if (_busy_callback) _busy_callback(_busy_callback_parameter);
else delay(1);
- if (digitalRead(_busy) != _busy_level) break;
+ if (_hal->getBusy() != _busy_level) break;
if (micros() - start > _busy_timeout)
{
Serial.println("Busy Timeout!");
@@ -169,120 +126,59 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
}
(void) start;
}
- else delay(busy_time);
}
void GxEPD2_EPD::_writeCommand(uint8_t c)
{
- _pSPIx->beginTransaction(_spi_settings);
- if (_dc >= 0) digitalWrite(_dc, LOW);
- if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(c);
- if (_cs >= 0) digitalWrite(_cs, HIGH);
- if (_dc >= 0) digitalWrite(_dc, HIGH);
- _pSPIx->endTransaction();
+ _hal->writeCmd(c);
}
void GxEPD2_EPD::_writeData(uint8_t d)
{
- _pSPIx->beginTransaction(_spi_settings);
- if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(d);
- if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _hal->write(d);
}
void GxEPD2_EPD::_writeData(const uint8_t* data, uint16_t n)
{
- _pSPIx->beginTransaction(_spi_settings);
- if (_cs >= 0) digitalWrite(_cs, LOW);
- for (uint16_t i = 0; i < n; i++)
- {
- _pSPIx->transfer(*data++);
- }
- if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _hal->write(data, n);
}
void GxEPD2_EPD::_writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes)
{
- _pSPIx->beginTransaction(_spi_settings);
- if (_cs >= 0) digitalWrite(_cs, LOW);
- for (uint16_t i = 0; i < n; i++)
- {
- _pSPIx->transfer(pgm_read_byte(&*data++));
- }
- while (fill_with_zeroes > 0)
- {
- _pSPIx->transfer(0x00);
- fill_with_zeroes--;
- }
- if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _hal->write(data, n, fill_with_zeroes);
}
void GxEPD2_EPD::_writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes)
{
- _pSPIx->beginTransaction(_spi_settings);
- for (uint8_t i = 0; i < n; i++)
- {
- if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(pgm_read_byte(&*data++));
- if (_cs >= 0) digitalWrite(_cs, HIGH);
- }
- while (fill_with_zeroes > 0)
- {
- if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(0x00);
- fill_with_zeroes--;
- if (_cs >= 0) digitalWrite(_cs, HIGH);
+ _hal->write(data, n);
+ if (fill_with_zeroes > 0) {
+ uint8_t buf[fill_with_zeroes];
+ memset(buf, 0, fill_with_zeroes);
+ _hal->write(buf, fill_with_zeroes);
}
- _pSPIx->endTransaction();
}
void GxEPD2_EPD::_writeCommandData(const uint8_t* pCommandData, uint8_t datalen)
{
- _pSPIx->beginTransaction(_spi_settings);
- if (_dc >= 0) digitalWrite(_dc, LOW);
- if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(*pCommandData++);
- if (_dc >= 0) digitalWrite(_dc, HIGH);
- for (uint8_t i = 0; i < datalen - 1; i++) // sub the command
- {
- _pSPIx->transfer(*pCommandData++);
- }
- if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _hal->writeCmd(pCommandData, datalen, false);
}
void GxEPD2_EPD::_writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen)
{
- _pSPIx->beginTransaction(_spi_settings);
- if (_dc >= 0) digitalWrite(_dc, LOW);
- if (_cs >= 0) digitalWrite(_cs, LOW);
- _pSPIx->transfer(pgm_read_byte(&*pCommandData++));
- if (_dc >= 0) digitalWrite(_dc, HIGH);
- for (uint8_t i = 0; i < datalen - 1; i++) // sub the command
- {
- _pSPIx->transfer(pgm_read_byte(&*pCommandData++));
- }
- if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _hal->writeCmd(pCommandData, datalen, true);
}
void GxEPD2_EPD::_startTransfer()
{
- _pSPIx->beginTransaction(_spi_settings);
- if (_cs >= 0) digitalWrite(_cs, LOW);
+ _hal->startTransfer();
}
void GxEPD2_EPD::_transfer(uint8_t value)
{
- _pSPIx->transfer(value);
+ _hal->transfer(value);
}
void GxEPD2_EPD::_endTransfer()
{
- if (_cs >= 0) digitalWrite(_cs, HIGH);
- _pSPIx->endTransaction();
+ _hal->endTransfer();
}
diff --git a/src/GxEPD2_EPD.h b/src/GxEPD2_EPD.h
index 34c1145..1e8ea64 100644
--- a/src/GxEPD2_EPD.h
+++ b/src/GxEPD2_EPD.h
@@ -13,9 +13,9 @@
#define _GxEPD2_EPD_H_
#include <Arduino.h>
-#include <SPI.h>
#include <GxEPD2.h>
+#include <GxEPD2_Hal.h>
#pragma GCC diagnostic ignored "-Wunused-parameter"
//#pragma GCC diagnostic ignored "-Wsign-compare"
@@ -31,7 +31,7 @@ class GxEPD2_EPD
const bool hasPartialUpdate;
const bool hasFastPartialUpdate;
// constructor
- GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout,
+ GxEPD2_EPD(GxEPD2_HalInterface *hal, int16_t busy_level, uint32_t busy_timeout,
uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu);
virtual void init(uint32_t serial_diag_bitrate = 0); // serial_diag_bitrate = 0 : disabled
virtual void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 10, bool pulldown_rst_mode = false);
@@ -97,7 +97,6 @@ class GxEPD2_EPD
{
return (a > b ? a : b);
};
- void selectSPI(SPIClass& spi, SPISettings spi_settings);
protected:
void _reset();
void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000);
@@ -112,16 +111,15 @@ class GxEPD2_EPD
void _transfer(uint8_t value);
void _endTransfer();
protected:
- int16_t _cs, _dc, _rst, _busy, _busy_level;
+ GxEPD2_HalInterface *_hal;
+ int16_t _busy_level;
uint32_t _busy_timeout;
bool _diag_enabled, _pulldown_rst_mode;
- SPIClass* _pSPIx;
- SPISettings _spi_settings;
bool _initial_write, _initial_refresh;
bool _power_is_on, _using_partial_mode, _hibernating;
bool _init_display_done;
uint16_t _reset_duration;
- void (*_busy_callback)(const void*);
+ void (*_busy_callback)(const void*);
const void* _busy_callback_parameter;
};
diff --git a/src/GxEPD2_Hal.h b/src/GxEPD2_Hal.h
new file mode 100644
index 0000000..13424b6
--- /dev/null
+++ b/src/GxEPD2_Hal.h
@@ -0,0 +1,19 @@
+#pragma once
+
+class GxEPD2_HalInterface {
+ public:
+ virtual void rstMode(uint8_t mode) = 0;
+ virtual void rst(bool level) = 0;
+ virtual int getBusy(void) = 0;
+ virtual bool isRst(void) = 0;
+
+ virtual void write(uint8_t buf) = 0;
+ virtual void write(const uint8_t *buf, uint16_t n) = 0;
+ virtual void write(const uint8_t *buf, uint16_t n, int16_t fill_with_zeroes) = 0;
+ virtual void writeCmd(const uint8_t val) = 0;
+ virtual void writeCmd(const uint8_t* pCommandData, uint8_t datalen, bool isPGM) = 0;
+
+ virtual void startTransfer(void) = 0;
+ virtual void endTransfer(void) = 0;
+ virtual void transfer(const uint8_t val) = 0;
+};
diff --git a/src/epd/GxEPD2_150_BN.cpp b/src/epd/GxEPD2_150_BN.cpp
index bfb3ddf..dba3d78 100644
--- a/src/epd/GxEPD2_150_BN.cpp
+++ b/src/epd/GxEPD2_150_BN.cpp
@@ -14,8 +14,8 @@
#include "GxEPD2_150_BN.h"
-GxEPD2_150_BN::GxEPD2_150_BN(int16_t cs, int16_t dc, int16_t rst, int16_t busy) :
- GxEPD2_EPD(cs, dc, rst, busy, HIGH, 10000000, WIDTH, HEIGHT, panel, hasColor, hasPartialUpdate, hasFastPartialUpdate)
+GxEPD2_150_BN::GxEPD2_150_BN(GxEPD2_HalInterface *hal) :
+ GxEPD2_EPD(hal, HIGH, 10000000, WIDTH, HEIGHT, panel, hasColor, hasPartialUpdate, hasFastPartialUpdate)
{
}
@@ -269,7 +269,7 @@ void GxEPD2_150_BN::refresh(int16_t x, int16_t y, int16_t w, int16_t h)
int16_t y1 = y < 0 ? 0 : y; // limit
w1 = x1 + w1 < int16_t(WIDTH) ? w1 : int16_t(WIDTH) - x1; // limit
h1 = y1 + h1 < int16_t(HEIGHT) ? h1 : int16_t(HEIGHT) - y1; // limit
- if ((w1 <= 0) || (h1 <= 0)) return;
+ if ((w1 <= 0) || (h1 <= 0)) return;
// make x1, w1 multiple of 8
w1 += x1 % 8;
if (w1 % 8 > 0) w1 += 8 - w1 % 8;
@@ -287,7 +287,7 @@ void GxEPD2_150_BN::powerOff()
void GxEPD2_150_BN::hibernate()
{
_PowerOff();
- if (_rst >= 0)
+ if (_hal->isRst())
{
_writeCommand(0x10); // deep sleep mode
_writeData(0x1); // enter deep sleep
diff --git a/src/epd/GxEPD2_150_BN.h b/src/epd/GxEPD2_150_BN.h
index bc46a45..954b9c4 100644
--- a/src/epd/GxEPD2_150_BN.h
+++ b/src/epd/GxEPD2_150_BN.h
@@ -16,6 +16,7 @@
#define _GxEPD2_150_BN_H_
#include "../GxEPD2_EPD.h"
+#include "../GxEPD2_Hal.h"
class GxEPD2_150_BN : public GxEPD2_EPD
{
@@ -33,7 +34,7 @@ class GxEPD2_150_BN : public GxEPD2_EPD
static const uint16_t full_refresh_time = 4000; // ms, e.g. 3825000us
static const uint16_t partial_refresh_time = 800; // ms, e.g. 736000us
// constructor
- GxEPD2_150_BN(int16_t cs, int16_t dc, int16_t rst, int16_t busy);
+ GxEPD2_150_BN(GxEPD2_HalInterface *hal);
// methods (virtual)
// Support for Bitmaps (Sprites) to Controller Buffer and to Screen
void clearScreen(uint8_t value = 0xFF); // init controller memory and screen (default white)

362
patches/GxEPD2_SW_SPI.patch

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

79
scripts/add_littlefs_binary.py

@ -0,0 +1,79 @@
import os
import subprocess
import shutil
from SCons.Script import DefaultEnvironment
Import("env")
def build_littlefs():
if os.path.isfile('data/settings.json') == False:
return # nothing to do
result = subprocess.run(["pio", "run", "--target", "buildfs", "--environment", env['PIOENV']])
if result.returncode != 0:
print("Error building LittleFS:")
exit(1)
else:
print("LittleFS build successful")
def merge_bins():
if os.path.isfile('data/settings.json') == False:
return # nothing to do
BOOTLOADER_OFFSET = 0x0000
PARTITIONS_OFFSET = 0x8000
FIRMWARE_OFFSET = 0x10000
if env['PIOENV'][:13] == "esp32-wroom32":
BOOTLOADER_OFFSET = 0x1000
flash_size = int(env.BoardConfig().get("upload.maximum_size", "1310720")) # 0x140000
app0_offset = 0x10000
if env['PIOENV'][:7] == "esp8266":
app0_offset = 0
elif env['PIOENV'][:7] == "esp8285":
app0_offset = 0
littlefs_offset = 0x290000
if flash_size == 0x330000:
littlefs_offset = 0x670000
elif flash_size == 0x640000:
littlefs_offset = 0xc90000
# save current wd
start = os.getcwd()
os.chdir('.pio/build/' + env['PIOENV'] + '/')
with open("bootloader.bin", "rb") as bootloader_file:
bootloader_data = bootloader_file.read()
with open("partitions.bin", "rb") as partitions_file:
partitions_data = partitions_file.read()
with open("firmware.bin", "rb") as firmware_file:
firmware_data = firmware_file.read()
with open("littlefs.bin", "rb") as littlefs_file:
littlefs_data = littlefs_file.read()
with open("firmware.factory.bin", "wb") as merged_file:
merged_file.write(b'\xFF' * BOOTLOADER_OFFSET)
merged_file.write(bootloader_data)
merged_file.write(b'\xFF' * (PARTITIONS_OFFSET - (BOOTLOADER_OFFSET + len(bootloader_data))))
merged_file.write(partitions_data)
merged_file.write(b'\xFF' * (FIRMWARE_OFFSET - (PARTITIONS_OFFSET + len(partitions_data))))
merged_file.write(firmware_data)
merged_file.write(b'\xFF' * (littlefs_offset - (FIRMWARE_OFFSET + len(firmware_data))))
merged_file.write(littlefs_data)
os.chdir(start)
def main(target, source, env):
build_littlefs()
merge_bins()
# ensure that script is called once firmeware was compiled
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", main)

9
scripts/applyPatches.py

@ -26,9 +26,10 @@ def applyPatch(libName, patchFile):
# list of patches to apply (relative to /src) # list of patches to apply (relative to /src)
if env['PIOENV'][:22] != "opendtufusion-ethernet": applyPatch("ESPAsyncWebServer-esphome", "../patches/AsyncWeb_Prometheus.patch")
applyPatch("ESP Async WebServer", "../patches/AsyncWeb_Prometheus.patch")
if env['PIOENV'][:13] == "opendtufusion": if (env['PIOENV'][:5] == "esp32") or (env['PIOENV'][:13] == "opendtufusion"):
applyPatch("GxEPD2", "../patches/GxEPD2_SW_SPI.patch") applyPatch("GxEPD2", "../patches/GxEPD2_HAL.patch")
if (env['PIOENV'][:13] == "opendtufusion") or (env['PIOENV'][:5] == "esp32"):
applyPatch("RF24", "../patches/RF24_Hal.patch") applyPatch("RF24", "../patches/RF24_Hal.patch")

22
scripts/buildManifest.py

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

172
scripts/convertHtml.py

@ -7,8 +7,58 @@ import json
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import configparser
Import("env") Import("env")
build_flags = []
import htmlPreprocessorDefines as prepro
def getFlagsOfEnv(env):
config = configparser.ConfigParser()
config.read('platformio.ini')
global build_flags
flags = config[env]['build_flags'].split('\n')
for i in range(len(flags)):
if flags[i][:2] == "-D" or flags[i][:2] == "${":
flags[i] = flags[i][2:]
if flags[i][-13:-1] == ".build_flags":
getFlagsOfEnv(flags[i].split(".build_flags")[0])
elif len(flags[i]) > 0:
build_flags = build_flags + [flags[i]]
def parseDefinesH():
global build_flags
pattern = r'^\s*#\s*define\s+(\w+)'
with open("defines.h", "r") as f:
for line in f:
match = re.match(pattern, line)
if match:
build_flags += [match.group(1)]
def get_build_flags():
getFlagsOfEnv("env:" + env['PIOENV'])
config = configparser.ConfigParser()
config.read('platformio.ini')
parseDefinesH()
# translate board
board = config["env:" + env['PIOENV']]['board']
if board == "esp12e" or board == "esp8285":
build_flags.append("ESP8266")
elif board == "lolin_d32":
build_flags.append("ESP32")
elif board == "lolin_s2_mini":
build_flags.append("ESP32")
build_flags.append("ESP32-S2")
elif board == "lolin_c3_mini":
build_flags.append("ESP32")
build_flags.append("ESP32-C3")
elif board == "esp32-s3-devkitc-1":
build_flags.append("ESP32")
build_flags.append("ESP32-S3")
def get_git_sha(): def get_git_sha():
try: try:
@ -50,38 +100,46 @@ def readVersionFull(path):
return version return version
def htmlParts(file, header, nav, footer, versionPath, lang): def htmlParts(file, header, nav, footer, versionPath, lang):
p = "";
f = open(file, "r") f = open(file, "r")
lines = f.readlines() lines = f.readlines()
f.close(); f.close();
f = open(header, "r") f = open(header, "r")
h = f.read().strip() h = f.readlines()
f.close() f.close()
f = open(nav, "r") f = open(nav, "r")
n = f.read().strip() n = f.readlines()
f.close() f.close()
f = open(footer, "r") f = open(footer, "r")
fo = f.read().strip() fo = f.readlines()
f.close() f.close()
linesExt = []
for line in lines: for line in lines:
line = line.replace("{#HTML_HEADER}", h) if line.find("{#HTML_HEADER}") != -1:
line = line.replace("{#HTML_NAV}", n) linesExt.extend(h)
line = line.replace("{#HTML_FOOTER}", fo) elif line.find("{#HTML_NAV}") != -1:
p += line linesExt.extend(n)
elif line.find("{#HTML_FOOTER}") != -1:
linesExt.extend(fo)
else:
linesExt.append(line)
linesMod = prepro.conv(linesExt, build_flags)
#placeholders #placeholders
version = readVersion(versionPath); version = readVersion(versionPath);
link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>' link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>'
p = ""
for line in linesMod:
p += line
p = p.replace("{#VERSION}", version) p = p.replace("{#VERSION}", version)
p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath)) p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath))
p = p.replace("{#VERSION_GIT}", link) p = p.replace("{#VERSION_GIT}", link)
# remove if - endif ESP32
p = checkIf(p)
p = translate(file, p, lang) p = translate(file, p, lang)
p = translate("general", p, lang) # menu / header / footer p = translate("general", p, lang) # menu / header / footer
@ -90,30 +148,6 @@ def htmlParts(file, header, nav, footer, versionPath, lang):
f.close(); f.close();
return p return p
def checkIf(data):
if (env['PIOENV'][0:5] == "esp32") or env['PIOENV'][0:4] == "open":
data = data.replace("<!--IF_ESP32-->", "")
data = data.replace("<!--ENDIF_ESP32-->", "")
data = data.replace("/*IF_ESP32*/", "")
data = data.replace("/*ENDIF_ESP32*/", "")
else:
while 1:
start = data.find("<!--IF_ESP32-->")
end = data.find("<!--ENDIF_ESP32-->")+18
if -1 == start:
break
else:
data = data[0:start] + data[end:]
while 1:
start = data.find("/*IF_ESP32*/")
end = data.find("/*ENDIF_ESP32*/")+15
if -1 == start:
break
else:
data = data[0:start] + data[end:]
return data
def findLang(file): def findLang(file):
with open('../lang.json') as j: with open('../lang.json') as j:
lang = json.load(j) lang = json.load(j)
@ -189,33 +223,41 @@ def convert2Header(inFile, versionPath, lang):
f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) f.write("#endif /*__{}_{}_H__*/\n".format(define, define2))
f.close() f.close()
# delete all files in the 'h' dir
wd = 'web/html/h' def main():
get_build_flags()
if os.path.exists(wd):
for f in os.listdir(wd): # delete all files in the 'h' dir
os.remove(os.path.join(wd, f)) wd = 'web/html/h'
wd += "/tmp"
if os.path.exists(wd): if os.path.exists(wd):
for f in os.listdir(wd): for f in os.listdir(wd):
os.remove(os.path.join(wd, f)) os.remove(os.path.join(wd, f))
wd += "/tmp"
# grab all files with following extensions if os.path.exists(wd):
os.chdir('./web/html') for f in os.listdir(wd):
types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types os.remove(os.path.join(wd, f))
files_grabbed = []
for files in types: # grab all files with following extensions
files_grabbed.extend(glob.glob(files)) os.chdir('./web/html')
types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types
Path("h").mkdir(exist_ok=True) files_grabbed = []
Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements for files in types:
shutil.copyfile("style.css", "tmp/style.css") files_grabbed.extend(glob.glob(files))
# get language from environment Path("h").mkdir(exist_ok=True)
lang = "en" Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements
if env['PIOENV'][-3:] == "-de": shutil.copyfile("style.css", "tmp/style.css")
lang = "de"
# get language from environment
# go throw the array lang = "en"
for val in files_grabbed: if env['PIOENV'][-3:] == "-de":
convert2Header(val, "../../defines.h", lang) lang = "de"
# go throw the array
for val in files_grabbed:
convert2Header(val, "../../defines.h", lang)
main()

3
scripts/getVersion.py

@ -76,8 +76,9 @@ def renameFw(path_define, env):
fname = version[:-1] + "_" + sha + "_" + env + ".bin" fname = version[:-1] + "_" + sha + "_" + env + ".bin"
os.rename("src/.pio/build/" + env + "/firmware.bin", dst + fname) os.rename("src/.pio/build/" + env + "/firmware.bin", dst + fname)
os.rename("src/.pio/build/" + env + "/firmware.elf.7z", dst + fname[:-3] + "elf.7z")
if env[:5] == "esp32": if env[:5] == "esp32" or env[:4] == "open":
os.rename("src/.pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin") os.rename("src/.pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin")
os.rename("src/.pio/build/" + env + "/partitions.bin", dst + "partitions.bin") os.rename("src/.pio/build/" + env + "/partitions.bin", dst + "partitions.bin")
genOtaBin(dst) genOtaBin(dst)

40
scripts/htmlPreprocessorDefines.py

@ -0,0 +1,40 @@
import re
import os
import queue
def error(msg):
print("ERROR: " + msg)
exit()
def check(inp, lst, pattern):
q = queue.LifoQueue()
out = []
keep = True
for line in inp:
x = re.findall(pattern, line)
if len(x) > 0:
if line.find("ENDIF_") != -1:
if not q.empty():
e = q.get()
if e[0] == x[0]:
keep = e[1]
elif line.find("IF_") != -1:
q.put((x[0], keep))
if keep is True:
keep = x[0] in lst
elif line.find("E") != -1:
if q.empty():
error("(ELSE) missing open statement!")
e = q.get()
q.put(e)
if e[1] is True:
keep = not keep
else:
if keep is True:
out.append(line)
return out
def conv(inp, lst):
#print(lst)
out = check(inp, lst, r'\/\*(?:IF_|ELS|ENDIF_)([A-Z0-9\-_]+)?\*\/')
return check(out, lst, r'\<\!\-\-(?:IF_|ELS|ENDIF_)([A-Z0-9\-_]+)?\-\-\>')

2
src/.gitignore

@ -3,3 +3,5 @@
.vscode/c_cpp_properties.json .vscode/c_cpp_properties.json
.vscode/launch.json .vscode/launch.json
.vscode/ipch .vscode/ipch
scripts/__pycache__/*
*.pyc

89
src/CHANGES.md

@ -1,41 +1,54 @@
Changelog v0.8.83 Changelog v0.8.140
* added German translations for all variants * added HMS-400-1T support (serial number 1125...)
* added reading grid profile * added further ESP8266 versions (-all, -minimal) because of small ressources on ESP8266
* added decimal place for active power control (APC aka power limit) * added some Gridprofiles
* added information about working IRQ for NRF24 and CMT2300A to `/system` * added support for characters in serial number of inverter (A-F)
* added loss rate to `/visualization` in the statistics window and MqTT * added default coordinates on fresh install, needed for history graph on display and WebUI
* added optional output to display whether it's night time or not. Can be reused as output to control battery system or mapped to a LED * added option to reset values on communication start (sunrise)
* added timestamp for `max ac power` as tooltip * added max inverter temperature to WebUI
* added wizard for initial WiFi connection * added yield day to history graph
* added history graph (still under development) * added script and [instructions](../manual/factory_firmware.md) how to generate factory firmware which includes predefined settings
* added simulator (must be activated before compile, standard: off) * added button for downloading coredump (ESP32 variants only) to `/system`. Once a crash happens the reason can be checked afterwards (even after a reboot)
* added minimal version (without: MqTT, Display, History), WebUI is not changed! (not compiled automatically) * added support of HERF inverters, serial number is converted in Javascript
* added info about installed binary to `/update` * added device name to HTML title
* added protection to prevent update to wrong firmware (environment check) * added feature to restart Ahoy using MqTT
* added optional custom link to the menu * added feature to publish MqTT messages as JSON as well (new setting)
* added support for other regions (USA, Indonesia) * add timestamp to JSON output
* added warning for WiFi channel 12-14 (ESP8266 only) * improved communication to inverter
* added `max_power` to MqTT total values * improved translation to German
* added API-Token authentification for external scripts * improved HTML pages, reduced in size by only including relevant contents depending by chip type
* improved MqTT by marking sent data and improved `last_success` resends * improved history graph in WebUI
* improved communication for HM and MI inverters * improved network routines
* improved reading live data from inverter * improved Wizard
* improved sending active power control command faster * improved WebUI by disabling upload and import buttons when no file is selected
* improved `/settings`: pinout has an own subgroup * improved queue, only add new object once they not exist in queue
* improved export by saving settings before they are exported (to have everything in JSON) * improved MqTT `OnMessage` (threadsafe)
* improved code quality (cppcheck) * improved read of alarms, prevent duplicates, update alarm time if there is an update
* seperated sunrise and sunset offset to two fields * improved alarms are now sorted in ascending direction
* fix MqTT night communication * improved by prevent add inverter multiple times
* fix missing favicon to html header * improved sending active power controll commands
* fix build on Windows of `opendtufusion` environments (git: trailing whitespaces) * improved refresh routine of ePaper, full refresh each 12h
* fix generation of DTU-ID * redesigned WebUI on `/system`
* fix: protect commands from popup in `/live` if password is set * changed MqTT retained flags
* fix: prevent sending commands to inverter which isn't active * change MqTT return value of power limit acknowledge from `boolean` to `float`. The value returned is the same as it was set to confirm reception (not the read back value)
* combined firmware and hardware version to JSON topics (MqTT) * converted ePaper and Ethernet to hal-SPI
* updated Prometheus with latest changes * combined Ethernet and WiFi variants - Ethernet is now always included, but needs to be enabled if needed
* upgraded most libraries to newer versions * changed: Ethernet variants (W5500) now support WiFi as fall back / configuration
* beautified typography, added spaces between value and unit for `/visualization` * switch AsyncWebserver library
* removed add to total (MqTT) inverter setting * fixed autodiscovery for homeassistant
* fix reset values functionality
* fix read back of active power control value, now it has one decimal place
* fix NTP issues
* fixed MqTT discovery field `ALARM_MES_ID`
* fix close button color of modal windows in dark mode
* fixed calculation of max AC power
* fixed reset values at midnight if WiFi isn't available
* fixed HMT-1800-4T number of inputs
* fix crash if invalid serial number was set -> inverter will be disabled automatically
* fixed ESP8266, ESP32 static IP
* fixed ethernet MAC address read back
* update several libraries to more recent versions
* removed `yield efficiency` because the inverter already calculates correct
full version log: [Development Log](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md) full version log: [Development Log](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md)

255
src/app.cpp

@ -33,44 +33,33 @@ void app::setup() {
resetSystem(); resetSystem();
esp_task_wdt_reset(); esp_task_wdt_reset();
mSettings.setup(); mSettings.setup(mConfig);
mSettings.getPtr(mConfig);
ah::Scheduler::setup(mConfig->inst.startWithoutTime); ah::Scheduler::setup(mConfig->inst.startWithoutTime);
DPRINT(DBG_INFO, F("Settings valid: ")); DPRINT(DBG_INFO, F("Settings valid: "));
DSERIAL.flush(); DBGPRINTLN(mConfig->valid ? F("true") : F("false"));
if (mSettings.getValid())
DBGPRINTLN(F("true"));
else
DBGPRINTLN(F("false"));
esp_task_wdt_reset(); esp_task_wdt_reset();
if(mConfig->nrf.enabled) { mNrfRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->nrf);
mNrfRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso);
}
#if defined(ESP32) #if defined(ESP32)
if(mConfig->cmt.enabled) { mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->cmt, mConfig->sys.region);
mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, mConfig->sys.region);
}
#endif #endif
#ifdef ETHERNET #ifdef ETHERNET
delay(1000); delay(1000);
mEth.setup(mConfig, &mTimestamp, [this](bool gotIp) { this->onNetwork(gotIp); }, [this](bool gotTime) { this->onNtpUpdate(gotTime); }); mNetwork = static_cast<AhoyNetwork*>(new AhoyEthernet());
#endif // ETHERNET #else
mNetwork = static_cast<AhoyNetwork*>(new AhoyWifi());
#if !defined(ETHERNET) #endif
mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onNetwork, this, std::placeholders::_1)); mNetwork->setup(mConfig, &mTimestamp, [this](bool gotIp) { this->onNetwork(gotIp); }, [this](bool gotTime) { this->onNtpUpdate(gotTime); });
#if !defined(AP_ONLY) mNetwork->begin();
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
#endif
#endif /* defined(ETHERNET) */
esp_task_wdt_reset(); esp_task_wdt_reset();
mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace); mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace);
mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); mCommunication.addPayloadListener([this] (uint8_t cmd, Inverter<> *iv) { payloadEventListener(cmd, iv); });
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); }); mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); });
#endif #endif
mSys.setup(&mTimestamp, &mConfig->inst, this); mSys.setup(&mTimestamp, &mConfig->inst, this);
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
@ -85,16 +74,14 @@ void app::setup() {
esp_task_wdt_reset(); esp_task_wdt_reset();
// when WiFi is in client mode, then enable mqtt broker // when WiFi is in client mode, then enable mqtt broker
#if !defined(AP_ONLY)
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
mMqttEnabled = (mConfig->mqtt.broker[0] > 0); mMqttEnabled = (mConfig->mqtt.broker[0] > 0);
if (mMqttEnabled) { if (mMqttEnabled) {
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime); mMqtt.setup(this, &mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime);
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); }); mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
} }
#endif #endif
#endif
setupLed(); setupLed();
esp_task_wdt_reset(); esp_task_wdt_reset();
@ -107,6 +94,7 @@ void app::setup() {
mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback) mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback)
#endif #endif
// Plugins // Plugins
mMaxPower.setup(&mTimestamp, mConfig->inst.sendInterval);
#if defined(PLUGIN_DISPLAY) #if defined(PLUGIN_DISPLAY)
if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type) if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
#if defined(ESP32) #if defined(ESP32)
@ -124,18 +112,13 @@ void app::setup() {
mPubSerial.setup(mConfig, &mSys, &mTimestamp); mPubSerial.setup(mConfig, &mSys, &mTimestamp);
#if !defined(ETHERNET)
//mImprov.setup(this, mConfig->sys.deviceName, mVersion); //mImprov.setup(this, mConfig->sys.deviceName, mVersion);
#endif
#if defined(ENABLE_SIMULATOR) #if defined(ENABLE_SIMULATOR)
mSimulator.setup(&mSys, &mTimestamp, 0); mSimulator.setup(&mSys, &mTimestamp, 0);
mSimulator.addPayloadListener([this](uint8_t cmd, Inverter<> *iv) { mSimulator.addPayloadListener([this](uint8_t cmd, Inverter<> *iv) { payloadEventListener(cmd, iv); });
payloadEventListener(cmd, iv);
});
#endif /*ENABLE_SIMULATOR*/ #endif /*ENABLE_SIMULATOR*/
esp_task_wdt_reset(); esp_task_wdt_reset();
regularTickers(); regularTickers();
} }
@ -144,12 +127,10 @@ void app::setup() {
void app::loop(void) { void app::loop(void) {
esp_task_wdt_reset(); esp_task_wdt_reset();
if(mConfig->nrf.enabled) mNrfRadio.loop();
mNrfRadio.loop();
#if defined(ESP32) #if defined(ESP32)
if(mConfig->cmt.enabled) mCmtRadio.loop();
mCmtRadio.loop();
#endif #endif
ah::Scheduler::loop(); ah::Scheduler::loop();
@ -159,25 +140,24 @@ void app::loop(void) {
if (mMqttEnabled && mNetworkConnected) if (mMqttEnabled && mNetworkConnected)
mMqtt.loop(); mMqtt.loop();
#endif #endif
#if defined(PLUGIN_DISPLAY)
mDisplay.loop();
#endif
yield(); yield();
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::onNetwork(bool gotIp) { void app::onNetwork(bool gotIp) {
DPRINTLN(DBG_DEBUG, F("onNetwork"));
mNetworkConnected = gotIp; mNetworkConnected = gotIp;
ah::Scheduler::resetTicker(); if(gotIp) {
regularTickers(); //reinstall regular tickers ah::Scheduler::resetTicker();
every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend"); regularTickers(); //reinstall regular tickers
mMqttReconnect = true; every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend");
mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! mTickerInstallOnce = true;
once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers!
#if !defined(ETHERNET) once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2");
if (WIFI_AP == WiFi.getMode()) {
mMqttEnabled = false;
} }
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
#endif /* !defined(ETHERNET) */
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -185,6 +165,10 @@ void app::regularTickers(void) {
DPRINTLN(DBG_DEBUG, F("regularTickers")); DPRINTLN(DBG_DEBUG, F("regularTickers"));
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
everySec([this]() { mProtection->tickSecond(); }, "prot"); everySec([this]() { mProtection->tickSecond(); }, "prot");
everySec([this]() {mNetwork->tickNetworkLoop(); }, "net");
if(mConfig->inst.startWithoutTime && !mNetworkConnected)
every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend");
// Plugins // Plugins
#if defined(PLUGIN_DISPLAY) #if defined(PLUGIN_DISPLAY)
@ -192,9 +176,7 @@ void app::regularTickers(void) {
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
#endif #endif
every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart"); every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart");
#if !defined(ETHERNET)
//everySec([this]() { mImprov.tickSerial(); }, "impro"); //everySec([this]() { mImprov.tickSerial(); }, "impro");
#endif
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
everySec(std::bind(&HistoryType::tickerSecond, &mHistory), "hist"); everySec(std::bind(&HistoryType::tickerSecond, &mHistory), "hist");
@ -205,75 +187,59 @@ void app::regularTickers(void) {
#endif /*ENABLE_SIMULATOR*/ #endif /*ENABLE_SIMULATOR*/
} }
#if defined(ETHERNET) //-----------------------------------------------------------------------------
void app::onNtpUpdate(bool gotTime) { void app::onNtpUpdate(bool gotTime) {
mNtpReceived = true; mNtpReceived = true;
} if ((0 == mSunrise) && (0.0 != mConfig->sun.lat) && (0.0 != mConfig->sun.lon)) {
#endif /* defined(ETHERNET) */ mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
tickCalcSunrise();
//-----------------------------------------------------------------------------
void app::updateNtp(void) {
#if defined(ENABLE_MQTT)
if (mMqttReconnect && mMqttEnabled) {
mMqtt.tickerSecond();
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
} }
#endif /*ENABLE_MQTT*/
// only install schedulers once even if NTP wasn't successful in first loop if (mTickerInstallOnce) {
if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed mTickerInstallOnce = false;
#if defined(ENABLE_MQTT)
if (mMqttEnabled) {
mMqtt.tickerSecond();
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
}
#endif /*ENABLE_MQTT*/
if (mConfig->inst.rstValsNotAvail) if (mConfig->inst.rstValsNotAvail)
everyMin(std::bind(&app::tickMinute, this), "tMin"); everyMin(std::bind(&app::tickMinute, this), "tMin");
uint32_t localTime = gTimezone.toLocal(mTimestamp); if(mNtpReceived) {
uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time uint32_t localTime = gTimezone.toLocal(mTimestamp);
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
if (mConfig->sys.schedReboot) { if (mConfig->sys.schedReboot) {
uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght
if (rebootTrig <= mTimestamp) { //necessary for times other than midnight to prevent reboot loop onceAt(std::bind(&app::tickReboot, this), rebootTrig, "midRe");
rebootTrig += 86400;
} }
onceAt(std::bind(&app::tickReboot, this), rebootTrig, "midRe");
} }
} }
}
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; void app::updateNtp(void) {
tickCalcSunrise(); if(mNtpReceived)
} onNtpUpdate(true);
mMqttReconnect = false;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::tickNtpUpdate(void) { void app::tickNtpUpdate(void) {
uint32_t nxtTrig = 5; // default: check again in 5 sec uint32_t nxtTrig = 5; // default: check again in 5 sec
bool isOK = false;
#if defined(ETHERNET)
if (!mNtpReceived) if (!mNtpReceived)
mEth.updateNtpTime(); mNetwork->updateNtpTime();
else { else {
nxtTrig = mConfig->ntp.interval * 60; // check again in configured interval
mNtpReceived = false; mNtpReceived = false;
isOK = true;
} }
#else
isOK = mWifi.getNtpTime();
#endif
if (isOK) {
this->updateNtp();
nxtTrig = mConfig->ntp.interval * 60; // check again in 12h
// immediately start communicating
if (mSendFirst) {
mSendFirst = false;
once(std::bind(&app::tickSend, this), 1, "senOn");
}
mMqttReconnect = false; updateNtp();
}
once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp");
} }
@ -315,6 +281,8 @@ void app::tickIVCommunication(void) {
if (mTimestamp >= (mSunset + mConfig->sun.offsetSecEvening)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise if (mTimestamp >= (mSunset + mConfig->sun.offsetSecEvening)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
nxtTrig = 0; nxtTrig = 0;
} else { // current time lies within communication start/stop time, set next trigger to communication stop } else { // current time lies within communication start/stop time, set next trigger to communication stop
if((!iv->commEnabled) && mConfig->inst.rstValsCommStart)
zeroValues = true;
iv->commEnabled = true; iv->commEnabled = true;
nxtTrig = mSunset + mConfig->sun.offsetSecEvening; nxtTrig = mSunset + mConfig->sun.offsetSecEvening;
} }
@ -387,18 +355,9 @@ void app::tickMidnight(void) {
// reset alarms // reset alarms
if(InverterStatus::OFF == iv->getStatus()) if(InverterStatus::OFF == iv->getStatus())
iv->resetAlarms(); iv->resetAlarms();
// clear max values
if(mConfig->inst.rstMaxValsMidNight) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t i = 0; i <= iv->channels; i++) {
uint8_t pos = iv->getPosByChFld(i, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f);
}
}
} }
if (mConfig->inst.rstYieldMidNight) { if (mConfig->inst.rstValsAtMidNight) {
zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY); zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY);
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
@ -423,29 +382,8 @@ void app::tickSend(void) {
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
Inverter<> *iv = mSys.getInverterByPos(i); Inverter<> *iv = mSys.getInverterByPos(i);
if(NULL == iv) if(!sendIv(iv))
continue; notAvail = false;
if(iv->config->enabled) {
if(!iv->commEnabled) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("no communication to the inverter (night time)"));
continue;
}
if(!iv->radio->isChipConnected())
continue;
if(InverterStatus::OFF != iv->status)
notAvail = false;
iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) {
if(isDevControl)
mCommunication.addImportant(iv, cmd);
else
mCommunication.add(iv, cmd);
});
}
} }
if(mAllIvNotAvail != notAvail) if(mAllIvNotAvail != notAvail)
@ -455,10 +393,44 @@ void app::tickSend(void) {
updateLed(); updateLed();
} }
//-----------------------------------------------------------------------------
bool app::sendIv(Inverter<> *iv) {
if(NULL == iv)
return true;
if(!iv->config->enabled)
return true;
if(!iv->commEnabled) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("no communication to the inverter (night time)"));
return true;
}
if(!iv->radio->isChipConnected())
return true;
bool notAvail = true;
if(InverterStatus::OFF != iv->status)
notAvail = false;
iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) {
if(isDevControl)
mCommunication.addImportant(iv, cmd);
else
mCommunication.add(iv, cmd);
});
return notAvail;
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
Inverter<> *iv; Inverter<> *iv;
bool changed = false; bool changed = false;
mMaxPower.reset();
// set values to zero, except yields // set values to zero, except yields
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id); iv = mSys.getInverterByPos(id);
@ -468,10 +440,11 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
continue; // skip to next inverter continue; // skip to next inverter
if (checkAvail) { if (checkAvail) {
if (!iv->isAvailable()) if (iv->isAvailable())
continue; continue;
} }
changed = true;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t ch = 0; ch <= iv->channels; ch++) { for(uint8_t ch = 0; ch <= iv->channels; ch++) {
uint8_t pos = 0; uint8_t pos = 0;
@ -488,20 +461,21 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
pos = iv->getPosByChFld(ch, fld, rec); pos = iv->getPosByChFld(ch, fld, rec);
iv->setValue(pos, rec, 0.0f); iv->setValue(pos, rec, 0.0f);
} }
// zero max power // zero max power and max temperature
if(!skipYieldDay) { if(mConfig->inst.rstIncludeMaxVals) {
pos = iv->getPosByChFld(ch, FLD_MP, rec); pos = iv->getPosByChFld(ch, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f); iv->setValue(pos, rec, 0.0f);
} pos = iv->getPosByChFld(ch, FLD_MT, rec);
iv->resetAlarms(); iv->setValue(pos, rec, 0.0f);
iv->resetAlarms(true);
} else
iv->resetAlarms();
iv->doCalculations(); iv->doCalculations();
} }
changed = true;
} }
if(changed) if(changed)
payloadEventListener(RealTimeRunData_Debug, NULL); payloadEventListener(RealTimeRunData_Debug, nullptr);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -548,7 +522,6 @@ void app::resetSystem(void) {
mTimestamp = 1; mTimestamp = 1;
#endif #endif
mSendFirst = true;
mAllIvNotAvail = true; mAllIvNotAvail = true;
mSunrise = 0; mSunrise = 0;
@ -562,10 +535,8 @@ void app::resetSystem(void) {
mSaveReboot = false; mSaveReboot = false;
mNetworkConnected = false; mNetworkConnected = false;
#if defined(ETHERNET)
mNtpReceived = false; mNtpReceived = false;
#endif mTickerInstallOnce = false;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

131
src/app.h

@ -17,9 +17,9 @@
#include "defines.h" #include "defines.h"
#include "appInterface.h" #include "appInterface.h"
#include "hm/hmSystem.h" #include "hm/hmSystem.h"
#include "hm/hmRadio.h" #include "hm/NrfRadio.h"
#if defined(ESP32) #if defined(ESP32)
#include "hms/hmsRadio.h" #include "hms/CmtRadio.h"
#endif #endif
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
#include "publisher/pubMqtt.h" #include "publisher/pubMqtt.h"
@ -31,17 +31,22 @@
#include "utils/syslog.h" #include "utils/syslog.h"
#include "web/RestApi.h" #include "web/RestApi.h"
#include "web/Protection.h" #include "web/Protection.h"
#include "plugins/MaxPower.h"
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
#include "plugins/history.h" #include "plugins/history.h"
#endif /*ENABLE_HISTORY*/ #endif /*ENABLE_HISTORY*/
#include "web/web.h" #include "web/web.h"
#include "hm/Communication.h" #include "hm/Communication.h"
#if defined(ETHERNET) #if defined(ETHERNET)
#include "eth/ahoyeth.h" #include "network/AhoyEthernet.h"
#else /* defined(ETHERNET) */ #else /* defined(ETHERNET) */
#include "wifi/ahoywifi.h" #if defined(ESP32)
#include "utils/improv.h" #include "network/AhoyWifiEsp32.h"
#else
#include "network/AhoyWifiEsp8266.h"
#endif
#endif /* defined(ETHERNET) */ #endif /* defined(ETHERNET) */
#include "utils/improv.h"
#if defined(ENABLE_SIMULATOR) #if defined(ENABLE_SIMULATOR)
#include "hm/simulator.h" #include "hm/simulator.h"
@ -162,32 +167,33 @@ class app : public IApp, public ah::Scheduler {
return mSaveReboot; return mSaveReboot;
} }
#if !defined(ETHERNET)
void scanAvailNetworks() override {
mWifi.scanAvailNetworks();
}
bool getAvailNetworks(JsonObject obj) override { bool getAvailNetworks(JsonObject obj) override {
return mWifi.getAvailNetworks(obj); return mNetwork->getAvailNetworks(obj, this);
} }
void setupStation(void) override { void setupStation(void) override {
mWifi.setupStation(); mNetwork->begin();
} }
void setStopApAllowedMode(bool allowed) override { bool getWasInCh12to14(void) const override {
mWifi.setStopApAllowedMode(allowed); #if defined(ESP8266)
return mNetwork->getWasInCh12to14();
#else
return false;
#endif
} }
String getStationIp(void) override { String getIp(void) override {
return mWifi.getStationIp(); return mNetwork->getIp();
} }
bool getWasInCh12to14(void) const override { String getMac(void) override {
return mWifi.getWasInCh12to14(); return mNetwork->getMac();
} }
#endif /* !defined(ETHERNET) */ bool isApActive(void) override {
return mNetwork->isApActive();
}
void setRebootFlag() override { void setRebootFlag() override {
once(std::bind(&app::tickReboot, this), 3, "rboot"); once(std::bind(&app::tickReboot, this), 3, "rboot");
@ -201,6 +207,10 @@ class app : public IApp, public ah::Scheduler {
return mVersionModules; return mVersionModules;
} }
void addOnce(ah::scdCb c, uint32_t timeout, const char *name) override {
once(c, timeout, name);
}
uint32_t getSunrise() override { uint32_t getSunrise() override {
return mSunrise; return mSunrise;
} }
@ -210,7 +220,7 @@ class app : public IApp, public ah::Scheduler {
} }
bool getSettingsValid() override { bool getSettingsValid() override {
return mSettings.getValid(); return mConfig->valid;
} }
bool getRebootRequestState() override { bool getRebootRequestState() override {
@ -247,6 +257,12 @@ class app : public IApp, public ah::Scheduler {
#endif #endif
} }
#if defined(ETHERNET)
bool isWiredConnection() override {
return mNetwork->isWiredConnection();
}
#endif
void lock(bool fromWeb) override { void lock(bool fromWeb) override {
mProtection->lock(fromWeb); mProtection->lock(fromWeb);
} }
@ -295,15 +311,15 @@ class app : public IApp, public ah::Scheduler {
DPRINT(DBG_DEBUG, F("setTimestamp: ")); DPRINT(DBG_DEBUG, F("setTimestamp: "));
DBGPRINTLN(String(newTime)); DBGPRINTLN(String(newTime));
if(0 == newTime) if(0 == newTime)
{ mNetwork->updateNtpTime();
#if defined(ETHERNET) else {
mEth.updateNtpTime();
#else /* defined(ETHERNET) */
mWifi.getNtpTime();
#endif /* defined(ETHERNET) */
}
else
Scheduler::setTimestamp(newTime); Scheduler::setTimestamp(newTime);
onNtpUpdate(false);
}
}
float getTotalMaxPower(void) override {
return mMaxPower.getTotalMaxPower();
} }
uint16_t getHistoryValue(uint8_t type, uint16_t i) override { uint16_t getHistoryValue(uint8_t type, uint16_t i) override {
@ -314,6 +330,14 @@ class app : public IApp, public ah::Scheduler {
#endif #endif
} }
uint32_t getHistoryPeriod(uint8_t type) override {
#if defined(ENABLE_HISTORY)
return mHistory.getPeriod((HistoryStorageType)type);
#else
return 0;
#endif
}
uint16_t getHistoryMaxDay() override { uint16_t getHistoryMaxDay() override {
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
return mHistory.getMaximumDay(); return mHistory.getMaximumDay();
@ -322,6 +346,21 @@ class app : public IApp, public ah::Scheduler {
#endif #endif
} }
uint32_t getHistoryLastValueTs(uint8_t type) override {
#if defined(ENABLE_HISTORY)
return mHistory.getLastValueTs((HistoryStorageType)type);
#else
return 0;
#endif
}
#if defined(ENABLE_HISTORY_LOAD_DATA)
void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) override {
#if defined(ENABLE_HISTORY)
return mHistory.addValue((HistoryStorageType)historyType, valueType, value);
#endif
}
#endif
private: private:
#define CHECK_AVAIL true #define CHECK_AVAIL true
#define SKIP_YIELD_DAY true #define SKIP_YIELD_DAY true
@ -330,15 +369,14 @@ class app : public IApp, public ah::Scheduler {
void zeroIvValues(bool checkAvail = false, bool skipYieldDay = true); void zeroIvValues(bool checkAvail = false, bool skipYieldDay = true);
void payloadEventListener(uint8_t cmd, Inverter<> *iv) { void payloadEventListener(uint8_t cmd, Inverter<> *iv) {
#if !defined(AP_ONLY) mMaxPower.payloadEvent(cmd, iv);
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
if (mMqttEnabled) if (mMqttEnabled)
mMqtt.payloadEventListener(cmd, iv); mMqtt.payloadEventListener(cmd, iv);
#endif /*ENABLE_MQTT*/
#endif #endif
#if defined(PLUGIN_DISPLAY) #if defined(PLUGIN_DISPLAY)
if(DISP_TYPE_T0_NONE != mConfig->plugin.display.type) if(DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
mDisplay.payloadEventListener(cmd); mDisplay.payloadEventListener(cmd);
#endif #endif
updateLed(); updateLed();
} }
@ -366,14 +404,14 @@ class app : public IApp, public ah::Scheduler {
} }
void tickNtpUpdate(void); void tickNtpUpdate(void);
#if defined(ETHERNET)
void onNtpUpdate(bool gotTime); void onNtpUpdate(bool gotTime);
bool mNtpReceived = false; bool mNtpReceived = false;
#endif /* defined(ETHERNET) */
void updateNtp(void); void updateNtp(void);
void triggerTickSend() override { void triggerTickSend(uint8_t id) override {
once(std::bind(&app::tickSend, this), 0, "tSend"); once([this, id]() {
sendIv(mSys.getInverterByPos(id));
}, 0, "devct");
} }
void tickCalcSunrise(void); void tickCalcSunrise(void);
@ -382,34 +420,28 @@ class app : public IApp, public ah::Scheduler {
void tickSunrise(void); void tickSunrise(void);
void tickComm(void); void tickComm(void);
void tickSend(void); void tickSend(void);
bool sendIv(Inverter<> *iv);
void tickMinute(void); void tickMinute(void);
void tickZeroValues(void); void tickZeroValues(void);
void tickMidnight(void); void tickMidnight(void);
void notAvailChanged(void); void notAvailChanged(void);
HmSystemType mSys; HmSystemType mSys;
HmRadio<> mNrfRadio; NrfRadio<> mNrfRadio;
Communication mCommunication; Communication mCommunication;
bool mShowRebootRequest = false; bool mShowRebootRequest = false;
#if defined(ETHERNET) AhoyNetwork *mNetwork = nullptr;
ahoyeth mEth;
#else /* defined(ETHERNET) */
ahoywifi mWifi;
#endif /* defined(ETHERNET) */
WebType mWeb; WebType mWeb;
RestApiType mApi; RestApiType mApi;
Protection *mProtection = nullptr; Protection *mProtection = nullptr;
#ifdef ENABLE_SYSLOG #ifdef ENABLE_SYSLOG
DbgSyslog mDbgSyslog; DbgSyslog mDbgSyslog;
#endif #endif
//PayloadType mPayload;
//MiPayloadType mMiPayload;
PubSerialType mPubSerial; PubSerialType mPubSerial;
#if !defined(ETHERNET)
//Improv mImprov; //Improv mImprov;
#endif
#ifdef ESP32 #ifdef ESP32
CmtRadio<> mCmtRadio; CmtRadio<> mCmtRadio;
#endif #endif
@ -422,16 +454,14 @@ class app : public IApp, public ah::Scheduler {
bool mSaveReboot = false; bool mSaveReboot = false;
uint8_t mSendLastIvId = 0; uint8_t mSendLastIvId = 0;
bool mSendFirst = false;
bool mAllIvNotAvail = false; bool mAllIvNotAvail = false;
bool mNetworkConnected = false; bool mNetworkConnected = false;
// mqtt
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
PubMqttType mMqtt; PubMqttType mMqtt;
#endif /*ENABLE_MQTT*/ #endif
bool mMqttReconnect = false; bool mTickerInstallOnce = false;
bool mMqttEnabled = false; bool mMqttEnabled = false;
// sun // sun
@ -439,6 +469,7 @@ class app : public IApp, public ah::Scheduler {
uint32_t mSunrise = 0, mSunset = 0; uint32_t mSunrise = 0, mSunset = 0;
// plugins // plugins
MaxPower<float> mMaxPower;
#if defined(PLUGIN_DISPLAY) #if defined(PLUGIN_DISPLAY)
DisplayType mDisplay; DisplayType mDisplay;
DisplayData mDispData; DisplayData mDispData;

28
src/appInterface.h

@ -7,11 +7,8 @@
#define __IAPP_H__ #define __IAPP_H__
#include "defines.h" #include "defines.h"
#if defined(ETHERNET)
#include "AsyncWebServer_ESP32_W5500.h"
#else
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
#endif #include "utils/scheduler.h"
// abstract interface to App. Make members of App accessible from child class // abstract interface to App. Make members of App accessible from child class
// like web or API without forward declaration // like web or API without forward declaration
@ -29,14 +26,14 @@ class IApp {
virtual const char *getVersion() = 0; virtual const char *getVersion() = 0;
virtual const char *getVersionModules() = 0; virtual const char *getVersionModules() = 0;
#if !defined(ETHERNET) virtual void addOnce(ah::scdCb c, uint32_t timeout, const char *name) = 0;
virtual void scanAvailNetworks() = 0;
virtual bool getAvailNetworks(JsonObject obj) = 0; virtual bool getAvailNetworks(JsonObject obj) = 0;
virtual void setupStation(void) = 0; virtual void setupStation(void) = 0;
virtual void setStopApAllowedMode(bool allowed) = 0;
virtual String getStationIp(void) = 0;
virtual bool getWasInCh12to14(void) const = 0; virtual bool getWasInCh12to14(void) const = 0;
#endif /* defined(ETHERNET) */ virtual String getIp(void) = 0;
virtual String getMac(void) = 0;
virtual bool isApActive(void) = 0;
virtual uint32_t getUptime() = 0; virtual uint32_t getUptime() = 0;
virtual uint32_t getTimestamp() = 0; virtual uint32_t getTimestamp() = 0;
@ -48,7 +45,7 @@ class IApp {
virtual void getSchedulerInfo(uint8_t *max) = 0; virtual void getSchedulerInfo(uint8_t *max) = 0;
virtual void getSchedulerNames() = 0; virtual void getSchedulerNames() = 0;
virtual void triggerTickSend() = 0; virtual void triggerTickSend(uint8_t id) = 0;
virtual bool getRebootRequestState() = 0; virtual bool getRebootRequestState() = 0;
virtual bool getSettingsValid() = 0; virtual bool getSettingsValid() = 0;
@ -61,14 +58,23 @@ class IApp {
virtual uint32_t getMqttRxCnt() = 0; virtual uint32_t getMqttRxCnt() = 0;
virtual uint32_t getMqttTxCnt() = 0; virtual uint32_t getMqttTxCnt() = 0;
#if defined(ETHERNET)
virtual bool isWiredConnection() = 0;
#endif
virtual void lock(bool fromWeb) = 0; virtual void lock(bool fromWeb) = 0;
virtual char *unlock(const char *clientIp, bool loginFromWeb) = 0; virtual char *unlock(const char *clientIp, bool loginFromWeb) = 0;
virtual void resetLockTimeout(void) = 0; virtual void resetLockTimeout(void) = 0;
virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0; virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0;
virtual float getTotalMaxPower(void) = 0;
virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0; virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0;
virtual uint32_t getHistoryPeriod(uint8_t type) = 0;
virtual uint16_t getHistoryMaxDay() = 0; virtual uint16_t getHistoryMaxDay() = 0;
virtual uint32_t getHistoryLastValueTs(uint8_t type) = 0;
#if defined(ENABLE_HISTORY_LOAD_DATA)
virtual void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) = 0;
#endif
virtual void* getRadioObj(bool nrf) = 0; virtual void* getRadioObj(bool nrf) = 0;
}; };

15
src/config/config.h

@ -16,9 +16,8 @@
//------------------------------------- //-------------------------------------
// Fallback WiFi Info // Fallback WiFi Info
#define FB_WIFI_SSID "YOUR_WIFI_SSID" #define FB_WIFI_SSID ""
#define FB_WIFI_PWD "YOUR_WIFI_PWD" #define FB_WIFI_PWD ""
// Access Point Info // Access Point Info
// In case there is no WiFi Network or Ahoy can not connect to it, it will act as an Access Point // In case there is no WiFi Network or Ahoy can not connect to it, it will act as an Access Point
@ -28,6 +27,11 @@
// If the next line is uncommented, Ahoy will stay in access point mode all the time // If the next line is uncommented, Ahoy will stay in access point mode all the time
//#define AP_ONLY //#define AP_ONLY
#if defined(AP_ONLY)
#if defined(ENABLE_MQTT)
#undef ENABLE_MQTT
#endif
#endif
// timeout for automatic logoff (20 minutes) // timeout for automatic logoff (20 minutes)
#define LOGOUT_TIMEOUT (20 * 60) #define LOGOUT_TIMEOUT (20 * 60)
@ -77,6 +81,9 @@
#ifndef DEF_ETH_CS_PIN #ifndef DEF_ETH_CS_PIN
#define DEF_ETH_CS_PIN 15 #define DEF_ETH_CS_PIN 15
#endif #endif
#ifndef DEF_ETH_RST_PIN
#define DEF_ETH_RST_PIN DEF_PIN_OFF
#endif
#else /* defined(ETHERNET) */ #else /* defined(ETHERNET) */
// time in seconds how long the station info (ssid + pwd) will be tried // time in seconds how long the station info (ssid + pwd) will be tried
#define WIFI_TRY_CONNECT_TIME 30 #define WIFI_TRY_CONNECT_TIME 30
@ -142,7 +149,7 @@
#ifndef DEF_MOTION_SENSOR_PIN #ifndef DEF_MOTION_SENSOR_PIN
#define DEF_MOTION_SENSOR_PIN DEF_PIN_OFF #define DEF_MOTION_SENSOR_PIN DEF_PIN_OFF
#endif #endif
#else #else // ESP8266
#ifndef DEF_NRF_CS_PIN #ifndef DEF_NRF_CS_PIN
#define DEF_NRF_CS_PIN 15 #define DEF_NRF_CS_PIN 15
#endif #endif

3
src/config/config_override_example.h

@ -6,9 +6,6 @@
#ifndef __CONFIG_OVERRIDE_H__ #ifndef __CONFIG_OVERRIDE_H__
#define __CONFIG_OVERRIDE_H__ #define __CONFIG_OVERRIDE_H__
// override fallback WiFi info
#define FB_WIFI_OVERRIDDEN
// each override must be preceded with an #undef statement // each override must be preceded with an #undef statement
#undef FB_WIFI_SSID #undef FB_WIFI_SSID
#define FB_WIFI_SSID "MY_SSID" #define FB_WIFI_SSID "MY_SSID"

168
src/config/settings.h

@ -33,7 +33,6 @@
#define CONFIG_VERSION 11 #define CONFIG_VERSION 11
#define PROT_MASK_INDEX 0x0001 #define PROT_MASK_INDEX 0x0001
#define PROT_MASK_LIVE 0x0002 #define PROT_MASK_LIVE 0x0002
#define PROT_MASK_SERIAL 0x0004 #define PROT_MASK_SERIAL 0x0004
@ -55,6 +54,20 @@
#define DEF_PROT_MQTT 0x0000 #define DEF_PROT_MQTT 0x0000
#define SSID_LEN 32
#define PWD_LEN 64
#define DEVNAME_LEN 16
#define NTP_ADDR_LEN 32 // DNS Name
#define MQTT_ADDR_LEN 64 // DNS Name
#define MQTT_CLIENTID_LEN 22 // number of chars is limited to 23 up to v3.1 of MQTT
#define MQTT_USER_LEN 65 // there is another byte necessary for \0
#define MQTT_PWD_LEN 65
#define MQTT_TOPIC_LEN 65
#define MQTT_MAX_PACKET_SIZE 384
typedef struct { typedef struct {
uint8_t ip[4]; // ip address uint8_t ip[4]; // ip address
uint8_t mask[4]; // sub mask uint8_t mask[4]; // sub mask
@ -63,6 +76,19 @@ typedef struct {
uint8_t gateway[4]; // standard gateway uint8_t gateway[4]; // standard gateway
} cfgIp_t; } cfgIp_t;
#if defined(ETHERNET)
typedef struct {
bool enabled;
uint8_t pinCs;
uint8_t pinSclk;
uint8_t pinMiso;
uint8_t pinMosi;
uint8_t pinIrq;
uint8_t pinRst;
} cfgEth_t;
#endif
typedef struct { typedef struct {
char deviceName[DEVNAME_LEN]; char deviceName[DEVNAME_LEN];
char adminPwd[PWD_LEN]; char adminPwd[PWD_LEN];
@ -72,13 +98,14 @@ typedef struct {
uint8_t region; uint8_t region;
int8_t timezone; int8_t timezone;
#if !defined(ETHERNET) char apPwd[PWD_LEN];
// wifi // wifi
char stationSsid[SSID_LEN]; char stationSsid[SSID_LEN];
char stationPwd[PWD_LEN]; char stationPwd[PWD_LEN];
char apPwd[PWD_LEN];
bool isHidden; bool isHidden;
#endif /* !defined(ETHERNET) */ #if defined(ETHERNET)
cfgEth_t eth;
#endif
cfgIp_t ip; cfgIp_t ip;
} cfgSys_t; } cfgSys_t;
@ -136,7 +163,9 @@ typedef struct {
char user[MQTT_USER_LEN]; char user[MQTT_USER_LEN];
char pwd[MQTT_PWD_LEN]; char pwd[MQTT_PWD_LEN];
char topic[MQTT_TOPIC_LEN]; char topic[MQTT_TOPIC_LEN];
bool json;
uint16_t interval; uint16_t interval;
bool enableRetain;
} cfgMqtt_t; } cfgMqtt_t;
typedef struct { typedef struct {
@ -152,16 +181,16 @@ typedef struct {
} cfgIv_t; } cfgIv_t;
typedef struct { typedef struct {
bool enabled; // bool enabled;
cfgIv_t iv[MAX_NUM_INVERTERS]; cfgIv_t iv[MAX_NUM_INVERTERS];
uint16_t sendInterval; uint16_t sendInterval;
bool rstYieldMidNight; bool rstValsAtMidNight;
bool rstValsNotAvail; bool rstValsNotAvail;
bool rstValsCommStop; bool rstValsCommStop;
bool rstMaxValsMidNight; bool rstValsCommStart;
bool rstIncludeMaxVals;
bool startWithoutTime; bool startWithoutTime;
float yieldEffiency;
bool readGrid; bool readGrid;
} cfgInst_t; } cfgInst_t;
@ -211,8 +240,9 @@ class settings {
std::fill(reinterpret_cast<char*>(&mCfg), reinterpret_cast<char*>(&mCfg) + sizeof(mCfg), 0); std::fill(reinterpret_cast<char*>(&mCfg), reinterpret_cast<char*>(&mCfg) + sizeof(mCfg), 0);
} }
void setup() { void setup(settings_t *&c) {
DPRINTLN(DBG_INFO, F("Initializing FS ..")); DPRINTLN(DBG_INFO, F("Initializing FS .."));
c = &mCfg;
mCfg.valid = false; mCfg.valid = false;
#if !defined(ESP32) #if !defined(ESP32)
@ -248,31 +278,11 @@ class settings {
DPRINTLN(DBG_INFO, F("FS stopped")); DPRINTLN(DBG_INFO, F("FS stopped"));
} }
void getPtr(settings_t *&cfg) {
cfg = &mCfg;
}
bool getValid(void) {
return mCfg.valid;
}
inline bool getLastSaveSucceed() { inline bool getLastSaveSucceed() {
return mLastSaveSucceed; return mLastSaveSucceed;
} }
void getInfo(uint32_t *used, uint32_t *size) {
#if !defined(ESP32)
FSInfo info;
LittleFS.info(info);
*used = info.usedBytes;
*size = info.totalBytes;
DPRINTLN(DBG_INFO, F("-- FILESYSTEM INFO --"));
DPRINTLN(DBG_INFO, String(info.usedBytes) + F(" of ") + String(info.totalBytes) + F(" used"));
#else
DPRINTLN(DBG_WARN, F("not supported by ESP32"));
#endif
}
bool readSettings(const char* path) { bool readSettings(const char* path) {
loadDefaults(); loadDefaults();
@ -387,14 +397,26 @@ class settings {
// restore temp settings // restore temp settings
if(keepWifi) if(keepWifi)
memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t));
#if !defined(ETHERNET)
else { else {
snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); mCfg.sys.stationSsid[0] = '\0';
snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); mCfg.sys.stationPwd[0] = '\0';
snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD);
mCfg.sys.isHidden = false; mCfg.sys.isHidden = false;
} }
#endif /* !defined(ETHERNET) */ snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD);
#if defined(ETHERNET)
#if defined(DEF_ETH_ENABLED)
mCfg.sys.eth.enabled = true;
#else
mCfg.sys.eth.enabled = false;
#endif
mCfg.sys.eth.pinCs = DEF_ETH_CS_PIN;
mCfg.sys.eth.pinSclk = DEF_ETH_SCK_PIN;
mCfg.sys.eth.pinMiso = DEF_ETH_MISO_PIN;
mCfg.sys.eth.pinMosi = DEF_ETH_MOSI_PIN;
mCfg.sys.eth.pinIrq = DEF_ETH_IRQ_PIN;
mCfg.sys.eth.pinRst = DEF_ETH_RST_PIN;
#endif
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME);
mCfg.sys.region = 0; // Europe mCfg.sys.region = 0; // Europe
@ -407,7 +429,11 @@ class settings {
mCfg.nrf.pinMosi = DEF_NRF_MOSI_PIN; mCfg.nrf.pinMosi = DEF_NRF_MOSI_PIN;
mCfg.nrf.pinSclk = DEF_NRF_SCLK_PIN; mCfg.nrf.pinSclk = DEF_NRF_SCLK_PIN;
#if defined(ETHERNET)
mCfg.nrf.enabled = false;
#else
mCfg.nrf.enabled = true; mCfg.nrf.enabled = true;
#endif
#if defined(ESP32) #if defined(ESP32)
mCfg.cmt.pinSclk = DEF_CMT_SCLK; mCfg.cmt.pinSclk = DEF_CMT_SCLK;
@ -428,8 +454,8 @@ class settings {
mCfg.ntp.port = DEF_NTP_PORT; mCfg.ntp.port = DEF_NTP_PORT;
mCfg.ntp.interval = 720; mCfg.ntp.interval = 720;
mCfg.sun.lat = 0.0; mCfg.sun.lat = 51.1; // mid of Germany
mCfg.sun.lon = 0.0; mCfg.sun.lon = 10.5; // mid of Germany
mCfg.sun.offsetSecMorning = 0; mCfg.sun.offsetSecMorning = 0;
mCfg.sun.offsetSecEvening = 0; mCfg.sun.offsetSecEvening = 0;
@ -445,15 +471,17 @@ class settings {
snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD);
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC);
mCfg.mqtt.interval = 0; // off mCfg.mqtt.interval = 0; // off
mCfg.mqtt.json = false; // off
mCfg.inst.sendInterval = SEND_INTERVAL; mCfg.mqtt.enableRetain = true;
mCfg.inst.rstYieldMidNight = false;
mCfg.inst.rstValsNotAvail = false; mCfg.inst.sendInterval = SEND_INTERVAL;
mCfg.inst.rstValsCommStop = false; mCfg.inst.rstValsAtMidNight = false;
mCfg.inst.startWithoutTime = false; mCfg.inst.rstValsNotAvail = false;
mCfg.inst.rstMaxValsMidNight = false; mCfg.inst.rstValsCommStop = false;
mCfg.inst.yieldEffiency = 1.0f; mCfg.inst.rstValsCommStart = false;
mCfg.inst.readGrid = true; mCfg.inst.startWithoutTime = false;
mCfg.inst.rstIncludeMaxVals = false;
mCfg.inst.readGrid = true;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value
@ -528,12 +556,10 @@ class settings {
void jsonNetwork(JsonObject obj, bool set = false) { void jsonNetwork(JsonObject obj, bool set = false) {
if(set) { if(set) {
char buf[16]; char buf[16];
#if !defined(ETHERNET)
obj[F("ssid")] = mCfg.sys.stationSsid; obj[F("ssid")] = mCfg.sys.stationSsid;
obj[F("pwd")] = mCfg.sys.stationPwd; obj[F("pwd")] = mCfg.sys.stationPwd;
obj[F("ap_pwd")] = mCfg.sys.apPwd; obj[F("ap_pwd")] = mCfg.sys.apPwd;
obj[F("hidd")] = (bool) mCfg.sys.isHidden; obj[F("hidd")] = (bool) mCfg.sys.isHidden;
#endif /* !defined(ETHERNET) */
obj[F("dev")] = mCfg.sys.deviceName; obj[F("dev")] = mCfg.sys.deviceName;
obj[F("adm")] = mCfg.sys.adminPwd; obj[F("adm")] = mCfg.sys.adminPwd;
obj[F("prot_mask")] = mCfg.sys.protectionMask; obj[F("prot_mask")] = mCfg.sys.protectionMask;
@ -546,13 +572,21 @@ class settings {
ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf); ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf); ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf);
ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf); ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf);
#if defined(ETHERNET)
obj[F("en")] = mCfg.sys.eth.enabled;
obj[F("cs")] = mCfg.sys.eth.pinCs;
obj[F("sclk")] = mCfg.sys.eth.pinSclk;
obj[F("miso")] = mCfg.sys.eth.pinMiso;
obj[F("mosi")] = mCfg.sys.eth.pinMosi;
obj[F("irq")] = mCfg.sys.eth.pinIrq;
obj[F("rst")] = mCfg.sys.eth.pinRst;
#endif
} else { } else {
#if !defined(ETHERNET)
getChar(obj, F("ssid"), mCfg.sys.stationSsid, SSID_LEN); getChar(obj, F("ssid"), mCfg.sys.stationSsid, SSID_LEN);
getChar(obj, F("pwd"), mCfg.sys.stationPwd, PWD_LEN); getChar(obj, F("pwd"), mCfg.sys.stationPwd, PWD_LEN);
getChar(obj, F("ap_pwd"), mCfg.sys.apPwd, PWD_LEN); getChar(obj, F("ap_pwd"), mCfg.sys.apPwd, PWD_LEN);
getVal<bool>(obj, F("hidd"), &mCfg.sys.isHidden); getVal<bool>(obj, F("hidd"), &mCfg.sys.isHidden);
#endif /* !defined(ETHERNET) */
getChar(obj, F("dev"), mCfg.sys.deviceName, DEVNAME_LEN); getChar(obj, F("dev"), mCfg.sys.deviceName, DEVNAME_LEN);
getChar(obj, F("adm"), mCfg.sys.adminPwd, PWD_LEN); getChar(obj, F("adm"), mCfg.sys.adminPwd, PWD_LEN);
getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask); getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask);
@ -569,6 +603,16 @@ class settings {
if(mCfg.sys.protectionMask == 0) if(mCfg.sys.protectionMask == 0)
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY; | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY;
#if defined(ETHERNET)
getVal<bool>(obj, F("en"), &mCfg.sys.eth.enabled);
getVal<uint8_t>(obj, F("cs"), &mCfg.sys.eth.pinCs);
getVal<uint8_t>(obj, F("sclk"), &mCfg.sys.eth.pinSclk);
getVal<uint8_t>(obj, F("miso"), &mCfg.sys.eth.pinMiso);
getVal<uint8_t>(obj, F("mosi"), &mCfg.sys.eth.pinMosi);
getVal<uint8_t>(obj, F("irq"), &mCfg.sys.eth.pinIrq);
getVal<uint8_t>(obj, F("rst"), &mCfg.sys.eth.pinRst);
#endif
} }
} }
@ -681,16 +725,20 @@ class settings {
obj[F("user")] = mCfg.mqtt.user; obj[F("user")] = mCfg.mqtt.user;
obj[F("pwd")] = mCfg.mqtt.pwd; obj[F("pwd")] = mCfg.mqtt.pwd;
obj[F("topic")] = mCfg.mqtt.topic; obj[F("topic")] = mCfg.mqtt.topic;
obj[F("json")] = mCfg.mqtt.json;
obj[F("intvl")] = mCfg.mqtt.interval; obj[F("intvl")] = mCfg.mqtt.interval;
obj[F("retain")] = mCfg.mqtt.enableRetain;
} else { } else {
getVal<uint16_t>(obj, F("port"), &mCfg.mqtt.port); getVal<uint16_t>(obj, F("port"), &mCfg.mqtt.port);
getVal<uint16_t>(obj, F("intvl"), &mCfg.mqtt.interval); getVal<uint16_t>(obj, F("intvl"), &mCfg.mqtt.interval);
getVal<bool>(obj, F("json"), &mCfg.mqtt.json);
getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_LEN); getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_LEN);
getChar(obj, F("user"), mCfg.mqtt.user, MQTT_USER_LEN); getChar(obj, F("user"), mCfg.mqtt.user, MQTT_USER_LEN);
getChar(obj, F("clientId"), mCfg.mqtt.clientId, MQTT_CLIENTID_LEN); getChar(obj, F("clientId"), mCfg.mqtt.clientId, MQTT_CLIENTID_LEN);
getChar(obj, F("pwd"), mCfg.mqtt.pwd, MQTT_PWD_LEN); getChar(obj, F("pwd"), mCfg.mqtt.pwd, MQTT_PWD_LEN);
getChar(obj, F("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN); getChar(obj, F("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN);
getVal<bool>(obj, F("retain"), &mCfg.mqtt.enableRetain);
} }
} }
@ -757,30 +805,25 @@ class settings {
void jsonInst(JsonObject obj, bool set = false) { void jsonInst(JsonObject obj, bool set = false) {
if(set) { if(set) {
obj[F("intvl")] = mCfg.inst.sendInterval; obj[F("intvl")] = mCfg.inst.sendInterval;
obj[F("en")] = (bool)mCfg.inst.enabled; // obj[F("en")] = (bool)mCfg.inst.enabled;
obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; obj[F("rstMidNight")] = (bool)mCfg.inst.rstValsAtMidNight;
obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop;
obj[F("rstComStart")] = (bool)mCfg.inst.rstValsCommStart;
obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime; obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime;
obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstMaxValsMidNight; obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstIncludeMaxVals;
obj[F("yldEff")] = mCfg.inst.yieldEffiency;
obj[F("rdGrid")] = (bool)mCfg.inst.readGrid; obj[F("rdGrid")] = (bool)mCfg.inst.readGrid;
} }
else { else {
getVal<uint16_t>(obj, F("intvl"), &mCfg.inst.sendInterval); getVal<uint16_t>(obj, F("intvl"), &mCfg.inst.sendInterval);
getVal<bool>(obj, F("en"), &mCfg.inst.enabled); // getVal<bool>(obj, F("en"), &mCfg.inst.enabled);
getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight); getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstValsAtMidNight);
getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail); getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail);
getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop); getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop);
getVal<bool>(obj, F("rstComStart"), &mCfg.inst.rstValsCommStart);
getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime); getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime);
getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstMaxValsMidNight); getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstIncludeMaxVals);
getVal<float>(obj, F("yldEff"), &mCfg.inst.yieldEffiency);
getVal<bool>(obj, F("rdGrid"), &mCfg.inst.readGrid); getVal<bool>(obj, F("rdGrid"), &mCfg.inst.readGrid);
if(mCfg.inst.yieldEffiency < 0.5)
mCfg.inst.yieldEffiency = 1.0f;
else if(mCfg.inst.yieldEffiency > 1.0f)
mCfg.inst.yieldEffiency = 1.0f;
} }
JsonArray ivArr; JsonArray ivArr;
@ -854,6 +897,7 @@ class settings {
} }
#endif #endif
private:
settings_t mCfg; settings_t mCfg;
bool mLastSaveSucceed = 0; bool mLastSaveSucceed = 0;
}; };

65
src/defines.h

@ -13,8 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 8 #define VERSION_MINOR 8
#define VERSION_PATCH 83 #define VERSION_PATCH 140
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {
uint8_t ch; uint8_t ch;
@ -24,41 +23,6 @@ typedef struct {
uint16_t millis; uint16_t millis;
} packet_t; } packet_t;
typedef enum {
InverterDevInform_Simple = 0, // 0x00
InverterDevInform_All = 1, // 0x01
GridOnProFilePara = 2, // 0x02
HardWareConfig = 3, // 0x03
SimpleCalibrationPara = 4, // 0x04
SystemConfigPara = 5, // 0x05
RealTimeRunData_Debug = 11, // 0x0b
RealTimeRunData_Reality = 12, // 0x0c
RealTimeRunData_A_Phase = 13, // 0x0d
RealTimeRunData_B_Phase = 14, // 0x0e
RealTimeRunData_C_Phase = 15, // 0x0f
AlarmData = 17, // 0x11, Alarm data - all unsent alarms
AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms
RecordData = 19, // 0x13
InternalData = 20, // 0x14
GetLossRate = 21, // 0x15
GetSelfCheckState = 30, // 0x1e
InitDataState = 0xff
} InfoCmdType;
typedef enum {
TurnOn = 0, // 0x00
TurnOff = 1, // 0x01
Restart = 2, // 0x02
Lock = 3, // 0x03
Unlock = 4, // 0x04
ActivePowerContr = 11, // 0x0b
ReactivePowerContr = 12, // 0x0c
PFSet = 13, // 0x0d
CleanState_LockAndAlarm = 20, // 0x14
SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files
Init = 0xff
} DevControlCmdType;
typedef enum { typedef enum {
AbsolutNonPersistent = 0UL, // 0x0000 AbsolutNonPersistent = 0UL, // 0x0000
RelativNonPersistent = 1UL, // 0x0001 RelativNonPersistent = 1UL, // 0x0001
@ -71,13 +35,6 @@ union serial_u {
uint8_t b[8]; uint8_t b[8];
}; };
#define MIN_SERIAL_INTERVAL 2 // 5
#define MIN_SEND_INTERVAL 15
#define MIN_MQTT_INTERVAL 60
enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
enum { enum {
DISP_TYPE_T0_NONE = 0, DISP_TYPE_T0_NONE = 0,
DISP_TYPE_T1_SSD1306_128X64 = 1, DISP_TYPE_T1_SSD1306_128X64 = 1,
@ -89,27 +46,9 @@ enum {
DISP_TYPE_T10_EPAPER = 10 DISP_TYPE_T10_EPAPER = 10
}; };
//-------------------------------------
// EEPROM
//-------------------------------------
#define SSID_LEN 32
#define PWD_LEN 64
#define DEVNAME_LEN 16
#define NTP_ADDR_LEN 32 // DNS Name
#define MQTT_ADDR_LEN 64 // DNS Name
#define MQTT_CLIENTID_LEN 22 // number of chars is limited to 23 up to v3.1 of MQTT
#define MQTT_USER_LEN 65 // there is another byte necessary for \0
#define MQTT_PWD_LEN 65
#define MQTT_TOPIC_LEN 65
#define MQTT_MAX_PACKET_SIZE 384
typedef struct { typedef struct {
uint32_t rxFail; uint32_t rxFail;
uint32_t rxFailNoAnser; uint32_t rxFailNoAnswer;
uint32_t rxSuccess; uint32_t rxSuccess;
uint32_t frmCnt; uint32_t frmCnt;
uint32_t txCnt; uint32_t txCnt;

261
src/eth/ahoyeth.cpp

@ -1,261 +0,0 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#if defined(ETHERNET)
#if defined(ESP32) && defined(F)
#undef F
#define F(sl) (sl)
#endif
#include "ahoyeth.h"
#include <ESPmDNS.h>
//-----------------------------------------------------------------------------
ahoyeth::ahoyeth()
{
// WiFi.onEvent(ESP32_W5500_event);
}
//-----------------------------------------------------------------------------
void ahoyeth::setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNetworkCB, OnTimeCB onTimeCB) {
mConfig = config;
mUtcTimestamp = utcTimestamp;
mOnNetworkCB = onNetworkCB;
mOnTimeCB = onTimeCB;
Serial.flush();
WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void { this->onEthernetEvent(event, info); });
Serial.flush();
#if defined(CONFIG_IDF_TARGET_ESP32S3)
mEthSpi.begin(DEF_ETH_MISO_PIN, DEF_ETH_MOSI_PIN, DEF_ETH_SCK_PIN, DEF_ETH_CS_PIN, DEF_ETH_IRQ_PIN, DEF_ETH_RST_PIN);
#else
ETH.begin(DEF_ETH_MISO_PIN, DEF_ETH_MOSI_PIN, DEF_ETH_SCK_PIN, DEF_ETH_CS_PIN, DEF_ETH_IRQ_PIN, ETH_SPI_CLOCK_MHZ, ETH_SPI_HOST);
#endif
if(mConfig->sys.ip.ip[0] != 0) {
IPAddress ip(mConfig->sys.ip.ip);
IPAddress mask(mConfig->sys.ip.mask);
IPAddress dns1(mConfig->sys.ip.dns1);
IPAddress dns2(mConfig->sys.ip.dns2);
IPAddress gateway(mConfig->sys.ip.gateway);
if(!ETH.config(ip, gateway, mask, dns1, dns2))
DPRINTLN(DBG_ERROR, F("failed to set static IP!"));
}
}
//-----------------------------------------------------------------------------
bool ahoyeth::updateNtpTime(void) {
DPRINTLN(DBG_DEBUG, F(__FUNCTION__)); Serial.flush();
Serial.printf("ETH.linkUp()=%s\n", ETH.linkUp() ? "up" : "down");
Serial.print("ETH.localIP()=");
Serial.println(ETH.localIP());
Serial.printf("Go on? %s\n", (!ETH.localIP()) ? "No..." : "Yes...");
if (!ETH.localIP())
return false;
DPRINTLN(DBG_DEBUG, F("updateNtpTime: checking udp \"connection\"...")); Serial.flush();
if (!mUdp.connected()) {
DPRINTLN(DBG_DEBUG, F("updateNtpTime: About to (re)connect...")); Serial.flush();
IPAddress timeServer;
if (!WiFi.hostByName(mConfig->ntp.addr, timeServer))
return false;
if (!mUdp.connect(timeServer, mConfig->ntp.port))
return false;
DPRINTLN(DBG_DEBUG, F("updateNtpTime: Connected...")); Serial.flush();
mUdp.onPacket([this](AsyncUDPPacket packet) {
DPRINTLN(DBG_DEBUG, F("updateNtpTime: about to handle ntp packet...")); Serial.flush();
this->handleNTPPacket(packet);
});
}
DPRINTLN(DBG_DEBUG, F("updateNtpTime: prepare packet...")); Serial.flush();
// set all bytes in the buffer to 0
memset(mUdpPacketBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
mUdpPacketBuffer[0] = 0b11100011; // LI, Version, Mode
mUdpPacketBuffer[1] = 0; // Stratum, or type of clock
mUdpPacketBuffer[2] = 6; // Polling Interval
mUdpPacketBuffer[3] = 0xEC; // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
mUdpPacketBuffer[12] = 49;
mUdpPacketBuffer[13] = 0x4E;
mUdpPacketBuffer[14] = 49;
mUdpPacketBuffer[15] = 52;
//Send unicast
DPRINTLN(DBG_DEBUG, F("updateNtpTime: send packet...")); Serial.flush();
mUdp.write(mUdpPacketBuffer, sizeof(mUdpPacketBuffer));
return true;
}
//-----------------------------------------------------------------------------
void ahoyeth::handleNTPPacket(AsyncUDPPacket packet) {
char buf[80];
memcpy(buf, packet.data(), sizeof(buf));
unsigned long highWord = word(buf[40], buf[41]);
unsigned long lowWord = word(buf[42], buf[43]);
// combine the four bytes (two words) into a long integer
// this is NTP time (seconds since Jan 1 1900):
unsigned long secsSince1900 = highWord << 16 | lowWord;
*mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time
DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC");
mOnTimeCB(true);
}
//-----------------------------------------------------------------------------
void ahoyeth::welcome(String ip, String mode) {
DBGPRINTLN(F("\n\n--------------------------------"));
DBGPRINTLN(F("Welcome to AHOY!"));
DBGPRINT(F("\npoint your browser to http://"));
DBGPRINT(ip);
DBGPRINTLN(mode);
DBGPRINTLN(F("to configure your device"));
DBGPRINTLN(F("--------------------------------\n"));
}
void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info) {
AWS_LOG(F("[ETH]: Got event..."));
switch (event) {
#if ( ( defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2) ) && ( ARDUINO_ESP32_GIT_VER != 0x46d5afb1 ) )
// For breaking core v2.0.0
// Why so strange to define a breaking enum arduino_event_id_t in WiFiGeneric.h
// compared to the old system_event_id_t, now in tools/sdk/esp32/include/esp_event/include/esp_event_legacy.h
// You can preserve the old enum order and just adding new items to do no harm
case ARDUINO_EVENT_ETH_START:
AWS_LOG(F("\nETH Started"));
//set eth hostname here
if(String(mConfig->sys.deviceName) != "")
ETH.setHostname(mConfig->sys.deviceName);
else
ETH.setHostname("ESP32_W5500");
break;
case ARDUINO_EVENT_ETH_CONNECTED:
AWS_LOG(F("ETH Connected"));
break;
case ARDUINO_EVENT_ETH_GOT_IP:
if (!ESP32_W5500_eth_connected) {
#if defined (CONFIG_IDF_TARGET_ESP32S3)
AWS_LOG3(F("ETH MAC: "), mEthSpi.macAddress(), F(", IPv4: "), ETH.localIP());
#else
AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP());
#endif
if (ETH.fullDuplex()) {
AWS_LOG0(F("FULL_DUPLEX, "));
} else {
AWS_LOG0(F("HALF_DUPLEX, "));
}
AWS_LOG1(ETH.linkSpeed(), F("Mbps"));
ESP32_W5500_eth_connected = true;
mOnNetworkCB(true);
}
if (!MDNS.begin(mConfig->sys.deviceName)) {
DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!"));
} else {
DBGPRINT(F("[WiFi] mDNS established: "));
DBGPRINT(mConfig->sys.deviceName);
DBGPRINTLN(F(".local"));
}
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
AWS_LOG("ETH Disconnected");
ESP32_W5500_eth_connected = false;
mUdp.close();
mOnNetworkCB(false);
break;
case ARDUINO_EVENT_ETH_STOP:
AWS_LOG("\nETH Stopped");
ESP32_W5500_eth_connected = false;
mUdp.close();
mOnNetworkCB(false);
break;
#else
// For old core v1.0.6-
// Core v2.0.0 defines a stupid enum arduino_event_id_t, breaking any code for ESP32_W5500 written for previous core
// Why so strange to define a breaking enum arduino_event_id_t in WiFiGeneric.h
// compared to the old system_event_id_t, now in tools/sdk/esp32/include/esp_event/include/esp_event_legacy.h
// You can preserve the old enum order and just adding new items to do no harm
case SYSTEM_EVENT_ETH_START:
AWS_LOG(F("\nETH Started"));
//set eth hostname here
if(String(mConfig->sys.deviceName) != "")
ETH.setHostname(mConfig->sys.deviceName);
else
ETH.setHostname("ESP32_W5500");
break;
case SYSTEM_EVENT_ETH_CONNECTED:
AWS_LOG(F("ETH Connected"));
break;
case SYSTEM_EVENT_ETH_GOT_IP:
if (!ESP32_W5500_eth_connected) {
AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP());
if (ETH.fullDuplex()) {
AWS_LOG0(F("FULL_DUPLEX, "));
} else {
AWS_LOG0(F("HALF_DUPLEX, "));
}
AWS_LOG1(ETH.linkSpeed(), F("Mbps"));
ESP32_W5500_eth_connected = true;
mOnNetworkCB(true);
}
if (!MDNS.begin(mConfig->sys.deviceName)) {
DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!"));
} else {
DBGPRINT(F("[WiFi] mDNS established: "));
DBGPRINT(mConfig->sys.deviceName);
DBGPRINTLN(F(".local"));
}
break;
case SYSTEM_EVENT_ETH_DISCONNECTED:
AWS_LOG("ETH Disconnected");
ESP32_W5500_eth_connected = false;
mUdp.close();
mOnNetworkCB(false);
break;
case SYSTEM_EVENT_ETH_STOP:
AWS_LOG("\nETH Stopped");
ESP32_W5500_eth_connected = false;
mUdp.close();
mOnNetworkCB(false);
break;
#endif
default:
break;
}
}
#endif /* defined(ETHERNET) */

64
src/eth/ahoyeth.h

@ -1,64 +0,0 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#if defined(ETHERNET)
#ifndef __AHOYETH_H__
#define __AHOYETH_H__
#include <functional>
#include <Arduino.h>
#include <AsyncUDP.h>
#include <DNSServer.h>
#include "ethSpi.h"
#include "../utils/dbg.h"
#include "../config/config.h"
#include "../config/settings.h"
#include "AsyncWebServer_ESP32_W5500.h"
class app;
#define NTP_PACKET_SIZE 48
class ahoyeth {
public: /* types */
typedef std::function<void(bool)> OnNetworkCB;
typedef std::function<void(bool)> OnTimeCB;
public:
ahoyeth();
void setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNetworkCB, OnTimeCB onTimeCB);
bool updateNtpTime(void);
private:
void setupEthernet();
void handleNTPPacket(AsyncUDPPacket packet);
void welcome(String ip, String mode);
void onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info);
private:
#if defined(CONFIG_IDF_TARGET_ESP32S3)
EthSpi mEthSpi;
#endif
settings_t *mConfig = nullptr;
uint32_t *mUtcTimestamp;
AsyncUDP mUdp; // for time server
byte mUdpPacketBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets
OnNetworkCB mOnNetworkCB;
OnTimeCB mOnTimeCB;
};
#endif /*__AHOYETH_H__*/
#endif /* defined(ETHERNET) */

27
src/hm/CommQueue.h

@ -19,13 +19,19 @@ template <uint8_t N=100>
class CommQueue { class CommQueue {
public: public:
void addImportant(Inverter<> *iv, uint8_t cmd) { void addImportant(Inverter<> *iv, uint8_t cmd) {
dec(&mRdPtr); queue_s q(iv, cmd, true);
mQueue[mRdPtr] = queue_s(iv, cmd, true); if(!isIncluded(&q)) {
dec(&mRdPtr);
mQueue[mRdPtr] = q;
}
} }
void add(Inverter<> *iv, uint8_t cmd) { void add(Inverter<> *iv, uint8_t cmd) {
mQueue[mWrPtr] = queue_s(iv, cmd, false); queue_s q(iv, cmd, false);
inc(&mWrPtr); if(!isIncluded(&q)) {
mQueue[mWrPtr] = q;
inc(&mWrPtr);
}
} }
void chgCmd(Inverter<> *iv, uint8_t cmd) { void chgCmd(Inverter<> *iv, uint8_t cmd) {
@ -117,6 +123,19 @@ class CommQueue {
--(*ptr); --(*ptr);
} }
private:
bool isIncluded(const queue_s *q) {
uint8_t ptr = mRdPtr;
while (ptr != mWrPtr) {
if(mQueue[ptr].cmd == q->cmd) {
if(mQueue[ptr].iv->id == q->iv->id)
return true;
}
inc(&ptr);
}
return false;
}
protected: protected:
std::array<queue_s, N> mQueue; std::array<queue_s, N> mQueue;
uint8_t mWrPtr = 0; uint8_t mWrPtr = 0;

92
src/hm/Communication.h

@ -117,7 +117,7 @@ class Communication : public CommQueue<> {
//q->iv->radioStatistics.txCnt++; //q->iv->radioStatistics.txCnt++;
q->iv->radio->mRadioWaitTime.startTimeMonitor(mTimeout); q->iv->radio->mRadioWaitTime.startTimeMonitor(mTimeout);
if(!mIsRetransmit && (q->cmd == AlarmData) || (q->cmd == GridOnProFilePara)) if((!mIsRetransmit && (q->cmd == AlarmData)) || (q->cmd == GridOnProFilePara))
incrAttempt((q->cmd == AlarmData)? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA); incrAttempt((q->cmd == AlarmData)? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA);
mIsRetransmit = false; mIsRetransmit = false;
@ -159,8 +159,6 @@ class Communication : public CommQueue<> {
mFirstTry = false; mFirstTry = false;
mHeu.evalTxChQuality(q->iv, false, 0, 0); mHeu.evalTxChQuality(q->iv, false, 0, 0);
mHeu.getTxCh(q->iv); mHeu.getTxCh(q->iv);
//q->iv->radioStatistics.rxFailNoAnser++; // should only be one of fail or retransmit.
//q->iv->radioStatistics.txCnt--;
q->iv->radioStatistics.retransmits++; q->iv->radioStatistics.retransmits++;
q->iv->radio->mRadioWaitTime.stopTimeMonitor(); q->iv->radio->mRadioWaitTime.stopTimeMonitor();
mState = States::START; mState = States::START;
@ -276,7 +274,7 @@ class Communication : public CommQueue<> {
DBGPRINT(F(" frames missing ")); DBGPRINT(F(" frames missing "));
DBGPRINTLN(F("-> complete retransmit")); DBGPRINTLN(F("-> complete retransmit"));
} }
mHeu.evalTxChQuality(q->iv, false, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt); mHeu.evalTxChQuality(q->iv, false, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt, true);
q->iv->radioStatistics.txCnt--; q->iv->radioStatistics.txCnt--;
q->iv->radioStatistics.retransmits++; q->iv->radioStatistics.retransmits++;
mCompleteRetry = true; mCompleteRetry = true;
@ -622,7 +620,7 @@ class Communication : public CommQueue<> {
else if(q->iv->mGotFragment || mCompleteRetry) else if(q->iv->mGotFragment || mCompleteRetry)
q->iv->radioStatistics.rxFail++; // got no complete payload q->iv->radioStatistics.rxFail++; // got no complete payload
else else
q->iv->radioStatistics.rxFailNoAnser++; // got nothing q->iv->radioStatistics.rxFailNoAnswer++; // got nothing
mWaitTime.startTimeMonitor(1); // maybe remove, side effects unknown mWaitTime.startTimeMonitor(1); // maybe remove, side effects unknown
bool keep = false; bool keep = false;
@ -893,50 +891,60 @@ class Communication : public CommQueue<> {
uint16_t prntsts = (statusMi == 3) ? 1 : statusMi; uint16_t prntsts = (statusMi == 3) ? 1 : statusMi;
bool stsok = true; bool stsok = true;
if ( prntsts != rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)] ) { //sth.'s changed? bool changedStatus = false; //if true, raise alarms and send via mqtt (might affect single channel only)
q->iv->alarmCnt = 1; // minimum... uint8_t oldState = rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)];
if ( prntsts != oldState ) { // sth.'s changed?
stsok = false; stsok = false;
//sth is or was wrong? if( (!oldState) || (!q->iv->alarmCnt) ) { // initial zero value? => just write this channel to main state and raise changed flags
if ((q->iv->type != INV_TYPE_1CH) changedStatus = true;
&& ((statusMi != 3) q->iv->alarmCnt = 1; // minimum...
|| ((q->iv->lastAlarm[stschan].code) && (q->iv->lastAlarm[stschan].code != 1))) } else {
) { //sth is or was wrong?
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); if (q->iv->type == INV_TYPE_1CH) {
q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0); changedStatus = true;
q->iv->alarmCnt = q->iv->type == INV_TYPE_2CH ? 3 : 5; if(q->iv->alarmCnt == 2) // we had sth. other than "producing" in the past
} else if ((q->iv->type == INV_TYPE_1CH) q->iv->lastAlarm[1].end = q->ts;
&& ( (statusMi != 3) else { // copy old state and mark as ended
|| ((q->iv->lastAlarm[stschan].code) && (q->iv->lastAlarm[stschan].code != 1))) q->iv->lastAlarm[1] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts);
) { q->iv->alarmCnt = 2;
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) } 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)
stsok = true; if (q->iv->alarmCnt == 1)
q->iv->alarmCnt = (q->iv->type == INV_TYPE_2CH) ? 5 : 9;
q->iv->alarmLastId = prntsts; //iv->alarmMesIndex; if(q->iv->lastAlarm[stschan].code != prntsts) { // changed?
changedStatus = true;
if (q->iv->alarmCnt > 1) { //more than one channel if(q->iv->lastAlarm[stschan].code) // copy old data and mark as ended (if any)
for (uint8_t ch = 0; ch < (q->iv->alarmCnt); ++ch) { //start with 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);
if (q->iv->lastAlarm[ch].code == 1) { q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0);
stsok = true; }
break; if(changedStatus) {
for (uint8_t i = 1; i <= q->iv->channels; i++) { //start with 1
if (q->iv->lastAlarm[i].code == 1) {
stsok = true;
break;
}
}
} }
} }
} }
if(*mSerialDebug) {
DPRINT(DBG_WARN, F("New state on CH"));
DBGPRINT(String(stschan)); DBGPRINT(F(" ("));
DBGPRINT(String(prntsts)); DBGPRINT(F("): "));
DBGPRINTLN(q->iv->getAlarmStr(prntsts));
}
if(!q->iv->miMultiParts)
q->iv->miMultiParts = 1; // indicate we got status info (1+2 ch types)
} }
if (!stsok) { if (!stsok) {
q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts); q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts);
q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0); q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0);
}
if (changedStatus || !stsok) {
rec->ts = q->ts; rec->ts = q->ts;
rec->mqttSentStatus = MqttSentStatus::NEW_DATA; rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
q->iv->alarmLastId = prntsts; //iv->alarmMesIndex;
if (NULL != mCbAlarm)
(mCbAlarm)(q->iv);
if(*mSerialDebug) {
DPRINT(DBG_WARN, F("New state on CH"));
DBGPRINT(String(stschan)); DBGPRINT(F(" ("));
DBGPRINT(String(prntsts)); DBGPRINT(F("): "));
DBGPRINTLN(q->iv->getAlarmStr(prntsts));
}
} }
if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) { if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) {
@ -947,6 +955,8 @@ class Communication : public CommQueue<> {
DBGPRINTLN(String(q->iv->alarmMesIndex)); DBGPRINTLN(String(q->iv->alarmMesIndex));
} }
} }
if(!q->iv->miMultiParts)
q->iv->miMultiParts = 1; // indicate we got status info (1+2 ch types)
} }
@ -986,10 +996,8 @@ class Communication : public CommQueue<> {
ac_pow += iv->getValue(iv->getPosByChFld(1, FLD_PDC, rec), rec); ac_pow += iv->getValue(iv->getPosByChFld(1, FLD_PDC, rec), rec);
} else { } else {
for(uint8_t i = 1; i <= iv->channels; i++) { for(uint8_t i = 1; i <= iv->channels; i++) {
if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1)) { if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1))
uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); ac_pow += iv->getValue(iv->getPosByChFld(i, FLD_PDC, rec), rec);
ac_pow += iv->getValue(pos, rec);
}
} }
} }
ac_pow = (int) (ac_pow*9.5); ac_pow = (int) (ac_pow*9.5);

23
src/hm/Heuristic.h

@ -38,6 +38,8 @@ class Heuristic {
ih->txRfChId = curId; ih->txRfChId = curId;
curId = (curId + 1) % RF_MAX_CHANNEL_ID; curId = (curId + 1) % RF_MAX_CHANNEL_ID;
} }
if(ih->txRfQuality[ih->txRfChId] == RF_MIN_QUALTIY) // all channels are bad, reset...
ih->clear();
if(ih->testPeriodSendCnt < 0xff) if(ih->testPeriodSendCnt < 0xff)
ih->testPeriodSendCnt++; ih->testPeriodSendCnt++;
@ -71,7 +73,7 @@ class Heuristic {
return id2Ch(ih->txRfChId); return id2Ch(ih->txRfChId);
} }
void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments) { void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments, bool quotaMissed = false) {
HeuristicInv *ih = &iv->heuristics; HeuristicInv *ih = &iv->heuristics;
#if (DBG_DEBUG == DEBUG_LEVEL) #if (DBG_DEBUG == DEBUG_LEVEL)
@ -84,8 +86,10 @@ class Heuristic {
DBGPRINT(", "); DBGPRINT(", ");
DBGPRINTLN(String(ih->lastRxFragments)); DBGPRINTLN(String(ih->lastRxFragments));
#endif #endif
if(quotaMissed) // we got not enough frames on this attempt, but iv was answering
updateQuality(ih, (rxFragments > 3 ? RF_TX_CHAN_QUALITY_GOOD : (rxFragments > 1 ? RF_TX_CHAN_QUALITY_OK : RF_TX_CHAN_QUALITY_LOW)));
if(ih->lastRxFragments == rxFragments) { else if(ih->lastRxFragments == rxFragments) {
if(crcPass) if(crcPass)
updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD); updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD);
else if(!retransmits || isNewTxCh(ih)) { // nothing received: send probably lost else if(!retransmits || isNewTxCh(ih)) { // nothing received: send probably lost
@ -149,7 +153,7 @@ class Heuristic {
DBGPRINT(F(", f: ")); DBGPRINT(F(", f: "));
DBGPRINT(String(iv->radioStatistics.rxFail)); DBGPRINT(String(iv->radioStatistics.rxFail));
DBGPRINT(F(", n: ")); DBGPRINT(F(", n: "));
DBGPRINT(String(iv->radioStatistics.rxFailNoAnser)); DBGPRINT(String(iv->radioStatistics.rxFailNoAnswer));
DBGPRINT(F(" | p: ")); // better debugging for helpers... DBGPRINT(F(" | p: ")); // better debugging for helpers...
if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen)) if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
DBGPRINTLN(String(iv->config->powerLevel-10)); DBGPRINTLN(String(iv->config->powerLevel-10));
@ -213,15 +217,12 @@ class Heuristic {
} }
inline uint8_t id2Ch(uint8_t id) { inline uint8_t id2Ch(uint8_t id) {
switch(id) { if (id < RF_MAX_CHANNEL_ID)
case 0: return 3; return mChList[id];
case 1: return 23; else
case 2: return 40; return 3; // standard
case 3: return 61;
case 4: return 75;
}
return 3; // standard
} }
uint8_t mChList[RF_MAX_CHANNEL_ID] = {03, 23, 40, 61, 75};
}; };

77
src/hm/hmRadio.h → src/hm/NrfRadio.h

@ -8,9 +8,10 @@
#include <RF24.h> #include <RF24.h>
#include "SPI.h" #include "SPI.h"
#include "radio.h" #include "Radio.h"
#include "../config/config.h" #include "../config/config.h"
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) #include "../config/settings.h"
#if defined(SPI_HAL)
#include "nrfHal.h" #include "nrfHal.h"
#endif #endif
@ -28,24 +29,30 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// HM Radio class // HM Radio class
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
template <uint8_t IRQ_PIN = DEF_NRF_IRQ_PIN, uint8_t CE_PIN = DEF_NRF_CE_PIN, uint8_t CS_PIN = DEF_NRF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW, uint8_t SCLK_PIN = DEF_NRF_SCLK_PIN, uint8_t MOSI_PIN = DEF_NRF_MOSI_PIN, uint8_t MISO_PIN = DEF_NRF_MISO_PIN, uint32_t DTU_SN = 0x81001765> template <uint32_t DTU_SN = 0x81001765>
class HmRadio : public Radio { class NrfRadio : public Radio {
public: public:
HmRadio() { NrfRadio() {
mDtuSn = DTU_SN; mDtuSn = DTU_SN;
mIrqRcvd = false; mIrqRcvd = false;
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) #if defined(SPI_HAL)
//mNrf24.reset(new RF24()); //mNrf24.reset(new RF24());
#else #else
mNrf24.reset(new RF24(CE_PIN, CS_PIN, SPI_SPEED)); mNrf24.reset(new RF24(DEF_NRF_CE_PIN, DEF_NRF_CS_PIN, SPI_SPEED));
#endif #endif
} }
~HmRadio() {} ~NrfRadio() {}
void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) { void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, cfgNrf24_t *cfg) {
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); DPRINTLN(DBG_VERBOSE, F("NrfRadio::setup"));
pinMode(irq, INPUT_PULLUP); mCfg = cfg;
//uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN
if(!mCfg->enabled)
return;
pinMode(mCfg->pinIrq, INPUT_PULLUP);
mSerialDebug = serialDebug; mSerialDebug = serialDebug;
mPrivacyMode = privacyMode; mPrivacyMode = privacyMode;
@ -55,8 +62,8 @@ class HmRadio : public Radio {
mDtuRadioId = ((uint64_t)(((mDtuSn >> 24) & 0xFF) | ((mDtuSn >> 8) & 0xFF00) | ((mDtuSn << 8) & 0xFF0000) | ((mDtuSn << 24) & 0xFF000000)) << 8) | 0x01; mDtuRadioId = ((uint64_t)(((mDtuSn >> 24) & 0xFF) | ((mDtuSn >> 8) & 0xFF00) | ((mDtuSn << 8) & 0xFF0000) | ((mDtuSn << 24) & 0xFF000000)) << 8) | 0x01;
#ifdef ESP32 #ifdef ESP32
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) #if defined(SPI_HAL)
mNrfHal.init(mosi, miso, sclk, cs, ce, SPI_SPEED); mNrfHal.init(mCfg->pinMosi, mCfg->pinMiso, mCfg->pinSclk, mCfg->pinCs, mCfg->pinCe, SPI_SPEED);
mNrf24.reset(new RF24(&mNrfHal)); mNrf24.reset(new RF24(&mNrfHal));
#else #else
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
@ -64,7 +71,7 @@ class HmRadio : public Radio {
#else #else
mSpi.reset(new SPIClass(VSPI)); mSpi.reset(new SPIClass(VSPI));
#endif #endif
mSpi->begin(sclk, miso, mosi, cs); mSpi->begin(mCfg->pinSclk, mCfg->pinMiso, mCfg->pinMosi, mCfg->pinCs);
#endif #endif
#else #else
//the old ESP82xx cannot freely place their SPI pins //the old ESP82xx cannot freely place their SPI pins
@ -72,12 +79,12 @@ class HmRadio : public Radio {
mSpi->begin(); mSpi->begin();
#endif #endif
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) #if defined(SPI_HAL)
mNrf24->begin(); mNrf24->begin();
#else #else
mNrf24->begin(mSpi.get(), ce, cs); mNrf24->begin(mSpi.get(), mCfg->pinCe, mCfg->pinCs);
#endif #endif
mNrf24->setRetries(3, 9); // wait 3*250 = 750us, 16 * 250us -> 4000us = 4ms mNrf24->setRetries(3, 15); // wait 3*250 = 750us, 16 * 250us -> 4000us = 4ms
mNrf24->setDataRate(RF24_250KBPS); mNrf24->setDataRate(RF24_250KBPS);
//mNrf24->setAutoAck(true); // enabled by default //mNrf24->setAutoAck(true); // enabled by default
@ -99,21 +106,24 @@ class HmRadio : public Radio {
} }
// returns true if communication is active // returns true if communication is active
bool loop(void) override { void loop(void) {
if(!mCfg->enabled)
return;
if (!mIrqRcvd && !mNRFisInRX) if (!mIrqRcvd && !mNRFisInRX)
return false; // first quick check => nothing to do at all here return; // first quick check => nothing to do at all here
if(NULL == mLastIv) // prevent reading on NULL object! if(NULL == mLastIv) // prevent reading on NULL object!
return false; return;
if(!mIrqRcvd) { // no news from nRF, check timers if(!mIrqRcvd) { // no news from nRF, check timers
if ((millis() - mTimeslotStart) < innerLoopTimeout) if ((millis() - mTimeslotStart) < innerLoopTimeout)
return true; // nothing to do, still waiting return; // nothing to do, still waiting
if (mRadioWaitTime.isTimeout()) { // timeout reached! if (mRadioWaitTime.isTimeout()) { // timeout reached!
mNRFisInRX = false; mNRFisInRX = false;
rx_ready = false; rx_ready = false;
return false; return;
} }
// otherwise switch to next RX channel // otherwise switch to next RX channel
@ -132,7 +142,7 @@ class HmRadio : public Radio {
mNrf24->setChannel(mRfChLst[tempRxChIdx]); mNrf24->setChannel(mRfChLst[tempRxChIdx]);
isRxInit = false; isRxInit = false;
return true; // communicating, but changed RX channel return; // communicating, but changed RX channel
} else { } else {
// here we got news from the nRF // here we got news from the nRF
mIrqRcvd = false; mIrqRcvd = false;
@ -145,7 +155,7 @@ class HmRadio : public Radio {
if(mNRFisInRX) { if(mNRFisInRX) {
DPRINTLN(DBG_WARN, F("unexpected tx irq!")); DPRINTLN(DBG_WARN, F("unexpected tx irq!"));
return false; return;
} }
mNRFisInRX = true; mNRFisInRX = true;
@ -159,7 +169,7 @@ class HmRadio : public Radio {
mTimeslotStart = millis(); mTimeslotStart = millis();
tempRxChIdx = mRxChIdx; // might be better to start off with one channel less? tempRxChIdx = mRxChIdx; // might be better to start off with one channel less?
mRxPendular = false; mRxPendular = false;
mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1); mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1 || mLastIv->mCmd == MI_REQ_CH2);
innerLoopTimeout = DURATION_LISTEN_MIN; innerLoopTimeout = DURATION_LISTEN_MIN;
} }
@ -181,18 +191,23 @@ class HmRadio : public Radio {
} }
} }
rx_ready = false; // reset rx_ready = false; // reset
return mNRFisInRX; return;
} }
} }
return false; return;
} }
bool isChipConnected(void) const override { bool isChipConnected(void) const override {
if(!mCfg->enabled)
return false;
return mNrf24->isChipConnected(); return mNrf24->isChipConnected();
} }
void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override { void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override {
if(!mCfg->enabled)
return;
DPRINT_IVID(DBG_INFO, iv->id); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("sendControlPacket cmd: ")); DBGPRINT(F("sendControlPacket cmd: "));
DBGHEXLN(cmd); DBGHEXLN(cmd);
@ -279,13 +294,14 @@ class HmRadio : public Radio {
} }
uint8_t getDataRate(void) const { uint8_t getDataRate(void) const {
if(!mNrf24->isChipConnected()) if(!isChipConnected())
return 3; // unknown return 3; // unknown
return mNrf24->getDataRate(); return mNrf24->getDataRate();
} }
bool isPVariant(void) const { bool isPVariant(void) const {
return mNrf24->isPVariant(); if(!isChipConnected())
return mNrf24->isPVariant();
} }
private: private:
@ -413,6 +429,7 @@ class HmRadio : public Radio {
} }
uint64_t mDtuRadioId = 0ULL; uint64_t mDtuRadioId = 0ULL;
cfgNrf24_t *mCfg = nullptr;
const uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz const uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz
uint8_t mTxChIdx = 0; uint8_t mTxChIdx = 0;
uint8_t mRxChIdx = 0; uint8_t mRxChIdx = 0;
@ -432,7 +449,7 @@ class HmRadio : public Radio {
std::unique_ptr<SPIClass> mSpi; std::unique_ptr<SPIClass> mSpi;
std::unique_ptr<RF24> mNrf24; std::unique_ptr<RF24> mNrf24;
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) #if defined(SPI_HAL)
nrfHal mNrfHal; nrfHal mNrfHal;
#endif #endif
Inverter<> *mLastIv = NULL; Inverter<> *mLastIv = NULL;

2
src/hm/radio.h → src/hm/Radio.h

@ -33,7 +33,7 @@ class Radio {
virtual uint16_t getBaseFreqMhz() { return 0; } virtual uint16_t getBaseFreqMhz() { return 0; }
virtual uint16_t getBootFreqMhz() { return 0; } virtual uint16_t getBootFreqMhz() { return 0; }
virtual std::pair<uint16_t,uint16_t> getFreqRangeMhz(void) { return std::make_pair(0, 0); } virtual std::pair<uint16_t,uint16_t> getFreqRangeMhz(void) { return std::make_pair(0, 0); }
virtual bool loop(void) = 0; virtual void loop(void) = 0;
Radio() : mTxBuf{} {} Radio() : mTxBuf{} {}

63
src/hm/hmDefines.h

@ -9,6 +9,41 @@
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include <cstdint> #include <cstdint>
typedef enum {
InverterDevInform_Simple = 0, // 0x00
InverterDevInform_All = 1, // 0x01
GridOnProFilePara = 2, // 0x02
HardWareConfig = 3, // 0x03
SimpleCalibrationPara = 4, // 0x04
SystemConfigPara = 5, // 0x05
RealTimeRunData_Debug = 11, // 0x0b
RealTimeRunData_Reality = 12, // 0x0c
RealTimeRunData_A_Phase = 13, // 0x0d
RealTimeRunData_B_Phase = 14, // 0x0e
RealTimeRunData_C_Phase = 15, // 0x0f
AlarmData = 17, // 0x11, Alarm data - all unsent alarms
AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms
RecordData = 19, // 0x13
InternalData = 20, // 0x14
GetLossRate = 21, // 0x15
GetSelfCheckState = 30, // 0x1e
InitDataState = 0xff
} InfoCmdType;
typedef enum {
TurnOn = 0, // 0x00
TurnOff = 1, // 0x01
Restart = 2, // 0x02
Lock = 3, // 0x03
Unlock = 4, // 0x04
ActivePowerContr = 11, // 0x0b
ReactivePowerContr = 12, // 0x0c
PFSet = 13, // 0x0d
CleanState_LockAndAlarm = 20, // 0x14
SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files
Init = 0xff
} DevControlCmdType;
// inverter generations // inverter generations
enum {IV_MI = 0, IV_HM, IV_HMS, IV_HMT, IV_UNKNOWN}; enum {IV_MI = 0, IV_HM, IV_HMS, IV_HMT, IV_UNKNOWN};
const char* const generationNames[] = {"MI", "HM", "HMS", "HMT", "UNKNOWN"}; const char* const generationNames[] = {"MI", "HM", "HMS", "HMT", "UNKNOWN"};
@ -24,20 +59,20 @@ enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT,
FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR, FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR,
FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_BOOTLOADER_VER, FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_BOOTLOADER_VER,
FLD_ACT_ACTIVE_PWR_LIMIT, FLD_PART_NUM, FLD_HW_VERSION, FLD_GRID_PROFILE_CODE, FLD_ACT_ACTIVE_PWR_LIMIT, FLD_PART_NUM, FLD_HW_VERSION, FLD_GRID_PROFILE_CODE,
FLD_GRID_PROFILE_VERSION, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP}; FLD_GRID_PROFILE_VERSION, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP, FLD_MT};
const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal",
"U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "UAC_12", "UAC_23", "UAC_31", "I_AC", "U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "U_AC_12", "U_AC_23", "U_AC_31", "I_AC",
"IAC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC", "I_AC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC",
"ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","BootloaderVersion", "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","BootloaderVersion",
"active_PowerLimit", "HWPartNumber", "HWVersion", "GridProfileCode", "active_PowerLimit", "HWPartNumber", "HWVersion", "GridProfileCode",
"GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower"}; "GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower", "MaxTemp"};
const char* const notAvail = "n/a"; const char* const notAvail = "n/a";
const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH, const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH,
UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_A, UNIT_A, UNIT_A, UNIT_A, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_A, UNIT_A, UNIT_A, UNIT_A,
UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR, UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR,
UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_W}; UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_W, UNIT_C};
// mqtt discovery device classes // mqtt discovery device classes
enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP};
@ -68,7 +103,7 @@ const byteAssign_fieldDeviceClass deviceFieldAssignment[] = {
#define DEVICE_CLS_ASSIGN_LIST_LEN (sizeof(deviceFieldAssignment) / sizeof(byteAssign_fieldDeviceClass)) #define DEVICE_CLS_ASSIGN_LIST_LEN (sizeof(deviceFieldAssignment) / sizeof(byteAssign_fieldDeviceClass))
// indices to calculation functions, defined in hmInverter.h // indices to calculation functions, defined in hmInverter.h
enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH, CALC_PDC_CH0, CALC_EFF_CH0, CALC_IRR_CH, CALC_MPAC_CH0, CALC_MPDC_CH}; enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH, CALC_PDC_CH0, CALC_EFF_CH0, CALC_IRR_CH, CALC_MPAC_CH0, CALC_MPDC_CH, CALC_MT_CH0};
enum {CMD_CALC = 0xffff}; enum {CMD_CALC = 0xffff};
@ -173,7 +208,8 @@ const byteAssign_t hm1chAssignment[] = {
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC },
{ FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC },
{ FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC }
}; };
#define HM1CH_LIST_LEN (sizeof(hm1chAssignment) / sizeof(byteAssign_t)) #define HM1CH_LIST_LEN (sizeof(hm1chAssignment) / sizeof(byteAssign_t))
#define HM1CH_PAYLOAD_LEN 30 #define HM1CH_PAYLOAD_LEN 30
@ -211,7 +247,8 @@ const byteAssign_t hm2chAssignment[] = {
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC },
{ FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC },
{ FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC }
}; };
#define HM2CH_LIST_LEN (sizeof(hm2chAssignment) / sizeof(byteAssign_t)) #define HM2CH_LIST_LEN (sizeof(hm2chAssignment) / sizeof(byteAssign_t))
@ -266,7 +303,8 @@ const byteAssign_t hm4chAssignment[] = {
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC },
{ FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC },
{ FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC }
}; };
#define HM4CH_LIST_LEN (sizeof(hm4chAssignment) / sizeof(byteAssign_t)) #define HM4CH_LIST_LEN (sizeof(hm4chAssignment) / sizeof(byteAssign_t))
#define HM4CH_PAYLOAD_LEN 62 #define HM4CH_PAYLOAD_LEN 62
@ -351,8 +389,11 @@ const devInfo_t devInfo[] = {
{ 0x102271, 2000 }, // v2 black backplane, 16A { 0x102271, 2000 }, // v2 black backplane, 16A
// HMT // HMT
{ 0x103311, 1800 }, { 0x103241, 1600 }, // -4T
{ 0x103331, 2250 } { 0x103251, 1800 }, // -4T
{ 0x103271, 2000 }, // -4T
{ 0x103311, 1800 }, // -6T
{ 0x103331, 2250 } // -6T
}; };
#define MI_REQ_CH1 0x09 #define MI_REQ_CH1 0x09

220
src/hm/hmInverter.h

@ -22,7 +22,7 @@
#include <functional> #include <functional>
#include "../config/settings.h" #include "../config/settings.h"
#include "radio.h" #include "Radio.h"
/** /**
* For values which are of interest and not transmitted by the inverter can be * For values which are of interest and not transmitted by the inverter can be
* calculated automatically. * calculated automatically.
@ -33,28 +33,31 @@
// prototypes // prototypes
template<class T=float> template<class T=float>
static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0); T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0);
template<class T=float> template<class T=float>
static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0); T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0);
template<class T=float> template<class T=float>
static T calcUdcCh(Inverter<> *iv, uint8_t arg0); T calcUdcCh(Inverter<> *iv, uint8_t arg0);
template<class T=float> template<class T=float>
static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0); T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0);
template<class T=float> template<class T=float>
static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0); T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0);
template<class T=float> template<class T=float>
static T calcIrradiation(Inverter<> *iv, uint8_t arg0); T calcIrradiation(Inverter<> *iv, uint8_t arg0);
template<class T=float> template<class T=float>
static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0); T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0);
template<class T=float> template<class T=float>
static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0); T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0);
template<class T=float>
T calcMaxTemperature(Inverter<> *iv, uint8_t arg0);
template<class T=float> template<class T=float>
using func_t = T (Inverter<> *, uint8_t); using func_t = T (Inverter<> *, uint8_t);
@ -84,7 +87,7 @@ struct record_t {
byteAssign_t* assign = nullptr; // assignment of bytes in payload byteAssign_t* assign = nullptr; // assignment of bytes in payload
uint8_t length = 0; // length of the assignment list uint8_t length = 0; // length of the assignment list
T *record = nullptr; // data pointer T *record = nullptr; // data pointer
uint32_t ts = 0; // timestamp of last received payload uint32_t ts = 0; // Timestamp of last received payload
uint8_t pyldLen = 0; // expected payload length for plausibility check uint8_t pyldLen = 0; // expected payload length for plausibility check
MqttSentStatus mqttSentStatus = MqttSentStatus:: NEW_DATA; // indicates the current MqTT sent status MqttSentStatus mqttSentStatus = MqttSentStatus:: NEW_DATA; // indicates the current MqTT sent status
}; };
@ -100,14 +103,15 @@ struct alarm_t {
// list of all available functions, mapped in hmDefines.h // list of all available functions, mapped in hmDefines.h
template<class T=float> template<class T=float>
const calcFunc_t<T> calcFunctions[] = { const calcFunc_t<T> calcFunctions[] = {
{ CALC_YT_CH0, &calcYieldTotalCh0 }, { CALC_YT_CH0, &calcYieldTotalCh0 },
{ CALC_YD_CH0, &calcYieldDayCh0 }, { CALC_YD_CH0, &calcYieldDayCh0 },
{ CALC_UDC_CH, &calcUdcCh }, { CALC_UDC_CH, &calcUdcCh },
{ CALC_PDC_CH0, &calcPowerDcCh0 }, { CALC_PDC_CH0, &calcPowerDcCh0 },
{ CALC_EFF_CH0, &calcEffiencyCh0 }, { CALC_EFF_CH0, &calcEffiencyCh0 },
{ CALC_IRR_CH, &calcIrradiation }, { CALC_IRR_CH, &calcIrradiation },
{ CALC_MPAC_CH0, &calcMaxPowerAcCh0 }, { CALC_MPAC_CH0, &calcMaxPowerAcCh0 },
{ CALC_MPDC_CH, &calcMaxPowerDc } { CALC_MPDC_CH, &calcMaxPowerDc },
{ CALC_MT_CH0, &calcMaxTemperature }
}; };
template <class REC_TYP> template <class REC_TYP>
@ -146,7 +150,8 @@ class Inverter {
statistics_t radioStatistics; // information about transmitted, failed, ... packets statistics_t radioStatistics; // information about transmitted, failed, ... packets
HeuristicInv heuristics; // heuristic information / logic HeuristicInv heuristics; // heuristic information / logic
uint8_t curCmtFreq = 0; // current used CMT frequency, used to check if freq. was changed during runtime uint8_t curCmtFreq = 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 uint32_t tsMaxAcPower = 0; // holds the Timestamp when the MaxAC power was seen
uint32_t tsMaxTemperature = 0; // holds the Timestamp when the max temperature was seen
bool commEnabled = true; // 'pause night communication' sets this field to false bool commEnabled = true; // 'pause night communication' sets this field to false
public: public:
@ -189,7 +194,7 @@ class Inverter {
cb(InverterDevInform_Simple, false); // get hardware version cb(InverterDevInform_Simple, false); // get hardware version
} else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) { } else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) {
cb(AlarmData, false); // get last alarms cb(AlarmData, false); // get last alarms
} else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile } else if((0 == mGridLen) && GeneralConfig->readGrid) { // read grid profile
cb(GridOnProFilePara, false); cb(GridOnProFilePara, false);
} else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate
mGetLossInterval = 1; mGetLossInterval = 1;
@ -213,7 +218,7 @@ class Inverter {
if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) { 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(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
mIvRxCnt +=2; mIvRxCnt +=2;
} else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile } else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && GeneralConfig->readGrid) // read grid profile
cb(0x10, false); // legacy GPF command cb(0x10, false); // legacy GPF command
} }
} }
@ -229,19 +234,20 @@ class Inverter {
initAssignment(&recordAlarm, AlarmData); initAssignment(&recordAlarm, AlarmData);
toRadioId(); toRadioId();
curCmtFreq = this->config->frequency; // update to frequency read from settings curCmtFreq = this->config->frequency; // update to frequency read from settings
resetAlarms(true);
} }
uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) { uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld"));
if(NULL != rec) { if(nullptr == 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
return 0xff; return 0xff;
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;
} }
byteAssign_t *getByteAssign(uint8_t pos, record_t<> *rec) { byteAssign_t *getByteAssign(uint8_t pos, record_t<> *rec) {
@ -270,15 +276,18 @@ class Inverter {
if(InverterStatus::OFF != status) { if(InverterStatus::OFF != status) {
mDevControlRequest = true; mDevControlRequest = true;
devControlCmd = cmd; devControlCmd = cmd;
//app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)" assert(App);
App->triggerTickSend(id);
return true;
} }
return (InverterStatus::OFF != status); return false;
} }
bool setDevCommand(uint8_t cmd) { bool setDevCommand(uint8_t cmd) {
if(InverterStatus::OFF != status) bool retval = (InverterStatus::OFF != status);
if(retval)
devControlCmd = cmd; devControlCmd = cmd;
return (InverterStatus::OFF != status); return retval;
} }
void addValue(uint8_t pos, const uint8_t buf[], record_t<> *rec) { void addValue(uint8_t pos, const uint8_t buf[], record_t<> *rec) {
@ -288,32 +297,30 @@ class Inverter {
uint8_t end = ptr + rec->assign[pos].num; uint8_t end = ptr + rec->assign[pos].num;
uint16_t div = rec->assign[pos].div; uint16_t div = rec->assign[pos].div;
if(NULL != rec) { if(CMD_CALC != div) {
if(CMD_CALC != div) { uint32_t val = 0;
uint32_t val = 0; do {
do { val <<= 8;
val <<= 8; val |= buf[ptr];
val |= buf[ptr]; } while(++ptr != end);
} while(++ptr != end);
if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) {
if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) { // temperature, Qvar, and power factor are a signed values
// temperature, Qvar, and power factor are a signed values rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div);
rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div); } else if (FLD_YT == rec->assign[pos].fieldId) {
} else if (FLD_YT == rec->assign[pos].fieldId) { rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]);
rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); } else if (FLD_YD == rec->assign[pos].fieldId) {
} else if (FLD_YD == rec->assign[pos].fieldId) { float actYD = (REC_TYP)(val) / (REC_TYP)(div);
float actYD = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency; uint8_t idx = rec->assign[pos].ch - 1;
uint8_t idx = rec->assign[pos].ch - 1; if (mLastYD[idx] > actYD)
if (mLastYD[idx] > actYD) mOffYD[idx] += mLastYD[idx];
mOffYD[idx] += mLastYD[idx]; mLastYD[idx] = actYD;
mLastYD[idx] = actYD; rec->record[pos] = mOffYD[idx] + actYD;
rec->record[pos] = mOffYD[idx] + actYD; } else {
} else { if ((REC_TYP)(div) > 1)
if ((REC_TYP)(div) > 1) rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); else
else rec->record[pos] = (REC_TYP)(val);
rec->record[pos] = (REC_TYP)(val);
}
} }
} }
@ -337,7 +344,7 @@ class Inverter {
// eg. hw version ... // eg. hw version ...
} else if (rec->assign == SystemConfigParaAssignment) { } else if (rec->assign == SystemConfigParaAssignment) {
DPRINTLN(DBG_DEBUG, "add config"); DPRINTLN(DBG_DEBUG, "add config");
if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos) {
actPowerLimit = rec->record[pos]; actPowerLimit = rec->record[pos];
DPRINT(DBG_DEBUG, F("Inverter actual power limit: ")); DPRINT(DBG_DEBUG, F("Inverter actual power limit: "));
DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1)); DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1));
@ -356,7 +363,7 @@ class Inverter {
bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) { bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue"));
if(NULL == rec) if(nullptr == rec)
return false; return false;
if(pos > rec->length) if(pos > rec->length)
return false; return false;
@ -407,23 +414,17 @@ class Inverter {
bool isAvailable() { bool isAvailable() {
bool avail = false; bool avail = false;
if((recordMeas.ts == 0) && (recordInfo.ts == 0) && (recordConfig.ts == 0) && (recordAlarm.ts == 0)) if(recordMeas.ts == 0)
return false; return false;
if((*timestamp - recordMeas.ts) < INVERTER_INACT_THRES_SEC) if(((*Timestamp) - recordMeas.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if((*timestamp - recordConfig.ts) < INVERTER_INACT_THRES_SEC)
avail = true;
if((*timestamp - recordAlarm.ts) < INVERTER_INACT_THRES_SEC)
avail = true; avail = true;
if(avail) { if(avail) {
if(status < InverterStatus::PRODUCING) if(status < InverterStatus::PRODUCING)
status = InverterStatus::STARTING; status = InverterStatus::STARTING;
} else { } else {
if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) { if(((*Timestamp) - recordMeas.ts) > INVERTER_OFF_THRES_SEC) {
if(status != InverterStatus::OFF) { if(status != InverterStatus::OFF) {
status = InverterStatus::OFF; status = InverterStatus::OFF;
actPowerLimit = 0xffff; // power limit will be read once inverter becomes available actPowerLimit = 0xffff; // power limit will be read once inverter becomes available
@ -537,6 +538,10 @@ class Inverter {
rec->length = (uint8_t)(HMS4CH_LIST_LEN); rec->length = (uint8_t)(HMS4CH_LIST_LEN);
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hms4chAssignment)); rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hms4chAssignment));
rec->pyldLen = HMS4CH_PAYLOAD_LEN; rec->pyldLen = HMS4CH_PAYLOAD_LEN;
} else if(IV_HMT == ivGen){
rec->length = (uint8_t)(HMT4CH_LIST_LEN);
rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hmt4chAssignment));
rec->pyldLen = HMT4CH_PAYLOAD_LEN;
} }
channels = 4; channels = 4;
} }
@ -584,7 +589,7 @@ class Inverter {
} }
} }
void resetAlarms() { void resetAlarms(bool clearTs = false) {
lastAlarm.fill({0, 0, 0}); lastAlarm.fill({0, 0, 0});
mAlarmNxtWrPos = 0; mAlarmNxtWrPos = 0;
alarmCnt = 0; alarmCnt = 0;
@ -592,6 +597,11 @@ class Inverter {
memset(mOffYD, 0, sizeof(float) * 6); memset(mOffYD, 0, sizeof(float) * 6);
memset(mLastYD, 0, sizeof(float) * 6); memset(mLastYD, 0, sizeof(float) * 6);
if(clearTs) {
tsMaxAcPower = *Timestamp;
tsMaxTemperature = *Timestamp;
}
} }
bool parseGetLossRate(const uint8_t pyld[], uint8_t len) { bool parseGetLossRate(const uint8_t pyld[], uint8_t len) {
@ -660,7 +670,6 @@ class Inverter {
DPRINTLN(DBG_DEBUG, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(start) + ", end: " + ah::getTimeStr(endTime)); DPRINTLN(DBG_DEBUG, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(start) + ", end: " + ah::getTimeStr(endTime));
addAlarm(pyld[startOff+1], start, endTime); addAlarm(pyld[startOff+1], start, endTime);
alarmCnt++;
alarmLastId = alarmMesIndex; alarmLastId = alarmMesIndex;
return pyld[startOff+1]; return pyld[startOff+1];
@ -808,6 +817,26 @@ class Inverter {
private: private:
inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) { inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) {
uint8_t i = 0;
if(start > end)
end = 0;
for(; i < 10; i++) {
++mAlarmNxtWrPos;
mAlarmNxtWrPos = mAlarmNxtWrPos % 10;
if(lastAlarm[mAlarmNxtWrPos].code == code && lastAlarm[mAlarmNxtWrPos].start == start) {
// replace with same or update end time
if(lastAlarm[mAlarmNxtWrPos].end == 0 || lastAlarm[mAlarmNxtWrPos].end == end) {
break;
}
}
}
if(alarmCnt < 10 && alarmCnt <= mAlarmNxtWrPos)
alarmCnt = mAlarmNxtWrPos + 1;
lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end); lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end);
if(++mAlarmNxtWrPos >= 10) // rolling buffer if(++mAlarmNxtWrPos >= 10) // rolling buffer
mAlarmNxtWrPos = 0; mAlarmNxtWrPos = 0;
@ -824,8 +853,9 @@ class Inverter {
} }
public: public:
static uint32_t *timestamp; // system timestamp static uint32_t *Timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup static cfgInst_t *GeneralConfig; // general inverter configuration from setup
static IApp *App;
uint16_t mDtuRxCnt = 0; uint16_t mDtuRxCnt = 0;
uint16_t mDtuTxCnt = 0; uint16_t mDtuTxCnt = 0;
@ -843,9 +873,11 @@ class Inverter {
}; };
template <class REC_TYP> template <class REC_TYP>
uint32_t *Inverter<REC_TYP>::timestamp {0}; uint32_t *Inverter<REC_TYP>::Timestamp {0};
template <class REC_TYP> template <class REC_TYP>
cfgInst_t *Inverter<REC_TYP>::generalConfig {0}; cfgInst_t *Inverter<REC_TYP>::GeneralConfig {0};
template <class REC_TYP>
IApp *Inverter<REC_TYP>::App {nullptr};
/** /**
@ -855,7 +887,7 @@ cfgInst_t *Inverter<REC_TYP>::generalConfig {0};
*/ */
template<class T=float> template<class T=float>
static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) { T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldTotalCh0")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldTotalCh0"));
if(NULL != iv) { if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@ -869,7 +901,7 @@ static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) {
} }
template<class T=float> template<class T=float>
static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) { T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldDayCh0")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldDayCh0"));
if(NULL != iv) { if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@ -883,7 +915,7 @@ static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) {
} }
template<class T=float> template<class T=float>
static T calcUdcCh(Inverter<> *iv, uint8_t arg0) { T calcUdcCh(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcUdcCh")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcUdcCh"));
// arg0 = channel of source // arg0 = channel of source
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@ -897,7 +929,7 @@ static T calcUdcCh(Inverter<> *iv, uint8_t arg0) {
} }
template<class T=float> template<class T=float>
static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) { T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcPowerDcCh0")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcPowerDcCh0"));
if(NULL != iv) { if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@ -911,7 +943,7 @@ static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) {
} }
template<class T=float> template<class T=float>
static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) { T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcEfficiencyCh0")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcEfficiencyCh0"));
if(NULL != iv) { if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
@ -927,7 +959,7 @@ static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) {
} }
template<class T=float> template<class T=float>
static T calcIrradiation(Inverter<> *iv, uint8_t arg0) { T calcIrradiation(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcIrradiation")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcIrradiation"));
// arg0 = channel // arg0 = channel
if(NULL != iv) { if(NULL != iv) {
@ -939,7 +971,7 @@ static T calcIrradiation(Inverter<> *iv, uint8_t arg0) {
} }
template<class T=float> template<class T=float>
static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) { T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerAcCh0")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerAcCh0"));
T acMaxPower = 0.0; T acMaxPower = 0.0;
if(NULL != iv) { if(NULL != iv) {
@ -952,7 +984,7 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
} }
} }
if(acPower > acMaxPower) { if(acPower > acMaxPower) {
iv->tsMaxAcPower = *iv->timestamp; iv->tsMaxAcPower = *iv->Timestamp;
return acPower; return acPower;
} }
} }
@ -960,7 +992,7 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
} }
template<class T=float> template<class T=float>
static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) { T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerDc")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerDc"));
// arg0 = channel // arg0 = channel
T dcMaxPower = 0.0; T dcMaxPower = 0.0;
@ -979,4 +1011,22 @@ static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) {
return dcMaxPower; return dcMaxPower;
} }
template<class T=float>
T calcMaxTemperature(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxTemperature"));
// arg0 = channel
if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
T temp = iv->getChannelFieldValue(arg0, FLD_T, rec);
T maxTemp = iv->getChannelFieldValue(arg0, FLD_MT, rec);
if(temp > maxTemp) {
iv->tsMaxTemperature = *iv->Timestamp;
return temp;
}
return maxTemp;
}
return 0;
}
#endif /*__HM_INVERTER_H__*/ #endif /*__HM_INVERTER_H__*/

27
src/hm/hmSystem.h

@ -16,8 +16,9 @@ class HmSystem {
HmSystem() {} HmSystem() {}
void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) { void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) {
mInverter[0].timestamp = timestamp; INVERTERTYPE::Timestamp = timestamp;
mInverter[0].generalConfig = config; INVERTERTYPE::GeneralConfig = config;
INVERTERTYPE::App = app;
//mInverter[0].app = app; //mInverter[0].app = app;
} }
@ -25,7 +26,7 @@ class HmSystem {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter")); DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter"));
INVERTERTYPE *iv = &mInverter[id]; INVERTERTYPE *iv = &mInverter[id];
iv->id = id; iv->id = id;
iv->config = &mInverter[0].generalConfig->iv[id]; iv->config = &INVERTERTYPE::GeneralConfig->iv[id];
DPRINT(DBG_VERBOSE, "SERIAL: " + String(iv->config->serial.b[5], HEX)); DPRINT(DBG_VERBOSE, "SERIAL: " + String(iv->config->serial.b[5], HEX));
DPRINTLN(DBG_VERBOSE, " " + String(iv->config->serial.b[4], HEX)); DPRINTLN(DBG_VERBOSE, " " + String(iv->config->serial.b[4], HEX));
if((iv->config->serial.b[5] == 0x11) || (iv->config->serial.b[5] == 0x10)) { if((iv->config->serial.b[5] == 0x11) || (iv->config->serial.b[5] == 0x10)) {
@ -35,6 +36,8 @@ class HmSystem {
case 0x21: iv->type = INV_TYPE_1CH; case 0x21: iv->type = INV_TYPE_1CH;
break; break;
case 0x25: // HMS-400 - 1 channel but payload like 2ch
case 0x44: // HMS-1000 case 0x44: // HMS-1000
case 0x42: case 0x42:
case 0x41: iv->type = INV_TYPE_2CH; case 0x41: iv->type = INV_TYPE_2CH;
@ -51,15 +54,14 @@ class HmSystem {
} }
if(iv->config->serial.b[5] == 0x11) { if(iv->config->serial.b[5] == 0x11) {
if((iv->config->serial.b[4] & 0x0f) == 0x04) { if(((iv->config->serial.b[4] & 0x0f) == 0x04) || ((iv->config->serial.b[4] & 0x0f) == 0x05)) {
iv->ivGen = IV_HMS; iv->ivGen = IV_HMS;
iv->ivRadioType = INV_RADIO_TYPE_CMT; iv->ivRadioType = INV_RADIO_TYPE_CMT;
} else { } else {
iv->ivGen = IV_HM; iv->ivGen = IV_HM;
iv->ivRadioType = INV_RADIO_TYPE_NRF; iv->ivRadioType = INV_RADIO_TYPE_NRF;
} }
} } else if((iv->config->serial.b[4] & 0x03) == 0x02) { // MI 3rd Gen -> same as HM
else if((iv->config->serial.b[4] & 0x03) == 0x02) { // MI 3rd Gen -> same as HM
iv->ivGen = IV_HM; iv->ivGen = IV_HM;
iv->ivRadioType = INV_RADIO_TYPE_NRF; iv->ivRadioType = INV_RADIO_TYPE_NRF;
} else { // MI 2nd Gen } else { // MI 2nd Gen
@ -67,11 +69,16 @@ class HmSystem {
iv->ivRadioType = INV_RADIO_TYPE_NRF; iv->ivRadioType = INV_RADIO_TYPE_NRF;
} }
} else if(iv->config->serial.b[5] == 0x13) { } else if(iv->config->serial.b[5] == 0x13) {
iv->ivGen = IV_HMT; iv->ivGen = IV_HMT;
if(iv->config->serial.b[4] == 0x61)
iv->type = INV_TYPE_4CH;
else
iv->type = INV_TYPE_6CH; iv->type = INV_TYPE_6CH;
iv->ivRadioType = INV_RADIO_TYPE_CMT;
iv->ivRadioType = INV_RADIO_TYPE_CMT;
} else if(iv->config->serial.u64 != 0ULL) { } else if(iv->config->serial.u64 != 0ULL) {
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); DPRINTLN(DBG_ERROR, F("inverter type can't be detected!"));
iv->config->enabled = false;
return; return;
} else } else
iv->ivGen = IV_UNKNOWN; iv->ivGen = IV_UNKNOWN;
@ -82,7 +89,7 @@ class HmSystem {
DPRINT(DBG_INFO, "added inverter "); DPRINT(DBG_INFO, "added inverter ");
if(iv->config->serial.b[5] == 0x11) { if(iv->config->serial.b[5] == 0x11) {
if((iv->config->serial.b[4] & 0x0f) == 0x04) if(((iv->config->serial.b[4] & 0x0f) == 0x04) || ((iv->config->serial.b[4] & 0x0f) == 0x05))
DBGPRINT("HMS"); DBGPRINT("HMS");
else else
DBGPRINT("HM"); DBGPRINT("HM");
@ -114,6 +121,8 @@ class HmSystem {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos"));
if(pos >= MAX_INVERTER) if(pos >= MAX_INVERTER)
return nullptr; return nullptr;
else if(nullptr == mInverter[pos].config)
return nullptr;
else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check)) else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check))
return &mInverter[pos]; return &mInverter[pos];
else else

20
src/hm/nrfHal.h

@ -9,18 +9,15 @@
#pragma once #pragma once
#include "../utils/spiPatcher.h" #include "../utils/spiPatcher.h"
#include <esp_rom_gpio.h> #include <esp_rom_gpio.h>
#include <RF24_hal.h> #include <RF24.h>
#define NRF_MAX_TRANSFER_SZ 64 #define NRF_MAX_TRANSFER_SZ 64
#define NRF_DEFAULT_SPI_SPEED 10000000 // 10 MHz #define NRF_DEFAULT_SPI_SPEED 10000000 // 10 MHz
class nrfHal: public RF24_hal, public SpiPatcherHandle { class nrfHal: public RF24_hal, public SpiPatcherHandle {
public: public:
nrfHal() { nrfHal() {}
mSpiPatcher = SpiPatcher::getInstance(SPI2_HOST);
}
void patch() override { void patch() override {
esp_rom_gpio_connect_out_signal(mPinMosi, spi_periph_signal[mHostDevice].spid_out, false, false); esp_rom_gpio_connect_out_signal(mPinMosi, spi_periph_signal[mHostDevice].spid_out, false, false);
@ -42,7 +39,13 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
mPinEn = static_cast<gpio_num_t>(en); mPinEn = static_cast<gpio_num_t>(en);
mSpiSpeed = speed; mSpiSpeed = speed;
mHostDevice = mSpiPatcher->getDevice(); #if defined(CONFIG_IDF_TARGET_ESP32S3)
mHostDevice = SPI2_HOST;
#else
mHostDevice = (14 == sclk) ? SPI2_HOST : SPI_HOST_OTHER;
#endif
mSpiPatcher = SpiPatcher::getInstance(mHostDevice);
gpio_reset_pin(mPinMosi); gpio_reset_pin(mPinMosi);
gpio_set_direction(mPinMosi, GPIO_MODE_OUTPUT); gpio_set_direction(mPinMosi, GPIO_MODE_OUTPUT);
@ -56,6 +59,7 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
gpio_set_level(mPinClk, 0); gpio_set_level(mPinClk, 0);
gpio_reset_pin(mPinCs); gpio_reset_pin(mPinCs);
request_spi();
spi_device_interface_config_t devcfg = { spi_device_interface_config_t devcfg = {
.command_bits = 0, .command_bits = 0,
.address_bits = 0, .address_bits = 0,
@ -72,14 +76,14 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
.pre_cb = nullptr, .pre_cb = nullptr,
.post_cb = nullptr .post_cb = nullptr
}; };
ESP_ERROR_CHECK(spi_bus_add_device(mHostDevice, &devcfg, &spi)); mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi);
release_spi();
gpio_reset_pin(mPinEn); gpio_reset_pin(mPinEn);
gpio_set_direction(mPinEn, GPIO_MODE_OUTPUT); gpio_set_direction(mPinEn, GPIO_MODE_OUTPUT);
gpio_set_level(mPinEn, 0); gpio_set_level(mPinEn, 0);
} }
bool begin() override { bool begin() override {
return true; return true;
} }

34
src/hms/hmsRadio.h → src/hms/CmtRadio.h

@ -7,7 +7,7 @@
#define __HMS_RADIO_H__ #define __HMS_RADIO_H__
#include "cmt2300a.h" #include "cmt2300a.h"
#include "../hm/radio.h" #include "../hm/Radio.h"
//#define CMT_SWITCH_CHANNEL_CYCLE 5 //#define CMT_SWITCH_CHANNEL_CYCLE 5
@ -15,25 +15,34 @@ template<uint32_t DTU_SN = 0x81001765>
class CmtRadio : public Radio { class CmtRadio : public Radio {
typedef Cmt2300a CmtType; typedef Cmt2300a CmtType;
public: public:
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) { void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, cfgCmt_t *cfg, uint8_t region = 0, bool genDtuSn = true) {
mCmt.setup(pinSclk, pinSdio, pinCsb, pinFcsb); mCfg = cfg;
reset(genDtuSn, static_cast<RegionCfg>(region));
if(!cfg->enabled)
return;
mPrivacyMode = privacyMode; mPrivacyMode = privacyMode;
mSerialDebug = serialDebug; mSerialDebug = serialDebug;
mPrintWholeTrace = printWholeTrace; mPrintWholeTrace = printWholeTrace;
mTxBuf.fill(0); mTxBuf.fill(0);
mCmt.setup(cfg->pinSclk, cfg->pinSdio, cfg->pinCsb, cfg->pinFcsb);
reset(genDtuSn, static_cast<RegionCfg>(region));
} }
bool loop() override { void loop() override {
if(!mCfg->enabled)
return;
mCmt.loop(); mCmt.loop();
if((!mIrqRcvd) && (!mRqstGetRx)) if((!mIrqRcvd) && (!mRqstGetRx))
return false; return;
getRx(); getRx();
if(CmtStatus::SUCCESS == mCmt.goRx()) { if(CmtStatus::SUCCESS == mCmt.goRx()) {
mIrqRcvd = false; mIrqRcvd = false;
mRqstGetRx = false; mRqstGetRx = false;
} }
return false; return;
} }
bool isChipConnected(void) const override { bool isChipConnected(void) const override {
@ -41,6 +50,9 @@ class CmtRadio : public Radio {
} }
void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override { void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override {
if(!mCfg->enabled)
return;
DPRINT(DBG_INFO, F("sendControlPacket cmd: ")); DPRINT(DBG_INFO, F("sendControlPacket cmd: "));
DBGHEXLN(cmd); DBGHEXLN(cmd);
initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME); initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME);
@ -59,6 +71,9 @@ class CmtRadio : public Radio {
} }
bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) override { bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) override {
if(!isChipConnected())
return false;
uint8_t fromCh = mCmt.freq2Chan(fromkHz); uint8_t fromCh = mCmt.freq2Chan(fromkHz);
uint8_t toCh = mCmt.freq2Chan(tokHz); uint8_t toCh = mCmt.freq2Chan(tokHz);
@ -68,6 +83,8 @@ class CmtRadio : public Radio {
bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) override { bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) override {
if((0xff == fromCh) || (0xff == toCh)) if((0xff == fromCh) || (0xff == toCh))
return false; return false;
if(!isChipConnected())
return false;
mCmt.switchChannel(fromCh); mCmt.switchChannel(fromCh);
sendSwitchChCmd(iv, toCh); sendSwitchChCmd(iv, toCh);
@ -183,11 +200,12 @@ class CmtRadio : public Radio {
if(p.packet[9] > ALL_FRAMES) { // indicates last frame if(p.packet[9] > ALL_FRAMES) { // indicates last frame
setExpectedFrames(p.packet[9] - ALL_FRAMES); setExpectedFrames(p.packet[9] - ALL_FRAMES);
mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first get back to rx mode? mRadioWaitTime.startTimeMonitor(2); // let the inverter first get back to rx mode?
} }
} }
CmtType mCmt; CmtType mCmt;
cfgCmt_t *mCfg = nullptr;
bool mCmtAvail = false; bool mCmtAvail = false;
bool mRqstGetRx = false; bool mRqstGetRx = false;
uint32_t mMillis = 0; uint32_t mMillis = 0;

4
src/hms/cmt2300a.h

@ -6,7 +6,7 @@
#ifndef __CMT2300A_H__ #ifndef __CMT2300A_H__
#define __CMT2300A_H__ #define __CMT2300A_H__
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) #if defined(SPI_HAL)
#include "cmtHal.h" #include "cmtHal.h"
#else #else
#include "esp32_3wSpi.h" #include "esp32_3wSpi.h"
@ -545,7 +545,7 @@ class Cmt2300a {
} }
private: private:
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) #if defined(SPI_HAL)
cmtHal mSpi; cmtHal mSpi;
#else #else
esp32_3wSpi mSpi; esp32_3wSpi mSpi;

16
src/hms/cmtHal.h

@ -16,9 +16,7 @@
class cmtHal : public SpiPatcherHandle { class cmtHal : public SpiPatcherHandle {
public: public:
cmtHal() { cmtHal() {}
mSpiPatcher = SpiPatcher::getInstance(DEF_CMT_SPI_HOST);
}
void patch() override { void patch() override {
esp_rom_gpio_connect_out_signal(mPinSdio, spi_periph_signal[mHostDevice].spid_out, false, false); esp_rom_gpio_connect_out_signal(mPinSdio, spi_periph_signal[mHostDevice].spid_out, false, false);
@ -39,7 +37,13 @@ class cmtHal : public SpiPatcherHandle {
mPinFcs = static_cast<gpio_num_t>(fcs); mPinFcs = static_cast<gpio_num_t>(fcs);
mSpiSpeed = speed; mSpiSpeed = speed;
mHostDevice = mSpiPatcher->getDevice(); #if defined(CONFIG_IDF_TARGET_ESP32S3)
mHostDevice = SPI2_HOST;
#else
mHostDevice = (14 == clk) ? SPI2_HOST : SPI_HOST_OTHER;
#endif
mSpiPatcher = SpiPatcher::getInstance(mHostDevice);
gpio_reset_pin(mPinSdio); gpio_reset_pin(mPinSdio);
gpio_set_direction(mPinSdio, GPIO_MODE_INPUT_OUTPUT); gpio_set_direction(mPinSdio, GPIO_MODE_INPUT_OUTPUT);
@ -50,6 +54,7 @@ class cmtHal : public SpiPatcherHandle {
gpio_set_level(mPinClk, 0); gpio_set_level(mPinClk, 0);
gpio_reset_pin(mPinCs); gpio_reset_pin(mPinCs);
request_spi();
spi_device_interface_config_t devcfg_reg = { spi_device_interface_config_t devcfg_reg = {
.command_bits = 1, .command_bits = 1,
.address_bits = 7, .address_bits = 7,
@ -66,7 +71,8 @@ class cmtHal : public SpiPatcherHandle {
.pre_cb = nullptr, .pre_cb = nullptr,
.post_cb = nullptr .post_cb = nullptr
}; };
ESP_ERROR_CHECK(spi_bus_add_device(mHostDevice, &devcfg_reg, &spi_reg)); mSpiPatcher->addDevice(mHostDevice, &devcfg_reg, &spi_reg);
release_spi();
gpio_reset_pin(mPinFcs); gpio_reset_pin(mPinFcs);
spi_device_interface_config_t devcfg_fifo = { spi_device_interface_config_t devcfg_fifo = {

73
src/hms/hmsDefines.h

@ -33,7 +33,8 @@ const byteAssign_t hms1chAssignment[] = {
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC },
{ FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC },
{ FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC }
}; };
#define HMS1CH_LIST_LEN (sizeof(hms1chAssignment) / sizeof(byteAssign_t)) #define HMS1CH_LIST_LEN (sizeof(hms1chAssignment) / sizeof(byteAssign_t))
#define HMS1CH_PAYLOAD_LEN 30 #define HMS1CH_PAYLOAD_LEN 30
@ -70,7 +71,8 @@ const byteAssign_t hms2chAssignment[] = {
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC },
{ FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC },
{ FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC }
}; };
#define HMS2CH_LIST_LEN (sizeof(hms2chAssignment) / sizeof(byteAssign_t)) #define HMS2CH_LIST_LEN (sizeof(hms2chAssignment) / sizeof(byteAssign_t))
#define HMS2CH_PAYLOAD_LEN 42 #define HMS2CH_PAYLOAD_LEN 42
@ -123,11 +125,73 @@ const byteAssign_t hms4chAssignment[] = {
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC },
{ FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC },
{ FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC }
}; };
#define HMS4CH_LIST_LEN (sizeof(hms4chAssignment) / sizeof(byteAssign_t)) #define HMS4CH_LIST_LEN (sizeof(hms4chAssignment) / sizeof(byteAssign_t))
#define HMS4CH_PAYLOAD_LEN 66 #define HMS4CH_PAYLOAD_LEN 66
//-------------------------------------
// HMT-1600, HMT-1800, HMT-2000
//-------------------------------------
const byteAssign_t hmt4chAssignment[] = {
{ FLD_UDC, UNIT_V, CH1, 2, 2, 10 },
{ FLD_IDC, UNIT_A, CH1, 4, 2, 100 },
{ FLD_PDC, UNIT_W, CH1, 8, 2, 10 },
{ FLD_YT, UNIT_KWH, CH1, 12, 4, 1000 },
{ FLD_YD, UNIT_WH, CH1, 20, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC },
{ FLD_MP, UNIT_W, CH1, CALC_MPDC_CH, CH1, CMD_CALC },
{ FLD_UDC, UNIT_V, CH2, CALC_UDC_CH, CH1, CMD_CALC },
{ FLD_IDC, UNIT_A, CH2, 6, 2, 100 },
{ FLD_PDC, UNIT_W, CH2, 10, 2, 10 },
{ FLD_YT, UNIT_KWH, CH2, 16, 4, 1000 },
{ FLD_YD, UNIT_WH, CH2, 22, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC },
{ FLD_MP, UNIT_W, CH2, CALC_MPDC_CH, CH2, CMD_CALC },
{ FLD_UDC, UNIT_V, CH3, 24, 2, 10 },
{ FLD_IDC, UNIT_A, CH3, 26, 2, 100 },
{ FLD_PDC, UNIT_W, CH3, 30, 2, 10 },
{ FLD_YT, UNIT_KWH, CH3, 34, 4, 1000 },
{ FLD_YD, UNIT_WH, CH3, 42, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC },
{ FLD_MP, UNIT_W, CH3, CALC_MPDC_CH, CH3, CMD_CALC },
{ FLD_UDC, UNIT_V, CH4, CALC_UDC_CH, CH3, CMD_CALC },
{ FLD_IDC, UNIT_A, CH4, 28, 2, 100 },
{ FLD_PDC, UNIT_W, CH4, 32, 2, 10 },
{ FLD_YT, UNIT_KWH, CH4, 38, 4, 1000 },
{ FLD_YD, UNIT_WH, CH4, 44, 2, 1 },
{ FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC },
{ FLD_MP, UNIT_W, CH4, CALC_MPDC_CH, CH4, CMD_CALC },
{ FLD_UAC_1N, UNIT_V, CH0, 68, 2, 10 },
{ FLD_UAC_2N, UNIT_V, CH0, 70, 2, 10 },
{ FLD_UAC_3N, UNIT_V, CH0, 72, 2, 10 },
{ FLD_UAC_12, UNIT_V, CH0, 74, 2, 10 },
{ FLD_UAC_23, UNIT_V, CH0, 76, 2, 10 },
{ FLD_UAC_31, UNIT_V, CH0, 78, 2, 10 },
{ FLD_F, UNIT_HZ, CH0, 80, 2, 100 },
{ FLD_PAC, UNIT_W, CH0, 82, 2, 10 },
{ FLD_Q, UNIT_VAR, CH0, 84, 2, 10 },
{ FLD_IAC_1, UNIT_A, CH0, 86, 2, 100 },
{ FLD_IAC_2, UNIT_A, CH0, 88, 2, 100 },
{ FLD_IAC_3, UNIT_A, CH0, 90, 2, 100 },
{ FLD_PF, UNIT_NONE, CH0, 92, 2, 1000 },
{ FLD_T, UNIT_C, CH0, 94, 2, 10 },
{ FLD_EVT, UNIT_NONE, CH0, 96, 2, 1 },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC },
{ FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC },
{ FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC }
};
#define HMT4CH_LIST_LEN (sizeof(hmt4chAssignment) / sizeof(byteAssign_t))
#define HMT4CH_PAYLOAD_LEN 98
//------------------------------------- //-------------------------------------
// HMT-1800, HMT-2250 // HMT-1800, HMT-2250
//------------------------------------- //-------------------------------------
@ -199,7 +263,8 @@ const byteAssign_t hmt6chAssignment[] = {
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
{ FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC },
{ FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC },
{ FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC }
}; };
#define HMT6CH_LIST_LEN (sizeof(hmt6chAssignment) / sizeof(byteAssign_t)) #define HMT6CH_LIST_LEN (sizeof(hmt6chAssignment) / sizeof(byteAssign_t))
#define HMT6CH_PAYLOAD_LEN 98 #define HMT6CH_PAYLOAD_LEN 98

151
src/network/AhoyEthernet.h

@ -0,0 +1,151 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __AHOY_ETHERNET_H__
#define __AHOY_ETHERNET_H__
#if defined(ETHERNET)
#include <functional>
#include <AsyncUDP.h>
#include <ETH.h>
#include "AhoyEthernetSpi.h"
#include "AhoyNetwork.h"
#include "AhoyWifiEsp32.h"
class AhoyEthernet : public AhoyWifi {
private:
enum class Mode {
WIRED,
WIRELESS
};
public:
AhoyEthernet()
: mMode (Mode::WIRELESS) {}
virtual void begin() override {
mMode = Mode::WIRELESS;
mAp.enable();
AhoyWifi::begin();
if(!mConfig->sys.eth.enabled)
return;
mEthSpi.begin(mConfig->sys.eth.pinMiso, mConfig->sys.eth.pinMosi, mConfig->sys.eth.pinSclk, mConfig->sys.eth.pinCs, mConfig->sys.eth.pinIrq, mConfig->sys.eth.pinRst);
ETH.setHostname(mConfig->sys.deviceName);
}
virtual String getIp(void) override {
if(Mode::WIRELESS == mMode)
return AhoyWifi::getIp();
else
return ETH.localIP().toString();
}
virtual String getMac(void) override {
if(Mode::WIRELESS == mMode)
return AhoyWifi::getMac();
else
return mEthSpi.macAddress();
}
virtual bool isWiredConnection() override {
return (Mode::WIRED == mMode);
}
private:
virtual void OnEvent(WiFiEvent_t event) override {
switch(event) {
case ARDUINO_EVENT_ETH_CONNECTED:
mMode = Mode::WIRED; // needed for static IP
[[fallthrough]];
case SYSTEM_EVENT_STA_CONNECTED:
mWifiConnecting = false;
if(NetworkState::CONNECTED != mStatus) {
if(ARDUINO_EVENT_ETH_CONNECTED == event)
WiFi.disconnect();
mStatus = NetworkState::CONNECTED;
DPRINTLN(DBG_INFO, F("Network connected"));
setStaticIp();
}
break;
case SYSTEM_EVENT_STA_GOT_IP:
mStatus = NetworkState::GOT_IP;
if(mAp.isEnabled())
mAp.disable();
mMode = Mode::WIRELESS;
if(!mConnected) {
mConnected = true;
ah::welcome(WiFi.localIP().toString(), F("Station WiFi"));
MDNS.begin(mConfig->sys.deviceName);
mOnNetworkCB(true);
}
break;
case ARDUINO_EVENT_ETH_GOT_IP:
mStatus = NetworkState::GOT_IP;
mMode = Mode::WIRED;
if(!mConnected) {
mAp.disable();
mConnected = true;
ah::welcome(ETH.localIP().toString(), F("Station Ethernet"));
MDNS.begin(mConfig->sys.deviceName);
mOnNetworkCB(true);
WiFi.disconnect();
}
break;
case ARDUINO_EVENT_ETH_STOP:
[[fallthrough]];
case ARDUINO_EVENT_ETH_DISCONNECTED:
mStatus = NetworkState::DISCONNECTED;
if(mConnected) {
mMode = Mode::WIRELESS;
mConnected = false;
mOnNetworkCB(false);
MDNS.end();
AhoyWifi::begin();
}
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
[[fallthrough]];
case ARDUINO_EVENT_WIFI_STA_STOP:
[[fallthrough]];
case SYSTEM_EVENT_STA_DISCONNECTED:
mStatus = NetworkState::DISCONNECTED;
if(mConnected && (Mode::WIRELESS == mMode)) {
mConnected = false;
mOnNetworkCB(false);
MDNS.end();
AhoyWifi::begin();
}
break;
default:
break;
}
}
void setStaticIp() override {
setupIp([this](IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2) -> bool {
if(Mode::WIRELESS == mMode)
return WiFi.config(ip, gateway, mask, dns1, dns2);
else
return ETH.config(ip, gateway, mask, dns1, dns2);
});
}
private:
AhoyEthernetSpi mEthSpi;
Mode mMode;
};
#endif /*ETHERNET*/
#endif /*__AHOY_ETHERNET_H__*/

67
src/eth/ethSpi.h → src/network/AhoyEthernetSpi.h

@ -1,10 +1,8 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // 2024 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#if defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(ETHERNET) #if defined(ETHERNET)
#ifndef __ETH_SPI_H__ #ifndef __ETH_SPI_H__
#define __ETH_SPI_H__ #define __ETH_SPI_H__
@ -14,23 +12,25 @@
#include <Arduino.h> #include <Arduino.h>
#include <esp_netif.h> #include <esp_netif.h>
#include <WiFiGeneric.h> #include <WiFiGeneric.h>
#include <driver/spi_master.h> #include "../utils/spiPatcher.h"
// Functions from WiFiGeneric // Functions from WiFiGeneric
void tcpipInit(); void tcpipInit();
void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif); void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif);
class EthSpi { class AhoyEthernetSpi {
public: public:
EthSpi() : AhoyEthernetSpi() :
eth_handle(nullptr), eth_handle(nullptr),
eth_netif(nullptr) {} eth_netif(nullptr) {}
void begin(int8_t pin_miso, int8_t pin_mosi, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) { void begin(int8_t pin_miso, int8_t pin_mosi, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) {
gpio_reset_pin(static_cast<gpio_num_t>(pin_rst)); if(-1 != pin_rst) {
gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT); gpio_reset_pin(static_cast<gpio_num_t>(pin_rst));
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0); gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0);
}
gpio_reset_pin(static_cast<gpio_num_t>(pin_sclk)); gpio_reset_pin(static_cast<gpio_num_t>(pin_sclk));
gpio_reset_pin(static_cast<gpio_num_t>(pin_mosi)); gpio_reset_pin(static_cast<gpio_num_t>(pin_mosi));
@ -44,22 +44,14 @@ class EthSpi {
gpio_reset_pin(static_cast<gpio_num_t>(pin_int)); gpio_reset_pin(static_cast<gpio_num_t>(pin_int));
gpio_set_pull_mode(static_cast<gpio_num_t>(pin_int), GPIO_PULLUP_ONLY); gpio_set_pull_mode(static_cast<gpio_num_t>(pin_int), GPIO_PULLUP_ONLY);
spi_bus_config_t buscfg = { #if defined(CONFIG_IDF_TARGET_ESP32S3)
.mosi_io_num = pin_mosi, mHostDevice = SPI3_HOST;
.miso_io_num = pin_miso, #else
.sclk_io_num = pin_sclk, mHostDevice = (14 == pin_sclk) ? SPI2_HOST : SPI3_HOST;
.quadwp_io_num = -1, #endif
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0, // uses default value internally
.flags = 0,
.intr_flags = 0
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); mSpiPatcher = SpiPatcher::getInstance(mHostDevice, false);
mSpiPatcher->initBus(pin_mosi, pin_miso, pin_sclk, SPI_DMA_CH_AUTO);
spi_device_interface_config_t devcfg = { spi_device_interface_config_t devcfg = {
.command_bits = 16, // actually address phase .command_bits = 16, // actually address phase
@ -78,13 +70,14 @@ class EthSpi {
.post_cb = nullptr .post_cb = nullptr
}; };
spi_device_handle_t spi; mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi);
ESP_ERROR_CHECK(spi_bus_add_device(SPI3_HOST, &devcfg, &spi));
// Reset sequence // Reset sequence
delayMicroseconds(500); if(-1 != pin_rst) {
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1); delayMicroseconds(500);
delayMicroseconds(1000); gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
delayMicroseconds(1000);
}
// Arduino function to start networking stack if not already started // Arduino function to start networking stack if not already started
tcpipInit(); tcpipInit();
@ -123,10 +116,14 @@ class EthSpi {
} }
String macAddress() { String macAddress() {
uint8_t mac_addr[6] = {0, 0, 0, 0, 0, 0}; uint8_t mac_addr[6];
esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr); esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr);
char mac_addr_str[24]; char mac_addr_str[19];
snprintf(mac_addr_str, sizeof(mac_addr_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); for(uint8_t i = 0; i < 6; i++) {
snprintf(&mac_addr_str[i*3], sizeof(mac_addr_str), "%02X", mac_addr[i]);
mac_addr_str[i*3+2] = ':';
}
mac_addr_str[17] = '\0';
return String(mac_addr_str); return String(mac_addr_str);
} }
@ -134,8 +131,10 @@ class EthSpi {
private: private:
esp_eth_handle_t eth_handle; esp_eth_handle_t eth_handle;
esp_netif_t *eth_netif; esp_netif_t *eth_netif;
spi_host_device_t mHostDevice;
spi_device_handle_t spi;
SpiPatcher *mSpiPatcher;
}; };
#endif /*__ETH_SPI_H__*/ #endif /*__ETH_SPI_H__*/
#endif /*ETHERNET*/ #endif /*ETHERNET*/
#endif /*CONFIG_IDF_TARGET_ESP32S3*/

254
src/network/AhoyNetwork.h

@ -0,0 +1,254 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __AHOY_NETWORK_H__
#define __AHOY_NETWORK_H__
#include "AhoyNetworkHelper.h"
#include "../config/settings.h"
#include "../utils/helper.h"
#include "AhoyWifiAp.h"
#include "AsyncJson.h"
#define NTP_PACKET_SIZE 48
class AhoyNetwork {
public:
typedef std::function<void(bool)> OnNetworkCB;
typedef std::function<void(bool)> OnTimeCB;
public:
void setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNetworkCB, OnTimeCB onTimeCB) {
mConfig = config;
mUtcTimestamp = utcTimestamp;
mOnNetworkCB = onNetworkCB;
mOnTimeCB = onTimeCB;
if('\0' == mConfig->sys.deviceName[0])
snprintf(mConfig->sys.deviceName, DEVNAME_LEN, "%s", DEF_DEVICE_NAME);
mAp.setup(&mConfig->sys);
#if defined(ESP32)
WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void {
OnEvent(event);
});
#else
wifiConnectHandler = WiFi.onStationModeConnected(
[this](const WiFiEventStationModeConnected& event) -> void {
OnEvent((WiFiEvent_t)SYSTEM_EVENT_STA_CONNECTED);
});
wifiGotIPHandler = WiFi.onStationModeGotIP(
[this](const WiFiEventStationModeGotIP& event) -> void {
OnEvent((WiFiEvent_t)SYSTEM_EVENT_STA_GOT_IP);
});
wifiDisconnectHandler = WiFi.onStationModeDisconnected(
[this](const WiFiEventStationModeDisconnected& event) -> void {
OnEvent((WiFiEvent_t)SYSTEM_EVENT_STA_DISCONNECTED);
});
#endif
}
bool isConnected() const {
return (mStatus == NetworkState::CONNECTED);
}
bool updateNtpTime(void) {
if(NetworkState::GOT_IP != mStatus)
return false;
if (!mUdp.connected()) {
IPAddress timeServer;
if (!WiFi.hostByName(mConfig->ntp.addr, timeServer))
return false;
if (!mUdp.connect(timeServer, mConfig->ntp.port))
return false;
}
mUdp.onPacket([this](AsyncUDPPacket packet) {
this->handleNTPPacket(packet);
});
sendNTPpacket();
return true;
}
public:
virtual void begin() = 0;
virtual void tickNetworkLoop() = 0;
virtual String getIp(void) = 0;
virtual String getMac(void) = 0;
virtual bool getWasInCh12to14() {
return false;
}
virtual bool isWiredConnection() {
return false;
}
bool isApActive() {
return mAp.isEnabled();
}
bool getAvailNetworks(JsonObject obj, IApp *app) {
if(!mScanActive) {
app->addOnce([this]() {scan();}, 1, "scan");
return false;
}
int n = WiFi.scanComplete();
if (WIFI_SCAN_RUNNING == n)
return false;
if(n > 0) {
JsonArray nets = obj.createNestedArray(F("networks"));
int sort[n];
sortRSSI(&sort[0], n);
for (int i = 0; i < n; ++i) {
nets[i][F("ssid")] = WiFi.SSID(sort[i]);
nets[i][F("rssi")] = WiFi.RSSI(sort[i]);
}
}
mScanActive = false;
WiFi.scanDelete();
return true;
}
void scan(void) {
mScanActive = true;
if(mWifiConnecting) {
mWifiConnecting = false;
WiFi.disconnect();
}
WiFi.scanNetworks(true, true);
}
protected:
virtual void setStaticIp() = 0;
void setupIp(std::function<bool(IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2)> cb) {
if(mConfig->sys.ip.ip[0] != 0) {
IPAddress ip(mConfig->sys.ip.ip);
IPAddress mask(mConfig->sys.ip.mask);
IPAddress dns1(mConfig->sys.ip.dns1);
IPAddress dns2(mConfig->sys.ip.dns2);
IPAddress gateway(mConfig->sys.ip.gateway);
if(cb(ip, gateway, mask, dns1, dns2))
DPRINTLN(DBG_ERROR, F("failed to set static IP!"));
}
}
virtual void OnEvent(WiFiEvent_t event) {
switch(event) {
case SYSTEM_EVENT_STA_CONNECTED:
[[fallthrough]];
case ARDUINO_EVENT_ETH_CONNECTED:
if(NetworkState::CONNECTED != mStatus) {
mStatus = NetworkState::CONNECTED;
DPRINTLN(DBG_INFO, F("Network connected"));
}
break;
case SYSTEM_EVENT_STA_GOT_IP:
[[fallthrough]];
case ARDUINO_EVENT_ETH_GOT_IP:
mStatus = NetworkState::GOT_IP;
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
[[fallthrough]];
case ARDUINO_EVENT_WIFI_STA_STOP:
[[fallthrough]];
case SYSTEM_EVENT_STA_DISCONNECTED:
[[fallthrough]];
case ARDUINO_EVENT_ETH_STOP:
[[fallthrough]];
case ARDUINO_EVENT_ETH_DISCONNECTED:
mStatus = NetworkState::DISCONNECTED;
break;
default:
break;
}
}
void sortRSSI(int *sort, int n) {
for (int i = 0; i < n; i++)
sort[i] = i;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i]))
std::swap(sort[i], sort[j]);
}
private:
void sendNTPpacket(void) {
uint8_t buf[NTP_PACKET_SIZE];
memset(buf, 0, NTP_PACKET_SIZE);
buf[0] = 0b11100011; // LI, Version, Mode
buf[1] = 0; // Stratum
buf[2] = 6; // Max Interval between messages in seconds
buf[3] = 0xEC; // Clock Precision
// bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset
buf[12] = 49; // four-byte reference ID identifying
buf[13] = 0x4E;
buf[14] = 49;
buf[15] = 52;
mUdp.write(buf, NTP_PACKET_SIZE);
}
void handleNTPPacket(AsyncUDPPacket packet) {
char buf[80];
memcpy(buf, packet.data(), sizeof(buf));
unsigned long highWord = word(buf[40], buf[41]);
unsigned long lowWord = word(buf[42], buf[43]);
// combine the four bytes (two words) into a long integer
// this is NTP time (seconds since Jan 1 1900):
unsigned long secsSince1900 = highWord << 16 | lowWord;
*mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time
DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC");
mOnTimeCB(true);
mUdp.close();
}
protected:
enum class NetworkState : uint8_t {
DISCONNECTED,
CONNECTED,
GOT_IP,
SCAN_READY, // ESP8266
CONNECTING // ESP8266
};
protected:
settings_t *mConfig = nullptr;
uint32_t *mUtcTimestamp = nullptr;
bool mConnected = false;
bool mScanActive = false;
bool mWifiConnecting = false;
OnNetworkCB mOnNetworkCB;
OnTimeCB mOnTimeCB;
NetworkState mStatus = NetworkState::DISCONNECTED;
AhoyWifiAp mAp;
DNSServer mDns;
AsyncUDP mUdp; // for time server
#if defined(ESP8266)
WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler;
#endif
};
#endif /*__AHOY_NETWORK_H__*/

20
src/network/AhoyNetworkHelper.cpp

@ -0,0 +1,20 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#include "AhoyNetworkHelper.h"
namespace ah {
void welcome(String ip, String info) {
DBGPRINTLN(F("\n\n-------------------"));
DBGPRINTLN(F("Welcome to AHOY!"));
DBGPRINT(F("\npoint your browser to http://"));
DBGPRINT(ip);
DBGPRINT(" (");
DBGPRINT(info);
DBGPRINTLN(")");
DBGPRINTLN(F("to configure your device"));
DBGPRINTLN(F("-------------------\n"));
}
}

39
src/network/AhoyNetworkHelper.h

@ -0,0 +1,39 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __AHOY_NETWORK_HELPER_H__
#define __AHOY_NETWORK_HELPER_H__
#include "../utils/dbg.h"
#include <Arduino.h>
#if defined(ESP32)
#include "ESPAsyncWebServer.h"
#include <WiFiType.h>
#include <ESPmDNS.h>
#else
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
//#include <WiFiUdp.h>
#include "ESPAsyncUDP.h"
enum {
SYSTEM_EVENT_STA_CONNECTED = 1,
ARDUINO_EVENT_ETH_CONNECTED,
SYSTEM_EVENT_STA_GOT_IP,
ARDUINO_EVENT_ETH_GOT_IP,
ARDUINO_EVENT_WIFI_STA_LOST_IP,
ARDUINO_EVENT_WIFI_STA_STOP,
SYSTEM_EVENT_STA_DISCONNECTED,
ARDUINO_EVENT_ETH_STOP,
ARDUINO_EVENT_ETH_DISCONNECTED
};
#endif
#include <DNSServer.h>
namespace ah {
void welcome(String ip, String info);
}
#endif /*__AHOY_NETWORK_HELPER_H__*/

76
src/network/AhoyWifiAp.h

@ -0,0 +1,76 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __AHOY_WIFI_AP_H__
#define __AHOY_WIFI_AP_H__
#include "../utils/dbg.h"
#include <Arduino.h>
#include "../config/settings.h"
#include "AhoyNetworkHelper.h"
class AhoyWifiAp {
public:
AhoyWifiAp() : mIp(192, 168, 4, 1) {}
void setup(cfgSys_t *cfg) {
mCfg = cfg;
}
void tickLoop() {
if(mEnabled)
mDns.processNextRequest();
if (WiFi.softAPgetStationNum() != mLast) {
mLast = WiFi.softAPgetStationNum();
if(mLast > 0)
DBGPRINTLN(F("AP client connected"));
}
}
void enable() {
if(mEnabled)
return;
ah::welcome(mIp.toString(), String(F("Password: ") + String(mCfg->apPwd)));
WiFi.mode(WIFI_AP_STA);
WiFi.softAPConfig(mIp, mIp, IPAddress(255, 255, 255, 0));
WiFi.softAP(WIFI_AP_SSID, mCfg->apPwd);
mDns.setErrorReplyCode(DNSReplyCode::NoError);
mDns.start(53, "*", mIp);
mEnabled = true;
tickLoop();
}
void disable() {
if(!mEnabled)
return;
if(WiFi.softAPgetStationNum() > 0)
return;
mDns.stop();
WiFi.softAPdisconnect();
WiFi.mode(WIFI_STA);
mEnabled = false;
}
bool isEnabled() const {
return mEnabled;
}
private:
cfgSys_t *mCfg = nullptr;
DNSServer mDns;
IPAddress mIp;
bool mEnabled = false;
uint8_t mLast = 0;
};
#endif /*__AHOY_WIFI_AP_H__*/

103
src/network/AhoyWifiEsp32.h

@ -0,0 +1,103 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __AHOY_WIFI_ESP32_H__
#define __AHOY_WIFI_ESP32_H__
#if defined(ESP32)
#include <functional>
#include <AsyncUDP.h>
#include "AhoyNetwork.h"
#include "ESPAsyncWebServer.h"
class AhoyWifi : public AhoyNetwork {
public:
virtual void begin() override {
mAp.enable();
if(strlen(mConfig->sys.stationSsid) == 0)
return; // no station wifi defined
WiFi.disconnect(); // clean up
WiFi.setHostname(mConfig->sys.deviceName);
#if !defined(AP_ONLY)
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
setStaticIp();
WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, WIFI_ALL_CHANNEL_SCAN);
mWifiConnecting = true;
DBGPRINT(F("connect to network '"));
DBGPRINT(mConfig->sys.stationSsid);
DBGPRINTLN(F("'"));
#endif
}
void tickNetworkLoop() override {
if(mAp.isEnabled())
mAp.tickLoop();
}
virtual String getIp(void) override {
return WiFi.localIP().toString();
}
virtual String getMac(void) override {
return WiFi.macAddress();
}
private:
virtual void OnEvent(WiFiEvent_t event) override {
switch(event) {
case SYSTEM_EVENT_STA_CONNECTED:
if(NetworkState::CONNECTED != mStatus) {
mStatus = NetworkState::CONNECTED;
mWifiConnecting = false;
DPRINTLN(DBG_INFO, F("Network connected"));
}
break;
case SYSTEM_EVENT_STA_GOT_IP:
mStatus = NetworkState::GOT_IP;
if(mAp.isEnabled())
mAp.disable();
if(!mConnected) {
mConnected = true;
ah::welcome(WiFi.localIP().toString(), F("Station"));
MDNS.begin(mConfig->sys.deviceName);
mOnNetworkCB(true);
}
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
[[fallthrough]];
case ARDUINO_EVENT_WIFI_STA_STOP:
[[fallthrough]];
case SYSTEM_EVENT_STA_DISCONNECTED:
mStatus = NetworkState::DISCONNECTED;
if(mConnected) {
mConnected = false;
mOnNetworkCB(false);
MDNS.end();
begin();
}
break;
default:
break;
}
}
virtual void setStaticIp() override {
setupIp([this](IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2) -> bool {
return WiFi.config(ip, gateway, mask, dns1, dns2);
});
}
};
#endif /*ESP32*/
#endif /*__AHOY_WIFI_ESP32_H__*/

170
src/network/AhoyWifiEsp8266.h

@ -0,0 +1,170 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __AHOY_WIFI_ESP8266_H__
#define __AHOY_WIFI_ESP8266_H__
#if defined(ESP8266)
#include <functional>
#include <list>
#include <WiFiUdp.h>
#include "AhoyNetwork.h"
#include "ESPAsyncWebServer.h"
class AhoyWifi : public AhoyNetwork {
public:
void begin() override {
mAp.enable();
WiFi.setHostname(mConfig->sys.deviceName);
mBSSIDList.clear();
}
void tickNetworkLoop() override {
if(mAp.isEnabled())
mAp.tickLoop();
mCnt++;
switch(mStatus) {
case NetworkState::DISCONNECTED:
if(mConnected) {
mConnected = false;
mWifiConnecting = false;
mOnNetworkCB(false);
mAp.enable();
MDNS.end();
}
if (WiFi.softAPgetStationNum() > 0) {
DBGPRINTLN(F("AP client connected"));
}
#if !defined(AP_ONLY)
else if (!mScanActive) {
DBGPRINT(F("scanning APs with SSID "));
DBGPRINTLN(String(mConfig->sys.stationSsid));
mScanCnt = 0;
mCnt = 0;
mScanActive = true;
WiFi.scanNetworks(true, true, 0U, ([this]() {
if (mConfig->sys.isHidden)
return (uint8_t*)NULL;
return (uint8_t*)(mConfig->sys.stationSsid);
})());
} else if(getBSSIDs()) {
mStatus = NetworkState::SCAN_READY;
DBGPRINT(F("connect to network '")); Serial.flush();
DBGPRINTLN(mConfig->sys.stationSsid);
}
#endif
break;
case NetworkState::SCAN_READY:
mStatus = NetworkState::CONNECTING;
DBGPRINT(F("try to connect to BSSID:"));
uint8_t bssid[6];
for (int j = 0; j < 6; j++) {
bssid[j] = mBSSIDList.front();
mBSSIDList.pop_front();
DBGPRINT(" " + String(bssid[j], HEX));
}
DBGPRINTLN("");
setStaticIp();
WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]);
mWifiConnecting = true;
break;
case NetworkState::CONNECTING:
if (isTimeout(TIMEOUT)) {
WiFi.disconnect();
mWifiConnecting = false;
mStatus = mBSSIDList.empty() ? NetworkState::DISCONNECTED : NetworkState::SCAN_READY;
}
break;
case NetworkState::CONNECTED:
break;
case NetworkState::GOT_IP:
if(!mConnected) {
mAp.disable();
mConnected = true;
ah::welcome(WiFi.localIP().toString(), F("Station"));
MDNS.begin(mConfig->sys.deviceName);
MDNSResponder::hMDNSService hRes = MDNS.addService(NULL, "http", "tcp", 80);
MDNS.addServiceTxt(hRes, "path", "/");
MDNS.announce();
mOnNetworkCB(true);
}
MDNS.update();
if(WiFi.channel() > 11)
mWasInCh12to14 = true;
break;
}
}
String getIp(void) override {
return WiFi.localIP().toString();
}
String getMac(void) override {
return WiFi.macAddress();
}
bool getWasInCh12to14() override {
return mWasInCh12to14;
}
private:
void setStaticIp() override {
setupIp([this](IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2) -> bool {
return WiFi.config(ip, gateway, mask, dns1, dns2);
});
}
bool getBSSIDs() {
bool result = false;
int n = WiFi.scanComplete();
if (n < 0) {
if (++mScanCnt < 20)
return false;
}
if(n > 0) {
mBSSIDList.clear();
int sort[n];
sortRSSI(&sort[0], n);
for (int i = 0; i < n; i++) {
DBGPRINT("BSSID " + String(i) + ":");
uint8_t *bssid = WiFi.BSSID(sort[i]);
for (int j = 0; j < 6; j++){
DBGPRINT(" " + String(bssid[j], HEX));
mBSSIDList.push_back(bssid[j]);
}
DBGPRINTLN("");
}
result = true;
}
mScanActive = false;
WiFi.scanDelete();
return result;
}
bool isTimeout(uint8_t timeout) {
return ((mCnt % timeout) == 0);
}
private:
uint8_t mCnt = 0;
uint8_t mScanCnt = 0;
std::list<uint8_t> mBSSIDList;
bool mWasInCh12to14 = false;
static constexpr uint8_t TIMEOUT = 20;
static constexpr uint8_t SCAN_TIMEOUT = 10;
};
#endif /*ESP8266*/
#endif /*__AHOY_WIFI_ESP8266_H__*/

375
src/platformio.ini

@ -23,16 +23,18 @@ extra_scripts =
pre:../scripts/convertHtml.py pre:../scripts/convertHtml.py
pre:../scripts/applyPatches.py pre:../scripts/applyPatches.py
pre:../scripts/reduceGxEPD2.py pre:../scripts/reduceGxEPD2.py
post:../scripts/add_littlefs_binary.py
lib_deps = lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer https://github.com/esphome/ESPAsyncWebServer @ ^3.2.2
https://github.com/nRF24/RF24 @ 1.4.8 https://github.com/nRF24/RF24.git#v1.4.8
paulstoffregen/Time @ ^1.6.1 paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.6.0 https://github.com/bertmelis/espMqttClient#v1.7.0
bblanchon/ArduinoJson @ ^6.21.3 bblanchon/ArduinoJson @ ^6.21.5
https://github.com/JChristensen/Timezone @ ^1.2.4 https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9 olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3 https://github.com/zinggjm/GxEPD2#1.5.3
build_flags = build_flags =
-std=c++17 -std=c++17
-std=gnu++17 -std=gnu++17
@ -40,67 +42,78 @@ build_unflags =
-std=gnu++11 -std=gnu++11
[env:esp8266] [env:esp8266-minimal]
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
lib_deps =
${env.lib_deps}
https://github.com/me-no-dev/ESPAsyncUDP
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096 -DEMC_MIN_FREE_MEMORY=4096
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
;-Wl,-Map,output.map ;-Wl,-Map,output.map
monitor_filters = monitor_filters =
esp8266_exception_decoder esp8266_exception_decoder
[env:esp8266]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
lib_deps = ${env:esp8266-minimal.lib_deps}
https://github.com/me-no-dev/ESPAsyncUDP
build_flags = ${env:esp8266-minimal.build_flags}
-DENABLE_MQTT
monitor_filters =
esp8266_exception_decoder
[env:esp8266-de] [env:esp8266-de]
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = ${env.build_flags} lib_deps = ${env:esp8266.lib_deps}
-DEMC_MIN_FREE_MEMORY=4096 build_flags = ${env:esp8266.build_flags}
-DLANG_DE -DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
;-Wl,-Map,output.map
monitor_filters = monitor_filters =
esp8266_exception_decoder esp8266_exception_decoder
[env:esp8266-prometheus] [env:esp8266-all]
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = ${env.build_flags} lib_deps = ${env:esp8266.lib_deps}
-DEMC_MIN_FREE_MEMORY=4096 build_flags = ${env:esp8266.build_flags}
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY -DPLUGIN_DISPLAY
-DENABLE_HISTORY -DENABLE_HISTORY
monitor_filters = monitor_filters =
esp8266_exception_decoder esp8266_exception_decoder
[env:esp8266-prometheus-de] [env:esp8266-all-de]
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = ${env.build_flags} lib_deps = ${env:esp8266.lib_deps}
-DEMC_MIN_FREE_MEMORY=4096 build_flags = ${env:esp8266-all.build_flags}
-DENABLE_PROMETHEUS_EP
-DLANG_DE -DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters = monitor_filters =
esp8266_exception_decoder esp8266_exception_decoder
[env:esp8266-minimal] [env:esp8266-prometheus]
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = ${env.build_flags} lib_deps = ${env:esp8266.lib_deps}
-DEMC_MIN_FREE_MEMORY=4096 build_flags = ${env:esp8266-all.build_flags}
;-Wl,-Map,output.map -DENABLE_PROMETHEUS_EP
monitor_filters =
esp8266_exception_decoder
[env:esp8266-prometheus-de]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
lib_deps = ${env:esp8266.lib_deps}
build_flags = ${env:esp8266-prometheus.build_flags}
-DLANG_DE
monitor_filters = monitor_filters =
esp8266_exception_decoder esp8266_exception_decoder
@ -111,6 +124,7 @@ platform = espressif8266
board = esp8285 board = esp8285
board_build.ldscript = eagle.flash.1m64.ld board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
lib_deps = ${env:esp8266.lib_deps}
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096 -DEMC_MIN_FREE_MEMORY=4096
-DENABLE_MQTT -DENABLE_MQTT
@ -124,6 +138,7 @@ platform = espressif8266
board = esp8285 board = esp8285
board_build.ldscript = eagle.flash.1m64.ld board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
lib_deps = ${env:esp8266.lib_deps}
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096 -DEMC_MIN_FREE_MEMORY=4096
-DLANG_DE -DLANG_DE
@ -133,114 +148,73 @@ build_flags = ${env.build_flags}
monitor_filters = monitor_filters =
esp8266_exception_decoder esp8266_exception_decoder
[env:esp32-wroom32] [env:esp32-wroom32-minimal]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = lolin_d32 board = lolin_d32
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD -DSPI_HAL
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-wroom32-minimal] [env:esp32-wroom32]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = lolin_d32 board = lolin_d32
build_flags = ${env.build_flags} build_flags = ${env:esp32-wroom32-minimal.build_flags}
-DUSE_HSPI_FOR_EPD -DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DETHERNET
-DDEF_ETH_CS_PIN=15
-DDEF_ETH_SCK_PIN=14
-DDEF_ETH_MISO_PIN=12
-DDEF_ETH_MOSI_PIN=13
-DDEF_ETH_IRQ_PIN=4
-DDEF_ETH_RST_PIN=255
-DDEF_NRF_CS_PIN=5
-DDEF_NRF_CE_PIN=17
-DDEF_NRF_IRQ_PIN=16
-DDEF_NRF_MISO_PIN=19
-DDEF_NRF_MOSI_PIN=23
-DDEF_NRF_SCLK_PIN=18
-DDEF_CMT_CSB=27
-DDEF_CMT_FCSB=26
-DDEF_CMT_IRQ=34
-DDEF_CMT_SDIO=14
-DDEF_CMT_SCLK=12
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-wroom32-de] [env:esp32-wroom32-de]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = lolin_d32 board = lolin_d32
build_flags = ${env.build_flags} build_flags = ${env:esp32-wroom32.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DLANG_DE -DLANG_DE
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-wroom32-prometheus] [env:esp32-wroom32-prometheus]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = lolin_d32 board = lolin_d32
build_flags = ${env.build_flags} build_flags = ${env:esp32-wroom32.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_PROMETHEUS_EP -DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-wroom32-prometheus-de] [env:esp32-wroom32-prometheus-de]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = lolin_d32 board = lolin_d32
build_flags = ${env.build_flags} build_flags = ${env:esp32-wroom32-prometheus.build_flags}
-DUSE_HSPI_FOR_EPD
-DLANG_DE
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-ethernet]
platform = espressif32
board = esp32dev
lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nRF24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
build_flags = ${env.build_flags}
-D ETHERNET
-DRELEASE
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-ethernet-de]
platform = espressif32
board = esp32dev
lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nRF24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
build_flags = ${env.build_flags}
-D ETHERNET
-DRELEASE
-DUSE_HSPI_FOR_EPD
-DLANG_DE -DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-s2-mini] [env:esp32-s2-mini]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = lolin_s2_mini board = lolin_s2_mini
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD -DUSE_HSPI_FOR_EPD
-DSPI_HAL
-DENABLE_MQTT -DENABLE_MQTT
-DPLUGIN_DISPLAY -DPLUGIN_DISPLAY
-DENABLE_HISTORY -DENABLE_HISTORY
@ -259,33 +233,19 @@ monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-s2-mini-de] [env:esp32-s2-mini-de]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = lolin_s2_mini board = lolin_s2_mini
build_flags = ${env.build_flags} build_flags = ${env:esp32-s2-mini.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DDEF_NRF_CS_PIN=12
-DDEF_NRF_CE_PIN=3
-DDEF_NRF_IRQ_PIN=5
-DDEF_NRF_MISO_PIN=9
-DDEF_NRF_MOSI_PIN=11
-DDEF_NRF_SCLK_PIN=7
-DDEF_CMT_CSB=16
-DDEF_CMT_FCSB=18
-DDEF_CMT_IRQ=33
-DDEF_CMT_SDIO=35
-DDEF_CMT_SCLK=37
-DLANG_DE -DLANG_DE
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-c3-mini] [env:esp32-c3-mini]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = lolin_c3_mini board = lolin_c3_mini
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD -DUSE_HSPI_FOR_EPD
-DSPI_HAL
-DENABLE_MQTT -DENABLE_MQTT
-DPLUGIN_DISPLAY -DPLUGIN_DISPLAY
-DENABLE_HISTORY -DENABLE_HISTORY
@ -304,36 +264,18 @@ monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-c3-mini-de] [env:esp32-c3-mini-de]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = lolin_c3_mini board = lolin_c3_mini
build_flags = ${env.build_flags} build_flags = ${env:esp32-c3-mini.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DDEF_NRF_CS_PIN=5
-DDEF_NRF_CE_PIN=0
-DDEF_NRF_IRQ_PIN=1
-DDEF_NRF_MISO_PIN=3
-DDEF_NRF_MOSI_PIN=4
-DDEF_NRF_SCLK_PIN=2
-DDEF_CMT_CSB=255
-DDEF_CMT_FCSB=255
-DDEF_CMT_IRQ=255
-DDEF_CMT_SDIO=255
-DDEF_CMT_SCLK=255
-DLANG_DE -DLANG_DE
monitor_filters = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:opendtufusion] [env:opendtufusion-minimal]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
upload_protocol = esp-builtin upload_protocol = esp-builtin
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DSPI_HAL -DSPI_HAL
-DDEF_NRF_CS_PIN=37 -DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38 -DDEF_NRF_CE_PIN=38
@ -354,145 +296,48 @@ build_flags = ${env.build_flags}
monitor_filters = monitor_filters =
esp32_exception_decoder, colorize esp32_exception_decoder, colorize
[env:opendtufusion-de] [env:opendtufusion]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
upload_protocol = esp-builtin upload_protocol = esp-builtin
build_flags = ${env.build_flags} build_flags = ${env:opendtufusion-minimal.build_flags}
-DLANG_DE -DETHERNET
-DENABLE_MQTT -DENABLE_MQTT
-DPLUGIN_DISPLAY -DPLUGIN_DISPLAY
-DENABLE_HISTORY -DENABLE_HISTORY
-DSPI_HAL -DDEF_ETH_CS_PIN=42
-DDEF_NRF_CS_PIN=37 -DDEF_ETH_SCK_PIN=39
-DDEF_NRF_CE_PIN=38 -DDEF_ETH_MISO_PIN=41
-DDEF_NRF_IRQ_PIN=47 -DDEF_ETH_MOSI_PIN=40
-DDEF_NRF_MISO_PIN=48 -DDEF_ETH_IRQ_PIN=44
-DDEF_NRF_MOSI_PIN=35 -DDEF_ETH_RST_PIN=43
-DDEF_NRF_SCLK_PIN=36
-DDEF_CMT_CSB=4
-DDEF_CMT_FCSB=21
-DDEF_CMT_IRQ=8
-DDEF_CMT_SDIO=5
-DDEF_CMT_SCLK=6
-DDEF_LED0=18
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
monitor_filters = monitor_filters =
esp32_exception_decoder, colorize esp32_exception_decoder, colorize
[env:opendtufusion-minimal] [env:opendtufusion-de]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
upload_protocol = esp-builtin upload_protocol = esp-builtin
build_flags = ${env.build_flags} build_flags = ${env:opendtufusion.build_flags}
-DSPI_HAL -DLANG_DE
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
-DDEF_NRF_MISO_PIN=48
-DDEF_NRF_MOSI_PIN=35
-DDEF_NRF_SCLK_PIN=36
-DDEF_CMT_CSB=4
-DDEF_CMT_FCSB=21
-DDEF_CMT_IRQ=8
-DDEF_CMT_SDIO=5
-DDEF_CMT_SCLK=6
-DDEF_LED0=18
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
monitor_filters = monitor_filters =
esp32_exception_decoder, colorize esp32_exception_decoder, colorize
[env:opendtufusion-ethernet] [env:opendtufusion-16MB]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
lib_deps = board_upload.flash_size = 16MB
khoih-prog/AsyncWebServer_ESP32_W5500 board_build.partitions = default_16MB.csv
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
upload_protocol = esp-builtin upload_protocol = esp-builtin
build_flags = ${env.build_flags} build_flags = ${env:opendtufusion.build_flags}
-DETHERNET
-DSPI_HAL
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DDEF_ETH_CS_PIN=42
-DDEF_ETH_SCK_PIN=39
-DDEF_ETH_MISO_PIN=41
-DDEF_ETH_MOSI_PIN=40
-DDEF_ETH_IRQ_PIN=44
-DDEF_ETH_RST_PIN=43
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
-DDEF_NRF_MISO_PIN=48
-DDEF_NRF_MOSI_PIN=35
-DDEF_NRF_SCLK_PIN=36
-DDEF_CMT_CSB=4
-DDEF_CMT_FCSB=21
-DDEF_CMT_IRQ=8
-DDEF_CMT_SDIO=5
-DDEF_CMT_SCLK=6
-DDEF_LED0=18
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
#-DARDUINO_USB_CDC_ON_BOOT=1
monitor_filters = monitor_filters =
esp32_exception_decoder, colorize esp32_exception_decoder, colorize
[env:opendtufusion-ethernet-de] [env:opendtufusion-16MB-de]
platform = espressif32@6.5.0 platform = espressif32@6.7.0
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
upload_protocol = esp-builtin upload_protocol = esp-builtin
build_flags = ${env.build_flags} build_flags = ${env:opendtufusion-16MB.build_flags}
-DETHERNET
-DSPI_HAL
-DLANG_DE -DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DDEF_ETH_CS_PIN=42
-DDEF_ETH_SCK_PIN=39
-DDEF_ETH_MISO_PIN=41
-DDEF_ETH_MOSI_PIN=40
-DDEF_ETH_IRQ_PIN=44
-DDEF_ETH_RST_PIN=43
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
-DDEF_NRF_MISO_PIN=48
-DDEF_NRF_MOSI_PIN=35
-DDEF_NRF_SCLK_PIN=36
-DDEF_CMT_CSB=4
-DDEF_CMT_FCSB=21
-DDEF_CMT_IRQ=8
-DDEF_CMT_SDIO=5
-DDEF_CMT_SCLK=6
-DDEF_LED0=18
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
#-DARDUINO_USB_CDC_ON_BOOT=1
monitor_filters = monitor_filters =
esp32_exception_decoder, colorize esp32_exception_decoder, colorize

58
src/plugins/Display/Display.h

@ -7,8 +7,9 @@
#include <U8g2lib.h> #include <U8g2lib.h>
#include "../../hm/hmSystem.h" #include "../../hm/hmSystem.h"
#include "../../hm/hmRadio.h" #include "../../hm/NrfRadio.h"
#include "../../utils/helper.h" #include "../../utils/helper.h"
#include "../plugin_lang.h"
#include "Display_Mono.h" #include "Display_Mono.h"
#include "Display_Mono_128X32.h" #include "Display_Mono_128X32.h"
#include "Display_Mono_128X64.h" #include "Display_Mono_128X64.h"
@ -24,9 +25,9 @@ class Display {
mMono = NULL; mMono = NULL;
} }
void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *hmradio, RADIO *hmsradio, uint32_t *utcTs) { void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *nrfRadio, RADIO *hmsradio, uint32_t *utcTs) {
mApp = app; mApp = app;
mHmRadio = hmradio; mNrfRadio = nrfRadio;
mHmsRadio = hmsradio; mHmsRadio = hmsradio;
mCfg = cfg; mCfg = cfg;
mSys = sys; mSys = sys;
@ -44,7 +45,7 @@ class Display {
case DISP_TYPE_T4_SSD1306_128X32: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91") case DISP_TYPE_T4_SSD1306_128X32: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91")
case DISP_TYPE_T5_SSD1306_64X48: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield) case DISP_TYPE_T5_SSD1306_64X48: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield)
case DISP_TYPE_T6_SSD1309_128X64: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42") case DISP_TYPE_T6_SSD1309_128X64: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42")
#if defined(ESP32) && !defined(ETHERNET) #if defined(ESP32)
case DISP_TYPE_T10_EPAPER: case DISP_TYPE_T10_EPAPER:
mMono = NULL; // ePaper does not use this mMono = NULL; // ePaper does not use this
mRefreshCycle = 0; mRefreshCycle = 0;
@ -71,6 +72,14 @@ class Display {
} }
void loop() {
#if defined(ESP32)
if ((nullptr != mCfg) && (DISP_TYPE_T10_EPAPER == mCfg->type)) {
mEpaper.refreshLoop();
}
#endif
}
void payloadEventListener(uint8_t cmd) { void payloadEventListener(uint8_t cmd) {
mNewPayload = true; mNewPayload = true;
} }
@ -78,16 +87,25 @@ class Display {
void tickerSecond() { void tickerSecond() {
bool request_refresh = false; bool request_refresh = false;
if (mMono != NULL) if (mMono != NULL) {
// maintain LCD and OLED displays with pixel shift screensavers, at least every 5 seconds
request_refresh = mMono->loop(motionSensorActive()); request_refresh = mMono->loop(motionSensorActive());
if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) {
if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) { DataScreen();
DataScreen(); mNewPayload = false;
mNewPayload = false; mLoopCnt = 0;
mLoopCnt = 0; }
} }
#if defined(ESP32) && !defined(ETHERNET) #if defined(ESP32)
else if (DISP_TYPE_T10_EPAPER == mCfg->type) {
// maintain ePaper at least every 15 seconds
if (mNewPayload || (((++mLoopCnt) % 15) == 0)) {
DataScreen();
mNewPayload = false;
mLoopCnt = 0;
}
mEpaper.tickerSecond(); mEpaper.tickerSecond();
}
#endif #endif
} }
@ -148,7 +166,7 @@ class Display {
mDisplayData.totalYieldDay = totalYieldDay; mDisplayData.totalYieldDay = totalYieldDay;
mDisplayData.totalYieldTotal = totalYieldTotal; mDisplayData.totalYieldTotal = totalYieldTotal;
bool nrf_en = mApp->getNrfEnabled(); bool nrf_en = mApp->getNrfEnabled();
bool nrf_ok = nrf_en && mHmRadio->isChipConnected(); bool nrf_ok = nrf_en && mNrfRadio->isChipConnected();
#if defined(ESP32) #if defined(ESP32)
bool cmt_en = mApp->getCmtEnabled(); bool cmt_en = mApp->getCmtEnabled();
bool cmt_ok = cmt_en && mHmsRadio->isChipConnected(); bool cmt_ok = cmt_en && mHmsRadio->isChipConnected();
@ -175,16 +193,18 @@ class Display {
if (mMono ) { if (mMono ) {
mMono->disp(); mMono->disp();
} }
#if defined(ESP32) && !defined(ETHERNET) #if defined(ESP32)
else if (DISP_TYPE_T10_EPAPER == mCfg->type) { else if (DISP_TYPE_T10_EPAPER == mCfg->type) {
mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod); mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
mRefreshCycle++; mRefreshCycle++;
}
if (mRefreshCycle > 480) { if (mRefreshCycle > 2880) { // 15 * 2280 = 44300s = 12h
mEpaper.fullRefresh(); mEpaper.fullRefresh();
mRefreshCycle = 0; mRefreshCycle = 0;
}
} }
#endif #endif
} }
@ -230,11 +250,11 @@ class Display {
uint32_t *mUtcTs = nullptr; uint32_t *mUtcTs = nullptr;
display_t *mCfg = nullptr; display_t *mCfg = nullptr;
HMSYSTEM *mSys = nullptr; HMSYSTEM *mSys = nullptr;
RADIO *mHmRadio = nullptr; RADIO *mNrfRadio = nullptr;
RADIO *mHmsRadio = nullptr; RADIO *mHmsRadio = nullptr;
uint16_t mRefreshCycle = 0; uint16_t mRefreshCycle = 0;
#if defined(ESP32) && !defined(ETHERNET) #if defined(ESP32)
DisplayEPaper mEpaper; DisplayEPaper mEpaper;
#endif #endif
DisplayMono *mMono = nullptr; DisplayMono *mMono = nullptr;

8
src/plugins/Display/Display_Mono_128X32.h

@ -40,20 +40,20 @@ class DisplayMono128X32 : public DisplayMono {
printText(mFmtText, 0); printText(mFmtText, 0);
} else { } else {
printText("offline", 0); printText(STR_OFFLINE, 0);
} }
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", mDisplayData->totalYieldDay); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %4.0f Wh", STR_TODAY, mDisplayData->totalYieldDay);
printText(mFmtText, 1); printText(mFmtText, 1);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", mDisplayData->totalYieldTotal); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %.1f kWh", STR_TOTAL, mDisplayData->totalYieldTotal);
printText(mFmtText, 2); printText(mFmtText, 2);
IPAddress ip = WiFi.localIP(); IPAddress ip = WiFi.localIP();
if (!(mExtra % 10) && (ip)) if (!(mExtra % 10) && (ip))
printText(ip.toString().c_str(), 3); printText(ip.toString().c_str(), 3);
else if (!(mExtra % 5)) { else if (!(mExtra % 5)) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", mDisplayData->nrProducing); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %d", STR_ACTIVE_INVERTERS, mDisplayData->nrProducing);
printText(mFmtText, 3); printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs) } else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3); printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);

6
src/plugins/Display/Display_Mono_128X64.h

@ -93,7 +93,7 @@ class DisplayMono128X64 : public DisplayMono {
// print Date and time // print Date and time
if (0 != mDisplayData->utcTs) if (0 != mDisplayData->utcTs)
printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff); printText(ah::getDateTimeStrShort_i18n(mDisplayData->utcTs).c_str(), l_Time, 0xff);
if (showLine(l_Status)) { if (showLine(l_Status)) {
// alternatively: // alternatively:
@ -108,7 +108,7 @@ class DisplayMono128X64 : public DisplayMono {
int8_t moon_pos = -1; int8_t moon_pos = -1;
setLineFont(l_Status); setLineFont(l_Status);
if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); snprintf(mFmtText, DISP_FMT_TEXT_LEN, STR_NO_INVERTER);
else if (0 == mDisplayData->nrSleeping) { else if (0 == mDisplayData->nrSleeping) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); snprintf(mFmtText, DISP_FMT_TEXT_LEN, " ");
sun_pos = 0; sun_pos = 0;
@ -145,7 +145,7 @@ class DisplayMono128X64 : public DisplayMono {
printText(mFmtText, l_TotalPower, 0xff); printText(mFmtText, l_TotalPower, 0xff);
} else { } else {
printText("offline", l_TotalPower, 0xff); printText(STR_OFFLINE, l_TotalPower, 0xff);
} }
} }

4
src/plugins/Display/Display_Mono_64X48.h

@ -42,7 +42,7 @@ class DisplayMono64X48 : public DisplayMono {
printText(mFmtText, 0); printText(mFmtText, 0);
} else { } else {
printText("offline", 0); printText(STR_OFFLINE, 0);
} }
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", mDisplayData->totalYieldDay); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", mDisplayData->totalYieldDay);
@ -55,7 +55,7 @@ class DisplayMono64X48 : public DisplayMono {
if (!(mExtra % 10) && (ip)) if (!(mExtra % 10) && (ip))
printText(ip.toString().c_str(), 3); printText(ip.toString().c_str(), 3);
else if (!(mExtra % 5)) { else if (!(mExtra % 5)) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", mDisplayData->nrProducing); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %d", STR_ACTIVE_INVERTERS, mDisplayData->nrProducing);
printText(mFmtText, 3); printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs) } else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3); printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);

9
src/plugins/Display/Display_Mono_84X48.h

@ -78,7 +78,7 @@ class DisplayMono84X48 : public DisplayMono {
// print Date and time // print Date and time
if (0 != mDisplayData->utcTs) if (0 != mDisplayData->utcTs)
printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff); printText(ah::getDateTimeStrShort_i18n(mDisplayData->utcTs).c_str(), l_Time, 0xff);
if (showLine(l_Status)) { if (showLine(l_Status)) {
// alternatively: // alternatively:
@ -90,7 +90,7 @@ class DisplayMono84X48 : public DisplayMono {
// print status of inverters // print status of inverters
else { else {
if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); snprintf(mFmtText, DISP_FMT_TEXT_LEN, STR_NO_INVERTER);
else if (0 == mDisplayData->nrSleeping) else if (0 == mDisplayData->nrSleeping)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol
else if (0 == mDisplayData->nrProducing) else if (0 == mDisplayData->nrProducing)
@ -110,9 +110,8 @@ class DisplayMono84X48 : public DisplayMono {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
printText(mFmtText, l_TotalPower, 0xff); printText(mFmtText, l_TotalPower, 0xff);
} else { } else
printText("offline", l_TotalPower, 0xff); printText(STR_OFFLINE, l_TotalPower, 0xff);
}
} }
if (showLine(l_YieldDay)) { if (showLine(l_YieldDay)) {

103
src/plugins/Display/Display_ePaper.cpp

@ -1,15 +1,11 @@
#include "Display_ePaper.h" #include "Display_ePaper.h"
#ifdef ESP8266 #if defined(ESP32)
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h> #include <WiFi.h>
#endif
#include "../../utils/helper.h" #include "../../utils/helper.h"
#include "imagedata.h" #include "imagedata.h"
#include "defines.h" #include "defines.h"
#include "../plugin_lang.h"
#if defined(ESP32)
static const uint32_t spiClk = 4000000; // 4 MHz static const uint32_t spiClk = 4000000; // 4 MHz
@ -30,20 +26,26 @@ void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, u
mRefreshState = RefreshStatus::LOGO; mRefreshState = RefreshStatus::LOGO;
mSecondCnt = 0; mSecondCnt = 0;
mLogoDisplayed = false;
if (DISP_TYPE_T10_EPAPER == type) { if (DISP_TYPE_T10_EPAPER == type) {
Serial.begin(115200); #if defined(SPI_HAL)
_display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); hal.init(_MOSI, _DC, _SCK, _CS, _RST, _BUSY);
_display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(&hal));
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) #else
hspi.begin(_SCK, _BUSY, _MOSI, _CS); _display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY));
_display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0)); #if defined(USE_HSPI_FOR_EPD)
#elif defined(ESP32) hspi.begin(_SCK, _BUSY, _MOSI, _CS);
_display->epd2.init(_SCK, _MOSI, 115200, true, 20, false); _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0));
#endif #elif defined(PLUGIN_DISPLAY)
_display->init(115200, true, 20, false); _display->epd2.init(_SCK, _MOSI, 115200, true, 20, false);
#endif
#endif
_display->init(0, true, 20, false);
_display->setRotation(mDisplayRotation); _display->setRotation(mDisplayRotation);
_display->setFullWindow(); _display->setFullWindow();
_display->setTextColor(GxEPD_BLACK);
_display->firstPage();
_version = version; _version = version;
} }
} }
@ -57,7 +59,8 @@ void DisplayEPaper::config(uint8_t rotation, bool enPowerSave) {
void DisplayEPaper::fullRefresh() { void DisplayEPaper::fullRefresh() {
if(RefreshStatus::DONE != mRefreshState) if(RefreshStatus::DONE != mRefreshState)
return; return;
mSecondCnt = 2; if(mLogoDisplayed)
return; // no refresh during logo display
mRefreshState = RefreshStatus::BLACK; mRefreshState = RefreshStatus::BLACK;
} }
@ -65,40 +68,42 @@ void DisplayEPaper::fullRefresh() {
void DisplayEPaper::refreshLoop() { void DisplayEPaper::refreshLoop() {
switch(mRefreshState) { switch(mRefreshState) {
case RefreshStatus::LOGO: case RefreshStatus::LOGO:
_display->fillScreen(GxEPD_BLACK); _display->fillScreen(GxEPD_WHITE);
_display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); _display->drawInvertedBitmap(0, 0, logo, 200, 200, GxEPD_BLACK);
mSecondCnt = 2; if(_display->nextPage())
mNextRefreshState = RefreshStatus::PARTITIALS; break;
mRefreshState = RefreshStatus::WAIT; mSecondCnt = 10;
_display->powerOff();
mRefreshState = RefreshStatus::LOGO_WAIT;
break;
case RefreshStatus::LOGO_WAIT:
if(0 != mSecondCnt)
break;
mRefreshState = RefreshStatus::WHITE;
_display->firstPage();
break; break;
case RefreshStatus::BLACK: case RefreshStatus::BLACK:
_display->fillScreen(GxEPD_BLACK); _display->fillScreen(GxEPD_BLACK);
mNextRefreshState = RefreshStatus::WHITE; if(_display->nextPage())
mRefreshState = RefreshStatus::WAIT; break;
mRefreshState = RefreshStatus::WHITE;
_display->firstPage();
break; break;
case RefreshStatus::WHITE: case RefreshStatus::WHITE:
if(0 != mSecondCnt)
break;
_display->fillScreen(GxEPD_WHITE); _display->fillScreen(GxEPD_WHITE);
mNextRefreshState = RefreshStatus::PARTITIALS; if(_display->nextPage())
mRefreshState = RefreshStatus::WAIT; break;
break; mRefreshState = RefreshStatus::PARTITIALS;
_display->firstPage();
case RefreshStatus::WAIT:
if(!_display->nextPage())
mRefreshState = mNextRefreshState;
break; break;
case RefreshStatus::PARTITIALS: case RefreshStatus::PARTITIALS:
if(0 != mSecondCnt)
break;
headlineIP(); headlineIP();
versionFooter(); versionFooter();
mSecondCnt = 4; // display Logo time during boot up mRefreshState = RefreshStatus::DONE;
mNextRefreshState = RefreshStatus::DONE;
mRefreshState = RefreshStatus::WAIT;
break; break;
default: // RefreshStatus::DONE default: // RefreshStatus::DONE
@ -120,7 +125,7 @@ void DisplayEPaper::headlineIP() {
if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) { if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) {
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str()); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str());
} else { } else {
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "WiFi not connected"); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_NO_WIFI);
} }
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx; uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -162,7 +167,7 @@ void DisplayEPaper::versionFooter() {
_display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding); _display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding);
_display->fillScreen(GxEPD_BLACK); _display->fillScreen(GxEPD_BLACK);
do { do {
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "Version: %s", _version); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s: %s", STR_VERSION, _version);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx; uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -183,7 +188,7 @@ void DisplayEPaper::offlineFooter() {
_display->fillScreen(GxEPD_BLACK); _display->fillScreen(GxEPD_BLACK);
do { do {
if (NULL != mUtcTs) { if (NULL != mUtcTs) {
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline"); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_OFFLINE);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx; uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -213,12 +218,17 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.0f W", totalPower); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.0f W", totalPower);
_changed = true; _changed = true;
} else } else
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline"); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_OFFLINE);
if ((totalPower == 0) && (mEnPowerSave)) { if ((totalPower == 0) && (mEnPowerSave)) {
_display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK); _display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK);
_display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
mLogoDisplayed = true;
} else { } else {
if(mLogoDisplayed) {
mLogoDisplayed = false;
fullRefresh();
}
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
x = ((_display->width() - tbw) / 2) - tbx; x = ((_display->width() - tbw) / 2) - tbx;
_display->setCursor(x, mHeadFootPadding + tbh + 10); _display->setCursor(x, mHeadFootPadding + tbh + 10);
@ -268,7 +278,7 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
// Inverter online // Inverter online
_display->setFont(&FreeSans12pt7b); _display->setFont(&FreeSans12pt7b);
y = _display->height() - (mHeadFootPadding + 10); y = _display->height() - (mHeadFootPadding + 10);
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d online", isprod); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d %s", isprod, STR_ONLINE);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
_display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK); _display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK);
x = ((_display->width() - tbw - 20) / 2) - tbx; x = ((_display->width() - tbw - 20) / 2) - tbx;
@ -305,8 +315,9 @@ void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYield
//*************************************************************************** //***************************************************************************
void DisplayEPaper::tickerSecond() { void DisplayEPaper::tickerSecond() {
if(mSecondCnt != 0) if(RefreshStatus::LOGO_WAIT == mRefreshState) {
mSecondCnt--; if(mSecondCnt > 0)
refreshLoop(); mSecondCnt--;
}
} }
#endif // ESP32 #endif // ESP32

15
src/plugins/Display/Display_ePaper.h

@ -12,7 +12,11 @@
#define EPAPER_MAX_TEXT_LEN 35 #define EPAPER_MAX_TEXT_LEN 35
#include <GxEPD2_BW.h> #include <GxEPD2_BW.h>
#if defined(SPI_HAL)
#include "epdHal.h"
#else
#include <SPI.h> #include <SPI.h>
#endif
// FreeFonts from Adafruit_GFX // FreeFonts from Adafruit_GFX
#include <Fonts/FreeSans12pt7b.h> #include <Fonts/FreeSans12pt7b.h>
@ -44,9 +48,9 @@ class DisplayEPaper {
DONE, DONE,
BLACK, BLACK,
WHITE, WHITE,
WAIT,
PARTITIALS, PARTITIALS,
LOGO LOGO,
LOGO_WAIT
}; };
uint8_t mDisplayRotation; uint8_t mDisplayRotation;
@ -58,8 +62,13 @@ class DisplayEPaper {
uint32_t* mUtcTs; uint32_t* mUtcTs;
bool mEnPowerSave; bool mEnPowerSave;
const char* _version; const char* _version;
RefreshStatus mRefreshState, mNextRefreshState; RefreshStatus mRefreshState;
uint8_t mSecondCnt; uint8_t mSecondCnt;
bool mLogoDisplayed;
#if defined(SPI_HAL)
epdHal hal;
#endif
}; };
#endif // ESP32 #endif // ESP32

304
src/plugins/Display/epdHal.h

@ -0,0 +1,304 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __EPD_HAL_H__
#define __EPD_HAL_H__
#pragma once
#include "../../utils/spiPatcher.h"
#include <esp_rom_gpio.h>
#include <GxEPD2_BW.h>
#define EPD_DEFAULT_SPI_SPEED 4000000 // 4 MHz
class epdHal: public GxEPD2_HalInterface, public SpiPatcherHandle {
public:
epdHal() {}
void patch() override {
esp_rom_gpio_connect_out_signal(mPinMosi, spi_periph_signal[mHostDevice].spid_out, false, false);
esp_rom_gpio_connect_in_signal(mPinBusy, spi_periph_signal[mHostDevice].spiq_in, false);
esp_rom_gpio_connect_out_signal(mPinClk, spi_periph_signal[mHostDevice].spiclk_out, false, false);
}
void unpatch() override {
esp_rom_gpio_connect_out_signal(mPinMosi, SIG_GPIO_OUT_IDX, false, false);
esp_rom_gpio_connect_in_signal(mPinBusy, GPIO_MATRIX_CONST_ZERO_INPUT, false);
esp_rom_gpio_connect_out_signal(mPinClk, SIG_GPIO_OUT_IDX, false, false);
}
void init(int8_t mosi, int8_t dc, int8_t sclk, int8_t cs, int8_t rst, int8_t busy, int32_t speed = EPD_DEFAULT_SPI_SPEED) {
mPinMosi = static_cast<gpio_num_t>(mosi);
mPinDc = static_cast<gpio_num_t>(dc);
mPinClk = static_cast<gpio_num_t>(sclk);
mPinCs = static_cast<gpio_num_t>(cs);
mPinRst = static_cast<gpio_num_t>(rst);
mPinBusy = static_cast<gpio_num_t>(busy);
mSpiSpeed = speed;
#if defined(CONFIG_IDF_TARGET_ESP32S3)
mHostDevice = SPI3_HOST;
#else
mHostDevice = (14 == sclk) ? SPI2_HOST : SPI_HOST_OTHER;
#endif
mSpiPatcher = SpiPatcher::getInstance(mHostDevice);
gpio_reset_pin(mPinMosi);
gpio_set_direction(mPinMosi, GPIO_MODE_OUTPUT);
gpio_set_level(mPinMosi, 1);
gpio_reset_pin(mPinClk);
gpio_set_direction(mPinClk, GPIO_MODE_OUTPUT);
gpio_set_level(mPinClk, 0);
gpio_reset_pin(mPinCs);
spi_device_interface_config_t devcfg = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.clock_speed_hz = mSpiSpeed,
.input_delay_ns = 0,
.spics_io_num = mPinCs,
.flags = 0,
.queue_size = 1,
.pre_cb = nullptr,
.post_cb = nullptr
};
mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi);
if(GPIO_NUM_NC != mPinRst) {
gpio_reset_pin(mPinRst);
gpio_set_direction(mPinRst, GPIO_MODE_OUTPUT);
gpio_set_level(mPinRst, HIGH);
}
gpio_reset_pin(mPinDc);
gpio_set_direction(mPinDc, GPIO_MODE_OUTPUT);
gpio_set_level(mPinDc, HIGH);
//gpio_reset_pin(mPinBusy);
//gpio_set_direction(mPinBusy, GPIO_MODE_INPUT);
}
void rstMode(uint8_t mode) override {
if(GPIO_NUM_NC != mPinRst)
gpio_set_direction(mPinRst, static_cast<gpio_mode_t>(mode));
}
void rst(bool level) override {
if(GPIO_NUM_NC != mPinRst)
gpio_set_level(mPinRst, level);
}
int getBusy(void) override {
return gpio_get_level(mPinBusy);
}
bool isRst(void) override {
return (GPIO_NUM_NC != mPinRst);
}
void write(uint8_t buf) override {
uint8_t data[1];
data[0] = buf;
request_spi();
size_t spiLen = static_cast<size_t>(1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
release_spi();
}
void write(const uint8_t *buf, uint16_t n) override {
uint8_t data[n];
std::copy(&buf[0], &buf[n], &data[0]);
request_spi();
size_t spiLen = static_cast<size_t>(n) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
release_spi();
}
void write(const uint8_t *buf, uint16_t n, int16_t fill_with_zeroes) override {
uint8_t data[n + fill_with_zeroes];
memset(data, 0, (n + fill_with_zeroes));
for (uint16_t i = 0; i < n; i++) {
data[i] = pgm_read_byte(&*buf++);
}
request_spi();
spi_transaction_t t = {
.flags = SPI_TRANS_CS_KEEP_ACTIVE,
.cmd = 0,
.addr = 0,
.length = 1u,
.rxlength = 1u,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
size_t offs = 0;
spi_device_acquire_bus(spi, portMAX_DELAY);
while(offs < (n + fill_with_zeroes)) {
t.length = (64u << 3);
t.rxlength = t.length;
t.tx_buffer = &data[offs];
offs += 64;
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
}
spi_device_release_bus(spi);
release_spi();
}
void writeCmd(const uint8_t val) override {
uint8_t data[1];
data[0] = val;
request_spi();
gpio_set_level(mPinDc, LOW);
size_t spiLen = static_cast<size_t>(1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
gpio_set_level(mPinDc, HIGH);
release_spi();
}
void writeCmd(const uint8_t *buf, uint8_t n, bool isPGM) override {
uint8_t data[n-1];
data[0] = (isPGM) ? pgm_read_byte(&*buf++) : buf[0];
request_spi();
gpio_set_level(mPinDc, LOW);
spi_device_acquire_bus(spi, portMAX_DELAY);
size_t spiLen = static_cast<size_t>(1u) << 3;
spi_transaction_t t = {
.flags = SPI_TRANS_CS_KEEP_ACTIVE,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
gpio_set_level(mPinDc, HIGH);
if(isPGM) {
for (uint16_t i = 0; i < n; i++) {
data[i] = pgm_read_byte(&*buf++);
}
} else
std::copy(&buf[1], &buf[n], &data[0]);
spiLen = static_cast<size_t>(n-1) << 3;
spi_transaction_t t1 = {
.flags = SPI_TRANS_CS_KEEP_ACTIVE,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t1));
spi_device_release_bus(spi);
release_spi();
}
void startTransfer(void) override {
request_spi();
}
void endTransfer(void) override {
release_spi();
}
void transfer(const uint8_t val) override {
uint8_t data[1];
data[0] = val;
size_t spiLen = static_cast<size_t>(1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = spiLen,
.rxlength = spiLen,
.user = NULL,
.tx_buffer = data,
.rx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t));
}
private:
inline void request_spi() {
mSpiPatcher->request(this);
}
inline void release_spi() {
mSpiPatcher->release();
}
private:
gpio_num_t mPinMosi = GPIO_NUM_NC;
gpio_num_t mPinDc = GPIO_NUM_NC;
gpio_num_t mPinClk = GPIO_NUM_NC;
gpio_num_t mPinCs = GPIO_NUM_NC;
gpio_num_t mPinRst = GPIO_NUM_NC;
gpio_num_t mPinBusy = GPIO_NUM_NC;
int32_t mSpiSpeed = EPD_DEFAULT_SPI_SPEED;
spi_host_device_t mHostDevice;
spi_device_handle_t spi;
SpiPatcher *mSpiPatcher;
};
#endif /*__EPD_HAL_H__*/

67
src/plugins/MaxPower.h

@ -0,0 +1,67 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __MAX_VALUE__
#define __MAX_VALUE__
#pragma once
#include <array>
#include <utility>
#include "../hm/hmDefines.h"
template<class T=float>
class MaxPower {
public:
MaxPower() {
mTs = nullptr;
mMaxDiff = 60;
reset();
}
void setup(uint32_t *ts, uint16_t interval) {
mTs = ts;
mMaxDiff = interval * 4;
}
void reset(void) {
mValues.fill(std::make_pair(0, 0.0));
mLast = 0.0;
}
void payloadEvent(uint8_t cmd, Inverter<> *iv) {
if(RealTimeRunData_Debug != cmd)
return;
if(nullptr == iv)
return;
if(iv->id >= MAX_NUM_INVERTERS)
return;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
mValues[iv->id] = std::make_pair(*mTs, iv->getChannelFieldValue(CH0, FLD_PAC, rec));
}
T getTotalMaxPower(void) {
T val = 0;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
if((mValues[i].first + mMaxDiff) >= *mTs)
val += mValues[i].second;
else if(mValues[i].first > 0)
break; // old data
}
if(val > mLast)
mLast = val;
return mLast;
}
private:
uint32_t *mTs;
uint32_t mMaxDiff;
float mLast;
std::array<std::pair<uint32_t, T>, MAX_NUM_INVERTERS> mValues;
};
#endif

223
src/plugins/history.h

@ -17,6 +17,7 @@
enum class HistoryStorageType : uint8_t { enum class HistoryStorageType : uint8_t {
POWER, POWER,
POWER_DAY,
YIELD YIELD
}; };
@ -27,12 +28,14 @@ class HistoryData {
uint16_t refreshCycle = 0; uint16_t refreshCycle = 0;
uint16_t loopCnt = 0; uint16_t loopCnt = 0;
uint16_t listIdx = 0; // index for next Element to write into WattArr 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 // ring buffer for watt history
std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data; std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data;
storage_t() { data.fill(0); } void reset() {
loopCnt = 0;
listIdx = 0;
data.fill(0);
}
}; };
public: public:
@ -43,33 +46,52 @@ class HistoryData {
mTs = ts; mTs = ts;
mCurPwr.refreshCycle = mConfig->inst.sendInterval; mCurPwr.refreshCycle = mConfig->inst.sendInterval;
//mYieldDay.refreshCycle = 60; mCurPwrDay.refreshCycle = mConfig->inst.sendInterval;
#if defined(ENABLE_HISTORY_YIELD_PER_DAY)
mYieldDay.refreshCycle = 60;
#endif
mLastValueTs = 0;
mPgPeriod=0;
mMaximumDay = 0;
} }
void tickerSecond() { void tickerSecond() {
;
float curPwr = 0; float curPwr = 0;
float maxPwr = 0;
float yldDay = -0.1; float yldDay = -0.1;
uint32_t ts = 0;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
Inverter<> *iv = mSys->getInverterByPos(i); Inverter<> *iv = mSys->getInverterByPos(i);
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL) if (iv == NULL)
continue; continue;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec); curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec);
yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
if (rec->ts > ts)
ts = rec->ts;
} }
if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) { if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) {
mCurPwr.loopCnt = 0; mCurPwr.loopCnt = 0;
if (curPwr > 0) if (curPwr > 0) {
mLastValueTs = ts;
addValue(&mCurPwr, roundf(curPwr)); addValue(&mCurPwr, roundf(curPwr));
if (maxPwr > 0) if (curPwr > mMaximumDay)
mMaximumDay = roundf(maxPwr); mMaximumDay = roundf(curPwr);
}
} }
/*if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { if ((++mCurPwrDay.loopCnt % mCurPwrDay.refreshCycle) == 0) {
mCurPwrDay.loopCnt = 0;
if (curPwr > 0) {
mLastValueTs = ts;
addValueDay(&mCurPwrDay, roundf(curPwr));
}
}
#if defined(ENABLE_HISTORY_YIELD_PER_DAY)
if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) {
mYieldDay.loopCnt = 0;
if (*mTs > mApp->getSunset()) { if (*mTs > mApp->getSunset()) {
if ((!mDayStored) && (yldDay > 0)) { if ((!mDayStored) && (yldDay > 0)) {
addValue(&mYieldDay, roundf(yldDay)); addValue(&mYieldDay, roundf(yldDay));
@ -77,28 +99,172 @@ class HistoryData {
} }
} else if (*mTs > mApp->getSunrise()) } else if (*mTs > mApp->getSunrise())
mDayStored = false; mDayStored = false;
}*/ }
#endif
} }
uint16_t valueAt(HistoryStorageType type, uint16_t i) { uint16_t valueAt(HistoryStorageType type, uint16_t i) {
//storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; storage_t *s = nullptr;
storage_t *s = &mCurPwr; uint16_t idx=i;
uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH; DPRINTLN(DBG_VERBOSE, F("valueAt ") + String((uint8_t)type) + " i=" + String(i));
return s->data[idx];
switch (type) {
default:
[[fallthrough]];
case HistoryStorageType::POWER:
s = &mCurPwr;
idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH;
break;
case HistoryStorageType::POWER_DAY:
s = &mCurPwrDay;
break;
#if defined(ENABLE_HISTORY_YIELD_PER_DAY)
case HistoryStorageType::YIELD:
s = &mYieldDay;
idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH;
break;
#endif
}
return (nullptr == s) ? 0 : s->data[idx];
} }
uint16_t getMaximumDay() { uint16_t getMaximumDay() {
return mMaximumDay; return mMaximumDay;
} }
uint32_t getLastValueTs(HistoryStorageType type) {
DPRINTLN(DBG_VERBOSE, F("getLastValueTs ") + String((uint8_t)type));
if (type == HistoryStorageType::POWER_DAY)
return mPgEndTime;
return mLastValueTs;
}
uint32_t getPeriod(HistoryStorageType type) {
DPRINTLN(DBG_VERBOSE, F("getPeriode ") + String((uint8_t)type));
switch (type) {
case HistoryStorageType::POWER:
return mCurPwr.refreshCycle;
break;
case HistoryStorageType::POWER_DAY:
return mPgPeriod / HISTORY_DATA_ARR_LENGTH;
break;
case HistoryStorageType::YIELD:
return (60 * 60 * 24); // 1 day
break;
}
return 0;
}
bool isDataValid(void) {
return ((0 != mPgStartTime) && (0 != mPgEndTime));
}
#if defined(ENABLE_HISTORY_LOAD_DATA)
void addValue(HistoryStorageType historyType, uint8_t valueType, uint32_t value) {
if (valueType < 2) {
storage_t *s = NULL;
switch (historyType) {
default:
[[fallthrough]];
case HistoryStorageType::POWER:
s = &mCurPwr;
break;
case HistoryStorageType::POWER_DAY:
s = &mCurPwrDay;
break;
#if defined(ENABLE_HISTORY_YIELD_PER_DAY)
case HistoryStorageType::YIELD:
s = &mYieldDay;
break;
#endif
}
if (s) {
if (0 == valueType)
addValue(s, value);
else {
if (historyType == HistoryStorageType::POWER)
s->refreshCycle = value;
if (historyType == HistoryStorageType::POWER_DAY)
mPgPeriod = value * HISTORY_DATA_ARR_LENGTH;
}
}
return;
}
if (2 == valueType) {
if (historyType == HistoryStorageType::POWER)
mLastValueTs = value;
if (historyType == HistoryStorageType::POWER_DAY)
mPgEndTime = value;
}
}
#endif
private: private:
void addValue(storage_t *s, uint16_t value) { void addValue(storage_t *s, uint16_t value) {
if (s->wrapped) // after 1st time array wrap we have to increase the display index
s->dispIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH);
s->data[s->listIdx] = value; s->data[s->listIdx] = value;
s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH);
if (s->listIdx == 0) }
s->wrapped = true;
void addValueDay(storage_t *s, uint16_t value) {
DPRINTLN(DBG_VERBOSE, F("addValueDay ") + String(value));
bool storeStartEndTimes = false;
bool store_entry = false;
uint32_t pGraphStartTime = mApp->getSunrise();
uint32_t pGraphEndTime = mApp->getSunset();
uint32_t utcTs = mApp->getTimestamp();
switch (mPgState) {
case PowerGraphState::NO_TIME_SYNC:
if ((pGraphStartTime > 0)
&& (pGraphEndTime > 0) // wait until period data is available ...
&& (utcTs >= pGraphStartTime)
&& (utcTs < pGraphEndTime)) // and current time is in period
{
storeStartEndTimes = true; // period was received -> store
store_entry = true;
mPgState = PowerGraphState::IN_PERIOD;
}
break;
case PowerGraphState::IN_PERIOD:
if (utcTs > mPgEndTime) // check if end of day is reached ...
mPgState = PowerGraphState::WAIT_4_NEW_PERIOD; // then wait for new period setting
else
store_entry = true;
break;
case PowerGraphState::WAIT_4_NEW_PERIOD:
if ((mPgStartTime != pGraphStartTime) || (mPgEndTime != pGraphEndTime)) { // wait until new time period was received ...
storeStartEndTimes = true; // and store it for next period
mPgState = PowerGraphState::WAIT_4_RESTART;
}
break;
case PowerGraphState::WAIT_4_RESTART:
if ((utcTs >= mPgStartTime) && (utcTs < mPgEndTime)) { // wait until current time is in period again ...
mCurPwrDay.reset(); // then reset power graph data
store_entry = true;
mPgState = PowerGraphState::IN_PERIOD;
mCurPwr.reset(); // also reset "last values" graph
mMaximumDay = 0; // and the maximum of the (last) day
}
break;
}
// store start and end times of current time period and calculate period length
if (storeStartEndTimes) {
mPgStartTime = pGraphStartTime;
mPgEndTime = pGraphEndTime;
mPgPeriod = pGraphEndTime - pGraphStartTime; // time period of power graph in sec for scaling of x-axis
}
if (store_entry) {
DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry") + String(value));
if (mPgPeriod) {
uint16_t pgPos = (utcTs - mPgStartTime) * (HISTORY_DATA_ARR_LENGTH - 1) / mPgPeriod;
s->listIdx = std::min(pgPos, (uint16_t)(HISTORY_DATA_ARR_LENGTH - 1));
} else
s->listIdx = 0;
DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry idx=") + String(s->listIdx));
s->data[s->listIdx] = std::max(s->data[s->listIdx], value); // update current datapoint to maximum of all seen values
}
} }
private: private:
@ -109,8 +275,23 @@ class HistoryData {
uint32_t *mTs = nullptr; uint32_t *mTs = nullptr;
storage_t mCurPwr; storage_t mCurPwr;
storage_t mCurPwrDay;
#if defined(ENABLE_HISTORY_YIELD_PER_DAY)
storage_t mYieldDay;
#endif
bool mDayStored = false; bool mDayStored = false;
uint16_t mMaximumDay = 0; uint16_t mMaximumDay = 0;
uint32_t mLastValueTs = 0;
enum class PowerGraphState {
NO_TIME_SYNC,
IN_PERIOD,
WAIT_4_NEW_PERIOD,
WAIT_4_RESTART
};
PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC;
uint32_t mPgStartTime = 0;
uint32_t mPgEndTime = 0;
uint32_t mPgPeriod = 0; // seconds
}; };
#endif /*ENABLE_HISTORY*/ #endif /*ENABLE_HISTORY*/

44
src/plugins/plugin_lang.h

@ -0,0 +1,44 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __PLUGIN_LANG_H__
#define __PLUGIN_LANG_H__
#ifdef LANG_DE
#define STR_MONTHNAME_3_CHAR_LIST "ErrJanFebMrzAprMaiJunJulAugSepOktNovDez"
#define STR_DAYNAME_3_CHAR_LIST "ErrSonMonDieMitDonFreSam"
#define STR_OFFLINE "aus"
#define STR_ONLINE "aktiv"
#define STR_NO_INVERTER "kein inverter"
#define STR_NO_WIFI "WLAN nicht verbunden"
#define STR_VERSION "Version"
#define STR_ACTIVE_INVERTERS "aktive WR"
#define STR_TODAY "heute"
#define STR_TOTAL "Gesamt"
#elif LANG_FR
#define STR_MONTHNAME_3_CHAR_LIST "ErrJanFevMarAvrMaiJunJulAouSepOctNovDec"
#define STR_DAYNAME_3_CHAR_LIST "ErrDimLunMarMerJeuVenSam"
#define STR_OFFLINE "eteint"
#define STR_ONLINE "online"
#define STR_NO_INVERTER "pas d'onduleur"
#define STR_NO_WIFI "WiFi not connected"
#define STR_VERSION "Version"
#define STR_ACTIVE_INVERTERS "active Inv"
#define STR_TODAY "today"
#define STR_TOTAL "total"
#else
#define STR_MONTHNAME_3_CHAR_LIST "ErrJanFebMarAprMayJunJulAugSepOctNovDec"
#define STR_DAYNAME_3_CHAR_LIST "ErrSunMonTueWedThuFriSat"
#define STR_OFFLINE "offline"
#define STR_ONLINE "online"
#define STR_NO_INVERTER "no inverter"
#define STR_NO_WIFI "WiFi not connected"
#define STR_VERSION "Version"
#define STR_ACTIVE_INVERTERS "active Inv"
#define STR_TODAY "today"
#define STR_TOTAL "total"
#endif
#endif /*__PLUGIN_LANG_H__*/

149
src/publisher/pubMqtt.h

@ -11,6 +11,8 @@
#if defined(ENABLE_MQTT) #if defined(ENABLE_MQTT)
#ifdef ESP8266 #ifdef ESP8266
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#define xSemaphoreTake(a, b) { while(a) { yield(); } a = true; }
#define xSemaphoreGive(a) { a = false; }
#elif defined(ESP32) #elif defined(ESP32)
#include <WiFi.h> #include <WiFi.h>
#endif #endif
@ -39,6 +41,13 @@ template<class HMSYSTEM>
class PubMqtt { class PubMqtt {
public: public:
PubMqtt() : SendIvData() { PubMqtt() : SendIvData() {
#if defined(ESP32)
mutex = xSemaphoreCreateBinaryStatic(&mutexBuffer);
xSemaphoreGive(mutex);
#else
mutex = false;
#endif
mLastIvState.fill(InverterStatus::OFF); mLastIvState.fill(InverterStatus::OFF);
mIvLastRTRpub.fill(0); mIvLastRTRpub.fill(0);
@ -50,9 +59,14 @@ class PubMqtt {
mSendAlarm.fill(false); mSendAlarm.fill(false);
} }
~PubMqtt() { } ~PubMqtt() {
#if defined(ESP32)
vSemaphoreDelete(mutex);
#endif
}
void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *uptime) { void setup(IApp *app, cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *uptime) {
mApp = app;
mCfgMqtt = cfg_mqtt; mCfgMqtt = cfg_mqtt;
mDevName = devName; mDevName = devName;
mVersion = version; mVersion = version;
@ -61,7 +75,7 @@ class PubMqtt {
mUptime = uptime; mUptime = uptime;
mIntervalTimeout = 1; mIntervalTimeout = 1;
SendIvData.setup(sys, utcTs, &mSendList); SendIvData.setup(app, sys, cfg_mqtt, utcTs, &mSendList);
SendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained, uint8_t qos) { SendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained, uint8_t qos) {
publish(subTopic, payload, retained, true, qos); publish(subTopic, payload, retained, true, qos);
}); });
@ -95,6 +109,17 @@ class PubMqtt {
} }
void loop() { void loop() {
std::queue<message_s> queue;
xSemaphoreTake(mutex, portMAX_DELAY);
queue.swap(mReceiveQueue);
xSemaphoreGive(mutex);
while (!queue.empty()) {
message_s *entry = &queue.front();
handleMessage(entry->topic, entry->payload, entry->len, entry->index, entry->total);
queue.pop();
}
SendIvData.loop(); SendIvData.loop();
#if defined(ESP8266) #if defined(ESP8266)
@ -204,6 +229,9 @@ class PubMqtt {
else else
snprintf(mTopic.data(), mTopic.size(), "%s", subTopic); snprintf(mTopic.data(), mTopic.size(), "%s", subTopic);
if(!mCfgMqtt->enableRetain)
retained = false;
mClient.publish(mTopic.data(), qos, retained, payload); mClient.publish(mTopic.data(), qos, retained, payload);
yield(); yield();
mTxCnt++; mTxCnt++;
@ -242,7 +270,8 @@ class PubMqtt {
void setPowerLimitAck(Inverter<> *iv) { void setPowerLimitAck(Inverter<> *iv) {
if (NULL != iv) { if (NULL != iv) {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]);
publish(mSubTopic.data(), "true", true, true, QOS_2); snprintf(mVal.data(), mVal.size(), "%.1f", iv->powerLimit[0]/10.0);
publish(mSubTopic.data(), mVal.data(), true, true, QOS_2);
} }
} }
@ -250,16 +279,15 @@ class PubMqtt {
void onConnect(bool sessionPreset) { void onConnect(bool sessionPreset) {
DPRINTLN(DBG_INFO, F("MQTT connected")); DPRINTLN(DBG_INFO, F("MQTT connected"));
publish(subtopics[MQTT_VERSION], mVersion, true); publish(subtopics[MQTT_VERSION], mVersion, false);
publish(subtopics[MQTT_DEVICE], mDevName, true); publish(subtopics[MQTT_DEVICE], mDevName, false);
#if defined(ETHERNET) publish(subtopics[MQTT_IP_ADDR], mApp->getIp().c_str(), true);
publish(subtopics[MQTT_IP_ADDR], ETH.localIP().toString().c_str(), true);
#else
publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true);
#endif
tickerMinute(); tickerMinute();
publish(mLwtTopic.data(), mqttStr[MQTT_STR_LWT_CONN], true, false); publish(mLwtTopic.data(), mqttStr[MQTT_STR_LWT_CONN], true, false);
snprintf(mVal.data(), mVal.size(), "ctrl/restart_ahoy");
subscribe(mVal.data());
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
snprintf(mVal.data(), mVal.size(), "ctrl/limit/%d", i); snprintf(mVal.data(), mVal.size(), "ctrl/limit/%d", i);
subscribe(mVal.data(), QOS_2); subscribe(mVal.data(), QOS_2);
@ -300,6 +328,14 @@ class PubMqtt {
void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
if(len == 0) if(len == 0)
return; return;
xSemaphoreTake(mutex, portMAX_DELAY);
mReceiveQueue.push(message_s(topic, payload, len, index, total));
xSemaphoreGive(mutex);
}
inline void handleMessage(const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]); DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]);
DBGPRINTLN(String(topic)); DBGPRINTLN(String(topic));
if(NULL == mSubscriptionCb) if(NULL == mSubscriptionCb)
@ -353,9 +389,9 @@ class PubMqtt {
pos++; pos++;
} }
/*char out[128]; char out[128];
serializeJson(root, out, 128); serializeJson(root, out, 128);
DPRINTLN(DBG_INFO, "json: " + String(out));*/ DPRINTLN(DBG_INFO, "json: " + String(out));
(mSubscriptionCb)(root); (mSubscriptionCb)(root);
mRxCnt++; mRxCnt++;
@ -397,6 +433,10 @@ class PubMqtt {
std::array<char, 32> name; std::array<char, 32> name;
std::array<char, 32> uniq_id; std::array<char, 32> uniq_id;
std::array<char, 350> buf; std::array<char, 350> buf;
topic.fill(0);
name.fill(0);
uniq_id.fill(0);
buf.fill(0);
const char *devCls, *stateCls; const char *devCls, *stateCls;
if (!total) { if (!total) {
if (rec->assign[mDiscovery.sub].ch == CH0) if (rec->assign[mDiscovery.sub].ch == CH0)
@ -419,8 +459,8 @@ class PubMqtt {
} }
DynamicJsonDocument doc2(512); DynamicJsonDocument doc2(512);
constexpr static char* unitTotal[] = {"W", "kWh", "Wh", "W"}; constexpr static const char* unitTotal[] = {"W", "kWh", "Wh", "W"};
doc2[F("name")] = name; doc2[F("name")] = String(name.data());
doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic.data()); doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic.data());
doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub])); doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub]));
doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id.data(); doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id.data();
@ -437,9 +477,9 @@ class PubMqtt {
else // total values else // total values
snprintf(topic.data(), topic.size(), "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(), fields[fldTotal[mDiscovery.sub]]); snprintf(topic.data(), topic.size(), "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(), fields[fldTotal[mDiscovery.sub]]);
size_t size = measureJson(doc2) + 1; size_t size = measureJson(doc2) + 1;
buf.fill(0);
serializeJson(doc2, buf.data(), size); serializeJson(doc2, buf.data(), size);
publish(topic.data(), buf.data(), true, false); if(FLD_EVT != rec->assign[mDiscovery.sub].fieldId)
publish(topic.data(), buf.data(), true, false);
if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) { if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) {
mDiscovery.sub = 0; mDiscovery.sub = 0;
@ -559,6 +599,9 @@ class PubMqtt {
} }
void sendData(Inverter<> *iv, uint8_t curInfoCmd) { void sendData(Inverter<> *iv, uint8_t curInfoCmd) {
if (mCfgMqtt->json)
return;
record_t<> *rec = iv->getRecordStruct(curInfoCmd); record_t<> *rec = iv->getRecordStruct(curInfoCmd);
uint32_t lastTs = iv->getLastTs(rec); uint32_t lastTs = iv->getLastTs(rec);
@ -602,10 +645,80 @@ class PubMqtt {
mLastAnyAvail = anyAvail; mLastAnyAvail = anyAvail;
} }
private:
enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
struct message_s
{
char *topic;
uint8_t *payload;
size_t len;
size_t index;
size_t total;
message_s()
: topic { nullptr }
, payload { nullptr }
, len { 0 }
, index { 0 }
, total { 0 }
{}
message_s(const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
uint8_t topic_len = strlen(topic) + 1;
this->topic = new char[topic_len];
this->payload = new uint8_t[len];
memcpy(this->topic, topic, topic_len);
memcpy(this->payload, payload, len);
this->len = len;
this->index = index;
this->total = total;
}
message_s(const message_s &) = delete;
message_s(message_s && other) : message_s {}
{
this->swap( other );
}
~message_s()
{
delete[] this->topic;
delete[] this->payload;
}
message_s &operator = (const message_s &) = delete;
message_s &operator = (message_s &&other)
{
this->swap(other);
return *this;
}
void swap(message_s &other)
{
std::swap(this->topic, other.topic);
std::swap(this->payload, other.payload);
std::swap(this->len, other.len);
std::swap(this->index, other.index);
std::swap(this->total, other.total);
}
};
private:
espMqttClient mClient; espMqttClient mClient;
cfgMqtt_t *mCfgMqtt = nullptr; cfgMqtt_t *mCfgMqtt = nullptr;
IApp *mApp;
#if defined(ESP8266) #if defined(ESP8266)
WiFiEventHandler mHWifiCon, mHWifiDiscon; WiFiEventHandler mHWifiCon, mHWifiDiscon;
volatile bool mutex;
#else
SemaphoreHandle_t mutex;
StaticSemaphore_t mutexBuffer;
#endif #endif
HMSYSTEM *mSys = nullptr; HMSYSTEM *mSys = nullptr;
@ -621,6 +734,8 @@ class PubMqtt {
std::array<uint32_t, MAX_NUM_INVERTERS> mIvLastRTRpub; std::array<uint32_t, MAX_NUM_INVERTERS> mIvLastRTRpub;
uint16_t mIntervalTimeout = 0; uint16_t mIntervalTimeout = 0;
std::queue<message_s> mReceiveQueue;
// last will topic and payload must be available through lifetime of 'espMqttClient' // last will topic and payload must be available through lifetime of 'espMqttClient'
std::array<char, (MQTT_TOPIC_LEN + 5)> mLwtTopic; std::array<char, (MQTT_TOPIC_LEN + 5)> mLwtTopic;
const char *mDevName = nullptr, *mVersion = nullptr; const char *mDevName = nullptr, *mVersion = nullptr;

96
src/publisher/pubMqttIvData.h

@ -24,8 +24,10 @@ class PubMqttIvData {
public: public:
PubMqttIvData() : mTotal{}, mSubTopic{}, mVal{} {} PubMqttIvData() : mTotal{}, mSubTopic{}, mVal{} {}
void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) { void setup(IApp *app, HMSYSTEM *sys, cfgMqtt_t *cfg_mqtt, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) {
mApp = app;
mSys = sys; mSys = sys;
mCfg = cfg_mqtt;
mUtcTimestamp = utcTs; mUtcTimestamp = utcTs;
mSendList = sendList; mSendList = sendList;
mState = IDLE; mState = IDLE;
@ -75,6 +77,7 @@ class PubMqttIvData {
mTotalFound = false; mTotalFound = false;
mSendTotalYd = true; mSendTotalYd = true;
mAllTotalFound = true; mAllTotalFound = true;
mAtLeastOneWasntSent = false;
if(!mSendList->empty()) { if(!mSendList->empty()) {
mCmd = mSendList->front().cmd; mCmd = mSendList->front().cmd;
mIvSend = mSendList->front().iv; mIvSend = mSendList->front().iv;
@ -113,7 +116,7 @@ class PubMqttIvData {
mPublish(mSubTopic.data(), mVal.data(), true, QOS_0); mPublish(mSubTopic.data(), mVal.data(), true, QOS_0);
if((mIv->ivGen == IV_HMS) || (mIv->ivGen == IV_HMT)) { if((mIv->ivGen == IV_HMS) || (mIv->ivGen == IV_HMT)) {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch0/rssi", mIv->config->name); snprintf(mSubTopic.data(), mSubTopic.size(), "%s/rssi", mIv->config->name);
snprintf(mVal.data(), mVal.size(), "%d", mIv->rssi); snprintf(mVal.data(), mVal.size(), "%d", mIv->rssi);
mPublish(mSubTopic.data(), mVal.data(), false, QOS_0); mPublish(mSubTopic.data(), mVal.data(), false, QOS_0);
} }
@ -122,7 +125,7 @@ class PubMqttIvData {
mIv->isProducing(); // recalculate status mIv->isProducing(); // recalculate status
mState = SEND_DATA; mState = SEND_DATA;
} else if(mSendTotals && mTotalFound) { } else if(mSendTotals && mTotalFound && mAtLeastOneWasntSent) {
if(mYldTotalStore > mTotal[2]) if(mYldTotalStore > mTotal[2])
mSendTotalYd = false; // don't send yield total if last value was greater mSendTotalYd = false; // don't send yield total if last value was greater
else else
@ -167,9 +170,6 @@ class PubMqttIvData {
case FLD_PDC: case FLD_PDC:
mTotal[3] += mIv->getValue(mPos, rec); mTotal[3] += mIv->getValue(mPos, rec);
break; break;
case FLD_MP:
mTotal[4] += mIv->getValue(mPos, rec);
break;
} }
} else } else
mAllTotalFound = false; mAllTotalFound = false;
@ -177,6 +177,7 @@ class PubMqttIvData {
} }
if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) {
mAtLeastOneWasntSent = true;
if(InverterDevInform_All == mCmd) { if(InverterDevInform_All == mCmd) {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/firmware", mIv->config->name); 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}", snprintf(mVal.data(), mVal.size(), "{\"version\":%d,\"build_year\":\"%d\",\"build_month_day\":%d,\"build_hour_min\":%d,\"bootloader\":%d}",
@ -185,7 +186,6 @@ class PubMqttIvData {
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_FW_BUILD_MONTH_DAY, 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_FW_BUILD_HOUR_MINUTE, rec)),
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_BOOTLOADER_VER, rec))); static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_BOOTLOADER_VER, rec)));
retained = true;
} else if(InverterDevInform_Simple == mCmd) { } else if(InverterDevInform_Simple == mCmd) {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/hardware", mIv->config->name); 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}", snprintf(mVal.data(), mVal.size(), "{\"part\":%d,\"version\":\"%d\",\"grid_profile_code\":%d,\"grid_profile_version\":%d}",
@ -193,20 +193,54 @@ class PubMqttIvData {
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_HW_VERSION, 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_CODE, rec)),
static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_GRID_PROFILE_VERSION, rec))); static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_GRID_PROFILE_VERSION, rec)));
retained = true;
} else { } else {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); if (!mCfg->json) {
snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec))); 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)));
} else {
if (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) {
uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0;
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", mIv->config->name, fields[rec->assign[mPos].fieldId]);
snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec)));
mPublish(mSubTopic.data(), mVal.data(), retained, qos);
}
}
} }
uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0; if ((InverterDevInform_All == mCmd) || (InverterDevInform_Simple == mCmd) || !mCfg->json)
if((FLD_EVT != rec->assign[mPos].fieldId) {
&& (FLD_LAST_ALARM_CODE != rec->assign[mPos].fieldId)) uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0;
mPublish(mSubTopic.data(), mVal.data(), 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++; mPos++;
} else { } else {
if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) {
if (mCfg->json && (RealTimeRunData_Debug == mCmd)) {
DynamicJsonDocument doc(300);
for (mPos = 0; mPos < rec->length; mPos++) {
doc[fields[rec->assign[mPos].fieldId]] = ah::round3(mIv->getValue(mPos, rec));
bool publish = false;
if (mPos != (rec->length - 1)) { // not last one
if (rec->assign[mPos].ch != rec->assign[mPos+1].ch)
publish = true;
} else
publish = true;
if (publish) {
doc[F("ts")] = rec->ts;
// if next channel or end->publish
serializeJson(doc, mVal.data(), mVal.size());
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d", mIv->config->name, rec->assign[mPos].ch);
mPublish(mSubTopic.data(), mVal.data(), false, QOS_0);
doc.clear();
}
}
}
sendRadioStat(rec->length); sendRadioStat(rec->length);
rec->mqttSentStatus = MqttSentStatus::DATA_SENT; rec->mqttSentStatus = MqttSentStatus::DATA_SENT;
} }
@ -220,7 +254,7 @@ class PubMqttIvData {
mIv->radioStatistics.txCnt, mIv->radioStatistics.txCnt,
mIv->radioStatistics.rxSuccess, mIv->radioStatistics.rxSuccess,
mIv->radioStatistics.rxFail, mIv->radioStatistics.rxFail,
mIv->radioStatistics.rxFailNoAnser, mIv->radioStatistics.rxFailNoAnswer,
mIv->radioStatistics.retransmits, mIv->radioStatistics.retransmits,
mIv->radioStatistics.ivLoss, mIv->radioStatistics.ivLoss,
mIv->radioStatistics.ivSent, mIv->radioStatistics.ivSent,
@ -261,19 +295,40 @@ class PubMqttIvData {
case 4: case 4:
fieldId = FLD_MP; fieldId = FLD_MP;
retained = false; retained = false;
mTotal[4] = mApp->getTotalMaxPower();
break; break;
} }
snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]); if (!mCfg->json) {
snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos])); snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]);
mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0); snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos]));
mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0);
}
mPos++; mPos++;
} else { } else {
if (mCfg->json) {
int type[5] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC, FLD_MP};
snprintf(mVal.data(), mVal.size(), "{");
for (mPos = 0; mPos < 5; mPos++) {
snprintf(mSubTopic.data(), mSubTopic.size(), "\"%s\":%g", fields[type[mPos]], ah::round3(mTotal[mPos]));
strcat(mVal.data(), mSubTopic.data());
if (mPos < 4)
strcat(mVal.data(), ",");
else
strcat(mVal.data(), "}");
}
mPublish("total", mVal.data(), true, QOS_0);
}
mSendList->pop(); mSendList->pop();
mSendTotals = false; mSendTotals = false;
mState = IDLE; mState = IDLE;
} }
} }
private:
IApp *mApp = nullptr;
cfgMqtt_t *mCfg = nullptr;
HMSYSTEM *mSys = nullptr; HMSYSTEM *mSys = nullptr;
uint32_t *mUtcTimestamp = nullptr; uint32_t *mUtcTimestamp = nullptr;
pubMqttPublisherType mPublish; pubMqttPublisherType mPublish;
@ -282,7 +337,8 @@ class PubMqttIvData {
uint8_t mCmd = 0; uint8_t mCmd = 0;
uint8_t mLastIvId = 0; uint8_t mLastIvId = 0;
bool mSendTotals = false, mTotalFound = false, mAllTotalFound = false, mSendTotalYd = false; bool mSendTotals = false, mTotalFound = false, mAllTotalFound = false;
bool mSendTotalYd = false, mAtLeastOneWasntSent = false;
float mTotal[5], mYldTotalStore = 0; float mTotal[5], mYldTotalStore = 0;
Inverter<> *mIv = nullptr, *mIvSend = nullptr; Inverter<> *mIv = nullptr, *mIvSend = nullptr;
@ -290,7 +346,7 @@ class PubMqttIvData {
bool mRTRDataHasBeenSent = false; bool mRTRDataHasBeenSent = false;
std::array<char, (32 + MAX_NAME_LENGTH + 1)> mSubTopic; std::array<char, (32 + MAX_NAME_LENGTH + 1)> mSubTopic;
std::array<char, 140> mVal; std::array<char, 300> mVal;
std::queue<sendListCmdIv> *mSendList = nullptr; std::queue<sendListCmdIv> *mSendList = nullptr;
}; };

2
src/utils/dbg.h

@ -110,7 +110,7 @@
#if DEBUG_LEVEL >= DBG_ERROR #if DEBUG_LEVEL >= DBG_ERROR
#define PERR(str) DBGPRINT(F("E: ")); DBGPRINT(str); #define PERR(str) DBGPRINT(F("E: ")); DBGPRINT(str);
#define PERRLN(str) DBGPRINT(F("E: ")); DBGPRINTLN(str); #define PERRLN(str) DBGPRINT(F("E: ")); DBGPRINTLN(str); DSERIAL.flush();
#else #else
#define PERR(str) #define PERR(str)
#define PERRLN(str) #define PERRLN(str)

35
src/utils/helper.cpp

@ -5,6 +5,12 @@
#include "helper.h" #include "helper.h"
#include "dbg.h" #include "dbg.h"
#include "../plugins/plugin_lang.h"
#define dt_SHORT_STR_LEN_i18n 3 // the length of short strings
static char buffer_i18n[dt_SHORT_STR_LEN_i18n + 1]; // must be big enough for longest string and the terminating null
const char monthShortNames_P[] PROGMEM = STR_MONTHNAME_3_CHAR_LIST;
const char dayShortNames_P[] PROGMEM = STR_DAYNAME_3_CHAR_LIST;
namespace ah { namespace ah {
void ip2Arr(uint8_t ip[], const char *ipStr) { void ip2Arr(uint8_t ip[], const char *ipStr) {
@ -28,6 +34,10 @@ namespace ah {
snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
} }
double round1(double value) {
return (int)(value * 10 + 0.5) / 10.0;
}
double round3(double value) { double round3(double value) {
return (int)(value * 1000 + 0.5) / 1000.0; return (int)(value * 1000 + 0.5) / 1000.0;
} }
@ -82,6 +92,31 @@ namespace ah {
return String(str); return String(str);
} }
static char* monthShortStr_i18n(uint8_t month) {
for (int i=0; i < dt_SHORT_STR_LEN_i18n; i++)
buffer_i18n[i] = pgm_read_byte(&(monthShortNames_P[i + month * dt_SHORT_STR_LEN_i18n]));
buffer_i18n[dt_SHORT_STR_LEN_i18n] = 0;
return buffer_i18n;
}
static char* dayShortStr_i18n(uint8_t day) {
for (int i=0; i < dt_SHORT_STR_LEN_i18n; i++)
buffer_i18n[i] = pgm_read_byte(&(dayShortNames_P[i + day * dt_SHORT_STR_LEN_i18n]));
buffer_i18n[dt_SHORT_STR_LEN_i18n] = 0;
return buffer_i18n;
}
String getDateTimeStrShort_i18n(time_t t) {
char str[20];
if(0 == t)
sprintf(str, "n/a");
else {
sprintf(str, "%3s ", dayShortStr_i18n(dayOfWeek(t)));
sprintf(str+4, "%2d.%3s %02d:%02d", day(t), monthShortStr_i18n(month(t)), hour(t), minute(t));
}
return String(str);
}
uint64_t Serial2u64(const char *val) { uint64_t Serial2u64(const char *val) {
char tmp[3]; char tmp[3];
uint64_t ret = 0ULL; uint64_t ret = 0ULL;

2
src/utils/helper.h

@ -39,9 +39,11 @@ static Timezone gTimezone(CEST, CET);
namespace ah { namespace ah {
void ip2Arr(uint8_t ip[], const char *ipStr); void ip2Arr(uint8_t ip[], const char *ipStr);
void ip2Char(uint8_t ip[], char *str); void ip2Char(uint8_t ip[], char *str);
double round1(double value);
double round3(double value); double round3(double value);
String getDateTimeStr(time_t t); String getDateTimeStr(time_t t);
String getDateTimeStrShort(time_t t); String getDateTimeStrShort(time_t t);
String getDateTimeStrShort_i18n(time_t t);
String getDateTimeStrFile(time_t t); String getDateTimeStrFile(time_t t);
String getTimeStr(time_t t); String getTimeStr(time_t t);
String getTimeStrMs(uint64_t t); String getTimeStrMs(uint64_t t);

8
src/utils/improv.h

@ -147,10 +147,12 @@ class Improv {
} }
void getNetworks(void) { void getNetworks(void) {
if(!mScanRunning)
mApp->scanAvailNetworks();
JsonObject obj; JsonObject obj;
if(!mScanRunning) {
mApp->getAvailNetworks(obj);
return;
}
if(!mApp->getAvailNetworks(obj)) if(!mApp->getAvailNetworks(obj))
return; return;

16
src/utils/scheduler.h

@ -35,14 +35,21 @@ namespace ah {
mMax = 0; mMax = 0;
mPrevMillis = millis(); mPrevMillis = millis();
mTsMillis = mPrevMillis % 1000; mTsMillis = mPrevMillis % 1000;
mFastTicker = false;
resetTicker(); resetTicker();
} }
virtual void loop(void) { virtual void loop(void) {
mMillis = millis(); mMillis = millis();
mDiff = mMillis - mPrevMillis; mDiff = mMillis - mPrevMillis;
if (mDiff < 1000) if (mDiff < 1000) {
if (mFastTicker) {
mDiffSeconds = 0;
checkTicker();
mFastTicker = false;
}
return; return;
}
mDiffSeconds = 1; mDiffSeconds = 1;
if (mDiff < 2000) if (mDiff < 2000)
@ -125,8 +132,10 @@ namespace ah {
mTicker[i].timeout = timeout; mTicker[i].timeout = timeout;
mTicker[i].reload = reload; mTicker[i].reload = reload;
mTicker[i].isTimestamp = isTimestamp; mTicker[i].isTimestamp = isTimestamp;
memset(mTicker[i].name, 0, 6); strncpy(mTicker[i].name, name, 5);
strncpy(mTicker[i].name, name, (strlen(name) < 6) ? strlen(name) : 5); mTicker[i].name[5]=0;
if (timeout == 0 && reload == false)
mFastTicker = true;
if(mMax == i) if(mMax == i)
mMax = i + 1; mMax = i + 1;
return i; return i;
@ -162,6 +171,7 @@ namespace ah {
uint32_t mMillis = 0, mPrevMillis = 0, mDiff = 0; uint32_t mMillis = 0, mPrevMillis = 0, mDiff = 0;
uint8_t mDiffSeconds = 0; uint8_t mDiffSeconds = 0;
uint8_t mMax = 0; uint8_t mMax = 0;
bool mFastTicker;
}; };
} }

3
src/utils/spiPatcher.cpp

@ -5,5 +5,6 @@
#if defined(ESP32) #if defined(ESP32)
#include "spiPatcher.h" #include "spiPatcher.h"
SpiPatcher *SpiPatcher::mInstance = nullptr; SpiPatcher *SpiPatcher::InstanceHost2 = nullptr;
SpiPatcher *SpiPatcher::InstanceHost3 = nullptr;
#endif #endif

78
src/utils/spiPatcher.h

@ -9,23 +9,38 @@
#if defined(ESP32) #if defined(ESP32)
#include "dbg.h"
#include "spiPatcherHandle.h" #include "spiPatcherHandle.h"
#include <driver/spi_master.h> #include <driver/spi_master.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#if (SOC_SPI_PERIPH_NUM > 2)
#define SPI_HOST_OTHER SPI3_HOST
#else
#define SPI_HOST_OTHER SPI2_HOST
#endif
class SpiPatcher { class SpiPatcher {
protected: protected:
explicit SpiPatcher(spi_host_device_t dev) : explicit SpiPatcher(spi_host_device_t dev) :
mHostDevice(dev), mCurHandle(nullptr) { mCurHandle(nullptr) {
// Use binary semaphore instead of mutex for performance reasons // Use binary semaphore instead of mutex for performance reasons
mutex = xSemaphoreCreateBinaryStatic(&mutex_buffer); mutex = xSemaphoreCreateBinaryStatic(&mutex_buffer);
xSemaphoreGive(mutex); xSemaphoreGive(mutex);
mDev = dev;
mBusState = ESP_FAIL;
}
spi_bus_config_t buscfg = { public:
.mosi_io_num = -1, SpiPatcher(const SpiPatcher &other) = delete;
.miso_io_num = -1, void operator=(const SpiPatcher &) = delete;
.sclk_io_num = -1,
esp_err_t initBus(int mosi = -1, int miso = -1, int sclk = -1, spi_common_dma_t dmaType = SPI_DMA_DISABLED) {
mBusConfig = spi_bus_config_t {
.mosi_io_num = mosi,
.miso_io_num = miso,
.sclk_io_num = sclk,
.quadwp_io_num = -1, .quadwp_io_num = -1,
.quadhd_io_num = -1, .quadhd_io_num = -1,
.data4_io_num = -1, .data4_io_num = -1,
@ -36,26 +51,48 @@ class SpiPatcher {
.flags = 0, .flags = 0,
.intr_flags = 0 .intr_flags = 0
}; };
ESP_ERROR_CHECK(spi_bus_initialize(mHostDevice, &buscfg, SPI_DMA_DISABLED)); ESP_ERROR_CHECK((mBusState = spi_bus_initialize(mDev, &mBusConfig, dmaType)));
}
public: return mBusState;
SpiPatcher(SpiPatcher &other) = delete; }
void operator=(const SpiPatcher &) = delete;
static SpiPatcher* getInstance(spi_host_device_t dev) { static SpiPatcher* getInstance(spi_host_device_t dev, bool initialize = true) {
if(nullptr == mInstance) if(SPI2_HOST == dev) {
mInstance = new SpiPatcher(dev); if(nullptr == InstanceHost2) {
return mInstance; InstanceHost2 = new SpiPatcher(dev);
if(initialize)
InstanceHost2->initBus();
}
return InstanceHost2;
} else { // SPI3_HOST
if(nullptr == InstanceHost3) {
InstanceHost3 = new SpiPatcher(dev);
if(initialize)
InstanceHost3->initBus();
}
return InstanceHost3;
}
} }
~SpiPatcher() { vSemaphoreDelete(mutex); } ~SpiPatcher() { vSemaphoreDelete(mutex); }
spi_host_device_t getDevice() { inline void addDevice(spi_host_device_t host_id, const spi_device_interface_config_t *dev_config, spi_device_handle_t *handle) {
return mHostDevice; assert(mBusState == ESP_OK);
if(SPI2_HOST == host_id)
mHost2Cnt++;
#if (SOC_SPI_PERIPH_NUM > 2)
if(SPI3_HOST == host_id)
mHost3Cnt++;
#endif
if((mHost2Cnt > 3) || (mHost3Cnt > 3))
DPRINTLN(DBG_ERROR, F("maximum number of SPI devices reached (3)"));
ESP_ERROR_CHECK(spi_bus_add_device(host_id, dev_config, handle));
} }
inline void request(SpiPatcherHandle* handle) { inline void request(SpiPatcherHandle* handle) {
assert(mBusState == ESP_OK);
xSemaphoreTake(mutex, portMAX_DELAY); xSemaphoreTake(mutex, portMAX_DELAY);
if (mCurHandle != handle) { if (mCurHandle != handle) {
@ -70,17 +107,22 @@ class SpiPatcher {
} }
inline void release() { inline void release() {
assert(mBusState == ESP_OK);
xSemaphoreGive(mutex); xSemaphoreGive(mutex);
} }
protected: protected:
static SpiPatcher *mInstance; static SpiPatcher *InstanceHost2;
static SpiPatcher *InstanceHost3;
private: private:
const spi_host_device_t mHostDevice;
SpiPatcherHandle* mCurHandle; SpiPatcherHandle* mCurHandle;
SemaphoreHandle_t mutex; SemaphoreHandle_t mutex;
StaticSemaphore_t mutex_buffer; StaticSemaphore_t mutex_buffer;
uint8_t mHost2Cnt = 0, mHost3Cnt = 0;
spi_host_device_t mDev = SPI2_HOST;
esp_err_t mBusState = ESP_FAIL;
spi_bus_config_t mBusConfig;
}; };
#endif /*ESP32*/ #endif /*ESP32*/

2
src/web/Protection.h

@ -24,7 +24,7 @@ class Protection {
} }
public: public:
Protection(Protection &other) = delete; Protection(const Protection &other) = delete;
void operator=(const Protection &) = delete; void operator=(const Protection &) = delete;
static Protection* getInstance(const char *pwd) { static Protection* getInstance(const char *pwd) {

534
src/web/RestApi.h

@ -17,11 +17,7 @@
#include "../utils/helper.h" #include "../utils/helper.h"
#include "lang.h" #include "lang.h"
#include "AsyncJson.h" #include "AsyncJson.h"
#if defined(ETHERNET)
#include "AsyncWebServer_ESP32_W5500.h"
#else
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
#endif
#include "plugins/history.h" #include "plugins/history.h"
@ -44,16 +40,24 @@ class RestApi {
mApp = app; mApp = app;
mSrv = srv; mSrv = srv;
mSys = sys; mSys = sys;
mRadioNrf = (HmRadio<>*)mApp->getRadioObj(true); mRadioNrf = (NrfRadio<>*)mApp->getRadioObj(true);
#if defined(ESP32) #if defined(ESP32)
mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false); mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false);
#endif #endif
mConfig = config; mConfig = config;
#if defined(ENABLE_HISTORY_LOAD_DATA)
mSrv->on("/api/addYDHist",
HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1),
std::bind(&RestApi::onApiPostYDHist,this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
#endif
mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody(
std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5));
mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1));
mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1));
#if defined(ESP32)
mSrv->on("/coredump", HTTP_GET, std::bind(&RestApi::getCoreDump, this, std::placeholders::_1));
#endif
} }
uint32_t getTimezoneOffset(void) { uint32_t getTimezoneOffset(void) {
@ -77,9 +81,19 @@ class RestApi {
#ifndef ESP32 #ifndef ESP32
mHeapFreeBlk = ESP.getMaxFreeBlockSize(); mHeapFreeBlk = ESP.getMaxFreeBlockSize();
mHeapFrag = ESP.getHeapFragmentation(); mHeapFrag = ESP.getHeapFragmentation();
#else
mHeapFreeBlk = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
if(mHeapFree > 0)
mHeapFrag = 100 - ((mHeapFreeBlk * 100) / mHeapFree);
else
mHeapFrag = 0;
#endif #endif
#if defined(ESP32)
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8000);
#else
AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000); AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000);
#endif
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
String path = request->url().substring(5); String path = request->url().substring(5);
@ -97,12 +111,13 @@ class RestApi {
else if(path == "inverter/list") getInverterList(root); else if(path == "inverter/list") getInverterList(root);
else if(path == "index") getIndex(request, root); else if(path == "index") getIndex(request, root);
else if(path == "setup") getSetup(request, root); else if(path == "setup") getSetup(request, root);
#if !defined(ETHERNET)
else if(path == "setup/networks") getNetworks(root); else if(path == "setup/networks") getNetworks(root);
else if(path == "setup/getip") getWifiIp(root); else if(path == "setup/getip") getIp(root);
#endif /* !defined(ETHERNET) */
else if(path == "live") getLive(request,root); else if(path == "live") getLive(request,root);
else if (path == "powerHistory") getPowerHistory(request, root); #if defined(ENABLE_HISTORY)
else if (path == "powerHistory") getPowerHistory(request, root, HistoryStorageType::POWER);
else if (path == "powerHistoryDay") getPowerHistory(request, root, HistoryStorageType::POWER_DAY);
#endif /*ENABLE_HISTORY*/
else { else {
if(path.substring(0, 12) == "inverter/id/") if(path.substring(0, 12) == "inverter/id/")
getInverter(root, request->url().substring(17).toInt()); getInverter(root, request->url().substring(17).toInt());
@ -137,11 +152,98 @@ class RestApi {
#endif #endif
} }
void onApiPostBody(AsyncWebServerRequest *request, const uint8_t *data, size_t len, size_t index, size_t total) { #if defined(ENABLE_HISTORY_LOAD_DATA)
// VArt67: For debugging history graph. Loading data into graph
void onApiPostYDHist(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, size_t final) {
uint32_t total = request->contentLength();
DPRINTLN(DBG_DEBUG, "[onApiPostYieldDHistory ] " + filename + " index:" + index + " len:" + len + " total:" + total + " final:" + final);
if (0 == index) {
if (NULL != mTmpBuf)
delete[] mTmpBuf;
mTmpBuf = new uint8_t[total + 1];
mTmpSize = total;
}
if (mTmpSize >= (len + index))
memcpy(&mTmpBuf[index], data, len);
if (!final)
return; // not last frame - nothing to do
mTmpSize = len + index; // correct the total size
mTmpBuf[mTmpSize] = 0;
#ifndef ESP32
DynamicJsonDocument json(ESP.getMaxFreeBlockSize() - 512); // need some memory on heap
#else
DynamicJsonDocument json(12000); // does this work? I have no ESP32 :-(
#endif
DeserializationError err = deserializeJson(json, static_cast<const char *>(mTmpBuf, mTmpSize));
json.shrinkToFit();
JsonObject obj = json.as<JsonObject>();
// Debugging
// mTmpBuf[mTmpSize] = 0;
// DPRINTLN(DBG_DEBUG, (const char *)mTmpBuf);
if (!err && obj) {
// insert data into yieldDayHistory object
HistoryStorageType dataType;
if (obj["maxDay"] > 0) // this is power history data
{
dataType = HistoryStorageType::POWER;
if (obj["refresh"] > 60)
dataType = HistoryStorageType::POWER_DAY;
}
else
dataType = HistoryStorageType::YIELD;
size_t cnt = obj[F("value")].size();
DPRINTLN(DBG_DEBUG, "ArraySize: " + String(cnt));
for (uint16_t i = 0; i < cnt; i++) {
uint16_t val = obj[F("value")][i];
mApp->addValueToHistory((uint8_t)dataType, 0, val);
// DPRINT(DBG_VERBOSE, "value " + String(i) + ": " + String(val) + ", ");
}
uint32_t refresh = obj[F("refresh")];
mApp->addValueToHistory((uint8_t)dataType, 1, refresh);
if (dataType != HistoryStorageType::YIELD) {
uint32_t ts = obj[F("lastValueTs")];
mApp->addValueToHistory((uint8_t)dataType, 2, ts);
}
} else {
switch (err.code()) {
case DeserializationError::Ok:
break;
case DeserializationError::IncompleteInput:
DPRINTLN(DBG_DEBUG, F("Incomplete input"));
break;
case DeserializationError::InvalidInput:
DPRINTLN(DBG_DEBUG, F("Invalid input"));
break;
case DeserializationError::NoMemory:
DPRINTLN(DBG_DEBUG, F("Not enough memory ") + String(json.capacity()) + " bytes");
break;
default:
DPRINTLN(DBG_DEBUG, F("Deserialization failed"));
break;
}
}
request->send(204); // Success with no page load
delete[] mTmpBuf;
mTmpBuf = NULL;
}
#endif
void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
DPRINTLN(DBG_VERBOSE, "onApiPostBody"); DPRINTLN(DBG_VERBOSE, "onApiPostBody");
if(0 == index) { if(0 == index) {
if(NULL != mTmpBuf) if(nullptr != mTmpBuf)
delete[] mTmpBuf; delete[] mTmpBuf;
mTmpBuf = new uint8_t[total+1]; mTmpBuf = new uint8_t[total+1];
mTmpSize = total; mTmpSize = total;
@ -154,36 +256,40 @@ class RestApi {
DynamicJsonDocument json(1000); DynamicJsonDocument json(1000);
DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize);
JsonObject obj = json.as<JsonObject>();
AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
root[F("success")] = (err) ? false : true; DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize);
if(!err) { if(!json.is<JsonObject>())
String path = request->url().substring(5); root[F("error")] = F(DESER_FAILED);
if(path == "ctrl") else {
root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str()); JsonObject obj = json.as<JsonObject>();
else if(path == "setup")
root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str()); root[F("success")] = (err) ? false : true;
else { if(!err) {
root[F("success")] = false; String path = request->url().substring(5);
root[F("error")] = F(PATH_NOT_FOUND) + path; if(path == "ctrl")
} root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str());
} else { else if(path == "setup")
switch (err.code()) { root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str());
case DeserializationError::Ok: break; else {
case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break; root[F("success")] = false;
case DeserializationError::InvalidInput: root[F("error")] = F(INVALID_INPUT); break; root[F("error")] = F(PATH_NOT_FOUND) + path;
case DeserializationError::NoMemory: root[F("error")] = F(NOT_ENOUGH_MEM); break; }
default: root[F("error")] = F(DESER_FAILED); break; } else {
switch (err.code()) {
case DeserializationError::Ok: break;
case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break;
case DeserializationError::InvalidInput: root[F("error")] = F(INVALID_INPUT); break;
case DeserializationError::NoMemory: root[F("error")] = F(NOT_ENOUGH_MEM); break;
default: root[F("error")] = F(DESER_FAILED); break;
}
} }
} }
response->setLength(); response->setLength();
request->send(response); request->send(response);
delete[] mTmpBuf; delete[] mTmpBuf;
mTmpBuf = NULL; mTmpBuf = nullptr;
} }
void getNotFound(JsonObject obj, String url) { void getNotFound(JsonObject obj, String url) {
@ -195,14 +301,13 @@ class RestApi {
ep[F("generic")] = url + F("generic"); ep[F("generic")] = url + F("generic");
ep[F("index")] = url + F("index"); ep[F("index")] = url + F("index");
ep[F("setup")] = url + F("setup"); ep[F("setup")] = url + F("setup");
#if !defined(ETHERNET)
ep[F("setup/networks")] = url + F("setup/networks"); ep[F("setup/networks")] = url + F("setup/networks");
ep[F("setup/getip")] = url + F("setup/getip"); ep[F("setup/getip")] = url + F("setup/getip");
#endif /* !defined(ETHERNET) */
ep[F("system")] = url + F("system"); ep[F("system")] = url + F("system");
ep[F("live")] = url + F("live"); ep[F("live")] = url + F("live");
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
ep[F("powerHistory")] = url + F("powerHistory"); ep[F("powerHistory")] = url + F("powerHistory");
ep[F("powerHistoryDay")] = url + F("powerHistoryDay");
#endif #endif
} }
@ -236,7 +341,7 @@ class RestApi {
if(-1 != i) { if(-1 != i) {
i+=5; i+=5;
String sn = tmp.substring(i, tmp.indexOf("\"", i)-1); String sn = tmp.substring(i, tmp.indexOf("\"", i)-1);
tmp.replace(sn, String(atoll(sn.c_str()), HEX)); tmp.replace(sn, String(sn) + ",\"note\":\"" + String(atoll(sn.c_str()), HEX) + "\"");
} }
} }
response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp);
@ -252,9 +357,38 @@ class RestApi {
fp.close(); fp.close();
} }
#if defined(ESP32)
void getCoreDump(AsyncWebServerRequest *request) {
const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_COREDUMP, "coredump");
if (partition != NULL) {
size_t size = partition->size;
AsyncWebServerResponse *response = request->beginResponse("application/octet-stream", size, [size, partition](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
if((index + maxLen) > size)
maxLen = size - index;
if (ESP_OK != esp_partition_read(partition, index, buffer, maxLen))
DPRINTLN(DBG_ERROR, F("can't read partition"));
return maxLen;
});
String filename = ah::getDateTimeStrFile(gTimezone.toLocal(mApp->getTimestamp()));
filename += "_v" + String(mApp->getVersion());
filename += "_" + String(ENV_NAME);
response->addHeader("Content-Description", "File Transfer");
response->addHeader("Content-Disposition", "attachment; filename=" + filename + "_coredump.bin");
request->send(response);
} else {
AsyncWebServerResponse *response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}");
request->send(response);
}
}
#endif
void getGeneric(AsyncWebServerRequest *request, JsonObject obj) { void getGeneric(AsyncWebServerRequest *request, JsonObject obj) {
mApp->resetLockTimeout(); mApp->resetLockTimeout();
obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI();
obj[F("ts_uptime")] = mApp->getUptime(); obj[F("ts_uptime")] = mApp->getUptime();
obj[F("ts_now")] = mApp->getTimestamp(); obj[F("ts_now")] = mApp->getTimestamp();
@ -262,6 +396,7 @@ class RestApi {
obj[F("modules")] = String(mApp->getVersionModules()); obj[F("modules")] = String(mApp->getVersionModules());
obj[F("build")] = String(AUTO_GIT_HASH); obj[F("build")] = String(AUTO_GIT_HASH);
obj[F("env")] = String(ENV_NAME); obj[F("env")] = String(ENV_NAME);
obj[F("host")] = mConfig->sys.deviceName;
obj[F("menu_prot")] = mApp->isProtected(request->client()->remoteIP().toString().c_str(), "", true); obj[F("menu_prot")] = mApp->isProtected(request->client()->remoteIP().toString().c_str(), "", true);
obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask );
obj[F("menu_protEn")] = (bool) (mConfig->sys.adminPwd[0] != '\0'); obj[F("menu_protEn")] = (bool) (mConfig->sys.adminPwd[0] != '\0');
@ -278,67 +413,38 @@ class RestApi {
} }
void getSysInfo(AsyncWebServerRequest *request, JsonObject obj) { void getSysInfo(AsyncWebServerRequest *request, JsonObject obj) {
#if !defined(ETHERNET)
obj[F("ssid")] = mConfig->sys.stationSsid;
obj[F("ap_pwd")] = mConfig->sys.apPwd;
obj[F("hidd")] = mConfig->sys.isHidden;
#endif /* !defined(ETHERNET) */
obj[F("device_name")] = mConfig->sys.deviceName; obj[F("device_name")] = mConfig->sys.deviceName;
obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; obj[F("dark_mode")] = (bool)mConfig->sys.darkMode;
obj[F("sched_reboot")] = (bool)mConfig->sys.schedReboot; obj[F("sched_reboot")] = (bool)mConfig->sys.schedReboot;
obj[F("mac")] = WiFi.macAddress();
obj[F("hostname")] = mConfig->sys.deviceName;
obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0);
obj[F("prot_mask")] = mConfig->sys.protectionMask; obj[F("prot_mask")] = mConfig->sys.protectionMask;
obj[F("sdk")] = ESP.getSdkVersion(); getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); getChipInfo(obj.createNestedObject(F("chip")));
obj[F("heap_free")] = mHeapFree;
obj[F("sketch_total")] = ESP.getFreeSketchSpace();
obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb
getGeneric(request, obj);
getRadioNrf(obj.createNestedObject(F("radioNrf"))); getRadioNrf(obj.createNestedObject(F("radioNrf")));
getMqttInfo(obj.createNestedObject(F("mqtt")));
getNetworkInfo(obj.createNestedObject(F("network")));
getMemoryInfo(obj.createNestedObject(F("memory")));
#if defined(ESP32) #if defined(ESP32)
getRadioCmtInfo(obj.createNestedObject(F("radioCmt"))); getRadioCmtInfo(obj.createNestedObject(F("radioCmt")));
#endif #endif
getMqttInfo(obj.createNestedObject(F("mqtt")));
#if defined(ESP32)
obj[F("chip_revision")] = ESP.getChipRevision();
obj[F("chip_model")] = ESP.getChipModel();
obj[F("chip_cores")] = ESP.getChipCores();
obj[F("heap_total")] = ESP.getHeapSize();
//obj[F("core_version")] = F("n/a");
//obj[F("flash_size")] = F("n/a");
//obj[F("heap_frag")] = F("n/a");
//obj[F("max_free_blk")] = F("n/a");
//obj[F("reboot_reason")] = F("n/a");
#else
//obj[F("heap_total")] = F("n/a");
//obj[F("chip_revision")] = F("n/a");
//obj[F("chip_model")] = F("n/a");
//obj[F("chip_cores")] = F("n/a");
obj[F("heap_frag")] = mHeapFrag;
obj[F("max_free_blk")] = mHeapFreeBlk;
obj[F("core_version")] = ESP.getCoreVersion();
obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb
obj[F("reboot_reason")] = ESP.getResetReason();
#endif
//obj[F("littlefs_total")] = LittleFS.totalBytes();
//obj[F("littlefs_used")] = LittleFS.usedBytes();
uint8_t max; /*uint8_t max;
mApp->getSchedulerInfo(&max); mApp->getSchedulerInfo(&max);
obj[F("schMax")] = max; obj[F("schMax")] = max;*/
} }
void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) { void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) {
getSysInfo(request, obj.createNestedObject(F("system"))); getSysInfo(request, obj.createNestedObject(F("system")));
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ESP32)
char tmp[300];
snprintf(tmp, 300, "<a href=\"/factory\" class=\"btn\">%s</a><br/><br/><a href=\"/reboot\" class=\"btn\">%s</a><br/><br/><a href=\"/coredump\" class=\"btn\">%s</a>", FACTORY_RESET, BTN_REBOOT, BTN_COREDUMP);
#else
char tmp[200]; char tmp[200];
snprintf(tmp, 200, "<a href=\"/factory\" class=\"btn\">%s</a><br/><br/><a href=\"/reboot\" class=\"btn\">%s</a>", FACTORY_RESET, BTN_REBOOT); snprintf(tmp, 200, "<a href=\"/factory\" class=\"btn\">%s</a><br/><br/><a href=\"/reboot\" class=\"btn\">%s</a>", FACTORY_RESET, BTN_REBOOT);
#endif
obj[F("html")] = String(tmp); obj[F("html")] = String(tmp);
} }
@ -351,8 +457,8 @@ class RestApi {
void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) { void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) #if defined(ETHERNET)
obj[F("refresh")] = 5; obj[F("refresh")] = (mConfig->sys.eth.enabled) ? 5 : 20;
#else #else
obj[F("refresh")] = 20; obj[F("refresh")] = 20;
#endif #endif
@ -365,8 +471,8 @@ class RestApi {
obj[F("pending")] = (bool)mApp->getSavePending(); obj[F("pending")] = (bool)mApp->getSavePending();
obj[F("success")] = (bool)mApp->getLastSaveSucceed(); obj[F("success")] = (bool)mApp->getLastSaveSucceed();
obj[F("reboot")] = (bool)mApp->getShouldReboot(); obj[F("reboot")] = (bool)mApp->getShouldReboot();
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) #if defined(ETHERNET)
obj[F("reload")] = 5; obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20;
#else #else
obj[F("reload")] = 20; obj[F("reload")] = 20;
#endif #endif
@ -383,7 +489,7 @@ class RestApi {
mApp->setRebootFlag(); mApp->setRebootFlag();
obj[F("html")] = F("Erase settings: success"); obj[F("html")] = F("Erase settings: success");
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
obj[F("reload")] = 5; obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20;
#else #else
obj[F("reload")] = 20; obj[F("reload")] = 20;
#endif #endif
@ -391,7 +497,9 @@ class RestApi {
void getHtmlFactory(AsyncWebServerRequest *request, JsonObject obj) { void getHtmlFactory(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("html")] = F("Factory reset? <a class=\"btn\" href=\"/factorytrue\">yes</a> <a class=\"btn\" href=\"/\">no</a>"); char tmp[200];
snprintf(tmp, 200, "%s <a class=\"btn\" href=\"/factorytrue\">%s</a> <a class=\"btn\" href=\"/\">%s</a>", FACTORY_RESET, BTN_YES, BTN_NO);
obj[F("html")] = tmp;
} }
void getHtmlFactoryTrue(AsyncWebServerRequest *request, JsonObject obj) { void getHtmlFactoryTrue(AsyncWebServerRequest *request, JsonObject obj) {
@ -399,8 +507,8 @@ class RestApi {
mApp->eraseSettings(true); mApp->eraseSettings(true);
mApp->setRebootFlag(); mApp->setRebootFlag();
obj[F("html")] = F("Factory reset: success"); obj[F("html")] = F("Factory reset: success");
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) #if defined(ETHERNET)
obj[F("reload")] = 5; obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20;
#else #else
obj[F("reload")] = 20; obj[F("reload")] = 20;
#endif #endif
@ -422,7 +530,7 @@ class RestApi {
obj[F("name")] = String(iv->config->name); obj[F("name")] = String(iv->config->name);
obj[F("rx_success")] = iv->radioStatistics.rxSuccess; obj[F("rx_success")] = iv->radioStatistics.rxSuccess;
obj[F("rx_fail")] = iv->radioStatistics.rxFail; obj[F("rx_fail")] = iv->radioStatistics.rxFail;
obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnser; obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnswer;
obj[F("frame_cnt")] = iv->radioStatistics.frmCnt; obj[F("frame_cnt")] = iv->radioStatistics.frmCnt;
obj[F("tx_cnt")] = iv->radioStatistics.txCnt; obj[F("tx_cnt")] = iv->radioStatistics.txCnt;
obj[F("retransmits")] = iv->radioStatistics.retransmits; obj[F("retransmits")] = iv->radioStatistics.retransmits;
@ -474,13 +582,13 @@ class RestApi {
} }
obj[F("interval")] = String(mConfig->inst.sendInterval); obj[F("interval")] = String(mConfig->inst.sendInterval);
obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; obj[F("max_num_inverters")] = MAX_NUM_INVERTERS;
obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; obj[F("rstMid")] = (bool)mConfig->inst.rstValsAtMidNight;
obj[F("rstNotAvail")] = (bool)mConfig->inst.rstValsNotAvail; obj[F("rstNotAvail")] = (bool)mConfig->inst.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop;
obj[F("rstComStart")] = (bool)mConfig->inst.rstValsCommStart;
obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime; obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime;
obj[F("rdGrid")] = (bool)mConfig->inst.readGrid; obj[F("rdGrid")] = (bool)mConfig->inst.readGrid;
obj[F("rstMaxMid")] = (bool)mConfig->inst.rstMaxValsMidNight; obj[F("rstMaxMid")] = (bool)mConfig->inst.rstIncludeMaxVals;
obj[F("yldEff")] = mConfig->inst.yieldEffiency;
} }
void getInverter(JsonObject obj, uint8_t id) { void getInverter(JsonObject obj, uint8_t id) {
@ -496,7 +604,7 @@ class RestApi {
obj[F("name")] = String(iv->config->name); obj[F("name")] = String(iv->config->name);
obj[F("serial")] = String(iv->config->serial.u64, HEX); obj[F("serial")] = String(iv->config->serial.u64, HEX);
obj[F("version")] = String(iv->getFwVersion()); obj[F("version")] = String(iv->getFwVersion());
obj[F("power_limit_read")] = iv->actPowerLimit; obj[F("power_limit_read")] = ah::round1(iv->getChannelFieldValue(CH0, FLD_ACT_ACTIVE_PWR_LIMIT, iv->getRecordStruct(SystemConfigPara)));
obj[F("power_limit_ack")] = iv->powerLimitAck; obj[F("power_limit_ack")] = iv->powerLimitAck;
obj[F("max_pwr")] = iv->getMaxPower(); obj[F("max_pwr")] = iv->getMaxPower();
obj[F("ts_last_success")] = rec->ts; obj[F("ts_last_success")] = rec->ts;
@ -505,6 +613,7 @@ class RestApi {
obj[F("alarm_cnt")] = iv->alarmCnt; obj[F("alarm_cnt")] = iv->alarmCnt;
obj[F("rssi")] = iv->rssi; obj[F("rssi")] = iv->rssi;
obj[F("ts_max_ac_pwr")] = iv->tsMaxAcPower; obj[F("ts_max_ac_pwr")] = iv->tsMaxAcPower;
obj[F("ts_max_temp")] = iv->tsMaxTemperature;
JsonArray ch = obj.createNestedArray("ch"); JsonArray ch = obj.createNestedArray("ch");
@ -561,11 +670,23 @@ class RestApi {
obj[F("last_id")] = iv->getChannelFieldValue(CH0, FLD_EVT, rec); obj[F("last_id")] = iv->getChannelFieldValue(CH0, FLD_EVT, rec);
JsonArray alarm = obj.createNestedArray(F("alarm")); JsonArray alarm = obj.createNestedArray(F("alarm"));
// find oldest alarm
uint8_t offset = 0;
uint32_t oldestStart = 0xffffffff;
for(uint8_t i = 0; i < 10; i++) { for(uint8_t i = 0; i < 10; i++) {
alarm[i][F("code")] = iv->lastAlarm[i].code; if((iv->lastAlarm[i].start != 0) && (iv->lastAlarm[i].start < oldestStart)) {
alarm[i][F("str")] = iv->getAlarmStr(iv->lastAlarm[i].code); offset = i;
alarm[i][F("start")] = iv->lastAlarm[i].start; oldestStart = iv->lastAlarm[i].start;
alarm[i][F("end")] = iv->lastAlarm[i].end; }
}
for(uint8_t i = 0; i < 10; i++) {
uint8_t pos = (i + offset) % 10;
alarm[pos][F("code")] = iv->lastAlarm[pos].code;
alarm[pos][F("str")] = iv->getAlarmStr(iv->lastAlarm[pos].code);
alarm[pos][F("start")] = iv->lastAlarm[pos].start;
alarm[pos][F("end")] = iv->lastAlarm[pos].end;
} }
} }
@ -612,7 +733,9 @@ class RestApi {
obj[F("user")] = String(mConfig->mqtt.user); obj[F("user")] = String(mConfig->mqtt.user);
obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String("");
obj[F("topic")] = String(mConfig->mqtt.topic); obj[F("topic")] = String(mConfig->mqtt.topic);
obj[F("json")] = (bool) mConfig->mqtt.json;
obj[F("interval")] = String(mConfig->mqtt.interval); obj[F("interval")] = String(mConfig->mqtt.interval);
obj[F("retain")] = (bool)mConfig->mqtt.enableRetain;
} }
void getNtp(JsonObject obj) { void getNtp(JsonObject obj) {
@ -665,6 +788,116 @@ class RestApi {
} }
#endif #endif
#if defined(ETHERNET)
void getEthernet(JsonObject obj) {
obj[F("en")] = mConfig->sys.eth.enabled;
obj[F("cs")] = mConfig->sys.eth.pinCs;
obj[F("sclk")] = mConfig->sys.eth.pinSclk;
obj[F("miso")] = mConfig->sys.eth.pinMiso;
obj[F("mosi")] = mConfig->sys.eth.pinMosi;
obj[F("irq")] = mConfig->sys.eth.pinIrq;
obj[F("reset")] = mConfig->sys.eth.pinRst;
}
#endif
void getNetworkInfo(JsonObject obj) {
#if defined(ETHERNET)
bool isWired = mApp->isWiredConnection();
if(!isWired)
obj[F("wifi_channel")] = WiFi.channel();
obj[F("wired")] = isWired;
#else
obj[F("wired")] = false;
obj[F("wifi_channel")] = WiFi.channel();
#endif
obj[F("ap_pwd")] = mConfig->sys.apPwd;
obj[F("ssid")] = mConfig->sys.stationSsid;
obj[F("hidd")] = mConfig->sys.isHidden;
obj[F("mac")] = mApp->getMac();
obj[F("ip")] = mApp->getIp();
}
void getChipInfo(JsonObject obj) {
obj[F("cpu_freq")] = ESP.getCpuFreqMHz();
obj[F("sdk")] = ESP.getSdkVersion();
#if defined(ESP32)
obj[F("revision")] = ESP.getChipRevision();
obj[F("model")] = ESP.getChipModel();
obj[F("cores")] = ESP.getChipCores();
switch (esp_reset_reason()) {
default:
[[fallthrough]];
case ESP_RST_UNKNOWN:
obj[F("reboot_reason")] = F("Unknown");
break;
case ESP_RST_POWERON:
obj[F("reboot_reason")] = F("Power on");
break;
case ESP_RST_EXT:
obj[F("reboot_reason")] = F("External");
break;
case ESP_RST_SW:
obj[F("reboot_reason")] = F("Software");
break;
case ESP_RST_PANIC:
obj[F("reboot_reason")] = F("Panic");
break;
case ESP_RST_INT_WDT:
obj[F("reboot_reason")] = F("Interrupt Watchdog");
break;
case ESP_RST_TASK_WDT:
obj[F("reboot_reason")] = F("Task Watchdog");
break;
case ESP_RST_WDT:
obj[F("reboot_reason")] = F("Watchdog");
break;
case ESP_RST_DEEPSLEEP:
obj[F("reboot_reason")] = F("Deepsleep");
break;
case ESP_RST_BROWNOUT:
obj[F("reboot_reason")] = F("Brownout");
break;
case ESP_RST_SDIO:
obj[F("reboot_reason")] = F("SDIO");
break;
}
#else
obj[F("core_version")] = ESP.getCoreVersion();
obj[F("reboot_reason")] = ESP.getResetReason();
#endif
}
void getMemoryInfo(JsonObject obj) {
obj[F("heap_frag")] = mHeapFrag;
obj[F("heap_max_free_blk")] = mHeapFreeBlk;
obj[F("heap_free")] = mHeapFree;
obj[F("par_size_app0")] = ESP.getFreeSketchSpace();
obj[F("par_used_app0")] = ESP.getSketchSize();
#if defined(ESP32)
obj[F("heap_total")] = ESP.getHeapSize();
const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_COREDUMP, "coredump");
if (partition != NULL)
obj[F("flash_size")] = partition->address + partition->size;
obj[F("par_size_spiffs")] = LittleFS.totalBytes();
obj[F("par_used_spiffs")] = LittleFS.usedBytes();
#else
obj[F("flash_size")] = ESP.getFlashChipRealSize();
FSInfo info;
LittleFS.info(info);
obj[F("par_used_spiffs")] = info.usedBytes;
obj[F("par_size_spiffs")] = info.totalBytes;
obj[F("heap_total")] = 24*1014; // FIXME: don't know correct value
#endif
}
void getRadioNrf(JsonObject obj) { void getRadioNrf(JsonObject obj) {
obj[F("en")] = (bool) mConfig->nrf.enabled; obj[F("en")] = (bool) mConfig->nrf.enabled;
if(mConfig->nrf.enabled) { if(mConfig->nrf.enabled) {
@ -752,11 +985,9 @@ class RestApi {
warn.add(F(REBOOT_ESP_APPLY_CHANGES)); warn.add(F(REBOOT_ESP_APPLY_CHANGES));
if(0 == mApp->getTimestamp()) if(0 == mApp->getTimestamp())
warn.add(F(TIME_NOT_SET)); warn.add(F(TIME_NOT_SET));
#if !defined(ETHERNET) #if !defined(ESP32)
#if !defined(ESP32) if(mApp->getWasInCh12to14())
if(mApp->getWasInCh12to14()) warn.add(F(WAS_IN_CH_12_TO_14));
warn.add(F(WAS_IN_CH_12_TO_14));
#endif
#endif #endif
} }
@ -771,24 +1002,28 @@ class RestApi {
#if defined(ESP32) #if defined(ESP32)
getRadioCmt(obj.createNestedObject(F("radioCmt"))); getRadioCmt(obj.createNestedObject(F("radioCmt")));
#endif #endif
#if defined(ETHERNET)
getEthernet(obj.createNestedObject(F("eth")));
#endif
getRadioNrf(obj.createNestedObject(F("radioNrf"))); getRadioNrf(obj.createNestedObject(F("radioNrf")));
getSerial(obj.createNestedObject(F("serial"))); getSerial(obj.createNestedObject(F("serial")));
getStaticIp(obj.createNestedObject(F("static_ip"))); getStaticIp(obj.createNestedObject(F("static_ip")));
getDisplay(obj.createNestedObject(F("display"))); getDisplay(obj.createNestedObject(F("display")));
} }
#if !defined(ETHERNET)
void getNetworks(JsonObject obj) { void getNetworks(JsonObject obj) {
mApp->getAvailNetworks(obj); obj[F("success")] = mApp->getAvailNetworks(obj);
obj[F("ip")] = mApp->getIp();
} }
void getWifiIp(JsonObject obj) {
obj[F("ip")] = mApp->getStationIp(); void getIp(JsonObject obj) {
obj[F("ip")] = mApp->getIp();
} }
#endif /* !defined(ETHERNET) */
void getLive(AsyncWebServerRequest *request, JsonObject obj) { void getLive(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("refresh")] = mConfig->inst.sendInterval; obj[F("refresh")] = mConfig->inst.sendInterval;
obj[F("max_total_pwr")] = ah::round3(mApp->getTotalMaxPower());
for (uint8_t fld = 0; fld < sizeof(acList); fld++) { for (uint8_t fld = 0; fld < sizeof(acList); fld++) {
obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]);
@ -809,23 +1044,50 @@ class RestApi {
} }
} }
void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) { #if defined(ENABLE_HISTORY)
void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj, HistoryStorageType type) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ENABLE_HISTORY) obj[F("refresh")] = mApp->getHistoryPeriod(static_cast<uint8_t>(type));
obj[F("refresh")] = mConfig->inst.sendInterval;
uint16_t max = 0; uint16_t max = 0;
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld); uint16_t value = mApp->getHistoryValue(static_cast<uint8_t>(type), fld);
obj[F("value")][fld] = value;
if (value > max)
max = value;
}
obj[F("max")] = max;
if(HistoryStorageType::POWER_DAY == type) {
float yldDay = 0;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
Inverter<> *iv = mSys->getInverterByPos(i);
if (iv == NULL)
continue;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
}
obj[F("yld")] = ah::round3(yldDay / 1000.0);
}
obj[F("lastValueTs")] = mApp->getHistoryLastValueTs(static_cast<uint8_t>(type));
}
#endif /*ENABLE_HISTORY*/
#if defined(ENABLE_HISTORY_YIELD_PER_DAY)
void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) {
obj[F("refresh")] = mApp->getHistoryPeriod((uint8_t)HistoryStorageType::YIELD);
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; obj[F("value")][fld] = value;
if (value > max) if (value > max)
max = value; max = value;
} }
obj[F("max")] = max; obj[F("max")] = max;
obj[F("maxDay")] = mApp->getHistoryMaxDay();
#else
obj[F("refresh")] = 86400; // 1 day;
#endif /*ENABLE_HISTORY*/
} }
#endif /*ENABLE_HISTORY_YIELD_PER_DAY*/
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) { bool setCtrl(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) {
if(jsonIn.containsKey(F("auth"))) { if(jsonIn.containsKey(F("auth"))) {
@ -867,11 +1129,11 @@ class RestApi {
iv->powerLimit[1] = AbsolutNonPersistent; iv->powerLimit[1] = AbsolutNonPersistent;
accepted = iv->setDevControlRequest(ActivePowerContr); accepted = iv->setDevControlRequest(ActivePowerContr);
if(accepted)
mApp->triggerTickSend();
} else if(F("dev") == jsonIn[F("cmd")]) { } else if(F("dev") == jsonIn[F("cmd")]) {
DPRINTLN(DBG_INFO, F("dev cmd")); DPRINTLN(DBG_INFO, F("dev cmd"));
iv->setDevCommand(jsonIn[F("val")].as<int>()); iv->setDevCommand(jsonIn[F("val")].as<int>());
} else if(F("restart_ahoy") == jsonIn[F("cmd")]) {
mApp->setRebootFlag();
} else { } else {
jsonOut[F("error")] = F("ERR_UNKNOWN_CMD"); jsonOut[F("error")] = F("ERR_UNKNOWN_CMD");
return false; return false;
@ -889,11 +1151,6 @@ class RestApi {
if(isProtected(jsonIn, jsonOut, clientIP)) if(isProtected(jsonIn, jsonOut, clientIP))
return false; return false;
#if !defined(ETHERNET)
if(F("scan_wifi") == jsonIn[F("cmd")])
mApp->scanAvailNetworks();
else
#endif /* !defined(ETHERNET) */
if(F("set_time") == jsonIn[F("cmd")]) if(F("set_time") == jsonIn[F("cmd")])
mApp->setTimestamp(jsonIn[F("val")]); mApp->setTimestamp(jsonIn[F("val")]);
else if(F("sync_ntp") == jsonIn[F("cmd")]) else if(F("sync_ntp") == jsonIn[F("cmd")])
@ -902,17 +1159,38 @@ class RestApi {
mTimezoneOffset = jsonIn[F("val")]; mTimezoneOffset = jsonIn[F("val")];
else if(F("discovery_cfg") == jsonIn[F("cmd")]) else if(F("discovery_cfg") == jsonIn[F("cmd")])
mApp->setMqttDiscoveryFlag(); // for homeassistant mApp->setMqttDiscoveryFlag(); // for homeassistant
#if !defined(ETHERNET)
else if(F("save_wifi") == jsonIn[F("cmd")]) { else if(F("save_wifi") == jsonIn[F("cmd")]) {
snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", jsonIn[F("ssid")].as<const char*>()); snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", jsonIn[F("ssid")].as<const char*>());
snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", jsonIn[F("pwd")].as<const char*>()); snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", jsonIn[F("pwd")].as<const char*>());
mApp->saveSettings(false); // without reboot mApp->saveSettings(false); // without reboot
mApp->setStopApAllowedMode(false);
mApp->setupStation(); mApp->setupStation();
} }
#endif /* !defined(ETHERNET */ #if defined(ETHERNET)
else if(F("save_eth") == jsonIn[F("cmd")]) {
mConfig->sys.eth.enabled = jsonIn[F("en")].as<bool>();
mConfig->sys.eth.pinCs = jsonIn[F("cs")].as<uint8_t>();
mConfig->sys.eth.pinSclk = jsonIn[F("sclk")].as<uint8_t>();
mConfig->sys.eth.pinMiso = jsonIn[F("miso")].as<uint8_t>();
mConfig->sys.eth.pinMosi = jsonIn[F("mosi")].as<uint8_t>();
mConfig->sys.eth.pinIrq = jsonIn[F("irq")].as<uint8_t>();
mConfig->sys.eth.pinRst = jsonIn[F("reset")].as<uint8_t>();
mApp->saveSettings(true);
}
#endif
else if(F("save_iv") == jsonIn[F("cmd")]) { else if(F("save_iv") == jsonIn[F("cmd")]) {
Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")], false); Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = mSys->getInverterByPos(i, true);
if(nullptr != iv) {
if((i != jsonIn[F("id")]) && (iv->config->serial.u64 == jsonIn[F("ser")])) {
jsonOut[F("error")] = F("ERR_DUPLICATE_INVERTER");
return false;
}
}
}
iv = mSys->getInverterByPos(jsonIn[F("id")], false);
iv->config->enabled = jsonIn[F("en")]; iv->config->enabled = jsonIn[F("en")];
iv->config->serial.u64 = jsonIn[F("ser")]; iv->config->serial.u64 = jsonIn[F("ser")];
snprintf(iv->config->name, MAX_NAME_LENGTH, "%s", jsonIn[F("name")].as<const char*>()); snprintf(iv->config->name, MAX_NAME_LENGTH, "%s", jsonIn[F("name")].as<const char*>());
@ -956,15 +1234,15 @@ class RestApi {
private: private:
constexpr static uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, 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}; FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP, FLD_MT};
constexpr static uint8_t acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, FLD_PAC, FLD_F, FLD_PF, FLD_T, 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}; FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP, FLD_MT};
constexpr static uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP}; constexpr static uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP};
private: private:
IApp *mApp = nullptr; IApp *mApp = nullptr;
HMSYSTEM *mSys = nullptr; HMSYSTEM *mSys = nullptr;
HmRadio<> *mRadioNrf = nullptr; NrfRadio<> *mRadioNrf = nullptr;
#if defined(ESP32) #if defined(ESP32)
CmtRadio<> *mRadioCmt = nullptr; CmtRadio<> *mRadioCmt = nullptr;
#endif #endif
@ -974,7 +1252,7 @@ class RestApi {
uint32_t mTimezoneOffset = 0; uint32_t mTimezoneOffset = 0;
uint32_t mHeapFree = 0, mHeapFreeBlk = 0; uint32_t mHeapFree = 0, mHeapFreeBlk = 0;
uint8_t mHeapFrag = 0; uint8_t mHeapFrag = 0;
uint8_t *mTmpBuf = NULL; uint8_t *mTmpBuf = nullptr;
uint32_t mTmpSize = 0; uint32_t mTmpSize = 0;
}; };

2
src/web/html/about.html

@ -14,7 +14,7 @@
<div class="p-2">Used Libraries</div> <div class="p-2">Used Libraries</div>
</div> </div>
<div class="row"><a href="https://github.com/bertmelis/espMqttClient" target="_blank">bertmelis/espMqttClient</a></div> <div class="row"><a href="https://github.com/bertmelis/espMqttClient" target="_blank">bertmelis/espMqttClient</a></div>
<div class="row"><a href="https://github.com/yubox-node-org/ESPAsyncWebServer" target="_blank">yubox-node-org/ESPAsyncWebServer</a></div> <div class="row"><a href="https://github.com/esphome/ESPAsyncWebServer" target="_blank">esphome/ESPAsyncWebServer</a></div>
<div class="row"><a href="https://github.com/bblanchon/ArduinoJson" target="_blank">bblanchon/ArduinoJson</a></div> <div class="row"><a href="https://github.com/bblanchon/ArduinoJson" target="_blank">bblanchon/ArduinoJson</a></div>
<div class="row"><a href="https://github.com/nrf24/RF24" target="_blank">nrf24/RF24</a></div> <div class="row"><a href="https://github.com/nrf24/RF24" target="_blank">nrf24/RF24</a></div>
<div class="row"><a href="https://github.com/paulstoffregen/Time" target="_blank">paulstoffregen/Time</a></div> <div class="row"><a href="https://github.com/paulstoffregen/Time" target="_blank">paulstoffregen/Time</a></div>

33
src/web/html/api.js

@ -61,6 +61,23 @@ function ml(tagName, ...args) {
return nester(el, args[1]) return nester(el, args[1])
} }
function mlNs(tagName, ...args) {
var el = document.createElementNS("http://www.w3.org/2000/svg", tagName);
if(args[0]) {
for(var name in args[0]) {
if(name.indexOf("on") === 0) {
el.addEventListener(name.substr(2).toLowerCase(), args[0][name], false)
} else {
el.setAttribute(name, args[0][name]);
}
}
}
if (!args[1]) {
return el;
}
return nester(el, args[1])
}
function nester(el, n) { function nester(el, n) {
if (typeof n === "string") { if (typeof n === "string") {
el.innerHTML = n; el.innerHTML = n;
@ -84,10 +101,12 @@ function topnav() {
} }
function parseNav(obj) { function parseNav(obj) {
for(i = 0; i < 13; i++) { for(i = 0; i < 14; i++) {
if(i == 2) if(i == 2)
continue; continue;
var l = document.getElementById("nav"+i); var l = document.getElementById("nav"+i);
if(null == l)
continue
if(12 == i) { if(12 == i) {
if(obj.cst_lnk.length > 0) { if(obj.cst_lnk.length > 0) {
l.href = obj.cst_lnk l.href = obj.cst_lnk
@ -124,7 +143,7 @@ function parseVersion(obj) {
function parseESP(obj) { function parseESP(obj) {
document.getElementById("esp_type").replaceChildren( document.getElementById("esp_type").replaceChildren(
document.createTextNode("Board: " + obj["esp_type"]) document.createTextNode("Board: " + obj.esp_type)
); );
} }
@ -134,7 +153,11 @@ function parseRssi(obj) {
icon = iconWifi1; icon = iconWifi1;
else if(obj["wifi_rssi"] <= -70) else if(obj["wifi_rssi"] <= -70)
icon = iconWifi2; icon = iconWifi2;
document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "icon-fg2", obj["wifi_rssi"])); document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "icon-fg2", obj.wifi_rssi));
}
function parseTitle(obj) {
document.title = obj.host + " - " + document.title
} }
function toIsoDateStr(d) { function toIsoDateStr(d) {
@ -221,6 +244,10 @@ function badge(success, text, second="error") {
return ml("span", {class: "badge badge-" + ((success) ? "success" : second)}, text); return ml("span", {class: "badge badge-" + ((success) ? "success" : second)}, text);
} }
function progress(val) {
return ml("div", {class: "progress"}, ml("div", {class: "progress-bar", style: "width: " + val + "%"}, null))
}
function tabChange(id) { function tabChange(id) {
var els = document.getElementsByClassName("nav-link"); var els = document.getElementsByClassName("nav-link");
[].forEach.call(els, function(e) { [].forEach.call(els, function(e) {

5
src/web/html/colorBright.css

@ -12,6 +12,7 @@
--nav-bg: #333; --nav-bg: #333;
--primary: #006ec0; --primary: #006ec0;
--primary-disabled: #ccc;
--primary-hover: #044e86; --primary-hover: #044e86;
--secondary: #0072c8; --secondary: #0072c8;
--nav-active: #555; --nav-active: #555;
@ -30,4 +31,8 @@
--ch-head-bg: #006ec0; --ch-head-bg: #006ec0;
--ts-head: #333; --ts-head: #333;
--ts-bg: #555; --ts-bg: #555;
--chart-cont: #fbfbfb;
--chart-bg: #f9f9f9;
--chart-text: #000000;
} }

5
src/web/html/colorDark.css

@ -12,6 +12,7 @@
--nav-bg: #333; --nav-bg: #333;
--primary: #004d87; --primary: #004d87;
--primary-disabled: #ccc;
--primary-hover: #023155; --primary-hover: #023155;
--secondary: #0072c8; --secondary: #0072c8;
--nav-active: #555; --nav-active: #555;
@ -30,4 +31,8 @@
--ch-head-bg: #236; --ch-head-bg: #236;
--ts-head: #333; --ts-head: #333;
--ts-bg: #555; --ts-bg: #555;
--chart-cont: #0b0b0b;
--chart-bg: #090909;
--chart-text: #FFFFFF;
} }

142
src/web/html/grid_info.json

@ -10,10 +10,13 @@
{"0x0908": "France_VFR2014"}, {"0x0908": "France_VFR2014"},
{"0x0a00": "DE NF_EN_50549-1:2019"}, {"0x0a00": "DE NF_EN_50549-1:2019"},
{"0x0c00": "AT_TOR_Erzeuger_default"}, {"0x0c00": "AT_TOR_Erzeuger_default"},
{"0x0c03": "AT_TOR_Erzeuger_cosphi=1"},
{"0x0c04": "AT_TOR_Erzeuger_default"},
{"0x0d00": "FR_VFR2019"}, {"0x0d00": "FR_VFR2019"},
{"0x0d04": "NF_EN_50549-1:2019"}, {"0x0d04": "NF_EN_50549-1:2019"},
{"0x1000": "ES_RD1699"}, {"0x1000": "ES_RD1699"},
{"0x1200": "EU_EN50438"}, {"0x1200": "EU_EN50438"},
{"0x1300": "MEX_NOM_220V"},
{"0x2600": "BE_C10_26"}, {"0x2600": "BE_C10_26"},
{"0x2900": "NL_NEN-EN50549-1_2019"}, {"0x2900": "NL_NEN-EN50549-1_2019"},
{"0x2a00": "PL_PN-EN 50549-1:2019"}, {"0x2a00": "PL_PN-EN 50549-1:2019"},
@ -35,6 +38,44 @@
{"0xb0": "Watt Power Factor"} {"0xb0": "Watt Power Factor"}
], ],
"group": [ "group": [
{
"0x0000": [
{
"name": "Nominal Voltage",
"div": 10,
"def": 220,
"unit": "V"
},
{
"name": "Low Voltage 1",
"div": 10,
"min": 170,
"max": 195.5,
"def": 184,
"unit": "V"
},
{
"name": "LV1 Maximum Trip Time",
"div": 10,
"def": 0.1,
"unit": "s"
},
{
"name": "High Voltage 1",
"div": 10,
"min": 253,
"max": 275,
"def": 270,
"unit": "V"
},
{
"name": "HV1 Maximum Trip Time",
"div": 10,
"def": 0.1,
"unit": "s"
}
]
},
{ {
"0x0003": [ "0x0003": [
{ {
@ -213,6 +254,78 @@
} }
] ]
}, },
{
"0x000b": [
{
"name": "Nominal Voltage",
"div": 10,
"def": 230,
"unit": "V"
},
{
"name": "Low Voltage 1",
"div": 10,
"min": 170,
"max": 184,
"def": 184,
"unit": "V"
},
{
"name": "LV1 Maximum Trip Time",
"div": 10,
"def": 1.5,
"unit": "s"
},
{
"name": "High Voltage 1",
"div": 10,
"min": 253,
"max": 270,
"def": 255.3,
"unit": "V"
},
{
"name": "HV1 Maximum Trip Time",
"div": 10,
"def": 0.1,
"unit": "s"
},
{
"name": "Low Voltage 2",
"div": 10,
"def": 57.5,
"unit": "V"
},
{
"name": "LV2 Maximum Trip Time",
"div": 100,
"def": 0.5,
"unit": "s"
},
{
"name": "High Voltage 2",
"div": 10,
"min": 264.5,
"max": 275,
"def": 264.5,
"unit": "V"
},
{
"name": "HV2 Maximum Trip Time",
"div": 100,
"def": 0.08,
"unit": "s"
},
{
"name": "10mins Average High Voltage",
"div": 10,
"min": 245,
"max": 255.3,
"def": 255.3,
"unit": "V"
}
]
},
{ {
"0x000c": [ "0x000c": [
{ {
@ -766,6 +879,35 @@
} }
] ]
}, },
{
"0x6004": [
{
"name": "VW Function Activated",
"div": 1,
"min": 0,
"max": 1,
"def": 1
},
{
"name": "Start of Voltage Watt Droop",
"div": 10,
"def": 253,
"unit": "V"
},
{
"name": "End of Voltage Watt Droop",
"div": 10,
"def": 257.6,
"unit": "V"
},
{
"name": "VW Droop Slope",
"div": 100,
"def": 21.74,
"unit": "Pn%/V"
}
]
},
{ {
"0x7000": [ "0x7000": [
{ {

216
src/web/html/history.html

@ -13,82 +13,178 @@
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<h3>{#TOTAL_POWER}</h3> <h3>{#TOTAL_POWER}</h3>
<div> <div class="chartDiv" id="pwrChart"></div>
<div class="chartDiv" id="pwrChart"> </div> <h3>{#TOTAL_POWER_DAY}</h3>
<p> <div class="chartDiv" id="pwrDayChart"></div>
{#MAX_DAY}: <span id="pwrMaxDay"></span> W. {#LAST_VALUE}: <span id="pwrLast"></span> W.<br /> <!--IF_ENABLE_HISTORY_YIELD_PER_DAY-->
{#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS} <h3>{#TOTAL_YIELD_PER_DAY}</h3>
</p> <div class="chartDiv" id="ydChart"></div>
</div> <!--ENDIF_ENABLE_HISTORY_YIELD_PER_DAY-->
<!--IF_ENABLE_HISTORY_LOAD_DATA-->
<h4 style="margin-bottom:0px;">Insert data into Yield per day history</h4>
<fieldset style="padding: 1px;">
<legend class="des" style="margin-top: 0px;">Insert data (*.json) i.e. from a saved "/api/yieldDayHistory" call
</legend>
<form id="form" method="POST" action="/api/addYDHist" enctype="multipart/form-data"
accept-charset="utf-8">
<input type="button" class="btn my-4" style="padding: 3px;margin: 3px;" value="Insert" onclick="submit()">
<input type="file" name="insert" style="width: 80%;">
</form>
</fieldset>
<!--ENDIF_ENABLE_HISTORY_LOAD_DATA-->
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}
<script type="text/javascript"> <script type="text/javascript">
const svgns = "http://www.w3.org/2000/svg"; const height = 250
var pwrExeOnce = true; var once = true
var ydExeOnce = true;
// make a simple rectangle function calcScale(obj) {
var mRefresh = 60; let s = {}
var mLastValue = 0; s.x_mul = 60
const mChartHeight = 250; s.ts_dur = obj.refresh * obj.value.length
s.ts_start = obj.lastValueTs - s.ts_dur
function parseHistory(obj, namePrefix, execOnce) { while(s.x_mul * 10 <= s.ts_dur)
mRefresh = obj.refresh s.x_mul += (s.x_mul == 60) ? 240 : ((s.x_mul < 1800) ? 300 : 1800)
var data = Object.assign({}, obj.value)
numDataPts = Object.keys(data).length s.y_mul = 10
while(s.y_mul * 10 <= obj.max)
if (true == execOnce) { s.y_mul += (s.y_mul < 100) ? 10 : 100
let s = document.createElementNS(svgns, "svg"); s.y_step = Math.ceil(obj.max / s.y_mul)
s.setAttribute("class", "chart"); s.y_max = s.y_mul * s.y_step
s.setAttribute("width", (numDataPts + 2) * 2); return s
s.setAttribute("height", mChartHeight); }
s.setAttribute("role", "img");
function setupSvg(id, obj) {
let g = document.createElementNS(svgns, "g"); let scale = calcScale(obj)
s.appendChild(g); let n = obj.value.length
for (var i = 0; i < numDataPts; i++) { return mlNs("svg", {class: "container", id: id+"_svg", viewBox: "0 0 "+String(n*2+50)+" "+String(height+20), width: "100%", height: "100%"}, [
val = data[i]; mlNs("defs", {}, [
let rect = document.createElementNS(svgns, "rect"); mlNs("linearGradient", {id: "gLine", x1: 0, y1: 0, x2: 0, y2: "100%"}, [
rect.setAttribute("id", namePrefix+"Rect" + i); mlNs("stop", {offset: 0, "stop-color": "#006ec0"}),
rect.setAttribute("x", i * 2); mlNs("stop", {offset: "80%", "stop-color": "#5050ff"}),
rect.setAttribute("width", 2); mlNs("stop", {offset: "100%", "stop-color": "gray"})
g.appendChild(rect); ]),
mlNs("linearGradient", {id: "gFill", x1: 0, y1: 0, x2: 0, y2: "100%"}, [
mlNs("stop", {offset: 0, "stop-color": "#006ec0"}),
mlNs("stop", {offset: "50%", "stop-color": "#0034c0"}),
mlNs("stop", {offset: "100%", "stop-color": "#e0e0e0"})
])
]),
...gridText(n*2, scale),
mlNs("g", {transform: "translate(30, 5)"}, [
...grid(n*2, scale),
...poly(n*2, obj, scale)
])
])
}
function gridText(x2, scale) {
let g = []
let div = height / scale.y_max
for(let i = 0; i <= scale.y_max; i += scale.y_mul) {
g.push(mlNs("text", {x: 0, y: height-(i*div)+9}, String(i)))
}
div = x2 / scale.ts_dur
for(let i = 0; i < scale.ts_dur; i++) {
if((i + scale.ts_start) % scale.x_mul == 0) {
let d = new Date((scale.ts_start + i) * 1000)
g.push(mlNs("text", {x: (i*div)+17, y: height+20}, ("0"+d.getHours()).slice(-2) + ":" + ("0"+d.getMinutes()).slice(-2)))
} }
document.getElementById(namePrefix+"Chart").appendChild(s);
} }
return g
}
// normalize data to chart function grid(x2, scale) {
let divider = obj.max / mChartHeight; let g = []
if (divider == 0) let div = height / scale.y_max
divider = 1; for(let i = 0; i <= scale.y_max; i += scale.y_mul) {
for (var i = 0; i < numDataPts; i++) { g.push(mlNs("line", {x1: 0, x2: x2, y1: height-i*div, y2: height-i*div, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"}))
val = data[i]; }
if (val > 0) div = x2 / scale.ts_dur
mLastValue = val for(let i = 0; i <= scale.ts_dur; i++) {
val = val / divider if((i + scale.ts_start) % scale.x_mul == 0) {
rect = document.getElementById(namePrefix+"Rect" + i); g.push(mlNs("line", {x1: (i*div), x2: (i*div), y1: 0, y2: height, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"}))
rect.setAttribute("height", val); }
rect.setAttribute("y", mChartHeight - val);
} }
document.getElementById(namePrefix + "Max").innerHTML = obj.max; return g
if (mRefresh < 5)
mRefresh = 5;
document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh;
} }
function poly(x2, obj, scale) {
let pts = ""
let i = 0, first = -1, last = -1, lastVal = 0
let div = scale.y_max / height
if(div == 0)
div = 1
for (val of obj.value) {
if(val > 0) {
lastVal = val
pts += " " + String(i) + "," + String(height - val / div)
if(first < 0)
first = i
last = i
}
i += 2
}
let pts2 = pts + " " + String(last) + "," + String(height)
pts2 += " " + String(first) + "," + String(height)
elm = [
mlNs("polyline", {stroke: "url(#gLine)", fill: "none", points: pts}),
mlNs("polyline", {stroke: "none", fill: "url(#gFill)", points: pts2}),
mlNs("text", {x: i*.8, y: 10}, "{#MAXIMUM}: " + String(obj.max) + "W"),
mlNs("text", {x: i*.8, y: 25}, "{#LAST_VALUE}: " + String(lastVal) + "W")
]
if(undefined !== obj.yld)
elm.push(mlNs("text", {x: i*.8, y: 40}, "{#YIELD_DAY}: " + String(obj.yld) + "kWh"))
return elm;
}
function parsePowerHistory(obj){ function parsePowerHistory(obj){
if(once) {
once = false
parseNav(obj.generic)
parseESP(obj.generic)
parseRssi(obj.generic)
parseTitle(obj.generic)
window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", obj.refresh * 1000)
setTimeout(() => {
window.setInterval("getAjax('/api/powerHistoryDay', parsePowerHistoryDay)", obj.refresh * 1000)
}, 200)
/*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
setTimeout(() => {
window.setInterval("getAjax('/api/yieldDayHistory', parseYieldDayHistory)", obj.refresh * 1000)
}, 400)
/*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/
}
if (null != obj) {
let svg = setupSvg("ph", obj);
document.getElementById("pwrChart").replaceChildren(svg);
setTimeout(() => { getAjax("/api/powerHistoryDay", parsePowerHistoryDay) }, 50);
}
}
function parsePowerHistoryDay(obj) {
if (null != obj) { if (null != obj) {
parseNav(obj.generic); let svg = setupSvg("phDay", obj);
parseHistory(obj,"pwr", pwrExeOnce) document.getElementById("pwrDayChart").replaceChildren(svg);
document.getElementById("pwrLast").innerHTML = mLastValue /*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
document.getElementById("pwrMaxDay").innerHTML = obj.maxDay setTimeout(() => { getAjax("/api/yieldDayHistory", parseYieldDayHistory) }, 50);
/*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/
} }
if (pwrExeOnce) { }
pwrExeOnce = false;
window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000); /*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
function parseYieldDayHistory(obj) {
if (null != obj) {
let svg = setupSvg("phDay", obj);
document.getElementById("pwrDayChart").replaceChildren(svg);
} }
} }
/*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/
getAjax("/api/powerHistory", parsePowerHistory); getAjax("/api/powerHistory", parsePowerHistory);
</script> </script>

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

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

11
src/web/html/includes/nav.html

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

44
src/web/html/index.html

@ -14,6 +14,7 @@
</p> </p>
<p> <p>
<span class="des">System Infos:</span> <span class="des">System Infos:</span>
<div id="total"></div>
<div id="iv"></div> <div id="iv"></div>
<div class="hr"></div> <div class="hr"></div>
<div id="warn_info"></div> <div id="warn_info"></div>
@ -23,9 +24,9 @@
<h3>{#SUPPORT}:</h3> <h3>{#SUPPORT}:</h3>
<ul> <ul>
<li><a href="https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md" target="_blank">{#CHANGELOG}</a></li> <li><a href="https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md" target="_blank">{#CHANGELOG}</a></li>
<li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB">Discord</a></li> <li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li>{#REPORT} <a href="https://github.com/lumapu/ahoy/issues" target="_blank">{#ISSUES}</a></li> <li>{#REPORT} <a href="https://github.com/lumapu/ahoy/issues" target="_blank">{#ISSUES}</a></li>
<li>{#CONTRIBUTE} <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">{#DOCUMENTATION}</a></li> <li>{#CONTRIBUTE} <a href="https://docs.ahoydtu.de" target="_blank">{#DOCUMENTATION}</a></li>
<li><a href="https://fw.ahoydtu.de/fw/dev/" target="_blank">Download</a> & Test {#DEV_FIRMWARE}, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">{#DEV_CHANGELOG}</a></li> <li><a href="https://fw.ahoydtu.de/fw/dev/" target="_blank">Download</a> & Test {#DEV_FIRMWARE}, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">{#DEV_CHANGELOG}</a></li>
<li>{#DON_MAKE} <a href="https://paypal.me/lupusch" target="_blank">{#DONATION}</a></li> <li>{#DON_MAKE} <a href="https://paypal.me/lupusch" target="_blank">{#DONATION}</a></li>
</ul> </ul>
@ -56,8 +57,10 @@
} }
function parseGeneric(obj) { function parseGeneric(obj) {
if(exeOnce) if(exeOnce) {
parseESP(obj) parseESP(obj)
parseTitle(obj)
}
parseRssi(obj) parseRssi(obj)
} }
@ -70,9 +73,9 @@
var min = parseInt(up / 60) % 60; var min = parseInt(up / 60) % 60;
var sec = up % 60; var sec = up % 60;
var e = document.getElementById("uptime"); var e = document.getElementById("uptime");
e.innerHTML = days + " Day"; e.innerHTML = days + " {#DAY}";
if(1 != days) if(1 != days)
e.innerHTML += "s"; e.innerHTML += "{#S}";
e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":" e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":" + ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2); + ("0"+sec).substr(-2);
@ -111,6 +114,10 @@
function parseIv(obj, ts) { function parseIv(obj, ts) {
var p = div(["none"]); var p = div(["none"]);
var total = 0;
var count = 0;
var mobile = window.screen.width < 470;
for(var i of obj) { for(var i of obj) {
var icon = iconSuccess; var icon = iconSuccess;
var cl = "icon-success"; var cl = "icon-success";
@ -126,20 +133,28 @@
} else if(0 == i["ts_last_success"]) { } else if(0 == i["ts_last_success"]) {
avail = "{#AVAIL_NO_DATA}"; avail = "{#AVAIL_NO_DATA}";
} else { } else {
avail = "{#AVAIL} "; if (!mobile)
avail = "{#AVAIL} ";
if(false == i["is_producing"]) if(false == i["is_producing"])
avail += "{#NOT_PRODUCING}"; avail += "{#NOT_PRODUCING}";
else { else {
icon = iconSuccessFull; icon = iconSuccessFull;
avail += "{#PRODUCING} " + i.cur_pwr + "W"; avail += "{#PRODUCING} " + i.cur_pwr + " W";
total += i.cur_pwr;
count += 1;
} }
} }
var text;
if (mobile)
text = "#";
else
text = "{#INVERTER} #";
p.append( p.append(
svg(icon, 30, 30, "icon " + cl), svg(icon, 30, 30, "icon " + cl),
span("{#INVERTER} #" + i["id"] + ": " + i["name"] + " {#IS} " + avail), span(text + i["id"] + ": " + i["name"] + " {#IS} " + avail),
br() br()
); );
if(false == i["is_avail"]) { if(false == i["is_avail"]) {
if(i["ts_last_success"] > 0) { if(i["ts_last_success"] > 0) {
@ -149,6 +164,13 @@
} }
} }
document.getElementById("iv").replaceChildren(p); document.getElementById("iv").replaceChildren(p);
if (count > 1) {
var t = div(["none"]);
t.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Total: " + Math.round(total).toLocaleString() + " W"), br());
document.getElementById("total").replaceChildren(t);
document.getElementById("total").appendChild(div(["hr"]));
}
} }
function parseWarn(warn) { function parseWarn(warn) {

15
src/web/html/serial.html

@ -35,15 +35,16 @@
var hrs = parseInt(up / 3600) % 24; var hrs = parseInt(up / 3600) % 24;
var min = parseInt(up / 60) % 60; var min = parseInt(up / 60) % 60;
var sec = up % 60; var sec = up % 60;
document.getElementById("uptime").innerHTML = days + " Days, " document.getElementById("uptime").innerHTML = days + " {#DAYS}, "
+ ("0"+hrs).substr(-2) + ":" + ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":" + ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2); + ("0"+sec).substr(-2);
parseRssi(obj); parseRssi(obj)
if(true == exeOnce) { if(true == exeOnce) {
parseNav(obj); parseNav(obj)
parseESP(obj); parseESP(obj)
parseTitle(obj)
window.setInterval("getAjax('/api/generic', parseGeneric)", 5000); window.setInterval("getAjax('/api/generic', parseGeneric)", 5000);
exeOnce = false; exeOnce = false;
setTimeOffset(); setTimeOffset();
@ -65,7 +66,7 @@
}); });
document.getElementById("scroll").addEventListener("click", function() { document.getElementById("scroll").addEventListener("click", function() {
mAutoScroll = !mAutoScroll; mAutoScroll = !mAutoScroll;
this.value = (mAutoScroll) ? "autoscroll" : "manual scroll"; this.value = (mAutoScroll) ? "{#BTN_AUTOSCROLL}" : "{#BTN_MANUALSCROLL}";
}); });
document.getElementById("copy").addEventListener("click", function() { document.getElementById("copy").addEventListener("click", function() {
con.value = version + " - " + build + "\n---------------\n" + con.value; con.value = version + " - " + build + "\n---------------\n" + con.value;
@ -80,10 +81,10 @@
try { try {
return document.execCommand("copy"); // Security exception may be thrown by some browsers. return document.execCommand("copy"); // Security exception may be thrown by some browsers.
} catch (ex) { } catch (ex) {
alert("Copy to clipboard failed" + ex); alert("{#CLIPBOARD_FAILED} " + ex);
} finally { } finally {
document.body.removeChild(ta); document.body.removeChild(ta);
alert("Copied to clipboard"); alert("{#COPIED_TO_CLIPBOARD}");
} }
} }
}); });

369
src/web/html/setup.html

@ -52,28 +52,17 @@
<div class="s_content"> <div class="s_content">
<fieldset class="mb-2"> <fieldset class="mb-2">
<legend class="des">WiFi</legend> <legend class="des">WiFi</legend>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">{#AP_PWD}</div> <div class="col-12 col-sm-3 my-2">{#AP_PWD}</div>
<div class="col-12 col-sm-9"><input type="text" name="ap_pwd" minlength="8" /></div> <div class="col-12 col-sm-9"><input type="text" name="ap_pwd" minlength="8" /></div>
</div> </div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">{#SEARCH_NETWORKS}</div>
<div class="col-12 col-sm-9"><input type="button" name="scanbtn" id="scanbtn" class="btn" value="{#BTN_SCAN}" onclick="scan()"/></div>
</div>
<div class="row mb-2 mb-sm-3"> <div class="row mb-2 mb-sm-3">
<div class="col-12 col-sm-3 my-2">{#AVAIL_NETWORKS}</div> <div class="col-12 col-sm-3 my-2">SSID</div>
<div class="col-12 col-sm-9"> <div class="col-12 col-sm-9">
<select name="networks" id="networks" onChange="selNet()"> <input type="text" name="ssid"/><br/>
<option value="-1" selected disabled hidden>{#NETWORK_NOT_SCANNED}</option> <a href="/wizard">{#SCAN_WIFI}</a>
</select>
</div> </div>
</div> </div>
<div class="row mb-2 mb-sm-3">
<div class="col-12 col-sm-3 my-2">SSID</div>
<div class="col-12 col-sm-9"><input type="text" name="ssid"/></div>
</div>
<div class="row mb-2 mb-sm-3"> <div class="row mb-2 mb-sm-3">
<div class="col-12 col-sm-3">{#SSID_HIDDEN}</div> <div class="col-12 col-sm-3">{#SSID_HIDDEN}</div>
<div class="col-12 col-sm-9"><input type="checkbox" name="hidd"/></div> <div class="col-12 col-sm-9"><input type="checkbox" name="hidd"/></div>
@ -138,7 +127,11 @@
<div class="col-4"><input type="checkbox" name="invRstMid"/></div> <div class="col-4"><input type="checkbox" name="invRstMid"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 mb-2">{#INV_PAUSE_SUNSET}</div> <div class="col-8 mb-2">{#INV_RESET_SUNRISE}</div>
<div class="col-4"><input type="checkbox" name="invRstComStart"/></div>
</div>
<div class="row mb-3">
<div class="col-8 mb-2">{#INV_RESET_SUNSET}</div>
<div class="col-4"><input type="checkbox" name="invRstComStop"/></div> <div class="col-4"><input type="checkbox" name="invRstComStop"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
@ -146,7 +139,7 @@
<div class="col-4"><input type="checkbox" name="invRstNotAvail"/></div> <div class="col-4"><input type="checkbox" name="invRstNotAvail"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8">{#INV_RESET_MAX_MIDNIGHT}</div> <div class="col-8">{#INV_RESET_MAX_VALUES}</div>
<div class="col-4"><input type="checkbox" name="invRstMaxMid"/></div> <div class="col-4"><input type="checkbox" name="invRstMaxMid"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
@ -157,10 +150,6 @@
<div class="col-8">{#INV_READ_GRID_PROFILE}</div> <div class="col-8">{#INV_READ_GRID_PROFILE}</div>
<div class="col-4"><input type="checkbox" name="rdGrid"/></div> <div class="col-4"><input type="checkbox" name="rdGrid"/></div>
</div> </div>
<div class="row mb-3">
<div class="col-8">{#INV_YIELD_EFF}</div>
<div class="col-4"><input type="number" name="yldEff" step="any"/></div>
</div>
</fieldset> </fieldset>
</div> </div>
@ -246,6 +235,10 @@
<div class="col-12 col-sm-3 my-2">Topic</div> <div class="col-12 col-sm-3 my-2">Topic</div>
<div class="col-12 col-sm-9"><input type="text" name="mqttTopic" pattern="[\-\+A-Za-z0-9\.\/#\$%&=_]+" title="Invalid input" /></div> <div class="col-12 col-sm-9"><input type="text" name="mqttTopic" pattern="[\-\+A-Za-z0-9\.\/#\$%&=_]+" title="Invalid input" /></div>
</div> </div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">{#MQTT_JSON}</div>
<div class="col-12 col-sm-9"><input type="checkbox" name="mqttJson" /></div>
</div>
<p class="des">{#MQTT_NOTE}</p> <p class="des">{#MQTT_NOTE}</p>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">{#INTERVAL}</div> <div class="col-12 col-sm-3 my-2">{#INTERVAL}</div>
@ -258,6 +251,10 @@
<span id="apiResultMqtt"></span> <span id="apiResultMqtt"></span>
</div> </div>
</div> </div>
<div class="row mb-3">
<div class="col-8 col-sm-3">{#RETAIN}</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="retain"/></div>
</div>
</fieldset> </fieldset>
</div> </div>
@ -274,9 +271,13 @@
<p class="des">{#RADIO} (CMT2300A)</p> <p class="des">{#RADIO} (CMT2300A)</p>
<div id="cmt"></div> <div id="cmt"></div>
<!--ENDIF_ESP32--> <!--ENDIF_ESP32-->
<!--IF_ETHERNET-->
<p class="des">Ethernet</p>
<div id="eth"></div>
<!--ENDIF_ETHERNET-->
</fieldset> </fieldset>
</div> </div>
<!--IF_PLUGIN_DISPLAY-->
<button type="button" class="s_collapsible">{#DISPLAY_CONFIG}</button> <button type="button" class="s_collapsible">{#DISPLAY_CONFIG}</button>
<div class="s_content"> <div class="s_content">
<fieldset class="mb-4"> <fieldset class="mb-4">
@ -290,7 +291,7 @@
<div id="screenSaver"></div> <div id="screenSaver"></div>
<div class="row mb-3" id="luminanceOption"> <div class="row mb-3" id="luminanceOption">
<div class="col-12 col-sm-3 my-2">{#DISP_LUMINANCE}</div> <div class="col-12 col-sm-3 my-2">{#DISP_LUMINANCE}</div>
<div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="255"></select></div> <div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="255"></div>
</div> </div>
<p class="des">{#DISP_PINOUT}</p> <p class="des">{#DISP_PINOUT}</p>
<div id="dispPins"></div> <div id="dispPins"></div>
@ -299,12 +300,13 @@
<p class="des">{#GRAPH_OPTIONS}</p> <p class="des">{#GRAPH_OPTIONS}</p>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">{#GRAPH_SHOW_RATIO}</div> <div class="col-12 col-sm-3 my-2">{#GRAPH_SHOW_RATIO}</div>
<div class="col-12 col-sm-9"><input type="number" name="disp_graph_ratio" min="0" max="100"></select></div> <div class="col-12 col-sm-9"><input type="number" name="disp_graph_ratio" min="0" max="100"></div>
</div> </div>
<div id="graphSize"></div> <div id="graphSize"></div>
</div> </div>
</fieldset> </fieldset>
</div> </div>
<!--ENDIF_PLUGIN_DISPLAY-->
<div class="row mb-4 mt-4"> <div class="row mb-4 mt-4">
<div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div> <div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div>
@ -324,8 +326,8 @@
<div class="col-12 col-sm-9"> <div class="col-12 col-sm-9">
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8"> <form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
<div class="row"> <div class="row">
<div class="col-12 col-sm-8 my-2"><input type="file" name="upload"></div> <div class="col-12 col-sm-8 my-2"><input type="file" id="importFileInput" name="upload"></div>
<div class="col-12 col-sm-4 my-2"><input type="button" class="btn" value="Import" onclick="hide()"></div> <div class="col-12 col-sm-4 my-2"><input type="button" id="importButton" class="btn" value="Import" onclick="hide()"></div>
</div> </div>
</form> </form>
</div> </div>
@ -345,6 +347,7 @@
var maxInv = 0; var maxInv = 0;
var ts = 0; var ts = 0;
/*IF_ESP8266*/
var esp8266pins = [ var esp8266pins = [
[255, "{#PIN_OFF}"], [255, "{#PIN_OFF}"],
[0, "D3 (GPIO0)"], [0, "D3 (GPIO0)"],
@ -365,6 +368,7 @@
[15, "D8 (GPIO15)"], [15, "D8 (GPIO15)"],
[16, "D0 (GPIO16 - {#PIN_NO_IRQ})"] [16, "D0 (GPIO16 - {#PIN_NO_IRQ})"]
]; ];
/*ENDIF_ESP8266*/
/*IF_ESP32*/ /*IF_ESP32*/
var esp32pins = [ var esp32pins = [
@ -396,6 +400,7 @@
[36, "VP (GPIO36, {#PIN_INPUT_ONLY})"], [36, "VP (GPIO36, {#PIN_INPUT_ONLY})"],
[39, "VN (GPIO39, {#PIN_INPUT_ONLY})"] [39, "VN (GPIO39, {#PIN_INPUT_ONLY})"]
]; ];
/*IF_ESP32-S2*/
var esp32sXpins = [ var esp32sXpins = [
[255, "off / default"], [255, "off / default"],
[0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
@ -444,6 +449,58 @@
[47, "GPIO47"], [47, "GPIO47"],
[48, "GPIO48"], [48, "GPIO48"],
]; ];
/*ENDIF_ESP32-S2*/
/*IF_ESP32-S3*/
var esp32sXpins = [
[255, "off / default"],
[0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
[1, "GPIO1"],
[2, "GPIO2"],
[3, "GPIO3"],
[4, "GPIO4 (CMT CSB)"],
[5, "GPIO5 (CMT SDIO)"],
[6, "GPIO6 (CMT SCLK)"],
[7, "GPIO7"],
[8, "GPIO8 (CMT GPIO3)"],
[9, "GPIO9 (DATA display)"],
[10, "GPIO10 (SCK display)"],
[11, "GPIO11 (CS display)"],
[12, "GPIO12 (DC display)"],
[13, "GPIO13 (RST display)"],
[14, "GPIO14 (BUSY display)"],
[15, "GPIO15"],
[16, "GPIO16"],
[17, "GPIO17"],
[18, "GPIO18"],
[19, "GPIO19 ({#PIN_DONT_USE} - USB-)"],
[20, "GPIO20 ({#PIN_DONT_USE} - USB+)"],
[21, "GPIO21 (CMT FCSB)"],
[26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"],
[27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"],
[28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"],
[29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"],
[30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"],
[31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"],
[32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"],
[33, "GPIO33 (not exposed on S3-WROOM modules)"],
[34, "GPIO34 (not exposed on S3-WROOM modules)"],
[35, "GPIO35 (MOSI NRF24)"],
[36, "GPIO36 (SCK NRF24)"],
[37, "GPIO37 (CSN NRF24)"],
[38, "GPIO38 (CE NRF24)"],
[39, "GPIO39 (SCK ETH)"],
[40, "GPIO40 (MOSI ETH)"],
[41, "GPIO41 (MISO ETH)"],
[42, "GPIO42 (CS ETH)"],
[43, "GPIO43 (RST ETH)"],
[44, "GPIO44 (INT ETH)"],
[45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"],
[46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"],
[47, "GPIO47 (IRQ NRF24)"],
[48, "GPIO48 (MISO NRF24)"],
];
/*ENDIF_ESP32-S3*/
/*IF_ESP32-C3*/
var esp32c3pins = [ var esp32c3pins = [
[255, "off / default"], [255, "off / default"],
[0, "GPIO0"], [0, "GPIO0"],
@ -469,6 +526,7 @@
[20, "GPIO20 (RX)"], [20, "GPIO20 (RX)"],
[21, "GPIO21 (TX)"], [21, "GPIO21 (TX)"],
]; ];
/*ENDIF_ESP32-C3*/
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
var nrfPa = [ var nrfPa = [
[0, "MIN ({#PIN_RECOMMENDED})"], [0, "MIN ({#PIN_RECOMMENDED})"],
@ -549,12 +607,6 @@
setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000) setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000)
} }
function scan() {
var obj = {cmd: "scan_wifi", token: "*"}
getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj));
setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 5000);
}
function syncTime() { function syncTime() {
var obj = {cmd: "sync_ntp", token: "*"} var obj = {cmd: "sync_ntp", token: "*"}
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj)) getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj))
@ -566,6 +618,22 @@
getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj)); getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj));
} }
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.querySelector('#importFileInput');
const button = document.querySelector('#importButton');
button.disabled = true;
button.title = "Please select a file first";
fileInput.addEventListener('change', () => {
if (fileInput.value) {
button.disabled = false;
button.title = "";
} else {
button.disabled = true;
button.title = "Please select a file first";
}
});
});
function hide() { function hide() {
document.getElementById("form").submit(); document.getElementById("form").submit();
var e = document.getElementById("content"); var e = document.getElementById("content");
@ -605,53 +673,55 @@
} }
function ivGlob(obj) { function ivGlob(obj) {
for(var i of [["invInterval", "interval"], ["yldEff", "yldEff"]]) for(var i of [["invInterval", "interval"]])
document.getElementsByName(i[0])[0].value = obj[i[1]]; document.getElementsByName(i[0])[0].value = obj[i[1]];
for(var i of ["Mid", "ComStop", "NotAvail", "MaxMid"]) for(var i of ["Mid", "ComStop", "ComStart", "NotAvail", "MaxMid"])
document.getElementsByName("invRst"+i)[0].checked = obj["rst" + i]; document.getElementsByName("invRst"+i)[0].checked = obj["rst" + i];
document.getElementsByName("strtWthtTm")[0].checked = obj["strtWthtTm"]; document.getElementsByName("strtWthtTm")[0].checked = obj["strtWthtTm"];
document.getElementsByName("rdGrid")[0].checked = obj["rdGrid"]; document.getElementsByName("rdGrid")[0].checked = obj["rdGrid"];
} }
function parseSys(obj) { function parseSys(obj) {
for(var i of [["device", "device_name"], ["ssid", "ssid"], ["ap_pwd", "ap_pwd"]]) document.getElementsByName("device")[0].value = obj.device_name;
document.getElementsByName(i[0])[0].value = obj[i[1]]; for(var i of [["ssid", "ssid"], ["ap_pwd", "ap_pwd"]])
document.getElementsByName("hidd")[0].checked = obj["hidd"]; document.getElementsByName(i[0])[0].value = obj.network[i[1]];
document.getElementsByName("darkMode")[0].checked = obj["dark_mode"]; document.getElementsByName("hidd")[0].checked = obj.network.hidd;
document.getElementsByName("schedReboot")[0].checked = obj["sched_reboot"];
document.getElementsByName("darkMode")[0].checked = obj.dark_mode;
document.getElementsByName("schedReboot")[0].checked = obj.sched_reboot;
e = document.getElementsByName("adminpwd")[0]; e = document.getElementsByName("adminpwd")[0];
if(!obj["pwd_set"]) if(!obj.pwd_set)
e.value = ""; e.value = "";
var d = document.getElementById("prot_mask"); var d = document.getElementById("prot_mask");
var a = ["Index", "{#NAV_LIVE}", "{#NAV_WEBSERIAL}", "{#NAV_SETTINGS}", "Update", "System", "{#NAV_HISTORY}"]; var a = ["Index", "{#NAV_LIVE}", "{#NAV_WEBSERIAL}", "{#NAV_SETTINGS}", "Update", "System", "{#NAV_HISTORY}"];
var el = []; var el = [];
for(var i = 0; i < 7; i++) { for(var i = 0; i < 7; i++) {
var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i)); var chk = ((obj.prot_mask & (1 << i)) == (1 << i));
el.push(mlCb("protMask" + i, a[i], chk)) el.push(mlCb("protMask" + i, a[i], chk))
} }
d.append(...el); d.append(...el);
var tz = []
for(i = 0; i < 24; i += 0.5)
tz.push([i, ((i-12 > 0) ? "+" : "") + String(i-12)]);
document.getElementById("timezone").append(sel("timezone", tz, obj.timezone + 12))
var region = [[0, "Europe (860 - 870 MHz)"], [1, "USA, Indonesia (905 - 925 MHz)"], [2, "Brazil (915 - 928 MHz)"]]
document.getElementById("region").append(sel("region", region, obj.region))
} }
function parseGeneric(obj) { function parseGeneric(obj) {
parseNav(obj); parseNav(obj)
parseESP(obj); parseESP(obj)
parseRssi(obj); parseRssi(obj)
parseTitle(obj)
if(0 != obj.cst_lnk.length) { if(0 != obj.cst_lnk.length) {
document.getElementsByName("cstLnk")[0].value = obj.cst_lnk document.getElementsByName("cstLnk")[0].value = obj.cst_lnk
document.getElementsByName("cstLnkTxt")[0].value = obj.cst_lnk_txt document.getElementsByName("cstLnkTxt")[0].value = obj.cst_lnk_txt
} }
ts = obj["ts_now"]; ts = obj.ts_now;
window.setInterval("tick()", 1000); window.setInterval("tick()", 1000);
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 parseStaticIp(obj) { function parseStaticIp(obj) {
@ -730,7 +800,7 @@
cbEn.checked = (obj.enabled); cbEn.checked = (obj.enabled);
cbDisNightCom.checked = (obj.disnightcom); cbDisNightCom.checked = (obj.disnightcom);
var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null); var ser = ml("input", {name: "ser", class: "text", type: "text", pattern: "[0-9a-fA-F]{12}", value: obj.serial}, null);
var html = ml("div", {}, [ var html = ml("div", {}, [
tabs(["{#TAB_GENERAL}", "{#TAB_INPUTS}", "{#TAB_RADIO}", "{#TAB_ADVANCED}"]), tabs(["{#TAB_GENERAL}", "{#TAB_INPUTS}", "{#TAB_RADIO}", "{#TAB_ADVANCED}"]),
ml("div", {id: "div{#TAB_GENERAL}", class: "tab-content"}, [ ml("div", {id: "div{#TAB_GENERAL}", class: "tab-content"}, [
@ -786,7 +856,8 @@
case 0x1000: nrf = true; break; case 0x1000: nrf = true; break;
case 0x1100: case 0x1100:
switch(sn & 0x000f) { switch(sn & 0x000f) {
case 0x0004: nrf = false; break; case 0x0004:
case 0x0005: nrf = false; break;
default: nrf = true; break; default: nrf = true; break;
} }
break; break;
@ -802,11 +873,16 @@
ser.dispatchEvent(new Event('change')); ser.dispatchEvent(new Event('change'));
function ivSave() { function ivSave() {
var o = new Object(); var o = {}
o.cmd = "save_iv" o.cmd = "save_iv"
o.token = "*" o.token = "*"
o.id = obj.id o.id = obj.id
o.ser = parseInt(document.getElementsByName("ser")[0].value, 16);
let sn = document.getElementsByName("ser")[0].value
if(sn[0] == 'A')
sn = convHerf(sn)
o.ser = parseInt(sn, 16)
o.name = document.getElementsByName("name")[0].value; o.name = document.getElementsByName("name")[0].value;
o.en = document.getElementsByName("enable")[0].checked; o.en = document.getElementsByName("enable")[0].checked;
o.ch = []; o.ch = [];
@ -826,6 +902,30 @@
getAjax("/api/setup", cb, "POST", JSON.stringify(o)); getAjax("/api/setup", cb, "POST", JSON.stringify(o));
} }
function convHerf(sn) {
let sn_int = 0n;
const CHARS = "0123456789ABCDEFGHJKLMNPRSTUVWXY";
for (let i = 0; i < 9; ++i) {
const pos = CHARS.indexOf(sn[i])
const shift = 42 - 5 * i - (i <= 2 ? 0 : 2)
sn_int |= BigInt(pos) << BigInt(shift)
}
let first4Hex = (sn_int >> 32n) & 0xFFFFn
if (first4Hex === 0x2841n)
first4Hex = 0x1121n
else if (first4Hex === 0x2821n)
first4Hex = 0x1141n
else if (first4Hex === 0x2801n)
first4Hex = 0x1161n
sn_int = (sn_int & ~(0xFFFFn << 32n)) | (first4Hex << 32n);
return sn_int.toString(16)
}
function cb(obj2) { function cb(obj2) {
var e = document.getElementById("res"); var e = document.getElementById("res");
if(!obj2.success) if(!obj2.success)
@ -847,6 +947,7 @@
function del() { function del() {
var o = new Object(); var o = new Object();
o.cmd = "save_iv"; o.cmd = "save_iv";
o.token = "*"
o.id = obj.id; o.id = obj.id;
o.ser = 0; o.ser = 0;
o.name = ""; o.name = "";
@ -873,6 +974,8 @@
function parseMqtt(obj) { function parseMqtt(obj) {
for(var i of [["Addr", "broker"], ["Port", "port"], ["ClientId", "clientId"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]]) for(var i of [["Addr", "broker"], ["Port", "port"], ["ClientId", "clientId"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]])
document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]]; document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]];
document.getElementsByName("mqttJson")[0].checked = obj["json"];
document.getElementsByName("retain")[0].checked = obj.retain
} }
function parseNtp(obj) { function parseNtp(obj) {
@ -891,13 +994,21 @@
} }
} }
function parsePinout(obj, type, system) { function parsePinout(obj) {
var e = document.getElementById("pinout"); var e = document.getElementById("pinout");
var pinList = esp8266pins;
/*IF_ESP32*/ /*IF_ESP32*/
var pinList = esp32pins; var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; /*IF_ESP32-S2*/
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; pinList = esp32sXpins;
/*ENDIF_ESP32-S2*/
/*IF_ESP32-S3*/
pinList = esp32sXpins;
/*ENDIF_ESP32-S3*/
/*IF_ESP32-C3*/
pinList = esp32c3pins;
/*ENDIF_ESP32-C3*/
/*ELSE*/
var pinList = esp8266pins;
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']]; pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']];
for(p of pins) { for(p of pins) {
@ -924,16 +1035,24 @@
) )
} }
function parseNrfRadio(obj, objPin, type, system) { function parseNrfRadio(obj, objPin) {
var e = document.getElementById("rf24"); var e = document.getElementById("rf24");
var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox"); var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox");
en.checked = obj["en"]; en.checked = obj["en"];
var pinList = esp8266pins;
/*IF_ESP32*/ /*IF_ESP32*/
var pinList = esp32pins; var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; /*IF_ESP32-S2*/
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; pinList = esp32sXpins;
/*ENDIF_ESP32-S2*/
/*IF_ESP32-S3*/
pinList = esp32sXpins;
/*ENDIF_ESP32-S3*/
/*IF_ESP32-C3*/
pinList = esp32c3pins;
/*ENDIF_ESP32-C3*/
/*ELSE*/
var pinList = esp8266pins;
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
e.replaceChildren ( e.replaceChildren (
@ -943,11 +1062,11 @@
]) ])
); );
if ("ESP8266" == type) { /*IF_ESP32*/
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']]; var pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso']];
} else { /*ELSE*/
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso']]; var pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']];
} /*ENDIF_ESP32*/
for(p of pins) { for(p of pins) {
e.append( e.append(
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
@ -961,15 +1080,21 @@
} }
/*IF_ESP32*/ /*IF_ESP32*/
function parseCmtRadio(obj, type, system) { function parseCmtRadio(obj) {
var e = document.getElementById("cmt"); var e = document.getElementById("cmt");
var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox"); var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox");
var pinList = esp32pins; var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; /*IF_ESP32-S2*/
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; pinList = esp32sXpins;
/*ENDIF_ESP32-S2*/
/*IF_ESP32-S3*/
pinList = esp32sXpins;
/*ENDIF_ESP32-S3*/
/*IF_ESP32-C3*/
pinList = esp32c3pins;
/*ENDIF_ESP32-C3*/
en.checked = obj["en"]; en.checked = obj["en"];
e.replaceChildren ( e.replaceChildren (
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-3 my-2"}, "{#CMT_ENABLE}"), ml("div", {class: "col-8 col-sm-3 my-2"}, "{#CMT_ENABLE}"),
@ -996,6 +1121,42 @@
} }
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
/*IF_ETHERNET*/
function parseEth(obj) {
var e = document.getElementById("eth");
var en = inp("ethEn", null, null, ["cb"], "ethEn", "checkbox");
var pinList = esp32pins;
/*IF_ESP32-S2*/
pinList = esp32sXpins;
/*ENDIF_ESP32-S2*/
/*IF_ESP32-S3*/
pinList = esp32sXpins;
/*ENDIF_ESP32-S3*/
/*IF_ESP32-C3*/
pinList = esp32c3pins;
/*ENDIF_ESP32-C3*/
en.checked = obj["en"];
e.replaceChildren (
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-3 my-2"}, "{#ETH_ENABLE}"),
ml("div", {class: "col-4 col-sm-9"}, en)
])
);
pins = [['cs', 'ethCs'], ['sclk', 'ethSclk'], ['miso', 'ethMiso'], ['mosi', 'ethMosi'], ['irq', 'ethIrq'], ['reset', 'ethRst']];
for(p of pins) {
e.append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"},
sel(p[1], pinList, obj[p[0]])
)
])
);
}
}
/*ENDIF_ETHERNET*/
function parseSerial(obj) { function parseSerial(obj) {
var e = document.getElementById("serialCb") 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}"]] 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}"]]
@ -1011,12 +1172,21 @@
} }
} }
/*IF_PLUGIN_DISPLAY*/
function parseDisplay(obj, type, system) { function parseDisplay(obj, type, system) {
var pinList = esp8266pins;
/*IF_ESP32*/ /*IF_ESP32*/
var pinList = esp32pins; var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; /*IF_ESP32-S2*/
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; pinList = esp32sXpins;
/*ENDIF_ESP32-S2*/
/*IF_ESP32-S3*/
pinList = esp32sXpins;
/*ENDIF_ESP32-S3*/
/*IF_ESP32-C3*/
pinList = esp32c3pins;
/*ENDIF_ESP32-C3*/
/*ELSE*/
var pinList = esp8266pins;
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
for(var i of ["disp_pwr"]) for(var i of ["disp_pwr"])
@ -1152,6 +1322,7 @@
setHide("screenSaver", !optionsMap.get(dispType)[2]); setHide("screenSaver", !optionsMap.get(dispType)[2]);
setHide("pirPin", !(optionsMap.get(dispType)[2] && (screenSaver==2))); // show pir pin only for motion screensaver setHide("pirPin", !(optionsMap.get(dispType)[2] && (screenSaver==2))); // show pir pin only for motion screensaver
} }
/*ENDIF_PLUGIN_DISPLAY*/
function tick() { function tick() {
document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000))); document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000)));
@ -1159,42 +1330,28 @@
function parse(root) { function parse(root) {
if(null != root) { if(null != root) {
parseGeneric(root["generic"]); parseGeneric(root.generic);
parseSys(root["system"]); parseSys(root.system);
parseStaticIp(root["static_ip"]); parseStaticIp(root.static_ip);
parseMqtt(root["mqtt"]); parseMqtt(root.mqtt);
parseNtp(root["ntp"]); parseNtp(root.ntp);
parseSun(root["sun"]); parseSun(root.sun);
parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]); parsePinout(root.pinout);
parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]); parseNrfRadio(root.radioNrf, root.pinout);
/*IF_ESP32*/ /*IF_ESP32*/
parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]); parseCmtRadio(root.radioCmt);
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
parseSerial(root["serial"]); /*IF_ETHERNET*/
parseDisplay(root["display"], root["system"]["esp_type"], root["system"]); parseEth(root.eth)
/*ENDIF_ETHERNET*/
parseSerial(root.serial);
/*IF_PLUGIN_DISPLAY*/
parseDisplay(root.display, root.system.esp_type, root.system);
/*ENDIF_PLUGIN_DISPLAY*/
getAjax("/api/inverter/list", parseIv); getAjax("/api/inverter/list", parseIv);
} }
} }
function listNetworks(root) {
var s = document.getElementById("networks");
selDelAllOpt(s);
if(root["networks"].length > 0) {
s.appendChild(opt("-1", "{#NETWORK_PLEASE_SELECT}"));
for(i = 0; i < root["networks"].length; i++) {
s.appendChild(opt(root["networks"][i]["ssid"], root["networks"][i]["ssid"] + " (" + root["networks"][i]["rssi"] + " dBm)"));
}
} else
s.appendChild(opt("-1", "{#NO_NETWORK_FOUND}"));
}
function selNet() {
var s = document.getElementById("networks");
var e = document.getElementsByName("ssid")[0];
if(-1 != s.value)
e.value = s.value;
}
getAjax("/api/setup", parse); getAjax("/api/setup", parse);
</script> </script>
</body> </body>

40
src/web/html/style.css

@ -33,13 +33,17 @@ textarea {
color: var(--fg2); color: var(--fg2);
} }
svg rect {fill: #00A;} svg polyline {
svg.chart { fill-opacity: .5;
background: #f2f2f2; stroke-width: 1;
border: 2px solid gray; }
padding: 1px;
svg text {
font-size: x-small;
fill: var(--chart-text);
} }
div.chartDivContainer { div.chartDivContainer {
padding: 1px; padding: 1px;
margin: 1px; margin: 1px;
@ -139,7 +143,7 @@ svg.icon {
background-color: var(--nav-active); background-color: var(--nav-active);
} }
span.seperator { span.separator {
width: 100%; width: 100%;
height: 1px; height: 1px;
margin: 5px 0 5px; margin: 5px 0 5px;
@ -559,7 +563,13 @@ input.btn {
cursor: pointer; cursor: pointer;
} }
input.btn:hover { input.btn:disabled {
background-color: var(--primary-disabled);
color: #888;
cursor: not-allowed;
}
input.btn:not(:disabled):hover {
background-color: #044e86; background-color: #044e86;
} }
@ -677,7 +687,7 @@ div.hr {
border-radius: 3px; border-radius: 3px;
display: inline-block; display: inline-block;
position: absolute; position: absolute;
transform: translate(-50%,-100%); transform: translate(-50%,-50%);
margin:0 auto; margin:0 auto;
color: var(--fg2); color: var(--fg2);
min-width: 100px; min-width: 100px;
@ -748,6 +758,7 @@ div.hr {
font-family: inherit; font-family: inherit;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
color: var(--fg);
} }
button.close { button.close {
@ -848,3 +859,16 @@ ul {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
.progress {
display: flex;
height: 1rem;
overflow: hidden;
background-color: #e9ecef;
border-radius: .25rem;
}
.progress-bar {
display: flex;
background-color: var(--primary);
}

120
src/web/html/system.html

@ -8,29 +8,44 @@
{#HTML_NAV} {#HTML_NAV}
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<div id="info" class="col-sm-12 col-md-6 mt-3"></div> <div id="info" class="col-sm-12 col-md-10 mt-3"></div>
<div id="html" class="mt-3 mb-3"></div> <div id="html" class="mt-3 mb-3"></div>
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}
<script type="text/javascript"> <script type="text/javascript">
function parseGeneric(obj) { function parseGeneric(obj) {
parseNav(obj); parseNav(obj)
parseESP(obj); parseESP(obj)
parseRssi(obj); parseRssi(obj)
parseTitle(obj)
}
function parseUptime(up) {
var days = parseInt(up / 86400) % 365
var hrs = parseInt(up / 3600) % 24
var min = parseInt(up / 60) % 60
var sec = up % 60
var str = days + " day"
if(1 != days)
str += "s"
str += ", " + ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2)
return ml("span", {}, str)
} }
function parseSysInfo(obj) { function parseSysInfo(obj) {
const data = ["sdk", "cpu_freq", "chip_revision", lines = [
"chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime", tr("{#DEVICE_NAME}", obj.device_name),
"flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag", tr("{#UPTIME}", parseUptime(obj.generic.ts_uptime)),
"max_free_blk", "version", "modules", "env", "core_version", "reboot_reason"]; tr("{#REBOOT_REASON}", obj.chip.reboot_reason),
tr("{#ENVIRONMENT}", obj.generic.env + " ({#BUILD_OPTIONS}: " + obj.generic.modules + ")"),
lines = []; tr("Version", obj.generic.version + " - " + obj.generic.build),
for (const [key, value] of Object.entries(obj)) { tr("Chip", "CPU: " + obj.chip.cpu_freq + "MHz, " + obj.chip.cores + " Core(s)"),
if(!data.includes(key) || (typeof value == 'undefined')) continue; tr("Chip Model", obj.chip.model)
lines.push(tr(key.replace('_', ' '), value)); ]
}
document.getElementById("info").append( document.getElementById("info").append(
headline("System Information"), headline("System Information"),
@ -46,9 +61,9 @@
function irqBadge(state) { function irqBadge(state) {
switch(state) { switch(state) {
case 0: return badge(false, "{#UNKNOWN}", "warning"); break; case 0: return badge(false, "unknown", "warning"); break;
case 1: return badge(true, "{#TRUE}"); break; case 1: return badge(true, "true"); break;
default: return badge(false, "{#FALSE}"); break; default: return badge(false, "false"); break;
} }
} }
@ -58,15 +73,15 @@
if(obj.radioNrf.en) { if(obj.radioNrf.en) {
lines = [ lines = [
tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")), tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")),
tr("{#IRQ_WORKING}", irqBadge(obj.radioNrf.irqOk)), tr("{#INTR_PIN_WORKING}", irqBadge(obj.radioNrf.irqOk)),
tr("{#NRF24_DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"), tr("NRF24 {#DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"),
tr("DTU Radio ID", obj.radioNrf.sn) tr("DTU {#RADIO} ID", obj.radioNrf.sn)
]; ];
} else } else
lines = [tr("NRF24L01", badge(false, "{#NOT_ENABLED}"))]; lines = [tr("NRF24L01", badge(false, "{#NOT} {#ENABLED}"))];
document.getElementById("info").append( document.getElementById("info").append(
headline("{#NRF24_RADIO}"), headline("{#RADIO} NRF24"),
ml("table", {class: "table"}, ml("table", {class: "table"},
ml("tbody", {}, lines) ml("tbody", {}, lines)
) )
@ -76,14 +91,14 @@
if(obj.radioCmt.en) { if(obj.radioCmt.en) {
cmt = [ cmt = [
tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")), tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")),
tr("{#IRQ_WORKING}", irqBadge(obj.radioCmt.irqOk)), tr("{#INTR_PIN_WORKING}", irqBadge(obj.radioCmt.irqOk)),
tr("DTU Radio ID", obj.radioCmt.sn) tr("DTU {#RADIO} ID", obj.radioCmt.sn)
]; ];
} else } else
cmt = [tr("CMT2300A", badge(false, "{#NOT_ENABLED}"))]; cmt = [tr("CMT2300A", badge(false, "{#NOT} {#ENABLED}"))];
document.getElementById("info").append( document.getElementById("info").append(
headline("{#CMT_RADIO}"), headline("{#RADIO} CMT"),
ml("table", {class: "table"}, ml("table", {class: "table"},
ml("tbody", {}, cmt) ml("tbody", {}, cmt)
) )
@ -91,16 +106,32 @@
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
} }
function parseNetwork(obj, gen) {
lines = [
tr("{#CONNECTION}", ((obj.wired) ? "{#WIRED}" : "{#WIFI} (SSID: " + obj.ssid + ", RSSI: " + gen.wifi_rssi + ", CH: " + obj.wifi_channel + ")")),
tr("Hostname", gen.host),
tr("IP {#ADDRESS}", obj.ip),
tr("MAC {#ADDRESS}", obj.mac)
]
document.getElementById("info").append(
headline("{#NETWORK}"),
ml("table", {class: "table"},
ml("tbody", {}, lines)
)
);
}
function parseMqtt(obj) { function parseMqtt(obj) {
if(obj.enabled) { if(obj.enabled) {
lines = [ lines = [
tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "{#TRUE}" : "{#FALSE}"))), tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "true" : "false"))),
tr("#TX", obj.tx_cnt), tr("#TX", obj.tx_cnt),
tr("#RX", obj.rx_cnt) tr("#RX", obj.rx_cnt)
]; ]
} else } else
lines = tr("{#ENABLED}", badge(false, "{#FALSE}")); lines = tr("enabled", badge(false, "false"));
document.getElementById("info").append( document.getElementById("info").append(
headline("MqTT"), headline("MqTT"),
@ -110,17 +141,34 @@
); );
} }
function parseMemory(obj) {
lines = [
tr("{#FLASH_SIZE}", obj.flash_size / 1024 / 1024 + "MB"),
tr("{#CONFIG_PARTITION} (" + Math.round(obj.par_used_spiffs / 1024) + "kB of " + obj.par_size_spiffs / 1024 + "kB)", progress(obj.par_used_spiffs / obj.par_size_spiffs * 100)),
tr("{#FIRMWARE_PARTITION} (" + Math.round(obj.par_used_app0 / 1024) + "kB of " + obj.par_size_app0 / 1024 + "kB)", progress(obj.par_used_app0 / obj.par_size_app0 * 100)),
tr("Heap (" + Math.round(obj.heap_free / 1024) + "kB of " + Math.round(obj.heap_total / 1024) + "kB)", progress(obj.heap_free / obj.heap_total * 100)),
tr("Heap {#MAX_FREE_BLOCK}", Math.round(obj.heap_max_free_blk / 1024) + "kB (Fragmentation: " + obj.heap_frag + ")")
]
document.getElementById("info").append(
headline("{#MEMORY}"),
ml("table", {class: "table"},
ml("tbody", {}, lines)
)
);
}
function parseIndex(obj) { function parseIndex(obj) {
if(obj.ts_sunrise > 0) { if(obj.ts_sunrise > 0) {
document.getElementById("info").append( document.getElementById("info").append(
headline("{#SUN}"), headline("Sun"),
ml("table", {class: "table"}, ml("table", {class: "table"},
ml("tbody", {}, [ ml("tbody", {}, [
tr("{#SUNRISE}", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')), tr("{#SUNRISE}", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')),
tr("{#SUNSET}", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')), tr("{#SUNSET}", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')),
tr("{#COMMUNICATION_START}", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')), tr("{#COMMUNICATION_START}", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')),
tr("{#COMMUNICATION_STOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')), tr("{#COMMUNICATION_STOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')),
tr("{#NIGHT_BEHAVE}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning")) tr("{#NIGHT_BEHAVIOR}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning"))
]) ])
) )
); );
@ -137,10 +185,12 @@
meta.content = obj.refresh + "; URL=" + obj.refresh_url; meta.content = obj.refresh + "; URL=" + obj.refresh_url;
document.getElementsByTagName('head')[0].appendChild(meta); document.getElementsByTagName('head')[0].appendChild(meta);
} else if(null != obj.system) { } else if(null != obj.system) {
parseRadio(obj.system); parseRadio(obj.system)
parseMqtt(obj.system.mqtt); parseNetwork(obj.system.network, obj.system.generic)
parseSysInfo(obj.system); parseMqtt(obj.system.mqtt)
getAjax('/api/index', parseIndex); parseMemory(obj.system.memory)
parseSysInfo(obj.system)
getAjax('/api/index', parseIndex)
} }
document.getElementById("html").innerHTML = obj.html; document.getElementById("html").innerHTML = obj.html;
} }

23
src/web/html/update.html

@ -12,22 +12,39 @@
<legend class="des">{#SELECT_FILE} (*.bin)</legend> <legend class="des">{#SELECT_FILE} (*.bin)</legend>
<p>{#INSTALLED_VERSION}:<br/><span id="version" style="background-color: var(--input-bg); padding: 7px; display: block; margin: 3px;"></span></p> <p>{#INSTALLED_VERSION}:<br/><span id="version" style="background-color: var(--input-bg); padding: 7px; display: block; margin: 3px;"></span></p>
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8"> <form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update"> <input type="file" id="uploadFileInput" name="update">
<input type="button" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()"> <input type="button" id="uploadButton" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()">
</form> </form>
</fieldset> </fieldset>
<div class="row mt-4"> <div class="row mt-4">
<a href="https://fw.ahoydtu.de" target="_blank">{#DOWNLOADS}<a/> <a href="https://fw.ahoydtu.de" target="_blank">{#DOWNLOADS}</a>
</div> </div>
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}
<script type="text/javascript"> <script type="text/javascript">
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.querySelector('#uploadFileInput');
const button = document.querySelector('#uploadButton');
button.disabled = true;
button.title = "Please select a file first";
fileInput.addEventListener('change', () => {
if (fileInput.value) {
button.disabled = false;
button.title = "";
} else {
button.disabled = true;
button.title = "Please select a file first";
}
});
});
var env; var env;
function parseGeneric(obj) { function parseGeneric(obj) {
parseNav(obj) parseNav(obj)
parseESP(obj) parseESP(obj)
parseRssi(obj) parseRssi(obj)
parseTitle(obj)
env = obj.env env = obj.env
document.getElementById("version").innerHTML = "{#VERSION_FULL}_" + obj.env + ".bin" document.getElementById("version").innerHTML = "{#VERSION_FULL}_" + obj.env + ".bin"
} }

41
src/web/html/visualization.html

@ -21,6 +21,7 @@
var mNum = 0; var mNum = 0;
var total = Array(6).fill(0); var total = Array(6).fill(0);
var tPwrAck; var tPwrAck;
var totalsRendered = false
function getErrStr(code) { function getErrStr(code) {
if("ERR_AUTH") return "{#ERR_AUTH}" if("ERR_AUTH") return "{#ERR_AUTH}"
@ -33,10 +34,11 @@
function parseGeneric(obj) { function parseGeneric(obj) {
if(true == exeOnce){ if(true == exeOnce){
parseNav(obj); parseNav(obj)
parseESP(obj); parseESP(obj)
parseTitle(obj)
} }
parseRssi(obj); parseRssi(obj)
} }
function numBig(val, unit, des) { function numBig(val, unit, des) {
@ -54,11 +56,11 @@
]); ]);
} }
function numMid(val, unit, des, opt={class: "fs-6"}) { function numMid(val, unit, des, opt={class: "row"}) {
return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [ return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [
ml("div", {class: "row"}, ml("div", opt,
ml("div", {class: "col"}, [ ml("div", {class: "col"}, [
ml("span", opt, String(Math.round(val * 100) / 100)), ml("span", {class: "fs-6"}, String(Math.round(val * 100) / 100)),
ml("span", {class: "fs-8 mx-1"}, unit) ml("span", {class: "fs-8 mx-1"}, unit)
]) ])
), ),
@ -74,6 +76,7 @@
for(var i = 0; i < 6; i++) { for(var i = 0; i < 6; i++) {
total[i] = Math.round(total[i] * 100) / 100; total[i] = Math.round(total[i] * 100) / 100;
} }
totalsRendered = true
return ml("div", {class: "row mt-3 mb-5"}, return ml("div", {class: "row mt-3 mb-5"},
ml("div", {class: "col"}, [ ml("div", {class: "col"}, [
@ -104,7 +107,6 @@
total[4] += obj.ch[0][8]; // P_DC total[4] += obj.ch[0][8]; // P_DC
total[5] += obj.ch[0][10]; // Q_AC total[5] += obj.ch[0][10]; // Q_AC
} }
total[3] += obj.ch[0][11]; // MAX P_AC
total[1] += obj.ch[0][7]; // YieldDay total[1] += obj.ch[0][7]; // YieldDay
total[2] += obj.ch[0][6]; // YieldTotal total[2] += obj.ch[0][6]; // YieldTotal
@ -116,10 +118,11 @@
if(65535 != obj.power_limit_read) { if(65535 != obj.power_limit_read) {
pwrLimit = obj.power_limit_read + "&nbsp;%"; pwrLimit = obj.power_limit_read + "&nbsp;%";
if(0 != obj.max_pwr) if(0 != obj.max_pwr)
pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "&nbsp;W"; pwrLimit += ", " + (obj.max_pwr * obj.power_limit_read / 100).toFixed(1) + "&nbsp;W";
} }
var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000)); var maxAcPwrDate = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000))
var maxTempDate = toIsoDateStr(new Date(obj.ts_max_temp * 1000))
return ml("div", {class: "row mt-2"}, return ml("div", {class: "row mt-2"},
ml("div", {class: "col"}, [ ml("div", {class: "col"}, [
ml("div", {class: "p-2 " + clh}, ml("div", {class: "p-2 " + clh},
@ -134,7 +137,7 @@
ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() { ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() {
getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm); getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm);
}}, ("{#ALARMS}: " + obj.alarm_cnt))), }}, ("{#ALARMS}: " + obj.alarm_cnt))),
ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5].toFixed(1)) + t.innerText) ml("div", {class: "col a-r mx-2 mx-md-1 tooltip", data: (obj.ch[0][12] + t.innerText + "\n" + maxTempDate)}, String(obj.ch[0][5].toFixed(1)) + t.innerText)
]) ])
), ),
ml("div", {class: "p-2 " + clbg}, [ ml("div", {class: "p-2 " + clbg}, [
@ -145,7 +148,7 @@
]), ]),
ml("div", {class: "hr"}), ml("div", {class: "hr"}),
ml("div", {class: "row mt-2"},[ ml("div", {class: "row mt-2"},[
numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "fs-6 tooltip", data: maxAcPwr}), numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "row tooltip", data: maxAcPwrDate}),
numMid(obj.ch[0][8], "W", "{#DC_POWER}"), numMid(obj.ch[0][8], "W", "{#DC_POWER}"),
numMid(obj.ch[0][0], "V", "{#AC_VOLTAGE}"), numMid(obj.ch[0][0], "V", "{#AC_VOLTAGE}"),
numMid(obj.ch[0][1], "A", "{#AC_CURRENT}"), numMid(obj.ch[0][1], "A", "{#AC_CURRENT}"),
@ -240,20 +243,18 @@
]) ])
); );
var last = true;
for(var i = obj.id + 1; i < ivEn.length; i++) { for(var i = obj.id + 1; i < ivEn.length; i++) {
if((i != ivEn.length) && ivEn[i]) { if((i != ivEn.length) && ivEn[i]) {
last = false;
getAjax("/api/inverter/id/" + i, parseIv); getAjax("/api/inverter/id/" + i, parseIv);
break; return
} }
} }
if(last) {
if(mNum > 1) if(mNum > 1) {
if(!totalsRendered)
mIvHtml.unshift(totals()); mIvHtml.unshift(totals());
document.getElementById("live").replaceChildren(...mIvHtml);
} }
document.getElementById("live").replaceChildren(...mIvHtml);
} }
function parseIvAlarm(obj) { function parseIvAlarm(obj) {
@ -275,7 +276,7 @@
ml("div", {class: "col mt-3"}, String(a.str)), ml("div", {class: "col mt-3"}, String(a.str)),
ml("div", {class: "col mt-3"}, String(a.code)), ml("div", {class: "col mt-3"}, String(a.code)),
ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.start + offs) * 1000)))), ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.start + offs) * 1000)))),
ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.end + offs) * 1000)))) ml("div", {class: "col mt-3"}, (a.end == 0) ? "-" : String(toIsoTimeStr(new Date((a.end + offs) * 1000))))
]) ])
); );
} }
@ -514,7 +515,9 @@
ivEn = Object.values(Object.assign({}, obj["iv"])); ivEn = Object.values(Object.assign({}, obj["iv"]));
mIvHtml = []; mIvHtml = [];
mNum = 0; mNum = 0;
totalsRendered = false
total.fill(0); total.fill(0);
total[3] = obj.max_total_pwr
for(var i = 0; i < obj.iv.length; i++) { for(var i = 0; i < obj.iv.length; i++) {
if(obj.iv[i]) { if(obj.iv[i]) {
getAjax("/api/inverter/id/" + i, parseIv); getAjax("/api/inverter/id/" + i, parseIv);

255
src/web/html/wizard.html

@ -4,7 +4,7 @@
<title>{#NAV_WIZARD}</title> <title>{#NAV_WIZARD}</title>
{#HTML_HEADER} {#HTML_HEADER}
</head> </head>
<body> <body onload="init()">
<div id="wrapper"> <div id="wrapper">
<div class="container d-flex aic jc"> <div class="container d-flex aic jc">
<div id="con"></div> <div id="con"></div>
@ -14,6 +14,166 @@
var v; var v;
var found = false; var found = false;
var c = document.getElementById("con"); var c = document.getElementById("con");
var redirIp = "http://192.168.4.1/index"
/*IF_ESP32*/
var pinList = [
[255, "{#PIN_OFF}"],
[0, "GPIO0"],
[1, "TX (GPIO1)"],
[2, "GPIO2 (LED)"],
[3, "RX (GPIO3)"],
[4, "GPIO4"],
[5, "GPIO5"],
[12, "GPIO12 (HSPI MISO)"],
[13, "GPIO13 (HSPI MOSI)"],
[14, "GPIO14 (HSPI SCLK)"],
[15, "GPIO15"],
[16, "GPIO16"],
[17, "GPIO17"],
[18, "GPIO18 (VSPI SCLK)"],
[19, "GPIO19 (VSPI MISO)"],
[21, "GPIO21 (SDA)"],
[22, "GPIO22 (SCL)"],
[23, "GPIO23 (VSPI MOSI)"],
[25, "GPIO25"],
[26, "GPIO26"],
[27, "GPIO27"],
[32, "GPIO32"],
[33, "GPIO33"],
[34, "GPIO34 ({#PIN_INPUT_ONLY})"],
[35, "GPIO35 ({#PIN_INPUT_ONLY})"],
[36, "VP (GPIO36, {#PIN_INPUT_ONLY})"],
[39, "VN (GPIO39, {#PIN_INPUT_ONLY})"]
];
/*IF_ESP32-S2*/
pinList = [
[255, "off / default"],
[0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
[1, "GPIO1"],
[2, "GPIO2"],
[3, "GPIO3"],
[4, "GPIO4"],
[5, "GPIO5"],
[6, "GPIO6"],
[7, "GPIO7"],
[8, "GPIO8"],
[9, "GPIO9"],
[10, "GPIO10"],
[11, "GPIO11"],
[12, "GPIO12"],
[13, "GPIO13"],
[14, "GPIO14"],
[15, "GPIO15"],
[16, "GPIO16"],
[17, "GPIO17"],
[18, "GPIO18"],
[19, "GPIO19 ({#PIN_DONT_USE} - USB-)"],
[20, "GPIO20 ({#PIN_DONT_USE} - USB+)"],
[21, "GPIO21"],
[26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"],
[27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"],
[28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"],
[29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"],
[30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"],
[31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"],
[32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"],
[33, "GPIO33 (not exposed on S3-WROOM modules)"],
[34, "GPIO34 (not exposed on S3-WROOM modules)"],
[35, "GPIO35"],
[36, "GPIO36"],
[37, "GPIO37"],
[38, "GPIO38"],
[39, "GPIO39"],
[40, "GPIO40"],
[41, "GPIO41"],
[42, "GPIO42"],
[43, "GPIO43"],
[44, "GPIO44"],
[45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"],
[46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"],
[47, "GPIO47"],
[48, "GPIO48"],
];
/*ENDIF_ESP32-S2*/
/*IF_ESP32-S3*/
pinList = [
[255, "off / default"],
[0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
[1, "GPIO1"],
[2, "GPIO2"],
[3, "GPIO3"],
[4, "GPIO4"],
[5, "GPIO5"],
[6, "GPIO6"],
[7, "GPIO7"],
[8, "GPIO8"],
[9, "GPIO9"],
[10, "GPIO10"],
[11, "GPIO11"],
[12, "GPIO12"],
[13, "GPIO13"],
[14, "GPIO14"],
[15, "GPIO15"],
[16, "GPIO16"],
[17, "GPIO17"],
[18, "GPIO18"],
[19, "GPIO19 ({#PIN_DONT_USE} - USB-)"],
[20, "GPIO20 ({#PIN_DONT_USE} - USB+)"],
[21, "GPIO21"],
[26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"],
[27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"],
[28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"],
[29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"],
[30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"],
[31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"],
[32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"],
[33, "GPIO33 (not exposed on S3-WROOM modules)"],
[34, "GPIO34 (not exposed on S3-WROOM modules)"],
[35, "GPIO35"],
[36, "GPIO36"],
[37, "GPIO37"],
[38, "GPIO38"],
[39, "GPIO39"],
[40, "GPIO40"],
[41, "GPIO41"],
[42, "GPIO42"],
[43, "GPIO43"],
[44, "GPIO44"],
[45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"],
[46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"],
[47, "GPIO47"],
[48, "GPIO48"],
];
/*ENDIF_ESP32-S3*/
/*IF_ESP32-C3*/
pinList = [
[255, "off / default"],
[0, "GPIO0"],
[1, "GPIO1"],
[2, "GPIO2"],
[3, "GPIO3"],
[4, "GPIO4"],
[5, "GPIO5"],
[6, "GPIO6"],
[7, "GPIO7"],
[8, "GPIO8"],
[9, "GPIO9"],
[10, "GPIO10"],
[11, "GPIO11"],
[12, "GPIO12 (PSRAM/FLASH)"],
[13, "GPIO13 (PSRAM/FLASH)"],
[14, "GPIO14 (PSRAM/FLASH)"],
[15, "GPIO15 (PSRAM/FLASH)"],
[16, "GPIO16 (PSRAM/FLASH)"],
[17, "GPIO17 (PSRAM/FLASH)"],
[18, "GPIO18 ({#PIN_DONT_USE} - USB-)"],
[19, "GPIO19 ({#PIN_DONT_USE} - USB+)"],
[20, "GPIO20 (RX)"],
[21, "GPIO21 (TX)"],
];
/*ENDIF_ESP32-C3*/
/*ENDIF_ESP32*/
function sect(e1, e2) { function sect(e1, e2) {
return ml("div", {class: "row"}, [ return ml("div", {class: "row"}, [
@ -22,7 +182,36 @@
]) ])
} }
function wifi() { /*IF_ETHERNET*/
var pins = ['cs', 'sclk', 'miso', 'mosi', 'irq', 'reset']
function step1(obj) {
console.log(obj)
lst = []
for(p of pins) {
lst.push(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p.toUpperCase()),
ml("div", {class: "col-12 col-sm-9"},
sel(p, pinList, obj[p])
)
])
)
}
let en = inp("en", null, null, ["cb"], "en", "checkbox");
en.checked = obj["en"];
return sect("{#NETWORK_SETUP}", [
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8"}, "{#ETH_ENABLE}"),
ml("div", {class: "col-4"}, en)
]),
...lst,
ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_REBOOT}", onclick: () => {saveEth()}}, null))),
ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {onclick: () => {redirect()}}, "{#STOP_WIZARD}")))
])
}
/*ENDIF_ETHERNET*/
function step1Wifi() {
return ml("div", {}, [ return ml("div", {}, [
ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "{#WELCOME}"))), ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "{#WELCOME}"))),
ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#NETWORK_SETUP}"))), ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#NETWORK_SETUP}"))),
@ -30,7 +219,7 @@
sect("{#WIFI_MANUAL}", ml("input", {id: "man", type: "text"})), sect("{#WIFI_MANUAL}", ml("input", {id: "man", type: "text"})),
sect("{#WIFI_PASSWORD}", ml("input", {id: "pwd", type: "password"})), sect("{#WIFI_PASSWORD}", ml("input", {id: "pwd", type: "password"})),
ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_NEXT}", onclick: () => {saveWifi()}}, null))), ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_NEXT}", onclick: () => {saveWifi()}}, null))),
ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}"))) ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {onclick: () => {redirect()}}, "{#STOP_WIZARD}")))
]) ])
} }
@ -40,23 +229,35 @@
ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#TEST_CONNECTION}"))), ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#TEST_CONNECTION}"))),
sect("{#TRY_TO_CONNECT}", ml("span", {id: "state"}, "{#CONNECTING}")), sect("{#TRY_TO_CONNECT}", ml("span", {id: "state"}, "{#CONNECTING}")),
ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn hide", id: "btn", value: "{#BTN_FINISH}", onclick: () => {redirect()}}, null))), ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn hide", id: "btn", value: "{#BTN_FINISH}", onclick: () => {redirect()}}, null))),
ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}"))) ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {onclick: () => {redirect()}}, "{#STOP_WIZARD}")))
) )
v = setInterval(() => {getAjax('/api/setup/getip', printIp)}, 2500); v = setInterval(() => {getAjax('/api/setup/getip', printIp)}, 1000);
} }
function redirect() { function redirect() {
window.location.replace("http://192.168.4.1/") window.location.replace(redirIp)
} }
function printIp(obj) { function printIp(obj) {
if("0.0.0.0" != obj["ip"]) { if("0.0.0.0" != obj.ip) {
clearInterval(v) clearInterval(v)
setHide("btn", false) setHide("btn", false)
document.getElementById("state").innerHTML = "{#NETWORK_SUCCESS}" + obj.ip document.getElementById("state").innerHTML = "{#NETWORK_SUCCESS}" + obj.ip
} }
} }
/*IF_ETHERNET*/
function saveEth() {
let o = {
cmd: "save_eth",
en: document.getElementsByName("en")[0].checked
}
for(p of pins) {
o[p] = document.getElementsByName(p)[0].value
}
getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify(o));
}
/*ENDIF_ETHERNET*/
function saveWifi() { function saveWifi() {
var ssid = document.getElementById("net").value; var ssid = document.getElementById("net").value;
if(-1 == ssid) if(-1 == ssid)
@ -64,24 +265,36 @@
getAjax("/api/setup", ((o) => {if(!o.error) checkWifi()}), "POST", JSON.stringify({cmd: "save_wifi", ssid: ssid, pwd: document.getElementById("pwd").value})); getAjax("/api/setup", ((o) => {if(!o.error) checkWifi()}), "POST", JSON.stringify({cmd: "save_wifi", ssid: ssid, pwd: document.getElementById("pwd").value}));
} }
function nets(obj) { function init() {
var e = document.getElementById("net"); /*IF_ETHERNET*/
if(obj.networks.length > 0) { getAjax("/api/setup", ((o) => c.append(step1(o.eth))));
var a = [] /*ENDIF_ETHERNET*/
a.push(ml("option", {value: -1}, obj.networks.length + " {#NUM_NETWORKS_FOUND}")) function nets(obj) {
for(n of obj.networks) { clearInterval(v)
a.push(ml("option", {value: n.ssid}, n.ssid + " (" + n.rssi + "dBm)")) v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 4000)
found = true;
if(!obj.success)
return;
var e = document.getElementById("net");
if(obj.networks.length > 0) {
var a = []
a.push(ml("option", {value: -1}, obj.networks.length + " {#NUM_NETWORKS_FOUND}"))
for(n of obj.networks) {
a.push(ml("option", {value: n.ssid}, n.ssid + " (" + n.rssi + "dBm)"))
found = true;
}
e.replaceChildren(...a)
} }
e.replaceChildren(...a)
if("0.0.0.0" != obj.ip)
redirIp = "http://" + obj.ip + "/index"
} }
getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"}));
}
getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"})); c.append(step1Wifi())
c.append(wifi()) getAjax('/api/setup/networks', nets)
}
v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 2500);
</script> </script>
</body> </body>
</html> </html>

24
src/web/lang.h

@ -72,4 +72,28 @@
#define BTN_REBOOT "Reboot" #define BTN_REBOOT "Reboot"
#endif #endif
#ifdef LANG_DE
#define BTN_REBOOT "Ahoy neustarten"
#else /*LANG_EN*/
#define BTN_REBOOT "Reboot"
#endif
#ifdef LANG_DE
#define BTN_YES "ja"
#else /*LANG_EN*/
#define BTN_YES "yes"
#endif
#ifdef LANG_DE
#define BTN_NO "nein"
#else /*LANG_EN*/
#define BTN_NO "no"
#endif
#ifdef LANG_DE
#define BTN_COREDUMP "CoreDump herunterladen"
#else /*LANG_EN*/
#define BTN_COREDUMP "download CoreDump"
#endif
#endif /*__LANG_H__*/ #endif /*__LANG_H__*/

312
src/web/lang.json

@ -6,7 +6,7 @@
{ {
"token": "NAV_WIZARD", "token": "NAV_WIZARD",
"en": "Setup Wizard", "en": "Setup Wizard",
"de": "Daten" "de": "Einrichtungsassitent"
}, },
{ {
"token": "NAV_LIVE", "token": "NAV_LIVE",
@ -81,17 +81,22 @@
{ {
"token": "BTN_NEXT", "token": "BTN_NEXT",
"en": "next >>", "en": "next >>",
"de": "prüfen >>" "de": "speichern >>"
},
{
"token": "BTN_REBOOT",
"en": "reboot >>",
"de": "Ahoy neustarten >>"
}, },
{ {
"token": "TEST_CONNECTION", "token": "TEST_CONNECTION",
"en": "Test Connection", "en": "Test Connection",
"de": "Verbindung wird überprüft" "de": "Verbindung wird &uuml;berpr&uuml;ft"
}, },
{ {
"token": "TRY_TO_CONNECT", "token": "TRY_TO_CONNECT",
"en": "AhoyDTU is trying to connect to your WiFi", "en": "AhoyDTU is trying to connect to your WiFi",
"de": "AhoyDTU versucht eine Verindung mit deinem Netzwerk herzustellen" "de": "AhoyDTU versucht eine Verbindung mit Deinem Netzwerk herzustellen"
}, },
{ {
"token": "CONNECTING", "token": "CONNECTING",
@ -101,7 +106,7 @@
{ {
"token": "NETWORK_SUCCESS", "token": "NETWORK_SUCCESS",
"en": "success, got following IP in your network: ", "en": "success, got following IP in your network: ",
"de": "Verindung erfolgreich. AhoyDTU hat die folgende IP bekommen: " "de": "Verbindung erfolgreich. AhoyDTU hat die folgende IP bekommen: "
}, },
{ {
"token": "BTN_FINISH", "token": "BTN_FINISH",
@ -112,6 +117,36 @@
"token": "NUM_NETWORKS_FOUND", "token": "NUM_NETWORKS_FOUND",
"en": "Network(s) found", "en": "Network(s) found",
"de": "Netzwerk(e) gefunden" "de": "Netzwerk(e) gefunden"
},
{
"token": "PIN_OFF",
"en": "off / default",
"de": "aus / Standard"
},
{
"token": "PIN_NO_IRQ",
"en": "no IRQ!",
"de": "kein Interrupt!"
},
{
"token": "PIN_INPUT_ONLY",
"en": "in only",
"de": "nur Eingang"
},
{
"token": "PIN_DONT_USE",
"en": "DONT USE",
"de": "nicht benutzen"
},
{
"token": "PIN_NOT_AVAIL",
"en": "not available",
"de": "nicht verf&uuml;gbar"
},
{
"token": "ETH_ENABLE",
"en": "Ethernet enable",
"de": "Ethernet aktivieren"
} }
] ]
}, },
@ -186,7 +221,7 @@
{ {
"token": "LOG_PRINT_INVERTER_DATA", "token": "LOG_PRINT_INVERTER_DATA",
"en": "print inverter data", "en": "print inverter data",
"de": "Livedaten ausgeben" "de": "Inverterwerte ausgeben"
}, },
{ {
"token": "LOG_SERIAL_DEBUG", "token": "LOG_SERIAL_DEBUG",
@ -224,19 +259,9 @@
"de": "Netzwerke suchen" "de": "Netzwerke suchen"
}, },
{ {
"token": "BTN_SCAN", "token": "SCAN_WIFI",
"en": "scan", "en": "scan for WiFi networks",
"de": "Suche starten" "de": "nach WiFi Netzwerken suchen"
},
{
"token": "AVAIL_NETWORKS",
"en": "Avail Networks",
"de": "Verf&uuml;gbare Netzwerke"
},
{
"token": "NETWORK_NOT_SCANNED",
"en": "not scanned",
"de": "nicht gesucht"
}, },
{ {
"token": "SSID_HIDDEN", "token": "SSID_HIDDEN",
@ -295,23 +320,28 @@
}, },
{ {
"token": "INV_RESET_MIDNIGHT", "token": "INV_RESET_MIDNIGHT",
"en": "Reset values and YieldDay at midnight. ('Pause communication during night' need to be set)", "en": "Reset values and YieldDay at midnight",
"de": "Werte und Gesamtertrag um Mitternacht zur&uuml;cksetzen ('Kommunikation w&auml;hrend der Nacht pausieren' muss gesetzt sein)" "de": "Werte und Gesamtertrag um Mitternacht zur&uuml;cksetzen"
}, },
{ {
"token": "INV_PAUSE_SUNSET", "token": "INV_RESET_SUNSET",
"en": "Reset values at sunset", "en": "Reset values at sunset",
"de": "Werte bei Sonnenuntergang zur&uuml;cksetzen" "de": "Werte bei Sonnenuntergang zur&uuml;cksetzen"
}, },
{
"token": "INV_RESET_SUNRISE",
"en": "Reset values at sunrise",
"de": "Werte bei Sonnenaufgang zur&uuml;cksetzen"
},
{ {
"token": "INV_RESET_NOT_AVAIL", "token": "INV_RESET_NOT_AVAIL",
"en": "Reset values when inverter status is 'not available'", "en": "Reset values when inverter status is 'not available'",
"de": "Werte zur&uuml;cksetzen, sobald der Wechselrichter nicht erreichbar ist" "de": "Werte zur&uuml;cksetzen, sobald der Wechselrichter nicht erreichbar ist"
}, },
{ {
"token": "INV_RESET_MAX_MIDNIGHT", "token": "INV_RESET_MAX_VALUES",
"en": "Reset 'max' values at midnight", "en": "Include reset 'max' values",
"de": "Maximalwerte mitternachts zur&uuml;cksetzen" "de": "Maximalwerte auch zur&uuml;cksetzen"
}, },
{ {
"token": "INV_START_WITHOUT_TIME", "token": "INV_START_WITHOUT_TIME",
@ -323,11 +353,6 @@
"en": "Read Grid Profile", "en": "Read Grid Profile",
"de": "Grid-Profil auslesen" "de": "Grid-Profil auslesen"
}, },
{
"token": "INV_YIELD_EFF",
"en": "Yield Efficiency (default 1.0)",
"de": "Ertragseffizienz (Standard 1.0)"
},
{ {
"token": "NTP_INTERVAL", "token": "NTP_INTERVAL",
"en": "NTP Interval (in minutes, min. 5 minutes)", "en": "NTP Interval (in minutes, min. 5 minutes)",
@ -388,11 +413,21 @@
"en": "Password (optional)", "en": "Password (optional)",
"de": "Passwort (optional)" "de": "Passwort (optional)"
}, },
{
"token": "MQTT_JSON",
"en": "Payload as JSON",
"de": "Ausgabe als JSON"
},
{ {
"token": "MQTT_NOTE", "token": "MQTT_NOTE",
"en": "Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)", "en": "Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)",
"de": "Wechselrichterdaten in fixem Intervall schicken, auch wenn es keine &Auml;nderung gab. Ein Wert von '0' deaktiviert das fixe Intervall, die Wechselrichterdaten werden &uuml;bertragen, sobald neue zur Verf&uuml;gung stehen. (Standard: 0)" "de": "Wechselrichterdaten in fixem Intervall schicken, auch wenn es keine &Auml;nderung gab. Ein Wert von '0' deaktiviert das fixe Intervall, die Wechselrichterdaten werden &uuml;bertragen, sobald neue zur Verf&uuml;gung stehen. (Standard: 0)"
}, },
{
"token": "RETAIN",
"en": "enable retain flag",
"de": "'Retain Flag' aktivieren"
},
{ {
"token": "DISPLAY_CONFIG", "token": "DISPLAY_CONFIG",
"en": "Display Config", "en": "Display Config",
@ -601,7 +636,7 @@
{ {
"token": "BTN_INV_ADD", "token": "BTN_INV_ADD",
"en": "add Inverter", "en": "add Inverter",
"de": "Wechselrichter hinzufuegen" "de": "Wechselrichter hinzuf\u00FCgen"
}, },
{ {
"token": "INV_INPUT", "token": "INV_INPUT",
@ -626,7 +661,7 @@
{ {
"token": "TAB_INPUTS", "token": "TAB_INPUTS",
"en": "Inputs", "en": "Inputs",
"de": "Eingaenge" "de": "Eing&auml;nge"
}, },
{ {
"token": "TAB_RADIO", "token": "TAB_RADIO",
@ -728,6 +763,11 @@
"en": "CMT2300A radio enable", "en": "CMT2300A radio enable",
"de": "CMT2300A Funkmodul aktivieren" "de": "CMT2300A Funkmodul aktivieren"
}, },
{
"token": "ETH_ENABLE",
"en": "Ethernet enable",
"de": "Ethernet aktivieren"
},
{ {
"token": "DISP_NONE", "token": "DISP_NONE",
"en": "None", "en": "None",
@ -867,6 +907,151 @@
"token": "COMMUNICATING", "token": "COMMUNICATING",
"en": "communicating", "en": "communicating",
"de": "aktiv" "de": "aktiv"
},
{
"token": "NETWORK",
"en": "Network",
"de": "Netzwerk"
},
{
"token": "CONNECTION_TYPE",
"en": "connection",
"de": "Verbindung"
},
{
"token": "WIRED",
"en": "ethernet cable",
"de": "Netzwerkkabel"
},
{
"token": "WIFI",
"en": "WiFi",
"de": "WiFi"
},
{
"token": "DEVICE_NAME",
"en": "Device name",
"de": "Ger&auml;tename"
},
{
"token": "UPTIME",
"en": "Uptime",
"de": "Laufzeit"
},
{
"token": "REBOOT_REASON",
"en": "Reboot reason",
"de": "Grund des Neustarts"
},
{
"token": "ENVIRONMENT",
"en": "Environment",
"de": "Umgebung"
},
{
"token": "BUILD_OPTIONS",
"en": "build options",
"de": "Module"
},
{
"token": "ADDRESS",
"en": "Address",
"de": "Adresse"
},
{
"token": "NETWORK",
"en": "Network",
"de": "Netzwerk"
},
{
"token": "MEMORY",
"en": "Memory",
"de": "Speicher"
},
{
"token": "CONFIG_PARTITION",
"en": "Config Partition",
"de": "Konfiguration"
},
{
"token": "FIRMWARE_PARTITION",
"en": "Firmware Partition",
"de": "Firmware"
},
{
"token": "INTR_PIN_WORKING",
"en": "Interrupt Pin working",
"de": "Interrupt Pin funktioniert"
},
{
"token": "DATA_RATE",
"en": "Data Rate",
"de": "Datenrate"
},
{
"token": "RADIO",
"en": "Radio",
"de": "Funkmodul"
},
{
"token": "NOT",
"en": "not",
"de": "nicht"
},
{
"token": "CONNECTED",
"en": "connected",
"de": "verbunden"
},
{
"token": "ENABLED",
"en": "enabled",
"de": "aktiviert"
},
{
"token": "CONNECTION",
"en": "connection",
"de": "Verbindung"
},
{
"token": "FLASH_SIZE",
"en": "Flash size",
"de": "Speichergr&ouml;&szlig;e"
},
{
"token": "MAX_FREE_BLOCK",
"en": "max free block",
"de": "maximale freie Blockgr&ouml;&szlig;e"
},
{
"token": "SUNRISE",
"en": "sunrise",
"de": "Sonnenaufgang"
},
{
"token": "SUNSET",
"en": "sunset",
"de": "Sonnenuntergang"
},
{
"token": "COMMUNICATION_START",
"en": "Communication start",
"de": "Start der Kommunikation"
},
{
"token": "COMMUNICATION_STOP",
"en": "Communication stop",
"de": "Ende der Kommunikation"
},
{
"token": "NIGHT_BEHAVIOR",
"en": "Night behavior",
"de": "Verhalten bei Nacht"
},
{
"token": "COMMUNICATING",
"en": "communicating",
"de": "kommunizierend"
} }
] ]
}, },
@ -883,6 +1068,11 @@
"en": "autoscroll", "en": "autoscroll",
"de": "automatisch scrollen" "de": "automatisch scrollen"
}, },
{
"token": "BTN_MANUALSCROLL",
"en": "manual scroll",
"de": "manuell scrollen"
},
{ {
"token": "BTN_COPY", "token": "BTN_COPY",
"en": "copy", "en": "copy",
@ -897,6 +1087,21 @@
"token": "UPTIME", "token": "UPTIME",
"en": "uptime", "en": "uptime",
"de": "Laufzeit" "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"
} }
] ]
}, },
@ -973,6 +1178,16 @@
"en": "Error", "en": "Error",
"de": "Fehler" "de": "Fehler"
}, },
{
"token": "DAY",
"en": "day",
"de": "Tag"
},
{
"token": "S",
"en": "s",
"de": "e"
},
{ {
"token": "NTP_UNREACH", "token": "NTP_UNREACH",
"en": "NTP timeserver unreachable", "en": "NTP timeserver unreachable",
@ -986,7 +1201,7 @@
{ {
"token": "NIGHT_TIME", "token": "NIGHT_TIME",
"en": "Night time, inverter polling disabled", "en": "Night time, inverter polling disabled",
"de": "Wechselrichterabfrage deaktivert (Nacht)" "de": "Wechselrichterabfrage deaktiviert (Nacht)"
}, },
{ {
"token": "PAUSED_AT", "token": "PAUSED_AT",
@ -1503,35 +1718,30 @@
"en": "Total Power", "en": "Total Power",
"de": "Gesamtleistung" "de": "Gesamtleistung"
}, },
{
"token": "TOTAL_POWER_DAY",
"en": "Total Power Today",
"de": "Gesamtleistung heute"
},
{ {
"token": "TOTAL_YIELD_PER_DAY", "token": "TOTAL_YIELD_PER_DAY",
"en": "Total Yield per day", "en": "Total Yield per day",
"de": "Gesamtertrag pro Tag" "de": "Gesamtertrag pro Tag"
}, },
{
"token": "MAX_DAY",
"en": "maximum day",
"de": "Tagesmaximum"
},
{
"token": "LAST_VALUE",
"en": "last value",
"de": "letzter Wert"
},
{ {
"token": "MAXIMUM", "token": "MAXIMUM",
"en": "maximum value", "en": "Maximum",
"de": "Maximalwert" "de": "Maximum"
}, },
{ {
"token": "UPDATED", "token": "LAST_VALUE",
"en": "Updated every", "en": "Last value",
"de": "aktualisiert alle" "de": "Letzter Wert"
}, },
{ {
"token": "SECONDS", "token": "YIELD_DAY",
"en": "seconds", "en": "Yield day",
"de": "Sekunden" "de": "Tagesertrag"
} }
] ]
} }

100
src/web/web.h

@ -16,11 +16,7 @@
#include "../appInterface.h" #include "../appInterface.h"
#include "../hm/hmSystem.h" #include "../hm/hmSystem.h"
#include "../utils/helper.h" #include "../utils/helper.h"
#if defined(ETHERNET)
#include "AsyncWebServer_ESP32_W5500.h"
#else /* defined(ETHERNET) */
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
#endif /* defined(ETHERNET) */
#include "html/h/api_js.h" #include "html/h/api_js.h"
#include "html/h/colorBright_css.h" #include "html/h/colorBright_css.h"
#include "html/h/colorDark_css.h" #include "html/h/colorDark_css.h"
@ -41,14 +37,19 @@
#define WEB_SERIAL_BUF_SIZE 2048 #define WEB_SERIAL_BUF_SIZE 2048
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLed2", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"}; const char* const pinArgNames[] = {
"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0",
"pinLed1", "pinLed2", "pinLedHighActive", "pinLedLum", "pinCmtSclk",
"pinSdio", "pinCsb", "pinFcsb", "pinGpio3"
#if defined (ETHERNET)
, "ethCs", "ethSclk", "ethMiso", "ethMosi", "ethIrq", "ethRst"
#endif
};
template <class HMSYSTEM> template <class HMSYSTEM>
class Web { class Web {
public: public:
Web(void) : mWeb(80), mEvts("/events") { Web(void) : mWeb(80), mEvts("/events") {}
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
}
void setup(IApp *app, HMSYSTEM *sys, settings_t *config) { void setup(IApp *app, HMSYSTEM *sys, settings_t *config) {
mApp = app; mApp = app;
@ -56,7 +57,8 @@ class Web {
mConfig = config; mConfig = config;
DPRINTLN(DBG_VERBOSE, F("app::setup-on")); DPRINTLN(DBG_VERBOSE, F("app::setup-on"));
mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1)); mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1, true));
mWeb.on("/index", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1, false));
mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1)); mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1));
mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1)); mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1));
mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1)); mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1));
@ -74,6 +76,7 @@ class Web {
mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1));
mWeb.on("/wizard", HTTP_GET, std::bind(&Web::onWizard, this, std::placeholders::_1)); mWeb.on("/wizard", HTTP_GET, std::bind(&Web::onWizard, this, std::placeholders::_1));
mWeb.on("/generate_204", HTTP_GET, std::bind(&Web::onWizard, this, std::placeholders::_1)); //Android captive portal
mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1)); mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1));
mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1)); mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1));
@ -105,11 +108,17 @@ class Web {
void tickSecond() { void tickSecond() {
if (mSerialClientConnnected) { if (mSerialClientConnnected) {
if(nullptr == mSerialBuf)
return;
if (mSerialBufFill > 0) { if (mSerialBufFill > 0) {
mEvts.send(mSerialBuf, "serial", millis()); mEvts.send(mSerialBuf, "serial", millis());
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0; mSerialBufFill = 0;
} }
} else if(nullptr != mSerialBuf) {
delete[] mSerialBuf;
mSerialBuf = nullptr;
} }
} }
@ -153,18 +162,14 @@ class Web {
mUploadFp.write(data, len); mUploadFp.write(data, len);
if (final) { if (final) {
mUploadFp.close(); mUploadFp.close();
#if !defined(ETHERNET)
char pwd[PWD_LEN]; char pwd[PWD_LEN];
strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD
#endif
if (!mApp->readSettings("/tmp.json")) { if (!mApp->readSettings("/tmp.json")) {
mUploadFail = true; mUploadFail = true;
DPRINTLN(DBG_ERROR, F("upload JSON error!")); DPRINTLN(DBG_ERROR, F("upload JSON error!"));
} else { } else {
LittleFS.remove("/tmp.json"); LittleFS.remove("/tmp.json");
#if !defined(ETHERNET)
strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD
#endif
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
if((mConfig->inst.iv[i].serial.u64 != 0) && (mConfig->inst.iv[i].serial.u64 < 138999999999)) { // hexadecimal if((mConfig->inst.iv[i].serial.u64 != 0) && (mConfig->inst.iv[i].serial.u64 < 138999999999)) { // hexadecimal
mConfig->inst.iv[i].serial.u64 = ah::Serial2u64(String(mConfig->inst.iv[i].serial.u64).c_str()); mConfig->inst.iv[i].serial.u64 = ah::Serial2u64(String(mConfig->inst.iv[i].serial.u64).c_str());
@ -181,6 +186,9 @@ class Web {
if (!mSerialClientConnnected) if (!mSerialClientConnnected)
return; return;
if(nullptr == mSerialBuf)
return;
msg.replace("\r\n", "<rn>"); msg.replace("\r\n", "<rn>");
if (mSerialAddTime) { if (mSerialAddTime) {
if ((13 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { if ((13 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
@ -260,8 +268,8 @@ class Web {
bool reboot = (!Update.hasError()); bool reboot = (!Update.hasError());
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\""); String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"");
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) #if defined(ETHERNET)
html += F("5"); html += (mConfig->sys.eth.enabled) ? F("5") : F("20");
#else #else
html += F("20"); html += F("20");
#endif #endif
@ -297,6 +305,10 @@ class Web {
void onConnect(AsyncEventSourceClient *client) { void onConnect(AsyncEventSourceClient *client) {
DPRINTLN(DBG_VERBOSE, "onConnect"); DPRINTLN(DBG_VERBOSE, "onConnect");
if(nullptr == mSerialBuf) {
mSerialBuf = new char[WEB_SERIAL_BUF_SIZE];
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
}
mSerialClientConnnected = true; mSerialClientConnnected = true;
if (client->lastId()) if (client->lastId())
@ -305,7 +317,19 @@ class Web {
client->send("hello!", NULL, millis(), 1000); client->send("hello!", NULL, millis(), 1000);
} }
void onIndex(AsyncWebServerRequest *request) { void onIndex(AsyncWebServerRequest *request, bool checkAp = true) {
#if !defined(ETHERNET)
if(mApp->isApActive() && checkAp) {
onWizard(request);
return;
}
#else
// show wizard only if ethernet is not configured
if(mApp->isApActive() && checkAp && !mConfig->sys.eth.enabled) {
onWizard(request);
return;
}
#endif
getPage(request, PROT_MASK_INDEX, index_html, index_html_len); getPage(request, PROT_MASK_INDEX, index_html, index_html_len);
} }
@ -388,6 +412,7 @@ class Web {
void showNotFound(AsyncWebServerRequest *request) { void showNotFound(AsyncWebServerRequest *request) {
checkProtection(request); checkProtection(request);
//DBGPRINTLN(request->url());
request->redirect("/wizard"); request->redirect("/wizard");
} }
@ -411,6 +436,13 @@ class Web {
} }
void onWizard(AsyncWebServerRequest *request) { void onWizard(AsyncWebServerRequest *request) {
#if defined(ETHERNET)
if(mConfig->sys.eth.enabled) {
getPage(request, PROT_MASK_INDEX, index_html, index_html_len);
return;
}
#endif
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), wizard_html, wizard_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), wizard_html, wizard_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
response->addHeader(F("content-type"), "text/html; charset=UTF-8"); response->addHeader(F("content-type"), "text/html; charset=UTF-8");
@ -428,15 +460,14 @@ class Web {
char buf[20] = {0}; char buf[20] = {0};
// general // general
#if !defined(ETHERNET)
if (request->arg("ssid") != "") if (request->arg("ssid") != "")
request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN); request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN);
if (request->arg("pwd") != "{PWD}") if (request->arg("pwd") != "{PWD}")
request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN);
mConfig->sys.isHidden = (request->arg("hidd") == "on");
if (request->arg("ap_pwd") != "") if (request->arg("ap_pwd") != "")
request->arg("ap_pwd").toCharArray(mConfig->sys.apPwd, PWD_LEN); request->arg("ap_pwd").toCharArray(mConfig->sys.apPwd, PWD_LEN);
mConfig->sys.isHidden = (request->arg("hidd") == "on");
#endif /* !defined(ETHERNET) */
if (request->arg("device") != "") if (request->arg("device") != "")
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
mConfig->sys.darkMode = (request->arg("darkMode") == "on"); mConfig->sys.darkMode = (request->arg("darkMode") == "on");
@ -477,17 +508,22 @@ class Web {
if (request->arg("invInterval") != "") if (request->arg("invInterval") != "")
mConfig->inst.sendInterval = request->arg("invInterval").toInt(); mConfig->inst.sendInterval = request->arg("invInterval").toInt();
mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on"); mConfig->inst.rstValsAtMidNight = (request->arg("invRstMid") == "on");
mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on"); mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on");
mConfig->inst.rstValsCommStart = (request->arg("invRstComStart") == "on");
mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on"); mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on");
mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on"); mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on");
mConfig->inst.readGrid = (request->arg("rdGrid") == "on"); mConfig->inst.readGrid = (request->arg("rdGrid") == "on");
mConfig->inst.rstMaxValsMidNight = (request->arg("invRstMaxMid") == "on"); mConfig->inst.rstIncludeMaxVals = (request->arg("invRstMaxMid") == "on");
mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat();
// pinout // pinout
for (uint8_t i = 0; i < 16; i++) { #if defined(ETHERNET)
for (uint8_t i = 0; i < 22; i++)
#else
for (uint8_t i = 0; i < 16; i++)
#endif
{
uint8_t pin = request->arg(String(pinArgNames[i])).toInt(); uint8_t pin = request->arg(String(pinArgNames[i])).toInt();
switch(i) { switch(i) {
case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN); break; case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN); break;
@ -506,11 +542,23 @@ class Web {
case 13: mConfig->cmt.pinCsb = pin; break; case 13: mConfig->cmt.pinCsb = pin; break;
case 14: mConfig->cmt.pinFcsb = pin; break; case 14: mConfig->cmt.pinFcsb = pin; break;
case 15: mConfig->cmt.pinIrq = pin; break; case 15: mConfig->cmt.pinIrq = pin; break;
#if defined(ETHERNET)
case 16: mConfig->sys.eth.pinCs = pin; break;
case 17: mConfig->sys.eth.pinSclk = pin; break;
case 18: mConfig->sys.eth.pinMiso = pin; break;
case 19: mConfig->sys.eth.pinMosi = pin; break;
case 20: mConfig->sys.eth.pinIrq = pin; break;
case 21: mConfig->sys.eth.pinRst = pin; break;
#endif
} }
} }
mConfig->nrf.enabled = (request->arg("nrfEnable") == "on"); mConfig->nrf.enabled = (request->arg("nrfEnable") == "on");
mConfig->cmt.enabled = (request->arg("cmtEnable") == "on"); mConfig->cmt.enabled = (request->arg("cmtEnable") == "on");
#if defined(ETHERNET)
mConfig->sys.eth.enabled = (request->arg("ethEn") == "on");
#endif
// ntp // ntp
if (request->arg("ntpAddr") != "") { if (request->arg("ntpAddr") != "") {
@ -544,8 +592,10 @@ class Web {
if (request->arg("mqttPwd") != "{PWD}") if (request->arg("mqttPwd") != "{PWD}")
request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN);
request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN);
mConfig->mqtt.json = (request->arg("mqttJson") == "on");
mConfig->mqtt.port = request->arg("mqttPort").toInt(); mConfig->mqtt.port = request->arg("mqttPort").toInt();
mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); mConfig->mqtt.interval = request->arg("mqttInterval").toInt();
mConfig->mqtt.enableRetain = (request->arg("retain") == "on");
// serial console // serial console
mConfig->serial.debug = (request->arg("serDbg") == "on"); mConfig->serial.debug = (request->arg("serDbg") == "on");
@ -662,7 +712,7 @@ class Web {
{ "max_power", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} }, { "max_power", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} },
{ "radio_rx_success", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} }, { "radio_rx_success", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} },
{ "radio_rx_fail", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} }, { "radio_rx_fail", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} },
{ "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} }, { "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnswer;} },
{ "radio_frame_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, { "radio_frame_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} },
{ "radio_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} }, { "radio_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} },
{ "radio_retransmits", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} }, { "radio_retransmits", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} },
@ -911,7 +961,7 @@ class Web {
settings_t *mConfig = nullptr; settings_t *mConfig = nullptr;
bool mSerialAddTime = true; bool mSerialAddTime = true;
char mSerialBuf[WEB_SERIAL_BUF_SIZE]; char *mSerialBuf = nullptr;
uint16_t mSerialBufFill = 0; uint16_t mSerialBufFill = 0;
bool mSerialClientConnnected = false; bool mSerialClientConnnected = false;

488
src/wifi/ahoywifi.cpp

@ -1,488 +0,0 @@
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#if !defined(ETHERNET)
#if defined(ESP32) && defined(F)
#undef F
#define F(sl) (sl)
#endif
#include "ahoywifi.h"
#if defined(ESP32)
#include <ESPmDNS.h>
#else
#include <ESP8266mDNS.h>
#endif
// NTP CONFIG
#define NTP_PACKET_SIZE 48
//-----------------------------------------------------------------------------
ahoywifi::ahoywifi() : mApIp(192, 168, 4, 1) {}
/**
* TODO: ESP32 has native strongest AP support!
* WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
*/
//-----------------------------------------------------------------------------
void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) {
mConfig = config;
mUtcTimestamp = utcTimestamp;
mAppWifiCb = cb;
mGotDisconnect = false;
mStaConn = DISCONNECTED;
mCnt = 0;
mScanActive = false;
mScanCnt = 0;
mStopApAllowed = true;
#if defined(ESP8266)
wifiConnectHandler = WiFi.onStationModeConnected(std::bind(&ahoywifi::onConnect, this, std::placeholders::_1));
wifiGotIPHandler = WiFi.onStationModeGotIP(std::bind(&ahoywifi::onGotIP, this, std::placeholders::_1));
wifiDisconnectHandler = WiFi.onStationModeDisconnected(std::bind(&ahoywifi::onDisconnect, this, std::placeholders::_1));
#else
WiFi.onEvent(std::bind(&ahoywifi::onWiFiEvent, this, std::placeholders::_1));
#endif
setupWifi(true);
}
//-----------------------------------------------------------------------------
void ahoywifi::setupWifi(bool startAP = false) {
#if !defined(FB_WIFI_OVERRIDDEN)
if(startAP) {
setupAp();
delay(1000);
}
#endif
#if !defined(AP_ONLY)
#if defined(FB_WIFI_OVERRIDDEN)
snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", FB_WIFI_SSID);
snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", FB_WIFI_PWD);
setupStation();
#else
if(mConfig->valid) {
if(strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) != 0)
setupStation();
}
#endif
#endif
}
void ahoywifi::tickWifiLoop() {
static const uint8_t TIMEOUT = 20;
static const uint8_t SCAN_TIMEOUT = 10;
#if !defined(AP_ONLY)
mCnt++;
switch (mStaConn) {
case IN_STA_MODE:
// Nothing to do
if (mGotDisconnect) {
mStaConn = RESET;
}
#if !defined(ESP32)
MDNS.update();
if(WiFi.channel() > 11)
mWasInCh12to14 = true;
#endif
return;
case IN_AP_MODE:
if ((WiFi.softAPgetStationNum() == 0) || (!mStopApAllowed)) {
mCnt = 0;
mDns.stop();
WiFi.mode(WIFI_AP_STA);
mStaConn = DISCONNECTED;
} else {
mDns.processNextRequest();
return;
}
break;
case DISCONNECTED:
if ((WiFi.softAPgetStationNum() > 0) && (mStopApAllowed)) {
mStaConn = IN_AP_MODE;
// first time switch to AP Mode
if (mScanActive) {
WiFi.scanDelete();
mScanActive = false;
}
DBGPRINTLN(F("AP client connected"));
welcome(mApIp.toString(), "");
WiFi.mode(WIFI_AP);
mDns.start(53, "*", mApIp);
mAppWifiCb(true);
mDns.processNextRequest();
return;
} else if (!mScanActive) {
DBGPRINT(F("scanning APs with SSID "));
DBGPRINTLN(String(mConfig->sys.stationSsid));
mScanCnt = 0;
mCnt = 0;
mScanActive = true;
#if defined(ESP8266)
WiFi.scanNetworks(true, true, 0U, ([this]() {
if (mConfig->sys.isHidden)
return (uint8_t*)NULL;
return (uint8_t*)(mConfig->sys.stationSsid);
})());
#else
WiFi.scanNetworks(true, true, false, 300U, 0U, ([this]() {
if (mConfig->sys.isHidden)
return (char*)NULL;
return (mConfig->sys.stationSsid);
})());
#endif
return;
} else if(getBSSIDs()) {
// Scan ready
mStaConn = SCAN_READY;
} else {
// In case of a timeout, what do we do?
// For now we start scanning again as the original code did.
// Would be better to into PA mode
if (isTimeout(SCAN_TIMEOUT)) {
WiFi.scanDelete();
mScanActive = false;
}
}
break;
case SCAN_READY:
mStaConn = CONNECTING;
mCnt = 0;
DBGPRINT(F("try to connect to AP with BSSID:"));
uint8_t bssid[6];
for (int j = 0; j < 6; j++) {
bssid[j] = mBSSIDList.front();
mBSSIDList.pop_front();
DBGPRINT(" " + String(bssid[j], HEX));
}
DBGPRINTLN("");
mGotDisconnect = false;
WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]);
break;
case CONNECTING:
if (isTimeout(TIMEOUT)) {
WiFi.disconnect();
mStaConn = mBSSIDList.empty() ? DISCONNECTED : SCAN_READY;
}
break;
case CONNECTED:
// Connection but no IP yet
if (isTimeout(TIMEOUT) || mGotDisconnect) {
mStaConn = RESET;
}
break;
case GOT_IP:
welcome(WiFi.localIP().toString(), F(" (Station)"));
if(mStopApAllowed) {
WiFi.softAPdisconnect();
WiFi.mode(WIFI_STA);
DBGPRINTLN(F("[WiFi] AP disabled"));
delay(100);
}
mAppWifiCb(true);
mGotDisconnect = false;
mStaConn = IN_STA_MODE;
if (!MDNS.begin(mConfig->sys.deviceName)) {
DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!"));
} else {
DBGPRINT(F("[WiFi] mDNS established: "));
DBGPRINT(mConfig->sys.deviceName);
DBGPRINTLN(F(".local"));
}
break;
case RESET:
mGotDisconnect = false;
mStaConn = DISCONNECTED;
mCnt = 5; // try to reconnect in 5 sec
setupWifi(); // reconnect with AP / Station setup
mAppWifiCb(false);
DPRINTLN(DBG_INFO, "[WiFi] Connection Lost");
break;
default:
DBGPRINTLN(F("Unhandled status"));
break;
}
#endif
}
//-----------------------------------------------------------------------------
void ahoywifi::setupAp(void) {
DPRINTLN(DBG_VERBOSE, F("wifi::setupAp"));
DBGPRINTLN(F("\n---------\nAhoyDTU Info:"));
DBGPRINT(F("Version: "));
DBGPRINT(String(VERSION_MAJOR));
DBGPRINT(F("."));
DBGPRINT(String(VERSION_MINOR));
DBGPRINT(F("."));
DBGPRINTLN(String(VERSION_PATCH));
DBGPRINT(F("Github Hash: "));
DBGPRINTLN(String(AUTO_GIT_HASH));
DBGPRINT(F("\n---------\nAP MODE\nSSID: "));
DBGPRINTLN(WIFI_AP_SSID);
DBGPRINT(F("PWD: "));
DBGPRINTLN(mConfig->sys.apPwd);
DBGPRINT(F("IP Address: http://"));
DBGPRINTLN(mApIp.toString());
DBGPRINTLN(F("---------\n"));
if(String(mConfig->sys.deviceName) != "")
WiFi.hostname(mConfig->sys.deviceName);
WiFi.mode(WIFI_AP_STA);
WiFi.softAPConfig(mApIp, mApIp, IPAddress(255, 255, 255, 0));
WiFi.softAP(WIFI_AP_SSID, mConfig->sys.apPwd);
}
//-----------------------------------------------------------------------------
void ahoywifi::setupStation(void) {
DPRINTLN(DBG_VERBOSE, F("wifi::setupStation"));
if(mConfig->sys.ip.ip[0] != 0) {
IPAddress ip(mConfig->sys.ip.ip);
IPAddress mask(mConfig->sys.ip.mask);
IPAddress dns1(mConfig->sys.ip.dns1);
IPAddress dns2(mConfig->sys.ip.dns2);
IPAddress gateway(mConfig->sys.ip.gateway);
if(!WiFi.config(ip, gateway, mask, dns1, dns2))
DPRINTLN(DBG_ERROR, F("failed to set static IP!"));
}
mBSSIDList.clear();
if(String(mConfig->sys.deviceName) != "")
WiFi.hostname(mConfig->sys.deviceName);
WiFi.mode(WIFI_AP_STA);
DBGPRINT(F("connect to network '"));
DBGPRINT(mConfig->sys.stationSsid);
DBGPRINTLN(F("' ..."));
}
//-----------------------------------------------------------------------------
bool ahoywifi::getNtpTime(void) {
if(IN_STA_MODE != mStaConn)
return false;
IPAddress timeServer;
uint8_t buf[NTP_PACKET_SIZE];
uint8_t retry = 0;
if (WiFi.hostByName(mConfig->ntp.addr, timeServer) != 1)
return false;
mUdp.begin(mConfig->ntp.port);
sendNTPpacket(timeServer);
while(retry++ < 5) {
int wait = 150;
while(--wait) {
if(NTP_PACKET_SIZE <= mUdp.parsePacket()) {
uint64_t secsSince1900;
mUdp.read(buf, NTP_PACKET_SIZE);
secsSince1900 = ((uint64_t)buf[40] << 24);
secsSince1900 |= (buf[41] << 16);
secsSince1900 |= (buf[42] << 8);
secsSince1900 |= (buf[43] );
*mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time
DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC");
return true;
} else
delay(10);
}
}
DPRINTLN(DBG_INFO, F("[NTP]: getNtpTime failed"));
return false;
}
//-----------------------------------------------------------------------------
void ahoywifi::sendNTPpacket(IPAddress& address) {
//DPRINTLN(DBG_VERBOSE, F("wifi::sendNTPpacket"));
uint8_t buf[NTP_PACKET_SIZE] = {0};
buf[0] = B11100011; // LI, Version, Mode
buf[1] = 0; // Stratum
buf[2] = 6; // Max Interval between messages in seconds
buf[3] = 0xEC; // Clock Precision
// bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset
buf[12] = 49; // four-byte reference ID identifying
buf[13] = 0x4E;
buf[14] = 49;
buf[15] = 52;
mUdp.beginPacket(address, 123); // NTP request, port 123
mUdp.write(buf, NTP_PACKET_SIZE);
mUdp.endPacket();
}
//-----------------------------------------------------------------------------
void ahoywifi::sortRSSI(int *sort, int n) {
for (int i = 0; i < n; i++)
sort[i] = i;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i]))
std::swap(sort[i], sort[j]);
}
//-----------------------------------------------------------------------------
void ahoywifi::scanAvailNetworks(void) {
if(!mScanActive) {
mScanActive = true;
if(WIFI_AP == WiFi.getMode())
WiFi.mode(WIFI_AP_STA);
WiFi.scanNetworks(true);
}
}
//-----------------------------------------------------------------------------
bool ahoywifi::getAvailNetworks(JsonObject obj) {
JsonArray nets = obj.createNestedArray("networks");
int n = WiFi.scanComplete();
if (n < 0)
return false;
if(n > 0) {
int sort[n];
sortRSSI(&sort[0], n);
for (int i = 0; i < n; ++i) {
nets[i]["ssid"] = WiFi.SSID(sort[i]);
nets[i]["rssi"] = WiFi.RSSI(sort[i]);
}
}
mScanActive = false;
WiFi.scanDelete();
if(mStaConn == IN_AP_MODE)
WiFi.mode(WIFI_AP);
return true;
}
//-----------------------------------------------------------------------------
bool ahoywifi::getBSSIDs() {
bool result = false;
int n = WiFi.scanComplete();
if (n < 0) {
if (++mScanCnt < 20)
return false;
}
if(n > 0) {
mBSSIDList.clear();
int sort[n];
sortRSSI(&sort[0], n);
for (int i = 0; i < n; i++) {
DBGPRINT("BSSID " + String(i) + ":");
uint8_t *bssid = WiFi.BSSID(sort[i]);
for (int j = 0; j < 6; j++){
DBGPRINT(" " + String(bssid[j], HEX));
mBSSIDList.push_back(bssid[j]);
}
DBGPRINTLN("");
}
result = true;
}
mScanActive = false;
WiFi.scanDelete();
return result;
}
//-----------------------------------------------------------------------------
void ahoywifi::connectionEvent(WiFiStatus_t status) {
DPRINTLN(DBG_INFO, "connectionEvent");
switch(status) {
case CONNECTED:
if(mStaConn != CONNECTED) {
mStaConn = CONNECTED;
mGotDisconnect = false;
DBGPRINTLN(F("\n[WiFi] Connected"));
}
break;
case GOT_IP:
mStaConn = GOT_IP;
break;
case DISCONNECTED:
mGotDisconnect = true;
break;
default:
break;
}
}
//-----------------------------------------------------------------------------
#if defined(ESP8266)
//-------------------------------------------------------------------------
void ahoywifi::onConnect(const WiFiEventStationModeConnected& event) {
connectionEvent(CONNECTED);
}
//-------------------------------------------------------------------------
void ahoywifi::onGotIP(const WiFiEventStationModeGotIP& event) {
connectionEvent(GOT_IP);
}
//-------------------------------------------------------------------------
void ahoywifi::onDisconnect(const WiFiEventStationModeDisconnected& event) {
connectionEvent(DISCONNECTED);
}
#else
//-------------------------------------------------------------------------
void ahoywifi::onWiFiEvent(WiFiEvent_t event) {
DBGPRINT(F("Wifi event: "));
DBGPRINTLN(String(event));
switch(event) {
case SYSTEM_EVENT_STA_CONNECTED:
connectionEvent(CONNECTED);
break;
case SYSTEM_EVENT_STA_GOT_IP:
connectionEvent(GOT_IP);
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
connectionEvent(DISCONNECTED);
break;
default:
break;
}
}
#endif
//-----------------------------------------------------------------------------
void ahoywifi::welcome(String ip, String mode) {
DBGPRINTLN(F("\n\n--------------------------------"));
DBGPRINTLN(F("Welcome to AHOY!"));
DBGPRINT(F("\npoint your browser to http://"));
DBGPRINT(ip);
DBGPRINTLN(mode);
DBGPRINTLN(F("to configure your device"));
DBGPRINTLN(F("--------------------------------\n"));
}
#endif /* !defined(ETHERNET) */

97
src/wifi/ahoywifi.h

@ -1,97 +0,0 @@
//------------------------------------//-----------------------------------------------------------------------------
// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#if !defined(ETHERNET)
#ifndef __AHOYWIFI_H__
#define __AHOYWIFI_H__
#include "../utils/dbg.h"
#include <Arduino.h>
#include <WiFiUdp.h>
#include <DNSServer.h>
#include "ESPAsyncWebServer.h"
#include "../config/settings.h"
class app;
class ahoywifi {
public:
typedef std::function<void(bool)> appWifiCb;
ahoywifi();
void setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb);
void tickWifiLoop(void);
bool getNtpTime(void);
void scanAvailNetworks(void);
bool getAvailNetworks(JsonObject obj);
void setStopApAllowedMode(bool allowed) {
mStopApAllowed = allowed;
}
String getStationIp(void) {
return WiFi.localIP().toString();
}
void setupStation(void);
bool getWasInCh12to14() const {
return mWasInCh12to14;
}
private:
typedef enum WiFiStatus {
DISCONNECTED = 0,
SCAN_READY,
CONNECTING,
CONNECTED,
IN_AP_MODE,
GOT_IP,
IN_STA_MODE,
RESET
} WiFiStatus_t;
void setupWifi(bool startAP);
void setupAp(void);
void sendNTPpacket(IPAddress& address);
void sortRSSI(int *sort, int n);
bool getBSSIDs(void);
void connectionEvent(WiFiStatus_t status);
bool isTimeout(uint8_t timeout) { return (mCnt % timeout) == 0; }
#if defined(ESP8266)
void onConnect(const WiFiEventStationModeConnected& event);
void onGotIP(const WiFiEventStationModeGotIP& event);
void onDisconnect(const WiFiEventStationModeDisconnected& event);
#else
void onWiFiEvent(WiFiEvent_t event);
#endif
void welcome(String ip, String mode);
settings_t *mConfig = nullptr;
appWifiCb mAppWifiCb;
DNSServer mDns;
IPAddress mApIp;
WiFiUDP mUdp; // for time server
#if defined(ESP8266)
WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler;
#endif
WiFiStatus_t mStaConn = DISCONNECTED;
uint8_t mCnt = 0;
uint32_t *mUtcTimestamp = nullptr;
uint8_t mScanCnt = 0;
bool mScanActive = false;
bool mGotDisconnect = false;
std::list<uint8_t> mBSSIDList;
bool mStopApAllowed = false;
bool mWasInCh12to14 = false;
};
#endif /*__AHOYWIFI_H__*/
#endif /* !defined(ETHERNET) */

466
tools/NodeRED/flows-mqtt-json-example.json

@ -0,0 +1,466 @@
[
{
"id": "67bced2c4e728783",
"type": "mqtt in",
"z": "5de5756d190f9086",
"name": "",
"topic": "hoymiles/+",
"qos": "0",
"datatype": "auto-detect",
"broker": "319864a4e0fd913f",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 80,
"y": 2100,
"wires": [
[
"a55632ad0dff0b69"
]
]
},
{
"id": "a7f0d307d7cf77e2",
"type": "mqtt in",
"z": "5de5756d190f9086",
"name": "",
"topic": "hoymiles/X/#",
"qos": "0",
"datatype": "auto-detect",
"broker": "319864a4e0fd913f",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 90,
"y": 2260,
"wires": [
[
"7e17e5a3f4df3011",
"1a8cca488d53394a"
]
]
},
{
"id": "7e17e5a3f4df3011",
"type": "debug",
"z": "5de5756d190f9086",
"name": "Inverter X",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 340,
"y": 2260,
"wires": []
},
{
"id": "fb7357db50501627",
"type": "change",
"z": "5de5756d190f9086",
"name": "Tags setzen",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "(\t $a := $split(topic, '/');\t [\t payload,\t {\t \"device\":$a[0],\t \"name\":$a[1],\t \"channel\":$a[2]\t }\t ]\t)\t",
"tot": "jsonata"
},
{
"t": "delete",
"p": "topic",
"pt": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 610,
"y": 2360,
"wires": [
[
"91a4607dfda84b67"
]
]
},
{
"id": "670eb9fbb5c31b2c",
"type": "debug",
"z": "5de5756d190f9086",
"name": "InfluxDB",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 2360,
"wires": []
},
{
"id": "1a8cca488d53394a",
"type": "switch",
"z": "5de5756d190f9086",
"name": "",
"property": "$split(topic, '/')[2]",
"propertyType": "jsonata",
"rules": [
{
"t": "eq",
"v": "available",
"vt": "str"
},
{
"t": "eq",
"v": "last_success",
"vt": "str"
},
{
"t": "regex",
"v": "(ch[0-6])\\b",
"vt": "str",
"case": false
},
{
"t": "eq",
"v": "radio_stat",
"vt": "str"
},
{
"t": "eq",
"v": "firmware",
"vt": "str"
},
{
"t": "eq",
"v": "hardware",
"vt": "str"
},
{
"t": "eq",
"v": "alarm",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 7,
"x": 330,
"y": 2380,
"wires": [
[
"845aeb93e39092c5"
],
[
"241a8e70e9fde93c"
],
[
"fb7357db50501627"
],
[
"9d38f021308664c1"
],
[
"a508355f0cc87966"
],
[
"d2c9aa1a8978aca6"
],
[
"b27032beb597d5a7"
]
]
},
{
"id": "845aeb93e39092c5",
"type": "debug",
"z": "5de5756d190f9086",
"name": "available",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 600,
"y": 2240,
"wires": []
},
{
"id": "241a8e70e9fde93c",
"type": "debug",
"z": "5de5756d190f9086",
"name": "last_success",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 610,
"y": 2300,
"wires": []
},
{
"id": "9d38f021308664c1",
"type": "debug",
"z": "5de5756d190f9086",
"name": "radio_stat",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 600,
"y": 2400,
"wires": []
},
{
"id": "a508355f0cc87966",
"type": "debug",
"z": "5de5756d190f9086",
"name": "firmware",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 600,
"y": 2440,
"wires": []
},
{
"id": "d2c9aa1a8978aca6",
"type": "debug",
"z": "5de5756d190f9086",
"name": "hardware",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 600,
"y": 2480,
"wires": []
},
{
"id": "b27032beb597d5a7",
"type": "debug",
"z": "5de5756d190f9086",
"name": "alarm",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 590,
"y": 2520,
"wires": []
},
{
"id": "d814738cf55ad663",
"type": "debug",
"z": "5de5756d190f9086",
"name": "total",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 590,
"y": 2160,
"wires": []
},
{
"id": "a55632ad0dff0b69",
"type": "switch",
"z": "5de5756d190f9086",
"name": "",
"property": "$split(topic, '/')[1]",
"propertyType": "jsonata",
"rules": [
{
"t": "eq",
"v": "uptime",
"vt": "str"
},
{
"t": "eq",
"v": "wifi_rssi",
"vt": "str"
},
{
"t": "eq",
"v": "status",
"vt": "str"
},
{
"t": "eq",
"v": "total",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 4,
"x": 330,
"y": 2100,
"wires": [
[
"1fbb0674d2576ee7"
],
[
"e6be1c98ac55f511"
],
[
"f9c2d3b30e34fdda"
],
[
"d814738cf55ad663"
]
]
},
{
"id": "f9c2d3b30e34fdda",
"type": "debug",
"z": "5de5756d190f9086",
"name": "status",
"active": false,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 590,
"y": 2100,
"wires": []
},
{
"id": "e6be1c98ac55f511",
"type": "debug",
"z": "5de5756d190f9086",
"name": "wifi_rssi",
"active": false,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 600,
"y": 2040,
"wires": []
},
{
"id": "1fbb0674d2576ee7",
"type": "debug",
"z": "5de5756d190f9086",
"name": "uptime",
"active": false,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "payload",
"statusType": "auto",
"x": 590,
"y": 1980,
"wires": []
},
{
"id": "91a4607dfda84b67",
"type": "change",
"z": "5de5756d190f9086",
"name": "Lösche",
"rules": [
{
"t": "delete",
"p": "payload[0].YieldDay",
"pt": "msg"
},
{
"t": "delete",
"p": "payload[0].MaxPower",
"pt": "msg"
},
{
"t": "delete",
"p": "payload[0].ALARM_MES_ID",
"pt": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 780,
"y": 2360,
"wires": [
[
"670eb9fbb5c31b2c"
]
]
},
{
"id": "319864a4e0fd913f",
"type": "mqtt-broker",
"name": "broker",
"broker": "localhost",
"port": "1883",
"clientid": "",
"autoConnect": true,
"usetls": false,
"protocolVersion": "4",
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"birthMsg": {},
"closeTopic": "",
"closeQos": "0",
"closePayload": "",
"closeMsg": {},
"willTopic": "",
"willQos": "0",
"willPayload": "",
"willMsg": {},
"userProps": "",
"sessionExpiry": ""
}
]
Loading…
Cancel
Save