oep-client-python 0.0.5__tar.gz → 0.0.6__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.5 → oep_client_python-0.0.6}/CHANGELOG.md +8 -0
  2. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/PKG-INFO +1 -1
  3. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/pyproject.toml +1 -1
  4. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/__init__.py +1 -1
  5. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/__main__.py +6 -1
  6. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/config.py +12 -7
  7. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/endpoint.py +28 -10
  8. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/fake.py +2 -2
  9. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/fake_serve.py +2 -2
  10. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/registry.py +4 -4
  11. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/riscv.py +31 -12
  12. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_config.py +44 -1
  13. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_fake_spec.py +39 -4
  14. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/.gitignore +0 -0
  15. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/LICENSE +0 -0
  16. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/README.ja.md +0 -0
  17. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/README.md +0 -0
  18. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/arm.py +0 -0
  19. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/capture.py +0 -0
  20. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/catalog.py +0 -0
  21. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/ch32_flash.py +0 -0
  22. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/cobs.py +0 -0
  23. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/console.py +0 -0
  24. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/core.py +0 -0
  25. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/decode.py +0 -0
  26. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/dump.py +0 -0
  27. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/esp32_targets.py +0 -0
  28. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/fake_serial.py +0 -0
  29. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/fixture.py +0 -0
  30. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/frames.py +0 -0
  31. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/hid_stream.py +0 -0
  32. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/host.py +0 -0
  33. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/interfaces.py +0 -0
  34. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/link.py +0 -0
  35. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/message.py +0 -0
  36. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/names.py +0 -0
  37. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/rp2350.py +0 -0
  38. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/target.py +0 -0
  39. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/uiapduino.py +0 -0
  40. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/src/oep_client/usb_stream.py +0 -0
  41. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_capabilities.py +0 -0
  42. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_capture.py +0 -0
  43. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_cobs.py +0 -0
  44. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_esp32_targets.py +0 -0
  45. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_flash_console.py +0 -0
  46. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_hid_stream.py +0 -0
  47. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_interfaces.py +0 -0
  48. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_link_host.py +0 -0
  49. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_link_serial.py +0 -0
  50. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_session.py +0 -0
  51. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_target_parts.py +0 -0
  52. {oep_client_python-0.0.5 → oep_client_python-0.0.6}/tests/test_usb_stream.py +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.6
6
+ - (EN) No default reset line (oep-if-debug §3): `Wire.attach_under_reset(channel, ...)` needs its channel, and `Wire.reset_channels()` reads the ones the probe allows (describe role_channels, role reset). The fake takes only those, and not one a plan holds.
7
+ - (JA) 既定の reset 線は無くなった(oep-if-debug §3): `Wire.attach_under_reset(channel, ...)` は channel が必須。probe が許す channel は `Wire.reset_channels()` で読む(describe の role_channels の role reset)。偽の probe はそれだけを受け、plan が持つ channel は断る。
8
+ - (EN) The line's settings are the target's, passed by the host: `attach(..., idle_clock="low")` (rvswd, critical) and the probe.config slot's new max_speed / idle_clock fields (`oep config slot --max-speed --idle-clock`) for the probe's own attach. The slot item's layout changed.
9
+ - (JA) 線の設定は target のもので、host が渡す: `attach(..., idle_clock="low")`(rvswd、critical)と、probe が自分で attach するための probe.config のスロットの max_speed / idle_clock(`oep config slot --max-speed --idle-clock`)。スロットの項目の形が変わった。
10
+ - (EN) The fake probe keeps a plan the settings put in (oep-core §8): plan_release leaves it (n = 0 included) and plan_apply naming its fn is rejected unavailable.
11
+ - (JA) 偽の probe は、設定が入れた plan を設定のものとして扱う(oep-core §8): plan_release はそれを解かず(n = 0 でも)、その fn を挙げた plan_apply は rejected unavailable。
12
+
5
13
  ## 0.0.5
6
14
  - (EN) USB (python-libusb1) streams close cleanly and at exit: the IN transfers are cancelled and taken back by the event thread before the handle and the context go. A program that ended without closing the link hung at exit, or libusb aborted.
7
15
  - (JA) USB(python-libusb1)の stream は、終了時にも片付いて閉じる: IN の転送を取り消し、event thread が取り戻してから handle と context を閉じる。link を閉じずに終わったプログラムが終了で固まったり、libusb が abort したりしていた。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.5
3
+ Version: 0.0.6
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.5"
7
+ version = "0.0.6"
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.5"
5
+ __version__ = "0.0.6"
@@ -64,6 +64,10 @@ def _config_parser(sub) -> None:
64
64
  slot.add_argument("--attach", choices=sorted(config.ATTACH), default="host")
