idftool 1.2.0__tar.gz → 1.3.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.
- {idftool-1.2.0 → idftool-1.3.0}/CHANGELOG.md +11 -0
- {idftool-1.2.0/idftool.egg-info → idftool-1.3.0}/PKG-INFO +1 -1
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/provisioning.md +6 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/bundles.md +1 -1
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/nvs.md +6 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/bundle.py +4 -3
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/bundles.py +1 -1
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/nvs.py +114 -19
- {idftool-1.2.0 → idftool-1.3.0}/idftool/flash.py +43 -31
- {idftool-1.2.0 → idftool-1.3.0/idftool.egg-info}/PKG-INFO +1 -1
- idftool-1.3.0/idftool.egg-info/scm_version.json +8 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/test_flash.py +24 -10
- {idftool-1.2.0 → idftool-1.3.0}/test/test_offline.py +51 -1
- {idftool-1.2.0 → idftool-1.3.0}/test/test_virtual.py +15 -0
- idftool-1.2.0/idftool.egg-info/scm_version.json +0 -8
- {idftool-1.2.0 → idftool-1.3.0}/.github/workflows/build.yml +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/.github/workflows/ci.yml +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/.github/workflows/docs.yml +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/.github/workflows/publish.yml +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/.gitignore +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/CONTRIBUTING.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/LICENSE.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/Makefile +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/README.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/changelog.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/contributing.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/backups-and-releases.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/choosing-a-device.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/flashing-firmware.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/getting-started.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/monitoring.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/partition-addressing.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/partitions-or-files.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/plugins.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/guide/scripting.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/index.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/installation.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/library.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/discovery.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/espefuse.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/esptool.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/filesystems.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/firmware.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/global-options.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/idf-py.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/images.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/index.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/misc.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/monitor.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/partition-io.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/partition-table.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/reference/write-options.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/docs/stylesheets/extra.css +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/examples/identify-plugin/idftool_chip_description.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/examples/identify-plugin/pyproject.toml +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/__init__.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/__main__.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/apps.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/cli.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/__init__.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/apps.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/firmware.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/fs.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/images.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/misc.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/partition_io.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/commands/table.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/display.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/fs/__init__.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/fs/common.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/fs/fatfs.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/fs/littlefs.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/fs/spiffs.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/fs/spiffsgen.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/fs/wl.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/nvs/__init__.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/nvs/common.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/nvs/crypto.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/nvs/edit.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/nvs/parser.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/params.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/partitions.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/plugins.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/ports.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/state.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool/traceback.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool-onedir.spec +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool.egg-info/SOURCES.txt +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool.egg-info/dependency_links.txt +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool.egg-info/entry_points.txt +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool.egg-info/requires.txt +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool.egg-info/scm_file_list.json +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool.egg-info/top_level.txt +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/idftool.spec +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/mkdocs.yml +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/pyproject.toml +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/setup.cfg +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/README.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/conftest.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/fixtures/README.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/fixtures/esp32s3/app-v1.bin +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/fixtures/esp32s3/app-v2.bin +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/fixtures/esp32s3/bootloader.bin +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/fixtures/esp32s3/flash-image.bin +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/fixtures/esp32s3/nvs.csv +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/fixtures/esp32s3/partition-table.bin +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/fixtures/esp32s3/partitions.csv +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/.gitignore +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/CMakeLists.txt +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/README.md +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/build_fixtures.sh +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/main/CMakeLists.txt +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/main/idf_component.yml +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/main/main.c +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/nvs.csv +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/partitions.csv +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/project/sdkconfig.defaults +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/samples/bootloader-template.csv +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/samples/nvs.csv +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/samples/partitions.csv +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/test_device.py +0 -0
- {idftool-1.2.0 → idftool-1.3.0}/test/test_nvs.py +0 -0
|
@@ -4,6 +4,16 @@ All notable changes to this project are documented here. The format is
|
|
|
4
4
|
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
5
5
|
this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [v1.3.0] — 2026-10-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `set-nvs --csv` sets every key in a CSV without touching the rest of the
|
|
11
|
+
partition. Bundle `set-nvs` ops take a CSV as `file`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- The progress bar for sector-by-sector writes matches esptool's: sizes in
|
|
15
|
+
kB/MB and elapsed time.
|
|
16
|
+
|
|
7
17
|
## [v1.2.0] — 2026-10-01
|
|
8
18
|
|
|
9
19
|
### Added
|
|
@@ -400,6 +410,7 @@ this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
400
410
|
### Added
|
|
401
411
|
- Initial release.
|
|
402
412
|
|
|
413
|
+
[v1.3.0]: https://github.com/nebkat/idftool/releases/tag/v1.3.0
|
|
403
414
|
[v1.2.0]: https://github.com/nebkat/idftool/releases/tag/v1.2.0
|
|
404
415
|
[v1.1.0]: https://github.com/nebkat/idftool/releases/tag/v1.1.0
|
|
405
416
|
[v1.0.0]: https://github.com/nebkat/idftool/releases/tag/v1.0.0
|
|
@@ -40,6 +40,12 @@ Add a key that doesn't exist yet by giving its type:
|
|
|
40
40
|
idftool set-nvs nvs config:serial:string=SN-0042
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Set several keys from a CSV, keeping everything else:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
idftool set-nvs nvs --csv config.csv
|
|
47
|
+
```
|
|
48
|
+
|
|
43
49
|
### Read values back
|
|
44
50
|
|
|
45
51
|
```console
|
|
@@ -51,7 +51,7 @@ order after the files:
|
|
|
51
51
|
| `erase` | `partition` | Erase a partition. |
|
|
52
52
|
| `write-fs` | `partition`, `file` | Write a filesystem image. |
|
|
53
53
|
| `edit-fs` | `partition`, `put`, `delete` | Put files (path → file in the bundle) in the filesystem, and delete paths. |
|
|
54
|
-
| `set-nvs` | `partition`, `set`, `delete` | Set keys (`ns:key` → `type:value`, or a bare value for a key that exists) and delete them. Without `partition`, the first NVS partition. |
|
|
54
|
+
| `set-nvs` | `partition`, `file`, `set`, `delete` | Set the keys in `file` (an NVS CSV), set keys (`ns:key` → `type:value`, or a bare value for a key that exists) and delete them. Without `partition`, the first NVS partition. |
|
|
55
55
|
| `set-boot` | `partition` | Boot that OTA slot next. |
|
|
56
56
|
| `clear-boot` | | Clear otadata so the factory app boots. |
|
|
57
57
|
|
|
@@ -137,6 +137,7 @@ idftool set-nvs nvs storage:serial:string=SN-0001 -d storage:old_key
|
|
|
137
137
|
idftool set-nvs nvs -n storage :cert:blob=@device.der
|
|
138
138
|
idftool set-nvs -f nvs.bin storage:device_id=42
|
|
139
139
|
idftool set-nvs nvs storage:device_id=42 --dry-run
|
|
140
|
+
idftool set-nvs nvs --csv keys.csv
|
|
140
141
|
```
|
|
141
142
|
|
|
142
143
|
`namespace:key=value`
|
|
@@ -157,6 +158,11 @@ idftool set-nvs nvs storage:device_id=42 --dry-run
|
|
|
157
158
|
`-d namespace:key`
|
|
158
159
|
: Delete a key.
|
|
159
160
|
|
|
161
|
+
`-c FILE`, `--csv FILE`
|
|
162
|
+
: Set every key in a CSV, in the format `create-nvs` takes and `read-nvs`
|
|
163
|
+
writes. Keys not in the CSV are left alone. Repeatable; CSV rows apply
|
|
164
|
+
first, then specs, then deletes.
|
|
165
|
+
|
|
160
166
|
Changes are **appended** the way firmware writes them: a new entry goes into
|
|
161
167
|
free space and the old one is marked erased. Everything else stays
|
|
162
168
|
byte-for-byte identical, and on a device only the 4 KiB pages that changed are
|
|
@@ -116,8 +116,8 @@ class Op:
|
|
|
116
116
|
f"\"delete\" a list")
|
|
117
117
|
if op == 'edit-fs' and not (put or delete):
|
|
118
118
|
raise BundleError(f"{where} (edit-fs): needs \"put\" and/or \"delete\"")
|
|
119
|
-
if op == 'set-nvs' and not (set_ or delete):
|
|
120
|
-
raise BundleError(f"{where} (set-nvs): needs \"set\" and/or \"delete\"")
|
|
119
|
+
if op == 'set-nvs' and not (set_ or delete or data.get('file')):
|
|
120
|
+
raise BundleError(f"{where} (set-nvs): needs \"file\", \"set\" and/or \"delete\"")
|
|
121
121
|
return cls(index, op, data.get('partition'), data.get('file'),
|
|
122
122
|
{str(k): str(v) for k, v in put.items()}, [str(d) for d in delete],
|
|
123
123
|
dict(set_))
|
|
@@ -138,7 +138,8 @@ class Op:
|
|
|
138
138
|
f"delete {', '.join(self.delete)}" if self.delete else '']
|
|
139
139
|
return f"Update files in '{self.partition}': {'; '.join(w for w in what if w)}"
|
|
140
140
|
if self.op == 'set-nvs':
|
|
141
|
-
what = [f"
|
|
141
|
+
what = ([f"keys from {self.file}"] if self.file else []) + \
|
|
142
|
+
[f"{k} = {v}" for k, v in self.set.items()] + [f"delete {d}" for d in self.delete]
|
|
142
143
|
where = f" ('{self.partition}')" if self.partition else ''
|
|
143
144
|
return f"Update NVS{where}: {', '.join(what)}"
|
|
144
145
|
if self.op == 'set-boot':
|
|
@@ -235,7 +235,7 @@ def write_bundle(state, input_file, hmac_key=None, **options):
|
|
|
235
235
|
f"'{op.partition}' is not an OTA app partition")
|
|
236
236
|
elif op.op == 'set-nvs':
|
|
237
237
|
try:
|
|
238
|
-
nvs_edits[op.index] = manifest_edits(op.set, op.delete, read_file)
|
|
238
|
+
nvs_edits[op.index] = manifest_edits(op.set, op.delete, read_file, op.file)
|
|
239
239
|
except (click.UsageError, RuntimeError) as e:
|
|
240
240
|
message = e.format_message() if isinstance(e, click.UsageError) else str(e)
|
|
241
241
|
raise BundleError(f"manifest.json: op {op.index} (set-nvs): {message}") from e
|
|
@@ -20,8 +20,8 @@ from idftool.partitions import get_partition
|
|
|
20
20
|
|
|
21
21
|
# Keep the pass-through write options in a panel of their own.
|
|
22
22
|
option_group('write-nvs', '--hmac-key')
|
|
23
|
-
option_group('set-nvs', '--file', '--
|
|
24
|
-
'--dry-run')
|
|
23
|
+
option_group('set-nvs', '--file', '--csv', '--delete', '--namespace', '--output', '--rewrite',
|
|
24
|
+
'--hmac-key', '--dry-run')
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def hmac_key_option(f):
|
|
@@ -338,25 +338,114 @@ def _parse_delete(spec, default_namespace):
|
|
|
338
338
|
return Edit(namespace, key)
|
|
339
339
|
|
|
340
340
|
|
|
341
|
+
def _fill_blob(encoding, value, key):
|
|
342
|
+
"""The bytes of a ``blob_fill(N;0xPP)`` or ``blob_sz_fill(N;0xPP)`` value."""
|
|
343
|
+
import re
|
|
344
|
+
import struct
|
|
345
|
+
|
|
346
|
+
match = re.fullmatch(r'(blob_fill|blob_sz_fill)\((\d+);(0x[0-9a-fA-F]{2})\)', encoding)
|
|
347
|
+
if not match:
|
|
348
|
+
raise click.UsageError(f"'{key}': invalid encoding '{encoding}'")
|
|
349
|
+
kind, length, pad = match.group(1), int(match.group(2)), int(match.group(3), 16)
|
|
350
|
+
data = value.encode('utf-8')
|
|
351
|
+
if len(data) > length:
|
|
352
|
+
raise click.UsageError(f"'{key}': value is longer than {length} bytes")
|
|
353
|
+
prefix = struct.pack('<I', len(data)) if kind == 'blob_sz_fill' else b''
|
|
354
|
+
return prefix + data + bytes([pad]) * (length - len(data))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _csv_value(key, row_type, encoding, value, read_file):
|
|
358
|
+
"""``(type, value)`` of one ``nvs_partition_gen`` CSV row."""
|
|
359
|
+
import base64
|
|
360
|
+
import binascii
|
|
361
|
+
from idftool.nvs import PRIMITIVES
|
|
362
|
+
|
|
363
|
+
if row_type == 'file':
|
|
364
|
+
raw = read_file(value)
|
|
365
|
+
if encoding == 'binary':
|
|
366
|
+
return 'blob', raw
|
|
367
|
+
value = raw.decode('utf-8')
|
|
368
|
+
if encoding == 'string':
|
|
369
|
+
return 'string', value
|
|
370
|
+
elif row_type != 'data':
|
|
371
|
+
raise click.UsageError(f"'{key}': unknown type '{row_type}' (expected namespace, data "
|
|
372
|
+
f"or file)")
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
if encoding in PRIMITIVES:
|
|
376
|
+
return encoding, int(value.strip(), 0)
|
|
377
|
+
if encoding == 'string':
|
|
378
|
+
return 'string', value
|
|
379
|
+
if encoding == 'hex2bin':
|
|
380
|
+
return 'blob', bytes.fromhex(value.strip())
|
|
381
|
+
if encoding == 'base64':
|
|
382
|
+
return 'blob', base64.b64decode(value)
|
|
383
|
+
if encoding == 'binary':
|
|
384
|
+
return 'blob', value.encode('utf-8')
|
|
385
|
+
except (ValueError, binascii.Error) as e:
|
|
386
|
+
raise click.UsageError(f"'{key}': '{value}' is not valid {encoding}: {e}")
|
|
387
|
+
if encoding.startswith(('blob_fill', 'blob_sz_fill')):
|
|
388
|
+
return 'blob', _fill_blob(encoding, value, key)
|
|
389
|
+
raise click.UsageError(f"'{key}': unknown encoding '{encoding}'")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def csv_edits(text, source, read_file=_read_host_file):
|
|
393
|
+
"""Edits setting every key in an ``nvs_partition_gen`` CSV, the format ``create-nvs``
|
|
394
|
+
takes and ``read-nvs`` writes. ``file`` rows are read through `read_file`."""
|
|
395
|
+
import csv
|
|
396
|
+
from idftool.nvs.edit import Edit
|
|
397
|
+
|
|
398
|
+
rows = csv.DictReader(line for line in text.splitlines() if not line.startswith('#'))
|
|
399
|
+
if not rows.fieldnames or not {'key', 'type', 'encoding', 'value'} <= set(rows.fieldnames):
|
|
400
|
+
raise click.UsageError(f"{source} needs a 'key,type,encoding,value' header")
|
|
401
|
+
edits, namespace = [], None
|
|
402
|
+
for row in rows:
|
|
403
|
+
key, row_type = row['key'], (row['type'] or '').strip()
|
|
404
|
+
if row_type == 'namespace':
|
|
405
|
+
namespace = key
|
|
406
|
+
continue
|
|
407
|
+
if namespace is None:
|
|
408
|
+
raise click.UsageError(f"{source}: '{key}' comes before any namespace row")
|
|
409
|
+
try:
|
|
410
|
+
type_name, value = _csv_value(key, row_type, (row['encoding'] or '').strip(),
|
|
411
|
+
row['value'] or '', read_file)
|
|
412
|
+
except click.UsageError as e:
|
|
413
|
+
raise click.UsageError(f"{source}: {e.format_message()}")
|
|
414
|
+
edits.append(Edit(namespace, key, type_name, value))
|
|
415
|
+
return edits
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _read_csv(path):
|
|
419
|
+
try:
|
|
420
|
+
text = open(path, encoding='utf-8').read()
|
|
421
|
+
except OSError as e:
|
|
422
|
+
raise click.UsageError(f"Cannot read CSV file '{path}': {e}")
|
|
423
|
+
return csv_edits(text, f"'{path}'")
|
|
424
|
+
|
|
425
|
+
|
|
341
426
|
def _resolve_untyped(image, edits, read_file=_read_host_file):
|
|
342
|
-
"""Fill in the type of any ``--set`` that didn't give one, from the entry it replaces
|
|
427
|
+
"""Fill in the type of any ``--set`` that didn't give one, from the entry it replaces —
|
|
428
|
+
an earlier edit of the same key, or else the image's."""
|
|
343
429
|
from idftool.nvs.edit import Edit
|
|
344
430
|
|
|
345
|
-
resolved = []
|
|
431
|
+
resolved, types = [], {}
|
|
346
432
|
for edit in edits:
|
|
347
433
|
if edit.is_delete or edit.type is not None:
|
|
348
434
|
resolved.append(edit)
|
|
435
|
+
types[edit.qualified] = edit.type
|
|
349
436
|
continue
|
|
350
437
|
existing = image.get(edit.namespace, edit.key)
|
|
351
|
-
|
|
438
|
+
type_name = types.get(edit.qualified, existing and existing.type)
|
|
439
|
+
if type_name is None:
|
|
352
440
|
from idftool.nvs import PRIMITIVES, NvsError
|
|
353
441
|
known = ', '.join(list(PRIMITIVES) + ['string', 'blob'])
|
|
354
442
|
raise NvsError(
|
|
355
443
|
f"'{edit.namespace}:{edit.key}' is not in the image, so its type cannot be "
|
|
356
444
|
f"inferred — write it as {edit.namespace}:{edit.key}:<type>={edit.value} "
|
|
357
445
|
f"(types: {known})")
|
|
358
|
-
|
|
359
|
-
|
|
446
|
+
types[edit.qualified] = type_name
|
|
447
|
+
resolved.append(Edit(edit.namespace, edit.key, type_name,
|
|
448
|
+
_parse_value(type_name, edit.value, read_file)))
|
|
360
449
|
return resolved
|
|
361
450
|
|
|
362
451
|
|
|
@@ -403,13 +492,14 @@ def _write_pages(esp, part, result, dirty, options, hmac_key=None):
|
|
|
403
492
|
**write_flash_options(options, skip_flashed=True, diff=True))
|
|
404
493
|
|
|
405
494
|
|
|
406
|
-
def manifest_edits(set_values, deletes, read_file):
|
|
407
|
-
"""Edits from a bundle manifest's ``set-nvs`` op: ``ns:key`` →
|
|
408
|
-
value for a key that exists),
|
|
495
|
+
def manifest_edits(set_values, deletes, read_file, csv_file=None):
|
|
496
|
+
"""Edits from a bundle manifest's ``set-nvs`` op: the keys of `csv_file`, then ``ns:key`` →
|
|
497
|
+
``type:value`` (or a bare value for a key that exists), then ``ns:key`` deletes."""
|
|
409
498
|
from idftool.nvs import PRIMITIVES
|
|
410
499
|
|
|
411
500
|
known = list(PRIMITIVES) + ['string', 'blob']
|
|
412
|
-
edits =
|
|
501
|
+
edits = (csv_edits(read_file(csv_file).decode('utf-8'), f"'{csv_file}'", read_file)
|
|
502
|
+
if csv_file else [])
|
|
413
503
|
for qualified, value in set_values.items():
|
|
414
504
|
value = str(value)
|
|
415
505
|
prefix, colon, rest = value.partition(':')
|
|
@@ -462,22 +552,23 @@ def _split_target(args, image_file, what):
|
|
|
462
552
|
|
|
463
553
|
|
|
464
554
|
def set_nvs(state, args, image_file, deletes, namespace, output_file, do_rewrite, dry_run,
|
|
465
|
-
hmac_key=None, **options):
|
|
555
|
+
hmac_key=None, csv_files=(), **options):
|
|
466
556
|
"""Set or delete keys in an NVS partition or image. Keyword arguments go to esptool's
|
|
467
557
|
``write_flash`` (see :data:`idftool.flash.WRITE_FLASH_OPTIONS`)."""
|
|
468
558
|
import idftool.nvs as nvs
|
|
469
559
|
from idftool.nvs.edit import apply
|
|
470
560
|
|
|
471
561
|
partition, specs = _split_target(args, image_file, "SPEC")
|
|
472
|
-
if not specs and not deletes:
|
|
562
|
+
if not specs and not deletes and not csv_files:
|
|
473
563
|
if partition and '=' in partition:
|
|
474
564
|
# The one positional given is a spec, so what's missing is the target it applies to.
|
|
475
565
|
raise click.UsageError(
|
|
476
566
|
f"'{partition}' looks like a SPEC, not a partition — name the partition first "
|
|
477
567
|
f"(idftool set-nvs nvs {partition}) or use -f/--file")
|
|
478
|
-
raise click.UsageError(f"Nothing to do — pass at least one SPEC or --delete.\n{SPEC_HELP}")
|
|
568
|
+
raise click.UsageError(f"Nothing to do — pass at least one SPEC, --csv or --delete.\n{SPEC_HELP}")
|
|
479
569
|
|
|
480
|
-
edits = ([
|
|
570
|
+
edits = ([edit for path in csv_files for edit in _read_csv(path)] +
|
|
571
|
+
[_parse_set(spec, namespace) for spec in specs] +
|
|
481
572
|
[_parse_delete(spec, namespace) for spec in deletes])
|
|
482
573
|
|
|
483
574
|
data, part, source = _load_image(state, partition, image_file, 'set-nvs', hmac_key)
|
|
@@ -518,6 +609,8 @@ def set_nvs(state, args, image_file, deletes, namespace, output_file, do_rewrite
|
|
|
518
609
|
@click.argument('args', nargs=-1, metavar='[PARTITION] SPEC...')
|
|
519
610
|
@click.option('-f', '--file', 'image_file', default=None,
|
|
520
611
|
help='Edit this NVS image file instead of a partition on the device')
|
|
612
|
+
@click.option('-c', '--csv', 'csv_files', multiple=True, metavar='CSV',
|
|
613
|
+
help='Set every key in this CSV (the create-nvs format); repeatable')
|
|
521
614
|
@click.option('-d', '--delete', 'deletes', multiple=True, metavar='KEY',
|
|
522
615
|
help='Delete a key: namespace:key')
|
|
523
616
|
@click.option('-n', '--namespace', default=None,
|
|
@@ -530,20 +623,22 @@ def set_nvs(state, args, image_file, deletes, namespace, output_file, do_rewrite
|
|
|
530
623
|
@hmac_key_option
|
|
531
624
|
@flash_options
|
|
532
625
|
@pass_state
|
|
533
|
-
def cmd_set_nvs(state, args, image_file, deletes, namespace, output_file, do_rewrite,
|
|
534
|
-
hmac_key, **options):
|
|
626
|
+
def cmd_set_nvs(state, args, image_file, csv_files, deletes, namespace, output_file, do_rewrite,
|
|
627
|
+
dry_run, hmac_key, **options):
|
|
535
628
|
"""Set or delete keys in an NVS partition on the device, or in an image file with --file.
|
|
536
629
|
|
|
537
630
|
A SPEC is `namespace:key=value`, or `namespace:key:type=value` to give the type of a key
|
|
538
631
|
that isn't there yet — for one that is, the type is taken from the entry being replaced.
|
|
539
|
-
A value of `@FILE` is read from that file.
|
|
632
|
+
A value of `@FILE` is read from that file. `--csv` sets every key in a CSV file of the
|
|
633
|
+
kind `create-nvs` takes and `read-nvs` writes, leaving the rest of the partition alone;
|
|
634
|
+
CSV rows apply first, then SPECs, then deletes.
|
|
540
635
|
|
|
541
636
|
Changes are appended the way the firmware would write them, so everything else in the
|
|
542
637
|
partition is left byte-for-byte alone and a device write only touches the pages that
|
|
543
638
|
changed. If there is no room left to append, the image is compacted instead.
|
|
544
639
|
"""
|
|
545
640
|
return set_nvs(state, args, image_file, deletes, namespace, output_file, do_rewrite, dry_run,
|
|
546
|
-
hmac_key, **options)
|
|
641
|
+
hmac_key, csv_files, **options)
|
|
547
642
|
|
|
548
643
|
|
|
549
644
|
def _parse_get(spec, default_namespace):
|
|
@@ -313,30 +313,51 @@ def plan_sector_writes(esp, address, data, on_scanned=None, sector_size=DIFF_SEC
|
|
|
313
313
|
|
|
314
314
|
|
|
315
315
|
class Progress:
|
|
316
|
-
"""One progress bar for a whole operation
|
|
316
|
+
"""One progress bar for a whole operation: esptool's own (``log.progress``).
|
|
317
317
|
|
|
318
318
|
The differential path scans and writes in the same pass, so its progress is two numbers
|
|
319
|
-
that advance independently — bytes compared, and bytes written.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
has to reach to be worth anything. It also means the bar looks like every other bar the
|
|
326
|
-
user sees from a flashing tool.
|
|
319
|
+
that advance independently — bytes compared, and bytes written. The bar tracks the scan
|
|
320
|
+
(whose total is known from the start); what has been written goes in its description.
|
|
321
|
+
|
|
322
|
+
Going through esptool's logger means the bar looks like every other bar esptool draws —
|
|
323
|
+
same layout, sizes and elapsed time — and obeys its decisions on ANSI, Unicode and
|
|
324
|
+
verbosity.
|
|
327
325
|
"""
|
|
328
326
|
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
class _StoppedShort(Exception):
|
|
328
|
+
"""Thrown into ``log.progress`` so a scan that stopped short isn't drawn as done."""
|
|
329
|
+
|
|
330
|
+
def __init__(self, description, total, enabled=True):
|
|
331
|
+
self.description, self.total, self.enabled = description, total, enabled and total > 0
|
|
331
332
|
self.scanned = self.written = 0
|
|
332
333
|
self._shown = None
|
|
334
|
+
self._drawn = 0
|
|
335
|
+
self._context = self._task = None
|
|
336
|
+
|
|
337
|
+
def __enter__(self):
|
|
338
|
+
self._context = log.progress(total=self.total, description=self.description,
|
|
339
|
+
unit='B', disable=not self.enabled)
|
|
340
|
+
self._task = self._context.__enter__()
|
|
341
|
+
return self
|
|
342
|
+
|
|
343
|
+
def __exit__(self, *exc):
|
|
344
|
+
if self.scanned < self.total:
|
|
345
|
+
# An abandoned or failed scan never reaches 100%: end the line where it stopped.
|
|
346
|
+
if self._shown is not None:
|
|
347
|
+
log.print("")
|
|
348
|
+
if exc[0] is None:
|
|
349
|
+
exc = (self._StoppedShort, self._StoppedShort(), None)
|
|
350
|
+
self._context.__exit__(*exc)
|
|
333
351
|
|
|
334
352
|
@staticmethod
|
|
335
353
|
def size(count):
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
354
|
+
"""A byte count as esptool's bar shows it, e.g. ``1.20MB``."""
|
|
355
|
+
if count < 1024:
|
|
356
|
+
return f'{count}B'
|
|
357
|
+
for unit in ('kB', 'MB'):
|
|
358
|
+
count /= 1024
|
|
359
|
+
if count < 1024 or unit == 'MB':
|
|
360
|
+
return f'{count:.2f}{unit}'
|
|
340
361
|
|
|
341
362
|
@staticmethod
|
|
342
363
|
def rate(count, seconds):
|
|
@@ -349,26 +370,20 @@ class Progress:
|
|
|
349
370
|
256 lines of scroll on one that cannot overwrite.
|
|
350
371
|
"""
|
|
351
372
|
if scanned is not None:
|
|
352
|
-
self.scanned = scanned
|
|
373
|
+
self.scanned = min(scanned, self.total)
|
|
353
374
|
if written is not None:
|
|
354
375
|
self.written = written
|
|
355
376
|
if not self.enabled:
|
|
356
377
|
return
|
|
357
|
-
percent = 100 *
|
|
378
|
+
percent = 100 * self.scanned // self.total
|
|
358
379
|
if percent == self._shown:
|
|
359
380
|
return
|
|
360
381
|
self._shown = percent
|
|
361
|
-
|
|
382
|
+
description = self.description
|
|
362
383
|
if self.written:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def finish(self):
|
|
368
|
-
"""End the bar's line if it stopped short — an abandoned scan never reaches 100%."""
|
|
369
|
-
if self._shown is not None and self._shown != 100:
|
|
370
|
-
log.print("")
|
|
371
|
-
self._shown = None
|
|
384
|
+
description += f' (written {self.size(self.written)})'
|
|
385
|
+
self._task.update(advance=self.scanned - self._drawn, description=description)
|
|
386
|
+
self._drawn = self.scanned
|
|
372
387
|
|
|
373
388
|
|
|
374
389
|
def _say(prefix, args, file=None):
|
|
@@ -490,12 +505,11 @@ def _write_changed_sectors(esp, address, prepared, described, kwargs):
|
|
|
490
505
|
fall back to comparing the region as a whole.
|
|
491
506
|
"""
|
|
492
507
|
print(f"Comparing {described} against flash at {address:#010x}...")
|
|
493
|
-
progress = Progress(described, len(prepared), enabled=not kwargs.get('no_progress'))
|
|
494
508
|
written = runs = 0
|
|
495
509
|
elapsed = 0.0
|
|
496
510
|
abandoned = False
|
|
497
511
|
|
|
498
|
-
|
|
512
|
+
with Progress(described, len(prepared), enabled=not kwargs.get('no_progress')) as progress:
|
|
499
513
|
for offset, length, reason in plan_sector_writes(
|
|
500
514
|
esp, address, prepared, on_scanned=lambda n: progress.update(scanned=n)):
|
|
501
515
|
if reason == 'remainder':
|
|
@@ -514,8 +528,6 @@ def _write_changed_sectors(esp, address, prepared, described, kwargs):
|
|
|
514
528
|
written += length
|
|
515
529
|
runs += 1
|
|
516
530
|
progress.update(written=written)
|
|
517
|
-
finally:
|
|
518
|
-
progress.finish()
|
|
519
531
|
|
|
520
532
|
# esptool reports what it wrote and how fast after every write; a differential one is
|
|
521
533
|
# made of many, so the same figures are totalled up and reported once here instead.
|
|
@@ -487,7 +487,7 @@ def test_changed_sectors_are_written_as_they_are_found(monkeypatch, capsys):
|
|
|
487
487
|
[(0x10000 + 5 * 0x1000, data[5 * 0x1000:7 * 0x1000])],
|
|
488
488
|
[(0x10000 + 40 * 0x1000, data[40 * 0x1000:41 * 0x1000])],
|
|
489
489
|
]
|
|
490
|
-
assert "wrote
|
|
490
|
+
assert "wrote 12.00kB of 256.00kB in 2 regions" in capsys.readouterr().out
|
|
491
491
|
|
|
492
492
|
|
|
493
493
|
def test_an_abandoned_scan_writes_the_region_whole(monkeypatch, capsys):
|
|
@@ -562,9 +562,8 @@ def test_app_commands_diff_by_default():
|
|
|
562
562
|
def test_progress_is_silent_when_progress_is_not_wanted(capsys):
|
|
563
563
|
from idftool.flash import Progress
|
|
564
564
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
bar.finish()
|
|
565
|
+
with Progress('x', 0x1000, enabled=False) as bar:
|
|
566
|
+
bar.update(scanned=0x800)
|
|
568
567
|
assert capsys.readouterr().out == ""
|
|
569
568
|
|
|
570
569
|
|
|
@@ -575,7 +574,8 @@ def test_progress_obeys_esptools_verbosity(capsys):
|
|
|
575
574
|
|
|
576
575
|
log.set_verbosity('silent')
|
|
577
576
|
try:
|
|
578
|
-
Progress('x', 0x1000)
|
|
577
|
+
with Progress('x', 0x1000) as bar:
|
|
578
|
+
bar.update(scanned=0x800)
|
|
579
579
|
assert capsys.readouterr().out == ""
|
|
580
580
|
finally:
|
|
581
581
|
log.set_verbosity('auto')
|
|
@@ -586,17 +586,31 @@ def test_progress_redraws_only_when_the_percentage_moves(capsys):
|
|
|
586
586
|
and 256 lines of scroll on one that cannot."""
|
|
587
587
|
from idftool.flash import Progress
|
|
588
588
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
589
|
+
with Progress('x', 1000 * 0x1000) as bar:
|
|
590
|
+
for sector in range(1, 21): # 20 updates spanning 0% to 2% of the region
|
|
591
|
+
bar.update(scanned=sector * 0x1000)
|
|
592
592
|
assert capsys.readouterr().out.count('%') == 3 # the first, then 1% and 2%
|
|
593
593
|
|
|
594
594
|
|
|
595
|
-
def
|
|
595
|
+
def test_progress_sizes_match_esptools_bar():
|
|
596
596
|
from idftool.flash import Progress
|
|
597
597
|
|
|
598
598
|
assert [Progress.size(n) for n in (512, 0x1000, 0x40000, 0x100000, 0x180000)] == \
|
|
599
|
-
["512B", "
|
|
599
|
+
["512B", "4.00kB", "256.00kB", "1.00MB", "1.50MB"]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def test_progress_uses_esptools_layout_and_stops_short_of_done(capsys):
|
|
603
|
+
"""An abandoned scan must not be drawn as complete on the way out."""
|
|
604
|
+
from idftool.flash import Progress
|
|
605
|
+
|
|
606
|
+
with Progress('x', 4 * 0x1000) as bar:
|
|
607
|
+
bar.update(scanned=0x1000)
|
|
608
|
+
bar.update(written=0x1000)
|
|
609
|
+
bar.update(scanned=0x2000)
|
|
610
|
+
out = capsys.readouterr().out
|
|
611
|
+
assert "x (written 4.00kB)" in out
|
|
612
|
+
assert "8.00kB/16.00kB [" in out
|
|
613
|
+
assert "100.0%" not in out
|
|
600
614
|
|
|
601
615
|
|
|
602
616
|
def test_quiet_esptool_keeps_what_matters_and_restores_itself(capsys):
|
|
@@ -224,7 +224,57 @@ def test_set_nvs_needs_something_to_do(run_offline, tmp_path):
|
|
|
224
224
|
image = tmp_path / "nvs.bin"
|
|
225
225
|
run_offline(f"create-nvs {SAMPLES / 'nvs.csv'} -o {image} --size 0x6000")
|
|
226
226
|
out = run_offline(f"set-nvs -f {image}", expect_error=True)
|
|
227
|
-
assert "at least one SPEC or --delete" in out
|
|
227
|
+
assert "at least one SPEC, --csv or --delete" in out
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_set_nvs_csv_merges(run_offline, tmp_path):
|
|
231
|
+
image = tmp_path / "nvs.bin"
|
|
232
|
+
run_offline(f"create-nvs {SAMPLES / 'nvs.csv'} -o {image} --size 0x6000")
|
|
233
|
+
(tmp_path / "cert.der").write_bytes(b"\x01\x02\x03")
|
|
234
|
+
keys = tmp_path / "keys.csv"
|
|
235
|
+
keys.write_text(
|
|
236
|
+
"key,type,encoding,value\n"
|
|
237
|
+
"# comments are skipped\n"
|
|
238
|
+
"storage,namespace,,\n"
|
|
239
|
+
"counter,data,u16,0x2a\n"
|
|
240
|
+
"serial,data,string,SN-42\n"
|
|
241
|
+
"token,data,base64,3q2+7w==\n"
|
|
242
|
+
"pad,data,blob_sz_fill(4;0x00),ab\n"
|
|
243
|
+
f"cert,file,binary,{tmp_path / 'cert.der'}\n"
|
|
244
|
+
"other,namespace,,\n"
|
|
245
|
+
"flag,data,u8,1\n")
|
|
246
|
+
# SPECs apply after the CSV, so they win.
|
|
247
|
+
run_offline(["set-nvs", "-f", str(image), "--csv", str(keys), "storage:serial=SN-43"])
|
|
248
|
+
get = lambda *keys: run_offline(["get-nvs", "-f", str(image), *keys]).splitlines()
|
|
249
|
+
assert get("storage:counter", "storage:serial", "storage:token", "storage:pad",
|
|
250
|
+
"storage:cert", "other:flag") == ["42", "SN-43", "deadbeef", "0200000061620000",
|
|
251
|
+
"010203", "1"]
|
|
252
|
+
# Keys not in the CSV stay.
|
|
253
|
+
assert get("storage:device_name") == ["idftool-test"]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_set_nvs_csv_round_trips_extract(run_offline, tmp_path):
|
|
257
|
+
image, other = tmp_path / "nvs.bin", tmp_path / "other.bin"
|
|
258
|
+
run_offline(f"create-nvs {SAMPLES / 'nvs.csv'} -o {image} --size 0x6000")
|
|
259
|
+
run_offline(f"set-nvs -f {image} storage:blob:blob=00ff storage:counter=9")
|
|
260
|
+
run_offline(f"extract-nvs -f {image} {tmp_path / 'back.csv'}")
|
|
261
|
+
(tmp_path / "empty.csv").write_text("key,type,encoding,value\nx,namespace,,\nk,data,u8,0\n")
|
|
262
|
+
run_offline(f"create-nvs {tmp_path / 'empty.csv'} -o {other} --size 0x6000")
|
|
263
|
+
out = run_offline(f"set-nvs -f {other} --csv {tmp_path / 'back.csv'}")
|
|
264
|
+
assert "+ storage:blob (blob) = 00ff" in out
|
|
265
|
+
assert run_offline(f"get-nvs -f {other} storage:counter x:k").splitlines() == ["9", "0"]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_set_nvs_csv_errors_name_the_row(run_offline, tmp_path):
|
|
269
|
+
image = tmp_path / "nvs.bin"
|
|
270
|
+
run_offline(f"create-nvs {SAMPLES / 'nvs.csv'} -o {image} --size 0x6000")
|
|
271
|
+
bad = tmp_path / "bad.csv"
|
|
272
|
+
bad.write_text("key,type,encoding,value\nstorage,namespace,,\ncounter,data,u16,lots\n")
|
|
273
|
+
assert "'counter': 'lots' is not valid u16" in run_offline(
|
|
274
|
+
f"set-nvs -f {image} --csv {bad}", expect_error=True)
|
|
275
|
+
bad.write_text("key,type,encoding,value\ncounter,data,u16,1\n")
|
|
276
|
+
assert "before any namespace" in run_offline(f"set-nvs -f {image} --csv {bad}",
|
|
277
|
+
expect_error=True)
|
|
228
278
|
|
|
229
279
|
|
|
230
280
|
def test_set_nvs_value_from_a_file(run_offline, tmp_path):
|
|
@@ -500,6 +500,21 @@ def test_print_bundle_without_a_table(run_offline, tmp_path):
|
|
|
500
500
|
assert "2.0.0" in out
|
|
501
501
|
|
|
502
502
|
|
|
503
|
+
def test_bundle_sets_nvs_from_a_csv(run, tmp_path, esp):
|
|
504
|
+
(tmp_path / "example.csv").write_text(
|
|
505
|
+
"key,type,encoding,value\nconfig,namespace,,\nserial,data,string,SN-0042\n"
|
|
506
|
+
"keep,data,u8,1\n")
|
|
507
|
+
run("write-nvs", "nvs", "example.csv")
|
|
508
|
+
csv = ("key,type,encoding,value\nconfig,namespace,,\nserial,data,string,SN-0050\n"
|
|
509
|
+
"cert,file,binary,nvs/cert.der\n")
|
|
510
|
+
bundle("b.zip", {"nvs/keys.csv": csv, "nvs/cert.der": b"\xca\xfe",
|
|
511
|
+
"manifest.json": {"ops": [{"op": "set-nvs", "file": "nvs/keys.csv"}]}})
|
|
512
|
+
assert "keys from nvs/keys.csv" in run("print-bundle", "-f", "b.zip")
|
|
513
|
+
run("write-bundle", "b.zip")
|
|
514
|
+
assert run("get-nvs", "nvs", "config:serial", "config:keep", "config:cert").splitlines()[:3] \
|
|
515
|
+
== ["SN-0050", "1", "cafe"]
|
|
516
|
+
|
|
517
|
+
|
|
503
518
|
def test_nvs_on_the_device(run, tmp_path, esp):
|
|
504
519
|
(tmp_path / "example.csv").write_text(
|
|
505
520
|
"key,type,encoding,value\nconfig,namespace,,\nserial,data,string,SN-0042\n")
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|