frugal-iot-client 2.0.5 → 2.0.7
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/CLAUDE.md +3 -1
- 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 +37 -14
- package/docs/FLASH_PLAN.md +53 -48
- package/flash.js +28 -8
- package/frugaliot.css +5 -1
- package/package.json +2 -2
- package/test/cardui.test.js +10 -0
- package/test/datatree.test.js +9 -0
- package/test/embedded.test.js +15 -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/CLAUDE.md
CHANGED
|
@@ -259,7 +259,9 @@ else — no `mqtt-wrapper`, no discovery tree, no `/config.json`. `standaloneTop
|
|
|
259
259
|
the `mqtt-client` element instead. `test/embedded.test.js` guards this — it has broken twice
|
|
260
260
|
unnoticed.
|
|
261
261
|
|
|
262
|
-
|
|
262
|
+
Graph history has no `server_config.logger.url` to read on such a page, so `loggerUrl()` falls back
|
|
263
|
+
to `/data` on the server the modules were loaded from. `/data` needs a login, so a page on another
|
|
264
|
+
site gets dotted "no data" gaps instead of history.
|
|
263
265
|
|
|
264
266
|
### Looking up topics
|
|
265
267
|
|
package/admin.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {EL, GET, HTMLElementExtended} from '/node_modules/html-element-extended/htmlelementextended.js';
|
|
9
9
|
import mqtt from '/node_modules/mqtt/dist/mqtt.esm.js'; // https://www.npmjs.com/package/mqtt
|
|
10
|
-
import { CssUrl, DELETE, POST, XXX, brokerHost, configSet, el, getString, getStringWith, hasCapability, mqttTempConnect, mqtt_subscribe, retainedPattern, locationParameterChange, mqtt_client, preferedLanguageSet, preferedLanguages, redirectToLogin, server_config } from './core.js';
|
|
10
|
+
import { CssUrl, DELETE, POST, XXX, brokerHost, configSet, el, getString, getStringWith, hasCapability, mqttTempConnect, mqtt_subscribe, mqtt_unsubscribe, retainedPattern, locationParameterChange, mqtt_client, preferedLanguageSet, preferedLanguages, redirectToLogin, server_config } from './core.js';
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
// The OTA upload is multipart, which POST() (JSON) cannot carry. Callback-style, like the rest.
|
|
@@ -173,8 +173,9 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
173
173
|
]);
|
|
174
174
|
}
|
|
175
175
|
// Fetch ota files and display
|
|
176
|
-
getOrDeleteOtaFiles(url) {
|
|
176
|
+
getOrDeleteOtaFiles(url, cb = () => {}) {
|
|
177
177
|
GET(url, {}, (err, json) => {
|
|
178
|
+
cb();
|
|
178
179
|
if (err) {
|
|
179
180
|
this.message(err);
|
|
180
181
|
return;
|
|
@@ -184,11 +185,11 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
184
185
|
}
|
|
185
186
|
});
|
|
186
187
|
}
|
|
187
|
-
getOtaFiles() {
|
|
188
|
-
if (this.state.org) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
getOtaFiles(cb = () => {}) {
|
|
189
|
+
if (this.state.org && this.otaOrgs.map(o => o[0]).includes(this.state.org)) {
|
|
190
|
+
this.getOrDeleteOtaFiles(`/ota_list/${this.state.org}`, cb);
|
|
191
|
+
} else {
|
|
192
|
+
cb();
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
onOtaDelete(val, ev) {
|
|
@@ -241,8 +242,9 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
241
242
|
);
|
|
242
243
|
}
|
|
243
244
|
// Fetch people and display
|
|
244
|
-
getOrChangeAdminPeople(url) {
|
|
245
|
+
getOrChangeAdminPeople(url, cb = () => {}) {
|
|
245
246
|
GET(url, {}, (err, json) => {
|
|
247
|
+
cb();
|
|
246
248
|
if (err) {
|
|
247
249
|
this.message(err.message);
|
|
248
250
|
return;
|
|
@@ -328,9 +330,10 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
328
330
|
onclick: this.onEnrolmentSecretAdd.bind(this)}),
|
|
329
331
|
]);
|
|
330
332
|
}
|
|
331
|
-
getEnrolledNodes() {
|
|
332
|
-
if (!this.state.org) return;
|
|
333
|
+
getEnrolledNodes(cb = () => {}) {
|
|
334
|
+
if (!this.state.org) { cb(); return; }
|
|
333
335
|
GET(`/nodes_list/${this.state.org}`, {}, (err, json) => {
|
|
336
|
+
cb();
|
|
334
337
|
if (err) { this.message(err.message); return; }
|
|
335
338
|
this.state.enrolled_nodes = json; // [{ project, nodeid, lora, enrolled_at }]
|
|
336
339
|
this.replaceElement("enrolled_nodes", this.enrolledNodesList());
|
|
@@ -345,9 +348,10 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
345
348
|
* itself is up. A Pi with a broken bridge still checks in, and that difference is exactly what
|
|
346
349
|
* is hard to see from production any other way.
|
|
347
350
|
*/
|
|
348
|
-
getBridges() {
|
|
349
|
-
if (!this.state.org) return;
|
|
351
|
+
getBridges(cb = () => {}) {
|
|
352
|
+
if (!this.state.org) { cb(); return; }
|
|
350
353
|
GET(`/bridges_list/${this.state.org}`, {}, (err, json) => {
|
|
354
|
+
cb();
|
|
351
355
|
if (err) { this.message(err.message); return; }
|
|
352
356
|
this.state.bridges = json; // [{ site, account, created_at, last_pull }]
|
|
353
357
|
this.subscribeBridgeState();
|
|
@@ -554,9 +558,7 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
554
558
|
const nodes = this.state.enrolled_nodes;
|
|
555
559
|
if (!nodes || !nodes.length) {
|
|
556
560
|
return el('p', {textContent:
|
|
557
|
-
"No nodes have enrolled yet, and none
|
|
558
|
-
"connects; one that asks and is refused is listed here too, so an empty list means nothing " +
|
|
559
|
-
"is reaching this server at all - check that the node's org and enrol URL are this one."});
|
|
561
|
+
"No nodes have enrolled yet, and none have asked and been rejected."});
|
|
560
562
|
}
|
|
561
563
|
const states = this.nodeStates();
|
|
562
564
|
return el('table', {class: 'nodestates'}, [
|
|
@@ -594,14 +596,17 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
594
596
|
}),
|
|
595
597
|
]);
|
|
596
598
|
}
|
|
597
|
-
getPeopleList() {
|
|
599
|
+
getPeopleList(cb = () => {}) {
|
|
598
600
|
if (this.state.org) {
|
|
599
|
-
this.getOrChangeAdminPeople(`/people_list/${this.state.org}
|
|
601
|
+
this.getOrChangeAdminPeople(`/people_list/${this.state.org}`, cb);
|
|
602
|
+
} else {
|
|
603
|
+
cb();
|
|
600
604
|
}
|
|
601
605
|
}
|
|
602
606
|
// Fetch projects and display
|
|
603
|
-
getOrChangeAdminProjects(url) {
|
|
607
|
+
getOrChangeAdminProjects(url, cb = () => {}) {
|
|
604
608
|
GET(url, {}, (err, json) => {
|
|
609
|
+
cb();
|
|
605
610
|
if (err) {
|
|
606
611
|
this.message(err.message);
|
|
607
612
|
return;
|
|
@@ -611,9 +616,11 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
611
616
|
}
|
|
612
617
|
});
|
|
613
618
|
}
|
|
614
|
-
getProjectsList() {
|
|
619
|
+
getProjectsList(cb = () => {}) {
|
|
615
620
|
if (this.state.org) {
|
|
616
|
-
this.getOrChangeAdminProjects(`/projects_list/${this.state.org}
|
|
621
|
+
this.getOrChangeAdminProjects(`/projects_list/${this.state.org}`, cb);
|
|
622
|
+
} else {
|
|
623
|
+
cb();
|
|
617
624
|
}
|
|
618
625
|
}
|
|
619
626
|
onPermissionsDelete(val, ev) {
|
|
@@ -717,9 +724,10 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
717
724
|
}
|
|
718
725
|
// Fetch all registered Farm-Platforms, and display. Not org-scoped - a platform is a many-to-many
|
|
719
726
|
// relationship with orgs/projects, recorded per-mapping in api_farms (see getFarmsList).
|
|
720
|
-
getPlatformsList() {
|
|
727
|
+
getPlatformsList(cb = () => {}) {
|
|
721
728
|
if (this.state.org) {
|
|
722
729
|
GET(`/api/platforms/list`, {}, (err, json) => {
|
|
730
|
+
cb();
|
|
723
731
|
if (err) {
|
|
724
732
|
this.message(err.message);
|
|
725
733
|
return;
|
|
@@ -733,6 +741,8 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
733
741
|
this.replaceElement("platforms_list_display", this.platformsListDisplay());
|
|
734
742
|
this.replaceElement("add_farm", this.collapsibleArea('add_farm', "Add Farm", this.farmRegisterForm));
|
|
735
743
|
});
|
|
744
|
+
} else {
|
|
745
|
+
cb();
|
|
736
746
|
}
|
|
737
747
|
}
|
|
738
748
|
onPlatformSelect(ev) {
|
|
@@ -802,9 +812,10 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
802
812
|
return el('div', {}, [el('h3', {textContent: "Register Platform"}), EL]);
|
|
803
813
|
}
|
|
804
814
|
// Fetch farm-to-project mappings registered for the selected organization, and display
|
|
805
|
-
getFarmsList() {
|
|
815
|
+
getFarmsList(cb = () => {}) {
|
|
806
816
|
if (this.state.org) {
|
|
807
817
|
GET(`/api/farms/list?org=${encodeURIComponent(this.state.org)}`, {}, (err, json) => {
|
|
818
|
+
cb();
|
|
808
819
|
if (err) {
|
|
809
820
|
this.message(err.message);
|
|
810
821
|
return;
|
|
@@ -819,6 +830,8 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
819
830
|
this.replaceElement("farm_node_actions", this.farmNodeActions());
|
|
820
831
|
this.getDeviceActionSchema();
|
|
821
832
|
});
|
|
833
|
+
} else {
|
|
834
|
+
cb();
|
|
822
835
|
}
|
|
823
836
|
}
|
|
824
837
|
onFarmSelect(ev) {
|
|
@@ -907,13 +920,13 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
907
920
|
// server_config locally - this is the API tab, so it should exercise the same API a real
|
|
908
921
|
// Farm-Platform would use. A farm can map to more than one project, so one request per project id,
|
|
909
922
|
// joined into a single row set once all have replied.
|
|
910
|
-
getFarmNodesList() {
|
|
923
|
+
getFarmNodesList(cb = () => {}) {
|
|
911
924
|
const org = this.state.org;
|
|
912
925
|
const projectIds = this.selectedFarmProjectIds();
|
|
913
926
|
if (!org || projectIds.length === 0) {
|
|
914
927
|
this.state.farm_nodes_list = [];
|
|
915
|
-
this.state.enrolled_nodes = null;
|
|
916
928
|
this.replaceElement("farm_nodes_table", this.farmNodesTable());
|
|
929
|
+
cb();
|
|
917
930
|
return;
|
|
918
931
|
}
|
|
919
932
|
let remaining = projectIds.length;
|
|
@@ -936,6 +949,7 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
936
949
|
});
|
|
937
950
|
}
|
|
938
951
|
if (--remaining === 0) {
|
|
952
|
+
cb();
|
|
939
953
|
this.state.farm_nodes_list = allNodes;
|
|
940
954
|
this.replaceElement("farm_nodes_table", this.farmNodesTable());
|
|
941
955
|
}
|
|
@@ -999,15 +1013,17 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
999
1013
|
}
|
|
1000
1014
|
// Fetch the selected node's Device Schema so the Action section can list its actions - triggered
|
|
1001
1015
|
// whenever the selected node changes.
|
|
1002
|
-
getDeviceActionSchema() {
|
|
1016
|
+
getDeviceActionSchema(cb = () => {}) {
|
|
1003
1017
|
const device = this.selectedFarmNodeDevice();
|
|
1004
1018
|
this.state.device_schema = null;
|
|
1005
1019
|
this.state.selected_action = null;
|
|
1006
1020
|
if (!device) {
|
|
1007
1021
|
this.replaceElement("action_section", this.actionSection());
|
|
1022
|
+
cb();
|
|
1008
1023
|
return;
|
|
1009
1024
|
}
|
|
1010
1025
|
GET(`/api/devices/schema?device=${encodeURIComponent(device)}`, {}, (err, json) => {
|
|
1026
|
+
cb();
|
|
1011
1027
|
if (err) {
|
|
1012
1028
|
this.message(err.message);
|
|
1013
1029
|
return;
|
|
@@ -1169,17 +1185,21 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
1169
1185
|
}
|
|
1170
1186
|
// Each of these lists is the result of one fetch made when the card opened - nothing pushes a
|
|
1171
1187
|
// change, so the heading carries the way to ask again.
|
|
1188
|
+
// refreshFn is given a callback for when its reply has arrived, which is what stops the spin.
|
|
1172
1189
|
refreshableHeading(title, refreshFn) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1190
|
+
const iconEl = el('span', {class: 'pseudolink refresh', i8n: false, title: getString("Refresh"),
|
|
1191
|
+
textContent: '\u27f3', onclick: () => {
|
|
1192
|
+
if (iconEl.classList.contains('busy')) return;
|
|
1193
|
+
iconEl.classList.add('busy');
|
|
1194
|
+
refreshFn.call(this, () => iconEl.classList.remove('busy'));
|
|
1195
|
+
}});
|
|
1196
|
+
return el('h3', {}, [el('span', {textContent: title}), iconEl]);
|
|
1178
1197
|
}
|
|
1179
1198
|
// The organization's nodes are read out of server_config, so refreshing this table means fetching
|
|
1180
1199
|
// /config.json again rather than re-rendering what is already here.
|
|
1181
|
-
refreshNodesTable() {
|
|
1200
|
+
refreshNodesTable(cb = () => {}) {
|
|
1182
1201
|
GET("/config.json", {}, (err, json) => {
|
|
1202
|
+
cb();
|
|
1183
1203
|
if (err) { this.message(err.message); return; }
|
|
1184
1204
|
configSet(json);
|
|
1185
1205
|
this.replaceElement("nodes_table", this.nodesTable());
|
|
@@ -1204,9 +1224,7 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
1204
1224
|
this.adminSections().forEach((section) => this.replaceElement(section.rest, this.gatedContent(section)));
|
|
1205
1225
|
// Which tabs do their own network fetch, and so need one again for a new organization. Only
|
|
1206
1226
|
// Dashboard does not (mqtt-wrapper handles it). "Nodes" is in the list because it now fetches
|
|
1207
|
-
// the enrolled-node list as well as reading server_config
|
|
1208
|
-
// always said "No nodes have enrolled yet": loadSectionIfNeeded returns early for a tab that is not
|
|
1209
|
-
// in this set, so the fetch never happened.
|
|
1227
|
+
// the enrolled-node list as well as reading server_config:
|
|
1210
1228
|
this.state.sectionsNeedingLoad = new Set(['OTA', 'Admin', 'Projects', 'Nodes', 'Bridges', 'API']);
|
|
1211
1229
|
this.loadSectionIfNeeded(this.state.activeSectionTitle);
|
|
1212
1230
|
// Only a card created without an organization has a dropdown of its own; replaceElement does
|
|
@@ -1393,32 +1411,55 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
1393
1411
|
}
|
|
1394
1412
|
|
|
1395
1413
|
onNodeProjectChange(nodeId, oldProjectId, selectElement, onChanged) {
|
|
1396
|
-
// Handle project change for a node
|
|
1397
1414
|
const newProjectId = selectElement.value;
|
|
1398
1415
|
if (newProjectId === oldProjectId) {
|
|
1399
1416
|
return; // No change
|
|
1400
1417
|
}
|
|
1418
|
+
const org = this.state.org;
|
|
1419
|
+
const topic = `${org}/${oldProjectId}/${nodeId}/set/frugal_iot/project`;
|
|
1420
|
+
console.log(`Publishing ${topic} = ${newProjectId}`);
|
|
1421
|
+
// Not retained: a sleeping node still gets it, from its persistent session, when it wakes.
|
|
1422
|
+
mqtt_client.publish(topic, newProjectId, {retain: false, qos: 1});
|
|
1423
|
+
this.message(getStringWith("Asked %s to move", nodeId));
|
|
1424
|
+
this.watchNodeMove(org, nodeId, oldProjectId, newProjectId, onChanged);
|
|
1425
|
+
onChanged();
|
|
1426
|
+
}
|
|
1401
1427
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1428
|
+
// The move is done when the node is heard at its new address - nothing else says so, because the
|
|
1429
|
+
// node restarts and enrols afresh there, and the server tidies the old address away only then.
|
|
1430
|
+
watchNodeMove(org, nodeId, fromProjectId, toProjectId, onChanged) {
|
|
1431
|
+
const moves = this.state.nodeMoves || (this.state.nodeMoves = {});
|
|
1432
|
+
if (moves[nodeId]) { mqtt_unsubscribe(moves[nodeId].watch); }
|
|
1433
|
+
const watch = `${org}/${toProjectId}/${nodeId}/#`;
|
|
1434
|
+
moves[nodeId] = {from: fromProjectId, to: toProjectId, watch};
|
|
1435
|
+
mqtt_subscribe(watch, (topic, message, retained) => {
|
|
1436
|
+
// A retained message is the broker's memory of an earlier stay in that project, not this node
|
|
1437
|
+
if (retained || moves[nodeId]?.watch !== watch) { return; }
|
|
1438
|
+
delete moves[nodeId];
|
|
1439
|
+
mqtt_unsubscribe(watch);
|
|
1440
|
+
this.moveNodeInConfig(org, nodeId, fromProjectId, toProjectId);
|
|
1441
|
+
onChanged();
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1409
1444
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1445
|
+
// server_config is only fetched at startup, so it is told here what the server now believes.
|
|
1446
|
+
moveNodeInConfig(org, nodeId, fromProjectId, toProjectId) {
|
|
1447
|
+
const projects = this.projectsOf(org);
|
|
1448
|
+
const from = projects[fromProjectId]?.nodes;
|
|
1449
|
+
const to = projects[toProjectId] && (projects[toProjectId].nodes || (projects[toProjectId].nodes = {}));
|
|
1450
|
+
if (!to) { return; }
|
|
1451
|
+
to[nodeId] = {...(from?.[nodeId] || {}), lastseen: new Date().toISOString()};
|
|
1452
|
+
if (from) { delete from[nodeId]; }
|
|
1412
1453
|
}
|
|
1413
1454
|
|
|
1414
1455
|
renderProjectIdCell(node, onChanged) {
|
|
1415
|
-
|
|
1456
|
+
const move = this.state.nodeMoves?.[node.nodeId];
|
|
1457
|
+
const pending = move && move.from === node.projectId;
|
|
1416
1458
|
return el('td', {
|
|
1417
1459
|
style: 'cursor: pointer; position: relative;',
|
|
1418
|
-
title:
|
|
1460
|
+
title: pending ? getStringWith("Waiting for %s to report", node.nodeId) : getString("Click to change project"),
|
|
1419
1461
|
onclick: (e) => {
|
|
1420
|
-
|
|
1421
|
-
const dropdown = this.projectsDropdownForNode(this.state.org, node.projectId);
|
|
1462
|
+
const dropdown = this.projectsDropdownForNode(this.state.org, pending ? move.to : node.projectId);
|
|
1422
1463
|
dropdown.addEventListener('change', (changeEvent) => {
|
|
1423
1464
|
this.onNodeProjectChange(node.nodeId, node.projectId, changeEvent.target, onChanged);
|
|
1424
1465
|
});
|
|
@@ -1427,7 +1468,7 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
1427
1468
|
// Close dropdown if focus lost
|
|
1428
1469
|
dropdown.addEventListener('blur', onChanged);
|
|
1429
1470
|
}
|
|
1430
|
-
}, [node.projectId]);
|
|
1471
|
+
}, [pending ? `${node.projectId} → ${move.to} …` : node.projectId]);
|
|
1431
1472
|
}
|
|
1432
1473
|
|
|
1433
1474
|
// Content of the OTA tab below the organization dropdown - only rendered once an org is selected
|
|
@@ -1627,20 +1668,17 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
|
|
|
1627
1668
|
el('section', {}, [
|
|
1628
1669
|
this.refreshableHeading("Enrolled nodes", this.getEnrolledNodes),
|
|
1629
1670
|
el('p', {textContent:
|
|
1630
|
-
"
|
|
1631
|
-
"
|
|
1632
|
-
"
|
|
1633
|
-
"node that asked and was refused is listed here as well, with why: click Approve on it " +
|
|
1634
|
-
"to accept its next request whatever secret it presents. It retries every half hour."}),
|
|
1671
|
+
"Nodes that have obtained broker credentials on first connection. " +
|
|
1672
|
+
"Forget reissues a new credential at next connection and is needed if memory has been erased. " +
|
|
1673
|
+
"Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes)."}),
|
|
1635
1674
|
this.state.elements.enrolled_nodes = this.enrolledNodesList(),
|
|
1636
1675
|
]),
|
|
1637
1676
|
el('section', {}, [
|
|
1638
1677
|
el('h3', {textContent: "Enrolment secret"}),
|
|
1639
|
-
el('p', {textContent:
|
|
1640
|
-
"Compiled into
|
|
1641
|
-
"
|
|
1642
|
-
"
|
|
1643
|
-
"withdraw the old one."}),
|
|
1678
|
+
el('p', {i8n: false, textContent: getString(
|
|
1679
|
+
"Compiled into firmware and used to request broker credential. " +
|
|
1680
|
+
"Add a second one to change it: both are accepted, so nodes already flashed keep working until you " +
|
|
1681
|
+
"withdraw the old one.")}),
|
|
1644
1682
|
this.state.elements.enrolment_secrets = this.enrolmentSecretsList(),
|
|
1645
1683
|
]),
|
|
1646
1684
|
]);
|
package/base/boards.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 8,
|
|
3
3
|
"coreVersion": "pioarduino framework-arduinoespressif32 3.3.9",
|
|
4
|
-
"comment": "Base bundles for USB flashing - see FLASH_PLAN.md. Bootloaders MUST come from the same framework the firmware is built against: generated from pioarduino framework-arduinoespressif32-libs/<chip>/bin/bootloader_qio_80m.elf via 'esptool elf2image --flash-mode dio --flash-freq 80m --flash-size 4MB'. The C3 one is byte-identical to a real pioarduino build; S2/S3 use the same recipe but are unverified against a reference build. The Arduino IDE core's dio_80m ELF produces a DIFFERENT bootloader that did not work. Flash-size independent (esptool patches the header at write time). Partition tables are chip-independent. boot_app0.bin is universal.",
|
|
4
|
+
"comment": "Base bundles for USB flashing - see FLASH_PLAN.md. Bootloaders MUST come from the same framework the firmware is built against: generated from pioarduino framework-arduinoespressif32-libs/<chip>/bin/bootloader_qio_80m.elf via 'esptool elf2image --flash-mode dio --flash-freq 80m --flash-size 4MB'. The C3 one is byte-identical to a real pioarduino build; S2/S3 use the same recipe but are unverified against a reference build. The Arduino IDE core's dio_80m ELF produces a DIFFERENT bootloader that did not work. Flash-size independent (esptool patches the header at write time). Partition tables are chip-independent, and are the core's stock CSVs, so they match what a PlatformIO env using the same scheme writes. boot_app0.bin is universal.",
|
|
5
5
|
"otadata": "boot_app0.bin",
|
|
6
6
|
"partitionTableOffset": 32768,
|
|
7
7
|
"bootloaderByChip": {
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"ESP32-C3-4MB": "min_spiffs",
|
|
14
14
|
"ESP32-S2-4MB": "min_spiffs",
|
|
15
15
|
"ESP32-S3-4MB": "min_spiffs",
|
|
16
|
-
"ESP32-S3-8MB": "
|
|
17
|
-
"ESP32-S3-16MB": "
|
|
16
|
+
"ESP32-S3-8MB": "default_8MB",
|
|
17
|
+
"ESP32-S3-16MB": "default_16MB"
|
|
18
18
|
},
|
|
19
19
|
"partitionsByScheme": {
|
|
20
20
|
"min_spiffs": "partitions/min_spiffs.bin",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"default_8MB": "partitions/default_8MB.bin",
|
|
22
|
+
"default_16MB": "partitions/default_16MB.bin"
|
|
23
23
|
},
|
|
24
24
|
"wholeImageChips": [
|
|
25
25
|
"ESP8266"
|
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Name, Type, SubType, Offset, Size, Flags
|
|
2
|
+
nvs, data, nvs, 0x9000, 0x5000,
|
|
3
|
+
otadata, data, ota, 0xe000, 0x2000,
|
|
4
|
+
app0, app, ota_0, 0x10000, 0x640000,
|
|
5
|
+
app1, app, ota_1, 0x650000,0x640000,
|
|
6
|
+
spiffs, data, spiffs, 0xc90000,0x360000,
|
|
7
|
+
coredump, data, coredump,0xFF0000,0x10000,
|
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Name, Type, SubType, Offset, Size, Flags
|
|
2
|
+
nvs, data, nvs, 0x9000, 0x5000,
|
|
3
|
+
otadata, data, ota, 0xe000, 0x2000,
|
|
4
|
+
app0, app, ota_0, 0x10000, 0x330000,
|
|
5
|
+
app1, app, ota_1, 0x340000,0x330000,
|
|
6
|
+
spiffs, data, spiffs, 0x670000,0x180000,
|
|
7
|
+
coredump, data, coredump,0x7F0000,0x10000,
|
package/cards.js
CHANGED
|
@@ -384,11 +384,20 @@ class MqttDeviceCard extends HTMLElementExtendedMinimum {
|
|
|
384
384
|
this.compact(hyst, {}),
|
|
385
385
|
]) : null,
|
|
386
386
|
].filter(Boolean)) : null;
|
|
387
|
+
// Any other control - a display's inputs, Blinken's timings - gets a plain row per topic, or its
|
|
388
|
+
// section came out empty and was dropped, leaving nothing to wire (control_lcd_ht did exactly that)
|
|
389
|
+
const laidOut = ['now', 'greater', 'limit', 'hysteresis', 'hysterisis', 'out', 'manual'];
|
|
390
|
+
const others = Object.entries(t).filter(([leaf]) => !laidOut.includes(leaf)).map(([, mt]) => {
|
|
391
|
+
const widget = this.widgetForTopic(mt, '', mt.wireable ? { wiring: 'open' } : {});
|
|
392
|
+
this.state.elements.widgets.push({ row: { mt }, widget });
|
|
393
|
+
return this.field(mt.name, widget);
|
|
394
|
+
});
|
|
387
395
|
return this.section(groupMt.state.name || groupId, [
|
|
388
396
|
t.now ? this.field(getString('Input'), this.widgetForTopic(t.now, '', { wiring: 'open' })) : null,
|
|
389
397
|
when ? this.field(getString('When'), when) : null,
|
|
390
398
|
t.out ? this.field(getString('Output'), this.widgetForTopic(t.out, '', { wiring: 'open' })) : null,
|
|
391
399
|
t.manual ? this.field(getString('Manual'), this.widgetForTopic(t.manual, '')) : null,
|
|
400
|
+
...others,
|
|
392
401
|
]);
|
|
393
402
|
}
|
|
394
403
|
// A bare input with no label of its own, for the inline "when" row
|