Compare commits

...
27 Commits
Author SHA1 Message Date
Proddy de9a082668 Merge pull request #3282 from emsesp/dependabot/github_actions/github-actions-6de4297576
Bump SonarSource/sonarqube-scan-action from 8.2.2 to 8.3.0 in the github-actions group
2026-10-11 16:50:41 +02:00
proddy 5ae89714c2 chore: update generated files for v3.9.0-dev.18 2026-10-11 14:33:31 +00:00
dependabot[bot] 84060449cd Bump SonarSource/sonarqube-scan-action
Bumps the github-actions group with 1 update: [SonarSource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action).


Updates `SonarSource/sonarqube-scan-action` from 8.2.2 to 8.3.0
- [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases)
- [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/v8.2.2...v8.3.0)

---
updated-dependencies:
- dependency-name: SonarSource/sonarqube-scan-action
  dependency-version: 8.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-11 14:27:10 +00:00
Proddy f9c9496c71 Merge pull request #3281 from proddy/dev
Recovery App and new Arduino custom-built libs
2026-10-11 16:25:10 +02:00
Proddy 546e07a534 Merge branch 'dev' into dev 2026-10-11 16:25:01 +02:00
proddy 956f7ab098 show recovery app in Version page 2026-10-11 16:23:08 +02:00
proddy 9934d05c5a use latest esp32-arduino-lib-builder 2026-10-11 11:55:33 +02:00
proddy 33a38c30fe optimize build 2026-10-11 10:43:47 +02:00
proddy 43778328e1 remove 2026-10-11 10:37:43 +02:00
proddy e7fd8bcdf0 don't run on forlks 2026-10-11 10:35:41 +02:00
proddy 229187d912 check GH action updates 2026-10-11 10:35:27 +02:00
proddy fe4fe7ef28 add comments 2026-10-11 10:35:14 +02:00
proddy 0094df41d9 package update 2026-10-11 10:34:39 +02:00
proddy ec49237bd9 chore: update generated files for v3.9.0-dev.18 2026-10-10 11:41:25 +00:00
proddy b730716791 update runners 2026-10-10 13:33:08 +02:00
proddy 62f2987b56 update changelog, use new core libs 2026-10-10 12:59:46 +02:00
proddyandCursor e10983cca1 Merge branch 'feat/custom-sdkconfig' into dev
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 12:34:51 +02:00
proddy 04d373b93c 3.9.0-dev.18 2026-10-10 00:00:00 +02:00
proddy c959cb1e8a #3275 2026-10-09 23:55:15 +02:00
proddy 09aa40e604 use new arduino core lib 2026-10-09 18:10:33 +02:00
proddy e3b1eb6c51 test new esp32-arduino-lib-builder packages 2026-10-09 17:47:01 +02:00
Proddy 673c7462f5 Merge branch 'emsesp:dev' into feat/custom-sdkconfig 2026-10-09 16:20:14 +02:00
proddy eaaea504df rename tasmota, fix includes 2026-10-09 08:36:15 +02:00
proddy cefd5a14cf fix include conflicts 2026-10-09 08:35:59 +02:00
Proddy 46f2090b30 Fix platformio.ini: remove duplicated content from previous commit 2026-10-08 23:34:11 +02:00
Proddy 07514260f4 Move custom_sdkconfig trial env from s3_16M_P to s_16M_P 2026-10-08 23:32:23 +02:00
Proddy dfe009f5c3 Add s3_16M_P_custom env to trial a trimmed framework via custom_sdkconfig 2026-10-08 23:28:40 +02:00
64 changed files with 16604 additions and 724 deletions
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
github-actions:
patterns:
- "*"
+251 -26
View File
@@ -11,30 +11,209 @@ on:
permissions:
contents: write
# Parallel graph (wall-clock is ~max of the firmware jobs, not the sum):
# webui ──────┐
# ├─► firmware (s_4M / s_16M / s_16M_P / s3_16M_P) ─┐
# modbus ─────┤ ├─► release
# └─► standalone ───────────────────────────────────┘
#
# Extra -j on a single runner does not help: PlatformIO already compiles each
# environment with all CPUs. The win is separate GitHub runners.
jobs:
pre-release:
name: 'Build Dev Release'
webui:
name: Build webUI
if: github.event_name == 'workflow_dispatch' || github.repository == 'emsesp/EMS-ESP32'
runs-on: ubuntu-latest
steps:
- name: Install python 3.13
uses: actions/setup-python@v7
- name: Checkout repository
uses: actions/checkout@v7
- name: Install pnpm
uses: pnpm/action-setup@v6
with:
python-version: '3.13'
package_json_file: interface/package.json
- name: Install Node.js 26
uses: actions/setup-node@v7
with:
node-version: 26
- name: Cache pnpm store
uses: actions/cache@v6
with:
path: ~/.local/share/pnpm/store
key: ${{ runner.os }}-pnpm-${{ hashFiles('interface/package.json') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Build webUI
env:
CI: true
working-directory: interface
run: |
pnpm install
pnpm build-webUI
- name: Upload WWWData.h
uses: actions/upload-artifact@v7
with:
name: wwwdata
path: src/ESP32React/WWWData.h
if-no-files-found: error
modbus:
name: Build modbus
if: github.event_name == 'workflow_dispatch' || github.repository == 'emsesp/EMS-ESP32'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install python 3.13
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install pioarduino
run: uv pip install --system pioarduino==6.2.0
- name: Build modbus
run: platformio run -e build_modbus
- name: Upload modbus header
uses: actions/upload-artifact@v7
with:
name: modbus-header
path: src/core/modbus_entity_parameters.hpp
if-no-files-found: error
standalone:
name: Build standalone
needs: modbus
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install python 3.13
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install pioarduino
run: uv pip install --system pioarduino==6.2.0
- name: Restore generated modbus header
uses: actions/download-artifact@v8
with:
name: modbus-header
path: src/core
- name: Build standalone
run: platformio run -e build_standalone
- name: Upload generated docs
uses: actions/upload-artifact@v7
with:
name: generated-docs
path: |
docs/dump_entities.csv
docs/dump_telegrams.csv
docs/Modbus-Entity-Registers.md
if-no-files-found: error
firmware:
name: Firmware ${{ matrix.env }}
needs: [webui, modbus]
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
# Keep in sync with platformio.ini default_envs
env: [s_4M, s_16M, s_16M_P, s3_16M_P]
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install python 3.13
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Cache PlatformIO packages
uses: actions/cache@v6
with:
path: |
~/.platformio/.cache
~/.platformio/packages
~/.platformio/platforms
key: ${{ runner.os }}-pio-${{ matrix.env }}-${{ hashFiles('platformio.ini') }}
restore-keys: |
${{ runner.os }}-pio-${{ matrix.env }}-
${{ runner.os }}-pio-
- name: Install pioarduino
run: uv pip install --system pioarduino==6.2.0
- name: Restore WWWData.h
uses: actions/download-artifact@v8
with:
name: wwwdata
path: src/ESP32React
- name: Restore generated modbus header
uses: actions/download-artifact@v8
with:
name: modbus-header
path: src/core
- name: Setup ccache
uses: hendrikmuhs/ccache-action@v1.2.24
with:
key: firmware-${{ matrix.env }}
restore-keys: firmware-${{ matrix.env }}-
max-size: 1G
- name: Build ${{ matrix.env }}
env:
CCACHE_COMPILERCHECK: content
run: platformio run -e ${{ matrix.env }}
- name: Upload firmware
uses: actions/upload-artifact@v7
with:
name: firmware-${{ matrix.env }}
path: build/firmware
if-no-files-found: error
release:
name: Create GitHub Release
needs: [firmware, standalone]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install pnpm
run: npm install -g pnpm
- name: Get the EMS-ESP version
id: build_info
@@ -49,27 +228,73 @@ jobs:
prev=`git rev-parse -q --verify 'refs/tags/latest^{commit}' || git describe --tags --abbrev=0 --match 'v*'`
echo "REF=$prev" >> $GITHUB_OUTPUT
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install -U platformio
python -m pip install intelhex
- name: Restore generated modbus header
uses: actions/download-artifact@v8
with:
name: modbus-header
path: src/core
- name: Build webUI
run: |
platformio run -e build-webUI
- name: Restore generated docs
uses: actions/download-artifact@v8
with:
name: generated-docs
path: .
- name: Build modbus
run: |
platformio run -e build_modbus
- name: Restore firmware binaries
uses: actions/download-artifact@v8
with:
pattern: firmware-*
merge-multiple: true
path: build/firmware
- name: Build standalone
run: |
platformio run -e build_standalone
- name: List firmware files
run: ls -la build/firmware
- name: Build all PIO target environments, from default_envs
run: |
platformio run
# the recovery build needs PlatformIO, and pnpm + Node.js to build its own WebUI (recovery/WWWData.h)
- name: Install python 3.13
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
# falls back to the firmware jobs' caches, which hold the same toolchains
- name: Cache PlatformIO packages
uses: actions/cache@v6
with:
path: |
~/.platformio/.cache
~/.platformio/packages
~/.platformio/platforms
key: ${{ runner.os }}-pio-recovery-${{ hashFiles('platformio.ini') }}
restore-keys: |
${{ runner.os }}-pio-recovery-
${{ runner.os }}-pio-
- name: Install pioarduino
run: uv pip install --system pioarduino==6.2.0
- name: Install pnpm
uses: pnpm/action-setup@v6
with:
package_json_file: interface/package.json
- name: Install Node.js 26
uses: actions/setup-node@v7
with:
node-version: 26
- name: Cache pnpm store
uses: actions/cache@v6
with:
path: ~/.local/share/pnpm/store
key: ${{ runner.os }}-pnpm-${{ hashFiles('interface/package.json') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Build the recovery firmware for the 16MB boards
run: |
+2 -2
View File
@@ -22,10 +22,10 @@ jobs:
with:
python-version: '3.13'
- name: Install PlatformIO
- name: Install pioarduino
run: |
pip install wheel
pip install -U platformio
pip install -U "pioarduino==6.2.0"
- name: Run unit tests
run: |
+2 -2
View File
@@ -23,11 +23,11 @@ jobs:
with:
fetch-depth: 0
- name: Install Build Wrapper
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@v8.2.2
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@v8.3.0
- name: Run Build Wrapper
run: build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v8.2.2
uses: SonarSource/sonarqube-scan-action@v8.3.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+233 -24
View File
@@ -9,28 +9,197 @@ on:
tags:
- 'v*'
# Parallel graph (wall-clock is ~max of the firmware jobs, not the sum):
# webui ──────┐
# ├─► firmware (s_4M / s_16M / s_16M_P / s3_16M_P) ─┐
# modbus ─────┤ ├─► release
# └─► standalone ───────────────────────────────────┘
#
# Extra -j on a single runner does not help: PlatformIO already compiles each
# environment with all CPUs. The win is separate GitHub runners.
jobs:
tagged-release:
name: 'Build Stable Release'
webui:
name: Build webUI
if: github.event_name == 'workflow_dispatch' || github.repository == 'emsesp/EMS-ESP32'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install python 3.13
uses: actions/setup-python@v7
- name: Install pnpm
uses: pnpm/action-setup@v6
with:
python-version: '3.13'
package_json_file: interface/package.json
- name: Install Node.js 26
uses: actions/setup-node@v7
with:
node-version: 26
- name: Cache pnpm store
uses: actions/cache@v6
with:
path: ~/.local/share/pnpm/store
key: ${{ runner.os }}-pnpm-${{ hashFiles('interface/package.json') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Build webUI
env:
CI: true
working-directory: interface
run: |
pnpm install
pnpm build-webUI
- name: Upload WWWData.h
uses: actions/upload-artifact@v7
with:
name: wwwdata
path: src/ESP32React/WWWData.h
if-no-files-found: error
modbus:
name: Build modbus
if: github.event_name == 'workflow_dispatch' || github.repository == 'emsesp/EMS-ESP32'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install pnpm
run: npm install -g pnpm
- name: Install python 3.13
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install pioarduino
run: uv pip install --system pioarduino==6.2.0
- name: Build modbus
run: platformio run -e build_modbus
- name: Upload modbus header
uses: actions/upload-artifact@v7
with:
name: modbus-header
path: src/core/modbus_entity_parameters.hpp
if-no-files-found: error
standalone:
name: Build standalone
needs: modbus
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install python 3.13
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Install pioarduino
run: uv pip install --system pioarduino==6.2.0
- name: Restore generated modbus header
uses: actions/download-artifact@v8
with:
name: modbus-header
path: src/core
- name: Build standalone
run: platformio run -e build_standalone
firmware:
name: Firmware ${{ matrix.env }}
needs: [webui, modbus]
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
# Keep in sync with platformio.ini default_envs
env: [s_4M, s_16M, s_16M_P, s3_16M_P]
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install python 3.13
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Cache PlatformIO packages
uses: actions/cache@v6
with:
path: |
~/.platformio/.cache
~/.platformio/packages
~/.platformio/platforms
key: ${{ runner.os }}-pio-${{ matrix.env }}-${{ hashFiles('platformio.ini') }}
restore-keys: |
${{ runner.os }}-pio-${{ matrix.env }}-
${{ runner.os }}-pio-
- name: Install pioarduino
run: uv pip install --system pioarduino==6.2.0
- name: Restore WWWData.h
uses: actions/download-artifact@v8
with:
name: wwwdata
path: src/ESP32React
- name: Restore generated modbus header
uses: actions/download-artifact@v8
with:
name: modbus-header
path: src/core
- name: Setup ccache
uses: hendrikmuhs/ccache-action@v1.2.24
with:
key: firmware-${{ matrix.env }}
restore-keys: firmware-${{ matrix.env }}-
max-size: 1G
- name: Build ${{ matrix.env }}
env:
CCACHE_COMPILERCHECK: content
run: platformio run -e ${{ matrix.env }}
- name: Upload firmware
uses: actions/upload-artifact@v7
with:
name: firmware-${{ matrix.env }}
path: build/firmware
if-no-files-found: error
release:
name: Create GitHub Release
needs: [firmware, standalone]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Get the EMS-ESP version
id: build_info
@@ -38,32 +207,72 @@ jobs:
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}'`
echo "VERSION=$version" >> $GITHUB_OUTPUT
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install -U platformio
python -m pip install intelhex
- name: Restore generated modbus header
uses: actions/download-artifact@v8
with:
name: modbus-header
path: src/core
- name: Build webUI
run: |
platformio run -e build-webUI
- name: Restore firmware binaries
uses: actions/download-artifact@v8
with:
pattern: firmware-*
merge-multiple: true
path: build/firmware
- name: Build modbus
run: |
platformio run -e build_modbus
# the recovery build needs PlatformIO, and pnpm + Node.js to build its own WebUI (recovery/WWWData.h)
- name: Install python 3.13
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Build standalone
run: |
platformio run -e build_standalone
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: false
- name: Build all PIO target environments, from default_envs
run: |
platformio run
# falls back to the firmware jobs' caches, which hold the same toolchains
- name: Cache PlatformIO packages
uses: actions/cache@v6
with:
path: |
~/.platformio/.cache
~/.platformio/packages
~/.platformio/platforms
key: ${{ runner.os }}-pio-recovery-${{ hashFiles('platformio.ini') }}
restore-keys: |
${{ runner.os }}-pio-recovery-
${{ runner.os }}-pio-
- name: Install pioarduino
run: uv pip install --system pioarduino==6.2.0
- name: Install pnpm
uses: pnpm/action-setup@v6
with:
package_json_file: interface/package.json
- name: Install Node.js 26
uses: actions/setup-node@v7
with:
node-version: 26
- name: Cache pnpm store
uses: actions/cache@v6
with:
path: ~/.local/share/pnpm/store
key: ${{ runner.os }}-pnpm-${{ hashFiles('interface/package.json') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Build the recovery firmware for the 16MB boards
run: |
platformio run -e recovery_s_16M -e recovery_s_16M_P -e recovery_s3_16M_P
- name: List firmware files
run: ls -la build/firmware
- name: Create GitHub Release
uses: emsesp/action-automatic-releases@v1.0.1
with:
+2
View File
@@ -10,6 +10,8 @@ on:
jobs:
stale:
name: Mark or close stale issues and PRs
if: github.repository == 'emsesp/EMS-ESP32'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v11
-79
View File
@@ -1,79 +0,0 @@
name: 'Build test release'
on:
workflow_dispatch:
push:
branches:
- 'test'
permissions:
contents: read
jobs:
pre-release:
name: 'Build Test Release'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Install python 3.13
uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install Node.js 26
uses: actions/setup-node@v7
with:
node-version: 26
- name: Checkout repository
uses: actions/checkout@v7
- name: Install pnpm
run: npm install -g pnpm
- name: Get the EMS-ESP version
id: build_info
run: |
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}'`
echo "VERSION=$version" >> $GITHUB_OUTPUT
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
pip install -U platformio
python -m pip install intelhex
- name: Build webUI
run: |
platformio run -e build-webUI
- name: Build modbus
run: |
platformio run -e build_modbus
- name: Build standalone
run: |
platformio run -e build_standalone
- name: Build all PIO target environments, from default_envs
run: |
platformio run
- name: Build the recovery firmware for the 16MB boards
run: |
platformio run -e recovery_s_16M -e recovery_s_16M_P -e recovery_s3_16M_P
- name: Create GitHub Release
id: 'automatic_releases'
uses: emsesp/action-automatic-releases@v1.0.1
with:
repo_token: '${{ secrets.GITHUB_TOKEN }}'
title: Test Build v${{steps.build_info.outputs.VERSION}}
automatic_release_tag: 'test'
prerelease: true
files: |
CHANGELOG_LATEST.md
./build/firmware/*.*
+8
View File
@@ -25,6 +25,9 @@ This release is based on the latest Espressif/Arduino core version 3. It brings
- on 16MB boards with the Recovery installed, EMS-ESP switches to it after crashing 5 times in a row. The Recovery shows why it was started and which firmware failed to start
- the Version page shows the installed Recovery version on 16MB boards
- crash test builds to try out the rollback and recovery, see `[env:crashtest_s3_16M_P]` in `platformio.ini`
- `s_16M_P_custom` PlatformIO env that HybridCompiles Arduino/IDF with a trimmed sdkconfig (no Bluetooth, PPP/modem, IP forward/NAPT, ULP, SPI Ethernet, GDBSTUB, FreeRTOS trace, mDNS browse/CLI) and places the UART ISR in IRAM
- 16MB builds use EMS-ESP's own Arduino/IDF libs from [esp32-arduino-lib-builder](https://github.com/emsesp/esp32-arduino-lib-builder) instead of Tasmota's prebuilt cores
- optional `ccache` wrapping when it is installed (`scripts/enable_ccache.py`) to speed up CI
- heat pump `dhw/disinfectduration`, the time allowed to reach the thermal disinfection temperature (0xEA offset 15, 120-240 min) [#3278](https://github.com/emsesp/EMS-ESP32/issues/3278)
## Fixed
@@ -51,6 +54,9 @@ This release is based on the latest Espressif/Arduino core version 3. It brings
- add 0kW to heat limits [#3262](https://github.com/emsesp/EMS-ESP32/issues/3262), [#3265](https://github.com/emsesp/EMS-ESP32/issues/3265)
- show temperature value for sensor 0x40 [#3272](https://github.com/emsesp/EMS-ESP32/issues/3272)
- no EMS bus on custom boards whose interface relies on a pull-up on the Rx line. The newer ESP-IDF no longer enables the internal pull-up on the UART Rx pin, so it is now set explicitly as it was in 3.8 [#3258](https://github.com/emsesp/EMS-ESP32/discussions/3258)
- Guru Meditation (cache error) on HybridCompile builds when the EMS or console UART ISR ran from flash during a SPI flash read (WiFi power-down, mDNS, NVS). The UART ISR and ringbuf ISR helpers are now in IRAM
- compile errors against Arduino 3.3.12 from Tasmota/Arduino headers named `console.h`, `network.h` and `httpClient.h`. Those EMS-ESP headers are now `emsesp_console.h`, `emsesp_network.h` and `emsesp_http_client.h`
- HTTP OTA from WSL/mDNS stalled on IPv6 and used tiny write chunks. Uploads now resolve IPv4 and stream in 64 KB blocks (`scripts/upload.py`, `scripts/upload_cli.py`)
## Changed
@@ -69,3 +75,5 @@ This release is based on the latest Espressif/Arduino core version 3. It brings
- failed versions.json fetches back off from 5 up to 160 minutes instead of retrying every 5 minutes, as each attempt blocks the main loop for as long as the DNS, connect and read timeouts allow
- optimized the LED library to support WS2812B V5 and the newer V6 RGB LEDs
- hold Tx while a K30RF/KM200 gateway is syncing [#3094](https://github.com/emsesp/EMS-ESP32/issues/3094)
- Tasmota platform 2026.05.50 (Arduino 3.3.8 / IDF 5.5.4) to 2026.09.50 (Arduino 3.3.12 / IDF 5.5.5). Building that platform needs PlatformIO Core ≥6.2.0; install `pioarduino==6.2.0` (official `platformio` is still 6.1.19). See CONTRIBUTING.md
- CI dev-release firmware jobs run in parallel (webUI, firmware targets, standalone) instead of one serial job
+34
View File
@@ -10,6 +10,40 @@ Everybody is welcome and invited to contribute to the EMS-ESP Project by:
This document describes rules that are in effect for this repository, meant for handling issues by contributors in the issue tracker and PRs.
## Building the firmware
Arduino/IDF 3.3.12 (Tasmota platform `2026.09.50` in `platformio.ini`) needs **PlatformIO Core ≥6.2.0**. The official `platformio` package on PyPI is still 6.1.19, so `pio upgrade` and `pip install -U platformio` stay on 6.1.19 and you get:
`IncompatiblePlatform: Development platform 'espressif32' is not compatible with PlatformIO Core v6.1.19 and depends on PlatformIO Core >=6.2.0.`
Install **pioarduino** 6.2.0 (drop-in Core fork). It provides the same `pio` / `platformio` commands.
**VS Code / Cursor** (PlatformIO IDE uses `~/.platformio/penv`):
Linux / macOS:
```bash
~/.platformio/penv/bin/pip install -U "pioarduino==6.2.0"
~/.platformio/penv/bin/pio --version
```
Windows (PowerShell):
```powershell
& "$env:USERPROFILE\.platformio\penv\Scripts\pip.exe" install -U "pioarduino==6.2.0"
& "$env:USERPROFILE\.platformio\penv\Scripts\pio.exe" --version
```
You should see `PlatformIO Core, version 6.2.0`. Reload the window (or restart the IDE), then build again.
**CLI-only / CI:**
```bash
pip install -U "pioarduino==6.2.0"
# or: uv pip install --system pioarduino==6.2.0
platformio --version
```
## Opening New Issues
1. Opening an issue means that a problem exists in the code and should be addressed by the project contributors.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -21,7 +21,7 @@
"arduino",
"espidf"
],
"name": "Tasmota ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
"name": "ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "32MB",
"maximum_ram_size": 327680,
+1 -1
View File
@@ -18,7 +18,7 @@
"arduino",
"espidf"
],
"name": "Tasmota ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
"name": "ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
+1 -1
View File
@@ -19,7 +19,7 @@
"arduino",
"espidf"
],
"name": "Tasmota ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
"name": "ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
+1 -1
View File
@@ -17,7 +17,7 @@
"arduino",
"espidf"
],
"name": "Tasmota ESP32 4M Flash, 4608KB Code/OTA, 2MB FS",
"name": "ESP32 4M Flash, 4608KB Code/OTA, 2MB FS",
"upload": {
"flash_size": "4MB",
"maximum_ram_size": 327680,
+6072
View File
File diff suppressed because it is too large Load Diff
+238
View File
@@ -0,0 +1,238 @@
telegram_type_id,name,is_fetched
0x04,UBAFactory,fetched
0x06,RCTime,
0x0A,EasyMonitor,fetched
0x10,UBAErrorMessage1,
0x11,UBAErrorMessage2,
0x12,RCErrorMessage,
0x13,RCErrorMessage2,
0x14,UBATotalUptime,fetched
0x15,UBAMaintenanceData,
0x16,UBAParameters,fetched
0x18,UBAMonitorFast,
0x19,UBAMonitorSlow,
0x1A,UBASetPoints,
0x1C,UBAMaintenanceStatus,
0x1E,HydrTemp,
0x23,JunkersSetMixer,fetched
0x27,UBASettingsWW,fetched
0x28,WeatherComp,fetched
0x2A,MC110Status,
0x2E,Meters,
0x33,UBAParameterWW,fetched
0x34,UBAMonitorWW,
0x35,UBAFlags,
0x37,WWSettings,fetched
0x38,WWTimer,fetched
0x39,WWCircTimer,fetched
0x3A,RC30WWSettings,fetched
0x3B,Energy,
0x3D,RC35Set,
0x3E,RC35Monitor,
0x3F,RC30Timer,
0x40,RC30Temp,
0x41,RC30Monitor,
0x42,RC35Timer2,
0x47,RC35Set,
0x48,RC35Monitor,
0x49,RC35Timer,
0x4C,RC35Timer2,
0x51,RC35Set,
0x52,RC35Monitor,
0x53,RC35Timer,
0x56,RC35Timer2,
0x5B,RC35Set,
0x5C,RC35Monitor,
0x5D,RC35Timer,
0x60,RC35Timer2,
0x96,SM10Config,fetched
0x97,SM10Monitor,
0x9C,WM10MonitorMessage,
0x9D,WM10SetMessage,
0xA2,RCError,
0xA3,RCOutdoorTemp,
0xA5,IBASettings,fetched
0xA7,RC30Set,
0xA9,RC30Vacation,fetched
0xAA,MMConfigMessage,fetched
0xAB,MMStatusMessage,
0xAC,MMSetMessage,
0xAF,RC20Remote,
0xB0,RC10Set,
0xB1,RC10Monitor,
0xBB,HybridSettings,fetched
0xBF,ErrorMessage,
0xC0,RCErrorMessage,
0xC2,UBAErrorMessage3,
0xC6,UBAErrorMessage3,
0xD1,UBAOutdoorTemp,
0xE3,UBAMonitorSlowPlus2,
0xE4,UBAMonitorFastPlus,
0xE5,UBAMonitorSlowPlus,
0xE6,UBAParametersPlus,fetched
0xE9,UBAMonitorWWPlus,
0xEA,UBAParameterWWPlus,fetched
0xEB,PumpKick,fetched
0x0101,ISM1Set,fetched
0x0103,ISM1StatusMessage,fetched
0x0104,ISM2StatusMessage,
0x010C,IPMStatusMessage,
0x011E,JunkersDisp,fetched
0x012E,HPEnergy1,
0x013B,HPEnergy2,
0x0165,JunkersSet,
0x0166,JunkersSet,
0x0167,JunkersSet,
0x0168,JunkersSet,
0x016E,Absent,fetched
0x016F,JunkersMonitor,
0x0170,JunkersMonitor,
0x0171,JunkersMonitor,
0x0172,JunkersMonitor,
0x0179,JunkersSet,
0x017A,JunkersSet,
0x017B,JunkersSet,
0x017C,JunkersSet,
0x01D3,JunkersDhw,fetched
0x023A,RC300OutdoorTemp,fetched
0x023E,PVSettings,fetched
0x0240,RC300Settings,fetched
0x0241,RC300Settings,fetched
0x0267,RC300Floordry,
0x0269,RC300Holiday,fetched
0x0291,HPMode,fetched
0x0292,HPMode,fetched
0x0293,HPMode,fetched
0x0294,HPMode,fetched
0x029B,RC300Curves,
0x029C,RC300Curves,
0x029D,RC300Curves,
0x029E,RC300Curves,
0x029F,RC300Curves,
0x02A0,RC300Curves,
0x02A1,RC300Curves,
0x02A2,RC300Curves,
0x02A5,CRFMonitor,
0x02A6,RC300Monitor,
0x02A7,RC300Monitor,
0x02A8,RC300Monitor,
0x02A9,RC300Monitor,
0x02AA,RC300Monitor,
0x02AB,RC300Monitor,
0x02AC,RC300Monitor,
0x02AF,RC300Summer,
0x02B0,RC300Summer,
0x02B1,RC300Summer,
0x02B2,RC300Summer,
0x02B3,RC300Summer,
0x02B4,RC300Summer,
0x02B5,RC300Summer,
0x02B6,RC300Summer,
0x02B9,RC300Set,
0x02BA,RC300Set,
0x02BB,RC300Set,
0x02BC,RC300Set,
0x02BD,RC300Set,
0x02BE,RC300Set,
0x02BF,RC300Set,
0x02C0,RC300Set,
0x02CC,HPPressure,fetched
0x02CD,MMPLUSConfigMessage,
0x02D6,HPPump2,fetched
0x02D7,MMPLUSStatusMessage,
0x02E0,UBASetPoints,
0x02F5,RC300WWmode,fetched
0x02F6,RC300WW2mode,fetched
0x0313,MMPLUSConfigMessage_WWC,fetched
0x031B,RC300WWtemp,fetched
0x031D,RC300WWmode2,
0x031E,RC300WWmode2,
0x0331,MMPLUSStatusMessage_WWC,
0x0358,SM100SystemConfig,fetched
0x035A,SM100CircuitConfig,fetched
0x035C,SM100HeatAssist,fetched
0x035D,SM100Circuit2Config,fetched
0x035F,SM100Config1,fetched
0x0361,SM100Differential,fetched
0x0362,SM100Monitor,
0x0363,SM100Monitor2,
0x0364,SM100Status,
0x0366,SM100Config,
0x036A,SM100Status2,
0x0380,SM100CollectorConfig,fetched
0x038E,SM100Energy,fetched
0x0391,SM100Time,fetched
0x0421,RC300Set2,
0x0422,RC300Set2,
0x0423,RC300Set2,
0x0424,RC300Set2,
0x043F,CRHolidays,fetched
0x0467,HPSet,
0x0468,HPSet,
0x0469,HPSet,
0x046A,HPSet,
0x0470,RC300Summer3,fetched
0x0471,RC300Summer2,
0x0472,RC300Summer2,
0x0473,RC300Summer2,
0x0474,RC300Summer2,
0x0475,RC300Summer2,
0x0476,RC300Summer2,
0x0477,RC300Summer2,
0x0478,RC300Summer2,
0x047B,HP2,
0x0484,HPSilentMode,fetched
0x0485,HpCooling,fetched
0x0486,HpInConfig,fetched
0x0488,HPValve,fetched
0x0489,HPpassive,
0x048A,HpPool,fetched
0x048B,HPPumps,fetched
0x048D,HpPower,fetched
0x048F,HpTemperatures,
0x0491,HPAdditionalHeater,fetched
0x0492,HpHeaterConfig,fetched
0x0494,UBAEnergySupplied,
0x0495,UBAInformation,
0x0499,HPDhwSettings,fetched
0x049C,HPSettings2,fetched
0x049D,HPSettings3,fetched
0x04A2,HpInput,fetched
0x04A5,HPFan,fetched
0x04A7,HPPowerLimit,fetched
0x04AA,HPPower2,fetched
0x04AE,HPEnergy,fetched
0x04AF,HPMeters,fetched
0x055C,VentilationSet,fetched
0x056B,VentilationMode,fetched
0x0583,VentilationMonitor,
0x0585,Blowerspeed,
0x0587,Bypass,
0x05BA,HpPoolStatus,fetched
0x05C4,HpPoolShunt,
0x05D9,Airquality,
0x0772,HIUSettings,
0x0779,HIUMonitor,
0x07A5,SM100wwCirc,fetched
0x07A6,SM100wwParam,fetched
0x07AA,SM100wwStatus,
0x07AB,SM100wwCommand,
0x07AC,SM100wwParam1,
0x07AD,SM100ValveStatus,
0x07AE,SM100wwKeepWarm,fetched
0x07D6,SM100wwTemperature,
0x07E0,SM100wwStatus2,fetched
0x0935,EM100SetMessage,fetched
0x0936,EM100OutMessage,
0x0937,EM100TempMessage,
0x0938,EM100InputMessage,
0x0939,EM100MonitorMessage,
0x093A,EM100ConfigMessage,
0x0998,HPSettings,fetched
0x0999,HPFunctionTest,fetched
0x099A,HPStarts,
0x099B,HPFlowTemp,
0x099C,HPComp,
0x099D,HPComp2,
0x09A0,HPTemperature,
0x09A8,HPVolume,
1 telegram_type_id name is_fetched
2 0x04 UBAFactory fetched
3 0x06 RCTime
4 0x0A EasyMonitor fetched
5 0x10 UBAErrorMessage1
6 0x11 UBAErrorMessage2
7 0x12 RCErrorMessage
8 0x13 RCErrorMessage2
9 0x14 UBATotalUptime fetched
10 0x15 UBAMaintenanceData
11 0x16 UBAParameters fetched
12 0x18 UBAMonitorFast
13 0x19 UBAMonitorSlow
14 0x1A UBASetPoints
15 0x1C UBAMaintenanceStatus
16 0x1E HydrTemp
17 0x23 JunkersSetMixer fetched
18 0x27 UBASettingsWW fetched
19 0x28 WeatherComp fetched
20 0x2A MC110Status
21 0x2E Meters
22 0x33 UBAParameterWW fetched
23 0x34 UBAMonitorWW
24 0x35 UBAFlags
25 0x37 WWSettings fetched
26 0x38 WWTimer fetched
27 0x39 WWCircTimer fetched
28 0x3A RC30WWSettings fetched
29 0x3B Energy
30 0x3D RC35Set
31 0x3E RC35Monitor
32 0x3F RC30Timer
33 0x40 RC30Temp
34 0x41 RC30Monitor
35 0x42 RC35Timer2
36 0x47 RC35Set
37 0x48 RC35Monitor
38 0x49 RC35Timer
39 0x4C RC35Timer2
40 0x51 RC35Set
41 0x52 RC35Monitor
42 0x53 RC35Timer
43 0x56 RC35Timer2
44 0x5B RC35Set
45 0x5C RC35Monitor
46 0x5D RC35Timer
47 0x60 RC35Timer2
48 0x96 SM10Config fetched
49 0x97 SM10Monitor
50 0x9C WM10MonitorMessage
51 0x9D WM10SetMessage
52 0xA2 RCError
53 0xA3 RCOutdoorTemp
54 0xA5 IBASettings fetched
55 0xA7 RC30Set
56 0xA9 RC30Vacation fetched
57 0xAA MMConfigMessage fetched
58 0xAB MMStatusMessage
59 0xAC MMSetMessage
60 0xAF RC20Remote
61 0xB0 RC10Set
62 0xB1 RC10Monitor
63 0xBB HybridSettings fetched
64 0xBF ErrorMessage
65 0xC0 RCErrorMessage
66 0xC2 UBAErrorMessage3
67 0xC6 UBAErrorMessage3
68 0xD1 UBAOutdoorTemp
69 0xE3 UBAMonitorSlowPlus2
70 0xE4 UBAMonitorFastPlus
71 0xE5 UBAMonitorSlowPlus
72 0xE6 UBAParametersPlus fetched
73 0xE9 UBAMonitorWWPlus
74 0xEA UBAParameterWWPlus fetched
75 0xEB PumpKick fetched
76 0x0101 ISM1Set fetched
77 0x0103 ISM1StatusMessage fetched
78 0x0104 ISM2StatusMessage
79 0x010C IPMStatusMessage
80 0x011E JunkersDisp fetched
81 0x012E HPEnergy1
82 0x013B HPEnergy2
83 0x0165 JunkersSet
84 0x0166 JunkersSet
85 0x0167 JunkersSet
86 0x0168 JunkersSet
87 0x016E Absent fetched
88 0x016F JunkersMonitor
89 0x0170 JunkersMonitor
90 0x0171 JunkersMonitor
91 0x0172 JunkersMonitor
92 0x0179 JunkersSet
93 0x017A JunkersSet
94 0x017B JunkersSet
95 0x017C JunkersSet
96 0x01D3 JunkersDhw fetched
97 0x023A RC300OutdoorTemp fetched
98 0x023E PVSettings fetched
99 0x0240 RC300Settings fetched
100 0x0241 RC300Settings fetched
101 0x0267 RC300Floordry
102 0x0269 RC300Holiday fetched
103 0x0291 HPMode fetched
104 0x0292 HPMode fetched
105 0x0293 HPMode fetched
106 0x0294 HPMode fetched
107 0x029B RC300Curves
108 0x029C RC300Curves
109 0x029D RC300Curves
110 0x029E RC300Curves
111 0x029F RC300Curves
112 0x02A0 RC300Curves
113 0x02A1 RC300Curves
114 0x02A2 RC300Curves
115 0x02A5 CRFMonitor
116 0x02A6 RC300Monitor
117 0x02A7 RC300Monitor
118 0x02A8 RC300Monitor
119 0x02A9 RC300Monitor
120 0x02AA RC300Monitor
121 0x02AB RC300Monitor
122 0x02AC RC300Monitor
123 0x02AF RC300Summer
124 0x02B0 RC300Summer
125 0x02B1 RC300Summer
126 0x02B2 RC300Summer
127 0x02B3 RC300Summer
128 0x02B4 RC300Summer
129 0x02B5 RC300Summer
130 0x02B6 RC300Summer
131 0x02B9 RC300Set
132 0x02BA RC300Set
133 0x02BB RC300Set
134 0x02BC RC300Set
135 0x02BD RC300Set
136 0x02BE RC300Set
137 0x02BF RC300Set
138 0x02C0 RC300Set
139 0x02CC HPPressure fetched
140 0x02CD MMPLUSConfigMessage
141 0x02D6 HPPump2 fetched
142 0x02D7 MMPLUSStatusMessage
143 0x02E0 UBASetPoints
144 0x02F5 RC300WWmode fetched
145 0x02F6 RC300WW2mode fetched
146 0x0313 MMPLUSConfigMessage_WWC fetched
147 0x031B RC300WWtemp fetched
148 0x031D RC300WWmode2
149 0x031E RC300WWmode2
150 0x0331 MMPLUSStatusMessage_WWC
151 0x0358 SM100SystemConfig fetched
152 0x035A SM100CircuitConfig fetched
153 0x035C SM100HeatAssist fetched
154 0x035D SM100Circuit2Config fetched
155 0x035F SM100Config1 fetched
156 0x0361 SM100Differential fetched
157 0x0362 SM100Monitor
158 0x0363 SM100Monitor2
159 0x0364 SM100Status
160 0x0366 SM100Config
161 0x036A SM100Status2
162 0x0380 SM100CollectorConfig fetched
163 0x038E SM100Energy fetched
164 0x0391 SM100Time fetched
165 0x0421 RC300Set2
166 0x0422 RC300Set2
167 0x0423 RC300Set2
168 0x0424 RC300Set2
169 0x043F CRHolidays fetched
170 0x0467 HPSet
171 0x0468 HPSet
172 0x0469 HPSet
173 0x046A HPSet
174 0x0470 RC300Summer3 fetched
175 0x0471 RC300Summer2
176 0x0472 RC300Summer2
177 0x0473 RC300Summer2
178 0x0474 RC300Summer2
179 0x0475 RC300Summer2
180 0x0476 RC300Summer2
181 0x0477 RC300Summer2
182 0x0478 RC300Summer2
183 0x047B HP2
184 0x0484 HPSilentMode fetched
185 0x0485 HpCooling fetched
186 0x0486 HpInConfig fetched
187 0x0488 HPValve fetched
188 0x0489 HPpassive
189 0x048A HpPool fetched
190 0x048B HPPumps fetched
191 0x048D HpPower fetched
192 0x048F HpTemperatures
193 0x0491 HPAdditionalHeater fetched
194 0x0492 HpHeaterConfig fetched
195 0x0494 UBAEnergySupplied
196 0x0495 UBAInformation
197 0x0499 HPDhwSettings fetched
198 0x049C HPSettings2 fetched
199 0x049D HPSettings3 fetched
200 0x04A2 HpInput fetched
201 0x04A5 HPFan fetched
202 0x04A7 HPPowerLimit fetched
203 0x04AA HPPower2 fetched
204 0x04AE HPEnergy fetched
205 0x04AF HPMeters fetched
206 0x055C VentilationSet fetched
207 0x056B VentilationMode fetched
208 0x0583 VentilationMonitor
209 0x0585 Blowerspeed
210 0x0587 Bypass
211 0x05BA HpPoolStatus fetched
212 0x05C4 HpPoolShunt
213 0x05D9 Airquality
214 0x0772 HIUSettings
215 0x0779 HIUMonitor
216 0x07A5 SM100wwCirc fetched
217 0x07A6 SM100wwParam fetched
218 0x07AA SM100wwStatus
219 0x07AB SM100wwCommand
220 0x07AC SM100wwParam1
221 0x07AD SM100ValveStatus
222 0x07AE SM100wwKeepWarm fetched
223 0x07D6 SM100wwTemperature
224 0x07E0 SM100wwStatus2 fetched
225 0x0935 EM100SetMessage fetched
226 0x0936 EM100OutMessage
227 0x0937 EM100TempMessage
228 0x0938 EM100InputMessage
229 0x0939 EM100MonitorMessage
230 0x093A EM100ConfigMessage
231 0x0998 HPSettings fetched
232 0x0999 HPFunctionTest fetched
233 0x099A HPStarts
234 0x099B HPFlowTemp
235 0x099C HPComp
236 0x099D HPComp2
237 0x09A0 HPTemperature
238 0x09A8 HPVolume
+3 -3
View File
@@ -44,7 +44,7 @@
"@eslint/js": "^10.0.1",
"@preact/preset-vite": "^2.10.6",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^26.6.4",
"@types/node": "^26.6.5",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"concurrently": "^10.0.6",
@@ -52,7 +52,7 @@
"eslint-config-prettier": "^10.1.8",
"etag": "^1.8.1",
"mime-types": "^3.0.2",
"prettier": "^3.9.9",
"prettier": "^3.9.10",
"rollup-plugin-visualizer": "^7.1.1",
"terser": "^5.51.2",
"typescript": "^6.0.3",
@@ -60,5 +60,5 @@
"vite": "^8.3.4",
"vite-plugin-imagemin": "^0.6.1"
},
"packageManager": "pnpm@12.10.1"
"packageManager": "pnpm@12.11.2"
}
+94 -94
View File
@@ -7,153 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.10.1
version: 12.10.1
specifier: 12.11.2
version: 12.11.2
packages:
'@pnpm/exe.android-arm64@12.10.1':
resolution: {integrity: sha512-CVTlHPwNvadQCJD9bGSMzWIhFX8QpjtqC2c27dhdnn/9iKzEjZ3qL62o3gHpKbIuo6qn4WxTKRoaokgNdReBig==}
'@pnpm/exe.android-arm64@12.11.2':
resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.10.1':
resolution: {integrity: sha512-NFiYLIaodY7da0XIebwcU3JoP7IK/lzF7zgKSSP+9H+QcFnQ1GDfBkuBKCYJzZ+Xu0WH54neIPLBBCaOjaaauA==}
'@pnpm/exe.android-x64@12.11.2':
resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.10.1':
resolution: {integrity: sha512-0O8tdQ8ijlPJudXspLMTcitMkdBEz6uUflazL5uQE0WMs85oQIp0U+5hfEktMFB64QqEhjqlvvujTwbzdr9oMg==}
'@pnpm/exe.darwin-arm64@12.11.2':
resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==}
cpu: [arm64]
os: [darwin]
'@pnpm/exe.darwin-x64@12.10.1':
resolution: {integrity: sha512-ud+1/uZOKEo8P/AsPynOJc/LNwfI0j6WwCElibj71rU9l4nIBnsSn+wWTIPZPkZ4BBSH5+82azSnAARMyJ1lNA==}
'@pnpm/exe.darwin-x64@12.11.2':
resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.freebsd-x64@12.10.1':
resolution: {integrity: sha512-mj9PE5rf8uAfQQ2K66HSATMSut+lNiB4eSqFd5+azFVCMnxNSy/32xpZQJT21PhnN3Y4qbK+iKHe5nWAxSuRhw==}
'@pnpm/exe.freebsd-x64@12.11.2':
resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.10.1':
resolution: {integrity: sha512-xba+00wZJ8Um6rBJJjWl88uJj0tydB8dtomvjlCLvi0uL32vpa9CRP8Vv26FIXvbiZ1bo2RCzYt/eChxJVV52w==}
'@pnpm/exe.linux-arm64-musl@12.11.2':
resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.10.1':
resolution: {integrity: sha512-oBdMp69ScC9UnniG4qZAQVr2fBDSXjm4yk/72vkEVyrlwS23yMxcRQl7Qdo3sgXPAygY6TWnPRKm4rXzjoyW7w==}
'@pnpm/exe.linux-arm64@12.11.2':
resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-ppc64@12.10.1':
resolution: {integrity: sha512-Yyb3wu09h3c7mLme270uHFiVaAIUHC411Ru9LIU2BKUIstrGlXa9OqqVWoXaRgYRdFdOFknkwG7GNn6ygwLbnw==}
'@pnpm/exe.linux-ppc64@12.11.2':
resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.10.1':
resolution: {integrity: sha512-PdkeUFFkybknnGkWP0H22eMpIiS4OZXL5BcQzF4lh+vmrW3IaC2HOkbANKgot/PKO0Kl77pzA/6PKFxgbgfXIQ==}
'@pnpm/exe.linux-riscv64@12.11.2':
resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.10.1':
resolution: {integrity: sha512-1okKuHKJwJPAbofDxfhZWAUxEWtee4ZSf7Pah8kcvocaJkf2WxFJ5KVTiNIvDml5GLidDQsgf7Pcf/WHbCak7Q==}
'@pnpm/exe.linux-s390x@12.11.2':
resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.10.1':
resolution: {integrity: sha512-e6IqzVNkPa7srFKJKnY23uWJ+qz0LaRWiTUo/sHfFXRRMRzB5uNOr6qWWip/jqhdGwU91n9J7LrEYZ6e0tRczw==}
'@pnpm/exe.linux-x64-musl@12.11.2':
resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.10.1':
resolution: {integrity: sha512-ZByV0DRgWzGaDSjT5ddRBASdwz+r/qPMYh7o6RGmPQjtRGNFbSl94xY2DcAM8mqCOZJUUDtAXQ+wpBXcdpKlkg==}
'@pnpm/exe.linux-x64@12.11.2':
resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.10.1':
resolution: {integrity: sha512-I7QZHcjioG7orFeCRUFUHByJ1G0zFPnAlaGGyrUvb6MGkv5oeNwNPl7t+Gp+7zHfnMcgLGJ1a+Wfim0IDw3lOw==}
'@pnpm/exe.win32-arm64@12.11.2':
resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.10.1':
resolution: {integrity: sha512-4WJFdp58EKlCj0zLobrXU0vpszSZIr4X/3y/uNbAQqGW0zE+VdruZB0gFc3L2kzsUzVwTYbaCn+XEhnCYSAlTg==}
'@pnpm/exe.win32-x64@12.11.2':
resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==}
cpu: [x64]
os: [win32]
pnpm@12.10.1:
resolution: {integrity: sha512-ukCjfrGjcNYP6oxM8dNkwTvMzvUY6YrSvO6VT1OdONo3RY5YJho/pV4GUjRQ6g//+fSXsTW+QfLBXBZCusrObA==}
pnpm@12.11.2:
resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.android-arm64@12.10.1':
'@pnpm/exe.android-arm64@12.11.2':
optional: true
'@pnpm/exe.android-x64@12.10.1':
'@pnpm/exe.android-x64@12.11.2':
optional: true
'@pnpm/exe.darwin-arm64@12.10.1':
'@pnpm/exe.darwin-arm64@12.11.2':
optional: true
'@pnpm/exe.darwin-x64@12.10.1':
'@pnpm/exe.darwin-x64@12.11.2':
optional: true
'@pnpm/exe.freebsd-x64@12.10.1':
'@pnpm/exe.freebsd-x64@12.11.2':
optional: true
'@pnpm/exe.linux-arm64-musl@12.10.1':
'@pnpm/exe.linux-arm64-musl@12.11.2':
optional: true
'@pnpm/exe.linux-arm64@12.10.1':
'@pnpm/exe.linux-arm64@12.11.2':
optional: true
'@pnpm/exe.linux-ppc64@12.10.1':
'@pnpm/exe.linux-ppc64@12.11.2':
optional: true
'@pnpm/exe.linux-riscv64@12.10.1':
'@pnpm/exe.linux-riscv64@12.11.2':
optional: true
'@pnpm/exe.linux-s390x@12.10.1':
'@pnpm/exe.linux-s390x@12.11.2':
optional: true
'@pnpm/exe.linux-x64-musl@12.10.1':
'@pnpm/exe.linux-x64-musl@12.11.2':
optional: true
'@pnpm/exe.linux-x64@12.10.1':
'@pnpm/exe.linux-x64@12.11.2':
optional: true
'@pnpm/exe.win32-arm64@12.10.1':
'@pnpm/exe.win32-arm64@12.11.2':
optional: true
'@pnpm/exe.win32-x64@12.10.1':
'@pnpm/exe.win32-x64@12.11.2':
optional: true
pnpm@12.10.1:
pnpm@12.11.2:
optionalDependencies:
'@pnpm/exe.android-arm64': 12.10.1
'@pnpm/exe.android-x64': 12.10.1
'@pnpm/exe.darwin-arm64': 12.10.1
'@pnpm/exe.darwin-x64': 12.10.1
'@pnpm/exe.freebsd-x64': 12.10.1
'@pnpm/exe.linux-arm64': 12.10.1
'@pnpm/exe.linux-arm64-musl': 12.10.1
'@pnpm/exe.linux-ppc64': 12.10.1
'@pnpm/exe.linux-riscv64': 12.10.1
'@pnpm/exe.linux-s390x': 12.10.1
'@pnpm/exe.linux-x64': 12.10.1
'@pnpm/exe.linux-x64-musl': 12.10.1
'@pnpm/exe.win32-arm64': 12.10.1
'@pnpm/exe.win32-x64': 12.10.1
'@pnpm/exe.android-arm64': 12.11.2
'@pnpm/exe.android-x64': 12.11.2
'@pnpm/exe.darwin-arm64': 12.11.2
'@pnpm/exe.darwin-x64': 12.11.2
'@pnpm/exe.freebsd-x64': 12.11.2
'@pnpm/exe.linux-arm64': 12.11.2
'@pnpm/exe.linux-arm64-musl': 12.11.2
'@pnpm/exe.linux-ppc64': 12.11.2
'@pnpm/exe.linux-riscv64': 12.11.2
'@pnpm/exe.linux-s390x': 12.11.2
'@pnpm/exe.linux-x64': 12.11.2
'@pnpm/exe.linux-x64-musl': 12.11.2
'@pnpm/exe.win32-arm64': 12.11.2
'@pnpm/exe.win32-x64': 12.11.2
---
lockfileVersion: '9.0'
@@ -214,13 +214,13 @@ importers:
version: 10.0.1(eslint@10.12.0(supports-color@10.2.2))
'@preact/preset-vite':
specifier: ^2.10.6
version: 2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@11.0.1)(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.4)(terser@5.51.2))
version: 2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@11.0.1)(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.5)(terser@5.51.2))
'@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.2
version: 6.0.2(prettier@3.9.9)(supports-color@10.2.2)
version: 6.0.2(prettier@3.9.10)(supports-color@10.2.2)
'@types/node':
specifier: ^26.6.4
version: 26.6.4
specifier: ^26.6.5
version: 26.6.5
'@types/react':
specifier: ^19.3.0
version: 19.3.0
@@ -243,8 +243,8 @@ importers:
specifier: ^3.0.2
version: 3.0.2
prettier:
specifier: ^3.9.9
version: 3.9.9
specifier: ^3.9.10
version: 3.9.10
rollup-plugin-visualizer:
specifier: ^7.1.1
version: 7.1.1(rolldown@1.2.13)
@@ -259,10 +259,10 @@ importers:
version: 8.71.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
vite:
specifier: ^8.3.4
version: 8.3.4(@types/node@26.6.4)(terser@5.51.2)
version: 8.3.4(@types/node@26.6.5)(terser@5.51.2)
vite-plugin-imagemin:
specifier: ^0.6.1
version: 0.6.1(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.4)(terser@5.51.2))
version: 0.6.1(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.5)(terser@5.51.2))
packages:
@@ -845,8 +845,8 @@ packages:
'@types/minimatch@5.1.2':
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
'@types/node@26.6.4':
resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==}
'@types/node@26.6.5':
resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==}
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -2554,8 +2554,8 @@ packages:
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
engines: {node: '>=4'}
prettier@3.9.9:
resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==}
prettier@3.9.10:
resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
engines: {node: '>=14'}
hasBin: true
@@ -3596,19 +3596,19 @@ snapshots:
'@popperjs/core@2.11.8': {}
'@preact/preset-vite@2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@11.0.1)(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.4)(terser@5.51.2))':
'@preact/preset-vite@2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@11.0.1)(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.5)(terser@5.51.2))':
dependencies:
'@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@prefresh/vite': 2.4.12(preact@11.0.1)(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.4)(terser@5.51.2))
'@prefresh/vite': 2.4.12(preact@11.0.1)(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.5)(terser@5.51.2))
'@rollup/pluginutils': 5.4.0
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.7(supports-color@10.2.2))
debug: 4.4.3(supports-color@10.2.2)
magic-string: 0.30.21
picocolors: 1.1.1
vite: 8.3.4(@types/node@26.6.4)(terser@5.51.2)
vite-prerender-plugin: 0.5.14(vite@8.3.4(@types/node@26.6.4)(terser@5.51.2))
vite: 8.3.4(@types/node@26.6.5)(terser@5.51.2)
vite-prerender-plugin: 0.5.14(vite@8.3.4(@types/node@26.6.5)(terser@5.51.2))
zimmerframe: 1.1.5
transitivePeerDependencies:
- preact
@@ -3625,7 +3625,7 @@ snapshots:
'@prefresh/utils@1.2.1': {}
'@prefresh/vite@2.4.12(preact@11.0.1)(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.4)(terser@5.51.2))':
'@prefresh/vite@2.4.12(preact@11.0.1)(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.5)(terser@5.51.2))':
dependencies:
'@babel/core': 7.29.7(supports-color@10.2.2)
'@prefresh/babel-plugin': 0.5.4(@babel/core@7.29.7(supports-color@10.2.2))
@@ -3633,7 +3633,7 @@ snapshots:
'@prefresh/utils': 1.2.1
'@rollup/pluginutils': 4.2.1
preact: 11.0.1
vite: 8.3.4(@types/node@26.6.4)(terser@5.51.2)
vite: 8.3.4(@types/node@26.6.5)(terser@5.51.2)
transitivePeerDependencies:
- supports-color
@@ -3708,7 +3708,7 @@ snapshots:
react-virtualized-auto-sizer: 1.0.26(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react-window: 1.8.11(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.9)(supports-color@10.2.2)':
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.10)(supports-color@10.2.2)':
dependencies:
'@babel/generator': 7.29.8
'@babel/parser': 7.29.9
@@ -3718,7 +3718,7 @@ snapshots:
lodash-es: 4.18.1
minimatch: 9.0.9
parse-imports-exports: 0.2.4
prettier: 3.9.9
prettier: 3.9.10
transitivePeerDependencies:
- supports-color
@@ -3729,7 +3729,7 @@ snapshots:
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 26.6.4
'@types/node': 26.6.5
'@types/imagemin-gifsicle@7.0.4':
dependencies:
@@ -3758,17 +3758,17 @@ snapshots:
'@types/imagemin@7.0.1':
dependencies:
'@types/node': 26.6.4
'@types/node': 26.6.5
'@types/json-schema@7.0.15': {}
'@types/keyv@3.1.4':
dependencies:
'@types/node': 26.6.4
'@types/node': 26.6.5
'@types/minimatch@5.1.2': {}
'@types/node@26.6.4':
'@types/node@26.6.5':
dependencies:
undici-types: 8.9.0
@@ -3790,11 +3790,11 @@ snapshots:
'@types/responselike@1.0.3':
dependencies:
'@types/node': 26.6.4
'@types/node': 26.6.5
'@types/svgo@2.6.4':
dependencies:
'@types/node': 26.6.4
'@types/node': 26.6.5
'@typescript-eslint/eslint-plugin@8.71.1(@typescript-eslint/parser@8.71.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)':
dependencies:
@@ -5514,7 +5514,7 @@ snapshots:
prepend-http@2.0.0: {}
prettier@3.9.9: {}
prettier@3.9.10: {}
process-nextick-args@2.0.1: {}
@@ -5987,7 +5987,7 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
vite-plugin-imagemin@0.6.1(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.4)(terser@5.51.2)):
vite-plugin-imagemin@0.6.1(supports-color@10.2.2)(vite@8.3.4(@types/node@26.6.5)(terser@5.51.2)):
dependencies:
'@types/imagemin': 7.0.1
'@types/imagemin-gifsicle': 7.0.4
@@ -6012,11 +6012,11 @@ snapshots:
imagemin-webp: 6.1.0
jpegtran-bin: 6.0.1
pathe: 0.2.0
vite: 8.3.4(@types/node@26.6.4)(terser@5.51.2)
vite: 8.3.4(@types/node@26.6.5)(terser@5.51.2)
transitivePeerDependencies:
- supports-color
vite-prerender-plugin@0.5.14(vite@8.3.4(@types/node@26.6.4)(terser@5.51.2)):
vite-prerender-plugin@0.5.14(vite@8.3.4(@types/node@26.6.5)(terser@5.51.2)):
dependencies:
kolorist: 1.8.0
magic-string: 0.30.21
@@ -6024,9 +6024,9 @@ snapshots:
simple-code-frame: 1.3.0
source-map: 0.7.6
stack-trace: 1.0.0
vite: 8.3.4(@types/node@26.6.4)(terser@5.51.2)
vite: 8.3.4(@types/node@26.6.5)(terser@5.51.2)
vite@8.3.4(@types/node@26.6.4)(terser@5.51.2):
vite@8.3.4(@types/node@26.6.5)(terser@5.51.2):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
@@ -6034,7 +6034,7 @@ snapshots:
rolldown: 1.2.13
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 26.6.4
'@types/node': 26.6.5
fsevents: 2.3.3
terser: 5.51.2
+270 -105
View File
@@ -3,11 +3,14 @@ import { Link } from 'react-router';
import CancelIcon from '@mui/icons-material/Cancel';
import CloseIcon from '@mui/icons-material/Close';
import DescriptionIcon from '@mui/icons-material/Description';
import CheckIcon from '@mui/icons-material/Done';
import DownloadIcon from '@mui/icons-material/GetApp';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import RefreshIcon from '@mui/icons-material/Refresh';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
import WarningIcon from '@mui/icons-material/Warning';
import {
Box,
@@ -51,6 +54,8 @@ const STABLE_RELNOTES_URL =
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const RELEASES_API_URL =
'https://api.github.com/repos/emsesp/EMS-ESP32/releases/tags/';
const MAX_VERSION_POLLS = 5; // approx 15 seconds of waiting
// Types for better type safety
@@ -75,6 +80,96 @@ interface VersionData {
disable_reset: boolean;
}
interface RecoveryRelease {
url: string;
version: string;
}
// finds the recovery firmware for this board in the GitHub releases, trying each release tag in turn.
// The version is in the filename, e.g. EMS-ESP-Recovery-1_0_0-ESP32S3-16MB+.bin is 1.0.0
const findRecovery = async (
tags: string[],
platform: string
): Promise<RecoveryRelease | undefined> => {
const prefix = 'ems-esp-recovery-';
const suffix = `-${platform}.bin`.toLowerCase();
for (const tag of tags) {
try {
const response = await fetch(`${RELEASES_API_URL}${tag}`);
if (!response.ok) {
continue;
}
const release = (await response.json()) as {
assets?: { name: string; browser_download_url: string }[];
};
const asset = release.assets?.find((a) => {
const name = a.name.toLowerCase();
return name.startsWith(prefix) && name.endsWith(suffix);
});
if (asset) {
return {
url: asset.browser_download_url,
version: asset.name
.slice(prefix.length, asset.name.length - suffix.length)
.replaceAll('_', '.')
};
}
} catch {
// try the next one
}
}
return undefined;
};
// the GitHub API only allows 60 requests an hour, so only look once per page load for each board and channel
const recoveryLookups = new Map<string, Promise<RecoveryRelease | undefined>>();
const findRecoveryCached = (tags: string[], platform: string) => {
const key = `${tags.join(',')}|${platform}`;
let lookup = recoveryLookups.get(key);
if (!lookup) {
lookup = findRecovery(tags, platform);
recoveryLookups.set(key, lookup);
void lookup.then((release) => {
if (!release) {
recoveryLookups.delete(key); // try again next time
}
});
}
return lookup;
};
// true if version a is newer than b, like 1.0.1 and 1.0.0. A prerelease, like 1.1.0-dev.1, is older than the release
const isNewerVersion = (a: string, b: string) => {
const parse = (v: string) => {
const dash = v.indexOf('-');
const numbers = (text: string) =>
text.split('.').map((n) => Number.parseInt(n, 10) || 0);
return {
main: numbers(dash < 0 ? v : v.slice(0, dash)),
pre: dash < 0 ? [] : numbers(v.slice(dash + 1)),
isPre: dash >= 0
};
};
const x = parse(a);
const y = parse(b);
for (let i = 0; i < Math.max(x.main.length, y.main.length); i++) {
const diff = (x.main[i] ?? 0) - (y.main[i] ?? 0);
if (diff !== 0) {
return diff > 0;
}
}
if (x.isPre !== y.isPre) {
return !x.isPre;
}
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
const diff = (x.pre[i] ?? 0) - (y.pre[i] ?? 0);
if (diff !== 0) {
return diff > 0;
}
}
return false;
};
// Memoized components for better performance
const VersionInfoDialog = memo(
({
@@ -235,6 +330,7 @@ const VersionInfoDialog = memo(
<DialogActions>
{!isPartition && (
<Button
startIcon={<DescriptionIcon />}
variant="outlined"
component="a"
href={relNotesUrl}
@@ -244,7 +340,12 @@ const VersionInfoDialog = memo(
Changelog
</Button>
)}
<Button variant="outlined" onClick={onClose} color="secondary">
<Button
startIcon={<CloseIcon />}
variant="outlined"
onClick={onClose}
color="secondary"
>
{LL.CLOSE()}
</Button>
</DialogActions>
@@ -372,9 +473,7 @@ const InstallPartitionDialog = memo(
return (
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
<DialogTitle>
{isRecovery ? LL.RECOVERY() : `${LL.INSTALL()} ${LL.STORED_VERSIONS()}`}
</DialogTitle>
<DialogTitle>{isRecovery ? LL.RECOVERY_APP() : LL.INSTALL()}</DialogTitle>
<DialogContent dividers>
<Typography sx={{ mb: 2 }}>
{isRecovery
@@ -394,7 +493,7 @@ const InstallPartitionDialog = memo(
<Button
startIcon={
isRecovery ? <PowerSettingsNewIcon /> : <WarningIcon color="warning" />
isRecovery ? <RestartAltIcon /> : <WarningIcon color="warning" />
}
variant="outlined"
onClick={() => onInstall(partition)}
@@ -418,6 +517,11 @@ const Version = () => {
const { me, versions, refreshVersions } = useContext(AuthenticatedContext);
const [restarting, setRestarting] = useState<boolean>(false);
const [findingRecovery, setFindingRecovery] = useState<boolean>(false);
const [installingRecovery, setInstallingRecovery] = useState<boolean>(false);
const [latestRecovery, setLatestRecovery] = useState<RecoveryRelease | undefined>(
undefined
);
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
@@ -499,9 +603,6 @@ const Version = () => {
const platform = data ? getPlatform(data) : '';
const otherPartitions =
data?.partitions.filter((p) => p.partition !== data.partition) ?? [];
const setPartitionVersionInfo = (partition: string) => {
setShowVersionInfo(3);
const partitionData = data?.partitions.find((p) => p.partition === partition);
@@ -544,6 +645,49 @@ const Version = () => {
await doRestart();
};
// the release of the channel we're on first, it may not have a recovery app yet so try the other one too
const stableTag = latestVersion ? `v${latestVersion.version}` : '';
const recoveryTags = (
usingDevVersion ? ['latest', stableTag] : [stableTag, 'latest']
).filter(Boolean);
const recoveryTagsKey = recoveryTags.join(',');
const hasRecoverySlot = data?.recovery_version !== undefined;
// look for the latest recovery app, to show if there is a newer one
useEffect(() => {
if (!hasRecoverySlot || !internetLive || !platform) {
return;
}
let cancelled = false;
void findRecoveryCached(recoveryTags, platform).then((release) => {
if (!cancelled) {
setLatestRecovery(release);
}
});
return () => {
cancelled = true;
};
}, [hasRecoverySlot, internetLive, platform, recoveryTagsKey]);
// downloads the recovery app from GitHub into the factory partition, EMS-ESP doesn't restart
const installRecovery = async () => {
setFindingRecovery(true);
const release =
latestRecovery ?? (await findRecoveryCached(recoveryTags, platform));
setFindingRecovery(false);
if (!release) {
toast.error(LL.RECOVERY_NOT_FOUND());
return;
}
await sendUploadURL(release.url)
.then(() => setInstallingRecovery(true))
.catch((error: Error) => {
toast.error(error.message);
});
};
const installPartitionFirmware = async (partition: string) => {
await sendSetPartition(partition).catch((error: Error) => {
toast.error(error.message);
@@ -607,8 +751,9 @@ const Version = () => {
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
</span>
<Button
sx={{ ml: 1 }}
sx={{ ml: 3 }}
variant="outlined"
startIcon={<RefreshIcon />}
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
@@ -624,9 +769,10 @@ const Version = () => {
return (
<Button
sx={{ ml: 1 }}
sx={{ ml: 3 }}
variant="outlined"
color={isUpdateAvailable ? 'success' : 'warning'}
startIcon={!isUpdateAvailable ? <SwapHorizIcon /> : undefined}
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
@@ -639,7 +785,7 @@ const Version = () => {
display: 'inline-block',
width: 8,
height: 8,
ml: 1,
ml: 3,
verticalAlign: 'middle',
borderRadius: '50%',
backgroundColor: '#ffeb3b',
@@ -655,6 +801,20 @@ const Version = () => {
return <SystemMonitor />;
}
if (installingRecovery) {
return (
<SystemMonitor
onDone={(success) => {
setInstallingRecovery(false);
if (success) {
toast.success(LL.RECOVERY_INSTALLED());
}
void loadData();
}}
/>
);
}
if (!data) {
return (
<SectionContent>
@@ -674,9 +834,13 @@ const Version = () => {
sx={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
gridAutoRows: 'minmax(32px, auto)', // same height for every row
columnGap: 3,
rowGap: 0.75,
alignItems: 'center'
alignItems: 'center',
// whole pixel line height so the labels and values are centered on the same pixel
'& .MuiTypography-root': { lineHeight: '24px' },
'& .MuiTypography-caption': { lineHeight: 1 }
}}
>
<Typography color="secondary">{LL.VERSION()}</Typography>
@@ -732,6 +896,51 @@ const Version = () => {
PSRAM)
</Typography>
</Box>
{data.recovery_version !== undefined && (
<>
<Typography color="secondary">{LL.RECOVERY_APP()}</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
minWidth: 0
}}
>
<Typography component="span">
{data.recovery_version || LL.RECOVERY_NOT_INSTALLED()}
</Typography>
{!data.recovery_version && me.admin && internetLive && (
<Button
startIcon={<DownloadIcon />}
sx={{ ml: 3 }}
variant="outlined"
size="small"
disabled={findingRecovery}
onClick={() => void installRecovery()}
>
{LL.INSTALL()}
</Button>
)}
{data.recovery_version && me.admin && (
<Button
startIcon={<RestartAltIcon />}
sx={{ ml: 3 }}
variant="outlined"
size="small"
onClick={() => {
const version = data.recovery_version;
if (version) {
showPartitionDialog(version, 'boot', '');
}
}}
>
{LL.RESTART_INTO_RECOVERY()}
</Button>
)}
</Box>
</>
)}
</Box>
{internetLive ? (
@@ -749,96 +958,6 @@ const Version = () => {
alignItems: 'center'
}}
>
{data.developer_mode && (
<>
{otherPartitions.length > 0 && (
<>
<Typography
color="secondary"
sx={{ alignSelf: 'start', pt: 0.5 }}
>
{LL.STORED_VERSIONS()}
</Typography>
<Box
sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}
>
{otherPartitions.map((partition) => (
<Box
key={partition.partition}
sx={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap'
}}
>
<Typography component="span">
{partition.version}
</Typography>
<IconButton
size="small"
onClick={() =>
setPartitionVersionInfo(partition.partition)
}
aria-label={LL.FIRMWARE_VERSION_INFO()}
sx={{ p: 0.5 }}
>
<InfoOutlinedIcon
color="primary"
sx={{ fontSize: 18 }}
/>
</IconButton>
<Button
variant="outlined"
size="small"
onClick={() =>
showPartitionDialog(
partition.version,
partition.partition,
partition.install_date ?? ''
)
}
>
{LL.INSTALL()}
</Button>
</Box>
))}
</Box>
</>
)}
{data.recovery_version !== undefined && (
<>
<Typography color="secondary">{LL.RECOVERY()}</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap'
}}
>
<Typography component="span">
{data.recovery_version || LL.RECOVERY_NOT_INSTALLED()}
</Typography>
{data.recovery_version && me.admin && (
<Button
sx={{ ml: 1 }}
variant="outlined"
size="small"
onClick={() => {
const version = data.recovery_version;
if (version) {
showPartitionDialog(version, 'boot', '');
}
}}
>
Boot
</Button>
)}
</Box>
<Box sx={{ gridColumn: '1 / -1', height: 12 }} />
</>
)}
</>
)}
<Typography color="secondary">{LL.STABLE()}</Typography>
<Box
sx={{
@@ -880,6 +999,52 @@ const Version = () => {
</IconButton>
{showButtons(true)}
</Box>
{data.recovery_version && latestRecovery && (
<>
<Typography color="secondary">{LL.RECOVERY_APP()}</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
minWidth: 0
}}
>
<Typography component="span">
{latestRecovery.version}
</Typography>
{isNewerVersion(
latestRecovery.version,
data.recovery_version
) ? (
me.admin && (
<Button
startIcon={<DownloadIcon />}
sx={{ ml: 3 }}
variant="outlined"
color="success"
size="small"
disabled={findingRecovery}
onClick={() => void installRecovery()}
>
{LL.INSTALL()}
</Button>
)
) : (
<>
<CheckIcon
color="success"
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
/>
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
{LL.LATEST_VERSION(LL.RECOVERY_APP())}
</span>
</>
)}
</Box>
</>
)}
</Box>
</>
) : (
@@ -978,7 +1143,7 @@ const Version = () => {
{LL.CANCEL()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
startIcon={<RestartAltIcon />}
variant="outlined"
onClick={doRestart}
color="success"
@@ -1010,7 +1175,7 @@ const Version = () => {
</Button>
)}
<Button
startIcon={<PowerSettingsNewIcon />}
startIcon={<RestartAltIcon />}
variant="outlined"
onClick={handleRestartClick}
color="success"
+16 -3
View File
@@ -14,7 +14,12 @@ import { useInterval } from 'utils';
import { LinearProgressWithLabel } from '../../components/upload/LinearProgressWithLabel';
const SystemMonitor = () => {
interface SystemMonitorProps {
// when set the page isn't reloaded when done, e.g. when the firmware was installed without a restart
onDone?: (success: boolean) => void;
}
const SystemMonitor = ({ onDone }: SystemMonitorProps) => {
const [errorMessage, setErrorMessage] = useState<string>();
const hasInitialized = useRef(false);
const { LL } = useI18nContext();
@@ -42,7 +47,11 @@ const SystemMonitor = () => {
event.data.status === SystemStatusCodes.SYSTEM_STATUS_NORMAL ||
event.data.status === undefined
) {
document.location.href = '/';
if (onDone) {
onDone(true);
} else {
document.location.href = '/';
}
} else if (
event.data.status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
) {
@@ -80,7 +89,11 @@ const SystemMonitor = () => {
const onCancel = async () => {
setErrorMessage(undefined);
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
document.location.href = '/';
if (onDone) {
onDone(false);
} else {
document.location.href = '/';
}
};
return (
+4 -3
View File
@@ -336,7 +336,6 @@ const cs: Translation = {
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi',
FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru',
USER_PROFILE: 'Uživatelský profil',
STORED_VERSIONS: 'Uložené verze',
ONLINE_HELP: 'online nápověda',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy',
UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že nejprve stáhnete systémovou zálohu před pokračováním a poté nahrajte tento soubor po instalaci nové verze.',
@@ -353,9 +352,11 @@ const cs: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Klikněte, abyste vybrali, nebo drop soubor firmware .bin/.md5 sem',
UPLOAD_MD5_RECEIVED: 'Digest přijat, nyní nahrajte firmware .bin',
UPLOAD_MD5_MATCHED: 'MD5 souhlasí, firmware se instaluje...',
RECOVERY: 'Obnova',
RECOVERY_APP: 'Aplikace pro obnovu',
RECOVERY_NOT_INSTALLED: 'Nenainstalováno',
RECOVERY_INSTALLED: 'Firmware pro obnovu nainstalován'
RECOVERY_INSTALLED: 'Firmware pro obnovu nainstalován',
RESTART_INTO_RECOVERY: 'Restartovat do aplikace pro obnovu',
RECOVERY_NOT_FOUND: 'Aplikace pro obnovu nebyla online nalezena'
};
export default cs;
+4 -3
View File
@@ -336,7 +336,6 @@ const de: Translation = {
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln',
FIRMWARE_VERSION_INFO: 'Firmware-Versionsinformation',
USER_PROFILE: 'Benutzerprofil',
STORED_VERSIONS: 'Gespeicherte Versionen',
ONLINE_HELP: 'Online-Hilfe',
UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren',
UPGRADE_IMPORTANT_MESSAGES_1: 'Für diese Aktualisierung ist ein Werksreset erforderlich. Stellen Sie sicher, dass Sie zuerst eine Systemsicherung herunterladen, bevor Sie fortfahren, und laden Sie diese Datei dann nach der Installation der neuen Version hoch.',
@@ -353,9 +352,11 @@ const de: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Klicken Sie, um eine Firmware-Datei (.bin/.md5) auszuwählen, oder drop ein',
UPLOAD_MD5_RECEIVED: 'Prüfsumme empfangen, jetzt die Firmware-Datei (.bin) hochladen',
UPLOAD_MD5_MATCHED: 'MD5 stimmt überein, Firmware wird installiert...',
RECOVERY: 'Wiederherstellung',
RECOVERY_APP: 'Wiederherstellungs-App',
RECOVERY_NOT_INSTALLED: 'Nicht installiert',
RECOVERY_INSTALLED: 'Wiederherstellungs-Firmware installiert'
RECOVERY_INSTALLED: 'Wiederherstellungs-Firmware installiert',
RESTART_INTO_RECOVERY: 'Neustart in die Wiederherstellungs-App',
RECOVERY_NOT_FOUND: 'Wiederherstellungs-App online nicht gefunden'
};
export default de;
+4 -3
View File
@@ -336,7 +336,6 @@ const en: Translation = {
SWITCH_RELEASE_TYPE: 'Switch to {0} release',
FIRMWARE_VERSION_INFO: 'Firmware Version Information',
USER_PROFILE: 'User Profile',
STORED_VERSIONS: 'Stored Versions',
ONLINE_HELP: 'online help',
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages',
UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you first download a System Backup before continuing, and then upload this file after the new version is installed.',
@@ -353,9 +352,11 @@ const en: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Click to select, or drop a firmware .bin/.md5 file here',
UPLOAD_MD5_RECEIVED: 'Digest received, now upload the firmware .bin file',
UPLOAD_MD5_MATCHED: 'MD5 matches, installing firmware...',
RECOVERY: 'Recovery',
RECOVERY_APP: 'Recovery App',
RECOVERY_NOT_INSTALLED: 'Not installed',
RECOVERY_INSTALLED: 'Recovery firmware installed'
RECOVERY_INSTALLED: 'Recovery firmware installed',
RESTART_INTO_RECOVERY: 'Restart into Recovery App',
RECOVERY_NOT_FOUND: 'Recovery App not found online'
};
export default en;
+4 -3
View File
@@ -336,7 +336,6 @@ const fr: Translation = {
SWITCH_RELEASE_TYPE: 'Passer à la version {0}',
FIRMWARE_VERSION_INFO: 'Informations sur la version du firmware',
USER_PROFILE: 'Profil utilisateur',
STORED_VERSIONS: 'Versions stockées',
ONLINE_HELP: 'aide en ligne',
UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants',
UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous de télécharger une sauvegarde système avant de continuer, et de la charger après l\'installation de la nouvelle version.',
@@ -353,9 +352,11 @@ const fr: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Cliquez pour sélectionner, ou drop un fichier firmware .bin/.md5 ici',
UPLOAD_MD5_RECEIVED: 'Empreinte reçue, téléversez maintenant le fichier firmware .bin',
UPLOAD_MD5_MATCHED: 'MD5 concordant, installation du firmware...',
RECOVERY: 'Récupération',
RECOVERY_APP: 'Application de récupération',
RECOVERY_NOT_INSTALLED: 'Non installé',
RECOVERY_INSTALLED: 'Firmware de récupération installé'
RECOVERY_INSTALLED: 'Firmware de récupération installé',
RESTART_INTO_RECOVERY: 'Redémarrer dans l\'application de récupération',
RECOVERY_NOT_FOUND: 'Application de récupération introuvable en ligne'
};
export default fr;
+4 -3
View File
@@ -336,7 +336,6 @@ const it: Translation = {
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio',
FIRMWARE_VERSION_INFO: 'Informazioni sulla versione del firmware',
USER_PROFILE: 'Profilo utente',
STORED_VERSIONS: 'Versioni memorizzate',
ONLINE_HELP: 'aiuto online',
UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti',
UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di prima scaricare un backup del sistema prima di continuare, e poi caricare questo file dopo l\'installazione della nuova versione.',
@@ -353,9 +352,11 @@ const it: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Klik per selezionare, o drop un file firmware .bin/.md5 qui',
UPLOAD_MD5_RECEIVED: 'Digest ricevuto, ora caricare il file firmware .bin',
UPLOAD_MD5_MATCHED: 'MD5 corrispondente, installazione firmware...',
RECOVERY: 'Ripristino',
RECOVERY_APP: 'App di ripristino',
RECOVERY_NOT_INSTALLED: 'Non installato',
RECOVERY_INSTALLED: 'Firmware di ripristino installato'
RECOVERY_INSTALLED: 'Firmware di ripristino installato',
RESTART_INTO_RECOVERY: 'Riavvia nell\'app di ripristino',
RECOVERY_NOT_FOUND: 'App di ripristino non trovata online'
};
export default it;
+4 -3
View File
@@ -336,7 +336,6 @@ const nl: Translation = {
SWITCH_RELEASE_TYPE: 'Switch naar {0} release',
FIRMWARE_VERSION_INFO: 'Informatie over firmwareversie',
USER_PROFILE: 'Gebruikersprofiel',
STORED_VERSIONS: 'Opgeslagen versies',
ONLINE_HELP: 'online help',
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten',
UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u eerst een Systeem Backup download voordat u doorgaat, en upload deze file na de installatie van de nieuwe versie.',
@@ -353,9 +352,11 @@ const nl: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Klik voor selecteren, of drop een firmware .bin/.md5 bestand hier',
UPLOAD_MD5_RECEIVED: 'Digest ontvangen, upload nu het firmware .bin-bestand',
UPLOAD_MD5_MATCHED: 'MD5 komt overeen, firmware wordt geïnstalleerd...',
RECOVERY: 'Herstel',
RECOVERY_APP: 'Herstel-app',
RECOVERY_NOT_INSTALLED: 'Niet geïnstalleerd',
RECOVERY_INSTALLED: 'Herstelfirmware geïnstalleerd'
RECOVERY_INSTALLED: 'Herstelfirmware geïnstalleerd',
RESTART_INTO_RECOVERY: 'Herstart naar de herstel-app',
RECOVERY_NOT_FOUND: 'Herstel-app niet online gevonden'
};
export default nl;
+4 -3
View File
@@ -336,7 +336,6 @@ const no: Translation = {
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse',
FIRMWARE_VERSION_INFO: 'Informasjon om firmwareversjon',
USER_PROFILE: 'Brukerprofil',
STORED_VERSIONS: 'Lagret versjoner',
ONLINE_HELP: 'online hjelp',
UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger',
UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du først lastet ned en System Backup før du fortsetter, og last denne filen etter at den nye versjonen er installert.',
@@ -353,9 +352,11 @@ const no: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Klikk for å velge, eller drop en firmware .bin/.md5 her',
UPLOAD_MD5_RECEIVED: 'MD5 mottatt, last nå opp firmware .bin-filen',
UPLOAD_MD5_MATCHED: 'MD5 stemmer, firmware installeres...',
RECOVERY: 'Gjenoppretting',
RECOVERY_APP: 'Gjenopprettingsapp',
RECOVERY_NOT_INSTALLED: 'Ikke installert',
RECOVERY_INSTALLED: 'Gjenopprettingsfirmware installert'
RECOVERY_INSTALLED: 'Gjenopprettingsfirmware installert',
RESTART_INTO_RECOVERY: 'Start på nytt i gjenopprettingsappen',
RECOVERY_NOT_FOUND: 'Fant ikke gjenopprettingsappen på nettet'
};
export default no;
+4 -3
View File
@@ -336,7 +336,6 @@ const pl: BaseTranslation = {
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie',
FIRMWARE_VERSION_INFO: 'Informacje o wersji firmware',
USER_PROFILE: 'Profil użytkownika',
STORED_VERSIONS: 'Zapisane wersje',
ONLINE_HELP: 'pomoc online',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości',
UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że najpierw pobierzesz kopię zapasową systemu przed kontynuowaniem, a następnie przesuń tę plik po zainstalowaniu nowej wersji.',
@@ -353,9 +352,11 @@ const pl: BaseTranslation = {
UPLOAD_FIRMWARE_TEXT: 'Kliknij, aby wybrać, lub drop plik firmware .bin/.md5 tutaj',
UPLOAD_MD5_RECEIVED: 'Suma MD5 odebrana, teraz wgraj plik firmware .bin',
UPLOAD_MD5_MATCHED: 'MD5 zgodne, instalowanie firmware...',
RECOVERY: 'Odzyskiwanie',
RECOVERY_APP: 'Aplikacja odzyskiwania',
RECOVERY_NOT_INSTALLED: 'Nie zainstalowano',
RECOVERY_INSTALLED: 'Zainstalowano firmware odzyskiwania'
RECOVERY_INSTALLED: 'Zainstalowano firmware odzyskiwania',
RESTART_INTO_RECOVERY: 'Uruchom ponownie w aplikacji odzyskiwania',
RECOVERY_NOT_FOUND: 'Nie znaleziono aplikacji odzyskiwania online'
};
export default pl;
+4 -3
View File
@@ -336,7 +336,6 @@ const sk: Translation = {
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu',
FIRMWARE_VERSION_INFO: 'Informácie o verzii firmware',
USER_PROFILE: 'Profil používateľa',
STORED_VERSIONS: 'Uložené verzie',
ONLINE_HELP: 'online pomoc',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy',
UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že najprv stiahnete systémovú zálohu pred pokračovaním, a potom nahrajte tento súbor po instalácii novej verzie.',
@@ -353,9 +352,11 @@ const sk: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Kliknite, aby ste vybrali, alebo drop súbor firmware .bin/.md5 sem',
UPLOAD_MD5_RECEIVED: 'Suma MD5 prijatá, teraz nahrajte firmware .bin',
UPLOAD_MD5_MATCHED: 'MD5 súhlasí, firmvér sa inštaluje...',
RECOVERY: 'Obnova',
RECOVERY_APP: 'Aplikácia na obnovu',
RECOVERY_NOT_INSTALLED: 'Nenainštalované',
RECOVERY_INSTALLED: 'Firmvér obnovy nainštalovaný'
RECOVERY_INSTALLED: 'Firmvér obnovy nainštalovaný',
RESTART_INTO_RECOVERY: 'Reštartovať do aplikácie na obnovu',
RECOVERY_NOT_FOUND: 'Aplikácia na obnovu sa online nenašla'
};
export default sk;
+4 -3
View File
@@ -336,7 +336,6 @@ const sv: Translation = {
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva',
FIRMWARE_VERSION_INFO: 'Information om firmwareversion',
USER_PROFILE: 'Användarprofil',
STORED_VERSIONS: 'Lagrad versioner',
ONLINE_HELP: 'online hjälp',
UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden',
UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du först laddar ned en System Backup innan du fortsätter, och ladda upp denna fil efter att den nya versionen är installerad.',
@@ -353,9 +352,11 @@ const sv: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Klicka för att välja, eller drop en firmware .bin/.md5 här',
UPLOAD_MD5_RECEIVED: 'MD5 mottagen, ladda nu upp firmware .bin-filen',
UPLOAD_MD5_MATCHED: 'MD5 stämmer, firmware installeras...',
RECOVERY: 'Återställning',
RECOVERY_APP: 'Återställningsapp',
RECOVERY_NOT_INSTALLED: 'Inte installerad',
RECOVERY_INSTALLED: 'Återställningsfirmware installerad'
RECOVERY_INSTALLED: 'Återställningsfirmware installerad',
RESTART_INTO_RECOVERY: 'Starta om i återställningsappen',
RECOVERY_NOT_FOUND: 'Återställningsappen hittades inte online'
};
export default sv;
+4 -3
View File
@@ -336,7 +336,6 @@ const tr: Translation = {
SWITCH_RELEASE_TYPE: '{0} sürümüne geç',
FIRMWARE_VERSION_INFO: 'Firmware Sürüm Bilgisi',
USER_PROFILE: 'Kullanıcı Profili',
STORED_VERSIONS: 'Kaydedilmiş Sürümler',
ONLINE_HELP: 'online yardım',
UPGRADE_IMPORTANT_MESSAGES: 'Önemli Mesajları Güncelle',
UPGRADE_IMPORTANT_MESSAGES_1: 'Bu güncelleme továrnı ayarlarını gerektirir. Yapılandırmanızı ve ayarlarınızı önce yedekleyin ve ardından yeni sürüm yüklendikten sonra yükleyin.',
@@ -353,9 +352,11 @@ const tr: Translation = {
UPLOAD_FIRMWARE_TEXT: 'Bir firmware .bin dosyası, buraya drop yapın',
UPLOAD_MD5_RECEIVED: 'Özet alındı, şimdi firmware .bin dosyasını yükleyin',
UPLOAD_MD5_MATCHED: 'MD5 eşleşti, firmware yükleniyor...',
RECOVERY: 'Kurtarma',
RECOVERY_APP: 'Kurtarma Uygulaması',
RECOVERY_NOT_INSTALLED: 'Yüklü değil',
RECOVERY_INSTALLED: 'Kurtarma firmware yüklendi'
RECOVERY_INSTALLED: 'Kurtarma firmware yüklendi',
RESTART_INTO_RECOVERY: 'Kurtarma uygulamasına yeniden başlat',
RECOVERY_NOT_FOUND: 'Kurtarma uygulaması çevrimiçi bulunamadı'
};
export default tr;
+132 -46
View File
@@ -2,6 +2,7 @@ import { type ReactNode, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import HealthAndSafetyIcon from '@mui/icons-material/HealthAndSafety';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import LogoutIcon from '@mui/icons-material/Logout';
import MemoryIcon from '@mui/icons-material/Memory';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
@@ -17,11 +18,16 @@ import {
DialogContent,
DialogTitle,
Divider,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemText,
MenuItem,
Table,
TableBody,
TableCell,
TableRow,
TextField,
Typography
} from '@mui/material';
@@ -38,6 +44,9 @@ import type { RecoveryPartition, RecoveryStatus } from './types';
const formatMB = (bytes: number) => `${(bytes / 1048576).toFixed(2)} MB`;
const formatInstallDate = (epoch: number) =>
epoch > 0 ? new Date(epoch * 1000).toLocaleString() : 'unknown';
const formatUptime = (seconds: number) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
@@ -81,13 +90,27 @@ const SectionTitle = ({ children }: { children: ReactNode }) => (
</Typography>
);
// same look as the version info dialog in the EMS-ESP app
const InfoRow = ({ label, value }: { label: string; value: ReactNode }) => (
<Box sx={{ display: 'flex', py: 0.5 }}>
<Typography sx={{ width: 160, flexShrink: 0 }} color="text.secondary">
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13,
width: 140
}}
>
{label}
</Typography>
<Typography>{value}</Typography>
</Box>
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{value}
</TableCell>
</TableRow>
);
interface DashboardProps {
@@ -98,6 +121,7 @@ interface DashboardProps {
const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
const [confirmBoot, setConfirmBoot] = useState<RecoveryPartition>();
const [infoPartition, setInfoPartition] = useState<RecoveryPartition>();
const [confirmFactoryReset, setConfirmFactoryReset] = useState(false);
const [pickedTarget, setPickedTarget] = useState<string>();
const uploadTarget = pickedTarget ?? status.upload_target;
@@ -240,6 +264,16 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
label="unconfirmed"
/>
)}
{p.valid && (
<IconButton
size="small"
onClick={() => setInfoPartition(p)}
aria-label="Firmware version info"
sx={{ p: 0.5 }}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
)}
</Box>
}
secondary={
@@ -287,46 +321,53 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
<SectionContent>
<SectionTitle>System</SectionTitle>
<InfoRow label="Recovery version" value={'v' + status.recovery_version} />
<InfoRow
label="Chip"
value={`${status.chip}, ${formatMB(status.flash_size)} flash${status.psram_size ? ', ' + formatMB(status.psram_size) + ' PSRAM' : ''}`}
/>
<InfoRow
label="Free heap"
value={`${Math.round(status.free_heap / 1024)} KB`}
/>
<InfoRow label="Uptime" value={formatUptime(status.uptime)} />
<InfoRow label="MAC address" value={status.mac} />
<InfoRow
label="Access point"
value={`${status.ap_ssid} on ${status.ap_ip} (${status.ap_clients} connected)`}
/>
{status.eth_state && (
<InfoRow
label="Ethernet"
value={
status.eth_state === 'connected'
? `connected, ${status.eth_ip ?? ''}${status.static_ip ? ' (static IP)' : ''}`
: ETH_STATE_TEXT[status.eth_state]
}
/>
)}
<InfoRow
label="WiFi"
value={
!status.wifi_ssid
? 'not configured'
: status.wifi_connected
? `${status.wifi_ssid}, ${status.wifi_ip ?? ''} (${status.wifi_rssi ?? 0} dBm)${status.static_ip ? ', static IP' : ''}`
: !status.wifi_started
? status.eth_state === 'connected'
? 'not used, Ethernet is connected'
: 'waiting for Ethernet'
: `connecting to ${status.wifi_ssid}`
}
/>
<InfoRow label="Hostname" value={status.hostname} />
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
<TableBody>
<InfoRow
label="Recovery version"
value={'v' + status.recovery_version}
/>
<InfoRow
label="Chip"
value={`${status.chip}, ${formatMB(status.flash_size)} flash${status.psram_size ? ', ' + formatMB(status.psram_size) + ' PSRAM' : ''}`}
/>
<InfoRow
label="Free heap"
value={`${Math.round(status.free_heap / 1024)} KB`}
/>
<InfoRow label="Uptime" value={formatUptime(status.uptime)} />
<InfoRow label="MAC address" value={status.mac} />
<InfoRow
label="Access point"
value={`${status.ap_ssid} on ${status.ap_ip} (${status.ap_clients} connected)`}
/>
{status.eth_state && (
<InfoRow
label="Ethernet"
value={
status.eth_state === 'connected'
? `connected, ${status.eth_ip ?? ''}${status.static_ip ? ' (static IP)' : ''}`
: ETH_STATE_TEXT[status.eth_state]
}
/>
)}
<InfoRow
label="WiFi"
value={
!status.wifi_ssid
? 'not configured'
: status.wifi_connected
? `${status.wifi_ssid}, ${status.wifi_ip ?? ''} (${status.wifi_rssi ?? 0} dBm)${status.static_ip ? ', static IP' : ''}`
: !status.wifi_started
? status.eth_state === 'connected'
? 'not used, Ethernet is connected'
: 'waiting for Ethernet'
: `connecting to ${status.wifi_ssid}`
}
/>
<InfoRow label="Hostname" value={status.hostname} />
</TableBody>
</Table>
<ButtonRow>
{!status.disable_reset && (
@@ -342,7 +383,7 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
<Button
startIcon={<RestartAltIcon />}
variant="outlined"
color="error"
color="success"
onClick={() => void doRestart(status.boot)}
>
Restart{status.boot !== status.running ? ` (boots ${status.boot})` : ''}
@@ -351,6 +392,7 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
startIcon={<LogoutIcon />}
variant="outlined"
color="secondary"
sx={{ float: 'right' }}
onClick={onSignOut}
>
Sign Out
@@ -424,6 +466,50 @@ const Dashboard = ({ status, onRestarting, onSignOut }: DashboardProps) => {
</Button>
</DialogActions>
</Dialog>
<Dialog
sx={dialogStyle}
open={!!infoPartition}
onClose={() => setInfoPartition(undefined)}
>
<DialogTitle>Firmware Version Info</DialogTitle>
<DialogContent dividers>
{infoPartition && (
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
<TableBody>
<InfoRow
label="Version"
value={
infoPartition.version ? 'v' + infoPartition.version : 'unknown'
}
/>
<InfoRow
label="Type"
value={infoPartition.factory ? 'Recovery App' : 'EMS-ESP Firmware'}
/>
<InfoRow label="Partition" value={infoPartition.label} />
<InfoRow
label="Size"
value={`${formatMB(infoPartition.image_size)} of ${formatMB(infoPartition.size)}`}
/>
<InfoRow
label="Install Date"
value={formatInstallDate(infoPartition.install_date)}
/>
</TableBody>
</Table>
)}
</DialogContent>
<DialogActions>
<Button
variant="outlined"
color="secondary"
onClick={() => setInfoPartition(undefined)}
>
Close
</Button>
</DialogActions>
</Dialog>
</>
);
};
+2 -2
View File
@@ -15,7 +15,7 @@
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"prettier": "^3.9.9"
"prettier": "^3.9.10"
},
"packageManager": "pnpm@12.10.1"
"packageManager": "pnpm@12.11.2"
}
+69 -69
View File
@@ -7,153 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
specifier: 12.10.1
version: 12.10.1
specifier: 12.11.2
version: 12.11.2
packages:
'@pnpm/exe.android-arm64@12.10.1':
resolution: {integrity: sha512-CVTlHPwNvadQCJD9bGSMzWIhFX8QpjtqC2c27dhdnn/9iKzEjZ3qL62o3gHpKbIuo6qn4WxTKRoaokgNdReBig==}
'@pnpm/exe.android-arm64@12.11.2':
resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.10.1':
resolution: {integrity: sha512-NFiYLIaodY7da0XIebwcU3JoP7IK/lzF7zgKSSP+9H+QcFnQ1GDfBkuBKCYJzZ+Xu0WH54neIPLBBCaOjaaauA==}
'@pnpm/exe.android-x64@12.11.2':
resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.10.1':
resolution: {integrity: sha512-0O8tdQ8ijlPJudXspLMTcitMkdBEz6uUflazL5uQE0WMs85oQIp0U+5hfEktMFB64QqEhjqlvvujTwbzdr9oMg==}
'@pnpm/exe.darwin-arm64@12.11.2':
resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==}
cpu: [arm64]
os: [darwin]
'@pnpm/exe.darwin-x64@12.10.1':
resolution: {integrity: sha512-ud+1/uZOKEo8P/AsPynOJc/LNwfI0j6WwCElibj71rU9l4nIBnsSn+wWTIPZPkZ4BBSH5+82azSnAARMyJ1lNA==}
'@pnpm/exe.darwin-x64@12.11.2':
resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==}
cpu: [x64]
os: [darwin]
'@pnpm/exe.freebsd-x64@12.10.1':
resolution: {integrity: sha512-mj9PE5rf8uAfQQ2K66HSATMSut+lNiB4eSqFd5+azFVCMnxNSy/32xpZQJT21PhnN3Y4qbK+iKHe5nWAxSuRhw==}
'@pnpm/exe.freebsd-x64@12.11.2':
resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.10.1':
resolution: {integrity: sha512-xba+00wZJ8Um6rBJJjWl88uJj0tydB8dtomvjlCLvi0uL32vpa9CRP8Vv26FIXvbiZ1bo2RCzYt/eChxJVV52w==}
'@pnpm/exe.linux-arm64-musl@12.11.2':
resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-arm64@12.10.1':
resolution: {integrity: sha512-oBdMp69ScC9UnniG4qZAQVr2fBDSXjm4yk/72vkEVyrlwS23yMxcRQl7Qdo3sgXPAygY6TWnPRKm4rXzjoyW7w==}
'@pnpm/exe.linux-arm64@12.11.2':
resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-ppc64@12.10.1':
resolution: {integrity: sha512-Yyb3wu09h3c7mLme270uHFiVaAIUHC411Ru9LIU2BKUIstrGlXa9OqqVWoXaRgYRdFdOFknkwG7GNn6ygwLbnw==}
'@pnpm/exe.linux-ppc64@12.11.2':
resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.10.1':
resolution: {integrity: sha512-PdkeUFFkybknnGkWP0H22eMpIiS4OZXL5BcQzF4lh+vmrW3IaC2HOkbANKgot/PKO0Kl77pzA/6PKFxgbgfXIQ==}
'@pnpm/exe.linux-riscv64@12.11.2':
resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.10.1':
resolution: {integrity: sha512-1okKuHKJwJPAbofDxfhZWAUxEWtee4ZSf7Pah8kcvocaJkf2WxFJ5KVTiNIvDml5GLidDQsgf7Pcf/WHbCak7Q==}
'@pnpm/exe.linux-s390x@12.11.2':
resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.10.1':
resolution: {integrity: sha512-e6IqzVNkPa7srFKJKnY23uWJ+qz0LaRWiTUo/sHfFXRRMRzB5uNOr6qWWip/jqhdGwU91n9J7LrEYZ6e0tRczw==}
'@pnpm/exe.linux-x64-musl@12.11.2':
resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@pnpm/exe.linux-x64@12.10.1':
resolution: {integrity: sha512-ZByV0DRgWzGaDSjT5ddRBASdwz+r/qPMYh7o6RGmPQjtRGNFbSl94xY2DcAM8mqCOZJUUDtAXQ+wpBXcdpKlkg==}
'@pnpm/exe.linux-x64@12.11.2':
resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@pnpm/exe.win32-arm64@12.10.1':
resolution: {integrity: sha512-I7QZHcjioG7orFeCRUFUHByJ1G0zFPnAlaGGyrUvb6MGkv5oeNwNPl7t+Gp+7zHfnMcgLGJ1a+Wfim0IDw3lOw==}
'@pnpm/exe.win32-arm64@12.11.2':
resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==}
cpu: [arm64]
os: [win32]
'@pnpm/exe.win32-x64@12.10.1':
resolution: {integrity: sha512-4WJFdp58EKlCj0zLobrXU0vpszSZIr4X/3y/uNbAQqGW0zE+VdruZB0gFc3L2kzsUzVwTYbaCn+XEhnCYSAlTg==}
'@pnpm/exe.win32-x64@12.11.2':
resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==}
cpu: [x64]
os: [win32]
pnpm@12.10.1:
resolution: {integrity: sha512-ukCjfrGjcNYP6oxM8dNkwTvMzvUY6YrSvO6VT1OdONo3RY5YJho/pV4GUjRQ6g//+fSXsTW+QfLBXBZCusrObA==}
pnpm@12.11.2:
resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
'@pnpm/exe.android-arm64@12.10.1':
'@pnpm/exe.android-arm64@12.11.2':
optional: true
'@pnpm/exe.android-x64@12.10.1':
'@pnpm/exe.android-x64@12.11.2':
optional: true
'@pnpm/exe.darwin-arm64@12.10.1':
'@pnpm/exe.darwin-arm64@12.11.2':
optional: true
'@pnpm/exe.darwin-x64@12.10.1':
'@pnpm/exe.darwin-x64@12.11.2':
optional: true
'@pnpm/exe.freebsd-x64@12.10.1':
'@pnpm/exe.freebsd-x64@12.11.2':
optional: true
'@pnpm/exe.linux-arm64-musl@12.10.1':
'@pnpm/exe.linux-arm64-musl@12.11.2':
optional: true
'@pnpm/exe.linux-arm64@12.10.1':
'@pnpm/exe.linux-arm64@12.11.2':
optional: true
'@pnpm/exe.linux-ppc64@12.10.1':
'@pnpm/exe.linux-ppc64@12.11.2':
optional: true
'@pnpm/exe.linux-riscv64@12.10.1':
'@pnpm/exe.linux-riscv64@12.11.2':
optional: true
'@pnpm/exe.linux-s390x@12.10.1':
'@pnpm/exe.linux-s390x@12.11.2':
optional: true
'@pnpm/exe.linux-x64-musl@12.10.1':
'@pnpm/exe.linux-x64-musl@12.11.2':
optional: true
'@pnpm/exe.linux-x64@12.10.1':
'@pnpm/exe.linux-x64@12.11.2':
optional: true
'@pnpm/exe.win32-arm64@12.10.1':
'@pnpm/exe.win32-arm64@12.11.2':
optional: true
'@pnpm/exe.win32-x64@12.10.1':
'@pnpm/exe.win32-x64@12.11.2':
optional: true
pnpm@12.10.1:
pnpm@12.11.2:
optionalDependencies:
'@pnpm/exe.android-arm64': 12.10.1
'@pnpm/exe.android-x64': 12.10.1
'@pnpm/exe.darwin-arm64': 12.10.1
'@pnpm/exe.darwin-x64': 12.10.1
'@pnpm/exe.freebsd-x64': 12.10.1
'@pnpm/exe.linux-arm64': 12.10.1
'@pnpm/exe.linux-arm64-musl': 12.10.1
'@pnpm/exe.linux-ppc64': 12.10.1
'@pnpm/exe.linux-riscv64': 12.10.1
'@pnpm/exe.linux-s390x': 12.10.1
'@pnpm/exe.linux-x64': 12.10.1
'@pnpm/exe.linux-x64-musl': 12.10.1
'@pnpm/exe.win32-arm64': 12.10.1
'@pnpm/exe.win32-x64': 12.10.1
'@pnpm/exe.android-arm64': 12.11.2
'@pnpm/exe.android-x64': 12.11.2
'@pnpm/exe.darwin-arm64': 12.11.2
'@pnpm/exe.darwin-x64': 12.11.2
'@pnpm/exe.freebsd-x64': 12.11.2
'@pnpm/exe.linux-arm64': 12.11.2
'@pnpm/exe.linux-arm64-musl': 12.11.2
'@pnpm/exe.linux-ppc64': 12.11.2
'@pnpm/exe.linux-riscv64': 12.11.2
'@pnpm/exe.linux-s390x': 12.11.2
'@pnpm/exe.linux-x64': 12.11.2
'@pnpm/exe.linux-x64-musl': 12.11.2
'@pnpm/exe.win32-arm64': 12.11.2
'@pnpm/exe.win32-x64': 12.11.2
---
lockfileVersion: '9.0'
@@ -175,10 +175,10 @@ importers:
devDependencies:
'@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.2
version: 6.0.2(prettier@3.9.9)
version: 6.0.2(prettier@3.9.10)
prettier:
specifier: ^3.9.9
version: 3.9.9
specifier: ^3.9.10
version: 3.9.10
packages:
@@ -303,8 +303,8 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
prettier@3.9.9:
resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==}
prettier@3.9.10:
resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
engines: {node: '>=14'}
hasBin: true
@@ -373,7 +373,7 @@ snapshots:
'@msgpack/msgpack@3.1.3': {}
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.9)':
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.10)':
dependencies:
'@babel/generator': 7.29.8
'@babel/parser': 7.29.9
@@ -383,7 +383,7 @@ snapshots:
lodash-es: 4.18.1
minimatch: 9.0.9
parse-imports-exports: 0.2.4
prettier: 3.9.9
prettier: 3.9.10
transitivePeerDependencies:
- supports-color
@@ -421,4 +421,4 @@ snapshots:
picocolors@1.1.1: {}
prettier@3.9.9: {}
prettier@3.9.10: {}
+9 -5
View File
@@ -103,7 +103,7 @@ let system_status = {
cpu_cores: 2,
cpu_freq_mhz: 240,
max_alloc_heap: 191,
arduino_version: 'Tasmota Arduino v3.3.8',
arduino_version: 'Arduino v3.3.8',
sdk_version: '5.5.4.260407',
partition: 'app0',
flash_chip_size: 16384,
@@ -116,8 +116,8 @@ let system_status = {
psram: true,
psram_size: 8189,
free_psram: 8166,
// recovery_version: '', // upload an EMS-ESP-Recovery-*.bin to set it
recovery_version: '1.0.0',
recovery_version: '', // upload an EMS-ESP-Recovery-*.bin to set it
// recovery_version: '1.0.0',
has_partition: true,
partitions: [
{
@@ -210,7 +210,7 @@ switch (emulate_esp) {
case 'ESP32':
system_status.esp_platform = 'ESP32';
system_status.cpu_type = 'ESP32-D0WD-V3';
system_status.arduino_version = 'Tasmota Arduino v3.3.8';
system_status.arduino_version = 'Arduino v3.3.8';
system_status.sdk_version = '5.5.4.260407';
system_status.psram = true;
system_status.psram_size = 4096;
@@ -226,7 +226,7 @@ switch (emulate_esp) {
default:
system_status.esp_platform = 'ESP32S3';
system_status.cpu_type = 'ESP32-S3';
system_status.arduino_version = 'Tasmota Arduino v3.3.8';
system_status.arduino_version = 'Arduino v3.3.8';
system_status.sdk_version = '5.5.4.260407';
system_status.psram = true;
system_status.psram_size = 8189;
@@ -5447,6 +5447,10 @@ router
} else if (action === 'uploadURL') {
// upload URL
console.log('upload File from URL', content.param);
if (String(content.param).toLowerCase().includes('recovery')) {
// the recovery app is installed without a restart
system_status.recovery_version = '1.0.0';
}
return status(200);
} else if (action === 'resetMQTT') {
// reset MQTT
+51 -22
View File
@@ -2,36 +2,46 @@
[common]
; custom build flags to use in my_build_flags
; -DEMSESP_UART_DEBUG ; debugging UART
; -DEMSESP_DEBUG ; enables DEBUG to the log. Will generate a lot of extra traffic on Console and Syslog
; -DEMSESP_DEFAULT_BOARD_PROFILE=\"NODEMCU\" ; hard code the default board name
; -DEMSESP_DEBUG_SENSOR ; additional debug for the sensors
; -DCORE_DEBUG_LEVEL=5 ; verbose core level debugging 5=verbose, 4=debug, 3=info
; -DEMSESP_TEST ; enable the tests. EN language only
; -DEMSESP_DE_ONLY ; only DE translated entity names
; -DEMSESP_EN_ONLY ; only EN translated entity names
; -DEMSESP_PINGTEST ; send log message every 1/2 second
; -D EMSESP_UART_DEBUG ; debugging UART
; -D EMSESP_DEBUG ; enables DEBUG to the log. Will generate a lot of extra traffic on Console and Syslog
; -D EMSESP_DEFAULT_BOARD_PROFILE=\"NODEMCU\" ; hard code the default board name
; -D EMSESP_DEBUG_SENSOR ; additional debug for the sensors
; -D CORE_DEBUG_LEVEL=5 ; verbose core level debugging 5=verbose, 4=debug, 3=info
; -D EMSESP_TEST ; enable the tests. EN language only
; -D EMSESP_DE_ONLY ; only DE translated entity names
; -D EMSESP_EN_ONLY ; only EN translated entity names
; -D EMSESP_PINGTEST ; send log message every 1/2 second
; -D EMSESP_GATEWAY_TX_HOLD ; hold Tx while a K30RF/KM200 gateway is syncing, see issue #3094
; -D EMSESP_TX_PRE_DELAY=1000 ; wait this many us before each Tx so we don't overlap the master's break, see issue #3094
; my_build_flags = -DEMSESP_TEST -DEMSESP_DEBUG
; my_build_flags = -DEMSESP_DEBUG -DEMSESP_TEST -DEMSESP_PINGTEST
; my_build_flags = -DEMSESP_DEBUG -DCORE_DEBUG_LEVEL=5
; my_build_flags = -DEMSESP_EN_ONLY -DEMSESP_TEST -DCORE_DEBUG_LEVEL=5 -DARDUINO_USB_CDC_ON_BOOT=1
my_build_flags =
; my_build_flags = -DEMSESP_EN_ONLY
; my_build_flags = -D EMSESP_GATEWAY_TX_HOLD -D EMSESP_TX_PRE_DELAY=1000
my_build_flags = -DEMSESP_DEBUG
; my_build_flags =
[platformio]
default_envs = s_16M_P ; BBQKees E32V2
; default_envs = build-webUI ; build the web interface only
; default_envs = s3_16M_P ; BBQKees S3
; default_envs = s_4M ; BBQKees older S32, 4MB no psram
; default_envs = s_16M ; BBQKees newer S32 V2, 16MB no psram
; default_envs = c6 ; XIAO ESP32C
; default_envs = debug
default_envs = s_16M_P, recovery_s_16M_P, build-recovery, build-webUI
; default_envs = s_16M_P ; BBQKees E32V2
; default_envs = s_16M_P_custom ; BBQKees E32V2 with custom Arduino/IDF libs
; default_envs = recovery_s_16M_P ; recovery firmware
; default_envs = crashtest_s_16M_P ; crash test
; default_envs = build-webUI ; build the main ems-esp webUI only
; default_envs = build-recovery ; build the recovery WebUI only
; default_envs = s3_16M_P ; BBQKees S3 firmware
; default_envs = s_4M ; firmware forBBQKees older S32, 4MB no psram
; default_envs = s_16M ; firmware for BBQKees newer S32 V2, 16MB no psram
; default_envs = c6 ; firmware for XIAO ESP32C
; default_envs = debug ; see below
[env]
; uncomment if you want to upload the firmware via OTA (must have upload_protocol = custom)
extra_scripts =
pre:scripts/build_interface.py ; builds the WebUI
scripts/rename_fw.py ; renames the firmware .bin file - comment out if not needed
post:scripts/rename_fw.py ; renames the firmware .bin file
scripts/upload.py ; optionally upload the firmware via OTA (must have upload_protocol = custom)
; set the username and password for the admin account (default password is admin)
@@ -40,13 +50,33 @@ custom_password = admin
; ** upload settings **
; for direct USB, which is the default you can set an optional port for example:
; upload_port = /dev/ttyUSB0 or COM3
; upload_speed = 921600
; upload_port = /dev/ttyUSB0
; upload_port = /dev/ttyACM0
; CH340G bridge tops out here reliably; drop back to 460800 if uploads fail
; 2000000 is the max speed for the CH340G bridge
upload_speed = 921600
; for direct via HTTP (OTA) set:
; upload_protocol = custom
; custom_emsesp_ip = <ip address> or ems-esp.local
upload_protocol = custom
custom_emsesp_ip = ems-esp.local ; E32V2.2 (test)
; custom_emsesp_ip = ems-espP.local ; E32V2.2 (prod)
; custom_emsesp_ip = ems-esps32.local ; S32, 4MB no psramS32
; custom_emsesp_ip = ems-espS3.local ; ems-espS3
; lib_deps =
; bblanchon/ArduinoJson @ 7.4.3
; ESP32Async/AsyncTCP @ 3.4.10
; ESP32Async/ESPAsyncWebServer @ 3.11.0
; https://github.com/mobizt/ReadyMail.git @ 0.4.0
; https://github.com/mobizt/ESP_SSLClient.git @ 3.1.3
; https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8
; ; file://${PROJECT_DIR}/../modules/EMS-ESP-Modules
; ** debug settings **
; to be used with an ESP32-S3 which has an onboard JTAG, connect the OTG USB port to the PC
; if using an external JTAG board like the ESP-PROG set debug_tool = esp-prog, and use zadig to set the correct driver for the USB port
@@ -69,4 +99,3 @@ debug_init_break =
upload_speed = 2000000
monitor_speed = 115200
extra_scripts =
; debug_build_flags = -Og -g2 -ggdb2
+51 -8
View File
@@ -6,6 +6,8 @@
; For example: EMS-ESP-3_7_0-dev_31-esp32-16MB+.bin
;
; boards can be found in https://github.com/pioarduino/platform-espressif32/tree/main/boards
; Tasmota platform 2026.09.50 needs PlatformIO Core >=6.2.0. Official platformio is still
; 6.1.19 — install pioarduino instead (see CONTRIBUTING.md). `pio upgrade` will not get 6.2.0.
[platformio]
; build all supported targets. Use pio_local.ini to override and build specific targets.
@@ -21,8 +23,8 @@ extra_configs =
pio_local.ini
[common]
core_build_flags = -std=gnu++20 -O3 -flto=auto -Wno-type-limits -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-format -Wno-missing-field-initializers
core_unbuild_flags = -std=gnu++11 -std=gnu++14 -std=gnu++17 -fno-lto
core_build_flags = -Wall -Wextra -Wno-type-limits -Wno-unused-parameter -Wno-unused-variable -Wno-format -Wno-missing-field-initializers
core_unbuild_flags = -O2 -O3 -Og -fno-lto
my_build_flags =
@@ -31,6 +33,9 @@ build_flags =
${factory_settings.build_flags}
${common.my_build_flags}
-I./src/core
; core 3.3.12 uses spi_host_device_t in ETH.h without including it. A framework
; refresh restores that header, so force the declaration on every compile.
-include hal/spi_types.h
-D ARDUINO_ARCH_ESP32
-D ONEWIRE_CRC16=0
-D CONFIG_UART_ISR_IN_IRAM
@@ -60,15 +65,11 @@ unbuild_flags =
[espressif32_base_4M]
framework = arduino
board_build.partitions = partitions/esp32_partition_4M.csv
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.05.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
; platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.09.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.12 based on IDF 5.5.5
; 16MB Flash variants
[espressif32_base_16M]
framework = arduino
board_build.partitions = partitions/esp32_partition_16M.csv
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.05.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
; platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.09.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.12 based on IDF 5.5.5
build_flags =
${common.build_flags}
-D EMSESP_HAS_RECOVERY ; installs and falls back to the recovery firmware in the factory partition
@@ -78,15 +79,21 @@ build_flags =
[espressif32_base_32M]
framework = arduino
board_build.partitions = partitions/esp32_partition_32M.csv
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.05.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
; platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.09.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.12 based on IDF 5.5.5
[env]
; use own Ardunio libs packages from https://github.com/emsesp/esp32-arduino-lib-builder which is based on Arduino Core 3.3.12 and IDF 5.5.5. Same as s_16M_P_custom
; with Tasmota's tool chain (platform)
; if we want to go back to Tasmota libs, use the following:
; platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.05.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.09.50/platform-espressif32.zip
platform_packages = framework-arduinoespressif32 @ https://github.com/emsesp/esp32-arduino-lib-builder/releases/download/1110-0945-5.5.5/framework-arduinoespressif32-v5.5.5-a63b839f.tar.xz
build_flags =
${common.build_flags}
build_unflags =
${common.unbuild_flags}
extra_scripts =
pre:scripts/enable_ccache.py ; uses ccache when it is installed
post:scripts/rename_fw.py ; renames the firmware .bin file
monitor_speed = 115200
monitor_filters = direct, esp32_exception_decoder
@@ -109,6 +116,14 @@ extra_scripts = pre:scripts/build_interface.py
; Exclude all source files so the C code doesn't build
build_src_filter = -<*>
; builds the recovery web interface only (recovery/WWWData.h), not the recovery firmware
[env:build-recovery]
platform = native
targets = build
extra_scripts = pre:scripts/build_recovery_interface.py
; Exclude all source files so the C code doesn't build
build_src_filter = -<*>
;
; Builds for different board types
; If you're building for a single target environment, we recommend creating a pio_local.ini (see example file)
@@ -129,6 +144,34 @@ board = s_16M
extends = espressif32_base_16M
board = s_16M_P
[env:s_16M_P_custom]
; same as s_16M_P, but the Arduino/IDF libs are rebuilt with HybridCompile using the custom_sdkconfig below
; this is same procedure as in the esp32-arduino-lib-builder repository
extends = env:s_16M_P
custom_sdkconfig =
'# CONFIG_BT_ENABLED is not set'
'# CONFIG_LWIP_PPP_SUPPORT is not set'
'# CONFIG_LWIP_IP_FORWARD is not set'
'# CONFIG_LWIP_IPV4_NAPT is not set'
'# CONFIG_LWIP_IPV4_NAPT_PORTMAP is not set'
'# CONFIG_ULP_COPROC_ENABLED is not set'
'# CONFIG_ETH_USE_SPI_ETHERNET is not set'
'# CONFIG_ETH_SPI_ETHERNET_W5500 is not set'
'# CONFIG_ETH_SPI_ETHERNET_DM9051 is not set'
'# CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL is not set'
'# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set'
'# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set'
'# CONFIG_FREERTOS_ENABLE_TASK_SNAPSHOT is not set'
'# CONFIG_ESP_MODEM_USE_PPP_MODE is not set'
'# CONFIG_ESP_GDBSTUB_ENABLED is not set'
'# CONFIG_MDNS_ENABLE_CONSOLE_CLI is not set'
'# CONFIG_MDNS_ENABLE_BROWSE is not set'
'# CONFIG_I2S_ISR_IRAM_SAFE is not set'
'# CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH is not set'
'CONFIG_UART_ISR_IN_IRAM=y'
'CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=256'
'CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=1024'
[env:s3_16M_P]
; 16MB ESP32-S3 - with PSRAM - like a BBQKees S3
extends = espressif32_base_16M
+87 -26
View File
@@ -28,6 +28,7 @@
#include <esp_partition.h>
#include <esp_random.h>
#include <algorithm>
#include <vector>
#include "WWWData.h" // generated by `pnpm build-recovery` in ./interface
@@ -37,18 +38,18 @@
#define FACTORY_AP_CHANNEL 1
#endif
static constexpr size_t MIN_FIRMWARE_SIZE = 512 * 1024;
static constexpr uint32_t RESTART_DELAY_MS = 500;
static constexpr uint32_t ETH_WAIT_MS = 15000; // how long Ethernet gets to obtain an address before WiFi is tried
static constexpr size_t MIN_FIRMWARE_SIZE = 512 * 1024; // smallest .bin accepted by the upload, anything smaller is rejected
static constexpr uint32_t RESTART_DELAY_MS = 500; // delay before restarting, so the HTTP response is sent first
static constexpr uint32_t ETH_WAIT_MS = 15000; // how long Ethernet gets to obtain an address before WiFi is tried
static constexpr char NVS_NAMESPACE[] = "ems-esp";
static constexpr char NVS_FRESH_FW[] = "fresh_firmware"; // EMSESP_NVS_BOOT_NEW_FIRMWARE in emsesp.h
static constexpr char NVS_REASON[] = "rec_reason"; // EMSESP_NVS_RECOVERY_REASON in emsesp.h
#define LOG(...) \
do { \
Serial.print("[recovery] "); \
Serial.printf(__VA_ARGS__); \
Serial.println(); \
#define LOG(...) \
do { \
Serial.print("[recovery] "); \
Serial.printf(__VA_ARGS__); \
Serial.println(); \
} while (0)
struct AdminUser {
@@ -102,7 +103,7 @@ static std::vector<PartitionInfo> partitions;
static uint32_t restart_at = 0;
static String session_token;
static String ap_ssid;
static String recovery_reason; // set by EMS-ESP when it switched to the recovery
static String recovery_reason; // set by EMS-ESP when it switched to the recovery
static const char * eth_state = ""; // "", "disabled", "failed" or "started"
static uint32_t wifi_join_at = 0; // when to try WiFi if Ethernet has no address, 0 if not waiting
static bool wifi_started = false;
@@ -113,6 +114,8 @@ static bool ethConnected();
static int upload_error = 0;
static bool upload_done = false;
static String upload_target;
static size_t upload_size = 0; // content length, for the progress
static size_t upload_step = 0; // last progress step logged, in tens of percent
// ----------------------------------------------------------------------------
// settings (read-only)
@@ -256,6 +259,7 @@ static void loadRecoveryReason() {
if (nvs.isKey(NVS_REASON)) {
recovery_reason = nvs.getString(NVS_REASON);
nvs.remove(NVS_REASON);
LOG("EMS-ESP switched to the recovery: %s", recovery_reason.c_str());
}
nvs.end();
}
@@ -272,8 +276,8 @@ static void loadPartitions() {
const esp_partition_t * part = esp_partition_get(it);
PartitionInfo info;
info.label = part->label;
info.factory = part->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY;
info.label = part->label;
info.factory = part->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY;
info.address = part->address;
info.size = part->size;
info.image_size = 0;
@@ -293,6 +297,10 @@ static void loadPartitions() {
if (part == running) {
info.version = RECOVERY_VERSION;
// EMS-ESP stores the install date when it installs the recovery. Not set if it was flashed over serial
if (has_nvs) {
info.install_date = nvs.getULong(("d_" + info.label).c_str(), 0);
}
} else if (info.valid) {
// EMS-ESP stores the version and install date of each partition in NVS, keyed by the label
if (has_nvs && nvs.isKey(part->label)) {
@@ -305,6 +313,17 @@ static void loadPartitions() {
}
}
LOG("Partition %s: %s%s, %uKB at 0x%x%s%s%s%s",
info.label.c_str(),
info.factory ? "factory, " : "",
info.valid ? "valid" : "empty",
static_cast<unsigned>(info.size / 1024),
static_cast<unsigned>(info.address),
info.version.isEmpty() ? "" : ", v",
info.version.c_str(),
info.ota_state.isEmpty() ? "" : ", ota state ",
info.ota_state.c_str());
partitions.push_back(info);
}
esp_partition_iterator_release(it);
@@ -353,6 +372,7 @@ static const esp_partition_t * uploadTarget(AsyncWebServerRequest * request) {
static void markFreshFirmware(const String & label) {
Preferences nvs;
if (!openNvs(nvs, false)) {
LOG("Cannot open NVS, EMS-ESP won't know it's a new firmware");
return;
}
nvs.putBool(NVS_FRESH_FW, true);
@@ -381,10 +401,11 @@ static String generateToken() {
}
static bool isAuthorized(AsyncWebServerRequest * request) {
if (session_token.isEmpty() || !request->hasHeader("Authorization")) {
return false;
const bool ok = !session_token.isEmpty() && request->hasHeader("Authorization") && request->header("Authorization") == "Bearer " + session_token;
if (!ok) {
LOG("Unauthorized request to %s", request->url().c_str());
}
return request->header("Authorization") == "Bearer " + session_token;
return ok;
}
// ----------------------------------------------------------------------------
@@ -529,6 +550,7 @@ static void handleSetBoot(AsyncWebServerRequest * request, JsonVariant & json) {
const String label = json["partition"] | "";
const esp_partition_t * part = findAppPartition(label.c_str());
if (part == nullptr) {
LOG("Cannot boot from %s: no such partition", label.c_str());
request->send(404);
return;
}
@@ -569,6 +591,7 @@ static void handleFactoryReset(AsyncWebServerRequest * request) {
return;
}
LOG("Factory reset requested");
const bool ok = LittleFS.format(); // the only place the recovery writes to the filesystem
LOG("Factory reset %s", ok ? "done" : "failed");
if (!ok) {
@@ -580,12 +603,12 @@ static void handleFactoryReset(AsyncWebServerRequest * request) {
restart_at = millis() + RESTART_DELAY_MS;
}
static void uploadFail(AsyncWebServerRequest * request, int code) {
static void uploadFail(AsyncWebServerRequest * request, int code, const char * reason) {
if (upload_error) {
return;
}
upload_error = code;
LOG("Upload error %d", code);
LOG("Upload error %d: %s", code, reason);
request->send(code);
if (code == 406) {
request->client()->close();
@@ -596,10 +619,12 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
if (!index) {
upload_error = 0;
upload_done = false;
upload_size = 0;
upload_step = 0;
upload_target.clear();
if (!isAuthorized(request)) {
uploadFail(request, 403);
uploadFail(request, 403, "not signed in");
return;
}
@@ -611,41 +636,51 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
if (ota.setMd5(data, len)) {
LOG("MD5 digest received (%s)", ota.md5());
} else {
uploadFail(request, 406);
uploadFail(request, 406, "invalid MD5 digest");
}
return;
default:
// the recovery app only flashes firmware, it never touches the settings
ota.clearMd5();
LOG("Unsupported file %s (%u bytes)", filename.c_str(), static_cast<unsigned>(filesize));
uploadFail(request, 406);
uploadFail(request, 406, "unsupported file");
return;
}
if (!OtaUpdater::isCompatibleFirmware(data, len)) {
uploadFail(request, 503);
uploadFail(request, 503, "not a firmware image for this chip");
return;
}
if (OtaUpdater::isRecoveryFirmware(data, len)) {
uploadFail(request, 409);
uploadFail(request, 409, "the recovery firmware can't be installed from the recovery");
return;
}
const esp_partition_t * target = uploadTarget(request);
if (target == nullptr) {
uploadFail(request, 400);
if (request->hasParam("partition")) {
LOG("Invalid upload target %s", request->getParam("partition")->value().c_str());
}
uploadFail(request, 400, "no valid target partition");
return;
}
if (!ota.beginFirmware(filesize, target)) {
LOG("Cannot write to %s: %s", target->label, ota.errorString());
uploadFail(request, 507);
uploadFail(request, 507, "cannot start the update");
return;
}
upload_target = target->label;
LOG("Uploading %s (%uKB) to %s%s", filename.c_str(), static_cast<unsigned>(filesize / 1024), target->label, ota.md5Applied() ? ", MD5 check enabled" : "");
upload_size = filesize;
LOG("Uploading %s (%uKB) to %s (%s)%s",
filename.c_str(),
static_cast<unsigned>(filesize / 1024),
target->label,
request->hasParam("partition") ? "selected" : "default target",
ota.md5Applied() ? ", MD5 check enabled" : "");
request->onDisconnect([] {
if (!upload_done && !upload_target.isEmpty()) {
LOG("Upload aborted at %uKB, the client disconnected", static_cast<unsigned>(ota.progress() / 1024));
ota.abort();
}
});
@@ -658,17 +693,27 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
if (!ota.write(data, len)) {
LOG("Write failed at %u: %s", static_cast<unsigned>(ota.progress()), ota.errorString());
ota.abort();
uploadFail(request, 500);
uploadFail(request, 500, "write failed");
return;
}
// log the progress every 10%, the content length is a little more than the image so it may stop just short of 100%
if (upload_size > 0) {
const size_t step = std::min<size_t>(ota.progress() * 10 / upload_size, 10);
if (step > upload_step) {
upload_step = step;
LOG("Uploading to %s, %u%%", upload_target.c_str(), static_cast<unsigned>(step * 10));
}
}
if (final) {
// on success this also makes the new partition the boot partition
if (!ota.end()) {
LOG("Upload failed: %s", ota.errorString());
uploadFail(request, 500);
uploadFail(request, 500, "image check failed");
return;
}
LOG("Upload finished, the image is verified");
upload_done = true;
}
}
@@ -785,6 +830,17 @@ static bool ethConnected() {
}
static void startNetwork() {
// the reason is logged once per change, since a missing network disconnects again every few seconds
WiFi.onEvent(
[](arduino_event_id_t, arduino_event_info_t info) {
static uint8_t last_reason = 0;
if (info.wifi_sta_disconnected.reason != last_reason) {
last_reason = info.wifi_sta_disconnected.reason;
LOG("WiFi disconnected, reason %d", last_reason);
}
},
ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
WiFi.persistent(false);
WiFi.setHostname(settings.hostname.c_str()); // also the default for the Ethernet interface
WiFi.mode(WIFI_AP);
@@ -826,6 +882,9 @@ void setup() {
Serial.begin(115200);
LOG("EMS-ESP Recovery v%s", RECOVERY_VERSION);
const esp_partition_t * boot = esp_ota_get_boot_partition();
LOG("Reset reason: %s, running %s, next boot %s", resetReasonName(esp_reset_reason()), esp_ota_get_running_partition()->label, boot ? boot->label : "unknown");
loadSettings();
loadRecoveryReason();
loadPartitions();
@@ -841,6 +900,8 @@ void loop() {
was_connected = WiFi.isConnected();
if (was_connected) {
LOG("WiFi connected, IP %s", WiFi.localIP().toString().c_str());
} else {
LOG("WiFi connection lost");
}
}
+6
View File
@@ -110,6 +110,12 @@ def build_webUI(*args, **kwargs):
env.Exit(0)
# The web interface is only built by the "build" target. Skip any other, e.g. `pio run -t upload` also runs this
# environment when it's one of the default_envs, and it has nothing to upload
if "build" not in [t.strip() for t in COMMAND_LINE_TARGETS]:
print("Skipping the web interface build, the target is not 'build'")
env.Exit(0)
# Create custom target that only runs the script and then exits, without continuing with the pio workflow
env.AddCustomTarget(
name="build",
+25 -2
View File
@@ -1,4 +1,5 @@
# Builds the Recovery WebUI into recovery/WWWData.h when it's missing or older than its sources
# Builds the Recovery WebUI into recovery/WWWData.h when it's missing or older than its sources.
# In the build-recovery environment it only builds the WebUI, always.
from pathlib import Path
import os
import shutil
@@ -52,7 +53,29 @@ def build():
env.Exit(1)
if is_stale():
if env.get("PIOENV") == "build-recovery":
# the WebUI is only built by the "build" target. Skip any other, e.g. `pio run -t upload` also runs this environment
# when it's one of the default_envs, and it has nothing to upload
if "build" not in [t.strip() for t in COMMAND_LINE_TARGETS]:
print("Skipping the Recovery WebUI build, the target is not 'build'")
env.Exit(0)
# `pio run -e build-recovery` only builds the WebUI, always, and exits without building any firmware
def build_recovery_webUI(*args, **kwargs):
print("Building Recovery WebUI...")
build()
print("Recovery WebUI build completed successfully!")
env.Exit(0)
env.AddCustomTarget(
name="build",
dependencies=None,
actions=[build_recovery_webUI],
title="build recovery web interface",
description="installs pnpm packages and builds the recovery web UI into recovery/WWWData.h",
always_build=True,
)
elif is_stale():
print("Building Recovery WebUI...")
build()
else:
+27
View File
@@ -0,0 +1,27 @@
# Wrap the C/C++ compilers with ccache when it is on PATH.
# No-op locally unless ccache is installed; CI installs it via ccache-action.
Import("env")
import shutil
ccache = shutil.which("ccache")
if ccache:
def wrap(tool):
cmd = env.get(tool)
if not cmd:
return
resolved = env.subst(cmd) if isinstance(cmd, str) else cmd
if isinstance(resolved, str):
if resolved.startswith("ccache"):
return
env[tool] = f"{ccache} {resolved}"
return
if isinstance(resolved, (list, tuple)):
if resolved and "ccache" in str(resolved[0]):
return
env[tool] = [ccache, *resolved]
wrap("CC")
wrap("CXX")
print("ccache enabled")
+107 -45
View File
@@ -14,12 +14,17 @@
# and
# extra_scripts = scripts/upload.py
#
# Prefer a numeric IP over .local — mDNS (especially under WSL2) adds latency
# and can stall on IPv6 before falling back to IPv4.
import hashlib
import os
import socket
import time
from urllib.parse import urlparse
import requests
import hashlib
from urllib.parse import urlparse
import time
import os
from requests.adapters import HTTPAdapter
Import("env")
@@ -35,6 +40,10 @@ except ImportError:
from tqdm import tqdm
from termcolor import cprint
# http.client/urllib3 default is 8–16 KiB, which starves a LAN TCP window.
UPLOAD_BLOCK_SIZE = 64 * 1024
UPLOAD_SNDBUF = 256 * 1024
def print_success(x):
cprint(x, 'green')
@@ -44,6 +53,43 @@ def print_fail(x):
cprint(f'Error: {x}', 'red')
def resolve_ipv4(host):
"""Resolve hostname to IPv4 so we skip mDNS/IPv6 retries on every request."""
if host.startswith('['):
return host
if ':' in host and host.count(':') == 1:
host = host.rsplit(':', 1)[0]
try:
socket.inet_pton(socket.AF_INET, host)
return host
except OSError:
pass
infos = socket.getaddrinfo(host, 80, socket.AF_INET, socket.SOCK_STREAM)
if not infos:
raise socket.gaierror(f'No IPv4 address for {host}')
return infos[0][4][0]
class FastHTTPAdapter(HTTPAdapter):
"""Larger write chunks and socket send buffer for streaming POSTs."""
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
socket_options = [
(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),
(socket.SOL_SOCKET, socket.SO_SNDBUF, UPLOAD_SNDBUF),
]
pool_kwargs.setdefault('socket_options', socket_options)
pool_kwargs.setdefault('blocksize', UPLOAD_BLOCK_SIZE)
super().init_poolmanager(connections, maxsize, block=block, **pool_kwargs)
def make_session():
session = requests.Session()
adapter = FastHTTPAdapter(pool_connections=2, pool_maxsize=2)
session.mount('http://', adapter)
return session
def build_headers(host_ip, emsesp_url, content_type='application/json', access_token=None, extra_headers=None):
"""Build common HTTP headers with optional overrides."""
headers = {
@@ -51,18 +97,17 @@ def build_headers(host_ip, emsesp_url, content_type='application/json', access_t
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0',
'Accept': '*/*',
'Accept-Language': 'en-US',
'Accept-Encoding': 'gzip, deflate',
'Referer': emsesp_url,
'Content-Type': content_type,
'Connection': 'keep-alive'
}
if access_token:
headers['Authorization'] = f'Bearer {access_token}'
if extra_headers:
headers.update(extra_headers)
return headers
@@ -79,44 +124,52 @@ def on_upload(source, target, env):
username = env.GetProjectOption('custom_username')
password = env.GetProjectOption('custom_password')
emsesp_ip = env.GetProjectOption('custom_emsesp_ip')
except Exception as e:
except Exception:
print_fail(f'Missing settings. Add these to your pio_local.ini file:\n\ncustom_username=username\ncustom_password=password\ncustom_emsesp_ip=ems-esp.local\n')
return
emsesp_url = f"http://{emsesp_ip}"
parsed_url = urlparse(emsesp_url)
host_ip = parsed_url.netloc
signon_url = f"{emsesp_url}/rest/signIn"
signon_headers = build_headers(host_ip, emsesp_url)
username_password = {
"username": username,
"password": password
}
response = requests.post(
signon_url, json=username_password, headers=signon_headers)
if response.status_code != 200:
print_fail("Authentication with EMS-ESP failed (code " +
str(response.status_code) + ")")
parsed_url = urlparse(f"http://{emsesp_ip}")
host_name = parsed_url.hostname or emsesp_ip
try:
host_ip = resolve_ipv4(host_name)
except OSError as e:
print_fail(f"Could not resolve {host_name} to IPv4: {e}")
return
print_success("Authentication with EMS-ESP successful")
access_token = response.json().get('access_token')
if host_ip != host_name:
print_success(f"Resolved {host_name} -> {host_ip}")
# start the upload
firmware_path = str(source[0])
emsesp_url = f"http://{host_ip}"
session = make_session()
with open(firmware_path, 'rb') as firmware:
md5 = hashlib.md5(firmware.read()).hexdigest()
try:
signon_url = f"{emsesp_url}/rest/signIn"
signon_headers = build_headers(host_ip, emsesp_url)
firmware.seek(0)
response = session.post(
signon_url,
json={"username": username, "password": password},
headers=signon_headers,
timeout=15)
if response.status_code != 200:
print_fail("Authentication with EMS-ESP failed (code " +
str(response.status_code) + ")")
return
print_success("Authentication with EMS-ESP successful")
access_token = response.json().get('access_token')
firmware_path = str(source[0])
with open(firmware_path, 'rb') as firmware_file:
firmware = firmware_file.read()
md5 = hashlib.md5(firmware).hexdigest()
filename = os.path.basename(firmware_path)
encoder = MultipartEncoder(fields={
'MD5': md5,
'file': (firmware_path, firmware, 'application/octet-stream')}
'file': (filename, firmware, 'application/octet-stream')}
)
bar = tqdm(desc='Upload Progress',
@@ -124,7 +177,8 @@ def on_upload(source, target, env):
dynamic_ncols=True,
unit='B',
unit_scale=True,
unit_divisor=1024
unit_divisor=1024,
mininterval=0.2
)
monitor = MultipartEncoderMonitor(
@@ -142,27 +196,35 @@ def on_upload(source, target, env):
)
upload_url = f"{emsesp_url}/rest/uploadFile"
response = requests.post(
upload_url, data=monitor, headers=post_headers)
started = time.perf_counter()
response = session.post(
upload_url, data=monitor, headers=post_headers, timeout=None)
elapsed = time.perf_counter() - started
bar.close()
time.sleep(0.1)
print()
if response.status_code != 200:
print_fail(f"Upload failed (code {response.status_code}).")
else:
print_success("Upload successful. Rebooting device.")
kibs = (encoder.len / 1024) / elapsed if elapsed > 0 else 0
print_success(
f"Upload successful ({kibs:.0f} KiB/s in {elapsed:.1f}s). Rebooting device.")
restart_headers = build_headers(
host_ip, emsesp_url, access_token=access_token)
restart_url = f"{emsesp_url}/api/system/restart"
response = requests.get(restart_url, headers=restart_headers)
if response.status_code != 200:
print_fail(f"Restart failed (code {response.status_code})")
try:
response = session.get(
restart_url, headers=restart_headers, timeout=10)
if response.status_code != 200:
print_fail(f"Restart failed (code {response.status_code})")
except requests.RequestException:
# Device often drops the connection as it reboots — that's OK.
pass
print()
finally:
session.close()
if env.get('UPLOAD_PROTOCOL') == 'custom':
+103 -68
View File
@@ -9,21 +9,27 @@
#
# Run using for example:
# python3 upload_cli.py -i 10.10.10.175 -f ../build/firmware/EMS-ESP-3_7_0-dev_34-ESP32S3-16MB+.bin
#
# Prefer a numeric IP over .local — mDNS (especially under WSL2) adds latency
# and can stall on IPv6 before falling back to IPv4.
import argparse
import hashlib
import socket
import sys
import time
from pathlib import Path
from urllib.parse import urlparse
import requests
from requests.adapters import HTTPAdapter
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
from termcolor import cprint
from tqdm import tqdm
# Constants
USER_AGENT = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0'
CHUNK_SIZE = 8192 # 8KB chunks for MD5 calculation
UPLOAD_BLOCK_SIZE = 64 * 1024
UPLOAD_SNDBUF = 256 * 1024
def print_success(x):
@@ -34,13 +40,34 @@ def print_fail(x):
return cprint(x, 'red')
def calculate_md5(file_path):
"""Calculate MD5 hash of a file in chunks for memory efficiency."""
md5_hash = hashlib.md5()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(CHUNK_SIZE), b''):
md5_hash.update(chunk)
return md5_hash.hexdigest()
def resolve_ipv4(host):
"""Resolve hostname to IPv4 so we skip mDNS/IPv6 retries on every request."""
if host.startswith('['):
return host
if ':' in host and host.count(':') == 1:
host = host.rsplit(':', 1)[0]
try:
socket.inet_pton(socket.AF_INET, host)
return host
except OSError:
pass
infos = socket.getaddrinfo(host, 80, socket.AF_INET, socket.SOCK_STREAM)
if not infos:
raise socket.gaierror(f'No IPv4 address for {host}')
return infos[0][4][0]
class FastHTTPAdapter(HTTPAdapter):
"""Larger write chunks and socket send buffer for streaming POSTs."""
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
socket_options = [
(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),
(socket.SOL_SOCKET, socket.SO_SNDBUF, UPLOAD_SNDBUF),
]
pool_kwargs.setdefault('socket_options', socket_options)
pool_kwargs.setdefault('blocksize', UPLOAD_BLOCK_SIZE)
super().init_poolmanager(connections, maxsize, block=block, **pool_kwargs)
def create_base_headers(host_ip, emsesp_url):
@@ -50,7 +77,6 @@ def create_base_headers(host_ip, emsesp_url):
'User-Agent': USER_AGENT,
'Accept': '*/*',
'Accept-Language': 'en-US',
'Accept-Encoding': 'gzip, deflate',
'Referer': emsesp_url,
'Connection': 'keep-alive'
}
@@ -58,35 +84,39 @@ def create_base_headers(host_ip, emsesp_url):
def upload(file, ip, username, password):
"""Upload firmware to EMS-ESP device."""
# Print welcome message
print()
print("EMS-ESP Firmware Upload")
# Validate file exists
file_path = Path(file)
if not file_path.exists():
print_fail(f"File not found: {file}")
return
# Setup URLs and headers
emsesp_url = f"http://{ip}"
host_ip = ip
# Use a session for connection pooling and persistence
parsed_url = urlparse(f"http://{ip}")
host_name = parsed_url.hostname or ip
try:
host_ip = resolve_ipv4(host_name)
except OSError as e:
print_fail(f"Could not resolve {host_name} to IPv4: {e}")
return
if host_ip != host_name:
print_success(f"Resolved {host_name} -> {host_ip}")
emsesp_url = f"http://{host_ip}"
session = requests.Session()
session.mount('http://', FastHTTPAdapter(pool_connections=2, pool_maxsize=2))
try:
# Authentication
signon_url = f"{emsesp_url}/rest/signIn"
signon_headers = create_base_headers(host_ip, emsesp_url)
signon_headers['Content-Type'] = 'application/json'
username_password = {
"username": username,
"password": password
}
response = session.post(signon_url, json=username_password, headers=signon_headers)
response = session.post(
signon_url,
json={"username": username, "password": password},
headers=signon_headers,
timeout=15)
if response.status_code != 200:
print_fail(f"Authentication failed (code {response.status_code})")
@@ -95,56 +125,62 @@ def upload(file, ip, username, password):
print_success("Authentication successful")
access_token = response.json().get('access_token')
# Calculate MD5 hash
print("Calculating MD5 hash...")
md5 = calculate_md5(file_path)
firmware = file_path.read_bytes()
md5 = hashlib.md5(firmware).hexdigest()
# Start the upload
with open(file_path, 'rb') as firmware:
encoder = MultipartEncoder(fields={
'MD5': md5,
'file': (file, firmware, 'application/octet-stream')
})
encoder = MultipartEncoder(fields={
'MD5': md5,
'file': (file_path.name, firmware, 'application/octet-stream')
})
bar = tqdm(
desc='Upload Progress',
total=encoder.len,
dynamic_ncols=True,
unit='B',
unit_scale=True,
unit_divisor=1024
)
bar = tqdm(
desc='Upload Progress',
total=encoder.len,
dynamic_ncols=True,
unit='B',
unit_scale=True,
unit_divisor=1024,
mininterval=0.2
)
monitor = MultipartEncoderMonitor(
encoder, lambda monitor: bar.update(monitor.bytes_read - bar.n))
monitor = MultipartEncoderMonitor(
encoder, lambda monitor: bar.update(monitor.bytes_read - bar.n))
post_headers = create_base_headers(host_ip, emsesp_url)
post_headers.update({
'Content-Type': monitor.content_type,
'Content-Length': str(monitor.len),
'Origin': emsesp_url,
post_headers = create_base_headers(host_ip, emsesp_url)
post_headers.update({
'Content-Type': monitor.content_type,
'Content-Length': str(monitor.len),
'Origin': emsesp_url,
'Authorization': f'Bearer {access_token}'
})
upload_url = f"{emsesp_url}/rest/uploadFile"
started = time.perf_counter()
response = session.post(
upload_url, data=monitor, headers=post_headers, timeout=None)
elapsed = time.perf_counter() - started
bar.close()
if response.status_code != 200:
print_fail(f"Upload failed (code {response.status_code})")
else:
kibs = (encoder.len / 1024) / elapsed if elapsed > 0 else 0
print_success(
f"Upload successful ({kibs:.0f} KiB/s in {elapsed:.1f}s). Rebooting device.")
restart_headers = create_base_headers(host_ip, emsesp_url)
restart_headers.update({
'Content-Type': 'application/json',
'Authorization': f'Bearer {access_token}'
})
upload_url = f"{emsesp_url}/rest/uploadFile"
response = session.post(upload_url, data=monitor, headers=post_headers)
bar.close()
time.sleep(0.1)
if response.status_code != 200:
print_fail(f"Upload failed (code {response.status_code})")
else:
print_success("Upload successful. Rebooting device.")
restart_headers = create_base_headers(host_ip, emsesp_url)
restart_headers.update({
'Content-Type': 'application/json',
'Authorization': f'Bearer {access_token}'
})
restart_url = f"{emsesp_url}/api/system/restart"
response = session.get(restart_url, headers=restart_headers)
restart_url = f"{emsesp_url}/api/system/restart"
try:
response = session.get(
restart_url, headers=restart_headers, timeout=10)
if response.status_code != 200:
print_fail(f"Restart failed (code {response.status_code})")
except requests.RequestException:
pass
except requests.RequestException as e:
print_fail(f"Network error: {e}")
@@ -161,7 +197,6 @@ def upload(file, ip, username, password):
print()
# main
parser = argparse.ArgumentParser(description="EMS-ESP Firmware Upload")
parser.add_argument("-f", "--file", metavar="FILE",
required=True, type=str, help="firmware file")
+1 -1
View File
@@ -21,7 +21,7 @@
#include "helpers.h"
#include "mqtt.h"
#include "console.h"
#include "emsesp_console.h"
#include <uuid/log.h>
+1 -1
View File
@@ -21,7 +21,7 @@
#include <map>
#include "console.h"
#include "emsesp_console.h"
#include <esp32-psram.h>
using uuid::console::Shell;
+1 -1
View File
@@ -17,7 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "console.h"
#include "emsesp_console.h"
#include "console_stream.h"
#include "emsesp.h"
+1 -1
View File
@@ -20,7 +20,7 @@
#include <uuid/console.h>
#include "console.h"
#include "emsesp_console.h"
namespace emsesp {
+1 -1
View File
@@ -304,7 +304,7 @@ enum {
#define STRINGIZE(s) #s
#endif
#define ARDUINO_VERSION_STR(major, minor, patch) "Tasmota Arduino v" STRINGIZE(major) "." STRINGIZE(minor) "." STRINGIZE(patch)
#define ARDUINO_VERSION_STR(major, minor, patch) "Arduino v" STRINGIZE(major) "." STRINGIZE(minor) "." STRINGIZE(patch)
#define ARDUINO_VERSION ARDUINO_VERSION_STR(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH)
#endif
+2 -2
View File
@@ -66,10 +66,10 @@
#include "mqtt.h"
#include "modbus.h"
#include "system.h"
#include "network.h"
#include "emsesp_network.h"
#include "temperaturesensor.h"
#include "analogsensor.h"
#include "console.h"
#include "emsesp_console.h"
#include "console_stream.h"
#include "shower.h"
#include "roomcontrol.h"
+1 -1
View File
@@ -18,7 +18,7 @@
#include "emsesp.h"
#include "httpClient.h"
#include "emsesp_http_client.h"
#include <WiFiClient.h>
#include <ESP_SSLClient.h>
+25 -24
View File
@@ -246,30 +246,31 @@ const std::initializer_list<Modbus::EntityModbusInfo> Modbus::modbus_register_ma
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwHystOn), 45, 1), // hyston
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwHystOff), 46, 1), // hystoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwDisinfectionTemp), 47, 1), // disinfectiontemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCircMode), 48, 1), // circmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCirc), 49, 1), // circ
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurTemp), 50, 1), // curtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurTemp2), 51, 1), // curtemp2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurFlow), 52, 1), // curflow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStorageTemp1), 53, 1), // storagetemp1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStorageTemp2), 54, 1), // storagetemp2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(activated), 55, 1), // activated
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwOneTime), 56, 1), // onetime
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwDisinfecting), 57, 1), // disinfecting
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCharging), 58, 1), // charging
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwRecharging), 59, 1), // recharging
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwTempOK), 60, 1), // tempok
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwActive), 61, 1), // active
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(ww3wayValve), 62, 1), // 3wayvalve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwChargePump), 63, 1), // chargepump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwMixerTemp), 64, 1), // mixertemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(cylMiddleTemp), 65, 1), // cylmiddletemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwprio), 66, 1), // dhwprio
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStarts), 67, 2), // starts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwWorkM), 69, 2), // workm
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(gasMeterWw), 71, 2), // gasmeter
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgWw2), 73, 2), // nrg2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwValve), 75, 1), // dhwvalve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwDisinfectDuration), 48, 1), // disinfectduration
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCircMode), 49, 1), // circmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCirc), 50, 1), // circ
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurTemp), 51, 1), // curtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurTemp2), 52, 1), // curtemp2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurFlow), 53, 1), // curflow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStorageTemp1), 54, 1), // storagetemp1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStorageTemp2), 55, 1), // storagetemp2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(activated), 56, 1), // activated
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwOneTime), 57, 1), // onetime
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwDisinfecting), 58, 1), // disinfecting
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCharging), 59, 1), // charging
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwRecharging), 60, 1), // recharging
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwTempOK), 61, 1), // tempok
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwActive), 62, 1), // active
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(ww3wayValve), 63, 1), // 3wayvalve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwChargePump), 64, 1), // chargepump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwMixerTemp), 65, 1), // mixertemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(cylMiddleTemp), 66, 1), // cylmiddletemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwprio), 67, 1), // dhwprio
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStarts), 68, 2), // starts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwWorkM), 70, 2), // workm
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(gasMeterWw), 72, 2), // gasmeter
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgWw2), 74, 2), // nrg2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwValve), 76, 1), // dhwvalve
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(errorCode), 0, 8), // errorcode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(lastCode), 8, 25), // lastcode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(dateTime), 33, 13), // datetime
+1 -1
View File
@@ -23,7 +23,7 @@
#include "helpers.h"
#include "system.h"
#include "console.h"
#include "emsesp_console.h"
#include "command.h"
#include "emsdevicevalue.h"
#include <esp32-psram.h>
+1 -1
View File
@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "network.h"
#include "emsesp_network.h"
#include "emsesp.h"
+1 -1
View File
@@ -22,7 +22,7 @@
#include "shuntingYard.h"
#include "httpClient.h"
#include "emsesp_http_client.h"
namespace emsesp {
+92 -14
View File
@@ -17,7 +17,7 @@
*/
#include "system.h"
#include "network.h"
#include "emsesp_network.h"
#include "emsesp.h" // for send_raw_telegram() command
#ifndef EMSESP_STANDALONE
@@ -34,7 +34,7 @@
#include "firmwareVersion.h"
#include "shuntingYard.h" // for compute() used by the message and sendmail commands
#include "httpClient.h" // for the shared TLS client settings
#include "emsesp_http_client.h" // for the shared TLS client settings
#if defined(EMSESP_TEST)
#include "../test/test.h"
@@ -568,7 +568,8 @@ void System::get_partition_info() {
void System::recovery_installed() {
#ifdef EMSESP_HAS_RECOVERY
// drop the version info of an EMS-ESP firmware that was in the factory partition before
// drop the version info of an EMS-ESP firmware that was in the factory partition before. The recovery reports its
// own version, but the install date is only known here, as it's not set when flashed over serial
const esp_partition_t * factory = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
if (factory != nullptr) {
char c[20];
@@ -576,8 +577,11 @@ void System::recovery_installed() {
if (EMSESP::nvs_.isKey(factory->label)) {
EMSESP::nvs_.remove(factory->label);
}
if (EMSESP::nvs_.isKey(c)) {
EMSESP::nvs_.remove(c);
const time_t now = time(nullptr);
if (now > 1500000000L) {
EMSESP::nvs_.putULong(c, now); // UTC epoch, read by the recovery app. Only if the clock is set (NTP)
} else if (EMSESP::nvs_.isKey(c)) {
EMSESP::nvs_.remove(c); // clock not set, the date of the previous firmware is wrong
}
partition_info_.erase(factory->label); // on the AsyncTCP task, same as the web handlers reading it
}
@@ -585,6 +589,19 @@ void System::recovery_installed() {
#endif
}
// the partition info is read by the web handlers on the AsyncTCP task, so the update after a recovery firmware
// download (which runs on the main loop) is deferred to be done there, on the next systemStatus request
void System::recovery_installed_deferred() {
recovery_installed_pending_ = true;
}
void System::recovery_installed_apply() {
if (recovery_installed_pending_) {
recovery_installed_pending_ = false;
recovery_installed();
}
}
std::string System::recovery_version() const {
#ifdef EMSESP_HAS_RECOVERY
esp_app_desc_t desc;
@@ -3511,21 +3528,44 @@ bool System::uploadFirmwareURL(const char * url) {
break;
}
// the recovery firmware is recognized by its filename. It's written to the factory partition and
// EMS-ESP keeps running, there is no restart
#ifdef EMSESP_HAS_RECOVERY
String file_name = saved_url.substring(saved_url.lastIndexOf('/') + 1);
file_name.toLowerCase();
const bool is_recovery = file_name.indexOf("recovery") >= 0;
#else
const bool is_recovery = false;
#endif
OtaUpdater ota; // only used for the recovery firmware
// check we have a valid size
if (firmware_size < (int)MIN_FIRMWARE_SIZE) {
if (firmware_size < (int)(is_recovery ? OtaUpdater::MIN_RECOVERY_FIRMWARE_SIZE : MIN_FIRMWARE_SIZE)) {
LOG_ERROR("Firmware upload failed - invalid size");
return false; // error
}
OtaUpdater::confirmRunningApp();
// check we have enough space for the upload in the ota partition
if (!Update.begin(firmware_size)) {
if (is_recovery) {
#ifdef EMSESP_HAS_RECOVERY
const esp_partition_t * factory = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
if (factory == nullptr || factory == esp_ota_get_running_partition()) {
LOG_ERROR("The recovery firmware can't be installed on this board");
return false; // error
}
if (!ota.beginFirmware(firmware_size, factory, false)) {
LOG_ERROR("Recovery firmware upload failed - %s", ota.errorString());
return false; // error
}
#endif
} else if (!Update.begin(firmware_size)) {
// check we have enough space for the upload in the ota partition
LOG_ERROR("Firmware upload failed - no space");
return false; // error
}
LOG_INFO("Firmware uploading (size: %dKB) over %s. Please wait...", firmware_size / 1024, is_https ? "HTTPS" : "HTTP");
LOG_INFO("%s uploading (size: %dKB) over %s. Please wait...", is_recovery ? "Recovery firmware" : "Firmware", firmware_size / 1024, is_https ? "HTTPS" : "HTTP");
Shell::loop_all(); // flush log buffers so latest messages are shown in console
@@ -3543,7 +3583,11 @@ bool System::uploadFirmwareURL(const char * url) {
// via the systemStatus action, which runs on the AsyncTCP task while we're blocked here
if (EMSESP::system_.systemStatus() < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) {
LOG_WARNING("Firmware upload cancelled at %u of %d bytes", (unsigned)total_read, firmware_size);
Update.abort(); // release the OTA partition handle so a later attempt can start cleanly
if (is_recovery) {
ota.abort();
} else {
Update.abort(); // release the OTA partition handle so a later attempt can start cleanly
}
ssl_client.stop(); // drop the connection
saved_url.clear(); // prevent it from downloading again
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
@@ -3573,6 +3617,9 @@ bool System::uploadFirmwareURL(const char * url) {
continue;
}
LOG_ERROR("Firmware upload failed - read stalled at %u of %d bytes", (unsigned)total_read, firmware_size);
if (is_recovery) {
ota.abort();
}
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false;
}
@@ -3585,6 +3632,9 @@ bool System::uploadFirmwareURL(const char * url) {
size_t n = stream->readBytes(buf, want);
if (n == 0) {
LOG_ERROR("Firmware upload failed - read returned 0 at %u of %d bytes", (unsigned)total_read, firmware_size);
if (is_recovery) {
ota.abort();
}
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false;
}
@@ -3594,14 +3644,33 @@ bool System::uploadFirmwareURL(const char * url) {
if (!magic_ok) {
if (buf[0] != 0xE9) {
LOG_ERROR("Firmware upload failed - bad magic byte 0x%02X (expected 0xE9, not an ESP32 firmware image?)", buf[0]);
if (is_recovery) {
ota.abort();
}
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false;
}
// the recovery firmware must be for this chip, and really be the recovery
if (is_recovery && !(OtaUpdater::isCompatibleFirmware(buf, n) && OtaUpdater::isRecoveryFirmware(buf, n))) {
LOG_ERROR("Recovery firmware upload failed - not a recovery firmware image for this chip");
if (is_recovery) {
ota.abort();
}
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false;
}
magic_ok = true;
}
if (Update.write(buf, n) != n) {
LOG_ERROR("Firmware upload failed - flash write error at %u of %d bytes: %s", (unsigned)total_read, firmware_size, Update.errorString());
const bool written = is_recovery ? ota.write(buf, n) : (Update.write(buf, n) == n);
if (!written) {
LOG_ERROR("Firmware upload failed - flash write error at %u of %d bytes: %s",
(unsigned)total_read,
firmware_size,
is_recovery ? ota.errorString() : Update.errorString());
if (is_recovery) {
ota.abort();
}
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false;
}
@@ -3618,13 +3687,22 @@ bool System::uploadFirmwareURL(const char * url) {
yield();
}
if (!Update.end(true)) {
LOG_ERROR("Firmware upload failed - %s", Update.errorString());
if (!(is_recovery ? ota.end() : Update.end(true))) {
LOG_ERROR("Firmware upload failed - %s", is_recovery ? ota.errorString() : Update.errorString());
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false; // error
}
saved_url.clear(); // prevent from downloading again
if (is_recovery) {
// no restart. The partition info is updated later on the AsyncTCP task, see recovery_installed_apply()
EMSESP::system_.recovery_installed_deferred();
LOG_INFO("Recovery firmware uploaded successfully");
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
return true;
}
LOG_INFO("Firmware uploaded successfully. Restarting...");
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
#endif
+5 -1
View File
@@ -23,7 +23,7 @@
#include <ArduinoJson.h>
#include "helpers.h"
#include "console.h"
#include "emsesp_console.h"
#include "mqtt.h"
#include "telegram.h"
#include "led.h"
@@ -84,6 +84,8 @@ class System {
static void check_crash_loop(); // call at boot once NVS is open, may restart into the factory partition
void recovery_installed(); // called on the AsyncTCP task after the recovery firmware was written
void recovery_installed_deferred(); // called from the main loop after the recovery firmware was downloaded from a URL
void recovery_installed_apply(); // called on the AsyncTCP task, handles the deferred call
std::string recovery_version() const; // empty if there's no recovery firmware
// commands
@@ -391,6 +393,8 @@ class System {
void set_partition_install_date();
volatile bool recovery_installed_pending_ = false; // set by the main loop, handled on the AsyncTCP task
// firmware is confirmed to the bootloader and the crash counter cleared once it has been up this long
static constexpr uint32_t FIRMWARE_HEALTHY_UPTIME = 120; // seconds
// consecutive crashes before falling back to the recovery firmware in the factory partition
+1 -1
View File
@@ -23,7 +23,7 @@
#include "helpers.h"
#include "mqtt.h"
#include "console.h"
#include "emsesp_console.h"
#include <uuid/log.h>
+1 -1
View File
@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.9.0-dev.17"
#define EMSESP_APP_VERSION "3.9.0-dev.18"
+1 -1
View File
@@ -458,7 +458,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
// S3 has 16MB flash
// EMSESP::system_.uploadFirmwareURL("https://github.com/emsesp/EMS-ESP32/releases/download/latest/EMS-ESP-3_7_0-dev_32-ESP32S3-16MB+.bin"); // S3
// Test for 4MB Tasmota builds
// Test for 4MB builds
// EMSESP::system_.uploadFirmwareURL("https://github.com/emsesp/EMS-ESP32/releases/download/latest/EMS-ESP-3_7_2-dev_8-ESP32-4MB.bin"); // S32
EMSESP::system_.uploadFirmwareURL("https://github.com/emsesp/EMS-ESP32/releases/download/latest/EMS-ESP-3_7_2-dev_8-ESP32-16MB.bin"); // E32
+1 -1
View File
@@ -20,7 +20,7 @@
#include "WebCommandService.h"
#include "shuntingYard.h"
#include "httpClient.h"
#include "emsesp_http_client.h"
namespace emsesp {
+3 -2
View File
@@ -21,7 +21,7 @@
#ifndef EMSESP_STANDALONE
#include <esp_ota_ops.h>
#endif
#include "httpClient.h"
#include "emsesp_http_client.h"
namespace emsesp {
@@ -45,7 +45,8 @@ WebStatusService::WebStatusService(AsyncWebServer * server, SecurityManager * se
// This contains both system & hardware Status to avoid having multiple costly endpoints
// This is also used for polling during the SystemMonitor to see if EMS-ESP is alive
void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap
EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap
EMSESP::system_.recovery_installed_apply(); // update the partition info if the recovery firmware was just downloaded
auto * response = new PsramAsyncJsonResponse(false);
JsonObject root = response->getRoot();