oep-client-python 0.0.2__tar.gz → 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/CHANGELOG.md +12 -0
- oep_client_python-0.0.4/PKG-INFO +125 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/README.ja.md +15 -1
- oep_client_python-0.0.4/README.md +103 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/pyproject.toml +3 -3
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/__init__.py +1 -1
- oep_client_python-0.0.4/src/oep_client/__main__.py +282 -0
- oep_client_python-0.0.4/src/oep_client/config.py +228 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/endpoint.py +7 -3
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/fake_serial.py +4 -2
- oep_client_python-0.0.4/tests/test_config.py +99 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_fake_spec.py +13 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_link_serial.py +7 -2
- 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.4}/.gitignore +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/LICENSE +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/arm.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/capture.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/catalog.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/ch32_flash.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/cobs.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/console.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/core.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/decode.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/dump.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/esp32_targets.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/fake.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/fake_serve.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/fixture.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/frames.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/hid_stream.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/host.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/interfaces.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/link.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/message.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/names.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/registry.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/riscv.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/rp2350.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/target.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/uiapduino.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/src/oep_client/usb_stream.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_capabilities.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_capture.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_cobs.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_esp32_targets.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_flash_console.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_hid_stream.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_interfaces.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_link_host.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_session.py +0 -0
- {oep_client_python-0.0.2 → oep_client_python-0.0.4}/tests/test_target_parts.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.4
|
|
6
|
+
- (EN) Fake serial port: a candidate that is only its 0x00 is not raw after the 200 ms gap (the probe firmware had the same bug: a stray 0x00 reached the target's console).
|
|
7
|
+
- (JA) 偽のシリアルの口: 0x00 だけの候補は、200 ms の後も生のバイトにしない(probe の firmware にも同じバグがあり、target のコンソールに 0x00 が届いていた)。
|
|
8
|
+
- (EN) `oep config slot` takes the probe's only RISC-V wire when `--wire` is left out (a SWIO-only probe needed `--wire swio`).
|
|
9
|
+
- (JA) `oep config slot` は `--wire` を省くと、probe の唯一の RISC-V の線を使う(SWIO だけの probe で `--wire swio` が要っていた)。
|
|
10
|
+
- (EN) `oep config plan <probe> <fn | name#k> ROLE=CH ...` (role names from the registry: rx, tx, line...), `oep config label` and `oep config idle`.
|
|
11
|
+
- (JA) `oep config plan <probe> <fn | 名前#k> ROLE=CH ...`(role の名前は registry から: rx、tx、line など)、`oep config label`、`oep config idle`。
|
|
12
|
+
|
|
13
|
+
## 0.0.3
|
|
14
|
+
- (EN) `oep_client.config` (oep.probe.config: slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) and the `oep config show | slot | bind | remove | save | erase` command. An English README.md, also the PyPI page.
|
|
15
|
+
- (JA) `oep_client.config`(oep.probe.config: スロット、bind、plan / label / idle の項目、get / set / save / erase、スロットと bind の今の状態)と、`oep config show | slot | bind | remove | save | erase` の命令。英語の README.md(PyPI のページも)。
|
|
16
|
+
|
|
5
17
|
## 0.0.2
|
|
6
18
|
- (EN) Breaking: the modules move from `oep_client.v1.*` to `oep_client.*` (`from oep_client import link`, `python -m oep_client`, `python -m oep_client.fake_serve`). The protocol revision stays in the registry and confirm.
|
|
7
19
|
- (JA) 破壊的変更: モジュールを `oep_client.v1.*` から `oep_client.*` に移した(`from oep_client import link`、`python -m oep_client`、`python -m oep_client.fake_serve`)。プロトコルの revision は registry と confirm が持つ。
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: oep-client-python
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe
|
|
5
|
+
Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
|
|
6
|
+
Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
|
|
7
|
+
Author: TANAKA Masayuki
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: pyserial>=3.5
|
|
16
|
+
Requires-Dist: pyusb>=1.3
|
|
17
|
+
Provides-Extra: hid
|
|
18
|
+
Requires-Dist: hidapi>=0.14; extra == 'hid'
|
|
19
|
+
Provides-Extra: usb-async
|
|
20
|
+
Requires-Dist: libusb1>=3; extra == 'usb-async'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# OEP Python client
|
|
24
|
+
|
|
25
|
+
[日本語](README.ja.md)
|
|
26
|
+
|
|
27
|
+
The host side of Open Embedded Probe (OEP). It speaks the v1 protocol of
|
|
28
|
+
[oep-spec](https://github.com/Open-Embedded-Probe/oep-spec) (`docs/oep-core.ja.md` and the standard interfaces
|
|
29
|
+
`docs/oep-if-*.ja.md`, a candidate being settled). The wire numbers come from `oep_client.registry`, a verbatim copy of
|
|
30
|
+
oep-spec's generated `generated/oep-v1/oep_v1_registry.py`. This is an experimental stage: breaking changes are expected and
|
|
31
|
+
no compatible API is promised. For a map of the specification, start with oep-spec's `docs/review-guide.ja.md`.
|
|
32
|
+
|
|
33
|
+
It follows OEP's division of work: the knowledge of the target lives in the host. The probe knows only its wires and DMI /
|
|
34
|
+
DP-AP transfers; the CH32 flash controller, the RAM loader, the RP2350 boot ROM, the Cortex-M debug registers and so on are
|
|
35
|
+
here.
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
|
|
39
|
+
uv run pytest # in a checkout
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`import oep_client` is all it takes. The registry is copied from oep-spec with `tools/sync_registry.sh`. PyPI's
|
|
43
|
+
`oep-client` is another project, so the distribution is named `oep-client-python`.
|
|
44
|
+
|
|
45
|
+
Releases: run the GitHub Actions workflow Release (workflow_dispatch, version X.Y.Z or X.Y.ZbN). `tools/prepare_release.py`
|
|
46
|
+
sets the version in pyproject.toml and `oep_client.__version__` and turns CHANGELOG.md's Unreleased into that version; after
|
|
47
|
+
the tests and the build it commits, tags, makes the GitHub Release and publishes to PyPI (Trusted Publishing). Record changes
|
|
48
|
+
under Unreleased in CHANGELOG.md, (EN) and (JA).
|
|
49
|
+
|
|
50
|
+
## Modules (`oep_client`)
|
|
51
|
+
|
|
52
|
+
| Module | Contents |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `host` | requests and results, the session id and the lock, `call()` (raises unless it worked), pipelining, the errors (`OepError` / `Rejected` / `Failed`) |
|
|
55
|
+
| `link` | transports: serial ports (always COBS + CRC as `0x00 <COBS> 0x00`, bytes outside frames skipped as noise, opened exclusively), USB vendor bulk / HID and TCP (length frames, the §5.1 resync); matching by corr and resending; `open_host(target)` |
|
|
56
|
+
| `core` | interfaces by name (cached), confirm, the probe's describe (labels, the transport list), taking the lock (`take`), the pin plan, the `Interface` base |
|
|
57
|
+
| `riscv` | `oep.wire.rvswd` / `oep.wire.swio`, `oep.target.riscv-dm`, finding the reset line, attach through GPIO |
|
|
58
|
+
| `console` | `oep.target.console` (position streams) and `ConsoleIO`, read as bytes |
|
|
59
|
+
| `fixture` | `oep.fixture.gpio` / `uart` (revision 1) |
|
|
60
|
+
| `config` | `oep.probe.config` (slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) |
|
|
61
|
+
| `capture` | `oep.fixture.capture` (revision 1, oep-spec oep-if-capture). Every segment read goes to the `Host.on_capture` callbacks as a `CaptureRecord` (the hook for run recorders; no wireskein dependency) |
|
|
62
|
+
| `esp32_targets` | the custom interfaces `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target` (the ESP32 I2C / SPI targets of oep-probe-arduino) |
|
|
63
|
+
| `decode` | decoding capture channels (I2C) |
|
|
64
|
+
| `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec) |
|
|
65
|
+
| `arm` | `oep.wire.swd`, `oep.target.arm-adi`, MEM-AP, halting and calling functions on a Cortex-M |
|
|
66
|
+
| `ch32_flash` | writing a CH32 (a RAM loader, page by page) |
|
|
67
|
+
| `rp2350` | flash and reboot through the RP2350 boot ROM |
|
|
68
|
+
| `uiapduino` | into and out of the UIAPduino bootloader |
|
|
69
|
+
| `catalog` / `names` / `interfaces` / `dump` | the capability list and describe shapes, display |
|
|
70
|
+
| `fake` / `endpoint` / `fake_serial` / `fake_serve` | the fake probe (below) |
|
|
71
|
+
| `target` | one place to import the main ones from |
|
|
72
|
+
|
|
73
|
+
## Example
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from oep_client import core, link, riscv, ch32_flash
|
|
77
|
+
|
|
78
|
+
hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelined
|
|
79
|
+
# a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
|
|
80
|
+
core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
|
|
81
|
+
wire = riscv.Wire(hst, "oep.wire.rvswd")
|
|
82
|
+
conn, _ = wire.attach(halt=True)
|
|
83
|
+
dm = riscv.RiscvDm(hst, conn)
|
|
84
|
+
dm.reset_halt()
|
|
85
|
+
result = ch32_flash.program(hst, dm, open("sketch.bin", "rb").read(), ch32_flash.PROFILES["x035"])
|
|
86
|
+
dm.reset(confirm=True)
|
|
87
|
+
wire.detach(conn)
|
|
88
|
+
hst.end()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## The `oep` command
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
oep dump --port <probe> # what the probe offers (--fake p4-x035: no hardware)
|
|
95
|
+
oep config show <probe> # the settings and the live slot / bind state
|
|
96
|
+
oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
|
|
97
|
+
oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
|
|
98
|
+
oep config save <probe> # kept over a restart (also: remove, erase)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`<probe>` is a serial port, `tcp://HOST:PORT` or `usb[:VID:PID[:SERIAL]]`. A change takes the lock (owner "oep config") and
|
|
102
|
+
ends the session after it; it takes effect at once and, after `save`, stays over a restart.
|
|
103
|
+
|
|
104
|
+
A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`, `oep_probe_checks.py`).
|
|
105
|
+
|
|
106
|
+
## The fake probe (a working spec)
|
|
107
|
+
|
|
108
|
+
`endpoint.Endpoint` is a fake probe that answers as oep-spec says; ch32rv, this client and the probe firmware are checked
|
|
109
|
+
against it (when the spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
|
|
110
|
+
`p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats), `fake_serial` the byte side of a serial
|
|
111
|
+
port (COBS candidates, raw bytes and binds, held during a session and resumed after it).
|
|
112
|
+
|
|
113
|
+
Other programs' tests run `fake_serve` as a child process:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
python -m oep_client.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
|
|
117
|
+
--console 'uptime %d\r\n' --every 100
|
|
118
|
+
# first line: PTY /dev/pts/N (PORT n with --tcp 0); it ends when stdin closes
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--framing cobs` (a serial port) or
|
|
122
|
+
`--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
|
|
123
|
+
so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
|
|
124
|
+
broken once). `--uart-plan` / `--uart-rx` give the first fixture UART a plan and RX bytes, `--run-hook` a host's own model of
|
|
125
|
+
riscv-dm run. The rest: `--help`.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# OEP Python client
|
|
2
2
|
|
|
3
|
+
[English](README.md)
|
|
4
|
+
|
|
3
5
|
Open Embedded Probe の host 側。v1(oep-spec の `docs/oep-core.ja.md` と `docs/oep-if-*.ja.md`、固める候補の形)を話す。番号は oep-spec の
|
|
4
6
|
`generated/oep-v1/oep_v1_registry.py` をそのまま写した `oep_client.registry` から取る。破壊的変更を前提とする
|
|
5
7
|
実験段階で、互換 API は約束しない。OEP を初めて読む人は oep-spec の `docs/review-guide.ja.md`(どこに何が書いてあるか)から。
|
|
@@ -29,6 +31,7 @@ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md
|
|
|
29
31
|
| `riscv` | `oep.wire.rvswd` / `oep.wire.swio`、`oep.target.riscv-dm`、リセット線の探索、GPIO 経由の attach |
|
|
30
32
|
| `console` | `oep.target.console`(位置つきのストリーム)と、バイト列として読む `ConsoleIO` |
|
|
31
33
|
| `fixture` | `oep.fixture.gpio` / `uart`(revision 1) |
|
|
34
|
+
| `config` | `oep.probe.config`(スロット、bind、plan / label / idle の項目、get / set / save / erase、スロットと bind の今の状態) |
|
|
32
35
|
| `capture` | `oep.fixture.capture`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
|
|
33
36
|
| `esp32_targets` | 独自インターフェース `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target`(oep-probe-arduino の ESP32 の I2C / SPI の target) |
|
|
34
37
|
| `decode` | キャプチャのチャネルの復号(I2C) |
|
|
@@ -59,7 +62,18 @@ wire.detach(conn)
|
|
|
59
62
|
hst.end()
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
|
|
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.4"
|
|
8
8
|
description = "Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe"
|
|
9
|
-
readme = "README.
|
|
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,282 @@
|
|
|
1
|
+
"""The oep command: what a probe offers (dump) and its settings (config).
|
|
2
|
+
|
|
3
|
+
oep dump --port /run/board-identify/by-id/<probe> oep dump --fake p4-x035 --prefix oep.target --json
|
|
4
|
+
oep config show <probe>
|
|
5
|
+
oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
|
|
6
|
+
oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
|
|
7
|
+
oep config plan <probe> oep.fixture.uart#2 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
|
|
8
|
+
oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
|
|
9
|
+
|
|
10
|
+
<probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
|
|
11
|
+
ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import struct
|
|
21
|
+
|
|
22
|
+
from . import catalog, config, core, dump, fake, host, link
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv=None) -> int:
|
|
26
|
+
parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
|
|
27
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
28
|
+
_config_parser(sub)
|
|
29
|
+
d = sub.add_parser("dump", help="list and describe every interface a probe offers")
|
|
30
|
+
src = d.add_mutually_exclusive_group(required=True)
|
|
31
|
+
src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
|
|
32
|
+
src.add_argument("--port", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID] (lock-free reads only)")
|
|
33
|
+
d.add_argument("--prefix", default="", help="only names under this namespace (label boundaries)")
|
|
34
|
+
d.add_argument("--exact", action="store_true", help="the prefix is a whole name")
|
|
35
|
+
d.add_argument("--json", action="store_true", help="machine-readable output")
|
|
36
|
+
args = parser.parse_args(argv)
|
|
37
|
+
if args.command == "config":
|
|
38
|
+
return _config(args)
|
|
39
|
+
|
|
40
|
+
if args.fake:
|
|
41
|
+
call = fake.PROFILES[args.fake]().call
|
|
42
|
+
else:
|
|
43
|
+
hst = link.open_host(args.port)
|
|
44
|
+
call = lambda fn, op, payload: hst.request(fn, op, payload, locked=False).payload # noqa: E731
|
|
45
|
+
caps = dump.collect(call, args.prefix, args.exact)
|
|
46
|
+
sys.stdout.write(dump.to_json(caps) + "\n" if args.json else dump.to_text(caps))
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---- oep config ----------------------------------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def _config_parser(sub) -> None:
|
|
53
|
+
c = sub.add_parser("config", help="the probe's settings (oep.probe.config): slots, binds, save")
|
|
54
|
+
cs = c.add_subparsers(dest="action", required=True)
|
|
55
|
+
show = cs.add_parser("show", help="the settings and the live slot / bind state")
|
|
56
|
+
show.add_argument("probe")
|
|
57
|
+
show.add_argument("--json", action="store_true")
|
|
58
|
+
slot = cs.add_parser("slot", help="register a slot (a place a target is wired to)")
|
|
59
|
+
slot.add_argument("probe")
|
|
60
|
+
slot.add_argument("--slot", type=int, default=0, help="the slot number (default 0)")
|
|
61
|
+
slot.add_argument("--name", required=True, help="1-32 of a-z 0-9 - _ (the oep://<probe>/<name> address)")
|
|
62
|
+
slot.add_argument("--wire", help="rvswd or swio (or an fn); default: the probe's only wire")
|
|
63
|
+
slot.add_argument("--pins", help="swdio,swclk (one pin on swio); default: the wire's only pin set")
|
|
64
|
+
slot.add_argument("--attach", choices=sorted(config.ATTACH), default="host")
|
|
65
|
+
slot.add_argument("--retry", type=int, default=0, help="at-boot: try again every N s while absent (0: never)")
|
|
66
|
+
slot.add_argument("--mechanism", choices=sorted(config.MECHANISM), default="dmseq")
|
|
67
|
+
slot.add_argument("--lock", help="MASK:VALUE (hex u32) the target_id (WCH DMI 0x7F) must match, e.g. ffffff0f:035e0600")
|
|
68
|
+
slot.add_argument("--save", action="store_true", help="save after the change")
|
|
69
|
+
bind = cs.add_parser("bind", help="what a serial port carries")
|
|
70
|
+
bind.add_argument("probe")
|
|
71
|
+
bind.add_argument("--port", type=int, required=True, help="the serial port (its transport index, see show)")
|
|
72
|
+
bind.add_argument("--mode", choices=sorted(config.MODE), default="last-reset")
|
|
73
|
+
bind.add_argument("--stream", action="append", required=True, help="slot:NAME, slot:N or uart:FN (repeatable)")
|
|
74
|
+
bind.add_argument("--select", type=int, default=0, help="manual: the stream it carries (index in --stream)")
|
|
75
|
+
bind.add_argument("--save", action="store_true")
|
|
76
|
+
plan = cs.add_parser("plan", help="the pins an interface keeps (its whole plan, kept as a setting)")
|
|
77
|
+
plan.add_argument("probe")
|
|
78
|
+
plan.add_argument("fn", help="an fn, or an interface name with #k for its k-th instance (1 = the first)")
|
|
79
|
+
plan.add_argument("roles", nargs="+", help="ROLE=CHANNEL, ROLE a number or the interface's role name (rx, tx, line...)")
|
|
80
|
+
plan.add_argument("--save", action="store_true")
|
|
81
|
+
label = cs.add_parser("label", help="name a channel (shown in oep.core's describe)")
|
|
82
|
+
label.add_argument("probe")
|
|
83
|
+
label.add_argument("channel", type=int)
|
|
84
|
+
label.add_argument("text")
|
|
85
|
+
label.add_argument("--save", action="store_true")
|
|
86
|
+
idle = cs.add_parser("idle", help="the state of a free pin")
|
|
87
|
+
idle.add_argument("probe")
|
|
88
|
+
idle.add_argument("channel", type=int)
|
|
89
|
+
idle.add_argument("mode", choices=sorted(config.IDLE))
|
|
90
|
+
idle.add_argument("--save", action="store_true")
|
|
91
|
+
rm = cs.add_parser("remove", help="remove one item: slot N, bind PORT, plan FN, label CH, idle CH")
|
|
92
|
+
rm.add_argument("probe")
|
|
93
|
+
rm.add_argument("kind", choices=["slot", "bind", "plan", "label", "idle"])
|
|
94
|
+
rm.add_argument("key", type=int)
|
|
95
|
+
rm.add_argument("--save", action="store_true")
|
|
96
|
+
for name in ("save", "erase"):
|
|
97
|
+
cs.add_parser(name, help=f"{name} the stored settings").add_argument("probe")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _pins(hst, fn: int, text: str | None, wire: str) -> tuple[int, int]:
|
|
101
|
+
if text:
|
|
102
|
+
parts = [int(x, 0) for x in text.split(",")]
|
|
103
|
+
return (parts[0], parts[1] if len(parts) > 1 else 0xFFFF)
|
|
104
|
+
groups = []
|
|
105
|
+
for tag, v in core.describe(hst, fn):
|
|
106
|
+
if tag & 0x7F == catalog.CHANNEL_GROUP:
|
|
107
|
+
roles = {v[1 + 3 * i]: struct.unpack_from("<H", v, 2 + 3 * i)[0] for i in range((len(v) - 1) // 3)}
|
|
108
|
+
groups.append((roles.get(1, 0xFFFF), roles.get(2, 0xFFFF)))
|
|
109
|
+
if len(groups) != 1:
|
|
110
|
+
raise SystemExit(f"{wire}: {len(groups)} pin sets on this probe - name one with --pins")
|
|
111
|
+
return groups[0]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _plan_fn(hst, spec: str) -> tuple[int, dict[str, int]]:
|
|
115
|
+
"""fn and its role names (from the registry) for `spec`: an fn number, or name[#k] (k-th instance, 1-based)."""
|
|
116
|
+
from . import registry as reg
|
|
117
|
+
if spec.isdigit():
|
|
118
|
+
fn = int(spec)
|
|
119
|
+
name = next((e.name for e in core.list_entries(hst) if e.fn == fn), "")
|
|
120
|
+
else:
|
|
121
|
+
name, _, k = spec.partition("#")
|
|
122
|
+
fns = core.find_all(hst, name)
|
|
123
|
+
if not fns:
|
|
124
|
+
raise SystemExit(f"the probe offers no {name}")
|
|
125
|
+
index = int(k) - 1 if k else 0
|
|
126
|
+
if not 0 <= index < len(fns):
|
|
127
|
+
raise SystemExit(f"{name}: {len(fns)} instance(s), fns {fns}")
|
|
128
|
+
fn = fns[index]
|
|
129
|
+
iface = reg.INTERFACES.get(name)
|
|
130
|
+
roles = dict(iface.enum.get("role", {})) if iface else {}
|
|
131
|
+
return fn, roles
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _wire_fn(hst, wire: str | None) -> int:
|
|
135
|
+
if wire is None:
|
|
136
|
+
wires = [e for e in core.list_entries(hst, "oep.wire") if e.name in ("oep.wire.rvswd", "oep.wire.swio")]
|
|
137
|
+
if len(wires) != 1:
|
|
138
|
+
raise SystemExit("this probe has " + (", ".join(f"{e.name} (fn {e.fn})" for e in wires) or "no RISC-V wire")
|
|
139
|
+
+ ": name one with --wire")
|
|
140
|
+
return wires[0].fn
|
|
141
|
+
name = wire if wire.startswith("oep.") else f"oep.wire.{wire}"
|
|
142
|
+
if wire.isdigit():
|
|
143
|
+
return int(wire)
|
|
144
|
+
try:
|
|
145
|
+
return core.find(hst, name)
|
|
146
|
+
except LookupError:
|
|
147
|
+
raise SystemExit(f"this probe does not offer {name}") from None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _stream(spec: str, slots: dict[str, int]) -> tuple[str, int]:
|
|
151
|
+
kind, _, key = spec.partition(":")
|
|
152
|
+
if kind not in config.STREAM or not key:
|
|
153
|
+
raise SystemExit(f"--stream {spec}: want slot:NAME, slot:N or uart:FN")
|
|
154
|
+
if kind == "slot" and not key.isdigit():
|
|
155
|
+
if key not in slots:
|
|
156
|
+
raise SystemExit(f"--stream {spec}: no slot named {key} (see oep config show)")
|
|
157
|
+
return kind, slots[key]
|
|
158
|
+
return kind, int(key)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _change(hst, cfg, items, save: bool) -> None:
|
|
162
|
+
core.take(hst, 3000, owner="oep config")
|
|
163
|
+
try:
|
|
164
|
+
h = cfg.set(items)
|
|
165
|
+
print(f"set: hash 0x{h:08x}")
|
|
166
|
+
if save:
|
|
167
|
+
print(f"saved: hash 0x{cfg.save():08x}")
|
|
168
|
+
finally:
|
|
169
|
+
hst.end()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _config(args) -> int:
|
|
173
|
+
hst = link.open_host(args.probe)
|
|
174
|
+
try:
|
|
175
|
+
cfg = config.ProbeConfig(hst)
|
|
176
|
+
if args.action == "show":
|
|
177
|
+
return _show(hst, cfg, args.json)
|
|
178
|
+
if args.action == "slot":
|
|
179
|
+
fn = _wire_fn(hst, args.wire)
|
|
180
|
+
args.wire = args.wire or str(fn)
|
|
181
|
+
lock = None
|
|
182
|
+
if args.lock:
|
|
183
|
+
mask, _, value = args.lock.partition(":")
|
|
184
|
+
lock = (1, struct.pack("<I", int(mask, 16)), struct.pack("<I", int(value, 16)))
|
|
185
|
+
it = config.Slot(args.slot, fn, _pins(hst, fn, args.pins, args.wire), args.name, args.attach, args.retry,
|
|
186
|
+
args.mechanism, lock)
|
|
187
|
+
_change(hst, cfg, [it], args.save)
|
|
188
|
+
elif args.action == "bind":
|
|
189
|
+
slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
|
|
190
|
+
it = config.Bind(args.port, args.mode, [_stream(s, slots) for s in args.stream], args.select)
|
|
191
|
+
_change(hst, cfg, [it], args.save)
|
|
192
|
+
elif args.action == "plan":
|
|
193
|
+
fn, roles = _plan_fn(hst, args.fn)
|
|
194
|
+
items = []
|
|
195
|
+
for spec in args.roles:
|
|
196
|
+
role, _, ch = spec.partition("=")
|
|
197
|
+
if not ch:
|
|
198
|
+
raise SystemExit(f"{spec}: want ROLE=CHANNEL")
|
|
199
|
+
number = int(role) if role.isdigit() else roles.get(role.lower())
|
|
200
|
+
if number is None:
|
|
201
|
+
raise SystemExit(f"{role}: not a role of fn {fn} (roles: {', '.join(sorted(roles)) or 'numbers only'})")
|
|
202
|
+
items.append(config.Plan(fn, number, int(ch, 0)))
|
|
203
|
+
_change(hst, cfg, items, args.save)
|
|
204
|
+
elif args.action == "label":
|
|
205
|
+
_change(hst, cfg, [config.Label(args.channel, args.text)], args.save)
|
|
206
|
+
elif args.action == "idle":
|
|
207
|
+
_change(hst, cfg, [config.Idle(args.channel, args.mode)], args.save)
|
|
208
|
+
elif args.action == "remove":
|
|
209
|
+
_change(hst, cfg, [config.remove(args.kind, args.key)], args.save)
|
|
210
|
+
else:
|
|
211
|
+
core.take(hst, 3000, owner="oep config")
|
|
212
|
+
try:
|
|
213
|
+
if args.action == "save":
|
|
214
|
+
print(f"saved: hash 0x{cfg.save():08x}")
|
|
215
|
+
else:
|
|
216
|
+
cfg.erase()
|
|
217
|
+
print("erased (the settings now in effect stay until the probe restarts)")
|
|
218
|
+
finally:
|
|
219
|
+
hst.end()
|
|
220
|
+
return 0
|
|
221
|
+
except host.Rejected as e:
|
|
222
|
+
print(f"refused: {e}", file=sys.stderr)
|
|
223
|
+
return 2
|
|
224
|
+
finally:
|
|
225
|
+
hst.link.close()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _show(hst, cfg, as_json: bool) -> int:
|
|
229
|
+
h, _ = cfg.get()
|
|
230
|
+
items = cfg.items()
|
|
231
|
+
st = cfg.state()
|
|
232
|
+
kinds = {i: _name(k) for i, k, _ in core.transports(hst)}
|
|
233
|
+
if as_json:
|
|
234
|
+
def plain(o):
|
|
235
|
+
return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
|
|
236
|
+
else list(o)
|
|
237
|
+
out = {"hash": h, "items": [dict(type=type(i).__name__, **plain(i)) if hasattr(i, "__dict__") else plain(i)
|
|
238
|
+
for i in items],
|
|
239
|
+
"state": {**{k: v for k, v in vars(st).items() if k not in ("slots", "binds")},
|
|
240
|
+
"slots": [plain(s) for s in st.slots], "binds": [plain(b) for b in st.binds]},
|
|
241
|
+
"transports": kinds}
|
|
242
|
+
print(json.dumps(out, indent=2, default=lambda o: o.hex() if isinstance(o, bytes) else str(o)))
|
|
243
|
+
return 0
|
|
244
|
+
print(f"storage: {st.storage} ({st.storage_bytes} bytes), saved hash 0x{st.saved_hash:08x}; now 0x{h:08x}")
|
|
245
|
+
print("transports: " + ", ".join(f"{i} {k}" for i, k in kinds.items()))
|
|
246
|
+
by_slot = {s.slot: s for s in st.slots}
|
|
247
|
+
print(f"slots (up to {st.slots_max}):")
|
|
248
|
+
for it in items:
|
|
249
|
+
if isinstance(it, config.Slot):
|
|
250
|
+
s = by_slot.get(it.slot)
|
|
251
|
+
pins = f"{it.pins[0]}" if it.pins[1] == 0xFFFF else f"{it.pins[0]},{it.pins[1]}"
|
|
252
|
+
retry = f" retry {it.retry_s} s" if it.attach == "at-boot" else ""
|
|
253
|
+
lock = (f" lock {int.from_bytes(it.lock[1], 'little'):08x}:{int.from_bytes(it.lock[2], 'little'):08x}"
|
|
254
|
+
if it.lock else "")
|
|
255
|
+
live = ""
|
|
256
|
+
if s:
|
|
257
|
+
tried = "never tried" if s.last_try_ms is None else f"tried {s.last_try_ms} ms ago"
|
|
258
|
+
tid = f" target_id {s.target_id[::-1].hex()}" if s.target_id else ""
|
|
259
|
+
live = f" -> {s.state}" + (f" (connection {s.connection})" if s.connection else f" ({tried})") + tid
|
|
260
|
+
print(f" {it.slot} {it.name}: fn {it.wire_fn} pins {pins} {it.attach}{retry} {it.mechanism}{lock}{live}")
|
|
261
|
+
by_port = {b.port: b for b in st.binds}
|
|
262
|
+
print(f"binds (modes: {', '.join(st.bind_modes) or '-'}):")
|
|
263
|
+
names = {it.slot: it.name for it in items if isinstance(it, config.Slot)}
|
|
264
|
+
for it in items:
|
|
265
|
+
if isinstance(it, config.Bind):
|
|
266
|
+
b = by_port.get(it.port)
|
|
267
|
+
streams = ", ".join(f"slot:{names.get(i, i)}" if k == "slot" else f"{k}:{i}" for k, i in it.streams)
|
|
268
|
+
sel = f" selected {it.selected}" if it.mode == "manual" else ""
|
|
269
|
+
live = f" -> {b.flow}" + (f", carrying {b.selected}" if b and b.selected is not None else "") if b else ""
|
|
270
|
+
print(f" port {it.port} ({kinds.get(it.port, '?')}): {it.mode} [{streams}]{sel}{live}")
|
|
271
|
+
for it in items:
|
|
272
|
+
if not isinstance(it, (config.Slot, config.Bind)):
|
|
273
|
+
print(f" {it}")
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _name(kind: int) -> str:
|
|
278
|
+
return {v: k for k, v in core.TRANSPORT_KIND.items()}.get(kind, str(kind))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
sys.exit(main())
|