Browse Source

Merge branch 'development03' into main

pull/352/head
lumapu 2 years ago
committed by GitHub
parent
commit
5fae7fa9e8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 5
      tools/esp8266/CircularBuffer.h
  3. 136
      tools/esp8266/README.md
  4. 8
      tools/esp8266/User_Manual.md
  5. 2
      tools/esp8266/ahoywifi.cpp
  6. 7
      tools/esp8266/ahoywifi.h
  7. 382
      tools/esp8266/app.cpp
  8. 41
      tools/esp8266/app.h
  9. 13
      tools/esp8266/config.h
  10. 3
      tools/esp8266/config_override_example.h
  11. 7
      tools/esp8266/crc.cpp
  12. 4
      tools/esp8266/crc.h
  13. 3
      tools/esp8266/dbg.cpp
  14. 38
      tools/esp8266/defines.h
  15. 194
      tools/esp8266/favicon.h
  16. 41
      tools/esp8266/hmDefines.h
  17. 569
      tools/esp8266/hmInverter.h
  18. 79
      tools/esp8266/hmRadio.h
  19. 10
      tools/esp8266/hmSystem.h
  20. 79
      tools/esp8266/html/api.js
  21. 47
      tools/esp8266/html/convert.py
  22. 100
      tools/esp8266/html/h/favicon_ico_gz.h
  23. 139
      tools/esp8266/html/index.html
  24. 188
      tools/esp8266/html/serial.html
  25. 280
      tools/esp8266/html/setup.html
  26. 28
      tools/esp8266/html/style.css
  27. 33
      tools/esp8266/html/update.html
  28. 129
      tools/esp8266/html/visualization.html
  29. 40
      tools/esp8266/include/dbg.h
  30. 31
      tools/esp8266/platformio.ini
  31. 693
      tools/esp8266/web.cpp
  32. 75
      tools/esp8266/web.h
  33. 415
      tools/esp8266/webApi.cpp
  34. 62
      tools/esp8266/webApi.h

1
.gitignore

@ -21,3 +21,4 @@ tools/esp8266/.vscode/extensions.json
.DS_Store .DS_Store
.vscode .vscode
tools/esp8266/platformio-device-monitor-*.log tools/esp8266/platformio-device-monitor-*.log
tools/esp8266/html/h/*

5
tools/esp8266/CircularBuffer.h

@ -21,10 +21,7 @@
#ifndef CircularBuffer_h #ifndef CircularBuffer_h
#define CircularBuffer_h #define CircularBuffer_h
#ifdef ESP8266 #if defined(ESP8266) || defined(ESP32)
#define DISABLE_IRQ noInterrupts()
#define RESTORE_IRQ interrupts()
#elif defined(ESP32)
#define DISABLE_IRQ noInterrupts() #define DISABLE_IRQ noInterrupts()
#define RESTORE_IRQ interrupts() #define RESTORE_IRQ interrupts()
#else #else

136
tools/esp8266/README.md

@ -1,23 +1,30 @@
## Table of Contents ## Table of Contents
- [Table of Contents](#table-of-contents)
- [Overview](#overview) - [Overview](#overview)
- [Compatiblity](#compatiblity) - [Compatiblity](#compatiblity)
- [Things needed](#things-needed) - [Things needed](#things-needed)
+ [Faked Modules Warning](#there-are-fake-nrf24l01-modules-out-there) - [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
- [Wiring things up](#wiring-things-up) - [Wiring things up](#wiring-things-up)
+ [ESP8266 wiring example](#esp8266-wiring-example) - [ESP8266 wiring example](#esp8266-wiring-example)
- [Schematic](#schematic)
- [Symbolic view](#symbolic-view)
- [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1)
- [ESP32 GPIO settings](#esp32-gpio-settings)
- [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware) - [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware)
+ [Compiling your own Version (the geek way)](#compiling-your-own-version) - [Compiling your own Version](#compiling-your-own-version)
- [Optional Configuration before compilation](#optional-configuration-before-compilation) - [Optional Configuration before compilation](#optional-configuration-before-compilation)
+ [Using a ready-to-flash binary using nodemcu-pyflasher (the easy way)](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher) - [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher)
- [Connect to your Ahoy DTU](#connect-to-your-ahoy-dtu) - [Connect to your Ahoy DTU](#connect-to-your-ahoy-dtu)
+ [Your Ahoy DTO is very verbose using the Serial Console](#your-ahoy-dto-is-very-verbose-using-the-serial-console) - [Your Ahoy DTU is very verbose using the Serial Console](#your-ahoy-dtu-is-very-verbose-using-the-serial-console)
+ [Connect to the Ahoy DTU Webinterface using your Browser](#connect-to-the-ahoy-dtu-webinterface-using-your-browser) - [Connect to the Ahoy DTU Webinterface using your Browser](#connect-to-the-ahoy-dtu-webinterface-using-your-browser)
- [HTTP based Pages](#http-based-pages) - [HTTP based Pages](#http-based-pages)
- [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface) - [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface)
- [Used Libraries](#used-libraries) - [Used Libraries](#used-libraries)
- [Contact](#contact) - [Contact](#contact)
- [ToDo's - remove when done](#todo) - [ToDo](#todo)
*** ***
@ -29,9 +36,11 @@ Further information will help you to communicate to the compatible inverters.
You find the full [User_Manual here](User_Manual.md) You find the full [User_Manual here](User_Manual.md)
## Compatiblity ## Compatiblity
For now the following Inverters should work out of the box: For now the following Inverters should work out of the box:
Hoymiles Inverters Hoymiles Inverters
- HM300 - HM300
- HM350 - HM350
- HM400 - HM400
@ -43,26 +52,46 @@ Hoymiles Inverters
- HM1500 - HM1500
TSun Inverters: TSun Inverters:
- TSOL-350 - TSOL-350
- TSOL-400 - TSOL-400
- othery may work as well (need to be veryfied). - others may work as well (need to be verified).
## Things needed ## Things needed
In order to build your own Ahoy DTU, you will need some things.<br/> In order to build your own Ahoy DTU, you will need some things.<br/>
This list is not closing as the Maker Community offers more Boards than we could cover in this Readme.<br/><br/> This list is not closing as the Maker Community offers more Boards than we could cover in this Readme.<br/><br/>
We suggest to use a WEMOS D1 mini Board as well as a NRF24L01+ Breakout Board.<br/> We suggest to use a WEMOS D1 mini Board as well as a NRF24L01+ Breakout Board as a bare minimum.<br/>
Make sure it has the "+" in its name as we depend on some features provided with the plus-variant.<br/> Any other ESP8266 Board with at least 4MBytes of ROM could work as well, depending on your skills and goals.<br/>
Any other ESP8266 Board with at least 4MBytes of ROM could work as well, depending on your skills. Make sure the NRF24L01+ module has the "+" in its name as we depend on the 250kbps features provided only with the plus-variant.
| **Parts** | **Price** |
| --- | --- |
| D1 ESP8266 Mini WLAN Board Mikrokontroller | 4,40 Euro |
| NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul | 3,45 Euro |
| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
| **Total costs** | **10,34 Euro** |
To also run our sister project OpenDTU and be upwards compatible for the future we would recommend to spend some more money on an ESP32 board which has two CPU cores and a NRF24L01+ module with external antenna.
| **Parts** | **Price** |
| --- | --- |
| ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 Euro |
| NRF24L01+ PA LNA SMA mit Antenne Long | 4,50 Euro |
| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
| **Total costs** | **14,89 Euro** |
#### There are fake NRF24L01+ Modules out there #### There are fake NRF24L01+ Modules out there
Whatch out, there are some fake NRF24L01+ Modules out there that seem to use rebranded NRF24L01 Chips (without the +).<br/>
An example can be found in [Issue #230](https://github.com/lumapu/ahoy/issues/230).<br/> Watch out, there are some fake NRF24L01+ Modules out there that seem to use rebranded NRF24L01 Chips (without the +).<br/>
You are welcome to add more examples of faked chips. We will that information here.<br/> An example can be found in [Issue #230](https://github.com/grindylow/ahoy/issues/230).<br/>
You are welcome to add more examples of faked chips. We will add that information here.<br/>
## Wiring things up ## Wiring things up
The NRF24L01+ radio module is connected to the standard SPI pins: The NRF24L01+ radio module is connected to the standard SPI pins:
- SCLK (Signal Clock), - SCLK (Signal Clock),
- MISO (Master In Slave Out) and - MISO (Master In Slave Out) and
- MOSI (Master Out Slave In) - MOSI (Master Out Slave In)
@ -70,6 +99,7 @@ The NRF24L01+ radio module is connected to the standard SPI pins:
*These pins need to be configured in the config.h.* *These pins need to be configured in the config.h.*
Additional, there are 3 pins, which can be set individual: Additional, there are 3 pins, which can be set individual:
- CS (Chip Select), - CS (Chip Select),
- CE (Chip Enable) and - CE (Chip Enable) and
- IRQ (Interrupt) - IRQ (Interrupt)
@ -77,23 +107,31 @@ Additional, there are 3 pins, which can be set individual:
*These pins can be changed from the /setup URL.* *These pins can be changed from the /setup URL.*
#### ESP8266 wiring example #### ESP8266 wiring example
This is an example wiring using a Wemos D1 mini.<br> This is an example wiring using a Wemos D1 mini.<br>
##### Schematic ##### Schematic
![Schematic](../../doc/AhoyWemos_Schaltplan.jpg) ![Schematic](../../doc/AhoyWemos_Schaltplan.jpg)
##### Symbolic view ##### Symbolic view
![Symbolic](../../doc/AhoyWemos_Steckplatine.jpg) ![Symbolic](../../doc/AhoyWemos_Steckplatine.jpg)
#### ESP32 wiring example #### ESP32 wiring example
Example wiring for a 38pin ESP32 module Example wiring for a 38pin ESP32 module
##### Schematic ##### Schematic
![Schematic](../../doc/Wiring_ESP32_Schematic.png) ![Schematic](../../doc/Wiring_ESP32_Schematic.png)
##### Symbolic view ##### Symbolic view
![Symbolic](../../doc/Wiring_ESP32_Symbol.png) ![Symbolic](../../doc/Wiring_ESP32_Symbol.png)
##### ESP32 GPIO settings ##### ESP32 GPIO settings
For this wiring, set the 3 individual GPIOs under the /setup URL: For this wiring, set the 3 individual GPIOs under the /setup URL:
``` ```
@ -103,11 +141,12 @@ IRQ D0 (GPIO16 - no IRQ!)
``` ```
## Flash the Firmware on your Ahoy DTU Hardware ## Flash the Firmware on your Ahoy DTU Hardware
Once your Hardware is ready to run, you need to flash the Ahoy DTU Firmware to your Board.
You can either build your own using your own configuration or use one or our pre-compiled generic builds.
Once your Hardware is ready to run, you need to flash the Ahoy DTU Firmware to your Board.
You can either build your own using your own configuration or use one of our pre-compiled generic builds.
#### Compiling your own Version #### Compiling your own Version
This information suits you if you want to configure and build your own firmware. This information suits you if you want to configure and build your own firmware.
This code comes to you as a **PlatformIO** project and can be compiled using the **PlatformIO** Addon.<br/> This code comes to you as a **PlatformIO** project and can be compiled using the **PlatformIO** Addon.<br/>
@ -118,13 +157,13 @@ If you do not want to compile your own build, you can use one of our ready-to-fl
- number of supported inverters (set to 3 by default) `config.h` - number of supported inverters (set to 3 by default) `config.h`
- DTU radio id `config.h` (default = 1234567801) - DTU radio id `config.h` (default = 1234567801)
- unformated list in webbrowser `/livedata` `config.h`, `LIVEDATA_VISUALIZED` - unformatted list in webbrowser `/livedata` `config.h`, `LIVEDATA_VISUALIZED`
Alternativly, instead of modifying `config.h`, `config_override_example.h` can be copied to `config_override.h` and customized. Alternativly, instead of modifying `config.h`, `config_override_example.h` can be copied to `config_override.h` and customized.
config_override.h is excluded from version control and stays local. config_override.h is excluded from version control and stays local.
#### Using a ready-to-flash binary using nodemcu-pyflasher #### Using a ready-to-flash binary using nodemcu-pyflasher
This information suits you if you just want to use an easy way. This information suits you if you just want to use an easy way.
1. download the flash-tool [nodemcu-pyflasher](https://github.com/marcelstoer/nodemcu-pyflasher) 1. download the flash-tool [nodemcu-pyflasher](https://github.com/marcelstoer/nodemcu-pyflasher)
@ -135,58 +174,53 @@ This information suits you if you just want to use an easy way.
6. flash the ESP with the compiled firmware using the UART pins or 6. flash the ESP with the compiled firmware using the UART pins or
7. repower the ESP 7. repower the ESP
8. the ESP will start as access point (AP) if there is no network config stored in its eeprom 8. the ESP will start as access point (AP) if there is no network config stored in its eeprom
9. connect to the AP, you will be forwarded to the setup page 9. connect to the AP (password: `esp_8266`), you will be forwarded to the setup page
10. configure your WiFi settings, save, repower 10. configure your WiFi settings, save, repower
11. check your router or serial console for the IP address of the module. You can try ping the configured device name as well. 11. check your router or serial console for the IP address of the module. You can try ping the configured device name as well.
Once your Ahoy DTU is running, you can use the Over The Air (OTA) capabilities to update your firmware. Once your Ahoy DTU is running, you can use the Over The Air (OTA) capabilities to update your firmware.
! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data! ! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data!
## Connect to your Ahoy DTU ## Connect to your Ahoy DTU
When everything is wired up and the firmware is flashed, it is time to connect to your Ahoy DTU.
When everything is wired up and the firmware is flashed, it is time to connect to your Ahoy DTU.
#### Your Ahoy DTU is very verbose using the Serial Console #### Your Ahoy DTU is very verbose using the Serial Console
When connected to your computer, you can open a Serial Console to obtain additional information.<br/> When connected to your computer, you can open a Serial Console to obtain additional information.<br/>
This might be useful in case of any troubles that might occur as well as to simply<br/> This might be useful in case of any troubles that might occur as well as to simply<br/>
obtain information about the converted values which were read out of the inverter(s). obtain information about the converted values which were read out of the inverter(s).
#### Connect to the Ahoy DTU Webinterface using your Browser #### Connect to the Ahoy DTU Webinterface using your Browser
After you have sucessfully flashed and powered your Ahoy DTU, you can access it via your Browser.<br/> After you have sucessfully flashed and powered your Ahoy DTU, you can access it via your Browser.<br/>
If your Ahoy DTU was able to log into the configured WiFi Network, it will try to obtain an IP-Address<br/> If your Ahoy DTU was able to log into the configured WiFi Network, it will try to obtain an IP-Address<br/>
from your local DHCP Server (in most cases thats your Router).<br/><br/> from your local DHCP Server (in most cases thats your Router).<br/><br/>
In case it could not connect to your configured Network, it will provide its own WiFi Network that you can<br/> In case it could not connect to your configured Network, it will provide its own WiFi Network that you can<br/>
connect to for furter configuration.<br/> connect to for furter configuration.<br/>
The WiFi SSID *(the WiFi Name)* and Passwort is configured in the config.h and defaults to the SSID "AHOY-DTU" with the Passwort "esp_8266".<br/> The WiFi SSID *(the WiFi Name)* and Passwort is configured in the config.h and defaults to the SSID "`AHOY-DTU`" with the Passwort "`esp_8266`".<br/>
The Ahoy DTU will keep that Network open for a certain amount of time (also configurable in the config.h and defaults to 60secs).<br/> The Ahoy DTU will keep that Network open for a certain amount of time (also configurable in the config.h and defaults to 60secs).<br/>
If nothing connects to it and that time runs up, it will retry to connect to the configured network an so on.<br/> If nothing connects to it and that time runs up, it will retry to connect to the configured network an so on.<br/>
<br/> <br/>
If connected to your local Network, you just have to find out the used IP Address. In most cases your Router will give you a hint.<br/> If connected to your local Network, you just have to find out the used IP Address or try the default name [http://ahoy-dtu/](http://ahoy-dtu/). In most cases your Router will give you a hint.<br/>
If you connect to the WiFi the Ahoy DTU opens in case it could not connect to any other Network, the IP-Address of your Ahoy DTU is 192.168.1.1.<br/> If you connect to the WiFi the Ahoy DTU opens in case it could not connect to any other Network, the IP-Address of your Ahoy DTU is [http://192.168.1.1/](http://192.168.1.1/).<br/>
Just open the IP-Address in your browser.<br/> Just open the IP-Address in your browser.<br/>
<br/> <br/>
The webinterface has the following abilities: The webinterface has the following abilities:
- OTA Update (Over The Air Update) - OTA Update (Over The Air Update)
- Configuration (Wifi, inverter(s), NTP Server, Pinout, MQTT, Amplifier Power Level, Debug) - Configuration (Wifi, inverter(s), NTP Server, Pinout, MQTT, Amplifier Power Level, Debug)
- visual display of the connected inverters / modules - visual display of the connected inverters / modules
- some statistics about communication (debug) - some statistics about communication (debug)
##### HTTP based Pages ##### HTTP based Pages
To take control of your Ahoy DTU, you can directly call one of the following sub-pages (e.g. http://192.168.1.1/setup ).<br/>
To take control of your Ahoy DTU, you can directly call one of the following sub-pages (e.g. [http://ahoy-dtu/setup](http://ahoy-dtu/setup) or [http://192.168.1.1/setup](http://192.168.1.1/setup) ).<br/>
| page | use | output | | page | use | output |
| ---- | ------ | ------ | | ---- | ------ | ------ |
| /uptime | displays the uptime uf your Ahoy DTU | 0 Days, 01:37:34; now: 2022-08-21 11:13:53 | | /uptime | displays the uptime of your Ahoy DTU | 0 Days, 01:37:34; now: 2022-08-21 11:13:53 |
| /reboot | reboots the Ahoy DTU | | | /reboot | reboots the Ahoy DTU | |
| /erase | erases the EEPROM | | | /erase | erases the EEPROM | |
| /factory | resets to the factory defaults configured in config.h | | | /factory | resets to the factory defaults configured in config.h | |
@ -198,31 +232,31 @@ When everything is wired up and the firmware is flashed, it is time to connect t
| /json | gets live-data in JSON format | json output from the livedata | | /json | gets live-data in JSON format | json output from the livedata |
| /api | | | | /api | | |
## MQTT command to set the DTU without webinterface ## MQTT command to set the DTU without webinterface
[Read here](User_Manual.md)
[Read here](tools/esp8266/User_Manual.md)
## Used Libraries ## Used Libraries
- `ESP8266WiFi` 1.0 | Name | version | License |
- `DNSServer` 1.1.0 | --------------------- | ------- | -------- |
- `Ticker` 1.0 | `ESP8266WiFi` | 1.0 | LGPL-2.1 |
- `ESP8266HTTPUpdateServer` 1.0 | `DNSServer` | 1.1.1 | LGPL-2.1 |
- `Time` 1.6.1 | `SPI` | 1.0 | LGPL-2.1 |
- `RF24` 1.4.5 | `Hash` | 1.0 | LGPL-2.1 |
- `PubSubClient` 2.8 | `EEPROM` | 1.0 | LGPL-2.1 |
- `ArduinoJson` 6.19.4 | `ESP Async WebServer` | 1.2.3 | LGPL-3.0 |
| `ESPAsyncTCP` | 1.2.2 | LGPL-3.0 |
| `Time` | 1.6.1 | LGPL-2.1 |
| `RF24` | 1.4.5 | GPL-2.0 |
| `PubSubClient` | 2.8 | MIT |
| `ArduinoJson` | 6.19.4 | MIT |
## Contact ## Contact
We run a Discord Server that can be used to get in touch with the Developers and Users.
https://discord.gg/WzhxEY62mB
We run a Discord Server that can be used to get in touch with the Developers and Users.
<https://discord.gg/WzhxEY62mB>
## ToDo ## ToDo

8
tools/esp8266/User_Manual.md

@ -18,11 +18,11 @@ The ahoy dtu will publish on the following topics
|U_AC | 233.300|actual AC Voltage in Volt| |U_AC | 233.300|actual AC Voltage in Volt|
|I_AC | 0.300 | actual AC Current in Ampere| |I_AC | 0.300 | actual AC Current in Ampere|
|P_AC | 71.000| actual AC Power in Watt| |P_AC | 71.000| actual AC Power in Watt|
|P_ACr | 21.200| actual AC reactive power in VAr| |Q_AC | 21.200| actual AC reactive power in var|
|Freq | 49.990|actual AC Frequency in 1/s| |F_AC | 49.990| actual AC Frequency in Hz|
|Pct | 95.800|actual AC Power factor in %| |PF_AC | 95.800| actual AC Power factor|
|Temp | 19.800|Temperature of inverter in Celsius| |Temp | 19.800|Temperature of inverter in Celsius|
|LARM_MES_ID | 9.000|Last Alarm Message Id| |EVT | 9.000|Last Event/Alarm Message Index|
|YieldDay | 51.000|Energy converted to AC per day in Watt hours (measured on DC)| |YieldDay | 51.000|Energy converted to AC per day in Watt hours (measured on DC)|
|YieldTotal | 465.294|Energy converted to AC since reset Watt hours (measured on DC)| |YieldTotal | 465.294|Energy converted to AC since reset Watt hours (measured on DC)|
|P_DC | 74.600|actual DC Power in Watt| |P_DC | 74.600|actual DC Power in Watt|

2
tools/esp8266/ahoywifi.cpp

@ -54,7 +54,7 @@ void ahoywifi::setup(uint32_t timeout, bool settingValid) {
if(mApActive) if(mApActive)
DBGPRINTLN(F("192.168.1.1")); DBGPRINTLN(F("192.168.1.1"));
else else
DBGPRINTLN(WiFi.localIP()); DBGPRINTLN(WiFi.localIP().toString());
DPRINTLN(DBG_INFO, F("to configure your device")); DPRINTLN(DBG_INFO, F("to configure your device"));
DPRINTLN(DBG_INFO, F("----------------------------------------\n")); DPRINTLN(DBG_INFO, F("----------------------------------------\n"));
} }

7
tools/esp8266/ahoywifi.h

@ -7,13 +7,6 @@
#define __AHOYWIFI_H__ #define __AHOYWIFI_H__
#include "dbg.h" #include "dbg.h"
#ifdef ESP8266
#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WebServer.h>
#include <WiFi.h>
#endif
// NTP // NTP
#include <WiFiUdp.h> #include <WiFiUdp.h>

382
tools/esp8266/app.cpp

@ -23,6 +23,7 @@ app::app() {
loadDefaultConfig(); loadDefaultConfig();
mSys = new HmSystemType(); mSys = new HmSystemType();
mShouldReboot = false;
} }
@ -41,7 +42,7 @@ void app::setup(uint32_t timeout) {
#endif #endif
mSys->setup(&mConfig); mSys->setup(&mConfig);
mWebInst = new web(this, &mSysConfig, &mConfig, mVersion); mWebInst = new web(this, &mSysConfig, &mConfig, &mStat, mVersion);
mWebInst->setup(); mWebInst->setup();
} }
@ -60,10 +61,19 @@ void app::loop(void) {
} }
if(checkTicker(&mNtpRefreshTicker, mNtpRefreshInterval)) { if(checkTicker(&mNtpRefreshTicker, mNtpRefreshInterval)) {
if(!apActive) { if(!apActive)
mUpdateNtp = true;
}
if(mUpdateNtp) {
mUpdateNtp = false;
mTimestamp = mWifi->getNtpTime(); mTimestamp = mWifi->getNtpTime();
DPRINTLN(DBG_INFO, "[NTP]: " + getDateTimeStr(mTimestamp)); DPRINTLN(DBG_INFO, "[NTP]: " + getDateTimeStr(mTimestamp));
} }
if(mShouldReboot) {
DPRINTLN(DBG_INFO, F("Rebooting..."));
ESP.restart();
} }
@ -84,30 +94,26 @@ void app::loop(void) {
DPRINT(DBG_INFO, "RX " + String(len) + "B Ch" + String(p->rxCh) + " | "); DPRINT(DBG_INFO, "RX " + String(len) + "B Ch" + String(p->rxCh) + " | ");
mSys->Radio.dumpBuf(NULL, p->packet, len); mSys->Radio.dumpBuf(NULL, p->packet, len);
} }
mFrameCnt++; mStat.frmCnt++;
if(0 != len) { if(0 != len) {
Inverter<> *iv = mSys->findInverter(&p->packet[1]); Inverter<> *iv = mSys->findInverter(&p->packet[1]);
if(NULL != iv && p->packet[0] == (TX_REQ_INFO + 0x80)) { // response from get information command if((NULL != iv) && (p->packet[0] == (TX_REQ_INFO + 0x80))) { // response from get information command
mPayload[iv->id].txId = p->packet[0]; mPayload[iv->id].txId = p->packet[0];
DPRINTLN(DBG_DEBUG, F("Response from info request received")); DPRINTLN(DBG_DEBUG, F("Response from info request received"));
uint8_t *pid = &p->packet[9]; uint8_t *pid = &p->packet[9];
if (*pid == 0x00) if (*pid == 0x00)
{
DPRINT(DBG_DEBUG, "fragment number zero received and ignored"); DPRINT(DBG_DEBUG, "fragment number zero received and ignored");
} else {
else DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX));
{ if ((*pid & 0x7F) < 5) {
if ((*pid & 0x7F) < 5)
{
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], len - 11); memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], len - 11);
mPayload[iv->id].len[(*pid & 0x7F) - 1] = len - 11; mPayload[iv->id].len[(*pid & 0x7F) - 1] = len - 11;
} }
if ((*pid & 0x80) == 0x80) if ((*pid & 0x80) == 0x80) {
{ // Last packet // Last packet
if ((*pid & 0x7f) > mPayload[iv->id].maxPackId) if ((*pid & 0x7f) > mPayload[iv->id].maxPackId) {
{
mPayload[iv->id].maxPackId = (*pid & 0x7f); mPayload[iv->id].maxPackId = (*pid & 0x7f);
if (*pid > 0x81) if (*pid > 0x81)
mLastPacketId = *pid; mLastPacketId = *pid;
@ -115,35 +121,17 @@ void app::loop(void) {
} }
} }
} }
if(NULL != iv && p->packet[0] == (TX_REQ_DEVCONTROL + 0x80)) { // response from dev control command if((NULL != iv) && (p->packet[0] == (TX_REQ_DEVCONTROL + 0x80))) { // response from dev control command
mPayload[iv->id].txId = p->packet[0]; mPayload[iv->id].txId = p->packet[0];
DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received"));
iv->devControlRequest = false; iv->devControlRequest = false;
switch (p->packet[12]) { if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) {
case ActivePowerContr: if (p->packet[10] == 0x00 && p->packet[11] == 0x00)
if (iv->devControlCmd >= ActivePowerContr && iv->devControlCmd <= PFSet) { // ok inverter accepted the set point copy it to dtu eeprom
if ((iv->powerLimit[1] & 0xff00) > 0) { // User want to have it persistent
mEep->write(ADDR_INV_PWR_LIM + iv->id * 2, iv->powerLimit[0]);
mEep->write(ADDR_INV_PWR_LIM_CON + iv->id * 2, iv->powerLimit[1]);
updateCrc();
mEep->commit();
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1]) + F(", written to dtu eeprom"));
} else
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1])); DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1]));
iv->devControlCmd = Init; else
} DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has NOT accepted power limit set point") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1]));
break;
default:
if (iv->devControlCmd == ActivePowerContr) {
//case inverter did not accept the sent limit; set back to last stored limit
mEep->read(ADDR_INV_PWR_LIM + iv->id * 2, (uint16_t *)&(iv->powerLimit[0]));
mEep->read(ADDR_INV_PWR_LIM_CON + iv->id * 2, (uint16_t *)&(iv->powerLimit[1]));
DPRINTLN(DBG_INFO, F("Inverter has not accepted power limit set point"));
} }
iv->devControlCmd = Init; iv->devControlCmd = Init;
break;
}
} }
} }
} }
@ -186,12 +174,13 @@ void app::loop(void) {
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) { for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id); Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) { if(NULL != iv) {
if(iv->isAvailable(mTimestamp)) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if(iv->isAvailable(mTimestamp, rec)) {
DPRINTLN(DBG_INFO, "Inverter: " + String(id)); DPRINTLN(DBG_INFO, "Inverter: " + String(id));
for(uint8_t i = 0; i < iv->listLen; i++) { for(uint8_t i = 0; i < rec->length; i++) {
if(0.0f != iv->getValue(i)) { if(0.0f != iv->getValue(i, rec)) {
snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, iv->getFieldName(i)); snprintf(topic, 30, "%s/ch%d/%s", iv->name, rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(val, 10, "%.3f %s", iv->getValue(i), iv->getUnit(i)); snprintf(val, 10, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec));
DPRINTLN(DBG_INFO, String(topic) + ": " + String(val)); DPRINTLN(DBG_INFO, String(topic) + ": " + String(val));
} }
yield(); yield();
@ -218,8 +207,8 @@ void app::loop(void) {
int8_t maxLoop = MAX_NUM_INVERTERS; int8_t maxLoop = MAX_NUM_INVERTERS;
Inverter<> *iv = mSys->getInverterByPos(mSendLastIvId); Inverter<> *iv = mSys->getInverterByPos(mSendLastIvId);
do { do {
if(NULL != iv) //if(NULL != iv)
mPayload[iv->id].requested = false; // mPayload[iv->id].requested = false;
mSendLastIvId = ((MAX_NUM_INVERTERS-1) == mSendLastIvId) ? 0 : mSendLastIvId + 1; mSendLastIvId = ((MAX_NUM_INVERTERS-1) == mSendLastIvId) ? 0 : mSendLastIvId + 1;
iv = mSys->getInverterByPos(mSendLastIvId); iv = mSys->getInverterByPos(mSendLastIvId);
} while((NULL == iv) && ((maxLoop--) > 0)); } while((NULL == iv) && ((maxLoop--) > 0));
@ -229,11 +218,14 @@ void app::loop(void) {
processPayload(false); processPayload(false);
if(!mPayload[iv->id].complete) { if(!mPayload[iv->id].complete) {
mRxFailed++; if(0 == mPayload[iv->id].maxPackId)
mStat.rxFailNoAnser++;
else
mStat.rxFail++;
iv->setQueuedCmdFinished(); // command failed iv->setQueuedCmdFinished(); // command failed
if(mConfig.serialDebug) { if(mConfig.serialDebug)
DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout")); DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
}
if(mConfig.serialDebug) { if(mConfig.serialDebug) {
DPRINT(DBG_INFO, F("Inverter #") + String(iv->id) + " "); DPRINT(DBG_INFO, F("Inverter #") + String(iv->id) + " ");
DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload[iv->id].retransmits) + ")"); DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload[iv->id].retransmits) + ")");
@ -241,19 +233,25 @@ void app::loop(void) {
} }
resetPayload(iv); resetPayload(iv);
mPayload[iv->id].requested = true;
yield(); yield();
if(mConfig.serialDebug) if(mConfig.serialDebug) {
DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status())); DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status()));
DPRINTLN(DBG_INFO, F("Requesting Inverter SN ") + String(iv->serial.u64, HEX)); DPRINTLN(DBG_INFO, F("Requesting Inverter SN ") + String(iv->serial.u64, HEX));
if(iv->devControlRequest && (iv->powerLimit[0] > 0) && (NoPowerLimit != iv->powerLimit[1])) { // prevent to "switch off" }
if(iv->devControlRequest) {
if(mConfig.serialDebug) if(mConfig.serialDebug)
DPRINTLN(DBG_INFO, F("Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0])); DPRINTLN(DBG_INFO, F("Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0]));
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit); mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit);
mPayload[iv->id].txCmd = iv->devControlCmd;
iv->clearCmdQueue(); iv->clearCmdQueue();
iv->enqueCommand<InfoCommand>(SystemConfigPara); iv->enqueCommand<InfoCommand>(SystemConfigPara);
} else { }
mSys->Radio.sendTimePacket(iv->radioId.u64,iv->getQueuedCmd(), mPayload[iv->id].ts,iv->alarmMesIndex); else {
uint8_t cmd = iv->getQueuedCmd();
mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex);
mPayload[iv->id].txCmd = cmd;
mRxTicker = 0; mRxTicker = 0;
} }
} }
@ -283,12 +281,12 @@ bool app::buildPayload(uint8_t id) {
for(uint8_t i = 0; i < mPayload[id].maxPackId; i ++) { for(uint8_t i = 0; i < mPayload[id].maxPackId; i ++) {
if(mPayload[id].len[i] > 0) { if(mPayload[id].len[i] > 0) {
if(i == (mPayload[id].maxPackId-1)) { if(i == (mPayload[id].maxPackId-1)) {
crc = Hoymiles::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc); crc = Ahoy::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc);
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8)
| (mPayload[id].data[i][mPayload[id].len[i] - 1]); | (mPayload[id].data[i][mPayload[id].len[i] - 1]);
} }
else else
crc = Hoymiles::crc16(mPayload[id].data[i], mPayload[id].len[i], crc); crc = Ahoy::crc16(mPayload[id].data[i], mPayload[id].len[i], crc);
} }
yield(); yield();
} }
@ -305,25 +303,32 @@ void app::processPayload(bool retransmit) {
boolean doMQTT = false; boolean doMQTT = false;
#endif #endif
DPRINTLN(DBG_VERBOSE, F("app::processPayload")); //DPRINTLN(DBG_INFO, F("processPayload"));
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) { for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id); Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) { if(NULL != iv) {
if(mPayload[iv->id].txId != (TX_REQ_INFO + 0x80)) { if((mPayload[iv->id].txId != (TX_REQ_INFO + 0x80)) && (0 != mPayload[iv->id].txId)) {
// no processing needed if txId is not 0x95 // no processing needed if txId is not 0x95
//DPRINTLN(DBG_INFO, F("processPayload - set complete, txId: ") + String(mPayload[iv->id].txId, HEX));
mPayload[iv->id].complete = true; mPayload[iv->id].complete = true;
} }
if(!mPayload[iv->id].complete ) { if(!mPayload[iv->id].complete ) {
if(!buildPayload(iv->id)) { if(!buildPayload(iv->id)) { // payload not complete
if(mPayload[iv->id].requested) { if(mPayload[iv->id].requested) {
if(retransmit) { if(retransmit) {
if(iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm ) {
// This is required to prevent retransmissions without answer.
DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm..."));
mPayload[iv->id].retransmits = mConfig.maxRetransPerPyld;
} else {
if(mPayload[iv->id].retransmits < mConfig.maxRetransPerPyld) { if(mPayload[iv->id].retransmits < mConfig.maxRetransPerPyld) {
mPayload[iv->id].retransmits++; mPayload[iv->id].retransmits++;
if(mPayload[iv->id].maxPackId != 0) { if(mPayload[iv->id].maxPackId != 0) {
for(uint8_t i = 0; i < (mPayload[iv->id].maxPackId-1); i++) { for(uint8_t i = 0; i < (mPayload[iv->id].maxPackId-1); i++) {
if(mPayload[iv->id].len[i] == 0) { if(mPayload[iv->id].len[i] == 0) {
if(mConfig.serialDebug) if(mConfig.serialDebug)
DPRINTLN(DBG_ERROR, F("while retrieving data: Frame ") + String(i+1) + F(" missing: Request Retransmit")); DPRINTLN(DBG_WARN, F("while retrieving data: Frame ") + String(i+1) + F(" missing: Request Retransmit"));
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME+i), true); mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME+i), true);
break; // only retransmit one frame per loop break; // only retransmit one frame per loop
} }
@ -332,20 +337,29 @@ void app::processPayload(bool retransmit) {
} }
else { else {
if(mConfig.serialDebug) if(mConfig.serialDebug)
DPRINTLN(DBG_ERROR, F("while retrieving data: last frame missing: Request Retransmit")); DPRINTLN(DBG_WARN, F("while retrieving data: last frame missing: Request Retransmit"));
if(0x00 != mLastPacketId) if(0x00 != mLastPacketId)
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, mLastPacketId, true); mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, mLastPacketId, true);
else else {
mSys->Radio.sendTimePacket(iv->radioId.u64, iv->getQueuedCmd(), mPayload[iv->id].ts,iv->alarmMesIndex); mPayload[iv->id].txCmd = iv->getQueuedCmd();
mSys->Radio.sendTimePacket(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex);
}
} }
mSys->Radio.switchRxCh(100); mSys->Radio.switchRxCh(100);
} }
} }
} }
} }
else { }
else { // payload complete
DPRINTLN(DBG_INFO, F("procPyld: cmd: ") + String(mPayload[iv->id].txCmd));
DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX));
DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId));
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
mPayload[iv->id].complete = true; mPayload[iv->id].complete = true;
iv->ts = mPayload[iv->id].ts; if(mPayload[iv->id].txId == (TX_REQ_INFO + 0x80))
mStat.rxSuccess++;
uint8_t payload[128]; uint8_t payload[128];
uint8_t offs = 0; uint8_t offs = 0;
@ -361,44 +375,109 @@ void app::processPayload(bool retransmit) {
DPRINT(DBG_INFO, F("Payload (") + String(offs) + "): "); DPRINT(DBG_INFO, F("Payload (") + String(offs) + "): ");
mSys->Radio.dumpBuf(NULL, payload, offs); mSys->Radio.dumpBuf(NULL, payload, offs);
} }
mRxSuccess++;
iv->getAssignment(); // choose the parser if(NULL == rec)
for(uint8_t i = 0; i < iv->listLen; i++) { DPRINTLN(DBG_ERROR, F("record is NULL!"));
iv->addValue(i, payload); // cmd value decides which parser is used to decode payload else {
rec->ts = mPayload[iv->id].ts;
for(uint8_t i = 0; i < rec->length; i++) {
iv->addValue(i, payload, rec);
yield(); yield();
} }
iv->doCalculations(); // cmd value decides which parser is used to decode payload iv->doCalculations();
iv->setQueuedCmdFinished();
// MQTT send out // MQTT send out
if(mMqttActive) { if(mMqttActive) {
char topic[30], val[10]; record_t<> *recRealtime = iv->getRecordStruct(RealTimeRunData_Debug);
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) char topic[32 + MAX_NAME_LENGTH], val[32];
{ float total[4];
memset(total, 0, sizeof(float) * 4);
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id); Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) if (NULL != iv) {
{ if (iv->isAvailable(mTimestamp, rec)) {
if (iv->isAvailable(mTimestamp)) for (uint8_t i = 0; i < rec->length; i++) {
{ snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
for (uint8_t i = 0; i < iv->listLen; i++) snprintf(val, 10, "%.3f", iv->getValue(i, rec));
{ mMqtt.sendMsg(topic, val);
snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, fields[iv->assign[i].fieldId]); if(recRealtime == rec) {
snprintf(val, 10, "%.3f", iv->getValue(i)); if(CH0 == rec->assign[i].ch) {
switch(rec->assign[i].fieldId) {
case FLD_PAC: total[0] += iv->getValue(i, rec); break;
case FLD_YT: total[1] += iv->getValue(i, rec); break;
case FLD_YD: total[2] += iv->getValue(i, rec); break;
case FLD_PDC: total[3] += iv->getValue(i, rec); break;
}
}
}
if(iv->isProducing(mTimestamp, rec)){
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->name);
snprintf(val, 32, DEF_MQTT_IV_MESSAGE_INVERTER_AVAIL_AND_PRODUCED);
mMqtt.sendMsg(topic, val);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->name);
snprintf(val, 32, "2");
mMqtt.sendMsg(topic, val);
} else {
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->name);
snprintf(val, 32, DEF_MQTT_IV_MESSAGE_INVERTER_AVAIL_AND_NOT_PRODUCED);
mMqtt.sendMsg(topic, val);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->name);
snprintf(val, 32, "1");
mMqtt.sendMsg(topic, val);
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->name);
snprintf(val, 48, "%i", iv->getLastTs(rec) * 1000);
mMqtt.sendMsg(topic, val); mMqtt.sendMsg(topic, val);
yield(); yield();
} }
} }
} }
} }
// total values (sum of all inverters)
if(recRealtime == rec) {
if(mSys->getNumInverters() > 1) {
uint8_t fieldId = 0;
for (uint8_t i = 0; i < 4; i++) {
switch(i) {
case 0: fieldId = FLD_PAC; break;
case 1: fieldId = FLD_YT; break;
case 2: fieldId = FLD_YD; break;
case 3: fieldId = FLD_PDC; break;
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
snprintf(val, 10, "%.3f", total[i]);
mMqtt.sendMsg(topic, val);
}
}
}
}
} }
iv->setQueuedCmdFinished();
#ifdef __MQTT_AFTER_RX__ #ifdef __MQTT_AFTER_RX__
doMQTT = true; doMQTT = true;
#endif #endif
} }
} }
if(mMqttActive) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
char topic[32 + MAX_NAME_LENGTH], val[32];
if (!iv->isAvailable(mTimestamp, rec) && !iv->isProducing(mTimestamp, rec)){
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->name);
snprintf(val, 32, DEF_MQTT_IV_MESSAGE_NOT_AVAIL_AND_NOT_PRODUCED);
mMqtt.sendMsg(topic, val);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->name);
snprintf(val, 32, "0");
mMqtt.sendMsg(topic, val);
}
}
yield(); yield();
} }
} }
@ -484,6 +563,11 @@ void app::cbMqtt(char* topic, byte* payload, unsigned int length) {
// uint16_t power_factor = std::stoi(strtok(NULL, "/")); // uint16_t power_factor = std::stoi(strtok(NULL, "/"));
DPRINTLN(DBG_INFO, F("Set Power Factor not implemented for inverter ") + String(iv->id) ); DPRINTLN(DBG_INFO, F("Set Power Factor not implemented for inverter ") + String(iv->id) );
break; break;
case CleanState_LockAndAlarm: // CleanState lock & alarm
iv->devControlCmd = CleanState_LockAndAlarm;
DPRINTLN(DBG_INFO, F("CleanState lock & alarm for inverter ") + String(iv->id) );
iv->devControlRequest = true;
break;
default: default:
DPRINTLN(DBG_INFO, "Not implemented"); DPRINTLN(DBG_INFO, "Not implemented");
break; break;
@ -499,82 +583,6 @@ void app::cbMqtt(char* topic, byte* payload, unsigned int length) {
} }
//-----------------------------------------------------------------------------
String app::getStatistics(void) {
String content = F("Receive success: ") + String(mRxSuccess) + "\n";
content += F("Receive fail: ") + String(mRxFailed) + "\n";
content += F("Frames received: ") + String(mFrameCnt) + "\n";
content += F("Send Cnt: ") + String(mSys->Radio.mSendCnt) + String("\n\n");
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = mSys->getInverterByPos(i);
content += F("Inverter #") + String(i) + F(": ");
if(NULL != iv) {
bool avail = true;
content += String(iv->name) + F(" (v") + String(iv->fwVersion) +F(")") + F(" is ");
if(!iv->isAvailable(mTimestamp)) {
content += F("not ");
avail = false;
}
content += F("available and is ");
if(!iv->isProducing(mTimestamp))
content += F("not ");
content += F("producing\n");
if(!avail) {
if(iv->getLastTs() > 0)
content += F("-> last successful transmission: ") + getDateTimeStr(iv->getLastTs()) + "\n";
}
}
else
content += F("n/a\n");
}
if(!mSys->Radio.isChipConnected())
content += F("WARNING! your NRF24 module can't be reached, check the wiring and pinout (<a href=\"/setup\">setup</a>)\n");
if(mShowRebootRequest)
content += F("INFO: reboot your ESP to apply all your configuration changes!\n");
if(!mSettingsValid)
content += F("INFO: your settings are invalid, please switch to <a href=\"/setup\">Setup</a> to correct this.\n");
content += F("MQTT: ");
if(!mMqtt.isConnected())
content += F("not ");
content += F("connected\n");
return content;
}
//-----------------------------------------------------------------------------
String app::getJson(void) {
DPRINTLN(DBG_VERBOSE, F("app::showJson"));
String modJson;
modJson = F("{\n");
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
char topic[40], val[25];
snprintf(topic, 30, "\"%s\": {\n", iv->name);
modJson += String(topic);
for(uint8_t i = 0; i < iv->listLen; i++) {
snprintf(topic, 40, "\t\"ch%d/%s\"", iv->assign[i].ch, iv->getFieldName(i));
snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i), iv->getUnit(i));
modJson += String(topic) + ": " + String(val) + F(",\n");
}
modJson += F("\t\"last_msg\": \"") + getDateTimeStr(iv->ts) + F("\"\n\t},\n");
}
}
modJson += F("\"json_ts\": \"") + String(getDateTimeStr(mTimestamp)) + F("\"\n}\n");
return modJson;
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
bool app::getWifiApActive(void) { bool app::getWifiApActive(void) {
return mWifi->getApActive(); return mWifi->getApActive();
@ -589,7 +597,8 @@ void app::sendMqttDiscoveryConfig(void) {
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) { for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id); Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) { if(NULL != iv) {
if(iv->isAvailable(mTimestamp) && mMqttConfigSendState[id] != true) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if(iv->isAvailable(mTimestamp, rec) && mMqttConfigSendState[id] != true) {
DynamicJsonDocument deviceDoc(128); DynamicJsonDocument deviceDoc(128);
deviceDoc["name"] = iv->name; deviceDoc["name"] = iv->name;
deviceDoc["ids"] = String(iv->serial.u64, HEX); deviceDoc["ids"] = String(iv->serial.u64, HEX);
@ -599,21 +608,21 @@ void app::sendMqttDiscoveryConfig(void) {
JsonObject deviceObj = deviceDoc.as<JsonObject>(); JsonObject deviceObj = deviceDoc.as<JsonObject>();
DynamicJsonDocument doc(384); DynamicJsonDocument doc(384);
for(uint8_t i = 0; i < iv->listLen; i++) { for(uint8_t i = 0; i < rec->length; i++) {
if (iv->assign[i].ch == CH0) { if (rec->assign[i].ch == CH0) {
snprintf(name, 32, "%s %s", iv->name, iv->getFieldName(i)); snprintf(name, 32, "%s %s", iv->name, iv->getFieldName(i, rec));
} else { } else {
snprintf(name, 32, "%s CH%d %s", iv->name, iv->assign[i].ch, iv->getFieldName(i)); snprintf(name, 32, "%s CH%d %s", iv->name, rec->assign[i].ch, iv->getFieldName(i, rec));
} }
snprintf(stateTopic, 64, "%s/%s/ch%d/%s", mConfig.mqtt.topic, iv->name, iv->assign[i].ch, iv->getFieldName(i)); snprintf(stateTopic, 64, "%s/%s/ch%d/%s", mConfig.mqtt.topic, iv->name, rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(discoveryTopic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->name, iv->assign[i].ch, iv->getFieldName(i)); snprintf(discoveryTopic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->name, rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(uniq_id, 32, "ch%d_%s", iv->assign[i].ch, iv->getFieldName(i)); snprintf(uniq_id, 32, "ch%d_%s", rec->assign[i].ch, iv->getFieldName(i, rec));
const char* devCls = getFieldDeviceClass(iv->assign[i].fieldId); const char* devCls = getFieldDeviceClass(rec->assign[i].fieldId);
const char* stateCls = getFieldStateClass(iv->assign[i].fieldId); const char* stateCls = getFieldStateClass(rec->assign[i].fieldId);
doc["name"] = name; doc["name"] = name;
doc["stat_t"] = stateTopic; doc["stat_t"] = stateTopic;
doc["unit_of_meas"] = iv->getUnit(i); doc["unit_of_meas"] = iv->getUnit(i, rec);
doc["uniq_id"] = String(iv->serial.u64, HEX) + "_" + uniq_id; doc["uniq_id"] = String(iv->serial.u64, HEX) + "_" + uniq_id;
doc["dev"] = deviceObj; doc["dev"] = deviceObj;
doc["exp_aft"] = mMqttInterval + 5; // add 5 sec if connection is bad or ESP too slow doc["exp_aft"] = mMqttInterval + 5; // add 5 sec if connection is bad or ESP too slow
@ -664,6 +673,7 @@ const char* app::getFieldStateClass(uint8_t fieldId) {
void app::resetSystem(void) { void app::resetSystem(void) {
mUptimeSecs = 0; mUptimeSecs = 0;
mPrevMillis = 0; mPrevMillis = 0;
mUpdateNtp = false;
mNtpRefreshTicker = 0; mNtpRefreshTicker = 0;
mNtpRefreshInterval = NTP_REFRESH_INTERVAL; // [ms] mNtpRefreshInterval = NTP_REFRESH_INTERVAL; // [ms]
@ -691,9 +701,7 @@ void app::resetSystem(void) {
memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t))); memset(mPayload, 0, (MAX_NUM_INVERTERS * sizeof(invPayload_t)));
mRxFailed = 0; memset(&mStat, 0, sizeof(statistics_t));
mRxSuccess = 0;
mFrameCnt = 0;
mLastPacketId = 0x00; mLastPacketId = 0x00;
} }
@ -761,23 +769,6 @@ void app::loadEEpconfig(void) {
if(0ULL != invSerial) { if(0ULL != invSerial) {
iv = mSys->addInverter(name, invSerial, modPwr); iv = mSys->addInverter(name, invSerial, modPwr);
if(NULL != iv) { // will run once on every dtu boot if(NULL != iv) { // will run once on every dtu boot
mEep->read(ADDR_INV_PWR_LIM + (i * 2),(uint16_t *)&(iv->powerLimit[0]));
mEep->read(ADDR_INV_PWR_LIM_CON + (i * 2),(uint16_t *)&(iv->powerLimit[1]));
// only set it, if it is changed by user. Default value in the html setup page is -1 = 0xffff
// it is "doppelt-gemoppelt" because the inverter shall remember the setting if the dtu makes a power cycle / reboot
if (iv->powerLimit[0] != 0xffff) {
iv->devControlCmd = ActivePowerContr; // set active power limit
DPRINT(DBG_INFO, F("add inverter: ") + String(name) + ", SN: " + String(invSerial, HEX));
if(iv->powerLimit[1] != NoPowerLimit) {
DBGPRINT(F(", Power Limit: ") + String(iv->powerLimit[0]));
if ((iv->powerLimit[1] & 0x0001) == 0x0001)
DBGPRINTLN(F(" in %"));
else
DBGPRINTLN(F(" in Watt"));
}
else
DBGPRINTLN(F(" "));
}
for(uint8_t j = 0; j < 4; j++) { for(uint8_t j = 0; j < 4; j++) {
mEep->read(ADDR_INV_CH_NAME + (i * 4 * MAX_NAME_LENGTH) + j * MAX_NAME_LENGTH, iv->chName[j], MAX_NAME_LENGTH); mEep->read(ADDR_INV_CH_NAME + (i * 4 * MAX_NAME_LENGTH) + j * MAX_NAME_LENGTH, iv->chName[j], MAX_NAME_LENGTH);
} }
@ -787,6 +778,12 @@ void app::loadEEpconfig(void) {
mMqttInterval += mConfig.sendInterval; mMqttInterval += mConfig.sendInterval;
} }
} }
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = mSys->getInverterByPos(i, false);
if(NULL != iv)
resetPayload(iv);
}
} }
} }
@ -801,8 +798,6 @@ void app::saveValues(void) {
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i, false); iv = mSys->getInverterByPos(i, false);
mEep->write(ADDR_INV_ADDR + (i * 8), iv->serial.u64); mEep->write(ADDR_INV_ADDR + (i * 8), iv->serial.u64);
mEep->write(ADDR_INV_PWR_LIM + i * 2, iv->powerLimit[0]);
mEep->write(ADDR_INV_PWR_LIM_CON + i * 2, iv->powerLimit[1]);
mEep->write(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), iv->name, MAX_NAME_LENGTH); mEep->write(ADDR_INV_NAME + (i * MAX_NAME_LENGTH), iv->name, MAX_NAME_LENGTH);
// max channel power / name // max channel power / name
for(uint8_t j = 0; j < 4; j++) { for(uint8_t j = 0; j < 4; j++) {
@ -812,7 +807,6 @@ void app::saveValues(void) {
} }
updateCrc(); updateCrc();
mEep->commit();
} }
@ -857,13 +851,13 @@ void app::setupMqtt(void) {
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::resetPayload(Inverter<>* iv) void app::resetPayload(Inverter<>* iv) {
{ DPRINTLN(DBG_INFO, "resetPayload: id: " + String(iv->id));
// reset payload data
memset(mPayload[iv->id].len, 0, MAX_PAYLOAD_ENTRIES); memset(mPayload[iv->id].len, 0, MAX_PAYLOAD_ENTRIES);
mPayload[iv->id].txCmd = 0;
mPayload[iv->id].retransmits = 0; mPayload[iv->id].retransmits = 0;
mPayload[iv->id].maxPackId = 0; mPayload[iv->id].maxPackId = 0;
mPayload[iv->id].complete = false; mPayload[iv->id].complete = false;
mPayload[iv->id].requested = true; mPayload[iv->id].requested = false;
mPayload[iv->id].ts = mTimestamp; mPayload[iv->id].ts = mTimestamp;
} }

41
tools/esp8266/app.h

@ -36,15 +36,9 @@ typedef HmRadio<DEF_RF24_CE_PIN, DEF_RF24_CS_PIN, BufferType> RadioType;
typedef Inverter<float> InverterType; typedef Inverter<float> InverterType;
typedef HmSystem<RadioType, BufferType, MAX_NUM_INVERTERS, InverterType> HmSystemType; typedef HmSystem<RadioType, BufferType, MAX_NUM_INVERTERS, InverterType> HmSystemType;
const char* const wemosPins[] = {"D3 (GPIO0)", "TX (GPIO1)", "D4 (GPIO2)", "RX (GPIO3)",
"D2 (GPIO4)", "D1 (GPIO5)", "GPIO6", "GPIO7", "GPIO8",
"GPIO9", "GPIO10", "GPIO11", "D6 (GPIO12)", "D7 (GPIO13)",
"D5 (GPIO14)", "D8 (GPIO15)", "D0 (GPIO16 - no IRQ!)"};
const char* const pinNames[] = {"CS", "CE", "IRQ"};
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq"};
typedef struct { typedef struct {
uint8_t txCmd;
uint8_t txId; uint8_t txId;
uint8_t invId; uint8_t invId;
uint32_t ts; uint32_t ts;
@ -56,7 +50,6 @@ typedef struct {
bool requested; bool requested;
} invPayload_t; } invPayload_t;
class ahoywifi; class ahoywifi;
class web; class web;
@ -71,8 +64,6 @@ class app {
void cbMqtt(char* topic, byte* payload, unsigned int length); void cbMqtt(char* topic, byte* payload, unsigned int length);
void saveValues(void); void saveValues(void);
void resetPayload(Inverter<>* iv); void resetPayload(Inverter<>* iv);
String getStatistics(void);
String getJson(void);
bool getWifiApActive(void); bool getWifiApActive(void);
uint8_t getIrqPin(void) { uint8_t getIrqPin(void) {
@ -104,6 +95,15 @@ class app {
return String(str); return String(str);
} }
String getTimeStr(void) {
char str[20];
if(0 == mTimestamp)
sprintf(str, "n/a");
else
sprintf(str, "%02d:%02d:%02d ", hour(mTimestamp), minute(mTimestamp), second(mTimestamp));
return String(str);
}
inline uint32_t getUptime(void) { inline uint32_t getUptime(void) {
return mUptimeSecs; return mUptimeSecs;
} }
@ -112,6 +112,14 @@ class app {
return mTimestamp; return mTimestamp;
} }
inline void setTimestamp(uint32_t newTime) {
DPRINTLN(DBG_DEBUG, F("setTimestamp: ") + String(newTime));
if(0 == newTime)
mUpdateNtp = true;
else
mTimestamp = newTime;
}
void eraseSettings(bool all = false) { void eraseSettings(bool all = false) {
//DPRINTLN(DBG_VERBOSE, F("main.h:eraseSettings")); //DPRINTLN(DBG_VERBOSE, F("main.h:eraseSettings"));
uint8_t buf[64]; uint8_t buf[64];
@ -144,7 +152,12 @@ class app {
return false; return false;
} }
inline bool mqttIsConnected(void) { return mMqtt.isConnected(); }
inline bool getSettingsValid(void) { return mSettingsValid; }
inline bool getRebootRequestState(void) { return mShowRebootRequest; }
HmSystemType *mSys; HmSystemType *mSys;
bool mShouldReboot;
private: private:
void resetSystem(void); void resetSystem(void);
@ -169,7 +182,7 @@ class app {
while(length > 0) { while(length > 0) {
len = (length < 32) ? length : 32; len = (length < 32) ? length : 32;
mEep->read(start, buf, len); mEep->read(start, buf, len);
crc = Hoymiles::crc16(buf, len, crc); crc = Ahoy::crc16(buf, len, crc);
start += len; start += len;
length -= len; length -= len;
} }
@ -234,6 +247,7 @@ class app {
eep *mEep; eep *mEep;
uint32_t mTimestamp; uint32_t mTimestamp;
bool mUpdateNtp;
bool mShowRebootRequest; bool mShowRebootRequest;
@ -247,14 +261,11 @@ class app {
uint8_t mSendLastIvId; uint8_t mSendLastIvId;
invPayload_t mPayload[MAX_NUM_INVERTERS]; invPayload_t mPayload[MAX_NUM_INVERTERS];
uint32_t mRxFailed; statistics_t mStat;
uint32_t mRxSuccess;
uint32_t mFrameCnt;
uint8_t mLastPacketId; uint8_t mLastPacketId;
// timer // timer
uint32_t mTicker; uint32_t mTicker;
uint32_t mRxTicker; uint32_t mRxTicker;
// mqtt // mqtt

13
tools/esp8266/config.h

@ -45,17 +45,14 @@
#define DEF_RF24_CE_PIN 2 #define DEF_RF24_CE_PIN 2
#define DEF_RF24_IRQ_PIN 0 #define DEF_RF24_IRQ_PIN 0
// default radio ID
#define DTU_RADIO_ID ((uint64_t)0x1234567801ULL)
// default NRF24 power, possible values (0 - 3) // default NRF24 power, possible values (0 - 3)
#define DEF_AMPLIFIERPOWER 2 #define DEF_AMPLIFIERPOWER 1
// number of packets hold in buffer // number of packets hold in buffer
#define PACKET_BUFFER_SIZE 30 #define PACKET_BUFFER_SIZE 30
// number of configurable inverters // number of configurable inverters
#define MAX_NUM_INVERTERS 3 #define MAX_NUM_INVERTERS 4
// default serial interval // default serial interval
#define SERIAL_INTERVAL 5 #define SERIAL_INTERVAL 5
@ -108,8 +105,10 @@
// default MQTT topic // default MQTT topic
#define DEF_MQTT_TOPIC "inverter" #define DEF_MQTT_TOPIC "inverter"
// changes the style of "/setup" page, visualized = nicer //default MQTT Message Inverter Status
#define LIVEDATA_VISUALIZED #define DEF_MQTT_IV_MESSAGE_NOT_AVAIL_AND_NOT_PRODUCED "not available and not producing" // STATUS 0
#define DEF_MQTT_IV_MESSAGE_INVERTER_AVAIL_AND_NOT_PRODUCED "available and not producing" // STATUS 1
#define DEF_MQTT_IV_MESSAGE_INVERTER_AVAIL_AND_PRODUCED "available and producing" // STATUS 2
#if __has_include("config_override.h") #if __has_include("config_override.h")
#include "config_override.h" #include "config_override.h"

3
tools/esp8266/config_override_example.h

@ -24,7 +24,4 @@
#undef DEF_RF24_IRQ_PIN #undef DEF_RF24_IRQ_PIN
#define DEF_RF24_IRQ_PIN 16 #define DEF_RF24_IRQ_PIN 16
#undef DTU_RADIO_ID
#define DTU_RADIO_ID ((uint64_t)0x1234567802ULL)
#endif /*__CONFIG_OVERRIDE_H__*/ #endif /*__CONFIG_OVERRIDE_H__*/

