oep-client-python 0.0.1__tar.gz → 0.0.3__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 (56) hide show
  1. oep_client_python-0.0.3/CHANGELOG.md +17 -0
  2. oep_client_python-0.0.3/PKG-INFO +125 -0
  3. {oep_client_python-0.0.1 → oep_client_python-0.0.3}/README.ja.md +20 -6
  4. oep_client_python-0.0.3/README.md +103 -0
  5. {oep_client_python-0.0.1 → oep_client_python-0.0.3}/pyproject.toml +4 -4
  6. oep_client_python-0.0.3/src/oep_client/__init__.py +5 -0
  7. oep_client_python-0.0.3/src/oep_client/__main__.py +217 -0
  8. oep_client_python-0.0.3/src/oep_client/config.py +228 -0
  9. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/endpoint.py +7 -3
  10. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/fake_serve.py +54 -12
  11. oep_client_python-0.0.1/tests/test_v1_capabilities.py → oep_client_python-0.0.3/tests/test_capabilities.py +1 -1
  12. oep_client_python-0.0.1/tests/test_v1_capture.py → oep_client_python-0.0.3/tests/test_capture.py +3 -3
  13. oep_client_python-0.0.1/tests/test_v1_cobs.py → oep_client_python-0.0.3/tests/test_cobs.py +1 -1
  14. oep_client_python-0.0.3/tests/test_config.py +67 -0
  15. oep_client_python-0.0.1/tests/test_v1_esp32_targets.py → oep_client_python-0.0.3/tests/test_esp32_targets.py +2 -2
  16. oep_client_python-0.0.1/tests/test_v1_fake_spec.py → oep_client_python-0.0.3/tests/test_fake_spec.py +30 -4
  17. oep_client_python-0.0.1/tests/test_v1_flash_console.py → oep_client_python-0.0.3/tests/test_flash_console.py +2 -2
  18. oep_client_python-0.0.1/tests/test_v1_hid_stream.py → oep_client_python-0.0.3/tests/test_hid_stream.py +1 -1
  19. oep_client_python-0.0.1/tests/test_v1_interfaces.py → oep_client_python-0.0.3/tests/test_interfaces.py +3 -3
  20. oep_client_python-0.0.1/tests/test_v1_link_host.py → oep_client_python-0.0.3/tests/test_link_host.py +1 -1
  21. oep_client_python-0.0.1/tests/test_v1_link_serial.py → oep_client_python-0.0.3/tests/test_link_serial.py +2 -2
  22. oep_client_python-0.0.1/tests/test_v1_session.py → oep_client_python-0.0.3/tests/test_session.py +3 -3
  23. oep_client_python-0.0.1/tests/test_v1_target_parts.py → oep_client_python-0.0.3/tests/test_target_parts.py +2 -2
  24. oep_client_python-0.0.1/CHANGELOG.md +0 -7
  25. oep_client_python-0.0.1/PKG-INFO +0 -106
  26. oep_client_python-0.0.1/src/oep_client/__init__.py +0 -5
  27. oep_client_python-0.0.1/src/oep_client/v1/__init__.py +0 -1
  28. oep_client_python-0.0.1/src/oep_client/v1/__main__.py +0 -40
  29. {oep_client_python-0.0.1 → oep_client_python-0.0.3}/.gitignore +0 -0
  30. {oep_client_python-0.0.1 → oep_client_python-0.0.3}/LICENSE +0 -0
  31. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/arm.py +0 -0
  32. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/capture.py +0 -0
  33. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/catalog.py +0 -0
  34. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/ch32_flash.py +0 -0
  35. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/cobs.py +0 -0
  36. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/console.py +0 -0
  37. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/core.py +0 -0
  38. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/decode.py +0 -0
  39. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/dump.py +0 -0
  40. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/esp32_targets.py +0 -0
  41. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/fake.py +0 -0
  42. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/fake_serial.py +0 -0
  43. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/fixture.py +0 -0
  44. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/frames.py +0 -0
  45. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/hid_stream.py +0 -0
  46. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/host.py +0 -0
  47. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/interfaces.py +0 -0
  48. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/link.py +0 -0
  49. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/message.py +0 -0
  50. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/names.py +0 -0
  51. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/registry.py +0 -0
  52. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/riscv.py +0 -0
  53. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/rp2350.py +0 -0
  54. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/target.py +0 -0
  55. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/uiapduino.py +0 -0
  56. {oep_client_python-0.0.1/src/oep_client/v1 → oep_client_python-0.0.3/src/oep_client}/usb_stream.py +0 -0
