oep-client-python 0.0.7__py3-none-any.whl → 0.0.9__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.7"
5
+ __version__ = "0.0.9"
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"))
@@ -254,16 +254,29 @@ class Endpoint:
254
254
  self.pairs: dict[int, list[tuple[int, int]]] = {} # wire fn -> allowed (swdio, swclk), declared order
255
255
  self.max_connections: dict[int, int] = {}
256
256
  self.reset_channels: dict[int, set[int]] = {} # wire fn -> channels attach_under_reset may take (role 3)
257
+ self.pin_roles: dict[int, dict[int, set[int]]] = {} # wire fn -> role -> candidates (role_channels wires)
257
258
  for fn, name in self.names.items():
258
259
  if name in WIRES:
259
260
  self.pairs[fn] = [self._group_pair(name, t) for t in self.static[fn] if t[0] == catalog.CHANNEL_GROUP]
261
+ roles: dict[int, set[int]] = {}
262
+ for t in self.static[fn]:
263
+ if t[0] == catalog.ROLE_CHANNELS and t[2] in (1, 2):
264
+ roles.setdefault(t[2], set()).update(
265
+ catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]]))
266
+ if roles:
267
+ self.pin_roles[fn] = roles
260
268
  self.reset_channels[fn] = {
261
269
  c for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS and t[2] == PIN_ROLE_RESET
262
270
  for c in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
263
271
  self.max_connections[fn] = next((t[2] for t in self.static[fn] if t[0] == fake.MAX_CONNECTIONS), 1)
264
272
  self.targets: dict[tuple[int, tuple[int, int]], FakeTarget] = {
265
273
  (fn, p): FakeTarget() for fn in sorted(self.pairs) for p in self.pairs[fn]}
274
+ for fn in sorted(self.pin_roles): # any pair: one target, on the first pair
275
+ first = self._role_pairs(fn)[0]
276
+ self.targets[(fn, first)] = FakeTarget()
266
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() if name == "oep.fixture.capture"}
267
280
  self.mechanisms = set()
268
281
  for fn, name in self.names.items():
269
282
  if name == "oep.target.console":
@@ -291,6 +304,9 @@ class Endpoint:
291
304
  self.dropped = 0 # requests a v0 endpoint dropped (role 0x81)
292
305
  self.requests: list[m.Request] = []
293
306
  self.subscribed: set[int] = set()
307
+ self.push_seq: dict[int, int] = {} # fn -> the next event / data seq (core §11.2)
308
+ self.outbox: list[bytes] = [] # events and data frames waiting to go out (pushes())
309
+ self.capture_slipped = False # every capture segment says flags bit2 (a pace that fell behind)
294
310
  self.plan: set[tuple[int, int, int]] = set() # (fn, role, channel), from plan_apply and the config
295
311
  self.plan_from_config: set[int] = set() # fns whose plan came from the config (not a session's)
296
312
  self.resend: OrderedDict[int, tuple[int, int, int, bytes | None]] = OrderedDict()
@@ -329,6 +345,123 @@ class Endpoint:
329
345
  def target_id(self, value: int | None) -> None:
330
346
  self.target.target_id = value
331
347
 
