oep-client-python 0.0.8__py3-none-any.whl → 0.0.10__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 CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.8"
5
+ __version__ = "0.0.10"
oep_client/capture.py CHANGED
@@ -1,5 +1,9 @@
1
- """oep.fixture.capture / oep.fixture.analog revision 1 (oep-spec docs/oep-if-capture.ja.md: §1 layouts, §2 segments,
2
- §3 operations). Numbers from `registry`.
1
+ """oep.fixture.capture / oep.fixture.analog / oep.fixture.capture-group revision 1 (oep-spec docs/oep-if-capture.ja.md:
2
+ §1 layouts, §2 segments, §3 operations, §3.8 calibration, §4 groups). Numbers from `registry`.
3
+
4
+ Times are the probe's one clock (ns since its boot, comparable within one boot_id): estimates with an uncertainty, the
5
+ probe's known corrections applied. Analog values are always raw; the probe's 1st-order scale, its calibration data and
6
+ its reference are for the host to choose from.
3
7
 
4
8
  configure: a TLV the probe cannot honour is refused (rejected unsupported, 0x0B, payload = the tag) when the host marked
5
9
  it critical, and otherwise ignored and listed in the answer's ignored TLV (0x7F, oep-core §2.3)."""
@@ -17,12 +21,17 @@ from .frames import FramingLost
17
21
  from .core import Interface, confirm
18
22
 
19
23
  _CAP = reg.FIXTURE_CAPTURE
24
+ _ANA = reg.FIXTURE_ANALOG
25
+ _GRP = reg.FIXTURE_CAPTURE_GROUP
20
26
  # configure TLVs; bit 7 of a tag = critical (the probe must reject what it cannot do)
21
27
  MODE, RATE, SAMPLES, SEGMENTS, TRIGGER, PRETRIGGER, FRONTEND = (
22
28
  _CAP.tlv["configure"][k] for k in ("mode", "rate", "samples", "segments", "trigger", "pretrigger", "frontend"))
23
- ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, TIMING, SCALE, BLOCKING = (
24
- _CAP.tlv["configure_answer"][k] for k in ("actual_rate", "layout", "actual_samples", "actual_segments", "timing",
25
- "scale", "blocking_ms"))
29
+ ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, TIMING, SCALE, BLOCKING, SKEW, FRONTEND_USED, REFERENCE, \
30
+ RATE_ACCURACY = (_ANA.tlv["configure_answer"][k] for k in (
31
+ "actual_rate", "layout", "actual_samples", "actual_segments", "timing", "scale", "blocking_ms", "skew",
32
+ "frontend_used", "reference", "rate_accuracy"))
33
+ FACTORY, VREFINT = _ANA.tlv["calibration_answer"]["factory"], _ANA.tlv["calibration_answer"]["vrefint"]
34
+ REFERENCE_SOURCE = {v: k for k, v in _ANA.enum["reference_source"].items()}
26
35
  IGNORED = m.TAG_IGNORED
27
36
  CRITICAL = m.TAG_CRITICAL
28
37
  ONE_SHOT, REPEAT, STREAMING = (_CAP.enum["mode"][k] for k in ("one_shot", "repeat", "streaming"))
@@ -37,7 +46,8 @@ def tlvs(payload: bytes) -> list[tuple[int, bytes]]:
37
46
  return m.split_tlvs(payload)
38
47
 
39
48
 
40
- SEGMENT_BYTES = 29 # serial u32, position u64, samples u32, start_us u64, trigger_index u32, flags u8
49
+ SEGMENT_BYTES = 33 # serial u32, position u64, samples u32, start_ns u64, start_uncertainty_ns u32, trigger_index u32,
50
+ # flags u8
41
51
 
42
52
 
43
53
  @dataclass
@@ -45,7 +55,8 @@ class Segment:
45
55
  serial: int
46
56
  position: int
47
57
  samples: int
48
- start_us: int
58
+ start_ns: int # the first sample's time on the probe's clock (an estimate)
59
+ start_uncertainty_ns: int # +- of start_ns (a guide, not a promise)
49
60
  trigger_index: int | None
50
61
  flags: int
51
62
 
@@ -56,8 +67,8 @@ class Segment:
56
67
 
57
68
  @classmethod
58
69
  def unpack(cls, b: bytes) -> "Segment":
59
- serial, position, samples, start_us, trig, flags = struct.unpack_from("<IQIQIB", b)
60
- return cls(serial, position, samples, start_us, None if trig == 0xFFFFFFFF else trig, flags)
70
+ serial, position, samples, start_ns, uncertainty, trig, flags = struct.unpack_from("<IQIQIIB", b)
71
+ return cls(serial, position, samples, start_ns, uncertainty, None if trig == 0xFFFFFFFF else trig, flags)
61
72
 
62
73
 
63
74
  @dataclass
@@ -74,9 +85,13 @@ class Config:
74
85
  segments: int = 0
75
86
  jitter_kind: int = 0
76
87
  jitter_ns: int = 0
77
- skew_ns: list[int] = field(default_factory=list)
78
- zero: int = 0
79
- scale_nv: int = 0
88
+ rate_measured: bool = False # rate_accuracy: the rate was measured (else computed from a divider)
89
+ rate_ppm: int = 0 # its uncertainty (0: unknown)
90
+ skew_ns: dict[int, int] = field(default_factory=dict) # analog, per channel (role)
91
+ zero: dict[int, int] = field(default_factory=dict) # analog, per channel
92
+ scale_nv: dict[int, int] = field(default_factory=dict) # analog, nV per value, per channel
93
+ frontend: dict[int, int] = field(default_factory=dict) # analog: the frontend each channel took
94
+ reference: tuple[str, int, bool] | None = None # analog: (source, mV, measured)
80
95
  blocking_ms: int = 0
81
96
  ignored: list[int] = field(default_factory=list)
82
97
 
@@ -140,9 +155,19 @@ def _config(payload: bytes, analog: bool) -> Config:
140
155
  c.segments = struct.unpack("<I", v)[0]
141
156
  elif tag == TIMING:
142
157
  c.jitter_kind, c.jitter_ns = v[0], struct.unpack_from("<I", v, 1)[0]
143
- c.skew_ns = [struct.unpack_from("<I", v, 5 + 4 * i)[0] for i in range((len(v) - 5) // 4)]
144
- elif tag == SCALE:
145
- c.zero, c.scale_nv = struct.unpack("<II", v)
158
+ elif tag == RATE_ACCURACY:
159
+ c.rate_measured, c.rate_ppm = v[0] == 1, struct.unpack_from("<I", v, 1)[0]
160
+ elif tag == SCALE and analog:
161
+ role, zero, scale = struct.unpack("<BII", v)
162
+ c.zero[role], c.scale_nv[role] = zero, scale
163
+ elif tag == SKEW and analog:
164
+ role, ns = struct.unpack("<BI", v)
165
+ c.skew_ns[role] = ns
166
+ elif tag == FRONTEND_USED and analog:
167
+ c.frontend[v[0]] = v[1]
168
+ elif tag == REFERENCE and analog:
169
+ source, mv, how = struct.unpack("<BIB", v)
170
+ c.reference = (REFERENCE_SOURCE.get(source, str(source)), mv, how == 1)
146
171
  elif tag == BLOCKING:
147
172
  c.blocking_ms = struct.unpack("<I", v)[0]
148
173
  elif tag == IGNORED:
@@ -165,7 +190,7 @@ class LogicCapture(Interface):
165
190
 
166
191
  def configure(self, *, rate: int, mode: int = ONE_SHOT, samples: int | None = None, segments: int | None = None,
167
192
  trigger: tuple[int, int, int] | None = None, pretrigger: int | None = None, query: bool = False,
168
- critical: set[int] = frozenset()) -> Config:
193
+ critical: set[int] = frozenset(), frontends: dict[int, int] | None = None) -> Config:
169
194
  """-> the probe's actual values (Config.ignored: tags the probe ignored). `critical`: tags the probe must honour
170
195
  or reject (host.Unsupported, .tag = the one it cannot)."""
171
196
  def tlv(tag: int, value: bytes) -> bytes:
@@ -179,6 +204,8 @@ class LogicCapture(Interface):
179
204
  body += tlv(TRIGGER, struct.pack("<BBH", *trigger))
180
205
  if pretrigger is not None:
181
206
  body += tlv(PRETRIGGER, struct.pack("<I", pretrigger))
207
+ for role, fe in sorted((frontends or {}).items()): # analog: the input range per channel (describe frontend)
208
+ body += tlv(FRONTEND, bytes([role, fe]))
182
209
  # query is its own operation: the lock is decided per operation, before the payload is looked at
183
210
  op = self.QUERY_OP if query else self.CONFIGURE
184
211
  c = _config(self._call(op, body, locked=not query).payload, self.ANALOG)
@@ -331,7 +358,8 @@ class LogicCapture(Interface):
331
358
  """The segment's bytes; every Host.on_capture callback gets them as a CaptureRecord (a run recorder, e.g.
332
359
  pytest-embedded-wireskein, without this package knowing it)."""
333
360
  c = self.config
334
- data = self.read(segment.position, (segment.samples * c.width + 7) // 8)
361
+ n = (segment.samples * c.width + 7) // 8 if c.width else segment.samples * len(c.order) * c.slot // 8
362
+ data = self.read(segment.position, n)
335
363
  if self.host.on_capture:
336
364
  record = CaptureRecord(self.fn, self.name, c, segment, data, self.armed_s, time.monotonic())
337
365
  for callback in list(self.host.on_capture):
@@ -365,3 +393,105 @@ class LogicCapture(Interface):
365
393
  z.writestr("version", "2")
366
394
  z.writestr("metadata", "\n".join(meta))
367
395
  z.writestr("logic-1-1", out)
396
+
397
+
398
+ @dataclass
399
+ class Calibration:
400
+ """What the probe knows for turning an analog value into a voltage (oep-if-capture §3.8), raw: the probe applies
401
+ none of it. factory: (frontend or None, scheme, raw bytes) - scheme names how to read raw; vrefint: (raw, ns), the
402
+ internal reference measured after the last start."""
403
+ factory: list[tuple[int | None, str, bytes]] = field(default_factory=list)
404
+ vrefint: tuple[int, int] | None = None
405
+
406
+
407
+ class AnalogCapture(LogicCapture):
408
+ """Basic analog capture (oep.fixture.analog): the same operations as the logic one; values are raw (§1.2)."""
409
+ NAME = "oep.fixture.analog"
410
+ ANALOG = True
411
+ CALIBRATION = _ANA.op["calibration"]
412
+
413
+ def values(self, data: bytes, k: int, samples: int | None = None) -> list[int]:
414
+ """Channel k's raw values (§1.2: slot s bits little endian, the value in bits o .. o+b-1, frames in `order`)."""
415
+ c = self.config
416
+ width = c.slot // 8
417
+ frame = width * len(c.order)
418
+ n = samples if samples is not None else len(data) // frame
419
+ m_ = c.order.index(k)
420
+ mask = (1 << c.bits) - 1
421
+ return [(int.from_bytes(data[i * frame + m_ * width:i * frame + (m_ + 1) * width], "little") >> c.offset) & mask
422
+ for i in range(n)]
423
+
424
+ def millivolts(self, k: int, value: int) -> float:
425
+ """The probe's own 1st-order reading of a raw value of channel k, (value - zero) x scale_nv (a nominal
426
+ reference: see reference and calibration() for others)."""
427
+ c = self.config
428
+ return (value - c.zero.get(k, 0)) * c.scale_nv.get(k, 0) / 1_000_000
429
+
430
+ def calibration(self) -> Calibration:
431
+ out = Calibration()
432
+ for tag, v in tlvs(self._call(self.CALIBRATION, locked=False).payload):
433
+ if tag == FACTORY:
434
+ n = v[1]
435
+ out.factory.append((None if v[0] == 0xFF else v[0], v[2:2 + n].decode("utf-8", "replace"), bytes(v[2 + n:])))
436
+ elif tag == VREFINT:
437
+ out.vrefint = struct.unpack("<IQ", v)
438
+ return out
439
+
440
+
441
+ @dataclass
442
+ class GroupStatus:
443
+ state: int
444
+ start_ns: int | None
445
+ trigger_ns: int | None
446
+ trigger_fn: int | None
447
+
448
+
449
+ class CaptureGroup(Interface):
450
+ """oep.fixture.capture-group (§4): tracks (LogicCapture / AnalogCapture, each configured as usual) started together,
451
+ one of them the trigger. Each track is read as usual; a track's offset is its first segment's start_ns minus the
452
+ group's start_ns, and every track's segment marks the trigger's instant (trigger_index)."""
453
+ NAME = "oep.fixture.capture-group"
454
+ REVISION = 1
455
+ BIND, START, STOP, FORCE, STATUS = (_GRP.op[k] for k in ("bind", "start", "stop", "force", "status"))
456
+ TAG_TRIGGER_TRACK = _GRP.tlv["bind"]["trigger_track"]
457
+ EVENT_TRIGGERED, EVENT_STOPPED = _GRP.event["triggered"], _GRP.event["stopped"]
458
+ NO_TIME = 0xFFFFFFFFFFFFFFFF
459
+
460
+ def bind(self, tracks: list[LogicCapture], trigger: LogicCapture | None = None) -> None:
461
+ """Bind these (configured) tracks; [] unbinds. `trigger`: the track whose configure trigger starts them all."""
462
+ body = struct.pack(f"<B{len(tracks)}H", len(tracks), *(t.fn for t in tracks))
463
+ if trigger is not None:
464
+ body += m.tlv(self.TAG_TRIGGER_TRACK, struct.pack("<H", trigger.fn), critical=True)
465
+ self._call(self.BIND, body)
466
+
467
+ def start(self, tracks: list[LogicCapture] = ()) -> tuple[int, int]:
468
+ """-> (blocking_ms, the group's start_ns). `tracks`: whose armed_s to set (for records)."""
469
+ blocking, start_ns = m.Reader(self._call(self.START).payload).take("IQ")
470
+ now = time.monotonic()
471
+ for t in tracks:
472
+ t.armed_s = now
473
+ return blocking, start_ns
474
+
475
+ def stop(self) -> None:
476
+ self._call(self.STOP)
477
+
478
+ def force(self) -> None:
479
+ self._call(self.FORCE)
480
+
481
+ def status(self) -> GroupStatus:
482
+ state, start, trig, fn = m.Reader(self._call(self.STATUS, locked=False).payload).take("BQQH")
483
+ none = self.NO_TIME
484
+ return GroupStatus(state, None if start == none else start, None if trig == none else trig, fn or None)
485
+
486
+ def wait(self, timeout: float = 5.0) -> GroupStatus:
487
+ """Poll until every track is done (one-shot)."""
488
+ deadline = time.monotonic() + timeout
489
+ while time.monotonic() < deadline:
490
+ st = self.status()
491
+ if st.state == STATE["done"]:
492
+ return st
493
+ if st.state == STATE["error"]:
494
+ raise h.Failed(None, "the group's capture stopped with an error")
495
+ time.sleep(0.002)
496
+ raise TimeoutError("the group's capture did not finish")
497
+
oep_client/endpoint.py CHANGED
@@ -39,7 +39,7 @@ from collections import OrderedDict
39
39
  from dataclasses import dataclass, field
40
40
  from typing import Callable
41
41
 
42
- from . import catalog, fake, message as m, registry as reg
42
+ from . import catalog, fake, fake_capture, message as m, registry as reg
43
43
 
44
44
  TOY_WRITE, TOY_READ = 0x01, 0x02
45
45
  OK, WAIT, LINE, FAULT, TIMEOUT, STATE = (reg.STATUS[k] for k in ("ok", "wait", "line", "fault", "timeout", "state"))
@@ -275,6 +275,11 @@ class Endpoint:
275
275
  first = self._role_pairs(fn)[0]
276
276
  self.targets[(fn, first)] = FakeTarget()
277
277
  self.target = next(iter(self.targets.values()), FakeTarget())
278
+ self.captures: dict[int, fake_capture.FakeCapture] = {
279
+ fn: self._capture_from(self.static[fn]) for fn, name in self.names.items()
280
+ if name in ("oep.fixture.capture", "oep.fixture.analog")}
281
+ self.groups: dict[int, fake_capture.FakeGroup] = {
282
+ fn: self._group_from(self.static[fn]) for fn, name in self.names.items() if name == "oep.fixture.capture-group"}
278
283
  self.mechanisms = set()
279
284
  for fn, name in self.names.items():
280
285
  if name == "oep.target.console":
@@ -302,6 +307,9 @@ class Endpoint:
302
307
  self.dropped = 0 # requests a v0 endpoint dropped (role 0x81)
303
308
  self.requests: list[m.Request] = []
304
309
  self.subscribed: set[int] = set()
310
+ self.push_seq: dict[int, int] = {} # fn -> the next event / data seq (core §11.2)
311
+ self.outbox: list[bytes] = [] # events and data frames waiting to go out (pushes())
312
+ self.capture_slipped = False # every capture segment says flags bit2 (a pace that fell behind)
305
313
  self.plan: set[tuple[int, int, int]] = set() # (fn, role, channel), from plan_apply and the config
306
314
  self.plan_from_config: set[int] = set() # fns whose plan came from the config (not a session's)
307
315
  self.resend: OrderedDict[int, tuple[int, int, int, bytes | None]] = OrderedDict()
@@ -340,6 +348,155 @@ class Endpoint:
340
348
  def target_id(self, value: int | None) -> None:
341
349
  self.target.target_id = value
342
350
 
351
+ @staticmethod
352
+ def _capture_from(tlvs: list[bytes]) -> fake_capture.FakeCapture:
353
+ """A capture as its describe declares it (modes, the w allowed, the rate range, max_read, the ring)."""
354
+ d = reg.FIXTURE_CAPTURE.tlv["describe"]
355
+ modes, widths, lo, hi, ring, most, fronts = set(), {8}, 1, 1_000_000, 1, 1024, {}
356
+ for t in tlvs:
357
+ tag, v = t[0], t[2:2 + t[1]]
358
+ if tag == d["frontend"]: # analog: frontend min_mv max_mv attenuation_mdb
359
+ fe, lo_mv, hi_mv, mdb = struct.unpack("<BiiI", v)
360
+ fronts[fe] = (lo_mv, hi_mv, mdb)
361
+ elif tag == d["mode"]:
362
+ modes.add(v[0])
363
+ elif tag == d["channels"]:
364
+ widths = {1 << i for i in range(6) if v[1] >> i & 1}
365
+ elif tag == catalog.MIN_CLOCK_HZ:
366
+ lo = struct.unpack("<I", v)[0]
367
+ elif tag == catalog.MAX_CLOCK_HZ:
368
+ hi = struct.unpack("<I", v)[0]
369
+ elif tag == d["segment_ring"]:
370
+ ring = struct.unpack("<H", v)[0]
371
+ elif tag == d["max_read"]:
372
+ most = struct.unpack("<I", v)[0]
373
+ return fake_capture.FakeCapture(modes or {fake_capture.MODE["one_shot"]}, widths, lo, hi, ring, most, fronts)
374
+
375
+ @staticmethod
376
+ def _group_from(tlvs: list[bytes]) -> fake_capture.FakeGroup:
377
+ d = reg.FIXTURE_CAPTURE_GROUP.tlv["describe"]
378
+ tracks, most, budgets = [], 1, []
379
+ for t in tlvs:
380
+ tag, v = t[0], t[2:2 + t[1]]
381
+ if tag == d["tracks"]:
382
+ tracks = list(struct.unpack(f"<{len(v) // 2}H", v))
383
+ elif tag == d["max_tracks"]:
384
+ most = v[0]
385
+ elif tag == d["budget"]:
386
+ budgets.append((struct.unpack_from("<I", v)[0], list(struct.unpack_from(f"<{(len(v) - 4) // 2}H", v, 4))))
387
+ return fake_capture.FakeGroup(tracks, most, budgets)
388
+
389
+ def _next_seq(self, fn: int) -> int:
390
+ seq = self.push_seq.get(fn, 0)
391
+ self.push_seq[fn] = (seq + 1) & 0xFFFF
392
+ return seq
393
+
394
+ def _events(self, fn: int, events: list[bytes]) -> None:
395
+ """Events (kind(u8) payload) of `fn` to go out while it is subscribed; unsubscribed, they are not sent."""
396
+ for e in events:
397
+ if fn in self.subscribed:
398
+ self.outbox.append(bytes([m.ROLE_EVENT]) + struct.pack("<HH", fn, self._next_seq(fn)) + e)
399
+
400
+ def pushes(self) -> list[bytes]:
401
+ """The frames the probe sends by itself now (core §11): events, and a streaming capture's data. A serving loop
402
+ frames and sends them; a test takes them from here."""
403
+ self.tick()
404
+ for fn, cap in self.captures.items():
405
+ if fn in self.subscribed:
406
+ self.outbox += cap.pushes(fn, lambda fn=fn: self._next_seq(fn), self.probe.max_frame)
407
+ out, self.outbox = self.outbox, []
408
+ return out
409
+
410
+ def _capture(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
411
+ cap, O = self.captures[fn], fake_capture.OP
412
+ self._events(fn, cap.tick(self.now())) # what the clock captured up to this request
413
+ roles = sorted(a[1] for a in self.plan if a[0] == fn)
414
+ budget = self.probe.max_frame - m.RESULT_HEADER
415
+ try:
416
+ if op in (O["configure"], O["query"]):
417
+ rest = t.data[t.at:]
418
+ analog = self.names[fn] == "oep.fixture.analog"
419
+ known = set(fake_capture.TLV.values()) - (set() if analog else {fake_capture.TLV["frontend"]})
420
+ got, ignored = t.tail(known)
421
+ # the frontend TLV comes once per channel: read them all
422
+ fronts = [tuple(v[:2]) for tag, v in m.split_tlvs(rest) if tag & 0x7F == fake_capture.TLV["frontend"]] \
423
+ if analog else []
424
+ settled = cap.settle(got, t.critical, len(roles), fronts)
425
+ if op == O["configure"]:
426
+ cap.apply(settled)
427
+ cap.slipped = self.capture_slipped
428
+ return self._answer(cap.answer(settled), ignored)
429
+ if op in (O["start"], O["stop"], O["force"]) and cap.group is not None:
430
+ raise Reject(m.UNAVAILABLE) # bound: the group starts and stops it
431
+ if op == O["start"]:
432
+ t.tail()
433
+ self._events(fn, cap.start(self.now()))
434
+ return m.COMPLETED, m.SUCCESS, struct.pack("<I", 0)
435
+ if op == O["stop"]:
436
+ t.tail()
437
+ self._events(fn, cap.stop())
438
+ return m.COMPLETED, m.SUCCESS, b""
439
+ if op == O["force"]:
440
+ t.tail()
441
+ return m.COMPLETED, m.SUCCESS, b"" # nothing waits: a trigger is found at start
442
+ if op == O["status"]:
443
+ t.tail()
444
+ return m.COMPLETED, m.SUCCESS, cap.status()
445
+ if op == O["read"]:
446
+ position, most = t.take("QI")
447
+ return m.COMPLETED, m.SUCCESS, cap.read(position, most, budget)
448
+ if op == O["segments"]:
449
+ first = t.take("I")
450
+ t.tail()
451
+ return m.COMPLETED, m.SUCCESS, cap.segment_list(first, budget)
452
+ if op == O["release"]:
453
+ serial = t.take("I")
454
+ t.tail()
455
+ cap.release(serial, self.now())
456
+ return m.COMPLETED, m.SUCCESS, b""
457
+ if op == fake_capture.ANA.op["calibration"] and cap.analog:
458
+ t.tail()
459
+ return m.COMPLETED, m.SUCCESS, cap.calibration()
460
+ except fake_capture.Reject as e:
461
+ raise Reject(e.reason, e.payload)
462
+ return m.REJECTED, m.UNKNOWN_OPERATION, b""
463
+
464
+ def _group(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
465
+ grp, O = self.groups[fn], fake_capture.GRP.op
466
+ try:
467
+ if op == O["bind"]:
468
+ n = t.take("B")
469
+ fns = [t.take("H") for _ in range(n)]
470
+ got, ignored = t.tail({fake_capture.GRP.tlv["bind"]["trigger_track"]})
471
+ src = got.get(fake_capture.GRP.tlv["bind"]["trigger_track"])
472
+ grp.bind(self.captures, fns, struct.unpack("<H", src)[0] if src else 0)
473
+ return self._answer(b"", ignored)
474
+ if op == O["start"]:
475
+ t.tail()
476
+ per, own = grp.start(self.captures, self.now())
477
+ for track, events in per:
478
+ self._events(track, events)
479
+ self._events(fn, own)
480
+ return m.COMPLETED, m.SUCCESS, struct.pack("<IQ", 0, grp.start_ns)
481
+ if op == O["stop"]:
482
+ t.tail()
483
+ per, own = grp.stop(self.captures)
484
+ for track, events in per:
485
+ self._events(track, events)
486
+ self._events(fn, own)
487
+ return m.COMPLETED, m.SUCCESS, b""
488
+ if op == O["force"]:
489
+ t.tail()
490
+ return m.COMPLETED, m.SUCCESS, b""
491
+ if op == O["status"]:
492
+ t.tail()
493
+ for track in grp.tracks:
494
+ self._events(track, self.captures[track].tick(self.now()))
495
+ return m.COMPLETED, m.SUCCESS, grp.status(self.captures)
496
+ except fake_capture.Reject as e:
497
+ raise Reject(e.reason, e.payload)
498
+ return m.REJECTED, m.UNKNOWN_OPERATION, b""
499
+
343
500
  def _role_pairs(self, fn: int) -> list[tuple[int, int]]:
344
501
  """A role_channels wire's pairs in the count = 0 order (oep-if-debug §1): swdio ascending, then swclk."""
345
502
  roles = self.pin_roles[fn]
@@ -361,7 +518,7 @@ class Endpoint:
361
518
  def _held(self, fn: int | None = None, pair: tuple[int, int] | None = None) -> set[int]:
362
519
  """Channels something holds (core §8.1): the plan, the slots' pairs, the live connections' pairs - except
363
520
  wire `fn`'s own connection on `pair` (attaching there again, or scanning through it)."""
364
- held = {a[2] for a in self.plan}
521
+ held = {a[2] for a in self.plan if a[0] not in self.captures} # a capture only listens
365
522
  held |= {p for s in self.slots.values() for p in s.pair if p != 0xFFFF and not (s.wire_fn == fn and s.pair == pair)}
366
523
  held |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF and not (c.fn == fn and c.pair == pair)}
367
524
  return held
@@ -613,6 +770,7 @@ class Endpoint:
613
770
  if fn != 0 and fn not in self.names:
614
771
  return m.REJECTED, m.UNAVAILABLE, b""
615
772
  self.subscribed.add(fn)
773
+ self.push_seq[fn] = 0 # seq from 0 at every subscribe (core §11.2)
616
774
  return m.COMPLETED, m.SUCCESS, b""
617
775
  if op == m.OP_UNSUBSCRIBE:
618
776
  fn = t.take("H")
@@ -644,14 +802,24 @@ class Endpoint:
644
802
  def _check_plan(self, got: list[tuple[int, int, int]]) -> None:
645
803
  """plan_apply's all-or-nothing check: the roles each fn has, and no pin another fn (or a slot) holds."""
646
804
  named = {fn for fn, _, _ in got}
647
- kept = {a for a in self.plan if a[0] not in named}
805
+ # a capture only listens: it shares pins with anything, and nothing is kept from a pin by it (core §8.1)
806
+ kept = {a for a in self.plan if a[0] not in named and a[0] not in self.captures}
648
807
  slot_pins = {p for s in self.slots.values() for p in s.pair if p != 0xFFFF}
649
808
  slot_pins |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF} # a live connection's pins too
650
809
  for fn, role, ch in got:
810
+ if fn in self.captures:
811
+ if role not in self._declared_roles(fn, ch):
812
+ raise Reject(m.UNAVAILABLE)
813
+ continue
651
814
  roles = {"oep.fixture.gpio": {1}, "oep.fixture.uart": {1, 2}}.get(self.names.get(fn, ""), set())
652
815
  if role not in roles or any(k[2] == ch for k in kept) or ch in slot_pins:
653
816
  raise Reject(m.UNAVAILABLE)
654
817
 
818
+ def _declared_roles(self, fn: int, channel: int) -> set[int]:
819
+ """The roles fn's describe offers on `channel` (role_channels)."""
820
+ return {t[2] for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS
821
+ and channel in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
822
+
655
823
  def _drop_plan(self, fn: int) -> None:
656
824
  for a in [a for a in self.plan if a[0] == fn]:
657
825
  self.gpio_modes.pop(a[2], None)
@@ -787,8 +955,11 @@ class Endpoint:
787
955
  if not self._allows(fn, pair) or set(pair) & self._held(fn, pair):
788
956
  raise Reject(m.UNAVAILABLE) # not allowed here, or its pins are held (§8.1)
789
957
  return pair
958
+ live = [c.pair for c in self.conns.values() if c.fn == fn]
959
+ if len(live) == 1:
960
+ return live[0] # no pins: the wire's one live connection
790
961
  allowed = self._allowed_pairs(fn)
791
- if len(allowed) != 1:
962
+ if live or len(allowed) != 1:
792
963
  raise Reject(m.UNAVAILABLE) # the host chooses among several
793
964
  return allowed[0]
794
965
 
@@ -1399,9 +1570,11 @@ class Endpoint:
1399
1570
  self.flows[(port, key)] = Flow(sid, 0)
1400
1571
 
1401
1572
  def tick(self) -> None:
1402
- """Time passes: the lease, at-boot retries, mixed lines closed by quiet."""
1573
+ """Time passes: the lease, at-boot retries, mixed lines closed by quiet, the captures."""
1403
1574
  self._lapse()
1404
1575
  now = self.now()
1576
+ for fn, cap in self.captures.items():
1577
+ self._events(fn, cap.tick(now))
1405
1578
  for n, s in self.slots.items():
1406
1579
  rt = self.slot_rt[n]
1407
1580
  if (s.attach == SLOT_ATTACH["at_boot"] and s.retry_s and not rt.evicted
@@ -1549,4 +1722,5 @@ class Endpoint:
1549
1722
 
1550
1723
  SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
1551
1724
  "oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
1552
- "oep.probe.config": "config_op"}
1725
+ "oep.probe.config": "config_op", "oep.fixture.capture": "capture", "oep.fixture.analog": "capture",
1726
+ "oep.fixture.capture-group": "group"}
oep_client/fake.py CHANGED
@@ -108,6 +108,42 @@ TRANSPORT = reg.CORE.enum["transport_kind"]
108
108
  SERIAL_KINDS = {TRANSPORT["uart_bridge"], TRANSPORT["usb_cdc"], TRANSPORT["usb_serial_jtag"]}
109
109
  NS = "io.github.ch32-riscv-ug"
110
110
  MECHANISMS = reg.TARGET_CONSOLE.tlv["describe"]["mechanisms"]
111
+ _CAPD = reg.FIXTURE_CAPTURE.tlv["describe"]
112
+ _ANAD = reg.FIXTURE_ANALOG.tlv["describe"]
113
+ _GRPD = reg.FIXTURE_CAPTURE_GROUP.tlv["describe"]
114
+ CORE_CHIP = reg.CORE.tlv["describe"]["chip"]
115
+
116
+
117
+ def _analog_decl(frontends: list[tuple[int, int, int, int]], max_samples: int) -> tuple[bytes, ...]:
118
+ """oep.fixture.analog's declarations: one-shot / repeat / streaming, 4 channels in 16-bit slots, the frontends
119
+ (number, min_mv, max_mv, attenuation_mdb), triggers cross up / down."""
120
+ out = tuple(catalog.tlv(_ANAD["mode"], struct.pack("<BBII", _CAPM[k], 1, max_samples, 8 if k == "repeat" else 1))
121
+ for k in ("one_shot", "repeat", "streaming"))
122
+ out += tuple(catalog.tlv(_ANAD["frontend"], struct.pack("<BiiI", *f)) for f in frontends)
123
+ return out + (catalog.tlv(_ANAD["channels"], bytes([4, 0b100])), # s 16
124
+ catalog.tlv(_ANAD["trigger"], struct.pack("<BI", 0b11001, max_samples - 1)), # immediate, cross up / down
125
+ catalog.u32(_ANAD["max_read"], 4096), catalog.u16(_ANAD["segment_ring"], 8),
126
+ catalog.u32(FEATURES, 0b001))
127
+
128
+
129
+ def _group_decl(tracks: list[int], budgets: list[tuple[int, list[int]]], skews: dict[int, int]) -> tuple[bytes, ...]:
130
+ return ((catalog.tlv(_GRPD["tracks"], struct.pack(f"<{len(tracks)}H", *tracks)), catalog.u8(_GRPD["max_tracks"], len(tracks)))
131
+ + tuple(catalog.tlv(_GRPD["budget"], struct.pack(f"<I{len(f)}H", most, *f)) for most, f in budgets)
132
+ + tuple(catalog.tlv(_GRPD["start_skew"], struct.pack("<HI", fn, ns)) for fn, ns in skews.items()))
133
+ _CAPM = reg.FIXTURE_CAPTURE.enum["mode"]
134
+
135
+
136
+ def _capture_decl(modes: list[str], max_channels: int, widths: list[int], max_samples: int, ring: int,
137
+ max_read: int) -> tuple[bytes, ...]:
138
+ """oep.fixture.capture's declarations (oep-if-capture §3.5): a mode entry per mode, channels (max, the w allowed as
139
+ bits: bit i = w 2^i), the trigger types (immediate, level, edge) with the most pretrigger, max_read, segment_ring."""
140
+ out = tuple(catalog.tlv(_CAPD["mode"], struct.pack("<BBII", _CAPM[k], 1, max_samples, ring if k == "repeat" else 1))
141
+ for k in modes)
142
+ bits = sum(1 << (w.bit_length() - 1) for w in widths)
143
+ return out + (catalog.tlv(_CAPD["channels"], bytes([max_channels, bits])),
144
+ catalog.tlv(_CAPD["trigger"], struct.pack("<BI", 0b111, max_samples - 1)),
145
+ catalog.u32(_CAPD["max_read"], max_read), catalog.u16(_CAPD["segment_ring"], ring),
146
+ catalog.u32(FEATURES, 0b001))
111
147
  MAX_CONNECTIONS = reg.WIRE_RVSWD.tlv["describe"]["max_connections"]
112
148
  _CFG = reg.PROBE_CONFIG.tlv["describe"]
113
149
 
@@ -153,7 +189,8 @@ def p4_x035() -> FakeProbe:
153
189
  _core("3.0.0", "esp32-p4-devkit", bytes.fromhex("30eda0e31108"), 55, reserved, f"{NS}.p4-x035",
154
190
  {2: "SWDIO", 54: "SWCLK", 51: "LED"},
155
191
  _transports([(TRANSPORT["usb_serial_jtag"], 0xFF), (TRANSPORT["vendor_bulk"], 0),
156
- (TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)]) + (catalog.u8(CORE_OEP_PID, 1),)),
192
+ (TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)])
193
+ + (catalog.u8(CORE_OEP_PID, 1), catalog.text(CORE_CHIP, "esp32p4 v1.0"))),
157
194
  Offered(1, 1, "oep.wire.rvswd", (
158
195
  catalog.channel_group(1, [(1, 2), (2, 54)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1),
159
196
  catalog.u8(MAX_CONNECTIONS, 1))),
@@ -165,15 +202,23 @@ def p4_x035() -> FakeProbe:
165
202
  catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
166
203
  Offered(6, 4, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
167
204
  catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
168
- Offered(7, 5, "oep.fixture.capture", _roles({k: pins for k in range(8)}) + (
205
+ # the P4's PARLIO: w 1-16, one-shot / repeat / streaming
206
+ Offered(7, 5, "oep.fixture.capture", _roles({k: pins for k in range(16)}) + (
169
207
  catalog.u32(MAX_CLOCK_HZ, 20_000_000), catalog.u32(MIN_CLOCK_HZ, 1_000),
170
- catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))),
208
+ catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))
209
+ + _capture_decl(["one_shot", "repeat", "streaming"], 16, [1, 2, 4, 8, 16], 1 << 20, 8, 4096)),
171
210
  Offered(8, 6, f"{NS}.esp32.i2c-target", _roles({1: pins, 2: pins}) + (
172
211
  catalog.u16(MAX_LENGTH, 128), catalog.u32(MAX_CLOCK_HZ, 1_000_000),
173
212
  catalog.u32(FEATURES, 0b11), catalog.u8(IMPLEMENTATION, 2))),
