oep-client-python 0.0.26__tar.gz → 0.0.27__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 (80) hide show
  1. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/CHANGELOG.md +20 -0
  2. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/PKG-INFO +34 -24
  3. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/README.ja.md +29 -19
  4. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/README.md +33 -23
  5. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/pyproject.toml +4 -1
  6. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/__init__.py +1 -1
  7. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/__main__.py +90 -10
  8. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/arm.py +7 -6
  9. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/ch32_flash.py +6 -7
  10. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/cobs.py +5 -0
  11. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/endpoint.py +41 -20
  12. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/fake.py +14 -4
  13. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/link.py +508 -226
  14. oep_client_python-0.0.27/src/oep_client/linktest.py +184 -0
  15. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/registry.py +1 -1
  16. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/riscv.py +45 -2
  17. oep_client_python-0.0.27/src/oep_client/speed_record.py +104 -0
  18. oep_client_python-0.0.27/tests/hw/README.ja.md +93 -0
  19. oep_client_python-0.0.27/tests/hw/README.md +95 -0
  20. oep_client_python-0.0.27/tests/hw/__init__.py +2 -0
  21. oep_client_python-0.0.27/tests/hw/boards.py +132 -0
  22. oep_client_python-0.0.27/tests/hw/conftest.py +91 -0
  23. oep_client_python-0.0.27/tests/hw/firmware.py +182 -0
  24. oep_client_python-0.0.27/tests/hw/flash.py +360 -0
  25. oep_client_python-0.0.27/tests/hw/record.py +260 -0
  26. oep_client_python-0.0.27/tests/hw/results/.gitignore +5 -0
  27. oep_client_python-0.0.27/tests/hw/results/9489dd2ae0953650-0.0.26+5bafb92-0.0.26.json +203 -0
  28. oep_client_python-0.0.27/tests/hw/results/9489dd2ae0953650-0.0.26+654b06a-0.0.26.json +203 -0
  29. oep_client_python-0.0.27/tests/hw/results/esp32-d0wd-v3-0070070d9394-0.0.26+4fad559-0.0.26.json +526 -0
  30. oep_client_python-0.0.27/tests/hw/results/esp32-d0wd-v3-0070070d9394-0.0.26+5bafb92-0.0.26.json +558 -0
  31. oep_client_python-0.0.27/tests/hw/results/esp32-pico-d4-50029191fe34-0.0.25-0.0.26.json +533 -0
  32. oep_client_python-0.0.27/tests/hw/results/esp32-pico-d4-50029191fe34-0.0.26+5bafb92-0.0.26.json +529 -0
  33. oep_client_python-0.0.27/tests/hw/results/esp32-pico-d4-50029191fe34-0.0.26+654b06a-0.0.26.json +529 -0
  34. oep_client_python-0.0.27/tests/hw/results/esp32-pico-d4-50029191fe34-0.0.26+e877631-0.0.26.json +528 -0
  35. oep_client_python-0.0.27/tests/hw/results/esp32-series-30eda0e31108-0.0.26+5bafb92-0.0.26.json +256 -0
  36. oep_client_python-0.0.27/tests/hw/results/esp32-series-30eda0e31108-0.0.26+654b06a-0.0.26.json +256 -0
  37. oep_client_python-0.0.27/tests/hw/results/esp32-series-30eda0e343c6-0.0.26+5bafb92-0.0.26.json +224 -0
  38. oep_client_python-0.0.27/tests/hw/results/esp32-series-30eda0e343c6-0.0.26+654b06a-0.0.26.json +224 -0
  39. oep_client_python-0.0.27/tests/hw/test_probe.py +389 -0
  40. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_fake_spec.py +31 -0
  41. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_interfaces.py +1 -0
  42. oep_client_python-0.0.27/tests/test_linktest.py +55 -0
  43. oep_client_python-0.0.27/tests/test_port_speed.py +724 -0
  44. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_target_parts.py +35 -5
  45. oep_client_python-0.0.26/tests/test_port_speed.py +0 -490
  46. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/.gitignore +0 -0
  47. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/LICENSE +0 -0
  48. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/capture.py +0 -0
  49. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/catalog.py +0 -0
  50. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/config.py +0 -0
  51. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/console.py +0 -0
  52. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/core.py +0 -0
  53. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/decode.py +0 -0
  54. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/dump.py +0 -0
  55. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/fake_capture.py +0 -0
  56. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/fake_serial.py +0 -0
  57. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/fake_serve.py +0 -0
  58. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/fixture.py +0 -0
  59. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/frames.py +0 -0
  60. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/hid_stream.py +0 -0
  61. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/host.py +0 -0
  62. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/interfaces.py +0 -0
  63. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/message.py +0 -0
  64. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/names.py +0 -0
  65. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/rp2350.py +0 -0
  66. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/uiapduino.py +0 -0
  67. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/src/oep_client/usb_stream.py +0 -0
  68. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_capabilities.py +0 -0
  69. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_capture.py +0 -0
  70. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_cobs.py +0 -0
  71. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_config.py +0 -0
  72. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_config_disable.py +0 -0
  73. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_fake_capture.py +0 -0
  74. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_fixture_targets.py +0 -0
  75. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_flash_console.py +0 -0
  76. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_hid_stream.py +0 -0
  77. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_link_host.py +0 -0
  78. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_link_serial.py +0 -0
  79. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_session.py +0 -0
  80. {oep_client_python-0.0.26 → oep_client_python-0.0.27}/tests/test_usb_stream.py +0 -0
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.27
6
+ - (EN) `tests/hw` port_speed verdict follows the host guide §7.3.2: a one-at-a-time cell at a raised rate fails only when broken + lost ≥ 3 and its ratio is over max(2 × the boot-speed ratio of the same flow, 5 % `OEP_HW_ERROR_MAX`) - the absolute 1 % failed the CH340 jig on bursts the boot speed shows too. Results with the probe 5bafb92 (port_speed handshake per core §3.5, block length) on the ATOM, the V003 jig (SWIO) and the X035 jig (DFU, RVSWD): all pass.
7
+ - (JA) `tests/hw` の port_speed の判定を host 開発ガイド §7.3.2 に合わせた: 上げた速さの 1 つずつの条件で、broken + lost が 3 以上かつ割合が max(起動時の速さの同じ流し方の割合 × 2, 5 %(`OEP_HW_ERROR_MAX`)) を超えたときだけ落とす(絶対の 1 % は、起動時の速さにもある CH340 のまとまった落ちで治具を落としていた)。probe 5bafb92(core §3.5 の握手、block 長)での結果: ATOM、V003 治具(SWIO)、X035 治具(DFU、RVSWD)すべて pass。
8
+ - (EN) port_speed follows the host guide's reference procedure (oep-spec host-development-guide.ja.md §7; core §3.5 is the handshake). `raise_speed(hst, candidates=(500000,), *, flows=None, verify=None, baseline=None, frames=16, verify_ms=None, idle_ms=3000, port=None, record=False)` replaces the old `rates, verify_bytes, verify_s, duplex_s`. The minimal form (§7.2, the default; ~50 ms, no measurement): try -> switch to the requested baud -> settle 20 ms -> confirm (100 ms x3) -> commit. The full form (`verify=True`, or `flows` given; §7.3): a baseline at the boot speed per flow (this session's frames when 60+ and under 10 %, else 60 measured per flow, re-measured at n=1 when over 10 %), then per candidate every `flows` ("in"|"out"|"duplex", n; default all three at the most the link keeps) for `frames` (16) frames at max_frame-16, a flow failing on broken+lost >= 3 and a ratio over max(2x baseline, 5 %), re-run at n=1 (then n=1 is the link's cap), one failed flow failing the candidate. `SpeedTrial` now carries `actual`, `switched`, `flows` (`FlowResult`: flow, n, frames, broken, lost, kb_s, ratio, passed), `n_cap`, `in_kb_s`/`out_kb_s`/`duplex_kb_s`; `SpeedReport` adds `verified`, `baseline`, `baseline_flows`, `step_downs` (`StepDown`), `skipped`. In use: the frames of the last 3 s (none judged under 50) over max(2x baseline, 10 %) broken or lost step the link down (was 3 strikes in 5 s); a missed answer still falls back to the boot speed within port_speed_idle_max_ms + 1 s (a link error when no confirm comes, never back to the raised rate). The keepalive interval is set under half of idle_ms. `set_baud` switches to the requested baud and to the probe's answered baud only when the OS refuses it (obligation 6/2). `oep speed` keeps working and gains `--candidates`, `--flows`, `--verify`/`--minimal`, `--frames`, `--no-record`.
9
+ - (JA) port_speed を host 開発ガイドの参考手順に合わせた(oep-spec host-development-guide.ja.md §7。core §3.5 は握手だけ)。`raise_speed(hst, candidates=(500000,), *, flows=None, verify=None, baseline=None, frames=16, verify_ms=None, idle_ms=3000, port=None, record=False)` が旧 `rates, verify_bytes, verify_s, duplex_s` を置き換える。最小の形(§7.2、既定。約 50 ms、計測なし): 試す → 要求した baud に切り替え → 20 ms → confirm(100 ms×3)→ 決める。完全な形(`verify=True` か `flows` を渡す。§7.3): 起動時の速さの基準を流し方ごとに取り(このセッションの 60 フレーム以上かつ 10 % 未満、無ければ 60 フレーム。10 % 超は n=1 で測り直し)、候補ごとに `flows`(("in"|"out"|"duplex", n)。既定は link が出す最大で 3 つ)を `frames`(16)フレーム max_frame−16 で流し、壊れ+失われが 3 以上かつ割合が max(基準×2, 5 %) 超で流し方は通らず、n=1 で流し直し(通れば n=1 が上限)、1 つでも通らなければ候補は通らない。`SpeedTrial` は `actual`、`switched`、`flows`(`FlowResult`: flow, n, frames, broken, lost, kb_s, ratio, passed)、`n_cap`、`in_kb_s`/`out_kb_s`/`duplex_kb_s` を持つ。`SpeedReport` に `verified`、`baseline`、`baseline_flows`、`step_downs`(`StepDown`)、`skipped` を足した。使用中は直近 3 秒のフレーム(50 未満は判定しない)が max(基準×2, 10 %) 超で降りる(旧: 5 秒に 3 回)。応答が来ない要求は port_speed_idle_max_ms + 1 秒の内に起動時の速さへ戻る(confirm が通らなければリンクの失敗。上げた速さへは戻さない)。keepalive の間隔は idle_ms の半分未満にした。`set_baud` は要求した baud に切り替え、OS が断ったときだけ probe の応答の baud にする(義務 6/2)。`oep speed` は動いたまま `--candidates`、`--flows`、`--verify`/`--minimal`、`--frames`、`--no-record` を足した。
10
+ - (EN) port_speed record (host guide §7.4): `speed_record.SpeedRecord` keeps passed / failed rates per (port path, unit_id) in `~/.cache/oep-client/link-speed.json` (`$XDG_CACHE_HOME`), 30-day expiry; `raise_speed(record=True)` (a bool, a path, or a SpeedRecord) reorders candidates (passed first, failed skipped -> `report.skipped`) and writes this run's outcomes and step-downs. On by default for the `oep speed` CLI, off for the library (`open_host(..., record=)` passes it through). A file that cannot be read or written is a cache miss, not an error (`SpeedRecord.error`).
11
+ - (JA) port_speed の記録(host ガイド §7.4): `speed_record.SpeedRecord` は通った / 通らなかった速さを(口のパス、unit_id)ごとに `~/.cache/oep-client/link-speed.json`(`$XDG_CACHE_HOME`)に 30 日残す。`raise_speed(record=True)`(真偽値・パス・SpeedRecord)は候補を並べ替え(通った速さを先頭、通らなかった速さは外す → `report.skipped`)、この実行の結果と降りた事実を書く。`oep speed` CLI は既定で ON、ライブラリは OFF(`open_host(..., record=)` で渡す)。読めない / 書けないファイルはキャッシュミスで、エラーにしない(`SpeedRecord.error`)。
12
+ - (EN) riscv-dm / arm-adi `max_length` (bytes) and `max_words` from describe (oep-if-debug §4.5 / §6): `RiscvDm` and `ArmAdi` take their block size from the declared max_length only, never `(max_frame - 18) / 4` or the like - `ch32_flash._block_size` and `arm.MemAp.chunk` do too. A probe with block ops that declares no usable max_length raises `riscv.NoMaxLength` (the spec makes it mandatory there). The fake declares max_length = max_frame - 24 rounded to a word (so a read_block answer and a write_block request both fit): 40 at 64-byte frames (esp32-v003), 1000 at 1024 (the P4 / RP2350 profiles) - was a flat 1000 / 40. A block over max_length is now rejected unsupported with payload 0x00 (was malformed); an odd address stays malformed. A port_speed step above 2 is malformed (was unsupported), and a step that does not fit the port's state is unavailable cause 6.
13
+ - (JA) riscv-dm / arm-adi の `max_length`(byte)と `max_words` を describe から取る(oep-if-debug §4.5 / §6): `RiscvDm` と `ArmAdi` は block の長さを宣言された max_length だけから決め、`(max_frame - 18) / 4` などからは計算しない。`ch32_flash._block_size` と `arm.MemAp.chunk` も同じ。block op を持つのに使える max_length を宣言しない probe は `riscv.NoMaxLength`(spec で必須)。偽の probe は max_length = max_frame − 24 を語に丸めて宣言する(read_block の応答と write_block の要求がどちらも収まる): 64 byte フレーム(esp32-v003)で 40、1024(P4 / RP2350)で 1000 ― 旧は一律 1000 / 40。max_length を超える block は payload 0x00 の rejected unsupported(旧 malformed)、奇数番地は malformed のまま。port_speed の step が 2 より大きいと malformed(旧 unsupported)、口の状態に合わない step は unavailable cause 6。
14
+ - (EN) The committed probe's self-revert condition 4 is now 3 broken candidates in a row with no good frame between (was 3 within a 1 s window); condition 2 (trying) counts a broken candidate only after the first good frame at the new speed arrives (the switch-over's leftovers do not count); idle_ms (condition 3) is not counted while a request runs. The fake and its tests follow.
15
+ - (JA) 決めた probe の自己復帰の条件 4 を「正常を挟まず壊れた候補 3 つ連続」にした(旧: 1 秒の窓に 3 つ)。条件 2(試し)は新しい速さで最初の正常フレームが来た後の壊れた候補だけ数える(切り替えの直後の残りは数えない)。idle_ms(条件 3)は要求の実行中は数えない。偽の probe とそのテストも追随。
16
+ - (EN) `tests/hw` on the bench's jigs (2026-10-02, with the bench's leave): V003 jig (ESP32-D0WD + CH340, CH32V003 over SWIO) and X035 jig (ESP32-P4 over DFU, CH32X035 over RVSWD) - flash, identity, config, wire (50 halt / read_block / resume loops, registers unchanged), gpio, uart, session pass on both; the CH340's port_speed cells fail the 1 % verdict now and then (bursty probe->host loss at every rate, oep-spec docs/link-measurements.ja.md §1.4). Fixes found by running there: one arduino-cli build path per profile (the shared sketch cache linked another profile's objects), the wire lookup asks list for `oep.wire` (a trailing dot matched nothing), the uart test runs on a UART the probe's settings already planned (a jig's plan) instead of failing to plan it again. The Pro Micro RP2350 run found a firmware fault (a UART plan on pins its UART cannot use froze the probe; fixed in oep-probe-arduino 654b06a).
17
+ - (JA) `tests/hw` を bench の治具で(2026-10-02、bench の了解のもと): V003 治具(ESP32-D0WD + CH340、SWIO 越しの CH32V003)と X035 治具(DFU で焼く ESP32-P4、RVSWD 越しの CH32X035)。flash / identity / config / wire(halt → read_block → resume 50 往復、レジスタ不変)/ gpio / uart / session はどちらも pass。CH340 の port_speed は 1 % の判定にときどき掛かる(どの速さでも probe→host がまとまって落ちる。oep-spec docs/link-measurements.ja.md §1.4)。回して見つけた直し: arduino-cli の build path を profile ごとに分ける(共有の sketch cache が別 profile の object を link した)、線の検索は list に `oep.wire` を渡す(末尾の点では何も当たらない)、uart の試験は probe の設定が既に plan した UART(治具の plan)の上で行う(もう一度 plan して失敗しない)。Pro Micro RP2350 の一巡で firmware の不具合を見つけた(UART が使えないピンへの plan で probe が固まる。oep-probe-arduino 654b06a で修正)。
18
+ - (EN) Link: on a serial port (COBS framing) the answer bytes expected in flight are bounded by `SerialLink.ANSWER_BURST_MAX` (6 KiB; `answer_burst` per link, 0 = off) - at most 5 requests in flight for a 1024-byte frame. Linux cdc_acm silently lost 20-30 % of 8 x 1008-byte answers from an ESP32-P4's HS CDC, none below 8 KiB, and none when the same endpoint is read through libusb (oep-spec docs/link-measurements.ja.md §1.1, core §3.4). Not applied to vendor bulk / HID. `cobs.frame_max(message_len)`. `oep linktest --json` flushes each line.
19
+ - (JA) link: シリアルの口(COBS)では、未解決の応答の見込み量を `SerialLink.ANSWER_BURST_MAX`(6 KiB。link ごとの `answer_burst`、0 で無し)に抑える。1024 B のフレームなら同時 5 まで。ESP32-P4 の HS CDC で 8 × 1008 B の応答を Linux の cdc_acm が 20〜30 % 黙って落とし、8 KiB 未満なら 0、同じエンドポイントを libusb で読めば 8 本でも 0 だった(oep-spec docs/link-measurements.ja.md §1.1、core §3.4)。vendor bulk / HID には掛けない。`cobs.frame_max(message_len)`。`oep linktest --json` は 1 行ずつ flush する。
20
+ - (EN) `tests/hw`: the pre-release hardware test of oep-spec docs/release-testing.ja.md (pytest, marker `hw`, skipped without `OEP_HW_BOARDS`). Per board: flash the firmware (a local build with `OEP_PROBE_DIR`, or a GitHub release with `OEP_PROBE_VERSION`, sha256 checked; `OEP_HW_NOFLASH=1` tests what is on the board), then confirm / list / describe, probe.config set / get / save / state / unset with a disable item and a reboot, the wire (with `OEP_HW_TARGET`), fixture gpio and uart, port_speed through `linktest.matrix` (verdict: one at a time ≤ 1 % broken + lost), the session's lease / expired / resumed / force. Flashers: esptool (classic ESP32), USB DFU in pyusb (ESP32-P4), the 1200-baud touch and picotool (RP2040 / RP2350). Results in `tests/hw/results/<board>-<firmware>-<client>.json`. Run on an M5Stack ATOM with the 0.0.26 main build and release 0.0.25 (2026-10-01): everything passes but port_speed, where the ATOM's bridge (a CH552 posing as FTDI) loses 0-8 % of 496-byte answers one at a time at 115200 (COBS-broken frames) and 921600 gets no confirm while 1500000 and 500000 do.
21
+ - (JA) `tests/hw`: oep-spec docs/release-testing.ja.md のリリース前の実機試験(pytest、マーカー `hw`。`OEP_HW_BOARDS` が無ければ skip)。ボードごとに: firmware を焼き(`OEP_PROBE_DIR` でローカルのビルド、`OEP_PROBE_VERSION` で GitHub のリリース。sha256 を照合。`OEP_HW_NOFLASH=1` は入っているものを試す)、confirm / list / describe、probe.config の set / get / save / state / unset(disable を含む)と再起動、線(`OEP_HW_TARGET` のとき)、fixture の gpio と uart、`linktest.matrix` による port_speed(判定: 1 つずつで broken + lost が 1 % 以下)、セッションの lease / expired / resumed / force。焼き方: esptool(classic ESP32)、pyusb の USB DFU(ESP32-P4)、1200 baud のタッチと picotool(RP2040 / RP2350)。結果は `tests/hw/results/<board>-<firmware>-<client>.json`。M5Stack ATOM で main の 0.0.26 ビルドとリリース 0.0.25 を回した(2026-10-01): port_speed 以外は通る。ATOM の変換(CH552 の FTDI 互換)は 115200 で 1 つずつでも 496 バイトの答えの 0〜8 % を落とし(COBS の壊れたフレーム)、921600 は confirm が返らない(1500000 と 500000 は通る)。
22
+ - (EN) `oep linktest` and `oep_client.linktest`: measure a link under conditions the caller chooses - rates to switch to with port_speed (or the speed in force), patterns (in / out / duplex), in-flight counts, frame sizes, frames or seconds, the answer wait, the driver's low-latency mode - and count ok / broken / lost frames and KB/s per cell. No fixed numbers in the measurement. Found with it: an M5Stack ATOM's FTDI corrupts the probe's answer while the host is transmitting, at 115200 as at 1.5 Mbaud (5-9 % of pipelined frames), and is clean one request at a time (2026-10-01).
23
+ - (JA) `oep linktest` と `oep_client.linktest`: 呼ぶ側が条件を決めて線を測る。port_speed で切り替える速さ(または今の速さ)、流し方(in / out / duplex)、同時に出す要求の数、フレームの大きさ、回数か時間、答えの待ち、ドライバの low latency。条件ごとに ok / broken / lost と KB/s を数える。決め打ちの数値は持たない。これで分かったこと: M5Stack ATOM の FTDI は、host が送っている間に受けた probe の答えを壊す(115200 でも 1.5 Mbaud でも、重ねたフレームの 5〜9 %)。1 つずつなら壊れない(2026-10-01)。
24
+
5
25
  ## 0.0.26
