oep-client-python 0.0.28__py3-none-any.whl → 0.0.29__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 (45) hide show
  1. oep_client/__init__.py +1 -1
  2. oep_client/__main__.py +292 -56
  3. oep_client/arm.py +5 -2
  4. oep_client/capture.py +340 -59
  5. oep_client/catalog.py +75 -30
  6. oep_client/ch32_flash.py +6 -3
  7. oep_client/cobs.py +3 -3
  8. oep_client/config.py +342 -132
  9. oep_client/console.py +53 -18
  10. oep_client/core.py +202 -39
  11. oep_client/discovery.py +457 -0
  12. oep_client/dump.py +111 -22
  13. oep_client/endpoint.py +1917 -1075
  14. oep_client/fixture.py +170 -62
  15. oep_client/frames.py +17 -8
  16. oep_client/host.py +384 -100
  17. oep_client/interfaces.py +91 -28
  18. oep_client/kept_session.py +162 -0
  19. oep_client/link.py +1247 -265
  20. oep_client/linktest.py +39 -28
  21. oep_client/message.py +83 -82
  22. oep_client/multirate.py +254 -0
  23. oep_client/names.py +12 -8
  24. oep_client/pins.py +57 -37
  25. oep_client/registry.py +67 -42
  26. oep_client/riscv.py +289 -50
  27. oep_client/speed_record.py +64 -21
  28. oep_client/targets.py +7 -4
  29. oep_client/uiapduino.py +13 -11
  30. oep_client/usb_stream.py +2 -2
  31. oep_client/virtual_bench.py +572 -0
  32. oep_client/virtual_bench_capture.py +688 -0
  33. oep_client/virtual_bench_mdns.py +286 -0
  34. oep_client/{fake_serial.py → virtual_bench_serial.py} +24 -9
  35. oep_client/virtual_bench_serve.py +802 -0
  36. oep_client_python-0.0.29.dist-info/METADATA +526 -0
  37. oep_client_python-0.0.29.dist-info/RECORD +43 -0
  38. oep_client/fake.py +0 -361
  39. oep_client/fake_capture.py +0 -493
  40. oep_client/fake_serve.py +0 -365
  41. oep_client_python-0.0.28.dist-info/METADATA +0 -253
  42. oep_client_python-0.0.28.dist-info/RECORD +0 -39
  43. {oep_client_python-0.0.28.dist-info → oep_client_python-0.0.29.dist-info}/WHEEL +0 -0
  44. {oep_client_python-0.0.28.dist-info → oep_client_python-0.0.29.dist-info}/entry_points.txt +0 -0
  45. {oep_client_python-0.0.28.dist-info → oep_client_python-0.0.29.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.28"
5
+ __version__ = "0.0.29"
oep_client/__main__.py CHANGED
@@ -1,21 +1,34 @@
1
1
  """The oep command: what a probe offers (dump) and its settings (config).
2
2
 
3
- oep dump --port /run/board-identify/by-id/<probe> oep dump --fake p4-x035 --prefix oep.target --json
3
+ oep dump --port /run/board-identify/by-id/<probe> oep dump --virtual p4-x035 --prefix oep.target --json
4
4
  oep config show <probe> (the settings and what the probe declares; lock-free)
5
5
  oep config state <probe> (the live slot / bind / storage state; lock-free)
6
6
  oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
7
- oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
7
+ oep config bind <probe> --port 1 --stream slot:x035 (the one stream a serial port carries)
8
8
  oep config plan <probe> oep.fixture.uart#1 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
9
9
  oep config uart <probe> oep.fixture.uart#1 115200 --format 8N1
10
10
  oep config disable <probe> 3 4 (channels the probe never uses or touches; remove disable CH re-enables)
11
11
  oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
12
- oep speed <probe> [--candidates 921600,500000] [--verify [--flows in:2,out:2]] (port_speed, core §3.5 and the host
13
- guide §7: try the candidates in order on a UART bridge, report)
12
+ oep config wifi <probe> --index 0 --ssid LAB --pass-prompt [--save] (a network the probe joins, probe.config §1.4;
13
+ the passphrase is asked without echo or read from --pass-env VAR, never
14
+ printed; without either the entry keeps its passphrase)
15
+ oep config wifi <probe> --from-env [--save] (OEP_WIFI_SSID_<n> / OEP_WIFI_PASS_<n>, n = the index)
16
+ oep config wifi-unset <probe> --index 0 [--save]
17
+ oep find [--timeout 2] [--json] [--no-verify] (probes announcing _oep._tcp by DNS-SD over mDNS: unit_id, host,
18
+ port, IP; each verified first by confirm + describe's unit_id, host guide
19
+ §4.1 - one that fails is dropped with a warning)
20
+ oep speed <probe> [--candidates 921600,500000] [--verify [--flows in:2,out:2]] (port_speed, oep-if-link §3 and the host
21
+ guide §17: try the candidates in order on a UART bridge, report; default
22
+ 500000 - a faster rate only when named, after its 1 s verify each way)
14
23
  oep linktest <probe> --rates now,921600 --patterns in,out,duplex --inflight 1,2 --sizes 128,496 --frames 300
15
24
  oep pins <probe> --power 5 --wire swio (find the target's debug pins and reset line: classify the channels, scan,
16
25
  identify, hold-low + attach under reset; prints a slot, --save writes it)
26
+ oep clock <probe> [-n 8] [--json] (fn 0's clock, core §7.7: the probe's uptime_ns and boot_id against this
27
+ host's time - the reading with the shortest round trip of n)
28
+ oep restart <probe> [--reopen-s S] (oep.probe.restart: the probe restarts; waits until it is back, its new boot_id)
17
29
 
18
- <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
30
+ <probe>: a serial port, tcp://HOST[:PORT] (no port: the one DNS-SD finds), tcp:UNIT_ID (found by DNS-SD) or
31
+ usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
19
32
  ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
20
33
  """
21
34
 
@@ -27,29 +40,51 @@ import sys
27
40
  import json
28
41
  import struct
29
42
 
30
- from . import catalog, config, core, dump, fake, host, link
43
+ from . import catalog, config, core, dump, virtual_bench, host, link
44
+
45
+ USB_HINT = ("install the usb / hid extras (pip install 'oep-client-python[usb-async,hid]') so every way in to the USB "
46
+ "device can be tried, or name the probe's port (a serial port such as /dev/ttyACM0 or COM3, tcp://HOST:PORT)")
47
+
48
+
49
+ def _open_host(target: str, **kw):
50
+ """link.open_host for a command: a probe that cannot be opened (no device, no port, not an OEP probe, a value the
51
+ host does not use) ends the command with one line on stderr instead of a traceback; for a usb target the line
52
+ says how to get another way in."""
53
+ try:
54
+ return link.open_host(target, **kw)
55
+ except (OSError, ValueError, ImportError, host.OepError) as e:
56
+ why = " ".join(str(e).split()).rstrip(".") or type(e).__name__
57
+ hint = f" Hint: {USB_HINT}." if isinstance(e, FileNotFoundError) and (target == "usb" or target.startswith("usb:")) \
58
+ else ""
59
+ raise SystemExit(f"oep: cannot open {target}: {why}.{hint}") from None
31
60
 
32
61
 
33
62
  def main(argv=None) -> int:
34
63
  parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
35
64
  sub = parser.add_subparsers(dest="command", required=True)
36
65
  _config_parser(sub)
37
- sp = sub.add_parser("speed", help="port_speed (core §3.5, host guide §7): try faster rates on a UART bridge, "
66
+ sp = sub.add_parser("speed", help="port_speed (oep-if-link §3, host guide §17): try faster rates on a UART bridge, "
38
67
  "print the report")
39
68
  sp.add_argument("probe", help="the probe's serial port (a UART bridge this host opens)")
40
69
  sp.add_argument("rates", nargs="?", default="", help="the candidates, comma-separated, in order of preference "
41
70
  "(the first that passes is kept; default 500000). Same as --candidates")
42
- sp.add_argument("--candidates", default="", help="the candidates, comma-separated (default 500000)")
71
+ sp.add_argument("--candidates", default="", help="the candidates, comma-separated (default 500000, the default "
72
+ "ceiling of oep-if-link §3 obligation 7). A rate above 500000 is tried only when named here, and "
73
+ "is committed only after full frames ran 1 s each way (in, out; duplex too with --verify's "
74
+ "default flows) at it (host guide §17.3.3)")
43
75
  sp.add_argument("--flows", default="", help="the flows to verify, comma-separated FLOW[:N] (in, out, duplex; N in "
44
76
  "flight, 0 = the most the link keeps; default: all three at that N). Selects --verify")
45
77
  form = sp.add_mutually_exclusive_group()
46
- form.add_argument("--verify", action="store_true", help="the full form (guide §7.3): baseline, the flows measured "
78
+ form.add_argument("--verify", action="store_true", help="the full form (guide §17.3): baseline, the flows measured "
47
79
  "at every candidate, a failed flow run again one at a time")
48
- form.add_argument("--minimal", action="store_true", help="the minimal form (guide §7.2, the default): try, confirm, "
80
+ form.add_argument("--minimal", action="store_true", help="the minimal form (guide §17.2, the default): try, confirm, "
49
81
  "commit, no measurement")
50
82
  sp.add_argument("--frames", type=int, default=link.FLOW_FRAMES, help="frames per flow when verifying (default 16)")
51
83
  sp.add_argument("--no-record", action="store_true", help="do not read or write the record of passed / failed rates "
52
- "(~/.cache/oep-client/link-speed.json, 30 days; on by default here, off in the library)")
84
+ "(~/.cache/oep-client/link-speed.json: a pass 30 days, a failure 1 day; on by default here, off "
85
+ "in the library)")
86
+ sp.add_argument("--max-tries", type=int, default=None, help="the most candidates tried after the record ordered "
87
+ "them (default: all)")
53
88
  sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
54
89
  sp.add_argument("--json", action="store_true")
55
90
  lt = sub.add_parser("linktest", help="measure the link: traffic patterns at rates the host asks for, what breaks")
@@ -61,7 +96,8 @@ def main(argv=None) -> int:
61
96
  lt.add_argument("--sizes", default="", help="comma-separated frame payload sizes (default: a whole frame)")
62
97
  lt.add_argument("--frames", type=int, default=300, help="frames per cell (default 300)")
63
98
  lt.add_argument("--seconds", type=float, default=None, help="per cell for this long instead of --frames")
64
- lt.add_argument("--timeout", type=float, default=0.3, help="seconds to wait for one answer (default 0.3)")
99
+ lt.add_argument("--timeout", type=float, default=None, help="seconds to wait for one answer (default 0.3; 3 over "
100
+ "TCP, where Wi-Fi retransmissions stall answers for 1-4 s)")
65
101
  lt.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed to open at (default 115200)")
66
102
  lt.add_argument("--low-latency", choices=("on", "off"), default="on", help="the serial driver's low-latency mode")
67
103
  lt.add_argument("--json", action="store_true", help="one JSON object per cell")
@@ -78,9 +114,29 @@ def main(argv=None) -> int:
78
114
  pn.add_argument("--slot", type=int, default=0, help="--save: the slot number (default 0)")
79
115
  pn.add_argument("--name", help="--save: the slot's name (default: the target family, else 'target')")
80
116
  pn.add_argument("--json", action="store_true", help="the report as JSON (the steps' lines go to stderr)")
117
+ ck = sub.add_parser("clock", help="the probe's clock (fn 0 clock, core §7.7) against this host's: lock-free")
118
+ ck.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]")
119
+ ck.add_argument("-n", type=int, default=8, help="readings; the one with the shortest round trip is kept (default 8)")
120
+ ck.add_argument("--json", action="store_true")
121
+ rs = sub.add_parser("restart", help="restart the probe (oep.probe.restart, optional) and wait until it is back")
122
+ rs.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]")
123
+ rs.add_argument("--force", action="store_true", help="take the lock from its holder")
124
+ rs.add_argument("--reopen-s", type=float, default=0.0, metavar="S",
125
+ help="when the probe is not back within its restart_max_ms, open it again for up to S more seconds "
126
+ "(your reopen: a host where the device returns late, e.g. WSL re-attaching it through usbipd)")
127
+ fd = sub.add_parser("find", help="probes on the local network that announce _oep._tcp (DNS-SD over mDNS)")
128
+ fd.add_argument("--timeout", type=float, default=2.0, help="seconds to listen for answers (default 2)")
129
+ fd.add_argument("--engine", choices=("auto", "zeroconf", "minimal"), default="auto",
130
+ help="python-zeroconf (the mdns extra) or this package's one-shot query; auto: zeroconf if installed")
131
+ fd.add_argument("--json", action="store_true")
132
+ fd.add_argument("--no-verify", action="store_true",
133
+ help="list every announced instance as it is (default: only those that answer confirm and whose "
134
+ "describe unit_id is their TXT unit_id, host guide §4.1)")
135
+ fd.add_argument("--verify-timeout", type=float, default=1.0, metavar="S",
136
+ help="seconds for each answer of the verify (default 1)")
81
137
  d = sub.add_parser("dump", help="list and describe every interface a probe offers")
82
138
  src = d.add_mutually_exclusive_group(required=True)
83
- src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
139
+ src.add_argument("--virtual", choices=sorted(virtual_bench.PROFILES), help="an in-process virtual bench (no hardware)")
84
140
  src.add_argument("--port", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID] (lock-free reads only)")
85
141
  d.add_argument("--prefix", default="", help="only names under this namespace (label boundaries)")
86
142
  d.add_argument("--exact", action="store_true", help="the prefix is a whole name")
@@ -94,24 +150,99 @@ def main(argv=None) -> int:
94
150
  return _linktest(args)
95
151
  if args.command == "pins":
96
152
  return _pins_cmd(args)
97
-
98
- if args.fake:
99
- call = fake.PROFILES[args.fake]().call
153
+ if args.command == "clock":
154
+ return _clock_cmd(args)
155
+ if args.command == "restart":
156
+ return _restart_cmd(args)
157
+ if args.command == "find":
158
+ return _find_cmd(args)
159
+
160
+ confirm = (0, 1)
161
+ if args.virtual:
162
+ call = virtual_bench.PROFILES[args.virtual]().call
100
163
  else:
101
- hst = link.open_host(args.port)
164
+ hst = _open_host(args.port)
102
165
  call = lambda fn, op, payload: hst.request(fn, op, payload, locked=False).payload # noqa: E731
103
- caps = dump.collect(call, args.prefix, args.exact)
166
+ confirm = hst.confirm_range() # confirmed already: the revision in use (core §7.1)
167
+ caps = dump.collect(call, args.prefix, args.exact, confirm)
104
168
  sys.stdout.write(dump.to_json(caps) + "\n" if args.json else dump.to_text(caps))
105
169
  return 0
106
170
 
107
171
 
172
+ # ---- oep find ------------------------------------------------------------------------------------------------------
173
+
174
+ def _find_cmd(args) -> int:
175
+ """DNS-SD browse for _oep._tcp (transports §3, host guide §4.1): what each instance says - its TXT unit_id, the SRV
176
+ host and port, the addresses. Each is verified first (discovery.verify: confirm and describe's unit_id, all at
177
+ once; no session opened) and one that fails is dropped with a warning on stderr; --no-verify lists them raw."""
178
+ from . import discovery
179
+ try:
180
+ found = discovery.browse(args.timeout, args.engine)
181
+ except ImportError:
182
+ raise SystemExit("oep find --engine zeroconf: install the mdns extra (pip install 'oep-client-python[mdns]')") \
183
+ from None
184
+ dropped = []
185
+ if not args.no_verify:
186
+ found, dropped = discovery.verify(found, args.verify_timeout)
187
+ for f, why in dropped:
188
+ print(f"warning: dropped {f.target or f.host or '?'} ({f.instance}, TXT unit_id {f.unit_id or 'none'}): not "
189
+ f"verified as an OEP probe - {why}", file=sys.stderr)
190
+ if args.json:
191
+ print(json.dumps([{"unit_id": f.unit_id, "instance": f.instance, "host": f.host, "port": f.port,
192
+ "addresses": f.addresses, "target": f.target} for f in found], indent=2))
193
+ return 0
194
+ if not found and dropped:
195
+ print(f"no verified probe: {len(dropped)} instance(s) of _oep._tcp dropped (above)", file=sys.stderr)
196
+ return 1
197
+ if not found:
198
+ print(f"no probe announces _oep._tcp within {args.timeout:g} s (mDNS stays on the local link: behind a NAT or "
199
+ "a router name the probe as tcp://HOST:PORT)", file=sys.stderr)
200
+ return 1
201
+ for f in found:
202
+ print(f"{f.unit_id or '(no unit_id)'} {f.host or '?'} port {f.port or '?'} "
203
+ f"{', '.join(f.addresses) or 'no address'} {f.target} ({f.instance})")
204
+ return 0
205
+
206
+
207
+ # ---- oep clock / oep restart ---------------------------------------------------------------------------------------
208
+
209
+ def _clock_cmd(args) -> int:
210
+ """The shortest round trip of n clock readings (host guide §12): the probe's uptime_ns stands for the midpoint of
211
+ this host's send and receive times (time.monotonic_ns), within half the round trip."""
212
+ hst = _open_host(args.probe)
213
+ r = hst.clock_best(max(1, args.n))
214
+ out = {"boot_id": r.boot_id, "uptime_ns": r.uptime_ns, "host_before_ns": r.before_ns, "host_after_ns": r.after_ns,
215
+ "round_trip_ns": r.round_trip_ns, "host_ns": r.host_ns, "uncertainty_ns": r.uncertainty_ns}
216
+ if args.json:
217
+ print(json.dumps(out))
218
+ else:
219
+ print(f"boot_id 0x{r.boot_id:08X}, uptime {r.uptime_ns / 1e9:.6f} s at host monotonic {r.host_ns / 1e9:.6f} s "
220
+ f"+/- {r.uncertainty_ns / 1e3:.1f} us (round trip {r.round_trip_ns / 1e3:.1f} us, best of {args.n})")
221
+ return 0
222
+
223
+
224
+ def _restart_cmd(args) -> int:
225
+ hst = _open_host(args.probe)
226
+ try:
227
+ core.restart_fn(hst)
228
+ except LookupError:
229
+ raise SystemExit("oep: the probe offers no oep.probe.restart (an optional interface)") from None
230
+ core.take(hst, owner="oep restart", force=args.force)
231
+ before = hst.limits["boot_id"] if hst.limits else None
232
+ after = hst.restart_probe(reopen_s=args.reopen_s)
233
+ if hst.restart_reopened:
234
+ print(f"not back within restart_max_ms; reopened within --reopen-s {args.reopen_s:g}")
235
+ print(f"restarted: boot_id 0x{before:08X} -> 0x{after:08X}" if before is not None else f"restarted: boot_id 0x{after:08X}")
236
+ return 0
237
+
238
+
108
239
  # ---- oep pins -----------------------------------------------------------------------------------------------------
109
240
 
110
241
  def _pins_cmd(args) -> int:
111
242
  from . import pins
112
243
  exclude = [int(x, 0) for x in args.exclude.replace(" ", "").split(",") if x]
113
244
  say = (lambda t: print(t, file=sys.stderr, flush=True)) if args.json else (lambda t: print(t, flush=True)) # noqa: E731
114
- hst = link.open_host(args.probe)
245
+ hst = _open_host(args.probe)
115
246
  try:
116
247
  core.take(hst, 10000, owner="oep pins")
117
248
  finder = pins.PinFinder(hst, wire=args.wire, power=args.power, exclude=exclude, say=say, probe=args.probe,
@@ -141,8 +272,8 @@ def _linktest(args) -> int:
141
272
  from . import linktest
142
273
  ints = lambda text: [int(v) for v in text.split(",") if v.strip()] # noqa: E731
143
274
  rates = [None if v.strip() in ("0", "now") else int(v) for v in args.rates.split(",") if v.strip()] or [None]
144
- hst = link.open_host(args.probe, baud=args.baud) if args.probe.startswith("/") or ":" not in args.probe \
145
- else link.open_host(args.probe)
275
+ hst = _open_host(args.probe, baud=args.baud) if args.probe.startswith("/") or ":" not in args.probe \
276
+ else _open_host(args.probe)
146
277
  stream = getattr(hst.link, "stream", None)
147
278
  if hasattr(stream, "set_low_latency_mode"):
148
279
  try:
@@ -191,11 +322,11 @@ def _speed(args) -> int:
191
322
  candidates = [int(r) for r in text.replace(" ", "").split(",") if r] or list(link.DEFAULT_CANDIDATES)
192
323
  flows = _flows(args.flows)
193
324
  verify = True if args.verify or flows else (False if args.minimal else None)
194
- hst = link.open_host(args.probe, baud=args.baud)
325
+ hst = _open_host(args.probe, baud=args.baud)
195
326
  try:
196
- core.take(hst, 5000, owner="oep speed")
327
+ core.take(hst, max(5000, link.lease_for(candidates, flows, verify)), owner="oep speed")
197
328
  report = link.raise_speed(hst, candidates, flows=flows, verify=verify, frames=args.frames,
198
- record=not args.no_record)
329
+ record=not args.no_record, max_tries=args.max_tries)
199
330
  if args.json:
200
331
  sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
201
332
  else:
@@ -231,21 +362,18 @@ def _config_parser(sub) -> None:
231
362
  help="the line's ceiling in Hz for the probe's own attach (0: none); the target's, e.g. 1000000")
