frugal-iot-client 1.3.12 → 1.3.13

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/webcomponents.js CHANGED
@@ -285,6 +285,7 @@ EN:
285
285
  Auth Token: Auth Token
286
286
  Base URL: Base URL
287
287
  Battery: Battery
288
+ Board: Board
288
289
  Brightness: Brightness
289
290
  Built in LED: Built in LED
290
291
  buttons: buttons
@@ -293,6 +294,7 @@ EN:
293
294
  Climate: Climate
294
295
  close: close
295
296
  Color: Color
297
+ Connect board: Connect board
296
298
  connected: connected
297
299
  connecting: connecting
298
300
  Control: Control
@@ -312,9 +314,17 @@ EN:
312
314
  Farm registered: Farm registered
313
315
  Farms: Farms
314
316
  File: File
317
+ Firmware: Firmware
318
+ Firmware file: Firmware file
319
+ Flash: Flash
320
+ Flash over USB: Flash over USB
321
+ Flash size was not detected reliably - flash anyway: Flash size was not detected reliably - flash anyway
322
+ Flash this over USB: Flash this over USB
323
+ Flashing needs the Web Serial API - use Chrome, Edge or Opera on a desktop computer: Flashing needs the Web Serial API - use Chrome, Edge or Opera on a desktop computer
315
324
  for node: for node
316
325
  From: From
317
326
  Frugal-IoT Username *: Frugal-IoT Username *
327
+ Full provision - all configuration on the board will be erased: Full provision - all configuration on the board will be erased
318
328
  Greater Than: Greater Than
319
329
  heating: heating
320
330
  humidifier: humidifier
@@ -335,6 +345,7 @@ EN:
335
345
  Manual: Manual
336
346
  Name: Name
337
347
  Never seen: Never seen
348
+ No board connected: No board connected
338
349
  No farms registered for this organization yet.: No farms registered for this organization yet.
339
350
  No nodes found for this farm's project(s).: No nodes found for this farm's project(s).
340
351
  No nodes found for this organization: No nodes found for this organization
@@ -380,6 +391,7 @@ EN:
380
391
  Project Name: Project Name
381
392
  Projects: Projects
382
393
  Property: Property
394
+ Provision instead (erases config): Provision instead (erases config)
383
395
  Publish Message: Publish Message
384
396
  "Published to ": "Published to "
385
397
  QoS: QoS
@@ -411,6 +423,7 @@ EN:
411
423
  Soil Temperature: Soil Temperature
412
424
  Sonoff R2 switch: Sonoff R2 switch
413
425
  Sonoff switch: Sonoff switch
426
+ Speed: Speed
414
427
  SSID: SSID
415
428
  Submit: Submit
416
429
  System: System
@@ -427,7 +440,10 @@ EN:
427
440
  Unable to find project: Unable to find project
428
441
  Unauthorized: Unauthorized
429
442
  undefined: undefined
443
+ Unsupported board: Unsupported board
430
444
  Unused: Unused
445
+ Updating the app only: Updating the app only
446
+ Updating the app only - the existing configuration will be preserved: Updating the app only - the existing configuration will be preserved
431
447
  Upload: Upload
432
448
  Username: Username
433
449
  Value: Value
@@ -450,6 +466,7 @@ FR:
450
466
  Auth Token: Jeton d'authentification
451
467
  Base URL: URL de base
452
468
  Battery: Batterie
469
+ Board: Carte
453
470
  Brightness: Luminosité
454
471
  Built in LED: LED intégrée
455
472
  buttons: boutons
@@ -458,6 +475,7 @@ FR:
458
475
  Climate: Climat
459
476
  close: fermer
460
477
  Color: Couleur
478
+ Connect board: Connecter la carte
461
479
  connected: connecté
462
480
  connecting: connexion
463
481
  Control: Contrôle
@@ -477,9 +495,17 @@ FR:
477
495
  Farm registered: Ferme enregistrée
478
496
  Farms: Fermes
479
497
  File: Fichier
498
+ Firmware: Micrologiciel
499
+ Firmware file: Fichier micrologiciel
500
+ Flash: Flasher
501
+ Flash over USB: Flasher par USB
502
+ Flash size was not detected reliably - flash anyway: Taille de la mémoire flash non détectée de façon fiable - flasher quand même
503
+ Flash this over USB: Flasher ceci par USB
504
+ Flashing needs the Web Serial API - use Chrome, Edge or Opera on a desktop computer: Le flashage nécessite l'API Web Serial - utilisez Chrome, Edge ou Opera sur un ordinateur de bureau
480
505
  for node: pour le nœud
481
506
  From: De
482
507
  Frugal-IoT Username *: Nom d'utilisateur Frugal-IoT *
508
+ Full provision - all configuration on the board will be erased: Provisionnement complet - toute la configuration de la carte sera effacée
483
509
  Greater Than: Supérieur à
484
510
  heating: chauffage
485
511
  humidifier: humidificateur
@@ -502,6 +528,7 @@ FR:
502
528
  Manual: Manuel
503
529
  Name: Nom
504
530
  Never seen: Jamais vu
531
+ No board connected: Aucune carte connectée
505
532
  No farms registered for this organization yet.: Aucune ferme enregistrée pour cette organisation pour l'instant.