348
+ @staticmethod
349
+ def _capture_from(tlvs: list[bytes]) -> fake_capture.FakeCapture:
350
+ """A capture as its describe declares it (modes, the w allowed, the rate range, max_read, the ring)."""
351
+ d = reg.FIXTURE_CAPTURE.tlv["describe"]
352
+ modes, widths, lo, hi, ring, most = set(), {8}, 1, 1_000_000, 1, 1024
353
+ for t in tlvs:
354
+ tag, v = t[0], t[2:2 + t[1]]
355
+ if tag == d["mode"]:
356
+ modes.add(v[0])
357
+ elif tag == d["channels"]:
358
+ widths = {1 << i for i in range(6) if v[1] >> i & 1}
359
+ elif tag == catalog.MIN_CLOCK_HZ:
360
+ lo = struct.unpack("<I", v)[0]
361
+ elif tag == catalog.MAX_CLOCK_HZ:
362
+ hi = struct.unpack("<I", v)[0]
363
+ elif tag == d["segment_ring"]:
364
+ ring = struct.unpack("<H", v)[0]
365
+ elif tag == d["max_read"]:
366
+ most = struct.unpack("<I", v)[0]
367
+ return fake_capture.FakeCapture(modes or {fake_capture.MODE["one_shot"]}, widths, lo, hi, ring, most)
368
+
369
+ def _next_seq(self, fn: int) -> int:
370
+ seq = self.push_seq.get(fn, 0)
371
+ self.push_seq[fn] = (seq + 1) & 0xFFFF
372
+ return seq
373
+
374
+ def _events(self, fn: int, events: list[bytes]) -> None:
375
+ """Events (kind(u8) payload) of `fn` to go out while it is subscribed; unsubscribed, they are not sent."""
376
+ for e in events:
377
+ if fn in self.subscribed:
378
+ self.outbox.append(bytes([m.ROLE_EVENT]) + struct.pack("<HH", fn, self._next_seq(fn)) + e)
379
+
380
+ def pushes(self) -> list[bytes]:
381
+ """The frames the probe sends by itself now (core §11): events, and a streaming capture's data. A serving loop
382
+ frames and sends them; a test takes them from here."""
383
+ self.tick()
384
+ for fn, cap in self.captures.items():
385
+ if fn in self.subscribed:
386
+ self.outbox += cap.pushes(fn, lambda fn=fn: self._next_seq(fn), self.probe.max_frame)
387
+ out, self.outbox = self.outbox, []
388
+ return out
389
+
390
+ def _capture(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
391
+ cap, O = self.captures[fn], fake_capture.OP
392
+ self._events(fn, cap.tick(self.now())) # what the clock captured up to this request
393
+ roles = sorted(a[1] for a in self.plan if a[0] == fn)
394
+ budget = self.probe.max_frame - m.RESULT_HEADER
395
+ try:
396
+ 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))
399
+ if op == O["configure"]:
400
+ cap.apply(settled)
401
+ cap.slipped = self.capture_slipped
402
+ return self._answer(cap.answer(settled), ignored)
403
+ if op == O["start"]:
404
+ t.tail()
405
+ self._events(fn, cap.start(self.now()))
406
+ return m.COMPLETED, m.SUCCESS, struct.pack("<I", 0)
407
+ if op == O["stop"]:
408
+ t.tail()
409
+ self._events(fn, cap.stop())
410
+ return m.COMPLETED, m.SUCCESS, b""
411
+ if op == O["force"]:
412
+ t.tail()
413
+ return m.COMPLETED, m.SUCCESS, b"" # nothing waits: a trigger is found at start
414
+ if op == O["status"]:
415
+ t.tail()
416
+ return m.COMPLETED, m.SUCCESS, cap.status()
417
+ if op == O["read"]:
418
+ position, most = t.take("QI")
419
+ return m.COMPLETED, m.SUCCESS, cap.read(position, most, budget)
420
+ if op == O["segments"]:
421
+ first = t.take("I")
422
+ t.tail()
423
+ return m.COMPLETED, m.SUCCESS, cap.segment_list(first, budget)
424
+ if op == O["release"]:
425
+ serial = t.take("I")
426
+ t.tail()
427
+ cap.release(serial, self.now())
428
+ return m.COMPLETED, m.SUCCESS, b""
429
+ except fake_capture.Reject as e:
430
+ raise Reject(e.reason, e.payload)
431
+ return m.REJECTED, m.UNKNOWN_OPERATION, b""
432
+
433
+ def _role_pairs(self, fn: int) -> list[tuple[int, int]]:
434
+ """A role_channels wire's pairs in the count = 0 order (oep-if-debug §1): swdio ascending, then swclk."""
435
+ roles = self.pin_roles[fn]
436
+ if self.names[fn] == "oep.wire.swio":
437
+ return [(d, 0xFFFF) for d in sorted(roles.get(1, ()))]
438
+ return [(d, c) for d in sorted(roles.get(1, ())) for c in sorted(roles.get(2, ())) if d != c]
439
+
440
+ def _allowed_pairs(self, fn: int) -> list[tuple[int, int]]:
441
+ return self._role_pairs(fn) if fn in self.pin_roles else self.pairs.get(fn, [])
442
+
443
+ def _allows(self, fn: int, pair: tuple[int, int]) -> bool:
444
+ if fn in self.pin_roles:
445
+ roles = self.pin_roles[fn]
446
+ swio = self.names[fn] == "oep.wire.swio"
447
+ return pair[0] in roles.get(1, ()) and (pair[1] == 0xFFFF if swio else
448
+ pair[1] in roles.get(2, ()) and pair[1] != pair[0])
449
+ return pair in self.pairs.get(fn, [])
450
+
451
+ def _held(self, fn: int | None = None, pair: tuple[int, int] | None = None) -> set[int]:
452
+ """Channels something holds (core §8.1): the plan, the slots' pairs, the live connections' pairs - except
453
+ wire `fn`'s own connection on `pair` (attaching there again, or scanning through it)."""
454
+ held = {a[2] for a in self.plan if a[0] not in self.captures} # a capture only listens
455
+ 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)}
456
+ 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)}
457
+ return held
458
+
459
+ def _target(self, fn: int, pair: tuple[int, int]) -> FakeTarget:
460
+ tg = self.targets.get((fn, pair))
461
+ if tg is None: # a pair nothing is wired to
462
+ tg = self.targets[(fn, pair)] = FakeTarget(present=False)
463
+ return tg
464
+
332
465
  @staticmethod
333
466
  def _group_pair(name: str, t: bytes) -> tuple[int, int]:
334
467
  roles = {t[3 + 3 * i]: struct.unpack_from("<H", t, 4 + 3 * i)[0] for i in range((t[1] - 1) // 3)}
@@ -570,6 +703,7 @@ class Endpoint:
570
703
  if fn != 0 and fn not in self.names:
571
704
  return m.REJECTED, m.UNAVAILABLE, b""
572
705
  self.subscribed.add(fn)
706
+ self.push_seq[fn] = 0 # seq from 0 at every subscribe (core §11.2)
573
707
  return m.COMPLETED, m.SUCCESS, b""
574
708
  if op == m.OP_UNSUBSCRIBE:
575
709
  fn = t.take("H")
@@ -601,13 +735,24 @@ class Endpoint:
601
735
  def _check_plan(self, got: list[tuple[int, int, int]]) -> None:
602
736
  """plan_apply's all-or-nothing check: the roles each fn has, and no pin another fn (or a slot) holds."""
603
737
  named = {fn for fn, _, _ in got}
604
- kept = {a for a in self.plan if a[0] not in named}
738
+ # a capture only listens: it shares pins with anything, and nothing is kept from a pin by it (core §8.1)
739
+ kept = {a for a in self.plan if a[0] not in named and a[0] not in self.captures}
605
740
  slot_pins = {p for s in self.slots.values() for p in s.pair if p != 0xFFFF}
741
+ slot_pins |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF} # a live connection's pins too
606
742
  for fn, role, ch in got:
743
+ if fn in self.captures:
744
+ if role not in self._declared_roles(fn, ch):
745
+ raise Reject(m.UNAVAILABLE)
746
+ continue
607
747
  roles = {"oep.fixture.gpio": {1}, "oep.fixture.uart": {1, 2}}.get(self.names.get(fn, ""), set())
608
748
  if role not in roles or any(k[2] == ch for k in kept) or ch in slot_pins:
609
749
  raise Reject(m.UNAVAILABLE)
610
750
 
751
+ def _declared_roles(self, fn: int, channel: int) -> set[int]:
752
+ """The roles fn's describe offers on `channel` (role_channels)."""
753
+ return {t[2] for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS
754
+ and channel in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
755
+
611
756
  def _drop_plan(self, fn: int) -> None:
612
757
  for a in [a for a in self.plan if a[0] == fn]:
613
758
  self.gpio_modes.pop(a[2], None)
@@ -646,17 +791,24 @@ class Endpoint:
646
791
 
647
792
  # ---- oep.wire.rvswd / swio ------------------------------------------------------------------
648
793
  def _wire(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
649
- allowed = self.pairs.get(fn, [])
650
794
  if op == 0x01: # scan: count(u8) pairs -> tried count found...
651
795
  count = t.take("B")
652
796
  pairs = [t.take("HH") for _ in range(count)]
653
- t.tail()
654
- if any(p not in allowed for p in pairs):
655
- raise Reject(m.UNAVAILABLE)
656
- pairs = pairs or allowed
797
+ got, _ = t.tail({0x01})
798
+ if 0x01 in got and (count or len(got[0x01]) != 2):
799
+ raise Reject(m.MALFORMED) # skip goes with count 0 only
800
+ live = [c.pair for c in self.conns.values() if c.fn == fn]
801
+ full = len(live) >= self.max_connections.get(fn, 1) # every seat taken: the live pairs only
802
+ if any(not self._allows(fn, p) or set(p) & self._held(fn, p) or (full and p not in live) for p in pairs):
803
+ raise Reject(m.UNAVAILABLE) # not allowed, held (§8.1), or no seat to try it on
804
+ if not pairs: # the count-0 list, from `skip` on (oep-if-debug §1)
805
+ skip = struct.unpack("<H", got[0x01])[0] if 0x01 in got else 0
806
+ pairs = [p for p in self._allowed_pairs(fn)
807
+ if not set(p) & self._held(fn, p) and (not full or p in live)][skip:]
808
+ pairs = pairs[:255] # tried is a u8
657
809
  found = []
658
810
  for p in pairs:
659
- tg = self.targets[(fn, p)]
811
+ tg = self._target(fn, p)
660
812
  if tg.present: # a live connection's pair: read over it, no restart
661
813
  found.append(struct.pack("<BHHI", 1, *p, tg.dmstatus()))
662
814
  return m.COMPLETED, m.SUCCESS, struct.pack("<BB", len(pairs), len(found)) + b"".join(found)
@@ -675,7 +827,7 @@ class Endpoint:
675
827
  raise Reject(m.MALFORMED)
676
828
  idle_clock = got[0x04][0] if 0x04 in got else 0
677
829
  pair = self._pick_pair(fn, got)
678
- tg = self.targets[(fn, pair)]
830
+ tg = self._target(fn, pair)
679
831
  speed = min(4_000_000, struct.unpack("<I", got[0x01])[0]) if 0x01 in got else 4_000_000
680
832
  cid = self._conn_at(fn, pair)
681
833
  flags = 0
@@ -729,15 +881,18 @@ class Endpoint:
729
881
  return m.REJECTED, m.UNKNOWN_OPERATION, b""
730
882
 
731
883
  def _pick_pair(self, fn: int, got: dict[int, bytes]) -> tuple[int, int]:
732
- allowed = self.pairs.get(fn, [])
733
884
  if 0x03 in got:
734
885
  if len(got[0x03]) != 4:
735
886
  raise Reject(m.MALFORMED)
736
887
  pair = struct.unpack("<HH", got[0x03])
737
- if pair not in allowed:
738
- raise Reject(m.UNAVAILABLE) # a pair this probe does not allow
888
+ if not self._allows(fn, pair) or set(pair) & self._held(fn, pair):
889
+ raise Reject(m.UNAVAILABLE) # not allowed here, or its pins are held (§8.1)
739
890
  return pair
740
- if len(allowed) != 1:
891
+ live = [c.pair for c in self.conns.values() if c.fn == fn]
892
+ if len(live) == 1:
893
+ return live[0] # no pins: the wire's one live connection
894
+ allowed = self._allowed_pairs(fn)
895
+ if live or len(allowed) != 1:
741
896
  raise Reject(m.UNAVAILABLE) # the host chooses among several
742
897
  return allowed[0]
743
898
 
@@ -779,7 +934,7 @@ class Endpoint:
779
934
 
780
935
  def _target_of(self, cid: int) -> FakeTarget:
781
936
  c = self.conns[cid]
782
- return self.targets[(c.fn, c.pair)]
937
+ return self._target(c.fn, c.pair)
783
938
 
784
939
  # ---- oep.target.riscv-dm --------------------------------------------------------------------
785
940
  @staticmethod
@@ -1181,7 +1336,7 @@ class Endpoint:
1181
1336
  places = [(s.wire_fn, s.pair) for s in slots.values()]
1182
1337
  if len(set(places)) != len(places):
1183
1338
  raise Reject(m.UNAVAILABLE)
1184
- for fn in self.pairs:
1339
+ for fn in self.pairs: # every wire (pin_roles wires have an empty list)
1185
1340
  if sum(1 for s in slots.values() if s.wire_fn == fn and s.attach == SLOT_ATTACH["at_boot"]) > \
1186
1341
  self.max_connections.get(fn, 1):
1187
1342
  raise Reject(m.UNAVAILABLE)
@@ -1244,7 +1399,7 @@ class Endpoint:
1244
1399
  raise Reject(m.MALFORMED)
1245
1400
  if not SLOT_NAME.fullmatch(name.decode("ascii", "replace")):
1246
1401
  raise Reject(m.MALFORMED)
1247
- if self.names.get(wire_fn) not in WIRES or (swdio, swclk) not in self.pairs.get(wire_fn, []):
1402
+ if self.names.get(wire_fn) not in WIRES or not self._allows(wire_fn, (swdio, swclk)):
1248
1403
  raise Reject(m.UNAVAILABLE)
1249
1404
  if mech not in self.mechanisms:
1250
1405
  raise Reject(m.UNSUPPORTED)
@@ -1301,7 +1456,7 @@ class Endpoint:
1301
1456
  s = self.slots[n]
1302
1457
  rt = self.slot_rt[n]
1303
1458
  rt.last_try_ms = self.now()
1304
- tg = self.targets[(s.wire_fn, s.pair)]
1459
+ tg = self._target(s.wire_fn, s.pair)
1305
1460
  cid = self._conn_at(s.wire_fn, s.pair)
1306
1461
  if cid is None:
1307
1462
  if not tg.present:
@@ -1348,9 +1503,11 @@ class Endpoint:
1348
1503
  self.flows[(port, key)] = Flow(sid, 0)
1349
1504
 
1350
1505
  def tick(self) -> None:
1351
- """Time passes: the lease, at-boot retries, mixed lines closed by quiet."""
1506
+ """Time passes: the lease, at-boot retries, mixed lines closed by quiet, the captures."""
1352
1507
  self._lapse()
1353
1508
  now = self.now()
1509
+ for fn, cap in self.captures.items():
1510
+ self._events(fn, cap.tick(now))
1354
1511
  for n, s in self.slots.items():
1355
1512
  rt = self.slot_rt[n]
1356
1513
  if (s.attach == SLOT_ATTACH["at_boot"] and s.retry_s and not rt.evicted
@@ -1498,4 +1655,4 @@ class Endpoint:
1498
1655
 
1499
1656
  SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
1500
1657
  "oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
1501
- "oep.probe.config": "config_op"}
1658
+ "oep.probe.config": "config_op", "oep.fixture.capture": "capture"}
oep_client/fake.py CHANGED
@@ -108,6 +108,21 @@ 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
+ _CAPM = reg.FIXTURE_CAPTURE.enum["mode"]
113
+
114
+
115
+ def _capture_decl(modes: list[str], max_channels: int, widths: list[int], max_samples: int, ring: int,
116
+ max_read: int) -> tuple[bytes, ...]:
117
+ """oep.fixture.capture's declarations (oep-if-capture §3.5): a mode entry per mode, channels (max, the w allowed as
118
+ bits: bit i = w 2^i), the trigger types (immediate, level, edge) with the most pretrigger, max_read, segment_ring."""
119
+ out = tuple(catalog.tlv(_CAPD["mode"], struct.pack("<BBII", _CAPM[k], 1, max_samples, ring if k == "repeat" else 1))
120
+ for k in modes)
121
+ bits = sum(1 << (w.bit_length() - 1) for w in widths)
122
+ return out + (catalog.tlv(_CAPD["channels"], bytes([max_channels, bits])),
123
+ catalog.tlv(_CAPD["trigger"], struct.pack("<BI", 0b111, max_samples - 1)),
124
+ catalog.u32(_CAPD["max_read"], max_read), catalog.u16(_CAPD["segment_ring"], ring),
125
+ catalog.u32(FEATURES, 0b001))
111
126
  MAX_CONNECTIONS = reg.WIRE_RVSWD.tlv["describe"]["max_connections"]
112
127
  _CFG = reg.PROBE_CONFIG.tlv["describe"]
113
128
 
@@ -139,7 +154,8 @@ def _core(firmware: str, model: str, unit_id: bytes, channels: int, reserved: li
139
154
  return Offered(0, 0, "oep.core", (
140
155
  catalog.text(CORE_FIRMWARE, firmware), catalog.text(CORE_MODEL, model), catalog.tlv(CORE_UNIT_ID, unit_id),
141
156
  catalog.u16(CORE_CHANNELS, channels), catalog.tlv(CORE_RESERVED, struct.pack("<H", base) + bits),
142
- catalog.text(CORE_PROFILE, profile)) + tuple(_label(c, n) for c, n in labels.items()) + extra)
157
+ ) + ((catalog.text(CORE_PROFILE, profile),) if profile else ()) + tuple(_label(c, n) for c, n in labels.items())
158
+ + extra)
143
159
 
144
160
 
145
161
  def p4_x035() -> FakeProbe:
@@ -164,9 +180,11 @@ def p4_x035() -> FakeProbe:
164
180
  catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
165
181
  Offered(6, 4, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
166
182
  catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
167
- Offered(7, 5, "oep.fixture.capture", _roles({k: pins for k in range(8)}) + (
183
+ # the P4's PARLIO: w 1-16, one-shot / repeat / streaming
184
+ Offered(7, 5, "oep.fixture.capture", _roles({k: pins for k in range(16)}) + (
168
185
  catalog.u32(MAX_CLOCK_HZ, 20_000_000), catalog.u32(MIN_CLOCK_HZ, 1_000),
169
- catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))),
186
+ catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))
187
+ + _capture_decl(["one_shot", "repeat", "streaming"], 16, [1, 2, 4, 8, 16], 1 << 20, 8, 4096)),
170
188
  Offered(8, 6, f"{NS}.esp32.i2c-target", _roles({1: pins, 2: pins}) + (
171
189
  catalog.u16(MAX_LENGTH, 128), catalog.u32(MAX_CLOCK_HZ, 1_000_000),
172
190
  catalog.u32(FEATURES, 0b11), catalog.u8(IMPLEMENTATION, 2))),
