oep-client-python 0.0.9__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.9"
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
@@ -276,7 +276,10 @@ class Endpoint:
276
276
  self.targets[(fn, first)] = FakeTarget()
277
277
  self.target = next(iter(self.targets.values()), FakeTarget())
278
278
  self.captures: dict[int, fake_capture.FakeCapture] = {
279
- fn: self._capture_from(self.static[fn]) for fn, name in self.names.items() if name == "oep.fixture.capture"}
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"}
280
283
  self.mechanisms = set()
281
284
  for fn, name in self.names.items():
282
285
  if name == "oep.target.console":
@@ -349,10 +352,13 @@ class Endpoint:
349
352
  def _capture_from(tlvs: list[bytes]) -> fake_capture.FakeCapture:
350
353
  """A capture as its describe declares it (modes, the w allowed, the rate range, max_read, the ring)."""
351
354
  d = reg.FIXTURE_CAPTURE.tlv["describe"]
352
- modes, widths, lo, hi, ring, most = set(), {8}, 1, 1_000_000, 1, 1024
355
+ modes, widths, lo, hi, ring, most, fronts = set(), {8}, 1, 1_000_000, 1, 1024, {}
353
356
  for t in tlvs:
354
357
  tag, v = t[0], t[2:2 + t[1]]
355
- if tag == d["mode"]:
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"]:
356
362
  modes.add(v[0])
357
363
  elif tag == d["channels"]:
358
364
  widths = {1 << i for i in range(6) if v[1] >> i & 1}
@@ -364,7 +370,21 @@ class Endpoint:
364
370
  ring = struct.unpack("<H", v)[0]
365
371
  elif tag == d["max_read"]:
366
372
  most = struct.unpack("<I", v)[0]
367
- return fake_capture.FakeCapture(modes or {fake_capture.MODE["one_shot"]}, widths, lo, hi, ring, most)
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)
368
388
 
369
389
  def _next_seq(self, fn: int) -> int:
370
390
  seq = self.push_seq.get(fn, 0)
@@ -394,12 +414,20 @@ class Endpoint:
394
414
  budget = self.probe.max_frame - m.RESULT_HEADER
395
415
  try:
396
416
  if op in (O["configure"], O["query"]):
397
- got, ignored = t.tail(set(fake_capture.TLV.values()) - {fake_capture.TLV["frontend"]})
398
- settled = cap.settle(got, t.critical, len(roles))
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)
399
425
  if op == O["configure"]:
400
426
  cap.apply(settled)
401
427
  cap.slipped = self.capture_slipped
402
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
403
431
  if op == O["start"]:
404
432
  t.tail()
405
433
  self._events(fn, cap.start(self.now()))
@@ -426,6 +454,45 @@ class Endpoint:
426
454
  t.tail()
427
455
  cap.release(serial, self.now())
428
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)
429
496
  except fake_capture.Reject as e:
430
497
  raise Reject(e.reason, e.payload)
431
498
  return m.REJECTED, m.UNKNOWN_OPERATION, b""
@@ -1655,4 +1722,5 @@ class Endpoint:
1655
1722
 
1656
1723
  SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
1657
1724
  "oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
1658
- "oep.probe.config": "config_op", "oep.fixture.capture": "capture"}
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
@@ -109,6 +109,27 @@ SERIAL_KINDS = {TRANSPORT["uart_bridge"], TRANSPORT["usb_cdc"], TRANSPORT["usb_s
109
109
  NS = "io.github.ch32-riscv-ug"
110
110
  MECHANISMS = reg.TARGET_CONSOLE.tlv["describe"]["mechanisms"]
111
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()))
112
133
  _CAPM = reg.FIXTURE_CAPTURE.enum["mode"]
113
134
 
114
135
 
