oep-client-python 0.0.38__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.
Files changed (45) hide show
  1. oep_client/__init__.py +1 -1
  2. oep_client/__main__.py +22 -7
  3. oep_client/capture.py +137 -7
  4. oep_client/capture_blocks.py +123 -2
  5. oep_client/conformance.py +19 -2
  6. oep_client/conformance_capture_serial.py +2 -2
  7. oep_client/conformance_capture_timeline.py +1 -1
  8. oep_client/conformance_core_fixed_fields.py +1 -1
  9. oep_client/conformance_core_smoke.py +3 -2
  10. oep_client/conformance_host_core.py +14 -8
  11. oep_client/conformance_host_core_sample.py +7 -5
  12. oep_client/conformance_host_deadline.py +3 -3
  13. oep_client/conformance_host_deadline_sample.py +1 -1
  14. oep_client/conformance_host_request_sample.py +27 -12
  15. oep_client/conformance_host_requests.py +66 -34
  16. oep_client/conformance_host_resend.py +1 -1
  17. oep_client/conformance_host_resend_sample.py +1 -1
  18. oep_client/conformance_host_tlv.py +1 -1
  19. oep_client/conformance_host_wait.py +2 -2
  20. oep_client/conformance_lease.py +1 -1
  21. oep_client/conformance_oep_smoke.py +4 -4
  22. oep_client/conformance_port_speed_execution.py +1 -1
  23. oep_client/conformance_response_race.py +1 -1
  24. oep_client/conformance_writer_lease.py +1 -1
  25. oep_client/endpoint.py +72 -25
  26. oep_client/fixture.py +389 -3
  27. oep_client/interfaces.py +25 -0
  28. oep_client/link.py +1 -1
  29. oep_client/pins.py +368 -58
  30. oep_client/probe_log.py +97 -0
  31. oep_client/registry.py +21 -1
  32. oep_client/settings.py +36 -7
  33. oep_client/stimulus.py +132 -0
  34. oep_client/targets.py +47 -10
  35. oep_client/virtual_bench.py +165 -13
  36. oep_client/virtual_bench_capture.py +582 -67
  37. oep_client/virtual_bench_serve.py +2 -2
  38. oep_client/virtual_bench_trigger.py +207 -45
  39. oep_client/virtual_core.py +2 -2
  40. oep_client/virtual_fixture_drive.py +404 -0
  41. {oep_client_python-0.0.38.dist-info → oep_client_python-0.0.40.dist-info}/METADATA +35 -22
  42. {oep_client_python-0.0.38.dist-info → oep_client_python-0.0.40.dist-info}/RECORD +45 -42
  43. {oep_client_python-0.0.38.dist-info → oep_client_python-0.0.40.dist-info}/WHEEL +1 -1
  44. {oep_client_python-0.0.38.dist-info → oep_client_python-0.0.40.dist-info}/entry_points.txt +0 -0
  45. {oep_client_python-0.0.38.dist-info → oep_client_python-0.0.40.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.38"
5
+ __version__ = "0.0.40"
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 /run/board-identify/by-id/<probe> oep dump --virtual p4-x035 --prefix oep.target --json
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("--steps", default="classify,hold,scan,identify,reset,labels",
105
- help="the steps to run, comma-separated (default all: classify, hold, scan, identify, reset, labels)")
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>.nrst / <name>.power_hi (a probe "
108
- "serving several targets; default: nrst / power_hi)")
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 == 'unique-candidate' else 1
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 a09fa45
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) -> Config:
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. Format 4 (test counter) needs nothing else. These go critical.
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 | CRITICAL, struct.pack("<I", 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 == DATA_FORMAT["logic_blocks"]:
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
 
@@ -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 = '4c5dcdcc779c5ba0ee85060a33b2b629deddad3e8f8f1a22e18bdeea9d471bdc' # oep-spec a09fa45 docs/oep-core.ja.md
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(set(self.core.values()) <= declared, 'mandatory core operation absent')
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':'430950c71e6bbd78b9e8233db075a99ab6587ad99b37aae30132d6e66eced9d9',
21
- 'interfaces/oep-if-common.ja.md':'4f1cfb88de976f5e934a0526edda3fbf302b51243b43c7faac0b3531d2f9975a'}
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 a09fa45: no imports of probe encoders, Host decoders,
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 (oep-spec a09fa45: confirm and list entries carry no flags), not inferred from a generated schema.
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 = '4c5dcdcc779c5ba0ee85060a33b2b629deddad3e8f8f1a22e18bdeea9d471bdc' # oep-spec a09fa45 docs/oep-core.ja.md
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
- require(set(self.core.values()) <= ops(values.get(self.reg['describe_common']['ops'], b'')), 'core ops')
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']