mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-10-28 11:14:58 +00:00
Merge branch 'feat/custom-sdkconfig' into dev
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,31 +11,210 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
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:
|
jobs:
|
||||||
pre-release:
|
webui:
|
||||||
name: 'Build Dev Release'
|
name: Build webUI
|
||||||
if: github.event_name == 'workflow_dispatch' || github.repository == 'emsesp/EMS-ESP32'
|
if: github.event_name == 'workflow_dispatch' || github.repository == 'emsesp/EMS-ESP32'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Install python 3.13
|
- name: Install pnpm
|
||||||
uses: actions/setup-python@v7
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.13'
|
package_json_file: interface/package.json
|
||||||
|
|
||||||
- name: Install Node.js 26
|
- name: Install Node.js 26
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 26
|
node-version: 26
|
||||||
|
|
||||||
|
- name: Cache pnpm store
|
||||||
|
uses: actions/cache@v4
|
||||||
|
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@v4
|
||||||
|
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@v4
|
||||||
|
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@v4
|
||||||
|
with:
|
||||||
|
name: modbus-header
|
||||||
|
path: src/core
|
||||||
|
|
||||||
|
- name: Build standalone
|
||||||
|
run: platformio run -e build_standalone
|
||||||
|
|
||||||
|
- name: Upload generated docs
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
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@v4
|
||||||
|
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 intelhex
|
||||||
|
|
||||||
|
- name: Restore WWWData.h
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: wwwdata
|
||||||
|
path: src/ESP32React
|
||||||
|
|
||||||
|
- name: Restore generated modbus header
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: modbus-header
|
||||||
|
path: src/core
|
||||||
|
|
||||||
|
- name: Setup ccache
|
||||||
|
uses: hendrikmuhs/ccache-action@v1.2
|
||||||
|
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@v4
|
||||||
|
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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
run: npm install -g pnpm
|
|
||||||
|
|
||||||
- name: Get the EMS-ESP version
|
- name: Get the EMS-ESP version
|
||||||
id: build_info
|
id: build_info
|
||||||
run: |
|
run: |
|
||||||
@@ -49,27 +228,27 @@ jobs:
|
|||||||
prev=`git rev-parse -q --verify 'refs/tags/latest^{commit}' || git describe --tags --abbrev=0 --match 'v*'`
|
prev=`git rev-parse -q --verify 'refs/tags/latest^{commit}' || git describe --tags --abbrev=0 --match 'v*'`
|
||||||
echo "REF=$prev" >> $GITHUB_OUTPUT
|
echo "REF=$prev" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Install PlatformIO
|
- name: Restore generated modbus header
|
||||||
run: |
|
uses: actions/download-artifact@v4
|
||||||
python -m pip install --upgrade pip
|
with:
|
||||||
pip install -U platformio
|
name: modbus-header
|
||||||
python -m pip install intelhex
|
path: src/core
|
||||||
|
|
||||||
- name: Build webUI
|
- name: Restore generated docs
|
||||||
run: |
|
uses: actions/download-artifact@v4
|
||||||
platformio run -e build-webUI
|
with:
|
||||||
|
name: generated-docs
|
||||||
|
path: .
|
||||||
|
|
||||||
- name: Build modbus
|
- name: Restore firmware binaries
|
||||||
run: |
|
uses: actions/download-artifact@v4
|
||||||
platformio run -e build_modbus
|
with:
|
||||||
|
pattern: firmware-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: build/firmware
|
||||||
|
|
||||||
- name: Build standalone
|
- name: List firmware files
|
||||||
run: |
|
run: ls -la build/firmware
|
||||||
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
|
- name: Build the recovery firmware for the 16MB boards
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -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
|
- 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
|
- 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`
|
- 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) (`0910-1604-5.5.5`) instead of Tasmota's prebuilt cores
|
||||||
|
- optional `ccache` wrapping when it is installed (`scripts/enable_ccache.py`)
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
@@ -50,6 +53,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)
|
- 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)
|
- 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)
|
- 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
|
## Changed
|
||||||
|
|
||||||
@@ -68,3 +74,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
|
- 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
|
- 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)
|
- 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)
|
||||||
|
- CI dev-release firmware jobs run in parallel (webUI, firmware targets, standalone) instead of one serial job
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"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": {
|
"upload": {
|
||||||
"flash_size": "32MB",
|
"flash_size": "32MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"espidf"
|
||||||
],
|
],
|
||||||
"name": "Tasmota ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
|
"name": "ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
|
||||||
"upload": {
|
"upload": {
|
||||||
"flash_size": "16MB",
|
"flash_size": "16MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"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": {
|
"upload": {
|
||||||
"flash_size": "16MB",
|
"flash_size": "16MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"espidf"
|
||||||
],
|
],
|
||||||
"name": "Tasmota ESP32 4M Flash, 4608KB Code/OTA, 2MB FS",
|
"name": "ESP32 4M Flash, 4608KB Code/OTA, 2MB FS",
|
||||||
"upload": {
|
"upload": {
|
||||||
"flash_size": "4MB",
|
"flash_size": "4MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ let system_status = {
|
|||||||
cpu_cores: 2,
|
cpu_cores: 2,
|
||||||
cpu_freq_mhz: 240,
|
cpu_freq_mhz: 240,
|
||||||
max_alloc_heap: 191,
|
max_alloc_heap: 191,
|
||||||
arduino_version: 'Tasmota Arduino v3.3.8',
|
arduino_version: 'Arduino v3.3.8',
|
||||||
sdk_version: '5.5.4.260407',
|
sdk_version: '5.5.4.260407',
|
||||||
partition: 'app0',
|
partition: 'app0',
|
||||||
flash_chip_size: 16384,
|
flash_chip_size: 16384,
|
||||||
@@ -210,7 +210,7 @@ switch (emulate_esp) {
|
|||||||
case 'ESP32':
|
case 'ESP32':
|
||||||
system_status.esp_platform = 'ESP32';
|
system_status.esp_platform = 'ESP32';
|
||||||
system_status.cpu_type = 'ESP32-D0WD-V3';
|
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.sdk_version = '5.5.4.260407';
|
||||||
system_status.psram = true;
|
system_status.psram = true;
|
||||||
system_status.psram_size = 4096;
|
system_status.psram_size = 4096;
|
||||||
@@ -226,7 +226,7 @@ switch (emulate_esp) {
|
|||||||
default:
|
default:
|
||||||
system_status.esp_platform = 'ESP32S3';
|
system_status.esp_platform = 'ESP32S3';
|
||||||
system_status.cpu_type = 'ESP32-S3';
|
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.sdk_version = '5.5.4.260407';
|
||||||
system_status.psram = true;
|
system_status.psram = true;
|
||||||
system_status.psram_size = 8189;
|
system_status.psram_size = 8189;
|
||||||
|
|||||||
+41
-8
@@ -21,8 +21,8 @@ extra_configs =
|
|||||||
pio_local.ini
|
pio_local.ini
|
||||||
|
|
||||||
[common]
|
[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_build_flags = -Wall -Wextra -Wno-type-limits -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_unbuild_flags = -O2 -O3 -Og -fno-lto
|
||||||
|
|
||||||
my_build_flags =
|
my_build_flags =
|
||||||
|
|
||||||
@@ -31,6 +31,9 @@ build_flags =
|
|||||||
${factory_settings.build_flags}
|
${factory_settings.build_flags}
|
||||||
${common.my_build_flags}
|
${common.my_build_flags}
|
||||||
-I./src/core
|
-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 ARDUINO_ARCH_ESP32
|
||||||
-D ONEWIRE_CRC16=0
|
-D ONEWIRE_CRC16=0
|
||||||
-D CONFIG_UART_ISR_IN_IRAM
|
-D CONFIG_UART_ISR_IN_IRAM
|
||||||
@@ -60,15 +63,18 @@ unbuild_flags =
|
|||||||
[espressif32_base_4M]
|
[espressif32_base_4M]
|
||||||
framework = arduino
|
framework = arduino
|
||||||
board_build.partitions = partitions/esp32_partition_4M.csv
|
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
|
board_upload.flash_size = 4MB
|
||||||
; 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
|
board_build.app_partition_name = app0
|
||||||
|
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.09.50/platform-espressif32.zip ; Arduino Core 3.3.12 based on IDF 5.5.5
|
||||||
|
|
||||||
; 16MB Flash variants
|
; 16MB Flash variants
|
||||||
[espressif32_base_16M]
|
[espressif32_base_16M]
|
||||||
framework = arduino
|
framework = arduino
|
||||||
board_build.partitions = partitions/esp32_partition_16M.csv
|
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
|
board_upload.flash_size = 16MB
|
||||||
; 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
|
board_build.app_partition_name = app0
|
||||||
|
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.09.50/platform-espressif32.zip ; Arduino Core 3.3.12 based on IDF 5.5.5
|
||||||
|
platform_packages = framework-arduinoespressif32 @ https://github.com/emsesp/esp32-arduino-lib-builder/releases/download/0910-1604-5.5.5/framework-arduinoespressif32-v5.5.5-a63b839f.tar.xz
|
||||||
build_flags =
|
build_flags =
|
||||||
${common.build_flags}
|
${common.build_flags}
|
||||||
-D EMSESP_HAS_RECOVERY ; installs and falls back to the recovery firmware in the factory partition
|
-D EMSESP_HAS_RECOVERY ; installs and falls back to the recovery firmware in the factory partition
|
||||||
@@ -78,8 +84,9 @@ build_flags =
|
|||||||
[espressif32_base_32M]
|
[espressif32_base_32M]
|
||||||
framework = arduino
|
framework = arduino
|
||||||
board_build.partitions = partitions/esp32_partition_32M.csv
|
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
|
board_upload.flash_size = 32MB
|
||||||
; 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
|
board_build.app_partition_name = app0
|
||||||
|
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.09.50/platform-espressif32.zip ; Arduino Core 3.3.12 based on IDF 5.5.5
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
build_flags =
|
build_flags =
|
||||||
@@ -87,6 +94,7 @@ build_flags =
|
|||||||
build_unflags =
|
build_unflags =
|
||||||
${common.unbuild_flags}
|
${common.unbuild_flags}
|
||||||
extra_scripts =
|
extra_scripts =
|
||||||
|
pre:scripts/enable_ccache.py ; uses ccache when it is installed
|
||||||
post:scripts/rename_fw.py ; renames the firmware .bin file
|
post:scripts/rename_fw.py ; renames the firmware .bin file
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_filters = direct, esp32_exception_decoder
|
monitor_filters = direct, esp32_exception_decoder
|
||||||
@@ -129,6 +137,31 @@ board = s_16M
|
|||||||
extends = espressif32_base_16M
|
extends = espressif32_base_16M
|
||||||
board = s_16M_P
|
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
|
||||||
|
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'
|
||||||
|
|
||||||
[env:s3_16M_P]
|
[env:s3_16M_P]
|
||||||
; 16MB ESP32-S3 - with PSRAM - like a BBQKees S3
|
; 16MB ESP32-S3 - with PSRAM - like a BBQKees S3
|
||||||
extends = espressif32_base_16M
|
extends = espressif32_base_16M
|
||||||
|
|||||||
@@ -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")
|
||||||
+92
-30
@@ -14,12 +14,17 @@
|
|||||||
# and
|
# and
|
||||||
# extra_scripts = scripts/upload.py
|
# 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 requests
|
||||||
import hashlib
|
from requests.adapters import HTTPAdapter
|
||||||
from urllib.parse import urlparse
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
|
|
||||||
Import("env")
|
Import("env")
|
||||||
|
|
||||||
@@ -35,6 +40,10 @@ except ImportError:
|
|||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from termcolor import cprint
|
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):
|
def print_success(x):
|
||||||
cprint(x, 'green')
|
cprint(x, 'green')
|
||||||
@@ -44,6 +53,43 @@ def print_fail(x):
|
|||||||
cprint(f'Error: {x}', 'red')
|
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):
|
def build_headers(host_ip, emsesp_url, content_type='application/json', access_token=None, extra_headers=None):
|
||||||
"""Build common HTTP headers with optional overrides."""
|
"""Build common HTTP headers with optional overrides."""
|
||||||
headers = {
|
headers = {
|
||||||
@@ -51,7 +97,6 @@ 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',
|
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'Accept-Language': 'en-US',
|
'Accept-Language': 'en-US',
|
||||||
'Accept-Encoding': 'gzip, deflate',
|
|
||||||
'Referer': emsesp_url,
|
'Referer': emsesp_url,
|
||||||
'Content-Type': content_type,
|
'Content-Type': content_type,
|
||||||
'Connection': 'keep-alive'
|
'Connection': 'keep-alive'
|
||||||
@@ -79,24 +124,33 @@ def on_upload(source, target, env):
|
|||||||
username = env.GetProjectOption('custom_username')
|
username = env.GetProjectOption('custom_username')
|
||||||
password = env.GetProjectOption('custom_password')
|
password = env.GetProjectOption('custom_password')
|
||||||
emsesp_ip = env.GetProjectOption('custom_emsesp_ip')
|
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')
|
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
|
return
|
||||||
|
|
||||||
emsesp_url = f"http://{emsesp_ip}"
|
parsed_url = urlparse(f"http://{emsesp_ip}")
|
||||||
parsed_url = urlparse(emsesp_url)
|
host_name = parsed_url.hostname or emsesp_ip
|
||||||
host_ip = parsed_url.netloc
|
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 = make_session()
|
||||||
|
|
||||||
|
try:
|
||||||
signon_url = f"{emsesp_url}/rest/signIn"
|
signon_url = f"{emsesp_url}/rest/signIn"
|
||||||
signon_headers = build_headers(host_ip, emsesp_url)
|
signon_headers = build_headers(host_ip, emsesp_url)
|
||||||
|
|
||||||
username_password = {
|
response = session.post(
|
||||||
"username": username,
|
signon_url,
|
||||||
"password": password
|
json={"username": username, "password": password},
|
||||||
}
|
headers=signon_headers,
|
||||||
|
timeout=15)
|
||||||
response = requests.post(
|
|
||||||
signon_url, json=username_password, headers=signon_headers)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print_fail("Authentication with EMS-ESP failed (code " +
|
print_fail("Authentication with EMS-ESP failed (code " +
|
||||||
@@ -106,17 +160,16 @@ def on_upload(source, target, env):
|
|||||||
print_success("Authentication with EMS-ESP successful")
|
print_success("Authentication with EMS-ESP successful")
|
||||||
access_token = response.json().get('access_token')
|
access_token = response.json().get('access_token')
|
||||||
|
|
||||||
# start the upload
|
|
||||||
firmware_path = str(source[0])
|
firmware_path = str(source[0])
|
||||||
|
with open(firmware_path, 'rb') as firmware_file:
|
||||||
|
firmware = firmware_file.read()
|
||||||
|
|
||||||
with open(firmware_path, 'rb') as firmware:
|
md5 = hashlib.md5(firmware).hexdigest()
|
||||||
md5 = hashlib.md5(firmware.read()).hexdigest()
|
filename = os.path.basename(firmware_path)
|
||||||
|
|
||||||
firmware.seek(0)
|
|
||||||
|
|
||||||
encoder = MultipartEncoder(fields={
|
encoder = MultipartEncoder(fields={
|
||||||
'MD5': md5,
|
'MD5': md5,
|
||||||
'file': (firmware_path, firmware, 'application/octet-stream')}
|
'file': (filename, firmware, 'application/octet-stream')}
|
||||||
)
|
)
|
||||||
|
|
||||||
bar = tqdm(desc='Upload Progress',
|
bar = tqdm(desc='Upload Progress',
|
||||||
@@ -124,7 +177,8 @@ def on_upload(source, target, env):
|
|||||||
dynamic_ncols=True,
|
dynamic_ncols=True,
|
||||||
unit='B',
|
unit='B',
|
||||||
unit_scale=True,
|
unit_scale=True,
|
||||||
unit_divisor=1024
|
unit_divisor=1024,
|
||||||
|
mininterval=0.2
|
||||||
)
|
)
|
||||||
|
|
||||||
monitor = MultipartEncoderMonitor(
|
monitor = MultipartEncoderMonitor(
|
||||||
@@ -142,27 +196,35 @@ def on_upload(source, target, env):
|
|||||||
)
|
)
|
||||||
|
|
||||||
upload_url = f"{emsesp_url}/rest/uploadFile"
|
upload_url = f"{emsesp_url}/rest/uploadFile"
|
||||||
|
started = time.perf_counter()
|
||||||
response = requests.post(
|
response = session.post(
|
||||||
upload_url, data=monitor, headers=post_headers)
|
upload_url, data=monitor, headers=post_headers, timeout=None)
|
||||||
|
elapsed = time.perf_counter() - started
|
||||||
|
|
||||||
bar.close()
|
bar.close()
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print_fail(f"Upload failed (code {response.status_code}).")
|
print_fail(f"Upload failed (code {response.status_code}).")
|
||||||
else:
|
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(
|
restart_headers = build_headers(
|
||||||
host_ip, emsesp_url, access_token=access_token)
|
host_ip, emsesp_url, access_token=access_token)
|
||||||
restart_url = f"{emsesp_url}/api/system/restart"
|
restart_url = f"{emsesp_url}/api/system/restart"
|
||||||
response = requests.get(restart_url, headers=restart_headers)
|
try:
|
||||||
|
response = session.get(
|
||||||
|
restart_url, headers=restart_headers, timeout=10)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print_fail(f"Restart failed (code {response.status_code})")
|
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()
|
print()
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
if env.get('UPLOAD_PROTOCOL') == 'custom':
|
if env.get('UPLOAD_PROTOCOL') == 'custom':
|
||||||
|
|||||||
+70
-35
@@ -9,21 +9,27 @@
|
|||||||
#
|
#
|
||||||
# Run using for example:
|
# 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
|
# 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 argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
||||||
from termcolor import cprint
|
from termcolor import cprint
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
# Constants
|
|
||||||
USER_AGENT = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0'
|
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):
|
def print_success(x):
|
||||||
@@ -34,13 +40,34 @@ def print_fail(x):
|
|||||||
return cprint(x, 'red')
|
return cprint(x, 'red')
|
||||||
|
|
||||||
|
|
||||||
def calculate_md5(file_path):
|
def resolve_ipv4(host):
|
||||||
"""Calculate MD5 hash of a file in chunks for memory efficiency."""
|
"""Resolve hostname to IPv4 so we skip mDNS/IPv6 retries on every request."""
|
||||||
md5_hash = hashlib.md5()
|
if host.startswith('['):
|
||||||
with open(file_path, 'rb') as f:
|
return host
|
||||||
for chunk in iter(lambda: f.read(CHUNK_SIZE), b''):
|
if ':' in host and host.count(':') == 1:
|
||||||
md5_hash.update(chunk)
|
host = host.rsplit(':', 1)[0]
|
||||||
return md5_hash.hexdigest()
|
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):
|
def create_base_headers(host_ip, emsesp_url):
|
||||||
@@ -50,7 +77,6 @@ def create_base_headers(host_ip, emsesp_url):
|
|||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'Accept-Language': 'en-US',
|
'Accept-Language': 'en-US',
|
||||||
'Accept-Encoding': 'gzip, deflate',
|
|
||||||
'Referer': emsesp_url,
|
'Referer': emsesp_url,
|
||||||
'Connection': 'keep-alive'
|
'Connection': 'keep-alive'
|
||||||
}
|
}
|
||||||
@@ -58,35 +84,39 @@ def create_base_headers(host_ip, emsesp_url):
|
|||||||
|
|
||||||
def upload(file, ip, username, password):
|
def upload(file, ip, username, password):
|
||||||
"""Upload firmware to EMS-ESP device."""
|
"""Upload firmware to EMS-ESP device."""
|
||||||
# Print welcome message
|
|
||||||
print()
|
print()
|
||||||
print("EMS-ESP Firmware Upload")
|
print("EMS-ESP Firmware Upload")
|
||||||
|
|
||||||
# Validate file exists
|
|
||||||
file_path = Path(file)
|
file_path = Path(file)
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
print_fail(f"File not found: {file}")
|
print_fail(f"File not found: {file}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Setup URLs and headers
|
parsed_url = urlparse(f"http://{ip}")
|
||||||
emsesp_url = f"http://{ip}"
|
host_name = parsed_url.hostname or ip
|
||||||
host_ip = ip
|
try:
|
||||||
|
host_ip = resolve_ipv4(host_name)
|
||||||
|
except OSError as e:
|
||||||
|
print_fail(f"Could not resolve {host_name} to IPv4: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
# Use a session for connection pooling and persistence
|
if host_ip != host_name:
|
||||||
|
print_success(f"Resolved {host_name} -> {host_ip}")
|
||||||
|
|
||||||
|
emsesp_url = f"http://{host_ip}"
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
|
session.mount('http://', FastHTTPAdapter(pool_connections=2, pool_maxsize=2))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Authentication
|
|
||||||
signon_url = f"{emsesp_url}/rest/signIn"
|
signon_url = f"{emsesp_url}/rest/signIn"
|
||||||
signon_headers = create_base_headers(host_ip, emsesp_url)
|
signon_headers = create_base_headers(host_ip, emsesp_url)
|
||||||
signon_headers['Content-Type'] = 'application/json'
|
signon_headers['Content-Type'] = 'application/json'
|
||||||
|
|
||||||
username_password = {
|
response = session.post(
|
||||||
"username": username,
|
signon_url,
|
||||||
"password": password
|
json={"username": username, "password": password},
|
||||||
}
|
headers=signon_headers,
|
||||||
|
timeout=15)
|
||||||
response = session.post(signon_url, json=username_password, headers=signon_headers)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print_fail(f"Authentication failed (code {response.status_code})")
|
print_fail(f"Authentication failed (code {response.status_code})")
|
||||||
@@ -95,15 +125,12 @@ def upload(file, ip, username, password):
|
|||||||
print_success("Authentication successful")
|
print_success("Authentication successful")
|
||||||
access_token = response.json().get('access_token')
|
access_token = response.json().get('access_token')
|
||||||
|
|
||||||
# Calculate MD5 hash
|
firmware = file_path.read_bytes()
|
||||||
print("Calculating MD5 hash...")
|
md5 = hashlib.md5(firmware).hexdigest()
|
||||||
md5 = calculate_md5(file_path)
|
|
||||||
|
|
||||||
# Start the upload
|
|
||||||
with open(file_path, 'rb') as firmware:
|
|
||||||
encoder = MultipartEncoder(fields={
|
encoder = MultipartEncoder(fields={
|
||||||
'MD5': md5,
|
'MD5': md5,
|
||||||
'file': (file, firmware, 'application/octet-stream')
|
'file': (file_path.name, firmware, 'application/octet-stream')
|
||||||
})
|
})
|
||||||
|
|
||||||
bar = tqdm(
|
bar = tqdm(
|
||||||
@@ -112,7 +139,8 @@ def upload(file, ip, username, password):
|
|||||||
dynamic_ncols=True,
|
dynamic_ncols=True,
|
||||||
unit='B',
|
unit='B',
|
||||||
unit_scale=True,
|
unit_scale=True,
|
||||||
unit_divisor=1024
|
unit_divisor=1024,
|
||||||
|
mininterval=0.2
|
||||||
)
|
)
|
||||||
|
|
||||||
monitor = MultipartEncoderMonitor(
|
monitor = MultipartEncoderMonitor(
|
||||||
@@ -127,24 +155,32 @@ def upload(file, ip, username, password):
|
|||||||
})
|
})
|
||||||
|
|
||||||
upload_url = f"{emsesp_url}/rest/uploadFile"
|
upload_url = f"{emsesp_url}/rest/uploadFile"
|
||||||
response = session.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()
|
bar.close()
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print_fail(f"Upload failed (code {response.status_code})")
|
print_fail(f"Upload failed (code {response.status_code})")
|
||||||
else:
|
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 = create_base_headers(host_ip, emsesp_url)
|
restart_headers = create_base_headers(host_ip, emsesp_url)
|
||||||
restart_headers.update({
|
restart_headers.update({
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': f'Bearer {access_token}'
|
'Authorization': f'Bearer {access_token}'
|
||||||
})
|
})
|
||||||
restart_url = f"{emsesp_url}/api/system/restart"
|
restart_url = f"{emsesp_url}/api/system/restart"
|
||||||
response = session.get(restart_url, headers=restart_headers)
|
try:
|
||||||
|
response = session.get(
|
||||||
|
restart_url, headers=restart_headers, timeout=10)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
print_fail(f"Restart failed (code {response.status_code})")
|
print_fail(f"Restart failed (code {response.status_code})")
|
||||||
|
except requests.RequestException:
|
||||||
|
pass
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
print_fail(f"Network error: {e}")
|
print_fail(f"Network error: {e}")
|
||||||
@@ -161,7 +197,6 @@ def upload(file, ip, username, password):
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
# main
|
|
||||||
parser = argparse.ArgumentParser(description="EMS-ESP Firmware Upload")
|
parser = argparse.ArgumentParser(description="EMS-ESP Firmware Upload")
|
||||||
parser.add_argument("-f", "--file", metavar="FILE",
|
parser.add_argument("-f", "--file", metavar="FILE",
|
||||||
required=True, type=str, help="firmware file")
|
required=True, type=str, help="firmware file")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
#include "helpers.h"
|
#include "helpers.h"
|
||||||
#include "mqtt.h"
|
#include "mqtt.h"
|
||||||
#include "console.h"
|
#include "emsesp_console.h"
|
||||||
|
|
||||||
#include <uuid/log.h>
|
#include <uuid/log.h>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#include "console.h"
|
#include "emsesp_console.h"
|
||||||
#include <esp32-psram.h>
|
#include <esp32-psram.h>
|
||||||
|
|
||||||
using uuid::console::Shell;
|
using uuid::console::Shell;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "console.h"
|
#include "emsesp_console.h"
|
||||||
#include "console_stream.h"
|
#include "console_stream.h"
|
||||||
#include "emsesp.h"
|
#include "emsesp.h"
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
#include <uuid/console.h>
|
#include <uuid/console.h>
|
||||||
|
|
||||||
#include "console.h"
|
#include "emsesp_console.h"
|
||||||
|
|
||||||
namespace emsesp {
|
namespace emsesp {
|
||||||
|
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ enum {
|
|||||||
#define STRINGIZE(s) #s
|
#define STRINGIZE(s) #s
|
||||||
#endif
|
#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)
|
#define ARDUINO_VERSION ARDUINO_VERSION_STR(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+2
-2
@@ -66,10 +66,10 @@
|
|||||||
#include "mqtt.h"
|
#include "mqtt.h"
|
||||||
#include "modbus.h"
|
#include "modbus.h"
|
||||||
#include "system.h"
|
#include "system.h"
|
||||||
#include "network.h"
|
#include "emsesp_network.h"
|
||||||
#include "temperaturesensor.h"
|
#include "temperaturesensor.h"
|
||||||
#include "analogsensor.h"
|
#include "analogsensor.h"
|
||||||
#include "console.h"
|
#include "emsesp_console.h"
|
||||||
#include "console_stream.h"
|
#include "console_stream.h"
|
||||||
#include "shower.h"
|
#include "shower.h"
|
||||||
#include "roomcontrol.h"
|
#include "roomcontrol.h"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
#include "emsesp.h"
|
#include "emsesp.h"
|
||||||
|
|
||||||
#include "httpClient.h"
|
#include "emsesp_http_client.h"
|
||||||
|
|
||||||
#include <WiFiClient.h>
|
#include <WiFiClient.h>
|
||||||
#include <ESP_SSLClient.h>
|
#include <ESP_SSLClient.h>
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
#include "helpers.h"
|
#include "helpers.h"
|
||||||
#include "system.h"
|
#include "system.h"
|
||||||
#include "console.h"
|
#include "emsesp_console.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "emsdevicevalue.h"
|
#include "emsdevicevalue.h"
|
||||||
#include <esp32-psram.h>
|
#include <esp32-psram.h>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "network.h"
|
#include "emsesp_network.h"
|
||||||
|
|
||||||
#include "emsesp.h"
|
#include "emsesp.h"
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
#include "shuntingYard.h"
|
#include "shuntingYard.h"
|
||||||
|
|
||||||
#include "httpClient.h"
|
#include "emsesp_http_client.h"
|
||||||
|
|
||||||
namespace emsesp {
|
namespace emsesp {
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "system.h"
|
#include "system.h"
|
||||||
#include "network.h"
|
#include "emsesp_network.h"
|
||||||
#include "emsesp.h" // for send_raw_telegram() command
|
#include "emsesp.h" // for send_raw_telegram() command
|
||||||
|
|
||||||
#ifndef EMSESP_STANDALONE
|
#ifndef EMSESP_STANDALONE
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
#include "firmwareVersion.h"
|
#include "firmwareVersion.h"
|
||||||
#include "shuntingYard.h" // for compute() used by the message and sendmail commands
|
#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)
|
#if defined(EMSESP_TEST)
|
||||||
#include "../test/test.h"
|
#include "../test/test.h"
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
#include "helpers.h"
|
#include "helpers.h"
|
||||||
#include "console.h"
|
#include "emsesp_console.h"
|
||||||
#include "mqtt.h"
|
#include "mqtt.h"
|
||||||
#include "telegram.h"
|
#include "telegram.h"
|
||||||
#include "led.h"
|
#include "led.h"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
#include "helpers.h"
|
#include "helpers.h"
|
||||||
#include "mqtt.h"
|
#include "mqtt.h"
|
||||||
#include "console.h"
|
#include "emsesp_console.h"
|
||||||
|
|
||||||
#include <uuid/log.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
|
// 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
|
// 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-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
|
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 "WebCommandService.h"
|
||||||
|
|
||||||
#include "shuntingYard.h"
|
#include "shuntingYard.h"
|
||||||
#include "httpClient.h"
|
#include "emsesp_http_client.h"
|
||||||
|
|
||||||
namespace emsesp {
|
namespace emsesp {
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
#ifndef EMSESP_STANDALONE
|
#ifndef EMSESP_STANDALONE
|
||||||
#include <esp_ota_ops.h>
|
#include <esp_ota_ops.h>
|
||||||
#endif
|
#endif
|
||||||
#include "httpClient.h"
|
#include "emsesp_http_client.h"
|
||||||
|
|
||||||
namespace emsesp {
|
namespace emsesp {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user