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. 46
      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. 122
      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"
description: "File a bug report"
title: "[Bug]"
labels: ["bug", "needs-triage"]
assignees:
- lumapu
name: "issue report"
description: "issue report"
labels: ["new", "needs-triage"]
body:
- type: markdown
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!
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!
Check our FAQ: https://ahoydtu.de/faq
- type: markdown
@ -35,7 +32,7 @@ body:
label: Assembly
description:
options:
- I did the assebly by myself
- I did the assembly by myself
- the DTU was already assembled
validations:
required: true
@ -84,7 +81,7 @@ body:
label: Connection picture
description:
options:
- label: I will attach/upload an Image of my wiring
- label: I will attach/upload an image of my wiring
validations:
required: true
- type: markdown

61
.github/workflows/compile_development.yml

@ -16,23 +16,25 @@ jobs:
- uses: actions/checkout@v4
build-en:
name: Build Environments (English)
name: Build (EN)
needs: check
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
variant:
- opendtufusion
- opendtufusion-16MB
- esp8266
- esp8266-all
- esp8266-minimal
- esp8266-prometheus
- esp8285
- esp32-wroom32
- esp32-wroom32-minimal
- esp32-wroom32-prometheus
- esp32-wroom32-ethernet
- esp32-s2-mini
- esp32-c3-mini
- opendtufusion
- opendtufusion-ethernet
steps:
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
@ -67,6 +69,11 @@ jobs:
- 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
@ -77,23 +84,23 @@ jobs:
path: firmware/*
build-de:
name: Build Environments (German)
name: Build (DE)
needs: check
runs-on: ubuntu-latest
continue-on-error: true
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-all-de
- esp8266-prometheus-de
- esp8285-de
- esp32-wroom32-de
- esp32-wroom32-prometheus-de
- esp32-wroom32-ethernet-de
- esp32-s2-mini-de
- esp32-c3-mini-de
- opendtufusion-de
- opendtufusion-ethernet-de
steps:
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
@ -128,6 +135,11 @@ jobs:
- 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
@ -138,7 +150,7 @@ jobs:
path: firmware/*
deploy:
name: Deploy Environments
name: Update Artifacts / Deploy
needs: [build-en, build-de]
runs-on: ubuntu-latest
continue-on-error: false
@ -164,9 +176,26 @@ jobs:
env:
VERSION: ${{ steps.version_name.outputs.name }}
- name: Create ESP Web Tools Manifest
working-directory: src
run: python ../scripts/buildManifest.py
- name: Copy install html
run: mv scripts/gh-action-dev-build-flash.html firmware/install.html
- name: Copy Changes.md
run: mv src/CHANGES.md firmware/CHANGES.md
- name: Rename firmware directory
run: mv firmware ${{ steps.version_name.outputs.name }}
- name: delete environment Artifacts
uses: geekyeggo/delete-artifact@v4
with:
name: dev-*
- name: Deploy
uses: nogsantos/scp-deploy@master
with:
@ -176,3 +205,17 @@ jobs:
port: ${{ secrets.FW_SSH_PORT }}
user: ${{ secrets.FW_SSH_USER }}
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
paths-ignore:
- '**.md' # Do no build on *.md changes
- '**.yaml' # e.g. issue report
jobs:
build:
name: Build Environments
check:
name: Check Repository
runs-on: ubuntu-latest
if: github.repository == 'lumapu/ahoy' && github.ref_name == 'main'
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:
matrix:
variant:
- opendtufusion
- opendtufusion-16MB
- esp8266
- esp8266-all
- esp8266-minimal
- esp8266-prometheus
- esp8285
- esp32-wroom32
- esp32-wroom32-minimal
- esp32-wroom32-prometheus
- esp32-wroom32-ethernet
- esp32-s2-mini
- esp32-c3-mini
- opendtufusion
- opendtufusion-ethernet
steps:
- 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-all-de
- esp8266-prometheus-de
- esp8285-de
- esp32-wroom32-de
- esp32-wroom32-prometheus-de
- esp32-wroom32-ethernet-de
- esp32-s2-mini-de
- esp32-c3-mini-de
- opendtufusion-de
- opendtufusion-ethernet-de
steps:
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
@ -69,6 +136,11 @@ jobs:
- 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
@ -78,11 +150,10 @@ jobs:
name: ${{ matrix.variant }}
path: firmware/*
release:
name: Create Release
needs: [build-en, build-de]
runs-on: ubuntu-latest
needs: [build]
continue-on-error: false
permissions:
contents: write
@ -140,7 +211,7 @@ jobs:
deploy:
name: Deploy Environments to fw.ahoydtu.de
needs: [build, release]
needs: [build-en, build-de, release]
runs-on: ubuntu-latest
continue-on-error: false
steps:

2
.gitignore

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

10
README.md

@ -24,6 +24,7 @@ This repository provides hardware and software solutions for communicating with
## Changelog
[latest Release](https://github.com/lumapu/ahoy/blob/main/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 |
| ------ | -- | -- | ------- | ------- | ---------- |
| [ESP8266/ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | |
| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
| [ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | [create your own DTU](https://ahoydtu.de/getting_started/) |
| ESP8266, C++ | ✔️ | ✔️ | ❌ | ⚠️ not recommended for new DTU |
⚠️ **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
[https://ahoydtu.de](https://ahoydtu.de)
[Firmware Archive https://fw.ahoydtu.de](https://fw.ahoydtu.de)
## 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)

14
manual/Getting_Started.md

@ -17,6 +17,7 @@ Hoymiles Inverters
| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
| ✔️ | HMT | 1600, 1800, 2250 | |
| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
| 🟡 | HERF | (supported) | |
## Table of Contents
@ -25,16 +26,16 @@ Hoymiles Inverters
- [Things needed](#things-needed)
- [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
- [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)
- [Schematic](#schematic)
- [Symbolic view](#symbolic-view)
- [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
- [Schematic](#schematic-2)
- [Symbolic view](#symbolic-view-2)
- [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1)
- [ESP32 GPIO settings](#esp32-gpio-settings)
- [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware)
- [Compiling your own Version](#compiling-your-own-version)
- [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher)
@ -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.
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!

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
index 12be5f8..cffeed7 100644
index 6e88da9..09359c3 100644
--- a/src/AsyncWebSocket.cpp
+++ b/src/AsyncWebSocket.cpp
@@ -737,7 +737,7 @@ void AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len)
IPAddress AsyncWebSocketClient::remoteIP() const
{
if (!_client)
- return IPAddress(0U);
+ return IPAddress();
@@ -827,7 +827,7 @@ void AsyncWebSocketClient::binary(AsyncWebSocketMessageBuffer * buffer)
IPAddress AsyncWebSocketClient::remoteIP() {
if(!_client) {
- return IPAddress((uint32_t)0);
+ return IPAddress();
}
return _client->remoteIP();
}
diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp
index 22a549f..e0b36b3 100644
index a22e991..babef18 100644
--- a/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);
return 0;
}
- outLen = sprintf_P((char*)buf+headLen, PSTR("%x"), readLen) + headLen;
+ outLen = sprintf_P((char*)buf+headLen, PSTR("%04x"), readLen) + headLen;
- outLen = sprintf((char*)buf+headLen, "%x", readLen) + headLen;
+ outLen = sprintf((char*)buf+headLen, "%04x", readLen) + headLen;
while(outLen < headLen + 4) buf[outLen++] = ' ';
buf[outLen++] = '\r';
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)
if env['PIOENV'][:22] != "opendtufusion-ethernet":
applyPatch("ESP Async WebServer", "../patches/AsyncWeb_Prometheus.patch")
applyPatch("ESPAsyncWebServer-esphome", "../patches/AsyncWeb_Prometheus.patch")
if env['PIOENV'][:13] == "opendtufusion":
applyPatch("GxEPD2", "../patches/GxEPD2_SW_SPI.patch")
if (env['PIOENV'][:5] == "esp32") or (env['PIOENV'][:13] == "opendtufusion"):
applyPatch("GxEPD2", "../patches/GxEPD2_HAL.patch")
if (env['PIOENV'][:13] == "opendtufusion") or (env['PIOENV'][:5] == "esp32"):
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/partitions.bin", "offset": 32768})
esp32["parts"].append({"path": "ESP32/ota.bin", "offset": 57344})
esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32.bin", "offset": 65536})
esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32-wroom32.bin", "offset": 65536})
data["builds"].append(esp32)
esp32s2 = {}
esp32s2["chipFamily"] = "ESP32-S2"
esp32s2["parts"] = []
esp32s2["parts"].append({"path": "ESP32-S2/bootloader.bin", "offset": 4096})
esp32s2["parts"].append({"path": "ESP32-S2/partitions.bin", "offset": 32768})
esp32s2["parts"].append({"path": "ESP32-S2/ota.bin", "offset": 57344})
esp32s2["parts"].append({"path": "ESP32-S2/" + version[1] + "_" + sha + "_esp32-s2-mini.bin", "offset": 65536})
data["builds"].append(esp32s2)
esp32s3 = {}
esp32s3["chipFamily"] = "ESP32-S3"
esp32s3["parts"] = []
esp32s3["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096})
esp32s3["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768})
esp32s3["parts"].append({"path": "ESP32/ota.bin", "offset": 57344})
esp32s3["parts"].append({"path": "ESP32-S3/" + version[1] + "_" + sha + "_opendtufusion.bin", "offset": 65536})
data["builds"].append(esp32s3)
esp8266 = {}
esp8266["chipFamily"] = "ESP8266"
esp8266["parts"] = []
@ -47,7 +65,7 @@ def buildManifest(path, infile, outfile):
jsonString = json.dumps(data, indent=2)
fp = open(path + "firmware/" + outfile, "w")
fp = open(path + "../firmware/" + outfile, "w")
fp.write(jsonString)
fp.close()

172
scripts/convertHtml.py

@ -7,8 +7,58 @@ import json
from datetime import date
from pathlib import Path
import subprocess
import configparser
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():
try:
@ -50,38 +100,46 @@ def readVersionFull(path):
return version
def htmlParts(file, header, nav, footer, versionPath, lang):
p = "";
f = open(file, "r")
lines = f.readlines()
f.close();
f = open(header, "r")
h = f.read().strip()
h = f.readlines()
f.close()
f = open(nav, "r")
n = f.read().strip()
n = f.readlines()
f.close()
f = open(footer, "r")
fo = f.read().strip()
fo = f.readlines()
f.close()
linesExt = []
for line in lines:
line = line.replace("{#HTML_HEADER}", h)
line = line.replace("{#HTML_NAV}", n)
line = line.replace("{#HTML_FOOTER}", fo)
p += line
if line.find("{#HTML_HEADER}") != -1:
linesExt.extend(h)
elif line.find("{#HTML_NAV}") != -1:
linesExt.extend(n)
elif line.find("{#HTML_FOOTER}") != -1:
linesExt.extend(fo)
else:
linesExt.append(line)
linesMod = prepro.conv(linesExt, build_flags)
#placeholders
version = readVersion(versionPath);
link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>'
p = ""
for line in linesMod:
p += line
p = p.replace("{#VERSION}", version)
p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath))
p = p.replace("{#VERSION_GIT}", link)
# remove if - endif ESP32
p = checkIf(p)
p = translate(file, p, lang)
p = translate("general", p, lang) # menu / header / footer
@ -90,30 +148,6 @@ def htmlParts(file, header, nav, footer, versionPath, lang):
f.close();
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):
with open('../lang.json') as j:
lang = json.load(j)
@ -189,33 +223,41 @@ def convert2Header(inFile, versionPath, lang):
f.write("#endif /*__{}_{}_H__*/\n".format(define, define2))
f.close()
# delete all files in the 'h' dir
wd = 'web/html/h'
if os.path.exists(wd):
for f in os.listdir(wd):
os.remove(os.path.join(wd, f))
wd += "/tmp"
if os.path.exists(wd):
for f in os.listdir(wd):
os.remove(os.path.join(wd, f))
# grab all files with following extensions
os.chdir('./web/html')
types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types
files_grabbed = []
for files in types:
files_grabbed.extend(glob.glob(files))
Path("h").mkdir(exist_ok=True)
Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements
shutil.copyfile("style.css", "tmp/style.css")
# get language from environment
lang = "en"
if env['PIOENV'][-3:] == "-de":
lang = "de"
# go throw the array
for val in files_grabbed:
convert2Header(val, "../../defines.h", lang)
def main():
get_build_flags()
# delete all files in the 'h' dir
wd = 'web/html/h'
if os.path.exists(wd):
for f in os.listdir(wd):
os.remove(os.path.join(wd, f))
wd += "/tmp"
if os.path.exists(wd):
for f in os.listdir(wd):
os.remove(os.path.join(wd, f))
# grab all files with following extensions
os.chdir('./web/html')
types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types
files_grabbed = []
for files in types:
files_grabbed.extend(glob.glob(files))
Path("h").mkdir(exist_ok=True)
Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements
shutil.copyfile("style.css", "tmp/style.css")
# get language from environment
lang = "en"
if env['PIOENV'][-3:] == "-de":
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"
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 + "/partitions.bin", dst + "partitions.bin")
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/launch.json
.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 reading grid profile
* added decimal place for active power control (APC aka power limit)
* added information about working IRQ for NRF24 and CMT2300A to `/system`
* added loss rate to `/visualization` in the statistics window and MqTT
* added optional output to display whether it's night time or not. Can be reused as output to control battery system or mapped to a LED
* added timestamp for `max ac power` as tooltip
* added wizard for initial WiFi connection
* added history graph (still under development)
* added simulator (must be activated before compile, standard: off)
* added minimal version (without: MqTT, Display, History), WebUI is not changed! (not compiled automatically)
* added info about installed binary to `/update`
* added protection to prevent update to wrong firmware (environment check)
* added optional custom link to the menu
* added support for other regions (USA, Indonesia)
* added warning for WiFi channel 12-14 (ESP8266 only)
* added `max_power` to MqTT total values
* added API-Token authentification for external scripts
* improved MqTT by marking sent data and improved `last_success` resends
* improved communication for HM and MI inverters
* improved reading live data from inverter
* improved sending active power control command faster
* improved `/settings`: pinout has an own subgroup
* improved export by saving settings before they are exported (to have everything in JSON)
* improved code quality (cppcheck)
* seperated sunrise and sunset offset to two fields
* fix MqTT night communication
* fix missing favicon to html header
* fix build on Windows of `opendtufusion` environments (git: trailing whitespaces)
* fix generation of DTU-ID
* fix: protect commands from popup in `/live` if password is set
* fix: prevent sending commands to inverter which isn't active
* combined firmware and hardware version to JSON topics (MqTT)
* updated Prometheus with latest changes
* upgraded most libraries to newer versions
* beautified typography, added spaces between value and unit for `/visualization`
* removed add to total (MqTT) inverter setting
* added HMS-400-1T support (serial number 1125...)
* added further ESP8266 versions (-all, -minimal) because of small ressources on ESP8266
* added some Gridprofiles
* added support for characters in serial number of inverter (A-F)
* added default coordinates on fresh install, needed for history graph on display and WebUI
* added option to reset values on communication start (sunrise)
* added max inverter temperature to WebUI
* added yield day to history graph
* added script and [instructions](../manual/factory_firmware.md) how to generate factory firmware which includes predefined settings
* 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 support of HERF inverters, serial number is converted in Javascript
* added device name to HTML title
* added feature to restart Ahoy using MqTT
* added feature to publish MqTT messages as JSON as well (new setting)
* add timestamp to JSON output
* improved communication to inverter
* improved translation to German
* improved HTML pages, reduced in size by only including relevant contents depending by chip type
* improved history graph in WebUI
* improved network routines
* improved Wizard
* improved WebUI by disabling upload and import buttons when no file is selected
* improved queue, only add new object once they not exist in queue
* improved MqTT `OnMessage` (threadsafe)
* improved read of alarms, prevent duplicates, update alarm time if there is an update
* improved alarms are now sorted in ascending direction
* improved by prevent add inverter multiple times
* improved sending active power controll commands
* improved refresh routine of ePaper, full refresh each 12h
* redesigned WebUI on `/system`
* changed MqTT retained flags
* 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)
* converted ePaper and Ethernet to hal-SPI
* combined Ethernet and WiFi variants - Ethernet is now always included, but needs to be enabled if needed
* changed: Ethernet variants (W5500) now support WiFi as fall back / configuration
* switch AsyncWebserver library
* 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)

