oep-client-python 0.0.1__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 (49) hide show
  1. oep_client_python-0.0.1/.gitignore +6 -0
  2. oep_client_python-0.0.1/CHANGELOG.md +7 -0
  3. oep_client_python-0.0.1/LICENSE +21 -0
  4. oep_client_python-0.0.1/PKG-INFO +106 -0
  5. oep_client_python-0.0.1/README.ja.md +84 -0
  6. oep_client_python-0.0.1/pyproject.toml +41 -0
  7. oep_client_python-0.0.1/src/oep_client/__init__.py +5 -0
  8. oep_client_python-0.0.1/src/oep_client/v1/__init__.py +1 -0
  9. oep_client_python-0.0.1/src/oep_client/v1/__main__.py +40 -0
  10. oep_client_python-0.0.1/src/oep_client/v1/arm.py +269 -0
  11. oep_client_python-0.0.1/src/oep_client/v1/capture.py +365 -0
  12. oep_client_python-0.0.1/src/oep_client/v1/catalog.py +181 -0
  13. oep_client_python-0.0.1/src/oep_client/v1/ch32_flash.py +254 -0
  14. oep_client_python-0.0.1/src/oep_client/v1/cobs.py +73 -0
  15. oep_client_python-0.0.1/src/oep_client/v1/console.py +165 -0
  16. oep_client_python-0.0.1/src/oep_client/v1/core.py +172 -0
  17. oep_client_python-0.0.1/src/oep_client/v1/decode.py +65 -0
  18. oep_client_python-0.0.1/src/oep_client/v1/dump.py +157 -0
  19. oep_client_python-0.0.1/src/oep_client/v1/endpoint.py +1479 -0
  20. oep_client_python-0.0.1/src/oep_client/v1/esp32_targets.py +137 -0
  21. oep_client_python-0.0.1/src/oep_client/v1/fake.py +233 -0
  22. oep_client_python-0.0.1/src/oep_client/v1/fake_serial.py +105 -0
  23. oep_client_python-0.0.1/src/oep_client/v1/fake_serve.py +299 -0
  24. oep_client_python-0.0.1/src/oep_client/v1/fixture.py +121 -0
  25. oep_client_python-0.0.1/src/oep_client/v1/frames.py +91 -0
  26. oep_client_python-0.0.1/src/oep_client/v1/hid_stream.py +257 -0
  27. oep_client_python-0.0.1/src/oep_client/v1/host.py +321 -0
  28. oep_client_python-0.0.1/src/oep_client/v1/interfaces.py +119 -0
  29. oep_client_python-0.0.1/src/oep_client/v1/link.py +414 -0
  30. oep_client_python-0.0.1/src/oep_client/v1/message.py +235 -0
  31. oep_client_python-0.0.1/src/oep_client/v1/names.py +86 -0
  32. oep_client_python-0.0.1/src/oep_client/v1/registry.py +59 -0
  33. oep_client_python-0.0.1/src/oep_client/v1/riscv.py +430 -0
  34. oep_client_python-0.0.1/src/oep_client/v1/rp2350.py +85 -0
  35. oep_client_python-0.0.1/src/oep_client/v1/target.py +16 -0
  36. oep_client_python-0.0.1/src/oep_client/v1/uiapduino.py +121 -0
  37. oep_client_python-0.0.1/src/oep_client/v1/usb_stream.py +209 -0
  38. oep_client_python-0.0.1/tests/test_v1_capabilities.py +158 -0
  39. oep_client_python-0.0.1/tests/test_v1_capture.py +104 -0
  40. oep_client_python-0.0.1/tests/test_v1_cobs.py +47 -0
  41. oep_client_python-0.0.1/tests/test_v1_esp32_targets.py +52 -0
  42. oep_client_python-0.0.1/tests/test_v1_fake_spec.py +346 -0
  43. oep_client_python-0.0.1/tests/test_v1_flash_console.py +163 -0
  44. oep_client_python-0.0.1/tests/test_v1_hid_stream.py +141 -0
  45. oep_client_python-0.0.1/tests/test_v1_interfaces.py +360 -0
  46. oep_client_python-0.0.1/tests/test_v1_link_host.py +250 -0
  47. oep_client_python-0.0.1/tests/test_v1_link_serial.py +127 -0
  48. oep_client_python-0.0.1/tests/test_v1_session.py +291 -0
  49. oep_client_python-0.0.1/tests/test_v1_target_parts.py +317 -0
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ *.egg-info/
5
+ build/
6
+ dist/
@@ -0,0 +1,7 @@
1
+ # Changelog / 変更履歴
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.0.1
6
+ - (EN) First beta on PyPI (`pip install oep-client-python`, imported as `oep_client`). OEP v1 host: serial ports always COBS with the probe's raw bytes skipped as noise and opened exclusively (TIOCEXCL), USB vendor bulk / HID and TCP (a broker) with length frames, all under one `Host` (`link.open_host(target)`); the session rules (owner, resend with the same corr, `core.take` for the lock); the standard interfaces; `Host.on_capture` for run recorders; a fake probe (`oep_client.v1.endpoint`) and `python -m oep_client.v1.fake_serve` (pty / TCP) as the working spec for other hosts' tests.
7
+ - (JA) PyPI での最初のβ版(`pip install oep-client-python`、import は `oep_client`)。OEP v1 の host: シリアルの口は常に COBS(probe の生のバイトは雑音として捨てる、排他で開く TIOCEXCL)、USB の vendor bulk / HID と TCP(ブローカー)は長さつきのフレームで、どれも同じ `Host`(`link.open_host(target)`)。セッションの規則(owner、同じ corr での送り直し、ロックの取り方 `core.take`)、標準インターフェース、記録の受け口 `Host.on_capture`。ほかの host の試験のための「動く spec」として、偽の probe(`oep_client.v1.endpoint`)と `python -m oep_client.v1.fake_serve`(pty / TCP)。
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Open Embedded Probe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.5
2
+ Name: oep-client-python
3
+ Version: 0.0.1
4
+ Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe
5
+ Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
6
+ Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
7
+ Author: TANAKA Masayuki
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Embedded Systems
13
+ Classifier: Topic :: Software Development :: Testing
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: pyserial>=3.5
16
+ Requires-Dist: pyusb>=1.3
17
+ Provides-Extra: hid
18
+ Requires-Dist: hidapi>=0.14; extra == 'hid'
19
+ Provides-Extra: usb-async
20
+ Requires-Dist: libusb1>=3; extra == 'usb-async'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # OEP Python client
24
+
25
+ Open Embedded Probe の host 側。v1(oep-spec の `docs/oep-core.ja.md` と `docs/oep-if-*.ja.md`、固める候補の形)を話す。番号は oep-spec の
26
+ `generated/oep-v1/oep_v1_registry.py` をそのまま写した `oep_client.v1.registry` から取る。破壊的変更を前提とする
27
+ 実験段階で、互換 API は約束しない。OEP を初めて読む人は oep-spec の `docs/review-guide.ja.md`(どこに何が書いてあるか)から。
28
+
29
+ target の知識は host にある、という OEP の分担に従う。probe は線と DMI / DP・AP の転送しか知らず、CH32 の flash
30
+ コントローラ、RAM ローダー、RP2350 の boot ROM、Cortex-M の debug レジスタなどはここに置く。
31
+
32
+ ```sh
33
+ pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
34
+ uv run pytest # in a checkout
35
+ ```
36
+
37
+ `import oep_client.v1` だけで使える(`sys.path` に `src/` を足す使い方は不要になった)。番号の表は `tools/sync_registry.sh` で
38
+ oep-spec から写す。PyPI の `oep-client` は別のプロジェクトなので、配布名は `oep-client-python`。
39
+
40
+ リリースは GitHub Actions の Release(workflow_dispatch、version = X.Y.Z か X.Y.ZbN): `tools/prepare_release.py` が
41
+ pyproject.toml と `oep_client.__version__` を書き換え、CHANGELOG.md の Unreleased をその版にし、試験と build の後に commit と tag、
42
+ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md の Unreleased に (EN) / (JA) で書き足しておく。
43
+
44
+ ## モジュール(`oep_client.v1`)
45
+
46
+ | モジュール | 中身 |
47
+ |---|---|
48
+ | `host` | 要求と結果、session_id とロック、`call()`(失敗なら例外)、pipeline、エラーの階層(`OepError` / `Rejected` / `Failed`) |
49
+ | `link` | transport: シリアルの口(常に COBS + CRC、`0x00 <COBS> 0x00`、フレームの外は雑音として捨てる、排他で開く)、USB vendor bulk / HID と TCP(長さつきフレーム、§5.1 の立て直し)、corr による照合と送り直し、`open_host(target)` |
50
+ | `core` | インターフェースを名前で探す(キャッシュつき)、confirm、probe の describe(ラベル、transport の一覧)、ロックの取り方(`take`)、ピンの割り当て(plan)、`Interface` の土台 |
51
+ | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`、`oep.target.riscv-dm`、リセット線の探索、GPIO 経由の attach |
52
+ | `console` | `oep.target.console`(位置つきのストリーム)と、バイト列として読む `ConsoleIO` |
53
+ | `fixture` | `oep.fixture.gpio` / `uart`(revision 1) |
54
+ | `capture` | `oep.fixture.capture`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
55
+ | `esp32_targets` | 独自インターフェース `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target`(oep-probe-arduino の ESP32 の I2C / SPI の target) |
56
+ | `decode` | キャプチャのチャネルの復号(I2C) |
57
+ | `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す) |
58
+ | `arm` | `oep.wire.swd`、`oep.target.arm-adi`、MEM-AP、Cortex-M の停止と関数呼び出し |
59
+ | `ch32_flash` | CH32 の書き込み(RAM ローダー、ページ単位の書き直し) |
60
+ | `rp2350` | RP2350 の boot ROM 経由の flash と reboot |
61
+ | `uiapduino` | UIAPduino のブートローダへの出入り |
62
+ | `catalog` / `names` / `interfaces` / `dump` | 能力の一覧と describe の形、表示 |
63
+ | `fake` / `endpoint` / `fake_serial` / `fake_serve` | 偽の probe(下の「偽の probe」) |
64
+ | `target` | 上の主なものを 1 か所から import する入口(最初の版に合わせて書いた呼び出し側のため) |
65
+
66
+ ## 使い方の例
67
+
68
+ ```python
69
+ from oep_client.v1 import core, link, riscv, ch32_flash
70
+
71
+ hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelining つき
72
+ # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
73
+ core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
74
+ wire = riscv.Wire(hst, "oep.wire.rvswd")
75
+ conn, _ = wire.attach(halt=True)
76
+ dm = riscv.RiscvDm(hst, conn)
77
+ dm.reset_halt()
78
+ result = ch32_flash.program(hst, dm, open("sketch.bin", "rb").read(), ch32_flash.PROFILES["x035"])
79
+ dm.reset(confirm=True)
80
+ wire.detach(conn)
81
+ hst.end()
82
+ ```
83
+
84
+ 能力の一覧は `uv run python -m oep_client.v1 dump --port <probe>`(`--fake p4-x035` でハードウェアなし)。
85
+ 実機での一通りの確認は ArduinoCore-CH32 の `tests/manual/oep_smoke/`(`oep_smoke.py`、`oep_probe_checks.py`)。
86
+
87
+ ## 偽の probe(動く spec)
88
+
89
+ `endpoint.Endpoint` は oep-spec の規範どおりに答える偽の probe で、ch32rv・この client・probe の firmware を突き合わせる
90
+ 「動く spec」として使う(spec が変わったら、probe の firmware より先にここを合わせる)。`fake` は宣言の例(profile:
91
+ `p4-x035`、`esp32-v003`、`p4-bench` = スロット 3 か所と席 2 つの架空の治具)、`fake_serial` はシリアルの口のバイトの側(COBS の
92
+ 候補、生のバイトと bind、セッション中の停止と再開)。
93
+
94
+ 外のプログラムの試験には `fake_serve` を子プロセスで使う:
95
+
96
+ ```sh
97
+ uv run python -m oep_client.v1.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
98
+ --console 'uptime %d\r\n' --every 100
99
+ # 最初の行: PTY /dev/pts/N(--tcp 0 なら PORT n)。stdin を閉じると終わる
100
+ ```
101
+
102
+ pty がシリアルの口(host が TIOCEXCL を掛けて開く)、`--tcp PORT` は `--framing cobs`(シリアルの口)か `--framing length`
103
+ (vendor bulk / TCP の形)。故障の注入は `--drop N`(N 番目の答えを 1 回出さない。要求は実行済みなので送り直しは覚えた答えを
104
+ 受ける)、`--noise TEXT`(答えの前に雑音)、`--corrupt N`(N 番目の答えの CRC を 1 回壊す)。ほかは `--help`。
105
+
106
+ v0 の client(`oep_client.v0`)は 2026-09-26 に消した(git の履歴に残る)。v0 を話す probe はもう無い。
@@ -0,0 +1,84 @@
1
+ # OEP Python client
2
+
3
+ Open Embedded Probe の host 側。v1(oep-spec の `docs/oep-core.ja.md` と `docs/oep-if-*.ja.md`、固める候補の形)を話す。番号は oep-spec の
4
+ `generated/oep-v1/oep_v1_registry.py` をそのまま写した `oep_client.v1.registry` から取る。破壊的変更を前提とする
5
+ 実験段階で、互換 API は約束しない。OEP を初めて読む人は oep-spec の `docs/review-guide.ja.md`(どこに何が書いてあるか)から。
6
+
7
+ target の知識は host にある、という OEP の分担に従う。probe は線と DMI / DP・AP の転送しか知らず、CH32 の flash
8
+ コントローラ、RAM ローダー、RP2350 の boot ROM、Cortex-M の debug レジスタなどはここに置く。
9
+
10
+ ```sh
11
+ pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
12
+ uv run pytest # in a checkout
13
+ ```
14
+
15
+ `import oep_client.v1` だけで使える(`sys.path` に `src/` を足す使い方は不要になった)。番号の表は `tools/sync_registry.sh` で
16
+ oep-spec から写す。PyPI の `oep-client` は別のプロジェクトなので、配布名は `oep-client-python`。
17
+
18
+ リリースは GitHub Actions の Release(workflow_dispatch、version = X.Y.Z か X.Y.ZbN): `tools/prepare_release.py` が
19
+ pyproject.toml と `oep_client.__version__` を書き換え、CHANGELOG.md の Unreleased をその版にし、試験と build の後に commit と tag、
20
+ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md の Unreleased に (EN) / (JA) で書き足しておく。
21
+
22
+ ## モジュール(`oep_client.v1`)
23
+
24
+ | モジュール | 中身 |
25
+ |---|---|
26
+ | `host` | 要求と結果、session_id とロック、`call()`(失敗なら例外)、pipeline、エラーの階層(`OepError` / `Rejected` / `Failed`) |
27
+ | `link` | transport: シリアルの口(常に COBS + CRC、`0x00 <COBS> 0x00`、フレームの外は雑音として捨てる、排他で開く)、USB vendor bulk / HID と TCP(長さつきフレーム、§5.1 の立て直し)、corr による照合と送り直し、`open_host(target)` |
28
+ | `core` | インターフェースを名前で探す(キャッシュつき)、confirm、probe の describe(ラベル、transport の一覧)、ロックの取り方(`take`)、ピンの割り当て(plan)、`Interface` の土台 |
29
+ | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`、`oep.target.riscv-dm`、リセット線の探索、GPIO 経由の attach |
30
+ | `console` | `oep.target.console`(位置つきのストリーム)と、バイト列として読む `ConsoleIO` |
31
+ | `fixture` | `oep.fixture.gpio` / `uart`(revision 1) |
32
+ | `capture` | `oep.fixture.capture`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
33
+ | `esp32_targets` | 独自インターフェース `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target`(oep-probe-arduino の ESP32 の I2C / SPI の target) |
34
+ | `decode` | キャプチャのチャネルの復号(I2C) |
35
+ | `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す) |
36
+ | `arm` | `oep.wire.swd`、`oep.target.arm-adi`、MEM-AP、Cortex-M の停止と関数呼び出し |
37
+ | `ch32_flash` | CH32 の書き込み(RAM ローダー、ページ単位の書き直し) |
38
+ | `rp2350` | RP2350 の boot ROM 経由の flash と reboot |
39
+ | `uiapduino` | UIAPduino のブートローダへの出入り |
40
+ | `catalog` / `names` / `interfaces` / `dump` | 能力の一覧と describe の形、表示 |
41
+ | `fake` / `endpoint` / `fake_serial` / `fake_serve` | 偽の probe(下の「偽の probe」) |
42
+ | `target` | 上の主なものを 1 か所から import する入口(最初の版に合わせて書いた呼び出し側のため) |
43
+
44
+ ## 使い方の例
45
+
46
+ ```python
47
+ from oep_client.v1 import core, link, riscv, ch32_flash
48
+
49
+ hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelining つき
50
+ # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
51
+ core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
52
+ wire = riscv.Wire(hst, "oep.wire.rvswd")
53
+ conn, _ = wire.attach(halt=True)
54
+ dm = riscv.RiscvDm(hst, conn)
55
+ dm.reset_halt()
56
+ result = ch32_flash.program(hst, dm, open("sketch.bin", "rb").read(), ch32_flash.PROFILES["x035"])
57
+ dm.reset(confirm=True)
58
+ wire.detach(conn)
59
+ hst.end()
60
+ ```
61
+
62
+ 能力の一覧は `uv run python -m oep_client.v1 dump --port <probe>`(`--fake p4-x035` でハードウェアなし)。
63
+ 実機での一通りの確認は ArduinoCore-CH32 の `tests/manual/oep_smoke/`(`oep_smoke.py`、`oep_probe_checks.py`)。
64
+
65
+ ## 偽の probe(動く spec)
66
+
67
+ `endpoint.Endpoint` は oep-spec の規範どおりに答える偽の probe で、ch32rv・この client・probe の firmware を突き合わせる
68
+ 「動く spec」として使う(spec が変わったら、probe の firmware より先にここを合わせる)。`fake` は宣言の例(profile:
69
+ `p4-x035`、`esp32-v003`、`p4-bench` = スロット 3 か所と席 2 つの架空の治具)、`fake_serial` はシリアルの口のバイトの側(COBS の
70
+ 候補、生のバイトと bind、セッション中の停止と再開)。
71
+
72
+ 外のプログラムの試験には `fake_serve` を子プロセスで使う:
73
+
74
+ ```sh
75
+ uv run python -m oep_client.v1.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
76
+ --console 'uptime %d\r\n' --every 100
77
+ # 最初の行: PTY /dev/pts/N(--tcp 0 なら PORT n)。stdin を閉じると終わる
78
+ ```
79
+
80
+ pty がシリアルの口(host が TIOCEXCL を掛けて開く)、`--tcp PORT` は `--framing cobs`(シリアルの口)か `--framing length`
81
+ (vendor bulk / TCP の形)。故障の注入は `--drop N`(N 番目の答えを 1 回出さない。要求は実行済みなので送り直しは覚えた答えを
82
+ 受ける)、`--noise TEXT`(答えの前に雑音)、`--corrupt N`(N 番目の答えの CRC を 1 回壊す)。ほかは `--help`。
83
+
84
+ v0 の client(`oep_client.v0`)は 2026-09-26 に消した(git の履歴に残る)。v0 を話す probe はもう無い。
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "oep-client-python"
7
+ version = "0.0.1"
8
+ description = "Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe"
9
+ readme = "README.ja.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "TANAKA Masayuki" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Software Development :: Embedded Systems",
19
+ "Topic :: Software Development :: Testing",
20
+ ]
21
+ dependencies = ["pyserial>=3.5", "pyusb>=1.3"]
22
+
23
+ [project.optional-dependencies]
24
+ usb-async = ["libusb1>=3"] # queued asynchronous IN transfers: streaming near the USB HS ceiling
25
+ hid = ["hidapi>=0.14"] # the vendor HID way in through the OS HID driver (Windows without WinUSB, Linux hidraw)
26
+
27
+ [project.scripts]
28
+ oep = "oep_client.v1.__main__:main"
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/Open-Embedded-Probe/oep-client-python"
32
+ Repository = "https://github.com/Open-Embedded-Probe/oep-client-python"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/oep_client"]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ only-include = ["src/oep_client", "tests", "README.ja.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
39
+
40
+ [dependency-groups]
41
+ dev = ["pytest>=8"]
@@ -0,0 +1,5 @@
1
+ """Open Embedded Probe (OEP) host client. `oep_client.v1` speaks the v1 protocol (oep-spec docs/oep-core.ja.md)."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.0.1"
@@ -0,0 +1 @@
1
+ """Draft: capability discovery by name (oep-spec capability-*.ja.md). No hardware yet."""
@@ -0,0 +1,40 @@
1
+ """Draft OEP capability discovery by name, on an in-process fake or a v1 draft probe.
2
+
3
+ uv run python -m oep_client.v1 dump --port /run/board-identify/by-id/<probe>
4
+ uv run python -m oep_client.v1 dump --fake p4-x035
5
+ uv run python -m oep_client.v1 dump --fake esp32-v003 --prefix oep.fixture
6
+ uv run python -m oep_client.v1 dump --fake p4-x035 --prefix oep.target --json
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+
14
+ from . import dump, fake, host, link
15
+
16
+
17
+ def main(argv=None) -> int:
18
+ parser = argparse.ArgumentParser(description="OEP capability discovery (draft)")
19
+ sub = parser.add_subparsers(dest="command", required=True)
20
+ d = sub.add_parser("dump", help="list and describe every interface a probe offers")
21
+ src = d.add_mutually_exclusive_group(required=True)
22
+ src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
23
+ src.add_argument("--port", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID] (lock-free reads only)")
24
+ d.add_argument("--prefix", default="", help="only names under this namespace (label boundaries)")
25
+ d.add_argument("--exact", action="store_true", help="the prefix is a whole name")
26
+ d.add_argument("--json", action="store_true", help="machine-readable output")
27
+ args = parser.parse_args(argv)
28
+
29
+ if args.fake:
30
+ call = fake.PROFILES[args.fake]().call
31
+ else:
32
+ hst = link.open_host(args.port)
33
+ call = lambda fn, op, payload: hst.request(fn, op, payload, locked=False).payload # noqa: E731
34
+ caps = dump.collect(call, args.prefix, args.exact)
35
+ sys.stdout.write(dump.to_json(caps) + "\n" if args.json else dump.to_text(caps))
36
+ return 0
37
+
38
+
39
+ if __name__ == "__main__":
40
+ sys.exit(main())
@@ -0,0 +1,269 @@
1
+ """oep.wire.swd and oep.target.arm-adi, revision 1 (oep-spec oep-if-debug §1, §5-§6).
2
+
3
+ The probe moves raw DP / AP transfers and MEM-AP blocks; everything above - power-up, SELECT (ADIv5 APSEL/APBANKSEL or
4
+ ADIv6 AP addresses), CSW, the Cortex-M debug registers - is here, as target knowledge belongs to the host.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import struct
10
+
11
+ from . import host as h, message as m, registry as reg
12
+ from .core import Interface
13
+ from .riscv import OK, TargetError, WireBase, check, ran, status_name # noqa: F401
14
+
15
+ DP_DPIDR = DP_ABORT = 0x0
16
+ DP_CTRL_STAT = 0x4
17
+ DP_SELECT = 0x8
18
+ DP_RDBUFF = 0xC
19
+ _ADI = reg.TARGET_ARM_ADI
20
+
21
+
22
+ class SwdWire(WireBase):
23
+ NAME = "oep.wire.swd"
24
+ TAG_TARGETSEL = reg.WIRE_SWD.tlv["attach"]["targetsel"]
25
+
26
+ def __init__(self, hst: h.Host):
27
+ super().__init__(hst)
28
+ self.speed_hz = 0
29
+ self.existing = False
30
+
31
+ def attach(self, targetsel: int | None = None, max_speed: int | None = None,
32
+ pins: tuple[int, int] | None = None) -> tuple[int, int, bool]:
33
+ """-> (connection, DPIDR, woke from dormant). targetsel (multidrop) and max_speed go as critical TLVs: a probe
34
+ that cannot honour them refuses. self.existing: the wire was attached already (its connection returned)."""
35
+ body = self._speed_tlv(max_speed) + self._pins_tlv(pins)
36
+ if targetsel is not None:
37
+ body += m.tlv(self.TAG_TARGETSEL, struct.pack("<I", targetsel), critical=True)
38
+ rd = m.Reader(self._call(self.ATTACH, body).payload)
39
+ conn, dpidr, flags, self.speed_hz = rd.take("HIBI")
40
+ self.existing = bool(flags & 2)
41
+ rd.tail()
42
+ return conn, dpidr, bool(flags & 1)
43
+
44
+
45
+ class AdiError(TargetError):
46
+ pass
47
+
48
+
49
+ def transfer_reads(steps: bytes) -> list[bool]:
50
+ """Per transfer in a packed list: True for a read (1 byte), False for a write (1 + 4 bytes)."""
51
+ out, at = [], 0
52
+ while at < len(steps):
53
+ read = bool(steps[at] & 2)
54
+ out.append(read)
55
+ at += 1 if read else 5
56
+ if at != len(steps):
57
+ raise ValueError("the transfer list ends inside a write")
58
+ return out
59
+
60
+
61
+ class ArmAdi(Interface):
62
+ NAME = "oep.target.arm-adi"
63
+ REVISION = 1
64
+ TRANSFER, READ_BLOCK, WRITE_BLOCK = _ADI.op["transfer"], _ADI.op["read_block"], _ADI.op["write_block"]
65
+
66
+ def __init__(self, hst: h.Host, conn: int, adiv6: bool = False):
67
+ super().__init__(hst, prefix=struct.pack("<H", conn))
68
+ self.conn = conn
69
+ self.adiv6 = adiv6
70
+ self._select: int | None = None
71
+
72
+ # ---- raw transfers ----
73
+ @staticmethod
74
+ def req(ap: bool, read: bool, addr: int, value: int = 0) -> bytes:
75
+ b = bytes([int(ap) | (int(read) << 1) | (((addr >> 2) & 3) << 2)])
76
+ return b if read else b + struct.pack("<I", value)
77
+
78
+ def transfer(self, steps: bytes) -> list[int]:
79
+ """A packed transfer list (req() concatenated). -> the values read, in order (an AP read's value arrives one
80
+ transfer late, as on the wire). A list that stopped raises AdiError (status, done, the values it read, and
81
+ self.last_ack = the raw ACK of the last transfer)."""
82
+ reads = transfer_reads(steps)
83
+ r = self._request(self.TRANSFER, struct.pack("<H", len(reads)) + steps)
84
+ rd = ran(r)
85
+ done, status, self.last_ack = rd.take("HBB")
86
+ values = rd.words(sum(reads[:done]))
87
+ rd.tail()
88
+ if status != OK or not r.succeeded or done != len(reads):
89
+ raise AdiError(f"transfer (ack {self.last_ack:#x})", status, r, done=done, values=values)
90
+ return values
91
+ def dp_read(self, addr: int) -> int:
92
+ return self.transfer(self.req(False, True, addr))[0]
93
+
94
+ def dp_write(self, addr: int, value: int) -> None:
95
+ self.transfer(self.req(False, False, addr, value))
96
+
97
+ def select(self, value: int) -> None:
98
+ if value != self._select:
99
+ self.dp_write(DP_SELECT, value)
100
+ self._select = value
101
+
102
+ def _ap_select(self, ap: int, reg: int) -> None:
103
+ """ADIv5: ap = APSEL (0..255), reg = register offset in the AP. ADIv6: ap = the AP's base address."""
104
+ if self.adiv6:
105
+ self.select((ap + reg) & ~0xF)
106
+ else:
107
+ self.select((ap << 24) | (reg & 0xF0))
108
+
109
+ def ap_read(self, ap: int, reg: int) -> int:
110
+ self._ap_select(ap, reg)
111
+ return self.transfer(self.req(True, True, reg) + self.req(False, True, DP_RDBUFF))[1] # posted
112
+
113
+ def ap_write(self, ap: int, reg: int, value: int) -> None:
114
+ self._ap_select(ap, reg)
115
+ self.transfer(self.req(True, False, reg, value))
116
+
117
+ def power_up(self) -> int:
118
+ """Clear sticky errors, request debug + system power, wait for both acks. -> CTRL/STAT"""
119
+ self.dp_write(DP_ABORT, 0x1E)
120
+ self._select = None
121
+ self.select(0)
122
+ self.dp_write(DP_CTRL_STAT, 0x50000000)
123
+ for _ in range(100):
124
+ cs = self.dp_read(DP_CTRL_STAT)
125
+ if (cs >> 29) & 1 and (cs >> 31) & 1:
126
+ return cs
127
+ raise h.OepError(f"no power-up ack: CTRL/STAT {cs:#010x}")
128
+
129
+
130
+ class MemAp:
131
+ """One MEM-AP (ADIv5 APSEL or ADIv6 base address) with 32-bit, auto-incrementing access."""
132
+
133
+ def __init__(self, adi: ArmAdi, ap: int, csw_set: int = 0, csw_clear: int = 0):
134
+ """csw_set / csw_clear: target-specific CSW bits (protection, security). The RP2350's AHB-APs come up
135
+ non-secure (CSW bit 30), and its SRAM then faults: pass csw_clear=1 << 30 there (2026-09-24)."""
136
+ self.adi, self.ap = adi, ap
137
+ self.base = 0xD00 if adi.adiv6 else 0x00 # CSW, TAR, DRW at base + 0x0 / 0x4 / 0xC
138
+ csw = adi.ap_read(ap, self.base)
139
+ adi.ap_write(ap, self.base, (((csw & ~0x37) | 0x12) | csw_set) & ~csw_clear) # 32 bits, AddrInc single
140
+ adi._ap_select(ap, self.base) # the bank the block operations assume
141
+ # Words per block operation, from the probe's frame limit: request header 6 + session 4 + connection 2 +
142
+ # address 4 + count 2 on the way in (the answer's 5 + done 2 + status 1 is smaller).
143
+ from .core import confirm
144
+ self.chunk = max(1, (confirm(adi.host)["max_frame"] - 18) // 4)
145
+
146
+ def write_many(self, pairs: list[tuple[int, int]]) -> None:
147
+ """Scattered single-word writes in one transfer list (TAR, DRW per word, RDBUFF at the end so the last one
148
+ has landed): one round trip instead of one per word - what a debug-register sequence needs."""
149
+ self.adi._ap_select(self.ap, self.base)
150
+ steps = b"".join(self.adi.req(True, False, self.base + 0x4, a) + self.adi.req(True, False, self.base + 0xC, v)
151
+ for a, v in pairs)
152
+ self.adi.transfer(steps + self.adi.req(False, True, DP_RDBUFF))
153
+
154
+ def read_block(self, address: int, words: int) -> list[int]:
155
+ out, chunk = [], self.chunk
156
+ for off in range(0, words, chunk):
157
+ self.adi._ap_select(self.ap, self.base)
158
+ n = min(chunk, words - off)
159
+ r = self.adi._request(ArmAdi.READ_BLOCK, struct.pack("<IH", address + off * 4, n))
160
+ rd = ran(r)
161
+ done, status = rd.take("HB")
162
+ got = rd.words(done)
163
+ rd.tail()
164
+ if status != OK or not r.succeeded or done != n:
165
+ raise AdiError("read_block", status, r, done=off + done, values=out + got)
166
+ out += got
167
+ return out
168
+
169
+ def write_block(self, address: int, values: list[int]) -> None:
170
+ chunk = self.chunk
171
+ for off in range(0, len(values), chunk):
172
+ self.adi._ap_select(self.ap, self.base)
173
+ part = values[off:off + chunk]
174
+ r = self.adi._request(ArmAdi.WRITE_BLOCK, struct.pack("<IH", address + off * 4, len(part))
175
+ + struct.pack(f"<{len(part)}I", *part))
176
+ rd = ran(r)
177
+ done, status = rd.take("HB")
178
+ rd.tail()
179
+ if status != OK or not r.succeeded:
180
+ raise AdiError("write_block", status, r, done=off + done)
181
+
182
+ def read32(self, address: int) -> int:
183
+ return self.read_block(address, 1)[0]
184
+
185
+ def write32(self, address: int, value: int) -> None:
186
+ self.write_block(address, [value])
187
+
188
+
189
+ class CortexM:
190
+ """Armv7-M / Armv8-M core debug through a MEM-AP: halt, resume, core registers through DCRSR / DCRDR, and running
191
+ a function on the target (arguments in r0-r3, LR at a BKPT in RAM, run until the core halts on it) - the way a
192
+ host-side flash algorithm drives the target's own ROM or a RAM loader."""
193
+
194
+ DHCSR, DCRSR, DCRDR, AIRCR = 0xE000EDF0, 0xE000EDF4, 0xE000EDF8, 0xE000ED0C
195
+ KEY = 0xA05F0000
196
+ C_DEBUGEN, C_HALT, C_MASKINTS = 1, 2, 8
197
+ S_REGRDY, S_HALT = 1 << 16, 1 << 17
198
+ SP, LR, PC, XPSR = 13, 14, 15, 16
199
+
200
+ def __init__(self, mem: MemAp, bkpt_at: int, stack_top: int):
201
+ """bkpt_at: a word of RAM the target does not need (the return breakpoint goes there); stack_top: where the
202
+ called function's stack starts (its RAM below is clobbered)."""
203
+ self.mem, self.bkpt_at, self.stack_top = mem, bkpt_at, stack_top
204
+
205
+ def _wait(self, mask: int, timeout: float):
206
+ import time
207
+ deadline = time.monotonic() + timeout
208
+ while True:
209
+ v = self.mem.read32(self.DHCSR)
210
+ if v & mask:
211
+ return v
212
+ if time.monotonic() > deadline:
213
+ raise TimeoutError(f"DHCSR {v:#010x}: waiting for {mask:#x}")
214
+
215
+ def halted(self) -> bool:
216
+ return bool(self.mem.read32(self.DHCSR) & self.S_HALT)
217
+
218
+ def halt(self) -> None:
219
+ self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
220
+ self._wait(self.S_HALT, 1.0)
221
+
222
+ def resume(self, mask_ints: bool = False) -> None:
223
+ self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | (self.C_MASKINTS if mask_ints else 0))
224
+
225
+ def release(self) -> None:
226
+ """Run, debug off. C_MASKINTS is cleared first: it lives in the debug domain and survives every reset but
227
+ power-on, and firmware left with it set runs without SysTick / USB interrupts (RP2350, 2026-09-24)."""
228
+ self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
229
+ self.mem.write32(self.DHCSR, self.KEY)
230
+
231
+ def reg(self, n: int) -> int:
232
+ self.mem.write32(self.DCRSR, n)
233
+ self._wait(self.S_REGRDY, 1.0)
234
+ return self.mem.read32(self.DCRDR)
235
+
236
+ def set_reg(self, n: int, value: int) -> None:
237
+ self.mem.write32(self.DCRDR, value)
238
+ self.mem.write32(self.DCRSR, (1 << 16) | n)
239
+ self._wait(self.S_REGRDY, 1.0)
240
+
241
+ def prepare_call(self, fn: int, args=()) -> None:
242
+ """Registers for fn(args...): r0-r3, SP, LR to the breakpoint, PC, Thumb bit, no active exception. The
243
+ writes go out as one transfer list; a register write takes the core a few cycles and each SWD transfer
244
+ takes microseconds, so S_REGRDY is checked once at the end rather than after each."""
245
+ xpsr = (self.reg(self.XPSR) | 1 << 24) & ~0x1FF
246
+ regs = [*enumerate(args), (self.SP, self.stack_top), (self.LR, self.bkpt_at | 1), (self.PC, fn & ~1),
247
+ (self.XPSR, xpsr)]
248
+ pairs = [(self.bkpt_at, 0xBE00BE00)] # bkpt #0, twice
249
+ for n, value in regs:
250
+ pairs += [(self.DCRDR, value), (self.DCRSR, (1 << 16) | n)]
251
+ self.mem.write_many(pairs)
252
+ self._wait(self.S_REGRDY, 1.0)
253
+
254
+ def call(self, fn: int, args=(), timeout: float = 10.0) -> int:
255
+ """Run fn(args...) on the halted core with interrupts masked (their handlers may live in flash that the call
256
+ makes unreadable), wait for the breakpoint, clear the mask, return r0."""
257
+ self.prepare_call(fn, args)
258
+ self.resume(mask_ints=True)
259
+ self._wait(self.S_HALT, timeout)
260
+ self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT) # MASKINTS off while halted
261
+ pc = self.reg(self.PC)
262
+ if pc & ~3 != self.bkpt_at:
263
+ raise h.OepError(f"stopped at {pc:#010x}, not at the return breakpoint")
264
+ return self.reg(0)
265
+
266
+ def sys_reset(self) -> None:
267
+ """AIRCR.SYSRESETREQ: the core restarts; debug-domain state (DHCSR) survives, so clear the mask first."""
268
+ self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
269
+ self.mem.write32(self.AIRCR, 0x05FA0004)