232
363
  slot.add_argument("--idle-clock", choices=sorted(config.IDLE_CLOCK), default="high",
233
364
  help="rvswd: SWCLK while the line rests (the target's: low on CH32L103 / V203)")
234
- slot.add_argument("--lock", help="MASK:VALUE (hex u32) the target_id (WCH DMI 0x7F) must match, e.g. ffffff0f:035e0600")
235
365
  slot.add_argument("--save", action="store_true", help="save after the change")
236
- bind = cs.add_parser("bind", help="what a serial port carries")
366
+ bind = cs.add_parser("bind", help="the one stream a serial port carries (set it again to change it)")
237
367
  bind.add_argument("probe")
238
368
  bind.add_argument("--port", type=int, required=True, help="the serial port (its transport index, see show)")
239
- bind.add_argument("--mode", choices=sorted(config.MODE), default="last-reset")
240
- bind.add_argument("--stream", action="append", required=True, help="slot:NAME, slot:N or uart:FN (repeatable)")
241
- bind.add_argument("--select", type=int, default=0, help="manual: the stream it carries (index in --stream)")
369
+ bind.add_argument("--stream", required=True, help="slot:NAME, slot:N or uart:FN")
242
370
  bind.add_argument("--save", action="store_true")
243
371
  plan = cs.add_parser("plan", help="the pins an interface keeps (its whole plan, kept as a setting)")