255
src/app.cpp

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

131
src/app.h

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

28
src/appInterface.h

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

15
src/config/config.h

@ -16,9 +16,8 @@
//-------------------------------------
// Fallback WiFi Info
#define FB_WIFI_SSID "YOUR_WIFI_SSID"
#define FB_WIFI_PWD "YOUR_WIFI_PWD"
#define FB_WIFI_SSID ""
#define FB_WIFI_PWD ""
// Access Point Info
// 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
//#define AP_ONLY
#if defined(AP_ONLY)
#if defined(ENABLE_MQTT)
#undef ENABLE_MQTT
#endif
#endif
// timeout for automatic logoff (20 minutes)
#define LOGOUT_TIMEOUT (20 * 60)
@ -77,6 +81,9 @@
#ifndef DEF_ETH_CS_PIN
#define DEF_ETH_CS_PIN 15
#endif
#ifndef DEF_ETH_RST_PIN
#define DEF_ETH_RST_PIN DEF_PIN_OFF
#endif
#else /* defined(ETHERNET) */
// time in seconds how long the station info (ssid + pwd) will be tried
#define WIFI_TRY_CONNECT_TIME 30
@ -142,7 +149,7 @@
#ifndef DEF_MOTION_SENSOR_PIN
#define DEF_MOTION_SENSOR_PIN DEF_PIN_OFF
#endif
#else
#else // ESP8266
#ifndef DEF_NRF_CS_PIN
#define DEF_NRF_CS_PIN 15
#endif

3
src/config/config_override_example.h

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

168
src/config/settings.h

@ -33,7 +33,6 @@
#define CONFIG_VERSION 11
#define PROT_MASK_INDEX 0x0001
#define PROT_MASK_LIVE 0x0002
#define PROT_MASK_SERIAL 0x0004
@ -55,6 +54,20 @@
#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 {
uint8_t ip[4]; // ip address
uint8_t mask[4]; // sub mask
@ -63,6 +76,19 @@ typedef struct {
uint8_t gateway[4]; // standard gateway
} 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 {
char deviceName[DEVNAME_LEN];
char adminPwd[PWD_LEN];
@ -72,13 +98,14 @@ typedef struct {
uint8_t region;
int8_t timezone;
#if !defined(ETHERNET)
char apPwd[PWD_LEN];
// wifi
char stationSsid[SSID_LEN];
char stationPwd[PWD_LEN];
char apPwd[PWD_LEN];
bool isHidden;
#endif /* !defined(ETHERNET) */
#if defined(ETHERNET)
cfgEth_t eth;
#endif
cfgIp_t ip;
} cfgSys_t;
@ -136,7 +163,9 @@ typedef struct {
char user[MQTT_USER_LEN];
char pwd[MQTT_PWD_LEN];
char topic[MQTT_TOPIC_LEN];
bool json;
uint16_t interval;
bool enableRetain;
} cfgMqtt_t;
typedef struct {
@ -152,16 +181,16 @@ typedef struct {
} cfgIv_t;
typedef struct {
bool enabled;
// bool enabled;
cfgIv_t iv[MAX_NUM_INVERTERS];
uint16_t sendInterval;
bool rstYieldMidNight;
bool rstValsAtMidNight;
bool rstValsNotAvail;
bool rstValsCommStop;
bool rstMaxValsMidNight;
bool rstValsCommStart;
bool rstIncludeMaxVals;
bool startWithoutTime;
float yieldEffiency;
bool readGrid;
} cfgInst_t;
@ -211,8 +240,9 @@ class settings {
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 .."));
c = &mCfg;
mCfg.valid = false;
#if !defined(ESP32)
@ -248,31 +278,11 @@ class settings {
DPRINTLN(DBG_INFO, F("FS stopped"));
}
void getPtr(settings_t *&cfg) {
cfg = &mCfg;
}
bool getValid(void) {
return mCfg.valid;
}
inline bool getLastSaveSucceed() {
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) {
loadDefaults();
@ -387,14 +397,26 @@ class settings {
// restore temp settings
if(keepWifi)
memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t));
#if !defined(ETHERNET)
else {
snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID);
snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD);
snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD);
mCfg.sys.stationSsid[0] = '\0';
mCfg.sys.stationPwd[0] = '\0';
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);
mCfg.sys.region = 0; // Europe
@ -407,7 +429,11 @@ class settings {
mCfg.nrf.pinMosi = DEF_NRF_MOSI_PIN;
mCfg.nrf.pinSclk = DEF_NRF_SCLK_PIN;
#if defined(ETHERNET)
mCfg.nrf.enabled = false;
#else
mCfg.nrf.enabled = true;
#endif
#if defined(ESP32)
mCfg.cmt.pinSclk = DEF_CMT_SCLK;
@ -428,8 +454,8 @@ class settings {
mCfg.ntp.port = DEF_NTP_PORT;
mCfg.ntp.interval = 720;
mCfg.sun.lat = 0.0;
mCfg.sun.lon = 0.0;
mCfg.sun.lat = 51.1; // mid of Germany
mCfg.sun.lon = 10.5; // mid of Germany
mCfg.sun.offsetSecMorning = 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.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC);
mCfg.mqtt.interval = 0; // off
mCfg.inst.sendInterval = SEND_INTERVAL;
mCfg.inst.rstYieldMidNight = false;
mCfg.inst.rstValsNotAvail = false;
mCfg.inst.rstValsCommStop = false;
mCfg.inst.startWithoutTime = false;
mCfg.inst.rstMaxValsMidNight = false;
mCfg.inst.yieldEffiency = 1.0f;
mCfg.inst.readGrid = true;
mCfg.mqtt.json = false; // off
mCfg.mqtt.enableRetain = true;
mCfg.inst.sendInterval = SEND_INTERVAL;
mCfg.inst.rstValsAtMidNight = false;
mCfg.inst.rstValsNotAvail = false;
mCfg.inst.rstValsCommStop = false;
mCfg.inst.rstValsCommStart = false;
mCfg.inst.startWithoutTime = false;
mCfg.inst.rstIncludeMaxVals = false;
mCfg.inst.readGrid = true;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value
@ -528,12 +556,10 @@ class settings {
void jsonNetwork(JsonObject obj, bool set = false) {
if(set) {
char buf[16];
#if !defined(ETHERNET)
obj[F("ssid")] = mCfg.sys.stationSsid;
obj[F("pwd")] = mCfg.sys.stationPwd;
obj[F("ap_pwd")] = mCfg.sys.apPwd;
obj[F("hidd")] = (bool) mCfg.sys.isHidden;
#endif /* !defined(ETHERNET) */
obj[F("dev")] = mCfg.sys.deviceName;
obj[F("adm")] = mCfg.sys.adminPwd;
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.dns2, buf); obj[F("dns2")] = 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 {
#if !defined(ETHERNET)
getChar(obj, F("ssid"), mCfg.sys.stationSsid, SSID_LEN);
getChar(obj, F("pwd"), mCfg.sys.stationPwd, PWD_LEN);
getChar(obj, F("ap_pwd"), mCfg.sys.apPwd, PWD_LEN);
getVal<bool>(obj, F("hidd"), &mCfg.sys.isHidden);
#endif /* !defined(ETHERNET) */
getChar(obj, F("dev"), mCfg.sys.deviceName, DEVNAME_LEN);
getChar(obj, F("adm"), mCfg.sys.adminPwd, PWD_LEN);
getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask);
@ -569,6 +603,16 @@ class settings {
if(mCfg.sys.protectionMask == 0)
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_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("pwd")] = mCfg.mqtt.pwd;
obj[F("topic")] = mCfg.mqtt.topic;
obj[F("json")] = mCfg.mqtt.json;
obj[F("intvl")] = mCfg.mqtt.interval;
obj[F("retain")] = mCfg.mqtt.enableRetain;
} else {
getVal<uint16_t>(obj, F("port"), &mCfg.mqtt.port);
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("user"), mCfg.mqtt.user, MQTT_USER_LEN);
getChar(obj, F("clientId"), mCfg.mqtt.clientId, MQTT_CLIENTID_LEN);
getChar(obj, F("pwd"), mCfg.mqtt.pwd, MQTT_PWD_LEN);
getChar(obj, F("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN);
getVal<bool>(obj, F("retain"), &mCfg.mqtt.enableRetain);
}
}
@ -757,30 +805,25 @@ class settings {
void jsonInst(JsonObject obj, bool set = false) {
if(set) {
obj[F("intvl")] = mCfg.inst.sendInterval;
obj[F("en")] = (bool)mCfg.inst.enabled;
obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight;
// obj[F("en")] = (bool)mCfg.inst.enabled;
obj[F("rstMidNight")] = (bool)mCfg.inst.rstValsAtMidNight;
obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail;
obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop;
obj[F("rstComStart")] = (bool)mCfg.inst.rstValsCommStart;
obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime;
obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstMaxValsMidNight;
obj[F("yldEff")] = mCfg.inst.yieldEffiency;
obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstIncludeMaxVals;
obj[F("rdGrid")] = (bool)mCfg.inst.readGrid;
}
else {
getVal<uint16_t>(obj, F("intvl"), &mCfg.inst.sendInterval);
getVal<bool>(obj, F("en"), &mCfg.inst.enabled);
getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight);
// getVal<bool>(obj, F("en"), &mCfg.inst.enabled);
getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstValsAtMidNight);
getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail);
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("rstMaxMidNight"), &mCfg.inst.rstMaxValsMidNight);
getVal<float>(obj, F("yldEff"), &mCfg.inst.yieldEffiency);
getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstIncludeMaxVals);
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;
@ -854,6 +897,7 @@ class settings {
}
#endif
private:
settings_t mCfg;
bool mLastSaveSucceed = 0;
};

65
src/defines.h

