oep-client-python 0.0.18__tar.gz → 0.0.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/CHANGELOG.md +22 -0
  2. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/PKG-INFO +10 -7
  3. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/README.ja.md +9 -6
  4. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/README.md +9 -6
  5. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/pyproject.toml +1 -1
  6. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/__init__.py +1 -1
  7. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/__main__.py +8 -6
  8. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/capture.py +5 -5
  9. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/catalog.py +6 -5
  10. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/ch32_flash.py +8 -8
  11. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/config.py +28 -21
  12. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/console.py +6 -4
  13. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/decode.py +1 -1
  14. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/endpoint.py +111 -30
  15. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/fake.py +17 -17
  16. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/fake_capture.py +5 -5
  17. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/fake_serve.py +1 -1
  18. oep_client_python-0.0.20/src/oep_client/fixture.py +233 -0
  19. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/host.py +39 -2
  20. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/interfaces.py +4 -4
  21. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/link.py +47 -1
  22. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/message.py +12 -0
  23. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/registry.py +12 -6
  24. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/riscv.py +1 -1
  25. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/uiapduino.py +8 -8
  26. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/usb_stream.py +8 -1
  27. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_capabilities.py +7 -6
  28. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_config.py +44 -8
  29. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_fake_capture.py +3 -2
  30. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_fake_spec.py +6 -5
  31. oep_client_python-0.0.18/tests/test_esp32_targets.py → oep_client_python-0.0.20/tests/test_fixture_targets.py +7 -6
  32. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_interfaces.py +1 -1
  33. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_session.py +1 -1
  34. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_target_parts.py +1 -1
  35. oep_client_python-0.0.18/src/oep_client/esp32_targets.py +0 -137
  36. oep_client_python-0.0.18/src/oep_client/fixture.py +0 -121
  37. oep_client_python-0.0.18/src/oep_client/target.py +0 -16
  38. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/.gitignore +0 -0
  39. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/LICENSE +0 -0
  40. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/arm.py +0 -0
  41. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/cobs.py +0 -0
  42. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/core.py +0 -0
  43. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/dump.py +0 -0
  44. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/fake_serial.py +0 -0
  45. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/frames.py +0 -0
  46. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/hid_stream.py +0 -0
  47. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/names.py +0 -0
  48. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/src/oep_client/rp2350.py +0 -0
  49. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_capture.py +0 -0
  50. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_cobs.py +0 -0
  51. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_flash_console.py +0 -0
  52. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_hid_stream.py +0 -0
  53. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_link_host.py +0 -0
  54. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_link_serial.py +0 -0
  55. {oep_client_python-0.0.18 → oep_client_python-0.0.20}/tests/test_usb_stream.py +0 -0
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.20
6
+ - (EN) A console write's chunk fits the probe's frame: frame - 12 - the stream's prefix (the console's stream u16; none on a fixture UART). It was frame - 13 for both, one byte over on a console with 64-byte frames. Found porting it to oep-client-js.
7
+ - (JA) console の書き込みの 1 回の大きさが probe のフレームに収まるようにした: frame − 12 − stream の前置き(console は stream の u16、fixture UART は無し)。両方とも frame − 13 だったので、64 byte のフレームの console で 1 byte はみ出していた。oep-client-js に移すときに見つかった。
8
+
9
+ ## 0.0.19
10
+ - (EN) The pre-freeze decisions (oep-spec docs/v1-freeze-decisions.ja.md), in the fake and the client at once:
11
+ - `oep.fixture.capture` is `oep.fixture.logic`; the I2C / SPI targets are the standard `oep.fixture.i2c-target` / `spi-target` (`fixture.I2cTarget` / `SpiTarget`; `esp32_targets` is gone). Their status is separate fields (`I2cStatus(state=, mode=, armed=, queued=, …)`); `read_hw` is gone, `set_stretch` is `stretch` (op 0x07).
12
+ - Answer lists put each element's length first (core §2.3): list, scan, connections, marks, capture segments (`message.Reader.element()`, `message.element()`); what an element carries after the known fields is skipped.
13
+ - probe.config: a slot's lock is length-prefixed (`lock_len`, 0 = none); settings are saved with the (name, instance, revision) of each interface they name and renumbered at boot, so another interface added or moved leaves them in force (the fake: `saved_ids`); the storage says why it is unreadable (`State.unreadable`). The item classes (`Plan`, `Label`, `Idle`, `Slot`, `Bind`) take keyword arguments only.
14
+ - rejected unavailable carries core §4.3's TLVs: `host.Unavailable` with `.cause`, `.channels`, `.holder_fn`, `.holder_kind` (and `.tlvs`; gpio's refused position is its TLV 0x40). An unknown console stream is `no_connection`.
15
+ - The analog's zero and scale are signed (i32).
16
+ - USB: the vendor transport is the class 0xFF interface's bulk pair (not the first bulk pair); `open_host("usb:<unit id>")` finds the OEP probe whose USB serial is that unit id; unit_id is text (the fake: `esp32p4` / `esp32` / `rp2350` models, lowercase hex unit ids).
17
+ - `target` (the first draft's re-exports) is gone: import the modules; README lists the public modules and the version pairing with the firmware.
18
+ - (JA) 凍結前の決定(oep-spec docs/v1-freeze-decisions.ja.md)を、fake とクライアントに一度に入れた:
19
+ - `oep.fixture.capture` は `oep.fixture.logic` になった。I2C / SPI の target は標準の `oep.fixture.i2c-target` / `spi-target`(`fixture.I2cTarget` / `SpiTarget`。`esp32_targets` は無くなった)。status は分けたフィールド(`I2cStatus(state=, mode=, armed=, queued=, …)`)、`read_hw` は無くなり、`set_stretch` は `stretch`(op 0x07)。
20
+ - 応答の並びは要素の前に長さを置く(core §2.3): list、scan、connections、marks、capture の segments(`message.Reader.element()`、`message.element()`)。要素の知っているフィールドの後ろは飛ばす。
21
+ - probe.config: スロットの錠は長さ付き(`lock_len`、0 は錠なし)。設定は、指すインターフェースの (name、instance、revision) と一緒に保存し、起動時に番号を読み替える。ほかのインターフェースを足したり動かしたりしても設定は効いたまま(fake: `saved_ids`)。storage は読めない理由を返す(`State.unreadable`)。項目のクラス(`Plan`、`Label`、`Idle`、`Slot`、`Bind`)はキーワード引数だけを取る。
22
+ - rejected unavailable は core §4.3 の TLV を持つ: `host.Unavailable` の `.cause`、`.channels`、`.holder_fn`、`.holder_kind`(と `.tlvs`。gpio の断った位置は、その TLV 0x40)。知らない console の stream は `no_connection`。
23
+ - アナログの zero と scale は符号付き(i32)。
24
+ - USB: vendor の経路は class 0xFF のインターフェースの bulk の組(最初の bulk の組ではない)。`open_host("usb:<unit id>")` は、USB の serial がその unit id の OEP の probe を探す。unit_id は text(fake: model は `esp32p4` / `esp32` / `rp2350`、unit id は小文字の 16 進)。
25
+ - `target`(最初の版の再輸出)は無くなった: モジュールを直接 import する。README に公開のモジュールと、firmware との版の組を書いた。
26
+
5
27
  ## 0.0.18
6
28
  - (EN) No user-facing changes recorded.
7
29
  - (JA) ユーザー向け変更の記録はありません。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.18
3
+ Version: 0.0.20
4
4
  Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe
5
5
  Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
6
6
  Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
@@ -49,6 +49,10 @@ under Unreleased in CHANGELOG.md, (EN) and (JA).
49
49
 
50
50
  ## Modules (`oep_client`)
51
51
 
52
+ The modules below are the public API; import them directly (`from oep_client import riscv`). Anything not listed, and
53
+ names starting with `_`, may change. A probe firmware and this client go together by version: OpenEmbeddedProbe X.Y.Z with
54
+ oep-client-python X.Y.Z (until the v1 freeze every release may break the wire; the versions move together).
55
+
52
56
  | Module | Contents |
53
57
  |---|---|
54
58
  | `host` | requests and results, the session id and the lock, `call()` (raises unless it worked), pipelining, the errors (`OepError` / `Rejected` / `Failed`) |
@@ -56,10 +60,9 @@ under Unreleased in CHANGELOG.md, (EN) and (JA).
56
60
  | `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
61
  | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`, `oep.target.riscv-dm`, finding the reset line, attach through GPIO |
58
62
  | `console` | `oep.target.console` (position streams) and `ConsoleIO`, read as bytes |
59
- | `fixture` | `oep.fixture.gpio` / `uart` (revision 1) |
63
+ | `fixture` | `oep.fixture.gpio` / `uart` / `i2c-target` / `spi-target` (revision 1) |
60
64
  | `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) |
65
+ | `capture` | `oep.fixture.logic` (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) |
63
66
  | `decode` | decoding capture channels (I2C) |
64
67
  | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec) |
65
68
  | `arm` | `oep.wire.swd`, `oep.target.arm-adi`, MEM-AP, halting and calling functions on a Cortex-M |
@@ -68,7 +71,6 @@ under Unreleased in CHANGELOG.md, (EN) and (JA).
68
71
  | `uiapduino` | into and out of the UIAPduino bootloader |
69
72
  | `catalog` / `names` / `interfaces` / `dump` | the capability list and describe shapes, display |
70
73
  | `fake` / `endpoint` / `fake_serial` / `fake_serve` | the fake probe (below) |
71
- | `target` | one place to import the main ones from |
72
74
 
73
75
  ## Example
74
76
 
@@ -76,7 +78,8 @@ under Unreleased in CHANGELOG.md, (EN) and (JA).
76
78
  from oep_client import core, link, riscv, ch32_flash
77
79
 
78
80
  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)
81
+ # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb:<unit id>" (the probe whose USB serial it is),
82
+ # "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
80
83
  core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
81
84
  wire = riscv.Wire(hst, "oep.wire.rvswd")
82
85
  conn, _ = wire.attach(halt=True)
@@ -111,7 +114,7 @@ against it (when the spec changes, this is brought in line before the firmware).
111
114
  chooses), `fake_serial` the byte side of a serial port (COBS candidates, raw bytes and binds, held during a session and
112
115
  resumed after it).
113
116
 
114
- `fake_capture` is `oep.fixture.capture` (logic): one-shot, repeat (segments with the clock at the actual rate, a ring,
117
+ `fake_capture` is `oep.fixture.logic` (logic): one-shot, repeat (segments with the clock at the actual rate, a ring,
115
118
  release) and streaming (data pushes while subscribed), level / edge triggers with a pretrigger, and events. What it
116
119
  captures is known: sample i is the counter i, channel k its bit k (a square wave of period 2^(k+1) samples), in the layout
117
120
  the profile allows (`p4-x035`: w 1-16 as the P4's PARLIO, three channels in w 4; `esp32-v003`: w 8 as the classic ESP32's
@@ -23,6 +23,10 @@ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md
23
23
 
24
24
  ## モジュール(`oep_client`)
25
25
 
26
+ 下の表のモジュールが公開の API です。直接 import します(`from oep_client import riscv`)。表に無いものと、`_` で始まる名前は
27
+ 変わることがあります。probe の firmware とこのクライアントは版で組になります: OpenEmbeddedProbe X.Y.Z と oep-client-python
28
+ X.Y.Z(v1 の凍結までは、どのリリースも wire を壊しうるので、版を一緒に動かします)。
29
+
26
30
  | モジュール | 中身 |
27
31
  |---|---|
28
32
  | `host` | 要求と結果、session_id とロック、`call()`(失敗なら例外)、pipeline、エラーの階層(`OepError` / `Rejected` / `Failed`) |
@@ -30,10 +34,9 @@ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md
30
34
  | `core` | インターフェースを名前で探す(キャッシュつき)、confirm、probe の describe(ラベル、transport の一覧)、ロックの取り方(`take`)、ピンの割り当て(plan)、`Interface` の土台 |
31
35
  | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`、`oep.target.riscv-dm`、リセット線の探索、GPIO 経由の attach |
32
36
  | `console` | `oep.target.console`(位置つきのストリーム)と、バイト列として読む `ConsoleIO` |
33
- | `fixture` | `oep.fixture.gpio` / `uart`(revision 1) |
37
+ | `fixture` | `oep.fixture.gpio` / `uart` / `i2c-target` / `spi-target`(revision 1) |
34
38
  | `config` | `oep.probe.config`(スロット、bind、plan / label / idle の項目、get / set / save / erase、スロットと bind の今の状態) |
35
- | `capture` | `oep.fixture.capture`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
36
- | `esp32_targets` | 独自インターフェース `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target`(oep-probe-arduino の ESP32 の I2C / SPI の target) |
39
+ | `capture` | `oep.fixture.logic`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
37
40
  | `decode` | キャプチャのチャネルの復号(I2C) |
38
41
  | `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す) |
39
42
  | `arm` | `oep.wire.swd`、`oep.target.arm-adi`、MEM-AP、Cortex-M の停止と関数呼び出し |
@@ -42,7 +45,6 @@ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md
42
45
  | `uiapduino` | UIAPduino のブートローダへの出入り |
43
46
  | `catalog` / `names` / `interfaces` / `dump` | 能力の一覧と describe の形、表示 |
44
47
  | `fake` / `endpoint` / `fake_serial` / `fake_serve` | 偽の probe(下の「偽の probe」) |
45
- | `target` | 上の主なものを 1 か所から import する入口(最初の版に合わせて書いた呼び出し側のため) |
46
48
 
47
49
  ## 使い方の例
48
50
 
@@ -50,7 +52,8 @@ GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md
50
52
  from oep_client import core, link, riscv, ch32_flash
51
53
 
52
54
  hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelining つき
53
- # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
55
+ # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb:<unit id>" (the probe whose USB serial it is),
56
+ # "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
54
57
  core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
55
58
  wire = riscv.Wire(hst, "oep.wire.rvswd")
56
59
  conn, _ = wire.attach(halt=True)
@@ -83,7 +86,7 @@ oep config save <probe> # 再起動の後も残す(remove
83
86
  `p4-x035`、`esp32-v003`、`p4-bench` = スロット 3 か所と席 2 つの架空の治具、`rp2350-pins` = host がピンを選ぶ wire)、`fake_serial` は
84
87
  シリアルの口のバイトの側(COBS の候補、生のバイトと bind、セッション中の停止と再開)。
85
88
 
86
- `fake_capture` は `oep.fixture.capture`(ロジック): ワンショット、リピート(実際のレートで時計どおりに区画ができ、リング、release)、
89
+ `fake_capture` は `oep.fixture.logic`(ロジック): ワンショット、リピート(実際のレートで時計どおりに区画ができ、リング、release)、
87
90
  ストリーミング(購読している間のデータの push)、レベル / エッジのトリガとプリトリガ、出来事。取れるものは決まっている: サンプル i は
88
91
  カウンタの値 i で、チャネル k はそのビット k(周期 2^(k+1) サンプルの方形波)。layout は profile が許す形(`p4-x035`: P4 の
89
92
  PARLIO と同じ w 1〜16、3 本なら w 4。`esp32-v003`: classic ESP32 の sampler と同じ w 8、ワンショットだけ)。capture は聞くだけ
@@ -27,6 +27,10 @@ under Unreleased in CHANGELOG.md, (EN) and (JA).
27
27
 
28
28
  ## Modules (`oep_client`)
29
29
 
30
+ The modules below are the public API; import them directly (`from oep_client import riscv`). Anything not listed, and
31
+ names starting with `_`, may change. A probe firmware and this client go together by version: OpenEmbeddedProbe X.Y.Z with
32
+ oep-client-python X.Y.Z (until the v1 freeze every release may break the wire; the versions move together).
33
+
30
34
  | Module | Contents |
31
35
  |---|---|
32
36
  | `host` | requests and results, the session id and the lock, `call()` (raises unless it worked), pipelining, the errors (`OepError` / `Rejected` / `Failed`) |
@@ -34,10 +38,9 @@ under Unreleased in CHANGELOG.md, (EN) and (JA).
34
38
  | `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
39
  | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`, `oep.target.riscv-dm`, finding the reset line, attach through GPIO |
36
40
  | `console` | `oep.target.console` (position streams) and `ConsoleIO`, read as bytes |
37
- | `fixture` | `oep.fixture.gpio` / `uart` (revision 1) |
41
+ | `fixture` | `oep.fixture.gpio` / `uart` / `i2c-target` / `spi-target` (revision 1) |
38
42
  | `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) |