6
26
  - (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
27
  - (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)。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.26
3
+ Version: 0.0.27
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
@@ -36,7 +36,8 @@ here.
36
36
 
37
37
  ```sh
38
38
  pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
39
- uv run pytest # in a checkout
39
+ uv run pytest # in a checkout (the fake probe; no hardware)
40
+ OEP_HW_BOARDS=<board id> OEP_PROBE_DIR=<oep-probe-arduino checkout> uv run pytest tests/hw -m hw # a real probe: tests/hw/README.md
40
41
  ```
41
42
 
42
43
  `import oep_client` is all it takes. The registry is copied from oep-spec with `tools/sync_registry.sh`. PyPI's
@@ -115,33 +116,42 @@ A probe whose describe declares `port_speed` (oep-core §3.5; the classic ESP32
115
116
  run its UART bridge faster than the boot speed (115200) for one session. Nothing changes unless the host asks:
116
117
 
117
118
  ```python
118
- hst = link.open_host("/dev/ttyUSB0", port_speed=[1500000, 921600, 500000]) # takes the lock, keeps the session open
119
- print(hst.link.speed.to_text()) # every rate tried: actual, in / out KB/s, broken frames, the one in force
120
- # or, in a session already taken: report = link.raise_speed(hst, [1500000, 921600], verify_bytes=32768, verify_s=1.0)
119
+ hst = link.open_host("/dev/ttyUSB0", port_speed=[921600, 500000]) # takes the lock, keeps the session open
120
+ print(hst.link.speed.to_text()) # every candidate tried, the baseline, the flows, the one in force
121
+ # the full form (a baseline and the flows measured), in a session already taken:
122
+ report = link.raise_speed(hst, [921600, 500000], verify=True, flows=[("out", 2)], record=True)
121
123
  ```
122
124
 
123
- Each rate in order: `try` (answered at the speed now, then the probe switches) -> the host switches -> a verify both ways
124
- with max_frame-sized frames (link_source / link_sink, pipelined) for `verify_bytes` or `verify_s`, counting broken frames
125
- and measuring KB/s each way -> 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
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
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
130
- but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), so the host chooses them. The report
131
- (`link.speed`: `rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials`) is there to budget a capture or a write.
125
+ The host's procedure is the oep-spec host guide §7 (core §3.5 is the handshake). The **minimal form** (the default, about
126
+ 50 ms, no measurement): each candidate in order - `try` (answered at the speed now, then the probe switches) -> the host
127
+ switches to the requested baud -> 20 ms -> a `confirm` (100 ms, up to 3) -> `commit`. The **full form** (`verify=True`,
128
+ or `flows=` given): a baseline at the boot speed per flow (this session's frames, or 60 measured), then for each
129
+ candidate every flow the session will use - `flows` of `("in"|"out"|"duplex", n)` (in = link_source probe -> host, out =
130
+ link_sink host -> probe, duplex = both interleaved; `n` in flight, 0 = the most the link keeps) - 16 frames at
131
+ max_frame - 16, counting broken and lost and measuring KB/s; a flow fails on broken + lost >= 3 over max(2 x baseline,
132
+ 5 %), runs once more at n = 1 first (then n = 1 is the link's cap), and one failed flow fails the candidate. A failed
133
+ candidate reverts (step 2) and goes back to the boot speed, confirmed there. The first candidate that passes is kept; a
134
+ rate the probe's UART cannot make is skipped. Which rates pass depends on the bridge chip and its driver (an FTDI took
135
+ only 3 MHz / n, a CH340 921600 but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), so the
136
+ host chooses them. The report (`link.speed`: `base`, `rate`, `chosen`, `baseline`, `trials` of `SpeedTrial` with
137
+ `flows`, `in_kb_s` / `out_kb_s` / `duplex_kb_s`) is there to budget a capture or a write.
132
138
 
133
139
  The probe goes back to the boot speed by itself when the session ends (`end`, a lapse, a force), when frames break or the
134
140
  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
142
- without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
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
144
- tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's profile is not 115200.
141
+ that): while raised the link sends a keepalive before a request when it has been quiet for less than half of `idle_ms`
142
+ (1 s), and `hst.link.keep_alive()` does the same for a caller that sits idle for long; opening a serial port retries its
143
+ first confirm for about 4 s to wait out a rate left over. The link follows an `end` or a revert at once, and a request
144
+ that goes unanswered at a raised rate (its resend too) takes the link back to the boot speed, confirmed, and goes once
145
+ more there - it never wedges; while raised each wait for an answer is at most a quarter of the lease, so this ends well
146
+ inside it. In use the link judges the frames of the last 3 s (none under 50): over max(2 x baseline, 10 %) broken or
147
+ lost steps the link down - port_speed revert at the raised rate, the boot speed, a confirm - and the rate is not raised
148
+ again in that session (`speed.stepped_down`, `speed.down_why`, `speed.step_downs`). `record=True` (a bool, a path, or a
149
+ `speed_record.SpeedRecord`; the `oep speed` CLI's default, off in the library) keeps passed / failed rates per (port,
150
+ unit_id) under `~/.cache/oep-client/link-speed.json` for 30 days, putting a passed rate first and leaving failed ones
151
+ out. A probe without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does
152
+ this, not the client. Serial ports are also opened in the driver's low-latency mode where it has one (an FTDI's latency
153
+ timer 16 -> 1 ms tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's
154
+ profile is not 115200.
145
155
 
