frugal-iot-client 2.0.5 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin.js +97 -59
- package/base/boards.json +6 -6
- package/base/partitions/{frugal_16mb.bin → default_16MB.bin} +0 -0
- package/base/partitions/default_16MB.csv +7 -0
- package/base/partitions/{frugal_8mb.bin → default_8MB.bin} +0 -0
- package/base/partitions/default_8MB.csv +7 -0
- package/cards.js +9 -0
- package/core.js +28 -12
- package/docs/FLASH_PLAN.md +53 -48
- package/flash.js +28 -8
- package/frugaliot.css +5 -1
- package/package.json +1 -1
- package/test/cardui.test.js +10 -0
- package/test/fixtures/config.json +189 -2
- package/test/flash.test.js +86 -1
- package/test/mock.js +13 -0
- package/test/page.test.js +30 -0
- package/base/partitions/frugal_16mb.csv +0 -8
- package/base/partitions/frugal_8mb.csv +0 -8
package/docs/FLASH_PLAN.md
CHANGED
|
@@ -55,7 +55,7 @@ So an ESP32 board needs three extra regions that are **independent of your sketc
|
|
|
55
55
|
| Piece | Depends on | Per-app? |
|
|
56
56
|
|---|---|---|
|
|
57
57
|
| `bootloader.bin` | chip + flash mode/freq | No |
|
|
58
|
-
| `partitions.bin` | partition scheme (`min_spiffs`, `
|
|
58
|
+
| `partitions.bin` | partition scheme (`min_spiffs`, `default_8MB`, …) | No |
|
|
59
59
|
| `boot_app0.bin` | nothing — fixed 8192-byte blob | No |
|
|
60
60
|
| `firmware.bin` | your app | Yes — this is what the OTA tab already stores |
|
|
61
61
|
|
|
@@ -194,18 +194,25 @@ This can be decided with high confidence by reading the flash back before writin
|
|
|
194
194
|
exposes `readFlash()`, and with the stub loaded, reading ~7 KB is fast.
|
|
195
195
|
|
|
196
196
|
1. Read 3 KB at `0x8000`. Absent partition-table magic (`AA 50`) → no valid layout → **provision**.
|
|
197
|
-
2. Parse the on-device table and check the **app slot only** — `app0` at `0x10000
|
|
198
|
-
|
|
197
|
+
2. Parse the on-device table and check the **app slot's position only** — `app0` at `0x10000`.
|
|
198
|
+
Elsewhere → the board is laid out for something else → **provision**. Its size is not compared
|
|
199
|
+
here; it becomes the limit the image is checked against once one is chosen (see the refusals).
|
|
199
200
|
3. Read 4 bytes at `chip.BOOTLOADER_FLASH_OFFSET`. Not `0xE9` → **provision**.
|
|
200
201
|
4. Read the first 4 KB of the on-device FS partition (offset from *its* table, not ours) and look for
|
|
201
202
|
the littlefs superblock magic — the ASCII string `littlefs`. Present → a filesystem exists worth
|
|
202
203
|
preserving → **app-only**.
|
|
203
204
|
|
|
204
|
-
Comparing only the app slot rather than the whole table is deliberate
|
|
205
|
-
|
|
206
|
-
perfectly safe to update app-only. That correctly covers a board
|
|
207
|
-
this tool, or flashed by hand from PlatformIO with stock
|
|
208
|
-
its config.
|
|
205
|
+
Comparing only where the app slot starts, rather than the whole table, is deliberate. An app image
|
|
206
|
+
runs from any slot it fits in, so a board laid out differently from our scheme — a bigger app slot,
|
|
207
|
+
a filesystem elsewhere — is still perfectly safe to update app-only. That correctly covers a board
|
|
208
|
+
provisioned by an earlier version of this tool, or flashed by hand from PlatformIO with any stock
|
|
209
|
+
scheme (`min_spiffs`, `default`, `default_8MB`), instead of needlessly wiping its config.
|
|
210
|
+
|
|
211
|
+
**App-only also resets otadata.** A board that has taken a wireless OTA update may be booting from
|
|
212
|
+
`app1`, and would then ignore the app just written to `app0`. So the app-only path writes
|
|
213
|
+
`boot_app0.bin` at the otadata offset from the *board's own* table — exactly what a PlatformIO
|
|
214
|
+
upload does — which selects `app0` without touching the filesystem. If the table could not be read,
|
|
215
|
+
otadata is left alone and the log says the old firmware may keep booting.
|
|
209
216
|
|
|
210
217
|
That resolves the "is it virgin?" question definitively. The one residue is step 4's blind spot: a
|
|
211
218
|
matching partition table plus a valid LittleFS **belonging to a different project**. Flash contents
|
|
@@ -272,42 +279,40 @@ base/
|
|
|
272
279
|
partitions/
|
|
273
280
|
min_spiffs.bin 3 KB — chip-independent (Arduino stock, 4 MB)
|
|
274
281
|
min_spiffs.csv — provenance for the above
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
282
|
+
default_8MB.bin — Arduino stock, see below
|
|
283
|
+
default_8MB.csv
|
|
284
|
+
default_16MB.bin
|
|
285
|
+
default_16MB.csv
|
|
279
286
|
```
|
|
280
287
|
|
|
281
288
|
Target chip set: **ESP8266** (no base needed at all), **ESP32-C3**, **ESP32-S2**, **ESP32-S3**.
|
|
282
289
|
`min_spiffs` for the smaller chips, per Frugal IoT convention.
|
|
283
290
|
|
|
284
|
-
### Partition schemes:
|
|
285
|
-
|
|
286
|
-
Bigger boards get the *same* app partitions as a 4 MB board, with the surplus going to the
|
|
287
|
-
filesystem. Arduino's stock large-flash schemes do the opposite (`default_8MB` gives 3.1 MB app
|
|
288
|
-
slots and only 1.5 MB of FS), so these are custom tables.
|
|
289
|
-
|
|
290
|
-
Starting from stock `min_spiffs` (4 MB), which is unchanged:
|
|
291
|
+
### Partition schemes: the stock tables PlatformIO uses
|
|
291
292
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
| `app0` | app/ota_0 | `0x10000` | `0x1E0000` | `0x1E0000` | `0x1E0000` |
|
|
297
|
-
| `app1` | app/ota_1 | `0x1F0000` | `0x1E0000` | `0x1E0000` | `0x1E0000` |
|
|
298
|
-
| `spiffs` | data/spiffs | `0x3D0000` | `0x20000` (128 KB) | `0x420000` (4.125 MB) | `0xC20000` (12.125 MB) |
|
|
299
|
-
| `coredump` | data/coredump | *varies* | `0x3F0000` | `0x7F0000` | `0xFF0000` |
|
|
293
|
+
Every scheme is Arduino's own, unmodified, so a board provisioned here has exactly the table a
|
|
294
|
+
PlatformIO upload of the matching env writes. That is the point: a USB upload from PlatformIO onto a
|
|
295
|
+
board provisioned here — or the reverse — rewrites the table with an identical one, and the
|
|
296
|
+
filesystem (with the board's WiFi and MQTT config) stays where it is.
|
|
300
297
|
|
|
301
|
-
|
|
302
|
-
|
|
298
|
+
| Partition | Type/Subtype | `min_spiffs` (4 MB) | `default_8MB` | `default_16MB` |
|
|
299
|
+
|---|---|---|---|---|
|
|
300
|
+
| `nvs` | data/nvs | `0x9000` `0x5000` | `0x9000` `0x5000` | `0x9000` `0x5000` |
|
|
301
|
+
| `otadata` | data/ota | `0xe000` `0x2000` | `0xe000` `0x2000` | `0xe000` `0x2000` |
|
|
302
|
+
| `app0` | app/ota_0 | `0x10000` `0x1E0000` | `0x10000` `0x330000` | `0x10000` `0x640000` |
|
|
303
|
+
| `app1` | app/ota_1 | `0x1F0000` `0x1E0000` | `0x340000` `0x330000` | `0x650000` `0x640000` |
|
|
304
|
+
| `spiffs` | data/spiffs | `0x3D0000` `0x20000` | `0x670000` `0x180000` | `0xC90000` `0x360000` |
|
|
305
|
+
| `coredump` | data/coredump | `0x3F0000` `0x10000` | `0x7F0000` `0x10000` | `0xFF0000` `0x10000` |
|
|
303
306
|
|
|
304
|
-
**
|
|
307
|
+
**What is relied on:** `app0` is at `0x10000` in every scheme, so the app-only path always writes
|
|
308
|
+
there. Its size varies, and an image fits any slot it is no bigger than — so the same
|
|
309
|
+
`firmware.bin` from a 4 MB build flashes to an 8 or 16 MB board, and a wireless OTA moves it between
|
|
310
|
+
`app0` and `app1` regardless. The image is checked against the slot actually on the board.
|
|
305
311
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
about existing builds needs revisiting.
|
|
312
|
+
An earlier version of this plan used custom `frugal_8mb`/`frugal_16mb` tables (`min_spiffs` app
|
|
313
|
+
slots, all surplus to LittleFS). They were dropped because the firmware's PlatformIO envs build for
|
|
314
|
+
the board default, and a PlatformIO upload onto a board provisioned with a different table moves the
|
|
315
|
+
filesystem and loses its config.
|
|
311
316
|
|
|
312
317
|
**Keep the label `spiffs`, even though the content is LittleFS.** Arduino's LittleFS mounts by
|
|
313
318
|
partition label, defaulting to `"spiffs"`
|
|
@@ -315,10 +320,9 @@ partition label, defaulting to `"spiffs"`
|
|
|
315
320
|
Renaming the partition to `littlefs` would stop the firmware mounting it unless `begin()` is also
|
|
316
321
|
changed. Not worth the coupling.
|
|
317
322
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
`.pio/build/<env>/partitions.bin` from a build configured with that CSV.
|
|
323
|
+
The `.csv` files are copied from the core's `tools/partitions/`, and the `.bin` generated from them
|
|
324
|
+
with the core's `tools/gen_esp32part.py` — or harvested from `.pio/build/<env>/partitions.bin` of a
|
|
325
|
+
build using that scheme.
|
|
322
326
|
|
|
323
327
|
`boards.json` maps a detected `<chip>-<flashsize>` to the parts to use, so lookup after detection is
|
|
324
328
|
a single key:
|
|
@@ -329,8 +333,8 @@ a single key:
|
|
|
329
333
|
"ESP32-C3-4MB": "min_spiffs",
|
|
330
334
|
"ESP32-S2-4MB": "min_spiffs",
|
|
331
335
|
"ESP32-S3-4MB": "min_spiffs",
|
|
332
|
-
"ESP32-S3-8MB": "
|
|
333
|
-
"ESP32-S3-16MB": "
|
|
336
|
+
"ESP32-S3-8MB": "default_8MB",
|
|
337
|
+
"ESP32-S3-16MB": "default_16MB"
|
|
334
338
|
},
|
|
335
339
|
"bootloaderByChip": {
|
|
336
340
|
"ESP32-C3": "bootloader/esp32c3.bin",
|
|
@@ -380,8 +384,8 @@ support without touching the client code:
|
|
|
380
384
|
|
|
381
385
|
1. Build the firmware for the board.
|
|
382
386
|
2. If the **chip** is new, copy `bootloader.bin` to `base/bootloader/<chip>.bin`.
|
|
383
|
-
3. If the **flash size** is new,
|
|
384
|
-
|
|
387
|
+
3. If the **flash size** is new, copy the core's stock CSV for it — the one the board's PlatformIO
|
|
388
|
+
env uses — and commit both it and the generated `.bin` to `base/partitions/`.
|
|
385
389
|
4. Add one `"<CHIP>-<SIZE>": "<scheme>"` line to `schemeByChipAndSize`.
|
|
386
390
|
|
|
387
391
|
`boot_app0.bin` never needs touching, and the app binary never needs rebuilding for a different flash
|
|
@@ -397,8 +401,10 @@ All four are **refusals, not warnings** — a bad combination here means a board
|
|
|
397
401
|
2. App image: byte 0 == `0xE9`, and for ESP32-family images `chip_id` (u16 LE at offset 12) ==
|
|
398
402
|
`esploader.chip.IMAGE_CHIP_ID` (0=ESP32, 2=S2, 5=C3, 9=S3, 12=C2, 13=C6, 16=H2 — the ROM classes
|
|
399
403
|
expose it). This catches the classic "C3 build onto a D1 Mini".
|
|
400
|
-
3. App byte length ≤ app-slot size
|
|
401
|
-
|
|
404
|
+
3. App byte length ≤ app-slot size — the board's own slot for an app-only write, the base scheme's
|
|
405
|
+
when provisioning. Catches a `default_8MB` build too big for a `min_spiffs` board. A too-big image
|
|
406
|
+
is refused rather than provisioned automatically, since provisioning erases the board's config;
|
|
407
|
+
the message says when "Provision instead" would make it fit.
|
|
402
408
|
4. `detectFlashSize()` must produce a `<chip>-<flashsize>` key present in `schemeByChipAndSize` — and
|
|
403
409
|
if the size came from the `"4MB"` fallback, require confirmation (and on an S3, warn about octal
|
|
404
410
|
flash).
|
|
@@ -847,9 +853,8 @@ Nothing structural — the flow works but is a little unintuitive. Known rough e
|
|
|
847
853
|
fact rather than a guess.
|
|
848
854
|
- Confirm `writeFlash` tolerates an omitted `calculateMD5Hash` (see Client change 7).
|
|
849
855
|
- **Ship 4 MB only at first.** Larger boards are still more expensive and not seen in practice, so
|
|
850
|
-
`min_spiffs` is the only scheme that needs to work on day one.
|
|
851
|
-
|
|
852
|
-
tested by someone holding the hardware rather than committed untested — an unlisted
|
|
856
|
+
`min_spiffs` is the only scheme that needs to work on day one. `default_8MB` / `default_16MB` are
|
|
857
|
+
the stock tables, but provisioning with them is untested on real hardware — an unlisted
|
|
853
858
|
`<chip>-<flashsize>` already fails cleanly as "unsupported board".
|
|
854
859
|
- Whether an ESP32-S2 `bootloader.bin` is worth harvesting up front, given `ARDUINO_LOLIN_S2_MINI`
|
|
855
860
|
appears in the firmware but may not be in active use.
|
package/flash.js
CHANGED
|
@@ -13,9 +13,11 @@ const PartitionTableOffset = 0x8000;
|
|
|
13
13
|
const PartitionTableMagic = 0x50aa; // AA 50 on the wire
|
|
14
14
|
const PartitionType = {app: 0x00, data: 0x01};
|
|
15
15
|
const PartitionSubtype = {factory: 0x00, ota0: 0x10, otadata: 0x00, spiffs: 0x82, littlefs: 0x83};
|
|
16
|
-
// Every
|
|
16
|
+
// Every Arduino scheme keeps app0 here; only its size varies, and an image fits any slot it is no
|
|
17
|
+
// bigger than - see FLASH_PLAN.md
|
|
17
18
|
const AppPartitionOffset = 0x10000;
|
|
18
|
-
|
|
19
|
+
// The smallest app0 we provision (min_spiffs) - the limit when the board's own table is unreadable
|
|
20
|
+
const AppPartitionSizeMin = 0x1e0000;
|
|
19
21
|
const ImageMagic = 0xe9;
|
|
20
22
|
const LittleFsMagic = [0x6c, 0x69, 0x74, 0x74, 0x6c, 0x65, 0x66, 0x73]; // "littlefs" in the superblock
|
|
21
23
|
const FlashBaudRates = [921600, 460800, 230400, 115200];
|
|
@@ -238,7 +240,6 @@ class MqttFlash extends HTMLElementExtended {
|
|
|
238
240
|
dev.wholeImage = true; // ESP8266 firmware.bin is a complete image at 0x0
|
|
239
241
|
dev.provision = false; // and does not overlap the filesystem
|
|
240
242
|
dev.appAddress = 0x0;
|
|
241
|
-
dev.appLimit = null;
|
|
242
243
|
return;
|
|
243
244
|
}
|
|
244
245
|
const scheme = boards.schemeByChipAndSize[`${dev.chipName}-${dev.flashSize}`];
|
|
@@ -255,11 +256,14 @@ class MqttFlash extends HTMLElementExtended {
|
|
|
255
256
|
partitions: {url: partUrl(boards.partitionsByScheme[scheme]), address: boards.partitionTableOffset},
|
|
256
257
|
otadata: {url: partUrl(boards.otadata), address: null}, // from the partition table below
|
|
257
258
|
};
|
|
259
|
+
// Fetched now rather than at flash time, so the size check knows the base's app slot
|
|
260
|
+
const baseApp = partitionApp(partitionsParse(await fetchBytes(dev.base.partitions.url)));
|
|
261
|
+
dev.base.appSize = baseApp ? baseApp.size : AppPartitionSizeMin;
|
|
258
262
|
// The filesystem partition is deliberately left erased - the firmware's LittleFS.begin(true)
|
|
259
263
|
// formats it on first boot. See FLASH_PLAN.md: it only appeared unable to when we were writing a
|
|
260
264
|
// bootloader from the wrong framework.
|
|
261
265
|
dev.appAddress = AppPartitionOffset;
|
|
262
|
-
dev.
|
|
266
|
+
dev.onDeviceAppSize = AppPartitionSizeMin;
|
|
263
267
|
await this.deviceInspect();
|
|
264
268
|
}
|
|
265
269
|
// Returns null rather than stalling, so a board that will not answer the stub's read command does
|
|
@@ -287,6 +291,7 @@ class MqttFlash extends HTMLElementExtended {
|
|
|
287
291
|
dev.inspected = false;
|
|
288
292
|
dev.provision = false;
|
|
289
293
|
this.logLine("Could not inspect the board - assuming it is already provisioned. Use \"Provision instead\" if this is a new board.");
|
|
294
|
+
this.logLine("Without its partition table otadata cannot be reset, so a board that has taken an OTA update may keep booting the old firmware.");
|
|
290
295
|
return;
|
|
291
296
|
}
|
|
292
297
|
dev.inspected = true;
|
|
@@ -295,8 +300,13 @@ class MqttFlash extends HTMLElementExtended {
|
|
|
295
300
|
const app = partitionApp(onDeviceParts);
|
|
296
301
|
if (!onDeviceParts.length) reasons.push("no partition table");
|
|
297
302
|
else if (!app) reasons.push("no app partition");
|
|
298
|
-
// Only the app slot has to match -
|
|
299
|
-
|
|
303
|
+
// Only the app slot's position has to match - its size is checked against the image at flash
|
|
304
|
+
// time, and a differently laid out filesystem is still safe to keep
|
|
305
|
+
else if (app.offset !== AppPartitionOffset) reasons.push("app partition not at 0x10000");
|
|
306
|
+
if (app) dev.onDeviceAppSize = app.size;
|
|
307
|
+
// Only an ota_0 slot is selected through otadata - a factory app boots without it
|
|
308
|
+
const otadata = partitionOtadata(onDeviceParts);
|
|
309
|
+
if (app && app.subtype === PartitionSubtype.ota0 && otadata) dev.onDeviceOtadataOffset = otadata.offset;
|
|
300
310
|
// Worth always logging - a layout that disagrees with the base explains most boot failures
|
|
301
311
|
if (onDeviceParts.length) {
|
|
302
312
|
this.logLine("On-device partition table:");
|
|
@@ -359,8 +369,15 @@ class MqttFlash extends HTMLElementExtended {
|
|
|
359
369
|
if (chipId !== this.state.esploader.chip.IMAGE_CHIP_ID) {
|
|
360
370
|
throw new Error(`${this.state.app.name} was built for chip id ${chipId}, but this board is ${dev.chipName} (chip id ${this.state.esploader.chip.IMAGE_CHIP_ID})`);
|
|
361
371
|
}
|
|
362
|
-
|
|
363
|
-
|
|
372
|
+
// Not provisioned automatically: that erases the board's configuration, so it is the user's call
|
|
373
|
+
if (dev.provision && bytes.length > dev.base.appSize) {
|
|
374
|
+
throw new Error(`${this.state.app.name} is ${bytesToSizeString(bytes.length)}, larger than the ${bytesToSizeString(dev.base.appSize)} app partition of ${dev.scheme}`);
|
|
375
|
+
}
|
|
376
|
+
if (!dev.provision && bytes.length > dev.onDeviceAppSize) {
|
|
377
|
+
const fix = bytes.length <= dev.base.appSize
|
|
378
|
+
? ` - "Provision instead" would lay it out as ${dev.scheme}, but erases its configuration`
|
|
379
|
+
: "";
|
|
380
|
+
throw new Error(`${this.state.app.name} is ${bytesToSizeString(bytes.length)}, larger than the ${bytesToSizeString(dev.onDeviceAppSize)} app partition on this board${fix}`);
|
|
364
381
|
}
|
|
365
382
|
}
|
|
366
383
|
if (dev.flashSizeGuessed && !this.state.confirmed) {
|
|
@@ -381,6 +398,9 @@ class MqttFlash extends HTMLElementExtended {
|
|
|
381
398
|
files.push({data: partitions, address: dev.base.partitions.address});
|
|
382
399
|
files.push({data: otadata, address: otadataPart.offset});
|
|
383
400
|
dev.verifyAddress = dev.base.partitions.address; // read back below to confirm the write landed
|
|
401
|
+
} else if (!dev.wholeImage && dev.onDeviceOtadataOffset != null) {
|
|
402
|
+
// After an OTA update the board boots app1, so the app written to app0 would never run
|
|
403
|
+
files.push({data: await fetchBytes(dev.base.otadata.url), address: dev.onDeviceOtadataOffset});
|
|
384
404
|
}
|
|
385
405
|
files.push({data: this.state.app.bytes, address: dev.appAddress});
|
|
386
406
|
return files;
|
package/frugaliot.css
CHANGED
|
@@ -193,6 +193,8 @@ div.mqtt-text {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
@keyframes fi-spin { to { transform: rotate(360deg); } }
|
|
197
|
+
|
|
196
198
|
/* Related to authentication */
|
|
197
199
|
.mqtt-admin {
|
|
198
200
|
.pseudolink {
|
|
@@ -201,7 +203,9 @@ div.mqtt-text {
|
|
|
201
203
|
cursor: pointer;
|
|
202
204
|
}
|
|
203
205
|
/* The \27f3 beside a heading: still a link, but underlining a glyph reads as a typo. */
|
|
204
|
-
.refresh { text-decoration: none; }
|
|
206
|
+
.refresh { text-decoration: none; display: inline-block; margin-left: 0.3em; }
|
|
207
|
+
/* Spins until the fetch it started replies - on a slow link a still glyph got clicked again and again. */
|
|
208
|
+
.refresh.busy { animation: fi-spin 1s linear infinite; cursor: progress; }
|
|
205
209
|
/* An OTA row's glyphs: the padding is the whole hit area, so a mis-tap cannot reach delete. */
|
|
206
210
|
.otaicon { text-decoration: none; padding: 0 0.3em; }
|
|
207
211
|
.message {margin: 0.5em; font-weight: bold; font-size: x-large; }
|
package/package.json
CHANGED
package/test/cardui.test.js
CHANGED
|
@@ -237,6 +237,16 @@ describe('back mode', () => {
|
|
|
237
237
|
card.remove();
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
test('a control that is not a hysteresis still shows its topics to wire', () => {
|
|
241
|
+
const { card } = cardFor('display-control', 'esp32-48ed20', { mode: 'back', at: T0 });
|
|
242
|
+
const titles = [...card.querySelectorAll('.fi-section__title')].map((h) => h.textContent);
|
|
243
|
+
assert.ok(titles.includes('LCD SHT'), `no LCD section in ${titles}`);
|
|
244
|
+
for (const leaf of ['temperature', 'humidity', 'message']) {
|
|
245
|
+
assert.ok(card.querySelector(`[topic$="control_lcd_ht/${leaf}"]`), `no widget for ${leaf}`);
|
|
246
|
+
}
|
|
247
|
+
card.remove();
|
|
248
|
+
});
|
|
249
|
+
|
|
240
250
|
test('nothing but Advanced is collapsed', () => {
|
|
241
251
|
const { card } = cardFor('control-wired', 'esp8266-fb94bb', { mode: 'back', at: T0 });
|
|
242
252
|
assert.equal(card.querySelectorAll('details').length, 1);
|
|
@@ -206,6 +206,141 @@
|
|
|
206
206
|
}
|
|
207
207
|
]
|
|
208
208
|
},
|
|
209
|
+
"control_lcd_ht": {
|
|
210
|
+
"summary": false,
|
|
211
|
+
"name": "LCD SHT",
|
|
212
|
+
"topics": [
|
|
213
|
+
{
|
|
214
|
+
"leaf": "temperature",
|
|
215
|
+
"leaf_from": "controlfloat",
|
|
216
|
+
"name": "Temperature",
|
|
217
|
+
"min": -40,
|
|
218
|
+
"max": 85,
|
|
219
|
+
"color": "#ff0000"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"leaf": "humidity",
|
|
223
|
+
"leaf_from": "controlfloat",
|
|
224
|
+
"name": "Humidity",
|
|
225
|
+
"color": "#0000ff"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"leaf": "message",
|
|
229
|
+
"leaf_from": "controltext",
|
|
230
|
+
"name": "Message",
|
|
231
|
+
"rw": "r"
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
"control_oled_gps": {
|
|
236
|
+
"summary": false,
|
|
237
|
+
"name": "OLED GPS",
|
|
238
|
+
"topics": [
|
|
239
|
+
{
|
|
240
|
+
"leaf": "latitude",
|
|
241
|
+
"leaf_from": "controlfloat",
|
|
242
|
+
"name": "Latitude",
|
|
243
|
+
"min": -90,
|
|
244
|
+
"max": 90,
|
|
245
|
+
"width": 10,
|
|
246
|
+
"color": "#0000ff"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"leaf": "longitude",
|
|
250
|
+
"leaf_from": "controlfloat",
|
|
251
|
+
"name": "Longitude",
|
|
252
|
+
"min": -180,
|
|
253
|
+
"max": 180,
|
|
254
|
+
"width": 11,
|
|
255
|
+
"color": "#0000ff"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"leaf": "altitude",
|
|
259
|
+
"leaf_from": "controlfloat",
|
|
260
|
+
"name": "Altitude",
|
|
261
|
+
"min": -500,
|
|
262
|
+
"max": 9000,
|
|
263
|
+
"width": 6,
|
|
264
|
+
"color": "#008000"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"leaf": "speed",
|
|
268
|
+
"leaf_from": "controlfloat",
|
|
269
|
+
"name": "Speed",
|
|
270
|
+
"max": 999,
|
|
271
|
+
"color": "#ffa500"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"leaf": "course",
|
|
275
|
+
"leaf_from": "controlfloat",
|
|
276
|
+
"name": "Course",
|
|
277
|
+
"max": 360
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"leaf": "satellites",
|
|
281
|
+
"leaf_from": "controlfloat",
|
|
282
|
+
"name": "Satellites",
|
|
283
|
+
"max": 32
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"leaf": "hdop",
|
|
287
|
+
"leaf_from": "controlfloat",
|
|
288
|
+
"name": "HDOP",
|
|
289
|
+
"max": 50
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
},
|
|
293
|
+
"control_oled_ht": {
|
|
294
|
+
"summary": false,
|
|
295
|
+
"name": "OLED HT",
|
|
296
|
+
"topics": [
|
|
297
|
+
{
|
|
298
|
+
"leaf": "temperature",
|
|
299
|
+
"leaf_from": "controlfloat",
|
|
300
|
+
"name": "Temperature",
|
|
301
|
+
"max": 50,
|
|
302
|
+
"color": "#ff0000"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"leaf": "humidity",
|
|
306
|
+
"leaf_from": "controlfloat",
|
|
307
|
+
"name": "Humidity",
|
|
308
|
+
"color": "#0000ff"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"leaf": "battery",
|
|
312
|
+
"leaf_from": "controlfloat",
|
|
313
|
+
"name": "Battery",
|
|
314
|
+
"max": 5000,
|
|
315
|
+
"color": "#00ff00"
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
},
|
|
319
|
+
"control_oled_loramesher": {
|
|
320
|
+
"summary": false,
|
|
321
|
+
"name": "OLED LoRaMesher",
|
|
322
|
+
"topics": [
|
|
323
|
+
{
|
|
324
|
+
"leaf": "battery",
|
|
325
|
+
"leaf_from": "controlfloat",
|
|
326
|
+
"name": "Battery",
|
|
327
|
+
"max": 5000,
|
|
328
|
+
"color": "#00ff00"
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
},
|
|
332
|
+
"control_oled_single": {
|
|
333
|
+
"summary": false,
|
|
334
|
+
"name": "OLED",
|
|
335
|
+
"topics": [
|
|
336
|
+
{
|
|
337
|
+
"leaf": "value",
|
|
338
|
+
"leaf_from": "controlfloat",
|
|
339
|
+
"name": "Value",
|
|
340
|
+
"max": 10000
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
},
|
|
209
344
|
"controlblinken": {
|
|
210
345
|
"name": "Control Blinken",
|
|
211
346
|
"topics": [
|
|
@@ -415,6 +550,24 @@
|
|
|
415
550
|
}
|
|
416
551
|
]
|
|
417
552
|
},
|
|
553
|
+
"hcsr04": {
|
|
554
|
+
"name": "HC-SR04 Distance",
|
|
555
|
+
"topics": [
|
|
556
|
+
{
|
|
557
|
+
"leaf": "hcsr04",
|
|
558
|
+
"leaf_from": "ultrasonic",
|
|
559
|
+
"max": 4000
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"leaf": "temperature",
|
|
563
|
+
"leaf_from": "controlfloat",
|
|
564
|
+
"name": "Air Temperature",
|
|
565
|
+
"min": -20,
|
|
566
|
+
"max": 50,
|
|
567
|
+
"color": "#ff0000"
|
|
568
|
+
}
|
|
569
|
+
]
|
|
570
|
+
},
|
|
418
571
|
"health": {
|
|
419
572
|
"name": "System",
|
|
420
573
|
"insidefrugaliot": true,
|
|
@@ -838,14 +991,16 @@
|
|
|
838
991
|
"leaf_from": "controlfloat",
|
|
839
992
|
"name": "Battery",
|
|
840
993
|
"min": 0,
|
|
841
|
-
"max": 16000
|
|
994
|
+
"max": 16000,
|
|
995
|
+
"log": true
|
|
842
996
|
},
|
|
843
997
|
{
|
|
844
998
|
"leaf": "panel",
|
|
845
999
|
"leaf_from": "controlfloat",
|
|
846
1000
|
"name": "Panel",
|
|
847
1001
|
"min": 0,
|
|
848
|
-
"max": 30000
|
|
1002
|
+
"max": 30000,
|
|
1003
|
+
"log": true
|
|
849
1004
|
},
|
|
850
1005
|
{
|
|
851
1006
|
"leaf": "profile",
|
|
@@ -889,6 +1044,18 @@
|
|
|
889
1044
|
}
|
|
890
1045
|
]
|
|
891
1046
|
},
|
|
1047
|
+
"time": {
|
|
1048
|
+
"name": "Time",
|
|
1049
|
+
"insidefrugaliot": true,
|
|
1050
|
+
"topics": [
|
|
1051
|
+
{
|
|
1052
|
+
"leaf": "epoch"
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
"leaf": "offset"
|
|
1056
|
+
}
|
|
1057
|
+
]
|
|
1058
|
+
},
|
|
892
1059
|
"ultrasonic": {
|
|
893
1060
|
"name": "Ultrasonic Distance",
|
|
894
1061
|
"topics": [
|
|
@@ -1178,6 +1345,16 @@
|
|
|
1178
1345
|
"rw": "r",
|
|
1179
1346
|
"graphable": true
|
|
1180
1347
|
},
|
|
1348
|
+
"epoch": {
|
|
1349
|
+
"leaf": "epoch",
|
|
1350
|
+
"name": "Epoch",
|
|
1351
|
+
"type": "int",
|
|
1352
|
+
"log": false,
|
|
1353
|
+
"display": "text",
|
|
1354
|
+
"color": "#000000",
|
|
1355
|
+
"rw": "r",
|
|
1356
|
+
"units": "s"
|
|
1357
|
+
},
|
|
1181
1358
|
"gas": {
|
|
1182
1359
|
"leaf": "gas",
|
|
1183
1360
|
"name": "Gas Resistance",
|
|
@@ -1412,6 +1589,16 @@
|
|
|
1412
1589
|
"rw": "w",
|
|
1413
1590
|
"retain": true
|
|
1414
1591
|
},
|
|
1592
|
+
"offset": {
|
|
1593
|
+
"leaf": "offset",
|
|
1594
|
+
"name": "UTC Offset",
|
|
1595
|
+
"type": "int",
|
|
1596
|
+
"log": false,
|
|
1597
|
+
"display": "text",
|
|
1598
|
+
"color": "#000000",
|
|
1599
|
+
"rw": "r",
|
|
1600
|
+
"units": "min"
|
|
1601
|
+
},
|
|
1415
1602
|
"on": {
|
|
1416
1603
|
"leaf": "on",
|
|
1417
1604
|
"name": "On",
|
package/test/flash.test.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// The flasher's hold on the serial port. WebSerial refuses a second open of the same port, so the
|
|
2
2
|
// element is the only thing that can close it - and a connect that fails is exactly when it must.
|
|
3
3
|
import './setup.js';
|
|
4
|
-
import { test, describe, before, beforeEach, afterEach } from 'node:test';
|
|
4
|
+
import { test, describe, before, after, beforeEach, afterEach } from 'node:test';
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
6
7
|
|
|
7
8
|
let ports; // what a fake Transport did, in order
|
|
8
9
|
|
|
@@ -218,3 +219,87 @@ describe('the serial monitor', () => {
|
|
|
218
219
|
flash.remove();
|
|
219
220
|
});
|
|
220
221
|
});
|
|
222
|
+
|
|
223
|
+
/*
|
|
224
|
+
* Provision vs app-only, against real partition tables from base/. A board is only re-laid out when
|
|
225
|
+
* its app slot is somewhere else - a different size is checked against the image instead, since
|
|
226
|
+
* provisioning erases the board's configuration.
|
|
227
|
+
*/
|
|
228
|
+
describe('flashing over a board that is already laid out', () => {
|
|
229
|
+
const readBase = (p) => new Uint8Array(readFileSync(new URL(`../base/${p}`, import.meta.url)));
|
|
230
|
+
const boards = JSON.parse(readFileSync(new URL('../base/boards.json', import.meta.url), 'utf8'));
|
|
231
|
+
let writes;
|
|
232
|
+
// An 8MB S3 whose flash holds `table` at 0x8000, a bootloader, and an erased filesystem
|
|
233
|
+
function fakeS3(table) {
|
|
234
|
+
return async () => ({
|
|
235
|
+
Transport: class { async disconnect() {} },
|
|
236
|
+
ESPLoader: class {
|
|
237
|
+
constructor() { this.chip = {CHIP_NAME: 'ESP32-S3', BOOTLOADER_FLASH_OFFSET: 0, IMAGE_CHIP_ID: 9}; }
|
|
238
|
+
async main() { return 'ESP32-S3'; }
|
|
239
|
+
async readFlashId() { return 0x17 << 16; }
|
|
240
|
+
async readFlash(offset, length) {
|
|
241
|
+
const block = new Uint8Array(length).fill(0xff);
|
|
242
|
+
if (offset === 0) block[0] = 0xe9;
|
|
243
|
+
if (offset === 0x8000) block.set(table.subarray(0, length));
|
|
244
|
+
return block;
|
|
245
|
+
}
|
|
246
|
+
async writeFlash({fileArray}) { writes.push(...fileArray.map((f) => f.address)); }
|
|
247
|
+
async after() {}
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async function connected(table) {
|
|
252
|
+
const flash = document.createElement('mqtt-flash');
|
|
253
|
+
document.body.append(flash);
|
|
254
|
+
flash.esptoolImport = fakeS3(table);
|
|
255
|
+
flash.state.boards = boards;
|
|
256
|
+
flash.monitorStart = () => {};
|
|
257
|
+
await flash.connectAndInspect({});
|
|
258
|
+
return flash;
|
|
259
|
+
}
|
|
260
|
+
const image = (size) => { const b = new Uint8Array(size); b[0] = 0xe9; b[12] = 9; return b; };
|
|
261
|
+
let realFetch;
|
|
262
|
+
before(() => {
|
|
263
|
+
realFetch = globalThis.fetch;
|
|
264
|
+
globalThis.fetch = async (url) => {
|
|
265
|
+
const bytes = readBase(String(url).replace(/^\.\/base\//, '').replace(/\?.*$/, ''));
|
|
266
|
+
return {ok: true, arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)};
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
after(() => { globalThis.fetch = realFetch; });
|
|
270
|
+
beforeEach(() => { writes = []; });
|
|
271
|
+
|
|
272
|
+
test('a board on a bigger app slot than our scheme is updated app-only, keeping its config', async () => {
|
|
273
|
+
const flash = await connected(readBase('partitions/default_8MB.bin'));
|
|
274
|
+
assert.equal(flash.state.device.provision, false, flash.state.log.join('\n'));
|
|
275
|
+
flash.state.app = {name: 'firmware.bin', bytes: image(0x200000)}; // too big for min_spiffs, fits here
|
|
276
|
+
await flash.flash();
|
|
277
|
+
assert.deepEqual(writes, [0xe000, 0x10000], 'otadata reset, then the app - nothing else');
|
|
278
|
+
flash.remove();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('an app-only write resets otadata where the board says, so it boots app0 and not an older app1', async () => {
|
|
282
|
+
const flash = await connected(readBase('partitions/min_spiffs.bin'));
|
|
283
|
+
flash.state.app = {name: 'firmware.bin', bytes: image(0x1000)};
|
|
284
|
+
await flash.flash();
|
|
285
|
+
assert.ok(writes.includes(0xe000), 'otadata should have been written');
|
|
286
|
+
flash.remove();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('an image too big for the board is refused, not provisioned behind the user’s back', async () => {
|
|
290
|
+
const flash = await connected(readBase('partitions/min_spiffs.bin'));
|
|
291
|
+
flash.state.app = {name: 'firmware.bin', bytes: image(0x200000)};
|
|
292
|
+
await assert.rejects(flash.flash(), /larger than the 1\.88MB app partition on this board - "Provision instead"/);
|
|
293
|
+
assert.deepEqual(writes, []);
|
|
294
|
+
flash.remove();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('a board whose app slot is somewhere else is provisioned', async () => {
|
|
298
|
+
const table = readBase('partitions/min_spiffs.bin').slice();
|
|
299
|
+
new DataView(table.buffer).setUint32(2 * 32 + 4, 0x20000, true); // app0 is the third record
|
|
300
|
+
const flash = await connected(table);
|
|
301
|
+
assert.equal(flash.state.device.provision, true);
|
|
302
|
+
assert.deepEqual(flash.state.device.provisionReasons, ['app partition not at 0x10000']);
|
|
303
|
+
flash.remove();
|
|
304
|
+
});
|
|
305
|
+
});
|