mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-10-28 11:14:58 +00:00
crashmode
This commit is contained in:
+14
-15
@@ -8,24 +8,23 @@ This guide covers testing on hardware of:
|
|||||||
- the fallback to the recovery after repeated crashes;
|
- the fallback to the recovery after repeated crashes;
|
||||||
- Ethernet and static IP support in the recovery.
|
- Ethernet and static IP support in the recovery.
|
||||||
|
|
||||||
It's written for an ESP32-S3 16MB board, since the crash test build is only defined for `s3_16M_P`. Ethernet testing needs an E32V2. Keep a USB serial cable connected with a serial monitor open throughout, as most of the checks are log lines.
|
It's written for an ESP32 16MB board with PSRAM (`s_16M_P`, e.g. a BBQKees E32V2). Other 16MB boards (`s_16M`, `s3_16M_P`) follow the same steps with their matching PlatformIO envs; Ethernet is E32V2 only. Keep a USB serial cable connected with a serial monitor open throughout, as most of the checks are log lines.
|
||||||
|
|
||||||
## 0. Preparation
|
## 0. Preparation
|
||||||
|
|
||||||
Build everything:
|
Build everything:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pio run -e s3_16M_P -e crashtest_s3_16M_P -e recovery_s3_16M_P
|
pio run -e s_16M_P -e crashtest_s_16M_P -e recovery_s_16M_P
|
||||||
pio run -e s_16M -e s_16M_P -e s_4M -e recovery_s_16M -e recovery_s_16M_P
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The firmware files are written to `build/firmware/`:
|
The firmware files are written to `build/firmware/`:
|
||||||
|
|
||||||
| File | What it is |
|
| File | What it is |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `EMS-ESP-<version>-ESP32S3-16MB+.bin` | EMS-ESP |
|
| `EMS-ESP-<version>-ESP32-16MB+.bin` | EMS-ESP |
|
||||||
| `EMS-ESP-CrashTest-<version>-ESP32S3-16MB+.bin` | crash test build |
|
| `EMS-ESP-CrashTest-<version>-ESP32-16MB+.bin` | crash test build |
|
||||||
| `EMS-ESP-Recovery-<version>-ESP32S3-16MB+.bin` | the recovery |
|
| `EMS-ESP-Recovery-<version>-ESP32-16MB+.bin` | the recovery |
|
||||||
|
|
||||||
Each has a matching `.md5` file.
|
Each has a matching `.md5` file.
|
||||||
|
|
||||||
@@ -33,13 +32,13 @@ Then:
|
|||||||
|
|
||||||
1. Run the unit tests with `pio run -e native-test -t exec`. All tests should pass.
|
1. Run the unit tests with `pio run -e native-test -t exec`. All tests should pass.
|
||||||
2. Optionally, check the recovery UI against mock data first: `cd interface && pnpm recovery`, then open <http://localhost:3001>.
|
2. Optionally, check the recovery UI against mock data first: `cd interface && pnpm recovery`, then open <http://localhost:3001>.
|
||||||
3. Open the serial monitor with `pio device monitor -e s3_16M_P`. Recovery log lines start with `[recovery]`.
|
3. Open the serial monitor with `pio device monitor -e s_16M_P`. Recovery log lines start with `[recovery]`.
|
||||||
|
|
||||||
The crash counter is kept in RTC memory. It survives crashes and software restarts but not a power cycle, so don't unplug the board during the crash tests.
|
The crash counter is kept in RTC memory. It survives crashes and software restarts but not a power cycle, so don't unplug the board during the crash tests.
|
||||||
|
|
||||||
## 1. Baseline: EMS-ESP over serial, no recovery
|
## 1. Baseline: EMS-ESP over serial, no recovery
|
||||||
|
|
||||||
1. Flash with `pio run -e s3_16M_P -t upload`. This also writes the current bootloader, which has rollback enabled. Boards flashed with an older bootloader don't roll back until they've been flashed over serial once.
|
1. Flash with `pio run -e s_16M_P -t upload`. This also writes the current bootloader, which has rollback enabled. Boards flashed with an older bootloader don't roll back until they've been flashed over serial once.
|
||||||
2. In the WebUI, open **Settings → Version**. Check that:
|
2. In the WebUI, open **Settings → Version**. Check that:
|
||||||
- the **Recovery** row shows "not installed";
|
- the **Recovery** row shows "not installed";
|
||||||
- the firmware partition list doesn't include `boot`.
|
- the firmware partition list doesn't include `boot`.
|
||||||
@@ -54,7 +53,7 @@ The crash counter is kept in RTC memory. It survives crashes and software restar
|
|||||||
- the install logged.
|
- the install logged.
|
||||||
2. Repeat, uploading the `.md5` first and then the `.bin`. The MD5 check should pass.
|
2. Repeat, uploading the `.md5` first and then the `.bin`. The MD5 check should pass.
|
||||||
3. Negative tests:
|
3. Negative tests:
|
||||||
- Upload the recovery built for another chip, e.g. the ESP32 file on an S3. It should be rejected, and nothing should be written.
|
- Upload the recovery built for another chip, e.g. the ESP32-S3 file on this ESP32. It should be rejected, and nothing should be written.
|
||||||
- Start a recovery upload and close the browser tab halfway through. EMS-ESP should keep running and the Recovery row should show "not installed". Reinstall it properly afterwards.
|
- Start a recovery upload and close the browser tab halfway through. EMS-ESP should keep running and the Recovery row should show "not installed". Reinstall it properly afterwards.
|
||||||
4. Restart EMS-ESP normally. It should boot EMS-ESP again, not the recovery, because installing the recovery doesn't change the boot partition.
|
4. Restart EMS-ESP normally. It should boot EMS-ESP again, not the recovery, because installing the recovery doesn't change the boot partition.
|
||||||
|
|
||||||
@@ -67,14 +66,14 @@ The crash counter is kept in RTC memory. It survives crashes and software restar
|
|||||||
- `[recovery] EMS-ESP Recovery v…` is logged;
|
- `[recovery] EMS-ESP Recovery v…` is logged;
|
||||||
- LittleFS is mounted;
|
- LittleFS is mounted;
|
||||||
- the access point has started;
|
- the access point has started;
|
||||||
- WiFi connects.
|
- Ethernet or WiFi connects.
|
||||||
3. Join the access point. Its name is the AP SSID plus `-recovery` (default `ems-esp-recovery`), and it uses the AP password (default `ems-esp-neo`). Open <http://192.168.4.1>; a captive portal page may open by itself. Alternatively, use the device's normal IP or `http://<hostname>.local` from the LAN.
|
3. Join the access point. Its name is the AP SSID plus `-recovery` (default `ems-esp-recovery`), and it uses the AP password (default `ems-esp-neo`). Open <http://192.168.4.1>; a captive portal page may open by itself. Alternatively, use the device's normal IP or `http://<hostname>.local` from the LAN.
|
||||||
4. Sign in with an EMS-ESP admin account (default `admin`/`admin`). A non-admin user must be refused.
|
4. Sign in with an EMS-ESP admin account (default `admin`/`admin`). A non-admin user must be refused.
|
||||||
5. On the dashboard, check that:
|
5. On the dashboard, check that:
|
||||||
- there's **no** red reason box, because the restart was requested;
|
- there's **no** red reason box, because the restart was requested;
|
||||||
- `Recovery (boot)` has the "running" chip;
|
- `Recovery (boot)` has the "running" chip;
|
||||||
- the installed firmware shows the correct version;
|
- the installed firmware shows the correct version;
|
||||||
- the System section shows the chip, flash, MAC, access point, and WiFi with its IP.
|
- the System section shows the chip, flash, MAC, access point, and the active network (Ethernet or WiFi) with its IP.
|
||||||
6. Click **Restart**. It should come back into the recovery, still without a reason box, since the reason is only shown once.
|
6. Click **Restart**. It should come back into the recovery, still without a reason box, since the reason is only shown once.
|
||||||
7. Click **Start** on the EMS-ESP slot. EMS-ESP should boot, and the Version page should still show the recovery version.
|
7. Click **Start** on the EMS-ESP slot. EMS-ESP should boot, and the Version page should still show the recovery version.
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@ If step 3 ends in a crash loop instead of a rollback, the bootloader on the boar
|
|||||||
|
|
||||||
## 6. Crash-loop fallback to the recovery (crash test mode 2)
|
## 6. Crash-loop fallback to the recovery (crash test mode 2)
|
||||||
|
|
||||||
1. In `platformio.ini`, change `-D EMSESP_CRASH_TEST=1` to `=2`, then run `pio run -e crashtest_s3_16M_P`.
|
1. In `platformio.ini`, change `-D EMSESP_CRASH_TEST=1` to `=2`, then run `pio run -e crashtest_s_16M_P`.
|
||||||
2. Upload the crash test build from the WebUI. It runs normally.
|
2. Upload the crash test build from the WebUI. It runs normally.
|
||||||
3. Wait for "New firmware confirmed as working", about 2 minutes after boot.
|
3. Wait for "New firmware confirmed as working", about 2 minutes after boot.
|
||||||
4. Restart it from the WebUI. It should crash 5 times in a row, logging the "N in a row" count each time.
|
4. Restart it from the WebUI. It should crash 5 times in a row, logging the "N in a row" count each time.
|
||||||
@@ -128,7 +127,7 @@ If step 3 ends in a crash loop instead of a rollback, the bootloader on the boar
|
|||||||
|
|
||||||
Optional, without a recovery installed:
|
Optional, without a recovery installed:
|
||||||
|
|
||||||
1. Erase the boot partition with `esptool --chip esp32s3 erase-region 0x10000 0x480000`. Older esptool versions use `erase_region`.
|
1. Erase the boot partition with `esptool --chip esp32 erase-region 0x10000 0x480000`. Older esptool versions use `erase_region`.
|
||||||
2. Repeat steps 2 to 4. It should keep crashing past 5, and needs to be reflashed over serial.
|
2. Repeat steps 2 to 4. It should keep crashing past 5, and needs to be reflashed over serial.
|
||||||
|
|
||||||
## 7. Network in the recovery
|
## 7. Network in the recovery
|
||||||
@@ -136,7 +135,7 @@ Optional, without a recovery installed:
|
|||||||
| Test | Setup | Expected in the recovery |
|
| Test | Setup | Expected in the recovery |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| WiFi with static IP | Enable a static IP in the EMS-ESP Network settings, then `restart boot` | Same IP as EMS-ESP, and the WiFi row ends with ", static IP" |
|
| WiFi with static IP | Enable a static IP in the EMS-ESP Network settings, then `restart boot` | Same IP as EMS-ESP, and the WiFi row ends with ", static IP" |
|
||||||
| Ethernet (E32V2) | Cable plugged in, `recovery_s_16M_P` installed, then `restart boot` | Log shows "Ethernet started" and "Ethernet connected, IP …". The Ethernet row shows "connected", and WiFi shows "not used, Ethernet is connected" |
|
| Ethernet | Cable plugged in, then `restart boot` | Log shows "Ethernet started" and "Ethernet connected, IP …". The Ethernet row shows "connected", and WiFi shows "not used, Ethernet is connected" |
|
||||||
| Ethernet with static IP | As above, with a static IP set | Ethernet uses the static IP and WiFi isn't started |
|
| Ethernet with static IP | As above, with a static IP set | Ethernet uses the static IP and WiFi isn't started |
|
||||||
| No cable | Unplugged, then `restart boot` | Ethernet shows "no cable connected" and WiFi shows "waiting for Ethernet". After about 15 seconds the log shows "No Ethernet connection" and WiFi connects |
|
| No cable | Unplugged, then `restart boot` | Ethernet shows "no cable connected" and WiFi shows "waiting for Ethernet". After about 15 seconds the log shows "No Ethernet connection" and WiFi connects |
|
||||||
| Cable plugged in later | Plug in after WiFi has joined | Ethernet gets an IP as well, and WiFi stays connected |
|
| Cable plugged in later | Plug in after WiFi has joined | Ethernet gets an IP as well, and WiFi stays connected |
|
||||||
@@ -153,4 +152,4 @@ Optional, without a recovery installed:
|
|||||||
|
|
||||||
## Reset to a clean state
|
## Reset to a clean state
|
||||||
|
|
||||||
`pio run -e s3_16M_P -t upload` restores EMS-ESP at any point. It leaves the recovery in `boot` and the settings untouched.
|
`pio run -e s_16M_P -t upload` restores EMS-ESP at any point. It leaves the recovery in `boot` and the settings untouched.
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# EMS-ESP Recovery Firmware
|
||||||
|
|
||||||
|
Small standalone app that lives in the factory (`boot`) partition on **16MB** boards. If EMS-ESP will not start, the recovery still can: it brings up an access point (and the saved WiFi/Ethernet if it can) and serves a cut-down WebUI to upload a new EMS-ESP image into `app0`/`app1` or choose which slot to boot.
|
||||||
|
|
||||||
|
4MB boards have no factory partition and no recovery.
|
||||||
|
|
||||||
|
Hardware test steps are in [Recovery-Testing.md](Recovery-Testing.md).
|
||||||
|
|
||||||
|
## Why it exists
|
||||||
|
|
||||||
|
ESP-IDF already rolls a failed OTA back to the **other** OTA slot (`app0` ↔ `app1`). That does not help when:
|
||||||
|
|
||||||
|
- both OTA slots are empty, corrupt, or crash on start
|
||||||
|
- a serial flash left otadata pointing at an empty slot
|
||||||
|
- the user needs a known-good way back in without a USB cable
|
||||||
|
|
||||||
|
Recovery is a third, smaller image that EMS-ESP does not overwrite during a normal upgrade. Installing it does **not** change which app boots.
|
||||||
|
|
||||||
|
## Partitions (16MB)
|
||||||
|
|
||||||
|
From `partitions/esp32_partition_16M.csv`:
|
||||||
|
|
||||||
|
| Label | Subtype | Role |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `boot` | factory | Recovery image |
|
||||||
|
| `app0` | ota_0 | EMS-ESP |
|
||||||
|
| `app1` | ota_1 | EMS-ESP (the other OTA slot) |
|
||||||
|
| `otadata` | ota | Which OTA slot the bootloader starts |
|
||||||
|
|
||||||
|
The bootloader reads **otadata**, not “whatever was flashed last”:
|
||||||
|
|
||||||
|
1. Blank otadata (`0xFF`, e.g. after `erase-flash` or a recovery serial upload) → start **factory** (`boot`).
|
||||||
|
2. Valid otadata → start that OTA slot (`app0` or `app1`).
|
||||||
|
3. If that image is invalid, it walks **down** toward factory, then **up** through remaining OTA slots. A valid recovery in `boot` is therefore preferred over a valid image sitting only in `app1` while otadata still points at empty `app0`.
|
||||||
|
|
||||||
|
PlatformIO’s `boot_app0.bin` always selects `app0`. The recovery upload script (`scripts/upload_factory.py`) replaces it with a blank otadata so a serial flash of recovery actually boots recovery.
|
||||||
|
|
||||||
|
A normal serial flash of EMS-ESP goes to `app0` and writes `boot_app0.bin`, so the board starts EMS-ESP. Recovery, if already in `boot`, is left in place.
|
||||||
|
|
||||||
|
## How recovery is installed
|
||||||
|
|
||||||
|
From a running EMS-ESP (**Settings → Version**, or the upload drop zone):
|
||||||
|
|
||||||
|
- Drop `EMS-ESP-Recovery-*.bin` (optional `.md5` first).
|
||||||
|
- EMS-ESP recognises it from the app description (`project_name` = `EMS-ESP-Recovery`) and writes it to the factory partition **without** calling `esp_ota_set_boot_partition`.
|
||||||
|
- EMS-ESP keeps running. The Version page shows the recovery version once it is in flash.
|
||||||
|
|
||||||
|
From serial, recovery must be written at **`0x10000`**, not at `0x1000` (that is the bootloader; the recovery `.bin` overlaps the partition table). See `scripts/do_fresh_install.sh`.
|
||||||
|
|
||||||
|
The recovery app itself **refuses** to install a recovery image. It only writes EMS-ESP into `app0`/`app1`.
|
||||||
|
|
||||||
|
## How EMS-ESP boots into recovery
|
||||||
|
|
||||||
|
Only 16MB builds with `-DEMSESP_HAS_RECOVERY` and a valid recovery image in `boot`.
|
||||||
|
|
||||||
|
| How | What happens |
|
||||||
|
| --- | --- |
|
||||||
|
| **Settings → Version** | Developer mode shows a Recovery row. If a version is installed, **Restart** confirms, then `setPartition("boot")`. |
|
||||||
|
| **Console** (serial or telnet, admin) | `restart boot` → `system_restart("boot")`. |
|
||||||
|
| **Button** | Long press ~3 s (not the ~10 s factory reset). |
|
||||||
|
| **Crash loop** | 5 panics or watchdog resets in a row (RTC counter; survives software restart, not a power cycle). |
|
||||||
|
|
||||||
|
The first three record NVS `rec_reason=request`. The crash path records `crash`. Recovery reads that once and clears it, so a user-requested entry does not show the red crash banner.
|
||||||
|
|
||||||
|
These do **not** enter recovery:
|
||||||
|
|
||||||
|
- Uploading the recovery `.bin` (install only).
|
||||||
|
- `/api/system/restart` or MQTT `system/restart` (ignore any partition argument; reboot the current app).
|
||||||
|
- Bootloader rollback of a pending OTA (goes to the previous **OTA** slot).
|
||||||
|
|
||||||
|
## What recovery does
|
||||||
|
|
||||||
|
- Access point `ems-esp-recovery` (AP SSID plus `-recovery`), plus the saved LAN (Ethernet preferred, else WiFi, including a static IP if one is set).
|
||||||
|
- Sign-in with an EMS-ESP admin account (settings are read from LittleFS, never written).
|
||||||
|
- List `boot` / `app0` / `app1`, show which is running, which will boot next, and rollback state.
|
||||||
|
- Upload an EMS-ESP `.bin` into an OTA slot (empty slot, or the older of the two if both are filled) and boot it.
|
||||||
|
- **Start** a chosen OTA slot without uploading.
|
||||||
|
|
||||||
|
After a WebUI/OTA install from recovery, the new EMS-ESP image is pending verification. After ~2 minutes of uptime it is confirmed so the bootloader will not roll it back.
|
||||||
|
|
||||||
|
## Rough boot flow
|
||||||
|
|
||||||
|
```
|
||||||
|
reset
|
||||||
|
→ ESP-IDF bootloader
|
||||||
|
otadata blank? → factory/boot (recovery)
|
||||||
|
otadata → app0/1 → that slot if the image is valid
|
||||||
|
selected image bad → try factory, then the other OTA slot
|
||||||
|
→ recovery or EMS-ESP
|
||||||
|
|
||||||
|
EMS-ESP, if it starts:
|
||||||
|
crash counter (RTC)
|
||||||
|
5 crashes in a row + recovery present → set boot=factory, restart
|
||||||
|
healthy for 2 minutes → confirm OTA image, clear crash counter
|
||||||
|
```
|
||||||
|
|
||||||
|
## Builds
|
||||||
|
|
||||||
|
| Env | Image |
|
||||||
|
| --- | --- |
|
||||||
|
| `recovery_s_16M` / `recovery_s_16M_P` / `recovery_s3_16M_P` | Recovery, flashed into `boot` at `0x10000` |
|
||||||
|
| `s_16M` / `s_16M_P` / `s3_16M_P` | EMS-ESP (`app0` on serial upload) |
|
||||||
|
|
||||||
|
Firmware files land in `build/firmware/` as `EMS-ESP-Recovery-<ver>-<chip>-16MB[+].bin`.
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"terser": "^5.51.2",
|
"terser": "^5.51.2",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.71.1",
|
"typescript-eslint": "^8.71.1",
|
||||||
"vite": "^8.3.2",
|
"vite": "^8.3.3",
|
||||||
"vite-plugin-imagemin": "^0.6.1"
|
"vite-plugin-imagemin": "^0.6.1"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@12.9.1"
|
"packageManager": "pnpm@12.9.1"
|
||||||
|
|||||||
Generated
+21
-21
@@ -214,7 +214,7 @@ importers:
|
|||||||
version: 10.0.1(eslint@10.12.0(supports-color@10.2.2))
|
version: 10.0.1(eslint@10.12.0(supports-color@10.2.2))
|
||||||
'@preact/preset-vite':
|
'@preact/preset-vite':
|
||||||
specifier: ^2.10.6
|
specifier: ^2.10.6
|
||||||
version: 2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@11.0.0)(supports-color@10.2.2)(vite@8.3.2(@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.0)(supports-color@10.2.2)(vite@8.3.3(@types/node@26.6.4)(terser@5.51.2))
|
||||||
'@trivago/prettier-plugin-sort-imports':
|
'@trivago/prettier-plugin-sort-imports':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2(prettier@3.9.9)(supports-color@10.2.2)
|
version: 6.0.2(prettier@3.9.9)(supports-color@10.2.2)
|
||||||
@@ -258,11 +258,11 @@ importers:
|
|||||||
specifier: ^8.71.1
|
specifier: ^8.71.1
|
||||||
version: 8.71.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
|
version: 8.71.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.3.2
|
specifier: ^8.3.3
|
||||||
version: 8.3.2(@types/node@26.6.4)(terser@5.51.2)
|
version: 8.3.3(@types/node@26.6.4)(terser@5.51.2)
|
||||||
vite-plugin-imagemin:
|
vite-plugin-imagemin:
|
||||||
specifier: ^0.6.1
|
specifier: ^0.6.1
|
||||||
version: 0.6.1(supports-color@10.2.2)(vite@8.3.2(@types/node@26.6.4)(terser@5.51.2))
|
version: 0.6.1(supports-color@10.2.2)(vite@8.3.3(@types/node@26.6.4)(terser@5.51.2))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1338,8 +1338,8 @@ packages:
|
|||||||
duplexer3@0.1.5:
|
duplexer3@0.1.5:
|
||||||
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
|
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.444:
|
electron-to-chromium@1.5.445:
|
||||||
resolution: {integrity: sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==}
|
resolution: {integrity: sha512-iX7o1nF+qNb3Bv5mLYgqfgF9Znh9e/8MKXdmbb8Gv2Q47OKIzTwEPD1oda1HrgEwYatTLJj0wQjHvIFt8Db53Q==}
|
||||||
|
|
||||||
emoji-regex@10.6.0:
|
emoji-regex@10.6.0:
|
||||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||||
@@ -3057,8 +3057,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: 5.x || 6.x || 7.x || 8.x
|
vite: 5.x || 6.x || 7.x || 8.x
|
||||||
|
|
||||||
vite@8.3.2:
|
vite@8.3.3:
|
||||||
resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==}
|
resolution: {integrity: sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3591,19 +3591,19 @@ snapshots:
|
|||||||
|
|
||||||
'@popperjs/core@2.11.8': {}
|
'@popperjs/core@2.11.8': {}
|
||||||
|
|
||||||
'@preact/preset-vite@2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@11.0.0)(supports-color@10.2.2)(vite@8.3.2(@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.0)(supports-color@10.2.2)(vite@8.3.3(@types/node@26.6.4)(terser@5.51.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@10.2.2)
|
'@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': 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)
|
'@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.0)(supports-color@10.2.2)(vite@8.3.2(@types/node@26.6.4)(terser@5.51.2))
|
'@prefresh/vite': 2.4.12(preact@11.0.0)(supports-color@10.2.2)(vite@8.3.3(@types/node@26.6.4)(terser@5.51.2))
|
||||||
'@rollup/pluginutils': 5.4.0
|
'@rollup/pluginutils': 5.4.0
|
||||||
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.7(supports-color@10.2.2))
|
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)
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
vite: 8.3.2(@types/node@26.6.4)(terser@5.51.2)
|
vite: 8.3.3(@types/node@26.6.4)(terser@5.51.2)
|
||||||
vite-prerender-plugin: 0.5.14(vite@8.3.2(@types/node@26.6.4)(terser@5.51.2))
|
vite-prerender-plugin: 0.5.14(vite@8.3.3(@types/node@26.6.4)(terser@5.51.2))
|
||||||
zimmerframe: 1.1.5
|
zimmerframe: 1.1.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- preact
|
- preact
|
||||||
@@ -3620,7 +3620,7 @@ snapshots:
|
|||||||
|
|
||||||
'@prefresh/utils@1.2.1': {}
|
'@prefresh/utils@1.2.1': {}
|
||||||
|
|
||||||
'@prefresh/vite@2.4.12(preact@11.0.0)(supports-color@10.2.2)(vite@8.3.2(@types/node@26.6.4)(terser@5.51.2))':
|
'@prefresh/vite@2.4.12(preact@11.0.0)(supports-color@10.2.2)(vite@8.3.3(@types/node@26.6.4)(terser@5.51.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@10.2.2)
|
'@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))
|
'@prefresh/babel-plugin': 0.5.4(@babel/core@7.29.7(supports-color@10.2.2))
|
||||||
@@ -3628,7 +3628,7 @@ snapshots:
|
|||||||
'@prefresh/utils': 1.2.1
|
'@prefresh/utils': 1.2.1
|
||||||
'@rollup/pluginutils': 4.2.1
|
'@rollup/pluginutils': 4.2.1
|
||||||
preact: 11.0.0
|
preact: 11.0.0
|
||||||
vite: 8.3.2(@types/node@26.6.4)(terser@5.51.2)
|
vite: 8.3.3(@types/node@26.6.4)(terser@5.51.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -4005,7 +4005,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.11.27
|
baseline-browser-mapping: 2.11.27
|
||||||
caniuse-lite: 1.0.30001814
|
caniuse-lite: 1.0.30001814
|
||||||
electron-to-chromium: 1.5.444
|
electron-to-chromium: 1.5.445
|
||||||
node-releases: 2.0.57
|
node-releases: 2.0.57
|
||||||
update-browserslist-db: 1.3.3(browserslist@4.29.3)
|
update-browserslist-db: 1.3.3(browserslist@4.29.3)
|
||||||
|
|
||||||
@@ -4370,7 +4370,7 @@ snapshots:
|
|||||||
|
|
||||||
duplexer3@0.1.5: {}
|
duplexer3@0.1.5: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.444: {}
|
electron-to-chromium@1.5.445: {}
|
||||||
|
|
||||||
emoji-regex@10.6.0: {}
|
emoji-regex@10.6.0: {}
|
||||||
|
|
||||||
@@ -5982,7 +5982,7 @@ snapshots:
|
|||||||
spdx-correct: 3.2.0
|
spdx-correct: 3.2.0
|
||||||
spdx-expression-parse: 3.0.1
|
spdx-expression-parse: 3.0.1
|
||||||
|
|
||||||
vite-plugin-imagemin@0.6.1(supports-color@10.2.2)(vite@8.3.2(@types/node@26.6.4)(terser@5.51.2)):
|
vite-plugin-imagemin@0.6.1(supports-color@10.2.2)(vite@8.3.3(@types/node@26.6.4)(terser@5.51.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/imagemin': 7.0.1
|
'@types/imagemin': 7.0.1
|
||||||
'@types/imagemin-gifsicle': 7.0.4
|
'@types/imagemin-gifsicle': 7.0.4
|
||||||
@@ -6007,11 +6007,11 @@ snapshots:
|
|||||||
imagemin-webp: 6.1.0
|
imagemin-webp: 6.1.0
|
||||||
jpegtran-bin: 6.0.1
|
jpegtran-bin: 6.0.1
|
||||||
pathe: 0.2.0
|
pathe: 0.2.0
|
||||||
vite: 8.3.2(@types/node@26.6.4)(terser@5.51.2)
|
vite: 8.3.3(@types/node@26.6.4)(terser@5.51.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
vite-prerender-plugin@0.5.14(vite@8.3.2(@types/node@26.6.4)(terser@5.51.2)):
|
vite-prerender-plugin@0.5.14(vite@8.3.3(@types/node@26.6.4)(terser@5.51.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
kolorist: 1.8.0
|
kolorist: 1.8.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
@@ -6019,9 +6019,9 @@ snapshots:
|
|||||||
simple-code-frame: 1.3.0
|
simple-code-frame: 1.3.0
|
||||||
source-map: 0.7.6
|
source-map: 0.7.6
|
||||||
stack-trace: 1.0.0
|
stack-trace: 1.0.0
|
||||||
vite: 8.3.2(@types/node@26.6.4)(terser@5.51.2)
|
vite: 8.3.3(@types/node@26.6.4)(terser@5.51.2)
|
||||||
|
|
||||||
vite@8.3.2(@types/node@26.6.4)(terser@5.51.2):
|
vite@8.3.3(@types/node@26.6.4)(terser@5.51.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.33.0
|
lightningcss: 1.33.0
|
||||||
picomatch: 4.0.7
|
picomatch: 4.0.7
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { memo, useContext } from 'react';
|
import { memo, useContext } from 'react';
|
||||||
|
|
||||||
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -53,6 +54,7 @@ const UserProfileComponent = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Button
|
<Button
|
||||||
|
startIcon={<LogoutIcon />}
|
||||||
sx={{ mt: 2 }}
|
sx={{ mt: 2 }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -500,6 +501,7 @@ const ApplicationSettings = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Button
|
<Button
|
||||||
|
startIcon={<SendIcon />}
|
||||||
sx={{ mt: 3 }}
|
sx={{ mt: 3 }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Grid,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -369,14 +368,18 @@ const InstallPartitionDialog = memo(
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onInstall: (partition: string) => void;
|
onInstall: (partition: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const isRecovery = partition === 'boot';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
|
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{LL.INSTALL()} {LL.STORED_VERSIONS()}
|
{isRecovery ? LL.RECOVERY() : `${LL.INSTALL()} ${LL.STORED_VERSIONS()}`}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography sx={{ mb: 2 }}>
|
<Typography sx={{ mb: 2 }}>
|
||||||
{LL.INSTALL_VERSION(LL.INSTALL(), version)}
|
{isRecovery
|
||||||
|
? 'This will boot in recovery mode'
|
||||||
|
: LL.INSTALL_VERSION(LL.INSTALL(), version)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -390,12 +393,14 @@ const InstallPartitionDialog = memo(
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
startIcon={<WarningIcon color="warning" />}
|
startIcon={
|
||||||
|
isRecovery ? <PowerSettingsNewIcon /> : <WarningIcon color="warning" />
|
||||||
|
}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => onInstall(partition)}
|
onClick={() => onInstall(partition)}
|
||||||
color="primary"
|
color={isRecovery ? 'success' : 'primary'}
|
||||||
>
|
>
|
||||||
{LL.INSTALL()}
|
{isRecovery ? LL.RESTART() : LL.INSTALL()}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -665,77 +670,69 @@ const Version = () => {
|
|||||||
{LL.THIS_VERSION()}
|
{LL.THIS_VERSION()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid
|
<Box
|
||||||
container
|
|
||||||
direction="row"
|
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'flex-start',
|
display: 'grid',
|
||||||
alignItems: 'baseline'
|
gridTemplateColumns: 'auto 1fr',
|
||||||
|
columnGap: 3,
|
||||||
|
rowGap: 0.75,
|
||||||
|
alignItems: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
<Typography color="secondary">{LL.VERSION()}</Typography>
|
||||||
<Typography color="secondary">{LL.VERSION()}</Typography>
|
<Box
|
||||||
</Grid>
|
sx={{
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
display: 'flex',
|
||||||
<Typography>
|
alignItems: 'center',
|
||||||
|
minWidth: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography component="span">
|
||||||
{data.emsesp_version}
|
{data.emsesp_version}
|
||||||
{data.build_flags && (
|
{data.build_flags && (
|
||||||
<Typography variant="caption">
|
<Typography component="span" variant="caption">
|
||||||
({data.build_flags})
|
({data.build_flags})
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<IconButton
|
|
||||||
onClick={() => setPartitionVersionInfo(data.partition)}
|
|
||||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
|
||||||
>
|
|
||||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
|
||||||
</IconButton>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => setPartitionVersionInfo(data.partition)}
|
||||||
|
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||||
|
sx={{ p: 0.5 }}
|
||||||
|
>
|
||||||
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
<Typography color="secondary">{LL.PLATFORM()}</Typography>
|
||||||
<Typography color="secondary">{LL.PLATFORM()}</Typography>
|
<Box
|
||||||
</Grid>
|
sx={{
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
display: 'flex',
|
||||||
<Typography>
|
alignItems: 'center',
|
||||||
{platform}
|
minWidth: 0
|
||||||
<Typography variant="caption">
|
}}
|
||||||
(
|
>
|
||||||
{data.psram ? (
|
<Typography component="span">{platform}</Typography>
|
||||||
<CheckIcon
|
<Typography
|
||||||
color="success"
|
component="span"
|
||||||
sx={{
|
variant="caption"
|
||||||
fontSize: '1.5em',
|
sx={{
|
||||||
verticalAlign: 'middle'
|
display: 'inline-flex',
|
||||||
}}
|
alignItems: 'center',
|
||||||
/>
|
ml: 0.75
|
||||||
) : (
|
}}
|
||||||
<CloseIcon
|
>
|
||||||
color="error"
|
(
|
||||||
sx={{
|
{data.psram ? (
|
||||||
fontSize: '1.5em',
|
<CheckIcon color="success" sx={{ fontSize: 16, mx: 0.25 }} />
|
||||||
verticalAlign: 'middle'
|
) : (
|
||||||
}}
|
<CloseIcon color="error" sx={{ fontSize: 16, mx: 0.25 }} />
|
||||||
/>
|
)}
|
||||||
)}
|
PSRAM)
|
||||||
PSRAM)
|
|
||||||
</Typography>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Box>
|
||||||
|
</Box>
|
||||||
{data.recovery_version !== undefined && (
|
|
||||||
<>
|
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
|
||||||
<Typography color="secondary">{LL.RECOVERY()}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
|
||||||
<Typography>
|
|
||||||
{data.recovery_version || LL.RECOVERY_NOT_INSTALLED()}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{internetLive ? (
|
{internetLive ? (
|
||||||
<>
|
<>
|
||||||
@@ -743,83 +740,147 @@ const Version = () => {
|
|||||||
{LL.AVAILABLE_VERSION()}
|
{LL.AVAILABLE_VERSION()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid
|
<Box
|
||||||
container
|
|
||||||
direction="row"
|
|
||||||
rowSpacing={1}
|
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'flex-start',
|
display: 'grid',
|
||||||
alignItems: 'baseline'
|
gridTemplateColumns: 'auto 1fr',
|
||||||
|
columnGap: 3,
|
||||||
|
rowGap: 0.75,
|
||||||
|
alignItems: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{otherPartitions.length > 0 && data.developer_mode && (
|
{data.developer_mode && (
|
||||||
<>
|
<>
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
{otherPartitions.length > 0 && (
|
||||||
<Typography color="secondary">{LL.STORED_VERSIONS()}</Typography>
|
<>
|
||||||
</Grid>
|
<Typography
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
color="secondary"
|
||||||
{otherPartitions.map((partition) => (
|
sx={{ alignSelf: 'start', pt: 0.5 }}
|
||||||
<Typography key={partition.partition} sx={{ mb: 1 }}>
|
>
|
||||||
{partition.version}
|
{LL.STORED_VERSIONS()}
|
||||||
<IconButton
|
|
||||||
onClick={() =>
|
|
||||||
setPartitionVersionInfo(partition.partition)
|
|
||||||
}
|
|
||||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
|
||||||
>
|
|
||||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
|
||||||
</IconButton>
|
|
||||||
<Button
|
|
||||||
sx={{ ml: 0 }}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onClick={() =>
|
|
||||||
showPartitionDialog(
|
|
||||||
partition.version,
|
|
||||||
partition.partition,
|
|
||||||
partition.install_date ?? ''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{LL.INSTALL()}
|
|
||||||
</Button>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
))}
|
<Box
|
||||||
</Grid>
|
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 }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
<Typography color="secondary">{LL.STABLE()}</Typography>
|
||||||
<Typography color="secondary">{LL.STABLE()}</Typography>
|
<Box
|
||||||
</Grid>
|
sx={{
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
display: 'flex',
|
||||||
<Typography>
|
alignItems: 'center',
|
||||||
{latestVersion?.version}
|
flexWrap: 'wrap',
|
||||||
<IconButton
|
minWidth: 0
|
||||||
onClick={() => setShowVersionInfo(1)}
|
}}
|
||||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
>
|
||||||
>
|
<Typography component="span">{latestVersion?.version}</Typography>
|
||||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
<IconButton
|
||||||
</IconButton>
|
size="small"
|
||||||
{showButtons(false)}
|
onClick={() => setShowVersionInfo(1)}
|
||||||
</Typography>
|
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||||
</Grid>
|
sx={{ p: 0.5 }}
|
||||||
|
>
|
||||||
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
{showButtons(false)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
<Typography color="secondary">{LL.DEVELOPMENT()}</Typography>
|
||||||
<Typography color="secondary">{LL.DEVELOPMENT()}</Typography>
|
<Box
|
||||||
</Grid>
|
sx={{
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
display: 'flex',
|
||||||
<Typography>
|
alignItems: 'center',
|
||||||
{latestDevVersion?.version}
|
flexWrap: 'wrap',
|
||||||
<IconButton
|
minWidth: 0
|
||||||
onClick={() => setShowVersionInfo(2)}
|
}}
|
||||||
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
>
|
||||||
>
|
<Typography component="span">{latestDevVersion?.version}</Typography>
|
||||||
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
<IconButton
|
||||||
</IconButton>
|
size="small"
|
||||||
{showButtons(true)}
|
onClick={() => setShowVersionInfo(2)}
|
||||||
</Typography>
|
aria-label={LL.FIRMWARE_VERSION_INFO()}
|
||||||
</Grid>
|
sx={{ p: 0.5 }}
|
||||||
</Grid>
|
>
|
||||||
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
</IconButton>
|
||||||
|
{showButtons(true)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Typography sx={{ mt: 2 }} color="warning">
|
<Typography sx={{ mt: 2 }} color="warning">
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ export interface SystemStatus {
|
|||||||
free_caps: number;
|
free_caps: number;
|
||||||
model: string;
|
model: string;
|
||||||
board: string;
|
board: string;
|
||||||
has_loader: boolean;
|
|
||||||
recovery_version?: string;
|
recovery_version?: string;
|
||||||
has_partition: boolean;
|
has_partition: boolean;
|
||||||
partitions: {
|
partitions: {
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
#include <Update.h>
|
#include <Update.h>
|
||||||
#include <esp_app_format.h>
|
#include <esp_app_format.h>
|
||||||
#include <esp_image_format.h>
|
#include <esp_image_format.h>
|
||||||
|
#ifdef OTA_UPDATER_ANY_PARTITION
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
#include <esp_partition.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
|
||||||
@@ -19,8 +23,14 @@ OtaUpdater::FileType OtaUpdater::classify(const String & filename, size_t filesi
|
|||||||
if (extension == "bin" && filename.endsWith("littlefs.bin")) {
|
if (extension == "bin" && filename.endsWith("littlefs.bin")) {
|
||||||
return FileType::FILESYSTEM;
|
return FileType::FILESYSTEM;
|
||||||
}
|
}
|
||||||
if (extension == "bin" && filesize >= min_firmware_size) {
|
if (extension == "bin") {
|
||||||
return FileType::FIRMWARE;
|
String lower = filename;
|
||||||
|
lower.toLowerCase();
|
||||||
|
const size_t min_size =
|
||||||
|
lower.indexOf("recovery") >= 0 ? MIN_RECOVERY_FIRMWARE_SIZE : min_firmware_size;
|
||||||
|
if (filesize >= min_size) {
|
||||||
|
return FileType::FIRMWARE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (extension == "json") {
|
if (extension == "json") {
|
||||||
return FileType::JSON;
|
return FileType::JSON;
|
||||||
@@ -114,6 +124,19 @@ bool OtaUpdater::hasMd5() const {
|
|||||||
return strlen(_md5.data()) == _md5.size() - 1;
|
return strlen(_md5.data()) == _md5.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef OTA_UPDATER_ANY_PARTITION
|
||||||
|
void OtaUpdater::setError(esp_err_t err) {
|
||||||
|
snprintf(_error_buf, sizeof(_error_buf), "%s (0x%x)", esp_err_to_name(err), static_cast<unsigned>(err));
|
||||||
|
_error = _error_buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtaUpdater::releaseTarget() {
|
||||||
|
_target = nullptr;
|
||||||
|
heap_caps_free(_dram);
|
||||||
|
_dram = nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
bool OtaUpdater::beginFirmware(size_t filesize, const esp_partition_t * target, bool set_boot) {
|
bool OtaUpdater::beginFirmware(size_t filesize, const esp_partition_t * target, bool set_boot) {
|
||||||
_md5_applied = false;
|
_md5_applied = false;
|
||||||
_error = "";
|
_error = "";
|
||||||
@@ -126,13 +149,14 @@ bool OtaUpdater::beginFirmware(size_t filesize, const esp_partition_t * target,
|
|||||||
_error = "Firmware does not fit in the partition";
|
_error = "Firmware does not fit in the partition";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const esp_err_t err = esp_ota_begin(target, OTA_WITH_SEQUENTIAL_WRITES, &_handle);
|
_dram = static_cast<uint8_t *>(heap_caps_malloc(DRAM_CHUNK, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
|
||||||
if (err != ESP_OK) {
|
if (_dram == nullptr) {
|
||||||
_error = esp_err_to_name(err);
|
_error = "No internal RAM for OTA buffer";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_target = target;
|
_target = target;
|
||||||
_written = 0;
|
_written = 0;
|
||||||
|
_erased = 0;
|
||||||
_set_boot = set_boot;
|
_set_boot = set_boot;
|
||||||
if (hasMd5()) {
|
if (hasMd5()) {
|
||||||
_md5Builder.begin();
|
_md5Builder.begin();
|
||||||
@@ -166,15 +190,39 @@ bool OtaUpdater::beginFilesystem() {
|
|||||||
bool OtaUpdater::write(const uint8_t * data, size_t len) {
|
bool OtaUpdater::write(const uint8_t * data, size_t len) {
|
||||||
#ifdef OTA_UPDATER_ANY_PARTITION
|
#ifdef OTA_UPDATER_ANY_PARTITION
|
||||||
if (_target != nullptr) {
|
if (_target != nullptr) {
|
||||||
const esp_err_t err = esp_ota_write(_handle, data, len);
|
while (len) {
|
||||||
if (err != ESP_OK) {
|
const size_t n = len < DRAM_CHUNK ? len : DRAM_CHUNK;
|
||||||
_error = esp_err_to_name(err);
|
memcpy(_dram, data, n);
|
||||||
return false;
|
|
||||||
|
const size_t need = _written + n;
|
||||||
|
if (need > _erased) {
|
||||||
|
size_t erase_to = (need + FLASH_SECTOR - 1) / FLASH_SECTOR * FLASH_SECTOR;
|
||||||
|
if (erase_to > _target->size) {
|
||||||
|
erase_to = _target->size;
|
||||||
|
}
|
||||||
|
const size_t erase_len = erase_to - _erased;
|
||||||
|
if (erase_len > 0) {
|
||||||
|
const esp_err_t err = esp_partition_erase_range(_target, _erased, erase_len);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
setError(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_erased = erase_to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_err_t err = esp_partition_write(_target, _written, _dram, n);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
setError(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_md5_applied) {
|
||||||
|
_md5Builder.add(_dram, n);
|
||||||
|
}
|
||||||
|
_written += n;
|
||||||
|
data += n;
|
||||||
|
len -= n;
|
||||||
}
|
}
|
||||||
if (_md5_applied) {
|
|
||||||
_md5Builder.add(data, len);
|
|
||||||
}
|
|
||||||
_written += len;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -184,27 +232,32 @@ bool OtaUpdater::write(const uint8_t * data, size_t len) {
|
|||||||
bool OtaUpdater::end() {
|
bool OtaUpdater::end() {
|
||||||
#ifdef OTA_UPDATER_ANY_PARTITION
|
#ifdef OTA_UPDATER_ANY_PARTITION
|
||||||
if (_target != nullptr) {
|
if (_target != nullptr) {
|
||||||
const esp_partition_t * target = _target;
|
const esp_partition_t * target = _target;
|
||||||
_target = nullptr;
|
const bool set_boot = _set_boot;
|
||||||
|
releaseTarget();
|
||||||
|
|
||||||
if (_md5_applied) {
|
if (_md5_applied) {
|
||||||
_md5Builder.calculate();
|
_md5Builder.calculate();
|
||||||
if (strcmp(_md5Builder.toString().c_str(), _md5.data()) != 0) {
|
if (strcmp(_md5Builder.toString().c_str(), _md5.data()) != 0) {
|
||||||
esp_ota_abort(_handle);
|
|
||||||
_error = "MD5 check failed";
|
_error = "MD5 check failed";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// esp_ota_end() validates the image
|
esp_image_metadata_t meta = {};
|
||||||
esp_err_t err = esp_ota_end(_handle);
|
esp_partition_pos_t part_pos = {.offset = target->address, .size = target->size};
|
||||||
if (err == ESP_OK && _set_boot) {
|
esp_err_t err = esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta);
|
||||||
err = esp_ota_set_boot_partition(target);
|
|
||||||
}
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
_error = esp_err_to_name(err);
|
setError(err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (set_boot) {
|
||||||
|
err = esp_ota_set_boot_partition(target);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
setError(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -214,12 +267,13 @@ bool OtaUpdater::end() {
|
|||||||
void OtaUpdater::abort() {
|
void OtaUpdater::abort() {
|
||||||
#ifdef OTA_UPDATER_ANY_PARTITION
|
#ifdef OTA_UPDATER_ANY_PARTITION
|
||||||
if (_target != nullptr) {
|
if (_target != nullptr) {
|
||||||
esp_ota_abort(_handle);
|
releaseTarget();
|
||||||
_target = nullptr;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Update.abort();
|
if (Update.isRunning()) {
|
||||||
|
Update.abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t OtaUpdater::progress() const {
|
size_t OtaUpdater::progress() const {
|
||||||
|
|||||||
@@ -16,11 +16,16 @@ class OtaUpdater {
|
|||||||
public:
|
public:
|
||||||
enum class FileType : uint8_t { UNSUPPORTED, FIRMWARE, FILESYSTEM, JSON, MD5 };
|
enum class FileType : uint8_t { UNSUPPORTED, FIRMWARE, FILESYSTEM, JSON, MD5 };
|
||||||
|
|
||||||
|
// min_firmware_size is for the main EMS-ESP image. Filenames containing "recovery"
|
||||||
|
// (case-insensitive) use MIN_RECOVERY_FIRMWARE_SIZE instead.
|
||||||
static FileType classify(const String & filename, size_t filesize, size_t min_firmware_size);
|
static FileType classify(const String & filename, size_t filesize, size_t min_firmware_size);
|
||||||
|
|
||||||
// project name in the app description of the recovery firmware, set by custom_fw_name in platformio.ini
|
// project name in the app description of the recovery firmware, set by custom_fw_name in platformio.ini
|
||||||
static constexpr char RECOVERY_PROJECT_NAME[] = "EMS-ESP-Recovery";
|
static constexpr char RECOVERY_PROJECT_NAME[] = "EMS-ESP-Recovery";
|
||||||
|
|
||||||
|
// recovery firmware is much smaller than the main image (~512 KB–1 MB)
|
||||||
|
static constexpr size_t MIN_RECOVERY_FIRMWARE_SIZE = 512 * 1024;
|
||||||
|
|
||||||
// checks the ESP image magic byte and that the chip id matches the chip we're running on
|
// checks the ESP image magic byte and that the chip id matches the chip we're running on
|
||||||
static bool isCompatibleFirmware(const uint8_t * data, size_t len);
|
static bool isCompatibleFirmware(const uint8_t * data, size_t len);
|
||||||
|
|
||||||
@@ -64,12 +69,21 @@ class OtaUpdater {
|
|||||||
const char * _error = "";
|
const char * _error = "";
|
||||||
|
|
||||||
#ifdef OTA_UPDATER_ANY_PARTITION
|
#ifdef OTA_UPDATER_ANY_PARTITION
|
||||||
// set when writing to an explicit target partition with the IDF OTA API instead of Update
|
// explicit target (factory/boot or an OTA slot) is written with the partition API;
|
||||||
|
// esp_ota_begin() returns ESP_ERR_INVALID_STATE (0x102) on the factory partition
|
||||||
const esp_partition_t * _target = nullptr;
|
const esp_partition_t * _target = nullptr;
|
||||||
esp_ota_handle_t _handle = 0;
|
|
||||||
size_t _written = 0;
|
size_t _written = 0;
|
||||||
|
size_t _erased = 0;
|
||||||
bool _set_boot = true;
|
bool _set_boot = true;
|
||||||
MD5Builder _md5Builder;
|
MD5Builder _md5Builder;
|
||||||
|
// AsyncTCP may hand us PSRAM pointers; ESP32 flash DMA needs internal RAM
|
||||||
|
uint8_t * _dram = nullptr;
|
||||||
|
static constexpr size_t DRAM_CHUNK = 1024;
|
||||||
|
static constexpr size_t FLASH_SECTOR = 4096;
|
||||||
|
char _error_buf[48]{};
|
||||||
|
|
||||||
|
void setError(esp_err_t err);
|
||||||
|
void releaseTarget();
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+23
-23
@@ -116,8 +116,8 @@ let system_status = {
|
|||||||
psram: true,
|
psram: true,
|
||||||
psram_size: 8189,
|
psram_size: 8189,
|
||||||
free_psram: 8166,
|
free_psram: 8166,
|
||||||
has_loader: true,
|
// recovery_version: '', // upload an EMS-ESP-Recovery-*.bin to set it
|
||||||
recovery_version: '', // upload an EMS-ESP-Recovery-*.bin to set it
|
recovery_version: '1.0.0',
|
||||||
has_partition: true,
|
has_partition: true,
|
||||||
partitions: [
|
partitions: [
|
||||||
{
|
{
|
||||||
@@ -152,12 +152,12 @@ let system_status = {
|
|||||||
let DEV_VERSION_IS_UPGRADEABLE: boolean;
|
let DEV_VERSION_IS_UPGRADEABLE: boolean;
|
||||||
let STABLE_VERSION_IS_UPGRADEABLE: boolean;
|
let STABLE_VERSION_IS_UPGRADEABLE: boolean;
|
||||||
let THIS_VERSION: string;
|
let THIS_VERSION: string;
|
||||||
let LATEST_STABLE_VERSION = '3.8.4';
|
let LATEST_STABLE_VERSION = '3.8.6';
|
||||||
let LATEST_DEV_VERSION = '3.9.0-dev.1';
|
let LATEST_DEV_VERSION = '3.9.0-dev.1';
|
||||||
|
|
||||||
// scenarios for testing versioning
|
// scenarios for testing versioning
|
||||||
let version_test = 0; // on latest stable, or switch to dev
|
// let version_test = 0; // on latest stable, or switch to dev
|
||||||
// let version_test = 1; // on latest dev, or switch back to stable
|
let version_test = 1; // on latest dev, or switch back to stable
|
||||||
// let version_test = 2; // upgrade an older stable to latest stable or switch to latest dev
|
// let version_test = 2; // upgrade an older stable to latest stable or switch to latest dev
|
||||||
// let version_test = 3; // upgrade dev to latest, or switch to stable
|
// let version_test = 3; // upgrade dev to latest, or switch to stable
|
||||||
// let version_test = 4; // downgrade to an older dev, or switch back to stable
|
// let version_test = 4; // downgrade to an older dev, or switch back to stable
|
||||||
@@ -344,10 +344,10 @@ function updateMask(entity: any, de: any, dd: any) {
|
|||||||
const old_custom_name = dd.nodes[dd_objIndex].cn;
|
const old_custom_name = dd.nodes[dd_objIndex].cn;
|
||||||
console.log(
|
console.log(
|
||||||
'comparing names, old (' +
|
'comparing names, old (' +
|
||||||
old_custom_name +
|
old_custom_name +
|
||||||
') with new (' +
|
') with new (' +
|
||||||
new_custom_name +
|
new_custom_name +
|
||||||
')'
|
')'
|
||||||
);
|
);
|
||||||
if (old_custom_name !== new_custom_name) {
|
if (old_custom_name !== new_custom_name) {
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -467,9 +467,9 @@ function upgradeImportantMessages(version: string) {
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'upgradeImportantMessageType: version=' +
|
'upgradeImportantMessageType: version=' +
|
||||||
version +
|
version +
|
||||||
' type=' +
|
' type=' +
|
||||||
upgradeImportantMessageType_n
|
upgradeImportantMessageType_n
|
||||||
);
|
);
|
||||||
return { upgradeImportantMessageType: upgradeImportantMessageType_n };
|
return { upgradeImportantMessageType: upgradeImportantMessageType_n };
|
||||||
}
|
}
|
||||||
@@ -520,17 +520,17 @@ function get_versions() {
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'getVersions: current=' +
|
'getVersions: current=' +
|
||||||
THIS_VERSION +
|
THIS_VERSION +
|
||||||
' stable=' +
|
' stable=' +
|
||||||
LATEST_STABLE_VERSION +
|
LATEST_STABLE_VERSION +
|
||||||
' (upgradeable=' +
|
' (upgradeable=' +
|
||||||
(STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') +
|
(STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') +
|
||||||
') dev=' +
|
') dev=' +
|
||||||
LATEST_DEV_VERSION +
|
LATEST_DEV_VERSION +
|
||||||
' (upgradeable=' +
|
' (upgradeable=' +
|
||||||
(DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') +
|
(DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') +
|
||||||
')' +
|
')' +
|
||||||
(MOCK_OFFLINE ? ' [offline]' : '')
|
(MOCK_OFFLINE ? ' [offline]' : '')
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -177,8 +177,8 @@ build_flags =
|
|||||||
; 1 crashes on every start, so the bootloader rolls back to the previous firmware
|
; 1 crashes on every start, so the bootloader rolls back to the previous firmware
|
||||||
; 2 runs normally until it's confirmed after 2 minutes, then crashes on every start. After 5 crashes in a row
|
; 2 runs normally until it's confirmed after 2 minutes, then crashes on every start. After 5 crashes in a row
|
||||||
; it switches to the recovery firmware. Without one it keeps crashing until reflashed over serial
|
; it switches to the recovery firmware. Without one it keeps crashing until reflashed over serial
|
||||||
[env:crashtest_s3_16M_P]
|
[env:crashtest_s_16M_P]
|
||||||
extends = env:s3_16M_P
|
extends = env:s_16M_P
|
||||||
build_flags =
|
build_flags =
|
||||||
${espressif32_base_16M.build_flags}
|
${espressif32_base_16M.build_flags}
|
||||||
-D EMSESP_CRASH_TEST=1
|
-D EMSESP_CRASH_TEST=1
|
||||||
|
|||||||
+45
-15
@@ -1,26 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Full fresh install of EMS-ESP on an ESP32 16MB board (s_16M_P).
|
||||||
|
#
|
||||||
|
# Run from the scripts folder:
|
||||||
|
# ./do_fresh_install.sh
|
||||||
|
#
|
||||||
|
# Flash map (partitions/esp32_partition_16M.csv):
|
||||||
|
# 0x1000 bootloader
|
||||||
|
# 0x8000 partition table
|
||||||
|
# 0xe000 otadata (boot_app0.bin selects app0)
|
||||||
|
# 0x10000 recovery (factory / boot)
|
||||||
|
# 0x490000 EMS-ESP (app0)
|
||||||
|
# 0xdf0000 LittleFS
|
||||||
|
#
|
||||||
|
# Because otadata points at app0, the device boots EMS-ESP. Recovery stays
|
||||||
|
# installed in factory for fallback. Do not write recovery at 0x1000 — that
|
||||||
|
# is the bootloader, and the image overlaps the partition table at 0x8000.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
ESPTOOL=~/.platformio/penv/bin/esptool
|
ESPTOOL=~/.platformio/penv/bin/esptool
|
||||||
PORT=/dev/ttyUSB0
|
PORT=/dev/ttyUSB0
|
||||||
BUILD=../.pio/build/s_16M_P
|
BAUD=${BAUD:-2000000}
|
||||||
|
|
||||||
|
BUILD_BIN=../.pio/build/s_16M_P
|
||||||
|
RECOVERY_BIN=../.pio/build/recovery_s_16M_P
|
||||||
BOOT_APP0=~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin
|
BOOT_APP0=~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin
|
||||||
|
|
||||||
# erase
|
for f in \
|
||||||
|
"$BUILD_BIN/bootloader.bin" \
|
||||||
|
"$BUILD_BIN/partitions.bin" \
|
||||||
|
"$BUILD_BIN/firmware.bin" \
|
||||||
|
"$RECOVERY_BIN/firmware.bin" \
|
||||||
|
"$BOOT_APP0"; do
|
||||||
|
if [[ ! -f $f ]]; then
|
||||||
|
echo "Missing $f — build s_16M_P and recovery_s_16M_P first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# full erase, stay in download mode
|
||||||
$ESPTOOL --chip esp32 --port $PORT --after no-reset erase-flash
|
$ESPTOOL --chip esp32 --port $PORT --after no-reset erase-flash
|
||||||
|
|
||||||
# create the filesystem
|
# create the filesystem, in case it's needed
|
||||||
~/.platformio/penv/bin/python build_littlefs.py data $BUILD/littlefs.bin
|
~/.platformio/penv/bin/python build_littlefs.py data $BUILD_BIN/littlefs.bin
|
||||||
|
|
||||||
# upload filesystem (spiffs partition @ 0xdf0000), leave halted so it doesn't boot yet
|
# bootloader, partitions, otadata, recovery, app, filesystem; then boot
|
||||||
$ESPTOOL --chip esp32 --port $PORT --baud 460800 \
|
$ESPTOOL --chip esp32 --port $PORT --baud $BAUD \
|
||||||
--before default-reset --after no-reset \
|
|
||||||
write-flash -z --flash-mode dio --flash-freq 80m --flash-size detect \
|
|
||||||
0xdf0000 $BUILD/littlefs.bin
|
|
||||||
|
|
||||||
# flash bootloader + partition table + app (app0 @ 0x490000), then boot
|
|
||||||
$ESPTOOL --chip esp32 --port $PORT --baud 460800 \
|
|
||||||
--before default-reset --after hard-reset \
|
--before default-reset --after hard-reset \
|
||||||
write-flash -z --flash-mode dio --flash-freq 80m --flash-size detect \
|
write-flash -z --flash-mode dio --flash-freq 80m --flash-size detect \
|
||||||
0x1000 $BUILD/bootloader.bin \
|
0x1000 $BUILD_BIN/bootloader.bin \
|
||||||
0x8000 $BUILD/partitions.bin \
|
0x8000 $BUILD_BIN/partitions.bin \
|
||||||
0xe000 $BOOT_APP0 \
|
0xe000 $BOOT_APP0 \
|
||||||
0x490000 $BUILD/firmware.bin
|
0x10000 $RECOVERY_BIN/firmware.bin \
|
||||||
|
0x490000 $BUILD_BIN/firmware.bin \
|
||||||
|
0xdf0000 $BUILD_BIN/littlefs.bin
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ UploadFileService::UploadFileService(AsyncWebServer * server, SecurityManager *
|
|||||||
: _securityManager(securityManager)
|
: _securityManager(securityManager)
|
||||||
, _is_firmware(false)
|
, _is_firmware(false)
|
||||||
, _is_filesystem(false)
|
, _is_filesystem(false)
|
||||||
, _is_recovery(false) {
|
, _is_recovery(false)
|
||||||
|
, _response_sent(false) {
|
||||||
server->on(
|
server->on(
|
||||||
UPLOAD_FILE_PATH,
|
UPLOAD_FILE_PATH,
|
||||||
HTTP_POST,
|
HTTP_POST,
|
||||||
@@ -31,9 +32,10 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
|
|||||||
// check details of the file, to see if its a valid bin or json file
|
// check details of the file, to see if its a valid bin or json file
|
||||||
const std::size_t filesize = request->contentLength();
|
const std::size_t filesize = request->contentLength();
|
||||||
|
|
||||||
_is_firmware = false;
|
_is_firmware = false;
|
||||||
_is_filesystem = false;
|
_is_filesystem = false;
|
||||||
_is_recovery = false;
|
_is_recovery = false;
|
||||||
|
_response_sent = false;
|
||||||
|
|
||||||
switch (OtaUpdater::classify(filename, filesize, emsesp::System::MIN_FIRMWARE_SIZE)) {
|
switch (OtaUpdater::classify(filename, filesize, emsesp::System::MIN_FIRMWARE_SIZE)) {
|
||||||
case OtaUpdater::FileType::FILESYSTEM:
|
case OtaUpdater::FileType::FILESYSTEM:
|
||||||
@@ -96,6 +98,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
|
|||||||
}
|
}
|
||||||
request->onDisconnect([this] { handleDisconnect(); }); // success, let's make sure we end the update if the client hangs up
|
request->onDisconnect([this] { handleDisconnect(); }); // success, let's make sure we end the update if the client hangs up
|
||||||
} else {
|
} else {
|
||||||
|
emsesp::EMSESP::logger().err("OTA begin failed: %s", _ota.errorString());
|
||||||
handleError(request, 507); // failed to begin, send an error response Insufficient Storage
|
handleError(request, 507); // failed to begin, send an error response Insufficient Storage
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -147,6 +150,19 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
|
void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
|
||||||
|
if (request->_tempObject) {
|
||||||
|
const int code = static_cast<int>(reinterpret_cast<intptr_t>(request->_tempObject));
|
||||||
|
if (!_response_sent && code >= 400) {
|
||||||
|
request->send(request->beginResponse(code));
|
||||||
|
_response_sent = true;
|
||||||
|
}
|
||||||
|
_is_firmware = false;
|
||||||
|
_is_filesystem = false;
|
||||||
|
_is_recovery = false;
|
||||||
|
emsesp::EMSESP::system_.systemStatus(emsesp::SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
emsesp::EMSESP::logger().info("Upload successful");
|
emsesp::EMSESP::logger().info("Upload successful");
|
||||||
|
|
||||||
// did we just complete uploading a json file?
|
// did we just complete uploading a json file?
|
||||||
@@ -217,26 +233,25 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UploadFileService::handleError(AsyncWebServerRequest * request, int code) {
|
void UploadFileService::handleError(AsyncWebServerRequest * request, int code) {
|
||||||
emsesp::EMSESP::logger().info("Upload error: %d", code);
|
|
||||||
emsesp::EMSESP::system_.uart_init(); // re-enable UART
|
|
||||||
|
|
||||||
// if we have had an error already, do nothing
|
// if we have had an error already, do nothing
|
||||||
if (request->_tempObject) {
|
if (request->_tempObject) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
request->_tempObject = reinterpret_cast<void *>(static_cast<intptr_t>(code)); // stop later chunks from retrying the write
|
||||||
|
|
||||||
// send the error code to the client and record the error code in the temp object
|
emsesp::EMSESP::logger().info("Upload error: %d", code);
|
||||||
AsyncWebServerResponse * response = request->beginResponse(code);
|
emsesp::EMSESP::system_.uart_init(); // re-enable UART
|
||||||
request->send(response);
|
_ota.abort();
|
||||||
|
|
||||||
// check for invalid extension and immediately kill the connection, which will throw an error
|
// 406: the client sent a file we won't accept. Close now so the browser fails fast.
|
||||||
// that is caught by the web code. Unfortunately the http error code is not sent to the client on fast network connections
|
// Other codes wait for uploadComplete() to send, so we don't destroy the request mid-stream.
|
||||||
if (code == 406) {
|
if (code == 406) {
|
||||||
|
request->send(request->beginResponse(code));
|
||||||
|
_response_sent = true;
|
||||||
request->client()->close();
|
request->client()->close();
|
||||||
_is_firmware = false;
|
_is_firmware = false;
|
||||||
_is_filesystem = false;
|
_is_filesystem = false;
|
||||||
_is_recovery = false;
|
_is_recovery = false;
|
||||||
_ota.abort();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we aborted a filesystem upload, remount LittleFS so the device keeps working
|
// if we aborted a filesystem upload, remount LittleFS so the device keeps working
|
||||||
@@ -249,13 +264,10 @@ void UploadFileService::handleDisconnect() {
|
|||||||
emsesp::EMSESP::logger().info("Upload finished");
|
emsesp::EMSESP::logger().info("Upload finished");
|
||||||
emsesp::EMSESP::system_.uart_init(); // re-enable UART
|
emsesp::EMSESP::system_.uart_init(); // re-enable UART
|
||||||
|
|
||||||
#ifdef EMSESP_HAS_RECOVERY
|
_ota.abort();
|
||||||
if (_is_recovery) {
|
if (_is_recovery || _is_firmware || _is_filesystem) {
|
||||||
// the client hung up before the upload completed
|
|
||||||
_ota.abort();
|
|
||||||
emsesp::EMSESP::system_.systemStatus(emsesp::SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
|
emsesp::EMSESP::system_.systemStatus(emsesp::SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
_is_firmware = false;
|
_is_firmware = false;
|
||||||
_is_filesystem = false;
|
_is_filesystem = false;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class UploadFileService {
|
|||||||
bool _is_firmware;
|
bool _is_firmware;
|
||||||
bool _is_filesystem;
|
bool _is_filesystem;
|
||||||
bool _is_recovery; // firmware for the factory partition, which doesn't change the boot partition
|
bool _is_recovery; // firmware for the factory partition, which doesn't change the boot partition
|
||||||
|
bool _response_sent;
|
||||||
OtaUpdater _ota;
|
OtaUpdater _ota;
|
||||||
|
|
||||||
void handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final);
|
void handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final);
|
||||||
|
|||||||
+57
-8
@@ -573,8 +573,12 @@ void System::recovery_installed() {
|
|||||||
if (factory != nullptr) {
|
if (factory != nullptr) {
|
||||||
char c[20];
|
char c[20];
|
||||||
snprintf(c, sizeof(c), "d_%s", factory->label);
|
snprintf(c, sizeof(c), "d_%s", factory->label);
|
||||||
EMSESP::nvs_.remove(factory->label);
|
if (EMSESP::nvs_.isKey(factory->label)) {
|
||||||
EMSESP::nvs_.remove(c);
|
EMSESP::nvs_.remove(factory->label);
|
||||||
|
}
|
||||||
|
if (EMSESP::nvs_.isKey(c)) {
|
||||||
|
EMSESP::nvs_.remove(c);
|
||||||
|
}
|
||||||
partition_info_.erase(factory->label); // on the AsyncTCP task, same as the web handlers reading it
|
partition_info_.erase(factory->label); // on the AsyncTCP task, same as the web handlers reading it
|
||||||
}
|
}
|
||||||
LOG_INFO("Recovery firmware v%s installed", recovery_version().c_str());
|
LOG_INFO("Recovery firmware v%s installed", recovery_version().c_str());
|
||||||
@@ -639,6 +643,12 @@ bool System::set_partition([[maybe_unused]] const char * partitionname) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef EMSESP_HAS_RECOVERY
|
||||||
|
if (partition->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY) {
|
||||||
|
EMSESP::nvs_.putString(EMSESP_NVS_RECOVERY_REASON, "request");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// initiate the restart
|
// initiate the restart
|
||||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED);
|
||||||
return true;
|
return true;
|
||||||
@@ -1323,6 +1333,16 @@ void System::show_system(uuid::console::Shell & shell) {
|
|||||||
shell.printfln(" [total %d]", available.size());
|
shell.printfln(" [total %d]", available.size());
|
||||||
// List all partitions and their version info
|
// List all partitions and their version info
|
||||||
shell.println(" Partitions:");
|
shell.println(" Partitions:");
|
||||||
|
#ifndef EMSESP_STANDALONE
|
||||||
|
const esp_partition_t * running = esp_ota_get_running_partition();
|
||||||
|
const esp_partition_t * boot = esp_ota_get_boot_partition();
|
||||||
|
const char * running_label = running ? running->label : "";
|
||||||
|
const char * boot_label = boot ? boot->label : "";
|
||||||
|
#else
|
||||||
|
const char * running_label = "";
|
||||||
|
const char * boot_label = "";
|
||||||
|
#endif
|
||||||
|
bool boot_listed = false;
|
||||||
for (const auto & partition : partition_info_) {
|
for (const auto & partition : partition_info_) {
|
||||||
if (partition.second.version.empty()) {
|
if (partition.second.version.empty()) {
|
||||||
continue; // no version, empty string
|
continue; // no version, empty string
|
||||||
@@ -1334,13 +1354,42 @@ void System::show_system(uuid::console::Shell & shell) {
|
|||||||
strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d));
|
strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d));
|
||||||
installed = std::string(", installed on ") + time_string;
|
installed = std::string(", installed on ") + time_string;
|
||||||
}
|
}
|
||||||
shell.printfln(" %s: v%s (%d KB%s) %s",
|
const bool is_running = strcmp(running_label, partition.first.c_str()) == 0;
|
||||||
partition.first.c_str(),
|
const bool is_boot = strcmp(boot_label, partition.first.c_str()) == 0;
|
||||||
partition.second.version.c_str(),
|
if (is_boot) {
|
||||||
partition.second.size,
|
boot_listed = true;
|
||||||
installed.c_str(),
|
}
|
||||||
(strcmp(esp_ota_get_running_partition()->label, partition.first.c_str()) == 0) ? "** active **" : "");
|
const char * mark = is_running && is_boot ? "** active, boot **" : is_running ? "** active **" : is_boot ? "** boot **" : "";
|
||||||
|
shell.printfln(" %s: v%s (%d KB%s) %s", partition.first.c_str(), partition.second.version.c_str(), partition.second.size, installed.c_str(), mark);
|
||||||
}
|
}
|
||||||
|
#ifndef EMSESP_STANDALONE
|
||||||
|
#ifdef EMSESP_HAS_RECOVERY
|
||||||
|
// 16MB boards: factory ("boot") holds the recovery app and is omitted from partition_info_
|
||||||
|
{
|
||||||
|
const esp_partition_t * factory = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
|
||||||
|
esp_app_desc_t desc;
|
||||||
|
if (OtaUpdater::isRecoveryPartition(factory, &desc)) {
|
||||||
|
const bool is_running = running && factory == running;
|
||||||
|
const bool is_boot = boot && factory == boot;
|
||||||
|
if (is_boot) {
|
||||||
|
boot_listed = true;
|
||||||
|
}
|
||||||
|
int size_kb = 0;
|
||||||
|
esp_image_metadata_t meta = {};
|
||||||
|
esp_partition_pos_t part_pos = {.offset = factory->address, .size = factory->size};
|
||||||
|
if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) {
|
||||||
|
size_kb = static_cast<int>(meta.image_len / 1024);
|
||||||
|
}
|
||||||
|
const char * mark = is_running && is_boot ? "** active, boot **" : is_running ? "** active **" : is_boot ? "** boot **" : "";
|
||||||
|
shell.printfln(" %s: Recovery v%s (%d KB) %s", factory->label, desc.version, size_kb, mark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// recovery/factory is omitted from partition_info_, still show it if it's the boot target
|
||||||
|
if (!boot_listed && boot_label[0] != '\0') {
|
||||||
|
shell.printfln(" %s: ** boot **", boot_label);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
// List all NVS values
|
// List all NVS values
|
||||||
#ifndef EMSESP_STANDALONE
|
#ifndef EMSESP_STANDALONE
|
||||||
shell.println(" NVS values:");
|
shell.println(" NVS values:");
|
||||||
|
|||||||
+2
-1
@@ -375,7 +375,8 @@ class System {
|
|||||||
static constexpr uint8_t HEALTHCHECK_NO_NETWORK = (1 << 1); // 2
|
static constexpr uint8_t HEALTHCHECK_NO_NETWORK = (1 << 1); // 2
|
||||||
static constexpr uint8_t HEALTHCHECK_RESET = (1 << 7); // 128
|
static constexpr uint8_t HEALTHCHECK_RESET = (1 << 7); // 128
|
||||||
|
|
||||||
// smallest .bin accepted as a firmware image, for both URL and web uploads - 1.6 MB
|
// smallest .bin accepted as a main firmware image, for both URL and web uploads - 1.6 MB
|
||||||
|
// recovery firmware uses OtaUpdater::MIN_RECOVERY_FIRMWARE_SIZE when the filename contains "recovery"
|
||||||
static constexpr size_t MIN_FIRMWARE_SIZE = 1677721;
|
static constexpr size_t MIN_FIRMWARE_SIZE = 1677721;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#define EMSESP_APP_VERSION "3.9.0-dev.16"
|
#define EMSESP_APP_VERSION "3.9.0-dev.17"
|
||||||
|
|||||||
@@ -134,13 +134,12 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
|
|||||||
root["temperature"] = (int)Helpers::transformNumFloat(EMSESP::system_.temperature(), 0, EMSESP::system_.fahrenheit() ? 2 : 0); // only 2 decimal places
|
root["temperature"] = (int)Helpers::transformNumFloat(EMSESP::system_.temperature(), 0, EMSESP::system_.fahrenheit() ? 2 : 0); // only 2 decimal places
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// check for a factory partition first
|
// factory partition is only on 16MB boards; recovery_version is empty until a recovery .bin is installed
|
||||||
const esp_partition_t * partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
|
const esp_partition_t * partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, nullptr);
|
||||||
root["has_loader"] = partition != NULL && partition != esp_ota_get_running_partition();
|
|
||||||
if (partition != NULL) {
|
if (partition != NULL) {
|
||||||
root["recovery_version"] = EMSESP::system_.recovery_version(); // empty if not installed
|
root["recovery_version"] = EMSESP::system_.recovery_version();
|
||||||
}
|
}
|
||||||
partition = esp_ota_get_next_update_partition(nullptr);
|
partition = esp_ota_get_next_update_partition(nullptr);
|
||||||
if (partition) {
|
if (partition) {
|
||||||
uint64_t buffer;
|
uint64_t buffer;
|
||||||
esp_partition_read(partition, 0, &buffer, 8);
|
esp_partition_read(partition, 0, &buffer, 8);
|
||||||
|
|||||||
Reference in New Issue
Block a user