244
372
  plan.add_argument("probe")
245
373
  plan.add_argument("fn", help="an fn, or name#instance (the list instance, 0 = the first; #0 may be left out, core §7.2)")
246
374
  plan.add_argument("roles", nargs="+", help="ROLE=CHANNEL, ROLE a number or the interface's role name (rx, tx, line...)")
247
375
  plan.add_argument("--save", action="store_true")
248
- label = cs.add_parser("label", help="name a channel (shown in oep.core's describe)")
376
+ label = cs.add_parser("label", help="name a channel (a setting, read with config get)")
249
377
  label.add_argument("probe")
250
378
  label.add_argument("channel", type=int)
251
379
  label.add_argument("text")
@@ -254,6 +382,10 @@ def _config_parser(sub) -> None:
254
382
  idle.add_argument("probe")
255
383
  idle.add_argument("channel", type=int)
256
384
  idle.add_argument("mode", choices=sorted(config.IDLE))
385
+ drive = idle.add_mutually_exclusive_group()
386
+ drive.add_argument("--drive-ma", type=int, help="output modes: the strongest drive level of about this many mA or "
387
+ "less, picked here from the gpio's describe drive_levels")
388
+ drive.add_argument("--drive-level", type=int, help="output modes: a drive level number of this probe")
257
389
  idle.add_argument("--save", action="store_true")