7
tools/esp8266/crc.cpp

@ -5,8 +5,7 @@
#include "crc.h" #include "crc.h"
namespace Hoymiles { namespace Ahoy {
uint8_t crc8(uint8_t buf[], uint8_t len) { uint8_t crc8(uint8_t buf[], uint8_t len) {
uint8_t crc = CRC8_INIT; uint8_t crc = CRC8_INIT;
for(uint8_t i = 0; i < len; i++) { for(uint8_t i = 0; i < len; i++) {
@ -14,7 +13,6 @@ uint8_t crc8(uint8_t buf[], uint8_t len) {
for(uint8_t b = 0; b < 8; b ++) { for(uint8_t b = 0; b < 8; b ++) {
crc = (crc << 1) ^ ((crc & 0x80) ? CRC8_POLY : 0x00); crc = (crc << 1) ^ ((crc & 0x80) ? CRC8_POLY : 0x00);
} }
yield();
} }
return crc; return crc;
} }
@ -31,8 +29,7 @@ uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start) {
if(shift != 0) if(shift != 0)
crc = crc ^ CRC16_MODBUS_POLYNOM; crc = crc ^ CRC16_MODBUS_POLYNOM;
} }
yield();
} }
return crc; return crc;
} }
} // namespace Hoymiles }

4
tools/esp8266/crc.h

@ -14,10 +14,8 @@
#define CRC16_MODBUS_POLYNOM 0xA001 #define CRC16_MODBUS_POLYNOM 0xA001
namespace Hoymiles { namespace Ahoy {
uint8_t crc8(uint8_t buf[], uint8_t len); uint8_t crc8(uint8_t buf[], uint8_t len);
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start = 0xffff); uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start = 0xffff);
} }
#endif /*__CRC_H__*/ #endif /*__CRC_H__*/

3
tools/esp8266/dbg.cpp

@ -0,0 +1,3 @@
#include "dbg.h"
DBG_CB mCb = NULL;

38
tools/esp8266/defines.h