@@ -0,0 +1,17 @@
1
+ # Changelog / 変更履歴
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.0.3
6
+ - (EN) `oep_client.config` (oep.probe.config: slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) and the `oep config show | slot | bind | remove | save | erase` command. An English README.md, also the PyPI page.
7
+ - (JA) `oep_client.config`(oep.probe.config: スロット、bind、plan / label / idle の項目、get / set / save / erase、スロットと bind の今の状態)と、`oep config show | slot | bind | remove | save | erase` の命令。英語の README.md(PyPI のページも)。
8
+
9
+ ## 0.0.2
10
+ - (EN) Breaking: the modules move from `oep_client.v1.*` to `oep_client.*` (`from oep_client import link`, `python -m oep_client`, `python -m oep_client.fake_serve`). The protocol revision stays in the registry and confirm.
11
+ - (JA) 破壊的変更: モジュールを `oep_client.v1.*` から `oep_client.*` に移した(`from oep_client import link`、`python -m oep_client`、`python -m oep_client.fake_serve`)。プロトコルの revision は registry と confirm が持つ。
12
+ - (EN) `fake_serve --uart-plan` puts the first fixture UART's RX / TX plan in at boot (as a saved config), and `--uart-rx TEXT` feeds its RX every `--every` ms once configured.
13
+ - (JA) `fake_serve --uart-plan` で最初の fixture UART の RX / TX の plan を起動時に入れる(保存した設定として)。`--uart-rx TEXT` で、configure の後、その RX に `--every` ms ごとに文字を入れる。
14
+
15
+ ## 0.0.1
16
+ - (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.
17
+ - (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,125 @@
1
+ Metadata-Version: 2.5
2
+ Name: oep-client-python
3
+ Version: 0.0.3
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
+ [日本語](README.ja.md)
26
+
27
+ The host side of Open Embedded Probe (OEP). It speaks the v1 protocol of
28
+ [oep-spec](https://github.com/Open-Embedded-Probe/oep-spec) (`docs/oep-core.ja.md` and the standard interfaces
29
+ `docs/oep-if-*.ja.md`, a candidate being settled). The wire numbers come from `oep_client.registry`, a verbatim copy of
30
+ oep-spec's generated `generated/oep-v1/oep_v1_registry.py`. This is an experimental stage: breaking changes are expected and
31
+ no compatible API is promised. For a map of the specification, start with oep-spec's `docs/review-guide.ja.md`.
32
+
33
+ It follows OEP's division of work: the knowledge of the target lives in the host. The probe knows only its wires and DMI /
34
+ DP-AP transfers; the CH32 flash controller, the RAM loader, the RP2350 boot ROM, the Cortex-M debug registers and so on are
35
+ here.
36
+
37
+ ```sh
38
+ pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
39
+ uv run pytest # in a checkout
40
+ ```
41
+
42
+ `import oep_client` is all it takes. The registry is copied from oep-spec with `tools/sync_registry.sh`. PyPI's
43
+ `oep-client` is another project, so the distribution is named `oep-client-python`.
44
+
45
+ Releases: run the GitHub Actions workflow Release (workflow_dispatch, version X.Y.Z or X.Y.ZbN). `tools/prepare_release.py`
46
+ sets the version in pyproject.toml and `oep_client.__version__` and turns CHANGELOG.md's Unreleased into that version; after
47
+ the tests and the build it commits, tags, makes the GitHub Release and publishes to PyPI (Trusted Publishing). Record changes
48
+ under Unreleased in CHANGELOG.md, (EN) and (JA).
49
+
50
+ ## Modules (`oep_client`)
51
+
52
+ | Module | Contents |
53
+ |---|---|
54
+ | `host` | requests and results, the session id and the lock, `call()` (raises unless it worked), pipelining, the errors (`OepError` / `Rejected` / `Failed`) |
55
+ | `link` | transports: serial ports (always COBS + CRC as `0x00 <COBS> 0x00`, bytes outside frames skipped as noise, opened exclusively), USB vendor bulk / HID and TCP (length frames, the §5.1 resync); matching by corr and resending; `open_host(target)` |
56
+ | `core` | interfaces by name (cached), confirm, the probe's describe (labels, the transport list), taking the lock (`take`), the pin plan, the `Interface` base |
57
+ | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`, `oep.target.riscv-dm`, finding the reset line, attach through GPIO |
58
+ | `console` | `oep.target.console` (position streams) and `ConsoleIO`, read as bytes |
59
+ | `fixture` | `oep.fixture.gpio` / `uart` (revision 1) |
60
+ | `config` | `oep.probe.config` (slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) |
61
+ | `capture` | `oep.fixture.capture` (revision 1, oep-spec oep-if-capture). Every segment read goes to the `Host.on_capture` callbacks as a `CaptureRecord` (the hook for run recorders; no wireskein dependency) |
62
+ | `esp32_targets` | the custom interfaces `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target` (the ESP32 I2C / SPI targets of oep-probe-arduino) |
63
+ | `decode` | decoding capture channels (I2C) |
64
+ | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec) |
65
+ | `arm` | `oep.wire.swd`, `oep.target.arm-adi`, MEM-AP, halting and calling functions on a Cortex-M |
66
+ | `ch32_flash` | writing a CH32 (a RAM loader, page by page) |
67
+ | `rp2350` | flash and reboot through the RP2350 boot ROM |
68
+ | `uiapduino` | into and out of the UIAPduino bootloader |
69
+ | `catalog` / `names` / `interfaces` / `dump` | the capability list and describe shapes, display |
70
+ | `fake` / `endpoint` / `fake_serial` / `fake_serve` | the fake probe (below) |
71
+ | `target` | one place to import the main ones from |
72
+
73
+ ## Example
74
+
75
+ ```python
76
+ from oep_client import core, link, riscv, ch32_flash
77
+
78
+ hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelined
79
+ # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
80
+ core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
81
+ wire = riscv.Wire(hst, "oep.wire.rvswd")
82
+ conn, _ = wire.attach(halt=True)
83
+ dm = riscv.RiscvDm(hst, conn)
84
+ dm.reset_halt()
85
+ result = ch32_flash.program(hst, dm, open("sketch.bin", "rb").read(), ch32_flash.PROFILES["x035"])
86
+ dm.reset(confirm=True)
87
+ wire.detach(conn)
88
+ hst.end()
89
+ ```
90
+
91
+ ## The `oep` command
92
+
93
+ ```sh
94
+ oep dump --port <probe> # what the probe offers (--fake p4-x035: no hardware)
95
+ oep config show <probe> # the settings and the live slot / bind state
96
+ oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
97
+ oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
98
+ oep config save <probe> # kept over a restart (also: remove, erase)
99
+ ```
100
+
101
+ `<probe>` is a serial port, `tcp://HOST:PORT` or `usb[:VID:PID[:SERIAL]]`. A change takes the lock (owner "oep config") and
102
+ ends the session after it; it takes effect at once and, after `save`, stays over a restart.
103
+
104
+ A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`, `oep_probe_checks.py`).
105
+
106
+ ## The fake probe (a working spec)
107
+
108
+ `endpoint.Endpoint` is a fake probe that answers as oep-spec says; ch32rv, this client and the probe firmware are checked
109
+ against it (when the spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
110
+ `p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats), `fake_serial` the byte side of a serial
111
+ port (COBS candidates, raw bytes and binds, held during a session and resumed after it).
112
+
113
+ Other programs' tests run `fake_serve` as a child process:
114
+
115
+ ```sh
116
+ python -m oep_client.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
117
+ --console 'uptime %d\r\n' --every 100
118
+ # first line: PTY /dev/pts/N (PORT n with --tcp 0); it ends when stdin closes
119
+ ```
120
+
121
+ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--framing cobs` (a serial port) or
122
+ `--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
123
+ so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
124
+ broken once). `--uart-plan` / `--uart-rx` give the first fixture UART a plan and RX bytes, `--run-hook` a host's own model of
125
+ riscv-dm run. The rest: `--help`.
@@ -1,7 +1,9 @@
1
1
  # OEP Python client