258
390
  dis = cs.add_parser("disable", help="channels the probe never uses or touches (not on this board)")
259
391
  dis.add_argument("probe")
@@ -270,6 +402,23 @@ def _config_parser(sub) -> None:
270
402
  rm.add_argument("kind", choices=sorted(config.ITEM))
271
403
  rm.add_argument("key", type=int)
272
404
  rm.add_argument("--save", action="store_true")
405
+ wifi = cs.add_parser("wifi", help="a Wi-Fi network the probe joins (the wifi item; the passphrase is never printed)")
406
+ wifi.add_argument("probe")
407
+ wifi.add_argument("--index", type=int, help="the entry (tried in index order; below the probe's wifi_max)")
408
+ wifi.add_argument("--ssid", help="the network's name (1-32 bytes)")
409
+ pw = wifi.add_mutually_exclusive_group()
410
+ pw.add_argument("--pass-prompt", action="store_true", help="ask for the passphrase (no echo)")
411
+ pw.add_argument("--pass-env", metavar="VAR", help="read the passphrase from environment variable VAR")
412
+ pw.add_argument("--open", action="store_true", help="no passphrase: an open network")
413
+ pw.add_argument("--from-env", action="store_true", help="every entry from OEP_WIFI_SSID_<n> / OEP_WIFI_PASS_<n> "
414
+ "(n = the index; no PASS: open); entries whose SSID and passphrase presence already match are not "
415
+ "sent again (a passphrase cannot be compared: --force sends them)")
416
+ wifi.add_argument("--force", action="store_true", help="--from-env: send every entry, passphrases included")
417
+ wifi.add_argument("--save", action="store_true")
418
+ wun = cs.add_parser("wifi-unset", help="remove Wi-Fi entries (the wifi item of these indexes)")
419
+ wun.add_argument("probe")
420
+ wun.add_argument("--index", type=int, action="append", required=True, help="an entry's index (repeatable)")
421
+ wun.add_argument("--save", action="store_true")
273
422
  for name in ("save", "erase"):
