oep-client-python 0.0.44__py3-none-any.whl → 0.0.45__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 (51) hide show
  1. oep_client/__init__.py +1 -1
  2. oep_client/__main__.py +4 -4
  3. oep_client/acquire.py +6 -3
  4. oep_client/capture.py +20 -50
  5. oep_client/conformance/__init__.py +47 -6
  6. oep_client/conformance/allocator.py +1 -1
  7. oep_client/conformance/capture_serial.py +1 -1
  8. oep_client/conformance/core_smoke.py +1 -1
  9. oep_client/conformance/cut.py +1 -1
  10. oep_client/conformance/data.py +1 -1
  11. oep_client/conformance/dependencies.py +1 -1
  12. oep_client/conformance/force_hw.py +147 -0
  13. oep_client/conformance/gpio_lifecycle.py +2 -2
  14. oep_client/conformance/host_core.py +1 -1
  15. oep_client/conformance/host_deadline.py +2 -2
  16. oep_client/conformance/host_request_sample.py +1 -2
  17. oep_client/conformance/host_requests.py +10 -15
  18. oep_client/conformance/host_wait.py +2 -2
  19. oep_client/conformance/lease.py +1 -1
  20. oep_client/conformance/notification_sequence.py +1 -1
  21. oep_client/conformance/notify_hw.py +322 -0
  22. oep_client/conformance/oep_smoke.py +2 -2
  23. oep_client/conformance/recovery.py +1 -1
  24. oep_client/conformance/release_hw.py +123 -0
  25. oep_client/conformance/retention.py +2 -2
  26. oep_client/conformance/retention_lifecycle.py +2 -2
  27. oep_client/conformance/retry.py +1 -1
  28. oep_client/conformance/routes.py +1 -1
  29. oep_client/conformance/serial.py +5 -0
  30. oep_client/conformance/tcp_peers.py +17 -8
  31. oep_client/conformance/tcp_recovery.py +1 -1
  32. oep_client/conformance/usb.py +24 -7
  33. oep_client/conformance/usb_cases.py +40 -1
  34. oep_client/conformance/usb_hw.py +313 -0
  35. oep_client/conformance/writer_lease.py +1 -1
  36. oep_client/endpoint.py +7 -17
  37. oep_client/host.py +1 -1
  38. oep_client/interfaces.py +3 -0
  39. oep_client/link.py +60 -60
  40. oep_client/memo.py +101 -0
  41. oep_client/pins.py +1 -1
  42. oep_client/registry.py +7 -3
  43. oep_client/speed_record.py +2 -2
  44. oep_client/virtual_bench.py +15 -44
  45. oep_client/virtual_bench_capture.py +50 -276
  46. oep_client/virtual_bench_serve.py +1 -1
  47. {oep_client_python-0.0.44.dist-info → oep_client_python-0.0.45.dist-info}/METADATA +1 -1
  48. {oep_client_python-0.0.44.dist-info → oep_client_python-0.0.45.dist-info}/RECORD +51 -46
  49. {oep_client_python-0.0.44.dist-info → oep_client_python-0.0.45.dist-info}/WHEEL +0 -0
  50. {oep_client_python-0.0.44.dist-info → oep_client_python-0.0.45.dist-info}/entry_points.txt +0 -0
  51. {oep_client_python-0.0.44.dist-info → oep_client_python-0.0.45.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.44"
5
+ __version__ = "0.0.45"
oep_client/__main__.py CHANGED
@@ -11,7 +11,7 @@
11
11
  port, IP; each verified first by confirm + describe's unit_id, host guide
12
12
  §4.1 - one that fails is dropped with a warning)
13
13
  oep speed <probe> [--candidates 921600,500000] [--verify [--flows in:2,out:2]] (port_speed, oep-if-link §3 and the host
14
- guide §17: try the candidates in order on a UART bridge, report; default
14
+ the port_speed procedure (README): try the candidates in order on a UART bridge, report; default
15
15
  500000 - a faster rate only when named, after its 1 s verify each way)
16
16
  oep linktest <probe> --rates now,921600 --patterns in,out,duplex --inflight 1,2 --sizes 128,496 --frames 300
17
17
  oep pins <probe> --power 5 --wire swio (find the target's debug pins and reset line: classify the channels, scan,
@@ -64,13 +64,13 @@ def main(argv=None) -> int:
64
64
  sp.add_argument("--candidates", default="", help="the candidates, comma-separated (default 500000, the default "
65
65
  "ceiling of oep-if-link §3 obligation 7). A rate above 500000 is tried only when named here, and "
66
66
  "is committed only after full frames ran 1 s each way (in, out; duplex too with --verify's "
67
- "default flows) at it (host guide §17.3.3)")
67
+ "default flows) at it (the port_speed procedure (README))")
68
68
  sp.add_argument("--flows", default="", help="the flows to verify, comma-separated FLOW[:N] (in, out, duplex; N in "
69
69
  "flight, 0 = the most the link keeps; default: all three at that N). Selects --verify")
70
70
  form = sp.add_mutually_exclusive_group()
