oep-client-python 0.0.7__tar.gz → 0.0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/CHANGELOG.md +4 -0
  2. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/PKG-INFO +1 -1
  3. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/pyproject.toml +1 -1
  4. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/__init__.py +1 -1
  5. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/endpoint.py +65 -14
  6. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/fake.py +23 -2
  7. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/registry.py +4 -4
  8. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/riscv.py +20 -7
  9. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_fake_spec.py +42 -0
  10. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/.gitignore +0 -0
  11. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/LICENSE +0 -0
  12. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/README.ja.md +0 -0
  13. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/README.md +0 -0
  14. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/__main__.py +0 -0
  15. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/arm.py +0 -0
  16. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/capture.py +0 -0
  17. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/catalog.py +0 -0
  18. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/ch32_flash.py +0 -0
  19. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/cobs.py +0 -0
  20. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/config.py +0 -0
  21. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/console.py +0 -0
  22. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/core.py +0 -0
  23. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/decode.py +0 -0
  24. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/dump.py +0 -0
  25. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/esp32_targets.py +0 -0
  26. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/fake_serial.py +0 -0
  27. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/fake_serve.py +0 -0
  28. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/fixture.py +0 -0
  29. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/frames.py +0 -0
  30. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/hid_stream.py +0 -0
  31. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/host.py +0 -0
  32. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/interfaces.py +0 -0
  33. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/link.py +0 -0
  34. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/message.py +0 -0
  35. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/names.py +0 -0
  36. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/rp2350.py +0 -0
  37. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/target.py +0 -0
  38. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/uiapduino.py +0 -0
  39. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/src/oep_client/usb_stream.py +0 -0
  40. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_capabilities.py +0 -0
  41. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_capture.py +0 -0
  42. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_cobs.py +0 -0
  43. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_config.py +0 -0
  44. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_esp32_targets.py +0 -0
  45. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_flash_console.py +0 -0
  46. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_hid_stream.py +0 -0
  47. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_interfaces.py +0 -0
  48. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_link_host.py +0 -0
  49. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_link_serial.py +0 -0
  50. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_session.py +0 -0
  51. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_target_parts.py +0 -0
  52. {oep_client_python-0.0.7 → oep_client_python-0.0.8}/tests/test_usb_stream.py +0 -0
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.8
6
+ - (EN) Wires whose pins the host chooses (oep-if-debug §1, role_channels): the fake takes any allowed free pair, skips the pairs whose pins something holds in a count-0 scan, holds a live connection's pins against plans, and has a profile for it (`rp2350-pins`). `Wire.scan()` goes through the whole list (at most 255 pairs a request; count 0 continues with the new skip TLV until tried = 0).
7
+ - (JA) host がピンを選ぶ wire(oep-if-debug §1、role_channels): 偽の probe は、許された空いている組ならどれでも受け、count = 0 の scan では何かが持っているピンの組を飛ばし、生きている接続のピンを plan から守る。そのための profile(`rp2350-pins`)を足した。`Wire.scan()` は並びを最後まで回る(1 回 255 組まで。count = 0 は新しい skip の TLV で tried = 0 まで続ける)。
8
+
5
9
  ## 0.0.7
6
10
  - (EN) `oep dump` names the wires' reset role (role 3) and no longer lists riscv-dm's clobbers (gone: the probe puts the registers back itself, oep-if-debug §4.5).
7
11
  - (JA) `oep dump` は wire の reset の役(role 3)を名前で出し、riscv-dm の clobbers を出さなくなった(無くなった: probe が自分でレジスタを戻す、oep-if-debug §4.5)。
@@ -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.8
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oep-client-python"
7
- version = "0.0.7"
7
+ version = "0.0.8"
8
8
  description = "Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.7"
5
+ __version__ = "0.0.8"
@@ -254,15 +254,26 @@ 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())
267
278
  self.mechanisms = set()
268
279
  for fn, name in self.names.items():
@@ -329,6 +340,38 @@ class Endpoint:
329
340
  def target_id(self, value: int | None) -> None:
330
341
  self.target.target_id = value
331
342
 