506
533
  No nodes found for this farm's project(s).: Aucun nœud trouvé pour le(s) projet(s) de cette ferme.
507
534
  No nodes found for this organization: Aucun nœud trouvé pour cette organisation
@@ -547,6 +574,7 @@ FR:
547
574
  Project Name: Nom du projet
548
575
  Projects: Projets
549
576
  Property: Propriété
577
+ Provision instead (erases config): Provisionner plutôt (effacer la configuration)
550
578
  Publish Message: Publier un message
551
579
  "Published to ": "Publié sur "
552
580
  QoS: QoS
@@ -578,6 +606,7 @@ FR:
578
606
  Soil Temperature: Température du sol
579
607
  Sonoff R2 switch: Interrupteur Sonoff R2
580
608
  Sonoff switch: Interrupteur Sonoff
609
+ Speed: Vitesse
581
610
  SSID: SSID
582
611
  Submit: Soumettre
583
612
  System: Système
@@ -594,7 +623,10 @@ FR:
594
623
  Unable to find project: Impossible de trouver le projet
595
624
  Unauthorized: Non autorisé
596
625
  undefined: indéfini
626
+ Unsupported board: Carte non prise en charge
597
627
  Unused: Inutilisé
628
+ Updating the app only: Mise à jour de l'application uniquement
629
+ Updating the app only - the existing configuration will be preserved: Mise à jour de l'application uniquement - la configuration existante sera conservée
598
630
  Upload: Téléverser
599
631
  Username: Nom de User
600
632
  Value: Valeur
@@ -617,6 +649,7 @@ HI:
617
649
  Auth Token: प्रमाणीकरण टोकन
618
650
  Base URL: बेस यूआरएल
619
651
  Battery: बैटरी
652
+ Board: बोर्ड
620
653
  Brightness: चमक
621
654
  Built in LED: बिल्ट-इन एलईडी
622
655
  buttons: बटन
@@ -625,6 +658,7 @@ HI:
625
658
  Climate: जलवायु
626
659
  close: बंद करें
627
660
  Color: रंग
661
+ Connect board: बोर्ड कनेक्ट करें
628
662
  connected: जुड़े हुए
629
663
  connecting: कनेक्ट हो रहा है
630
664
  Control: नियंत्रण
@@ -644,9 +678,17 @@ HI:
644
678
  Farm registered: फार्म पंजीकृत किया गया
645
679
  Farms: फार्म
646
680
  File: फ़ाइल
681
+ Firmware: फर्मवेयर
682
+ Firmware file: फर्मवेयर फ़ाइल
683
+ Flash: फ्लैश करें
684
+ Flash over USB: USB से फ्लैश करें
685
+ Flash size was not detected reliably - flash anyway: फ्लैश आकार का विश्वसनीय पता नहीं चला - फिर भी फ्लैश करें
686
+ Flash this over USB: इसे USB से फ्लैश करें
687
+ Flashing needs the Web Serial API - use Chrome, Edge or Opera on a desktop computer: फ्लैश करने के लिए Web Serial API आवश्यक है - डेस्कटॉप कंप्यूटर पर Chrome, Edge या Opera का उपयोग करें
647
688
  for node: नोड के लिए
648
689
  From: से
649
690
  Frugal-IoT Username *: Frugal-IoT उपयोगकर्ता नाम *
691
+ Full provision - all configuration on the board will be erased: पूर्ण प्रोविजनिंग - बोर्ड की सारी कॉन्फ़िगरेशन मिट जाएगी
650
692
  Greater Than: इससे बड़ा
651
693
  heating: हीटिंग
652
694
  humidifier: ह्यूमिडिफ़ायर
@@ -669,6 +711,7 @@ HI:
669
711
  Manual: मैनुअल
670
712
  Name: नाम
671
713
  Never seen: कभी नहीं देखा
714
+ No board connected: कोई बोर्ड कनेक्ट नहीं है
672
715
  No farms registered for this organization yet.: इस संगठन के लिए अभी तक कोई फार्म पंजीकृत नहीं है।
673
716
  No nodes found for this farm's project(s).: इस फार्म के प्रोजेक्ट (प्रोजेक्ट्स) के लिए कोई नोड नहीं मिला।
674
717
  No nodes found for this organization: इस संगठन के लिए कोई नोड नहीं मिला
@@ -714,6 +757,7 @@ HI:
714
757
  Project Name: प्रोजेक्ट का नाम
715
758
  Projects: प्रोजेक्ट्स
716
759
  Property: प्रॉपर्टी
760
+ Provision instead (erases config): इसके बजाय प्रोविजन करें (कॉन्फ़िगरेशन मिटेगी)
717
761
  Publish Message: संदेश प्रकाशित करें
718
762
  "Published to ": "पर प्रकाशित किया गया "
719
763
  QoS: QoS
@@ -745,6 +789,7 @@ HI:
745
789
  Soil Temperature: मिट्टी का तापमान
746
790
  Sonoff R2 switch: सोनऑफ R2 स्विच
747
791
  Sonoff switch: सोनऑफ स्विच
792
+ Speed: गति
748
793
  SSID: SSID
749
794
  Submit: जमा करें
750
795
  System: सिस्टम
@@ -761,7 +806,10 @@ HI:
761
806
  Unable to find project: प्रोजेक्ट नहीं मिल सका
