oep-client-python 0.0.23__tar.gz → 0.0.24__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 (54) hide show
  1. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/CHANGELOG.md +10 -0
  2. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/PKG-INFO +31 -2
  3. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/README.ja.md +28 -1
  4. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/README.md +30 -1
  5. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/pyproject.toml +1 -1
  6. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/__init__.py +1 -1
  7. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/__main__.py +38 -1
  8. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/config.py +21 -5
  9. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/core.py +3 -1
  10. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/endpoint.py +230 -11
  11. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/fake.py +3 -2
  12. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/fake_serial.py +81 -2
  13. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/fake_serve.py +14 -0
  14. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/link.py +372 -11
  15. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/registry.py +4 -4
  16. oep_client_python-0.0.24/tests/test_config_disable.py +212 -0
  17. oep_client_python-0.0.24/tests/test_port_speed.py +290 -0
  18. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/.gitignore +0 -0
  19. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/LICENSE +0 -0
  20. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/arm.py +0 -0
  21. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/capture.py +0 -0
  22. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/catalog.py +0 -0
  23. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/ch32_flash.py +0 -0
  24. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/cobs.py +0 -0
  25. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/console.py +0 -0
  26. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/decode.py +0 -0
  27. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/dump.py +0 -0
  28. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/fake_capture.py +0 -0
  29. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/fixture.py +0 -0
  30. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/frames.py +0 -0
  31. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/hid_stream.py +0 -0
  32. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/host.py +0 -0
  33. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/interfaces.py +0 -0
  34. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/message.py +0 -0
  35. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/names.py +0 -0
  36. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/riscv.py +0 -0
  37. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/rp2350.py +0 -0
  38. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/uiapduino.py +0 -0
  39. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/src/oep_client/usb_stream.py +0 -0
  40. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_capabilities.py +0 -0
  41. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_capture.py +0 -0
  42. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_cobs.py +0 -0
  43. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_config.py +0 -0
  44. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_fake_capture.py +0 -0
  45. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_fake_spec.py +0 -0
  46. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_fixture_targets.py +0 -0
  47. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_flash_console.py +0 -0
  48. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_hid_stream.py +0 -0
  49. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_interfaces.py +0 -0
  50. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_link_host.py +0 -0
  51. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_link_serial.py +0 -0
  52. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_session.py +0 -0
  53. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_target_parts.py +0 -0
  54. {oep_client_python-0.0.23 → oep_client_python-0.0.24}/tests/test_usb_stream.py +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.24
