mpytool 2.5.0__tar.gz → 2.6.0__tar.gz

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.
Files changed (52) hide show
  1. {mpytool-2.5.0 → mpytool-2.6.0}/PKG-INFO +89 -14
  2. {mpytool-2.5.0 → mpytool-2.6.0}/README.md +88 -13
  3. {mpytool-2.5.0 → mpytool-2.6.0}/README_API.md +164 -6
  4. {mpytool-2.5.0 → mpytool-2.6.0}/completions/_mpytool +20 -6
  5. {mpytool-2.5.0 → mpytool-2.6.0}/completions/mpytool.bash +16 -7
  6. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/__init__.py +3 -1
  7. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/_version.py +3 -3
  8. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/conn.py +34 -5
  9. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/conn_serial.py +16 -3
  10. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/conn_socket.py +1 -0
  11. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/mpy.py +418 -5
  12. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/mpy_comm.py +27 -0
  13. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/mpytool.py +188 -20
  14. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/terminal_unix.py +24 -14
  15. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/utils.py +4 -1
  16. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool.egg-info/PKG-INFO +89 -14
  17. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool.egg-info/SOURCES.txt +1 -0
  18. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool.egg-info/scm_file_list.json +1 -0
  19. mpytool-2.6.0/mpytool.egg-info/scm_version.json +8 -0
  20. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_cli.py +12 -0
  21. mpytool-2.6.0/tests/test_conn_serial.py +38 -0
  22. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_mpy.py +395 -1
  23. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_mpytool.py +104 -5
  24. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_utils.py +1 -1
  25. mpytool-2.5.0/mpytool.egg-info/scm_version.json +0 -8
  26. {mpytool-2.5.0 → mpytool-2.6.0}/.github/workflows/integration.yml +0 -0
  27. {mpytool-2.5.0 → mpytool-2.6.0}/.github/workflows/publish.yml +0 -0
  28. {mpytool-2.5.0 → mpytool-2.6.0}/.github/workflows/tests.yml +0 -0
  29. {mpytool-2.5.0 → mpytool-2.6.0}/.gitignore +0 -0
  30. {mpytool-2.5.0 → mpytool-2.6.0}/LICENSE +0 -0
  31. {mpytool-2.5.0 → mpytool-2.6.0}/README_bench.md +0 -0
  32. {mpytool-2.5.0 → mpytool-2.6.0}/README_mpremote.md +0 -0
  33. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/cmd_cp.py +0 -0
  34. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/logger.py +0 -0
  35. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/mount.py +0 -0
  36. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/mpy_cross.py +0 -0
  37. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/speedtest.py +0 -0
  38. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/terminal.py +0 -0
  39. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool/terminal_win.py +0 -0
  40. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool.egg-info/dependency_links.txt +0 -0
  41. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool.egg-info/entry_points.txt +0 -0
  42. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool.egg-info/requires.txt +0 -0
  43. {mpytool-2.5.0 → mpytool-2.6.0}/mpytool.egg-info/top_level.txt +0 -0
  44. {mpytool-2.5.0 → mpytool-2.6.0}/pyproject.toml +0 -0
  45. {mpytool-2.5.0 → mpytool-2.6.0}/setup.cfg +0 -0
  46. {mpytool-2.5.0 → mpytool-2.6.0}/tests/__init__.py +0 -0
  47. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_cmd_cp.py +0 -0
  48. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_conn_socket.py +0 -0
  49. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_errors.py +0 -0
  50. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_helpers.py +0 -0
  51. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_integration.py +0 -0
  52. {mpytool-2.5.0 → mpytool-2.6.0}/tests/test_mount.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpytool
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: MPY tool - manage files on devices running MicroPython
5
5
  Author-email: Pavel Revak <pavelrevak@gmail.com>
6
6
  License-Expression: MIT
@@ -386,6 +386,35 @@ RUN: script.py (128 bytes)
386
386
  Done!
