oep-client-python 0.0.21__py3-none-any.whl → 0.0.22__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 +66 -19
- oep_client/arm.py +11 -9
- oep_client/capture.py +157 -49
- oep_client/catalog.py +16 -9
- oep_client/config.py +195 -61
- oep_client/console.py +68 -18
- oep_client/core.py +21 -3
- oep_client/endpoint.py +664 -246
- oep_client/fake.py +46 -28
- oep_client/fake_capture.py +107 -51
- oep_client/fake_serve.py +3 -3
- oep_client/fixture.py +56 -16
- oep_client/host.py +88 -29
- oep_client/interfaces.py +14 -8
- oep_client/link.py +4 -3
- oep_client/message.py +49 -17
- oep_client/registry.py +26 -21
- oep_client/riscv.py +143 -56
- oep_client/usb_stream.py +10 -4
- {oep_client_python-0.0.21.dist-info → oep_client_python-0.0.22.dist-info}/METADATA +16 -12
- oep_client_python-0.0.22.dist-info/RECORD +35 -0
- oep_client_python-0.0.21.dist-info/RECORD +0 -35
- {oep_client_python-0.0.21.dist-info → oep_client_python-0.0.22.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.21.dist-info → oep_client_python-0.0.22.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.21.dist-info → oep_client_python-0.0.22.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/__main__.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""The oep command: what a probe offers (dump) and its settings (config).
|
|
2
2
|
|
|
3
3
|
oep dump --port /run/board-identify/by-id/<probe> oep dump --fake p4-x035 --prefix oep.target --json
|
|
4
|
-
oep config show <probe>
|
|
4
|
+
oep config show <probe> (the settings and what the probe declares; lock-free)
|
|
5
|
+
oep config state <probe> (the live slot / bind / storage state; lock-free)
|
|
5
6
|
oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
|
|
6
7
|
oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
|
|
7
8
|
oep config plan <probe> oep.fixture.uart#1 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
|
|
9
|
+
oep config uart <probe> oep.fixture.uart#1 115200 --format 8N1
|
|
8
10
|
oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
|
|
9
11
|
|
|
10
12
|
<probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
|
|
@@ -52,9 +54,12 @@ def main(argv=None) -> int:
|
|
|
52
54
|
def _config_parser(sub) -> None:
|
|
53
55
|
c = sub.add_parser("config", help="the probe's settings (oep.probe.config): slots, binds, save")
|
|
54
56
|
cs = c.add_subparsers(dest="action", required=True)
|
|
55
|
-
show = cs.add_parser("show", help="the settings and the live slot / bind state")
|
|
57
|
+
show = cs.add_parser("show", help="the settings, what the probe declares, and the live slot / bind state")
|
|
56
58
|
show.add_argument("probe")
|
|
57
59
|
show.add_argument("--json", action="store_true")
|
|
60
|
+
state = cs.add_parser("state", help="the live slot / bind / storage state (op state, lock-free)")
|
|
61
|
+
state.add_argument("probe")
|
|
62
|
+
state.add_argument("--json", action="store_true")
|
|
58
63
|
slot = cs.add_parser("slot", help="register a slot (a place a target is wired to)")
|
|
59
64
|
slot.add_argument("probe")
|
|
60
65
|
slot.add_argument("--slot", type=int, default=0, help="the slot number (default 0)")
|
|
@@ -62,8 +67,9 @@ def _config_parser(sub) -> None:
|
|
|
62
67
|
slot.add_argument("--wire", help="rvswd or swio (or an fn); default: the probe's only wire")
|
|
63
68
|
slot.add_argument("--pins", help="swdio,swclk (one pin on swio); default: the wire's only pin set")
|
|
64
69
|
slot.add_argument("--attach", choices=sorted(config.ATTACH), default="host")
|
|
65
|
-
slot.add_argument("--retry", type=
|
|
66
|
-
slot.add_argument("--mechanism", choices=sorted(config.MECHANISM), default="dmseq"
|
|
70
|
+
slot.add_argument("--retry", type=float, default=0, help="at-boot: try again every N s while absent (0: never)")
|
|
71
|
+
slot.add_argument("--mechanism", choices=sorted(config.MECHANISM), default="dmseq",
|
|
72
|
+
help="the console's mechanism, or none (no console on this slot)")
|
|
67
73
|
slot.add_argument("--max-speed", type=int, default=0,
|
|
68
74
|
help="the line's ceiling in Hz for the probe's own attach (0: none); the target's, e.g. 1000000")
|
|
69
75
|
slot.add_argument("--idle-clock", choices=sorted(config.IDLE_CLOCK), default="high",
|
|
@@ -92,9 +98,15 @@ def _config_parser(sub) -> None:
|
|
|
92
98
|
idle.add_argument("channel", type=int)
|
|
93
99
|
idle.add_argument("mode", choices=sorted(config.IDLE))
|
|
94
100
|
idle.add_argument("--save", action="store_true")
|
|
95
|
-
|
|
101
|
+
uart = cs.add_parser("uart", help="a fixture UART's baud / format, applied whenever its plan has RX or TX")
|
|
102
|
+
uart.add_argument("probe")
|
|
103
|
+
uart.add_argument("fn", help="an fn, or name#instance of the oep.fixture.uart")
|
|
104
|
+
uart.add_argument("baud", type=int)
|
|
105
|
+
uart.add_argument("--format", default="8N1", help="data bits, parity, stop bits: 8N1 (default), 8E1, 7O2 ...")
|
|
106
|
+
uart.add_argument("--save", action="store_true")
|
|
107
|
+
rm = cs.add_parser("remove", help="remove one item (unset): slot N, bind PORT, plan FN, label CH, idle CH, uart FN")
|
|
96
108
|
rm.add_argument("probe")
|
|
97
|
-
rm.add_argument("kind", choices=
|
|
109
|
+
rm.add_argument("kind", choices=sorted(config.ITEM))
|
|
98
110
|
rm.add_argument("key", type=int)
|
|
99
111
|
rm.add_argument("--save", action="store_true")
|
|
100
112
|
for name in ("save", "erase"):
|
|
@@ -108,7 +120,7 @@ def _pins(hst, fn: int, text: str | None, wire: str) -> tuple[int, int]:
|
|
|
108
120
|
groups = []
|
|
109
121
|
for tag, v in core.describe(hst, fn):
|
|
110
122
|
if tag & 0x7F == catalog.CHANNEL_GROUP:
|
|
111
|
-
roles =
|
|
123
|
+
roles = dict(catalog.unpack_channel_group(v)[1])
|
|
112
124
|
groups.append((roles.get(1, 0xFFFF), roles.get(2, 0xFFFF)))
|
|
113
125
|
if len(groups) != 1:
|
|
114
126
|
raise SystemExit(f"{wire}: {len(groups)} pin sets on this probe - name one with --pins")
|
|
@@ -180,6 +192,8 @@ def _config(args) -> int:
|
|
|
180
192
|
cfg = config.ProbeConfig(hst)
|
|
181
193
|
if args.action == "show":
|
|
182
194
|
return _show(hst, cfg, args.json)
|
|
195
|
+
if args.action == "state":
|
|
196
|
+
return _state(cfg, args.json)
|
|
183
197
|
if args.action == "slot":
|
|
184
198
|
fn = _wire_fn(hst, args.wire)
|
|
185
199
|
args.wire = args.wire or str(fn)
|
|
@@ -208,6 +222,15 @@ def _config(args) -> int:
|
|
|
208
222
|
raise SystemExit(f"{role}: not a role of fn {fn} (roles: {', '.join(sorted(roles)) or 'numbers only'})")
|
|
209
223
|
items.append(config.Plan(fn=fn, role=number, channel=int(ch, 0)))
|
|
210
224
|
_change(hst, cfg, items, args.save)
|
|
225
|
+
elif args.action == "uart":
|
|
226
|
+
fn, _ = _plan_fn(hst, args.fn)
|
|
227
|
+
from .fixture import FixtureUart
|
|
228
|
+
fmt = args.format.upper()
|
|
229
|
+
try:
|
|
230
|
+
byte = FixtureUart.format_byte(int(fmt[0]), fmt[1], int(fmt[2]))
|
|
231
|
+
except (KeyError, ValueError, IndexError):
|
|
232
|
+
raise SystemExit(f"--format {args.format}: want <data bits><parity><stop bits>, e.g. 8N1, 8E2, 7O1") from None
|
|
233
|
+
_change(hst, cfg, [config.Uart(fn=fn, baud=args.baud, format=byte)], args.save)
|
|
211
234
|
elif args.action == "label":
|
|
212
235
|
_change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
|
|
213
236
|
elif args.action == "idle":
|
|
@@ -232,42 +255,66 @@ def _config(args) -> int:
|
|
|
232
255
|
hst.link.close()
|
|
233
256
|
|
|
234
257
|
|
|
258
|
+
def _plain(o):
|
|
259
|
+
return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
|
|
260
|
+
else list(o)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _state_dict(st) -> dict:
|
|
264
|
+
return {**{k: v for k, v in vars(st).items() if k not in ("slots", "binds")},
|
|
265
|
+
"slots": [_plain(s) for s in st.slots], "binds": [_plain(b) for b in st.binds]}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _state(cfg, as_json: bool) -> int:
|
|
269
|
+
"""The live state alone (op state, lock-free): what a monitor polls."""
|
|
270
|
+
st = cfg.state()
|
|
271
|
+
if as_json:
|
|
272
|
+
print(json.dumps(_state_dict(st), indent=2, default=lambda o: o.hex() if isinstance(o, bytes) else str(o)))
|
|
273
|
+
return 0
|
|
274
|
+
why = f" ({st.unreadable})" if st.unreadable else ""
|
|
275
|
+
print(f"storage: {st.storage}{why}, saved hash 0x{st.saved_hash:08x}")
|
|
276
|
+
for s in st.slots:
|
|
277
|
+
print(f" slot {s.slot}: {s.state}" + (f", connection {s.connection}" if s.connection else "")
|
|
278
|
+
+ (f", tried at {s.last_try_at_ns / 1e9:.3f} s" if s.last_try_at_ns is not None else "")
|
|
279
|
+
+ (f", target_id {s.target_id[::-1].hex()}" if s.target_id else ""))
|
|
280
|
+
for b in st.binds:
|
|
281
|
+
print(f" port {b.port}: {b.mode}, {b.flow}" + (f", carrying {b.selected}" if b.selected is not None else ""))
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
235
285
|
def _show(hst, cfg, as_json: bool) -> int:
|
|
236
286
|
h, _ = cfg.get()
|
|
237
287
|
items = cfg.items()
|
|
288
|
+
decl = cfg.describe()
|
|
238
289
|
st = cfg.state()
|
|
239
290
|
kinds = {i: _name(k) for i, k, _ in core.transports(hst)}
|
|
240
291
|
if as_json:
|
|
241
|
-
|
|
242
|
-
return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
|
|
243
|
-
else list(o)
|
|
244
|
-
out = {"hash": h, "items": [dict(type=type(i).__name__, **plain(i)) if hasattr(i, "__dict__") else plain(i)
|
|
292
|
+
out = {"hash": h, "items": [dict(type=type(i).__name__, **_plain(i)) if hasattr(i, "__dict__") else _plain(i)
|
|
245
293
|
for i in items],
|
|
246
|
-
"
|
|
247
|
-
"slots": [plain(s) for s in st.slots], "binds": [plain(b) for b in st.binds]},
|
|
248
|
-
"transports": kinds}
|
|
294
|
+
"declared": _plain(decl), "state": _state_dict(st), "transports": kinds}
|
|
249
295
|
print(json.dumps(out, indent=2, default=lambda o: o.hex() if isinstance(o, bytes) else str(o)))
|
|
250
296
|
return 0
|
|
251
|
-
|
|
297
|
+
why = f" ({st.unreadable})" if st.unreadable else ""
|
|
298
|
+
print(f"storage: {st.storage}{why} ({decl.storage_bytes} bytes), saved hash 0x{st.saved_hash:08x}; now 0x{h:08x}")
|
|
252
299
|
print("transports: " + ", ".join(f"{i} {k}" for i, k in kinds.items()))
|
|
253
300
|
by_slot = {s.slot: s for s in st.slots}
|
|
254
|
-
print(f"slots (up to {
|
|
301
|
+
print(f"slots (up to {decl.slots_max}):")
|
|
255
302
|
for it in items:
|
|
256
303
|
if isinstance(it, config.Slot):
|
|
257
304
|
s = by_slot.get(it.slot)
|
|
258
305
|
pins = f"{it.pins[0]}" if it.pins[1] == 0xFFFF else f"{it.pins[0]},{it.pins[1]}"
|
|
259
|
-
retry = f" retry {it.retry_s} s" if it.attach == "at-boot" else ""
|
|
306
|
+
retry = f" retry {it.retry_s:g} s" if it.attach == "at-boot" else ""
|
|
260
307
|
retry += (f" max {it.max_speed} Hz" if it.max_speed else "") + (" idle-low" if it.idle_clock == "low" else "")
|
|
261
308
|
lock = (f" lock {int.from_bytes(it.lock[1], 'little'):08x}:{int.from_bytes(it.lock[2], 'little'):08x}"
|
|
262
309
|
if it.lock else "")
|
|
263
310
|
live = ""
|
|
264
311
|
if s:
|
|
265
|
-
tried = "never tried" if s.
|
|
312
|
+
tried = "never tried" if s.last_try_at_ns is None else f"tried at {s.last_try_at_ns / 1e9:.3f} s"
|
|
266
313
|
tid = f" target_id {s.target_id[::-1].hex()}" if s.target_id else ""
|
|
267
314
|
live = f" -> {s.state}" + (f" (connection {s.connection})" if s.connection else f" ({tried})") + tid
|
|
268
315
|
print(f" {it.slot} {it.name}: fn {it.wire_fn} pins {pins} {it.attach}{retry} {it.mechanism}{lock}{live}")
|
|
269
316
|
by_port = {b.port: b for b in st.binds}
|
|
270
|
-
print(f"binds (modes: {', '.join(
|
|
317
|
+
print(f"binds (modes: {', '.join(decl.bind_modes) or '-'}):")
|
|
271
318
|
names = {it.slot: it.name for it in items if isinstance(it, config.Slot)}
|
|
272
319
|
for it in items:
|
|
273
320
|
if isinstance(it, config.Bind):
|
oep_client/arm.py
CHANGED
|
@@ -29,17 +29,19 @@ class SwdWire(WireBase):
|
|
|
29
29
|
self.existing = False
|
|
30
30
|
|
|
31
31
|
def attach(self, targetsel: int | None = None, max_speed: int | None = None,
|
|
32
|
-
pins: tuple[int, int] | None = None) -> tuple[int, int, bool]:
|
|
33
|
-
"""-> (connection, DPIDR, woke from dormant).
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
pins: tuple[int, int] | None = None, reset: tuple[int, int] | None = None) -> tuple[int, int, bool]:
|
|
33
|
+
"""-> (connection, DPIDR, woke from dormant). The request is method 0 (swd has no halting attach) and TLVs:
|
|
34
|
+
max_speed (required, critical; None: the wire's declared max_clock_hz), targetsel (multidrop, critical), pins,
|
|
35
|
+
reset = (channel, hold_ms) (critical). self.existing: the wire was attached already (its connection returned).
|
|
36
|
+
self.flags: the attach_flags byte."""
|
|
37
|
+
body = bytes([0]) + self._speed_tlv(self._speed_or_default(max_speed)) + self._pins_tlv(pins) + self._reset_tlv(reset)
|
|
36
38
|
if targetsel is not None:
|
|
37
39
|
body += m.tlv(self.TAG_TARGETSEL, struct.pack("<I", targetsel), critical=True)
|
|
38
40
|
rd = m.Reader(self._call(self.ATTACH, body).payload)
|
|
39
|
-
conn, dpidr, flags, self.speed_hz = rd.take("HIBI")
|
|
40
|
-
self.existing = bool(flags &
|
|
41
|
+
conn, dpidr, self.flags, self.speed_hz = rd.take("HIBI")
|
|
42
|
+
self.existing = bool(self.flags & self.FLAGS["existing"])
|
|
41
43
|
rd.tail()
|
|
42
|
-
return conn, dpidr, bool(flags &
|
|
44
|
+
return conn, dpidr, bool(self.flags & self.FLAGS["dormant_woken"])
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
class AdiError(TargetError):
|
|
@@ -82,8 +84,8 @@ class ArmAdi(Interface):
|
|
|
82
84
|
reads = transfer_reads(steps)
|
|
83
85
|
r = self._request(self.TRANSFER, struct.pack("<H", len(reads)) + steps)
|
|
84
86
|
rd = ran(r)
|
|
85
|
-
done, status, self.last_ack = rd.take("
|
|
86
|
-
values = rd.words(
|
|
87
|
+
done, status, self.last_ack, nvals = rd.take("HBBH") # nvals: the values the answer carries (debug §6)
|
|
88
|
+
values = rd.words(nvals)
|
|
87
89
|
rd.tail()
|
|
88
90
|
if status != OK or not r.succeeded or done != len(reads):
|
|
89
91
|
raise AdiError(f"transfer (ack {self.last_ack:#x})", status, r, done=done, values=values)
|
oep_client/capture.py
CHANGED
|
@@ -5,6 +5,11 @@ Times are the probe's one clock (ns since its boot, comparable within one boot_i
|
|
|
5
5
|
probe's known corrections applied. Analog values are always raw; the probe's 1st-order scale, its calibration data and
|
|
6
6
|
its reference are for the host to choose from.
|
|
7
7
|
|
|
8
|
+
Every start begins a new generation (u32, from 1): segment serials and positions count from 0 inside it, and read and
|
|
9
|
+
release name it, so a read sent for the last capture never returns the next one's bytes (oep-if-capture §3.2). The
|
|
10
|
+
client keeps it (`LogicCapture.generation`, from start / status / the group's start) and passes it on; `read_segment`
|
|
11
|
+
takes the segment's own.
|
|
12
|
+
|
|
8
13
|
configure: a TLV the probe cannot honour is refused (rejected unsupported, 0x0B, payload = the tag) when the host marked
|
|
9
14
|
it critical, and otherwise ignored and listed in the answer's ignored TLV (0x7F, oep-core §2.3)."""
|
|
10
15
|
|
|
@@ -24,13 +29,17 @@ _CAP = reg.FIXTURE_LOGIC
|
|
|
24
29
|
_ANA = reg.FIXTURE_ANALOG
|
|
25
30
|
_GRP = reg.FIXTURE_CAPTURE_GROUP
|
|
26
31
|
# configure TLVs; bit 7 of a tag = critical (the probe must reject what it cannot do)
|
|
27
|
-
MODE, RATE, SAMPLES, SEGMENTS, TRIGGER, PRETRIGGER
|
|
28
|
-
_CAP.tlv["configure"][k] for k in ("mode", "rate", "samples", "segments", "trigger", "pretrigger"
|
|
32
|
+
MODE, RATE, SAMPLES, SEGMENTS, TRIGGER, PRETRIGGER = (
|
|
33
|
+
_CAP.tlv["configure"][k] for k in ("mode", "rate", "samples", "segments", "trigger", "pretrigger"))
|
|
34
|
+
FRONTEND = _ANA.tlv["configure"]["frontend"] # analog only
|
|
29
35
|
ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, TIMING, SCALE, BLOCKING, SKEW, FRONTEND_USED, REFERENCE, \
|
|
30
36
|
RATE_ACCURACY = (_ANA.tlv["configure_answer"][k] for k in (
|
|
31
37
|
"actual_rate", "layout", "actual_samples", "actual_segments", "timing", "scale", "blocking_ms", "skew",
|
|
32
38
|
"frontend_used", "reference", "rate_accuracy"))
|
|
33
39
|
FACTORY, VREFINT = _ANA.tlv["calibration_answer"]["factory"], _ANA.tlv["calibration_answer"]["vrefint"]
|
|
40
|
+
STATUS_ERROR = _CAP.tlv["status_answer"]["error"] # status's TLV: why the state is 6
|
|
41
|
+
DATA_GENERATION = _CAP.tlv["data"]["generation"] # a data frame's TLV: its generation (always in streaming)
|
|
42
|
+
GROUP_GENERATIONS = _GRP.tlv["start_answer"]["generations"]
|
|
34
43
|
REFERENCE_SOURCE = {v: k for k, v in _ANA.enum["reference_source"].items()}
|
|
35
44
|
IGNORED = m.TAG_IGNORED
|
|
36
45
|
CRITICAL = m.TAG_CRITICAL
|
|
@@ -38,6 +47,8 @@ ONE_SHOT, REPEAT, STREAMING = (_CAP.enum["mode"][k] for k in ("one_shot", "repea
|
|
|
38
47
|
IMMEDIATE, LEVEL, EDGE, CROSS_UP, CROSS_DOWN = range(5)
|
|
39
48
|
STATE = _CAP.enum["state"]
|
|
40
49
|
SEGMENT_SLIPPED = _CAP.enum["segment_flag"]["slipped"]
|
|
50
|
+
STATUS_FLAGS = _CAP.enum["status_flag"] # dropped, slipped (reset at start)
|
|
51
|
+
ERRORS = {v: k for k, v in _CAP.enum["error"].items()} # state 6's reason
|
|
41
52
|
# events (oep-core §11, role 0x05)
|
|
42
53
|
EVENT_SEGMENT, EVENT_STOPPED, EVENT_TRIGGERED = (_CAP.event[k] for k in ("segment", "stopped", "triggered"))
|
|
43
54
|
|
|
@@ -46,8 +57,8 @@ def tlvs(payload: bytes) -> list[tuple[int, bytes]]:
|
|
|
46
57
|
return m.split_tlvs(payload)
|
|
47
58
|
|
|
48
59
|
|
|
49
|
-
SEGMENT_BYTES =
|
|
50
|
-
# flags u8
|
|
60
|
+
SEGMENT_BYTES = 37 # serial u32, position u64, samples u32, start_ns u64, start_uncertainty_ns u32, trigger_index u32,
|
|
61
|
+
# flags u8, generation u32 (oep-if-capture §2)
|
|
51
62
|
|
|
52
63
|
|
|
53
64
|
@dataclass
|
|
@@ -59,6 +70,7 @@ class Segment:
|
|
|
59
70
|
start_uncertainty_ns: int # +- of start_ns (a guide, not a promise)
|
|
60
71
|
trigger_index: int | None
|
|
61
72
|
flags: int
|
|
73
|
+
generation: int = 0 # the start this segment belongs to (read / release name it)
|
|
62
74
|
|
|
63
75
|
@property
|
|
64
76
|
def slipped(self) -> bool:
|
|
@@ -67,8 +79,34 @@ class Segment:
|
|
|
67
79
|
|
|
68
80
|
@classmethod
|
|
69
81
|
def unpack(cls, b: bytes) -> "Segment":
|
|
70
|
-
serial, position, samples, start_ns, uncertainty, trig, flags = struct.unpack_from("<
|
|
71
|
-
return cls(serial, position, samples, start_ns, uncertainty, None if trig == 0xFFFFFFFF else trig, flags
|
|
82
|
+
serial, position, samples, start_ns, uncertainty, trig, flags, generation = struct.unpack_from("<IQIQIIBI", b)
|
|
83
|
+
return cls(serial, position, samples, start_ns, uncertainty, None if trig == 0xFFFFFFFF else trig, flags,
|
|
84
|
+
generation)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class Status:
|
|
89
|
+
"""status's answer (oep-if-capture §3.2). Iterates as (state, serial_done, write_pos, flags), the first shape."""
|
|
90
|
+
state: int
|
|
91
|
+
serial_done: int # segments finished
|
|
92
|
+
write_pos: int # bytes taken so far (dropped ones counted: the next byte's position)
|
|
93
|
+
flags: int # bit0 dropped, bit1 slipped - since start
|
|
94
|
+
generation: int
|
|
95
|
+
error: int | None = None # state 6: why (ERRORS)
|
|
96
|
+
|
|
97
|
+
def __iter__(self):
|
|
98
|
+
return iter((self.state, self.serial_done, self.write_pos, self.flags))
|
|
99
|
+
|
|
100
|
+
def __getitem__(self, i: int):
|
|
101
|
+
return (self.state, self.serial_done, self.write_pos, self.flags)[i]
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def dropped(self) -> bool:
|
|
105
|
+
return bool(self.flags & STATUS_FLAGS["dropped"])
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def error_name(self) -> str | None:
|
|
109
|
+
return None if self.error is None else ERRORS.get(self.error, f"error 0x{self.error:02x}")
|
|
72
110
|
|
|
73
111
|
|
|
74
112
|
@dataclass
|
|
@@ -124,17 +162,27 @@ class Received:
|
|
|
124
162
|
gaps: list[tuple[int, int]] = field(default_factory=list) # (index in data where it skipped, bytes skipped)
|
|
125
163
|
seq_lost: int = 0 # push frames missing by seq
|
|
126
164
|
frames: int = 0
|
|
165
|
+
stale: int = 0 # pushes of an earlier generation, dropped
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def unpack_push(frame: bytes) -> tuple[int, int, int, bytes, int | None]:
|
|
169
|
+
"""A data frame (core §11.2: role fn seq position(u64) len(u16) data [TLV]) -> (fn, seq, position, data,
|
|
170
|
+
generation): the TLV 0x01 generation, None when the frame carries none."""
|
|
171
|
+
fn, seq, position = struct.unpack_from("<HHQ", frame, 1)
|
|
172
|
+
rd = m.Reader(frame[13:])
|
|
173
|
+
data = rd.counted("H")
|
|
174
|
+
g = rd.tail().get(DATA_GENERATION)
|
|
175
|
+
return fn, seq, position, data, struct.unpack("<I", g)[0] if g is not None and len(g) == 4 else None
|
|
127
176
|
|
|
128
177
|
|
|
129
|
-
def take_pushes(link, fn: int) -> list[tuple[int, int, bytes]]:
|
|
130
|
-
"""Remove this fn's data pushes (role 0x06) from the link: [(seq, position, data)], oldest first."""
|
|
178
|
+
def take_pushes(link, fn: int) -> list[tuple[int, int, bytes, int | None]]:
|
|
179
|
+
"""Remove this fn's data pushes (role 0x06) from the link: [(seq, position, data, generation)], oldest first."""
|
|
131
180
|
mine, rest = [], []
|
|
132
181
|
for f in link.pushes:
|
|
133
182
|
(mine if struct.unpack_from("<H", f, 1)[0] == fn else rest).append(f)
|
|
134
183
|
link.pushes.clear()
|
|
135
184
|
link.pushes.extend(rest)
|
|
136
|
-
|
|
137
|
-
return [(struct.unpack_from("<H", f, 3)[0], struct.unpack_from("<Q", f, 5)[0], f[13:]) for f in mine]
|
|
185
|
+
return [unpack_push(f)[1:] for f in mine]
|
|
138
186
|
|
|
139
187
|
|
|
140
188
|
def _config(payload: bytes, analog: bool) -> Config:
|
|
@@ -183,10 +231,13 @@ class LogicCapture(Interface):
|
|
|
183
231
|
CONFIGURE, START, STOP, FORCE, STATUS, READ, SEGMENTS, RELEASE, QUERY_OP = (
|
|
184
232
|
_CAP.op[k] for k in ("configure", "start", "stop", "force", "status", "read", "segments", "release", "query"))
|
|
185
233
|
|
|
234
|
+
generation: int | None = None # the current capture's generation (start / status / the group's start)
|
|
235
|
+
|
|
186
236
|
def __init__(self, hst: h.Host, fn: int | None = None, name: str | None = None):
|
|
187
237
|
super().__init__(hst, name, fn=fn)
|
|
188
238
|
self.config: Config | None = None
|
|
189
239
|
self.armed_s: float | None = None # time.monotonic() at the last start()
|
|
240
|
+
self.generation = None
|
|
190
241
|
|
|
191
242
|
def configure(self, *, rate: int, mode: int = ONE_SHOT, samples: int | None = None, segments: int | None = None,
|
|
192
243
|
trigger: tuple[int, int, int] | None = None, pretrigger: int | None = None, query: bool = False,
|
|
@@ -201,7 +252,7 @@ class LogicCapture(Interface):
|
|
|
201
252
|
if segments is not None:
|
|
202
253
|
body += tlv(SEGMENTS, struct.pack("<I", segments))
|
|
203
254
|
if trigger is not None:
|
|
204
|
-
body += tlv(TRIGGER, struct.pack("<
|
|
255
|
+
body += tlv(TRIGGER, struct.pack("<BBI", *trigger)) # type, role, value (u32)
|
|
205
256
|
if pretrigger is not None:
|
|
206
257
|
body += tlv(PRETRIGGER, struct.pack("<I", pretrigger))
|
|
207
258
|
for role, fe in sorted((frontends or {}).items()): # analog: the input range per channel (describe frontend)
|
|
@@ -225,8 +276,9 @@ class LogicCapture(Interface):
|
|
|
225
276
|
into: Received | None = None, keepalive_s: float = 1.0) -> Received:
|
|
226
277
|
"""Streaming: collect data pushes until `nbytes` have arrived or `seconds` have passed (at least one is needed).
|
|
227
278
|
A position that does not follow the previous push is a probe-side drop (a gap); a seq that skips is a lost frame.
|
|
228
|
-
|
|
229
|
-
|
|
279
|
+
A push of another generation than this capture's (a leftover of the start before, oep-if-capture §3.4) is
|
|
280
|
+
dropped. The subscription ends with the lock, so the lock is kept alive every `keepalive_s` while collecting (a
|
|
281
|
+
stream longer than the lease otherwise stopped: 35 MB missing at the end of 10 s at 150 MHz with a 10 s lease)."""
|
|
230
282
|
if seconds is None and nbytes is None:
|
|
231
283
|
raise ValueError("stream() needs seconds or nbytes")
|
|
232
284
|
got = into or Received()
|
|
@@ -238,7 +290,7 @@ class LogicCapture(Interface):
|
|
|
238
290
|
for e in link.events:
|
|
239
291
|
if struct.unpack_from("<H", e, 1)[0] == self.fn:
|
|
240
292
|
event_seqs.add(struct.unpack_from("<H", e, 3)[0])
|
|
241
|
-
for seq, position, data in take_pushes(link, self.fn):
|
|
293
|
+
for seq, position, data, generation in take_pushes(link, self.fn):
|
|
242
294
|
while expect_seq is not None and expect_seq != seq:
|
|
243
295
|
if expect_seq in event_seqs:
|
|
244
296
|
event_seqs.discard(expect_seq)
|
|
@@ -246,6 +298,9 @@ class LogicCapture(Interface):
|
|
|
246
298
|
got.seq_lost += 1
|
|
247
299
|
expect_seq = (expect_seq + 1) & 0xFFFF
|
|
248
300
|
expect_seq = (seq + 1) & 0xFFFF
|
|
301
|
+
if generation is not None and self.generation is not None and generation != self.generation:
|
|
302
|
+
got.stale += 1 # the generation before: not this capture's bytes
|
|
303
|
+
continue
|
|
249
304
|
if got.start is None:
|
|
250
305
|
got.start = position
|
|
251
306
|
else:
|
|
@@ -268,7 +323,7 @@ class LogicCapture(Interface):
|
|
|
268
323
|
def finish(self, link, got: Received, timeout: float = 5.0) -> Received:
|
|
269
324
|
"""Streaming, after stop(): collect the pushes still to come, up to the last byte captured (status's write
|
|
270
325
|
position), or until `timeout`."""
|
|
271
|
-
end = self.status()
|
|
326
|
+
end = self.status().write_pos
|
|
272
327
|
deadline = time.monotonic() + timeout
|
|
273
328
|
while time.monotonic() < deadline:
|
|
274
329
|
if got.start is not None:
|
|
@@ -279,8 +334,10 @@ class LogicCapture(Interface):
|
|
|
279
334
|
return got
|
|
280
335
|
|
|
281
336
|
def start(self) -> int:
|
|
282
|
-
"""-> blocking_ms (0: the probe keeps answering while it captures)."""
|
|
283
|
-
|
|
337
|
+
"""-> blocking_ms (0: the probe keeps answering while it captures). self.generation: the new capture's."""
|
|
338
|
+
rd = m.Reader(self._call(self.START).payload)
|
|
339
|
+
blocking, self.generation = rd.take("II")
|
|
340
|
+
rd.tail()
|
|
284
341
|
self.armed_s = time.monotonic()
|
|
285
342
|
return blocking
|
|
286
343
|
|
|
@@ -291,19 +348,44 @@ class LogicCapture(Interface):
|
|
|
291
348
|
"""Waiting for the trigger: start now (the segment's trigger_index marks where)."""
|
|
292
349
|
self._call(self.FORCE)
|
|
293
350
|
|
|
294
|
-
def status(self) ->
|
|
295
|
-
"""-> state, segments done, write position, flags.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
self.
|
|
301
|
-
|
|
302
|
-
|
|
351
|
+
def status(self) -> Status:
|
|
352
|
+
"""-> Status (state, segments done, write position, flags, generation, error). Lock-free; it also brings the
|
|
353
|
+
generation a host that did not start the capture needs for read."""
|
|
354
|
+
rd = m.Reader(self._call(self.STATUS, locked=False).payload)
|
|
355
|
+
state, done, pos, flags, generation = rd.take("BIQBI")
|
|
356
|
+
error = rd.tail().get(STATUS_ERROR)
|
|
357
|
+
self.generation = generation
|
|
358
|
+
return Status(state, done, pos, flags, generation, error[0] if error else None)
|
|
359
|
+
|
|
360
|
+
def release(self, serial: int, generation: int | None = None) -> None:
|
|
361
|
+
"""Repeat: segments up to and including `serial` may be reused (of this generation; another one is rejected
|
|
362
|
+
unavailable). In state 5 (no free segment) the probe goes on by itself once there is room."""
|
|
363
|
+
self._call(self.RELEASE, struct.pack("<II", self._generation(generation), serial))
|
|
364
|
+
|
|
365
|
+
def _generation(self, generation: int | None) -> int:
|
|
366
|
+
if generation is not None:
|
|
367
|
+
return generation
|
|
368
|
+
if self.generation is None:
|
|
369
|
+
self.status() # a host that did not start it: ask
|
|
370
|
+
return self.generation
|
|
371
|
+
|
|
372
|
+
def segments_page(self, from_serial: int = 0) -> tuple[list[Segment], bool]:
|
|
373
|
+
"""One answer's segment records from `from_serial` on. -> (segments, more)."""
|
|
303
374
|
rd = m.Reader(self._call(self.SEGMENTS, struct.pack("<I", from_serial), locked=False).payload)
|
|
304
|
-
|
|
375
|
+
more, count = rd.take("BB")
|
|
376
|
+
out = [Segment.unpack(rd.element().bytes(SEGMENT_BYTES)) for _ in range(count)]
|
|
305
377
|
rd.tail()
|
|
306
|
-
return out
|
|
378
|
+
return out, bool(more)
|
|
379
|
+
|
|
380
|
+
def segments(self, from_serial: int = 0) -> list[Segment]:
|
|
381
|
+
"""Every segment record from `from_serial` on, following `more`."""
|
|
382
|
+
out: list[Segment] = []
|
|
383
|
+
while True:
|
|
384
|
+
page, more = self.segments_page(from_serial)
|
|
385
|
+
out += page
|
|
386
|
+
if not more or not page:
|
|
387
|
+
return out
|
|
388
|
+
from_serial = page[-1].serial + 1
|
|
307
389
|
|
|
308
390
|
def wait(self, timeout: float = 5.0, keepalive_s: float = 1.0) -> list[Segment]:
|
|
309
391
|
"""Poll status until the one-shot is done (or failed). -> its segments. Waiting for a trigger may take longer
|
|
@@ -314,11 +396,12 @@ class LogicCapture(Interface):
|
|
|
314
396
|
if keepalive_s and self.host.session is not None and time.monotonic() - kept >= keepalive_s:
|
|
315
397
|
self.host.keepalive()
|
|
316
398
|
kept = time.monotonic()
|
|
317
|
-
|
|
399
|
+
st = self.status()
|
|
400
|
+
state = st.state
|
|
318
401
|
if state == STATE["done"]:
|
|
319
402
|
return self.segments()
|
|
320
403
|
if state == STATE["error"]:
|
|
321
|
-
raise h.Failed(None, "the capture stopped with an error")
|
|
404
|
+
raise h.Failed(None, f"the capture stopped with an error ({st.error_name or 'unknown'})")
|
|
322
405
|
if state not in STATE.values():
|
|
323
406
|
raise h.ProtocolError(f"capture state {state} is not one this client knows")
|
|
324
407
|
time.sleep(0.002)
|
|
@@ -327,14 +410,25 @@ class LogicCapture(Interface):
|
|
|
327
410
|
READ_TRIES = 4 # batches of reads sent again after the link's own repeat failed too
|
|
328
411
|
BATCH = 16 # frame-sized reads per pipeline; a keepalive between batches when a session is open
|
|
329
412
|
|
|
330
|
-
|
|
331
|
-
|
|
413
|
+
READ_HEAD = 13 # the answer's position(u64) flags(u8) len(u32) in front of the data
|
|
414
|
+
|
|
415
|
+
@staticmethod
|
|
416
|
+
def _read_data(payload: bytes) -> bytes:
|
|
417
|
+
rd = m.Reader(payload)
|
|
418
|
+
rd.take("QB")
|
|
419
|
+
return rd.counted("I") # position flags len(u32) data [TLV]
|
|
420
|
+
|
|
421
|
+
def read(self, position: int, length: int, generation: int | None = None) -> bytes:
|
|
422
|
+
"""Bytes [position, position+length) of the stream, pipelined in frame-sized reads. `generation`: the capture
|
|
423
|
+
they belong to (default: the one this client saw at start / status); the probe refuses another one as
|
|
424
|
+
unavailable (cause 6), so an old read never gets the next capture's bytes.
|
|
332
425
|
|
|
333
426
|
The reads need no lock and go without the session id, so a repeat after a broken reply is simply run again
|
|
334
427
|
(reads are not deduplicated, oep-core §5.2). They do not extend the lease, though: a long read (64 KB over a
|
|
335
428
|
115200 bps UART probe takes seconds) sends a keepalive between batches, or the lease lapsed mid-read, the plan
|
|
336
429
|
went with it (core §9) and the capture read back nothing (2026-09-26, V003 jig)."""
|
|
337
|
-
|
|
430
|
+
g = self._generation(generation)
|
|
431
|
+
chunk = max(1, confirm(self.host)["max_frame"] - m.RESULT_HEADER - self.READ_HEAD)
|
|
338
432
|
offsets = list(range(0, length, chunk))
|
|
339
433
|
out = bytearray()
|
|
340
434
|
for at in range(0, len(offsets), self.BATCH):
|
|
@@ -342,7 +436,8 @@ class LogicCapture(Interface):
|
|
|
342
436
|
self.host.keepalive()
|
|
343
437
|
batch = offsets[at:at + self.BATCH]
|
|
344
438
|
for attempt in range(self.READ_TRIES):
|
|
345
|
-
reqs = [self.request(self.READ, struct.pack("<
|
|
439
|
+
reqs = [self.request(self.READ, struct.pack("<IQI", g, position + off, min(chunk, length - off)))
|
|
440
|
+
for off in batch]
|
|
346
441
|
try:
|
|
347
442
|
replies = self.host.pipeline_calls(reqs, locked=False)
|
|
348
443
|
break
|
|
@@ -352,11 +447,11 @@ class LogicCapture(Interface):
|
|
|
352
447
|
if attempt == self.READ_TRIES - 1:
|
|
353
448
|
raise
|
|
354
449
|
for off, r in zip(batch, replies):
|
|
355
|
-
data = r.payload
|
|
450
|
+
data = self._read_data(r.payload)
|
|
356
451
|
want = min(chunk, length - off)
|
|
357
452
|
while len(data) < want: # a short answer: read on from where it stopped
|
|
358
|
-
more = self._call(self.READ, struct.pack(
|
|
359
|
-
|
|
453
|
+
more = self._read_data(self._call(self.READ, struct.pack(
|
|
454
|
+
"<IQI", g, position + off + len(data), want - len(data)), locked=False).payload)
|
|
360
455
|
if not more:
|
|
361
456
|
raise h.ProtocolError(f"read at {position + off + len(data)} returned nothing")
|
|
362
457
|
data += more
|
|
@@ -364,11 +459,11 @@ class LogicCapture(Interface):
|
|
|
364
459
|
return bytes(out)
|
|
365
460
|
|
|
366
461
|
def read_segment(self, segment: Segment) -> bytes:
|
|
367
|
-
"""The segment's bytes; every Host.on_capture callback gets them as a CaptureRecord (a run
|
|
368
|
-
pytest-embedded-wireskein, without this package knowing it)."""
|
|
462
|
+
"""The segment's bytes (of its generation); every Host.on_capture callback gets them as a CaptureRecord (a run
|
|
463
|
+
recorder, e.g. pytest-embedded-wireskein, without this package knowing it)."""
|
|
369
464
|
c = self.config
|
|
370
465
|
n = (segment.samples * c.width + 7) // 8 if c.width else segment.samples * len(c.order) * c.slot // 8
|
|
371
|
-
data = self.read(segment.position, n)
|
|
466
|
+
data = self.read(segment.position, n, segment.generation or None)
|
|
372
467
|
if self.host.on_capture:
|
|
373
468
|
record = CaptureRecord(self.fn, self.name, c, segment, data, self.armed_s, time.monotonic())
|
|
374
469
|
for callback in list(self.host.on_capture):
|
|
@@ -408,9 +503,11 @@ class LogicCapture(Interface):
|
|
|
408
503
|
class Calibration:
|
|
409
504
|
"""What the probe knows for turning an analog value into a voltage (oep-if-capture §3.8), raw: the probe applies
|
|
410
505
|
none of it. factory: (frontend or None, scheme, raw bytes) - scheme names how to read raw; vrefint: (raw, ns), the
|
|
411
|
-
internal reference measured after the last start
|
|
506
|
+
internal reference measured after the last start, and vrefint_nominal_mv its nominal voltage (what the supply is
|
|
507
|
+
worked back from)."""
|
|
412
508
|
factory: list[tuple[int | None, str, bytes]] = field(default_factory=list)
|
|
413
509
|
vrefint: tuple[int, int] | None = None
|
|
510
|
+
vrefint_nominal_mv: int | None = None
|
|
414
511
|
|
|
415
512
|
|
|
416
513
|
class AnalogCapture(LogicCapture):
|
|
@@ -439,11 +536,14 @@ class AnalogCapture(LogicCapture):
|
|
|
439
536
|
def calibration(self) -> Calibration:
|
|
440
537
|
out = Calibration()
|
|
441
538
|
for tag, v in tlvs(self._call(self.CALIBRATION, locked=False).payload):
|
|
442
|
-
if tag == FACTORY:
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
out.
|
|
539
|
+
if tag == FACTORY: # frontend scheme_len scheme raw_len(u16) raw
|
|
540
|
+
rd = m.Reader(v)
|
|
541
|
+
fe = rd.u8()
|
|
542
|
+
scheme = rd.counted("B").decode("utf-8", "replace")
|
|
543
|
+
out.factory.append((None if fe == 0xFF else fe, scheme, bytes(rd.counted("H"))))
|
|
544
|
+
elif tag == VREFINT: # raw(u32) ns(u64) nominal_mv(u32)
|
|
545
|
+
raw, ns, nominal = struct.unpack_from("<IQI", v)
|
|
546
|
+
out.vrefint, out.vrefint_nominal_mv = (raw, ns), nominal
|
|
447
547
|
return out
|
|
448
548
|
|
|
449
549
|
|
|
@@ -474,11 +574,17 @@ class CaptureGroup(Interface):
|
|
|
474
574
|
self._call(self.BIND, body)
|
|
475
575
|
|
|
476
576
|
def start(self, tracks: list[LogicCapture] = ()) -> tuple[int, int]:
|
|
477
|
-
"""-> (blocking_ms, the group's start_ns). `tracks`: whose armed_s to set (
|
|
478
|
-
|
|
577
|
+
"""-> (blocking_ms, the group's start_ns). `tracks`: whose armed_s and generation to set (the answer's TLV
|
|
578
|
+
generations names each track's new generation; self.generations keeps them by fn)."""
|
|
579
|
+
rd = m.Reader(self._call(self.START).payload)
|
|
580
|
+
blocking, start_ns = rd.take("IQ")
|
|
581
|
+
gens = rd.tail().get(GROUP_GENERATIONS) or b""
|
|
582
|
+
self.generations = {fn: g for fn, g in struct.iter_unpack("<HI", gens[:len(gens) // 6 * 6])}
|
|
479
583
|
now = time.monotonic()
|
|
480
584
|
for t in tracks:
|
|
481
585
|
t.armed_s = now
|
|
586
|
+
if t.fn in self.generations:
|
|
587
|
+
t.generation = self.generations[t.fn]
|
|
482
588
|
return blocking, start_ns
|
|
483
589
|
|
|
484
590
|
def stop(self) -> None:
|
|
@@ -488,7 +594,9 @@ class CaptureGroup(Interface):
|
|
|
488
594
|
self._call(self.FORCE)
|
|
489
595
|
|
|
490
596
|
def status(self) -> GroupStatus:
|
|
491
|
-
|
|
597
|
+
rd = m.Reader(self._call(self.STATUS, locked=False).payload)
|
|
598
|
+
state, start, trig, fn = rd.take("BQQH")
|
|
599
|
+
rd.tail()
|
|
492
600
|
none = self.NO_TIME
|
|
493
601
|
return GroupStatus(state, None if start == none else start, None if trig == none else trig, fn or None)
|
|
494
602
|
|