@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 5 #define VERSION_MINOR 5
#define VERSION_PATCH 17 #define VERSION_PATCH 19
//------------------------------------- //-------------------------------------
@ -57,21 +57,15 @@ typedef enum {
Init = 0xff Init = 0xff
} DevControlCmdType; } DevControlCmdType;
typedef enum { // ToDo: to be verified by field tests typedef enum {
NoPowerLimit = 0xffff, // ahoy internal value, no hoymiles value!
AbsolutNonPersistent = 0UL, // 0x0000 AbsolutNonPersistent = 0UL, // 0x0000
RelativNonPersistent = 1UL, // 0x0001 RelativNonPersistent = 1UL, // 0x0001
AbsolutPersistent = 256UL, // 0x0100 AbsolutPersistent = 256UL, // 0x0100
RelativPersistent = 257UL // 0x0101 RelativPersistent = 257UL // 0x0101
} PowerLimitControlType; } PowerLimitControlType;
// minimum serial interval
#define MIN_SERIAL_INTERVAL 5 #define MIN_SERIAL_INTERVAL 5
// minimum send interval
#define MIN_SEND_INTERVAL 15 #define MIN_SEND_INTERVAL 15
// minimum mqtt interval
#define MIN_MQTT_INTERVAL 60 #define MIN_MQTT_INTERVAL 60
//------------------------------------- //-------------------------------------
@ -90,27 +84,16 @@ typedef enum { // ToDo: to be verified by field tests
#define INV_MAX_RTRY_LEN 1 // uint8_t #define INV_MAX_RTRY_LEN 1 // uint8_t
#define INV_PWR_LIM_LEN MAX_NUM_INVERTERS * 2 // uint16_t #define INV_PWR_LIM_LEN MAX_NUM_INVERTERS * 2 // uint16_t
#define PINOUT_LEN 3 // 3 pins: CS, CE, IRQ
#define RF24_AMP_PWR_LEN 1
#define NTP_ADDR_LEN 32 // DNS Name #define NTP_ADDR_LEN 32 // DNS Name
#define NTP_PORT_LEN 2 // uint16_t
#define MQTT_ADDR_LEN 32 // DNS Name #define MQTT_ADDR_LEN 32 // DNS Name
#define MQTT_USER_LEN 16 #define MQTT_USER_LEN 16
#define MQTT_PWD_LEN 32 #define MQTT_PWD_LEN 32
#define MQTT_TOPIC_LEN 32 #define MQTT_TOPIC_LEN 32
#define MQTT_INTERVAL_LEN 2 // uint16_t
#define MQTT_PORT_LEN 2 // uint16_t
#define MQTT_DISCOVERY_PREFIX "homeassistant" #define MQTT_DISCOVERY_PREFIX "homeassistant"
#define MQTT_MAX_PACKET_SIZE 384 #define MQTT_MAX_PACKET_SIZE 384
#define MQTT_RECONNECT_DELAY 5000 #define MQTT_RECONNECT_DELAY 5000
#define SER_ENABLE_LEN 1 // uint8_t
#define SER_DEBUG_LEN 1 // uint8_t
#define SER_INTERVAL_LEN 2 // uint16_t
#pragma pack(push) // push current alignment to stack #pragma pack(push) // push current alignment to stack
#pragma pack(1) // set alignment to 1 byte boundary #pragma pack(1) // set alignment to 1 byte boundary
typedef struct { typedef struct {
@ -119,8 +102,10 @@ typedef struct {
char user[MQTT_USER_LEN]; char user[MQTT_USER_LEN];
char pwd[MQTT_PWD_LEN]; char pwd[MQTT_PWD_LEN];
char topic[MQTT_TOPIC_LEN]; char topic[MQTT_TOPIC_LEN];
} /*__attribute__((__packed__))*/ mqttConfig_t; } mqttConfig_t;
#pragma pack(pop) // restore original alignment from stack #pragma pack(pop) // restore original alignment from stack
typedef struct { typedef struct {
char deviceName[DEVNAME_LEN]; char deviceName[DEVNAME_LEN];
@ -151,9 +136,16 @@ typedef struct {
uint16_t serialInterval; uint16_t serialInterval;
bool serialShowIv; bool serialShowIv;
bool serialDebug; bool serialDebug;
} /*__attribute__((__packed__))*/ config_t; } config_t;
#pragma pack(pop) // restore original alignment from stack #pragma pack(pop) // restore original alignment from stack
typedef struct {
uint32_t rxFail;
uint32_t rxFailNoAnser;
uint32_t rxSuccess;
uint32_t frmCnt;
} statistics_t;
#define CFG_MQTT_LEN MQTT_ADDR_LEN + 2 + MQTT_USER_LEN + MQTT_PWD_LEN +MQTT_TOPIC_LEN #define CFG_MQTT_LEN MQTT_ADDR_LEN + 2 + MQTT_USER_LEN + MQTT_PWD_LEN +MQTT_TOPIC_LEN
#define CFG_SYS_LEN DEVNAME_LEN + SSID_LEN + PWD_LEN + 1 #define CFG_SYS_LEN DEVNAME_LEN + SSID_LEN + PWD_LEN + 1
@ -173,10 +165,8 @@ typedef struct {
#define ADDR_INV_CH_NAME ADDR_INV_CH_PWR + INV_CH_CH_PWR_LEN #define ADDR_INV_CH_NAME ADDR_INV_CH_PWR + INV_CH_CH_PWR_LEN
#define ADDR_INV_INTERVAL ADDR_INV_CH_NAME + INV_CH_CH_NAME_LEN #define ADDR_INV_INTERVAL ADDR_INV_CH_NAME + INV_CH_CH_NAME_LEN
#define ADDR_INV_MAX_RTRY ADDR_INV_INTERVAL + INV_INTERVAL_LEN #define ADDR_INV_MAX_RTRY ADDR_INV_INTERVAL + INV_INTERVAL_LEN
#define ADDR_INV_PWR_LIM ADDR_INV_MAX_RTRY + INV_MAX_RTRY_LEN
#define ADDR_INV_PWR_LIM_CON ADDR_INV_PWR_LIM + INV_PWR_LIM_LEN
#define ADDR_NEXT ADDR_INV_PWR_LIM_CON + INV_PWR_LIM_LEN #define ADDR_NEXT ADDR_INV_MAX_RTRY + INV_INTERVAL_LEN
#define ADDR_SETTINGS_CRC ADDR_NEXT + 2 #define ADDR_SETTINGS_CRC ADDR_NEXT + 2

194
tools/esp8266/favicon.h

@ -1,194 +0,0 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
// a) https://www.favicon-generator.org/
// b) exiftool -all:all= -r
// c) hexlify.py:
// import sys
// f = open (sys.argv[1], 'rb').read()
// for n, c in enumerate(f):
// if n % 16 == 0: print (' "', end = '')
// print (f"\\x{c:02x}", end = '')
// if n % 16 == 15: print ('" \\')
// if n % 16 != 15: print ('"')
#define FAVICON_PANEL_16 \
"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" \
"\x00\x00\x00\x10\x00\x00\x00\x10\x08\x03\x00\x00\x00\x28\x2d\x0f" \
"\x53\x00\x00\x00\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61" \
"\x05\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x26\x00\x00\x80" \
"\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00\x75\x30\x00\x00\xea" \
"\x60\x00\x00\x3a\x98\x00\x00\x17\x70\x9c\xba\x51\x3c\x00\x00\x01" \
"\x9b\x50\x4c\x54\x45\xfc\xfe\xff\xff\xff\xff\xcb\xcd\xcf\x22\x25" \
"\x30\x12\x16\x21\x11\x15\x21\x11\x15\x23\x12\x16\x25\x10\x13\x1f" \
"\x8f\x91\x93\x9b\x9d\xa1\x14\x17\x22\x14\x18\x25\x14\x17\x23\x13" \
"\x17\x22\x14\x17\x24\x17\x19\x27\x22\x24\x2e\xc1\xc2\xc3\xf8\xfa" \
"\xfb\x61\x63\x6b\x11\x13\x1f\x15\x19\x24\x17\x19\x24\x18\x1a\x24" \
"\x18\x1b\x26\x16\x19\x26\x42\x44\x4b\xe7\xe8\xe9\xfd\xff\xff\xfe" \
"\xff\xff\xe0\xe2\xe4\x33\x36\x3f\x16\x19\x24\x16\x17\x22\x16\x18" \
"\x22\x17\x18\x23\x11\x12\x1e\x71\x73\x76\xfb\xfc\xfd\xb5\xb7\xba" \
"\x1a\x1c\x27\x15\x17\x22\x15\x17\x21\x14\x15\x20\x14\x16\x23\x17" \
"\x1a\x26\x1b\x1c\x25\xaa\xac\xad\x7c\x7f\x85\x11\x13\x1d\x16\x18" \
"\x24\x17\x19\x25\x18\x1a\x25\x30\x32\x38\xd7\xd9\xd9\xef\xf1\xf2" \
"\x47\x49\x51\x12\x13\x1e\x17\x19\x22\x16\x17\x23\x10\x11\x1c\x59" \
"\x5a\x5f\xf4\xf6\xf7\xcc\xce\xd0\x24\x25\x2e\x16\x18\x23\x19\x1a" \
"\x24\x16\x17\x21\x15\x16\x21\x17\x17\x21\x15\x17\x20\x92\x93\x95" \
"\x99\x9a\x9e\x16\x16\x20\x17\x19\x23\x1a\x1b\x25\x23\x25\x2c\xc4" \
"\xc6\xc7\xf8\xfa\xfa\x61\x62\x68\x15\x16\x20\x11\x12\x1d\x44\x45" \
"\x49\xea\xec\xec\xdf\xe1\xe2\x33\x35\x3b\x15\x16\x1f\x13\x14\x1d" \
"\x78\x7a\x7c\xfc\xfd\xfe\xb2\xb4\xb7\x1a\x1c\x26\x18\x1b\x25\x1a" \
"\x1b\x22\x19\x1b\x22\xae\xaf\xb0\x86\x88\x8b\x14\x16\x1e\x19\x1b" \
"\x25\x17\x19\x21\x17\x19\x20\x12\x13\x1a\x2c\x2d\x32\xcc\xce\xcf" \
"\xfb\xfe\xff\xd8\xdb\xdb\x72\x74\x76\x25\x26\x2c\x19\x1c\x24\x13" \
"\x16\x1d\x11\x13\x18\x4c\x4d\x4f\xc1\xc3\xc3\xe2\xe4\xe6\xf6\xf8" \
"\xf8\xb6\xb9\xb9\x4f\x51\x55\x1b\x1c\x23\x19\x1b\x23\x15\x17\x1f" \
"\x70\x71\x72\xbb\xbb\xbc\xc3\xc4\xc5\xde\xe0\xe1\xf6\xf8\xf9\xfd" \
"\xfe\xff\xe3\xe5\xe6\x7e\x81\x82\x21\x22\x27\x1d\x1f\x26\x94\x95" \
"\x95\xbd\xbd\xbe\xbd\xbe\xbe\xc8\xca\xcb\xe5\xe7\xe9\xfa\xfc\xfd" \
"\x52\x5f\xd3\xea\x00\x00\x00\x01\x62\x4b\x47\x44\x01\xff\x02\x2d" \
"\xde\x00\x00\x00\xd2\x49\x44\x41\x54\x18\xd3\x63\x60\x00\x02\x46" \
"\x26\x66\x16\x16\x56\x36\x76\x0e\x4e\x46\x06\x30\x60\xe4\xe2\xe6" \
"\xe1\xe5\xe3\x17\x10\x14\x82\x0a\x08\x8b\x88\x8a\x89\x4b\x48\x4a" \
"\x49\xcb\xc8\x82\xf9\x72\xf2\x0a\xdc\x8a\x4a\xca\x2a\xaa\x6a\xea" \
"\x50\x1d\x1a\x9a\x5a\xda\x3a\xba\x7a\xfa\x06\x50\x1d\xb2\x86\x46" \
"\x5a\xbc\xc6\x26\xa6\x66\xe6\x72\x10\xbe\x85\xa5\x95\xb5\x89\xa9" \
"\x8d\xad\x9d\x3d\xc4\x08\x46\x07\x47\x27\x67\x17\x57\x37\x77\x0f" \
"\x39\xa8\x11\x9e\x4a\xa6\x36\x5e\xde\x3e\xbe\x7e\x50\x23\xfc\x03" \
"\x02\x55\x8c\x9d\x55\x82\x82\x43\xa0\x96\x86\x86\x85\x4b\x48\xb8" \
"\x84\x47\x44\x46\x41\x75\x44\xc7\x78\xc7\xba\x87\xc7\xc5\x27\x40" \
"\x75\xc8\x25\x26\x25\x7b\xa7\xa4\xa6\xa5\x67\x64\x42\x5d\x91\x95" \
"\x9d\x93\x92\x9b\x97\x5f\x50\x58\x24\x0c\x55\x52\x5c\x52\x5a\x56" \
"\x5e\x51\x59\x55\x5d\x53\x0b\x11\xa9\x93\xab\x6f\x68\x6c\x6a\x6e" \
"\x69\x6d\x6b\xef\x60\x00\x00\x01\x53\x2a\x2a\x63\x34\xcd\xf7\x00" \
"\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82"
#define FAVICON_PANEL_32 \
"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" \
"\x00\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a" \
"\xf4\x00\x00\x00\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61" \
"\x05\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x26\x00\x00\x80" \
"\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00\x75\x30\x00\x00\xea" \
"\x60\x00\x00\x3a\x98\x00\x00\x17\x70\x9c\xba\x51\x3c\x00\x00\x00" \
"\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00" \
"\x00\x07\x4a\x49\x44\x41\x54\x58\xc3\x9d\x97\xd9\x72\x5d\xc5\x15" \
"\x86\xbf\xee\xde\xd3\xd9\x67\xd0\x99\x74\x34\x3b\x48\x32\x48\xe0" \
"\x21\x60\xe1\x32\xd8\x15\x5e\x80\xdc\xf2\x00\xe4\x41\x12\x9e\x21" \
"\x37\xc9\x43\xa4\x20\x95\xe2\x9a\x82\x0c\x10\x70\x62\x06\x0f\xc2" \
"\x24\x15\xe2\x59\x96\xce\xbc\xe7\xb1\x73\x21\x93\x32\x20\x4b\xa7" \
"\xe8\xaa\x7d\xb3\x2f\x7a\x7d\xf5\xff\xfd\xaf\x5e\x2d\xf2\x52\x6b" \
"\x66\x58\x4a\xc0\x67\xff\xf8\x27\x6f\xbd\xf5\x36\xa3\xf1\x04\xad" \
"\x0b\x84\x94\x68\xad\xc9\xf3\x1c\xc7\x76\x08\xa3\x04\xdb\xb6\xc8" \
"\xf3\x12\x0d\xb8\x15\x9b\x28\x8c\x90\x52\xb0\xfe\xdc\x02\xbf\xff" \
"\xdd\x6f\xb9\x72\xf9\x32\xc5\x53\x15\xe5\x2c\xc5\xbf\x5b\xdf\x7e" \
"\x7b\x87\x34\x4b\x68\x36\xeb\x38\x8e\x4d\xb3\x59\xc7\xb6\x0d\x3a" \
"\x9d\x16\xb6\x63\x52\x71\x6c\xea\xb5\x2a\x52\x0a\x6a\xd5\x0a\x96" \
"\x65\x02\x1a\xc7\x36\xd9\xde\xda\x64\x6b\x6b\x9b\xf2\x07\x7b\x1a" \
"\xb3\x16\xcf\x8a\x82\x4f\x3f\xbd\x4a\x18\x86\x34\x1a\x73\x68\x24" \
"\x52\x1a\x08\xa1\xa8\x54\x1c\xa2\x30\xa4\xd9\xac\xd3\x9d\xef\xa0" \
"\xb5\x66\x65\x65\x91\x30\x8c\x50\x52\xd3\x6a\xd5\xd9\xda\x7a\x81" \
"\x56\xbb\xcd\x0f\xf5\x9e\x09\x40\x0a\x78\xb4\xf7\x98\x8f\x3f\xfe" \
"\x8c\xa2\x28\x99\x4c\xa6\x94\x25\xe4\x59\x49\x14\x47\xc4\x51\x4c" \
"\x92\x24\x54\x9c\x0a\xfd\xfe\x88\x3c\xcb\x89\xc2\x88\x2c\xcb\xd1" \
"\x5a\x23\x74\xce\xf6\xf6\x36\x4a\xf0\x3d\xf9\x67\x06\x10\xc0\xd7" \
"\xb7\xbf\xa1\x3f\x18\xb1\xb0\x30\x8f\xef\xfb\x98\xa6\x49\x96\x6b" \
"\xda\x9d\x39\x6c\x4b\x31\x1c\x8e\xa9\xd5\xaa\x8c\xc7\x1e\xae\xeb" \
"\x50\x71\xab\x0c\xfa\x63\x4c\x43\xb2\x79\xfa\x39\x2e\xbd\xf6\x1a" \
"\x47\x1d\xb6\x99\x2d\xb8\x7a\xf5\x73\xee\xde\x79\x80\x6d\x9b\x64" \
"\x79\x86\x6d\xdb\x64\x59\x86\x28\x73\x22\xa1\xa1\x2c\xb1\x2d\x03" \
"\xd3\x54\x2c\x2e\xf6\x70\x6c\x87\xb2\x28\xa8\xd5\x1c\xce\x9f\x3b" \
"\xc3\xda\xda\x29\x4a\xfd\x13\x00\x84\x00\xcf\x0f\xb8\x7e\xfd\x3a" \
"\xbd\x85\x0e\x8e\x6d\x31\x99\x7a\x58\x96\xc5\x70\x38\x22\x8a\x63" \
"\xd2\x24\xc5\xb2\x4c\x1e\xde\x7f\x44\x9c\xa4\xa4\x71\x42\x91\xe7" \
"\x14\x65\x49\x96\xda\xac\xad\xad\xe2\x58\xe6\x8f\xe4\x9f\x09\x40" \
"\x02\xb7\xbf\xf9\x86\x4f\x3e\xb9\x4a\x92\xc4\x68\x5d\xd2\x68\xd4" \
"\x71\x9c\x0a\x4a\x49\x1c\xdb\xe2\xe0\x60\xc0\xe2\x52\x0f\x7f\xea" \
"\xe3\xe4\x19\xae\x5b\xe5\xe0\x60\x88\x10\x8a\x4e\xbb\xc5\xeb\x97" \
"\x2f\x3f\x73\xff\x99\x2c\xd8\xdd\xbd\xcd\x70\x34\x46\x6b\xcd\x74" \
"\xea\x63\x99\x16\x79\x9e\x63\x5b\x06\x9e\x80\x2c\xcb\x99\x4e\x7d" \
"\x92\xa4\xa0\xd9\x6a\xb1\xd0\xeb\x90\x65\x39\x96\x65\xb0\xb3\xf3" \
"\x73\xce\xbc\x74\xe6\x47\xf1\x9b\x19\x20\x2b\x0a\xbe\xf8\xe2\x3a" \
"\x95\x4a\x85\xb9\x46\x95\xfe\x60\x48\xb7\xdb\x65\x32\x1a\xa1\x94" \
"\x62\x3a\x0d\x90\x52\x12\x85\x11\x7e\x90\x50\x14\x05\xfd\xbd\x3d" \
"\xb2\x3c\xa3\xde\x70\x59\x5b\x5b\x65\xae\x39\xc7\xb3\xda\xdd\xb1" \
"\x00\x52\xc0\x83\x47\x7b\x7c\xf8\xd1\xdf\x18\x8f\x27\x44\xbe\x07" \
"\x42\xe1\xfb\x11\x85\x96\x74\xda\x6d\xb4\x50\xb8\xae\x4d\x51\x94" \
"\x98\x56\x82\xeb\x3a\x0c\xfb\x03\x84\x90\x28\x25\x39\x7f\xfe\x3c" \
"\x12\x28\x7e\x8a\x02\x02\xd8\xdd\xfd\x9a\xbd\xbd\x7d\x96\x16\x7b" \
"\x84\xbe\x87\x46\x3e\xe9\x03\x9a\xfb\xf7\xf6\x88\x93\x84\x38\x76" \
"\x28\x72\xa8\x56\x5d\xe6\xe7\x3b\x14\x79\x86\xd6\x70\xee\xdc\xf3" \
"\x5c\xba\x74\x89\xe3\x7a\xfd\x89\x16\x5c\xbb\xf6\x25\xfd\x7e\x9f" \
"\xb9\x46\x0d\x29\x25\xed\x76\x0b\x06\x23\x9a\xed\x16\x9e\x17\x81" \
"\x00\x43\x29\x02\xcf\x47\x08\xf8\xd7\xed\x7f\x13\xc7\x09\x8d\xb9" \
"\x3a\xa7\x37\x37\x58\x5e\x59\x39\x32\x7e\x27\x02\x08\x01\x5e\x10" \
"\x70\xfd\xc6\x4d\x2a\x8e\x4d\x96\x66\x24\x49\x42\x9e\x17\x44\x51" \
"\x8c\x32\x14\x68\x58\x5d\x5d\x24\x4f\x13\xa4\x94\xd4\xea\xd5\x43" \
"\xf9\xa5\xa4\x2c\x33\x36\xd6\x9f\xc3\x32\x8c\x23\xe3\x77\x22\x80" \
"\x7c\x22\xff\xd5\xcf\xae\xd2\xeb\x75\x48\xd3\x0c\xcb\xb2\x48\x93" \
"\x84\x2c\xcb\x49\xa2\x98\x30\x88\x88\xa3\x94\xb2\x48\x31\x0c\x03" \
"\x77\xbe\x45\xde\xa8\xe3\xe4\x05\xa7\xd6\x7a\x5c\xbe\x72\xe5\x24" \
"\x81\x8f\xb7\xe0\xe6\xcd\x5b\x3c\x7c\xf0\x08\x29\x0c\x0a\xad\xe9" \
"\x74\x5a\x20\x24\xcb\x2b\x8b\xa4\x69\x8a\x65\xd9\x28\x43\x31\x1a" \
"\x44\x80\xe4\xe1\x83\x3d\x82\x20\xa2\x5a\xaf\xb2\xbd\xf5\x02\xdb" \
"\x2f\xbe\xf8\xcc\xf8\x9d\x08\x90\xe5\x05\x37\x6f\xdc\xa2\xd1\x68" \
"\x50\x6f\xcc\xb1\xbf\xdf\x27\x08\x62\xa2\x30\xc4\x77\x2c\xb4\x2e" \
"\x69\xb7\x9b\x74\xe6\xbb\xa4\x69\x4a\xaf\xd7\x65\xd0\x1f\x20\xa4" \
"\x40\x88\x92\x53\xa7\x56\xa9\xd7\x6a\xc7\xfa\xff\x4c\x00\x29\xe0" \
"\xde\xc3\x87\x7c\xf0\xc1\x5f\x08\xc2\x14\x43\x05\x74\x3b\x73\x18" \
"\x96\xc3\x74\x3c\x01\x34\xa3\xe1\x98\xd1\x70\x42\x7f\x30\xa1\x2c" \
"\x72\xb2\x34\xc3\xa9\xb8\x28\x43\xd1\xee\x34\xb8\x70\xe1\x02\xe2" \
"\x44\x03\x9e\x01\x70\x18\xbf\x5d\x06\xc3\x21\x8e\x63\x33\x19\x4f" \
"\xb0\x2c\x93\x42\x2b\x1a\xcd\x26\xad\xa6\x4b\x51\xe4\xd4\x1b\x75" \
"\x1e\xef\x1d\x00\x30\x1a\x4d\xf0\xbd\x90\x8a\x6b\xb1\xbe\x73\x96" \
"\x57\x2f\x5e\x64\x96\x51\xeb\x48\x00\x0d\x7c\x7e\xed\x4b\x26\x93" \
"\x29\xbd\x5e\x97\x40\x81\x53\xa9\xf0\xf8\xf1\x90\xf1\x68\xcc\xb0" \
"\xbf\x8f\x69\x2a\x5c\xd7\xa5\x5a\xab\xd2\xed\xb6\xf1\xfd\x10\xdf" \
"\x0f\x50\x0a\xd6\x7f\x76\x8a\x85\x85\xc5\x13\xe5\x87\x23\x46\xb2" \
"\xc3\xdb\xcf\xe7\xda\xb5\x6b\x64\x69\x42\x7f\xbf\x4f\x92\xa4\xb8" \
"\xd5\x0a\xcd\x66\x8d\x85\xc5\x0e\x08\x41\xa9\x05\x77\xff\x7b\x9f" \
"\xc9\xd8\xc3\x0f\x42\x84\x28\x69\xb6\x1b\x74\xba\x2d\x5e\xd8\xda" \
"\xc2\x54\xb3\x4d\x7b\xf2\xa8\x1f\xbb\xbb\xbb\xdc\xb8\x79\x8b\xc5" \
"\xa5\x1e\xae\xeb\xa0\x81\x7b\x77\x1f\xe2\xf9\x01\xe8\x82\x7a\xdd" \
"\xa5\x3b\xdf\xc2\xb4\x14\x52\x68\xa6\xe3\x31\x7b\x8f\x1e\x13\xf8" \
"\x01\xed\x76\x8b\x2b\x33\xc4\xef\x58\x0b\xbe\xfa\xea\x16\x77\xee" \
"\x3c\xc2\x75\x1d\x2c\xd3\xa0\xd9\x6e\xe2\xfb\x11\x59\x96\xb1\xbf" \
"\xb7\x4f\x9e\x17\x28\x29\x31\x2d\x83\xd5\xce\x12\x42\x1c\xaa\xa1" \
"\x94\xe2\xf4\xe6\x3a\xa7\x9f\x7f\xfe\xc4\xf8\x3d\x13\x20\xcd\x0b" \
"\xae\xdf\xb8\x49\xad\xd1\xc0\x50\x8a\xe9\x64\x42\x56\x68\xa4\x54" \
"\x74\x3b\x4d\x3c\xcf\x27\x89\x13\xc2\x20\x24\x0c\x02\x04\x1a\xcb" \
"\xb6\x69\x34\x1b\x54\xaa\x15\x4e\x6f\x6e\x50\x75\xdd\x99\xfc\xff" \
"\x11\x80\x14\x70\xe7\xfe\x3d\x3e\xfa\xf0\xcf\x58\xa6\xa2\x56\xab" \
"\x61\x59\x26\x69\x9a\x32\x9d\x78\xa0\x0b\x40\xb3\xb4\xbc\x80\xe7" \
"\x79\x24\x69\x4c\xe0\x07\x8c\xfa\x63\x84\x61\xd0\x68\xb8\x9c\x39" \
"\x7b\x96\xa2\xd4\x08\x31\x4b\x08\x7f\x00\x20\x80\x7b\xf7\xef\x93" \
"\xc4\x01\xa1\x37\xc1\x9f\x4e\x59\x5a\x5e\xc2\xb2\x4c\x2c\xcb\x24" \
"\x8e\x22\xbc\xa9\x47\x59\x14\x08\x43\x31\xbf\xd0\xc5\x75\x1d\xee" \
"\xfc\xe7\x2e\x52\xc1\xea\xca\x12\x4b\xcb\x2b\x0c\x87\x23\xea\xf5" \
"\xfa\x93\x77\xc1\xf1\x4b\xfd\xfa\x37\xef\xbc\xf3\x34\x41\xbd\x56" \
"\x67\x71\xb9\xc7\x78\x32\xe2\xe0\xf1\x3e\x81\x17\xe0\x79\x3e\xdd" \
"\x6e\x07\xc3\x30\x10\x42\x20\xa4\xc4\x9b\x7a\x68\x01\xba\x28\x91" \
"\x4a\xd2\x6c\xd6\x79\xe9\xa5\x2d\x76\x76\x76\xc8\x8b\x02\x21\x04" \
"\xa6\x69\xa0\x94\x3a\x56\x8d\xef\x03\x00\xae\x5b\xe1\xdc\xd9\xb3" \
"\xbc\xfc\xca\xcb\x38\xae\xcd\x64\x32\x66\xd0\x1f\x10\xf8\x3e\x41" \
"\x10\xd2\x5b\xe8\x61\x3b\x0e\x49\x92\x52\x16\x05\xde\xd4\x23\x4d" \
"\x12\x2c\x5b\xf1\xfa\x6b\x97\xd8\xd8\xd8\x20\x4b\x53\xf2\x3c\x07" \
"\x0e\x21\xbe\x03\x9f\x09\xe0\x50\x09\xc1\x42\xaf\xc7\xc5\x8b\x17" \
"\x59\xdf\x5c\x47\x8b\x92\xc9\x74\xc2\x68\x30\x22\x89\x53\xa2\x30" \
"\xa6\x56\xab\xd2\x9d\xef\x12\xf8\x11\x5a\xc3\xf2\x52\x8f\x5f\xbe" \
"\xf9\x26\x8d\x46\x9d\xa2\x28\xc8\xb2\x9c\x2c\xcf\x0f\x7d\x36\x0c" \
"\x0c\xc3\x3c\x12\xe2\x68\x00\x0e\xbb\xa1\x65\x59\x9c\xde\xdc\xe4" \
"\xc2\x2b\xaf\xd0\x6c\xcd\x31\x1a\x8f\x89\xe3\x98\xe9\x68\x8c\x2e" \
"\x0b\xb4\x06\xa9\x14\xcd\x56\x93\xed\xad\x4d\xde\x78\xe3\x17\x48" \
"\xa9\x00\x8d\x2e\x4b\x8a\x22\x27\xcf\x0f\x5f\x47\x87\x10\x06\x52" \
"\x7e\x1f\xe2\xd8\xeb\xf8\xbb\x41\x72\x75\x75\x95\x5f\xbd\xfd\x36" \
"\xaf\xee\xec\xf0\xa7\xf7\xdf\xe7\xdd\x3f\xfc\x91\x83\xfd\x7d\xa6" \
"\xe3\x11\x85\x96\xd8\x8e\xc9\xd2\xe2\x02\x8e\xe3\xa0\xff\x3f\x7d" \
"\x6a\x8a\x3c\x27\x0c\x43\x84\x10\x68\x0d\x5a\x6b\xaa\xd5\x2a\xea" \
"\xa9\x2e\x39\xd3\x58\xae\x35\x98\x86\xc9\xab\x3b\x3b\xac\x9d\x3a" \
"\xc5\xc6\xfa\x3a\xef\xbe\xf7\x1e\x7f\xff\xeb\xa7\xe4\x71\x46\x99" \
"\xe7\xac\xaf\xaf\xa3\x94\xa2\x2c\xf5\x93\x2b\x59\x22\xe5\xe1\xa7" \
"\x35\x94\x65\x49\x59\x16\x68\x5d\xf2\x74\x03\xfe\x1f\xc2\x60\x72" \
"\xe2\x6a\x9b\x4e\x8f\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60" \
"\x82"

41
tools/esp8266/hmDefines.h

@ -17,19 +17,21 @@ union serial_u {
// units // units
enum {UNIT_V = 0, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_HZ, UNIT_C, UNIT_PCT, UNIT_VA, UNIT_NONE}; enum {UNIT_V = 0, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_HZ, UNIT_C, UNIT_PCT, UNIT_VAR, UNIT_NONE};
const char* const units[] = {"V", "A", "W", "Wh", "kWh", "Hz", "°C", "%","VAr",""}; const char* const units[] = {"V", "A", "W", "Wh", "kWh", "Hz", "°C", "%", "var", ""};
// field types // field types
enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT, enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT,
FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_T, FLD_PCT, FLD_EFF, FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_T, FLD_PF, FLD_EFF,
FLD_IRR, FLD_PRA,FLD_ALARM_MES_ID,FLD_FW_VERSION,FLD_FW_BUILD_YEAR, FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR,
FLD_FW_BUILD_MONTH_DAY,FLD_HW_ID,FLD_ACT_PWR_LIMIT,FLD_LAST_ALARM_CODE}; FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_HW_ID,
FLD_ACT_ACTIVE_PWR_LIMIT, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE};
const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal",
"U_AC", "I_AC", "P_AC", "Freq", "Temp", "Pct", "Efficiency", "Irradiation","P_ACr", "U_AC", "I_AC", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC",
"ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","HWPartId","PowerLimit","LastAlarmCode"}; "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","HWPartId",
"active PowerLimit", /*"reactive PowerLimit","Powerfactor",*/ "LastAlarmCode"};
const char* const notAvail = "n/a";
// mqtt discovery device classes // mqtt discovery device classes
enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP};
@ -53,7 +55,7 @@ const byteAssign_fieldDeviceClass deviceFieldAssignment[] = {
{FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT}, {FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT},
{FLD_F, DEVICE_CLS_FREQ, STATE_CLS_NONE}, {FLD_F, DEVICE_CLS_FREQ, STATE_CLS_NONE},
{FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT}, {FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT},
{FLD_PCT, DEVICE_CLS_NONE, STATE_CLS_NONE}, {FLD_PF, DEVICE_CLS_NONE, STATE_CLS_NONE},
{FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE}, {FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE},
{FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE} {FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE}
}; };
@ -92,12 +94,15 @@ const byteAssign_t InfoAssignment[] = {
{ FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, { FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 },
{ FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, { FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 },
{ FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, { FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 },
{ FLD_FW_BUILD_HOUR_MINUTE, UNIT_NONE, CH0, 6, 2, 1 },
{ FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } { FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 }
}; };
#define HMINFO_LIST_LEN (sizeof(InfoAssignment) / sizeof(byteAssign_t)) #define HMINFO_LIST_LEN (sizeof(InfoAssignment) / sizeof(byteAssign_t))
const byteAssign_t SystemConfigParaAssignment[] = { const byteAssign_t SystemConfigParaAssignment[] = {
{ FLD_ACT_PWR_LIMIT, UNIT_PCT, CH0, 2, 2, 10 } { FLD_ACT_ACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 2, 2, 10 }/*,
{ FLD_ACT_REACTIVE_PWR_LIMIT, UNIT_PCT, CH0, 4, 2, 10 },
{ FLD_ACT_PF, UNIT_NONE, CH0, 6, 2, 1000 }*/
}; };
#define HMSYSTEM_LIST_LEN (sizeof(SystemConfigParaAssignment) / sizeof(byteAssign_t)) #define HMSYSTEM_LIST_LEN (sizeof(SystemConfigParaAssignment) / sizeof(byteAssign_t))
@ -122,10 +127,11 @@ const byteAssign_t hm1chAssignment[] = {
{ FLD_UAC, UNIT_V, CH0, 14, 2, 10 }, { FLD_UAC, UNIT_V, CH0, 14, 2, 10 },
{ FLD_IAC, UNIT_A, CH0, 22, 2, 100 }, { FLD_IAC, UNIT_A, CH0, 22, 2, 100 },
{ FLD_PAC, UNIT_W, CH0, 18, 2, 10 }, { FLD_PAC, UNIT_W, CH0, 18, 2, 10 },
{ FLD_PRA, UNIT_VA, CH0, 20, 2, 10 }, { FLD_Q, UNIT_VAR, CH0, 20, 2, 10 },
{ FLD_F, UNIT_HZ, CH0, 16, 2, 100 }, { FLD_F, UNIT_HZ, CH0, 16, 2, 100 },
{ FLD_PF, UNIT_NONE, CH0, 24, 2, 1000 },
{ FLD_T, UNIT_C, CH0, 26, 2, 10 }, { FLD_T, UNIT_C, CH0, 26, 2, 10 },
{ FLD_ALARM_MES_ID, UNIT_NONE, CH0, 24, 2, 1 }, { FLD_EVT, UNIT_NONE, CH0, 28, 2, 1 },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
@ -155,10 +161,11 @@ const byteAssign_t hm2chAssignment[] = {
{ FLD_UAC, UNIT_V, CH0, 26, 2, 10 }, { FLD_UAC, UNIT_V, CH0, 26, 2, 10 },
{ FLD_IAC, UNIT_A, CH0, 34, 2, 100 }, { FLD_IAC, UNIT_A, CH0, 34, 2, 100 },
{ FLD_PAC, UNIT_W, CH0, 30, 2, 10 }, { FLD_PAC, UNIT_W, CH0, 30, 2, 10 },
{ FLD_PRA, UNIT_VA, CH0, 32, 2, 10 }, { FLD_Q, UNIT_VAR, CH0, 32, 2, 10 },
{ FLD_F, UNIT_HZ, CH0, 28, 2, 100 }, { FLD_F, UNIT_HZ, CH0, 28, 2, 100 },
{ FLD_PF, UNIT_NONE, CH0, 36, 2, 1000 },
{ FLD_T, UNIT_C, CH0, 38, 2, 10 }, { FLD_T, UNIT_C, CH0, 38, 2, 10 },
{ FLD_ALARM_MES_ID, UNIT_NONE, CH0, 40, 2, 1 }, { FLD_EVT, UNIT_NONE, CH0, 40, 2, 1 },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },
@ -203,11 +210,11 @@ const byteAssign_t hm4chAssignment[] = {
{ FLD_UAC, UNIT_V, CH0, 46, 2, 10 }, { FLD_UAC, UNIT_V, CH0, 46, 2, 10 },
{ FLD_IAC, UNIT_A, CH0, 54, 2, 100 }, { FLD_IAC, UNIT_A, CH0, 54, 2, 100 },
{ FLD_PAC, UNIT_W, CH0, 50, 2, 10 }, { FLD_PAC, UNIT_W, CH0, 50, 2, 10 },
{ FLD_PRA, UNIT_VA, CH0, 52, 2, 10 }, { FLD_Q, UNIT_VAR, CH0, 52, 2, 10 },
{ FLD_F, UNIT_HZ, CH0, 48, 2, 100 }, { FLD_F, UNIT_HZ, CH0, 48, 2, 100 },
{ FLD_PCT, UNIT_PCT, CH0, 56, 2, 10 }, { FLD_PF, UNIT_NONE, CH0, 56, 2, 1000 },
{ FLD_T, UNIT_C, CH0, 58, 2, 10 }, { FLD_T, UNIT_C, CH0, 58, 2, 10 },
{ FLD_ALARM_MES_ID, UNIT_NONE, CH0, 60, 2, 1 }, { FLD_EVT, UNIT_NONE, CH0, 60, 2, 1 },
{ FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC },
{ FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC },
{ FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC },

569
tools/esp8266/hmInverter.h

@ -23,7 +23,7 @@
*/ */
// forward declaration of class // forward declaration of class
template <class RECORDTYPE=float> template <class REC_TYP=float>
class Inverter; class Inverter;
@ -55,6 +55,13 @@ struct calcFunc_t {
func_t<T>* func; // function pointer func_t<T>* func; // function pointer
}; };
template<class T=float>
struct record_t {
byteAssign_t* assign; // assigment of bytes in payload
uint8_t length; // length of the assignment list
T *record; // data pointer
uint32_t ts; // timestamp of last received payload
};
class CommandAbstract { class CommandAbstract {
public: public:
@ -64,8 +71,7 @@ class CommandAbstract {
}; };
virtual ~CommandAbstract() {}; virtual ~CommandAbstract() {};
const uint8_t getCmd() const uint8_t getCmd() {
{
return _Cmd; return _Cmd;
} }
@ -94,34 +100,33 @@ const calcFunc_t<T> calcFunctions[] = {
}; };
template <class RECORDTYPE> template <class REC_TYP>
class Inverter { class Inverter {
public: public:
uint8_t id; // unique id uint8_t id; // unique id
char name[MAX_NAME_LENGTH]; // human readable name, eg. "HM-600.1" char name[MAX_NAME_LENGTH]; // human readable name, eg. "HM-600.1"
uint8_t type; // integer which refers to inverter type uint8_t type; // integer which refers to inverter type
byteAssign_t* assign; // type of inverter
uint8_t listLen; // length of assignments
uint16_t alarmMesIndex; // Last recorded Alarm Message Index uint16_t alarmMesIndex; // Last recorded Alarm Message Index
uint16_t fwVersion; // Firmware Version from Info Command Request uint16_t fwVersion; // Firmware Version from Info Command Request
uint16_t powerLimit[2]; // limit power output uint16_t powerLimit[2]; // limit power output
uint16_t actPowerLimit; // float actPowerLimit; // actual power limit
uint8_t devControlCmd; // carries the requested cmd uint8_t devControlCmd; // carries the requested cmd
bool devControlRequest; // true if change needed bool devControlRequest; // true if change needed
serial_u serial; // serial number as on barcode serial_u serial; // serial number as on barcode
serial_u radioId; // id converted to modbus serial_u radioId; // id converted to modbus
uint8_t channels; // number of PV channels (1-4) uint8_t channels; // number of PV channels (1-4)
uint32_t ts; // timestamp of last received payload record_t<REC_TYP> recordMeas; // structure for measured values
RECORDTYPE *record; // pointer for values record_t<REC_TYP> recordInfo; // structure for info values
record_t<REC_TYP> recordConfig; // structure for system config values
record_t<REC_TYP> recordAlarm; // structure for alarm values
uint16_t chMaxPwr[4]; // maximum power of the modules (Wp) uint16_t chMaxPwr[4]; // maximum power of the modules (Wp)
char chName[4][MAX_NAME_LENGTH]; // human readable name for channel char chName[4][MAX_NAME_LENGTH]; // human readable name for channels
String lastAlarmMsg; String lastAlarmMsg;
bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null) bool initialized; // needed to check if the inverter was correctly added (ESP32 specific - union types are never null)
Inverter() { Inverter() {
ts = 0;
powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited
powerLimit[1] = NoPowerLimit; // powerLimit[1] = AbsolutNonPersistent; // default power limit setting
actPowerLimit = 0xffff; // init feedback from inverter to -1 actPowerLimit = 0xffff; // init feedback from inverter to -1
devControlRequest = false; devControlRequest = false;
devControlCmd = InitDataState; devControlCmd = InitDataState;
@ -136,8 +141,7 @@ class Inverter {
} }
template <typename T> template <typename T>
void enqueCommand(uint8_t cmd) void enqueCommand(uint8_t cmd) {
{
_commandQueue.push(std::make_shared<T>(cmd)); _commandQueue.push(std::make_shared<T>(cmd));
DPRINTLN(DBG_INFO, "enqueuedCmd: " + String(cmd)); DPRINTLN(DBG_INFO, "enqueuedCmd: " + String(cmd));
} }
@ -155,8 +159,8 @@ class Inverter {
_commandQueue.pop(); _commandQueue.pop();
} }
} }
uint8_t getQueuedCmd()
{ uint8_t getQueuedCmd() {
if (_commandQueue.empty()){ if (_commandQueue.empty()){
// Fill with default commands // Fill with default commands
enqueCommand<InfoCommand>(RealTimeRunData_Debug); enqueCommand<InfoCommand>(RealTimeRunData_Debug);
@ -175,394 +179,289 @@ class Inverter {
void init(void) { void init(void) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:init")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:init"));
getAssignment(); initAssignment(&recordMeas, RealTimeRunData_Debug);
initAssignment(&recordInfo, InverterDevInform_All);
initAssignment(&recordConfig, SystemConfigPara);
initAssignment(&recordAlarm, AlarmData);
toRadioId(); toRadioId();
record = new RECORDTYPE[listLen];
memset(name, 0, MAX_NAME_LENGTH); memset(name, 0, MAX_NAME_LENGTH);
memset(chName, 0, MAX_NAME_LENGTH * 4); memset(chName, 0, MAX_NAME_LENGTH * 4);
memset(record, 0, sizeof(RECORDTYPE) * listLen);
initialized = true; initialized = true;
} }
uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId) { uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld"));
uint8_t pos = 0; uint8_t pos = 0;
for(; pos < listLen; pos++) { if(NULL != rec) {
if((assign[pos].ch == channel) && (assign[pos].fieldId == fieldId)) for(; pos < rec->length; pos++) {
if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
break; break;
} }
return (pos >= listLen) ? 0xff : pos; return (pos >= rec->length) ? 0xff : pos;
}
else
return 0xff;
} }
const char *getFieldName(uint8_t pos) { byteAssign_t *getByteAssign(uint8_t pos, record_t<> *rec) {
return &rec->assign[pos];
}
const char *getFieldName(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getFieldName")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getFieldName"));
return fields[assign[pos].fieldId]; if(NULL != rec)
return fields[rec->assign[pos].fieldId];
return notAvail;
} }
const char *getUnit(uint8_t pos) { const char *getUnit(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getUnit")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getUnit"));
return units[assign[pos].unitId]; if(NULL != rec)
return units[rec->assign[pos].unitId];
return notAvail;
} }
uint8_t getChannel(uint8_t pos) { uint8_t getChannel(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getChannel")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getChannel"));
return assign[pos].ch; if(NULL != rec)
return rec->assign[pos].ch;
return 0;
} }
void addValue(uint8_t pos, uint8_t buf[]) { void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:addValue")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:addValue"));
uint8_t cmd = getQueuedCmd(); if(NULL != rec) {
uint8_t ptr = assign[pos].start; uint8_t ptr = rec->assign[pos].start;
uint8_t end = ptr + assign[pos].num; uint8_t end = ptr + rec->assign[pos].num;
uint16_t div = assign[pos].div; uint16_t div = rec->assign[pos].div;
if(NULL != rec) {
if(CMD_CALC != div) { if(CMD_CALC != div) {
uint32_t val = 0; uint32_t val = 0;
do { do {
val <<= 8; val <<= 8;
val |= buf[ptr]; val |= buf[ptr];
} while(++ptr != end); } while(++ptr != end);
if ((RECORDTYPE)(div) > 1){ if ((REC_TYP)(div) > 1)
record[pos] = (RECORDTYPE)(val) / (RECORDTYPE)(div); rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div);
else
rec->record[pos] = (REC_TYP)(val);
} }
else {
record[pos] = (RECORDTYPE)(val);
} }
} if(rec == &recordMeas) {
if (cmd == RealTimeRunData_Debug) { DPRINTLN(DBG_VERBOSE, "add real time");
// get last alarm message index and save it in the inverter object // get last alarm message index and save it in the inverter object
if (getPosByChFld(0, FLD_ALARM_MES_ID) == pos){ if (getPosByChFld(0, FLD_EVT, rec) == pos){
if (alarmMesIndex < record[pos]){ if (alarmMesIndex < rec->record[pos]){
alarmMesIndex = record[pos]; alarmMesIndex = rec->record[pos];
//enqueCommand<InfoCommand>(AlarmUpdate); // What is the function of AlarmUpdate? //enqueCommand<InfoCommand>(AlarmUpdate); // What is the function of AlarmUpdate?
enqueCommand<InfoCommand>(AlarmData); enqueCommand<InfoCommand>(AlarmData);
} }
else { else {
alarmMesIndex = record[pos]; // no change alarmMesIndex = rec->record[pos]; // no change
} }
} }
} }
if (cmd == InverterDevInform_All) { else if (rec->assign == InfoAssignment) {
DPRINTLN(DBG_DEBUG, "add info");
// get at least the firmware version and save it to the inverter object // get at least the firmware version and save it to the inverter object
if (getPosByChFld(0, FLD_FW_VERSION) == pos){ if (getPosByChFld(0, FLD_FW_VERSION, rec) == pos){
fwVersion = record[pos]; fwVersion = rec->record[pos];
DPRINT(DBG_DEBUG, F("Inverter FW-Version: ") + String(fwVersion)); DPRINT(DBG_DEBUG, F("Inverter FW-Version: ") + String(fwVersion));
} }
} }
if (cmd == SystemConfigPara) { else if (rec->assign == SystemConfigParaAssignment) {
DPRINTLN(DBG_DEBUG, "add config");
// get at least the firmware version and save it to the inverter object // get at least the firmware version and save it to the inverter object
if (getPosByChFld(0, FLD_ACT_PWR_LIMIT) == pos){ if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){
actPowerLimit = record[pos]; actPowerLimit = rec->record[pos];
DPRINT(DBG_DEBUG, F("Inverter actual power limit: ") + String(actPowerLimit)); DPRINT(DBG_DEBUG, F("Inverter actual power limit: ") + String(actPowerLimit, 1));
}
} }
else if (rec->assign == AlarmDataAssignment) {
DPRINTLN(DBG_DEBUG, "add alarm");
if (getPosByChFld(0, FLD_LAST_ALARM_CODE, rec) == pos){
lastAlarmMsg = getAlarmStr(rec->record[pos]);
} }
if (cmd == AlarmData){
if (getPosByChFld(0, FLD_LAST_ALARM_CODE) == pos){
lastAlarmMsg = getAlarmStr(record[pos]);
} }
else
DPRINTLN(DBG_WARN, F("add with unknown assginment"));
} }
else
DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));
} }
RECORDTYPE getValue(uint8_t pos) { REC_TYP getValue(uint8_t pos, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getValue"));
return record[pos]; if(NULL == rec)
return 0;
return rec->record[pos];
} }
void doCalculations() { void doCalculations() {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:doCalculations")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:doCalculations"));
uint8_t cmd = getQueuedCmd(); record_t<> *rec = getRecordStruct(RealTimeRunData_Debug);
getAssignment(); for(uint8_t i = 0; i < rec->length; i++) {
if (cmd == RealTimeRunData_Debug){ if(CMD_CALC == rec->assign[i].div) {
for(uint8_t i = 0; i < listLen; i++) { rec->record[i] = calcFunctions<REC_TYP>[rec->assign[i].start].func(this, rec->assign[i].num);
if(CMD_CALC == assign[i].div) {
record[i] = calcFunctions<RECORDTYPE>[assign[i].start].func(this, assign[i].num);
} }
yield(); yield();
} }
} }
}
bool isAvailable(uint32_t timestamp) { bool isAvailable(uint32_t timestamp, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isAvailable")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isAvailable"));
return ((timestamp - ts) < INACT_THRES_SEC); return ((timestamp - rec->ts) < INACT_THRES_SEC);
} }
bool isProducing(uint32_t timestamp) { bool isProducing(uint32_t timestamp, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:isProducing"));
if(isAvailable(timestamp)) { if(isAvailable(timestamp, rec)) {
uint8_t pos = getPosByChFld(CH0, FLD_PAC); uint8_t pos = getPosByChFld(CH0, FLD_PAC, rec);
return (getValue(pos) > INACT_PWR_THRESH); return (getValue(pos, rec) > INACT_PWR_THRESH);
} }
return false; return false;
} }
uint32_t getLastTs(void) uint32_t getLastTs(record_t<> *rec) {
{
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getLastTs")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getLastTs"));
return ts; return rec->ts;
}
record_t<> *getRecordStruct(uint8_t cmd) {
switch (cmd) {
case RealTimeRunData_Debug: return &recordMeas;
case InverterDevInform_All: return &recordInfo;
case SystemConfigPara: return &recordConfig;
case AlarmData: return &recordAlarm;
default: break;
}
return NULL;
} }
void getAssignment() void initAssignment(record_t<> *rec, uint8_t cmd) {
{ DPRINTLN(DBG_VERBOSE, F("hmInverter.h:initAssignment"));
DPRINTLN(DBG_DEBUG, F("hmInverter.h:getAssignment")); rec->ts = 0;
// Default assignment; rec->length = 0;
if (INV_TYPE_1CH == type) switch (cmd) {
{ case RealTimeRunData_Debug:
listLen = (uint8_t)(HM1CH_LIST_LEN); if (INV_TYPE_1CH == type) {
assign = (byteAssign_t *)hm1chAssignment; rec->length = (uint8_t)(HM1CH_LIST_LEN);
rec->assign = (byteAssign_t *)hm1chAssignment;
channels = 1; channels = 1;
} }
else if (INV_TYPE_2CH == type) else if (INV_TYPE_2CH == type) {
{ rec->length = (uint8_t)(HM2CH_LIST_LEN);
listLen = (uint8_t)(HM2CH_LIST_LEN); rec->assign = (byteAssign_t *)hm2chAssignment;
assign = (byteAssign_t *)hm2chAssignment;
channels = 2; channels = 2;
} }
else if (INV_TYPE_4CH == type) else if (INV_TYPE_4CH == type) {
{ rec->length = (uint8_t)(HM4CH_LIST_LEN);
listLen = (uint8_t)(HM4CH_LIST_LEN); rec->assign = (byteAssign_t *)hm4chAssignment;
assign = (byteAssign_t *)hm4chAssignment;
channels = 4; channels = 4;
} }
else else {
{ rec->length = 0;
listLen = 0; rec->assign = NULL;
channels = 0; channels = 0;
assign = NULL;
} }
switch (getQueuedCmd()) {
case RealTimeRunData_Debug:
// Do nothing will use default
break; break;
case InverterDevInform_All: case InverterDevInform_All:
listLen = (uint8_t)(HMINFO_LIST_LEN); rec->length = (uint8_t)(HMINFO_LIST_LEN);
assign = (byteAssign_t *)InfoAssignment; rec->assign = (byteAssign_t *)InfoAssignment;
break; break;
case SystemConfigPara: case SystemConfigPara:
listLen = (uint8_t)(HMSYSTEM_LIST_LEN); rec->length = (uint8_t)(HMSYSTEM_LIST_LEN);
assign = (byteAssign_t *)SystemConfigParaAssignment; rec->assign = (byteAssign_t *)SystemConfigParaAssignment;
break; break;
case AlarmData: case AlarmData:
listLen = (uint8_t)(HMALARMDATA_LIST_LEN); rec->length = (uint8_t)(HMALARMDATA_LIST_LEN);
assign = (byteAssign_t *)AlarmDataAssignment; rec->assign = (byteAssign_t *)AlarmDataAssignment;
break; break;
default: default:
DPRINTLN(DBG_INFO, "Parser not implemented"); DPRINTLN(DBG_INFO, F("initAssignment: Parser not implemented"));
break; break;
} }
}
String getAlarmStr(u_int16_t alarmCode) if(0 != rec->length) {
{ rec->record = new REC_TYP[rec->length];
switch (alarmCode) memset(rec->record, 0, sizeof(REC_TYP) * rec->length);
{ }
case 1: }
return String(F("Inverter start"));
break; String getAlarmStr(u_int16_t alarmCode) {
case 2: switch (alarmCode) { // breaks are intentionally missing!
return String(F("DTU command failed")); case 1: return String(F("Inverter start"));
break; case 2: return String(F("DTU command failed"));
case 121: case 121: return String(F("Over temperature protection"));
return String(F("Over temperature protection")); case 125: return String(F("Grid configuration parameter error"));
break; case 126: return String(F("Software error code 126"));
case 125: case 127: return String(F("Firmware error"));
return String(F("Grid configuration parameter error")); case 128: return String(F("Software error code 128"));
break; case 129: return String(F("Software error code 129"));
case 126: case 130: return String(F("Offline"));
return String(F("Software error code 126")); case 141: return String(F("Grid overvoltage"));
break; case 142: return String(F("Average grid overvoltage"));
case 127: case 143: return String(F("Grid undervoltage"));
return String(F("Firmware error")); case 144: return String(F("Grid overfrequency"));
break; case 145: return String(F("Grid underfrequency"));
case 128: case 146: return String(F("Rapid grid frequency change"));
return String(F("Software error code 128")); case 147: return String(F("Power grid outage"));
break; case 148: return String(F("Grid disconnection"));
case 129: case 149: return String(F("Island detected"));
return String(F("Software error code 129")); case 205: return String(F("Input port 1 & 2 overvoltage"));
break; case 206: return String(F("Input port 3 & 4 overvoltage"));
case 130: case 207: return String(F("Input port 1 & 2 undervoltage"));
return String(F("Offline")); case 208: return String(F("Input port 3 & 4 undervoltage"));
break; case 209: return String(F("Port 1 no input"));
case 141: case 210: return String(F("Port 2 no input"));
return String(F("Grid overvoltage")); case 211: return String(F("Port 3 no input"));
break; case 212: return String(F("Port 4 no input"));
case 142: case 213: return String(F("PV-1 & PV-2 abnormal wiring"));
return String(F("Average grid overvoltage")); case 214: return String(F("PV-3 & PV-4 abnormal wiring"));
break; case 215: return String(F("PV-1 Input overvoltage"));
case 143: case 216: return String(F("PV-1 Input undervoltage"));
return String(F("Grid undervoltage")); case 217: return String(F("PV-2 Input overvoltage"));
break; case 218: return String(F("PV-2 Input undervoltage"));
case 144: case 219: return String(F("PV-3 Input overvoltage"));
return String(F("Grid overfrequency")); case 220: return String(F("PV-3 Input undervoltage"));
break; case 221: return String(F("PV-4 Input overvoltage"));
case 145: case 222: return String(F("PV-4 Input undervoltage"));
return String(F("Grid underfrequency")); case 301: return String(F("Hardware error code 301"));
break; case 302: return String(F("Hardware error code 302"));
case 146: case 303: return String(F("Hardware error code 303"));
return String(F("Rapid grid frequency change")); case 304: return String(F("Hardware error code 304"));
break; case 305: return String(F("Hardware error code 305"));
case 147: case 306: return String(F("Hardware error code 306"));
return String(F("Power grid outage")); case 307: return String(F("Hardware error code 307"));
break; case 308: return String(F("Hardware error code 308"));
case 148: case 309: return String(F("Hardware error code 309"));
return String(F("Grid disconnection")); case 310: return String(F("Hardware error code 310"));
break; case 311: return String(F("Hardware error code 311"));
case 149: case 312: return String(F("Hardware error code 312"));
return String(F("Island detected")); case 313: return String(F("Hardware error code 313"));
break; case 314: return String(F("Hardware error code 314"));
case 205: case 5041: return String(F("Error code-04 Port 1"));
return String(F("Input port 1 & 2 overvoltage")); case 5042: return String(F("Error code-04 Port 2"));
break; case 5043: return String(F("Error code-04 Port 3"));
case 206: case 5044: return String(F("Error code-04 Port 4"));
return String(F("Input port 3 & 4 overvoltage")); case 5051: return String(F("PV Input 1 Overvoltage/Undervoltage"));
break; case 5052: return String(F("PV Input 2 Overvoltage/Undervoltage"));
case 207: case 5053: return String(F("PV Input 3 Overvoltage/Undervoltage"));
return String(F("Input port 1 & 2 undervoltage")); case 5054: return String(F("PV Input 4 Overvoltage/Undervoltage"));
break; case 5060: return String(F("Abnormal bias"));
case 208: case 5070: return String(F("Over temperature protection"));
return String(F("Input port 3 & 4 undervoltage")); case 5080: return String(F("Grid Overvoltage/Undervoltage"));
break; case 5090: return String(F("Grid Overfrequency/Underfrequency"));
case 209: case 5100: return String(F("Island detected"));
return String(F("Port 1 no input")); case 5120: return String(F("EEPROM reading and writing error"));
break; case 5150: return String(F("10 min value grid overvoltage"));
case 210: case 5200: return String(F("Firmware error"));
return String(F("Port 2 no input")); case 8310: return String(F("Shut down"));
break; case 9000: return String(F("Microinverter is suspected of being stolen"));
case 211: default: return String(F("Unknown"));
return String(F("Port 3 no input"));
break;
case 212:
return String(F("Port 4 no input"));
break;
case 213:
return String(F("PV-1 & PV-2 abnormal wiring"));
break;
case 214:
return String(F("PV-3 & PV-4 abnormal wiring"));
break;
case 215:
return String(F("PV-1 Input overvoltage"));
break;
case 216:
return String(F("PV-1 Input undervoltage"));
break;
case 217:
return String(F("PV-2 Input overvoltage"));
break;
case 218:
return String(F("PV-2 Input undervoltage"));
break;
case 219:
return String(F("PV-3 Input overvoltage"));
break;
case 220:
return String(F("PV-3 Input undervoltage"));
break;
case 221:
return String(F("PV-4 Input overvoltage"));
break;
case 222:
return String(F("PV-4 Input undervoltage"));
break;
case 301:
return String(F("Hardware error code 301"));
break;
case 302:
return String(F("Hardware error code 302"));
break;
case 303:
return String(F("Hardware error code 303"));
break;
case 304:
return String(F("Hardware error code 304"));
break;
case 305:
return String(F("Hardware error code 305"));
break;
case 306:
return String(F("Hardware error code 306"));
break;
case 307:
return String(F("Hardware error code 307"));
break;
case 308:
return String(F("Hardware error code 308"));
break;
case 309:
return String(F("Hardware error code 309"));
break;
case 310:
return String(F("Hardware error code 310"));
break;
case 311:
return String(F("Hardware error code 311"));
break;
case 312:
return String(F("Hardware error code 312"));
break;
case 313:
return String(F("Hardware error code 313"));
break;
case 314:
return String(F("Hardware error code 314"));
break;
case 5041:
return String(F("Error code-04 Port 1"));
break;
case 5042:
return String(F("Error code-04 Port 2"));
break;
case 5043:
return String(F("Error code-04 Port 3"));
break;
case 5044:
return String(F("Error code-04 Port 4"));
break;
case 5051:
return String(F("PV Input 1 Overvoltage/Undervoltage"));
break;
case 5052:
return String(F("PV Input 2 Overvoltage/Undervoltage"));
break;
case 5053:
return String(F("PV Input 3 Overvoltage/Undervoltage"));
break;
case 5054:
return String(F("PV Input 4 Overvoltage/Undervoltage"));
break;
case 5060:
return String(F("Abnormal bias"));
break;
case 5070:
return String(F("Over temperature protection"));
break;
case 5080:
return String(F("Grid Overvoltage/Undervoltage"));
break;
case 5090:
return String(F("Grid Overfrequency/Underfrequency"));
break;
case 5100:
return String(F("Island detected"));
break;
case 5120:
return String(F("EEPROM reading and writing error"));
break;
case 5150:
return String(F("10 min value grid overvoltage"));
break;
case 5200:
return String(F("Firmware error"));
break;
case 8310:
return String(F("Shut down"));
break;
case 9000:
return String(F("Microinverter is suspected of being stolen"));
break;
default:
return String(F("Unknown"));
break;
} }
} }
@ -590,10 +489,11 @@ template<class T=float>
static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) { static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldTotalCh0")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldTotalCh0"));
if(NULL != iv) { if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
T yield = 0; T yield = 0;
for(uint8_t i = 1; i <= iv->channels; i++) { for(uint8_t i = 1; i <= iv->channels; i++) {
uint8_t pos = iv->getPosByChFld(i, FLD_YT); uint8_t pos = iv->getPosByChFld(i, FLD_YT, rec);
yield += iv->getValue(pos); yield += iv->getValue(pos, rec);
} }
return yield; return yield;
} }
@ -604,10 +504,11 @@ template<class T=float>
static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) { static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldDayCh0")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldDayCh0"));
if(NULL != iv) { if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
T yield = 0; T yield = 0;
for(uint8_t i = 1; i <= iv->channels; i++) { for(uint8_t i = 1; i <= iv->channels; i++) {
uint8_t pos = iv->getPosByChFld(i, FLD_YD); uint8_t pos = iv->getPosByChFld(i, FLD_YD, rec);
yield += iv->getValue(pos); yield += iv->getValue(pos, rec);
} }
return yield; return yield;
} }
@ -618,9 +519,10 @@ template<class T=float>
static T calcUdcCh(Inverter<> *iv, uint8_t arg0) { static T calcUdcCh(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcUdcCh")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcUdcCh"));
// arg0 = channel of source // arg0 = channel of source
for(uint8_t i = 0; i < iv->listLen; i++) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if((FLD_UDC == iv->assign[i].fieldId) && (arg0 == iv->assign[i].ch)) { for(uint8_t i = 0; i < rec->length; i++) {
return iv->getValue(i); if((FLD_UDC == rec->assign[i].fieldId) && (arg0 == rec->assign[i].ch)) {
return iv->getValue(i, rec);
} }
} }
@ -631,10 +533,11 @@ template<class T=float>
static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) { static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcPowerDcCh0")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcPowerDcCh0"));
if(NULL != iv) { if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
T dcPower = 0; T dcPower = 0;
for(uint8_t i = 1; i <= iv->channels; i++) { for(uint8_t i = 1; i <= iv->channels; i++) {
uint8_t pos = iv->getPosByChFld(i, FLD_PDC); uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec);
dcPower += iv->getValue(pos); dcPower += iv->getValue(pos, rec);
} }
return dcPower; return dcPower;
} }
@ -645,12 +548,13 @@ template<class T=float>
static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) { static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcEfficiencyCh0")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcEfficiencyCh0"));
if(NULL != iv) { if(NULL != iv) {
uint8_t pos = iv->getPosByChFld(CH0, FLD_PAC); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
T acPower = iv->getValue(pos); uint8_t pos = iv->getPosByChFld(CH0, FLD_PAC, rec);
T acPower = iv->getValue(pos, rec);
T dcPower = 0; T dcPower = 0;
for(uint8_t i = 1; i <= iv->channels; i++) { for(uint8_t i = 1; i <= iv->channels; i++) {
pos = iv->getPosByChFld(i, FLD_PDC); pos = iv->getPosByChFld(i, FLD_PDC, rec);
dcPower += iv->getValue(pos); dcPower += iv->getValue(pos, rec);
} }
if(dcPower > 0) if(dcPower > 0)
return acPower / dcPower * 100.0f; return acPower / dcPower * 100.0f;
@ -663,9 +567,10 @@ static T calcIrradiation(Inverter<> *iv, uint8_t arg0) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcIrradiation")); DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcIrradiation"));
// arg0 = channel // arg0 = channel
if(NULL != iv) { if(NULL != iv) {
uint8_t pos = iv->getPosByChFld(arg0, FLD_PDC); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
uint8_t pos = iv->getPosByChFld(arg0, FLD_PDC, rec);
if(iv->chMaxPwr[arg0-1] > 0) if(iv->chMaxPwr[arg0-1] > 0)
return iv->getValue(pos) / iv->chMaxPwr[arg0-1] * 100.0f; return iv->getValue(pos, rec) / iv->chMaxPwr[arg0-1] * 100.0f;
} }
return 0.0; return 0.0;
} }

