oep-client-python 0.0.39__py3-none-any.whl → 0.0.40__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 +22 -7
- oep_client/capture.py +137 -7
- oep_client/capture_blocks.py +123 -2
- oep_client/conformance.py +19 -2
- oep_client/conformance_capture_serial.py +2 -2
- oep_client/conformance_capture_timeline.py +1 -1
- oep_client/conformance_core_fixed_fields.py +1 -1
- oep_client/conformance_core_smoke.py +3 -2
- oep_client/conformance_host_core.py +14 -8
- oep_client/conformance_host_core_sample.py +7 -5
- oep_client/conformance_host_deadline.py +3 -3
- oep_client/conformance_host_deadline_sample.py +1 -1
- oep_client/conformance_host_request_sample.py +27 -12
- oep_client/conformance_host_requests.py +66 -34
- oep_client/conformance_host_resend.py +1 -1
- oep_client/conformance_host_resend_sample.py +1 -1
- oep_client/conformance_host_tlv.py +1 -1
- oep_client/conformance_host_wait.py +2 -2
- oep_client/conformance_lease.py +1 -1
- oep_client/conformance_oep_smoke.py +4 -4
- oep_client/conformance_port_speed_execution.py +1 -1
- oep_client/conformance_response_race.py +1 -1
- oep_client/conformance_writer_lease.py +1 -1
- oep_client/endpoint.py +72 -25
- oep_client/fixture.py +389 -3
- oep_client/interfaces.py +25 -0
- oep_client/link.py +1 -1
- oep_client/pins.py +368 -58
- oep_client/probe_log.py +97 -0
- oep_client/registry.py +21 -1
- oep_client/settings.py +36 -7
- oep_client/stimulus.py +132 -0
- oep_client/targets.py +47 -10
- oep_client/virtual_bench.py +165 -13
- oep_client/virtual_bench_capture.py +582 -67
- oep_client/virtual_bench_serve.py +2 -2
- oep_client/virtual_bench_trigger.py +207 -45
- oep_client/virtual_core.py +2 -2
- oep_client/virtual_fixture_drive.py +404 -0
- {oep_client_python-0.0.39.dist-info → oep_client_python-0.0.40.dist-info}/METADATA +35 -22
- {oep_client_python-0.0.39.dist-info → oep_client_python-0.0.40.dist-info}/RECORD +45 -42
- {oep_client_python-0.0.39.dist-info → oep_client_python-0.0.40.dist-info}/WHEEL +1 -1
- {oep_client_python-0.0.39.dist-info → oep_client_python-0.0.40.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.39.dist-info → oep_client_python-0.0.40.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/__main__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""The oep command: what a probe offers (dump) and its settings (settings, core §13).
|
|
2
2
|
|
|
3
|
-
oep dump --port /
|
|
3
|
+
oep dump --port /dev/ttyACM0 oep dump --virtual p4-x035 --prefix oep.target --json
|
|
4
4
|
oep settings show <probe> (every value, the storage state; lock-free)
|
|
5
5
|
oep settings items <probe> (what each owner declares: names, forms, choices)
|
|
6
6
|
oep settings set <probe> core channel.GPIO23.label=v003.nrst channel.GPIO20.idle=pull-up [--save]
|
|
@@ -101,11 +101,19 @@ def main(argv=None) -> int:
|
|
|
101
101
|
"see which channels follow it (never touched without this)")
|
|
102
102
|
pn.add_argument("--exclude", default="", help="channels never to read, scan or hold, comma-separated")
|
|
103
103
|
pn.add_argument("--wire", choices=("swio", "rvswd", "swd"), default="swio")
|
|
104
|
-
pn.add_argument("--
|
|
105
|
-
help="the
|
|
104
|
+
pn.add_argument("--pairs", default="",
|
|
105
|
+
help="the pairs to scan instead of what classify allows: SWDIO:SWCLK,... (swio: SWIO,...); the host's "
|
|
106
|
+
"word - no reading decides")
|
|
107
|
+
pn.add_argument("--steps", default="classify,hold,scan,identify,map,reset,labels",
|
|
108
|
+
help="the steps to run, comma-separated (default all: classify, hold, scan, identify, map, reset, "
|
|
109
|
+
"labels)")
|
|
110
|
+
pn.add_argument("--budget", type=float, default=55.0, help="seconds the whole run may take (default 55); a wider "
|
|
111
|
+
"scan wants more (pairs go the nearest first: about 0.1 s each over rvswd)")
|
|
112
|
+
pn.add_argument("--all-pairs", action="store_true", help="scan every pair, not only up to the first that answers "
|
|
113
|
+
"(a second target shows; needs the time)")
|
|
106
114
|
pn.add_argument("--save", action="store_true", help="write the suggested labels (settings set, then save)")
|
|
107
|
-
pn.add_argument("--name", help="the host's connection profile name: labels <name>.
|
|
108
|
-
"
|
|
115
|
+
pn.add_argument("--name", help="the host's connection profile name: labels <name>.swdio / <name>.swclk (or "
|
|
116
|
+
"<name>.swio), <name>.nrst, <name>.power_hi; the probe is then opened as oep://<unit_id>/<name>")
|
|
109
117
|
pn.add_argument("--json", action="store_true", help="the report as JSON (the steps' lines go to stderr)")
|
|
110
118
|
ck = sub.add_parser("clock", help="the probe's clock (fn 0 clock, core §7.6) against this host's: lock-free")
|
|
111
119
|
ck.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]")
|
|
@@ -238,8 +246,15 @@ def _pins_cmd(args) -> int:
|
|
|
238
246
|
hst = _open_host(args.probe)
|
|
239
247
|
try:
|
|
240
248
|
core.take(hst, 10000, owner="oep pins")
|
|
249
|
+
pairs = None
|
|
250
|
+
if args.pairs:
|
|
251
|
+
pairs = []
|
|
252
|
+
for item in args.pairs.replace(" ", "").split(","):
|
|
253
|
+
dio, _, clk = item.partition(":")
|
|
254
|
+
pairs.append((int(dio, 0), int(clk, 0) if clk else 0xFFFF))
|
|
241
255
|
finder = pins.PinFinder(hst, wire=args.wire, power=args.power, exclude=exclude, say=say, probe=args.probe,
|
|
242
|
-
save=args.save, name=args.name
|
|
256
|
+
save=args.save, name=args.name, pairs=pairs, budget_s=args.budget,
|
|
257
|
+
all_pairs=args.all_pairs)
|
|
243
258
|
report = finder.run(steps=tuple(s for s in args.steps.split(",") if s))
|
|
244
259
|
say(f"done in {report.elapsed_s:.1f} s")
|
|
245
260
|
if args.json:
|
|
@@ -253,7 +268,7 @@ def _pins_cmd(args) -> int:
|
|
|
253
268
|
return 2
|
|
254
269
|
finally:
|
|
255
270
|
hst.link.close()
|
|
256
|
-
return 0 if report.resolution
|
|
271
|
+
return 0 if report.resolution in ('unique-candidate', 'unique-in-partial') else 1 # one target found
|
|
257
272
|
|
|
258
273
|
|
|
259
274
|
# ---- oep linktest -------------------------------------------------------------------------------------------------
|
oep_client/capture.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""oep.fixture.logic / oep.fixture.analog / oep.fixture.capture-group revision 1 (oep-spec
|
|
1
|
+
"""oep.fixture.logic / oep.fixture.analog / oep.fixture.capture-group revision 1 (oep-spec
|
|
2
2
|
interfaces/oep-if-capture.ja.md: §1 layouts, §2 segments, §3 operations, §3.7 calibration, §4 groups). Numbers from
|
|
3
3
|
`registry`.
|
|
4
4
|
|
|
@@ -51,6 +51,13 @@ MODE, RATE, SAMPLES, SEGMENTS, TRIGGER, PRETRIGGER, TRIGGER_OPTS, FORMAT, CARRIA
|
|
|
51
51
|
_CAP.tlv["configure"][k] for k in ("mode", "rate", "samples", "segments", "trigger", "pretrigger", "trigger_opts",
|
|
52
52
|
"format", "carriage", "block_max"))
|
|
53
53
|
ANSWER_FORMAT, ANSWER_BLOCK = _CAP.tlv["configure_answer"]["format"], _CAP.tlv["configure_answer"]["block"]
|
|
54
|
+
ANSWER_FORMAT_NAME = _CAP.tlv["configure_answer"]["format_name"]
|
|
55
|
+
# §3.8 / §3.9, optional (a probe declares them in its describe): configure TLVs by name, sent critical
|
|
56
|
+
OPTION_TLV = {**_CAP.tlv["configure"], **_ANA.tlv["configure"]}
|
|
57
|
+
LOGIC_USED = {k: _CAP.tlv["configure_answer"][k] for k in ("threshold_used", "glitch_used", "timebase")}
|
|
58
|
+
ANALOG_USED = {k: _ANA.tlv["configure_answer"][k] for k in ("coupling_used", "offset_used", "bandwidth_used",
|
|
59
|
+
"resolution_used", "oversample_used")}
|
|
60
|
+
NO_QUALIFIER = 0xFF # clock: qualifier_role none (§3.8)
|
|
54
61
|
STREAM_BUFFER = _CAP.tlv["configure_answer"]["stream_buffer"]
|
|
55
62
|
JUDGE_LATENCY = _CAP.tlv["configure_answer"]["judge_latency_ns"]
|
|
56
63
|
DATA_FORMAT = _CAP.enum["data_format"]
|
|
@@ -172,6 +179,11 @@ class Config:
|
|
|
172
179
|
judge_latency_ns: int | None = None # judging a group's trigger source condition, the longest to know it (§3.3, §4.4)
|
|
173
180
|
raw_roles: list[int] = field(default_factory=list) # format 1: the raw channels' roles (as asked)
|
|
174
181
|
event_d: dict[int, int] = field(default_factory=dict) # format 1: events role -> D (as asked)
|
|
182
|
+
carriage: dict[int, tuple[int, int]] = field(default_factory=dict) # format 2: role -> (kind, D) (as asked)
|
|
183
|
+
format_name: str | None = None # format 0xFF: the named format in use
|
|
184
|
+
timebase: Fraction | None = None # format 3: the delta's tick rate (Hz)
|
|
185
|
+
external_clock: bool = False # sampled on a clock line (actual_rate 0 / 1, §3.8)
|
|
186
|
+
used: dict[str, dict[int, int] | int] = field(default_factory=dict) # §3.8's *_used: name -> group/role -> value
|
|
175
187
|
|
|
176
188
|
def tolerable_delay_s(self, worst_bytes_per_s: float | None = None) -> float | None:
|
|
177
189
|
"""Streaming: how long the host may stop reading before the probe drops (§3.3: stream_buffer over the data rate;
|
|
@@ -183,7 +195,9 @@ class Config:
|
|
|
183
195
|
return self.stream_buffer / rate if rate else None
|
|
184
196
|
|
|
185
197
|
def segment_bytes(self, samples: int) -> int:
|
|
186
|
-
"""Length of a segment of `samples` samples in the stream (§1.1 rule 4, §1.2 rule 3)."""
|
|
198
|
+
"""Length of a segment of `samples` samples in the stream (§1.1 rule 4, §1.2 rule 3; format 2: its blocks, §1.4)."""
|
|
199
|
+
if self.format == DATA_FORMAT["analog_blocks"]:
|
|
200
|
+
return capture_blocks.analog_segment_bytes(samples, self.order, self.carriage, self.block, self.bits)
|
|
187
201
|
if self.width:
|
|
188
202
|
return (samples * self.width + 7) // 8
|
|
189
203
|
return samples * len(self.order) * self.slot // 8
|
|
@@ -437,6 +451,27 @@ def take_events(link, fn: int) -> list[bytes]:
|
|
|
437
451
|
return mine
|
|
438
452
|
|
|
439
453
|
|
|
454
|
+
def _options(options: dict) -> bytes:
|
|
455
|
+
"""§3.8 / §3.9's configure TLVs, critical but overflow (configure's `options`)."""
|
|
456
|
+
forms = {"threshold": "<iI", "glitch_filter": "<I", "coupling": "<B", "offset": "<i", "bandwidth": "<I"}
|
|
457
|
+
out = b""
|
|
458
|
+
for name, value in options.items():
|
|
459
|
+
tag = OPTION_TLV[name] | (0 if name == "overflow" else CRITICAL) # overflow: §3.3's table, as block_max
|
|
460
|
+
if name in forms: # per group / role
|
|
461
|
+
for key, v in sorted(value.items()):
|
|
462
|
+
out += m.tlv(tag, bytes([key]) + struct.pack(forms[name], *(v if isinstance(v, tuple) else (v,))))
|
|
463
|
+
elif name == "clock":
|
|
464
|
+
role, edge, qualifier, level = value
|
|
465
|
+
out += m.tlv(tag, bytes([role, edge, NO_QUALIFIER if qualifier is None else qualifier, level]))
|
|
466
|
+
elif name == "oversample":
|
|
467
|
+
out += m.tlv(tag, struct.pack("<H", value))
|
|
468
|
+
elif name == "data_path":
|
|
469
|
+
out += m.tlv(tag, bytes(value))
|
|
470
|
+
else: # resolution, overflow: u8
|
|
471
|
+
out += m.tlv(tag, bytes([value]))
|
|
472
|
+
return out
|
|
473
|
+
|
|
474
|
+
|
|
440
475
|
def _config(payload: bytes, analog: bool) -> Config:
|
|
441
476
|
"""configure's answer TLVs (§3.3). A tag that does not repeat is taken from its first copy only, and a value not of
|
|
442
477
|
its defined length (layout: its count and length) is broken and not used - nor replaced by a later copy (core
|
|
@@ -452,6 +487,7 @@ def _config(payload: bytes, analog: bool) -> Config:
|
|
|
452
487
|
if tag == ACTUAL_RATE and len(v) == 8:
|
|
453
488
|
num, den = struct.unpack("<II", v)
|
|
454
489
|
c.rate = Fraction(num, den) if den else c.rate
|
|
490
|
+
c.external_clock = num == 0 # 0 / 1: sampled on a clock, no time (§3.8)
|
|
455
491
|
elif tag == LAYOUT and not analog and len(v) >= 2 and len(v) == 2 + v[1]:
|
|
456
492
|
c.width, n = v[0], v[1]
|
|
457
493
|
c.positions = list(v[2:2 + n])
|
|
@@ -478,6 +514,27 @@ def _config(payload: bytes, analog: bool) -> Config:
|
|
|
478
514
|
c.skew_ns[role] = ns
|
|
479
515
|
elif tag == FRONTEND_USED and analog and len(v) == 2:
|
|
480
516
|
c.frontend[v[0]] = v[1]
|
|
517
|
+
elif tag == ANSWER_FORMAT_NAME:
|
|
518
|
+
c.format_name = c.format_name or v.decode("ascii", "replace")
|
|
519
|
+
elif not analog and tag == LOGIC_USED["timebase"] and len(v) == 8:
|
|
520
|
+
num, den = struct.unpack("<II", v)
|
|
521
|
+
c.timebase = Fraction(num, den) if den else None
|
|
522
|
+
elif not analog and tag == LOGIC_USED["threshold_used"] and len(v) == 9:
|
|
523
|
+
group, mv, hyst = struct.unpack("<BiI", v)
|
|
524
|
+
c.used.setdefault("threshold_used", {})[group] = (mv, hyst)
|
|
525
|
+
elif not analog and tag == LOGIC_USED["glitch_used"] and len(v) == 5:
|
|
526
|
+
group, ns = struct.unpack("<BI", v)
|
|
527
|
+
c.used.setdefault("glitch_used", {})[group] = ns
|
|
528
|
+
elif analog and tag in (ANALOG_USED["coupling_used"], ANALOG_USED["offset_used"], ANALOG_USED["bandwidth_used"]):
|
|
529
|
+
name = next(k for k, t in ANALOG_USED.items() if t == tag)
|
|
530
|
+
form = {"coupling_used": "<BB", "offset_used": "<Bi", "bandwidth_used": "<BI"}[name]
|
|
531
|
+
if len(v) == struct.calcsize(form):
|
|
532
|
+
role, value = struct.unpack(form, v)
|
|
533
|
+
c.used.setdefault(name, {})[role] = value
|
|
534
|
+
elif analog and tag == ANALOG_USED["resolution_used"] and len(v) == 1:
|
|
535
|
+
c.used["resolution_used"] = v[0]
|
|
536
|
+
elif analog and tag == ANALOG_USED["oversample_used"] and len(v) == 2:
|
|
537
|
+
c.used["oversample_used"] = struct.unpack("<H", v)[0]
|
|
481
538
|
elif tag == REFERENCE and analog and len(v) == 6:
|
|
482
539
|
source, mv, how = struct.unpack("<BIB", v)
|
|
483
540
|
# how: nominal False, measured True; an unknown value is shown as it is, not as nominal (core §2.4)
|
|
@@ -508,14 +565,23 @@ class LogicCapture(Interface):
|
|
|
508
565
|
trigger: tuple[int, int, int] | None = None, pretrigger: int | None = None, query: bool = False,
|
|
509
566
|
frontends: dict[int, int] | None = None, trigger_opts: TriggerOpts | None = None,
|
|
510
567
|
data_format: int = 0, events: dict[int, int] | None = None, roles: list[int] | None = None,
|
|
511
|
-
block_max: int | None = None
|
|
568
|
+
block_max: int | None = None, carriage: dict[int, tuple[int, int]] | None = None,
|
|
569
|
+
format_name: str | None = None, options: dict | None = None) -> Config:
|
|
512
570
|
"""-> the probe's actual values. A value the probe cannot honour is refused: host.Unsupported, .tag = the TLV as
|
|
513
571
|
sent (§3.3). The TLVs go without the critical bit (§3.3). Read Config.samples / .segments: the probe rounds
|
|
514
572
|
samples down.
|
|
515
573
|
|
|
516
574
|
Format 1 (logic blocks, §1.3): `data_format` DATA_FORMAT["logic_blocks"], `events` role -> D for the channels
|
|
517
575
|
carried as events (the others raw), `roles` the plan's roles (to name the raw channels), `block_max` the
|
|
518
|
-
longest block.
|
|
576
|
+
longest block (§3.3's table: without the critical bit). Format 2 (analog blocks, §1.4): `carriage` role ->
|
|
577
|
+
(kind, D), kind a bit set of capture_blocks.PACKED / MINMAX / SUM (the roles not named packed). Format 4 (test
|
|
578
|
+
counter) needs nothing else. Format 0xFF: `format_name`. The others go critical.
|
|
579
|
+
|
|
580
|
+
`options` (§3.8 / §3.9, critical; a probe without one refuses it): "threshold" group -> (mv, hysteresis_mv),
|
|
581
|
+
"glitch_filter" group -> ns, "clock" (role, edge, qualifier role or None, qualifier level), "coupling" role ->
|
|
582
|
+
kind, "offset" role -> mV, "bandwidth" role -> Hz, "resolution" bits, "oversample" ratio, "data_path" (path,
|
|
583
|
+
mux, kind), "overflow" 0 / 1 (plain: §3.3's table). The answer's values in use: Config.used, by the answer TLV's
|
|
584
|
+
name.
|
|
519
585
|
|
|
520
586
|
§3.3's contract is checked before anything is sent (ValueError): `samples` is needed in one-shot and repeat
|
|
521
587
|
and not taken in streaming, `segments` is repeat's only, `pretrigger` needs a trigger (type other than 0) - a
|
|
@@ -551,12 +617,19 @@ class LogicCapture(Interface):
|
|
|
551
617
|
body += tlv(FRONTEND, bytes([role, fe]))
|
|
552
618
|
if data_format: # critical: a probe without it refuses (§3.3)
|
|
553
619
|
body += m.tlv(FORMAT | CRITICAL, bytes([data_format]))
|
|
620
|
+
if (data_format == DATA_FORMAT["named"]) != (format_name is not None):
|
|
621
|
+
raise ValueError("capture configure: format 0xFF goes with its format_name, and only it (oep-if-capture §3.3)")
|
|
622
|
+
if format_name is not None:
|
|
623
|
+
body += m.tlv(OPTION_TLV["format_name"] | CRITICAL, format_name.encode("ascii"))
|
|
624
|
+
body += _options(options or {})
|
|
554
625
|
if data_format == DATA_FORMAT["logic_blocks"] and roles is None:
|
|
555
626
|
raise ValueError("capture configure: format 1 needs the plan's roles (to name the raw channels)")
|
|
556
627
|
for role, d in sorted((events or {}).items()):
|
|
557
628
|
body += m.tlv(CARRIAGE | CRITICAL, struct.pack("<BBI", role, LOGIC_CARRIAGE["events"], d))
|
|
629
|
+
for role, (kind, d) in sorted((carriage or {}).items()):
|
|
630
|
+
body += m.tlv(CARRIAGE | CRITICAL, struct.pack("<BBI", role, kind, d))
|
|
558
631
|
if block_max is not None:
|
|
559
|
-
body += m.tlv(BLOCK_MAX
|
|
632
|
+
body += m.tlv(BLOCK_MAX, struct.pack("<I", block_max)) # §3.3's table: every probe takes it
|
|
560
633
|
if trigger_opts is not None: # critical: a probe without it refuses (§3.3)
|
|
561
634
|
o = trigger_opts
|
|
562
635
|
body += m.tlv(TRIGGER_OPTS | CRITICAL, struct.pack("<BBBI", o.arm, o.rearm, o.when, o.hysteresis))
|
|
@@ -567,6 +640,8 @@ class LogicCapture(Interface):
|
|
|
567
640
|
if data_format == DATA_FORMAT["logic_blocks"]:
|
|
568
641
|
c.event_d = dict(events or {})
|
|
569
642
|
c.raw_roles = [r for r in sorted(roles) if r not in c.event_d]
|
|
643
|
+
if c.format == DATA_FORMAT["analog_blocks"]:
|
|
644
|
+
c.carriage = {r: kd for r, kd in (carriage or {}).items() if kd[0] != capture_blocks.PACKED}
|
|
570
645
|
if mode == ONE_SHOT and not c.segments:
|
|
571
646
|
c.segments = 1 # one-shot's answer has no actual_segments (§3.3)
|
|
572
647
|
if not query:
|
|
@@ -843,7 +918,7 @@ class LogicCapture(Interface):
|
|
|
843
918
|
"""The segment's bytes (of its generation); every Host.on_capture callback gets them as a CaptureRecord (a run
|
|
844
919
|
recorder, e.g. pytest-embedded-wireskein, without this package knowing it)."""
|
|
845
920
|
c = self.config
|
|
846
|
-
if c.format
|
|
921
|
+
if c.format in (DATA_FORMAT["logic_blocks"], DATA_FORMAT["state_timed"]):
|
|
847
922
|
data = self._read_blocks(segment)
|
|
848
923
|
else:
|
|
849
924
|
data = self.read(segment.position, c.segment_bytes(segment.samples), segment.generation or None)
|
|
@@ -863,8 +938,11 @@ class LogicCapture(Interface):
|
|
|
863
938
|
return data[:self._blocks_length(data, segment.samples)]
|
|
864
939
|
|
|
865
940
|
def _blocks_length(self, data: bytes, samples: int) -> int:
|
|
866
|
-
"""The bytes the blocks of `samples` samples take at the start of `data` (from their headers, §1.3
|
|
941
|
+
"""The bytes the blocks of `samples` samples take at the start of `data` (from their headers, §1.3; format 3:
|
|
942
|
+
its entries, §3.8)."""
|
|
867
943
|
c = self.config
|
|
944
|
+
if c.format == DATA_FORMAT["state_timed"]:
|
|
945
|
+
return capture_blocks.state_timed_length(data, samples, c.width)
|
|
868
946
|
nb = (len(c.event_d) + 7) // 8
|
|
869
947
|
at = 0
|
|
870
948
|
for first in range(0, samples, c.block):
|
|
@@ -884,6 +962,15 @@ class LogicCapture(Interface):
|
|
|
884
962
|
c = self.config
|
|
885
963
|
return capture_blocks.read_logic_blocks(data, samples, c.raw_roles, c.event_d, c.block)
|
|
886
964
|
|
|
965
|
+
def state_timed(self, data: bytes, samples: int) -> list[tuple[int, int]]:
|
|
966
|
+
"""A format 3 segment's bytes -> (ticks from start, state) per sample (§3.8; Config.timebase the tick rate)."""
|
|
967
|
+
return capture_blocks.read_state_timed(data, samples, self.config.width)
|
|
968
|
+
|
|
969
|
+
def analog_blocks(self, data: bytes, samples: int) -> list[capture_blocks.AnalogBlock]:
|
|
970
|
+
"""A format 2 segment's bytes -> its blocks (each channel packed values or its intervals' summaries, §1.4)."""
|
|
971
|
+
c = self.config
|
|
972
|
+
return capture_blocks.read_analog_blocks(data, samples, c.order, c.carriage, c.block, c.bits)
|
|
973
|
+
|
|
887
974
|
def columns(self, data: bytes, samples: int) -> dict:
|
|
888
975
|
"""A format 1 segment's bytes -> role -> its raw bits or its EventColumn over the whole segment, without blocks
|
|
889
976
|
(capture_blocks.segment_columns; needs numpy)."""
|
|
@@ -1021,6 +1108,9 @@ class GroupStatus:
|
|
|
1021
1108
|
GROUP_BIND = _GRP.tlv["bind"]
|
|
1022
1109
|
COND = _GRP.enum["trigger_cond"]
|
|
1023
1110
|
COND_SRC = _GRP.enum["cond_src"]
|
|
1111
|
+
SERIAL_PROTOCOL, SERIAL_EVENT = _GRP.enum["serial_protocol"], _GRP.enum["serial_event"]
|
|
1112
|
+
PIN_ROLE = _GRP.enum["pin_role"] # the group's own plan roles: trig_in, trig_out
|
|
1113
|
+
NO_ROLE = 0xFF
|
|
1024
1114
|
|
|
1025
1115
|
|
|
1026
1116
|
@dataclass(frozen=True)
|
|
@@ -1064,6 +1154,43 @@ class Cond:
|
|
|
1064
1154
|
"""ns after the stage was entered."""
|
|
1065
1155
|
return Cond(0, 0, COND["timer"], struct.pack("<Q", ns), COND_SRC["track"])
|
|
1066
1156
|
|
|
1157
|
+
@staticmethod
|
|
1158
|
+
def pulse(track: "LogicCapture | None", role: int, level: int, cond: int, min_ns: int = 0, max_ns: int = 0) -> "Cond":
|
|
1159
|
+
"""A run at `level`: cond 0 longer than min_ns (the instant it gets there), 1 at most max_ns, 2 within min_ns ..
|
|
1160
|
+
max_ns (at its end). `track` None: the group's external input `role` (trig_in)."""
|
|
1161
|
+
return Cond(track.fn if track else 0, role, COND["pulse"], struct.pack("<BBQQ", level, cond, min_ns, max_ns),
|
|
1162
|
+
COND_SRC["track"] if track else COND_SRC["external"])
|
|
1163
|
+
|
|
1164
|
+
@staticmethod
|
|
1165
|
+
def analog_pulse(track: "AnalogCapture", role: int, value: int, side: int, cond: int, min_ns: int = 0,
|
|
1166
|
+
max_ns: int = 0, hysteresis: int = 0) -> "Cond":
|
|
1167
|
+
"""A run below (side 0) / above (side 1) `value`, timed as pulse()."""
|
|
1168
|
+
return Cond(track.fn, role, COND["analog_pulse"], struct.pack("<IIBBQQ", value, hysteresis, side, cond, min_ns, max_ns))
|
|
1169
|
+
|
|
1170
|
+
@staticmethod
|
|
1171
|
+
def pattern(track: "LogicCapture", levels: dict[int, int], when: int = 0) -> "Cond":
|
|
1172
|
+
"""The track's roles at these levels together (role -> 0 / 1; bit k of mask / value is role k)."""
|
|
1173
|
+
n = max(levels) // 8 + 1
|
|
1174
|
+
mask = sum(1 << r for r in levels).to_bytes(n, "little")
|
|
1175
|
+
value = sum(v << r for r, v in levels.items()).to_bytes(n, "little")
|
|
1176
|
+
return Cond(track.fn, 0, COND["pattern"], bytes([n]) + mask + value + bytes([when]))
|
|
1177
|
+
|
|
1178
|
+
@staticmethod
|
|
1179
|
+
def uart(track: "LogicCapture", role: int, baud: int, match: bytes = b"", mask: bytes | None = None,
|
|
1180
|
+
event: int = SERIAL_EVENT["match"], bits: int = 8, parity: int = 0, flags: int = 0) -> "Cond":
|
|
1181
|
+
"""serial, UART on RX `role`: the bytes `match` (under `mask`, default all bits), or an event (framing, parity,
|
|
1182
|
+
break)."""
|
|
1183
|
+
mask = mask if mask is not None else b"\xff" * len(match)
|
|
1184
|
+
return Cond(track.fn, role, COND["serial"], struct.pack("<BBBBIBBBB", SERIAL_PROTOCOL["uart"], event, NO_ROLE,
|
|
1185
|
+
NO_ROLE, baud, bits, parity, flags, len(match)) + mask + match)
|
|
1186
|
+
|
|
1187
|
+
@staticmethod
|
|
1188
|
+
def external(kind: str, role: int = PIN_ROLE["trig_in"], **args) -> "Cond":
|
|
1189
|
+
"""A condition on the group's external input (trig_in): level(level, when) or edge(edge)."""
|
|
1190
|
+
packed = struct.pack("<BB", args.get("level", 1), args.get("when", 0)) if kind == "level" else \
|
|
1191
|
+
struct.pack("<B", args.get("edge", 0))
|
|
1192
|
+
return Cond(0, role, COND[kind], packed, COND_SRC["external"])
|
|
1193
|
+
|
|
1067
1194
|
|
|
1068
1195
|
@dataclass(frozen=True)
|
|
1069
1196
|
class Stage:
|
|
@@ -1084,6 +1211,7 @@ class TriggerSource:
|
|
|
1084
1211
|
holdoff_ns: int = 0
|
|
1085
1212
|
arm: int = 1
|
|
1086
1213
|
rearm: int = 0
|
|
1214
|
+
trigger_out: tuple[int, int] | None = None # (width_ns, polarity) on the group's trig_out role
|
|
1087
1215
|
|
|
1088
1216
|
def tlvs(self) -> bytes:
|
|
1089
1217
|
"""The bind TLVs, every one critical (§4.4)."""
|
|
@@ -1096,6 +1224,8 @@ class TriggerSource:
|
|
|
1096
1224
|
for c in conds:
|
|
1097
1225
|
out += m.tlv(GROUP_BIND["cond"] | CRITICAL,
|
|
1098
1226
|
struct.pack("<BBBBHBB", k, part, term, c.src, c.fn, c.role, c.kind) + c.args)
|
|
1227
|
+
if self.trigger_out is not None:
|
|
1228
|
+
out += m.tlv(GROUP_BIND["trigger_out"] | CRITICAL, struct.pack("<BIB", PIN_ROLE["trig_out"], *self.trigger_out))
|
|
1099
1229
|
return out
|
|
1100
1230
|
|
|
1101
1231
|
|
oep_client/capture_blocks.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# SPDX-License-Identifier: MIT
|
|
2
|
-
"""Reading a capture's data formats beyond 0 (oep-if-capture §1): format 1, the logic blocks (§1.3)
|
|
2
|
+
"""Reading a capture's data formats beyond 0 (oep-if-capture §1): format 1, the logic blocks (§1.3), and format 2, the
|
|
3
|
+
analog blocks (§1.4).
|
|
3
4
|
|
|
4
5
|
A segment of format 1 is a run of blocks of L original samples (the last one r = samples mod L). Each block gives:
|
|
5
6
|
|
|
@@ -19,7 +20,7 @@ from __future__ import annotations
|
|
|
19
20
|
from dataclasses import dataclass, field
|
|
20
21
|
from typing import NamedTuple
|
|
21
22
|
|
|
22
|
-
from . import host as h
|
|
23
|
+
from . import host as h, registry as reg
|
|
23
24
|
|
|
24
25
|
try:
|
|
25
26
|
import numpy as _np
|
|
@@ -292,3 +293,123 @@ def worst_bytes(R: int, event_d: list[int], block: int) -> int:
|
|
|
292
293
|
"""The most bytes one block of L samples takes (§1.3): 2 ceil(E/8) + 4 + L R / 8 + sum min(L/8, (L/D) 4)."""
|
|
293
294
|
E = len(event_d)
|
|
294
295
|
return 2 * ((E + 7) // 8) + 4 + block * R // 8 + sum(min(block // 8, block // d * EVENT_BYTES) for d in event_d)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ---- format 2, the analog blocks (§1.4) ----------------------------------------------------------------------------
|
|
299
|
+
PACKED, MINMAX, SUM = (reg.FIXTURE_ANALOG.enum["analog_carriage"][k] for k in ("packed", "minmax", "sum"))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@dataclass
|
|
303
|
+
class AnalogChannel:
|
|
304
|
+
"""One channel's part of a format 2 block (§1.4): `values` (packed, lossless), or per interval of D (the last one
|
|
305
|
+
partial: its points from the block's r) `mins` / `maxs` and / or `sums` (summaries)."""
|
|
306
|
+
kind: int
|
|
307
|
+
d: int
|
|
308
|
+
values: list[int] = field(default_factory=list)
|
|
309
|
+
mins: list[int] = field(default_factory=list)
|
|
310
|
+
maxs: list[int] = field(default_factory=list)
|
|
311
|
+
sums: list[int] = field(default_factory=list)
|
|
312
|
+
|
|
313
|
+
def points(self, r: int) -> list[int]:
|
|
314
|
+
"""The original samples each interval covers (a sum over its points is their mean)."""
|
|
315
|
+
return [min(self.d, r - lo) for lo in range(0, r, self.d)]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass
|
|
319
|
+
class AnalogBlock:
|
|
320
|
+
"""One block of a format 2 segment: samples `first` .. `first + samples - 1`, role -> its part."""
|
|
321
|
+
first: int
|
|
322
|
+
samples: int
|
|
323
|
+
channels: dict[int, AnalogChannel] = field(default_factory=dict)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _part_bits(r: int, kind: int, d: int, b: int) -> int:
|
|
327
|
+
if kind == PACKED:
|
|
328
|
+
return r * b
|
|
329
|
+
per = (2 * b if kind & MINMAX else 0) + (b + (d - 1).bit_length() if kind & SUM else 0)
|
|
330
|
+
return -(-r // d) * per
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def analog_block_bytes(r: int, order: list[int], carriage: dict[int, tuple[int, int]], b: int) -> int:
|
|
334
|
+
"""The bytes of one format 2 block of r samples: every channel's part, each ending on a byte (§1.4)."""
|
|
335
|
+
return sum((_part_bits(r, *carriage.get(k, (PACKED, 1)), b) + 7) // 8 for k in order)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def analog_segment_bytes(samples: int, order: list[int], carriage: dict[int, tuple[int, int]], block: int,
|
|
339
|
+
b: int) -> int:
|
|
340
|
+
"""A format 2 segment's bytes: whole blocks of L, the last of what is left (§1.4; a fixed size)."""
|
|
341
|
+
whole, rest = divmod(samples, block)
|
|
342
|
+
return (whole * analog_block_bytes(block, order, carriage, b)
|
|
343
|
+
+ (analog_block_bytes(rest, order, carriage, b) if rest else 0))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def read_analog_blocks(data: bytes, samples: int, order: list[int], carriage: dict[int, tuple[int, int]],
|
|
347
|
+
block: int, b: int) -> list[AnalogBlock]:
|
|
348
|
+
"""A format 2 segment's bytes (§1.4) -> its blocks. `order`: the layout's order, `carriage`: role -> (kind, d) as
|
|
349
|
+
configured (the others packed), `block`: the answer's L, `b`: the layout's b. Bytes left over, or too few, raise
|
|
350
|
+
ProtocolError."""
|
|
351
|
+
if len(data) != analog_segment_bytes(samples, order, carriage, block, b):
|
|
352
|
+
raise h.ProtocolError(f"format 2: {len(data)} bytes for {samples} samples "
|
|
353
|
+
f"({analog_segment_bytes(samples, order, carriage, block, b)} expected)")
|
|
354
|
+
out, pos = [], 0
|
|
355
|
+
for first in range(0, samples, block):
|
|
356
|
+
r = min(block, samples - first)
|
|
357
|
+
blk = AnalogBlock(first, r)
|
|
358
|
+
for k in order:
|
|
359
|
+
kind, d = carriage.get(k, (PACKED, 1))
|
|
360
|
+
nbytes = (_part_bits(r, kind, d, b) + 7) // 8
|
|
361
|
+
acc, at = int.from_bytes(data[pos:pos + nbytes], "little"), 0
|
|
362
|
+
pos += nbytes
|
|
363
|
+
|
|
364
|
+
def take(bits: int) -> int:
|
|
365
|
+
nonlocal at
|
|
366
|
+
v = acc >> at & ((1 << bits) - 1)
|
|
367
|
+
at += bits
|
|
368
|
+
return v
|
|
369
|
+
c = AnalogChannel(kind, d)
|
|
370
|
+
if kind == PACKED:
|
|
371
|
+
c.values = [take(b) for _ in range(r)]
|
|
372
|
+
else:
|
|
373
|
+
for _ in range(-(-r // d)):
|
|
374
|
+
if kind & MINMAX:
|
|
375
|
+
c.mins.append(take(b))
|
|
376
|
+
c.maxs.append(take(b))
|
|
377
|
+
if kind & SUM:
|
|
378
|
+
c.sums.append(take(b + (d - 1).bit_length()))
|
|
379
|
+
blk.channels[k] = c
|
|
380
|
+
out.append(blk)
|
|
381
|
+
return out
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ---- format 3, the timed states (§3.8) -----------------------------------------------------------------------------
|
|
385
|
+
ESCAPE = 0xFFFFFFFF # a delta that only moves time on: its state is no sample
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def read_state_timed(data: bytes, samples: int, w: int) -> list[tuple[int, int]]:
|
|
389
|
+
"""A format 3 segment's bytes -> (ticks from start, state) for each of its `samples` samples: the first delta
|
|
390
|
+
counts from start, the others from the sample before, an escape (0xFFFFFFFF) moves time on without a sample.
|
|
391
|
+
Bytes left over, or too few, raise ProtocolError."""
|
|
392
|
+
size = 4 + (w + 7) // 8
|
|
393
|
+
out, pos, ticks = [], 0, 0
|
|
394
|
+
while len(out) < samples:
|
|
395
|
+
if pos + size > len(data):
|
|
396
|
+
raise h.ProtocolError(f"format 3: {len(out)} of {samples} samples in {len(data)} bytes")
|
|
397
|
+
delta = int.from_bytes(data[pos:pos + 4], "little")
|
|
398
|
+
ticks += delta
|
|
399
|
+
if delta != ESCAPE:
|
|
400
|
+
out.append((ticks, int.from_bytes(data[pos + 4:pos + size], "little") & ((1 << w) - 1)))
|
|
401
|
+
pos += size
|
|
402
|
+
if pos != len(data):
|
|
403
|
+
raise h.ProtocolError(f"format 3: {len(data) - pos} bytes past the segment's {samples} samples")
|
|
404
|
+
return out
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def state_timed_length(data: bytes, samples: int, w: int) -> int:
|
|
408
|
+
"""The bytes `samples` samples of format 3 take at the start of `data` (escapes included)."""
|
|
409
|
+
size, pos, n = 4 + (w + 7) // 8, 0, 0
|
|
410
|
+
while n < samples:
|
|
411
|
+
if pos + size > len(data):
|
|
412
|
+
raise h.ProtocolError("format 3: the segment's bytes end inside its entries")
|
|
413
|
+
n += int.from_bytes(data[pos:pos + 4], "little") != ESCAPE
|
|
414
|
+
pos += size
|
|
415
|
+
return pos
|
oep_client/conformance.py
CHANGED
|
@@ -144,7 +144,7 @@ def checker_sources_sha256():
|
|
|
144
144
|
for name in CHECKER_SOURCES}
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
REVIEWED_CORE_SHA256 = '
|
|
147
|
+
REVIEWED_CORE_SHA256 = 'd63f99f1809146ad41f1643526c81417b366d7a9d85af3845daee6654a229a14' # oep-spec 02c08c9 docs/oep-core.ja.md
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
def require_reviewed_contract(registry, spec=None):
|
|
@@ -157,6 +157,23 @@ def require_reviewed_contract(registry, spec=None):
|
|
|
157
157
|
'checker migration required: core document differs from the reviewed checker contract')
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
def required_core_ops(registry, declared, describe):
|
|
161
|
+
"""fn 0's required ops (core §11): the eight core ops always; get with settings, state with save, network with a netif
|
|
162
|
+
(core §13, transports §7.3). declared: the ops fn 0 declares; describe: fn 0's describe TLVs by tag."""
|
|
163
|
+
core = {row['name']: row['code'] for row in registry['core']['op']}
|
|
164
|
+
required = {core[name] for name in ('confirm', 'list', 'describe', 'clock', 'open', 'end', 'keepalive', 'lock_state')}
|
|
165
|
+
if 'get' not in core: # a registry without the settings (core §13): nothing more to require
|
|
166
|
+
return required
|
|
167
|
+
setting, netif = registry['describe_common'].get('setting'), registry['core']['tlv']['describe'].get('netif')
|
|
168
|
+
if declared & {core[name] for name in ('get', 'set', 'unset', 'save', 'state')} or setting in describe:
|
|
169
|
+
required.add(core['get'])
|
|
170
|
+
if core['save'] in declared:
|
|
171
|
+
required.add(core['state'])
|
|
172
|
+
if netif in describe:
|
|
173
|
+
required.add(core['network'])
|
|
174
|
+
return required
|
|
175
|
+
|
|
176
|
+
|
|
160
177
|
def spec_identity(path):
|
|
161
178
|
path = Path(path).resolve(strict=True)
|
|
162
179
|
raw = (path / 'registry/oep-v1.toml').read_bytes()
|
|
@@ -376,7 +393,7 @@ class Checks:
|
|
|
376
393
|
tag = self.reg['describe_common']['ops']
|
|
377
394
|
require(tag in values, 'core describe missing current ops tag')
|
|
378
395
|
declared = ops(values[tag])
|
|
379
|
-
require(
|
|
396
|
+
require(required_core_ops(self.reg, declared, values) <= declared, 'mandatory core operation absent')
|
|
380
397
|
self.transport_declarations()
|
|
381
398
|
channels = values.get(self.reg['core']['tlv']['describe']['channels'], b'\0\0')
|
|
382
399
|
require(len(channels) == 2, 'channels must be u16')
|
|
@@ -17,8 +17,8 @@ def refused_stale(reply):
|
|
|
17
17
|
return reply[3:5]==b'\0\4' and refusal_carries('unavailable',reply[5:],b'\1\1\0\6')
|
|
18
18
|
from .conformance_host_deadline import reviewed_identity as core_identity
|
|
19
19
|
|
|
20
|
-
REVIEWED={'interfaces/oep-if-capture.ja.md':'
|
|
21
|
-
'interfaces/oep-if-common.ja.md':'
|
|
20
|
+
REVIEWED={'interfaces/oep-if-capture.ja.md':'ffd89a6a1abf006aea788a6b47b270e6ac0e6d8190a716f4123c4f499bd3a61e',
|
|
21
|
+
'interfaces/oep-if-common.ja.md':'25fed0b1b8b7f246a8b71d02cea810249803b1c3c1e9c3df466c04f38687f70c'}
|
|
22
22
|
|
|
23
23
|
def reviewed_identity(path):
|
|
24
24
|
identity=core_identity(path)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Selected-SPEC cross-rate immediate group acquisition checks with an independent waveform oracle.
|
|
2
2
|
|
|
3
|
-
Self-contained for capture
|
|
3
|
+
Self-contained for capture: no imports of probe encoders, Host decoders,
|
|
4
4
|
the bench endpoint or the other capture checkers. Layouts are decoded from the
|
|
5
5
|
configure answers; the waveform and clock are the documented fixture's.
|
|
6
6
|
"""
|
|
@@ -3,7 +3,7 @@ import struct
|
|
|
3
3
|
from .conformance import require
|
|
4
4
|
from .conformance_host_core import HostCoreShapeChecks,CORE_OPS,fixed_vectors,result,field
|
|
5
5
|
|
|
6
|
-
# Reviewed from core §1.1/4.3/6.4/7 (
|
|
6
|
+
# Reviewed from core §1.1/4.3/6.4/7 (confirm and list entries carry no flags), not inferred from a generated schema.
|
|
7
7
|
# Each tuple lists all mandatory request fields and fixed response prefixes.
|
|
8
8
|
CORE_SHAPES=(
|
|
9
9
|
('confirm',1,(('magic',4),('min_rev',1),('max_rev',1)),(('magic',4),('revision',1),('max_frame',2),('window',4),('max_inflight',1),('boot_id',4))),
|
|
@@ -8,7 +8,7 @@ import struct
|
|
|
8
8
|
from .conformance import ops, require, tlvs
|
|
9
9
|
from . import conformance as _helpers
|
|
10
10
|
|
|
11
|
-
REVIEWED_CORE_SHA256 = '
|
|
11
|
+
REVIEWED_CORE_SHA256 = 'd63f99f1809146ad41f1643526c81417b366d7a9d85af3845daee6654a229a14' # oep-spec 02c08c9 docs/oep-core.ja.md
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class CoreSmokeChecks:
|
|
@@ -85,7 +85,8 @@ class CoreSmokeChecks:
|
|
|
85
85
|
identity = values.get(tags['unit_id'], b'').decode('ascii')
|
|
86
86
|
require(identity.lower() == self.unit.lower() and 1 <= len(identity) <= 32 and
|
|
87
87
|
all(c in 'abcdefghijklmnopqrstuvwxyz0123456789-' for c in identity), 'unit_id')
|
|
88
|
-
|
|
88
|
+
declared = ops(values.get(self.reg['describe_common']['ops'], b''))
|
|
89
|
+
require(_helpers.required_core_ops(self.reg, declared, values) <= declared, 'core ops')
|
|
89
90
|
maximum = values.get(tags['max_op_ms'], b'')
|
|
90
91
|
require(len(maximum) == 4 and 1 <= int.from_bytes(maximum, 'little') <= 600000, 'max_op_ms')
|
|
91
92
|
require(any(t == tags['transport'] and len(v) == 3 and v[0] == self.observed['confirm']['transport']
|