@@ -168,7 +189,8 @@ def p4_x035() -> FakeProbe:
168
189
  _core("3.0.0", "esp32-p4-devkit", bytes.fromhex("30eda0e31108"), 55, reserved, f"{NS}.p4-x035",
169
190
  {2: "SWDIO", 54: "SWCLK", 51: "LED"},
170
191
  _transports([(TRANSPORT["usb_serial_jtag"], 0xFF), (TRANSPORT["vendor_bulk"], 0),
171
- (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"))),
172
194
  Offered(1, 1, "oep.wire.rvswd", (
173
195
  catalog.channel_group(1, [(1, 2), (2, 54)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1),
174
196
  catalog.u8(MAX_CONNECTIONS, 1))),
@@ -191,6 +213,12 @@ def p4_x035() -> FakeProbe:
191
213
  Offered(9, 7, f"{NS}.esp32.spi-target", _roles({1: pins, 2: pins, 3: pins, 4: pins}) + (
192
214
  catalog.u16(MAX_LENGTH, 64), catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
193
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})),
194
222
  ])
195
223
 
196
224
 
@@ -1,33 +1,47 @@
1
1
  # SPDX-License-Identifier: MIT
2
- """oep.fixture.capture in the fake probe (oep-spec docs/oep-if-capture.ja.md, revision 1, logic).
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).
3
4
 
4
- What it captures is known in advance, so a receiver can check it: sample i of a capture (counted from its start, across
5
- segments) is the counter i, and channel k is bit k of it - a square wave of period 2^(k+1) samples. The layout is the
6
- probe's (§1.1): w is the smallest width the describe allows (channels tag 0x44) that holds the channels, pos[k] = k.
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.
7
17
 
8
18
  Modes: one-shot (the segment is there as soon as start answers), repeat (segments come with the clock at the actual
9
19
  rate, up to the ring; a full ring stops the capture, release frees it) and streaming (the bytes come with the clock and
10
- go out as data pushes while subscribed). Triggers: level and edge on a channel, with a pretrigger. `slipped` sets flags
11
- bit2 on every segment (a probe whose software pace fell behind).
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).
12
22
  """
13
23
 
14
24
  from __future__ import annotations
15
25
 
26
+ import math
16
27
  import struct
17
28
  from dataclasses import dataclass, field
18
29
  from fractions import Fraction
19
30
 
20
31
  from . import message as m, registry as reg
21
32
 
22
- CAP = reg.FIXTURE_CAPTURE
33
+ CAP, ANA, GRP = reg.FIXTURE_CAPTURE, reg.FIXTURE_ANALOG, reg.FIXTURE_CAPTURE_GROUP
23
34
  OP = CAP.op
24
- TLV, ANSWER = CAP.tlv["configure"], CAP.tlv["configure_answer"]
35
+ TLV, ANSWER = CAP.tlv["configure"], ANA.tlv["configure_answer"]
25
36
  MODE, STATE, TRIGGER = CAP.enum["mode"], CAP.enum["state"], CAP.enum["trigger"]
26
37
  STOPPED, FLAG = CAP.enum["stopped_reason"], CAP.enum["segment_flag"]
27
38
  EVENT = CAP.event
39
+ CALIBRATION = ANA.tlv["calibration_answer"]
28
40
  NONE = 0xFFFFFFFF
41
+ NO_TIME = 0xFFFFFFFFFFFFFFFF
29
42
  MAX_SAMPLES = 1 << 20 # a fake keeps its captures in memory
30
- SEGMENT = struct.Struct("<IQIQIB") # serial position samples start_us trigger_index flags (§2)
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)
31
45
 
32
46
 
33
47
  class Reject(Exception):
@@ -40,23 +54,35 @@ class Segment:
40
54
  serial: int
41
55
  position: int
42
56
  samples: int
43
- start_us: int
57
+ start_ns: int
58
+ start_uncertainty_ns: int
44
59
  trigger_index: int = NONE
45
60
  flags: int = 0
46
61
 
47
62
  def pack(self) -> bytes:
48
- return SEGMENT.pack(self.serial, self.position, self.samples, self.start_us, self.trigger_index, self.flags)
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))
49
73
 
50
74
 
51
75
  @dataclass
52
76
  class FakeCapture:
53
- """One capture interface. `now_ms` is the endpoint's clock; `roles()` the channels the plan gives it (role order)."""
77
+ """One capture track (logic, or analog when `frontends` is given). Times: the endpoint's clock in ms, the probe's in
78
+ ns."""
54
79
  modes: set[int] # the modes the describe declares
55
- widths: set[int] # the w the describe allows
80
+ widths: set[int] # logic: the w the describe allows
56
81
  min_hz: int
57
82
  max_hz: int
58
83
  ring: int = 8 # segment_ring
59
84
  max_read: int = 4096
85
+ frontends: dict[int, tuple[int, int, int]] = field(default_factory=dict) # analog: n -> (min_mv, max_mv, mdb)
60
86
  slipped: bool = False
61
87
  state: int = STATE["unconfigured"]
62
88
  mode: int = MODE["one_shot"]
@@ -65,31 +91,48 @@ class FakeCapture:
65
91
  segments_max: int = 0
66
92
  trigger: tuple[int, int, int] | None = None
67
93
  pretrigger: int = 0