@@ -193,8 +211,10 @@ def esp32_v003() -> FakeProbe:
193
211
  Offered(4, 2, "oep.fixture.gpio", _roles({1: wired + [23]})),
194
212
  Offered(5, 3, "oep.fixture.uart", _roles({1: wired, 2: wired}) + (
195
213
  catalog.u32(MAX_CLOCK_HZ, 115_200), catalog.u8(IMPLEMENTATION, 2))),
214
+ # the classic ESP32's GPIO sampler: a byte a sample (w 8), one-shot
196
215
  Offered(6, 4, "oep.fixture.capture", _roles({k: wired for k in range(4)}) + (
197
- catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))),
216
+ catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))
217
+ + _capture_decl(["one_shot"], 8, [8], 65536, 1, 480)),
198
218
  Offered(7, 5, f"{NS}.esp32.i2c-target", _roles({1: wired, 2: wired}) + (
199
219
  catalog.u16(MAX_LENGTH, 16), catalog.u32(MAX_CLOCK_HZ, 100_000), catalog.u8(IMPLEMENTATION, 2))),
200
220
  # Two fixed pin sets (an example of channel_group; GPIO23 is the DUT's NRST on this jig).
@@ -230,4 +250,24 @@ def p4_bench() -> FakeProbe:
230
250
  ])
