crashmode

This commit is contained in:
proddy
2026-10-07 23:00:14 +02:00
parent 6411b6ffbd
commit 965d93661a
19 changed files with 600 additions and 273 deletions
+14 -15
View File
@@ -8,24 +8,23 @@ This guide covers testing on hardware of:
- the fallback to the recovery after repeated crashes;
- 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
Build everything:
```bash
pio run -e s3_16M_P -e crashtest_s3_16M_P -e recovery_s3_16M_P
pio run -e s_16M -e s_16M_P -e s_4M -e recovery_s_16M -e recovery_s_16M_P
pio run -e s_16M_P -e crashtest_s_16M_P -e recovery_s_16M_P
```
The firmware files are written to `build/firmware/`:
| File | What it is |
| --- | --- |
| `EMS-ESP-<version>-ESP32S3-16MB+.bin` | EMS-ESP |
| `EMS-ESP-CrashTest-<version>-ESP32S3-16MB+.bin` | crash test build |
| `EMS-ESP-Recovery-<version>-ESP32S3-16MB+.bin` | the recovery |
| `EMS-ESP-<version>-ESP32-16MB+.bin` | EMS-ESP |
| `EMS-ESP-CrashTest-<version>-ESP32-16MB+.bin` | crash test build |
| `EMS-ESP-Recovery-<version>-ESP32-16MB+.bin` | the recovery |
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.
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.
## 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:
- the **Recovery** row shows "not installed";
- 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.
2. Repeat, uploading the `.md5` first and then the `.bin`. The MD5 check should pass.
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.
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;
- LittleFS is mounted;
- 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.
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:
- there's **no** red reason box, because the restart was requested;
- `Recovery (boot)` has the "running" chip;
- 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.
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)
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.
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.
@@ -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:
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.
## 7. Network in the recovery
@@ -136,7 +135,7 @@ Optional, without a recovery installed:
| 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" |
| 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 |
| 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 |
@@ -153,4 +152,4 @@ Optional, without a recovery installed:
## 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.
+104
View File
@@ -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`.
+1 -1
View File
@@ -57,7 +57,7 @@
"terser": "^5.51.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.71.1",
"vite": "^8.3.2",
"vite": "^8.3.3",
"vite-plugin-imagemin": "^0.6.1"
},
"packageManager": "pnpm@12.9.1"
+21 -21
View File
@@ -214,7 +214,7 @@ importers:
version: 10.0.1(eslint@10.12.0(supports-color@10.2.2))
'@preact/preset-vite':
specifier: ^2.10.6
version: 2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@11.0.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':
specifier: ^6.0.2
version: 6.0.2(prettier@3.9.9)(supports-color@10.2.2)
@@ -258,11 +258,11 @@ importers:
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)
vite:
specifier: ^8.3.2
version: 8.3.2(@types/node@26.6.4)(terser@5.51.2)
specifier: ^8.3.3
version: 8.3.3(@types/node@26.6.4)(terser@5.51.2)
vite-plugin-imagemin:
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:
@@ -1338,8 +1338,8 @@ packages:
duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
electron-to-chromium@1.5.444:
resolution: {integrity: sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==}
electron-to-chromium@1.5.445:
resolution: {integrity: sha512-iX7o1nF+qNb3Bv5mLYgqfgF9Znh9e/8MKXdmbb8Gv2Q47OKIzTwEPD1oda1HrgEwYatTLJj0wQjHvIFt8Db53Q==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -3057,8 +3057,8 @@ packages:
peerDependencies:
vite: 5.x || 6.x || 7.x || 8.x
vite@8.3.2:
resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==}
vite@8.3.3:
resolution: {integrity: sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -3591,19 +3591,19 @@ snapshots:
'@popperjs/core@2.11.8': {}
'@preact/preset-vite@2.10.6(@babel/core@7.29.7(supports-color@10.2.2))(preact@11.0.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:
'@babel/core': 7.29.7(supports-color@10.2.2)
'@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
'@prefresh/vite': 2.4.12(preact@11.0.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
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.7(supports-color@10.2.2))
debug: 4.4.3(supports-color@10.2.2)
magic-string: 0.30.21
picocolors: 1.1.1
vite: 8.3.2(@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: 8.3.3(@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
transitivePeerDependencies:
- preact
@@ -3620,7 +3620,7 @@ snapshots:
'@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:
'@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
'@rollup/pluginutils': 4.2.1
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:
- supports-color
@@ -4005,7 +4005,7 @@ snapshots:
dependencies:
baseline-browser-mapping: 2.11.27
caniuse-lite: 1.0.30001814
electron-to-chromium: 1.5.444
electron-to-chromium: 1.5.445
node-releases: 2.0.57
update-browserslist-db: 1.3.3(browserslist@4.29.3)
@@ -4370,7 +4370,7 @@ snapshots:
duplexer3@0.1.5: {}
electron-to-chromium@1.5.444: {}
electron-to-chromium@1.5.445: {}
emoji-regex@10.6.0: {}
@@ -5982,7 +5982,7 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
vite-plugin-imagemin@0.6.1(supports-color@10.2.2)(vite@8.3.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:
'@types/imagemin': 7.0.1
'@types/imagemin-gifsicle': 7.0.4
@@ -6007,11 +6007,11 @@ snapshots:
imagemin-webp: 6.1.0
jpegtran-bin: 6.0.1
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:
- 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:
kolorist: 1.8.0
magic-string: 0.30.21
@@ -6019,9 +6019,9 @@ snapshots:
simple-code-frame: 1.3.0
source-map: 0.7.6
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:
lightningcss: 1.33.0
picomatch: 4.0.7
+2
View File
@@ -1,5 +1,6 @@
import { memo, useContext } from 'react';
import LogoutIcon from '@mui/icons-material/Logout';
import PersonIcon from '@mui/icons-material/Person';
import {
Avatar,
@@ -53,6 +54,7 @@ const UserProfileComponent = () => {
</Box>
<Divider />
<Button
startIcon={<LogoutIcon />}
sx={{ mt: 2 }}
variant="outlined"
color="primary"
@@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import SendIcon from '@mui/icons-material/Send';
import WarningIcon from '@mui/icons-material/Warning';
import {
Box,
@@ -500,6 +501,7 @@ const ApplicationSettings = () => {
</Grid>
<Grid>
<Button
startIcon={<SendIcon />}
sx={{ mt: 3 }}
variant="outlined"
color="primary"
+149 -88
View File
@@ -16,7 +16,6 @@ import {
DialogActions,
DialogContent,
DialogTitle,
Grid,
IconButton,
Table,
TableBody,
@@ -369,14 +368,18 @@ const InstallPartitionDialog = memo(
onClose: () => void;
onInstall: (partition: string) => void;
}) => {
const isRecovery = partition === 'boot';
return (
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
<DialogTitle>
{LL.INSTALL()} {LL.STORED_VERSIONS()}
{isRecovery ? LL.RECOVERY() : `${LL.INSTALL()} ${LL.STORED_VERSIONS()}`}
</DialogTitle>
<DialogContent dividers>
<Typography sx={{ mb: 2 }}>
{LL.INSTALL_VERSION(LL.INSTALL(), version)}
{isRecovery
? 'This will boot in recovery mode'
: LL.INSTALL_VERSION(LL.INSTALL(), version)}
</Typography>
</DialogContent>
<DialogActions>
@@ -390,12 +393,14 @@ const InstallPartitionDialog = memo(
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
startIcon={
isRecovery ? <PowerSettingsNewIcon /> : <WarningIcon color="warning" />
}
variant="outlined"
onClick={() => onInstall(partition)}
color="primary"
color={isRecovery ? 'success' : 'primary'}
>
{LL.INSTALL()}
{isRecovery ? LL.RESTART() : LL.INSTALL()}
</Button>
</DialogActions>
</Dialog>
@@ -665,77 +670,69 @@ const Version = () => {
{LL.THIS_VERSION()}
</Typography>
<Grid
container
direction="row"
<Box
sx={{
justifyContent: 'flex-start',
alignItems: 'baseline'
display: 'grid',
gridTemplateColumns: 'auto 1fr',
columnGap: 3,
rowGap: 0.75,
alignItems: 'center'
}}
>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.VERSION()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
minWidth: 0
}}
>
<Typography component="span">
{data.emsesp_version}
{data.build_flags && (
<Typography variant="caption">
&nbsp; &#40;{data.build_flags}&#41;
<Typography component="span" variant="caption">
&nbsp;({data.build_flags})
</Typography>
)}
</Typography>
<IconButton
size="small"
onClick={() => setPartitionVersionInfo(data.partition)}
aria-label={LL.FIRMWARE_VERSION_INFO()}
sx={{ p: 0.5 }}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
</Typography>
</Grid>
</Box>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.PLATFORM()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{platform}
<Typography variant="caption">
&nbsp; &#40;
<Box
sx={{
display: 'flex',
alignItems: 'center',
minWidth: 0
}}
>
<Typography component="span">{platform}</Typography>
<Typography
component="span"
variant="caption"
sx={{
display: 'inline-flex',
alignItems: 'center',
ml: 0.75
}}
>
(
{data.psram ? (
<CheckIcon
color="success"
sx={{
fontSize: '1.5em',
verticalAlign: 'middle'
}}
/>
<CheckIcon color="success" sx={{ fontSize: 16, mx: 0.25 }} />
) : (
<CloseIcon
color="error"
sx={{
fontSize: '1.5em',
verticalAlign: 'middle'
}}
/>
<CloseIcon color="error" sx={{ fontSize: 16, mx: 0.25 }} />
)}
PSRAM&#41;
PSRAM)
</Typography>
</Typography>
</Grid>
{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>
</Box>
</Box>
{internetLive ? (
<>
@@ -743,34 +740,54 @@ const Version = () => {
{LL.AVAILABLE_VERSION()}
</Typography>
<Grid
container
direction="row"
rowSpacing={1}
<Box
sx={{
justifyContent: 'flex-start',
alignItems: 'baseline'
display: 'grid',
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 }}>
<Typography color="secondary">{LL.STORED_VERSIONS()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
{otherPartitions.length > 0 && (
<>
<Typography
color="secondary"
sx={{ alignSelf: 'start', pt: 0.5 }}
>
{LL.STORED_VERSIONS()}
</Typography>
<Box
sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}
>
{otherPartitions.map((partition) => (
<Typography key={partition.partition} sx={{ mb: 1 }}>
<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 }} />
<InfoOutlinedIcon
color="primary"
sx={{ fontSize: 18 }}
/>
</IconButton>
<Button
sx={{ ml: 0 }}
variant="outlined"
size="small"
onClick={() =>
@@ -783,43 +800,87 @@ const Version = () => {
>
{LL.INSTALL()}
</Button>
</Typography>
</Box>
))}
</Grid>
</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>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{latestVersion?.version}
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
minWidth: 0
}}
>
<Typography component="span">{latestVersion?.version}</Typography>
<IconButton
size="small"
onClick={() => setShowVersionInfo(1)}
aria-label={LL.FIRMWARE_VERSION_INFO()}
sx={{ p: 0.5 }}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
{showButtons(false)}
</Typography>
</Grid>
</Box>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.DEVELOPMENT()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{latestDevVersion?.version}
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
minWidth: 0
}}
>
<Typography component="span">{latestDevVersion?.version}</Typography>
<IconButton
size="small"
onClick={() => setShowVersionInfo(2)}
aria-label={LL.FIRMWARE_VERSION_INFO()}
sx={{ p: 0.5 }}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
{showButtons(true)}
</Typography>
</Grid>
</Grid>
</Box>
</Box>
</>
) : (
<Typography sx={{ mt: 2 }} color="warning">
-1
View File
@@ -51,7 +51,6 @@ export interface SystemStatus {
free_caps: number;
model: string;
board: string;
has_loader: boolean;
recovery_version?: string;
has_partition: boolean;
partitions: {
+72 -18
View File
@@ -3,6 +3,10 @@
#include <Update.h>
#include <esp_app_format.h>
#include <esp_image_format.h>
#ifdef OTA_UPDATER_ANY_PARTITION
#include <esp_heap_caps.h>
#include <esp_partition.h>
#endif
#include <cstddef>
@@ -19,9 +23,15 @@ OtaUpdater::FileType OtaUpdater::classify(const String & filename, size_t filesi
if (extension == "bin" && filename.endsWith("littlefs.bin")) {
return FileType::FILESYSTEM;
}
if (extension == "bin" && filesize >= min_firmware_size) {
if (extension == "bin") {
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") {
return FileType::JSON;
}
@@ -114,6 +124,19 @@ bool OtaUpdater::hasMd5() const {
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) {
_md5_applied = false;
_error = "";
@@ -126,13 +149,14 @@ bool OtaUpdater::beginFirmware(size_t filesize, const esp_partition_t * target,
_error = "Firmware does not fit in the partition";
return false;
}
const esp_err_t err = esp_ota_begin(target, OTA_WITH_SEQUENTIAL_WRITES, &_handle);
if (err != ESP_OK) {
_error = esp_err_to_name(err);
_dram = static_cast<uint8_t *>(heap_caps_malloc(DRAM_CHUNK, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
if (_dram == nullptr) {
_error = "No internal RAM for OTA buffer";
return false;
}
_target = target;
_written = 0;
_erased = 0;
_set_boot = set_boot;
if (hasMd5()) {
_md5Builder.begin();
@@ -166,15 +190,39 @@ bool OtaUpdater::beginFilesystem() {
bool OtaUpdater::write(const uint8_t * data, size_t len) {
#ifdef OTA_UPDATER_ANY_PARTITION
if (_target != nullptr) {
const esp_err_t err = esp_ota_write(_handle, data, len);
while (len) {
const size_t n = len < DRAM_CHUNK ? len : DRAM_CHUNK;
memcpy(_dram, data, n);
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) {
_error = esp_err_to_name(err);
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(data, len);
_md5Builder.add(_dram, n);
}
_written += n;
data += n;
len -= n;
}
_written += len;
return true;
}
#endif
@@ -185,26 +233,31 @@ bool OtaUpdater::end() {
#ifdef OTA_UPDATER_ANY_PARTITION
if (_target != nullptr) {
const esp_partition_t * target = _target;
_target = nullptr;
const bool set_boot = _set_boot;
releaseTarget();
if (_md5_applied) {
_md5Builder.calculate();
if (strcmp(_md5Builder.toString().c_str(), _md5.data()) != 0) {
esp_ota_abort(_handle);
_error = "MD5 check failed";
return false;
}
}
// esp_ota_end() validates the image
esp_err_t err = esp_ota_end(_handle);
if (err == ESP_OK && _set_boot) {
err = esp_ota_set_boot_partition(target);
}
esp_image_metadata_t meta = {};
esp_partition_pos_t part_pos = {.offset = target->address, .size = target->size};
esp_err_t err = esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta);
if (err != ESP_OK) {
_error = esp_err_to_name(err);
setError(err);
return false;
}
if (set_boot) {
err = esp_ota_set_boot_partition(target);
if (err != ESP_OK) {
setError(err);
return false;
}
}
return true;
}
#endif
@@ -214,12 +267,13 @@ bool OtaUpdater::end() {
void OtaUpdater::abort() {
#ifdef OTA_UPDATER_ANY_PARTITION
if (_target != nullptr) {
esp_ota_abort(_handle);
_target = nullptr;
releaseTarget();
return;
}
#endif
if (Update.isRunning()) {
Update.abort();
}
}
size_t OtaUpdater::progress() const {
+16 -2
View File
@@ -16,11 +16,16 @@ class OtaUpdater {
public:
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);
// 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";
// 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
static bool isCompatibleFirmware(const uint8_t * data, size_t len);
@@ -64,12 +69,21 @@ class OtaUpdater {
const char * _error = "";
#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;
esp_ota_handle_t _handle = 0;
size_t _written = 0;
size_t _erased = 0;
bool _set_boot = true;
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
};
+5 -5
View File
@@ -116,8 +116,8 @@ let system_status = {
psram: true,
psram_size: 8189,
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,
partitions: [
{
@@ -152,12 +152,12 @@ let system_status = {
let DEV_VERSION_IS_UPGRADEABLE: boolean;
let STABLE_VERSION_IS_UPGRADEABLE: boolean;
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';
// scenarios for testing versioning
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 = 0; // on latest stable, or switch to dev
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 = 3; // upgrade dev to latest, or switch to stable
// let version_test = 4; // downgrade to an older dev, or switch back to stable
+2 -2
View File
@@ -177,8 +177,8 @@ build_flags =
; 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
; it switches to the recovery firmware. Without one it keeps crashing until reflashed over serial
[env:crashtest_s3_16M_P]
extends = env:s3_16M_P
[env:crashtest_s_16M_P]
extends = env:s_16M_P
build_flags =
${espressif32_base_16M.build_flags}
-D EMSESP_CRASH_TEST=1
+45 -15
View File
@@ -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
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
# 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
# create the filesystem
~/.platformio/penv/bin/python build_littlefs.py data $BUILD/littlefs.bin
# create the filesystem, in case it's needed
~/.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
$ESPTOOL --chip esp32 --port $PORT --baud 460800 \
--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 \
# bootloader, partitions, otadata, recovery, app, filesystem; then boot
$ESPTOOL --chip esp32 --port $PORT --baud $BAUD \
--before default-reset --after hard-reset \
write-flash -z --flash-mode dio --flash-freq 80m --flash-size detect \
0x1000 $BUILD/bootloader.bin \
0x8000 $BUILD/partitions.bin \
0x1000 $BUILD_BIN/bootloader.bin \
0x8000 $BUILD_BIN/partitions.bin \
0xe000 $BOOT_APP0 \
0x490000 $BUILD/firmware.bin
0x10000 $RECOVERY_BIN/firmware.bin \
0x490000 $BUILD_BIN/firmware.bin \
0xdf0000 $BUILD_BIN/littlefs.bin
+26 -14
View File
@@ -8,7 +8,8 @@ UploadFileService::UploadFileService(AsyncWebServer * server, SecurityManager *
: _securityManager(securityManager)
, _is_firmware(false)
, _is_filesystem(false)
, _is_recovery(false) {
, _is_recovery(false)
, _response_sent(false) {
server->on(
UPLOAD_FILE_PATH,
HTTP_POST,
@@ -34,6 +35,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
_is_firmware = false;
_is_filesystem = false;
_is_recovery = false;
_response_sent = false;
switch (OtaUpdater::classify(filename, filesize, emsesp::System::MIN_FIRMWARE_SIZE)) {
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
} else {
emsesp::EMSESP::logger().err("OTA begin failed: %s", _ota.errorString());
handleError(request, 507); // failed to begin, send an error response Insufficient Storage
return;
}
@@ -147,6 +150,19 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
}
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");
// did we just complete uploading a json file?
@@ -217,26 +233,25 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
}
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 (request->_tempObject) {
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
AsyncWebServerResponse * response = request->beginResponse(code);
request->send(response);
emsesp::EMSESP::logger().info("Upload error: %d", code);
emsesp::EMSESP::system_.uart_init(); // re-enable UART
_ota.abort();
// check for invalid extension and immediately kill the connection, which will throw an error
// that is caught by the web code. Unfortunately the http error code is not sent to the client on fast network connections
// 406: the client sent a file we won't accept. Close now so the browser fails fast.
// Other codes wait for uploadComplete() to send, so we don't destroy the request mid-stream.
if (code == 406) {
request->send(request->beginResponse(code));
_response_sent = true;
request->client()->close();
_is_firmware = false;
_is_filesystem = false;
_is_recovery = false;
_ota.abort();
}
// 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::system_.uart_init(); // re-enable UART
#ifdef EMSESP_HAS_RECOVERY
if (_is_recovery) {
// the client hung up before the upload completed
_ota.abort();
if (_is_recovery || _is_firmware || _is_filesystem) {
emsesp::EMSESP::system_.systemStatus(emsesp::SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
}
#endif
_is_firmware = false;
_is_filesystem = false;
+1
View File
@@ -22,6 +22,7 @@ class UploadFileService {
bool _is_firmware;
bool _is_filesystem;
bool _is_recovery; // firmware for the factory partition, which doesn't change the boot partition
bool _response_sent;
OtaUpdater _ota;
void handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final);
+55 -6
View File
@@ -573,8 +573,12 @@ void System::recovery_installed() {
if (factory != nullptr) {
char c[20];
snprintf(c, sizeof(c), "d_%s", factory->label);
if (EMSESP::nvs_.isKey(factory->label)) {
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
}
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;
}
#ifdef EMSESP_HAS_RECOVERY
if (partition->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY) {
EMSESP::nvs_.putString(EMSESP_NVS_RECOVERY_REASON, "request");
}
#endif
// initiate the restart
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED);
return true;
@@ -1323,6 +1333,16 @@ void System::show_system(uuid::console::Shell & shell) {
shell.printfln(" [total %d]", available.size());
// List all partitions and their version info
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_) {
if (partition.second.version.empty()) {
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));
installed = std::string(", installed on ") + time_string;
}
shell.printfln(" %s: v%s (%d KB%s) %s",
partition.first.c_str(),
partition.second.version.c_str(),
partition.second.size,
installed.c_str(),
(strcmp(esp_ota_get_running_partition()->label, partition.first.c_str()) == 0) ? "** active **" : "");
const bool is_running = strcmp(running_label, partition.first.c_str()) == 0;
const bool is_boot = strcmp(boot_label, partition.first.c_str()) == 0;
if (is_boot) {
boot_listed = true;
}
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
#ifndef EMSESP_STANDALONE
shell.println(" NVS values:");
+2 -1
View File
@@ -375,7 +375,8 @@ class System {
static constexpr uint8_t HEALTHCHECK_NO_NETWORK = (1 << 1); // 2
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;
private:
+1 -1
View File
@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.9.0-dev.16"
#define EMSESP_APP_VERSION "3.9.0-dev.17"
+2 -3
View File
@@ -134,11 +134,10 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) {
root["temperature"] = (int)Helpers::transformNumFloat(EMSESP::system_.temperature(), 0, EMSESP::system_.fahrenheit() ? 2 : 0); // only 2 decimal places
#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);
root["has_loader"] = partition != NULL && partition != esp_ota_get_running_partition();
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);
if (partition) {