oep-client-python 0.0.2__tar.gz → 0.0.4__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 (53) hide show
  1. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/CHANGELOG.md +12 -0
  2. oep_client_python-0.0.4/PKG-INFO +125 -0
  3. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/README.ja.md +15 -1
  4. oep_client_python-0.0.4/README.md +103 -0
  5. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/pyproject.toml +3 -3
  6. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/__init__.py +1 -1
  7. oep_client_python-0.0.4/src/oep_client/__main__.py +282 -0
  8. oep_client_python-0.0.4/src/oep_client/config.py +228 -0
  9. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/endpoint.py +7 -3
  10. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/fake_serial.py +4 -2
  11. oep_client_python-0.0.4/tests/test_config.py +99 -0
  12. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_fake_spec.py +13 -0
  13. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_link_serial.py +7 -2
  14. oep_client_python-0.0.2/PKG-INFO +0 -106
  15. oep_client_python-0.0.2/src/oep_client/__main__.py +0 -40
  16. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/.gitignore +0 -0
  17. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/LICENSE +0 -0
  18. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/arm.py +0 -0
  19. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/capture.py +0 -0
  20. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/catalog.py +0 -0
  21. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/ch32_flash.py +0 -0
  22. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/cobs.py +0 -0
  23. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/console.py +0 -0
  24. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/core.py +0 -0
  25. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/decode.py +0 -0
  26. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/dump.py +0 -0
  27. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/esp32_targets.py +0 -0
  28. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/fake.py +0 -0
  29. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/fake_serve.py +0 -0
  30. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/fixture.py +0 -0
  31. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/frames.py +0 -0
  32. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/hid_stream.py +0 -0
  33. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/host.py +0 -0
  34. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/interfaces.py +0 -0
  35. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/link.py +0 -0
  36. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/message.py +0 -0
  37. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/names.py +0 -0
  38. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/registry.py +0 -0
  39. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/riscv.py +0 -0
  40. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/rp2350.py +0 -0
  41. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/target.py +0 -0
  42. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/uiapduino.py +0 -0
  43. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/usb_stream.py +0 -0
  44. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_capabilities.py +0 -0
  45. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_capture.py +0 -0
  46. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_cobs.py +0 -0
  47. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_esp32_targets.py +0 -0
  48. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_flash_console.py +0 -0
  49. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_hid_stream.py +0 -0
  50. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_interfaces.py +0 -0
  51. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_link_host.py +0 -0
  52. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_session.py +0 -0
  53. {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_target_parts.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.4
6
+ - (EN) Fake serial port: a candidate that is only its 0x00 is not raw after the 200 ms gap (the probe firmware had the same bug: a stray 0x00 reached the target's console).
7
+ - (JA) 偽のシリアルの口: 0x00 だけの候補は、200 ms の後も生のバイトにしない(probe の firmware にも同じバグがあり、target のコンソールに 0x00 が届いていた)。
8
+ - (EN) `oep config slot` takes the probe's only RISC-V wire when `--wire` is left out (a SWIO-only probe needed `--wire swio`).
9
+ - (JA) `oep config slot` は `--wire` を省くと、probe の唯一の RISC-V の線を使う(SWIO だけの probe で `--wire swio` が要っていた)。
10
+ - (EN) `oep config plan <probe> <fn | name#k> ROLE=CH ...` (role names from the registry: rx, tx, line...), `oep config label` and `oep config idle`.
11
+ - (JA) `oep config plan <probe> <fn | 名前#k> ROLE=CH ...`(role の名前は registry から: rx、tx、line など)、`oep config label`、`oep config idle`。
12
+
13
+ ## 0.0.3
14
+ - (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.
15
+ - (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 のページも)。
16
+
5
17
  ## 0.0.2
6
18
  - (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.
7
19
  - (JA) 破壊的変更: モジュールを `oep_client.v1.*` から `oep_client.*` に移した(`from oep_client import link`、`python -m oep_client`、`python -m oep_client.fake_serve`)。プロトコルの revision は registry と confirm が持つ。
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.5
2
+ Name: oep-client-python
3
+ Version: 0.0.4
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,5 +1,7 @@
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
6
  `generated/oep-v1/oep_v1_registry.py` をそのまま写した `oep_client.registry` から取る。破壊的変更を前提とする
5
7
  実験段階で、互換 API は約束しない。OEP を初めて読む人は oep-spec の `docs/review-guide.ja.md`(どこに何が書いてあるか)から。
@@ -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) |
@@ -59,7 +62,18 @@ wire.detach(conn)
59
62
  hst.end()
60
63
  ```
61
64
 
62
- 能力の一覧は `uv run python -m oep_client 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)
@@ -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.2"
7
+ version = "0.0.4"
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 = [
@@ -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"]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.2"
5
+ __version__ = "0.0.4"
@@ -0,0 +1,282 @@
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 plan <probe> oep.fixture.uart#2 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
8
+ oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
9
+
10
+ <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
11
+ ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import sys
18
+
19
+ import json
20
+ import struct
21
+
22
+ from . import catalog, config, core, dump, fake, host, link
23
+
24
+
25
+ def main(argv=None) -> int:
26
+ parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
27
+ sub = parser.add_subparsers(dest="command", required=True)
28
+ _config_parser(sub)
29
+ d = sub.add_parser("dump", help="list and describe every interface a probe offers")
30
+ src = d.add_mutually_exclusive_group(required=True)
31
+ src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
32
+ src.add_argument("--port", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID] (lock-free reads only)")
33
+ d.add_argument("--prefix", default="", help="only names under this namespace (label boundaries)")
34
+ d.add_argument("--exact", action="store_true", help="the prefix is a whole name")
35
+ d.add_argument("--json", action="store_true", help="machine-readable output")
36
+ args = parser.parse_args(argv)
37
+ if args.command == "config":
38
+ return _config(args)
39
+
40
+ if args.fake:
41
+ call = fake.PROFILES[args.fake]().call
42
+ else:
43
+ hst = link.open_host(args.port)
44
+ call = lambda fn, op, payload: hst.request(fn, op, payload, locked=False).payload # noqa: E731
45
+ caps = dump.collect(call, args.prefix, args.exact)
46
+ sys.stdout.write(dump.to_json(caps) + "\n" if args.json else dump.to_text(caps))
47
+ return 0
48
+
49
+
50
+ # ---- oep config ----------------------------------------------------------------------------------------------------
51
+
52
+ def _config_parser(sub) -> None:
53
+ c = sub.add_parser("config", help="the probe's settings (oep.probe.config): slots, binds, save")
54
+ cs = c.add_subparsers(dest="action", required=True)
55
+ show = cs.add_parser("show", help="the settings and the live slot / bind state")
56
+ show.add_argument("probe")
57
+ show.add_argument("--json", action="store_true")
58
+ slot = cs.add_parser("slot", help="register a slot (a place a target is wired to)")
59
+ slot.add_argument("probe")
60
+ slot.add_argument("--slot", type=int, default=0, help="the slot number (default 0)")
61
+ slot.add_argument("--name", required=True, help="1-32 of a-z 0-9 - _ (the oep://<probe>/<name> address)")
62
+ slot.add_argument("--wire", help="rvswd or swio (or an fn); default: the probe's only wire")
63
+ slot.add_argument("--pins", help="swdio,swclk (one pin on swio); default: the wire's only pin set")
64
+ slot.add_argument("--attach", choices=sorted(config.ATTACH), default="host")
65
+ slot.add_argument("--retry", type=int, default=0, help="at-boot: try again every N s while absent (0: never)")
66
+ slot.add_argument("--mechanism", choices=sorted(config.MECHANISM), default="dmseq")
67
+ slot.add_argument("--lock", help="MASK:VALUE (hex u32) the target_id (WCH DMI 0x7F) must match, e.g. ffffff0f:035e0600")
68
+ slot.add_argument("--save", action="store_true", help="save after the change")
69
+ bind = cs.add_parser("bind", help="what a serial port carries")
70
+ bind.add_argument("probe")
71
+ bind.add_argument("--port", type=int, required=True, help="the serial port (its transport index, see show)")
72
+ bind.add_argument("--mode", choices=sorted(config.MODE), default="last-reset")
73
+ bind.add_argument("--stream", action="append", required=True, help="slot:NAME, slot:N or uart:FN (repeatable)")
74
+ bind.add_argument("--select", type=int, default=0, help="manual: the stream it carries (index in --stream)")
75
+ bind.add_argument("--save", action="store_true")
76
+ plan = cs.add_parser("plan", help="the pins an interface keeps (its whole plan, kept as a setting)")
77
+ plan.add_argument("probe")
78
+ plan.add_argument("fn", help="an fn, or an interface name with #k for its k-th instance (1 = the first)")
79
+ plan.add_argument("roles", nargs="+", help="ROLE=CHANNEL, ROLE a number or the interface's role name (rx, tx, line...)")
80
+ plan.add_argument("--save", action="store_true")
81
+ label = cs.add_parser("label", help="name a channel (shown in oep.core's describe)")
82
+ label.add_argument("probe")
83
+ label.add_argument("channel", type=int)
84
+ label.add_argument("text")
85
+ label.add_argument("--save", action="store_true")
86
+ idle = cs.add_parser("idle", help="the state of a free pin")
87
+ idle.add_argument("probe")
88
+ idle.add_argument("channel", type=int)
89
+ idle.add_argument("mode", choices=sorted(config.IDLE))
90
+ idle.add_argument("--save", action="store_true")
91
+ rm = cs.add_parser("remove", help="remove one item: slot N, bind PORT, plan FN, label CH, idle CH")
92
+ rm.add_argument("probe")
93
+ rm.add_argument("kind", choices=["slot", "bind", "plan", "label", "idle"])
94
+ rm.add_argument("key", type=int)
95
+ rm.add_argument("--save", action="store_true")
96
+ for name in ("save", "erase"):
97
+ cs.add_parser(name, help=f"{name} the stored settings").add_argument("probe")
98
+
99
+
100
+ def _pins(hst, fn: int, text: str | None, wire: str) -> tuple[int, int]:
101
+ if text:
102
+ parts = [int(x, 0) for x in text.split(",")]
103
+ return (parts[0], parts[1] if len(parts) > 1 else 0xFFFF)
104
+ groups = []
105
+ for tag, v in core.describe(hst, fn):
106
+ if tag & 0x7F == catalog.CHANNEL_GROUP:
107
+ roles = {v[1 + 3 * i]: struct.unpack_from("<H", v, 2 + 3 * i)[0] for i in range((len(v) - 1) // 3)}
108
+ groups.append((roles.get(1, 0xFFFF), roles.get(2, 0xFFFF)))
109
+ if len(groups) != 1:
110
+ raise SystemExit(f"{wire}: {len(groups)} pin sets on this probe - name one with --pins")
111
+ return groups[0]
112
+
113
+
114
+ def _plan_fn(hst, spec: str) -> tuple[int, dict[str, int]]:
115
+ """fn and its role names (from the registry) for `spec`: an fn number, or name[#k] (k-th instance, 1-based)."""
116
+ from . import registry as reg
117
+ if spec.isdigit():
118
+ fn = int(spec)
119
+ name = next((e.name for e in core.list_entries(hst) if e.fn == fn), "")
120
+ else:
121
+ name, _, k = spec.partition("#")
122
+ fns = core.find_all(hst, name)
123
+ if not fns:
124
+ raise SystemExit(f"the probe offers no {name}")
125
+ index = int(k) - 1 if k else 0
126
+ if not 0 <= index < len(fns):
127
+ raise SystemExit(f"{name}: {len(fns)} instance(s), fns {fns}")
128
+ fn = fns[index]
129
+ iface = reg.INTERFACES.get(name)
130
+ roles = dict(iface.enum.get("role", {})) if iface else {}
131
+ return fn, roles
132
+
133
+
134
+ def _wire_fn(hst, wire: str | None) -> int:
135
+ if wire is None:
136
+ wires = [e for e in core.list_entries(hst, "oep.wire") if e.name in ("oep.wire.rvswd", "oep.wire.swio")]
137
+ if len(wires) != 1:
138
+ raise SystemExit("this probe has " + (", ".join(f"{e.name} (fn {e.fn})" for e in wires) or "no RISC-V wire")
139
+ + ": name one with --wire")
140
+ return wires[0].fn
141
+ name = wire if wire.startswith("oep.") else f"oep.wire.{wire}"
142
+ if wire.isdigit():
143
+ return int(wire)
144
+ try:
145
+ return core.find(hst, name)
146
+ except LookupError:
147
+ raise SystemExit(f"this probe does not offer {name}") from None
148
+
149
+
150
+ def _stream(spec: str, slots: dict[str, int]) -> tuple[str, int]:
151
+ kind, _, key = spec.partition(":")
152
+ if kind not in config.STREAM or not key:
153
+ raise SystemExit(f"--stream {spec}: want slot:NAME, slot:N or uart:FN")
154
+ if kind == "slot" and not key.isdigit():
155
+ if key not in slots:
156
+ raise SystemExit(f"--stream {spec}: no slot named {key} (see oep config show)")
157
+ return kind, slots[key]
158
+ return kind, int(key)
159
+
160
+
161
+ def _change(hst, cfg, items, save: bool) -> None:
162
+ core.take(hst, 3000, owner="oep config")
163
+ try:
164
+ h = cfg.set(items)
165
+ print(f"set: hash 0x{h:08x}")
166
+ if save:
167
+ print(f"saved: hash 0x{cfg.save():08x}")
168
+ finally:
169
+ hst.end()
170
+
171
+
172
+ def _config(args) -> int:
173
+ hst = link.open_host(args.probe)
174
+ try:
175
+ cfg = config.ProbeConfig(hst)
176
+ if args.action == "show":
177
+ return _show(hst, cfg, args.json)
178
+ if args.action == "slot":
179
+ fn = _wire_fn(hst, args.wire)
180
+ args.wire = args.wire or str(fn)
181
+ lock = None
182
+ if args.lock:
183
+ mask, _, value = args.lock.partition(":")
184
+ lock = (1, struct.pack("<I", int(mask, 16)), struct.pack("<I", int(value, 16)))
185
+ it = config.Slot(args.slot, fn, _pins(hst, fn, args.pins, args.wire), args.name, args.attach, args.retry,
186
+ args.mechanism, lock)
187
+ _change(hst, cfg, [it], args.save)
188
+ elif args.action == "bind":
189
+ slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
190
+ it = config.Bind(args.port, args.mode, [_stream(s, slots) for s in args.stream], args.select)
191
+ _change(hst, cfg, [it], args.save)
192
+ elif args.action == "plan":
193
+ fn, roles = _plan_fn(hst, args.fn)
194
+ items = []
195
+ for spec in args.roles:
196
+ role, _, ch = spec.partition("=")
197
+ if not ch:
198
+ raise SystemExit(f"{spec}: want ROLE=CHANNEL")
199
+ number = int(role) if role.isdigit() else roles.get(role.lower())
200
+ if number is None:
201
+ raise SystemExit(f"{role}: not a role of fn {fn} (roles: {', '.join(sorted(roles)) or 'numbers only'})")
202
+ items.append(config.Plan(fn, number, int(ch, 0)))
203
+ _change(hst, cfg, items, args.save)
204
+ elif args.action == "label":
205
+ _change(hst, cfg, [config.Label(args.channel, args.text)], args.save)
206
+ elif args.action == "idle":
207
+ _change(hst, cfg, [config.Idle(args.channel, args.mode)], args.save)
208
+ elif args.action == "remove":
209
+ _change(hst, cfg, [config.remove(args.kind, args.key)], args.save)
210
+ else:
211
+ core.take(hst, 3000, owner="oep config")
212
+ try:
213
+ if args.action == "save":
214
+ print(f"saved: hash 0x{cfg.save():08x}")
215
+ else:
216
+ cfg.erase()
217
+ print("erased (the settings now in effect stay until the probe restarts)")
218
+ finally:
219
+ hst.end()
220
+ return 0
221
+ except host.Rejected as e:
222
+ print(f"refused: {e}", file=sys.stderr)
223
+ return 2
224
+ finally:
225
+ hst.link.close()
226
+
227
+
228
+ def _show(hst, cfg, as_json: bool) -> int:
229
+ h, _ = cfg.get()
230
+ items = cfg.items()
231
+ st = cfg.state()
232
+ kinds = {i: _name(k) for i, k, _ in core.transports(hst)}
233
+ if as_json:
234
+ def plain(o):
235
+ return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
236
+ else list(o)
237
+ out = {"hash": h, "items": [dict(type=type(i).__name__, **plain(i)) if hasattr(i, "__dict__") else plain(i)
238
+ for i in items],
239
+ "state": {**{k: v for k, v in vars(st).items() if k not in ("slots", "binds")},
240
+ "slots": [plain(s) for s in st.slots], "binds": [plain(b) for b in st.binds]},
241
+ "transports": kinds}
242
+ print(json.dumps(out, indent=2, default=lambda o: o.hex() if isinstance(o, bytes) else str(o)))
243
+ return 0
244
+ print(f"storage: {st.storage} ({st.storage_bytes} bytes), saved hash 0x{st.saved_hash:08x}; now 0x{h:08x}")
245
+ print("transports: " + ", ".join(f"{i} {k}" for i, k in kinds.items()))
246
+ by_slot = {s.slot: s for s in st.slots}
247
+ print(f"slots (up to {st.slots_max}):")
248
+ for it in items:
249
+ if isinstance(it, config.Slot):
250
+ s = by_slot.get(it.slot)
251
+ pins = f"{it.pins[0]}" if it.pins[1] == 0xFFFF else f"{it.pins[0]},{it.pins[1]}"
252
+ retry = f" retry {it.retry_s} s" if it.attach == "at-boot" else ""
253
+ lock = (f" lock {int.from_bytes(it.lock[1], 'little'):08x}:{int.from_bytes(it.lock[2], 'little'):08x}"
254
+ if it.lock else "")
255
+ live = ""
256
+ if s:
257
+ tried = "never tried" if s.last_try_ms is None else f"tried {s.last_try_ms} ms ago"
258
+ tid = f" target_id {s.target_id[::-1].hex()}" if s.target_id else ""
259
+ live = f" -> {s.state}" + (f" (connection {s.connection})" if s.connection else f" ({tried})") + tid
260
+ print(f" {it.slot} {it.name}: fn {it.wire_fn} pins {pins} {it.attach}{retry} {it.mechanism}{lock}{live}")
261
+ by_port = {b.port: b for b in st.binds}
262
+ print(f"binds (modes: {', '.join(st.bind_modes) or '-'}):")
263
+ names = {it.slot: it.name for it in items if isinstance(it, config.Slot)}
264
+ for it in items:
265
+ if isinstance(it, config.Bind):
266
+ b = by_port.get(it.port)
267
+ streams = ", ".join(f"slot:{names.get(i, i)}" if k == "slot" else f"{k}:{i}" for k, i in it.streams)
268
+ sel = f" selected {it.selected}" if it.mode == "manual" else ""
269
+ live = f" -> {b.flow}" + (f", carrying {b.selected}" if b and b.selected is not None else "") if b else ""
270
+ print(f" port {it.port} ({kinds.get(it.port, '?')}): {it.mode} [{streams}]{sel}{live}")
271
+ for it in items:
272
+ if not isinstance(it, (config.Slot, config.Bind)):
273
+ print(f" {it}")
274
+ return 0
275
+
276
+
277
+ def _name(kind: int) -> str:
278
+ return {v: k for k, v in core.TRANSPORT_KIND.items()}.get(kind, str(kind))
279
+
280
+
281
+ if __name__ == "__main__":
282
+ sys.exit(main())