231
251
 
232
252
 
233
- PROFILES = {"p4-x035": p4_x035, "esp32-v003": esp32_v003, "p4-bench": p4_bench}
253
+ def rp2350_pins() -> FakeProbe:
254
+ """A board firmware whose wire takes its pins from the host (oep-if-debug §1, role_channels): any two of GP0-GP29
255
+ but GP19 (the Pro Micro RP2350's PSRAM CS) are an RVSWD pair, a reset line or fixture pins. USB CDC (serial port 0).
256
+ The fake target answers on (0, 1), the first pair."""
257
+ reserved = [19]
258
+ pins = [p for p in range(30) if p not in reserved]
259
+ return FakeProbe("rp2350-pins", 1024, [
260
+ _core("3.0.0", "sparkfun-promicro-rp2350", bytes.fromhex("e66138935f2b1f2c"), 30, reserved, "",
261
+ {}, _transports([(TRANSPORT["usb_cdc"], 0)])),
262
+ Offered(1, 1, "oep.wire.rvswd", _roles({1: pins, 2: pins, 3: pins}) + (
263
+ catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
264
+ Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
265
+ catalog.u16(MAX_LENGTH, 1000))),
266
+ Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
267
+ Offered(4, 2, "oep.fixture.gpio", _roles({1: pins})),
268
+ Offered(5, 3, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
269
+ catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
270
+ ])
271
+
272
+
273
+ PROFILES = {"p4-x035": p4_x035, "esp32-v003": esp32_v003, "p4-bench": p4_bench, "rp2350-pins": rp2350_pins}
@@ -0,0 +1,267 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """oep.fixture.capture in the fake probe (oep-spec docs/oep-if-capture.ja.md, revision 1, logic).
3
+
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.
7
+
8
+ Modes: one-shot (the segment is there as soon as start answers), repeat (segments come with the clock at the actual
9
+ 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).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import struct
17
+ from dataclasses import dataclass, field
18
+ from fractions import Fraction
19
+
20
+ from . import message as m, registry as reg
21
+
22
+ CAP = reg.FIXTURE_CAPTURE
23
+ OP = CAP.op
24
+ TLV, ANSWER = CAP.tlv["configure"], CAP.tlv["configure_answer"]
25
+ MODE, STATE, TRIGGER = CAP.enum["mode"], CAP.enum["state"], CAP.enum["trigger"]
26
+ STOPPED, FLAG = CAP.enum["stopped_reason"], CAP.enum["segment_flag"]
27
+ EVENT = CAP.event
28
+ NONE = 0xFFFFFFFF
29
+ 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)
31
+
32
+
33
+ class Reject(Exception):
34
+ def __init__(self, reason: int, payload: bytes = b""):
35
+ self.reason, self.payload = reason, payload
36
+
37
+
38
+ @dataclass
39
+ class Segment:
40
+ serial: int
41
+ position: int
42
+ samples: int
43
+ start_us: int
44
+ trigger_index: int = NONE
45
+ flags: int = 0
46
+
47
+ def pack(self) -> bytes:
48
+ return SEGMENT.pack(self.serial, self.position, self.samples, self.start_us, self.trigger_index, self.flags)
49
+
50
+
51
+ @dataclass
52
+ class FakeCapture:
53
+ """One capture interface. `now_ms` is the endpoint's clock; `roles()` the channels the plan gives it (role order)."""
54
+ modes: set[int] # the modes the describe declares
55
+ widths: set[int] # the w the describe allows
56
+ min_hz: int
57
+ max_hz: int
58
+ ring: int = 8 # segment_ring
59
+ max_read: int = 4096
60
+ slipped: bool = False
61
+ state: int = STATE["unconfigured"]
62
+ mode: int = MODE["one_shot"]
63
+ rate: Fraction = Fraction(0)
64
+ samples: int = 0
65
+ segments_max: int = 0
66
+ trigger: tuple[int, int, int] | None = None
67
+ pretrigger: int = 0
68
+ width: int = 0
69
+ 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
72
+ segs: list[Segment] = field(default_factory=list)
73
+ serial_done: int = 0
74
+ started_ms: int = 0
75
+ produced: int = 0 # samples produced since start (repeat / streaming)
76
+ 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
+ gap_next: bool = False # repeat: the capture stopped for want of a segment; the next one says so
79
+
80
+ # ---- 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)."""
83
+ mode = got[TLV["mode"]][0] if TLV["mode"] in got else MODE["one_shot"]
84
+ if mode not in self.modes:
85
+ raise Reject(m.UNSUPPORTED, bytes([TLV["mode"] | (m.TAG_CRITICAL if TLV["mode"] in critical else 0)]))
86
+ if TLV["rate"] not in got or len(got[TLV["rate"]]) != 4:
87
+ raise Reject(m.MALFORMED)
88
+ 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)))
93
+ rate = Fraction(self.max_hz, div)
94
+ samples = struct.unpack("<I", got[TLV["samples"]])[0] if TLV["samples"] in got else 4096
95
+ samples = max(1, min(samples, MAX_SAMPLES))
96
+ segs = struct.unpack("<I", got[TLV["segments"]])[0] if TLV["segments"] in got else self.ring
97
+ segs = max(1, min(segs, self.ring)) if mode == MODE["repeat"] else 1
98
+ trigger = None
99
+ if TLV["trigger"] in got:
100
+ 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):
102
+ raise Reject(m.UNSUPPORTED, bytes([TLV["trigger"] | m.TAG_CRITICAL]))
103
+ trigger = (kind, role, value) if kind else None
104
+ 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
110
+ return {"mode": mode, "rate": rate, "samples": samples, "segments": segs, "trigger": trigger,
111
+ "pretrigger": min(pre, samples - 1), "width": width, "channels": channels}
112
+
113
+ def apply(self, s: dict) -> None:
114
+ self.mode, self.rate, self.samples, self.segments_max = s["mode"], s["rate"], s["samples"], s["segments"]
115
+ self.trigger, self.pretrigger, self.width, self.channels = s["trigger"], s["pretrigger"], s["width"], s["channels"]
116
+ self.state = STATE["configured"]
117
+ self._clear()
118
+
119
+ @staticmethod
120
+ def answer(s: dict) -> bytes:
121
+ def tlv(tag: int, v: bytes) -> bytes:
122
+ 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"]))
126
+ + tlv(ANSWER["actual_segments"], struct.pack("<I", s["segments"]))
127
+ + tlv(ANSWER["timing"], struct.pack("<BI", 1 if s["rate"].denominator != 1 else 0, 0))
128
+ + tlv(ANSWER["blocking_ms"], struct.pack("<I", 0)))
129
+
130
+ # ---- the data ---------------------------------------------------------------------------------------------------
131
+ def _clear(self) -> None:
132
+ 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
134
+
135
+ 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
138
+ 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))
142
+ out = bytearray((n * w + 7) // 8)
143
+ mask = (1 << w) - 1
144
+ for i in range(n):
145
+ bit = i * w
146
+ out[bit >> 3] |= ((first + i) & mask) << (bit & 7)
147
+ return bytes(out)
148
+
149
+ def _trigger_at(self) -> int:
150
+ """The first sample, at or after the pretrigger, where the trigger holds (the counter's bit `role`)."""
151
+ 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
156
+ if kind == TRIGGER["level"] and now == (value & 1):
157
+ 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:
166
+ if self.gap_next:
167
+ 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)
171
+ self.produced += n
172
+ self.segs.append(seg)
173
+ self.serial_done += 1
174
+ return seg
175
+
176
+ # ---- 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."""
179
+ if self.state == STATE["unconfigured"]:
180
+ raise Reject(m.UNAVAILABLE)
181
+ self._clear()
182
+ self.started_ms = now_ms
183
+ events = []
184
+ 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)
189
+ self.state = STATE["done"]
190
+ events += [bytes([EVENT["segment"]]) + seg.pack(), bytes([EVENT["stopped"], STOPPED["complete"]])]
191
+ else:
192
+ self.state = STATE["capturing"]
193
+ return events
194
+
195
+ def tick(self, now_ms: int) -> list[bytes]:
196
+ """Time passes: repeat's segments, streaming's bytes. -> events."""
197
+ if self.state != STATE["capturing"]:
198
+ return []
199
+ due = int((now_ms - self.started_ms) * self.rate / 1000) # samples the clock has taken since start
200
+ events = []
201
+ if self.mode == MODE["repeat"]:
202
+ while due - self.produced >= self.samples:
203
+ if len(self.segs) >= self.segments_max: # no free segment: the capture stops
204
+ self.state = STATE["paused"]
205
+ events.append(bytes([EVENT["stopped"], STOPPED["no_free_segment"]]))
206
+ break
207
+ seg = self._segment(self.samples, now_ms)
208
+ events.append(bytes([EVENT["segment"]]) + seg.pack())
209
+ 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
212
+ if n > 0:
213
+ self.data += self._pack(self.first_sample + self.produced, min(n, MAX_SAMPLES))
214
+ self.produced += min(n, MAX_SAMPLES)
215
+ return events
216
+
217
+ def stop(self) -> list[bytes]:
218
+ if self.state not in (STATE["capturing"], STATE["waiting"], STATE["paused"]):
219
+ return []
220
+ self.state = STATE["done"]
221
+ return [bytes([EVENT["stopped"], STOPPED["host"]])]
222
+
223
+ def status(self) -> bytes:
224
+ return struct.pack("<BIQB", self.state, self.serial_done, self.base + len(self.data), 0)
225
+
226
+ def read(self, position: int, most: int, budget: int) -> bytes:
227
+ flags = 0
228
+ if position < self.base:
229
+ position, flags = self.base, flags | 0x02 # re-used: from what is kept (gap)
230
+ end = self.base + len(self.data)
231
+ take = max(0, min(most, self.max_read, budget - 9, end - position))
232
+ data = bytes(self.data[position - self.base:position - self.base + take])
233
+ if position + take < end:
234
+ flags |= 0x01 # more
235
+ return struct.pack("<QB", position, flags) + data
236
+
237
+ def segment_list(self, first: int, budget: int) -> bytes:
238
+ out = [s.pack() for s in self.segs if s.serial >= first]
239
+ out = out[:max(0, (budget - 1) // SEGMENT.size)][:255]
240
+ return bytes([len(out)]) + b"".join(out)
241
+
242
+ def release(self, serial: int, now_ms: int) -> None:
243
+ if self.mode != MODE["repeat"]:
244
+ return # one-shot: the next start clears; streaming: pushed
245
+ keep = [s for s in self.segs if s.serial > serial]
246
+ drop_to = keep[0].position if keep else self.base + len(self.data)
247
+ del self.data[:drop_to - self.base]
248
+ self.base = drop_to
249
+ self.segs = keep
250
+ if self.state == STATE["paused"]: # it goes on from now; the gap shows (flags bit0)
251
+ self.state, self.gap_next = STATE["capturing"], True
252
+ self.started_ms = now_ms - int(self.produced * 1000 / self.rate)
253
+
254
+ def pushes(self, fn: int, next_seq, budget: int) -> list[bytes]:
255
+ """Streaming: the bytes not pushed yet, as data frames (position(u64) then data, common §1.5)."""
256
+ if self.mode != MODE["streaming"]:
257
+ return []
258
+ out = []
259
+ end = self.base + len(self.data)
260
+ while self.sent < end:
261
+ n = min(end - self.sent, budget - 13)
262
+ chunk = bytes(self.data[self.sent - self.base:self.sent - self.base + n])
263
+ out.append(bytes([m.ROLE_DATA]) + struct.pack("<HHQ", fn, next_seq(), self.sent) + chunk)
264
+ self.sent += n
265
+ del self.data[:self.sent - self.base] # pushed: the probe re-uses it
266
+ self.base = self.sent
267
+ return out
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 = "dd6d527421bb6206"
5
+ REGISTRY_HASH = "08b7d1656bf59269"
6
6
  SCHEMA = 1
7
7
  PROTOCOL_REVISION = 1
8
8
  CONFIRM_REQUEST_MAGIC = 'OEP?'
@@ -25,13 +25,13 @@ CORE = _NS(name="oep.core", revision=1, op={"confirm": 0x01, "list": 0x02, "desc
25
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={})
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
- closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "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={})
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={})
29
29
  INTERFACES["oep.wire.rvswd"] = WIRE_RVSWD
30
30
  WIRE_SWIO = _NS(name="oep.wire.swio", revision=1, op={"scan": 0x01, "attach": 0x02, "detach": 0x03, "attach_under_reset": 0x04, "connections": 0x05}, lock_free={0x05},
31
- closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03}, "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, "reset": 0x03}}, own={})
31
+ closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "scan": {"skip": 0x01}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03}, "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, "reset": 0x03}}, own={})
32
32
  INTERFACES["oep.wire.swio"] = WIRE_SWIO
33
33
  WIRE_SWD = _NS(name="oep.wire.swd", revision=1, op={"scan": 0x01, "attach": 0x02, "detach": 0x03, "connections": 0x05}, lock_free={0x05},
34
- closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "targetsel": 0x02, "pins": 0x03}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "pin_role": {"swdio": 0x01, "swclk": 0x02}}, own={})
34
+ closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "scan": {"skip": 0x01}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "targetsel": 0x02, "pins": 0x03}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "pin_role": {"swdio": 0x01, "swclk": 0x02}}, own={})
35
35
  INTERFACES["oep.wire.swd"] = WIRE_SWD