6
+ - (EN) A serial link whose port a session holds sends a request again as soon as a broken frame arrives, instead of after the timeout: the probe sends no raw bytes on a held port (oep-core §3.4), so the broken frame was the reply, and the repeat is answered from the probe's retry table. A 60 KB logic capture read on an M5Stack ATOM went from 4.0 to 8.3 KB/s at 115200 and runs at 76 KB/s at 1.5 Mbaud (dogfooding port_speed, 2026-10-01).
7
+ - (JA) セッションが口を持っているシリアルの経路では、壊れたフレームが来たら時間切れを待たずにすぐ送り直す: 持たれている口には probe が生のバイトを送らない(oep-core §3.4)ので、壊れたフレームは応答で、送り直しには probe の送り直しの表が答える。M5Stack ATOM で 60 KB のロジックのキャプチャの読み出しが 115200 で 4.0 → 8.3 KB/s、1.5 Mbaud で 76 KB/s になった(port_speed のドッグフーディング、2026-10-01)。
8
+ - (EN) `raise_speed` (used on an M5Stack ATOM, 2026-10-01): waits 20 ms after a baud change and finds the new rate with a confirm before measuring; verifies pipelined first and, when frames break with both ways busy (the ATOM's FTDI at 500 kbaud and up), once more one request at a time, then keeps that limit on the link (`SpeedTrial.inflight`, `link.inflight_cap`); a rate whose confirm never comes back says so. The ATOM now commits 1.5 Mbaud at 76 / 58 KB/s (in flight 1) where the first version rejected every rate.
9
+ - (JA) `raise_speed`(M5Stack ATOM で使った、2026-10-01): 速さを変えたら 20 ms 待ち、測る前に confirm で新しい速さを確かめる。まずパイプラインで確かめ、両方向が同時に動くとフレームが壊れる線(ATOM の FTDI の 500 kbaud 以上)では 1 つずつでもう一度確かめ、その上限を link に残す(`SpeedTrial.inflight`、`link.inflight_cap`)。confirm が返らない速さはそう書く。最初の版はどの速さも断ったが、今は ATOM で 1.5 Mbaud が 76 / 58 KB/s(同時 1)で決まる。
10
+ - (EN) port_speed (oep-spec 3b9311c, core §3.5, fn 0 op 0x14; registry synced), opt-in. `link.raise_speed(hst, rates, *, verify_bytes=32768, verify_s=1.0, verify_ms=None, idle_ms=0, port=None)` tries the rates in order on the UART bridge this host opened: try -> the host switches its baud -> link_source / link_sink both ways with max_frame-sized frames (pipelined) for verify_bytes or verify_s, broken frames counted and KB/s measured each way -> commit when nothing broke, else revert (step 2) and back to the boot speed, re-confirmed (waiting out verify_ms). The first rate that passes is kept. The `SpeedReport` (`rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials` of `SpeedTrial`, `to_text()`) is kept as `link.speed`. `open_host(target, ..., baud=115200, port_speed=None | [rates], lease_ms=3000, owner=None)`: with port_speed it takes the lock and runs it (the session stays open). A completed `end` or revert puts the link back at the boot speed at once; a request unanswered at a raised rate (its resend too) goes back to the boot speed, confirms and goes once more (`link.speed_lost`). A probe without it (no describe port_speed, or unknown_operation) is reported not supported and stays at its speed; TCP / USB links are not this host's serial port. Serial ports open in pyserial's low-latency mode where the driver has it. `oep speed <probe> <rate,rate,...>` prints the report. Fake: the op and its state machine (try / commit / revert, verify_ms, idle_ms, a broken candidate while trying, 3 within 1 s when committed, the session's end, wrong port cause 6, unmakeable rates unsupported), on in `esp32-v003` (`Endpoint.port_speed_base`, None = off), the line modelled by `Endpoint.broken_rates` (`BrokenRate(min_size, to_host, to_probe)`), `fake_serial.FakeSerialStream` (an in-process pyserial-shaped port that garbles while the host's rate differs from the probe's), `fake_serve --no-port-speed` / `--broken-rate RATE[:MIN][:in|out]`.
11
+ - (JA) port_speed(oep-spec 3b9311c、core §3.5、fn 0 の op 0x14。registry を写し直した)、使うときだけ。`link.raise_speed(hst, rates, *, verify_bytes=32768, verify_s=1.0, verify_ms=None, idle_ms=0, port=None)` は、この host が開いた UART bridge で速さを順に試す: 試す → host が baud を切り替える → 両方向に max_frame の大きさで link_source / link_sink(パイプライン)を verify_bytes か verify_s の分流し、壊れたフレームを数え、向きごとの KB/s を測る → 何も壊れなければ決める、壊れれば戻す(step 2)して起動時の速さに戻り confirm し直す(verify_ms を待つ)。最初に通った速さを使う。`SpeedReport`(`rate`、`chosen`、`in_kb_s` / `out_kb_s`、`SpeedTrial` の `trials`、`to_text()`)は `link.speed` に残る。`open_host(target, ..., baud=115200, port_speed=None | [速さ], lease_ms=3000, owner=None)`: port_speed があればロックを取って行う(セッションは開いたまま)。完了した `end` と戻すでは link もすぐ起動時の速さに戻り、上げた速さで応答の来ない要求(送り直しも)は起動時の速さに戻って confirm し、もう一度送る(`link.speed_lost`)。機能の無い probe(describe に port_speed が無い、または unknown_operation)は not supported と報告し、速さは変えない。TCP / USB の link はこの host のシリアルの口ではない。シリアルの口はドライバにあれば pyserial の low-latency のモードで開く。`oep speed <probe> <速さ,速さ,...>` は結果を出す。偽の probe: op と状態の遷移(試す / 決める / 戻す、verify_ms、idle_ms、試しの間の壊れた候補、決めた後の 1 秒に 3 つ、セッションの終わり、違う口は cause 6、作れない速さは unsupported)。`esp32-v003` で ON(`Endpoint.port_speed_base`、None で OFF)。線は `Endpoint.broken_rates`(`BrokenRate(min_size, to_host, to_probe)`)で表す。`fake_serial.FakeSerialStream`(同じプロセスの pyserial の形の口。host の速さが probe と違う間は壊す)、`fake_serve --no-port-speed` / `--broken-rate RATE[:MIN][:in|out]`。
12
+ - (EN) `oep.probe.config` item `disable` (0x07, oep-spec 6088438; registry synced): `config.Disable(channel)`, removed with `remove("disable", ch)` / `unset([("disable", ch)])`, in the canonical hash; CLI `oep config disable <probe> <channel...> [--save]`. `core.PinsTaken` names a disabled channel. The fake probe follows it: any request naming a disabled channel (plan_apply, a settings plan, a slot's pins, an attach's pins / reset TLV, scan pairs, gpio set / read) is rejected unavailable cause 5 with the channel; scan's count-0 list and a pinless attach skip it; disabling a channel in use (a plan, a connection, a slot) is cause 1; idle and disable on one channel is malformed; the free pins' idle state (`Endpoint.parked`, set at boot after the saved items and whenever a pin is released) never touches a disabled channel; describe is unchanged. Following oep-spec 4842e95 (registry resynced): a cause 5 refusal for a disabled channel carries holder_kind 6 `disabled` (`Unavailable.holder_kind == "disabled"`), and disabling a channel the probe does not declare is rejected unsupported (as idle).
13
+ - (JA) `oep.probe.config` の項目 `disable`(0x07、oep-spec 6088438。registry を写し直した): `config.Disable(channel)`。`remove("disable", ch)` / `unset([("disable", ch)])` で消す。正規形の hash に入る。CLI `oep config disable <probe> <channel...> [--save]`。`core.PinsTaken` は無効にした channel を示す。fake の probe も追随した: 無効にした channel を指す要求(plan_apply、設定の plan、スロットのピン、attach の pins / reset TLV、scan の組、gpio の set / read)は channel 付きの rejected unavailable cause 5。scan の count 0 の並びとピンを指さない attach はそれを飛ばす。使われている channel(plan、接続、スロット)を無効にする set は cause 1。同じ channel の idle と disable は malformed。空きのピンの状態(`Endpoint.parked`。起動時は保存の項目の後に、ピンが解放されるたびに掛ける)は無効にした channel に触れない。describe は変わらない。oep-spec 4842e95 に追随(registry を写し直した): 無効にした channel の cause 5 の断りは holder_kind 6 `disabled` を持ち(`Unavailable.holder_kind == "disabled"`)、probe が宣言していない channel の disable は rejected unsupported(idle と同じ)。
14
+
5
15
  ## 0.0.23
6
16
  - (EN) `link.open_host("tcp://…")` waits 15 s for a reply and does not send a request again by itself (`resend=False`): behind ch32rv's broker, the broker retries towards the probe, and a second copy from the client only raced it (V003 jig, 2026-10-01). `open_host(..., timeout=, resend=)` set both explicitly; serial and USB keep 3 s and one resend.
7
17
  - (JA) `link.open_host("tcp://…")` は応答を 15 秒待ち、自分では送り直さない(`resend=False`)。ch32rv のブローカーの後ろでは、ブローカーが probe に送り直すので、client からの 2 通目は競合するだけだった(V003 ジグ、2026-10-01)。`open_host(..., timeout=, resend=)` で明示できる。シリアルと USB は 3 秒と 1 回の送り直しのまま。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.23
3
+ Version: 0.0.24
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
@@ -101,6 +101,7 @@ oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --
101
101
  oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
102
102
  oep config uart <probe> oep.fixture.uart 115200 --format 8N1 # applied whenever that UART's plan has RX or TX
103
103
  oep config save <probe> # kept over a restart (also: remove = unset, erase)
104
+ oep speed <probe> 1500000,921600,500000 # port_speed: try faster rates on a UART bridge, print the report (below)
104
105
  ```
105
106
 
106
107
  `<probe>` is a serial port, `tcp://HOST:PORT` or `usb[:VID:PID[:SERIAL]]`. A change takes the lock (owner "oep config") and
@@ -108,6 +109,32 @@ ends the session after it; it takes effect at once and, after `save`, stays over
108
109
 
109
110
  A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`, `oep_probe_checks.py`).
110
111
 
112
+ ## A faster UART bridge (port_speed, opt-in)
113
+
114
+ A probe whose describe declares `port_speed` (oep-core §3.5; the classic ESP32 reference firmware does) lets the host
115
+ run its UART bridge faster than the boot speed (115200) for one session. Nothing changes unless the host asks:
116
+
117
+ ```python
118
+ hst = link.open_host("/dev/ttyUSB0", port_speed=[1500000, 921600, 500000]) # takes the lock, keeps the session open
119
+ print(hst.link.speed.to_text()) # every rate tried: actual, in / out KB/s, broken frames, the one in force
120
+ # or, in a session already taken: report = link.raise_speed(hst, [1500000, 921600], verify_bytes=32768, verify_s=1.0)
121
+ ```
122
+
123
+ Each rate in order: `try` (answered at the speed now, then the probe switches) -> the host switches -> a verify both ways
124
+ with max_frame-sized frames (link_source / link_sink, pipelined) for `verify_bytes` or `verify_s`, counting broken frames
125
+ and measuring KB/s each way -> `commit` when nothing broke, else revert and back to the boot speed, confirmed there (a
126
+ probe whose revert was lost waits out its `verify_ms`). The first rate that passes is kept; a rate the probe's UART cannot
127
+ make is skipped. Which rates pass depends on the bridge chip and its driver (an FTDI took only 3 MHz / n, a CH340 921600
128
+ but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), so the host chooses them. The report
129
+ (`link.speed`: `rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials`) is there to budget a capture or a write.
130
+
131
+ The probe goes back to the boot speed by itself when the session ends (`end`, a lapse, a force), when frames break or the
132
+ line goes quiet (`idle_ms`): the link follows an `end` at once, and a request that goes unanswered at a raised rate (its
133
+ resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges. A probe
134
+ without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
135
+ client. Serial ports are also opened in the driver's low-latency mode where it has one (an FTDI's latency timer 16 -> 1 ms
136
+ tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's profile is not 115200.
137
+
111
138
  ## The fake probe (a working spec)
112
139
 
113
140
  `endpoint.Endpoint` is a fake probe that answers as oep-spec says (2026-10-01: every answer carries the length of its
@@ -140,5 +167,7 @@ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--f
140
167
  `--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
141
168
  so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
142
169
  broken once). `--uart-plan` / `--uart-rx` give the first fixture UART a plan and RX bytes, `--run-hook` a host's own model of
143
- riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. Events and data pushes go out on the pty and on TCP
170
+ riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. port_speed: `esp32-v003` has it
171
+ (`--no-port-speed` turns it off), and `--broken-rate RATE[:MIN_SIZE][:in|out]` makes a rate break frames (in process,
172
+ `fake_serial.FakeSerialStream` also garbles everything while the host's own rate differs from the probe's). Events and data pushes go out on the pty and on TCP
144
173
  (both framings). The rest: `--help`.
@@ -75,12 +75,37 @@ oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --
75
75
  oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
76
76
  oep config uart <probe> oep.fixture.uart 115200 --format 8N1 # その UART の plan に RX / TX が付くたびに掛かる
77
77
  oep config save <probe> # 再起動の後も残す(remove = unset、erase もある)
78
+ oep speed <probe> 1500000,921600,500000 # port_speed: UART bridge の速い速さを試し、結果を出す(下)
78
79
  ```
79
80
 
80
81
  `<probe>` はシリアルの口、`tcp://HOST:PORT`、`usb[:VID:PID[:SERIAL]]`。変更はロックを取り(owner "oep config")、終わったら
81
82
  セッションを閉じる。変更はすぐ効き、`save` の後は再起動しても残る。
82
83
  実機での一通りの確認は ArduinoCore-CH32 の `tests/manual/oep_smoke/`(`oep_smoke.py`、`oep_probe_checks.py`)。
83
84
 
85
+ ## UART bridge を速くする(port_speed、使うときだけ)
86
+
87
+ describe に `port_speed` を宣言する probe(oep-core §3.5。参照の classic ESP32 の firmware は宣言する)では、host は 1 つのセッションの
88
+ 間、UART bridge を起動時の速さ(115200)より速くできます。host が頼まない限り何も変わりません:
89
+
90
+ ```python
91
+ hst = link.open_host("/dev/ttyUSB0", port_speed=[1500000, 921600, 500000]) # ロックを取り、セッションを開いたままにする
92
+ print(hst.link.speed.to_text()) # 試した速さごとに、実際の速さ、in / out の KB/s、壊れたフレーム、今の速さ
93
+ # 取ってあるセッションの中では: report = link.raise_speed(hst, [1500000, 921600], verify_bytes=32768, verify_s=1.0)
94
+ ```
95
+
96
+ 速さごとに順に: `試す`(今の速さで応答してから probe が切り替える)→ host も切り替える → 両方向に max_frame の大きさのフレームで
97
+ (link_source / link_sink、パイプライン)`verify_bytes` か `verify_s` の分だけ確かめ、壊れたフレームを数え、それぞれの向きの KB/s を
98
+ 測る → 何も壊れなければ `決める`、壊れれば戻して起動時の速さに戻り、そこで confirm し直す(戻すが届かなかった probe は `verify_ms`
99
+ を待って戻る)。最初に通った速さを使う。probe の UART が作れない速さは飛ばす。通る速さは変換チップとドライバで決まる(FTDI は
100
+ 3 MHz ÷ n だけ、CH340 は 921600 は通り 1500000 は probe → host が壊れた: oep-spec docs/uart-speed-negotiation.ja.md)ので、速さは
101
+ host が選ぶ。結果(`link.speed`: `rate`、`chosen`、`in_kb_s` / `out_kb_s`、`trials`)はキャプチャや書き込みの予算を立てるのに使う。
102
+
103
+ probe はセッションが終わったとき(`end`、lease の期限切れ、force)、フレームが壊れたとき、線が黙ったとき(`idle_ms`)に自分で
104
+ 起動時の速さに戻る: link は `end` にはすぐ合わせ、上げた速さで応答の来ない要求(送り直しも)があれば、起動時の速さに戻って
105
+ confirm し、そこでもう一度送る(固まらない)。機能の無い probe は `not supported` で、速さは変わらない。ブローカー(TCP)の後ろでは
106
+ client ではなくブローカーが行う。シリアルの口は、ドライバにあれば low-latency のモードで開く(FTDI の latency timer 16 → 1 ms で
107
+ UART bridge の速度が 3 倍になった)。ボードの起動時の速さが 115200 でなければ `open_host(..., baud=)` で渡す。
108
+
84
109
  ## 偽の probe(動く spec)
85
110
 
86
111
  `endpoint.Endpoint` は oep-spec の規範どおりに答える偽の probe で(2026-10-01: 応答のデータと並びは長さを持ち、どの応答にも TLV が
@@ -109,6 +134,8 @@ uv run python -m oep_client.fake_serve --pty --profile p4-bench --slot x035 --bi
109
134
  pty がシリアルの口(host が TIOCEXCL を掛けて開く)、`--tcp PORT` は `--framing cobs`(シリアルの口)か `--framing length`
110
135
  (vendor bulk / TCP の形)。故障の注入は `--drop N`(N 番目の答えを 1 回出さない。要求は実行済みなので送り直しは覚えた答えを
111
136
  受ける)、`--noise TEXT`(答えの前に雑音)、`--corrupt N`(N 番目の答えの CRC を 1 回壊す)。`--capture-slipped` は capture の
112
- 区画すべてに flags bit2 を立てる。出来事とデータの push は pty にも TCP(両方の framing)にも出る。ほかは `--help`。
137
+ 区画すべてに flags bit2 を立てる。port_speed: `esp32-v003` は持つ(`--no-port-speed` で外す)。
138
+ `--broken-rate RATE[:MIN_SIZE][:in|out]` はその速さでフレームを壊す(同じプロセスの `fake_serial.FakeSerialStream` は、host の速さが
139
+ probe の速さと違う間、両方向のバイトをすべて壊す)。出来事とデータの push は pty にも TCP(両方の framing)にも出る。ほかは `--help`。
113
140
 
114
141
  v0 の client(`oep_client.v0`)は 2026-09-26 に消した(git の履歴に残る)。v0 を話す probe はもう無い。
@@ -79,6 +79,7 @@ oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --
79
79
  oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
80
80
  oep config uart <probe> oep.fixture.uart 115200 --format 8N1 # applied whenever that UART's plan has RX or TX
81
81
  oep config save <probe> # kept over a restart (also: remove = unset, erase)
82
+ oep speed <probe> 1500000,921600,500000 # port_speed: try faster rates on a UART bridge, print the report (below)
82
83
  ```
83
84
 
84
85
  `<probe>` is a serial port, `tcp://HOST:PORT` or `usb[:VID:PID[:SERIAL]]`. A change takes the lock (owner "oep config") and
@@ -86,6 +87,32 @@ ends the session after it; it takes effect at once and, after `save`, stays over
86
87
 
87
88
  A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`, `oep_probe_checks.py`).
88
89
 
90
+ ## A faster UART bridge (port_speed, opt-in)
91
+
92
+ A probe whose describe declares `port_speed` (oep-core §3.5; the classic ESP32 reference firmware does) lets the host
93
+ run its UART bridge faster than the boot speed (115200) for one session. Nothing changes unless the host asks:
94
+
95
+ ```python
96
+ hst = link.open_host("/dev/ttyUSB0", port_speed=[1500000, 921600, 500000]) # takes the lock, keeps the session open
97
+ print(hst.link.speed.to_text()) # every rate tried: actual, in / out KB/s, broken frames, the one in force
98
+ # or, in a session already taken: report = link.raise_speed(hst, [1500000, 921600], verify_bytes=32768, verify_s=1.0)
99
+ ```
100
+
101
+ Each rate in order: `try` (answered at the speed now, then the probe switches) -> the host switches -> a verify both ways
102
+ with max_frame-sized frames (link_source / link_sink, pipelined) for `verify_bytes` or `verify_s`, counting broken frames
103
+ and measuring KB/s each way -> `commit` when nothing broke, else revert and back to the boot speed, confirmed there (a
104
+ probe whose revert was lost waits out its `verify_ms`). The first rate that passes is kept; a rate the probe's UART cannot
105
+ make is skipped. Which rates pass depends on the bridge chip and its driver (an FTDI took only 3 MHz / n, a CH340 921600
106
+ but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), so the host chooses them. The report
107
+ (`link.speed`: `rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials`) is there to budget a capture or a write.
108
+
109
+ The probe goes back to the boot speed by itself when the session ends (`end`, a lapse, a force), when frames break or the
110
+ line goes quiet (`idle_ms`): the link follows an `end` at once, and a request that goes unanswered at a raised rate (its
111
+ resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges. A probe
112
+ without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
113
+ client. Serial ports are also opened in the driver's low-latency mode where it has one (an FTDI's latency timer 16 -> 1 ms
114
+ tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's profile is not 115200.
115
+
89
116
  ## The fake probe (a working spec)
90
117
 
91
118
  `endpoint.Endpoint` is a fake probe that answers as oep-spec says (2026-10-01: every answer carries the length of its
@@ -118,5 +145,7 @@ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--f
118
145
  `--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
119
146
  so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
120
147
  broken once). `--uart-plan` / `--uart-rx` give the first fixture UART a plan and RX bytes, `--run-hook` a host's own model of
121
- riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. Events and data pushes go out on the pty and on TCP
148
+ riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. port_speed: `esp32-v003` has it
149
+ (`--no-port-speed` turns it off), and `--broken-rate RATE[:MIN_SIZE][:in|out]` makes a rate break frames (in process,
150
+ `fake_serial.FakeSerialStream` also garbles everything while the host's own rate differs from the probe's). Events and data pushes go out on the pty and on TCP
122
151
  (both framings). The rest: `--help`.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oep-client-python"
7
- version = "0.0.23"
7
+ version = "0.0.24"
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.23"
5
+ __version__ = "0.0.24"
@@ -7,7 +7,9 @@
7
7
  oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
8
8
  oep config plan <probe> oep.fixture.uart#1 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
9
9
  oep config uart <probe> oep.fixture.uart#1 115200 --format 8N1
10
+ oep config disable <probe> 3 4 (channels the probe never uses or touches; remove disable CH re-enables)
10
11
  oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
12
+ oep speed <probe> 1500000,921600,500000 (port_speed, core §3.5: try the rates in order on a UART bridge, report)
11
13
 
12
14
  <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
13
15
  ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
@@ -28,6 +30,13 @@ def main(argv=None) -> int:
28
30
  parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
29
31
  sub = parser.add_subparsers(dest="command", required=True)
30
32
  _config_parser(sub)
33
+ sp = sub.add_parser("speed", help="port_speed (core §3.5): try faster rates on a UART bridge, print the report")
34
+ sp.add_argument("probe", help="the probe's serial port (a UART bridge this host opens)")
35
+ sp.add_argument("rates", help="comma-separated, in order of preference (the first that passes is kept)")
36
+ sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
37
+ sp.add_argument("--verify-bytes", type=int, default=32768)
38
+ sp.add_argument("--verify-s", type=float, default=1.0)
39
+ sp.add_argument("--json", action="store_true")
31
40
  d = sub.add_parser("dump", help="list and describe every interface a probe offers")
32
41
  src = d.add_mutually_exclusive_group(required=True)
33
42
  src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
@@ -38,6 +47,8 @@ def main(argv=None) -> int:
38
47
  args = parser.parse_args(argv)
39
48
  if args.command == "config":
40
49
  return _config(args)
50
+ if args.command == "speed":
51
+ return _speed(args)
41
52
 
42
53
  if args.fake:
43
54
  call = fake.PROFILES[args.fake]().call
@@ -49,6 +60,26 @@ def main(argv=None) -> int:
49
60
  return 0
50
61
 
51
62
 
63
+ # ---- oep speed ----------------------------------------------------------------------------------------------------
64
+
65
+ def _speed(args) -> int:
66
+ """Take the lock, raise_speed, print the report; the session's end puts the port back at its boot speed."""
67
+ import dataclasses
68
+ rates = [int(r) for r in args.rates.replace(" ", "").split(",") if r]
69
+ hst = link.open_host(args.probe, baud=args.baud)
70
+ try:
71
+ core.take(hst, 5000, owner="oep speed")
72
+ report = link.raise_speed(hst, rates, verify_bytes=args.verify_bytes, verify_s=args.verify_s)
73
+ if args.json:
74
+ sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
75
+ else:
76
+ sys.stdout.write(report.to_text())
77
+ hst.end()
78
+ finally:
79
+ hst.link.close()
80
+ return 0 if report.supported else 2
81
+
82
+
52
83
  # ---- oep config ----------------------------------------------------------------------------------------------------
53
84
 
54
85
  def _config_parser(sub) -> None:
@@ -98,13 +129,17 @@ def _config_parser(sub) -> None:
98
129
  idle.add_argument("channel", type=int)
99
130
  idle.add_argument("mode", choices=sorted(config.IDLE))
100
131
  idle.add_argument("--save", action="store_true")
132
+ dis = cs.add_parser("disable", help="channels the probe never uses or touches (not on this board)")
133
+ dis.add_argument("probe")
134
+ dis.add_argument("channels", type=int, nargs="+")
135
+ dis.add_argument("--save", action="store_true")
101
136
  uart = cs.add_parser("uart", help="a fixture UART's baud / format, applied whenever its plan has RX or TX")
102
137
  uart.add_argument("probe")
103
138
  uart.add_argument("fn", help="an fn, or name#instance of the oep.fixture.uart")
104
139
  uart.add_argument("baud", type=int)
105
140
  uart.add_argument("--format", default="8N1", help="data bits, parity, stop bits: 8N1 (default), 8E1, 7O2 ...")
106
141
  uart.add_argument("--save", action="store_true")
107
- rm = cs.add_parser("remove", help="remove one item (unset): slot N, bind PORT, plan FN, label CH, idle CH, uart FN")
142
+ rm = cs.add_parser("remove", help="remove one item (unset): slot N, bind PORT, plan FN, label CH, idle CH, uart FN, disable CH")
108
143
  rm.add_argument("probe")
109
144
  rm.add_argument("kind", choices=sorted(config.ITEM))
110
145
  rm.add_argument("key", type=int)
@@ -235,6 +270,8 @@ def _config(args) -> int:
235
270
  _change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
236
271
  elif args.action == "idle":
237
272
  _change(hst, cfg, [config.Idle(channel=args.channel, mode=args.mode)], args.save)
273
+ elif args.action == "disable":
274
+ _change(hst, cfg, [config.Disable(channel=ch) for ch in args.channels], args.save)
238
275
  elif args.action == "remove":
239
276
  _change(hst, cfg, [config.remove(args.kind, args.key)], args.save)
240
277
  else:
@@ -1,5 +1,5 @@
1
1
  """oep.probe.config revision 1 (oep-spec docs/oep-if-probe-config.ja.md): the probe's settings - plan, labels, idle
2
- pins, slots, binds, fixture UART settings - read and set as items, removed with unset, saved when the host says so, and
2
+ pins, slots, binds, fixture UART settings, disabled channels - read and set as items, removed with unset, saved when the host says so, and
3
3
  the live slot / bind / storage state as its own lock-free operation (describe is declarations only, core §7.3).
4
4
 
5
5
  cfg = config.ProbeConfig(hst)
@@ -84,6 +84,20 @@ class Idle:
84
84
  return struct.pack("<HB", self.channel, IDLE[self.mode])
85
85
 
86
86
 
87
+ @dataclass(kw_only=True)
88
+ class Disable:
89
+ """A channel the probe never uses or touches (probe.config §1, item 0x07): not on this board, or wired to another
90
+ part. Any request naming it is refused unavailable (cause 5, held by settings); describe still declares it."""
91
+ channel: int
92
+ TAG = ITEM["disable"]
93
+
94
+ def key(self) -> tuple:
95
+ return (self.channel,)
96
+
97
+ def value(self) -> bytes:
98
+ return struct.pack("<H", self.channel)
99
+
100
+
87
101
  @dataclass(kw_only=True)
88
102
  class Slot:
89
103
  """A place a target is wired to (probe.config §1.1). pins: (swdio, swclk), swclk 0xFFFF on one wire (swio).
@@ -160,7 +174,7 @@ class Removal:
160
174
  key: int
161
175
 
162
176
  def encoded(self) -> bytes:
163
- """len(u8) tag(u8) key: the key is fn(u16) for plan / uart, channel(u16) for label / idle, slot(u8), port(u8)."""
177
+ """len(u8) tag(u8) key: the key is fn(u16) for plan / uart, channel(u16) for label / idle / disable, slot(u8), port(u8)."""
164
178
  tag = ITEM[self.kind]
165
179
  key = bytes([self.key]) if self.kind in ("slot", "bind") else struct.pack("<H", self.key)
166
180
  return bytes([1 + len(key), tag]) + key
@@ -172,7 +186,7 @@ def item(it) -> bytes:
172
186
 
173
187
  def remove(kind: str, key: int) -> Removal:
174
188
  """The removal of the item of this key (for `unset`, or in a `set` list): kind plan (key fn: its whole plan), label
175
- / idle (channel), slot, bind (port), uart (fn)."""
189
+ / idle / disable (channel), slot, bind (port), uart (fn)."""
176
190
  if kind not in ITEM:
177
191
  raise ValueError(f"no item kind {kind!r}")
178
192
  return Removal(kind, key)
@@ -187,6 +201,8 @@ def decode(tag: int, v: bytes):
187
201
  return Label(channel=struct.unpack_from("<H", v)[0], text=v[2:].decode("utf-8", "replace"))
188
202
  if tag == ITEM["idle"] and len(v) >= 3:
189
203
  return Idle(channel=struct.unpack_from("<H", v)[0], mode=_name(IDLE, v[2]))
204
+ if tag == ITEM["disable"] and len(v) >= 2:
205
+ return Disable(channel=struct.unpack_from("<H", v)[0])
190
206
  if tag == ITEM["slot"] and len(v) >= SLOT_HEAD.size + 1:
191
207
  n, wire_fn, swdio, swclk, attach, retry_ms, max_speed, idle, mech, name_len = SLOT_HEAD.unpack_from(v)
192
208
  at = SLOT_HEAD.size
@@ -219,7 +235,7 @@ def decode(tag: int, v: bytes):
219
235
 
220
236
  def _sort_key(tag: int, value: bytes) -> tuple:
221
237
  """The canonical order's key of one item (probe.config §2): tag, then the key - plan (fn, role, channel), label /
222
- idle channel, slot, port, uart fn."""
238
+ idle / disable channel, slot, port, uart fn."""
223
239
  if tag == ITEM["plan"] and len(value) >= 5:
224
240
  return (tag,) + struct.unpack_from("<HBH", value)
225
241
  if tag in (ITEM["slot"], ITEM["bind"]):
@@ -299,7 +315,7 @@ class ProbeConfig(Interface):
299
315
  return h, out
300
316
 
301
317
  def items(self) -> list:
302
- """The current settings, decoded (Plan, Label, Idle, Slot, Bind, Uart)."""
318
+ """The current settings, decoded (Plan, Label, Idle, Slot, Bind, Uart, Disable)."""
303
319
  return [decode(t, v) for t, v in self.get()[1]]
304
320
 
305
321
  def set(self, items: list) -> int:
@@ -140,7 +140,7 @@ def plan_apply(hst: h.Host, assignments: list[tuple[int, int, int]]) -> None:
140
140
 
141
141
  class PinsTaken(h.Rejected):
142
142
  """plan_apply refused (unavailable) and the probe's settings say why: a pin kept by another fn's saved plan or by a
143
- slot (oep.probe.config). `holders`: (channel, what holds it)."""
143
+ slot, or a channel the settings disable (oep.probe.config). `holders`: (channel, what holds it)."""
144
144
 
145
145
  def __init__(self, result, holders: list[tuple[int, str]]):
146
146
  super().__init__(result)
@@ -167,6 +167,8 @@ def _pin_holders(hst: h.Host, assignments: list[tuple[int, int, int]]) -> list[t
167
167
  out.append((ch, f"the saved plan of fn {it.fn} (role {it.role})"))
168
168
  elif isinstance(it, config.Slot) and ch in it.pins:
169
169
  out.append((ch, f"slot {it.slot} {it.name}"))
170
+ elif isinstance(it, config.Disable) and it.channel == ch:
171
+ out.append((ch, "the settings (disabled; oep config remove <probe> disable <channel> enables it)"))
170
172
  return out
171
173
 
172
174