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/FLASH_PLAN.md +584 -0
- package/base/boards.json +25 -0
- package/base/boot_app0.bin +0 -0
- package/base/bootloader/esp32c3.bin +0 -0
- package/base/bootloader/esp32s2.bin +0 -0
- package/base/bootloader/esp32s3.bin +0 -0
- package/base/partitions/frugal_16mb.bin +0 -0
- package/base/partitions/frugal_16mb.csv +8 -0
- package/base/partitions/frugal_8mb.bin +0 -0
- package/base/partitions/frugal_8mb.csv +8 -0
- package/base/partitions/min_spiffs.bin +0 -0
- package/base/partitions/min_spiffs.csv +7 -0
- package/dashboard_example.html +2 -1
- package/frugaliot.css +21 -0
- package/index-embedded.html +2 -1
- package/index-project.html +2 -1
- package/index.html +2 -1
- package/login.html +2 -1
- package/package.json +2 -1
- package/webcomponents.js +517 -0
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
|