274
423
  cs.add_parser(name, help=f"{name} the stored settings").add_argument("probe")
275
424
 
@@ -342,13 +491,16 @@ def _change(hst, cfg, items, save: bool) -> None:
342
491
  h = cfg.set(items)
343
492
  print(f"set: hash 0x{h:08x}")
344
493
  if save:
345
- print(f"saved: hash 0x{cfg.save():08x}")
494
+ if cfg.needs_save():
495
+ print(f"saved: hash 0x{cfg.save():08x}")
496
+ else:
497
+ print("saved already (storage_hash is the settings' hash): not written")
346
498
  finally:
347
499
  hst.end()
348
500
 
349
501
 
350
502
  def _config(args) -> int:
351
- hst = link.open_host(args.probe)
503
+ hst = _open_host(args.probe)
352
504
  try:
353
505
  cfg = config.ProbeConfig(hst)
354
506
  if args.action == "show":
@@ -358,19 +510,13 @@ def _config(args) -> int:
358
510
  if args.action == "slot":
359
511
  fn = _wire_fn(hst, args.wire)
360
512
  args.wire = args.wire or str(fn)
361
- lock = None
362
- if args.lock:
363
- mask, _, value = args.lock.partition(":")
364
- lock = (1, struct.pack("<I", int(mask, 16)), struct.pack("<I", int(value, 16)))
365
513
  it = config.Slot(slot=args.slot, wire_fn=fn, pins=_pins(hst, fn, args.pins, args.wire), name=args.name,
366
- attach=args.attach, retry_s=args.retry, mechanism=args.mechanism, lock=lock,
514
+ attach=args.attach, retry_s=args.retry, mechanism=args.mechanism,
367
515
  max_speed=args.max_speed, idle_clock=args.idle_clock)