343
+ def _role_pairs(self, fn: int) -> list[tuple[int, int]]:
344
+ """A role_channels wire's pairs in the count = 0 order (oep-if-debug §1): swdio ascending, then swclk."""
345
+ roles = self.pin_roles[fn]
346
+ if self.names[fn] == "oep.wire.swio":
347
+ return [(d, 0xFFFF) for d in sorted(roles.get(1, ()))]
348
+ return [(d, c) for d in sorted(roles.get(1, ())) for c in sorted(roles.get(2, ())) if d != c]
349
+
350
+ def _allowed_pairs(self, fn: int) -> list[tuple[int, int]]:
351
+ return self._role_pairs(fn) if fn in self.pin_roles else self.pairs.get(fn, [])
352
+
353
+ def _allows(self, fn: int, pair: tuple[int, int]) -> bool:
354
+ if fn in self.pin_roles:
355
+ roles = self.pin_roles[fn]
356
+ swio = self.names[fn] == "oep.wire.swio"
357
+ return pair[0] in roles.get(1, ()) and (pair[1] == 0xFFFF if swio else
358
+ pair[1] in roles.get(2, ()) and pair[1] != pair[0])
359
+ return pair in self.pairs.get(fn, [])
360
+
361
+ def _held(self, fn: int | None = None, pair: tuple[int, int] | None = None) -> set[int]:
362
+ """Channels something holds (core §8.1): the plan, the slots' pairs, the live connections' pairs - except
363
+ wire `fn`'s own connection on `pair` (attaching there again, or scanning through it)."""
364
+ held = {a[2] for a in self.plan}
365
+ held |= {p for s in self.slots.values() for p in s.pair if p != 0xFFFF and not (s.wire_fn == fn and s.pair == pair)}
366
+ held |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF and not (c.fn == fn and c.pair == pair)}
367
+ return held
368
+
369
+ def _target(self, fn: int, pair: tuple[int, int]) -> FakeTarget:
370
+ tg = self.targets.get((fn, pair))
371
+ if tg is None: # a pair nothing is wired to
372
+ tg = self.targets[(fn, pair)] = FakeTarget(present=False)
373
+ return tg
374
+
332
375
  @staticmethod
333
376
  def _group_pair(name: str, t: bytes) -> tuple[int, int]:
334
377
  roles = {t[3 + 3 * i]: struct.unpack_from("<H", t, 4 + 3 * i)[0] for i in range((t[1] - 1) // 3)}
@@ -603,6 +646,7 @@ class Endpoint:
603
646
  named = {fn for fn, _, _ in got}
604
647
  kept = {a for a in self.plan if a[0] not in named}
605
648
  slot_pins = {p for s in self.slots.values() for p in s.pair if p != 0xFFFF}
649
+ slot_pins |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF} # a live connection's pins too
606
650
  for fn, role, ch in got:
607
651
  roles = {"oep.fixture.gpio": {1}, "oep.fixture.uart": {1, 2}}.get(self.names.get(fn, ""), set())
608
652
  if role not in roles or any(k[2] == ch for k in kept) or ch in slot_pins:
@@ -646,17 +690,24 @@ class Endpoint:
646
690
 
647
691
  # ---- oep.wire.rvswd / swio ------------------------------------------------------------------
648
692
  def _wire(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
649
- allowed = self.pairs.get(fn, [])
650
693
  if op == 0x01: # scan: count(u8) pairs -> tried count found...
651
694
  count = t.take("B")
652
695
  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
696
+ got, _ = t.tail({0x01})
697
+ if 0x01 in got and (count or len(got[0x01]) != 2):
698
+ raise Reject(m.MALFORMED) # skip goes with count 0 only
699
+ live = [c.pair for c in self.conns.values() if c.fn == fn]
700
+ full = len(live) >= self.max_connections.get(fn, 1) # every seat taken: the live pairs only
701
+ 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):
702
+ raise Reject(m.UNAVAILABLE) # not allowed, held (§8.1), or no seat to try it on
703
+ if not pairs: # the count-0 list, from `skip` on (oep-if-debug §1)
704
+ skip = struct.unpack("<H", got[0x01])[0] if 0x01 in got else 0
705
+ pairs = [p for p in self._allowed_pairs(fn)
706
+ if not set(p) & self._held(fn, p) and (not full or p in live)][skip:]
707
+ pairs = pairs[:255] # tried is a u8
657
708
  found = []
658
709
  for p in pairs:
659
- tg = self.targets[(fn, p)]
710
+ tg = self._target(fn, p)
660
711
  if tg.present: # a live connection's pair: read over it, no restart
661
712
  found.append(struct.pack("<BHHI", 1, *p, tg.dmstatus()))
662
713
  return m.COMPLETED, m.SUCCESS, struct.pack("<BB", len(pairs), len(found)) + b"".join(found)
@@ -675,7 +726,7 @@ class Endpoint:
675
726
  raise Reject(m.MALFORMED)
676
727
  idle_clock = got[0x04][0] if 0x04 in got else 0
677
728
  pair = self._pick_pair(fn, got)
678
- tg = self.targets[(fn, pair)]
729
+ tg = self._target(fn, pair)
679
730
  speed = min(4_000_000, struct.unpack("<I", got[0x01])[0]) if 0x01 in got else 4_000_000
680
731
  cid = self._conn_at(fn, pair)
681
732
  flags = 0
@@ -729,14 +780,14 @@ class Endpoint:
729
780
  return m.REJECTED, m.UNKNOWN_OPERATION, b""
730
781
 
731
782
  def _pick_pair(self, fn: int, got: dict[int, bytes]) -> tuple[int, int]:
732
- allowed = self.pairs.get(fn, [])
733
783
  if 0x03 in got:
734
784
  if len(got[0x03]) != 4:
735
785
  raise Reject(m.MALFORMED)
736
786
  pair = struct.unpack("<HH", got[0x03])
737
- if pair not in allowed:
738
- raise Reject(m.UNAVAILABLE) # a pair this probe does not allow
787
+ if not self._allows(fn, pair) or set(pair) & self._held(fn, pair):
788
+ raise Reject(m.UNAVAILABLE) # not allowed here, or its pins are held (§8.1)
739
789
  return pair
790
+ allowed = self._allowed_pairs(fn)
740
791
  if len(allowed) != 1:
741
792
  raise Reject(m.UNAVAILABLE) # the host chooses among several
742
793
  return allowed[0]
@@ -779,7 +830,7 @@ class Endpoint:
779
830
 
780
831
  def _target_of(self, cid: int) -> FakeTarget:
781
832
  c = self.conns[cid]
782
- return self.targets[(c.fn, c.pair)]
833
+ return self._target(c.fn, c.pair)
783
834
 
784
835
  # ---- oep.target.riscv-dm --------------------------------------------------------------------
785
836
  @staticmethod
@@ -1181,7 +1232,7 @@ class Endpoint:
1181
1232
  places = [(s.wire_fn, s.pair) for s in slots.values()]
1182
1233
  if len(set(places)) != len(places):
1183
1234
  raise Reject(m.UNAVAILABLE)
1184
- for fn in self.pairs:
1235
+ for fn in self.pairs: # every wire (pin_roles wires have an empty list)
1185
1236
  if sum(1 for s in slots.values() if s.wire_fn == fn and s.attach == SLOT_ATTACH["at_boot"]) > \
1186
1237
  self.max_connections.get(fn, 1):
1187
1238
  raise Reject(m.UNAVAILABLE)
@@ -1244,7 +1295,7 @@ class Endpoint:
1244
1295
  raise Reject(m.MALFORMED)
1245
1296
  if not SLOT_NAME.fullmatch(name.decode("ascii", "replace")):
1246
1297
  raise Reject(m.MALFORMED)
1247
- if self.names.get(wire_fn) not in WIRES or (swdio, swclk) not in self.pairs.get(wire_fn, []):
1298
+ if self.names.get(wire_fn) not in WIRES or not self._allows(wire_fn, (swdio, swclk)):
1248
1299
  raise Reject(m.UNAVAILABLE)
1249
1300
  if mech not in self.mechanisms:
1250
1301
  raise Reject(m.UNSUPPORTED)
@@ -1301,7 +1352,7 @@ class Endpoint:
1301
1352
  s = self.slots[n]
1302
1353
  rt = self.slot_rt[n]
1303
1354
  rt.last_try_ms = self.now()
1304
- tg = self.targets[(s.wire_fn, s.pair)]
1355
+ tg = self._target(s.wire_fn, s.pair)
1305
1356
  cid = self._conn_at(s.wire_fn, s.pair)
1306
1357
  if cid is None:
1307
1358
  if not tg.present:
@@ -139,7 +139,8 @@ def _core(firmware: str, model: str, unit_id: bytes, channels: int, reserved: li
139
139
  return Offered(0, 0, "oep.core", (
140
140
  catalog.text(CORE_FIRMWARE, firmware), catalog.text(CORE_MODEL, model), catalog.tlv(CORE_UNIT_ID, unit_id),
141
141
  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)
142
+ ) + ((catalog.text(CORE_PROFILE, profile),) if profile else ()) + tuple(_label(c, n) for c, n in labels.items())
143
+ + extra)
143
144
 
144
145
 
145
146
  def p4_x035() -> FakeProbe:
@@ -230,4 +231,24 @@ def p4_bench() -> FakeProbe:
230
231
  ])
231
232
 
232
233
 
