oep-client-python 0.0.33__py3-none-any.whl → 0.0.36__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 +8 -0
- oep_client/capture.py +353 -28
- oep_client/capture_blocks.py +294 -0
- oep_client/config.py +23 -0
- oep_client/conformance_capture_group.py +1 -1
- oep_client/conformance_registered_ops.py +5 -5
- oep_client/endpoint.py +84 -15
- oep_client/frames.py +3 -2
- oep_client/link.py +7 -0
- oep_client/message.py +21 -0
- oep_client/registry.py +12 -12
- oep_client/riscv.py +14 -1
- oep_client/usb_stream.py +8 -4
- oep_client/virtual_attach_model.py +3 -2
- oep_client/virtual_bench.py +32 -11
- oep_client/virtual_bench_capture.py +417 -44
- oep_client/virtual_bench_serve.py +11 -1
- oep_client/virtual_bench_trigger.py +277 -0
- oep_client/virtual_oep_declaration_model.py +3 -3
- oep_client/virtual_registered_ops_model.py +1 -1
- oep_client/virtual_scan_options_model.py +1 -1
- {oep_client_python-0.0.33.dist-info → oep_client_python-0.0.36.dist-info}/METADATA +1 -1
- {oep_client_python-0.0.33.dist-info → oep_client_python-0.0.36.dist-info}/RECORD +27 -25
- {oep_client_python-0.0.33.dist-info → oep_client_python-0.0.36.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.33.dist-info → oep_client_python-0.0.36.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.33.dist-info → oep_client_python-0.0.36.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/__main__.py
CHANGED
|
@@ -359,6 +359,12 @@ def _config_parser(sub) -> None:
|
|
|
359
359
|
rest.add_argument("channel", type=int, help="the port's SWCLK channel")
|
|
360
360
|
rest.add_argument("mode", choices=sorted(config.REST_MODE))
|
|
361
361
|
rest.add_argument("--save", action="store_true")
|
|
362
|
+
hold = cs.add_parser("wire-hold", help="whether a debug wire's lines stay held at their rest after a close "
|
|
363
|
+
"(on, the default) or go free at once (off; debug §2)")
|
|
364
|
+
hold.add_argument("probe")
|
|
365
|
+
hold.add_argument("channel", type=int, help="the port's SWDIO channel")
|
|
366
|
+
hold.add_argument("mode", choices=sorted(config.HOLD_MODE))
|
|
367
|
+
hold.add_argument("--save", action="store_true")
|
|
362
368
|
dis = cs.add_parser("disable", help="channels the probe never drives or sets up (not on this board)")
|
|
363
369
|
dis.add_argument("probe")
|
|
364
370
|
dis.add_argument("channels", type=int, nargs="+")
|
|
@@ -441,6 +447,8 @@ def _config(args) -> int:
|
|
|
441
447
|
_change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
|
|
442
448
|
elif args.action == "idle":
|
|
443
449
|
_change(hst, cfg, [config.Idle(channel=args.channel, mode=args.mode)], args.save)
|
|
450
|
+
elif args.action == "wire-hold":
|
|
451
|
+
_change(hst, cfg, [config.WireHold(channel=args.channel, mode=args.mode)], args.save)
|
|
444
452
|
elif args.action == "rvswd-rest":
|
|
445
453
|
_change(hst, cfg, [config.RvswdRest(channel=args.channel, mode=args.mode)], args.save)
|
|
446
454
|
elif args.action == "disable":
|
oep_client/capture.py
CHANGED
|
@@ -35,19 +35,26 @@ from __future__ import annotations
|
|
|
35
35
|
import struct
|
|
36
36
|
import time
|
|
37
37
|
import zipfile
|
|
38
|
+
from typing import NamedTuple
|
|
38
39
|
from dataclasses import dataclass, field
|
|
39
40
|
from fractions import Fraction
|
|
40
41
|
|
|
41
|
-
from . import cobs, host as h, message as m, registry as reg
|
|
42
|
+
from . import capture_blocks, cobs, host as h, message as m, registry as reg
|
|
42
43
|
from .frames import FramingLost
|
|
43
|
-
from .core import Interface, confirm
|
|
44
|
+
from .core import Interface, confirm, describe
|
|
44
45
|
|
|
45
46
|
_CAP = reg.FIXTURE_LOGIC
|
|
46
47
|
_ANA = reg.FIXTURE_ANALOG
|
|
47
48
|
_GRP = reg.FIXTURE_CAPTURE_GROUP
|
|
48
49
|
# configure TLVs; bit 7 of a tag = critical (the probe must reject what it cannot do)
|
|
49
|
-
MODE, RATE, SAMPLES, SEGMENTS, TRIGGER, PRETRIGGER = (
|
|
50
|
-
_CAP.tlv["configure"][k] for k in ("mode", "rate", "samples", "segments", "trigger", "pretrigger"
|
|
50
|
+
MODE, RATE, SAMPLES, SEGMENTS, TRIGGER, PRETRIGGER, TRIGGER_OPTS, FORMAT, CARRIAGE, BLOCK_MAX = (
|
|
51
|
+
_CAP.tlv["configure"][k] for k in ("mode", "rate", "samples", "segments", "trigger", "pretrigger", "trigger_opts",
|
|
52
|
+
"format", "carriage", "block_max"))
|
|
53
|
+
ANSWER_FORMAT, ANSWER_BLOCK = _CAP.tlv["configure_answer"]["format"], _CAP.tlv["configure_answer"]["block"]
|
|
54
|
+
STREAM_BUFFER = _CAP.tlv["configure_answer"]["stream_buffer"]
|
|
55
|
+
JUDGE_LATENCY = _CAP.tlv["configure_answer"]["judge_latency_ns"]
|
|
56
|
+
DATA_FORMAT = _CAP.enum["data_format"]
|
|
57
|
+
LOGIC_CARRIAGE = _CAP.enum["logic_carriage"]
|
|
51
58
|
FRONTEND = _ANA.tlv["configure"]["frontend"] # analog only
|
|
52
59
|
ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, SCALE, SKEW, FRONTEND_USED, REFERENCE = (
|
|
53
60
|
_ANA.tlv["configure_answer"][k] for k in (
|
|
@@ -60,7 +67,8 @@ REFERENCE_SOURCE = {v: k for k, v in _ANA.enum["reference_source"].items()}
|
|
|
60
67
|
REFERENCE_MEASURED = {_ANA.enum["reference_how"]["nominal"]: False, _ANA.enum["reference_how"]["measured"]: True}
|
|
61
68
|
CRITICAL = m.TAG_CRITICAL
|
|
62
69
|
ONE_SHOT, REPEAT, STREAMING = (_CAP.enum["mode"][k] for k in ("one_shot", "repeat", "streaming"))
|
|
63
|
-
IMMEDIATE, LEVEL, EDGE,
|
|
70
|
+
IMMEDIATE, LEVEL, EDGE, LINKED = (_CAP.enum["trigger"][k] for k in ("immediate", "level", "edge", "linked"))
|
|
71
|
+
CROSS_UP, CROSS_DOWN = _ANA.enum["trigger"]["cross_up"], _ANA.enum["trigger"]["cross_down"]
|
|
64
72
|
STATE = _CAP.enum["state"]
|
|
65
73
|
SEGMENT_SLIPPED = _CAP.enum["segment_flag"]["slipped"]
|
|
66
74
|
STATUS_FLAGS = _CAP.enum["status_flag"] # dropped, slipped (reset at start)
|
|
@@ -123,6 +131,7 @@ class Status:
|
|
|
123
131
|
flags: int # bit0 dropped, bit1 slipped - since start
|
|
124
132
|
generation: int
|
|
125
133
|
error: int | None = None # state 6: why (ERRORS)
|
|
134
|
+
named: dict[str, bytes] = field(default_factory=dict) # named extensions (core §2.2): a probe's own diagnostics
|
|
126
135
|
|
|
127
136
|
def __iter__(self):
|
|
128
137
|
return iter((self.state, self.serial_done, self.write_pos, self.flags))
|
|
@@ -157,6 +166,21 @@ class Config:
|
|
|
157
166
|
frontend: dict[int, int] = field(default_factory=dict) # analog: the frontend each channel took
|
|
158
167
|
reference: tuple[str, int, bool | str] | None = None # analog: (source, mV, measured; an unknown how as str)
|
|
159
168
|
pretrigger: int = 0 # what this host asked
|
|
169
|
+
format: int = 0 # the data format in use (§1; absent: 0)
|
|
170
|
+
block: int = 0 # format 1 / 2: L
|
|
171
|
+
stream_buffer: int | None = None # streaming: the most bytes held unsent (§3.3; None: not said)
|
|
172
|
+
judge_latency_ns: int | None = None # judging a group's trigger source condition, the longest to know it (§3.3, §4.4)
|
|
173
|
+
raw_roles: list[int] = field(default_factory=list) # format 1: the raw channels' roles (as asked)
|
|
174
|
+
event_d: dict[int, int] = field(default_factory=dict) # format 1: events role -> D (as asked)
|
|
175
|
+
|
|
176
|
+
def tolerable_delay_s(self, worst_bytes_per_s: float | None = None) -> float | None:
|
|
177
|
+
"""Streaming: how long the host may stop reading before the probe drops (§3.3: stream_buffer over the data rate;
|
|
178
|
+
formats 1 / 2: pass their worst bandwidth, a lower bound). None when the probe does not say."""
|
|
179
|
+
if self.stream_buffer is None:
|
|
180
|
+
return None
|
|
181
|
+
rate = worst_bytes_per_s if worst_bytes_per_s is not None else float(self.rate) * (
|
|
182
|
+
self.width / 8 if self.width else len(self.order) * self.slot / 8)
|
|
183
|
+
return self.stream_buffer / rate if rate else None
|
|
160
184
|
|
|
161
185
|
def segment_bytes(self, samples: int) -> int:
|
|
162
186
|
"""Length of a segment of `samples` samples in the stream (§1.1 rule 4, §1.2 rule 3)."""
|
|
@@ -272,7 +296,7 @@ def take_pushes(link, fn: int) -> list[tuple[int, int, bytes, int | None, Segmen
|
|
|
272
296
|
class Event:
|
|
273
297
|
"""A capture event (oep-if-capture §3.4, §4.2; core §10.2 role 0x03). `generation`: the start it was made in - a
|
|
274
298
|
track's for a track's event, the group's for the group's stopped. kind "segment" has `segment`; "stopped" has
|
|
275
|
-
`reason` and `error`; "triggered" has `serial`, `trigger_index` and `
|
|
299
|
+
`reason` and `error`; "triggered" has `serial`, `trigger_index`, `record`, `sample`, `trigger_ns` and `uncertainty_ns`. An event of another kind (or a
|
|
276
300
|
group's other than stopped) keeps its fixed part in `raw` and generation None."""
|
|
277
301
|
fn: int
|
|
278
302
|
seq: int
|
|
@@ -284,6 +308,10 @@ class Event:
|
|
|
284
308
|
serial: int | None = None
|
|
285
309
|
trigger_index: int | None = None
|
|
286
310
|
trigger_ns: int | None = None
|
|
311
|
+
record: int | None = None # triggered: the trigger record's serial (None: no record, §3.10)
|
|
312
|
+
sample: int | None = None # triggered: the original sample from the generation's start
|
|
313
|
+
uncertainty_ns: int | None = None # triggered: trigger_ns's +-ns
|
|
314
|
+
trigger_fn: int | None = None # a group's triggered: the track that judged it (0: force, timer)
|
|
287
315
|
raw: bytes = b""
|
|
288
316
|
|
|
289
317
|
@property
|
|
@@ -309,9 +337,14 @@ def unpack_event(frame: bytes, group: bool = False) -> Event:
|
|
|
309
337
|
elif kind == EVENT_STOPPED:
|
|
310
338
|
reason, error, generation = rd.take("BBI")
|
|
311
339
|
e = Event(fn, seq, "stopped", generation, reason=reason, error=error)
|
|
340
|
+
elif kind == EVENT_TRIGGERED and group: # §4.4: trigger_fn record trigger_ns uncertainty generation
|
|
341
|
+
trigger_fn, record, trigger_ns, uncertainty, generation = rd.take("HIQII")
|
|
342
|
+
e = Event(fn, seq, "triggered", generation, trigger_ns=trigger_ns, uncertainty_ns=uncertainty,
|
|
343
|
+
record=None if record == NO_RECORD else record, trigger_fn=trigger_fn)
|
|
312
344
|
elif kind == EVENT_TRIGGERED and not group:
|
|
313
|
-
serial, index, trigger_ns, generation = rd.take("
|
|
314
|
-
e = Event(fn, seq, "triggered", generation, serial=serial, trigger_index=index, trigger_ns=trigger_ns
|
|
345
|
+
serial, index, record, sample, trigger_ns, uncertainty, generation = rd.take("IIIQQII")
|
|
346
|
+
e = Event(fn, seq, "triggered", generation, serial=serial, trigger_index=index, trigger_ns=trigger_ns,
|
|
347
|
+
record=None if record == NO_RECORD else record, sample=sample, uncertainty_ns=uncertainty)
|
|
315
348
|
else:
|
|
316
349
|
return Event(fn, seq, f"kind 0x{kind:02x}", None, raw=bytes(body))
|
|
317
350
|
except m.ShortPayload as err:
|
|
@@ -320,6 +353,73 @@ def unpack_event(frame: bytes, group: bool = False) -> Event:
|
|
|
320
353
|
return e
|
|
321
354
|
|
|
322
355
|
|
|
356
|
+
NO_RECORD = 0xFFFFFFFF
|
|
357
|
+
TRIGGER_RECORD_BYTES = 24 # serial u32, sample u64, trigger_ns u64, uncertainty_ns u32 (§3.10)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
CAPTURE_DROPS = "io.github.open-embedded-probe.capture-drops"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@dataclass(frozen=True)
|
|
364
|
+
class CaptureDrops:
|
|
365
|
+
"""The Arduino reference firmware's own diagnostics in a capture's status (named extension CAPTURE_DROPS, not an OEP
|
|
366
|
+
definition): where data a stream dropped went, since start - bytes no free stage took, bytes the transport would not
|
|
367
|
+
queue, DMA chunks rewritten before they were copied (a hole: the track stops in error), chunks the harvest queue had
|
|
368
|
+
no room for - then the way the data goes (1 the zero-copy stages, 0 the store), the stages held, and the memory a
|
|
369
|
+
capture draws on: internal RAM's and DMA-capable internal RAM's largest free blocks, PSRAM's total (0: none); then
|
|
370
|
+
the most the stream's buffer (the DMA ring, in PSRAM on a P4) held not yet sent since start (bytes; None from a
|
|
371
|
+
firmware before it)."""
|
|
372
|
+
no_stage_bytes: int
|
|
373
|
+
unqueued_bytes: int
|
|
374
|
+
dma_overruns: int
|
|
375
|
+
queue_overflows: int
|
|
376
|
+
zero_copy: bool
|
|
377
|
+
stages: int
|
|
378
|
+
internal_largest: int
|
|
379
|
+
dma_largest: int
|
|
380
|
+
psram_total: int
|
|
381
|
+
buffer_peak: int | None = None
|
|
382
|
+
|
|
383
|
+
FORMAT = "<IIIIBBIIII"
|
|
384
|
+
FORMAT_BEFORE_PEAK = "<IIIIBBIII" # a firmware before buffer_peak (until 2026-10-20)
|
|
385
|
+
|
|
386
|
+
@classmethod
|
|
387
|
+
def unpack(cls, data: bytes) -> "CaptureDrops | None":
|
|
388
|
+
"""-> its fields, or None for data of another length (a form this host does not know)."""
|
|
389
|
+
for form in (cls.FORMAT, cls.FORMAT_BEFORE_PEAK):
|
|
390
|
+
if len(data) == struct.calcsize(form):
|
|
391
|
+
f = struct.unpack(form, data)
|
|
392
|
+
return cls(*f[:4], bool(f[4]), *f[5:])
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
@classmethod
|
|
396
|
+
def of(cls, status: "Status") -> "CaptureDrops | None":
|
|
397
|
+
data = status.named.get(CAPTURE_DROPS)
|
|
398
|
+
return None if data is None else cls.unpack(data)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@dataclass(frozen=True)
|
|
402
|
+
class TriggerOpts:
|
|
403
|
+
"""configure's trigger_opts (§3.3): `arm` 1 looks once the pretrigger is there (0: at once, a short segment);
|
|
404
|
+
`rearm` 1 a trigger for every segment (streaming: a record for every event, §2.1, §3.10); `when` 1 a level
|
|
405
|
+
already true at arming counts; `hysteresis` (analog cross, in ADC values) back that far before the next crossing.
|
|
406
|
+
The defaults are every probe's; the others as describe's trigger opts says."""
|
|
407
|
+
arm: int = 1
|
|
408
|
+
rearm: int = 0
|
|
409
|
+
when: int = 0
|
|
410
|
+
hysteresis: int = 0
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@dataclass(frozen=True)
|
|
414
|
+
class TriggerRecord:
|
|
415
|
+
"""A trigger record (§3.10): serial (from 0 in the generation), the original sample from the generation's start,
|
|
416
|
+
trigger_ns and its uncertainty."""
|
|
417
|
+
serial: int
|
|
418
|
+
sample: int
|
|
419
|
+
trigger_ns: int
|
|
420
|
+
uncertainty_ns: int
|
|
421
|
+
|
|
422
|
+
|
|
323
423
|
def take_events(link, fn: int) -> list[bytes]:
|
|
324
424
|
"""Remove this fn's event frames (role 0x03) from the link, oldest first."""
|
|
325
425
|
mine, rest = [], []
|
|
@@ -337,7 +437,8 @@ def _config(payload: bytes, analog: bool) -> Config:
|
|
|
337
437
|
c = Config()
|
|
338
438
|
seen: set[int] = set()
|
|
339
439
|
for tag, v in tlvs(payload):
|
|
340
|
-
if tag in (ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, REFERENCE
|
|
440
|
+
if tag in (ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, REFERENCE, ANSWER_FORMAT, ANSWER_BLOCK, STREAM_BUFFER,
|
|
441
|
+
JUDGE_LATENCY):
|
|
341
442
|
if tag in seen:
|
|
342
443
|
continue
|
|
343
444
|
seen.add(tag)
|
|
@@ -350,6 +451,14 @@ def _config(payload: bytes, analog: bool) -> Config:
|
|
|
350
451
|
elif tag == LAYOUT and analog and len(v) >= 4 and len(v) == 4 + v[3]:
|
|
351
452
|
c.slot, c.offset, c.bits, n = v[0], v[1], v[2], v[3]
|
|
352
453
|
c.order = list(v[4:4 + n])
|
|
454
|
+
elif tag == ANSWER_FORMAT and len(v) == 1:
|
|
455
|
+
c.format = v[0]
|
|
456
|
+
elif tag == ANSWER_BLOCK and len(v) == 4:
|
|
457
|
+
c.block = struct.unpack("<I", v)[0]
|
|
458
|
+
elif tag == STREAM_BUFFER and len(v) == 4:
|
|
459
|
+
c.stream_buffer = struct.unpack("<I", v)[0]
|
|
460
|
+
elif tag == JUDGE_LATENCY and len(v) == 4:
|
|
461
|
+
c.judge_latency_ns = struct.unpack("<I", v)[0]
|
|
353
462
|
elif tag == ACTUAL_SAMPLES and len(v) == 4:
|
|
354
463
|
c.samples = struct.unpack("<I", v)[0]
|
|
355
464
|
elif tag == ACTUAL_SEGMENTS and len(v) == 4:
|
|
@@ -376,8 +485,9 @@ class LogicCapture(Interface):
|
|
|
376
485
|
NAME = "oep.fixture.logic"
|
|
377
486
|
REVISION = 1
|
|
378
487
|
ANALOG = False
|
|
379
|
-
CONFIGURE, START, STOP, FORCE, STATUS, READ, SEGMENTS, RELEASE, QUERY_OP = (
|
|
380
|
-
_CAP.op[k] for k in ("configure", "start", "stop", "force", "status", "read", "segments", "release", "query"
|
|
488
|
+
CONFIGURE, START, STOP, FORCE, STATUS, READ, SEGMENTS, RELEASE, QUERY_OP, TRIGGERS = (
|
|
489
|
+
_CAP.op[k] for k in ("configure", "start", "stop", "force", "status", "read", "segments", "release", "query",
|
|
490
|
+
"triggers"))
|
|
381
491
|
|
|
382
492
|
generation: int | None = None # the current capture's generation (start / status / the group's start)
|
|
383
493
|
|
|
@@ -389,11 +499,17 @@ class LogicCapture(Interface):
|
|
|
389
499
|
|
|
390
500
|
def configure(self, *, rate: int, mode: int = ONE_SHOT, samples: int | None = None, segments: int | None = None,
|
|
391
501
|
trigger: tuple[int, int, int] | None = None, pretrigger: int | None = None, query: bool = False,
|
|
392
|
-
frontends: dict[int, int] | None = None
|
|
502
|
+
frontends: dict[int, int] | None = None, trigger_opts: TriggerOpts | None = None,
|
|
503
|
+
data_format: int = 0, events: dict[int, int] | None = None, roles: list[int] | None = None,
|
|
504
|
+
block_max: int | None = None) -> Config:
|
|
393
505
|
"""-> the probe's actual values. A value the probe cannot honour is refused: host.Unsupported, .tag = the TLV as
|
|
394
506
|
sent (§3.3). The TLVs go without the critical bit (§3.3). Read Config.samples / .segments: the probe rounds
|
|
395
507
|
samples down.
|
|
396
508
|
|
|
509
|
+
Format 1 (logic blocks, §1.3): `data_format` DATA_FORMAT["logic_blocks"], `events` role -> D for the channels
|
|
510
|
+
carried as events (the others raw), `roles` the plan's roles (to name the raw channels), `block_max` the
|
|
511
|
+
longest block. Format 4 (test counter) needs nothing else. These go critical.
|
|
512
|
+
|
|
397
513
|
§3.3's contract is checked before anything is sent (ValueError): `samples` is needed in one-shot and repeat
|
|
398
514
|
and not taken in streaming, `segments` is repeat's only, `pretrigger` needs a trigger (type other than 0) - a
|
|
399
515
|
pretrigger of 0 without one is simply not sent."""
|
|
@@ -426,10 +542,24 @@ class LogicCapture(Interface):
|
|
|
426
542
|
body += tlv(PRETRIGGER, struct.pack("<I", pretrigger))
|
|
427
543
|
for role, fe in sorted((frontends or {}).items()): # analog: the input range per channel (describe frontend)
|
|
428
544
|
body += tlv(FRONTEND, bytes([role, fe]))
|
|
545
|
+
if data_format: # critical: a probe without it refuses (§3.3)
|
|
546
|
+
body += m.tlv(FORMAT | CRITICAL, bytes([data_format]))
|
|
547
|
+
if data_format == DATA_FORMAT["logic_blocks"] and roles is None:
|
|
548
|
+
raise ValueError("capture configure: format 1 needs the plan's roles (to name the raw channels)")
|
|
549
|
+
for role, d in sorted((events or {}).items()):
|
|
550
|
+
body += m.tlv(CARRIAGE | CRITICAL, struct.pack("<BBI", role, LOGIC_CARRIAGE["events"], d))
|
|
551
|
+
if block_max is not None:
|
|
552
|
+
body += m.tlv(BLOCK_MAX | CRITICAL, struct.pack("<I", block_max))
|
|
553
|
+
if trigger_opts is not None: # critical: a probe without it refuses (§3.3)
|
|
554
|
+
o = trigger_opts
|
|
555
|
+
body += m.tlv(TRIGGER_OPTS | CRITICAL, struct.pack("<BBBI", o.arm, o.rearm, o.when, o.hysteresis))
|
|
429
556
|
# query is its own operation: the lock is decided per operation, before the payload is looked at
|
|
430
557
|
op = self.QUERY_OP if query else self.CONFIGURE
|
|
431
558
|
c = _config(self._call(op, body, locked=not query).payload, self.ANALOG)
|
|
432
559
|
c.pretrigger = pretrigger or 0
|
|
560
|
+
if data_format == DATA_FORMAT["logic_blocks"]:
|
|
561
|
+
c.event_d = dict(events or {})
|
|
562
|
+
c.raw_roles = [r for r in sorted(roles) if r not in c.event_d]
|
|
433
563
|
if mode == ONE_SHOT and not c.segments:
|
|
434
564
|
c.segments = 1 # one-shot's answer has no actual_segments (§3.3)
|
|
435
565
|
if not query:
|
|
@@ -468,13 +598,15 @@ class LogicCapture(Interface):
|
|
|
468
598
|
A position that does not follow the previous push is a probe-side drop (a gap); a seq that skips is a lost frame.
|
|
469
599
|
A push of another generation than this capture's (a leftover of the start before, oep-if-capture §3.4) is
|
|
470
600
|
dropped; each push's segment_info goes to `segments`. The subscription ends with the lock, so the lock is kept alive every `keepalive_s` while collecting (a
|
|
471
|
-
stream longer than the lease otherwise stopped: 35 MB missing at the end of 10 s at 150 MHz with a 10 s lease)
|
|
601
|
+
stream longer than the lease otherwise stopped: 35 MB missing at the end of 10 s at 150 MHz with a 10 s lease) -
|
|
602
|
+
counted across calls with the same `into` (a host calling every 0.2 s lost its 30 s lease: each call began the
|
|
603
|
+
count again)."""
|
|
472
604
|
if seconds is None and nbytes is None:
|
|
473
605
|
raise ValueError("stream() needs seconds or nbytes")
|
|
474
606
|
got = into or Received()
|
|
475
607
|
deadline = time.monotonic() + seconds if seconds is not None else None
|
|
476
|
-
kept = time.monotonic()
|
|
477
|
-
expect_seq = getattr(got, "_seq", None)
|
|
608
|
+
kept = getattr(got, "_kept", None) or time.monotonic() # on from the last call with the same `into`: a host
|
|
609
|
+
expect_seq = getattr(got, "_seq", None) # calling in short slices still keeps the lease
|
|
478
610
|
event_seqs = getattr(got, "_events", set()) # events share the fn's seq (they stay on the link for the caller)
|
|
479
611
|
while True:
|
|
480
612
|
for e in link.events:
|
|
@@ -506,6 +638,7 @@ class LogicCapture(Interface):
|
|
|
506
638
|
if keepalive_s and time.monotonic() - kept >= keepalive_s:
|
|
507
639
|
self.host.keepalive()
|
|
508
640
|
kept = time.monotonic()
|
|
641
|
+
got._kept = kept
|
|
509
642
|
if nbytes is not None and len(got.data) >= nbytes:
|
|
510
643
|
return got
|
|
511
644
|
if deadline is not None and time.monotonic() >= deadline:
|
|
@@ -559,9 +692,11 @@ class LogicCapture(Interface):
|
|
|
559
692
|
generation a host that did not start the capture needs for read."""
|
|
560
693
|
rd = m.Reader(self._call(self.STATUS, locked=False).payload)
|
|
561
694
|
state, done, pos, flags, generation = rd.take("BIQBI")
|
|
562
|
-
|
|
695
|
+
tail = rd.tail()
|
|
696
|
+
error = tail.get(STATUS_ERROR) # u8: another length is broken, not used (core §2.3)
|
|
563
697
|
self.generation = generation
|
|
564
|
-
return Status(state, done, pos, flags, generation, error[0] if error is not None and len(error) == 1 else None
|
|
698
|
+
return Status(state, done, pos, flags, generation, error[0] if error is not None and len(error) == 1 else None,
|
|
699
|
+
tail.named())
|
|
565
700
|
|
|
566
701
|
def release(self, serial: int, generation: int | None = None) -> None:
|
|
567
702
|
"""Repeat: segments up to and including `serial` may be reused (of this generation; another one is rejected
|
|
@@ -584,6 +719,25 @@ class LogicCapture(Interface):
|
|
|
584
719
|
rd.tail()
|
|
585
720
|
return out, bool(more)
|
|
586
721
|
|
|
722
|
+
def triggers_page(self, from_serial: int = 0, generation: int | None = None) -> tuple[list[TriggerRecord], bool]:
|
|
723
|
+
"""One answer's trigger records (§3.10) from `from_serial` on, paged as segments are. -> (records, more)."""
|
|
724
|
+
payload = struct.pack("<II", self._generation(generation), from_serial)
|
|
725
|
+
rd = m.Reader(self._call(self.TRIGGERS, payload, locked=False).payload)
|
|
726
|
+
more, count = rd.take("BB")
|
|
727
|
+
out = [TriggerRecord(*struct.unpack("<IQQI", rd.bytes(TRIGGER_RECORD_BYTES))) for _ in range(count)]
|
|
728
|
+
rd.tail()
|
|
729
|
+
return out, bool(more)
|
|
730
|
+
|
|
731
|
+
def triggers(self, from_serial: int = 0, generation: int | None = None) -> list[TriggerRecord]:
|
|
732
|
+
"""Every trigger record kept from `from_serial` on; a jump in the serials is what was overwritten."""
|
|
733
|
+
out: list[TriggerRecord] = []
|
|
734
|
+
while True:
|
|
735
|
+
page, more = self.triggers_page(from_serial, generation)
|
|
736
|
+
out += page
|
|
737
|
+
if not more or not page:
|
|
738
|
+
return out
|
|
739
|
+
from_serial = (page[-1].serial + 1) & 0xFFFFFFFF
|
|
740
|
+
|
|
587
741
|
def segments(self, from_serial: int = 0) -> list[Segment]:
|
|
588
742
|
"""Every segment record from `from_serial` on, following `more` (each next page from the last serial + 1,
|
|
589
743
|
until more 0)."""
|
|
@@ -678,14 +832,53 @@ class LogicCapture(Interface):
|
|
|
678
832
|
"""The segment's bytes (of its generation); every Host.on_capture callback gets them as a CaptureRecord (a run
|
|
679
833
|
recorder, e.g. pytest-embedded-wireskein, without this package knowing it)."""
|
|
680
834
|
c = self.config
|
|
681
|
-
|
|
682
|
-
|
|
835
|
+
if c.format == DATA_FORMAT["logic_blocks"]:
|
|
836
|
+
data = self._read_blocks(segment)
|
|
837
|
+
else:
|
|
838
|
+
data = self.read(segment.position, c.segment_bytes(segment.samples), segment.generation or None)
|
|
683
839
|
if self.host.on_capture:
|
|
684
840
|
record = CaptureRecord(self.fn, self.name, c, segment, data, self.armed_s, time.monotonic())
|
|
685
841
|
for callback in list(self.host.on_capture):
|
|
686
842
|
callback(record)
|
|
687
843
|
return data
|
|
688
844
|
|
|
845
|
+
def _read_blocks(self, segment: Segment) -> bytes:
|
|
846
|
+
"""A format 1 segment's bytes: its length is not in its record (§1.3), so it ends where the next segment
|
|
847
|
+
begins, or at write_pos for the last; read at once, then cut by its blocks' headers."""
|
|
848
|
+
after = [s.position for s in self.segments((segment.serial + 1) & 0xFFFFFFFF)
|
|
849
|
+
if s.generation == segment.generation and s.position > segment.position]
|
|
850
|
+
end = after[0] if after else self.status().write_pos
|
|
851
|
+
data = self.read(segment.position, end - segment.position, segment.generation or None)
|
|
852
|
+
return data[:self._blocks_length(data, segment.samples)]
|
|
853
|
+
|
|
854
|
+
def _blocks_length(self, data: bytes, samples: int) -> int:
|
|
855
|
+
"""The bytes the blocks of `samples` samples take at the start of `data` (from their headers, §1.3)."""
|
|
856
|
+
c = self.config
|
|
857
|
+
nb = (len(c.event_d) + 7) // 8
|
|
858
|
+
at = 0
|
|
859
|
+
for first in range(0, samples, c.block):
|
|
860
|
+
r = min(c.block, samples - first)
|
|
861
|
+
head = data[at:at + 2 * nb + 4]
|
|
862
|
+
if len(head) < 2 * nb + 4:
|
|
863
|
+
raise h.ProtocolError(f"format 1: the segment's bytes end inside block {first // c.block}")
|
|
864
|
+
fallbacks = bin(int.from_bytes(head[:nb], "little")).count("1")
|
|
865
|
+
n = int.from_bytes(head[2 * nb:], "little")
|
|
866
|
+
at += len(head) + (r * (len(c.raw_roles) + fallbacks) + 7) // 8 + capture_blocks.EVENT_BYTES * n
|
|
867
|
+
if at > len(data):
|
|
868
|
+
raise h.ProtocolError("format 1: the segment's bytes end inside its last block")
|
|
869
|
+
return at
|
|
870
|
+
|
|
871
|
+
def blocks(self, data: bytes, samples: int) -> list[capture_blocks.LogicBlock]:
|
|
872
|
+
"""A format 1 segment's bytes -> its blocks (raw channels as bits, events channels as ticks, §1.3)."""
|
|
873
|
+
c = self.config
|
|
874
|
+
return capture_blocks.read_logic_blocks(data, samples, c.raw_roles, c.event_d, c.block)
|
|
875
|
+
|
|
876
|
+
def columns(self, data: bytes, samples: int) -> dict:
|
|
877
|
+
"""A format 1 segment's bytes -> role -> its raw bits or its EventColumn over the whole segment, without blocks
|
|
878
|
+
(capture_blocks.segment_columns; needs numpy)."""
|
|
879
|
+
c = self.config
|
|
880
|
+
return capture_blocks.segment_columns(data, samples, c.raw_roles, c.event_d, c.block)
|
|
881
|
+
|
|
689
882
|
# ---- the §3.0 layout ---------------------------------------------------------------------------------
|
|
690
883
|
def channel(self, data: bytes, k: int, samples: int | None = None) -> list[int]:
|
|
691
884
|
"""Channel k's values, one per sample (§1.1 rules 1-3: bit i*w + pos[k] of the stream, bit j being bit j mod 8
|
|
@@ -811,6 +1004,119 @@ class GroupStatus:
|
|
|
811
1004
|
state: int
|
|
812
1005
|
start_ns: int | None # None before the group's first start
|
|
813
1006
|
generation: int = 0 # the group's (§4.1; 0 before its first start)
|
|
1007
|
+
trigger: tuple[int, int, int] | None = None # §4.4, once the event happened: (fn judging, 0 force / timer; ns; +-ns)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
GROUP_BIND = _GRP.tlv["bind"]
|
|
1011
|
+
COND = _GRP.enum["trigger_cond"]
|
|
1012
|
+
COND_SRC = _GRP.enum["cond_src"]
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
@dataclass(frozen=True)
|
|
1016
|
+
class Cond:
|
|
1017
|
+
"""A condition of the trigger source (§4.4): built with level() / edge() / cross_up() / cross_down() / above() /
|
|
1018
|
+
below() / timer(). `fn` the bound track (0 for timer), `role` its plan role, `kind` enum trigger_cond, `args` the
|
|
1019
|
+
type's arguments packed."""
|
|
1020
|
+
fn: int
|
|
1021
|
+
role: int
|
|
1022
|
+
kind: int
|
|
1023
|
+
args: bytes
|
|
1024
|
+
src: int = COND_SRC["track"]
|
|
1025
|
+
|
|
1026
|
+
@staticmethod
|
|
1027
|
+
def level(track: "LogicCapture", role: int, level: int, when: int = 0) -> "Cond":
|
|
1028
|
+
return Cond(track.fn, role, COND["level"], struct.pack("<BB", level, when))
|
|
1029
|
+
|
|
1030
|
+
@staticmethod
|
|
1031
|
+
def edge(track: "LogicCapture", role: int, edge: int = 0) -> "Cond":
|
|
1032
|
+
"""edge 0 rising, 1 falling, 2 both."""
|
|
1033
|
+
return Cond(track.fn, role, COND["edge"], struct.pack("<B", edge))
|
|
1034
|
+
|
|
1035
|
+
@staticmethod
|
|
1036
|
+
def cross_up(track: "LogicCapture", role: int, value: int, hysteresis: int = 0) -> "Cond":
|
|
1037
|
+
return Cond(track.fn, role, COND["cross_up"], struct.pack("<II", value, hysteresis))
|
|
1038
|
+
|
|
1039
|
+
@staticmethod
|
|
1040
|
+
def cross_down(track: "LogicCapture", role: int, value: int, hysteresis: int = 0) -> "Cond":
|
|
1041
|
+
return Cond(track.fn, role, COND["cross_down"], struct.pack("<II", value, hysteresis))
|
|
1042
|
+
|
|
1043
|
+
@staticmethod
|
|
1044
|
+
def above(track: "LogicCapture", role: int, value: int, hysteresis: int = 0, when: int = 0) -> "Cond":
|
|
1045
|
+
return Cond(track.fn, role, COND["above"], struct.pack("<IIB", value, hysteresis, when))
|
|
1046
|
+
|
|
1047
|
+
@staticmethod
|
|
1048
|
+
def below(track: "LogicCapture", role: int, value: int, hysteresis: int = 0, when: int = 0) -> "Cond":
|
|
1049
|
+
return Cond(track.fn, role, COND["below"], struct.pack("<IIB", value, hysteresis, when))
|
|
1050
|
+
|
|
1051
|
+
@staticmethod
|
|
1052
|
+
def timer(ns: int) -> "Cond":
|
|
1053
|
+
"""ns after the stage was entered."""
|
|
1054
|
+
return Cond(0, 0, COND["timer"], struct.pack("<Q", ns), COND_SRC["track"])
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
@dataclass(frozen=True)
|
|
1058
|
+
class Stage:
|
|
1059
|
+
"""A stage (§4.4): its terms are OR'd, a term's conditions AND'd; `count` times the OR becomes true; `within_ns`
|
|
1060
|
+
from entering it (0 none); `on_timeout` 0 back to stage 0 when it passes, 1 the "should come" stage (its OR in
|
|
1061
|
+
time goes back to stage 0, the time passing makes it hold). `aborts`: terms that send it back to stage 0."""
|
|
1062
|
+
terms: list[list[Cond]]
|
|
1063
|
+
count: int = 1
|
|
1064
|
+
within_ns: int = 0
|
|
1065
|
+
on_timeout: int = 0
|
|
1066
|
+
aborts: list[list[Cond]] = field(default_factory=list)
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
@dataclass(frozen=True)
|
|
1070
|
+
class TriggerSource:
|
|
1071
|
+
"""The group's trigger source (§4.4): stages in order, the last one holding is the event."""
|
|
1072
|
+
stages: list[Stage]
|
|
1073
|
+
holdoff_ns: int = 0
|
|
1074
|
+
arm: int = 1
|
|
1075
|
+
rearm: int = 0
|
|
1076
|
+
|
|
1077
|
+
def tlvs(self) -> bytes:
|
|
1078
|
+
"""The bind TLVs, every one critical (§4.4)."""
|
|
1079
|
+
out = m.tlv(GROUP_BIND["trigger_source"] | CRITICAL,
|
|
1080
|
+
struct.pack("<BQBB", len(self.stages), self.holdoff_ns, self.arm, self.rearm))
|
|
1081
|
+
for k, st in enumerate(self.stages):
|
|
1082
|
+
out += m.tlv(GROUP_BIND["stage"] | CRITICAL, struct.pack("<BIQB", k, st.count, st.within_ns, st.on_timeout))
|
|
1083
|
+
for part, terms in ((0, st.terms), (1, st.aborts)):
|
|
1084
|
+
for term, conds in enumerate(terms):
|
|
1085
|
+
for c in conds:
|
|
1086
|
+
out += m.tlv(GROUP_BIND["cond"] | CRITICAL,
|
|
1087
|
+
struct.pack("<BBBBHBB", k, part, term, c.src, c.fn, c.role, c.kind) + c.args)
|
|
1088
|
+
return out
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
class TriggerCaps(NamedTuple):
|
|
1092
|
+
"""capture-group describe's trigger_caps (§4.4, tag 0x41): what its trigger source takes - a declaration (core §7.3),
|
|
1093
|
+
bind's answer is the probe's word. `types`: the condition types (enum trigger_cond, bit k = type k); `protocols`:
|
|
1094
|
+
serial's (bit k = protocol k); trigger_out_latency_ns None: no trigger_out."""
|
|
1095
|
+
max_stages: int
|
|
1096
|
+
max_terms: int
|
|
1097
|
+
max_conds: int
|
|
1098
|
+
cross_track: bool
|
|
1099
|
+
types: int
|
|
1100
|
+
protocols: int
|
|
1101
|
+
trigger_out_latency_ns: int | None
|
|
1102
|
+
|
|
1103
|
+
FORMAT = "<BBBBIBI"
|
|
1104
|
+
|
|
1105
|
+
@classmethod
|
|
1106
|
+
def unpack(cls, v: bytes) -> "TriggerCaps | None":
|
|
1107
|
+
"""-> its fields, or None for a value of another length (a form this host does not know)."""
|
|
1108
|
+
if len(v) != struct.calcsize(cls.FORMAT):
|
|
1109
|
+
return None
|
|
1110
|
+
st, te, co, ct, types, prot, lat = struct.unpack(cls.FORMAT, v)
|
|
1111
|
+
return cls(st, te, co, bool(ct), types, prot, None if lat == 0xFFFFFFFF else lat)
|
|
1112
|
+
|
|
1113
|
+
def takes(self, kind: int) -> bool:
|
|
1114
|
+
"""Whether the condition type `kind` (COND[...]) is declared."""
|
|
1115
|
+
return 0 <= kind < 32 and bool(self.types >> kind & 1)
|
|
1116
|
+
|
|
1117
|
+
def kinds(self) -> list[str]:
|
|
1118
|
+
"""The declared condition types by name (COND), in number order."""
|
|
1119
|
+
return [name for name, k in sorted(COND.items(), key=lambda kv: kv[1]) if self.takes(k)]
|
|
814
1120
|
|
|
815
1121
|
|
|
816
1122
|
class CaptureGroup(Interface):
|
|
@@ -820,19 +1126,36 @@ class CaptureGroup(Interface):
|
|
|
820
1126
|
uncertainty place it)."""
|
|
821
1127
|
NAME = "oep.fixture.capture-group"
|
|
822
1128
|
REVISION = 1
|
|
823
|
-
BIND, START, STOP, STATUS = (_GRP.op[k] for k in ("bind", "start", "stop", "status"))
|
|
1129
|
+
BIND, START, STOP, STATUS, FORCE = (_GRP.op[k] for k in ("bind", "start", "stop", "status", "force"))
|
|
824
1130
|
EVENT_STOPPED = _GRP.event["stopped"]
|
|
825
1131
|
NO_TIME = 0xFFFFFFFFFFFFFFFF
|
|
826
1132
|
|
|
827
|
-
def bind(self, tracks: list[LogicCapture]) -> None:
|
|
828
|
-
"""Bind these
|
|
829
|
-
here before sending (ValueError, §4.1)
|
|
830
|
-
|
|
1133
|
+
def bind(self, tracks: list[LogicCapture], source: TriggerSource | None = None) -> None:
|
|
1134
|
+
"""Bind these configured tracks; [] unbinds. Without `source` they start at once (immediate triggers): a track
|
|
1135
|
+
with a trigger of its own is refused here before sending (ValueError, §4.1). With `source` (§4.4, describe
|
|
1136
|
+
features bit 0) every track has the linked trigger (with its own pretrigger) and the group's event starts them.
|
|
1137
|
+
The probe refuses a track not in state 1/4/6 or modes apart (unavailable cause 6, `.fn`), one in another group
|
|
1138
|
+
(cause 4) and more than it takes together (cause 2)."""
|
|
831
1139
|
for t in tracks:
|
|
832
|
-
if t.config is not None and t.config.pretrigger:
|
|
1140
|
+
if source is None and t.config is not None and t.config.pretrigger:
|
|
833
1141
|
raise ValueError(f"capture-group bind: fn {t.fn} has a trigger of its own - a group starts its tracks "
|
|
834
1142
|
"at once (oep-if-capture §4.1)")
|
|
835
|
-
self._call(self.BIND, struct.pack(f"<B{len(tracks)}H", len(tracks), *(t.fn for t in tracks))
|
|
1143
|
+
self._call(self.BIND, struct.pack(f"<B{len(tracks)}H", len(tracks), *(t.fn for t in tracks))
|
|
1144
|
+
+ (source.tlvs() if source is not None else b""))
|
|
1145
|
+
|
|
1146
|
+
def force(self) -> None:
|
|
1147
|
+
"""§4.4: waiting for the event, it happens now (nothing otherwise)."""
|
|
1148
|
+
self._call(self.FORCE)
|
|
1149
|
+
|
|
1150
|
+
TRIGGER_CAPS = _GRP.tlv["describe"]["trigger_caps"]
|
|
1151
|
+
|
|
1152
|
+
def trigger_caps(self) -> TriggerCaps | None:
|
|
1153
|
+
"""describe's trigger_caps (§4.4): what the group's trigger source takes; None when the probe declares none (no
|
|
1154
|
+
trigger source, or a form this host does not know). A declaration: bind still has the last word (§4.3)."""
|
|
1155
|
+
for tag, v in describe(self.host, self.fn):
|
|
1156
|
+
if tag == self.TRIGGER_CAPS:
|
|
1157
|
+
return TriggerCaps.unpack(v)
|
|
1158
|
+
return None
|
|
836
1159
|
|
|
837
1160
|
generation: int | None = None # the group's current generation (start / status, §4.1)
|
|
838
1161
|
stale_events: int = 0 # the group's events of an earlier generation that events() dropped
|
|
@@ -869,9 +1192,11 @@ class CaptureGroup(Interface):
|
|
|
869
1192
|
def status(self) -> GroupStatus:
|
|
870
1193
|
rd = m.Reader(self._call(self.STATUS, locked=False).payload)
|
|
871
1194
|
state, start, generation = rd.take("BQI")
|
|
872
|
-
rd.tail()
|
|
1195
|
+
tail = rd.tail()
|
|
873
1196
|
self.generation = generation
|
|
874
|
-
|
|
1197
|
+
trig = tail.get(_GRP.tlv["status_answer"]["trigger"])
|
|
1198
|
+
trigger = struct.unpack("<HQI", trig) if trig is not None and len(trig) == 14 else None
|
|
1199
|
+
return GroupStatus(state, None if start == self.NO_TIME else start, generation, trigger)
|
|
875
1200
|
|
|
876
1201
|
def events(self, link) -> list[Event]:
|
|
877
1202
|
"""The group's own events waiting on the link (taken off it; stopped, §4.2), the current group generation's
|