36
36
  TARGET_RISCV_DM = _NS(name="oep.target.riscv-dm", revision=1, op={"dmi": 0x01, "halt": 0x02, "resume": 0x03, "reset": 0x04, "read_block": 0x05, "write_block": 0x06, "run": 0x07, "step": 0x08}, lock_free={},
37
37
  closed_tail={}, tlv={"describe": {}, "reset": {"method": 0x01}}, event={}, enum={"dmi_step": {"write": 0x01, "read": 0x02, "poll_reads": 0x03, "wait_us": 0x04, "poll_us": 0x05}, "reset_mode": {"run": 0x00, "run_verified": 0x01, "halt_at_reset": 0x02}, "reset_method": {"probe_default": 0x00, "ndmreset": 0x01, "system_reset": 0x02}}, own={})
oep_client/riscv.py CHANGED
@@ -76,23 +76,36 @@ class WireBase(Interface):
76
76
  TAG_MAX_SPEED = 0x01
77
77
  TAG_PINS = 0x03 # swdio(u16) swclk(u16, 0xFFFF on one wire), critical (oep-if-debug §1)
78
78
 
79
+ TAG_SKIP = 0x01 # scan, count 0 only: pairs of the count-0 list to skip (oep-if-debug §1)
80
+
79
81
  def scan(self, pairs: list[tuple[int, int]] | None = None) -> list[Found]:
80
- """Try `pairs` of (swdio, swclk); None = every pair the probe allows (describe's channel_group /
81
- role_channels). A pair the probe does not allow refuses the whole scan (rejected unavailable). The probe stops
82
- when its answer would not fit one frame and says how many pairs it tried; the rest go again (oep-if-debug §1)."""
83
- pairs = list(pairs or [])
82
+ """Try `pairs` of (swdio, swclk); None = every pair the probe allows and nothing holds (describe's
83
+ channel_group / role_channels, oep-if-debug §1). A pair the probe does not allow, or one whose pins something
84
+ holds, refuses the whole scan (rejected unavailable). The probe tries at most 255 pairs a request and stops early
85
+ when its answer would not fit one frame; this goes on until every pair is tried (count 0: with skip until
86
+ tried = 0)."""
84
87
  out = []
88
+ pairs = list(pairs or [])
89
+ skip = 0
85
90
  while True:
86
- body = bytes([len(pairs)]) + b"".join(struct.pack("<HH", d, c) for d, c in pairs)
91
+ chunk = pairs[:255]
92
+ body = bytes([len(chunk)]) + b"".join(struct.pack("<HH", d, c) for d, c in chunk)
93
+ if not pairs and skip:
94
+ body += m.tlv(self.TAG_SKIP, struct.pack("<H", skip))
87
95
  rd = m.Reader(self._call(self.SCAN, body).payload)