@ -13,8 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 8
#define VERSION_PATCH 83
#define VERSION_PATCH 140
//-------------------------------------
typedef struct {
uint8_t ch;
@ -24,41 +23,6 @@ typedef struct {
uint16_t millis;
} 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 {
AbsolutNonPersistent = 0UL, // 0x0000
RelativNonPersistent = 1UL, // 0x0001
@ -71,13 +35,6 @@ union serial_u {
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 {
DISP_TYPE_T0_NONE = 0,
DISP_TYPE_T1_SSD1306_128X64 = 1,
@ -89,27 +46,9 @@ enum {
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 {
uint32_t rxFail;
uint32_t rxFailNoAnser;
uint32_t rxFailNoAnswer;
uint32_t rxSuccess;
uint32_t frmCnt;
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 {
public:
void addImportant(Inverter<> *iv, uint8_t cmd) {
dec(&mRdPtr);
mQueue[mRdPtr] = queue_s(iv, cmd, true);
queue_s q(iv, cmd, true);
if(!isIncluded(&q)) {
dec(&mRdPtr);
mQueue[mRdPtr] = q;
}
}
void add(Inverter<> *iv, uint8_t cmd) {
mQueue[mWrPtr] = queue_s(iv, cmd, false);
inc(&mWrPtr);
queue_s q(iv, cmd, false);
if(!isIncluded(&q)) {
mQueue[mWrPtr] = q;
inc(&mWrPtr);
}
}
void chgCmd(Inverter<> *iv, uint8_t cmd) {
@ -117,6 +123,19 @@ class CommQueue {
--(*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:
std::array<queue_s, N> mQueue;
uint8_t mWrPtr = 0;

92
src/hm/Communication.h

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

23
src/hm/Heuristic.h

@ -38,6 +38,8 @@ class Heuristic {
ih->txRfChId = curId;
curId = (curId + 1) % RF_MAX_CHANNEL_ID;
}
if(ih->txRfQuality[ih->txRfChId] == RF_MIN_QUALTIY) // all channels are bad, reset...
ih->clear();
if(ih->testPeriodSendCnt < 0xff)
ih->testPeriodSendCnt++;
@ -71,7 +73,7 @@ class Heuristic {
return id2Ch(ih->txRfChId);
}
void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments) {
void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments, bool quotaMissed = false) {
HeuristicInv *ih = &iv->heuristics;
#if (DBG_DEBUG == DEBUG_LEVEL)
@ -84,8 +86,10 @@ class Heuristic {
DBGPRINT(", ");
DBGPRINTLN(String(ih->lastRxFragments));
#endif
if(quotaMissed) // we got not enough frames on this attempt, but iv was answering
updateQuality(ih, (rxFragments > 3 ? RF_TX_CHAN_QUALITY_GOOD : (rxFragments > 1 ? RF_TX_CHAN_QUALITY_OK : RF_TX_CHAN_QUALITY_LOW)));
if(ih->lastRxFragments == rxFragments) {
else if(ih->lastRxFragments == rxFragments) {
if(crcPass)
updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD);
else if(!retransmits || isNewTxCh(ih)) { // nothing received: send probably lost
@ -149,7 +153,7 @@ class Heuristic {
DBGPRINT(F(", f: "));
DBGPRINT(String(iv->radioStatistics.rxFail));
DBGPRINT(F(", n: "));
DBGPRINT(String(iv->radioStatistics.rxFailNoAnser));
DBGPRINT(String(iv->radioStatistics.rxFailNoAnswer));
DBGPRINT(F(" | p: ")); // better debugging for helpers...
if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
DBGPRINTLN(String(iv->config->powerLevel-10));
@ -213,15 +217,12 @@ class Heuristic {
}
inline uint8_t id2Ch(uint8_t id) {
switch(id) {
case 0: return 3;
case 1: return 23;
case 2: return 40;
case 3: return 61;
case 4: return 75;
}
return 3; // standard
if (id < RF_MAX_CHANNEL_ID)
return mChList[id];
else
return 3; // standard
}
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 "SPI.h"
#include "radio.h"
#include "Radio.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"
#endif
@ -28,24 +29,30 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
//-----------------------------------------------------------------------------
// 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>
class HmRadio : public Radio {
template <uint32_t DTU_SN = 0x81001765>
class NrfRadio : public Radio {
public:
HmRadio() {
NrfRadio() {
mDtuSn = DTU_SN;
mIrqRcvd = false;
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
#if defined(SPI_HAL)
//mNrf24.reset(new RF24());
#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
}
~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) {
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup"));
void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, cfgNrf24_t *cfg) {
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;
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;
#ifdef ESP32
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
mNrfHal.init(mosi, miso, sclk, cs, ce, SPI_SPEED);
#if defined(SPI_HAL)
mNrfHal.init(mCfg->pinMosi, mCfg->pinMiso, mCfg->pinSclk, mCfg->pinCs, mCfg->pinCe, SPI_SPEED);
mNrf24.reset(new RF24(&mNrfHal));
#else
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
@ -64,7 +71,7 @@ class HmRadio : public Radio {
#else
mSpi.reset(new SPIClass(VSPI));
#endif
mSpi->begin(sclk, miso, mosi, cs);
mSpi->begin(mCfg->pinSclk, mCfg->pinMiso, mCfg->pinMosi, mCfg->pinCs);
#endif
#else
//the old ESP82xx cannot freely place their SPI pins
@ -72,12 +79,12 @@ class HmRadio : public Radio {
mSpi->begin();
#endif
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
#if defined(SPI_HAL)
mNrf24->begin();
#else
mNrf24->begin(mSpi.get(), ce, cs);
mNrf24->begin(mSpi.get(), mCfg->pinCe, mCfg->pinCs);
#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->setAutoAck(true); // enabled by default
@ -99,21 +106,24 @@ class HmRadio : public Radio {
}
// returns true if communication is active
bool loop(void) override {
void loop(void) {
if(!mCfg->enabled)
return;
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!
return false;
return;
if(!mIrqRcvd) { // no news from nRF, check timers
if ((millis() - mTimeslotStart) < innerLoopTimeout)
return true; // nothing to do, still waiting
return; // nothing to do, still waiting
if (mRadioWaitTime.isTimeout()) { // timeout reached!
mNRFisInRX = false;
rx_ready = false;
return false;
return;
}
// otherwise switch to next RX channel
@ -132,7 +142,7 @@ class HmRadio : public Radio {
mNrf24->setChannel(mRfChLst[tempRxChIdx]);
isRxInit = false;
return true; // communicating, but changed RX channel
return; // communicating, but changed RX channel
} else {
// here we got news from the nRF
mIrqRcvd = false;
@ -145,7 +155,7 @@ class HmRadio : public Radio {
if(mNRFisInRX) {
DPRINTLN(DBG_WARN, F("unexpected tx irq!"));
return false;
return;
}
mNRFisInRX = true;
@ -159,7 +169,7 @@ class HmRadio : public Radio {
mTimeslotStart = millis();
tempRxChIdx = mRxChIdx; // might be better to start off with one channel less?
mRxPendular = false;
mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1);
mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1 || mLastIv->mCmd == MI_REQ_CH2);
innerLoopTimeout = DURATION_LISTEN_MIN;
}
@ -181,18 +191,23 @@ class HmRadio : public Radio {
}
}
rx_ready = false; // reset
return mNRFisInRX;
return;
}
}
return false;
return;
}
bool isChipConnected(void) const override {
if(!mCfg->enabled)
return false;
return mNrf24->isChipConnected();
}
void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override {
if(!mCfg->enabled)
return;
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("sendControlPacket cmd: "));
DBGHEXLN(cmd);
@ -279,13 +294,14 @@ class HmRadio : public Radio {
}
uint8_t getDataRate(void) const {
if(!mNrf24->isChipConnected())
if(!isChipConnected())
return 3; // unknown
return mNrf24->getDataRate();
}
bool isPVariant(void) const {
return mNrf24->isPVariant();
if(!isChipConnected())
return mNrf24->isPVariant();
}
private:
@ -413,6 +429,7 @@ class HmRadio : public Radio {
}
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
uint8_t mTxChIdx = 0;
uint8_t mRxChIdx = 0;
@ -432,7 +449,7 @@ class HmRadio : public Radio {
std::unique_ptr<SPIClass> mSpi;
std::unique_ptr<RF24> mNrf24;
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
#if defined(SPI_HAL)
nrfHal mNrfHal;
#endif
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 getBootFreqMhz() { return 0; }
virtual std::pair<uint16_t,uint16_t> getFreqRangeMhz(void) { return std::make_pair(0, 0); }
virtual bool loop(void) = 0;
virtual void loop(void) = 0;
Radio() : mTxBuf{} {}

63
src/hm/hmDefines.h

@ -9,6 +9,41 @@
#include "../utils/dbg.h"
#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
enum {IV_MI = 0, IV_HM, IV_HMS, IV_HMT, IV_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_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_BOOTLOADER_VER,
FLD_ACT_ACTIVE_PWR_LIMIT, FLD_PART_NUM, FLD_HW_VERSION, FLD_GRID_PROFILE_CODE,
FLD_GRID_PROFILE_VERSION, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP};
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",
"U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "UAC_12", "UAC_23", "UAC_31", "I_AC",
"IAC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC",
"U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "U_AC_12", "U_AC_23", "U_AC_31", "I_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",
"active_PowerLimit", "HWPartNumber", "HWVersion", "GridProfileCode",
"GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower"};
"GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower", "MaxTemp"};
const char* const notAvail = "n/a";
const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH,
UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_A, UNIT_A, UNIT_A, UNIT_A,
UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR,
UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE, UNIT_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
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))
// 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};
@ -173,7 +208,8 @@ const byteAssign_t hm1chAssignment[] = {
{ 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_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_PAYLOAD_LEN 30
@ -211,7 +247,8 @@ const byteAssign_t hm2chAssignment[] = {
{ 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_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))
@ -266,7 +303,8 @@ const byteAssign_t hm4chAssignment[] = {
{ 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_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_PAYLOAD_LEN 62
@ -351,8 +389,11 @@ const devInfo_t devInfo[] = {
{ 0x102271, 2000 }, // v2 black backplane, 16A
// HMT
{ 0x103311, 1800 },
{ 0x103331, 2250 }
{ 0x103241, 1600 }, // -4T
{ 0x103251, 1800 }, // -4T
{ 0x103271, 2000 }, // -4T
{ 0x103311, 1800 }, // -6T
{ 0x103331, 2250 } // -6T
};
#define MI_REQ_CH1 0x09

220
src/hm/hmInverter.h

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

27
src/hm/hmSystem.h

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

20
src/hm/nrfHal.h

@ -9,18 +9,15 @@
#pragma once
#include "../utils/spiPatcher.h"
#include <esp_rom_gpio.h>
#include <RF24_hal.h>
#include <RF24.h>
#define NRF_MAX_TRANSFER_SZ 64
#define NRF_DEFAULT_SPI_SPEED 10000000 // 10 MHz
class nrfHal: public RF24_hal, public SpiPatcherHandle {
public:
nrfHal() {
mSpiPatcher = SpiPatcher::getInstance(SPI2_HOST);
}
nrfHal() {}
void patch() override {
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);
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_set_direction(mPinMosi, GPIO_MODE_OUTPUT);
@ -56,6 +59,7 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
gpio_set_level(mPinClk, 0);
gpio_reset_pin(mPinCs);
request_spi();
spi_device_interface_config_t devcfg = {
.command_bits = 0,
.address_bits = 0,
@ -72,14 +76,14 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
.pre_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_set_direction(mPinEn, GPIO_MODE_OUTPUT);
gpio_set_level(mPinEn, 0);
}
bool begin() override {
return true;
}

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

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

4
src/hms/cmt2300a.h

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

16
src/hms/cmtHal.h

@ -16,9 +16,7 @@
class cmtHal : public SpiPatcherHandle {
public:
cmtHal() {
mSpiPatcher = SpiPatcher::getInstance(DEF_CMT_SPI_HOST);
}
cmtHal() {}
void patch() override {
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);
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_set_direction(mPinSdio, GPIO_MODE_INPUT_OUTPUT);
@ -50,6 +54,7 @@ class cmtHal : public SpiPatcherHandle {
gpio_set_level(mPinClk, 0);
gpio_reset_pin(mPinCs);
request_spi();
spi_device_interface_config_t devcfg_reg = {
.command_bits = 1,
.address_bits = 7,
@ -66,7 +71,8 @@ class cmtHal : public SpiPatcherHandle {
.pre_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);
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_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_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_PAYLOAD_LEN 30
@ -70,7 +71,8 @@ const byteAssign_t hms2chAssignment[] = {
{ 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_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_PAYLOAD_LEN 42
@ -123,11 +125,73 @@ const byteAssign_t hms4chAssignment[] = {
{ 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_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_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
//-------------------------------------
@ -199,7 +263,8 @@ const byteAssign_t hmt6chAssignment[] = {
{ 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_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_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
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#if defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(ETHERNET)
#ifndef __ETH_SPI_H__
#define __ETH_SPI_H__
@ -14,23 +12,25 @@
#include <Arduino.h>
#include <esp_netif.h>
#include <WiFiGeneric.h>
#include <driver/spi_master.h>
#include "../utils/spiPatcher.h"
// Functions from WiFiGeneric
void tcpipInit();
void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif);
class EthSpi {
class AhoyEthernetSpi {
public:
EthSpi() :
AhoyEthernetSpi() :
eth_handle(nullptr),
eth_netif(nullptr) {}
void begin(int8_t pin_miso, int8_t pin_mosi, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) {
gpio_reset_pin(static_cast<gpio_num_t>(pin_rst));
gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0);
if(-1 != pin_rst) {
gpio_reset_pin(static_cast<gpio_num_t>(pin_rst));
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_mosi));
@ -44,22 +44,14 @@ class EthSpi {
gpio_reset_pin(static_cast<gpio_num_t>(pin_int));
gpio_set_pull_mode(static_cast<gpio_num_t>(pin_int), GPIO_PULLUP_ONLY);
spi_bus_config_t buscfg = {
.mosi_io_num = pin_mosi,
.miso_io_num = pin_miso,
.sclk_io_num = pin_sclk,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0, // uses default value internally
.flags = 0,
.intr_flags = 0
};
#if defined(CONFIG_IDF_TARGET_ESP32S3)
mHostDevice = SPI3_HOST;
#else
mHostDevice = (14 == pin_sclk) ? SPI2_HOST : SPI3_HOST;
#endif
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 = {
.command_bits = 16, // actually address phase
@ -78,13 +70,14 @@ class EthSpi {
.post_cb = nullptr
};
spi_device_handle_t spi;
ESP_ERROR_CHECK(spi_bus_add_device(SPI3_HOST, &devcfg, &spi));
mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi);
// Reset sequence
delayMicroseconds(500);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
delayMicroseconds(1000);
if(-1 != pin_rst) {
delayMicroseconds(500);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
delayMicroseconds(1000);
}
// Arduino function to start networking stack if not already started
tcpipInit();
@ -123,10 +116,14 @@ class EthSpi {
}
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);
char mac_addr_str[24];
snprintf(mac_addr_str, sizeof(mac_addr_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
char mac_addr_str[19];
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);
}
@ -134,8 +131,10 @@ class EthSpi {
private:
esp_eth_handle_t eth_handle;
esp_netif_t *eth_netif;
spi_host_device_t mHostDevice;
spi_device_handle_t spi;
SpiPatcher *mSpiPatcher;
};
#endif /*__ETH_SPI_H__*/
#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/applyPatches.py
pre:../scripts/reduceGxEPD2.py
post:../scripts/add_littlefs_binary.py
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
https://github.com/nRF24/RF24 @ 1.4.8
https://github.com/esphome/ESPAsyncWebServer @ ^3.2.2
https://github.com/nRF24/RF24.git#v1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/bertmelis/espMqttClient#v1.7.0
bblanchon/ArduinoJson @ ^6.21.5
https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.35.9
https://github.com/zinggjm/GxEPD2#1.5.3
build_flags =
-std=c++17
-std=gnu++17
@ -40,67 +42,78 @@ build_unflags =
-std=gnu++11
[env:esp8266]
[env:esp8266-minimal]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
lib_deps =
${env.lib_deps}
https://github.com/me-no-dev/ESPAsyncUDP
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
;-Wl,-Map,output.map
monitor_filters =
esp8266_exception_decoder
[env:esp8266]
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]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
lib_deps = ${env:esp8266.lib_deps}
build_flags = ${env:esp8266.build_flags}
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
;-Wl,-Map,output.map
monitor_filters =
esp8266_exception_decoder
[env:esp8266-prometheus]
[env:esp8266-all]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
lib_deps = ${env:esp8266.lib_deps}
build_flags = ${env:esp8266.build_flags}
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
[env:esp8266-prometheus-de]
[env:esp8266-all-de]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_PROMETHEUS_EP
lib_deps = ${env:esp8266.lib_deps}
build_flags = ${env:esp8266-all.build_flags}
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
[env:esp8266-minimal]
[env:esp8266-prometheus]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
;-Wl,-Map,output.map
lib_deps = ${env:esp8266.lib_deps}
build_flags = ${env:esp8266-all.build_flags}
-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 =
esp8266_exception_decoder
@ -111,6 +124,7 @@ platform = espressif8266
board = esp8285
board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L
lib_deps = ${env:esp8266.lib_deps}
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DENABLE_MQTT
@ -124,6 +138,7 @@ platform = espressif8266
board = esp8285
board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L
lib_deps = ${env:esp8266.lib_deps}
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
-DLANG_DE
@ -133,114 +148,73 @@ build_flags = ${env.build_flags}
monitor_filters =
esp8266_exception_decoder
[env:esp32-wroom32]
platform = espressif32@6.5.0
[env:esp32-wroom32-minimal]
platform = espressif32@6.7.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DSPI_HAL
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-minimal]
platform = espressif32@6.5.0
[env:esp32-wroom32]
platform = espressif32@6.7.0
board = lolin_d32
build_flags = ${env.build_flags}
build_flags = ${env:esp32-wroom32-minimal.build_flags}
-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 =
esp32_exception_decoder
[env:esp32-wroom32-de]
platform = espressif32@6.5.0
platform = espressif32@6.7.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
build_flags = ${env:esp32-wroom32.build_flags}
-DLANG_DE
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-prometheus]
platform = espressif32@6.5.0
platform = espressif32@6.7.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
build_flags = ${env:esp32-wroom32.build_flags}
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-prometheus-de]
platform = espressif32@6.5.0
platform = espressif32@6.7.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DLANG_DE
-DENABLE_PROMETHEUS_EP
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[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
build_flags = ${env:esp32-wroom32-prometheus.build_flags}
-DLANG_DE
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[env:esp32-s2-mini]
platform = espressif32@6.5.0
platform = espressif32@6.7.0
board = lolin_s2_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DSPI_HAL
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
@ -259,33 +233,19 @@ monitor_filters =
esp32_exception_decoder
[env:esp32-s2-mini-de]
platform = espressif32@6.5.0
platform = espressif32@6.7.0
board = lolin_s2_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DDEF_NRF_CS_PIN=12
-DDEF_NRF_CE_PIN=3
-DDEF_NRF_IRQ_PIN=5
-DDEF_NRF_MISO_PIN=9
-DDEF_NRF_MOSI_PIN=11
-DDEF_NRF_SCLK_PIN=7
-DDEF_CMT_CSB=16
-DDEF_CMT_FCSB=18
-DDEF_CMT_IRQ=33
-DDEF_CMT_SDIO=35
-DDEF_CMT_SCLK=37
build_flags = ${env:esp32-s2-mini.build_flags}
-DLANG_DE
monitor_filters =
esp32_exception_decoder
[env:esp32-c3-mini]
platform = espressif32@6.5.0
platform = espressif32@6.7.0
board = lolin_c3_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DSPI_HAL
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
@ -304,36 +264,18 @@ monitor_filters =
esp32_exception_decoder
[env:esp32-c3-mini-de]
platform = espressif32@6.5.0
platform = espressif32@6.7.0
board = lolin_c3_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DDEF_NRF_CS_PIN=5
-DDEF_NRF_CE_PIN=0
-DDEF_NRF_IRQ_PIN=1
-DDEF_NRF_MISO_PIN=3
-DDEF_NRF_MOSI_PIN=4
-DDEF_NRF_SCLK_PIN=2
-DDEF_CMT_CSB=255
-DDEF_CMT_FCSB=255
-DDEF_CMT_IRQ=255
-DDEF_CMT_SDIO=255
-DDEF_CMT_SCLK=255
build_flags = ${env:esp32-c3-mini.build_flags}
-DLANG_DE
monitor_filters =
esp32_exception_decoder
[env:opendtufusion]
platform = espressif32@6.5.0
[env:opendtufusion-minimal]
platform = espressif32@6.7.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DSPI_HAL
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
@ -354,145 +296,48 @@ build_flags = ${env.build_flags}
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-de]
platform = espressif32@6.5.0
[env:opendtufusion]
platform = espressif32@6.7.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DLANG_DE
build_flags = ${env:opendtufusion-minimal.build_flags}
-DETHERNET
-DENABLE_MQTT
-DPLUGIN_DISPLAY
-DENABLE_HISTORY
-DSPI_HAL
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
-DDEF_NRF_MISO_PIN=48
-DDEF_NRF_MOSI_PIN=35
-DDEF_NRF_SCLK_PIN=36
-DDEF_CMT_CSB=4
-DDEF_CMT_FCSB=21
-DDEF_CMT_IRQ=8
-DDEF_CMT_SDIO=5
-DDEF_CMT_SCLK=6
-DDEF_LED0=18
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
-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
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-minimal]
platform = espressif32@6.5.0
[env:opendtufusion-de]
platform = espressif32@6.7.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DSPI_HAL
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
-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
build_flags = ${env:opendtufusion.build_flags}
-DLANG_DE
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-ethernet]
platform = espressif32@6.5.0
[env:opendtufusion-16MB]
platform = espressif32@6.7.0
board = esp32-s3-devkitc-1
lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.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
board_upload.flash_size = 16MB
board_build.partitions = default_16MB.csv
upload_protocol = esp-builtin
build_flags = ${env.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
build_flags = ${env:opendtufusion.build_flags}
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-ethernet-de]
platform = espressif32@6.5.0
[env:opendtufusion-16MB-de]
platform = espressif32@6.7.0
board = esp32-s3-devkitc-1
lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.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
build_flags = ${env.build_flags}
-DETHERNET
-DSPI_HAL
build_flags = ${env:opendtufusion-16MB.build_flags}
-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 =
esp32_exception_decoder, colorize

58
src/plugins/Display/Display.h

@ -7,8 +7,9 @@
#include <U8g2lib.h>
#include "../../hm/hmSystem.h"
#include "../../hm/hmRadio.h"
#include "../../hm/NrfRadio.h"
#include "../../utils/helper.h"
#include "../plugin_lang.h"
#include "Display_Mono.h"
#include "Display_Mono_128X32.h"
#include "Display_Mono_128X64.h"
@ -24,9 +25,9 @@ class Display {
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;
mHmRadio = hmradio;
mNrfRadio = nrfRadio;
mHmsRadio = hmsradio;
mCfg = cfg;
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_T5_SSD1306_64X48: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield)
case DISP_TYPE_T6_SSD1309_128X64: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42")
#if defined(ESP32) && !defined(ETHERNET)
#if defined(ESP32)
case DISP_TYPE_T10_EPAPER:
mMono = NULL; // ePaper does not use this
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) {
mNewPayload = true;
}
@ -78,16 +87,25 @@ class Display {
void tickerSecond() {
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());
if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) {
DataScreen();
mNewPayload = false;
mLoopCnt = 0;
if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) {
DataScreen();
mNewPayload = false;
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();
}
#endif
}
@ -148,7 +166,7 @@ class Display {
mDisplayData.totalYieldDay = totalYieldDay;
mDisplayData.totalYieldTotal = totalYieldTotal;
bool nrf_en = mApp->getNrfEnabled();
bool nrf_ok = nrf_en && mHmRadio->isChipConnected();
bool nrf_ok = nrf_en && mNrfRadio->isChipConnected();
#if defined(ESP32)
bool cmt_en = mApp->getCmtEnabled();
bool cmt_ok = cmt_en && mHmsRadio->isChipConnected();
@ -175,16 +193,18 @@ class Display {
if (mMono ) {
mMono->disp();
}
#if defined(ESP32) && !defined(ETHERNET)
#if defined(ESP32)
else if (DISP_TYPE_T10_EPAPER == mCfg->type) {
mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
mRefreshCycle++;
}
if (mRefreshCycle > 480) {
mEpaper.fullRefresh();
mRefreshCycle = 0;
if (mRefreshCycle > 2880) { // 15 * 2280 = 44300s = 12h
mEpaper.fullRefresh();
mRefreshCycle = 0;
}
}
#endif
}
@ -230,11 +250,11 @@ class Display {
uint32_t *mUtcTs = nullptr;
display_t *mCfg = nullptr;
HMSYSTEM *mSys = nullptr;
RADIO *mHmRadio = nullptr;
RADIO *mNrfRadio = nullptr;
RADIO *mHmsRadio = nullptr;
uint16_t mRefreshCycle = 0;
#if defined(ESP32) && !defined(ETHERNET)
#if defined(ESP32)
DisplayEPaper mEpaper;
#endif
DisplayMono *mMono = nullptr;

8
src/plugins/Display/Display_Mono_128X32.h

@ -40,20 +40,20 @@ class DisplayMono128X32 : public DisplayMono {
printText(mFmtText, 0);
} 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);
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);
IPAddress ip = WiFi.localIP();
if (!(mExtra % 10) && (ip))
printText(ip.toString().c_str(), 3);
else if (!(mExtra % 5)) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", mDisplayData->nrProducing);
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %d", STR_ACTIVE_INVERTERS, mDisplayData->nrProducing);
printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs)
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
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)) {
// alternatively:
@ -108,7 +108,7 @@ class DisplayMono128X64 : public DisplayMono {
int8_t moon_pos = -1;
setLineFont(l_Status);
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) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, " ");
sun_pos = 0;
@ -145,7 +145,7 @@ class DisplayMono128X64 : public DisplayMono {
printText(mFmtText, l_TotalPower, 0xff);
} 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);
} else {
printText("offline", 0);
printText(STR_OFFLINE, 0);
}
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", mDisplayData->totalYieldDay);
@ -55,7 +55,7 @@ class DisplayMono64X48 : public DisplayMono {
if (!(mExtra % 10) && (ip))
printText(ip.toString().c_str(), 3);
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);
} else if (0 != mDisplayData->utcTs)
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
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)) {
// alternatively:
@ -90,7 +90,7 @@ class DisplayMono84X48 : public DisplayMono {
// print status of inverters
else {
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)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol
else if (0 == mDisplayData->nrProducing)
@ -110,9 +110,8 @@ class DisplayMono84X48 : public DisplayMono {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
printText(mFmtText, l_TotalPower, 0xff);
} else {
printText("offline", l_TotalPower, 0xff);
}
} else
printText(STR_OFFLINE, l_TotalPower, 0xff);
}
if (showLine(l_YieldDay)) {

103
src/plugins/Display/Display_ePaper.cpp

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

15
src/plugins/Display/Display_ePaper.h

@ -12,7 +12,11 @@
#define EPAPER_MAX_TEXT_LEN 35
#include <GxEPD2_BW.h>
#if defined(SPI_HAL)
#include "epdHal.h"
#else
#include <SPI.h>
#endif
// FreeFonts from Adafruit_GFX
#include <Fonts/FreeSans12pt7b.h>
@ -44,9 +48,9 @@ class DisplayEPaper {
DONE,
BLACK,
WHITE,
WAIT,
PARTITIALS,
LOGO
LOGO,
LOGO_WAIT
};
uint8_t mDisplayRotation;
@ -58,8 +62,13 @@ class DisplayEPaper {
uint32_t* mUtcTs;
bool mEnPowerSave;
const char* _version;
RefreshStatus mRefreshState, mNextRefreshState;
RefreshStatus mRefreshState;
uint8_t mSecondCnt;
bool mLogoDisplayed;
#if defined(SPI_HAL)
epdHal hal;
#endif
};
#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 {
POWER,
POWER_DAY,
YIELD
};
@ -27,12 +28,14 @@ class HistoryData {
uint16_t refreshCycle = 0;
uint16_t loopCnt = 0;
uint16_t listIdx = 0; // index for next Element to write into WattArr
uint16_t dispIdx = 0; // index for 1st Element to display from WattArr
bool wrapped = false;
// ring buffer for watt history
std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data;
storage_t() { data.fill(0); }
void reset() {
loopCnt = 0;
listIdx = 0;
data.fill(0);
}
};
public:
@ -43,33 +46,52 @@ class HistoryData {
mTs = ts;
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() {
;
float curPwr = 0;
float maxPwr = 0;
float yldDay = -0.1;
uint32_t ts = 0;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
Inverter<> *iv = mSys->getInverterByPos(i);
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL)
continue;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec);
yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
if (rec->ts > ts)
ts = rec->ts;
}
if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) {
mCurPwr.loopCnt = 0;
if (curPwr > 0)
if (curPwr > 0) {
mLastValueTs = ts;
addValue(&mCurPwr, roundf(curPwr));
if (maxPwr > 0)
mMaximumDay = roundf(maxPwr);
if (curPwr > mMaximumDay)
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 ((!mDayStored) && (yldDay > 0)) {
addValue(&mYieldDay, roundf(yldDay));
@ -77,28 +99,172 @@ class HistoryData {
}
} else if (*mTs > mApp->getSunrise())
mDayStored = false;
}*/
}
#endif
}
uint16_t valueAt(HistoryStorageType type, uint16_t i) {
//storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay;
storage_t *s = &mCurPwr;
uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH;
return s->data[idx];
storage_t *s = nullptr;
uint16_t idx=i;
DPRINTLN(DBG_VERBOSE, F("valueAt ") + String((uint8_t)type) + " i=" + String(i));
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() {
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:
void addValue(storage_t *s, uint16_t value) {
if (s->wrapped) // after 1st time array wrap we have to increase the display index
s->dispIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH);
s->data[s->listIdx] = value;
s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH);
if (s->listIdx == 0)
s->wrapped = true;
}
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:
@ -109,8 +275,23 @@ class HistoryData {
uint32_t *mTs = nullptr;
storage_t mCurPwr;
storage_t mCurPwrDay;
#if defined(ENABLE_HISTORY_YIELD_PER_DAY)
storage_t mYieldDay;
#endif
bool mDayStored = false;
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*/

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)
#ifdef ESP8266
#include <ESP8266WiFi.h>
#define xSemaphoreTake(a, b) { while(a) { yield(); } a = true; }
#define xSemaphoreGive(a) { a = false; }
#elif defined(ESP32)
#include <WiFi.h>
#endif
@ -39,6 +41,13 @@ template<class HMSYSTEM>
class PubMqtt {
public:
PubMqtt() : SendIvData() {
#if defined(ESP32)
mutex = xSemaphoreCreateBinaryStatic(&mutexBuffer);
xSemaphoreGive(mutex);
#else
mutex = false;
#endif
mLastIvState.fill(InverterStatus::OFF);
mIvLastRTRpub.fill(0);
@ -50,9 +59,14 @@ class PubMqtt {
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;
mDevName = devName;
mVersion = version;
@ -61,7 +75,7 @@ class PubMqtt {
mUptime = uptime;
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) {
publish(subTopic, payload, retained, true, qos);
});
@ -95,6 +109,17 @@ class PubMqtt {
}
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();
#if defined(ESP8266)
@ -204,6 +229,9 @@ class PubMqtt {
else
snprintf(mTopic.data(), mTopic.size(), "%s", subTopic);
if(!mCfgMqtt->enableRetain)
retained = false;
mClient.publish(mTopic.data(), qos, retained, payload);
yield();
mTxCnt++;
@ -242,7 +270,8 @@ class PubMqtt {
void setPowerLimitAck(Inverter<> *iv) {
if (NULL != iv) {
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) {
DPRINTLN(DBG_INFO, F("MQTT connected"));
publish(subtopics[MQTT_VERSION], mVersion, true);
publish(subtopics[MQTT_DEVICE], mDevName, true);
#if defined(ETHERNET)
publish(subtopics[MQTT_IP_ADDR], ETH.localIP().toString().c_str(), true);
#else
publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true);
#endif
publish(subtopics[MQTT_VERSION], mVersion, false);
publish(subtopics[MQTT_DEVICE], mDevName, false);
publish(subtopics[MQTT_IP_ADDR], mApp->getIp().c_str(), true);
tickerMinute();
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++) {
snprintf(mVal.data(), mVal.size(), "ctrl/limit/%d", i);
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) {
if(len == 0)
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]);
DBGPRINTLN(String(topic));
if(NULL == mSubscriptionCb)
@ -353,9 +389,9 @@ class PubMqtt {
pos++;
}
/*char out[128];
char out[128];
serializeJson(root, out, 128);
DPRINTLN(DBG_INFO, "json: " + String(out));*/
DPRINTLN(DBG_INFO, "json: " + String(out));
(mSubscriptionCb)(root);
mRxCnt++;
@ -397,6 +433,10 @@ class PubMqtt {
std::array<char, 32> name;
std::array<char, 32> uniq_id;
std::array<char, 350> buf;
topic.fill(0);
name.fill(0);
uniq_id.fill(0);
buf.fill(0);
const char *devCls, *stateCls;
if (!total) {
if (rec->assign[mDiscovery.sub].ch == CH0)
@ -419,8 +459,8 @@ class PubMqtt {
}
DynamicJsonDocument doc2(512);
constexpr static char* unitTotal[] = {"W", "kWh", "Wh", "W"};
doc2[F("name")] = name;
constexpr static const char* unitTotal[] = {"W", "kWh", "Wh", "W"};
doc2[F("name")] = String(name.data());
doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic.data());
doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub]));
doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id.data();
@ -437,9 +477,9 @@ class PubMqtt {
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]]);
size_t size = measureJson(doc2) + 1;
buf.fill(0);
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)) {
mDiscovery.sub = 0;
@ -559,6 +599,9 @@ class PubMqtt {
}
void sendData(Inverter<> *iv, uint8_t curInfoCmd) {
if (mCfgMqtt->json)
return;
record_t<> *rec = iv->getRecordStruct(curInfoCmd);
uint32_t lastTs = iv->getLastTs(rec);
@ -602,10 +645,80 @@ class PubMqtt {
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;
cfgMqtt_t *mCfgMqtt = nullptr;
IApp *mApp;
#if defined(ESP8266)
WiFiEventHandler mHWifiCon, mHWifiDiscon;
volatile bool mutex;
#else
SemaphoreHandle_t mutex;
StaticSemaphore_t mutexBuffer;
#endif
HMSYSTEM *mSys = nullptr;
@ -621,6 +734,8 @@ class PubMqtt {
std::array<uint32_t, MAX_NUM_INVERTERS> mIvLastRTRpub;
uint16_t mIntervalTimeout = 0;
std::queue<message_s> mReceiveQueue;
// last will topic and payload must be available through lifetime of 'espMqttClient'
std::array<char, (MQTT_TOPIC_LEN + 5)> mLwtTopic;
const char *mDevName = nullptr, *mVersion = nullptr;

96
src/publisher/pubMqttIvData.h

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

2
src/utils/dbg.h

@ -110,7 +110,7 @@
#if DEBUG_LEVEL >= DBG_ERROR
#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
#define PERR(str)
#define PERRLN(str)

35
src/utils/helper.cpp

@ -5,6 +5,12 @@
#include "helper.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 {
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]);
}
double round1(double value) {
return (int)(value * 10 + 0.5) / 10.0;
}
double round3(double value) {
return (int)(value * 1000 + 0.5) / 1000.0;
}
@ -82,6 +92,31 @@ namespace ah {
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) {
char tmp[3];
uint64_t ret = 0ULL;

2
src/utils/helper.h

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

8
src/utils/improv.h

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

16
src/utils/scheduler.h

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

3
src/utils/spiPatcher.cpp

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

78
src/utils/spiPatcher.h

@ -9,23 +9,38 @@
#if defined(ESP32)
#include "dbg.h"
#include "spiPatcherHandle.h"
#include <driver/spi_master.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 {
protected:
explicit SpiPatcher(spi_host_device_t dev) :
mHostDevice(dev), mCurHandle(nullptr) {
mCurHandle(nullptr) {
// Use binary semaphore instead of mutex for performance reasons
mutex = xSemaphoreCreateBinaryStatic(&mutex_buffer);
xSemaphoreGive(mutex);
mDev = dev;
mBusState = ESP_FAIL;
}
spi_bus_config_t buscfg = {
.mosi_io_num = -1,
.miso_io_num = -1,
.sclk_io_num = -1,
public:
SpiPatcher(const SpiPatcher &other) = delete;
void operator=(const SpiPatcher &) = delete;
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,
.quadhd_io_num = -1,
.data4_io_num = -1,
@ -36,26 +51,48 @@ class SpiPatcher {
.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:
SpiPatcher(SpiPatcher &other) = delete;
void operator=(const SpiPatcher &) = delete;
return mBusState;
}
static SpiPatcher* getInstance(spi_host_device_t dev) {
if(nullptr == mInstance)
mInstance = new SpiPatcher(dev);
return mInstance;
static SpiPatcher* getInstance(spi_host_device_t dev, bool initialize = true) {
if(SPI2_HOST == dev) {
if(nullptr == InstanceHost2) {
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); }
spi_host_device_t getDevice() {
return mHostDevice;
inline void addDevice(spi_host_device_t host_id, const spi_device_interface_config_t *dev_config, spi_device_handle_t *handle) {
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) {
assert(mBusState == ESP_OK);
xSemaphoreTake(mutex, portMAX_DELAY);
if (mCurHandle != handle) {
@ -70,17 +107,22 @@ class SpiPatcher {
}
inline void release() {
assert(mBusState == ESP_OK);
xSemaphoreGive(mutex);
}
protected:
static SpiPatcher *mInstance;
static SpiPatcher *InstanceHost2;
static SpiPatcher *InstanceHost3;
private:
const spi_host_device_t mHostDevice;
SpiPatcherHandle* mCurHandle;
SemaphoreHandle_t mutex;
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*/

2
src/web/Protection.h

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

534
src/web/RestApi.h

@ -17,11 +17,7 @@
#include "../utils/helper.h"
#include "lang.h"
#include "AsyncJson.h"
#if defined(ETHERNET)
#include "AsyncWebServer_ESP32_W5500.h"
#else
#include "ESPAsyncWebServer.h"
#endif
#include "plugins/history.h"
@ -44,16 +40,24 @@ class RestApi {
mApp = app;
mSrv = srv;
mSys = sys;
mRadioNrf = (HmRadio<>*)mApp->getRadioObj(true);
mRadioNrf = (NrfRadio<>*)mApp->getRadioObj(true);
#if defined(ESP32)
mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false);
#endif
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(
std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5));
mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1));
mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1));
#if defined(ESP32)
mSrv->on("/coredump", HTTP_GET, std::bind(&RestApi::getCoreDump, this, std::placeholders::_1));
#endif
}
uint32_t getTimezoneOffset(void) {
@ -77,9 +81,19 @@ class RestApi {
#ifndef ESP32
mHeapFreeBlk = ESP.getMaxFreeBlockSize();
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
#if defined(ESP32)
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8000);
#else
AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000);
#endif
JsonObject root = response->getRoot();
String path = request->url().substring(5);
@ -97,12 +111,13 @@ class RestApi {
else if(path == "inverter/list") getInverterList(root);
else if(path == "index") getIndex(request, root);
else if(path == "setup") getSetup(request, root);
#if !defined(ETHERNET)
else if(path == "setup/networks") getNetworks(root);
else if(path == "setup/getip") getWifiIp(root);
#endif /* !defined(ETHERNET) */
else if(path == "setup/getip") getIp(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 {
if(path.substring(0, 12) == "inverter/id/")
getInverter(root, request->url().substring(17).toInt());
@ -137,11 +152,98 @@ class RestApi {
#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");
if(0 == index) {
if(NULL != mTmpBuf)
if(nullptr != mTmpBuf)
delete[] mTmpBuf;
mTmpBuf = new uint8_t[total+1];
mTmpSize = total;
@ -154,36 +256,40 @@ class RestApi {
DynamicJsonDocument json(1000);
DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize);
JsonObject obj = json.as<JsonObject>();
AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
JsonObject root = response->getRoot();
root[F("success")] = (err) ? false : true;
if(!err) {
String path = request->url().substring(5);
if(path == "ctrl")
root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str());
else if(path == "setup")
root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str());
else {
root[F("success")] = false;
root[F("error")] = F(PATH_NOT_FOUND) + path;
}
} else {
switch (err.code()) {
case DeserializationError::Ok: break;
case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break;
case DeserializationError::InvalidInput: root[F("error")] = F(INVALID_INPUT); break;
case DeserializationError::NoMemory: root[F("error")] = F(NOT_ENOUGH_MEM); break;
default: root[F("error")] = F(DESER_FAILED); break;
DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize);
if(!json.is<JsonObject>())
root[F("error")] = F(DESER_FAILED);
else {
JsonObject obj = json.as<JsonObject>();
root[F("success")] = (err) ? false : true;
if(!err) {
String path = request->url().substring(5);
if(path == "ctrl")
root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str());
else if(path == "setup")
root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str());
else {
root[F("success")] = false;
root[F("error")] = F(PATH_NOT_FOUND) + path;
}
} else {
switch (err.code()) {
case DeserializationError::Ok: break;
case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break;
case DeserializationError::InvalidInput: root[F("error")] = F(INVALID_INPUT); break;
case DeserializationError::NoMemory: root[F("error")] = F(NOT_ENOUGH_MEM); break;
default: root[F("error")] = F(DESER_FAILED); break;
}
}
}
response->setLength();
request->send(response);
delete[] mTmpBuf;
mTmpBuf = NULL;
mTmpBuf = nullptr;
}
void getNotFound(JsonObject obj, String url) {
@ -195,14 +301,13 @@ class RestApi {
ep[F("generic")] = url + F("generic");
ep[F("index")] = url + F("index");
ep[F("setup")] = url + F("setup");
#if !defined(ETHERNET)
ep[F("setup/networks")] = url + F("setup/networks");
ep[F("setup/getip")] = url + F("setup/getip");
#endif /* !defined(ETHERNET) */
ep[F("system")] = url + F("system");
ep[F("live")] = url + F("live");
#if defined(ENABLE_HISTORY)
ep[F("powerHistory")] = url + F("powerHistory");
ep[F("powerHistoryDay")] = url + F("powerHistoryDay");
#endif
}
@ -236,7 +341,7 @@ class RestApi {
if(-1 != i) {
i+=5;
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);
@ -252,9 +357,38 @@ class RestApi {
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) {
mApp->resetLockTimeout();
obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI();
obj[F("ts_uptime")] = mApp->getUptime();
obj[F("ts_now")] = mApp->getTimestamp();
@ -262,6 +396,7 @@ class RestApi {
obj[F("modules")] = String(mApp->getVersionModules());
obj[F("build")] = String(AUTO_GIT_HASH);
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_mask")] = (uint16_t)(mConfig->sys.protectionMask );
obj[F("menu_protEn")] = (bool) (mConfig->sys.adminPwd[0] != '\0');
@ -278,67 +413,38 @@ class RestApi {
}
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("dark_mode")] = (bool)mConfig->sys.darkMode;
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("prot_mask")] = mConfig->sys.protectionMask;
obj[F("sdk")] = ESP.getSdkVersion();
obj[F("cpu_freq")] = ESP.getCpuFreqMHz();
obj[F("heap_free")] = mHeapFree;
obj[F("sketch_total")] = ESP.getFreeSketchSpace();
obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb
getGeneric(request, obj);
getGeneric(request, obj.createNestedObject(F("generic")));
getChipInfo(obj.createNestedObject(F("chip")));
getRadioNrf(obj.createNestedObject(F("radioNrf")));
getMqttInfo(obj.createNestedObject(F("mqtt")));
getNetworkInfo(obj.createNestedObject(F("network")));
getMemoryInfo(obj.createNestedObject(F("memory")));
#if defined(ESP32)
getRadioCmtInfo(obj.createNestedObject(F("radioCmt")));
#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);
obj[F("schMax")] = max;
obj[F("schMax")] = max;*/
}
void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) {
getSysInfo(request, obj.createNestedObject(F("system")));
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];
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);
}
@ -351,8 +457,8 @@ class RestApi {
void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
obj[F("refresh")] = 5;
#if defined(ETHERNET)
obj[F("refresh")] = (mConfig->sys.eth.enabled) ? 5 : 20;
#else
obj[F("refresh")] = 20;
#endif
@ -365,8 +471,8 @@ class RestApi {
obj[F("pending")] = (bool)mApp->getSavePending();
obj[F("success")] = (bool)mApp->getLastSaveSucceed();
obj[F("reboot")] = (bool)mApp->getShouldReboot();
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
obj[F("reload")] = 5;
#if defined(ETHERNET)
obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20;
#else
obj[F("reload")] = 20;
#endif
@ -383,7 +489,7 @@ class RestApi {
mApp->setRebootFlag();
obj[F("html")] = F("Erase settings: success");
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
obj[F("reload")] = 5;
obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20;
#else
obj[F("reload")] = 20;
#endif
@ -391,7 +497,9 @@ class RestApi {
void getHtmlFactory(AsyncWebServerRequest *request, JsonObject obj) {
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) {
@ -399,8 +507,8 @@ class RestApi {
mApp->eraseSettings(true);
mApp->setRebootFlag();
obj[F("html")] = F("Factory reset: success");
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
obj[F("reload")] = 5;
#if defined(ETHERNET)
obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20;
#else
obj[F("reload")] = 20;
#endif
@ -422,7 +530,7 @@ class RestApi {
obj[F("name")] = String(iv->config->name);
obj[F("rx_success")] = iv->radioStatistics.rxSuccess;
obj[F("rx_fail")] = iv->radioStatistics.rxFail;
obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnser;
obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnswer;
obj[F("frame_cnt")] = iv->radioStatistics.frmCnt;
obj[F("tx_cnt")] = iv->radioStatistics.txCnt;
obj[F("retransmits")] = iv->radioStatistics.retransmits;
@ -474,13 +582,13 @@ class RestApi {
}
obj[F("interval")] = String(mConfig->inst.sendInterval);
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("rstComStop")] = (bool)mConfig->inst.rstValsCommStop;
obj[F("rstComStart")] = (bool)mConfig->inst.rstValsCommStart;
obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime;
obj[F("rdGrid")] = (bool)mConfig->inst.readGrid;
obj[F("rstMaxMid")] = (bool)mConfig->inst.rstMaxValsMidNight;
obj[F("yldEff")] = mConfig->inst.yieldEffiency;
obj[F("rstMaxMid")] = (bool)mConfig->inst.rstIncludeMaxVals;
}
void getInverter(JsonObject obj, uint8_t id) {
@ -496,7 +604,7 @@ class RestApi {
obj[F("name")] = String(iv->config->name);
obj[F("serial")] = String(iv->config->serial.u64, HEX);
obj[F("version")] = String(iv->getFwVersion());
obj[F("power_limit_read")] = 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("max_pwr")] = iv->getMaxPower();
obj[F("ts_last_success")] = rec->ts;
@ -505,6 +613,7 @@ class RestApi {
obj[F("alarm_cnt")] = iv->alarmCnt;
obj[F("rssi")] = iv->rssi;
obj[F("ts_max_ac_pwr")] = iv->tsMaxAcPower;
obj[F("ts_max_temp")] = iv->tsMaxTemperature;
JsonArray ch = obj.createNestedArray("ch");
@ -561,11 +670,23 @@ class RestApi {
obj[F("last_id")] = iv->getChannelFieldValue(CH0, FLD_EVT, rec);
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++) {
alarm[i][F("code")] = iv->lastAlarm[i].code;
alarm[i][F("str")] = iv->getAlarmStr(iv->lastAlarm[i].code);
alarm[i][F("start")] = iv->lastAlarm[i].start;
alarm[i][F("end")] = iv->lastAlarm[i].end;
if((iv->lastAlarm[i].start != 0) && (iv->lastAlarm[i].start < oldestStart)) {
offset = i;
oldestStart = iv->lastAlarm[i].start;
}
}
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("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String("");
obj[F("topic")] = String(mConfig->mqtt.topic);
obj[F("json")] = (bool) mConfig->mqtt.json;
obj[F("interval")] = String(mConfig->mqtt.interval);
obj[F("retain")] = (bool)mConfig->mqtt.enableRetain;
}
void getNtp(JsonObject obj) {
@ -665,6 +788,116 @@ class RestApi {
}
#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) {
obj[F("en")] = (bool) mConfig->nrf.enabled;
if(mConfig->nrf.enabled) {
@ -752,11 +985,9 @@ class RestApi {
warn.add(F(REBOOT_ESP_APPLY_CHANGES));
if(0 == mApp->getTimestamp())
warn.add(F(TIME_NOT_SET));
#if !defined(ETHERNET)
#if !defined(ESP32)
if(mApp->getWasInCh12to14())
warn.add(F(WAS_IN_CH_12_TO_14));
#endif
#if !defined(ESP32)
if(mApp->getWasInCh12to14())
warn.add(F(WAS_IN_CH_12_TO_14));
#endif
}
@ -771,24 +1002,28 @@ class RestApi {
#if defined(ESP32)
getRadioCmt(obj.createNestedObject(F("radioCmt")));
#endif
#if defined(ETHERNET)
getEthernet(obj.createNestedObject(F("eth")));
#endif
getRadioNrf(obj.createNestedObject(F("radioNrf")));
getSerial(obj.createNestedObject(F("serial")));
getStaticIp(obj.createNestedObject(F("static_ip")));
getDisplay(obj.createNestedObject(F("display")));
}
#if !defined(ETHERNET)
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) {
getGeneric(request, obj.createNestedObject(F("generic")));
obj[F("refresh")] = mConfig->inst.sendInterval;
obj[F("max_total_pwr")] = ah::round3(mApp->getTotalMaxPower());
for (uint8_t fld = 0; fld < sizeof(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")));
#if defined(ENABLE_HISTORY)
obj[F("refresh")] = mConfig->inst.sendInterval;
obj[F("refresh")] = mApp->getHistoryPeriod(static_cast<uint8_t>(type));
uint16_t max = 0;
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld);
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;
if (value > max)
max = value;
}
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) {
if(jsonIn.containsKey(F("auth"))) {
@ -867,11 +1129,11 @@ class RestApi {
iv->powerLimit[1] = AbsolutNonPersistent;
accepted = iv->setDevControlRequest(ActivePowerContr);
if(accepted)
mApp->triggerTickSend();
} else if(F("dev") == jsonIn[F("cmd")]) {
DPRINTLN(DBG_INFO, F("dev cmd"));
iv->setDevCommand(jsonIn[F("val")].as<int>());
} else if(F("restart_ahoy") == jsonIn[F("cmd")]) {
mApp->setRebootFlag();
} else {
jsonOut[F("error")] = F("ERR_UNKNOWN_CMD");
return false;
@ -889,11 +1151,6 @@ class RestApi {
if(isProtected(jsonIn, jsonOut, clientIP))
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")])
mApp->setTimestamp(jsonIn[F("val")]);
else if(F("sync_ntp") == jsonIn[F("cmd")])
@ -902,17 +1159,38 @@ class RestApi {
mTimezoneOffset = jsonIn[F("val")];
else if(F("discovery_cfg") == jsonIn[F("cmd")])
mApp->setMqttDiscoveryFlag(); // for homeassistant
#if !defined(ETHERNET)
else if(F("save_wifi") == jsonIn[F("cmd")]) {
snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", jsonIn[F("ssid")].as<const char*>());
snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", jsonIn[F("pwd")].as<const char*>());
mApp->saveSettings(false); // without reboot
mApp->setStopApAllowedMode(false);
mApp->setupStation();
}
#endif /* !defined(ETHERNET */
#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")]) {
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->serial.u64 = jsonIn[F("ser")];
snprintf(iv->config->name, MAX_NAME_LENGTH, "%s", jsonIn[F("name")].as<const char*>());
@ -956,15 +1234,15 @@ class RestApi {
private:
constexpr static uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT,
FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP};
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,
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};
private:
IApp *mApp = nullptr;
HMSYSTEM *mSys = nullptr;
HmRadio<> *mRadioNrf = nullptr;
NrfRadio<> *mRadioNrf = nullptr;
#if defined(ESP32)
CmtRadio<> *mRadioCmt = nullptr;
#endif
@ -974,7 +1252,7 @@ class RestApi {
uint32_t mTimezoneOffset = 0;
uint32_t mHeapFree = 0, mHeapFreeBlk = 0;
uint8_t mHeapFrag = 0;
uint8_t *mTmpBuf = NULL;
uint8_t *mTmpBuf = nullptr;
uint32_t mTmpSize = 0;
};

2
src/web/html/about.html

@ -14,7 +14,7 @@
<div class="p-2">Used Libraries</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/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/nrf24/RF24" target="_blank">nrf24/RF24</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])
}
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) {
if (typeof n === "string") {
el.innerHTML = n;
@ -84,10 +101,12 @@ function topnav() {
}
function parseNav(obj) {
for(i = 0; i < 13; i++) {
for(i = 0; i < 14; i++) {
if(i == 2)
continue;
var l = document.getElementById("nav"+i);
if(null == l)
continue
if(12 == i) {
if(obj.cst_lnk.length > 0) {
l.href = obj.cst_lnk
@ -124,7 +143,7 @@ function parseVersion(obj) {
function parseESP(obj) {
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;
else if(obj["wifi_rssi"] <= -70)
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) {
@ -221,6 +244,10 @@ function badge(success, text, second="error") {
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) {
var els = document.getElementsByClassName("nav-link");
[].forEach.call(els, function(e) {

5
src/web/html/colorBright.css

@ -12,6 +12,7 @@
--nav-bg: #333;
--primary: #006ec0;
--primary-disabled: #ccc;
--primary-hover: #044e86;
--secondary: #0072c8;
--nav-active: #555;
@ -30,4 +31,8 @@
--ch-head-bg: #006ec0;
--ts-head: #333;
--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;
--primary: #004d87;
--primary-disabled: #ccc;
--primary-hover: #023155;
--secondary: #0072c8;
--nav-active: #555;
@ -30,4 +31,8 @@
--ch-head-bg: #236;
--ts-head: #333;
--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"},
{"0x0a00": "DE NF_EN_50549-1:2019"},
{"0x0c00": "AT_TOR_Erzeuger_default"},
{"0x0c03": "AT_TOR_Erzeuger_cosphi=1"},
{"0x0c04": "AT_TOR_Erzeuger_default"},
{"0x0d00": "FR_VFR2019"},
{"0x0d04": "NF_EN_50549-1:2019"},
{"0x1000": "ES_RD1699"},
{"0x1200": "EU_EN50438"},
{"0x1300": "MEX_NOM_220V"},
{"0x2600": "BE_C10_26"},
{"0x2900": "NL_NEN-EN50549-1_2019"},
{"0x2a00": "PL_PN-EN 50549-1:2019"},
@ -35,6 +38,44 @@
{"0xb0": "Watt Power Factor"}
],
"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": [
{
@ -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": [
{
@ -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": [
{

216
src/web/html/history.html

@ -13,82 +13,178 @@
<div id="wrapper">
<div id="content">
<h3>{#TOTAL_POWER}</h3>
<div>
<div class="chartDiv" id="pwrChart"> </div>
<p>
{#MAX_DAY}: <span id="pwrMaxDay"></span> W. {#LAST_VALUE}: <span id="pwrLast"></span> W.<br />
{#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS}
</p>
</div>
<div class="chartDiv" id="pwrChart"></div>
<h3>{#TOTAL_POWER_DAY}</h3>
<div class="chartDiv" id="pwrDayChart"></div>
<!--IF_ENABLE_HISTORY_YIELD_PER_DAY-->
<h3>{#TOTAL_YIELD_PER_DAY}</h3>
<div class="chartDiv" id="ydChart"></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>
{#HTML_FOOTER}
<script type="text/javascript">
const svgns = "http://www.w3.org/2000/svg";
var pwrExeOnce = true;
var ydExeOnce = true;
// make a simple rectangle
var mRefresh = 60;
var mLastValue = 0;
const mChartHeight = 250;
function parseHistory(obj, namePrefix, execOnce) {
mRefresh = obj.refresh
var data = Object.assign({}, obj.value)
numDataPts = Object.keys(data).length
if (true == execOnce) {
let s = document.createElementNS(svgns, "svg");
s.setAttribute("class", "chart");
s.setAttribute("width", (numDataPts + 2) * 2);
s.setAttribute("height", mChartHeight);
s.setAttribute("role", "img");
let g = document.createElementNS(svgns, "g");
s.appendChild(g);
for (var i = 0; i < numDataPts; i++) {
val = data[i];
let rect = document.createElementNS(svgns, "rect");
rect.setAttribute("id", namePrefix+"Rect" + i);
rect.setAttribute("x", i * 2);
rect.setAttribute("width", 2);
g.appendChild(rect);
const height = 250
var once = true
function calcScale(obj) {
let s = {}
s.x_mul = 60
s.ts_dur = obj.refresh * obj.value.length
s.ts_start = obj.lastValueTs - s.ts_dur
while(s.x_mul * 10 <= s.ts_dur)
s.x_mul += (s.x_mul == 60) ? 240 : ((s.x_mul < 1800) ? 300 : 1800)
s.y_mul = 10
while(s.y_mul * 10 <= obj.max)
s.y_mul += (s.y_mul < 100) ? 10 : 100
s.y_step = Math.ceil(obj.max / s.y_mul)
s.y_max = s.y_mul * s.y_step
return s
}
function setupSvg(id, obj) {
let scale = calcScale(obj)
let n = obj.value.length
return mlNs("svg", {class: "container", id: id+"_svg", viewBox: "0 0 "+String(n*2+50)+" "+String(height+20), width: "100%", height: "100%"}, [
mlNs("defs", {}, [
mlNs("linearGradient", {id: "gLine", x1: 0, y1: 0, x2: 0, y2: "100%"}, [
mlNs("stop", {offset: 0, "stop-color": "#006ec0"}),
mlNs("stop", {offset: "80%", "stop-color": "#5050ff"}),
mlNs("stop", {offset: "100%", "stop-color": "gray"})
]),
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
let divider = obj.max / mChartHeight;
if (divider == 0)
divider = 1;
for (var i = 0; i < numDataPts; i++) {
val = data[i];
if (val > 0)
mLastValue = val
val = val / divider
rect = document.getElementById(namePrefix+"Rect" + i);
rect.setAttribute("height", val);
rect.setAttribute("y", mChartHeight - val);
function grid(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("line", {x1: 0, x2: x2, y1: height-i*div, y2: height-i*div, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"}))
}
div = x2 / scale.ts_dur
for(let i = 0; i <= scale.ts_dur; i++) {
if((i + scale.ts_start) % scale.x_mul == 0) {
g.push(mlNs("line", {x1: (i*div), x2: (i*div), y1: 0, y2: height, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"}))
}
}
document.getElementById(namePrefix + "Max").innerHTML = obj.max;
if (mRefresh < 5)
mRefresh = 5;
document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh;
return g
}
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){
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) {
parseNav(obj.generic);
parseHistory(obj,"pwr", pwrExeOnce)
document.getElementById("pwrLast").innerHTML = mLastValue
document.getElementById("pwrMaxDay").innerHTML = obj.maxDay
let svg = setupSvg("phDay", obj);
document.getElementById("pwrDayChart").replaceChildren(svg);
/*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
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);
</script>

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

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

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

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

46
src/web/html/index.html

@ -14,6 +14,7 @@
</p>
<p>
<span class="des">System Infos:</span>
<div id="total"></div>
<div id="iv"></div>
<div class="hr"></div>
<div id="warn_info"></div>
@ -23,9 +24,9 @@
<h3>{#SUPPORT}:</h3>
<ul>
<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>{#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>{#DON_MAKE} <a href="https://paypal.me/lupusch" target="_blank">{#DONATION}</a></li>
</ul>
@ -56,8 +57,10 @@
}
function parseGeneric(obj) {
if(exeOnce)
if(exeOnce) {
parseESP(obj)
parseTitle(obj)
}
parseRssi(obj)
}
@ -70,9 +73,9 @@
var min = parseInt(up / 60) % 60;
var sec = up % 60;
var e = document.getElementById("uptime");
e.innerHTML = days + " Day";
e.innerHTML = days + " {#DAY}";
if(1 != days)
e.innerHTML += "s";
e.innerHTML += "{#S}";
e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2);
@ -111,6 +114,10 @@
function parseIv(obj, ts) {
var p = div(["none"]);
var total = 0;
var count = 0;
var mobile = window.screen.width < 470;
for(var i of obj) {
var icon = iconSuccess;
var cl = "icon-success";
@ -126,20 +133,28 @@
} else if(0 == i["ts_last_success"]) {
avail = "{#AVAIL_NO_DATA}";
} else {
avail = "{#AVAIL} ";
if (!mobile)
avail = "{#AVAIL} ";
if(false == i["is_producing"])
avail += "{#NOT_PRODUCING}";
else {
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(
svg(icon, 30, 30, "icon " + cl),
span("{#INVERTER} #" + i["id"] + ": " + i["name"] + " {#IS} " + avail),
br()
);
svg(icon, 30, 30, "icon " + cl),
span(text + i["id"] + ": " + i["name"] + " {#IS} " + avail),
br()
);
if(false == i["is_avail"]) {
if(i["ts_last_success"] > 0) {
@ -149,6 +164,13 @@
}
}
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) {
@ -165,7 +187,7 @@
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#UPDATE_AVAIL}: " + release), br());
else if(getVerInt("{#VERSION}") > getVerInt(release))
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#USING_DEV_VERSION} {#VERSION}. {#DEV_ISSUE_RELEASE_VERSION}: " + release), br());
else
else
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#RELEASE_INSTALLED}: " + release), br());
}

15
src/web/html/serial.html

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

369
src/web/html/setup.html

@ -52,28 +52,17 @@
<div class="s_content">
<fieldset class="mb-2">
<legend class="des">WiFi</legend>
<div class="row mb-3">
<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>
<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="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">
<select name="networks" id="networks" onChange="selNet()">
<option value="-1" selected disabled hidden>{#NETWORK_NOT_SCANNED}</option>
</select>
<input type="text" name="ssid"/><br/>
<a href="/wizard">{#SCAN_WIFI}</a>
</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="col-12 col-sm-3">{#SSID_HIDDEN}</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>
<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>
<div class="row mb-3">
@ -146,7 +139,7 @@
<div class="col-4"><input type="checkbox" name="invRstNotAvail"/></div>
</div>
<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>
<div class="row mb-3">
@ -157,10 +150,6 @@
<div class="col-8">{#INV_READ_GRID_PROFILE}</div>
<div class="col-4"><input type="checkbox" name="rdGrid"/></div>
</div>
<div class="row mb-3">
<div class="col-8">{#INV_YIELD_EFF}</div>
<div class="col-4"><input type="number" name="yldEff" step="any"/></div>
</div>
</fieldset>
</div>
@ -246,6 +235,10 @@
<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>
<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>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">{#INTERVAL}</div>
@ -258,6 +251,10 @@
<span id="apiResultMqtt"></span>
</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>
</div>
@ -274,9 +271,13 @@
<p class="des">{#RADIO} (CMT2300A)</p>
<div id="cmt"></div>
<!--ENDIF_ESP32-->
<!--IF_ETHERNET-->
<p class="des">Ethernet</p>
<div id="eth"></div>
<!--ENDIF_ETHERNET-->
</fieldset>
</div>
<!--IF_PLUGIN_DISPLAY-->
<button type="button" class="s_collapsible">{#DISPLAY_CONFIG}</button>
<div class="s_content">
<fieldset class="mb-4">
@ -290,7 +291,7 @@
<div id="screenSaver"></div>
<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-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>
<p class="des">{#DISP_PINOUT}</p>
<div id="dispPins"></div>
@ -299,12 +300,13 @@
<p class="des">{#GRAPH_OPTIONS}</p>
<div class="row mb-3">
<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 id="graphSize"></div>
</div>
</fieldset>
</div>
<!--ENDIF_PLUGIN_DISPLAY-->
<div class="row mb-4 mt-4">
<div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div>
@ -324,8 +326,8 @@
<div class="col-12 col-sm-9">
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
<div class="row">
<div class="col-12 col-sm-8 my-2"><input type="file" 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-8 my-2"><input type="file" id="importFileInput" name="upload"></div>
<div class="col-12 col-sm-4 my-2"><input type="button" id="importButton" class="btn" value="Import" onclick="hide()"></div>
</div>
</form>
</div>
@ -345,6 +347,7 @@
var maxInv = 0;
var ts = 0;
/*IF_ESP8266*/
var esp8266pins = [
[255, "{#PIN_OFF}"],
[0, "D3 (GPIO0)"],
@ -365,6 +368,7 @@
[15, "D8 (GPIO15)"],
[16, "D0 (GPIO16 - {#PIN_NO_IRQ})"]
];
/*ENDIF_ESP8266*/
/*IF_ESP32*/
var esp32pins = [
@ -396,6 +400,7 @@
[36, "VP (GPIO36, {#PIN_INPUT_ONLY})"],
[39, "VN (GPIO39, {#PIN_INPUT_ONLY})"]
];
/*IF_ESP32-S2*/
var esp32sXpins = [
[255, "off / default"],
[0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
@ -444,6 +449,58 @@
[47, "GPIO47"],
[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 = [
[255, "off / default"],
[0, "GPIO0"],
@ -469,6 +526,7 @@
[20, "GPIO20 (RX)"],
[21, "GPIO21 (TX)"],
];
/*ENDIF_ESP32-C3*/
/*ENDIF_ESP32*/
var nrfPa = [
[0, "MIN ({#PIN_RECOMMENDED})"],
@ -549,12 +607,6 @@
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() {
var obj = {cmd: "sync_ntp", token: "*"}
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj))
@ -566,6 +618,22 @@
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() {
document.getElementById("form").submit();
var e = document.getElementById("content");
@ -605,53 +673,55 @@
}
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]];
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("strtWthtTm")[0].checked = obj["strtWthtTm"];
document.getElementsByName("rdGrid")[0].checked = obj["rdGrid"];
}
function parseSys(obj) {
for(var i of [["device", "device_name"], ["ssid", "ssid"], ["ap_pwd", "ap_pwd"]])
document.getElementsByName(i[0])[0].value = obj[i[1]];
document.getElementsByName("hidd")[0].checked = obj["hidd"];
document.getElementsByName("darkMode")[0].checked = obj["dark_mode"];
document.getElementsByName("schedReboot")[0].checked = obj["sched_reboot"];
document.getElementsByName("device")[0].value = obj.device_name;
for(var i of [["ssid", "ssid"], ["ap_pwd", "ap_pwd"]])
document.getElementsByName(i[0])[0].value = obj.network[i[1]];
document.getElementsByName("hidd")[0].checked = obj.network.hidd;
document.getElementsByName("darkMode")[0].checked = obj.dark_mode;
document.getElementsByName("schedReboot")[0].checked = obj.sched_reboot;
e = document.getElementsByName("adminpwd")[0];
if(!obj["pwd_set"])
if(!obj.pwd_set)
e.value = "";
var d = document.getElementById("prot_mask");
var a = ["Index", "{#NAV_LIVE}", "{#NAV_WEBSERIAL}", "{#NAV_SETTINGS}", "Update", "System", "{#NAV_HISTORY}"];
var el = [];
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))
}
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) {
parseNav(obj);
parseESP(obj);
parseRssi(obj);
parseNav(obj)
parseESP(obj)
parseRssi(obj)
parseTitle(obj)
if(0 != obj.cst_lnk.length) {
document.getElementsByName("cstLnk")[0].value = obj.cst_lnk
document.getElementsByName("cstLnkTxt")[0].value = obj.cst_lnk_txt
}
ts = obj["ts_now"];
ts = obj.ts_now;
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) {
@ -730,7 +800,7 @@
cbEn.checked = (obj.enabled);
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", {}, [
tabs(["{#TAB_GENERAL}", "{#TAB_INPUTS}", "{#TAB_RADIO}", "{#TAB_ADVANCED}"]),
ml("div", {id: "div{#TAB_GENERAL}", class: "tab-content"}, [
@ -786,7 +856,8 @@
case 0x1000: nrf = true; break;
case 0x1100:
switch(sn & 0x000f) {
case 0x0004: nrf = false; break;
case 0x0004:
case 0x0005: nrf = false; break;
default: nrf = true; break;
}
break;
@ -802,11 +873,16 @@
ser.dispatchEvent(new Event('change'));
function ivSave() {
var o = new Object();
var o = {}
o.cmd = "save_iv"
o.token = "*"
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.en = document.getElementsByName("enable")[0].checked;
o.ch = [];
@ -826,6 +902,30 @@
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) {
var e = document.getElementById("res");
if(!obj2.success)
@ -847,6 +947,7 @@
function del() {
var o = new Object();
o.cmd = "save_iv";
o.token = "*"
o.id = obj.id;
o.ser = 0;
o.name = "";
@ -873,6 +974,8 @@
function parseMqtt(obj) {
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("mqttJson")[0].checked = obj["json"];
document.getElementsByName("retain")[0].checked = obj.retain
}
function parseNtp(obj) {
@ -891,13 +994,21 @@
}
}
function parsePinout(obj, type, system) {
function parsePinout(obj) {
var e = document.getElementById("pinout");
var pinList = esp8266pins;
/*IF_ESP32*/
var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins;
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
/*IF_ESP32-S2*/
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*/
pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']];
for(p of pins) {
@ -924,16 +1035,24 @@
)
}
function parseNrfRadio(obj, objPin, type, system) {
function parseNrfRadio(obj, objPin) {
var e = document.getElementById("rf24");
var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox");
en.checked = obj["en"];
var pinList = esp8266pins;
/*IF_ESP32*/
var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins;
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
/*IF_ESP32-S2*/
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*/
e.replaceChildren (
@ -943,11 +1062,11 @@
])
);
if ("ESP8266" == type) {
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']];
} else {
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso']];
}
/*IF_ESP32*/
var pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso']];
/*ELSE*/
var pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']];
/*ENDIF_ESP32*/
for(p of pins) {
e.append(
ml("div", {class: "row mb-3"}, [
@ -961,15 +1080,21 @@
}
/*IF_ESP32*/
function parseCmtRadio(obj, type, system) {
function parseCmtRadio(obj) {
var e = document.getElementById("cmt");
var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox");
var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins;
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
/*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"}, "{#CMT_ENABLE}"),
@ -996,6 +1121,42 @@
}
/*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) {
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}"]]
@ -1011,12 +1172,21 @@
}
}
/*IF_PLUGIN_DISPLAY*/
function parseDisplay(obj, type, system) {
var pinList = esp8266pins;
/*IF_ESP32*/
var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins;
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins;
/*IF_ESP32-S2*/
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*/
for(var i of ["disp_pwr"])
@ -1152,6 +1322,7 @@
setHide("screenSaver", !optionsMap.get(dispType)[2]);
setHide("pirPin", !(optionsMap.get(dispType)[2] && (screenSaver==2))); // show pir pin only for motion screensaver
}
/*ENDIF_PLUGIN_DISPLAY*/
function tick() {
document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000)));
@ -1159,42 +1330,28 @@
function parse(root) {
if(null != root) {
parseGeneric(root["generic"]);
parseSys(root["system"]);
parseStaticIp(root["static_ip"]);
parseMqtt(root["mqtt"]);
parseNtp(root["ntp"]);
parseSun(root["sun"]);
parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]);
parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]);
parseGeneric(root.generic);
parseSys(root.system);
parseStaticIp(root.static_ip);
parseMqtt(root.mqtt);
parseNtp(root.ntp);
parseSun(root.sun);
parsePinout(root.pinout);
parseNrfRadio(root.radioNrf, root.pinout);
/*IF_ESP32*/
parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]);
parseCmtRadio(root.radioCmt);
/*ENDIF_ESP32*/
parseSerial(root["serial"]);
parseDisplay(root["display"], root["system"]["esp_type"], root["system"]);
/*IF_ETHERNET*/
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);
}
}
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);
</script>
</body>

40
src/web/html/style.css

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

122
src/web/html/system.html

@ -8,29 +8,44 @@
{#HTML_NAV}
<div id="wrapper">
<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>
</div>
{#HTML_FOOTER}
<script type="text/javascript">
function parseGeneric(obj) {
parseNav(obj);
parseESP(obj);
parseRssi(obj);
parseNav(obj)
parseESP(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) {
const data = ["sdk", "cpu_freq", "chip_revision",
"chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime",
"flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag",
"max_free_blk", "version", "modules", "env", "core_version", "reboot_reason"];
lines = [];
for (const [key, value] of Object.entries(obj)) {
if(!data.includes(key) || (typeof value == 'undefined')) continue;
lines.push(tr(key.replace('_', ' '), value));
}
lines = [
tr("{#DEVICE_NAME}", obj.device_name),
tr("{#UPTIME}", parseUptime(obj.generic.ts_uptime)),
tr("{#REBOOT_REASON}", obj.chip.reboot_reason),
tr("{#ENVIRONMENT}", obj.generic.env + " ({#BUILD_OPTIONS}: " + obj.generic.modules + ")"),
tr("Version", obj.generic.version + " - " + obj.generic.build),
tr("Chip", "CPU: " + obj.chip.cpu_freq + "MHz, " + obj.chip.cores + " Core(s)"),
tr("Chip Model", obj.chip.model)
]
document.getElementById("info").append(
headline("System Information"),
@ -46,10 +61,10 @@
function irqBadge(state) {
switch(state) {
case 0: return badge(false, "{#UNKNOWN}", "warning"); break;
case 1: return badge(true, "{#TRUE}"); break;
default: return badge(false, "{#FALSE}"); break;
}
case 0: return badge(false, "unknown", "warning"); break;
case 1: return badge(true, "true"); break;
default: return badge(false, "false"); break;
}
}
function parseRadio(obj) {
@ -58,15 +73,15 @@
if(obj.radioNrf.en) {
lines = [
tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")),
tr("{#IRQ_WORKING}", irqBadge(obj.radioNrf.irqOk)),
tr("{#NRF24_DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"),
tr("DTU Radio ID", obj.radioNrf.sn)
tr("{#INTR_PIN_WORKING}", irqBadge(obj.radioNrf.irqOk)),
tr("NRF24 {#DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"),
tr("DTU {#RADIO} ID", obj.radioNrf.sn)
];
} else
lines = [tr("NRF24L01", badge(false, "{#NOT_ENABLED}"))];
lines = [tr("NRF24L01", badge(false, "{#NOT} {#ENABLED}"))];
document.getElementById("info").append(
headline("{#NRF24_RADIO}"),
headline("{#RADIO} NRF24"),
ml("table", {class: "table"},
ml("tbody", {}, lines)
)
@ -76,14 +91,14 @@
if(obj.radioCmt.en) {
cmt = [
tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")),
tr("{#IRQ_WORKING}", irqBadge(obj.radioCmt.irqOk)),
tr("DTU Radio ID", obj.radioCmt.sn)
tr("{#INTR_PIN_WORKING}", irqBadge(obj.radioCmt.irqOk)),
tr("DTU {#RADIO} ID", obj.radioCmt.sn)
];
} else
cmt = [tr("CMT2300A", badge(false, "{#NOT_ENABLED}"))];
cmt = [tr("CMT2300A", badge(false, "{#NOT} {#ENABLED}"))];
document.getElementById("info").append(
headline("{#CMT_RADIO}"),
headline("{#RADIO} CMT"),
ml("table", {class: "table"},
ml("tbody", {}, cmt)
)
@ -91,16 +106,32 @@
/*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) {
if(obj.enabled) {
lines = [
tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "{#TRUE}" : "{#FALSE}"))),
tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "true" : "false"))),
tr("#TX", obj.tx_cnt),
tr("#RX", obj.rx_cnt)
];
]
} else
lines = tr("{#ENABLED}", badge(false, "{#FALSE}"));
lines = tr("enabled", badge(false, "false"));
document.getElementById("info").append(
headline("MqTT"),
@ -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) {
if(obj.ts_sunrise > 0) {
document.getElementById("info").append(
headline("{#SUN}"),
headline("Sun"),
ml("table", {class: "table"},
ml("tbody", {}, [
tr("{#SUNRISE}", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')),
tr("{#SUNSET}", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')),
tr("{#COMMUNICATION_START}", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')),
tr("{#COMMUNICATION_STOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')),
tr("{#NIGHT_BEHAVE}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning"))
tr("{#NIGHT_BEHAVIOR}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning"))
])
)
);
@ -137,10 +185,12 @@
meta.content = obj.refresh + "; URL=" + obj.refresh_url;
document.getElementsByTagName('head')[0].appendChild(meta);
} else if(null != obj.system) {
parseRadio(obj.system);
parseMqtt(obj.system.mqtt);
parseSysInfo(obj.system);
getAjax('/api/index', parseIndex);
parseRadio(obj.system)
parseNetwork(obj.system.network, obj.system.generic)
parseMqtt(obj.system.mqtt)
parseMemory(obj.system.memory)
parseSysInfo(obj.system)
getAjax('/api/index', parseIndex)
}
document.getElementById("html").innerHTML = obj.html;
}

23
src/web/html/update.html

@ -12,22 +12,39 @@
<legend class="des">{#SELECT_FILE} (*.bin)</legend>
<p>{#INSTALLED_VERSION}:<br/><span id="version" style="background-color: var(--input-bg); padding: 7px; display: block; margin: 3px;"></span></p>
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update">
<input type="button" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()">
<input type="file" id="uploadFileInput" name="update">
<input type="button" id="uploadButton" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()">
</form>
</fieldset>
<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>
{#HTML_FOOTER}
<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;
function parseGeneric(obj) {
parseNav(obj)
parseESP(obj)
parseRssi(obj)
parseTitle(obj)
env = obj.env
document.getElementById("version").innerHTML = "{#VERSION_FULL}_" + obj.env + ".bin"
}

41
src/web/html/visualization.html

@ -21,6 +21,7 @@
var mNum = 0;
var total = Array(6).fill(0);
var tPwrAck;
var totalsRendered = false
function getErrStr(code) {
if("ERR_AUTH") return "{#ERR_AUTH}"
@ -33,10 +34,11 @@
function parseGeneric(obj) {
if(true == exeOnce){
parseNav(obj);
parseESP(obj);
parseNav(obj)
parseESP(obj)
parseTitle(obj)
}
parseRssi(obj);
parseRssi(obj)
}
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"}, [
ml("div", {class: "row"},
ml("div", opt,
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)
])
),
@ -74,6 +76,7 @@
for(var i = 0; i < 6; i++) {
total[i] = Math.round(total[i] * 100) / 100;
}
totalsRendered = true
return ml("div", {class: "row mt-3 mb-5"},
ml("div", {class: "col"}, [
@ -104,7 +107,6 @@
total[4] += obj.ch[0][8]; // P_DC
total[5] += obj.ch[0][10]; // Q_AC
}
total[3] += obj.ch[0][11]; // MAX P_AC
total[1] += obj.ch[0][7]; // YieldDay
total[2] += obj.ch[0][6]; // YieldTotal
@ -116,10 +118,11 @@
if(65535 != obj.power_limit_read) {
pwrLimit = obj.power_limit_read + "&nbsp;%";
if(0 != obj.max_pwr)
pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "&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"},
ml("div", {class: "col"}, [
ml("div", {class: "p-2 " + clh},
@ -134,7 +137,7 @@
ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() {
getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm);
}}, ("{#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}, [
@ -145,7 +148,7 @@
]),
ml("div", {class: "hr"}),
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][0], "V", "{#AC_VOLTAGE}"),
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++) {
if((i != ivEn.length) && ivEn[i]) {
last = false;
getAjax("/api/inverter/id/" + i, parseIv);
break;
return
}
}
if(last) {
if(mNum > 1)
if(mNum > 1) {
if(!totalsRendered)
mIvHtml.unshift(totals());
document.getElementById("live").replaceChildren(...mIvHtml);
}
document.getElementById("live").replaceChildren(...mIvHtml);
}
function parseIvAlarm(obj) {
@ -275,7 +276,7 @@
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(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"]));
mIvHtml = [];
mNum = 0;
totalsRendered = false
total.fill(0);
total[3] = obj.max_total_pwr
for(var i = 0; i < obj.iv.length; i++) {
if(obj.iv[i]) {
getAjax("/api/inverter/id/" + i, parseIv);

255
src/web/html/wizard.html

@ -4,7 +4,7 @@
<title>{#NAV_WIZARD}</title>
{#HTML_HEADER}
</head>
<body>
<body onload="init()">
<div id="wrapper">
<div class="container d-flex aic jc">
<div id="con"></div>
@ -14,6 +14,166 @@
var v;
var found = false;
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) {
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", {}, [
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}"))),
@ -30,7 +219,7 @@
sect("{#WIFI_MANUAL}", ml("input", {id: "man", type: "text"})),
sect("{#WIFI_PASSWORD}", ml("input", {id: "pwd", type: "password"})),
ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_NEXT}", onclick: () => {saveWifi()}}, null))),
ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}")))
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}"))),
sect("{#TRY_TO_CONNECT}", ml("span", {id: "state"}, "{#CONNECTING}")),
ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn hide", id: "btn", value: "{#BTN_FINISH}", onclick: () => {redirect()}}, null))),
ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}")))
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() {
window.location.replace("http://192.168.4.1/")
window.location.replace(redirIp)
}
function printIp(obj) {
if("0.0.0.0" != obj["ip"]) {
if("0.0.0.0" != obj.ip) {
clearInterval(v)
setHide("btn", false)
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() {
var ssid = document.getElementById("net").value;
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}));
}
function nets(obj) {
var e = document.getElementById("net");
if(obj.networks.length > 0) {
var a = []
a.push(ml("option", {value: -1}, obj.networks.length + " {#NUM_NETWORKS_FOUND}"))
for(n of obj.networks) {
a.push(ml("option", {value: n.ssid}, n.ssid + " (" + n.rssi + "dBm)"))
found = true;
function init() {
/*IF_ETHERNET*/
getAjax("/api/setup", ((o) => c.append(step1(o.eth))));
/*ENDIF_ETHERNET*/
function nets(obj) {
clearInterval(v)
v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 4000)
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(wifi())
c.append(step1Wifi())
getAjax('/api/setup/networks', nets)
}
v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 2500);
</script>
</body>
</html>

24
src/web/lang.h

@ -72,4 +72,28 @@
#define BTN_REBOOT "Reboot"
#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__*/

312
src/web/lang.json

@ -6,7 +6,7 @@
{
"token": "NAV_WIZARD",
"en": "Setup Wizard",
"de": "Daten"
"de": "Einrichtungsassitent"
},
{
"token": "NAV_LIVE",
@ -81,17 +81,22 @@
{
"token": "BTN_NEXT",
"en": "next >>",
"de": "prüfen >>"
"de": "speichern >>"
},
{
"token": "BTN_REBOOT",
"en": "reboot >>",
"de": "Ahoy neustarten >>"
},
{
"token": "TEST_CONNECTION",
"en": "Test Connection",
"de": "Verbindung wird überprüft"
"de": "Verbindung wird &uuml;berpr&uuml;ft"
},
{
"token": "TRY_TO_CONNECT",
"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",
@ -101,7 +106,7 @@
{
"token": "NETWORK_SUCCESS",
"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",
@ -112,6 +117,36 @@
"token": "NUM_NETWORKS_FOUND",
"en": "Network(s) found",
"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",
"en": "print inverter data",
"de": "Livedaten ausgeben"
"de": "Inverterwerte ausgeben"
},
{
"token": "LOG_SERIAL_DEBUG",
@ -224,19 +259,9 @@
"de": "Netzwerke suchen"
},
{
"token": "BTN_SCAN",
"en": "scan",
"de": "Suche starten"
},
{
"token": "AVAIL_NETWORKS",
"en": "Avail Networks",
"de": "Verf&uuml;gbare Netzwerke"
},
{
"token": "NETWORK_NOT_SCANNED",
"en": "not scanned",
"de": "nicht gesucht"
"token": "SCAN_WIFI",
"en": "scan for WiFi networks",
"de": "nach WiFi Netzwerken suchen"
},
{
"token": "SSID_HIDDEN",
@ -295,23 +320,28 @@
},
{
"token": "INV_RESET_MIDNIGHT",
"en": "Reset values and YieldDay at midnight. ('Pause communication during night' need to be set)",
"de": "Werte und Gesamtertrag um Mitternacht zur&uuml;cksetzen ('Kommunikation w&auml;hrend der Nacht pausieren' muss gesetzt sein)"
"en": "Reset values and YieldDay at midnight",
"de": "Werte und Gesamtertrag um Mitternacht zur&uuml;cksetzen"
},
{
"token": "INV_PAUSE_SUNSET",
"token": "INV_RESET_SUNSET",
"en": "Reset values at sunset",
"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",
"en": "Reset values when inverter status is 'not available'",
"de": "Werte zur&uuml;cksetzen, sobald der Wechselrichter nicht erreichbar ist"
},
{
"token": "INV_RESET_MAX_MIDNIGHT",
"en": "Reset 'max' values at midnight",
"de": "Maximalwerte mitternachts zur&uuml;cksetzen"
"token": "INV_RESET_MAX_VALUES",
"en": "Include reset 'max' values",
"de": "Maximalwerte auch zur&uuml;cksetzen"
},
{
"token": "INV_START_WITHOUT_TIME",
@ -323,11 +353,6 @@
"en": "Read Grid Profile",
"de": "Grid-Profil auslesen"
},
{
"token": "INV_YIELD_EFF",
"en": "Yield Efficiency (default 1.0)",
"de": "Ertragseffizienz (Standard 1.0)"
},
{
"token": "NTP_INTERVAL",
"en": "NTP Interval (in minutes, min. 5 minutes)",
@ -388,11 +413,21 @@
"en": "Password (optional)",
"de": "Passwort (optional)"
},
{
"token": "MQTT_JSON",
"en": "Payload as JSON",
"de": "Ausgabe als JSON"
},
{
"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)",
"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",
"en": "Display Config",
@ -601,7 +636,7 @@
{
"token": "BTN_INV_ADD",
"en": "add Inverter",
"de": "Wechselrichter hinzufuegen"
"de": "Wechselrichter hinzuf\u00FCgen"
},
{
"token": "INV_INPUT",
@ -626,7 +661,7 @@
{
"token": "TAB_INPUTS",
"en": "Inputs",
"de": "Eingaenge"
"de": "Eing&auml;nge"
},
{
"token": "TAB_RADIO",
@ -728,6 +763,11 @@
"en": "CMT2300A radio enable",
"de": "CMT2300A Funkmodul aktivieren"
},
{
"token": "ETH_ENABLE",
"en": "Ethernet enable",
"de": "Ethernet aktivieren"
},
{
"token": "DISP_NONE",
"en": "None",
@ -867,6 +907,151 @@
"token": "COMMUNICATING",
"en": "communicating",
"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",
"de": "automatisch scrollen"
},
{
"token": "BTN_MANUALSCROLL",
"en": "manual scroll",
"de": "manuell scrollen"
},
{
"token": "BTN_COPY",
"en": "copy",
@ -897,6 +1087,21 @@
"token": "UPTIME",
"en": "uptime",
"de": "Laufzeit"
},
{
"token": "DAYS",
"en": "days",
"de": "Tage"
},
{
"token": "COPIED_TO_CLIPBOARD",
"en": "Copied to clipboard",
"de": "in die Zwischenablage kopiert"
},
{
"token": "CLIPBOARD_FAILED",
"en": "Copy failed",
"de": "kopieren fehlgeschlagen"
}
]
},
@ -973,6 +1178,16 @@
"en": "Error",
"de": "Fehler"
},
{
"token": "DAY",
"en": "day",
"de": "Tag"
},
{
"token": "S",
"en": "s",
"de": "e"
},
{
"token": "NTP_UNREACH",
"en": "NTP timeserver unreachable",
@ -986,7 +1201,7 @@
{
"token": "NIGHT_TIME",
"en": "Night time, inverter polling disabled",
"de": "Wechselrichterabfrage deaktivert (Nacht)"
"de": "Wechselrichterabfrage deaktiviert (Nacht)"
},
{
"token": "PAUSED_AT",
@ -1503,35 +1718,30 @@
"en": "Total Power",
"de": "Gesamtleistung"
},
{
"token": "TOTAL_POWER_DAY",
"en": "Total Power Today",
"de": "Gesamtleistung heute"
},
{
"token": "TOTAL_YIELD_PER_DAY",
"en": "Total Yield per day",
"de": "Gesamtertrag pro Tag"
},
{
"token": "MAX_DAY",
"en": "maximum day",
"de": "Tagesmaximum"
},
{
"token": "LAST_VALUE",
"en": "last value",
"de": "letzter Wert"
},
{
"token": "MAXIMUM",
"en": "maximum value",
"de": "Maximalwert"
"en": "Maximum",
"de": "Maximum"
},
{
"token": "UPDATED",
"en": "Updated every",
"de": "aktualisiert alle"
"token": "LAST_VALUE",
"en": "Last value",
"de": "Letzter Wert"
},
{
"token": "SECONDS",
"en": "seconds",
"de": "Sekunden"
"token": "YIELD_DAY",
"en": "Yield day",
"de": "Tagesertrag"
}
]
}

100
src/web/web.h

@ -16,11 +16,7 @@
#include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/helper.h"
#if defined(ETHERNET)
#include "AsyncWebServer_ESP32_W5500.h"
#else /* defined(ETHERNET) */
#include "ESPAsyncWebServer.h"
#endif /* defined(ETHERNET) */
#include "html/h/api_js.h"
#include "html/h/colorBright_css.h"
#include "html/h/colorDark_css.h"
@ -41,14 +37,19 @@
#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>
class Web {
public:
Web(void) : mWeb(80), mEvts("/events") {
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
}
Web(void) : mWeb(80), mEvts("/events") {}
void setup(IApp *app, HMSYSTEM *sys, settings_t *config) {
mApp = app;
@ -56,7 +57,8 @@ class Web {
mConfig = config;
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("/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));
@ -74,6 +76,7 @@ class Web {
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("/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("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1));
@ -105,11 +108,17 @@ class Web {
void tickSecond() {
if (mSerialClientConnnected) {
if(nullptr == mSerialBuf)
return;
if (mSerialBufFill > 0) {
mEvts.send(mSerialBuf, "serial", millis());
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
}
} else if(nullptr != mSerialBuf) {
delete[] mSerialBuf;
mSerialBuf = nullptr;
}
}
@ -153,18 +162,14 @@ class Web {
mUploadFp.write(data, len);
if (final) {
mUploadFp.close();
#if !defined(ETHERNET)
char pwd[PWD_LEN];
strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD
#endif
if (!mApp->readSettings("/tmp.json")) {
mUploadFail = true;
DPRINTLN(DBG_ERROR, F("upload JSON error!"));
} else {
LittleFS.remove("/tmp.json");
#if !defined(ETHERNET)
strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD
#endif
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
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)
return;
if(nullptr == mSerialBuf)
return;
msg.replace("\r\n", "<rn>");
if (mSerialAddTime) {
if ((13 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
@ -260,8 +268,8 @@ class Web {
bool reboot = (!Update.hasError());
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"");
#if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3)
html += F("5");
#if defined(ETHERNET)
html += (mConfig->sys.eth.enabled) ? F("5") : F("20");
#else
html += F("20");
#endif
@ -297,6 +305,10 @@ class Web {
void onConnect(AsyncEventSourceClient *client) {
DPRINTLN(DBG_VERBOSE, "onConnect");
if(nullptr == mSerialBuf) {
mSerialBuf = new char[WEB_SERIAL_BUF_SIZE];
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
}
mSerialClientConnnected = true;
if (client->lastId())
@ -305,7 +317,19 @@ class Web {
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);
}
@ -388,6 +412,7 @@ class Web {
void showNotFound(AsyncWebServerRequest *request) {
checkProtection(request);
//DBGPRINTLN(request->url());
request->redirect("/wizard");
}
@ -411,6 +436,13 @@ class Web {
}
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);
response->addHeader(F("Content-Encoding"), "gzip");
response->addHeader(F("content-type"), "text/html; charset=UTF-8");
@ -428,15 +460,14 @@ class Web {
char buf[20] = {0};
// general
#if !defined(ETHERNET)
if (request->arg("ssid") != "")
request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN);
if (request->arg("pwd") != "{PWD}")
request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN);
mConfig->sys.isHidden = (request->arg("hidd") == "on");
if (request->arg("ap_pwd") != "")
request->arg("ap_pwd").toCharArray(mConfig->sys.apPwd, PWD_LEN);
mConfig->sys.isHidden = (request->arg("hidd") == "on");
#endif /* !defined(ETHERNET) */
if (request->arg("device") != "")
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
mConfig->sys.darkMode = (request->arg("darkMode") == "on");
@ -477,17 +508,22 @@ class Web {
if (request->arg("invInterval") != "")
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.rstValsCommStart = (request->arg("invRstComStart") == "on");
mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on");
mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on");
mConfig->inst.readGrid = (request->arg("rdGrid") == "on");
mConfig->inst.rstMaxValsMidNight = (request->arg("invRstMaxMid") == "on");
mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat();
mConfig->inst.rstIncludeMaxVals = (request->arg("invRstMaxMid") == "on");
// 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();
switch(i) {
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 14: mConfig->cmt.pinFcsb = 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->cmt.enabled = (request->arg("cmtEnable") == "on");
#if defined(ETHERNET)
mConfig->sys.eth.enabled = (request->arg("ethEn") == "on");
#endif
// ntp
if (request->arg("ntpAddr") != "") {
@ -544,8 +592,10 @@ class Web {
if (request->arg("mqttPwd") != "{PWD}")
request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_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.interval = request->arg("mqttInterval").toInt();
mConfig->mqtt.enableRetain = (request->arg("retain") == "on");
// serial console
mConfig->serial.debug = (request->arg("serDbg") == "on");
@ -662,7 +712,7 @@ class Web {
{ "max_power", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} },
{ "radio_rx_success", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} },
{ "radio_rx_fail", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} },
{ "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} },
{ "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnswer;} },
{ "radio_frame_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} },
{ "radio_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} },
{ "radio_retransmits", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} },
@ -911,7 +961,7 @@ class Web {
settings_t *mConfig = nullptr;
bool mSerialAddTime = true;
char mSerialBuf[WEB_SERIAL_BUF_SIZE];
char *mSerialBuf = nullptr;
uint16_t mSerialBufFill = 0;
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