762
807
  Unauthorized: अनधिकृत
763
808
  undefined: अपरिभाषित
809
+ Unsupported board: असमर्थित बोर्ड
764
810
  Unused: अप्रयुक्त
811
+ Updating the app only: केवल ऐप अपडेट हो रहा है
812
+ Updating the app only - the existing configuration will be preserved: केवल ऐप अपडेट हो रहा है - मौजूदा कॉन्फ़िगरेशन सुरक्षित रहेगी
765
813
  Upload: अपलोड
766
814
  Username: उपयोगकर्ता नाम
767
815
  Value: मान
@@ -784,6 +832,7 @@ ID:
784
832
  Auth Token: Token Autentikasi
785
833
  Base URL: URL Dasar
786
834
  Battery: Baterai
835
+ Board: Papan
787
836
  Brightness: Kecerahan
788
837
  Built in LED: LED bawaan
789
838
  buttons: tombol
@@ -792,6 +841,7 @@ ID:
792
841
  Climate: Iklim
793
842
  close: tutup
794
843
  Color: Warna
844
+ Connect board: Hubungkan papan
795
845
  connected: terhubung
796
846
  connecting: menghubungkan
797
847
  Control: Kontrol
@@ -811,9 +861,17 @@ ID:
811
861
  Farm registered: Farm terdaftar
812
862
  Farms: Farm
813
863
  File: Berkas
864
+ Firmware: Firmware
865
+ Firmware file: Berkas firmware
866
+ Flash: Flash
867
+ Flash over USB: Flash melalui USB
868
+ Flash size was not detected reliably - flash anyway: Ukuran flash tidak terdeteksi dengan pasti - flash saja
869
+ Flash this over USB: Flash ini melalui USB
870
+ Flashing needs the Web Serial API - use Chrome, Edge or Opera on a desktop computer: Flashing memerlukan Web Serial API - gunakan Chrome, Edge atau Opera di komputer desktop
814
871
  for node: untuk node
815
872
  From: Dari
816
873
  Frugal-IoT Username *: Nama Pengguna Frugal-IoT *
874
+ Full provision - all configuration on the board will be erased: Provisioning penuh - semua konfigurasi pada papan akan dihapus
817
875
  Greater Than: Lebih dari
818
876
  heating: pemanas
819
877
  humidifier: pelembap
@@ -836,6 +894,7 @@ ID:
836
894
  Manual: Manual
837
895
  Name: Nama
838
896
  Never seen: Belum pernah terlihat
897
+ No board connected: Tidak ada papan terhubung
839
898
  No farms registered for this organization yet.: Belum ada farm yang terdaftar untuk organisasi ini.
840
899
  No nodes found for this farm's project(s).: Tidak ada node ditemukan untuk proyek farm ini.
841
900
  No nodes found for this organization: Tidak ada node ditemukan untuk organisasi ini
@@ -881,6 +940,7 @@ ID:
881
940
  Project Name: Nama Proyek
882
941
  Projects: Proyek
883
942
  Property: Properti
943
+ Provision instead (erases config): Provisioning saja (menghapus konfigurasi)
884
944
  Publish Message: Publikasikan Pesan
885
945
  "Published to ": "Dipublikasikan ke "
886
946
  QoS: QoS
@@ -912,6 +972,7 @@ ID:
912
972
  Soil Temperature: Suhu Tanah
913
973
  Sonoff R2 switch: Saklar Sonoff R2
914
974
  Sonoff switch: Saklar Sonoff
975
+ Speed: Kecepatan
915
976
  SSID: SSID
916
977
  Submit: Kirim
917
978
  System: Sistem
@@ -928,7 +989,10 @@ ID:
928
989
  Unable to find project: Tidak dapat menemukan proyek
929
990
  Unauthorized: Tidak sah
930
991
  undefined: tidak ditentukan
992
+ Unsupported board: Papan tidak didukung
931
993
  Unused: Tidak digunakan
994
+ Updating the app only: Hanya memperbarui aplikasi
995
+ Updating the app only - the existing configuration will be preserved: Hanya memperbarui aplikasi - konfigurasi yang ada akan dipertahankan
932
996
  Upload: Unggah
933
997
  Username: Nama Pengguna
934
998
  Value: Nilai
@@ -2132,6 +2196,451 @@ class TabbedDisplay extends HTMLElementExtended {
2132
2196
  }
2133
2197
  customElements.define('tabbed-display', TabbedDisplay);
2134
2198
 