2
2
 
3
+ [English](README.md)
4
+
3
5
  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` から取る。破壊的変更を前提とする
6
+ `generated/oep-v1/oep_v1_registry.py` をそのまま写した `oep_client.registry` から取る。破壊的変更を前提とする
5
7
  実験段階で、互換 API は約束しない。OEP を初めて読む人は oep-spec の `docs/review-guide.ja.md`(どこに何が書いてあるか)から。
6
8
 
7
9
  target の知識は host にある、という OEP の分担に従う。probe は線と DMI / DP・AP の転送しか知らず、CH32 の flash
@@ -12,14 +14,14 @@ pip install oep-client-python # PyPI (import oep_client); a checkout: pip in
12
14
  uv run pytest # in a checkout
13
15
  ```
14
16
 
15
- `import oep_client.v1` だけで使える(`sys.path` に `src/` を足す使い方は不要になった)。番号の表は `tools/sync_registry.sh` で
17
+ `import oep_client` だけで使える(`sys.path` に `src/` を足す使い方は不要になった)。番号の表は `tools/sync_registry.sh` で
16
18
  oep-spec から写す。PyPI の `oep-client` は別のプロジェクトなので、配布名は `oep-client-python`。
17
19
 
18
20
  リリースは GitHub Actions の Release(workflow_dispatch、version = X.Y.Z か X.Y.ZbN): `tools/prepare_release.py` が
