oep-client-python 0.0.19__tar.gz → 0.0.21__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.19 → oep_client_python-0.0.21}/CHANGELOG.md +16 -0
  2. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/PKG-INFO +2 -2
  3. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/README.ja.md +1 -1
  4. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/README.md +1 -1
  5. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/pyproject.toml +1 -1
  6. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/__init__.py +1 -1
  7. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/__main__.py +10 -9
  8. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/config.py +7 -2
  9. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/console.py +5 -3
  10. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/endpoint.py +21 -9
  11. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/fake.py +32 -32
  12. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/fake_serve.py +1 -1
  13. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/link.py +1 -1
  14. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_capabilities.py +3 -3
  15. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_fake_spec.py +11 -1
  16. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/.gitignore +0 -0
  17. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/LICENSE +0 -0
  18. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/arm.py +0 -0
  19. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/capture.py +0 -0
  20. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/catalog.py +0 -0
  21. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/ch32_flash.py +0 -0
  22. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/cobs.py +0 -0
  23. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/core.py +0 -0
  24. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/decode.py +0 -0
  25. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/dump.py +0 -0
  26. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/fake_capture.py +0 -0
  27. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/fake_serial.py +0 -0
  28. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/fixture.py +0 -0
  29. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/frames.py +0 -0
  30. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/hid_stream.py +0 -0
  31. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/host.py +0 -0
  32. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/interfaces.py +0 -0
  33. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/message.py +0 -0
  34. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/names.py +0 -0
  35. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/registry.py +0 -0
  36. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/riscv.py +0 -0
  37. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/rp2350.py +0 -0
  38. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/uiapduino.py +0 -0
  39. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/src/oep_client/usb_stream.py +0 -0
  40. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_capture.py +0 -0
  41. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_cobs.py +0 -0
  42. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_config.py +0 -0
  43. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_fake_capture.py +0 -0
  44. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_fixture_targets.py +0 -0
  45. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_flash_console.py +0 -0
  46. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_hid_stream.py +0 -0
  47. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_interfaces.py +0 -0
  48. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_link_host.py +0 -0
  49. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_link_serial.py +0 -0
  50. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_session.py +0 -0
  51. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_target_parts.py +0 -0
  52. {oep_client_python-0.0.19 → oep_client_python-0.0.21}/tests/test_usb_stream.py +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.21
6
+ - (EN) Breaking: a bind's streams carry a length each (oep-spec probe.config §1.2, freeze decision 1, missed until now): `n × (len, kind, id)`, len 3 today; the fake skips a longer one's tail and refuses one under 3 as malformed.
7
+ - (JA) 破壊的変更: bind のストリームの並びの各要素の前に長さを置く(oep-spec probe.config §1.2。凍結の決定 1 で漏れていた): `n × (len、kind、id)`、今の len は 3。fake は長い要素の後ろを飛ばし、3 未満は malformed で断る。
8
+ - (EN) `usb:X` is always a unit id, whatever its length (a 4-character unit id was read as a VID); a VID comes with its PID, `usb:VID:PID[:SERIAL]`.
9
+ - (JA) `usb:X` は長さによらず unit id として読む(4 文字の unit id を VID と読んでいた)。VID は PID と一緒に `usb:VID:PID[:SERIAL]` で書く。
10
+ - (EN) Breaking: the CLI's `name#k` is the list's instance, from 0 (oep-spec core §7.2's `name#instance`; `#0` may be left out). It was the k-th from 1.
11
+ - (JA) 破壊的変更: CLI の `name#k` は list の instance(0 から)にした(oep-spec core §7.2 の `name#instance`、`#0` は省ける)。1 から数えた k 番目だった。
12
+ - (EN) The fake probes number each interface's instance from 0 per name (core §7.2), as the probe firmware now does; they had arbitrary numbers.
13
+ - (JA) fake の probe はインターフェースの instance を名前ごとに 0 から振る(core §7.2)。probe の firmware も同じにした。これまでは適当な番号だった。
14
+ - (EN) README: `registry.INTERFACES[name]` is the public way to reach an interface's numbers by name.
15
+ - (JA) README: 名前からインターフェースの番号を引く公開の入口は `registry.INTERFACES[name]` と書いた。
16
+
17
+ ## 0.0.20
18
+ - (EN) A console write's chunk fits the probe's frame: frame - 12 - the stream's prefix (the console's stream u16; none on a fixture UART). It was frame - 13 for both, one byte over on a console with 64-byte frames. Found porting it to oep-client-js.
19
+ - (JA) console の書き込みの 1 回の大きさが probe のフレームに収まるようにした: frame − 12 − stream の前置き(console は stream の u16、fixture UART は無し)。両方とも frame − 13 だったので、64 byte のフレームの console で 1 byte はみ出していた。oep-client-js に移すときに見つかった。
20
+
5
21
  ## 0.0.19
