oep-client-python 0.0.2__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 (53) hide show
  1. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/CHANGELOG.md +4 -0
  2. oep_client_python-0.0.3/PKG-INFO +125 -0
  3. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/README.ja.md +15 -1
  4. oep_client_python-0.0.3/README.md +103 -0
  5. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/pyproject.toml +3 -3
  6. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/__init__.py +1 -1
  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.2 → oep_client_python-0.0.3}/src/oep_client/endpoint.py +7 -3
  10. oep_client_python-0.0.3/tests/test_config.py +67 -0
  11. oep_client_python-0.0.2/PKG-INFO +0 -106
  12. oep_client_python-0.0.2/src/oep_client/__main__.py +0 -40
  13. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/.gitignore +0 -0
  14. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/LICENSE +0 -0
  15. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/arm.py +0 -0
  16. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/capture.py +0 -0
  17. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/catalog.py +0 -0
  18. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/ch32_flash.py +0 -0
  19. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/cobs.py +0 -0
  20. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/console.py +0 -0
  21. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/core.py +0 -0
  22. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/decode.py +0 -0
  23. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/dump.py +0 -0
  24. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/esp32_targets.py +0 -0
  25. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/fake.py +0 -0
  26. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/fake_serial.py +0 -0
  27. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/fake_serve.py +0 -0
  28. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/fixture.py +0 -0
  29. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/frames.py +0 -0
  30. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/hid_stream.py +0 -0
  31. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/host.py +0 -0
  32. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/interfaces.py +0 -0
  33. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/link.py +0 -0
  34. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/message.py +0 -0
  35. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/names.py +0 -0
  36. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/registry.py +0 -0
  37. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/riscv.py +0 -0
  38. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/rp2350.py +0 -0
  39. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/target.py +0 -0
  40. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/uiapduino.py +0 -0
  41. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/usb_stream.py +0 -0
  42. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_capabilities.py +0 -0
  43. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_capture.py +0 -0
  44. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_cobs.py +0 -0
  45. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_esp32_targets.py +0 -0
  46. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_fake_spec.py +0 -0
  47. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_flash_console.py +0 -0
  48. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_hid_stream.py +0 -0
  49. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_interfaces.py +0 -0
  50. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_link_host.py +0 -0
  51. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_link_serial.py +0 -0
  52. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_session.py +0 -0
  53. {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_target_parts.py +0 -0
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
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
+
5
9
  ## 0.0.2
6
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.
7
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 が持つ。
@@ -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,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.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 = [
@@ -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.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())
@@ -0,0 +1,228 @@
1
+ """oep.probe.config revision 1 (oep-spec docs/oep-if-probe-config.ja.md): the probe's settings - plan, labels, idle
2
+ pins, slots, binds - read and set as items, saved when the host says so, and the live slot / bind state.
3
+
4
+ cfg = config.ProbeConfig(hst)
5
+ cfg.set([config.Slot(0, wire_fn, (2, 54), "x035", attach="at-boot", retry_s=1, mechanism="dmseq"),
6
+ config.Bind(1, "last-reset", [("slot", 0)])])
7
+ cfg.save()
8
+ cfg.items(), cfg.state()
9
+
10
+ An item goes as its TLV; one with only its key removes the item of that key (`config.remove`). A set replaces the keys it
11
+ carries and keeps the others; the probe checks the whole and changes nothing on a refusal.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import struct
17
+ from dataclasses import dataclass, field
18
+
19
+ from . import catalog, core, message as m, registry as reg
20
+ from .core import Interface
21
+
22
+ _CFG = reg.PROBE_CONFIG
23
+ ITEM = _CFG.tlv["item"]
24
+ DESCRIBE = _CFG.tlv["describe"]
25
+ ATTACH = {k.replace("_", "-"): v for k, v in _CFG.enum["slot_attach"].items()}
26
+ MODE = {k.replace("_", "-"): v for k, v in _CFG.enum["bind_mode"].items()}
27
+ STREAM = {"slot": _CFG.enum["bind_stream"]["slot_console"], "uart": _CFG.enum["bind_stream"]["fixture_uart"]}
28
+ MECHANISM = {k: v for k, v in reg.TARGET_CONSOLE.enum["mechanism"].items()}
29
+ SLOT_STATE = {v: k.replace("_", "-") for k, v in _CFG.enum["slot_state"].items()}
30
+ BIND_FLOW = {v: k for k, v in _CFG.enum["bind_flow"].items()}
31
+ IDLE = {k.replace("_", "-"): v for k, v in _CFG.enum["idle_mode"].items()}
32
+ STORAGE_STATE = {v: k for k, v in _CFG.enum["storage_state"].items()}
33
+
34
+
35
+ def _name(table: dict[str, int], value: int) -> str:
36
+ return next((k for k, v in table.items() if v == value), str(value))
37
+
38
+
39
+ @dataclass
40
+ class Plan:
41
+ fn: int
42
+ role: int
43
+ channel: int
44
+ TAG = ITEM["plan"]
45
+
46
+ def value(self) -> bytes:
47
+ return struct.pack("<HBH", self.fn, self.role, self.channel)
48
+
49
+
50
+ @dataclass
51
+ class Label:
52
+ channel: int
53
+ text: str
54
+ TAG = ITEM["label"]
55
+
56
+ def value(self) -> bytes:
57
+ return struct.pack("<H", self.channel) + self.text.encode()
58
+
59
+
60
+ @dataclass
61
+ class Idle:
62
+ channel: int
63
+ mode: str = "pull-up" # hi-z, pull-up, pull-down
64
+ TAG = ITEM["idle"]
65
+
66
+ def value(self) -> bytes:
67
+ return struct.pack("<HB", self.channel, IDLE[self.mode])
68
+
69
+
70
+ @dataclass
71
+ class Slot:
72
+ """A place a target is wired to (probe.config §1.1). pins: (swdio, swclk), swclk 0xFFFF on one wire (swio).
73
+ lock: (scheme, mask, value) - the target_id a connection must show, e.g. (1, mask u32 LE, value u32 LE)."""
74
+ slot: int
75
+ wire_fn: int
76
+ pins: tuple[int, int]
77
+ name: str
78
+ attach: str = "host" # host, at-boot
79
+ retry_s: int = 0 # at-boot: try again every retry_s while the target is not there (0: never)
80
+ mechanism: str = "dmseq" # sdi, dmdata, dmseq
81
+ lock: tuple[int, bytes, bytes] | None = None
82
+ TAG = ITEM["slot"]
83
+
84
+ def value(self) -> bytes:
85
+ name = self.name.encode()
86
+ v = struct.pack("<BHHHBHBB", self.slot, self.wire_fn, *self.pins, ATTACH[self.attach],
87
+ self.retry_s if self.attach == "at-boot" else 0, MECHANISM[self.mechanism], len(name)) + name
88
+ if self.lock is None:
89
+ return v + b"\x00"
90
+ scheme, mask, value = self.lock
91
+ if len(mask) != len(value) or not mask:
92
+ raise ValueError("a lock's mask and value have the same length, at least 1 byte")
93
+ return v + bytes([scheme]) + mask + value
94
+
95
+
96
+ @dataclass
97
+ class Bind:
98
+ """What serial port `port` (the describe transport index) carries (probe.config §1.2). streams: ("slot", n) or
99
+ ("uart", fn); selected: manual's choice (an index into streams)."""
100
+ port: int
101
+ mode: str = "last-reset" # last-reset, manual, mixed
102
+ streams: list[tuple[str, int]] = field(default_factory=list)
103
+ selected: int = 0
104
+ TAG = ITEM["bind"]
105
+
106
+ def value(self) -> bytes:
107
+ v = struct.pack("<BBBB", self.port, MODE[self.mode], self.selected if self.mode == "manual" else 0,
108
+ len(self.streams))
109
+ return v + b"".join(struct.pack("<BH", STREAM[kind], i) for kind, i in self.streams)
110
+
111
+
112
+ def item(it) -> bytes:
113
+ return m.tlv(it.TAG, it.value())
114
+
115
+
116
+ def remove(kind: str, key: int) -> bytes:
117
+ """The item that removes the item of this key: kind plan (key fn), label / idle (channel), slot, bind (port)."""
118
+ tag = ITEM[kind]
119
+ return m.tlv(tag, bytes([key]) if kind in ("slot", "bind") else struct.pack("<H", key))
120
+
121
+
122
+ def decode(tag: int, v: bytes):
123
+ """One item as one of the classes above (an unknown tag: (tag, value))."""
124
+ if tag == ITEM["plan"] and len(v) == 5:
125
+ return Plan(*struct.unpack("<HBH", v))
126
+ if tag == ITEM["label"] and len(v) >= 2:
127
+ return Label(struct.unpack_from("<H", v)[0], v[2:].decode("utf-8", "replace"))
128
+ if tag == ITEM["idle"] and len(v) == 3:
129
+ return Idle(struct.unpack_from("<H", v)[0], _name(IDLE, v[2]))
130
+ if tag == ITEM["slot"] and len(v) >= 13:
131
+ n, wire_fn, swdio, swclk, attach, retry_s, mech, name_len = struct.unpack_from("<BHHHBHBB", v)
132
+ name = v[12:12 + name_len].decode("ascii", "replace")
133
+ rest = v[12 + name_len:]
134
+ lock = None
135
+ if rest and rest[0]:
136
+ half = (len(rest) - 1) // 2
137
+ lock = (rest[0], rest[1:1 + half], rest[1 + half:])
138
+ return Slot(n, wire_fn, (swdio, swclk), name, _name(ATTACH, attach), retry_s, _name(MECHANISM, mech), lock)
139
+ if tag == ITEM["bind"] and len(v) >= 4:
140
+ port, mode, selected, n = struct.unpack_from("<BBBB", v)
141
+ streams = [(_name(STREAM, v[4 + 3 * k]), struct.unpack_from("<H", v, 5 + 3 * k)[0]) for k in range(n)]
142
+ return Bind(port, _name(MODE, mode), streams, selected)
143
+ return (tag, v)
144
+
145
+
146
+ @dataclass
147
+ class SlotState:
148
+ slot: int
149
+ state: str # connected, absent, lock-mismatch, no-target-id
150
+ connection: int
151
+ last_try_ms: int | None # since the last automatic attach (None: never tried)
152
+ target_id: bytes | None
153
+
154
+
155
+ @dataclass
156
+ class BindState:
157
+ port: int
158
+ mode: str
159
+ selected: int | None # None in mixed
160
+ flow: str # idle, streaming, held
161
+
162
+
163
+ @dataclass
164
+ class State:
165
+ storage_bytes: int = 0
166
+ storage: str = "none" # none, applied, unreadable
167
+ saved_hash: int = 0
168
+ items: list[int] = field(default_factory=list)
169
+ slots_max: int = 0
170
+ bind_modes: list[str] = field(default_factory=list)
171
+ slots: list[SlotState] = field(default_factory=list)
172
+ binds: list[BindState] = field(default_factory=list)
173
+
174
+
175
+ class ProbeConfig(Interface):
176
+ NAME = "oep.probe.config"
177
+ REVISION = 1
178
+ GET, SET, SAVE, ERASE = (_CFG.op[k] for k in ("get", "set", "save", "erase"))
179
+
180
+ def get(self) -> tuple[int, list[tuple[int, bytes]]]:
181
+ """-> (hash, the items as (tag, value) in the canonical order), paged. No lock."""
182
+ out, h = [], 0
183
+ while True:
184
+ p = self._call(self.GET, struct.pack("<H", len(out)), locked=False).payload
185
+ more, h = struct.unpack_from("<BI", p)
186
+ page = catalog.split_tlv(p[5:])
187
+ out += page
188
+ if not more or not page:
189
+ return h, out
190
+
191
+ def items(self) -> list:
192
+ """The current settings, decoded (Plan, Label, Idle, Slot, Bind)."""
193
+ return [decode(t, v) for t, v in self.get()[1]]
194
+
195
+ def set(self, items: list) -> int:
196
+ """Items (objects of the classes above, or item TLV bytes such as remove()) -> the new hash. Needs the lock."""
197
+ body = b"".join(it if isinstance(it, (bytes, bytearray)) else item(it) for it in items)
198
+ return struct.unpack("<I", self._call(self.SET, body).payload[:4])[0]
199
+
200
+ def save(self) -> int:
201
+ return struct.unpack("<I", self._call(self.SAVE).payload[:4])[0]
202
+
203
+ def erase(self) -> None:
204
+ self._call(self.ERASE)
205
+
206
+ def state(self) -> State:
207
+ """storage, the items taken, slots_max, bind modes, and the live slot_state / bind_state (lock-free)."""
208
+ st = State()
209
+ for tag, v in core.describe(self.host, self.fn):
210
+ tag &= 0x7F
211
+ if tag == DESCRIBE["storage"] and len(v) >= 9:
212
+ st.storage_bytes, state, st.saved_hash = struct.unpack_from("<IBI", v)
213
+ st.storage = STORAGE_STATE.get(state, str(state))
214
+ elif tag == DESCRIBE["items"]:
215
+ st.items = list(v)
216
+ elif tag == DESCRIBE["slots_max"] and v:
217
+ st.slots_max = v[0]
218
+ elif tag == DESCRIBE["bind_modes"] and v:
219
+ st.bind_modes = [name for name, bit in MODE.items() if v[0] >> bit & 1]
220
+ elif tag == DESCRIBE["slot_state"] and len(v) >= 10:
221
+ n, state, conn, age, _scheme, tlen = struct.unpack_from("<BBHIBB", v)
222
+ st.slots.append(SlotState(n, SLOT_STATE.get(state, str(state)), conn,
223
+ None if age == 0xFFFFFFFF else age, bytes(v[10:10 + tlen]) or None))
224
+ elif tag == DESCRIBE["bind_state"] and len(v) >= 4:
225
+ port, mode, sel, flow = v[:4]
226
+ st.binds.append(BindState(port, _name(MODE, mode), None if sel == 0xFF else sel,
227
+ BIND_FLOW.get(flow, str(flow))))
228
+ return st
@@ -207,6 +207,7 @@ class SlotRuntime:
207
207
  last_try_ms: int | None = None
208
208
  evicted: bool = False # the seat rule closed its connection: no retry until a new cue
209
209
  mismatch_tid: int | None = None # what the last automatic attach saw when the lock did not match
210
+ no_tid: bool = False # ... or it saw no target_id to check a lock against
210
211
 
211
212
 
212
213
  @dataclass
@@ -1295,11 +1296,13 @@ class Endpoint:
1295
1296
  return
1296
1297
  tg.havereset = False
1297
1298
  c = self.conns[cid]
1298
- if self._lock_ok(s, c.tid) is True:
1299
+ ok = self._lock_ok(s, c.tid)
1300
+ rt.no_tid = ok is None
1301
+ if ok is True:
1299
1302
  c.users.add(("slot", n))
1300
1303
  rt.mismatch_tid = None
1301
1304
  else:
1302
- rt.mismatch_tid = c.tid # found, wrong chip: let go of it
1305
+ rt.mismatch_tid = c.tid # found, wrong chip (or no id): let go of it
1303
1306
  if not c.users:
1304
1307
  self._close_conn(cid, MARK["detach"])
1305
1308
 
@@ -1347,7 +1350,8 @@ class Endpoint:
1347
1350
  state = SLOT_STATE["connected"] if ok else (SLOT_STATE["no_target_id"] if ok is None
1348
1351
  else SLOT_STATE["lock_mismatch"])
1349
1352
  else:
1350
- state = SLOT_STATE["lock_mismatch"] if rt.mismatch_tid is not None else SLOT_STATE["absent"]
1353
+ state = (SLOT_STATE["no_target_id"] if rt.no_tid else
1354
+ SLOT_STATE["lock_mismatch"] if rt.mismatch_tid is not None else SLOT_STATE["absent"])
1351
1355
  age = NEVER if rt.last_try_ms is None else min(NEVER - 1, self.now() - rt.last_try_ms)
1352
1356
  raw = b"" if tid is None else struct.pack("<I", tid)
1353
1357
  return struct.pack("<BBHIBB", n, state, cid or 0, age, 1 if raw else 0, len(raw)) + raw
@@ -0,0 +1,67 @@
1
+ """oep.probe.config from the client (oep_client.config) and the `oep config` command, against the fake probe."""
2
+
3
+ import struct
4
+
5
+ from oep_client import __main__ as cli, config, endpoint, fake, host as h
6
+
7
+
8
+ class Clock:
9
+ t = 0
10
+
11
+ def __call__(self):
12
+ return self.t
13
+
14
+
15
+ def open_bench():
16
+ ep = endpoint.Endpoint(fake.p4_bench(), Clock())
17
+ hst = h.Host(lambda b: ep.handle(b, 1)) # over vendor bulk
18
+ hst.open(3000)
19
+ return ep, hst
20
+
21
+
22
+ def test_slots_and_binds_round_trip_and_show_their_state():
23
+ ep, hst = open_bench()
24
+ cfg = config.ProbeConfig(hst)
25
+ pair = ep.pairs[1][0]
26
+ ep.targets[(1, pair)].target_id = 0x035E0601
27
+ lock = (1, struct.pack("<I", 0xFFFFFF0F), struct.pack("<I", 0x035E0601 & 0xFFFFFF0F))
28
+ cfg.set([config.Slot(0, 1, pair, "x035", "at-boot", 1, "dmseq", lock),
29
+ config.Bind(3, "manual", [("slot", 0), ("uart", 5)], selected=0)])
30
+ items = cfg.items()
31
+ assert [type(i).__name__ for i in items] == ["Slot", "Bind"]
32
+ assert items[0].name == "x035" and items[0].lock == lock and items[1].streams == [("slot", 0), ("uart", 5)]
33
+ st = cfg.state()
34
+ assert st.slots_max == 4 and st.bind_modes == ["last-reset", "manual", "mixed"]
35
+ assert st.slots[0].state == "connected" and st.slots[0].target_id == struct.pack("<I", 0x035E0601)
36
+ assert st.binds[0].port == 3 and st.binds[0].flow == "streaming"
37
+ saved = cfg.save()
38
+ assert cfg.state().saved_hash == saved and cfg.state().storage == "applied"
39
+ cfg.set([config.remove("bind", 3)])
40
+ assert [type(i).__name__ for i in cfg.items()] == ["Slot"]
41
+
42
+
43
+ def test_a_refused_set_changes_nothing():
44
+ ep, hst = open_bench()
45
+ cfg = config.ProbeConfig(hst)
46
+ before = cfg.get()[0]
47
+ try:
48
+ cfg.set([config.Bind(1, "last-reset", [("slot", 0)])]) # port 1 is vendor bulk, and slot 0 is not there
49
+ except h.Rejected:
50
+ pass
51
+ assert cfg.get()[0] == before
52
+
53
+
54
+ def test_the_command(capsys, monkeypatch):
55
+ ep, hst = open_bench()
56
+
57
+ class FakeLink:
58
+ def close(self):
59
+ pass
60
+ hst.link = FakeLink()
61
+ hst.end() # the command takes the lock itself
62
+ monkeypatch.setattr(cli.link, "open_host", lambda target: hst)
63
+ assert cli.main(["config", "slot", "x", "--name", "x035", "--pins", "2,3", "--attach", "at-boot", "--retry", "1"]) == 0
64
+ assert cli.main(["config", "bind", "x", "--port", "0", "--mode", "last-reset", "--stream", "slot:x035"]) == 0
65
+ assert cli.main(["config", "show", "x"]) == 0
66
+ out = capsys.readouterr().out
67
+ assert "0 x035: fn 1 pins 2,3 at-boot retry 1 s dmseq" in out and "port 0 (usb_serial_jtag): last-reset [slot:x035]" in out
@@ -1,106 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: oep-client-python
3
- Version: 0.0.2
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.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` だけで使える(`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`)
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 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 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.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 はもう無い。
@@ -1,40 +0,0 @@
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 dump --port /run/board-identify/by-id/<probe>
4
- uv run python -m oep_client dump --fake p4-x035
5
- uv run python -m oep_client dump --fake esp32-v003 --prefix oep.fixture
6
- uv run python -m oep_client 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())