idftool 0.1.0__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idftool
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Utility for flashing, provisioning, and interacting with Espressif SOCs running ESP-IDF
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Source, https://github.com/nebkat/idftool
@@ -9,6 +9,7 @@ Description-Content-Type: text/markdown
9
9
  License-File: LICENSE.md
10
10
  Requires-Dist: esptool~=5.2.0
11
11
  Requires-Dist: esp-idf-defs~=0.1.0
12
+ Requires-Dist: esp-idf-nvs-partition-gen~=0.2.0
12
13
  Dynamic: license-file
13
14
 
14
15
  # idftool
@@ -39,9 +40,31 @@ especially when dealing with multiple devices/partition tables.
39
40
  partition binaries into a single ZIP; optionally flashing a partition
40
41
  table. `write-bundle` reflashes the lot in one command.
41
42
 
42
- > Pre-built binaries are published on the
43
- > [Releases](https://github.com/nebkat/idftool/releases) page.
44
- > See `CONTRIBUTING.md` for building from source.
43
+ ## Installation
44
+
45
+ ### pipx (recommended)
46
+
47
+ [pipx](https://pipx.pypa.io) installs idftool into an isolated environment and
48
+ puts it on your `PATH` automatically:
49
+
50
+ ```bash
51
+ pipx install idftool
52
+ ```
53
+
54
+ Install pipx first if you don't have it:
55
+
56
+ | Platform | Command |
57
+ |----------|---------|
58
+ | macOS | `brew install pipx` |
59
+ | Windows | `winget install python.pipx` |
60
+ | Linux / other | `pip install pipx` |
61
+
62
+ ### Binary
63
+
64
+ If you don't have Python, download the pre-built binary from the
65
+ [Releases](https://github.com/nebkat/idftool/releases) page. Prefer the `-dir`
66
+ archive over the single-file download — it starts instantly rather than extracting
67
+ itself on each run.
45
68
 
46
69
  ## At a glance
47
70
 
@@ -72,50 +95,41 @@ $ idftool set-boot ota_1
72
95
  Setting boot partition to 'ota_1'...
73
96
  ```
74
97
 
75
- ## Global options
76
-
77
- These flags apply to every subcommand and go **before** the command name:
78
-
79
- | Flag | Purpose |
80
- |------|---------|
81
- | `-p`, `--port PATH` | Serial port device. If omitted, idftool auto-picks one. |
82
- | `-b`, `--baud N` | Serial baud rate (defaults to esptool's ROM baud, 115200). |
83
- | `--no-reset` | Skip the hard reset that normally happens after a command. |
84
- | `--partition-table-file PATH` | Use a CSV or binary partition table from disk instead of reading it off the device. |
85
- | `--partition-table-offset OFFSET` | Where to expect the partition table in flash (default `0x8000`). |
86
- | `--partition-table-size SIZE` | Size of the partition table region (default `0x1000`). |
87
- | `--primary-bootloader-offset OFFSET` | Primary bootloader offset, or a chip name like `esp32s3` to pick a default. Only needed when addressing the `bootloader` partition by name in **offline** mode (`--partition-table-file` with no device); auto-detected from a connected chip otherwise. |
88
- | `--recovery-bootloader-offset OFFSET` | Recovery bootloader offset; same scope as `--primary-bootloader-offset`. |
89
-
90
- The commands `list`, `merge-bin`, and `create-bundle` will work **without**
91
- a device when you supply `--partition-table-file`; everything else needs a
92
- connected ESP.
93
-
94
- ## Partition addressing
95
-
96
- Wherever a command takes a `partition` argument you can pass:
97
-
98
- - A **name** from the partition table (`nvs`, `ota_0`, `storage`, …).
99
- - A **numeric address** that matches an existing partition's start
100
- offset exactly — equivalent to looking the partition up by name, just
101
- keyed on its address.
102
- - An **offset into a partition**: `name[offset]`. Negative values count
103
- from the end. Sets the starting point for the operation. Accepted by
104
- `write` and `merge-bin`.
105
- - A **slice of a partition**: `name[start:stop]`. Negative values count
106
- from the end, and a `+N` stop is a length relative to `start`.
107
- Accepted by `read`, `erase`, and `view`. Examples:
108
- - `nvs[0:0x100]` — first 256 bytes of `nvs`
109
- - `storage[-0x1000:]` — last 4 KiB of `storage`
110
- - `ota_0[0x1000:+0x800]` — 2 KiB starting 4 KiB into `ota_0`
111
-
112
- All numeric values in addresses, offsets, and sizes accept either
113
- decimal (`4096`) or hex (`0x1000`).
114
-
115
98
  ---
116
99
 
117
100
  ## Command reference
118
101
 
102
+ | Command | Description |
103
+ |---------|-------------|
104
+ | **Discovery** | |
105
+ | [`devices`](#devices) | List serial ports with hardware IDs |
106
+ | [`list`](#list) | Print the partition table |
107
+ | **Partition I/O** | |
108
+ | [`read`](#read) | Read a partition (or slice) into a file |
109
+ | [`write`](#write) | Write one or more files to named partitions |
110
+ | [`erase`](#erase) | Erase a partition (or slice) |
111
+ | [`view`](#view) | Pretty-print a partition's contents |
112
+ | **Firmware** | |
113
+ | [`ota`](#ota) | Push an app to the next OTA slot and switch to it |
114
+ | [`factory`](#factory) | Flash an app to the factory partition |
115
+ | **Boot selection** | |
116
+ | [`get-boot`](#get-boot) | Show the currently-active OTA slot |
117
+ | [`set-boot`](#set-boot) | Force the next boot to a specific OTA partition |
118
+ | [`clear-boot`](#clear-boot) | Erase otadata and let the bootloader fall back |
119
+ | **Images** | |
120
+ | [`create-image`](#create-image) | Merge partition binaries into a single flash image |
121
+ | [`dump-image`](#dump-image) | Dump the entire flash to an image file |
122
+ | [`write-image`](#write-image-alias-reflash) | Write a full flash image to the device |
123
+ | **Bundles** | |
124
+ | [`create-bundle`](#create-bundle) | Pack partition images into a ZIP bundle |
125
+ | [`dump-bundle`](#dump-bundle) | Pack every partition from the device into a ZIP |
126
+ | [`write-bundle`](#write-bundle) | Flash every binary in a bundle ZIP |
127
+ | **NVS** | |
128
+ | [`create-nvs`](#create-nvs) | Generate an NVS partition image from a CSV file |
129
+ | [`write-nvs`](#write-nvs) | Generate an NVS image from CSV and flash it |
130
+ | **Misc** | |
131
+ | [`enter-bootloader`](#enter-bootloader) | Drop the chip into ROM bootloader mode |
132
+
119
133
  ### Discovery
120
134
 
121
135
  #### `devices`
@@ -278,6 +292,37 @@ to match) instead of reading the table from flash.
278
292
  idftool write-bundle release.zip
279
293
  ```
280
294
 
295
+ ### NVS
296
+
297
+ idftool can generate a binary NVS (Non-Volatile Storage) partition image
298
+ from a CSV file, using the same format as ESP-IDF's
299
+ [`nvs_partition_gen.py`](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/storage/nvs_partition_gen.html).
300
+
301
+ Example CSV:
302
+ ```csv
303
+ key,type,encoding,value
304
+ storage,namespace,,
305
+ device_name,data,string,My Device
306
+ device_id,data,u32,12345
307
+ api_key,data,string,abc123def456
308
+ ```
309
+
310
+ #### `create-nvs`
311
+ Generate an NVS partition image from a CSV file, offline. Requires either
312
+ `--size` (explicit partition size) or `--partition` (look up the size from
313
+ the partition table — needs `--partition-table-file` or a device).
314
+ ```text
315
+ idftool create-nvs nvs.csv -o nvs.bin --size 0x6000
316
+ idftool --partition-table-file partitions.csv create-nvs nvs.csv -o nvs.bin --partition nvs
317
+ ```
318
+
319
+ #### `write-nvs`
320
+ Generate an NVS partition image from a CSV file and flash it to the named
321
+ partition on the device.
322
+ ```text
323
+ idftool write-nvs nvs nvs.csv
324
+ ```
325
+
281
326
  ### Misc
282
327
 
283
328
  #### `enter-bootloader`
@@ -291,3 +336,45 @@ isn't fully settled) are retried silently.
291
336
  idftool -p /dev/cu.usbmodem1101 enter-bootloader
292
337
  ```
293
338
  Requires `-p`/`--port`.
339
+
340
+ --
341
+
342
+ ## Global options
343
+
344
+ These flags apply to every subcommand and go **before** the command name:
345
+
346
+ | Flag | Purpose |
347
+ |------|---------|
348
+ | `-p`, `--port PATH` | Serial port device. If omitted, idftool auto-picks one. |
349
+ | `-b`, `--baud N` | Serial baud rate (defaults to esptool's ROM baud, 115200). |
350
+ | `--no-reset` | Skip the hard reset that normally happens after a command. |
351
+ | `--partition-table-file PATH` | Use a CSV or binary partition table from disk instead of reading it off the device. |
352
+ | `--partition-table-offset OFFSET` | Where to expect the partition table in flash (default `0x8000`). |
353
+ | `--partition-table-size SIZE` | Size of the partition table region (default `0x1000`). |
354
+ | `--primary-bootloader-offset OFFSET` | Primary bootloader offset, or a chip name like `esp32s3` to pick a default. Only needed when addressing the `bootloader` partition by name in **offline** mode (`--partition-table-file` with no device); auto-detected from a connected chip otherwise. |
355
+ | `--recovery-bootloader-offset OFFSET` | Recovery bootloader offset; same scope as `--primary-bootloader-offset`. |
356
+
357
+ The commands `list`, `merge-bin`, and `create-bundle` will work **without**
358
+ a device when you supply `--partition-table-file`; everything else needs a
359
+ connected ESP.
360
+
361
+ ## Partition addressing
362
+
363
+ Wherever a command takes a `partition` argument you can pass:
364
+
365
+ - A **name** from the partition table (`nvs`, `ota_0`, `storage`, …).
366
+ - A **numeric address** that matches an existing partition's start
367
+ offset exactly — equivalent to looking the partition up by name, just
368
+ keyed on its address.
369
+ - An **offset into a partition**: `name[offset]`. Negative values count
370
+ from the end. Sets the starting point for the operation. Accepted by
371
+ `write` and `merge-bin`.
372
+ - A **slice of a partition**: `name[start:stop]`. Negative values count
373
+ from the end, and a `+N` stop is a length relative to `start`.
374
+ Accepted by `read`, `erase`, and `view`. Examples:
375
+ - `nvs[0:0x100]` — first 256 bytes of `nvs`
376
+ - `storage[-0x1000:]` — last 4 KiB of `storage`
377
+ - `ota_0[0x1000:+0x800]` — 2 KiB starting 4 KiB into `ota_0`
378
+
379
+ All numeric values in addresses, offsets, and sizes accept either
380
+ decimal (`4096`) or hex (`0x1000`).
@@ -26,9 +26,31 @@ especially when dealing with multiple devices/partition tables.
26
26
  partition binaries into a single ZIP; optionally flashing a partition
27
27
  table. `write-bundle` reflashes the lot in one command.
28
28
 
29
- > Pre-built binaries are published on the
30
- > [Releases](https://github.com/nebkat/idftool/releases) page.
31
- > See `CONTRIBUTING.md` for building from source.
29
+ ## Installation
30
+
31
+ ### pipx (recommended)
32
+
33
+ [pipx](https://pipx.pypa.io) installs idftool into an isolated environment and
34
+ puts it on your `PATH` automatically:
35
+
36
+ ```bash
37
+ pipx install idftool
38
+ ```
39
+
40
+ Install pipx first if you don't have it:
41
+
42
+ | Platform | Command |
43
+ |----------|---------|
44
+ | macOS | `brew install pipx` |
45
+ | Windows | `winget install python.pipx` |
46
+ | Linux / other | `pip install pipx` |
47
+
48
+ ### Binary
49
+
50
+ If you don't have Python, download the pre-built binary from the
51
+ [Releases](https://github.com/nebkat/idftool/releases) page. Prefer the `-dir`
52
+ archive over the single-file download — it starts instantly rather than extracting
53
+ itself on each run.
32
54
 
33
55
  ## At a glance
34
56
 
@@ -59,50 +81,41 @@ $ idftool set-boot ota_1
59
81
  Setting boot partition to 'ota_1'...
60
82
  ```
61
83
 
62
- ## Global options
63
-
64
- These flags apply to every subcommand and go **before** the command name:
65
-
66
- | Flag | Purpose |
67
- |------|---------|
68
- | `-p`, `--port PATH` | Serial port device. If omitted, idftool auto-picks one. |
69
- | `-b`, `--baud N` | Serial baud rate (defaults to esptool's ROM baud, 115200). |
70
- | `--no-reset` | Skip the hard reset that normally happens after a command. |
71
- | `--partition-table-file PATH` | Use a CSV or binary partition table from disk instead of reading it off the device. |
72
- | `--partition-table-offset OFFSET` | Where to expect the partition table in flash (default `0x8000`). |
73
- | `--partition-table-size SIZE` | Size of the partition table region (default `0x1000`). |
74
- | `--primary-bootloader-offset OFFSET` | Primary bootloader offset, or a chip name like `esp32s3` to pick a default. Only needed when addressing the `bootloader` partition by name in **offline** mode (`--partition-table-file` with no device); auto-detected from a connected chip otherwise. |
75
- | `--recovery-bootloader-offset OFFSET` | Recovery bootloader offset; same scope as `--primary-bootloader-offset`. |
76
-
77
- The commands `list`, `merge-bin`, and `create-bundle` will work **without**
78
- a device when you supply `--partition-table-file`; everything else needs a
79
- connected ESP.
80
-
81
- ## Partition addressing
82
-
83
- Wherever a command takes a `partition` argument you can pass:
84
-
85
- - A **name** from the partition table (`nvs`, `ota_0`, `storage`, …).
86
- - A **numeric address** that matches an existing partition's start
87
- offset exactly — equivalent to looking the partition up by name, just
88
- keyed on its address.
89
- - An **offset into a partition**: `name[offset]`. Negative values count
90
- from the end. Sets the starting point for the operation. Accepted by
91
- `write` and `merge-bin`.
92
- - A **slice of a partition**: `name[start:stop]`. Negative values count
93
- from the end, and a `+N` stop is a length relative to `start`.
94
- Accepted by `read`, `erase`, and `view`. Examples:
95
- - `nvs[0:0x100]` — first 256 bytes of `nvs`
96
- - `storage[-0x1000:]` — last 4 KiB of `storage`
97
- - `ota_0[0x1000:+0x800]` — 2 KiB starting 4 KiB into `ota_0`
98
-
99
- All numeric values in addresses, offsets, and sizes accept either
100
- decimal (`4096`) or hex (`0x1000`).
101
-
102
84
  ---
103
85
 
104
86
  ## Command reference
105
87
 
88
+ | Command | Description |
89
+ |---------|-------------|
90
+ | **Discovery** | |
91
+ | [`devices`](#devices) | List serial ports with hardware IDs |
92
+ | [`list`](#list) | Print the partition table |
93
+ | **Partition I/O** | |
94
+ | [`read`](#read) | Read a partition (or slice) into a file |
95
+ | [`write`](#write) | Write one or more files to named partitions |
96
+ | [`erase`](#erase) | Erase a partition (or slice) |
97
+ | [`view`](#view) | Pretty-print a partition's contents |
98
+ | **Firmware** | |
99
+ | [`ota`](#ota) | Push an app to the next OTA slot and switch to it |
100
+ | [`factory`](#factory) | Flash an app to the factory partition |
101
+ | **Boot selection** | |
102
+ | [`get-boot`](#get-boot) | Show the currently-active OTA slot |
103
+ | [`set-boot`](#set-boot) | Force the next boot to a specific OTA partition |
104
+ | [`clear-boot`](#clear-boot) | Erase otadata and let the bootloader fall back |
105
+ | **Images** | |
106
+ | [`create-image`](#create-image) | Merge partition binaries into a single flash image |
107
+ | [`dump-image`](#dump-image) | Dump the entire flash to an image file |
108
+ | [`write-image`](#write-image-alias-reflash) | Write a full flash image to the device |
109
+ | **Bundles** | |
110
+ | [`create-bundle`](#create-bundle) | Pack partition images into a ZIP bundle |
111
+ | [`dump-bundle`](#dump-bundle) | Pack every partition from the device into a ZIP |
112
+ | [`write-bundle`](#write-bundle) | Flash every binary in a bundle ZIP |
113
+ | **NVS** | |
114
+ | [`create-nvs`](#create-nvs) | Generate an NVS partition image from a CSV file |
115
+ | [`write-nvs`](#write-nvs) | Generate an NVS image from CSV and flash it |
116
+ | **Misc** | |
117
+ | [`enter-bootloader`](#enter-bootloader) | Drop the chip into ROM bootloader mode |
118
+
106
119
  ### Discovery
107
120
 
108
121
  #### `devices`
@@ -265,6 +278,37 @@ to match) instead of reading the table from flash.
265
278
  idftool write-bundle release.zip
266
279
  ```
267
280
 
281
+ ### NVS
282
+
283
+ idftool can generate a binary NVS (Non-Volatile Storage) partition image
284
+ from a CSV file, using the same format as ESP-IDF's
285
+ [`nvs_partition_gen.py`](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/storage/nvs_partition_gen.html).
286
+
287
+ Example CSV:
288
+ ```csv
289
+ key,type,encoding,value
290
+ storage,namespace,,
291
+ device_name,data,string,My Device
292
+ device_id,data,u32,12345
293
+ api_key,data,string,abc123def456
294
+ ```
295
+
296
+ #### `create-nvs`
297
+ Generate an NVS partition image from a CSV file, offline. Requires either
298
+ `--size` (explicit partition size) or `--partition` (look up the size from
299
+ the partition table — needs `--partition-table-file` or a device).
300
+ ```text
301
+ idftool create-nvs nvs.csv -o nvs.bin --size 0x6000
302
+ idftool --partition-table-file partitions.csv create-nvs nvs.csv -o nvs.bin --partition nvs
303
+ ```
304
+
305
+ #### `write-nvs`
306
+ Generate an NVS partition image from a CSV file and flash it to the named
307
+ partition on the device.
308
+ ```text
309
+ idftool write-nvs nvs nvs.csv
310
+ ```
311
+
268
312
  ### Misc
269
313
 
270
314
  #### `enter-bootloader`
@@ -278,3 +322,45 @@ isn't fully settled) are retried silently.
278
322
  idftool -p /dev/cu.usbmodem1101 enter-bootloader
279
323
  ```
280
324
  Requires `-p`/`--port`.
325
+
326
+ --
327
+
328
+ ## Global options
329
+
330
+ These flags apply to every subcommand and go **before** the command name:
331
+
332
+ | Flag | Purpose |
333
+ |------|---------|
334
+ | `-p`, `--port PATH` | Serial port device. If omitted, idftool auto-picks one. |
335
+ | `-b`, `--baud N` | Serial baud rate (defaults to esptool's ROM baud, 115200). |
336
+ | `--no-reset` | Skip the hard reset that normally happens after a command. |
337
+ | `--partition-table-file PATH` | Use a CSV or binary partition table from disk instead of reading it off the device. |
338
+ | `--partition-table-offset OFFSET` | Where to expect the partition table in flash (default `0x8000`). |
339
+ | `--partition-table-size SIZE` | Size of the partition table region (default `0x1000`). |
340
+ | `--primary-bootloader-offset OFFSET` | Primary bootloader offset, or a chip name like `esp32s3` to pick a default. Only needed when addressing the `bootloader` partition by name in **offline** mode (`--partition-table-file` with no device); auto-detected from a connected chip otherwise. |
341
+ | `--recovery-bootloader-offset OFFSET` | Recovery bootloader offset; same scope as `--primary-bootloader-offset`. |
342
+
343
+ The commands `list`, `merge-bin`, and `create-bundle` will work **without**
344
+ a device when you supply `--partition-table-file`; everything else needs a
345
+ connected ESP.
346
+
347
+ ## Partition addressing
348
+
349
+ Wherever a command takes a `partition` argument you can pass:
350
+
351
+ - A **name** from the partition table (`nvs`, `ota_0`, `storage`, …).
352
+ - A **numeric address** that matches an existing partition's start
353
+ offset exactly — equivalent to looking the partition up by name, just
354
+ keyed on its address.
355
+ - An **offset into a partition**: `name[offset]`. Negative values count
356
+ from the end. Sets the starting point for the operation. Accepted by
357
+ `write` and `merge-bin`.
358
+ - A **slice of a partition**: `name[start:stop]`. Negative values count
359
+ from the end, and a `+N` stop is a length relative to `start`.
360
+ Accepted by `read`, `erase`, and `view`. Examples:
361
+ - `nvs[0:0x100]` — first 256 bytes of `nvs`
362
+ - `storage[-0x1000:]` — last 4 KiB of `storage`
363
+ - `ota_0[0x1000:+0x800]` — 2 KiB starting 4 KiB into `ota_0`
364
+
365
+ All numeric values in addresses, offsets, and sizes accept either
366
+ decimal (`4096`) or hex (`0x1000`).
@@ -11,6 +11,8 @@ from serial import SerialException
11
11
  from serial.tools import list_ports
12
12
  from serial.tools.list_ports_common import ListPortInfo
13
13
 
14
+ from idftool.traceback import install as install_excepthook, print_exception
15
+
14
16
  from esp_idf_defs import ImageMetadata, ChipId
15
17
  from esp_idf_defs.otadata import OtaDataParameters, OtaDataSelectEntry
16
18
  from esp_idf_defs.partitions import PARTITION_TABLE_SIZE, PARTITION_TABLE_OFFSET, PartitionTable, print_partition_table, \
@@ -523,6 +525,48 @@ def command_factory(esp: ESPLoader, partition_table: PartitionTable, app_binary_
523
525
  print("Erasing 'otadata' partition...")
524
526
  esp.erase_region(offset=otadata_partition.offset, size=otadata_partition.size)
525
527
 
528
+ def generate_nvs_image(csv_file: str, size: int) -> bytes:
529
+ import tempfile
530
+ from esp_idf_nvs_partition_gen import nvs_partition_gen
531
+
532
+ with tempfile.NamedTemporaryFile(suffix='.bin', delete=False) as tmp:
533
+ tmp_path = tmp.name
534
+ try:
535
+ nvs_args = argparse.Namespace(
536
+ input=csv_file,
537
+ output=os.path.basename(tmp_path),
538
+ outdir=os.path.dirname(tmp_path),
539
+ size=f"{size:#x}",
540
+ version=2,
541
+ )
542
+ nvs_partition_gen.generate(nvs_args)
543
+ with open(tmp_path, 'rb') as f:
544
+ return f.read()
545
+ finally:
546
+ if os.path.exists(tmp_path):
547
+ os.unlink(tmp_path)
548
+
549
+ def command_create_nvs(csv_file: str, size: int, output_file: str):
550
+ print(f"Generating NVS image from '{csv_file}' (size={size:#x})...")
551
+ image = generate_nvs_image(csv_file, size)
552
+ with open(output_file, 'wb') as f:
553
+ f.write(image)
554
+ print(f"Wrote {len(image):#x} bytes to '{output_file}'")
555
+
556
+ def command_write_nvs(
557
+ esp: ESPLoader,
558
+ partition_table: PartitionTable,
559
+ partition_table_entry: PartitionDefinition,
560
+ bootloader_entry: Optional[PartitionDefinition],
561
+ csv_file: str,
562
+ partition_name: str
563
+ ):
564
+ partition = get_partition(partition_table, partition_table_entry, bootloader_entry, partition_name)
565
+ print(f"Generating NVS image from '{csv_file}' for partition '{partition.name}' (size={partition.size:#x})...")
566
+ image = generate_nvs_image(csv_file, partition.size)
567
+ print(f"Writing NVS image to partition '{partition.name}' (offset={partition.offset:#x}, size={partition.size:#x})")
568
+ write_flash(esp=esp, addr_data=[(partition.offset, image)], flash_size='detect')
569
+
526
570
  def command_app_info(app_binary_file: str):
527
571
  app_binary = open(app_binary_file, 'rb').read()
528
572
  try:
@@ -640,8 +684,13 @@ def main(args):
640
684
  command_enter_bootloader(port=args.port, baud=args.baud)
641
685
  return
642
686
 
687
+ # create-nvs with explicit --size needs neither device nor partition table
688
+ if args.command == 'create-nvs' and args.size is not None:
689
+ command_create_nvs(args.csv_file, args.size, args.output)
690
+ return
691
+
643
692
  # Connect to ESP device if required
644
- requires_esp = args.command not in ['list', 'create-image', 'create-bundle'] or not args.partition_table_file
693
+ requires_esp = args.command not in ['list', 'create-image', 'create-bundle', 'create-nvs'] or not args.partition_table_file
645
694
  esp = get_esp(port=args.port, baud=args.baud) if requires_esp else None
646
695
  if not args.primary_bootloader_offset and esp:
647
696
  args.primary_bootloader_offset = esp.BOOTLOADER_FLASH_OFFSET
@@ -794,12 +843,25 @@ def main(args):
794
843
  command_factory(esp=esp, partition_table=partition_table, app_binary_file=args.app_binary_file)
795
844
  elif args.command == 'write-image':
796
845
  command_write_image(esp=esp, bootloader_entry=bootloader_entry, image_file_path=args.image_file)
846
+ elif args.command == 'create-nvs':
847
+ partition = get_partition(partition_table, partition_table_entry, bootloader_entry, args.partition)
848
+ command_create_nvs(args.csv_file, partition.size, args.output)
849
+ elif args.command == 'write-nvs':
850
+ command_write_nvs(
851
+ esp=esp,
852
+ partition_table=partition_table,
853
+ partition_table_entry=partition_table_entry,
854
+ bootloader_entry=bootloader_entry,
855
+ csv_file=args.csv_file,
856
+ partition_name=args.partition
857
+ )
797
858
  else:
798
859
  print(f"Unknown command: {args.command}", file=sys.stderr)
799
860
 
800
861
  if esp and not args.no_reset: esp.hard_reset()
801
862
 
802
863
  def _main():
864
+ install_excepthook()
803
865
  parser = argparse.ArgumentParser()
804
866
 
805
867
  parser.add_argument('-p', '--port', help='Serial port device')
@@ -888,6 +950,19 @@ def _main():
888
950
  write_bundle_parser = subparsers.add_parser('write-bundle', help='Write a ZIP bundle of partition binaries')
889
951
  write_bundle_parser.add_argument('input_file', help='Input ZIP filename')
890
952
 
953
+ # Create NVS subcommand
954
+ create_nvs_parser = subparsers.add_parser('create-nvs', help='Generate NVS partition image from CSV')
955
+ create_nvs_parser.add_argument('csv_file', help='Input NVS CSV file')
956
+ create_nvs_parser.add_argument('-o', '--output', required=True, help='Output binary filename (.bin)')
957
+ create_nvs_size_group = create_nvs_parser.add_mutually_exclusive_group(required=True)
958
+ create_nvs_size_group.add_argument('--size', type=auto_int, help='Partition size in bytes (e.g. 0x6000)')
959
+ create_nvs_size_group.add_argument('--partition', help='Partition name to read size from partition table')
960
+
961
+ # Write NVS subcommand
962
+ write_nvs_parser = subparsers.add_parser('write-nvs', help='Generate NVS partition image from CSV and write to device')
963
+ write_nvs_parser.add_argument('partition', help='NVS partition name')
964
+ write_nvs_parser.add_argument('csv_file', help='Input NVS CSV file')
965
+
891
966
  # Factory subcommand
892
967
  factory_parser = subparsers.add_parser('factory', help='Perform factory flash and clear boot partition')
893
968
  factory_parser.add_argument('app_binary_file', help='Input file containing app binary')
@@ -914,7 +989,13 @@ def _main():
914
989
  args.command = 'write-image'
915
990
  if args.command == 'enter-bootloader' and not args.port:
916
991
  parser.error("enter-bootloader requires -p/--port")
917
- main(args)
992
+ try:
993
+ main(args)
994
+ except KeyboardInterrupt:
995
+ sys.exit(130)
996
+ except BaseException as e:
997
+ print_exception(e)
998
+ sys.exit(1)
918
999
 
919
1000
  if __name__ == "__main__":
920
1001
  _main()
@@ -0,0 +1,114 @@
1
+ import os
2
+ import sys
3
+ import traceback as _tb
4
+ from types import TracebackType
5
+ from typing import Optional
6
+
7
+
8
+ def _term_width(default: int = 80) -> int:
9
+ try:
10
+ return os.get_terminal_size().columns
11
+ except OSError:
12
+ return default
13
+
14
+
15
+ def _box(lines: list[str], title: str, width: int) -> list[str]:
16
+ inner = max(width - 4, 4)
17
+
18
+ title_str = f"─ {title} "
19
+ if len(title_str) + 3 <= width:
20
+ top = '╭' + title_str + '─' * (width - 2 - len(title_str)) + '╮'
21
+ else:
22
+ top = '╭' + '─' * (width - 2) + '╮'
23
+
24
+ body = []
25
+ for line in lines:
26
+ line = line.expandtabs(4)
27
+ if len(line) > inner:
28
+ line = line[:inner - 1] + '…'
29
+ body.append('│ ' + line.ljust(inner) + ' │')
30
+
31
+ bottom = '╰' + '─' * (width - 2) + '╯'
32
+
33
+ return [top] + body + [bottom]
34
+
35
+
36
+ def _walk_chain(exc: BaseException) -> list[tuple[BaseException, Optional[str]]]:
37
+ """Walk __cause__ / __context__ chain, return (exception, separator_after) oldest-first."""
38
+ chain: list[tuple[BaseException, Optional[str]]] = []
39
+ e: Optional[BaseException] = exc
40
+ # First build a list of (exc, link_type_to_previous) from newest to oldest
41
+ newest_to_oldest: list[tuple[BaseException, Optional[str]]] = []
42
+ while e is not None:
43
+ if e.__cause__ is not None:
44
+ link = "cause"
45
+ prev: Optional[BaseException] = e.__cause__
46
+ elif not e.__suppress_context__ and e.__context__ is not None:
47
+ link = "context"
48
+ prev = e.__context__
49
+ else:
50
+ link = None
51
+ prev = None
52
+ newest_to_oldest.append((e, link))
53
+ e = prev
54
+
55
+ # Reverse to oldest-first. The link recorded with each exception describes
56
+ # how the NEXT (newer) exception relates to this one — i.e. the separator
57
+ # we should print AFTER this exception.
58
+ oldest_to_newest = list(reversed(newest_to_oldest))
59
+ result: list[tuple[BaseException, Optional[str]]] = []
60
+ for i, (exc_i, _) in enumerate(oldest_to_newest):
61
+ if i + 1 < len(oldest_to_newest):
62
+ next_link = oldest_to_newest[i + 1][1]
63
+ if next_link == "cause":
64
+ sep: Optional[str] = "The above exception was the direct cause of the following exception:"
65
+ else:
66
+ sep = "During handling of the above exception, another exception occurred:"
67
+ else:
68
+ sep = None
69
+ result.append((exc_i, sep))
70
+ return result
71
+
72
+
73
+ def _format_one(exc: BaseException, *, color: bool, width: int) -> list[str]:
74
+ frames = _tb.extract_tb(exc.__traceback__)
75
+ frame_lines = [f"in {f.name}:{f.lineno} ({f.filename})" for f in frames]
76
+ if not frame_lines:
77
+ frame_lines = ["<no traceback available>"]
78
+
79
+ box_lines = _box(frame_lines, "Traceback (most recent call last)", width)
80
+ err_line = f"{type(exc).__name__}: {exc}"
81
+
82
+ if color:
83
+ box_lines = [f"\033[2m{l}\033[0m" for l in box_lines]
84
+ err_line = f"\033[1;31m{err_line}\033[0m"
85
+
86
+ return box_lines + [err_line]
87
+
88
+
89
+ def print_exception(exc: BaseException) -> None:
90
+ """Print an exception (and its chain) in rich-style boxed format to stderr."""
91
+ color = sys.stderr.isatty()
92
+ width = min(_term_width(), 120)
93
+
94
+ chain = _walk_chain(exc)
95
+ for i, (e, sep) in enumerate(chain):
96
+ if i > 0:
97
+ print(file=sys.stderr)
98
+ for line in _format_one(e, color=color, width=width):
99
+ print(line, file=sys.stderr)
100
+ if sep is not None:
101
+ print(file=sys.stderr)
102
+ print(sep, file=sys.stderr)
103
+
104
+
105
+ def _excepthook(exc_type: type, exc_value: BaseException, exc_tb: Optional[TracebackType]) -> None:
106
+ if issubclass(exc_type, KeyboardInterrupt):
107
+ sys.__excepthook__(exc_type, exc_value, exc_tb)
108
+ return
109
+ print_exception(exc_value)
110
+
111
+
112
+ def install() -> None:
113
+ """Replace sys.excepthook with a rich-style boxed formatter."""
114
+ sys.excepthook = _excepthook
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: idftool
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Utility for flashing, provisioning, and interacting with Espressif SOCs running ESP-IDF
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Source, https://github.com/nebkat/idftool
@@ -9,6 +9,7 @@ Description-Content-Type: text/markdown
9
9
  License-File: LICENSE.md
10
10
  Requires-Dist: esptool~=5.2.0
11
11
  Requires-Dist: esp-idf-defs~=0.1.0
12
+ Requires-Dist: esp-idf-nvs-partition-gen~=0.2.0
12
13
  Dynamic: license-file
13
14
 
14
15
  # idftool
@@ -39,9 +40,31 @@ especially when dealing with multiple devices/partition tables.
39
40
  partition binaries into a single ZIP; optionally flashing a partition
40
41
  table. `write-bundle` reflashes the lot in one command.
41
42
 
42
- > Pre-built binaries are published on the
43
- > [Releases](https://github.com/nebkat/idftool/releases) page.
44
- > See `CONTRIBUTING.md` for building from source.
43
+ ## Installation
44
+
45
+ ### pipx (recommended)
46
+
47
+ [pipx](https://pipx.pypa.io) installs idftool into an isolated environment and
48
+ puts it on your `PATH` automatically:
49
+
50
+ ```bash
51
+ pipx install idftool
52
+ ```
53
+
54
+ Install pipx first if you don't have it:
55
+
56
+ | Platform | Command |
57
+ |----------|---------|
58
+ | macOS | `brew install pipx` |
59
+ | Windows | `winget install python.pipx` |
60
+ | Linux / other | `pip install pipx` |
61
+
62
+ ### Binary
63
+
64
+ If you don't have Python, download the pre-built binary from the
65
+ [Releases](https://github.com/nebkat/idftool/releases) page. Prefer the `-dir`
66
+ archive over the single-file download — it starts instantly rather than extracting
67
+ itself on each run.
45
68
 
46
69
  ## At a glance
47
70
 
@@ -72,50 +95,41 @@ $ idftool set-boot ota_1
72
95
  Setting boot partition to 'ota_1'...
73
96
  ```
74
97
 
75
- ## Global options
76
-
77
- These flags apply to every subcommand and go **before** the command name:
78
-
79
- | Flag | Purpose |
80
- |------|---------|
81
- | `-p`, `--port PATH` | Serial port device. If omitted, idftool auto-picks one. |
82
- | `-b`, `--baud N` | Serial baud rate (defaults to esptool's ROM baud, 115200). |
83
- | `--no-reset` | Skip the hard reset that normally happens after a command. |
84
- | `--partition-table-file PATH` | Use a CSV or binary partition table from disk instead of reading it off the device. |
85
- | `--partition-table-offset OFFSET` | Where to expect the partition table in flash (default `0x8000`). |
86
- | `--partition-table-size SIZE` | Size of the partition table region (default `0x1000`). |
87
- | `--primary-bootloader-offset OFFSET` | Primary bootloader offset, or a chip name like `esp32s3` to pick a default. Only needed when addressing the `bootloader` partition by name in **offline** mode (`--partition-table-file` with no device); auto-detected from a connected chip otherwise. |
88
- | `--recovery-bootloader-offset OFFSET` | Recovery bootloader offset; same scope as `--primary-bootloader-offset`. |
89
-
90
- The commands `list`, `merge-bin`, and `create-bundle` will work **without**
91
- a device when you supply `--partition-table-file`; everything else needs a
92
- connected ESP.
93
-
94
- ## Partition addressing
95
-
96
- Wherever a command takes a `partition` argument you can pass:
97
-
98
- - A **name** from the partition table (`nvs`, `ota_0`, `storage`, …).
99
- - A **numeric address** that matches an existing partition's start
100
- offset exactly — equivalent to looking the partition up by name, just
101
- keyed on its address.
102
- - An **offset into a partition**: `name[offset]`. Negative values count
103
- from the end. Sets the starting point for the operation. Accepted by
104
- `write` and `merge-bin`.
105
- - A **slice of a partition**: `name[start:stop]`. Negative values count
106
- from the end, and a `+N` stop is a length relative to `start`.
107
- Accepted by `read`, `erase`, and `view`. Examples:
108
- - `nvs[0:0x100]` — first 256 bytes of `nvs`
109
- - `storage[-0x1000:]` — last 4 KiB of `storage`
110
- - `ota_0[0x1000:+0x800]` — 2 KiB starting 4 KiB into `ota_0`
111
-
112
- All numeric values in addresses, offsets, and sizes accept either
113
- decimal (`4096`) or hex (`0x1000`).
114
-
115
98
  ---
116
99
 
117
100
  ## Command reference
118
101
 
102
+ | Command | Description |
103
+ |---------|-------------|
104
+ | **Discovery** | |
105
+ | [`devices`](#devices) | List serial ports with hardware IDs |
106
+ | [`list`](#list) | Print the partition table |
107
+ | **Partition I/O** | |
108
+ | [`read`](#read) | Read a partition (or slice) into a file |
109
+ | [`write`](#write) | Write one or more files to named partitions |
110
+ | [`erase`](#erase) | Erase a partition (or slice) |
111
+ | [`view`](#view) | Pretty-print a partition's contents |
112
+ | **Firmware** | |
113
+ | [`ota`](#ota) | Push an app to the next OTA slot and switch to it |
114
+ | [`factory`](#factory) | Flash an app to the factory partition |
115
+ | **Boot selection** | |
116
+ | [`get-boot`](#get-boot) | Show the currently-active OTA slot |
117
+ | [`set-boot`](#set-boot) | Force the next boot to a specific OTA partition |
118
+ | [`clear-boot`](#clear-boot) | Erase otadata and let the bootloader fall back |
119
+ | **Images** | |
120
+ | [`create-image`](#create-image) | Merge partition binaries into a single flash image |
121
+ | [`dump-image`](#dump-image) | Dump the entire flash to an image file |
122
+ | [`write-image`](#write-image-alias-reflash) | Write a full flash image to the device |
123
+ | **Bundles** | |
124
+ | [`create-bundle`](#create-bundle) | Pack partition images into a ZIP bundle |
125
+ | [`dump-bundle`](#dump-bundle) | Pack every partition from the device into a ZIP |
126
+ | [`write-bundle`](#write-bundle) | Flash every binary in a bundle ZIP |
127
+ | **NVS** | |
128
+ | [`create-nvs`](#create-nvs) | Generate an NVS partition image from a CSV file |
129
+ | [`write-nvs`](#write-nvs) | Generate an NVS image from CSV and flash it |
130
+ | **Misc** | |
131
+ | [`enter-bootloader`](#enter-bootloader) | Drop the chip into ROM bootloader mode |
132
+
119
133
  ### Discovery
120
134
 
121
135
  #### `devices`
@@ -278,6 +292,37 @@ to match) instead of reading the table from flash.
278
292
  idftool write-bundle release.zip
279
293
  ```
280
294
 
295
+ ### NVS
296
+
297
+ idftool can generate a binary NVS (Non-Volatile Storage) partition image
298
+ from a CSV file, using the same format as ESP-IDF's
299
+ [`nvs_partition_gen.py`](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/storage/nvs_partition_gen.html).
300
+
301
+ Example CSV:
302
+ ```csv
303
+ key,type,encoding,value
304
+ storage,namespace,,
305
+ device_name,data,string,My Device
306
+ device_id,data,u32,12345
307
+ api_key,data,string,abc123def456
308
+ ```
309
+
310
+ #### `create-nvs`
311
+ Generate an NVS partition image from a CSV file, offline. Requires either
312
+ `--size` (explicit partition size) or `--partition` (look up the size from
313
+ the partition table — needs `--partition-table-file` or a device).
314
+ ```text
315
+ idftool create-nvs nvs.csv -o nvs.bin --size 0x6000
316
+ idftool --partition-table-file partitions.csv create-nvs nvs.csv -o nvs.bin --partition nvs
317
+ ```
318
+
319
+ #### `write-nvs`
320
+ Generate an NVS partition image from a CSV file and flash it to the named
321
+ partition on the device.
322
+ ```text
323
+ idftool write-nvs nvs nvs.csv
324
+ ```
325
+
281
326
  ### Misc
282
327
 
283
328
  #### `enter-bootloader`
@@ -291,3 +336,45 @@ isn't fully settled) are retried silently.
291
336
  idftool -p /dev/cu.usbmodem1101 enter-bootloader
292
337
  ```
293
338
  Requires `-p`/`--port`.
339
+
340
+ --
341
+
342
+ ## Global options
343
+
344
+ These flags apply to every subcommand and go **before** the command name:
345
+
346
+ | Flag | Purpose |
347
+ |------|---------|
348
+ | `-p`, `--port PATH` | Serial port device. If omitted, idftool auto-picks one. |
349
+ | `-b`, `--baud N` | Serial baud rate (defaults to esptool's ROM baud, 115200). |
350
+ | `--no-reset` | Skip the hard reset that normally happens after a command. |
351
+ | `--partition-table-file PATH` | Use a CSV or binary partition table from disk instead of reading it off the device. |
352
+ | `--partition-table-offset OFFSET` | Where to expect the partition table in flash (default `0x8000`). |
353
+ | `--partition-table-size SIZE` | Size of the partition table region (default `0x1000`). |
354
+ | `--primary-bootloader-offset OFFSET` | Primary bootloader offset, or a chip name like `esp32s3` to pick a default. Only needed when addressing the `bootloader` partition by name in **offline** mode (`--partition-table-file` with no device); auto-detected from a connected chip otherwise. |
355
+ | `--recovery-bootloader-offset OFFSET` | Recovery bootloader offset; same scope as `--primary-bootloader-offset`. |
356
+
357
+ The commands `list`, `merge-bin`, and `create-bundle` will work **without**
358
+ a device when you supply `--partition-table-file`; everything else needs a
359
+ connected ESP.
360
+
361
+ ## Partition addressing
362
+
363
+ Wherever a command takes a `partition` argument you can pass:
364
+
365
+ - A **name** from the partition table (`nvs`, `ota_0`, `storage`, …).
366
+ - A **numeric address** that matches an existing partition's start
367
+ offset exactly — equivalent to looking the partition up by name, just
368
+ keyed on its address.
369
+ - An **offset into a partition**: `name[offset]`. Negative values count
370
+ from the end. Sets the starting point for the operation. Accepted by
371
+ `write` and `merge-bin`.
372
+ - A **slice of a partition**: `name[start:stop]`. Negative values count
373
+ from the end, and a `+N` stop is a length relative to `start`.
374
+ Accepted by `read`, `erase`, and `view`. Examples:
375
+ - `nvs[0:0x100]` — first 256 bytes of `nvs`
376
+ - `storage[-0x1000:]` — last 4 KiB of `storage`
377
+ - `ota_0[0x1000:+0x800]` — 2 KiB starting 4 KiB into `ota_0`
378
+
379
+ All numeric values in addresses, offsets, and sizes accept either
380
+ decimal (`4096`) or hex (`0x1000`).
@@ -11,6 +11,7 @@ pyproject.toml
11
11
  .github/workflows/publish.yml
12
12
  idftool/__init__.py
13
13
  idftool/__main__.py
14
+ idftool/traceback.py
14
15
  idftool.egg-info/PKG-INFO
15
16
  idftool.egg-info/SOURCES.txt
16
17
  idftool.egg-info/dependency_links.txt
@@ -1,2 +1,3 @@
1
1
  esptool~=5.2.0
2
2
  esp-idf-defs~=0.1.0
3
+ esp-idf-nvs-partition-gen~=0.2.0
@@ -12,6 +12,7 @@
12
12
  dependencies = [
13
13
  "esptool~=5.2.0",
14
14
  "esp-idf-defs~=0.1.0",
15
+ "esp-idf-nvs-partition-gen~=0.2.0",
15
16
  ]
16
17
 
17
18
  [project.scripts]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes