oep-client-python 0.0.36__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.
- oep_client/__init__.py +1 -1
- oep_client/__main__.py +171 -205
- oep_client/capture.py +74 -32
- oep_client/conformance.py +1 -1
- oep_client/conformance_capture_metadata.py +7 -10
- oep_client/conformance_host_request_sample.py +0 -2
- oep_client/conformance_host_requests.py +3 -9
- oep_client/conformance_host_wait.py +7 -7
- oep_client/conformance_host_wait_sample.py +6 -6
- oep_client/conformance_registered_ops.py +1 -2
- oep_client/conformance_selected_metadata.py +3 -9
- oep_client/core.py +24 -18
- oep_client/discovery.py +1 -1
- oep_client/dump.py +7 -3
- oep_client/endpoint.py +367 -328
- oep_client/hid_stream.py +10 -3
- oep_client/host.py +8 -2
- oep_client/interfaces.py +3 -7
- oep_client/link.py +21 -7
- oep_client/message.py +2 -2
- oep_client/pins.py +23 -21
- oep_client/registry.py +23 -27
- oep_client/riscv.py +8 -1
- oep_client/settings.py +570 -0
- oep_client/usb_stream.py +292 -20
- oep_client/virtual_analog_model.py +2 -2
- oep_client/virtual_bench.py +128 -58
- oep_client/virtual_bench_serve.py +3 -3
- oep_client/virtual_capture_group_model.py +1 -1
- oep_client/virtual_capture_metadata_model.py +3 -3
- oep_client/virtual_capture_timeline_model.py +1 -1
- oep_client/virtual_registered_ops_model.py +1 -1
- oep_client/virtual_selected_metadata_model.py +4 -4
- {oep_client_python-0.0.36.dist-info → oep_client_python-0.0.37.dist-info}/METADATA +18 -15
- {oep_client_python-0.0.36.dist-info → oep_client_python-0.0.37.dist-info}/RECORD +38 -55
- oep_client/config.py +0 -675
- oep_client/conformance_config.py +0 -139
- oep_client/conformance_config_execution.py +0 -84
- oep_client/conformance_config_execution_sample.py +0 -13
- oep_client/conformance_config_fallback.py +0 -72
- oep_client/conformance_config_fallback_sample.py +0 -25
- oep_client/conformance_config_paging.py +0 -87
- oep_client/conformance_config_paging_sample.py +0 -29
- oep_client/conformance_config_power_loss.py +0 -89
- oep_client/conformance_config_power_loss_sample.py +0 -15
- oep_client/conformance_config_sample.py +0 -13
- oep_client/conformance_config_storage.py +0 -79
- oep_client/conformance_config_storage_sample.py +0 -15
- oep_client/virtual_config_execution_model.py +0 -68
- oep_client/virtual_config_model.py +0 -176
- oep_client/virtual_config_paging_model.py +0 -12
- oep_client/virtual_config_power_loss_model.py +0 -77
- oep_client/virtual_config_storage_model.py +0 -20
- {oep_client_python-0.0.36.dist-info → oep_client_python-0.0.37.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.36.dist-info → oep_client_python-0.0.37.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.36.dist-info → oep_client_python-0.0.37.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/__main__.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
"""The oep command: what a probe offers (dump) and its settings (
|
|
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
|
|
5
|
-
oep
|
|
6
|
-
oep
|
|
7
|
-
oep
|
|
8
|
-
oep
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
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 == "
|
|
140
|
-
return
|
|
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
|
|
333
|
+
# ---- oep settings ---------------------------------------------------------------------------------------------------
|
|
335
334
|
|
|
336
|
-
def
|
|
337
|
-
c = sub.add_parser("
|
|
338
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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 =
|
|
428
|
-
|
|
378
|
+
gen = fn()
|
|
379
|
+
if gen is not None:
|
|
380
|
+
print(f"set: generation {gen}")
|
|
429
381
|
if save:
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
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
|
-
|
|
404
|
+
st = settings.Settings(hst)
|
|
442
405
|
if args.action == "show":
|
|
443
|
-
return _show(
|
|
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(
|
|
446
|
-
if args.action == "
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
_change(hst,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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(
|
|
490
|
+
def _network(st, as_json: bool) -> int:
|
|
531
491
|
"""op network (lock-free): the TCP port and each netif's state."""
|
|
532
|
-
net =
|
|
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
|
|
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
|
-
|
|
508
|
+
s = st.state()
|
|
554
509
|
if as_json:
|
|
555
|
-
print(json.dumps(vars(
|
|
510
|
+
print(json.dumps(vars(s), indent=2))
|
|
556
511
|
return 0
|
|
557
|
-
why = f" ({
|
|
558
|
-
print(f"storage: {
|
|
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(
|
|
563
|
-
gen, _ =
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
569
|
-
|
|
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
|
|
574
|
-
why = f" ({
|
|
575
|
-
print(f"storage: {
|
|
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
|
|
580
|
-
|
|
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
|
|
oep_client/capture.py
CHANGED
|
@@ -368,7 +368,9 @@ class CaptureDrops:
|
|
|
368
368
|
no room for - then the way the data goes (1 the zero-copy stages, 0 the store), the stages held, and the memory a
|
|
369
369
|
capture draws on: internal RAM's and DMA-capable internal RAM's largest free blocks, PSRAM's total (0: none); then
|
|
370
370
|
the most the stream's buffer (the DMA ring, in PSRAM on a P4) held not yet sent since start (bytes; None from a
|
|
371
|
-
firmware before it)
|
|
371
|
+
firmware before it); then the spill behind the ring: the most it held since start and its size (bytes, 0 0: none;
|
|
372
|
+
None from a firmware before it); then every byte the stream dropped since start, whatever the way (the copied way's
|
|
373
|
+
segments with no room to send too; None from a firmware before it)."""
|
|
372
374
|
no_stage_bytes: int
|
|
373
375
|
unqueued_bytes: int
|
|
374
376
|
dma_overruns: int
|
|
@@ -379,14 +381,19 @@ class CaptureDrops:
|
|
|
379
381
|
dma_largest: int
|
|
380
382
|
psram_total: int
|
|
381
383
|
buffer_peak: int | None = None
|
|
384
|
+
spill_peak: int | None = None
|
|
385
|
+
spill_bytes: int | None = None
|
|
386
|
+
dropped_bytes: int | None = None
|
|
382
387
|
|
|
383
|
-
FORMAT = "<
|
|
388
|
+
FORMAT = "<IIIIBBIIIIIIQ"
|
|
389
|
+
FORMAT_BEFORE_DROPPED = "<IIIIBBIIIIII" # a firmware before dropped_bytes (until 2026-10-21)
|
|
390
|
+
FORMAT_BEFORE_SPILL = "<IIIIBBIIII" # a firmware before the spill (until 2026-10-20)
|
|
384
391
|
FORMAT_BEFORE_PEAK = "<IIIIBBIII" # a firmware before buffer_peak (until 2026-10-20)
|
|
385
392
|
|
|
386
393
|
@classmethod
|
|
387
394
|
def unpack(cls, data: bytes) -> "CaptureDrops | None":
|
|
388
395
|
"""-> its fields, or None for data of another length (a form this host does not know)."""
|
|
389
|
-
for form in (cls.FORMAT, cls.FORMAT_BEFORE_PEAK):
|
|
396
|
+
for form in (cls.FORMAT, cls.FORMAT_BEFORE_DROPPED, cls.FORMAT_BEFORE_SPILL, cls.FORMAT_BEFORE_PEAK):
|
|
390
397
|
if len(data) == struct.calcsize(form):
|
|
391
398
|
f = struct.unpack(form, data)
|
|
392
399
|
return cls(*f[:4], bool(f[4]), *f[5:])
|
|
@@ -606,35 +613,8 @@ class LogicCapture(Interface):
|
|
|
606
613
|
got = into or Received()
|
|
607
614
|
deadline = time.monotonic() + seconds if seconds is not None else None
|
|
608
615
|
kept = getattr(got, "_kept", None) or time.monotonic() # on from the last call with the same `into`: a host
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
while True:
|
|
612
|
-
for e in link.events:
|
|
613
|
-
if struct.unpack_from("<H", e, 1)[0] == self.fn:
|
|
614
|
-
event_seqs.add(struct.unpack_from("<H", e, 3)[0])
|
|
615
|
-
for seq, position, data, generation, info in take_pushes(link, self.fn):
|
|
616
|
-
while expect_seq is not None and expect_seq != seq:
|
|
617
|
-
if expect_seq in event_seqs:
|
|
618
|
-
event_seqs.discard(expect_seq)
|
|
619
|
-
else:
|
|
620
|
-
got.seq_lost += 1
|
|
621
|
-
expect_seq = (expect_seq + 1) & 0xFFFF
|
|
622
|
-
expect_seq = (seq + 1) & 0xFFFF
|
|
623
|
-
if generation is not None and self.generation is not None and generation != self.generation:
|
|
624
|
-
got.stale += 1 # the generation before: not this capture's bytes
|
|
625
|
-
continue
|
|
626
|
-
if info is not None:
|
|
627
|
-
got.segments[info.position] = info # the latest: samples grow while it is taken
|
|
628
|
-
if got.start is None:
|
|
629
|
-
got.start = position
|
|
630
|
-
else:
|
|
631
|
-
expected = got.start + len(got.data) + sum(n for _, n in got.gaps) # u64 positions: no wrap
|
|
632
|
-
skipped = position - expected
|
|
633
|
-
if skipped:
|
|
634
|
-
got.gaps.append((len(got.data), skipped))
|
|
635
|
-
got.data += data
|
|
636
|
-
got.frames += 1
|
|
637
|
-
got._seq, got._events = expect_seq, event_seqs
|
|
616
|
+
while True: # calling in short slices still keeps the lease
|
|
617
|
+
self._collect(link, got)
|
|
638
618
|
if keepalive_s and time.monotonic() - kept >= keepalive_s:
|
|
639
619
|
self.host.keepalive()
|
|
640
620
|
kept = time.monotonic()
|
|
@@ -645,6 +625,37 @@ class LogicCapture(Interface):
|
|
|
645
625
|
return got
|
|
646
626
|
link.pump(0.02, until_one=True)
|
|
647
627
|
|
|
628
|
+
def _collect(self, link, got: Received) -> None:
|
|
629
|
+
"""The pushes of this fn waiting on the link into `got` (stream()'s rules: seq, generation, gaps, segments)."""
|
|
630
|
+
expect_seq = getattr(got, "_seq", None)
|
|
631
|
+
event_seqs = getattr(got, "_events", set()) # events share the fn's seq (they stay on the link for the caller)
|
|
632
|
+
for e in link.events:
|
|
633
|
+
if struct.unpack_from("<H", e, 1)[0] == self.fn:
|
|
634
|
+
event_seqs.add(struct.unpack_from("<H", e, 3)[0])
|
|
635
|
+
for seq, position, data, generation, info in take_pushes(link, self.fn):
|
|
636
|
+
while expect_seq is not None and expect_seq != seq:
|
|
637
|
+
if expect_seq in event_seqs:
|
|
638
|
+
event_seqs.discard(expect_seq)
|
|
639
|
+
else:
|
|
640
|
+
got.seq_lost += 1
|
|
641
|
+
expect_seq = (expect_seq + 1) & 0xFFFF
|
|
642
|
+
expect_seq = (seq + 1) & 0xFFFF
|
|
643
|
+
if generation is not None and self.generation is not None and generation != self.generation:
|
|
644
|
+
got.stale += 1 # the generation before: not this capture's bytes
|
|
645
|
+
continue
|
|
646
|
+
if info is not None:
|
|
647
|
+
got.segments[info.position] = info # the latest: samples grow while it is taken
|
|
648
|
+
if got.start is None:
|
|
649
|
+
got.start = position
|
|
650
|
+
else:
|
|
651
|
+
expected = got.start + len(got.data) + sum(n for _, n in got.gaps) # u64 positions: no wrap
|
|
652
|
+
skipped = position - expected
|
|
653
|
+
if skipped:
|
|
654
|
+
got.gaps.append((len(got.data), skipped))
|
|
655
|
+
got.data += data
|
|
656
|
+
got.frames += 1
|
|
657
|
+
got._seq, got._events = expect_seq, event_seqs
|
|
658
|
+
|
|
648
659
|
def finish(self, link, got: Received, timeout: float = 5.0) -> Received:
|
|
649
660
|
"""Streaming, after stop() or an error stop: collect the pushes still to come, up to the last byte captured
|
|
650
661
|
(status's write position), or until `timeout`. In state 6 the bytes at or past write_pos are dropped (§2.2)."""
|
|
@@ -1210,6 +1221,37 @@ class CaptureGroup(Interface):
|
|
|
1210
1221
|
self.stale_events += len(got) - len(out)
|
|
1211
1222
|
return out
|
|
1212
1223
|
|
|
1224
|
+
def stream(self, link, tracks: list[LogicCapture], *, seconds: float | None = None, nbytes: int | None = None,
|
|
1225
|
+
into: dict[int, Received] | None = None, keepalive_s: float = 1.0) -> dict[int, Received]:
|
|
1226
|
+
"""Streaming tracks of this group (§4.1: all in mode 3, each subscribed): collect every track's pushes together,
|
|
1227
|
+
each into its own Received by fn (LogicCapture.stream()'s rules per track), until each has `nbytes` or
|
|
1228
|
+
`seconds` have passed (at least one is needed). The lock is kept alive every `keepalive_s`."""
|
|
1229
|
+
if seconds is None and nbytes is None:
|
|
1230
|
+
raise ValueError("stream() needs seconds or nbytes")
|
|
1231
|
+
got = into if into is not None else {}
|
|
1232
|
+
for t in tracks:
|
|
1233
|
+
got.setdefault(t.fn, Received())
|
|
1234
|
+
deadline = time.monotonic() + seconds if seconds is not None else None
|
|
1235
|
+
kept = getattr(self, "_kept", None) or time.monotonic()
|
|
1236
|
+
while True:
|
|
1237
|
+
for t in tracks:
|
|
1238
|
+
t._collect(link, got[t.fn])
|
|
1239
|
+
if keepalive_s and time.monotonic() - kept >= keepalive_s:
|
|
1240
|
+
self.host.keepalive()
|
|
1241
|
+
kept = time.monotonic()
|
|
1242
|
+
self._kept = kept
|
|
1243
|
+
if nbytes is not None and all(len(got[t.fn].data) >= nbytes for t in tracks):
|
|
1244
|
+
return got
|
|
1245
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
1246
|
+
return got
|
|
1247
|
+
link.pump(0.02, until_one=True)
|
|
1248
|
+
|
|
1249
|
+
def finish(self, link, tracks: list[LogicCapture], got: dict[int, Received], timeout: float = 5.0) -> dict[int, Received]:
|
|
1250
|
+
"""Streaming, after stop(): each track's pushes still to come (LogicCapture.finish() per track)."""
|
|
1251
|
+
for t in tracks:
|
|
1252
|
+
t.finish(link, got.setdefault(t.fn, Received()), timeout)
|
|
1253
|
+
return got
|
|
1254
|
+
|
|
1213
1255
|
def wait(self, timeout: float = 5.0, keepalive_s: float = 1.0) -> GroupStatus:
|
|
1214
1256
|
"""Poll until every track is done (one-shot). The lock is kept alive every `keepalive_s` while it waits."""
|
|
1215
1257
|
deadline = time.monotonic() + timeout
|