diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..9fcb3325 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +patches/GxEPD2_HAL.patch eol=lf \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/report.yaml b/.github/ISSUE_TEMPLATE/report.yaml index 9a2824c4..99a7a7a5 100644 --- a/.github/ISSUE_TEMPLATE/report.yaml +++ b/.github/ISSUE_TEMPLATE/report.yaml @@ -1,9 +1,6 @@ -name: "AhoyDTU bug" -description: "File a bug report" -title: "[Bug]" -labels: ["bug", "needs-triage"] -assignees: - - lumapu +name: "issue report" +description: "issue report" +labels: ["new", "needs-triage"] body: - type: markdown attributes: @@ -12,7 +9,7 @@ body: Wir lesen auch gerne Deutsch, bitte fülle die u.a. Fragen aus damit wir Dir bestmöglich helfen können Danke! Bitte unser FAQ als Hilfestellung prüfen: https://ahoydtu.de/faq - Please read, copy & fill in the template from our Posting Guide lines into your Support Forum post. + Please read, then copy & fill in the template from our Posting Guide lines into your Support Forum post. We do enjoy the english language, but we need a couple of things to best support you in your goal, please fill in all / most of the details given below. Thanks! Check our FAQ: https://ahoydtu.de/faq - type: markdown @@ -35,7 +32,7 @@ body: label: Assembly description: options: - - I did the assebly by myself + - I did the assembly by myself - the DTU was already assembled validations: required: true @@ -84,7 +81,7 @@ body: label: Connection picture description: options: - - label: I will attach/upload an Image of my wiring + - label: I will attach/upload an image of my wiring validations: required: true - type: markdown diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index c15bd883..cdd2cdf3 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -16,23 +16,25 @@ jobs: - uses: actions/checkout@v4 build-en: - name: Build Environments (English) + name: Build (EN) needs: check runs-on: ubuntu-latest continue-on-error: true strategy: matrix: variant: + - opendtufusion + - opendtufusion-16MB - esp8266 + - esp8266-all + - esp8266-minimal - esp8266-prometheus - esp8285 - esp32-wroom32 + - esp32-wroom32-minimal - esp32-wroom32-prometheus - - esp32-wroom32-ethernet - esp32-s2-mini - esp32-c3-mini - - opendtufusion - - opendtufusion-ethernet steps: - uses: actions/checkout@v4 - uses: benjlevesque/short-sha@v3.0 @@ -67,6 +69,11 @@ jobs: - name: Run PlatformIO run: pio run -d src -e ${{ matrix.variant }} + - name: Compress .elf + uses: edgarrc/action-7z@v1 + with: + args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf + - name: Rename Firmware run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT @@ -77,23 +84,23 @@ jobs: path: firmware/* build-de: - name: Build Environments (German) + name: Build (DE) needs: check runs-on: ubuntu-latest continue-on-error: true strategy: matrix: variant: + - opendtufusion-de + #- opendtufusion-16MB-de #not needed, only the partions.bin is different and can be used from english build - esp8266-de + - esp8266-all-de - esp8266-prometheus-de - esp8285-de - esp32-wroom32-de - esp32-wroom32-prometheus-de - - esp32-wroom32-ethernet-de - esp32-s2-mini-de - esp32-c3-mini-de - - opendtufusion-de - - opendtufusion-ethernet-de steps: - uses: actions/checkout@v4 - uses: benjlevesque/short-sha@v3.0 @@ -128,6 +135,11 @@ jobs: - name: Run PlatformIO run: pio run -d src -e ${{ matrix.variant }} + - name: Compress .elf + uses: edgarrc/action-7z@v1 + with: + args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf + - name: Rename Firmware run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT @@ -138,7 +150,7 @@ jobs: path: firmware/* deploy: - name: Deploy Environments + name: Update Artifacts / Deploy needs: [build-en, build-de] runs-on: ubuntu-latest continue-on-error: false @@ -164,9 +176,26 @@ jobs: env: VERSION: ${{ steps.version_name.outputs.name }} + + - name: Create ESP Web Tools Manifest + working-directory: src + run: python ../scripts/buildManifest.py + + - name: Copy install html + run: mv scripts/gh-action-dev-build-flash.html firmware/install.html + + - name: Copy Changes.md + run: mv src/CHANGES.md firmware/CHANGES.md + + - name: Rename firmware directory run: mv firmware ${{ steps.version_name.outputs.name }} + - name: delete environment Artifacts + uses: geekyeggo/delete-artifact@v4 + with: + name: dev-* + - name: Deploy uses: nogsantos/scp-deploy@master with: @@ -176,3 +205,17 @@ jobs: port: ${{ secrets.FW_SSH_PORT }} user: ${{ secrets.FW_SSH_USER }} key: ${{ secrets.FW_SSH_KEY }} + + - name: Clean elf files (7z compressed) for Artifact + run: | + rm -f \ + ${{ steps.version_name.outputs.name }}/*/*.elf.7z + + - name: Create Artifact + uses: actions/upload-artifact@v4 + with: + name: dev-${{ steps.version_name.outputs.name }} + path: | + ${{ steps.version_name.outputs.name }}/* + manual/User_Manual.md + manual/Getting_Started.md diff --git a/.github/workflows/compile_release.yml b/.github/workflows/compile_release.yml index 59adf28c..8ba1c06a 100644 --- a/.github/workflows/compile_release.yml +++ b/.github/workflows/compile_release.yml @@ -5,36 +5,103 @@ on: branches: main paths-ignore: - '**.md' # Do no build on *.md changes + - '**.yaml' # e.g. issue report jobs: - build: - name: Build Environments + check: + name: Check Repository runs-on: ubuntu-latest if: github.repository == 'lumapu/ahoy' && github.ref_name == 'main' continue-on-error: false + steps: + - uses: actions/checkout@v4 + + build-en: + name: Build (EN) + needs: check + runs-on: ubuntu-latest + continue-on-error: false strategy: matrix: variant: + - opendtufusion + - opendtufusion-16MB - esp8266 + - esp8266-all + - esp8266-minimal - esp8266-prometheus - esp8285 - esp32-wroom32 + - esp32-wroom32-minimal - esp32-wroom32-prometheus - - esp32-wroom32-ethernet - esp32-s2-mini - esp32-c3-mini - - opendtufusion - - opendtufusion-ethernet + steps: + - uses: actions/checkout@v4 + - uses: benjlevesque/short-sha@v3.0 + id: short-sha + with: + length: 7 + + - name: Cache Pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install PlatformIO + run: | + python -m pip install setuptools --upgrade pip + pip install --upgrade platformio + + - name: Run PlatformIO + run: pio run -d src -e ${{ matrix.variant }} + + - name: Compress .elf + uses: edgarrc/action-7z@v1 + with: + args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf + + - name: Rename Firmware + run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT + + - name: Create Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.variant }} + path: firmware/* + + build-de: + name: Build (DE) + needs: check + runs-on: ubuntu-latest + continue-on-error: false + strategy: + matrix: + variant: + - opendtufusion-de + #- opendtufusion-16MB-de #not needed, only the partions.bin is different and can be used from english build - esp8266-de + - esp8266-all-de - esp8266-prometheus-de - esp8285-de - esp32-wroom32-de - esp32-wroom32-prometheus-de - - esp32-wroom32-ethernet-de - esp32-s2-mini-de - esp32-c3-mini-de - - opendtufusion-de - - opendtufusion-ethernet-de steps: - uses: actions/checkout@v4 - uses: benjlevesque/short-sha@v3.0 @@ -69,6 +136,11 @@ jobs: - name: Run PlatformIO run: pio run -d src -e ${{ matrix.variant }} + - name: Compress .elf + uses: edgarrc/action-7z@v1 + with: + args: 7z a -t7z -mx=9 src/.pio/build/${{ matrix.variant }}/firmware.elf.7z ./src/.pio/build/${{ matrix.variant }}/firmware.elf + - name: Rename Firmware run: python scripts/getVersion.py ${{ matrix.variant }} >> $GITHUB_OUTPUT @@ -78,11 +150,10 @@ jobs: name: ${{ matrix.variant }} path: firmware/* - release: name: Create Release + needs: [build-en, build-de] runs-on: ubuntu-latest - needs: [build] continue-on-error: false permissions: contents: write @@ -140,7 +211,7 @@ jobs: deploy: name: Deploy Environments to fw.ahoydtu.de - needs: [build, release] + needs: [build-en, build-de, release] runs-on: ubuntu-latest continue-on-error: false steps: diff --git a/.gitignore b/.gitignore index 21ae2a57..0df18078 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ src/config/config_override.h src/web/html/h/* src/web/html/tmp/* +src/data/* /**/Debug /**/v16/* *.db @@ -15,3 +16,4 @@ src/web/html/tmp/* src/output.map /.venv +/scripts/__pycache__/* diff --git a/README.md b/README.md index 16b2a256..07d60002 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ This repository provides hardware and software solutions for communicating with ## Changelog [latest Release](https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md) + [Development Version](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md) @@ -31,10 +32,9 @@ Table of approaches: | Board | MI | HM | HMS/HMT | comment | HowTo start | | ------ | -- | -- | ------- | ------- | ---------- | -| [ESP8266/ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) | -| [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | -| [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | | -| [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | +| [ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | [create your own DTU](https://ahoydtu.de/getting_started/) | +| ESP8266, C++ | ✔️ | ✔️ | ❌ | ⚠️ not recommended for new DTU | + ⚠️ **Warning: HMS-XXXXW-2T WiFi inverters are not supported. They have a 'W' in their name and a DTU serial number on its sticker** @@ -48,6 +48,8 @@ Table of approaches: ## Our Website [https://ahoydtu.de](https://ahoydtu.de) +[Firmware Archive https://fw.ahoydtu.de](https://fw.ahoydtu.de) + ## Success Stories - [Getting the data into influxDB and visualize them in a Grafana Dashboard](https://grafana.com/grafana/dashboards/16850-pv-power-ahoy/) (thx @Carl) diff --git a/manual/Getting_Started.md b/manual/Getting_Started.md index b1ef7995..33637e6f 100644 --- a/manual/Getting_Started.md +++ b/manual/Getting_Started.md @@ -17,6 +17,7 @@ Hoymiles Inverters | ✔️ | HMS | 350, 500, 800, 1000, 1600, 1800, 2000 | | | ✔️ | HMT | 1600, 1800, 2250 | | | ⚠️ | TSUN | [TSOL-M350](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M400](https://www.tsun-ess.com/Micro-Inverter/M350-M400), [TSOL-M800/TSOL-M800(DE)](https://www.tsun-ess.com/Micro-Inverter/M800) | others may work as well (need to be verified). | +| 🟡 | HERF | (supported) | | ## Table of Contents @@ -25,16 +26,16 @@ Hoymiles Inverters - [Things needed](#things-needed) - [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there) - [Wiring things up](#wiring-things-up) + - [ESP32 wiring example](#esp32-wiring-example) + - [Schematic](#schematic-1) + - [Symbolic view](#symbolic-view-1) + - [ESP32 GPIO settings](#esp32-gpio-settings) - [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example) - [Schematic](#schematic) - [Symbolic view](#symbolic-view) - [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2) - [Schematic](#schematic-2) - [Symbolic view](#symbolic-view-2) - - [ESP32 wiring example](#esp32-wiring-example) - - [Schematic](#schematic-1) - - [Symbolic view](#symbolic-view-1) - - [ESP32 GPIO settings](#esp32-gpio-settings) - [Flash the Firmware on your Ahoy DTU Hardware](#flash-the-firmware-on-your-ahoy-dtu-hardware) - [Compiling your own Version](#compiling-your-own-version) - [Using a ready-to-flash binary using nodemcu-pyflasher](#using-a-ready-to-flash-binary-using-nodemcu-pyflasher) @@ -56,9 +57,10 @@ Solenso Inverters: To build your own AhoyDTU, you only need a few things. Remember that the maker community is always developing new and innovative options that we may not have covered in this readme. -Start with an ESP8266 or ESP32, and combine it with an NRF24L01+ breakout board. Other ESP boards with at least 4MBytes of ROM may also be suitable. +Start with an ESP32 or ESP8266 (not recommended), and combine it with an NRF24L01+ breakout board for HM-series inverters. To communicate with a HMS or HMT inverter you need to use a CMT2300A radio module. +Other ESP boards with at least 4MBytes of ROM may also be suitable. -Make sure to choose an NRF24L01+ module that includes the '+' in its name. This is important because we need the 250kbps features that are only available in the plus-variant. +Note for NRF24 radio module: Make sure to choose an NRF24L01+ module that includes the '+' in its name. This is important because we need the 250kbps features that are only available in the plus-variant. **Attention**: The NRF24L01+ can only communicate with the MI/HM/TSUN inverter. For the HMS/HMT it is needed to use a CMT2300A! diff --git a/manual/factory_firmware.md b/manual/factory_firmware.md new file mode 100644 index 00000000..047dd4b0 --- /dev/null +++ b/manual/factory_firmware.md @@ -0,0 +1,56 @@ +# Generate factory firmware (ESP32) + +If the firmware should already contain predefined settings this guide will help you to compile these into a single binary file. + +## Generate default settings + +First install on the requested platform the standard firmware and configure everything to your needs. Once you did all changes store them and export them to a `json` file. + +## Further prepare default settings + +First create a directory `data` inside the following project path: `src/`. + +As the export removes all your passwords you need to add them again to the `json` file. Open the `json` file with a text editor and search for all the `"pwd":""` sections. Between the second bunch of quotation marks you have to place the password. + +*Note: It's recommended to keep all information in one line to save space on the ESP littlefs partition* + +Next rename your export file to `settings.json` and move it to the new created directory. It should be look similar to this: + +``` +ahoy + |-- src + |-- data + |-- settings.json + |-- config + |-- network + ... +``` + +## build firmware + +Choose your prefered environment and build firmware as usual. Once the process is finished you should find along with the standard `firmware.bin` an additional file called `firmware.factory.bin`. Both files are located here: `src/.pio/build/[ENVIRONMENT]/` + +## Upload to device + +Navigate to the firmware output directory `src/.pio/build/[ENVIRONMENT]/` and open a terminal or vice versa. + +Python: +`esptool.py -b 921600 write_flash --flash_mode dio --flash_size detect 0x0 firmware.factory.bin` + +Windows: +`esptool.exe -b 921600 write_flash --flash_mode dio --flash_size detect 0x0 firmware.factory.bin` + +The upload should be finished within one minute. + +## Testing + +Reboot your ESP an check if all your settings are present. + +## Get updated with 'Mainline' + +From time to time a new version of AhoyDTU will be published. To get the changes into your already prepared factory binary generation environment you have to do only a few steps: + +1. pull new changes from remote: `git pull` +2. check if the `data` folder is still there and contains the `settings.json` +3. build and upload +4. enjoy diff --git a/patches/AsyncWeb_Prometheus.patch b/patches/AsyncWeb_Prometheus.patch index 21fe22cd..3c7deac4 100644 --- a/patches/AsyncWeb_Prometheus.patch +++ b/patches/AsyncWeb_Prometheus.patch @@ -1,26 +1,26 @@ diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp -index 12be5f8..cffeed7 100644 +index 6e88da9..09359c3 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp -@@ -737,7 +737,7 @@ void AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len) - IPAddress AsyncWebSocketClient::remoteIP() const - { - if (!_client) -- return IPAddress(0U); -+ return IPAddress(); +@@ -827,7 +827,7 @@ void AsyncWebSocketClient::binary(AsyncWebSocketMessageBuffer * buffer) + IPAddress AsyncWebSocketClient::remoteIP() { + if(!_client) { +- return IPAddress((uint32_t)0); ++ return IPAddress(); + } return _client->remoteIP(); } diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp -index 22a549f..e0b36b3 100644 +index a22e991..babef18 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp -@@ -318,7 +318,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u +@@ -317,7 +317,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u free(buf); return 0; } -- outLen = sprintf_P((char*)buf+headLen, PSTR("%x"), readLen) + headLen; -+ outLen = sprintf_P((char*)buf+headLen, PSTR("%04x"), readLen) + headLen; +- outLen = sprintf((char*)buf+headLen, "%x", readLen) + headLen; ++ outLen = sprintf((char*)buf+headLen, "%04x", readLen) + headLen; while(outLen < headLen + 4) buf[outLen++] = ' '; buf[outLen++] = '\r'; buf[outLen++] = '\n'; diff --git a/patches/GxEPD2_HAL.patch b/patches/GxEPD2_HAL.patch new file mode 100644 index 00000000..d7b394eb --- /dev/null +++ b/patches/GxEPD2_HAL.patch @@ -0,0 +1,392 @@ +diff --git a/src/GxEPD2_EPD.cpp b/src/GxEPD2_EPD.cpp +index 8df8bef..e9dfb19 100644 +--- a/src/GxEPD2_EPD.cpp ++++ b/src/GxEPD2_EPD.cpp +@@ -17,11 +17,10 @@ + #include <avr/pgmspace.h> + #endif + +-GxEPD2_EPD::GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout, ++GxEPD2_EPD::GxEPD2_EPD(GxEPD2_HalInterface *hal, int16_t busy_level, uint32_t busy_timeout, + uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu) : + WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu), +- _cs(cs), _dc(dc), _rst(rst), _busy(busy), _busy_level(busy_level), _busy_timeout(busy_timeout), _diag_enabled(false), +- _pSPIx(&SPI), _spi_settings(4000000, MSBFIRST, SPI_MODE0) ++ _hal(hal), _busy_level(busy_level), _busy_timeout(busy_timeout), _diag_enabled(false) + { + _initial_write = true; + _initial_refresh = true; +@@ -54,44 +53,10 @@ void GxEPD2_EPD::init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset + Serial.begin(serial_diag_bitrate); + _diag_enabled = true; + } +- if (_cs >= 0) +- { +- digitalWrite(_cs, HIGH); // preset (less glitch for any analyzer) +- pinMode(_cs, OUTPUT); +- digitalWrite(_cs, HIGH); // set (needed e.g. for RP2040) +- } +- if (_dc >= 0) +- { +- digitalWrite(_dc, HIGH); // preset (less glitch for any analyzer) +- pinMode(_dc, OUTPUT); +- digitalWrite(_dc, HIGH); // set (needed e.g. for RP2040) +- } +- _reset(); +- if (_busy >= 0) +- { +- pinMode(_busy, INPUT); +- } +- _pSPIx->begin(); +- if (_busy == MISO) // may be overridden +- { +- pinMode(_busy, INPUT); +- } +- if (_dc == MISO) // may be overridden, TTGO T5 V2.66 +- { +- pinMode(_dc, OUTPUT); +- } +- if (_cs == MISO) // may be overridden +- { +- pinMode(_cs, INPUT); +- } + } + + void GxEPD2_EPD::end() + { +- _pSPIx->end(); +- if (_cs >= 0) pinMode(_cs, INPUT); +- if (_dc >= 0) pinMode(_dc, INPUT); +- if (_rst >= 0) pinMode(_rst, INPUT); + } + + void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter) +@@ -100,34 +65,27 @@ void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* + _busy_callback_parameter = busy_callback_parameter; + } + +-void GxEPD2_EPD::selectSPI(SPIClass& spi, SPISettings spi_settings) +-{ +- _pSPIx = &spi; +- _spi_settings = spi_settings; +-} +- + void GxEPD2_EPD::_reset() + { +- if (_rst >= 0) + { + if (_pulldown_rst_mode) + { +- digitalWrite(_rst, LOW); +- pinMode(_rst, OUTPUT); +- digitalWrite(_rst, LOW); ++ _hal->rst(LOW); ++ _hal->rstMode(OUTPUT); ++ _hal->rst(LOW); + delay(_reset_duration); +- pinMode(_rst, INPUT_PULLUP); ++ _hal->rstMode(INPUT_PULLUP); + delay(_reset_duration > 10 ? _reset_duration : 10); + } + else + { +- digitalWrite(_rst, HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, preset (less glitch for any analyzer) +- pinMode(_rst, OUTPUT); +- digitalWrite(_rst, HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, set (needed e.g. for RP2040) ++ _hal->rst(HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, preset (less glitch for any analyzer) ++ _hal->rstMode(OUTPUT); ++ _hal->rst(HIGH); // NEEDED for Waveshare "clever" reset circuit, power controller before reset pulse, set (needed e.g. for RP2040) + delay(10); // NEEDED for Waveshare "clever" reset circuit, at least delay(2); +- digitalWrite(_rst, LOW); ++ _hal->rst(LOW); + delay(_reset_duration); +- digitalWrite(_rst, HIGH); ++ _hal->rst(HIGH); + delay(_reset_duration > 10 ? _reset_duration : 10); + } + _hibernating = false; +@@ -136,16 +94,15 @@ void GxEPD2_EPD::_reset() + + void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) + { +- if (_busy >= 0) + { + delay(1); // add some margin to become active + unsigned long start = micros(); + while (1) + { +- if (digitalRead(_busy) != _busy_level) break; ++ if (_hal->getBusy() != _busy_level) break; + if (_busy_callback) _busy_callback(_busy_callback_parameter); + else delay(1); +- if (digitalRead(_busy) != _busy_level) break; ++ if (_hal->getBusy() != _busy_level) break; + if (micros() - start > _busy_timeout) + { + Serial.println("Busy Timeout!"); +@@ -169,120 +126,59 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) + } + (void) start; + } +- else delay(busy_time); + } + + void GxEPD2_EPD::_writeCommand(uint8_t c) + { +- _pSPIx->beginTransaction(_spi_settings); +- if (_dc >= 0) digitalWrite(_dc, LOW); +- if (_cs >= 0) digitalWrite(_cs, LOW); +- _pSPIx->transfer(c); +- if (_cs >= 0) digitalWrite(_cs, HIGH); +- if (_dc >= 0) digitalWrite(_dc, HIGH); +- _pSPIx->endTransaction(); ++ _hal->writeCmd(c); + } + + void GxEPD2_EPD::_writeData(uint8_t d) + { +- _pSPIx->beginTransaction(_spi_settings); +- if (_cs >= 0) digitalWrite(_cs, LOW); +- _pSPIx->transfer(d); +- if (_cs >= 0) digitalWrite(_cs, HIGH); +- _pSPIx->endTransaction(); ++ _hal->write(d); + } + + void GxEPD2_EPD::_writeData(const uint8_t* data, uint16_t n) + { +- _pSPIx->beginTransaction(_spi_settings); +- if (_cs >= 0) digitalWrite(_cs, LOW); +- for (uint16_t i = 0; i < n; i++) +- { +- _pSPIx->transfer(*data++); +- } +- if (_cs >= 0) digitalWrite(_cs, HIGH); +- _pSPIx->endTransaction(); ++ _hal->write(data, n); + } + + void GxEPD2_EPD::_writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) + { +- _pSPIx->beginTransaction(_spi_settings); +- if (_cs >= 0) digitalWrite(_cs, LOW); +- for (uint16_t i = 0; i < n; i++) +- { +- _pSPIx->transfer(pgm_read_byte(&*data++)); +- } +- while (fill_with_zeroes > 0) +- { +- _pSPIx->transfer(0x00); +- fill_with_zeroes--; +- } +- if (_cs >= 0) digitalWrite(_cs, HIGH); +- _pSPIx->endTransaction(); ++ _hal->write(data, n, fill_with_zeroes); + } + + void GxEPD2_EPD::_writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) + { +- _pSPIx->beginTransaction(_spi_settings); +- for (uint8_t i = 0; i < n; i++) +- { +- if (_cs >= 0) digitalWrite(_cs, LOW); +- _pSPIx->transfer(pgm_read_byte(&*data++)); +- if (_cs >= 0) digitalWrite(_cs, HIGH); +- } +- while (fill_with_zeroes > 0) +- { +- if (_cs >= 0) digitalWrite(_cs, LOW); +- _pSPIx->transfer(0x00); +- fill_with_zeroes--; +- if (_cs >= 0) digitalWrite(_cs, HIGH); ++ _hal->write(data, n); ++ if (fill_with_zeroes > 0) { ++ uint8_t buf[fill_with_zeroes]; ++ memset(buf, 0, fill_with_zeroes); ++ _hal->write(buf, fill_with_zeroes); + } +- _pSPIx->endTransaction(); + } + + void GxEPD2_EPD::_writeCommandData(const uint8_t* pCommandData, uint8_t datalen) + { +- _pSPIx->beginTransaction(_spi_settings); +- if (_dc >= 0) digitalWrite(_dc, LOW); +- if (_cs >= 0) digitalWrite(_cs, LOW); +- _pSPIx->transfer(*pCommandData++); +- if (_dc >= 0) digitalWrite(_dc, HIGH); +- for (uint8_t i = 0; i < datalen - 1; i++) // sub the command +- { +- _pSPIx->transfer(*pCommandData++); +- } +- if (_cs >= 0) digitalWrite(_cs, HIGH); +- _pSPIx->endTransaction(); ++ _hal->writeCmd(pCommandData, datalen, false); + } + + void GxEPD2_EPD::_writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen) + { +- _pSPIx->beginTransaction(_spi_settings); +- if (_dc >= 0) digitalWrite(_dc, LOW); +- if (_cs >= 0) digitalWrite(_cs, LOW); +- _pSPIx->transfer(pgm_read_byte(&*pCommandData++)); +- if (_dc >= 0) digitalWrite(_dc, HIGH); +- for (uint8_t i = 0; i < datalen - 1; i++) // sub the command +- { +- _pSPIx->transfer(pgm_read_byte(&*pCommandData++)); +- } +- if (_cs >= 0) digitalWrite(_cs, HIGH); +- _pSPIx->endTransaction(); ++ _hal->writeCmd(pCommandData, datalen, true); + } + + void GxEPD2_EPD::_startTransfer() + { +- _pSPIx->beginTransaction(_spi_settings); +- if (_cs >= 0) digitalWrite(_cs, LOW); ++ _hal->startTransfer(); + } + + void GxEPD2_EPD::_transfer(uint8_t value) + { +- _pSPIx->transfer(value); ++ _hal->transfer(value); + } + + void GxEPD2_EPD::_endTransfer() + { +- if (_cs >= 0) digitalWrite(_cs, HIGH); +- _pSPIx->endTransaction(); ++ _hal->endTransfer(); + } +diff --git a/src/GxEPD2_EPD.h b/src/GxEPD2_EPD.h +index 34c1145..1e8ea64 100644 +--- a/src/GxEPD2_EPD.h ++++ b/src/GxEPD2_EPD.h +@@ -13,9 +13,9 @@ + #define _GxEPD2_EPD_H_ + + #include <Arduino.h> +-#include <SPI.h> + + #include <GxEPD2.h> ++#include <GxEPD2_Hal.h> + + #pragma GCC diagnostic ignored "-Wunused-parameter" + //#pragma GCC diagnostic ignored "-Wsign-compare" +@@ -31,7 +31,7 @@ class GxEPD2_EPD + const bool hasPartialUpdate; + const bool hasFastPartialUpdate; + // constructor +- GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout, ++ GxEPD2_EPD(GxEPD2_HalInterface *hal, int16_t busy_level, uint32_t busy_timeout, + uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu); + virtual void init(uint32_t serial_diag_bitrate = 0); // serial_diag_bitrate = 0 : disabled + virtual void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 10, bool pulldown_rst_mode = false); +@@ -97,7 +97,6 @@ class GxEPD2_EPD + { + return (a > b ? a : b); + }; +- void selectSPI(SPIClass& spi, SPISettings spi_settings); + protected: + void _reset(); + void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000); +@@ -112,16 +111,15 @@ class GxEPD2_EPD + void _transfer(uint8_t value); + void _endTransfer(); + protected: +- int16_t _cs, _dc, _rst, _busy, _busy_level; ++ GxEPD2_HalInterface *_hal; ++ int16_t _busy_level; + uint32_t _busy_timeout; + bool _diag_enabled, _pulldown_rst_mode; +- SPIClass* _pSPIx; +- SPISettings _spi_settings; + bool _initial_write, _initial_refresh; + bool _power_is_on, _using_partial_mode, _hibernating; + bool _init_display_done; + uint16_t _reset_duration; +- void (*_busy_callback)(const void*); ++ void (*_busy_callback)(const void*); + const void* _busy_callback_parameter; + }; + +diff --git a/src/GxEPD2_Hal.h b/src/GxEPD2_Hal.h +new file mode 100644 +index 0000000..13424b6 +--- /dev/null ++++ b/src/GxEPD2_Hal.h +@@ -0,0 +1,19 @@ ++#pragma once ++ ++class GxEPD2_HalInterface { ++ public: ++ virtual void rstMode(uint8_t mode) = 0; ++ virtual void rst(bool level) = 0; ++ virtual int getBusy(void) = 0; ++ virtual bool isRst(void) = 0; ++ ++ virtual void write(uint8_t buf) = 0; ++ virtual void write(const uint8_t *buf, uint16_t n) = 0; ++ virtual void write(const uint8_t *buf, uint16_t n, int16_t fill_with_zeroes) = 0; ++ virtual void writeCmd(const uint8_t val) = 0; ++ virtual void writeCmd(const uint8_t* pCommandData, uint8_t datalen, bool isPGM) = 0; ++ ++ virtual void startTransfer(void) = 0; ++ virtual void endTransfer(void) = 0; ++ virtual void transfer(const uint8_t val) = 0; ++}; +diff --git a/src/epd/GxEPD2_150_BN.cpp b/src/epd/GxEPD2_150_BN.cpp +index bfb3ddf..dba3d78 100644 +--- a/src/epd/GxEPD2_150_BN.cpp ++++ b/src/epd/GxEPD2_150_BN.cpp +@@ -14,8 +14,8 @@ + + #include "GxEPD2_150_BN.h" + +-GxEPD2_150_BN::GxEPD2_150_BN(int16_t cs, int16_t dc, int16_t rst, int16_t busy) : +- GxEPD2_EPD(cs, dc, rst, busy, HIGH, 10000000, WIDTH, HEIGHT, panel, hasColor, hasPartialUpdate, hasFastPartialUpdate) ++GxEPD2_150_BN::GxEPD2_150_BN(GxEPD2_HalInterface *hal) : ++ GxEPD2_EPD(hal, HIGH, 10000000, WIDTH, HEIGHT, panel, hasColor, hasPartialUpdate, hasFastPartialUpdate) + { + } + +@@ -269,7 +269,7 @@ void GxEPD2_150_BN::refresh(int16_t x, int16_t y, int16_t w, int16_t h) + int16_t y1 = y < 0 ? 0 : y; // limit + w1 = x1 + w1 < int16_t(WIDTH) ? w1 : int16_t(WIDTH) - x1; // limit + h1 = y1 + h1 < int16_t(HEIGHT) ? h1 : int16_t(HEIGHT) - y1; // limit +- if ((w1 <= 0) || (h1 <= 0)) return; ++ if ((w1 <= 0) || (h1 <= 0)) return; + // make x1, w1 multiple of 8 + w1 += x1 % 8; + if (w1 % 8 > 0) w1 += 8 - w1 % 8; +@@ -287,7 +287,7 @@ void GxEPD2_150_BN::powerOff() + void GxEPD2_150_BN::hibernate() + { + _PowerOff(); +- if (_rst >= 0) ++ if (_hal->isRst()) + { + _writeCommand(0x10); // deep sleep mode + _writeData(0x1); // enter deep sleep +diff --git a/src/epd/GxEPD2_150_BN.h b/src/epd/GxEPD2_150_BN.h +index bc46a45..954b9c4 100644 +--- a/src/epd/GxEPD2_150_BN.h ++++ b/src/epd/GxEPD2_150_BN.h +@@ -16,6 +16,7 @@ + #define _GxEPD2_150_BN_H_ + + #include "../GxEPD2_EPD.h" ++#include "../GxEPD2_Hal.h" + + class GxEPD2_150_BN : public GxEPD2_EPD + { +@@ -33,7 +34,7 @@ class GxEPD2_150_BN : public GxEPD2_EPD + static const uint16_t full_refresh_time = 4000; // ms, e.g. 3825000us + static const uint16_t partial_refresh_time = 800; // ms, e.g. 736000us + // constructor +- GxEPD2_150_BN(int16_t cs, int16_t dc, int16_t rst, int16_t busy); ++ GxEPD2_150_BN(GxEPD2_HalInterface *hal); + // methods (virtual) + // Support for Bitmaps (Sprites) to Controller Buffer and to Screen + void clearScreen(uint8_t value = 0xFF); // init controller memory and screen (default white) diff --git a/patches/GxEPD2_SW_SPI.patch b/patches/GxEPD2_SW_SPI.patch deleted file mode 100644 index dc3fa9ca..00000000 --- a/patches/GxEPD2_SW_SPI.patch +++ /dev/null @@ -1,362 +0,0 @@ -diff --git a/src/GxEPD2_EPD.cpp b/src/GxEPD2_EPD.cpp -index 8df8bef..91d7f49 100644 ---- a/src/GxEPD2_EPD.cpp -+++ b/src/GxEPD2_EPD.cpp -@@ -19,9 +19,9 @@ - - GxEPD2_EPD::GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout, - uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu) : -- WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu), -+ WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu), _sck(-1), _mosi(-1), - _cs(cs), _dc(dc), _rst(rst), _busy(busy), _busy_level(busy_level), _busy_timeout(busy_timeout), _diag_enabled(false), -- _pSPIx(&SPI), _spi_settings(4000000, MSBFIRST, SPI_MODE0) -+ _spi_settings(4000000, MSBFIRST, SPI_MODE0) - { - _initial_write = true; - _initial_refresh = true; -@@ -71,27 +71,30 @@ void GxEPD2_EPD::init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset - { - pinMode(_busy, INPUT); - } -- _pSPIx->begin(); -- if (_busy == MISO) // may be overridden -- { -- pinMode(_busy, INPUT); -- } -- if (_dc == MISO) // may be overridden, TTGO T5 V2.66 -- { -- pinMode(_dc, OUTPUT); -- } -- if (_cs == MISO) // may be overridden -+ if (_sck < 0) SPI.begin(); -+} -+ -+void GxEPD2_EPD::init(int16_t sck, int16_t mosi, uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration, bool pulldown_rst_mode) -+{ -+ if ((sck >= 0) && (mosi >= 0)) - { -- pinMode(_cs, INPUT); -- } -+ _sck = sck; -+ _mosi = mosi; -+ digitalWrite(_sck, LOW); -+ digitalWrite(_mosi, LOW); -+ pinMode(_sck, OUTPUT); -+ pinMode(_mosi, OUTPUT); -+ } else _sck = -1; -+ init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode); - } - - void GxEPD2_EPD::end() - { -- _pSPIx->end(); - if (_cs >= 0) pinMode(_cs, INPUT); - if (_dc >= 0) pinMode(_dc, INPUT); - if (_rst >= 0) pinMode(_rst, INPUT); -+ if (_sck >= 0) pinMode(_sck, INPUT); -+ if (_mosi >= 0) pinMode(_mosi, INPUT); - } - - void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter) -@@ -100,12 +103,6 @@ void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* - _busy_callback_parameter = busy_callback_parameter; - } - --void GxEPD2_EPD::selectSPI(SPIClass& spi, SPISettings spi_settings) --{ -- _pSPIx = &spi; -- _spi_settings = spi_settings; --} -- - void GxEPD2_EPD::_reset() - { - if (_rst >= 0) -@@ -174,115 +171,201 @@ void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) - - void GxEPD2_EPD::_writeCommand(uint8_t c) - { -- _pSPIx->beginTransaction(_spi_settings); -+ _beginTransaction(_spi_settings); - if (_dc >= 0) digitalWrite(_dc, LOW); - if (_cs >= 0) digitalWrite(_cs, LOW); -- _pSPIx->transfer(c); -+ _spi_write(c); - if (_cs >= 0) digitalWrite(_cs, HIGH); - if (_dc >= 0) digitalWrite(_dc, HIGH); -- _pSPIx->endTransaction(); -+ _endTransaction(); - } - - void GxEPD2_EPD::_writeData(uint8_t d) - { -- _pSPIx->beginTransaction(_spi_settings); -+ _beginTransaction(_spi_settings); - if (_cs >= 0) digitalWrite(_cs, LOW); -- _pSPIx->transfer(d); -+ _spi_write(d); - if (_cs >= 0) digitalWrite(_cs, HIGH); -- _pSPIx->endTransaction(); -+ _endTransaction(); - } - - void GxEPD2_EPD::_writeData(const uint8_t* data, uint16_t n) - { -- _pSPIx->beginTransaction(_spi_settings); -+ _beginTransaction(_spi_settings); - if (_cs >= 0) digitalWrite(_cs, LOW); -- for (uint16_t i = 0; i < n; i++) -+ for (uint8_t i = 0; i < n; i++) - { -- _pSPIx->transfer(*data++); -+ _spi_write(*data++); - } - if (_cs >= 0) digitalWrite(_cs, HIGH); -- _pSPIx->endTransaction(); -+ _endTransaction(); - } - - void GxEPD2_EPD::_writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) - { -- _pSPIx->beginTransaction(_spi_settings); -+ _beginTransaction(_spi_settings); - if (_cs >= 0) digitalWrite(_cs, LOW); -- for (uint16_t i = 0; i < n; i++) -+ for (uint8_t i = 0; i < n; i++) - { -- _pSPIx->transfer(pgm_read_byte(&*data++)); -+ _spi_write(pgm_read_byte(&*data++)); - } - while (fill_with_zeroes > 0) - { -- _pSPIx->transfer(0x00); -+ _spi_write(0x00); - fill_with_zeroes--; - } - if (_cs >= 0) digitalWrite(_cs, HIGH); -- _pSPIx->endTransaction(); -+ _endTransaction(); - } - - void GxEPD2_EPD::_writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) - { -- _pSPIx->beginTransaction(_spi_settings); -+ _beginTransaction(_spi_settings); - for (uint8_t i = 0; i < n; i++) - { - if (_cs >= 0) digitalWrite(_cs, LOW); -- _pSPIx->transfer(pgm_read_byte(&*data++)); -+ _spi_write(pgm_read_byte(&*data++)); - if (_cs >= 0) digitalWrite(_cs, HIGH); - } - while (fill_with_zeroes > 0) - { - if (_cs >= 0) digitalWrite(_cs, LOW); -- _pSPIx->transfer(0x00); -+ _spi_write(0x00); - fill_with_zeroes--; - if (_cs >= 0) digitalWrite(_cs, HIGH); - } -- _pSPIx->endTransaction(); -+ _endTransaction(); - } - - void GxEPD2_EPD::_writeCommandData(const uint8_t* pCommandData, uint8_t datalen) - { -- _pSPIx->beginTransaction(_spi_settings); -+ _beginTransaction(_spi_settings); - if (_dc >= 0) digitalWrite(_dc, LOW); - if (_cs >= 0) digitalWrite(_cs, LOW); -- _pSPIx->transfer(*pCommandData++); -+ _spi_write(*pCommandData++); - if (_dc >= 0) digitalWrite(_dc, HIGH); - for (uint8_t i = 0; i < datalen - 1; i++) // sub the command - { -- _pSPIx->transfer(*pCommandData++); -+ _spi_write(*pCommandData++); - } - if (_cs >= 0) digitalWrite(_cs, HIGH); -- _pSPIx->endTransaction(); -+ _endTransaction(); - } - - void GxEPD2_EPD::_writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen) - { -- _pSPIx->beginTransaction(_spi_settings); -+ _beginTransaction(_spi_settings); - if (_dc >= 0) digitalWrite(_dc, LOW); - if (_cs >= 0) digitalWrite(_cs, LOW); -- _pSPIx->transfer(pgm_read_byte(&*pCommandData++)); -+ _spi_write(pgm_read_byte(&*pCommandData++)); - if (_dc >= 0) digitalWrite(_dc, HIGH); - for (uint8_t i = 0; i < datalen - 1; i++) // sub the command - { -- _pSPIx->transfer(pgm_read_byte(&*pCommandData++)); -+ _spi_write(pgm_read_byte(&*pCommandData++)); - } - if (_cs >= 0) digitalWrite(_cs, HIGH); -- _pSPIx->endTransaction(); -+ _endTransaction(); - } - - void GxEPD2_EPD::_startTransfer() - { -- _pSPIx->beginTransaction(_spi_settings); -+ _beginTransaction(_spi_settings); - if (_cs >= 0) digitalWrite(_cs, LOW); - } - - void GxEPD2_EPD::_transfer(uint8_t value) - { -- _pSPIx->transfer(value); -+ _spi_write(value); - } - - void GxEPD2_EPD::_endTransfer() - { - if (_cs >= 0) digitalWrite(_cs, HIGH); -- _pSPIx->endTransaction(); -+ _endTransaction(); -+} -+ -+void GxEPD2_EPD::_beginTransaction(const SPISettings& settings) -+{ -+ if (_sck < 0) SPI.beginTransaction(settings); -+} -+ -+void GxEPD2_EPD::_spi_write(uint8_t data) -+{ -+ if (_sck < 0) SPI.transfer(data); -+ else -+ { -+#if defined (ESP8266) -+ yield(); -+#endif -+ for (int i = 0; i < 8; i++) -+ { -+ digitalWrite(_mosi, (data & 0x80) ? HIGH : LOW); -+ data <<= 1; -+ digitalWrite(_sck, HIGH); -+ digitalWrite(_sck, LOW); -+ } -+ } -+} -+ -+void GxEPD2_EPD::_endTransaction() -+{ -+ if (_sck < 0) SPI.endTransaction(); -+} -+ -+uint8_t GxEPD2_EPD::_readData() -+{ -+ uint8_t data = 0; -+ _beginTransaction(_spi_settings); -+ if (_cs >= 0) digitalWrite(_cs, LOW); -+ if (_sck < 0) -+ { -+ data = SPI.transfer(0); -+ } -+ else -+ { -+ pinMode(_mosi, INPUT); -+ for (int i = 0; i < 8; i++) -+ { -+ data <<= 1; -+ digitalWrite(_sck, HIGH); -+ data |= digitalRead(_mosi); -+ digitalWrite(_sck, LOW); -+ } -+ pinMode(_mosi, OUTPUT); -+ } -+ if (_cs >= 0) digitalWrite(_cs, HIGH); -+ _endTransaction(); -+ return data; -+} -+ -+void GxEPD2_EPD::_readData(uint8_t* data, uint16_t n) -+{ -+ _beginTransaction(_spi_settings); -+ if (_cs >= 0) digitalWrite(_cs, LOW); -+ if (_sck < 0) -+ { -+ for (uint8_t i = 0; i < n; i++) -+ { -+ *data++ = SPI.transfer(0); -+ } -+ } -+ else -+ { -+ pinMode(_mosi, INPUT); -+ for (uint8_t i = 0; i < n; i++) -+ { -+ *data = 0; -+ for (int i = 0; i < 8; i++) -+ { -+ *data <<= 1; -+ digitalWrite(_sck, HIGH); -+ *data |= digitalRead(_mosi); -+ digitalWrite(_sck, LOW); -+ } -+ data++; -+ } -+ pinMode(_mosi, OUTPUT); -+ } -+ if (_cs >= 0) digitalWrite(_cs, HIGH); -+ _endTransaction(); - } -diff --git a/src/GxEPD2_EPD.h b/src/GxEPD2_EPD.h -index 34c1145..c480b7d 100644 ---- a/src/GxEPD2_EPD.h -+++ b/src/GxEPD2_EPD.h -@@ -8,6 +8,10 @@ - // Version: see library.properties - // - // Library: https://github.com/ZinggJM/GxEPD2 -+// To use SW SPI with GxEPD2: -+// add the special call to the added init method BEFORE the normal init method: -+// display.epd2.init(SW_SCK, SW_MOSI, 115200, true, 20, false); // define or replace SW_SCK, SW_MOSI -+// display.init(115200); // needed to init upper level - - #ifndef _GxEPD2_EPD_H_ - #define _GxEPD2_EPD_H_ -@@ -35,6 +39,7 @@ class GxEPD2_EPD - uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu); - virtual void init(uint32_t serial_diag_bitrate = 0); // serial_diag_bitrate = 0 : disabled - virtual void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 10, bool pulldown_rst_mode = false); -+ virtual void init(int16_t sck, int16_t mosi, uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false); - virtual void end(); // release SPI and control pins - // Support for Bitmaps (Sprites) to Controller Buffer and to Screen - virtual void clearScreen(uint8_t value) = 0; // init controller memory and screen (default white) -@@ -97,7 +102,6 @@ class GxEPD2_EPD - { - return (a > b ? a : b); - }; -- void selectSPI(SPIClass& spi, SPISettings spi_settings); - protected: - void _reset(); - void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000); -@@ -111,17 +115,22 @@ class GxEPD2_EPD - void _startTransfer(); - void _transfer(uint8_t value); - void _endTransfer(); -+ void _beginTransaction(const SPISettings& settings); -+ void _spi_write(uint8_t data); -+ void _endTransaction(); -+ public: -+ uint8_t _readData(); -+ void _readData(uint8_t* data, uint16_t n); - protected: -- int16_t _cs, _dc, _rst, _busy, _busy_level; -+ int16_t _cs, _dc, _rst, _busy, _busy_level, _sck, _mosi;; - uint32_t _busy_timeout; - bool _diag_enabled, _pulldown_rst_mode; -- SPIClass* _pSPIx; - SPISettings _spi_settings; - bool _initial_write, _initial_refresh; - bool _power_is_on, _using_partial_mode, _hibernating; - bool _init_display_done; - uint16_t _reset_duration; -- void (*_busy_callback)(const void*); -+ void (*_busy_callback)(const void*); - const void* _busy_callback_parameter; - }; - diff --git a/scripts/add_littlefs_binary.py b/scripts/add_littlefs_binary.py new file mode 100644 index 00000000..ffc948cb --- /dev/null +++ b/scripts/add_littlefs_binary.py @@ -0,0 +1,79 @@ +import os +import subprocess +import shutil +from SCons.Script import DefaultEnvironment +Import("env") + + +def build_littlefs(): + if os.path.isfile('data/settings.json') == False: + return # nothing to do + + result = subprocess.run(["pio", "run", "--target", "buildfs", "--environment", env['PIOENV']]) + if result.returncode != 0: + print("Error building LittleFS:") + exit(1) + else: + print("LittleFS build successful") + +def merge_bins(): + if os.path.isfile('data/settings.json') == False: + return # nothing to do + + BOOTLOADER_OFFSET = 0x0000 + PARTITIONS_OFFSET = 0x8000 + FIRMWARE_OFFSET = 0x10000 + + if env['PIOENV'][:13] == "esp32-wroom32": + BOOTLOADER_OFFSET = 0x1000 + + flash_size = int(env.BoardConfig().get("upload.maximum_size", "1310720")) # 0x140000 + app0_offset = 0x10000 + if env['PIOENV'][:7] == "esp8266": + app0_offset = 0 + elif env['PIOENV'][:7] == "esp8285": + app0_offset = 0 + + littlefs_offset = 0x290000 + if flash_size == 0x330000: + littlefs_offset = 0x670000 + elif flash_size == 0x640000: + littlefs_offset = 0xc90000 + + # save current wd + start = os.getcwd() + os.chdir('.pio/build/' + env['PIOENV'] + '/') + + with open("bootloader.bin", "rb") as bootloader_file: + bootloader_data = bootloader_file.read() + + with open("partitions.bin", "rb") as partitions_file: + partitions_data = partitions_file.read() + + with open("firmware.bin", "rb") as firmware_file: + firmware_data = firmware_file.read() + + with open("littlefs.bin", "rb") as littlefs_file: + littlefs_data = littlefs_file.read() + + with open("firmware.factory.bin", "wb") as merged_file: + merged_file.write(b'\xFF' * BOOTLOADER_OFFSET) + merged_file.write(bootloader_data) + + merged_file.write(b'\xFF' * (PARTITIONS_OFFSET - (BOOTLOADER_OFFSET + len(bootloader_data)))) + merged_file.write(partitions_data) + + merged_file.write(b'\xFF' * (FIRMWARE_OFFSET - (PARTITIONS_OFFSET + len(partitions_data)))) + merged_file.write(firmware_data) + + merged_file.write(b'\xFF' * (littlefs_offset - (FIRMWARE_OFFSET + len(firmware_data)))) + merged_file.write(littlefs_data) + + os.chdir(start) + +def main(target, source, env): + build_littlefs() + merge_bins() + +# ensure that script is called once firmeware was compiled +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", main) diff --git a/scripts/applyPatches.py b/scripts/applyPatches.py index 147fb0f3..1672ab2f 100644 --- a/scripts/applyPatches.py +++ b/scripts/applyPatches.py @@ -26,9 +26,10 @@ def applyPatch(libName, patchFile): # list of patches to apply (relative to /src) -if env['PIOENV'][:22] != "opendtufusion-ethernet": - applyPatch("ESP Async WebServer", "../patches/AsyncWeb_Prometheus.patch") +applyPatch("ESPAsyncWebServer-esphome", "../patches/AsyncWeb_Prometheus.patch") -if env['PIOENV'][:13] == "opendtufusion": - applyPatch("GxEPD2", "../patches/GxEPD2_SW_SPI.patch") +if (env['PIOENV'][:5] == "esp32") or (env['PIOENV'][:13] == "opendtufusion"): + applyPatch("GxEPD2", "../patches/GxEPD2_HAL.patch") + +if (env['PIOENV'][:13] == "opendtufusion") or (env['PIOENV'][:5] == "esp32"): applyPatch("RF24", "../patches/RF24_Hal.patch") diff --git a/scripts/buildManifest.py b/scripts/buildManifest.py index 2664a39f..b91145cd 100644 --- a/scripts/buildManifest.py +++ b/scripts/buildManifest.py @@ -36,9 +36,27 @@ def buildManifest(path, infile, outfile): esp32["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096}) esp32["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768}) esp32["parts"].append({"path": "ESP32/ota.bin", "offset": 57344}) - esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32.bin", "offset": 65536}) + esp32["parts"].append({"path": "ESP32/" + version[1] + "_" + sha + "_esp32-wroom32.bin", "offset": 65536}) data["builds"].append(esp32) + esp32s2 = {} + esp32s2["chipFamily"] = "ESP32-S2" + esp32s2["parts"] = [] + esp32s2["parts"].append({"path": "ESP32-S2/bootloader.bin", "offset": 4096}) + esp32s2["parts"].append({"path": "ESP32-S2/partitions.bin", "offset": 32768}) + esp32s2["parts"].append({"path": "ESP32-S2/ota.bin", "offset": 57344}) + esp32s2["parts"].append({"path": "ESP32-S2/" + version[1] + "_" + sha + "_esp32-s2-mini.bin", "offset": 65536}) + data["builds"].append(esp32s2) + + esp32s3 = {} + esp32s3["chipFamily"] = "ESP32-S3" + esp32s3["parts"] = [] + esp32s3["parts"].append({"path": "ESP32/bootloader.bin", "offset": 4096}) + esp32s3["parts"].append({"path": "ESP32/partitions.bin", "offset": 32768}) + esp32s3["parts"].append({"path": "ESP32/ota.bin", "offset": 57344}) + esp32s3["parts"].append({"path": "ESP32-S3/" + version[1] + "_" + sha + "_opendtufusion.bin", "offset": 65536}) + data["builds"].append(esp32s3) + esp8266 = {} esp8266["chipFamily"] = "ESP8266" esp8266["parts"] = [] @@ -47,7 +65,7 @@ def buildManifest(path, infile, outfile): jsonString = json.dumps(data, indent=2) - fp = open(path + "firmware/" + outfile, "w") + fp = open(path + "../firmware/" + outfile, "w") fp.write(jsonString) fp.close() diff --git a/scripts/convertHtml.py b/scripts/convertHtml.py index c39e95ac..9a83d081 100644 --- a/scripts/convertHtml.py +++ b/scripts/convertHtml.py @@ -7,8 +7,58 @@ import json from datetime import date from pathlib import Path import subprocess +import configparser Import("env") - +build_flags = [] + +import htmlPreprocessorDefines as prepro + +def getFlagsOfEnv(env): + config = configparser.ConfigParser() + config.read('platformio.ini') + global build_flags + flags = config[env]['build_flags'].split('\n') + + for i in range(len(flags)): + if flags[i][:2] == "-D" or flags[i][:2] == "${": + flags[i] = flags[i][2:] + if flags[i][-13:-1] == ".build_flags": + getFlagsOfEnv(flags[i].split(".build_flags")[0]) + elif len(flags[i]) > 0: + build_flags = build_flags + [flags[i]] + +def parseDefinesH(): + global build_flags + pattern = r'^\s*#\s*define\s+(\w+)' + + with open("defines.h", "r") as f: + for line in f: + match = re.match(pattern, line) + if match: + build_flags += [match.group(1)] + + +def get_build_flags(): + getFlagsOfEnv("env:" + env['PIOENV']) + config = configparser.ConfigParser() + config.read('platformio.ini') + parseDefinesH() + + # translate board + board = config["env:" + env['PIOENV']]['board'] + if board == "esp12e" or board == "esp8285": + build_flags.append("ESP8266") + elif board == "lolin_d32": + build_flags.append("ESP32") + elif board == "lolin_s2_mini": + build_flags.append("ESP32") + build_flags.append("ESP32-S2") + elif board == "lolin_c3_mini": + build_flags.append("ESP32") + build_flags.append("ESP32-C3") + elif board == "esp32-s3-devkitc-1": + build_flags.append("ESP32") + build_flags.append("ESP32-S3") def get_git_sha(): try: @@ -50,38 +100,46 @@ def readVersionFull(path): return version def htmlParts(file, header, nav, footer, versionPath, lang): - p = ""; f = open(file, "r") lines = f.readlines() f.close(); f = open(header, "r") - h = f.read().strip() + h = f.readlines() f.close() f = open(nav, "r") - n = f.read().strip() + n = f.readlines() f.close() f = open(footer, "r") - fo = f.read().strip() + fo = f.readlines() f.close() + linesExt = [] for line in lines: - line = line.replace("{#HTML_HEADER}", h) - line = line.replace("{#HTML_NAV}", n) - line = line.replace("{#HTML_FOOTER}", fo) - p += line + if line.find("{#HTML_HEADER}") != -1: + linesExt.extend(h) + elif line.find("{#HTML_NAV}") != -1: + linesExt.extend(n) + elif line.find("{#HTML_FOOTER}") != -1: + linesExt.extend(fo) + else: + linesExt.append(line) + + linesMod = prepro.conv(linesExt, build_flags) #placeholders version = readVersion(versionPath); link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>' + p = "" + for line in linesMod: + p += line + p = p.replace("{#VERSION}", version) p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath)) p = p.replace("{#VERSION_GIT}", link) - # remove if - endif ESP32 - p = checkIf(p) p = translate(file, p, lang) p = translate("general", p, lang) # menu / header / footer @@ -90,30 +148,6 @@ def htmlParts(file, header, nav, footer, versionPath, lang): f.close(); return p -def checkIf(data): - if (env['PIOENV'][0:5] == "esp32") or env['PIOENV'][0:4] == "open": - data = data.replace("<!--IF_ESP32-->", "") - data = data.replace("<!--ENDIF_ESP32-->", "") - data = data.replace("/*IF_ESP32*/", "") - data = data.replace("/*ENDIF_ESP32*/", "") - else: - while 1: - start = data.find("<!--IF_ESP32-->") - end = data.find("<!--ENDIF_ESP32-->")+18 - if -1 == start: - break - else: - data = data[0:start] + data[end:] - while 1: - start = data.find("/*IF_ESP32*/") - end = data.find("/*ENDIF_ESP32*/")+15 - if -1 == start: - break - else: - data = data[0:start] + data[end:] - - return data - def findLang(file): with open('../lang.json') as j: lang = json.load(j) @@ -189,33 +223,41 @@ def convert2Header(inFile, versionPath, lang): f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) f.close() -# delete all files in the 'h' dir -wd = 'web/html/h' - -if os.path.exists(wd): - for f in os.listdir(wd): - os.remove(os.path.join(wd, f)) -wd += "/tmp" -if os.path.exists(wd): - for f in os.listdir(wd): - os.remove(os.path.join(wd, f)) - -# grab all files with following extensions -os.chdir('./web/html') -types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types -files_grabbed = [] -for files in types: - files_grabbed.extend(glob.glob(files)) - -Path("h").mkdir(exist_ok=True) -Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements -shutil.copyfile("style.css", "tmp/style.css") - -# get language from environment -lang = "en" -if env['PIOENV'][-3:] == "-de": - lang = "de" - -# go throw the array -for val in files_grabbed: - convert2Header(val, "../../defines.h", lang) + +def main(): + get_build_flags() + + # delete all files in the 'h' dir + wd = 'web/html/h' + + if os.path.exists(wd): + for f in os.listdir(wd): + os.remove(os.path.join(wd, f)) + wd += "/tmp" + if os.path.exists(wd): + for f in os.listdir(wd): + os.remove(os.path.join(wd, f)) + + # grab all files with following extensions + os.chdir('./web/html') + types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types + files_grabbed = [] + for files in types: + files_grabbed.extend(glob.glob(files)) + + Path("h").mkdir(exist_ok=True) + Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements + shutil.copyfile("style.css", "tmp/style.css") + + # get language from environment + lang = "en" + if env['PIOENV'][-3:] == "-de": + lang = "de" + + + # go throw the array + for val in files_grabbed: + convert2Header(val, "../../defines.h", lang) + + +main() diff --git a/scripts/getVersion.py b/scripts/getVersion.py index a60a772d..f579b56a 100644 --- a/scripts/getVersion.py +++ b/scripts/getVersion.py @@ -76,8 +76,9 @@ def renameFw(path_define, env): fname = version[:-1] + "_" + sha + "_" + env + ".bin" os.rename("src/.pio/build/" + env + "/firmware.bin", dst + fname) + os.rename("src/.pio/build/" + env + "/firmware.elf.7z", dst + fname[:-3] + "elf.7z") - if env[:5] == "esp32": + if env[:5] == "esp32" or env[:4] == "open": os.rename("src/.pio/build/" + env + "/bootloader.bin", dst + "bootloader.bin") os.rename("src/.pio/build/" + env + "/partitions.bin", dst + "partitions.bin") genOtaBin(dst) diff --git a/scripts/htmlPreprocessorDefines.py b/scripts/htmlPreprocessorDefines.py new file mode 100644 index 00000000..f5d7cc31 --- /dev/null +++ b/scripts/htmlPreprocessorDefines.py @@ -0,0 +1,40 @@ +import re +import os +import queue + +def error(msg): + print("ERROR: " + msg) + exit() + +def check(inp, lst, pattern): + q = queue.LifoQueue() + out = [] + keep = True + for line in inp: + x = re.findall(pattern, line) + if len(x) > 0: + if line.find("ENDIF_") != -1: + if not q.empty(): + e = q.get() + if e[0] == x[0]: + keep = e[1] + elif line.find("IF_") != -1: + q.put((x[0], keep)) + if keep is True: + keep = x[0] in lst + elif line.find("E") != -1: + if q.empty(): + error("(ELSE) missing open statement!") + e = q.get() + q.put(e) + if e[1] is True: + keep = not keep + else: + if keep is True: + out.append(line) + return out + +def conv(inp, lst): + #print(lst) + out = check(inp, lst, r'\/\*(?:IF_|ELS|ENDIF_)([A-Z0-9\-_]+)?\*\/') + return check(out, lst, r'\<\!\-\-(?:IF_|ELS|ENDIF_)([A-Z0-9\-_]+)?\-\-\>') diff --git a/src/.gitignore b/src/.gitignore index 89cc49cb..30f1d1ca 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -3,3 +3,5 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch +scripts/__pycache__/* +*.pyc diff --git a/src/CHANGES.md b/src/CHANGES.md index 5fd4fc31..cede20cb 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,41 +1,54 @@ -Changelog v0.8.83 +Changelog v0.8.140 -* added German translations for all variants -* added reading grid profile -* added decimal place for active power control (APC aka power limit) -* added information about working IRQ for NRF24 and CMT2300A to `/system` -* added loss rate to `/visualization` in the statistics window and MqTT -* added optional output to display whether it's night time or not. Can be reused as output to control battery system or mapped to a LED -* added timestamp for `max ac power` as tooltip -* added wizard for initial WiFi connection -* added history graph (still under development) -* added simulator (must be activated before compile, standard: off) -* added minimal version (without: MqTT, Display, History), WebUI is not changed! (not compiled automatically) -* added info about installed binary to `/update` -* added protection to prevent update to wrong firmware (environment check) -* added optional custom link to the menu -* added support for other regions (USA, Indonesia) -* added warning for WiFi channel 12-14 (ESP8266 only) -* added `max_power` to MqTT total values -* added API-Token authentification for external scripts -* improved MqTT by marking sent data and improved `last_success` resends -* improved communication for HM and MI inverters -* improved reading live data from inverter -* improved sending active power control command faster -* improved `/settings`: pinout has an own subgroup -* improved export by saving settings before they are exported (to have everything in JSON) -* improved code quality (cppcheck) -* seperated sunrise and sunset offset to two fields -* fix MqTT night communication -* fix missing favicon to html header -* fix build on Windows of `opendtufusion` environments (git: trailing whitespaces) -* fix generation of DTU-ID -* fix: protect commands from popup in `/live` if password is set -* fix: prevent sending commands to inverter which isn't active -* combined firmware and hardware version to JSON topics (MqTT) -* updated Prometheus with latest changes -* upgraded most libraries to newer versions -* beautified typography, added spaces between value and unit for `/visualization` -* removed add to total (MqTT) inverter setting +* added HMS-400-1T support (serial number 1125...) +* added further ESP8266 versions (-all, -minimal) because of small ressources on ESP8266 +* added some Gridprofiles +* added support for characters in serial number of inverter (A-F) +* added default coordinates on fresh install, needed for history graph on display and WebUI +* added option to reset values on communication start (sunrise) +* added max inverter temperature to WebUI +* added yield day to history graph +* added script and [instructions](../manual/factory_firmware.md) how to generate factory firmware which includes predefined settings +* added button for downloading coredump (ESP32 variants only) to `/system`. Once a crash happens the reason can be checked afterwards (even after a reboot) +* added support of HERF inverters, serial number is converted in Javascript +* added device name to HTML title +* added feature to restart Ahoy using MqTT +* added feature to publish MqTT messages as JSON as well (new setting) +* add timestamp to JSON output +* improved communication to inverter +* improved translation to German +* improved HTML pages, reduced in size by only including relevant contents depending by chip type +* improved history graph in WebUI +* improved network routines +* improved Wizard +* improved WebUI by disabling upload and import buttons when no file is selected +* improved queue, only add new object once they not exist in queue +* improved MqTT `OnMessage` (threadsafe) +* improved read of alarms, prevent duplicates, update alarm time if there is an update +* improved alarms are now sorted in ascending direction +* improved by prevent add inverter multiple times +* improved sending active power controll commands +* improved refresh routine of ePaper, full refresh each 12h +* redesigned WebUI on `/system` +* changed MqTT retained flags +* change MqTT return value of power limit acknowledge from `boolean` to `float`. The value returned is the same as it was set to confirm reception (not the read back value) +* converted ePaper and Ethernet to hal-SPI +* combined Ethernet and WiFi variants - Ethernet is now always included, but needs to be enabled if needed +* changed: Ethernet variants (W5500) now support WiFi as fall back / configuration +* switch AsyncWebserver library +* fixed autodiscovery for homeassistant +* fix reset values functionality +* fix read back of active power control value, now it has one decimal place +* fix NTP issues +* fixed MqTT discovery field `ALARM_MES_ID` +* fix close button color of modal windows in dark mode +* fixed calculation of max AC power +* fixed reset values at midnight if WiFi isn't available +* fixed HMT-1800-4T number of inputs +* fix crash if invalid serial number was set -> inverter will be disabled automatically +* fixed ESP8266, ESP32 static IP +* fixed ethernet MAC address read back +* update several libraries to more recent versions +* removed `yield efficiency` because the inverter already calculates correct full version log: [Development Log](https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md) diff --git a/src/app.cpp b/src/app.cpp index 0bead49e..c6a689ee 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -33,44 +33,33 @@ void app::setup() { resetSystem(); esp_task_wdt_reset(); - mSettings.setup(); - mSettings.getPtr(mConfig); + mSettings.setup(mConfig); ah::Scheduler::setup(mConfig->inst.startWithoutTime); DPRINT(DBG_INFO, F("Settings valid: ")); - DSERIAL.flush(); - if (mSettings.getValid()) - DBGPRINTLN(F("true")); - else - DBGPRINTLN(F("false")); + DBGPRINTLN(mConfig->valid ? F("true") : F("false")); esp_task_wdt_reset(); - if(mConfig->nrf.enabled) { - mNrfRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs, mConfig->nrf.pinSclk, mConfig->nrf.pinMosi, mConfig->nrf.pinMiso); - } + mNrfRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->nrf); #if defined(ESP32) - if(mConfig->cmt.enabled) { - mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, mConfig->cmt.pinSclk, mConfig->cmt.pinSdio, mConfig->cmt.pinCsb, mConfig->cmt.pinFcsb, mConfig->sys.region); - } + mCmtRadio.setup(&mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace, &mConfig->cmt, mConfig->sys.region); #endif + #ifdef ETHERNET delay(1000); - mEth.setup(mConfig, &mTimestamp, [this](bool gotIp) { this->onNetwork(gotIp); }, [this](bool gotTime) { this->onNtpUpdate(gotTime); }); - #endif // ETHERNET - - #if !defined(ETHERNET) - mWifi.setup(mConfig, &mTimestamp, std::bind(&app::onNetwork, this, std::placeholders::_1)); - #if !defined(AP_ONLY) - everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); - #endif - #endif /* defined(ETHERNET) */ + mNetwork = static_cast<AhoyNetwork*>(new AhoyEthernet()); + #else + mNetwork = static_cast<AhoyNetwork*>(new AhoyWifi()); + #endif + mNetwork->setup(mConfig, &mTimestamp, [this](bool gotIp) { this->onNetwork(gotIp); }, [this](bool gotTime) { this->onNtpUpdate(gotTime); }); + mNetwork->begin(); esp_task_wdt_reset(); mCommunication.setup(&mTimestamp, &mConfig->serial.debug, &mConfig->serial.privacyLog, &mConfig->serial.printWholeTrace); - mCommunication.addPayloadListener(std::bind(&app::payloadEventListener, this, std::placeholders::_1, std::placeholders::_2)); + mCommunication.addPayloadListener([this] (uint8_t cmd, Inverter<> *iv) { payloadEventListener(cmd, iv); }); #if defined(ENABLE_MQTT) - mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); }); + mCommunication.addPowerLimitAckListener([this] (Inverter<> *iv) { mMqtt.setPowerLimitAck(iv); }); #endif mSys.setup(&mTimestamp, &mConfig->inst, this); for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { @@ -85,16 +74,14 @@ void app::setup() { esp_task_wdt_reset(); // when WiFi is in client mode, then enable mqtt broker - #if !defined(AP_ONLY) #if defined(ENABLE_MQTT) mMqttEnabled = (mConfig->mqtt.broker[0] > 0); if (mMqttEnabled) { - mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime); + mMqtt.setup(this, &mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime); mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1)); mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); }); } #endif - #endif setupLed(); esp_task_wdt_reset(); @@ -107,6 +94,7 @@ void app::setup() { mDbgSyslog.setup(mConfig); // be sure to init after mWeb.setup (webSerial uses also debug callback) #endif // Plugins + mMaxPower.setup(&mTimestamp, mConfig->inst.sendInterval); #if defined(PLUGIN_DISPLAY) if (DISP_TYPE_T0_NONE != mConfig->plugin.display.type) #if defined(ESP32) @@ -124,18 +112,13 @@ void app::setup() { mPubSerial.setup(mConfig, &mSys, &mTimestamp); - #if !defined(ETHERNET) //mImprov.setup(this, mConfig->sys.deviceName, mVersion); - #endif #if defined(ENABLE_SIMULATOR) mSimulator.setup(&mSys, &mTimestamp, 0); - mSimulator.addPayloadListener([this](uint8_t cmd, Inverter<> *iv) { - payloadEventListener(cmd, iv); - }); + mSimulator.addPayloadListener([this](uint8_t cmd, Inverter<> *iv) { payloadEventListener(cmd, iv); }); #endif /*ENABLE_SIMULATOR*/ - esp_task_wdt_reset(); regularTickers(); } @@ -144,12 +127,10 @@ void app::setup() { void app::loop(void) { esp_task_wdt_reset(); - if(mConfig->nrf.enabled) - mNrfRadio.loop(); + mNrfRadio.loop(); #if defined(ESP32) - if(mConfig->cmt.enabled) - mCmtRadio.loop(); + mCmtRadio.loop(); #endif ah::Scheduler::loop(); @@ -159,25 +140,24 @@ void app::loop(void) { if (mMqttEnabled && mNetworkConnected) mMqtt.loop(); #endif + + #if defined(PLUGIN_DISPLAY) + mDisplay.loop(); + #endif yield(); } //----------------------------------------------------------------------------- void app::onNetwork(bool gotIp) { - DPRINTLN(DBG_DEBUG, F("onNetwork")); mNetworkConnected = gotIp; - ah::Scheduler::resetTicker(); - regularTickers(); //reinstall regular tickers - every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend"); - mMqttReconnect = true; - mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! - once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); - #if !defined(ETHERNET) - if (WIFI_AP == WiFi.getMode()) { - mMqttEnabled = false; + if(gotIp) { + ah::Scheduler::resetTicker(); + regularTickers(); //reinstall regular tickers + every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend"); + mTickerInstallOnce = true; + mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers! + once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2"); } - everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); - #endif /* !defined(ETHERNET) */ } //----------------------------------------------------------------------------- @@ -185,6 +165,10 @@ void app::regularTickers(void) { DPRINTLN(DBG_DEBUG, F("regularTickers")); everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); everySec([this]() { mProtection->tickSecond(); }, "prot"); + everySec([this]() {mNetwork->tickNetworkLoop(); }, "net"); + + if(mConfig->inst.startWithoutTime && !mNetworkConnected) + every(std::bind(&app::tickSend, this), mConfig->inst.sendInterval, "tSend"); // Plugins #if defined(PLUGIN_DISPLAY) @@ -192,9 +176,7 @@ void app::regularTickers(void) { everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); #endif every(std::bind(&PubSerialType::tick, &mPubSerial), 5, "uart"); - #if !defined(ETHERNET) //everySec([this]() { mImprov.tickSerial(); }, "impro"); - #endif #if defined(ENABLE_HISTORY) everySec(std::bind(&HistoryType::tickerSecond, &mHistory), "hist"); @@ -205,75 +187,59 @@ void app::regularTickers(void) { #endif /*ENABLE_SIMULATOR*/ } -#if defined(ETHERNET) +//----------------------------------------------------------------------------- void app::onNtpUpdate(bool gotTime) { mNtpReceived = true; -} -#endif /* defined(ETHERNET) */ - -//----------------------------------------------------------------------------- -void app::updateNtp(void) { - #if defined(ENABLE_MQTT) - if (mMqttReconnect && mMqttEnabled) { - mMqtt.tickerSecond(); - everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); - everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); + if ((0 == mSunrise) && (0.0 != mConfig->sun.lat) && (0.0 != mConfig->sun.lon)) { + mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600; + tickCalcSunrise(); } - #endif /*ENABLE_MQTT*/ - // only install schedulers once even if NTP wasn't successful in first loop - if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed + if (mTickerInstallOnce) { + mTickerInstallOnce = false; + #if defined(ENABLE_MQTT) + if (mMqttEnabled) { + mMqtt.tickerSecond(); + everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt), "mqttS"); + everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt), "mqttM"); + } + #endif /*ENABLE_MQTT*/ + if (mConfig->inst.rstValsNotAvail) everyMin(std::bind(&app::tickMinute, this), "tMin"); - uint32_t localTime = gTimezone.toLocal(mTimestamp); - uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time - onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); + if(mNtpReceived) { + uint32_t localTime = gTimezone.toLocal(mTimestamp); + uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time + onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi"); - if (mConfig->sys.schedReboot) { - uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght - if (rebootTrig <= mTimestamp) { //necessary for times other than midnight to prevent reboot loop - rebootTrig += 86400; + if (mConfig->sys.schedReboot) { + uint32_t rebootTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86410); // reboot 10 secs after midnght + onceAt(std::bind(&app::tickReboot, this), rebootTrig, "midRe"); } - onceAt(std::bind(&app::tickReboot, this), rebootTrig, "midRe"); } } +} - if ((0 == mSunrise) && (0.0 != mConfig->sun.lat) && (0.0 != mConfig->sun.lon)) { - mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600; - tickCalcSunrise(); - } - - mMqttReconnect = false; +//----------------------------------------------------------------------------- +void app::updateNtp(void) { + if(mNtpReceived) + onNtpUpdate(true); } //----------------------------------------------------------------------------- void app::tickNtpUpdate(void) { uint32_t nxtTrig = 5; // default: check again in 5 sec - bool isOK = false; - #if defined(ETHERNET) if (!mNtpReceived) - mEth.updateNtpTime(); + mNetwork->updateNtpTime(); else { + nxtTrig = mConfig->ntp.interval * 60; // check again in configured interval mNtpReceived = false; - isOK = true; } - #else - isOK = mWifi.getNtpTime(); - #endif - if (isOK) { - this->updateNtp(); - nxtTrig = mConfig->ntp.interval * 60; // check again in 12h - - // immediately start communicating - if (mSendFirst) { - mSendFirst = false; - once(std::bind(&app::tickSend, this), 1, "senOn"); - } - mMqttReconnect = false; - } + updateNtp(); + once(std::bind(&app::tickNtpUpdate, this), nxtTrig, "ntp"); } @@ -315,6 +281,8 @@ void app::tickIVCommunication(void) { if (mTimestamp >= (mSunset + mConfig->sun.offsetSecEvening)) { // current time is past communication stop, nothing to do. Next update will be done at midnight by tickCalcSunrise nxtTrig = 0; } else { // current time lies within communication start/stop time, set next trigger to communication stop + if((!iv->commEnabled) && mConfig->inst.rstValsCommStart) + zeroValues = true; iv->commEnabled = true; nxtTrig = mSunset + mConfig->sun.offsetSecEvening; } @@ -387,18 +355,9 @@ void app::tickMidnight(void) { // reset alarms if(InverterStatus::OFF == iv->getStatus()) iv->resetAlarms(); - - // clear max values - if(mConfig->inst.rstMaxValsMidNight) { - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - for(uint8_t i = 0; i <= iv->channels; i++) { - uint8_t pos = iv->getPosByChFld(i, FLD_MP, rec); - iv->setValue(pos, rec, 0.0f); - } - } } - if (mConfig->inst.rstYieldMidNight) { + if (mConfig->inst.rstValsAtMidNight) { zeroIvValues(!CHECK_AVAIL, !SKIP_YIELD_DAY); #if defined(ENABLE_MQTT) @@ -423,29 +382,8 @@ void app::tickSend(void) { for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { Inverter<> *iv = mSys.getInverterByPos(i); - if(NULL == iv) - continue; - - if(iv->config->enabled) { - if(!iv->commEnabled) { - DPRINT_IVID(DBG_INFO, iv->id); - DBGPRINTLN(F("no communication to the inverter (night time)")); - continue; - } - - if(!iv->radio->isChipConnected()) - continue; - - if(InverterStatus::OFF != iv->status) - notAvail = false; - - iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) { - if(isDevControl) - mCommunication.addImportant(iv, cmd); - else - mCommunication.add(iv, cmd); - }); - } + if(!sendIv(iv)) + notAvail = false; } if(mAllIvNotAvail != notAvail) @@ -455,10 +393,44 @@ void app::tickSend(void) { updateLed(); } +//----------------------------------------------------------------------------- +bool app::sendIv(Inverter<> *iv) { + if(NULL == iv) + return true; + + if(!iv->config->enabled) + return true; + + if(!iv->commEnabled) { + DPRINT_IVID(DBG_INFO, iv->id); + DBGPRINTLN(F("no communication to the inverter (night time)")); + return true; + } + + if(!iv->radio->isChipConnected()) + return true; + + bool notAvail = true; + if(InverterStatus::OFF != iv->status) + notAvail = false; + + iv->tickSend([this, iv](uint8_t cmd, bool isDevControl) { + if(isDevControl) + mCommunication.addImportant(iv, cmd); + else + mCommunication.add(iv, cmd); + }); + + return notAvail; +} + //----------------------------------------------------------------------------- void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { Inverter<> *iv; bool changed = false; + + mMaxPower.reset(); + // set values to zero, except yields for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); @@ -468,10 +440,11 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { continue; // skip to next inverter if (checkAvail) { - if (!iv->isAvailable()) + if (iv->isAvailable()) continue; } + changed = true; record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); for(uint8_t ch = 0; ch <= iv->channels; ch++) { uint8_t pos = 0; @@ -488,20 +461,21 @@ void app:: zeroIvValues(bool checkAvail, bool skipYieldDay) { pos = iv->getPosByChFld(ch, fld, rec); iv->setValue(pos, rec, 0.0f); } - // zero max power - if(!skipYieldDay) { + // zero max power and max temperature + if(mConfig->inst.rstIncludeMaxVals) { pos = iv->getPosByChFld(ch, FLD_MP, rec); iv->setValue(pos, rec, 0.0f); - } - iv->resetAlarms(); - + pos = iv->getPosByChFld(ch, FLD_MT, rec); + iv->setValue(pos, rec, 0.0f); + iv->resetAlarms(true); + } else + iv->resetAlarms(); iv->doCalculations(); } - changed = true; } if(changed) - payloadEventListener(RealTimeRunData_Debug, NULL); + payloadEventListener(RealTimeRunData_Debug, nullptr); } //----------------------------------------------------------------------------- @@ -548,7 +522,6 @@ void app::resetSystem(void) { mTimestamp = 1; #endif - mSendFirst = true; mAllIvNotAvail = true; mSunrise = 0; @@ -562,10 +535,8 @@ void app::resetSystem(void) { mSaveReboot = false; mNetworkConnected = false; - -#if defined(ETHERNET) mNtpReceived = false; -#endif + mTickerInstallOnce = false; } //----------------------------------------------------------------------------- diff --git a/src/app.h b/src/app.h index b16d7aeb..d23f307e 100644 --- a/src/app.h +++ b/src/app.h @@ -17,9 +17,9 @@ #include "defines.h" #include "appInterface.h" #include "hm/hmSystem.h" -#include "hm/hmRadio.h" +#include "hm/NrfRadio.h" #if defined(ESP32) -#include "hms/hmsRadio.h" +#include "hms/CmtRadio.h" #endif #if defined(ENABLE_MQTT) #include "publisher/pubMqtt.h" @@ -31,17 +31,22 @@ #include "utils/syslog.h" #include "web/RestApi.h" #include "web/Protection.h" +#include "plugins/MaxPower.h" #if defined(ENABLE_HISTORY) #include "plugins/history.h" #endif /*ENABLE_HISTORY*/ #include "web/web.h" #include "hm/Communication.h" #if defined(ETHERNET) - #include "eth/ahoyeth.h" + #include "network/AhoyEthernet.h" #else /* defined(ETHERNET) */ - #include "wifi/ahoywifi.h" - #include "utils/improv.h" + #if defined(ESP32) + #include "network/AhoyWifiEsp32.h" + #else + #include "network/AhoyWifiEsp8266.h" + #endif #endif /* defined(ETHERNET) */ +#include "utils/improv.h" #if defined(ENABLE_SIMULATOR) #include "hm/simulator.h" @@ -162,32 +167,33 @@ class app : public IApp, public ah::Scheduler { return mSaveReboot; } - #if !defined(ETHERNET) - void scanAvailNetworks() override { - mWifi.scanAvailNetworks(); - } - bool getAvailNetworks(JsonObject obj) override { - return mWifi.getAvailNetworks(obj); + return mNetwork->getAvailNetworks(obj, this); } void setupStation(void) override { - mWifi.setupStation(); + mNetwork->begin(); } - void setStopApAllowedMode(bool allowed) override { - mWifi.setStopApAllowedMode(allowed); + bool getWasInCh12to14(void) const override { + #if defined(ESP8266) + return mNetwork->getWasInCh12to14(); + #else + return false; + #endif } - String getStationIp(void) override { - return mWifi.getStationIp(); + String getIp(void) override { + return mNetwork->getIp(); } - bool getWasInCh12to14(void) const override { - return mWifi.getWasInCh12to14(); + String getMac(void) override { + return mNetwork->getMac(); } - #endif /* !defined(ETHERNET) */ + bool isApActive(void) override { + return mNetwork->isApActive(); + } void setRebootFlag() override { once(std::bind(&app::tickReboot, this), 3, "rboot"); @@ -201,6 +207,10 @@ class app : public IApp, public ah::Scheduler { return mVersionModules; } + void addOnce(ah::scdCb c, uint32_t timeout, const char *name) override { + once(c, timeout, name); + } + uint32_t getSunrise() override { return mSunrise; } @@ -210,7 +220,7 @@ class app : public IApp, public ah::Scheduler { } bool getSettingsValid() override { - return mSettings.getValid(); + return mConfig->valid; } bool getRebootRequestState() override { @@ -247,6 +257,12 @@ class app : public IApp, public ah::Scheduler { #endif } + #if defined(ETHERNET) + bool isWiredConnection() override { + return mNetwork->isWiredConnection(); + } + #endif + void lock(bool fromWeb) override { mProtection->lock(fromWeb); } @@ -295,15 +311,15 @@ class app : public IApp, public ah::Scheduler { DPRINT(DBG_DEBUG, F("setTimestamp: ")); DBGPRINTLN(String(newTime)); if(0 == newTime) - { - #if defined(ETHERNET) - mEth.updateNtpTime(); - #else /* defined(ETHERNET) */ - mWifi.getNtpTime(); - #endif /* defined(ETHERNET) */ - } - else + mNetwork->updateNtpTime(); + else { Scheduler::setTimestamp(newTime); + onNtpUpdate(false); + } + } + + float getTotalMaxPower(void) override { + return mMaxPower.getTotalMaxPower(); } uint16_t getHistoryValue(uint8_t type, uint16_t i) override { @@ -314,6 +330,14 @@ class app : public IApp, public ah::Scheduler { #endif } + uint32_t getHistoryPeriod(uint8_t type) override { + #if defined(ENABLE_HISTORY) + return mHistory.getPeriod((HistoryStorageType)type); + #else + return 0; + #endif + } + uint16_t getHistoryMaxDay() override { #if defined(ENABLE_HISTORY) return mHistory.getMaximumDay(); @@ -322,6 +346,21 @@ class app : public IApp, public ah::Scheduler { #endif } + uint32_t getHistoryLastValueTs(uint8_t type) override { + #if defined(ENABLE_HISTORY) + return mHistory.getLastValueTs((HistoryStorageType)type); + #else + return 0; + #endif + } + #if defined(ENABLE_HISTORY_LOAD_DATA) + void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) override { + #if defined(ENABLE_HISTORY) + return mHistory.addValue((HistoryStorageType)historyType, valueType, value); + #endif + } + #endif + private: #define CHECK_AVAIL true #define SKIP_YIELD_DAY true @@ -330,15 +369,14 @@ class app : public IApp, public ah::Scheduler { void zeroIvValues(bool checkAvail = false, bool skipYieldDay = true); void payloadEventListener(uint8_t cmd, Inverter<> *iv) { - #if !defined(AP_ONLY) + mMaxPower.payloadEvent(cmd, iv); #if defined(ENABLE_MQTT) if (mMqttEnabled) mMqtt.payloadEventListener(cmd, iv); - #endif /*ENABLE_MQTT*/ #endif #if defined(PLUGIN_DISPLAY) - if(DISP_TYPE_T0_NONE != mConfig->plugin.display.type) - mDisplay.payloadEventListener(cmd); + if(DISP_TYPE_T0_NONE != mConfig->plugin.display.type) + mDisplay.payloadEventListener(cmd); #endif updateLed(); } @@ -366,14 +404,14 @@ class app : public IApp, public ah::Scheduler { } void tickNtpUpdate(void); - #if defined(ETHERNET) void onNtpUpdate(bool gotTime); bool mNtpReceived = false; - #endif /* defined(ETHERNET) */ void updateNtp(void); - void triggerTickSend() override { - once(std::bind(&app::tickSend, this), 0, "tSend"); + void triggerTickSend(uint8_t id) override { + once([this, id]() { + sendIv(mSys.getInverterByPos(id)); + }, 0, "devct"); } void tickCalcSunrise(void); @@ -382,34 +420,28 @@ class app : public IApp, public ah::Scheduler { void tickSunrise(void); void tickComm(void); void tickSend(void); + bool sendIv(Inverter<> *iv); void tickMinute(void); void tickZeroValues(void); void tickMidnight(void); void notAvailChanged(void); HmSystemType mSys; - HmRadio<> mNrfRadio; + NrfRadio<> mNrfRadio; Communication mCommunication; bool mShowRebootRequest = false; - #if defined(ETHERNET) - ahoyeth mEth; - #else /* defined(ETHERNET) */ - ahoywifi mWifi; - #endif /* defined(ETHERNET) */ + AhoyNetwork *mNetwork = nullptr; WebType mWeb; RestApiType mApi; Protection *mProtection = nullptr; #ifdef ENABLE_SYSLOG DbgSyslog mDbgSyslog; #endif - //PayloadType mPayload; - //MiPayloadType mMiPayload; + PubSerialType mPubSerial; - #if !defined(ETHERNET) //Improv mImprov; - #endif #ifdef ESP32 CmtRadio<> mCmtRadio; #endif @@ -422,16 +454,14 @@ class app : public IApp, public ah::Scheduler { bool mSaveReboot = false; uint8_t mSendLastIvId = 0; - bool mSendFirst = false; bool mAllIvNotAvail = false; bool mNetworkConnected = false; - // mqtt #if defined(ENABLE_MQTT) PubMqttType mMqtt; - #endif /*ENABLE_MQTT*/ - bool mMqttReconnect = false; + #endif + bool mTickerInstallOnce = false; bool mMqttEnabled = false; // sun @@ -439,6 +469,7 @@ class app : public IApp, public ah::Scheduler { uint32_t mSunrise = 0, mSunset = 0; // plugins + MaxPower<float> mMaxPower; #if defined(PLUGIN_DISPLAY) DisplayType mDisplay; DisplayData mDispData; diff --git a/src/appInterface.h b/src/appInterface.h index 536455e0..d49f907e 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -7,11 +7,8 @@ #define __IAPP_H__ #include "defines.h" -#if defined(ETHERNET) -#include "AsyncWebServer_ESP32_W5500.h" -#else #include "ESPAsyncWebServer.h" -#endif +#include "utils/scheduler.h" // abstract interface to App. Make members of App accessible from child class // like web or API without forward declaration @@ -29,14 +26,14 @@ class IApp { virtual const char *getVersion() = 0; virtual const char *getVersionModules() = 0; - #if !defined(ETHERNET) - virtual void scanAvailNetworks() = 0; + virtual void addOnce(ah::scdCb c, uint32_t timeout, const char *name) = 0; + virtual bool getAvailNetworks(JsonObject obj) = 0; virtual void setupStation(void) = 0; - virtual void setStopApAllowedMode(bool allowed) = 0; - virtual String getStationIp(void) = 0; virtual bool getWasInCh12to14(void) const = 0; - #endif /* defined(ETHERNET) */ + virtual String getIp(void) = 0; + virtual String getMac(void) = 0; + virtual bool isApActive(void) = 0; virtual uint32_t getUptime() = 0; virtual uint32_t getTimestamp() = 0; @@ -48,7 +45,7 @@ class IApp { virtual void getSchedulerInfo(uint8_t *max) = 0; virtual void getSchedulerNames() = 0; - virtual void triggerTickSend() = 0; + virtual void triggerTickSend(uint8_t id) = 0; virtual bool getRebootRequestState() = 0; virtual bool getSettingsValid() = 0; @@ -61,14 +58,23 @@ class IApp { virtual uint32_t getMqttRxCnt() = 0; virtual uint32_t getMqttTxCnt() = 0; + #if defined(ETHERNET) + virtual bool isWiredConnection() = 0; + #endif + virtual void lock(bool fromWeb) = 0; virtual char *unlock(const char *clientIp, bool loginFromWeb) = 0; virtual void resetLockTimeout(void) = 0; virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0; + virtual float getTotalMaxPower(void) = 0; virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0; + virtual uint32_t getHistoryPeriod(uint8_t type) = 0; virtual uint16_t getHistoryMaxDay() = 0; - + virtual uint32_t getHistoryLastValueTs(uint8_t type) = 0; + #if defined(ENABLE_HISTORY_LOAD_DATA) + virtual void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) = 0; + #endif virtual void* getRadioObj(bool nrf) = 0; }; diff --git a/src/config/config.h b/src/config/config.h index 9e59f146..7a3dbb64 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -16,9 +16,8 @@ //------------------------------------- // Fallback WiFi Info -#define FB_WIFI_SSID "YOUR_WIFI_SSID" -#define FB_WIFI_PWD "YOUR_WIFI_PWD" - +#define FB_WIFI_SSID "" +#define FB_WIFI_PWD "" // Access Point Info // In case there is no WiFi Network or Ahoy can not connect to it, it will act as an Access Point @@ -28,6 +27,11 @@ // If the next line is uncommented, Ahoy will stay in access point mode all the time //#define AP_ONLY +#if defined(AP_ONLY) + #if defined(ENABLE_MQTT) + #undef ENABLE_MQTT + #endif +#endif // timeout for automatic logoff (20 minutes) #define LOGOUT_TIMEOUT (20 * 60) @@ -77,6 +81,9 @@ #ifndef DEF_ETH_CS_PIN #define DEF_ETH_CS_PIN 15 #endif + #ifndef DEF_ETH_RST_PIN + #define DEF_ETH_RST_PIN DEF_PIN_OFF + #endif #else /* defined(ETHERNET) */ // time in seconds how long the station info (ssid + pwd) will be tried #define WIFI_TRY_CONNECT_TIME 30 @@ -142,7 +149,7 @@ #ifndef DEF_MOTION_SENSOR_PIN #define DEF_MOTION_SENSOR_PIN DEF_PIN_OFF #endif -#else +#else // ESP8266 #ifndef DEF_NRF_CS_PIN #define DEF_NRF_CS_PIN 15 #endif diff --git a/src/config/config_override_example.h b/src/config/config_override_example.h index 44623c1f..a84c1e8a 100644 --- a/src/config/config_override_example.h +++ b/src/config/config_override_example.h @@ -6,9 +6,6 @@ #ifndef __CONFIG_OVERRIDE_H__ #define __CONFIG_OVERRIDE_H__ -// override fallback WiFi info -#define FB_WIFI_OVERRIDDEN - // each override must be preceded with an #undef statement #undef FB_WIFI_SSID #define FB_WIFI_SSID "MY_SSID" diff --git a/src/config/settings.h b/src/config/settings.h index 18725b48..0d8970d5 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -33,7 +33,6 @@ #define CONFIG_VERSION 11 - #define PROT_MASK_INDEX 0x0001 #define PROT_MASK_LIVE 0x0002 #define PROT_MASK_SERIAL 0x0004 @@ -55,6 +54,20 @@ #define DEF_PROT_MQTT 0x0000 +#define SSID_LEN 32 +#define PWD_LEN 64 +#define DEVNAME_LEN 16 +#define NTP_ADDR_LEN 32 // DNS Name + +#define MQTT_ADDR_LEN 64 // DNS Name +#define MQTT_CLIENTID_LEN 22 // number of chars is limited to 23 up to v3.1 of MQTT +#define MQTT_USER_LEN 65 // there is another byte necessary for \0 +#define MQTT_PWD_LEN 65 +#define MQTT_TOPIC_LEN 65 + +#define MQTT_MAX_PACKET_SIZE 384 + + typedef struct { uint8_t ip[4]; // ip address uint8_t mask[4]; // sub mask @@ -63,6 +76,19 @@ typedef struct { uint8_t gateway[4]; // standard gateway } cfgIp_t; + +#if defined(ETHERNET) +typedef struct { + bool enabled; + uint8_t pinCs; + uint8_t pinSclk; + uint8_t pinMiso; + uint8_t pinMosi; + uint8_t pinIrq; + uint8_t pinRst; +} cfgEth_t; +#endif + typedef struct { char deviceName[DEVNAME_LEN]; char adminPwd[PWD_LEN]; @@ -72,13 +98,14 @@ typedef struct { uint8_t region; int8_t timezone; -#if !defined(ETHERNET) + char apPwd[PWD_LEN]; // wifi char stationSsid[SSID_LEN]; char stationPwd[PWD_LEN]; - char apPwd[PWD_LEN]; bool isHidden; -#endif /* !defined(ETHERNET) */ + #if defined(ETHERNET) + cfgEth_t eth; + #endif cfgIp_t ip; } cfgSys_t; @@ -136,7 +163,9 @@ typedef struct { char user[MQTT_USER_LEN]; char pwd[MQTT_PWD_LEN]; char topic[MQTT_TOPIC_LEN]; + bool json; uint16_t interval; + bool enableRetain; } cfgMqtt_t; typedef struct { @@ -152,16 +181,16 @@ typedef struct { } cfgIv_t; typedef struct { - bool enabled; +// bool enabled; cfgIv_t iv[MAX_NUM_INVERTERS]; uint16_t sendInterval; - bool rstYieldMidNight; + bool rstValsAtMidNight; bool rstValsNotAvail; bool rstValsCommStop; - bool rstMaxValsMidNight; + bool rstValsCommStart; + bool rstIncludeMaxVals; bool startWithoutTime; - float yieldEffiency; bool readGrid; } cfgInst_t; @@ -211,8 +240,9 @@ class settings { std::fill(reinterpret_cast<char*>(&mCfg), reinterpret_cast<char*>(&mCfg) + sizeof(mCfg), 0); } - void setup() { + void setup(settings_t *&c) { DPRINTLN(DBG_INFO, F("Initializing FS ..")); + c = &mCfg; mCfg.valid = false; #if !defined(ESP32) @@ -248,31 +278,11 @@ class settings { DPRINTLN(DBG_INFO, F("FS stopped")); } - void getPtr(settings_t *&cfg) { - cfg = &mCfg; - } - - bool getValid(void) { - return mCfg.valid; - } - inline bool getLastSaveSucceed() { return mLastSaveSucceed; } - void getInfo(uint32_t *used, uint32_t *size) { - #if !defined(ESP32) - FSInfo info; - LittleFS.info(info); - *used = info.usedBytes; - *size = info.totalBytes; - DPRINTLN(DBG_INFO, F("-- FILESYSTEM INFO --")); - DPRINTLN(DBG_INFO, String(info.usedBytes) + F(" of ") + String(info.totalBytes) + F(" used")); - #else - DPRINTLN(DBG_WARN, F("not supported by ESP32")); - #endif - } bool readSettings(const char* path) { loadDefaults(); @@ -387,14 +397,26 @@ class settings { // restore temp settings if(keepWifi) memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); - #if !defined(ETHERNET) else { - snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); - snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); - snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD); + mCfg.sys.stationSsid[0] = '\0'; + mCfg.sys.stationPwd[0] = '\0'; mCfg.sys.isHidden = false; } - #endif /* !defined(ETHERNET) */ + snprintf(mCfg.sys.apPwd, PWD_LEN, WIFI_AP_PWD); + + #if defined(ETHERNET) + #if defined(DEF_ETH_ENABLED) + mCfg.sys.eth.enabled = true; + #else + mCfg.sys.eth.enabled = false; + #endif + mCfg.sys.eth.pinCs = DEF_ETH_CS_PIN; + mCfg.sys.eth.pinSclk = DEF_ETH_SCK_PIN; + mCfg.sys.eth.pinMiso = DEF_ETH_MISO_PIN; + mCfg.sys.eth.pinMosi = DEF_ETH_MOSI_PIN; + mCfg.sys.eth.pinIrq = DEF_ETH_IRQ_PIN; + mCfg.sys.eth.pinRst = DEF_ETH_RST_PIN; + #endif snprintf(mCfg.sys.deviceName, DEVNAME_LEN, DEF_DEVICE_NAME); mCfg.sys.region = 0; // Europe @@ -407,7 +429,11 @@ class settings { mCfg.nrf.pinMosi = DEF_NRF_MOSI_PIN; mCfg.nrf.pinSclk = DEF_NRF_SCLK_PIN; + #if defined(ETHERNET) + mCfg.nrf.enabled = false; + #else mCfg.nrf.enabled = true; + #endif #if defined(ESP32) mCfg.cmt.pinSclk = DEF_CMT_SCLK; @@ -428,8 +454,8 @@ class settings { mCfg.ntp.port = DEF_NTP_PORT; mCfg.ntp.interval = 720; - mCfg.sun.lat = 0.0; - mCfg.sun.lon = 0.0; + mCfg.sun.lat = 51.1; // mid of Germany + mCfg.sun.lon = 10.5; // mid of Germany mCfg.sun.offsetSecMorning = 0; mCfg.sun.offsetSecEvening = 0; @@ -445,15 +471,17 @@ class settings { snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); mCfg.mqtt.interval = 0; // off - - mCfg.inst.sendInterval = SEND_INTERVAL; - mCfg.inst.rstYieldMidNight = false; - mCfg.inst.rstValsNotAvail = false; - mCfg.inst.rstValsCommStop = false; - mCfg.inst.startWithoutTime = false; - mCfg.inst.rstMaxValsMidNight = false; - mCfg.inst.yieldEffiency = 1.0f; - mCfg.inst.readGrid = true; + mCfg.mqtt.json = false; // off + mCfg.mqtt.enableRetain = true; + + mCfg.inst.sendInterval = SEND_INTERVAL; + mCfg.inst.rstValsAtMidNight = false; + mCfg.inst.rstValsNotAvail = false; + mCfg.inst.rstValsCommStop = false; + mCfg.inst.rstValsCommStart = false; + mCfg.inst.startWithoutTime = false; + mCfg.inst.rstIncludeMaxVals = false; + mCfg.inst.readGrid = true; for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { mCfg.inst.iv[i].powerLevel = 0xff; // impossible high value @@ -528,12 +556,10 @@ class settings { void jsonNetwork(JsonObject obj, bool set = false) { if(set) { char buf[16]; - #if !defined(ETHERNET) obj[F("ssid")] = mCfg.sys.stationSsid; obj[F("pwd")] = mCfg.sys.stationPwd; obj[F("ap_pwd")] = mCfg.sys.apPwd; obj[F("hidd")] = (bool) mCfg.sys.isHidden; - #endif /* !defined(ETHERNET) */ obj[F("dev")] = mCfg.sys.deviceName; obj[F("adm")] = mCfg.sys.adminPwd; obj[F("prot_mask")] = mCfg.sys.protectionMask; @@ -546,13 +572,21 @@ class settings { ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf); ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf); ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf); + + #if defined(ETHERNET) + obj[F("en")] = mCfg.sys.eth.enabled; + obj[F("cs")] = mCfg.sys.eth.pinCs; + obj[F("sclk")] = mCfg.sys.eth.pinSclk; + obj[F("miso")] = mCfg.sys.eth.pinMiso; + obj[F("mosi")] = mCfg.sys.eth.pinMosi; + obj[F("irq")] = mCfg.sys.eth.pinIrq; + obj[F("rst")] = mCfg.sys.eth.pinRst; + #endif } else { - #if !defined(ETHERNET) getChar(obj, F("ssid"), mCfg.sys.stationSsid, SSID_LEN); getChar(obj, F("pwd"), mCfg.sys.stationPwd, PWD_LEN); getChar(obj, F("ap_pwd"), mCfg.sys.apPwd, PWD_LEN); getVal<bool>(obj, F("hidd"), &mCfg.sys.isHidden); - #endif /* !defined(ETHERNET) */ getChar(obj, F("dev"), mCfg.sys.deviceName, DEVNAME_LEN); getChar(obj, F("adm"), mCfg.sys.adminPwd, PWD_LEN); getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask); @@ -569,6 +603,16 @@ class settings { if(mCfg.sys.protectionMask == 0) mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT | DEF_PROT_HISTORY; + + #if defined(ETHERNET) + getVal<bool>(obj, F("en"), &mCfg.sys.eth.enabled); + getVal<uint8_t>(obj, F("cs"), &mCfg.sys.eth.pinCs); + getVal<uint8_t>(obj, F("sclk"), &mCfg.sys.eth.pinSclk); + getVal<uint8_t>(obj, F("miso"), &mCfg.sys.eth.pinMiso); + getVal<uint8_t>(obj, F("mosi"), &mCfg.sys.eth.pinMosi); + getVal<uint8_t>(obj, F("irq"), &mCfg.sys.eth.pinIrq); + getVal<uint8_t>(obj, F("rst"), &mCfg.sys.eth.pinRst); + #endif } } @@ -681,16 +725,20 @@ class settings { obj[F("user")] = mCfg.mqtt.user; obj[F("pwd")] = mCfg.mqtt.pwd; obj[F("topic")] = mCfg.mqtt.topic; + obj[F("json")] = mCfg.mqtt.json; obj[F("intvl")] = mCfg.mqtt.interval; + obj[F("retain")] = mCfg.mqtt.enableRetain; } else { getVal<uint16_t>(obj, F("port"), &mCfg.mqtt.port); getVal<uint16_t>(obj, F("intvl"), &mCfg.mqtt.interval); + getVal<bool>(obj, F("json"), &mCfg.mqtt.json); getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_LEN); getChar(obj, F("user"), mCfg.mqtt.user, MQTT_USER_LEN); getChar(obj, F("clientId"), mCfg.mqtt.clientId, MQTT_CLIENTID_LEN); getChar(obj, F("pwd"), mCfg.mqtt.pwd, MQTT_PWD_LEN); getChar(obj, F("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN); + getVal<bool>(obj, F("retain"), &mCfg.mqtt.enableRetain); } } @@ -757,30 +805,25 @@ class settings { void jsonInst(JsonObject obj, bool set = false) { if(set) { obj[F("intvl")] = mCfg.inst.sendInterval; - obj[F("en")] = (bool)mCfg.inst.enabled; - obj[F("rstMidNight")] = (bool)mCfg.inst.rstYieldMidNight; +// obj[F("en")] = (bool)mCfg.inst.enabled; + obj[F("rstMidNight")] = (bool)mCfg.inst.rstValsAtMidNight; obj[F("rstNotAvail")] = (bool)mCfg.inst.rstValsNotAvail; obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; + obj[F("rstComStart")] = (bool)mCfg.inst.rstValsCommStart; obj[F("strtWthtTime")] = (bool)mCfg.inst.startWithoutTime; - obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstMaxValsMidNight; - obj[F("yldEff")] = mCfg.inst.yieldEffiency; + obj[F("rstMaxMidNight")] = (bool)mCfg.inst.rstIncludeMaxVals; obj[F("rdGrid")] = (bool)mCfg.inst.readGrid; } else { getVal<uint16_t>(obj, F("intvl"), &mCfg.inst.sendInterval); - getVal<bool>(obj, F("en"), &mCfg.inst.enabled); - getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight); +// getVal<bool>(obj, F("en"), &mCfg.inst.enabled); + getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstValsAtMidNight); getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail); getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop); + getVal<bool>(obj, F("rstComStart"), &mCfg.inst.rstValsCommStart); getVal<bool>(obj, F("strtWthtTime"), &mCfg.inst.startWithoutTime); - getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstMaxValsMidNight); - getVal<float>(obj, F("yldEff"), &mCfg.inst.yieldEffiency); + getVal<bool>(obj, F("rstMaxMidNight"), &mCfg.inst.rstIncludeMaxVals); getVal<bool>(obj, F("rdGrid"), &mCfg.inst.readGrid); - - if(mCfg.inst.yieldEffiency < 0.5) - mCfg.inst.yieldEffiency = 1.0f; - else if(mCfg.inst.yieldEffiency > 1.0f) - mCfg.inst.yieldEffiency = 1.0f; } JsonArray ivArr; @@ -854,6 +897,7 @@ class settings { } #endif + private: settings_t mCfg; bool mLastSaveSucceed = 0; }; diff --git a/src/defines.h b/src/defines.h index bf111d99..94674315 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,8 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 83 - +#define VERSION_PATCH 140 //------------------------------------- typedef struct { uint8_t ch; @@ -24,41 +23,6 @@ typedef struct { uint16_t millis; } packet_t; -typedef enum { - InverterDevInform_Simple = 0, // 0x00 - InverterDevInform_All = 1, // 0x01 - GridOnProFilePara = 2, // 0x02 - HardWareConfig = 3, // 0x03 - SimpleCalibrationPara = 4, // 0x04 - SystemConfigPara = 5, // 0x05 - RealTimeRunData_Debug = 11, // 0x0b - RealTimeRunData_Reality = 12, // 0x0c - RealTimeRunData_A_Phase = 13, // 0x0d - RealTimeRunData_B_Phase = 14, // 0x0e - RealTimeRunData_C_Phase = 15, // 0x0f - AlarmData = 17, // 0x11, Alarm data - all unsent alarms - AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms - RecordData = 19, // 0x13 - InternalData = 20, // 0x14 - GetLossRate = 21, // 0x15 - GetSelfCheckState = 30, // 0x1e - InitDataState = 0xff -} InfoCmdType; - -typedef enum { - TurnOn = 0, // 0x00 - TurnOff = 1, // 0x01 - Restart = 2, // 0x02 - Lock = 3, // 0x03 - Unlock = 4, // 0x04 - ActivePowerContr = 11, // 0x0b - ReactivePowerContr = 12, // 0x0c - PFSet = 13, // 0x0d - CleanState_LockAndAlarm = 20, // 0x14 - SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files - Init = 0xff -} DevControlCmdType; - typedef enum { AbsolutNonPersistent = 0UL, // 0x0000 RelativNonPersistent = 1UL, // 0x0001 @@ -71,13 +35,6 @@ union serial_u { uint8_t b[8]; }; -#define MIN_SERIAL_INTERVAL 2 // 5 -#define MIN_SEND_INTERVAL 15 -#define MIN_MQTT_INTERVAL 60 - - -enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE}; - enum { DISP_TYPE_T0_NONE = 0, DISP_TYPE_T1_SSD1306_128X64 = 1, @@ -89,27 +46,9 @@ enum { DISP_TYPE_T10_EPAPER = 10 }; - -//------------------------------------- -// EEPROM -//------------------------------------- -#define SSID_LEN 32 -#define PWD_LEN 64 -#define DEVNAME_LEN 16 -#define NTP_ADDR_LEN 32 // DNS Name - -#define MQTT_ADDR_LEN 64 // DNS Name -#define MQTT_CLIENTID_LEN 22 // number of chars is limited to 23 up to v3.1 of MQTT -#define MQTT_USER_LEN 65 // there is another byte necessary for \0 -#define MQTT_PWD_LEN 65 -#define MQTT_TOPIC_LEN 65 - -#define MQTT_MAX_PACKET_SIZE 384 - - typedef struct { uint32_t rxFail; - uint32_t rxFailNoAnser; + uint32_t rxFailNoAnswer; uint32_t rxSuccess; uint32_t frmCnt; uint32_t txCnt; diff --git a/src/eth/ahoyeth.cpp b/src/eth/ahoyeth.cpp deleted file mode 100644 index 2226fce6..00000000 --- a/src/eth/ahoyeth.cpp +++ /dev/null @@ -1,261 +0,0 @@ -//----------------------------------------------------------------------------- -// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ -//----------------------------------------------------------------------------- - -#if defined(ETHERNET) - -#if defined(ESP32) && defined(F) - #undef F - #define F(sl) (sl) -#endif -#include "ahoyeth.h" -#include <ESPmDNS.h> - -//----------------------------------------------------------------------------- -ahoyeth::ahoyeth() -{ - // WiFi.onEvent(ESP32_W5500_event); -} - - -//----------------------------------------------------------------------------- -void ahoyeth::setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNetworkCB, OnTimeCB onTimeCB) { - mConfig = config; - mUtcTimestamp = utcTimestamp; - mOnNetworkCB = onNetworkCB; - mOnTimeCB = onTimeCB; - - Serial.flush(); - WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void { this->onEthernetEvent(event, info); }); - - Serial.flush(); - #if defined(CONFIG_IDF_TARGET_ESP32S3) - mEthSpi.begin(DEF_ETH_MISO_PIN, DEF_ETH_MOSI_PIN, DEF_ETH_SCK_PIN, DEF_ETH_CS_PIN, DEF_ETH_IRQ_PIN, DEF_ETH_RST_PIN); - #else - ETH.begin(DEF_ETH_MISO_PIN, DEF_ETH_MOSI_PIN, DEF_ETH_SCK_PIN, DEF_ETH_CS_PIN, DEF_ETH_IRQ_PIN, ETH_SPI_CLOCK_MHZ, ETH_SPI_HOST); - #endif - - if(mConfig->sys.ip.ip[0] != 0) { - IPAddress ip(mConfig->sys.ip.ip); - IPAddress mask(mConfig->sys.ip.mask); - IPAddress dns1(mConfig->sys.ip.dns1); - IPAddress dns2(mConfig->sys.ip.dns2); - IPAddress gateway(mConfig->sys.ip.gateway); - if(!ETH.config(ip, gateway, mask, dns1, dns2)) - DPRINTLN(DBG_ERROR, F("failed to set static IP!")); - } -} - - -//----------------------------------------------------------------------------- -bool ahoyeth::updateNtpTime(void) { - DPRINTLN(DBG_DEBUG, F(__FUNCTION__)); Serial.flush(); - Serial.printf("ETH.linkUp()=%s\n", ETH.linkUp() ? "up" : "down"); - Serial.print("ETH.localIP()="); - Serial.println(ETH.localIP()); - Serial.printf("Go on? %s\n", (!ETH.localIP()) ? "No..." : "Yes..."); - if (!ETH.localIP()) - return false; - - DPRINTLN(DBG_DEBUG, F("updateNtpTime: checking udp \"connection\"...")); Serial.flush(); - if (!mUdp.connected()) { - DPRINTLN(DBG_DEBUG, F("updateNtpTime: About to (re)connect...")); Serial.flush(); - IPAddress timeServer; - if (!WiFi.hostByName(mConfig->ntp.addr, timeServer)) - return false; - - if (!mUdp.connect(timeServer, mConfig->ntp.port)) - return false; - - DPRINTLN(DBG_DEBUG, F("updateNtpTime: Connected...")); Serial.flush(); - mUdp.onPacket([this](AsyncUDPPacket packet) { - DPRINTLN(DBG_DEBUG, F("updateNtpTime: about to handle ntp packet...")); Serial.flush(); - this->handleNTPPacket(packet); - }); - } - - DPRINTLN(DBG_DEBUG, F("updateNtpTime: prepare packet...")); Serial.flush(); - - // set all bytes in the buffer to 0 - memset(mUdpPacketBuffer, 0, NTP_PACKET_SIZE); - // Initialize values needed to form NTP request - // (see URL above for details on the packets) - - mUdpPacketBuffer[0] = 0b11100011; // LI, Version, Mode - mUdpPacketBuffer[1] = 0; // Stratum, or type of clock - mUdpPacketBuffer[2] = 6; // Polling Interval - mUdpPacketBuffer[3] = 0xEC; // Peer Clock Precision - - // 8 bytes of zero for Root Delay & Root Dispersion - mUdpPacketBuffer[12] = 49; - mUdpPacketBuffer[13] = 0x4E; - mUdpPacketBuffer[14] = 49; - mUdpPacketBuffer[15] = 52; - - //Send unicast - DPRINTLN(DBG_DEBUG, F("updateNtpTime: send packet...")); Serial.flush(); - mUdp.write(mUdpPacketBuffer, sizeof(mUdpPacketBuffer)); - - return true; -} - -//----------------------------------------------------------------------------- -void ahoyeth::handleNTPPacket(AsyncUDPPacket packet) { - char buf[80]; - - memcpy(buf, packet.data(), sizeof(buf)); - - unsigned long highWord = word(buf[40], buf[41]); - unsigned long lowWord = word(buf[42], buf[43]); - - // combine the four bytes (two words) into a long integer - // this is NTP time (seconds since Jan 1 1900): - unsigned long secsSince1900 = highWord << 16 | lowWord; - - *mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time - DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC"); - mOnTimeCB(true); -} - -//----------------------------------------------------------------------------- -void ahoyeth::welcome(String ip, String mode) { - DBGPRINTLN(F("\n\n--------------------------------")); - DBGPRINTLN(F("Welcome to AHOY!")); - DBGPRINT(F("\npoint your browser to http://")); - DBGPRINT(ip); - DBGPRINTLN(mode); - DBGPRINTLN(F("to configure your device")); - DBGPRINTLN(F("--------------------------------\n")); -} - -void ahoyeth::onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info) { - AWS_LOG(F("[ETH]: Got event...")); - switch (event) { -#if ( ( defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2) ) && ( ARDUINO_ESP32_GIT_VER != 0x46d5afb1 ) ) - // For breaking core v2.0.0 - // Why so strange to define a breaking enum arduino_event_id_t in WiFiGeneric.h - // compared to the old system_event_id_t, now in tools/sdk/esp32/include/esp_event/include/esp_event_legacy.h - // You can preserve the old enum order and just adding new items to do no harm - case ARDUINO_EVENT_ETH_START: - AWS_LOG(F("\nETH Started")); - //set eth hostname here - if(String(mConfig->sys.deviceName) != "") - ETH.setHostname(mConfig->sys.deviceName); - else - ETH.setHostname("ESP32_W5500"); - break; - - case ARDUINO_EVENT_ETH_CONNECTED: - AWS_LOG(F("ETH Connected")); - break; - - case ARDUINO_EVENT_ETH_GOT_IP: - if (!ESP32_W5500_eth_connected) { - #if defined (CONFIG_IDF_TARGET_ESP32S3) - AWS_LOG3(F("ETH MAC: "), mEthSpi.macAddress(), F(", IPv4: "), ETH.localIP()); - #else - AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP()); - #endif - - if (ETH.fullDuplex()) { - AWS_LOG0(F("FULL_DUPLEX, ")); - } else { - AWS_LOG0(F("HALF_DUPLEX, ")); - } - - AWS_LOG1(ETH.linkSpeed(), F("Mbps")); - - ESP32_W5500_eth_connected = true; - mOnNetworkCB(true); - } - if (!MDNS.begin(mConfig->sys.deviceName)) { - DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!")); - } else { - DBGPRINT(F("[WiFi] mDNS established: ")); - DBGPRINT(mConfig->sys.deviceName); - DBGPRINTLN(F(".local")); - } - break; - - case ARDUINO_EVENT_ETH_DISCONNECTED: - AWS_LOG("ETH Disconnected"); - ESP32_W5500_eth_connected = false; - mUdp.close(); - mOnNetworkCB(false); - break; - - case ARDUINO_EVENT_ETH_STOP: - AWS_LOG("\nETH Stopped"); - ESP32_W5500_eth_connected = false; - mUdp.close(); - mOnNetworkCB(false); - break; - -#else - - // For old core v1.0.6- - // Core v2.0.0 defines a stupid enum arduino_event_id_t, breaking any code for ESP32_W5500 written for previous core - // Why so strange to define a breaking enum arduino_event_id_t in WiFiGeneric.h - // compared to the old system_event_id_t, now in tools/sdk/esp32/include/esp_event/include/esp_event_legacy.h - // You can preserve the old enum order and just adding new items to do no harm - case SYSTEM_EVENT_ETH_START: - AWS_LOG(F("\nETH Started")); - //set eth hostname here - if(String(mConfig->sys.deviceName) != "") - ETH.setHostname(mConfig->sys.deviceName); - else - ETH.setHostname("ESP32_W5500"); - break; - - case SYSTEM_EVENT_ETH_CONNECTED: - AWS_LOG(F("ETH Connected")); - break; - - case SYSTEM_EVENT_ETH_GOT_IP: - if (!ESP32_W5500_eth_connected) { - AWS_LOG3(F("ETH MAC: "), ETH.macAddress(), F(", IPv4: "), ETH.localIP()); - - if (ETH.fullDuplex()) { - AWS_LOG0(F("FULL_DUPLEX, ")); - } else { - AWS_LOG0(F("HALF_DUPLEX, ")); - } - - AWS_LOG1(ETH.linkSpeed(), F("Mbps")); - - ESP32_W5500_eth_connected = true; - mOnNetworkCB(true); - } - if (!MDNS.begin(mConfig->sys.deviceName)) { - DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!")); - } else { - DBGPRINT(F("[WiFi] mDNS established: ")); - DBGPRINT(mConfig->sys.deviceName); - DBGPRINTLN(F(".local")); - } - break; - - case SYSTEM_EVENT_ETH_DISCONNECTED: - AWS_LOG("ETH Disconnected"); - ESP32_W5500_eth_connected = false; - mUdp.close(); - mOnNetworkCB(false); - break; - - case SYSTEM_EVENT_ETH_STOP: - AWS_LOG("\nETH Stopped"); - ESP32_W5500_eth_connected = false; - mUdp.close(); - mOnNetworkCB(false); - break; -#endif - - default: - - break; - } - -} - -#endif /* defined(ETHERNET) */ diff --git a/src/eth/ahoyeth.h b/src/eth/ahoyeth.h deleted file mode 100644 index ebd91c67..00000000 --- a/src/eth/ahoyeth.h +++ /dev/null @@ -1,64 +0,0 @@ -//----------------------------------------------------------------------------- -// 2024 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed -//----------------------------------------------------------------------------- - -#if defined(ETHERNET) -#ifndef __AHOYETH_H__ -#define __AHOYETH_H__ - -#include <functional> - -#include <Arduino.h> -#include <AsyncUDP.h> -#include <DNSServer.h> - -#include "ethSpi.h" -#include "../utils/dbg.h" -#include "../config/config.h" -#include "../config/settings.h" - -#include "AsyncWebServer_ESP32_W5500.h" - - -class app; - -#define NTP_PACKET_SIZE 48 - -class ahoyeth { - public: /* types */ - typedef std::function<void(bool)> OnNetworkCB; - typedef std::function<void(bool)> OnTimeCB; - - public: - ahoyeth(); - - void setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNetworkCB, OnTimeCB onTimeCB); - bool updateNtpTime(void); - - private: - void setupEthernet(); - - void handleNTPPacket(AsyncUDPPacket packet); - - void welcome(String ip, String mode); - - void onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info); - - private: - #if defined(CONFIG_IDF_TARGET_ESP32S3) - EthSpi mEthSpi; - #endif - settings_t *mConfig = nullptr; - - uint32_t *mUtcTimestamp; - AsyncUDP mUdp; // for time server - byte mUdpPacketBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets - - OnNetworkCB mOnNetworkCB; - OnTimeCB mOnTimeCB; - -}; - -#endif /*__AHOYETH_H__*/ -#endif /* defined(ETHERNET) */ diff --git a/src/hm/CommQueue.h b/src/hm/CommQueue.h index 328309ac..bf6f6861 100644 --- a/src/hm/CommQueue.h +++ b/src/hm/CommQueue.h @@ -19,13 +19,19 @@ template <uint8_t N=100> class CommQueue { public: void addImportant(Inverter<> *iv, uint8_t cmd) { - dec(&mRdPtr); - mQueue[mRdPtr] = queue_s(iv, cmd, true); + queue_s q(iv, cmd, true); + if(!isIncluded(&q)) { + dec(&mRdPtr); + mQueue[mRdPtr] = q; + } } void add(Inverter<> *iv, uint8_t cmd) { - mQueue[mWrPtr] = queue_s(iv, cmd, false); - inc(&mWrPtr); + queue_s q(iv, cmd, false); + if(!isIncluded(&q)) { + mQueue[mWrPtr] = q; + inc(&mWrPtr); + } } void chgCmd(Inverter<> *iv, uint8_t cmd) { @@ -117,6 +123,19 @@ class CommQueue { --(*ptr); } + private: + bool isIncluded(const queue_s *q) { + uint8_t ptr = mRdPtr; + while (ptr != mWrPtr) { + if(mQueue[ptr].cmd == q->cmd) { + if(mQueue[ptr].iv->id == q->iv->id) + return true; + } + inc(&ptr); + } + return false; + } + protected: std::array<queue_s, N> mQueue; uint8_t mWrPtr = 0; diff --git a/src/hm/Communication.h b/src/hm/Communication.h index a37bcdb2..3192c513 100644 --- a/src/hm/Communication.h +++ b/src/hm/Communication.h @@ -117,7 +117,7 @@ class Communication : public CommQueue<> { //q->iv->radioStatistics.txCnt++; q->iv->radio->mRadioWaitTime.startTimeMonitor(mTimeout); - if(!mIsRetransmit && (q->cmd == AlarmData) || (q->cmd == GridOnProFilePara)) + if((!mIsRetransmit && (q->cmd == AlarmData)) || (q->cmd == GridOnProFilePara)) incrAttempt((q->cmd == AlarmData)? MORE_ATTEMPS_ALARMDATA : MORE_ATTEMPS_GRIDONPROFILEPARA); mIsRetransmit = false; @@ -159,8 +159,6 @@ class Communication : public CommQueue<> { mFirstTry = false; mHeu.evalTxChQuality(q->iv, false, 0, 0); mHeu.getTxCh(q->iv); - //q->iv->radioStatistics.rxFailNoAnser++; // should only be one of fail or retransmit. - //q->iv->radioStatistics.txCnt--; q->iv->radioStatistics.retransmits++; q->iv->radio->mRadioWaitTime.stopTimeMonitor(); mState = States::START; @@ -276,7 +274,7 @@ class Communication : public CommQueue<> { DBGPRINT(F(" frames missing ")); DBGPRINTLN(F("-> complete retransmit")); } - mHeu.evalTxChQuality(q->iv, false, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt); + mHeu.evalTxChQuality(q->iv, false, (q->attemptsMax - 1 - q->attempts), q->iv->curFrmCnt, true); q->iv->radioStatistics.txCnt--; q->iv->radioStatistics.retransmits++; mCompleteRetry = true; @@ -622,7 +620,7 @@ class Communication : public CommQueue<> { else if(q->iv->mGotFragment || mCompleteRetry) q->iv->radioStatistics.rxFail++; // got no complete payload else - q->iv->radioStatistics.rxFailNoAnser++; // got nothing + q->iv->radioStatistics.rxFailNoAnswer++; // got nothing mWaitTime.startTimeMonitor(1); // maybe remove, side effects unknown bool keep = false; @@ -893,50 +891,60 @@ class Communication : public CommQueue<> { uint16_t prntsts = (statusMi == 3) ? 1 : statusMi; bool stsok = true; - if ( prntsts != rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)] ) { //sth.'s changed? - q->iv->alarmCnt = 1; // minimum... + bool changedStatus = false; //if true, raise alarms and send via mqtt (might affect single channel only) + uint8_t oldState = rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]; + if ( prntsts != oldState ) { // sth.'s changed? stsok = false; - //sth is or was wrong? - if ((q->iv->type != INV_TYPE_1CH) - && ((statusMi != 3) - || ((q->iv->lastAlarm[stschan].code) && (q->iv->lastAlarm[stschan].code != 1))) - ) { - q->iv->lastAlarm[stschan+q->iv->type==INV_TYPE_2CH ? 2: 4] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts); - q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0); - q->iv->alarmCnt = q->iv->type == INV_TYPE_2CH ? 3 : 5; - } else if ((q->iv->type == INV_TYPE_1CH) - && ( (statusMi != 3) - || ((q->iv->lastAlarm[stschan].code) && (q->iv->lastAlarm[stschan].code != 1))) - ) { - q->iv->lastAlarm[stschan] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts); - } else if (q->iv->type == INV_TYPE_1CH) - stsok = true; - - q->iv->alarmLastId = prntsts; //iv->alarmMesIndex; - - if (q->iv->alarmCnt > 1) { //more than one channel - for (uint8_t ch = 0; ch < (q->iv->alarmCnt); ++ch) { //start with 1 - if (q->iv->lastAlarm[ch].code == 1) { - stsok = true; - break; + if( (!oldState) || (!q->iv->alarmCnt) ) { // initial zero value? => just write this channel to main state and raise changed flags + changedStatus = true; + q->iv->alarmCnt = 1; // minimum... + } else { + //sth is or was wrong? + if (q->iv->type == INV_TYPE_1CH) { + changedStatus = true; + if(q->iv->alarmCnt == 2) // we had sth. other than "producing" in the past + q->iv->lastAlarm[1].end = q->ts; + else { // copy old state and mark as ended + q->iv->lastAlarm[1] = alarm_t(q->iv->lastAlarm[0].code, q->iv->lastAlarm[0].start,q->ts); + q->iv->alarmCnt = 2; + } + } else if((prntsts != 1) || (q->iv->alarmCnt > 1) ) { // we had sth. other than "producing" in the past in at least one channel (2 and 4 ch types) + if (q->iv->alarmCnt == 1) + q->iv->alarmCnt = (q->iv->type == INV_TYPE_2CH) ? 5 : 9; + if(q->iv->lastAlarm[stschan].code != prntsts) { // changed? + changedStatus = true; + if(q->iv->lastAlarm[stschan].code) // copy old data and mark as ended (if any) + q->iv->lastAlarm[(stschan + (q->iv->type==INV_TYPE_2CH ? 2 : 4))] = alarm_t(q->iv->lastAlarm[stschan].code, q->iv->lastAlarm[stschan].start,q->ts); + q->iv->lastAlarm[stschan] = alarm_t(prntsts, q->ts,0); + } + if(changedStatus) { + for (uint8_t i = 1; i <= q->iv->channels; i++) { //start with 1 + if (q->iv->lastAlarm[i].code == 1) { + stsok = true; + break; + } + } } } } - if(*mSerialDebug) { - DPRINT(DBG_WARN, F("New state on CH")); - DBGPRINT(String(stschan)); DBGPRINT(F(" (")); - DBGPRINT(String(prntsts)); DBGPRINT(F("): ")); - DBGPRINTLN(q->iv->getAlarmStr(prntsts)); - } - if(!q->iv->miMultiParts) - q->iv->miMultiParts = 1; // indicate we got status info (1+2 ch types) } if (!stsok) { q->iv->setValue(q->iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts); q->iv->lastAlarm[0] = alarm_t(prntsts, q->ts, 0); + } + if (changedStatus || !stsok) { rec->ts = q->ts; rec->mqttSentStatus = MqttSentStatus::NEW_DATA; + q->iv->alarmLastId = prntsts; //iv->alarmMesIndex; + if (NULL != mCbAlarm) + (mCbAlarm)(q->iv); + if(*mSerialDebug) { + DPRINT(DBG_WARN, F("New state on CH")); + DBGPRINT(String(stschan)); DBGPRINT(F(" (")); + DBGPRINT(String(prntsts)); DBGPRINT(F("): ")); + DBGPRINTLN(q->iv->getAlarmStr(prntsts)); + } } if (q->iv->alarmMesIndex < rec->record[q->iv->getPosByChFld(0, FLD_EVT, rec)]) { @@ -947,6 +955,8 @@ class Communication : public CommQueue<> { DBGPRINTLN(String(q->iv->alarmMesIndex)); } } + if(!q->iv->miMultiParts) + q->iv->miMultiParts = 1; // indicate we got status info (1+2 ch types) } @@ -986,10 +996,8 @@ class Communication : public CommQueue<> { ac_pow += iv->getValue(iv->getPosByChFld(1, FLD_PDC, rec), rec); } else { for(uint8_t i = 1; i <= iv->channels; i++) { - if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1)) { - uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec); - ac_pow += iv->getValue(pos, rec); - } + if ((!iv->lastAlarm[i].code) || (iv->lastAlarm[i].code == 1)) + ac_pow += iv->getValue(iv->getPosByChFld(i, FLD_PDC, rec), rec); } } ac_pow = (int) (ac_pow*9.5); diff --git a/src/hm/Heuristic.h b/src/hm/Heuristic.h index ecf82aa7..1220692e 100644 --- a/src/hm/Heuristic.h +++ b/src/hm/Heuristic.h @@ -38,6 +38,8 @@ class Heuristic { ih->txRfChId = curId; curId = (curId + 1) % RF_MAX_CHANNEL_ID; } + if(ih->txRfQuality[ih->txRfChId] == RF_MIN_QUALTIY) // all channels are bad, reset... + ih->clear(); if(ih->testPeriodSendCnt < 0xff) ih->testPeriodSendCnt++; @@ -71,7 +73,7 @@ class Heuristic { return id2Ch(ih->txRfChId); } - void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments) { + void evalTxChQuality(Inverter<> *iv, bool crcPass, uint8_t retransmits, uint8_t rxFragments, bool quotaMissed = false) { HeuristicInv *ih = &iv->heuristics; #if (DBG_DEBUG == DEBUG_LEVEL) @@ -84,8 +86,10 @@ class Heuristic { DBGPRINT(", "); DBGPRINTLN(String(ih->lastRxFragments)); #endif + if(quotaMissed) // we got not enough frames on this attempt, but iv was answering + updateQuality(ih, (rxFragments > 3 ? RF_TX_CHAN_QUALITY_GOOD : (rxFragments > 1 ? RF_TX_CHAN_QUALITY_OK : RF_TX_CHAN_QUALITY_LOW))); - if(ih->lastRxFragments == rxFragments) { + else if(ih->lastRxFragments == rxFragments) { if(crcPass) updateQuality(ih, RF_TX_CHAN_QUALITY_GOOD); else if(!retransmits || isNewTxCh(ih)) { // nothing received: send probably lost @@ -149,7 +153,7 @@ class Heuristic { DBGPRINT(F(", f: ")); DBGPRINT(String(iv->radioStatistics.rxFail)); DBGPRINT(F(", n: ")); - DBGPRINT(String(iv->radioStatistics.rxFailNoAnser)); + DBGPRINT(String(iv->radioStatistics.rxFailNoAnswer)); DBGPRINT(F(" | p: ")); // better debugging for helpers... if((IV_HMS == iv->ivGen) || (IV_HMT == iv->ivGen)) DBGPRINTLN(String(iv->config->powerLevel-10)); @@ -213,15 +217,12 @@ class Heuristic { } inline uint8_t id2Ch(uint8_t id) { - switch(id) { - case 0: return 3; - case 1: return 23; - case 2: return 40; - case 3: return 61; - case 4: return 75; - } - return 3; // standard + if (id < RF_MAX_CHANNEL_ID) + return mChList[id]; + else + return 3; // standard } + uint8_t mChList[RF_MAX_CHANNEL_ID] = {03, 23, 40, 61, 75}; }; diff --git a/src/hm/hmRadio.h b/src/hm/NrfRadio.h similarity index 90% rename from src/hm/hmRadio.h rename to src/hm/NrfRadio.h index d1d24364..21d0c676 100644 --- a/src/hm/hmRadio.h +++ b/src/hm/NrfRadio.h @@ -8,9 +8,10 @@ #include <RF24.h> #include "SPI.h" -#include "radio.h" +#include "Radio.h" #include "../config/config.h" -#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) +#include "../config/settings.h" +#if defined(SPI_HAL) #include "nrfHal.h" #endif @@ -28,24 +29,30 @@ const char* const rf24AmpPowerNames[] = {"MIN", "LOW", "HIGH", "MAX"}; //----------------------------------------------------------------------------- // HM Radio class //----------------------------------------------------------------------------- -template <uint8_t IRQ_PIN = DEF_NRF_IRQ_PIN, uint8_t CE_PIN = DEF_NRF_CE_PIN, uint8_t CS_PIN = DEF_NRF_CS_PIN, uint8_t AMP_PWR = RF24_PA_LOW, uint8_t SCLK_PIN = DEF_NRF_SCLK_PIN, uint8_t MOSI_PIN = DEF_NRF_MOSI_PIN, uint8_t MISO_PIN = DEF_NRF_MISO_PIN, uint32_t DTU_SN = 0x81001765> -class HmRadio : public Radio { +template <uint32_t DTU_SN = 0x81001765> +class NrfRadio : public Radio { public: - HmRadio() { + NrfRadio() { mDtuSn = DTU_SN; mIrqRcvd = false; - #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) + #if defined(SPI_HAL) //mNrf24.reset(new RF24()); #else - mNrf24.reset(new RF24(CE_PIN, CS_PIN, SPI_SPEED)); + mNrf24.reset(new RF24(DEF_NRF_CE_PIN, DEF_NRF_CS_PIN, SPI_SPEED)); #endif } - ~HmRadio() {} + ~NrfRadio() {} - void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN) { - DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup")); + void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, cfgNrf24_t *cfg) { + DPRINTLN(DBG_VERBOSE, F("NrfRadio::setup")); - pinMode(irq, INPUT_PULLUP); + mCfg = cfg; + //uint8_t irq = IRQ_PIN, uint8_t ce = CE_PIN, uint8_t cs = CS_PIN, uint8_t sclk = SCLK_PIN, uint8_t mosi = MOSI_PIN, uint8_t miso = MISO_PIN + + if(!mCfg->enabled) + return; + + pinMode(mCfg->pinIrq, INPUT_PULLUP); mSerialDebug = serialDebug; mPrivacyMode = privacyMode; @@ -55,8 +62,8 @@ class HmRadio : public Radio { mDtuRadioId = ((uint64_t)(((mDtuSn >> 24) & 0xFF) | ((mDtuSn >> 8) & 0xFF00) | ((mDtuSn << 8) & 0xFF0000) | ((mDtuSn << 24) & 0xFF000000)) << 8) | 0x01; #ifdef ESP32 - #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) - mNrfHal.init(mosi, miso, sclk, cs, ce, SPI_SPEED); + #if defined(SPI_HAL) + mNrfHal.init(mCfg->pinMosi, mCfg->pinMiso, mCfg->pinSclk, mCfg->pinCs, mCfg->pinCe, SPI_SPEED); mNrf24.reset(new RF24(&mNrfHal)); #else #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 @@ -64,7 +71,7 @@ class HmRadio : public Radio { #else mSpi.reset(new SPIClass(VSPI)); #endif - mSpi->begin(sclk, miso, mosi, cs); + mSpi->begin(mCfg->pinSclk, mCfg->pinMiso, mCfg->pinMosi, mCfg->pinCs); #endif #else //the old ESP82xx cannot freely place their SPI pins @@ -72,12 +79,12 @@ class HmRadio : public Radio { mSpi->begin(); #endif - #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) + #if defined(SPI_HAL) mNrf24->begin(); #else - mNrf24->begin(mSpi.get(), ce, cs); + mNrf24->begin(mSpi.get(), mCfg->pinCe, mCfg->pinCs); #endif - mNrf24->setRetries(3, 9); // wait 3*250 = 750us, 16 * 250us -> 4000us = 4ms + mNrf24->setRetries(3, 15); // wait 3*250 = 750us, 16 * 250us -> 4000us = 4ms mNrf24->setDataRate(RF24_250KBPS); //mNrf24->setAutoAck(true); // enabled by default @@ -99,21 +106,24 @@ class HmRadio : public Radio { } // returns true if communication is active - bool loop(void) override { + void loop(void) { + if(!mCfg->enabled) + return; + if (!mIrqRcvd && !mNRFisInRX) - return false; // first quick check => nothing to do at all here + return; // first quick check => nothing to do at all here if(NULL == mLastIv) // prevent reading on NULL object! - return false; + return; if(!mIrqRcvd) { // no news from nRF, check timers if ((millis() - mTimeslotStart) < innerLoopTimeout) - return true; // nothing to do, still waiting + return; // nothing to do, still waiting if (mRadioWaitTime.isTimeout()) { // timeout reached! mNRFisInRX = false; rx_ready = false; - return false; + return; } // otherwise switch to next RX channel @@ -132,7 +142,7 @@ class HmRadio : public Radio { mNrf24->setChannel(mRfChLst[tempRxChIdx]); isRxInit = false; - return true; // communicating, but changed RX channel + return; // communicating, but changed RX channel } else { // here we got news from the nRF mIrqRcvd = false; @@ -145,7 +155,7 @@ class HmRadio : public Radio { if(mNRFisInRX) { DPRINTLN(DBG_WARN, F("unexpected tx irq!")); - return false; + return; } mNRFisInRX = true; @@ -159,7 +169,7 @@ class HmRadio : public Radio { mTimeslotStart = millis(); tempRxChIdx = mRxChIdx; // might be better to start off with one channel less? mRxPendular = false; - mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1); + mNRFloopChannels = (mLastIv->mCmd == MI_REQ_CH1 || mLastIv->mCmd == MI_REQ_CH2); innerLoopTimeout = DURATION_LISTEN_MIN; } @@ -181,18 +191,23 @@ class HmRadio : public Radio { } } rx_ready = false; // reset - return mNRFisInRX; + return; } } - return false; + return; } bool isChipConnected(void) const override { + if(!mCfg->enabled) + return false; return mNrf24->isChipConnected(); } void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override { + if(!mCfg->enabled) + return; + DPRINT_IVID(DBG_INFO, iv->id); DBGPRINT(F("sendControlPacket cmd: ")); DBGHEXLN(cmd); @@ -279,13 +294,14 @@ class HmRadio : public Radio { } uint8_t getDataRate(void) const { - if(!mNrf24->isChipConnected()) + if(!isChipConnected()) return 3; // unknown return mNrf24->getDataRate(); } bool isPVariant(void) const { - return mNrf24->isPVariant(); + if(!isChipConnected()) + return mNrf24->isPVariant(); } private: @@ -413,6 +429,7 @@ class HmRadio : public Radio { } uint64_t mDtuRadioId = 0ULL; + cfgNrf24_t *mCfg = nullptr; const uint8_t mRfChLst[RF_CHANNELS] = {03, 23, 40, 61, 75}; // channel List:2403, 2423, 2440, 2461, 2475MHz uint8_t mTxChIdx = 0; uint8_t mRxChIdx = 0; @@ -432,7 +449,7 @@ class HmRadio : public Radio { std::unique_ptr<SPIClass> mSpi; std::unique_ptr<RF24> mNrf24; - #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) + #if defined(SPI_HAL) nrfHal mNrfHal; #endif Inverter<> *mLastIv = NULL; diff --git a/src/hm/radio.h b/src/hm/Radio.h similarity index 99% rename from src/hm/radio.h rename to src/hm/Radio.h index 31643980..12e80850 100644 --- a/src/hm/radio.h +++ b/src/hm/Radio.h @@ -33,7 +33,7 @@ class Radio { virtual uint16_t getBaseFreqMhz() { return 0; } virtual uint16_t getBootFreqMhz() { return 0; } virtual std::pair<uint16_t,uint16_t> getFreqRangeMhz(void) { return std::make_pair(0, 0); } - virtual bool loop(void) = 0; + virtual void loop(void) = 0; Radio() : mTxBuf{} {} diff --git a/src/hm/hmDefines.h b/src/hm/hmDefines.h index 6ba92774..4287d26e 100644 --- a/src/hm/hmDefines.h +++ b/src/hm/hmDefines.h @@ -9,6 +9,41 @@ #include "../utils/dbg.h" #include <cstdint> +typedef enum { + InverterDevInform_Simple = 0, // 0x00 + InverterDevInform_All = 1, // 0x01 + GridOnProFilePara = 2, // 0x02 + HardWareConfig = 3, // 0x03 + SimpleCalibrationPara = 4, // 0x04 + SystemConfigPara = 5, // 0x05 + RealTimeRunData_Debug = 11, // 0x0b + RealTimeRunData_Reality = 12, // 0x0c + RealTimeRunData_A_Phase = 13, // 0x0d + RealTimeRunData_B_Phase = 14, // 0x0e + RealTimeRunData_C_Phase = 15, // 0x0f + AlarmData = 17, // 0x11, Alarm data - all unsent alarms + AlarmUpdate = 18, // 0x12, Alarm data - all pending alarms + RecordData = 19, // 0x13 + InternalData = 20, // 0x14 + GetLossRate = 21, // 0x15 + GetSelfCheckState = 30, // 0x1e + InitDataState = 0xff +} InfoCmdType; + +typedef enum { + TurnOn = 0, // 0x00 + TurnOff = 1, // 0x01 + Restart = 2, // 0x02 + Lock = 3, // 0x03 + Unlock = 4, // 0x04 + ActivePowerContr = 11, // 0x0b + ReactivePowerContr = 12, // 0x0c + PFSet = 13, // 0x0d + CleanState_LockAndAlarm = 20, // 0x14 + SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files + Init = 0xff +} DevControlCmdType; + // inverter generations enum {IV_MI = 0, IV_HM, IV_HMS, IV_HMT, IV_UNKNOWN}; const char* const generationNames[] = {"MI", "HM", "HMS", "HMT", "UNKNOWN"}; @@ -24,20 +59,20 @@ enum {FLD_UDC = 0, FLD_IDC, FLD_PDC, FLD_YD, FLD_YW, FLD_YT, FLD_IRR, FLD_Q, FLD_EVT, FLD_FW_VERSION, FLD_FW_BUILD_YEAR, FLD_FW_BUILD_MONTH_DAY, FLD_FW_BUILD_HOUR_MINUTE, FLD_BOOTLOADER_VER, FLD_ACT_ACTIVE_PWR_LIMIT, FLD_PART_NUM, FLD_HW_VERSION, FLD_GRID_PROFILE_CODE, - FLD_GRID_PROFILE_VERSION, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP}; + FLD_GRID_PROFILE_VERSION, /*FLD_ACT_REACTIVE_PWR_LIMIT, FLD_ACT_PF,*/ FLD_LAST_ALARM_CODE, FLD_MP, FLD_MT}; const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "YieldTotal", - "U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "UAC_12", "UAC_23", "UAC_31", "I_AC", - "IAC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC", + "U_AC", "U_AC_1N", "U_AC_2N", "U_AC_3N", "U_AC_12", "U_AC_23", "U_AC_31", "I_AC", + "I_AC_1", "I_AC_2", "I_AC_3", "P_AC", "F_AC", "Temp", "PF_AC", "Efficiency", "Irradiation","Q_AC", "ALARM_MES_ID","FWVersion","FWBuildYear","FWBuildMonthDay","FWBuildHourMinute","BootloaderVersion", "active_PowerLimit", "HWPartNumber", "HWVersion", "GridProfileCode", - "GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower"}; + "GridProfileVersion", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode", "MaxPower", "MaxTemp"}; const char* const notAvail = "n/a"; const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_V, UNIT_A, UNIT_A, UNIT_A, UNIT_A, UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR, - UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_W}; + UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_W, UNIT_C}; // mqtt discovery device classes enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; @@ -68,7 +103,7 @@ const byteAssign_fieldDeviceClass deviceFieldAssignment[] = { #define DEVICE_CLS_ASSIGN_LIST_LEN (sizeof(deviceFieldAssignment) / sizeof(byteAssign_fieldDeviceClass)) // indices to calculation functions, defined in hmInverter.h -enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH, CALC_PDC_CH0, CALC_EFF_CH0, CALC_IRR_CH, CALC_MPAC_CH0, CALC_MPDC_CH}; +enum {CALC_YT_CH0 = 0, CALC_YD_CH0, CALC_UDC_CH, CALC_PDC_CH0, CALC_EFF_CH0, CALC_IRR_CH, CALC_MPAC_CH0, CALC_MPDC_CH, CALC_MT_CH0}; enum {CMD_CALC = 0xffff}; @@ -173,7 +208,8 @@ const byteAssign_t hm1chAssignment[] = { { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, - { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } + { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC }, + { FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC } }; #define HM1CH_LIST_LEN (sizeof(hm1chAssignment) / sizeof(byteAssign_t)) #define HM1CH_PAYLOAD_LEN 30 @@ -211,7 +247,8 @@ const byteAssign_t hm2chAssignment[] = { { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, - { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } + { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC }, + { FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC } }; #define HM2CH_LIST_LEN (sizeof(hm2chAssignment) / sizeof(byteAssign_t)) @@ -266,7 +303,8 @@ const byteAssign_t hm4chAssignment[] = { { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, - { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } + { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC }, + { FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC } }; #define HM4CH_LIST_LEN (sizeof(hm4chAssignment) / sizeof(byteAssign_t)) #define HM4CH_PAYLOAD_LEN 62 @@ -351,8 +389,11 @@ const devInfo_t devInfo[] = { { 0x102271, 2000 }, // v2 black backplane, 16A // HMT - { 0x103311, 1800 }, - { 0x103331, 2250 } + { 0x103241, 1600 }, // -4T + { 0x103251, 1800 }, // -4T + { 0x103271, 2000 }, // -4T + { 0x103311, 1800 }, // -6T + { 0x103331, 2250 } // -6T }; #define MI_REQ_CH1 0x09 diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 50b4267c..026bf6d8 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -22,7 +22,7 @@ #include <functional> #include "../config/settings.h" -#include "radio.h" +#include "Radio.h" /** * For values which are of interest and not transmitted by the inverter can be * calculated automatically. @@ -33,28 +33,31 @@ // prototypes template<class T=float> -static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0); +T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0); template<class T=float> -static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0); +T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0); template<class T=float> -static T calcUdcCh(Inverter<> *iv, uint8_t arg0); +T calcUdcCh(Inverter<> *iv, uint8_t arg0); template<class T=float> -static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0); +T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0); template<class T=float> -static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0); +T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0); template<class T=float> -static T calcIrradiation(Inverter<> *iv, uint8_t arg0); +T calcIrradiation(Inverter<> *iv, uint8_t arg0); template<class T=float> -static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0); +T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0); template<class T=float> -static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0); +T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0); + +template<class T=float> +T calcMaxTemperature(Inverter<> *iv, uint8_t arg0); template<class T=float> using func_t = T (Inverter<> *, uint8_t); @@ -84,7 +87,7 @@ struct record_t { byteAssign_t* assign = nullptr; // assignment of bytes in payload uint8_t length = 0; // length of the assignment list T *record = nullptr; // data pointer - uint32_t ts = 0; // timestamp of last received payload + uint32_t ts = 0; // Timestamp of last received payload uint8_t pyldLen = 0; // expected payload length for plausibility check MqttSentStatus mqttSentStatus = MqttSentStatus:: NEW_DATA; // indicates the current MqTT sent status }; @@ -100,14 +103,15 @@ struct alarm_t { // list of all available functions, mapped in hmDefines.h template<class T=float> const calcFunc_t<T> calcFunctions[] = { - { CALC_YT_CH0, &calcYieldTotalCh0 }, - { CALC_YD_CH0, &calcYieldDayCh0 }, - { CALC_UDC_CH, &calcUdcCh }, - { CALC_PDC_CH0, &calcPowerDcCh0 }, - { CALC_EFF_CH0, &calcEffiencyCh0 }, - { CALC_IRR_CH, &calcIrradiation }, - { CALC_MPAC_CH0, &calcMaxPowerAcCh0 }, - { CALC_MPDC_CH, &calcMaxPowerDc } + { CALC_YT_CH0, &calcYieldTotalCh0 }, + { CALC_YD_CH0, &calcYieldDayCh0 }, + { CALC_UDC_CH, &calcUdcCh }, + { CALC_PDC_CH0, &calcPowerDcCh0 }, + { CALC_EFF_CH0, &calcEffiencyCh0 }, + { CALC_IRR_CH, &calcIrradiation }, + { CALC_MPAC_CH0, &calcMaxPowerAcCh0 }, + { CALC_MPDC_CH, &calcMaxPowerDc }, + { CALC_MT_CH0, &calcMaxTemperature } }; template <class REC_TYP> @@ -146,7 +150,8 @@ class Inverter { statistics_t radioStatistics; // information about transmitted, failed, ... packets HeuristicInv heuristics; // heuristic information / logic uint8_t curCmtFreq = 0; // current used CMT frequency, used to check if freq. was changed during runtime - uint32_t tsMaxAcPower = 0; // holds the timestamp when the MaxAC power was seen + uint32_t tsMaxAcPower = 0; // holds the Timestamp when the MaxAC power was seen + uint32_t tsMaxTemperature = 0; // holds the Timestamp when the max temperature was seen bool commEnabled = true; // 'pause night communication' sets this field to false public: @@ -189,7 +194,7 @@ class Inverter { cb(InverterDevInform_Simple, false); // get hardware version } else if((alarmLastId != alarmMesIndex) && (alarmMesIndex != 0)) { cb(AlarmData, false); // get last alarms - } else if((0 == mGridLen) && generalConfig->readGrid) { // read grid profile + } else if((0 == mGridLen) && GeneralConfig->readGrid) { // read grid profile cb(GridOnProFilePara, false); } else if (mGetLossInterval > AHOY_GET_LOSS_INTERVAL) { // get loss rate mGetLossInterval = 1; @@ -213,7 +218,7 @@ class Inverter { if (getChannelFieldValue(CH0, FLD_PART_NUM, rec) == 0) { cb(0x0f, false); // hard- and firmware version for missing HW part nr, delivered by frame 1 mIvRxCnt +=2; - } else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && generalConfig->readGrid) // read grid profile + } else if((getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec) == 0) && GeneralConfig->readGrid) // read grid profile cb(0x10, false); // legacy GPF command } } @@ -229,19 +234,20 @@ class Inverter { initAssignment(&recordAlarm, AlarmData); toRadioId(); curCmtFreq = this->config->frequency; // update to frequency read from settings + resetAlarms(true); } uint8_t getPosByChFld(uint8_t channel, uint8_t fieldId, record_t<> *rec) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:getPosByChFld")); - if(NULL != rec) { - uint8_t pos = 0; - for(; pos < rec->length; pos++) { - if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId)) - break; - } - return (pos >= rec->length) ? 0xff : pos; - } else + if(nullptr == rec) return 0xff; + + uint8_t pos = 0; + for(; pos < rec->length; pos++) { + if((rec->assign[pos].ch == channel) && (rec->assign[pos].fieldId == fieldId)) + break; + } + return (pos >= rec->length) ? 0xff : pos; } byteAssign_t *getByteAssign(uint8_t pos, record_t<> *rec) { @@ -270,15 +276,18 @@ class Inverter { if(InverterStatus::OFF != status) { mDevControlRequest = true; devControlCmd = cmd; - //app->triggerTickSend(); // done in RestApi.h, because of "chicken-and-egg problem ;-)" + assert(App); + App->triggerTickSend(id); + return true; } - return (InverterStatus::OFF != status); + return false; } bool setDevCommand(uint8_t cmd) { - if(InverterStatus::OFF != status) + bool retval = (InverterStatus::OFF != status); + if(retval) devControlCmd = cmd; - return (InverterStatus::OFF != status); + return retval; } void addValue(uint8_t pos, const uint8_t buf[], record_t<> *rec) { @@ -288,32 +297,30 @@ class Inverter { uint8_t end = ptr + rec->assign[pos].num; uint16_t div = rec->assign[pos].div; - if(NULL != rec) { - if(CMD_CALC != div) { - uint32_t val = 0; - do { - val <<= 8; - val |= buf[ptr]; - } while(++ptr != end); - - if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) { - // temperature, Qvar, and power factor are a signed values - rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div); - } else if (FLD_YT == rec->assign[pos].fieldId) { - rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); - } else if (FLD_YD == rec->assign[pos].fieldId) { - float actYD = (REC_TYP)(val) / (REC_TYP)(div) * generalConfig->yieldEffiency; - uint8_t idx = rec->assign[pos].ch - 1; - if (mLastYD[idx] > actYD) - mOffYD[idx] += mLastYD[idx]; - mLastYD[idx] = actYD; - rec->record[pos] = mOffYD[idx] + actYD; - } else { - if ((REC_TYP)(div) > 1) - rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); - else - rec->record[pos] = (REC_TYP)(val); - } + if(CMD_CALC != div) { + uint32_t val = 0; + do { + val <<= 8; + val |= buf[ptr]; + } while(++ptr != end); + + if ((FLD_T == rec->assign[pos].fieldId) || (FLD_Q == rec->assign[pos].fieldId) || (FLD_PF == rec->assign[pos].fieldId)) { + // temperature, Qvar, and power factor are a signed values + rec->record[pos] = ((REC_TYP)((int16_t)val)) / (REC_TYP)(div); + } else if (FLD_YT == rec->assign[pos].fieldId) { + rec->record[pos] = ((REC_TYP)(val) / (REC_TYP)(div)) + ((REC_TYP)config->yieldCor[rec->assign[pos].ch-1]); + } else if (FLD_YD == rec->assign[pos].fieldId) { + float actYD = (REC_TYP)(val) / (REC_TYP)(div); + uint8_t idx = rec->assign[pos].ch - 1; + if (mLastYD[idx] > actYD) + mOffYD[idx] += mLastYD[idx]; + mLastYD[idx] = actYD; + rec->record[pos] = mOffYD[idx] + actYD; + } else { + if ((REC_TYP)(div) > 1) + rec->record[pos] = (REC_TYP)(val) / (REC_TYP)(div); + else + rec->record[pos] = (REC_TYP)(val); } } @@ -337,7 +344,7 @@ class Inverter { // eg. hw version ... } else if (rec->assign == SystemConfigParaAssignment) { DPRINTLN(DBG_DEBUG, "add config"); - if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ + if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos) { actPowerLimit = rec->record[pos]; DPRINT(DBG_DEBUG, F("Inverter actual power limit: ")); DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1)); @@ -356,7 +363,7 @@ class Inverter { bool setValue(uint8_t pos, record_t<> *rec, REC_TYP val) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:setValue")); - if(NULL == rec) + if(nullptr == rec) return false; if(pos > rec->length) return false; @@ -407,23 +414,17 @@ class Inverter { bool isAvailable() { bool avail = false; - if((recordMeas.ts == 0) && (recordInfo.ts == 0) && (recordConfig.ts == 0) && (recordAlarm.ts == 0)) + if(recordMeas.ts == 0) return false; - if((*timestamp - recordMeas.ts) < INVERTER_INACT_THRES_SEC) - avail = true; - if((*timestamp - recordInfo.ts) < INVERTER_INACT_THRES_SEC) - avail = true; - if((*timestamp - recordConfig.ts) < INVERTER_INACT_THRES_SEC) - avail = true; - if((*timestamp - recordAlarm.ts) < INVERTER_INACT_THRES_SEC) + if(((*Timestamp) - recordMeas.ts) < INVERTER_INACT_THRES_SEC) avail = true; if(avail) { if(status < InverterStatus::PRODUCING) status = InverterStatus::STARTING; } else { - if((*timestamp - recordMeas.ts) > INVERTER_OFF_THRES_SEC) { + if(((*Timestamp) - recordMeas.ts) > INVERTER_OFF_THRES_SEC) { if(status != InverterStatus::OFF) { status = InverterStatus::OFF; actPowerLimit = 0xffff; // power limit will be read once inverter becomes available @@ -537,6 +538,10 @@ class Inverter { rec->length = (uint8_t)(HMS4CH_LIST_LEN); rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hms4chAssignment)); rec->pyldLen = HMS4CH_PAYLOAD_LEN; + } else if(IV_HMT == ivGen){ + rec->length = (uint8_t)(HMT4CH_LIST_LEN); + rec->assign = reinterpret_cast<byteAssign_t*>(const_cast<byteAssign_t*>(hmt4chAssignment)); + rec->pyldLen = HMT4CH_PAYLOAD_LEN; } channels = 4; } @@ -584,7 +589,7 @@ class Inverter { } } - void resetAlarms() { + void resetAlarms(bool clearTs = false) { lastAlarm.fill({0, 0, 0}); mAlarmNxtWrPos = 0; alarmCnt = 0; @@ -592,6 +597,11 @@ class Inverter { memset(mOffYD, 0, sizeof(float) * 6); memset(mLastYD, 0, sizeof(float) * 6); + + if(clearTs) { + tsMaxAcPower = *Timestamp; + tsMaxTemperature = *Timestamp; + } } bool parseGetLossRate(const uint8_t pyld[], uint8_t len) { @@ -660,7 +670,6 @@ class Inverter { DPRINTLN(DBG_DEBUG, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(start) + ", end: " + ah::getTimeStr(endTime)); addAlarm(pyld[startOff+1], start, endTime); - alarmCnt++; alarmLastId = alarmMesIndex; return pyld[startOff+1]; @@ -808,6 +817,26 @@ class Inverter { private: inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) { + uint8_t i = 0; + + if(start > end) + end = 0; + + for(; i < 10; i++) { + ++mAlarmNxtWrPos; + mAlarmNxtWrPos = mAlarmNxtWrPos % 10; + + if(lastAlarm[mAlarmNxtWrPos].code == code && lastAlarm[mAlarmNxtWrPos].start == start) { + // replace with same or update end time + if(lastAlarm[mAlarmNxtWrPos].end == 0 || lastAlarm[mAlarmNxtWrPos].end == end) { + break; + } + } + } + + if(alarmCnt < 10 && alarmCnt <= mAlarmNxtWrPos) + alarmCnt = mAlarmNxtWrPos + 1; + lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end); if(++mAlarmNxtWrPos >= 10) // rolling buffer mAlarmNxtWrPos = 0; @@ -824,8 +853,9 @@ class Inverter { } public: - static uint32_t *timestamp; // system timestamp - static cfgInst_t *generalConfig; // general inverter configuration from setup + static uint32_t *Timestamp; // system timestamp + static cfgInst_t *GeneralConfig; // general inverter configuration from setup + static IApp *App; uint16_t mDtuRxCnt = 0; uint16_t mDtuTxCnt = 0; @@ -843,9 +873,11 @@ class Inverter { }; template <class REC_TYP> -uint32_t *Inverter<REC_TYP>::timestamp {0}; +uint32_t *Inverter<REC_TYP>::Timestamp {0}; template <class REC_TYP> -cfgInst_t *Inverter<REC_TYP>::generalConfig {0}; +cfgInst_t *Inverter<REC_TYP>::GeneralConfig {0}; +template <class REC_TYP> +IApp *Inverter<REC_TYP>::App {nullptr}; /** @@ -855,7 +887,7 @@ cfgInst_t *Inverter<REC_TYP>::generalConfig {0}; */ template<class T=float> -static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) { +T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldTotalCh0")); if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); @@ -869,7 +901,7 @@ static T calcYieldTotalCh0(Inverter<> *iv, uint8_t arg0) { } template<class T=float> -static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) { +T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcYieldDayCh0")); if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); @@ -883,7 +915,7 @@ static T calcYieldDayCh0(Inverter<> *iv, uint8_t arg0) { } template<class T=float> -static T calcUdcCh(Inverter<> *iv, uint8_t arg0) { +T calcUdcCh(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcUdcCh")); // arg0 = channel of source record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); @@ -897,7 +929,7 @@ static T calcUdcCh(Inverter<> *iv, uint8_t arg0) { } template<class T=float> -static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) { +T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcPowerDcCh0")); if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); @@ -911,7 +943,7 @@ static T calcPowerDcCh0(Inverter<> *iv, uint8_t arg0) { } template<class T=float> -static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) { +T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcEfficiencyCh0")); if(NULL != iv) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); @@ -927,7 +959,7 @@ static T calcEffiencyCh0(Inverter<> *iv, uint8_t arg0) { } template<class T=float> -static T calcIrradiation(Inverter<> *iv, uint8_t arg0) { +T calcIrradiation(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcIrradiation")); // arg0 = channel if(NULL != iv) { @@ -939,7 +971,7 @@ static T calcIrradiation(Inverter<> *iv, uint8_t arg0) { } template<class T=float> -static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) { +T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerAcCh0")); T acMaxPower = 0.0; if(NULL != iv) { @@ -952,7 +984,7 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) { } } if(acPower > acMaxPower) { - iv->tsMaxAcPower = *iv->timestamp; + iv->tsMaxAcPower = *iv->Timestamp; return acPower; } } @@ -960,7 +992,7 @@ static T calcMaxPowerAcCh0(Inverter<> *iv, uint8_t arg0) { } template<class T=float> -static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) { +T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) { DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxPowerDc")); // arg0 = channel T dcMaxPower = 0.0; @@ -979,4 +1011,22 @@ static T calcMaxPowerDc(Inverter<> *iv, uint8_t arg0) { return dcMaxPower; } +template<class T=float> +T calcMaxTemperature(Inverter<> *iv, uint8_t arg0) { + DPRINTLN(DBG_VERBOSE, F("hmInverter.h:calcMaxTemperature")); + // arg0 = channel + if(NULL != iv) { + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + T temp = iv->getChannelFieldValue(arg0, FLD_T, rec); + T maxTemp = iv->getChannelFieldValue(arg0, FLD_MT, rec); + + if(temp > maxTemp) { + iv->tsMaxTemperature = *iv->Timestamp; + return temp; + } + return maxTemp; + } + return 0; +} + #endif /*__HM_INVERTER_H__*/ diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h index 7e79f30a..81507105 100644 --- a/src/hm/hmSystem.h +++ b/src/hm/hmSystem.h @@ -16,8 +16,9 @@ class HmSystem { HmSystem() {} void setup(uint32_t *timestamp, cfgInst_t *config, IApp *app) { - mInverter[0].timestamp = timestamp; - mInverter[0].generalConfig = config; + INVERTERTYPE::Timestamp = timestamp; + INVERTERTYPE::GeneralConfig = config; + INVERTERTYPE::App = app; //mInverter[0].app = app; } @@ -25,7 +26,7 @@ class HmSystem { DPRINTLN(DBG_VERBOSE, F("hmSystem.h:addInverter")); INVERTERTYPE *iv = &mInverter[id]; iv->id = id; - iv->config = &mInverter[0].generalConfig->iv[id]; + iv->config = &INVERTERTYPE::GeneralConfig->iv[id]; DPRINT(DBG_VERBOSE, "SERIAL: " + String(iv->config->serial.b[5], HEX)); DPRINTLN(DBG_VERBOSE, " " + String(iv->config->serial.b[4], HEX)); if((iv->config->serial.b[5] == 0x11) || (iv->config->serial.b[5] == 0x10)) { @@ -35,6 +36,8 @@ class HmSystem { case 0x21: iv->type = INV_TYPE_1CH; break; + case 0x25: // HMS-400 - 1 channel but payload like 2ch + case 0x44: // HMS-1000 case 0x42: case 0x41: iv->type = INV_TYPE_2CH; @@ -51,15 +54,14 @@ class HmSystem { } if(iv->config->serial.b[5] == 0x11) { - if((iv->config->serial.b[4] & 0x0f) == 0x04) { + if(((iv->config->serial.b[4] & 0x0f) == 0x04) || ((iv->config->serial.b[4] & 0x0f) == 0x05)) { iv->ivGen = IV_HMS; iv->ivRadioType = INV_RADIO_TYPE_CMT; } else { iv->ivGen = IV_HM; iv->ivRadioType = INV_RADIO_TYPE_NRF; } - } - else if((iv->config->serial.b[4] & 0x03) == 0x02) { // MI 3rd Gen -> same as HM + } else if((iv->config->serial.b[4] & 0x03) == 0x02) { // MI 3rd Gen -> same as HM iv->ivGen = IV_HM; iv->ivRadioType = INV_RADIO_TYPE_NRF; } else { // MI 2nd Gen @@ -67,11 +69,16 @@ class HmSystem { iv->ivRadioType = INV_RADIO_TYPE_NRF; } } else if(iv->config->serial.b[5] == 0x13) { - iv->ivGen = IV_HMT; + iv->ivGen = IV_HMT; + if(iv->config->serial.b[4] == 0x61) + iv->type = INV_TYPE_4CH; + else iv->type = INV_TYPE_6CH; - iv->ivRadioType = INV_RADIO_TYPE_CMT; + + iv->ivRadioType = INV_RADIO_TYPE_CMT; } else if(iv->config->serial.u64 != 0ULL) { DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); + iv->config->enabled = false; return; } else iv->ivGen = IV_UNKNOWN; @@ -82,7 +89,7 @@ class HmSystem { DPRINT(DBG_INFO, "added inverter "); if(iv->config->serial.b[5] == 0x11) { - if((iv->config->serial.b[4] & 0x0f) == 0x04) + if(((iv->config->serial.b[4] & 0x0f) == 0x04) || ((iv->config->serial.b[4] & 0x0f) == 0x05)) DBGPRINT("HMS"); else DBGPRINT("HM"); @@ -114,6 +121,8 @@ class HmSystem { DPRINTLN(DBG_VERBOSE, F("hmSystem.h:getInverterByPos")); if(pos >= MAX_INVERTER) return nullptr; + else if(nullptr == mInverter[pos].config) + return nullptr; else if((mInverter[pos].config->serial.u64 != 0ULL) || (false == check)) return &mInverter[pos]; else diff --git a/src/hm/nrfHal.h b/src/hm/nrfHal.h index b9265626..a838f8bc 100644 --- a/src/hm/nrfHal.h +++ b/src/hm/nrfHal.h @@ -9,18 +9,15 @@ #pragma once #include "../utils/spiPatcher.h" - #include <esp_rom_gpio.h> -#include <RF24_hal.h> +#include <RF24.h> #define NRF_MAX_TRANSFER_SZ 64 #define NRF_DEFAULT_SPI_SPEED 10000000 // 10 MHz class nrfHal: public RF24_hal, public SpiPatcherHandle { public: - nrfHal() { - mSpiPatcher = SpiPatcher::getInstance(SPI2_HOST); - } + nrfHal() {} void patch() override { esp_rom_gpio_connect_out_signal(mPinMosi, spi_periph_signal[mHostDevice].spid_out, false, false); @@ -42,7 +39,13 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle { mPinEn = static_cast<gpio_num_t>(en); mSpiSpeed = speed; - mHostDevice = mSpiPatcher->getDevice(); + #if defined(CONFIG_IDF_TARGET_ESP32S3) + mHostDevice = SPI2_HOST; + #else + mHostDevice = (14 == sclk) ? SPI2_HOST : SPI_HOST_OTHER; + #endif + + mSpiPatcher = SpiPatcher::getInstance(mHostDevice); gpio_reset_pin(mPinMosi); gpio_set_direction(mPinMosi, GPIO_MODE_OUTPUT); @@ -56,6 +59,7 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle { gpio_set_level(mPinClk, 0); gpio_reset_pin(mPinCs); + request_spi(); spi_device_interface_config_t devcfg = { .command_bits = 0, .address_bits = 0, @@ -72,14 +76,14 @@ class nrfHal: public RF24_hal, public SpiPatcherHandle { .pre_cb = nullptr, .post_cb = nullptr }; - ESP_ERROR_CHECK(spi_bus_add_device(mHostDevice, &devcfg, &spi)); + mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi); + release_spi(); gpio_reset_pin(mPinEn); gpio_set_direction(mPinEn, GPIO_MODE_OUTPUT); gpio_set_level(mPinEn, 0); } - bool begin() override { return true; } diff --git a/src/hms/hmsRadio.h b/src/hms/CmtRadio.h similarity index 90% rename from src/hms/hmsRadio.h rename to src/hms/CmtRadio.h index d074442b..b6405329 100644 --- a/src/hms/hmsRadio.h +++ b/src/hms/CmtRadio.h @@ -7,7 +7,7 @@ #define __HMS_RADIO_H__ #include "cmt2300a.h" -#include "../hm/radio.h" +#include "../hm/Radio.h" //#define CMT_SWITCH_CHANNEL_CYCLE 5 @@ -15,25 +15,34 @@ template<uint32_t DTU_SN = 0x81001765> class CmtRadio : public Radio { typedef Cmt2300a CmtType; public: - void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, uint8_t pinSclk, uint8_t pinSdio, uint8_t pinCsb, uint8_t pinFcsb, uint8_t region = 0, bool genDtuSn = true) { - mCmt.setup(pinSclk, pinSdio, pinCsb, pinFcsb); - reset(genDtuSn, static_cast<RegionCfg>(region)); + void setup(bool *serialDebug, bool *privacyMode, bool *printWholeTrace, cfgCmt_t *cfg, uint8_t region = 0, bool genDtuSn = true) { + mCfg = cfg; + + if(!cfg->enabled) + return; + mPrivacyMode = privacyMode; mSerialDebug = serialDebug; mPrintWholeTrace = printWholeTrace; mTxBuf.fill(0); + + mCmt.setup(cfg->pinSclk, cfg->pinSdio, cfg->pinCsb, cfg->pinFcsb); + reset(genDtuSn, static_cast<RegionCfg>(region)); } - bool loop() override { + void loop() override { + if(!mCfg->enabled) + return; + mCmt.loop(); if((!mIrqRcvd) && (!mRqstGetRx)) - return false; + return; getRx(); if(CmtStatus::SUCCESS == mCmt.goRx()) { mIrqRcvd = false; mRqstGetRx = false; } - return false; + return; } bool isChipConnected(void) const override { @@ -41,6 +50,9 @@ class CmtRadio : public Radio { } void sendControlPacket(Inverter<> *iv, uint8_t cmd, uint16_t *data, bool isRetransmit) override { + if(!mCfg->enabled) + return; + DPRINT(DBG_INFO, F("sendControlPacket cmd: ")); DBGHEXLN(cmd); initPacket(iv->radioId.u64, TX_REQ_DEVCONTROL, SINGLE_FRAME); @@ -59,6 +71,9 @@ class CmtRadio : public Radio { } bool switchFrequency(Inverter<> *iv, uint32_t fromkHz, uint32_t tokHz) override { + if(!isChipConnected()) + return false; + uint8_t fromCh = mCmt.freq2Chan(fromkHz); uint8_t toCh = mCmt.freq2Chan(tokHz); @@ -68,6 +83,8 @@ class CmtRadio : public Radio { bool switchFrequencyCh(Inverter<> *iv, uint8_t fromCh, uint8_t toCh) override { if((0xff == fromCh) || (0xff == toCh)) return false; + if(!isChipConnected()) + return false; mCmt.switchChannel(fromCh); sendSwitchChCmd(iv, toCh); @@ -183,11 +200,12 @@ class CmtRadio : public Radio { if(p.packet[9] > ALL_FRAMES) { // indicates last frame setExpectedFrames(p.packet[9] - ALL_FRAMES); - mRadioWaitTime.startTimeMonitor(DURATION_PAUSE_LASTFR); // let the inverter first get back to rx mode? + mRadioWaitTime.startTimeMonitor(2); // let the inverter first get back to rx mode? } } CmtType mCmt; + cfgCmt_t *mCfg = nullptr; bool mCmtAvail = false; bool mRqstGetRx = false; uint32_t mMillis = 0; diff --git a/src/hms/cmt2300a.h b/src/hms/cmt2300a.h index 23911b15..ed3aab54 100644 --- a/src/hms/cmt2300a.h +++ b/src/hms/cmt2300a.h @@ -6,7 +6,7 @@ #ifndef __CMT2300A_H__ #define __CMT2300A_H__ -#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) +#if defined(SPI_HAL) #include "cmtHal.h" #else #include "esp32_3wSpi.h" @@ -545,7 +545,7 @@ class Cmt2300a { } private: - #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(SPI_HAL) + #if defined(SPI_HAL) cmtHal mSpi; #else esp32_3wSpi mSpi; diff --git a/src/hms/cmtHal.h b/src/hms/cmtHal.h index a4bec587..8556a043 100644 --- a/src/hms/cmtHal.h +++ b/src/hms/cmtHal.h @@ -16,9 +16,7 @@ class cmtHal : public SpiPatcherHandle { public: - cmtHal() { - mSpiPatcher = SpiPatcher::getInstance(DEF_CMT_SPI_HOST); - } + cmtHal() {} void patch() override { esp_rom_gpio_connect_out_signal(mPinSdio, spi_periph_signal[mHostDevice].spid_out, false, false); @@ -39,7 +37,13 @@ class cmtHal : public SpiPatcherHandle { mPinFcs = static_cast<gpio_num_t>(fcs); mSpiSpeed = speed; - mHostDevice = mSpiPatcher->getDevice(); + #if defined(CONFIG_IDF_TARGET_ESP32S3) + mHostDevice = SPI2_HOST; + #else + mHostDevice = (14 == clk) ? SPI2_HOST : SPI_HOST_OTHER; + #endif + + mSpiPatcher = SpiPatcher::getInstance(mHostDevice); gpio_reset_pin(mPinSdio); gpio_set_direction(mPinSdio, GPIO_MODE_INPUT_OUTPUT); @@ -50,6 +54,7 @@ class cmtHal : public SpiPatcherHandle { gpio_set_level(mPinClk, 0); gpio_reset_pin(mPinCs); + request_spi(); spi_device_interface_config_t devcfg_reg = { .command_bits = 1, .address_bits = 7, @@ -66,7 +71,8 @@ class cmtHal : public SpiPatcherHandle { .pre_cb = nullptr, .post_cb = nullptr }; - ESP_ERROR_CHECK(spi_bus_add_device(mHostDevice, &devcfg_reg, &spi_reg)); + mSpiPatcher->addDevice(mHostDevice, &devcfg_reg, &spi_reg); + release_spi(); gpio_reset_pin(mPinFcs); spi_device_interface_config_t devcfg_fifo = { diff --git a/src/hms/hmsDefines.h b/src/hms/hmsDefines.h index 61275dc1..07aec682 100644 --- a/src/hms/hmsDefines.h +++ b/src/hms/hmsDefines.h @@ -33,7 +33,8 @@ const byteAssign_t hms1chAssignment[] = { { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, - { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } + { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC }, + { FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC } }; #define HMS1CH_LIST_LEN (sizeof(hms1chAssignment) / sizeof(byteAssign_t)) #define HMS1CH_PAYLOAD_LEN 30 @@ -70,7 +71,8 @@ const byteAssign_t hms2chAssignment[] = { { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, - { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } + { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC }, + { FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC } }; #define HMS2CH_LIST_LEN (sizeof(hms2chAssignment) / sizeof(byteAssign_t)) #define HMS2CH_PAYLOAD_LEN 42 @@ -123,11 +125,73 @@ const byteAssign_t hms4chAssignment[] = { { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, - { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } + { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC }, + { FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC } }; #define HMS4CH_LIST_LEN (sizeof(hms4chAssignment) / sizeof(byteAssign_t)) #define HMS4CH_PAYLOAD_LEN 66 +//------------------------------------- +// HMT-1600, HMT-1800, HMT-2000 +//------------------------------------- +const byteAssign_t hmt4chAssignment[] = { + { FLD_UDC, UNIT_V, CH1, 2, 2, 10 }, + { FLD_IDC, UNIT_A, CH1, 4, 2, 100 }, + { FLD_PDC, UNIT_W, CH1, 8, 2, 10 }, + { FLD_YT, UNIT_KWH, CH1, 12, 4, 1000 }, + { FLD_YD, UNIT_WH, CH1, 20, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH1, CALC_IRR_CH, CH1, CMD_CALC }, + { FLD_MP, UNIT_W, CH1, CALC_MPDC_CH, CH1, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH2, CALC_UDC_CH, CH1, CMD_CALC }, + { FLD_IDC, UNIT_A, CH2, 6, 2, 100 }, + { FLD_PDC, UNIT_W, CH2, 10, 2, 10 }, + { FLD_YT, UNIT_KWH, CH2, 16, 4, 1000 }, + { FLD_YD, UNIT_WH, CH2, 22, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH2, CALC_IRR_CH, CH2, CMD_CALC }, + { FLD_MP, UNIT_W, CH2, CALC_MPDC_CH, CH2, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH3, 24, 2, 10 }, + { FLD_IDC, UNIT_A, CH3, 26, 2, 100 }, + { FLD_PDC, UNIT_W, CH3, 30, 2, 10 }, + { FLD_YT, UNIT_KWH, CH3, 34, 4, 1000 }, + { FLD_YD, UNIT_WH, CH3, 42, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH3, CALC_IRR_CH, CH3, CMD_CALC }, + { FLD_MP, UNIT_W, CH3, CALC_MPDC_CH, CH3, CMD_CALC }, + + { FLD_UDC, UNIT_V, CH4, CALC_UDC_CH, CH3, CMD_CALC }, + { FLD_IDC, UNIT_A, CH4, 28, 2, 100 }, + { FLD_PDC, UNIT_W, CH4, 32, 2, 10 }, + { FLD_YT, UNIT_KWH, CH4, 38, 4, 1000 }, + { FLD_YD, UNIT_WH, CH4, 44, 2, 1 }, + { FLD_IRR, UNIT_PCT, CH4, CALC_IRR_CH, CH4, CMD_CALC }, + { FLD_MP, UNIT_W, CH4, CALC_MPDC_CH, CH4, CMD_CALC }, + + { FLD_UAC_1N, UNIT_V, CH0, 68, 2, 10 }, + { FLD_UAC_2N, UNIT_V, CH0, 70, 2, 10 }, + { FLD_UAC_3N, UNIT_V, CH0, 72, 2, 10 }, + { FLD_UAC_12, UNIT_V, CH0, 74, 2, 10 }, + { FLD_UAC_23, UNIT_V, CH0, 76, 2, 10 }, + { FLD_UAC_31, UNIT_V, CH0, 78, 2, 10 }, + { FLD_F, UNIT_HZ, CH0, 80, 2, 100 }, + { FLD_PAC, UNIT_W, CH0, 82, 2, 10 }, + { FLD_Q, UNIT_VAR, CH0, 84, 2, 10 }, + { FLD_IAC_1, UNIT_A, CH0, 86, 2, 100 }, + { FLD_IAC_2, UNIT_A, CH0, 88, 2, 100 }, + { FLD_IAC_3, UNIT_A, CH0, 90, 2, 100 }, + { FLD_PF, UNIT_NONE, CH0, 92, 2, 1000 }, + { FLD_T, UNIT_C, CH0, 94, 2, 10 }, + { FLD_EVT, UNIT_NONE, CH0, 96, 2, 1 }, + { FLD_YD, UNIT_WH, CH0, CALC_YD_CH0, 0, CMD_CALC }, + { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, + { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, + { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, + { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC }, + { FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC } +}; +#define HMT4CH_LIST_LEN (sizeof(hmt4chAssignment) / sizeof(byteAssign_t)) +#define HMT4CH_PAYLOAD_LEN 98 + //------------------------------------- // HMT-1800, HMT-2250 //------------------------------------- @@ -199,7 +263,8 @@ const byteAssign_t hmt6chAssignment[] = { { FLD_YT, UNIT_KWH, CH0, CALC_YT_CH0, 0, CMD_CALC }, { FLD_PDC, UNIT_W, CH0, CALC_PDC_CH0, 0, CMD_CALC }, { FLD_EFF, UNIT_PCT, CH0, CALC_EFF_CH0, 0, CMD_CALC }, - { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC } + { FLD_MP, UNIT_W, CH0, CALC_MPAC_CH0, 0, CMD_CALC }, + { FLD_MT, UNIT_C, CH0, CALC_MT_CH0, 0, CMD_CALC } }; #define HMT6CH_LIST_LEN (sizeof(hmt6chAssignment) / sizeof(byteAssign_t)) #define HMT6CH_PAYLOAD_LEN 98 diff --git a/src/network/AhoyEthernet.h b/src/network/AhoyEthernet.h new file mode 100644 index 00000000..9ed2de98 --- /dev/null +++ b/src/network/AhoyEthernet.h @@ -0,0 +1,151 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __AHOY_ETHERNET_H__ +#define __AHOY_ETHERNET_H__ + +#if defined(ETHERNET) +#include <functional> +#include <AsyncUDP.h> +#include <ETH.h> +#include "AhoyEthernetSpi.h" +#include "AhoyNetwork.h" +#include "AhoyWifiEsp32.h" + +class AhoyEthernet : public AhoyWifi { + private: + enum class Mode { + WIRED, + WIRELESS + }; + + public: + AhoyEthernet() + : mMode (Mode::WIRELESS) {} + + virtual void begin() override { + mMode = Mode::WIRELESS; + mAp.enable(); + AhoyWifi::begin(); + + if(!mConfig->sys.eth.enabled) + return; + + mEthSpi.begin(mConfig->sys.eth.pinMiso, mConfig->sys.eth.pinMosi, mConfig->sys.eth.pinSclk, mConfig->sys.eth.pinCs, mConfig->sys.eth.pinIrq, mConfig->sys.eth.pinRst); + ETH.setHostname(mConfig->sys.deviceName); + } + + virtual String getIp(void) override { + if(Mode::WIRELESS == mMode) + return AhoyWifi::getIp(); + else + return ETH.localIP().toString(); + } + + virtual String getMac(void) override { + if(Mode::WIRELESS == mMode) + return AhoyWifi::getMac(); + else + return mEthSpi.macAddress(); + } + + virtual bool isWiredConnection() override { + return (Mode::WIRED == mMode); + } + + private: + virtual void OnEvent(WiFiEvent_t event) override { + switch(event) { + case ARDUINO_EVENT_ETH_CONNECTED: + mMode = Mode::WIRED; // needed for static IP + [[fallthrough]]; + case SYSTEM_EVENT_STA_CONNECTED: + mWifiConnecting = false; + if(NetworkState::CONNECTED != mStatus) { + if(ARDUINO_EVENT_ETH_CONNECTED == event) + WiFi.disconnect(); + + mStatus = NetworkState::CONNECTED; + DPRINTLN(DBG_INFO, F("Network connected")); + setStaticIp(); + } + break; + + case SYSTEM_EVENT_STA_GOT_IP: + mStatus = NetworkState::GOT_IP; + if(mAp.isEnabled()) + mAp.disable(); + + mMode = Mode::WIRELESS; + if(!mConnected) { + mConnected = true; + ah::welcome(WiFi.localIP().toString(), F("Station WiFi")); + MDNS.begin(mConfig->sys.deviceName); + mOnNetworkCB(true); + } + break; + + case ARDUINO_EVENT_ETH_GOT_IP: + mStatus = NetworkState::GOT_IP; + mMode = Mode::WIRED; + if(!mConnected) { + mAp.disable(); + mConnected = true; + ah::welcome(ETH.localIP().toString(), F("Station Ethernet")); + MDNS.begin(mConfig->sys.deviceName); + mOnNetworkCB(true); + WiFi.disconnect(); + } + break; + + case ARDUINO_EVENT_ETH_STOP: + [[fallthrough]]; + case ARDUINO_EVENT_ETH_DISCONNECTED: + mStatus = NetworkState::DISCONNECTED; + if(mConnected) { + mMode = Mode::WIRELESS; + mConnected = false; + mOnNetworkCB(false); + MDNS.end(); + AhoyWifi::begin(); + } + break; + + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + [[fallthrough]]; + case ARDUINO_EVENT_WIFI_STA_STOP: + [[fallthrough]]; + case SYSTEM_EVENT_STA_DISCONNECTED: + mStatus = NetworkState::DISCONNECTED; + if(mConnected && (Mode::WIRELESS == mMode)) { + mConnected = false; + mOnNetworkCB(false); + MDNS.end(); + AhoyWifi::begin(); + } + break; + + default: + break; + } + } + + void setStaticIp() override { + setupIp([this](IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2) -> bool { + if(Mode::WIRELESS == mMode) + return WiFi.config(ip, gateway, mask, dns1, dns2); + else + return ETH.config(ip, gateway, mask, dns1, dns2); + }); + } + + private: + AhoyEthernetSpi mEthSpi; + Mode mMode; + +}; + +#endif /*ETHERNET*/ +#endif /*__AHOY_ETHERNET_H__*/ diff --git a/src/eth/ethSpi.h b/src/network/AhoyEthernetSpi.h similarity index 70% rename from src/eth/ethSpi.h rename to src/network/AhoyEthernetSpi.h index d0ef9487..ec750592 100644 --- a/src/eth/ethSpi.h +++ b/src/network/AhoyEthernetSpi.h @@ -1,10 +1,8 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed //----------------------------------------------------------------------------- - -#if defined(CONFIG_IDF_TARGET_ESP32S3) #if defined(ETHERNET) #ifndef __ETH_SPI_H__ #define __ETH_SPI_H__ @@ -14,23 +12,25 @@ #include <Arduino.h> #include <esp_netif.h> #include <WiFiGeneric.h> -#include <driver/spi_master.h> +#include "../utils/spiPatcher.h" // Functions from WiFiGeneric void tcpipInit(); void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif); -class EthSpi { +class AhoyEthernetSpi { public: - EthSpi() : + AhoyEthernetSpi() : eth_handle(nullptr), eth_netif(nullptr) {} void begin(int8_t pin_miso, int8_t pin_mosi, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) { - gpio_reset_pin(static_cast<gpio_num_t>(pin_rst)); - gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT); - gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0); + if(-1 != pin_rst) { + gpio_reset_pin(static_cast<gpio_num_t>(pin_rst)); + gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT); + gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0); + } gpio_reset_pin(static_cast<gpio_num_t>(pin_sclk)); gpio_reset_pin(static_cast<gpio_num_t>(pin_mosi)); @@ -44,22 +44,14 @@ class EthSpi { gpio_reset_pin(static_cast<gpio_num_t>(pin_int)); gpio_set_pull_mode(static_cast<gpio_num_t>(pin_int), GPIO_PULLUP_ONLY); - spi_bus_config_t buscfg = { - .mosi_io_num = pin_mosi, - .miso_io_num = pin_miso, - .sclk_io_num = pin_sclk, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .data4_io_num = -1, - .data5_io_num = -1, - .data6_io_num = -1, - .data7_io_num = -1, - .max_transfer_sz = 0, // uses default value internally - .flags = 0, - .intr_flags = 0 - }; + #if defined(CONFIG_IDF_TARGET_ESP32S3) + mHostDevice = SPI3_HOST; + #else + mHostDevice = (14 == pin_sclk) ? SPI2_HOST : SPI3_HOST; + #endif - ESP_ERROR_CHECK(spi_bus_initialize(SPI3_HOST, &buscfg, SPI_DMA_CH_AUTO)); + mSpiPatcher = SpiPatcher::getInstance(mHostDevice, false); + mSpiPatcher->initBus(pin_mosi, pin_miso, pin_sclk, SPI_DMA_CH_AUTO); spi_device_interface_config_t devcfg = { .command_bits = 16, // actually address phase @@ -78,13 +70,14 @@ class EthSpi { .post_cb = nullptr }; - spi_device_handle_t spi; - ESP_ERROR_CHECK(spi_bus_add_device(SPI3_HOST, &devcfg, &spi)); + mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi); // Reset sequence - delayMicroseconds(500); - gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1); - delayMicroseconds(1000); + if(-1 != pin_rst) { + delayMicroseconds(500); + gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1); + delayMicroseconds(1000); + } // Arduino function to start networking stack if not already started tcpipInit(); @@ -123,10 +116,14 @@ class EthSpi { } String macAddress() { - uint8_t mac_addr[6] = {0, 0, 0, 0, 0, 0}; + uint8_t mac_addr[6]; esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr); - char mac_addr_str[24]; - snprintf(mac_addr_str, sizeof(mac_addr_str), "%02X:%02X:%02X:%02X:%02X:%02X", mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); + char mac_addr_str[19]; + for(uint8_t i = 0; i < 6; i++) { + snprintf(&mac_addr_str[i*3], sizeof(mac_addr_str), "%02X", mac_addr[i]); + mac_addr_str[i*3+2] = ':'; + } + mac_addr_str[17] = '\0'; return String(mac_addr_str); } @@ -134,8 +131,10 @@ class EthSpi { private: esp_eth_handle_t eth_handle; esp_netif_t *eth_netif; + spi_host_device_t mHostDevice; + spi_device_handle_t spi; + SpiPatcher *mSpiPatcher; }; #endif /*__ETH_SPI_H__*/ #endif /*ETHERNET*/ -#endif /*CONFIG_IDF_TARGET_ESP32S3*/ diff --git a/src/network/AhoyNetwork.h b/src/network/AhoyNetwork.h new file mode 100644 index 00000000..e64a13b1 --- /dev/null +++ b/src/network/AhoyNetwork.h @@ -0,0 +1,254 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __AHOY_NETWORK_H__ +#define __AHOY_NETWORK_H__ + +#include "AhoyNetworkHelper.h" +#include "../config/settings.h" +#include "../utils/helper.h" +#include "AhoyWifiAp.h" +#include "AsyncJson.h" + +#define NTP_PACKET_SIZE 48 + +class AhoyNetwork { + public: + typedef std::function<void(bool)> OnNetworkCB; + typedef std::function<void(bool)> OnTimeCB; + + public: + void setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNetworkCB, OnTimeCB onTimeCB) { + mConfig = config; + mUtcTimestamp = utcTimestamp; + mOnNetworkCB = onNetworkCB; + mOnTimeCB = onTimeCB; + + if('\0' == mConfig->sys.deviceName[0]) + snprintf(mConfig->sys.deviceName, DEVNAME_LEN, "%s", DEF_DEVICE_NAME); + + mAp.setup(&mConfig->sys); + + #if defined(ESP32) + WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void { + OnEvent(event); + }); + #else + wifiConnectHandler = WiFi.onStationModeConnected( + [this](const WiFiEventStationModeConnected& event) -> void { + OnEvent((WiFiEvent_t)SYSTEM_EVENT_STA_CONNECTED); + }); + wifiGotIPHandler = WiFi.onStationModeGotIP( + [this](const WiFiEventStationModeGotIP& event) -> void { + OnEvent((WiFiEvent_t)SYSTEM_EVENT_STA_GOT_IP); + }); + wifiDisconnectHandler = WiFi.onStationModeDisconnected( + [this](const WiFiEventStationModeDisconnected& event) -> void { + OnEvent((WiFiEvent_t)SYSTEM_EVENT_STA_DISCONNECTED); + }); + #endif + } + + bool isConnected() const { + return (mStatus == NetworkState::CONNECTED); + } + + bool updateNtpTime(void) { + if(NetworkState::GOT_IP != mStatus) + return false; + + if (!mUdp.connected()) { + IPAddress timeServer; + if (!WiFi.hostByName(mConfig->ntp.addr, timeServer)) + return false; + if (!mUdp.connect(timeServer, mConfig->ntp.port)) + return false; + } + + mUdp.onPacket([this](AsyncUDPPacket packet) { + this->handleNTPPacket(packet); + }); + sendNTPpacket(); + + return true; + } + + public: + virtual void begin() = 0; + virtual void tickNetworkLoop() = 0; + virtual String getIp(void) = 0; + virtual String getMac(void) = 0; + + virtual bool getWasInCh12to14() { + return false; + } + + virtual bool isWiredConnection() { + return false; + } + + bool isApActive() { + return mAp.isEnabled(); + } + + bool getAvailNetworks(JsonObject obj, IApp *app) { + if(!mScanActive) { + app->addOnce([this]() {scan();}, 1, "scan"); + return false; + } + + int n = WiFi.scanComplete(); + if (WIFI_SCAN_RUNNING == n) + return false; + + if(n > 0) { + JsonArray nets = obj.createNestedArray(F("networks")); + int sort[n]; + sortRSSI(&sort[0], n); + for (int i = 0; i < n; ++i) { + nets[i][F("ssid")] = WiFi.SSID(sort[i]); + nets[i][F("rssi")] = WiFi.RSSI(sort[i]); + } + } + mScanActive = false; + WiFi.scanDelete(); + + return true; + } + + void scan(void) { + mScanActive = true; + if(mWifiConnecting) { + mWifiConnecting = false; + WiFi.disconnect(); + } + WiFi.scanNetworks(true, true); + } + + protected: + virtual void setStaticIp() = 0; + + void setupIp(std::function<bool(IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2)> cb) { + if(mConfig->sys.ip.ip[0] != 0) { + IPAddress ip(mConfig->sys.ip.ip); + IPAddress mask(mConfig->sys.ip.mask); + IPAddress dns1(mConfig->sys.ip.dns1); + IPAddress dns2(mConfig->sys.ip.dns2); + IPAddress gateway(mConfig->sys.ip.gateway); + if(cb(ip, gateway, mask, dns1, dns2)) + DPRINTLN(DBG_ERROR, F("failed to set static IP!")); + } + } + + virtual void OnEvent(WiFiEvent_t event) { + switch(event) { + case SYSTEM_EVENT_STA_CONNECTED: + [[fallthrough]]; + case ARDUINO_EVENT_ETH_CONNECTED: + if(NetworkState::CONNECTED != mStatus) { + mStatus = NetworkState::CONNECTED; + DPRINTLN(DBG_INFO, F("Network connected")); + } + break; + + case SYSTEM_EVENT_STA_GOT_IP: + [[fallthrough]]; + case ARDUINO_EVENT_ETH_GOT_IP: + mStatus = NetworkState::GOT_IP; + break; + + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + [[fallthrough]]; + case ARDUINO_EVENT_WIFI_STA_STOP: + [[fallthrough]]; + case SYSTEM_EVENT_STA_DISCONNECTED: + [[fallthrough]]; + case ARDUINO_EVENT_ETH_STOP: + [[fallthrough]]; + case ARDUINO_EVENT_ETH_DISCONNECTED: + mStatus = NetworkState::DISCONNECTED; + break; + + default: + break; + } + } + + void sortRSSI(int *sort, int n) { + for (int i = 0; i < n; i++) + sort[i] = i; + for (int i = 0; i < n; i++) + for (int j = i + 1; j < n; j++) + if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i])) + std::swap(sort[i], sort[j]); + } + + private: + void sendNTPpacket(void) { + uint8_t buf[NTP_PACKET_SIZE]; + memset(buf, 0, NTP_PACKET_SIZE); + + buf[0] = 0b11100011; // LI, Version, Mode + buf[1] = 0; // Stratum + buf[2] = 6; // Max Interval between messages in seconds + buf[3] = 0xEC; // Clock Precision + // bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset + buf[12] = 49; // four-byte reference ID identifying + buf[13] = 0x4E; + buf[14] = 49; + buf[15] = 52; + + mUdp.write(buf, NTP_PACKET_SIZE); + } + + void handleNTPPacket(AsyncUDPPacket packet) { + char buf[80]; + + memcpy(buf, packet.data(), sizeof(buf)); + + unsigned long highWord = word(buf[40], buf[41]); + unsigned long lowWord = word(buf[42], buf[43]); + + // combine the four bytes (two words) into a long integer + // this is NTP time (seconds since Jan 1 1900): + unsigned long secsSince1900 = highWord << 16 | lowWord; + + *mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time + DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC"); + mOnTimeCB(true); + mUdp.close(); + } + + protected: + enum class NetworkState : uint8_t { + DISCONNECTED, + CONNECTED, + GOT_IP, + SCAN_READY, // ESP8266 + CONNECTING // ESP8266 + }; + + protected: + settings_t *mConfig = nullptr; + uint32_t *mUtcTimestamp = nullptr; + bool mConnected = false; + bool mScanActive = false; + bool mWifiConnecting = false; + + OnNetworkCB mOnNetworkCB; + OnTimeCB mOnTimeCB; + + NetworkState mStatus = NetworkState::DISCONNECTED; + + AhoyWifiAp mAp; + DNSServer mDns; + + AsyncUDP mUdp; // for time server + #if defined(ESP8266) + WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler; + #endif +}; + +#endif /*__AHOY_NETWORK_H__*/ diff --git a/src/network/AhoyNetworkHelper.cpp b/src/network/AhoyNetworkHelper.cpp new file mode 100644 index 00000000..09678023 --- /dev/null +++ b/src/network/AhoyNetworkHelper.cpp @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#include "AhoyNetworkHelper.h" + +namespace ah { + void welcome(String ip, String info) { + DBGPRINTLN(F("\n\n-------------------")); + DBGPRINTLN(F("Welcome to AHOY!")); + DBGPRINT(F("\npoint your browser to http://")); + DBGPRINT(ip); + DBGPRINT(" ("); + DBGPRINT(info); + DBGPRINTLN(")"); + DBGPRINTLN(F("to configure your device")); + DBGPRINTLN(F("-------------------\n")); + } +} diff --git a/src/network/AhoyNetworkHelper.h b/src/network/AhoyNetworkHelper.h new file mode 100644 index 00000000..378ba033 --- /dev/null +++ b/src/network/AhoyNetworkHelper.h @@ -0,0 +1,39 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __AHOY_NETWORK_HELPER_H__ +#define __AHOY_NETWORK_HELPER_H__ + +#include "../utils/dbg.h" +#include <Arduino.h> +#if defined(ESP32) + #include "ESPAsyncWebServer.h" + #include <WiFiType.h> + #include <ESPmDNS.h> +#else + #include <ESP8266WiFi.h> + #include <ESP8266mDNS.h> + //#include <WiFiUdp.h> + #include "ESPAsyncUDP.h" + + enum { + SYSTEM_EVENT_STA_CONNECTED = 1, + ARDUINO_EVENT_ETH_CONNECTED, + SYSTEM_EVENT_STA_GOT_IP, + ARDUINO_EVENT_ETH_GOT_IP, + ARDUINO_EVENT_WIFI_STA_LOST_IP, + ARDUINO_EVENT_WIFI_STA_STOP, + SYSTEM_EVENT_STA_DISCONNECTED, + ARDUINO_EVENT_ETH_STOP, + ARDUINO_EVENT_ETH_DISCONNECTED + }; +#endif +#include <DNSServer.h> + +namespace ah { + void welcome(String ip, String info); +} + +#endif /*__AHOY_NETWORK_HELPER_H__*/ diff --git a/src/network/AhoyWifiAp.h b/src/network/AhoyWifiAp.h new file mode 100644 index 00000000..ed1ad3a3 --- /dev/null +++ b/src/network/AhoyWifiAp.h @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __AHOY_WIFI_AP_H__ +#define __AHOY_WIFI_AP_H__ + +#include "../utils/dbg.h" +#include <Arduino.h> +#include "../config/settings.h" +#include "AhoyNetworkHelper.h" + +class AhoyWifiAp { + public: + AhoyWifiAp() : mIp(192, 168, 4, 1) {} + + void setup(cfgSys_t *cfg) { + mCfg = cfg; + } + + void tickLoop() { + if(mEnabled) + mDns.processNextRequest(); + + if (WiFi.softAPgetStationNum() != mLast) { + mLast = WiFi.softAPgetStationNum(); + if(mLast > 0) + DBGPRINTLN(F("AP client connected")); + } + } + + void enable() { + if(mEnabled) + return; + + ah::welcome(mIp.toString(), String(F("Password: ") + String(mCfg->apPwd))); + + WiFi.mode(WIFI_AP_STA); + WiFi.softAPConfig(mIp, mIp, IPAddress(255, 255, 255, 0)); + WiFi.softAP(WIFI_AP_SSID, mCfg->apPwd); + + mDns.setErrorReplyCode(DNSReplyCode::NoError); + mDns.start(53, "*", mIp); + + mEnabled = true; + tickLoop(); + } + + void disable() { + if(!mEnabled) + return; + + if(WiFi.softAPgetStationNum() > 0) + return; + + mDns.stop(); + WiFi.softAPdisconnect(); + WiFi.mode(WIFI_STA); + + mEnabled = false; + } + + bool isEnabled() const { + return mEnabled; + } + + private: + cfgSys_t *mCfg = nullptr; + DNSServer mDns; + IPAddress mIp; + bool mEnabled = false; + uint8_t mLast = 0; +}; + +#endif /*__AHOY_WIFI_AP_H__*/ diff --git a/src/network/AhoyWifiEsp32.h b/src/network/AhoyWifiEsp32.h new file mode 100644 index 00000000..70017518 --- /dev/null +++ b/src/network/AhoyWifiEsp32.h @@ -0,0 +1,103 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __AHOY_WIFI_ESP32_H__ +#define __AHOY_WIFI_ESP32_H__ + +#if defined(ESP32) +#include <functional> +#include <AsyncUDP.h> +#include "AhoyNetwork.h" +#include "ESPAsyncWebServer.h" + +class AhoyWifi : public AhoyNetwork { + public: + virtual void begin() override { + mAp.enable(); + + if(strlen(mConfig->sys.stationSsid) == 0) + return; // no station wifi defined + + + WiFi.disconnect(); // clean up + WiFi.setHostname(mConfig->sys.deviceName); + #if !defined(AP_ONLY) + WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); + WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); + setStaticIp(); + WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, WIFI_ALL_CHANNEL_SCAN); + mWifiConnecting = true; + + DBGPRINT(F("connect to network '")); + DBGPRINT(mConfig->sys.stationSsid); + DBGPRINTLN(F("'")); + #endif + } + + void tickNetworkLoop() override { + if(mAp.isEnabled()) + mAp.tickLoop(); + } + + virtual String getIp(void) override { + return WiFi.localIP().toString(); + } + + virtual String getMac(void) override { + return WiFi.macAddress(); + } + + private: + virtual void OnEvent(WiFiEvent_t event) override { + switch(event) { + case SYSTEM_EVENT_STA_CONNECTED: + if(NetworkState::CONNECTED != mStatus) { + mStatus = NetworkState::CONNECTED; + mWifiConnecting = false; + DPRINTLN(DBG_INFO, F("Network connected")); + } + break; + + case SYSTEM_EVENT_STA_GOT_IP: + mStatus = NetworkState::GOT_IP; + if(mAp.isEnabled()) + mAp.disable(); + + if(!mConnected) { + mConnected = true; + ah::welcome(WiFi.localIP().toString(), F("Station")); + MDNS.begin(mConfig->sys.deviceName); + mOnNetworkCB(true); + } + break; + + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + [[fallthrough]]; + case ARDUINO_EVENT_WIFI_STA_STOP: + [[fallthrough]]; + case SYSTEM_EVENT_STA_DISCONNECTED: + mStatus = NetworkState::DISCONNECTED; + if(mConnected) { + mConnected = false; + mOnNetworkCB(false); + MDNS.end(); + begin(); + } + break; + + default: + break; + } + } + + virtual void setStaticIp() override { + setupIp([this](IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2) -> bool { + return WiFi.config(ip, gateway, mask, dns1, dns2); + }); + } +}; + +#endif /*ESP32*/ +#endif /*__AHOY_WIFI_ESP32_H__*/ diff --git a/src/network/AhoyWifiEsp8266.h b/src/network/AhoyWifiEsp8266.h new file mode 100644 index 00000000..c72f06b5 --- /dev/null +++ b/src/network/AhoyWifiEsp8266.h @@ -0,0 +1,170 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __AHOY_WIFI_ESP8266_H__ +#define __AHOY_WIFI_ESP8266_H__ + +#if defined(ESP8266) +#include <functional> +#include <list> +#include <WiFiUdp.h> +#include "AhoyNetwork.h" +#include "ESPAsyncWebServer.h" + +class AhoyWifi : public AhoyNetwork { + public: + void begin() override { + mAp.enable(); + + WiFi.setHostname(mConfig->sys.deviceName); + mBSSIDList.clear(); + } + + void tickNetworkLoop() override { + if(mAp.isEnabled()) + mAp.tickLoop(); + + mCnt++; + + switch(mStatus) { + case NetworkState::DISCONNECTED: + if(mConnected) { + mConnected = false; + mWifiConnecting = false; + mOnNetworkCB(false); + mAp.enable(); + MDNS.end(); + } + + if (WiFi.softAPgetStationNum() > 0) { + DBGPRINTLN(F("AP client connected")); + } + #if !defined(AP_ONLY) + else if (!mScanActive) { + DBGPRINT(F("scanning APs with SSID ")); + DBGPRINTLN(String(mConfig->sys.stationSsid)); + mScanCnt = 0; + mCnt = 0; + mScanActive = true; + WiFi.scanNetworks(true, true, 0U, ([this]() { + if (mConfig->sys.isHidden) + return (uint8_t*)NULL; + return (uint8_t*)(mConfig->sys.stationSsid); + })()); + } else if(getBSSIDs()) { + mStatus = NetworkState::SCAN_READY; + DBGPRINT(F("connect to network '")); Serial.flush(); + DBGPRINTLN(mConfig->sys.stationSsid); + } + #endif + break; + + case NetworkState::SCAN_READY: + mStatus = NetworkState::CONNECTING; + DBGPRINT(F("try to connect to BSSID:")); + uint8_t bssid[6]; + for (int j = 0; j < 6; j++) { + bssid[j] = mBSSIDList.front(); + mBSSIDList.pop_front(); + DBGPRINT(" " + String(bssid[j], HEX)); + } + DBGPRINTLN(""); + setStaticIp(); + WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]); + mWifiConnecting = true; + break; + + case NetworkState::CONNECTING: + if (isTimeout(TIMEOUT)) { + WiFi.disconnect(); + mWifiConnecting = false; + mStatus = mBSSIDList.empty() ? NetworkState::DISCONNECTED : NetworkState::SCAN_READY; + } + break; + + case NetworkState::CONNECTED: + break; + + case NetworkState::GOT_IP: + if(!mConnected) { + mAp.disable(); + mConnected = true; + ah::welcome(WiFi.localIP().toString(), F("Station")); + MDNS.begin(mConfig->sys.deviceName); + MDNSResponder::hMDNSService hRes = MDNS.addService(NULL, "http", "tcp", 80); + MDNS.addServiceTxt(hRes, "path", "/"); + MDNS.announce(); + mOnNetworkCB(true); + } + + MDNS.update(); + + if(WiFi.channel() > 11) + mWasInCh12to14 = true; + break; + } + } + + String getIp(void) override { + return WiFi.localIP().toString(); + } + + String getMac(void) override { + return WiFi.macAddress(); + } + + bool getWasInCh12to14() override { + return mWasInCh12to14; + } + + private: + void setStaticIp() override { + setupIp([this](IPAddress ip, IPAddress gateway, IPAddress mask, IPAddress dns1, IPAddress dns2) -> bool { + return WiFi.config(ip, gateway, mask, dns1, dns2); + }); + } + + bool getBSSIDs() { + bool result = false; + int n = WiFi.scanComplete(); + if (n < 0) { + if (++mScanCnt < 20) + return false; + } + if(n > 0) { + mBSSIDList.clear(); + int sort[n]; + sortRSSI(&sort[0], n); + for (int i = 0; i < n; i++) { + DBGPRINT("BSSID " + String(i) + ":"); + uint8_t *bssid = WiFi.BSSID(sort[i]); + for (int j = 0; j < 6; j++){ + DBGPRINT(" " + String(bssid[j], HEX)); + mBSSIDList.push_back(bssid[j]); + } + DBGPRINTLN(""); + } + result = true; + } + mScanActive = false; + WiFi.scanDelete(); + return result; + } + + bool isTimeout(uint8_t timeout) { + return ((mCnt % timeout) == 0); + } + + private: + uint8_t mCnt = 0; + uint8_t mScanCnt = 0; + std::list<uint8_t> mBSSIDList; + bool mWasInCh12to14 = false; + static constexpr uint8_t TIMEOUT = 20; + static constexpr uint8_t SCAN_TIMEOUT = 10; +}; + +#endif /*ESP8266*/ +#endif /*__AHOY_WIFI_ESP8266_H__*/ diff --git a/src/platformio.ini b/src/platformio.ini index f949aa37..0639d16f 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -23,16 +23,18 @@ extra_scripts = pre:../scripts/convertHtml.py pre:../scripts/applyPatches.py pre:../scripts/reduceGxEPD2.py + post:../scripts/add_littlefs_binary.py lib_deps = - https://github.com/yubox-node-org/ESPAsyncWebServer - https://github.com/nRF24/RF24 @ 1.4.8 + https://github.com/esphome/ESPAsyncWebServer @ ^3.2.2 + https://github.com/nRF24/RF24.git#v1.4.8 paulstoffregen/Time @ ^1.6.1 - https://github.com/bertmelis/espMqttClient#v1.6.0 - bblanchon/ArduinoJson @ ^6.21.3 + https://github.com/bertmelis/espMqttClient#v1.7.0 + bblanchon/ArduinoJson @ ^6.21.5 https://github.com/JChristensen/Timezone @ ^1.2.4 olikraus/U8g2 @ ^2.35.9 https://github.com/zinggjm/GxEPD2#1.5.3 + build_flags = -std=c++17 -std=gnu++17 @@ -40,67 +42,78 @@ build_unflags = -std=gnu++11 -[env:esp8266] +[env:esp8266-minimal] platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L +lib_deps = + ${env.lib_deps} + https://github.com/me-no-dev/ESPAsyncUDP build_flags = ${env.build_flags} -DEMC_MIN_FREE_MEMORY=4096 - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY ;-Wl,-Map,output.map monitor_filters = esp8266_exception_decoder +[env:esp8266] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L +lib_deps = ${env:esp8266-minimal.lib_deps} + https://github.com/me-no-dev/ESPAsyncUDP +build_flags = ${env:esp8266-minimal.build_flags} + -DENABLE_MQTT +monitor_filters = + esp8266_exception_decoder + [env:esp8266-de] platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L -build_flags = ${env.build_flags} - -DEMC_MIN_FREE_MEMORY=4096 +lib_deps = ${env:esp8266.lib_deps} +build_flags = ${env:esp8266.build_flags} -DLANG_DE - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY - ;-Wl,-Map,output.map monitor_filters = esp8266_exception_decoder -[env:esp8266-prometheus] +[env:esp8266-all] platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L -build_flags = ${env.build_flags} - -DEMC_MIN_FREE_MEMORY=4096 - -DENABLE_PROMETHEUS_EP - -DENABLE_MQTT +lib_deps = ${env:esp8266.lib_deps} +build_flags = ${env:esp8266.build_flags} -DPLUGIN_DISPLAY -DENABLE_HISTORY monitor_filters = esp8266_exception_decoder -[env:esp8266-prometheus-de] +[env:esp8266-all-de] platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L -build_flags = ${env.build_flags} - -DEMC_MIN_FREE_MEMORY=4096 - -DENABLE_PROMETHEUS_EP +lib_deps = ${env:esp8266.lib_deps} +build_flags = ${env:esp8266-all.build_flags} -DLANG_DE - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY monitor_filters = esp8266_exception_decoder -[env:esp8266-minimal] +[env:esp8266-prometheus] platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L -build_flags = ${env.build_flags} - -DEMC_MIN_FREE_MEMORY=4096 - ;-Wl,-Map,output.map +lib_deps = ${env:esp8266.lib_deps} +build_flags = ${env:esp8266-all.build_flags} + -DENABLE_PROMETHEUS_EP +monitor_filters = + esp8266_exception_decoder + +[env:esp8266-prometheus-de] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L +lib_deps = ${env:esp8266.lib_deps} +build_flags = ${env:esp8266-prometheus.build_flags} + -DLANG_DE monitor_filters = esp8266_exception_decoder @@ -111,6 +124,7 @@ platform = espressif8266 board = esp8285 board_build.ldscript = eagle.flash.1m64.ld board_build.f_cpu = 80000000L +lib_deps = ${env:esp8266.lib_deps} build_flags = ${env.build_flags} -DEMC_MIN_FREE_MEMORY=4096 -DENABLE_MQTT @@ -124,6 +138,7 @@ platform = espressif8266 board = esp8285 board_build.ldscript = eagle.flash.1m64.ld board_build.f_cpu = 80000000L +lib_deps = ${env:esp8266.lib_deps} build_flags = ${env.build_flags} -DEMC_MIN_FREE_MEMORY=4096 -DLANG_DE @@ -133,114 +148,73 @@ build_flags = ${env.build_flags} monitor_filters = esp8266_exception_decoder -[env:esp32-wroom32] -platform = espressif32@6.5.0 +[env:esp32-wroom32-minimal] +platform = espressif32@6.7.0 board = lolin_d32 build_flags = ${env.build_flags} - -DUSE_HSPI_FOR_EPD - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY + -DSPI_HAL monitor_filters = esp32_exception_decoder -[env:esp32-wroom32-minimal] -platform = espressif32@6.5.0 +[env:esp32-wroom32] +platform = espressif32@6.7.0 board = lolin_d32 -build_flags = ${env.build_flags} +build_flags = ${env:esp32-wroom32-minimal.build_flags} -DUSE_HSPI_FOR_EPD + -DENABLE_MQTT + -DPLUGIN_DISPLAY + -DENABLE_HISTORY + -DETHERNET + -DDEF_ETH_CS_PIN=15 + -DDEF_ETH_SCK_PIN=14 + -DDEF_ETH_MISO_PIN=12 + -DDEF_ETH_MOSI_PIN=13 + -DDEF_ETH_IRQ_PIN=4 + -DDEF_ETH_RST_PIN=255 + -DDEF_NRF_CS_PIN=5 + -DDEF_NRF_CE_PIN=17 + -DDEF_NRF_IRQ_PIN=16 + -DDEF_NRF_MISO_PIN=19 + -DDEF_NRF_MOSI_PIN=23 + -DDEF_NRF_SCLK_PIN=18 + -DDEF_CMT_CSB=27 + -DDEF_CMT_FCSB=26 + -DDEF_CMT_IRQ=34 + -DDEF_CMT_SDIO=14 + -DDEF_CMT_SCLK=12 monitor_filters = esp32_exception_decoder [env:esp32-wroom32-de] -platform = espressif32@6.5.0 +platform = espressif32@6.7.0 board = lolin_d32 -build_flags = ${env.build_flags} - -DUSE_HSPI_FOR_EPD - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY +build_flags = ${env:esp32-wroom32.build_flags} -DLANG_DE monitor_filters = esp32_exception_decoder [env:esp32-wroom32-prometheus] -platform = espressif32@6.5.0 +platform = espressif32@6.7.0 board = lolin_d32 -build_flags = ${env.build_flags} - -DUSE_HSPI_FOR_EPD +build_flags = ${env:esp32-wroom32.build_flags} -DENABLE_PROMETHEUS_EP - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY monitor_filters = esp32_exception_decoder [env:esp32-wroom32-prometheus-de] -platform = espressif32@6.5.0 +platform = espressif32@6.7.0 board = lolin_d32 -build_flags = ${env.build_flags} - -DUSE_HSPI_FOR_EPD - -DLANG_DE - -DENABLE_PROMETHEUS_EP - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY -monitor_filters = - esp32_exception_decoder - -[env:esp32-wroom32-ethernet] -platform = espressif32 -board = esp32dev -lib_deps = - khoih-prog/AsyncWebServer_ESP32_W5500 - khoih-prog/AsyncUDP_ESP32_W5500 - https://github.com/nRF24/RF24 @ ^1.4.8 - paulstoffregen/Time @ ^1.6.1 - https://github.com/bertmelis/espMqttClient#v1.6.0 - bblanchon/ArduinoJson @ ^6.21.3 - https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.35.9 - https://github.com/zinggjm/GxEPD2#1.5.3 -build_flags = ${env.build_flags} - -D ETHERNET - -DRELEASE - -DUSE_HSPI_FOR_EPD - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY -monitor_filters = - esp32_exception_decoder - -[env:esp32-wroom32-ethernet-de] -platform = espressif32 -board = esp32dev -lib_deps = - khoih-prog/AsyncWebServer_ESP32_W5500 - khoih-prog/AsyncUDP_ESP32_W5500 - https://github.com/nRF24/RF24 @ ^1.4.8 - paulstoffregen/Time @ ^1.6.1 - https://github.com/bertmelis/espMqttClient#v1.6.0 - bblanchon/ArduinoJson @ ^6.21.3 - https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.35.9 - https://github.com/zinggjm/GxEPD2#1.5.3 -build_flags = ${env.build_flags} - -D ETHERNET - -DRELEASE - -DUSE_HSPI_FOR_EPD +build_flags = ${env:esp32-wroom32-prometheus.build_flags} -DLANG_DE - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY monitor_filters = esp32_exception_decoder [env:esp32-s2-mini] -platform = espressif32@6.5.0 +platform = espressif32@6.7.0 board = lolin_s2_mini build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD + -DSPI_HAL -DENABLE_MQTT -DPLUGIN_DISPLAY -DENABLE_HISTORY @@ -259,33 +233,19 @@ monitor_filters = esp32_exception_decoder [env:esp32-s2-mini-de] -platform = espressif32@6.5.0 +platform = espressif32@6.7.0 board = lolin_s2_mini -build_flags = ${env.build_flags} - -DUSE_HSPI_FOR_EPD - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY - -DDEF_NRF_CS_PIN=12 - -DDEF_NRF_CE_PIN=3 - -DDEF_NRF_IRQ_PIN=5 - -DDEF_NRF_MISO_PIN=9 - -DDEF_NRF_MOSI_PIN=11 - -DDEF_NRF_SCLK_PIN=7 - -DDEF_CMT_CSB=16 - -DDEF_CMT_FCSB=18 - -DDEF_CMT_IRQ=33 - -DDEF_CMT_SDIO=35 - -DDEF_CMT_SCLK=37 +build_flags = ${env:esp32-s2-mini.build_flags} -DLANG_DE monitor_filters = esp32_exception_decoder [env:esp32-c3-mini] -platform = espressif32@6.5.0 +platform = espressif32@6.7.0 board = lolin_c3_mini build_flags = ${env.build_flags} -DUSE_HSPI_FOR_EPD + -DSPI_HAL -DENABLE_MQTT -DPLUGIN_DISPLAY -DENABLE_HISTORY @@ -304,36 +264,18 @@ monitor_filters = esp32_exception_decoder [env:esp32-c3-mini-de] -platform = espressif32@6.5.0 +platform = espressif32@6.7.0 board = lolin_c3_mini -build_flags = ${env.build_flags} - -DUSE_HSPI_FOR_EPD - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY - -DDEF_NRF_CS_PIN=5 - -DDEF_NRF_CE_PIN=0 - -DDEF_NRF_IRQ_PIN=1 - -DDEF_NRF_MISO_PIN=3 - -DDEF_NRF_MOSI_PIN=4 - -DDEF_NRF_SCLK_PIN=2 - -DDEF_CMT_CSB=255 - -DDEF_CMT_FCSB=255 - -DDEF_CMT_IRQ=255 - -DDEF_CMT_SDIO=255 - -DDEF_CMT_SCLK=255 +build_flags = ${env:esp32-c3-mini.build_flags} -DLANG_DE monitor_filters = esp32_exception_decoder -[env:opendtufusion] -platform = espressif32@6.5.0 +[env:opendtufusion-minimal] +platform = espressif32@6.7.0 board = esp32-s3-devkitc-1 upload_protocol = esp-builtin build_flags = ${env.build_flags} - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY -DSPI_HAL -DDEF_NRF_CS_PIN=37 -DDEF_NRF_CE_PIN=38 @@ -354,145 +296,48 @@ build_flags = ${env.build_flags} monitor_filters = esp32_exception_decoder, colorize -[env:opendtufusion-de] -platform = espressif32@6.5.0 +[env:opendtufusion] +platform = espressif32@6.7.0 board = esp32-s3-devkitc-1 upload_protocol = esp-builtin -build_flags = ${env.build_flags} - -DLANG_DE +build_flags = ${env:opendtufusion-minimal.build_flags} + -DETHERNET -DENABLE_MQTT -DPLUGIN_DISPLAY -DENABLE_HISTORY - -DSPI_HAL - -DDEF_NRF_CS_PIN=37 - -DDEF_NRF_CE_PIN=38 - -DDEF_NRF_IRQ_PIN=47 - -DDEF_NRF_MISO_PIN=48 - -DDEF_NRF_MOSI_PIN=35 - -DDEF_NRF_SCLK_PIN=36 - -DDEF_CMT_CSB=4 - -DDEF_CMT_FCSB=21 - -DDEF_CMT_IRQ=8 - -DDEF_CMT_SDIO=5 - -DDEF_CMT_SCLK=6 - -DDEF_LED0=18 - -DDEF_LED1=17 - -DLED_ACTIVE_HIGH - -DARDUINO_USB_MODE=1 + -DDEF_ETH_CS_PIN=42 + -DDEF_ETH_SCK_PIN=39 + -DDEF_ETH_MISO_PIN=41 + -DDEF_ETH_MOSI_PIN=40 + -DDEF_ETH_IRQ_PIN=44 + -DDEF_ETH_RST_PIN=43 monitor_filters = esp32_exception_decoder, colorize -[env:opendtufusion-minimal] -platform = espressif32@6.5.0 +[env:opendtufusion-de] +platform = espressif32@6.7.0 board = esp32-s3-devkitc-1 upload_protocol = esp-builtin -build_flags = ${env.build_flags} - -DSPI_HAL - -DDEF_NRF_CS_PIN=37 - -DDEF_NRF_CE_PIN=38 - -DDEF_NRF_IRQ_PIN=47 - -DDEF_NRF_MISO_PIN=48 - -DDEF_NRF_MOSI_PIN=35 - -DDEF_NRF_SCLK_PIN=36 - -DDEF_CMT_CSB=4 - -DDEF_CMT_FCSB=21 - -DDEF_CMT_IRQ=8 - -DDEF_CMT_SDIO=5 - -DDEF_CMT_SCLK=6 - -DDEF_LED0=18 - -DDEF_LED1=17 - -DLED_ACTIVE_HIGH - -DARDUINO_USB_MODE=1 +build_flags = ${env:opendtufusion.build_flags} + -DLANG_DE monitor_filters = esp32_exception_decoder, colorize -[env:opendtufusion-ethernet] -platform = espressif32@6.5.0 +[env:opendtufusion-16MB] +platform = espressif32@6.7.0 board = esp32-s3-devkitc-1 -lib_deps = - khoih-prog/AsyncWebServer_ESP32_W5500 - khoih-prog/AsyncUDP_ESP32_W5500 - https://github.com/nrf24/RF24 @ ^1.4.8 - paulstoffregen/Time @ ^1.6.1 - https://github.com/bertmelis/espMqttClient#v1.6.0 - bblanchon/ArduinoJson @ ^6.21.3 - https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.35.9 - https://github.com/zinggjm/GxEPD2#1.5.3 +board_upload.flash_size = 16MB +board_build.partitions = default_16MB.csv upload_protocol = esp-builtin -build_flags = ${env.build_flags} - -DETHERNET - -DSPI_HAL - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY - -DDEF_ETH_CS_PIN=42 - -DDEF_ETH_SCK_PIN=39 - -DDEF_ETH_MISO_PIN=41 - -DDEF_ETH_MOSI_PIN=40 - -DDEF_ETH_IRQ_PIN=44 - -DDEF_ETH_RST_PIN=43 - -DDEF_NRF_CS_PIN=37 - -DDEF_NRF_CE_PIN=38 - -DDEF_NRF_IRQ_PIN=47 - -DDEF_NRF_MISO_PIN=48 - -DDEF_NRF_MOSI_PIN=35 - -DDEF_NRF_SCLK_PIN=36 - -DDEF_CMT_CSB=4 - -DDEF_CMT_FCSB=21 - -DDEF_CMT_IRQ=8 - -DDEF_CMT_SDIO=5 - -DDEF_CMT_SCLK=6 - -DDEF_LED0=18 - -DDEF_LED1=17 - -DLED_ACTIVE_HIGH - -DARDUINO_USB_MODE=1 - #-DARDUINO_USB_CDC_ON_BOOT=1 +build_flags = ${env:opendtufusion.build_flags} monitor_filters = esp32_exception_decoder, colorize -[env:opendtufusion-ethernet-de] -platform = espressif32@6.5.0 +[env:opendtufusion-16MB-de] +platform = espressif32@6.7.0 board = esp32-s3-devkitc-1 -lib_deps = - khoih-prog/AsyncWebServer_ESP32_W5500 - khoih-prog/AsyncUDP_ESP32_W5500 - https://github.com/nrf24/RF24 @ ^1.4.8 - paulstoffregen/Time @ ^1.6.1 - https://github.com/bertmelis/espMqttClient#v1.6.0 - bblanchon/ArduinoJson @ ^6.21.3 - https://github.com/JChristensen/Timezone @ ^1.2.4 - olikraus/U8g2 @ ^2.35.9 - https://github.com/zinggjm/GxEPD2#1.5.3 upload_protocol = esp-builtin -build_flags = ${env.build_flags} - -DETHERNET - -DSPI_HAL +build_flags = ${env:opendtufusion-16MB.build_flags} -DLANG_DE - -DENABLE_MQTT - -DPLUGIN_DISPLAY - -DENABLE_HISTORY - -DDEF_ETH_CS_PIN=42 - -DDEF_ETH_SCK_PIN=39 - -DDEF_ETH_MISO_PIN=41 - -DDEF_ETH_MOSI_PIN=40 - -DDEF_ETH_IRQ_PIN=44 - -DDEF_ETH_RST_PIN=43 - -DDEF_NRF_CS_PIN=37 - -DDEF_NRF_CE_PIN=38 - -DDEF_NRF_IRQ_PIN=47 - -DDEF_NRF_MISO_PIN=48 - -DDEF_NRF_MOSI_PIN=35 - -DDEF_NRF_SCLK_PIN=36 - -DDEF_CMT_CSB=4 - -DDEF_CMT_FCSB=21 - -DDEF_CMT_IRQ=8 - -DDEF_CMT_SDIO=5 - -DDEF_CMT_SCLK=6 - -DDEF_LED0=18 - -DDEF_LED1=17 - -DLED_ACTIVE_HIGH - -DARDUINO_USB_MODE=1 - #-DARDUINO_USB_CDC_ON_BOOT=1 monitor_filters = esp32_exception_decoder, colorize diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index b2c88b05..4654a4f4 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -7,8 +7,9 @@ #include <U8g2lib.h> #include "../../hm/hmSystem.h" -#include "../../hm/hmRadio.h" +#include "../../hm/NrfRadio.h" #include "../../utils/helper.h" +#include "../plugin_lang.h" #include "Display_Mono.h" #include "Display_Mono_128X32.h" #include "Display_Mono_128X64.h" @@ -24,9 +25,9 @@ class Display { mMono = NULL; } - void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *hmradio, RADIO *hmsradio, uint32_t *utcTs) { + void setup(IApp *app, display_t *cfg, HMSYSTEM *sys, RADIO *nrfRadio, RADIO *hmsradio, uint32_t *utcTs) { mApp = app; - mHmRadio = hmradio; + mNrfRadio = nrfRadio; mHmsRadio = hmsradio; mCfg = cfg; mSys = sys; @@ -44,7 +45,7 @@ class Display { case DISP_TYPE_T4_SSD1306_128X32: mMono = new DisplayMono128X32(); break; // SSD1306_128X32 (0.91") case DISP_TYPE_T5_SSD1306_64X48: mMono = new DisplayMono64X48(); break; // SSD1306_64X48 (0.66" - Wemos OLED Shield) case DISP_TYPE_T6_SSD1309_128X64: mMono = new DisplayMono128X64(); break; // SSD1309_128X64 (2.42") - #if defined(ESP32) && !defined(ETHERNET) + #if defined(ESP32) case DISP_TYPE_T10_EPAPER: mMono = NULL; // ePaper does not use this mRefreshCycle = 0; @@ -71,6 +72,14 @@ class Display { } + void loop() { + #if defined(ESP32) + if ((nullptr != mCfg) && (DISP_TYPE_T10_EPAPER == mCfg->type)) { + mEpaper.refreshLoop(); + } + #endif + } + void payloadEventListener(uint8_t cmd) { mNewPayload = true; } @@ -78,16 +87,25 @@ class Display { void tickerSecond() { bool request_refresh = false; - if (mMono != NULL) + if (mMono != NULL) { + // maintain LCD and OLED displays with pixel shift screensavers, at least every 5 seconds request_refresh = mMono->loop(motionSensorActive()); - - if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) { - DataScreen(); - mNewPayload = false; - mLoopCnt = 0; + if (mNewPayload || (((++mLoopCnt) % 5) == 0) || request_refresh) { + DataScreen(); + mNewPayload = false; + mLoopCnt = 0; + } } - #if defined(ESP32) && !defined(ETHERNET) + #if defined(ESP32) + else if (DISP_TYPE_T10_EPAPER == mCfg->type) { + // maintain ePaper at least every 15 seconds + if (mNewPayload || (((++mLoopCnt) % 15) == 0)) { + DataScreen(); + mNewPayload = false; + mLoopCnt = 0; + } mEpaper.tickerSecond(); + } #endif } @@ -148,7 +166,7 @@ class Display { mDisplayData.totalYieldDay = totalYieldDay; mDisplayData.totalYieldTotal = totalYieldTotal; bool nrf_en = mApp->getNrfEnabled(); - bool nrf_ok = nrf_en && mHmRadio->isChipConnected(); + bool nrf_ok = nrf_en && mNrfRadio->isChipConnected(); #if defined(ESP32) bool cmt_en = mApp->getCmtEnabled(); bool cmt_ok = cmt_en && mHmsRadio->isChipConnected(); @@ -175,16 +193,18 @@ class Display { if (mMono ) { mMono->disp(); } - #if defined(ESP32) && !defined(ETHERNET) + #if defined(ESP32) else if (DISP_TYPE_T10_EPAPER == mCfg->type) { mEpaper.loop((totalPower), totalYieldDay, totalYieldTotal, nrprod); mRefreshCycle++; - } - if (mRefreshCycle > 480) { - mEpaper.fullRefresh(); - mRefreshCycle = 0; + if (mRefreshCycle > 2880) { // 15 * 2280 = 44300s = 12h + mEpaper.fullRefresh(); + mRefreshCycle = 0; + } + } + #endif } @@ -230,11 +250,11 @@ class Display { uint32_t *mUtcTs = nullptr; display_t *mCfg = nullptr; HMSYSTEM *mSys = nullptr; - RADIO *mHmRadio = nullptr; + RADIO *mNrfRadio = nullptr; RADIO *mHmsRadio = nullptr; uint16_t mRefreshCycle = 0; - #if defined(ESP32) && !defined(ETHERNET) + #if defined(ESP32) DisplayEPaper mEpaper; #endif DisplayMono *mMono = nullptr; diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h index 6262e3f6..def87507 100644 --- a/src/plugins/Display/Display_Mono_128X32.h +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -40,20 +40,20 @@ class DisplayMono128X32 : public DisplayMono { printText(mFmtText, 0); } else { - printText("offline", 0); + printText(STR_OFFLINE, 0); } - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", mDisplayData->totalYieldDay); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %4.0f Wh", STR_TODAY, mDisplayData->totalYieldDay); printText(mFmtText, 1); - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", mDisplayData->totalYieldTotal); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %.1f kWh", STR_TOTAL, mDisplayData->totalYieldTotal); printText(mFmtText, 2); IPAddress ip = WiFi.localIP(); if (!(mExtra % 10) && (ip)) printText(ip.toString().c_str(), 3); else if (!(mExtra % 5)) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", mDisplayData->nrProducing); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %d", STR_ACTIVE_INVERTERS, mDisplayData->nrProducing); printText(mFmtText, 3); } else if (0 != mDisplayData->utcTs) printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3); diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index c63f0b22..c93c5c1a 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -93,7 +93,7 @@ class DisplayMono128X64 : public DisplayMono { // print Date and time if (0 != mDisplayData->utcTs) - printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff); + printText(ah::getDateTimeStrShort_i18n(mDisplayData->utcTs).c_str(), l_Time, 0xff); if (showLine(l_Status)) { // alternatively: @@ -108,7 +108,7 @@ class DisplayMono128X64 : public DisplayMono { int8_t moon_pos = -1; setLineFont(l_Status); if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, STR_NO_INVERTER); else if (0 == mDisplayData->nrSleeping) { snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); sun_pos = 0; @@ -145,7 +145,7 @@ class DisplayMono128X64 : public DisplayMono { printText(mFmtText, l_TotalPower, 0xff); } else { - printText("offline", l_TotalPower, 0xff); + printText(STR_OFFLINE, l_TotalPower, 0xff); } } diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h index 7f98cae5..799d787e 100644 --- a/src/plugins/Display/Display_Mono_64X48.h +++ b/src/plugins/Display/Display_Mono_64X48.h @@ -42,7 +42,7 @@ class DisplayMono64X48 : public DisplayMono { printText(mFmtText, 0); } else { - printText("offline", 0); + printText(STR_OFFLINE, 0); } snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", mDisplayData->totalYieldDay); @@ -55,7 +55,7 @@ class DisplayMono64X48 : public DisplayMono { if (!(mExtra % 10) && (ip)) printText(ip.toString().c_str(), 3); else if (!(mExtra % 5)) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", mDisplayData->nrProducing); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %d", STR_ACTIVE_INVERTERS, mDisplayData->nrProducing); printText(mFmtText, 3); } else if (0 != mDisplayData->utcTs) printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3); diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index b5daacd5..ccbc8083 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -78,7 +78,7 @@ class DisplayMono84X48 : public DisplayMono { // print Date and time if (0 != mDisplayData->utcTs) - printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff); + printText(ah::getDateTimeStrShort_i18n(mDisplayData->utcTs).c_str(), l_Time, 0xff); if (showLine(l_Status)) { // alternatively: @@ -90,7 +90,7 @@ class DisplayMono84X48 : public DisplayMono { // print status of inverters else { if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, STR_NO_INVERTER); else if (0 == mDisplayData->nrSleeping) snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol else if (0 == mDisplayData->nrProducing) @@ -110,9 +110,8 @@ class DisplayMono84X48 : public DisplayMono { snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); printText(mFmtText, l_TotalPower, 0xff); - } else { - printText("offline", l_TotalPower, 0xff); - } + } else + printText(STR_OFFLINE, l_TotalPower, 0xff); } if (showLine(l_YieldDay)) { diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp index 087d784b..654627d2 100644 --- a/src/plugins/Display/Display_ePaper.cpp +++ b/src/plugins/Display/Display_ePaper.cpp @@ -1,15 +1,11 @@ #include "Display_ePaper.h" -#ifdef ESP8266 -#include <ESP8266WiFi.h> -#elif defined(ESP32) +#if defined(ESP32) #include <WiFi.h> -#endif #include "../../utils/helper.h" #include "imagedata.h" #include "defines.h" - -#if defined(ESP32) +#include "../plugin_lang.h" static const uint32_t spiClk = 4000000; // 4 MHz @@ -30,20 +26,26 @@ void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, u mRefreshState = RefreshStatus::LOGO; mSecondCnt = 0; + mLogoDisplayed = false; if (DISP_TYPE_T10_EPAPER == type) { - Serial.begin(115200); - _display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); - -#if defined(ESP32) && defined(USE_HSPI_FOR_EPD) - hspi.begin(_SCK, _BUSY, _MOSI, _CS); - _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0)); -#elif defined(ESP32) - _display->epd2.init(_SCK, _MOSI, 115200, true, 20, false); -#endif - _display->init(115200, true, 20, false); + #if defined(SPI_HAL) + hal.init(_MOSI, _DC, _SCK, _CS, _RST, _BUSY); + _display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(&hal)); + #else + _display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY)); + #if defined(USE_HSPI_FOR_EPD) + hspi.begin(_SCK, _BUSY, _MOSI, _CS); + _display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0)); + #elif defined(PLUGIN_DISPLAY) + _display->epd2.init(_SCK, _MOSI, 115200, true, 20, false); + #endif + #endif + _display->init(0, true, 20, false); _display->setRotation(mDisplayRotation); _display->setFullWindow(); + _display->setTextColor(GxEPD_BLACK); + _display->firstPage(); _version = version; } } @@ -57,7 +59,8 @@ void DisplayEPaper::config(uint8_t rotation, bool enPowerSave) { void DisplayEPaper::fullRefresh() { if(RefreshStatus::DONE != mRefreshState) return; - mSecondCnt = 2; + if(mLogoDisplayed) + return; // no refresh during logo display mRefreshState = RefreshStatus::BLACK; } @@ -65,40 +68,42 @@ void DisplayEPaper::fullRefresh() { void DisplayEPaper::refreshLoop() { switch(mRefreshState) { case RefreshStatus::LOGO: - _display->fillScreen(GxEPD_BLACK); - _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); - mSecondCnt = 2; - mNextRefreshState = RefreshStatus::PARTITIALS; - mRefreshState = RefreshStatus::WAIT; + _display->fillScreen(GxEPD_WHITE); + _display->drawInvertedBitmap(0, 0, logo, 200, 200, GxEPD_BLACK); + if(_display->nextPage()) + break; + mSecondCnt = 10; + _display->powerOff(); + mRefreshState = RefreshStatus::LOGO_WAIT; + break; + + case RefreshStatus::LOGO_WAIT: + if(0 != mSecondCnt) + break; + mRefreshState = RefreshStatus::WHITE; + _display->firstPage(); break; case RefreshStatus::BLACK: _display->fillScreen(GxEPD_BLACK); - mNextRefreshState = RefreshStatus::WHITE; - mRefreshState = RefreshStatus::WAIT; + if(_display->nextPage()) + break; + mRefreshState = RefreshStatus::WHITE; + _display->firstPage(); break; case RefreshStatus::WHITE: - if(0 != mSecondCnt) - break; _display->fillScreen(GxEPD_WHITE); - mNextRefreshState = RefreshStatus::PARTITIALS; - mRefreshState = RefreshStatus::WAIT; - break; - - case RefreshStatus::WAIT: - if(!_display->nextPage()) - mRefreshState = mNextRefreshState; + if(_display->nextPage()) + break; + mRefreshState = RefreshStatus::PARTITIALS; + _display->firstPage(); break; case RefreshStatus::PARTITIALS: - if(0 != mSecondCnt) - break; headlineIP(); versionFooter(); - mSecondCnt = 4; // display Logo time during boot up - mNextRefreshState = RefreshStatus::DONE; - mRefreshState = RefreshStatus::WAIT; + mRefreshState = RefreshStatus::DONE; break; default: // RefreshStatus::DONE @@ -120,7 +125,7 @@ void DisplayEPaper::headlineIP() { if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) { snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str()); } else { - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "WiFi not connected"); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_NO_WIFI); } _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((_display->width() - tbw) / 2) - tbx; @@ -162,7 +167,7 @@ void DisplayEPaper::versionFooter() { _display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding); _display->fillScreen(GxEPD_BLACK); do { - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "Version: %s", _version); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s: %s", STR_VERSION, _version); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((_display->width() - tbw) / 2) - tbx; @@ -183,7 +188,7 @@ void DisplayEPaper::offlineFooter() { _display->fillScreen(GxEPD_BLACK); do { if (NULL != mUtcTs) { - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline"); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_OFFLINE); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((_display->width() - tbw) / 2) - tbx; @@ -213,12 +218,17 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.0f W", totalPower); _changed = true; } else - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline"); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_OFFLINE); if ((totalPower == 0) && (mEnPowerSave)) { _display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK); _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); + mLogoDisplayed = true; } else { + if(mLogoDisplayed) { + mLogoDisplayed = false; + fullRefresh(); + } _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); x = ((_display->width() - tbw) / 2) - tbx; _display->setCursor(x, mHeadFootPadding + tbh + 10); @@ -268,7 +278,7 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa // Inverter online _display->setFont(&FreeSans12pt7b); y = _display->height() - (mHeadFootPadding + 10); - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d online", isprod); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d %s", isprod, STR_ONLINE); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK); x = ((_display->width() - tbw - 20) / 2) - tbx; @@ -305,8 +315,9 @@ void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYield //*************************************************************************** void DisplayEPaper::tickerSecond() { - if(mSecondCnt != 0) - mSecondCnt--; - refreshLoop(); + if(RefreshStatus::LOGO_WAIT == mRefreshState) { + if(mSecondCnt > 0) + mSecondCnt--; + } } #endif // ESP32 diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h index c26d3b42..2106c492 100644 --- a/src/plugins/Display/Display_ePaper.h +++ b/src/plugins/Display/Display_ePaper.h @@ -12,7 +12,11 @@ #define EPAPER_MAX_TEXT_LEN 35 #include <GxEPD2_BW.h> +#if defined(SPI_HAL) +#include "epdHal.h" +#else #include <SPI.h> +#endif // FreeFonts from Adafruit_GFX #include <Fonts/FreeSans12pt7b.h> @@ -44,9 +48,9 @@ class DisplayEPaper { DONE, BLACK, WHITE, - WAIT, PARTITIALS, - LOGO + LOGO, + LOGO_WAIT }; uint8_t mDisplayRotation; @@ -58,8 +62,13 @@ class DisplayEPaper { uint32_t* mUtcTs; bool mEnPowerSave; const char* _version; - RefreshStatus mRefreshState, mNextRefreshState; + RefreshStatus mRefreshState; + uint8_t mSecondCnt; + bool mLogoDisplayed; + #if defined(SPI_HAL) + epdHal hal; + #endif }; #endif // ESP32 diff --git a/src/plugins/Display/epdHal.h b/src/plugins/Display/epdHal.h new file mode 100644 index 00000000..1718b838 --- /dev/null +++ b/src/plugins/Display/epdHal.h @@ -0,0 +1,304 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __EPD_HAL_H__ +#define __EPD_HAL_H__ + +#pragma once +#include "../../utils/spiPatcher.h" +#include <esp_rom_gpio.h> +#include <GxEPD2_BW.h> + + +#define EPD_DEFAULT_SPI_SPEED 4000000 // 4 MHz + +class epdHal: public GxEPD2_HalInterface, public SpiPatcherHandle { + public: + epdHal() {} + + void patch() override { + esp_rom_gpio_connect_out_signal(mPinMosi, spi_periph_signal[mHostDevice].spid_out, false, false); + esp_rom_gpio_connect_in_signal(mPinBusy, spi_periph_signal[mHostDevice].spiq_in, false); + esp_rom_gpio_connect_out_signal(mPinClk, spi_periph_signal[mHostDevice].spiclk_out, false, false); + } + + void unpatch() override { + esp_rom_gpio_connect_out_signal(mPinMosi, SIG_GPIO_OUT_IDX, false, false); + esp_rom_gpio_connect_in_signal(mPinBusy, GPIO_MATRIX_CONST_ZERO_INPUT, false); + esp_rom_gpio_connect_out_signal(mPinClk, SIG_GPIO_OUT_IDX, false, false); + } + + void init(int8_t mosi, int8_t dc, int8_t sclk, int8_t cs, int8_t rst, int8_t busy, int32_t speed = EPD_DEFAULT_SPI_SPEED) { + mPinMosi = static_cast<gpio_num_t>(mosi); + mPinDc = static_cast<gpio_num_t>(dc); + mPinClk = static_cast<gpio_num_t>(sclk); + mPinCs = static_cast<gpio_num_t>(cs); + mPinRst = static_cast<gpio_num_t>(rst); + mPinBusy = static_cast<gpio_num_t>(busy); + mSpiSpeed = speed; + + #if defined(CONFIG_IDF_TARGET_ESP32S3) + mHostDevice = SPI3_HOST; + #else + mHostDevice = (14 == sclk) ? SPI2_HOST : SPI_HOST_OTHER; + #endif + + mSpiPatcher = SpiPatcher::getInstance(mHostDevice); + + gpio_reset_pin(mPinMosi); + gpio_set_direction(mPinMosi, GPIO_MODE_OUTPUT); + gpio_set_level(mPinMosi, 1); + + gpio_reset_pin(mPinClk); + gpio_set_direction(mPinClk, GPIO_MODE_OUTPUT); + gpio_set_level(mPinClk, 0); + + gpio_reset_pin(mPinCs); + spi_device_interface_config_t devcfg = { + .command_bits = 0, + .address_bits = 0, + .dummy_bits = 0, + .mode = 0, + .duty_cycle_pos = 0, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .clock_speed_hz = mSpiSpeed, + .input_delay_ns = 0, + .spics_io_num = mPinCs, + .flags = 0, + .queue_size = 1, + .pre_cb = nullptr, + .post_cb = nullptr + }; + mSpiPatcher->addDevice(mHostDevice, &devcfg, &spi); + + if(GPIO_NUM_NC != mPinRst) { + gpio_reset_pin(mPinRst); + gpio_set_direction(mPinRst, GPIO_MODE_OUTPUT); + gpio_set_level(mPinRst, HIGH); + } + + gpio_reset_pin(mPinDc); + gpio_set_direction(mPinDc, GPIO_MODE_OUTPUT); + gpio_set_level(mPinDc, HIGH); + + //gpio_reset_pin(mPinBusy); + //gpio_set_direction(mPinBusy, GPIO_MODE_INPUT); + } + + void rstMode(uint8_t mode) override { + if(GPIO_NUM_NC != mPinRst) + gpio_set_direction(mPinRst, static_cast<gpio_mode_t>(mode)); + } + + void rst(bool level) override { + if(GPIO_NUM_NC != mPinRst) + gpio_set_level(mPinRst, level); + } + + int getBusy(void) override { + return gpio_get_level(mPinBusy); + } + + bool isRst(void) override { + return (GPIO_NUM_NC != mPinRst); + } + + void write(uint8_t buf) override { + uint8_t data[1]; + data[0] = buf; + request_spi(); + + size_t spiLen = static_cast<size_t>(1u) << 3; + spi_transaction_t t = { + .flags = 0, + .cmd = 0, + .addr = 0, + .length = spiLen, + .rxlength = spiLen, + .user = NULL, + .tx_buffer = data, + .rx_buffer = data + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t)); + + release_spi(); + } + + void write(const uint8_t *buf, uint16_t n) override { + uint8_t data[n]; + std::copy(&buf[0], &buf[n], &data[0]); + + request_spi(); + + size_t spiLen = static_cast<size_t>(n) << 3; + spi_transaction_t t = { + .flags = 0, + .cmd = 0, + .addr = 0, + .length = spiLen, + .rxlength = spiLen, + .user = NULL, + .tx_buffer = data, + .rx_buffer = data + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t)); + + release_spi(); + } + + void write(const uint8_t *buf, uint16_t n, int16_t fill_with_zeroes) override { + uint8_t data[n + fill_with_zeroes]; + memset(data, 0, (n + fill_with_zeroes)); + for (uint16_t i = 0; i < n; i++) { + data[i] = pgm_read_byte(&*buf++); + } + + request_spi(); + spi_transaction_t t = { + .flags = SPI_TRANS_CS_KEEP_ACTIVE, + .cmd = 0, + .addr = 0, + .length = 1u, + .rxlength = 1u, + .user = NULL, + .tx_buffer = data, + .rx_buffer = data + }; + + size_t offs = 0; + spi_device_acquire_bus(spi, portMAX_DELAY); + while(offs < (n + fill_with_zeroes)) { + t.length = (64u << 3); + t.rxlength = t.length; + t.tx_buffer = &data[offs]; + offs += 64; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t)); + } + spi_device_release_bus(spi); + + release_spi(); + } + + void writeCmd(const uint8_t val) override { + uint8_t data[1]; + data[0] = val; + + request_spi(); + gpio_set_level(mPinDc, LOW); + + size_t spiLen = static_cast<size_t>(1u) << 3; + spi_transaction_t t = { + .flags = 0, + .cmd = 0, + .addr = 0, + .length = spiLen, + .rxlength = spiLen, + .user = NULL, + .tx_buffer = data, + .rx_buffer = data + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t)); + gpio_set_level(mPinDc, HIGH); + + release_spi(); + } + + void writeCmd(const uint8_t *buf, uint8_t n, bool isPGM) override { + uint8_t data[n-1]; + data[0] = (isPGM) ? pgm_read_byte(&*buf++) : buf[0]; + + request_spi(); + gpio_set_level(mPinDc, LOW); + spi_device_acquire_bus(spi, portMAX_DELAY); + + size_t spiLen = static_cast<size_t>(1u) << 3; + spi_transaction_t t = { + .flags = SPI_TRANS_CS_KEEP_ACTIVE, + .cmd = 0, + .addr = 0, + .length = spiLen, + .rxlength = spiLen, + .user = NULL, + .tx_buffer = data, + .rx_buffer = data + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t)); + gpio_set_level(mPinDc, HIGH); + + if(isPGM) { + for (uint16_t i = 0; i < n; i++) { + data[i] = pgm_read_byte(&*buf++); + } + } else + std::copy(&buf[1], &buf[n], &data[0]); + + spiLen = static_cast<size_t>(n-1) << 3; + spi_transaction_t t1 = { + .flags = SPI_TRANS_CS_KEEP_ACTIVE, + .cmd = 0, + .addr = 0, + .length = spiLen, + .rxlength = spiLen, + .user = NULL, + .tx_buffer = data, + .rx_buffer = data + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t1)); + spi_device_release_bus(spi); + + release_spi(); + } + + void startTransfer(void) override { + request_spi(); + } + + void endTransfer(void) override { + release_spi(); + } + + void transfer(const uint8_t val) override { + uint8_t data[1]; + data[0] = val; + + size_t spiLen = static_cast<size_t>(1u) << 3; + spi_transaction_t t = { + .flags = 0, + .cmd = 0, + .addr = 0, + .length = spiLen, + .rxlength = spiLen, + .user = NULL, + .tx_buffer = data, + .rx_buffer = data + }; + ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &t)); + } + + private: + inline void request_spi() { + mSpiPatcher->request(this); + } + + inline void release_spi() { + mSpiPatcher->release(); + } + + private: + gpio_num_t mPinMosi = GPIO_NUM_NC; + gpio_num_t mPinDc = GPIO_NUM_NC; + gpio_num_t mPinClk = GPIO_NUM_NC; + gpio_num_t mPinCs = GPIO_NUM_NC; + gpio_num_t mPinRst = GPIO_NUM_NC; + gpio_num_t mPinBusy = GPIO_NUM_NC; + int32_t mSpiSpeed = EPD_DEFAULT_SPI_SPEED; + + spi_host_device_t mHostDevice; + spi_device_handle_t spi; + SpiPatcher *mSpiPatcher; +}; + +#endif /*__EPD_HAL_H__*/ diff --git a/src/plugins/MaxPower.h b/src/plugins/MaxPower.h new file mode 100644 index 00000000..68665d61 --- /dev/null +++ b/src/plugins/MaxPower.h @@ -0,0 +1,67 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://github.com/lumpapu/ahoy +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __MAX_VALUE__ +#define __MAX_VALUE__ +#pragma once + +#include <array> +#include <utility> +#include "../hm/hmDefines.h" + +template<class T=float> +class MaxPower { + public: + MaxPower() { + mTs = nullptr; + mMaxDiff = 60; + reset(); + } + + void setup(uint32_t *ts, uint16_t interval) { + mTs = ts; + mMaxDiff = interval * 4; + } + + void reset(void) { + mValues.fill(std::make_pair(0, 0.0)); + mLast = 0.0; + } + + void payloadEvent(uint8_t cmd, Inverter<> *iv) { + if(RealTimeRunData_Debug != cmd) + return; + + if(nullptr == iv) + return; + + if(iv->id >= MAX_NUM_INVERTERS) + return; + + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + mValues[iv->id] = std::make_pair(*mTs, iv->getChannelFieldValue(CH0, FLD_PAC, rec)); + } + + T getTotalMaxPower(void) { + T val = 0; + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { + if((mValues[i].first + mMaxDiff) >= *mTs) + val += mValues[i].second; + else if(mValues[i].first > 0) + break; // old data + } + if(val > mLast) + mLast = val; + return mLast; + } + + private: + uint32_t *mTs; + uint32_t mMaxDiff; + float mLast; + std::array<std::pair<uint32_t, T>, MAX_NUM_INVERTERS> mValues; +}; + +#endif diff --git a/src/plugins/history.h b/src/plugins/history.h index 5076e295..bffbc6d5 100644 --- a/src/plugins/history.h +++ b/src/plugins/history.h @@ -17,6 +17,7 @@ enum class HistoryStorageType : uint8_t { POWER, + POWER_DAY, YIELD }; @@ -27,12 +28,14 @@ class HistoryData { uint16_t refreshCycle = 0; uint16_t loopCnt = 0; uint16_t listIdx = 0; // index for next Element to write into WattArr - uint16_t dispIdx = 0; // index for 1st Element to display from WattArr - bool wrapped = false; // ring buffer for watt history std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data; - storage_t() { data.fill(0); } + void reset() { + loopCnt = 0; + listIdx = 0; + data.fill(0); + } }; public: @@ -43,33 +46,52 @@ class HistoryData { mTs = ts; mCurPwr.refreshCycle = mConfig->inst.sendInterval; - //mYieldDay.refreshCycle = 60; + mCurPwrDay.refreshCycle = mConfig->inst.sendInterval; + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + mYieldDay.refreshCycle = 60; + #endif + mLastValueTs = 0; + mPgPeriod=0; + mMaximumDay = 0; } void tickerSecond() { - ; float curPwr = 0; - float maxPwr = 0; float yldDay = -0.1; + uint32_t ts = 0; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { Inverter<> *iv = mSys->getInverterByPos(i); - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); if (iv == NULL) continue; + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec); - maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec); yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + if (rec->ts > ts) + ts = rec->ts; } if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) { mCurPwr.loopCnt = 0; - if (curPwr > 0) + if (curPwr > 0) { + mLastValueTs = ts; addValue(&mCurPwr, roundf(curPwr)); - if (maxPwr > 0) - mMaximumDay = roundf(maxPwr); + if (curPwr > mMaximumDay) + mMaximumDay = roundf(curPwr); + } } - /*if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { + if ((++mCurPwrDay.loopCnt % mCurPwrDay.refreshCycle) == 0) { + mCurPwrDay.loopCnt = 0; + if (curPwr > 0) { + mLastValueTs = ts; + addValueDay(&mCurPwrDay, roundf(curPwr)); + } + } + + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { + mYieldDay.loopCnt = 0; if (*mTs > mApp->getSunset()) { if ((!mDayStored) && (yldDay > 0)) { addValue(&mYieldDay, roundf(yldDay)); @@ -77,28 +99,172 @@ class HistoryData { } } else if (*mTs > mApp->getSunrise()) mDayStored = false; - }*/ + } + #endif } uint16_t valueAt(HistoryStorageType type, uint16_t i) { - //storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; - storage_t *s = &mCurPwr; - uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH; - return s->data[idx]; + storage_t *s = nullptr; + uint16_t idx=i; + DPRINTLN(DBG_VERBOSE, F("valueAt ") + String((uint8_t)type) + " i=" + String(i)); + + switch (type) { + default: + [[fallthrough]]; + case HistoryStorageType::POWER: + s = &mCurPwr; + idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH; + break; + case HistoryStorageType::POWER_DAY: + s = &mCurPwrDay; + break; + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + case HistoryStorageType::YIELD: + s = &mYieldDay; + idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH; + break; + #endif + } + + return (nullptr == s) ? 0 : s->data[idx]; } uint16_t getMaximumDay() { return mMaximumDay; } + uint32_t getLastValueTs(HistoryStorageType type) { + DPRINTLN(DBG_VERBOSE, F("getLastValueTs ") + String((uint8_t)type)); + if (type == HistoryStorageType::POWER_DAY) + return mPgEndTime; + return mLastValueTs; + } + + uint32_t getPeriod(HistoryStorageType type) { + DPRINTLN(DBG_VERBOSE, F("getPeriode ") + String((uint8_t)type)); + switch (type) { + case HistoryStorageType::POWER: + return mCurPwr.refreshCycle; + break; + case HistoryStorageType::POWER_DAY: + return mPgPeriod / HISTORY_DATA_ARR_LENGTH; + break; + case HistoryStorageType::YIELD: + return (60 * 60 * 24); // 1 day + break; + } + return 0; + } + + bool isDataValid(void) { + return ((0 != mPgStartTime) && (0 != mPgEndTime)); + } + + #if defined(ENABLE_HISTORY_LOAD_DATA) + void addValue(HistoryStorageType historyType, uint8_t valueType, uint32_t value) { + if (valueType < 2) { + storage_t *s = NULL; + switch (historyType) { + default: + [[fallthrough]]; + case HistoryStorageType::POWER: + s = &mCurPwr; + break; + case HistoryStorageType::POWER_DAY: + s = &mCurPwrDay; + break; + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + case HistoryStorageType::YIELD: + s = &mYieldDay; + break; + #endif + } + if (s) { + if (0 == valueType) + addValue(s, value); + else { + if (historyType == HistoryStorageType::POWER) + s->refreshCycle = value; + if (historyType == HistoryStorageType::POWER_DAY) + mPgPeriod = value * HISTORY_DATA_ARR_LENGTH; + } + } + return; + } + if (2 == valueType) { + if (historyType == HistoryStorageType::POWER) + mLastValueTs = value; + if (historyType == HistoryStorageType::POWER_DAY) + mPgEndTime = value; + } + } + #endif + private: void addValue(storage_t *s, uint16_t value) { - if (s->wrapped) // after 1st time array wrap we have to increase the display index - s->dispIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); s->data[s->listIdx] = value; s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); - if (s->listIdx == 0) - s->wrapped = true; + } + + void addValueDay(storage_t *s, uint16_t value) { + DPRINTLN(DBG_VERBOSE, F("addValueDay ") + String(value)); + bool storeStartEndTimes = false; + bool store_entry = false; + uint32_t pGraphStartTime = mApp->getSunrise(); + uint32_t pGraphEndTime = mApp->getSunset(); + uint32_t utcTs = mApp->getTimestamp(); + switch (mPgState) { + case PowerGraphState::NO_TIME_SYNC: + if ((pGraphStartTime > 0) + && (pGraphEndTime > 0) // wait until period data is available ... + && (utcTs >= pGraphStartTime) + && (utcTs < pGraphEndTime)) // and current time is in period + { + storeStartEndTimes = true; // period was received -> store + store_entry = true; + mPgState = PowerGraphState::IN_PERIOD; + } + break; + case PowerGraphState::IN_PERIOD: + if (utcTs > mPgEndTime) // check if end of day is reached ... + mPgState = PowerGraphState::WAIT_4_NEW_PERIOD; // then wait for new period setting + else + store_entry = true; + break; + case PowerGraphState::WAIT_4_NEW_PERIOD: + if ((mPgStartTime != pGraphStartTime) || (mPgEndTime != pGraphEndTime)) { // wait until new time period was received ... + storeStartEndTimes = true; // and store it for next period + mPgState = PowerGraphState::WAIT_4_RESTART; + } + break; + case PowerGraphState::WAIT_4_RESTART: + if ((utcTs >= mPgStartTime) && (utcTs < mPgEndTime)) { // wait until current time is in period again ... + mCurPwrDay.reset(); // then reset power graph data + store_entry = true; + mPgState = PowerGraphState::IN_PERIOD; + mCurPwr.reset(); // also reset "last values" graph + mMaximumDay = 0; // and the maximum of the (last) day + } + break; + } + + // store start and end times of current time period and calculate period length + if (storeStartEndTimes) { + mPgStartTime = pGraphStartTime; + mPgEndTime = pGraphEndTime; + mPgPeriod = pGraphEndTime - pGraphStartTime; // time period of power graph in sec for scaling of x-axis + } + + if (store_entry) { + DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry") + String(value)); + if (mPgPeriod) { + uint16_t pgPos = (utcTs - mPgStartTime) * (HISTORY_DATA_ARR_LENGTH - 1) / mPgPeriod; + s->listIdx = std::min(pgPos, (uint16_t)(HISTORY_DATA_ARR_LENGTH - 1)); + } else + s->listIdx = 0; + DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry idx=") + String(s->listIdx)); + s->data[s->listIdx] = std::max(s->data[s->listIdx], value); // update current datapoint to maximum of all seen values + } } private: @@ -109,8 +275,23 @@ class HistoryData { uint32_t *mTs = nullptr; storage_t mCurPwr; + storage_t mCurPwrDay; + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + storage_t mYieldDay; + #endif bool mDayStored = false; uint16_t mMaximumDay = 0; + uint32_t mLastValueTs = 0; + enum class PowerGraphState { + NO_TIME_SYNC, + IN_PERIOD, + WAIT_4_NEW_PERIOD, + WAIT_4_RESTART + }; + PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC; + uint32_t mPgStartTime = 0; + uint32_t mPgEndTime = 0; + uint32_t mPgPeriod = 0; // seconds }; #endif /*ENABLE_HISTORY*/ diff --git a/src/plugins/plugin_lang.h b/src/plugins/plugin_lang.h new file mode 100644 index 00000000..8d7a987f --- /dev/null +++ b/src/plugins/plugin_lang.h @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __PLUGIN_LANG_H__ +#define __PLUGIN_LANG_H__ + +#ifdef LANG_DE + #define STR_MONTHNAME_3_CHAR_LIST "ErrJanFebMrzAprMaiJunJulAugSepOktNovDez" + #define STR_DAYNAME_3_CHAR_LIST "ErrSonMonDieMitDonFreSam" + #define STR_OFFLINE "aus" + #define STR_ONLINE "aktiv" + #define STR_NO_INVERTER "kein inverter" + #define STR_NO_WIFI "WLAN nicht verbunden" + #define STR_VERSION "Version" + #define STR_ACTIVE_INVERTERS "aktive WR" + #define STR_TODAY "heute" + #define STR_TOTAL "Gesamt" +#elif LANG_FR + #define STR_MONTHNAME_3_CHAR_LIST "ErrJanFevMarAvrMaiJunJulAouSepOctNovDec" + #define STR_DAYNAME_3_CHAR_LIST "ErrDimLunMarMerJeuVenSam" + #define STR_OFFLINE "eteint" + #define STR_ONLINE "online" + #define STR_NO_INVERTER "pas d'onduleur" + #define STR_NO_WIFI "WiFi not connected" + #define STR_VERSION "Version" + #define STR_ACTIVE_INVERTERS "active Inv" + #define STR_TODAY "today" + #define STR_TOTAL "total" +#else + #define STR_MONTHNAME_3_CHAR_LIST "ErrJanFebMarAprMayJunJulAugSepOctNovDec" + #define STR_DAYNAME_3_CHAR_LIST "ErrSunMonTueWedThuFriSat" + #define STR_OFFLINE "offline" + #define STR_ONLINE "online" + #define STR_NO_INVERTER "no inverter" + #define STR_NO_WIFI "WiFi not connected" + #define STR_VERSION "Version" + #define STR_ACTIVE_INVERTERS "active Inv" + #define STR_TODAY "today" + #define STR_TOTAL "total" +#endif + +#endif /*__PLUGIN_LANG_H__*/ diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 369d9ead..6e021bed 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -11,6 +11,8 @@ #if defined(ENABLE_MQTT) #ifdef ESP8266 #include <ESP8266WiFi.h> + #define xSemaphoreTake(a, b) { while(a) { yield(); } a = true; } + #define xSemaphoreGive(a) { a = false; } #elif defined(ESP32) #include <WiFi.h> #endif @@ -39,6 +41,13 @@ template<class HMSYSTEM> class PubMqtt { public: PubMqtt() : SendIvData() { + #if defined(ESP32) + mutex = xSemaphoreCreateBinaryStatic(&mutexBuffer); + xSemaphoreGive(mutex); + #else + mutex = false; + #endif + mLastIvState.fill(InverterStatus::OFF); mIvLastRTRpub.fill(0); @@ -50,9 +59,14 @@ class PubMqtt { mSendAlarm.fill(false); } - ~PubMqtt() { } + ~PubMqtt() { + #if defined(ESP32) + vSemaphoreDelete(mutex); + #endif + } - void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *uptime) { + void setup(IApp *app, cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *uptime) { + mApp = app; mCfgMqtt = cfg_mqtt; mDevName = devName; mVersion = version; @@ -61,7 +75,7 @@ class PubMqtt { mUptime = uptime; mIntervalTimeout = 1; - SendIvData.setup(sys, utcTs, &mSendList); + SendIvData.setup(app, sys, cfg_mqtt, utcTs, &mSendList); SendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained, uint8_t qos) { publish(subTopic, payload, retained, true, qos); }); @@ -95,6 +109,17 @@ class PubMqtt { } void loop() { + std::queue<message_s> queue; + xSemaphoreTake(mutex, portMAX_DELAY); + queue.swap(mReceiveQueue); + xSemaphoreGive(mutex); + + while (!queue.empty()) { + message_s *entry = &queue.front(); + handleMessage(entry->topic, entry->payload, entry->len, entry->index, entry->total); + queue.pop(); + } + SendIvData.loop(); #if defined(ESP8266) @@ -204,6 +229,9 @@ class PubMqtt { else snprintf(mTopic.data(), mTopic.size(), "%s", subTopic); + if(!mCfgMqtt->enableRetain) + retained = false; + mClient.publish(mTopic.data(), qos, retained, payload); yield(); mTxCnt++; @@ -242,7 +270,8 @@ class PubMqtt { void setPowerLimitAck(Inverter<> *iv) { if (NULL != iv) { snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", iv->config->name, subtopics[MQTT_ACK_PWR_LMT]); - publish(mSubTopic.data(), "true", true, true, QOS_2); + snprintf(mVal.data(), mVal.size(), "%.1f", iv->powerLimit[0]/10.0); + publish(mSubTopic.data(), mVal.data(), true, true, QOS_2); } } @@ -250,16 +279,15 @@ class PubMqtt { void onConnect(bool sessionPreset) { DPRINTLN(DBG_INFO, F("MQTT connected")); - publish(subtopics[MQTT_VERSION], mVersion, true); - publish(subtopics[MQTT_DEVICE], mDevName, true); - #if defined(ETHERNET) - publish(subtopics[MQTT_IP_ADDR], ETH.localIP().toString().c_str(), true); - #else - publish(subtopics[MQTT_IP_ADDR], WiFi.localIP().toString().c_str(), true); - #endif + publish(subtopics[MQTT_VERSION], mVersion, false); + publish(subtopics[MQTT_DEVICE], mDevName, false); + publish(subtopics[MQTT_IP_ADDR], mApp->getIp().c_str(), true); tickerMinute(); publish(mLwtTopic.data(), mqttStr[MQTT_STR_LWT_CONN], true, false); + snprintf(mVal.data(), mVal.size(), "ctrl/restart_ahoy"); + subscribe(mVal.data()); + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { snprintf(mVal.data(), mVal.size(), "ctrl/limit/%d", i); subscribe(mVal.data(), QOS_2); @@ -300,6 +328,14 @@ class PubMqtt { void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { if(len == 0) return; + + xSemaphoreTake(mutex, portMAX_DELAY); + mReceiveQueue.push(message_s(topic, payload, len, index, total)); + xSemaphoreGive(mutex); + + } + + inline void handleMessage(const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]); DBGPRINTLN(String(topic)); if(NULL == mSubscriptionCb) @@ -353,9 +389,9 @@ class PubMqtt { pos++; } - /*char out[128]; + char out[128]; serializeJson(root, out, 128); - DPRINTLN(DBG_INFO, "json: " + String(out));*/ + DPRINTLN(DBG_INFO, "json: " + String(out)); (mSubscriptionCb)(root); mRxCnt++; @@ -397,6 +433,10 @@ class PubMqtt { std::array<char, 32> name; std::array<char, 32> uniq_id; std::array<char, 350> buf; + topic.fill(0); + name.fill(0); + uniq_id.fill(0); + buf.fill(0); const char *devCls, *stateCls; if (!total) { if (rec->assign[mDiscovery.sub].ch == CH0) @@ -419,8 +459,8 @@ class PubMqtt { } DynamicJsonDocument doc2(512); - constexpr static char* unitTotal[] = {"W", "kWh", "Wh", "W"}; - doc2[F("name")] = name; + constexpr static const char* unitTotal[] = {"W", "kWh", "Wh", "W"}; + doc2[F("name")] = String(name.data()); doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic.data()); doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub])); doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id.data(); @@ -437,9 +477,9 @@ class PubMqtt { else // total values snprintf(topic.data(), topic.size(), "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(), fields[fldTotal[mDiscovery.sub]]); size_t size = measureJson(doc2) + 1; - buf.fill(0); serializeJson(doc2, buf.data(), size); - publish(topic.data(), buf.data(), true, false); + if(FLD_EVT != rec->assign[mDiscovery.sub].fieldId) + publish(topic.data(), buf.data(), true, false); if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) { mDiscovery.sub = 0; @@ -559,6 +599,9 @@ class PubMqtt { } void sendData(Inverter<> *iv, uint8_t curInfoCmd) { + if (mCfgMqtt->json) + return; + record_t<> *rec = iv->getRecordStruct(curInfoCmd); uint32_t lastTs = iv->getLastTs(rec); @@ -602,10 +645,80 @@ class PubMqtt { mLastAnyAvail = anyAvail; } + private: + enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE}; + + struct message_s + { + char *topic; + uint8_t *payload; + size_t len; + size_t index; + size_t total; + + message_s() + : topic { nullptr } + , payload { nullptr } + , len { 0 } + , index { 0 } + , total { 0 } + {} + + message_s(const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) + { + uint8_t topic_len = strlen(topic) + 1; + this->topic = new char[topic_len]; + this->payload = new uint8_t[len]; + + memcpy(this->topic, topic, topic_len); + memcpy(this->payload, payload, len); + this->len = len; + this->index = index; + this->total = total; + } + + message_s(const message_s &) = delete; + + message_s(message_s && other) : message_s {} + { + this->swap( other ); + } + + ~message_s() + { + delete[] this->topic; + delete[] this->payload; + } + + message_s &operator = (const message_s &) = delete; + + message_s &operator = (message_s &&other) + { + this->swap(other); + return *this; + } + + void swap(message_s &other) + { + std::swap(this->topic, other.topic); + std::swap(this->payload, other.payload); + std::swap(this->len, other.len); + std::swap(this->index, other.index); + std::swap(this->total, other.total); + } + + }; + + private: espMqttClient mClient; cfgMqtt_t *mCfgMqtt = nullptr; + IApp *mApp; #if defined(ESP8266) WiFiEventHandler mHWifiCon, mHWifiDiscon; + volatile bool mutex; + #else + SemaphoreHandle_t mutex; + StaticSemaphore_t mutexBuffer; #endif HMSYSTEM *mSys = nullptr; @@ -621,6 +734,8 @@ class PubMqtt { std::array<uint32_t, MAX_NUM_INVERTERS> mIvLastRTRpub; uint16_t mIntervalTimeout = 0; + std::queue<message_s> mReceiveQueue; + // last will topic and payload must be available through lifetime of 'espMqttClient' std::array<char, (MQTT_TOPIC_LEN + 5)> mLwtTopic; const char *mDevName = nullptr, *mVersion = nullptr; diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index c3ae3814..5ba21f6d 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -24,8 +24,10 @@ class PubMqttIvData { public: PubMqttIvData() : mTotal{}, mSubTopic{}, mVal{} {} - void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) { + void setup(IApp *app, HMSYSTEM *sys, cfgMqtt_t *cfg_mqtt, uint32_t *utcTs, std::queue<sendListCmdIv> *sendList) { + mApp = app; mSys = sys; + mCfg = cfg_mqtt; mUtcTimestamp = utcTs; mSendList = sendList; mState = IDLE; @@ -75,6 +77,7 @@ class PubMqttIvData { mTotalFound = false; mSendTotalYd = true; mAllTotalFound = true; + mAtLeastOneWasntSent = false; if(!mSendList->empty()) { mCmd = mSendList->front().cmd; mIvSend = mSendList->front().iv; @@ -113,7 +116,7 @@ class PubMqttIvData { mPublish(mSubTopic.data(), mVal.data(), true, QOS_0); if((mIv->ivGen == IV_HMS) || (mIv->ivGen == IV_HMT)) { - snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch0/rssi", mIv->config->name); + snprintf(mSubTopic.data(), mSubTopic.size(), "%s/rssi", mIv->config->name); snprintf(mVal.data(), mVal.size(), "%d", mIv->rssi); mPublish(mSubTopic.data(), mVal.data(), false, QOS_0); } @@ -122,7 +125,7 @@ class PubMqttIvData { mIv->isProducing(); // recalculate status mState = SEND_DATA; - } else if(mSendTotals && mTotalFound) { + } else if(mSendTotals && mTotalFound && mAtLeastOneWasntSent) { if(mYldTotalStore > mTotal[2]) mSendTotalYd = false; // don't send yield total if last value was greater else @@ -167,9 +170,6 @@ class PubMqttIvData { case FLD_PDC: mTotal[3] += mIv->getValue(mPos, rec); break; - case FLD_MP: - mTotal[4] += mIv->getValue(mPos, rec); - break; } } else mAllTotalFound = false; @@ -177,6 +177,7 @@ class PubMqttIvData { } if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { + mAtLeastOneWasntSent = true; if(InverterDevInform_All == mCmd) { snprintf(mSubTopic.data(), mSubTopic.size(), "%s/firmware", mIv->config->name); snprintf(mVal.data(), mVal.size(), "{\"version\":%d,\"build_year\":\"%d\",\"build_month_day\":%d,\"build_hour_min\":%d,\"bootloader\":%d}", @@ -185,7 +186,6 @@ class PubMqttIvData { static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_FW_BUILD_MONTH_DAY, rec)), static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_FW_BUILD_HOUR_MINUTE, rec)), static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_BOOTLOADER_VER, rec))); - retained = true; } else if(InverterDevInform_Simple == mCmd) { snprintf(mSubTopic.data(), mSubTopic.size(), "%s/hardware", mIv->config->name); snprintf(mVal.data(), mVal.size(), "{\"part\":%d,\"version\":\"%d\",\"grid_profile_code\":%d,\"grid_profile_version\":%d}", @@ -193,20 +193,54 @@ class PubMqttIvData { static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_HW_VERSION, rec)), static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_GRID_PROFILE_CODE, rec)), static_cast<int>(mIv->getChannelFieldValue(CH0, FLD_GRID_PROFILE_VERSION, rec))); - retained = true; } else { - snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); - snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec))); + if (!mCfg->json) { + snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]); + snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec))); + } else { + if (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) { + uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0; + snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", mIv->config->name, fields[rec->assign[mPos].fieldId]); + snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec))); + mPublish(mSubTopic.data(), mVal.data(), retained, qos); + } + } } - uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0; - if((FLD_EVT != rec->assign[mPos].fieldId) - && (FLD_LAST_ALARM_CODE != rec->assign[mPos].fieldId)) - mPublish(mSubTopic.data(), mVal.data(), retained, qos); + if ((InverterDevInform_All == mCmd) || (InverterDevInform_Simple == mCmd) || !mCfg->json) + { + uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0; + if((FLD_EVT != rec->assign[mPos].fieldId) && (FLD_LAST_ALARM_CODE != rec->assign[mPos].fieldId)) + mPublish(mSubTopic.data(), mVal.data(), retained, qos); + } } mPos++; } else { if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { + if (mCfg->json && (RealTimeRunData_Debug == mCmd)) { + DynamicJsonDocument doc(300); + + for (mPos = 0; mPos < rec->length; mPos++) { + doc[fields[rec->assign[mPos].fieldId]] = ah::round3(mIv->getValue(mPos, rec)); + + bool publish = false; + if (mPos != (rec->length - 1)) { // not last one + if (rec->assign[mPos].ch != rec->assign[mPos+1].ch) + publish = true; + } else + publish = true; + + if (publish) { + doc[F("ts")] = rec->ts; + // if next channel or end->publish + serializeJson(doc, mVal.data(), mVal.size()); + snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d", mIv->config->name, rec->assign[mPos].ch); + mPublish(mSubTopic.data(), mVal.data(), false, QOS_0); + doc.clear(); + } + } + } + sendRadioStat(rec->length); rec->mqttSentStatus = MqttSentStatus::DATA_SENT; } @@ -220,7 +254,7 @@ class PubMqttIvData { mIv->radioStatistics.txCnt, mIv->radioStatistics.rxSuccess, mIv->radioStatistics.rxFail, - mIv->radioStatistics.rxFailNoAnser, + mIv->radioStatistics.rxFailNoAnswer, mIv->radioStatistics.retransmits, mIv->radioStatistics.ivLoss, mIv->radioStatistics.ivSent, @@ -261,19 +295,40 @@ class PubMqttIvData { case 4: fieldId = FLD_MP; retained = false; + mTotal[4] = mApp->getTotalMaxPower(); break; } - snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]); - snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos])); - mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0); + if (!mCfg->json) { + snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]); + snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos])); + mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0); + } mPos++; } else { + if (mCfg->json) { + int type[5] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC, FLD_MP}; + snprintf(mVal.data(), mVal.size(), "{"); + + for (mPos = 0; mPos < 5; mPos++) { + snprintf(mSubTopic.data(), mSubTopic.size(), "\"%s\":%g", fields[type[mPos]], ah::round3(mTotal[mPos])); + strcat(mVal.data(), mSubTopic.data()); + if (mPos < 4) + strcat(mVal.data(), ","); + else + strcat(mVal.data(), "}"); + } + mPublish("total", mVal.data(), true, QOS_0); + } mSendList->pop(); mSendTotals = false; mState = IDLE; } } + private: + IApp *mApp = nullptr; + cfgMqtt_t *mCfg = nullptr; + HMSYSTEM *mSys = nullptr; uint32_t *mUtcTimestamp = nullptr; pubMqttPublisherType mPublish; @@ -282,7 +337,8 @@ class PubMqttIvData { uint8_t mCmd = 0; uint8_t mLastIvId = 0; - bool mSendTotals = false, mTotalFound = false, mAllTotalFound = false, mSendTotalYd = false; + bool mSendTotals = false, mTotalFound = false, mAllTotalFound = false; + bool mSendTotalYd = false, mAtLeastOneWasntSent = false; float mTotal[5], mYldTotalStore = 0; Inverter<> *mIv = nullptr, *mIvSend = nullptr; @@ -290,7 +346,7 @@ class PubMqttIvData { bool mRTRDataHasBeenSent = false; std::array<char, (32 + MAX_NAME_LENGTH + 1)> mSubTopic; - std::array<char, 140> mVal; + std::array<char, 300> mVal; std::queue<sendListCmdIv> *mSendList = nullptr; }; diff --git a/src/utils/dbg.h b/src/utils/dbg.h index 9e754ba6..f6dd8012 100644 --- a/src/utils/dbg.h +++ b/src/utils/dbg.h @@ -110,7 +110,7 @@ #if DEBUG_LEVEL >= DBG_ERROR #define PERR(str) DBGPRINT(F("E: ")); DBGPRINT(str); - #define PERRLN(str) DBGPRINT(F("E: ")); DBGPRINTLN(str); + #define PERRLN(str) DBGPRINT(F("E: ")); DBGPRINTLN(str); DSERIAL.flush(); #else #define PERR(str) #define PERRLN(str) diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp index 24a4d9ee..edb9b9b9 100644 --- a/src/utils/helper.cpp +++ b/src/utils/helper.cpp @@ -5,6 +5,12 @@ #include "helper.h" #include "dbg.h" +#include "../plugins/plugin_lang.h" + +#define dt_SHORT_STR_LEN_i18n 3 // the length of short strings +static char buffer_i18n[dt_SHORT_STR_LEN_i18n + 1]; // must be big enough for longest string and the terminating null +const char monthShortNames_P[] PROGMEM = STR_MONTHNAME_3_CHAR_LIST; +const char dayShortNames_P[] PROGMEM = STR_DAYNAME_3_CHAR_LIST; namespace ah { void ip2Arr(uint8_t ip[], const char *ipStr) { @@ -28,6 +34,10 @@ namespace ah { snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); } + double round1(double value) { + return (int)(value * 10 + 0.5) / 10.0; + } + double round3(double value) { return (int)(value * 1000 + 0.5) / 1000.0; } @@ -82,6 +92,31 @@ namespace ah { return String(str); } + static char* monthShortStr_i18n(uint8_t month) { + for (int i=0; i < dt_SHORT_STR_LEN_i18n; i++) + buffer_i18n[i] = pgm_read_byte(&(monthShortNames_P[i + month * dt_SHORT_STR_LEN_i18n])); + buffer_i18n[dt_SHORT_STR_LEN_i18n] = 0; + return buffer_i18n; + } + + static char* dayShortStr_i18n(uint8_t day) { + for (int i=0; i < dt_SHORT_STR_LEN_i18n; i++) + buffer_i18n[i] = pgm_read_byte(&(dayShortNames_P[i + day * dt_SHORT_STR_LEN_i18n])); + buffer_i18n[dt_SHORT_STR_LEN_i18n] = 0; + return buffer_i18n; + } + + String getDateTimeStrShort_i18n(time_t t) { + char str[20]; + if(0 == t) + sprintf(str, "n/a"); + else { + sprintf(str, "%3s ", dayShortStr_i18n(dayOfWeek(t))); + sprintf(str+4, "%2d.%3s %02d:%02d", day(t), monthShortStr_i18n(month(t)), hour(t), minute(t)); + } + return String(str); + } + uint64_t Serial2u64(const char *val) { char tmp[3]; uint64_t ret = 0ULL; diff --git a/src/utils/helper.h b/src/utils/helper.h index 1dbba3d9..ff1a9aed 100644 --- a/src/utils/helper.h +++ b/src/utils/helper.h @@ -39,9 +39,11 @@ static Timezone gTimezone(CEST, CET); namespace ah { void ip2Arr(uint8_t ip[], const char *ipStr); void ip2Char(uint8_t ip[], char *str); + double round1(double value); double round3(double value); String getDateTimeStr(time_t t); String getDateTimeStrShort(time_t t); + String getDateTimeStrShort_i18n(time_t t); String getDateTimeStrFile(time_t t); String getTimeStr(time_t t); String getTimeStrMs(uint64_t t); diff --git a/src/utils/improv.h b/src/utils/improv.h index 20b2bcad..d2ccc0c3 100644 --- a/src/utils/improv.h +++ b/src/utils/improv.h @@ -147,10 +147,12 @@ class Improv { } void getNetworks(void) { - if(!mScanRunning) - mApp->scanAvailNetworks(); - JsonObject obj; + if(!mScanRunning) { + mApp->getAvailNetworks(obj); + return; + } + if(!mApp->getAvailNetworks(obj)) return; diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h index 16009778..5ce43a36 100644 --- a/src/utils/scheduler.h +++ b/src/utils/scheduler.h @@ -35,14 +35,21 @@ namespace ah { mMax = 0; mPrevMillis = millis(); mTsMillis = mPrevMillis % 1000; + mFastTicker = false; resetTicker(); } virtual void loop(void) { mMillis = millis(); mDiff = mMillis - mPrevMillis; - if (mDiff < 1000) + if (mDiff < 1000) { + if (mFastTicker) { + mDiffSeconds = 0; + checkTicker(); + mFastTicker = false; + } return; + } mDiffSeconds = 1; if (mDiff < 2000) @@ -125,8 +132,10 @@ namespace ah { mTicker[i].timeout = timeout; mTicker[i].reload = reload; mTicker[i].isTimestamp = isTimestamp; - memset(mTicker[i].name, 0, 6); - strncpy(mTicker[i].name, name, (strlen(name) < 6) ? strlen(name) : 5); + strncpy(mTicker[i].name, name, 5); + mTicker[i].name[5]=0; + if (timeout == 0 && reload == false) + mFastTicker = true; if(mMax == i) mMax = i + 1; return i; @@ -162,6 +171,7 @@ namespace ah { uint32_t mMillis = 0, mPrevMillis = 0, mDiff = 0; uint8_t mDiffSeconds = 0; uint8_t mMax = 0; + bool mFastTicker; }; } diff --git a/src/utils/spiPatcher.cpp b/src/utils/spiPatcher.cpp index 3b7b5681..b3d27482 100644 --- a/src/utils/spiPatcher.cpp +++ b/src/utils/spiPatcher.cpp @@ -5,5 +5,6 @@ #if defined(ESP32) #include "spiPatcher.h" -SpiPatcher *SpiPatcher::mInstance = nullptr; +SpiPatcher *SpiPatcher::InstanceHost2 = nullptr; +SpiPatcher *SpiPatcher::InstanceHost3 = nullptr; #endif diff --git a/src/utils/spiPatcher.h b/src/utils/spiPatcher.h index 210b2a09..c8f0ba3c 100644 --- a/src/utils/spiPatcher.h +++ b/src/utils/spiPatcher.h @@ -9,23 +9,38 @@ #if defined(ESP32) +#include "dbg.h" #include "spiPatcherHandle.h" #include <driver/spi_master.h> #include <freertos/semphr.h> +#if (SOC_SPI_PERIPH_NUM > 2) + #define SPI_HOST_OTHER SPI3_HOST +#else + #define SPI_HOST_OTHER SPI2_HOST +#endif + class SpiPatcher { protected: explicit SpiPatcher(spi_host_device_t dev) : - mHostDevice(dev), mCurHandle(nullptr) { + mCurHandle(nullptr) { // Use binary semaphore instead of mutex for performance reasons mutex = xSemaphoreCreateBinaryStatic(&mutex_buffer); xSemaphoreGive(mutex); + mDev = dev; + mBusState = ESP_FAIL; + } - spi_bus_config_t buscfg = { - .mosi_io_num = -1, - .miso_io_num = -1, - .sclk_io_num = -1, + public: + SpiPatcher(const SpiPatcher &other) = delete; + void operator=(const SpiPatcher &) = delete; + + esp_err_t initBus(int mosi = -1, int miso = -1, int sclk = -1, spi_common_dma_t dmaType = SPI_DMA_DISABLED) { + mBusConfig = spi_bus_config_t { + .mosi_io_num = mosi, + .miso_io_num = miso, + .sclk_io_num = sclk, .quadwp_io_num = -1, .quadhd_io_num = -1, .data4_io_num = -1, @@ -36,26 +51,48 @@ class SpiPatcher { .flags = 0, .intr_flags = 0 }; - ESP_ERROR_CHECK(spi_bus_initialize(mHostDevice, &buscfg, SPI_DMA_DISABLED)); - } + ESP_ERROR_CHECK((mBusState = spi_bus_initialize(mDev, &mBusConfig, dmaType))); - public: - SpiPatcher(SpiPatcher &other) = delete; - void operator=(const SpiPatcher &) = delete; + return mBusState; + } - static SpiPatcher* getInstance(spi_host_device_t dev) { - if(nullptr == mInstance) - mInstance = new SpiPatcher(dev); - return mInstance; + static SpiPatcher* getInstance(spi_host_device_t dev, bool initialize = true) { + if(SPI2_HOST == dev) { + if(nullptr == InstanceHost2) { + InstanceHost2 = new SpiPatcher(dev); + if(initialize) + InstanceHost2->initBus(); + } + return InstanceHost2; + } else { // SPI3_HOST + if(nullptr == InstanceHost3) { + InstanceHost3 = new SpiPatcher(dev); + if(initialize) + InstanceHost3->initBus(); + } + return InstanceHost3; + } } ~SpiPatcher() { vSemaphoreDelete(mutex); } - spi_host_device_t getDevice() { - return mHostDevice; + inline void addDevice(spi_host_device_t host_id, const spi_device_interface_config_t *dev_config, spi_device_handle_t *handle) { + assert(mBusState == ESP_OK); + if(SPI2_HOST == host_id) + mHost2Cnt++; + #if (SOC_SPI_PERIPH_NUM > 2) + if(SPI3_HOST == host_id) + mHost3Cnt++; + #endif + + if((mHost2Cnt > 3) || (mHost3Cnt > 3)) + DPRINTLN(DBG_ERROR, F("maximum number of SPI devices reached (3)")); + + ESP_ERROR_CHECK(spi_bus_add_device(host_id, dev_config, handle)); } inline void request(SpiPatcherHandle* handle) { + assert(mBusState == ESP_OK); xSemaphoreTake(mutex, portMAX_DELAY); if (mCurHandle != handle) { @@ -70,17 +107,22 @@ class SpiPatcher { } inline void release() { + assert(mBusState == ESP_OK); xSemaphoreGive(mutex); } protected: - static SpiPatcher *mInstance; + static SpiPatcher *InstanceHost2; + static SpiPatcher *InstanceHost3; private: - const spi_host_device_t mHostDevice; SpiPatcherHandle* mCurHandle; SemaphoreHandle_t mutex; StaticSemaphore_t mutex_buffer; + uint8_t mHost2Cnt = 0, mHost3Cnt = 0; + spi_host_device_t mDev = SPI2_HOST; + esp_err_t mBusState = ESP_FAIL; + spi_bus_config_t mBusConfig; }; #endif /*ESP32*/ diff --git a/src/web/Protection.h b/src/web/Protection.h index 74f04b52..e41249ac 100644 --- a/src/web/Protection.h +++ b/src/web/Protection.h @@ -24,7 +24,7 @@ class Protection { } public: - Protection(Protection &other) = delete; + Protection(const Protection &other) = delete; void operator=(const Protection &) = delete; static Protection* getInstance(const char *pwd) { diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 09bba06e..eac91cb9 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -17,11 +17,7 @@ #include "../utils/helper.h" #include "lang.h" #include "AsyncJson.h" -#if defined(ETHERNET) -#include "AsyncWebServer_ESP32_W5500.h" -#else #include "ESPAsyncWebServer.h" -#endif #include "plugins/history.h" @@ -44,16 +40,24 @@ class RestApi { mApp = app; mSrv = srv; mSys = sys; - mRadioNrf = (HmRadio<>*)mApp->getRadioObj(true); + mRadioNrf = (NrfRadio<>*)mApp->getRadioObj(true); #if defined(ESP32) mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false); #endif mConfig = config; + #if defined(ENABLE_HISTORY_LOAD_DATA) + mSrv->on("/api/addYDHist", + HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1), + std::bind(&RestApi::onApiPostYDHist,this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + #endif mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1)); + #if defined(ESP32) + mSrv->on("/coredump", HTTP_GET, std::bind(&RestApi::getCoreDump, this, std::placeholders::_1)); + #endif } uint32_t getTimezoneOffset(void) { @@ -77,9 +81,19 @@ class RestApi { #ifndef ESP32 mHeapFreeBlk = ESP.getMaxFreeBlockSize(); mHeapFrag = ESP.getHeapFragmentation(); + #else + mHeapFreeBlk = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT); + if(mHeapFree > 0) + mHeapFrag = 100 - ((mHeapFreeBlk * 100) / mHeapFree); + else + mHeapFrag = 0; #endif + #if defined(ESP32) + AsyncJsonResponse* response = new AsyncJsonResponse(false, 8000); + #else AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000); + #endif JsonObject root = response->getRoot(); String path = request->url().substring(5); @@ -97,12 +111,13 @@ class RestApi { else if(path == "inverter/list") getInverterList(root); else if(path == "index") getIndex(request, root); else if(path == "setup") getSetup(request, root); - #if !defined(ETHERNET) else if(path == "setup/networks") getNetworks(root); - else if(path == "setup/getip") getWifiIp(root); - #endif /* !defined(ETHERNET) */ + else if(path == "setup/getip") getIp(root); else if(path == "live") getLive(request,root); - else if (path == "powerHistory") getPowerHistory(request, root); + #if defined(ENABLE_HISTORY) + else if (path == "powerHistory") getPowerHistory(request, root, HistoryStorageType::POWER); + else if (path == "powerHistoryDay") getPowerHistory(request, root, HistoryStorageType::POWER_DAY); + #endif /*ENABLE_HISTORY*/ else { if(path.substring(0, 12) == "inverter/id/") getInverter(root, request->url().substring(17).toInt()); @@ -137,11 +152,98 @@ class RestApi { #endif } - void onApiPostBody(AsyncWebServerRequest *request, const uint8_t *data, size_t len, size_t index, size_t total) { + #if defined(ENABLE_HISTORY_LOAD_DATA) + // VArt67: For debugging history graph. Loading data into graph + void onApiPostYDHist(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, size_t final) { + uint32_t total = request->contentLength(); + DPRINTLN(DBG_DEBUG, "[onApiPostYieldDHistory ] " + filename + " index:" + index + " len:" + len + " total:" + total + " final:" + final); + + if (0 == index) { + if (NULL != mTmpBuf) + delete[] mTmpBuf; + mTmpBuf = new uint8_t[total + 1]; + mTmpSize = total; + } + if (mTmpSize >= (len + index)) + memcpy(&mTmpBuf[index], data, len); + + if (!final) + return; // not last frame - nothing to do + + mTmpSize = len + index; // correct the total size + mTmpBuf[mTmpSize] = 0; + + #ifndef ESP32 + DynamicJsonDocument json(ESP.getMaxFreeBlockSize() - 512); // need some memory on heap + #else + DynamicJsonDocument json(12000); // does this work? I have no ESP32 :-( + #endif + DeserializationError err = deserializeJson(json, static_cast<const char *>(mTmpBuf, mTmpSize)); + json.shrinkToFit(); + JsonObject obj = json.as<JsonObject>(); + + // Debugging + // mTmpBuf[mTmpSize] = 0; + // DPRINTLN(DBG_DEBUG, (const char *)mTmpBuf); + + if (!err && obj) { + // insert data into yieldDayHistory object + HistoryStorageType dataType; + if (obj["maxDay"] > 0) // this is power history data + { + dataType = HistoryStorageType::POWER; + if (obj["refresh"] > 60) + dataType = HistoryStorageType::POWER_DAY; + + } + else + dataType = HistoryStorageType::YIELD; + + size_t cnt = obj[F("value")].size(); + DPRINTLN(DBG_DEBUG, "ArraySize: " + String(cnt)); + + for (uint16_t i = 0; i < cnt; i++) { + uint16_t val = obj[F("value")][i]; + mApp->addValueToHistory((uint8_t)dataType, 0, val); + // DPRINT(DBG_VERBOSE, "value " + String(i) + ": " + String(val) + ", "); + } + uint32_t refresh = obj[F("refresh")]; + mApp->addValueToHistory((uint8_t)dataType, 1, refresh); + if (dataType != HistoryStorageType::YIELD) { + uint32_t ts = obj[F("lastValueTs")]; + mApp->addValueToHistory((uint8_t)dataType, 2, ts); + } + + } else { + switch (err.code()) { + case DeserializationError::Ok: + break; + case DeserializationError::IncompleteInput: + DPRINTLN(DBG_DEBUG, F("Incomplete input")); + break; + case DeserializationError::InvalidInput: + DPRINTLN(DBG_DEBUG, F("Invalid input")); + break; + case DeserializationError::NoMemory: + DPRINTLN(DBG_DEBUG, F("Not enough memory ") + String(json.capacity()) + " bytes"); + break; + default: + DPRINTLN(DBG_DEBUG, F("Deserialization failed")); + break; + } + } + + request->send(204); // Success with no page load + delete[] mTmpBuf; + mTmpBuf = NULL; + } + #endif + + void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { DPRINTLN(DBG_VERBOSE, "onApiPostBody"); if(0 == index) { - if(NULL != mTmpBuf) + if(nullptr != mTmpBuf) delete[] mTmpBuf; mTmpBuf = new uint8_t[total+1]; mTmpSize = total; @@ -154,36 +256,40 @@ class RestApi { DynamicJsonDocument json(1000); - DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize); - JsonObject obj = json.as<JsonObject>(); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 200); JsonObject root = response->getRoot(); - root[F("success")] = (err) ? false : true; - if(!err) { - String path = request->url().substring(5); - if(path == "ctrl") - root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str()); - else if(path == "setup") - root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str()); - else { - root[F("success")] = false; - root[F("error")] = F(PATH_NOT_FOUND) + path; - } - } else { - switch (err.code()) { - case DeserializationError::Ok: break; - case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break; - case DeserializationError::InvalidInput: root[F("error")] = F(INVALID_INPUT); break; - case DeserializationError::NoMemory: root[F("error")] = F(NOT_ENOUGH_MEM); break; - default: root[F("error")] = F(DESER_FAILED); break; + DeserializationError err = deserializeJson(json, reinterpret_cast<const char*>(mTmpBuf), mTmpSize); + if(!json.is<JsonObject>()) + root[F("error")] = F(DESER_FAILED); + else { + JsonObject obj = json.as<JsonObject>(); + + root[F("success")] = (err) ? false : true; + if(!err) { + String path = request->url().substring(5); + if(path == "ctrl") + root[F("success")] = setCtrl(obj, root, request->client()->remoteIP().toString().c_str()); + else if(path == "setup") + root[F("success")] = setSetup(obj, root, request->client()->remoteIP().toString().c_str()); + else { + root[F("success")] = false; + root[F("error")] = F(PATH_NOT_FOUND) + path; + } + } else { + switch (err.code()) { + case DeserializationError::Ok: break; + case DeserializationError::IncompleteInput: root[F("error")] = F(INCOMPLETE_INPUT); break; + case DeserializationError::InvalidInput: root[F("error")] = F(INVALID_INPUT); break; + case DeserializationError::NoMemory: root[F("error")] = F(NOT_ENOUGH_MEM); break; + default: root[F("error")] = F(DESER_FAILED); break; + } } } response->setLength(); request->send(response); delete[] mTmpBuf; - mTmpBuf = NULL; + mTmpBuf = nullptr; } void getNotFound(JsonObject obj, String url) { @@ -195,14 +301,13 @@ class RestApi { ep[F("generic")] = url + F("generic"); ep[F("index")] = url + F("index"); ep[F("setup")] = url + F("setup"); - #if !defined(ETHERNET) ep[F("setup/networks")] = url + F("setup/networks"); ep[F("setup/getip")] = url + F("setup/getip"); - #endif /* !defined(ETHERNET) */ ep[F("system")] = url + F("system"); ep[F("live")] = url + F("live"); #if defined(ENABLE_HISTORY) ep[F("powerHistory")] = url + F("powerHistory"); + ep[F("powerHistoryDay")] = url + F("powerHistoryDay"); #endif } @@ -236,7 +341,7 @@ class RestApi { if(-1 != i) { i+=5; String sn = tmp.substring(i, tmp.indexOf("\"", i)-1); - tmp.replace(sn, String(atoll(sn.c_str()), HEX)); + tmp.replace(sn, String(sn) + ",\"note\":\"" + String(atoll(sn.c_str()), HEX) + "\""); } } response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp); @@ -252,9 +357,38 @@ class RestApi { fp.close(); } + #if defined(ESP32) + void getCoreDump(AsyncWebServerRequest *request) { + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_COREDUMP, "coredump"); + if (partition != NULL) { + size_t size = partition->size; + + AsyncWebServerResponse *response = request->beginResponse("application/octet-stream", size, [size, partition](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + if((index + maxLen) > size) + maxLen = size - index; + + if (ESP_OK != esp_partition_read(partition, index, buffer, maxLen)) + DPRINTLN(DBG_ERROR, F("can't read partition")); + + return maxLen; + }); + + String filename = ah::getDateTimeStrFile(gTimezone.toLocal(mApp->getTimestamp())); + filename += "_v" + String(mApp->getVersion()); + filename += "_" + String(ENV_NAME); + + response->addHeader("Content-Description", "File Transfer"); + response->addHeader("Content-Disposition", "attachment; filename=" + filename + "_coredump.bin"); + request->send(response); + } else { + AsyncWebServerResponse *response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}"); + request->send(response); + } + } + #endif + void getGeneric(AsyncWebServerRequest *request, JsonObject obj) { mApp->resetLockTimeout(); - obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); obj[F("ts_uptime")] = mApp->getUptime(); obj[F("ts_now")] = mApp->getTimestamp(); @@ -262,6 +396,7 @@ class RestApi { obj[F("modules")] = String(mApp->getVersionModules()); obj[F("build")] = String(AUTO_GIT_HASH); obj[F("env")] = String(ENV_NAME); + obj[F("host")] = mConfig->sys.deviceName; obj[F("menu_prot")] = mApp->isProtected(request->client()->remoteIP().toString().c_str(), "", true); obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask ); obj[F("menu_protEn")] = (bool) (mConfig->sys.adminPwd[0] != '\0'); @@ -278,67 +413,38 @@ class RestApi { } void getSysInfo(AsyncWebServerRequest *request, JsonObject obj) { - #if !defined(ETHERNET) - obj[F("ssid")] = mConfig->sys.stationSsid; - obj[F("ap_pwd")] = mConfig->sys.apPwd; - obj[F("hidd")] = mConfig->sys.isHidden; - #endif /* !defined(ETHERNET) */ obj[F("device_name")] = mConfig->sys.deviceName; obj[F("dark_mode")] = (bool)mConfig->sys.darkMode; obj[F("sched_reboot")] = (bool)mConfig->sys.schedReboot; - obj[F("mac")] = WiFi.macAddress(); - obj[F("hostname")] = mConfig->sys.deviceName; obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0); obj[F("prot_mask")] = mConfig->sys.protectionMask; - obj[F("sdk")] = ESP.getSdkVersion(); - obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); - obj[F("heap_free")] = mHeapFree; - obj[F("sketch_total")] = ESP.getFreeSketchSpace(); - obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb - getGeneric(request, obj); - + getGeneric(request, obj.createNestedObject(F("generic"))); + getChipInfo(obj.createNestedObject(F("chip"))); getRadioNrf(obj.createNestedObject(F("radioNrf"))); + getMqttInfo(obj.createNestedObject(F("mqtt"))); + getNetworkInfo(obj.createNestedObject(F("network"))); + getMemoryInfo(obj.createNestedObject(F("memory"))); #if defined(ESP32) getRadioCmtInfo(obj.createNestedObject(F("radioCmt"))); #endif - getMqttInfo(obj.createNestedObject(F("mqtt"))); - - #if defined(ESP32) - obj[F("chip_revision")] = ESP.getChipRevision(); - obj[F("chip_model")] = ESP.getChipModel(); - obj[F("chip_cores")] = ESP.getChipCores(); - obj[F("heap_total")] = ESP.getHeapSize(); - //obj[F("core_version")] = F("n/a"); - //obj[F("flash_size")] = F("n/a"); - //obj[F("heap_frag")] = F("n/a"); - //obj[F("max_free_blk")] = F("n/a"); - //obj[F("reboot_reason")] = F("n/a"); - #else - //obj[F("heap_total")] = F("n/a"); - //obj[F("chip_revision")] = F("n/a"); - //obj[F("chip_model")] = F("n/a"); - //obj[F("chip_cores")] = F("n/a"); - obj[F("heap_frag")] = mHeapFrag; - obj[F("max_free_blk")] = mHeapFreeBlk; - obj[F("core_version")] = ESP.getCoreVersion(); - obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb - obj[F("reboot_reason")] = ESP.getResetReason(); - #endif - //obj[F("littlefs_total")] = LittleFS.totalBytes(); - //obj[F("littlefs_used")] = LittleFS.usedBytes(); - uint8_t max; + /*uint8_t max; mApp->getSchedulerInfo(&max); - obj[F("schMax")] = max; + obj[F("schMax")] = max;*/ } void getHtmlSystem(AsyncWebServerRequest *request, JsonObject obj) { getSysInfo(request, obj.createNestedObject(F("system"))); getGeneric(request, obj.createNestedObject(F("generic"))); + #if defined(ESP32) + char tmp[300]; + snprintf(tmp, 300, "<a href=\"/factory\" class=\"btn\">%s</a><br/><br/><a href=\"/reboot\" class=\"btn\">%s</a><br/><br/><a href=\"/coredump\" class=\"btn\">%s</a>", FACTORY_RESET, BTN_REBOOT, BTN_COREDUMP); + #else char tmp[200]; snprintf(tmp, 200, "<a href=\"/factory\" class=\"btn\">%s</a><br/><br/><a href=\"/reboot\" class=\"btn\">%s</a>", FACTORY_RESET, BTN_REBOOT); + #endif obj[F("html")] = String(tmp); } @@ -351,8 +457,8 @@ class RestApi { void getHtmlReboot(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); - #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) - obj[F("refresh")] = 5; + #if defined(ETHERNET) + obj[F("refresh")] = (mConfig->sys.eth.enabled) ? 5 : 20; #else obj[F("refresh")] = 20; #endif @@ -365,8 +471,8 @@ class RestApi { obj[F("pending")] = (bool)mApp->getSavePending(); obj[F("success")] = (bool)mApp->getLastSaveSucceed(); obj[F("reboot")] = (bool)mApp->getShouldReboot(); - #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) - obj[F("reload")] = 5; + #if defined(ETHERNET) + obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20; #else obj[F("reload")] = 20; #endif @@ -383,7 +489,7 @@ class RestApi { mApp->setRebootFlag(); obj[F("html")] = F("Erase settings: success"); #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) - obj[F("reload")] = 5; + obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20; #else obj[F("reload")] = 20; #endif @@ -391,7 +497,9 @@ class RestApi { void getHtmlFactory(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); - obj[F("html")] = F("Factory reset? <a class=\"btn\" href=\"/factorytrue\">yes</a> <a class=\"btn\" href=\"/\">no</a>"); + char tmp[200]; + snprintf(tmp, 200, "%s <a class=\"btn\" href=\"/factorytrue\">%s</a> <a class=\"btn\" href=\"/\">%s</a>", FACTORY_RESET, BTN_YES, BTN_NO); + obj[F("html")] = tmp; } void getHtmlFactoryTrue(AsyncWebServerRequest *request, JsonObject obj) { @@ -399,8 +507,8 @@ class RestApi { mApp->eraseSettings(true); mApp->setRebootFlag(); obj[F("html")] = F("Factory reset: success"); - #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) - obj[F("reload")] = 5; + #if defined(ETHERNET) + obj[F("reload")] = (mConfig->sys.eth.enabled) ? 5 : 20; #else obj[F("reload")] = 20; #endif @@ -422,7 +530,7 @@ class RestApi { obj[F("name")] = String(iv->config->name); obj[F("rx_success")] = iv->radioStatistics.rxSuccess; obj[F("rx_fail")] = iv->radioStatistics.rxFail; - obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnser; + obj[F("rx_fail_answer")] = iv->radioStatistics.rxFailNoAnswer; obj[F("frame_cnt")] = iv->radioStatistics.frmCnt; obj[F("tx_cnt")] = iv->radioStatistics.txCnt; obj[F("retransmits")] = iv->radioStatistics.retransmits; @@ -474,13 +582,13 @@ class RestApi { } obj[F("interval")] = String(mConfig->inst.sendInterval); obj[F("max_num_inverters")] = MAX_NUM_INVERTERS; - obj[F("rstMid")] = (bool)mConfig->inst.rstYieldMidNight; + obj[F("rstMid")] = (bool)mConfig->inst.rstValsAtMidNight; obj[F("rstNotAvail")] = (bool)mConfig->inst.rstValsNotAvail; obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; + obj[F("rstComStart")] = (bool)mConfig->inst.rstValsCommStart; obj[F("strtWthtTm")] = (bool)mConfig->inst.startWithoutTime; obj[F("rdGrid")] = (bool)mConfig->inst.readGrid; - obj[F("rstMaxMid")] = (bool)mConfig->inst.rstMaxValsMidNight; - obj[F("yldEff")] = mConfig->inst.yieldEffiency; + obj[F("rstMaxMid")] = (bool)mConfig->inst.rstIncludeMaxVals; } void getInverter(JsonObject obj, uint8_t id) { @@ -496,7 +604,7 @@ class RestApi { obj[F("name")] = String(iv->config->name); obj[F("serial")] = String(iv->config->serial.u64, HEX); obj[F("version")] = String(iv->getFwVersion()); - obj[F("power_limit_read")] = iv->actPowerLimit; + obj[F("power_limit_read")] = ah::round1(iv->getChannelFieldValue(CH0, FLD_ACT_ACTIVE_PWR_LIMIT, iv->getRecordStruct(SystemConfigPara))); obj[F("power_limit_ack")] = iv->powerLimitAck; obj[F("max_pwr")] = iv->getMaxPower(); obj[F("ts_last_success")] = rec->ts; @@ -505,6 +613,7 @@ class RestApi { obj[F("alarm_cnt")] = iv->alarmCnt; obj[F("rssi")] = iv->rssi; obj[F("ts_max_ac_pwr")] = iv->tsMaxAcPower; + obj[F("ts_max_temp")] = iv->tsMaxTemperature; JsonArray ch = obj.createNestedArray("ch"); @@ -561,11 +670,23 @@ class RestApi { obj[F("last_id")] = iv->getChannelFieldValue(CH0, FLD_EVT, rec); JsonArray alarm = obj.createNestedArray(F("alarm")); + + // find oldest alarm + uint8_t offset = 0; + uint32_t oldestStart = 0xffffffff; for(uint8_t i = 0; i < 10; i++) { - alarm[i][F("code")] = iv->lastAlarm[i].code; - alarm[i][F("str")] = iv->getAlarmStr(iv->lastAlarm[i].code); - alarm[i][F("start")] = iv->lastAlarm[i].start; - alarm[i][F("end")] = iv->lastAlarm[i].end; + if((iv->lastAlarm[i].start != 0) && (iv->lastAlarm[i].start < oldestStart)) { + offset = i; + oldestStart = iv->lastAlarm[i].start; + } + } + + for(uint8_t i = 0; i < 10; i++) { + uint8_t pos = (i + offset) % 10; + alarm[pos][F("code")] = iv->lastAlarm[pos].code; + alarm[pos][F("str")] = iv->getAlarmStr(iv->lastAlarm[pos].code); + alarm[pos][F("start")] = iv->lastAlarm[pos].start; + alarm[pos][F("end")] = iv->lastAlarm[pos].end; } } @@ -612,7 +733,9 @@ class RestApi { obj[F("user")] = String(mConfig->mqtt.user); obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); obj[F("topic")] = String(mConfig->mqtt.topic); + obj[F("json")] = (bool) mConfig->mqtt.json; obj[F("interval")] = String(mConfig->mqtt.interval); + obj[F("retain")] = (bool)mConfig->mqtt.enableRetain; } void getNtp(JsonObject obj) { @@ -665,6 +788,116 @@ class RestApi { } #endif + #if defined(ETHERNET) + void getEthernet(JsonObject obj) { + obj[F("en")] = mConfig->sys.eth.enabled; + obj[F("cs")] = mConfig->sys.eth.pinCs; + obj[F("sclk")] = mConfig->sys.eth.pinSclk; + obj[F("miso")] = mConfig->sys.eth.pinMiso; + obj[F("mosi")] = mConfig->sys.eth.pinMosi; + obj[F("irq")] = mConfig->sys.eth.pinIrq; + obj[F("reset")] = mConfig->sys.eth.pinRst; + } + #endif + + void getNetworkInfo(JsonObject obj) { + #if defined(ETHERNET) + bool isWired = mApp->isWiredConnection(); + if(!isWired) + obj[F("wifi_channel")] = WiFi.channel(); + + obj[F("wired")] = isWired; + #else + obj[F("wired")] = false; + obj[F("wifi_channel")] = WiFi.channel(); + #endif + + obj[F("ap_pwd")] = mConfig->sys.apPwd; + obj[F("ssid")] = mConfig->sys.stationSsid; + obj[F("hidd")] = mConfig->sys.isHidden; + obj[F("mac")] = mApp->getMac(); + obj[F("ip")] = mApp->getIp(); + } + + void getChipInfo(JsonObject obj) { + obj[F("cpu_freq")] = ESP.getCpuFreqMHz(); + obj[F("sdk")] = ESP.getSdkVersion(); + #if defined(ESP32) + obj[F("revision")] = ESP.getChipRevision(); + obj[F("model")] = ESP.getChipModel(); + obj[F("cores")] = ESP.getChipCores(); + + switch (esp_reset_reason()) { + default: + [[fallthrough]]; + case ESP_RST_UNKNOWN: + obj[F("reboot_reason")] = F("Unknown"); + break; + case ESP_RST_POWERON: + obj[F("reboot_reason")] = F("Power on"); + break; + case ESP_RST_EXT: + obj[F("reboot_reason")] = F("External"); + break; + case ESP_RST_SW: + obj[F("reboot_reason")] = F("Software"); + break; + case ESP_RST_PANIC: + obj[F("reboot_reason")] = F("Panic"); + break; + case ESP_RST_INT_WDT: + obj[F("reboot_reason")] = F("Interrupt Watchdog"); + break; + case ESP_RST_TASK_WDT: + obj[F("reboot_reason")] = F("Task Watchdog"); + break; + case ESP_RST_WDT: + obj[F("reboot_reason")] = F("Watchdog"); + break; + case ESP_RST_DEEPSLEEP: + obj[F("reboot_reason")] = F("Deepsleep"); + break; + case ESP_RST_BROWNOUT: + obj[F("reboot_reason")] = F("Brownout"); + break; + case ESP_RST_SDIO: + obj[F("reboot_reason")] = F("SDIO"); + break; + } + #else + obj[F("core_version")] = ESP.getCoreVersion(); + obj[F("reboot_reason")] = ESP.getResetReason(); + #endif + } + + void getMemoryInfo(JsonObject obj) { + obj[F("heap_frag")] = mHeapFrag; + obj[F("heap_max_free_blk")] = mHeapFreeBlk; + obj[F("heap_free")] = mHeapFree; + + obj[F("par_size_app0")] = ESP.getFreeSketchSpace(); + obj[F("par_used_app0")] = ESP.getSketchSize(); + + #if defined(ESP32) + obj[F("heap_total")] = ESP.getHeapSize(); + + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_COREDUMP, "coredump"); + if (partition != NULL) + obj[F("flash_size")] = partition->address + partition->size; + + obj[F("par_size_spiffs")] = LittleFS.totalBytes(); + obj[F("par_used_spiffs")] = LittleFS.usedBytes(); + #else + obj[F("flash_size")] = ESP.getFlashChipRealSize(); + + FSInfo info; + LittleFS.info(info); + obj[F("par_used_spiffs")] = info.usedBytes; + obj[F("par_size_spiffs")] = info.totalBytes; + obj[F("heap_total")] = 24*1014; // FIXME: don't know correct value + #endif + } + void getRadioNrf(JsonObject obj) { obj[F("en")] = (bool) mConfig->nrf.enabled; if(mConfig->nrf.enabled) { @@ -752,11 +985,9 @@ class RestApi { warn.add(F(REBOOT_ESP_APPLY_CHANGES)); if(0 == mApp->getTimestamp()) warn.add(F(TIME_NOT_SET)); - #if !defined(ETHERNET) - #if !defined(ESP32) - if(mApp->getWasInCh12to14()) - warn.add(F(WAS_IN_CH_12_TO_14)); - #endif + #if !defined(ESP32) + if(mApp->getWasInCh12to14()) + warn.add(F(WAS_IN_CH_12_TO_14)); #endif } @@ -771,24 +1002,28 @@ class RestApi { #if defined(ESP32) getRadioCmt(obj.createNestedObject(F("radioCmt"))); #endif + #if defined(ETHERNET) + getEthernet(obj.createNestedObject(F("eth"))); + #endif getRadioNrf(obj.createNestedObject(F("radioNrf"))); getSerial(obj.createNestedObject(F("serial"))); getStaticIp(obj.createNestedObject(F("static_ip"))); getDisplay(obj.createNestedObject(F("display"))); } - #if !defined(ETHERNET) void getNetworks(JsonObject obj) { - mApp->getAvailNetworks(obj); + obj[F("success")] = mApp->getAvailNetworks(obj); + obj[F("ip")] = mApp->getIp(); } - void getWifiIp(JsonObject obj) { - obj[F("ip")] = mApp->getStationIp(); + + void getIp(JsonObject obj) { + obj[F("ip")] = mApp->getIp(); } - #endif /* !defined(ETHERNET) */ void getLive(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); obj[F("refresh")] = mConfig->inst.sendInterval; + obj[F("max_total_pwr")] = ah::round3(mApp->getTotalMaxPower()); for (uint8_t fld = 0; fld < sizeof(acList); fld++) { obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]); @@ -809,23 +1044,50 @@ class RestApi { } } - void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) { + #if defined(ENABLE_HISTORY) + void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj, HistoryStorageType type) { getGeneric(request, obj.createNestedObject(F("generic"))); - #if defined(ENABLE_HISTORY) - obj[F("refresh")] = mConfig->inst.sendInterval; + obj[F("refresh")] = mApp->getHistoryPeriod(static_cast<uint8_t>(type)); + uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { - uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld); + uint16_t value = mApp->getHistoryValue(static_cast<uint8_t>(type), fld); + obj[F("value")][fld] = value; + if (value > max) + max = value; + } + obj[F("max")] = max; + + if(HistoryStorageType::POWER_DAY == type) { + float yldDay = 0; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { + Inverter<> *iv = mSys->getInverterByPos(i); + if (iv == NULL) + continue; + record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); + yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + } + obj[F("yld")] = ah::round3(yldDay / 1000.0); + } + + obj[F("lastValueTs")] = mApp->getHistoryLastValueTs(static_cast<uint8_t>(type)); + } + #endif /*ENABLE_HISTORY*/ + + + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) { + obj[F("refresh")] = mApp->getHistoryPeriod((uint8_t)HistoryStorageType::YIELD); + uint16_t max = 0; + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld); obj[F("value")][fld] = value; if (value > max) max = value; } obj[F("max")] = max; - obj[F("maxDay")] = mApp->getHistoryMaxDay(); - #else - obj[F("refresh")] = 86400; // 1 day; - #endif /*ENABLE_HISTORY*/ } + #endif /*ENABLE_HISTORY_YIELD_PER_DAY*/ bool setCtrl(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) { if(jsonIn.containsKey(F("auth"))) { @@ -867,11 +1129,11 @@ class RestApi { iv->powerLimit[1] = AbsolutNonPersistent; accepted = iv->setDevControlRequest(ActivePowerContr); - if(accepted) - mApp->triggerTickSend(); } else if(F("dev") == jsonIn[F("cmd")]) { DPRINTLN(DBG_INFO, F("dev cmd")); iv->setDevCommand(jsonIn[F("val")].as<int>()); + } else if(F("restart_ahoy") == jsonIn[F("cmd")]) { + mApp->setRebootFlag(); } else { jsonOut[F("error")] = F("ERR_UNKNOWN_CMD"); return false; @@ -889,11 +1151,6 @@ class RestApi { if(isProtected(jsonIn, jsonOut, clientIP)) return false; - #if !defined(ETHERNET) - if(F("scan_wifi") == jsonIn[F("cmd")]) - mApp->scanAvailNetworks(); - else - #endif /* !defined(ETHERNET) */ if(F("set_time") == jsonIn[F("cmd")]) mApp->setTimestamp(jsonIn[F("val")]); else if(F("sync_ntp") == jsonIn[F("cmd")]) @@ -902,17 +1159,38 @@ class RestApi { mTimezoneOffset = jsonIn[F("val")]; else if(F("discovery_cfg") == jsonIn[F("cmd")]) mApp->setMqttDiscoveryFlag(); // for homeassistant - #if !defined(ETHERNET) else if(F("save_wifi") == jsonIn[F("cmd")]) { snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", jsonIn[F("ssid")].as<const char*>()); snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", jsonIn[F("pwd")].as<const char*>()); mApp->saveSettings(false); // without reboot - mApp->setStopApAllowedMode(false); mApp->setupStation(); } - #endif /* !defined(ETHERNET */ + #if defined(ETHERNET) + else if(F("save_eth") == jsonIn[F("cmd")]) { + mConfig->sys.eth.enabled = jsonIn[F("en")].as<bool>(); + mConfig->sys.eth.pinCs = jsonIn[F("cs")].as<uint8_t>(); + mConfig->sys.eth.pinSclk = jsonIn[F("sclk")].as<uint8_t>(); + mConfig->sys.eth.pinMiso = jsonIn[F("miso")].as<uint8_t>(); + mConfig->sys.eth.pinMosi = jsonIn[F("mosi")].as<uint8_t>(); + mConfig->sys.eth.pinIrq = jsonIn[F("irq")].as<uint8_t>(); + mConfig->sys.eth.pinRst = jsonIn[F("reset")].as<uint8_t>(); + mApp->saveSettings(true); + } + #endif else if(F("save_iv") == jsonIn[F("cmd")]) { - Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")], false); + Inverter<> *iv; + + for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { + iv = mSys->getInverterByPos(i, true); + if(nullptr != iv) { + if((i != jsonIn[F("id")]) && (iv->config->serial.u64 == jsonIn[F("ser")])) { + jsonOut[F("error")] = F("ERR_DUPLICATE_INVERTER"); + return false; + } + } + } + + iv = mSys->getInverterByPos(jsonIn[F("id")], false); iv->config->enabled = jsonIn[F("en")]; iv->config->serial.u64 = jsonIn[F("ser")]; snprintf(iv->config->name, MAX_NAME_LENGTH, "%s", jsonIn[F("name")].as<const char*>()); @@ -956,15 +1234,15 @@ class RestApi { private: constexpr static uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, - FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP}; + FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP, FLD_MT}; constexpr static uint8_t acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, FLD_PAC, FLD_F, FLD_PF, FLD_T, - FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP}; + FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP, FLD_MT}; constexpr static uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP}; private: IApp *mApp = nullptr; HMSYSTEM *mSys = nullptr; - HmRadio<> *mRadioNrf = nullptr; + NrfRadio<> *mRadioNrf = nullptr; #if defined(ESP32) CmtRadio<> *mRadioCmt = nullptr; #endif @@ -974,7 +1252,7 @@ class RestApi { uint32_t mTimezoneOffset = 0; uint32_t mHeapFree = 0, mHeapFreeBlk = 0; uint8_t mHeapFrag = 0; - uint8_t *mTmpBuf = NULL; + uint8_t *mTmpBuf = nullptr; uint32_t mTmpSize = 0; }; diff --git a/src/web/html/about.html b/src/web/html/about.html index c0eb8c5e..1b27ac9d 100644 --- a/src/web/html/about.html +++ b/src/web/html/about.html @@ -14,7 +14,7 @@ <div class="p-2">Used Libraries</div> </div> <div class="row"><a href="https://github.com/bertmelis/espMqttClient" target="_blank">bertmelis/espMqttClient</a></div> - <div class="row"><a href="https://github.com/yubox-node-org/ESPAsyncWebServer" target="_blank">yubox-node-org/ESPAsyncWebServer</a></div> + <div class="row"><a href="https://github.com/esphome/ESPAsyncWebServer" target="_blank">esphome/ESPAsyncWebServer</a></div> <div class="row"><a href="https://github.com/bblanchon/ArduinoJson" target="_blank">bblanchon/ArduinoJson</a></div> <div class="row"><a href="https://github.com/nrf24/RF24" target="_blank">nrf24/RF24</a></div> <div class="row"><a href="https://github.com/paulstoffregen/Time" target="_blank">paulstoffregen/Time</a></div> diff --git a/src/web/html/api.js b/src/web/html/api.js index 5cce4206..12ab239b 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -61,6 +61,23 @@ function ml(tagName, ...args) { return nester(el, args[1]) } +function mlNs(tagName, ...args) { + var el = document.createElementNS("http://www.w3.org/2000/svg", tagName); + if(args[0]) { + for(var name in args[0]) { + if(name.indexOf("on") === 0) { + el.addEventListener(name.substr(2).toLowerCase(), args[0][name], false) + } else { + el.setAttribute(name, args[0][name]); + } + } + } + if (!args[1]) { + return el; + } + return nester(el, args[1]) +} + function nester(el, n) { if (typeof n === "string") { el.innerHTML = n; @@ -84,10 +101,12 @@ function topnav() { } function parseNav(obj) { - for(i = 0; i < 13; i++) { + for(i = 0; i < 14; i++) { if(i == 2) continue; var l = document.getElementById("nav"+i); + if(null == l) + continue if(12 == i) { if(obj.cst_lnk.length > 0) { l.href = obj.cst_lnk @@ -124,7 +143,7 @@ function parseVersion(obj) { function parseESP(obj) { document.getElementById("esp_type").replaceChildren( - document.createTextNode("Board: " + obj["esp_type"]) + document.createTextNode("Board: " + obj.esp_type) ); } @@ -134,7 +153,11 @@ function parseRssi(obj) { icon = iconWifi1; else if(obj["wifi_rssi"] <= -70) icon = iconWifi2; - document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "icon-fg2", obj["wifi_rssi"])); + document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "icon-fg2", obj.wifi_rssi)); +} + +function parseTitle(obj) { + document.title = obj.host + " - " + document.title } function toIsoDateStr(d) { @@ -221,6 +244,10 @@ function badge(success, text, second="error") { return ml("span", {class: "badge badge-" + ((success) ? "success" : second)}, text); } +function progress(val) { + return ml("div", {class: "progress"}, ml("div", {class: "progress-bar", style: "width: " + val + "%"}, null)) +} + function tabChange(id) { var els = document.getElementsByClassName("nav-link"); [].forEach.call(els, function(e) { diff --git a/src/web/html/colorBright.css b/src/web/html/colorBright.css index 2e676029..ebf4f12a 100644 --- a/src/web/html/colorBright.css +++ b/src/web/html/colorBright.css @@ -12,6 +12,7 @@ --nav-bg: #333; --primary: #006ec0; + --primary-disabled: #ccc; --primary-hover: #044e86; --secondary: #0072c8; --nav-active: #555; @@ -30,4 +31,8 @@ --ch-head-bg: #006ec0; --ts-head: #333; --ts-bg: #555; + + --chart-cont: #fbfbfb; + --chart-bg: #f9f9f9; + --chart-text: #000000; } diff --git a/src/web/html/colorDark.css b/src/web/html/colorDark.css index 40bd4cf3..23e7a2cf 100644 --- a/src/web/html/colorDark.css +++ b/src/web/html/colorDark.css @@ -12,6 +12,7 @@ --nav-bg: #333; --primary: #004d87; + --primary-disabled: #ccc; --primary-hover: #023155; --secondary: #0072c8; --nav-active: #555; @@ -30,4 +31,8 @@ --ch-head-bg: #236; --ts-head: #333; --ts-bg: #555; + + --chart-cont: #0b0b0b; + --chart-bg: #090909; + --chart-text: #FFFFFF; } diff --git a/src/web/html/grid_info.json b/src/web/html/grid_info.json index 12760ccf..5b5d4186 100644 --- a/src/web/html/grid_info.json +++ b/src/web/html/grid_info.json @@ -10,10 +10,13 @@ {"0x0908": "France_VFR2014"}, {"0x0a00": "DE NF_EN_50549-1:2019"}, {"0x0c00": "AT_TOR_Erzeuger_default"}, + {"0x0c03": "AT_TOR_Erzeuger_cosphi=1"}, + {"0x0c04": "AT_TOR_Erzeuger_default"}, {"0x0d00": "FR_VFR2019"}, {"0x0d04": "NF_EN_50549-1:2019"}, {"0x1000": "ES_RD1699"}, {"0x1200": "EU_EN50438"}, + {"0x1300": "MEX_NOM_220V"}, {"0x2600": "BE_C10_26"}, {"0x2900": "NL_NEN-EN50549-1_2019"}, {"0x2a00": "PL_PN-EN 50549-1:2019"}, @@ -35,6 +38,44 @@ {"0xb0": "Watt Power Factor"} ], "group": [ + { + "0x0000": [ + { + "name": "Nominal Voltage", + "div": 10, + "def": 220, + "unit": "V" + }, + { + "name": "Low Voltage 1", + "div": 10, + "min": 170, + "max": 195.5, + "def": 184, + "unit": "V" + }, + { + "name": "LV1 Maximum Trip Time", + "div": 10, + "def": 0.1, + "unit": "s" + }, + { + "name": "High Voltage 1", + "div": 10, + "min": 253, + "max": 275, + "def": 270, + "unit": "V" + }, + { + "name": "HV1 Maximum Trip Time", + "div": 10, + "def": 0.1, + "unit": "s" + } + ] + }, { "0x0003": [ { @@ -213,6 +254,78 @@ } ] }, + { + "0x000b": [ + { + "name": "Nominal Voltage", + "div": 10, + "def": 230, + "unit": "V" + }, + { + "name": "Low Voltage 1", + "div": 10, + "min": 170, + "max": 184, + "def": 184, + "unit": "V" + }, + { + "name": "LV1 Maximum Trip Time", + "div": 10, + "def": 1.5, + "unit": "s" + }, + { + "name": "High Voltage 1", + "div": 10, + "min": 253, + "max": 270, + "def": 255.3, + "unit": "V" + }, + { + "name": "HV1 Maximum Trip Time", + "div": 10, + "def": 0.1, + "unit": "s" + }, + { + "name": "Low Voltage 2", + "div": 10, + "def": 57.5, + "unit": "V" + }, + { + "name": "LV2 Maximum Trip Time", + "div": 100, + "def": 0.5, + "unit": "s" + }, + { + "name": "High Voltage 2", + "div": 10, + "min": 264.5, + "max": 275, + "def": 264.5, + "unit": "V" + }, + { + "name": "HV2 Maximum Trip Time", + "div": 100, + "def": 0.08, + "unit": "s" + }, + { + "name": "10mins Average High Voltage", + "div": 10, + "min": 245, + "max": 255.3, + "def": 255.3, + "unit": "V" + } + ] + }, { "0x000c": [ { @@ -766,6 +879,35 @@ } ] }, + { + "0x6004": [ + { + "name": "VW Function Activated", + "div": 1, + "min": 0, + "max": 1, + "def": 1 + }, + { + "name": "Start of Voltage Watt Droop", + "div": 10, + "def": 253, + "unit": "V" + }, + { + "name": "End of Voltage Watt Droop", + "div": 10, + "def": 257.6, + "unit": "V" + }, + { + "name": "VW Droop Slope", + "div": 100, + "def": 21.74, + "unit": "Pn%/V" + } + ] + }, { "0x7000": [ { diff --git a/src/web/html/history.html b/src/web/html/history.html index 7e317b59..8b3b63c7 100644 --- a/src/web/html/history.html +++ b/src/web/html/history.html @@ -13,82 +13,178 @@ <div id="wrapper"> <div id="content"> <h3>{#TOTAL_POWER}</h3> - <div> - <div class="chartDiv" id="pwrChart"> </div> - <p> - {#MAX_DAY}: <span id="pwrMaxDay"></span> W. {#LAST_VALUE}: <span id="pwrLast"></span> W.<br /> - {#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS} - </p> - </div> + <div class="chartDiv" id="pwrChart"></div> + <h3>{#TOTAL_POWER_DAY}</h3> + <div class="chartDiv" id="pwrDayChart"></div> + <!--IF_ENABLE_HISTORY_YIELD_PER_DAY--> + <h3>{#TOTAL_YIELD_PER_DAY}</h3> + <div class="chartDiv" id="ydChart"></div> + <!--ENDIF_ENABLE_HISTORY_YIELD_PER_DAY--> + <!--IF_ENABLE_HISTORY_LOAD_DATA--> + <h4 style="margin-bottom:0px;">Insert data into Yield per day history</h4> + <fieldset style="padding: 1px;"> + <legend class="des" style="margin-top: 0px;">Insert data (*.json) i.e. from a saved "/api/yieldDayHistory" call + </legend> + <form id="form" method="POST" action="/api/addYDHist" enctype="multipart/form-data" + accept-charset="utf-8"> + <input type="button" class="btn my-4" style="padding: 3px;margin: 3px;" value="Insert" onclick="submit()"> + <input type="file" name="insert" style="width: 80%;"> + </form> + </fieldset> + <!--ENDIF_ENABLE_HISTORY_LOAD_DATA--> </div> </div> {#HTML_FOOTER} <script type="text/javascript"> - const svgns = "http://www.w3.org/2000/svg"; - var pwrExeOnce = true; - var ydExeOnce = true; - // make a simple rectangle - var mRefresh = 60; - var mLastValue = 0; - const mChartHeight = 250; - - function parseHistory(obj, namePrefix, execOnce) { - mRefresh = obj.refresh - var data = Object.assign({}, obj.value) - numDataPts = Object.keys(data).length - - if (true == execOnce) { - let s = document.createElementNS(svgns, "svg"); - s.setAttribute("class", "chart"); - s.setAttribute("width", (numDataPts + 2) * 2); - s.setAttribute("height", mChartHeight); - s.setAttribute("role", "img"); - - let g = document.createElementNS(svgns, "g"); - s.appendChild(g); - for (var i = 0; i < numDataPts; i++) { - val = data[i]; - let rect = document.createElementNS(svgns, "rect"); - rect.setAttribute("id", namePrefix+"Rect" + i); - rect.setAttribute("x", i * 2); - rect.setAttribute("width", 2); - g.appendChild(rect); + const height = 250 + var once = true + + function calcScale(obj) { + let s = {} + s.x_mul = 60 + s.ts_dur = obj.refresh * obj.value.length + s.ts_start = obj.lastValueTs - s.ts_dur + while(s.x_mul * 10 <= s.ts_dur) + s.x_mul += (s.x_mul == 60) ? 240 : ((s.x_mul < 1800) ? 300 : 1800) + + s.y_mul = 10 + while(s.y_mul * 10 <= obj.max) + s.y_mul += (s.y_mul < 100) ? 10 : 100 + s.y_step = Math.ceil(obj.max / s.y_mul) + s.y_max = s.y_mul * s.y_step + return s + } + + function setupSvg(id, obj) { + let scale = calcScale(obj) + let n = obj.value.length + return mlNs("svg", {class: "container", id: id+"_svg", viewBox: "0 0 "+String(n*2+50)+" "+String(height+20), width: "100%", height: "100%"}, [ + mlNs("defs", {}, [ + mlNs("linearGradient", {id: "gLine", x1: 0, y1: 0, x2: 0, y2: "100%"}, [ + mlNs("stop", {offset: 0, "stop-color": "#006ec0"}), + mlNs("stop", {offset: "80%", "stop-color": "#5050ff"}), + mlNs("stop", {offset: "100%", "stop-color": "gray"}) + ]), + mlNs("linearGradient", {id: "gFill", x1: 0, y1: 0, x2: 0, y2: "100%"}, [ + mlNs("stop", {offset: 0, "stop-color": "#006ec0"}), + mlNs("stop", {offset: "50%", "stop-color": "#0034c0"}), + mlNs("stop", {offset: "100%", "stop-color": "#e0e0e0"}) + ]) + ]), + ...gridText(n*2, scale), + mlNs("g", {transform: "translate(30, 5)"}, [ + ...grid(n*2, scale), + ...poly(n*2, obj, scale) + ]) + ]) + } + + function gridText(x2, scale) { + let g = [] + let div = height / scale.y_max + for(let i = 0; i <= scale.y_max; i += scale.y_mul) { + g.push(mlNs("text", {x: 0, y: height-(i*div)+9}, String(i))) + } + div = x2 / scale.ts_dur + for(let i = 0; i < scale.ts_dur; i++) { + if((i + scale.ts_start) % scale.x_mul == 0) { + let d = new Date((scale.ts_start + i) * 1000) + g.push(mlNs("text", {x: (i*div)+17, y: height+20}, ("0"+d.getHours()).slice(-2) + ":" + ("0"+d.getMinutes()).slice(-2))) } - document.getElementById(namePrefix+"Chart").appendChild(s); } + return g + } - // normalize data to chart - let divider = obj.max / mChartHeight; - if (divider == 0) - divider = 1; - for (var i = 0; i < numDataPts; i++) { - val = data[i]; - if (val > 0) - mLastValue = val - val = val / divider - rect = document.getElementById(namePrefix+"Rect" + i); - rect.setAttribute("height", val); - rect.setAttribute("y", mChartHeight - val); + function grid(x2, scale) { + let g = [] + let div = height / scale.y_max + for(let i = 0; i <= scale.y_max; i += scale.y_mul) { + g.push(mlNs("line", {x1: 0, x2: x2, y1: height-i*div, y2: height-i*div, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"})) + } + div = x2 / scale.ts_dur + for(let i = 0; i <= scale.ts_dur; i++) { + if((i + scale.ts_start) % scale.x_mul == 0) { + g.push(mlNs("line", {x1: (i*div), x2: (i*div), y1: 0, y2: height, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"})) + } } - document.getElementById(namePrefix + "Max").innerHTML = obj.max; - if (mRefresh < 5) - mRefresh = 5; - document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh; + return g } + function poly(x2, obj, scale) { + let pts = "" + let i = 0, first = -1, last = -1, lastVal = 0 + let div = scale.y_max / height + if(div == 0) + div = 1 + for (val of obj.value) { + if(val > 0) { + lastVal = val + pts += " " + String(i) + "," + String(height - val / div) + if(first < 0) + first = i + last = i + } + i += 2 + } + let pts2 = pts + " " + String(last) + "," + String(height) + pts2 += " " + String(first) + "," + String(height) + elm = [ + mlNs("polyline", {stroke: "url(#gLine)", fill: "none", points: pts}), + mlNs("polyline", {stroke: "none", fill: "url(#gFill)", points: pts2}), + mlNs("text", {x: i*.8, y: 10}, "{#MAXIMUM}: " + String(obj.max) + "W"), + mlNs("text", {x: i*.8, y: 25}, "{#LAST_VALUE}: " + String(lastVal) + "W") + ] + + if(undefined !== obj.yld) + elm.push(mlNs("text", {x: i*.8, y: 40}, "{#YIELD_DAY}: " + String(obj.yld) + "kWh")) + + return elm; + } + + function parsePowerHistory(obj){ + if(once) { + once = false + parseNav(obj.generic) + parseESP(obj.generic) + parseRssi(obj.generic) + parseTitle(obj.generic) + window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", obj.refresh * 1000) + setTimeout(() => { + window.setInterval("getAjax('/api/powerHistoryDay', parsePowerHistoryDay)", obj.refresh * 1000) + }, 200) + /*IF_ENABLE_HISTORY_YIELD_PER_DAY*/ + setTimeout(() => { + window.setInterval("getAjax('/api/yieldDayHistory', parseYieldDayHistory)", obj.refresh * 1000) + }, 400) + /*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/ + } + if (null != obj) { + let svg = setupSvg("ph", obj); + document.getElementById("pwrChart").replaceChildren(svg); + setTimeout(() => { getAjax("/api/powerHistoryDay", parsePowerHistoryDay) }, 50); + } + } + + function parsePowerHistoryDay(obj) { if (null != obj) { - parseNav(obj.generic); - parseHistory(obj,"pwr", pwrExeOnce) - document.getElementById("pwrLast").innerHTML = mLastValue - document.getElementById("pwrMaxDay").innerHTML = obj.maxDay + let svg = setupSvg("phDay", obj); + document.getElementById("pwrDayChart").replaceChildren(svg); + /*IF_ENABLE_HISTORY_YIELD_PER_DAY*/ + setTimeout(() => { getAjax("/api/yieldDayHistory", parseYieldDayHistory) }, 50); + /*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/ } - if (pwrExeOnce) { - pwrExeOnce = false; - window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000); + } + + /*IF_ENABLE_HISTORY_YIELD_PER_DAY*/ + function parseYieldDayHistory(obj) { + if (null != obj) { + let svg = setupSvg("phDay", obj); + document.getElementById("pwrDayChart").replaceChildren(svg); } } + /*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/ getAjax("/api/powerHistory", parsePowerHistory); </script> diff --git a/src/web/html/includes/footer.html b/src/web/html/includes/footer.html index 6aa89673..ce0c6852 100644 --- a/src/web/html/includes/footer.html +++ b/src/web/html/includes/footer.html @@ -1,6 +1,6 @@ <div id="footer"> <div class="left"> - <a href="https://ahoydtu.de" target="_blank">AhoyDTU © 2024</a> + <a href="https://ahoydtu.de" target="_blank">AhoyDTU © 2024</a> <ul> <li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li> <li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li> diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html index 447bf411..4718e257 100644 --- a/src/web/html/includes/nav.html +++ b/src/web/html/includes/nav.html @@ -7,18 +7,21 @@ </a> <div id="topnav" class="mobile"> <a id="nav3" class="hide" href="/live?v={#VERSION}">{#NAV_LIVE}</a> + <!--IF_ENABLE_HISTORY--> <a id="nav11" class="acitve" href="/history?v={#VERSION}">{#NAV_HISTORY}</a> + <!--ENDIF_ENABLE_HISTORY--> <a id="nav4" class="hide" href="/serial?v={#VERSION}">{#NAV_WEBSERIAL}</a> <a id="nav5" class="hide" href="/setup?v={#VERSION}">{#NAV_SETTINGS}</a> - <span class="seperator"></span> + <span class="separator"></span> <a id="nav6" class="hide" href="/update?v={#VERSION}">Update</a> <a id="nav7" class="hide" href="/system?v={#VERSION}">System</a> - <span class="seperator"></span> + <span class="separator"></span> <a id="nav8" href="/api" target="_blank">REST API</a> - <a id="nav9" href="https://ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</a> + <a id="nav9" href="https://docs.ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</a> + <a id="nav13" href="https://ahoydtu.de" target="_blank">Website</a> <a id="nav10" href="/about?v={#VERSION}">{#NAV_ABOUT}</a> <a id="nav12" href="#" class="hide" target="_blank">Custom Link</a> - <span class="seperator"></span> + <span class="separator"></span> <a id="nav0" class="hide" href="/login">Login</a> <a id="nav1" class="hide" href="/logout">Logout</a> </div> diff --git a/src/web/html/index.html b/src/web/html/index.html index 2611db5b..ee78ac8e 100644 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -14,6 +14,7 @@ </p> <p> <span class="des">System Infos:</span> + <div id="total"></div> <div id="iv"></div> <div class="hr"></div> <div id="warn_info"></div> @@ -23,9 +24,9 @@ <h3>{#SUPPORT}:</h3> <ul> <li><a href="https://github.com/lumapu/ahoy/blob/main/src/CHANGES.md" target="_blank">{#CHANGELOG}</a></li> - <li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB">Discord</a></li> + <li>{#DISCUSS} <a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li> <li>{#REPORT} <a href="https://github.com/lumapu/ahoy/issues" target="_blank">{#ISSUES}</a></li> - <li>{#CONTRIBUTE} <a href="https://github.com/lumapu/ahoy/blob/main/User_Manual.md" target="_blank">{#DOCUMENTATION}</a></li> + <li>{#CONTRIBUTE} <a href="https://docs.ahoydtu.de" target="_blank">{#DOCUMENTATION}</a></li> <li><a href="https://fw.ahoydtu.de/fw/dev/" target="_blank">Download</a> & Test {#DEV_FIRMWARE}, <a href="https://github.com/lumapu/ahoy/blob/development03/src/CHANGES.md" target="_blank">{#DEV_CHANGELOG}</a></li> <li>{#DON_MAKE} <a href="https://paypal.me/lupusch" target="_blank">{#DONATION}</a></li> </ul> @@ -56,8 +57,10 @@ } function parseGeneric(obj) { - if(exeOnce) + if(exeOnce) { parseESP(obj) + parseTitle(obj) + } parseRssi(obj) } @@ -70,9 +73,9 @@ var min = parseInt(up / 60) % 60; var sec = up % 60; var e = document.getElementById("uptime"); - e.innerHTML = days + " Day"; + e.innerHTML = days + " {#DAY}"; if(1 != days) - e.innerHTML += "s"; + e.innerHTML += "{#S}"; e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":" + ("0"+min).substr(-2) + ":" + ("0"+sec).substr(-2); @@ -111,6 +114,10 @@ function parseIv(obj, ts) { var p = div(["none"]); + var total = 0; + var count = 0; + var mobile = window.screen.width < 470; + for(var i of obj) { var icon = iconSuccess; var cl = "icon-success"; @@ -126,20 +133,28 @@ } else if(0 == i["ts_last_success"]) { avail = "{#AVAIL_NO_DATA}"; } else { - avail = "{#AVAIL} "; + if (!mobile) + avail = "{#AVAIL} "; if(false == i["is_producing"]) avail += "{#NOT_PRODUCING}"; else { icon = iconSuccessFull; - avail += "{#PRODUCING} " + i.cur_pwr + "W"; + avail += "{#PRODUCING} " + i.cur_pwr + " W"; + total += i.cur_pwr; + count += 1; } } + var text; + if (mobile) + text = "#"; + else + text = "{#INVERTER} #"; p.append( - svg(icon, 30, 30, "icon " + cl), - span("{#INVERTER} #" + i["id"] + ": " + i["name"] + " {#IS} " + avail), - br() - ); + svg(icon, 30, 30, "icon " + cl), + span(text + i["id"] + ": " + i["name"] + " {#IS} " + avail), + br() + ); if(false == i["is_avail"]) { if(i["ts_last_success"] > 0) { @@ -149,6 +164,13 @@ } } document.getElementById("iv").replaceChildren(p); + + if (count > 1) { + var t = div(["none"]); + t.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Total: " + Math.round(total).toLocaleString() + " W"), br()); + document.getElementById("total").replaceChildren(t); + document.getElementById("total").appendChild(div(["hr"])); + } } function parseWarn(warn) { @@ -165,7 +187,7 @@ p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#UPDATE_AVAIL}: " + release), br()); else if(getVerInt("{#VERSION}") > getVerInt(release)) p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#USING_DEV_VERSION} {#VERSION}. {#DEV_ISSUE_RELEASE_VERSION}: " + release), br()); - else + else p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("{#RELEASE_INSTALLED}: " + release), br()); } diff --git a/src/web/html/serial.html b/src/web/html/serial.html index 83e614c8..39ba0ac2 100644 --- a/src/web/html/serial.html +++ b/src/web/html/serial.html @@ -35,15 +35,16 @@ var hrs = parseInt(up / 3600) % 24; var min = parseInt(up / 60) % 60; var sec = up % 60; - document.getElementById("uptime").innerHTML = days + " Days, " + document.getElementById("uptime").innerHTML = days + " {#DAYS}, " + ("0"+hrs).substr(-2) + ":" + ("0"+min).substr(-2) + ":" + ("0"+sec).substr(-2); - parseRssi(obj); + parseRssi(obj) if(true == exeOnce) { - parseNav(obj); - parseESP(obj); + parseNav(obj) + parseESP(obj) + parseTitle(obj) window.setInterval("getAjax('/api/generic', parseGeneric)", 5000); exeOnce = false; setTimeOffset(); @@ -65,7 +66,7 @@ }); document.getElementById("scroll").addEventListener("click", function() { mAutoScroll = !mAutoScroll; - this.value = (mAutoScroll) ? "autoscroll" : "manual scroll"; + this.value = (mAutoScroll) ? "{#BTN_AUTOSCROLL}" : "{#BTN_MANUALSCROLL}"; }); document.getElementById("copy").addEventListener("click", function() { con.value = version + " - " + build + "\n---------------\n" + con.value; @@ -80,10 +81,10 @@ try { return document.execCommand("copy"); // Security exception may be thrown by some browsers. } catch (ex) { - alert("Copy to clipboard failed" + ex); + alert("{#CLIPBOARD_FAILED} " + ex); } finally { document.body.removeChild(ta); - alert("Copied to clipboard"); + alert("{#COPIED_TO_CLIPBOARD}"); } } }); diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 47d935b9..2b59146c 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -52,28 +52,17 @@ <div class="s_content"> <fieldset class="mb-2"> <legend class="des">WiFi</legend> - <div class="row mb-3"> <div class="col-12 col-sm-3 my-2">{#AP_PWD}</div> <div class="col-12 col-sm-9"><input type="text" name="ap_pwd" minlength="8" /></div> </div> - <div class="row mb-3"> - <div class="col-12 col-sm-3 my-2">{#SEARCH_NETWORKS}</div> - <div class="col-12 col-sm-9"><input type="button" name="scanbtn" id="scanbtn" class="btn" value="{#BTN_SCAN}" onclick="scan()"/></div> - </div> - <div class="row mb-2 mb-sm-3"> - <div class="col-12 col-sm-3 my-2">{#AVAIL_NETWORKS}</div> + <div class="col-12 col-sm-3 my-2">SSID</div> <div class="col-12 col-sm-9"> - <select name="networks" id="networks" onChange="selNet()"> - <option value="-1" selected disabled hidden>{#NETWORK_NOT_SCANNED}</option> - </select> + <input type="text" name="ssid"/><br/> + <a href="/wizard">{#SCAN_WIFI}</a> </div> </div> - <div class="row mb-2 mb-sm-3"> - <div class="col-12 col-sm-3 my-2">SSID</div> - <div class="col-12 col-sm-9"><input type="text" name="ssid"/></div> - </div> <div class="row mb-2 mb-sm-3"> <div class="col-12 col-sm-3">{#SSID_HIDDEN}</div> <div class="col-12 col-sm-9"><input type="checkbox" name="hidd"/></div> @@ -138,7 +127,11 @@ <div class="col-4"><input type="checkbox" name="invRstMid"/></div> </div> <div class="row mb-3"> - <div class="col-8 mb-2">{#INV_PAUSE_SUNSET}</div> + <div class="col-8 mb-2">{#INV_RESET_SUNRISE}</div> + <div class="col-4"><input type="checkbox" name="invRstComStart"/></div> + </div> + <div class="row mb-3"> + <div class="col-8 mb-2">{#INV_RESET_SUNSET}</div> <div class="col-4"><input type="checkbox" name="invRstComStop"/></div> </div> <div class="row mb-3"> @@ -146,7 +139,7 @@ <div class="col-4"><input type="checkbox" name="invRstNotAvail"/></div> </div> <div class="row mb-3"> - <div class="col-8">{#INV_RESET_MAX_MIDNIGHT}</div> + <div class="col-8">{#INV_RESET_MAX_VALUES}</div> <div class="col-4"><input type="checkbox" name="invRstMaxMid"/></div> </div> <div class="row mb-3"> @@ -157,10 +150,6 @@ <div class="col-8">{#INV_READ_GRID_PROFILE}</div> <div class="col-4"><input type="checkbox" name="rdGrid"/></div> </div> - <div class="row mb-3"> - <div class="col-8">{#INV_YIELD_EFF}</div> - <div class="col-4"><input type="number" name="yldEff" step="any"/></div> - </div> </fieldset> </div> @@ -246,6 +235,10 @@ <div class="col-12 col-sm-3 my-2">Topic</div> <div class="col-12 col-sm-9"><input type="text" name="mqttTopic" pattern="[\-\+A-Za-z0-9\.\/#\$%&=_]+" title="Invalid input" /></div> </div> + <div class="row mb-3"> + <div class="col-12 col-sm-3 my-2">{#MQTT_JSON}</div> + <div class="col-12 col-sm-9"><input type="checkbox" name="mqttJson" /></div> + </div> <p class="des">{#MQTT_NOTE}</p> <div class="row mb-3"> <div class="col-12 col-sm-3 my-2">{#INTERVAL}</div> @@ -258,6 +251,10 @@ <span id="apiResultMqtt"></span> </div> </div> + <div class="row mb-3"> + <div class="col-8 col-sm-3">{#RETAIN}</div> + <div class="col-4 col-sm-9"><input type="checkbox" name="retain"/></div> + </div> </fieldset> </div> @@ -274,9 +271,13 @@ <p class="des">{#RADIO} (CMT2300A)</p> <div id="cmt"></div> <!--ENDIF_ESP32--> + <!--IF_ETHERNET--> + <p class="des">Ethernet</p> + <div id="eth"></div> + <!--ENDIF_ETHERNET--> </fieldset> </div> - + <!--IF_PLUGIN_DISPLAY--> <button type="button" class="s_collapsible">{#DISPLAY_CONFIG}</button> <div class="s_content"> <fieldset class="mb-4"> @@ -290,7 +291,7 @@ <div id="screenSaver"></div> <div class="row mb-3" id="luminanceOption"> <div class="col-12 col-sm-3 my-2">{#DISP_LUMINANCE}</div> - <div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="255"></select></div> + <div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="255"></div> </div> <p class="des">{#DISP_PINOUT}</p> <div id="dispPins"></div> @@ -299,12 +300,13 @@ <p class="des">{#GRAPH_OPTIONS}</p> <div class="row mb-3"> <div class="col-12 col-sm-3 my-2">{#GRAPH_SHOW_RATIO}</div> - <div class="col-12 col-sm-9"><input type="number" name="disp_graph_ratio" min="0" max="100"></select></div> + <div class="col-12 col-sm-9"><input type="number" name="disp_graph_ratio" min="0" max="100"></div> </div> <div id="graphSize"></div> </div> </fieldset> </div> + <!--ENDIF_PLUGIN_DISPLAY--> <div class="row mb-4 mt-4"> <div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div> @@ -324,8 +326,8 @@ <div class="col-12 col-sm-9"> <form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8"> <div class="row"> - <div class="col-12 col-sm-8 my-2"><input type="file" name="upload"></div> - <div class="col-12 col-sm-4 my-2"><input type="button" class="btn" value="Import" onclick="hide()"></div> + <div class="col-12 col-sm-8 my-2"><input type="file" id="importFileInput" name="upload"></div> + <div class="col-12 col-sm-4 my-2"><input type="button" id="importButton" class="btn" value="Import" onclick="hide()"></div> </div> </form> </div> @@ -345,6 +347,7 @@ var maxInv = 0; var ts = 0; + /*IF_ESP8266*/ var esp8266pins = [ [255, "{#PIN_OFF}"], [0, "D3 (GPIO0)"], @@ -365,6 +368,7 @@ [15, "D8 (GPIO15)"], [16, "D0 (GPIO16 - {#PIN_NO_IRQ})"] ]; + /*ENDIF_ESP8266*/ /*IF_ESP32*/ var esp32pins = [ @@ -396,6 +400,7 @@ [36, "VP (GPIO36, {#PIN_INPUT_ONLY})"], [39, "VN (GPIO39, {#PIN_INPUT_ONLY})"] ]; + /*IF_ESP32-S2*/ var esp32sXpins = [ [255, "off / default"], [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], @@ -444,6 +449,58 @@ [47, "GPIO47"], [48, "GPIO48"], ]; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + var esp32sXpins = [ + [255, "off / default"], + [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], + [1, "GPIO1"], + [2, "GPIO2"], + [3, "GPIO3"], + [4, "GPIO4 (CMT CSB)"], + [5, "GPIO5 (CMT SDIO)"], + [6, "GPIO6 (CMT SCLK)"], + [7, "GPIO7"], + [8, "GPIO8 (CMT GPIO3)"], + [9, "GPIO9 (DATA display)"], + [10, "GPIO10 (SCK display)"], + [11, "GPIO11 (CS display)"], + [12, "GPIO12 (DC display)"], + [13, "GPIO13 (RST display)"], + [14, "GPIO14 (BUSY display)"], + [15, "GPIO15"], + [16, "GPIO16"], + [17, "GPIO17"], + [18, "GPIO18"], + [19, "GPIO19 ({#PIN_DONT_USE} - USB-)"], + [20, "GPIO20 ({#PIN_DONT_USE} - USB+)"], + [21, "GPIO21 (CMT FCSB)"], + [26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"], + [27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"], + [28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"], + [29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"], + [30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"], + [31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"], + [32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"], + [33, "GPIO33 (not exposed on S3-WROOM modules)"], + [34, "GPIO34 (not exposed on S3-WROOM modules)"], + [35, "GPIO35 (MOSI NRF24)"], + [36, "GPIO36 (SCK NRF24)"], + [37, "GPIO37 (CSN NRF24)"], + [38, "GPIO38 (CE NRF24)"], + [39, "GPIO39 (SCK ETH)"], + [40, "GPIO40 (MOSI ETH)"], + [41, "GPIO41 (MISO ETH)"], + [42, "GPIO42 (CS ETH)"], + [43, "GPIO43 (RST ETH)"], + [44, "GPIO44 (INT ETH)"], + [45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"], + [46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"], + [47, "GPIO47 (IRQ NRF24)"], + [48, "GPIO48 (MISO NRF24)"], + ]; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ var esp32c3pins = [ [255, "off / default"], [0, "GPIO0"], @@ -469,6 +526,7 @@ [20, "GPIO20 (RX)"], [21, "GPIO21 (TX)"], ]; + /*ENDIF_ESP32-C3*/ /*ENDIF_ESP32*/ var nrfPa = [ [0, "MIN ({#PIN_RECOMMENDED})"], @@ -549,12 +607,6 @@ setTimeout(function() {getAjax('/api/index', apiCbNtp2)}, 2000) } - function scan() { - var obj = {cmd: "scan_wifi", token: "*"} - getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj)); - setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 5000); - } - function syncTime() { var obj = {cmd: "sync_ntp", token: "*"} getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj)) @@ -566,6 +618,22 @@ getAjax("/api/setup", apiCbMqtt, "POST", JSON.stringify(obj)); } + document.addEventListener('DOMContentLoaded', () => { + const fileInput = document.querySelector('#importFileInput'); + const button = document.querySelector('#importButton'); + button.disabled = true; + button.title = "Please select a file first"; + fileInput.addEventListener('change', () => { + if (fileInput.value) { + button.disabled = false; + button.title = ""; + } else { + button.disabled = true; + button.title = "Please select a file first"; + } + }); + }); + function hide() { document.getElementById("form").submit(); var e = document.getElementById("content"); @@ -605,53 +673,55 @@ } function ivGlob(obj) { - for(var i of [["invInterval", "interval"], ["yldEff", "yldEff"]]) + for(var i of [["invInterval", "interval"]]) document.getElementsByName(i[0])[0].value = obj[i[1]]; - for(var i of ["Mid", "ComStop", "NotAvail", "MaxMid"]) + for(var i of ["Mid", "ComStop", "ComStart", "NotAvail", "MaxMid"]) document.getElementsByName("invRst"+i)[0].checked = obj["rst" + i]; document.getElementsByName("strtWthtTm")[0].checked = obj["strtWthtTm"]; document.getElementsByName("rdGrid")[0].checked = obj["rdGrid"]; } function parseSys(obj) { - for(var i of [["device", "device_name"], ["ssid", "ssid"], ["ap_pwd", "ap_pwd"]]) - document.getElementsByName(i[0])[0].value = obj[i[1]]; - document.getElementsByName("hidd")[0].checked = obj["hidd"]; - document.getElementsByName("darkMode")[0].checked = obj["dark_mode"]; - document.getElementsByName("schedReboot")[0].checked = obj["sched_reboot"]; + document.getElementsByName("device")[0].value = obj.device_name; + for(var i of [["ssid", "ssid"], ["ap_pwd", "ap_pwd"]]) + document.getElementsByName(i[0])[0].value = obj.network[i[1]]; + document.getElementsByName("hidd")[0].checked = obj.network.hidd; + + document.getElementsByName("darkMode")[0].checked = obj.dark_mode; + document.getElementsByName("schedReboot")[0].checked = obj.sched_reboot; e = document.getElementsByName("adminpwd")[0]; - if(!obj["pwd_set"]) + if(!obj.pwd_set) e.value = ""; var d = document.getElementById("prot_mask"); var a = ["Index", "{#NAV_LIVE}", "{#NAV_WEBSERIAL}", "{#NAV_SETTINGS}", "Update", "System", "{#NAV_HISTORY}"]; var el = []; for(var i = 0; i < 7; i++) { - var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i)); + var chk = ((obj.prot_mask & (1 << i)) == (1 << i)); el.push(mlCb("protMask" + i, a[i], chk)) } d.append(...el); - - var tz = [] - for(i = 0; i < 24; i += 0.5) - tz.push([i, ((i-12 > 0) ? "+" : "") + String(i-12)]); - document.getElementById("timezone").append(sel("timezone", tz, obj.timezone + 12)) - var region = [[0, "Europe (860 - 870 MHz)"], [1, "USA, Indonesia (905 - 925 MHz)"], [2, "Brazil (915 - 928 MHz)"]] - document.getElementById("region").append(sel("region", region, obj.region)) - } function parseGeneric(obj) { - parseNav(obj); - parseESP(obj); - parseRssi(obj); + parseNav(obj) + parseESP(obj) + parseRssi(obj) + parseTitle(obj) if(0 != obj.cst_lnk.length) { document.getElementsByName("cstLnk")[0].value = obj.cst_lnk document.getElementsByName("cstLnkTxt")[0].value = obj.cst_lnk_txt } - ts = obj["ts_now"]; + ts = obj.ts_now; window.setInterval("tick()", 1000); + + var tz = [] + for(i = 0; i < 24; i += 0.5) + tz.push([i, ((i-12 > 0) ? "+" : "") + String(i-12)]); + document.getElementById("timezone").append(sel("timezone", tz, obj.timezone + 12)) + var region = [[0, "Europe (860 - 870 MHz)"], [1, "USA, Indonesia (905 - 925 MHz)"], [2, "Brazil (915 - 928 MHz)"]] + document.getElementById("region").append(sel("region", region, obj.region)) } function parseStaticIp(obj) { @@ -730,7 +800,7 @@ cbEn.checked = (obj.enabled); cbDisNightCom.checked = (obj.disnightcom); - var ser = ml("input", {name: "ser", class: "text", type: "number", max: 138999999999, value: obj.serial}, null); + var ser = ml("input", {name: "ser", class: "text", type: "text", pattern: "[0-9a-fA-F]{12}", value: obj.serial}, null); var html = ml("div", {}, [ tabs(["{#TAB_GENERAL}", "{#TAB_INPUTS}", "{#TAB_RADIO}", "{#TAB_ADVANCED}"]), ml("div", {id: "div{#TAB_GENERAL}", class: "tab-content"}, [ @@ -786,7 +856,8 @@ case 0x1000: nrf = true; break; case 0x1100: switch(sn & 0x000f) { - case 0x0004: nrf = false; break; + case 0x0004: + case 0x0005: nrf = false; break; default: nrf = true; break; } break; @@ -802,11 +873,16 @@ ser.dispatchEvent(new Event('change')); function ivSave() { - var o = new Object(); + var o = {} o.cmd = "save_iv" o.token = "*" o.id = obj.id - o.ser = parseInt(document.getElementsByName("ser")[0].value, 16); + + let sn = document.getElementsByName("ser")[0].value + if(sn[0] == 'A') + sn = convHerf(sn) + o.ser = parseInt(sn, 16) + o.name = document.getElementsByName("name")[0].value; o.en = document.getElementsByName("enable")[0].checked; o.ch = []; @@ -826,6 +902,30 @@ getAjax("/api/setup", cb, "POST", JSON.stringify(o)); } + function convHerf(sn) { + let sn_int = 0n; + const CHARS = "0123456789ABCDEFGHJKLMNPRSTUVWXY"; + + for (let i = 0; i < 9; ++i) { + const pos = CHARS.indexOf(sn[i]) + const shift = 42 - 5 * i - (i <= 2 ? 0 : 2) + sn_int |= BigInt(pos) << BigInt(shift) + } + + let first4Hex = (sn_int >> 32n) & 0xFFFFn + + if (first4Hex === 0x2841n) + first4Hex = 0x1121n + else if (first4Hex === 0x2821n) + first4Hex = 0x1141n + else if (first4Hex === 0x2801n) + first4Hex = 0x1161n + + sn_int = (sn_int & ~(0xFFFFn << 32n)) | (first4Hex << 32n); + + return sn_int.toString(16) + } + function cb(obj2) { var e = document.getElementById("res"); if(!obj2.success) @@ -847,6 +947,7 @@ function del() { var o = new Object(); o.cmd = "save_iv"; + o.token = "*" o.id = obj.id; o.ser = 0; o.name = ""; @@ -873,6 +974,8 @@ function parseMqtt(obj) { for(var i of [["Addr", "broker"], ["Port", "port"], ["ClientId", "clientId"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]]) document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]]; + document.getElementsByName("mqttJson")[0].checked = obj["json"]; + document.getElementsByName("retain")[0].checked = obj.retain } function parseNtp(obj) { @@ -891,13 +994,21 @@ } } - function parsePinout(obj, type, system) { + function parsePinout(obj) { var e = document.getElementById("pinout"); - var pinList = esp8266pins; /*IF_ESP32*/ var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; - else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*IF_ESP32-S2*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = esp32c3pins; + /*ENDIF_ESP32-C3*/ + /*ELSE*/ + var pinList = esp8266pins; /*ENDIF_ESP32*/ pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']]; for(p of pins) { @@ -924,16 +1035,24 @@ ) } - function parseNrfRadio(obj, objPin, type, system) { + function parseNrfRadio(obj, objPin) { var e = document.getElementById("rf24"); var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox"); en.checked = obj["en"]; - var pinList = esp8266pins; /*IF_ESP32*/ var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; - else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*IF_ESP32-S2*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = esp32c3pins; + /*ENDIF_ESP32-C3*/ + /*ELSE*/ + var pinList = esp8266pins; /*ENDIF_ESP32*/ e.replaceChildren ( @@ -943,11 +1062,11 @@ ]) ); - if ("ESP8266" == type) { - pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']]; - } else { - pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso']]; - } + /*IF_ESP32*/ + var pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq'], ['sclk', 'pinSclk'], ['mosi', 'pinMosi'], ['miso', 'pinMiso']]; + /*ELSE*/ + var pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']]; + /*ENDIF_ESP32*/ for(p of pins) { e.append( ml("div", {class: "row mb-3"}, [ @@ -961,15 +1080,21 @@ } /*IF_ESP32*/ - function parseCmtRadio(obj, type, system) { + function parseCmtRadio(obj) { var e = document.getElementById("cmt"); var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox"); var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; - else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*IF_ESP32-S2*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = esp32c3pins; + /*ENDIF_ESP32-C3*/ en.checked = obj["en"]; - e.replaceChildren ( ml("div", {class: "row mb-3"}, [ ml("div", {class: "col-8 col-sm-3 my-2"}, "{#CMT_ENABLE}"), @@ -996,6 +1121,42 @@ } /*ENDIF_ESP32*/ + /*IF_ETHERNET*/ + function parseEth(obj) { + var e = document.getElementById("eth"); + var en = inp("ethEn", null, null, ["cb"], "ethEn", "checkbox"); + var pinList = esp32pins; + /*IF_ESP32-S2*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = esp32c3pins; + /*ENDIF_ESP32-C3*/ + + en.checked = obj["en"]; + e.replaceChildren ( + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-8 col-sm-3 my-2"}, "{#ETH_ENABLE}"), + ml("div", {class: "col-4 col-sm-9"}, en) + ]) + ); + pins = [['cs', 'ethCs'], ['sclk', 'ethSclk'], ['miso', 'ethMiso'], ['mosi', 'ethMosi'], ['irq', 'ethIrq'], ['reset', 'ethRst']]; + for(p of pins) { + e.append( + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), + ml("div", {class: "col-12 col-sm-9"}, + sel(p[1], pinList, obj[p[0]]) + ) + ]) + ); + } + } + /*ENDIF_ETHERNET*/ + function parseSerial(obj) { var e = document.getElementById("serialCb") var l = [["serEn", "show_live_data", "{#LOG_PRINT_INVERTER_DATA}"], ["serDbg", "debug", "{#LOG_SERIAL_DEBUG}"], ["priv", "priv", "{#LOG_PRIVACY_MODE}"], ["wholeTrace", "wholeTrace", "{#LOG_PRINT_TRACES}"], ["log2mqtt", "log2mqtt", "{#LOG_TO_MQTT}"]] @@ -1011,12 +1172,21 @@ } } + /*IF_PLUGIN_DISPLAY*/ function parseDisplay(obj, type, system) { - var pinList = esp8266pins; /*IF_ESP32*/ var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; - else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*IF_ESP32-S2*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = esp32c3pins; + /*ENDIF_ESP32-C3*/ + /*ELSE*/ + var pinList = esp8266pins; /*ENDIF_ESP32*/ for(var i of ["disp_pwr"]) @@ -1152,6 +1322,7 @@ setHide("screenSaver", !optionsMap.get(dispType)[2]); setHide("pirPin", !(optionsMap.get(dispType)[2] && (screenSaver==2))); // show pir pin only for motion screensaver } + /*ENDIF_PLUGIN_DISPLAY*/ function tick() { document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000))); @@ -1159,42 +1330,28 @@ function parse(root) { if(null != root) { - parseGeneric(root["generic"]); - parseSys(root["system"]); - parseStaticIp(root["static_ip"]); - parseMqtt(root["mqtt"]); - parseNtp(root["ntp"]); - parseSun(root["sun"]); - parsePinout(root["pinout"], root["system"]["esp_type"], root["system"]); - parseNrfRadio(root["radioNrf"], root["pinout"], root["system"]["esp_type"], root["system"]); + parseGeneric(root.generic); + parseSys(root.system); + parseStaticIp(root.static_ip); + parseMqtt(root.mqtt); + parseNtp(root.ntp); + parseSun(root.sun); + parsePinout(root.pinout); + parseNrfRadio(root.radioNrf, root.pinout); /*IF_ESP32*/ - parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]); + parseCmtRadio(root.radioCmt); /*ENDIF_ESP32*/ - parseSerial(root["serial"]); - parseDisplay(root["display"], root["system"]["esp_type"], root["system"]); + /*IF_ETHERNET*/ + parseEth(root.eth) + /*ENDIF_ETHERNET*/ + parseSerial(root.serial); + /*IF_PLUGIN_DISPLAY*/ + parseDisplay(root.display, root.system.esp_type, root.system); + /*ENDIF_PLUGIN_DISPLAY*/ getAjax("/api/inverter/list", parseIv); } } - function listNetworks(root) { - var s = document.getElementById("networks"); - selDelAllOpt(s); - if(root["networks"].length > 0) { - s.appendChild(opt("-1", "{#NETWORK_PLEASE_SELECT}")); - for(i = 0; i < root["networks"].length; i++) { - s.appendChild(opt(root["networks"][i]["ssid"], root["networks"][i]["ssid"] + " (" + root["networks"][i]["rssi"] + " dBm)")); - } - } else - s.appendChild(opt("-1", "{#NO_NETWORK_FOUND}")); - } - - function selNet() { - var s = document.getElementById("networks"); - var e = document.getElementsByName("ssid")[0]; - if(-1 != s.value) - e.value = s.value; - } - getAjax("/api/setup", parse); </script> </body> diff --git a/src/web/html/style.css b/src/web/html/style.css index 2d6a03c7..6f05ea77 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -33,13 +33,17 @@ textarea { color: var(--fg2); } -svg rect {fill: #00A;} -svg.chart { - background: #f2f2f2; - border: 2px solid gray; - padding: 1px; +svg polyline { + fill-opacity: .5; + stroke-width: 1; +} + +svg text { + font-size: x-small; + fill: var(--chart-text); } + div.chartDivContainer { padding: 1px; margin: 1px; @@ -139,7 +143,7 @@ svg.icon { background-color: var(--nav-active); } -span.seperator { +span.separator { width: 100%; height: 1px; margin: 5px 0 5px; @@ -559,7 +563,13 @@ input.btn { cursor: pointer; } -input.btn:hover { +input.btn:disabled { + background-color: var(--primary-disabled); + color: #888; + cursor: not-allowed; +} + +input.btn:not(:disabled):hover { background-color: #044e86; } @@ -677,7 +687,7 @@ div.hr { border-radius: 3px; display: inline-block; position: absolute; - transform: translate(-50%,-100%); + transform: translate(-50%,-50%); margin:0 auto; color: var(--fg2); min-width: 100px; @@ -748,6 +758,7 @@ div.hr { font-family: inherit; cursor: pointer; padding: 0; + color: var(--fg); } button.close { @@ -848,3 +859,16 @@ ul { height: 100%; overflow: auto; } + +.progress { + display: flex; + height: 1rem; + overflow: hidden; + background-color: #e9ecef; + border-radius: .25rem; +} + +.progress-bar { + display: flex; + background-color: var(--primary); +} diff --git a/src/web/html/system.html b/src/web/html/system.html index a646e8b8..477b3a7e 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -8,29 +8,44 @@ {#HTML_NAV} <div id="wrapper"> <div id="content"> - <div id="info" class="col-sm-12 col-md-6 mt-3"></div> + <div id="info" class="col-sm-12 col-md-10 mt-3"></div> <div id="html" class="mt-3 mb-3"></div> </div> </div> {#HTML_FOOTER} <script type="text/javascript"> function parseGeneric(obj) { - parseNav(obj); - parseESP(obj); - parseRssi(obj); + parseNav(obj) + parseESP(obj) + parseRssi(obj) + parseTitle(obj) + } + + function parseUptime(up) { + var days = parseInt(up / 86400) % 365 + var hrs = parseInt(up / 3600) % 24 + var min = parseInt(up / 60) % 60 + var sec = up % 60 + var str = days + " day" + if(1 != days) + str += "s" + str += ", " + ("0"+hrs).substr(-2) + ":" + + ("0"+min).substr(-2) + ":" + + ("0"+sec).substr(-2) + + return ml("span", {}, str) } function parseSysInfo(obj) { - const data = ["sdk", "cpu_freq", "chip_revision", - "chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime", - "flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag", - "max_free_blk", "version", "modules", "env", "core_version", "reboot_reason"]; - - lines = []; - for (const [key, value] of Object.entries(obj)) { - if(!data.includes(key) || (typeof value == 'undefined')) continue; - lines.push(tr(key.replace('_', ' '), value)); - } + lines = [ + tr("{#DEVICE_NAME}", obj.device_name), + tr("{#UPTIME}", parseUptime(obj.generic.ts_uptime)), + tr("{#REBOOT_REASON}", obj.chip.reboot_reason), + tr("{#ENVIRONMENT}", obj.generic.env + " ({#BUILD_OPTIONS}: " + obj.generic.modules + ")"), + tr("Version", obj.generic.version + " - " + obj.generic.build), + tr("Chip", "CPU: " + obj.chip.cpu_freq + "MHz, " + obj.chip.cores + " Core(s)"), + tr("Chip Model", obj.chip.model) + ] document.getElementById("info").append( headline("System Information"), @@ -46,10 +61,10 @@ function irqBadge(state) { switch(state) { - case 0: return badge(false, "{#UNKNOWN}", "warning"); break; - case 1: return badge(true, "{#TRUE}"); break; - default: return badge(false, "{#FALSE}"); break; - } + case 0: return badge(false, "unknown", "warning"); break; + case 1: return badge(true, "true"); break; + default: return badge(false, "false"); break; + } } function parseRadio(obj) { @@ -58,15 +73,15 @@ if(obj.radioNrf.en) { lines = [ tr("NRF24L01", badge(obj.radioNrf.isconnected, ((obj.radioNrf.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")), - tr("{#IRQ_WORKING}", irqBadge(obj.radioNrf.irqOk)), - tr("{#NRF24_DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"), - tr("DTU Radio ID", obj.radioNrf.sn) + tr("{#INTR_PIN_WORKING}", irqBadge(obj.radioNrf.irqOk)), + tr("NRF24 {#DATA_RATE}", dr[obj.radioNrf.dataRate] + "bps"), + tr("DTU {#RADIO} ID", obj.radioNrf.sn) ]; } else - lines = [tr("NRF24L01", badge(false, "{#NOT_ENABLED}"))]; + lines = [tr("NRF24L01", badge(false, "{#NOT} {#ENABLED}"))]; document.getElementById("info").append( - headline("{#NRF24_RADIO}"), + headline("{#RADIO} NRF24"), ml("table", {class: "table"}, ml("tbody", {}, lines) ) @@ -76,14 +91,14 @@ if(obj.radioCmt.en) { cmt = [ tr("CMT2300A", badge(obj.radioCmt.isconnected, ((obj.radioCmt.isconnected) ? "" : "{#NOT} ") + "{#CONNECTED}")), - tr("{#IRQ_WORKING}", irqBadge(obj.radioCmt.irqOk)), - tr("DTU Radio ID", obj.radioCmt.sn) + tr("{#INTR_PIN_WORKING}", irqBadge(obj.radioCmt.irqOk)), + tr("DTU {#RADIO} ID", obj.radioCmt.sn) ]; } else - cmt = [tr("CMT2300A", badge(false, "{#NOT_ENABLED}"))]; + cmt = [tr("CMT2300A", badge(false, "{#NOT} {#ENABLED}"))]; document.getElementById("info").append( - headline("{#CMT_RADIO}"), + headline("{#RADIO} CMT"), ml("table", {class: "table"}, ml("tbody", {}, cmt) ) @@ -91,16 +106,32 @@ /*ENDIF_ESP32*/ } + function parseNetwork(obj, gen) { + lines = [ + tr("{#CONNECTION}", ((obj.wired) ? "{#WIRED}" : "{#WIFI} (SSID: " + obj.ssid + ", RSSI: " + gen.wifi_rssi + ", CH: " + obj.wifi_channel + ")")), + tr("Hostname", gen.host), + tr("IP {#ADDRESS}", obj.ip), + tr("MAC {#ADDRESS}", obj.mac) + ] + + document.getElementById("info").append( + headline("{#NETWORK}"), + ml("table", {class: "table"}, + ml("tbody", {}, lines) + ) + ); + } + function parseMqtt(obj) { if(obj.enabled) { lines = [ - tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "{#TRUE}" : "{#FALSE}"))), + tr("{#CONNECTED}", badge(obj.connected, ((obj.connected) ? "true" : "false"))), tr("#TX", obj.tx_cnt), tr("#RX", obj.rx_cnt) - ]; + ] } else - lines = tr("{#ENABLED}", badge(false, "{#FALSE}")); + lines = tr("enabled", badge(false, "false")); document.getElementById("info").append( headline("MqTT"), @@ -110,17 +141,34 @@ ); } + function parseMemory(obj) { + lines = [ + tr("{#FLASH_SIZE}", obj.flash_size / 1024 / 1024 + "MB"), + tr("{#CONFIG_PARTITION} (" + Math.round(obj.par_used_spiffs / 1024) + "kB of " + obj.par_size_spiffs / 1024 + "kB)", progress(obj.par_used_spiffs / obj.par_size_spiffs * 100)), + tr("{#FIRMWARE_PARTITION} (" + Math.round(obj.par_used_app0 / 1024) + "kB of " + obj.par_size_app0 / 1024 + "kB)", progress(obj.par_used_app0 / obj.par_size_app0 * 100)), + tr("Heap (" + Math.round(obj.heap_free / 1024) + "kB of " + Math.round(obj.heap_total / 1024) + "kB)", progress(obj.heap_free / obj.heap_total * 100)), + tr("Heap {#MAX_FREE_BLOCK}", Math.round(obj.heap_max_free_blk / 1024) + "kB (Fragmentation: " + obj.heap_frag + ")") + ] + + document.getElementById("info").append( + headline("{#MEMORY}"), + ml("table", {class: "table"}, + ml("tbody", {}, lines) + ) + ); + } + function parseIndex(obj) { if(obj.ts_sunrise > 0) { document.getElementById("info").append( - headline("{#SUN}"), + headline("Sun"), ml("table", {class: "table"}, ml("tbody", {}, [ tr("{#SUNRISE}", new Date(obj.ts_sunrise * 1000).toLocaleString('de-DE')), tr("{#SUNSET}", new Date(obj.ts_sunset * 1000).toLocaleString('de-DE')), tr("{#COMMUNICATION_START}", new Date((obj.ts_sunrise + obj.ts_offsSr) * 1000).toLocaleString('de-DE')), tr("{#COMMUNICATION_STOP}", new Date((obj.ts_sunset + obj.ts_offsSs) * 1000).toLocaleString('de-DE')), - tr("{#NIGHT_BEHAVE}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning")) + tr("{#NIGHT_BEHAVIOR}", badge(obj.disNightComm, ((obj.disNightComm) ? "{#NOT}" : "") + " {#COMMUNICATING}", "warning")) ]) ) ); @@ -137,10 +185,12 @@ meta.content = obj.refresh + "; URL=" + obj.refresh_url; document.getElementsByTagName('head')[0].appendChild(meta); } else if(null != obj.system) { - parseRadio(obj.system); - parseMqtt(obj.system.mqtt); - parseSysInfo(obj.system); - getAjax('/api/index', parseIndex); + parseRadio(obj.system) + parseNetwork(obj.system.network, obj.system.generic) + parseMqtt(obj.system.mqtt) + parseMemory(obj.system.memory) + parseSysInfo(obj.system) + getAjax('/api/index', parseIndex) } document.getElementById("html").innerHTML = obj.html; } diff --git a/src/web/html/update.html b/src/web/html/update.html index 52ace5f1..4b670d45 100644 --- a/src/web/html/update.html +++ b/src/web/html/update.html @@ -12,22 +12,39 @@ <legend class="des">{#SELECT_FILE} (*.bin)</legend> <p>{#INSTALLED_VERSION}:<br/><span id="version" style="background-color: var(--input-bg); padding: 7px; display: block; margin: 3px;"></span></p> <form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8"> - <input type="file" name="update"> - <input type="button" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()"> + <input type="file" id="uploadFileInput" name="update"> + <input type="button" id="uploadButton" class="btn my-4" value="{#BTN_UPDATE}" onclick="hide()"> </form> </fieldset> <div class="row mt-4"> - <a href="https://fw.ahoydtu.de" target="_blank">{#DOWNLOADS}<a/> + <a href="https://fw.ahoydtu.de" target="_blank">{#DOWNLOADS}</a> </div> </div> </div> {#HTML_FOOTER} <script type="text/javascript"> + document.addEventListener('DOMContentLoaded', () => { + const fileInput = document.querySelector('#uploadFileInput'); + const button = document.querySelector('#uploadButton'); + button.disabled = true; + button.title = "Please select a file first"; + fileInput.addEventListener('change', () => { + if (fileInput.value) { + button.disabled = false; + button.title = ""; + } else { + button.disabled = true; + button.title = "Please select a file first"; + } + }); + }); + var env; function parseGeneric(obj) { parseNav(obj) parseESP(obj) parseRssi(obj) + parseTitle(obj) env = obj.env document.getElementById("version").innerHTML = "{#VERSION_FULL}_" + obj.env + ".bin" } diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 81962add..507a6f9f 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -21,6 +21,7 @@ var mNum = 0; var total = Array(6).fill(0); var tPwrAck; + var totalsRendered = false function getErrStr(code) { if("ERR_AUTH") return "{#ERR_AUTH}" @@ -33,10 +34,11 @@ function parseGeneric(obj) { if(true == exeOnce){ - parseNav(obj); - parseESP(obj); + parseNav(obj) + parseESP(obj) + parseTitle(obj) } - parseRssi(obj); + parseRssi(obj) } function numBig(val, unit, des) { @@ -54,11 +56,11 @@ ]); } - function numMid(val, unit, des, opt={class: "fs-6"}) { + function numMid(val, unit, des, opt={class: "row"}) { return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [ - ml("div", {class: "row"}, + ml("div", opt, ml("div", {class: "col"}, [ - ml("span", opt, String(Math.round(val * 100) / 100)), + ml("span", {class: "fs-6"}, String(Math.round(val * 100) / 100)), ml("span", {class: "fs-8 mx-1"}, unit) ]) ), @@ -74,6 +76,7 @@ for(var i = 0; i < 6; i++) { total[i] = Math.round(total[i] * 100) / 100; } + totalsRendered = true return ml("div", {class: "row mt-3 mb-5"}, ml("div", {class: "col"}, [ @@ -104,7 +107,6 @@ total[4] += obj.ch[0][8]; // P_DC total[5] += obj.ch[0][10]; // Q_AC } - total[3] += obj.ch[0][11]; // MAX P_AC total[1] += obj.ch[0][7]; // YieldDay total[2] += obj.ch[0][6]; // YieldTotal @@ -116,10 +118,11 @@ if(65535 != obj.power_limit_read) { pwrLimit = obj.power_limit_read + " %"; if(0 != obj.max_pwr) - pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + " W"; + pwrLimit += ", " + (obj.max_pwr * obj.power_limit_read / 100).toFixed(1) + " W"; } - var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000)); + var maxAcPwrDate = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000)) + var maxTempDate = toIsoDateStr(new Date(obj.ts_max_temp * 1000)) return ml("div", {class: "row mt-2"}, ml("div", {class: "col"}, [ ml("div", {class: "p-2 " + clh}, @@ -134,7 +137,7 @@ ml("div", {class: "col a-c"}, ml("span", { class: "pointer", onclick: function() { getAjax("/api/inverter/alarm/" + obj.id, parseIvAlarm); }}, ("{#ALARMS}: " + obj.alarm_cnt))), - ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5].toFixed(1)) + t.innerText) + ml("div", {class: "col a-r mx-2 mx-md-1 tooltip", data: (obj.ch[0][12] + t.innerText + "\n" + maxTempDate)}, String(obj.ch[0][5].toFixed(1)) + t.innerText) ]) ), ml("div", {class: "p-2 " + clbg}, [ @@ -145,7 +148,7 @@ ]), ml("div", {class: "hr"}), ml("div", {class: "row mt-2"},[ - numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "fs-6 tooltip", data: maxAcPwr}), + numMid(obj.ch[0][11], "W", "{#MAX_AC_POWER}", {class: "row tooltip", data: maxAcPwrDate}), numMid(obj.ch[0][8], "W", "{#DC_POWER}"), numMid(obj.ch[0][0], "V", "{#AC_VOLTAGE}"), numMid(obj.ch[0][1], "A", "{#AC_CURRENT}"), @@ -240,20 +243,18 @@ ]) ); - - var last = true; for(var i = obj.id + 1; i < ivEn.length; i++) { if((i != ivEn.length) && ivEn[i]) { - last = false; getAjax("/api/inverter/id/" + i, parseIv); - break; + return } } - if(last) { - if(mNum > 1) + + if(mNum > 1) { + if(!totalsRendered) mIvHtml.unshift(totals()); - document.getElementById("live").replaceChildren(...mIvHtml); } + document.getElementById("live").replaceChildren(...mIvHtml); } function parseIvAlarm(obj) { @@ -275,7 +276,7 @@ ml("div", {class: "col mt-3"}, String(a.str)), ml("div", {class: "col mt-3"}, String(a.code)), ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.start + offs) * 1000)))), - ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.end + offs) * 1000)))) + ml("div", {class: "col mt-3"}, (a.end == 0) ? "-" : String(toIsoTimeStr(new Date((a.end + offs) * 1000)))) ]) ); } @@ -514,7 +515,9 @@ ivEn = Object.values(Object.assign({}, obj["iv"])); mIvHtml = []; mNum = 0; + totalsRendered = false total.fill(0); + total[3] = obj.max_total_pwr for(var i = 0; i < obj.iv.length; i++) { if(obj.iv[i]) { getAjax("/api/inverter/id/" + i, parseIv); diff --git a/src/web/html/wizard.html b/src/web/html/wizard.html index 3df44dc4..199f66db 100644 --- a/src/web/html/wizard.html +++ b/src/web/html/wizard.html @@ -4,7 +4,7 @@ <title>{#NAV_WIZARD}</title> {#HTML_HEADER} </head> - <body> + <body onload="init()"> <div id="wrapper"> <div class="container d-flex aic jc"> <div id="con"></div> @@ -14,6 +14,166 @@ var v; var found = false; var c = document.getElementById("con"); + var redirIp = "http://192.168.4.1/index" + + /*IF_ESP32*/ + var pinList = [ + [255, "{#PIN_OFF}"], + [0, "GPIO0"], + [1, "TX (GPIO1)"], + [2, "GPIO2 (LED)"], + [3, "RX (GPIO3)"], + [4, "GPIO4"], + [5, "GPIO5"], + [12, "GPIO12 (HSPI MISO)"], + [13, "GPIO13 (HSPI MOSI)"], + [14, "GPIO14 (HSPI SCLK)"], + [15, "GPIO15"], + [16, "GPIO16"], + [17, "GPIO17"], + [18, "GPIO18 (VSPI SCLK)"], + [19, "GPIO19 (VSPI MISO)"], + [21, "GPIO21 (SDA)"], + [22, "GPIO22 (SCL)"], + [23, "GPIO23 (VSPI MOSI)"], + [25, "GPIO25"], + [26, "GPIO26"], + [27, "GPIO27"], + [32, "GPIO32"], + [33, "GPIO33"], + [34, "GPIO34 ({#PIN_INPUT_ONLY})"], + [35, "GPIO35 ({#PIN_INPUT_ONLY})"], + [36, "VP (GPIO36, {#PIN_INPUT_ONLY})"], + [39, "VN (GPIO39, {#PIN_INPUT_ONLY})"] + ]; + /*IF_ESP32-S2*/ + pinList = [ + [255, "off / default"], + [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], + [1, "GPIO1"], + [2, "GPIO2"], + [3, "GPIO3"], + [4, "GPIO4"], + [5, "GPIO5"], + [6, "GPIO6"], + [7, "GPIO7"], + [8, "GPIO8"], + [9, "GPIO9"], + [10, "GPIO10"], + [11, "GPIO11"], + [12, "GPIO12"], + [13, "GPIO13"], + [14, "GPIO14"], + [15, "GPIO15"], + [16, "GPIO16"], + [17, "GPIO17"], + [18, "GPIO18"], + [19, "GPIO19 ({#PIN_DONT_USE} - USB-)"], + [20, "GPIO20 ({#PIN_DONT_USE} - USB+)"], + [21, "GPIO21"], + [26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"], + [27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"], + [28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"], + [29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"], + [30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"], + [31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"], + [32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"], + [33, "GPIO33 (not exposed on S3-WROOM modules)"], + [34, "GPIO34 (not exposed on S3-WROOM modules)"], + [35, "GPIO35"], + [36, "GPIO36"], + [37, "GPIO37"], + [38, "GPIO38"], + [39, "GPIO39"], + [40, "GPIO40"], + [41, "GPIO41"], + [42, "GPIO42"], + [43, "GPIO43"], + [44, "GPIO44"], + [45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"], + [46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"], + [47, "GPIO47"], + [48, "GPIO48"], + ]; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = [ + [255, "off / default"], + [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], + [1, "GPIO1"], + [2, "GPIO2"], + [3, "GPIO3"], + [4, "GPIO4"], + [5, "GPIO5"], + [6, "GPIO6"], + [7, "GPIO7"], + [8, "GPIO8"], + [9, "GPIO9"], + [10, "GPIO10"], + [11, "GPIO11"], + [12, "GPIO12"], + [13, "GPIO13"], + [14, "GPIO14"], + [15, "GPIO15"], + [16, "GPIO16"], + [17, "GPIO17"], + [18, "GPIO18"], + [19, "GPIO19 ({#PIN_DONT_USE} - USB-)"], + [20, "GPIO20 ({#PIN_DONT_USE} - USB+)"], + [21, "GPIO21"], + [26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"], + [27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"], + [28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"], + [29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"], + [30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"], + [31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"], + [32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"], + [33, "GPIO33 (not exposed on S3-WROOM modules)"], + [34, "GPIO34 (not exposed on S3-WROOM modules)"], + [35, "GPIO35"], + [36, "GPIO36"], + [37, "GPIO37"], + [38, "GPIO38"], + [39, "GPIO39"], + [40, "GPIO40"], + [41, "GPIO41"], + [42, "GPIO42"], + [43, "GPIO43"], + [44, "GPIO44"], + [45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"], + [46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"], + [47, "GPIO47"], + [48, "GPIO48"], + ]; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = [ + [255, "off / default"], + [0, "GPIO0"], + [1, "GPIO1"], + [2, "GPIO2"], + [3, "GPIO3"], + [4, "GPIO4"], + [5, "GPIO5"], + [6, "GPIO6"], + [7, "GPIO7"], + [8, "GPIO8"], + [9, "GPIO9"], + [10, "GPIO10"], + [11, "GPIO11"], + [12, "GPIO12 (PSRAM/FLASH)"], + [13, "GPIO13 (PSRAM/FLASH)"], + [14, "GPIO14 (PSRAM/FLASH)"], + [15, "GPIO15 (PSRAM/FLASH)"], + [16, "GPIO16 (PSRAM/FLASH)"], + [17, "GPIO17 (PSRAM/FLASH)"], + [18, "GPIO18 ({#PIN_DONT_USE} - USB-)"], + [19, "GPIO19 ({#PIN_DONT_USE} - USB+)"], + [20, "GPIO20 (RX)"], + [21, "GPIO21 (TX)"], + ]; + /*ENDIF_ESP32-C3*/ + /*ENDIF_ESP32*/ function sect(e1, e2) { return ml("div", {class: "row"}, [ @@ -22,7 +182,36 @@ ]) } - function wifi() { + /*IF_ETHERNET*/ + var pins = ['cs', 'sclk', 'miso', 'mosi', 'irq', 'reset'] + function step1(obj) { + console.log(obj) + lst = [] + for(p of pins) { + lst.push( + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-12 col-sm-3 my-2"}, p.toUpperCase()), + ml("div", {class: "col-12 col-sm-9"}, + sel(p, pinList, obj[p]) + ) + ]) + ) + } + let en = inp("en", null, null, ["cb"], "en", "checkbox"); + en.checked = obj["en"]; + + return sect("{#NETWORK_SETUP}", [ + ml("div", {class: "row mb-3"}, [ + ml("div", {class: "col-8"}, "{#ETH_ENABLE}"), + ml("div", {class: "col-4"}, en) + ]), + ...lst, + ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_REBOOT}", onclick: () => {saveEth()}}, null))), + ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {onclick: () => {redirect()}}, "{#STOP_WIZARD}"))) + ]) + } + /*ENDIF_ETHERNET*/ + function step1Wifi() { return ml("div", {}, [ ml("div", {class: "row my-5"}, ml("div", {class: "col"}, ml("span", {class: "fs-1"}, "{#WELCOME}"))), ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#NETWORK_SETUP}"))), @@ -30,7 +219,7 @@ sect("{#WIFI_MANUAL}", ml("input", {id: "man", type: "text"})), sect("{#WIFI_PASSWORD}", ml("input", {id: "pwd", type: "password"})), ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn", value: "{#BTN_NEXT}", onclick: () => {saveWifi()}}, null))), - ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}"))) + ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {onclick: () => {redirect()}}, "{#STOP_WIZARD}"))) ]) } @@ -40,23 +229,35 @@ ml("div", {class: "row"}, ml("div", {class: "col"}, ml("span", {class: "fs-5"}, "{#TEST_CONNECTION}"))), sect("{#TRY_TO_CONNECT}", ml("span", {id: "state"}, "{#CONNECTING}")), ml("div", {class: "row my-4"}, ml("div", {class: "col a-r"}, ml("input", {type: "button", class:"btn hide", id: "btn", value: "{#BTN_FINISH}", onclick: () => {redirect()}}, null))), - ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {href: "http://192.168.4.1/"}, "{#STOP_WIZARD}"))) + ml("div", {class: "row mt-5"}, ml("div", {class: "col a-c"}, ml("a", {onclick: () => {redirect()}}, "{#STOP_WIZARD}"))) ) - v = setInterval(() => {getAjax('/api/setup/getip', printIp)}, 2500); + v = setInterval(() => {getAjax('/api/setup/getip', printIp)}, 1000); } function redirect() { - window.location.replace("http://192.168.4.1/") + window.location.replace(redirIp) } function printIp(obj) { - if("0.0.0.0" != obj["ip"]) { + if("0.0.0.0" != obj.ip) { clearInterval(v) setHide("btn", false) document.getElementById("state").innerHTML = "{#NETWORK_SUCCESS}" + obj.ip } } + /*IF_ETHERNET*/ + function saveEth() { + let o = { + cmd: "save_eth", + en: document.getElementsByName("en")[0].checked + } + for(p of pins) { + o[p] = document.getElementsByName(p)[0].value + } + getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify(o)); + } + /*ENDIF_ETHERNET*/ function saveWifi() { var ssid = document.getElementById("net").value; if(-1 == ssid) @@ -64,24 +265,36 @@ getAjax("/api/setup", ((o) => {if(!o.error) checkWifi()}), "POST", JSON.stringify({cmd: "save_wifi", ssid: ssid, pwd: document.getElementById("pwd").value})); } - function nets(obj) { - var e = document.getElementById("net"); - if(obj.networks.length > 0) { - var a = [] - a.push(ml("option", {value: -1}, obj.networks.length + " {#NUM_NETWORKS_FOUND}")) - for(n of obj.networks) { - a.push(ml("option", {value: n.ssid}, n.ssid + " (" + n.rssi + "dBm)")) - found = true; + function init() { + /*IF_ETHERNET*/ + getAjax("/api/setup", ((o) => c.append(step1(o.eth)))); + /*ENDIF_ETHERNET*/ + function nets(obj) { + clearInterval(v) + v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 4000) + + if(!obj.success) + return; + + var e = document.getElementById("net"); + if(obj.networks.length > 0) { + var a = [] + a.push(ml("option", {value: -1}, obj.networks.length + " {#NUM_NETWORKS_FOUND}")) + for(n of obj.networks) { + a.push(ml("option", {value: n.ssid}, n.ssid + " (" + n.rssi + "dBm)")) + found = true; + } + e.replaceChildren(...a) } - e.replaceChildren(...a) + + if("0.0.0.0" != obj.ip) + redirIp = "http://" + obj.ip + "/index" } - getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"})); - } - getAjax("/api/setup", ((o) => {}), "POST", JSON.stringify({cmd: "scan_wifi"})); - c.append(wifi()) + c.append(step1Wifi()) + getAjax('/api/setup/networks', nets) + } - v = setInterval(() => {getAjax('/api/setup/networks', nets)}, 2500); </script> </body> </html> diff --git a/src/web/lang.h b/src/web/lang.h index fb5506ee..dd6640b2 100644 --- a/src/web/lang.h +++ b/src/web/lang.h @@ -72,4 +72,28 @@ #define BTN_REBOOT "Reboot" #endif +#ifdef LANG_DE + #define BTN_REBOOT "Ahoy neustarten" +#else /*LANG_EN*/ + #define BTN_REBOOT "Reboot" +#endif + +#ifdef LANG_DE + #define BTN_YES "ja" +#else /*LANG_EN*/ + #define BTN_YES "yes" +#endif + +#ifdef LANG_DE + #define BTN_NO "nein" +#else /*LANG_EN*/ + #define BTN_NO "no" +#endif + +#ifdef LANG_DE + #define BTN_COREDUMP "CoreDump herunterladen" +#else /*LANG_EN*/ + #define BTN_COREDUMP "download CoreDump" +#endif + #endif /*__LANG_H__*/ diff --git a/src/web/lang.json b/src/web/lang.json index 066370c5..179c3e4e 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -6,7 +6,7 @@ { "token": "NAV_WIZARD", "en": "Setup Wizard", - "de": "Daten" + "de": "Einrichtungsassitent" }, { "token": "NAV_LIVE", @@ -81,17 +81,22 @@ { "token": "BTN_NEXT", "en": "next >>", - "de": "prüfen >>" + "de": "speichern >>" + }, + { + "token": "BTN_REBOOT", + "en": "reboot >>", + "de": "Ahoy neustarten >>" }, { "token": "TEST_CONNECTION", "en": "Test Connection", - "de": "Verbindung wird überprüft" + "de": "Verbindung wird überprüft" }, { "token": "TRY_TO_CONNECT", "en": "AhoyDTU is trying to connect to your WiFi", - "de": "AhoyDTU versucht eine Verindung mit deinem Netzwerk herzustellen" + "de": "AhoyDTU versucht eine Verbindung mit Deinem Netzwerk herzustellen" }, { "token": "CONNECTING", @@ -101,7 +106,7 @@ { "token": "NETWORK_SUCCESS", "en": "success, got following IP in your network: ", - "de": "Verindung erfolgreich. AhoyDTU hat die folgende IP bekommen: " + "de": "Verbindung erfolgreich. AhoyDTU hat die folgende IP bekommen: " }, { "token": "BTN_FINISH", @@ -112,6 +117,36 @@ "token": "NUM_NETWORKS_FOUND", "en": "Network(s) found", "de": "Netzwerk(e) gefunden" + }, + { + "token": "PIN_OFF", + "en": "off / default", + "de": "aus / Standard" + }, + { + "token": "PIN_NO_IRQ", + "en": "no IRQ!", + "de": "kein Interrupt!" + }, + { + "token": "PIN_INPUT_ONLY", + "en": "in only", + "de": "nur Eingang" + }, + { + "token": "PIN_DONT_USE", + "en": "DONT USE", + "de": "nicht benutzen" + }, + { + "token": "PIN_NOT_AVAIL", + "en": "not available", + "de": "nicht verfügbar" + }, + { + "token": "ETH_ENABLE", + "en": "Ethernet enable", + "de": "Ethernet aktivieren" } ] }, @@ -186,7 +221,7 @@ { "token": "LOG_PRINT_INVERTER_DATA", "en": "print inverter data", - "de": "Livedaten ausgeben" + "de": "Inverterwerte ausgeben" }, { "token": "LOG_SERIAL_DEBUG", @@ -224,19 +259,9 @@ "de": "Netzwerke suchen" }, { - "token": "BTN_SCAN", - "en": "scan", - "de": "Suche starten" - }, - { - "token": "AVAIL_NETWORKS", - "en": "Avail Networks", - "de": "Verfügbare Netzwerke" - }, - { - "token": "NETWORK_NOT_SCANNED", - "en": "not scanned", - "de": "nicht gesucht" + "token": "SCAN_WIFI", + "en": "scan for WiFi networks", + "de": "nach WiFi Netzwerken suchen" }, { "token": "SSID_HIDDEN", @@ -295,23 +320,28 @@ }, { "token": "INV_RESET_MIDNIGHT", - "en": "Reset values and YieldDay at midnight. ('Pause communication during night' need to be set)", - "de": "Werte und Gesamtertrag um Mitternacht zurücksetzen ('Kommunikation während der Nacht pausieren' muss gesetzt sein)" + "en": "Reset values and YieldDay at midnight", + "de": "Werte und Gesamtertrag um Mitternacht zurücksetzen" }, { - "token": "INV_PAUSE_SUNSET", + "token": "INV_RESET_SUNSET", "en": "Reset values at sunset", "de": "Werte bei Sonnenuntergang zurücksetzen" }, + { + "token": "INV_RESET_SUNRISE", + "en": "Reset values at sunrise", + "de": "Werte bei Sonnenaufgang zurücksetzen" + }, { "token": "INV_RESET_NOT_AVAIL", "en": "Reset values when inverter status is 'not available'", "de": "Werte zurücksetzen, sobald der Wechselrichter nicht erreichbar ist" }, { - "token": "INV_RESET_MAX_MIDNIGHT", - "en": "Reset 'max' values at midnight", - "de": "Maximalwerte mitternachts zurücksetzen" + "token": "INV_RESET_MAX_VALUES", + "en": "Include reset 'max' values", + "de": "Maximalwerte auch zurücksetzen" }, { "token": "INV_START_WITHOUT_TIME", @@ -323,11 +353,6 @@ "en": "Read Grid Profile", "de": "Grid-Profil auslesen" }, - { - "token": "INV_YIELD_EFF", - "en": "Yield Efficiency (default 1.0)", - "de": "Ertragseffizienz (Standard 1.0)" - }, { "token": "NTP_INTERVAL", "en": "NTP Interval (in minutes, min. 5 minutes)", @@ -388,11 +413,21 @@ "en": "Password (optional)", "de": "Passwort (optional)" }, + { + "token": "MQTT_JSON", + "en": "Payload as JSON", + "de": "Ausgabe als JSON" + }, { "token": "MQTT_NOTE", "en": "Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)", "de": "Wechselrichterdaten in fixem Intervall schicken, auch wenn es keine Änderung gab. Ein Wert von '0' deaktiviert das fixe Intervall, die Wechselrichterdaten werden übertragen, sobald neue zur Verfügung stehen. (Standard: 0)" }, + { + "token": "RETAIN", + "en": "enable retain flag", + "de": "'Retain Flag' aktivieren" + }, { "token": "DISPLAY_CONFIG", "en": "Display Config", @@ -601,7 +636,7 @@ { "token": "BTN_INV_ADD", "en": "add Inverter", - "de": "Wechselrichter hinzufuegen" + "de": "Wechselrichter hinzuf\u00FCgen" }, { "token": "INV_INPUT", @@ -626,7 +661,7 @@ { "token": "TAB_INPUTS", "en": "Inputs", - "de": "Eingaenge" + "de": "Eingänge" }, { "token": "TAB_RADIO", @@ -728,6 +763,11 @@ "en": "CMT2300A radio enable", "de": "CMT2300A Funkmodul aktivieren" }, + { + "token": "ETH_ENABLE", + "en": "Ethernet enable", + "de": "Ethernet aktivieren" + }, { "token": "DISP_NONE", "en": "None", @@ -867,6 +907,151 @@ "token": "COMMUNICATING", "en": "communicating", "de": "aktiv" + }, + { + "token": "NETWORK", + "en": "Network", + "de": "Netzwerk" + }, + { + "token": "CONNECTION_TYPE", + "en": "connection", + "de": "Verbindung" + }, + { + "token": "WIRED", + "en": "ethernet cable", + "de": "Netzwerkkabel" + }, + { + "token": "WIFI", + "en": "WiFi", + "de": "WiFi" + }, + { + "token": "DEVICE_NAME", + "en": "Device name", + "de": "Gerätename" + }, + { + "token": "UPTIME", + "en": "Uptime", + "de": "Laufzeit" + }, + { + "token": "REBOOT_REASON", + "en": "Reboot reason", + "de": "Grund des Neustarts" + }, + { + "token": "ENVIRONMENT", + "en": "Environment", + "de": "Umgebung" + }, + { + "token": "BUILD_OPTIONS", + "en": "build options", + "de": "Module" + }, + { + "token": "ADDRESS", + "en": "Address", + "de": "Adresse" + }, + { + "token": "NETWORK", + "en": "Network", + "de": "Netzwerk" + }, + { + "token": "MEMORY", + "en": "Memory", + "de": "Speicher" + }, + { + "token": "CONFIG_PARTITION", + "en": "Config Partition", + "de": "Konfiguration" + }, + { + "token": "FIRMWARE_PARTITION", + "en": "Firmware Partition", + "de": "Firmware" + }, + { + "token": "INTR_PIN_WORKING", + "en": "Interrupt Pin working", + "de": "Interrupt Pin funktioniert" + }, + { + "token": "DATA_RATE", + "en": "Data Rate", + "de": "Datenrate" + }, + { + "token": "RADIO", + "en": "Radio", + "de": "Funkmodul" + }, + { + "token": "NOT", + "en": "not", + "de": "nicht" + }, + { + "token": "CONNECTED", + "en": "connected", + "de": "verbunden" + }, + { + "token": "ENABLED", + "en": "enabled", + "de": "aktiviert" + }, + { + "token": "CONNECTION", + "en": "connection", + "de": "Verbindung" + }, + { + "token": "FLASH_SIZE", + "en": "Flash size", + "de": "Speichergröße" + }, + { + "token": "MAX_FREE_BLOCK", + "en": "max free block", + "de": "maximale freie Blockgröße" + }, + { + "token": "SUNRISE", + "en": "sunrise", + "de": "Sonnenaufgang" + }, + { + "token": "SUNSET", + "en": "sunset", + "de": "Sonnenuntergang" + }, + { + "token": "COMMUNICATION_START", + "en": "Communication start", + "de": "Start der Kommunikation" + }, + { + "token": "COMMUNICATION_STOP", + "en": "Communication stop", + "de": "Ende der Kommunikation" + }, + { + "token": "NIGHT_BEHAVIOR", + "en": "Night behavior", + "de": "Verhalten bei Nacht" + }, + { + "token": "COMMUNICATING", + "en": "communicating", + "de": "kommunizierend" } ] }, @@ -883,6 +1068,11 @@ "en": "autoscroll", "de": "automatisch scrollen" }, + { + "token": "BTN_MANUALSCROLL", + "en": "manual scroll", + "de": "manuell scrollen" + }, { "token": "BTN_COPY", "en": "copy", @@ -897,6 +1087,21 @@ "token": "UPTIME", "en": "uptime", "de": "Laufzeit" + }, + { + "token": "DAYS", + "en": "days", + "de": "Tage" + }, + { + "token": "COPIED_TO_CLIPBOARD", + "en": "Copied to clipboard", + "de": "in die Zwischenablage kopiert" + }, + { + "token": "CLIPBOARD_FAILED", + "en": "Copy failed", + "de": "kopieren fehlgeschlagen" } ] }, @@ -973,6 +1178,16 @@ "en": "Error", "de": "Fehler" }, + { + "token": "DAY", + "en": "day", + "de": "Tag" + }, + { + "token": "S", + "en": "s", + "de": "e" + }, { "token": "NTP_UNREACH", "en": "NTP timeserver unreachable", @@ -986,7 +1201,7 @@ { "token": "NIGHT_TIME", "en": "Night time, inverter polling disabled", - "de": "Wechselrichterabfrage deaktivert (Nacht)" + "de": "Wechselrichterabfrage deaktiviert (Nacht)" }, { "token": "PAUSED_AT", @@ -1503,35 +1718,30 @@ "en": "Total Power", "de": "Gesamtleistung" }, + { + "token": "TOTAL_POWER_DAY", + "en": "Total Power Today", + "de": "Gesamtleistung heute" + }, { "token": "TOTAL_YIELD_PER_DAY", "en": "Total Yield per day", "de": "Gesamtertrag pro Tag" }, - { - "token": "MAX_DAY", - "en": "maximum day", - "de": "Tagesmaximum" - }, - { - "token": "LAST_VALUE", - "en": "last value", - "de": "letzter Wert" - }, { "token": "MAXIMUM", - "en": "maximum value", - "de": "Maximalwert" + "en": "Maximum", + "de": "Maximum" }, { - "token": "UPDATED", - "en": "Updated every", - "de": "aktualisiert alle" + "token": "LAST_VALUE", + "en": "Last value", + "de": "Letzter Wert" }, { - "token": "SECONDS", - "en": "seconds", - "de": "Sekunden" + "token": "YIELD_DAY", + "en": "Yield day", + "de": "Tagesertrag" } ] } diff --git a/src/web/web.h b/src/web/web.h index 8495ba23..b337228a 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -16,11 +16,7 @@ #include "../appInterface.h" #include "../hm/hmSystem.h" #include "../utils/helper.h" -#if defined(ETHERNET) -#include "AsyncWebServer_ESP32_W5500.h" -#else /* defined(ETHERNET) */ #include "ESPAsyncWebServer.h" -#endif /* defined(ETHERNET) */ #include "html/h/api_js.h" #include "html/h/colorBright_css.h" #include "html/h/colorDark_css.h" @@ -41,14 +37,19 @@ #define WEB_SERIAL_BUF_SIZE 2048 -const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", "pinLed1", "pinLed2", "pinLedHighActive", "pinLedLum", "pinCmtSclk", "pinSdio", "pinCsb", "pinFcsb", "pinGpio3"}; +const char* const pinArgNames[] = { + "pinCs", "pinCe", "pinIrq", "pinSclk", "pinMosi", "pinMiso", "pinLed0", + "pinLed1", "pinLed2", "pinLedHighActive", "pinLedLum", "pinCmtSclk", + "pinSdio", "pinCsb", "pinFcsb", "pinGpio3" + #if defined (ETHERNET) + , "ethCs", "ethSclk", "ethMiso", "ethMosi", "ethIrq", "ethRst" + #endif +}; template <class HMSYSTEM> class Web { public: - Web(void) : mWeb(80), mEvts("/events") { - memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); - } + Web(void) : mWeb(80), mEvts("/events") {} void setup(IApp *app, HMSYSTEM *sys, settings_t *config) { mApp = app; @@ -56,7 +57,8 @@ class Web { mConfig = config; DPRINTLN(DBG_VERBOSE, F("app::setup-on")); - mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1)); + mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1, true)); + mWeb.on("/index", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1, false)); mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1)); mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1)); mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1)); @@ -74,6 +76,7 @@ class Web { mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); mWeb.on("/wizard", HTTP_GET, std::bind(&Web::onWizard, this, std::placeholders::_1)); + mWeb.on("/generate_204", HTTP_GET, std::bind(&Web::onWizard, this, std::placeholders::_1)); //Android captive portal mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1)); mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1)); @@ -105,11 +108,17 @@ class Web { void tickSecond() { if (mSerialClientConnnected) { + if(nullptr == mSerialBuf) + return; + if (mSerialBufFill > 0) { mEvts.send(mSerialBuf, "serial", millis()); memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); mSerialBufFill = 0; } + } else if(nullptr != mSerialBuf) { + delete[] mSerialBuf; + mSerialBuf = nullptr; } } @@ -153,18 +162,14 @@ class Web { mUploadFp.write(data, len); if (final) { mUploadFp.close(); - #if !defined(ETHERNET) char pwd[PWD_LEN]; strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD - #endif if (!mApp->readSettings("/tmp.json")) { mUploadFail = true; DPRINTLN(DBG_ERROR, F("upload JSON error!")); } else { LittleFS.remove("/tmp.json"); - #if !defined(ETHERNET) strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD - #endif for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { if((mConfig->inst.iv[i].serial.u64 != 0) && (mConfig->inst.iv[i].serial.u64 < 138999999999)) { // hexadecimal mConfig->inst.iv[i].serial.u64 = ah::Serial2u64(String(mConfig->inst.iv[i].serial.u64).c_str()); @@ -181,6 +186,9 @@ class Web { if (!mSerialClientConnnected) return; + if(nullptr == mSerialBuf) + return; + msg.replace("\r\n", "<rn>"); if (mSerialAddTime) { if ((13 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { @@ -260,8 +268,8 @@ class Web { bool reboot = (!Update.hasError()); String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\""); - #if defined(ETHERNET) && defined(CONFIG_IDF_TARGET_ESP32S3) - html += F("5"); + #if defined(ETHERNET) + html += (mConfig->sys.eth.enabled) ? F("5") : F("20"); #else html += F("20"); #endif @@ -297,6 +305,10 @@ class Web { void onConnect(AsyncEventSourceClient *client) { DPRINTLN(DBG_VERBOSE, "onConnect"); + if(nullptr == mSerialBuf) { + mSerialBuf = new char[WEB_SERIAL_BUF_SIZE]; + memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE); + } mSerialClientConnnected = true; if (client->lastId()) @@ -305,7 +317,19 @@ class Web { client->send("hello!", NULL, millis(), 1000); } - void onIndex(AsyncWebServerRequest *request) { + void onIndex(AsyncWebServerRequest *request, bool checkAp = true) { + #if !defined(ETHERNET) + if(mApp->isApActive() && checkAp) { + onWizard(request); + return; + } + #else + // show wizard only if ethernet is not configured + if(mApp->isApActive() && checkAp && !mConfig->sys.eth.enabled) { + onWizard(request); + return; + } + #endif getPage(request, PROT_MASK_INDEX, index_html, index_html_len); } @@ -388,6 +412,7 @@ class Web { void showNotFound(AsyncWebServerRequest *request) { checkProtection(request); + //DBGPRINTLN(request->url()); request->redirect("/wizard"); } @@ -411,6 +436,13 @@ class Web { } void onWizard(AsyncWebServerRequest *request) { + #if defined(ETHERNET) + if(mConfig->sys.eth.enabled) { + getPage(request, PROT_MASK_INDEX, index_html, index_html_len); + return; + } + #endif + AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), wizard_html, wizard_html_len); response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("content-type"), "text/html; charset=UTF-8"); @@ -428,15 +460,14 @@ class Web { char buf[20] = {0}; // general - #if !defined(ETHERNET) if (request->arg("ssid") != "") request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN); if (request->arg("pwd") != "{PWD}") request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); + mConfig->sys.isHidden = (request->arg("hidd") == "on"); + if (request->arg("ap_pwd") != "") request->arg("ap_pwd").toCharArray(mConfig->sys.apPwd, PWD_LEN); - mConfig->sys.isHidden = (request->arg("hidd") == "on"); - #endif /* !defined(ETHERNET) */ if (request->arg("device") != "") request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); mConfig->sys.darkMode = (request->arg("darkMode") == "on"); @@ -477,17 +508,22 @@ class Web { if (request->arg("invInterval") != "") mConfig->inst.sendInterval = request->arg("invInterval").toInt(); - mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on"); + mConfig->inst.rstValsAtMidNight = (request->arg("invRstMid") == "on"); mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on"); + mConfig->inst.rstValsCommStart = (request->arg("invRstComStart") == "on"); mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on"); mConfig->inst.startWithoutTime = (request->arg("strtWthtTm") == "on"); mConfig->inst.readGrid = (request->arg("rdGrid") == "on"); - mConfig->inst.rstMaxValsMidNight = (request->arg("invRstMaxMid") == "on"); - mConfig->inst.yieldEffiency = (request->arg("yldEff")).toFloat(); + mConfig->inst.rstIncludeMaxVals = (request->arg("invRstMaxMid") == "on"); // pinout - for (uint8_t i = 0; i < 16; i++) { + #if defined(ETHERNET) + for (uint8_t i = 0; i < 22; i++) + #else + for (uint8_t i = 0; i < 16; i++) + #endif + { uint8_t pin = request->arg(String(pinArgNames[i])).toInt(); switch(i) { case 0: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_NRF_CS_PIN); break; @@ -506,11 +542,23 @@ class Web { case 13: mConfig->cmt.pinCsb = pin; break; case 14: mConfig->cmt.pinFcsb = pin; break; case 15: mConfig->cmt.pinIrq = pin; break; + + #if defined(ETHERNET) + case 16: mConfig->sys.eth.pinCs = pin; break; + case 17: mConfig->sys.eth.pinSclk = pin; break; + case 18: mConfig->sys.eth.pinMiso = pin; break; + case 19: mConfig->sys.eth.pinMosi = pin; break; + case 20: mConfig->sys.eth.pinIrq = pin; break; + case 21: mConfig->sys.eth.pinRst = pin; break; + #endif } } mConfig->nrf.enabled = (request->arg("nrfEnable") == "on"); mConfig->cmt.enabled = (request->arg("cmtEnable") == "on"); + #if defined(ETHERNET) + mConfig->sys.eth.enabled = (request->arg("ethEn") == "on"); + #endif // ntp if (request->arg("ntpAddr") != "") { @@ -544,8 +592,10 @@ class Web { if (request->arg("mqttPwd") != "{PWD}") request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); + mConfig->mqtt.json = (request->arg("mqttJson") == "on"); mConfig->mqtt.port = request->arg("mqttPort").toInt(); mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); + mConfig->mqtt.enableRetain = (request->arg("retain") == "on"); // serial console mConfig->serial.debug = (request->arg("serDbg") == "on"); @@ -662,7 +712,7 @@ class Web { { "max_power", "gauge", metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->getMaxPower();} }, { "radio_rx_success", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxSuccess;} }, { "radio_rx_fail", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFail;} }, - { "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnser;} }, + { "radio_rx_fail_answer", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.rxFailNoAnswer;} }, { "radio_frame_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.frmCnt;} }, { "radio_tx_cnt", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.txCnt;} }, { "radio_retransmits", "counter" ,metricConstInverterFormat, [](Inverter<> *iv)-> uint64_t {return iv->radioStatistics.retransmits;} }, @@ -911,7 +961,7 @@ class Web { settings_t *mConfig = nullptr; bool mSerialAddTime = true; - char mSerialBuf[WEB_SERIAL_BUF_SIZE]; + char *mSerialBuf = nullptr; uint16_t mSerialBufFill = 0; bool mSerialClientConnnected = false; diff --git a/src/wifi/ahoywifi.cpp b/src/wifi/ahoywifi.cpp deleted file mode 100644 index 9bcbdadc..00000000 --- a/src/wifi/ahoywifi.cpp +++ /dev/null @@ -1,488 +0,0 @@ -//----------------------------------------------------------------------------- -// 2024 Ahoy, https://ahoydtu.de -// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed -//----------------------------------------------------------------------------- - -#if !defined(ETHERNET) -#if defined(ESP32) && defined(F) - #undef F - #define F(sl) (sl) -#endif -#include "ahoywifi.h" - -#if defined(ESP32) -#include <ESPmDNS.h> -#else -#include <ESP8266mDNS.h> -#endif - -// NTP CONFIG -#define NTP_PACKET_SIZE 48 - -//----------------------------------------------------------------------------- -ahoywifi::ahoywifi() : mApIp(192, 168, 4, 1) {} - - -/** - * TODO: ESP32 has native strongest AP support! - * WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); - WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); -*/ - -//----------------------------------------------------------------------------- -void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb) { - mConfig = config; - mUtcTimestamp = utcTimestamp; - mAppWifiCb = cb; - - mGotDisconnect = false; - mStaConn = DISCONNECTED; - mCnt = 0; - mScanActive = false; - mScanCnt = 0; - mStopApAllowed = true; - - #if defined(ESP8266) - wifiConnectHandler = WiFi.onStationModeConnected(std::bind(&ahoywifi::onConnect, this, std::placeholders::_1)); - wifiGotIPHandler = WiFi.onStationModeGotIP(std::bind(&ahoywifi::onGotIP, this, std::placeholders::_1)); - wifiDisconnectHandler = WiFi.onStationModeDisconnected(std::bind(&ahoywifi::onDisconnect, this, std::placeholders::_1)); - #else - WiFi.onEvent(std::bind(&ahoywifi::onWiFiEvent, this, std::placeholders::_1)); - #endif - - setupWifi(true); -} - - -//----------------------------------------------------------------------------- -void ahoywifi::setupWifi(bool startAP = false) { - #if !defined(FB_WIFI_OVERRIDDEN) - if(startAP) { - setupAp(); - delay(1000); - } - #endif - #if !defined(AP_ONLY) - #if defined(FB_WIFI_OVERRIDDEN) - snprintf(mConfig->sys.stationSsid, SSID_LEN, "%s", FB_WIFI_SSID); - snprintf(mConfig->sys.stationPwd, PWD_LEN, "%s", FB_WIFI_PWD); - setupStation(); - #else - if(mConfig->valid) { - if(strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) != 0) - setupStation(); - } - #endif - #endif -} - - -void ahoywifi::tickWifiLoop() { - static const uint8_t TIMEOUT = 20; - static const uint8_t SCAN_TIMEOUT = 10; - #if !defined(AP_ONLY) - - mCnt++; - - switch (mStaConn) { - case IN_STA_MODE: - // Nothing to do - if (mGotDisconnect) { - mStaConn = RESET; - } - #if !defined(ESP32) - MDNS.update(); - if(WiFi.channel() > 11) - mWasInCh12to14 = true; - #endif - return; - case IN_AP_MODE: - if ((WiFi.softAPgetStationNum() == 0) || (!mStopApAllowed)) { - mCnt = 0; - mDns.stop(); - WiFi.mode(WIFI_AP_STA); - mStaConn = DISCONNECTED; - } else { - mDns.processNextRequest(); - return; - } - break; - case DISCONNECTED: - if ((WiFi.softAPgetStationNum() > 0) && (mStopApAllowed)) { - mStaConn = IN_AP_MODE; - // first time switch to AP Mode - if (mScanActive) { - WiFi.scanDelete(); - mScanActive = false; - } - DBGPRINTLN(F("AP client connected")); - welcome(mApIp.toString(), ""); - WiFi.mode(WIFI_AP); - mDns.start(53, "*", mApIp); - mAppWifiCb(true); - mDns.processNextRequest(); - return; - } else if (!mScanActive) { - DBGPRINT(F("scanning APs with SSID ")); - DBGPRINTLN(String(mConfig->sys.stationSsid)); - mScanCnt = 0; - mCnt = 0; - mScanActive = true; -#if defined(ESP8266) - WiFi.scanNetworks(true, true, 0U, ([this]() { - if (mConfig->sys.isHidden) - return (uint8_t*)NULL; - return (uint8_t*)(mConfig->sys.stationSsid); - })()); -#else - WiFi.scanNetworks(true, true, false, 300U, 0U, ([this]() { - if (mConfig->sys.isHidden) - return (char*)NULL; - return (mConfig->sys.stationSsid); - })()); -#endif - return; - } else if(getBSSIDs()) { - // Scan ready - mStaConn = SCAN_READY; - } else { - // In case of a timeout, what do we do? - // For now we start scanning again as the original code did. - // Would be better to into PA mode - - if (isTimeout(SCAN_TIMEOUT)) { - WiFi.scanDelete(); - mScanActive = false; - } - } - break; - case SCAN_READY: - mStaConn = CONNECTING; - mCnt = 0; - DBGPRINT(F("try to connect to AP with BSSID:")); - uint8_t bssid[6]; - for (int j = 0; j < 6; j++) { - bssid[j] = mBSSIDList.front(); - mBSSIDList.pop_front(); - DBGPRINT(" " + String(bssid[j], HEX)); - } - DBGPRINTLN(""); - mGotDisconnect = false; - WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]); - - break; - case CONNECTING: - if (isTimeout(TIMEOUT)) { - WiFi.disconnect(); - mStaConn = mBSSIDList.empty() ? DISCONNECTED : SCAN_READY; - } - break; - case CONNECTED: - // Connection but no IP yet - if (isTimeout(TIMEOUT) || mGotDisconnect) { - mStaConn = RESET; - } - break; - case GOT_IP: - welcome(WiFi.localIP().toString(), F(" (Station)")); - if(mStopApAllowed) { - WiFi.softAPdisconnect(); - WiFi.mode(WIFI_STA); - DBGPRINTLN(F("[WiFi] AP disabled")); - delay(100); - } - mAppWifiCb(true); - mGotDisconnect = false; - mStaConn = IN_STA_MODE; - - if (!MDNS.begin(mConfig->sys.deviceName)) { - DPRINTLN(DBG_ERROR, F("Error setting up MDNS responder!")); - } else { - DBGPRINT(F("[WiFi] mDNS established: ")); - DBGPRINT(mConfig->sys.deviceName); - DBGPRINTLN(F(".local")); - } - - break; - case RESET: - mGotDisconnect = false; - mStaConn = DISCONNECTED; - mCnt = 5; // try to reconnect in 5 sec - setupWifi(); // reconnect with AP / Station setup - mAppWifiCb(false); - DPRINTLN(DBG_INFO, "[WiFi] Connection Lost"); - break; - default: - DBGPRINTLN(F("Unhandled status")); - break; - } - -#endif -} - -//----------------------------------------------------------------------------- -void ahoywifi::setupAp(void) { - DPRINTLN(DBG_VERBOSE, F("wifi::setupAp")); - - DBGPRINTLN(F("\n---------\nAhoyDTU Info:")); - DBGPRINT(F("Version: ")); - DBGPRINT(String(VERSION_MAJOR)); - DBGPRINT(F(".")); - DBGPRINT(String(VERSION_MINOR)); - DBGPRINT(F(".")); - DBGPRINTLN(String(VERSION_PATCH)); - DBGPRINT(F("Github Hash: ")); - DBGPRINTLN(String(AUTO_GIT_HASH)); - - DBGPRINT(F("\n---------\nAP MODE\nSSID: ")); - DBGPRINTLN(WIFI_AP_SSID); - DBGPRINT(F("PWD: ")); - DBGPRINTLN(mConfig->sys.apPwd); - DBGPRINT(F("IP Address: http://")); - DBGPRINTLN(mApIp.toString()); - DBGPRINTLN(F("---------\n")); - - if(String(mConfig->sys.deviceName) != "") - WiFi.hostname(mConfig->sys.deviceName); - - WiFi.mode(WIFI_AP_STA); - WiFi.softAPConfig(mApIp, mApIp, IPAddress(255, 255, 255, 0)); - WiFi.softAP(WIFI_AP_SSID, mConfig->sys.apPwd); -} - - -//----------------------------------------------------------------------------- -void ahoywifi::setupStation(void) { - DPRINTLN(DBG_VERBOSE, F("wifi::setupStation")); - if(mConfig->sys.ip.ip[0] != 0) { - IPAddress ip(mConfig->sys.ip.ip); - IPAddress mask(mConfig->sys.ip.mask); - IPAddress dns1(mConfig->sys.ip.dns1); - IPAddress dns2(mConfig->sys.ip.dns2); - IPAddress gateway(mConfig->sys.ip.gateway); - if(!WiFi.config(ip, gateway, mask, dns1, dns2)) - DPRINTLN(DBG_ERROR, F("failed to set static IP!")); - } - mBSSIDList.clear(); - if(String(mConfig->sys.deviceName) != "") - WiFi.hostname(mConfig->sys.deviceName); - WiFi.mode(WIFI_AP_STA); - - DBGPRINT(F("connect to network '")); - DBGPRINT(mConfig->sys.stationSsid); - DBGPRINTLN(F("' ...")); -} - - -//----------------------------------------------------------------------------- -bool ahoywifi::getNtpTime(void) { - if(IN_STA_MODE != mStaConn) - return false; - - IPAddress timeServer; - uint8_t buf[NTP_PACKET_SIZE]; - uint8_t retry = 0; - - if (WiFi.hostByName(mConfig->ntp.addr, timeServer) != 1) - return false; - - mUdp.begin(mConfig->ntp.port); - sendNTPpacket(timeServer); - - while(retry++ < 5) { - int wait = 150; - while(--wait) { - if(NTP_PACKET_SIZE <= mUdp.parsePacket()) { - uint64_t secsSince1900; - mUdp.read(buf, NTP_PACKET_SIZE); - secsSince1900 = ((uint64_t)buf[40] << 24); - secsSince1900 |= (buf[41] << 16); - secsSince1900 |= (buf[42] << 8); - secsSince1900 |= (buf[43] ); - - *mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time - DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC"); - return true; - } else - delay(10); - } - } - - DPRINTLN(DBG_INFO, F("[NTP]: getNtpTime failed")); - return false; -} - - -//----------------------------------------------------------------------------- -void ahoywifi::sendNTPpacket(IPAddress& address) { - //DPRINTLN(DBG_VERBOSE, F("wifi::sendNTPpacket")); - uint8_t buf[NTP_PACKET_SIZE] = {0}; - - buf[0] = B11100011; // LI, Version, Mode - buf[1] = 0; // Stratum - buf[2] = 6; // Max Interval between messages in seconds - buf[3] = 0xEC; // Clock Precision - // bytes 4 - 11 are for Root Delay and Dispersion and were set to 0 by memset - buf[12] = 49; // four-byte reference ID identifying - buf[13] = 0x4E; - buf[14] = 49; - buf[15] = 52; - - mUdp.beginPacket(address, 123); // NTP request, port 123 - mUdp.write(buf, NTP_PACKET_SIZE); - mUdp.endPacket(); -} - -//----------------------------------------------------------------------------- -void ahoywifi::sortRSSI(int *sort, int n) { - for (int i = 0; i < n; i++) - sort[i] = i; - for (int i = 0; i < n; i++) - for (int j = i + 1; j < n; j++) - if (WiFi.RSSI(sort[j]) > WiFi.RSSI(sort[i])) - std::swap(sort[i], sort[j]); -} - -//----------------------------------------------------------------------------- -void ahoywifi::scanAvailNetworks(void) { - if(!mScanActive) { - mScanActive = true; - if(WIFI_AP == WiFi.getMode()) - WiFi.mode(WIFI_AP_STA); - WiFi.scanNetworks(true); - } -} - -//----------------------------------------------------------------------------- -bool ahoywifi::getAvailNetworks(JsonObject obj) { - JsonArray nets = obj.createNestedArray("networks"); - - int n = WiFi.scanComplete(); - if (n < 0) - return false; - if(n > 0) { - int sort[n]; - sortRSSI(&sort[0], n); - for (int i = 0; i < n; ++i) { - nets[i]["ssid"] = WiFi.SSID(sort[i]); - nets[i]["rssi"] = WiFi.RSSI(sort[i]); - } - } - mScanActive = false; - WiFi.scanDelete(); - if(mStaConn == IN_AP_MODE) - WiFi.mode(WIFI_AP); - - return true; -} - -//----------------------------------------------------------------------------- -bool ahoywifi::getBSSIDs() { - bool result = false; - int n = WiFi.scanComplete(); - if (n < 0) { - if (++mScanCnt < 20) - return false; - } - if(n > 0) { - mBSSIDList.clear(); - int sort[n]; - sortRSSI(&sort[0], n); - for (int i = 0; i < n; i++) { - DBGPRINT("BSSID " + String(i) + ":"); - uint8_t *bssid = WiFi.BSSID(sort[i]); - for (int j = 0; j < 6; j++){ - DBGPRINT(" " + String(bssid[j], HEX)); - mBSSIDList.push_back(bssid[j]); - } - DBGPRINTLN(""); - } - result = true; - } - mScanActive = false; - WiFi.scanDelete(); - return result; -} - -//----------------------------------------------------------------------------- -void ahoywifi::connectionEvent(WiFiStatus_t status) { - DPRINTLN(DBG_INFO, "connectionEvent"); - - switch(status) { - case CONNECTED: - if(mStaConn != CONNECTED) { - mStaConn = CONNECTED; - mGotDisconnect = false; - DBGPRINTLN(F("\n[WiFi] Connected")); - } - break; - - case GOT_IP: - mStaConn = GOT_IP; - break; - - case DISCONNECTED: - mGotDisconnect = true; - break; - - default: - break; - } -} - - -//----------------------------------------------------------------------------- -#if defined(ESP8266) - //------------------------------------------------------------------------- - void ahoywifi::onConnect(const WiFiEventStationModeConnected& event) { - connectionEvent(CONNECTED); - } - - //------------------------------------------------------------------------- - void ahoywifi::onGotIP(const WiFiEventStationModeGotIP& event) { - connectionEvent(GOT_IP); - } - - //------------------------------------------------------------------------- - void ahoywifi::onDisconnect(const WiFiEventStationModeDisconnected& event) { - connectionEvent(DISCONNECTED); - } - -#else - //------------------------------------------------------------------------- - void ahoywifi::onWiFiEvent(WiFiEvent_t event) { - DBGPRINT(F("Wifi event: ")); - DBGPRINTLN(String(event)); - - switch(event) { - case SYSTEM_EVENT_STA_CONNECTED: - connectionEvent(CONNECTED); - break; - - case SYSTEM_EVENT_STA_GOT_IP: - connectionEvent(GOT_IP); - break; - - case SYSTEM_EVENT_STA_DISCONNECTED: - connectionEvent(DISCONNECTED); - break; - - default: - break; - } - } -#endif - - -//----------------------------------------------------------------------------- -void ahoywifi::welcome(String ip, String mode) { - DBGPRINTLN(F("\n\n--------------------------------")); - DBGPRINTLN(F("Welcome to AHOY!")); - DBGPRINT(F("\npoint your browser to http://")); - DBGPRINT(ip); - DBGPRINTLN(mode); - DBGPRINTLN(F("to configure your device")); - DBGPRINTLN(F("--------------------------------\n")); -} - -#endif /* !defined(ETHERNET) */ diff --git a/src/wifi/ahoywifi.h b/src/wifi/ahoywifi.h deleted file mode 100644 index d38701aa..00000000 --- a/src/wifi/ahoywifi.h +++ /dev/null @@ -1,97 +0,0 @@ -//------------------------------------//----------------------------------------------------------------------------- -// 2024 Ahoy, https://github.com/lumpapu/ahoy -// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed -//----------------------------------------------------------------------------- - -#if !defined(ETHERNET) -#ifndef __AHOYWIFI_H__ -#define __AHOYWIFI_H__ - -#include "../utils/dbg.h" -#include <Arduino.h> -#include <WiFiUdp.h> -#include <DNSServer.h> -#include "ESPAsyncWebServer.h" - -#include "../config/settings.h" - -class app; - -class ahoywifi { - public: - typedef std::function<void(bool)> appWifiCb; - - ahoywifi(); - - - void setup(settings_t *config, uint32_t *utcTimestamp, appWifiCb cb); - void tickWifiLoop(void); - bool getNtpTime(void); - void scanAvailNetworks(void); - bool getAvailNetworks(JsonObject obj); - void setStopApAllowedMode(bool allowed) { - mStopApAllowed = allowed; - } - String getStationIp(void) { - return WiFi.localIP().toString(); - } - void setupStation(void); - - bool getWasInCh12to14() const { - return mWasInCh12to14; - } - - private: - typedef enum WiFiStatus { - DISCONNECTED = 0, - SCAN_READY, - CONNECTING, - CONNECTED, - IN_AP_MODE, - GOT_IP, - IN_STA_MODE, - RESET - } WiFiStatus_t; - - void setupWifi(bool startAP); - void setupAp(void); - void sendNTPpacket(IPAddress& address); - void sortRSSI(int *sort, int n); - bool getBSSIDs(void); - void connectionEvent(WiFiStatus_t status); - bool isTimeout(uint8_t timeout) { return (mCnt % timeout) == 0; } - -#if defined(ESP8266) - void onConnect(const WiFiEventStationModeConnected& event); - void onGotIP(const WiFiEventStationModeGotIP& event); - void onDisconnect(const WiFiEventStationModeDisconnected& event); - #else - void onWiFiEvent(WiFiEvent_t event); - #endif - void welcome(String ip, String mode); - - - settings_t *mConfig = nullptr; - appWifiCb mAppWifiCb; - - DNSServer mDns; - IPAddress mApIp; - WiFiUDP mUdp; // for time server - #if defined(ESP8266) - WiFiEventHandler wifiConnectHandler, wifiDisconnectHandler, wifiGotIPHandler; - #endif - - WiFiStatus_t mStaConn = DISCONNECTED; - uint8_t mCnt = 0; - uint32_t *mUtcTimestamp = nullptr; - - uint8_t mScanCnt = 0; - bool mScanActive = false; - bool mGotDisconnect = false; - std::list<uint8_t> mBSSIDList; - bool mStopApAllowed = false; - bool mWasInCh12to14 = false; -}; - -#endif /*__AHOYWIFI_H__*/ -#endif /* !defined(ETHERNET) */ diff --git a/tools/NodeRED/flows-mqtt-json-example.json b/tools/NodeRED/flows-mqtt-json-example.json new file mode 100644 index 00000000..5e2e09a1 --- /dev/null +++ b/tools/NodeRED/flows-mqtt-json-example.json @@ -0,0 +1,466 @@ +[ + { + "id": "67bced2c4e728783", + "type": "mqtt in", + "z": "5de5756d190f9086", + "name": "", + "topic": "hoymiles/+", + "qos": "0", + "datatype": "auto-detect", + "broker": "319864a4e0fd913f", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 80, + "y": 2100, + "wires": [ + [ + "a55632ad0dff0b69" + ] + ] + }, + { + "id": "a7f0d307d7cf77e2", + "type": "mqtt in", + "z": "5de5756d190f9086", + "name": "", + "topic": "hoymiles/X/#", + "qos": "0", + "datatype": "auto-detect", + "broker": "319864a4e0fd913f", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 90, + "y": 2260, + "wires": [ + [ + "7e17e5a3f4df3011", + "1a8cca488d53394a" + ] + ] + }, + { + "id": "7e17e5a3f4df3011", + "type": "debug", + "z": "5de5756d190f9086", + "name": "Inverter X", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 340, + "y": 2260, + "wires": [] + }, + { + "id": "fb7357db50501627", + "type": "change", + "z": "5de5756d190f9086", + "name": "Tags setzen", + "rules": [ + { + "t": "set", + "p": "payload", + "pt": "msg", + "to": "(\t $a := $split(topic, '/');\t [\t payload,\t {\t \"device\":$a[0],\t \"name\":$a[1],\t \"channel\":$a[2]\t }\t ]\t)\t", + "tot": "jsonata" + }, + { + "t": "delete", + "p": "topic", + "pt": "msg" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 610, + "y": 2360, + "wires": [ + [ + "91a4607dfda84b67" + ] + ] + }, + { + "id": "670eb9fbb5c31b2c", + "type": "debug", + "z": "5de5756d190f9086", + "name": "InfluxDB", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 940, + "y": 2360, + "wires": [] + }, + { + "id": "1a8cca488d53394a", + "type": "switch", + "z": "5de5756d190f9086", + "name": "", + "property": "$split(topic, '/')[2]", + "propertyType": "jsonata", + "rules": [ + { + "t": "eq", + "v": "available", + "vt": "str" + }, + { + "t": "eq", + "v": "last_success", + "vt": "str" + }, + { + "t": "regex", + "v": "(ch[0-6])\\b", + "vt": "str", + "case": false + }, + { + "t": "eq", + "v": "radio_stat", + "vt": "str" + }, + { + "t": "eq", + "v": "firmware", + "vt": "str" + }, + { + "t": "eq", + "v": "hardware", + "vt": "str" + }, + { + "t": "eq", + "v": "alarm", + "vt": "str" + } + ], + "checkall": "true", + "repair": false, + "outputs": 7, + "x": 330, + "y": 2380, + "wires": [ + [ + "845aeb93e39092c5" + ], + [ + "241a8e70e9fde93c" + ], + [ + "fb7357db50501627" + ], + [ + "9d38f021308664c1" + ], + [ + "a508355f0cc87966" + ], + [ + "d2c9aa1a8978aca6" + ], + [ + "b27032beb597d5a7" + ] + ] + }, + { + "id": "845aeb93e39092c5", + "type": "debug", + "z": "5de5756d190f9086", + "name": "available", + "active": true, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 600, + "y": 2240, + "wires": [] + }, + { + "id": "241a8e70e9fde93c", + "type": "debug", + "z": "5de5756d190f9086", + "name": "last_success", + "active": true, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 610, + "y": 2300, + "wires": [] + }, + { + "id": "9d38f021308664c1", + "type": "debug", + "z": "5de5756d190f9086", + "name": "radio_stat", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 600, + "y": 2400, + "wires": [] + }, + { + "id": "a508355f0cc87966", + "type": "debug", + "z": "5de5756d190f9086", + "name": "firmware", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 600, + "y": 2440, + "wires": [] + }, + { + "id": "d2c9aa1a8978aca6", + "type": "debug", + "z": "5de5756d190f9086", + "name": "hardware", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 600, + "y": 2480, + "wires": [] + }, + { + "id": "b27032beb597d5a7", + "type": "debug", + "z": "5de5756d190f9086", + "name": "alarm", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 590, + "y": 2520, + "wires": [] + }, + { + "id": "d814738cf55ad663", + "type": "debug", + "z": "5de5756d190f9086", + "name": "total", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 590, + "y": 2160, + "wires": [] + }, + { + "id": "a55632ad0dff0b69", + "type": "switch", + "z": "5de5756d190f9086", + "name": "", + "property": "$split(topic, '/')[1]", + "propertyType": "jsonata", + "rules": [ + { + "t": "eq", + "v": "uptime", + "vt": "str" + }, + { + "t": "eq", + "v": "wifi_rssi", + "vt": "str" + }, + { + "t": "eq", + "v": "status", + "vt": "str" + }, + { + "t": "eq", + "v": "total", + "vt": "str" + } + ], + "checkall": "true", + "repair": false, + "outputs": 4, + "x": 330, + "y": 2100, + "wires": [ + [ + "1fbb0674d2576ee7" + ], + [ + "e6be1c98ac55f511" + ], + [ + "f9c2d3b30e34fdda" + ], + [ + "d814738cf55ad663" + ] + ] + }, + { + "id": "f9c2d3b30e34fdda", + "type": "debug", + "z": "5de5756d190f9086", + "name": "status", + "active": false, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 590, + "y": 2100, + "wires": [] + }, + { + "id": "e6be1c98ac55f511", + "type": "debug", + "z": "5de5756d190f9086", + "name": "wifi_rssi", + "active": false, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 600, + "y": 2040, + "wires": [] + }, + { + "id": "1fbb0674d2576ee7", + "type": "debug", + "z": "5de5756d190f9086", + "name": "uptime", + "active": false, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 590, + "y": 1980, + "wires": [] + }, + { + "id": "91a4607dfda84b67", + "type": "change", + "z": "5de5756d190f9086", + "name": "Lösche", + "rules": [ + { + "t": "delete", + "p": "payload[0].YieldDay", + "pt": "msg" + }, + { + "t": "delete", + "p": "payload[0].MaxPower", + "pt": "msg" + }, + { + "t": "delete", + "p": "payload[0].ALARM_MES_ID", + "pt": "msg" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 780, + "y": 2360, + "wires": [ + [ + "670eb9fbb5c31b2c" + ] + ] + }, + { + "id": "319864a4e0fd913f", + "type": "mqtt-broker", + "name": "broker", + "broker": "localhost", + "port": "1883", + "clientid": "", + "autoConnect": true, + "usetls": false, + "protocolVersion": "4", + "keepalive": "60", + "cleansession": true, + "birthTopic": "", + "birthQos": "0", + "birthPayload": "", + "birthMsg": {}, + "closeTopic": "", + "closeQos": "0", + "closePayload": "", + "closeMsg": {}, + "willTopic": "", + "willQos": "0", + "willPayload": "", + "willMsg": {}, + "userProps": "", + "sessionExpiry": "" + } +] \ No newline at end of file