65
65
  slot.add_argument("--retry", type=int, default=0, help="at-boot: try again every N s while absent (0: never)")
66
66
  slot.add_argument("--mechanism", choices=sorted(config.MECHANISM), default="dmseq")
67
+ slot.add_argument("--max-speed", type=int, default=0,
68
+ help="the line's ceiling in Hz for the probe's own attach (0: none); the target's, e.g. 1000000")
69
+ slot.add_argument("--idle-clock", choices=sorted(config.IDLE_CLOCK), default="high",
70
+ help="rvswd: SWCLK while the line rests (the target's: low on CH32L103 / V203)")
67
71
  slot.add_argument("--lock", help="MASK:VALUE (hex u32) the target_id (WCH DMI 0x7F) must match, e.g. ffffff0f:035e0600")
68
72
  slot.add_argument("--save", action="store_true", help="save after the change")
69
73
  bind = cs.add_parser("bind", help="what a serial port carries")
@@ -183,7 +187,7 @@ def _config(args) -> int:
183
187
  mask, _, value = args.lock.partition(":")
184
188
  lock = (1, struct.pack("<I", int(mask, 16)), struct.pack("<I", int(value, 16)))
185
189
  it = config.Slot(args.slot, fn, _pins(hst, fn, args.pins, args.wire), args.name, args.attach, args.retry,
186
- args.mechanism, lock)
190
+ args.mechanism, lock, args.max_speed, args.idle_clock)
187
191
  _change(hst, cfg, [it], args.save)
188
192
  elif args.action == "bind":
189
193
  slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
@@ -250,6 +254,7 @@ def _show(hst, cfg, as_json: bool) -> int:
250
254
  s = by_slot.get(it.slot)
251
255
  pins = f"{it.pins[0]}" if it.pins[1] == 0xFFFF else f"{it.pins[0]},{it.pins[1]}"
252
256
  retry = f" retry {it.retry_s} s" if it.attach == "at-boot" else ""
257
+ retry += (f" max {it.max_speed} Hz" if it.max_speed else "") + (" idle-low" if it.idle_clock == "low" else "")
253
258
  lock = (f" lock {int.from_bytes(it.lock[1], 'little'):08x}:{int.from_bytes(it.lock[2], 'little'):08x}"
254
259
  if it.lock else "")
255
260
  live = ""
@@ -29,6 +29,7 @@ MECHANISM = {k: v for k, v in reg.TARGET_CONSOLE.enum["mechanism"].items()}
29
29
  SLOT_STATE = {v: k.replace("_", "-") for k, v in _CFG.enum["slot_state"].items()}
30
30
  BIND_FLOW = {v: k for k, v in _CFG.enum["bind_flow"].items()}
31
31
  IDLE = {k.replace("_", "-"): v for k, v in _CFG.enum["idle_mode"].items()}
32
+ IDLE_CLOCK = dict(reg.WIRE_RVSWD.enum["idle_clock"]) # a slot's idle_clock (oep-if-debug §3)
32
33
  STORAGE_STATE = {v: k for k, v in _CFG.enum["storage_state"].items()}
33
34
 
34
35
 
@@ -79,12 +80,15 @@ class Slot:
79
80
  retry_s: int = 0 # at-boot: try again every retry_s while the target is not there (0: never)
80
81
  mechanism: str = "dmseq" # sdi, dmdata, dmseq
81
82
  lock: tuple[int, bytes, bytes] | None = None
83
+ max_speed: int = 0 # the line's ceiling in Hz for the probe's own attach (0: none) - the target's
84
+ idle_clock: str = "high" # rvswd: SWCLK while the line rests, high / low - the target's (oep-if-debug §3)
82
85
  TAG = ITEM["slot"]
83
86
 
84
87
  def value(self) -> bytes:
85
88
  name = self.name.encode()
86
- v = struct.pack("<BHHHBHBB", self.slot, self.wire_fn, *self.pins, ATTACH[self.attach],
87
- self.retry_s if self.attach == "at-boot" else 0, MECHANISM[self.mechanism], len(name)) + name
89
+ v = struct.pack("<BHHHBHIBBB", self.slot, self.wire_fn, *self.pins, ATTACH[self.attach],
90
+ self.retry_s if self.attach == "at-boot" else 0, self.max_speed, IDLE_CLOCK[self.idle_clock],
91
+ MECHANISM[self.mechanism], len(name)) + name
88
92
  if self.lock is None:
89
93
  return v + b"\x00"
90
94
  scheme, mask, value = self.lock
@@ -127,15 +131,16 @@ def decode(tag: int, v: bytes):
127
131
  return Label(struct.unpack_from("<H", v)[0], v[2:].decode("utf-8", "replace"))
128
132
  if tag == ITEM["idle"] and len(v) == 3:
129
133
  return Idle(struct.unpack_from("<H", v)[0], _name(IDLE, v[2]))
130
- if tag == ITEM["slot"] and len(v) >= 13:
131
- n, wire_fn, swdio, swclk, attach, retry_s, mech, name_len = struct.unpack_from("<BHHHBHBB", v)
132
- name = v[12:12 + name_len].decode("ascii", "replace")
133
- rest = v[12 + name_len:]
134
+ if tag == ITEM["slot"] and len(v) >= 18:
135
+ n, wire_fn, swdio, swclk, attach, retry_s, max_speed, idle, mech, name_len = struct.unpack_from("<BHHHBHIBBB", v)
136
+ name = v[17:17 + name_len].decode("ascii", "replace")
137
+ rest = v[17 + name_len:]
134
138
  lock = None
135
139
  if rest and rest[0]:
136
140
  half = (len(rest) - 1) // 2
137
141
  lock = (rest[0], rest[1:1 + half], rest[1 + half:])
138
- return Slot(n, wire_fn, (swdio, swclk), name, _name(ATTACH, attach), retry_s, _name(MECHANISM, mech), lock)
142
+ return Slot(n, wire_fn, (swdio, swclk), name, _name(ATTACH, attach), retry_s, _name(MECHANISM, mech), lock,
143
+ max_speed, _name(IDLE_CLOCK, idle))
139
144
  if tag == ITEM["bind"] and len(v) >= 4:
140
145
  port, mode, selected, n = struct.unpack_from("<BBBB", v)
141
146
  streams = [(_name(STREAM, v[4 + 3 * k]), struct.unpack_from("<H", v, 5 + 3 * k)[0]) for k in range(n)]
@@ -60,6 +60,7 @@ WIRES = ("oep.wire.rvswd", "oep.wire.swio")
60
60
  OWNER = reg.CORE.tlv["open"]["owner"]
61
61
  SLOT_NAME = re.compile(r"[a-z0-9_-]{1,32}")
62
62
  NO_SLOT, NEVER = 0xFF, 0xFFFFFFFF
63
+ PIN_ROLE_RESET = reg.WIRE_RVSWD.enum["pin_role"]["reset"] # the channels attach_under_reset may take
63
64
 
64
65
 
65
66
  class Reject(Exception):
@@ -180,6 +181,7 @@ class Connection:
180
181
  speed: int = 4_000_000
181
182
  tid: int | None = None
182
183
  users: set = field(default_factory=set) # "host" and/or ("slot", n)
184
+ idle_clock: int = 0 # rvswd: SWCLK while the line rests, 0 high / 1 low
183
185
 
184
186
 
185
187
  @dataclass(frozen=True)
@@ -189,6 +191,8 @@ class Slot:
189
191
  pair: tuple[int, int]
190
192
  attach: int
191
193
  retry_s: int
194
+ max_speed: int # 0: no ceiling (oep-if-debug §3: the target's, the host's to set)
195
+ idle_clock: int
192
196
  mechanism: int
193
197
  name: str
194
198
  lock: tuple[int, bytes, bytes] | None # (scheme, mask, value)
@@ -249,9 +253,13 @@ class Endpoint:
249
253
  self.serial_ports = {i for i, k in enumerate(self.transports) if k in fake.SERIAL_KINDS}
250
254
  self.pairs: dict[int, list[tuple[int, int]]] = {} # wire fn -> allowed (swdio, swclk), declared order
251
255
  self.max_connections: dict[int, int] = {}
256
+ self.reset_channels: dict[int, set[int]] = {} # wire fn -> channels attach_under_reset may take (role 3)
252
257
  for fn, name in self.names.items():
253
258
  if name in WIRES:
254
259
  self.pairs[fn] = [self._group_pair(name, t) for t in self.static[fn] if t[0] == catalog.CHANNEL_GROUP]
260
+ self.reset_channels[fn] = {
261
+ c for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS and t[2] == PIN_ROLE_RESET
262
+ for c in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
255
263
  self.max_connections[fn] = next((t[2] for t in self.static[fn] if t[0] == fake.MAX_CONNECTIONS), 1)
256
264
  self.targets: dict[tuple[int, tuple[int, int]], FakeTarget] = {
257
265
  (fn, p): FakeTarget() for fn in sorted(self.pairs) for p in self.pairs[fn]}
@@ -575,17 +583,18 @@ class Endpoint:
575
583
  got.append(struct.unpack_from("<HBH", value))
576
584
  elif tag & m.TAG_CRITICAL:
577
585
  return m.REJECTED, m.UNSUPPORTED, bytes([tag])
578
- self._check_plan(got)
579
586
  named = {fn for fn, _, _ in got}
587
+ if named & self.plan_from_config: # the settings' plan is the settings' (core §8)
588
+ raise Reject(m.UNAVAILABLE)
589
+ self._check_plan(got)
580
590
  self.plan = {a for a in self.plan if a[0] not in named} | set(got)
581
- self.plan_from_config -= named
582
591
  return m.COMPLETED, m.SUCCESS, b""
583
592
  if op == m.OP_PLAN_RELEASE: # n(u8) n x fn(u16); n = 0: every fn
584
593
  n = t.take("B")
585
594
  fns = {t.take("H") for _ in range(n)}
586
595
  t.tail()
587
- for fn in {a[0] for a in self.plan if not fns or a[0] in fns}:
588
- self._drop_plan(fn)
596
+ for fn in {a[0] for a in self.plan if not fns or a[0] in fns} - self.plan_from_config:
597
+ self._drop_plan(fn) # the settings' plans stay, n = 0 too (core §8)
589
598
  return m.COMPLETED, m.SUCCESS, b""
590
599
  return m.REJECTED, m.UNKNOWN_OPERATION, b""
591
600
 
@@ -657,10 +666,14 @@ class Endpoint:
657
666
  if method > 1:
658
667
  raise Reject(m.UNSUPPORTED)
659
668
  else:
660
- channel, _hold = t.take("HH")
661
- if channel not in (0xFFFF, self._channel_named("NRST")):
669
+ channel, _hold = t.take("HH") # no default reset line (oep-if-debug §3)
670
+ if channel not in self.reset_channels.get(fn, set()) or any(a[2] == channel for a in self.plan):
662
671
  raise Reject(m.UNAVAILABLE)
663
- got, ignored = t.tail({0x01, 0x03})
672
+ rvswd = self.names[fn] == "oep.wire.rvswd"
673
+ got, ignored = t.tail({0x01, 0x03, 0x04} if rvswd else {0x01, 0x03})
674
+ if 0x04 in got and (len(got[0x04]) != 1 or got[0x04][0] > 1):
675
+ raise Reject(m.MALFORMED)
676
+ idle_clock = got[0x04][0] if 0x04 in got else 0
664
677
  pair = self._pick_pair(fn, got)
665
678
  tg = self.targets[(fn, pair)]
666
679
  speed = min(4_000_000, struct.unpack("<I", got[0x01])[0]) if 0x01 in got else 4_000_000
@@ -676,6 +689,7 @@ class Endpoint:
676
689
  flags |= 2
677
690
  self.conns[cid].speed = min(self.conns[cid].speed, speed)
678
691
  c = self.conns[cid]
692
+ c.idle_clock = idle_clock # an existing connection takes the new rest level
679
693
  c.users.add("host")
680
694
  for n, s in self.slots.items(): # a new connection for an evicted slot: a new cue
681
695
  if s.wire_fn == fn and s.pair == pair:
@@ -1218,7 +1232,7 @@ class Endpoint:
1218
1232
 
1219
1233
  def _parse_slot(self, v: bytes) -> Slot:
1220
1234
  t = Take(v)
1221
- n, wire_fn, swdio, swclk, attach, retry_s, mech, name_len = t.take("BHHHBHBB")
1235
+ n, wire_fn, swdio, swclk, attach, retry_s, max_speed, idle_clock, mech, name_len = t.take("BHHHBHIBBB")
1222
1236
  name = t.bytes(name_len)
1223
1237
  scheme = t.take("B")
1224
1238
  rest = v[t.at:]
@@ -1226,6 +1240,8 @@ class Endpoint:
1226
1240
  raise Reject(m.MALFORMED)
1227
1241
  if retry_s and attach != SLOT_ATTACH["at_boot"]:
1228
1242
  raise Reject(m.MALFORMED)
1243
+ if idle_clock > 1 or (idle_clock and self.names.get(wire_fn) != "oep.wire.rvswd"):
1244
+ raise Reject(m.MALFORMED)
1229
1245
  if not SLOT_NAME.fullmatch(name.decode("ascii", "replace")):
1230
1246
  raise Reject(m.MALFORMED)
1231
1247
  if self.names.get(wire_fn) not in WIRES or (swdio, swclk) not in self.pairs.get(wire_fn, []):
@@ -1239,7 +1255,7 @@ class Endpoint:
1239
1255
  lock = (scheme, rest[:len(rest) // 2], rest[len(rest) // 2:])
1240
1256
  elif rest:
1241
1257
  raise Reject(m.MALFORMED)
1242
- return Slot(n, wire_fn, (swdio, swclk), attach, retry_s, mech, name.decode(), lock)
1258
+ return Slot(n, wire_fn, (swdio, swclk), attach, retry_s, max_speed, idle_clock, mech, name.decode(), lock)
1243
1259
 
1244
1260
  def _parse_bind(self, v: bytes, slots: dict[int, Slot]) -> Bind:
1245
1261
  t = Take(v)
@@ -1291,7 +1307,9 @@ class Endpoint:
1291
1307
  if not tg.present:
1292
1308
  return
1293
1309
  try:
1294
- cid = self._seat(s.wire_fn, s.pair, tg, 4_000_000, evict=False) # automatic: never evicts
1310
+ speed = min(4_000_000, s.max_speed) if s.max_speed else 4_000_000 # the slot's line settings
1311
+ cid = self._seat(s.wire_fn, s.pair, tg, speed, evict=False) # automatic: never evicts
1312
+ self.conns[cid].idle_clock = s.idle_clock
1295
1313
  except Reject:
1296
1314
  return
1297
1315
  tg.havereset = False
@@ -185,8 +185,8 @@ def esp32_v003() -> FakeProbe:
185
185
  _core("3.0.0", "esp32-d0wd", bytes.fromhex("0070070d9394"), 40, reserved, f"{NS}.esp32-v003",
186
186
  {16: "SWIO", 23: "NRST", 22: "DUT TX", 21: "DUT RX"},
187
187
  _transports([(TRANSPORT["uart_bridge"], 0xFF)])),
188
- Offered(1, 1, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.u8(IMPLEMENTATION, 1),
189
- catalog.u8(MAX_CONNECTIONS, 1))),
188
+ Offered(1, 1, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.role_channels(3, [23]),
189
+ catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
190
190
  Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b0111), catalog.u8(IMPLEMENTATION, 1),
191
191
  catalog.u16(MAX_LENGTH, 40))),
192
192
  Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
@@ -77,8 +77,8 @@ def build(a: argparse.Namespace) -> endpoint.Endpoint:
77
77
  swdio, swclk = ep.pairs[wire][n]
78
78
  mech = 2 if 2 in ep.mechanisms else min(ep.mechanisms)
79
79
  raw = name.encode()
80
- value = struct.pack("<BHHHBHBB", n, wire, swdio, swclk, reg.PROBE_CONFIG.enum["slot_attach"]["at_boot"], 1,
81
- mech, len(raw)) + raw + b"\x00" # no lock
80
+ value = struct.pack("<BHHHBHIBBB", n, wire, swdio, swclk, reg.PROBE_CONFIG.enum["slot_attach"]["at_boot"], 1,
81
+ 0, 0, mech, len(raw)) + raw + b"\x00" # no speed ceiling, rests high, no lock
82
82
  items.append(bytes([_ITEM["slot"], len(value)]) + value)
83
83
  if a.bind:
84
84
  streams = b"".join(struct.pack("<BH", reg.PROBE_CONFIG.enum["bind_stream"]["slot_console"], n)
@@ -2,7 +2,7 @@
2
2
 
3
3
  from types import SimpleNamespace as _NS
4
4
 
5
- REGISTRY_HASH = "7ba704094c2898d2"
5
+ REGISTRY_HASH = "049bd156b4775990"
6
6
  SCHEMA = 1
7
7
  PROTOCOL_REVISION = 1
8
8
  CONFIRM_REQUEST_MAGIC = 'OEP?'
@@ -22,13 +22,13 @@ 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}}, 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}}, 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}, "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, "swclk": 0x02}}, own={})
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={})
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}}, own={})
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={})
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
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={})
@@ -14,8 +14,8 @@ import struct
14
14
  import time
15
15
  from dataclasses import dataclass, field
16
16
 
17
- from . import host as h, message as m, registry as reg
18
- from .core import Interface
17
+ from . import catalog, host as h, message as m, registry as reg
18
+ from .core import Interface, describe
19
19
  from .fixture import Gpio
20
20
 
21
21
  STATUS = reg.STATUS
@@ -109,7 +109,9 @@ class WireBase(Interface):
109
109
  class Wire(WireBase):