387
387
  ```
388
388
 
389
+ The script can also come from **STDIN** using the `-` convention (or simply by
390
+ piping when no file is given), which is handy for generated code or one-off
391
+ snippets:
392
+ ```
393
+ $ cat script.py | mpytool run - # explicit '-' = read STDIN
394
+ $ mpytool run - <<< 'print("hi")' # here-string (single line)
395
+ $ my_codegen | mpytool run # piped, no file argument
396
+ $ cat boot.py | mpytool run - -- reset -- monitor # run from STDIN, then chain
397
+ ```
398
+
399
+ For **multi-line** code, feed a here-document straight into `run -` (no `cat`,
400
+ no pipe needed - the here-doc *is* the standard input):
401
+ ```
402
+ $ mpytool run - << 'EOF'
403
+ import machine, time
404
+ led = machine.Pin('LED', machine.Pin.OUT)
405
+ for _ in range(3):
406
+ led.toggle()
407
+ time.sleep(0.2)
408
+ print('done')
409
+ EOF
410
+ ```
411
+ Quote the delimiter (`'EOF'`) so the shell does not expand `$`, backticks or
412
+ `{}` inside the Python code. The closing `EOF` must be on a line of its own.
413
+
414
+ Because STDIN can be read only once, at most one command per invocation may
415
+ consume it: combining a STDIN `run` with another STDIN `run` or with `repl`
416
+ (which needs the terminal) is rejected up front with a clear error.
417
+
389
418
  ### Edit file on device
390
419
  ```
391
420
  $ mpytool edit :boot.py # edit file (uses $VISUAL or $EDITOR)
@@ -528,10 +557,25 @@ Version: 3.4.0; MicroPython v1.27.0 on 2025-12-09
528
557
  Impl: micropython
529
558
  Machine: Raspberry Pi Pico with RP2040
530
559
  Serial: e660123456789abc
531
- Memory: 36.4 KB / 240 KB (15.15%)
560
+ GC heap: 36.4 KB / 240 KB (15.15%)
532
561
  Flash: 120 KB / 1.38 MB (8.52%)
533
562
  ```
534
563
 
564
+ On ESP32 the physical memory regions are listed too (from the ESP-IDF
565
+ allocator), which stays stable regardless of what the GC heap is doing:
566
+
567
+ ```
568
+ SRAM: 412 KB / 640 KB (64.38%)
569
+ PSRAM: 12.0 MB / 32.0 MB (37.50%)
570
+ GC heap: 209 KB / 20.0 MB (1.02%)
571
+ ```
572
+
573
+ `GC heap` is the heap MicroPython uses for Python objects - **not** the amount
574
+ of RAM in the chip. On builds with PSRAM it grows and shrinks on demand, so its
575
+ total changes over time: right after a reset it can claim nearly all free PSRAM,
576
+ and it shrinks once drivers allocate PSRAM outside the GC heap (e.g. camera
577
+ framebuffers). Use the `SRAM` / `PSRAM` lines to judge actual memory pressure.
578
+
535
579
  On devices with WiFi or Ethernet, MAC addresses are also shown:
536
580
  ```
537
581
  MAC WiFi: aa:bb:cc:dd:ee:01
@@ -548,15 +592,18 @@ $ mpytool flash erase # quick erase (reset filesystem)
548
592
  $ mpytool flash erase --full # full erase
549
593
 
550
594
  # ESP32 - partitions (by label)
551
- $ mpytool flash # list all partitions with filesystem info
552
- Label Type Subtype Address Size Block Actual FS Flags
553
- ------------------------------------------------------------------------------------------
554
- factory app factory 0x10000 1.94M running
555
- nvs data nvs 0x9000 24.0K
556
- vfs data littlefs 0x200000 2.00M 4 KB littlefs2
557
-
558
- Boot partition: factory
559
- Next OTA: (none)
595
+ $ mpytool flash # list all partitions, sorted by address
596
+ Label Type Subtype Address Size Block Content Flags
597
+ --------------------------------------------------------------------------------------------
598
+ nvs data nvs 0x9000 16.0K
599
+ otadata data ota 0xd000 8.00K
600
+ phy_init data phy 0xf000 4.00K
601
+ ota_0 app ota_0 0x10000 2.50M app ESP32-P4 running, ota:valid
602
+ ota_1 app ota_1 0x290000 2.50M app ESP32-P4 ota:valid
603
+ vfs data fat 0x510000 10.9M 4.00K littlefs2
604
+
605
+ Boot partition: ota_0
606
+ Next OTA: ota_1
560
607
 
561
608
  $ mpytool flash read vfs backup.bin # backup partition to file
562
609
  $ mpytool flash write nvs nvs_backup.bin # restore partition from file
@@ -564,11 +611,39 @@ $ mpytool flash erase vfs # quick erase partition
564
611
  $ mpytool flash erase vfs --full # full erase partition
