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.
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/CHANGELOG.md +4 -0
- oep_client_python-0.0.3/PKG-INFO +125 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/README.ja.md +15 -1
- oep_client_python-0.0.3/README.md +103 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/pyproject.toml +3 -3
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/__init__.py +1 -1
- oep_client_python-0.0.3/src/oep_client/__main__.py +217 -0
- oep_client_python-0.0.3/src/oep_client/config.py +228 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/endpoint.py +7 -3
- oep_client_python-0.0.3/tests/test_config.py +67 -0
- oep_client_python-0.0.2/PKG-INFO +0 -106
- oep_client_python-0.0.2/src/oep_client/__main__.py +0 -40
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/.gitignore +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/LICENSE +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/arm.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/capture.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/catalog.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/ch32_flash.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/cobs.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/console.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/core.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/decode.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/dump.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/esp32_targets.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/fake.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/fake_serial.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/fake_serve.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/fixture.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/frames.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/hid_stream.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/host.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/interfaces.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/link.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/message.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/names.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/registry.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/riscv.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/rp2350.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/target.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/uiapduino.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/src/oep_client/usb_stream.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_capabilities.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_capture.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_cobs.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_esp32_targets.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_fake_spec.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_flash_console.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_hid_stream.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_interfaces.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_link_host.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_link_serial.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.3}/tests/test_session.py +0 -0
- {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
|
-
|
|
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.
|
|
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.
|
|
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"]
|
|
@@ -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
|
-
|
|
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["
|
|
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
|
oep_client_python-0.0.2/PKG-INFO
DELETED
|
@@ -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())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|