43
+ | `capture` | `oep.fixture.logic` (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) |
41
44
  | `decode` | decoding capture channels (I2C) |
42
45
  | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec) |
43
46
  | `arm` | `oep.wire.swd`, `oep.target.arm-adi`, MEM-AP, halting and calling functions on a Cortex-M |
@@ -46,7 +49,6 @@ under Unreleased in CHANGELOG.md, (EN) and (JA).
46
49
  | `uiapduino` | into and out of the UIAPduino bootloader |
47
50
  | `catalog` / `names` / `interfaces` / `dump` | the capability list and describe shapes, display |
48
51
  | `fake` / `endpoint` / `fake_serial` / `fake_serve` | the fake probe (below) |
49
- | `target` | one place to import the main ones from |
50
52
 
51
53
  ## Example
52
54
 
@@ -54,7 +56,8 @@ under Unreleased in CHANGELOG.md, (EN) and (JA).
54
56
  from oep_client import core, link, riscv, ch32_flash
55
57
 
56
58
  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)
59
+ # a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb:<unit id>" (the probe whose USB serial it is),
60
+ # "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
58
61
  core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
59
62
  wire = riscv.Wire(hst, "oep.wire.rvswd")
60
63
  conn, _ = wire.attach(halt=True)
@@ -89,7 +92,7 @@ against it (when the spec changes, this is brought in line before the firmware).
89
92
  chooses), `fake_serial` the byte side of a serial port (COBS candidates, raw bytes and binds, held during a session and
90
93
  resumed after it).
