oep-client-python 0.0.34__py3-none-any.whl → 0.0.37__py3-none-any.whl

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 (61) hide show
  1. oep_client/__init__.py +1 -1
  2. oep_client/__main__.py +171 -205
  3. oep_client/capture.py +423 -56
  4. oep_client/capture_blocks.py +294 -0
  5. oep_client/conformance.py +1 -1
  6. oep_client/conformance_capture_group.py +1 -1
  7. oep_client/conformance_capture_metadata.py +7 -10
  8. oep_client/conformance_host_request_sample.py +0 -2
  9. oep_client/conformance_host_requests.py +3 -9
  10. oep_client/conformance_host_wait.py +7 -7
  11. oep_client/conformance_host_wait_sample.py +6 -6
  12. oep_client/conformance_registered_ops.py +6 -7
  13. oep_client/conformance_selected_metadata.py +3 -9
  14. oep_client/core.py +24 -18
  15. oep_client/discovery.py +1 -1
  16. oep_client/dump.py +7 -3
  17. oep_client/endpoint.py +403 -336
  18. oep_client/frames.py +3 -2
  19. oep_client/hid_stream.py +10 -3
  20. oep_client/host.py +8 -2
  21. oep_client/interfaces.py +3 -7
  22. oep_client/link.py +28 -7
  23. oep_client/message.py +23 -2
  24. oep_client/pins.py +23 -21
  25. oep_client/registry.py +29 -33
  26. oep_client/riscv.py +8 -1
  27. oep_client/settings.py +570 -0
  28. oep_client/usb_stream.py +294 -18
  29. oep_client/virtual_analog_model.py +2 -2
  30. oep_client/virtual_bench.py +157 -68
  31. oep_client/virtual_bench_capture.py +417 -44
  32. oep_client/virtual_bench_serve.py +14 -4
  33. oep_client/virtual_bench_trigger.py +277 -0
  34. oep_client/virtual_capture_group_model.py +1 -1
  35. oep_client/virtual_capture_metadata_model.py +3 -3
  36. oep_client/virtual_capture_timeline_model.py +1 -1
  37. oep_client/virtual_registered_ops_model.py +1 -1
  38. oep_client/virtual_selected_metadata_model.py +4 -4
  39. {oep_client_python-0.0.34.dist-info → oep_client_python-0.0.37.dist-info}/METADATA +18 -15
  40. {oep_client_python-0.0.34.dist-info → oep_client_python-0.0.37.dist-info}/RECORD +43 -58
  41. oep_client/config.py +0 -675
  42. oep_client/conformance_config.py +0 -139
  43. oep_client/conformance_config_execution.py +0 -84
  44. oep_client/conformance_config_execution_sample.py +0 -13
  45. oep_client/conformance_config_fallback.py +0 -72
  46. oep_client/conformance_config_fallback_sample.py +0 -25
  47. oep_client/conformance_config_paging.py +0 -87
  48. oep_client/conformance_config_paging_sample.py +0 -29
  49. oep_client/conformance_config_power_loss.py +0 -89
  50. oep_client/conformance_config_power_loss_sample.py +0 -15
  51. oep_client/conformance_config_sample.py +0 -13
  52. oep_client/conformance_config_storage.py +0 -79
  53. oep_client/conformance_config_storage_sample.py +0 -15
  54. oep_client/virtual_config_execution_model.py +0 -68
  55. oep_client/virtual_config_model.py +0 -176
  56. oep_client/virtual_config_paging_model.py +0 -12
  57. oep_client/virtual_config_power_loss_model.py +0 -77
  58. oep_client/virtual_config_storage_model.py +0 -20
  59. {oep_client_python-0.0.34.dist-info → oep_client_python-0.0.37.dist-info}/WHEEL +0 -0
  60. {oep_client_python-0.0.34.dist-info → oep_client_python-0.0.37.dist-info}/entry_points.txt +0 -0
  61. {oep_client_python-0.0.34.dist-info → oep_client_python-0.0.37.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.34"
5
+ __version__ = "0.0.37"
oep_client/__main__.py CHANGED
@@ -1,11 +1,12 @@
1
- """The oep command: what a probe offers (dump) and its settings (config).
1
+ """The oep command: what a probe offers (dump) and its settings (settings, core §13).
2
2
 
3
3
  oep dump --port /run/board-identify/by-id/<probe> oep dump --virtual p4-x035 --prefix oep.target --json
4
- oep config show <probe> (the settings and what the probe declares; lock-free)
5
- oep config state <probe> (the storage state; lock-free)
6
- oep config label <probe> 23 nrst oep config idle <probe> 20 pull-up
7
- oep config disable <probe> 3 4 (channels the probe never drives or sets up; remove disable CH re-enables)
8
- oep config remove <probe> idle 20 oep config save <probe> oep config erase <probe>
4
+ oep settings show <probe> (every value with its origin, the storage state; lock-free)
5
+ oep settings items <probe> (what each owner declares: names, forms, choices)
6
+ oep settings set <probe> core channel.GPIO23.label=nrst channel.GPIO20.idle=pull-up [--save]
7
+ oep settings unset <probe> core channel.GPIO20.idle oep settings unset --all <probe> --save (empties the storage)
8
+ oep settings save <probe> oep settings state <probe> oep settings network <probe>
9
+ oep settings export <probe> jig.oep-settings.json oep settings import <probe> jig.oep-settings.json [--save]
9
10
  oep find [--timeout 2] [--json] [--no-verify] (probes announcing _oep._tcp by DNS-SD over mDNS: unit_id, host,
10
11
  port, IP; each verified first by confirm + describe's unit_id, host guide
11
12
  §4.1 - one that fails is dropped with a warning)
@@ -20,7 +21,7 @@
20
21
  oep restart <probe> [--reopen-s S] (oep.probe.restart: the probe restarts; waits until it is back, its new boot_id)
21
22
 
22
23
  <probe>: a serial port, tcp://HOST[:PORT] (no port: the one DNS-SD finds), tcp:UNIT_ID (found by DNS-SD) or
23
- usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
24
+ usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep settings") and
24
25
  ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
25
26
  """
26
27
 
@@ -29,12 +30,10 @@ from __future__ import annotations
29
30
  import argparse
30
31
  import sys
31
32
 
32
- import getpass
33
33
  import json
34
34
  import os
35
- import struct
36
35
 
37
- from . import catalog, config, core, dump, virtual_bench, host, link
36
+ from . import catalog, core, dump, settings, virtual_bench, host, link
38
37
 
39
38
  USB_HINT = ("install the usb / hid extras (pip install 'oep-client-python[usb-async,hid]') so every way in to the USB "
40
39
  "device can be tried, or name the probe's port (a serial port such as /dev/ttyACM0 or COM3, tcp://HOST:PORT)")
@@ -56,7 +55,7 @@ def _open_host(target: str, **kw):
56
55
  def main(argv=None) -> int:
57
56
  parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
58
57
  sub = parser.add_subparsers(dest="command", required=True)
59
- _config_parser(sub)
58
+ _settings_parser(sub)
60
59
  sp = sub.add_parser("speed", help="port_speed (oep-if-link §3; this client's procedure): try faster rates on a UART bridge, "
61
60
  "print the report")
62
61
  sp.add_argument("probe", help="the probe's serial port (a UART bridge this host opens)")
@@ -104,7 +103,7 @@ def main(argv=None) -> int:
104
103
  pn.add_argument("--wire", choices=("swio", "rvswd", "swd"), default="swio")
105
104
  pn.add_argument("--steps", default="classify,hold,scan,identify,reset,labels",
106
105
  help="the steps to run, comma-separated (default all: classify, hold, scan, identify, reset, labels)")
107
- pn.add_argument("--save", action="store_true", help="write the suggested labels (config set, then save)")
106
+ pn.add_argument("--save", action="store_true", help="write the suggested labels (settings set, then save)")
108
107
  pn.add_argument("--name", help="the host's connection profile name: labels <name>.nrst / <name>.power_hi (a probe "
109
108
  "serving several targets; default: nrst / power_hi)")
110
109
  pn.add_argument("--json", action="store_true", help="the report as JSON (the steps' lines go to stderr)")
@@ -136,8 +135,8 @@ def main(argv=None) -> int:
136
135
  d.add_argument("--exact", action="store_true", help="the prefix is a whole name")
137
136
  d.add_argument("--json", action="store_true", help="machine-readable output")
138
137
  args = parser.parse_args(argv)
139
- if args.command == "config":
140
- return _config(args)
138
+ if args.command == "settings":
139
+ return _settings(args)
141
140
  if args.command == "speed":
142
141
  return _speed(args)
143
142
  if args.command == "linktest":
@@ -331,172 +330,114 @@ def _speed(args) -> int:
331
330
  return 0 if report.supported else 2
332
331
 
333
332
 
334
- # ---- oep config ----------------------------------------------------------------------------------------------------
333
+ # ---- oep settings ---------------------------------------------------------------------------------------------------
335
334
 
336
- def _config_parser(sub) -> None:
337
- c = sub.add_parser("config", help="the probe's settings (oep.probe.config): labels, idle, disable, pass-throughs, "
338
- "Wi-Fi / IPv4, its own settings, save")
335
+ def _settings_parser(sub) -> None:
336
+ c = sub.add_parser("settings", help="the probe's settings (core §13): show, set / unset by name, save, export / "
337
+ "import a settings file, the network")
339
338
  cs = c.add_subparsers(dest="action", required=True)
340
- show = cs.add_parser("show", help="the settings, what the probe declares, and the storage state")
339
+ show = cs.add_parser("show", help="every value (category, name, value, origin) and the storage state (lock-free)")
341
340
  show.add_argument("probe")
342
341
  show.add_argument("--json", action="store_true")
343
- state = cs.add_parser("state", help="the storage state (op state, lock-free)")
342
+ items = cs.add_parser("items", help="what the probe declares: each category's items, forms and choices")
343
+ items.add_argument("probe")
344
+ items.add_argument("--json", action="store_true")
345
+ st = cs.add_parser("set", help="values by name in one category: NAME=VALUE ... (core channel.GPIO2.label=nrst)")
346
+ st.add_argument("probe")
347
+ st.add_argument("category", help="core, or an interface's name/instance (oep.fixture.uart/0)")
348
+ st.add_argument("values", nargs="+", metavar="NAME=VALUE")
349
+ st.add_argument("--save", action="store_true")
350
+ un = cs.add_parser("unset", help="values by name back to their default (or none)")
351
+ un.add_argument("probe")
352
+ un.add_argument("category", nargs="?")
353
+ un.add_argument("names", nargs="*")
354
+ un.add_argument("--all", action="store_true", help="every value the user set (then --save empties the storage)")
355
+ un.add_argument("--save", action="store_true")
356
+ cs.add_parser("save", help="save the values now in effect").add_argument("probe")
357
+ state = cs.add_parser("state", help="the storage state (lock-free)")
344
358
  state.add_argument("probe")
345
359
  state.add_argument("--json", action="store_true")
346
- label = cs.add_parser("label", help="name a channel (a setting, read with config show)")
347
- label.add_argument("probe")
348
- label.add_argument("channel", type=int)
349
- label.add_argument("text")
350
- label.add_argument("--save", action="store_true")
351
- idle = cs.add_parser("idle", help="the state of a free pin")
352
- idle.add_argument("probe")
353
- idle.add_argument("channel", type=int)
354
- idle.add_argument("mode", choices=sorted(config.IDLE))
355
- idle.add_argument("--save", action="store_true")
356
- rest = cs.add_parser("rvswd-rest", help="how rvswd rests a port's SWCLK: auto (the probe decides from the target_id), "
357
- "or high / low fixed (the automatic choice off; debug §3.1)")
358
- rest.add_argument("probe")
359
- rest.add_argument("channel", type=int, help="the port's SWCLK channel")
360
- rest.add_argument("mode", choices=sorted(config.REST_MODE))
361
- rest.add_argument("--save", action="store_true")
362
- hold = cs.add_parser("wire-hold", help="whether a debug wire's lines stay held at their rest after a close "
363
- "(on, the default) or go free at once (off; debug §2)")
364
- hold.add_argument("probe")
365
- hold.add_argument("channel", type=int, help="the port's SWDIO channel")
366
- hold.add_argument("mode", choices=sorted(config.HOLD_MODE))
367
- hold.add_argument("--save", action="store_true")
368
- dis = cs.add_parser("disable", help="channels the probe never drives or sets up (not on this board)")
369
- dis.add_argument("probe")
370
- dis.add_argument("channels", type=int, nargs="+")
371
- dis.add_argument("--save", action="store_true")
372
- rm = cs.add_parser("remove", help="remove one item (unset): label CH, idle CH, disable CH")
373
- rm.add_argument("probe")
374
- rm.add_argument("kind", help="the item's name: label, idle, disable, passthrough, wifi, ... or a probe's own")
375
- rm.add_argument("channel", type=int)
376
- rm.add_argument("--save", action="store_true")
377
- wifi = cs.add_parser("wifi", help="a Wi-Fi network the probe tries, by entry (the passphrase asked, never shown)")
378
- wifi.add_argument("probe")
379
- wifi.add_argument("entry", type=int)
380
- wifi.add_argument("ssid")
381
- pw = wifi.add_mutually_exclusive_group()
382
- pw.add_argument("--open", action="store_true", help="a network without a passphrase")
383
- pw.add_argument("--keep", action="store_true", help="keep the entry's passphrase (pass_len 0xFF)")
384
- pw.add_argument("--passphrase-env", metavar="VAR", help="take the passphrase from this environment variable")
385
- wifi.add_argument("--save", action="store_true")
386
- ip = cs.add_parser("ipv4", help="how a netif gets its IPv4 address: dhcp, or a.b.c.d/prefix")
387
- ip.add_argument("probe")
388
- ip.add_argument("address", help="dhcp, or a.b.c.d/prefix")
389
- ip.add_argument("--netif", default="wifi", help="the netif's name (describe's netif list; default wifi)")
390
- ip.add_argument("--gateway")
391
- ip.add_argument("--dns")
392
- ip.add_argument("--save", action="store_true")
393
360
  net = cs.add_parser("network", help="the network now: the TCP port, each netif's state and address (lock-free)")
394
361
  net.add_argument("probe")
395
362
  net.add_argument("--json", action="store_true")
396
- pt = cs.add_parser("passthrough", help="a serial port carries a fixture UART's receive outside the OEP frames")
397
- pt.add_argument("probe")
398
- pt.add_argument("port", type=int, help="fn 0's transport index of a serial port")
399
- pt.add_argument("uart", help="the fixture UART: fn number or name#instance")
400
- pt.add_argument("rx", type=int, help="the RX channel")
401
- pt.add_argument("--baud", type=int, default=115200)
402
- pt.add_argument("--format", type=int, default=0)
403
- pt.add_argument("--save", action="store_true")
404
- cp = cs.add_parser("console-passthrough", help="a serial port carries the target's debug console outside the frames")
405
- cp.add_argument("probe")
406
- cp.add_argument("port", type=int, help="fn 0's transport index of a serial port")
407
- cp.add_argument("wire", help="the wire: fn number or name#instance (oep.wire.rvswd / oep.wire.swio)")
408
- cp.add_argument("swdio", type=int)
409
- cp.add_argument("swclk", type=int, nargs="?", default=0xFFFF)
410
- cp.add_argument("--console", required=True, help="the console: fn number or name#instance")
411
- cp.add_argument("--mechanism", required=True, help="the console's mechanism by name (sdi, dmdata, dmseq, ...)")
412
- cp.add_argument("--max-speed", type=int, default=1_000_000)
413
- cp.add_argument("--save", action="store_true")
414
- own = cs.add_parser("set", help="a probe's own setting by its name (reverse-DNS), its form from describe")
415
- own.add_argument("probe")
416
- own.add_argument("name")
417
- own.add_argument("value", help="text, a number (u32), 0 / 1 (bool), a.b.c.d (ipv4) or hex (bytes)")
418
- own.add_argument("--key", type=int, default=0)
419
- own.add_argument("--save", action="store_true")
420
- for name in ("save", "erase"):
421
- cs.add_parser(name, help=f"{name} the stored settings").add_argument("probe")
422
-
423
-
424
- def _change(hst, cfg, items, save: bool) -> None:
425
- core.take(hst, 3000, owner="oep config")
363
+ ex = cs.add_parser("export", help="write the user's values to a settings file (JSON; secrets left out)")
364
+ ex.add_argument("probe")
365
+ ex.add_argument("file", help="the file to write, - for stdout")
366
+ im = cs.add_parser("import", help="make the probe's user values a settings file's (shows what changes first)")
367
+ im.add_argument("probe")
368
+ im.add_argument("file")
369
+ im.add_argument("--yes", action="store_true", help="apply without asking")
370
+ im.add_argument("--save", action="store_true")
371
+ im.add_argument("--secret-env", action="append", default=[], metavar="CATEGORY:NAME=VAR",
372
+ help="a secret the file cannot carry, from an environment variable")
373
+
374
+
375
+ def _change(hst, fn, save: bool) -> None:
376
+ core.take(hst, 3000, owner="oep settings")
426
377
  try:
427
- gen = cfg.set(items)
428
- print(f"set: generation {gen}")
378
+ gen = fn()
379
+ if gen is not None:
380
+ print(f"set: generation {gen}")
429
381
  if save:
430
- if cfg.needs_save():
431
- print(f"saved: generation {cfg.save()}")
382
+ st = settings.Settings(hst)
383
+ if st.needs_save():
384
+ print(f"saved: generation {st.save()}")
432
385
  else:
433
386
  print("saved already (saved_generation is the current generation): not written")
434
387
  finally:
435
388
  hst.end()
436
389
 
437
390
 
438
- def _config(args) -> int:
391
+ def _pairs(texts: list[str]) -> dict:
392
+ out = {}
393
+ for t in texts:
394
+ name, eq, value = t.partition("=")
395
+ if not eq:
396
+ raise SystemExit(f"{t!r}: NAME=VALUE")
397
+ out[name] = value
398
+ return out
399
+
400
+
401
+ def _settings(args) -> int:
439
402
  hst = _open_host(args.probe)
440
403
  try:
441
- cfg = config.ProbeConfig(hst)
404
+ st = settings.Settings(hst)
442
405
  if args.action == "show":
443
- return _show(hst, cfg, args.json)
406
+ return _show(st, args.json)
407
+ if args.action == "items":
408
+ return _items(st, args.json)
444
409
  if args.action == "state":
445
- return _state(cfg, args.json)
446
- if args.action == "label":
447
- _change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
448
- elif args.action == "idle":
449
- _change(hst, cfg, [config.Idle(channel=args.channel, mode=args.mode)], args.save)
450
- elif args.action == "wire-hold":
451
- _change(hst, cfg, [config.WireHold(channel=args.channel, mode=args.mode)], args.save)
452
- elif args.action == "rvswd-rest":
453
- _change(hst, cfg, [config.RvswdRest(channel=args.channel, mode=args.mode)], args.save)
454
- elif args.action == "disable":
455
- _change(hst, cfg, [config.Disable(channel=ch) for ch in args.channels], args.save)
456
- elif args.action == "remove":
457
- _change(hst, cfg, [config.remove(args.kind, args.channel)], args.save)
458
- elif args.action == "wifi":
459
- if args.keep:
460
- passphrase = None
461
- elif args.open:
462
- passphrase = ""
463
- elif args.passphrase_env:
464
- passphrase = os.environ[args.passphrase_env]
410
+ return _state(st, args.json)
411
+ if args.action == "network":
412
+ return _network(st, args.json)
413
+ if args.action == "set":
414
+ values = _pairs(args.values)
415
+ _change(hst, lambda: st.set_named(args.category, values), args.save)
416
+ elif args.action == "unset":
417
+ if args.all:
418
+ _change(hst, st.unset_all, args.save)
465
419
  else:
466
- passphrase = getpass.getpass(f"passphrase for {args.ssid}: ")
467
- _change(hst, cfg, [config.Wifi(entry=args.entry, ssid=args.ssid, passphrase=passphrase)], args.save)
468
- elif args.action == "ipv4":
469
- address = None if args.address == "dhcp" else args.address
470
- _change(hst, cfg, [config.Ipv4(netif=args.netif, address=address, gateway=args.gateway, dns=args.dns)],
471
- args.save)
472
- elif args.action == "network":
473
- return _network(cfg, args.json)
474
- elif args.action == "passthrough":
475
- _change(hst, cfg, [config.Passthrough(port=args.port, fn=_fn(hst, args.uart), rx=args.rx, baud=args.baud,
476
- format=args.format)], args.save)
477
- elif args.action == "console-passthrough":
478
- from . import console as _console
479
- cfn = _fn(hst, args.console)
480
- names = [v.decode() for t, v in core.describe(hst, cfn) if t == _console.Console.TAG_MECHANISM]
481
- if args.mechanism not in names:
482
- raise SystemExit(f"the console declares no mechanism {args.mechanism} ({', '.join(names)})")
483
- mech = names.index(args.mechanism) + 1 # the place in describe's list, from 1 (core §7.7)
484
- _change(hst, cfg, [config.ConsolePassthrough(port=args.port, wire_fn=_fn(hst, args.wire), swdio=args.swdio,
485
- swclk=args.swclk, max_speed=args.max_speed, console_fn=cfn,
486
- mechanism=mech)], args.save)
487
- elif args.action == "set":
488
- _change(hst, cfg, [config.Own(args.name, _own_value(cfg, args.name, args.value), args.key)], args.save)
489
- else:
490
- core.take(hst, 3000, owner="oep config")
491
- try:
492
- if args.action == "save":
493
- print(f"saved: generation {cfg.save()}")
494
- else:
495
- cfg.erase()
496
- print("erased (the settings now in effect stay until the probe restarts)")
497
- finally:
498
- hst.end()
420
+ if not args.category or not args.names:
421
+ raise SystemExit("unset: CATEGORY NAME ..., or --all")
422
+ _change(hst, lambda: st.unset_named(args.category, args.names), args.save)
423
+ elif args.action == "save":
424
+ _change(hst, lambda: None, True)
425
+ elif args.action == "export":
426
+ doc = st.export()
427
+ text = json.dumps(doc, indent=2, ensure_ascii=False) + "\n"
428
+ if args.file == "-":
429
+ sys.stdout.write(text)
430
+ else:
431
+ with open(args.file, "w", encoding="utf-8") as f:
432
+ f.write(text)
433
+ n = sum(len(v) for v in doc["settings"].values())
434
+ print(f"wrote {n} values to {args.file}")
435
+ elif args.action == "import":
436
+ return _import(hst, st, args)
499
437
  return 0
438
+ except (LookupError, ValueError) as e:
439
+ print(f"settings: {e}", file=sys.stderr)
440
+ return 2
500
441
  except host.Rejected as e:
501
442
  print(f"refused: {e}", file=sys.stderr)
502
443
  return 2
@@ -504,32 +445,51 @@ def _config(args) -> int:
504
445
  hst.link.close()
505
446
 
506
447
 
507
- def _fn(hst, ref: str) -> int:
508
- """A fn by number, or by `name#instance` (host guide §5.3; #0 may be left out) resolved through list."""
509
- if ref.isdigit():
510
- return int(ref)
511
- name, _, instance = ref.partition("#")
512
- for e in core.list_entries(hst, name, exact=True):
513
- if e.instance == int(instance or 0):
514
- return e.fn
515
- raise SystemExit(f"no {ref} on this probe")
516
-
517
-
518
- def _own_value(cfg, name: str, text: str):
519
- """A probe's own setting's value from the command line, by the form its describe declares (probe.config §1.6)."""
520
- form = cfg.describe().by_name(name).form
521
- if form == config.FORM["u32"]:
522
- return int(text, 0)
523
- if form == config.FORM["bool"]:
524
- return text.lower() in ("1", "true", "on", "yes")
525
- if form == config.FORM["bytes"]:
526
- return bytes.fromhex(text)
527
- return text
448
+ def _import(hst, st, args) -> int:
449
+ with open(args.file, encoding="utf-8") as f:
450
+ doc = json.load(f)
451
+ plan = st.plan_import(doc)
452
+ cat = st.catalog()
453
+ secrets = {}
454
+ for spec in args.secret_env:
455
+ where, eq, var = spec.partition("=")
456
+ category, _, name = where.partition(":")
457
+ if not eq or not name:
458
+ raise SystemExit(f"--secret-env {spec!r}: CATEGORY:NAME=VAR")
459
+ secrets[(category, name)] = os.environ[var]
460
+ if plan.other_probe:
461
+ print(f"note: the file is of another probe ({doc.get('probe', {})})")
462
+ for category, flat in plan.unresolved:
463
+ print(f" not on this probe: {category} {flat}")
464
+ for fn, number, key, data in plan.sets:
465
+ it = cat.item(fn, number)
466
+ print(f" set {cat.category[fn]} {cat.flat(it, key)} = {json.dumps(cat.to_json(it, data))}")
467
+ for fn, number, key in plan.unsets:
468
+ it = cat.item(fn, number)
469
+ print(f" unset {cat.category[fn]} {cat.flat(it, key)}")
470
+ for category, flat in plan.secret_without_value:
471
+ if (category, flat) in secrets:
472
+ it, key = cat.resolve(category, flat)
473
+ plan.sets.append((it.fn, it.number, key, secrets[(category, flat)].encode()))
474
+ print(f" set {category} {flat} (secret, from the environment)")
475
+ else:
476
+ print(f" no value on the probe for secret {category} {flat}: give it with --secret-env")
477
+ if not plan.sets and not plan.unsets:
478
+ print("nothing to change")
479
+ if args.save:
480
+ _change(hst, lambda: None, True)
481
+ return 0
482
+ if not args.yes:
483
+ if input("apply? [y/N] ").strip().lower() not in ("y", "yes"):
484
+ print("not applied")
485
+ return 1
486
+ _change(hst, lambda: st.apply_import(plan), args.save)
487
+ return 0
528
488
 
529
489
 
530
- def _network(cfg, as_json: bool) -> int:
490
+ def _network(st, as_json: bool) -> int:
531
491
  """op network (lock-free): the TCP port and each netif's state."""
532
- net = cfg.network()
492
+ net = st.network()
533
493
  if as_json:
534
494
  print(json.dumps({"tcp_port": net.tcp_port, "netifs": [vars(n) for n in net.netifs]}, indent=2))
535
495
  return 0
@@ -543,41 +503,47 @@ def _network(cfg, as_json: bool) -> int:
543
503
  return 0
544
504
 
545
505
 
546
- def _plain(o):
547
- return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
548
- else list(o)
549
-
550
-
551
- def _state(cfg, as_json: bool) -> int:
506
+ def _state(st, as_json: bool) -> int:
552
507
  """The storage state alone (op state, lock-free)."""
553
- st = cfg.state()
508
+ s = st.state()
554
509
  if as_json:
555
- print(json.dumps(vars(st), indent=2))
510
+ print(json.dumps(vars(s), indent=2))
556
511
  return 0
557
- why = f" ({st.reason})" if st.reason else ""
558
- print(f"storage: {st.storage}{why}, saved generation {st.saved_generation}")
512
+ why = f" ({s.reason})" if s.reason else ""
513
+ print(f"storage: {s.storage}{why}, saved generation {s.saved_generation}")
559
514
  return 0
560
515
 
561
516
 
562
- def _show(hst, cfg, as_json: bool) -> int:
563
- gen, _ = cfg.get()
564
- items = cfg.items()
565
- decl = cfg.describe()
566
- st = cfg.state() if cfg.offers(cfg.STATE) else None
517
+ def _show(st, as_json: bool) -> int:
518
+ gen, _ = st.get()
519
+ rows = st.show()
520
+ state = st.state() if st.offers("state") else None
567
521
  if as_json:
568
- out = {"generation": gen, "items": [dict(type=type(i).__name__, **_plain(i)) if hasattr(i, "__dict__")
569
- else _plain(i) for i in items],
570
- "declared": _plain(decl), "state": vars(st) if st else None}
571
- print(json.dumps(out, indent=2, default=lambda o: o.hex() if isinstance(o, bytes) else str(o)))
522
+ print(json.dumps({"generation": gen, "state": vars(state) if state else None,
523
+ "values": [dict(category=c, name=n, value=v, origin=o) for c, n, v, o in rows]}, indent=2))
572
524
  return 0
573
- if st is not None:
574
- why = f" ({st.reason})" if st.reason else ""
575
- print(f"storage: {st.storage}{why} ({decl.storage_bytes} bytes), saved generation {st.saved_generation}; "
576
- f"now {gen}")
525
+ if state is not None:
526
+ why = f" ({state.reason})" if state.reason else ""
527
+ print(f"storage: {state.storage}{why}, saved generation {state.saved_generation}; now {gen}")
577
528
  else:
578
529
  print(f"no storage; generation {gen}")
579
- for it in items:
580
- print(f" {it}")
530
+ for c, n, v, o in rows:
531
+ shown = "(secret)" if isinstance(v, dict) else json.dumps(v)
532
+ print(f" {c} {n} = {shown}" + ("" if o == "user" else f" ({o})"))
533
+ return 0
534
+
535
+
536
+ def _items(st, as_json: bool) -> int:
537
+ cat = st.catalog()
538
+ rows = [dict(category=cat.category[fn], number=it.number, name=it.name, form=it.form, key=it.key_kind,
539
+ keys=it.keys, max_len=it.max_len, secret=it.secret, choices=sorted(it.choices.values()))
540
+ for fn in sorted(cat.items) for it in cat.items[fn]]
541
+ if as_json:
542
+ print(json.dumps(rows, indent=2))
543
+ return 0
544
+ for r in rows:
545
+ extra = f" {'|'.join(r['choices'])}" if r["choices"] else ""
546
+ print(f" {r['category']} {r['name']} {r['form']}{extra}" + (" secret" if r["secret"] else ""))
581
547
  return 0
582
548
 
583
549