79
tools/esp8266/hmRadio.h

@ -21,7 +21,7 @@
#define RF_CHANNELS 5 #define RF_CHANNELS 5
#define RF_LOOP_CNT 300 #define RF_LOOP_CNT 300
#define TX_REQ_INFO 0X15 #define TX_REQ_INFO 0x15
#define TX_REQ_DEVCONTROL 0x51 #define TX_REQ_DEVCONTROL 0x51
#define ALL_FRAMES 0x80 #define ALL_FRAMES 0x80
#define SINGLE_FRAME 0x81 #define SINGLE_FRAME 0x81
@ -54,7 +54,7 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// HM Radio class // HM Radio class
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
template <uint8_t CE_PIN, uint8_t CS_PIN, class BUFFER, uint64_t DTU_ID=DTU_RADIO_ID> template <uint8_t CE_PIN, uint8_t CS_PIN, class BUFFER>
class HmRadio { class HmRadio {
public: public:
HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) { HmRadio() : mNrf24(CE_PIN, CS_PIN, SPI_SPEED) {
@ -89,6 +89,25 @@ class HmRadio {
pinMode(config->pinIrq, INPUT_PULLUP); pinMode(config->pinIrq, INPUT_PULLUP);
mBufCtrl = ctrl; mBufCtrl = ctrl;
mSerialDebug = config->serialDebug;
uint32_t DTU_SN = 0x87654321;
uint32_t chipID = 0; // will be filled with last 3 bytes of MAC
#ifdef ESP32
uint64_t MAC = ESP.getEfuseMac();
chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF);
#else
chipID = ESP.getChipId();
#endif
if(chipID) {
DTU_SN = 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
for(int i = 0; i < 7; i++) {
DTU_SN |= (chipID % 10) << (i * 4);
chipID /= 10;
}
}
// change the byte order of the DTU serial number and append the required 0x01 at the end
DTU_RADIO_ID = ((uint64_t)(((DTU_SN >> 24) & 0xFF) | ((DTU_SN >> 8) & 0xFF00) | ((DTU_SN << 8) & 0xFF0000) | ((DTU_SN << 24) & 0xFF000000)) << 8) | 0x01;
mNrf24.begin(config->pinCe, config->pinCs); mNrf24.begin(config->pinCe, config->pinCs);
mNrf24.setRetries(0, 0); mNrf24.setRetries(0, 0);
@ -163,30 +182,31 @@ class HmRadio {
} }
void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) { void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) {
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendControlPacket")); DPRINTLN(DBG_INFO, F("sendControlPacket cmd: ") + String(cmd));
sendCmdPacket(invId, TX_REQ_DEVCONTROL, ALL_FRAMES, false); // 0x80 implementation as original DTU code sendCmdPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME, false);
int cnt = 0; uint8_t cnt = 0;
mTxBuf[10] = cmd; // cmd --> 0x0b => Type_ActivePowerContr, 0 on, 1 off, 2 restart, 12 reactive power, 13 power factor mTxBuf[10 + cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[10 + (++cnt)] = 0x00; mTxBuf[10 + cnt++] = 0x00;
if (cmd >= ActivePowerContr && cmd <= PFSet){ if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
mTxBuf[10 + (++cnt)] = ((data[0] * 10) >> 8) & 0xff; // power limit mTxBuf[10 + cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit
mTxBuf[10 + (++cnt)] = ((data[0] * 10) ) & 0xff; // power limit mTxBuf[10 + cnt++] = ((data[0] * 10) ) & 0xff; // power limit
mTxBuf[10 + (++cnt)] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings mTxBuf[10 + cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
mTxBuf[10 + (++cnt)] = ((data[1] ) ) & 0xff; // setting for persistens handling mTxBuf[10 + cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
} }
// crc control data // crc control data
uint16_t crc = Hoymiles::crc16(&mTxBuf[10], cnt+1); uint16_t crc = Ahoy::crc16(&mTxBuf[10], cnt);
mTxBuf[10 + (++cnt)] = (crc >> 8) & 0xff; mTxBuf[10 + cnt++] = (crc >> 8) & 0xff;
mTxBuf[10 + (++cnt)] = (crc ) & 0xff; mTxBuf[10 + cnt++] = (crc ) & 0xff;
// crc over all // crc over all
cnt +=1; mTxBuf[10 + cnt] = Ahoy::crc8(mTxBuf, 10 + cnt);
mTxBuf[10 + cnt] = Hoymiles::crc8(mTxBuf, 10 + cnt);
sendPacket(invId, mTxBuf, 10 + (++cnt), true); sendPacket(invId, mTxBuf, 10 + cnt + 1, true);
} }
void sendTimePacket(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId) { void sendTimePacket(uint64_t invId, uint8_t cmd, uint32_t ts, uint16_t alarmMesId) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendTimePacket")); DPRINTLN(DBG_INFO, F("sendTimePacket"));
sendCmdPacket(invId, TX_REQ_INFO, ALL_FRAMES, false); sendCmdPacket(invId, TX_REQ_INFO, ALL_FRAMES, false);
mTxBuf[10] = cmd; // cid mTxBuf[10] = cmd; // cid
mTxBuf[11] = 0x00; mTxBuf[11] = 0x00;
@ -194,33 +214,30 @@ class HmRadio {
if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) { if (cmd == RealTimeRunData_Debug || cmd == AlarmData ) {
mTxBuf[18] = (alarmMesId >> 8) & 0xff; mTxBuf[18] = (alarmMesId >> 8) & 0xff;
mTxBuf[19] = (alarmMesId ) & 0xff; mTxBuf[19] = (alarmMesId ) & 0xff;
} else {
mTxBuf[18] = 0x00;
mTxBuf[19] = 0x00;
} }
uint16_t crc = Hoymiles::crc16(&mTxBuf[10], 14); uint16_t crc = Ahoy::crc16(&mTxBuf[10], 14);
mTxBuf[24] = (crc >> 8) & 0xff; mTxBuf[24] = (crc >> 8) & 0xff;
mTxBuf[25] = (crc ) & 0xff; mTxBuf[25] = (crc ) & 0xff;
mTxBuf[26] = Hoymiles::crc8(mTxBuf, 26); mTxBuf[26] = Ahoy::crc8(mTxBuf, 26);
sendPacket(invId, mTxBuf, 27, true); sendPacket(invId, mTxBuf, 27, true);
} }
void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool calcCrc = true) { void sendCmdPacket(uint64_t invId, uint8_t mid, uint8_t pid, bool calcCrc = true) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:sendCmdPacket")); DPRINTLN(DBG_VERBOSE, F("sendCmdPacket, mid: ") + String(mid, HEX) + F(" pid: ") + String(pid, HEX));
memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE); memset(mTxBuf, 0, MAX_RF_PAYLOAD_SIZE);
mTxBuf[0] = mid; // message id mTxBuf[0] = mid; // message id
CP_U32_BigEndian(&mTxBuf[1], (invId >> 8)); CP_U32_BigEndian(&mTxBuf[1], (invId >> 8));
CP_U32_BigEndian(&mTxBuf[5], (DTU_ID >> 8)); CP_U32_BigEndian(&mTxBuf[5], (DTU_RADIO_ID >> 8));
mTxBuf[9] = pid; mTxBuf[9] = pid;
if(calcCrc) { if(calcCrc) {
mTxBuf[10] = Hoymiles::crc8(mTxBuf, 10); mTxBuf[10] = Ahoy::crc8(mTxBuf, 10);
sendPacket(invId, mTxBuf, 11, false); sendPacket(invId, mTxBuf, 11, false);
} }
} }
bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t rxCh) { bool checkPaketCrc(uint8_t buf[], uint8_t *len, uint8_t rxCh) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:checkPaketCrc")); //DPRINTLN(DBG_INFO, F("hmRadio.h:checkPaketCrc"));
*len = (buf[0] >> 2); *len = (buf[0] >> 2);
if(*len > (MAX_RF_PAYLOAD_SIZE - 2)) if(*len > (MAX_RF_PAYLOAD_SIZE - 2))
*len = MAX_RF_PAYLOAD_SIZE - 2; *len = MAX_RF_PAYLOAD_SIZE - 2;
@ -228,7 +245,7 @@ class HmRadio {
buf[i-1] = (buf[i] << 1) | (buf[i+1] >> 7); buf[i-1] = (buf[i] << 1) | (buf[i+1] >> 7);
} }
uint8_t crc = Hoymiles::crc8(buf, *len-1); uint8_t crc = Ahoy::crc8(buf, *len-1);
bool valid = (crc == buf[*len-1]); bool valid = (crc == buf[*len-1]);
return valid; return valid;
@ -236,8 +253,6 @@ class HmRadio {
bool switchRxCh(uint16_t addLoop = 0) { bool switchRxCh(uint16_t addLoop = 0) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:switchRxCh")); //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:switchRxCh"));
//DPRINTLN(DBG_VERBOSE, F("R"));
mRxLoopCnt += addLoop; mRxLoopCnt += addLoop;
if(mRxLoopCnt != 0) { if(mRxLoopCnt != 0) {
mRxLoopCnt--; mRxLoopCnt--;
@ -325,6 +340,8 @@ class HmRadio {
return mRfChLst[mRxChIdx]; return mRfChLst[mRxChIdx];
} }
uint64_t DTU_RADIO_ID;
uint8_t mTxCh; uint8_t mTxCh;
uint8_t mTxChIdx; uint8_t mTxChIdx;

10
tools/esp8266/hmSystem.h

@ -63,15 +63,9 @@ class HmSystem {
uint8_t len = (uint8_t)strlen(name); uint8_t len = (uint8_t)strlen(name);
strncpy(p->name, name, (len > MAX_NAME_LENGTH) ? MAX_NAME_LENGTH : len); strncpy(p->name, name, (len > MAX_NAME_LENGTH) ? MAX_NAME_LENGTH : len);
if(NULL == p->assign) {
DPRINT(DBG_ERROR, F("no assignment for type found!"));
return NULL;
}
else {
mNumInv ++; mNumInv ++;
return p; return p;
} }
}
INVERTERTYPE *findInverter(uint8_t buf[]) { INVERTERTYPE *findInverter(uint8_t buf[]) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter")); DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter"));
@ -89,7 +83,9 @@ class HmSystem {
INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) { INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos"));
if((mInverter[pos].initialized && mInverter[pos].serial.u64 != 0ULL) || false == check) if(pos >= MAX_INVERTER)
return NULL;
else if((mInverter[pos].initialized && mInverter[pos].serial.u64 != 0ULL) || false == check)
return &mInverter[pos]; return &mInverter[pos];
else else
return NULL; return NULL;

79
tools/esp8266/html/api.js

@ -0,0 +1,79 @@
function toggle(id, hide) {
var elm = document.getElementById(id);
if(hide) {
if(!elm.classList.contains("hide"))
elm.classList.add("hide");
}
else
elm.classList.remove('hide');
}
function getAjax(url, ptr, method="GET", json=null) {
var xhr = new XMLHttpRequest();
if(xhr != null) {
xhr.open(method, url, true);
xhr.onreadystatechange = p;
if("POST" == method)
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.send(json);
}
function p() {
if(xhr.readyState == 4) {
if(null != xhr.responseText)
ptr(JSON.parse(xhr.responseText));
}
}
}
function des(val) {
e = document.createElement('p');
e.classList.add("subdes");
e.innerHTML = val;
return e;
}
function lbl(htmlfor, val, cl=null, id=null) {
e = document.createElement('label');
e.htmlFor = htmlfor;
e.innerHTML = val;
if(null != cl) e.classList.add(...cl);
if(null != id) e.id = id;
return e;
}
function inp(name, val, max=32, cl=["text"], id=null) {
e = document.createElement('input');
e.classList.add(...cl);
e.name = name;
e.value = val;
e.maxLength = max;
if(null != id) e.id = id;
return e;
}
function sel(name, opt, selId) {
e = document.createElement('select');
e.name = name;
for(it of opt) {
o = document.createElement('option');
o.value = it[0];
o.innerHTML = it[1];
if(it[0] == selId)
o.selected = true;
e.appendChild(o);
}
return e;
}
function div(cl) {
e = document.createElement('div');
e.classList.add(...cl);
return e;
}
function span(val, cl) {
e = document.createElement('span');
e.innerHTML = val;
e.classList.add(...cl);
return e;
}

47
tools/esp8266/html/convert.py

@ -1,17 +1,17 @@
import re import re
import sys
import os import os
import gzip
from pathlib import Path from pathlib import Path
def convert2Header(inFile): def convert2Header(inFile, compress):
fileType = inFile.split(".")[1] fileType = inFile.split(".")[1]
define = inFile.split(".")[0].upper() define = inFile.split(".")[0].upper()
define2 = inFile.split(".")[1].upper() define2 = inFile.split(".")[1].upper()
inFileVarName = inFile.replace(".", "_") inFileVarName = inFile.replace(".", "_")
print(inFile + ", compress: " + str(compress))
if os.getcwd()[-4:] != "html": if os.getcwd()[-4:] != "html":
print("ok")
outName = "html/" + "h/" + inFileVarName + ".h" outName = "html/" + "h/" + inFileVarName + ".h"
inFile = "html/" + inFile inFile = "html/" + inFile
Path("html/h").mkdir(exist_ok=True) Path("html/h").mkdir(exist_ok=True)
@ -20,23 +20,52 @@ def convert2Header(inFile):
Path("h").mkdir(exist_ok=True) Path("h").mkdir(exist_ok=True)
f = open(inFile, "r") f = open(inFile, "r")
data = f.read().replace('\n', '') data = f.read()
f.close() f.close()
if fileType == "html": if fileType == "html":
if False == compress:
data = data.replace('\n', '')
data = re.sub(r"\>\s+\<", '><', data) # whitespaces between xml tags data = re.sub(r"\>\s+\<", '><', data) # whitespaces between xml tags
data = re.sub(r"(\;|\}|\>|\{)\s+", r'\1', data) # whitespaces inner javascript data = re.sub(r"(\r\n|\r|\n)(\s+|\s?)", '', data) # whitespaces inner javascript
length = len(data) # get unescaped length
if False == compress:
data = re.sub(r"\"", '\\\"', data) # escape quotation marks
elif fileType == "js":
#data = re.sub(r"(\r\n|\r|\n)(\s+|\s?)", '', data) # whitespaces inner javascript
#data = re.sub(r"\s?(\=|\!\=|\{|,)+\s?", r'\1', data) # whitespaces inner javascript
length = len(data) # get unescaped length
if False == compress:
data = re.sub(r"\"", '\\\"', data) # escape quotation marks data = re.sub(r"\"", '\\\"', data) # escape quotation marks
else: else:
data = data.replace('\n', '')
data = re.sub(r"(\;|\}|\:|\{)\s+", r'\1', data) # whitespaces inner css data = re.sub(r"(\;|\}|\:|\{)\s+", r'\1', data) # whitespaces inner css
length = len(data) # get unescaped length # get unescaped length
f = open(outName, "w") f = open(outName, "w")
f.write("#ifndef __{}_{}_H__\n".format(define, define2)) f.write("#ifndef __{}_{}_H__\n".format(define, define2))
f.write("#define __{}_{}_H__\n".format(define, define2)) f.write("#define __{}_{}_H__\n".format(define, define2))
if compress:
zipped = gzip.compress(bytes(data, 'utf-8'))
zippedStr = ""
for i in range(len(zipped)):
zippedStr += "0x{:02x}".format(zipped[i]) #hex(zipped[i])
if (i + 1) != len(zipped):
zippedStr += ", "
if (i + 1) % 16 == 0 and i != 0:
zippedStr += "\n"
f.write("#define {}_len {}\n".format(inFileVarName, len(zipped)))
f.write("const uint8_t {}[] PROGMEM = {{\n{}}};\n".format(inFileVarName, zippedStr))
else:
f.write("const char {}[] PROGMEM = \"{}\";\n".format(inFileVarName, data)) f.write("const char {}[] PROGMEM = \"{}\";\n".format(inFileVarName, data))
f.write("const uint32_t {}_len = {};\n".format(inFileVarName, length))
f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) f.write("#endif /*__{}_{}_H__*/\n".format(define, define2))
f.close() f.close()
convert2Header("index.html") convert2Header("index.html", True)
convert2Header("setup.html") convert2Header("setup.html", True)
convert2Header("visualization.html") convert2Header("visualization.html", True)
convert2Header("style.css") convert2Header("update.html", True)
convert2Header("serial.html", True)
convert2Header("style.css", True)
convert2Header("api.js", True)