91
94
 
92
- `fake_capture` is `oep.fixture.capture` (logic): one-shot, repeat (segments with the clock at the actual rate, a ring,
95
+ `fake_capture` is `oep.fixture.logic` (logic): one-shot, repeat (segments with the clock at the actual rate, a ring,
93
96
  release) and streaming (data pushes while subscribed), level / edge triggers with a pretrigger, and events. What it
94
97
  captures is known: sample i is the counter i, channel k its bit k (a square wave of period 2^(k+1) samples), in the layout
95
98
  the profile allows (`p4-x035`: w 1-16 as the P4's PARLIO, three channels in w 4; `esp32-v003`: w 8 as the classic ESP32's
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oep-client-python"
7
- version = "0.0.18"
7
+ version = "0.0.20"
8
8
  description = "Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.18"
5
+ __version__ = "0.0.20"
@@ -186,12 +186,14 @@ def _config(args) -> int:
186
186
  if args.lock:
187
187
  mask, _, value = args.lock.partition(":")
188
188
  lock = (1, struct.pack("<I", int(mask, 16)), struct.pack("<I", int(value, 16)))
189
- it = config.Slot(args.slot, fn, _pins(hst, fn, args.pins, args.wire), args.name, args.attach, args.retry,
190
- args.mechanism, lock, args.max_speed, args.idle_clock)
189
+ it = config.Slot(slot=args.slot, wire_fn=fn, pins=_pins(hst, fn, args.pins, args.wire), name=args.name,
190
+ attach=args.attach, retry_s=args.retry, mechanism=args.mechanism, lock=lock,
191
+ max_speed=args.max_speed, idle_clock=args.idle_clock)
191
192
  _change(hst, cfg, [it], args.save)
192
193
  elif args.action == "bind":
193
194
  slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
194
- it = config.Bind(args.port, args.mode, [_stream(s, slots) for s in args.stream], args.select)
195
+ it = config.Bind(port=args.port, mode=args.mode, streams=[_stream(s, slots) for s in args.stream],
196
+ selected=args.select)
195
197
  _change(hst, cfg, [it], args.save)
196
198
  elif args.action == "plan":
197
199
  fn, roles = _plan_fn(hst, args.fn)
@@ -203,12 +205,12 @@ def _config(args) -> int:
203
205
  number = int(role) if role.isdigit() else roles.get(role.lower())
204
206
  if number is None:
205
207
  raise SystemExit(f"{role}: not a role of fn {fn} (roles: {', '.join(sorted(roles)) or 'numbers only'})")
206
- items.append(config.Plan(fn, number, int(ch, 0)))
208
+ items.append(config.Plan(fn=fn, role=number, channel=int(ch, 0)))
207
209
  _change(hst, cfg, items, args.save)
208
210
  elif args.action == "label":
209
- _change(hst, cfg, [config.Label(args.channel, args.text)], args.save)
211
+ _change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
210
212
  elif args.action == "idle":
211
- _change(hst, cfg, [config.Idle(args.channel, args.mode)], args.save)
213
+ _change(hst, cfg, [config.Idle(channel=args.channel, mode=args.mode)], args.save)
212
214
  elif args.action == "remove":
213
215
  _change(hst, cfg, [config.remove(args.kind, args.key)], args.save)
214
216
  else:
@@ -1,4 +1,4 @@
1
- """oep.fixture.capture / oep.fixture.analog / oep.fixture.capture-group revision 1 (oep-spec docs/oep-if-capture.ja.md:
1
+ """oep.fixture.logic / oep.fixture.analog / oep.fixture.capture-group revision 1 (oep-spec docs/oep-if-capture.ja.md:
2
2
  §1 layouts, §2 segments, §3 operations, §3.8 calibration, §4 groups). Numbers from `registry`.
3
3
 
4
4
  Times are the probe's one clock (ns since its boot, comparable within one boot_id): estimates with an uncertainty, the
@@ -20,7 +20,7 @@ from . import cobs, host as h, message as m, registry as reg
20
20
  from .frames import FramingLost
21
21
  from .core import Interface, confirm
22
22
 
23
- _CAP = reg.FIXTURE_CAPTURE
23
+ _CAP = reg.FIXTURE_LOGIC
24
24
  _ANA = reg.FIXTURE_ANALOG
25
25
  _GRP = reg.FIXTURE_CAPTURE_GROUP
26
26
  # configure TLVs; bit 7 of a tag = critical (the probe must reject what it cannot do)
@@ -158,7 +158,7 @@ def _config(payload: bytes, analog: bool) -> Config:
158
158
  elif tag == RATE_ACCURACY:
159
159
  c.rate_measured, c.rate_ppm = v[0] == 1, struct.unpack_from("<I", v, 1)[0]
160
160
  elif tag == SCALE and analog:
161
- role, zero, scale = struct.unpack("<BII", v)
161
+ role, zero, scale = struct.unpack("<Bii", v) # signed: an inverting frontend (§3.3)
162
162
  c.zero[role], c.scale_nv[role] = zero, scale
163
163
  elif tag == SKEW and analog:
164
164
  role, ns = struct.unpack("<BI", v)
@@ -177,7 +177,7 @@ def _config(payload: bytes, analog: bool) -> Config:
177
177
 
178
178
  class LogicCapture(Interface):
179
179
  """Basic logic capture. Channels are the plan's roles 0..C-1."""
180
- NAME = "oep.fixture.capture"
180
+ NAME = "oep.fixture.logic"
181
181
  REVISION = 1
182
182
  ANALOG = False
183
183
  CONFIGURE, START, STOP, FORCE, STATUS, READ, SEGMENTS, RELEASE, QUERY_OP = (
@@ -301,7 +301,7 @@ class LogicCapture(Interface):
301
301
 
302
302
  def segments(self, from_serial: int = 0) -> list[Segment]:
303
303
  rd = m.Reader(self._call(self.SEGMENTS, struct.pack("<I", from_serial), locked=False).payload)
304
- out = [Segment.unpack(rd.bytes(SEGMENT_BYTES)) for _ in range(rd.u8())]
304
+ out = [Segment.unpack(rd.element().bytes(SEGMENT_BYTES)) for _ in range(rd.u8())]
305
305
  rd.tail()
306
306
  return out
307
307
 
@@ -1,7 +1,7 @@
1
1
  """Draft wire forms for capability discovery by name (oep-spec docs/capability-declaration-model.ja.md).
2
2
 
3
3
  list request : flags(u8) first(u16) prefix_len(u8) prefix flags bit0 = exact
4
- list result : total(u16) count(u8) entries [TLV tail] oep.core (fn 0) is the first entry
4
+ list result : total(u16) count(u8) count x (len(u8) entry) [TLV tail] oep.core (fn 0) is the first entry
5
5
  list entry : fn(u16) instance(u16) revision(u8) flags(u8) name_len(u8) name
6
6
  describe : request fn(u16) first(u16); result more(u8) then TLV bytes (tag u8, len u8, value;
7
7
  tag bit 7 = critical). more = 1: TLVs remain after this page, ask again from first + count
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
  import struct
16
16
  from dataclasses import dataclass, field
17
17
 
18
- from .message import Reader, split_tlvs
18
+ from .message import Reader, element, split_tlvs
19
19
 
20
20
  LIST_EXACT = 0x01
21
21
 
@@ -65,7 +65,7 @@ def pack_entry(e: ListEntry) -> bytes:
65
65
 
66
66
 
67
67
  def pack_list_result(total: int, entries: list[ListEntry]) -> bytes:
68
- return struct.pack("<HB", total, len(entries)) + b"".join(pack_entry(e) for e in entries)
68
+ return struct.pack("<HB", total, len(entries)) + b"".join(element(pack_entry(e)) for e in entries)
69
69
 
70
70
 
71
71
  def unpack_list_result(payload: bytes) -> tuple[int, list[ListEntry]]:
@@ -74,8 +74,9 @@ def unpack_list_result(payload: bytes) -> tuple[int, list[ListEntry]]:
74
74
  total, count = rd.take("HB")
75
75
  out = []
76
76
  for _ in range(count):
77
- fn, instance, revision, flags, n = rd.take("HHBBB")
78
- out.append(ListEntry(fn, instance, revision, flags, rd.bytes(n).decode("ascii", "replace")))
77
+ e = rd.element()
78
+ fn, instance, revision, flags, n = e.take("HHBBB")
79
+ out.append(ListEntry(fn, instance, revision, flags, e.bytes(n).decode("ascii", "replace")))
79
80
  rd.tail()
80
81
  return total, out
81
82
 
@@ -17,7 +17,7 @@ import struct
17
17
  import time
18
18
  from dataclasses import dataclass, field
19
19
 
20
- from . import host as h, riscv, target
20
+ from . import core, host as h, riscv
21
21
 
22
22
  # oep-spec experiments/flash-primitives/x035_loader.S, assembled at 0x20000000 (riscv32-esp-elf binutils)
23
23
  FAST_PAGE_LOADER = bytes.fromhex(
@@ -92,13 +92,13 @@ V003_INPUT, V003_STACK, V003_EBREAK, V003_RUN = 0x20000200, 0x20000800, 0x200001
92
92
 
93
93
 
94
94
  def _block_size(hst: h.Host) -> int:
95
- info = target.confirm(hst)
95
+ info = core.confirm(hst)
96
96
  # request header 6 + session 4 + connection 1 + address 4 + count 2 (the read answer's 5 + done 2 + status 1 is
97
97
  # smaller); a little slack for the result header
98
98
  return (info["max_frame"] - 5 - 6 - 4 - 1 - 4 - 2) // 4 * 4
99
99
 
100
100
 
101
- def _write_requests(dm: target.RiscvDm, address: int, data: bytes, block: int) -> list[tuple[int, int, bytes]]:
101
+ def _write_requests(dm: riscv.RiscvDm, address: int, data: bytes, block: int) -> list[tuple[int, int, bytes]]:
102
102
  return [dm.request(dm.WRITE_BLOCK, dm.write_block_body(address + off, data[off:off + block]))
103
103
  for off in range(0, len(data), block)]
104
104
 
@@ -108,20 +108,20 @@ def _write_ok(r) -> bool:
108
108
  if not r.succeeded:
109
109
  return False
110
110
  try:
111
- rd = target.ran(r)
111
+ rd = riscv.ran(r)
112
112
  rd.u16()
113
113
  return rd.u8() == riscv.OK
114
114
  except h.OepError:
115
115
  return False
116
116
 
117
117
 
118
- def _write(dm: target.RiscvDm, address: int, data: bytes, block: int) -> None:
118
+ def _write(dm: riscv.RiscvDm, address: int, data: bytes, block: int) -> None:
119
119
  for r in dm.host.pipeline_calls(_write_requests(dm, address, data, block)):
120
120
  if not _write_ok(r):
121
- raise riscv.TargetError("write_block", target.ran(r).take("HB")[1], r)
121
+ raise riscv.TargetError("write_block", riscv.ran(r).take("HB")[1], r)
122
122
 
123
123
 
124
- def _read(dm: target.RiscvDm, address: int, length: int, block: int) -> bytes:
124
+ def _read(dm: riscv.RiscvDm, address: int, length: int, block: int) -> bytes:
125
125
  """Read back in blocks, pipelined when the host has the link's exchange (reads are independent)."""
126
126
  spans = [(address + off, min(block, length - off) // 4) for off in range(0, length, block)]
127
127
  out = b""
@@ -137,7 +137,7 @@ def _read(dm: target.RiscvDm, address: int, length: int, block: int) -> bytes:
137
137
  PIPELINE_PAGES = 16 # pages per pipelined batch: enough to keep the link busy, small enough to show progress
138
138
 
139
139
 
140
- def program(hst: h.Host, dm: target.RiscvDm, image: bytes, profile: FlashProfile) -> ProgramResult:
140
+ def program(hst: h.Host, dm: riscv.RiscvDm, image: bytes, profile: FlashProfile) -> ProgramResult:
141
141
  """Program `image` at profile.base on a halted, attached target (dm), verify by reading back."""
142
142
  image = image + b"\xff" * (-len(image) % profile.page)
143
143
  if profile.size and len(image) > profile.size:
@@ -2,8 +2,8 @@
2
2
  pins, slots, binds - read and set as items, saved when the host says so, and the live slot / bind state.
3
3
 
4
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)])])
5
+ cfg.set([config.Slot(slot=0, wire_fn=wire_fn, pins=(2, 54), name="x035", attach="at-boot", retry_s=1),
6
+ config.Bind(port=1, mode="last-reset", streams=[("slot", 0)])])
7
7
  cfg.save()