6
22
  - (EN) The pre-freeze decisions (oep-spec docs/v1-freeze-decisions.ja.md), in the fake and the client at once:
7
23
  - `oep.fixture.capture` is `oep.fixture.logic`; the I2C / SPI targets are the standard `oep.fixture.i2c-target` / `spi-target` (`fixture.I2cTarget` / `SpiTarget`; `esp32_targets` is gone). Their status is separate fields (`I2cStatus(state=, mode=, armed=, queued=, …)`); `read_hw` is gone, `set_stretch` is `stretch` (op 0x07).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.19
3
+ Version: 0.0.21
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
@@ -64,7 +64,7 @@ oep-client-python X.Y.Z (until the v1 freeze every release may break the wire; t
64
64
  | `config` | `oep.probe.config` (slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) |
65
65
  | `capture` | `oep.fixture.logic` (revision 1, oep-spec oep-if-capture). Every segment read goes to the `Host.on_capture` callbacks as a `CaptureRecord` (the hook for run recorders; no wireskein dependency) |
66
66
  | `decode` | decoding capture channels (I2C) |
67
- | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec) |
67
+ | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec). The public way to reach an interface by name is `registry.INTERFACES[name]` (`.revision`, `.op`, `.tlv`, `.enum`, e.g. `INTERFACES["oep.fixture.uart"].enum["role"]`); the module-level names (`FIXTURE_UART`, ...) are the same objects |
68
68
  | `arm` | `oep.wire.swd`, `oep.target.arm-adi`, MEM-AP, halting and calling functions on a Cortex-M |
69
69
  | `ch32_flash` | writing a CH32 (a RAM loader, page by page) |
70
70
  | `rp2350` | flash and reboot through the RP2350 boot ROM |
@@ -38,7 +38,7 @@ X.Y.Z(v1 の凍結までは、どのリリースも wire を壊しうるので
38
38
  | `config` | `oep.probe.config`(スロット、bind、plan / label / idle の項目、get / set / save / erase、スロットと bind の今の状態) |
39
39
  | `capture` | `oep.fixture.logic`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
40
40
  | `decode` | キャプチャのチャネルの復号(I2C) |
41
- | `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す) |
41
+ | `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す)。名前からインターフェースの番号を引く公開の入口は `registry.INTERFACES[name]`(`.revision`、`.op`、`.tlv`、`.enum`。例 `INTERFACES["oep.fixture.uart"].enum["role"]`)。`FIXTURE_UART` などのモジュールの名前は同じもの |
42
42
  | `arm` | `oep.wire.swd`、`oep.target.arm-adi`、MEM-AP、Cortex-M の停止と関数呼び出し |
43
43
  | `ch32_flash` | CH32 の書き込み(RAM ローダー、ページ単位の書き直し) |
44
44
  | `rp2350` | RP2350 の boot ROM 経由の flash と reboot |