565
612
  ```
566
613
 
614
+ `Content` shows what a partition actually holds - the detected filesystem for
615
+ data partitions, the image type for app partitions (`app <CHIP>`, or
616
+ `BOOTLOADER!` / `INVALID!` / `empty`). The `ota:` flags come from the `otadata`
617
+ partition and describe the **rollback bookkeeping** of a slot, not the image
618
+ stored in it: a slot can read `ota:valid` while holding an unbootable image.
619
+
567
620
  ### OTA firmware update (ESP32)
621
+
622
+ OTA needs an **app-only** image (`micropython.bin`), not the combined
623
+ `firmware.bin` (bootloader + partition table + app) that `esptool` writes.
624
+ mpytool checks the image header before uploading, so a wrong file is rejected
625
+ immediately instead of failing with `ESP_ERR_OTA_VALIDATE_FAILED` after the
626
+ whole transfer. It also refuses an image built for a different chip.
627
+
628
+ ```
629
+ $ mpytool flash ota micropython.bin # write to next OTA partition
630
+ $ mpytool flash ota micropython.bin -- reset --machine # write and reboot
631
+ $ mpytool flash ota micropython.bin -- reset --machine -t 30 # ... with 30s timeout
632
+ $ mpytool flash ota micropython.bin --force # write despite failed validation
633
+
634
+ $ mpytool flash ota confirm # mark running app valid (cancel rollback)
635
+ $ mpytool flash ota boot # boot the other slot (manual rollback)
636
+ $ mpytool flash ota boot ota_1 # boot a specific app partition
637
+ ```
638
+
639
+ If the firmware enables rollback, a freshly booted OTA image stays in
640
+ `pending-verify` and reverts on the next reset until it is confirmed - either by
641
+ the app itself (`esp32.Partition(esp32.Partition.RUNNING).mark_app_valid_cancel_rollback()`)
642
+ or from the host:
643
+
568
644
  ```
569
- $ mpytool flash ota firmware.app-bin # flash to next OTA partition
570
- $ mpytool flash ota firmware.app-bin -- reset --machine # flash and reboot
571
- $ mpytool flash ota firmware.app-bin -- reset --machine -t 30 # flash and reboot with 30s timeout
645
+ $ mpytool flash ota micropython.bin -- reset --machine
646
+ $ mpytool flash ota confirm
572
647
  ```
573
648
 
574
649
  ### Multiple commands separated by `--`
@@ -371,6 +371,35 @@ RUN: script.py (128 bytes)
371
371
  Done!
372
372
  ```
373
373
 
374
+ The script can also come from **STDIN** using the `-` convention (or simply by
375
+ piping when no file is given), which is handy for generated code or one-off
376
+ snippets:
377
+ ```
378
+ $ cat script.py | mpytool run - # explicit '-' = read STDIN
379
+ $ mpytool run - <<< 'print("hi")' # here-string (single line)
380
+ $ my_codegen | mpytool run # piped, no file argument
381
+ $ cat boot.py | mpytool run - -- reset -- monitor # run from STDIN, then chain
382
+ ```
383
+
384
+ For **multi-line** code, feed a here-document straight into `run -` (no `cat`,
385
+ no pipe needed - the here-doc *is* the standard input):
386
+ ```
387
+ $ mpytool run - << 'EOF'
388
+ import machine, time
389
+ led = machine.Pin('LED', machine.Pin.OUT)
390
+ for _ in range(3):
391
+ led.toggle()
392
+ time.sleep(0.2)
393
+ print('done')
394
+ EOF
395
+ ```
396
+ Quote the delimiter (`'EOF'`) so the shell does not expand `$`, backticks or
397
+ `{}` inside the Python code. The closing `EOF` must be on a line of its own.
398
+
399
+ Because STDIN can be read only once, at most one command per invocation may
400
+ consume it: combining a STDIN `run` with another STDIN `run` or with `repl`
401
+ (which needs the terminal) is rejected up front with a clear error.
402
+
374
403
  ### Edit file on device
375
404
  ```
376
405
  $ mpytool edit :boot.py # edit file (uses $VISUAL or $EDITOR)
@@ -513,10 +542,25 @@ Version: 3.4.0; MicroPython v1.27.0 on 2025-12-09
513
542
  Impl: micropython
514
543
  Machine: Raspberry Pi Pico with RP2040
515
544
  Serial: e660123456789abc
516
- Memory: 36.4 KB / 240 KB (15.15%)
545
+ GC heap: 36.4 KB / 240 KB (15.15%)
517
546
  Flash: 120 KB / 1.38 MB (8.52%)
518
547
  ```
519
548
 