8
8
  cfg.items(), cfg.state()
9
9
 
@@ -31,13 +31,14 @@ BIND_FLOW = {v: k for k, v in _CFG.enum["bind_flow"].items()}
31
31
  IDLE = {k.replace("_", "-"): v for k, v in _CFG.enum["idle_mode"].items()}
32
32
  IDLE_CLOCK = dict(reg.WIRE_RVSWD.enum["idle_clock"]) # a slot's idle_clock (oep-if-debug §3)
33
33
  STORAGE_STATE = {v: k for k, v in _CFG.enum["storage_state"].items()}
34
+ UNREADABLE = {1: "unreadable form", 2: "an interface it names is gone or of another revision", 3: "refused when applied"}
34
35
 
35
36
 
36
37
  def _name(table: dict[str, int], value: int) -> str:
37
38
  return next((k for k, v in table.items() if v == value), str(value))
38
39
 
39
40
 
40
- @dataclass
41
+ @dataclass(kw_only=True)
41
42
  class Plan:
42
43
  fn: int
43
44
  role: int
@@ -48,7 +49,7 @@ class Plan:
48
49
  return struct.pack("<HBH", self.fn, self.role, self.channel)
49
50
 
50
51
 
51
- @dataclass
52
+ @dataclass(kw_only=True)
52
53
  class Label:
53
54
  channel: int
54
55
  text: str
@@ -58,7 +59,7 @@ class Label:
58
59
  return struct.pack("<H", self.channel) + self.text.encode()
59
60
 
60
61
 
61
- @dataclass
62
+ @dataclass(kw_only=True)
62
63
  class Idle:
63
64
  channel: int
64
65
  mode: str = "pull-up" # hi-z, pull-up, pull-down
@@ -68,7 +69,7 @@ class Idle:
68
69
  return struct.pack("<HB", self.channel, IDLE[self.mode])
69
70
 
70
71
 
71
- @dataclass
72
+ @dataclass(kw_only=True)
72
73
  class Slot:
73
74
  """A place a target is wired to (probe.config §1.1). pins: (swdio, swclk), swclk 0xFFFF on one wire (swio).
74
75
  lock: (scheme, mask, value) - the target_id a connection must show, e.g. (1, mask u32 LE, value u32 LE)."""
@@ -90,14 +91,14 @@ class Slot:
90
91
  self.retry_s if self.attach == "at-boot" else 0, self.max_speed, IDLE_CLOCK[self.idle_clock],
91
92
  MECHANISM[self.mechanism], len(name)) + name
92
93
  if self.lock is None:
93
- return v + b"\x00"
94
+ return v + b"\x00" # lock_len 0: no lock
94
95
  scheme, mask, value = self.lock
