oep-client-python 0.0.25__tar.gz → 0.0.26__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.25 → oep_client_python-0.0.26}/CHANGELOG.md +6 -0
  2. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/PKG-INFO +8 -3
  3. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/README.ja.md +5 -2
  4. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/README.md +7 -2
  5. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/pyproject.toml +1 -1
  6. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/__init__.py +1 -1
  7. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/__main__.py +3 -1
  8. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/arm.py +1 -1
  9. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/capture.py +3 -2
  10. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/config.py +3 -1
  11. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/core.py +15 -5
  12. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/endpoint.py +24 -4
  13. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/fake_serial.py +34 -5
  14. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/fake_serve.py +7 -4
  15. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/host.py +30 -5
  16. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/link.py +220 -65
  17. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/riscv.py +20 -3
  18. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_port_speed.py +126 -1
  19. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/.gitignore +0 -0
  20. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/LICENSE +0 -0
  21. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/catalog.py +0 -0
  22. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/ch32_flash.py +0 -0
  23. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/cobs.py +0 -0
  24. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/console.py +0 -0
  25. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/decode.py +0 -0
  26. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/dump.py +0 -0
  27. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/fake.py +0 -0
  28. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/fake_capture.py +0 -0
  29. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/fixture.py +0 -0
  30. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/frames.py +0 -0
  31. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/hid_stream.py +0 -0
  32. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/interfaces.py +0 -0
  33. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/message.py +0 -0
  34. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/names.py +0 -0
  35. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/registry.py +0 -0
  36. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/rp2350.py +0 -0
  37. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/uiapduino.py +0 -0
  38. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/src/oep_client/usb_stream.py +0 -0
  39. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_capabilities.py +0 -0
  40. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_capture.py +0 -0
  41. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_cobs.py +0 -0
  42. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_config.py +0 -0
  43. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_config_disable.py +0 -0
  44. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_fake_capture.py +0 -0
  45. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_fake_spec.py +0 -0
  46. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_fixture_targets.py +0 -0
  47. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_flash_console.py +0 -0
  48. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_hid_stream.py +0 -0
  49. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_interfaces.py +0 -0
  50. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_link_host.py +0 -0
  51. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_link_serial.py +0 -0
  52. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_session.py +0 -0
  53. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_target_parts.py +0 -0
  54. {oep_client_python-0.0.25 → oep_client_python-0.0.26}/tests/test_usb_stream.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.26