100
tools/esp8266/html/h/favicon_ico_gz.h

@ -0,0 +1,100 @@
#ifndef __FAVICON_ICO_GZ_H__
#define __FAVICON_ICO_GZ_H__
#define favicon_ico_gz_len 1533
const uint8_t favicon_ico_gz[] PROGMEM = {0x1f, 0x8b, 0x08, 0x08, 0xf2, 0xc5, 0xd5, 0x62, 0x04, 0x00, 0x66, 0x61, 0x76, 0x69, 0x63, 0x6f,
0x6e, 0x2e, 0x69, 0x63, 0x6f, 0x00, 0xed, 0x5c, 0x49, 0x68, 0x13, 0x51, 0x18, 0xfe, 0x62, 0xa3,
0x51, 0x28, 0xd6, 0x83, 0x82, 0xa0, 0x98, 0xb8, 0x1c, 0xbc, 0x59, 0x11, 0x5c, 0x50, 0xac, 0x88,
0x8a, 0xb8, 0xdd, 0x3c, 0x89, 0xd0, 0x93, 0x7a, 0x53, 0x51, 0x9b, 0x80, 0x4b, 0x46, 0xad, 0xfb,
0xd2, 0xb4, 0x2e, 0xb8, 0xa3, 0xc6, 0xba, 0xe1, 0x02, 0xae, 0xad, 0x0a, 0x26, 0x3d, 0xe8, 0xc5,
0x83, 0x57, 0x31, 0x2d, 0xc1, 0x8b, 0xb7, 0x92, 0x63, 0x0e, 0xa1, 0xcf, 0xff, 0xcf, 0xbc, 0xc9,
0x32, 0xa4, 0x66, 0xcf, 0x4b, 0xf3, 0xfa, 0xc3, 0xc7, 0x97, 0xcc, 0xcc, 0xcb, 0xf7, 0xbe, 0x6f,
0x26, 0x6f, 0x26, 0xf3, 0x92, 0x00, 0x0e, 0x34, 0x61, 0xda, 0x34, 0x66, 0x0f, 0xf6, 0x38, 0x81,
0xa5, 0x00, 0x3c, 0x1e, 0xf3, 0xf9, 0x53, 0x5a, 0x7e, 0x8f, 0x96, 0xad, 0x59, 0x63, 0x3e, 0x5f,
0xb8, 0x16, 0xd8, 0x30, 0x03, 0x58, 0x48, 0xdb, 0xd0, 0x2a, 0x5a, 0x62, 0x2e, 0x1f, 0xad, 0x06,
0xc3, 0x5d, 0xc6, 0x60, 0x38, 0x20, 0x08, 0x5e, 0xf1, 0x08, 0x77, 0x09, 0x42, 0xf4, 0xe2, 0xd7,
0x67, 0x2f, 0x56, 0xf5, 0x7b, 0x21, 0x18, 0xfc, 0xb8, 0x63, 0x0b, 0x7e, 0x11, 0x84, 0x77, 0x0b,
0xee, 0x2e, 0x9a, 0x03, 0x6f, 0xab, 0x1b, 0x82, 0x31, 0x38, 0x10, 0x38, 0xce, 0xaf, 0x51, 0x72,
0x7b, 0xd2, 0xe6, 0xf6, 0xbf, 0xbf, 0xf6, 0xcc, 0x2e, 0xbd, 0x7d, 0xf2, 0x35, 0x76, 0x88, 0x20,
0xf6, 0x96, 0xde, 0x3e, 0xe0, 0xa5, 0x76, 0xad, 0xd4, 0xfe, 0x64, 0xc9, 0xed, 0xa9, 0xff, 0xd4,
0xd6, 0x4f, 0x68, 0x2f, 0xb9, 0x3d, 0xf5, 0x9f, 0x70, 0xb8, 0xaf, 0x03, 0x4b, 0x4b, 0x6e, 0x4f,
0xfd, 0xe7, 0x7d, 0x4b, 0x6d, 0x4f, 0x96, 0xd3, 0xde, 0x7a, 0x8d, 0x72, 0xda, 0x73, 0x7d, 0xdb,
0x8f, 0x29, 0x45, 0xb5, 0x7f, 0x84, 0x4e, 0xfb, 0x71, 0xda, 0xb1, 0x19, 0x9d, 0x05, 0xb5, 0xef,
0xc5, 0xa5, 0xd1, 0x8e, 0x75, 0xef, 0x56, 0x9c, 0xfa, 0x6f, 0xfb, 0x87, 0x58, 0x86, 0x3c, 0x75,
0x70, 0x13, 0x96, 0xe5, 0x6a, 0x1f, 0x19, 0xe8, 0x6a, 0x47, 0x81, 0xd5, 0xea, 0x41, 0x7b, 0x56,
0xfb, 0x50, 0xe0, 0x72, 0xae, 0xed, 0xbe, 0x1a, 0x98, 0xcc, 0xc8, 0xf9, 0x1a, 0x73, 0x70, 0x9a,
0xda, 0x8f, 0xda, 0xf6, 0xb9, 0x81, 0x49, 0xfd, 0x3e, 0xbc, 0xa5, 0xfd, 0xd7, 0xcf, 0xf9, 0x8f,
0xf6, 0x1a, 0xff, 0xeb, 0xe7, 0x04, 0x42, 0x13, 0xc3, 0x20, 0x84, 0x00, 0x57, 0x14, 0x68, 0x89,
0x01, 0xee, 0x38, 0xb0, 0xfa, 0x08, 0x70, 0x64, 0xb5, 0x39, 0xce, 0x78, 0x08, 0x6b, 0x0a, 0x1f,
0x67, 0x2c, 0x0c, 0xf3, 0x72, 0xf1, 0x18, 0xab, 0xe4, 0x3e, 0x4f, 0x23, 0x88, 0x37, 0xbc, 0xae,
0xcf, 0x07, 0xaf, 0xf9, 0x1e, 0x48, 0x83, 0x97, 0xf1, 0xba, 0x43, 0x5b, 0xf0, 0x86, 0x8f, 0x87,
0x4c, 0xd0, 0xfe, 0x5d, 0xc5, 0xeb, 0x28, 0x97, 0x61, 0x33, 0xdf, 0x34, 0x78, 0x39, 0x8f, 0x55,
0xaa, 0xf4, 0x23, 0x03, 0x81, 0x53, 0x56, 0x1f, 0x54, 0xe8, 0xb3, 0xe6, 0x50, 0x28, 0x70, 0xde,
0xea, 0x83, 0x0a, 0xfd, 0x64, 0x1f, 0xc2, 0x81, 0x93, 0xbc, 0x0d, 0xf1, 0x01, 0x15, 0xfa, 0x49,
0x0c, 0x74, 0x9d, 0xe3, 0xed, 0x94, 0xe9, 0x13, 0x78, 0x1f, 0x24, 0xf5, 0x7b, 0xb1, 0x54, 0x89,
0x3e, 0xef, 0x7f, 0x59, 0x7c, 0xce, 0x53, 0xa5, 0x2f, 0x7a, 0xe0, 0x92, 0x7d, 0x38, 0xad, 0x44,
0x9f, 0xf6, 0x3f, 0x9f, 0xb3, 0x65, 0x1f, 0x3a, 0x95, 0xe8, 0x9b, 0xbe, 0x8f, 0x59, 0x7d, 0x50,
0xa4, 0x9f, 0xea, 0x03, 0xe9, 0xaf, 0x57, 0xa4, 0x9f, 0xea, 0x83, 0x42, 0x7d, 0xbe, 0x7e, 0xd9,
0xcf, 0xeb, 0x3e, 0xf9, 0xd0, 0xae, 0x44, 0x9f, 0xf7, 0xbf, 0x2c, 0x3a, 0x87, 0x9f, 0x56, 0xa9,
0x6f, 0xf5, 0x41, 0xa5, 0xbe, 0xbc, 0x0e, 0x3d, 0xa3, 0x52, 0xdf, 0xea, 0x83, 0x4a, 0x7d, 0xae,
0x0f, 0x87, 0x30, 0x5f, 0xa5, 0x3e, 0x57, 0x0d, 0xf4, 0x8d, 0x7c, 0xfa, 0xa4, 0x67, 0x54, 0x49,
0xff, 0x86, 0x10, 0x74, 0x29, 0x9b, 0xbf, 0x1c, 0xa4, 0xd9, 0x5d, 0x51, 0xfd, 0x20, 0x6e, 0x16,
0xa8, 0x9d, 0xd5, 0x87, 0x8a, 0xe8, 0x17, 0xaf, 0x9d, 0xea, 0x03, 0xe9, 0xf6, 0x94, 0xa9, 0x7f,
0xab, 0x44, 0xed, 0xac, 0x3e, 0x94, 0xa4, 0xff, 0x1c, 0xcd, 0x65, 0x6a, 0xa7, 0xfa, 0x60, 0x6c,
0x47, 0x73, 0xb1, 0xfa, 0xd5, 0xa8, 0x42, 0xf4, 0x23, 0xe1, 0xc0, 0x15, 0x54, 0xa9, 0x16, 0xbb,
0x71, 0xe5, 0x7f, 0xfa, 0x91, 0x50, 0xf7, 0x6d, 0x21, 0x8c, 0x09, 0xa8, 0x5e, 0x39, 0x48, 0xf3,
0x6a, 0x86, 0x7e, 0x59, 0xda, 0xfd, 0x07, 0x31, 0x97, 0x81, 0xe2, 0x2a, 0xdd, 0x87, 0xb4, 0xfe,
0x9d, 0x62, 0xb5, 0xe9, 0xde, 0xcb, 0x1c, 0x3a, 0xef, 0x0c, 0xf5, 0x77, 0xe0, 0xcf, 0x17, 0x1f,
0xe6, 0xa1, 0xb8, 0xe2, 0x3e, 0x5c, 0x23, 0x94, 0xa4, 0xfd, 0xd1, 0x07, 0x0f, 0x5d, 0x03, 0x45,
0xad, 0x73, 0x3f, 0x3f, 0xe6, 0x65, 0x28, 0xae, 0x92, 0x7d, 0x70, 0x4e, 0x9e, 0xe6, 0xe9, 0x24,
0xac, 0x23, 0x2c, 0x20, 0x4c, 0x27, 0x34, 0x13, 0x78, 0xf9, 0x24, 0xc2, 0x44, 0x7e, 0x7c, 0x96,
0xb0, 0x91, 0xb0, 0x90, 0x30, 0xd3, 0x5c, 0xe7, 0x6c, 0x26, 0x4c, 0x25, 0xb4, 0x30, 0x02, 0x84,
0x9f, 0x84, 0x58, 0x1a, 0xee, 0xb8, 0xc7, 0xd9, 0x96, 0xf0, 0x38, 0xfd, 0x23, 0x1e, 0xa7, 0x10,
0x1e, 0x97, 0x10, 0x51, 0x0b, 0x2d, 0x23, 0xfe, 0x98, 0x3b, 0xd1, 0x16, 0x6f, 0x8b, 0xbb, 0x13,
0x0b, 0xe5, 0x3d, 0x0a, 0xa3, 0xf8, 0xfb, 0x14, 0xb9, 0x3e, 0xbb, 0x45, 0xac, 0xed, 0xe5, 0x67,
0x38, 0x2f, 0x8f, 0xa1, 0x79, 0xd1, 0x8b, 0x5d, 0xb6, 0xeb, 0x89, 0x61, 0xce, 0x35, 0x0f, 0x86,
0x6d, 0xf7, 0xc1, 0x76, 0xf1, 0x58, 0x9b, 0x17, 0x9b, 0xe1, 0xb5, 0x8d, 0x09, 0x91, 0xf4, 0xfb,
0x21, 0x8d, 0x02, 0xee, 0x93, 0x68, 0xe7, 0x3f, 0x6f, 0x06, 0x1a, 0xf8, 0xe7, 0x1a, 0x0a, 0x75,
0x9d, 0xd0, 0xcd, 0xff, 0xe0, 0x40, 0xf7, 0xa5, 0xbc, 0x19, 0x34, 0xb2, 0x7f, 0xf3, 0xda, 0xe0,
0xa2, 0x3d, 0x03, 0x9d, 0xfc, 0xe7, 0xca, 0x80, 0xef, 0x5b, 0xea, 0xe4, 0xdf, 0xcc, 0xa0, 0xeb,
0x82, 0x3d, 0x03, 0x9d, 0xfc, 0xe7, 0xca, 0x20, 0x12, 0xee, 0xee, 0xd4, 0xc9, 0x3f, 0x21, 0x35,
0x77, 0x91, 0xce, 0x20, 0xb0, 0x5b, 0x27, 0xff, 0xe9, 0xb9, 0x93, 0x74, 0xe9, 0xe6, 0xdf, 0xca,
0x40, 0x67, 0xff, 0xd6, 0xdc, 0x91, 0xd6, 0xfe, 0x93, 0xe8, 0xf6, 0xdb, 0xfc, 0xb7, 0xe7, 0xf5,
0xde, 0x48, 0xfe, 0x6d, 0xe3, 0xbf, 0x35, 0x7f, 0xa5, 0xb3, 0xff, 0x82, 0x32, 0x68, 0x70, 0xff,
0x32, 0x83, 0x33, 0xba, 0xf9, 0x17, 0x0f, 0x30, 0x2b, 0xeb, 0x79, 0x10, 0x97, 0xb5, 0xf2, 0x4f,
0xe3, 0x3f, 0xc1, 0x6f, 0xcb, 0xe0, 0xac, 0x56, 0xfe, 0xe5, 0xdc, 0xad, 0x3d, 0x03, 0xcd, 0xfc,
0x67, 0x65, 0xc0, 0xf3, 0x0a, 0xf4, 0xbc, 0x5b, 0x33, 0xff, 0x3c, 0x97, 0x74, 0xd4, 0x9e, 0x81,
0x56, 0xfe, 0x73, 0x67, 0xd0, 0xa3, 0x95, 0x7f, 0x13, 0x87, 0x6d, 0x19, 0xcc, 0xd6, 0xcc, 0x3f,
0x1f, 0xf3, 0x47, 0x32, 0xb7, 0xd5, 0xce, 0xbf, 0x3c, 0x0e, 0x34, 0xf7, 0x9f, 0xcc, 0x40, 0x73,
0xff, 0x3c, 0x26, 0x6e, 0xb3, 0xda, 0xc8, 0xef, 0x57, 0x5d, 0xd1, 0xca, 0xbf, 0x6d, 0xfc, 0x17,
0x34, 0x15, 0xd9, 0xe7, 0xc5, 0x55, 0x5d, 0xfd, 0x17, 0x98, 0x41, 0x43, 0xfb, 0xb7, 0x32, 0xa0,
0xef, 0xd8, 0x5d, 0xd3, 0xd5, 0x7f, 0x66, 0x06, 0xba, 0xfa, 0xcf, 0xcc, 0x40, 0x57, 0xff, 0x56,
0x06, 0xe4, 0xf9, 0xba, 0xae, 0xfe, 0x33, 0x33, 0xd0, 0xd5, 0x7f, 0x66, 0x06, 0xba, 0xfa, 0xcf,
0x18, 0x0f, 0x4e, 0xeb, 0xea, 0xdf, 0xaa, 0x71, 0xff, 0xe3, 0xfe, 0x1b, 0xc4, 0xff, 0x4e, 0x94,
0x59, 0x1d, 0x5b, 0xb1, 0x33, 0xaf, 0xf7, 0x7a, 0xf4, 0x1f, 0xc4, 0x13, 0xf1, 0x9c, 0x7e, 0x4e,
0x5a, 0x66, 0x19, 0x06, 0x26, 0x50, 0x06, 0xf7, 0xc7, 0x94, 0xff, 0x0a, 0x79, 0x2f, 0x2a, 0x83,
0xfa, 0xf1, 0xff, 0x54, 0x7c, 0x85, 0x13, 0x15, 0x2e, 0xce, 0x80, 0x7e, 0x83, 0xf2, 0xa0, 0xce,
0xfd, 0x57, 0xc5, 0xbb, 0x55, 0xdb, 0xb7, 0xa3, 0x89, 0x33, 0xa8, 0x53, 0xff, 0xcf, 0xaa, 0xe9,
0x3d, 0x33, 0x03, 0xf2, 0xfb, 0xb0, 0xce, 0xfc, 0xd7, 0xc4, 0xbb, 0x3d, 0x83, 0x3a, 0xf1, 0x5f,
0x53, 0xef, 0x59, 0xef, 0x85, 0xad, 0x08, 0x2a, 0xf6, 0xff, 0x5c, 0x85, 0x77, 0x7b, 0x06, 0x8a,
0xfc, 0x2b, 0xf5, 0x6e, 0xcf, 0xa0, 0xa6, 0xfe, 0x83, 0x58, 0x54, 0xc9, 0xf3, 0x7b, 0xb9, 0xc5,
0x19, 0xd0, 0xff, 0x9c, 0x2c, 0xaa, 0x95, 0xff, 0xb1, 0x50, 0xe3, 0xfe, 0xc7, 0xfd, 0xb7, 0x56,
0xc8, 0xff, 0x50, 0xb8, 0xeb, 0x3e, 0xc6, 0x58, 0xd1, 0x7f, 0xfd, 0xdc, 0x6f, 0xad, 0x8c, 0xff,
0x17, 0x3f, 0x7e, 0xdc, 0x9c, 0x88, 0x31, 0x56, 0xdb, 0xe9, 0x2f, 0x8c, 0xc8, 0x5f, 0x6f, 0x99,
0xfe, 0xc7, 0xa4, 0xf7, 0x62, 0x32, 0x68, 0x54, 0xef, 0xb6, 0x0c, 0x1e, 0x17, 0xe9, 0xff, 0x65,
0x23, 0x78, 0x2f, 0x24, 0x03, 0x95, 0xde, 0x69, 0x1e, 0xf8, 0x14, 0x03, 0x35, 0x28, 0xce, 0x60,
0x91, 0x1b, 0x4f, 0x6c, 0xfe, 0xd5, 0x79, 0xf7, 0xe1, 0x44, 0x6a, 0xce, 0xab, 0x03, 0xe7, 0x51,
0x83, 0xca, 0x91, 0x41, 0xe6, 0x77, 0xfc, 0xdf, 0xfd, 0xfe, 0xd0, 0xe3, 0x42, 0x0d, 0xea, 0x93,
0x17, 0xc7, 0xed, 0xf3, 0xbe, 0x74, 0x1c, 0x9c, 0x43, 0x0d, 0x6a, 0xc9, 0x12, 0x4c, 0x24, 0xdf,
0xaf, 0x6c, 0xfe, 0xdf, 0xab, 0xf4, 0xae, 0x28, 0x83, 0xd7, 0x84, 0xba, 0xf1, 0x6e, 0x81, 0xb7,
0x41, 0x0d, 0xca, 0xca, 0x00, 0xcb, 0x11, 0x75, 0xd1, 0xd7, 0x14, 0x0d, 0xc9, 0x21, 0xc9, 0x51,
0xc9, 0xfb, 0x24, 0x2f, 0x97, 0x3c, 0x5b, 0xf2, 0x54, 0xc9, 0x2e, 0xc9, 0x4d, 0x7d, 0x26, 0x3b,
0xe2, 0x26, 0xc3, 0x62, 0x9f, 0xe4, 0xe5, 0x92, 0x57, 0x49, 0x5e, 0x2d, 0xb9, 0x4d, 0xf2, 0x4a,
0xc3, 0xe4, 0x15, 0x21, 0xb9, 0x7d, 0x54, 0x72, 0xbb, 0xe4, 0x56, 0xc9, 0x33, 0x25, 0x37, 0x4b,
0x76, 0x49, 0x6e, 0x92, 0xec, 0xb0, 0xf4, 0xec, 0x1c, 0x93, 0x1c, 0x97, 0x9c, 0x90, 0x3c, 0x22,
0x59, 0x58, 0x7c, 0x46, 0xf2, 0x77, 0xc9, 0x7f, 0x25, 0x8b, 0x82, 0xd8, 0x41, 0x7f, 0x3b, 0x91,
0xec, 0x8f, 0x10, 0x21, 0x66, 0xfe, 0x67, 0x06, 0xe6, 0x16, 0x21, 0x62, 0xcc, 0x6e, 0x21, 0xe2,
0xcc, 0x6d, 0x42, 0x24, 0x98, 0xfd, 0x42, 0x8c, 0x30, 0x0b, 0x2a, 0xf6, 0xcf, 0x7c, 0x87, 0x72,
0x61, 0x4e, 0xe4, 0x67, 0x3f, 0xf3, 0x08, 0x7d, 0x28, 0x4f, 0x16, 0x1c, 0x26, 0x1b, 0x4d, 0xb4,
0x90, 0x56, 0x85, 0x5c, 0xb4, 0x11, 0x6d, 0x1a, 0x6d, 0x21, 0x55, 0x52, 0x8e, 0x31, 0xf8, 0x31,
0x2f, 0xe3, 0x75, 0xff, 0x00, 0xd3, 0x39, 0x74, 0x2c, 0x6e, 0x57, 0x00, 0x00};
#endif /*__FAVICON_ICO_GZ_H__*/