@@ -42,7 +42,7 @@ oep-client-python X.Y.Z (until the v1 freeze every release may break the wire; t
42
42
  | `config` | `oep.probe.config` (slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) |
43
43
  | `capture` | `oep.fixture.logic` (revision 1, oep-spec oep-if-capture). Every segment read goes to the `Host.on_capture` callbacks as a `CaptureRecord` (the hook for run recorders; no wireskein dependency) |
44
44
  | `decode` | decoding capture channels (I2C) |
45
- | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec) |
45
+ | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec). The public way to reach an interface by name is `registry.INTERFACES[name]` (`.revision`, `.op`, `.tlv`, `.enum`, e.g. `INTERFACES["oep.fixture.uart"].enum["role"]`); the module-level names (`FIXTURE_UART`, ...) are the same objects |
46
46
  | `arm` | `oep.wire.swd`, `oep.target.arm-adi`, MEM-AP, halting and calling functions on a Cortex-M |
47
47
  | `ch32_flash` | writing a CH32 (a RAM loader, page by page) |
48
48
  | `rp2350` | flash and reboot through the RP2350 boot ROM |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oep-client-python"
7
- version = "0.0.19"
7
+ version = "0.0.21"
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.19"
5
+ __version__ = "0.0.21"
@@ -4,7 +4,7 @@
4
4
  oep config show <probe>
5
5
  oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
6
6
  oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
7
- oep config plan <probe> oep.fixture.uart#2 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
7
+ oep config plan <probe> oep.fixture.uart#1 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
8
8
  oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
9
9
 
10
10
  <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
@@ -79,7 +79,7 @@ def _config_parser(sub) -> None:
79
79
  bind.add_argument("--save", action="store_true")
80
80
  plan = cs.add_parser("plan", help="the pins an interface keeps (its whole plan, kept as a setting)")
81
81
  plan.add_argument("probe")
82
- plan.add_argument("fn", help="an fn, or an interface name with #k for its k-th instance (1 = the first)")
82
+ plan.add_argument("fn", help="an fn, or name#instance (the list instance, 0 = the first; #0 may be left out, core §7.2)")
83
83
  plan.add_argument("roles", nargs="+", help="ROLE=CHANNEL, ROLE a number or the interface's role name (rx, tx, line...)")
84
84
  plan.add_argument("--save", action="store_true")
85
85
  label = cs.add_parser("label", help="name a channel (shown in oep.core's describe)")
@@ -116,20 +116,21 @@ def _pins(hst, fn: int, text: str | None, wire: str) -> tuple[int, int]:
116
116
 
117
117
 
118
118
  def _plan_fn(hst, spec: str) -> tuple[int, dict[str, int]]:
119
- """fn and its role names (from the registry) for `spec`: an fn number, or name[#k] (k-th instance, 1-based)."""
119
+ """fn and its role names (from the registry) for `spec`: an fn number, or name[#instance] (the list's instance,
120
+ 0-based, core §7.2)."""
120
121
  from . import registry as reg
121
122
  if spec.isdigit():
122
123
  fn = int(spec)
123
124
  name = next((e.name for e in core.list_entries(hst) if e.fn == fn), "")
124
125
  else:
125
126
  name, _, k = spec.partition("#")
126
- fns = core.find_all(hst, name)
127
- if not fns:
127
+ entries = [e for e in core.list_entries(hst, name, exact=True)]
128
+ if not entries:
128
129
  raise SystemExit(f"the probe offers no {name}")
129
- index = int(k) - 1 if k else 0
130
- if not 0 <= index < len(fns):
131
- raise SystemExit(f"{name}: {len(fns)} instance(s), fns {fns}")
132
- fn = fns[index]
130
+ instance = int(k) if k else 0
131
+ fn = next((e.fn for e in entries if e.instance == instance), None)
132
+ if fn is None:
133
+ raise SystemExit(f"{name}: instances {sorted(e.instance for e in entries)}, not {instance}")
133
134
  iface = reg.INTERFACES.get(name)
134
135
  roles = dict(iface.enum.get("role", {})) if iface else {}
135
136
  return fn, roles
@@ -111,7 +111,7 @@ class Bind:
111
111
  def value(self) -> bytes:
112
112
  v = struct.pack("<BBBB", self.port, MODE[self.mode], self.selected if self.mode == "manual" else 0,
113
113
  len(self.streams))