68
- width: int = 0
94
+ width: int = 0 # logic: w; analog: the bytes of one sample (C x 2)
69
95
  channels: int = 0
70
- data: bytearray = field(default_factory=bytearray) # the stream from `base` on
71
- base: int = 0 # the byte position data[0] is at
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
72
99
  segs: list[Segment] = field(default_factory=list)
73
100
  serial_done: int = 0
74
101
  started_ms: int = 0
75
- produced: int = 0 # samples produced since start (repeat / streaming)
102
+ t0_ns: int = 0 # the probe-clock time of the track's first sample
103
+ produced: int = 0 # samples produced since start
76
104
  sent: int = 0 # streaming: the byte position pushed so far
77
- first_sample: int = 0 # the counter value of the capture's first sample
78
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
79
120
 
80
121
  # ---- configure ------------------------------------------------------------------------------------------------
81
- def settle(self, got: dict[int, bytes], critical: set[int], channels: int) -> dict:
82
- """The actual values for a configure / query request (nothing changed)."""
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
83
126
  mode = got[TLV["mode"]][0] if TLV["mode"] in got else MODE["one_shot"]
84
127
  if mode not in self.modes:
85
128
  raise Reject(m.UNSUPPORTED, bytes([TLV["mode"] | (m.TAG_CRITICAL if TLV["mode"] in critical else 0)]))
86
129
  if TLV["rate"] not in got or len(got[TLV["rate"]]) != 4:
87
130
  raise Reject(m.MALFORMED)
88
131
  asked = struct.unpack("<I", got[TLV["rate"]])[0]
89
- if not asked:
90
- raise Reject(m.MALFORMED)
91
- # the source clock divided by a whole number: the nearest rate at or under the one asked, inside the range
92
- div = max(1, -(-self.max_hz // min(max(asked, self.min_hz), self.max_hz)))
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)))
93
136
  rate = Fraction(self.max_hz, div)
94
137
  samples = struct.unpack("<I", got[TLV["samples"]])[0] if TLV["samples"] in got else 4096
95
138
  samples = max(1, min(samples, MAX_SAMPLES))
@@ -98,94 +141,154 @@ class FakeCapture:
98
141
  trigger = None
99
142
  if TLV["trigger"] in got:
100
143
  kind, role, value = struct.unpack("<BBH", got[TLV["trigger"]])
101
- if kind not in (TRIGGER["immediate"], TRIGGER["level"], TRIGGER["edge"]) or (kind and role >= channels):
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):
102
146
  raise Reject(m.UNSUPPORTED, bytes([TLV["trigger"] | m.TAG_CRITICAL]))
103
147
  trigger = (kind, role, value) if kind else None
104
148
  pre = struct.unpack("<I", got[TLV["pretrigger"]])[0] if TLV["pretrigger"] in got else 0
105
- width = min((w for w in self.widths if w >= max(channels, 1)), default=None)
106
- if width is None or not channels:
107
- raise Reject(m.UNAVAILABLE) # no channels planned, or more than a sample holds
108
- if mode != MODE["one_shot"] and width < 8:
109
- samples = -(-samples // (8 // width)) * (8 // width) # segments end on a byte
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
110
165
  return {"mode": mode, "rate": rate, "samples": samples, "segments": segs, "trigger": trigger,
111
- "pretrigger": min(pre, samples - 1), "width": width, "channels": channels}
166
+ "pretrigger": min(pre, samples - 1), "width": width, "channels": channels, "frontends": chosen}
112
167
 
113
168
  def apply(self, s: dict) -> None:
114
169
  self.mode, self.rate, self.samples, self.segments_max = s["mode"], s["rate"], s["samples"], s["segments"]
115
170
  self.trigger, self.pretrigger, self.width, self.channels = s["trigger"], s["pretrigger"], s["width"], s["channels"]
171
+ self.frontend_of = s["frontends"]
116
172
  self.state = STATE["configured"]
117
173
  self._clear()
118
174
 
119
- @staticmethod
120
- def answer(s: dict) -> bytes:
175
+ def answer(self, s: dict) -> bytes:
121
176
  def tlv(tag: int, v: bytes) -> bytes:
122
177
  return bytes([tag, len(v)]) + v