139
tools/esp8266/html/index.html

@ -1,72 +1,127 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Index - {DEVICE}</title> <title>Index</title>
<link rel="stylesheet" type="text/css" href="style.css"/> <link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript"> <script type="text/javascript" src="api.js"></script>
getAjax('/uptime', 'uptime');
getAjax('/cmdstat', 'cmds');
window.setInterval("getAjax('/uptime', 'uptime')", {JS_TS});
window.setInterval("getAjax('/cmdstat', 'cmds')", {JS_TS});
function getAjax(url, resid) {
var http = null;
http = new XMLHttpRequest();
if(http != null) {
http.open("GET", url, true);
http.onreadystatechange = print;
http.send(null);
}
function print() {
if(http.readyState == 4) {
document.getElementById(resid).innerHTML = http.responseText;
}
}
}
function getInverterInfo(data){
var http = null;
http = new XMLHttpRequest();
if(http != null) {
http.open("POST", "/api");
http.setRequestHeader("Accept", "application/json");
http.setRequestHeader("Content-Type", "application/json");
http.send(data);
}
}
</script>
</head> </head>
<body> <body>
<h1>AHOY - {DEVICE}</h1> <h1>AHOY</h1>
<div id="content" class="content"> <div id="content" class="content">
<p> <p>
<a href="/visualization">Visualization</a><br/> <a href="/live">Visualization</a><br/>
<br/> <br/>
<a href="/setup">Setup</a><br/> <a href="/setup">Setup</a><br/>
<a href="/serial">Webserial & Commands</a><br/>
</p> </p>
<p><span class="des">Uptime: </span><span id="uptime"></span></p> <p><span class="des">Uptime: </span><span id="uptime"></span></p>
<p><span class="des">Statistics: </span><pre id="cmds"></pre></p> <p><span class="des">ESP-Time: </span><span id="date"></span></p>
<p>Every {TS}seconds the values are updated</p> <p><span class="des">RSSI: </span><span id="wifi_rssi"></span>dBm</p>
<p>
<span class="des">Statistics: </span>
<pre id="stat"></pre>
<pre id="iv"></pre>
<pre id="warn_info"></pre>
</p>
<p>Every <span id="refresh"></span> seconds the values are updated</p>
<div id="note"> <div id="note">
This project was started from <a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion. (Mikrocontroller.net)</a><br/> This project was started from <a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion. (Mikrocontroller.net)</a><br/>
New updates can be found on Github: <a href="https://github.com/grindylow/ahoy" target="_blank">https://github.com/grindylow/ahoy</a><br/> New updates can be found on Github: <a href="https://github.com/lumapu/ahoy" target="_blank">https://github.com/lumapu/ahoy</a><br/>
<br/> <br/>
Please report issues using the feature provided by <a href="https://github.com/grindylow/ahoy/issues">Github</a><br/> Please report issues using the feature provided by <a href="https://github.com/lumapu/ahoy/issues">Github</a><br/>
<br/> <br/>
Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a> Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a>
<br/> <br/>
<p class="lic"><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de">Creative Commons - https://creativecommons.org/licenses/by-nc-sa/3.0/de/</a><br/> <p class="lic"><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de">Creative Commons - https://creativecommons.org/licenses/by-nc-sa/3.0/de/</a><br/>
Check the licenses which are published on <a href="https://github.com/grindylow/ahoy">https://github.com/grindylow/ahoy</a> as well</p> Check the licenses which are published on <a href="https://github.com/lumapu/ahoy">https://github.com/lumapu/ahoy</a> as well</p>
</div> </div>
</div> </div>
<div id="footer"> <div id="footer">
<p class="left">&copy 2022</p> <p class="left">&copy 2022</p>
<p class="left"><a href="/update">Update Firmware</a></p> <p class="left"><a href="/update">Update Firmware</a></p>
<p class="right">AHOY :: {VERSION}</p> <p class="right" id="version"></p>
<p class="right"><a href="/reboot">Reboot</a></p> <p class="right"><a href="/reboot">Reboot</a></p>
<p class="right">Git SHA: {BUILD}</p> <p class="right"><a href="/api">REST API</a></p>
</div> </div>
<script type="text/javascript">
var mIntervalSet = false;
function parseSys(obj) {
document.getElementById("version").innerHTML = "Git SHA: " + obj["build"] + " :: " + obj["version"];
document.getElementById("wifi_rssi").innerHTML = obj["wifi_rssi"];;
var date = new Date(obj["ts_now"] * 1000);
var up = obj["ts_uptime"];
var days = parseInt(up / 86400) % 365;
var hrs = parseInt(up / 3600) % 24;
var min = parseInt(up / 60) % 60;
var sec = up % 60;
document.getElementById("uptime").innerHTML = days + " Days, "
+ ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2);
document.getElementById("date").innerHTML = date.toLocaleString('de-DE', {timeZone: 'UTC'});
}
function parseStat(obj) {
document.getElementById("stat").innerHTML = "RX success: " + obj["rx_success"]
+ "\nRX fail: " + obj["rx_fail"]
+ "\nRX no anwser: " + obj["rx_fail_answer"]
+ "\nFrames received: " + obj["frame_cnt"]
+ "\nTX Cnt: " + obj["tx_cnt"];
}
function parseIv(obj) {
var html = "";
for(var i of obj) {
html += "Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is ";
if(false == i["is_avail"])
html += "not ";
html += "available and is ";
if(false == i["is_producing"])
html += "not ";
html += "producing\n";
if(false == i["is_avail"]) {
if(i["ts_last_success"] > 0) {
var date = new Date(i["ts_last_success"] * 1000);
html += "-> last successful transmission: " + date.toLocaleString('de-DE', {timeZone: 'UTC'});
}
}
}
document.getElementById("iv").innerHTML = html;
}
function parseWarnInfo(warn, info) {
var html = "";
for(var w of warn) {
html += "WARN: " + w + "\n";
}
for(var i of info) {
html += "INFO: " + i + "\n";
}
document.getElementById("warn_info").innerHTML = html;
}
function parse(obj) {
if(null != obj) {
parseSys(obj["system"]);
parseStat(obj["statistics"]);
parseIv(obj["inverter"]);
parseWarnInfo(obj["warnings"], obj["infos"]);
document.getElementById("refresh").innerHTML = obj["refresh_interval"];
if(false == mIntervalSet) {
window.setInterval("getAjax('/api/index', parse)", obj["refresh_interval"] * 1000);
mIntervalSet = true;
}
}
else
document.getElementById("refresh").innerHTML = "n/a";
}
getAjax("/api/index", parse);
</script>
</body> </body>
</html> </html>

188
tools/esp8266/html/serial.html

@ -0,0 +1,188 @@
<!doctype html>
<html>
<head>
<title>Serial Console</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
</head>
<body>
<h1>Serial Console</h1>
<div id="content" class="content">
<div class="serial">
<textarea id="serial" cols="80" rows="20" readonly></textarea><br/>
connected: <span class="dot" id="connected"></span>
Uptime: <span id="uptime"></span>
<input type="button" value="clear" class="btn" id="clear"/>
<input type="button" value="autoscroll" class="btn" id="scroll"/>
<br/>
<br/>
<br/>
<br/>
<hr>
<h3>Commands</h3>
<br/>
<label for="iv">Select Inverter:</label>
<select name="iv" id="InvID">
</select>
<br/>
<div id="power">
<input type="button" value="Restart" class="btn" id="restart"/>
<input type="button" value="Turn Off" class="btn" id="power_off"/>
<input type="button" value="Turn On" class="btn" id="power_on"/>
</div>
<br/>
<br/>
<br/>
<br/>
<br/>
<label>Send Power Limit: </label>
<input type="number" class="text" name="pwrlimval" maxlength="4"/>
<label> </label>
<select name="pwrlimcntrl" id="pwrlimcntrl">
<option value="" selected disabled hidden>select the unit and persistence</option>
<option value="0">absolute in Watt non persistent</option>
<option value="1">relative in percent non persistent</option>
<option value="256">absolute in Watt persistent</option>
<option value="257">relative in percent persistent</option>
</select>
<br/>
<input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/>
<br/>
<p>Ctrl result: <span id="result">n/a</span></p>
</div>
</div>
<div id="footer">
<p class="left">&copy 2022</p>
<p class="left"><a href="/">Home</a></p>
<p class="right" id="version"></p>
</div>
<script type="text/javascript">
var mAutoScroll = true;
var con = document.getElementById("serial");
var mIntervalSet = false;
function parseSys(obj) {
var up = obj["ts_uptime"];
var days = parseInt(up / 86400) % 365;
var hrs = parseInt(up / 3600) % 24;
var min = parseInt(up / 60) % 60;
var sec = up % 60;
document.getElementById("uptime").innerHTML = days + " Days, "
+ ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2);
if(false == mIntervalSet) {
document.getElementById("version").innerHTML = "Git SHA: " + obj["build"] + " :: " + obj["version"];
window.setInterval("getAjax('/api/system', parseSys)", 10000);
mIntervalSet = true;
}
}
function parse(root) {
select = document.getElementById('InvID');
if(null == root) return;
root = root.inverter;
for(var i = 0; i < root.inverter.length; i++)
{
inv = root.inverter[i];
var opt = document.createElement('option');
opt.value = inv.id;
opt.innerHTML = inv.name;
select.appendChild(opt);
}
}
document.getElementById("clear").addEventListener("click", function() {
con.value = "";
});
document.getElementById("scroll").addEventListener("click", function() {
mAutoScroll = !mAutoScroll;
this.value = (mAutoScroll) ? "autoscroll" : "manual scoll";
});
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
document.getElementById("connected").style.backgroundColor = "#0c0";
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
document.getElementById("connected").style.backgroundColor = "#f00";
}
}, false);
source.addEventListener('serial', function(e) {
con.value += e.data.replace(/\<rn\>/g, '\r\n');
if(mAutoScroll)
con.scrollTop = con.scrollHeight;
}, false);
}
getAjax("/api/system", parseSys);
// only for test
function ctrlCb(obj) {
var e = document.getElementById("result");
if(obj["success"])
e.innerHTML = "ok";
else
e.innerHTML = "Error: " + obj["error"];
}
function get_selected_iv()
{
var e = document.getElementById("InvID");
return parseInt(e.value);
}
const wrapper = document.getElementById('power');
wrapper.addEventListener('click', (event) => {
var power = event.target.value;
var obj = new Object();
switch (power)
{
case "Turn On":
obj.cmd = 0;
break;
case "Turn Off":
obj.cmd = 1;
break;
default:
obj.cmd = 2;
}
obj.inverter = get_selected_iv();
obj.tx_request = 81;
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
});
document.getElementById("sendpwrlim").addEventListener("click", function() {
var val = parseInt(document.getElementsByName('pwrlimval')[0].value);
var ctrl = parseInt(document.getElementsByName('pwrlimcntrl')[0].value);
if((ctrl == 1 || ctrl == 257) && unit < 2) unit = 2;
if(isNaN(val) || isNaN(ctrl))
{
var tmp = (isNaN(val)) ? "Value" : "Unit";
document.getElementById("result").textContent = tmp + " is missing";
return;
}
var obj = new Object();
obj.inverter = get_selected_iv();
obj.cmd = 11;
obj.tx_request = 81;
obj.payload = [val, ctrl];
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
});
getAjax("/api/setup", parse);
</script>
</body>
</html>

280
tools/esp8266/html/setup.html

@ -1,51 +1,20 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Setup - {DEVICE}</title> <title>Setup</title>
<link rel="stylesheet" type="text/css" href="style.css"/> <link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
<script type="text/javascript"> <script type="text/javascript">
function toggle(name, hide) {
var elm = document.getElementsByName(name)[0];
if(hide) {
if(!elm.classList.contains("hide"))
elm.classList.add("hide");
}
else
elm.classList.remove('hide');
}
function load() { function load() {
document.querySelectorAll('input[name^="inv"][name$="Addr"]').forEach(elm => { for(it of document.getElementsByClassName("s_collapsible")) {
elm.addEventListener("keyup", (e) => { it.addEventListener("click", function() {
serial = elm.value.substring(0,4); this.classList.toggle("active");
iv = elm.name.substring(3,4); var content = this.nextElementSibling;
max = 0; content.style.display = (content.style.display === "block") ? "none" : "block";
for(i=0;i<4;i++) {
toggle("inv"+iv+"ModPwr"+i, true);
toggle("inv"+iv+"ModName"+i, true);
}
toggle("lbl"+iv+"ModPwr", true);
toggle("lbl"+iv+"ModName", true);
if(serial == "1161") max = 4;
else if(serial == "1141") max = 2;
else if(serial == "1121") max = 1;
for(i=0;i<max;i++) {
toggle("inv"+iv+"ModPwr"+i, false);
toggle("inv"+iv+"ModName"+i, false);
}
if(max != 0) {
toggle("lbl"+iv+"ModPwr", false);
toggle("lbl"+iv+"ModName", false);
}
});
evt = document.createEvent("HTMLEvents");
evt.initEvent("keyup", false, true);
elm.dispatchEvent(evt);
}); });
} }
}
</script> </script>
</head> </head>
<body onload="load()"> <body onload="load()">
@ -54,11 +23,11 @@
<div id="content"> <div id="content">
<a class="erase" href="/erase">ERASE SETTINGS (not WiFi)</a> <a class="erase" href="/erase">ERASE SETTINGS (not WiFi)</a>
<form method="post" action="{IP}/save"> <form method="post" action="/save">
<fieldset> <fieldset>
<legend class="des">Device Host Name</legend> <legend class="des">Device Host Name</legend>
<label for="device">Device Name</label> <label for="device">Device Name</label>
<input type="text" class="text" name="device" value="{DEVICE}"/> <input type="text" name="device" class="text"/>
</fieldset> </fieldset>
<button type="button" class="s_collapsible">WiFi</button> <button type="button" class="s_collapsible">WiFi</button>
@ -67,7 +36,7 @@
<legend class="des">WiFi</legend> <legend class="des">WiFi</legend>
<p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p> <p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p>
<label for="ssid">SSID</label> <label for="ssid">SSID</label>
<input type="text" class="text" name="ssid" value="{SSID}"/> <input type="text" name="ssid" class="text"/>
<label for="pwd">Password</label> <label for="pwd">Password</label>
<input type="password" class="text" name="pwd" value="{PWD}"/> <input type="password" class="text" name="pwd" value="{PWD}"/>
</fieldset> </fieldset>
@ -77,12 +46,13 @@
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset>
<legend class="des">Inverter</legend> <legend class="des">Inverter</legend>
{INVERTERS}<br/> <div id="inverter"></div><br/>
<input type="button" name="btnAdd" value="Add Inverter"/>
<p class="subdes">General</p> <p class="subdes">General</p>
<label for="invInterval">Interval [s]</label> <label for="invInterval">Interval [s]</label>
<input type="text" class="text" name="invInterval" value="{INV_INTVL}"/> <input type="text" class="text" name="invInterval"/>
<label for="invRetry">Max retries per Payload</label> <label for="invRetry">Max retries per Payload</label>
<input type="text" class="text" name="invRetry" value="{INV_RETRIES}"/> <input type="text" class="text" name="invRetry"/>
</fieldset> </fieldset>
</div> </div>
@ -91,9 +61,13 @@
<fieldset> <fieldset>
<legend class="des">NTP Server</legend> <legend class="des">NTP Server</legend>
<label for="ntpAddr">NTP Server / IP</label> <label for="ntpAddr">NTP Server / IP</label>
<input type="text" class="text" name="ntpAddr" value="{NTP_ADDR}"/> <input type="text" class="text" name="ntpAddr"/>
<label for="ntpPort">NTP Port</label> <label for="ntpPort">NTP Port</label>
<input type="text" class="text" name="ntpPort" value="{NTP_PORT}"/> <input type="text" class="text" name="ntpPort"/>
<label for="ntpBtn">set system time</label>
<input type="button" name="ntpBtn" id="ntpBtn" class="btn" value="from browser" onclick="setTime()"/>
<input type="button" name="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/>
<span id="apiResult">n/a</span>
</fieldset> </fieldset>
</div> </div>
@ -102,15 +76,15 @@
<fieldset> <fieldset>
<legend class="des">MQTT</legend> <legend class="des">MQTT</legend>
<label for="mqttAddr">Broker / Server IP</label> <label for="mqttAddr">Broker / Server IP</label>
<input type="text" class="text" name="mqttAddr" value="{MQTT_ADDR}" maxlength="32" /> <input type="text" class="text" name="mqttAddr" maxlength="32" />
<label for="mqttPort">Port</label> <label for="mqttPort">Port</label>
<input type="text" class="text" name="mqttPort" value="{MQTT_PORT}"/> <input type="text" class="text" name="mqttPort"/>
<label for="mqttUser">Username (optional)</label> <label for="mqttUser">Username (optional)</label>
<input type="text" class="text" name="mqttUser" value="{MQTT_USER}"/> <input type="text" class="text" name="mqttUser"/>
<label for="mqttPwd">Password (optional)</label> <label for="mqttPwd">Password (optional)</label>
<input type="text" class="text" name="mqttPwd" value="{MQTT_PWD}"/> <input type="password" class="text" name="mqttPwd"/>
<label for="mqttTopic">Topic</label> <label for="mqttTopic">Topic</label>
<input type="text" class="text" name="mqttTopic" value="{MQTT_TOPIC}"/> <input type="text" class="text" name="mqttTopic"/>
</fieldset> </fieldset>
</div> </div>
@ -119,47 +93,207 @@
<fieldset> <fieldset>
<legend class="des">System Config</legend> <legend class="des">System Config</legend>
<p class="des">Pinout (Wemos)</p> <p class="des">Pinout (Wemos)</p>
{PINOUT} <div id="pinout"></div>
<p class="des">Radio (NRF24L01+)</p> <p class="des">Radio (NRF24L01+)</p>
<label for="rf24Power">Amplifier Power Level</label> <div id="rf24"></div>
<select name="rf24Power">{RF24}</select>
<p class="des">Serial Console</p> <p class="des">Serial Console</p>
<label for="serEn">print inverter data</label> <label for="serEn">print inverter data</label>
<input type="checkbox" class="cb" name="serEn" {SER_VAL_CB}/><br/> <input type="checkbox" class="cb" name="serEn"/><br/>
<label for="serDbg">Serial Debug</label> <label for="serDbg">Serial Debug</label>
<input type="checkbox" class="cb" name="serDbg" {SER_DBG_CB}/><br/> <input type="checkbox" class="cb" name="serDbg"/><br/>
<label for="serIntvl">Interval [s]</label> <label for="serIntvl">Interval [s]</label>
<input type="text" class="text" name="serIntvl" value="{SER_INTVL}"/> <input type="text" class="text" name="serIntvl"/>
</fieldset> </fieldset>
</div> </div>
<label for="reboot">Reboot device after successful save</label> <label for="reboot">Reboot device after successful save</label>
<input type="checkbox" class="cb" name="reboot"/> <input type="checkbox" class="cb" name="reboot"/>
<input type="submit" value="save" class="btn" /> <input type="submit" value="save" class="btn right"/>
</form> </form>
</div> </div>
</div> </div>
<div id="footer"> <div id="footer">
<p class="left"><a href="{IP}/">Home</a></p> <p class="left"><a href="/">Home</a></p>
<p class="left"><a href="{IP}/update">Update Firmware</a></p> <p class="left"><a href="/update">Update Firmware</a></p>
<p class="right">AHOY - {VERSION}</p> <p class="right" id="version"></p>
<p class="right"><a href="{IP}/factory">Factory Reset</a></p> <p class="right"><a href="/factory">Factory Reset</a></p>
<p class="right"><a href="{IP}/reboot">Reboot</a></p> <p class="right"><a href="/reboot">Reboot</a></p>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var coll = document.getElementsByClassName("s_collapsible"); var highestId = 0;
var i; var maxInv = 0;
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function() { const re = /11[2,4,6]1.*/;
this.classList.toggle("active");
var content = this.nextElementSibling; document.getElementsByName("btnAdd")[0].addEventListener("click", function() {
content.style.display = (content.style.display === "block") ? "none" : "block"; if(highestId < (maxInv-1))
ivHtml(JSON.parse('{"name":"","serial":"","channels":4,"ch_max_power":[0,0,0,0],"ch_name":["","","",""]}'), highestId + 1);
}); });
function apiCb(obj) {
var e = document.getElementById("apiResult");
if(obj["success"])
e.innerHTML = "command excuted";
else
e.innerHTML = "Error: " + obj["error"];
} }
function setTime() {
var date = new Date();
var offset = date.getTimezoneOffset() * -60;
var obj = new Object();
obj.cmd = "set_time";
obj.ts = parseInt(offset + (date.getTime() / 1000));
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj));
}
function syncTime() {
var obj = new Object();
obj.cmd = "sync_ntp";
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj));
}
function ivHtml(obj, id) {
highestId = id;
if(highestId == (maxInv - 1))
toggle("btnAdd", true);
iv = document.getElementById("inverter");
iv.appendChild(des("Inverter " + id));
id = "inv" + id;
iv.appendChild(lbl(id + "Addr", "Address*"));
var addr = inp(id + "Addr", obj["serial"], 12)
iv.appendChild(addr);
addr.addEventListener("keyup", (e) => {
var serial = addr.value.substring(0,4);
var max = 0;
for(var i=0;i<4;i++) {
toggle(id+"ModPwr"+i, true);
toggle(id+"ModName"+i, true);
}
toggle("lbl"+id+"ModPwr", true);
toggle("lbl"+id+"ModName", true);
if(serial == "1161") max = 4;
else if(serial == "1141") max = 2;
else if(serial == "1121") max = 1;
for(var i=0;i<max;i++) {
toggle(id+"ModPwr"+i, false);
toggle(id+"ModName"+i, false);
}
if(max != 0) {
toggle("lbl"+id+"ModPwr", false);
toggle("lbl"+id+"ModName", false);
}
});
for(var i of [["Name", "name", "Name*", 32]]) { // so richtig?
iv.appendChild(lbl(id + i[0], i[2]));
iv.appendChild(inp(id + i[0], obj[i[1]], i[3]));
}
for(var j of [["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4], ["ModName", "ch_name", "Module Name", 16]]) {
var cl = (re.test(obj["serial"])) ? null : ["hide"];
iv.appendChild(lbl(null, j[2], cl, "lbl" + id + j[0]));
d = div([j[0]]);
i = 0;
cl = (re.test(obj["serial"])) ? ["text", "sh"] : ["text", "sh", "hide"];
for(it of obj[j[1]]) {
d.appendChild(inp(id + j[0] + i, it, j[3], cl, id + j[0] + i));
i++;
}
iv.appendChild(d);
}
}
function ivGlob(obj) {
for(var i of [["invInterval", "interval"], ["invRetry", "retries"]])
document.getElementsByName(i[0])[0].value = obj[i[1]];
}
function parseSys(obj) {
for(var i of [["device", "device_name"], ["ssid", "ssid"]])
document.getElementsByName(i[0])[0].value = obj[i[1]];
document.getElementById("version").innerHTML = "Git SHA: " + obj["build"] + " :: " + obj["version"];
}
function parseIv(obj) {
for(var i = 0; i < obj.inverter.length; i++)
ivHtml(obj.inverter[i], i);
ivGlob(obj);
maxInv = obj["max_num_inverters"];
}
function parseMqtt(obj) {
for(var i of [["Addr", "broker"], ["Port", "port"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"]])
document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]];
}
function parseNtp(obj) {
for(var i of [["ntpAddr", "addr"], ["ntpPort", "port"]])
document.getElementsByName(i[0])[0].value = obj[i[1]];
}
function parsePinout(obj) {
var e = document.getElementById("pinout");
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']];
for(p of pins) {
e.appendChild(lbl(p[1], p[0].toUpperCase()));
e.appendChild(sel(p[1], [
[0, "D3 (GPIO0)"],
[1, "TX (GPIO1)"],
[2, "D4 (GPIO2)"],
[3, "RX (GPIO3)"],
[4, "D2 (GPIO4)"],
[5, "D1 (GPIO5)"],
[6, "GPIO6"],
[7, "GPIO7"],
[8, "GPIO8"],
[9, "GPIO9"],
[10, "GPIO10"],
[11, "GPIO11"],
[12, "D6 (GPIO12)"],
[13, "D7 (GPIO13)"],
[14, "D5 (GPIO14)"],
[15, "D8 (GPIO15)"],
[16, "D0 (GPIO16 - no IRQ!)"]
], obj[p[0]]));
}
}
function parseRadio(obj) {
var e = document.getElementById("rf24");
e.appendChild(lbl("rf24Power", "Amplifier Power Level"));
e.appendChild(sel("rf24Power", [
[0, "MIN"],
[1, "LOW"],
[2, "HIGH"],
[3, "MAX"]
], obj["power_level"]));
}
function parseSerial(obj) {
for(var i of [["serEn", "show_live_data"], ["serDbg", "debug"]])
document.getElementsByName(i[0])[0].checked = obj[i[1]];
document.getElementsByName("serIntvl")[0].value = obj["interval"];
}
function parse(root) {
if(null != root) {
parseSys(root["system"]);
parseIv(root["inverter"]);
parseMqtt(root["mqtt"]);
parseNtp(root["ntp"]);
parsePinout(root["pinout"]);
parseRadio(root["radio"]);
parseSerial(root["serial"]);
}
}
getAjax("/api/setup", parse);
</script> </script>
</body> </body>
</html> </html>

28
tools/esp8266/html/style.css

@ -133,9 +133,14 @@ input.btn {
background-color: #006ec0; background-color: #006ec0;
color: #fff; color: #fff;
border: 0px; border: 0px;
float: right; padding: 7px 20px 7px 20px;
margin: 10px 0 30px; margin-bottom: 10px;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer;
}
input.btn:hover {
background-color: #044e86;
} }
input.cb { input.cb {
@ -245,7 +250,7 @@ div.ts {
padding: 7px; padding: 7px;
} }
div.modpwr, div.modname { div.ModPwr, div.ModName {
width:70%; width:70%;
display: inline-block; display: inline-block;
} }
@ -271,3 +276,20 @@ div.modpwr, div.modname {
width: 180px; width: 180px;
} }
} }
#serial {
width: 100%;
}
#content .serial {
max-width: 1000px;
}
.dot {
height: 15px;
width: 15px;
background-color: #f00;
border-radius: 50%;
display: inline-block;
margin-top: 15px;
}