114
- return v + b"".join(struct.pack("<BH", STREAM[kind], i) for kind, i in self.streams)
114
+ return v + b"".join(struct.pack("<BBH", 3, STREAM[kind], i) for kind, i in self.streams) # len, kind, id
115
115
 
116
116
 
117
117
  def item(it) -> bytes:
@@ -147,7 +147,12 @@ def decode(tag: int, v: bytes):
147
147
  mechanism=_name(MECHANISM, mech), lock=lock, max_speed=max_speed, idle_clock=_name(IDLE_CLOCK, idle))
148
148
  if tag == ITEM["bind"] and len(v) >= 4:
149
149
  port, mode, selected, n = struct.unpack_from("<BBBB", v)
150
- streams = [(_name(STREAM, v[4 + 3 * k]), struct.unpack_from("<H", v, 5 + 3 * k)[0]) for k in range(n)]
150
+ streams, at = [], 4
151
+ for _ in range(n): # len, kind, id: a longer one's tail skipped
152
+ if at >= len(v) or v[at] < 3 or at + 1 + v[at] > len(v):
153
+ return (tag, v)
154
+ streams.append((_name(STREAM, v[at + 1]), struct.unpack_from("<H", v, at + 2)[0]))
155
+ at += 1 + v[at]
151
156
  return Bind(port=port, mode=_name(MODE, mode), streams=streams, selected=selected)
152
157
  return (tag, v)
153
158
 
@@ -136,10 +136,12 @@ class StreamIO:
136
136
  self.lost = 0
137
137
 
138
138
  def _limits(self) -> tuple[int, int]:
139
- """(read, write) chunk sizes that fit the probe's frame: request header 6 + session 4 + stream 1 + count 2,
140
- result header 5 + start 8 + flags 1."""
139
+ """(read, write) chunk sizes that fit the probe's frame: a write is request header 6 + session 4 + the stream's
140
+ prefix (the console's stream u16, none on a fixture UART) + count 2; a read's result is header 5 + start 8 +
141
+ flags 1. (The write was frame - 13 for both: one byte over on the console's 64-byte frames.)"""
141
142
  frame = self.source.host.confirmed()["max_frame"]
142
- return max(1, min(self.MAX_READ, frame - 14)), max(1, min(self.MAX_WRITE, frame - 13))
143
+ write = frame - 12 - len(self.source._stream_prefix())
144
+ return max(1, min(self.MAX_READ, frame - 14)), max(1, min(self.MAX_WRITE, write))
143
145
 
144
146
  def read(self, n: int = 512) -> bytes:
145
147
  c = self.source.read_from(self.position, min(n, self._limits()[0]))
@@ -217,6 +217,17 @@ class Slot:
217
217
  lock: tuple[int, bytes, bytes] | None # (scheme, mask, value)
218
218
 
219
219
 
220
+ def _bind_streams(v: bytes) -> list[int]:
221
+ """Where each stream of a bind value starts (probe.config §1.2: n × (len, kind, id), len >= 3, its tail skipped)."""
222
+ at, out = 4, []
223
+ for _ in range(v[3]):
224
+ if at >= len(v) or v[at] < 3 or at + 1 + v[at] > len(v):
225
+ raise Reject(m.MALFORMED)
226
+ out.append(at + 1)
227
+ at += 1 + v[at]
228
+ return out
229
+
230
+
220
231
  @dataclass(frozen=True)
221
232
  class Bind:
222
233
  port: int
@@ -1435,9 +1446,9 @@ class Endpoint:
1435
1446
  elif tag == ITEM["slot"]:
1436
1447
  fns.add(struct.unpack_from("<H", value, 1)[0])
1437
1448
  elif tag == ITEM["bind"]:
1438
- for k in range(value[3]):
1439
- if value[4 + 3 * k] == BIND_STREAM["fixture_uart"]:
1440
- fns.add(struct.unpack_from("<H", value, 5 + 3 * k)[0])
1449
+ for at in _bind_streams(value):
1450
+ if value[at] == BIND_STREAM["fixture_uart"]:
1451
+ fns.add(struct.unpack_from("<H", value, at + 1)[0])
1441
1452
  return fns
1442
1453
 