146
156
  ## The fake probe (a working spec)
147
157
 
@@ -11,7 +11,8 @@ target の知識は host にある、という OEP の分担に従う。probe
11
11
 
12
12
  ```sh
13
13
  pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
14
- uv run pytest # in a checkout
14
+ uv run pytest # in a checkout(偽の probe。実機なし)
15
+ OEP_HW_BOARDS=<board id> OEP_PROBE_DIR=<oep-probe-arduino の checkout> uv run pytest tests/hw -m hw # 実機: tests/hw/README.ja.md
15
16
  ```
16
17
 
17
18
  `import oep_client` だけで使える(`sys.path` に `src/` を足す使い方は不要になった)。番号の表は `tools/sync_registry.sh` で
@@ -75,7 +76,7 @@ oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --
75
76
  oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
76
77
  oep config uart <probe> oep.fixture.uart 115200 --format 8N1 # その UART の plan に RX / TX が付くたびに掛かる
77
78
  oep config save <probe> # 再起動の後も残す(remove = unset、erase もある)
78
- oep speed <probe> 1500000,921600,500000 # port_speed: UART bridge の速い速さを試し、結果を出す(下)
79
+ oep speed <probe> [--candidates 921600,500000] [--verify [--flows out:2]] # port_speed: 速い速さを試し、結果を出す(下)
79
80
  ```
80
81
 
81
82
  `<probe>` はシリアルの口、`tcp://HOST:PORT`、`usb[:VID:PID[:SERIAL]]`。変更はロックを取り(owner "oep config")、終わったら
