oep-client-python 0.0.27__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 +336 -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 +361 -95
  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 +2107 -964
  14. oep_client/fixture.py +217 -60
  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 +634 -0
  25. oep_client/registry.py +67 -42
  26. oep_client/riscv.py +321 -58
  27. oep_client/speed_record.py +64 -21
  28. oep_client/targets.py +100 -0
  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 -330
  39. oep_client/fake_capture.py +0 -493
  40. oep_client/fake_serve.py +0 -365
  41. oep_client_python-0.0.27.dist-info/METADATA +0 -191
  42. oep_client_python-0.0.27.dist-info/RECORD +0 -37
  43. {oep_client_python-0.0.27.dist-info → oep_client_python-0.0.29.dist-info}/WHEEL +0 -0
  44. {oep_client_python-0.0.27.dist-info → oep_client_python-0.0.29.dist-info}/entry_points.txt +0 -0
  45. {oep_client_python-0.0.27.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.27"
5
+ __version__ = "0.0.29"
oep_client/__main__.py CHANGED
@@ -1,19 +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
-
16
- <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
24
+ oep pins <probe> --power 5 --wire swio (find the target's debug pins and reset line: classify the channels, scan,
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)
29
+
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
17
32
  ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
18
33
  """
19
34
 
@@ -25,29 +40,51 @@ import sys
25
40
  import json
26
41
  import struct
27
42
 
28
- 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
29
60
 
30
61
 
31
62
  def main(argv=None) -> int:
32
63
  parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
33
64
  sub = parser.add_subparsers(dest="command", required=True)
34
65
  _config_parser(sub)
35
- 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, "
36
67
  "print the report")
37
68
  sp.add_argument("probe", help="the probe's serial port (a UART bridge this host opens)")
38
69
  sp.add_argument("rates", nargs="?", default="", help="the candidates, comma-separated, in order of preference "
39
70
  "(the first that passes is kept; default 500000). Same as --candidates")
40
- 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)")
41
75
  sp.add_argument("--flows", default="", help="the flows to verify, comma-separated FLOW[:N] (in, out, duplex; N in "
42
76
  "flight, 0 = the most the link keeps; default: all three at that N). Selects --verify")
43
77
  form = sp.add_mutually_exclusive_group()
44
- 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 "
45
79
  "at every candidate, a failed flow run again one at a time")
46
- 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, "
47
81
  "commit, no measurement")
48
82
  sp.add_argument("--frames", type=int, default=link.FLOW_FRAMES, help="frames per flow when verifying (default 16)")
49
83
  sp.add_argument("--no-record", action="store_true", help="do not read or write the record of passed / failed rates "
50
- "(~/.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)")
51
88
  sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
52
89
  sp.add_argument("--json", action="store_true")
53
90
  lt = sub.add_parser("linktest", help="measure the link: traffic patterns at rates the host asks for, what breaks")
@@ -59,13 +96,47 @@ def main(argv=None) -> int:
59
96
  lt.add_argument("--sizes", default="", help="comma-separated frame payload sizes (default: a whole frame)")
60
97
  lt.add_argument("--frames", type=int, default=300, help="frames per cell (default 300)")
61
98
  lt.add_argument("--seconds", type=float, default=None, help="per cell for this long instead of --frames")
62
- 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)")
63
101
  lt.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed to open at (default 115200)")
64
102
  lt.add_argument("--low-latency", choices=("on", "off"), default="on", help="the serial driver's low-latency mode")
65
103
  lt.add_argument("--json", action="store_true", help="one JSON object per cell")
104
+ pn = sub.add_parser("pins", help="find where a target is wired: classify the channels, scan the wire, find the "
105
+ "reset line, suggest a slot (writes nothing without --save)")
106
+ pn.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]")
107
+ pn.add_argument("--power", type=int, help="the gpio channel that powers the target: switched off, then on, to "
108
+ "see which channels follow it (never touched without this)")
109
+ pn.add_argument("--exclude", default="", help="channels never to read, scan or hold, comma-separated")
110
+ pn.add_argument("--wire", choices=("swio", "rvswd", "swd"), default="swio")
111
+ pn.add_argument("--steps", default="classify,hold,scan,identify,reset,slot",
112
+ help="the steps to run, comma-separated (default all: classify, hold, scan, identify, reset, slot)")
113
+ pn.add_argument("--save", action="store_true", help="write the suggested slot (config set, then save)")
114
+ pn.add_argument("--slot", type=int, default=0, help="--save: the slot number (default 0)")
115
+ pn.add_argument("--name", help="--save: the slot's name (default: the target family, else 'target')")
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)")
66
137
  d = sub.add_parser("dump", help="list and describe every interface a probe offers")
67
138
  src = d.add_mutually_exclusive_group(required=True)
68
- 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)")
69
140
  src.add_argument("--port", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID] (lock-free reads only)")
70
141
  d.add_argument("--prefix", default="", help="only names under this namespace (label boundaries)")
71
142
  d.add_argument("--exact", action="store_true", help="the prefix is a whole name")
@@ -77,17 +148,121 @@ def main(argv=None) -> int:
77
148
  return _speed(args)
78
149
  if args.command == "linktest":
79
150
  return _linktest(args)
80
-
81
- if args.fake:
82
- call = fake.PROFILES[args.fake]().call
151
+ if args.command == "pins":
152
+ return _pins_cmd(args)
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
83
163
  else:
84
- hst = link.open_host(args.port)
164
+ hst = _open_host(args.port)
85
165
  call = lambda fn, op, payload: hst.request(fn, op, payload, locked=False).payload # noqa: E731
86
- 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)
87
168
  sys.stdout.write(dump.to_json(caps) + "\n" if args.json else dump.to_text(caps))
88
169
  return 0
89
170
 
90
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
+
239
+ # ---- oep pins -----------------------------------------------------------------------------------------------------
240
+
241
+ def _pins_cmd(args) -> int:
242
+ from . import pins
243
+ exclude = [int(x, 0) for x in args.exclude.replace(" ", "").split(",") if x]
244
+ say = (lambda t: print(t, file=sys.stderr, flush=True)) if args.json else (lambda t: print(t, flush=True)) # noqa: E731
245
+ hst = _open_host(args.probe)
246
+ try:
247
+ core.take(hst, 10000, owner="oep pins")
248
+ finder = pins.PinFinder(hst, wire=args.wire, power=args.power, exclude=exclude, say=say, probe=args.probe,
249
+ save=args.save, slot=args.slot, name=args.name)
250
+ report = finder.run(steps=tuple(s for s in args.steps.split(",") if s))
251
+ say(f"done in {report.elapsed_s:.1f} s")
252
+ if args.json:
253
+ print(json.dumps(report.as_dict(), indent=2))
254
+ try:
255
+ hst.end()
256
+ except host.OepError:
257
+ pass
258
+ except host.Rejected as e:
259
+ print(f"refused: {e}", file=sys.stderr)
260
+ return 2
261
+ finally:
262
+ hst.link.close()
263
+ return 0 if report.found else 1
264
+
265
+
91
266
  # ---- oep linktest -------------------------------------------------------------------------------------------------
92
267
 
93
268
  def _linktest(args) -> int:
@@ -97,8 +272,8 @@ def _linktest(args) -> int:
97
272
  from . import linktest
98
273
  ints = lambda text: [int(v) for v in text.split(",") if v.strip()] # noqa: E731
99
274
  rates = [None if v.strip() in ("0", "now") else int(v) for v in args.rates.split(",") if v.strip()] or [None]
100
- hst = link.open_host(args.probe, baud=args.baud) if args.probe.startswith("/") or ":" not in args.probe \
101
- 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)
102
277
  stream = getattr(hst.link, "stream", None)
103
278
  if hasattr(stream, "set_low_latency_mode"):
104
279
  try:
@@ -147,11 +322,11 @@ def _speed(args) -> int:
147
322
  candidates = [int(r) for r in text.replace(" ", "").split(",") if r] or list(link.DEFAULT_CANDIDATES)
148
323
  flows = _flows(args.flows)
149
324
  verify = True if args.verify or flows else (False if args.minimal else None)
150
- hst = link.open_host(args.probe, baud=args.baud)
325
+ hst = _open_host(args.probe, baud=args.baud)
151
326
  try:
152
- core.take(hst, 5000, owner="oep speed")
327
+ core.take(hst, max(5000, link.lease_for(candidates, flows, verify)), owner="oep speed")
153
328
  report = link.raise_speed(hst, candidates, flows=flows, verify=verify, frames=args.frames,
154
- record=not args.no_record)
329
+ record=not args.no_record, max_tries=args.max_tries)
155
330
  if args.json:
156
331
  sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
157
332
  else:
@@ -187,21 +362,18 @@ def _config_parser(sub) -> None:
187
362
  help="the line's ceiling in Hz for the probe's own attach (0: none); the target's, e.g. 1000000")
188
363
  slot.add_argument("--idle-clock", choices=sorted(config.IDLE_CLOCK), default="high",
189
364
  help="rvswd: SWCLK while the line rests (the target's: low on CH32L103 / V203)")
190
- slot.add_argument("--lock", help="MASK:VALUE (hex u32) the target_id (WCH DMI 0x7F) must match, e.g. ffffff0f:035e0600")
191
365
  slot.add_argument("--save", action="store_true", help="save after the change")
192
- 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)")
193
367
  bind.add_argument("probe")
194
368
  bind.add_argument("--port", type=int, required=True, help="the serial port (its transport index, see show)")
195
- bind.add_argument("--mode", choices=sorted(config.MODE), default="last-reset")
196
- bind.add_argument("--stream", action="append", required=True, help="slot:NAME, slot:N or uart:FN (repeatable)")
197
- 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")
198
370
  bind.add_argument("--save", action="store_true")
199
371
  plan = cs.add_parser("plan", help="the pins an interface keeps (its whole plan, kept as a setting)")
200
372
  plan.add_argument("probe")
201
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)")
202
374
  plan.add_argument("roles", nargs="+", help="ROLE=CHANNEL, ROLE a number or the interface's role name (rx, tx, line...)")
203
375
  plan.add_argument("--save", action="store_true")
204
- 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)")
205
377
  label.add_argument("probe")
206
378
  label.add_argument("channel", type=int)
207
379
  label.add_argument("text")
@@ -210,6 +382,10 @@ def _config_parser(sub) -> None:
210
382
  idle.add_argument("probe")
211
383
  idle.add_argument("channel", type=int)
212
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")
213
389
  idle.add_argument("--save", action="store_true")
214
390
  dis = cs.add_parser("disable", help="channels the probe never uses or touches (not on this board)")
215
391
  dis.add_argument("probe")
@@ -226,6 +402,23 @@ def _config_parser(sub) -> None:
226
402
  rm.add_argument("kind", choices=sorted(config.ITEM))
227
403
  rm.add_argument("key", type=int)
228
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")
229
422
  for name in ("save", "erase"):
230
423
  cs.add_parser(name, help=f"{name} the stored settings").add_argument("probe")
231
424
 
@@ -298,13 +491,16 @@ def _change(hst, cfg, items, save: bool) -> None:
298
491
  h = cfg.set(items)
299
492
  print(f"set: hash 0x{h:08x}")
300
493
  if save:
301
- 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")
302
498
  finally:
303
499
  hst.end()
304
500
 
305
501
 
306
502
  def _config(args) -> int:
307
- hst = link.open_host(args.probe)
503
+ hst = _open_host(args.probe)
308
504
  try:
309
505
  cfg = config.ProbeConfig(hst)
310
506
  if args.action == "show":
@@ -314,19 +510,13 @@ def _config(args) -> int:
314
510
  if args.action == "slot":
315
511
  fn = _wire_fn(hst, args.wire)
316
512
  args.wire = args.wire or str(fn)
317
- lock = None
318
- if args.lock:
319
- mask, _, value = args.lock.partition(":")
320
- lock = (1, struct.pack("<I", int(mask, 16)), struct.pack("<I", int(value, 16)))
321
513
  it = config.Slot(slot=args.slot, wire_fn=fn, pins=_pins(hst, fn, args.pins, args.wire), name=args.name,
322
- attach=args.attach, retry_s=args.retry, mechanism=args.mechanism, lock=lock,
514
+ attach=args.attach, retry_s=args.retry, mechanism=args.mechanism,
323
515
  max_speed=args.max_speed, idle_clock=args.idle_clock)
324
516
  _change(hst, cfg, [it], args.save)
325
517
  elif args.action == "bind":
326
518
  slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
327
- it = config.Bind(port=args.port, mode=args.mode, streams=[_stream(s, slots) for s in args.stream],
328
- selected=args.select)
329
- _change(hst, cfg, [it], args.save)
519
+ _change(hst, cfg, [config.Bind(port=args.port, stream=_stream(args.stream, slots))], args.save)
330
520
  elif args.action == "plan":
331
521
  fn, roles = _plan_fn(hst, args.fn)
332
522
  items = []
@@ -351,11 +541,24 @@ def _config(args) -> int:
351
541
  elif args.action == "label":
352
542
  _change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
353
543
  elif args.action == "idle":
354
- _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)
355
554
  elif args.action == "disable":
356
555
  _change(hst, cfg, [config.Disable(channel=ch) for ch in args.channels], args.save)
357
556
  elif args.action == "remove":
358
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)
359
562
  else:
360
563
  core.take(hst, 3000, owner="oep config")
361
564
  try:
@@ -374,14 +577,68 @@ def _config(args) -> int:
374
577
  hst.link.close()
375
578
 
376
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
+
377
631
  def _plain(o):
632
+ if hasattr(o, "shown"):
633
+ return o.shown() # a wifi item: never its passphrase
378
634
  return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
379
635
  else list(o)
380
636
 
381
637
 
382
638
  def _state_dict(st) -> dict:
383
- return {**{k: v for k, v in vars(st).items() if k not in ("slots", "binds")},
384
- "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 {})}
385
642
 
386
643
 
387
644
  def _state(cfg, as_json: bool) -> int:
@@ -394,13 +651,30 @@ def _state(cfg, as_json: bool) -> int:
394
651
  print(f"storage: {st.storage}{why}, saved hash 0x{st.saved_hash:08x}")
395
652
  for s in st.slots:
396
653
  print(f" slot {s.slot}: {s.state}" + (f", connection {s.connection}" if s.connection else "")
397
- + (f", tried at {s.last_try_at_ns / 1e9:.3f} s" if s.last_try_at_ns is not None else "")
398
- + (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 ""))
399
655
  for b in st.binds:
400
- 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()}")
401
659
  return 0
402
660
 
403
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
+
404
678
  def _show(hst, cfg, as_json: bool) -> int:
405
679
  h, _ = cfg.get()
406
680
  items = cfg.items()
@@ -417,6 +691,7 @@ def _show(hst, cfg, as_json: bool) -> int:
417
691
  print(f"storage: {st.storage}{why} ({decl.storage_bytes} bytes), saved hash 0x{st.saved_hash:08x}; now 0x{h:08x}")
418
692
  print("transports: " + ", ".join(f"{i} {k}" for i, k in kinds.items()))
419
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 {}
420
695
  print(f"slots (up to {decl.slots_max}):")
421
696
  for it in items:
422
697
  if isinstance(it, config.Slot):
@@ -424,26 +699,31 @@ def _show(hst, cfg, as_json: bool) -> int:
424
699
  pins = f"{it.pins[0]}" if it.pins[1] == 0xFFFF else f"{it.pins[0]},{it.pins[1]}"
425
700
  retry = f" retry {it.retry_s:g} s" if it.attach == "at-boot" else ""
426
701
  retry += (f" max {it.max_speed} Hz" if it.max_speed else "") + (" idle-low" if it.idle_clock == "low" else "")
427
- lock = (f" lock {int.from_bytes(it.lock[1], 'little'):08x}:{int.from_bytes(it.lock[2], 'little'):08x}"
428
- if it.lock else "")
429
702
  live = ""
430
703
  if s:
431
704
  tried = "never tried" if s.last_try_at_ns is None else f"tried at {s.last_try_at_ns / 1e9:.3f} s"
432
- 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 ""
433
706
  live = f" -> {s.state}" + (f" (connection {s.connection})" if s.connection else f" ({tried})") + tid
434
- 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}")
435
708
  by_port = {b.port: b for b in st.binds}
436
- print(f"binds (modes: {', '.join(decl.bind_modes) or '-'}):")
709
+ print("binds:")
437
710
  names = {it.slot: it.name for it in items if isinstance(it, config.Slot)}
438
711
  for it in items:
439
712
  if isinstance(it, config.Bind):
440
713
  b = by_port.get(it.port)
441
- streams = ", ".join(f"slot:{names.get(i, i)}" if k == "slot" else f"{k}:{i}" for k, i in it.streams)
442
- sel = f" selected {it.selected}" if it.mode == "manual" else ""
443
- live = f" -> {b.flow}" + (f", carrying {b.selected}" if b and b.selected is not None else "") if b else ""
444
- 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}")
445
725
  for it in items:
446
- if not isinstance(it, (config.Slot, config.Bind)):
726
+ if not isinstance(it, (config.Slot, config.Bind, config.Wifi)):
447
727
  print(f" {it}")
448
728
  return 0
449
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