549
+ On ESP32 the physical memory regions are listed too (from the ESP-IDF
550
+ allocator), which stays stable regardless of what the GC heap is doing:
551
+
552
+ ```
553
+ SRAM: 412 KB / 640 KB (64.38%)
554
+ PSRAM: 12.0 MB / 32.0 MB (37.50%)
555
+ GC heap: 209 KB / 20.0 MB (1.02%)
556
+ ```
557
+
558
+ `GC heap` is the heap MicroPython uses for Python objects - **not** the amount
559
+ of RAM in the chip. On builds with PSRAM it grows and shrinks on demand, so its
560
+ total changes over time: right after a reset it can claim nearly all free PSRAM,
561
+ and it shrinks once drivers allocate PSRAM outside the GC heap (e.g. camera
562
+ framebuffers). Use the `SRAM` / `PSRAM` lines to judge actual memory pressure.
563
+
520
564
  On devices with WiFi or Ethernet, MAC addresses are also shown:
521
565
  ```
522
566
  MAC WiFi: aa:bb:cc:dd:ee:01
@@ -533,15 +577,18 @@ $ mpytool flash erase # quick erase (reset filesystem)
533
577
  $ mpytool flash erase --full # full erase
534
578
 
535
579
  # ESP32 - partitions (by label)
536
- $ mpytool flash # list all partitions with filesystem info
537
- Label Type Subtype Address Size Block Actual FS Flags
538
- ------------------------------------------------------------------------------------------
539
- factory app factory 0x10000 1.94M running
540
- nvs data nvs 0x9000 24.0K
541
- vfs data littlefs 0x200000 2.00M 4 KB littlefs2
542
-
543
- Boot partition: factory
544
- Next OTA: (none)
580
+ $ mpytool flash # list all partitions, sorted by address
581
+ Label Type Subtype Address Size Block Content Flags
582
+ --------------------------------------------------------------------------------------------
583
+ nvs data nvs 0x9000 16.0K
584
+ otadata data ota 0xd000 8.00K
585
+ phy_init data phy 0xf000 4.00K
586
+ ota_0 app ota_0 0x10000 2.50M app ESP32-P4 running, ota:valid
587
+ ota_1 app ota_1 0x290000 2.50M app ESP32-P4 ota:valid
588
+ vfs data fat 0x510000 10.9M 4.00K littlefs2
589
+
590
+ Boot partition: ota_0
591
+ Next OTA: ota_1
545
592
 
546
593
  $ mpytool flash read vfs backup.bin # backup partition to file
547
594
  $ mpytool flash write nvs nvs_backup.bin # restore partition from file
@@ -549,11 +596,39 @@ $ mpytool flash erase vfs # quick erase partition
549
596
  $ mpytool flash erase vfs --full # full erase partition
550
597
  ```
551
598
 
599
+ `Content` shows what a partition actually holds - the detected filesystem for
600
+ data partitions, the image type for app partitions (`app <CHIP>`, or
601
+ `BOOTLOADER!` / `INVALID!` / `empty`). The `ota:` flags come from the `otadata`
602
+ partition and describe the **rollback bookkeeping** of a slot, not the image
603
+ stored in it: a slot can read `ota:valid` while holding an unbootable image.
604
+
552
605
  ### OTA firmware update (ESP32)
606
+
607
+ OTA needs an **app-only** image (`micropython.bin`), not the combined
608
+ `firmware.bin` (bootloader + partition table + app) that `esptool` writes.
609
+ mpytool checks the image header before uploading, so a wrong file is rejected
610
+ immediately instead of failing with `ESP_ERR_OTA_VALIDATE_FAILED` after the
611
+ whole transfer. It also refuses an image built for a different chip.
612
+
613
+ ```
614
+ $ mpytool flash ota micropython.bin # write to next OTA partition
615
+ $ mpytool flash ota micropython.bin -- reset --machine # write and reboot
616
+ $ mpytool flash ota micropython.bin -- reset --machine -t 30 # ... with 30s timeout
617
+ $ mpytool flash ota micropython.bin --force # write despite failed validation
618
+
619
+ $ mpytool flash ota confirm # mark running app valid (cancel rollback)
620
+ $ mpytool flash ota boot # boot the other slot (manual rollback)
621
+ $ mpytool flash ota boot ota_1 # boot a specific app partition
622
+ ```
623
+
624
+ If the firmware enables rollback, a freshly booted OTA image stays in
625
+ `pending-verify` and reverts on the next reset until it is confirmed - either by
626
+ the app itself (`esp32.Partition(esp32.Partition.RUNNING).mark_app_valid_cancel_rollback()`)
627
+ or from the host:
628
+
553
629
  ```
554
- $ mpytool flash ota firmware.app-bin # flash to next OTA partition
555
- $ mpytool flash ota firmware.app-bin -- reset --machine # flash and reboot
556
- $ mpytool flash ota firmware.app-bin -- reset --machine -t 30 # flash and reboot with 30s timeout
630
+ $ mpytool flash ota micropython.bin -- reset --machine
631
+ $ mpytool flash ota confirm
557
632
  ```
