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
package/core.js
CHANGED
|
@@ -208,6 +208,13 @@ function mqtt_deliver(topic, msg, retained) {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
+
// Drop one subscription, made with mqtt_subscribe, that was only wanted for a while.
|
|
212
|
+
function mqtt_unsubscribe(topic) {
|
|
213
|
+
mqtt_subscriptions = mqtt_subscriptions.filter((s) => s.topic !== topic);
|
|
214
|
+
if (mqtt_client && mqtt_client.connected) {
|
|
215
|
+
mqtt_client.unsubscribe(topic, (err) => { if (err) console.error(err); });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
211
218
|
// Drop every subscription belonging to an organization - all its topics start with the organization id.
|
|
212
219
|
// Called when switching organization, both to tell the broker (if still connected) and so that the
|
|
213
220
|
// connection made for the new organization does not resubscribe to the old organization's topics.
|
|
@@ -423,7 +430,7 @@ EN:
|
|
|
423
430
|
close: close
|
|
424
431
|
Collapse: Collapse
|
|
425
432
|
Color: Color
|
|
426
|
-
"Compiled into
|
|
433
|
+
"Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one."
|
|
427
434
|
Connect board: Connect board
|
|
428
435
|
connected: connected
|
|
429
436
|
connecting: connecting
|
|
@@ -446,7 +453,6 @@ EN:
|
|
|
446
453
|
Done: Done
|
|
447
454
|
ds18b20: ds18b20
|
|
448
455
|
e.g. LiteFarm: e.g. LiteFarm
|
|
449
|
-
Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.: Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.
|
|
450
456
|
eCO2: eCO2
|
|
451
457
|
Email: Email
|
|
452
458
|
Enrol URL: Enrol URL
|
|
@@ -533,7 +539,7 @@ EN:
|
|
|
533
539
|
No farms registered for this organization yet.: No farms registered for this organization yet.
|
|
534
540
|
No nodes found for this farm's project(s).: No nodes found for this farm's project(s).
|
|
535
541
|
No nodes found for this organization: No nodes found for this organization
|
|
536
|
-
No nodes have enrolled yet
|
|
542
|
+
No nodes have enrolled yet, and none have asked and been rejected.: No nodes have enrolled yet, and none have asked and been rejected.
|
|
537
543
|
No organization: No organization
|
|
538
544
|
No organization selected: No organization selected
|
|
539
545
|
No OTA files uploaded yet.: No OTA files uploaded yet.
|
|
@@ -550,6 +556,7 @@ EN:
|
|
|
550
556
|
Nodes: Nodes
|
|
551
557
|
Nodes in Farm: Nodes in Farm
|
|
552
558
|
Nodes in Organization: Nodes in Organization
|
|
559
|
+
Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).: Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).
|
|
553
560
|
Not in the schema: Not in the schema
|
|
554
561
|
Not selected: Not selected
|
|
555
562
|
Note this is your organization - not the organizations whose devices you want to access.: Note this is your organization - not the organizations whose devices you want to access.
|
|
@@ -581,6 +588,8 @@ EN:
|
|
|
581
588
|
Pressure: Pressure
|
|
582
589
|
Project: Project
|
|
583
590
|
Project changed to: Project changed to
|
|
591
|
+
Asked %s to move: Asked %s to move
|
|
592
|
+
Waiting for %s to report: Waiting for %s to report
|
|
584
593
|
Project ID: Project ID
|
|
585
594
|
Project ID *: Project ID *
|
|
586
595
|
Project Name: Project Name
|
|
@@ -733,7 +742,7 @@ FR:
|
|
|
733
742
|
close: fermer
|
|
734
743
|
Collapse: Réduire
|
|
735
744
|
Color: Couleur
|
|
736
|
-
"Compiled into
|
|
745
|
+
"Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Compilé dans le micrologiciel et utilisé pour demander l'identifiant de broker. Ajoutez-en un second pour le changer : les deux sont acceptés, donc les nœuds déjà flashés continuent de fonctionner jusqu'à ce que vous retiriez l'ancien."
|
|
737
746
|
Connect board: Connecter la carte
|
|
738
747
|
connected: connecté
|
|
739
748
|
connecting: connexion
|
|
@@ -756,7 +765,6 @@ FR:
|
|
|
756
765
|
Done: Fait
|
|
757
766
|
ds18b20: ds18b20
|
|
758
767
|
e.g. LiteFarm: par ex. LiteFarm
|
|
759
|
-
Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.: Chaque nœud a son propre identifiant de broker, qu'il récupère sur ce serveur lors de sa première connexion. Oubliez-en un pour qu'un nouvel identifiant lui soit délivré - nécessaire si son système de fichiers a été effacé, car il ne peut alors plus prouver quel nœud il est.
|
|
760
768
|
eCO2: eCO2
|
|
761
769
|
Email: Email
|
|
762
770
|
Enrol URL: URL d'enrôlement
|
|
@@ -843,7 +851,7 @@ FR:
|
|
|
843
851
|
No farms registered for this organization yet.: Aucune ferme enregistrée pour cette organisation pour l'instant.
|
|
844
852
|
No nodes found for this farm's project(s).: Aucun nœud trouvé pour le(s) projet(s) de cette ferme.
|
|
845
853
|
No nodes found for this organization: Aucun nœud trouvé pour cette organisation
|
|
846
|
-
No nodes have enrolled yet
|
|
854
|
+
No nodes have enrolled yet, and none have asked and been rejected.: Aucun nœud ne s'est encore enrôlé, et aucun n'a été refusé.
|
|
847
855
|
No organization: Aucune organisation
|
|
848
856
|
No organization selected: Aucune organisation sélectionnée
|
|
849
857
|
No OTA files uploaded yet.: Aucun fichier OTA téléversé pour l'instant.
|
|
@@ -860,6 +868,7 @@ FR:
|
|
|
860
868
|
Nodes: Nœuds
|
|
861
869
|
Nodes in Farm: Nœuds dans la ferme
|
|
862
870
|
Nodes in Organization: Nœuds dans l'organisation
|
|
871
|
+
Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).: Nœuds ayant obtenu leur identifiant de broker à la première connexion. Oublier en délivre un nouveau à la prochaine connexion, nécessaire si la mémoire a été effacée. Les nœuds refusés sont listés - cliquez sur Approuver pour autoriser (à la prochaine tentative, généralement 30 minutes).
|
|
863
872
|
Not in the schema: Absent du schéma
|
|
864
873
|
Not selected: Non sélectionné
|
|
865
874
|
Note this is your organization - not the organizations whose devices you want to access.: Ceci est votre organisation - pas les organisations dont vous voulez accéder aux appareils.
|
|
@@ -891,6 +900,8 @@ FR:
|
|
|
891
900
|
Pressure: Pression
|
|
892
901
|
Project: Projet
|
|
893
902
|
Project changed to: Projet changé en
|
|
903
|
+
Asked %s to move: Déplacement demandé à %s
|
|
904
|
+
Waiting for %s to report: En attente du rapport de %s
|
|
894
905
|
Project ID: ID du projet
|
|
895
906
|
Project ID *: ID du projet *
|
|
896
907
|
Project Name: Nom du projet
|
|
@@ -1043,7 +1054,7 @@ HI:
|
|
|
1043
1054
|
close: बंद करें
|
|
1044
1055
|
Collapse: छोटा करें
|
|
1045
1056
|
Color: रंग
|
|
1046
|
-
"Compiled into
|
|
1057
|
+
"Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "फ़र्मवेयर में संकलित और ब्रोकर क्रेडेंशियल माँगने के लिए उपयोग किया जाता है। इसे बदलने के लिए दूसरा जोड़ें: दोनों स्वीकार किए जाते हैं, इसलिए पहले फ़्लैश किए गए नोड तब तक काम करते रहते हैं जब तक आप पुराना वापस नहीं लेते।"
|
|
1047
1058
|
Connect board: बोर्ड कनेक्ट करें
|
|
1048
1059
|
connected: जुड़े हुए
|
|
1049
1060
|
connecting: कनेक्ट हो रहा है
|
|
@@ -1066,7 +1077,6 @@ HI:
|
|
|
1066
1077
|
Done: पूर्ण
|
|
1067
1078
|
ds18b20: ds18b20
|
|
1068
1079
|
e.g. LiteFarm: उदाहरण के लिए LiteFarm
|
|
1069
|
-
Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.: हर नोड का अपना ब्रोकर क्रेडेंशियल होता है, जो वह पहली बार जुड़ते समय इस सर्वर से लेता है। किसी को भुला दें ताकि उसे नया क्रेडेंशियल मिले - यह ज़रूरी है यदि उसका फ़ाइल-सिस्टम मिटा दिया गया हो, क्योंकि तब वह यह सिद्ध नहीं कर सकता कि वह कौन सा नोड है।
|
|
1070
1080
|
eCO2: ईसीओ2
|
|
1071
1081
|
Email: ईमेल
|
|
1072
1082
|
Enrol URL: पंजीकरण URL
|
|
@@ -1153,7 +1163,7 @@ HI:
|
|
|
1153
1163
|
No farms registered for this organization yet.: इस संगठन के लिए अभी तक कोई फार्म पंजीकृत नहीं है।
|
|
1154
1164
|
No nodes found for this farm's project(s).: इस फार्म के प्रोजेक्ट (प्रोजेक्ट्स) के लिए कोई नोड नहीं मिला।
|
|
1155
1165
|
No nodes found for this organization: इस संगठन के लिए कोई नोड नहीं मिला
|
|
1156
|
-
No nodes have enrolled yet
|
|
1166
|
+
No nodes have enrolled yet, and none have asked and been rejected.: अभी तक कोई नोड पंजीकृत नहीं हुआ है, और किसी को अस्वीकार नहीं किया गया है।
|
|
1157
1167
|
No organization: कोई संगठन नहीं
|
|
1158
1168
|
No organization selected: कोई संगठन चयनित नहीं
|
|
1159
1169
|
No OTA files uploaded yet.: अभी तक कोई OTA फ़ाइल अपलोड नहीं की गई है।
|
|
@@ -1170,6 +1180,7 @@ HI:
|
|
|
1170
1180
|
Nodes: नोड्स
|
|
1171
1181
|
Nodes in Farm: फार्म में नोड्स
|
|
1172
1182
|
Nodes in Organization: संगठन में नोड्स
|
|
1183
|
+
Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).: वे नोड जिन्होंने पहली बार जुड़ने पर ब्रोकर क्रेडेंशियल प्राप्त किए हैं। भुलाएँ अगली बार जुड़ने पर नया क्रेडेंशियल जारी करता है, और यह ज़रूरी है यदि मेमोरी मिटा दी गई हो। अस्वीकृत नोड भी सूचीबद्ध हैं - अनुमति देने के लिए स्वीकृत करें पर क्लिक करें (अगले प्रयास पर, आमतौर पर 30 मिनट)।
|
|
1173
1184
|
Not in the schema: स्कीमा में नहीं
|
|
1174
1185
|
Not selected: चयनित नहीं
|
|
1175
1186
|
Note this is your organization - not the organizations whose devices you want to access.: ध्यान दें यह आपका संगठन है - वे संगठन नहीं जिनके उपकरणों तक आप पहुंचना चाहते हैं।
|
|
@@ -1201,6 +1212,8 @@ HI:
|
|
|
1201
1212
|
Pressure: दाब
|
|
1202
1213
|
Project: परियोजना
|
|
1203
1214
|
Project changed to: प्रोजेक्ट बदलकर किया गया
|
|
1215
|
+
Asked %s to move: "%s को स्थानांतरित करने को कहा"
|
|
1216
|
+
Waiting for %s to report: "%s की रिपोर्ट की प्रतीक्षा"
|
|
1204
1217
|
Project ID: प्रोजेक्ट आईडी
|
|
1205
1218
|
Project ID *: प्रोजेक्ट आईडी *
|
|
1206
1219
|
Project Name: प्रोजेक्ट का नाम
|
|
@@ -1353,7 +1366,7 @@ ID:
|
|
|
1353
1366
|
close: tutup
|
|
1354
1367
|
Collapse: Ciutkan
|
|
1355
1368
|
Color: Warna
|
|
1356
|
-
"Compiled into
|
|
1369
|
+
"Compiled into firmware and used to request broker credential. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Dikompilasi ke dalam firmware dan digunakan untuk meminta kredensial broker. Tambahkan yang kedua untuk menggantinya: keduanya diterima, sehingga node yang sudah di-flash tetap bekerja sampai Anda mencabut yang lama."
|
|
1357
1370
|
Connect board: Hubungkan papan
|
|
1358
1371
|
connected: terhubung
|
|
1359
1372
|
connecting: menghubungkan
|
|
@@ -1376,7 +1389,6 @@ ID:
|
|
|
1376
1389
|
Done: Selesai
|
|
1377
1390
|
ds18b20: ds18b20
|
|
1378
1391
|
e.g. LiteFarm: misalnya LiteFarm
|
|
1379
|
-
Each node has its own broker credential, which it collects from this server the first time it connects. Forget one to have it issued a new credential - needed if its filesystem has been erased, because it can then no longer prove which node it is.: Setiap node memiliki kredensial broker sendiri, yang diambilnya dari server ini saat pertama kali tersambung. Lupakan salah satu agar diberi kredensial baru - diperlukan jika sistem berkasnya telah dihapus, karena node itu tidak lagi dapat membuktikan node yang mana dirinya.
|
|
1380
1392
|
eCO2: eCO2
|
|
1381
1393
|
Email: Email
|
|
1382
1394
|
Enrol URL: URL pendaftaran
|
|
@@ -1463,7 +1475,7 @@ ID:
|
|
|
1463
1475
|
No farms registered for this organization yet.: Belum ada farm yang terdaftar untuk organisasi ini.
|
|
1464
1476
|
No nodes found for this farm's project(s).: Tidak ada node ditemukan untuk proyek farm ini.
|
|
1465
1477
|
No nodes found for this organization: Tidak ada node ditemukan untuk organisasi ini
|
|
1466
|
-
No nodes have enrolled yet
|
|
1478
|
+
No nodes have enrolled yet, and none have asked and been rejected.: Belum ada node yang terdaftar, dan tidak ada yang ditolak.
|
|
1467
1479
|
No organization: Tidak ada organisasi
|
|
1468
1480
|
No organization selected: Tidak ada organisasi yang dipilih
|
|
1469
1481
|
No OTA files uploaded yet.: Belum ada berkas OTA yang diunggah.
|
|
@@ -1480,6 +1492,7 @@ ID:
|
|
|
1480
1492
|
Nodes: Node
|
|
1481
1493
|
Nodes in Farm: Node dalam Farm
|
|
1482
1494
|
Nodes in Organization: Node dalam Organisasi
|
|
1495
|
+
Nodes that have obtained broker credentials on first connection. Forget reissues a new credential at next connection and is needed if memory has been erased. Refused nodes are listed - click Approve to allow (on next retry, typically 30 minutes).: Node yang telah memperoleh kredensial broker saat pertama kali tersambung. Lupakan menerbitkan kredensial baru pada sambungan berikutnya, dan diperlukan jika memori telah dihapus. Node yang ditolak juga dicantumkan - klik Setujui untuk mengizinkan (pada percobaan berikutnya, biasanya 30 menit).
|
|
1483
1496
|
Not in the schema: Tidak ada di skema
|
|
1484
1497
|
Not selected: Tidak dipilih
|
|
1485
1498
|
Note this is your organization - not the organizations whose devices you want to access.: Ini adalah organisasi Anda - bukan organisasi yang perangkatnya ingin Anda akses.
|
|
@@ -1511,6 +1524,8 @@ ID:
|
|
|
1511
1524
|
Pressure: Tekanan
|
|
1512
1525
|
Project: Proyek
|
|
1513
1526
|
Project changed to: Proyek diubah menjadi
|
|
1527
|
+
Asked %s to move: Meminta %s pindah
|
|
1528
|
+
Waiting for %s to report: Menunggu laporan %s
|
|
1514
1529
|
Project ID: ID Proyek
|
|
1515
1530
|
Project ID *: ID Proyek *
|
|
1516
1531
|
Project Name: Nama Proyek
|
|
@@ -1818,8 +1833,13 @@ class MqttTopic {
|
|
|
1818
1833
|
// Creation & initialization =========
|
|
1819
1834
|
constructor() {
|
|
1820
1835
|
this.data = []; // Tracks previous values of this topic - used for graphing
|
|
1821
|
-
this
|
|
1822
|
-
|
|
1836
|
+
/* For what this publishes - "set/..." commands to a node. QoS 1 so the broker holds them for a
|
|
1837
|
+
* node that is asleep, until it reconnects to its persistent session; QoS 0 is dropped for anyone
|
|
1838
|
+
* not connected at that moment. Not retained: the node writes a set value to its own flash, and
|
|
1839
|
+
* a retained command would be replayed at every fresh subscribe, e.g. each reboot.
|
|
1840
|
+
*/
|
|
1841
|
+
this.qos = 1;
|
|
1842
|
+
this.retain = false;
|
|
1823
1843
|
this.state = {}; // Dynamic state: value, groupMt roll-up fields (now_wired, out_wired, on, etc.)
|
|
1824
1844
|
}
|
|
1825
1845
|
|
|
@@ -2021,8 +2041,6 @@ class MqttTopic {
|
|
|
2021
2041
|
case 'toggle':
|
|
2022
2042
|
//noinspection JSUnresolvedVariable
|
|
2023
2043
|
elx = el('mqtt-toggle', {color: this.color, graphable: this.graphable });
|
|
2024
|
-
this.retain = true;
|
|
2025
|
-
this.qos = 1;
|
|
2026
2044
|
break;
|
|
2027
2045
|
case 'bar':
|
|
2028
2046
|
// noinspection JSUnresolvedReference
|
|
@@ -2305,7 +2323,7 @@ class MqttTopic {
|
|
|
2305
2323
|
}
|
|
2306
2324
|
publishWired(val) {
|
|
2307
2325
|
console.log("Publishing ", this.topicWiredPath, val, this.retain ? "retain" : "", this.qos ? `qos=${this.qos}` : "");
|
|
2308
|
-
mqtt_client.publish(this.topicWiredPath, val, {retain:
|
|
2326
|
+
mqtt_client.publish(this.topicWiredPath, val, {retain: this.retain, qos: this.qos});
|
|
2309
2327
|
}
|
|
2310
2328
|
// Adds historical data to the chart - typically chart updates data for each line, then updates the chart.
|
|
2311
2329
|
addDataFrom(filename, first, cb) {
|
|
@@ -3329,6 +3347,7 @@ export {
|
|
|
3329
3347
|
mqttTempConnect,
|
|
3330
3348
|
mqtt_deliver,
|
|
3331
3349
|
mqtt_subscribe,
|
|
3350
|
+
mqtt_unsubscribe,
|
|
3332
3351
|
mqtt_unsubscribe_organization,
|
|
3333
3352
|
nextUniqueId,
|
|
3334
3353
|
nowMs,
|
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/graph.js
CHANGED
|
@@ -503,7 +503,7 @@ customElements.define('mqtt-graphdataset', MqttGraphDataset);
|
|
|
503
503
|
// This event is used by custom dashboards to send
|
|
504
504
|
document.addEventListener('frugaliot:publish', ({detail}) => {
|
|
505
505
|
if (mqtt_client && detail.topic && detail.value !== undefined)
|
|
506
|
-
mqtt_client.publish(detail.topic, String(detail.value), detail.options || {retain:
|
|
506
|
+
mqtt_client.publish(detail.topic, String(detail.value), detail.options || {retain: false, qos: 1}); // As for MqttTopic - see its constructor
|
|
507
507
|
});
|
|
508
508
|
|
|
509
509
|
// Public API for custom dashboards and pages that import this module.
|
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);
|