88
96
  tried, count = rd.take("BB")
89
97
  for _ in range(count):
90
98
  kind, dio, clk, status = rd.take("BHHI")
91
99
  out.append(Found(kind, (dio, clk), status))
92
100
  rd.tail()
93
- if not pairs or tried >= len(pairs) or tried == 0:
101
+ if tried == 0:
94
102
  return out
95
- pairs = pairs[tried:]
103
+ if pairs:
104
+ pairs = pairs[tried:]
105
+ if not pairs:
106
+ return out
107
+ else:
108
+ skip += tried
96
109
 
97
110
  def detach(self, conn: int) -> None:
98
111
  self._call(self.DETACH, struct.pack("<H", conn))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.7
3
+ Version: 0.0.9
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,15 @@ 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.
112
119
 
113
120
  Other programs' tests run `fake_serve` as a child process:
114
121
 
@@ -122,4 +129,5 @@ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--f
122
129
  `--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
123
130
  so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
124
131
  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`.
132
+ riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. Events and data pushes go out on the pty and on TCP
133
+ (both framings). The rest: `--help`.
@@ -1,4 +1,4 @@
1
- oep_client/__init__.py,sha256=Eo4zAyMLP7Kn4ZVfKG_wGeLWVvEnMO3lQdewP4ORx6k,163
1
+ oep_client/__init__.py,sha256=lMaMtVlHAJo6QrVhc8eg4Rho6TvB_sVCUg-_MvgWC0s,163
2
2
  oep_client/__main__.py,sha256=qtVGdpAHULBtLY2SedabJf-uwDZZhMf_YmrbqRzM57U,14669
3
3
  oep_client/arm.py,sha256=JLyUhLSPcmsk73schZcaJYoOKuHzrvnU8kPmfrEvVuo,12331
4
4
  oep_client/capture.py,sha256=hs8mQU1gE1P67GOivxw2C2Ur8wimWqCdhtk1nqGN71Q,18092
@@ -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=tRx9ZUIiFN0J9NS3OwZ89NorIV7XqmZbYeUMnvJb6bE,74082
13
+ oep_client/endpoint.py,sha256=Qp3oXoQ1K6WBzh7oEzeE72QD3I45oeZC0mgpHPQrPFY,83625
14
14
  oep_client/esp32_targets.py,sha256=b8tNH-nb2KC-XgNfWg5l3TO0cyhZOF-gJajaVxNVSGQ,4988
15
- oep_client/fake.py,sha256=TJpzwSCFDSco-3HCE2ZHzusMx0r0dtrD1bkGn6aos90,13136
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=SXTQSySxTDWAmUBGMb3jEuaH-O23aT-42NzrwMWBgjw,15846
16
+ oep_client/fake_capture.py,sha256=ZgRZm83ViJir5BD8s4nfG-6uc_UPoOuJuuh54vvQuAo,13789
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=uO9Ilx_pPPhLD-17TosgXH9QfI11DymYNIR-EacaZOw,9984
27
- oep_client/riscv.py,sha256=PNeo_HXlq08b4h083olOOrGxqejD6ggQ7QzVqkIHEMA,22621
27
+ oep_client/registry.py,sha256=6FVVEs1b6krGygH21zma84mmMB_B65dh8S7PrjYrME0,10056
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.7.dist-info/METADATA,sha256=Shg-dYEbV4747Sl1HZlRz-AxoIdVQ2Z9LzszmjCTdnY,7368
33
- oep_client_python-0.0.7.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
34
- oep_client_python-0.0.7.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
35
- oep_client_python-0.0.7.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
36
- oep_client_python-0.0.7.dist-info/RECORD,,
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,,