1443
1454
  def _apply_saved(self) -> None:
@@ -1459,9 +1470,9 @@ class Endpoint:
1459
1470
  new[(tag, key)] = value[:1] + pack(struct.unpack_from("<H", value, 1)[0]) + value[3:]
1460
1471
  elif tag == ITEM["bind"]:
1461
1472
  v = bytearray(value)
1462
- for k in range(v[3]):
1463
- if v[4 + 3 * k] == BIND_STREAM["fixture_uart"]:
1464
- v[5 + 3 * k:7 + 3 * k] = pack(struct.unpack_from("<H", v, 5 + 3 * k)[0])
1473
+ for at in _bind_streams(value):
1474
+ if v[at] == BIND_STREAM["fixture_uart"]:
1475
+ v[at + 1:at + 3] = pack(struct.unpack_from("<H", v, at + 1)[0])
1465
1476
  new[(tag, key)] = bytes(v)
1466
1477
  else:
1467
1478
  new[(tag, key)] = value
@@ -1574,9 +1585,10 @@ class Endpoint:
1574
1585
  return Slot(n, wire_fn, (swdio, swclk), attach, retry_s, max_speed, idle_clock, mech, name.decode(), lock)
1575
1586
 
1576
1587
  def _parse_bind(self, v: bytes, slots: dict[int, Slot]) -> Bind:
1577
- t = Take(v)
1578
- port, mode, selected, n = t.take("BBBB")
1579
- streams = tuple(t.take("BH") for _ in range(n))
1588
+ if len(v) < 4:
1589
+ raise Reject(m.MALFORMED)
1590
+ port, mode, selected, n = v[:4]
1591
+ streams = tuple((v[at], struct.unpack_from("<H", v, at + 1)[0]) for at in _bind_streams(v))
1580
1592
  if n == 0: # after the streams: later fields, skipped
1581
1593
  raise Reject(m.MALFORMED)
1582
1594
  if port not in self.serial_ports:
@@ -191,34 +191,34 @@ def p4_x035() -> FakeProbe:
191
191
  _transports([(TRANSPORT["usb_serial_jtag"], 0xFF), (TRANSPORT["vendor_bulk"], 0),
192
192
  (TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)])
193
193
  + (catalog.u8(CORE_OEP_PID, 1), catalog.text(CORE_CHIP, "esp32p4 v1.0"))),