123
- return (tlv(ANSWER["actual_rate"], struct.pack("<II", s["rate"].numerator, s["rate"].denominator))
124
- + tlv(ANSWER["layout"], bytes([s["width"], s["channels"]]) + bytes(range(s["channels"])))
125
- + tlv(ANSWER["actual_samples"], struct.pack("<I", s["samples"]))
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"]))
126
185
  + tlv(ANSWER["actual_segments"], struct.pack("<I", s["segments"]))
127
186
  + tlv(ANSWER["timing"], struct.pack("<BI", 1 if s["rate"].denominator != 1 else 0, 0))
128
- + tlv(ANSWER["blocking_ms"], struct.pack("<I", 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
129
213
 
130
214
  # ---- the data ---------------------------------------------------------------------------------------------------
131
215
  def _clear(self) -> None:
132
216
  self.data, self.base, self.segs, self.serial_done = bytearray(), 0, [], 0
133
- self.produced, self.sent, self.first_sample, self.gap_next = 0, 0, 0, False
217
+ self.produced, self.sent, self.gap_next = 0, 0, False
134
218
 
135
219
  def _pack(self, first: int, n: int) -> bytes:
136
- """Samples first .. first+n-1 of the counter in the layout (w bits each, little endian, low samples first)."""
137
- w = self.width
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
138
225
  if w >= 8:
139
- per = w // 8
140
- mask = (1 << w) - 1
141
- return b"".join(((first + i) & mask).to_bytes(per, "little") for i in range(n))
226
+ return b"".join(((first + i) & mask).to_bytes(w // 8, "little") for i in range(n))
142
227
  out = bytearray((n * w + 7) // 8)
143
- mask = (1 << w) - 1
144
228
  for i in range(n):
145
229
  bit = i * w
146
230
  out[bit >> 3] |= ((first + i) & mask) << (bit & 7)
147
231
  return bytes(out)
148
232
 
149
- def _trigger_at(self) -> int:
150
- """The first sample, at or after the pretrigger, where the trigger holds (the counter's bit `role`)."""
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)."""
151
238
  kind, role, value = self.trigger
152
- period = 1 << (role + 1)
153
- i = self.pretrigger
154
- while True: # within two periods of the channel
155
- now, before = (i >> role) & 1, ((i - 1) >> role) & 1 if i else None
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)
156
242
  if kind == TRIGGER["level"] and now == (value & 1):
157
243
  return i
158
- if kind == TRIGGER["edge"] and before is not None and now != before:
159
- if value == 2 or (value == 0 and now == 1) or (value == 1 and now == 0):
160
- return i
161
- i += 1
162
- if i > self.pretrigger + 2 * period + 1:
163
- return self.pretrigger
164
-
165
- def _segment(self, n: int, now_ms: int, trigger_index: int = NONE, flags: int = 0) -> Segment:
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:
166
262
  if self.gap_next:
167
263
  flags, self.gap_next = flags | FLAG["gap"], False
168
- seg = Segment(self.serial_done, self.base + len(self.data), n, now_ms * 1000, trigger_index,
169
- flags | (FLAG["slipped"] if self.slipped else 0))
170
- self.data += self._pack(self.first_sample + self.produced, n)
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)
171
267
  self.produced += n
172
268
  self.segs.append(seg)
173
269
  self.serial_done += 1
174
270
  return seg
175
271
 
176
272
  # ---- the operations ---------------------------------------------------------------------------------------------
177
- def start(self, now_ms: int) -> list[bytes]:
178
- """-> the events it raises (kind(u8) payload), for the endpoint to send when subscribed."""
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."""
179
276
  if self.state == STATE["unconfigured"]:
180
277
  raise Reject(m.UNAVAILABLE)
181
278
  self._clear()
182
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
183
282
  events = []
184
283
  if self.mode == MODE["one_shot"]:
185
- index = self._trigger_at() if self.trigger else NONE
186
- if self.trigger:
187
- events.append(bytes([EVENT["triggered"]]) + struct.pack("<II", 0, index))
188
- seg = self._segment(self.samples, now_ms, index)
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)
189
292
  self.state = STATE["done"]
190
293
  events += [bytes([EVENT["segment"]]) + seg.pack(), bytes([EVENT["stopped"], STOPPED["complete"]])]
191
294
  else:
@@ -204,14 +307,14 @@ class FakeCapture:
204
307
  self.state = STATE["paused"]
205
308
  events.append(bytes([EVENT["stopped"], STOPPED["no_free_segment"]]))
206
309
  break
207
- seg = self._segment(self.samples, now_ms)
310
+ seg = self._segment(self.samples)
208
311
  events.append(bytes([EVENT["segment"]]) + seg.pack())
209
312
  elif self.mode == MODE["streaming"]:
210
- per_byte = 8 // self.width if self.width < 8 else 1 # whole bytes only
211
- n = (due - self.produced) // per_byte * per_byte
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)
212
315
  if n > 0:
213
- self.data += self._pack(self.first_sample + self.produced, min(n, MAX_SAMPLES))
214
- self.produced += min(n, MAX_SAMPLES)
316
+ self.data += self._pack(self.produced, n)
317
+ self.produced += n
215
318
  return events
216
319
 
217
320
  def stop(self) -> list[bytes]:
@@ -265,3 +368,70 @@ class FakeCapture:
265
368
  del self.data[:self.sent - self.base] # pushed: the probe re-uses it
266
369
  self.base = self.sent
267
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/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.9
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
@@ -115,7 +115,11 @@ resumed after it).
115
115
  release) and streaming (data pushes while subscribed), level / edge triggers with a pretrigger, and events. What it
116
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
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.
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.
119
123
 
120
124
  Other programs' tests run `fake_serve` as a child process:
121
125
 
@@ -1,7 +1,7 @@
1
- oep_client/__init__.py,sha256=lMaMtVlHAJo6QrVhc8eg4Rho6TvB_sVCUg-_MvgWC0s,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,10 +10,10 @@ 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=Qp3oXoQ1K6WBzh7oEzeE72QD3I45oeZC0mgpHPQrPFY,83625
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=SXTQSySxTDWAmUBGMb3jEuaH-O23aT-42NzrwMWBgjw,15846
16
- oep_client/fake_capture.py,sha256=ZgRZm83ViJir5BD8s4nfG-6uc_UPoOuJuuh54vvQuAo,13789
15
+ oep_client/fake.py,sha256=jTaiii8DtByI_CyUYVRoUnGT05k5OdIerSdQig4HKrk,18096
16
+ oep_client/fake_capture.py,sha256=6BadaybnZ0jRDfxjnkLXttw65rkoPgsIY5FvAWKxMg8,22812
17
17
  oep_client/fake_serial.py,sha256=nXpbBuPl9sAh5BdoWSsa5EDYvPC7VLOvaBYrslGlniA,4659
18
18
  oep_client/fake_serve.py,sha256=CzJ_TO_gWVNPP0r5ggQ57rgwAgBapunGI0YtyW8MrRM,15133
19
19
  oep_client/fixture.py,sha256=653hCD-Iz0Lfs8dT4Hdg3QyubbbOGs1_xjq_3nb8foU,5564
@@ -24,14 +24,14 @@ oep_client/interfaces.py,sha256=TNvof5blyLjO1Mc0OentKcMVBD_5e4riRG18UgEh2ZE,5380
24
24
  oep_client/link.py,sha256=J9VtBWfTgNoIgUQuj8SsH9hHesQM2uEN545IoxKVS8E,19406
25
25
  oep_client/message.py,sha256=MEM1UT6KFM7AsT56jvBLt_FJK_6SJ7O_nGAiipW0_zo,9912
26
26
  oep_client/names.py,sha256=ztCZFWs__aR-a5G14TnJP-Q4YfLhbyrzvOVOMJuG93k,3653
27
- oep_client/registry.py,sha256=6FVVEs1b6krGygH21zma84mmMB_B65dh8S7PrjYrME0,10056
27
+ oep_client/registry.py,sha256=g6TfTWVPer9M46we6qSjMZqnqDbL6A49rdxM0v67Hx0,10758
28
28
  oep_client/riscv.py,sha256=H9kE_oUsnaBzqdTXDAVgDQkDvsrn5ZwsVi8dF1W_BDM,23117
29
29
  oep_client/rp2350.py,sha256=7OMyj270k3AY1e3-ljicPwOjf9jOlokbT6QNkA0b0yY,4227
30
30
  oep_client/target.py,sha256=R8BvYfFfYNhZF0MysJkNb8b69YgBlmeGXNX8ReZIe5k,1068
31
31
  oep_client/uiapduino.py,sha256=IEPG3LbZPksi8g3wmAf6T0CRWSfYKW1_qrH_GBVQdxY,6090
32
32
  oep_client/usb_stream.py,sha256=VJMCxsOlQCk005FcS92pqTh33fRSFBizoL__L0VbUMA,10242
33
- oep_client_python-0.0.9.dist-info/METADATA,sha256=VFcyTZyeXECe3xA8I1CRUnJwog_iXVZQIFM9QHHgcH0,8128
34
- oep_client_python-0.0.9.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
35
- oep_client_python-0.0.9.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
36
- oep_client_python-0.0.9.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
37
- oep_client_python-0.0.9.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,,