174
213
  Offered(9, 7, f"{NS}.esp32.spi-target", _roles({1: pins, 2: pins, 3: pins, 4: pins}) + (
175
214
  catalog.u16(MAX_LENGTH, 64), catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
176
215
  _config(10, 8, slots_max=1),
216
+ # the P4's ADC1 (GPIO16-23): one ADC for all its channels, 611 Hz - 83.3 kHz in all; ESP32-style attenuations
217
+ Offered(11, 1, "oep.fixture.analog", _roles({k: list(range(16, 24)) for k in range(4)}) + (
218
+ catalog.u32(MAX_CLOCK_HZ, 83_333), catalog.u32(MIN_CLOCK_HZ, 611), catalog.u8(IMPLEMENTATION, 3))
219
+ + _analog_decl([(0, 0, 950, 0), (1, 0, 1250, 2500), (2, 0, 1750, 6000), (3, 0, 3100, 12000)], 65536)),
220
+ # the logic (fn 7) and the analog (fn 11) together; the ADC's 83.3 kHz is shared by its channels
221
+ Offered(12, 1, "oep.fixture.capture-group", _group_decl([7, 11], [(83_333, [11])], {11: 5000})),
177
222
  ])
178
223
 
179
224
 
@@ -194,8 +239,10 @@ def esp32_v003() -> FakeProbe:
194
239
  Offered(4, 2, "oep.fixture.gpio", _roles({1: wired + [23]})),