110
110
  """oep.wire.rvswd / oep.wire.swio (CH32 debug links to a RISC-V debug module)."""
111
111
  NAME = "oep.wire.rvswd"
112
- DEFAULT_RESET = 0xFFFF
112
+ ROLE_RESET = reg.WIRE_RVSWD.enum["pin_role"]["reset"]
113
+ TAG_IDLE_CLOCK = reg.WIRE_RVSWD.tlv["attach"]["idle_clock"]
114
+ IDLE_CLOCK = reg.WIRE_RVSWD.enum["idle_clock"]
113
115
  RUN, HALT = reg.WIRE_RVSWD.enum["attach_method"]["run"], reg.WIRE_RVSWD.enum["attach_method"]["halt"]
114
116
  TAG_TARGET_ID = reg.WIRE_RVSWD.tlv["attach_answer"]["target_id"]
115
117
  SCHEME_WCH_DMI_7F = reg.WIRE_RVSWD.enum["target_id_scheme"]["wch_dmi_7f"]
@@ -125,12 +127,28 @@ class Wire(WireBase):
125
127
  v = tail.get(self.TAG_TARGET_ID)
126
128
  self.target_id = (v[0], bytes(v[1:])) if v else None
127
129
 
128
- def attach(self, halt: bool = True, max_speed: int | None = None, pins: tuple[int, int] | None = None) -> tuple[int, int]:
130
+ def _idle_tlv(self, idle_clock: str | None) -> bytes:
131
+ # rvswd only, critical: a probe that cannot rest the line that way must refuse (oep-if-debug §3)
132
+ return b"" if idle_clock is None else m.tlv(self.TAG_IDLE_CLOCK, bytes([self.IDLE_CLOCK[idle_clock]]), critical=True)
133
+
134
+ def reset_channels(self) -> list[int]:
135
+ """The channels attach_under_reset may take (describe role_channels, role reset). There is no default reset
136
+ line: the host names one every time (oep-if-debug §3)."""
137
+ out = set()
138
+ for tag, v in describe(self.host, self.fn):
139
+ if tag & ~m.TAG_CRITICAL == catalog.ROLE_CHANNELS and v[0] == self.ROLE_RESET:
140
+ out.update(catalog.bitmap_to_channels(struct.unpack_from("<H", v, 1)[0], v[3:]))
141
+ return sorted(out)
142
+
143
+ def attach(self, halt: bool = True, max_speed: int | None = None, pins: tuple[int, int] | None = None,
144
+ idle_clock: str | None = None) -> tuple[int, int]:
129
145
  """-> (connection, DMSTATUS). Attaching an attached wire returns its connection as it is (self.existing).
130
146
  self.had_reset: a pending havereset was acknowledged first (a V00x's DMSTATUS halt / run bits stay frozen
131
147
  until then); self.speed_hz: the speed the probe chose; max_speed: a ceiling the probe must keep (critical);
132
- self.target_id: (scheme, value) of the target's identity when the probe could read one (oep-if-debug §1)."""
133
- body = bytes([self.HALT if halt else self.RUN]) + self._speed_tlv(max_speed) + self._pins_tlv(pins)
148
+ idle_clock: "high" / "low", how rvswd rests SWCLK (critical). Both are the target's, known by the host
149
+ (oep-if-debug §3). self.target_id: (scheme, value) of the target's identity when the probe could read one."""
150
+ body = (bytes([self.HALT if halt else self.RUN]) + self._speed_tlv(max_speed) + self._pins_tlv(pins)
151
+ + self._idle_tlv(idle_clock))
134
152
  rd = m.Reader(self._call(self.ATTACH, body).payload)
135
153
  conn, status, flags, self.speed_hz = rd.take("HIBI")
136
154
  self.had_reset, self.existing = bool(flags & 1), bool(flags & 2)
@@ -139,12 +157,13 @@ class Wire(WireBase):
139
157
  self._take_target_id(tail)
140
158
  return conn, status
141
159
 