368
516
  _change(hst, cfg, [it], args.save)
369
517
  elif args.action == "bind":
370
518
  slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
371
- it = config.Bind(port=args.port, mode=args.mode, streams=[_stream(s, slots) for s in args.stream],
372
- selected=args.select)
373
- _change(hst, cfg, [it], args.save)
519
+ _change(hst, cfg, [config.Bind(port=args.port, stream=_stream(args.stream, slots))], args.save)
374
520
  elif args.action == "plan":
375
521
  fn, roles = _plan_fn(hst, args.fn)
376
522
  items = []
@@ -395,11 +541,24 @@ def _config(args) -> int:
395
541
  elif args.action == "label":
396
542
  _change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
397
543
  elif args.action == "idle":
398
- _change(hst, cfg, [config.Idle(channel=args.channel, mode=args.mode)], args.save)
544
+ from .fixture import Drive, Gpio
545
+ drive = Drive.level(args.drive_level) if args.drive_level is not None else None
546
+ if args.drive_ma is not None:
547
+ levels = Gpio(hst).drive_levels()
548
+ if levels is None:
549
+ raise SystemExit("--drive-ma: this probe declares no drive_levels (its strength cannot be chosen)")
550
+ drive = levels.at_most(args.drive_ma)
551
+ if drive is not None and args.mode not in ("output-low", "output-high"):
552
+ raise SystemExit(f"--drive-ma / --drive-level go with output-low / output-high, not {args.mode}")
553
+ _change(hst, cfg, [config.Idle(channel=args.channel, mode=args.mode, drive=drive)], args.save)
399
554
  elif args.action == "disable":
400
555
  _change(hst, cfg, [config.Disable(channel=ch) for ch in args.channels], args.save)
401
556
  elif args.action == "remove":
402
557
  _change(hst, cfg, [config.remove(args.kind, args.key)], args.save)
558
+ elif args.action == "wifi":
559
+ _wifi_set(hst, cfg, args)
560
+ elif args.action == "wifi-unset":
561
+ _change(hst, cfg, [config.remove("wifi", i) for i in args.index], args.save)
403
562
  else:
404
563
  core.take(hst, 3000, owner="oep config")
405
564
  try:
@@ -418,14 +577,68 @@ def _config(args) -> int:
418
577
  hst.link.close()
419
578
 
420
579
 