194
- Offered(1, 1, "oep.wire.rvswd", (
194
+ Offered(1, 0, "oep.wire.rvswd", (
195
195
  catalog.channel_group(1, [(1, 2), (2, 54)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1),
196
196
  catalog.u8(MAX_CONNECTIONS, 1))),
197
- Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
197
+ Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
198
198
  catalog.u16(MAX_LENGTH, 1000))),
199
- Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
200
- Offered(4, 2, "oep.fixture.gpio", _roles({1: pins})),
201
- Offered(5, 3, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
199
+ Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
200
+ Offered(4, 0, "oep.fixture.gpio", _roles({1: pins})),
201
+ Offered(5, 0, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
202
202
  catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
203
- Offered(6, 4, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
203
+ Offered(6, 1, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
204
204
  catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
205
205
  # the P4's PARLIO: w 1-16, one-shot / repeat / streaming
206
- Offered(7, 5, "oep.fixture.logic", _roles({k: pins for k in range(16)}) + (
206
+ Offered(7, 0, "oep.fixture.logic", _roles({k: pins for k in range(16)}) + (
207
207
  catalog.u32(MAX_CLOCK_HZ, 20_000_000), catalog.u32(MIN_CLOCK_HZ, 1_000),
208
208
  catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))
209
209
  + _capture_decl(["one_shot", "repeat", "streaming"], 16, [1, 2, 4, 8, 16], 1 << 20, 8, 4096)),
210
- Offered(8, 6, "oep.fixture.i2c-target", _roles({1: pins, 2: pins}) + (
210
+ Offered(8, 0, "oep.fixture.i2c-target", _roles({1: pins, 2: pins}) + (
211
211
  catalog.u16(MAX_LENGTH, 128), catalog.u32(MAX_CLOCK_HZ, 1_000_000),
212
212
  catalog.u32(FEATURES, 0b11), catalog.u8(IMPLEMENTATION, 2))),
213
- Offered(9, 7, "oep.fixture.spi-target", _roles({1: pins, 2: pins, 3: pins, 4: pins}) + (
213
+ Offered(9, 0, "oep.fixture.spi-target", _roles({1: pins, 2: pins, 3: pins, 4: pins}) + (
214
214
  catalog.u16(MAX_LENGTH, 64), catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
215
- _config(10, 8, slots_max=1),
215
+ _config(10, 0, slots_max=1),
216
216
  # the P4's ADC1 (GPIO16-23): one ADC for all its channels, 611 Hz - 83.3 kHz in all; ESP32-style attenuations
217
- Offered(11, 1, "oep.fixture.analog", _roles({k: list(range(16, 24)) for k in range(4)}) + (
217
+ Offered(11, 0, "oep.fixture.analog", _roles({k: list(range(16, 24)) for k in range(4)}) + (
218
218
  catalog.u32(MAX_CLOCK_HZ, 83_333), catalog.u32(MIN_CLOCK_HZ, 611), catalog.u8(IMPLEMENTATION, 3))
219
219
  + _analog_decl([(0, 0, 950, 0), (1, 0, 1250, 2500), (2, 0, 1750, 6000), (3, 0, 3100, 12000)], 65536)),
220
220
  # the logic (fn 7) and the analog (fn 11) together; the ADC's 83.3 kHz is shared by its channels
221
- Offered(12, 1, "oep.fixture.capture-group", _group_decl([7, 11], [(83_333, [11])], {11: 5000})),
221
+ Offered(12, 0, "oep.fixture.capture-group", _group_decl([7, 11], [(83_333, [11])], {11: 5000})),
222
222
  ])
223
223
 
224
224
 
@@ -231,26 +231,26 @@ def esp32_v003() -> FakeProbe:
231
231
  _core("3.0.0", "esp32", "0070070d9394", 40, reserved, f"{NS}.esp32-v003",
232
232
  {16: "SWIO", 23: "NRST", 22: "DUT TX", 21: "DUT RX"},
233
233
  _transports([(TRANSPORT["uart_bridge"], 0xFF)])),
234
- Offered(1, 1, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.role_channels(3, [23]),
234
+ Offered(1, 0, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.role_channels(3, [23]),
235
235
  catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
236
- Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b0111), catalog.u8(IMPLEMENTATION, 1),
236
+ Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b0111), catalog.u8(IMPLEMENTATION, 1),
237
237
  catalog.u16(MAX_LENGTH, 40))),
238
- Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
239
- Offered(4, 2, "oep.fixture.gpio", _roles({1: wired + [23]})),
240
- Offered(5, 3, "oep.fixture.uart", _roles({1: wired, 2: wired}) + (
238
+ Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
239
+ Offered(4, 0, "oep.fixture.gpio", _roles({1: wired + [23]})),
240
+ Offered(5, 0, "oep.fixture.uart", _roles({1: wired, 2: wired}) + (
241
241
  catalog.u32(MAX_CLOCK_HZ, 115_200), catalog.u8(IMPLEMENTATION, 2))),
242
242
  # the classic ESP32's GPIO sampler: a byte a sample (w 8), one-shot
243
- Offered(6, 4, "oep.fixture.logic", _roles({k: wired for k in range(4)}) + (
243
+ Offered(6, 0, "oep.fixture.logic", _roles({k: wired for k in range(4)}) + (
244
244
  catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))
245
245
  + _capture_decl(["one_shot"], 8, [8], 65536, 1, 480)),
246
- Offered(7, 5, "oep.fixture.i2c-target", _roles({1: wired, 2: wired}) + (
246
+ Offered(7, 0, "oep.fixture.i2c-target", _roles({1: wired, 2: wired}) + (
247
247
  catalog.u16(MAX_LENGTH, 16), catalog.u32(MAX_CLOCK_HZ, 100_000), catalog.u8(IMPLEMENTATION, 2))),
248
248
  # Two fixed pin sets (an example of channel_group; GPIO23 is the DUT's NRST on this jig).
249
- Offered(8, 6, "oep.fixture.spi-target", (
249
+ Offered(8, 0, "oep.fixture.spi-target", (
250
250
  catalog.channel_group(1, [(1, 18), (2, 19), (3, 5), (4, 4)]),
251
251
  catalog.channel_group(2, [(1, 14), (2, 13), (3, 27), (4, 26)]),
252
252
  catalog.u16(MAX_LENGTH, 32), catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
253
- _config(9, 7, slots_max=1, modes=0b011, storage=1024),
253
+ _config(9, 0, slots_max=1, modes=0b011, storage=1024),
254
254
  ])
255
255
 
256
256
 
@@ -264,17 +264,17 @@ def p4_bench() -> FakeProbe:
264
264
  {2: "A SWDIO", 3: "A SWCLK", 4: "B SWDIO", 5: "B SWCLK", 6: "C SWDIO", 7: "C SWCLK"},
265
265
  _transports([(TRANSPORT["usb_serial_jtag"], 0xFF), (TRANSPORT["vendor_bulk"], 0),
266
266
  (TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)]) + (catalog.u8(CORE_OEP_PID, 1),)),
267
- Offered(1, 1, "oep.wire.rvswd", (
267
+ Offered(1, 0, "oep.wire.rvswd", (
268
268
  catalog.channel_group(1, [(1, 2), (2, 3)]), catalog.channel_group(2, [(1, 4), (2, 5)]),
269
269
  catalog.channel_group(3, [(1, 6), (2, 7)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000),
270
270
  catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 2))),
271
- Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
271
+ Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
272
272
  catalog.u16(MAX_LENGTH, 1000))),
273
- Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
274
- Offered(4, 2, "oep.fixture.gpio", _roles({1: pins})),
275
- Offered(5, 3, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
273
+ Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
274
+ Offered(4, 0, "oep.fixture.gpio", _roles({1: pins})),
275
+ Offered(5, 0, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
276
276
  catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
277
- _config(6, 4, slots_max=4),
277
+ _config(6, 0, slots_max=4),
278
278
  ])
279
279
 
280
280
 
@@ -287,13 +287,13 @@ def rp2350_pins() -> FakeProbe:
287
287
  return FakeProbe("rp2350-pins", 1024, [
288
288
  _core("3.0.0", "rp2350", "e66138935f2b1f2c", 30, reserved, "",
289
289
  {}, _transports([(TRANSPORT["usb_cdc"], 0)])),
290
- Offered(1, 1, "oep.wire.rvswd", _roles({1: pins, 2: pins, 3: pins}) + (
290
+ Offered(1, 0, "oep.wire.rvswd", _roles({1: pins, 2: pins, 3: pins}) + (
291
291
  catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
292
- Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
292
+ Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
293
293
  catalog.u16(MAX_LENGTH, 1000))),
294
- Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
295
- Offered(4, 2, "oep.fixture.gpio", _roles({1: pins})),
296
- Offered(5, 3, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
294
+ Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
295
+ Offered(4, 0, "oep.fixture.gpio", _roles({1: pins})),
296
+ Offered(5, 0, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
297
297
  catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
298
298
  ])
299
299
 
@@ -83,7 +83,7 @@ def build(a: argparse.Namespace) -> endpoint.Endpoint:
83
83
  0, 0, mech, len(raw)) + raw + b"\x00" # no speed ceiling, rests high, no lock
84
84
  items.append(bytes([_ITEM["slot"], len(value)]) + value)
85
85
  if a.bind:
86
- streams = b"".join(struct.pack("<BH", reg.PROBE_CONFIG.enum["bind_stream"]["slot_console"], n)
86
+ streams = b"".join(struct.pack("<BBH", 3, reg.PROBE_CONFIG.enum["bind_stream"]["slot_console"], n)
87
87
  for n in range(len(a.slot)))
88
88
  value = struct.pack("<BBBB", a.port_index, _MODES[a.bind.replace("-", "_")], 0, len(a.slot)) + streams
89
89
  items.append(bytes([_ITEM["bind"], len(value)]) + value)
@@ -446,7 +446,7 @@ def open_host(target: str, timeout: float = 3.0):
446
446
  lk.transport = "tcp"
447
447
  elif target == "usb" or target.startswith("usb:"):
448
448
  parts = target.split(":")[1:]
449
- if len(parts) == 1 and len(parts[0]) != 4: # usb:UNIT_ID (a VID is 4 hex digits)
449
+ if len(parts) == 1: # usb:UNIT_ID (any length: a VID comes with its PID)
450
450
  vid, pid = find_usb(parts[0])
451
451
  return open_usb_host(vid, pid, parts[0], timeout)
452
452
  vid = int(parts[0], 16) if parts else USB_VID
@@ -103,8 +103,8 @@ def test_p4_follows_the_agreed_names():
103
103
  by_name = {o.entry.name: o for o in caps.offers}
104
104
  assert len(caps.offers) == 13
105
105
  assert caps.requests["list"] == 1 and caps.requests["describe"] == 13
106
- # the debug port: wire, riscv-dm and console are one instance
107
- assert {by_name[n].entry.instance for n in ("oep.wire.rvswd", "oep.target.riscv-dm", "oep.target.console")} == {1}
106
+ # the debug port: wire, riscv-dm and console are the first instance of each
107
+ assert {by_name[n].entry.instance for n in ("oep.wire.rvswd", "oep.target.riscv-dm", "oep.target.console")} == {0}
108
108
  assert by_name["oep.wire.rvswd"].description.groups[1] == [(1, 2), (2, 54)]
109
109
  i2c = by_name["oep.fixture.i2c-target"]
110
110
  assert i2c.description.roles[1] == i2c.description.roles[2]
@@ -153,7 +153,7 @@ def test_unknown_interfaces_are_shown_raw():
153
153
  def test_text_and_json_outputs():
154
154
  caps = dump.collect(fake.p4_x035().call)
155
155
  text = dump.to_text(caps)
156
- assert "instance 6" in text and "oep.fixture.i2c-target" in text
156
+ assert "instance 0" in text and "oep.fixture.i2c-target" in text
157
157
  assert "features: preloaded tx, clock stretching" in text
158
158
  data = json.loads(dump.to_json(caps))
159
159
  assert data["max_frame"] == 1024 and len(data["interfaces"]) == 13
@@ -59,7 +59,7 @@ def slot_item(n, wire, pair, attach=ATTACH["at_boot"], retry=1, mech=2, name=Non
59
59
 
60
60
  def bind_item(port, mode, streams, selected=0):
61
61
  return m.tlv(ITEM["bind"], struct.pack("<BBBB", port, mode, selected, len(streams))
62
- + b"".join(struct.pack("<BH", k, i) for k, i in streams))
62
+ + b"".join(struct.pack("<BBH", 3, k, i) for k, i in streams))
63
63
 
64
64
 
65
65
  def describe(ep, fn):
@@ -204,6 +204,16 @@ def test_slot_names_are_url_safe(name):
204
204
 
205
205
  # ---- binds and the serial port ---------------------------------------------------------------------------
206
206
 
207
+ def test_bind_streams_carry_a_len_and_a_longer_ones_tail_is_skipped():
208
+ ep, h = bench()
209
+ head = struct.pack("<BBBB", 0, MODE["mixed"], 0, 1)
210
+ longer = m.tlv(ITEM["bind"], head + struct.pack("<BBHH", 5, KIND["slot_console"], 0, 0xBEEF))
211
+ assert h.raw(6, 0x02, slot_item(0, 1, ep.pairs[1][0]) + longer).succeeded
212
+ assert ep.binds[0].streams == ((KIND["slot_console"], 0),)
213
+ short = m.tlv(ITEM["bind"], head + struct.pack("<BBH", 2, KIND["slot_console"], 0))
214
+ assert h.raw(6, 0x02, short).detail == m.MALFORMED
215
+
216
+
207
217
  def framed(req):
208
218
  return cobs.frame(req.pack())
209
219