142
- def attach_under_reset(self, channel: int | None = None, hold_ms: int = 20,
143
- max_speed: int | None = None, pins: tuple[int, int] | None = None) -> tuple[int, int]:
144
- """Hold the target in reset through `channel` (None: the probe's default reset line), attach, release and
145
- halt it at once - the way back from firmware that turns the debug pins into GPIOs. -> (connection, dpc)"""
146
- body = (struct.pack("<HH", self.DEFAULT_RESET if channel is None else channel, hold_ms) + self._speed_tlv(max_speed)
147
- + self._pins_tlv(pins))
160
+ def attach_under_reset(self, channel: int, hold_ms: int = 20, max_speed: int | None = None,
161
+ pins: tuple[int, int] | None = None, idle_clock: str | None = None) -> tuple[int, int]:
162
+ """Hold the target in reset through `channel` (always named: there is no default reset line; the probe
163
+ allows reset_channels()), attach, release and halt it at once - the way back from firmware that turns the
164
+ debug pins into GPIOs. -> (connection, dpc)"""
165
+ body = (struct.pack("<HH", channel, hold_ms) + self._speed_tlv(max_speed) + self._pins_tlv(pins)
166
+ + self._idle_tlv(idle_clock))
148
167
  rd = m.Reader(self._call(self.ATTACH_UNDER_RESET, body).payload)
149
168
  conn, dpc, self.speed_hz = rd.take("HII")
150
169
  self._take_target_id(rd.tail())
@@ -2,7 +2,9 @@
2
2
 
3
3
  import struct
4
4
 
5
- from oep_client import __main__ as cli, config, endpoint, fake, host as h
5
+ import pytest
6
+
7
+ from oep_client import __main__ as cli, config, endpoint, fake, host as h, message as m
6
8
 
7
9
 
8
10
  class Clock:
@@ -108,3 +110,44 @@ def test_a_plan_refused_for_a_saved_plan_names_the_holder():
108
110
  with pytest.raises(core.PinsTaken) as e:
109
111
  core.plan_apply(hst, [(4, 1, 21)]) # gpio wants 21
110
112
  assert e.value.holders == [(21, "the saved plan of fn 5 (role 2)")] and "fn 5" in str(e.value)
113
+
114
+
115
+ def test_a_settings_plan_is_not_the_sessions():
116
+ """core §8: plan_release leaves a plan the settings put in (n = 0 too); plan_apply naming its fn is refused."""
117
+ ep, hst = open_bench()
118
+ config.ProbeConfig(hst).set([config.Plan(5, 1, 20), config.Plan(5, 2, 21)])
119
+ assert 5 in ep.plan_from_config
120
+ with pytest.raises(h.Rejected) as e:
121
+ hst.call(m.CORE_FN, m.OP_PLAN_APPLY, m.tlv(0x90, struct.pack("<HBH", 5, 1, 22)))
122
+ assert e.value.result.detail == m.UNAVAILABLE
123
+ hst.call(m.CORE_FN, m.OP_PLAN_RELEASE, b"\x00")
124
+ assert {(5, 1, 20), (5, 2, 21)} <= ep.plan and 5 in ep.plan_from_config
125
+
126
+
127
+ def test_reset_channels_and_a_named_reset_line():
128
+ """oep-if-debug §3: the host reads the reset channels the probe allows and names one; there is no default."""
129
+ from oep_client import target
130
+ ep = endpoint.Endpoint(fake.esp32_v003(), Clock())
131
+ hst = h.Host(lambda b: ep.handle(b, 0))
132
+ hst.open(3000)
133
+ wire = target.Wire(hst, "oep.wire.swio")
134
+ assert wire.reset_channels() == [23]
135
+ conn, _dpc = wire.attach_under_reset(23)
136
+ assert conn in ep.conns
137
+
138
+
139
+ def test_slot_line_settings_from_the_command(capsys, monkeypatch):
140
+ ep, hst = open_bench()
141
+
142
+ class FakeLink:
143
+ def close(self):
144
+ pass
145
+ hst.link = FakeLink()
146
+ hst.end()
147
+ monkeypatch.setattr(cli.link, "open_host", lambda target: hst)
148
+ assert cli.main(["config", "slot", "x", "--name", "l103", "--pins", "2,3", "--attach", "at-boot", "--retry", "1",
149
+ "--max-speed", "1000000", "--idle-clock", "low"]) == 0
150
+ (slot,) = [i for i in config.ProbeConfig(hst).items() if isinstance(i, config.Slot)]
151
+ assert (slot.max_speed, slot.idle_clock) == (1_000_000, "low")
152
+ assert cli.main(["config", "show", "x"]) == 0
153
+ assert "max 1000000 Hz idle-low" in capsys.readouterr().out
@@ -49,10 +49,10 @@ class Host:
49
49
  return r.payload
50
50
 
51
51
 
52
- def slot_item(n, wire, pair, attach=ATTACH["at_boot"], retry=1, mech=2, name=None, lock=None):
52
+ def slot_item(n, wire, pair, attach=ATTACH["at_boot"], retry=1, mech=2, name=None, lock=None, max_speed=0, idle=0):
53
53
  raw = (name or f"s{n}").encode()