6
+ - (EN) A request that may take long on the probe is waited for that long (core §6.1: the probe does not count the lease while it runs one): `Host.expecting(ms)` (and `request(..., expect_ms=)` / `call(..., expect_ms=)`) makes the link wait at least that and 0.5 s (`link.EXPECT_MARGIN_S`) for each answer, at any rate - so the raised-rate wait of a quarter of the lease applies to ordinary requests only. Passed down by riscv-dm run (`timeout_ms`), dmi (its wait_us and poll_us steps, `riscv.dmi_wait_ms`), attach with a reset (`hold_ms`, riscv and arm), capture start (the configured `blocking_ms`; a group's the longest of its tracks) and probe.config save (`config.SAVE_EXPECT_MS`, 2 s). Such a request that times out at a raised rate still falls back. The link's own confirms, keepalives, the step down's revert and `pump` never wait longer. Fix: the keepalive a raised link sends after 1 s of quiet now goes out before the request takes its corr (`Host.before_request`); it used to go out with a higher corr first, so the probe took the request for an old one (rejected result_lost).
7
+ - (JA) probe で長くかかりうる要求はその間待つ(core §6.1: probe は要求の実行中 lease を数えない): `Host.expecting(ms)`(と `request(..., expect_ms=)` / `call(..., expect_ms=)`)で、link はどの速さでも応答ごとに少なくともその時間と 0.5 秒(`link.EXPECT_MARGIN_S`)待つ。上げた速さでの lease の 4 分の 1 の待ちは普通の要求だけに掛かる。riscv-dm の run(`timeout_ms`)、dmi(wait_us と poll_us の step の和、`riscv.dmi_wait_ms`)、reset 付きの attach(`hold_ms`、riscv と arm)、capture の start(configure の `blocking_ms`。組はトラックの最長)、probe.config の save(`config.SAVE_EXPECT_MS`、2 秒)が渡す。そうした要求も上げた速さで時間切れなら戻る。link 自身の confirm・keepalive・降りるときの戻す・`pump` は長く待たない。修正: 上げた link が 1 秒黙った後に送る keepalive を、要求が corr を取る前に送るようにした(`Host.before_request`)。これまでは大きい corr で先に出ていたため、probe が要求を古いものとみなした(rejected result_lost)。
8
+ - (EN) port_speed (core §3.5 items 3 and 7), after a V003 jig (classic ESP32 + CH340) passed 921600 each way alone but broke a frame every ~0.5 s under sustained duplex use, the probe reverted by itself and the session's lease lapsed: `raise_speed` adds a duplex phase after the in / out phases - link_source and link_sink (max_frame) interleaved, pipelined at the in-flight that passed, for `duplex_s` (1.0) and at least `duplex_frames` (64); a broken frame fails the rate ("broke both ways at once"). `SpeedTrial.duplex_bytes` / `duplex_kb_s` / `broken_duplex`, `SpeedReport.duplex_kb_s`, a duplex column in `to_text()`, `oep speed --duplex-s`. In use: while a raised rate is in force, the link counts broken frames and resends; 3 within 5 s (`STRIKE_MAX`, `STRIKE_WINDOW_S`) step down - port_speed step 2 at the raised rate (0.2 s, never sent again), the boot speed, a confirm, the request again there. A request at a raised rate that gets no answer at all keeps the old fallback (back, confirm, once more there), but each wait is now at most a quarter of the lease (at least 0.3 s), so it ends well inside the lease. Either way the rate is not used again in that session (`SpeedReport.stepped_down`, `.down_why`, a line in `to_text()`; `raise_speed` skips it, "stepped down from earlier in this session"). Fake: `BrokenRate(duplex=True)` breaks only while a request of `min_size` comes in with such an answer still unread (the request towards the probe, that answer towards the host), `BrokenRate(every=N)` only every Nth frame; `fake_serve --broken-rate RATE[:MIN][:in|out][:duplex][:everyN]`.
9
+ - (JA) port_speed(core §3.5 の 3 と 7)。V003 のジグ(classic ESP32 + CH340)で 921600 が向きごとには通ったのに、両方向を続けて使うと約 0.5 秒ごとにフレームが壊れ、probe が自分で戻り、セッションの lease が切れたため: `raise_speed` は in / out の後に両方向同時の確かめを足した - link_source と link_sink(max_frame)を交互に、通った同時の数で並べ、`duplex_s`(1.0)かつ `duplex_frames`(64)以上。1 つでも壊れればその速さは使わない("broke both ways at once")。`SpeedTrial.duplex_bytes` / `duplex_kb_s` / `broken_duplex`、`SpeedReport.duplex_kb_s`、`to_text()` に duplex の列、`oep speed --duplex-s`。使っている間: 上げた速さの間、link は壊れたフレームと送り直しを数え、5 秒の内に 3 つ(`STRIKE_MAX`、`STRIKE_WINDOW_S`)で降りる - 上げた速さで port_speed の step 2(0.2 秒、送り直さない)、起動時の速さ、confirm、要求をそこでもう一度。上げた速さで応答がまったく来ない要求はこれまでの戻り方(戻る、confirm、そこでもう一度)のままだが、待つ 1 回を lease の 4 分の 1(最短 0.3 秒)までにしたので lease の内に十分収まる。どちらでもその速さはそのセッションの間使わない(`SpeedReport.stepped_down`、`.down_why`、`to_text()` に 1 行。`raise_speed` は飛ばす: "stepped down from earlier in this session")。偽の probe: `BrokenRate(duplex=True)` は `min_size` 以上の要求が、同じ大きさの応答が読まれないうちに来たときだけ壊す(probe へはその要求、host へはその応答)、`BrokenRate(every=N)` は N 個に 1 つだけ壊す。`fake_serve --broken-rate RATE[:MIN][:in|out][:duplex][:everyN]`。
10
+
5
11
  ## 0.0.25
6
12
  - (EN) port_speed's idle limit (oep-spec 877cf01, core §3.5 items 5 and 6; registry synced, `port_speed_idle_max_ms` = 3000): a committed rate goes back after at most 3 s with no good frame, so a host that raised the speed and died leaves it no longer than that. The fake clamps idle_ms the same way (0 and anything longer: the maximum; 0 used to mean never). `raise_speed`'s `idle_ms` defaults to the maximum (0 or more than it: the maximum). While raised, the link sends a keepalive before a request (or a pipelined exchange, or inside a long `pump`) when it has been quiet for 1 s; `SerialLink.keep_alive()` does the same for a caller that sits idle for long (no background thread). `open_host` on a serial port (COBS) retries its first confirm for about 4 s (`OPEN_RETRY_S`, the idle maximum and a second; 0.5 s each) to wait out a rate left over, then raises TimeoutError; other transports keep their timeout.
7
13
  - (JA) port_speed の黙りの上限(oep-spec 877cf01、core §3.5 の 5 と 6。registry を写し直した。`port_speed_idle_max_ms` = 3000): 決めた速さは正しいフレームが来ないまま最長 3 秒で戻るので、速さを上げた host が落ちてもそれより長くは残らない。fake も idle_ms を同じに切り詰める(0 とそれより長い値は最長。0 はこれまで戻らないだった)。`raise_speed` の `idle_ms` の既定は最長(0 と最長より長い値も最長)。上げている間、link は 1 秒黙っていれば要求の前(並べた送り、長い `pump` の中も)に keepalive を送る。長く黙る呼び出し側は `SerialLink.keep_alive()` で同じことをする(裏のスレッドは無い)。シリアルの口(COBS)の `open_host` は最初の confirm を約 4 秒(`OPEN_RETRY_S`、黙りの最長と 1 秒。1 回 0.5 秒)繰り返して残った速さが戻るのを待ち、それでも答えが無ければ TimeoutError。ほかの経路の時間切れは変わらない。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.25
3
+ Version: 0.0.26
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
@@ -122,7 +122,9 @@ print(hst.link.speed.to_text()) # every rate tried: actual, in / out
122
122
 
123
123
  Each rate in order: `try` (answered at the speed now, then the probe switches) -> the host switches -> a verify both ways
124
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
125
+ and measuring KB/s each way -> both ways at once at the in-flight that passed (link_source and link_sink interleaved,
126
+ about 1 s and at least 64 frames, `duplex_kb_s`; a line that carries each way alone can still break under duplex use - a
127
+ CH340 at 921600 did) -> `commit` when nothing broke, else revert and back to the boot speed, confirmed there (a
126
128
  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
129
  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
130
  but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), so the host chooses them. The report
@@ -133,7 +135,10 @@ line goes quiet (`idle_ms`, at most `port_speed_idle_max_ms` = 3 s; a host that
133
135
  that): while raised the link sends a keepalive before a request when it has been quiet for 1 s, and `hst.link.keep_alive()`
134
136
  does the same for a caller that sits idle for long; opening a serial port retries its first confirm for about 4 s to
135
137
  wait out a rate left over. The link follows an `end` at once, and a request that goes unanswered at a raised rate (its
136
- resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges. A probe
138
+ resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges; while raised
139
+ each wait for an answer is at most a quarter of the lease, so this ends well inside it. In use, 3 broken frames or
140
+ resends within 5 s step the link down: port_speed revert at the raised rate, the boot speed, a confirm, the request
141
+ again there. A rate left either way is not used again in that session (`speed.stepped_down`, `speed.down_why`). A probe
137
142
  without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
138
143
  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
139
144
  tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's profile is not 115200.
@@ -95,7 +95,8 @@ print(hst.link.speed.to_text()) # 試した速さごとに、実際
95
95
 
96
96
  速さごとに順に: `試す`(今の速さで応答してから probe が切り替える)→ host も切り替える → 両方向に max_frame の大きさのフレームで
97
97
  (link_source / link_sink、パイプライン)`verify_bytes` か `verify_s` の分だけ確かめ、壊れたフレームを数え、それぞれの向きの KB/s を
98
- 測る → 何も壊れなければ `決める`、壊れれば戻して起動時の速さに戻り、そこで confirm し直す(戻すが届かなかった probe は `verify_ms`
98
+ 測る → 通った同時の数で両方向を同時に(link_source と link_sink を交互に、約 1 秒、64 フレーム以上。`duplex_kb_s`。向きごとには
99
+ 通っても同時に流すと壊れる線がある。921600 の CH340 がそうだった)流す → 何も壊れなければ `決める`、壊れれば戻して起動時の速さに戻り、そこで confirm し直す(戻すが届かなかった probe は `verify_ms`
99
100
  を待って戻る)。最初に通った速さを使う。probe の UART が作れない速さは飛ばす。通る速さは変換チップとドライバで決まる(FTDI は
100
101
  3 MHz ÷ n だけ、CH340 は 921600 は通り 1500000 は probe → host が壊れた: oep-spec docs/uart-speed-negotiation.ja.md)ので、速さは
101
102
  host が選ぶ。結果(`link.speed`: `rate`、`chosen`、`in_kb_s` / `out_kb_s`、`trials`)はキャプチャや書き込みの予算を立てるのに使う。
@@ -104,7 +105,9 @@ probe はセッションが終わったとき(`end`、lease の期限切れ、
104
105
  `port_speed_idle_max_ms` = 3 秒。落ちた host の速さもそれより長くは残らない)に自分で起動時の速さに戻る: 上げている間、link は
105
106
  1 秒黙っていれば要求の前に keepalive を送り、長く黙る呼び出し側は `hst.link.keep_alive()` で同じことをする。シリアルの口を開くと
106
107
  最初の confirm を約 4 秒繰り返して、残った速さが戻るのを待つ。link は `end` にはすぐ合わせ、上げた速さで応答の来ない要求(送り直しも)があれば、起動時の速さに戻って
107
- confirm し、そこでもう一度送る(固まらない)。機能の無い probe は `not supported` で、速さは変わらない。ブローカー(TCP)の後ろでは
108
+ confirm し、そこでもう一度送る(固まらない)。上げている間は応答を待つ 1 回が lease の 4 分の 1 までなので、lease の内に十分収まる。
109
+ 使っている間に 5 秒の内に 3 回フレームが壊れるか送り直すと、上げた速さで port_speed の戻すを送り、起動時の速さに戻って confirm し、
110
+ 要求をそこで送る。どちらで離れた速さもそのセッションの間は使わない(`speed.stepped_down`、`speed.down_why`)。機能の無い probe は `not supported` で、速さは変わらない。ブローカー(TCP)の後ろでは
108
111
  client ではなくブローカーが行う。シリアルの口は、ドライバにあれば low-latency のモードで開く(FTDI の latency timer 16 → 1 ms で
109
112
  UART bridge の速度が 3 倍になった)。ボードの起動時の速さが 115200 でなければ `open_host(..., baud=)` で渡す。
110
113
 
@@ -100,7 +100,9 @@ print(hst.link.speed.to_text()) # every rate tried: actual, in / out
100
100
 
101
101
  Each rate in order: `try` (answered at the speed now, then the probe switches) -> the host switches -> a verify both ways
102
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
103
+ and measuring KB/s each way -> both ways at once at the in-flight that passed (link_source and link_sink interleaved,
104
+ about 1 s and at least 64 frames, `duplex_kb_s`; a line that carries each way alone can still break under duplex use - a
105
+ CH340 at 921600 did) -> `commit` when nothing broke, else revert and back to the boot speed, confirmed there (a
104
106
  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
107
  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
108
  but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), so the host chooses them. The report
@@ -111,7 +113,10 @@ line goes quiet (`idle_ms`, at most `port_speed_idle_max_ms` = 3 s; a host that
111
113
  that): while raised the link sends a keepalive before a request when it has been quiet for 1 s, and `hst.link.keep_alive()`
112
114
  does the same for a caller that sits idle for long; opening a serial port retries its first confirm for about 4 s to
113
115
  wait out a rate left over. The link follows an `end` at once, and a request that goes unanswered at a raised rate (its
114
- resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges. A probe
116
+ resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges; while raised
117
+ each wait for an answer is at most a quarter of the lease, so this ends well inside it. In use, 3 broken frames or
118
+ resends within 5 s step the link down: port_speed revert at the raised rate, the boot speed, a confirm, the request
119
+ again there. A rate left either way is not used again in that session (`speed.stepped_down`, `speed.down_why`). A probe
115
120
  without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
116
121
  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
117
122
  tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's profile is not 115200.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oep-client-python"
7
- version = "0.0.25"
7
+ version = "0.0.26"
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.25"
5
+ __version__ = "0.0.26"
@@ -36,6 +36,7 @@ def main(argv=None) -> int:
36
36
  sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
37
37
  sp.add_argument("--verify-bytes", type=int, default=32768)
38
38
  sp.add_argument("--verify-s", type=float, default=1.0)
39
+ sp.add_argument("--duplex-s", type=float, default=1.0, help="both ways at once for this long (default 1)")
39
40
  sp.add_argument("--json", action="store_true")
40
41
  d = sub.add_parser("dump", help="list and describe every interface a probe offers")
41
42
  src = d.add_mutually_exclusive_group(required=True)
@@ -69,7 +70,8 @@ def _speed(args) -> int:
69
70
  hst = link.open_host(args.probe, baud=args.baud)
70
71
  try:
71
72
  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
+ report = link.raise_speed(hst, rates, verify_bytes=args.verify_bytes, verify_s=args.verify_s,
74
+ duplex_s=args.duplex_s)
73
75
  if args.json:
74
76
  sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
75
77
  else:
@@ -37,7 +37,7 @@ class SwdWire(WireBase):
37
37
  body = bytes([0]) + self._speed_tlv(self._speed_or_default(max_speed)) + self._pins_tlv(pins) + self._reset_tlv(reset)
38
38
  if targetsel is not None:
39
39
  body += m.tlv(self.TAG_TARGETSEL, struct.pack("<I", targetsel), critical=True)
40
- rd = m.Reader(self._call(self.ATTACH, body).payload)
40
+ rd = m.Reader(self._call(self.ATTACH, body, expect_ms=reset[1] if reset else 0).payload)
41
41
  conn, dpidr, self.flags, self.speed_hz = rd.take("HIBI")
42
42
  self.existing = bool(self.flags & self.FLAGS["existing"])
43
43
  rd.tail()
@@ -335,7 +335,7 @@ class LogicCapture(Interface):
335
335
 
336
336
  def start(self) -> int:
337
337
  """-> blocking_ms (0: the probe keeps answering while it captures). self.generation: the new capture's."""
338
- rd = m.Reader(self._call(self.START).payload)
338
+ rd = m.Reader(self._call(self.START, expect_ms=self.config.blocking_ms if self.config else 0).payload)
339
339
  blocking, self.generation = rd.take("II")
340
340
  rd.tail()
341
341
  self.armed_s = time.monotonic()
@@ -576,7 +576,8 @@ class CaptureGroup(Interface):
576
576
  def start(self, tracks: list[LogicCapture] = ()) -> tuple[int, int]:
577
577
  """-> (blocking_ms, the group's start_ns). `tracks`: whose armed_s and generation to set (the answer's TLV
578
578
  generations names each track's new generation; self.generations keeps them by fn)."""
579
- rd = m.Reader(self._call(self.START).payload)
579
+ rd = m.Reader(self._call(self.START, expect_ms=max([t.config.blocking_ms for t in tracks if t.config] or [0]))
580
+ .payload)
580
581
  blocking, start_ns = rd.take("IQ")
581
582
  gens = rd.tail().get(GROUP_GENERATIONS) or b""
582
583
  self.generations = {fn: g for fn, g in struct.iter_unpack("<HI", gens[:len(gens) // 6 * 6])}
@@ -292,6 +292,8 @@ class State:
292
292
  binds: list[BindState] = field(default_factory=list)
293
293
 
294
294
 
295
+ SAVE_EXPECT_MS = 2000 # a save writes the probe's flash: the link waits at least this (Host.expecting)
296
+
295
297
  class ProbeConfig(Interface):
296
298
  NAME = "oep.probe.config"
297
299
  REVISION = 1
@@ -346,7 +348,7 @@ class ProbeConfig(Interface):
346
348
 
347
349
  def save(self) -> int:
348
350
  """Save the current settings (a probe with storage; the whole is replaced). -> the hash saved."""
349
- return self._hash_answer(self._call(self.SAVE))
351
+ return self._hash_answer(self._call(self.SAVE, expect_ms=SAVE_EXPECT_MS))
350
352
 
351
353
  def erase(self) -> None:
352
354
  self._call(self.ERASE)
@@ -4,6 +4,8 @@ entry, and calls that raise unless they worked)."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import contextlib
8
+
7
9
  import struct
8
10
 
9
11
  from . import catalog, host as h, message as m, registry as reg
@@ -196,12 +198,20 @@ class Interface:
196
198
  raise UnsupportedRevision(f"{self.name} (fn {self.fn}) is revision {rev} on this probe; this client "
197
199
  f"speaks revision {self.REVISION} only")
198
200
 
199
- def _call(self, op: int, body: bytes = b"", *, locked: bool = True) -> m.Result:
200
- return self.host.call(self.fn, op, self.prefix + body, locked=locked)
201
+ def _expecting(self, ms: int):
202
+ """Host.expecting(ms) when the host has it (a stand-in host may not)."""
203
+ expecting = getattr(self.host, "expecting", None) if ms else None
204
+ return expecting(ms) if expecting else contextlib.nullcontext()
205
+
206
+ def _call(self, op: int, body: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
207
+ with self._expecting(expect_ms):
208
+ return self.host.call(self.fn, op, self.prefix + body, locked=locked)
201
209
 
202
- def _request(self, op: int, body: bytes = b"", *, locked: bool = True) -> m.Result:
203
- """Rejections raise; completed results of any outcome come back for the caller to decode."""
204
- return self.host.request(self.fn, op, self.prefix + body, locked=locked)
210
+ def _request(self, op: int, body: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
211
+ """Rejections raise; completed results of any outcome come back for the caller to decode. expect_ms: how
212
+ long it may take on the probe (Host.expecting)."""
213
+ with self._expecting(expect_ms):
214
+ return self.host.request(self.fn, op, self.prefix + body, locked=locked)
205
215
 
206
216
  def request(self, op: int, body: bytes = b"") -> tuple[int, int, bytes]:
207
217
  """The raw (fn, op, payload) of one operation, for Host.pipeline / pipeline_calls."""
@@ -39,7 +39,8 @@ define:
39
39
  verify_ms without a commit or at a broken candidate, a commit after idle_ms (at most port_speed_idle_max_ms, 3000;
40
40
  0 and anything longer count as that maximum) with no good frame or 3 broken
41
41
  candidates within 1 s, the session's end reverts after its answer. The line itself is modelled by `broken_rates`
42
- (rate -> BrokenRate): frames at such a rate break, from a size and in the directions given (`fake_serial` applies it).
42
+ (rate -> BrokenRate): frames at such a rate break, from a size and in the directions given, only both ways at once
43
+ (`duplex`), only every Nth (`every`) (`fake_serial` applies it).
43
44
 
44
45
  Every other non-core fn gets two stand-in operations so the session rules can be exercised - FAKE ONLY, they mean
45
46
  nothing on a real probe: 0x01 write(u32) changes state, 0x02 read -> u32 needs no lock.
@@ -317,6 +318,16 @@ class BrokenRate:
317
318
  min_size: int = 0
318
319
  to_host: bool = True
319
320
  to_probe: bool = True
321
+ duplex: bool = False # only while both ways carry such frames at once (a request of min_size and more comes in
322
+ # while an answer of min_size and more is still unread): the request breaks (to_probe),
323
+ # the unread answer breaks (to_host)
324
+ every: int = 1 # of the frames that would break, only every Nth does (1: all)
325
+ seen: int = 0 # the frames that would have broken so far (`every` counts these)
326
+
327
+ def hit(self) -> bool:
328
+ """One more frame that would break: True when this one does (`every`)."""
329
+ self.seen += 1
330
+ return self.seen % max(1, self.every) == 0
320
331
 
321
332
 
322
333
  PORT_SPEED_TAG = reg.CORE.tlv["describe"]["port_speed"]
@@ -1218,10 +1229,19 @@ class Endpoint:
1218
1229
  return self.speed_rate
1219
1230
  return self.port_speed_base or 115200
1220
1231
 
1221
- def breaks(self, port: int, size: int, to_host: bool) -> bool:
1222
- """The line model: a frame of `size` bytes on `port` at its rate now breaks (`broken_rates`)."""
1232
+ def breaks(self, port: int, size: int, to_host: bool, duplex: bool = False) -> bool:
1233
+ """The line model: a frame of `size` bytes on `port` at its rate now breaks (`broken_rates`). duplex: the other
1234
+ way carries a frame of the rate's min_size or more at the same time (a BrokenRate with `duplex` breaks only
1235
+ then)."""
1236
+ b = self.broken_rates.get(self.port_baud(port))
1237
+ if b is None or size < b.min_size or not (b.to_host if to_host else b.to_probe) or (b.duplex and not duplex):
1238
+ return False
1239
+ return b.hit()
1240
+
1241
+ def duplex_rate(self, port: int) -> BrokenRate | None:
1242
+ """The rate `port` runs at now when it breaks only both ways at once (fake_serial checks the unread answers)."""
1223
1243
  b = self.broken_rates.get(self.port_baud(port))
1224
- return b is not None and size >= b.min_size and (b.to_host if to_host else b.to_probe)
1244
+ return b if b is not None and b.duplex else None
1225
1245
 
1226
1246
  def speed_after_answer(self) -> None:
1227
1247
  """The answer that asked for a switch or a revert is out (at the old speed): now do it."""
@@ -17,7 +17,8 @@ bytes to send, or None to send nothing (fault injection for tests).
17
17
  port_speed (core §3.5): every closed candidate tells the endpoint whether it was a frame (`Endpoint.speed_frame`), a
18
18
  switch or revert a request asked for happens once its answer is queued (at the old speed), and the endpoint's
19
19
  `broken_rates` break frames at the port's rate now: a candidate from the host is then not a frame, an answer or push
20
- goes out with a spoiled CRC. `FakeSerialStream` is the port as a pyserial-shaped stream for an in-process host, with
20
+ goes out with a spoiled CRC (a `duplex` one only while a request of its size comes in with such an answer still
21
+ unread: the request, or that answer, breaks). `FakeSerialStream` is the port as a pyserial-shaped stream for an in-process host, with
21
22
  the host's own `baudrate`: while it differs from the probe's, every byte either way arrives garbled.
22
23
  """
23
24
 
@@ -40,6 +41,7 @@ class FakeSerialPort:
40
41
  self.frames: list[bytes] = [] # framed answers waiting to go out
41
42
  self.answers = 0
42
43
  self.garble: Callable[[bytes, int], bytes] | None = None # (frame, the rate it goes at) -> what arrives
44
+ self.spoiled: set[int] = set() # the waiting answers a duplex BrokenRate already broke
43
45
 
44
46
  def feed(self, data: bytes) -> None:
45
47
  now = self.ep.now()
@@ -65,8 +67,10 @@ class FakeSerialPort:
65
67
  self.cand = None
66
68
  if not body:
67
69
  return # 0x00 0x00: an empty frame
70
+ size = len(body) + 2
71
+ duplex = self._both_ways(size)
68
72
  try:
69
- if self.ep.breaks(self.index, len(body) + 2, to_host=False):
73
+ if self.ep.breaks(self.index, size, to_host=False, duplex=duplex):
70
74
  raise cobs.CorruptFrame("the line's rate breaks this frame")
71
75
  msg = cobs.unframe(body)
72
76
  except cobs.CorruptFrame:
@@ -90,6 +94,22 @@ class FakeSerialPort:
90
94
  self.frames.append(self._line(wire))
91
95
  self.ep.speed_after_answer() # port_speed: switch (or revert) now the answer is out
92
96
 
97
+ def _both_ways(self, size: int) -> bool:
98
+ """A request of `size` bytes came in: with a BrokenRate that breaks only both ways at once (`duplex`), whether
99
+ an answer of its min_size or more is still unread (both ways busy) - and then that answer breaks if the rate
100
+ breaks frames towards the host."""
101
+ b = self.ep.duplex_rate(self.index)
102
+ if b is None or size < b.min_size:
103
+ return False
104
+ big = [k for k, f in enumerate(self.frames) if len(f) >= b.min_size]
105
+ if not big:
106
+ return False
107
+ k = big[-1] # the answer on the line now (the latest); never spoiled twice
108
+ if k not in self.spoiled and self.ep.breaks(self.index, len(self.frames[k]), to_host=True, duplex=True):
109
+ self.frames[k] = _spoil(self.frames[k])
110
+ self.spoiled.add(k)
111
+ return True
112
+
93
113
  def _gap(self, now: int) -> None:
94
114
  if self.cand is not None and now - self.last_ms >= GAP_MS:
95
115
  raw, self.cand = bytes(self.cand), None
@@ -109,9 +129,7 @@ class FakeSerialPort:
109
129
  def _line(self, wire: bytes) -> bytes:
110
130
  """A framed message as the line delivers it at the port's rate now (`Endpoint.broken_rates`, `garble`)."""
111
131
  if self.ep.breaks(self.index, len(wire), to_host=True):
112
- body = bytearray(cobs.decode(wire[1:-1]))
113
- body[-1] ^= 0xFF # the CRC's high byte
114
- wire = b"\x00" + cobs.encode(bytes(body)) + b"\x00"
132
+ wire = _spoil(wire)
115
133
  return self.garble(wire, self.ep.port_baud(self.index)) if self.garble else wire
116
134
 
117
135
  def output(self, room: int = 4096) -> bytes:
@@ -119,6 +137,7 @@ class FakeSerialPort:
119
137
  out = bytearray()
120
138
  while self.frames and len(out) + len(self.frames[0]) <= max(room, len(self.frames[0])):
121
139
  out += self.frames.pop(0)
140
+ self.spoiled = {k - 1 for k in self.spoiled if k}
122
141
  if len(out) >= room:
123
142
  return bytes(out)
124
143
  while len(out) < room:
@@ -129,6 +148,16 @@ class FakeSerialPort:
129
148
  return bytes(out)
130
149
 
131
150
 
151
+ def _spoil(wire: bytes) -> bytes:
152
+ """A framed message with its CRC's high byte spoiled (a frame the line broke); bytes that are no frame as they are."""
153
+ try:
154
+ body = bytearray(cobs.decode(wire[1:-1]))
155
+ except cobs.CorruptFrame:
156
+ return wire
157
+ body[-1] ^= 0xFF
158
+ return b"\x00" + cobs.encode(bytes(body)) + b"\x00"
159
+
160
+
132
161
  def _garble(data: bytes) -> bytes:
133
162
  """Bytes sent at one rate and read at another: a different byte for every byte, 0x00s among them (so the reader
134
163
  sees broken candidates, not silence)."""
@@ -30,9 +30,10 @@ Options:
30
30
  "DUT RX" labels when the profile has them, else the first free channels); configure then works
31
31
  --uart-rx TEXT what arrives on that UART's RX every --every ms once it is configured (%d = a counter)
32
32
  --no-port-speed the profile's port_speed (core §3.5; esp32-v003 has it) off: the op is unknown_operation
33
- --broken-rate SPEC port_speed's line model: frames at RATE break (repeatable). SPEC is RATE[:MIN_SIZE][:in|out]:
34
- only frames of MIN_SIZE bytes and more on the wire (default every frame), only towards the
35
- host (in) or the probe (out) (default both). The fake cannot see the host's own rate (a pty,
33
+ --broken-rate SPEC port_speed's line model: frames at RATE break (repeatable). SPEC is
34
+ RATE[:MIN_SIZE][:in|out][:duplex][:everyN]: only frames of MIN_SIZE bytes and more on the wire
35
+ (default every frame), only towards the host (in) or the probe (out) (default both), only
36
+ while both ways carry such frames at once (duplex), only every Nth of them (everyN). The fake cannot see the host's own rate (a pty,
36
37
  TCP): only the probe's rate decides
37
38
  --run-hook SPEC what riscv-dm run does on every target: SPEC is module:function or path/file.py:function,
38
39
  called as function(target, pc, regs) -> (stopped, dpc, elapsed_us). `target` is the
@@ -83,7 +84,9 @@ def build(a: argparse.Namespace) -> endpoint.Endpoint:
83
84
  rate, *rest = spec.split(":")
84
85
  size = next((int(x) for x in rest if x.isdigit()), 0)
85
86
  way = next((x for x in rest if x in ("in", "out")), None)
86
- ep.broken_rates[int(rate)] = endpoint.BrokenRate(size, to_host=way != "out", to_probe=way != "in")
87
+ every = next((int(x[5:]) for x in rest if x.startswith("every") and x[5:].isdigit()), 1)
88
+ ep.broken_rates[int(rate)] = endpoint.BrokenRate(size, to_host=way != "out", to_probe=way != "in",
89
+ duplex="duplex" in rest, every=every)
87
90
  for n in a.absent:
88
91
  ep.targets[(wire, ep.pairs[wire][n])].present = False
89
92
  items = []
@@ -15,6 +15,7 @@ opens again (host guide §2.5).
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import contextlib
18
19
  import random
19
20
  import struct
20
21
  from dataclasses import dataclass, field
@@ -194,14 +195,32 @@ class Host:
194
195
  _revisions: dict = field(default_factory=dict) # fn -> interface revision from list
195
196
  _describes: dict = field(default_factory=dict) # fn -> its describe TLVs (declarations: valid for one boot_id)
196
197
  _boot_id: int | None = None
198
+ expect_ms: int = 0 # how long the request going out now may take on the probe (`expecting`)
199
+ # Called before a request takes its corr (the link's keepalive at a raised port_speed rate): what it sends first
200
+ # must carry the lower corr, or the probe takes the request for an old one (core §4.1)
201
+ before_request: Callable[[], None] | None = None
202
+
203
+ @contextlib.contextmanager
204
+ def expecting(self, ms: int):
205
+ """Requests sent inside take up to `ms` on the probe (a run's timeout_ms, a dmi list's waits, an attach's
206
+ hold_ms, a capture's blocking, a save; core §6.1: the probe does not count the lease meanwhile). The link waits
207
+ at least that and a margin for each answer - also at a raised port_speed rate, where an ordinary request waits
208
+ a quarter of the lease. Nested: the longest holds."""
209
+ saved = self.expect_ms
210
+ self.expect_ms = max(saved, int(ms or 0))
211
+ try:
212
+ yield
213
+ finally:
214
+ self.expect_ms = saved
197
215
 
198
216
  def next_corr(self) -> int:
199
217
  self._corr = self._corr % 0xFFFF + 1
200
218
  return self._corr
201
219
 
202
- def call(self, fn: int, op: int, payload: bytes = b"", *, locked: bool = True) -> m.Result:
220
+ def call(self, fn: int, op: int, payload: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
203
221
  """request() that also raises Failed unless the probe says it worked: what every operation wants."""
204
- r = self.request(fn, op, payload, locked=locked)
222
+ with self.expecting(expect_ms):
223
+ r = self.request(fn, op, payload, locked=locked)
205
224
  if not r.succeeded:
206
225
  raise Failed(r)
207
226
  return r
@@ -212,11 +231,15 @@ class Host:
212
231
  self.require_v1()
213
232
  return self.session
214
233
 
215
- def request(self, fn: int, op: int, payload: bytes = b"", *, locked: bool = True) -> m.Result:
234
+ def request(self, fn: int, op: int, payload: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
216
235
  """locked=True sends the session id (role 0x81) once a session is open; lock-free requests may leave it off.
217
- Rejections raise; any other answer is returned (Result.succeeded / .ran say what it was)."""
236
+ expect_ms: how long this request may take on the probe (`expecting`). Rejections raise; any other answer is
237
+ returned (Result.succeeded / .ran say what it was)."""
238
+ if self.before_request is not None:
239
+ self.before_request()
218
240
  req = m.Request(self.next_corr(), fn, op, payload, self._session_for(locked))
219
- result = m.Result.unpack(self.send(req.pack()))
241
+ with self.expecting(expect_ms):
242
+ result = m.Result.unpack(self.send(req.pack()))
220
243
  if result.corr != req.corr:
221
244
  raise ProtocolError(f"result for correlation {result.corr}, expected {req.corr}")
222
245
  if result.resolution == m.REJECTED:
@@ -253,6 +276,8 @@ class Host:
253
276
  """Several requests in flight (`exchange` keeps the probe's in-flight and window limits); results in
254
277
  order, rejects NOT raised - the caller looks at each result. Without `exchange`, one at a time."""
255
278
  session = self._session_for(locked)
279
+ if self.before_request is not None:
280
+ self.before_request()
256
281
  reqs = [m.Request(self.next_corr(), fn, op, payload, session) for fn, op, payload in requests]
257
282
  packed = [r.pack() for r in reqs]
258
283
  exchange = exchange or self.exchange