19
21
  pyproject.toml と `oep_client.__version__` を書き換え、CHANGELOG.md の Unreleased をその版にし、試験と build の後に commit と tag、
20
22
  GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md の Unreleased に (EN) / (JA) で書き足しておく。
21
23
 
22
- ## モジュール(`oep_client.v1`)
24
+ ## モジュール(`oep_client`)
23
25
 
24
26
  | モジュール | 中身 |
25
27
  |---|---|
@@ -29,6 +31,7 @@ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md
29
31
  | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`、`oep.target.riscv-dm`、リセット線の探索、GPIO 経由の attach |
30
32
  | `console` | `oep.target.console`(位置つきのストリーム)と、バイト列として読む `ConsoleIO` |
31
33
  | `fixture` | `oep.fixture.gpio` / `uart`(revision 1) |
34
+ | `config` | `oep.probe.config`(スロット、bind、plan / label / idle の項目、get / set / save / erase、スロットと bind の今の状態) |
32
35
  | `capture` | `oep.fixture.capture`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
33
36
  | `esp32_targets` | 独自インターフェース `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target`(oep-probe-arduino の ESP32 の I2C / SPI の target) |
34
37
  | `decode` | キャプチャのチャネルの復号(I2C) |
@@ -44,7 +47,7 @@ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md
44
47
  ## 使い方の例
45
48
 
46
49
  ```python
47
- from oep_client.v1 import core, link, riscv, ch32_flash
50
+ from oep_client import core, link, riscv, ch32_flash
48
51
 
49
52
  hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelining つき
50
53
  # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
@@ -59,7 +62,18 @@ wire.detach(conn)
59
62
  hst.end()
60
63
  ```
61
64
 
62
- 能力の一覧は `uv run python -m oep_client.v1 dump --port <probe>`(`--fake p4-x035` でハードウェアなし)。
65
+ ## `oep` の命令
66
+
67
+ ```sh
68
+ oep dump --port <probe> # 能力の一覧(--fake p4-x035 でハードウェアなし)
69
+ oep config show <probe> # 設定とスロット / bind の今の状態
70
+ oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
71
+ oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
72
+ oep config save <probe> # 再起動の後も残す(remove / erase もある)
73
+ ```
74
+
75
+ `<probe>` はシリアルの口、`tcp://HOST:PORT`、`usb[:VID:PID[:SERIAL]]`。変更はロックを取り(owner "oep config")、終わったら
76
+ セッションを閉じる。変更はすぐ効き、`save` の後は再起動しても残る。
63
77
  実機での一通りの確認は ArduinoCore-CH32 の `tests/manual/oep_smoke/`(`oep_smoke.py`、`oep_probe_checks.py`)。
64
78
 
65
79
  ## 偽の probe(動く spec)
@@ -72,7 +86,7 @@ hst.end()
72
86
  外のプログラムの試験には `fake_serve` を子プロセスで使う:
73
87
 
74
88
  ```sh
75
- uv run python -m oep_client.v1.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
89
+ uv run python -m oep_client.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
76
90
  --console 'uptime %d\r\n' --every 100
77
91
  # 最初の行: PTY /dev/pts/N(--tcp 0 なら PORT n)。stdin を閉じると終わる
78
92
  ```
