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/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 a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "Compiled into a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one."
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. A node enrols itself the first time it connects.: No nodes have enrolled yet. A node enrols itself the first time it connects.
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 a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. 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 d'un nœud, c'est ce qui lui permet de demander à ce serveur son propre identifiant de broker. Il n'accorde rien d'autre - ni lecture ni publication. 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."
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. A node enrols itself the first time it connects.: Aucun nœud ne s'est encore enrôlé. Un nœud s'enrôle lui-même lors de sa première connexion.
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 a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. Add a second one to change it: both are accepted, so nodes already flashed keep working until you withdraw the old one.": "नोड के फ़र्मवेयर में संकलित, यही उसे इस सर्वर से अपना ब्रोकर क्रेडेंशियल माँगने देता है। यह और कुछ नहीं देता - न पढ़ना, न प्रकाशित करना। इसे बदलने के लिए दूसरा जोड़ें: दोनों स्वीकार किए जाते हैं, इसलिए पहले फ़्लैश किए गए नोड तब तक काम करते रहते हैं जब तक आप पुराना वापस नहीं लेते।"
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. A node enrols itself the first time it connects.: अभी तक कोई नोड पंजीकृत नहीं हुआ है। नोड पहली बार जुड़ते समय स्वयं पंजीकृत हो जाता है।
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 a node's firmware, this is what lets it ask this server for its own broker credential. It grants nothing else - no reading and no publishing. 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 sebuah node, inilah yang memungkinkannya meminta kredensial broker miliknya sendiri dari server ini. Ia tidak memberi apa pun selain itu - tidak membaca dan tidak menerbitkan. Tambahkan yang kedua untuk menggantinya: keduanya diterima, sehingga node yang sudah di-flash tetap bekerja sampai Anda mencabut yang lama."
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. A node enrols itself the first time it connects.: Belum ada node yang mendaftar. Sebuah node mendaftarkan dirinya sendiri saat pertama kali tersambung.
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.qos = 0; // Default to send and not care if received
1822
- this.retain = false; // Default to not retain
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: true, qos: 1});
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,
@@ -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`, `frugal_8mb`, …) | No |
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`, size `0x1E0000`.
198
- Differs → the board is laid out for something else → **provision**.
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, and it's what the fixed app
205
- geometry buys: a board whose FS partition is a different size than our scheme would produce is still
206
- perfectly safe to update app-only. That correctly covers a board provisioned by an earlier version of
207
- this tool, or flashed by hand from PlatformIO with stock `min_spiffs`, instead of needlessly wiping
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
- frugal_8mb.bin — custom, see below
276
- frugal_8mb.csv
277
- frugal_16mb.bin
278
- frugal_16mb.csv
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: fixed app size, all spare space to LittleFS
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
- | Partition | Type/Subtype | Offset | `min_spiffs` (4 MB) | `frugal_8mb` | `frugal_16mb` |
293
- |---|---|---|---|---|---|
294
- | `nvs` | data/nvs | `0x9000` | `0x5000` | `0x5000` | `0x5000` |
295
- | `otadata` | data/ota | `0xe000` | `0x2000` | `0x2000` | `0x2000` |
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
- App partitions stay 64 KB-aligned and the FS partition stays 4 KB-aligned in all three, and each
302
- table sums exactly to its flash size.
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
- **Two consequences worth relying on:**
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
- 1. **The app image is flash-size-independent.** `app0` has the same offset *and* size in every
307
- scheme, so one `firmware.bin` flashes to a 4, 8 or 16 MB board. The OTA files in the tab need no
308
- per-size variants, and the app-only path always writes at `0x10000`.
309
- 2. **A 1.875 MB app slot is generous** and unchanged from what 4 MB boards already run, so nothing
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
- These CSVs are Frugal IoT's own, so they belong in the **firmware** repo (so builds can reference them
319
- via `board_build.partitions`), with the generated `.bin` plus a copy of the `.csv` committed here for
320
- the flasher. Generate with the core's `tools/gen_esp32part.py`, or just harvest
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": "frugal_8mb",
333
- "ESP32-S3-16MB": "frugal_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, write a CSV following the table above — same app partitions, all
384
- surplus to `spiffs` — and commit both it and the generated `.bin` to `base/partitions/`.
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 from the parsed partition table. Catches a `min_spiffs` app
401
- dropped onto a `default`-scheme board.
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. The `frugal_8mb` / `frugal_16mb`
851
- tables above are specified so they *can* be added without redesign, but they should be generated and
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 Frugal IoT scheme keeps app0 here, so one firmware.bin suits any flash size - see FLASH_PLAN.md
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
- const AppPartitionSize = 0x1e0000;
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.appLimit = AppPartitionSize;
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 - a differently sized filesystem is still safe to keep
299
- else if (app.offset !== AppPartitionOffset || app.size !== AppPartitionSize) reasons.push("app partition differs");
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
- if (dev.appLimit && bytes.length > dev.appLimit) {
363
- throw new Error(`${this.state.app.name} is ${bytesToSizeString(bytes.length)}, larger than the ${bytesToSizeString(dev.appLimit)} app partition`);
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: true, qos: 1});
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
@@ -41,5 +41,5 @@
41
41
  "unlink:hee": "rm -f node_modules/html-element-extended && npm install html-element-extended"
42
42
  },
43
43
  "type": "module",
44
- "version": "2.0.4"
44
+ "version": "2.0.6"
45
45
  }
@@ -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);