oep-client-python 0.0.28__py3-none-any.whl → 0.0.30__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 +292 -56
- oep_client/arm.py +5 -2
- oep_client/capture.py +340 -59
- oep_client/catalog.py +75 -30
- oep_client/ch32_flash.py +6 -3
- oep_client/cobs.py +3 -3
- oep_client/config.py +342 -132
- oep_client/console.py +53 -18
- oep_client/core.py +202 -39
- oep_client/discovery.py +457 -0
- oep_client/dump.py +111 -22
- oep_client/endpoint.py +1917 -1075
- oep_client/fixture.py +170 -62
- oep_client/frames.py +17 -8
- oep_client/host.py +384 -100
- oep_client/interfaces.py +91 -28
- oep_client/kept_session.py +162 -0
- oep_client/link.py +1247 -265
- oep_client/linktest.py +39 -28
- oep_client/message.py +83 -82
- oep_client/multirate.py +254 -0
- oep_client/names.py +12 -8
- oep_client/pins.py +57 -37
- oep_client/registry.py +67 -42
- oep_client/riscv.py +289 -50
- oep_client/speed_record.py +64 -21
- oep_client/targets.py +7 -4
- oep_client/uiapduino.py +13 -11
- oep_client/usb_stream.py +2 -2
- oep_client/virtual_bench.py +572 -0
- oep_client/virtual_bench_capture.py +688 -0
- oep_client/virtual_bench_mdns.py +286 -0
- oep_client/{fake_serial.py → virtual_bench_serial.py} +24 -9
- oep_client/virtual_bench_serve.py +802 -0
- oep_client_python-0.0.30.dist-info/METADATA +526 -0
- oep_client_python-0.0.30.dist-info/RECORD +43 -0
- oep_client/fake.py +0 -361
- oep_client/fake_capture.py +0 -493
- oep_client/fake_serve.py +0 -365
- oep_client_python-0.0.28.dist-info/METADATA +0 -253
- oep_client_python-0.0.28.dist-info/RECORD +0 -39
- {oep_client_python-0.0.28.dist-info → oep_client_python-0.0.30.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.28.dist-info → oep_client_python-0.0.30.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.28.dist-info → oep_client_python-0.0.30.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
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 --
|
|
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 --
|
|
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
|
|
13
|
-
|
|
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
|
|
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,
|
|
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 (
|
|
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 §
|
|
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 §
|
|
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
|
|
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=
|
|
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("--
|
|
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
|
-
|
|
99
|
-
|
|
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 =
|
|
164
|
+
hst = _open_host(args.port)
|
|
102
165
|
call = lambda fn, op, payload: hst.request(fn, op, payload, locked=False).payload # noqa: E731
|
|
103
|
-
|
|
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 =
|
|
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 =
|
|
145
|
-
else
|
|
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 =
|
|
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="
|
|
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("--
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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}{
|
|
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(
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
live = f" -> {b.flow}"
|
|
488
|
-
print(f" port {it.port} ({kinds.get(it.port, '?')}): {
|
|
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
|
|
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
|
|