195
240
  Offered(5, 3, "oep.fixture.uart", _roles({1: wired, 2: wired}) + (
196
241
  catalog.u32(MAX_CLOCK_HZ, 115_200), catalog.u8(IMPLEMENTATION, 2))),
242
+ # the classic ESP32's GPIO sampler: a byte a sample (w 8), one-shot
197
243
  Offered(6, 4, "oep.fixture.capture", _roles({k: wired for k in range(4)}) + (
198
- catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))),
244
+ catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))
245
+ + _capture_decl(["one_shot"], 8, [8], 65536, 1, 480)),
199
246
  Offered(7, 5, f"{NS}.esp32.i2c-target", _roles({1: wired, 2: wired}) + (
200
247
  catalog.u16(MAX_LENGTH, 16), catalog.u32(MAX_CLOCK_HZ, 100_000), catalog.u8(IMPLEMENTATION, 2))),
201
248
  # Two fixed pin sets (an example of channel_group; GPIO23 is the DUT's NRST on this jig).
@@ -0,0 +1,437 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """oep.fixture.capture / oep.fixture.analog / oep.fixture.capture-group in the fake probe (oep-spec
3
+ docs/oep-if-capture.ja.md, revision 1).
4
+
5
+ What it captures is known in advance, so a receiver can check it (sample i counted from the track's start, across
6
+ segments):
7
+
8
+ - logic: sample i is the counter i, channel k bit k of it - a square wave of period 2^(k+1) samples. The layout is the
9
+ probe's (§1.1): w is the smallest width the describe allows (channels tag 0x44) that holds the channels, pos[k] = k.
10
+ - analog: 12-bit values in 16-bit slots (s 16, o 0, b 12), channels in role order. Channel k's period is
11
+ P = 64 (k // 2 + 1) samples: an even k is a square wave (4095 for the first half of the period, then 0), an odd k a sine
12
+ round(2047.5 + 2047 sin(2 pi i / P)). zero 0, scale_nv = the frontend's range / 4095.
13
+
14
+ Time (the probe's one clock, ns): a track's first sample is at its start (the group's start, or the endpoint's clock
15
+ at start) plus the track's start offset - logic 0 (+-50 ns), analog 5 us (+-2 us), as a probe that corrected what it
16
+ knows and says how sure it is. Later segments follow at the actual rate.
17
+
18
+ Modes: one-shot (the segment is there as soon as start answers), repeat (segments come with the clock at the actual
19
+ rate, up to the ring; a full ring stops the capture, release frees it) and streaming (the bytes come with the clock and
20
+ go out as data pushes while subscribed). Triggers: logic level / edge, analog cross up / down, with a pretrigger.
21
+ `slipped` sets flags bit2 on every segment (a probe whose software pace fell behind).
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import math
27
+ import struct
28
+ from dataclasses import dataclass, field
29
+ from fractions import Fraction
30
+
31
+ from . import message as m, registry as reg
32
+
33
+ CAP, ANA, GRP = reg.FIXTURE_CAPTURE, reg.FIXTURE_ANALOG, reg.FIXTURE_CAPTURE_GROUP
34
+ OP = CAP.op
35
+ TLV, ANSWER = CAP.tlv["configure"], ANA.tlv["configure_answer"]
36
+ MODE, STATE, TRIGGER = CAP.enum["mode"], CAP.enum["state"], CAP.enum["trigger"]
37
+ STOPPED, FLAG = CAP.enum["stopped_reason"], CAP.enum["segment_flag"]
38
+ EVENT = CAP.event
39
+ CALIBRATION = ANA.tlv["calibration_answer"]
40
+ NONE = 0xFFFFFFFF
41
+ NO_TIME = 0xFFFFFFFFFFFFFFFF
42
+ MAX_SAMPLES = 1 << 20 # a fake keeps its captures in memory
43
+ SEGMENT = struct.Struct("<IQIQIIB") # serial position samples start_ns start_uncertainty_ns trigger_index flags (§2)
44
+ FULL = 4095 # the analog value's top (12 bits)
45
+
46
+
47
+ class Reject(Exception):
48
+ def __init__(self, reason: int, payload: bytes = b""):
49
+ self.reason, self.payload = reason, payload
50
+
51
+
52
+ @dataclass
53
+ class Segment:
54
+ serial: int
55
+ position: int
56
+ samples: int
57
+ start_ns: int
58
+ start_uncertainty_ns: int
59
+ trigger_index: int = NONE
60
+ flags: int = 0
61
+
62
+ def pack(self) -> bytes:
63
+ return SEGMENT.pack(self.serial, self.position, self.samples, self.start_ns, self.start_uncertainty_ns,
64
+ self.trigger_index, self.flags)
65
+
66
+
67
+ def analog_value(k: int, i: int) -> int:
68
+ """Channel k's value at sample i of the fake analog waveform."""
69
+ period = 64 * (k // 2 + 1)
70
+ if k % 2 == 0:
71
+ return FULL if i % period < period // 2 else 0
72
+ return round(2047.5 + 2047 * math.sin(2 * math.pi * i / period))
73
+
74
+
75
+ @dataclass
76
+ class FakeCapture:
77
+ """One capture track (logic, or analog when `frontends` is given). Times: the endpoint's clock in ms, the probe's in
78
+ ns."""
79
+ modes: set[int] # the modes the describe declares
80
+ widths: set[int] # logic: the w the describe allows
81
+ min_hz: int
82
+ max_hz: int
83
+ ring: int = 8 # segment_ring
84
+ max_read: int = 4096
85
+ frontends: dict[int, tuple[int, int, int]] = field(default_factory=dict) # analog: n -> (min_mv, max_mv, mdb)
86
+ slipped: bool = False
87
+ state: int = STATE["unconfigured"]
88
+ mode: int = MODE["one_shot"]
89
+ rate: Fraction = Fraction(0)
90
+ samples: int = 0
91
+ segments_max: int = 0
92
+ trigger: tuple[int, int, int] | None = None
93
+ pretrigger: int = 0
94
+ width: int = 0 # logic: w; analog: the bytes of one sample (C x 2)
95
+ channels: int = 0
96
+ frontend_of: dict[int, int] = field(default_factory=dict) # analog: channel -> frontend
97
+ data: bytearray = field(default_factory=bytearray) # the stream from `base` on
98
+ base: int = 0 # the byte position data[0] is at
99
+ segs: list[Segment] = field(default_factory=list)
100
+ serial_done: int = 0
101
+ started_ms: int = 0
102
+ t0_ns: int = 0 # the probe-clock time of the track's first sample
103
+ produced: int = 0 # samples produced since start
104
+ sent: int = 0 # streaming: the byte position pushed so far
105
+ gap_next: bool = False # repeat: the capture stopped for want of a segment; the next one says so
106
+ group: object | None = None # the FakeGroup that binds it
107
+ vrefint_ns: int = NO_TIME
108
+
109
+ @property
110
+ def analog(self) -> bool:
111
+ return bool(self.frontends)
112
+
113
+ @property
114
+ def start_offset_ns(self) -> int:
115
+ return 5000 if self.analog else 0
116
+
117
+ @property
118
+ def uncertainty_ns(self) -> int:
119
+ return 2000 if self.analog else 50
120
+
121
+ # ---- configure ------------------------------------------------------------------------------------------------
122
+ def settle(self, got: dict[int, bytes], critical: set[int], channels: int, frontends: list[tuple[int, int]]) -> dict:
123
+ """The actual values for a configure / query request (nothing changed). `frontends`: (role, frontend) asked."""
124
+ if self.group is not None:
125
+ raise Reject(m.UNAVAILABLE) # bound: configure again after unbinding
126
+ mode = got[TLV["mode"]][0] if TLV["mode"] in got else MODE["one_shot"]
127
+ if mode not in self.modes:
128
+ raise Reject(m.UNSUPPORTED, bytes([TLV["mode"] | (m.TAG_CRITICAL if TLV["mode"] in critical else 0)]))
129
+ if TLV["rate"] not in got or len(got[TLV["rate"]]) != 4:
130
+ raise Reject(m.MALFORMED)
131
+ asked = struct.unpack("<I", got[TLV["rate"]])[0]
132
+ if not asked or not channels:
133
+ raise Reject(m.MALFORMED if not asked else m.UNAVAILABLE)
134
+ top = self.max_hz // channels if self.analog else self.max_hz # an ADC's rate is shared by its channels
135
+ div = max(1, -(-self.max_hz // min(max(asked, self.min_hz), top)))
136
+ rate = Fraction(self.max_hz, div)
137
+ samples = struct.unpack("<I", got[TLV["samples"]])[0] if TLV["samples"] in got else 4096
138
+ samples = max(1, min(samples, MAX_SAMPLES))
139
+ segs = struct.unpack("<I", got[TLV["segments"]])[0] if TLV["segments"] in got else self.ring
140
+ segs = max(1, min(segs, self.ring)) if mode == MODE["repeat"] else 1
141
+ trigger = None
142
+ if TLV["trigger"] in got:
143
+ kind, role, value = struct.unpack("<BBH", got[TLV["trigger"]])
144
+ allowed = (TRIGGER["cross_up"], TRIGGER["cross_down"]) if self.analog else (TRIGGER["level"], TRIGGER["edge"])
145
+ if (kind and kind not in allowed) or (kind and role >= channels):
146
+ raise Reject(m.UNSUPPORTED, bytes([TLV["trigger"] | m.TAG_CRITICAL]))
147
+ trigger = (kind, role, value) if kind else None
148
+ pre = struct.unpack("<I", got[TLV["pretrigger"]])[0] if TLV["pretrigger"] in got else 0
149
+ chosen = {}
150
+ if self.analog:
151
+ chosen = {k: max(self.frontends) for k in range(channels)} # the widest range unless asked
152
+ for role, fe in frontends:
153
+ if role >= channels or fe not in self.frontends:
154
+ raise Reject(m.UNAVAILABLE)
155
+ chosen[role] = fe
156
+ width = 2 * channels
157
+ else:
158
+ if frontends:
159
+ raise Reject(m.UNSUPPORTED, bytes([TLV["frontend"] | m.TAG_CRITICAL]))
160
+ width = min((w for w in self.widths if w >= channels), default=None)
161
+ if width is None:
162
+ raise Reject(m.UNAVAILABLE) # more than a sample holds
163
+ if mode != MODE["one_shot"] and width < 8:
164
+ samples = -(-samples // (8 // width)) * (8 // width) # segments end on a byte
165
+ return {"mode": mode, "rate": rate, "samples": samples, "segments": segs, "trigger": trigger,
166
+ "pretrigger": min(pre, samples - 1), "width": width, "channels": channels, "frontends": chosen}
167
+
168
+ def apply(self, s: dict) -> None:
169
+ self.mode, self.rate, self.samples, self.segments_max = s["mode"], s["rate"], s["samples"], s["segments"]
170
+ self.trigger, self.pretrigger, self.width, self.channels = s["trigger"], s["pretrigger"], s["width"], s["channels"]
171
+ self.frontend_of = s["frontends"]
172
+ self.state = STATE["configured"]
173
+ self._clear()
174
+
175
+ def answer(self, s: dict) -> bytes:
176
+ def tlv(tag: int, v: bytes) -> bytes:
177
+ return bytes([tag, len(v)]) + v
178
+ c = s["channels"]
179
+ out = tlv(ANSWER["actual_rate"], struct.pack("<II", s["rate"].numerator, s["rate"].denominator))
180
+ if self.analog:
181
+ out += tlv(ANSWER["layout"], bytes([16, 0, 12, c]) + bytes(range(c)))
182
+ else:
183
+ out += tlv(ANSWER["layout"], bytes([s["width"], c]) + bytes(range(c)))
184
+ out += (tlv(ANSWER["actual_samples"], struct.pack("<I", s["samples"]))
185
+ + tlv(ANSWER["actual_segments"], struct.pack("<I", s["segments"]))
186
+ + tlv(ANSWER["timing"], struct.pack("<BI", 1 if s["rate"].denominator != 1 else 0, 0))
187
+ + tlv(ANSWER["blocking_ms"], struct.pack("<I", 0))
188
+ # the logic's rate is the divider's; the ADC's is off by some per mille, as the P4's (+0.15 %)
189
+ + tlv(ANSWER["rate_accuracy"], struct.pack("<BI", 1, 1500) if self.analog else struct.pack("<BI", 0, 0)))
190
+ if self.analog:
191
+ step_ns = int(1e9 / (s["rate"] * c)) # one ADC, the channels in turn
192
+ for k in range(c):
193
+ lo, hi, _ = self.frontends[s["frontends"][k]]
194
+ out += tlv(ANSWER["scale"], struct.pack("<BII", k, 0, (hi - lo) * 1_000_000 // FULL))
195
+ out += tlv(ANSWER["skew"], struct.pack("<BI", k, k * step_ns))
196
+ out += tlv(ANSWER["frontend_used"], bytes([k, s["frontends"][k]]))
197
+ out += tlv(ANSWER["reference"], struct.pack("<BIB", ANA.enum["reference_source"]["internal"], 1100, 0))
198
+ return out
199
+
200
+ def calibration(self) -> bytes:
201
+ """The analog's calibration (§3.8): a made-up two-point value per frontend, and the reference measured at start."""
202
+ if not self.analog:
203
+ raise Reject(m.UNKNOWN_OPERATION)
204
+ scheme = b"org.example.fake.two-point"
205
+ out = b""
206
+ for fe in sorted(self.frontends):
207
+ v = bytes([fe, len(scheme)]) + scheme + struct.pack("<HH", 150, 3950) # raw at the range's 5 % / 95 %
208
+ out += bytes([CALIBRATION["factory"], len(v)]) + v
209
+ if self.vrefint_ns != NO_TIME:
210
+ v = struct.pack("<IQ", 1365, self.vrefint_ns) # 1100 mV against the 3300 mV full scale
211
+ out += bytes([CALIBRATION["vrefint"], len(v)]) + v
212
+ return out
213
+
214
+ # ---- the data ---------------------------------------------------------------------------------------------------
215
+ def _clear(self) -> None:
216
+ self.data, self.base, self.segs, self.serial_done = bytearray(), 0, [], 0
217
+ self.produced, self.sent, self.gap_next = 0, 0, False
218
+
219
+ def _pack(self, first: int, n: int) -> bytes:
220
+ if self.analog:
221
+ return b"".join(struct.pack(f"<{self.channels}H", *(analog_value(k, first + i) for k in range(self.channels)))
222
+ for i in range(n))
223
+ w = self.width # logic: the counter, w bits a sample
224
+ mask = (1 << w) - 1
225
+ if w >= 8:
226
+ return b"".join(((first + i) & mask).to_bytes(w // 8, "little") for i in range(n))
227
+ out = bytearray((n * w + 7) // 8)
228
+ for i in range(n):
229
+ bit = i * w
230
+ out[bit >> 3] |= ((first + i) & mask) << (bit & 7)
231
+ return bytes(out)
232
+
233
+ def value(self, k: int, i: int) -> int:
234
+ return analog_value(k, i) if self.analog else (i >> k) & 1
235
+
236
+ def trigger_at(self) -> int:
237
+ """The first sample, at or after the pretrigger, where the trigger holds (within a few periods)."""
238
+ kind, role, value = self.trigger
239
+ span = (1 << (role + 1)) if not self.analog else 64 * (role // 2 + 1)
240
+ for i in range(max(self.pretrigger, 1), self.pretrigger + 3 * span + 2):
241
+ now, before = self.value(role, i), self.value(role, i - 1)
242
+ if kind == TRIGGER["level"] and now == (value & 1):
243
+ return i
244
+ if kind == TRIGGER["edge"] and now != before and (value == 2 or (value == 0) == (now == 1)):
245
+ return i
246
+ if kind == TRIGGER["cross_up"] and before < value <= now:
247
+ return i
248
+ if kind == TRIGGER["cross_down"] and before > value >= now:
249
+ return i
250
+ return self.pretrigger
251
+
252
+ def time_of(self, i: int) -> int:
253
+ """The probe-clock time of the track's sample i."""
254
+ return self.t0_ns + int(i * 1_000_000_000 / self.rate)
255
+
256
+ def index_at(self, t_ns: int) -> int:
257
+ """The track's sample nearest the probe-clock time t (NONE when outside the one-shot segment)."""
258
+ i = round((t_ns - self.t0_ns) * self.rate / 1_000_000_000)
259
+ return i if 0 <= i < self.samples else NONE
260
+
261
+ def _segment(self, n: int, trigger_index: int = NONE, flags: int = 0) -> Segment:
262
+ if self.gap_next:
263
+ flags, self.gap_next = flags | FLAG["gap"], False
264
+ seg = Segment(self.serial_done, self.base + len(self.data), n, self.time_of(self.produced), self.uncertainty_ns,
265
+ trigger_index, flags | (FLAG["slipped"] if self.slipped else 0))
266
+ self.data += self._pack(self.produced, n)
267
+ self.produced += n
268
+ self.segs.append(seg)
269
+ self.serial_done += 1
270
+ return seg
271
+
272
+ # ---- the operations ---------------------------------------------------------------------------------------------
273
+ def start(self, now_ms: int, group_ns: int | None = None, trigger_ns: int | None = None) -> list[bytes]:
274
+ """-> the events it raises (kind(u8) payload). In a group: its start and, from the trigger track, the trigger's
275
+ time to mark on this track."""
276
+ if self.state == STATE["unconfigured"]:
277
+ raise Reject(m.UNAVAILABLE)
278
+ self._clear()
279
+ self.started_ms = now_ms
280
+ self.t0_ns = (group_ns if group_ns is not None else now_ms * 1_000_000) + self.start_offset_ns
281
+ self.vrefint_ns = self.t0_ns if self.analog else NO_TIME
282
+ events = []
283
+ if self.mode == MODE["one_shot"]:
284
+ if trigger_ns is not None:
285
+ index = self.index_at(trigger_ns)
286
+ elif self.trigger:
287
+ index = self.trigger_at()
288
+ events.append(bytes([EVENT["triggered"]]) + struct.pack("<IIQ", 0, index, self.time_of(index)))
289
+ else:
290
+ index = NONE
291
+ seg = self._segment(self.samples, index)
292
+ self.state = STATE["done"]
293
+ events += [bytes([EVENT["segment"]]) + seg.pack(), bytes([EVENT["stopped"], STOPPED["complete"]])]
294
+ else:
295
+ self.state = STATE["capturing"]
296
+ return events
297
+
298
+ def tick(self, now_ms: int) -> list[bytes]:
299
+ """Time passes: repeat's segments, streaming's bytes. -> events."""
300
+ if self.state != STATE["capturing"]:
301
+ return []
302
+ due = int((now_ms - self.started_ms) * self.rate / 1000) # samples the clock has taken since start
303
+ events = []
304
+ if self.mode == MODE["repeat"]:
305
+ while due - self.produced >= self.samples:
306
+ if len(self.segs) >= self.segments_max: # no free segment: the capture stops
307
+ self.state = STATE["paused"]
308
+ events.append(bytes([EVENT["stopped"], STOPPED["no_free_segment"]]))
309
+ break
310
+ seg = self._segment(self.samples)
311
+ events.append(bytes([EVENT["segment"]]) + seg.pack())
312
+ elif self.mode == MODE["streaming"]:
313
+ per_byte = 8 // self.width if not self.analog and self.width < 8 else 1 # whole bytes only
314
+ n = min((due - self.produced) // per_byte * per_byte, MAX_SAMPLES)
315
+ if n > 0:
316
+ self.data += self._pack(self.produced, n)
317
+ self.produced += n
318
+ return events
319
+
320
+ def stop(self) -> list[bytes]:
321
+ if self.state not in (STATE["capturing"], STATE["waiting"], STATE["paused"]):
322
+ return []
323
+ self.state = STATE["done"]
324
+ return [bytes([EVENT["stopped"], STOPPED["host"]])]
325
+
326
+ def status(self) -> bytes:
327
+ return struct.pack("<BIQB", self.state, self.serial_done, self.base + len(self.data), 0)
328
+
329
+ def read(self, position: int, most: int, budget: int) -> bytes:
330
+ flags = 0
331
+ if position < self.base:
332
+ position, flags = self.base, flags | 0x02 # re-used: from what is kept (gap)
333
+ end = self.base + len(self.data)
334
+ take = max(0, min(most, self.max_read, budget - 9, end - position))
335
+ data = bytes(self.data[position - self.base:position - self.base + take])
336
+ if position + take < end:
337
+ flags |= 0x01 # more
338
+ return struct.pack("<QB", position, flags) + data
339
+
340
+ def segment_list(self, first: int, budget: int) -> bytes:
341
+ out = [s.pack() for s in self.segs if s.serial >= first]
342
+ out = out[:max(0, (budget - 1) // SEGMENT.size)][:255]
343
+ return bytes([len(out)]) + b"".join(out)
344
+
345
+ def release(self, serial: int, now_ms: int) -> None:
346
+ if self.mode != MODE["repeat"]:
347
+ return # one-shot: the next start clears; streaming: pushed
348
+ keep = [s for s in self.segs if s.serial > serial]
349
+ drop_to = keep[0].position if keep else self.base + len(self.data)
350
+ del self.data[:drop_to - self.base]
351
+ self.base = drop_to
352
+ self.segs = keep
353
+ if self.state == STATE["paused"]: # it goes on from now; the gap shows (flags bit0)
354
+ self.state, self.gap_next = STATE["capturing"], True
355
+ self.started_ms = now_ms - int(self.produced * 1000 / self.rate)
356
+
357
+ def pushes(self, fn: int, next_seq, budget: int) -> list[bytes]:
358
+ """Streaming: the bytes not pushed yet, as data frames (position(u64) then data, common §1.5)."""
359
+ if self.mode != MODE["streaming"]:
360
+ return []
361
+ out = []
362
+ end = self.base + len(self.data)
363
+ while self.sent < end:
364
+ n = min(end - self.sent, budget - 13)
365
+ chunk = bytes(self.data[self.sent - self.base:self.sent - self.base + n])
366
+ out.append(bytes([m.ROLE_DATA]) + struct.pack("<HHQ", fn, next_seq(), self.sent) + chunk)
367
+ self.sent += n
368
+ del self.data[:self.sent - self.base] # pushed: the probe re-uses it
369
+ self.base = self.sent
370
+ return out
371
+
372
+
373
+ @dataclass
374
+ class FakeGroup:
375
+ """oep.fixture.capture-group (§4): tracks started together, one of them the trigger."""
376
+ tracks_allowed: list[int] # the fns it may bind (describe tracks)
377
+ max_tracks: int
378
+ budgets: list[tuple[int, list[int]]] # (max channel-samples / s, the fns sharing it)
379
+ tracks: list[int] = field(default_factory=list)
380
+ trigger_fn: int = 0
381
+ start_ns: int = NO_TIME
382
+ trigger_ns: int = NO_TIME
383
+
384
+ def bind(self, caps: dict[int, FakeCapture], fns: list[int], trigger_fn: int) -> None:
385
+ for fn in self.tracks:
386
+ caps[fn].group = None
387
+ self.tracks, self.trigger_fn, self.start_ns, self.trigger_ns = [], 0, NO_TIME, NO_TIME
388
+ if not fns:
389
+ return
390
+ chosen = [caps.get(fn) for fn in fns]
391
+ if (len(set(fns)) != len(fns) or len(fns) > self.max_tracks or any(fn not in self.tracks_allowed for fn in fns)
392
+ or any(c is None or c.state == STATE["unconfigured"] for c in chosen)
393
+ or len({c.mode for c in chosen}) != 1 or (trigger_fn and trigger_fn not in fns)
394
+ or any(c.trigger for fn, c in zip(fns, chosen) if fn != trigger_fn)):
395
+ raise Reject(m.UNAVAILABLE)
396
+ for most, shared in self.budgets:
397
+ if sum(caps[fn].channels * caps[fn].rate for fn in fns if fn in shared) > most:
398
+ raise Reject(m.UNAVAILABLE)
399
+ self.tracks, self.trigger_fn = list(fns), trigger_fn
400
+ for c in chosen:
401
+ c.group = self
402
+
403
+ def start(self, caps: dict[int, FakeCapture], now_ms: int) -> tuple[list[tuple[int, list[bytes]]], list[bytes]]:
404
+ """-> (each track's events, the group's events)."""
405
+ if not self.tracks:
406
+ raise Reject(m.UNAVAILABLE)
407
+ self.start_ns, self.trigger_ns = now_ms * 1_000_000, NO_TIME
408
+ per, own = [], []
409
+ trigger_ns = None
410
+ src = caps.get(self.trigger_fn)
411
+ if src is not None and src.trigger and src.mode == MODE["one_shot"]:
412
+ per.append((self.trigger_fn, src.start(now_ms, self.start_ns)))
413
+ trigger_ns = self.trigger_ns = src.time_of(src.segs[0].trigger_index)
414
+ own.append(bytes([GRP.event["triggered"]]) + struct.pack("<HQ", self.trigger_fn, trigger_ns))
415
+ for fn in self.tracks:
416
+ if fn != self.trigger_fn or trigger_ns is None:
417
+ per.append((fn, caps[fn].start(now_ms, self.start_ns, trigger_ns)))
418
+ if all(caps[fn].state == STATE["done"] for fn in self.tracks):
419
+ own.append(bytes([GRP.event["stopped"], STOPPED["complete"]]))
420
+ return per, own
421
+
422
+ def stop(self, caps: dict[int, FakeCapture]) -> tuple[list[tuple[int, list[bytes]]], list[bytes]]:
423
+ per = [(fn, caps[fn].stop()) for fn in self.tracks]
424
+ return per, [bytes([GRP.event["stopped"], STOPPED["host"]])] if any(e for _, e in per) else []
425
+
426
+ def status(self, caps: dict[int, FakeCapture]) -> bytes:
427
+ states = [caps[fn].state for fn in self.tracks]
428
+ if not states:
429
+ state = STATE["unconfigured"]
430
+ elif all(s == STATE["done"] for s in states):
431
+ state = STATE["done"]
432
+ elif any(s in (STATE["capturing"], STATE["paused"]) for s in states):
433
+ state = STATE["capturing"]
434
+ else:
435
+ state = STATE["configured"]
436
+ return struct.pack("<BQQH", state, self.start_ns, self.trigger_ns,
437
+ self.trigger_fn if self.trigger_ns != NO_TIME else 0)
oep_client/fake_serial.py CHANGED
@@ -91,6 +91,8 @@ class FakeSerialPort:
91
91
  def tick(self) -> None:
92
92
  self._gap(self.ep.now())
93
93
  self.ep.tick()
94
+ for f in self.ep.pushes(): # events and data the probe sends by itself (core §11)
95
+ self.frames.append(cobs.frame(f))
94
96
 
95
97
  def output(self, room: int = 4096) -> bytes:
96
98
  """What the probe sends now: waiting answers first, then raw bytes by the bind (up to `room` in all)."""
oep_client/fake_serve.py CHANGED
@@ -12,7 +12,7 @@ The first line on stdout says where to open: `PTY /dev/pts/N` or `PORT n`. The p
12
12
  (so a test's child never stays behind), or with --once when the first TCP connection closes.
13
13
 
14
14
  Options:
15
- --profile NAME p4-x035 (default), esp32-v003 or p4-bench (p4_x035 style names work too)
15
+ --profile NAME p4-x035 (default), esp32-v003, p4-bench or rp2350-pins (p4_x035 style names work too)
16
16
  --port-index N which serial port of the profile the pty / cobs TCP is (default: the first one)
17
17
  --noise TEXT raw bytes written in front of every answer (the host must skip them)
18
18
  --drop N the N-th answer (1-based) is not sent, once (the request did run: a resend gets the
@@ -25,6 +25,7 @@ Options:
25
25
  --bind MODE bind the serial port to every --slot: last-reset, manual or mixed
26
26
  --target-id HEX the target_id every target's attach reports (wch_dmi_7f)
27
27
  --absent N the N-th pin pair of the first wire has no target (repeatable)
28
+ --capture-slipped every oep.fixture.capture segment says flags bit2 (slipped: a pace that fell behind)
28
29
  --uart-plan the first oep.fixture.uart gets its RX / TX plan at boot, as if saved (the jig's "DUT TX" /
29
30
  "DUT RX" labels when the profile has them, else the first free channels); configure then works
30
31
  --uart-rx TEXT what arrives on that UART's RX every --every ms once it is configured (%d = a counter)
@@ -70,6 +71,7 @@ def build(a: argparse.Namespace) -> endpoint.Endpoint:
70
71
  hook = _load_hook(a.run_hook)
71
72
  for tg in ep.targets.values():
72
73
  tg.run_hook = (lambda t: lambda pc, regs: hook(t, pc, regs))(tg)
74
+ ep.capture_slipped = getattr(a, "capture_slipped", False)
73
75
  for n in a.absent:
74
76
  ep.targets[(wire, ep.pairs[wire][n])].present = False
75
77
  items = []
@@ -294,6 +296,10 @@ def _serve_conn(a, ep, console, conn, watch_stdin) -> bool:
294
296
  conn.setblocking(False)
295
297
  else:
296
298
  ep.tick()
299
+ for f in ep.pushes(): # events and data pushes (core §11)
300
+ conn.setblocking(True)
301
+ conn.sendall(struct.pack("<H", len(f)) + f)
302
+ conn.setblocking(False)
297
303
 
298
304
 
299
305
  def main(argv: list[str] | None = None) -> None:
@@ -317,6 +323,7 @@ def main(argv: list[str] | None = None) -> None:
317
323
  ap.add_argument("--uart-rx")
318
324
  ap.add_argument("--run-hook")
319
325
  ap.add_argument("--once", action="store_true")
326
+ ap.add_argument("--capture-slipped", action="store_true")
320
327
  a = ap.parse_args(argv)
321
328
  if a.tcp is None and a.framing == "length":
322
329
  ap.error("--framing length is for --tcp")
oep_client/registry.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  from types import SimpleNamespace as _NS
4
4
 
5
- REGISTRY_HASH = "08b7d1656bf59269"
5
+ REGISTRY_HASH = "27f1876a8dfdabb7"
6
6
  SCHEMA = 1
7
7
  PROTOCOL_REVISION = 1
8
8
  CONFIRM_REQUEST_MAGIC = 'OEP?'
@@ -22,7 +22,7 @@ TIMING = {"resync_quiet_ms": 0x32, "probe_frame_gap_ms": 0xC8, "host_frame_pause
22
22
 
23
23
  INTERFACES = {}
24
24
  CORE = _NS(name="oep.core", revision=1, op={"confirm": 0x01, "list": 0x02, "describe": 0x03, "plan_apply": 0x04, "plan_release": 0x05, "open": 0x10, "end": 0x11, "keepalive": 0x12, "lock_state": 0x13, "subscribe": 0x30, "unsubscribe": 0x32, "link_source": 0x40, "link_sink": 0x41}, lock_free={0x01, 0x02, 0x03, 0x10, 0x13, 0x40, 0x41},
25
- closed_tail={0x40}, tlv={"plan_apply": {"role_assignment": 0x90}, "open": {"owner": 0x01}, "lock_state_answer": {"owner": 0x01}, "locked_payload": {"owner": 0x01}, "describe": {"firmware": 0x40, "model": 0x41, "unit_id": 0x42, "channels": 0x43, "reserved": 0x44, "profile": 0x45, "label": 0x46, "resets_on_open": 0x47, "transport": 0x49, "oep_pid": 0x4A, "plan_roles": 0x4B}}, event={"heartbeat": 0x01}, enum={"transport_kind": {"uart_bridge": 0x01, "usb_cdc": 0x02, "usb_serial_jtag": 0x03, "vendor_bulk": 0x04, "hid": 0x05, "tcp": 0x06}}, own={})
25
+ closed_tail={0x40}, tlv={"plan_apply": {"role_assignment": 0x90}, "open": {"owner": 0x01}, "lock_state_answer": {"owner": 0x01}, "locked_payload": {"owner": 0x01}, "describe": {"firmware": 0x40, "model": 0x41, "unit_id": 0x42, "channels": 0x43, "reserved": 0x44, "profile": 0x45, "label": 0x46, "resets_on_open": 0x47, "transport": 0x49, "oep_pid": 0x4A, "plan_roles": 0x4B, "chip": 0x4C}}, event={"heartbeat": 0x01}, enum={"transport_kind": {"uart_bridge": 0x01, "usb_cdc": 0x02, "usb_serial_jtag": 0x03, "vendor_bulk": 0x04, "hid": 0x05, "tcp": 0x06}}, own={})
26
26
  INTERFACES["oep.core"] = CORE
27
27
  WIRE_RVSWD = _NS(name="oep.wire.rvswd", revision=1, op={"scan": 0x01, "attach": 0x02, "detach": 0x03, "attach_under_reset": 0x04, "connections": 0x05}, lock_free={0x05},
28
28
  closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "scan": {"skip": 0x01}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03, "idle_clock": 0x04}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03, "idle_clock": 0x04}, "attach_answer": {"target_id": 0x10}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "target_id_scheme": {"wch_dmi_7f": 0x01}, "attach_method": {"run": 0x00, "halt": 0x01}, "pin_role": {"swdio": 0x01, "swclk": 0x02, "reset": 0x03}, "idle_clock": {"high": 0x00, "low": 0x01}}, own={})
@@ -49,11 +49,14 @@ FIXTURE_UART = _NS(name="oep.fixture.uart", revision=1, op={"configure": 0x01, "
49
49
  closed_tail={0x02}, tlv={"configure": {"format": 0x01}, "describe": {"formats": 0x40}}, event={}, enum={"role": {"rx": 0x01, "tx": 0x02}}, own={})
50
50
  INTERFACES["oep.fixture.uart"] = FIXTURE_UART
51
51
  FIXTURE_CAPTURE = _NS(name="oep.fixture.capture", revision=1, op={"configure": 0x01, "start": 0x02, "stop": 0x03, "force": 0x04, "status": 0x05, "read": 0x06, "segments": 0x07, "release": 0x08, "query": 0x09}, lock_free={0x05, 0x06, 0x07, 0x09},
52
- closed_tail={0x06}, tlv={"configure": {"mode": 0x40, "rate": 0x42, "samples": 0x43, "segments": 0x44, "trigger": 0x45, "pretrigger": 0x46, "frontend": 0x47}, "configure_answer": {"actual_rate": 0x50, "layout": 0x51, "actual_samples": 0x52, "actual_segments": 0x53, "timing": 0x54, "scale": 0x55, "blocking_ms": 0x56, "skew": 0x57}, "describe": {"mode": 0x40, "rate_range": 0x41, "rate_list": 0x42, "rate_limit": 0x43, "channels": 0x44, "trigger": 0x45, "frontend": 0x46, "max_read": 0x47, "segment_ring": 0x48, "frontend_shared": 0x49}}, event={"segment": 0x01, "stopped": 0x02, "triggered": 0x03}, enum={"mode": {"one_shot": 0x01, "repeat": 0x02, "streaming": 0x03}, "trigger": {"immediate": 0x00, "level": 0x01, "edge": 0x02, "cross_up": 0x03, "cross_down": 0x04}, "state": {"unconfigured": 0x00, "configured": 0x01, "waiting": 0x02, "capturing": 0x03, "done": 0x04, "paused": 0x05, "error": 0x06}, "stopped_reason": {"complete": 0x00, "host": 0x01, "no_free_segment": 0x02, "error": 0x03}, "segment_flag": {"gap": 0x01, "short": 0x02, "slipped": 0x04}}, own={})
52
+ closed_tail={0x06}, tlv={"configure": {"mode": 0x40, "rate": 0x42, "samples": 0x43, "segments": 0x44, "trigger": 0x45, "pretrigger": 0x46, "frontend": 0x47}, "configure_answer": {"actual_rate": 0x50, "layout": 0x51, "actual_samples": 0x52, "actual_segments": 0x53, "timing": 0x54, "scale": 0x55, "blocking_ms": 0x56, "rate_accuracy": 0x5A, "skew": 0x57}, "describe": {"mode": 0x40, "rate_range": 0x41, "rate_list": 0x42, "rate_limit": 0x43, "channels": 0x44, "trigger": 0x45, "frontend": 0x46, "max_read": 0x47, "segment_ring": 0x48, "frontend_shared": 0x49}}, event={"segment": 0x01, "stopped": 0x02, "triggered": 0x03}, enum={"mode": {"one_shot": 0x01, "repeat": 0x02, "streaming": 0x03}, "trigger": {"immediate": 0x00, "level": 0x01, "edge": 0x02, "cross_up": 0x03, "cross_down": 0x04}, "state": {"unconfigured": 0x00, "configured": 0x01, "waiting": 0x02, "capturing": 0x03, "done": 0x04, "paused": 0x05, "error": 0x06}, "stopped_reason": {"complete": 0x00, "host": 0x01, "no_free_segment": 0x02, "error": 0x03}, "segment_flag": {"gap": 0x01, "short": 0x02, "slipped": 0x04}}, own={})
53
53
  INTERFACES["oep.fixture.capture"] = FIXTURE_CAPTURE
54
- FIXTURE_ANALOG = _NS(name="oep.fixture.analog", revision=1, op={"configure": 0x01, "start": 0x02, "stop": 0x03, "force": 0x04, "status": 0x05, "read": 0x06, "segments": 0x07, "release": 0x08, "query": 0x09}, lock_free={0x05, 0x06, 0x07, 0x09},
55
- closed_tail={0x06}, tlv={"configure": {"mode": 0x40, "rate": 0x42, "samples": 0x43, "segments": 0x44, "trigger": 0x45, "pretrigger": 0x46, "frontend": 0x47}, "configure_answer": {"actual_rate": 0x50, "layout": 0x51, "actual_samples": 0x52, "actual_segments": 0x53, "timing": 0x54, "scale": 0x55, "blocking_ms": 0x56, "skew": 0x57}, "describe": {"mode": 0x40, "rate_range": 0x41, "rate_list": 0x42, "rate_limit": 0x43, "channels": 0x44, "trigger": 0x45, "frontend": 0x46, "max_read": 0x47, "segment_ring": 0x48, "frontend_shared": 0x49}}, event={"segment": 0x01, "stopped": 0x02, "triggered": 0x03}, enum={"mode": {"one_shot": 0x01, "repeat": 0x02, "streaming": 0x03}, "trigger": {"immediate": 0x00, "level": 0x01, "edge": 0x02, "cross_up": 0x03, "cross_down": 0x04}, "state": {"unconfigured": 0x00, "configured": 0x01, "waiting": 0x02, "capturing": 0x03, "done": 0x04, "paused": 0x05, "error": 0x06}, "stopped_reason": {"complete": 0x00, "host": 0x01, "no_free_segment": 0x02, "error": 0x03}, "segment_flag": {"gap": 0x01, "short": 0x02, "slipped": 0x04}}, own={})
54
+ FIXTURE_ANALOG = _NS(name="oep.fixture.analog", revision=1, op={"configure": 0x01, "start": 0x02, "stop": 0x03, "force": 0x04, "status": 0x05, "read": 0x06, "segments": 0x07, "release": 0x08, "query": 0x09, "calibration": 0x0A}, lock_free={0x05, 0x06, 0x07, 0x09, 0x0A},
55
+ closed_tail={0x06}, tlv={"configure": {"mode": 0x40, "rate": 0x42, "samples": 0x43, "segments": 0x44, "trigger": 0x45, "pretrigger": 0x46, "frontend": 0x47}, "configure_answer": {"actual_rate": 0x50, "layout": 0x51, "actual_samples": 0x52, "actual_segments": 0x53, "timing": 0x54, "scale": 0x55, "blocking_ms": 0x56, "skew": 0x57, "rate_accuracy": 0x5A, "frontend_used": 0x58, "reference": 0x59}, "calibration_answer": {"factory": 0x01, "vrefint": 0x02}, "describe": {"mode": 0x40, "rate_range": 0x41, "rate_list": 0x42, "rate_limit": 0x43, "channels": 0x44, "trigger": 0x45, "frontend": 0x46, "max_read": 0x47, "segment_ring": 0x48, "frontend_shared": 0x49}}, event={"segment": 0x01, "stopped": 0x02, "triggered": 0x03}, enum={"mode": {"one_shot": 0x01, "repeat": 0x02, "streaming": 0x03}, "trigger": {"immediate": 0x00, "level": 0x01, "edge": 0x02, "cross_up": 0x03, "cross_down": 0x04}, "reference_source": {"supply": 0x00, "internal": 0x01, "external": 0x02}, "state": {"unconfigured": 0x00, "configured": 0x01, "waiting": 0x02, "capturing": 0x03, "done": 0x04, "paused": 0x05, "error": 0x06}, "stopped_reason": {"complete": 0x00, "host": 0x01, "no_free_segment": 0x02, "error": 0x03}, "segment_flag": {"gap": 0x01, "short": 0x02, "slipped": 0x04}}, own={})
56
56
  INTERFACES["oep.fixture.analog"] = FIXTURE_ANALOG
57
+ FIXTURE_CAPTURE_GROUP = _NS(name="oep.fixture.capture-group", revision=1, op={"bind": 0x01, "start": 0x02, "stop": 0x03, "force": 0x04, "status": 0x05}, lock_free={0x05},
58
+ closed_tail={}, tlv={"bind": {"trigger_track": 0x01}, "describe": {"tracks": 0x40, "max_tracks": 0x41, "budget": 0x42, "start_skew": 0x43}}, event={"triggered": 0x01, "stopped": 0x02}, enum={}, own={})
59
+ INTERFACES["oep.fixture.capture-group"] = FIXTURE_CAPTURE_GROUP
57
60
  PROBE_CONFIG = _NS(name="oep.probe.config", revision=1, op={"get": 0x01, "set": 0x02, "save": 0x03, "erase": 0x04}, lock_free={0x01},
58
61
  closed_tail={}, tlv={"item": {"plan": 0x01, "label": 0x02, "idle": 0x03, "slot": 0x04, "bind": 0x05}, "describe": {"storage": 0x40, "items": 0x41, "slots_max": 0x42, "bind_modes": 0x43, "slot_state": 0x44, "bind_state": 0x45}}, event={}, enum={"idle_mode": {"hi_z": 0x00, "pull_up": 0x01, "pull_down": 0x02}, "slot_attach": {"host": 0x00, "at_boot": 0x01}, "slot_state": {"connected": 0x00, "absent": 0x01, "lock_mismatch": 0x02, "no_target_id": 0x03}, "bind_mode": {"last_reset": 0x00, "manual": 0x01, "mixed": 0x02}, "bind_stream": {"slot_console": 0x01, "fixture_uart": 0x02}, "bind_flow": {"idle": 0x00, "streaming": 0x01, "held": 0x02}, "storage_state": {"none": 0x00, "applied": 0x01, "unreadable": 0x02}}, own={})
59
62
  INTERFACES["oep.probe.config"] = PROBE_CONFIG
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.8
3
+ Version: 0.0.10
4
4
  Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe
5
5
  Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
6
6
  Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
@@ -107,8 +107,19 @@ A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`,
107
107
 
108
108
  `endpoint.Endpoint` is a fake probe that answers as oep-spec says; ch32rv, this client and the probe firmware are checked
109
109
  against it (when the spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
110
- `p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats), `fake_serial` the byte side of a serial
111
- port (COBS candidates, raw bytes and binds, held during a session and resumed after it).
110
+ `p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats, `rp2350-pins` = a wire whose pins the host
111
+ chooses), `fake_serial` the byte side of a serial port (COBS candidates, raw bytes and binds, held during a session and
112
+ resumed after it).
113
+
114
+ `fake_capture` is `oep.fixture.capture` (logic): one-shot, repeat (segments with the clock at the actual rate, a ring,
115
+ release) and streaming (data pushes while subscribed), level / edge triggers with a pretrigger, and events. What it
116
+ captures is known: sample i is the counter i, channel k its bit k (a square wave of period 2^(k+1) samples), in the layout
117
+ the profile allows (`p4-x035`: w 1-16 as the P4's PARLIO, three channels in w 4; `esp32-v003`: w 8 as the classic ESP32's
118
+ sampler, one-shot only). A capture only listens, so it may be planned on pins other interfaces hold. `p4-x035` also has
119
+ `oep.fixture.analog` (4 channels of the P4's ADC1 on GPIO16-23: an even channel k a square wave, an odd one a sine, of
120
+ period 64 (k // 2 + 1) samples, 12-bit values in 16-bit slots; ESP32-style frontends; a made-up two-point calibration and a
121
+ Vrefint) and `oep.fixture.capture-group` binding the logic and the analog: started together, the analog 5 us later
122
+ (+-2 us), the trigger of one marked on both. Times are ns on the probe's one clock.
112
123
 
113
124
  Other programs' tests run `fake_serve` as a child process:
114
125
 
@@ -122,4 +133,5 @@ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--f
122
133
  `--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
123
134
  so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
124
135
  broken once). `--uart-plan` / `--uart-rx` give the first fixture UART a plan and RX bytes, `--run-hook` a host's own model of
125
- riscv-dm run. The rest: `--help`.
136
+ riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. Events and data pushes go out on the pty and on TCP
137
+ (both framings). The rest: `--help`.
@@ -1,7 +1,7 @@
1
- oep_client/__init__.py,sha256=6mobCxy7gb0xBip7N4wVAbSsDJZjvzzWtMDUU5jpXu8,163
1
+ oep_client/__init__.py,sha256=W4Jmi9TS_xd7VSEtxOeaXMp33zr8gXCFqH9a0VwBDy0,164
2
2
  oep_client/__main__.py,sha256=qtVGdpAHULBtLY2SedabJf-uwDZZhMf_YmrbqRzM57U,14669
3
3
  oep_client/arm.py,sha256=JLyUhLSPcmsk73schZcaJYoOKuHzrvnU8kPmfrEvVuo,12331
4
- oep_client/capture.py,sha256=hs8mQU1gE1P67GOivxw2C2Ur8wimWqCdhtk1nqGN71Q,18092
4
+ oep_client/capture.py,sha256=mL_C2_5hK9D9nR_F4f4YNN-uaRVegMN-8vnwHR7rQkg,24962
5
5
  oep_client/catalog.py,sha256=EqAQnSgHFxGbCO2GQ_Ee-uheJKbBBUUpL1-0qHfNMXU,6574
6
6
  oep_client/ch32_flash.py,sha256=Fc-6jEj15cQtMSKmfC5dmDetMXJ8hAdAuJ9d9odwrsA,12651
7
7
  oep_client/cobs.py,sha256=hqddQI_X7rsBvBvcP_MBE3bXPyB13-Azjqp_9JfdQ1g,2288
@@ -10,11 +10,12 @@ oep_client/console.py,sha256=YYcuc9Qxe3kihla8wynYbDaLVr7wBLEvswrM-cMK3Tk,6859
10
10
  oep_client/core.py,sha256=Cyyaknjipkg6tS76dZe5GMqjmOoQjlfeP0WFtMHkJNs,10403
11
11
  oep_client/decode.py,sha256=M-bRARrA2qGZLoJn0BdXeTb4WjL_2Hb2nk_g1IGXIw4,2382
12
12
  oep_client/dump.py,sha256=GAnmu2abuDmGmbWcbbEfC3EUuPp5Hady9vQVNcQ-724,6743
13
- oep_client/endpoint.py,sha256=Cmzrjsrv--70cUu2UVNpJ0RZVJesV9DNJweg1jOYe0s,77818
13
+ oep_client/endpoint.py,sha256=aa9znMaedjEnMDNcGsV-4gxTZJSd6IxNyPpcJM2PEzk,87244
14
14
  oep_client/esp32_targets.py,sha256=b8tNH-nb2KC-XgNfWg5l3TO0cyhZOF-gJajaVxNVSGQ,4988
15
- oep_client/fake.py,sha256=oaEMn0cjIalcQKPQ2mZ5KL-E6U_43Ppr3-dj96dS7Q0,14513
16
- oep_client/fake_serial.py,sha256=uKl0UNcifhdeykQtm8gnRRhqV7TT4vPwBFuBhbf-9BI,4504
17
- oep_client/fake_serve.py,sha256=Ke_hGRiOayCyut-uYOa0PaOFzy-zlvPHGwjs4HEZkAQ,14644
15
+ oep_client/fake.py,sha256=jTaiii8DtByI_CyUYVRoUnGT05k5OdIerSdQig4HKrk,18096
16
+ oep_client/fake_capture.py,sha256=6BadaybnZ0jRDfxjnkLXttw65rkoPgsIY5FvAWKxMg8,22812
17
+ oep_client/fake_serial.py,sha256=nXpbBuPl9sAh5BdoWSsa5EDYvPC7VLOvaBYrslGlniA,4659
18
+ oep_client/fake_serve.py,sha256=CzJ_TO_gWVNPP0r5ggQ57rgwAgBapunGI0YtyW8MrRM,15133
18
19
  oep_client/fixture.py,sha256=653hCD-Iz0Lfs8dT4Hdg3QyubbbOGs1_xjq_3nb8foU,5564
19
20
  oep_client/frames.py,sha256=xjgjnXmXUqNJZ5m0h91r4Cy48WoVK1mEMW4gRg-mlMI,3991
20
21
  oep_client/hid_stream.py,sha256=99Dsm4Io1keukuETG_UNqjBGiVYZZeb__Pv4ytaKg9k,10695
@@ -23,14 +24,14 @@ oep_client/interfaces.py,sha256=TNvof5blyLjO1Mc0OentKcMVBD_5e4riRG18UgEh2ZE,5380
23
24
  oep_client/link.py,sha256=J9VtBWfTgNoIgUQuj8SsH9hHesQM2uEN545IoxKVS8E,19406
24
25
  oep_client/message.py,sha256=MEM1UT6KFM7AsT56jvBLt_FJK_6SJ7O_nGAiipW0_zo,9912
25
26
  oep_client/names.py,sha256=ztCZFWs__aR-a5G14TnJP-Q4YfLhbyrzvOVOMJuG93k,3653
26
- oep_client/registry.py,sha256=6FVVEs1b6krGygH21zma84mmMB_B65dh8S7PrjYrME0,10056
27
+ oep_client/registry.py,sha256=g6TfTWVPer9M46we6qSjMZqnqDbL6A49rdxM0v67Hx0,10758
27
28
  oep_client/riscv.py,sha256=H9kE_oUsnaBzqdTXDAVgDQkDvsrn5ZwsVi8dF1W_BDM,23117
28
29
  oep_client/rp2350.py,sha256=7OMyj270k3AY1e3-ljicPwOjf9jOlokbT6QNkA0b0yY,4227
29
30
  oep_client/target.py,sha256=R8BvYfFfYNhZF0MysJkNb8b69YgBlmeGXNX8ReZIe5k,1068
30
31
  oep_client/uiapduino.py,sha256=IEPG3LbZPksi8g3wmAf6T0CRWSfYKW1_qrH_GBVQdxY,6090
31
32
  oep_client/usb_stream.py,sha256=VJMCxsOlQCk005FcS92pqTh33fRSFBizoL__L0VbUMA,10242
32
- oep_client_python-0.0.8.dist-info/METADATA,sha256=TKbEmCwYIw03I3j7Rt-N1El8OQdlAvTQcLtVvb2mNMY,7368
33
- oep_client_python-0.0.8.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
34
- oep_client_python-0.0.8.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
35
- oep_client_python-0.0.8.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
36
- oep_client_python-0.0.8.dist-info/RECORD,,
33
+ oep_client_python-0.0.10.dist-info/METADATA,sha256=K29Cy_nfBC_19RrkZ_hZQtD9nzLoW1oTWw9rinSp8Hc,8590
34
+ oep_client_python-0.0.10.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
35
+ oep_client_python-0.0.10.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
36
+ oep_client_python-0.0.10.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
37
+ oep_client_python-0.0.10.dist-info/RECORD,,