@@ -88,28 +89,37 @@ describe に `port_speed` を宣言する probe(oep-core §3.5。参照の cla
88
89
  間、UART bridge を起動時の速さ(115200)より速くできます。host が頼まない限り何も変わりません:
89
90
 
90
91
  ```python
91
- hst = link.open_host("/dev/ttyUSB0", port_speed=[1500000, 921600, 500000]) # ロックを取り、セッションを開いたままにする
92
- print(hst.link.speed.to_text()) # 試した速さごとに、実際の速さ、in / out の KB/s、壊れたフレーム、今の速さ
93
- # 取ってあるセッションの中では: report = link.raise_speed(hst, [1500000, 921600], verify_bytes=32768, verify_s=1.0)
92
+ hst = link.open_host("/dev/ttyUSB0", port_speed=[921600, 500000]) # ロックを取り、セッションを開いたままにする
93
+ print(hst.link.speed.to_text()) # 候補ごとに、基準、流し方、今の速さ
94
+ # 完全な形(基準と流し方を測る)を取ってあるセッションの中で:
95
+ report = link.raise_speed(hst, [921600, 500000], verify=True, flows=[("out", 2)], record=True)
94
96
  ```
95
97
 
96
- 速さごとに順に: `試す`(今の速さで応答してから probe が切り替える)→ host も切り替える → 両方向に max_frame の大きさのフレームで
97
- (link_source / link_sink、パイプライン)`verify_bytes` か `verify_s` の分だけ確かめ、壊れたフレームを数え、それぞれの向きの KB/s を
98
- 測る → 通った同時の数で両方向を同時に(link_source と link_sink を交互に、約 1 秒、64 フレーム以上。`duplex_kb_s`。向きごとには
99
- 通っても同時に流すと壊れる線がある。921600 の CH340 がそうだった)流す → 何も壊れなければ `決める`、壊れれば戻して起動時の速さに戻り、そこで confirm し直す(戻すが届かなかった probe は `verify_ms`
100
- を待って戻る)。最初に通った速さを使う。probe の UART が作れない速さは飛ばす。通る速さは変換チップとドライバで決まる(FTDI は
101
- 3 MHz ÷ n だけ、CH340 は 921600 は通り 1500000 は probe → host が壊れた: oep-spec docs/uart-speed-negotiation.ja.md)ので、速さは
102
- host が選ぶ。結果(`link.speed`: `rate`、`chosen`、`in_kb_s` / `out_kb_s`、`trials`)はキャプチャや書き込みの予算を立てるのに使う。
98
+ 手順は oep-spec の host 開発ガイド §7(core §3.5 は握手だけ)。**最小の形**(既定、約 50 ms、計測なし): 候補ごとに順に
99
+ `試す`(今の速さで応答してから probe が切り替える)→ host は要求した baud に切り替える → 20 ms → `confirm`(100 ms、3 回まで)→
100
+ `決める`。**完全な形**(`verify=True` か `flows=` を渡す): 起動時の速さの基準を流し方ごとに取り(このセッションのフレーム、
101
+ 無ければ 60 フレーム)、候補ごとに使う流し方だけ流す。流し方 = `("in"|"out"|"duplex", n)`(in = link_source probe → host、
102
+ out = link_sink host → probe、duplex = 両方を交互。`n` は同時数、0 = link が出す最大)で、max_frame − 16 のフレームを 16 個流し、
103
+ 壊れと失われを数え KB/s を測る。壊れ + 失われが 3 以上で割合が max(基準 × 2, 5 %) を超えたら流し方は通らず、n = 1 で流し直し
104
+ (通れば n = 1 が link の上限)、1 つでも通らなければ候補は通らない。通らない候補は戻して(step 2)起動時の速さに戻り confirm し直す。
105
+ 最初に通った候補を使う。probe の UART が作れない速さは飛ばす。通る速さは変換チップとドライバで決まる(FTDI は 3 MHz ÷ n だけ、
106
+ CH340 は 921600 は通り 1500000 は probe → host が壊れた: oep-spec docs/uart-speed-negotiation.ja.md)ので、速さは host が選ぶ。
107
+ 結果(`link.speed`: `base`、`rate`、`chosen`、`baseline`、`trials` = `flows` と `in_kb_s` / `out_kb_s` / `duplex_kb_s` を持つ
108
+ `SpeedTrial`)はキャプチャや書き込みの予算を立てるのに使う。
103
109
 
104
110
  probe はセッションが終わったとき(`end`、lease の期限切れ、force)、フレームが壊れたとき、線が黙ったとき(`idle_ms`、最長
105
111
  `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)の後ろでは
111
- client ではなくブローカーが行う。シリアルの口は、ドライバにあれば low-latency のモードで開く(FTDI の latency timer 16 → 1 ms で
112
- UART bridge の速度が 3 倍になった)。ボードの起動時の速さが 115200 でなければ `open_host(..., baud=)` で渡す。
112
+ `idle_ms` の半分より短く黙れば要求の前に keepalive を送り、長く黙る呼び出し側は `hst.link.keep_alive()` で同じことをする。
113
+ シリアルの口を開くと最初の confirm を約 4 秒繰り返して、残った速さが戻るのを待つ。link は `end` と戻すにはすぐ合わせ、上げた速さで
114
+ 応答の来ない要求(送り直しも)があれば起動時の速さに戻って confirm し、そこでもう一度送る(固まらない)。上げている間は応答を待つ
115
+ 1 回が lease の 4 分の 1 までなので、lease の内に十分収まる。使っている間は直近 3 秒のフレーム(50 未満なら判定しない)を見て、
116
+ max(基準 × 2, 10 %) を超えて壊れ・失われたら、上げた速さで port_speed の戻すを送り、起動時の速さに戻って confirm し、その速さは
117
+ そのセッションの間は使わない(`speed.stepped_down`、`speed.down_why`、`speed.step_downs`)。`record=True`(真偽値・パス・
118
+ `speed_record.SpeedRecord`。`oep speed` CLI は既定で ON、ライブラリは OFF)は、通った / 通らなかった速さを(口、unit_id)ごとに
119
+ `~/.cache/oep-client/link-speed.json` に 30 日残し、通った速さを先頭に、通らなかった速さを外す。機能の無い probe は `not supported`
120
+ で、速さは変わらない。ブローカー(TCP)の後ろでは client ではなくブローカーが行う。シリアルの口は、ドライバにあれば low-latency の
121
+ モードで開く(FTDI の latency timer 16 → 1 ms で UART bridge の速度が 3 倍になった)。ボードの起動時の速さが 115200 でなければ
122
+ `open_host(..., baud=)` で渡す。
113
123
 
114
124
  ## 偽の probe(動く spec)
115
125
 
@@ -14,7 +14,8 @@ here.
14
14
 
15
15
  ```sh
16
16
  pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
17
- uv run pytest # in a checkout
17
+ uv run pytest # in a checkout (the fake probe; no hardware)
18
+ OEP_HW_BOARDS=<board id> OEP_PROBE_DIR=<oep-probe-arduino checkout> uv run pytest tests/hw -m hw # a real probe: tests/hw/README.md
18
19
  ```
19
20
 
20
21
  `import oep_client` is all it takes. The registry is copied from oep-spec with `tools/sync_registry.sh`. PyPI's
@@ -93,33 +94,42 @@ A probe whose describe declares `port_speed` (oep-core §3.5; the classic ESP32
93
94
  run its UART bridge faster than the boot speed (115200) for one session. Nothing changes unless the host asks:
94
95
 
95
96
  ```python
96
- hst = link.open_host("/dev/ttyUSB0", port_speed=[1500000, 921600, 500000]) # takes the lock, keeps the session open
97
- print(hst.link.speed.to_text()) # every rate tried: actual, in / out KB/s, broken frames, the one in force
98
- # or, in a session already taken: report = link.raise_speed(hst, [1500000, 921600], verify_bytes=32768, verify_s=1.0)
97
+ hst = link.open_host("/dev/ttyUSB0", port_speed=[921600, 500000]) # takes the lock, keeps the session open
98
+ print(hst.link.speed.to_text()) # every candidate tried, the baseline, the flows, the one in force
99
+ # the full form (a baseline and the flows measured), in a session already taken:
100
+ report = link.raise_speed(hst, [921600, 500000], verify=True, flows=[("out", 2)], record=True)
99
101
  ```
100
102
 
101
- Each rate in order: `try` (answered at the speed now, then the probe switches) -> the host switches -> a verify both ways
102
- with max_frame-sized frames (link_source / link_sink, pipelined) for `verify_bytes` or `verify_s`, counting broken frames
103
- and measuring KB/s each way -> 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
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
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
108
- but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), so the host chooses them. The report
109
- (`link.speed`: `rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials`) is there to budget a capture or a write.
103
+ The host's procedure is the oep-spec host guide §7 (core §3.5 is the handshake). The **minimal form** (the default, about
104
+ 50 ms, no measurement): each candidate in order - `try` (answered at the speed now, then the probe switches) -> the host
105
+ switches to the requested baud -> 20 ms -> a `confirm` (100 ms, up to 3) -> `commit`. The **full form** (`verify=True`,
106
+ or `flows=` given): a baseline at the boot speed per flow (this session's frames, or 60 measured), then for each
107
+ candidate every flow the session will use - `flows` of `("in"|"out"|"duplex", n)` (in = link_source probe -> host, out =
108
+ link_sink host -> probe, duplex = both interleaved; `n` in flight, 0 = the most the link keeps) - 16 frames at
109
+ max_frame - 16, counting broken and lost and measuring KB/s; a flow fails on broken + lost >= 3 over max(2 x baseline,
110
+ 5 %), runs once more at n = 1 first (then n = 1 is the link's cap), and one failed flow fails the candidate. A failed
111
+ candidate reverts (step 2) and goes back to the boot speed, confirmed there. The first candidate that passes is kept; a
112
+ rate the probe's UART cannot make is skipped. Which rates pass depends on the bridge chip and its driver (an FTDI took
113
+ only 3 MHz / n, a CH340 921600 but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), so the
114
+ host chooses them. The report (`link.speed`: `base`, `rate`, `chosen`, `baseline`, `trials` of `SpeedTrial` with
115
+ `flows`, `in_kb_s` / `out_kb_s` / `duplex_kb_s`) is there to budget a capture or a write.
110
116
 
111
117
  The probe goes back to the boot speed by itself when the session ends (`end`, a lapse, a force), when frames break or the
112
118
  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
120
- without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
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
122
- tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's profile is not 115200.
119
+ that): while raised the link sends a keepalive before a request when it has been quiet for less than half of `idle_ms`
120
+ (1 s), and `hst.link.keep_alive()` does the same for a caller that sits idle for long; opening a serial port retries its
121
+ first confirm for about 4 s to wait out a rate left over. The link follows an `end` or a revert at once, and a request
122
+ that goes unanswered at a raised rate (its resend too) takes the link back to the boot speed, confirmed, and goes once
123
+ more there - it never wedges; while raised each wait for an answer is at most a quarter of the lease, so this ends well
124
+ inside it. In use the link judges the frames of the last 3 s (none under 50): over max(2 x baseline, 10 %) broken or
125
+ lost steps the link down - port_speed revert at the raised rate, the boot speed, a confirm - and the rate is not raised
126
+ again in that session (`speed.stepped_down`, `speed.down_why`, `speed.step_downs`). `record=True` (a bool, a path, or a
127
+ `speed_record.SpeedRecord`; the `oep speed` CLI's default, off in the library) keeps passed / failed rates per (port,
128
+ unit_id) under `~/.cache/oep-client/link-speed.json` for 30 days, putting a passed rate first and leaving failed ones
129
+ out. A probe without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does
130
+ this, not the client. Serial ports are also opened in the driver's low-latency mode where it has one (an FTDI's latency
131
+ timer 16 -> 1 ms tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's
132
+ profile is not 115200.
123
133
 
124
134
  ## The fake probe (a working spec)
125
135
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oep-client-python"
7
- version = "0.0.26"
7
+ version = "0.0.27"
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"
@@ -39,3 +39,6 @@ only-include = ["src/oep_client", "tests", "README.md", "README.ja.md", "CHANGEL
39
39
 
40
40
  [dependency-groups]
41
41
  dev = ["pytest>=8"]
42
+
43
+ [tool.pytest.ini_options]
44
+ markers = ["hw: needs a probe board (OEP_HW_BOARDS=<board-identify ids>); skipped otherwise (tests/hw/README.md)"]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.26"
5
+ __version__ = "0.0.27"
@@ -9,7 +9,9 @@
9
9
  oep config uart <probe> oep.fixture.uart#1 115200 --format 8N1
10
10
  oep config disable <probe> 3 4 (channels the probe never uses or touches; remove disable CH re-enables)
11
11
  oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
12
- oep speed <probe> 1500000,921600,500000 (port_speed, core §3.5: try the rates in order on a UART bridge, report)
12
+ oep speed <probe> [--candidates 921600,500000] [--verify [--flows in:2,out:2]] (port_speed, core §3.5 and the host
13
+ guide §7: try the candidates in order on a UART bridge, report)
14
+ oep linktest <probe> --rates now,921600 --patterns in,out,duplex --inflight 1,2 --sizes 128,496 --frames 300
13
15
 
14
16
  <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
15
17
  ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
@@ -30,14 +32,37 @@ def main(argv=None) -> int:
30
32
  parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
31
33
  sub = parser.add_subparsers(dest="command", required=True)
32
34
  _config_parser(sub)
33
- sp = sub.add_parser("speed", help="port_speed (core §3.5): try faster rates on a UART bridge, print the report")
35
+ sp = sub.add_parser("speed", help="port_speed (core §3.5, host guide §7): try faster rates on a UART bridge, "
36
+ "print the report")
34
37
  sp.add_argument("probe", help="the probe's serial port (a UART bridge this host opens)")
35
- sp.add_argument("rates", help="comma-separated, in order of preference (the first that passes is kept)")
38
+ sp.add_argument("rates", nargs="?", default="", help="the candidates, comma-separated, in order of preference "
39
+ "(the first that passes is kept; default 500000). Same as --candidates")
40
+ sp.add_argument("--candidates", default="", help="the candidates, comma-separated (default 500000)")
41
+ sp.add_argument("--flows", default="", help="the flows to verify, comma-separated FLOW[:N] (in, out, duplex; N in "
42
+ "flight, 0 = the most the link keeps; default: all three at that N). Selects --verify")
43
+ form = sp.add_mutually_exclusive_group()
44
+ form.add_argument("--verify", action="store_true", help="the full form (guide §7.3): baseline, the flows measured "
45
+ "at every candidate, a failed flow run again one at a time")
46
+ form.add_argument("--minimal", action="store_true", help="the minimal form (guide §7.2, the default): try, confirm, "
47
+ "commit, no measurement")
48
+ sp.add_argument("--frames", type=int, default=link.FLOW_FRAMES, help="frames per flow when verifying (default 16)")
49
+ sp.add_argument("--no-record", action="store_true", help="do not read or write the record of passed / failed rates "
50
+ "(~/.cache/oep-client/link-speed.json, 30 days; on by default here, off in the library)")
36
51
  sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
37
- sp.add_argument("--verify-bytes", type=int, default=32768)
38
- sp.add_argument("--verify-s", type=float, default=1.0)
39
- sp.add_argument("--duplex-s", type=float, default=1.0, help="both ways at once for this long (default 1)")
40
52
  sp.add_argument("--json", action="store_true")
53
+ lt = sub.add_parser("linktest", help="measure the link: traffic patterns at rates the host asks for, what breaks")
54
+ lt.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID]")
55
+ lt.add_argument("--rates", default="", help="comma-separated rates to switch to with port_speed and measure at "
56
+ "(each on its own; 0 or 'now' = the speed in force). Default: only the speed in force")
57
+ lt.add_argument("--patterns", default="in,out,duplex", help="in (probe->host), out (host->probe), duplex")
58
+ lt.add_argument("--inflight", default="1", help="comma-separated in-flight counts (above the probe's max: skipped)")
59
+ lt.add_argument("--sizes", default="", help="comma-separated frame payload sizes (default: a whole frame)")
60
+ lt.add_argument("--frames", type=int, default=300, help="frames per cell (default 300)")
61
+ lt.add_argument("--seconds", type=float, default=None, help="per cell for this long instead of --frames")
62
+ lt.add_argument("--timeout", type=float, default=0.3, help="seconds to wait for one answer (default 0.3)")
63
+ lt.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed to open at (default 115200)")
64
+ lt.add_argument("--low-latency", choices=("on", "off"), default="on", help="the serial driver's low-latency mode")
65
+ lt.add_argument("--json", action="store_true", help="one JSON object per cell")
41
66
  d = sub.add_parser("dump", help="list and describe every interface a probe offers")
42
67
  src = d.add_mutually_exclusive_group(required=True)
43
68
  src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
@@ -50,6 +75,8 @@ def main(argv=None) -> int:
50
75
  return _config(args)
51
76
  if args.command == "speed":
52
77
  return _speed(args)
78
+ if args.command == "linktest":
79
+ return _linktest(args)
53
80
 
54
81
  if args.fake:
55
82
  call = fake.PROFILES[args.fake]().call
@@ -61,17 +88,70 @@ def main(argv=None) -> int:
61
88
  return 0
62
89
 
63
90
 
91
+ # ---- oep linktest -------------------------------------------------------------------------------------------------
92
+
93
+ def _linktest(args) -> int:
94
+ """Every parameter on the command line: no fixed numbers in the measurement."""
95
+ import dataclasses
96
+ import json
97
+ from . import linktest
98
+ ints = lambda text: [int(v) for v in text.split(",") if v.strip()] # noqa: E731
99
+ rates = [None if v.strip() in ("0", "now") else int(v) for v in args.rates.split(",") if v.strip()] or [None]
100
+ hst = link.open_host(args.probe, baud=args.baud) if args.probe.startswith("/") or ":" not in args.probe \
101
+ else link.open_host(args.probe)
102
+ stream = getattr(hst.link, "stream", None)
103
+ if hasattr(stream, "set_low_latency_mode"):
104
+ try:
105
+ stream.set_low_latency_mode(args.low_latency == "on")
106
+ except (OSError, ValueError, NotImplementedError):
107
+ pass
108
+ try:
109
+ core.take(hst, 30000, owner="oep linktest")
110
+ for result in linktest.matrix(hst, rates=rates, patterns=[p for p in args.patterns.split(",") if p],
111
+ inflight=ints(args.inflight), sizes=ints(args.sizes) or None,
112
+ frames=args.frames, seconds=args.seconds, timeout=args.timeout):
113
+ if args.json:
114
+ for c in result.cells:
115
+ print(json.dumps({**dataclasses.asdict(c), "actual": result.actual, "low_latency": args.low_latency}), flush=True)
116
+ if result.why:
117
+ print(json.dumps({"rate": result.rate, "why": result.why}), flush=True)
118
+ else:
119
+ print(result.text(), flush=True)
120
+ try:
121
+ hst.end()
122
+ except Exception:
123
+ pass
124
+ finally:
125
+ hst.link.close()
126
+ return 0
127
+
128
+
64
129
  # ---- oep speed ----------------------------------------------------------------------------------------------------
65
130
 
131
+ def _flows(text: str) -> list[tuple[str, int]] | None:
132
+ """--flows "in:2,out,duplex:1" -> [("in", 2), ("out", 0), ("duplex", 1)]; "" -> None (the default flows)."""
133
+ out = []
134
+ for item in text.replace(" ", "").split(","):
135
+ if not item:
136
+ continue
137
+ name, _, n = item.partition(":")
138
+ out.append((name, int(n) if n else 0))
139
+ return out or None
140
+
141
+
66
142
  def _speed(args) -> int:
67
- """Take the lock, raise_speed, print the report; the session's end puts the port back at its boot speed."""
143
+ """Take the lock, raise_speed by the host guide's procedure, print the report; the session's end puts the port
144
+ back at its boot speed. The record of passed / failed rates is on unless --no-record."""
68
145
  import dataclasses
69
- rates = [int(r) for r in args.rates.replace(" ", "").split(",") if r]
146
+ text = args.candidates or args.rates
147
+ candidates = [int(r) for r in text.replace(" ", "").split(",") if r] or list(link.DEFAULT_CANDIDATES)
148
+ flows = _flows(args.flows)
149
+ verify = True if args.verify or flows else (False if args.minimal else None)
70
150
  hst = link.open_host(args.probe, baud=args.baud)
71
151
  try:
72
152
  core.take(hst, 5000, owner="oep speed")
73
- report = link.raise_speed(hst, rates, verify_bytes=args.verify_bytes, verify_s=args.verify_s,
74
- duplex_s=args.duplex_s)
153
+ report = link.raise_speed(hst, candidates, flows=flows, verify=verify, frames=args.frames,
154
+ record=not args.no_record)
75
155
  if args.json:
76
156
  sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
77
157
  else:
@@ -10,7 +10,7 @@ import struct
10
10
 
11
11
  from . import host as h, message as m, registry as reg
12
12
  from .core import Interface
13
- from .riscv import OK, TargetError, WireBase, check, ran, status_name # noqa: F401
13
+ from .riscv import OK, BlockLength, TargetError, WireBase, check, ran, status_name # noqa: F401
14
14
 
15
15
  DP_DPIDR = DP_ABORT = 0x0
16
16
  DP_CTRL_STAT = 0x4
@@ -60,7 +60,9 @@ def transfer_reads(steps: bytes) -> list[bool]:
60
60
  return out
61
61
 
62
62
 
63
- class ArmAdi(Interface):
63
+ class ArmAdi(Interface, BlockLength):
64
+ """oep.target.arm-adi on one connection. `max_length` / `max_words` bound read_block / write_block (from the
65
+ probe's describe, oep-if-debug §6: mandatory there)."""
64
66
  NAME = "oep.target.arm-adi"
65
67
  REVISION = 1
66
68
  TRANSFER, READ_BLOCK, WRITE_BLOCK = _ADI.op["transfer"], _ADI.op["read_block"], _ADI.op["write_block"]
@@ -70,6 +72,7 @@ class ArmAdi(Interface):
70
72
  self.conn = conn
71
73
  self.adiv6 = adiv6
72
74
  self._select: int | None = None
75
+ self._max_length = None
73
76
 
74
77
  # ---- raw transfers ----
75
78
  @staticmethod
@@ -140,10 +143,8 @@ class MemAp:
140
143
  csw = adi.ap_read(ap, self.base)
141
144
  adi.ap_write(ap, self.base, (((csw & ~0x37) | 0x12) | csw_set) & ~csw_clear) # 32 bits, AddrInc single
142
145
  adi._ap_select(ap, self.base) # the bank the block operations assume
143
- # Words per block operation, from the probe's frame limit: request header 6 + session 4 + connection 2 +
144
- # address 4 + count 2 on the way in (the answer's 5 + done 2 + status 1 is smaller).
145
- from .core import confirm
146
- self.chunk = max(1, (confirm(adi.host)["max_frame"] - 18) // 4)
146
+ # Words per block operation: the probe's declared max_length (oep-if-debug §6), never computed from max_frame
147
+ self.chunk = adi.max_words
147
148
 
148
149
  def write_many(self, pairs: list[tuple[int, int]]) -> None:
149
150
  """Scattered single-word writes in one transfer list (TAR, DRW per word, RDBUFF at the end so the last one
@@ -17,7 +17,7 @@ import struct
17
17
  import time
18
18
  from dataclasses import dataclass, field
19
19
 
20
- from . import core, host as h, riscv
20
+ from . import host as h, riscv
21
21
 
22
22
  # oep-spec experiments/flash-primitives/x035_loader.S, assembled at 0x20000000 (riscv32-esp-elf binutils)
23
23
  FAST_PAGE_LOADER = bytes.fromhex(
@@ -91,11 +91,10 @@ LOADER, FAST_BUFFER, FAST_DONE = 0x20000000, 0x20000400, 0x200000B0
91
91
  V003_INPUT, V003_STACK, V003_EBREAK, V003_RUN = 0x20000200, 0x20000800, 0x2000015C, 1024
92
92
 
93
93
 
94
- def _block_size(hst: h.Host) -> int:
95
- info = core.confirm(hst)
96
- # request header 6 + session 4 + connection 1 + address 4 + count 2 (the read answer's 5 + done 2 + status 1 is
97
- # smaller); a little slack for the result header
98
- return (info["max_frame"] - 5 - 6 - 4 - 1 - 4 - 2) // 4 * 4
94
+ def _block_size(dm: riscv.RiscvDm) -> int:
95
+ """Bytes per read_block / write_block: the probe's declared max_length (oep-if-debug §4.5), never a value computed
96
+ from max_frame (riscv.NoMaxLength when the probe declares none)."""
97
+ return dm.max_words * 4
99
98
 
100
99
 
101
100
  def _write_requests(dm: riscv.RiscvDm, address: int, data: bytes, block: int) -> list[tuple[int, int, bytes]]:
@@ -142,7 +141,7 @@ def program(hst: h.Host, dm: riscv.RiscvDm, image: bytes, profile: FlashProfile)
142
141
  image = image + b"\xff" * (-len(image) % profile.page)
143
142
  if profile.size and len(image) > profile.size:
144
143
  raise ValueError(f"image of {len(image)} bytes exceeds {profile.size}")
145
- block = _block_size(hst)
144
+ block = _block_size(dm)
146
145
  t = {}
147
146
  t0 = time.perf_counter()
148
147
  if profile.method == "fast-page":
@@ -71,3 +71,8 @@ def unframe(raw: bytes) -> bytes:
71
71
  if crc16(body) != got:
72
72
  raise CorruptFrame("CRC mismatch")
73
73
  return body
74
+
75
+ def frame_max(message_len: int) -> int:
76
+ """The longest frame a message of this length makes: CRC16, a COBS code per 254 bytes and one more, the delimiter."""
77
+ n = message_len + 2
78
+ return n + (n + 253) // 254 + 1 + 1