558
633
 
559
634
  ### Multiple commands separated by `--`
@@ -681,14 +681,45 @@ mpy.unique_id()
681
681
 
682
682
  #### memory()
683
683
 
684
- Get memory usage.
684
+ Get MicroPython **GC heap** usage - the heap available to Python objects, not
685
+ physical RAM. On ESP32 builds with PSRAM the GC heap grows and shrinks on
686
+ demand, so `total` changes over time: right after a reset it can claim nearly
687
+ all free PSRAM, and it shrinks again once drivers allocate PSRAM outside the GC
688
+ heap (e.g. camera framebuffers via `heap_caps_malloc`). Use
689
+ [`esp_ram()`](#esp_ram) for the physical memory regions.
685
690
 
686
691
  ```python
687
692
  mpy.memory()
688
693
  ```
689
694
 
690
695
  **Returns:**
691
- - `dict`: `{'alloc': bytes_used, 'free': bytes_free, 'total': total_bytes}`
696
+ - `dict`: `{'alloc': bytes_used, 'free': bytes_free, 'total': gc_heap_bytes}`
697
+
698
+ #### esp_ram()
699
+
700
+ Get physical RAM usage per region (ESP32 only), read via
701
+ `esp32.idf_heap_info()`. Unlike `memory()` this is stable, since it reflects the
702
+ actual silicon rather than the floating GC heap.
703
+
704
+ ```python
705
+ mpy.esp_ram()
706
+ ```
707
+
708
+ **Returns:**
709
+ - `dict` with `'sram'` and, when PSRAM is fitted, `'psram'`; each is
710
+ `{'total': bytes, 'free': bytes, 'used': bytes}`. Empty dict on non-ESP32
711
+ devices or builds without `esp32.idf_heap_info`.
712
+
713
+ **Note:** totals cover memory registered with the ESP-IDF allocator, so they
714
+ exclude static `.data`/`.bss` and code in IRAM - they are the heap-available
715
+ portion of each region, not the raw chip capacity.
716
+
717
+ **Example:**
718
+ ```python
719
+ >>> mpy.esp_ram()
720
+ {'sram': {'total': 640000, 'free': 140000, 'used': 500000},
721
+ 'psram': {'total': 33554432, 'free': 20971520, 'used': 12582912}}
722
+ ```
692
723
 
693
724
  ### Mount
694
725
 
@@ -953,9 +984,14 @@ Get partition table information (ESP32 only).
953
984
  mpy.partitions()
954
985
  ```
955
986
 
987
+ Partitions are sorted by flash offset.
988
+
956
989
  **Returns:**
957
990
  - `dict` with keys:
958
- - `'partitions'`: List of partition dicts with keys: `label`, `type`, `type_name`, `subtype`, `subtype_name`, `offset`, `size`, `encrypted`, `running`, `filesystem`, `fs_block_size`
991
+ - `'partitions'`: List of partition dicts with keys: `label`, `type`, `type_name`, `subtype`, `subtype_name`, `offset`, `size`, `encrypted`, `running`, `filesystem`, `fs_block_size`, plus for OTA app partitions:
992
+ - `'ota_state'`: rollback state recorded in `otadata` - `'new'`, `'pending-verify'`, `'valid'`, `'invalid'`, `'aborted'` or `'undefined'`. This is bookkeeping for the *slot*; it says nothing about the image currently stored there.
993
+ - `'ota_boot'`: `True` for the slot the bootloader will start
994
+ - `'image'`: what the partition actually holds, as returned by [`parse_esp_image()`](#parse_esp_imagedata)
959
995
  - `'boot'`: Boot partition label or None
960
996
  - `'next_ota'`: Next OTA partition label or None
961
997
  - `'next_ota_size'`: Next OTA partition size or None
@@ -1068,24 +1104,146 @@ mpy.flash_erase(label=None, full=False, progress_callback=None)
1068
1104
  >>> mpy.flash_erase(label='nvs', full=True)
1069
1105
  ```
1070
1106
 
1071
- #### ota_write(data, progress_callback=None, compress=None)
1107
+ #### ota_write(data, progress_callback=None, compress=None, validate=True, force=False)
1072
1108
 
1073
1109
  Write firmware to next OTA partition and set it as boot partition.
1074
1110
 
1111
+ The image must be an **app-only** build (`micropython.bin`), not the combined
1112
+ `firmware.bin` that also contains the bootloader and partition table.
1113
+
1075
1114
  ```python
1076
- mpy.ota_write(data, progress_callback=None, compress=None)
1115
+ mpy.ota_write(data, progress_callback=None, compress=None,
1116
+ validate=True, force=False)
1077
1117
  ```
1078
1118
 
1079
1119
  **Parameters:**
1080
1120
  - `data` (bytes): Firmware content (.app-bin file)
1081
1121
  - `progress_callback` (callable, optional): Callback `(transferred, total, wire_bytes)`
1082
1122
  - `compress` (bool, optional): Enable/disable compression
1123
+ - `validate` (bool): Check the image header first; set `False` only if `check_ota_image()` was already called on this data
1124
+ - `force` (bool): With `validate`, warn instead of refusing a bad image
1083
1125
 
1084
1126
  **Returns:**
1085
1127
  - `dict`: `{'target': label, 'offset': partition_offset, 'size': fw_size, 'wire_bytes': bytes_sent, 'compressed': bool}`
1086
1128
 
1087
1129
  **Raises:**
1088
- - `MpyError`: If OTA not available or firmware too large
1130
+ - `MpyError`: If OTA not available, firmware too large, or the image fails validation
1131
+
1132
+ #### ota_confirm()
1133
+
1134
+ Mark the running app valid, cancelling a pending OTA rollback (wraps
1135
+ `esp32.Partition.mark_app_valid_cancel_rollback()`). After a fresh OTA boot the
1136
+ image is in `pending-verify` and reverts on the next reset until confirmed.
1137
+
1138
+ ```python
1139
+ mpy.ota_confirm()
1140
+ ```
1141
+
1142
+ **Returns:**
1143
+ - `str`: Label of the running partition
1144
+
1145
+ **Raises:**
1146
+ - `MpyError`: On non-ESP32 devices, without OTA partitions, or if rollback is unsupported
1147
+
1148
+ #### ota_set_boot(label=None)
1149
+
1150
+ Switch the boot partition for the next reset.
1151
+
1152
+ ```python
1153
+ mpy.ota_set_boot(label=None)
1154
+ ```
1155
+
1156
+ **Parameters:**
1157
+ - `label` (str, optional): Target app partition (e.g. `'ota_1'`). If `None`, the passive slot from `get_next_update()` is used - a manual rollback.
1158
+
1159
+ **Returns:**
1160
+ - `str`: Label of the partition set as boot
1161
+
1162
+ **Raises:**
1163
+ - `MpyError`: On non-ESP32 devices, no OTA layout, or unknown label
1164
+
1165
+ **Example:**
1166
+ ```python
1167
+ >>> mpy.ota_set_boot() # boot the other image
1168
+ 'ota_1'
1169
+ >>> mpy.machine_reset()
1170
+ ```
1171
+
1172
+ #### check_ota_image(data, force=False)
1173
+
1174
+ Validate firmware before an OTA upload. Rejects bootloader/combined images,
1175
+ empty or corrupt data, and images built for a different chip - turning a long
1176
+ upload followed by `ESP_ERR_OTA_VALIDATE_FAILED` into an immediate error.
1177
+
1178
+ ```python
1179
+ mpy.check_ota_image(data, force=False)
1180
+ ```
1181
+
1182
+ **Parameters:**
1183
+ - `data` (bytes): Firmware image
1184
+ - `force` (bool): Report problems as warnings and continue instead of raising
1185
+
1186
+ **Raises:**
1187
+ - `MpyError`: If the data is not an application image for this chip (unless `force`)
1188
+
1189
+ #### running_chip_id()
1190
+
1191
+ Chip id of the currently running application, read from its image header. The
1192
+ bootloader validates `chip_id` against the real silicon before booting, so a
1193
+ running image proves its id matches the actual chip - and the header layout is
1194
+ stable across IDF/MicroPython versions.
1195
+
1196
+ ```python
1197
+ mpy.running_chip_id()
1198
+ ```
1199
+
1200
+ The value is cached; `reset_state()` clears it.
1201
+
1202
+ **Returns:**
1203
+ - `int`: Chip id (see `mpytool.mpy.ESP_CHIP_IDS`), or `None` if undetermined
1204
+
1205
+ ### Module Functions
1206
+
1207
+ #### esp_chip_name(chip_id)
1208
+
1209
+ Display name for an `esp_chip_id_t` value.
1210
+
1211
+ ```python
1212
+ from mpytool import esp_chip_name
1213
+ ```
1214
+
1215
+ **Parameters:**
1216
+ - `chip_id` (int or None): Chip id, e.g. from `running_chip_id()`
1217
+
1218
+ **Returns:**
1219
+ - `str`: Name such as `'ESP32-P4'`, or `'0x0099'` for a chip not in `ESP_CHIP_IDS` (the table is display-only, so newer chips still work); `None` if `chip_id` is `None`
1220
+
1221
+ #### parse_esp_image(data)
1222
+
1223
+ Parse the header of an ESP32 flash image. Distinguishes an application image
1224
+ (what OTA requires) from a bootloader or combined flash image.
1225
+
1226
+ ```python
1227
+ from mpytool import parse_esp_image
1228
+ ```
1229
+
1230
+ **Parameters:**
1231
+ - `data` (bytes): At least the first ~176 bytes of the image
1232
+
1233
+ **Returns:**
1234
+ - `dict` with `'kind'` (`'app'`, `'bootloader'`, `'empty'` or `'invalid'`), plus:
1235
+ - `'chip_id'`, `'chip_name'`, `'segments'` (app/bootloader)
1236
+ - `'idf_ver'`, `'date'`, `'time'`, `'version'`, `'project'` (app only; MicroPython leaves version/project empty)
1237
+ - `'app_offset'` - where the app starts inside a combined image (bootloader only)
1238
+
1239
+ **Example:**
1240
+ ```python
1241
+ >>> parse_esp_image(open('micropython.bin', 'rb').read())
1242
+ {'segments': 8, 'chip_id': 18, 'chip_name': 'ESP32-P4', 'kind': 'app', ...}
1243
+ >>> parse_esp_image(open('firmware.bin', 'rb').read())
1244
+ {'segments': 3, 'chip_id': 18, 'chip_name': 'ESP32-P4', 'kind': 'bootloader',
1245
+ 'app_offset': 57344}
1246
+ ```
1089
1247
 
1090
1248
  ## MpyComm Class
1091
1249
 
@@ -281,11 +281,12 @@ _mpytool() {
281
281
  [[ $nargs -ge 1 ]] && compadd -- '--'
282
282
  ;;
283
283
  run)
284
- # 1 local .py file, -- after it
284
+ # optional local .py file or '-' for STDIN, -- after it
285
285
  if [[ $pos -eq 2 ]]; then
286
286
  _files -g '*.py'
287
+ compadd -- '-'
287
288
  fi
288
- [[ $nargs -ge 1 ]] && compadd -- '--'
289
+ compadd -- '--'
289
290
  ;;
290
291
  edit)