233
- PROFILES = {"p4-x035": p4_x035, "esp32-v003": esp32_v003, "p4-bench": p4_bench}
234
+ def rp2350_pins() -> FakeProbe:
235
+ """A board firmware whose wire takes its pins from the host (oep-if-debug §1, role_channels): any two of GP0-GP29
236
+ but GP19 (the Pro Micro RP2350's PSRAM CS) are an RVSWD pair, a reset line or fixture pins. USB CDC (serial port 0).
237
+ The fake target answers on (0, 1), the first pair."""
238
+ reserved = [19]
239
+ pins = [p for p in range(30) if p not in reserved]
240
+ return FakeProbe("rp2350-pins", 1024, [
241
+ _core("3.0.0", "sparkfun-promicro-rp2350", bytes.fromhex("e66138935f2b1f2c"), 30, reserved, "",
242
+ {}, _transports([(TRANSPORT["usb_cdc"], 0)])),
243
+ Offered(1, 1, "oep.wire.rvswd", _roles({1: pins, 2: pins, 3: pins}) + (
244
+ catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
245
+ Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
246
+ catalog.u16(MAX_LENGTH, 1000))),
247
+ Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
248
+ Offered(4, 2, "oep.fixture.gpio", _roles({1: pins})),
249
+ Offered(5, 3, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
250
+ catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
251
+ ])
252
+
253
+
254
+ PROFILES = {"p4-x035": p4_x035, "esp32-v003": esp32_v003, "p4-bench": p4_bench, "rp2350-pins": rp2350_pins}
@@ -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={})
@@ -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))
@@ -431,3 +431,45 @@ def test_idle_clock_is_rvswd_s_and_a_slot_carries_the_line_settings():
431
431
  assert hv.raw(9, 0x02, slot_item(0, 1, v003.pairs[1][0], idle=1)).detail == m.MALFORMED # swio has no idle_clock
432
432
  r = hv.raw(1, 0x02, b"\x01" + idle_low)
433
433
  assert r.resolution == m.REJECTED and r.detail == m.UNSUPPORTED # an unknown critical TLV on swio
434
+
435
+
436
+ def pins_probe():
437
+ ep = endpoint.Endpoint(fake.rp2350_pins(), Clock())
438
+ h = Host(ep)
439
+ h.open()
440
+ return ep, h
441
+
442
+
443
+ def test_a_wire_takes_any_free_pair_the_host_names():
444
+ """oep-if-debug §1: role_channels pairs; count 0 in swdio / swclk order; a live connection holds its pins."""
445
+ ep, h = pins_probe()
446
+ pins = lambda d, c: m.tlv(0x03, struct.pack("<HH", d, c), critical=True)
447
+ r = m.Reader(h.ok(1, 0x01, b"\x00")) # count 0: the first 255 free pairs
448
+ tried, count = r.take("BB")
449
+ assert tried == 255 and count == 1 and r.take("BHHI")[1:3] == (0, 1)
450
+ skip = lambda n: m.tlv(0x01, struct.pack("<H", n))
451
+ assert m.Reader(h.ok(1, 0x01, b"\x00" + skip(29 * 28 - 10))).take("B") == 10 # the last ten
452
+ assert m.Reader(h.ok(1, 0x01, b"\x00" + skip(29 * 28))).take("B") == 0 # the end
453
+ assert h.raw(1, 0x01, b"\x01" + struct.pack("<HH", 0, 1) + skip(1)).detail == m.MALFORMED
454
+ conn = struct.unpack_from("<H", h.ok(1, 0x02, b"\x01" + pins(0, 1)))[0]
455
+ assert h.raw(1, 0x02, b"\x01" + pins(1, 2)).detail == m.UNAVAILABLE # GP1 is the live connection's
456
+ assert h.raw(0, m.OP_PLAN_APPLY, m.tlv(0x90, struct.pack("<HBH", 4, 1, 0))).detail == m.UNAVAILABLE
457
+ assert m.Reader(h.ok(1, 0x01, b"\x00")).take("B") == 1 # its one seat is taken: the live pair only
458
+ assert h.raw(1, 0x01, b"\x01" + struct.pack("<HH", 2, 3)).detail == m.UNAVAILABLE
459
+ h.ok(1, 0x03, struct.pack("<H", conn) + m.tlv(0x01, b"")) # detach (force): the pins go back
460
+ h.ok(0, m.OP_PLAN_APPLY, m.tlv(0x90, struct.pack("<HBH", 4, 1, 5)))
461
+ assert h.raw(1, 0x01, b"\x01" + struct.pack("<HH", 5, 6)).detail == m.UNAVAILABLE # GP5 is the plan's
462
+ assert h.raw(1, 0x02, b"\x01" + pins(3, 3)).detail == m.UNAVAILABLE # one channel twice
463
+ assert h.raw(1, 0x02, b"\x01" + pins(19, 3)).detail == m.UNAVAILABLE # not offered (PSRAM CS)
464
+ r = h.raw(1, 0x02, b"\x01" + pins(7, 8)) # a free pair nothing answers on
465
+ assert r.resolution == m.COMPLETED and not r.succeeded
466
+
467
+
468
+ def test_the_client_scan_walks_the_whole_count_0_list():
469
+ from oep_client import host as hh, target
470
+ ep = endpoint.Endpoint(fake.rp2350_pins(), Clock())
471
+ hst = hh.Host(lambda b: ep.handle(b, 0))
472
+ hst.open(3000)
473
+ ep._target(1, (28, 29)).present = True # the last pair of the list
474
+ found = target.Wire(hst).scan()
475
+ assert [f.pins for f in found] == [(0, 1), (28, 29)]