95
- if len(mask) != len(value) or not mask:
96
- raise ValueError("a lock's mask and value have the same length, at least 1 byte")
97
- return v + bytes([scheme]) + mask + value
96
+ if len(mask) != len(value) or not mask or not scheme:
97
+ raise ValueError("a lock has a scheme and a mask and value of the same length, at least 1 byte")
98
+ return v + bytes([1 + 2 * len(mask), scheme]) + mask + value
98
99
 
99
100
 
100
- @dataclass
101
+ @dataclass(kw_only=True)
101
102
  class Bind:
102
103
  """What serial port `port` (the describe transport index) carries (probe.config §1.2). streams: ("slot", n) or
103
104
  ("uart", fn); selected: manual's choice (an index into streams)."""
@@ -126,25 +127,28 @@ def remove(kind: str, key: int) -> bytes:
126
127
  def decode(tag: int, v: bytes):
127
128
  """One item as one of the classes above (an unknown tag: (tag, value))."""
128
129
  if tag == ITEM["plan"] and len(v) == 5:
129
- return Plan(*struct.unpack("<HBH", v))
130
+ fn, role, channel = struct.unpack("<HBH", v)
131
+ return Plan(fn=fn, role=role, channel=channel)
130
132
  if tag == ITEM["label"] and len(v) >= 2:
131
- return Label(struct.unpack_from("<H", v)[0], v[2:].decode("utf-8", "replace"))
133
+ return Label(channel=struct.unpack_from("<H", v)[0], text=v[2:].decode("utf-8", "replace"))
132
134
  if tag == ITEM["idle"] and len(v) == 3:
133
- return Idle(struct.unpack_from("<H", v)[0], _name(IDLE, v[2]))
135
+ return Idle(channel=struct.unpack_from("<H", v)[0], mode=_name(IDLE, v[2]))
134
136
  if tag == ITEM["slot"] and len(v) >= 18:
135
137
  n, wire_fn, swdio, swclk, attach, retry_s, max_speed, idle, mech, name_len = struct.unpack_from("<BHHHBHIBBB", v)
136
138
  name = v[17:17 + name_len].decode("ascii", "replace")
137
- rest = v[17 + name_len:]
139
+ at = 17 + name_len
140
+ lock_len = v[at] if at < len(v) else 0
141
+ part = v[at + 1:at + 1 + lock_len] # after it: later fields (core §2.3), skipped
138
142
  lock = None
139
- if rest and rest[0]:
140
- half = (len(rest) - 1) // 2
141
- lock = (rest[0], rest[1:1 + half], rest[1 + half:])
142
- return Slot(n, wire_fn, (swdio, swclk), name, _name(ATTACH, attach), retry_s, _name(MECHANISM, mech), lock,
143
- max_speed, _name(IDLE_CLOCK, idle))
143
+ if lock_len >= 3:
144
+ half = (lock_len - 1) // 2
145
+ lock = (part[0], part[1:1 + half], part[1 + half:])
146
+ return Slot(slot=n, wire_fn=wire_fn, pins=(swdio, swclk), name=name, attach=_name(ATTACH, attach), retry_s=retry_s,
147
+ mechanism=_name(MECHANISM, mech), lock=lock, max_speed=max_speed, idle_clock=_name(IDLE_CLOCK, idle))
144
148
  if tag == ITEM["bind"] and len(v) >= 4:
145
149
  port, mode, selected, n = struct.unpack_from("<BBBB", v)
146
150
  streams = [(_name(STREAM, v[4 + 3 * k]), struct.unpack_from("<H", v, 5 + 3 * k)[0]) for k in range(n)]
147
- return Bind(port, _name(MODE, mode), streams, selected)
151
+ return Bind(port=port, mode=_name(MODE, mode), streams=streams, selected=selected)
148
152
  return (tag, v)
149
153
 
150
154
 
@@ -170,6 +174,7 @@ class State:
170
174
  storage_bytes: int = 0
171
175
  storage: str = "none" # none, applied, unreadable
172
176
  saved_hash: int = 0
177
+ unreadable: str | None = None # why, when unreadable (probe.config §4)
173
178
  items: list[int] = field(default_factory=list)
174
179
  slots_max: int = 0
175
180
  bind_modes: list[str] = field(default_factory=list)
@@ -216,6 +221,8 @@ class ProbeConfig(Interface):
216
221
  if tag == DESCRIBE["storage"] and len(v) >= 9:
217
222
  st.storage_bytes, state, st.saved_hash = struct.unpack_from("<IBI", v)
218
223
  st.storage = STORAGE_STATE.get(state, str(state))
224
+ if len(v) >= 14 and v[13]:
225
+ st.unreadable = UNREADABLE.get(v[13], str(v[13]))
219
226
  elif tag == DESCRIBE["items"]:
220
227
  st.items = list(v)
221
228
  elif tag == DESCRIBE["slots_max"] and v:
@@ -65,7 +65,7 @@ class PositionStream(Interface):
65
65
  """One answer's marks with serial >= from_serial (in serial order). -> (marks, more)."""
66
66
  rd = m.Reader(self._call(self.MARKS, self._stream_prefix() + struct.pack("<I", from_serial), locked=False).payload)
67
67
  more, count = rd.take("BB")
68
- marks = [Mark(*rd.take("IQBIB")) for _ in range(count)]
68
+ marks = [Mark(*rd.element().take("IQBIB")) for _ in range(count)]
69
69
  rd.tail()
70
70
  return marks, bool(more)
71
71
 
@@ -136,10 +136,12 @@ class StreamIO:
136
136
  self.lost = 0
137
137
 
138
138
  def _limits(self) -> tuple[int, int]:
139
- """(read, write) chunk sizes that fit the probe's frame: request header 6 + session 4 + stream 1 + count 2,
140
- result header 5 + start 8 + flags 1."""
139
+ """(read, write) chunk sizes that fit the probe's frame: a write is request header 6 + session 4 + the stream's
140
+ prefix (the console's stream u16, none on a fixture UART) + count 2; a read's result is header 5 + start 8 +
141
+ flags 1. (The write was frame - 13 for both: one byte over on the console's 64-byte frames.)"""
141
142
  frame = self.source.host.confirmed()["max_frame"]
142
- return max(1, min(self.MAX_READ, frame - 14)), max(1, min(self.MAX_WRITE, frame - 13))
143
+ write = frame - 12 - len(self.source._stream_prefix())
144
+ return max(1, min(self.MAX_READ, frame - 14)), max(1, min(self.MAX_WRITE, write))
143
145
 
144
146
  def read(self, n: int = 512) -> bytes:
145
147
  c = self.source.read_from(self.position, min(n, self._limits()[0]))
@@ -1,4 +1,4 @@
1
- """Host-side decoders for capture channels (oep.fixture.capture: `LogicCapture.channel(data, k)` gives one value
1
+ """Host-side decoders for capture channels (oep.fixture.logic: `LogicCapture.channel(data, k)` gives one value
2
2
  per sample)."""
3
3
 
4
4
  from __future__ import annotations