291
292
  # --editor CMD + 1 remote file, -- after file
@@ -330,10 +331,23 @@ _mpytool() {
330
331
  )
331
332
  _describe -t subcmds 'flash subcommand' subcmds
332
333
  elif [[ "$flash_subcmd" == "ota" ]]; then
333
- # ota: 1 firmware file, -- after it
334
+ # ota: <firmware.bin> | confirm | boot [label]
335
+ local ota_action="${words[cmd_start+2]}"
334
336
  if [[ $pos -eq 3 ]]; then
337
+ local -a otacmds=(
338
+ 'confirm:mark running app valid (cancel rollback)'
339
+ 'boot:switch boot partition'
340
+ )
341
+ _describe -t otacmds 'ota action' otacmds
335
342
  _files -g "*.bin"
343
+ compadd -- '--force'
344
+ elif [[ "$ota_action" == "boot" && $pos -eq 4 ]]; then
345
+ # partition label (device-specific, no completion)
346
+ compadd -- '--'
347
+ elif [[ "$ota_action" == "confirm" ]]; then
348
+ compadd -- '--'
336
349
  else
350
+ compadd -- '--force'
337
351
  compadd -- '--'
338
352
  fi
339
353
  elif [[ $pos -eq 3 && "$flash_subcmd" == "erase" ]]; then
