diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml
index 6eba4879..7eaf9b34 100644
--- a/.github/workflows/compile_development.yml
+++ b/.github/workflows/compile_development.yml
@@ -5,14 +5,94 @@ on:
branches: development*
paths-ignore:
- '**.md' # Do no build on *.md changes
+
jobs:
- build:
+ check:
runs-on: ubuntu-latest
+ if: github.repository == 'lumapu/ahoy' && github.ref_name == 'development03'
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v3
+ build-en:
+ needs: check
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ strategy:
+ matrix:
+ variant:
+ - esp8266
+ - esp8266-prometheus
+ - esp8285
+ - esp32-wroom32
+ - esp32-wroom32-prometheus
+ - esp32-wroom32-ethernet
+ - esp32-s2-mini
+ - esp32-c3-mini
+ - opendtufusion
+ - opendtufusion-ethernet
steps:
- uses: actions/checkout@v3
+ - uses: benjlevesque/short-sha@v2.1
+ id: short-sha
+ with:
+ length: 7
+
+ - name: Cache Pip
+ uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+
+ - name: Cache PlatformIO
+ uses: actions/cache@v3
+ with:
+ path: ~/.platformio
+ key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
+
+ - name: Setup Python
+ uses: actions/setup-python@v4.3.0
+ with:
+ python-version: "3.x"
+
+ - name: Install PlatformIO
+ run: |
+ python -m pip install setuptools --upgrade pip
+ pip install --upgrade platformio
+
+ - name: Run PlatformIO
+ run: pio run -d src -e ${{ matrix.variant }}
+
+ - name: Rename Firmware
+ run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
+
+ - name: Create Artifact
+ uses: actions/upload-artifact@v4
with:
- ref: development03
+ name: dev-${{ matrix.variant }}
+ path: firmware/*
+
+ build-de:
+ needs: check
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ strategy:
+ matrix:
+ variant:
+ - esp8266-de
+ - esp8266-prometheus-de
+ - esp8285-de
+ - esp32-wroom32-de
+ - esp32-wroom32-prometheus-de
+ - esp32-wroom32-ethernet-de
+ - esp32-s2-mini-de
+ - esp32-c3-mini-de
+ - opendtufusion-de
+ - opendtufusion-ethernet-de
+ steps:
+ - uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
id: short-sha
with:
@@ -43,43 +123,49 @@ jobs:
pip install --upgrade platformio
- name: Run PlatformIO
- run: pio run -d src --environment esp8266 --environment esp8266-prometheus --environment esp8285 --environment esp32-wroom32 --environment esp32-wroom32-prometheus --environment esp32-wroom32-ethernet --environment esp32-s2-mini --environment esp32-c3-mini --environment opendtufusion --environment opendtufusion-ethernet
+ run: pio run -d src -e ${{ matrix.variant }}
- - name: Copy boot_app0.bin
- run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
+ - name: Rename Firmware
+ run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- - name: Rename Binary files
- id: rename-binary-files
- working-directory: src
- run: python ../scripts/getVersion.py >> $GITHUB_OUTPUT
+ - name: Create Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: dev-${{ matrix.variant }}
+ path: firmware/*
+
+ deploy:
+ needs: [build-en, build-de]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ #- name: Copy boot_app0.bin
+ # run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
+
+ - name: Get Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ merge-multiple: true
+ path: firmware
+
+ - name: Get Version from code
+ id: version_name
+ run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Set Version
uses: cschleiden/replace-tokens@v1
with:
files: tools/esp8266/User_Manual.md
env:
- VERSION: ${{ steps.rename-binary-files.outputs.name }}
-
- - name: Create Manifest
- working-directory: src
- run: python ../scripts/buildManifest.py
-
- - name: Create Artifact
- uses: actions/upload-artifact@v3
- with:
- name: ahoydtu_dev
- path: |
- src/firmware/*
- src/User_Manual.md
- src/install.html
+ VERSION: ${{ steps.version_name.outputs.name }}
- name: Rename firmware directory
- run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }}
+ run: mv firmware ${{ steps.version_name.outputs.name }}
- name: Deploy
uses: nogsantos/scp-deploy@master
with:
- src: src/${{ steps.rename-binary-files.outputs.name }}/
+ src: ${{ steps.version_name.outputs.name }}/
host: ${{ secrets.FW_SSH_HOST }}
remote: ${{ secrets.FW_SSH_DIR }}/dev
port: ${{ secrets.FW_SSH_PORT }}
diff --git a/Getting_Started.md b/Getting_Started.md
deleted file mode 100644
index f6a89dde..00000000
--- a/Getting_Started.md
+++ /dev/null
@@ -1,302 +0,0 @@
- ## Overview
-
-On this page, you'll find detailed instructions on how to wire the module of a Wemos D1 mini or ESP32 to the radio module, as well as how to flash it with the latest firmware. This information will enable you to communicate with compatible inverters.
-
-You find the full [User_Manual here](User_Manual.md)
-
-## Compatiblity
-
-The following inverters are currently supported out of the box:
-
-Hoymiles Inverters
-
-| Status | Serie | Model | comment |
-| ----- | ----- | ------ | ------- |
-| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet |
-| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | |
-| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
-| ✔️ | HMT | 1600, 1800, 2250 | |
-| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
-
-## Table of Contents
-
-- [Table of Contents](#table-of-contents)
-- [Overview](#overview)
-- [Compatiblity](#compatiblity)
-- [Things needed](#things-needed)
- - [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
-- [Wiring things up](#wiring-things-up)
- - [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
- - [Schematic](#schematic)
- - [Symbolic view](#symbolic-view)
- - [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
- - [Schematic](#schematic-2)
- - [Symbolic view](#symbolic-view-2)
- - [ESP32 wiring example](#esp32-wiring-example)
- - [Schematic](#schematic-1)
- - [Symbolic view](#symbolic-view-1)
- - [ESP32 GPIO settings](#esp32-gpio-settings)
-- [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware)
- - [Compiling your own Version](#compiling-your-own-version)
- - [Optional Configuration before compilation](#optional-configuration-before-compilation)
- - [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)
- - [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)
- - [HTTP based Pages](#http-based-pages)
-- [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface)
-- [Used Libraries](#used-libraries)
-- [ToDo](#todo)
-
-***
-
-Solenso Inverters:
-
-- SOL-H350
-
-## Things needed
-
-If you're interested in building your own AhoyDTU, you'll need a few things to get started. While we've provided a list of recommended boards below, keep in mind that the maker community is constantly developing new and innovative options that we may not have covered in this readme..
-
-For optimal performance, we recommend using a Wemos D1 mini or ESP32 along with a NRF24L01+ breakout board as a bare minimum. However, if you have experience working with other ESP boards, any board with at least 4MBytes of ROM may be suitable, depending on your skills.
-
-Just be sure that the NRF24L01+ module you choose includes the "+" in its name, as we rely on the 250kbps features that are only provided by the plus-variant.
-
-| **Parts** | **Price** |
-| --- | --- |
-| D1 ESP8266 Mini WLAN Board Microcontroller | 4,40 Euro |
-| NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul | 3,45 Euro |
-| 100µF / 10V Capacitor Kondensator | 0,15 Euro |
-| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
-| **Total costs** | **10,34 Euro** |
-
-If you're interested in using our sister project OpenDTU or you want to future-proof your setup, we recommend investing in an ESP32 board that features two CPU cores. As Radio you can also use a NRF24L01+ module with an external antenna. While this option may cost a bit more, it will provide superior performance and ensure compatibility with upcoming developments.
-
-| **Parts** | **Price** |
-| --- | --- |
-| ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 Euro |
-| NRF24L01+ PA LNA SMA mit Antenne Long | 4,50 Euro |
-| 100µF / 10V Capacitor Kondensator | 0,15 Euro |
-| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
-| **Total costs** | **14,89 Euro** |
-
-#### There are fake NRF24L01+ Modules out there
-
-Watch out, there are some fake NRF24L01+ Modules out there that seem to use rebranded NRF24L01 Chips (without the +).
-An example can be found in [Issue #230](https://github.com/lumapu/ahoy/issues/230).
-You are welcome to add more examples of faked chips. We will add that information here.
-
-Some users reported better connection or longer range through more walls when using the
-"E01-ML01DP5" EBYTE 2,4 GHz Wireless Modul nRF24L01 + PA + LNA RF Modul, SMA-K Antenna connector,
-which has an eye-catching HF cover. But beware: It comes without the antenna!
-
-In any case you should stabilize the Vcc power by a capacitor and don't exceed the Amplifier Power Level "LOW".
-Users reporting good connection over 10m through walls / ceilings with Amplifier Power Level "MIN".
-It is not always the bigger the better...
-
-Power levels "HIGH" and "MAX" are meant to wirings where the nRF24 is supplied by an extra 3.3 Volt regulator.
-The bultin regulator on ESP boards has only low reserves in case WiFi and nRF are sending simultaneously.
-If you operate additional interfaces like a display, the reserve is again reduced.
-
-## Wiring things up
-
-The NRF24L01+ radio module is connected to the standard SPI pins:
-
-- SCLK (Signal Clock),
-- MISO (Master In Slave Out) and
-- MOSI (Master Out Slave In)
-
-*These pins need to be configured in the config.h.*
-
-Additional, there are 3 pins, which can be set individual:
-
-- CS (Chip Select),
-- CE (Chip Enable) and
-- IRQ (Interrupt)
-
-*These pins can be changed from the /setup URL.*
-
-#### ESP8266 wiring example on WEMOS D1
-
-This is an example wiring using a Wemos D1 mini.
-
-##### Schematic
-
-
-
-##### Symbolic view
-
-
-
-#### ESP8266 wiring example on 30pin Lolin NodeMCU v3
-
-This is an example wiring using a NodeMCU V3.
-
-##### Schematic
-
-
-
-##### Symbolic view
-
-
-
-#### ESP32 wiring example
-
-Example wiring for a 38pin ESP32 module
-
-##### Schematic
-
-
-
-##### Symbolic view
-
-
-
-##### ESP32 GPIO settings
-
-CS, CE, IRQ must be set according to how they are wired up. For the diagram above, set the 3 individual GPIOs under the /setup URL as follows:
-
-```
-CS D1 (GPIO5)
-CE D2 (GPIO4)
-IRQ D0 (GPIO16 - no IRQ!)
-```
-
-IMPORTANT: From development version 108/release 0.6.0 onwards, also MISO, MOSI, and SCLK
-are configurable. On new installations, their defaults are correct for most ESP32 boards.
-These pins cannot be configured for ESP82xx boards, as this chip cannot move them elsewhere.
-
-If you are upgrading an existing install though, you might see that these pins are set to '0' in the web GUI.
-Communication with the NRF module wont work. For upgrading an existing installations, set MISO=19, MOSI=23, SCLK=18 in the settings.
-This is the correct default for most ESP32 boards. On ESP82xx, simply saving the settings without changes should suffice.
-Save and reboot.
-
-
-## 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 of our pre-compiled generic builds.
-
-### Flash from your browser (easy)
-
-The easiest step for you is to flash online. A browser MS Edge or Google Chrome is required.
-[Here you go](https://ahoydtu.de/web_install/)
-
-### Compiling your own Version
-
-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.
-Visual Studio Code, AtomIDE and other IDE's support the PlatformIO Addon.
-If you do not want to compile your own build, you can use one of our ready-to-flash binaries.
-
-##### Optional Configuration before compilation
-
-- number of supported inverters (set to 3 by default) `config.h`
-- DTU radio id `config.h` (default = 1234567801)
-- 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.
-config_override.h is excluded from version control and stays local.
-
-#### Using a ready-to-flash binary using nodemcu-pyflasher
-
-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)
-2. download latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
-3. open flash-tool and connect the target device to your computer.
-4. Set the correct serial port and select the correct *.bin file
-5. click on "Flash NodeMCU"
-6. flash the ESP with the compiled firmware using the UART pins or
-7. repower the ESP
-8. the ESP will start as access point (AP) if there is no network config stored in its eeprom
-9. connect to the AP (password: `esp_8266`), you will be forwarded to the setup page
-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.
-
-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!
-
-#### Flashing on Linux with `esptool.py` (ESP32)
-1. install [esptool.py](https://docs.espressif.com/projects/esptool/en/latest/esp32/) if you haven't already.
-2. download and extract the latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
-3. `cd ahoy_v && cp *esp32.bin esp32.bin`
-4. Perhaps you need to replace `/dev/ttyUSB0` to match your acual device in the following command. Execute it afterwards: `esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset write_flash --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 esp32.bin`
-5. Unplug and replug your device.
-6. Open a serial monitor (e.g. Putty) @ 115200 Baud. You should see some messages regarding wifi.
-
-## 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
-
- When connected to your computer, you can open a Serial Console to obtain additional information.
- This might be useful in case of any troubles that might occur as well as to simply
- obtain information about the converted values which were read out of the inverter(s).
-
-#### 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.
- If your Ahoy DTU was able to log into the configured WiFi Network, it will try to obtain an IP-Address
- from your local DHCP Server (in most cases thats your Router).
- In case it could not connect to your configured Network, it will provide its own WiFi Network that you can
- connect to for furter configuration.
- 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`".
- The Ahoy DTU will keep that Network open for a certain amount of time (also configurable in the config.h and defaults to 60secs).
- If nothing connects to it and that time runs up, it will retry to connect to the configured network an so on.
-
- 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.
- 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.4.1/](http://192.168.4.1/).
- Just open the IP-Address in your browser.
-
- The webinterface has the following abilities:
-
-- OTA Update (Over The Air Update)
-- Configuration (Wifi, inverter(s), NTP Server, Pinout, MQTT, Amplifier Power Level, Debug)
-- visual display of the connected inverters / modules
-- some statistics about communication (debug)
-
-##### HTTP based Pages
-
- 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.4.1/setup](http://192.168.4.1/setup) ).
-
-| page | use | output | default availability |
-| ---- | ------ | ------ | ------ |
-| /uptime | displays the uptime uf your Ahoy DTU | 0 Days, 01:37:34; now: 2022-08-21 11:13:53 | yes |
-| /reboot | reboots the Ahoy DTU | | yes |
-| /erase | erases the EEPROM | | yes |
-| /factory | resets to the factory defaults configured in config.h | | yes |
-| /setup | opens the setup page | | yes |
-| /save | | | yes |
-| /cmdstat | show stat from the home page | | yes |
-| /visualization | displays the information from your converter | | yes |
-| /livedata | displays the live data | | yes |
-| /metrics | gets live-data for prometheus | prometheus metrics from the livedata | no - enable via config_override.h |
-| /api | gets configuration and live-data in JSON format | json output from the configuration or livedata | yes |
-
-## MQTT command to set the DTU without webinterface
-
-[Read here](User_Manual.md)
-
-## Used Libraries
-
-| Name | version | License |
-| --------------------- | ------- | -------- |
-| `ESP8266WiFi` | 1.0 | LGPL-2.1 |
-| `DNSServer` | 1.1.1 | LGPL-2.1 |
-| `SPI` | 1.0 | LGPL-2.1 |
-| `Hash` | 1.0 | LGPL-2.1 |
-| `EEPROM` | 1.0 | LGPL-2.1 |
-| `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.7 | GPL-2.0 |
-| `espMqttClient` | 1.4.4 | MIT |
-| `ArduinoJson` | 6.21.3 | MIT |
-
-## ToDo
-
-[See this post](https://github.com/lumapu/ahoy/issues/142)
diff --git a/README.md b/README.md
index e352e414..7a51a98a 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ This work is licensed under a
# 🖐 Ahoy!

-This repository offers hardware and software solutions for communicating with Hoymiles inverters via radio. With our system, you can easily obtain real-time values such as power, current, and daily energy. Additionally, you can set parameters like the power limit of your inverter to achieve zero export. You can access these functionalities through our user-friendly web interface, MQTT, or JSON. Whether you're monitoring your solar panel system's performance or fine-tuning its settings, our solutions make it easy to achieve your goals.
+This repository provides hardware and software solutions for communicating with Hoymiles inverters via radio. Our system allows you to easily obtain real-time values, such as power, current, and daily energy, as well as set parameters like the power limit of your inverter to achieve zero export. You can access these functionalities through our user-friendly web interface, MQTT, or JSON. Our solutions simplify the process of monitoring and fine-tuning your solar panel system to help you achieve your goals.
## Changelog
[latest Release](https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md)
@@ -39,9 +39,11 @@ Table of approaches:
⚠️ **Warning: HMS-XXXXW-2T WiFi inverters are not supported. They have a 'W' in their name and a DTU serial number on its sticker**
## Getting Started
-[Guide how to start with a ESP module](Getting_Started.md)
+1. [Guide how to start with a ESP module](Getting_Started.md)
-[ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install)
+2. [ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install)
+
+3. [Ahoy Configuration ](ahoy_config.md)
## Our Website
[https://ahoydtu.de](https://ahoydtu.de)
@@ -50,11 +52,11 @@ Table of approaches:
- [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl)
## Support, Feedback, Information and Discussion
-- [Discord Server (~ 3.800 Users)](https://discord.gg/WzhxEY62mB)
+- [Discord Server (~ 7.300 Users)](https://discord.gg/WzhxEY62mB)
- [The root of development](https://www.mikrocontroller.net/topic/525778)
### Development
-If you run into any issues, please feel free to use the issue tracker here on Github. When describing your issue, please be as detailed and precise as possible, and take a moment to consider whether the issue is related to our software. This will help us to provide more effective solutions to your problem.
+If you encounter any problems, use the issue tracker on Github. Provide a detailed description of the issue and consider if it is related to our software. This will help us provide effective solutions.
**Contributors are always welcome!**
diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md
index 711299dd..4b266da4 100644
--- a/doc/prometheus_ep_description.md
+++ b/doc/prometheus_ep_description.md
@@ -1,10 +1,10 @@
# Prometheus Endpoint
Metrics available for AhoyDTU device, inverters and channels.
-Prometheus metrics provided at `/metrics`.
+Prometheus metrics provided at `/metrics`.
## Labels
-| Label name | Description |
+| Label name | Description |
|:-------------|:--------------------------------------|
| version | current installed version of AhoyDTU |
| image | currently not used |
@@ -19,11 +19,25 @@ Prometheus metrics provided at `/metrics`.
|----------------------------------------------|---------|----------------------------------------------------------|--------------|
| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename |
| `ahoy_solar_uptime` | Counter | Seconds since boot of the AhoyDTU device | devicename |
-| `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename |
+| `ahoy_solar_freeheap` | Gauge | free heap memory of the AhoyDTU device | devicename |
+| `ahoy_solar_wifi_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename |
| `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial |
| `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter |
| `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter |
| `ahoy_solar_inverter_is_producing` | Gauge | Is the inverter producing? | inverter |
+| `ahoy_solar_inverter_power_limit_read` | Gauge | Power Limit read from inverter. Defaults to 65535 | inverter |
+| `ahoy_solar_inverter_power_limit_ack` | Gauge | Power Limit acknowledged by inverter | inverter |
+| `ahoy_solar_inverter_max_power` | Gauge | Max Power of inverter | inverter |
+| `ahoy_solar_inverter_radio_rx_success` | Counter | NRF24 statistic of inverter | inverter |
+| `ahoy_solar_inverter_radio_rx_fail` | Counter | NRF24 statistic of inverter | inverter |
+| `ahoy_solar_inverter_radio_rx_fail_answer` | Counter | NRF24 statistic of inverter | inverter |
+| `ahoy_solar_inverter_radio_frame_cnt` | Counter | NRF24 statistic of inverter | inverter |
+| `ahoy_solar_inverter_radio_tx_cnt` | Counter | NRF24 statistic of inverter | inverter |
+| `ahoy_solar_inverter_radio_retransmits` | Counter | NRF24 statistic of inverter | inverter |
+| `ahoy_solar_inverter_radio_iv_loss_cnt` | Counter | NRF24 statistic of inverter | inverter |
+| `ahoy_solar_inverter_radio_iv_sent_cnt` | Counter | NRF24 statistic of inverter | inverter |
+| `ahoy_solar_inverter_radio_dtu_loss_cnt` | Counter | NRF24 statistic of inverter | inverter |
+| `ahoy_solar_inverter_radio_dtu_sent_cnt` | Counter | NRF24 statistic of inverter | inverter |
| `ahoy_solar_U_AC_volt` | Gauge | AC voltage of inverter [V] | inverter |
| `ahoy_solar_I_AC_ampere` | Gauge | AC current of inverter [A] | inverter |
| `ahoy_solar_P_AC_watt` | Gauge | AC power of inverter [W] | inverter |
@@ -46,9 +60,4 @@ Prometheus metrics provided at `/metrics`.
| `ahoy_solar_YieldDay_wattHours` | Counter | Energy converted to AC per day [Wh] | inverter, channel |
| `ahoy_solar_YieldTotal_kilowattHours` | Counter | Energy converted to AC since reset [kWh] | inverter, channel |
| `ahoy_solar_Irradiation_ratio` | Gauge | ratio DC Power over set maximum power per channel [%] | inverter, channel |
-| `ahoy_solar_radio_rx_success` | Gauge | NRF24 statistic | |
-| `ahoy_solar_radio_rx_fail` | Gauge | NRF24 statistic | |
-| `ahoy_solar_radio_rx_fail_answer` | Gauge | NRF24 statistic | |
-| `ahoy_solar_radio_frame_cnt` | Gauge | NRF24 statistic | |
-| `ahoy_solar_radio_tx_cnt` | Gauge | NRF24 statistic | |
diff --git a/doc/screenshots/inverterSettings.png b/doc/screenshots/inverterSettings.png
new file mode 100644
index 00000000..ef9484c1
Binary files /dev/null and b/doc/screenshots/inverterSettings.png differ
diff --git a/doc/screenshots/settings.png b/doc/screenshots/settings.png
new file mode 100644
index 00000000..179a5d07
Binary files /dev/null and b/doc/screenshots/settings.png differ
diff --git a/manual/Getting_Started.md b/manual/Getting_Started.md
new file mode 100644
index 00000000..b1ef7995
--- /dev/null
+++ b/manual/Getting_Started.md
@@ -0,0 +1,301 @@
+## Overview
+
+This page contains detailed instructions on building a module and flashing it with the latest firmware. Following these instructions will allow you to communicate with compatible inverters.
+
+You find the full [User_Manual here](User_Manual.md)
+
+## Compatiblity
+
+Currently, the following inverters are supported:
+
+Hoymiles Inverters
+
+| Status | Serie | Model | comment |
+| ----- | ----- | ------ | ------- |
+| ✔️ | MI | 300, 600, 1000/1200/⚠️ 1500 | 4-Channel is not tested yet |
+| ✔️ | HM | 300, 350, 400, 600, 700, 800, 1000?, 1200, 1500 | |
+| ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | |
+| ✔️ | HMT | 1600, 1800, 2250 | |
+| ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). |
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Compatiblity](#compatiblity)
+- [Things needed](#things-needed)
+ - [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
+- [Wiring things up](#wiring-things-up)
+ - [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
+ - [Schematic](#schematic)
+ - [Symbolic view](#symbolic-view)
+ - [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
+ - [Schematic](#schematic-2)
+ - [Symbolic view](#symbolic-view-2)
+ - [ESP32 wiring example](#esp32-wiring-example)
+ - [Schematic](#schematic-1)
+ - [Symbolic view](#symbolic-view-1)
+ - [ESP32 GPIO settings](#esp32-gpio-settings)
+- [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware)
+ - [Compiling your own Version](#compiling-your-own-version)
+ - [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher)
+- [Connect to your Ahoy DTU](#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)
+ - [Connect to the Ahoy DTU Webinterface using your Browser](#connect-to-the-ahoy-dtu-webinterface-using-your-browser)
+ - [HTTP based Pages](#http-based-pages)
+- [MQTT command to set the DTU without webinterface](#mqtt-command-to-set-the-dtu-without-webinterface)
+- [Used Libraries](#used-libraries)
+- [ToDo](#todo)
+
+***
+
+Solenso Inverters:
+
+- SOL-H350
+
+## Things needed
+
+To build your own AhoyDTU, you only need a few things. Remember that the maker community is always developing new and innovative options that we may not have covered in this readme.
+
+Start with an ESP8266 or ESP32, and combine it with an NRF24L01+ breakout board. Other ESP boards with at least 4MBytes of ROM may also be suitable.
+
+Make sure to choose an NRF24L01+ module that includes the '+' in its name. This is important because we need the 250kbps features that are only available in the plus-variant.
+
+**Attention**: The NRF24L01+ can only communicate with the MI/HM/TSUN inverter. For the HMS/HMT it is needed to use a CMT2300A!
+
+
+| **Parts** | **Price** |
+| --- | --- |
+| D1 ESP8266 Mini WLAN Board Microcontroller | 4,40 €|
+| *NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul (not for HMS/HMT)* | *3,45 €*|
+| *CMT2300A 868/915MHz (E49-900M20S)* | *4,59 €* |
+| 100µF / 10V Capacitor Kondensator | 0,15 €|
+| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 €|
+| **Total costs** | **10,34 € / 11,48 €** |
+
+To future-proof your setup and use our sister project OpenDTU, we recommend investing in an ESP32 board with two CPU cores. Additionally, you can use a NRF24L01+ module with an external antenna as a radio for superior performance and compatibility with upcoming developments.
+
+| **Parts** | **Price** |
+| --- | --- |
+| ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 €|
+| *NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul (not for HMS/HMT)* | *3,45 €*|
+| *CMT2300A 868/915MHz (E49-900M20S)* | *4,59 €* |
+| 100µF / 10V Capacitor Kondensator | 0,15 €|
+| Jumper Wire breadboard female-female | 2,49 €|
+| **Total costs** | **13,99 € / 15,13 €** |
+
+#### There are fake NRF24L01+ Modules out there
+Beware of fake NRF24L01+ modules that use rebranded NRF24L01 chips (without the +).
+An example of this can be found in Issue #230 (https://github.com/lumapu/ahoy/issues/230).
+If you have any additional examples of fake chips, please share them with us and we will add the information here.
+
+#### NRF24L01+ improvements
+Users have reported improved connections and longer range through walls when using these modules.
+The "E01-ML01DP5" module is a 2.4 GHz wireless module that utilizes the nRF24L01+PA+LNA RF module and features an SMA-K antenna connector.
+**The product includes an HF cover, but please note that it does not come with an antenna.**
+
+To achieve the best results, stabilize the Vcc power by using a capacitor and do not exceed the 'LOW' Amplifier Power Level.
+Users have reported good connections over 10m through walls and ceilings when using the Amplifier Power Level 'MIN'.
+It's important to remember that bigger is not always better.
+
+If you are using the NRF24 directly on the ESP board, make sure to set the transmission power to the lowest possible level (this can be adjusted later in the web interface). Using a high transmission power can potentially cause problems.
+The ESP board's built-in controller has limited reserves in case both WiFi and nRF are transmitting simultaneously.
+If you are using additional interfaces, such as a display, the reserves will be further reduced.
+
+## Wiring things up
+
+The NRF24L01+ radio module is connected to the standard SPI pins:
+
+- SCLK (Signal Clock),
+- MISO (Master In Slave Out) and
+- MOSI (Master Out Slave In)
+
+*These pins need to be configured in the config.h.*
+
+Additional, there are 3 pins, which can be set individual:
+
+- CS (Chip Select),
+- CE (Chip Enable) and
+- IRQ (Interrupt)
+
+*These pins can be changed from the /setup URL.*
+
+#### ESP8266 wiring example on WEMOS D1
+This is an example wiring using a Wemos D1 mini.
+
+##### Schematic
+
+
+##### Symbolic view
+
+
+#### ESP8266 wiring example on 30pin Lolin NodeMCU v3
+
+This is an example wiring using a NodeMCU V3.
+
+##### Schematic
+
+
+
+##### Symbolic view
+
+
+
+#### ESP32 wiring example
+
+Example wiring for a 38pin ESP32 module
+
+##### Schematic
+
+
+
+##### Symbolic view
+
+
+
+##### ESP32 GPIO settings
+
+CS, CE, IRQ must be set according to how they are wired up. For the diagram above, set the 3 individual GPIOs under the /setup URL as follows:
+
+```
+CS D1 (GPIO5)
+CE D2 (GPIO4)
+IRQ D0 (GPIO16 - no IRQ!)
+```
+
+**IMPORTANT**: Starting from development version 108/release 0.6.0, MISO, MOSI, and SCLK are also included.
+ For most ESP32 boards, the default settings are correct on new installations.
+However, it is not possible to configure these pins for ESP82xx boards, as they cannot be moved elsewhere.
+
+If you are upgrading an existing installation, you may notice that the pins are set to '0' in the web GUI, which will prevent communication with the NRF module.
+To resolve this, set MISO=19, MOSI=23, SCLK=18 in the settings.
+This is the correct default for most ESP32 boards. For ESP82xx, simply saving the settings without changes should suffice.
+Save and reboot.
+
+
+## Flash the Firmware on your Ahoy DTU Hardware
+
+After preparing your hardware, you must flash the Ahoy DTU Firmware to your board.
+You can either create your own firmware using your configuration or use one of our pre-compiled generic builds.
+
+Are you ready to flash? Then go to next Step here.
+
+### Flash from your browser (easy)
+
+The easiest step for you is to flash online. A browser MS Edge or Google Chrome is required.
+[Here you go](https://ahoydtu.de/web_install/)
+
+### Compiling your own Version (expert)
+This information is for those who wish to configure and build their own firmware.
+
+The code is provided as a PlatformIO project and can be compiled using the PlatformIO Addon.
+The PlatformIO Addon is supported by Visual Studio Code, AtomIDE, and other IDEs.
+If you do not wish to compile your own build, you can use one of our pre-compiled binaries.
+
+#### Using a ready-to-flash binary using nodemcu-pyflasher
+
+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)
+2. download latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
+3. open flash-tool and connect the target device to your computer.
+4. Set the correct serial port and select the correct *.bin file
+5. click on "Flash NodeMCU"
+6. flash the ESP with the compiled firmware using the UART pins or
+7. repower the ESP
+8. the ESP will start as access point (AP) if there is no network config stored in its eeprom
+9. connect to the AP (password: `esp_8266`), you will be forwarded to the setup page
+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.
+
+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!**
+
+#### Flashing on Linux with `esptool.py` (ESP32)
+1. install [esptool.py](https://docs.espressif.com/projects/esptool/en/latest/esp32/) if you haven't already.
+2. download and extract the latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases)
+3. `cd ahoy_v && cp *esp32.bin esp32.bin`
+4. Perhaps you need to replace `/dev/ttyUSB0` to match your acual device in the following command. Execute it afterwards: `esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset write_flash --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 esp32.bin`
+5. Unplug and replug your device.
+6. Open a serial monitor (e.g. Putty) @ 115200 Baud. You should see some messages regarding wifi.
+
+## Connect to your Ahoy DTU
+
+Once everything is wired and the firmware is flashed, it is time to connect to your Ahoy DTU.
+
+#### Your Ahoy DTU is very verbose using the Serial Console
+
+ Once connected to your computer, you can open a serial console to get additional information.
+ This can be useful for troubleshooting, as well as simply to get
+ information about the converted values read from the inverter(s).
+
+#### Connect to the Ahoy DTU Webinterface using your Browser
+
+ After you have successfully flashed and powered up your Ahoy DTU, you can access it from your browser.
+ If your Ahoy DTU was able to log on to the configured WiFi network, it will try to obtain an IP address from your local DHCP server (in most cases this is your router).
+
+ If it cannot connect to your configured network, it will provide its own WiFi network that you can
+ to for further configuration.
+
+ The WiFi SSID *(the WiFi name)* and password are pre-configured and are set to SSID "`AHOY-DTU`" and password "`esp_8266`" by default.
+
+ The Ahoy DTU will keep this network open for a certain amount of time (default is 60sec).
+ If nothing connects to it and the time expires, it will retry to connect to the configured network, and so on.
+
+ If you are connected to your local network, just find out the IP address used or try the default name [http://ahoy-dtu/](http://ahoy-dtu/).
+ In most cases, your router will give you a hint.
+
+ 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.4.1/](http://192.168.4.1/).
+ Just open the IP-Address in your browser.
+
+ The web interface has the following capabilities:
+
+- Live data (values updated every 5 seconds)
+ Click on the title/name/alarm for more actions.
+- Webserial (Debug)
+- Settings (System Config, Network, Protection, Inverter, NTP Server, Sunrise/Sunset, MQTT, Display Config)
+- Update (Over The Air Update)
+- System (status about the modules)
+
+##### HTTP based Pages
+
+ 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.4.1/setup](http://192.168.4.1/setup) ).
+
+| page | use | output | default availability |
+| ---- | ------ | ------ | ------ |
+| /logout| logout the user from webinterface | | yes |
+| /reboot | reboots the Ahoy DTU | | yes |
+| /system| show system inforamtion | | yes |
+| /live | displays the live data | | yes |
+| /save | | | yes |
+| /erase | erases the EEPROM | | yes |
+| /factory | resets to the factory defaults configured in config.h | | yes |
+| /setup | opens the setup page | | yes |
+| /metrics | gets live-data for prometheus | prometheus metrics from the livedata | no - enable via config_override.h |
+| /api | gets configuration and live-data in JSON format | json output from the configuration or livedata | yes |
+
+## MQTT command to set the DTU without webinterface
+
+[Read here](User_Manual.md)
+
+## Used Libraries
+
+| Name | version | License |
+| --------------------- | ------- | -------- |
+| `ESP8266WiFi` | 1.0 | LGPL-2.1 |
+| `DNSServer` | 1.1.1 | LGPL-2.1 |
+| `SPI` | 1.0 | LGPL-2.1 |
+| `Hash` | 1.0 | LGPL-2.1 |
+| `EEPROM` | 1.0 | LGPL-2.1 |
+| `ESPAsyncWebServer` | 1.2.3 | LGPL-3.0 |
+| [ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) | 1.2.2 | [LGPL-3.0 license](https://github.com/me-no-dev/ESPAsyncTCP#LGPL-3.0-1-ov-file) |
+| [Time](https://github.com/PaulStoffregen/Time) | 1.6.1 | ? |
+| [RF24](https://github.com/nRF24/RF24) | 1.4.8 | [GPL-2.0 license](https://github.com/nRF24/RF24#GPL-2.0-1-ov-file) |
+| [espMqttClient](https://github.com/bertmelis/espMqttClient) | ? | [MIT license](https://github.com/bertmelis/espMqttClient#MIT-1-ov-file) |
+| [ArduinoJson](https://github.com/bblanchon/ArduinoJson) | 6.21.3 | [MIT license](https://github.com/bblanchon/ArduinoJson#MIT-1-ov-file)|
+| [GxEPD2](https://github.com/ZinggJM/GxEPD2) | 1.5.2 | [GPL-3.0 license](https://github.com/ZinggJM/GxEPD2#GPL-3.0-1-ov-file)|
+| [U8g2_Arduino](https://registry.platformio.org/libraries/olikraus/U8g2) | [2.35.9](https://registry.platformio.org/libraries/olikraus/U8g2/versions) | [BSD-2-Clause](https://spdx.org/licenses/BSD-2-Clause.html) |
+
+## ToDo
+
+[See this post](https://github.com/lumapu/ahoy/issues/142)
diff --git a/User_Manual.md b/manual/User_Manual.md
similarity index 96%
rename from User_Manual.md
rename to manual/User_Manual.md
index b4ef8666..53da34f4 100644
--- a/User_Manual.md
+++ b/manual/User_Manual.md
@@ -195,8 +195,9 @@ The `` should be set to `1` = `ON` and `0` = `OFF`
}
```
+**beginning from verson `0.8.39` the wattage and percentage has one decimal place!**
-### Power Limit relative persistent [%]
+### Power Limit (active power control) relative persistent [%]
```json
{
@@ -205,10 +206,10 @@ The `` should be set to `1` = `ON` and `0` = `OFF`
"val":
}
```
-The `VALUE` represents a percent number in a range of `[2 .. 100]`
+The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]`
-### Power Limit absolute persistent [Watts]
+### Power Limit (active power control) absolute persistent [Watts]
```json
{
@@ -217,10 +218,10 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]`
"val":
}
```
-The `VALUE` represents watts in a range of `[0 .. 65535]`
+The `VALUE` represents watts in a range of `[1.0 .. 6553.5]`
-### Power Limit relative non persistent [%]
+### Power Limit (active power control) relative non persistent [%]
```json
{
@@ -229,10 +230,10 @@ The `VALUE` represents watts in a range of `[0 .. 65535]`
"val":
}
```
-The `VALUE` represents a percent number in a range of `[2 .. 100]`
+The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]`
-### Power Limit absolute non persistent [Watts]
+### Power Limit (active power control) absolute non persistent [Watts]
```json
{
@@ -241,7 +242,7 @@ The `VALUE` represents a percent number in a range of `[2 .. 100]`
"val":
}
```
-The `VALUE` represents watts in a range of `[0 .. 65535]`
+The `VALUE` represents watts in a range of `[1.0 .. 6553.5]`
@@ -328,7 +329,7 @@ Send Power Limit:
- If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit.
### Update your AHOY-DTU Firmware
To update your AHOY-DTU, you have to download the latest firmware package.
-Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip) available for download.
+Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://fw.ahoydtu.de/dev) available for download.
As soon as you have downloaded the firmware package, unzip it. On the WebUI, navigate to Update and press on select firmware file.
From the unzipped files, select the right .bin file for your hardware and needs.
- If you use an ESP8266, select the file ending with esp8266.bin
diff --git a/manual/ahoy_config.md b/manual/ahoy_config.md
new file mode 100644
index 00000000..1971848f
--- /dev/null
+++ b/manual/ahoy_config.md
@@ -0,0 +1,71 @@
+# Ahoy configuration
+
+## Prerequists
+You have build your own hardware (or purchased one). The firmware is already loaded on the ESP and the WebUI is accessible from your browser.
+
+## Start
+But how do I get my data from the inverter?
+
+The following steps are required:
+1. Set the pinning to communicate with the radio module.
+2. Check if Ahoy has a current time
+3. Configure the inverter data (e.g. serialnumber)
+
+### 1.) Set the pinning
+Once you are in the web interface, you will find the "System Config" sub-item in the Setup area.
+
+This is where you tell the ESP how you connected the radio module.
+Note the schematics you saw earlier. - If you haven't noticed them yet, here's another table of connections.
+
+
+#### OpenDTU Fusion (ESP32-S3)
+| NRF24 Pin | ESP Pin|
+|---------| --------|
+| CS (4) | GPIO37
+| CE (3)| GPIO38
+| IRQ (8) | GPIO47
+| SCLK (5)| GPIO36
+| MOSI (6)| GPIO35
+| MISO (7)| GPIO48
+
+| CMT2300A | Pin |
+|---------| --------|
+| CMT| Enabled |
+| SCLK| GPIO6
+| SDIO| GPIO5
+| CSB| GPIO4
+| FCSB| GPIO21
+| GPIO3| GPIO8
+
+### 2.) Set current time (standard: skip this step)
+Ahoy needs a current date and time to talk to the inverter.
+It works without, but it is recommended to include a time. This allows you to analyze information from the inverter in more detail.
+Normally, a date/time should be automatically retrieved from the NTP server. However, it may happen that the firewall of some routers does not allow this.
+In the section "Settings -> NTP Server" you can also get the time from your own computer. Or set up your own NTP server.
+
+### 3.) Set inverter data
+
+#### add new inverter
+Now it's time to place the inverter. This is necessary because it is not the inverter that speaks first, but the DTU (Ahoy).
+
+Each inverter has its own S.Nr. This also serves as an identity for communication between the DTU and the inverter.
+
+The S.Nr is a 12-digit number. Check [here (german)](https://github.com/lumapu/ahoy/wiki/Hardware#wie-ist-die-serien-nummer-der-inverter-aufgebaut) for more information.
+
+#### set pv-modules (not necessary)
+Click on "Add Inverter" and enter the S.No. and a name. Please keep the name short!
+
+
+
+
+In the upper tab "Inputs" you can enter the data of the solar modules. These are only used directly in Ahoy for calculation and have no influence on the inverter.
+
+#### set radio parameter (not necessary, only for EU)
+In the next tab "Radio" you can adjust the power and other parameters if necessary. However, these should be left as default (EU only).
+
+#### advanced options (not necessary to be changed)
+In the "Advanced" section, you can customize more settings.
+
+Save and reboot.
+
+## ✅ Done - Now check the live site
diff --git a/patches/GxEPD2_SW_SPI.patch b/patches/GxEPD2_SW_SPI.patch
index 9697eec8..dc3fa9ca 100644
--- a/patches/GxEPD2_SW_SPI.patch
+++ b/patches/GxEPD2_SW_SPI.patch
@@ -1,5 +1,5 @@
diff --git a/src/GxEPD2_EPD.cpp b/src/GxEPD2_EPD.cpp
-index 1588444..592869b 100644
+index 8df8bef..91d7f49 100644
--- a/src/GxEPD2_EPD.cpp
+++ b/src/GxEPD2_EPD.cpp
@@ -19,9 +19,9 @@
@@ -71,7 +71,7 @@ index 1588444..592869b 100644
void GxEPD2_EPD::_reset()
{
if (_rst >= 0)
-@@ -174,115 +169,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
+@@ -174,115 +171,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time)
void GxEPD2_EPD::_writeCommand(uint8_t c)
{
@@ -304,7 +304,7 @@ index 1588444..592869b 100644
+ _endTransaction();
}
diff --git a/src/GxEPD2_EPD.h b/src/GxEPD2_EPD.h
-index ef2318f..50aa961 100644
+index 34c1145..c480b7d 100644
--- a/src/GxEPD2_EPD.h
+++ b/src/GxEPD2_EPD.h
@@ -8,6 +8,10 @@
@@ -334,7 +334,7 @@ index ef2318f..50aa961 100644
protected:
void _reset();
void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000);
-@@ -111,9 +115,14 @@ class GxEPD2_EPD
+@@ -111,17 +115,22 @@ class GxEPD2_EPD
void _startTransfer();
void _transfer(uint8_t value);
void _endTransfer();
@@ -351,7 +351,9 @@ index ef2318f..50aa961 100644
bool _diag_enabled, _pulldown_rst_mode;
- SPIClass* _pSPIx;
SPISettings _spi_settings;
-@@ -123,5 +124,5 @@ class GxEPD2_EPD
+ bool _initial_write, _initial_refresh;
+ bool _power_is_on, _using_partial_mode, _hibernating;
+ bool _init_display_done;
uint16_t _reset_duration;
- void (*_busy_callback)(const void*);
+ void (*_busy_callback)(const void*);
diff --git a/scripts/auto_firmware_version.py b/scripts/auto_firmware_version.py
index c4ab270d..75bf7379 100644
--- a/scripts/auto_firmware_version.py
+++ b/scripts/auto_firmware_version.py
@@ -21,7 +21,8 @@ def get_firmware_specifier_build_flag():
except:
build_version = "g0000000"
- build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version[1:] + "\\\""
+ build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version[1:] + "\\\" "
+ build_flag += "-DENV_NAME=\\\"" + env["PIOENV"] + "\\\" ";
print ("Firmware Revision: " + build_version)
return (build_flag)
diff --git a/scripts/convertHtml.py b/scripts/convertHtml.py
index 6eaa92a3..c39e95ac 100644
--- a/scripts/convertHtml.py
+++ b/scripts/convertHtml.py
@@ -3,6 +3,7 @@ import os
import gzip
import glob
import shutil
+import json
from datetime import date
from pathlib import Path
import subprocess
@@ -22,18 +23,33 @@ def readVersion(path):
today = date.today()
search = ["_MAJOR", "_MINOR", "_PATCH"]
- version = today.strftime("%y%m%d") + "_ahoy_"
ver = ""
for line in lines:
if(line.find("VERSION_") != -1):
for s in search:
p = line.find(s)
if(p != -1):
- version += line[p+13:].rstrip() + "."
ver += line[p+13:].rstrip() + "."
return ver[:-1]
-def htmlParts(file, header, nav, footer, version):
+def readVersionFull(path):
+ f = open(path, "r")
+ lines = f.readlines()
+ f.close()
+
+ today = date.today()
+ search = ["_MAJOR", "_MINOR", "_PATCH"]
+ version = today.strftime("%y%m%d") + "_ahoy_"
+ for line in lines:
+ if(line.find("VERSION_") != -1):
+ for s in search:
+ p = line.find(s)
+ if(p != -1):
+ version += line[p+13:].rstrip() + "."
+ version = version[:-1] + "_" + get_git_sha()
+ return version
+
+def htmlParts(file, header, nav, footer, versionPath, lang):
p = "";
f = open(file, "r")
lines = f.readlines()
@@ -58,12 +74,16 @@ def htmlParts(file, header, nav, footer, version):
p += line
#placeholders
+ version = readVersion(versionPath);
link = 'GIT SHA: ' + get_git_sha() + ' :: ' + version + ''
p = p.replace("{#VERSION}", version)
+ p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath))
p = p.replace("{#VERSION_GIT}", link)
# remove if - endif ESP32
p = checkIf(p)
+ p = translate(file, p, lang)
+ p = translate("general", p, lang) # menu / header / footer
f = open("tmp/" + file, "w")
f.write(p);
@@ -94,7 +114,30 @@ def checkIf(data):
return data
-def convert2Header(inFile, version):
+def findLang(file):
+ with open('../lang.json') as j:
+ lang = json.load(j)
+
+ for l in lang["files"]:
+ if l["name"] == file:
+ return l
+
+ return None
+
+def translate(file, data, lang="de"):
+ json = findLang(file)
+
+ if None != json:
+ matches = re.findall(r'\{\#([A-Z0-9_]+)\}', data)
+ for x in matches:
+ for e in json["list"]:
+ if x == e["token"]:
+ #print("replace " + "{#" + x + "}" + " with " + e[lang])
+ data = data.replace("{#" + x + "}", e[lang])
+ return data
+
+
+def convert2Header(inFile, versionPath, lang):
fileType = inFile.split(".")[1]
define = inFile.split(".")[0].upper()
define2 = inFile.split(".")[1].upper()
@@ -114,7 +157,7 @@ def convert2Header(inFile, version):
f.close()
else:
if fileType == "html":
- data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version)
+ data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", versionPath, lang)
else:
f = open(inFile, "r")
data = f.read()
@@ -167,8 +210,12 @@ for files in types:
Path("h").mkdir(exist_ok=True)
Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements
shutil.copyfile("style.css", "tmp/style.css")
-version = readVersion("../../defines.h")
+
+# get language from environment
+lang = "en"
+if env['PIOENV'][-3:] == "-de":
+ lang = "de"
# go throw the array
for val in files_grabbed:
- convert2Header(val, version)
+ convert2Header(val, "../../defines.h", lang)
diff --git a/scripts/getVersion.py b/scripts/getVersion.py
index cdb39ae8..a60a772d 100644
--- a/scripts/getVersion.py
+++ b/scripts/getVersion.py
@@ -2,6 +2,7 @@ import os
import shutil
import gzip
from datetime import date
+import sys
def genOtaBin(path):
arr = []
@@ -32,8 +33,8 @@ def gzip_bin(bin_file, gzip_file):
with gzip.open(gzip_file, "wb", compresslevel = 9) as f:
shutil.copyfileobj(fp, f)
-def readVersion(path, infile):
- f = open(path + infile, "r")
+def getVersion(path_define):
+ f = open(path_define, "r")
lines = f.readlines()
f.close()
@@ -48,106 +49,44 @@ def readVersion(path, infile):
if(p != -1):
version += line[p+13:].rstrip() + "."
versionnumber += line[p+13:].rstrip() + "."
-
- os.mkdir(path + "firmware/")
- os.mkdir(path + "firmware/ESP8266/")
- os.mkdir(path + "firmware/ESP8285/")
- os.mkdir(path + "firmware/ESP32/")
- os.mkdir(path + "firmware/ESP32-S2/")
- os.mkdir(path + "firmware/ESP32-S3/")
- os.mkdir(path + "firmware/ESP32-C3/")
- os.mkdir(path + "firmware/ESP32-S3-ETH/")
- sha = os.getenv("SHA",default="sha")
- versionout = version[:-1] + "_" + sha + "_esp8266.bin"
- src = path + ".pio/build/esp8266/firmware.bin"
- dst = path + "firmware/ESP8266/" + versionout
- os.rename(src, dst)
+ return [version, versionnumber]
- versionout = version[:-1] + "_" + sha + "_esp8266_prometheus.bin"
- src = path + ".pio/build/esp8266-prometheus/firmware.bin"
- dst = path + "firmware/ESP8266/" + versionout
- os.rename(src, dst)
+def renameFw(path_define, env):
+ version = getVersion(path_define)[0]
- versionout = version[:-1] + "_" + sha + "_esp8285.bin"
- src = path + ".pio/build/esp8285/firmware.bin"
- dst = path + "firmware/ESP8285/" + versionout
- os.rename(src, dst)
- gzip_bin(dst, dst + ".gz")
-
- versionout = version[:-1] + "_" + sha + "_esp32.bin"
- src = path + ".pio/build/esp32-wroom32/firmware.bin"
- dst = path + "firmware/ESP32/" + versionout
- os.rename(src, dst)
-
- versionout = version[:-1] + "_" + sha + "_esp32_prometheus.bin"
- src = path + ".pio/build/esp32-wroom32-prometheus/firmware.bin"
- dst = path + "firmware/ESP32/" + versionout
- os.rename(src, dst)
-
- versionout = version[:-1] + "_" + sha + "_esp32_ethernet.bin"
- src = path + ".pio/build/esp32-wroom32-ethernet/firmware.bin"
- dst = path + "firmware/ESP32/" + versionout
- os.rename(src, dst)
-
- versionout = version[:-1] + "_" + sha + "_esp32s2-mini.bin"
- src = path + ".pio/build/esp32-s2-mini/firmware.bin"
- dst = path + "firmware/ESP32-S2/" + versionout
- os.rename(src, dst)
-
- versionout = version[:-1] + "_" + sha + "_esp32c3-mini.bin"
- src = path + ".pio/build/esp32-c3-mini/firmware.bin"
- dst = path + "firmware/ESP32-C3/" + versionout
- os.rename(src, dst)
-
- versionout = version[:-1] + "_" + sha + "_esp32s3.bin"
- src = path + ".pio/build/opendtufusion/firmware.bin"
- dst = path + "firmware/ESP32-S3/" + versionout
- os.rename(src, dst)
-
- versionout = version[:-1] + "_" + sha + "_esp32s3_ethernet.bin"
- src = path + ".pio/build/opendtufusion-ethernet/firmware.bin"
- dst = path + "firmware/ESP32-S3-ETH/" + versionout
- os.rename(src, dst)
-
- # other ESP32 bin files
- src = path + ".pio/build/esp32-wroom32/"
- dst = path + "firmware/ESP32/"
- os.rename(src + "bootloader.bin", dst + "bootloader.bin")
- os.rename(src + "partitions.bin", dst + "partitions.bin")
- genOtaBin(dst)
-
- # other ESP32-S2 bin files
- src = path + ".pio/build/esp32-s2-mini/"
- dst = path + "firmware/ESP32-S2/"
- os.rename(src + "bootloader.bin", dst + "bootloader.bin")
- os.rename(src + "partitions.bin", dst + "partitions.bin")
- genOtaBin(dst)
+ os.mkdir("firmware/")
+ fwDir = ""
+ if env[:7] == "esp8266":
+ fwDir = "ESP8266/"
+ elif env[:7] == "esp8285":
+ fwDir = "ESP8285/"
+ elif env[:7] == "esp32-w":
+ fwDir = "ESP32/"
+ elif env[:8] == "esp32-s2":
+ fwDir = "ESP32-S2/"
+ elif env[:4] == "open":
+ fwDir = "ESP32-S3/"
+ elif env[:8] == "esp32-c3":
+ fwDir = "ESP32-C3/"
+ os.mkdir("firmware/" + fwDir)
+ sha = os.getenv("SHA",default="sha")
- # other ESP32-C3 bin files
- src = path + ".pio/build/esp32-c3-mini/"
- dst = path + "firmware/ESP32-C3/"
- os.rename(src + "bootloader.bin", dst + "bootloader.bin")
- os.rename(src + "partitions.bin", dst + "partitions.bin")
- genOtaBin(dst)
+ dst = "firmware/" + fwDir
+ fname = version[:-1] + "_" + sha + "_" + env + ".bin"
- # other ESP32-S3 bin files
- src = path + ".pio/build/opendtufusion/"
- dst = path + "firmware/ESP32-S3/"
- os.rename(src + "bootloader.bin", dst + "bootloader.bin")
- os.rename(src + "partitions.bin", dst + "partitions.bin")
- genOtaBin(dst)
+ os.rename("src/.pio/build/" + env + "/firmware.bin", dst + fname)
- # other ESP32-S3-Eth bin files
- src = path + ".pio/build/opendtufusion-ethernet/"
- dst = path + "firmware/ESP32-S3-ETH/"
- os.rename(src + "bootloader.bin", dst + "bootloader.bin")
- os.rename(src + "partitions.bin", dst + "partitions.bin")
- genOtaBin(dst)
+ if env[:5] == "esp32":
+ os.rename("src/.pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin")
+ os.rename("src/.pio/build/" + env + "/partitions.bin", dst + "partitions.bin")
+ genOtaBin(dst)
- os.rename("../scripts/gh-action-dev-build-flash.html", path + "install.html")
+ if env[:7] == "esp8285":
+ gzip_bin(dst + fname, dst + fname[:-4] + ".gz")
- print("name=" + versionnumber[:-1] )
-
-
-readVersion("", "defines.h")
+if len(sys.argv) == 1:
+ print("name=" + getVersion("src/defines.h")[1][:-1])
+else:
+ # arg1: environment
+ renameFw("src/defines.h", sys.argv[1])
diff --git a/scripts/reduceGxEPD2.py b/scripts/reduceGxEPD2.py
new file mode 100644
index 00000000..f05e5c3b
--- /dev/null
+++ b/scripts/reduceGxEPD2.py
@@ -0,0 +1,41 @@
+import os
+import subprocess
+import glob
+Import("env")
+
+def rmDirWithFiles(path):
+ if os.path.isdir(path):
+ for f in glob.glob(path + "/*"):
+ os.remove(f)
+ os.rmdir(path)
+
+def clean(libName):
+ # save current wd
+ start = os.getcwd()
+
+ if os.path.exists('.pio/libdeps/' + env['PIOENV'] + '/' + libName) == False:
+ print("path '" + '.pio/libdeps/' + env['PIOENV'] + '/' + libName + "' does not exist")
+ return
+
+ os.chdir('.pio/libdeps/' + env['PIOENV'] + '/' + libName)
+ os.chdir('src/')
+ types = ('epd/*.h', 'epd/*.cpp') # the tuple of file types
+ files = []
+ for t in types:
+ files.extend(glob.glob(t))
+
+ for f in files:
+ if f.count('GxEPD2_150_BN') == 0:
+ os.remove(f)
+
+ rmDirWithFiles("epd3c")
+ rmDirWithFiles("epd4c")
+ rmDirWithFiles("epd7c")
+ rmDirWithFiles("gdeq")
+ rmDirWithFiles("gdey")
+ rmDirWithFiles("it8951")
+
+ os.chdir(start)
+
+
+clean("GxEPD2")
diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json
index fa166086..8930e87f 100644
--- a/src/.vscode/settings.json
+++ b/src/.vscode/settings.json
@@ -84,5 +84,5 @@
},
"cmake.configureOnOpen": false,
"editor.formatOnSave": false,
- "cmake.sourceDirectory": "C:/lpusch/github/ahoy/src/.pio/libdeps/esp32-wroom32-release-prometheus/Adafruit BusIO",
-}
\ No newline at end of file
+ "cmake.sourceDirectory": "C:/lpusch/github/ahoy/src/.pio/libdeps/esp32-wroom32-release-prometheus/Adafruit BusIO"
+}
diff --git a/src/CHANGES.md b/src/CHANGES.md
index 32efd79b..a20c20be 100644
--- a/src/CHANGES.md
+++ b/src/CHANGES.md
@@ -1,5 +1,159 @@
# Development Changes
+## 0.8.63 - 2024-01-22
+* made code review
+* fixed endless loop #1387
+
+## 0.8.62 - 2024-01-21
+* updated version in footer #1381
+* repaired radio statistics #1382
+
+## 0.8.61 - 2024-01-21
+* add favicon to header
+* improved NRF communication
+* merge PR: provide localized times to display mono classes #1376
+* merge PR: Bypass OOM-Crash on minimal version & history access #1378
+* merge PR: Add some REST Api Endpoints to avail_endpoints #1380
+
+## 0.8.60 - 2024-01-20
+* merge PR: non blocking nRF loop #1371
+* merge PR: fixed millis in serial log #1373
+* merge PR: fix powergraph scale #1374
+* changed inverter gap to `1` as default (old settings will be overridden)
+
+## 0.8.59 - 2024-01-18
+* merge PR: solve display settings dependencies #1369
+* fix language typos #1346
+* full update of ePaper after booting #1107
+* fix MqTT yield day reset even if `pause inverter during nighttime` isn't active #1368
+
+## 0.8.58 - 2024-01-17
+* fix missing refresh URL #1366
+* fix view of grid profile #1365
+* fix webUI translation #1346
+* fix protection mask #1352
+* merge PR: Add Watchdog for ESP32 #1367
+* merge PR: ETH support for CMT2300A - HMS/HMT #1356
+* full refresh of ePaper after booting #1107
+* add optional custom link #1199
+* pinout has an own subgroup in `/settings`
+* grid profile will be displayed as hex in every case #1199
+
+## 0.8.57 - 2024-01-15
+* merge PR: fix immediate clearing of display after sunset #1364
+* merge PR: MI-MQTT and last retransmit #1363
+* fixed DTU-ID, now built from the unique part of the MAC
+* fix lang in `/system` #1346
+* added protection to prevent update to wrong firmware (environment check)
+
+## 0.8.56 - 2024-01-15
+* potential fix of update problems and random reboots #1359 #1354
+
+## 0.8.55 - 2024-01-14
+* merge PR: fix reboot problem with deactivated power graph #1360
+* changed scope of variables and member functions inside display classes
+* removed automatically "minimal" builds
+* fix include of "settings.h" (was already done in #1360)
+* merge PR: Enhancement: Add info about compiled modules to version string #1357
+* add info about installed binary to `/update` #1353
+* fix lang in `/system` #1346
+
+## 0.8.54 - 2024-01-13
+* added minimal version (without: MqTT, Display, History), WebUI is not changed!
+* added simulator (must be activated before compile, standard: off)
+* changed communication attempts back to 5
+
+## 0.8.53 - 2024-01-12
+* fix history graph
+* fix MqTT yield day #1331
+
+## 0.8.52 - 2024-01-11
+* possible fix of 'division by zero' #1345
+* fix lang #1348 #1346
+* fix timestamp `max AC power` #1324
+* fix stylesheet overlay `max AC power` #1324
+* fix download link #1340
+* fix history graph
+* try to fix #1331
+
+## 0.8.51 - 2024-01-10
+* fix translation #1346
+* further improve sending active power control command faster #1332
+* added history protection mask
+* merge PR: display graph improvements #1347
+
+## 0.8.50 - 2024-01-09
+* merge PR: added history charts to web #1336
+* merge PR: small display changes #1339
+* merge PR: MI - add "get loss logic" #1341
+* translated `/history`
+* fix translations in title of documents
+* added translations for error messages #1343
+
+## 0.8.49 - 2024-01-08
+* fix send total values if inverter state is different from `OFF` #1331
+* fix german language issues #1335
+
+## 0.8.48 - 2024-01-07
+* merge PR: pin selection for ESP-32 S2 #1334
+* merge PR: enhancement: power graph display option #1330
+
+## 0.8.47 - 2024-01-06
+* reduce GxEPD2 lib to compile faster
+* upgraded GxEPD2 lib to `1.5.3`
+* updated espressif32 platform to `6.5.0`
+* updated U8g2 to `2.35.9`
+* started to convert deprecated functions of new ArduinoJson `7.0.0`
+* started to have german translations of all variants (environments) #925 #1199
+* merge PR: add defines for retry attempts #1329
+
+## 0.8.46 - 2024-01-06
+* improved communication
+
+## 0.8.45 - 2024-01-05
+* fix MqTT total values #1326
+* start implementing a wizard for initial (WiFi) configuration #1199
+
+## 0.8.44 - 2024-01-05
+* fix MqTT transmission of data #1326
+* live data is read much earlier / faster and more often #1272
+
+## 0.8.43 - 2024-01-04
+* fix display of sunrise in `/system` #1308
+* fix overflow of `getLossRate` calculation #1318
+* improved MqTT by marking sent data and improved `last_success` resends #1319
+* added timestamp for `max ac power` as tooltip #1324 #1123 #1199
+* repaired Power-limit acknowledge #1322
+* fix `max_power` in `/visualization` was set to `0` after sunset
+
+## 0.8.42 - 2024-01-02
+* add LED to display whether it's night time or not. Can be reused as output to control battery system #1308
+* merge PR: beautifiying typography, added spaces between value and unit for `/visualization` #1314
+* merge PR: Prometheus add `getLossRate` and bugfixing #1315
+* add loss rate to `/visualization` in the statistics window
+* corrected `getLossRate` infos for MqTT and prometheus
+* added information about working IRQ for NRF24 and CMT2300A to `/system`
+
+## 0.8.41 - 2024-01-02
+* fix display timeout (OLED) to 60s
+* change offs to signed value
+
+## 0.8.40 - 2024-01-02
+* fix display of sunrise and sunset in `/system` #1308
+* fix MqTT set power limit #1313
+
+## 0.8.39 - 2024-01-01
+* fix MqTT dis_night_comm in the morning #1309 #1286
+* seperated offset for sunrise and sunset #1308
+* powerlimit (active power control) now has one decimal place (MqTT / API) #1199
+* merge Prometheus metrics fix #1310
+* merge MI grid profile request #1306
+* merge update documentation / readme #1305
+* add `getLossRate` to radio statistics and to MqTT #1199
+
+## 0.8.38 - 2023-12-31
+* fix Grid-Profile JSON #1304
+
## 0.8.37 - 2023-12-30
* added grid profiles
* format version of grid profile
diff --git a/src/app.cpp b/src/app.cpp
index 0bba14da..7ef64b45 100644
--- a/src/app.cpp
+++ b/src/app.cpp
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://ahoydtu.de
+// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -7,6 +7,10 @@
#include "app.h"
#include "utils/sun.h"
+#if !defined(ESP32)
+ void esp_task_wdt_reset() {}
+#endif
+
//-----------------------------------------------------------------------------
app::app() : ah::Scheduler {} {}
@@ -18,7 +22,13 @@ void app::setup() {
while (!Serial)
yield();
+ #if defined(ESP32)
+ esp_task_wdt_init(WDT_TIMEOUT_SECONDS, true);
+ esp_task_wdt_add(NULL);
+ #endif
+
resetSystem();
+ esp_task_wdt_reset();
mSettings.setup();
mSettings.getPtr(mConfig);
@@ -30,12 +40,14 @@ void app::setup() {
else
DBGPRINTLN(F("false"));
+ esp_task_wdt_reset();
+
if(mConfig->nrf.enabled) {
mNrfRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso);
}
#if defined(ESP32)
if(mConfig->cmt.enabled) {
- mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, false);
+ mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb);
}
#endif
#ifdef ETHERNET
@@ -50,9 +62,14 @@ void app::setup() {
#endif
#endif /* defined(ETHERNET) */
+ esp_task_wdt_reset();
+
mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs);
mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
- mSys.setup(&mTimestamp, &mConfig->inst);
+ #if defined(ENABLE_MQTT)
+ mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); });
+ #endif
+ mSys.setup(&mTimestamp, &mConfig->inst, this);
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
initInverter(i);
}
@@ -62,8 +79,11 @@ void app::setup() {
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
+ esp_task_wdt_reset();
+
// when WiFi is in client mode, then enable mqtt broker
#if !defined(AP_ONLY)
+ #if defined(ENABLE_MQTT)
mMqttEnabled = (mConfig->mqtt.broker[0] > 0);
if (mMqttEnabled) {
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime);
@@ -71,8 +91,11 @@ void app::setup() {
mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
}
#endif
+ #endif
setupLed();
+ esp_task_wdt_reset();
+
mWeb.setup(this, &mSys, mConfig);
mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
@@ -83,7 +106,7 @@ void app::setup() {
#endif
// Plugins
#if defined(PLUGIN_DISPLAY)
- if (mConfig->plugin.display.type != 0)
+ if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
#if defined(ESP32)
mDisplay.setup(this, &mConfig->plugin.display, &mSys, &mNrfRadio, &mCmtRadio, &mTimestamp);
#else
@@ -91,19 +114,37 @@ void app::setup() {
#endif
#endif
+ esp_task_wdt_reset();
+
+ #if defined(ENABLE_HISTORY)
+ mHistory.setup(this, &mSys, mConfig, &mTimestamp);
+ #endif /*ENABLE_HISTORY*/
+
mPubSerial.setup(mConfig, &mSys, &mTimestamp);
#if !defined(ETHERNET)
//mImprov.setup(this, mConfig->sys.deviceName, mVersion);
#endif
+ #if defined(ENABLE_SIMULATOR)
+ mSimulator.setup(&mSys, &mTimestamp, 0);
+ mSimulator.addPayloadListener([this](uint8_t cmd, Inverter<> *iv) {
+ payloadEventListener(cmd, iv);
+ });
+ #endif /*ENABLE_SIMULATOR*/
+
+
+ esp_task_wdt_reset();
regularTickers();
}
//-----------------------------------------------------------------------------
void app::loop(void) {
+ esp_task_wdt_reset();
+
if(mConfig->nrf.enabled)
mNrfRadio.loop();
+
#if defined(ESP32)
if(mConfig->cmt.enabled)
mCmtRadio.loop();
@@ -112,8 +153,11 @@ void app::loop(void) {
ah::Scheduler::loop();
mCommunication.loop();
+ #if defined(ENABLE_MQTT)
if (mMqttEnabled && mNetworkConnected)
mMqtt.loop();
+ #endif
+ yield();
}
//-----------------------------------------------------------------------------
@@ -141,13 +185,21 @@ void app::regularTickers(void) {
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
// Plugins
#if defined(PLUGIN_DISPLAY)
- if (mConfig->plugin.display.type != 0)
+ if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
#endif
every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart");
#if !defined(ETHERNET)
//everySec([this]() { mImprov.tickSerial(); }, "impro");
#endif
+
+ #if defined(ENABLE_HISTORY)
+ everySec(std::bind(&HistoryType::tickerSecond, &mHistory), "hist");
+ #endif /*ENABLE_HISTORY*/
+
+ #if defined(ENABLE_SIMULATOR)
+ every(std::bind(&SimulatorType::tick, &mSimulator), 5, "sim");
+ #endif /*ENABLE_SIMULATOR*/
}
#if defined(ETHERNET)
@@ -163,11 +215,13 @@ void app::onNtpUpdate(bool gotTime) {
//-----------------------------------------------------------------------------
void app::updateNtp(void) {
+ #if defined(ENABLE_MQTT)
if (mMqttReconnect && mMqttEnabled) {
mMqtt.tickerSecond();
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
}
+ #endif /*ENABLE_MQTT*/
// only install schedulers once even if NTP wasn't successful in first loop
if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed
@@ -226,15 +280,18 @@ void app::tickCalcSunrise(void) {
if (mSunrise == 0) // on boot/reboot calc sun values for current time
ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
- if (mTimestamp > (mSunset + mConfig->sun.offsetSec)) // current time is past communication stop, calc sun values for next day
+ if (mTimestamp > (mSunset + mConfig->sun.offsetSecEvening)) // current time is past communication stop, calc sun values for next day
ah::calculateSunriseSunset(mTimestamp + 86400, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
tickIVCommunication();
- uint32_t nxtTrig = mSunset + mConfig->sun.offsetSec + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop
+ uint32_t nxtTrig = mSunset + mConfig->sun.offsetSecEvening + 60; // set next trigger to communication stop, +60 for safety that it is certain past communication stop
onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig, "Sunri");
- if (mMqttEnabled)
+ if (mMqttEnabled) {
tickSun();
+ nxtTrig = mSunrise + mConfig->sun.offsetSecMorning + 1; // one second safety to trigger correctly
+ onceAt(std::bind(&app::tickSunrise, this), nxtTrig, "mqSr"); // trigger on sunrise to update 'dis_night_comm'
+ }
}
//-----------------------------------------------------------------------------
@@ -251,14 +308,14 @@ void app::tickIVCommunication(void) {
iv->commEnabled = !iv->config->disNightCom; // if sun.disNightCom is false, communication is always on
if (!iv->commEnabled) { // inverter communication only during the day
- if (mTimestamp < (mSunrise - mConfig->sun.offsetSec)) { // current time is before communication start, set next trigger to communication start
- nxtTrig = mSunrise - mConfig->sun.offsetSec;
+ if (mTimestamp < (mSunrise + mConfig->sun.offsetSecMorning)) { // current time is before communication start, set next trigger to communication start
+ nxtTrig = mSunrise + mConfig->sun.offsetSecMorning;
} else {
- if (mTimestamp >= (mSunset + mConfig->sun.offsetSec)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
+ if (mTimestamp >= (mSunset + mConfig->sun.offsetSecEvening)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise
nxtTrig = 0;
} else { // current time lies within communication start/stop time, set next trigger to communication stop
iv->commEnabled = true;
- nxtTrig = mSunset + mConfig->sun.offsetSec;
+ nxtTrig = mSunset + mConfig->sun.offsetSecEvening;
}
}
if (nxtTrig != 0)
@@ -279,8 +336,27 @@ void app::tickIVCommunication(void) {
//-----------------------------------------------------------------------------
void app::tickSun(void) {
// only used and enabled by MQTT (see setup())
- if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec))
+ #if defined(ENABLE_MQTT)
+ if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening))
once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry
+ #endif
+}
+
+//-----------------------------------------------------------------------------
+void app::tickSunrise(void) {
+ // only used and enabled by MQTT (see setup())
+ #if defined(ENABLE_MQTT)
+ if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSecMorning, mConfig->sun.offsetSecEvening, true))
+ once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry
+ #endif
+}
+
+//-----------------------------------------------------------------------------
+void app::notAvailChanged(void) {
+ #if defined(ENABLE_MQTT)
+ if (mMqttEnabled)
+ mMqtt.notAvailChanged(mAllIvNotAvail);
+ #endif
}
//-----------------------------------------------------------------------------
@@ -325,13 +401,16 @@ void app::tickMidnight(void) {
if (mConfig->inst.rstYieldMidNight) {
zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY);
+ #if defined(ENABLE_MQTT)
if (mMqttEnabled)
mMqtt.tickerMidnight();
+ #endif
}
}
//-----------------------------------------------------------------------------
void app::tickSend(void) {
+ bool notAvail = true;
uint8_t fill = mCommunication.getFillState();
uint8_t max = mCommunication.getMaxFill();
if((max-MAX_NUM_INVERTERS) <= fill) {
@@ -357,6 +436,9 @@ void app::tickSend(void) {
if(!iv->radio->isChipConnected())
continue;
+ if(InverterStatus::OFF != iv->status)
+ notAvail = false;
+
iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) {
if(isDevControl)
mCommunication.addImportant(iv, cmd);
@@ -366,6 +448,10 @@ void app::tickSend(void) {
}
}
+ if(mAllIvNotAvail != notAvail)
+ once(std::bind(&app::notAvailChanged, this), 1, "avail");
+ mAllIvNotAvail = notAvail;
+
updateLed();
}
@@ -415,22 +501,56 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
changed = true;
}
- if(changed) {
- if(mMqttEnabled && !skipYieldDay)
- mMqtt.setZeroValuesEnable();
+ if(changed)
payloadEventListener(RealTimeRunData_Debug, NULL);
- }
}
//-----------------------------------------------------------------------------
void app::resetSystem(void) {
- snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
+ snprintf(mVersion, sizeof(mVersion), "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
+ snprintf(mVersionModules, sizeof(mVersionModules), "%s",
+ #ifdef ENABLE_PROMETHEUS_EP
+ "P"
+ #endif
+
+ #ifdef ENABLE_MQTT
+ "M"
+ #endif
+
+ #ifdef PLUGIN_DISPLAY
+ "D"
+ #endif
+
+ #ifdef ENABLE_HISTORY
+ "H"
+ #endif
+
+ #ifdef AP_ONLY
+ "A"
+ #endif
+
+ #ifdef ENABLE_SYSLOG
+ "Y"
+ #endif
+
+ #ifdef ENABLE_SIMULATOR
+ "S"
+ #endif
+
+ "-"
+ #ifdef LANG_DE
+ "de"
+ #else
+ "en"
+ #endif
+ );
#ifdef AP_ONLY
mTimestamp = 1;
#endif
mSendFirst = true;
+ mAllIvNotAvail = true;
mSunrise = 0;
mSunset = 0;
@@ -453,14 +573,11 @@ void app::mqttSubRxCb(JsonObject obj) {
//-----------------------------------------------------------------------------
void app::setupLed(void) {
uint8_t led_off = (mConfig->led.high_active) ? 0 : 255;
-
- if (mConfig->led.led0 != DEF_PIN_OFF) {
- pinMode(mConfig->led.led0, OUTPUT);
- analogWrite(mConfig->led.led0, led_off);
- }
- if (mConfig->led.led1 != DEF_PIN_OFF) {
- pinMode(mConfig->led.led1, OUTPUT);
- analogWrite(mConfig->led.led1, led_off);
+ for(uint8_t i = 0; i < 3; i ++) {
+ if (mConfig->led.led[i] != DEF_PIN_OFF) {
+ pinMode(mConfig->led.led[i], OUTPUT);
+ analogWrite(mConfig->led.led[i], led_off);
+ }
}
}
@@ -469,27 +586,34 @@ void app::updateLed(void) {
uint8_t led_off = (mConfig->led.high_active) ? 0 : 255;
uint8_t led_on = (mConfig->led.high_active) ? (mConfig->led.luminance) : (255-mConfig->led.luminance);
- if (mConfig->led.led0 != DEF_PIN_OFF) {
+ if (mConfig->led.led[0] != DEF_PIN_OFF) {
Inverter<> *iv;
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL != iv) {
if (iv->isProducing()) {
// turn on when at least one inverter is producing
- analogWrite(mConfig->led.led0, led_on);
+ analogWrite(mConfig->led.led[0], led_on);
break;
}
else if(iv->config->enabled)
- analogWrite(mConfig->led.led0, led_off);
+ analogWrite(mConfig->led.led[0], led_off);
}
}
}
- if (mConfig->led.led1 != DEF_PIN_OFF) {
+ if (mConfig->led.led[1] != DEF_PIN_OFF) {
if (getMqttIsConnected()) {
- analogWrite(mConfig->led.led1, led_on);
+ analogWrite(mConfig->led.led[1], led_on);
} else {
- analogWrite(mConfig->led.led1, led_off);
+ analogWrite(mConfig->led.led[1], led_off);
}
}
+
+ if (mConfig->led.led[2] != DEF_PIN_OFF) {
+ if((mTimestamp > (mSunset + mConfig->sun.offsetSecEvening)) || (mTimestamp < (mSunrise + mConfig->sun.offsetSecMorning)))
+ analogWrite(mConfig->led.led[2], led_on);
+ else
+ analogWrite(mConfig->led.led[2], led_off);
+ }
}
diff --git a/src/app.h b/src/app.h
index a24cccb3..2f965087 100644
--- a/src/app.h
+++ b/src/app.h
@@ -8,6 +8,10 @@
#include
#include
+#if defined(ESP32)
+#include
+#define WDT_TIMEOUT_SECONDS 8 // Watchdog Timeout 8s
+#endif
#include "config/settings.h"
#include "defines.h"
@@ -17,13 +21,18 @@
#if defined(ESP32)
#include "hms/hmsRadio.h"
#endif
+#if defined(ENABLE_MQTT)
#include "publisher/pubMqtt.h"
+#endif /*ENABLE_MQTT*/
#include "publisher/pubSerial.h"
#include "utils/crc.h"
#include "utils/dbg.h"
#include "utils/scheduler.h"
#include "utils/syslog.h"
#include "web/RestApi.h"
+#if defined(ENABLE_HISTORY)
+#include "plugins/history.h"
+#endif /*ENABLE_HISTORY*/
#include "web/web.h"
#include "hm/Communication.h"
#if defined(ETHERNET)
@@ -33,8 +42,13 @@
#include "utils/improv.h"
#endif /* defined(ETHERNET) */
+#if defined(ENABLE_SIMULATOR)
+ #include "hm/simulator.h"
+#endif /*ENABLE_SIMULATOR*/
+
#include // position is relevant since version 1.4.7 of this library
+
// convert degrees and radians for sun calculation
#define SIN(x) (sin(radians(x)))
#define COS(x) (cos(radians(x)))
@@ -42,12 +56,18 @@
#define ACOS(x) (degrees(acos(x)))
typedef HmSystem HmSystemType;
-#ifdef ESP32
-#endif
typedef Web WebType;
typedef RestApi RestApiType;
+#if defined(ENABLE_MQTT)
typedef PubMqtt PubMqttType;
+#endif /*ENABLE_MQTT*/
typedef PubSerial PubSerialType;
+#if defined(ENABLE_HISTORY)
+typedef HistoryData HistoryType;
+#endif /*ENABLE_HISTORY*/
+#if defined (ENABLE_SIMULATOR)
+typedef Simulator SimulatorType;
+#endif /*ENABLE_SIMULATOR*/
// PLUGINS
#if defined(PLUGIN_DISPLAY)
@@ -62,7 +82,7 @@ class app : public IApp, public ah::Scheduler {
~app() {}
void setup(void);
- void loop(void);
+ void loop(void) override;
void onNetwork(bool gotIp);
void regularTickers(void);
@@ -96,7 +116,7 @@ class app : public IApp, public ah::Scheduler {
}
uint64_t getTimestampMs() {
- return ((uint64_t)Scheduler::mTimestamp * 1000) + (uint64_t)Scheduler::mTsMillis;
+ return ((uint64_t)Scheduler::mTimestamp * 1000) + ((uint64_t)millis() - (uint64_t)Scheduler::mTsMillis) % 1000;
}
bool saveSettings(bool reboot) {
@@ -150,6 +170,18 @@ class app : public IApp, public ah::Scheduler {
return mWifi.getAvailNetworks(obj);
}
+ void setupStation(void) {
+ mWifi.setupStation();
+ }
+
+ void setStopApAllowedMode(bool allowed) {
+ mWifi.setStopApAllowedMode(allowed);
+ }
+
+ String getStationIp(void) {
+ return mWifi.getStationIp();
+ }
+
#endif /* !defined(ETHERNET) */
void setRebootFlag() {
@@ -160,6 +192,10 @@ class app : public IApp, public ah::Scheduler {
return mVersion;
}
+ const char *getVersionModules() {
+ return mVersionModules;
+ }
+
uint32_t getSunrise() {
return mSunrise;
}
@@ -177,23 +213,33 @@ class app : public IApp, public ah::Scheduler {
}
void setMqttDiscoveryFlag() {
+ #if defined(ENABLE_MQTT)
once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf");
- }
-
- void setMqttPowerLimitAck(Inverter<> *iv) {
- mMqtt.setPowerLimitAck(iv);
+ #endif
}
bool getMqttIsConnected() {
- return mMqtt.isConnected();
+ #if defined(ENABLE_MQTT)
+ return mMqtt.isConnected();
+ #else
+ return false;
+ #endif
}
uint32_t getMqttTxCnt() {
- return mMqtt.getTxCnt();
+ #if defined(ENABLE_MQTT)
+ return mMqtt.getTxCnt();
+ #else
+ return 0;
+ #endif
}
uint32_t getMqttRxCnt() {
- return mMqtt.getRxCnt();
+ #if defined(ENABLE_MQTT)
+ return mMqtt.getRxCnt();
+ #else
+ return 0;
+ #endif
}
bool getProtection(AsyncWebServerRequest *request) {
@@ -243,6 +289,22 @@ class app : public IApp, public ah::Scheduler {
Scheduler::setTimestamp(newTime);
}
+ uint16_t getHistoryValue(uint8_t type, uint16_t i) {
+ #if defined(ENABLE_HISTORY)
+ return mHistory.valueAt((HistoryStorageType)type, i);
+ #else
+ return 0;
+ #endif
+ }
+
+ uint16_t getHistoryMaxDay() {
+ #if defined(ENABLE_HISTORY)
+ return mHistory.getMaximumDay();
+ #else
+ return 0;
+ #endif
+ }
+
private:
#define CHECK_AVAIL true
#define SKIP_YIELD_DAY true
@@ -252,11 +314,13 @@ class app : public IApp, public ah::Scheduler {
void payloadEventListener(uint8_t cmd, Inverter<> *iv) {
#if !defined(AP_ONLY)
- if (mMqttEnabled)
- mMqtt.payloadEventListener(cmd, iv);
+ #if defined(ENABLE_MQTT)
+ if (mMqttEnabled)
+ mMqtt.payloadEventListener(cmd, iv);
+ #endif /*ENABLE_MQTT*/
#endif
#if defined(PLUGIN_DISPLAY)
- if(mConfig->plugin.display.type != 0)
+ if(DISP_TYPE_T0_NONE != mConfig->plugin.display.type)
mDisplay.payloadEventListener(cmd);
#endif
updateLed();
@@ -290,14 +354,20 @@ class app : public IApp, public ah::Scheduler {
#endif /* defined(ETHERNET) */
void updateNtp(void);
+ void triggerTickSend() {
+ once(std::bind(&app::tickSend, this), 0, "tSend");
+ }
+
void tickCalcSunrise(void);
void tickIVCommunication(void);
void tickSun(void);
+ void tickSunrise(void);
void tickComm(void);
void tickSend(void);
void tickMinute(void);
void tickZeroValues(void);
void tickMidnight(void);
+ void notAvailChanged(void);
HmSystemType mSys;
HmRadio<> mNrfRadio;
@@ -326,6 +396,7 @@ class app : public IApp, public ah::Scheduler {
#endif
char mVersion[12];
+ char mVersionModules[12];
settings mSettings;
settings_t *mConfig;
bool mSavePending;
@@ -333,11 +404,14 @@ class app : public IApp, public ah::Scheduler {
uint8_t mSendLastIvId;
bool mSendFirst;
+ bool mAllIvNotAvail;
bool mNetworkConnected;
// mqtt
+ #if defined(ENABLE_MQTT)
PubMqttType mMqtt;
+ #endif /*ENABLE_MQTT*/
bool mMqttReconnect;
bool mMqttEnabled;
@@ -350,6 +424,13 @@ class app : public IApp, public ah::Scheduler {
DisplayType mDisplay;
DisplayData mDispData;
#endif
+ #if defined(ENABLE_HISTORY)
+ HistoryType mHistory;
+ #endif /*ENABLE_HISTORY*/
+
+ #if defined(ENABLE_SIMULATOR)
+ SimulatorType mSimulator;
+ #endif /*ENABLE_SIMULATOR*/
};
#endif /*__APP_H__*/
diff --git a/src/appInterface.h b/src/appInterface.h
index 625f72db..08ca525e 100644
--- a/src/appInterface.h
+++ b/src/appInterface.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2022 Ahoy, https://ahoydtu.de
+// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -7,14 +7,8 @@
#define __IAPP_H__
#include "defines.h"
-#include "hm/hmSystem.h"
#include "ESPAsyncWebServer.h"
-//#include "hms/hmsRadio.h"
-#if defined(ESP32)
-//typedef CmtRadio> CmtRadioType;
-#endif
-
// abstract interface to App. Make members of App accessible from child class
// like web or API without forward declaration
class IApp {
@@ -29,10 +23,14 @@ class IApp {
virtual bool getShouldReboot() = 0;
virtual void setRebootFlag() = 0;
virtual const char *getVersion() = 0;
+ virtual const char *getVersionModules() = 0;
#if !defined(ETHERNET)
virtual void scanAvailNetworks() = 0;
virtual bool getAvailNetworks(JsonObject obj) = 0;
+ virtual void setupStation(void) = 0;
+ virtual void setStopApAllowedMode(bool allowed) = 0;
+ virtual String getStationIp(void) = 0;
#endif /* defined(ETHERNET) */
virtual uint32_t getUptime() = 0;
@@ -45,10 +43,11 @@ class IApp {
virtual void getSchedulerInfo(uint8_t *max) = 0;
virtual void getSchedulerNames() = 0;
+ virtual void triggerTickSend() = 0;
+
virtual bool getRebootRequestState() = 0;
virtual bool getSettingsValid() = 0;
virtual void setMqttDiscoveryFlag() = 0;
- virtual void setMqttPowerLimitAck(Inverter<> *iv) = 0;
virtual bool getMqttIsConnected() = 0;
virtual bool getNrfEnabled() = 0;
@@ -59,8 +58,10 @@ class IApp {
virtual bool getProtection(AsyncWebServerRequest *request) = 0;
- virtual void* getRadioObj(bool nrf) = 0;
+ virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0;
+ virtual uint16_t getHistoryMaxDay() = 0;
+ virtual void* getRadioObj(bool nrf) = 0;
};
#endif /*__IAPP_H__*/
diff --git a/src/config/config.h b/src/config/config.h
index 2cb9bcd3..a633f3af 100644
--- a/src/config/config.h
+++ b/src/config/config.h
@@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
-// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __CONFIG_H__
@@ -32,12 +32,33 @@
// timeout for automatic logoff (20 minutes)
#define LOGOUT_TIMEOUT (20 * 60)
+
+//-------------------------------------
+// MODULE SELECTOR - done by platform.ini
+//-------------------------------------
+
+// MqTT connection
+//#define ENABLE_MQTT
+
+// display plugin
+//#define PLUGIN_DISPLAY
+
+// history graph (WebUI)
+//#define ENABLE_HISTORY
+
+// inverter simulation
+//#define ENABLE_SIMULATOR
+
+// to enable the syslog logging (will disable web-serial)
+//#define ENABLE_SYSLOG
+
+
+
//-------------------------------------
// CONFIGURATION - COMPILE TIME
//-------------------------------------
// ethernet
-
#if defined(ETHERNET)
#define ETH_SPI_HOST SPI2_HOST
#define ETH_SPI_CLOCK_MHZ 25
@@ -93,6 +114,16 @@
#define DEF_NRF_SCLK_PIN 18
#endif
+#if defined(ETHERNET) && !defined(SPI_HAL)
+ #ifndef DEF_CMT_SPI_HOST
+ #define DEF_CMT_SPI_HOST SPI3_HOST
+ #endif
+#else
+ #ifndef DEF_CMT_SPI_HOST
+ #define DEF_CMT_SPI_HOST SPI2_HOST
+ #endif
+#endif /* defined(ETHERNET) */
+
#ifndef DEF_CMT_SCLK
#define DEF_CMT_SCLK 12
#endif
@@ -142,6 +173,9 @@
#ifndef DEF_LED1
#define DEF_LED1 DEF_PIN_OFF
#endif
+#ifndef DEF_LED2
+ #define DEF_LED2 DEF_PIN_OFF
+#endif
#ifdef LED_ACTIVE_HIGH
#define LED_HIGH_ACTIVE true
#else
@@ -181,7 +215,7 @@
#define INVERTER_OFF_THRES_SEC 15*60
// threshold of minimum power on which the inverter is marked as inactive
-#define INACT_PWR_THRESH 3
+#define INACT_PWR_THRESH 1
// Timezone
#define TIMEZONE 1
@@ -219,6 +253,17 @@
// reconnect delay
#define MQTT_RECONNECT_DELAY 5000
+// maximum custom link length
+#define MAX_CUSTOM_LINK_LEN 100
+#define MAX_CUSTOM_LINK_TEXT_LEN 32
+// syslog settings
+#ifdef ENABLE_SYSLOG
+#define SYSLOG_HOST ""
+#define SYSLOG_APP "ahoy"
+#define SYSLOG_FACILITY FAC_USER
+#define SYSLOG_PORT 514
+#endif
+
#if __has_include("config_override.h")
#include "config_override.h"
#endif
diff --git a/src/config/config_override_example.h b/src/config/config_override_example.h
index b90bbdbd..44623c1f 100644
--- a/src/config/config_override_example.h
+++ b/src/config/config_override_example.h
@@ -35,13 +35,5 @@
// #define ENABLE_PROMETHEUS_EP
-// to enable the syslog logging (will disable web-serial)
-//#define ENABLE_SYSLOG
-#ifdef ENABLE_SYSLOG
-#define SYSLOG_HOST ""
-#define SYSLOG_APP "ahoy"
-#define SYSLOG_FACILITY FAC_USER
-#define SYSLOG_PORT 514
-#endif
#endif /*__CONFIG_OVERRIDE_H__*/
diff --git a/src/config/settings.h b/src/config/settings.h
index fe2ad1b0..d8c93b10 100644
--- a/src/config/settings.h
+++ b/src/config/settings.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://ahoydtu.de
+// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -30,7 +30,7 @@
* https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout
* */
-#define CONFIG_VERSION 7
+#define CONFIG_VERSION 9
#define PROT_MASK_INDEX 0x0001
@@ -39,8 +39,9 @@
#define PROT_MASK_SETUP 0x0008
#define PROT_MASK_UPDATE 0x0010
#define PROT_MASK_SYSTEM 0x0020
-#define PROT_MASK_API 0x0040
-#define PROT_MASK_MQTT 0x0080
+#define PROT_MASK_HISTORY 0x0040
+#define PROT_MASK_API 0x0080
+#define PROT_MASK_MQTT 0x0100
#define DEF_PROT_INDEX 0x0001
#define DEF_PROT_LIVE 0x0000
@@ -48,6 +49,7 @@
#define DEF_PROT_SETUP 0x0008
#define DEF_PROT_UPDATE 0x0010
#define DEF_PROT_SYSTEM 0x0020
+#define DEF_PROT_HISTORY 0x0000
#define DEF_PROT_API 0x0000
#define DEF_PROT_MQTT 0x0000
@@ -106,7 +108,8 @@ typedef struct {
typedef struct {
float lat;
float lon;
- uint16_t offsetSec;
+ int16_t offsetSecMorning;
+ int16_t offsetSecEvening;
} cfgSun_t;
typedef struct {
@@ -117,8 +120,7 @@ typedef struct {
} cfgSerial_t;
typedef struct {
- uint8_t led0; // first LED pin
- uint8_t led1; // second LED pin
+ uint8_t led[3]; // LED pins
bool high_active; // determines if LEDs are high or low active
uint8_t luminance; // luminance of LED
} cfgLed_t;
@@ -165,6 +167,8 @@ typedef struct {
uint8_t type;
bool pwrSaveAtIvOffline;
uint8_t screenSaver;
+ uint8_t graph_ratio;
+ uint8_t graph_size;
uint8_t rot;
//uint16_t wakeUp;
//uint16_t sleepAt;
@@ -180,6 +184,8 @@ typedef struct {
typedef struct {
display_t display;
+ char customLink[MAX_CUSTOM_LINK_LEN];
+ char customLinkText[MAX_CUSTOM_LINK_TEXT_LEN];
} plugins_t;
typedef struct {
@@ -308,18 +314,18 @@ class settings {
DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE);
JsonObject root = json.to();
json[F("version")] = CONFIG_VERSION;
- jsonNetwork(root.createNestedObject(F("wifi")), true);
- jsonNrf(root.createNestedObject(F("nrf")), true);
+ jsonNetwork(root[F("wifi")].to(), true);
+ jsonNrf(root[F("nrf")].to(), true);
#if defined(ESP32)
- jsonCmt(root.createNestedObject(F("cmt")), true);
+ jsonCmt(root[F("cmt")].to(), true);
#endif
- jsonNtp(root.createNestedObject(F("ntp")), true);
- jsonSun(root.createNestedObject(F("sun")), true);
- jsonSerial(root.createNestedObject(F("serial")), true);
- jsonMqtt(root.createNestedObject(F("mqtt")), true);
- jsonLed(root.createNestedObject(F("led")), true);
- jsonPlugin(root.createNestedObject(F("plugin")), true);
- jsonInst(root.createNestedObject(F("inst")), true);
+ jsonNtp(root[F("ntp")].to(), true);
+ jsonSun(root[F("sun")].to(), true);
+ jsonSerial(root[F("serial")].to(), true);
+ jsonMqtt(root[F("mqtt")].to(), true);
+ jsonLed(root[F("led")].to(), true);
+ jsonPlugin(root[F("plugin")].to(), true);
+ jsonInst(root[F("inst")].to(), true);
DPRINT(DBG_INFO, F("memory usage: "));
DBGPRINTLN(String(json.memoryUsage()));
@@ -373,7 +379,7 @@ class settings {
// erase all settings and reset to default
memset(&mCfg, 0, sizeof(settings_t));
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
- | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT;
+ | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY;
mCfg.sys.darkMode = false;
mCfg.sys.schedReboot = false;
// restore temp settings
@@ -420,7 +426,8 @@ class settings {
mCfg.sun.lat = 0.0;
mCfg.sun.lon = 0.0;
- mCfg.sun.offsetSec = 0;
+ mCfg.sun.offsetSecMorning = 0;
+ mCfg.sun.offsetSecEvening = 0;
mCfg.serial.showIv = false;
mCfg.serial.debug = false;
@@ -441,7 +448,7 @@ class settings {
mCfg.inst.startWithoutTime = false;
mCfg.inst.rstMaxValsMidNight = false;
mCfg.inst.yieldEffiency = 1.0f;
- mCfg.inst.gapMs = 500;
+ mCfg.inst.gapMs = 1;
mCfg.inst.readGrid = true;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
@@ -451,14 +458,17 @@ class settings {
mCfg.inst.iv[i].add2Total = true;
}
- mCfg.led.led0 = DEF_LED0;
- mCfg.led.led1 = DEF_LED1;
+ mCfg.led.led[0] = DEF_LED0;
+ mCfg.led.led[1] = DEF_LED1;
+ mCfg.led.led[2] = DEF_LED2;
mCfg.led.high_active = LED_HIGH_ACTIVE;
mCfg.led.luminance = 255;
mCfg.plugin.display.pwrSaveAtIvOffline = false;
- mCfg.plugin.display.contrast = 60;
+ mCfg.plugin.display.contrast = 140;
mCfg.plugin.display.screenSaver = 1; // default: 1 .. pixelshift for OLED for downward compatibility
+ mCfg.plugin.display.graph_ratio = 0;
+ mCfg.plugin.display.graph_size = 2;
mCfg.plugin.display.rot = 0;
mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA
mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL
@@ -466,7 +476,7 @@ class settings {
mCfg.plugin.display.disp_reset = DEF_PIN_OFF;
mCfg.plugin.display.disp_busy = DEF_PIN_OFF;
mCfg.plugin.display.disp_dc = DEF_PIN_OFF;
- mCfg.plugin.display.pirPin = DEF_MOTION_SENSOR_PIN;
+ mCfg.plugin.display.pirPin = DEF_PIN_OFF;
}
void loadAddedDefaults() {
@@ -496,6 +506,12 @@ class settings {
if(mCfg.configVersion < 7) {
mCfg.led.luminance = 255;
}
+ if(mCfg.configVersion < 8) {
+ mCfg.sun.offsetSecEvening = mCfg.sun.offsetSecMorning;
+ }
+ if(mCfg.configVersion < 9) {
+ mCfg.inst.gapMs = 1;
+ }
}
}
@@ -546,7 +562,7 @@ class settings {
if(mCfg.sys.protectionMask == 0)
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
- | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT;
+ | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY;
}
}
@@ -625,11 +641,13 @@ class settings {
if(set) {
obj[F("lat")] = mCfg.sun.lat;
obj[F("lon")] = mCfg.sun.lon;
- obj[F("offs")] = mCfg.sun.offsetSec;
+ obj[F("offs")] = mCfg.sun.offsetSecMorning;
+ obj[F("offsEve")] = mCfg.sun.offsetSecEvening;
} else {
getVal(obj, F("lat"), &mCfg.sun.lat);
getVal(obj, F("lon"), &mCfg.sun.lon);
- getVal(obj, F("offs"), &mCfg.sun.offsetSec);
+ getVal(obj, F("offs"), &mCfg.sun.offsetSecMorning);
+ getVal(obj, F("offsEve"), &mCfg.sun.offsetSecEvening);
}
}
@@ -670,13 +688,15 @@ class settings {
void jsonLed(JsonObject obj, bool set = false) {
if(set) {
- obj[F("0")] = mCfg.led.led0;
- obj[F("1")] = mCfg.led.led1;
+ obj[F("0")] = mCfg.led.led[0];
+ obj[F("1")] = mCfg.led.led[1];
+ obj[F("2")] = mCfg.led.led[2];
obj[F("act_high")] = mCfg.led.high_active;
obj[F("lum")] = mCfg.led.luminance;
} else {
- getVal(obj, F("0"), &mCfg.led.led0);
- getVal(obj, F("1"), &mCfg.led.led1);
+ getVal(obj, F("0"), &mCfg.led.led[0]);
+ getVal(obj, F("1"), &mCfg.led.led[1]);
+ getVal(obj, F("2"), &mCfg.led.led[2]);
getVal(obj, F("act_high"), &mCfg.led.high_active);
getVal(obj, F("lum"), &mCfg.led.luminance);
}
@@ -688,6 +708,8 @@ class settings {
disp[F("type")] = mCfg.plugin.display.type;
disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline;
disp[F("screenSaver")] = mCfg.plugin.display.screenSaver;
+ disp[F("graph_ratio")] = mCfg.plugin.display.graph_ratio;
+ disp[F("graph_size")] = mCfg.plugin.display.graph_size;
disp[F("rotation")] = mCfg.plugin.display.rot;
//disp[F("wake")] = mCfg.plugin.display.wakeUp;
//disp[F("sleep")] = mCfg.plugin.display.sleepAt;
@@ -699,11 +721,15 @@ class settings {
disp[F("busy")] = mCfg.plugin.display.disp_busy;
disp[F("dc")] = mCfg.plugin.display.disp_dc;
disp[F("pirPin")] = mCfg.plugin.display.pirPin;
+ obj[F("cst_lnk")] = mCfg.plugin.customLink;
+ obj[F("cst_lnk_txt")] = mCfg.plugin.customLinkText;
} else {
JsonObject disp = obj["disp"];
getVal(disp, F("type"), &mCfg.plugin.display.type);
getVal(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline);
getVal(disp, F("screenSaver"), &mCfg.plugin.display.screenSaver);
+ getVal(disp, F("graph_ratio"), &mCfg.plugin.display.graph_ratio);
+ getVal(disp, F("graph_size"), &mCfg.plugin.display.graph_size);
getVal(disp, F("rotation"), &mCfg.plugin.display.rot);
//mCfg.plugin.display.wakeUp = disp[F("wake")];
//mCfg.plugin.display.sleepAt = disp[F("sleep")];
@@ -715,6 +741,8 @@ class settings {
getVal(disp, F("busy"), &mCfg.plugin.display.disp_busy);
getVal(disp, F("dc"), &mCfg.plugin.display.disp_dc);
getVal(disp, F("pirPin"), &mCfg.plugin.display.pirPin);
+ getChar(obj, F("cst_lnk"), mCfg.plugin.customLink, MAX_CUSTOM_LINK_LEN);
+ getChar(obj, F("cst_lnk_txt"), mCfg.plugin.customLinkText, MAX_CUSTOM_LINK_TEXT_LEN);
}
}
@@ -796,8 +824,10 @@ class settings {
#if defined(ESP32)
void getChar(JsonObject obj, const char *key, char *dst, int maxLen) {
- if(obj.containsKey(key))
+ if(obj.containsKey(key)) {
snprintf(dst, maxLen, "%s", obj[key].as());
+ dst[maxLen-1] = '\0';
+ }
}
template
@@ -807,8 +837,10 @@ class settings {
}
#else
void getChar(JsonObject obj, const __FlashStringHelper *key, char *dst, int maxLen) {
- if(obj.containsKey(key))
+ if(obj.containsKey(key)) {
snprintf(dst, maxLen, "%s", obj[key].as());
+ dst[maxLen-1] = '\0';
+ }
}
template
diff --git a/src/defines.h b/src/defines.h
index 787e9ccf..77ea6726 100644
--- a/src/defines.h
+++ b/src/defines.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://ahoydtu.de
+// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -13,7 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 8
-#define VERSION_PATCH 37
+#define VERSION_PATCH 63
//-------------------------------------
typedef struct {
@@ -78,6 +78,18 @@ union serial_u {
enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
+enum {
+ DISP_TYPE_T0_NONE = 0,
+ DISP_TYPE_T1_SSD1306_128X64 = 1,
+ DISP_TYPE_T2_SH1106_128X64 = 2,
+ DISP_TYPE_T3_PCD8544_84X48 = 3,
+ DISP_TYPE_T4_SSD1306_128X32 = 4,
+ DISP_TYPE_T5_SSD1306_64X48 = 5,
+ DISP_TYPE_T6_SSD1309_128X64 = 6,
+ DISP_TYPE_T10_EPAPER = 10
+};
+
+
//-------------------------------------
// EEPROM
//-------------------------------------
@@ -94,7 +106,6 @@ enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
#define MQTT_MAX_PACKET_SIZE 384
-#define PLUGIN_DISPLAY
typedef struct {
uint32_t rxFail;
@@ -103,6 +114,10 @@ typedef struct {
uint32_t frmCnt;
uint32_t txCnt;
uint32_t retransmits;
+ uint16_t ivLoss; // lost frames (from GetLossRate)
+ uint16_t ivSent; // sent frames (from GetLossRate)
+ uint16_t dtuLoss; // current DTU lost frames (since last GetLossRate)
+ uint16_t dtuSent; // current DTU sent frames (since last GetLossRate)
} statistics_t;
#endif /*__DEFINES_H__*/
diff --git a/src/eth/ahoyeth.h b/src/eth/ahoyeth.h
index 2b0f8b62..600cc0ea 100644
--- a/src/eth/ahoyeth.h
+++ b/src/eth/ahoyeth.h
@@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#if defined(ETHERNET)
@@ -49,7 +49,7 @@ class ahoyeth {
#if defined(CONFIG_IDF_TARGET_ESP32S3)
EthSpi mEthSpi;
#endif
- settings_t *mConfig;
+ settings_t *mConfig = NULL;
uint32_t *mUtcTimestamp;
AsyncUDP mUdp; // for time server
diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h
index bb815a7e..d3fe1c69 100644
--- a/src/hm/CommQueue.h
+++ b/src/hm/CommQueue.h
@@ -11,6 +11,10 @@
#include "hmInverter.h"
#include "../utils/dbg.h"
+#define DEFAULT_ATTEMPS 5
+#define MORE_ATTEMPS_ALARMDATA 8
+#define MORE_ATTEMPS_GRIDONPROFILEPARA 5
+
template
class CommQueue {
public:
@@ -44,11 +48,12 @@ class CommQueue {
Inverter<> *iv;
uint8_t cmd;
uint8_t attempts;
+ uint8_t attemptsMax;
uint32_t ts;
bool isDevControl;
queue_s() {}
queue_s(Inverter<> *i, uint8_t c, bool dev) :
- iv(i), cmd(c), attempts(5), ts(0), isDevControl(dev) {}
+ iv(i), cmd(c), attempts(DEFAULT_ATTEMPS), attemptsMax(DEFAULT_ATTEMPS), ts(0), isDevControl(dev) {}
};
protected:
@@ -59,8 +64,10 @@ class CommQueue {
void add(const queue_s *q, bool rstAttempts = false) {
mQueue[mWrPtr] = *q;
- if(rstAttempts)
- mQueue[mWrPtr].attempts = 5;
+ if(rstAttempts) {
+ mQueue[mWrPtr].attempts = DEFAULT_ATTEMPS;
+ mQueue[mWrPtr].attemptsMax = DEFAULT_ATTEMPS;
+ }
inc(&mWrPtr);
}
@@ -79,7 +86,8 @@ class CommQueue {
void cmdDone(bool keep = false) {
if(keep) {
- mQueue[mRdPtr].attempts = 5;
+ mQueue[mRdPtr].attempts = DEFAULT_ATTEMPS;
+ mQueue[mRdPtr].attemptsMax = DEFAULT_ATTEMPS;
add(mQueue[mRdPtr]); // add to the end again
}
inc(&mRdPtr);
@@ -96,6 +104,8 @@ class CommQueue {
void incrAttempt(uint8_t attempts = 1) {
mQueue[mRdPtr].attempts += attempts;
+ if (mQueue[mRdPtr].attempts > mQueue[mRdPtr].attemptsMax)
+ mQueue[mRdPtr].attemptsMax = mQueue[mRdPtr].attempts;
}
void inc(uint8_t *ptr) {
diff --git a/src/hm/Communication.h b/src/hm/Communication.h
index 9f96533d..80b43ab3 100644
--- a/src/hm/Communication.h
+++ b/src/hm/Communication.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://github.com/lumpapu/ahoy
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -12,13 +12,10 @@
#include "../utils/timemonitor.h"
#include "Heuristic.h"
-#define MI_TIMEOUT 250 // timeout for MI type requests
-#define FRSTMSG_TIMEOUT 150 // how long to wait for first msg to be received
-#define DEFAULT_TIMEOUT 500 // timeout for regular requests
-#define SINGLEFR_TIMEOUT 100 // timeout for single frame requests
#define MAX_BUFFER 250
typedef std::function *)> payloadListenerType;
+typedef std::function *)> powerLimitAckListenerType;
typedef std::function *)> alarmListenerType;
class Communication : public CommQueue<> {
@@ -40,6 +37,10 @@ class Communication : public CommQueue<> {
mCbPayload = cb;
}
+ void addPowerLimitAckListener(powerLimitAckListenerType cb) {
+ mCbPwrAck = cb;
+ }
+
void addAlarmListener(alarmListenerType cb) {
mCbAlarm = cb;
}
@@ -60,205 +61,221 @@ class Communication : public CommQueue<> {
mLastEmptyQueueMillis = millis();
mPrintSequenceDuration = true;
- uint16_t timeout = (q->iv->ivGen == IV_MI) ? MI_TIMEOUT : (((q->iv->mGotFragment && q->iv->mGotLastMsg) || mIsRetransmit) ? SINGLEFR_TIMEOUT : ((q->cmd != AlarmData) && (q->cmd != GridOnProFilePara) ? DEFAULT_TIMEOUT : (1.5 * DEFAULT_TIMEOUT)));
-
- /*if(mDebugState != mState) {
- DPRINT(DBG_INFO, F("State: "));
- DBGHEXLN((uint8_t)(mState));
- mDebugState = mState;
- }*/
- switch(mState) {
- case States::RESET:
- if (!mWaitTime.isTimeout())
- return;
+ innerLoop(q);
+ });
+ }
- mMaxFrameId = 0;
- for(uint8_t i = 0; i < MAX_PAYLOAD_ENTRIES; i++) {
- mLocalBuf[i].len = 0;
- }
+ private:
+ inline void innerLoop(const queue_s *q) {
+ switch(mState) {
+ case States::RESET:
+ if (!mWaitTime.isTimeout())
+ return;
+
+ mMaxFrameId = 0;
+ for(uint8_t i = 0; i < MAX_PAYLOAD_ENTRIES; i++) {
+ mLocalBuf[i].len = 0;
+ }
- if(*mSerialDebug)
- mHeu.printStatus(q->iv);
- mHeu.getTxCh(q->iv);
- q->iv->mGotFragment = false;
- q->iv->mGotLastMsg = false;
- q->iv->curFrmCnt = 0;
- mIsRetransmit = false;
- if(NULL == q->iv->radio)
- cmdDone(false); // can't communicate while radio is not defined!
- q->iv->mCmd = q->cmd;
- q->iv->mIsSingleframeReq = false;
- mState = States::START;
- break;
+ if(*mSerialDebug)
+ mHeu.printStatus(q->iv);
+ mHeu.getTxCh(q->iv);
+ q->iv->mGotFragment = false;
+ q->iv->mGotLastMsg = false;
+ q->iv->curFrmCnt = 0;
+ mIsRetransmit = false;
+ if(NULL == q->iv->radio)
+ cmdDone(false); // can't communicate while radio is not defined!
+ mFirstTry = q->iv->isAvailable();
+ q->iv->mCmd = q->cmd;
+ q->iv->mIsSingleframeReq = false;
+ mFramesExpected = getFramesExpected(q); // function to get expected frame count.
+ mTimeout = DURATION_TXFRAME + mFramesExpected*DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType];
+
+ mState = States::START;
+ break;
- case States::START:
- setTs(mTimestamp);
- if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) {
- // frequency was changed during runtime
- if(q->iv->curCmtFreq != q->iv->config->frequency) {
- if(q->iv->radio->switchFrequencyCh(q->iv, q->iv->curCmtFreq, q->iv->config->frequency))
- q->iv->curCmtFreq = q->iv->config->frequency;
- }
+ case States::START:
+ setTs(mTimestamp);
+ if(INV_RADIO_TYPE_CMT == q->iv->ivRadioType) {
+ // frequency was changed during runtime
+ if(q->iv->curCmtFreq != q->iv->config->frequency) {
+ if(q->iv->radio->switchFrequencyCh(q->iv, q->iv->curCmtFreq, q->iv->config->frequency))
+ q->iv->curCmtFreq = q->iv->config->frequency;
}
+ }
- if(q->isDevControl) {
- if(ActivePowerContr == q->cmd)
- q->iv->powerLimitAck = false;
- q->iv->radio->sendControlPacket(q->iv, q->cmd, q->iv->powerLimit, false);
- } else
- q->iv->radio->prepareDevInformCmd(q->iv, q->cmd, q->ts, q->iv->alarmLastId, false);
-
- q->iv->radioStatistics.txCnt++;
- mWaitTime.startTimeMonitor(timeout);
- mIsRetransmit = false;
- setAttempt();
- if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara))
- incrAttempt(q->cmd == AlarmData? 5 : 3);
-
- mState = States::WAIT;
- break;
+ if(q->isDevControl) {
+ if(ActivePowerContr == q->cmd)
+ q->iv->powerLimitAck = false;
+ q->iv->radio->sendControlPacket(q->iv, q->cmd, q->iv->powerLimit, false);
+ } else
+ q->iv->radio->prepareDevInformCmd(q->iv, q->cmd, q->ts, q->iv->alarmLastId, false);
+
+ q->iv->radioStatistics.txCnt++;
+ q->iv->radio->mRadioWaitTime.startTimeMonitor(mTimeout);
+
+ mIsRetransmit = false;
+ setAttempt();
+ if((q->cmd == AlarmData) || (q->cmd == GridOnProFilePara))
+ incrAttempt(q->cmd == AlarmData? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA);
+ mState = States::WAIT;
+ break;
- case States::WAIT:
- if (!mWaitTime.isTimeout())
- return;
- mState = States::CHECK_FRAMES;
- break;
+ case States::WAIT:
+ if (!q->iv->radio->mRadioWaitTime.isTimeout())
+ return;
+ mState = States::CHECK_FRAMES;
+ break;
- case States::CHECK_FRAMES: {
- if((q->iv->radio->mBufCtrl.empty() && !mIsRetransmit) || (0 == q->attempts)) { // radio buffer empty or no more answers
- if(*mSerialDebug) {
- DPRINT_IVID(DBG_INFO, q->iv->id);
- DBGPRINT(F("request timeout: "));
- DBGPRINT(String(mWaitTime.getRunTime()));
- DBGPRINTLN(F("ms"));
- }
- if(!q->iv->mGotFragment) {
- if((IV_HMS == q->iv->ivGen) || (IV_HMT == q->iv->ivGen)) {
- q->iv->radio->switchFrequency(q->iv, HOY_BOOT_FREQ_KHZ, (q->iv->config->frequency*FREQ_STEP_KHZ + HOY_BASE_FREQ_KHZ));
- mWaitTime.startTimeMonitor(1000);
+ case States::CHECK_FRAMES: {
+ if((q->iv->radio->mBufCtrl.empty() && !mIsRetransmit) || (0 == q->attempts)) { // radio buffer empty or no more answers
+ if(*mSerialDebug) {
+ DPRINT_IVID(DBG_INFO, q->iv->id);
+ DBGPRINT(F("request timeout: "));
+ DBGPRINT(String(q->iv->radio->mRadioWaitTime.getRunTime()));
+ DBGPRINTLN(F("ms"));
+ }
+ if(!q->iv->mGotFragment) {
+ if(q->iv->ivRadioType == INV_RADIO_TYPE_CMT) {
+ q->iv->radio->switchFrequency(q->iv, HOY_BOOT_FREQ_KHZ, (q->iv->config->frequency*FREQ_STEP_KHZ + HOY_BASE_FREQ_KHZ));
+ mWaitTime.startTimeMonitor(1000);
+ } else {
+ if(IV_MI == q->iv->ivGen)
+ q->iv->mIvTxCnt++;
+
+ if(mFirstTry) {
+ mFirstTry = false;
+ setAttempt();
+ mHeu.evalTxChQuality(q->iv, false, 0, 0);
+ q->iv->radioStatistics.rxFailNoAnser++;
+ q->iv->radioStatistics.retransmits++;
+ q->iv->radio->mRadioWaitTime.stopTimeMonitor();
+ mState = States::START;
+
+ return;
}
}
- closeRequest(q, false);
- break;
}
- mFirstTry = false; // for correct reset
- if((IV_MI != q->iv->ivGen) || (0 == q->attempts))
- mIsRetransmit = false;
+ closeRequest(q, false);
+ break;
+ }
+ mFirstTry = false; // for correct reset
+ if((IV_MI != q->iv->ivGen) || (0 == q->attempts))
+ mIsRetransmit = false;
- while(!q->iv->radio->mBufCtrl.empty()) {
- packet_t *p = &q->iv->radio->mBufCtrl.front();
- printRxInfo(q, p);
+ while(!q->iv->radio->mBufCtrl.empty()) {
+ packet_t *p = &q->iv->radio->mBufCtrl.front();
- if(validateIvSerial(&p->packet[1], q->iv)) {
- q->iv->radioStatistics.frmCnt++;
- q->iv->mDtuRxCnt++;
-
- if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
- if(parseFrame(p))
- q->iv->curFrmCnt++;
- } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
- if(parseDevCtrl(p, q))
- closeRequest(q, true);
- else
- closeRequest(q, false);
- q->iv->radio->mBufCtrl.pop();
- return; // don't wait for empty buffer
- } else if(IV_MI == q->iv->ivGen) {
- if(parseMiFrame(p, q))
- q->iv->curFrmCnt++;
- }
- } //else -> serial does not match
+ if(validateIvSerial(&p->packet[1], q->iv)) {
+ printRxInfo(q, p);
+ q->iv->radioStatistics.frmCnt++;
+ q->iv->mDtuRxCnt++;
+
+ if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command
+ if(parseFrame(p))
+ q->iv->curFrmCnt++;
+ } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command
+ if(parseDevCtrl(p, q))
+ closeRequest(q, true);
+ else
+ closeRequest(q, false);
+ q->iv->radio->mBufCtrl.pop();
+ return; // don't wait for empty buffer
+ } else if(IV_MI == q->iv->ivGen) {
+ if(parseMiFrame(p, q))
+ q->iv->curFrmCnt++;
+ }
+ } //else -> serial does not match
- q->iv->radio->mBufCtrl.pop();
- yield();
- }
+ q->iv->radio->mBufCtrl.pop();
+ yield();
+ }
- if(0 == q->attempts) {
- DPRINT_IVID(DBG_INFO, q->iv->id);
- DBGPRINT(F("no attempts left"));
- closeRequest(q, false);
+ if(q->iv->ivGen != IV_MI) {
+ mState = States::CHECK_PACKAGE;
+ } else {
+ bool fastNext = true;
+ if(q->iv->miMultiParts < 6) {
+ mState = States::WAIT;
+ if((q->iv->radio->mRadioWaitTime.isTimeout() && mIsRetransmit) || !mIsRetransmit) {
+ miRepeatRequest(q);
+ return;
+ }
} else {
- if(q->iv->ivGen != IV_MI) {
- mState = States::CHECK_PACKAGE;
- } else {
- bool fastNext = true;
- if(q->iv->miMultiParts < 6) {
- mState = States::WAIT;
- if((mWaitTime.isTimeout() && mIsRetransmit) || !mIsRetransmit) {
- miRepeatRequest(q);
- return;
- }
- } else {
- mHeu.evalTxChQuality(q->iv, true, (4 - q->attempts), q->iv->curFrmCnt);
- if(((q->cmd == 0x39) && (q->iv->type == INV_TYPE_4CH))
- || ((q->cmd == MI_REQ_CH2) && (q->iv->type == INV_TYPE_2CH))
- || ((q->cmd == MI_REQ_CH1) && (q->iv->type == INV_TYPE_1CH))) {
- miComplete(q->iv);
- fastNext = false;
- }
- if(fastNext)
- miNextRequest(q->iv->type == INV_TYPE_4CH ? MI_REQ_4CH : MI_REQ_CH1, q);
- else
- closeRequest(q, true);
- }
+ mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
+ if(((q->cmd == 0x39) && (q->iv->type == INV_TYPE_4CH))
+ || ((q->cmd == MI_REQ_CH2) && (q->iv->type == INV_TYPE_2CH))
+ || ((q->cmd == MI_REQ_CH1) && (q->iv->type == INV_TYPE_1CH))) {
+ miComplete(q->iv);
+ fastNext = false;
}
+ if(fastNext)
+ miNextRequest(q->iv->type == INV_TYPE_4CH ? MI_REQ_4CH : MI_REQ_CH1, q);
+ else
+ closeRequest(q, true);
}
+ }
- }
- break;
+ }
+ break;
- case States::CHECK_PACKAGE:
- uint8_t framnr = 0;
- if(0 == mMaxFrameId) {
- uint8_t i = 0;
- while(i < MAX_PAYLOAD_ENTRIES) {
- if(mLocalBuf[i].len == 0) {
- framnr = i+1;
- break;
- }
- i++;
+ case States::CHECK_PACKAGE:
+ uint8_t framnr = 0;
+ if(0 == mMaxFrameId) {
+ uint8_t i = 0;
+ while(i < MAX_PAYLOAD_ENTRIES) {
+ if(mLocalBuf[i].len == 0) {
+ framnr = i+1;
+ break;
}
+ i++;
}
+ }
- if(!framnr) {
- for(uint8_t i = 0; i < mMaxFrameId; i++) {
- if(mLocalBuf[i].len == 0) {
- framnr = i+1;
- break;
- }
+ if(!framnr) {
+ for(uint8_t i = 0; i < mMaxFrameId; i++) {
+ if(mLocalBuf[i].len == 0) {
+ framnr = i+1;
+ break;
}
}
+ }
- if(framnr) {
- setAttempt();
-
- if(*mSerialDebug) {
- DPRINT_IVID(DBG_WARN, q->iv->id);
- DBGPRINT(F("frame "));
- DBGPRINT(String(framnr));
- DBGPRINT(F(" missing: request retransmit ("));
- DBGPRINT(String(q->attempts));
- DBGPRINTLN(F(" attempts left)"));
- }
- if (!mIsRetransmit)
- q->iv->mIsSingleframeReq = true;
- sendRetransmit(q, (framnr-1));
- mIsRetransmit = true;
+ if(framnr) {
+ if(0 == q->attempts) {
+ DPRINT_IVID(DBG_INFO, q->iv->id);
+ DBGPRINT(F("no attempts left"));
+ closeRequest(q, false);
return;
}
+ setAttempt();
+
+ if(*mSerialDebug) {
+ DPRINT_IVID(DBG_WARN, q->iv->id);
+ DBGPRINT(F("frame "));
+ DBGPRINT(String(framnr));
+ DBGPRINT(F(" missing: request retransmit ("));
+ DBGPRINT(String(q->attempts));
+ DBGPRINTLN(F(" attempts left)"));
+ }
+ if (!mIsRetransmit)
+ q->iv->mIsSingleframeReq = true;
+ sendRetransmit(q, (framnr-1));
+ mIsRetransmit = true;
+ return;
+ }
- compilePayload(q);
+ compilePayload(q);
- if((NULL != mCbPayload) && (GridOnProFilePara != q->cmd) && (GetLossRate != q->cmd))
- (mCbPayload)(q->cmd, q->iv);
+ if((NULL != mCbPayload) && (GridOnProFilePara != q->cmd) && (GetLossRate != q->cmd))
+ (mCbPayload)(q->cmd, q->iv);
- closeRequest(q, true);
- break;
- }
- });
+ closeRequest(q, true);
+ break;
+ }
}
- private:
inline void printRxInfo(const queue_s *q, packet_t *p) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("RX "));
@@ -267,7 +284,7 @@ class Communication : public CommQueue<> {
DBGPRINT(String(p->millis));
DBGPRINT(F("ms | "));
DBGPRINT(String(p->len));
- if((IV_HM == q->iv->ivGen) || (IV_MI == q->iv->ivGen)) {
+ if(INV_RADIO_TYPE_NRF == q->iv->ivRadioType) {
DBGPRINT(F(" CH"));
if(3 == p->ch)
DBGPRINT(F("0"));
@@ -292,16 +309,61 @@ class Communication : public CommQueue<> {
}
}
+
+ inline uint8_t getFramesExpected(const queue_s *q) {
+ if(q->isDevControl)
+ return 1;
+
+ if(q->iv->ivGen != IV_MI) {
+ if (q->cmd == RealTimeRunData_Debug) {
+ uint8_t framecnt[4] = {2, 3, 4, 7};
+ return framecnt[q->iv->type];
+ }
+
+ switch (q->cmd) {
+ case InverterDevInform_All:
+ case GetLossRate:
+ case SystemConfigPara:
+ return 1;
+ case AlarmData: return 0x0c;
+ case GridOnProFilePara: return 6;
+
+ /*HardWareConfig = 3, // 0x03
+ SimpleCalibrationPara = 4, // 0x04
+ RealTimeRunData_Reality = 12, // 0x0c
+ RealTimeRunData_A_Phase = 13, // 0x0d
+ RealTimeRunData_B_Phase = 14, // 0x0e
+ RealTimeRunData_C_Phase = 15, // 0x0f
+ AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms
+ RecordData = 19, // 0x13
+ InternalData = 20, // 0x14
+ GetSelfCheckState = 30, // 0x1e
+ */
+
+ default: return 8; // for the moment, this should result in sth. like a default timeout of 500ms
+ }
+
+ } else { //MI
+ switch (q->cmd) {
+ case MI_REQ_CH1:
+ case MI_REQ_CH2:
+ return 2;
+ case 0x0f: return 3;
+ default: return 1;
+ }
+ }
+ }
+
inline bool validateIvSerial(uint8_t buf[], Inverter<> *iv) {
uint8_t tmp[4];
CP_U32_BigEndian(tmp, iv->radioId.u64 >> 8);
for(uint8_t i = 0; i < 4; i++) {
if(tmp[i] != buf[i]) {
- DPRINT(DBG_WARN, F("Inverter serial does not match, got: 0x"));
+ /*DPRINT(DBG_WARN, F("Inverter serial does not match, got: 0x"));
DHEX(buf[0]);DHEX(buf[1]);DHEX(buf[2]);DHEX(buf[3]);
DBGPRINT(F(", expected: 0x"));
DHEX(tmp[0]);DHEX(tmp[1]);DHEX(tmp[2]);DHEX(tmp[3]);
- DBGPRINTLN("");
+ DBGPRINTLN("");*/
return false;
}
}
@@ -351,8 +413,13 @@ class Communication : public CommQueue<> {
// small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39
//mPayload[iv->id].txId = p->packet[0];
miDataDecode(p, q);
- } else if (p->packet[0] == (0x0f + ALL_FRAMES))
+ } else if (p->packet[0] == (0x0f + ALL_FRAMES)) {
miHwDecode(p, q);
+ } else if (p->packet[0] == ( 0x10 + ALL_FRAMES)) {
+ // MI response from get Grid Profile information request
+ miGPFDecode(p, q);
+ }
+
else if ((p->packet[0] == 0x88) || (p->packet[0] == 0x92)) {
record_t<> *rec = q->iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure
rec->ts = q->ts;
@@ -392,10 +459,11 @@ class Communication : public CommQueue<> {
DBGPRINT(F("has "));
if(!accepted) DBGPRINT(F("not "));
DBGPRINT(F("accepted power limit set point "));
- DBGPRINT(String(q->iv->powerLimit[0]));
+ DBGPRINT(String((float)q->iv->powerLimit[0]/10.0));
DBGPRINT(F(" with PowerLimitControl "));
DBGPRINTLN(String(q->iv->powerLimit[1]));
q->iv->actPowerLimit = 0xffff; // unknown, readback current value
+ (mCbPwrAck)(q->iv);
return accepted;
}
@@ -487,6 +555,7 @@ class Communication : public CommQueue<> {
for (uint8_t i = 0; i < rec->length; i++) {
q->iv->addValue(i, mPayload, rec);
}
+ rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
q->iv->rssi = rssi;
q->iv->doCalculations();
@@ -504,20 +573,18 @@ class Communication : public CommQueue<> {
}
void sendRetransmit(const queue_s *q, uint8_t i) {
- if(q->attempts) {
- q->iv->radio->sendCmdPacket(q->iv, TX_REQ_INFO, (SINGLE_FRAME + i), true);
- q->iv->radioStatistics.retransmits++;
- mWaitTime.startTimeMonitor(SINGLEFR_TIMEOUT); // timeout
- mState = States::WAIT;
- } else {
- //add(q, true);
- closeRequest(q, false);
- }
+ mFramesExpected = 1;
+ q->iv->radio->setExpectedFrames(mFramesExpected);
+ q->iv->radio->sendCmdPacket(q->iv, TX_REQ_INFO, (SINGLE_FRAME + i), true);
+ q->iv->radioStatistics.retransmits++;
+ q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
+
+ mState = States::WAIT;
}
private:
void closeRequest(const queue_s *q, bool crcPass) {
- mHeu.evalTxChQuality(q->iv, crcPass, (4 - q->attempts), q->iv->curFrmCnt);
+ mHeu.evalTxChQuality(q->iv, crcPass, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
if(crcPass)
q->iv->radioStatistics.rxSuccess++;
else if(q->iv->mGotFragment)
@@ -535,7 +602,6 @@ class Communication : public CommQueue<> {
q->iv->mGotLastMsg = false;
q->iv->miMultiParts = 0;
mIsRetransmit = false;
- mFirstTry = false; // for correct reset
mState = States::RESET;
DBGPRINTLN(F("-----"));
}
@@ -592,6 +658,8 @@ class Communication : public CommQueue<> {
rec->ts = q->ts;
q->iv->setValue(1, rec, (uint32_t) ((p->packet[24] << 8) + p->packet[25])/1);
q->iv->miMultiParts +=4;
+ rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
+
} else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10
DPRINT_IVID(DBG_INFO, q->iv->id);
if ( p->packet[9] == 0x01 ) {
@@ -608,6 +676,7 @@ class Communication : public CommQueue<> {
record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec->ts = q->ts;
q->iv->setValue(0, rec, (uint32_t) ((((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])/1);
+ rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
if(*mSerialDebug) {
DPRINT(DBG_INFO,F("HW_FB_TLmValue "));
@@ -650,22 +719,32 @@ class Communication : public CommQueue<> {
(mCbPayload)(InverterDevInform_Simple, q->iv);
q->iv->miMultiParts++;
}
- //if(q->iv->miMultiParts > 5)
- //closeRequest(q->iv, true);
- //else
- //if(q->iv->miMultiParts < 6)
- // mState = States::WAIT;
-
- /*if (mPayload[iv->id].multi_parts > 5) {
- iv->setQueuedCmdFinished();
- mPayload[iv->id].complete = true;
- mPayload[iv->id].rxTmo = true;
- mPayload[iv->id].requested= false;
- iv->radioStatistics.rxSuccess++;
- }
- if (mHighPrioIv == NULL)
- mHighPrioIv = iv;
- */
+ }
+
+ inline void miGPFDecode(packet_t *p, const queue_s *q) {
+ record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
+ rec->ts = q->ts;
+ rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
+
+ q->iv->setValue(2, rec, (uint32_t) (((p->packet[10] << 8) | p->packet[11]))); //FLD_GRID_PROFILE_CODE
+ q->iv->setValue(3, rec, (uint32_t) (((p->packet[12] << 8) | p->packet[13]))); //FLD_GRID_PROFILE_VERSION
+
+ /* according to xlsx (different start byte -1!)
+ Polling Grid-connected Protection Parameter File Command - Receipt
+ byte[10] ST1 indicates the status of the grid-connected protection file. ST1=1 indicates the default grid-connected protection file, ST=2 indicates that the grid-connected protection file is configured and normal, ST=3 indicates that the grid-connected protection file cannot be recognized, ST=4 indicates that the grid-connected protection file is damaged
+ byte[11] byte[12] CountryStd variable indicates the national standard code of the grid-connected protection file
+ byte[13] byte[14] Version indicates the version of the grid-connected protection file
+ byte[15] byte[16]
+ */
+ /*if(mSerialDebug) {
+ DPRINT(DBG_INFO,F("ST1 "));
+ DBGPRINTLN(String(p->packet[9]));
+ DPRINT(DBG_INFO,F("CountryStd "));
+ DBGPRINTLN(String((p->packet[10] << 8) + p->packet[11]));
+ DPRINT(DBG_INFO,F("Version "));
+ DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13]));
+ }*/
+ q->iv->miMultiParts = 7; // indicate we are ready
}
inline void miDataDecode(packet_t *p, const queue_s *q) {
@@ -709,21 +788,20 @@ class Communication : public CommQueue<> {
miStsConsolidate(q, datachan, rec, p->packet[23], p->packet[24]);
if (p->packet[0] < (0x39 + ALL_FRAMES) ) {
- mHeu.evalTxChQuality(q->iv, true, (4 - q->attempts), 1);
+ mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), 1);
miNextRequest((p->packet[0] - ALL_FRAMES + 1), q);
} else {
q->iv->miMultiParts = 7; // indicate we are ready
- //miComplete(q->iv);
}
} else if((p->packet[0] == (MI_REQ_CH1 + ALL_FRAMES)) && (q->iv->type == INV_TYPE_2CH)) {
//addImportant(q->iv, MI_REQ_CH2);
miNextRequest(MI_REQ_CH2, q);
- mHeu.evalTxChQuality(q->iv, true, (4 - q->attempts), q->iv->curFrmCnt);
- //use also miMultiParts here for better statistics?
- //mHeu.setGotFragment(q->iv);
- } else { // first data msg for 1ch, 2nd for 2ch
+ mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
+ q->iv->mIvRxCnt++; // statistics workaround...
+
+ } else { // first data msg for 1ch, 2nd for 2ch
q->iv->miMultiParts += 6; // indicate we are ready
- //miComplete(q->iv);
+
}
}
@@ -737,16 +815,14 @@ class Communication : public CommQueue<> {
DBGHEXLN(cmd);
}
- if(q->iv->miMultiParts == 7) {
- //mHeu.setGotAll(q->iv);
+ if(q->iv->miMultiParts == 7)
q->iv->radioStatistics.rxSuccess++;
- } else
- //mHeu.setGotFragment(q->iv);
- /*iv->radioStatistics.rxFail++; // got no complete payload*/
- //q->iv->radioStatistics.retransmits++;
+
+ mFramesExpected = getFramesExpected(q);
+ q->iv->radio->setExpectedFrames(mFramesExpected);
q->iv->radio->sendCmdPacket(q->iv, cmd, 0x00, true);
- mWaitTime.startTimeMonitor(MI_TIMEOUT);
+ q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
q->iv->miMultiParts = 0;
q->iv->mGotFragment = 0;
mIsRetransmit = true;
@@ -766,8 +842,7 @@ class Communication : public CommQueue<> {
q->iv->radio->sendCmdPacket(q->iv, q->cmd, 0x00, true);
- mWaitTime.startTimeMonitor(MI_TIMEOUT);
- //mState = States::WAIT;
+ q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
mIsRetransmit = false;
}
@@ -833,6 +908,8 @@ class Communication : public CommQueue<> {
if (!stsok) {
q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts);
q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0);
+ rec->ts = q->ts;
+ rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
}
if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) {
@@ -851,6 +928,26 @@ class Communication : public CommQueue<> {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("got all data msgs"));
}
+
+ if (iv->mGetLossInterval >= AHOY_GET_LOSS_INTERVAL) { // initially mIvRxCnt = mIvTxCnt = 0
+ iv->mGetLossInterval = 1;
+ iv->radioStatistics.ivSent = iv->mIvRxCnt + iv->mDtuTxCnt; // iv->mIvRxCnt is the nr. of additional answer frames, default we expect one frame per request
+ iv->radioStatistics.ivLoss = iv->radioStatistics.ivSent - iv->mDtuRxCnt; // this is what we didn't receive
+ iv->radioStatistics.dtuLoss = iv->mIvTxCnt; // this is somehow the requests w/o answers in that periode
+ iv->radioStatistics.dtuSent = iv->mDtuTxCnt;
+ if (mSerialDebug) {
+ DPRINT_IVID(DBG_INFO, iv->id);
+ DBGPRINTLN("DTU loss: " +
+ String (iv->radioStatistics.ivLoss) + "/" +
+ String (iv->radioStatistics.ivSent) + " frames for " +
+ String (iv->radioStatistics.dtuSent) + " requests");
+ }
+ iv->mIvRxCnt = 0; // start new interval, iVRxCnt is abused to collect additional possible frames
+ iv->mIvTxCnt = 0; // start new interval, iVTxCnt is abused to collect nr. of unanswered requests
+ iv->mDtuRxCnt = 0; // start new interval
+ iv->mDtuTxCnt = 0; // start new interval
+ }
+
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0));
@@ -871,17 +968,12 @@ class Communication : public CommQueue<> {
iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) ac_pow/10);
iv->doCalculations();
+ rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
// update status state-machine,
if (ac_pow)
iv->isProducing();
- //closeRequest(iv, iv->miMultiParts > 5);
-
- //mHeu.setGotAll(iv);
- //cmdDone(false);
if(NULL != mCbPayload)
(mCbPayload)(RealTimeRunData_Debug, iv);
-
- //mState = States::RESET; // everything ok, next request
}
private:
@@ -905,14 +997,15 @@ class Communication : public CommQueue<> {
bool mFirstTry = false; // see, if we should do a second try
bool mIsRetransmit = false; // we already had waited one complete cycle
uint8_t mMaxFrameId;
+ uint8_t mFramesExpected = 12; // 0x8c was highest last frame for alarm data
+ uint16_t mTimeout = 0; // calculating that once should be ok
uint8_t mPayload[MAX_BUFFER];
payloadListenerType mCbPayload = NULL;
+ powerLimitAckListenerType mCbPwrAck = NULL;
alarmListenerType mCbAlarm = NULL;
Heuristic mHeu;
uint32_t mLastEmptyQueueMillis = 0;
bool mPrintSequenceDuration = false;
-
- //States mDebugState = States::START;
};
#endif /*__COMMUNICATION_H__*/
diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h
index 55259289..969e4e1b 100644
--- a/src/hm/hmDefines.h
+++ b/src/hm/hmDefines.h
@@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://github.com/lumpapu/ahoy
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HM_DEFINES_H__
@@ -76,6 +76,7 @@ enum {CMD_CALC = 0xffff};
enum {CH0 = 0, CH1, CH2, CH3, CH4, CH5, CH6};
enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH};
+enum {INV_RADIO_TYPE_NRF = 0, INV_RADIO_TYPE_CMT};
#define WORK_FREQ_KHZ 865000 // desired work frequency between DTU and
// inverter in kHz
@@ -86,6 +87,12 @@ enum {INV_TYPE_1CH = 0, INV_TYPE_2CH, INV_TYPE_4CH, INV_TYPE_6CH};
#define FREQ_WARN_MIN_KHZ 863000 // for EU 863 - 870 MHz is allowed
#define FREQ_WARN_MAX_KHZ 870000 // for EU 863 - 870 MHz is allowed
+#define DURATION_ONEFRAME 50 // timeout parameter for each expected frame (ms)
+//#define DURATION_RESERVE {90,120} // timeout parameter to still wait after last expected frame (ms)
+#define DURATION_TXFRAME 85 // timeout parameter for first transmission and first expected frame (time to first channel switch from tx start!) (ms)
+#define DURATION_LISTEN_MIN 5 // time to stay at least on a listening channel (ms)
+#define DURATION_PAUSE_LASTFR 45 // how long to pause after last frame (ms)
+const uint8_t duration_reserve[2] = {115,115};
typedef struct {
uint8_t fieldId; // field id
diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h
index 776b9c6f..b28682e5 100644
--- a/src/hm/hmInverter.h
+++ b/src/hm/hmInverter.h
@@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HM_INVERTER_H__
@@ -14,6 +14,7 @@
#define MAX_GRID_LENGTH 150
#include "hmDefines.h"
+#include "../appInterface.h"
#include "HeuristicInv.h"
#include "../hms/hmsDefines.h"
#include
@@ -64,13 +65,28 @@ struct calcFunc_t {
func_t* func; // function pointer
};
+enum class MqttSentStatus : uint8_t {
+ NEW_DATA,
+ LAST_SUCCESS_SENT,
+ DATA_SENT
+};
+
+enum class InverterStatus : uint8_t {
+ OFF,
+ STARTING,
+ PRODUCING,
+ WAS_PRODUCING,
+ WAS_ON
+};
+
template
struct record_t {
- byteAssign_t* assign; // assignment of bytes in payload
- uint8_t length; // length of the assignment list
- T *record; // data pointer
- uint32_t ts; // timestamp of last received payload
- uint8_t pyldLen; // expected payload length for plausibility check
+ byteAssign_t* assign; // assignment of bytes in payload
+ uint8_t length; // length of the assignment list
+ T *record; // data pointer
+ uint32_t ts; // timestamp of last received payload
+ uint8_t pyldLen; // expected payload length for plausibility check
+ MqttSentStatus mqttSentStatus; // indicates the current MqTT sent status
};
struct alarm_t {
@@ -94,23 +110,16 @@ const calcFunc_t calcFunctions[] = {
{ CALC_MPDC_CH, &calcMaxPowerDc }
};
-enum class InverterStatus : uint8_t {
- OFF,
- STARTING,
- PRODUCING,
- WAS_PRODUCING,
- WAS_ON
-};
-
template
class Inverter {
public:
uint8_t ivGen; // generation of inverter (HM / MI)
+ uint8_t ivRadioType; // refers to used radio (nRF24 / CMT)
cfgIv_t *config; // stored settings
uint8_t id; // unique id
uint8_t type; // integer which refers to inverter type
uint16_t alarmMesIndex; // Last recorded Alarm Message Index
- uint16_t powerLimit[2]; // limit power output
+ uint16_t powerLimit[2]; // limit power output (multiplied by 10)
float actPowerLimit; // actual power limit
bool powerLimitAck; // acknowledged power limit (default: false)
uint8_t devControlCmd; // carries the requested cmd
@@ -124,35 +133,32 @@ class Inverter {
bool isConnected; // shows if inverter was successfully identified (fw version and hardware info)
InverterStatus status; // indicates the current inverter status
std::array lastAlarm; // holds last 10 alarms
- uint8_t alarmNxtWrPos; // indicates the position in array (rolling buffer)
+ int8_t rssi; // RSSI
uint16_t alarmCnt; // counts the total number of occurred alarms
uint16_t alarmLastId; // lastId which was received
- int8_t rssi; // RSSI
+ uint8_t mCmd; // holds the command to send
+ bool mGotFragment; // shows if inverter has sent at least one fragment
uint8_t miMultiParts; // helper info for MI multiframe msgs
uint8_t outstandingFrames; // helper info to count difference between expected and received frames
- bool mGotFragment; // shows if inverter has sent at least one fragment
uint8_t curFrmCnt; // count received frames in current loop
bool mGotLastMsg; // shows if inverter has already finished transmission cycle
- uint8_t mCmd; // holds the command to send
bool mIsSingleframeReq; // indicates this is a missing single frame request
Radio *radio; // pointer to associated radio class
statistics_t radioStatistics; // information about transmitted, failed, ... packets
HeuristicInv heuristics; // heuristic information / logic
uint8_t curCmtFreq; // current used CMT frequency, used to check if freq. was changed during runtime
bool commEnabled; // 'pause night communication' sets this field to false
+ uint32_t tsMaxAcPower; // holds the timestamp when the MaxAC power was seen
- uint16_t mIvRxCnt; // last iv rx frames (from GetLossRate)
- uint16_t mIvTxCnt; // last iv tx frames (from GetLossRate)
- uint16_t mDtuRxCnt; // cur dtu rx frames (since last GetLossRate)
- uint16_t mDtuTxCnt; // cur dtu tx frames (since last getLoassRate)
- uint8_t mGetLossInterval; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debu
-
- static uint32_t *timestamp; // system timestamp
+ static uint32_t *timestamp; // system timestamp
static cfgInst_t *generalConfig; // general inverter configuration from setup
+ //static IApp *app; // pointer to app interface
+
+ public:
Inverter() {
ivGen = IV_HM;
- powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited
+ powerLimit[0] = 0xffff; // 6553.5 W Limit -> unlimited
powerLimit[1] = AbsolutNonPersistent; // default power limit setting
powerLimitAck = false;
actPowerLimit = 0xffff; // init feedback from inverter to -1
@@ -161,7 +167,6 @@ class Inverter {
alarmMesIndex = 0;
isConnected = false;
status = InverterStatus::OFF;
- alarmNxtWrPos = 0;
alarmCnt = 0;
alarmLastId = 0;
rssi = -127;
@@ -171,10 +176,7 @@ class Inverter {
mIsSingleframeReq = false;
radio = NULL;
commEnabled = true;
- mIvRxCnt = 0;
- mIvTxCnt = 0;
- mDtuRxCnt = 0;
- mDtuTxCnt = 0;
+ tsMaxAcPower = 0;
memset(&radioStatistics, 0, sizeof(statistics_t));
memset(heuristics.txRfQuality, -6, 5);
@@ -187,36 +189,49 @@ class Inverter {
if(mDevControlRequest) {
cb(devControlCmd, true);
mDevControlRequest = false;
- } else if (IV_MI != ivGen) {
+ } else if (IV_MI != ivGen) { // HM / HMS / HMT
mGetLossInterval++;
- if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0))
- cb(AlarmData, false); // get last alarms
- else if(0 == getFwVersion())
- cb(InverterDevInform_All, false); // get firmware version
- else if(0 == getHwVersion())
- cb(InverterDevInform_Simple, false); // get hardware version
- else if(actPowerLimit == 0xffff)
- cb(SystemConfigPara, false); // power limit info
- else if(InitDataState != devControlCmd) {
- cb(devControlCmd, false); // custom command which was received by API
- devControlCmd = InitDataState;
- mGetLossInterval = 1;
- } else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile
- cb(GridOnProFilePara, false);
- } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate
- mGetLossInterval = 1;
- cb(GetLossRate, false);
- } else
+ if(mNextLive)
cb(RealTimeRunData_Debug, false); // get live data
- } else {
- if(0 == getFwVersion())
- cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number
else {
+ if(actPowerLimit == 0xffff)
+ cb(SystemConfigPara, false); // power limit info
+ else if(InitDataState != devControlCmd) {
+ cb(devControlCmd, false); // custom command which was received by API
+ devControlCmd = InitDataState;
+ mGetLossInterval = 1;
+ } else if(0 == getFwVersion())
+ cb(InverterDevInform_All, false); // get firmware version
+ else if(0 == getHwVersion())
+ cb(InverterDevInform_Simple, false); // get hardware version
+ else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0))
+ cb(AlarmData, false); // get last alarms
+ else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile
+ cb(GridOnProFilePara, false);
+ } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate
+ mGetLossInterval = 1;
+ cb(RealTimeRunData_Debug, false); // get live data
+ cb(GetLossRate, false);
+ } else
+ cb(RealTimeRunData_Debug, false); // get live data
+ }
+ } else { // MI
+ if(0 == getFwVersion()) {
+ mIvRxCnt +=2;
+ cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number
+ } else {
record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
- if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0)
+ if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) {
cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
- else
+ mIvRxCnt +=2;
+ } else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile
+ cb(0x10, false); // legacy GPF command
+ else {
cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false);
+ mGetLossInterval++;
+ if (type != INV_TYPE_4CH)
+ mIvRxCnt++; // statistics workaround...
+ }
}
}
}
@@ -275,6 +290,7 @@ class Inverter {
if(isConnected) {
mDevControlRequest = true;
devControlCmd = cmd;
+ //app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)"
}
return isConnected;
}
@@ -321,8 +337,8 @@ class Inverter {
}
if(rec == &recordMeas) {
+ mNextLive = false; // live data received
DPRINTLN(DBG_VERBOSE, "add real time");
-
// get last alarm message index and save it in the inverter object
if (getPosByChFld(0, FLD_EVT, rec) == pos) {
if (alarmMesIndex < rec->record[pos]) {
@@ -333,28 +349,27 @@ class Inverter {
}
}
}
- else if (rec->assign == InfoAssignment) {
- DPRINTLN(DBG_DEBUG, "add info");
- // eg. fw version ...
- isConnected = true;
- }
- else if (rec->assign == SimpleInfoAssignment) {
- DPRINTLN(DBG_DEBUG, "add simple info");
- // eg. hw version ...
- }
- else if (rec->assign == SystemConfigParaAssignment) {
- DPRINTLN(DBG_DEBUG, "add config");
- if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){
- actPowerLimit = rec->record[pos];
- DPRINT(DBG_DEBUG, F("Inverter actual power limit: "));
- DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1));
- }
- }
- else if (rec->assign == AlarmDataAssignment) {
- DPRINTLN(DBG_DEBUG, "add alarm");
+ else {
+ mNextLive = true;
+ if (rec->assign == InfoAssignment) {
+ DPRINTLN(DBG_DEBUG, "add info");
+ // eg. fw version ...
+ isConnected = true;
+ } else if (rec->assign == SimpleInfoAssignment) {
+ DPRINTLN(DBG_DEBUG, "add simple info");
+ // eg. hw version ...
+ } else if (rec->assign == SystemConfigParaAssignment) {
+ DPRINTLN(DBG_DEBUG, "add config");
+ if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){
+ actPowerLimit = rec->record[pos];
+ DPRINT(DBG_DEBUG, F("Inverter actual power limit: "));
+ DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1));
+ }
+ } else if (rec->assign == AlarmDataAssignment) {
+ DPRINTLN(DBG_DEBUG, "add alarm");
+ } else
+ DPRINTLN(DBG_WARN, F("add with unknown assignment"));
}
- else
- DPRINTLN(DBG_WARN, F("add with unknown assignment"));
}
else
DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));
@@ -506,6 +521,7 @@ class Inverter {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:initAssignment"));
rec->ts = 0;
rec->length = 0;
+ rec->mqttSentStatus = MqttSentStatus::DATA_SENT; // nothing new to transmit
switch (cmd) {
case RealTimeRunData_Debug:
if (INV_TYPE_1CH == type) {
@@ -590,7 +606,7 @@ class Inverter {
void resetAlarms() {
lastAlarm.fill({0, 0, 0});
- alarmNxtWrPos = 0;
+ mAlarmNxtWrPos = 0;
alarmCnt = 0;
alarmLastId = 0;
@@ -603,21 +619,44 @@ class Inverter {
uint16_t rxCnt = (pyld[0] << 8) + pyld[1];
uint16_t txCnt = (pyld[2] << 8) + pyld[3];
- if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past
+ if (mIvRxCnt || mIvTxCnt) { // there was successful GetLossRate in the past
+ radioStatistics.ivSent = mDtuTxCnt;
+ if (rxCnt < mIvRxCnt) // overflow
+ radioStatistics.ivLoss = radioStatistics.ivSent - (rxCnt + ((uint16_t)65535 - mIvRxCnt) + 1);
+ else
+ radioStatistics.ivLoss = radioStatistics.ivSent - (rxCnt - mIvRxCnt);
+
+ if (txCnt < mIvTxCnt) // overflow
+ radioStatistics.dtuSent = txCnt + ((uint16_t)65535 - mIvTxCnt) + 1;
+ else
+ radioStatistics.dtuSent = txCnt - mIvTxCnt;
+
+ radioStatistics.dtuLoss = radioStatistics.dtuSent - mDtuRxCnt;
+
DPRINT_IVID(DBG_INFO, id);
- DBGPRINTLN("Inv loss: " +
- String (mDtuTxCnt - (rxCnt - mIvRxCnt)) + " of " +
- String (mDtuTxCnt) + ", DTU loss: " +
- String (txCnt - mIvTxCnt - mDtuRxCnt) + " of " +
- String (txCnt - mIvTxCnt));
+ DBGPRINT(F("Inv loss: "));
+ DBGPRINT(String(radioStatistics.ivLoss));
+ DBGPRINT(F(" of "));
+ DBGPRINT(String(radioStatistics.ivSent));
+ DBGPRINT(F(", DTU loss: "));
+ DBGPRINT(String(radioStatistics.dtuLoss));
+ DBGPRINT(F(" of "));
+ if(mAckCount) {
+ DBGPRINT(String(radioStatistics.dtuSent));
+ DBGPRINT(F(". ACKs: "));
+ DBGPRINTLN(String(mAckCount));
+ mAckCount = 0;
+ } else
+ DBGPRINTLN(String(radioStatistics.dtuSent));
}
- mIvRxCnt = rxCnt;
- mIvTxCnt = txCnt;
+ mIvRxCnt = rxCnt;
+ mIvTxCnt = txCnt;
mDtuRxCnt = 0; // start new interval
mDtuTxCnt = 0; // start new interval
return true;
}
+
return false;
}
@@ -789,9 +828,9 @@ class Inverter {
private:
inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) {
- lastAlarm[alarmNxtWrPos] = alarm_t(code, start, end);
- if(++alarmNxtWrPos >= 10) // rolling buffer
- alarmNxtWrPos = 0;
+ lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end);
+ if(++mAlarmNxtWrPos >= 10) // rolling buffer
+ mAlarmNxtWrPos = 0;
}
void toRadioId(void) {
@@ -809,6 +848,16 @@ class Inverter {
bool mDevControlRequest; // true if change needed
uint8_t mGridLen = 0;
uint8_t mGridProfile[MAX_GRID_LENGTH];
+ uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer)
+ bool mNextLive = true; // first read live data after booting up then version etc.
+
+ public:
+ uint16_t mDtuRxCnt = 0;
+ uint16_t mDtuTxCnt = 0;
+ uint8_t mGetLossInterval = 0; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug
+ uint16_t mIvRxCnt = 0;
+ uint16_t mIvTxCnt = 0;
+ uint16_t mAckCount = 0;
};
template
@@ -920,8 +969,10 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) {
acMaxPower = iv->getValue(i, rec);
}
}
- if(acPower > acMaxPower)
+ if(acPower > acMaxPower) {
+ iv->tsMaxAcPower = *iv->timestamp;
return acPower;
+ }
}
return acMaxPower;
}
diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h
index 0754f83c..67b1abcd 100644
--- a/src/hm/hmRadio.h
+++ b/src/hm/hmRadio.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://github.com/lumpapu/ahoy
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -48,8 +48,8 @@ class HmRadio : public Radio {
pinMode(irq, INPUT_PULLUP);
- mSerialDebug = serialDebug;
- mPrivacyMode = privacyMode;
+ mSerialDebug = serialDebug;
+ mPrivacyMode = privacyMode;
mPrintWholeTrace = printWholeTrace;
generateDtuSn();
@@ -78,21 +78,16 @@ class HmRadio : public Radio {
#else
mNrf24->begin(mSpi.get(), ce, cs);
#endif
- mNrf24->setRetries(3, 15); // 3*250us + 250us and 15 loops -> 15ms
+ mNrf24->setRetries(3, 15); // wait 3*250 = 750us, 16 * 250us -> 4000us = 4ms
- mNrf24->setChannel(mRfChLst[mRxChIdx]);
- mNrf24->startListening();
mNrf24->setDataRate(RF24_250KBPS);
- mNrf24->setAutoAck(true);
- mNrf24->enableDynamicAck();
+ //mNrf24->setAutoAck(true); // enabled by default
+ //mNrf24->enableDynamicAck();
mNrf24->enableDynamicPayloads();
mNrf24->setCRCLength(RF24_CRC_16);
mNrf24->setAddressWidth(5);
mNrf24->openReadingPipe(1, reinterpret_cast(&DTU_RADIO_ID));
-
- // enable all receiving interrupts
- mNrf24->maskIRQ(false, false, false);
-
+ mNrf24->maskIRQ(false, false, false); // enable all receiving interrupts
mNrf24->setPALevel(1); // low is default
if(mNrf24->isChipConnected()) {
@@ -104,62 +99,97 @@ class HmRadio : public Radio {
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
- void loop(void) {
- if (!mIrqRcvd)
- return; // nothing to do
- mIrqRcvd = false;
- bool tx_ok, tx_fail, rx_ready;
- mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
- mNrf24->flush_tx(); // empty TX FIFO
-
- // start listening
- uint8_t chOffset = 2;
- mRxChIdx = (mTxChIdx + chOffset) % RF_CHANNELS;
- mNrf24->setChannel(mRfChLst[mRxChIdx]);
- mNrf24->startListening();
+ // returns true if communication is active
+ bool loop(void) {
+ if (!mIrqRcvd && !mNRFisInRX)
+ return false; // first quick check => nothing to do at all here
if(NULL == mLastIv) // prevent reading on NULL object!
- return;
+ return false;
- uint32_t innerLoopTimeout = 55000;
- uint32_t loopMillis = millis();
- uint32_t outerLoopTimeout = (mLastIv->mIsSingleframeReq) ? 100 : ((mLastIv->mCmd != AlarmData) && (mLastIv->mCmd != GridOnProFilePara)) ? 400 : 600;
- bool isRxInit = true;
+ if(!mIrqRcvd) { // no news from nRF, check timers
+ if ((millis() - mTimeslotStart) < innerLoopTimeout)
+ return true; // nothing to do, still waiting
+ if (mRadioWaitTime.isTimeout()) { // timeout reached!
+ mNRFisInRX = false;
+ return false;
+ }
- while ((millis() - loopMillis) < outerLoopTimeout) {
- uint32_t startMicros = micros();
- while ((micros() - startMicros) < innerLoopTimeout) { // listen (4088us or?) 5110us to each channel
- if (mIrqRcvd) {
- mIrqRcvd = false;
+ // otherwise switch to next RX channel
+ mTimeslotStart = millis();
+ if(!mNRFloopChannels && ((mTimeslotStart - mLastIrqTime) > (DURATION_TXFRAME+DURATION_ONEFRAME)))
+ mNRFloopChannels = true;
- if (getReceived()) { // everything received
- return;
- }
+ rxPendular = !rxPendular;
+ //innerLoopTimeout = (rxPendular ? 1 : 2)*DURATION_LISTEN_MIN;
+ innerLoopTimeout = DURATION_LISTEN_MIN;
- innerLoopTimeout = 4088*5;
- if (isRxInit) {
- isRxInit = false;
- if (micros() - startMicros < 42000) {
- innerLoopTimeout = 4088*12;
- mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
- mNrf24->setChannel(mRfChLst[mRxChIdx]);
- }
- }
+ if(mNRFloopChannels)
+ tempRxChIdx = (tempRxChIdx + 4) % RF_CHANNELS;
+ else
+ tempRxChIdx = (mRxChIdx + rxPendular*4) % RF_CHANNELS;
+
+ mNrf24->setChannel(mRfChLst[tempRxChIdx]);
+ isRxInit = false;
+
+ return true; // communicating, but changed RX channel
+ } else {
+ // here we got news from the nRF
+ mIrqRcvd = false;
+ mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
+ mLastIrqTime = millis();
- startMicros = micros();
+ if(tx_ok || tx_fail) { // tx related interrupt, basically we should start listening
+ mNrf24->flush_tx(); // empty TX FIFO
+ mTxSetupTime = millis() - mMillis;
+
+ if(mNRFisInRX) {
+ DPRINTLN(DBG_WARN, F("unexpected tx irq!"));
+ return false;
}
- yield();
+
+ mNRFisInRX = true;
+ if(tx_ok)
+ mLastIv->mAckCount++;
+
+ mRxChIdx = (mTxChIdx + 2) % RF_CHANNELS;
+ mNrf24->setChannel(mRfChLst[mRxChIdx]);
+ mNrf24->startListening();
+ mTimeslotStart = millis();
+ tempRxChIdx = mRxChIdx;
+ rxPendular = false;
+ mNRFloopChannels = (mLastIv->ivGen == IV_MI);
+
+ innerLoopTimeout = DURATION_TXFRAME;
}
- // switch to next RX channel
- mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
- mNrf24->setChannel(mRfChLst[mRxChIdx]);
- innerLoopTimeout = 4088;
- isRxInit = false;
+
+ if(rx_ready) {
+ if (getReceived()) { // check what we got, returns true for last package
+ mNRFisInRX = false;
+ mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first end his transmissions
+ mNrf24->stopListening();
+ } else {
+ innerLoopTimeout = DURATION_LISTEN_MIN;
+ mTimeslotStart = millis();
+ if (!mNRFloopChannels) {
+ //rxPendular = true; // stay longer on the next rx channel
+ if (isRxInit) {
+ isRxInit = false;
+ tempRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
+ mNrf24->setChannel(mRfChLst[tempRxChIdx]);
+ } else
+ mRxChIdx = tempRxChIdx;
+ }
+ }
+ return mNRFisInRX;
+ } /*else if(tx_fail) {
+ mNRFisInRX = false;
+ return false;
+ }*/
}
- // not finished but time is over
- return;
+ return false;
}
bool isChipConnected(void) {
@@ -177,10 +207,10 @@ class HmRadio : public Radio {
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[cnt++] = 0x00;
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
- mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit
- mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit
- mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
- mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
+ mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction)
+ mTxBuf[cnt++] = (data[0] ) & 0xff; // power limit
+ mTxBuf[cnt++] = (data[1] >> 8) & 0xff; // setting for persistens handlings
+ mTxBuf[cnt++] = (data[1] ) & 0xff; // setting for persistens handling
}
} else { //MI 2nd gen. specific
uint16_t powerMax = ((iv->powerLimit[1] == RelativNonPersistent) ? 0 : iv->getMaxPower());
@@ -249,6 +279,7 @@ class HmRadio : public Radio {
}
cnt++;
}
+
sendPacket(iv, cnt, isRetransmit, (IV_MI != iv->ivGen));
}
@@ -264,37 +295,40 @@ class HmRadio : public Radio {
private:
inline bool getReceived(void) {
- bool tx_ok, tx_fail, rx_ready;
- mNrf24->whatHappened(tx_ok, tx_fail, rx_ready); // resets the IRQ pin to HIGH
-
bool isLastPackage = false;
+ rx_ready = false; // reset for ACK case
+
while(mNrf24->available()) {
- uint8_t len;
- len = mNrf24->getDynamicPayloadSize(); // if payload size > 32, corrupt payload has been flushed
+ uint8_t len = mNrf24->getDynamicPayloadSize(); // payload size > 32 -> corrupt payload
+
if (len > 0) {
packet_t p;
- p.ch = mRfChLst[mRxChIdx];
- p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len;
+ p.ch = mRfChLst[tempRxChIdx];
+ p.len = (len > MAX_RF_PAYLOAD_SIZE) ? MAX_RF_PAYLOAD_SIZE : len;
p.rssi = mNrf24->testRPD() ? -64 : -75;
p.millis = millis() - mMillis;
mNrf24->read(p.packet, p.len);
+
if (p.packet[0] != 0x00) {
if(!checkIvSerial(p.packet, mLastIv)) {
DPRINT(DBG_WARN, "RX other inverter ");
- if(*mPrivacyMode)
- ah::dumpBuf(p.packet, p.len, 1, 4);
- else
+ if(!*mPrivacyMode)
ah::dumpBuf(p.packet, p.len);
- return false;
+ } else {
+ mLastIv->mGotFragment = true;
+ mBufCtrl.push(p);
+
+ if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
+ isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received
+
+ if(IV_MI == mLastIv->ivGen) {
+ if (p.packet[0] == (0x0f + ALL_FRAMES)) // response from MI get information command
+ isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received
+ else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore MI status messages //#0 was p.packet[0] != 0x00 &&
+ isLastPackage = true; // response from dev control command
+ }
+ rx_ready = true; //reset in case we first read messages from other inverter or ACK zero payloads
}
- mLastIv->mGotFragment = true;
- mBufCtrl.push(p);
- if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
- isLastPackage = (p.packet[9] > ALL_FRAMES); // > ALL_FRAMES indicates last packet received
- else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command
- isLastPackage = (p.packet[9] > 0x10); // > 0x10 indicates last packet received
- else if ((p.packet[0] != 0x88) && (p.packet[0] != 0x92)) // ignore MI status messages //#0 was p.packet[0] != 0x00 &&
- isLastPackage = true; // response from dev control command
}
}
yield();
@@ -312,6 +346,12 @@ class HmRadio : public Radio {
mTxChIdx = iv->heuristics.txRfChId;
if(*mSerialDebug) {
+ if(!isRetransmit) {
+ DPRINT(DBG_INFO, "last tx setup: ");
+ DBGPRINT(String(mTxSetupTime));
+ DBGPRINTLN("ms");
+ }
+
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("TX "));
DBGPRINT(String(len));
@@ -340,6 +380,7 @@ class HmRadio : public Radio {
mLastIv = iv;
iv->mDtuTxCnt++;
+ mNRFisInRX = false;
}
uint64_t getIvId(Inverter<> *iv) {
@@ -362,8 +403,18 @@ class HmRadio : public Radio {
uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz
uint8_t mTxChIdx = 0;
uint8_t mRxChIdx = 0;
+ uint8_t tempRxChIdx = mRxChIdx;
bool mGotLastMsg = false;
uint32_t mMillis;
+ bool tx_ok, tx_fail, rx_ready = false;
+ unsigned long mTimeslotStart = 0;
+ unsigned long mLastIrqTime = 0;
+ bool mNRFloopChannels = false;
+ bool mNRFisInRX = false;
+ bool isRxInit = true;
+ bool rxPendular = false;
+ uint32_t innerLoopTimeout = DURATION_LISTEN_MIN;
+ uint8_t mTxSetupTime = 0;
std::unique_ptr mSpi;
std::unique_ptr mNrf24;
diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h
index b86f8d08..8229378e 100644
--- a/src/hm/hmSystem.h
+++ b/src/hm/hmSystem.h
@@ -1,11 +1,12 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://github.com/lumpapu/ahoy
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+// 2024 Ahoy, https://ahoydtu.de
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __HM_SYSTEM_H__
#define __HM_SYSTEM_H__
+#include "../appInterface.h"
#include "hmInverter.h"
#include
@@ -14,9 +15,10 @@ class HmSystem {
public:
HmSystem() {}
- void setup(uint32_t *timestamp, cfgInst_t *config) {
- mInverter[0].timestamp = timestamp;
+ void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) {
+ mInverter[0].timestamp = timestamp;
mInverter[0].generalConfig = config;
+ //mInverter[0].app = app;
}
void addInverter(uint8_t id, std::function *iv)> cb) {
@@ -49,15 +51,21 @@ class HmSystem {
}
if(iv->config->serial.b[5] == 0x11) {
- if((iv->config->serial.b[4] & 0x0f) == 0x04)
+ if((iv->config->serial.b[4] & 0x0f) == 0x04) {
iv->ivGen = IV_HMS;
- else
+ iv->ivRadioType = INV_RADIO_TYPE_CMT;
+ } else {
iv->ivGen = IV_HM;
+ iv->ivRadioType = INV_RADIO_TYPE_NRF;
+ }
}
- else if((iv->config->serial.b[4] & 0x03) == 0x02) // MI 3rd Gen -> same as HM
+ else if((iv->config->serial.b[4] & 0x03) == 0x02) { // MI 3rd Gen -> same as HM
iv->ivGen = IV_HM;
- else // MI 2nd Gen
+ iv->ivRadioType = INV_RADIO_TYPE_NRF;
+ } else { // MI 2nd Gen
iv->ivGen = IV_MI;
+ iv->ivRadioType = INV_RADIO_TYPE_NRF;
+ }
} else if(iv->config->serial.b[5] == 0x13) {
iv->ivGen = IV_HMT;
iv->type = INV_TYPE_6CH;
@@ -85,7 +93,7 @@ class HmSystem {
DBGPRINTLN(String(iv->config->serial.u64, HEX));
if((iv->config->serial.b[5] == 0x10) && ((iv->config->serial.b[4] & 0x03) == 0x01))
- DPRINTLN(DBG_WARN, F("MI Inverter are not fully supported now!!!"));
+ DPRINTLN(DBG_WARN, F("MI Inverter, has some restrictions!"));
cb(iv);
}
diff --git a/src/hm/nrfHal.h b/src/hm/nrfHal.h
index c9fbcdc7..0532a524 100644
--- a/src/hm/nrfHal.h
+++ b/src/hm/nrfHal.h
@@ -1,6 +1,6 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
-// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __NRF_HAL_H__
@@ -144,6 +144,8 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t len) override {
uint8_t data[NRF_MAX_TRANSFER_SZ];
data[0] = cmd;
+ if(len > NRF_MAX_TRANSFER_SZ)
+ len = NRF_MAX_TRANSFER_SZ;
memset(&data[1], 0xff, len);
request_spi();
@@ -168,13 +170,16 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
}
uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t data_len, uint8_t blank_len) override {
- uint8_t data[NRF_MAX_TRANSFER_SZ];
+ uint8_t data[NRF_MAX_TRANSFER_SZ + 1];
+ uint8_t len = data_len + blank_len;
data[0] = cmd;
- memset(&data[1], 0xff, (data_len + blank_len));
+ if(len > (NRF_MAX_TRANSFER_SZ + 1))
+ len = (NRF_MAX_TRANSFER_SZ + 1);
+ memset(&data[1], 0xff, len);
request_spi();
- size_t spiLen = (static_cast(data_len) + static_cast(blank_len) + 1u) << 3;
+ size_t spiLen = (static_cast(len) + 1u) << 3;
spi_transaction_t t = {
.flags = 0,
.cmd = 0,
diff --git a/src/hm/radio.h b/src/hm/radio.h
index 2fe4f640..09bd9134 100644
--- a/src/hm/radio.h
+++ b/src/hm/radio.h
@@ -13,6 +13,9 @@
#include "../utils/dbg.h"
#include "../utils/crc.h"
+#include "../utils/timemonitor.h"
+
+enum { IRQ_UNKNOWN = 0, IRQ_OK, IRQ_ERROR };
// forward declaration of class
template
@@ -25,11 +28,11 @@ class Radio {
virtual bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) { return true; }
virtual bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) { return true; }
virtual bool isChipConnected(void) { return false; }
-
- virtual void loop(void) {};
+ virtual bool loop(void) = 0;
void handleIntr(void) {
mIrqRcvd = true;
+ mIrqOk = IRQ_OK;
}
void sendCmdPacket(Inverter<> *iv, uint8_t mid, uint8_t pid, bool isRetransmit, bool appendCrc16=true) {
@@ -39,8 +42,10 @@ class Radio {
void prepareDevInformCmd(Inverter<> *iv, uint8_t cmd, uint32_t ts, uint16_t alarmMesId, bool isRetransmit, uint8_t reqfld=TX_REQ_INFO) { // might not be necessary to add additional arg.
if(IV_MI == getIvGen(iv)) {
- DPRINT(DBG_DEBUG, F("legacy cmd 0x"));
- DPRINTLN(DBG_DEBUG,String(cmd, HEX));
+ if(*mSerialDebug) {
+ DPRINT(DBG_DEBUG, F("legacy cmd 0x"));
+ DPRINTLN(DBG_DEBUG,String(cmd, HEX));
+ }
sendCmdPacket(iv, cmd, cmd, false, false);
return;
}
@@ -63,8 +68,14 @@ class Radio {
return mDtuSn;
}
+ void setExpectedFrames(uint8_t framesExpected) {
+ mFramesExpected = framesExpected;
+ }
+
public:
std::queue mBufCtrl;
+ uint8_t mIrqOk = IRQ_UNKNOWN;
+ TimeMonitor mRadioWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state)
protected:
virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0;
@@ -77,6 +88,8 @@ class Radio {
CP_U32_LittleEndian(&mTxBuf[5], mDtuSn);
mTxBuf[9] = pid;
memset(&mTxBuf[10], 0x00, (MAX_RF_PAYLOAD_SIZE-10));
+ if(IRQ_UNKNOWN == mIrqOk)
+ mIrqOk = IRQ_ERROR;
}
void updateCrcs(uint8_t *len, bool appendCrc16=true) {
@@ -95,23 +108,26 @@ class Radio {
void generateDtuSn(void) {
uint32_t chipID = 0;
#ifdef ESP32
- uint64_t MAC = ESP.getEfuseMac();
- chipID = ((MAC >> 8) & 0xFF0000) | ((MAC >> 24) & 0xFF00) | ((MAC >> 40) & 0xFF);
+ chipID = (ESP.getEfuseMac() & 0xffffffff);
#else
chipID = ESP.getChipId();
#endif
- mDtuSn = 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++) {
- mDtuSn |= (chipID % 10) << (i * 4);
- chipID /= 10;
+
+ uint8_t t;
+ for(int i = 0; i < (7 << 2); i += 4) {
+ t = (chipID >> i) & 0x0f;
+ if(t > 0x09)
+ t -= 6;
+ mDtuSn |= (t << i);
}
- }
+ mDtuSn |= 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
+ }
uint32_t mDtuSn;
volatile bool mIrqRcvd;
bool *mSerialDebug, *mPrivacyMode, *mPrintWholeTrace;
uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
-
+ uint8_t mFramesExpected = 0x0c;
};
#endif /*__RADIO_H__*/
diff --git a/src/hm/simulator.h b/src/hm/simulator.h
new file mode 100644
index 00000000..4e06062e
--- /dev/null
+++ b/src/hm/simulator.h
@@ -0,0 +1,175 @@
+//-----------------------------------------------------------------------------
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
+//-----------------------------------------------------------------------------
+
+#ifndef __SIMULATOR_H__
+#define __SIMULATOR_H__
+
+#if defined(ENABLE_SIMULATOR)
+
+#include "../defines.h"
+#include "../utils/dbg.h"
+#include "../utils/helper.h"
+#include "hmSystem.h"
+#include "hmInverter.h"
+#include "Communication.h"
+
+template
+class Simulator {
+ public:
+ void setup(HMSYSTEM *sys, uint32_t *ts, uint8_t ivId = 0) {
+ mTimestamp = ts;
+ mSys = sys;
+ mIvId = ivId;
+ }
+
+ void addPayloadListener(payloadListenerType cb) {
+ mCbPayload = cb;
+ }
+
+ void tick() {
+ uint8_t cmd, len;
+ uint8_t *payload;
+ getPayload(&cmd, &payload, &len);
+
+ Inverter<> *iv = mSys->getInverterByPos(mIvId);
+ if (NULL == iv)
+ return;
+
+ DPRINT(DBG_INFO, F("add payload with cmd: 0x"));
+ DBGHEXLN(cmd);
+
+ if(GridOnProFilePara == cmd) {
+ iv->addGridProfile(payload, len);
+ return;
+ }
+
+ record_t<> *rec = iv->getRecordStruct(cmd);
+ rec->ts = *mTimestamp;
+ for (uint8_t i = 0; i < rec->length; i++) {
+ iv->addValue(i, payload, rec);
+ yield();
+ }
+ iv->doCalculations();
+
+ if((nullptr != mCbPayload) && (GridOnProFilePara != cmd))
+ (mCbPayload)(cmd, iv);
+ }
+
+ private:
+ inline void getPayload(uint8_t *cmd, uint8_t *payload[], uint8_t *len) {
+ switch(payloadCtrl) {
+ default: *cmd = RealTimeRunData_Debug; break;
+ case 1: *cmd = SystemConfigPara; break;
+ case 3: *cmd = InverterDevInform_All; break;
+ case 5: *cmd = InverterDevInform_Simple; break;
+ case 7: *cmd = GridOnProFilePara; break;
+ }
+
+ if(payloadCtrl < 8)
+ payloadCtrl++;
+
+ switch(*cmd) {
+ default:
+ case RealTimeRunData_Debug:
+ *payload = plRealtime;
+ modifyAcPwr();
+ *len = 62;
+ break;
+ case InverterDevInform_All:
+ *payload = plFirmware;
+ *len = 14;
+ break;
+ case InverterDevInform_Simple:
+ *payload = plPart;
+ *len = 14;
+ break;
+ case SystemConfigPara:
+ *payload = plLimit;
+ *len = 14;
+ break;
+ case AlarmData:
+ *payload = plAlarm;
+ *len = 26;
+ break;
+ case GridOnProFilePara:
+ *payload = plGrid;
+ *len = 70;
+ break;
+ }
+ }
+
+ inline void modifyAcPwr() {
+ uint16_t cur = (plRealtime[50] << 8) | plRealtime[51];
+ uint16_t change = cur ^ 0xa332;
+ if(0 == change)
+ change = 140;
+ else if(change > 200)
+ change = (change % 200) + 1;
+
+ if(cur > 7000)
+ cur -= change;
+ else
+ cur += change;
+
+ plRealtime[50] = (cur >> 8) & 0xff;
+ plRealtime[51] = (cur ) & 0xff;
+ }
+
+ private:
+ HMSYSTEM *mSys;
+ uint8_t mIvId;
+ uint32_t *mTimestamp;
+ payloadListenerType mCbPayload = nullptr;
+ uint8_t payloadCtrl = 0;
+
+ private:
+ uint8_t plRealtime[62] = {
+ 0x00, 0x01, 0x01, 0x24, 0x00, 0x22, 0x00, 0x23,
+ 0x00, 0x63, 0x00, 0x65, 0x00, 0x08, 0x5c, 0xbb,
+ 0x00, 0x09, 0x6f, 0x08, 0x00, 0x0c, 0x00, 0x0c,
+ 0x01, 0x1e, 0x00, 0x22, 0x00, 0x21, 0x00, 0x60,
+ 0x00, 0x5f, 0x00, 0x08, 0xdd, 0x84, 0x00, 0x09,
+ 0x13, 0x6f, 0x00, 0x0b, 0x00, 0x0b, 0x09, 0x27,
+ 0x13, 0x8c, 0x01, 0x75, 0x00, 0xc2, 0x00, 0x10,
+ 0x03, 0x77, 0x00, 0x61, 0x00, 0x02
+ };
+
+ uint8_t plPart[14] = {
+ 0x27, 0x1c, 0x10, 0x12, 0x10, 0x01, 0x01, 0x00,
+ 0x0a, 0x00, 0x20, 0x01, 0x00, 0x00
+ };
+
+ uint8_t plFirmware[14] = {
+ 0x00, 0x01, 0x80, 0x01, 0x00, 0x01, 0x60, 0x42,
+ 0x60, 0x42, 0x00, 0x00, 0x00, 0x00
+ };
+
+ uint8_t plLimit[14] = {
+ 0x00, 0x01, 0x03, 0xe8, 0x00, 0x00, 0x03, 0xe8,
+ 0xff, 0xff, 0xff, 0xff, 0x01, 0x68
+ };
+
+ uint8_t plGrid[70] = {
+ 0x0D, 0x00, 0x20, 0x00, 0x00, 0x08, 0x08, 0xFC,
+ 0x07, 0x30, 0x00, 0x01, 0x0A, 0x55, 0x00, 0x01,
+ 0x09, 0xE2, 0x10, 0x00, 0x13, 0x88, 0x12, 0x8E,
+ 0x00, 0x01, 0x14, 0x1E, 0x00, 0x01, 0x20, 0x00,
+ 0x00, 0x01, 0x30, 0x07, 0x01, 0x2C, 0x0A, 0x55,
+ 0x07, 0x30, 0x14, 0x1E, 0x12, 0x8E, 0x00, 0x32,
+ 0x00, 0x1E, 0x40, 0x00, 0x07, 0xD0, 0x00, 0x10,
+ 0x50, 0x00, 0x00, 0x01, 0x13, 0x9C, 0x01, 0x90,
+ 0x00, 0x10, 0x70, 0x00, 0x00, 0x01
+ };
+
+ uint8_t plAlarm[26] = {
+ 0x00, 0x01, 0x80, 0x01, 0x00, 0x01, 0x51, 0xc7,
+ 0x51, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x80, 0x02,
+ 0x00, 0x02, 0xa6, 0xc9, 0xa6, 0xc9, 0x65, 0x3e,
+ 0x47, 0x21
+ };
+};
+
+#endif /*ENABLE_SIMULATOR*/
+#endif /*__SIMULATOR_H__*/
diff --git a/src/hms/cmtHal.h b/src/hms/cmtHal.h
index d5e31ea0..768b8da5 100644
--- a/src/hms/cmtHal.h
+++ b/src/hms/cmtHal.h
@@ -17,7 +17,7 @@
class cmtHal : public SpiPatcherHandle {
public:
cmtHal() {
- mSpiPatcher = SpiPatcher::getInstance(SPI2_HOST);
+ mSpiPatcher = SpiPatcher::getInstance(DEF_CMT_SPI_HOST);
}
void patch() override {
diff --git a/src/hms/esp32_3wSpi.h b/src/hms/esp32_3wSpi.h
index 20c42632..c562671a 100644
--- a/src/hms/esp32_3wSpi.h
+++ b/src/hms/esp32_3wSpi.h
@@ -21,7 +21,7 @@
// for ESP32 this is the so-called HSPI
// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore,
// it is simply the first externally usable hardware SPI master controller
-#define SPI_CMT SPI2_HOST
+//#define SPI_CMT SPI2_HOST
class esp32_3wSpi {
public:
@@ -54,8 +54,8 @@ class esp32_3wSpi {
.post_cb = NULL,
};
- ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED));
- ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg));
+ ESP_ERROR_CHECK(spi_bus_initialize(DEF_CMT_SPI_HOST, &buscfg, SPI_DMA_DISABLED));
+ ESP_ERROR_CHECK(spi_bus_add_device(DEF_CMT_SPI_HOST, &devcfg, &spi_reg));
// FiFo
spi_device_interface_config_t devcfg2 = {
@@ -72,9 +72,9 @@ class esp32_3wSpi {
.pre_cb = NULL,
.post_cb = NULL,
};
- ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo));
+ ESP_ERROR_CHECK(spi_bus_add_device(DEF_CMT_SPI_HOST, &devcfg2, &spi_fifo));
- esp_rom_gpio_connect_out_signal(pinSdio, spi_periph_signal[SPI_CMT].spid_out, true, false);
+ esp_rom_gpio_connect_out_signal(pinSdio, spi_periph_signal[DEF_CMT_SPI_HOST].spid_out, true, false);
delay(100);
//pinMode(pinGpio3, INPUT);
diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h
index d2779012..48ff3750 100644
--- a/src/hms/hmsRadio.h
+++ b/src/hms/hmsRadio.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://github.com/lumpapu/ahoy
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -26,15 +26,16 @@ class CmtRadio : public Radio {
mPrintWholeTrace = printWholeTrace;
}
- void loop() {
+ bool loop() {
mCmt.loop();
if((!mIrqRcvd) && (!mRqstGetRx))
- return;
+ return false;
getRx();
if(CMT_SUCCESS == mCmt.goRx()) {
mIrqRcvd = false;
mRqstGetRx = false;
}
+ return false;
}
bool isChipConnected(void) {
@@ -50,10 +51,10 @@ class CmtRadio : public Radio {
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[cnt++] = 0x00;
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
- mTxBuf[cnt++] = ((data[0] * 10) >> 8) & 0xff; // power limit
- mTxBuf[cnt++] = ((data[0] * 10) ) & 0xff; // power limit
- mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
- mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
+ mTxBuf[cnt++] = (data[0] >> 8) & 0xff; // power limit, multiplied by 10 (because of fraction)
+ mTxBuf[cnt++] = (data[0] ) & 0xff; // power limit
+ mTxBuf[cnt++] = (data[1] >> 8) & 0xff; // setting for persistens handlings
+ mTxBuf[cnt++] = (data[1] ) & 0xff; // setting for persistens handling
}
sendPacket(iv, cnt, isRetransmit);
@@ -134,8 +135,8 @@ class CmtRadio : public Radio {
mCmt.goRx();
}
- mIrqRcvd = false;
- mRqstGetRx = false;
+ mIrqRcvd = false;
+ mRqstGetRx = false;
}
inline void sendSwitchChCmd(Inverter<> *iv, uint8_t ch) {
@@ -163,11 +164,16 @@ class CmtRadio : public Radio {
uint8_t status = mCmt.getRx(p.packet, &p.len, 28, &p.rssi);
if(CMT_SUCCESS == status)
mBufCtrl.push(p);
+
+ // this code completly stops communication!
+ //if(p.packet[9] > ALL_FRAMES) // indicates last frame
+ // mRadioWaitTime.stopTimeMonitor(); // we got everything we expected and can exit rx mode...
+ //optionally instead: mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first get back to rx mode?
}
CmtType mCmt;
- bool mRqstGetRx;
bool mCmtAvail;
+ bool mRqstGetRx = false;
uint32_t mMillis;
};
diff --git a/src/platformio.ini b/src/platformio.ini
index e58c6804..d45472ba 100644
--- a/src/platformio.ini
+++ b/src/platformio.ini
@@ -22,6 +22,7 @@ extra_scripts =
pre:../scripts/auto_firmware_version.py
pre:../scripts/convertHtml.py
pre:../scripts/applyPatches.py
+ pre:../scripts/reduceGxEPD2.py
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
@@ -30,8 +31,8 @@ lib_deps =
https://github.com/bertmelis/espMqttClient#v1.5.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
- olikraus/U8g2 @ ^2.35.7
- https://github.com/zinggjm/GxEPD2 @ ^1.5.2
+ olikraus/U8g2 @ ^2.35.9
+ https://github.com/zinggjm/GxEPD2#1.5.3
build_flags =
-std=c++17
-std=gnu++17
@@ -45,21 +46,66 @@ board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
;-Wl,-Map,output.map
monitor_filters =
esp8266_exception_decoder
+[env:esp8266-de]
+platform = espressif8266
+board = esp12e
+board_build.f_cpu = 80000000L
+build_flags = ${env.build_flags}
+ -DEMC_MIN_FREE_MEMORY=4096
+ -DLANG_DE
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+ ;-Wl,-Map,output.map
+monitor_filters =
+ esp8266_exception_decoder
[env:esp8266-prometheus]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
+ -DEMC_MIN_FREE_MEMORY=4096
-DENABLE_PROMETHEUS_EP
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+monitor_filters =
+ esp8266_exception_decoder
+
+[env:esp8266-prometheus-de]
+platform = espressif8266
+board = esp12e
+board_build.f_cpu = 80000000L
+build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
+ -DENABLE_PROMETHEUS_EP
+ -DLANG_DE
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
+[env:esp8266-minimal]
+platform = espressif8266
+board = esp12e
+board_build.f_cpu = 80000000L
+build_flags = ${env.build_flags}
+ -DEMC_MIN_FREE_MEMORY=4096
+ ;-Wl,-Map,output.map
+monitor_filters =
+ esp8266_exception_decoder
+
+
+
[env:esp8285]
platform = espressif8266
board = esp8285
@@ -67,23 +113,79 @@ board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L
build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+monitor_filters =
+ esp8266_exception_decoder
+
+[env:esp8285-de]
+platform = espressif8266
+board = esp8285
+board_build.ldscript = eagle.flash.1m64.ld
+board_build.f_cpu = 80000000L
+build_flags = ${env.build_flags}
+ -DEMC_MIN_FREE_MEMORY=4096
+ -DLANG_DE
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
monitor_filters =
esp8266_exception_decoder
[env:esp32-wroom32]
-platform = espressif32@6.4.0
+platform = espressif32@6.5.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+monitor_filters =
+ esp32_exception_decoder
+
+[env:esp32-wroom32-minimal]
+platform = espressif32@6.5.0
+board = lolin_d32
+build_flags = ${env.build_flags}
+ -DUSE_HSPI_FOR_EPD
+monitor_filters =
+ esp32_exception_decoder
+
+[env:esp32-wroom32-de]
+platform = espressif32@6.5.0
+board = lolin_d32
+build_flags = ${env.build_flags}
+ -DUSE_HSPI_FOR_EPD
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+ -DLANG_DE
monitor_filters =
esp32_exception_decoder
[env:esp32-wroom32-prometheus]
-platform = espressif32@6.4.0
+platform = espressif32@6.5.0
+board = lolin_d32
+build_flags = ${env.build_flags}
+ -DUSE_HSPI_FOR_EPD
+ -DENABLE_PROMETHEUS_EP
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+monitor_filters =
+ esp32_exception_decoder
+
+[env:esp32-wroom32-prometheus-de]
+platform = espressif32@6.5.0
board = lolin_d32
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
+ -DLANG_DE
-DENABLE_PROMETHEUS_EP
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
@@ -94,42 +196,172 @@ build_flags = ${env.build_flags}
-D ETHERNET
-DRELEASE
-DUSE_HSPI_FOR_EPD
- -DLOG_LOCAL_LEVEL=ESP_LOG_INFO
- -DDEBUG_LEVEL=DBG_INFO
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+monitor_filters =
+ esp32_exception_decoder
+
+[env:esp32-wroom32-ethernet-de]
+platform = espressif32
+board = esp32dev
+build_flags = ${env.build_flags}
+ -D ETHERNET
+ -DRELEASE
+ -DUSE_HSPI_FOR_EPD
+ -DLANG_DE
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
monitor_filters =
esp32_exception_decoder
[env:esp32-s2-mini]
-platform = espressif32@6.4.0
+platform = espressif32@6.5.0
board = lolin_s2_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
-DDEF_NRF_CS_PIN=12
-DDEF_NRF_CE_PIN=3
-DDEF_NRF_IRQ_PIN=5
-DDEF_NRF_MISO_PIN=9
-DDEF_NRF_MOSI_PIN=11
-DDEF_NRF_SCLK_PIN=7
+ -DDEF_CMT_CSB=16
+ -DDEF_CMT_FCSB=18
+ -DDEF_CMT_IRQ=33
+ -DDEF_CMT_SDIO=35
+ -DDEF_CMT_SCLK=37
+monitor_filters =
+ esp32_exception_decoder
+
+[env:esp32-s2-mini-de]
+platform = espressif32@6.5.0
+board = lolin_s2_mini
+build_flags = ${env.build_flags}
+ -DUSE_HSPI_FOR_EPD
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+ -DDEF_NRF_CS_PIN=12
+ -DDEF_NRF_CE_PIN=3
+ -DDEF_NRF_IRQ_PIN=5
+ -DDEF_NRF_MISO_PIN=9
+ -DDEF_NRF_MOSI_PIN=11
+ -DDEF_NRF_SCLK_PIN=7
+ -DDEF_CMT_CSB=16
+ -DDEF_CMT_FCSB=18
+ -DDEF_CMT_IRQ=33
+ -DDEF_CMT_SDIO=35
+ -DDEF_CMT_SCLK=37
+ -DLANG_DE
monitor_filters =
esp32_exception_decoder
[env:esp32-c3-mini]
-platform = espressif32@6.4.0
+platform = espressif32@6.5.0
board = lolin_c3_mini
build_flags = ${env.build_flags}
-DUSE_HSPI_FOR_EPD
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
-DDEF_NRF_CS_PIN=5
-DDEF_NRF_CE_PIN=0
-DDEF_NRF_IRQ_PIN=1
-DDEF_NRF_MISO_PIN=3
-DDEF_NRF_MOSI_PIN=4
-DDEF_NRF_SCLK_PIN=2
+ -DDEF_CMT_CSB=255
+ -DDEF_CMT_FCSB=255
+ -DDEF_CMT_IRQ=255
+ -DDEF_CMT_SDIO=255
+ -DDEF_CMT_SCLK=255
monitor_filters =
esp32_exception_decoder
+[env:esp32-c3-mini-de]
+platform = espressif32@6.5.0
+board = lolin_c3_mini
+build_flags = ${env.build_flags}
+ -DUSE_HSPI_FOR_EPD
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+ -DDEF_NRF_CS_PIN=5
+ -DDEF_NRF_CE_PIN=0
+ -DDEF_NRF_IRQ_PIN=1
+ -DDEF_NRF_MISO_PIN=3
+ -DDEF_NRF_MOSI_PIN=4
+ -DDEF_NRF_SCLK_PIN=2
+ -DDEF_CMT_CSB=255
+ -DDEF_CMT_FCSB=255
+ -DDEF_CMT_IRQ=255
+ -DDEF_CMT_SDIO=255
+ -DDEF_CMT_SCLK=255
+ -DLANG_DE
+monitor_filters =
+ esp32_exception_decoder
[env:opendtufusion]
-platform = espressif32@6.4.0
+platform = espressif32@6.5.0
+board = esp32-s3-devkitc-1
+upload_protocol = esp-builtin
+build_flags = ${env.build_flags}
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+ -DDEF_NRF_CS_PIN=37
+ -DDEF_NRF_CE_PIN=38
+ -DDEF_NRF_IRQ_PIN=47
+ -DDEF_NRF_MISO_PIN=48
+ -DDEF_NRF_MOSI_PIN=35
+ -DDEF_NRF_SCLK_PIN=36
+ -DDEF_CMT_CSB=4
+ -DDEF_CMT_FCSB=21
+ -DDEF_CMT_IRQ=8
+ -DDEF_CMT_SDIO=5
+ -DDEF_CMT_SCLK=6
+ -DDEF_LED0=18
+ -DDEF_LED1=17
+ -DLED_ACTIVE_HIGH
+ -DARDUINO_USB_MODE=1
+ #-DARDUINO_USB_CDC_ON_BOOT=1
+monitor_filters =
+ esp32_exception_decoder, colorize
+
+[env:opendtufusion-de]
+platform = espressif32@6.5.0
+board = esp32-s3-devkitc-1
+upload_protocol = esp-builtin
+build_flags = ${env.build_flags}
+ -DLANG_DE
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+ -DDEF_NRF_CS_PIN=37
+ -DDEF_NRF_CE_PIN=38
+ -DDEF_NRF_IRQ_PIN=47
+ -DDEF_NRF_MISO_PIN=48
+ -DDEF_NRF_MOSI_PIN=35
+ -DDEF_NRF_SCLK_PIN=36
+ -DDEF_CMT_CSB=4
+ -DDEF_CMT_FCSB=21
+ -DDEF_CMT_IRQ=8
+ -DDEF_CMT_SDIO=5
+ -DDEF_CMT_SCLK=6
+ -DDEF_LED0=18
+ -DDEF_LED1=17
+ -DLED_ACTIVE_HIGH
+ -DARDUINO_USB_MODE=1
+monitor_filters =
+ esp32_exception_decoder, colorize
+
+[env:opendtufusion-minimal]
+platform = espressif32@6.5.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
@@ -152,7 +384,7 @@ monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-ethernet]
-platform = espressif32@6.4.0
+platform = espressif32@6.5.0
board = esp32-s3-devkitc-1
lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500
@@ -162,13 +394,15 @@ lib_deps =
https://github.com/bertmelis/espMqttClient#v1.5.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
- olikraus/U8g2 @ ^2.35.7
- zinggjm/GxEPD2 @ ^1.5.2
+ olikraus/U8g2 @ ^2.35.9
+ https://github.com/zinggjm/GxEPD2#1.5.3
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
-DETHERNET
-DSPI_HAL
- -DUSE_HSPI_FOR_EPD
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
-DDEF_ETH_CS_PIN=42
-DDEF_ETH_SCK_PIN=39
-DDEF_ETH_MISO_PIN=41
@@ -194,20 +428,33 @@ build_flags = ${env.build_flags}
monitor_filters =
esp32_exception_decoder, colorize
-[env:opendtufusion-dev]
-platform = espressif32@6.4.0
+[env:opendtufusion-ethernet-de]
+platform = espressif32@6.5.0
board = esp32-s3-devkitc-1
lib_deps =
- https://github.com/yubox-node-org/ESPAsyncWebServer
+ khoih-prog/AsyncWebServer_ESP32_W5500
+ khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.5.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
- olikraus/U8g2 @ ^2.35.7
- https://github.com/zinggjm/GxEPD2 @ ^1.5.2
+ olikraus/U8g2 @ ^2.35.9
+ https://github.com/zinggjm/GxEPD2#1.5.3
upload_protocol = esp-builtin
build_flags = ${env.build_flags}
+ -DETHERNET
+ -DSPI_HAL
+ -DLANG_DE
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+ -DDEF_ETH_CS_PIN=42
+ -DDEF_ETH_SCK_PIN=39
+ -DDEF_ETH_MISO_PIN=41
+ -DDEF_ETH_MOSI_PIN=40
+ -DDEF_ETH_IRQ_PIN=44
+ -DDEF_ETH_RST_PIN=43
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
@@ -223,7 +470,6 @@ build_flags = ${env.build_flags}
-DDEF_LED1=17
-DLED_ACTIVE_HIGH
-DARDUINO_USB_MODE=1
- -DARDUINO_USB_CDC_ON_BOOT=1
- -DSPI_HAL
+ #-DARDUINO_USB_CDC_ON_BOOT=1
monitor_filters =
esp32_exception_decoder, colorize
diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h
index 0cfbd710..862e452b 100644
--- a/src/plugins/Display/Display.h
+++ b/src/plugins/Display/Display.h
@@ -1,6 +1,8 @@
#ifndef __DISPLAY__
#define __DISPLAY__
+#if defined(PLUGIN_DISPLAY)
+
#include
#include
@@ -17,221 +19,228 @@
template
class Display {
- public:
- Display() {
- mMono = NULL;
- }
-
- void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *hmradio, RADIO *hmsradio, uint32_t *utcTs) {
- mApp = app;
- mHmRadio = hmradio;
- mHmsRadio = hmsradio;
- mCfg = cfg;
- mSys = sys;
- mUtcTs = utcTs;
- mNewPayload = false;
- mLoopCnt = 0;
-
- mDisplayData.version = app->getVersion(); // version never changes, so only set once
-
- switch (mCfg->type) {
- case 0: mMono = NULL; break; // None
- case 1: mMono = new DisplayMono128X64(); break; // SSD1306_128X64 (0.96", 1.54")
- case 2: mMono = new DisplayMono128X64(); break; // SH1106_128X64 (1.3")
- case 3: mMono = new DisplayMono84X48(); break; // PCD8544_84X48 (1.6" - Nokia 5110)
- case 4: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91")
- case 5: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield)
- case 6: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42")
-#if defined(ESP32) && !defined(ETHERNET)
- case 10:
- mMono = NULL; // ePaper does not use this
- mRefreshCycle = 0;
- mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline);
- mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mDisplayData.version);
- break;
-#endif
-
- default: mMono = NULL; break;
- }
- if(mMono) {
- mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->screenSaver, mCfg->contrast);
- mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, &mDisplayData);
+ public:
+ Display() {
+ mMono = NULL;
}
- // setup PIR pin for motion sensor
-#ifdef ESP32
- if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF))
- pinMode(mCfg->pirPin, INPUT);
-#endif
-#ifdef ESP8266
- if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF) && (mCfg->pirPin != A0))
- pinMode(mCfg->pirPin, INPUT);
-#endif
+ void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *hmradio, RADIO *hmsradio, uint32_t *utcTs) {
+ mApp = app;
+ mHmRadio = hmradio;
+ mHmsRadio = hmsradio;
+ mCfg = cfg;
+ mSys = sys;
+ mUtcTs = utcTs;
+ mNewPayload = false;
+ mLoopCnt = 0;
- }
+ mDisplayData.version = app->getVersion(); // version never changes, so only set once
+
+ switch (mCfg->type) {
+ case DISP_TYPE_T0_NONE: mMono = NULL; break; // None
+ case DISP_TYPE_T1_SSD1306_128X64: mMono = new DisplayMono128X64(); break; // SSD1306_128X64 (0.96", 1.54")
+ case DISP_TYPE_T2_SH1106_128X64: mMono = new DisplayMono128X64(); break; // SH1106_128X64 (1.3")
+ case DISP_TYPE_T3_PCD8544_84X48: mMono = new DisplayMono84X48(); break; // PCD8544_84X48 (1.6" - Nokia 5110)
+ case DISP_TYPE_T4_SSD1306_128X32: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91")
+ case DISP_TYPE_T5_SSD1306_64X48: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield)
+ case DISP_TYPE_T6_SSD1309_128X64: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42")
+ #if defined(ESP32) && !defined(ETHERNET)
+ case DISP_TYPE_T10_EPAPER:
+ mMono = NULL; // ePaper does not use this
+ mRefreshCycle = 0;
+ mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline);
+ mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mDisplayData.version);
+ break;
+ #endif
+ default: mMono = NULL; break;
+ }
+ if(mMono) {
+ mMono->config(mCfg);
+ mMono->init(&mDisplayData);
+ }
- void payloadEventListener(uint8_t cmd) {
- mNewPayload = true;
- }
+ // setup PIR pin for motion sensor
+ #ifdef ESP32
+ if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF))
+ pinMode(mCfg->pirPin, INPUT);
+ #endif
+ #ifdef ESP8266
+ if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF) && (mCfg->pirPin != A0))
+ pinMode(mCfg->pirPin, INPUT);
+ #endif
- void tickerSecond() {
- if (mMono != NULL)
- mMono->loop(mCfg->contrast, motionSensorActive());
+ }
- if (mNewPayload || (((++mLoopCnt) % 5) == 0)) {
- DataScreen();
- mNewPayload = false;
- mLoopCnt = 0;
+ void payloadEventListener(uint8_t cmd) {
+ mNewPayload = true;
}
- #if defined(ESP32) && !defined(ETHERNET)
- mEpaper.tickerSecond();
- #endif
- }
- private:
- void DataScreen() {
- if (mCfg->type == 0)
- return;
-
- float totalPower = 0.0;
- float totalYieldDay = 0.0;
- float totalYieldTotal = 0.0;
-
- uint8_t nrprod = 0;
- uint8_t nrsleep = 0;
- int8_t minQAllInv = 4;
-
- Inverter<> *iv;
- record_t<> *rec;
- bool allOff = true;
- uint8_t nInv = mSys->getNumInverters();
- for (uint8_t i = 0; i < nInv; i++) {
- iv = mSys->getInverterByPos(i);
- if (iv == NULL)
- continue;
-
- if (iv->isProducing()) // also updates inverter state engine
- nrprod++;
- else
- nrsleep++;
+ void tickerSecond() {
+ bool request_refresh = false;
- rec = iv->getRecordStruct(RealTimeRunData_Debug);
+ if (mMono != NULL)
+ request_refresh = mMono->loop(motionSensorActive());
- if (iv->isAvailable()) { // consider only radio quality of inverters still communicating
- int8_t maxQInv = -6;
- for(uint8_t ch = 0; ch < RF_MAX_CHANNEL_ID; ch++) {
- int8_t q = iv->heuristics.txRfQuality[ch];
- if (q > maxQInv)
- maxQInv = q;
+ if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) {
+ DataScreen();
+ mNewPayload = false;
+ mLoopCnt = 0;
+ }
+ #if defined(ESP32) && !defined(ETHERNET)
+ mEpaper.tickerSecond();
+ #endif
+ }
+
+ private:
+ void DataScreen() {
+ if (DISP_TYPE_T0_NONE == mCfg->type)
+ return;
+
+ float totalPower = 0.0;
+ float totalYieldDay = 0.0;
+ float totalYieldTotal = 0.0;
+
+ uint8_t nrprod = 0;
+ uint8_t nrsleep = 0;
+ int8_t minQAllInv = 4;
+
+ Inverter<> *iv;
+ record_t<> *rec;
+ bool allOff = true;
+ uint8_t nInv = mSys->getNumInverters();
+ for (uint8_t i = 0; i < nInv; i++) {
+ iv = mSys->getInverterByPos(i);
+ if (iv == NULL)
+ continue;
+
+ if (iv->isProducing()) // also updates inverter state engine
+ nrprod++;
+ else
+ nrsleep++;
+
+ rec = iv->getRecordStruct(RealTimeRunData_Debug);
+
+ if (iv->isAvailable()) { // consider only radio quality of inverters still communicating
+ int8_t maxQInv = -6;
+ for(uint8_t ch = 0; ch < RF_MAX_CHANNEL_ID; ch++) {
+ int8_t q = iv->heuristics.txRfQuality[ch];
+ if (q > maxQInv)
+ maxQInv = q;
+ }
+ if (maxQInv < minQAllInv)
+ minQAllInv = maxQInv;
+
+ totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); // add only FLD_PAC from inverters still communicating
+ allOff = false;
}
- if (maxQInv < minQAllInv)
- minQAllInv = maxQInv;
- totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec); // add only FLD_PAC from inverters still communicating
- allOff = false;
+ totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
+ totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
}
- totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
- totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
- }
+ if (allOff)
+ minQAllInv = -6;
+
+ // prepare display data
+ mDisplayData.nrProducing = nrprod;
+ mDisplayData.nrSleeping = nrsleep;
+ mDisplayData.totalPower = totalPower;
+ mDisplayData.totalYieldDay = totalYieldDay;
+ mDisplayData.totalYieldTotal = totalYieldTotal;
+ bool nrf_en = mApp->getNrfEnabled();
+ bool nrf_ok = nrf_en && mHmRadio->isChipConnected();
+ #if defined(ESP32)
+ bool cmt_en = mApp->getCmtEnabled();
+ bool cmt_ok = cmt_en && mHmsRadio->isChipConnected();
+ #else
+ bool cmt_en = false;
+ bool cmt_ok = false;
+ #endif
+ mDisplayData.RadioSymbol = (nrf_ok && !cmt_en) || (cmt_ok && !nrf_en) || (nrf_ok && cmt_ok);
+ mDisplayData.WifiSymbol = (WiFi.status() == WL_CONNECTED);
+ mDisplayData.MQTTSymbol = mApp->getMqttIsConnected();
+ mDisplayData.RadioRSSI = ivQuality2RadioRSSI(minQAllInv); // Workaround as NRF24 has no RSSI. Approximation by quality levels from heuristic function
+ mDisplayData.WifiRSSI = (WiFi.status() == WL_CONNECTED) ? WiFi.RSSI() : SCHAR_MIN;
+ mDisplayData.ipAddress = WiFi.localIP();
+
+ // provide localized times to display classes
+ time_t utc= mApp->getTimestamp();
+ if (year(utc) > 2020)
+ mDisplayData.utcTs = gTimezone.toLocal(utc);
+ else
+ mDisplayData.utcTs = 0;
+ mDisplayData.pGraphStartTime = gTimezone.toLocal(mApp->getSunrise());
+ mDisplayData.pGraphEndTime = gTimezone.toLocal(mApp->getSunset());
- if (allOff)
- minQAllInv = -6;
-
- // prepare display data
- mDisplayData.nrProducing = nrprod;
- mDisplayData.nrSleeping = nrsleep;
- mDisplayData.totalPower = totalPower;
- mDisplayData.totalYieldDay = totalYieldDay;
- mDisplayData.totalYieldTotal = totalYieldTotal;
- bool nrf_en = mApp->getNrfEnabled();
- bool nrf_ok = nrf_en && mHmRadio->isChipConnected();
- #if defined(ESP32)
- bool cmt_en = mApp->getCmtEnabled();
- bool cmt_ok = cmt_en && mHmsRadio->isChipConnected();
- #else
- bool cmt_en = false;
- bool cmt_ok = false;
- #endif
- mDisplayData.RadioSymbol = (nrf_ok && !cmt_en) || (cmt_ok && !nrf_en) || (nrf_ok && cmt_ok);
- mDisplayData.WifiSymbol = (WiFi.status() == WL_CONNECTED);
- mDisplayData.MQTTSymbol = mApp->getMqttIsConnected();
- mDisplayData.RadioRSSI = ivQuality2RadioRSSI(minQAllInv); // Workaround as NRF24 has no RSSI. Approximation by quality levels from heuristic function
- mDisplayData.WifiRSSI = (WiFi.status() == WL_CONNECTED) ? WiFi.RSSI() : SCHAR_MIN;
- mDisplayData.ipAddress = WiFi.localIP();
- time_t utc= mApp->getTimestamp();
- if (year(utc) > 2020)
- mDisplayData.utcTs = utc;
- else
- mDisplayData.utcTs = 0;
-
- if (mMono ) {
- mMono->disp();
- }
-#if defined(ESP32) && !defined(ETHERNET)
- else if (mCfg->type == 10) {
- mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
- mRefreshCycle++;
- }
+ if (mMono ) {
+ mMono->disp();
+ }
+ #if defined(ESP32) && !defined(ETHERNET)
+ else if (DISP_TYPE_T10_EPAPER == mCfg->type) {
+ mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod);
+ mRefreshCycle++;
+ }
- if (mRefreshCycle > 480) {
- mEpaper.fullRefresh();
- mRefreshCycle = 0;
+ if (mRefreshCycle > 480) {
+ mEpaper.fullRefresh();
+ mRefreshCycle = 0;
+ }
+ #endif
}
-#endif
- }
-
- bool motionSensorActive() {
- if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF)) {
-#if defined(ESP8266)
- if (mCfg->pirPin == A0)
- return((analogRead(A0) >= 512));
- else
+
+ bool motionSensorActive() {
+ if ((mCfg->screenSaver == 2) && (mCfg->pirPin != DEF_PIN_OFF)) {
+ #if defined(ESP8266)
+ if (mCfg->pirPin == A0)
+ return((analogRead(A0) >= 512));
+ else
+ return(digitalRead(mCfg->pirPin));
+ #elif defined(ESP32)
return(digitalRead(mCfg->pirPin));
-#elif defined(ESP32)
- return(digitalRead(mCfg->pirPin));
-#endif
+ #endif
+ }
+ else
+ return(false);
}
- else
- return(false);
- }
-
- // approximate RSSI in dB by invQuality levels from heuristic function (very unscientific but better than nothing :-) )
- int8_t ivQuality2RadioRSSI(int8_t invQuality) {
- int8_t pseudoRSSIdB;
- switch(invQuality) {
- case 4: pseudoRSSIdB = -55; break;
- case 3:
- case 2:
- case 1: pseudoRSSIdB = -65; break;
- case 0:
- case -1:
- case -2: pseudoRSSIdB = -75; break;
- case -3:
- case -4:
- case -5: pseudoRSSIdB = -85; break;
- case -6:
- default: pseudoRSSIdB = -95; break;
+
+ // approximate RSSI in dB by invQuality levels from heuristic function (very unscientific but better than nothing :-) )
+ int8_t ivQuality2RadioRSSI(int8_t invQuality) {
+ int8_t pseudoRSSIdB;
+ switch(invQuality) {
+ case 4: pseudoRSSIdB = -55; break;
+ case 3:
+ case 2:
+ case 1: pseudoRSSIdB = -65; break;
+ case 0:
+ case -1:
+ case -2: pseudoRSSIdB = -75; break;
+ case -3:
+ case -4:
+ case -5: pseudoRSSIdB = -85; break;
+ case -6:
+ default: pseudoRSSIdB = -95; break;
+ }
+ return (pseudoRSSIdB);
}
- return (pseudoRSSIdB);
- }
-
- // private member variables
- IApp *mApp;
- DisplayData mDisplayData;
- bool mNewPayload;
- uint8_t mLoopCnt;
- uint32_t *mUtcTs;
- display_t *mCfg;
- HMSYSTEM *mSys;
- RADIO *mHmRadio;
- RADIO *mHmsRadio;
- uint16_t mRefreshCycle;
-
-#if defined(ESP32) && !defined(ETHERNET)
- DisplayEPaper mEpaper;
-#endif
- DisplayMono *mMono;
+
+ // private member variables
+ IApp *mApp;
+ DisplayData mDisplayData;
+ bool mNewPayload;
+ uint8_t mLoopCnt;
+ uint32_t *mUtcTs;
+ display_t *mCfg;
+ HMSYSTEM *mSys;
+ RADIO *mHmRadio;
+ RADIO *mHmsRadio;
+ uint16_t mRefreshCycle;
+
+ #if defined(ESP32) && !defined(ETHERNET)
+ DisplayEPaper mEpaper;
+ #endif
+ DisplayMono *mMono;
};
+#endif /*PLUGIN_DISPLAY*/
+
#endif /*__DISPLAY__*/
diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h
index 3e998b6d..2c1fc935 100644
--- a/src/plugins/Display/Display_Mono.h
+++ b/src/plugins/Display/Display_Mono.h
@@ -20,101 +20,314 @@
#include "Display_data.h"
#include "../../utils/dbg.h"
#include "../../utils/timemonitor.h"
+#include "../../config/settings.h"
class DisplayMono {
- public:
- DisplayMono() {};
-
- virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) = 0;
- virtual void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) = 0;
- virtual void disp(void) = 0;
-
- // Common loop function, manages display on/off functions for powersave and screensaver with motionsensor
- // can be overridden by subclasses
- virtual void loop(uint8_t lum, bool motion) {
-
- bool dispConditions = (!mEnPowerSave || (mDisplayData->nrProducing > 0)) &&
- ((mScreenSaver != 2) || motion); // screensaver 2 .. motionsensor
-
- if (mDisplayActive) {
- if (!dispConditions) {
- if (mDisplayTime.isTimeout()) { // switch display off after timeout
- mDisplayActive = false;
- mDisplay->setPowerSave(true);
- DBGPRINTLN("**** Display off ****");
+ public:
+ DisplayMono() {};
+
+ virtual void init(DisplayData *displayData) = 0;
+ virtual void config(display_t *cfg) = 0;
+ virtual void disp(void) = 0;
+
+ // Common loop function, manages display on/off functions for powersave and screensaver with motionsensor
+ // can be overridden by subclasses
+ virtual bool loop(bool motion) {
+
+ bool dispConditions = (!mCfg->pwrSaveAtIvOffline || (mDisplayData->nrProducing > 0)) &&
+ ((mCfg->screenSaver != 2) || motion); // screensaver 2 .. motionsensor
+
+ if (mDisplayActive) {
+ if (!dispConditions) {
+ if (mDisplayTime.isTimeout()) { // switch display off after timeout
+ mDisplayActive = false;
+ mDisplay->setPowerSave(true);
+ }
}
+ else
+ mDisplayTime.reStartTimeMonitor(); // keep display on
}
- else
- mDisplayTime.reStartTimeMonitor(); // keep display on
- }
- else {
- if (dispConditions) {
- mDisplayActive = true;
- mDisplayTime.reStartTimeMonitor(); // switch display on
- mDisplay->setPowerSave(false);
- DBGPRINTLN("**** Display on ****");
+ else {
+ if (dispConditions) {
+ mDisplayActive = true;
+ mDisplayTime.reStartTimeMonitor(); // switch display on
+ mDisplay->setPowerSave(false);
+ }
}
- }
- if(mLuminance != lum) {
- mLuminance = lum;
+ if(mLuminance != mCfg->contrast) {
+ mLuminance = mCfg->contrast;
+ mDisplay->setContrast(mLuminance);
+ }
+
+ return(monoMaintainDispSwitchState()); // return flag, if display content should be updated immediately
+ }
+
+ protected:
+ enum class DispSwitchState {
+ TEXT,
+ GRAPH
+ };
+
+ protected:
+ // Common initialization function to be called by subclasses
+ void monoInit(U8G2* display, DisplayData *displayData) {
+ mDisplay = display;
+ mDisplayData = displayData;
+ mDisplay->begin();
+ mDisplay->setPowerSave(false); // always start with display on
mDisplay->setContrast(mLuminance);
- }
- }
-
- protected:
- U8G2* mDisplay;
- DisplayData *mDisplayData;
-
- uint8_t mType;
- uint16_t mDispWidth;
- uint16_t mDispHeight;
-
- bool mEnPowerSave;
- uint8_t mScreenSaver = 1; // 0 .. off; 1 .. pixelShift; 2 .. motionsensor
- uint8_t mLuminance;
-
- uint8_t mLoopCnt;
- uint8_t mLineXOffsets[5] = {};
- uint8_t mLineYOffsets[5] = {};
-
- uint8_t mExtra;
- int8_t mPixelshift=0;
- TimeMonitor mDisplayTime = TimeMonitor(1000 * 15, true);
- bool mDisplayActive = true; // always start with display on
- char mFmtText[DISP_FMT_TEXT_LEN];
-
- // Common initialization function to be called by subclasses
- void monoInit(U8G2* display, uint8_t type, DisplayData *displayData) {
- mDisplay = display;
- mType = type;
- mDisplayData = displayData;
- mDisplay->begin();
- mDisplay->setPowerSave(false); // always start with display on
- mDisplay->setContrast(mLuminance);
- mDisplay->clearBuffer();
- mDispWidth = mDisplay->getDisplayWidth();
- mDispHeight = mDisplay->getDisplayHeight();
- }
-
- void calcPixelShift(int range) {
- int8_t mod = (millis() / 10000) % ((range >> 1) << 2);
- mPixelshift = mScreenSaver == 1 ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0;
- }
+ mDisplay->clearBuffer();
+ mDispWidth = mDisplay->getDisplayWidth();
+ mDispHeight = mDisplay->getDisplayHeight();
+ mDispSwitchTime.stopTimeMonitor();
+ if (100 == mCfg->graph_ratio) // if graph ratio is 100% start in graph mode
+ mDispSwitchState = DispSwitchState::GRAPH;
+ else if (mCfg->graph_ratio != 0)
+ mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio)); // start display mode change only if ratio is neither 0 nor 100
+ }
+
+ // pixelshift screensaver with wipe effect
+ void calcPixelShift(int range) {
+ int8_t mod = (millis() / 10000) % ((range >> 1) << 2);
+ mPixelshift = (1 == mCfg->screenSaver) ? ((mod < range) ? mod - (range >> 1) : -(mod - range - (range >> 1) + 1)) : 0;
+ }
+
+ protected:
+ enum class PowerGraphState {
+ NO_TIME_SYNC,
+ IN_PERIOD,
+ WAIT_4_NEW_PERIOD,
+ WAIT_4_RESTART
+ };
+
+ // initialize power graph and allocate data buffer based on pixel width
+ void initPowerGraph(uint8_t width, uint8_t height) {
+ DBGPRINTLN(F("---- Init Power Graph ----"));
+ mPgWidth = width;
+ mPgHeight = height;
+ mPgData = new float[mPgWidth];
+ mPgState = PowerGraphState::NO_TIME_SYNC;
+ resetPowerGraph();
+ }
+
+ // add new value to power graph and maintain state engine for period times
+ void addPowerGraphEntry(float val) {
+ if (nullptr == mPgData) // power graph not initialized
+ return;
+
+ bool storeStartEndTimes = false;
+ bool store_entry = false;
+ switch(mPgState) {
+ case PowerGraphState::NO_TIME_SYNC:
+ if ((mDisplayData->pGraphStartTime > 0)
+ && (mDisplayData->pGraphEndTime > 0) // wait until period data is available ...
+ && (mDisplayData->utcTs >= mDisplayData->pGraphStartTime)
+ && (mDisplayData->utcTs < mDisplayData->pGraphEndTime)) // and current time is in period
+ {
+ storeStartEndTimes = true; // period was received -> store
+ store_entry = true;
+ mPgState = PowerGraphState::IN_PERIOD;
+ }
+ break;
+ case PowerGraphState::IN_PERIOD:
+ if (mDisplayData->utcTs > mPgEndTime) // check if end of day is reached ...
+ mPgState = PowerGraphState::WAIT_4_NEW_PERIOD; // then wait for new period setting
+ else
+ store_entry = true;
+ break;
+ case PowerGraphState::WAIT_4_NEW_PERIOD:
+ if ((mPgStartTime != mDisplayData->pGraphStartTime) || (mPgEndTime != mDisplayData->pGraphEndTime)) { // wait until new time period was received ...
+ storeStartEndTimes = true; // and store it for next period
+ mPgState = PowerGraphState::WAIT_4_RESTART;
+ }
+ break;
+ case PowerGraphState::WAIT_4_RESTART:
+ if ((mDisplayData->utcTs >= mPgStartTime) && (mDisplayData->utcTs < mPgEndTime)) { // wait until current time is in period again ...
+ resetPowerGraph(); // then reset power graph data
+ store_entry = true;
+ mPgState = PowerGraphState::IN_PERIOD;
+ }
+ break;
+ }
+
+ // store start and end times of current time period and calculate period length
+ if (storeStartEndTimes) {
+ mPgStartTime = mDisplayData->pGraphStartTime;
+ mPgEndTime = mDisplayData->pGraphEndTime;
+ mPgPeriod = mDisplayData->pGraphEndTime - mDisplayData->pGraphStartTime; // time period of power graph in sec for scaling of x-axis
+ }
+
+ // store new value to mPgData
+ if (store_entry) {
+ mPgLastTime = mDisplayData->utcTs; // time of last datapoint
+ mPgLastPos = std::min((uint8_t) sss2PgPos(mPgLastTime - mPgStartTime), (uint8_t) (mPgWidth - 1)); // last datapoint based on seconds since start
+ mPgData[mPgLastPos] = std::max(mPgData[mPgLastPos], val); // update current datapoint to maximum of all seen values (= envelope curve)
+ mPgMaxPwr = std::max(mPgMaxPwr, val); // update max value of stored data for scaling of y-axis
+ }
+ }
+
+ // plot power graph to given display offset
+ void plotPowerGraph(uint8_t xoff, uint8_t yoff) {
+ if (nullptr == mPgData) // power graph not initialized
+ return;
+
+ // draw axes
+ mDisplay->drawLine(xoff, yoff, xoff, yoff - mPgHeight); // vertical axis
+ mDisplay->drawLine(xoff, yoff, xoff + mPgWidth, yoff); // horizontal axis
+
+ // do not draw as long as time is not set correctly and no data was received
+ if ((0 == mPgStartTime) || (0 == mPgEndTime) || (0 == mPgLastTime) || (0 == mPgLastPos) || (mPgMaxPwr < 1))
+ return;
+
+ // draw X scale
+ tmElements_t tm;
+ breakTime(mPgEndTime, tm);
+ uint8_t endHourPg = tm.Hour; // absolute last hour in diagram
+ breakTime(mPgLastTime, tm);
+ uint8_t endHour = std::min(endHourPg, tm.Hour); // last hour of current data point in scaled diagram
+
+ breakTime(mPgStartTime, tm);
+ tm.Hour += 1;
+ tm.Minute = 0;
+ tm.Second = 0;
+ for (; tm.Hour <= endHour; tm.Hour++) {
+ uint8_t x_pos_screen = getPowerGraphXpos(sss2PgPos((uint32_t) makeTime(tm) - mPgStartTime)); // scale horizontal axis
+ if (12 == tm.Hour) {
+ mDisplay->drawLine(xoff + x_pos_screen, yoff, xoff + x_pos_screen, yoff - 2); // mark noon
+ mDisplay->drawLine(xoff + x_pos_screen - 1, yoff - 1, xoff + x_pos_screen + 1, yoff - 1);
+ }
+ else
+ mDisplay->drawPixel(xoff + x_pos_screen, yoff - 1);
+ }
+
+ // draw Y scale
+ uint16_t scale_y = 10;
+ uint32_t maxpwr_int = static_cast(std::round(mPgMaxPwr));
+ if (maxpwr_int > 100)
+ scale_y = 100;
+
+ for (uint32_t i = scale_y; i <= maxpwr_int; i += scale_y) {
+ uint8_t ypos = yoff - static_cast(std::round(i * (float) mPgHeight / mPgMaxPwr)); // scale vertical axis
+ mDisplay->drawPixel(xoff + 1, ypos);
+ }
+
+ // draw curve
+ for (uint8_t i = 1; i <= mPgLastPos; i++) {
+ mDisplay->drawLine(xoff + getPowerGraphXpos(i - 1), yoff - getPowerGraphValueYpos(i - 1),
+ xoff + getPowerGraphXpos(i), yoff - getPowerGraphValueYpos(i));
+ }
+
+ // print max power value
+ mDisplay->setFont(u8g2_font_4x6_tr);
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%dW", static_cast(std::round(mPgMaxPwr)));
+ mDisplay->drawStr(xoff + 3, yoff - mPgHeight + 5, mFmtText);
+ }
+
+ private:
+ bool monoMaintainDispSwitchState(void) {
+ bool change = false;
+ switch(mDispSwitchState) {
+ case DispSwitchState::TEXT:
+ if (mDispSwitchTime.isTimeout()) {
+ mDispSwitchState = DispSwitchState::GRAPH;
+ mDispSwitchTime.startTimeMonitor(150 * mCfg->graph_ratio); // graph_ratio: 0-100 Gesamtperiode 15000 ms
+ change = true;
+ }
+ break;
+ case DispSwitchState::GRAPH:
+ if (mDispSwitchTime.isTimeout()) {
+ mDispSwitchState = DispSwitchState::TEXT;
+ mDispSwitchTime.startTimeMonitor(150 * (100 - mCfg->graph_ratio));
+ change = true;
+ }
+ break;
+ }
+ return change;
+ }
+
+ // reset power graph
+ void resetPowerGraph() {
+ if (mPgData != nullptr) {
+ mPgMaxPwr = 0.0;
+ mPgLastPos = 0;
+ mPgLastTime = 0;
+ for (uint8_t i = 0; i < mPgWidth; i++) {
+ mPgData[i] = 0.0;
+ }
+ }
+ }
+
+ // get power graph datapoint index, scaled to current time period, by seconds since start
+ uint8_t sss2PgPos(uint seconds_since_start) {
+ if(mPgPeriod)
+ return (seconds_since_start * (mPgWidth - 1) / mPgPeriod);
+ else
+ return 0;
+ }
+
+ // get X-position of power graph, scaled to lastpos, by according data point index
+ uint8_t getPowerGraphXpos(uint8_t p) {
+ if ((p <= mPgLastPos) && (mPgLastPos > 0))
+ return((p * (mPgWidth - 1)) / mPgLastPos); // scaling of x-axis
+ else
+ return 0;
+ }
+
+ // get Y-position of power graph, scaled to maximum value, by according datapoint index
+ uint8_t getPowerGraphValueYpos(uint8_t p) {
+ if ((p < mPgWidth) && (mPgMaxPwr > 0))
+ return((mPgData[p] * (uint32_t) mPgHeight / mPgMaxPwr)); // scaling of data to graph height
+ else
+ return 0;
+ }
+
+ protected:
+ display_t *mCfg;
+ U8G2 *mDisplay;
+ DisplayData *mDisplayData;
+ DispSwitchState mDispSwitchState = DispSwitchState::TEXT;
+
+ uint16_t mDispWidth;
+ uint8_t mExtra;
+ int8_t mPixelshift=0;
+ char mFmtText[DISP_FMT_TEXT_LEN];
+ uint8_t mLineXOffsets[5] = {};
+ uint8_t mLineYOffsets[5] = {};
+
+ uint8_t mPgWidth = 0;
+
+ private:
+ float *mPgData = nullptr;
+ uint8_t mPgHeight = 0;
+ float mPgMaxPwr = 0.0;
+ uint32_t mPgStartTime = 0;
+ uint32_t mPgEndTime = 0;
+ uint32_t mPgPeriod = 0; // seconds
+ uint8_t mPgLastPos = 0;
+ uint32_t mPgLastTime = 0;
+ PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC;
+
+ uint16_t mDispHeight;
+ uint8_t mLuminance;
+
+ TimeMonitor mDisplayTime = TimeMonitor(1000 * DISP_DEFAULT_TIMEOUT, true);
+ TimeMonitor mDispSwitchTime = TimeMonitor();
+ bool mDisplayActive = true; // always start with display on
};
/* adapted 5x8 Font for low-res displays with symbols
Symbols:
- \x80 ... antenna
- \x81 ... WiFi
- \x82 ... suncurve
- \x83 ... sum/sigma
- \x84 ... antenna crossed
- \x85 ... WiFi crossed
- \x86 ... sun
- \x87 ... moon
- \x88 ... calendar/day
- \x89 ... MQTT */
+ \x80 ... antenna
+ \x81 ... WiFi
+ \x82 ... suncurve
+ \x83 ... sum/sigma
+ \x84 ... antenna crossed
+ \x85 ... WiFi crossed
+ \x86 ... sun
+ \x87 ... moon
+ \x88 ... calendar/day
+ \x89 ... MQTT */
const uint8_t u8g2_font_5x8_symbols_ahoy[1049] U8G2_FONT_SECTION("u8g2_font_5x8_symbols_ahoy") =
"j\0\3\2\4\4\3\4\5\10\10\0\377\6\377\6\0\1\61\2b\4\0 \5\0\304\11!\7a\306"
"\212!\11\42\7\63\335\212\304\22#\16u\304\232R\222\14JePJI\2$\14u\304\252l\251m"
diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h
index fa0cacdf..3e5a1762 100644
--- a/src/plugins/Display/Display_Mono_128X32.h
+++ b/src/plugins/Display/Display_Mono_128X32.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://ahoydtu.de
+// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -12,15 +12,13 @@ class DisplayMono128X32 : public DisplayMono {
mExtra = 0;
}
- void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
- mEnPowerSave = enPowerSave;
- mScreenSaver = screenSaver;
- mLuminance = lum;
+ void config(display_t *cfg) {
+ mCfg = cfg;
}
- void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) {
- u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
- monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data), type, displayData);
+ void init(DisplayData *displayData) {
+ u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
+ monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
calcLinePositions();
printText("Ahoy!", 0);
printText("ahoydtu.de", 2);
@@ -58,7 +56,7 @@ class DisplayMono128X32 : public DisplayMono {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", mDisplayData->nrProducing);
printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs)
- printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3);
+ printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);
mDisplay->sendBuffer();
@@ -109,7 +107,7 @@ class DisplayMono128X32 : public DisplayMono {
void printText(const char *text, uint8_t line) {
setFont(line);
- uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift;
+ uint8_t dispX = mLineXOffsets[line] + (pixelShiftRange / 2) + mPixelshift;
if (isTwoRowLine(line)) {
String stringText = String(text);
diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h
index afb581dd..69afae04 100644
--- a/src/plugins/Display/Display_Mono_128X64.h
+++ b/src/plugins/Display/Display_Mono_128X64.h
@@ -4,6 +4,7 @@
//-----------------------------------------------------------------------------
#pragma once
+#include "Display.h"
#include "Display_Mono.h"
class DisplayMono128X64 : public DisplayMono {
@@ -12,27 +13,54 @@ class DisplayMono128X64 : public DisplayMono {
mExtra = 0;
}
- void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
- mEnPowerSave = enPowerSave;
- mScreenSaver = screenSaver;
- mLuminance = lum;
+ void config(display_t *cfg) {
+ mCfg = cfg;
}
- void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) {
- u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
- switch (type) {
+ void init(DisplayData *displayData) {
+ u8g2_cb_t *rot = (u8g2_cb_t *)(( mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
+ switch (mCfg->type) {
+ case DISP_TYPE_T1_SSD1306_128X64:
+ monoInit(new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
+ break;
+ case DISP_TYPE_T2_SH1106_128X64:
+ monoInit(new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
+ break;
+ case DISP_TYPE_T6_SSD1309_128X64:
+ default:
+ monoInit(new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
+ break;
+ }
+ calcLinePositions();
+
+ switch(mCfg->graph_size) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]];
+ case 0:
+ graph_first_line = 1;
+ graph_last_line = 2;
+ break;
case 1:
- monoInit(new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData);
+ graph_first_line = 2;
+ graph_last_line = 3;
break;
case 2:
- monoInit(new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data), type, displayData);
+ graph_first_line = 1;
+ graph_last_line = 3;
break;
- case 6:
+ case 3:
+ graph_first_line = 2;
+ graph_last_line = 4;
+ break;
+ case 4:
default:
- monoInit(new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(rot, reset, clock, data), type, displayData);
+ graph_first_line = 1;
+ graph_last_line = 4;
break;
}
- calcLinePositions();
+
+ widthShrink = (mCfg->screenSaver == 1) ? pixelShiftRange : 0; // shrink graphwidth for pixelshift screensaver
+
+ if (mCfg->graph_ratio > 0)
+ initPowerGraph(mDispWidth - 22 - widthShrink, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2);
printText("Ahoy!", l_Ahoy, 0xff);
printText("ahoydtu.de", l_Website, 0xff);
@@ -61,106 +89,117 @@ class DisplayMono128X64 : public DisplayMono {
// calculate current pixelshift for pixelshift screensaver
calcPixelShift(pixelShiftRange);
- // print total power
- if (mDisplayData->nrProducing > 0) {
- if (mDisplayData->totalPower > 9999.0)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
- else
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
-
- printText(mFmtText, l_TotalPower, 0xff);
- } else {
- printText("offline", l_TotalPower, 0xff);
- }
+ // add new power data to power graph
+ if (mDisplayData->nrProducing > 0)
+ addPowerGraphEntry(mDisplayData->totalPower);
// print Date and time
if (0 != mDisplayData->utcTs)
- printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff);
+ printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff);
- // dynamic status bar, alternatively:
- // print ip address
- if (!(mExtra % 5) && (mDisplayData->ipAddress)) {
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str());
- printText(mFmtText, l_Status, 0xff);
- }
- // print status of inverters
- else {
- sun_pos = -1;
- moon_pos = -1;
- setLineFont(l_Status);
- if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
- else if (0 == mDisplayData->nrSleeping) {
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, " ");
- sun_pos = 0;
- }
- else if (0 == mDisplayData->nrProducing) {
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, " ");
- moon_pos = 0;
+ if (showLine(l_Status)) {
+ // alternatively:
+ // print ip address
+ if (!(mExtra % 5) && (mDisplayData->ipAddress)) {
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str());
+ printText(mFmtText, l_Status, 0xff);
}
+ // print status of inverters
else {
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing);
- sun_pos = mDisplay->getStrWidth(mFmtText) + 1;
- snprintf(mFmtText+2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping);
- moon_pos = mDisplay->getStrWidth(mFmtText) + 1;
- snprintf(mFmtText+7, DISP_FMT_TEXT_LEN, " ");
+ sun_pos = -1;
+ moon_pos = -1;
+ setLineFont(l_Status);
+ if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
+ else if (0 == mDisplayData->nrSleeping) {
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, " ");
+ sun_pos = 0;
+ }
+ else if (0 == mDisplayData->nrProducing) {
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, " ");
+ moon_pos = 0;
+ }
+ else {
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2d", mDisplayData->nrProducing);
+ sun_pos = mDisplay->getStrWidth(mFmtText) + 1;
+ snprintf(mFmtText+2, DISP_FMT_TEXT_LEN, " %2d", mDisplayData->nrSleeping);
+ moon_pos = mDisplay->getStrWidth(mFmtText) + 1;
+ snprintf(mFmtText+7, DISP_FMT_TEXT_LEN, " ");
+ }
+ printText(mFmtText, l_Status, 0xff);
+
+ pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2;
+ mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy);
+ if (sun_pos!=-1)
+ mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol
+ if (moon_pos!=-1)
+ mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol
}
- printText(mFmtText, l_Status, 0xff);
-
- pos = (mDispWidth - mDisplay->getStrWidth(mFmtText)) / 2;
- mDisplay->setFont(u8g2_font_ncenB08_symbols8_ahoy);
- if (sun_pos!=-1)
- mDisplay->drawStr(pos + sun_pos + mPixelshift, mLineYOffsets[l_Status], "G"); // sun symbol
- if (moon_pos!=-1)
- mDisplay->drawStr(pos + moon_pos + mPixelshift, mLineYOffsets[l_Status], "H"); // moon symbol
}
- // print yields
- mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy);
- mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol
- mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol
+ if (showLine(l_TotalPower)) {
+ // print total power
+ if (mDisplayData->nrProducing > 0) {
+ if (mDisplayData->totalPower > 9999.0)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
+ else
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
- if (mDisplayData->totalYieldDay > 9999.0)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0);
- else
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay);
- printText(mFmtText, l_YieldDay, 0xff);
+ printText(mFmtText, l_TotalPower, 0xff);
+ } else {
+ printText("offline", l_TotalPower, 0xff);
+ }
+ }
- if (mDisplayData->totalYieldTotal > 9999.0)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0);
- else
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal);
- printText(mFmtText, l_YieldTotal, 0xff);
+ if (showLine(l_YieldDay)) {
+ // print day yield
+ mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy);
+ mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldDay], "I"); // day symbol
+ mDisplay->drawStr(16 + mPixelshift, mLineYOffsets[l_YieldTotal], "D"); // total symbol
+
+ if (mDisplayData->totalYieldDay > 9999.0)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0);
+ else
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay);
+ printText(mFmtText, l_YieldDay, 0xff);
+ }
+
+ if (showLine(l_YieldTotal)) {
+ // print total yield
+ if (mDisplayData->totalYieldTotal > 9999.0)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0);
+ else
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal);
+ printText(mFmtText, l_YieldTotal, 0xff);
+ }
+
+ if ((mCfg->graph_ratio > 0) && (mDispSwitchState == DispSwitchState::GRAPH)) {
+ // plot power graph
+ plotPowerGraph((mDispWidth - mPgWidth) / 2 + mPixelshift, mLineYOffsets[graph_last_line] - 1);
+ }
// draw dynamic RSSI bars
- int xoffs;
- if (mScreenSaver == 1) // shrink screenwidth for pixelshift screensaver
- xoffs = pixelShiftRange/2;
- else
- xoffs = 0;
int rssi_bar_height = 9;
for (int i = 0; i < 4; i++) {
int radio_rssi_threshold = -60 - i * 10;
int wifi_rssi_threshold = -60 - i * 10;
+ uint8_t barwidth = std::min(4 - i, 3);
if (mDisplayData->RadioRSSI > radio_rssi_threshold)
- mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height);
+ mDisplay->drawBox(widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
if (mDisplayData->WifiRSSI > wifi_rssi_threshold)
- mDisplay->drawBox(mDispWidth - 4 - xoffs + mPixelshift + i, 8 + (rssi_bar_height + 1) * i, 4 - i, rssi_bar_height);
+ mDisplay->drawBox(mDispWidth - barwidth - widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
}
// draw dynamic antenna and WiFi symbols
mDisplay->setFont(u8g2_font_ncenB10_symbols10_ahoy);
char sym[]=" ";
sym[0] = mDisplayData->RadioSymbol?'A':'E'; // NRF
- mDisplay->drawStr(xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym);
+ mDisplay->drawStr((widthShrink / 2) + mPixelshift, mLineYOffsets[l_RSSI], sym);
if (mDisplayData->MQTTSymbol)
sym[0] = 'J'; // MQTT
else
sym[0] = mDisplayData->WifiSymbol?'B':'F'; // Wifi
- mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - xoffs + mPixelshift, mLineYOffsets[l_RSSI], sym);
- mDisplay->sendBuffer();
-
-
+ mDisplay->drawStr(mDispWidth - mDisplay->getStrWidth(sym) - (widthShrink / 2) + mPixelshift, mLineYOffsets[l_RSSI], sym);
mDisplay->sendBuffer();
mExtra++;
@@ -184,7 +223,11 @@ class DisplayMono128X64 : public DisplayMono {
l_MAX_LINES = 5,
};
+ uint8_t graph_first_line;
+ uint8_t graph_last_line;
+
const uint8_t pixelShiftRange = 11; // number of pixels to shift from left to right (centered -> must be odd!)
+ uint8_t widthShrink;
void calcLinePositions() {
uint8_t yOff = 0;
@@ -198,8 +241,8 @@ class DisplayMono128X64 : public DisplayMono {
mLineYOffsets[i] = yOff;
dsc = mDisplay->getDescent();
yOff -= dsc;
- if (l_Time==i) // prevent time and status line to touch
- yOff+=1; // -> one pixels space
+ if (l_Time == i) // prevent time and status line to touch
+ yOff++; // -> one pixels space
i++;
} while(l_MAX_LINES>i);
}
@@ -226,4 +269,8 @@ class DisplayMono128X64 : public DisplayMono {
dispX += mPixelshift;
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
}
+
+ bool showLine(uint8_t line) {
+ return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line)));
+ }
};
diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h
index 68cac96f..84e40126 100644
--- a/src/plugins/Display/Display_Mono_64X48.h
+++ b/src/plugins/Display/Display_Mono_64X48.h
@@ -12,16 +12,14 @@ class DisplayMono64X48 : public DisplayMono {
mExtra = 0;
}
- void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
- mEnPowerSave = enPowerSave;
- mScreenSaver = screenSaver;
- mLuminance = lum;
+ void config(display_t *cfg) {
+ mCfg = cfg;
}
- void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) {
- u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
+ void init(DisplayData *displayData) {
+ u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
// Wemos OLed Shield is not defined in u8 lib -> use nearest compatible
- monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, reset, clock, data), type, displayData);
+ monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
calcLinePositions();
printText("Ahoy!", 0);
@@ -60,7 +58,7 @@ class DisplayMono64X48 : public DisplayMono {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", mDisplayData->nrProducing);
printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs)
- printText(ah::getTimeStr(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), 3);
+ printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);
mDisplay->sendBuffer();
@@ -98,7 +96,7 @@ class DisplayMono64X48 : public DisplayMono {
}
void printText(const char *text, uint8_t line) {
- uint8_t dispX = mLineXOffsets[line] + pixelShiftRange/2 + mPixelshift;
+ uint8_t dispX = mLineXOffsets[line] + pixelShiftRange / 2 + mPixelshift;
setFont(line);
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h
index 7e5c157f..d3be1f31 100644
--- a/src/plugins/Display/Display_Mono_84X48.h
+++ b/src/plugins/Display/Display_Mono_84X48.h
@@ -1,11 +1,10 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://ahoydtu.de
+// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#pragma once
#include "Display_Mono.h"
-#include "../../utils/dbg.h"
class DisplayMono84X48 : public DisplayMono {
public:
@@ -13,16 +12,43 @@ class DisplayMono84X48 : public DisplayMono {
mExtra = 0;
}
- void config(bool enPowerSave, uint8_t screenSaver, uint8_t lum) {
- mEnPowerSave = enPowerSave;
- mScreenSaver = screenSaver;
- mLuminance = lum;
+ void config(display_t *cfg) {
+ mCfg = cfg;
}
- void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, DisplayData *displayData) {
- u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
- monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset), type, displayData);
+ void init(DisplayData *displayData) {
+ u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
+
+ monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, mCfg->disp_clk, mCfg->disp_data, mCfg->disp_cs, mCfg->disp_dc, 0xff), displayData);
calcLinePositions();
+
+ switch(mCfg->graph_size) { // var opts2 = [[0, "Line 1 - 2"], [1, "Line 2 - 3"], [2, "Line 1 - 3"], [3, "Line 2 - 4"], [4, "Line 1 - 4"]];
+ case 0:
+ graph_first_line = 1;
+ graph_last_line = 2;
+ break;
+ case 1:
+ graph_first_line = 2;
+ graph_last_line = 3;
+ break;
+ case 2:
+ graph_first_line = 1;
+ graph_last_line = 3;
+ break;
+ case 3:
+ graph_first_line = 2;
+ graph_last_line = 4;
+ break;
+ case 4:
+ default:
+ graph_first_line = 1;
+ graph_last_line = 4;
+ break;
+ }
+
+ if (mCfg->graph_ratio > 0)
+ initPowerGraph(mDispWidth - 16, mLineYOffsets[graph_last_line] - mLineYOffsets[graph_first_line - 1] - 2);
+
printText("Ahoy!", l_Ahoy, 0xff);
printText("ahoydtu.de", l_Website, 0xff);
printText(mDisplayData->version, l_Version, 0xff);
@@ -45,66 +71,85 @@ class DisplayMono84X48 : public DisplayMono {
mDisplay->drawPixel(mDispWidth-1, mDispHeight-1);
*/
- // print total power
+ // add new power data to power graph
if (mDisplayData->nrProducing > 0) {
- if (mDisplayData->totalPower > 9999.0)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
- else
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
-
- printText(mFmtText, l_TotalPower, 0xff);
- } else {
- printText("offline", l_TotalPower, 0xff);
+ addPowerGraphEntry(mDisplayData->totalPower);
}
// print Date and time
if (0 != mDisplayData->utcTs)
- printText(ah::getDateTimeStrShort(gTimezone.toLocal(mDisplayData->utcTs)).c_str(), l_Time, 0xff);
+ printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff);
+
+ if (showLine(l_Status)) {
+ // alternatively:
+ // print ip address
+ if (!(mExtra % 5) && (mDisplayData->ipAddress)) {
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str());
+ printText(mFmtText, l_Status, 0xff);
+ }
+ // print status of inverters
+ else {
+ if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
+ else if (0 == mDisplayData->nrSleeping)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol
+ else if (0 == mDisplayData->nrProducing)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x87"); // moon symbol
+ else
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d\x86 %d\x87", mDisplayData->nrProducing, mDisplayData->nrSleeping);
+ printText(mFmtText, l_Status, 0xff);
+ }
+ }
- // alternatively:
- // print ip address
- if (!(mExtra % 5) && (mDisplayData->ipAddress)) {
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s", (mDisplayData->ipAddress).toString().c_str());
- printText(mFmtText, l_Status, 0xff);
+ if (showLine(l_TotalPower)) {
+ // print total power
+ if (mDisplayData->nrProducing > 0) {
+ if (mDisplayData->totalPower > 9999.0)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kW", (mDisplayData->totalPower / 1000.0));
+ else
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
+
+ printText(mFmtText, l_TotalPower, 0xff);
+ } else {
+ printText("offline", l_TotalPower, 0xff);
+ }
}
- // print status of inverters
- else {
- if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
- else if (0 == mDisplayData->nrSleeping)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol
- else if (0 == mDisplayData->nrProducing)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x87"); // moon symbol
+
+ if (showLine(l_YieldDay)) {
+ // print day yield
+ printText("\x88", l_YieldDay, 10); // day symbol
+ if (mDisplayData->totalYieldDay > 9999.0)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0);
else
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d\x86 %d\x87", mDisplayData->nrProducing, mDisplayData->nrSleeping);
- printText(mFmtText, l_Status, 0xff);
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay);
+ printText(mFmtText, l_YieldDay, 0xff);
}
- // print yields
- printText("\x88", l_YieldDay, 10); // day symbol
- printText("\x83", l_YieldTotal, 10); // total symbol
-
- if (mDisplayData->totalYieldDay > 9999.0)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f kWh", mDisplayData->totalYieldDay / 1000.0);
- else
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f Wh", mDisplayData->totalYieldDay);
- printText(mFmtText, l_YieldDay, 0xff);
+ if (showLine(l_YieldTotal)) {
+ // print total yield
+ printText("\x83", l_YieldTotal, 10); // total symbol
+ if (mDisplayData->totalYieldTotal > 9999.0)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0);
+ else
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal);
+ printText(mFmtText, l_YieldTotal, 0xff);
+ }
- if (mDisplayData->totalYieldTotal > 9999.0)
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.2f MWh", mDisplayData->totalYieldTotal / 1000.0);
- else
- snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f kWh", mDisplayData->totalYieldTotal);
- printText(mFmtText, l_YieldTotal, 0xff);
+ if ((mCfg->graph_ratio > 0) && (mDispSwitchState == DispSwitchState::GRAPH)) {
+ // plot power graph
+ plotPowerGraph(8, mLineYOffsets[graph_last_line] - 1);
+ }
- // draw dynamic Nokia RSSI bars
+ // draw dynamic RSSI bars
int rssi_bar_height = 7;
- for (int i=0; i<4;i++) {
- int radio_rssi_threshold = -60 - i*10; // radio rssi not yet tested in reality!
- int wifi_rssi_threshold = -60 - i*10;
+ for (int i = 0; i < 4; i++) {
+ int radio_rssi_threshold = -60 - i * 10;
+ int wifi_rssi_threshold = -60 - i * 10;
+ uint8_t barwidth = std::min(4 - i, 3);
if (mDisplayData->RadioRSSI > radio_rssi_threshold)
- mDisplay->drawBox(0, 8+(rssi_bar_height+1)*i, 4-i,rssi_bar_height);
+ mDisplay->drawBox(0, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
if (mDisplayData->WifiRSSI > wifi_rssi_threshold)
- mDisplay->drawBox(mDispWidth-4+i, 8+(rssi_bar_height+1)*i, 4-i,rssi_bar_height);
+ mDisplay->drawBox(mDispWidth - barwidth, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
}
// draw dynamic antenna and WiFi symbols
@@ -139,6 +184,9 @@ class DisplayMono84X48 : public DisplayMono {
l_MAX_LINES = 5,
};
+ uint8_t graph_first_line;
+ uint8_t graph_last_line;
+
void calcLinePositions() {
uint8_t yOff = 0;
uint8_t i = 0;
@@ -150,7 +198,7 @@ class DisplayMono84X48 : public DisplayMono {
yOff += asc;
mLineYOffsets[i] = yOff;
dsc = mDisplay->getDescent();
- if (l_TotalPower!=i) // power line needs no descent spacing
+ if (l_TotalPower != i) // power line needs no descent spacing
yOff -= dsc;
yOff++; // instead lets spend one pixel space between all lines
i++;
@@ -158,7 +206,8 @@ class DisplayMono84X48 : public DisplayMono {
}
inline void setLineFont(uint8_t line) {
- if ((line == l_TotalPower) || (line == l_Ahoy))
+ if ((line == l_TotalPower) ||
+ (line == l_Ahoy))
mDisplay->setFont(u8g2_font_logisoso16_tr);
else
mDisplay->setFont(u8g2_font_5x8_symbols_ahoy);
@@ -174,6 +223,10 @@ class DisplayMono84X48 : public DisplayMono {
dispX = col;
mDisplay->drawStr(dispX, mLineYOffsets[line], text);
}
+
+ bool showLine(uint8_t line) {
+ return ((mDispSwitchState == DispSwitchState::TEXT) || ((line < graph_first_line) || (line > graph_last_line)));
+ }
};
diff --git a/src/plugins/Display/Display_data.h b/src/plugins/Display/Display_data.h
index a400377d..d6ae90e8 100644
--- a/src/plugins/Display/Display_data.h
+++ b/src/plugins/Display/Display_data.h
@@ -4,19 +4,21 @@
#define __DISPLAY_DATA__
struct DisplayData {
- const char *version=nullptr;
- float totalPower=0.0f; // indicate current power (W)
- float totalYieldDay=0.0f; // indicate day yield (Wh)
- float totalYieldTotal=0.0f; // indicate total yield (kWh)
- uint32_t utcTs=0; // indicate absolute timestamp (utc unix time). 0 = time is not synchonized
- uint8_t nrProducing=0; // indicate number of producing inverters
- uint8_t nrSleeping=0; // indicate number of sleeping inverters
- bool WifiSymbol = false; // indicate if WiFi is connected
- bool RadioSymbol = false; // indicate if radio module is connecting and working
- bool MQTTSymbol = false; // indicate if MQTT is connected
- int8_t WifiRSSI=SCHAR_MIN; // indicate RSSI value for WiFi
- int8_t RadioRSSI=SCHAR_MIN; // indicate RSSI value for radio
- IPAddress ipAddress; // indicate ip adress of ahoy
+ const char *version=nullptr;
+ float totalPower=0.0f; // indicate current power (W)
+ float totalYieldDay=0.0f; // indicate day yield (Wh)
+ float totalYieldTotal=0.0f; // indicate total yield (kWh)
+ uint32_t utcTs=0; // indicate absolute timestamp (localized utc unix time). 0 = time is not synchonized
+ uint32_t pGraphStartTime=0; // localized starttime for power graph (e.g. sunRise)
+ uint32_t pGraphEndTime=0; // localized endttime for power graph (e.g. sunSet)
+ uint8_t nrProducing=0; // indicate number of producing inverters
+ uint8_t nrSleeping=0; // indicate number of sleeping inverters
+ bool WifiSymbol = false; // indicate if WiFi is connected
+ bool RadioSymbol = false; // indicate if radio module is connecting and working
+ bool MQTTSymbol = false; // indicate if MQTT is connected
+ int8_t WifiRSSI=SCHAR_MIN; // indicate RSSI value for WiFi
+ int8_t RadioRSSI=SCHAR_MIN; // indicate RSSI value for radio
+ IPAddress ipAddress; // indicate ip adress of ahoy
};
#endif /*__DISPLAY_DATA__*/
diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp
index d12da365..fdcec767 100644
--- a/src/plugins/Display/Display_ePaper.cpp
+++ b/src/plugins/Display/Display_ePaper.cpp
@@ -7,6 +7,7 @@
#endif
#include "../../utils/helper.h"
#include "imagedata.h"
+#include "defines.h"
#if defined(ESP32)
@@ -19,6 +20,7 @@ SPIClass hspi(HSPI);
DisplayEPaper::DisplayEPaper() {
mDisplayRotation = 2;
mHeadFootPadding = 16;
+ memset(_fmtText, 0, EPAPER_MAX_TEXT_LEN);
}
@@ -29,7 +31,7 @@ void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, u
mRefreshState = RefreshStatus::LOGO;
mSecondCnt = 0;
- if (type == 10) {
+ if (DISP_TYPE_T10_EPAPER == type) {
Serial.begin(115200);
_display = new GxEPD2_BW(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY));
@@ -65,6 +67,7 @@ void DisplayEPaper::refreshLoop() {
case RefreshStatus::LOGO:
_display->fillScreen(GxEPD_BLACK);
_display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
+ _display->display(false); // full update
mNextRefreshState = RefreshStatus::PARTITIALS;
mRefreshState = RefreshStatus::WAIT;
break;
@@ -112,9 +115,9 @@ void DisplayEPaper::headlineIP() {
do {
if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) {
- snprintf(_fmtText, sizeof(_fmtText), "%s", WiFi.localIP().toString().c_str());
+ snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str());
} else {
- snprintf(_fmtText, sizeof(_fmtText), "WiFi not connected");
+ snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "WiFi not connected");
}
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@@ -135,7 +138,7 @@ void DisplayEPaper::lastUpdatePaged() {
_display->fillScreen(GxEPD_BLACK);
do {
if (NULL != mUtcTs) {
- snprintf(_fmtText, sizeof(_fmtText), ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str());
+ snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str());
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@@ -156,7 +159,7 @@ void DisplayEPaper::versionFooter() {
_display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding);
_display->fillScreen(GxEPD_BLACK);
do {
- snprintf(_fmtText, sizeof(_fmtText), "Version: %s", _version);
+ snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "Version: %s", _version);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@@ -177,7 +180,7 @@ void DisplayEPaper::offlineFooter() {
_display->fillScreen(GxEPD_BLACK);
do {
if (NULL != mUtcTs) {
- snprintf(_fmtText, sizeof(_fmtText), "offline");
+ snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline");
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@@ -201,13 +204,13 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
do {
// actual Production
if (totalPower > 9999) {
- snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 1000));
+ snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.1f kW", (totalPower / 1000));
_changed = true;
} else if ((totalPower > 0) && (totalPower <= 9999)) {
- snprintf(_fmtText, sizeof(_fmtText), "%.0f W", totalPower);
+ snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.0f W", totalPower);
_changed = true;
} else
- snprintf(_fmtText, sizeof(_fmtText), "offline");
+ snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline");
if ((totalPower == 0) && (mEnPowerSave)) {
_display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK);
@@ -262,7 +265,7 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
// Inverter online
_display->setFont(&FreeSans12pt7b);
y = _display->height() - (mHeadFootPadding + 10);
- snprintf(_fmtText, sizeof(_fmtText), " %d online", isprod);
+ snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d online", isprod);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
_display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK);
x = ((_display->width() - tbw - 20) / 2) - tbx;
diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h
index c9a0fbf5..c26d3b42 100644
--- a/src/plugins/Display/Display_ePaper.h
+++ b/src/plugins/Display/Display_ePaper.h
@@ -9,11 +9,11 @@
// enable GxEPD2_GFX base class
#define ENABLE_GxEPD2_GFX 1
-#include
+#define EPAPER_MAX_TEXT_LEN 35
+
#include
#include
-#include