33
tools/esp8266/html/update.html

@ -0,0 +1,33 @@
<!doctype html>
<html>
<head>
<title>Update</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
</head>
<body>
<h1>Update</h1>
<div id="content" class="content">
<div>
Make sure that you have noted all your settings before starting an update. New versions may have changed their memory layout which can break your existing settings.
</div>
<br/><br/>
<form method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update"><input type="submit" value="Update">
</form>
</div>
<div id="footer">
<p class="left">&copy 2022</p>
<p class="left"><a href="/">Home</a></p>
<p class="right" id="version"></p>
</div>
<script type="text/javascript">
function parseSys(obj) {
document.getElementById("version").innerHTML = "Git SHA: " + obj["build"] + " :: " + obj["version"];
}
getAjax("/api/system", parseSys);
</script>
</body>
</html>

129
tools/esp8266/html/visualization.html

@ -1,43 +1,118 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Index - {DEVICE}</title> <title>Live</title>
<link rel="stylesheet" type="text/css" href="style.css"/> <link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<script type="text/javascript"> <script type="text/javascript" src="api.js"></script>
getAjax('/livedata', 'livedata');
window.setInterval("getAjax('/livedata', 'livedata')", {JS_TS});
function getAjax(url, resid) {
var http = null;
http = new XMLHttpRequest();
if(http != null) {
http.open("GET", url, true);
http.onreadystatechange = print;
http.send(null);
}
function print() {
if(http.readyState == 4) {
document.getElementById(resid).innerHTML = http.responseText;
}
}
}
</script>
<style type="text/css">
</style>
</head> </head>
<body> <body>
<h1>AHOY - {DEVICE}</h1> <h1>AHOY</h1>
<div id="content" class="content"> <div id="content" class="content">
<div id="livedata"></div> <div id="live"></div>
<p>Every {TS}seconds the values are updated</p> <p>Every <span id="refresh"></span> seconds the values are updated</p>
</div> </div>
<div id="footer"> <div id="footer">
<p class="left">&copy 2022</p> <p class="left">&copy 2022</p>
<p class="left"><a href="/">Home</a></p> <p class="left"><a href="/">Home</a></p>
<p class="right">AHOY :: {VERSION}</p> <p class="right" id="version"></p>
</div> </div>
<script type="text/javascript">
var intervalSet = false;
function parseSys(obj) {
document.getElementById("version").innerHTML = "Git SHA: " + obj["build"] + " :: " + obj["version"];
}
function parseIv(obj, root) {
var ivHtml = [];
var tDiv = div(["ch-all", "iv"]);
tDiv.appendChild(span("Total", ["head"]));
var total = new Array(root.ch0_fld_names.length).fill(0);
if(obj.length > 1)
ivHtml.push(tDiv);
for(var iv of obj) {
main = div(["iv"]);
var ch0 = div(["ch-iv"]);
var limit = iv["power_limit_read"] + "%";
if(limit == "65535%")
limit = "n/a";
ch0.appendChild(span(iv["name"] + " Limit " + limit + " | last Alarm: " + iv["last_alarm"], ["head"]));
for(var j = 0; j < root.ch0_fld_names.length; j++) {
var val = Math.round(iv["ch"][0][j] * 100) / 100;
if(val > 0) {
var sub = div(["subgrp"]);
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"]));
sub.appendChild(span(root["ch0_fld_names"][j], ["info"]));
ch0.appendChild(sub);
switch(j) {
case 2: total[j] += val; break; // P_AC
case 6: total[j] += val; break; // YieldTotal
case 7: total[j] += val; break; // YieldDay
case 8: total[j] += val; break; // P_DC
case 10: total[j] += val; break; // Q_AC
}
}
}
main.appendChild(ch0);
for(var i = 1; i < (iv["channels"] + 1); i++) {
var ch = div(["ch"]);
ch.appendChild(span(("" == iv["ch_names"][i]) ? ("CHANNEL " + i) : iv["ch_names"][i], ["head"]));
for(var j = 0; j < root.fld_names.length; j++) {
var val = Math.round(iv["ch"][i][j] * 100) / 100;
if(val > 0) {
ch.appendChild(span(val + " " + span(root["fld_units"][j], ["unit"]).innerHTML, ["value"]));
ch.appendChild(span(root["fld_names"][j], ["info"]));
}
}
main.appendChild(ch);
}
var ts = div(["ts"]);
var date = new Date(iv["ts_last_success"] * 1000);
ts.innerHTML = "Last received data requested at: " + date.toLocaleString('de-DE', {timeZone: 'UTC'});
main.appendChild(ts);
ivHtml.push(main);
}
// total
if(obj.length > 1) {
for(var j = 0; j < root.ch0_fld_names.length; j++) {
var val = total[j];
if(val > 0) {
var sub = div(["subgrp"]);
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"]));
sub.appendChild(span(root["ch0_fld_names"][j], ["info"]));
tDiv.appendChild(sub);
}
}
}
document.getElementById("live").replaceChildren(...ivHtml);
}
function parse(obj) {
if(null != obj) {
parseSys(obj["system"]);
parseIv(obj["inverter"], obj);
document.getElementById("refresh").innerHTML = obj["refresh_interval"];
if(false == intervalSet) {
window.setInterval("getAjax('/api/live', parse)", obj["refresh_interval"] * 1000);
intervalSet = true;
}
}
else
document.getElementById("refresh").innerHTML = "n/a";
}
getAjax("/api/live", parse);
</script>
</body> </body>
</html> </html>

40
tools/esp8266/include/dbg.h

@ -10,6 +10,7 @@
#define F(sl) (sl) #define F(sl) (sl)
#endif #endif
#include <functional>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// available levels // available levels
#define DBG_ERROR 1 #define DBG_ERROR 1
@ -21,7 +22,9 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// globally used level // globally used level
#ifndef DEBUG_LEVEL
#define DEBUG_LEVEL DBG_INFO #define DEBUG_LEVEL DBG_INFO
#endif
#ifdef ARDUINO #ifdef ARDUINO
#include "Arduino.h" #include "Arduino.h"
@ -32,23 +35,40 @@
#define DBGPRINTLN(str) #define DBGPRINTLN(str)
#else #else
#ifdef ARDUINO #ifdef ARDUINO
#define DBG_CB std::function<void(String)>
extern DBG_CB mCb;
inline void registerDebugCb(DBG_CB cb) {
mCb = cb;
}
#ifndef DSERIAL #ifndef DSERIAL
#define DSERIAL Serial #define DSERIAL Serial
#endif #endif
template <class T> //template <class T>
inline void DBGPRINT(T str) { DSERIAL.print(str); } inline void DBGPRINT(String str) { DSERIAL.print(str); if(NULL != mCb) mCb(str); }
template <class T> //template <class T>
inline void DBGPRINTLN(T str) { DBGPRINT(str); DBGPRINT(F("\r\n")); } inline void DBGPRINTLN(String str) { DBGPRINT(str); DBGPRINT(F("\r\n")); }
inline void DHEX(uint8_t b) { inline void DHEX(uint8_t b) {
if( b<0x10 ) DSERIAL.print('0'); if( b<0x10 ) DSERIAL.print(F("0"));
DSERIAL.print(b,HEX); DSERIAL.print(b,HEX);
if(NULL != mCb) {
if( b<0x10 ) mCb(F("0"));
mCb(String(b, HEX));
}
} }
inline void DHEX(uint16_t b) { inline void DHEX(uint16_t b) {
if( b<0x10 ) DSERIAL.print(F("000")); if( b<0x10 ) DSERIAL.print(F("000"));
else if( b<0x100 ) DSERIAL.print(F("00")); else if( b<0x100 ) DSERIAL.print(F("00"));
else if( b<0x1000 ) DSERIAL.print(F("0")); else if( b<0x1000 ) DSERIAL.print(F("0"));
DSERIAL.print(b, HEX); DSERIAL.print(b, HEX);
if(NULL != mCb) {
if( b<0x10 ) mCb(F("000"));
else if( b<0x100 ) mCb(F("00"));
else if( b<0x1000 ) mCb(F("0"));
mCb(String(b, HEX));
}
} }
inline void DHEX(uint32_t b) { inline void DHEX(uint32_t b) {
if( b<0x10 ) DSERIAL.print(F("0000000")); if( b<0x10 ) DSERIAL.print(F("0000000"));
@ -59,6 +79,16 @@
else if( b<0x1000000 ) DSERIAL.print(F("00")); else if( b<0x1000000 ) DSERIAL.print(F("00"));
else if( b<0x10000000 ) DSERIAL.print(F("0")); else if( b<0x10000000 ) DSERIAL.print(F("0"));
DSERIAL.print(b, HEX); DSERIAL.print(b, HEX);
if(NULL != mCb) {
if( b<0x10 ) mCb(F("0000000"));
else if( b<0x100 ) mCb(F("000000"));
else if( b<0x1000 ) mCb(F("00000"));
else if( b<0x10000 ) mCb(F("0000"));
else if( b<0x100000 ) mCb(F("000"));
else if( b<0x1000000 ) mCb(F("00"));
else if( b<0x10000000 ) mCb(F("0"));
mCb(String(b, HEX));
}
} }
#endif #endif
#endif #endif

31
tools/esp8266/platformio.ini

@ -32,17 +32,16 @@ extra_scripts =
pre:html/convert.py pre:html/convert.py
lib_deps = lib_deps =
nrf24/RF24@1.4.5 https://github.com/yubox-node-org/ESPAsyncWebServer
paulstoffregen/Time@^1.6.1 nrf24/RF24
knolleary/PubSubClient@^2.8 paulstoffregen/Time
bblanchon/ArduinoJson@^6.19.4 knolleary/PubSubClient
;esp8266/DNSServer@1.1.0 bblanchon/ArduinoJson
;esp8266/EEPROM@^1.0 ;esp8266/DNSServer
;esp8266/ESP8266HTTPUpdateServer@^1.0 ;esp8266/EEPROM
;esp8266/ESP8266WebServer@^1.0 ;esp8266/ESP8266WiFi
;esp8266/ESP8266WiFi@^1.0 ;esp8266/SPI
;esp8266/SPI@1.0 ;esp8266/Ticker
;esp8266/Ticker@^1.0
[env:esp8266-release] [env:esp8266-release]
platform = espressif8266 platform = espressif8266
@ -58,7 +57,7 @@ monitor_filters =
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial
build_type = debug build_type = debug
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
@ -68,7 +67,9 @@ monitor_filters =
[env:esp32-wroom32-release] [env:esp32-wroom32-release]
platform = espressif32 platform = espressif32
board = lolin_d32 board = lolin_d32
build_flags = -D RELEASE build_flags = -D RELEASE -std=gnu++14
build_unflags = -std=gnu++11
upload_port = /dev/cu.SLAB_USBtoUART
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line time ; Add timestamp with milliseconds for each new line
@ -77,8 +78,10 @@ monitor_filters =
[env:esp32-wroom32-debug] [env:esp32-wroom32-debug]
platform = espressif32 platform = espressif32
board = lolin_d32 board = lolin_d32
build_flags = -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -std=gnu++14
build_unflags = -std=gnu++11
build_type = debug build_type = debug
upload_port = /dev/cu.SLAB_USBtoUART
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line time ; Add timestamp with milliseconds for each new line

693
tools/esp8266/web.cpp