@@ -351,9 +365,9 @@ _mpytool() {
351
365
  # -- after complete subcommand
352
366
  if [[ "$flash_subcmd" == "erase" && $pos -ge 3 ]]; then
353
367
  : # already handled above
354
- elif [[ "$flash_subcmd" == "ota" && $pos -ge 4 ]]; then
355
- compadd -- '--'
356
- elif [[ "$flash_subcmd" != "erase" && "$flash_subcmd" != "ota" && $pos -ge 5 ]]; then
368
+ elif [[ "$flash_subcmd" == "ota" ]]; then
369
+ : # already handled above
370
+ elif [[ "$flash_subcmd" != "erase" && $pos -ge 5 ]]; then
357
371
  compadd -- '--'
358
372
  fi
359
373
  ;;
@@ -284,11 +284,12 @@ _mpytool() {
284
284
  [[ $nargs -ge 1 && ( -z "$cur" || "--" == "$cur"* ) ]] && COMPREPLY+=("--")
285
285
  ;;
286
286
  run)
287
- # 1 local .py file, -- after it
287
+ # optional local .py file or '-' for STDIN, -- after it
288
288
  if [[ $pos -eq 2 ]]; then
289
289
  COMPREPLY=($(compgen -f -X '!*.py' -- "$cur"))
290
+ [[ "-" == "$cur"* ]] && COMPREPLY+=("-")
290
291
  fi
291
- [[ $nargs -ge 1 && ( -z "$cur" || "--" == "$cur"* ) ]] && COMPREPLY+=("--")
292
+ [[ -z "$cur" || "--" == "$cur"* ]] && COMPREPLY+=("--")
292
293
  ;;
293
294
  edit)
