oep-client-python 0.0.24__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.
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/CHANGELOG.md +10 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/PKG-INFO +12 -4
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/README.ja.md +9 -4
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/README.md +11 -3
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/pyproject.toml +1 -1
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/__init__.py +1 -1
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/__main__.py +3 -1
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/arm.py +1 -1
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/capture.py +3 -2
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/config.py +3 -1
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/core.py +15 -5
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/endpoint.py +30 -8
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/fake_serial.py +34 -5
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/fake_serve.py +7 -4
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/host.py +30 -5
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/link.py +285 -69
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/registry.py +2 -2
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/riscv.py +20 -3
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_port_speed.py +202 -2
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/.gitignore +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/LICENSE +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/catalog.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/ch32_flash.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/cobs.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/console.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/decode.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/dump.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/fake.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/fake_capture.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/fixture.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/frames.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/hid_stream.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/interfaces.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/message.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/names.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/rp2350.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/uiapduino.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/src/oep_client/usb_stream.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_capabilities.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_capture.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_cobs.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_config.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_config_disable.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_fake_capture.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_fake_spec.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_fixture_targets.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_flash_console.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_hid_stream.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_interfaces.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_link_host.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_link_serial.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_session.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_target_parts.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.26}/tests/test_usb_stream.py +0 -0
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.0.25
|
|
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.
|
|
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。ほかの経路の時間切れは変わらない。
|
|
14
|
+
|
|
5
15
|
## 0.0.24
|
|
6
16
|
- (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
17
|
- (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)。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: oep-client-python
|
|
3
|
-
Version: 0.0.
|
|
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,15 +122,23 @@ 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 ->
|
|
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
|
|
129
131
|
(`link.speed`: `rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials`) is there to budget a capture or a write.
|
|
130
132
|
|
|
131
133
|
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
|
|
133
|
-
|
|
134
|
+
line goes quiet (`idle_ms`, at most `port_speed_idle_max_ms` = 3 s; a host that died leaves the rate no longer than
|
|
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()`
|
|
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
|
|
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
|
|
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
|
|
134
142
|
without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
|
|
135
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
|
|
136
144
|
tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's profile is not 115200.
|
|
@@ -95,14 +95,19 @@ 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
|
-
測る →
|
|
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`)はキャプチャや書き込みの予算を立てるのに使う。
|
|
102
103
|
|
|
103
|
-
probe はセッションが終わったとき(`end`、lease の期限切れ、force)、フレームが壊れたとき、線が黙ったとき(`idle_ms
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
probe はセッションが終わったとき(`end`、lease の期限切れ、force)、フレームが壊れたとき、線が黙ったとき(`idle_ms`、最長
|
|
105
|
+
`port_speed_idle_max_ms` = 3 秒。落ちた host の速さもそれより長くは残らない)に自分で起動時の速さに戻る: 上げている間、link は
|
|
106
|
+
1 秒黙っていれば要求の前に keepalive を送り、長く黙る呼び出し側は `hst.link.keep_alive()` で同じことをする。シリアルの口を開くと
|
|
107
|
+
最初の confirm を約 4 秒繰り返して、残った速さが戻るのを待つ。link は `end` にはすぐ合わせ、上げた速さで応答の来ない要求(送り直しも)があれば、起動時の速さに戻って
|
|
108
|
+
confirm し、そこでもう一度送る(固まらない)。上げている間は応答を待つ 1 回が lease の 4 分の 1 までなので、lease の内に十分収まる。
|
|
109
|
+
使っている間に 5 秒の内に 3 回フレームが壊れるか送り直すと、上げた速さで port_speed の戻すを送り、起動時の速さに戻って confirm し、
|
|
110
|
+
要求をそこで送る。どちらで離れた速さもそのセッションの間は使わない(`speed.stepped_down`、`speed.down_why`)。機能の無い probe は `not supported` で、速さは変わらない。ブローカー(TCP)の後ろでは
|
|
106
111
|
client ではなくブローカーが行う。シリアルの口は、ドライバにあれば low-latency のモードで開く(FTDI の latency timer 16 → 1 ms で
|
|
107
112
|
UART bridge の速度が 3 倍になった)。ボードの起動時の速さが 115200 でなければ `open_host(..., baud=)` で渡す。
|
|
108
113
|
|
|
@@ -100,15 +100,23 @@ 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 ->
|
|
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
|
|
107
109
|
(`link.speed`: `rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials`) is there to budget a capture or a write.
|
|
108
110
|
|
|
109
111
|
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
|
|
111
|
-
|
|
112
|
+
line goes quiet (`idle_ms`, at most `port_speed_idle_max_ms` = 3 s; a host that died leaves the rate no longer than
|
|
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()`
|
|
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
|
|
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
|
|
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
|
|
112
120
|
without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
|
|
113
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
|
|
114
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.
|
|
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"
|
|
@@ -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
|
|
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
|
|
200
|
-
|
|
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
|
-
|
|
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."""
|
|
@@ -36,9 +36,11 @@ define:
|
|
|
36
36
|
- port_speed (core §3.5, optional): on when the profile's describe declares it (`port_speed_base`, the boot speed;
|
|
37
37
|
None = off: the op is unknown_operation). try / commit / revert on the UART bridge the request came in on (else
|
|
38
38
|
unavailable cause 6), a rate the fake UART cannot make (outside 300..5000000) unsupported; a try reverts after
|
|
39
|
-
verify_ms without a commit or at a broken candidate, a commit after idle_ms
|
|
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
|
+
0 and anything longer count as that maximum) with no good frame or 3 broken
|
|
40
41
|
candidates within 1 s, the session's end reverts after its answer. The line itself is modelled by `broken_rates`
|
|
41
|
-
(rate -> BrokenRate): frames at such a rate break, from a size and in the directions given
|
|
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).
|
|
42
44
|
|
|
43
45
|
Every other non-core fn gets two stand-in operations so the session rules can be exercised - FAKE ONLY, they mean
|
|
44
46
|
nothing on a real probe: 0x01 write(u32) changes state, 0x02 read -> u32 needs no lock.
|
|
@@ -316,11 +318,22 @@ class BrokenRate:
|
|
|
316
318
|
min_size: int = 0
|
|
317
319
|
to_host: bool = True
|
|
318
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
|
|
319
331
|
|
|
320
332
|
|
|
321
333
|
PORT_SPEED_TAG = reg.CORE.tlv["describe"]["port_speed"]
|
|
322
334
|
OP_PORT_SPEED = reg.CORE.op["port_speed"]
|
|
323
335
|
SPEED_STEP = reg.CORE.enum["port_speed_step"]
|
|
336
|
+
SPEED_IDLE_MAX_MS = reg.TIMING["port_speed_idle_max_ms"] # committed: idle_ms at most this (0 and longer: this)
|
|
324
337
|
SPEED_BAD_MAX, SPEED_BAD_WINDOW_MS = 3, 1000 # committed: this many broken candidates within the window revert
|
|
325
338
|
SPEED_RATES = (300, 5_000_000) # what the fake's UART makes (anything between, exactly)
|
|
326
339
|
|
|
@@ -472,7 +485,7 @@ class Endpoint:
|
|
|
472
485
|
self.speed_rate = 0 # the rate it runs at
|
|
473
486
|
self.speed_asked = 0 # the baud the try asked (the commit names it again)
|
|
474
487
|
self.speed_until_ms = 0 # try: the commit's deadline (verify_ms)
|
|
475
|
-
self.speed_idle_ms = 0 # committed: revert after this long with no good frame
|
|
488
|
+
self.speed_idle_ms = 0 # committed: revert after this long with no good frame
|
|
476
489
|
self.speed_good_ms = 0 # the last good frame on that port
|
|
477
490
|
self.speed_bad: list[int] = [] # committed: the times of the recent broken candidates
|
|
478
491
|
self.speed_pending: tuple | None = None # ("switch", port, baud, verify_ms) / ("revert",): after the answer
|
|
@@ -1203,7 +1216,7 @@ class Endpoint:
|
|
|
1203
1216
|
if self.speed_state == "base" or port != self.speed_port or baud != self.speed_asked:
|
|
1204
1217
|
raise unavailable("wrong_state") # nothing tried, or another baud
|
|
1205
1218
|
self.speed_state = "committed"
|
|
1206
|
-
self.speed_idle_ms = idle_ms
|
|
1219
|
+
self.speed_idle_ms = idle_ms if 0 < idle_ms <= SPEED_IDLE_MAX_MS else SPEED_IDLE_MAX_MS
|
|
1207
1220
|
self.speed_good_ms = self.now()
|
|
1208
1221
|
self.speed_bad.clear()
|
|
1209
1222
|
return self._answer(struct.pack("<I", self.speed_rate), ignored)
|
|
@@ -1216,10 +1229,19 @@ class Endpoint:
|
|
|
1216
1229
|
return self.speed_rate
|
|
1217
1230
|
return self.port_speed_base or 115200
|
|
1218
1231
|
|
|
1219
|
-
def breaks(self, port: int, size: int, to_host: bool) -> bool:
|
|
1220
|
-
"""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)."""
|
|
1221
1243
|
b = self.broken_rates.get(self.port_baud(port))
|
|
1222
|
-
return b is not None and
|
|
1244
|
+
return b if b is not None and b.duplex else None
|
|
1223
1245
|
|
|
1224
1246
|
def speed_after_answer(self) -> None:
|
|
1225
1247
|
"""The answer that asked for a switch or a revert is out (at the old speed): now do it."""
|
|
@@ -1264,7 +1286,7 @@ class Endpoint:
|
|
|
1264
1286
|
now = self.now()
|
|
1265
1287
|
if self.speed_state == "try" and now >= self.speed_until_ms:
|
|
1266
1288
|
self._speed_revert() # no commit within verify_ms
|
|
1267
|
-
elif self.speed_state == "committed" and
|
|
1289
|
+
elif self.speed_state == "committed" and now - self.speed_good_ms >= self.speed_idle_ms:
|
|
1268
1290
|
self._speed_revert()
|
|
1269
1291
|
|
|
1270
1292
|
def _page(self, tlvs: list[bytes], first: int) -> bytes:
|
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
|
34
|
-
only frames of MIN_SIZE bytes and more on the wire
|
|
35
|
-
host (in) or the probe (out) (default both)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|