54
- value = struct.pack("<BHHHBHBB", n, wire, *pair, attach, retry if attach == ATTACH["at_boot"] else 0, mech,
55
- len(raw)) + raw
54
+ value = struct.pack("<BHHHBHIBBB", n, wire, *pair, attach, retry if attach == ATTACH["at_boot"] else 0, max_speed,
55
+ idle, mech, len(raw)) + raw
56
56
  value += b"\x00" if lock is None else bytes([1]) + lock[0] + lock[1]
57
57
  return m.tlv(ITEM["slot"], value)
58
58
 
@@ -198,7 +198,7 @@ def test_too_many_at_boot_slots_are_refused():
198
198
  def test_slot_names_are_url_safe(name):
199
199
  ep, h = bench()
200
200
  assert h.raw(6, 0x02, slot_item(0, 1, ep.pairs[1][0], name=name or None) if name else
201
- m.tlv(ITEM["slot"], struct.pack("<BHHHBHBB", 0, 1, *ep.pairs[1][0], 1, 1, 2, 0) + b"\x00")).detail == m.MALFORMED
201
+ m.tlv(ITEM["slot"], struct.pack("<BHHHBHIBBB", 0, 1, *ep.pairs[1][0], 1, 1, 0, 0, 2, 0) + b"\x00")).detail == m.MALFORMED
202
202
 
203
203
 
204
204
  # ---- binds and the serial port ---------------------------------------------------------------------------
@@ -396,3 +396,38 @@ def test_read_from_a_last_mark_that_is_not_there_is_from_now():
396
396
  reset = reg.TARGET_CONSOLE.enum["mark_kind"]["reset"]
397
397
  got = h.raw(uart_fn, 0x02, struct.pack("<BQH", 3, reset, 64), session=False).payload
398
398
  assert struct.unpack_from("<Q", got)[0] == 10 and got[9:] == b"" # no reset mark: from now, not the old bytes
399
+
400
+
401
+ def test_no_default_reset_line_only_the_declared_channels():
402
+ """oep-if-debug §3: attach_under_reset names its channel; the probe takes only role 3 (reset) channels, and not
403
+ one a plan holds."""
404
+ ep = endpoint.Endpoint(fake.esp32_v003(), Clock())
405
+ h = Host(ep)
406
+ h.open()
407
+ assert ep.reset_channels[1] == {23}
408
+ for channel in (0xFFFF, 22): # no default; 22 is not a reset line
409
+ assert h.raw(1, 0x04, struct.pack("<HH", channel, 20)).detail == m.UNAVAILABLE
410
+ h.ok(1, 0x04, struct.pack("<HH", 23, 20))
411
+ h.ok(0, m.OP_PLAN_APPLY, m.tlv(0x90, struct.pack("<HBH", 4, 1, 23))) # fixture.gpio takes NRST
412
+ assert h.raw(1, 0x04, struct.pack("<HH", 23, 20)).detail == m.UNAVAILABLE
413
+
414
+
415
+ def test_idle_clock_is_rvswd_s_and_a_slot_carries_the_line_settings():
416
+ ep, h = bench()
417
+ pair = ep.pairs[1][0]
418
+ idle_low = m.tlv(0x04, b"\x01", critical=True)
419
+ h.ok(1, 0x02, b"\x01" + m.tlv(0x03, struct.pack("<HH", *pair), critical=True) + idle_low)
420
+ assert ep.conns[ep._conn_at(1, pair)].idle_clock == 1
421
+ assert h.raw(1, 0x02, b"\x01" + m.tlv(0x03, struct.pack("<HH", *pair), critical=True)
422
+ + m.tlv(0x04, b"\x02", critical=True)).detail == m.MALFORMED
423
+ ep2, h2 = bench()
424
+ p2 = ep2.pairs[1][1]
425
+ h2.ok(6, 0x02, slot_item(0, 1, p2, name="l103", max_speed=1_000_000, idle=1))
426
+ c = ep2.conns[ep2._conn_at(1, p2)]
427
+ assert (c.speed, c.idle_clock) == (1_000_000, 1) # the probe's own attach uses the slot's settings
428
+ v003 = endpoint.Endpoint(fake.esp32_v003(), Clock())
429
+ hv = Host(v003)
430
+ hv.open()
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
+ r = hv.raw(1, 0x02, b"\x01" + idle_low)
433
+ assert r.resolution == m.REJECTED and r.detail == m.UNSUPPORTED # an unknown critical TLV on swio