294
295
  # --editor CMD + 1 remote file, -- after file
@@ -324,10 +325,18 @@ _mpytool() {
324
325
  if [[ $pos -eq 2 ]]; then
325
326
  COMPREPLY=($(compgen -W "read write erase ota" -- "$cur"))
326
327
  elif [[ "$flash_subcmd" == "ota" ]]; then
327
- # ota: 1 firmware file, -- after it
328
+ # ota: <firmware.bin> | confirm | boot [label]
329
+ local ota_action="${COMP_WORDS[cmd_start+2]}"
328
330
  if [[ $pos -eq 3 ]]; then
329
- COMPREPLY=($(compgen -f -X '!*.bin' -- "$cur"))
331
+ COMPREPLY=($(compgen -W "confirm boot --force" -- "$cur"))
332
+ COMPREPLY+=($(compgen -f -X '!*.bin' -- "$cur"))
333
+ elif [[ "$ota_action" == "boot" && $pos -eq 4 ]]; then
334
+ # partition label (device-specific, no completion)
335
+ [[ -z "$cur" || "--" == "$cur"* ]] && COMPREPLY+=("--")
336
+ elif [[ "$ota_action" == "confirm" ]]; then
337
+ [[ -z "$cur" || "--" == "$cur"* ]] && COMPREPLY+=("--")
330
338
  else
339
+ COMPREPLY=($(compgen -W "--force" -- "$cur"))
331
340
  [[ -z "$cur" || "--" == "$cur"* ]] && COMPREPLY+=("--")
332
341
  fi
333
342
  elif [[ $pos -eq 3 && "$flash_subcmd" == "erase" ]]; then
@@ -343,9 +352,9 @@ _mpytool() {
343
352
  [[ -z "$cur" || "--" == "$cur"* ]] && COMPREPLY+=("--")
344
353
  fi
345
354
  # -- after complete subcommand
346
- if [[ "$flash_subcmd" == "ota" && $pos -ge 4 ]]; then
347
- [[ -z "$cur" || "--" == "$cur"* ]] && COMPREPLY+=("--")
348
- elif [[ "$flash_subcmd" != "erase" && "$flash_subcmd" != "ota" && $pos -ge 5 ]]; then
355
+ if [[ "$flash_subcmd" == "ota" ]]; then
356
+ : # already handled above
357
+ elif [[ "$flash_subcmd" != "erase" && $pos -ge 5 ]]; then
349
358
  [[ -z "$cur" || "--" == "$cur"* ]] && COMPREPLY+=("--")
350
359
  fi
351
360
  ;;
@@ -4,5 +4,7 @@ from mpytool.conn import ConnError, Timeout
4
4
  from mpytool.conn_serial import ConnSerial
5
5
  from mpytool.conn_socket import ConnSocket
6
6
  from mpytool.mpy_comm import MpyComm, MpyError, CmdError
7
- from mpytool.mpy import Mpy, PathNotFound, FileNotFound, DirNotFound
7
+ from mpytool.mpy import (
8
+ Mpy, PathNotFound, FileNotFound, DirNotFound, parse_esp_image,
9
+ esp_chip_name)
8
10
  from mpytool.logger import SimpleColorLogger
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.5.0'
22
- __version_tuple__ = version_tuple = (2, 5, 0)
21
+ __version__ = version = '2.6.0'
22
+ __version_tuple__ = version_tuple = (2, 6, 0)
23
23
 
24
- __commit_id__ = commit_id = 'ga70fd9bfe'
24
+ __commit_id__ = commit_id = 'g69a52db1e'