580
+ def _wifi_set(hst, cfg, args) -> None:
581
+ """oep config wifi (host guide §15.1): the passphrase from a prompt without echo or an environment variable, never
582
+ from the command line, never printed; without one the entry keeps its passphrase (pass_len 0xFF) - an entry that
583
+ does not exist yet needs --pass-prompt, --pass-env or --open."""
584
+ import os
585
+ decl = cfg.describe()
586
+ if config.ITEM["wifi"] not in decl.items:
587
+ raise SystemExit("this probe has no wifi item (describe's items)")
588
+ if args.from_env:
589
+ wanted = config.wifi_from_env(count=decl.wifi_max)
590
+ if not wanted:
591
+ raise SystemExit("--from-env: no OEP_WIFI_SSID_<n> set (n = 0 .. wifi_max - 1)")
592
+ have = {it.index: it for it in cfg.items() if isinstance(it, config.Wifi)}
593
+ send = [w for w in wanted if args.force or not config.same_items([have[w.index]] if w.index in have else [], [w])]
594
+ for w in wanted:
595
+ print(f"wifi {w.index}: " + ("sent" if w in send else "unchanged (ssid and passphrase presence match)"))
596
+ if send:
597
+ _change(hst, cfg, send, args.save)
598
+ elif args.save and cfg.needs_save():
599
+ core.take(hst, 3000, owner="oep config")
600
+ try:
601
+ print(f"saved: hash 0x{cfg.save():08x}")
602
+ finally:
603
+ hst.end()
604
+ return
605
+ if args.index is None or args.ssid is None:
606
+ raise SystemExit("oep config wifi: --index and --ssid (or --from-env)")
607
+ if not 0 <= args.index < max(decl.wifi_max, 1):
608
+ raise SystemExit(f"--index {args.index}: 0 to {decl.wifi_max - 1} (the probe's wifi_max is {decl.wifi_max})")
609
+ if args.pass_prompt:
610
+ import getpass
611
+ passphrase = getpass.getpass(f"passphrase for {args.ssid!r} (not shown): ")
612
+ elif args.pass_env:
613
+ passphrase = os.environ.get(args.pass_env)
614
+ if passphrase is None:
615
+ raise SystemExit(f"--pass-env {args.pass_env}: not set")
616
+ elif args.open:
617
+ passphrase = None
618
+ else:
619
+ if not any(isinstance(it, config.Wifi) and it.index == args.index for it in cfg.items()):
620
+ raise SystemExit(f"wifi entry {args.index} does not exist yet: give --pass-prompt, --pass-env VAR or --open")
621
+ passphrase = config.KEEP
622
+ it = config.Wifi(index=args.index, ssid=args.ssid, passphrase=passphrase)
623
+ try:
624
+ it.value()
625
+ except ValueError as e:
626
+ raise SystemExit(str(e)) from None
627
+ _change(hst, cfg, [it], args.save)
628
+ print(f"wifi {args.index}: {it.shown()['ssid']!r}, passphrase {it.shown()['passphrase']}")
629
+
630
+
421
631
  def _plain(o):
632
+ if hasattr(o, "shown"):
633
+ return o.shown() # a wifi item: never its passphrase
422
634
  return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
423
635
  else list(o)
424
636
 
425
637
 
426
638
  def _state_dict(st) -> dict:
427
- return {**{k: v for k, v in vars(st).items() if k not in ("slots", "binds")},
428
- "slots": [_plain(s) for s in st.slots], "binds": [_plain(b) for b in st.binds]}
639
+ return {**{k: v for k, v in vars(st).items() if k not in ("slots", "binds", "wifi")},
640
+ "slots": [_plain(s) for s in st.slots], "binds": [_plain(b) for b in st.binds],
641
+ **({"wifi": vars(st.wifi)} if st.wifi is not None else {})}
429
642
 
430
643
 
431
644
  def _state(cfg, as_json: bool) -> int:
@@ -438,13 +651,30 @@ def _state(cfg, as_json: bool) -> int:
438
651
  print(f"storage: {st.storage}{why}, saved hash 0x{st.saved_hash:08x}")
439
652
  for s in st.slots:
440
653
  print(f" slot {s.slot}: {s.state}" + (f", connection {s.connection}" if s.connection else "")
441
- + (f", tried at {s.last_try_at_ns / 1e9:.3f} s" if s.last_try_at_ns is not None else "")
442
- + (f", target_id {s.target_id[::-1].hex()}" if s.target_id else ""))
654
+ + (f", tried at {s.last_try_at_ns / 1e9:.3f} s" if s.last_try_at_ns is not None else ""))
443
655
  for b in st.binds:
444
- print(f" port {b.port}: {b.mode}, {b.flow}" + (f", carrying {b.selected}" if b.selected is not None else ""))
656
+ print(f" port {b.port}: {b.flow}")
657
+ if st.wifi is not None:
658
+ print(f"wifi: {st.wifi.text()}")
445
659
  return 0
446
660
 
447
661
 
662
+ def _tids(hst) -> dict[int, str]:
663
+ """connection -> its target_id as hex, from every wire's connections (the slot's target is the host's to check,
664
+ probe.config §1.1, debug §2.1); best effort."""
665
+ from . import riscv
666
+ out = {}
667
+ for e in core.list_entries(hst):
668
+ if e.name in ("oep.wire.rvswd", "oep.wire.swio"):
669
+ try:
670
+ for c in riscv.Wire(hst, e.name).connections():
671
+ if c.target_id:
672
+ out[c.connection] = c.target_id[1][::-1].hex()
673
+ except host.OepError:
674
+ pass
675
+ return out
676
+
677
+
448
678
  def _show(hst, cfg, as_json: bool) -> int:
449
679
  h, _ = cfg.get()
450
680
  items = cfg.items()
@@ -461,6 +691,7 @@ def _show(hst, cfg, as_json: bool) -> int:
461
691
  print(f"storage: {st.storage}{why} ({decl.storage_bytes} bytes), saved hash 0x{st.saved_hash:08x}; now 0x{h:08x}")
462
692
  print("transports: " + ", ".join(f"{i} {k}" for i, k in kinds.items()))
463
693
  by_slot = {s.slot: s for s in st.slots}
694
+ tids = _tids(hst) if any(s.connection for s in st.slots) else {}
464
695
  print(f"slots (up to {decl.slots_max}):")
