frugal-iot-client 2.0.4 → 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 +36 -17
- package/docs/FLASH_PLAN.md +53 -48
- package/flash.js +28 -8
- package/frugaliot.css +5 -1
- package/graph.js +1 -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
|
@@ -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
|
+
});
|
package/test/mock.js
CHANGED
|
@@ -132,6 +132,19 @@ export const scenarios = {
|
|
|
132
132
|
}),
|
|
133
133
|
},
|
|
134
134
|
|
|
135
|
+
'display-control': {
|
|
136
|
+
// A control that is not hysteresis-shaped: two inputs wired to another node, one output to the LCD
|
|
137
|
+
title: 'An LCD display control wired to another node',
|
|
138
|
+
messages: [
|
|
139
|
+
...device('esp32-48ed20', 'LCD', { 'control_lcd_ht/temperature': '21.5', 'control_lcd_ht/humidity': '60' },
|
|
140
|
+
{ ota: 'lcd_ht_supermini' }),
|
|
141
|
+
...wiring('esp32-48ed20', 'control_lcd_ht', {
|
|
142
|
+
temperature: `${ORG}/${PROJECT}/esp32-784702/sht/temperature`,
|
|
143
|
+
humidity: `${ORG}/${PROJECT}/esp32-784702/sht/humidity`,
|
|
144
|
+
}),
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
|
|
135
148
|
'unknown-module': {
|
|
136
149
|
// Nothing in modules.yaml declares 'quantumflux' - it must still reach the back of the card,
|
|
137
150
|
// because that is what someone adding a module needs to see
|
package/test/page.test.js
CHANGED
|
@@ -483,3 +483,33 @@ describe('the page', () => {
|
|
|
483
483
|
page.remove();
|
|
484
484
|
});
|
|
485
485
|
});
|
|
486
|
+
|
|
487
|
+
describe('moving a node to another project', () => {
|
|
488
|
+
test('shows pending until the node is heard live at its new address', () => {
|
|
489
|
+
const config = structuredClone(base);
|
|
490
|
+
config.organizations.dev.projects = {
|
|
491
|
+
lotus: { name: 'Lotus Ponds', nodes: { n1: { 'frugal_iot/name': 'Pond' } } },
|
|
492
|
+
developers: { name: 'Developers', nodes: {} },
|
|
493
|
+
};
|
|
494
|
+
mock.loadConfig({ ...config, user: { id: 2, name: 'test', permissions: [{ org: 'dev', capability: 'ADMIN' }] } });
|
|
495
|
+
const admin = document.createElement('mqtt-admin');
|
|
496
|
+
admin.setAttribute('section', 'admin');
|
|
497
|
+
document.body.append(admin);
|
|
498
|
+
let redrawn = 0;
|
|
499
|
+
admin.watchNodeMove('dev', 'n1', 'lotus', 'developers', () => redrawn++);
|
|
500
|
+
const cell = () => admin.renderProjectIdCell({ nodeId: 'n1', projectId: 'lotus' }, () => {}).textContent;
|
|
501
|
+
assert.match(cell(), /lotus → developers/);
|
|
502
|
+
|
|
503
|
+
// Left over from an earlier stay in that project - not this node arriving
|
|
504
|
+
core.mqtt_deliver('dev/developers/n1/frugal_iot/name', 'Pond', true);
|
|
505
|
+
assert.equal(redrawn, 0);
|
|
506
|
+
assert.ok(admin.projectsOf('dev').lotus.nodes.n1);
|
|
507
|
+
|
|
508
|
+
core.mqtt_deliver('dev/developers/n1/frugal_iot/name', 'Pond', false);
|
|
509
|
+
assert.equal(redrawn, 1);
|
|
510
|
+
assert.equal(admin.projectsOf('dev').lotus.nodes.n1, undefined);
|
|
511
|
+
assert.equal(admin.projectsOf('dev').developers.nodes.n1['frugal_iot/name'], 'Pond');
|
|
512
|
+
assert.equal(cell(), 'lotus'); // no longer pending - and the row itself is now in developers
|
|
513
|
+
admin.remove();
|
|
514
|
+
});
|
|
515
|
+
});
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Frugal IoT 16MB: app partitions identical to min_spiffs, all surplus to LittleFS (label must stay "spiffs")
|
|
2
|
-
# Name, Type, SubType, Offset, Size, Flags
|
|
3
|
-
nvs, data, nvs, 0x9000, 0x5000,
|
|
4
|
-
otadata, data, ota, 0xe000, 0x2000,
|
|
5
|
-
app0, app, ota_0, 0x10000, 0x1E0000,
|
|
6
|
-
app1, app, ota_1, 0x1F0000,0x1E0000,
|
|
7
|
-
spiffs, data, spiffs, 0x3D0000,0xC20000,
|
|
8
|
-
coredump, data, coredump,0xFF0000,0x10000,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Frugal IoT 8MB: app partitions identical to min_spiffs, all surplus to LittleFS (label must stay "spiffs")
|
|
2
|
-
# Name, Type, SubType, Offset, Size, Flags
|
|
3
|
-
nvs, data, nvs, 0x9000, 0x5000,
|
|
4
|
-
otadata, data, ota, 0xe000, 0x2000,
|
|
5
|
-
app0, app, ota_0, 0x10000, 0x1E0000,
|
|
6
|
-
app1, app, ota_1, 0x1F0000,0x1E0000,
|
|
7
|
-
spiffs, data, spiffs, 0x3D0000,0x420000,
|
|
8
|
-
coredump, data, coredump,0x7F0000,0x10000,
|