@ -12,41 +12,31 @@
#include "html/h/index_html.h" #include "html/h/index_html.h"
#include "html/h/style_css.h" #include "html/h/style_css.h"
#include "favicon.h" #include "html/h/api_js.h"
#include "html/h/favicon_ico_gz.h"
#include "html/h/setup_html.h" #include "html/h/setup_html.h"
#include "html/h/visualization_html.h" #include "html/h/visualization_html.h"
#include "html/h/update_html.h"
#include "html/h/serial_html.h"
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq"};
const uint16_t pwrLimitOptionValues[] {
NoPowerLimit,
AbsolutNonPersistent,
AbsolutPersistent,
RelativNonPersistent,
RelativPersistent
};
const char* const pwrLimitOptions[] {
"no power limit",
"absolute in Watt non persistent",
"absolute in Watt persistent",
"relativ in percent non persistent",
"relativ in percent persistent"
};
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
web::web(app *main, sysConfig_t *sysCfg, config_t *config, char version[]) { web::web(app *main, sysConfig_t *sysCfg, config_t *config, statistics_t *stat, char version[]) {
mMain = main; mMain = main;
mSysCfg = sysCfg; mSysCfg = sysCfg;
mConfig = config; mConfig = config;
mStat = stat;
mVersion = version; mVersion = version;
#ifdef ESP8266 mWeb = new AsyncWebServer(80);
mWeb = new ESP8266WebServer(80); mEvts = new AsyncEventSource("/events");
mUpdater = new ESP8266HTTPUpdateServer(); mApi = new webApi(mWeb, main, sysCfg, config, stat, version);
#elif defined(ESP32)
mWeb = new WebServer(80); memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mUpdater = new HTTPUpdateServer(); mSerialBufFill = 0;
#endif mWebSerialTicker = 0;
mUpdater->setup(mWeb); mWebSerialInterval = 1000; // [ms]
mSerialAddTime = true;
} }
@ -55,107 +45,141 @@ void web::setup(void) {
DPRINTLN(DBG_VERBOSE, F("app::setup-begin")); DPRINTLN(DBG_VERBOSE, F("app::setup-begin"));
mWeb->begin(); mWeb->begin();
DPRINTLN(DBG_VERBOSE, F("app::setup-on")); DPRINTLN(DBG_VERBOSE, F("app::setup-on"));
mWeb->on("/", std::bind(&web::showIndex, this)); mWeb->on("/", HTTP_GET, std::bind(&web::onIndex, this, std::placeholders::_1));
mWeb->on("/style.css", std::bind(&web::showCss, this)); mWeb->on("/style.css", HTTP_GET, std::bind(&web::onCss, this, std::placeholders::_1));
mWeb->on("/favicon.ico", std::bind(&web::showFavicon, this)); mWeb->on("/api.js", HTTP_GET, std::bind(&web::onApiJs, this, std::placeholders::_1));
mWeb->onNotFound ( std::bind(&web::showNotFound, this)); mWeb->on("/favicon.ico", HTTP_GET, std::bind(&web::onFavicon, this, std::placeholders::_1));
mWeb->on("/uptime", std::bind(&web::showUptime, this)); mWeb->onNotFound ( std::bind(&web::showNotFound, this, std::placeholders::_1));
mWeb->on("/reboot", std::bind(&web::showReboot, this)); mWeb->on("/reboot", HTTP_ANY, std::bind(&web::onReboot, this, std::placeholders::_1));
mWeb->on("/erase", std::bind(&web::showErase, this)); mWeb->on("/erase", HTTP_ANY, std::bind(&web::showErase, this, std::placeholders::_1));
mWeb->on("/factory", std::bind(&web::showFactoryRst, this)); mWeb->on("/factory", HTTP_ANY, std::bind(&web::showFactoryRst, this, std::placeholders::_1));
mWeb->on("/setup", HTTP_GET, std::bind(&web::onSetup, this, std::placeholders::_1));
mWeb->on("/save", HTTP_ANY, std::bind(&web::showSave, this, std::placeholders::_1));
mWeb->on("/live", HTTP_ANY, std::bind(&web::onLive, this, std::placeholders::_1));
mWeb->on("/api1", HTTP_POST, std::bind(&web::showWebApi, this, std::placeholders::_1));
mWeb->on("/update", HTTP_GET, std::bind(&web::onUpdate, this, std::placeholders::_1));
mWeb->on("/update", HTTP_POST, std::bind(&web::showUpdate, this, std::placeholders::_1),
std::bind(&web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
mWeb->on("/serial", HTTP_GET, std::bind(&web::onSerial, this, std::placeholders::_1));
mEvts->onConnect(std::bind(&web::onConnect, this, std::placeholders::_1));
mWeb->addHandler(mEvts);
mWeb->on("/setup", std::bind(&web::showSetup, this)); mApi->setup();
mWeb->on("/save", std::bind(&web::showSave, this));
mWeb->on("/cmdstat", std::bind(&web::showStatistics, this)); registerDebugCb(std::bind(&web::serialCb, this, std::placeholders::_1));
mWeb->on("/visualization", std::bind(&web::showVisualization, this));
mWeb->on("/livedata", std::bind(&web::showLiveData, this));
mWeb->on("/json", std::bind(&web::showJson, this));
mWeb->on("/api", HTTP_POST, std::bind(&web::showWebApi, this));
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::loop(void) { void web::loop(void) {
mWeb->handleClient(); mApi->loop();
if(mMain->checkTicker(&mWebSerialTicker, mWebSerialInterval)) {
if(mSerialBufFill > 0) {
mEvts->send(mSerialBuf, "serial", millis());
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
}
}
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showIndex(void) { void web::onConnect(AsyncEventSourceClient *client) {
DPRINTLN(DBG_VERBOSE, F("showIndex")); DPRINTLN(DBG_VERBOSE, "onConnect");
String html = FPSTR(index_html);
html.replace(F("{DEVICE}"), mSysCfg->deviceName); if(client->lastId())
html.replace(F("{VERSION}"), mVersion); DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId()));
html.replace(F("{TS}"), String(mConfig->sendInterval) + " ");
html.replace(F("{JS_TS}"), String(mConfig->sendInterval * 1000)); client->send("hello!", NULL, millis(), 1000);
html.replace(F("{BUILD}"), String(AUTO_GIT_HASH));
mWeb->send(200, "text/html", html);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showCss(void) { void web::onIndex(AsyncWebServerRequest *request) {
mWeb->send(200, "text/css", FPSTR(style_css)); DPRINTLN(DBG_VERBOSE, F("onIndex"));
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), index_html, index_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showFavicon(void) { void web::onCss(AsyncWebServerRequest *request) {
static const char favicon_type[] PROGMEM = "image/x-icon"; AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len);
static const char favicon_content[] PROGMEM = FAVICON_PANEL_16; response->addHeader(F("Content-Encoding"), "gzip");
mWeb->send_P(200, favicon_type, favicon_content, sizeof(favicon_content)); request->send(response);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showNotFound(void) { void web::onApiJs(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showNotFound - ") + mWeb->uri()); DPRINTLN(DBG_VERBOSE, F("onApiJs"));
String msg = F("File Not Found\n\nURI: ");
msg += mWeb->uri(); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len);
mWeb->send(404, F("text/plain"), msg); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showUptime(void) { void web::onFavicon(AsyncWebServerRequest *request) {
char time[21] = {0}; static const char favicon_type[] PROGMEM = "image/x-icon";
uint32_t uptime = mMain->getUptime(); AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico_gz, favicon_ico_gz_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
uint32_t upTimeSc = uint32_t((uptime) % 60);
uint32_t upTimeMn = uint32_t((uptime / (60)) % 60);
uint32_t upTimeHr = uint32_t((uptime / (60 * 60)) % 24);
uint32_t upTimeDy = uint32_t((uptime / (60 * 60 * 24)) % 365);
snprintf(time, 20, "%d Days, %02d:%02d:%02d", upTimeDy, upTimeHr, upTimeMn, upTimeSc); //-----------------------------------------------------------------------------
void web::showNotFound(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showNotFound - ") + request->url());
String msg = F("File Not Found\n\nURL: ");
msg += request->url();
msg += F("\nMethod: ");
msg += ( request->method() == HTTP_GET ) ? "GET" : "POST";
msg += F("\nArguments: ");
msg += request->args();
msg += "\n";
mWeb->send(200, "text/plain", String(time) + "; now: " + mMain->getDateTimeStr(mMain->getTimestamp())); for(uint8_t i = 0; i < request->args(); i++ ) {
msg += " " + request->argName(i) + ": " + request->arg(i) + "\n";
}
request->send(404, F("text/plain"), msg);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showReboot(void) { void web::onReboot(AsyncWebServerRequest *request) {
mWeb->send(200, F("text/html"), F("<!doctype html><html><head><title>Rebooting ...</title><meta http-equiv=\"refresh\" content=\"10; URL=/\"></head><body>rebooting ... auto reload after 10s</body></html>")); request->send(200, F("text/html"), F("<!doctype html><html><head><title>Rebooting ...</title><meta http-equiv=\"refresh\" content=\"10; URL=/\"></head><body>rebooting ... auto reload after 10s</body></html>"));
delay(1000); mMain->mShouldReboot = true;
ESP.restart();
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showErase() { void web::showErase(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showErase")); DPRINTLN(DBG_VERBOSE, F("showErase"));
mMain->eraseSettings(); mMain->eraseSettings();
showReboot(); onReboot(request);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showFactoryRst(void) { void web::showFactoryRst(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showFactoryRst")); DPRINTLN(DBG_VERBOSE, F("showFactoryRst"));
String content = ""; String content = "";
int refresh = 3; int refresh = 3;
if(mWeb->args() > 0) { if(request->args() > 0) {
if(mWeb->arg("reset").toInt() == 1) { if(request->arg("reset").toInt() == 1) {
mMain->eraseSettings(true); mMain->eraseSettings(true);
content = F("factory reset: success\n\nrebooting ... "); content = F("factory reset: success\n\nrebooting ... ");
refresh = 10; refresh = 10;
@ -170,7 +194,7 @@ void web::showFactoryRst(void) {
"<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>"); "<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>");
refresh = 120; refresh = 120;
} }
mWeb->send(200, F("text/html"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>")); request->send(200, F("text/html"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>"));
if(refresh == 10) { if(refresh == 10) {
delay(1000); delay(1000);
ESP.restart(); ESP.restart();
@ -179,193 +203,59 @@ void web::showFactoryRst(void) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showSetup(void) { void web::onSetup(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showSetup")); DPRINTLN(DBG_VERBOSE, F("onSetup"));
String html = FPSTR(setup_html);
html.replace(F("{SSID}"), mSysCfg->stationSsid);
// PWD will be left at the default value (for protection)
// -> the PWD will only be changed if it does not match the default "{PWD}"
html.replace(F("{DEVICE}"), String(mSysCfg->deviceName));
html.replace(F("{VERSION}"), String(mVersion));
if(mMain->getWifiApActive())
html.replace("{IP}", String(F("http://192.168.1.1")));
else
html.replace("{IP}", (F("http://") + String(WiFi.localIP().toString())));
String inv = "";
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mMain->mSys->getInverterByPos(i);
inv += F("<p class=\"subdes\">Inverter ") + String(i) + "</p>";
inv += F("<label for=\"inv") + String(i) + F("Addr\">Address*</label>");
inv += F("<input type=\"text\" class=\"text\" name=\"inv") + String(i) + F("Addr\" value=\"");
if(NULL != iv)
inv += String(iv->serial.u64, HEX);
inv += F("\"/ maxlength=\"12\">");
inv += F("<label for=\"inv") + String(i) + F("Name\">Name*</label>");
inv += F("<input type=\"text\" class=\"text\" name=\"inv") + String(i) + F("Name\" value=\"");
if(NULL != iv)
inv += String(iv->name);
inv += F("\"/ maxlength=\"") + String(MAX_NAME_LENGTH) + "\">";
inv += F("<label for=\"inv") + String(i) + F("ActivePowerLimit\">Active Power Limit</label>");
inv += F("<input type=\"text\" class=\"text\" name=\"inv") + String(i) + F("ActivePowerLimit\" value=\"");
if(NULL != iv)
inv += String(iv->powerLimit[0]);
inv += F("\"/ maxlength=\"") + String(6) + "\">";
inv += F("<label for=\"inv") + String(i) + F("ActivePowerLimitConType\">Active Power Limit Control Type</label>");
inv += F("<select name=\"inv") + String(i) + F("PowerLimitControl\">");
for(uint8_t j = 0; j < 5; j++) {
inv += F("<option value=\"") + String(pwrLimitOptionValues[j]) + F("\"");
if(NULL != iv) {
if(iv->powerLimit[1] == pwrLimitOptionValues[j])
inv += F(" selected");
}
inv += F(">") + String(pwrLimitOptions[j]) + F("</option>");
}
inv += F("</select>");
inv += F("<label for=\"inv") + String(i) + F("ModPwr0\" name=\"lbl") + String(i);
inv += F("ModPwr\">Max Module Power (Wp)</label><div class=\"modpwr\">");
for(uint8_t j = 0; j < 4; j++) {
inv += F("<input type=\"text\" class=\"text sh\" name=\"inv") + String(i) + F("ModPwr") + String(j) + F("\" value=\"");
if(NULL != iv)
inv += String(iv->chMaxPwr[j]);
inv += F("\"/ maxlength=\"4\">");
}
inv += F("</div><br/><label for=\"inv") + String(i) + F("ModName0\" name=\"lbl") + String(i);
inv += F("ModName\">Module Name</label><div class=\"modname\">");
for(uint8_t j = 0; j < 4; j++) {
inv += F("<input type=\"text\" class=\"text sh\" name=\"inv") + String(i) + F("ModName") + String(j) + F("\" value=\"");
if(NULL != iv)
inv += String(iv->chName[j]);
inv += F("\"/ maxlength=\"") + String(MAX_NAME_LENGTH) + "\">";
}
inv += F("</div>");
}
html.replace(F("{INVERTERS}"), String(inv));
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), setup_html, setup_html_len);
// pinout response->addHeader(F("Content-Encoding"), "gzip");
String pinout; request->send(response);
for(uint8_t i = 0; i < 3; i++) {
pinout += F("<label for=\"") + String(pinArgNames[i]) + "\">" + String(pinNames[i]) + F("</label>");
pinout += F("<select name=\"") + String(pinArgNames[i]) + "\">";
for(uint8_t j = 0; j <= 16; j++) {
pinout += F("<option value=\"") + String(j) + "\"";
switch(i) {
default: if(j == mConfig->pinCs) pinout += F(" selected"); break;
case 1: if(j == mConfig->pinCe) pinout += F(" selected"); break;
case 2: if(j == mConfig->pinIrq) pinout += F(" selected"); break;
}
pinout += ">" + String(wemosPins[j]) + F("</option>");
}
pinout += F("</select>");
}
html.replace(F("{PINOUT}"), String(pinout));
// nrf24l01+
String rf24;
for(uint8_t i = 0; i <= 3; i++) {
rf24 += F("<option value=\"") + String(i) + "\"";
if(i == mConfig->amplifierPower)
rf24 += F(" selected");
rf24 += ">" + String(rf24AmpPowerNames[i]) + F("</option>");
}
html.replace(F("{RF24}"), String(rf24));
html.replace(F("{INV_INTVL}"), String(mConfig->sendInterval));
html.replace(F("{INV_RETRIES}"), String(mConfig->maxRetransPerPyld));
html.replace(F("{SER_INTVL}"), String(mConfig->serialInterval));
html.replace(F("{SER_VAL_CB}"), (mConfig->serialShowIv) ? "checked" : "");
html.replace(F("{SER_DBG_CB}"), (mConfig->serialDebug) ? "checked" : "");
html.replace(F("{NTP_ADDR}"), String(mConfig->ntpAddr));
html.replace(F("{NTP_PORT}"), String(mConfig->ntpPort));
html.replace(F("{MQTT_ADDR}"), String(mConfig->mqtt.broker));
html.replace(F("{MQTT_PORT}"), String(mConfig->mqtt.port));
html.replace(F("{MQTT_USER}"), String(mConfig->mqtt.user));
html.replace(F("{MQTT_PWD}"), String(mConfig->mqtt.pwd));
html.replace(F("{MQTT_TOPIC}"), String(mConfig->mqtt.topic));
mWeb->send(200, F("text/html"), html);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showSave(void) { void web::showSave(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showSave")); DPRINTLN(DBG_VERBOSE, F("showSave"));
if(mWeb->args() > 0) { if(request->args() > 0) {
char buf[20] = {0}; char buf[20] = {0};
// general // general
if(mWeb->arg("ssid") != "") if(request->arg("ssid") != "")
mWeb->arg("ssid").toCharArray(mSysCfg->stationSsid, SSID_LEN); request->arg("ssid").toCharArray(mSysCfg->stationSsid, SSID_LEN);
if(mWeb->arg("pwd") != "{PWD}") if(request->arg("pwd") != "{PWD}")
mWeb->arg("pwd").toCharArray(mSysCfg->stationPwd, PWD_LEN); request->arg("pwd").toCharArray(mSysCfg->stationPwd, PWD_LEN);
if(mWeb->arg("device") != "") if(request->arg("device") != "")
mWeb->arg("device").toCharArray(mSysCfg->deviceName, DEVNAME_LEN); request->arg("device").toCharArray(mSysCfg->deviceName, DEVNAME_LEN);
// inverter // inverter
Inverter<> *iv; Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mMain->mSys->getInverterByPos(i, false); iv = mMain->mSys->getInverterByPos(i, false);
// address // address
mWeb->arg("inv" + String(i) + "Addr").toCharArray(buf, 20); request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20);
if(strlen(buf) == 0) if(strlen(buf) == 0)
memset(buf, 0, 20); memset(buf, 0, 20);
iv->serial.u64 = mMain->Serial2u64(buf); iv->serial.u64 = mMain->Serial2u64(buf);
// active power limit
uint16_t actPwrLimit = mWeb->arg("inv" + String(i) + "ActivePowerLimit").toInt();
uint16_t actPwrLimitControl = mWeb->arg("inv" + String(i) + "PowerLimitControl").toInt();
if (actPwrLimit != 0xffff && actPwrLimit > 0){
iv->powerLimit[0] = actPwrLimit;
iv->powerLimit[1] = actPwrLimitControl;
iv->devControlCmd = ActivePowerContr;
iv->devControlRequest = true;
if ((iv->powerLimit[1] & 0x0001) == 0x0001)
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("%") );
else {
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W") );
DPRINTLN(DBG_INFO, F("Power Limit Control Setting ") + String(iv->powerLimit[1]));
}
}
if (actPwrLimit == 0xffff) { // set to 100%
iv->powerLimit[0] = 100;
iv->powerLimit[1] = RelativPersistent;
iv->devControlCmd = ActivePowerContr;
iv->devControlRequest = true;
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to unlimted"));
}
// name // name
mWeb->arg("inv" + String(i) + "Name").toCharArray(iv->name, MAX_NAME_LENGTH); request->arg("inv" + String(i) + "Name").toCharArray(iv->name, MAX_NAME_LENGTH);
// max channel power / name // max channel power / name
for(uint8_t j = 0; j < 4; j++) { for(uint8_t j = 0; j < 4; j++) {
iv->chMaxPwr[j] = mWeb->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff; iv->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff;
mWeb->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->chName[j], MAX_NAME_LENGTH); request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->chName[j], MAX_NAME_LENGTH);
} }
iv->initialized = true; iv->initialized = true;
} }
if(mWeb->arg("invInterval") != "") if(request->arg("invInterval") != "")
mConfig->sendInterval = mWeb->arg("invInterval").toInt(); mConfig->sendInterval = request->arg("invInterval").toInt();
if(mWeb->arg("invRetry") != "") if(request->arg("invRetry") != "")
mConfig->maxRetransPerPyld = mWeb->arg("invRetry").toInt(); mConfig->maxRetransPerPyld = request->arg("invRetry").toInt();
// pinout // pinout
uint8_t pin; uint8_t pin;
for(uint8_t i = 0; i < 3; i ++) { for(uint8_t i = 0; i < 3; i ++) {
pin = mWeb->arg(String(pinArgNames[i])).toInt(); pin = request->arg(String(pinArgNames[i])).toInt();
switch(i) { switch(i) {
default: mConfig->pinCs = pin; break; default: mConfig->pinCs = pin; break;
case 1: mConfig->pinCe = pin; break; case 1: mConfig->pinCe = pin; break;
@ -374,218 +264,72 @@ void web::showSave(void) {
} }
// nrf24 amplifier power // nrf24 amplifier power
mConfig->amplifierPower = mWeb->arg("rf24Power").toInt() & 0x03; mConfig->amplifierPower = request->arg("rf24Power").toInt() & 0x03;
// ntp // ntp
if(mWeb->arg("ntpAddr") != "") { if(request->arg("ntpAddr") != "") {
mWeb->arg("ntpAddr").toCharArray(mConfig->ntpAddr, NTP_ADDR_LEN); request->arg("ntpAddr").toCharArray(mConfig->ntpAddr, NTP_ADDR_LEN);
mConfig->ntpPort = mWeb->arg("ntpPort").toInt() & 0xffff; mConfig->ntpPort = request->arg("ntpPort").toInt() & 0xffff;
} }
// mqtt // mqtt
if(mWeb->arg("mqttAddr") != "") { if(request->arg("mqttAddr") != "") {
String addr = mWeb->arg("mqttAddr"); String addr = request->arg("mqttAddr");
addr.trim(); addr.trim();
addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN);
mWeb->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN);
mWeb->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); if(request->arg("mqttPwd") != "{PWD}")
mWeb->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN);
mConfig->mqtt.port = mWeb->arg("mqttPort").toInt(); request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN);
mConfig->mqtt.port = request->arg("mqttPort").toInt();
} }
// serial console // serial console
if(mWeb->arg("serIntvl") != "") { if(request->arg("serIntvl") != "") {
mConfig->serialInterval = mWeb->arg("serIntvl").toInt() & 0xffff; mConfig->serialInterval = request->arg("serIntvl").toInt() & 0xffff;
mConfig->serialDebug = (mWeb->arg("serDbg") == "on"); mConfig->serialDebug = (request->arg("serDbg") == "on");
mConfig->serialShowIv = (mWeb->arg("serEn") == "on"); mConfig->serialShowIv = (request->arg("serEn") == "on");
// Needed to log TX buffers to serial console // Needed to log TX buffers to serial console
mMain->mSys->Radio.mSerialDebug = mConfig->serialDebug; mMain->mSys->Radio.mSerialDebug = mConfig->serialDebug;
} }
mMain->saveValues(); mMain->saveValues();
if(mWeb->arg("reboot") == "on") if(request->arg("reboot") == "on")
showReboot(); onReboot(request);
else else
mWeb->send(200, F("text/html"), F("<!doctype html><html><head><title>Setup saved</title><meta http-equiv=\"refresh\" content=\"0; URL=/setup\"></head><body>" request->send(200, F("text/html"), F("<!doctype html><html><head><title>Setup saved</title><meta http-equiv=\"refresh\" content=\"0; URL=/setup\"></head><body>"
"<p>saved</p></body></html>")); "<p>saved</p></body></html>"));
} }
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showStatistics(void) { void web::onLive(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showStatistics")); DPRINTLN(DBG_VERBOSE, F("onLive"));
mWeb->send(200, F("text/plain"), mMain->getStatistics());
}
//----------------------------------------------------------------------------- AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), visualization_html, visualization_html_len);
void web::showVisualization(void) { response->addHeader(F("Content-Encoding"), "gzip");
DPRINTLN(DBG_VERBOSE, F("web::showVisualization")); request->send(response);
String html = FPSTR(visualization_html);
html.replace(F("{DEVICE}"), mSysCfg->deviceName);
html.replace(F("{VERSION}"), mVersion);
html.replace(F("{TS}"), String(mConfig->sendInterval) + " ");
html.replace(F("{JS_TS}"), String(mConfig->sendInterval * 1000));
mWeb->send(200, F("text/html"), html);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void web::showLiveData(void) { void web::showWebApi(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showLiveData"));
String modHtml, totalModHtml;
float totalYield = 0, totalYieldToday = 0, totalActual = 0;
uint8_t count = 0;
for (uint8_t id = 0; id < mMain->mSys->getNumInverters(); id++) {
count++;
Inverter<> *iv = mMain->mSys->getInverterByPos(id);
if (NULL != iv) {
#ifdef LIVEDATA_VISUALIZED
uint8_t modNum, pos;
switch (iv->type) {
default:
case INV_TYPE_1CH: modNum = 1; break;
case INV_TYPE_2CH: modNum = 2; break;
case INV_TYPE_4CH: modNum = 4; break;
}
modHtml += F("<div class=\"iv\">"
"<div class=\"ch-iv\"><span class=\"head\">")
+ String(iv->name) + F(" Limit ")
+ String(iv->actPowerLimit) + F("%");
if(NoPowerLimit == iv->powerLimit[1])
modHtml += F(" (not controlled)");
modHtml += F(" | last Alarm: ") + iv->lastAlarmMsg + F("</span>");
uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PCT, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_PRA, FLD_ALARM_MES_ID};
for (uint8_t fld = 0; fld < 11; fld++) {
pos = (iv->getPosByChFld(CH0, list[fld]));
if(fld == 6){
totalYield += iv->getValue(pos);
}
if(fld == 7){
totalYieldToday += iv->getValue(pos);
}
if(fld == 2){
totalActual += iv->getValue(pos);
}
if (0xff != pos) {
modHtml += F("<div class=\"subgrp\">");
modHtml += F("<span class=\"value\">") + String(iv->getValue(pos));
modHtml += F("<span class=\"unit\">") + String(iv->getUnit(pos)) + F("</span></span>");
modHtml += F("<span class=\"info\">") + String(iv->getFieldName(pos)) + F("</span>");
modHtml += F("</div>");
}
}
modHtml += "</div>";
for (uint8_t ch = 1; ch <= modNum; ch++) {
modHtml += F("<div class=\"ch\"><span class=\"head\">");
if (iv->chName[ch - 1][0] == 0)
modHtml += F("CHANNEL ") + String(ch);
else
modHtml += String(iv->chName[ch - 1]);
modHtml += F("</span>");
for (uint8_t j = 0; j < 6; j++) {
switch (j) {
default: pos = (iv->getPosByChFld(ch, FLD_UDC)); break;
case 1: pos = (iv->getPosByChFld(ch, FLD_IDC)); break;
case 2: pos = (iv->getPosByChFld(ch, FLD_PDC)); break;
case 3: pos = (iv->getPosByChFld(ch, FLD_YD)); break;
case 4: pos = (iv->getPosByChFld(ch, FLD_YT)); break;
case 5: pos = (iv->getPosByChFld(ch, FLD_IRR)); break;
}
if (0xff != pos) {
modHtml += F("<span class=\"value\">") + String(iv->getValue(pos));
modHtml += F("<span class=\"unit\">") + String(iv->getUnit(pos)) + F("</span></span>");
modHtml += F("<span class=\"info\">") + String(iv->getFieldName(pos)) + F("</span>");
}
}
modHtml += "</div>";
yield();
}
modHtml += F("<div class=\"ts\">Last received data requested at: ") + mMain->getDateTimeStr(iv->ts) + F("</div>");
modHtml += F("</div>");
#else
// dump all data to web frontend
modHtml = F("<pre>");
char topic[30], val[10];
for (uint8_t i = 0; i < iv->listLen; i++) {
snprintf(topic, 30, "%s/ch%d/%s", iv->name, iv->assign[i].ch, iv->getFieldName(i));
snprintf(val, 10, "%.3f %s", iv->getValue(i), iv->getUnit(i));
modHtml += String(topic) + ": " + String(val) + "\n";
}
modHtml += F("</pre>");
#endif
}
}
if(count > 1){
totalModHtml += F("<div class=\"iv\">"
"<div class=\"ch-all\"><span class=\"head\">Gesamt</span>");
totalModHtml += F("<div class=\"subgrp\">");
totalModHtml += F("<span class=\"value\">") + String(totalActual);
totalModHtml += F("<span class=\"unit\">W</span></span>");
totalModHtml += F("<span class=\"info\">P_AC All</span>");
totalModHtml += F("</div>");
totalModHtml += F("<div class=\"subgrp\">");
totalModHtml += F("<span class=\"value\">") + String(totalYieldToday);
totalModHtml += F("<span class=\"unit\">Wh</span></span>");
totalModHtml += F("<span class=\"info\">YieldDayAll</span>");
totalModHtml += F("</div>");
totalModHtml += F("<div class=\"subgrp\">");
totalModHtml += F("<span class=\"value\">") + String(totalYield);
totalModHtml += F("<span class=\"unit\">kWh</span></span>");
totalModHtml += F("<span class=\"info\">YieldTotalAll</span>");
totalModHtml += F("</div>");
totalModHtml += F("</div>");
totalModHtml += F("</div>");
mWeb->send(200, F("text/html"), totalModHtml + modHtml);
} else {
mWeb->send(200, F("text/html"), modHtml);
}
}
//-----------------------------------------------------------------------------
void web::showJson(void) {
DPRINTLN(DBG_VERBOSE, F("web::showJson"));
mWeb->send(200, F("application/json"), mMain->getJson());
}
//-----------------------------------------------------------------------------
void web::showWebApi(void)
{
DPRINTLN(DBG_VERBOSE, F("web::showWebApi")); DPRINTLN(DBG_VERBOSE, F("web::showWebApi"));
DPRINTLN(DBG_DEBUG, mWeb->arg("plain")); DPRINTLN(DBG_DEBUG, request->arg("plain"));
const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity. const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity.
DynamicJsonDocument response(capacity); DynamicJsonDocument response(capacity);
// Parse JSON object // Parse JSON object
deserializeJson(response, mWeb->arg("plain")); deserializeJson(response, request->arg("plain"));
// ToDo: error handling for payload // ToDo: error handling for payload
uint8_t iv_id = response["inverter"]; uint8_t iv_id = response["inverter"];
uint8_t cmd = response["cmd"]; uint8_t cmd = response["cmd"];
Inverter<> *iv = mMain->mSys->getInverterByPos(iv_id); Inverter<> *iv = mMain->mSys->getInverterByPos(iv_id);
if (NULL != iv) if (NULL != iv) {
{ if (response["tx_request"] == (uint8_t)TX_REQ_INFO) {
if (response["tx_request"] == (uint8_t)TX_REQ_INFO)
{
// if the AlarmData is requested set the Alarm Index to the requested one // if the AlarmData is requested set the Alarm Index to the requested one
if (cmd == AlarmData || cmd == AlarmUpdate) { if (cmd == AlarmData || cmd == AlarmUpdate) {
// set the AlarmMesIndex for the request from user input // set the AlarmMesIndex for the request from user input
@ -597,32 +341,21 @@ void web::showWebApi(void)
} }
if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) {
{ if (response["cmd"] == (uint8_t)ActivePowerContr) {
if (response["cmd"] == (uint8_t)ActivePowerContr)
{
uint16_t webapiPayload = response["payload"]; uint16_t webapiPayload = response["payload"];
uint16_t webapiPayload2 = response["payload2"]; uint16_t webapiPayload2 = response["payload2"];
if (webapiPayload > 0 && webapiPayload < 10000) if (webapiPayload > 0 && webapiPayload < 10000) {
{
iv->devControlCmd = ActivePowerContr; iv->devControlCmd = ActivePowerContr;
iv->powerLimit[0] = webapiPayload; iv->powerLimit[0] = webapiPayload;
if (webapiPayload2 > 0) if (webapiPayload2 > 0)
{
iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check
} else // if not set, set it to 0x0000 default
else iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporary in Watt absolut
{ // if not set, set it to 0x0000 default
iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporay in Watt absolut
}
if (iv->powerLimit[1] & 0x0001) if (iv->powerLimit[1] & 0x0001)
{
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API")); DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API"));
}
else else
{
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API")); DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API"));
}
iv->devControlRequest = true; // queue it in the request loop iv->devControlRequest = true; // queue it in the request loop
} }
} }
@ -634,7 +367,111 @@ void web::showWebApi(void)
iv->devControlCmd = TurnOn; iv->devControlCmd = TurnOn;
iv->devControlRequest = true; // queue it in the request loop iv->devControlRequest = true; // queue it in the request loop
} }
if (response["cmd"] == (uint8_t)CleanState_LockAndAlarm) {
iv->devControlCmd = CleanState_LockAndAlarm;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)Restart) {
iv->devControlCmd = Restart;
iv->devControlRequest = true; // queue it in the request loop
}
} }
} }
mWeb->send(200, "text/json", "{success:true}"); request->send(200, "text/json", "{success:true}");
}
//-----------------------------------------------------------------------------
void web::onUpdate(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onUpdate"));
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), update_html, update_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::showUpdate(AsyncWebServerRequest *request) {
bool reboot = !Update.hasError();
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: ");
if(reboot)
html += "success";
else
html += "failed";
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html);
response->addHeader("Connection", "close");
request->send(response);
mMain->mShouldReboot = reboot;
}
//-----------------------------------------------------------------------------
void web::showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if(!index) {
Serial.printf("Update Start: %s\n", filename.c_str());
#ifndef ESP32
Update.runAsync(true);
#endif
if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
Update.printError(Serial);
}
}
if(!Update.hasError()) {
if(Update.write(data, len) != len){
Update.printError(Serial);
}
}
if(final) {
if(Update.end(true)) {
Serial.printf("Update Success: %uB\n", index+len);
} else {
Update.printError(Serial);
}
}
}
//-----------------------------------------------------------------------------
void web::onSerial(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSerial"));
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), serial_html, serial_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::serialCb(String msg) {
msg.replace("\r\n", "<rn>");
if(mSerialAddTime) {
if((9 + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], mMain->getTimeStr().c_str(), 9);
mSerialBufFill += 9;
}
else {
mSerialBufFill = 0;
mEvts->send("webSerial, buffer overflow!", "serial", millis());
}
mSerialAddTime = false;
}
if(msg.endsWith("<rn>"))
mSerialAddTime = true;
uint16_t length = msg.length();
if((length + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length);
mSerialBufFill += length;
}
else {
mSerialBufFill = 0;
mEvts->send("webSerial, buffer overflow!", "serial", millis());
}
} }

75
tools/esp8266/web.h

@ -7,56 +7,69 @@
#define __WEB_H__ #define __WEB_H__
#include "dbg.h" #include "dbg.h"
#ifdef ESP8266 #ifdef ESP32
#include <ESP8266WebServer.h> #include "AsyncTCP.h"
#include <ESP8266HTTPUpdateServer.h> #include "Update.h"
#elif defined(ESP32) #else
#include <WebServer.h> #include "ESPAsyncTCP.h"
#include <HTTPUpdateServer.h>
#endif #endif
#include "ESPAsyncWebServer.h"
#include "app.h" #include "app.h"
#include "webApi.h"
#define WEB_SERIAL_BUF_SIZE 2048
class app; class app;
class webApi;
class web { class web {
public: public:
web(app *main, sysConfig_t *sysCfg, config_t *config, char version[]); web(app *main, sysConfig_t *sysCfg, config_t *config, statistics_t *stat, char version[]);
~web() {} ~web() {}
void setup(void); void setup(void);
void loop(void); void loop(void);
void showIndex(void); void onConnect(AsyncEventSourceClient *client);
void showCss(void);
void showFavicon(void); void onIndex(AsyncWebServerRequest *request);
void showNotFound(void); void onCss(AsyncWebServerRequest *request);
void showUptime(void); void onApiJs(AsyncWebServerRequest *request);
void showReboot(void); void onFavicon(AsyncWebServerRequest *request);
void showErase(); void showNotFound(AsyncWebServerRequest *request);
void showFactoryRst(void); void onReboot(AsyncWebServerRequest *request);
void showSetup(void); void showErase(AsyncWebServerRequest *request);
void showSave(void); void showFactoryRst(AsyncWebServerRequest *request);
void onSetup(AsyncWebServerRequest *request);
void showStatistics(void); void showSave(AsyncWebServerRequest *request);
void showVisualization(void);
void showLiveData(void); void onLive(AsyncWebServerRequest *request);
void showJson(void); void showWebApi(AsyncWebServerRequest *request);
void showWebApi(void);
void onUpdate(AsyncWebServerRequest *request);
void showUpdate(AsyncWebServerRequest *request);
void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
void serialCb(String msg);
private: private:
#ifdef ESP8266 void onSerial(AsyncWebServerRequest *request);
ESP8266WebServer *mWeb;
ESP8266HTTPUpdateServer *mUpdater; AsyncWebServer *mWeb;
#elif defined(ESP32) AsyncEventSource *mEvts;
WebServer *mWeb;
HTTPUpdateServer *mUpdater;
#endif
config_t *mConfig; config_t *mConfig;
sysConfig_t *mSysCfg; sysConfig_t *mSysCfg;
statistics_t *mStat;
char *mVersion; char *mVersion;
app *mMain; app *mMain;
webApi *mApi;
bool mSerialAddTime;
char mSerialBuf[WEB_SERIAL_BUF_SIZE];
uint16_t mSerialBufFill;
uint32_t mWebSerialTicker;
uint32_t mWebSerialInterval;
}; };
#endif /*__WEB_H__*/ #endif /*__WEB_H__*/

415
tools/esp8266/webApi.cpp

@ -0,0 +1,415 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#if defined(ESP32) && defined(F)
#undef F
#define F(sl) (sl)
#endif
#include "webApi.h"
//-----------------------------------------------------------------------------
webApi::webApi(AsyncWebServer *srv, app *app, sysConfig_t *sysCfg, config_t *config, statistics_t *stat, char version[]) {
mSrv = srv;
mApp = app;
mSysCfg = sysCfg;
mConfig = config;
mStat = stat;
mVersion = version;
}
//-----------------------------------------------------------------------------
void webApi::setup(void) {
mSrv->on("/api", HTTP_GET, std::bind(&webApi::onApi, this, std::placeholders::_1));
mSrv->on("/api", HTTP_POST, std::bind(&webApi::onApiPost, this, std::placeholders::_1)).onBody(
std::bind(&webApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5));
}
//-----------------------------------------------------------------------------
void webApi::loop(void) {
}
//-----------------------------------------------------------------------------
void webApi::onApi(AsyncWebServerRequest *request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
JsonObject root = response->getRoot();
Inverter<> *iv = mApp->mSys->getInverterByPos(0, false);
String path = request->url().substring(5);
if(path == "system") getSystem(root);
else if(path == "statistics") getStatistics(root);
else if(path == "inverter/list") getInverterList(root);
else if(path == "index") getIndex(root);
else if(path == "setup") getSetup(root);
else if(path == "live") getLive(root);
else if(path == "record/info") getRecord(root, iv->getRecordStruct(InverterDevInform_All));
else if(path == "record/alarm") getRecord(root, iv->getRecordStruct(AlarmData));
else if(path == "record/config") getRecord(root, iv->getRecordStruct(SystemConfigPara));
else if(path == "record/live") getRecord(root, iv->getRecordStruct(RealTimeRunData_Debug));
else
getNotFound(root, F("http://") + request->host() + F("/api/"));
response->addHeader("Access-Control-Allow-Origin", "*");
response->addHeader("Access-Control-Allow-Headers", "content-type");
response->setLength();
request->send(response);
}
//-----------------------------------------------------------------------------
void webApi::onApiPost(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, "onApiPost");
}
//-----------------------------------------------------------------------------
void webApi::onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
DPRINTLN(DBG_VERBOSE, "onApiPostBody");
DynamicJsonDocument json(200);
AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
JsonObject root = response->getRoot();
DeserializationError err = deserializeJson(json, (const char *)data);
root[F("success")] = (err) ? false : true;
if(!err) {
String path = request->url().substring(5);
if(path == "ctrl")
root[F("success")] = setCtrl(json, root);
else if(path == "setup")
root[F("success")] = setSetup(json, root);
else {
root[F("success")] = false;
root[F("error")] = "Path not found: " + path;
}
}
else {
switch (err.code()) {
case DeserializationError::Ok: break;
case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break;
case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break;
default: root[F("error")] = F("Deserialization failed"); break;
}
}
response->setLength();
request->send(response);
}
//-----------------------------------------------------------------------------
void webApi::getNotFound(JsonObject obj, String url) {
JsonObject ep = obj.createNestedObject("avail_endpoints");
ep[F("system")] = url + F("system");
ep[F("statistics")] = url + F("statistics");
ep[F("inverter/list")] = url + F("inverter/list");
ep[F("index")] = url + F("index");
ep[F("setup")] = url + F("setup");
ep[F("live")] = url + F("live");
ep[F("record/info")] = url + F("record/info");
ep[F("record/alarm")] = url + F("record/alarm");
ep[F("record/config")] = url + F("record/config");
ep[F("record/live")] = url + F("record/live");
}
//-----------------------------------------------------------------------------
void webApi::getSystem(JsonObject obj) {
obj[F("ssid")] = mSysCfg->stationSsid;
obj[F("device_name")] = mSysCfg->deviceName;
obj[F("version")] = String(mVersion);
obj[F("build")] = String(AUTO_GIT_HASH);
obj[F("ts_uptime")] = mApp->getUptime();
obj[F("ts_now")] = mApp->getTimestamp();
obj[F("wifi_rssi")] = WiFi.RSSI();
}
//-----------------------------------------------------------------------------
void webApi::getStatistics(JsonObject obj) {
obj[F("rx_success")] = mStat->rxSuccess;
obj[F("rx_fail")] = mStat->rxFail;
obj[F("rx_fail_answer")] = mStat->rxFailNoAnser;
obj[F("frame_cnt")] = mStat->frmCnt;
obj[F("tx_cnt")] = mApp->mSys->Radio.mSendCnt;
}
//-----------------------------------------------------------------------------
void webApi::getInverterList(JsonObject obj) {
JsonArray invArr = obj.createNestedArray(F("inverter"));
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mApp->mSys->getInverterByPos(i);
if(NULL != iv) {
JsonObject obj2 = invArr.createNestedObject();
obj2[F("id")] = i;
obj2[F("name")] = String(iv->name);
obj2[F("serial")] = String(iv->serial.u64, HEX);
obj2[F("channels")] = iv->channels;
obj2[F("version")] = String(iv->fwVersion);
for(uint8_t j = 0; j < iv->channels; j ++) {
obj2[F("ch_max_power")][j] = iv->chMaxPwr[j];
obj2[F("ch_name")][j] = iv->chName[j];
}
}
}
obj[F("interval")] = String(mConfig->sendInterval);
obj[F("retries")] = String(mConfig->maxRetransPerPyld);
obj[F("max_num_inverters")] = MAX_NUM_INVERTERS;
}
//-----------------------------------------------------------------------------
void webApi::getMqtt(JsonObject obj) {
obj[F("broker")] = String(mConfig->mqtt.broker);
obj[F("port")] = String(mConfig->mqtt.port);
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);
}
//-----------------------------------------------------------------------------
void webApi::getNtp(JsonObject obj) {
obj[F("addr")] = String(mConfig->ntpAddr);
obj[F("port")] = String(mConfig->ntpPort);
}
//-----------------------------------------------------------------------------
void webApi::getPinout(JsonObject obj) {
obj[F("cs")] = mConfig->pinCs;
obj[F("ce")] = mConfig->pinCe;
obj[F("irq")] = mConfig->pinIrq;
}
//-----------------------------------------------------------------------------
void webApi::getRadio(JsonObject obj) {
obj[F("power_level")] = mConfig->amplifierPower;
}
//-----------------------------------------------------------------------------
void webApi::getSerial(JsonObject obj) {
obj[F("interval")] = (uint16_t)mConfig->serialInterval;
obj[F("show_live_data")] = mConfig->serialShowIv;
obj[F("debug")] = mConfig->serialDebug;
}
//-----------------------------------------------------------------------------
void webApi::getIndex(JsonObject obj) {
getSystem(obj.createNestedObject(F("system")));
getStatistics(obj.createNestedObject(F("statistics")));
obj["refresh_interval"] = SEND_INTERVAL;
JsonArray inv = obj.createNestedArray(F("inverter"));
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mApp->mSys->getInverterByPos(i);
if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
JsonObject invObj = inv.createNestedObject();
invObj[F("id")] = i;
invObj[F("name")] = String(iv->name);
invObj[F("version")] = String(iv->fwVersion);
invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp(), rec);
invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp(), rec);
invObj[F("ts_last_success")] = iv->getLastTs(rec);
}
}
JsonArray warn = obj.createNestedArray(F("warnings"));
if(!mApp->mSys->Radio.isChipConnected())
warn.add(F("your NRF24 module can't be reached, check the wiring and pinout"));
if(!mApp->mqttIsConnected())
warn.add(F("MQTT is not connected"));
JsonArray info = obj.createNestedArray(F("infos"));
if(mApp->getRebootRequestState())
info.add(F("reboot your ESP to apply all your configuration changes!"));
if(!mApp->getSettingsValid())
info.add(F("your settings are invalid"));
if(mApp->mqttIsConnected())
info.add(F("MQTT is connected"));
}
//-----------------------------------------------------------------------------
void webApi::getSetup(JsonObject obj) {
getSystem(obj.createNestedObject(F("system")));
getInverterList(obj.createNestedObject(F("inverter")));
getMqtt(obj.createNestedObject(F("mqtt")));
getNtp(obj.createNestedObject(F("ntp")));
getPinout(obj.createNestedObject(F("pinout")));
getRadio(obj.createNestedObject(F("radio")));
getSerial(obj.createNestedObject(F("serial")));
}
//-----------------------------------------------------------------------------
void webApi::getLive(JsonObject obj) {
getSystem(obj.createNestedObject(F("system")));
JsonArray invArr = obj.createNestedArray(F("inverter"));
obj["refresh_interval"] = SEND_INTERVAL;
uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q};
Inverter<> *iv;
uint8_t pos;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mApp->mSys->getInverterByPos(i);
if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
JsonObject obj2 = invArr.createNestedObject();
obj2[F("name")] = String(iv->name);
obj2[F("channels")] = iv->channels;
obj2[F("power_limit_read")] = round3(iv->actPowerLimit);
obj2[F("last_alarm")] = String(iv->lastAlarmMsg);
obj2[F("ts_last_success")] = rec->ts;
JsonArray ch = obj2.createNestedArray("ch");
JsonArray ch0 = ch.createNestedArray();
obj2[F("ch_names")][0] = "AC";
for (uint8_t fld = 0; fld < sizeof(list); fld++) {
pos = (iv->getPosByChFld(CH0, list[fld], rec));
ch0[fld] = (0xff != pos) ? round3(iv->getValue(pos, rec)) : 0.0;
obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
}
for(uint8_t j = 1; j <= iv->channels; j ++) {
obj2[F("ch_names")][j] = String(iv->chName[j-1]);
JsonArray cur = ch.createNestedArray();
for (uint8_t k = 0; k < 6; k++) {
switch(k) {
default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break;
case 1: pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break;
case 2: pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break;
case 3: pos = (iv->getPosByChFld(j, FLD_YD, rec)); break;
case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break;
case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break;
}
cur[k] = (0xff != pos) ? round3(iv->getValue(pos, rec)) : 0.0;
if(1 == j) {
obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
}
}
}
}
}
}
//-----------------------------------------------------------------------------
void webApi::getRecord(JsonObject obj, record_t<> *rec) {
JsonArray invArr = obj.createNestedArray(F("inverter"));
Inverter<> *iv;
uint8_t pos;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mApp->mSys->getInverterByPos(i);
if(NULL != iv) {
JsonArray obj2 = invArr.createNestedArray();
for(uint8_t j = 0; j < rec->length; j++) {
byteAssign_t *assign = iv->getByteAssign(j, rec);
pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec));
obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail;
}
}
}
}
//-----------------------------------------------------------------------------
bool webApi::setCtrl(DynamicJsonDocument jsonIn, JsonObject jsonOut) {
uint8_t cmd = jsonIn[F("cmd")];
// Todo: num is the inverter number 0-3. For better display in DPRINTLN
uint8_t num = jsonIn[F("inverter")];
uint8_t tx_request = jsonIn[F("tx_request")];
if(TX_REQ_DEVCONTROL == tx_request)
{
DPRINTLN(DBG_INFO, F("devcontrol [") + String(num) + F("], cmd: 0x") + String(cmd, HEX));
Inverter<> *iv = getInverter(jsonIn, jsonOut);
JsonArray payload = jsonIn[F("payload")].as<JsonArray>();
if(NULL != iv)
{
switch (cmd)
{
case TurnOn:
iv->devControlCmd = TurnOn;
iv->devControlRequest = true;
break;
case TurnOff:
iv->devControlCmd = TurnOff;
iv->devControlRequest = true;
break;
case CleanState_LockAndAlarm:
iv->devControlCmd = CleanState_LockAndAlarm;
iv->devControlRequest = true;
break;
case Restart:
iv->devControlCmd = Restart;
iv->devControlRequest = true;
break;
case ActivePowerContr:
iv->devControlCmd = ActivePowerContr;
iv->devControlRequest = true;
iv->powerLimit[0] = payload[0];
iv->powerLimit[1] = payload[1];
break;
default:
jsonOut["error"] = "unknown 'cmd' = " + String(cmd);
return false;
}
} else {
return false;
}
}
else {
jsonOut[F("error")] = F("unknown 'tx_request'");
return false;
}
return true;
}
//-----------------------------------------------------------------------------
bool webApi::setSetup(DynamicJsonDocument jsonIn, JsonObject jsonOut) {
if(F("set_time") == jsonIn[F("cmd")])
mApp->setTimestamp(jsonIn[F("ts")]);
else if(F("sync_ntp") == jsonIn[F("cmd")])
mApp->setTimestamp(0); // 0: update ntp flag
else {
jsonOut[F("error")] = F("unknown cmd");
return false;
}
return true;
}
//-----------------------------------------------------------------------------
Inverter<> *webApi::getInverter(DynamicJsonDocument jsonIn, JsonObject jsonOut) {
uint8_t id = jsonIn[F("inverter")];
Inverter<> *iv = mApp->mSys->getInverterByPos(id);
if(NULL == iv)
jsonOut[F("error")] = F("inverter index to high: ") + String(id);
return iv;
}

62
tools/esp8266/webApi.h

@ -0,0 +1,62 @@
#ifndef __WEB_API_H__
#define __WEB_API_H__
#include "dbg.h"
#ifdef ESP32
#include "AsyncTCP.h"
#else
#include "ESPAsyncTCP.h"
#endif
#include "ESPAsyncWebServer.h"
#include "AsyncJson.h"
#include "app.h"
class app;
class webApi {
public:
webApi(AsyncWebServer *srv, app *app, sysConfig_t *sysCfg, config_t *config, statistics_t *stat, char version[]);
void setup(void);
void loop(void);
private:
void onApi(AsyncWebServerRequest *request);
void onApiPost(AsyncWebServerRequest *request);
void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total);
void getNotFound(JsonObject obj, String url);
void getSystem(JsonObject obj);
void getStatistics(JsonObject obj);
void getInverterList(JsonObject obj);
void getMqtt(JsonObject obj);
void getNtp(JsonObject obj);
void getPinout(JsonObject obj);
void getRadio(JsonObject obj);
void getSerial(JsonObject obj);
void getIndex(JsonObject obj);
void getSetup(JsonObject obj);
void getLive(JsonObject obj);
void getRecord(JsonObject obj, record_t<> *rec);
bool setCtrl(DynamicJsonDocument jsonIn, JsonObject jsonOut);
bool setSetup(DynamicJsonDocument jsonIn, JsonObject jsonOut);
Inverter<> *getInverter(DynamicJsonDocument jsonIn, JsonObject jsonOut);
double round3(double value) {
return (int)(value * 1000 + 0.5) / 1000.0;
}
AsyncWebServer *mSrv;
app *mApp;
config_t *mConfig;
sysConfig_t *mSysCfg;
statistics_t *mStat;
char *mVersion;
};
#endif /*__WEB_API_H__*/
Loading…
Cancel
Save