@@ -0,0 +1,103 @@
1
+ # OEP Python client
2
+
3
+ [日本語](README.ja.md)
4
+
5
+ The host side of Open Embedded Probe (OEP). It speaks the v1 protocol of
6
+ [oep-spec](https://github.com/Open-Embedded-Probe/oep-spec) (`docs/oep-core.ja.md` and the standard interfaces
7
+ `docs/oep-if-*.ja.md`, a candidate being settled). The wire numbers come from `oep_client.registry`, a verbatim copy of
8
+ oep-spec's generated `generated/oep-v1/oep_v1_registry.py`. This is an experimental stage: breaking changes are expected and
9
+ no compatible API is promised. For a map of the specification, start with oep-spec's `docs/review-guide.ja.md`.
10
+
11
+ It follows OEP's division of work: the knowledge of the target lives in the host. The probe knows only its wires and DMI /
12
+ DP-AP transfers; the CH32 flash controller, the RAM loader, the RP2350 boot ROM, the Cortex-M debug registers and so on are
13
+ here.
14
+
15
+ ```sh
16
+ pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
17
+ uv run pytest # in a checkout
18
+ ```
19
+
20
+ `import oep_client` is all it takes. The registry is copied from oep-spec with `tools/sync_registry.sh`. PyPI's
21
+ `oep-client` is another project, so the distribution is named `oep-client-python`.
22
+
23
+ Releases: run the GitHub Actions workflow Release (workflow_dispatch, version X.Y.Z or X.Y.ZbN). `tools/prepare_release.py`
24
+ sets the version in pyproject.toml and `oep_client.__version__` and turns CHANGELOG.md's Unreleased into that version; after
25
+ the tests and the build it commits, tags, makes the GitHub Release and publishes to PyPI (Trusted Publishing). Record changes
26
+ under Unreleased in CHANGELOG.md, (EN) and (JA).
27
+
28
+ ## Modules (`oep_client`)
29
+
30
+ | Module | Contents |
31
+ |---|---|
32
+ | `host` | requests and results, the session id and the lock, `call()` (raises unless it worked), pipelining, the errors (`OepError` / `Rejected` / `Failed`) |
33
+ | `link` | transports: serial ports (always COBS + CRC as `0x00 <COBS> 0x00`, bytes outside frames skipped as noise, opened exclusively), USB vendor bulk / HID and TCP (length frames, the §5.1 resync); matching by corr and resending; `open_host(target)` |
34
+ | `core` | interfaces by name (cached), confirm, the probe's describe (labels, the transport list), taking the lock (`take`), the pin plan, the `Interface` base |
35
+ | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`, `oep.target.riscv-dm`, finding the reset line, attach through GPIO |
36
+ | `console` | `oep.target.console` (position streams) and `ConsoleIO`, read as bytes |
37
+ | `fixture` | `oep.fixture.gpio` / `uart` (revision 1) |
38
+ | `config` | `oep.probe.config` (slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) |
39
+ | `capture` | `oep.fixture.capture` (revision 1, oep-spec oep-if-capture). Every segment read goes to the `Host.on_capture` callbacks as a `CaptureRecord` (the hook for run recorders; no wireskein dependency) |
40
+ | `esp32_targets` | the custom interfaces `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target` (the ESP32 I2C / SPI targets of oep-probe-arduino) |
41
+ | `decode` | decoding capture channels (I2C) |
42
+ | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec) |
43
+ | `arm` | `oep.wire.swd`, `oep.target.arm-adi`, MEM-AP, halting and calling functions on a Cortex-M |
44
+ | `ch32_flash` | writing a CH32 (a RAM loader, page by page) |
45
+ | `rp2350` | flash and reboot through the RP2350 boot ROM |
46
+ | `uiapduino` | into and out of the UIAPduino bootloader |
47
+ | `catalog` / `names` / `interfaces` / `dump` | the capability list and describe shapes, display |
48
+ | `fake` / `endpoint` / `fake_serial` / `fake_serve` | the fake probe (below) |
49
+ | `target` | one place to import the main ones from |
50
+
51
+ ## Example
52
+
53
+ ```python
54
+ from oep_client import core, link, riscv, ch32_flash
55
+
56
+ hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelined
57
+ # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
58
+ core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
59
+ wire = riscv.Wire(hst, "oep.wire.rvswd")
60
+ conn, _ = wire.attach(halt=True)
61
+ dm = riscv.RiscvDm(hst, conn)
62
+ dm.reset_halt()
63
+ result = ch32_flash.program(hst, dm, open("sketch.bin", "rb").read(), ch32_flash.PROFILES["x035"])
64
+ dm.reset(confirm=True)
65
+ wire.detach(conn)
66
+ hst.end()
67
+ ```
68
+
69
+ ## The `oep` command
70
+
71
+ ```sh
72
+ oep dump --port <probe> # what the probe offers (--fake p4-x035: no hardware)
73
+ oep config show <probe> # the settings and the live slot / bind state
74
+ oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
75
+ oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
76
+ oep config save <probe> # kept over a restart (also: remove, erase)
77
+ ```
78
+
79
+ `<probe>` is a serial port, `tcp://HOST:PORT` or `usb[:VID:PID[:SERIAL]]`. A change takes the lock (owner "oep config") and
80
+ ends the session after it; it takes effect at once and, after `save`, stays over a restart.
81
+
82
+ A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`, `oep_probe_checks.py`).
83
+
84
+ ## The fake probe (a working spec)
85
+
86
+ `endpoint.Endpoint` is a fake probe that answers as oep-spec says; ch32rv, this client and the probe firmware are checked
87
+ against it (when the spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
88
+ `p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats), `fake_serial` the byte side of a serial
89
+ port (COBS candidates, raw bytes and binds, held during a session and resumed after it).
90
+
91
+ Other programs' tests run `fake_serve` as a child process:
92
+
93
+ ```sh
94
+ python -m oep_client.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
95
+ --console 'uptime %d\r\n' --every 100
96
+ # first line: PTY /dev/pts/N (PORT n with --tcp 0); it ends when stdin closes
97
+ ```
98
+
99
+ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--framing cobs` (a serial port) or
100
+ `--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
101
+ so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
102
+ broken once). `--uart-plan` / `--uart-rx` give the first fixture UART a plan and RX bytes, `--run-hook` a host's own model of
103
+ riscv-dm run. The rest: `--help`.
@@ -4,9 +4,9 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oep-client-python"
7
- version = "0.0.1"
7
+ version = "0.0.3"
8
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"
9
+ readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.10"
12
12
  authors = [
@@ -25,7 +25,7 @@ usb-async = ["libusb1>=3"] # queued asynchronous IN transfers: streaming near
25
25
  hid = ["hidapi>=0.14"] # the vendor HID way in through the OS HID driver (Windows without WinUSB, Linux hidraw)
26
26
 
27
27
  [project.scripts]
28
- oep = "oep_client.v1.__main__:main"
28
+ oep = "oep_client.__main__:main"
29
29
 
30
30
  [project.urls]
31
31
  Homepage = "https://github.com/Open-Embedded-Probe/oep-client-python"
@@ -35,7 +35,7 @@ Repository = "https://github.com/Open-Embedded-Probe/oep-client-python"
35
35
  packages = ["src/oep_client"]
36
36
 
37
37
  [tool.hatch.build.targets.sdist]
38
- only-include = ["src/oep_client", "tests", "README.ja.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
38
+ only-include = ["src/oep_client", "tests", "README.md", "README.ja.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
39
39
 
40
40
  [dependency-groups]
41
41
  dev = ["pytest>=8"]
@@ -0,0 +1,5 @@
1
+ """Open Embedded Probe (OEP) host client. `oep_client` speaks the v1 protocol (oep-spec docs/oep-core.ja.md)."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.0.3"
@@ -0,0 +1,217 @@
1
+ """The oep command: what a probe offers (dump) and its settings (config).
2
+
3
+ oep dump --port /run/board-identify/by-id/<probe> oep dump --fake p4-x035 --prefix oep.target --json
4
+ oep config show <probe>
5
+ oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
6
+ oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
7
+ oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
8
+
9
+ <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
10
+ ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import sys
17
+
18
+ import json
19
+ import struct
20
+
21
+ from . import catalog, config, core, dump, fake, host, link
22
+
23
+
24
+ def main(argv=None) -> int:
25
+ parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
26
+ sub = parser.add_subparsers(dest="command", required=True)
27
+ _config_parser(sub)
28
+ d = sub.add_parser("dump", help="list and describe every interface a probe offers")
29
+ src = d.add_mutually_exclusive_group(required=True)
30
+ src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
31
+ src.add_argument("--port", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID] (lock-free reads only)")
32
+ d.add_argument("--prefix", default="", help="only names under this namespace (label boundaries)")
33
+ d.add_argument("--exact", action="store_true", help="the prefix is a whole name")
34
+ d.add_argument("--json", action="store_true", help="machine-readable output")
35
+ args = parser.parse_args(argv)
36
+ if args.command == "config":
37
+ return _config(args)
38
+
39
+ if args.fake:
40
+ call = fake.PROFILES[args.fake]().call
41
+ else:
42
+ hst = link.open_host(args.port)
43
+ call = lambda fn, op, payload: hst.request(fn, op, payload, locked=False).payload # noqa: E731
44
+ caps = dump.collect(call, args.prefix, args.exact)
45
+ sys.stdout.write(dump.to_json(caps) + "\n" if args.json else dump.to_text(caps))
46
+ return 0
47
+
48
+
49
+ # ---- oep config ----------------------------------------------------------------------------------------------------
50
+
51
+ def _config_parser(sub) -> None:
52
+ c = sub.add_parser("config", help="the probe's settings (oep.probe.config): slots, binds, save")
53
+ cs = c.add_subparsers(dest="action", required=True)
54
+ show = cs.add_parser("show", help="the settings and the live slot / bind state")
55
+ show.add_argument("probe")
56
+ show.add_argument("--json", action="store_true")
57
+ slot = cs.add_parser("slot", help="register a slot (a place a target is wired to)")
58
+ slot.add_argument("probe")
59
+ slot.add_argument("--slot", type=int, default=0, help="the slot number (default 0)")
60
+ slot.add_argument("--name", required=True, help="1-32 of a-z 0-9 - _ (the oep://<probe>/<name> address)")
61
+ slot.add_argument("--wire", default="rvswd", help="rvswd or swio (or an fn)")
62
+ slot.add_argument("--pins", help="swdio,swclk (one pin on swio); default: the wire's only pin set")
63
+ slot.add_argument("--attach", choices=sorted(config.ATTACH), default="host")
64
+ slot.add_argument("--retry", type=int, default=0, help="at-boot: try again every N s while absent (0: never)")
65
+ slot.add_argument("--mechanism", choices=sorted(config.MECHANISM), default="dmseq")
66
+ slot.add_argument("--lock", help="MASK:VALUE (hex u32) the target_id (WCH DMI 0x7F) must match, e.g. ffffff0f:035e0600")
67
+ slot.add_argument("--save", action="store_true", help="save after the change")
68
+ bind = cs.add_parser("bind", help="what a serial port carries")
69
+ bind.add_argument("probe")
70
+ bind.add_argument("--port", type=int, required=True, help="the serial port (its transport index, see show)")
71
+ bind.add_argument("--mode", choices=sorted(config.MODE), default="last-reset")
72
+ bind.add_argument("--stream", action="append", required=True, help="slot:NAME, slot:N or uart:FN (repeatable)")
73
+ bind.add_argument("--select", type=int, default=0, help="manual: the stream it carries (index in --stream)")
74
+ bind.add_argument("--save", action="store_true")
75
+ rm = cs.add_parser("remove", help="remove one item: slot N, bind PORT, plan FN, label CH, idle CH")
76
+ rm.add_argument("probe")
77
+ rm.add_argument("kind", choices=["slot", "bind", "plan", "label", "idle"])
78
+ rm.add_argument("key", type=int)
79
+ rm.add_argument("--save", action="store_true")
80
+ for name in ("save", "erase"):
81
+ cs.add_parser(name, help=f"{name} the stored settings").add_argument("probe")
82
+
83
+
84
+ def _pins(hst, fn: int, text: str | None, wire: str) -> tuple[int, int]:
85
+ if text:
86
+ parts = [int(x, 0) for x in text.split(",")]
87
+ return (parts[0], parts[1] if len(parts) > 1 else 0xFFFF)
88
+ groups = []
89
+ for tag, v in core.describe(hst, fn):
90
+ if tag & 0x7F == catalog.CHANNEL_GROUP:
91
+ roles = {v[1 + 3 * i]: struct.unpack_from("<H", v, 2 + 3 * i)[0] for i in range((len(v) - 1) // 3)}
92
+ groups.append((roles.get(1, 0xFFFF), roles.get(2, 0xFFFF)))
93
+ if len(groups) != 1:
94
+ raise SystemExit(f"{wire}: {len(groups)} pin sets on this probe - name one with --pins")
95
+ return groups[0]
96
+
97
+
98
+ def _wire_fn(hst, wire: str) -> int:
99
+ return int(wire) if wire.isdigit() else core.find(hst, wire if wire.startswith("oep.") else f"oep.wire.{wire}")
100
+
101
+
102
+ def _stream(spec: str, slots: dict[str, int]) -> tuple[str, int]:
103
+ kind, _, key = spec.partition(":")
104
+ if kind not in config.STREAM or not key:
105
+ raise SystemExit(f"--stream {spec}: want slot:NAME, slot:N or uart:FN")
106
+ if kind == "slot" and not key.isdigit():
107
+ if key not in slots:
108
+ raise SystemExit(f"--stream {spec}: no slot named {key} (see oep config show)")
109
+ return kind, slots[key]
110
+ return kind, int(key)
111
+
112
+
113
+ def _change(hst, cfg, items, save: bool) -> None:
114
+ core.take(hst, 3000, owner="oep config")
115
+ try:
116
+ h = cfg.set(items)
117
+ print(f"set: hash 0x{h:08x}")
118
+ if save:
119
+ print(f"saved: hash 0x{cfg.save():08x}")
120
+ finally:
121
+ hst.end()
122
+
123
+
124
+ def _config(args) -> int:
125
+ hst = link.open_host(args.probe)
126
+ try:
127
+ cfg = config.ProbeConfig(hst)
128
+ if args.action == "show":
129
+ return _show(hst, cfg, args.json)
130
+ if args.action == "slot":
131
+ fn = _wire_fn(hst, args.wire)
132
+ lock = None
133
+ if args.lock:
134
+ mask, _, value = args.lock.partition(":")
135
+ lock = (1, struct.pack("<I", int(mask, 16)), struct.pack("<I", int(value, 16)))
136
+ it = config.Slot(args.slot, fn, _pins(hst, fn, args.pins, args.wire), args.name, args.attach, args.retry,
137
+ args.mechanism, lock)
138
+ _change(hst, cfg, [it], args.save)
139
+ elif args.action == "bind":
140
+ slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
141
+ it = config.Bind(args.port, args.mode, [_stream(s, slots) for s in args.stream], args.select)
142
+ _change(hst, cfg, [it], args.save)
143
+ elif args.action == "remove":
144
+ _change(hst, cfg, [config.remove(args.kind, args.key)], args.save)
145
+ else:
146
+ core.take(hst, 3000, owner="oep config")
147
+ try:
148
+ if args.action == "save":
149
+ print(f"saved: hash 0x{cfg.save():08x}")
150
+ else:
151
+ cfg.erase()
152
+ print("erased (the settings now in effect stay until the probe restarts)")
153
+ finally:
154
+ hst.end()
155
+ return 0
156
+ except host.Rejected as e:
157
+ print(f"refused: {e}", file=sys.stderr)
158
+ return 2
159
+ finally:
160
+ hst.link.close()
161
+
162
+
163
+ def _show(hst, cfg, as_json: bool) -> int:
164
+ h, _ = cfg.get()
165
+ items = cfg.items()
166
+ st = cfg.state()
167
+ kinds = {i: _name(k) for i, k, _ in core.transports(hst)}
168
+ if as_json:
169
+ def plain(o):
170
+ return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
171
+ else list(o)
172
+ out = {"hash": h, "items": [dict(type=type(i).__name__, **plain(i)) if hasattr(i, "__dict__") else plain(i)
173
+ for i in items],
174
+ "state": {**{k: v for k, v in vars(st).items() if k not in ("slots", "binds")},
175
+ "slots": [plain(s) for s in st.slots], "binds": [plain(b) for b in st.binds]},
176
+ "transports": kinds}
177
+ print(json.dumps(out, indent=2, default=lambda o: o.hex() if isinstance(o, bytes) else str(o)))
178
+ return 0
179
+ print(f"storage: {st.storage} ({st.storage_bytes} bytes), saved hash 0x{st.saved_hash:08x}; now 0x{h:08x}")
180
+ print("transports: " + ", ".join(f"{i} {k}" for i, k in kinds.items()))
181
+ by_slot = {s.slot: s for s in st.slots}
182
+ print(f"slots (up to {st.slots_max}):")
183
+ for it in items:
184
+ if isinstance(it, config.Slot):
185
+ s = by_slot.get(it.slot)
186
+ pins = f"{it.pins[0]}" if it.pins[1] == 0xFFFF else f"{it.pins[0]},{it.pins[1]}"
187
+ retry = f" retry {it.retry_s} s" if it.attach == "at-boot" else ""
188
+ lock = (f" lock {int.from_bytes(it.lock[1], 'little'):08x}:{int.from_bytes(it.lock[2], 'little'):08x}"
189
+ if it.lock else "")
190
+ live = ""
191
+ if s:
192
+ tried = "never tried" if s.last_try_ms is None else f"tried {s.last_try_ms} ms ago"
193
+ tid = f" target_id {s.target_id[::-1].hex()}" if s.target_id else ""
194
+ live = f" -> {s.state}" + (f" (connection {s.connection})" if s.connection else f" ({tried})") + tid
195
+ print(f" {it.slot} {it.name}: fn {it.wire_fn} pins {pins} {it.attach}{retry} {it.mechanism}{lock}{live}")
196
+ by_port = {b.port: b for b in st.binds}
197
+ print(f"binds (modes: {', '.join(st.bind_modes) or '-'}):")
198
+ names = {it.slot: it.name for it in items if isinstance(it, config.Slot)}
199
+ for it in items:
200
+ if isinstance(it, config.Bind):
201
+ b = by_port.get(it.port)
202
+ streams = ", ".join(f"slot:{names.get(i, i)}" if k == "slot" else f"{k}:{i}" for k, i in it.streams)
203
+ sel = f" selected {it.selected}" if it.mode == "manual" else ""
204
+ live = f" -> {b.flow}" + (f", carrying {b.selected}" if b and b.selected is not None else "") if b else ""
205
+ print(f" port {it.port} ({kinds.get(it.port, '?')}): {it.mode} [{streams}]{sel}{live}")
206
+ for it in items:
207
+ if not isinstance(it, (config.Slot, config.Bind)):
208
+ print(f" {it}")
209
+ return 0
210
+
211
+
212
+ def _name(kind: int) -> str:
213
+ return {v: k for k, v in core.TRANSPORT_KIND.items()}.get(kind, str(kind))
214
+
215
+
216
+ if __name__ == "__main__":
217
+ sys.exit(main())