diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml
index 6eba4879..c15bd883 100644
--- a/.github/workflows/compile_development.yml
+++ b/.github/workflows/compile_development.yml
@@ -1,25 +1,47 @@
-name: Ahoy Dev-Build for ESP8266/ESP32
+name: Ahoy Development
on:
push:
branches: development*
paths-ignore:
- '**.md' # Do no build on *.md changes
+
jobs:
- build:
+ check:
+ name: Check Repository
runs-on: ubuntu-latest
+ if: github.repository == 'lumapu/ahoy' && github.ref_name == 'development03'
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v4
+ build-en:
+ name: Build Environments (English)
+ 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
- with:
- ref: development03
- - uses: benjlevesque/short-sha@v2.1
+ - uses: actions/checkout@v4
+ - uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- name: Cache Pip
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -27,13 +49,13 @@ jobs:
${{ runner.os }}-pip-
- name: Cache PlatformIO
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
- uses: actions/setup-python@v4.3.0
+ uses: actions/setup-python@v5
with:
python-version: "3.x"
@@ -43,43 +65,112 @@ 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/*
- - name: Set Version
- uses: cschleiden/replace-tokens@v1
+ build-de:
+ name: Build Environments (German)
+ 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@v4
+ - uses: benjlevesque/short-sha@v3.0
+ id: short-sha
with:
- files: tools/esp8266/User_Manual.md
- env:
- VERSION: ${{ steps.rename-binary-files.outputs.name }}
+ length: 7
+
+ - name: Cache Pip
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
- - name: Create Manifest
- working-directory: src
- run: python ../scripts/buildManifest.py
+ - name: Cache PlatformIO
+ uses: actions/cache@v4
+ with:
+ path: ~/.platformio
+ key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ - name: Install PlatformIO
+ run: |
+ python -m pip install setuptools --upgrade pip
+ pip install --upgrade platformio
+
+ - name: Run PlatformIO
+ run: pio run -d src -e ${{ matrix.variant }}
+
+ - name: Rename Firmware
+ run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- name: Create Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
- name: ahoydtu_dev
- path: |
- src/firmware/*
- src/User_Manual.md
- src/install.html
+ name: dev-${{ matrix.variant }}
+ path: firmware/*
+
+ deploy:
+ name: Deploy Environments
+ needs: [build-en, build-de]
+ runs-on: ubuntu-latest
+ continue-on-error: false
+ steps:
+ - uses: actions/checkout@v4
+ #- name: Copy boot_app0.bin
+ # run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
+
+ - name: Get Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ merge-multiple: true
+ path: firmware
+
+ - name: Get Version from code
+ id: version_name
+ run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
+
+ - name: Set Version
+ uses: cschleiden/replace-tokens@v1
+ with:
+ files: manual/User_Manual.md
+ env:
+ VERSION: ${{ steps.version_name.outputs.name }}
- name: Rename firmware directory
- run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }}
+ run: mv firmware ${{ steps.version_name.outputs.name }}
- name: Deploy
uses: nogsantos/scp-deploy@master
with:
- src: src/${{ steps.rename-binary-files.outputs.name }}/
+ src: ${{ steps.version_name.outputs.name }}/
host: ${{ secrets.FW_SSH_HOST }}
remote: ${{ secrets.FW_SSH_DIR }}/dev
port: ${{ secrets.FW_SSH_PORT }}
diff --git a/.github/workflows/compile_release.yml b/.github/workflows/compile_release.yml
index a7e3511d..59adf28c 100644
--- a/.github/workflows/compile_release.yml
+++ b/.github/workflows/compile_release.yml
@@ -1,29 +1,49 @@
-name: Ahoy Release for ESP8266/ESP32
+name: Ahoy Release
on:
push:
branches: main
- paths:
- - 'src/**' # build only when changes occur here
- - '.github/workflows/compile_release.yml'
- - '!README.md'
- - '!CHANGES.md'
- - '!User_Manual.md'
+ paths-ignore:
+ - '**.md' # Do no build on *.md changes
+
jobs:
build:
+ name: Build Environments
runs-on: ubuntu-latest
-
+ if: github.repository == 'lumapu/ahoy' && github.ref_name == 'main'
+ continue-on-error: false
+ strategy:
+ matrix:
+ variant:
+ - esp8266
+ - esp8266-prometheus
+ - esp8285
+ - esp32-wroom32
+ - esp32-wroom32-prometheus
+ - esp32-wroom32-ethernet
+ - esp32-s2-mini
+ - esp32-c3-mini
+ - opendtufusion
+ - opendtufusion-ethernet
+ - esp8266-de
+ - esp8266-prometheus-de
+ - esp8285-de
+ - esp32-wroom32-de
+ - esp32-wroom32-prometheus-de
+ - esp32-wroom32-ethernet-de
+ - esp32-s2-mini-de
+ - esp32-c3-mini-de
+ - opendtufusion-de
+ - opendtufusion-ethernet-de
steps:
- - uses: actions/checkout@v3
- with:
- ref: main
- - uses: benjlevesque/short-sha@v2.1
+ - uses: actions/checkout@v4
+ - uses: benjlevesque/short-sha@v3.0
id: short-sha
with:
length: 7
- name: Cache Pip
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -31,13 +51,13 @@ jobs:
${{ runner.os }}-pip-
- name: Cache PlatformIO
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- name: Setup Python
- uses: actions/setup-python@v4.3.0
+ uses: actions/setup-python@v5
with:
python-version: "3.x"
@@ -47,56 +67,108 @@ jobs:
pip install --upgrade platformio
- name: Run PlatformIO
- run: pio run -d src --environment esp8266 --environment esp8266-prometheus --environment esp8285 --environment esp32-wroom32 --environment esp32-wroom32-prometheus --environment esp32-wroom32-ethernet --environment esp32-s2-mini --environment esp32-c3-mini --environment opendtufusion --environment opendtufusion-ethernet
+ run: pio run -d src -e ${{ matrix.variant }}
+
+ - name: Rename Firmware
+ run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
- - name: Copy boot_app0.bin
- run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin src/.pio/build/opendtufusion/ota.bin
+ - name: Create Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: ${{ matrix.variant }}
+ path: firmware/*
- - name: Rename Binary files
- id: rename-binary-files
- working-directory: src
- run: python ../scripts/getVersion.py >> $GITHUB_OUTPUT
- - name: Create Release
- id: create-release
- uses: actions/create-release@v1
+ release:
+ name: Create Release
+ runs-on: ubuntu-latest
+ needs: [build]
+ continue-on-error: false
+ permissions:
+ contents: write
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Get Artifacts
+ uses: actions/download-artifact@v4
with:
- draft: false
- prerelease: false
- release_name: ${{ steps.rename-binary-files.outputs.name }}
- tag_name: ${{ steps.rename-binary-files.outputs.name }}
- body_path: src/CHANGES.md
- env:
- GITHUB_TOKEN: ${{ github.token }}
+ merge-multiple: true
+ path: firmware
+
+ - name: Get Version from code
+ id: version_name
+ run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
+
+ - name: Create tag
+ uses: actions/github-script@v7
+ with:
+ script: |
+ github.rest.git.createRef({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ ref: 'refs/tags/${{ steps.version_name.outputs.name }}',
+ sha: context.sha
+ })
- name: Set Version
uses: cschleiden/replace-tokens@v1
with:
- files: User_Manual.md
+ files: manual/User_Manual.md
env:
- VERSION: ${{ steps.rename-binary-files.outputs.name }}
+ VERSION: ${{ steps.version_name.outputs.name }}
- - name: Create Artifact
- run: zip --junk-paths ${{ steps.rename-binary-files.outputs.name }}.zip src/firmware/* User_Manual.md
+ - name: Rename firmware directory
+ run: mv firmware ${{ steps.version_name.outputs.name }}
- - name: Upload Release
- id: upload-release
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Rename firmware directory
+ uses: vimtor/action-zip@v1.2
+ with:
+ files: ${{ steps.version_name.outputs.name }} manual/User_Manual.md manual/Getting_Started.md
+ dest: '${{ steps.version_name.outputs.name }}.zip'
+
+ - name: Publish Release
+ uses: ncipollo/release-action@v1
with:
- upload_url: ${{ steps.create-release.outputs.upload_url }}
- asset_path: ./${{ steps.rename-binary-files.outputs.name }}.zip
- asset_name: ${{ steps.rename-binary-files.outputs.name }}.zip
- asset_content_type: application/zip
+ artifactErrorsFailBuild: true
+ skipIfReleaseExists: true
+ bodyFile: src/CHANGES.md
+ artifacts: '${{ steps.version_name.outputs.name }}.zip'
+ tag: ${{ steps.version_name.outputs.name }}
+ name: ${{ steps.version_name.outputs.name }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+
+ deploy:
+ name: Deploy Environments to fw.ahoydtu.de
+ needs: [build, release]
+ runs-on: ubuntu-latest
+ continue-on-error: false
+ steps:
+ - uses: actions/checkout@v4
+ - name: Get Artifacts
+ uses: actions/download-artifact@v4
+ with:
+ merge-multiple: true
+ path: firmware
+
+ - name: Get Version from code
+ id: version_name
+ run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT
+
+ - name: Set Version
+ uses: cschleiden/replace-tokens@v1
+ with:
+ files: manual/User_Manual.md
+ env:
+ VERSION: ${{ steps.version_name.outputs.name }}
- name: Rename firmware directory
- run: mv src/firmware src/${{ steps.rename-binary-files.outputs.name }}
+ run: mv firmware ${{ steps.version_name.outputs.name }}
- name: Deploy
uses: nogsantos/scp-deploy@master
with:
- src: src/${{ steps.rename-binary-files.outputs.name }}/
+ src: ${{ steps.version_name.outputs.name }}/
host: ${{ secrets.FW_SSH_HOST }}
remote: ${{ secrets.FW_SSH_DIR }}/release
port: ${{ secrets.FW_SSH_PORT }}
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..16b2a256 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)
@@ -31,7 +31,7 @@ Table of approaches:
| Board | MI | HM | HMS/HMT | comment | HowTo start |
| ------ | -- | -- | ------- | ------- | ---------- |
-| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
+| [ESP8266/ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) |
| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | |
| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | |
@@ -39,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](manual/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 ](manual/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!**
@@ -62,4 +64,4 @@ If you run into any issues, please feel free to use the issue tracker here on Gi
- [OpenDTU](https://github.com/tbnobody/OpenDTU)
<- Our sister project ✨ for Hoymiles HM- and HMS-/HMT-series (for ESP32 only!)
- [hms-mqtt-publisher](https://github.com/DennisOSRM/hms-mqtt-publisher)
- <- a project which can handle WiFi inverters like HMS-XXXXW-2T
\ No newline at end of file
+ <- a project which can handle WiFi inverters like HMS-XXXXW-2T
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 91%
rename from User_Manual.md
rename to manual/User_Manual.md
index b4ef8666..b5cf5a7e 100644
--- a/User_Manual.md
+++ b/manual/User_Manual.md
@@ -166,6 +166,8 @@ inverter/ctrl/limit/0 600W
### Power Limit persistent
This feature was removed. The persisten limit should not be modified cyclic by a script because of potential wearout of the flash inside the inverter.
+
+
## Control via REST API
### Generic Information
@@ -174,6 +176,46 @@ The rest API works with *JSON* POST requests. All the following instructions mus
👆 `` is the number of the specific inverter in the setup page.
+### Authentication (new for versions > `0.8.79`)
+
+The authentication is only needed if a password was set.
+To authenticate from API you have to add the following `JSON` to your request:
+
+```json
+{
+ "auth":
+}
+```
+`` is your DTU password in plain text.
+
+As Response you get the following `JSON` if successful:
+
+```json
+{
+ "success": true,
+ "token": ""
+}
+```
+Where `` is a random token with a length of 16 characters.
+
+For all following commands you have only to include the token into your `JSON`:
+```json
+{
+ "token": ""
+}
+```
+
+ℹ️ Do not pass the plain text password with each command. Authenticate once and then use the token for all following commands. The token expires once the token wasn't sent for 20 minutes.
+
+If the authentication fails or the token is expired you will receive the following `JSON`:
+
+```json
+{
+ "success": false,
+ "error": "ERR_PROTECTED"
+}
+```
+
### Inverter Power (On / Off)
```json
@@ -195,8 +237,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 +248,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 +260,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 +272,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,21 +284,8 @@ 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]`
-### Developer Information REST API (obsolete)
-In the same approach as for MQTT any other SubCmd and also MainCmd can be applied and the response payload can be observed in the serial logs. Eg. request the Alarm-Data from the Alarm-Index 5 from inverter 0 will look like this:
-```json
-{
- "inverter":0,
- "tx_request": 21,
- "cmd": 17,
- "payload": 5,
- "payload2": 0
-}
-```
## Zero Export Control (needs rework)
* You can use the mqtt topic `/devcontrol//11` with a number as payload (eg. 300 -> 300 Watt) to set the power limit to the published number in Watt. (In regular cases the inverter will use the new set point within one intervall period; to verify this see next bullet)
@@ -328,7 +358,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/patches/RF24_Hal.patch b/patches/RF24_Hal.patch
index 1a4f159e..88a53bf9 100644
--- a/patches/RF24_Hal.patch
+++ b/patches/RF24_Hal.patch
@@ -1,5 +1,5 @@
diff --git a/RF24.cpp b/RF24.cpp
-index c0cc732..b6708d9 100644
+index 9e5b4a8..af00758 100644
--- a/RF24.cpp
+++ b/RF24.cpp
@@ -12,228 +12,24 @@
@@ -727,7 +727,7 @@ index c0cc732..b6708d9 100644
}
/****************************************************************************/
-@@ -1676,15 +1136,8 @@ void RF24::closeReadingPipe(uint8_t pipe)
+@@ -1675,15 +1135,8 @@ void RF24::closeReadingPipe(uint8_t pipe)
void RF24::toggle_features(void)
{
@@ -745,8 +745,20 @@ index c0cc732..b6708d9 100644
}
/****************************************************************************/
+@@ -1871,6 +1324,11 @@ uint8_t RF24::getARC(void)
+ return read_register(OBSERVE_TX) & 0x0F;
+ }
+
++uint8_t RF24::getPLOS(void)
++{
++ return (read_register(OBSERVE_TX) >> 4) & 0x0F;
++}
++
+ /****************************************************************************/
+
+ bool RF24::setDataRate(rf24_datarate_e speed)
diff --git a/RF24.h b/RF24.h
-index dbd32ae..f774bba 100644
+index dbd32ae..74ae35d 100644
--- a/RF24.h
+++ b/RF24.h
@@ -16,12 +16,7 @@
@@ -932,7 +944,15 @@ index dbd32ae..f774bba 100644
*/
void encodeRadioDetails(uint8_t* encoded_status);
-@@ -1896,18 +1800,6 @@ private:
+@@ -1644,6 +1548,7 @@ public:
+ * @return Returns values from 0 to 15.
+ */
+ uint8_t getARC(void);
++ uint8_t getPLOS(void);
+
+ /**
+ * Set the transmission @ref Datarate
+@@ -1896,18 +1801,6 @@ private:
*/
bool _init_pins();
diff --git a/scripts/applyPatches.py b/scripts/applyPatches.py
index 3ba30a5f..147fb0f3 100644
--- a/scripts/applyPatches.py
+++ b/scripts/applyPatches.py
@@ -12,11 +12,11 @@ def applyPatch(libName, patchFile):
os.chdir('.pio/libdeps/' + env['PIOENV'] + '/' + libName)
- process = subprocess.run(['git', 'apply', '--reverse', '--check', '../../../../' + patchFile], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ process = subprocess.run(['git', 'apply', '--ignore-whitespace', '--reverse', '--check', '../../../../' + patchFile], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if (process.returncode == 0):
print('\'' + patchFile + '\' already applied')
else:
- process = subprocess.run(['git', 'apply', '../../../../' + patchFile])
+ process = subprocess.run(['git', 'apply', '--ignore-whitespace', '../../../../' + patchFile])
if (process.returncode == 0):
print('\'' + patchFile + '\' applied')
else:
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 e67748fd..5fd4fc31 100644
--- a/src/CHANGES.md
+++ b/src/CHANGES.md
@@ -1,12 +1,41 @@
-Changelog v0.8.36
+Changelog v0.8.83
-* added dim option for LEDS
-* changed reload time for opendtufusion after update to 5s
-* fix default interval and gap for communication
-* fix serial number in exported json (was decimal, now correct as hexdecimal number)
-* beautified factory reset
-* added second stage for erase settings
-* increased maximal number of inverters to 32 for opendtufusion board (ESP32-S3)
-* fixed crash if CMT inverter is enabled, but CMT isn't configured
+* added German translations for all variants
+* added reading grid profile
+* added decimal place for active power control (APC aka power limit)
+* added information about working IRQ for NRF24 and CMT2300A to `/system`
+* added loss rate to `/visualization` in the statistics window and MqTT
+* added optional output to display whether it's night time or not. Can be reused as output to control battery system or mapped to a LED
+* added timestamp for `max ac power` as tooltip
+* added wizard for initial WiFi connection
+* added history graph (still under development)
+* added simulator (must be activated before compile, standard: off)
+* added minimal version (without: MqTT, Display, History), WebUI is not changed! (not compiled automatically)
+* added info about installed binary to `/update`
+* added protection to prevent update to wrong firmware (environment check)
+* added optional custom link to the menu
+* added support for other regions (USA, Indonesia)
+* added warning for WiFi channel 12-14 (ESP8266 only)
+* added `max_power` to MqTT total values
+* added API-Token authentification for external scripts
+* improved MqTT by marking sent data and improved `last_success` resends
+* improved communication for HM and MI inverters
+* improved reading live data from inverter
+* improved sending active power control command faster
+* improved `/settings`: pinout has an own subgroup
+* improved export by saving settings before they are exported (to have everything in JSON)
+* improved code quality (cppcheck)
+* seperated sunrise and sunset offset to two fields
+* fix MqTT night communication
+* fix missing favicon to html header
+* fix build on Windows of `opendtufusion` environments (git: trailing whitespaces)
+* fix generation of DTU-ID
+* fix: protect commands from popup in `/live` if password is set
+* fix: prevent sending commands to inverter which isn't active
+* combined firmware and hardware version to JSON topics (MqTT)
+* updated Prometheus with latest changes
+* upgraded most libraries to newer versions
+* beautified typography, added spaces between value and unit for `/visualization`
+* removed add to total (MqTT) inverter setting
full version log: [Development Log](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md)
diff --git a/src/app.cpp b/src/app.cpp
index 0bba14da..0bead49e 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,9 +7,16 @@
#include "app.h"
#include "utils/sun.h"
+#if !defined(ESP32)
+ void esp_task_wdt_reset() {}
+#endif
+
//-----------------------------------------------------------------------------
-app::app() : ah::Scheduler {} {}
+app::app() : ah::Scheduler {} {
+ memset(mVersion, 0, sizeof(char) * 12);
+ memset(mVersionModules, 0, sizeof(char) * 12);
+}
//-----------------------------------------------------------------------------
@@ -18,7 +25,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 +43,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, mConfig->sys.region);
}
#endif
#ifdef ETHERNET
@@ -50,9 +65,14 @@ void app::setup() {
#endif
#endif /* defined(ETHERNET) */
- mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->inst.gapMs);
+ esp_task_wdt_reset();
+
+ mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace);
mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2));
- 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 +82,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,19 +94,21 @@ void app::setup() {
mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
}
#endif
+ #endif
setupLed();
- mWeb.setup(this, &mSys, mConfig);
- mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
+ esp_task_wdt_reset();
+ mWeb.setup(this, &mSys, mConfig);
mApi.setup(this, &mSys, mWeb.getWebSrvPtr(), mConfig);
+ mProtection = Protection::getInstance(mConfig->sys.adminPwd);
#ifdef ENABLE_SYSLOG
mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback)
#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 +116,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 +155,11 @@ void app::loop(void) {
ah::Scheduler::loop();
mCommunication.loop();
+ #if defined(ENABLE_MQTT)
if (mMqttEnabled && mNetworkConnected)
mMqtt.loop();
+ #endif
+ yield();
}
//-----------------------------------------------------------------------------
@@ -126,7 +172,6 @@ void app::onNetwork(bool gotIp) {
mMqttReconnect = true;
mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers!
once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2");
- //tickNtpUpdate();
#if !defined(ETHERNET)
if (WIFI_AP == WiFi.getMode()) {
mMqttEnabled = false;
@@ -139,35 +184,42 @@ void app::onNetwork(bool gotIp) {
void app::regularTickers(void) {
DPRINTLN(DBG_DEBUG, F("regularTickers"));
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
+ everySec([this]() { mProtection->tickSecond(); }, "prot");
+
// Plugins
#if defined(PLUGIN_DISPLAY)
- if (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)
void app::onNtpUpdate(bool gotTime) {
- uint32_t nxtTrig = 5; // default: check again in 5 sec
- if (gotTime || mTimestamp != 0) {
- this->updateNtp();
- nxtTrig = gotTime ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min
- }
- once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp");
+ mNtpReceived = true;
}
#endif /* defined(ETHERNET) */
//-----------------------------------------------------------------------------
void app::updateNtp(void) {
+ #if defined(ENABLE_MQTT)
if (mMqttReconnect && mMqttEnabled) {
mMqtt.tickerSecond();
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS");
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM");
}
+ #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
@@ -179,7 +231,6 @@ void app::updateNtp(void) {
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
if (mConfig->sys.schedReboot) {
- uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght
if (rebootTrig <= mTimestamp) { //necessary for times other than midnight to prevent reboot loop
rebootTrig += 86400;
@@ -188,7 +239,7 @@ void app::updateNtp(void) {
}
}
- if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) {
+ if ((0 == mSunrise) && (0.0 != mConfig->sun.lat) && (0.0 != mConfig->sun.lon)) {
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
tickCalcSunrise();
}
@@ -199,19 +250,24 @@ void app::updateNtp(void) {
//-----------------------------------------------------------------------------
void app::tickNtpUpdate(void) {
uint32_t nxtTrig = 5; // default: check again in 5 sec
+ bool isOK = false;
+
#if defined(ETHERNET)
- bool isOK = (mTimestamp != 0);
- mEth.updateNtpTime();
+ if (!mNtpReceived)
+ mEth.updateNtpTime();
+ else {
+ mNtpReceived = false;
+ isOK = true;
+ }
#else
- bool isOK = mWifi.getNtpTime();
+ isOK = mWifi.getNtpTime();
#endif
- if (isOK || mTimestamp != 0) {
+ if (isOK) {
this->updateNtp();
-
- nxtTrig = isOK ? (mConfig->ntp.interval * 60) : 60; // depending on NTP update success check again in 12h (depends on setting) or in 1 min
+ nxtTrig = mConfig->ntp.interval * 60; // check again in 12h
// immediately start communicating
- if (isOK && mSendFirst) {
+ if (mSendFirst) {
mSendFirst = false;
once(std::bind(&app::tickSend, this), 1, "senOn");
}
@@ -226,15 +282,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'
+ }
}
//-----------------------------------------------------------------------------
@@ -243,22 +302,21 @@ void app::tickIVCommunication(void) {
bool zeroValues = false;
uint32_t nxtTrig = 0;
- Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
- iv = mSys.getInverterByPos(i);
+ Inverter<> *iv = mSys.getInverterByPos(i);
if(NULL == iv)
continue;
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 +337,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
}
//-----------------------------------------------------------------------------
@@ -313,10 +390,9 @@ void app::tickMidnight(void) {
// clear max values
if(mConfig->inst.rstMaxValsMidNight) {
- uint8_t pos;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t i = 0; i <= iv->channels; i++) {
- pos = iv->getPosByChFld(i, FLD_MP, rec);
+ uint8_t pos = iv->getPosByChFld(i, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f);
}
}
@@ -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();
}
@@ -380,8 +466,6 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
continue; // skip to next inverter
if (!iv->config->enabled)
continue; // skip to next inverter
- if (iv->commEnabled)
- continue; // skip to next inverter
if (checkAvail) {
if (!iv->isAvailable())
@@ -409,28 +493,63 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) {
pos = iv->getPosByChFld(ch, FLD_MP, rec);
iv->setValue(pos, rec, 0.0f);
}
+ iv->resetAlarms();
iv->doCalculations();
}
changed = true;
}
- if(changed) {
- 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;
@@ -443,6 +562,10 @@ void app::resetSystem(void) {
mSaveReboot = false;
mNetworkConnected = false;
+
+#if defined(ETHERNET)
+ mNtpReceived = false;
+#endif
}
//-----------------------------------------------------------------------------
@@ -453,14 +576,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 +589,33 @@ 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) {
- Inverter<> *iv;
+ if (mConfig->led.led[0] != DEF_PIN_OFF) {
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
- iv = mSys.getInverterByPos(id);
+ Inverter<> *iv = mSys.getInverterByPos(id);
if (NULL != iv) {
if (iv->isProducing()) {
// turn on when at least one inverter is producing
- 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..b16d7aeb 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,19 @@
#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"
+#include "web/Protection.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 +43,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 +57,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,14 +83,14 @@ 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);
void handleIntr(void) {
mNrfRadio.handleIntr();
}
- void* getRadioObj(bool nrf) {
+ void* getRadioObj(bool nrf) override {
if(nrf)
return (void*)&mNrfRadio;
else {
@@ -87,19 +108,19 @@ class app : public IApp, public ah::Scheduler {
}
#endif
- uint32_t getUptime() {
+ uint32_t getUptime() override {
return Scheduler::getUptime();
}
- uint32_t getTimestamp() {
+ uint32_t getTimestamp() override {
return Scheduler::mTimestamp;
}
- uint64_t getTimestampMs() {
- return ((uint64_t)Scheduler::mTimestamp * 1000) + (uint64_t)Scheduler::mTsMillis;
+ uint64_t getTimestampMs() override {
+ return ((uint64_t)Scheduler::mTimestamp * 1000) + ((uint64_t)millis() - (uint64_t)Scheduler::mTsMillis) % 1000;
}
- bool saveSettings(bool reboot) {
+ bool saveSettings(bool reboot) override {
mShowRebootRequest = true; // only message on index, no reboot
mSavePending = true;
mSaveReboot = reboot;
@@ -110,7 +131,7 @@ class app : public IApp, public ah::Scheduler {
return true;
}
- void initInverter(uint8_t id) {
+ void initInverter(uint8_t id) override {
mSys.addInverter(id, [this](Inverter<> *iv) {
if((IV_MI == iv->ivGen) || (IV_HM == iv->ivGen))
iv->radio = &mNrfRadio;
@@ -121,7 +142,7 @@ class app : public IApp, public ah::Scheduler {
});
}
- bool readSettings(const char *path) {
+ bool readSettings(const char *path) override {
return mSettings.readSettings(path);
}
@@ -129,82 +150,124 @@ class app : public IApp, public ah::Scheduler {
return mSettings.eraseSettings(eraseWifi);
}
- bool getSavePending() {
+ bool getSavePending() override {
return mSavePending;
}
- bool getLastSaveSucceed() {
+ bool getLastSaveSucceed() override {
return mSettings.getLastSaveSucceed();
}
- bool getShouldReboot() {
+ bool getShouldReboot() override {
return mSaveReboot;
}
#if !defined(ETHERNET)
- void scanAvailNetworks() {
+ void scanAvailNetworks() override {
mWifi.scanAvailNetworks();
}
- bool getAvailNetworks(JsonObject obj) {
+ bool getAvailNetworks(JsonObject obj) override {
return mWifi.getAvailNetworks(obj);
}
+ void setupStation(void) override {
+ mWifi.setupStation();
+ }
+
+ void setStopApAllowedMode(bool allowed) override {
+ mWifi.setStopApAllowedMode(allowed);
+ }
+
+ String getStationIp(void) override {
+ return mWifi.getStationIp();
+ }
+
+ bool getWasInCh12to14(void) const override {
+ return mWifi.getWasInCh12to14();
+ }
+
#endif /* !defined(ETHERNET) */
- void setRebootFlag() {
+ void setRebootFlag() override {
once(std::bind(&app::tickReboot, this), 3, "rboot");
}
- const char *getVersion() {
+ const char *getVersion() override {
return mVersion;
}
- uint32_t getSunrise() {
+ const char *getVersionModules() override {
+ return mVersionModules;
+ }
+
+ uint32_t getSunrise() override {
return mSunrise;
}
- uint32_t getSunset() {
+ uint32_t getSunset() override {
return mSunset;
}
- bool getSettingsValid() {
+ bool getSettingsValid() override {
return mSettings.getValid();
}
- bool getRebootRequestState() {
+ bool getRebootRequestState() override {
return mShowRebootRequest;
}
- void setMqttDiscoveryFlag() {
+ void setMqttDiscoveryFlag() override {
+ #if defined(ENABLE_MQTT)
once(std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt), 1, "disCf");
+ #endif
}
- void setMqttPowerLimitAck(Inverter<> *iv) {
- mMqtt.setPowerLimitAck(iv);
+ bool getMqttIsConnected() override {
+ #if defined(ENABLE_MQTT)
+ return mMqtt.isConnected();
+ #else
+ return false;
+ #endif
+ }
+
+ uint32_t getMqttTxCnt() override {
+ #if defined(ENABLE_MQTT)
+ return mMqtt.getTxCnt();
+ #else
+ return 0;
+ #endif
}
- bool getMqttIsConnected() {
- return mMqtt.isConnected();
+ uint32_t getMqttRxCnt() override {
+ #if defined(ENABLE_MQTT)
+ return mMqtt.getRxCnt();
+ #else
+ return 0;
+ #endif
+ }
+
+ void lock(bool fromWeb) override {
+ mProtection->lock(fromWeb);
}
- uint32_t getMqttTxCnt() {
- return mMqtt.getTxCnt();
+ char *unlock(const char *clientIp, bool loginFromWeb) override {
+ return mProtection->unlock(clientIp, loginFromWeb);
}
- uint32_t getMqttRxCnt() {
- return mMqtt.getRxCnt();
+ void resetLockTimeout(void) override {
+ mProtection->resetLockTimeout();
}
- bool getProtection(AsyncWebServerRequest *request) {
- return mWeb.isProtected(request);
+ bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const override {
+ return mProtection->isProtected(clientIp, token, askedFromWeb);
}
- bool getNrfEnabled(void) {
+ bool getNrfEnabled(void) override {
return mConfig->nrf.enabled;
}
- bool getCmtEnabled(void) {
+ bool getCmtEnabled(void) override {
return mConfig->cmt.enabled;
}
@@ -216,19 +279,19 @@ class app : public IApp, public ah::Scheduler {
return mConfig->cmt.pinIrq;
}
- uint32_t getTimezoneOffset() {
+ uint32_t getTimezoneOffset() override {
return mApi.getTimezoneOffset();
}
- void getSchedulerInfo(uint8_t *max) {
+ void getSchedulerInfo(uint8_t *max) override {
getStat(max);
}
- void getSchedulerNames(void) {
+ void getSchedulerNames(void) override {
printSchedulers();
}
- void setTimestamp(uint32_t newTime) {
+ void setTimestamp(uint32_t newTime) override {
DPRINT(DBG_DEBUG, F("setTimestamp: "));
DBGPRINTLN(String(newTime));
if(0 == newTime)
@@ -243,6 +306,22 @@ class app : public IApp, public ah::Scheduler {
Scheduler::setTimestamp(newTime);
}
+ uint16_t getHistoryValue(uint8_t type, uint16_t i) override {
+ #if defined(ENABLE_HISTORY)
+ return mHistory.valueAt((HistoryStorageType)type, i);
+ #else
+ return 0;
+ #endif
+ }
+
+ uint16_t getHistoryMaxDay() override {
+ #if defined(ENABLE_HISTORY)
+ return mHistory.getMaximumDay();
+ #else
+ return 0;
+ #endif
+ }
+
private:
#define CHECK_AVAIL true
#define SKIP_YIELD_DAY true
@@ -252,11 +331,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();
@@ -287,23 +368,30 @@ class app : public IApp, public ah::Scheduler {
void tickNtpUpdate(void);
#if defined(ETHERNET)
void onNtpUpdate(bool gotTime);
+ bool mNtpReceived = false;
#endif /* defined(ETHERNET) */
void updateNtp(void);
+ void triggerTickSend() override {
+ once(std::bind(&app::tickSend, this), 0, "tSend");
+ }
+
void 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;
Communication mCommunication;
- bool mShowRebootRequest;
+ bool mShowRebootRequest = false;
#if defined(ETHERNET)
ahoyeth mEth;
@@ -312,6 +400,7 @@ class app : public IApp, public ah::Scheduler {
#endif /* defined(ETHERNET) */
WebType mWeb;
RestApiType mApi;
+ Protection *mProtection = nullptr;
#ifdef ENABLE_SYSLOG
DbgSyslog mDbgSyslog;
#endif
@@ -326,30 +415,41 @@ class app : public IApp, public ah::Scheduler {
#endif
char mVersion[12];
+ char mVersionModules[12];
settings mSettings;
- settings_t *mConfig;
- bool mSavePending;
- bool mSaveReboot;
+ settings_t *mConfig = nullptr;
+ bool mSavePending = false;
+ bool mSaveReboot = false;
- uint8_t mSendLastIvId;
- bool mSendFirst;
+ uint8_t mSendLastIvId = 0;
+ bool mSendFirst = false;
+ bool mAllIvNotAvail = false;
- bool mNetworkConnected;
+ bool mNetworkConnected = false;
// mqtt
+ #if defined(ENABLE_MQTT)
PubMqttType mMqtt;
- bool mMqttReconnect;
- bool mMqttEnabled;
+ #endif /*ENABLE_MQTT*/
+ bool mMqttReconnect = false;
+ bool mMqttEnabled = false;
// sun
- int32_t mCalculatedTimezoneOffset;
- uint32_t mSunrise, mSunset;
+ int32_t mCalculatedTimezoneOffset = 0;
+ uint32_t mSunrise = 0, mSunset = 0;
// plugins
#if defined(PLUGIN_DISPLAY)
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 34dc5ddc..536455e0 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,24 +7,18 @@
#define __IAPP_H__
#include "defines.h"
-#include "hm/hmSystem.h"
#if defined(ETHERNET)
#include "AsyncWebServer_ESP32_W5500.h"
#else
#include "ESPAsyncWebServer.h"
#endif
-//#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 {
public:
virtual ~IApp() {}
- virtual bool saveSettings(bool stopFs) = 0;
+ virtual bool saveSettings(bool reboot) = 0;
virtual void initInverter(uint8_t id) = 0;
virtual bool readSettings(const char *path) = 0;
virtual bool eraseSettings(bool eraseWifi) = 0;
@@ -33,10 +27,15 @@ 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;
+ virtual bool getWasInCh12to14(void) const = 0;
#endif /* defined(ETHERNET) */
virtual uint32_t getUptime() = 0;
@@ -49,10 +48,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;
@@ -61,10 +61,15 @@ class IApp {
virtual uint32_t getMqttRxCnt() = 0;
virtual uint32_t getMqttTxCnt() = 0;
- virtual bool getProtection(AsyncWebServerRequest *request) = 0;
+ virtual void lock(bool fromWeb) = 0;
+ virtual char *unlock(const char *clientIp, bool loginFromWeb) = 0;
+ virtual void resetLockTimeout(void) = 0;
+ virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0;
- virtual 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..9e59f146 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 0
// 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..18725b48 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
//-----------------------------------------------------------------------------
@@ -13,6 +13,7 @@
#include
#include
+#include
#include
#include "../defines.h"
@@ -30,7 +31,7 @@
* https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout
* */
-#define CONFIG_VERSION 7
+#define CONFIG_VERSION 11
#define PROT_MASK_INDEX 0x0001
@@ -39,8 +40,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 +50,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
@@ -66,6 +69,8 @@ typedef struct {
uint16_t protectionMask;
bool darkMode;
bool schedReboot;
+ uint8_t region;
+ int8_t timezone;
#if !defined(ETHERNET)
// wifi
@@ -106,7 +111,8 @@ typedef struct {
typedef struct {
float lat;
float lon;
- uint16_t offsetSec;
+ int16_t offsetSecMorning;
+ int16_t offsetSecEvening;
} cfgSun_t;
typedef struct {
@@ -114,11 +120,11 @@ typedef struct {
bool debug;
bool privacyLog;
bool printWholeTrace;
+ bool log2mqtt;
} 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;
@@ -143,7 +149,6 @@ typedef struct {
uint8_t frequency;
uint8_t powerLevel;
bool disNightCom; // disable night communication
- bool add2Total; // add values to total values - useful if one inverter is on battery to turn off
} cfgIv_t;
typedef struct {
@@ -157,7 +162,6 @@ typedef struct {
bool rstMaxValsMidNight;
bool startWithoutTime;
float yieldEffiency;
- uint16_t gapMs;
bool readGrid;
} cfgInst_t;
@@ -165,6 +169,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 +186,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 {
@@ -200,7 +208,7 @@ typedef struct {
class settings {
public:
settings() {
- mLastSaveSucceed = false;
+ std::fill(reinterpret_cast(&mCfg), reinterpret_cast(&mCfg) + sizeof(mCfg), 0);
}
void setup() {
@@ -308,18 +316,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()));
@@ -371,9 +379,9 @@ class settings {
memcpy(&tmp, &mCfg.sys, sizeof(cfgSys_t));
}
// erase all settings and reset to default
- memset(&mCfg, 0, sizeof(settings_t));
+ std::fill(reinterpret_cast(&mCfg), reinterpret_cast(&mCfg) + sizeof(mCfg), 0);
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
- | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT;
+ | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY;
mCfg.sys.darkMode = false;
mCfg.sys.schedReboot = false;
// restore temp settings
@@ -389,6 +397,8 @@ class settings {
#endif /* !defined(ETHERNET) */
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME);
+ mCfg.sys.region = 0; // Europe
+ mCfg.sys.timezone = 1;
mCfg.nrf.pinCs = DEF_NRF_CS_PIN;
mCfg.nrf.pinCe = DEF_NRF_CE_PIN;
@@ -420,12 +430,14 @@ 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;
mCfg.serial.privacyLog = true;
mCfg.serial.printWholeTrace = false;
+ mCfg.serial.log2mqtt = false;
mCfg.mqtt.port = DEF_MQTT_PORT;
snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", DEF_MQTT_BROKER);
@@ -441,24 +453,25 @@ class settings {
mCfg.inst.startWithoutTime = false;
mCfg.inst.rstMaxValsMidNight = false;
mCfg.inst.yieldEffiency = 1.0f;
- mCfg.inst.gapMs = 500;
mCfg.inst.readGrid = true;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value
mCfg.inst.iv[i].frequency = 0x12; // 863MHz (minimum allowed frequency)
mCfg.inst.iv[i].disNightCom = false;
- mCfg.inst.iv[i].add2Total = true;
}
- mCfg.led.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 +479,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() {
@@ -477,25 +490,30 @@ class settings {
}
if(mCfg.configVersion < 2) {
mCfg.inst.iv[i].disNightCom = false;
- mCfg.inst.iv[i].add2Total = true;
}
if(mCfg.configVersion < 3) {
mCfg.serial.printWholeTrace = false;
}
- if(mCfg.configVersion < 4) {
- mCfg.inst.gapMs = 500;
- }
if(mCfg.configVersion < 5) {
mCfg.inst.sendInterval = SEND_INTERVAL;
mCfg.serial.printWholeTrace = false;
}
if(mCfg.configVersion < 6) {
- mCfg.inst.gapMs = 500;
mCfg.inst.readGrid = true;
}
if(mCfg.configVersion < 7) {
mCfg.led.luminance = 255;
}
+ if(mCfg.configVersion < 8) {
+ mCfg.sun.offsetSecEvening = mCfg.sun.offsetSecMorning;
+ }
+ if(mCfg.configVersion < 10) {
+ mCfg.sys.region = 0; // Europe
+ mCfg.sys.timezone = 1;
+ }
+ if(mCfg.configVersion < 11) {
+ mCfg.serial.log2mqtt = false;
+ }
}
}
@@ -521,6 +539,8 @@ class settings {
obj[F("prot_mask")] = mCfg.sys.protectionMask;
obj[F("dark")] = mCfg.sys.darkMode;
obj[F("reb")] = (bool) mCfg.sys.schedReboot;
+ obj[F("region")] = mCfg.sys.region;
+ obj[F("timezone")] = mCfg.sys.timezone;
ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf);
ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf);
ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
@@ -538,6 +558,8 @@ class settings {
getVal(obj, F("prot_mask"), &mCfg.sys.protectionMask);
getVal(obj, F("dark"), &mCfg.sys.darkMode);
getVal(obj, F("reb"), &mCfg.sys.schedReboot);
+ getVal(obj, F("region"), &mCfg.sys.region);
+ getVal(obj, F("timezone"), &mCfg.sys.timezone);
if(obj.containsKey(F("ip"))) ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as());
if(obj.containsKey(F("mask"))) ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as());
if(obj.containsKey(F("dns1"))) ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as());
@@ -546,7 +568,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 +647,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);
}
}
@@ -639,11 +663,13 @@ class settings {
obj[F("debug")] = mCfg.serial.debug;
obj[F("prv")] = (bool) mCfg.serial.privacyLog;
obj[F("trc")] = (bool) mCfg.serial.printWholeTrace;
+ obj[F("mqtt")] = (bool) mCfg.serial.log2mqtt;
} else {
getVal(obj, F("show"), &mCfg.serial.showIv);
getVal(obj, F("debug"), &mCfg.serial.debug);
getVal(obj, F("prv"), &mCfg.serial.privacyLog);
getVal(obj, F("trc"), &mCfg.serial.printWholeTrace);
+ getVal(obj, F("mqtt"), &mCfg.serial.log2mqtt);
}
}
@@ -670,13 +696,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 +716,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 +729,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 +749,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);
}
}
@@ -728,7 +764,6 @@ class settings {
obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime;
obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstMaxValsMidNight;
obj[F("yldEff")] = mCfg.inst.yieldEffiency;
- obj[F("gap")] = mCfg.inst.gapMs;
obj[F("rdGrid")] = (bool)mCfg.inst.readGrid;
}
else {
@@ -740,7 +775,6 @@ class settings {
getVal(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime);
getVal(obj, F("rstMaxMidNight"), &mCfg.inst.rstMaxValsMidNight);
getVal(obj, F("yldEff"), &mCfg.inst.yieldEffiency);
- getVal(obj, F("gap"), &mCfg.inst.gapMs);
getVal(obj, F("rdGrid"), &mCfg.inst.readGrid);
if(mCfg.inst.yieldEffiency < 0.5)
@@ -769,7 +803,6 @@ class settings {
obj[F("freq")] = cfg->frequency;
obj[F("pa")] = cfg->powerLevel;
obj[F("dis")] = cfg->disNightCom;
- obj[F("add")] = cfg->add2Total;
for(uint8_t i = 0; i < 6; i++) {
obj[F("yield")][i] = cfg->yieldCor[i];
obj[F("pwr")][i] = cfg->chMaxPwr[i];
@@ -782,7 +815,6 @@ class settings {
getVal(obj, F("freq"), &cfg->frequency);
getVal(obj, F("pa"), &cfg->powerLevel);
getVal(obj, F("dis"), &cfg->disNightCom);
- getVal(obj, F("add"), &cfg->add2Total);
uint8_t size = 4;
if(obj.containsKey(F("pwr")))
size = obj[F("pwr")].size();
@@ -796,8 +828,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 +841,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
@@ -819,7 +855,7 @@ class settings {
#endif
settings_t mCfg;
- bool mLastSaveSucceed;
+ bool mLastSaveSucceed = 0;
};
#endif /*__SETTINGS_H__*/
diff --git a/src/defines.h b/src/defines.h
index 23919899..bf111d99 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 36
+#define VERSION_PATCH 83
//-------------------------------------
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 157a9c76..ebd91c67 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 = nullptr;
uint32_t *mUtcTimestamp;
AsyncUDP mUdp; // for time server
diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h
index bb815a7e..328309ac 100644
--- a/src/hm/CommQueue.h
+++ b/src/hm/CommQueue.h
@@ -11,11 +11,13 @@
#include "hmInverter.h"
#include "../utils/dbg.h"
+#define DEFAULT_ATTEMPS 5
+#define MORE_ATTEMPS_ALARMDATA 3 // 8
+#define MORE_ATTEMPS_GRIDONPROFILEPARA 0 // 5
+
template
class CommQueue {
public:
- CommQueue() {}
-
void addImportant(Inverter<> *iv, uint8_t cmd) {
dec(&mRdPtr);
mQueue[mRdPtr] = queue_s(iv, cmd, true);
@@ -30,12 +32,12 @@ class CommQueue {
mQueue[mWrPtr] = queue_s(iv, cmd, false);
}
- uint8_t getFillState(void) {
+ uint8_t getFillState(void) const {
//DPRINTLN(DBG_INFO, "wr: " + String(mWrPtr) + ", rd: " + String(mRdPtr));
return abs(mRdPtr - mWrPtr);
}
- uint8_t getMaxFill(void) {
+ uint8_t getMaxFill(void) const {
return N;
}
@@ -44,11 +46,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 +62,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,13 +84,14 @@ 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);
}
- void setTs(uint32_t *ts) {
+ void setTs(const uint32_t *ts) {
mQueue[mRdPtr].ts = *ts;
}
@@ -96,6 +102,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..a37bcdb2 100644
--- a/src/hm/Communication.h
+++ b/src/hm/Communication.h
@@ -1,34 +1,31 @@
//-----------------------------------------------------------------------------
-// 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
//-----------------------------------------------------------------------------
#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__
+#include
#include "CommQueue.h"
#include
#include "../utils/crc.h"
#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
+#define MAX_BUFFER 200
typedef std::function *)> payloadListenerType;
+typedef std::function *)> powerLimitAckListenerType;
typedef std::function *)> alarmListenerType;
class Communication : public CommQueue<> {
public:
- void setup(uint32_t *timestamp, bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint16_t *inverterGap) {
+ void setup(uint32_t *timestamp, bool *serialDebug, bool *privacyMode, bool *printWholeTrace) {
mTimestamp = timestamp;
mPrivacyMode = privacyMode;
mSerialDebug = serialDebug;
mPrintWholeTrace = printWholeTrace;
- mInverterGap = inverterGap;
}
void addImportant(Inverter<> *iv, uint8_t cmd) {
@@ -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,259 @@ 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;
+ q->iv->radioStatistics.txCnt++;
+ mIsRetransmit = false;
+ if(NULL == q->iv->radio)
+ cmdDone(false); // can't communicate while radio is not defined!
+ mFirstTry = (INV_RADIO_TYPE_NRF == q->iv->ivRadioType) && (q->iv->isAvailable());
+ q->iv->mCmd = q->cmd;
+ q->iv->mIsSingleframeReq = false;
+ mFramesExpected = getFramesExpected(q); // function to get expected frame count.
+ mTimeout = DURATION_TXFRAME + mFramesExpected*DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType];
+ if((q->iv->ivGen == IV_MI) && ((q->cmd == MI_REQ_CH1) || (q->cmd == MI_REQ_4CH)))
+ incrAttempt(q->iv->channels); // 2 more attempts for 2ch, 4 more for 4ch
+
+ mState = States::START;
+ break;
- 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);
+ 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);
+ if(!mIsRetransmit && (q->cmd == AlarmData) || (q->cmd == GridOnProFilePara))
+ incrAttempt((q->cmd == AlarmData)? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA);
+
+ mIsRetransmit = false;
+ setAttempt();
+ mState = States::WAIT;
+ break;
- mState = States::WAIT;
- break;
+ case States::WAIT:
+ if (!q->iv->radio->mRadioWaitTime.isTimeout())
+ return;
+ mState = States::CHECK_FRAMES;
+ break;
- case States::WAIT:
- if (!mWaitTime.isTimeout())
- return;
- mState = States::CHECK_FRAMES;
- break;
+ case States::CHECK_FRAMES: {
+ if((q->iv->radio->mBufCtrl.empty() && !mIsRetransmit) ) { // || (0 == q->attempts)) { // radio buffer empty. No more answers will be checked later
+ if(*mSerialDebug) {
+ DPRINT_IVID(DBG_INFO, q->iv->id);
+ DBGPRINT(F("request timeout: "));
+ DBGPRINT(String(q->iv->radio->mRadioWaitTime.getRunTime()));
+ DBGPRINTLN(F("ms"));
+ }
- 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);
+ if(!q->iv->mGotFragment) {
+ if(INV_RADIO_TYPE_CMT == q->iv->ivRadioType) {
+ #if defined(ESP32)
+ if(!q->iv->radio->switchFrequency(q->iv, q->iv->radio->getBootFreqMhz() * 1000, (q->iv->config->frequency*FREQ_STEP_KHZ + q->iv->radio->getBaseFreqMhz() * 1000))) {
+ DPRINT_IVID(DBG_INFO, q->iv->id);
+ DBGPRINTLN(F("switch frequency failed!"));
+ }
+ mWaitTime.startTimeMonitor(1000);
+ #endif
+ } else {
+ mHeu.setIvRetriesBad(q->iv);
+ if(IV_MI == q->iv->ivGen)
+ q->iv->mIvTxCnt++;
+
+ if(mFirstTry) {
+ if(q->attempts < 3 || !q->iv->isProducing())
+ mFirstTry = false;
+ mHeu.evalTxChQuality(q->iv, false, 0, 0);
+ mHeu.getTxCh(q->iv);
+ //q->iv->radioStatistics.rxFailNoAnser++; // should only be one of fail or retransmit.
+ //q->iv->radioStatistics.txCnt--;
+ q->iv->radioStatistics.retransmits++;
+ q->iv->radio->mRadioWaitTime.stopTimeMonitor();
+ mState = States::START;
+ 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++;
+ 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++;
+ if(!mIsRetransmit && ((p->packet[9] == 0x02) || (p->packet[9] == 0x82)) && (p->millis < LIMIT_FAST_IV))
+ mHeu.setIvRetriesGood(q->iv,p->millis < LIMIT_VERYFAST_IV);
}
- } //else -> serial does not match
+ } 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) {
+ 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 {
+ if(q->iv->miMultiParts < 6) {
+ mState = States::WAIT;
+ if(q->iv->radio->mRadioWaitTime.isTimeout() && q->attempts) {
+ 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);
+ }
+ if(*mSerialDebug) {
+ DPRINT_IVID(DBG_INFO, q->iv->id);
+ DBGPRINTLN(F("Payload (MI got all)"));
}
+ closeRequest(q, true);
}
+ }
+ }
+ 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++;
}
- 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++;
+ 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) {
+ if(0 == q->attempts) {
+ DPRINT_IVID(DBG_INFO, q->iv->id);
+ DBGPRINTLN(F("timeout, no attempts left"));
+ closeRequest(q, false);
+ return;
+ }
+ //count missing frames
+ if(!q->iv->mIsSingleframeReq && (q->iv->ivRadioType == INV_RADIO_TYPE_NRF)) { // already checked?
+ uint8_t missedFrames = 0;
+ for(uint8_t i = 0; i < q->iv->radio->mFramesExpected; i++) {
+ if(mLocalBuf[i].len == 0)
+ missedFrames++;
+ }
+ if(missedFrames > 3 || (q->cmd == RealTimeRunData_Debug && missedFrames > 1) || ((missedFrames > 1) && ((missedFrames + 2) > q->attempts))) {
+ if(*mSerialDebug) {
+ DPRINT_IVID(DBG_INFO, q->iv->id);
+ DBGPRINT(String(missedFrames));
+ DBGPRINT(F(" frames missing "));
+ DBGPRINTLN(F("-> complete retransmit"));
}
+ mHeu.evalTxChQuality(q->iv, false, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
+ q->iv->radioStatistics.txCnt--;
+ q->iv->radioStatistics.retransmits++;
+ mCompleteRetry = true;
+ mState = States::RESET;
+ return;
}
}
- if(framnr) {
- setAttempt();
+ 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;
+ 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);
-
+ if(compilePayload(q)) {
if((NULL != mCbPayload) && (GridOnProFilePara != q->cmd) && (GetLossRate != q->cmd))
(mCbPayload)(q->cmd, q->iv);
closeRequest(q, true);
- break;
- }
- });
+ } else
+ closeRequest(q, false);
+
+ break;
+ }
}
- private:
inline void printRxInfo(const queue_s *q, packet_t *p) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("RX "));
@@ -267,7 +322,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 +347,61 @@ class Communication : public CommQueue<> {
}
}
- inline bool validateIvSerial(uint8_t buf[], Inverter<> *iv) {
+
+ 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(const uint8_t buf[], Inverter<> *iv) {
uint8_t tmp[4];
CP_U32_BigEndian(tmp, iv->radioId.u64 >> 8);
for(uint8_t i = 0; i < 4; i++) {
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;
}
}
@@ -342,28 +442,31 @@ class Communication : public CommQueue<> {
return true;
}
- inline bool parseMiFrame(packet_t *p, const queue_s *q) {
+ inline void parseMiFrame(packet_t *p, const queue_s *q) {
+ if((!mIsRetransmit && p->packet[9] == 0x00) && (p->millis < LIMIT_FAST_IV_MI)) //first frame is fast?
+ mHeu.setIvRetriesGood(q->iv,p->millis < LIMIT_VERYFAST_IV_MI);
if ((p->packet[0] == MI_REQ_CH1 + ALL_FRAMES)
|| (p->packet[0] == MI_REQ_CH2 + ALL_FRAMES)
|| ((p->packet[0] >= (MI_REQ_4CH + ALL_FRAMES))
&& (p->packet[0] < (0x39 + SINGLE_FRAME))
- )) { //&& (p->packet[0] != (0x0f + ALL_FRAMES)))) {
+ )) {
// small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39
- //mPayload[iv->id].txId = p->packet[0];
miDataDecode(p, q);
- } else if (p->packet[0] == (0x0f + ALL_FRAMES))
+ } 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;
miStsConsolidate(q, ((p->packet[0] == 0x88) ? 1 : 2), rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]);
- //mHeu.setGotFragment(q->iv); only do this when we are through the cycle?
}
-
- return true;
}
- inline bool parseDevCtrl(packet_t *p, const queue_s *q) {
+ inline bool parseDevCtrl(const packet_t *p, const queue_s *q) {
switch(p->packet[12]) {
case ActivePowerContr:
if(p->packet[13] != 0x00)
@@ -392,15 +495,16 @@ 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;
}
- inline void compilePayload(const queue_s *q) {
+ inline bool compilePayload(const queue_s *q) {
uint16_t crc = 0xffff, crcRcv = 0x0000;
for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(i == (mMaxFrameId - 1)) {
@@ -416,26 +520,22 @@ class Communication : public CommQueue<> {
DBGPRINT(F("CRC Error "));
if(q->attempts == 0) {
DBGPRINTLN(F("-> Fail"));
- closeRequest(q, false);
} else
DBGPRINTLN(F("-> complete retransmit"));
+ mCompleteRetry = true;
mState = States::RESET;
- return;
+ return false;
}
- /*DPRINT_IVID(DBG_INFO, q->iv->id);
- DBGPRINT(F("procPyld: cmd: 0x"));
- DBGHEXLN(q->cmd);*/
-
- memset(mPayload, 0, MAX_BUFFER);
+ mPayload.fill(0);
int8_t rssi = -127;
uint8_t len = 0;
for(uint8_t i = 0; i < mMaxFrameId; i++) {
if(mLocalBuf[i].len + len > MAX_BUFFER) {
DPRINTLN(DBG_ERROR, F("payload buffer to small!"));
- return;
+ return true;
}
memcpy(&mPayload[len], mLocalBuf[i].buf, mLocalBuf[i].len);
len += mLocalBuf[i].len;
@@ -446,30 +546,31 @@ class Communication : public CommQueue<> {
len -= 2;
- DPRINT_IVID(DBG_INFO, q->iv->id);
- DBGPRINT(F("Payload ("));
- DBGPRINT(String(len));
- if(*mPrintWholeTrace) {
- DBGPRINT(F("): "));
- ah::dumpBuf(mPayload, len);
- } else
- DBGPRINTLN(F(")"));
+ if(*mSerialDebug) {
+ DPRINT_IVID(DBG_INFO, q->iv->id);
+ DBGPRINT(F("Payload ("));
+ DBGPRINT(String(len));
+ if(*mPrintWholeTrace) {
+ DBGPRINT(F("): "));
+ ah::dumpBuf(mPayload.data(), len);
+ } else
+ DBGPRINTLN(F(")"));
+ }
if(GridOnProFilePara == q->cmd) {
- q->iv->addGridProfile(mPayload, len);
- return;
+ q->iv->addGridProfile(mPayload.data(), len);
+ return true;
}
record_t<> *rec = q->iv->getRecordStruct(q->cmd);
if(NULL == rec) {
if(GetLossRate == q->cmd) {
- q->iv->parseGetLossRate(mPayload, len);
- return;
- } else {
+ q->iv->parseGetLossRate(mPayload.data(), len);
+ return true;
+ } else
DPRINTLN(DBG_ERROR, F("record is NULL!"));
- closeRequest(q, false);
- }
- return;
+
+ return false;
}
if((rec->pyldLen != len) && (0 != rec->pyldLen)) {
if(*mSerialDebug) {
@@ -477,16 +578,15 @@ class Communication : public CommQueue<> {
DBGPRINT(String(rec->pyldLen));
DBGPRINTLN(F(" bytes"));
}
- /*q->iv->radioStatistics.rxFail++;*/
- closeRequest(q, false);
- return;
+ return false;
}
rec->ts = q->ts;
for (uint8_t i = 0; i < rec->length; i++) {
- q->iv->addValue(i, mPayload, rec);
+ q->iv->addValue(i, mPayload.data(), rec);
}
+ rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
q->iv->rssi = rssi;
q->iv->doCalculations();
@@ -494,37 +594,36 @@ class Communication : public CommQueue<> {
if(AlarmData == q->cmd) {
uint8_t i = 0;
while(1) {
- if(0 == q->iv->parseAlarmLog(i++, mPayload, len))
+ if(0 == q->iv->parseAlarmLog(i++, mPayload.data(), len))
break;
if (NULL != mCbAlarm)
(mCbAlarm)(q->iv);
yield();
}
}
+ return true;
}
void sendRetransmit(const queue_s *q, uint8_t i) {
- 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)
+ else if(q->iv->mGotFragment || mCompleteRetry)
q->iv->radioStatistics.rxFail++; // got no complete payload
else
q->iv->radioStatistics.rxFailNoAnser++; // got nothing
- mWaitTime.startTimeMonitor(*mInverterGap);
+ mWaitTime.startTimeMonitor(1); // maybe remove, side effects unknown
bool keep = false;
if(q->isDevControl)
@@ -535,7 +634,7 @@ class Communication : public CommQueue<> {
q->iv->mGotLastMsg = false;
q->iv->miMultiParts = 0;
mIsRetransmit = false;
- mFirstTry = false; // for correct reset
+ mCompleteRetry = false;
mState = States::RESET;
DBGPRINTLN(F("-----"));
}
@@ -577,21 +676,22 @@ class Communication : public CommQueue<> {
};
*/
- if ( p->packet[9] == 0x00 ) {//first frame
+ if ( p->packet[9] == 0x00 ) { //first frame
//FLD_FW_VERSION
for (uint8_t i = 0; i < 5; i++) {
q->iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1);
}
- q->iv->isConnected = true;
if(*mSerialDebug) {
DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("HW_VER is "));
DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25]));
}
- record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
+ rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec->ts = q->ts;
q->iv->setValue(1, rec, (uint32_t) ((p->packet[24] << 8) + p->packet[25])/1);
q->iv->miMultiParts +=4;
+ 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 ) {
@@ -605,9 +705,10 @@ class Communication : public CommQueue<> {
byte[23] to byte[26] Matching_APPFW_PN*/
DPRINT(DBG_INFO,F("HW_PartNo "));
DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13]));
- record_t<> *rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
+ rec = q->iv->getRecordStruct(InverterDevInform_Simple); // choose the record structure
rec->ts = q->ts;
q->iv->setValue(0, rec, (uint32_t) ((((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13])/1);
+ rec->mqttSentStatus = MqttSentStatus::NEW_DATA;
if(*mSerialDebug) {
DPRINT(DBG_INFO,F("HW_FB_TLmValue "));
@@ -650,22 +751,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,46 +820,37 @@ 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);
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
+ 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);
- }
}
void miNextRequest(uint8_t cmd, const queue_s *q) {
- incrAttempt(); // if function is called, we got something, and we necessarily need more transmissions for MI types...
- if(*mSerialDebug) {
- DPRINT_IVID(DBG_WARN, q->iv->id);
- DBGPRINT(F("next request ("));
- DBGPRINT(String(q->attempts));
- DBGPRINT(F(" attempts left): 0x"));
- DBGHEXLN(cmd);
- }
+ mHeu.evalTxChQuality(q->iv, true, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt);
+ mHeu.getTxCh(q->iv);
+ q->iv->radioStatistics.ivSent++;
- if(q->iv->miMultiParts == 7) {
- //mHeu.setGotAll(q->iv);
- 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;
+ if(*mSerialDebug) {
+ DPRINT_IVID(DBG_INFO, q->iv->id);
+ DBGPRINT(F("next: ("));
+ DBGPRINT(String(q->attempts));
+ DBGPRINT(F(" attempts left): 0x"));
+ DBGHEXLN(cmd);
+ }
mIsRetransmit = true;
chgCmd(cmd);
//mState = States::WAIT;
@@ -756,19 +858,17 @@ class Communication : public CommQueue<> {
void miRepeatRequest(const queue_s *q) {
setAttempt(); // if function is called, we got something, and we necessarily need more transmissions for MI types...
+ q->iv->radio->sendCmdPacket(q->iv, q->cmd, 0x00, true);
+ q->iv->radioStatistics.retransmits++;
+ q->iv->radio->mRadioWaitTime.startTimeMonitor(DURATION_TXFRAME + DURATION_ONEFRAME + duration_reserve[q->iv->ivRadioType]);
if(*mSerialDebug) {
- DPRINT_IVID(DBG_WARN, q->iv->id);
+ DPRINT_IVID(DBG_INFO, q->iv->id);
DBGPRINT(F("resend request ("));
DBGPRINT(String(q->attempts));
DBGPRINT(F(" attempts left): 0x"));
DBGHEXLN(q->cmd);
}
-
- q->iv->radio->sendCmdPacket(q->iv, q->cmd, 0x00, true);
-
- mWaitTime.startTimeMonitor(MI_TIMEOUT);
- //mState = States::WAIT;
- mIsRetransmit = false;
+ //mIsRetransmit = false;
}
void miStsConsolidate(const queue_s *q, uint8_t stschan, record_t<> *rec, uint8_t uState, uint8_t uEnum, uint8_t lState = 0, uint8_t lEnum = 0) {
@@ -791,20 +891,22 @@ class Communication : public CommQueue<> {
statusMi = 8310; //trick?
}
- uint16_t prntsts = statusMi == 3 ? 1 : statusMi;
+ uint16_t prntsts = (statusMi == 3) ? 1 : statusMi;
bool stsok = true;
if ( prntsts != rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)] ) { //sth.'s changed?
q->iv->alarmCnt = 1; // minimum...
stsok = false;
//sth is or was wrong?
- if ( (q->iv->type != INV_TYPE_1CH) && ( (statusMi != 3)
- || ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1)))
+ if ((q->iv->type != INV_TYPE_1CH)
+ && ((statusMi != 3)
+ || ((q->iv->lastAlarm[stschan].code) && (q->iv->lastAlarm[stschan].code != 1)))
) {
q->iv->lastAlarm[stschan+q->iv->type==INV_TYPE_2CH ? 2: 4] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts);
q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0);
q->iv->alarmCnt = q->iv->type == INV_TYPE_2CH ? 3 : 5;
- } else if ( (q->iv->type == INV_TYPE_1CH) && ( (statusMi != 3)
- || ((q->iv->lastAlarm[stschan].code) && (statusMi == 3) && (q->iv->lastAlarm[stschan].code != 1)))
+ } else if ((q->iv->type == INV_TYPE_1CH)
+ && ( (statusMi != 3)
+ || ((q->iv->lastAlarm[stschan].code) && (q->iv->lastAlarm[stschan].code != 1)))
) {
q->iv->lastAlarm[stschan] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts);
} else if (q->iv->type == INV_TYPE_1CH)
@@ -833,6 +935,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)]) {
@@ -847,10 +951,31 @@ class Communication : public CommQueue<> {
void miComplete(Inverter<> *iv) {
- if (*mSerialDebug) {
- DPRINT_IVID(DBG_INFO, iv->id);
- DBGPRINTLN(F("got all data msgs"));
+ if (iv->mGetLossInterval >= AHOY_GET_LOSS_INTERVAL) { // initially mIvRxCnt = mIvTxCnt = 0
+ iv->mGetLossInterval = 1;
+ iv->radioStatistics.ivSent = iv->mIvRxCnt + iv->mDtuTxCnt; // iv->mIvRxCnt is the nr. of additional answer frames, default we expect one frame per request
+ iv->radioStatistics.ivLoss = iv->radioStatistics.ivSent - iv->mDtuRxCnt; // this is what we didn't receive
+ iv->radioStatistics.dtuLoss = iv->mIvTxCnt; // this is somehow the requests w/o answers in that periode
+ iv->radioStatistics.dtuSent = iv->mDtuTxCnt;
+ if (*mSerialDebug) {
+ DPRINT_IVID(DBG_INFO, iv->id);
+ DBGPRINT(F("DTU loss: ") +
+ String (iv->radioStatistics.ivLoss) + F("/") +
+ String (iv->radioStatistics.ivSent) + F(" frames for ") +
+ String (iv->radioStatistics.dtuSent) + F(" requests"));
+ if(iv->mAckCount) {
+ DBGPRINT(F(". ACKs: "));
+ DBGPRINTLN(String(iv->mAckCount));
+ iv->mAckCount = 0;
+ } else
+ DBGPRINTLN(F(""));
+ }
+ iv->mIvRxCnt = 0; // start new interval, iVRxCnt is abused to collect additional possible frames
+ iv->mIvTxCnt = 0; // start new interval, iVTxCnt is abused to collect nr. of unanswered requests
+ 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 +996,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:
@@ -897,22 +1017,23 @@ class Communication : public CommQueue<> {
private:
States mState = States::RESET;
- uint32_t *mTimestamp;
- bool *mPrivacyMode, *mSerialDebug, *mPrintWholeTrace;
- uint16_t *mInverterGap;
+ uint32_t *mTimestamp = nullptr;
+ bool *mPrivacyMode = nullptr, *mSerialDebug = nullptr, *mPrintWholeTrace = nullptr;
TimeMonitor mWaitTime = TimeMonitor(0, true); // start as expired (due to code in RESET state)
std::array mLocalBuf;
- bool mFirstTry = false; // see, if we should do a second try
- bool mIsRetransmit = false; // we already had waited one complete cycle
- uint8_t mMaxFrameId;
- uint8_t mPayload[MAX_BUFFER];
+ bool mFirstTry = false; // see, if we should do a second try
+ bool mCompleteRetry = false; // remember if we did request a complete retransmission
+ bool mIsRetransmit = false; // we already had waited one complete cycle
+ uint8_t mMaxFrameId = 0;
+ uint8_t mFramesExpected = 12; // 0x8c was highest last frame for alarm data
+ uint16_t mTimeout = 0; // calculating that once should be ok
+ std::array mPayload;
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/Heuristic.h b/src/hm/Heuristic.h
index 64c78cc8..ecf82aa7 100644
--- a/src/hm/Heuristic.h
+++ b/src/hm/Heuristic.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://github.com/lumpapu/ahoy
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -23,8 +23,8 @@
class Heuristic {
public:
uint8_t getTxCh(Inverter<> *iv) {
- if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen))
- return 0; // not used for these inverter types
+ if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
+ return 0; // not used for other than nRF inverter types
HeuristicInv *ih = &iv->heuristics;
@@ -66,6 +66,8 @@ class Heuristic {
ih->testPeriodFailCnt = 0;
}
+ iv->radio->mTxRetriesNext = getIvRetries(iv);
+
return id2Ch(ih->txRfChId);
}
@@ -130,7 +132,7 @@ class Heuristic {
ih->lastRxFragments = rxFragments;
}
- void printStatus(Inverter<> *iv) {
+ void printStatus(const Inverter<> *iv) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("Radio infos:"));
if((IV_HMS != iv->ivGen) && (IV_HMT != iv->ivGen)) {
@@ -155,8 +157,50 @@ class Heuristic {
DBGPRINTLN(String(iv->config->powerLevel));
}
+ uint8_t getIvRetries(const Inverter<> *iv) const {
+ if(iv->heuristics.rxSpeeds[0])
+ return RETRIES_VERYFAST_IV;
+ if(iv->heuristics.rxSpeeds[1])
+ return RETRIES_FAST_IV;
+ return 15;
+ }
+
+ void setIvRetriesGood(Inverter<> *iv, bool veryGood) {
+ if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
+ return; // not used for other than nRF inverter types
+
+ if(iv->heuristics.rxSpeedCnt[veryGood] > 9)
+ return;
+ iv->heuristics.rxSpeedCnt[veryGood]++;
+ iv->heuristics.rxSpeeds[veryGood] = true;
+ }
+
+ void setIvRetriesBad(Inverter<> *iv) {
+ if(iv->ivRadioType != INV_RADIO_TYPE_NRF)
+ return; // not used for other than nRF inverter types
+
+ if(iv->heuristics.rxSpeedCnt[0]) {
+ iv->heuristics.rxSpeedCnt[0]--;
+ return;
+ }
+ if(iv->heuristics.rxSpeeds[0]) {
+ iv->heuristics.rxSpeeds[0] = false;
+ return;
+ }
+
+ if(iv->heuristics.rxSpeedCnt[1]) {
+ iv->heuristics.rxSpeedCnt[1]--;
+ return;
+ }
+ if(iv->heuristics.rxSpeeds[1]) {
+ iv->heuristics.rxSpeeds[1] = false;
+ return;
+ }
+ return;
+ }
+
private:
- bool isNewTxCh(HeuristicInv *ih) {
+ bool isNewTxCh(const HeuristicInv *ih) const {
return ih->txRfChId != ih->lastBestTxChId;
}
@@ -178,9 +222,6 @@ class Heuristic {
}
return 3; // standard
}
-
- private:
- uint8_t mChList[5] = {03, 23, 40, 61, 75};
};
diff --git a/src/hm/HeuristicInv.h b/src/hm/HeuristicInv.h
index e7ad6edd..913732bc 100644
--- a/src/hm/HeuristicInv.h
+++ b/src/hm/HeuristicInv.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://github.com/lumpapu/ahoy
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -14,7 +14,27 @@
class HeuristicInv {
public:
HeuristicInv() {
- memset(txRfQuality, -6, RF_MAX_CHANNEL_ID);
+ clear();
+ }
+
+ void clear() {
+ memset(txRfQuality, 0, RF_MAX_CHANNEL_ID);
+ txRfChId = 0;
+ lastBestTxChId = 0;
+ testPeriodSendCnt = 0;
+ testPeriodFailCnt = 0;
+ testChId = 0;
+ saveOldTestQuality = -6;
+ lastRxFragments = 0;
+
+ rxSpeeds[0] = false;
+ rxSpeeds[1] = false;
+ rxSpeedCnt[0] = 0;
+ rxSpeedCnt[1] = 0;
+ }
+
+ bool isTxAtMax(void) const {
+ return (RF_MAX_QUALITY == txRfQuality[txRfChId]);
}
public:
@@ -27,6 +47,8 @@ class HeuristicInv {
uint8_t testChId = 0;
int8_t saveOldTestQuality = -6;
uint8_t lastRxFragments = 0;
+ bool rxSpeeds[2] = {false, false}; // is inverter responding very fast respective fast?
+ uint8_t rxSpeedCnt[2] = {0, 0}; // count how many messages had been received very fast respective fast (10 max)
};
#endif /*__HEURISTIC_INV_H__*/
diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h
index 55259289..6ba92774 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,15 +76,22 @@ 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};
-
-#define WORK_FREQ_KHZ 865000 // desired work frequency between DTU and
- // inverter in kHz
-#define HOY_BASE_FREQ_KHZ 860000 // in kHz
-#define HOY_MAX_FREQ_KHZ 923500 // 0xFE * 250kHz + Base_freq
-#define HOY_BOOT_FREQ_KHZ 868000 // Hoymiles boot/init frequency after power up inverter
-#define FREQ_STEP_KHZ 250 // channel step size in kHz
-#define FREQ_WARN_MIN_KHZ 863000 // for EU 863 - 870 MHz is allowed
-#define FREQ_WARN_MAX_KHZ 870000 // for EU 863 - 870 MHz is allowed
+enum {INV_RADIO_TYPE_UNKNOWN = 0, INV_RADIO_TYPE_NRF, INV_RADIO_TYPE_CMT};
+
+
+#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] = {65, 115};
+
+#define LIMIT_FAST_IV 85 // time limit to qualify an inverter as very fast answering inverter
+#define LIMIT_VERYFAST_IV 70 // time limit to qualify an inverter as very fast answering inverter
+#define LIMIT_FAST_IV_MI 35 // time limit to qualify a MI type inverter as fast answering inverter
+#define LIMIT_VERYFAST_IV_MI 25 // time limit to qualify a MI type inverter as very fast answering inverter
+#define RETRIES_FAST_IV 12 // how often shall a message be automatically retransmitted by the nRF (fast answering inverter)
+#define RETRIES_VERYFAST_IV 9 // how often shall a message be automatically retransmitted by the nRF (very fast answering inverter)
typedef struct {
diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h
index 776b9c6f..50b4267c 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 = nullptr; // assignment of bytes in payload
+ uint8_t length = 0; // length of the assignment list
+ T *record = nullptr; // data pointer
+ uint32_t ts = 0; // timestamp of last received payload
+ uint8_t pyldLen = 0; // expected payload length for plausibility check
+ MqttSentStatus mqttSentStatus = MqttSentStatus:: NEW_DATA; // indicates the current MqTT sent status
};
struct alarm_t {
@@ -94,129 +110,112 @@ 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)
- 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
- float actPowerLimit; // actual power limit
- bool powerLimitAck; // acknowledged power limit (default: false)
- uint8_t devControlCmd; // carries the requested cmd
- serial_u radioId; // id converted to modbus
- uint8_t channels; // number of PV channels (1-4)
- record_t recordMeas; // structure for measured values
- record_t recordInfo; // structure for info values
- record_t recordHwInfo; // structure for simple (hardware) info values
- record_t recordConfig; // structure for system config values
- record_t recordAlarm; // structure for alarm values
- bool isConnected; // shows if inverter was successfully identified (fw version and hardware info)
- InverterStatus status; // indicates the current inverter status
- std::array lastAlarm; // holds last 10 alarms
- uint8_t alarmNxtWrPos; // indicates the position in array (rolling buffer)
- uint16_t alarmCnt; // counts the total number of occurred alarms
- uint16_t alarmLastId; // lastId which was received
- int8_t rssi; // RSSI
- 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
-
- 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 cfgInst_t *generalConfig; // general inverter configuration from setup
+ uint8_t ivGen = IV_UNKNOWN; // generation of inverter (HM / MI)
+ uint8_t ivRadioType = INV_RADIO_TYPE_UNKNOWN; // refers to used radio (nRF24 / CMT)
+ cfgIv_t *config = nullptr; // stored settings
+ uint8_t id = 0; // unique id
+ uint8_t type = INV_TYPE_1CH; // integer which refers to inverter type
+ uint16_t alarmMesIndex = 0; // Last recorded Alarm Message Index
+ uint16_t powerLimit[2] = {0xffff, AbsolutNonPersistent}; // limit power output (multiplied by 10)
+ uint16_t actPowerLimit = 0xffff; // actual power limit
+ bool powerLimitAck = false; // acknowledged power limit
+ uint8_t devControlCmd = InitDataState; // carries the requested cmd
+ serial_u radioId; // id converted to modbus
+ uint8_t channels = 1; // number of PV channels (1-4)
+ record_t recordMeas; // structure for measured values
+ record_t recordInfo; // structure for info values
+ record_t recordHwInfo; // structure for simple (hardware) info values
+ record_t recordConfig; // structure for system config values
+ record_t recordAlarm; // structure for alarm values
+ InverterStatus status = InverterStatus::OFF; // indicates the current inverter status
+ std::array lastAlarm; // holds last 10 alarms
+ int8_t rssi = 0; // RSSI
+ uint16_t alarmCnt = 0; // counts the total number of occurred alarms
+ uint16_t alarmLastId = 0; // lastId which was received
+ uint8_t mCmd = InitDataState; // holds the command to send
+ bool mGotFragment = false; // shows if inverter has sent at least one fragment
+ uint8_t miMultiParts = 0; // helper info for MI multiframe msgs
+ uint8_t outstandingFrames = 0; // helper info to count difference between expected and received frames
+ uint8_t curFrmCnt = 0; // count received frames in current loop
+ bool mGotLastMsg = false; // shows if inverter has already finished transmission cycle
+ bool mIsSingleframeReq = false; // indicates this is a missing single frame request
+ Radio *radio = nullptr; // pointer to associated radio class
+ statistics_t radioStatistics; // information about transmitted, failed, ... packets
+ HeuristicInv heuristics; // heuristic information / logic
+ uint8_t curCmtFreq = 0; // current used CMT frequency, used to check if freq. was changed during runtime
+ uint32_t tsMaxAcPower = 0; // holds the timestamp when the MaxAC power was seen
+ bool commEnabled = true; // 'pause night communication' sets this field to false
- Inverter() {
- ivGen = IV_HM;
- powerLimit[0] = 0xffff; // 65535 W Limit -> unlimited
- powerLimit[1] = AbsolutNonPersistent; // default power limit setting
- powerLimitAck = false;
- actPowerLimit = 0xffff; // init feedback from inverter to -1
- mDevControlRequest = false;
- devControlCmd = InitDataState;
- alarmMesIndex = 0;
- isConnected = false;
- status = InverterStatus::OFF;
- alarmNxtWrPos = 0;
- alarmCnt = 0;
- alarmLastId = 0;
- rssi = -127;
- miMultiParts = 0;
- mGotLastMsg = false;
- mCmd = InitDataState;
- mIsSingleframeReq = false;
- radio = NULL;
- commEnabled = true;
- mIvRxCnt = 0;
- mIvTxCnt = 0;
- mDtuRxCnt = 0;
- mDtuTxCnt = 0;
+ public:
+ Inverter() {
memset(&radioStatistics, 0, sizeof(statistics_t));
- memset(heuristics.txRfQuality, -6, 5);
-
memset(mOffYD, 0, sizeof(float) * 6);
memset(mLastYD, 0, sizeof(float) * 6);
+ mGridProfile.fill(0);
}
void tickSend(std::function cb) {
if(mDevControlRequest) {
- cb(devControlCmd, true);
+ if(InverterStatus::OFF != status) {
+ cb(devControlCmd, true);
+ devControlCmd = InitDataState;
+ } else
+ DPRINTLN(DBG_WARN, F("Inverter is not avail"));
mDevControlRequest = false;
- } else if (IV_MI != ivGen) {
+ } 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)
+ if(INV_RADIO_TYPE_NRF == ivRadioType) {
+ // get live data until quality reaches maximum
+ if(!heuristics.isTxAtMax()) {
+ cb(RealTimeRunData_Debug, false); // get live data
+ return;
+ }
+ }
+
+ if(actPowerLimit == 0xffff) {
cb(SystemConfigPara, false); // power limit info
- else if(InitDataState != devControlCmd) {
+ } else if(InitDataState != devControlCmd) {
cb(devControlCmd, false); // custom command which was received by API
devControlCmd = InitDataState;
mGetLossInterval = 1;
+ return;
+ } else if(0 == getFwVersion()) {
+ cb(InverterDevInform_All, false); // get firmware version
+ } else if(0 == getHwVersion()) {
+ cb(InverterDevInform_Simple, false); // get hardware version
+ } else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) {
+ cb(AlarmData, false); // get last alarms
} else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile
cb(GridOnProFilePara, false);
} else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate
mGetLossInterval = 1;
+ cb(RealTimeRunData_Debug, false); // get live data
cb(GetLossRate, false);
- } else
- 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 {
- record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
- if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0)
- cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
- else
- cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false);
+ return;
+ }
+
+ cb(RealTimeRunData_Debug, false); // get live data
+ } else { // MI
+ cb(((type == INV_TYPE_4CH) ? MI_REQ_4CH : MI_REQ_CH1), false);
+ mGetLossInterval++;
+ if (type != INV_TYPE_4CH)
+ mIvRxCnt++; // statistics workaround...
+ if(isAvailable()) {
+ if(0 == getFwVersion()) {
+ mIvRxCnt +=2;
+ cb(0x0f, false); // get firmware version; for MI, this makes part of polling the device software and hardware version number
+ } else {
+ record_t<> *rec = getRecordStruct(InverterDevInform_Simple);
+ if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) {
+ cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1
+ mIvRxCnt +=2;
+ } else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile
+ cb(0x10, false); // legacy GPF command
+ }
}
}
}
@@ -234,15 +233,14 @@ class Inverter {
uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld"));
- uint8_t pos = 0;
if(NULL != rec) {
+ uint8_t pos = 0;
for(; pos < rec->length; pos++) {
if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
break;
}
return (pos >= rec->length) ? 0xff : pos;
- }
- else
+ } else
return 0xff;
}
@@ -251,41 +249,39 @@ class Inverter {
}
const char *getFieldName(uint8_t pos, record_t<> *rec) {
- DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getFieldName"));
if(NULL != rec)
return fields[rec->assign[pos].fieldId];
return notAvail;
}
const char *getUnit(uint8_t pos, record_t<> *rec) {
- DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getUnit"));
if(NULL != rec)
return units[rec->assign[pos].unitId];
return notAvail;
}
uint8_t getChannel(uint8_t pos, record_t<> *rec) {
- DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getChannel"));
if(NULL != rec)
return rec->assign[pos].ch;
return 0;
}
bool setDevControlRequest(uint8_t cmd) {
- if(isConnected) {
+ if(InverterStatus::OFF != status) {
mDevControlRequest = true;
devControlCmd = cmd;
+ //app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)"
}
- return isConnected;
+ return (InverterStatus::OFF != status);
}
bool setDevCommand(uint8_t cmd) {
- if(isConnected)
+ if(InverterStatus::OFF != status)
devControlCmd = cmd;
- return isConnected;
+ return (InverterStatus::OFF != status);
}
- void addValue(uint8_t pos, uint8_t buf[], record_t<> *rec) {
+ void addValue(uint8_t pos, const uint8_t buf[], record_t<> *rec) {
DPRINTLN(DBG_VERBOSE, F("hmInverter.h:addValue"));
if(NULL != rec) {
uint8_t ptr = rec->assign[pos].start;
@@ -299,6 +295,7 @@ class Inverter {
val <<= 8;
val |= buf[ptr];
} while(++ptr != end);
+
if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) {
// temperature, Qvar, and power factor are a signed values
rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div);
@@ -322,7 +319,6 @@ class Inverter {
if(rec == &recordMeas) {
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]) {
@@ -332,31 +328,26 @@ class Inverter {
DBGPRINTLN(String(alarmMesIndex));
}
}
+ } else {
+ if (rec->assign == InfoAssignment) {
+ DPRINTLN(DBG_DEBUG, "add info");
+ // eg. fw version ...
+ } 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 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
+ } else
DPRINTLN(DBG_ERROR, F("addValue: assignment not found with cmd 0x"));
// update status state-machine
@@ -374,18 +365,18 @@ class Inverter {
}
REC_TYP getChannelFieldValue(uint8_t channel, uint8_t fieldId, record_t<> *rec) {
- uint8_t pos = 0;
if(NULL != rec) {
+ uint8_t pos = 0;
for(; pos < rec->length; pos++) {
if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId))
break;
}
+
if(pos >= rec->length)
return 0;
return rec->record[pos];
- }
- else
+ } else
return 0;
}
@@ -433,11 +424,14 @@ class Inverter {
status = InverterStatus::STARTING;
} else {
if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) {
- status = InverterStatus::OFF;
- actPowerLimit = 0xffff; // power limit will be read once inverter becomes available
- alarmMesIndex = 0;
- }
- else
+ if(status != InverterStatus::OFF) {
+ status = InverterStatus::OFF;
+ actPowerLimit = 0xffff; // power limit will be read once inverter becomes available
+ alarmMesIndex = 0;
+ if(INV_RADIO_TYPE_NRF == ivRadioType)
+ heuristics.clear();
+ }
+ } else
status = InverterStatus::WAS_ON;
}
@@ -455,6 +449,7 @@ class Inverter {
else if(InverterStatus::PRODUCING == status)
status = InverterStatus::WAS_PRODUCING;
}
+
return producing;
}
@@ -506,16 +501,17 @@ 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) {
if((IV_HM == ivGen) || (IV_MI == ivGen)) {
rec->length = (uint8_t)(HM1CH_LIST_LEN);
- rec->assign = (byteAssign_t *)hm1chAssignment;
+ rec->assign = reinterpret_cast(const_cast(hm1chAssignment));
rec->pyldLen = HM1CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS1CH_LIST_LEN);
- rec->assign = (byteAssign_t *)hms1chAssignment;
+ rec->assign = reinterpret_cast(const_cast(hms1chAssignment));
rec->pyldLen = HMS1CH_PAYLOAD_LEN;
}
channels = 1;
@@ -523,11 +519,11 @@ class Inverter {
else if (INV_TYPE_2CH == type) {
if((IV_HM == ivGen) || (IV_MI == ivGen)) {
rec->length = (uint8_t)(HM2CH_LIST_LEN);
- rec->assign = (byteAssign_t *)hm2chAssignment;
+ rec->assign = reinterpret_cast(const_cast(hm2chAssignment));
rec->pyldLen = HM2CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS2CH_LIST_LEN);
- rec->assign = (byteAssign_t *)hms2chAssignment;
+ rec->assign = reinterpret_cast(const_cast(hms2chAssignment));
rec->pyldLen = HMS2CH_PAYLOAD_LEN;
}
channels = 2;
@@ -535,18 +531,18 @@ class Inverter {
else if (INV_TYPE_4CH == type) {
if((IV_HM == ivGen) || (IV_MI == ivGen)) {
rec->length = (uint8_t)(HM4CH_LIST_LEN);
- rec->assign = (byteAssign_t *)hm4chAssignment;
+ rec->assign = reinterpret_cast(const_cast(hm4chAssignment));
rec->pyldLen = HM4CH_PAYLOAD_LEN;
} else if(IV_HMS == ivGen) {
rec->length = (uint8_t)(HMS4CH_LIST_LEN);
- rec->assign = (byteAssign_t *)hms4chAssignment;
+ rec->assign = reinterpret_cast(const_cast(hms4chAssignment));
rec->pyldLen = HMS4CH_PAYLOAD_LEN;
}
channels = 4;
}
else if (INV_TYPE_6CH == type) {
rec->length = (uint8_t)(HMT6CH_LIST_LEN);
- rec->assign = (byteAssign_t *)hmt6chAssignment;
+ rec->assign = reinterpret_cast(const_cast(hmt6chAssignment));
rec->pyldLen = HMT6CH_PAYLOAD_LEN;
channels = 6;
}
@@ -559,22 +555,22 @@ class Inverter {
break;
case InverterDevInform_All:
rec->length = (uint8_t)(HMINFO_LIST_LEN);
- rec->assign = (byteAssign_t *)InfoAssignment;
+ rec->assign = reinterpret_cast(const_cast(InfoAssignment));
rec->pyldLen = HMINFO_PAYLOAD_LEN;
break;
case InverterDevInform_Simple:
rec->length = (uint8_t)(HMSIMPLE_INFO_LIST_LEN);
- rec->assign = (byteAssign_t *)SimpleInfoAssignment;
+ rec->assign = reinterpret_cast(const_cast(SimpleInfoAssignment));
rec->pyldLen = HMSIMPLE_INFO_PAYLOAD_LEN;
break;
case SystemConfigPara:
rec->length = (uint8_t)(HMSYSTEM_LIST_LEN);
- rec->assign = (byteAssign_t *)SystemConfigParaAssignment;
+ rec->assign = reinterpret_cast(const_cast(SystemConfigParaAssignment));
rec->pyldLen = HMSYSTEM_PAYLOAD_LEN;
break;
case AlarmData:
rec->length = (uint8_t)(HMALARMDATA_LIST_LEN);
- rec->assign = (byteAssign_t *)AlarmDataAssignment;
+ rec->assign = reinterpret_cast(const_cast(AlarmDataAssignment));
rec->pyldLen = HMALARMDATA_PAYLOAD_LEN;
break;
default:
@@ -590,7 +586,7 @@ class Inverter {
void resetAlarms() {
lastAlarm.fill({0, 0, 0});
- alarmNxtWrPos = 0;
+ mAlarmNxtWrPos = 0;
alarmCnt = 0;
alarmLastId = 0;
@@ -598,26 +594,49 @@ class Inverter {
memset(mLastYD, 0, sizeof(float) * 6);
}
- bool parseGetLossRate(uint8_t pyld[], uint8_t len) {
+ bool parseGetLossRate(const uint8_t pyld[], uint8_t len) {
if (len == HMGETLOSSRATE_PAYLOAD_LEN) {
uint16_t rxCnt = (pyld[0] << 8) + pyld[1];
uint16_t txCnt = (pyld[2] << 8) + pyld[3];
- 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;
}
@@ -774,7 +793,7 @@ class Inverter {
void addGridProfile(uint8_t buf[], uint8_t length) {
mGridLen = (length > MAX_GRID_LENGTH) ? MAX_GRID_LENGTH : length;
- std::copy(buf, &buf[mGridLen], mGridProfile);
+ std::copy(buf, &buf[mGridLen], mGridProfile.data());
}
String getGridProfile(void) {
@@ -789,9 +808,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) {
@@ -804,11 +823,23 @@ class Inverter {
radioId.b[0] = 0x01;
}
+ public:
+ static uint32_t *timestamp; // system timestamp
+ static cfgInst_t *generalConfig; // general inverter configuration from setup
+
+ uint16_t mDtuRxCnt = 0;
+ uint16_t mDtuTxCnt = 0;
+ uint8_t mGetLossInterval = 0; // request iv every AHOY_GET_LOSS_INTERVAL RealTimeRunData_Debug
+ uint16_t mIvRxCnt = 0;
+ uint16_t mIvTxCnt = 0;
+ uint16_t mAckCount = 0;
+
private:
float mOffYD[6], mLastYD[6];
- bool mDevControlRequest; // true if change needed
+ bool mDevControlRequest = false; // true if change needed
uint8_t mGridLen = 0;
- uint8_t mGridProfile[MAX_GRID_LENGTH];
+ std::array mGridProfile;
+ uint8_t mAlarmNxtWrPos = 0; // indicates the position in array (rolling buffer)
};
template
@@ -920,8 +951,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..d1d24364 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
//-----------------------------------------------------------------------------
@@ -15,7 +15,6 @@
#endif
#define SPI_SPEED 1000000
-
#define RF_CHANNELS 5
const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"};
@@ -48,12 +47,12 @@ class HmRadio : public Radio {
pinMode(irq, INPUT_PULLUP);
- mSerialDebug = serialDebug;
- mPrivacyMode = privacyMode;
+ mSerialDebug = serialDebug;
+ mPrivacyMode = privacyMode;
mPrintWholeTrace = printWholeTrace;
generateDtuSn();
- DTU_RADIO_ID = ((uint64_t)(((mDtuSn >> 24) & 0xFF) | ((mDtuSn >> 8) & 0xFF00) | ((mDtuSn << 8) & 0xFF0000) | ((mDtuSn << 24) & 0xFF000000)) << 8) | 0x01;
+ mDtuRadioId = ((uint64_t)(((mDtuSn >> 24) & 0xFF) | ((mDtuSn >> 8) & 0xFF00) | ((mDtuSn << 8) & 0xFF0000) | ((mDtuSn << 24) & 0xFF000000)) << 8) | 0x01;
#ifdef ESP32
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
@@ -78,21 +77,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, 9); // 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->openReadingPipe(1, reinterpret_cast(&mDtuRadioId));
+ mNrf24->maskIRQ(false, false, false); // enable all receiving interrupts
mNrf24->setPALevel(1); // low is default
if(mNrf24->isChipConnected()) {
@@ -104,70 +98,101 @@ 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) override {
+ 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;
+ rx_ready = 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_TXFRAME+DURATION_ONEFRAME)))
+ mNRFloopChannels = true;
- if (getReceived()) { // everything received
- return;
- }
+ mRxPendular = !mRxPendular;
+ 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 + mRxPendular*4) % RF_CHANNELS;
+
+ mNrf24->setChannel(mRfChLst[tempRxChIdx]);
+ isRxInit = false;
- startMicros = micros();
+ 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();
+
+ 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++;
+
+ rxOffset = mLastIv->ivGen == IV_HM ? 3 : 2; // holds the default channel offset between tx and rx channel (nRF only)
+ mRxChIdx = (mTxChIdx + rxOffset) % RF_CHANNELS;
+ mNrf24->setChannel(mRfChLst[mRxChIdx]);
+ mNrf24->startListening();
+ mTimeslotStart = millis();
+ tempRxChIdx = mRxChIdx; // might be better to start off with one channel less?
+ mRxPendular = false;
+ mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1);
+ innerLoopTimeout = DURATION_LISTEN_MIN;
+ }
+
+ if(rx_ready) {
+ if (getReceived()) { // check what we got, returns true for last package or success for single frame request
+ mNRFisInRX = false;
+ mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first end his transmissions
+ mNrf24->stopListening();
+ } else {
+ innerLoopTimeout = DURATION_LISTEN_MIN;
+ mTimeslotStart = millis();
+ if (!mNRFloopChannels) {
+ if (isRxInit) {
+ isRxInit = false;
+ tempRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
+ mNrf24->setChannel(mRfChLst[tempRxChIdx]);
+ } else
+ mRxChIdx = tempRxChIdx;
+ }
+ }
+ rx_ready = false; // reset
+ return mNRFisInRX;
}
- // switch to next RX channel
- mRxChIdx = (mRxChIdx + 4) % RF_CHANNELS;
- mNrf24->setChannel(mRfChLst[mRxChIdx]);
- innerLoopTimeout = 4088;
- isRxInit = false;
}
- // not finished but time is over
- return;
+ return false;
}
- bool isChipConnected(void) {
- //DPRINTLN(DBG_VERBOSE, F("hmRadio.h:isChipConnected"));
+ bool isChipConnected(void) const override {
return mNrf24->isChipConnected();
}
- void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) {
+ void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("sendControlPacket cmd: "));
DBGHEXLN(cmd);
@@ -177,10 +202,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,59 +274,72 @@ class HmRadio : public Radio {
}
cnt++;
}
+
sendPacket(iv, cnt, isRetransmit, (IV_MI != iv->ivGen));
}
- uint8_t getDataRate(void) {
+ uint8_t getDataRate(void) const {
if(!mNrf24->isChipConnected())
return 3; // unknown
return mNrf24->getDataRate();
}
- bool isPVariant(void) {
+ bool isPVariant(void) const {
return mNrf24->isPVariant();
}
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;
+ bool isRetransmitAnswer = 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
+ DPRINT(DBG_WARN, F("RX other inverter "));
+ if(!*mPrivacyMode)
ah::dumpBuf(p.packet, p.len);
- return false;
+ else
+ DBGPRINTLN(F(""));
+ } 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(mLastIv->mIsSingleframeReq) // we only expect one frame here...
+ isRetransmitAnswer = true;
+
+ if(isLastPackage)
+ setExpectedFrames(p.packet[9] - ALL_FRAMES);
+ }
+
+ if(IV_MI == mLastIv->ivGen) {
+ if (p.packet[0] == (0x0f + ALL_FRAMES)) // response from MI get information command
+ 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();
}
if(isLastPackage)
mLastIv->mGotLastMsg = true;
- return isLastPackage;
+ return isLastPackage || isRetransmitAnswer;
}
void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) {
@@ -312,17 +350,27 @@ 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));
DBGPRINT(" CH");
+ if(mTxChIdx == 0)
+ DBGPRINT("0");
DBGPRINT(String(mRfChLst[mTxChIdx]));
- DBGPRINT(F(" | "));
+ DBGPRINT(F(", "));
+ DBGPRINT(String(mTxRetriesNext));
+ DBGPRINT(F(" ret. | "));
if(*mPrintWholeTrace) {
if(*mPrivacyMode)
- ah::dumpBuf(mTxBuf, len, 1, 4);
+ ah::dumpBuf(mTxBuf.data(), len, 1, 4);
else
- ah::dumpBuf(mTxBuf, len);
+ ah::dumpBuf(mTxBuf.data(), len);
} else {
DHEX(mTxBuf[0]);
DBGPRINT(F(" "));
@@ -333,24 +381,30 @@ class HmRadio : public Radio {
}
mNrf24->stopListening();
+ mNrf24->flush_rx();
+ if(!isRetransmit && (mTxRetries != mTxRetriesNext)) {
+ mNrf24->setRetries(3, mTxRetriesNext);
+ mTxRetries = mTxRetriesNext;
+ }
mNrf24->setChannel(mRfChLst[mTxChIdx]);
mNrf24->openWritingPipe(reinterpret_cast(&iv->radioId.u64));
- mNrf24->startWrite(mTxBuf, len, false); // false = request ACK response
+ mNrf24->startFastWrite(mTxBuf.data(), len, false, true); // false (3) = request ACK response; true (4) reset CE to high after transmission
mMillis = millis();
mLastIv = iv;
iv->mDtuTxCnt++;
+ mNRFisInRX = false;
}
- uint64_t getIvId(Inverter<> *iv) {
+ uint64_t getIvId(Inverter<> *iv) const override {
return iv->radioId.u64;
}
- uint8_t getIvGen(Inverter<> *iv) {
+ uint8_t getIvGen(Inverter<> *iv) const override {
return iv->ivGen;
}
- inline bool checkIvSerial(uint8_t buf[], Inverter<> *iv) {
+ inline bool checkIvSerial(const uint8_t buf[], Inverter<> *iv) {
for(uint8_t i = 1; i < 5; i++) {
if(buf[i] != iv->radioId.b[i])
return false;
@@ -358,12 +412,23 @@ class HmRadio : public Radio {
return true;
}
- uint64_t DTU_RADIO_ID;
- uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz
+ uint64_t mDtuRadioId = 0ULL;
+ const uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz
uint8_t mTxChIdx = 0;
uint8_t mRxChIdx = 0;
+ uint8_t tempRxChIdx = 0;
bool mGotLastMsg = false;
- uint32_t mMillis;
+ uint32_t mMillis = 0;
+ bool tx_ok = false, tx_fail = false, rx_ready = false;
+ unsigned long mTimeslotStart = 0;
+ unsigned long mLastIrqTime = 0;
+ bool mNRFloopChannels = false;
+ bool mNRFisInRX = false;
+ bool isRxInit = true;
+ bool mRxPendular = false;
+ uint32_t innerLoopTimeout = DURATION_LISTEN_MIN;
+ uint8_t mTxRetries = 15; // memorize last setting for mNrf24->setRetries(3, 15);
+ uint8_t rxOffset = 3; // holds the channel offset between tx and rx channel used for actual inverter
std::unique_ptr mSpi;
std::unique_ptr mNrf24;
diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h
index b86f8d08..7e79f30a 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,18 +51,25 @@ 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;
+ iv->ivRadioType = INV_RADIO_TYPE_CMT;
} else if(iv->config->serial.u64 != 0ULL) {
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!"));
return;
@@ -84,34 +93,31 @@ 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!!!"));
-
+ if(IV_MI == iv->ivGen)
+ DPRINTLN(DBG_WARN, F("MI Inverter, has some restrictions!"));
cb(iv);
}
- INVERTERTYPE *findInverter(uint8_t buf[]) {
- DPRINTLN(DBG_VERBOSE, F("hmSystem.h:findInverter"));
- INVERTERTYPE *p;
+ INVERTERTYPE *findInverter(const uint8_t buf[]) {
for(uint8_t i = 0; i < MAX_INVERTER; i++) {
- p = &mInverter[i];
+ INVERTERTYPE *p = &mInverter[i];
if((p->config->serial.b[3] == buf[0])
&& (p->config->serial.b[2] == buf[1])
&& (p->config->serial.b[1] == buf[2])
&& (p->config->serial.b[0] == buf[3]))
return p;
}
- return NULL;
+ return nullptr;
}
INVERTERTYPE *getInverterByPos(uint8_t pos, bool check = true) {
DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos"));
if(pos >= MAX_INVERTER)
- return NULL;
+ return nullptr;
else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check))
return &mInverter[pos];
else
- return NULL;
+ return nullptr;
}
uint8_t getNumInverters(void) {
diff --git a/src/hm/nrfHal.h b/src/hm/nrfHal.h
index c9fbcdc7..b9265626 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__
@@ -142,8 +142,10 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle {
}
uint8_t read(uint8_t cmd, uint8_t* buf, uint8_t len) override {
- uint8_t data[NRF_MAX_TRANSFER_SZ];
+ uint8_t data[NRF_MAX_TRANSFER_SZ + 1];
data[0] = cmd;
+ if(len > NRF_MAX_TRANSFER_SZ)
+ len = NRF_MAX_TRANSFER_SZ;
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..31643980 100644
--- a/src/hm/radio.h
+++ b/src/hm/radio.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://github.com/lumpapu/ahoy
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -11,8 +11,13 @@
#define ALL_FRAMES 0x80
#define SINGLE_FRAME 0x81
+#include
+#include
#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
@@ -24,12 +29,17 @@ class Radio {
virtual void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) = 0;
virtual bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) { return true; }
virtual bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) { return true; }
- virtual bool isChipConnected(void) { return false; }
+ virtual bool isChipConnected(void) const { return false; }
+ virtual uint16_t getBaseFreqMhz() { return 0; }
+ virtual uint16_t getBootFreqMhz() { return 0; }
+ virtual std::pair getFreqRangeMhz(void) { return std::make_pair(0, 0); }
+ virtual bool loop(void) = 0;
- virtual void loop(void) {};
+ Radio() : mTxBuf{} {}
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 +49,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;
}
@@ -59,17 +71,25 @@ class Radio {
sendPacket(iv, 24, isRetransmit);
}
- uint32_t getDTUSn(void) {
+ uint32_t getDTUSn(void) const {
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)
+ uint8_t mTxRetriesNext = 15; // let heuristics tell us the next reties count (for nRF type radios only)
+ uint8_t mFramesExpected = 0x0c;
protected:
virtual void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) = 0;
- virtual uint64_t getIvId(Inverter<> *iv) = 0;
- virtual uint8_t getIvGen(Inverter<> *iv) = 0;
+ virtual uint64_t getIvId(Inverter<> *iv) const = 0;
+ virtual uint8_t getIvGen(Inverter<> *iv) const = 0;
void initPacket(uint64_t ivId, uint8_t mid, uint8_t pid) {
mTxBuf[0] = mid;
@@ -77,6 +97,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) {
@@ -88,30 +110,33 @@ class Radio {
mTxBuf[(*len)++] = (crc ) & 0xff;
}
// crc over all
- mTxBuf[*len] = ah::crc8(mTxBuf, *len);
+ mTxBuf[*len] = ah::crc8(mTxBuf.data(), *len);
(*len)++;
}
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;
+
+ mDtuSn = 0;
+ for(int i = 0; i < (7 << 2); i += 4) {
+ uint8_t t = (chipID >> i) & 0x0f;
+ if(t > 0x09)
+ t -= 6;
+ mDtuSn |= (t << i);
}
+ mDtuSn |= 0x80000000; // the first digit is an 8 for DTU production year 2022, the rest is filled with the ESP chipID in decimal
}
- uint32_t mDtuSn;
- volatile bool mIrqRcvd;
- bool *mSerialDebug, *mPrivacyMode, *mPrintWholeTrace;
- uint8_t mTxBuf[MAX_RF_PAYLOAD_SIZE];
-
+ protected:
+ uint32_t mDtuSn = 0;
+ std::atomic mIrqRcvd = false;
+ bool *mSerialDebug = nullptr, *mPrivacyMode = nullptr, *mPrintWholeTrace = nullptr;
+ std::array mTxBuf;
};
#endif /*__RADIO_H__*/
diff --git a/src/hm/simulator.h b/src/hm/simulator.h
new file mode 100644
index 00000000..3d0b43d7
--- /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 = nullptr;
+ uint8_t mIvId = 0;
+ uint32_t *mTimestamp = nullptr;
+ 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/cmt2300a.h b/src/hms/cmt2300a.h
index 1ff112e2..23911b15 100644
--- a/src/hms/cmt2300a.h
+++ b/src/hms/cmt2300a.h
@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------------
-// 2023 Ahoy, https://github.com/lumpapu/ahoy
+// 2024 Ahoy, https://github.com/lumpapu/ahoy
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
@@ -12,8 +12,23 @@
#include "esp32_3wSpi.h"
#endif
-// detailed register infos from AN142_CMT2300AW_Quick_Start_Guide-Rev0.8.pdf
+#include
+
+enum class RegionCfg : uint8_t {
+ EUROPE, USA, BRAZIL, NUM
+};
+
+enum class CmtStatus : uint8_t {
+ SUCCESS = 0,
+ ERR_SWITCH_STATE,
+ ERR_TX_PENDING,
+ FIFO_EMPTY,
+ ERR_RX_IN_FIFO
+};
+#define FREQ_STEP_KHZ 250 // channel step size in kHz
+
+// detailed register infos from AN142_CMT2300AW_Quick_Start_Guide-Rev0.8.pdf
#define CMT2300A_MASK_CFG_RETAIN 0x10
#define CMT2300A_MASK_RSTN_IN_EN 0x20
#define CMT2300A_MASK_LOCKING_EN 0x20
@@ -152,67 +167,6 @@
#define CMT2300A_MASK_TX_DONE_FLG 0x08
#define CMT2300A_MASK_PKT_OK_FLG 0x01
-// this list and the TX5, TX10 registers were compiled from the output of
-// HopeRF RFPDK Tool v1.54
-static uint8_t paLevelList[31][2] PROGMEM = {
- {0x17, 0x01}, // -10dBm
- {0x1a, 0x01}, // -09dBm
- {0x1d, 0x01}, // -08dBm
- {0x21, 0x01}, // -07dBm
- {0x25, 0x01}, // -06dBm
- {0x29, 0x01}, // -05dBm
- {0x2d, 0x01}, // -04dBm
- {0x33, 0x01}, // -03dBm
- {0x39, 0x02}, // -02dBm
- {0x41, 0x02}, // -01dBm
- {0x4b, 0x02}, // 00dBm
- {0x56, 0x03}, // 01dBm
- {0x63, 0x03}, // 02dBm
- {0x71, 0x04}, // 03dBm
- {0x80, 0x04}, // 04dBm
- {0x22, 0x01}, // 05dBm
- {0x27, 0x04}, // 06dBm
- {0x2c, 0x05}, // 07dBm
- {0x31, 0x06}, // 08dBm
- {0x38, 0x06}, // 09dBm
- {0x3f, 0x07}, // 10dBm
- {0x48, 0x08}, // 11dBm
- {0x52, 0x09}, // 12dBm
- {0x5d, 0x0b}, // 13dBm
- {0x6a, 0x0c}, // 14dBm
- {0x79, 0x0d}, // 15dBm
- {0x46, 0x10}, // 16dBm
- {0x51, 0x10}, // 17dBm
- {0x60, 0x12}, // 18dBm
- {0x71, 0x14}, // 19dBm
- {0x8c, 0x1c} // 20dBm
-};
-
-// default CMT parameters
-static uint8_t cmtConfig[0x60] PROGMEM {
- // 0x00 - 0x0f -- RSSI offset +- 0 and 13dBm
- 0x00, 0x66, 0xEC, 0x1C, 0x70, 0x80, 0x14, 0x08,
- 0x11, 0x02, 0x02, 0x00, 0xAE, 0xE0, 0x35, 0x00,
- // 0x10 - 0x1f
- 0x00, 0xF4, 0x10, 0xE2, 0x42, 0x20, 0x0C, 0x81,
- 0x42, 0x32, 0xCF, 0x82, 0x42, 0x27, 0x76, 0x12, // 860MHz as default
- // 0x20 - 0x2f
- 0xA6, 0xC9, 0x20, 0x20, 0xD2, 0x35, 0x0C, 0x0A,
- 0x9F, 0x4B, 0x29, 0x29, 0xC0, 0x14, 0x05, 0x53,
- // 0x30 - 0x3f
- 0x10, 0x00, 0xB4, 0x00, 0x00, 0x01, 0x00, 0x00,
- 0x12, 0x1E, 0x00, 0xAA, 0x06, 0x00, 0x00, 0x00,
- // 0x40 - 0x4f
- 0x00, 0x48, 0x5A, 0x48, 0x4D, 0x01, 0x1F, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x60,
- // 0x50 - 0x5f
- 0xFF, 0x00, 0x00, 0x1F, 0x10, 0x70, 0x4D, 0x06,
- 0x00, 0x07, 0x50, 0x00, 0x5D, 0x0B, 0x3F, 0x7F // - TX 13dBm
-};
-
-
-enum {CMT_SUCCESS = 0, CMT_ERR_SWITCH_STATE, CMT_ERR_TX_PENDING, CMT_FIFO_EMPTY, CMT_ERR_RX_IN_FIFO};
-
class Cmt2300a {
public:
Cmt2300a() {}
@@ -234,12 +188,12 @@ class Cmt2300a {
}
}
- uint8_t goRx(void) {
+ CmtStatus goRx(void) {
if(mTxPending)
- return CMT_ERR_TX_PENDING;
+ return CmtStatus::ERR_TX_PENDING;
if(mInRxMode)
- return CMT_SUCCESS;
+ return CmtStatus::SUCCESS;
mSpi.readReg(CMT2300A_CUS_INT1_CTL);
mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE);
@@ -260,47 +214,47 @@ class Cmt2300a {
mSpi.writeReg(0x16, 0x0C); // [4:3]: RSSI_DET_SEL, [2:0]: RSSI_AVG_MODE
if(!cmtSwitchStatus(CMT2300A_GO_RX, CMT2300A_STA_RX))
- return CMT_ERR_SWITCH_STATE;
+ return CmtStatus::ERR_SWITCH_STATE;
mInRxMode = true;
- return CMT_SUCCESS;
+ return CmtStatus::SUCCESS;
}
- uint8_t getRx(uint8_t buf[], uint8_t *rxLen, uint8_t maxlen, int8_t *rssi) {
+ CmtStatus getRx(uint8_t buf[], uint8_t *rxLen, uint8_t maxlen, int8_t *rssi) {
if(mTxPending)
- return CMT_ERR_TX_PENDING;
+ return CmtStatus::ERR_TX_PENDING;
if(0x1b != (mSpi.readReg(CMT2300A_CUS_INT_FLAG) & 0x1b))
- return CMT_FIFO_EMPTY;
+ return CmtStatus::FIFO_EMPTY;
// receive ok (pream, sync, node, crc)
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
- return CMT_ERR_SWITCH_STATE;
+ return CmtStatus::ERR_SWITCH_STATE;
mSpi.readFifo(buf, rxLen, maxlen);
*rssi = mSpi.readReg(CMT2300A_CUS_RSSI_DBM) - 128;
if(!cmtSwitchStatus(CMT2300A_GO_SLEEP, CMT2300A_STA_SLEEP))
- return CMT_ERR_SWITCH_STATE;
+ return CmtStatus::ERR_SWITCH_STATE;
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
- return CMT_ERR_SWITCH_STATE;
+ return CmtStatus::ERR_SWITCH_STATE;
mInRxMode = false;
mCusIntFlag = mSpi.readReg(CMT2300A_CUS_INT_FLAG);
- return CMT_SUCCESS;
+ return CmtStatus::SUCCESS;
}
- uint8_t tx(uint8_t buf[], uint8_t len) {
+ CmtStatus tx(uint8_t buf[], uint8_t len) {
if(mTxPending)
- return CMT_ERR_TX_PENDING;
+ return CmtStatus::ERR_TX_PENDING;
if(mInRxMode) {
mInRxMode = false;
if(!cmtSwitchStatus(CMT2300A_GO_STBY, CMT2300A_STA_STBY))
- return CMT_ERR_SWITCH_STATE;
+ return CmtStatus::ERR_SWITCH_STATE;
}
mSpi.writeReg(CMT2300A_CUS_INT1_CTL, CMT2300A_INT_SEL_TX_DONE);
@@ -325,16 +279,17 @@ class Cmt2300a {
}
if(!cmtSwitchStatus(CMT2300A_GO_TX, CMT2300A_STA_TX))
- return CMT_ERR_SWITCH_STATE;
+ return CmtStatus::ERR_SWITCH_STATE;
// wait for tx done
mTxPending = true;
- return CMT_SUCCESS;
+ return CmtStatus::SUCCESS;
}
// initialize CMT2300A, returns true on success
- bool reset(void) {
+ bool reset(RegionCfg region) {
+ mRegionCfg = region;
mSpi.writeReg(0x7f, 0xff); // soft reset
delay(30);
@@ -346,9 +301,18 @@ class Cmt2300a {
if(mSpi.readReg(0x62) != 0x20)
return false; // not connected!
- for(uint8_t i = 0; i < 0x60; i++) {
+ for(uint8_t i = 0; i < 0x18; i++) {
mSpi.writeReg(i, cmtConfig[i]);
}
+ for(uint8_t i = 0; i < 8; i++) {
+ mSpi.writeReg(0x18 + i, mBaseFreqCfg[static_cast(region)][i]);
+ }
+ for(uint8_t i = 0x20; i < 0x60; i++) {
+ mSpi.writeReg(i, cmtConfig[i]);
+ }
+
+ if(RegionCfg::EUROPE != region)
+ mSpi.writeReg(0x27, 0x0B);
mSpi.writeReg(CMT2300A_CUS_IO_SEL, 0x20); // -> GPIO3_SEL[1:0] = 0x02
@@ -389,23 +353,14 @@ class Cmt2300a {
}
inline uint8_t freq2Chan(const uint32_t freqKhz) {
- if((freqKhz % FREQ_STEP_KHZ) != 0) {
- DPRINT(DBG_WARN, F("switch frequency to "));
- DBGPRINT(String(freqKhz));
- DBGPRINT(F("kHz not possible!"));
+ if((freqKhz % FREQ_STEP_KHZ) != 0)
return 0xff; // error
- // apply the nearest frequency
- //freqKhz = (freqKhz + FREQ_STEP_KHZ/2) / FREQ_STEP_KHZ;
- //freqKhz *= FREQ_STEP_KHZ;
- }
- if((freqKhz < HOY_BASE_FREQ_KHZ) || (freqKhz > HOY_MAX_FREQ_KHZ))
+ std::pair range = getFreqRangeMhz();
+ if((freqKhz < (range.first * 1000)) || (freqKhz > (range.second * 1000)))
return 0xff; // error
- if((freqKhz < FREQ_WARN_MIN_KHZ) || (freqKhz > FREQ_WARN_MAX_KHZ))
- DPRINTLN(DBG_WARN, F("Desired frequency is out of EU legal range! (863 - 870MHz)"));
-
- return (freqKhz - HOY_BASE_FREQ_KHZ) / FREQ_STEP_KHZ;
+ return (freqKhz - (getBaseFreqMhz() * 1000)) / FREQ_STEP_KHZ;
}
inline void switchChannel(uint8_t ch) {
@@ -414,9 +369,9 @@ class Cmt2300a {
inline uint32_t getFreqKhz(void) {
if(0xff != mRqstCh)
- return HOY_BASE_FREQ_KHZ + (mRqstCh * FREQ_STEP_KHZ);
+ return getBaseFreqMhz() * 1000 + (mRqstCh * FREQ_STEP_KHZ);
else
- return HOY_BASE_FREQ_KHZ + (mCurCh * FREQ_STEP_KHZ);
+ return getBaseFreqMhz() * 1000 + (mCurCh * FREQ_STEP_KHZ);
}
uint8_t getCurrentChannel(void) {
@@ -443,6 +398,114 @@ class Cmt2300a {
mSpi.writeReg(CMT2300A_CUS_TX9, paLevelList[level][1]);
}
+ public:
+ uint16_t getBaseFreqMhz(void) {
+ switch(mRegionCfg) {
+ default:
+ [[fallthrough]];
+ case RegionCfg::EUROPE:
+ break;
+ case RegionCfg::USA:
+ return 905;
+ case RegionCfg::BRAZIL:
+ return 915;
+ }
+ return 860;
+ }
+
+ uint16_t getBootFreqMhz(void) {
+ switch(mRegionCfg) {
+ default:
+ [[fallthrough]];
+ case RegionCfg::EUROPE:
+ break;
+ case RegionCfg::USA:
+ return 915;
+ case RegionCfg::BRAZIL:
+ return 915;
+ }
+ return 868;
+ }
+
+ std::pair getFreqRangeMhz(void) {
+ switch(mRegionCfg) {
+ default:
+ [[fallthrough]];
+ case RegionCfg::EUROPE:
+ break;
+ case RegionCfg::USA:
+ return std::make_pair(905, 925);
+ case RegionCfg::BRAZIL:
+ return std::make_pair(915, 928);
+ }
+ return std::make_pair(860, 870); // Europe
+ }
+
+ private:
+ // this list and the TX5, TX10 registers were compiled from the output of
+ // HopeRF RFPDK Tool v1.54
+ constexpr static uint8_t paLevelList[31][2] PROGMEM = {
+ {0x17, 0x01}, // -10dBm
+ {0x1a, 0x01}, // -09dBm
+ {0x1d, 0x01}, // -08dBm
+ {0x21, 0x01}, // -07dBm
+ {0x25, 0x01}, // -06dBm
+ {0x29, 0x01}, // -05dBm
+ {0x2d, 0x01}, // -04dBm
+ {0x33, 0x01}, // -03dBm
+ {0x39, 0x02}, // -02dBm
+ {0x41, 0x02}, // -01dBm
+ {0x4b, 0x02}, // 00dBm
+ {0x56, 0x03}, // 01dBm
+ {0x63, 0x03}, // 02dBm
+ {0x71, 0x04}, // 03dBm
+ {0x80, 0x04}, // 04dBm
+ {0x22, 0x01}, // 05dBm
+ {0x27, 0x04}, // 06dBm
+ {0x2c, 0x05}, // 07dBm
+ {0x31, 0x06}, // 08dBm
+ {0x38, 0x06}, // 09dBm
+ {0x3f, 0x07}, // 10dBm
+ {0x48, 0x08}, // 11dBm
+ {0x52, 0x09}, // 12dBm
+ {0x5d, 0x0b}, // 13dBm
+ {0x6a, 0x0c}, // 14dBm
+ {0x79, 0x0d}, // 15dBm
+ {0x46, 0x10}, // 16dBm
+ {0x51, 0x10}, // 17dBm
+ {0x60, 0x12}, // 18dBm
+ {0x71, 0x14}, // 19dBm
+ {0x8c, 0x1c} // 20dBm
+ };
+
+ // default CMT parameters
+ constexpr static uint8_t cmtConfig[0x60] PROGMEM {
+ // 0x00 - 0x0f -- RSSI offset +- 0 and 13dBm
+ 0x00, 0x66, 0xEC, 0x1C, 0x70, 0x80, 0x14, 0x08,
+ 0x11, 0x02, 0x02, 0x00, 0xAE, 0xE0, 0x35, 0x00,
+ // 0x10 - 0x1f
+ 0x00, 0xF4, 0x10, 0xE2, 0x42, 0x20, 0x0C, 0x81,
+ 0x42, 0x32, 0xCF, 0x82, 0x42, 0x27, 0x76, 0x12, // 860MHz as default
+ // 0x20 - 0x2f
+ 0xA6, 0xC9, 0x20, 0x20, 0xD2, 0x35, 0x0C, 0x0A,
+ 0x9F, 0x4B, 0x29, 0x29, 0xC0, 0x14, 0x05, 0x53,
+ // 0x30 - 0x3f
+ 0x10, 0x00, 0xB4, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x12, 0x1E, 0x00, 0xAA, 0x06, 0x00, 0x00, 0x00,
+ // 0x40 - 0x4f
+ 0x00, 0x48, 0x5A, 0x48, 0x4D, 0x01, 0x1F, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xC3, 0x00, 0x00, 0x60,
+ // 0x50 - 0x5f
+ 0xFF, 0x00, 0x00, 0x1F, 0x10, 0x70, 0x4D, 0x06,
+ 0x00, 0x07, 0x50, 0x00, 0x5D, 0x0B, 0x3F, 0x7F // TX 13dBm
+ };
+
+ constexpr static uint8_t mBaseFreqCfg[static_cast(RegionCfg::NUM)][8] {
+ {0x42, 0x32, 0xCF, 0x82, 0x42, 0x27, 0x76, 0x12}, // 860MHz
+ {0x45, 0xA8, 0x31, 0x8A, 0x45, 0x9D, 0xD8, 0x19}, // 905MHz (USA, Indonesia)
+ {0x46, 0x6D, 0x80, 0x86, 0x46, 0x62, 0x27, 0x16} // 915MHz (Brazil)
+ };
+
private:
void init() {
mTxPending = false;
@@ -466,7 +529,8 @@ class Cmt2300a {
return false;
}
- inline bool switchDtuFreq(const uint32_t freqKhz) {
+ // maybe used in future
+ /*inline bool switchDtuFreq(const uint32_t freqKhz) {
uint8_t toCh = freq2Chan(freqKhz);
if(0xff == toCh)
return false;
@@ -474,22 +538,24 @@ class Cmt2300a {
switchChannel(toCh);
return true;
- }
+ }*/
inline uint8_t getChipStatus(void) {
return mSpi.readReg(CMT2300A_CUS_MODE_STA) & CMT2300A_MASK_CHIP_MODE_STA;
}
+ private:
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL)
cmtHal mSpi;
#else
esp32_3wSpi mSpi;
#endif
- uint8_t mCnt;
- bool mTxPending;
- bool mInRxMode;
- uint8_t mCusIntFlag;
- uint8_t mRqstCh, mCurCh;
+ uint8_t mCnt = 0;
+ bool mTxPending = false;
+ bool mInRxMode = false;
+ uint8_t mCusIntFlag = 0;
+ uint8_t mRqstCh = 0, mCurCh = 0;
+ RegionCfg mRegionCfg = RegionCfg::EUROPE;
};
#endif /*__CMT2300A_H__*/
diff --git a/src/hms/cmtHal.h b/src/hms/cmtHal.h
index d5e31ea0..a4bec587 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 {
@@ -89,7 +89,7 @@ class cmtHal : public SpiPatcherHandle {
}
uint8_t readReg(uint8_t addr) {
- uint8_t data;
+ uint8_t data = 0;
request_spi();
diff --git a/src/hms/esp32_3wSpi.h b/src/hms/esp32_3wSpi.h
index 20c42632..a0366b23 100644
--- a/src/hms/esp32_3wSpi.h
+++ b/src/hms/esp32_3wSpi.h
@@ -10,6 +10,7 @@
#if defined(ESP32)
#include "driver/spi_master.h"
#include "esp_rom_gpio.h" // for esp_rom_gpio_connect_out_signal
+#include "../config/config.h"
#define SPI_CLK 1 * 1000 * 1000 // 1MHz
@@ -21,7 +22,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 +55,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 +73,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);
@@ -104,7 +105,7 @@ class esp32_3wSpi {
if(!mInitialized)
return 0;
- uint8_t rx_data;
+ uint8_t rx_data = 0;
spi_transaction_t t = {
.cmd = 0,
.addr = (uint64_t)(~addr),
@@ -121,7 +122,7 @@ class esp32_3wSpi {
return rx_data;
}
- void writeFifo(uint8_t buf[], uint8_t len) {
+ void writeFifo(const uint8_t buf[], uint8_t len) {
if(!mInitialized)
return;
uint8_t tx_data;
@@ -144,7 +145,7 @@ class esp32_3wSpi {
void readFifo(uint8_t buf[], uint8_t *len, uint8_t maxlen) {
if(!mInitialized)
return;
- uint8_t rx_data;
+ uint8_t rx_data = 0;
spi_transaction_t t = {
.length = 8,
diff --git a/src/hms/hmsRadio.h b/src/hms/hmsRadio.h
index d2779012..d074442b 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
//-----------------------------------------------------------------------------
@@ -9,39 +9,38 @@
#include "cmt2300a.h"
#include "../hm/radio.h"
+//#define CMT_SWITCH_CHANNEL_CYCLE 5
+
template
class CmtRadio : public Radio {
typedef Cmt2300a CmtType;
public:
- CmtRadio() {
- mDtuSn = DTU_SN;
- mCmtAvail = false;
- }
-
- void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t pinSclk, uint8_t pinSdio, uint8_t pinCsb, uint8_t pinFcsb, bool genDtuSn = true) {
+ void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t pinSclk, uint8_t pinSdio, uint8_t pinCsb, uint8_t pinFcsb, uint8_t region = 0, bool genDtuSn = true) {
mCmt.setup(pinSclk, pinSdio, pinCsb, pinFcsb);
- reset(genDtuSn);
+ reset(genDtuSn, static_cast(region));
mPrivacyMode = privacyMode;
mSerialDebug = serialDebug;
mPrintWholeTrace = printWholeTrace;
+ mTxBuf.fill(0);
}
- void loop() {
+ bool loop() override {
mCmt.loop();
if((!mIrqRcvd) && (!mRqstGetRx))
- return;
+ return false;
getRx();
- if(CMT_SUCCESS == mCmt.goRx()) {
+ if(CmtStatus::SUCCESS == mCmt.goRx()) {
mIrqRcvd = false;
mRqstGetRx = false;
}
+ return false;
}
- bool isChipConnected(void) {
+ bool isChipConnected(void) const override {
return mCmtAvail;
}
- void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) {
+ void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override {
DPRINT(DBG_INFO, F("sendControlPacket cmd: "));
DBGHEXLN(cmd);
initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME);
@@ -50,23 +49,23 @@ 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);
}
- bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) {
+ bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) override {
uint8_t fromCh = mCmt.freq2Chan(fromkHz);
uint8_t toCh = mCmt.freq2Chan(tokHz);
return switchFrequencyCh(iv, fromCh, toCh);
}
- bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) {
+ bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) override {
if((0xff == fromCh) || (0xff == toCh))
return false;
@@ -76,9 +75,21 @@ class CmtRadio : public Radio {
return true;
}
+ uint16_t getBaseFreqMhz(void) override {
+ return mCmt.getBaseFreqMhz();
+ }
+
+ uint16_t getBootFreqMhz(void) override {
+ return mCmt.getBootFreqMhz();
+ }
+
+ std::pair getFreqRangeMhz(void) override {
+ return mCmt.getFreqRangeMhz();
+ }
+
private:
- void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) {
+ void sendPacket(Inverter<> *iv, uint8_t len, bool isRetransmit, bool appendCrc16=true) override {
// inverters have maybe different settings regarding frequency
if(mCmt.getCurrentChannel() != iv->config->frequency)
mCmt.switchChannel(iv->config->frequency);
@@ -92,9 +103,9 @@ class CmtRadio : public Radio {
DBGPRINT(F("Mhz | "));
if(*mPrintWholeTrace) {
if(*mPrivacyMode)
- ah::dumpBuf(mTxBuf, len, 1, 4);
+ ah::dumpBuf(mTxBuf.data(), len, 1, 4);
else
- ah::dumpBuf(mTxBuf, len);
+ ah::dumpBuf(mTxBuf.data(), len);
} else {
DHEX(mTxBuf[0]);
DBGPRINT(F(" "));
@@ -104,29 +115,29 @@ class CmtRadio : public Radio {
}
}
- uint8_t status = mCmt.tx(mTxBuf, len);
+ CmtStatus status = mCmt.tx(mTxBuf.data(), len);
mMillis = millis();
- if(CMT_SUCCESS != status) {
+ if(CmtStatus::SUCCESS != status) {
DPRINT(DBG_WARN, F("CMT TX failed, code: "));
- DBGPRINTLN(String(status));
- if(CMT_ERR_RX_IN_FIFO == status)
+ DBGPRINTLN(String(static_cast(status)));
+ if(CmtStatus::ERR_RX_IN_FIFO == status)
mIrqRcvd = true;
}
iv->mDtuTxCnt++;
}
- uint64_t getIvId(Inverter<> *iv) {
+ uint64_t getIvId(Inverter<> *iv) const override {
return iv->radioId.u64;
}
- uint8_t getIvGen(Inverter<> *iv) {
+ uint8_t getIvGen(Inverter<> *iv) const override {
return iv->ivGen;
}
- inline void reset(bool genDtuSn) {
+ inline void reset(bool genDtuSn, RegionCfg region) {
if(genDtuSn)
generateDtuSn();
- if(!mCmt.reset()) {
+ if(!mCmt.reset(region)) {
mCmtAvail = false;
DPRINTLN(DBG_WARN, F("Initializing CMT2300A failed!"));
} else {
@@ -134,11 +145,15 @@ class CmtRadio : public Radio {
mCmt.goRx();
}
- mIrqRcvd = false;
- mRqstGetRx = false;
+ mIrqRcvd = false;
+ mRqstGetRx = false;
}
inline void sendSwitchChCmd(Inverter<> *iv, uint8_t ch) {
+ //if(CMT_SWITCH_CHANNEL_CYCLE > ++mSwitchCycle)
+ // return;
+ //mSwitchCycle = 0;
+
/** ch:
* 0x00: 860.00 MHz
* 0x01: 860.25 MHz
@@ -160,15 +175,23 @@ class CmtRadio : public Radio {
inline void getRx(void) {
packet_t p;
p.millis = millis() - mMillis;
- uint8_t status = mCmt.getRx(p.packet, &p.len, 28, &p.rssi);
- if(CMT_SUCCESS == status)
+ if(CmtStatus::SUCCESS == mCmt.getRx(p.packet, &p.len, 28, &p.rssi)) {
+ //mSwitchCycle = 0;
+ p.ch = 0; // not used for CMT inverters
mBufCtrl.push(p);
+ }
+
+ if(p.packet[9] > ALL_FRAMES) { // indicates last frame
+ setExpectedFrames(p.packet[9] - ALL_FRAMES);
+ mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first get back to rx mode?
+ }
}
CmtType mCmt;
- bool mRqstGetRx;
- bool mCmtAvail;
- uint32_t mMillis;
+ bool mCmtAvail = false;
+ bool mRqstGetRx = false;
+ uint32_t mMillis = 0;
+ //uint8_t mSwitchCycle = 0;
};
#endif /*__HMS_RADIO_H__*/
diff --git a/src/platformio.ini b/src/platformio.ini
index 2b4b25cf..f949aa37 100644
--- a/src/platformio.ini
+++ b/src/platformio.ini
@@ -22,16 +22,17 @@ 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
- nrf24/RF24 @ 1.4.8
+ https://github.com/nRF24/RF24 @ 1.4.8
paulstoffregen/Time @ ^1.6.1
- https://github.com/bertmelis/espMqttClient#v1.5.0
+ https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
- olikraus/U8g2 @ ^2.35.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
@@ -93,56 +195,199 @@ board = esp32dev
lib_deps =
khoih-prog/AsyncWebServer_ESP32_W5500
khoih-prog/AsyncUDP_ESP32_W5500
- nrf24/RF24 @ ^1.4.8
+ https://github.com/nRF24/RF24 @ ^1.4.8
+ paulstoffregen/Time @ ^1.6.1
+ https://github.com/bertmelis/espMqttClient#v1.6.0
+ bblanchon/ArduinoJson @ ^6.21.3
+ https://github.com/JChristensen/Timezone @ ^1.2.4
+ olikraus/U8g2 @ ^2.35.9
+ https://github.com/zinggjm/GxEPD2#1.5.3
+build_flags = ${env.build_flags}
+ -D ETHERNET
+ -DRELEASE
+ -DUSE_HSPI_FOR_EPD
+ -DENABLE_MQTT
+ -DPLUGIN_DISPLAY
+ -DENABLE_HISTORY
+monitor_filters =
+ esp32_exception_decoder
+
+[env:esp32-wroom32-ethernet-de]
+platform = espressif32
+board = esp32dev
+lib_deps =
+ khoih-prog/AsyncWebServer_ESP32_W5500
+ khoih-prog/AsyncUDP_ESP32_W5500
+ https://github.com/nRF24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
- https://github.com/bertmelis/espMqttClient#v1.5.0
+ https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
- olikraus/U8g2 @ ^2.35.7
- zinggjm/GxEPD2 @ ^1.5.2
+ olikraus/U8g2 @ ^2.35.9
+ https://github.com/zinggjm/GxEPD2#1.5.3
build_flags = ${env.build_flags}
-D ETHERNET
-DRELEASE
-DUSE_HSPI_FOR_EPD
- -DLOG_LOCAL_LEVEL=ESP_LOG_INFO
- -DDEBUG_LEVEL=DBG_INFO
+ -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
+ -DSPI_HAL
+ -DDEF_NRF_CS_PIN=37
+ -DDEF_NRF_CE_PIN=38
+ -DDEF_NRF_IRQ_PIN=47
+ -DDEF_NRF_MISO_PIN=48
+ -DDEF_NRF_MOSI_PIN=35
+ -DDEF_NRF_SCLK_PIN=36
+ -DDEF_CMT_CSB=4
+ -DDEF_CMT_FCSB=21
+ -DDEF_CMT_IRQ=8
+ -DDEF_CMT_SDIO=5
+ -DDEF_CMT_SCLK=6
+ -DDEF_LED0=18
+ -DDEF_LED1=17
+ -DLED_ACTIVE_HIGH
+ -DARDUINO_USB_MODE=1
+ #-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
+ -DSPI_HAL
+ -DDEF_NRF_CS_PIN=37
+ -DDEF_NRF_CE_PIN=38
+ -DDEF_NRF_IRQ_PIN=47
+ -DDEF_NRF_MISO_PIN=48
+ -DDEF_NRF_MOSI_PIN=35
+ -DDEF_NRF_SCLK_PIN=36
+ -DDEF_CMT_CSB=4
+ -DDEF_CMT_FCSB=21
+ -DDEF_CMT_IRQ=8
+ -DDEF_CMT_SDIO=5
+ -DDEF_CMT_SCLK=6
+ -DDEF_LED0=18
+ -DDEF_LED1=17
+ -DLED_ACTIVE_HIGH
+ -DARDUINO_USB_MODE=1
+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}
+ -DSPI_HAL
-DDEF_NRF_CS_PIN=37
-DDEF_NRF_CE_PIN=38
-DDEF_NRF_IRQ_PIN=47
@@ -162,23 +407,25 @@ 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
khoih-prog/AsyncUDP_ESP32_W5500
https://github.com/nrf24/RF24 @ ^1.4.8
paulstoffregen/Time @ ^1.6.1
- https://github.com/bertmelis/espMqttClient#v1.5.0
+ https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
- olikraus/U8g2 @ ^2.35.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
@@ -204,20 +451,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
+ https://github.com/bertmelis/espMqttClient#v1.6.0
bblanchon/ArduinoJson @ ^6.21.3
https://github.com/JChristensen/Timezone @ ^1.2.4
- olikraus/U8g2 @ ^2.35.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
@@ -233,7 +493,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..b2c88b05 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,227 @@
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;
+
+ if (mMono != NULL)
+ request_refresh = mMono->loop(motionSensorActive());
- rec = iv->getRecordStruct(RealTimeRunData_Debug);
+ if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) {
+ DataScreen();
+ mNewPayload = false;
+ mLoopCnt = 0;
+ }
+ #if defined(ESP32) && !defined(ETHERNET)
+ mEpaper.tickerSecond();
+ #endif
+ }
- 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;
+ 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
- return(digitalRead(mCfg->pirPin));
-#elif defined(ESP32)
- return(digitalRead(mCfg->pirPin));
-#endif
+
+ 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);
+ #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 = nullptr;
+ DisplayData mDisplayData;
+ bool mNewPayload = false;
+ uint8_t mLoopCnt = 0;
+ uint32_t *mUtcTs = nullptr;
+ display_t *mCfg = nullptr;
+ HMSYSTEM *mSys = nullptr;
+ RADIO *mHmRadio = nullptr;
+ RADIO *mHmsRadio = nullptr;
+ uint16_t mRefreshCycle = 0;
+
+ #if defined(ESP32) && !defined(ETHERNET)
+ DisplayEPaper mEpaper;
+ #endif
+ DisplayMono *mMono = nullptr;
};
+#endif /*PLUGIN_DISPLAY*/
+
#endif /*__DISPLAY__*/
diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h
index 3e998b6d..e855eeb9 100644
--- a/src/plugins/Display/Display_Mono.h
+++ b/src/plugins/Display/Display_Mono.h
@@ -20,101 +20,312 @@
#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:
+ 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 != mCfg->contrast) {
+ mLuminance = mCfg->contrast;
+ mDisplay->setContrast(mLuminance);
}
- }
- if(mLuminance != lum) {
- mLuminance = lum;
+ 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 = 0;
+ int8_t mPixelshift=0;
+ char mFmtText[DISP_FMT_TEXT_LEN];
+ uint8_t mLineXOffsets[5] = {0, 0, 0, 0, 0};
+ uint8_t mLineYOffsets[5] = {0, 0, 0, 0, 0};
+
+ 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 = 0;
+ uint8_t mLuminance = 0;
+
+ 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..6262e3f6 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) override {
+ 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) override {
+ u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
+ monoInit(new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, 0xff, mCfg->disp_clk, mCfg->disp_data), displayData);
calcLinePositions();
printText("Ahoy!", 0);
printText("ahoydtu.de", 2);
@@ -28,7 +26,7 @@ class DisplayMono128X32 : public DisplayMono {
mDisplay->sendBuffer();
}
- void disp(void) {
+ void disp(void) override {
mDisplay->clearBuffer();
// calculate current pixelshift for pixelshift screensaver
@@ -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..c63f0b22 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) override {
+ 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) override {
+ 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);
@@ -40,9 +68,7 @@ class DisplayMono128X64 : public DisplayMono {
mDisplay->sendBuffer();
}
- void disp(void) {
- uint8_t pos, sun_pos, moon_pos;
-
+ void disp(void) override {
mDisplay->clearBuffer();
// Layout-Test
@@ -61,106 +87,116 @@ 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, " ");
+ int8_t sun_pos = -1;
+ int8_t moon_pos = -1;
+ setLineFont(l_Status);
+ if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing)
+ snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter");
+ 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);
+
+ uint8_t 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;
- if (mDisplayData->RadioRSSI > radio_rssi_threshold)
- mDisplay->drawBox(xoffs + mPixelshift, 8 + (rssi_bar_height + 1) * i, 4 - i, 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);
+ int rssi_threshold = -60 - i * 10;
+ uint8_t barwidth = std::min(4 - i, 3);
+ if (mDisplayData->RadioRSSI > rssi_threshold)
+ mDisplay->drawBox(widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
+ if (mDisplayData->WifiRSSI > rssi_threshold)
+ mDisplay->drawBox(mDispWidth - barwidth - widthShrink / 2 + mPixelshift, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
}
// draw dynamic antenna and WiFi symbols
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,29 +220,31 @@ class DisplayMono128X64 : public DisplayMono {
l_MAX_LINES = 5,
};
+ uint8_t graph_first_line = 0;
+ uint8_t graph_last_line = 0;
+
const uint8_t pixelShiftRange = 11; // number of pixels to shift from left to right (centered -> must be odd!)
+ uint8_t widthShrink = 0;
void calcLinePositions() {
uint8_t yOff = 0;
uint8_t i = 0;
- uint8_t asc, dsc;
do {
setLineFont(i);
- asc = mDisplay->getAscent();
+ uint8_t asc = mDisplay->getAscent();
yOff += asc;
mLineYOffsets[i] = yOff;
- dsc = mDisplay->getDescent();
+ uint8_t dsc = mDisplay->getDescent();
yOff -= dsc;
- if (l_Time==i) // prevent time and status line to touch
- yOff+=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);
}
inline void setLineFont(uint8_t line) {
- if ((line == l_TotalPower) ||
- (line == l_Ahoy))
+ if (line == l_TotalPower) // || (line == l_Ahoy) -> l_TotalPower == l_Ahoy == 2
mDisplay->setFont(u8g2_font_ncenB14_tr);
else if ((line == l_YieldDay) ||
(line == l_YieldTotal))
@@ -226,4 +264,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..7f98cae5 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) override {
+ 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) override {
+ u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
// Wemos OLed Shield is not defined in u8 lib -> use nearest compatible
- monoInit(new U8G2_SSD1306_64X48_ER_F_HW_I2C(rot, 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);
@@ -30,7 +28,7 @@ class DisplayMono64X48 : public DisplayMono {
mDisplay->sendBuffer();
}
- void disp(void) {
+ void disp(void) override {
mDisplay->clearBuffer();
// calculate current pixelshift for pixelshift screensaver
@@ -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..b5daacd5 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,23 +12,50 @@ 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) override {
+ 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) override {
+ u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot != 0x00) ? U8G2_R2 : U8G2_R0);
+
+ monoInit(new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, mCfg->disp_clk, mCfg->disp_data, mCfg->disp_cs, mCfg->disp_dc, 0xff), displayData);
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);
mDisplay->sendBuffer();
}
- void disp(void) {
+ void disp(void) override {
mDisplay->clearBuffer();
// Layout-Test
@@ -45,66 +71,84 @@ 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;
- if (mDisplayData->RadioRSSI > radio_rssi_threshold)
- mDisplay->drawBox(0, 8+(rssi_bar_height+1)*i, 4-i,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);
+ for (int i = 0; i < 4; i++) {
+ int rssi_threshold = -60 - i * 10;
+ uint8_t barwidth = std::min(4 - i, 3);
+ if (mDisplayData->RadioRSSI > rssi_threshold)
+ mDisplay->drawBox(0, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
+ if (mDisplayData->WifiRSSI > rssi_threshold)
+ mDisplay->drawBox(mDispWidth - barwidth, 8 + (rssi_bar_height + 1) * i, barwidth, rssi_bar_height);
}
// draw dynamic antenna and WiFi symbols
@@ -139,26 +183,28 @@ class DisplayMono84X48 : public DisplayMono {
l_MAX_LINES = 5,
};
+ uint8_t graph_first_line = 0;
+ uint8_t graph_last_line = 0;
+
void calcLinePositions() {
uint8_t yOff = 0;
uint8_t i = 0;
- uint8_t asc, dsc;
do {
setLineFont(i);
- asc = mDisplay->getAscent();
+ uint8_t asc = mDisplay->getAscent();
yOff += asc;
mLineYOffsets[i] = yOff;
- dsc = mDisplay->getDescent();
- if (l_TotalPower!=i) // power line needs no descent spacing
+ uint8_t dsc = mDisplay->getDescent();
+ if (l_TotalPower != i) // power line needs no descent spacing
yOff -= dsc;
yOff++; // instead lets spend one pixel space between all lines
i++;
- } while(l_MAX_LINES>i);
+ } while(l_MAX_LINES > i);
}
inline void setLineFont(uint8_t line) {
- if ((line == l_TotalPower) || (line == l_Ahoy))
+ if (line == l_TotalPower) // || (line == l_Ahoy) -> l_TotalPower == l_Ahoy == 2
mDisplay->setFont(u8g2_font_logisoso16_tr);
else
mDisplay->setFont(u8g2_font_5x8_symbols_ahoy);
@@ -174,6 +220,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..087d784b 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);
+ mSecondCnt = 2;
mNextRefreshState = RefreshStatus::PARTITIALS;
mRefreshState = RefreshStatus::WAIT;
break;
@@ -76,11 +79,11 @@ void DisplayEPaper::refreshLoop() {
break;
case RefreshStatus::WHITE:
- if(mSecondCnt == 0) {
- _display->fillScreen(GxEPD_WHITE);
- mNextRefreshState = RefreshStatus::PARTITIALS;
- mRefreshState = RefreshStatus::WAIT;
- }
+ if(0 != mSecondCnt)
+ break;
+ _display->fillScreen(GxEPD_WHITE);
+ mNextRefreshState = RefreshStatus::PARTITIALS;
+ mRefreshState = RefreshStatus::WAIT;
break;
case RefreshStatus::WAIT:
@@ -89,10 +92,13 @@ void DisplayEPaper::refreshLoop() {
break;
case RefreshStatus::PARTITIALS:
+ if(0 != mSecondCnt)
+ break;
headlineIP();
versionFooter();
mSecondCnt = 4; // display Logo time during boot up
- mRefreshState = RefreshStatus::DONE;
+ mNextRefreshState = RefreshStatus::DONE;
+ mRefreshState = RefreshStatus::WAIT;
break;
default: // RefreshStatus::DONE
@@ -112,9 +118,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 +141,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 +162,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 +183,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 +207,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 +268,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