2199
+ // ---------- USB flashing over WebSerial ----------
2200
+ // The .bin files the OTA tab handles are application images. An ESP32 also needs a bootloader,
2201
+ // partition table and otadata, which are app-independent and kept in base/ - see FLASH_PLAN.md.
2202
+ const BaseUrl = './base';
2203
+ const PartitionTableOffset = 0x8000;
2204
+ const PartitionTableMagic = 0x50aa; // AA 50 on the wire
2205
+ const PartitionType = {app: 0x00, data: 0x01};
2206
+ const PartitionSubtype = {factory: 0x00, ota0: 0x10, otadata: 0x00, spiffs: 0x82, littlefs: 0x83};
2207
+ // Every Frugal IoT scheme keeps app0 here, so one firmware.bin suits any flash size - see FLASH_PLAN.md
2208
+ const AppPartitionOffset = 0x10000;
2209
+ const AppPartitionSize = 0x1e0000;
2210
+ const ImageMagic = 0xe9;
2211
+ const LittleFsMagic = [0x6c, 0x69, 0x74, 0x74, 0x6c, 0x65, 0x66, 0x73]; // "littlefs" in the superblock
2212
+ const FlashBaudRates = [921600, 460800, 230400, 115200];
2213
+ // JEDEC capacity byte -> size, mirroring esptool-js's own table so we can tell a real detection from
2214
+ // its silent 4MB fallback, which on an S3 is also the signature of unsupported octal flash
2215
+ const DetectedFlashSizes = {
2216
+ 0x12: "256KB", 0x13: "512KB", 0x14: "1MB", 0x15: "2MB", 0x16: "4MB", 0x17: "8MB", 0x18: "16MB",
2217
+ 0x19: "32MB", 0x1a: "64MB", 0x1b: "128MB", 0x1c: "256MB",
2218
+ 0x20: "64MB", 0x21: "128MB", 0x22: "256MB",
2219
+ 0x32: "256KB", 0x33: "512KB", 0x34: "1MB", 0x35: "2MB", 0x36: "4MB", 0x37: "8MB", 0x38: "16MB",
2220
+ 0x39: "32MB", 0x3a: "64MB",
2221
+ };
2222
+
2223
+ // 32-byte records: magic u16, type, subtype, offset u32, size u32, label[16], flags u32
2224
+ function partitionsParse(bytes) {
2225
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2226
+ const parts = [];
2227
+ for (let o = 0; o + 32 <= bytes.length; o += 32) {
2228
+ if (dv.getUint16(o, true) !== PartitionTableMagic) break;
2229
+ parts.push({
2230
+ type: bytes[o + 2],
2231
+ subtype: bytes[o + 3],
2232
+ offset: dv.getUint32(o + 4, true),
2233
+ size: dv.getUint32(o + 8, true),
2234
+ label: new TextDecoder().decode(bytes.subarray(o + 12, o + 28)).replace(/\0.*$/, ''),
2235
+ });
2236
+ }
2237
+ return parts;
2238
+ }
2239
+ function partitionApp(parts) {
2240
+ return parts.find((p) => p.type === PartitionType.app &&
2241
+ (p.subtype === PartitionSubtype.factory || p.subtype === PartitionSubtype.ota0));
2242
+ }
2243
+ function partitionOtadata(parts) {
2244
+ return parts.find((p) => p.type === PartitionType.data && p.subtype === PartitionSubtype.otadata);
2245
+ }
2246
+ // Arduino's LittleFS keeps the partition labelled "spiffs", so match on subtype not label
2247
+ function partitionFs(parts) {
2248
+ return parts.find((p) => p.type === PartitionType.data &&
2249
+ (p.subtype === PartitionSubtype.spiffs || p.subtype === PartitionSubtype.littlefs));
2250
+ }
2251
+ function bytesInclude(haystack, needle) {
2252
+ for (let i = 0; i + needle.length <= haystack.length; i++) {
2253
+ if (needle.every((b, j) => haystack[i + j] === b)) return true;
2254
+ }
2255
+ return false;
2256
+ }
2257
+ // ESP32 image header holds chip_id at byte 12; ESP8266 images have no such field
2258
+ function imageChipId(bytes) {
2259
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint16(12, true);
2260
+ }
2261
+ function bytesToSizeString(n) {
2262
+ return n >= 1048576 ? `${(n / 1048576).toFixed(2)}MB` : `${Math.round(n / 1024)}KB`;
2263
+ }
2264
+ // GET() parses JSON, so it cannot fetch a firmware image
2265
+ async function fetchBytes(url) {
2266
+ const res = await fetch(url);
2267
+ if (!res.ok) throw new Error(`${url} ${res.status}`);
2268
+ return new Uint8Array(await res.arrayBuffer());
2269
+ }
2270
+ async function fetchJson(url) {
2271
+ const res = await fetch(url);
2272
+ if (!res.ok) throw new Error(`${url} ${res.status}`);
2273
+ return res.json();
2274
+ }
2275
+
2276
+ class MqttFlash extends HTMLElementExtended {
2277
+ constructor(props) {
2278
+ super(props);
2279
+ this.state = {elements: {}, log: [], app: null, device: null, busy: false, baudrate: FlashBaudRates[0]};
2280
+ }
2281
+ get supported() { return 'serial' in navigator; }
2282
+ // esploader goes null after flashing, so a second flash needs a reconnect rather than a stale loader
2283
+ get ready() {
2284
+ return !!(this.state.app && this.state.device && this.state.esploader && !this.state.device.refusal && !this.state.busy);
2285
+ }
2286
+ async transportRelease() {
2287
+ if (this.state.transport) {
2288
+ try { await this.state.transport.disconnect(); } catch (e) { XXX(`transport disconnect: ${e.message}`); }
2289
+ }
2290
+ this.state.transport = null;
2291
+ this.state.esploader = null;
2292
+ }
2293
+
2294
+ // ---- logging ----
2295
+ logTrim() {
2296
+ if (this.state.log.length > 400) this.state.log.splice(0, this.state.log.length - 400);
2297
+ const pre = this.state.elements.log;
2298
+ if (pre) {
2299
+ pre.textContent = this.state.log.join('\n');
2300
+ pre.scrollTop = pre.scrollHeight;
2301
+ }
2302
+ }
2303
+ logLine(text) {
2304
+ this.state.log.push(String(text));
2305
+ this.logTrim();
2306
+ }
2307
+ // esptool writes progress without newlines, so append to the last line
2308
+ logWrite(text) {
2309
+ const lines = String(text).replace(/\r/g, '').split('\n');
2310
+ if (!this.state.log.length) this.state.log.push('');
2311
+ this.state.log[this.state.log.length - 1] += lines.shift();
2312
+ lines.forEach((l) => this.state.log.push(l));
2313
+ this.logTrim();
2314
+ }
2315
+ get terminal() {
2316
+ return {
2317
+ clean: () => { this.state.log = []; this.logTrim(); },
2318
+ writeLine: (data) => this.logLine(data),
2319
+ write: (data) => this.logWrite(data),
2320
+ };
2321
+ }
2322
+
2323
+ // ---- app image source ----
2324
+ onLocalFile(ev) {
2325
+ const file = ev.target.files && ev.target.files[0];
2326
+ if (!file) return;
2327
+ file.arrayBuffer().then((buf) => {
2328
+ this.state.app = {name: file.name, bytes: new Uint8Array(buf)};
2329
+ this.logLine(`Selected ${file.name} (${bytesToSizeString(this.state.app.bytes.length)})`);
2330
+ this.statusRender();
2331
+ });
2332
+ }
2333
+ // Called by MqttAdmin when the ⚡ beside a server-hosted OTA file is clicked
2334
+ setRemoteSource(org, path) {
2335
+ const url = `/ota_get/${org}/${path}`;
2336
+ this.logLine(`Fetching ${url}`);
2337
+ fetch(url)
2338
+ .then((res) => { if (!res.ok) throw new Error(`${url} ${res.status}`); return res.arrayBuffer(); })
2339
+ .then((buf) => {
2340
+ this.state.app = {name: `${path}/firmware.bin`, bytes: new Uint8Array(buf)};
2341
+ this.logLine(`Selected ${this.state.app.name} (${bytesToSizeString(this.state.app.bytes.length)})`);
2342
+ this.statusRender();
2343
+ })
2344
+ .catch((e) => { this.logLine(`Error: ${e.message}`); this.statusRender(); });
2345
+ }
2346
+
2347
+ // ---- connect and inspect ----
2348
+ onConnect() {
2349
+ if (!this.supported || this.state.busy) return;
2350
+ const portPromise = navigator.serial.requestPort(); // must be called before any await, inside the gesture
2351
+ this.state.busy = true;
2352
+ this.statusRender();
2353
+ portPromise
2354
+ .then((port) => this.connectAndInspect(port))
2355
+ .catch((e) => {
2356
+ this.logLine(`Error: ${e.message}`);
2357
+ this.state.device = null;
2358
+ return this.transportRelease(); // otherwise a half-open port stays locked
2359
+ })
2360
+ .then(() => { this.state.busy = false; this.statusRender(); });
2361
+ }
2362
+ async connectAndInspect(port) {
2363
+ await this.transportRelease(); // clicking Connect twice must not stack transports on one port
2364
+ const {ESPLoader, Transport} = await import('esptool-js'); // 214KB, so not loaded until first use
2365
+ const transport = new Transport(port, true);
2366
+ const esploader = new ESPLoader({transport, baudrate: this.state.baudrate, terminal: this.terminal});
2367
+ const description = await esploader.main(); // returns the description, not the chip name
2368
+ const chipName = esploader.chip.CHIP_NAME;
2369
+ const flashIdCapacity = (await esploader.readFlashId()) >> 16 & 0xff;
2370
+ const flashSizeGuessed = !DetectedFlashSizes[flashIdCapacity];
2371
+ const flashSize = DetectedFlashSizes[flashIdCapacity] || await esploader.detectFlashSize();
2372
+ this.state.port = port;
2373
+ this.state.transport = transport;
2374
+ this.state.esploader = esploader;
2375
+ this.state.device = {chipName, description, flashSize, flashSizeGuessed};
2376
+ if (flashSizeGuessed) {
2377
+ this.logLine(`WARNING: flash size not recognised (capacity byte 0x${flashIdCapacity.toString(16)}), assuming ${flashSize}`);
2378
+ if (chipName === "ESP32-S3") this.logLine("On an S3 this is the signature of octal (OPI) flash, which cannot be flashed from a browser.");
2379
+ }
2380
+ await this.planBuild();
2381
+ }
2382
+ // Decide provision vs app-only by reading the board back - see FLASH_PLAN.md
2383
+ async planBuild() {
2384
+ const dev = this.state.device;
2385
+ const boards = this.state.boards || (this.state.boards = await fetchJson(`${BaseUrl}/boards.json`));
2386
+ if (boards.wholeImageChips.includes(dev.chipName)) {
2387
+ dev.wholeImage = true; // ESP8266 firmware.bin is a complete image at 0x0
2388
+ dev.provision = false; // and does not overlap the filesystem
2389
+ dev.appAddress = 0x0;
2390
+ dev.appLimit = null;
2391
+ return;
2392
+ }
2393
+ const scheme = boards.schemeByChipAndSize[`${dev.chipName}-${dev.flashSize}`];
2394
+ const bootloaderPath = boards.bootloaderByChip[dev.chipName];
2395
+ if (!scheme || !bootloaderPath) {
2396
+ dev.refusal = `No base bundle for ${dev.chipName} ${dev.flashSize}`;
2397
+ this.logLine(`Unsupported board: ${dev.refusal}`);
2398
+ return;
2399
+ }
2400
+ dev.scheme = scheme;
2401
+ dev.base = {
2402
+ bootloader: {url: `${BaseUrl}/${bootloaderPath}`, address: this.state.esploader.chip.BOOTLOADER_FLASH_OFFSET},
2403
+ partitions: {url: `${BaseUrl}/${boards.partitionsByScheme[scheme]}`, address: boards.partitionTableOffset},
2404
+ otadata: {url: `${BaseUrl}/${boards.otadata}`, address: null}, // from the partition table below
2405
+ };
2406
+ dev.appAddress = AppPartitionOffset;
2407
+ dev.appLimit = AppPartitionSize;
2408
+ await this.deviceInspect();
2409
+ }
2410
+ async deviceInspect() {
2411
+ const dev = this.state.device;
2412
+ const esploader = this.state.esploader;
2413
+ const bootloaderOffset = esploader.chip.BOOTLOADER_FLASH_OFFSET;
2414
+ const reasons = [];
2415
+ let onDeviceParts = [];
2416
+ try {
2417
+ // Read whole 4096-byte blocks - readFlash works in blocks and partitionsParse stops at the
2418
+ // first record without the magic anyway
2419
+ const head = await esploader.readFlash(bootloaderOffset, 4096);
2420
+ if (head[0] !== ImageMagic) reasons.push("no bootloader");
2421
+ onDeviceParts = partitionsParse(await esploader.readFlash(PartitionTableOffset, 4096));
2422
+ const app = partitionApp(onDeviceParts);
2423
+ if (!onDeviceParts.length) reasons.push("no partition table");
2424
+ else if (!app) reasons.push("no app partition");
2425
+ // Only the app slot has to match - a differently sized filesystem is still safe to keep
2426
+ else if (app.offset !== AppPartitionOffset || app.size !== AppPartitionSize) reasons.push("app partition differs");
2427
+ } catch (e) {
2428
+ reasons.push(`could not read flash (${e.message})`);
2429
+ }
2430
+ if (!reasons.length) {
2431
+ const fs = partitionFs(onDeviceParts);
2432
+ if (fs) {
2433
+ const sb = await esploader.readFlash(fs.offset, 4096);
2434
+ dev.hasConfig = bytesInclude(sb, LittleFsMagic);
2435
+ }
2436
+ }
2437
+ dev.provision = reasons.length > 0;
2438
+ dev.provisionReasons = reasons;
2439
+ this.logLine(dev.provision
2440
+ ? `Board needs provisioning: ${reasons.join(', ')}`
2441
+ : `Board already provisioned${dev.hasConfig ? " and has a filesystem - config will be preserved" : ""}`);
2442
+ }
2443
+ onProvisionOverride() {
2444
+ if (!this.state.device) return;
2445
+ this.state.device.provision = true;
2446
+ this.state.device.provisionReasons = ["requested"];
2447
+ this.logLine("Provisioning requested - existing configuration will be erased");
2448
+ this.statusRender();
2449
+ }
2450
+
2451
+ // ---- flash ----
2452
+ onFlash() {
2453
+ if (!this.ready) return;
2454
+ this.state.busy = true;
2455
+ this.statusRender();
2456
+ this.flash()
2457
+ .catch((e) => this.logLine(`Error: ${e.message}`))
2458
+ .then(() => { this.state.busy = false; this.statusRender(); });
2459
+ }
2460
+ // Refusals rather than warnings - a bad combination here means a board that will not boot
2461
+ appCheck() {
2462
+ const dev = this.state.device;
2463
+ const bytes = this.state.app.bytes;
2464
+ if (bytes[0] !== ImageMagic) {
2465
+ throw new Error(`${this.state.app.name} is not an ESP firmware image (first byte 0x${bytes[0].toString(16)}, expected 0xe9)`);
2466
+ }
2467
+ if (!dev.wholeImage) {
2468
+ const chipId = imageChipId(bytes);
2469
+ if (chipId !== this.state.esploader.chip.IMAGE_CHIP_ID) {
2470
+ 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})`);
2471
+ }
2472
+ if (dev.appLimit && bytes.length > dev.appLimit) {
2473
+ throw new Error(`${this.state.app.name} is ${bytesToSizeString(bytes.length)}, larger than the ${bytesToSizeString(dev.appLimit)} app partition`);
2474
+ }
2475
+ }
2476
+ if (dev.flashSizeGuessed && !this.state.confirmed) {
2477
+ throw new Error("Flash size was not detected reliably - tick the confirmation box to flash anyway");
2478
+ }
2479
+ }
2480
+ async fileArrayBuild() {
2481
+ const dev = this.state.device;
2482
+ const files = [];
2483
+ if (dev.provision && !dev.wholeImage) {
2484
+ const [bootloader, partitions, otadata] = await Promise.all(
2485
+ [dev.base.bootloader, dev.base.partitions, dev.base.otadata].map((p) => fetchBytes(p.url)));
2486
+ // otadata goes wherever the table we are about to write says, not at a hardcoded offset
2487
+ const otadataPart = partitionOtadata(partitionsParse(partitions));
2488
+ if (!otadataPart) throw new Error(`${dev.base.partitions.url} has no otadata partition`);
2489
+ files.push({data: bootloader, address: dev.base.bootloader.address});
2490
+ files.push({data: partitions, address: dev.base.partitions.address});
2491
+ files.push({data: otadata, address: otadataPart.offset});
2492
+ }
2493
+ files.push({data: this.state.app.bytes, address: dev.appAddress});
2494
+ return files;
2495
+ }
2496
+ async flash() {
2497
+ const dev = this.state.device;
2498
+ const esploader = this.state.esploader;
2499
+ this.appCheck();
2500
+ const fileArray = await this.fileArrayBuild();
2501
+ fileArray.forEach((f) => this.logLine(`Will write ${bytesToSizeString(f.data.length)} at 0x${f.address.toString(16)}`));
2502
+ if (this.state.elements.progress) this.state.elements.progress.value = 0;
2503
+ try {
2504
+ await esploader.writeFlash({
2505
+ fileArray,
2506
+ flashSize: 'detect', // patch only the size into the header, from the real chip
2507
+ // 'keep' preserves the mode/freq the image was built with. Our base bootloaders are already
2508
+ // dio_80m, and on an ESP8266 the app image itself sits at the bootloader offset and would
2509
+ // otherwise be rewritten to settings its flash may not support.
2510
+ flashMode: 'keep',
2511
+ flashFreq: 'keep',
2512
+ eraseAll: dev.provision,
2513
+ compress: true,
2514
+ reportProgress: (index, written, total) => {
2515
+ const bar = this.state.elements.progress;
2516
+ if (bar) { bar.max = total; bar.value = written; }
2517
+ },
2518
+ });
2519
+ this.logLine("Flash complete - resetting. Reconnect the board if you want to flash again.");
2520
+ await esploader.after("hard_reset");
2521
+ dev.flashed = true;
2522
+ } finally {
2523
+ await this.transportRelease(); // a failed flash must still free the port
2524
+ }
2525
+ await this.bootLogStream();
2526
+ }
2527
+ // Reopen the port with plain WebSerial to show what the board says on boot
2528
+ async bootLogStream() {
2529
+ const port = this.state.port;
2530
+ if (!port) return;
2531
+ this.logLine("---- boot log ----");
2532
+ try {
2533
+ await port.open({baudRate: 115200});
2534
+ } catch (e) {
2535
+ this.logLine(`(could not reopen port for boot log: ${e.message})`);
2536
+ return;
2537
+ }
2538
+ const decoder = new TextDecoder();
2539
+ const reader = port.readable.getReader();
2540
+ this.state.bootReader = reader;
2541
+ const stopAt = Date.now() + 20000; // long enough to see WiFi come up or the captive portal start
2542
+ try {
2543
+ while (Date.now() < stopAt) {
2544
+ const {value, done} = await reader.read();
2545
+ if (done) break;
2546
+ if (value) this.logWrite(decoder.decode(value, {stream: true}));
2547
+ }
2548
+ } catch (e) {
2549
+ this.logLine(`(boot log ended: ${e.message})`);
2550
+ } finally {
2551
+ try { await reader.cancel(); } catch (e) { XXX(`boot log reader cancel: ${e.message}`); }
2552
+ reader.releaseLock();
2553
+ this.state.bootReader = null;
2554
+ try { await port.close(); } catch (e) { XXX(`boot log port close: ${e.message}`); }
2555
+ this.logLine("---- end of boot log ----");
2556
+ }
2557
+ }
2558
+
2559
+ // ---- render ----
2560
+ statusLines() {
2561
+ const dev = this.state.device;
2562
+ const app = this.state.app;
2563
+ const lines = [];
2564
+ lines.push(el('p', {}, [
2565
+ el('span', {textContent: "Firmware"}),
2566
+ el('span', {i8n: false, textContent: app ? ` ${app.name} (${bytesToSizeString(app.bytes.length)})` : " -"}),
2567
+ ]));
2568
+ if (!dev) {
2569
+ lines.push(el('p', {textContent: "No board connected"}));
2570
+ return lines;
2571
+ }
2572
+ lines.push(el('p', {}, [
2573
+ el('span', {textContent: "Board"}),
2574
+ el('span', {i8n: false, textContent: ` ${dev.description} ${dev.flashSize}${dev.scheme ? ` (${dev.scheme})` : ''}`}),
2575
+ ]));
2576
+ if (dev.refusal) {
2577
+ lines.push(el('p', {class: 'error'}, [
2578
+ el('span', {textContent: "Unsupported board"}),
2579
+ el('span', {i8n: false, textContent: ` - ${dev.refusal}`}),
2580
+ ]));
2581
+ return lines;
2582
+ }
2583
+ if (dev.flashSizeGuessed) {
2584
+ // Held in state, not read off the checkbox - statusRender() rebuilds these elements
2585
+ lines.push(el('p', {class: 'warning'}, [
2586
+ el('label', {}, [
2587
+ el('input', {type: 'checkbox', checked: this.state.confirmed,
2588
+ onchange: (ev) => { this.state.confirmed = ev.target.checked; }}),
2589
+ el('span', {textContent: "Flash size was not detected reliably - flash anyway"}),
2590
+ ]),
2591
+ ]));
2592
+ }
2593
+ if (dev.provision) {
2594
+ lines.push(el('p', {class: 'warning', textContent: "Full provision - all configuration on the board will be erased"}));
2595
+ } else {
2596
+ lines.push(el('p', {textContent: dev.hasConfig
2597
+ ? "Updating the app only - the existing configuration will be preserved"
2598
+ : "Updating the app only"}));
2599
+ lines.push(el('button', {class: 'submit', type: 'button', textContent: "Provision instead (erases config)",
2600
+ onclick: this.onProvisionOverride.bind(this)}));
2601
+ }
2602
+ return lines;
2603
+ }
2604
+ statusRender() {
2605
+ const status = this.state.elements.status;
2606
+ if (status) status.replaceChildren(...this.statusLines());
2607
+ const flashButton = this.state.elements.flashButton;
2608
+ if (flashButton) flashButton.disabled = !this.ready;
2609
+ const connectButton = this.state.elements.connectButton;
2610
+ if (connectButton) connectButton.disabled = this.state.busy;
2611
+ }
2612
+ render() {
2613
+ if (!this.supported) {
2614
+ return [
2615
+ el('link', {rel: 'stylesheet', href: CssUrl}),
2616
+ el('div', {class: 'mqtt-flash'}, [
2617
+ el('h4', {textContent: "Flash over USB"}),
2618
+ el('p', {textContent: "Flashing needs the Web Serial API - use Chrome, Edge or Opera on a desktop computer"}),
2619
+ ]),
2620
+ ];
2621
+ }
2622
+ return [
2623
+ el('link', {rel: 'stylesheet', href: CssUrl}),
2624
+ el('div', {class: 'mqtt-flash'}, [
2625
+ el('h4', {textContent: "Flash over USB"}),
2626
+ el('label', {for: 'flashfile', textContent: "Firmware file"}),
2627
+ el('input', {id: 'flashfile', type: 'file', accept: '.bin', onchange: this.onLocalFile.bind(this)}),
2628
+ el('label', {for: 'flashbaud', textContent: "Speed"}),
2629
+ el('select', {id: 'flashbaud', onchange: (ev) => { this.state.baudrate = Number(ev.target.value); }},
2630
+ FlashBaudRates.map((b) => el('option', {i8n: false, value: b, textContent: String(b)}))),
2631
+ this.state.elements.connectButton = el('button', {class: 'submit', type: 'button',
2632
+ textContent: "Connect board", onclick: this.onConnect.bind(this)}),
2633
+ this.state.elements.flashButton = el('button', {class: 'submit', type: 'button', disabled: true,
2634
+ textContent: "Flash", onclick: this.onFlash.bind(this)}),
2635
+ this.state.elements.status = el('div', {class: 'flash-status'}, this.statusLines()),
2636
+ this.state.elements.progress = el('progress', {max: 100, value: 0}),
2637
+ this.state.elements.log = el('pre', {class: 'flash-log', i8n: false}),
2638
+ ]),
2639
+ ];
2640
+ }
2641
+ }
2642
+ customElements.define('mqtt-flash', MqttFlash);
2643
+
2135
2644
  class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organization
2136
2645
  constructor(props) {
2137
2646
  super(props);
@@ -2266,10 +2775,15 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
2266
2775
  console.log(ev,val);
2267
2776
  this.getOrDeleteOtaFiles(`/ota_delete/${val}`);
2268
2777
  }
2778
+ onOtaFlash(path) {
2779
+ const flashEl = this.state.elements.flash;
2780
+ if (flashEl) flashEl.setRemoteSource(this.state.org, path);
2781
+ }
2269
2782
  otaFilesList() {
2270
2783
  return this.state.ota_files.length === 0 ?
2271
2784
  el('p', {textContent: "No OTA files uploaded yet."}) :
2272
2785
  el('p', {}, this.state.ota_files.map(f => [
2786
+ el('span', {class: 'pseudolink', title: getString("Flash this over USB"), textContent: '⚡', onclick: this.onOtaFlash.bind(this, f)}),
2273
2787
  el('span', {class: 'pseudolink', textContent: `🗑 ${f}`, onclick: this.onOtaDelete.bind(this,`${this.state.org}/${f}`)}),
2274
2788
  el('br', {}),
2275
2789
  ])
@@ -3137,6 +3651,9 @@ class MqttAdmin extends HTMLElementExtended { // TODO-89 may depend on organizat
3137
3651
  el('h3', {textContent: "Existing OTA Files"}),
3138
3652
  this.state.elements.ota_files = this.otaFilesList(),
3139
3653
  ]), // section ota
3654
+ el('section', {}, [
3655
+ this.state.elements.flash = el('mqtt-flash', {}),
3656
+ ]), // section flash
3140
3657
  ]);
3141
3658
  }
3142
3659
  // Content of the Admin tab below the organization dropdown - only rendered once an org is selected