oep-client-python 0.0.24__tar.gz → 0.0.25__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.25}/CHANGELOG.md +4 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/PKG-INFO +5 -2
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/README.ja.md +4 -2
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/README.md +4 -1
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/pyproject.toml +1 -1
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/__init__.py +1 -1
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/endpoint.py +6 -4
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/link.py +69 -8
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/registry.py +2 -2
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_port_speed.py +76 -1
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/.gitignore +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/LICENSE +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/__main__.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/arm.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/capture.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/catalog.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/ch32_flash.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/cobs.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/config.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/console.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/core.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/decode.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/dump.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/fake.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/fake_capture.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/fake_serial.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/fake_serve.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/fixture.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/frames.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/hid_stream.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/host.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/interfaces.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/message.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/names.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/riscv.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/rp2350.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/uiapduino.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/src/oep_client/usb_stream.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_capabilities.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_capture.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_cobs.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_config.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_config_disable.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_fake_capture.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_fake_spec.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_fixture_targets.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_flash_console.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_hid_stream.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_interfaces.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_link_host.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_link_serial.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_session.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_target_parts.py +0 -0
- {oep_client_python-0.0.24 → oep_client_python-0.0.25}/tests/test_usb_stream.py +0 -0
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.25
|
|
6
|
+
- (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
|
+
- (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。ほかの経路の時間切れは変わらない。
|
|
8
|
+
|
|
5
9
|
## 0.0.24
|
|
6
10
|
- (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
11
|
- (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.25
|
|
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
|
|
@@ -129,7 +129,10 @@ but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), s
|
|
|
129
129
|
(`link.speed`: `rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials`) is there to budget a capture or a write.
|
|
130
130
|
|
|
131
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
|
|
132
|
+
line goes quiet (`idle_ms`, at most `port_speed_idle_max_ms` = 3 s; a host that died leaves the rate no longer than
|
|
133
|
+
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
|
+
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
|
+
wait out a rate left over. The link follows an `end` at once, and a request that goes unanswered at a raised rate (its
|
|
133
136
|
resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges. A probe
|
|
134
137
|
without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
|
|
135
138
|
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
|
|
@@ -100,8 +100,10 @@ print(hst.link.speed.to_text()) # 試した速さごとに、実際
|
|
|
100
100
|
3 MHz ÷ n だけ、CH340 は 921600 は通り 1500000 は probe → host が壊れた: oep-spec docs/uart-speed-negotiation.ja.md)ので、速さは
|
|
101
101
|
host が選ぶ。結果(`link.speed`: `rate`、`chosen`、`in_kb_s` / `out_kb_s`、`trials`)はキャプチャや書き込みの予算を立てるのに使う。
|
|
102
102
|
|
|
103
|
-
probe はセッションが終わったとき(`end`、lease の期限切れ、force)、フレームが壊れたとき、線が黙ったとき(`idle_ms
|
|
104
|
-
|
|
103
|
+
probe はセッションが終わったとき(`end`、lease の期限切れ、force)、フレームが壊れたとき、線が黙ったとき(`idle_ms`、最長
|
|
104
|
+
`port_speed_idle_max_ms` = 3 秒。落ちた host の速さもそれより長くは残らない)に自分で起動時の速さに戻る: 上げている間、link は
|
|
105
|
+
1 秒黙っていれば要求の前に keepalive を送り、長く黙る呼び出し側は `hst.link.keep_alive()` で同じことをする。シリアルの口を開くと
|
|
106
|
+
最初の confirm を約 4 秒繰り返して、残った速さが戻るのを待つ。link は `end` にはすぐ合わせ、上げた速さで応答の来ない要求(送り直しも)があれば、起動時の速さに戻って
|
|
105
107
|
confirm し、そこでもう一度送る(固まらない)。機能の無い probe は `not supported` で、速さは変わらない。ブローカー(TCP)の後ろでは
|
|
106
108
|
client ではなくブローカーが行う。シリアルの口は、ドライバにあれば low-latency のモードで開く(FTDI の latency timer 16 → 1 ms で
|
|
107
109
|
UART bridge の速度が 3 倍になった)。ボードの起動時の速さが 115200 でなければ `open_host(..., baud=)` で渡す。
|
|
@@ -107,7 +107,10 @@ but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), s
|
|
|
107
107
|
(`link.speed`: `rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials`) is there to budget a capture or a write.
|
|
108
108
|
|
|
109
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
|
|
110
|
+
line goes quiet (`idle_ms`, at most `port_speed_idle_max_ms` = 3 s; a host that died leaves the rate no longer than
|
|
111
|
+
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
|
+
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
|
+
wait out a rate left over. The link follows an `end` at once, and a request that goes unanswered at a raised rate (its
|
|
111
114
|
resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges. A probe
|
|
112
115
|
without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
|
|
113
116
|
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
|
|
@@ -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.25"
|
|
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,7 +36,8 @@ 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
42
|
(rate -> BrokenRate): frames at such a rate break, from a size and in the directions given (`fake_serial` applies it).
|
|
42
43
|
|
|
@@ -321,6 +322,7 @@ class BrokenRate:
|
|
|
321
322
|
PORT_SPEED_TAG = reg.CORE.tlv["describe"]["port_speed"]
|
|
322
323
|
OP_PORT_SPEED = reg.CORE.op["port_speed"]
|
|
323
324
|
SPEED_STEP = reg.CORE.enum["port_speed_step"]
|
|
325
|
+
SPEED_IDLE_MAX_MS = reg.TIMING["port_speed_idle_max_ms"] # committed: idle_ms at most this (0 and longer: this)
|
|
324
326
|
SPEED_BAD_MAX, SPEED_BAD_WINDOW_MS = 3, 1000 # committed: this many broken candidates within the window revert
|
|
325
327
|
SPEED_RATES = (300, 5_000_000) # what the fake's UART makes (anything between, exactly)
|
|
326
328
|
|
|
@@ -472,7 +474,7 @@ class Endpoint:
|
|
|
472
474
|
self.speed_rate = 0 # the rate it runs at
|
|
473
475
|
self.speed_asked = 0 # the baud the try asked (the commit names it again)
|
|
474
476
|
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
|
|
477
|
+
self.speed_idle_ms = 0 # committed: revert after this long with no good frame
|
|
476
478
|
self.speed_good_ms = 0 # the last good frame on that port
|
|
477
479
|
self.speed_bad: list[int] = [] # committed: the times of the recent broken candidates
|
|
478
480
|
self.speed_pending: tuple | None = None # ("switch", port, baud, verify_ms) / ("revert",): after the answer
|
|
@@ -1203,7 +1205,7 @@ class Endpoint:
|
|
|
1203
1205
|
if self.speed_state == "base" or port != self.speed_port or baud != self.speed_asked:
|
|
1204
1206
|
raise unavailable("wrong_state") # nothing tried, or another baud
|
|
1205
1207
|
self.speed_state = "committed"
|
|
1206
|
-
self.speed_idle_ms = idle_ms
|
|
1208
|
+
self.speed_idle_ms = idle_ms if 0 < idle_ms <= SPEED_IDLE_MAX_MS else SPEED_IDLE_MAX_MS
|
|
1207
1209
|
self.speed_good_ms = self.now()
|
|
1208
1210
|
self.speed_bad.clear()
|
|
1209
1211
|
return self._answer(struct.pack("<I", self.speed_rate), ignored)
|
|
@@ -1264,7 +1266,7 @@ class Endpoint:
|
|
|
1264
1266
|
now = self.now()
|
|
1265
1267
|
if self.speed_state == "try" and now >= self.speed_until_ms:
|
|
1266
1268
|
self._speed_revert() # no commit within verify_ms
|
|
1267
|
-
elif self.speed_state == "committed" and
|
|
1269
|
+
elif self.speed_state == "committed" and now - self.speed_good_ms >= self.speed_idle_ms:
|
|
1268
1270
|
self._speed_revert()
|
|
1269
1271
|
|
|
1270
1272
|
def _page(self, tlvs: list[bytes], first: int) -> bytes:
|
|
@@ -26,6 +26,10 @@ transfer both ways (link_source / link_sink with max_frame-sized frames) that co
|
|
|
26
26
|
throughput, then commit it, or revert and wait the probe out and re-confirm at the boot speed. The report stays on the
|
|
27
27
|
link (`link.speed`). Once raised, a request whose answer never comes even after its resend sends the link back to the
|
|
28
28
|
boot speed (the probe reverts by itself) and goes once more there: the link never wedges at a rate the probe left.
|
|
29
|
+
The probe also goes back after port_speed_idle_max_ms (3 s) with no good frame, so while raised the link sends a
|
|
30
|
+
keepalive before a request when it has been quiet for 1 s, and `keep_alive()` does the same for a caller that sits
|
|
31
|
+
idle for long. A host opening a serial port retries its first confirm for that maximum and a little (4 s in all): a
|
|
32
|
+
host that raised the speed and died leaves the probe at its rate until then.
|
|
29
33
|
"""
|
|
30
34
|
|
|
31
35
|
from __future__ import annotations
|
|
@@ -55,6 +59,10 @@ class PortBusy(OSError):
|
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
BASE_BAUD = 115200 # the boot speed of every reference UART bridge (the board's profile decides; core §3.5)
|
|
62
|
+
IDLE_MAX_MS = reg.TIMING["port_speed_idle_max_ms"] # a committed rate goes back after this with no good frame (§3.5)
|
|
63
|
+
KEEPALIVE_S = 1.0 # raised: a keepalive once the link has been quiet this long (well inside IDLE_MAX_MS)
|
|
64
|
+
OPEN_RETRY_S = IDLE_MAX_MS / 1000 + 1.0 # opening a serial port: confirm retried this long (a raised rate left over)
|
|
65
|
+
OPEN_TRY_S = 0.5 # each of those confirms waits this long (at most the link's timeout)
|
|
58
66
|
|
|
59
67
|
|
|
60
68
|
def open_serial(port: str, baud: int = BASE_BAUD):
|
|
@@ -158,6 +166,8 @@ class SerialLink:
|
|
|
158
166
|
self.speed_lost = 0 # times a raised rate was found gone (back to the boot speed)
|
|
159
167
|
self.fallback = True # a raised rate that stops answering: back to the boot speed
|
|
160
168
|
self.corr_source = self._own_corr # the host's correlation counter once bound (open_host)
|
|
169
|
+
self.keepalive_frame = None # raised: a keepalive request in the session, once bound (attach_host)
|
|
170
|
+
self.last_tx = time.monotonic() # when the link last wrote (raised: quiet for KEEPALIVE_S = keepalive)
|
|
161
171
|
self.held = lambda: False # a session holds the port (its raw transfer stopped): broken = resend
|
|
162
172
|
self.blind = lambda: [] # the host's blind stops (unsubscribe / end) once bound
|
|
163
173
|
self._corr_n = 0x8000
|
|
@@ -178,6 +188,7 @@ class SerialLink:
|
|
|
178
188
|
self.frames.send_many(messages)
|
|
179
189
|
else:
|
|
180
190
|
self.stream.write(b"".join(cobs.frame(msg) for msg in messages))
|
|
191
|
+
self.last_tx = time.monotonic()
|
|
181
192
|
|
|
182
193
|
def _recv(self) -> bytes:
|
|
183
194
|
if self.framing == "length":
|
|
@@ -250,10 +261,17 @@ class SerialLink:
|
|
|
250
261
|
n = 0
|
|
251
262
|
try:
|
|
252
263
|
while True:
|
|
264
|
+
if self._raised(): # a long pump at a raised rate keeps the line alive (core §3.5)
|
|
265
|
+
self.timeout = saved
|
|
266
|
+
self._keep_raised()
|
|
253
267
|
self.timeout = max(0.0, deadline - time.monotonic())
|
|
268
|
+
if self._raised():
|
|
269
|
+
self.timeout = min(self.timeout, KEEPALIVE_S)
|
|
254
270
|
try:
|
|
255
271
|
frame = self._recv()
|
|
256
272
|
except TimeoutError:
|
|
273
|
+
if self._raised() and time.monotonic() < deadline:
|
|
274
|
+
continue
|
|
257
275
|
return n
|
|
258
276
|
except FramingLost:
|
|
259
277
|
self.timeout = saved
|
|
@@ -307,6 +325,7 @@ class SerialLink:
|
|
|
307
325
|
self.resync()
|
|
308
326
|
|
|
309
327
|
def send(self, message: bytes) -> bytes:
|
|
328
|
+
self._keep_raised()
|
|
310
329
|
try:
|
|
311
330
|
reply = self._send(message)
|
|
312
331
|
except TimeoutError:
|
|
@@ -355,6 +374,7 @@ class SerialLink:
|
|
|
355
374
|
answered go once more with the same corr (oep-core §5.2: the probe answers a repeat from what it kept); a second
|
|
356
375
|
failure is raised.
|
|
357
376
|
"""
|
|
377
|
+
self._keep_raised()
|
|
358
378
|
replies: list[bytes] = []
|
|
359
379
|
try:
|
|
360
380
|
return self._exchange_once(messages, max_inflight, window_bytes, replies)
|
|
@@ -397,6 +417,15 @@ class SerialLink:
|
|
|
397
417
|
finally:
|
|
398
418
|
self.timeout = saved
|
|
399
419
|
|
|
420
|
+
def _confirm_within(self, wait_s: float, each_s: float) -> bool:
|
|
421
|
+
"""Confirms, each waiting `each_s`, until one is answered (True) or `wait_s` has passed (False)."""
|
|
422
|
+
deadline = time.monotonic() + wait_s
|
|
423
|
+
while True:
|
|
424
|
+
if self.confirm_raw(each_s):
|
|
425
|
+
return True
|
|
426
|
+
if time.monotonic() >= deadline:
|
|
427
|
+
return False
|
|
428
|
+
|
|
400
429
|
def back_to_base(self, wait_s: float = 3.0) -> bool:
|
|
401
430
|
"""The host at the boot speed again, and the probe confirmed there: confirms every 0.25 s up to `wait_s` (a
|
|
402
431
|
probe still trying waits out its verify_ms; one committed reverts at the broken candidates these make)."""
|
|
@@ -404,12 +433,31 @@ class SerialLink:
|
|
|
404
433
|
if self.base_baud is None:
|
|
405
434
|
return False
|
|
406
435
|
self.set_baud(self.base_baud)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
436
|
+
return self._confirm_within(wait_s, 0.25)
|
|
437
|
+
|
|
438
|
+
def _raised(self) -> bool:
|
|
439
|
+
return self.base_baud is not None and self.baud != self.base_baud
|
|
440
|
+
|
|
441
|
+
def keep_alive(self) -> bool:
|
|
442
|
+
"""While a raised rate is in force and a session holds the port: a keepalive when the link has been quiet for
|
|
443
|
+
KEEPALIVE_S (1 s). The probe goes back to the boot speed after port_speed_idle_max_ms (3 s) with no good frame
|
|
444
|
+
(core §3.5); every request already does this before it goes out, so only a caller that sits idle for long
|
|
445
|
+
(waiting on a person, a sleep between requests) calls it - often is fine, it sends nothing otherwise.
|
|
446
|
+
True when a keepalive went out."""
|
|
447
|
+
if not self._raised() or self.keepalive_frame is None or not self.held():
|
|
448
|
+
return False
|
|
449
|
+
if time.monotonic() - self.last_tx < KEEPALIVE_S:
|
|
450
|
+
return False
|
|
451
|
+
self.last_tx = time.monotonic() # before sending: send() asks again and must not recurse
|
|
452
|
+
self.send(self.keepalive_frame())
|
|
453
|
+
return True
|
|
454
|
+
|
|
455
|
+
def _keep_raised(self) -> None:
|
|
456
|
+
"""keep_alive before a request; a keepalive that fails is left to the request itself to find out."""
|
|
457
|
+
try:
|
|
458
|
+
self.keep_alive()
|
|
459
|
+
except (TimeoutError, cobs.CorruptFrame, FramingLost):
|
|
460
|
+
pass
|
|
413
461
|
|
|
414
462
|
def _speed_fallback(self) -> bool:
|
|
415
463
|
"""A request went unanswered (its resend too) while the link ran above the boot speed: the probe went back
|
|
@@ -461,12 +509,23 @@ class SerialLink:
|
|
|
461
509
|
self.corr_source = hst.next_corr
|
|
462
510
|
self.blind = hst.blind_stop
|
|
463
511
|
self.held = lambda: hst.session is not None
|
|
512
|
+
self.keepalive_frame = lambda: m.Request(hst.next_corr(), m.CORE_FN, m.OP_KEEPALIVE, b"", hst.session).pack()
|
|
464
513
|
hst.link = self
|
|
514
|
+
if self.framing == "cobs" and getattr(self, "transport", None) == "serial":
|
|
515
|
+
self.wait_boot_speed()
|
|
465
516
|
limits = hst.confirm()
|
|
466
517
|
hst.exchange = self.bind(limits)
|
|
467
518
|
if self.framing == "length" and limits.get("max_frame"):
|
|
468
519
|
self.frames.max_frame = limits["max_frame"]
|
|
469
520
|
|
|
521
|
+
def wait_boot_speed(self, wait_s: float | None = None) -> None:
|
|
522
|
+
"""A serial port just opened: confirm at the boot speed, retried for OPEN_RETRY_S (port_speed_idle_max_ms and
|
|
523
|
+
a second; at least the link's timeout) - a host that raised the speed and died leaves the probe at that rate
|
|
524
|
+
until its idle limit runs out (core §3.5 item 6). TimeoutError when none was answered."""
|
|
525
|
+
wait_s = max(OPEN_RETRY_S, self.timeout) if wait_s is None else wait_s
|
|
526
|
+
if not self._confirm_within(wait_s, min(self.timeout, OPEN_TRY_S)):
|
|
527
|
+
raise TimeoutError(f"no answer to confirm at {self.baud} for {wait_s:.1f} s")
|
|
528
|
+
|
|
470
529
|
def close(self) -> None:
|
|
471
530
|
self.stream.close()
|
|
472
531
|
|
|
@@ -736,7 +795,7 @@ SWITCH_SETTLE_S = 0.02 # after a baud change, before the first byte at the new
|
|
|
736
795
|
|
|
737
796
|
|
|
738
797
|
def raise_speed(hst, rates: list[int], *, verify_bytes: int = 32768, verify_s: float = 1.0,
|
|
739
|
-
verify_ms: int | None = None, idle_ms: int =
|
|
798
|
+
verify_ms: int | None = None, idle_ms: int = IDLE_MAX_MS, port: int | None = None) -> SpeedReport:
|
|
740
799
|
"""port_speed (oep-core §3.5), opt-in: try `rates` in order on the UART bridge this host opened, and commit the
|
|
741
800
|
first that passes. Each: try (answered at the speed now) -> the host switches -> verify both ways with
|
|
742
801
|
max_frame-sized frames for verify_bytes or verify_s in all (broken frames counted, KB/s measured each way) ->
|
|
@@ -745,7 +804,8 @@ def raise_speed(hst, rates: list[int], *, verify_bytes: int = 32768, verify_s: f
|
|
|
745
804
|
probe's UART cannot make is skipped (unsupported). The session must be open (the rate lasts as long as it does).
|
|
746
805
|
|
|
747
806
|
verify_ms: how long the probe waits for the commit (default: verify_s + 1.5 s, at most 65535). idle_ms: once
|
|
748
|
-
committed, the probe reverts after this long with no good frame (
|
|
807
|
+
committed, the probe reverts after this long with no good frame (default and at most port_speed_idle_max_ms,
|
|
808
|
+
3000; 0 and anything longer mean that maximum). The link keeps the line alive meanwhile (`keep_alive`).
|
|
749
809
|
port: the transport index (default: the probe's first UART bridge). -> the report, also kept as `hst.link.speed`."""
|
|
750
810
|
lk = getattr(hst, "link", None)
|
|
751
811
|
base = getattr(lk, "base_baud", None)
|
|
@@ -766,6 +826,7 @@ def raise_speed(hst, rates: list[int], *, verify_bytes: int = 32768, verify_s: f
|
|
|
766
826
|
report.supported = True
|
|
767
827
|
wait = (verify_ms if verify_ms is not None else min(65535, int(verify_s * 1000) + 1500))
|
|
768
828
|
lk.fallback = False # every failure here is handled here
|
|
829
|
+
idle_ms = idle_ms if 0 < idle_ms <= IDLE_MAX_MS else IDLE_MAX_MS
|
|
769
830
|
try:
|
|
770
831
|
return _raise(hst, lk, report, rates, port, base, wait, verify_bytes, verify_s, idle_ms)
|
|
771
832
|
finally:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from types import SimpleNamespace as _NS
|
|
4
4
|
|
|
5
|
-
REGISTRY_HASH = "
|
|
5
|
+
REGISTRY_HASH = "f98b4992f5b8ebe7"
|
|
6
6
|
SCHEMA = 1
|
|
7
7
|
PROTOCOL_REVISION = 1
|
|
8
8
|
CONFIRM_REQUEST_MAGIC = 'OEP?'
|
|
@@ -20,7 +20,7 @@ OUTCOMES = {"success": 0x00, "failed": 0x01, "partial": 0x02}
|
|
|
20
20
|
REJECT_REASONS = {"unknown_function": 0x01, "unknown_operation": 0x02, "malformed": 0x03, "unavailable": 0x04, "busy": 0x05, "window_exceeded": 0x06, "no_session": 0x07, "locked": 0x08, "session_required": 0x09, "no_connection": 0x0A, "unsupported": 0x0B, "result_lost": 0x0C, "corr_reused": 0x0D, "expired": 0x0E}
|
|
21
21
|
STATUS = {"ok": 0x00, "wait": 0x01, "line": 0x02, "fault": 0x03, "timeout": 0x04, "state": 0x05}
|
|
22
22
|
DESCRIBE_COMMON = {"role_channels": 0x01, "max_clock_hz": 0x02, "max_length": 0x03, "min_clock_hz": 0x05, "features": 0x06, "implementation": 0x07, "channel_group": 0x08}
|
|
23
|
-
TIMING = {"resync_quiet_ms": 0x32, "probe_frame_gap_ms": 0xC8, "host_frame_pause_max_ms": 0x64, "heartbeat_default_ms": 0x3E8}
|
|
23
|
+
TIMING = {"resync_quiet_ms": 0x32, "probe_frame_gap_ms": 0xC8, "host_frame_pause_max_ms": 0x64, "heartbeat_default_ms": 0x3E8, "port_speed_idle_max_ms": 0xBB8}
|
|
24
24
|
USB = {"iproduct_prefix": 'OEP', "vendor_bulk_class": 0xFF, "vendor_bulk_subclass": 0x4F, "vendor_bulk_protocol": 0x45, "hid_usage_page": 0xFF4F, "hid_usage": 0x45, "reference_vid": 0x1209, "reference_pid": 0x4F45}
|
|
25
25
|
LIMITS = {"lease_min_ms": 0x3E8, "lease_max_ms": 0xEA60, "owner_max_bytes": 0x20, "unit_id_max_bytes": 0x20, "slot_name_max_bytes": 0x20, "interface_name_max_bytes": 0x40, "max_op_ms_reference": 0x2710}
|
|
26
26
|
COMMON = _NS(enum={"read_from": {"position": 0x00, "oldest": 0x01, "now": 0x02, "last_mark": 0x03}, "read_flags": {"more": 0x01, "gap": 0x02}, "mark_kind": {"reset": 0x01, "restart": 0x02, "attach": 0x03, "detach": 0x04, "lost": 0x05, "clear": 0x06, "host": 0x07, "link_lost": 0x08, "closed": 0x09}, "mark_detail_reset": {"ndmreset": 0x01, "nrst": 0x02, "attach_reset": 0x03}, "mark_detail_restart": {"havereset": 0x01, "resync": 0x02}, "mark_detail_lost": {"overflow": 0x01, "framing": 0x02, "parity": 0x03, "target_timeout": 0x04}, "mark_detail_closed": {"all_released": 0x01, "expired": 0x02, "slot_changed": 0x03, "connection_closed": 0x04}})
|
|
@@ -87,8 +87,22 @@ def test_try_then_commit_answered_at_the_old_speed():
|
|
|
87
87
|
assert r.detail == m.UNAVAILABLE and r.payload[:3] == bytes([0x01, 1, 6])
|
|
88
88
|
r = p.send(0, PS, ps(0, 1500000, COMMIT), sid)
|
|
89
89
|
assert r.succeeded and p.ep.speed_state == "committed"
|
|
90
|
-
p.tick(
|
|
90
|
+
p.tick(endpoint.SPEED_IDLE_MAX_MS - 1) # idle_ms 0: the maximum, not never
|
|
91
91
|
assert p.ep.port_baud(0) == 1500000
|
|
92
|
+
p.tick(1)
|
|
93
|
+
assert p.ep.port_baud(0) == 115200
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_a_long_idle_ms_is_clamped_to_the_maximum():
|
|
97
|
+
p = Port()
|
|
98
|
+
sid = opened(p)
|
|
99
|
+
p.send(0, PS, ps(0, 500000, TRY), sid)
|
|
100
|
+
assert p.send(0, PS, ps(0, 500000, COMMIT, 0, 600_000), sid).succeeded
|
|
101
|
+
assert p.ep.speed_idle_ms == endpoint.SPEED_IDLE_MAX_MS == 3000
|
|
102
|
+
p.tick(2999)
|
|
103
|
+
assert p.ep.port_baud(0) == 500000
|
|
104
|
+
p.tick(1)
|
|
105
|
+
assert p.ep.port_baud(0) == 115200
|
|
92
106
|
|
|
93
107
|
|
|
94
108
|
def test_try_times_out_and_a_late_commit_is_wrong_state():
|
|
@@ -235,6 +249,67 @@ def test_a_revert_seen_as_a_timeout_goes_back_to_the_boot_speed():
|
|
|
235
249
|
assert lk.baud == 115200 and lk.speed_lost == 1 and lk.speed.lost and lk.speed.rate == 115200
|
|
236
250
|
|
|
237
251
|
|
|
252
|
+
def test_raise_speed_commits_the_idle_maximum_by_default():
|
|
253
|
+
ep, hst, lk = in_process()
|
|
254
|
+
link.raise_speed(hst, [750000], **FAST)
|
|
255
|
+
assert ep.speed_idle_ms == link.IDLE_MAX_MS == 3000
|
|
256
|
+
link.raise_speed(hst, [500000], idle_ms=0, **FAST) # 0: the maximum too
|
|
257
|
+
assert ep.speed_idle_ms == 3000
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_a_raised_link_keeps_the_line_alive_when_quiet():
|
|
261
|
+
ep, hst, lk = in_process()
|
|
262
|
+
link.raise_speed(hst, [750000], **FAST)
|
|
263
|
+
sent = []
|
|
264
|
+
frame = lk.keepalive_frame
|
|
265
|
+
lk.keepalive_frame = lambda: sent.append(1) or frame()
|
|
266
|
+
assert not lk.keep_alive() # just spoke: nothing to do
|
|
267
|
+
for _ in range(3): # 3.6 s in all, past the probe's idle limit
|
|
268
|
+
time.sleep(1.2)
|
|
269
|
+
assert lk.keep_alive()
|
|
270
|
+
ep.tick()
|
|
271
|
+
assert ep.port_baud(0) == 750000 and len(sent) == 3
|
|
272
|
+
time.sleep(1.2)
|
|
273
|
+
hst.request(0, m.OP_LOCK_STATE, locked=False) # a request after 1 s of quiet: a keepalive first
|
|
274
|
+
assert len(sent) == 4 and lk.baud == 750000 and lk.speed_lost == 0
|
|
275
|
+
hst.end()
|
|
276
|
+
time.sleep(1.2)
|
|
277
|
+
assert not lk.keep_alive() # back at the boot speed: none
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def second_host(ep):
|
|
281
|
+
lk = link.SerialLink.on_stream(fake_serial.FakeSerialStream(ep, 0), "cobs", 0.5)
|
|
282
|
+
lk.transport = "serial"
|
|
283
|
+
hst = h.Host(lk.send)
|
|
284
|
+
t0 = time.monotonic()
|
|
285
|
+
lk.attach_host(hst)
|
|
286
|
+
return hst, lk, time.monotonic() - t0
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_open_waits_out_a_raised_rate_a_host_that_died_left():
|
|
290
|
+
ep, hst, lk = in_process()
|
|
291
|
+
link.raise_speed(hst, [750000], **FAST) # this host dies here, its lease long
|
|
292
|
+
hst2, lk2, took = second_host(ep) # the confirms at 115200 are broken candidates: reverts
|
|
293
|
+
assert ep.port_baud(0) == 115200 and lk2.baud == 115200 and took < link.OPEN_RETRY_S
|
|
294
|
+
hst2.confirm()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_open_waits_for_the_idle_limit_when_broken_candidates_do_not_revert():
|
|
298
|
+
ep, hst, lk = in_process()
|
|
299
|
+
link.raise_speed(hst, [750000], **FAST)
|
|
300
|
+
heard = ep.speed_frame
|
|
301
|
+
ep.speed_frame = lambda port, good: heard(port, good) if good else None # only the idle limit takes it back
|
|
302
|
+
hst2, lk2, took = second_host(ep)
|
|
303
|
+
assert ep.port_baud(0) == 115200 and 2.5 < took < link.OPEN_RETRY_S + 1.5
|
|
304
|
+
ep2, hst3, lk3 = in_process()
|
|
305
|
+
link.raise_speed(hst3, [750000], **FAST)
|
|
306
|
+
ep2._speed_revert = lambda: None # never comes back: the open gives up
|
|
307
|
+
lk4 = link.SerialLink.on_stream(fake_serial.FakeSerialStream(ep2, 0), "cobs", 0.5)
|
|
308
|
+
lk4.corr_source = lambda: 7
|
|
309
|
+
with pytest.raises(TimeoutError):
|
|
310
|
+
lk4.wait_boot_speed(0.6)
|
|
311
|
+
|
|
312
|
+
|
|
238
313
|
def test_an_off_probe_is_not_supported_and_stays_at_the_boot_speed(monkeypatch):
|
|
239
314
|
ep, hst, lk = in_process()
|
|
240
315
|
ep.port_speed_base = None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|