465
696
  for it in items:
466
697
  if isinstance(it, config.Slot):
@@ -468,26 +699,31 @@ def _show(hst, cfg, as_json: bool) -> int:
468
699
  pins = f"{it.pins[0]}" if it.pins[1] == 0xFFFF else f"{it.pins[0]},{it.pins[1]}"
469
700
  retry = f" retry {it.retry_s:g} s" if it.attach == "at-boot" else ""
470
701
  retry += (f" max {it.max_speed} Hz" if it.max_speed else "") + (" idle-low" if it.idle_clock == "low" else "")
471
- lock = (f" lock {int.from_bytes(it.lock[1], 'little'):08x}:{int.from_bytes(it.lock[2], 'little'):08x}"
472
- if it.lock else "")
473
702
  live = ""
474
703
  if s:
475
704
  tried = "never tried" if s.last_try_at_ns is None else f"tried at {s.last_try_at_ns / 1e9:.3f} s"
476
- tid = f" target_id {s.target_id[::-1].hex()}" if s.target_id else ""
705
+ tid = f" target_id {tids[s.connection]}" if s.connection in tids else ""
477
706
  live = f" -> {s.state}" + (f" (connection {s.connection})" if s.connection else f" ({tried})") + tid
478
- print(f" {it.slot} {it.name}: fn {it.wire_fn} pins {pins} {it.attach}{retry} {it.mechanism}{lock}{live}")
707
+ print(f" {it.slot} {it.name}: fn {it.wire_fn} pins {pins} {it.attach}{retry} {it.mechanism}{live}")
479
708
  by_port = {b.port: b for b in st.binds}
480
- print(f"binds (modes: {', '.join(decl.bind_modes) or '-'}):")
709
+ print("binds:")
481
710
  names = {it.slot: it.name for it in items if isinstance(it, config.Slot)}
482
711
  for it in items:
483
712
  if isinstance(it, config.Bind):
484
713
  b = by_port.get(it.port)
485
- streams = ", ".join(f"slot:{names.get(i, i)}" if k == "slot" else f"{k}:{i}" for k, i in it.streams)
486
- sel = f" selected {it.selected}" if it.mode == "manual" else ""
487
- live = f" -> {b.flow}" + (f", carrying {b.selected}" if b and b.selected is not None else "") if b else ""
488
- print(f" port {it.port} ({kinds.get(it.port, '?')}): {it.mode} [{streams}]{sel}{live}")
714
+ k, i = it.stream
715
+ stream = f"slot:{names.get(i, i)}" if k == "slot" else f"{k}:{i}"
716
+ live = f" -> {b.flow}" if b else ""
717
+ print(f" port {it.port} ({kinds.get(it.port, '?')}): {stream}{live}")
718
+ if config.ITEM["wifi"] in decl.items:
719
+ print(f"wifi (up to {decl.wifi_max}): " + (st.wifi.text() if st.wifi is not None else "no state"))
720
+ for it in items:
721
+ if isinstance(it, config.Wifi):
722
+ s = it.shown()
723
+ use = " <- in use" if st.wifi is not None and st.wifi.entry == it.index else ""
724
+ print(f" {s['index']} {s['ssid']!r}: passphrase {s['passphrase']}{use}")
489
725
  for it in items:
490
- if not isinstance(it, (config.Slot, config.Bind)):
726
+ if not isinstance(it, (config.Slot, config.Bind, config.Wifi)):
491
727
  print(f" {it}")
492
728
  return 0
493
729
 
oep_client/arm.py CHANGED
@@ -21,6 +21,7 @@ _ADI = reg.TARGET_ARM_ADI
21
21
 
22
22
  class SwdWire(WireBase):
23
23
  NAME = "oep.wire.swd"
24
+ FLAGS = reg.WIRE_SWD.enum["attach_flags"] # existing, dormant_woken (0x01 / 0x08 are riscv's only)
24
25
  TAG_TARGETSEL = reg.WIRE_SWD.tlv["attach"]["targetsel"]
25
26
 
26
27
  def __init__(self, hst: h.Host):
@@ -37,10 +38,12 @@ class SwdWire(WireBase):
37
38
  body = bytes([0]) + self._speed_tlv(self._speed_or_default(max_speed)) + self._pins_tlv(pins) + self._reset_tlv(reset)
38
39
  if targetsel is not None:
39
40
  body += m.tlv(self.TAG_TARGETSEL, struct.pack("<I", targetsel), critical=True)
40
- rd = m.Reader(self._call(self.ATTACH, body, expect_ms=reset[1] if reset else 0).payload)
41
+ rd = m.Reader(self._call(self.ATTACH, body, expect_ms=self.attach_ms(reset)).payload)
41
42
  conn, dpidr, self.flags, self.speed_hz = rd.take("HIBI")
42
43
  self.existing = bool(self.flags & self.FLAGS["existing"])
43
- rd.tail()
44
+ tail = rd.tail()
45
+ tries = tail.get(reg.WIRE_SWD.tlv["attach_answer"]["search_retries"]) # oep-if-debug §1 (optional)
46
+ self.search_retries = struct.unpack_from("<H", tries)[0] if tries and len(tries) >= 2 else None
44
47
  return conn, dpidr, bool(self.flags & self.FLAGS["dormant_woken"])
45
48
 
46
49