mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-10-28 11:14:58 +00:00
Compare commits
27
Commits
c6157dee22
...
de9a082668
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de9a082668 | ||
|
|
5ae89714c2 | ||
|
|
84060449cd | ||
|
|
f9c9496c71 | ||
|
|
546e07a534 | ||
|
|
956f7ab098 | ||
|
|
9934d05c5a | ||
|
|
33a38c30fe | ||
|
|
43778328e1 | ||
|
|
e7fd8bcdf0 | ||
|
|
229187d912 | ||
|
|
fe4fe7ef28 | ||
|
|
0094df41d9 | ||
|
|
ec49237bd9 | ||
|
|
b730716791 | ||
|
|
62f2987b56 | ||
|
|
e10983cca1 | ||
|
|
04d373b93c | ||
|
|
c959cb1e8a | ||
|
|
09aa40e604 | ||
|
|
e3b1eb6c51 | ||
|
|
673c7462f5 | ||
|
|
eaaea504df | ||
|
|
cefd5a14cf | ||
|
|
46f2090b30 | ||
|
|
07514260f4 | ||
|
|
dfe009f5c3 |
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -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: |
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/*.*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+94
-94
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+69
-69
@@ -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: {}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#include "helpers.h"
|
||||
#include "mqtt.h"
|
||||
#include "console.h"
|
||||
#include "emsesp_console.h"
|
||||
|
||||
#include <uuid/log.h>
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "console.h"
|
||||
#include "emsesp_console.h"
|
||||
#include <esp32-psram.h>
|
||||
|
||||
using uuid::console::Shell;
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
#include <uuid/console.h>
|
||||
|
||||
#include "console.h"
|
||||
#include "emsesp_console.h"
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
#include "emsesp.h"
|
||||
|
||||
#include "httpClient.h"
|
||||
#include "emsesp_http_client.h"
|
||||
|
||||
#include <WiFiClient.h>
|
||||
#include <ESP_SSLClient.h>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
#include "shuntingYard.h"
|
||||
|
||||
#include "httpClient.h"
|
||||
#include "emsesp_http_client.h"
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
|
||||
+92
-14
@@ -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
@@ -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
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
#include "helpers.h"
|
||||
#include "mqtt.h"
|
||||
#include "console.h"
|
||||
#include "emsesp_console.h"
|
||||
|
||||
#include <uuid/log.h>
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define EMSESP_APP_VERSION "3.9.0-dev.17"
|
||||
#define EMSESP_APP_VERSION "3.9.0-dev.18"
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "WebCommandService.h"
|
||||
|
||||
#include "shuntingYard.h"
|
||||
#include "httpClient.h"
|
||||
#include "emsesp_http_client.h"
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user