71
- form.add_argument("--verify", action="store_true", help="the full form (guide §17.3): baseline, the flows measured "
71
+ form.add_argument("--verify", action="store_true", help="the full form (the port_speed procedure (README)): baseline, the flows measured "
72
72
  "at every candidate, a failed flow run again one at a time")
73
- form.add_argument("--minimal", action="store_true", help="the minimal form (guide §17.2, the default): try, confirm, "
73
+ form.add_argument("--minimal", action="store_true", help="the minimal form (the port_speed procedure (README), the default): try, confirm, "
74
74
  "commit, no measurement")
75
75
  sp.add_argument("--frames", type=int, default=link.FLOW_FRAMES, help="frames per flow when verifying (default 16)")
76
76
  sp.add_argument("--no-record", action="store_true", help="do not read or write the record of passed / failed rates "
oep_client/acquire.py CHANGED
@@ -41,6 +41,8 @@ from dataclasses import dataclass, field
41
41
  from fractions import Fraction
42
42
  from pathlib import Path
43
43
 
44
+ from . import registry as _reg
45
+
44
46
  __all__ = ["Analog", "Bits", "Changes", "OnDisk", "Part", "Request", "Result", "capture", "capture_at"]
45
47
 
46
48
  TRIGGERS = ("high", "low", "rise", "fall", "both")
@@ -338,13 +340,14 @@ def from_interleaved(data: bytes | bytearray | memoryview, names: list[str], wid
338
340
  return out
339
341
 
340
342
 
341
- TRIGGER = {"high": (1, 1), "low": (1, 0), "rise": (2, 0), "fall": (2, 1), "both": (2, 2)} # (type, value), oep-if-capture
342
- TAG_CHIP, TAG_MODEL, TAG_FIRMWARE, TAG_UNIT_ID = 0x4C, 0x41, 0x40, 0x42 # core describe
343
+ _LEVEL, _EDGE = (_reg.FIXTURE_LOGIC.enum["trigger"][k] for k in ("level", "edge"))
344
+ TRIGGER = {"high": (_LEVEL, 1), "low": (_LEVEL, 0), "rise": (_EDGE, 0), "fall": (_EDGE, 1), "both": (_EDGE, 2)} # (type, value), capture §3.3
345
+ TAG_CHIP, TAG_MODEL, TAG_FIRMWARE, TAG_UNIT_ID = (_reg.CORE.tlv["describe"][k] for k in ("chip", "model", "firmware", "unit_id"))
343
346
  # UART link rates tried by default (port_speed): fastest first. oep-client refuses a rate that breaks and drops back
344
347
  # to the boot speed on repeated broken frames, so trying never ends slower than the boot speed; capture_at's
345
348
  # fast=[] keeps the boot speed, fast=[RATE, ...] tries those.
346
349
  FAST = [1_500_000, 921_600, 500_000]
347
- PER_SESSION = 2 # candidates tried per open: each costs up to ~1 s, plus a wait when it fails (core §3.5)
350
+ PER_SESSION = 2 # candidates tried per open: each costs up to ~1 s, plus a wait when it fails (oep-if-link §3)
348
351
  FLOWS = [("in", 0)] # a capture reads back: verify probe -> host only, at the link's largest in-flight n
349
352
 
350
353
 
oep_client/capture.py CHANGED
@@ -51,12 +51,9 @@ 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
54
+ # §3.8, optional (a probe declares them in its describe): configure TLVs by name, sent critical
56
55
  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")}
56
+ LOGIC_USED = {k: _CAP.tlv["configure_answer"][k] for k in ("glitch_used", "timebase")}
60
57
  NO_QUALIFIER = 0xFF # clock: qualifier_role none (§3.8)
61
58
  STREAM_BUFFER = _CAP.tlv["configure_answer"]["stream_buffer"]
62
59
  JUDGE_LATENCY = _CAP.tlv["configure_answer"]["judge_latency_ns"]
@@ -180,7 +177,6 @@ class Config:
180
177
  raw_roles: list[int] = field(default_factory=list) # format 1: the raw channels' roles (as asked)
181
178
  event_d: dict[int, int] = field(default_factory=dict) # format 1: events role -> D (as asked)
182
179
  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
180
  timebase: Fraction | None = None # format 3: the delta's tick rate (Hz)
185
181
  external_clock: bool = False # sampled on a clock line (actual_rate 0 / 1, §3.8)
186
182
  used: dict[str, dict[int, int] | int] = field(default_factory=dict) # §3.8's *_used: name -> group/role -> value
@@ -322,7 +318,7 @@ class Event:
322
318
  serial: int | None = None
323
319
  trigger_index: int | None = None
324
320
  trigger_ns: int | None = None
325
- record: int | None = None # triggered: the trigger record's serial (None: no record, §3.10)
321
+ record: int | None = None # triggered: the trigger record's serial (None: no record, §3.9)
326
322
  sample: int | None = None # triggered: the original sample from the generation's start
327
323
  uncertainty_ns: int | None = None # triggered: trigger_ns's +-ns
328
324
  trigger_fn: int | None = None # a group's triggered: the track that judged it (0: force, timer)
@@ -368,7 +364,7 @@ def unpack_event(frame: bytes, group: bool = False) -> Event:
368
364
 
369
365
 
370
366
  NO_RECORD = 0xFFFFFFFF
371
- TRIGGER_RECORD_BYTES = 24 # serial u32, sample u64, trigger_ns u64, uncertainty_ns u32 (§3.10)
367
+ TRIGGER_RECORD_BYTES = 24 # serial u32, sample u64, trigger_ns u64, uncertainty_ns u32 (§3.9)
372
368
 
373
369
 
374
370
  CAPTURE_DROPS = "io.github.open-embedded-probe.capture-drops"
@@ -422,7 +418,7 @@ class CaptureDrops:
422
418
  @dataclass(frozen=True)
423
419
  class TriggerOpts:
424
420
  """configure's trigger_opts (§3.3): `arm` 1 looks once the pretrigger is there (0: at once, a short segment);
425
- `rearm` 1 a trigger for every segment (streaming: a record for every event, §2.1, §3.10); `when` 1 a level
421
+ `rearm` 1 a trigger for every segment (streaming: a record for every event, §2.1, §3.9); `when` 1 a level
426
422
  already true at arming counts; `hysteresis` (analog cross, in ADC values) back that far before the next crossing.
427
423
  The defaults are every probe's; the others as describe's trigger opts says."""
428
424
  arm: int = 1
@@ -433,7 +429,7 @@ class TriggerOpts:
433
429
 
434
430
  @dataclass(frozen=True)
435
431
  class TriggerRecord:
436
- """A trigger record (§3.10): serial (from 0 in the generation), the original sample from the generation's start,
432
+ """A trigger record (§3.9): serial (from 0 in the generation), the original sample from the generation's start,
437
433
  trigger_ns and its uncertainty."""
438
434
  serial: int
439
435
  sample: int
@@ -452,23 +448,18 @@ def take_events(link, fn: int) -> list[bytes]:
452
448
 
453
449
 
454
450
  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"}
451
+ """§3.8's configure TLVs, critical (configure's `options`)."""
457
452
  out = b""
458
453
  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,))))
454
+ tag = OPTION_TLV[name] | CRITICAL
455
+ if name == "glitch_filter": # per group (group 0 only, §3.8)
456
+ for group, ns in sorted(value.items()):
457
+ out += m.tlv(tag, bytes([group]) + struct.pack("<I", ns))
463
458
  elif name == "clock":
464
459
  role, edge, qualifier, level = value
465
460
  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]))
461
+ else:
462
+ raise ValueError(f"capture configure: no option {name!r} (oep-if-capture §3.8)")
472
463
  return out
473
464
 
474
465
 
@@ -514,27 +505,12 @@ def _config(payload: bytes, analog: bool) -> Config:
514
505
  c.skew_ns[role] = ns
515
506
  elif tag == FRONTEND_USED and analog and len(v) == 2:
516
507
  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
508
  elif not analog and tag == LOGIC_USED["timebase"] and len(v) == 8:
520
509
  num, den = struct.unpack("<II", v)
521
510
  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
511
  elif not analog and tag == LOGIC_USED["glitch_used"] and len(v) == 5:
526
512
  group, ns = struct.unpack("<BI", v)
527
513
  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]
538
514
  elif tag == REFERENCE and analog and len(v) == 6:
539
515
  source, mv, how = struct.unpack("<BIB", v)
540
516
  # how: nominal False, measured True; an unknown value is shown as it is, not as nominal (core §2.4)
@@ -566,7 +542,7 @@ class LogicCapture(Interface):
566
542
  frontends: dict[int, int] | None = None, trigger_opts: TriggerOpts | None = None,
567
543
  data_format: int = 0, events: dict[int, int] | None = None, roles: list[int] | None = None,
568
544
  block_max: int | None = None, carriage: dict[int, tuple[int, int]] | None = None,
569
- format_name: str | None = None, options: dict | None = None) -> Config:
545
+ options: dict | None = None) -> Config:
570
546
  """-> the probe's actual values. A value the probe cannot honour is refused: host.Unsupported, .tag = the TLV as
571
547
  sent (§3.3). The TLVs go without the critical bit (§3.3). Read Config.samples / .segments: the probe rounds
572
548
  samples down.
@@ -575,13 +551,11 @@ class LogicCapture(Interface):
575
551
  carried as events (the others raw), `roles` the plan's roles (to name the raw channels), `block_max` the
576
552
  longest block (§3.3's table: without the critical bit). Format 2 (analog blocks, §1.4): `carriage` role ->
577
553
  (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.
554
+ counter) needs nothing else. The others go critical.
579
555
 
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.
556
+ `options` (§3.8, critical; a probe without one refuses it): "glitch_filter" group -> ns (group 0 only),
557
+ "clock" (role, edge, qualifier role or None, qualifier level). The answer's values in use: Config.used, by the
558
+ answer TLV's name.
585
559
 
586
560
  §3.3's contract is checked before anything is sent (ValueError): `samples` is needed in one-shot and repeat
587
561
  and not taken in streaming, `segments` is repeat's only, `pretrigger` needs a trigger (type other than 0) - a
@@ -617,10 +591,6 @@ class LogicCapture(Interface):
617
591
  body += tlv(FRONTEND, bytes([role, fe]))
618
592
  if data_format: # critical: a probe without it refuses (§3.3)
619
593
  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
594
  body += _options(options or {})
625
595
  if data_format == DATA_FORMAT["logic_blocks"] and roles is None:
626
596
  raise ValueError("capture configure: format 1 needs the plan's roles (to name the raw channels)")
@@ -806,7 +776,7 @@ class LogicCapture(Interface):
806
776
  return out, bool(more)
807
777
 
808
778
  def triggers_page(self, from_serial: int = 0, generation: int | None = None) -> tuple[list[TriggerRecord], bool]:
809
- """One answer's trigger records (§3.10) from `from_serial` on, paged as segments are. -> (records, more)."""
779
+ """One answer's trigger records (§3.9) from `from_serial` on, paged as segments are. -> (records, more)."""
810
780
  payload = struct.pack("<II", self._generation(generation), from_serial)
811
781
  rd = m.Reader(self._call(self.TRIGGERS, payload, locked=False).payload)
812
782
  more, count = rd.take("BB")
@@ -879,7 +849,7 @@ class LogicCapture(Interface):
879
849
  unavailable (cause 6), so an old read never gets the next capture's bytes.
880
850
 
881
851
  The reads need no lock and go without the session id, so a repeat after a broken reply is simply run again
882
- (reads are not deduplicated, oep-core §5.2). They do not extend the lease, though: a long read (64 KB over a
852
+ (reads are not deduplicated, oep-core §5.1). They do not extend the lease, though: a long read (64 KB over a
883
853
  115200 bps UART probe takes seconds) sends a keepalive between batches, or the lease lapsed mid-read, the plan
884
854
  went with it (core §9) and the capture read back nothing (2026-09-26, V003 jig)."""
885
855
  g = self._generation(generation)
@@ -66,7 +66,7 @@ SERIAL_CASES = tuple(('CORE-SERIAL-' + kind.upper(), kind) for kind in
66
66
 
67
67
 
68
68
  RECONNECT_CASES = tuple(('CORE-RECONNECT-' + kind.upper(), kind) for kind in
69
- ('session', 'replay', 'ended', 'lease'))
69
+ ('session', 'replay', 'ended', 'lease', 'unread'))
70
70
 
71
71
 
72
72
  TCP_CASES = tuple(('CORE-TCP-' + kind.upper(), kind) for kind in
@@ -75,6 +75,7 @@ TCP_CASES = tuple(('CORE-TCP-' + kind.upper(), kind) for kind in
75
75
 
76
76
  TCP_PEER_CASES = tuple(('CORE-TCP-PEER-' + kind.upper(), kind) for kind in
77
77
  ('identity', 'lock', 'route', 'partial', 'close', 'oversized'))
78
+ ROUTE_PEER_CASES = tuple(('CORE-ROUTE-PEER-' + kind.upper(), kind) for kind in ('identity', 'lock', 'route'))
78
79
 
79
80
 
80
81
  class NotApplicable(Exception):
@@ -121,7 +122,7 @@ CHECKER_SOURCES = ('__init__.py', 'core_smoke.py', 'session_smoke.py', 'interfac
121
122
  'port_speed.py', 'port_speed_sample.py', 'port_speed_lifecycle.py', 'port_speed_execution.py', 'port_speed_execution_sample.py',
122
123
  'host_tlv.py', 'host_tlv_sample.py', 'host_core.py', 'core_fixed_fields.py', 'probe_input.py', 'probe_input_sample.py', 'refusal_order.py', 'refusal_order_sample.py', 'restart.py', 'restart_sample.py', 'host_wait.py', 'host_wait_sample.py', 'host_deadline.py', 'host_deadline_sample.py', 'host_resend.py', 'host_resend_sample.py', 'tcp_recovery.py', 'tcp_recovery_sample.py', 'host_core_sample.py', 'allocator.py', 'allocator_sample.py', 'discovery.py', 'clock.py', 'clock_sample.py',
123
124
  'notification_sequence.py', 'notification_sequence_sample.py', 'notification_retention.py', 'notification_retention_sample.py', 'serial.py', 'tcp.py',
124
- 'tcp_peers.py', 'usb.py', 'usb_cases.py',
125
+ 'tcp_peers.py', 'usb.py', 'usb_cases.py', 'usb_hw.py', 'force_hw.py', 'notify_hw.py', 'release_hw.py',
125
126
  '../pytest_conformance.py', 'resources.py', 'resource_sample.py',
126
127
  'subscriptions.py', 'subscription_sample.py',
127
128
  'data.py', 'data_sample.py',
@@ -144,7 +145,7 @@ def checker_sources_sha256():
144
145
  for name in CHECKER_SOURCES}
145
146
 
146
147
 
147
- REVIEWED_CORE_SHA256 = '699f7eb6333b6eb6678ddcf27074c03544f0f0a6ee0ef6ccdf02edd3757f962e' # oep-spec docs/oep-core.ja.md
148
+ REVIEWED_CORE_SHA256 = 'b90ae1b15f10442ff411929e80d8457e754d5f11f227167d3c142f54ff14768a' # oep-spec docs/oep-core.ja.md
148
149
 
149
150
 
150
151
  def require_reviewed_contract(registry, spec=None):
@@ -208,10 +209,12 @@ class Checks:
208
209
  self.abort = False
209
210
  self.wire = None
210
211
  self.reopen = None
212
+ self.post = None # with reopen: write one framed request and read nothing (the answer is left behind)
211
213
  self.tcp_wire = None
212
214
  self.usb_wire = None
213
215
  self.tcp_peer_open = None
214
216
  self.tcp_reopen = None
217
+ self.route_peer = None # another route of the same probe: (open, Checks attribute, this route's wire)
215
218
 
216
219
  def request(self, op, payload=b'', *, session=0, corr=None, fn=0):
217
220
  automatic = corr is None
@@ -452,7 +455,17 @@ class Checks:
452
455
  def reconnect(self, kind):
453
456
  lease = 1000 if kind == 'lease' else 60000
454
457
  with self.holding(lease) as sid:
455
- request, original = self.clock(sid)
458
+ if kind == 'unread':
459
+ # the answer is lost with the connection: the request goes on the old one, nothing read (core §5.1,
460
+ # transports §3: what was not yet sent on a closed route is dropped)
461
+ if self.post is None:
462
+ raise NotApplicable('no raw write on this route')
463
+ request, original = self.request(self.core['clock'], session=sid), None
464
+ self.post(request)
465
+ time.sleep(0.3)
466
+ posted = time.monotonic_ns()
467
+ else:
468
+ request, original = self.clock(sid)
456
469
  if kind == 'ended':
457
470
  tlvs(self.success(self.request(self.core['end'], session=sid)))
458
471
  self.session = None
@@ -491,6 +504,19 @@ class Checks:
491
504
  self.session = sid # clean up buggy resurrection
492
505
  require(reply[3:5] == bytes((0, self.reasons['no_session'])),
493
506
  'reconnect forgot ended session and allowed reopening')
507
+ elif kind == 'unread':
508
+ # the same request on the new route: the recorded answer or result_lost, never run again - a
509
+ # recorded clock answer is older than the time since it was posted, a run again is not
510
+ reply = self.exchange(request)
511
+ if reply[3:5] != bytes((0, self.reasons['result_lost'])):
512
+ require(reply[3:5] == b'\x01\0' and len(reply) >= 17, 'resend after the route closed: '
513
+ 'neither the recorded answer nor result_lost')
514
+ then = int.from_bytes(reply[9:17], 'little')
515
+ waited = time.monotonic_ns() - posted
516
+ _, now = self.clock()
517
+ require(int.from_bytes(now[4:12], 'little') - then >= waited - 50_000_000,
518
+ 'the request resent on the new route ran again (core §5.1)')
519
+ tlvs(self.success(self.request(self.core['keepalive'], session=sid)))
494
520
  elif kind == 'lease':
495
521
  time.sleep(0.6)
496
522
  state = self.success(self.request(self.core['lock_state']))
@@ -987,10 +1013,16 @@ class Checks:
987
1013
  for name, kind in RECONNECT_CASES:
988
1014
  self.check(name, 'transports §3; core §5.1/6.1/9', lambda kind=kind: self.reconnect(kind))
989
1015
  if self.usb_wire is not None:
990
- from .usb_cases import USB_CASES, HID_CASES, run
991
- cases = USB_CASES + (HID_CASES if self.usb_wire.kind == 'hid' else ())
1016
+ from .usb_cases import USB_CASES, BULK_CASES, HID_CASES, run
1017
+ cases = USB_CASES + (HID_CASES if self.usb_wire.kind == 'hid' else BULK_CASES)
992
1018
  for name, kind in cases:
993
1019
  self.check(name, 'transports §1/2/3; core §2.4', lambda kind=kind: run(self, kind))
1020
+ if self.route_peer is not None:
1021
+ from .tcp_peers import Peers
1022
+ open_peer, attr, own = self.route_peer
1023
+ routes = Peers(self, open_peer, attr, own)
1024
+ for name, kind in ROUTE_PEER_CASES:
1025
+ self.check(name, 'transports §3; core §6', lambda kind=kind: getattr(routes, kind)())
994
1026
  if self.tcp_peer_open is not None:
995
1027
  from .tcp_peers import Peers
996
1028
  peers = Peers(self)
@@ -1065,6 +1097,8 @@ def main(argv=None):
1065
1097
  wire.stream.close()
1066
1098
  wire.stream = link.open_serial(args.address)
1067
1099
  checks.reopen = reopen
1100
+ from .serial import frame as serial_frame
1101
+ checks.post = lambda message: wire.stream.write(serial_frame(message))
1068
1102
  report.update(checks.run())
1069
1103
  finally:
1070
1104
  link._exclusive_off(wire.stream)
@@ -1075,6 +1109,13 @@ def main(argv=None):
1075
1109
  try:
1076
1110
  checks = Checks(wire.send, registry, args.unit)
1077
1111
  checks.tcp_wire = wire
1112
+ from .tcp import frame as tcp_frame
1113
+
1114
+ def reopen(): # a new connection: the route moves (transports §3)
1115
+ wire.stream.close()
1116
+ wire.stream = TcpWire.open(args.address).stream
1117
+ checks.reopen = reopen
1118
+ checks.post = lambda message: wire.stream.sendall(tcp_frame(message))
1078
1119
  if args.tcp_peer:
1079
1120
  checks.tcp_peer_open = lambda: TcpWire.open(args.tcp_peer)
1080
1121
  def reopen_tcp():
@@ -80,7 +80,7 @@ class AllocatorChecks(ResourceChecks):
80
80
  if self.c.results[-1]['status']!='passed':self.c.abort=True
81
81
  for name,case in [('BOUNDARY',self.boundary),('END',self.ended_boundary),('EXPIRY',lambda:self.ended_boundary(True)),
82
82
  ('FORCE',self.force_boundary),('REPLAY',self.replay_boundary),('REBOOT',self.reboot_boundary)]:
83
- first=len(self.a.trace);self.c.check('CORE-ALLOCATOR-'+name,'core §5.2/6/9',case);self.c.results[-1]['adapter_trace']=self.a.trace[first:]
83
+ first=len(self.a.trace);self.c.check('CORE-ALLOCATOR-'+name,'core §5.1/6/9',case);self.c.results[-1]['adapter_trace']=self.a.trace[first:]
84
84
  rows=self.c.results[start:]
85
85
  return dict(status='passed' if all(x['status']=='passed' for x in rows) else 'failed',checks=rows,adapter_trace=self.a.trace,
86
86
  observed=self.c.observed,full_conformance=False,scope='logical seeded u32 allocation boundary and session/reboot invariants',
@@ -17,7 +17,7 @@ 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 .host_deadline import reviewed_identity as core_identity
19
19
 
20
- REVIEWED={'interfaces/oep-if-capture.ja.md':'ffd89a6a1abf006aea788a6b47b270e6ac0e6d8190a716f4123c4f499bd3a61e',
20
+ REVIEWED={'interfaces/oep-if-capture.ja.md':'9bab21bc5e8cdf069448dea059af459b6d7a46757554ca7aee4196b04e1dccd6',
21
21
  'interfaces/oep-if-common.ja.md':'25fed0b1b8b7f246a8b71d02cea810249803b1c3c1e9c3df466c04f38687f70c'}
22
22
 
23
23
  def reviewed_identity(path):
@@ -8,7 +8,7 @@ import struct
8
8
  from . import ops, require, tlvs
9
9
  from .. import conformance as _helpers
10
10
 
11
- REVIEWED_CORE_SHA256 = '699f7eb6333b6eb6678ddcf27074c03544f0f0a6ee0ef6ccdf02edd3757f962e' # oep-spec docs/oep-core.ja.md
11
+ REVIEWED_CORE_SHA256 = 'b90ae1b15f10442ff411929e80d8457e754d5f11f227167d3c142f54ff14768a' # oep-spec docs/oep-core.ja.md
12
12
 
13
13
 
14
14
  class CoreSmokeChecks:
@@ -78,6 +78,6 @@ class CutChecks(DebugChecks):
78
78
  if self.c.results[-1]['status']!='passed':self.c.abort=True
79
79
  cases=[('REQUEST',self.request_cut),('REQUEST-HALTED',lambda:self.request_cut(True)),('REQUEST-PARTIAL',lambda:self.request_cut(partial=True)),('CONSOLE',self.console_cut),('CONSOLE-HALTED',lambda:self.console_cut(True)),('RECOVERY',self.recovery),('ISOLATED',self.isolated),('REJECTED',self.rejected)]
80
80
  for name,case in cases:
81
- first=len(self.a.trace);self.c.check('OEP-CUT-'+name,'debug §2; common §1/2; core §5.2/9',case);self.c.results[-1]['adapter_trace']=self.a.trace[first:]
81
+ first=len(self.a.trace);self.c.check('OEP-CUT-'+name,'debug §2; common §1/2; core §5.1/9',case);self.c.results[-1]['adapter_trace']=self.a.trace[first:]
82
82
  rows=self.c.results[start:]
83
83
  return dict(status='passed' if all(r['status']=='passed' for r in rows) else 'failed',checks=rows,adapter_trace=self.a.trace,observed=self.c.observed,full_conformance=False,scope='logical response writer before request wire-cut; console line loss and dependent release',unchecked=['physical writer/pins and actual transport delivery','writer concurrency, lease expiry while blocked and alternate budgets','resync/TO/framing marks','poll/wait DMI failure injection and store-trigger writes','production Host integration'])
@@ -301,7 +301,7 @@ class DataChecks(SubscriptionChecks):
301
301
  super().run()
302
302
  for name, method in DATA_CASES:
303
303
  first = len(self.notifications)
304
- self.c.check(name, 'core §11.2/11.3', getattr(self, method))
304
+ self.c.check(name, 'core §10.2/11.3', getattr(self, method))
305
305
  self.c.results[-1]['notification_observations'] = self.notifications[first:]
306
306
  rows = self.c.results[start:]
307
307
  return {'status': 'passed' if all(row['status'] == 'passed' for row in rows) else 'failed',
@@ -250,7 +250,7 @@ class DependencyChecks:
250
250
  cases += [('MARKS',self.marks),('MALFORMED',lambda:self.refusal(True)),('OWNER',self.refusal),('DETACH',self.detach),('BREAK',lambda:self.detach(True)),('REPLAY',self.replay),('DETACH-REPLAY',self.detach_replay),('SAME-SESSION',self.same_session)]
251
251
  cases += [(kind,lambda kind=kind:self.ending(kind)) for kind in ('END','EXPIRY','FORCE')]+[('LOCKS',self.locks)]
252
252
  for name,case in cases:
253
- first=len(self.a.trace);self.c.check('IF-DEPENDENCY-'+name,'common §1/2; core §5.2/9',case)
253
+ first=len(self.a.trace);self.c.check('IF-DEPENDENCY-'+name,'common §1/2; core §5.1/9',case)
254
254
  self.c.results[-1]['adapter_trace']=self.a.trace[first:]
255
255
  rows=self.c.results[start:]
256
256
  return {'status':'passed' if all(r['status']=='passed' for r in rows) else 'failed','full_conformance':False,
@@ -0,0 +1,147 @@
1
+ """force releases the resources of the session it takes from (core §6.4, §9; debug-common §2), on a real probe.
2
+
3
+ A session S attaches to a target over a wire (a debug connection: a numbered resource), then a session T of another id
4
+ opens with force. Then: S's connection is gone from connections, a detach of its number is rejected no_resource, S's
5
+ own requests are not completed, and T's attach to the same pins gets a new, larger number (numbers are never re-used
6
+ within a boot). T detaches and ends. The wire, its pins and the speed are named by the user (the target's wiring); the
7
+ route is the probe's USB vendor bulk, opened as conformance.usb_hw opens it. Numbers come from the SPEC registry.
8
+
9
+ python -m oep_client.conformance.force_hw --unit UNIT --wire oep.wire.rvswd --swdio 14 --swclk 15 \\
10
+ --spec SPEC --out NEW.json --lock LOCK
11
+ """
12
+ import argparse
13
+ import json
14
+ import os
15
+ import secrets
16
+ import struct
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+
20
+ from . import Checks, checker_sources_sha256, require, spec_identity, tlvs
21
+ from .usb import UsbWire
22
+
23
+
24
+ def interface(registry, name):
25
+ spec = next(i for i in registry['interface'] if i['name'] == name)
26
+ return {o['name']: o['code'] for o in spec['op']}
27
+
28
+
29
+ def connections(check, fn, op):
30
+ """The live connection numbers of the wire `fn` (lock-free, session 0; paged by first, debug §2.1)."""
31
+ first, found = 0, []
32
+ while True:
33
+ p = check.success(check.request(op, bytes((first,)), fn=fn))
34
+ require(len(p) >= 2, 'connections fixed part')
35
+ more, count = p[0], p[1]
36
+ at = 2
37
+ for _ in range(count):
38
+ require(len(p) >= at + 14, 'connections entry')
39
+ found.append(struct.unpack_from('<I', p, at)[0])
40
+ tid_len = p[at + 13]
41
+ at += 14 + tid_len
42
+ if not more or not count:
43
+ return found
44
+ first += count
45
+
46
+
47
+ def run(check, *, wire_name, swdio, swclk, speed):
48
+ ops = interface(check.reg, wire_name)
49
+ entries = check.list_snapshot()
50
+ fn = next((e['fn'] for e in entries if e['name'] == wire_name), None)
51
+ require(fn is not None, f'{wire_name} is not listed')
52
+ attach_body = struct.pack('<BHHI', 0, swdio, swclk, speed) # method 0: run, do not halt (debug §1)
53
+ out = {'wire_fn': fn}
54
+
55
+ def open_session(force):
56
+ sid = secrets.randbelow(0xffffffff) + 1
57
+ check.corr = 0
58
+ p = check.success(check.request(check.core['open'], struct.pack('<IB', 60000, force), session=sid))
59
+ require(len(p) >= 8 and struct.unpack_from('<I', p, 4)[0] == check.boot, 'open boot')
60
+ check.session = sid
61
+ return sid
62
+
63
+ def attach(sid):
64
+ p = check.success(check.request(ops['attach'], attach_body, session=sid, fn=fn))
65
+ require(len(p) >= 13, 'attach fixed part')
66
+ return struct.unpack_from('<I', p)[0]
67
+
68
+ s = open_session(0)
69
+ first = attach(s)
70
+ out['s_connection'] = first
71
+ require(first in connections(check, fn, ops['connections']), 'the attach is not in connections')
72
+ t = open_session(1) # another id, force: takes the lock from S (core §6.4)
73
+ out['t_session_differs'] = t != s
74
+ live = connections(check, fn, ops['connections'])
75
+ out['after_force'] = live
76
+ require(first not in live, "force left S's connection open (core §9)")
77
+ check.rejected(check.request(ops['detach'], struct.pack('<I', first), session=t, fn=fn), 'no_resource')
78
+ saved, check.corr = check.corr, 1000 # S's own numbers, far from T's
79
+ reply = check.exchange(check.request(check.core['keepalive'], session=s))
80
+ check.corr = saved
81
+ out['s_keepalive'] = reply[3:5].hex()
82
+ require(reply[3] == check.reg['resolutions']['rejected'], 'S kept a session after the force')
83
+ second = attach(t)
84
+ out['t_connection'] = second
85
+ require(second != first and second > first, 'a connection number was re-used or went back (core §9)')
86
+ tlvs(check.success(check.request(ops['detach'], struct.pack('<I', second), session=t, fn=fn)))
87
+ require(second not in connections(check, fn, ops['connections']), 'detach left the connection')
88
+ tlvs(check.success(check.request(check.core['end'], session=t)))
89
+ check.session = None
90
+ return out
91
+
92
+
93
+ def main(argv=None):
94
+ from .. import __version__
95
+ from ..hardware import equipment_lock
96
+ from .usb_hw import BulkBackend, find_device
97
+ parser = argparse.ArgumentParser(description=__doc__)
98
+ for name, env in [('unit', 'OEP_CONFORMANCE_UNIT_ID'), ('spec', 'OEP_CONFORMANCE_SPEC'),
99
+ ('out', 'OEP_CONFORMANCE_OUT'), ('lock', 'OEP_HW_LOCK')]:
100
+ parser.add_argument('--' + name, default=os.environ.get(env))
101
+ parser.add_argument('--wire', required=True, help='the wire interface name, e.g. oep.wire.rvswd')
102
+ parser.add_argument('--swdio', type=int, required=True, help="the target's SWDIO / SWIO channel")
103
+ parser.add_argument('--swclk', type=int, default=0xFFFF, help="the target's SWCLK channel (0xFFFF: none)")
104
+ parser.add_argument('--speed', type=int, default=1_000_000, help='attach max_speed (Hz)')
105
+ args = parser.parse_args(argv)
106
+ if not all(getattr(args, name) for name in ('unit', 'spec', 'out', 'lock')):
107
+ parser.error('explicit unit, SPEC checkout, new output file and existing shared lock required')
108
+ registry, identity = spec_identity(args.spec)
109
+ report = {'status': 'failed', 'spec': identity, 'client_version': __version__,
110
+ 'checker_sources_sha256': checker_sources_sha256(), 'expected_unit': args.unit,
111
+ 'wire': args.wire, 'pins': [args.swdio, args.swclk], 'full_conformance': False,
112
+ 'started_at': datetime.now(timezone.utc).isoformat()}
113
+ with Path(args.out).open('x') as artifact:
114
+ try:
115
+ with equipment_lock(args.lock):
116
+ dev = find_device(registry['usb']['project_vid'], registry['usb']['project_pid'], args.unit)
117
+ backend = BulkBackend(dev, registry['usb'])
118
+ try:
119
+ wire = UsbWire(backend, 'bulk', **backend.shape)
120
+ check = Checks(wire.send, registry, args.unit)
121
+ check.usb_wire = wire
122
+ check.confirm()
123
+ check.identity()
124
+ try:
125
+ report['observed'] = run(check, wire_name=args.wire, swdio=args.swdio, swclk=args.swclk,
126
+ speed=args.speed)
127
+ report['status'] = 'passed'
128
+ finally:
129
+ if check.session is not None: # a failed run: end what this run holds
130
+ try:
131
+ check.exchange(check.request(check.core['end'], session=check.session))
132
+ except Exception:
133
+ pass
134
+ report['trace'] = check.trace
135
+ finally:
136
+ backend.close()
137
+ except Exception as exc:
138
+ report.update(status='failed', error=f'{type(exc).__name__}: {exc}')
139
+ report['finished_at'] = datetime.now(timezone.utc).isoformat()
140
+ json.dump(report, artifact, ensure_ascii=False, indent=2, default=str)
141
+ artifact.write('\n')
142
+ print(f"{report['status']}: {args.out}" + (f" ({report.get('error')})" if report.get('error') else ''))
143
+ return int(report['status'] != 'passed')
144
+
145
+
146
+ if __name__ == '__main__':
147
+ raise SystemExit(main())
@@ -59,11 +59,11 @@ class GpioLifecycleChecks(GpioChecks):
59
59
  specs={s['name']:s for s in self.c.reg['interface']};require(all(specs[n]['revision']==1 for n in self.a.selection.values()),'selected revision differs')
60
60
  self.codes={fn:{r['name']:r['code'] for r in specs[name]['op']} for fn,name in self.a.selection.items()};self.assignment=specs['oep.probe.plan']['tlv']['plan_apply']['role_assignment']
61
61
  self.c.confirm();self.c.identity();self.c.declarations();entries=self.c.list_snapshot();require(all(any(r['fn']==fn and r['name']==name and r['revision']==1 for r in entries) for fn,name in self.a.selection.items()),'selected fn identity differs');require(self.a.state()['clock_scope']=='explicit-logical-ms','explicit clock required')
62
- self.c.check('CORE-GPIO-LIFECYCLE-CONTEXT','core §5.2/6/8/9; logical fixture',context)
62
+ self.c.check('CORE-GPIO-LIFECYCLE-CONTEXT','core §5.1/6/8/9; logical fixture',context)
63
63
  if self.c.results[-1]['status']!='passed':self.c.abort=True
64
64
  for kind in self.cases:
65
65
  action=(lambda kind=kind:self.expiry(kind)) if kind.startswith('EXPIR') else self.ended_id if kind=='ENDED-ID' else self.same_id if kind=='SAME-ID-OPEN' else (lambda:self.force(False)) if kind=='FORCE' else (lambda:self.force(True)) if kind=='FORCE-OLD' else (lambda:self.renewed(False)) if kind=='KEEPALIVE' else (lambda:self.renewed(True)) if kind=='REJECTED-RENEWAL' else self.nonrenewal
66
66
  def isolated(action=action):self.fresh();action()
67
- first=len(self.a.trace);self.c.check('CORE-GPIO-LIFECYCLE-'+kind,'core §5.2/6/8/9; GPIO plan resources',isolated);self.c.results[-1]['adapter_trace']=self.a.trace[first:]
67
+ first=len(self.a.trace);self.c.check('CORE-GPIO-LIFECYCLE-'+kind,'core §5.1/6/8/9; GPIO plan resources',isolated);self.c.results[-1]['adapter_trace']=self.a.trace[first:]
68
68
  rows=self.c.results[start:]
69
69
  return dict(status='passed' if all(r['status']=='passed' for r in rows) else 'failed',checks=rows,observed=self.c.observed,adapter_trace=self.a.trace,full_conformance=False,scope='core session ownership/expiry/replay with explicit logical GPIO plan resources',unchecked=['physical pin transition timing and persisted config idle/disable','deferred writer/force concurrency with these GPIO resources','other fixture plan role constraints/channel_group','production Host and other standard operation behaviors'])
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  import struct
5
5
  from . import require,spec_identity
6
6
 
7
- REVIEWED_CORE_SHA256='699f7eb6333b6eb6678ddcf27074c03544f0f0a6ee0ef6ccdf02edd3757f962e'
7
+ REVIEWED_CORE_SHA256='b90ae1b15f10442ff411929e80d8457e754d5f11f227167d3c142f54ff14768a'
8
8
  CORE_OPS={'confirm':1,'list':2,'describe':3,'clock':4,'open':16,'end':17,'keepalive':18,'lock_state':19}
9
9
  # the settings' ops (core §13.5 / §13.7, transports §7.3): their numbers reviewed, their results not read here
10
10
  SETTINGS_OPS={'get':32,'set':33,'unset':34,'save':35,'state':36,'network':37}
@@ -9,8 +9,8 @@ from pathlib import Path
9
9
  from . import require,spec_identity
10
10
 
11
11
 
12
- REVIEWED={'docs/oep-core.ja.md':'699f7eb6333b6eb6678ddcf27074c03544f0f0a6ee0ef6ccdf02edd3757f962e',
13
- 'docs/oep-transports.ja.md':'2117de20712f7dfad6c81e247cf1b0b206ccf7528fcdf70702cbfce1c17dbadc'}
12
+ REVIEWED={'docs/oep-core.ja.md':'b90ae1b15f10442ff411929e80d8457e754d5f11f227167d3c142f54ff14768a',
13
+ 'docs/oep-transports.ja.md':'2e99bf8039a3d8f5b55c80b66ac97ac40ddf7ad4711079177ffc947c5790103f'}
14
14
  QUICK_MS=1000 # core_quick_op_max_ms
15
15
 
16
16
 
@@ -4,8 +4,7 @@ import struct
4
4
  from .. import registry
5
5
 
6
6
  # The request TLVs this example sends critical, by name (core §2.3: ones that must not be ignored; capture §3.3 / §3.8)
7
- _CAPTURE_CRITICAL = {'format', 'format_name', 'data_path', 'carriage', 'trigger_opts', 'threshold', 'clock',
8
- 'glitch_filter', 'coupling', 'offset', 'bandwidth', 'resolution', 'oversample'}
7
+ _CAPTURE_CRITICAL = {'format', 'carriage', 'trigger_opts', 'clock', 'glitch_filter'}
9
8
  _POLICY = {
10
9
  'scan': {'max_speed', 'idle_clock', 'targetsel'},
11
10
  'attach': {'reset', 'idle_clock', 'targetsel'},