oep-client-python 0.0.20__tar.gz → 0.0.22__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 (54) hide show
  1. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/CHANGELOG.md +30 -0
  2. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/PKG-INFO +17 -13
  3. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/README.ja.md +15 -12
  4. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/README.md +16 -12
  5. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/pyproject.toml +1 -1
  6. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/__init__.py +1 -1
  7. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/__main__.py +76 -28
  8. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/arm.py +11 -9
  9. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/capture.py +157 -49
  10. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/catalog.py +16 -9
  11. oep_client_python-0.0.22/src/oep_client/config.py +379 -0
  12. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/console.py +68 -18
  13. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/core.py +21 -3
  14. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/endpoint.py +685 -255
  15. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/fake.py +69 -51
  16. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/fake_capture.py +107 -51
  17. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/fake_serve.py +4 -4
  18. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/fixture.py +56 -16
  19. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/host.py +88 -29
  20. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/interfaces.py +14 -8
  21. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/link.py +5 -4
  22. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/message.py +49 -17
  23. oep_client_python-0.0.22/src/oep_client/registry.py +73 -0
  24. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/riscv.py +143 -56
  25. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/usb_stream.py +10 -4
  26. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_capabilities.py +3 -3
  27. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_capture.py +25 -7
  28. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_config.py +110 -6
  29. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_fake_capture.py +37 -7
  30. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_fake_spec.py +177 -47
  31. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_fixture_targets.py +9 -6
  32. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_flash_console.py +4 -4
  33. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_interfaces.py +110 -6
  34. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_link_host.py +2 -1
  35. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_link_serial.py +2 -2
  36. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_session.py +110 -9
  37. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_target_parts.py +35 -20
  38. oep_client_python-0.0.20/src/oep_client/config.py +0 -240
  39. oep_client_python-0.0.20/src/oep_client/registry.py +0 -68
  40. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/.gitignore +0 -0
  41. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/LICENSE +0 -0
  42. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/ch32_flash.py +0 -0
  43. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/cobs.py +0 -0
  44. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/decode.py +0 -0
  45. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/dump.py +0 -0
  46. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/fake_serial.py +0 -0
  47. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/frames.py +0 -0
  48. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/hid_stream.py +0 -0
  49. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/names.py +0 -0
  50. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/rp2350.py +0 -0
  51. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/src/oep_client/uiapduino.py +0 -0
  52. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_cobs.py +0 -0
  53. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_hid_stream.py +0 -0
  54. {oep_client_python-0.0.20 → oep_client_python-0.0.22}/tests/test_usb_stream.py +0 -0
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.22
6
+ - (EN) Breaking: the wire follows oep-spec's zero-base rewrite of 2026-10-01 (docs/v1-zero-base-proposal.ja.md §3 / §7; registry synced). Every answer that ended in data or a list now carries its length and may be followed by TLVs: read answers `start flags len data [TLV]` (console, fixture UART; capture `len` u32), gpio read `n n×level`, dmi `done status nvals values`, arm-adi transfer `done status ack nvals values`, run `status stopped dpc elapsed_us nvals values` (stopped 2 = the hart could not be halted: `RunResult.not_halted`), data frames `position len data [TLV]`, events `kind fixed-part [TLV]`. TLVs have a long form (`tag 0xFF len(u16) value` for 255 bytes and more; `message.tlv` / `split_tlvs`, the long form with a short value is `message.BadTlv`). confirm answers the probe's `boot_id`; subscribe's `max_delay_ms` is u32; `rejected unsupported` is always `tag [TLV]` (0x00 = a fixed-part value: `Unsupported.tag` None, `.tlvs`).
7
+ - (JA) 破壊的変更: wire を oep-spec の 2026-10-01 のゼロベース見直し(docs/v1-zero-base-proposal.ja.md §3 / §7。registry を写し直した)に揃えた。データや並びで終わっていた応答はすべて長さを持ち、後ろに TLV を置ける: read の応答 `start flags len data [TLV]`(console、fixture UART。capture の `len` は u32)、gpio read `n n×level`、dmi `done status nvals values`、arm-adi transfer `done status ack nvals values`、run `status stopped dpc elapsed_us nvals values`(stopped 2 = hart を止められなかった: `RunResult.not_halted`)、データフレーム `position len data [TLV]`、出来事 `kind 固定部 [TLV]`。TLV に長い形(255 byte 以上は `tag 0xFF len(u16) value`。`message.tlv` / `split_tlvs`、短い値の長い形は `message.BadTlv`)。confirm は probe の `boot_id` を返す。subscribe の `max_delay_ms` は u32。`rejected unsupported` は常に `tag [TLV]`(0x00 = 固定部の値: `Unsupported.tag` は None、`.tlvs`)。
8
+ - (EN) Breaking: after the lease lapsed, the same session id's next request is `rejected expired` (0x0E): `host.Expired(Rejected)` with `.lease_ms`, never re-opened silently; `open()` then answers `Opened.resumed == 2` (`.swept`) and `Host.epoch` moves (as it does on `no_session` - what an id forced out by another meets after `locked`). `Wire.connections()` and `Console.streams()` page through `first` / `more`. Subscriptions survive a same-id open while the lock is held; a subscribe to an fn that emits nothing is unsupported, an unsubscribe of nothing is ok.
9
+ - (JA) 破壊的変更: lease が切れた後の同じ session_id の要求は `rejected expired`(0x0E): `host.Expired(Rejected)`(`.lease_ms` つき)。黙って open し直さない。その後の `open()` は `Opened.resumed == 2`(`.swept`)を返し、`Host.epoch` が進む(`no_session` でも進む。force で奪われた側は locked のあと no_session)。`Wire.connections()` と `Console.streams()` は `first` / `more` でページを辿る。保持中の同じ ID の open で購読は残る。送り出さない fn の subscribe は unsupported、購読の無い unsubscribe は ok。
10
+ - (EN) Breaking: `oep.probe.config` - describe is declarations only (`ProbeConfig.describe()` -> `Declared`: storage `max_bytes` u32, items, slots_max, bind_modes u32); the live storage / slot / bind state is the lock-free `state` op (0x06, paged; `ProbeConfig.state()` -> `State`, `SlotState.last_try_at_ns`); `unset` (0x05) removes items by key (`ProbeConfig.unset`, `config.remove()` in a `set` list goes as an unset - a key alone no longer removes); the new `uart` item (`config.Uart(fn, baud, format)`) is applied whenever that UART's plan has RX or TX (a session's configure wins until the plan goes); slot fields are `retry_ms` (u32; the Python `Slot.retry_s` stays, seconds) and `max_speed_hz` (u32; `Slot.max_speed` stays, Hz); mechanism `none` (0xFF) = a slot without a console; the hash is the CRC-32 of the canonical form (`config.canonical` / `hash_of`). CLI: `oep config state`, `oep config uart`, `remove` for every item kind.
11
+ - (JA) 破壊的変更: `oep.probe.config` - describe は宣言だけ(`ProbeConfig.describe()` -> `Declared`: storage は `max_bytes` u32、items、slots_max、bind_modes u32)。保存・スロット・bind の今の状態はロック不要の `state` op(0x06、ページング。`ProbeConfig.state()` -> `State`、`SlotState.last_try_at_ns`)。`unset`(0x05)はキーで項目を消す(`ProbeConfig.unset`。`set` の並びの `config.remove()` は unset として送る。キーだけの項目で消す規則は廃止)。新しい `uart` 項目(`config.Uart(fn, baud, format)`)は、その UART の plan に RX / TX が付くたびに掛かる(セッションの configure は plan を解くまで勝つ)。スロットのフィールドは `retry_ms`(u32。Python の `Slot.retry_s` は秒のまま)と `max_speed_hz`(u32。`Slot.max_speed` は Hz のまま)。mechanism `none`(0xFF)= コンソール無しのスロット。hash は正規形の CRC-32(`config.canonical` / `hash_of`)。CLI: `oep config state`、`oep config uart`、`remove` はすべての項目。
12
+ - (EN) Breaking: wires - `attach_under_reset` (op 0x04) is gone: `Wire.attach(reset=(channel, hold_ms))` sends the attach's critical TLV 0x05 (the `attach_under_reset()` method stays as a wrapper and returns the dpc from the answer's TLV 0x11); `max_speed` is required by the probe, so `attach()` always sends one (None: the wire's declared `max_clock_hz`, else 1 MHz); the answer's flags are the common `attach_flags` (`Wire.halted` / `.dpc`; swd's dormant bit moved to bit2); scan takes `max_speed` / `idle_clock` TLVs and its skip is TLV 0x02; `WireBase.connections()` and `detach(force=)`. `RiscvDm.run(timeout_ms=None)` is the probe's `max_op_ms` (`core.max_op_ms`), no longer 0xFFFFFFFF.
13
+ - (JA) 破壊的変更: 線 - `attach_under_reset`(op 0x04)は無くなり、`Wire.attach(reset=(channel, hold_ms))` が attach の critical TLV 0x05 を送る(`attach_under_reset()` メソッドは包みとして残り、応答の TLV 0x11 の dpc を返す)。probe は `max_speed` を必須にしたので `attach()` は常に送る(None: その線の宣言の `max_clock_hz`、無ければ 1 MHz)。応答の flags は共通の `attach_flags`(`Wire.halted` / `.dpc`。swd の dormant は bit2 に移った)。scan は `max_speed` / `idle_clock` の TLV を取り、skip は TLV 0x02。`WireBase.connections()` と `detach(force=)`。`RiscvDm.run(timeout_ms=None)` は probe の `max_op_ms`(`core.max_op_ms`)で、0xFFFFFFFF ではなくなった。
14
+ - (EN) Breaking: console - marks carry `time_ns` (u64, the probe's one clock; `Mark.time_ns`, 22 bytes), a stream lives while anything uses it (the sessions that opened it, a bound slot; `close` takes one share; a lapse takes the session's), one live stream per connection (another mechanism: unavailable cause 6), a closed stream of the same place and mechanism comes back under its old number, mark `closed` (0x09) with why, the lock-free `streams` op (`Console.streams()` -> `StreamInfo`), a write that fits nothing is completed failed. Connections and streams share one u16 number space (wrapping); a number of the other kind is unavailable cause 6. `registry.COMMON.enum` holds `read_from`, `read_flags`, `mark_kind` and the mark details (they left the console's enum).
15
+ - (JA) 破壊的変更: console - マークは `time_ns`(u64、probe の 1 本の時計。`Mark.time_ns`、22 byte)。ストリームは使っているもの(open したセッション、bind されたスロット)がある間生きる(`close` は自分の分、lease 切れはセッションの分を外す)。1 つの connection に生きているストリームは 1 つ(別の mechanism は unavailable cause 6)。閉じたストリームは同じ場所・同じ mechanism の open で同じ番号で戻る。マーク `closed`(0x09)に理由。ロック不要の `streams` op(`Console.streams()` -> `StreamInfo`)。何も入らなかった write は completed failed。connection とストリームは 1 つの u16 の番号空間(一周する)で、別の種類の番号は unavailable cause 6。`registry.COMMON.enum` に `read_from`、`read_flags`、`mark_kind` とマークの detail(console の enum から移った)。
16
+ - (EN) Breaking: capture - every start is a generation (u32): start answers `blocking_ms generation`, status `state serial_done write_pos flags generation [TLV error]` (`LogicCapture.status()` -> `Status`, still iterable as the old 4-tuple), segment records are 37 bytes (`Segment.generation`), read is `generation position max`, release `generation serial` (another generation: unavailable cause 6), streaming data frames carry TLV generation (`stream()` drops another generation's, `Received.stale`); `LogicCapture.generation` / `read(..., generation=)`; `read_segment(segment)` passes the segment's own. Trigger values are u32; segments answers have `more`; state 5 goes on by itself after release (no stopped 2 event); describe `channels` layouts / `trigger` types are u32, `rate_list` / group `tracks` / `budget` carry `n`; `Calibration.vrefint_nominal_mv`, factory data has `raw_len`; the group's start answers each track's generation (`CaptureGroup.generations`), its events are triggered 0x03 / stopped 0x02.
17
+ - (JA) 破壊的変更: capture - start ごとに世代(u32): start の応答 `blocking_ms generation`、status `state serial_done write_pos flags generation [TLV error]`(`LogicCapture.status()` -> `Status`。前の 4 つ組としても展開できる)、区画の情報は 37 byte(`Segment.generation`)、read は `generation position max`、release は `generation serial`(違う世代は unavailable cause 6)、ストリーミングのデータフレームは TLV generation を持つ(`stream()` は別の世代を捨てる。`Received.stale`)。`LogicCapture.generation` / `read(..., generation=)`。`read_segment(segment)` は区画の世代を付ける。trigger の value は u32。segments の応答に `more`。state 5 は release で自動で再開(stopped 2 の出来事は無い)。describe の `channels` の layout / `trigger` の types は u32、`rate_list` / 組の `tracks` / `budget` は `n` を持つ。`Calibration.vrefint_nominal_mv`。factory に `raw_len`。組の start は各トラックの世代を返す(`CaptureGroup.generations`)、組の出来事は triggered 0x03 / stopped 0x02。
18
+ - (EN) Breaking: fixture - gpio read answers `n n×level`, its describe `modes` is u32 (a mode the probe lacks is unsupported with channel / index TLVs; mode 8+ malformed); the UART's stream is made by the plan (RX or TX) and goes with it, its position never going back within a boot; `FixtureUart.status()` (op 0x07) -> `UartStatus` (`configured` is a name: default / session / item / item_fallback, registry `uart_configured`); an undefined format bit is malformed, an undeclared format unsupported, a baud more than 5 % off unsupported; i2c / spi `errors` are u32, `read_rx` reads its TLV tail (`last_ns`), describe `queue_depth`.
19
+ - (JA) 破壊的変更: fixture - gpio read の応答は `n n×level`、describe の `modes` は u32(持たない mode は channel / index の TLV つきの unsupported。mode 8 以上は malformed)。UART のストリームは plan(RX か TX)が作り、plan と一緒に消える。位置は起動の中で戻らない。`FixtureUart.status()`(op 0x07)-> `UartStatus`(`configured` は名前: default / session / item / item_fallback、registry の `uart_configured`)。format の未定義のビットは malformed、宣言に無い format は unsupported、±5% を超える baud は unsupported。i2c / spi の `errors` は u32、`read_rx` は TLV の後ろを読む(`last_ns`)、describe に `queue_depth`。
20
+ - (EN) The fake probe follows all of it (the working spec): `boot_id` in confirm, `expired` and `resumed` 2, one resource number space, the heartbeat `boot_id uptime_ns`, the plan's `plan_roles` limit and reason table, attach's reset TLV / required max_speed / scan TLVs, dmi / run answers with nvals (run's `FakeTarget.unstoppable` for stopped 2), the console's stream lifetime and `streams`, gpio modes, the UART's plan-made stream / `status` / formats, probe.config's `state` / `unset` / `uart` item / canonical hash, capture generations and the group's generations TLV. Its describes declare `max_op_ms` 10000, `plan_roles`, `discoverable` (was `oep_pid`), gpio `modes`, UART `formats`. USB: the vendor interface is told by class 0xFF, subclass 0x4F, protocol 0x45 and a probe by its iProduct `OEP` alone (`registry.USB`).
21
+ - (JA) fake の probe もすべてに追随した(動く spec): confirm の `boot_id`、`expired` と `resumed` 2、資源番号の 1 つの空間、ハートビート `boot_id uptime_ns`、plan の `plan_roles` の上限と断り方の表、attach の reset TLV / 必須の max_speed / scan の TLV、nvals を持つ dmi / run の応答(stopped 2 は `FakeTarget.unstoppable`)、console のストリームの寿命と `streams`、gpio の modes、UART の plan が作るストリーム / `status` / formats、probe.config の `state` / `unset` / `uart` 項目 / 正規形の hash、capture の世代と組の世代の TLV。describe は `max_op_ms` 10000、`plan_roles`、`discoverable`(`oep_pid` から改名)、gpio の `modes`、UART の `formats` を宣言する。USB: vendor の interface は class 0xFF・subclass 0x4F・protocol 0x45 で、probe は iProduct `OEP` だけで見分ける(`registry.USB`)。
22
+
23
+ ## 0.0.21
24
+ - (EN) Breaking: a bind's streams carry a length each (oep-spec probe.config §1.2, freeze decision 1, missed until now): `n × (len, kind, id)`, len 3 today; the fake skips a longer one's tail and refuses one under 3 as malformed.
25
+ - (JA) 破壊的変更: bind のストリームの並びの各要素の前に長さを置く(oep-spec probe.config §1.2。凍結の決定 1 で漏れていた): `n × (len、kind、id)`、今の len は 3。fake は長い要素の後ろを飛ばし、3 未満は malformed で断る。
26
+ - (EN) `usb:X` is always a unit id, whatever its length (a 4-character unit id was read as a VID); a VID comes with its PID, `usb:VID:PID[:SERIAL]`.
27
+ - (JA) `usb:X` は長さによらず unit id として読む(4 文字の unit id を VID と読んでいた)。VID は PID と一緒に `usb:VID:PID[:SERIAL]` で書く。
28
+ - (EN) Breaking: the CLI's `name#k` is the list's instance, from 0 (oep-spec core §7.2's `name#instance`; `#0` may be left out). It was the k-th from 1.
29
+ - (JA) 破壊的変更: CLI の `name#k` は list の instance(0 から)にした(oep-spec core §7.2 の `name#instance`、`#0` は省ける)。1 から数えた k 番目だった。
30
+ - (EN) The fake probes number each interface's instance from 0 per name (core §7.2), as the probe firmware now does; they had arbitrary numbers.
31
+ - (JA) fake の probe はインターフェースの instance を名前ごとに 0 から振る(core §7.2)。probe の firmware も同じにした。これまでは適当な番号だった。
32
+ - (EN) README: `registry.INTERFACES[name]` is the public way to reach an interface's numbers by name.
33
+ - (JA) README: 名前からインターフェースの番号を引く公開の入口は `registry.INTERFACES[name]` と書いた。
34
+
5
35
  ## 0.0.20
6
36
  - (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
37
  - (JA) console の書き込みの 1 回の大きさが probe のフレームに収まるようにした: frame − 12 − stream の前置き(console は stream の u16、fixture UART は無し)。両方とも frame − 13 だったので、64 byte のフレームの console で 1 byte はみ出していた。oep-client-js に移すときに見つかった。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.20
3
+ Version: 0.0.22
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
@@ -55,16 +55,16 @@ oep-client-python X.Y.Z (until the v1 freeze every release may break the wire; t
55
55
 
56
56
  | Module | Contents |
57
57
  |---|---|
58
- | `host` | requests and results, the session id and the lock, `call()` (raises unless it worked), pipelining, the errors (`OepError` / `Rejected` / `Failed`) |
58
+ | `host` | requests and results, the session id and the lock, `call()` (raises unless it worked), pipelining, the errors (`OepError` / `Rejected` / `Failed`; `Expired` when the lease lapsed - the session is never re-opened behind the caller's back, `Host.epoch` moves) |
59
59
  | `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)` |
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 |
61
- | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`, `oep.target.riscv-dm`, finding the reset line, attach through GPIO |
62
- | `console` | `oep.target.console` (position streams) and `ConsoleIO`, read as bytes |
63
- | `fixture` | `oep.fixture.gpio` / `uart` / `i2c-target` / `spi-target` (revision 1) |
64
- | `config` | `oep.probe.config` (slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) |
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) |
60
+ | `core` | interfaces by name (cached), confirm (with the probe's `boot_id`), the probe's describe (declarations only, cached per boot: labels, the transport list, `max_op_ms`), taking the lock (`take`), the pin plan, the `Interface` base |
61
+ | `riscv` | `oep.wire.rvswd` / `oep.wire.swio` (scan, attach - `max_speed` always sent, `reset=(channel, hold_ms)` for an attach under reset -, detach, connections), `oep.target.riscv-dm` (answers count their values; `RunResult.not_halted`), finding the reset line, attach through GPIO |
62
+ | `console` | `oep.target.console` (position streams: read answers carry their length, marks are `time_ns`, the lock-free `streams()` list) and `ConsoleIO`, read as bytes |
63
+ | `fixture` | `oep.fixture.gpio` / `uart` (its stream is the plan's; `status()`) / `i2c-target` / `spi-target` (revision 1) |
64
+ | `config` | `oep.probe.config` (slots, binds, plan / label / idle / uart items, get / set / unset / save / erase; `describe()` = the declarations, `state()` = the live storage / slot / bind state; `hash_of(items)` = the probe's hash) |
65
+ | `capture` | `oep.fixture.logic` / `analog` / `capture-group` (revision 1, oep-spec oep-if-capture). Every start is a generation (`LogicCapture.generation`) that read and release name - `read_segment(segment)` does it by itself; `status()` returns a `Status`. Every segment read goes to the `Host.on_capture` callbacks as a `CaptureRecord` (the hook for run recorders; no wireskein dependency) |
66
66
  | `decode` | decoding capture channels (I2C) |
67
- | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec) |
67
+ | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec). The public way to reach an interface by name is `registry.INTERFACES[name]` (`.revision`, `.op`, `.tlv`, `.enum`, e.g. `INTERFACES["oep.fixture.uart"].enum["role"]`); the module-level names (`FIXTURE_UART`, ...) are the same objects |
68
68
  | `arm` | `oep.wire.swd`, `oep.target.arm-adi`, MEM-AP, halting and calling functions on a Cortex-M |
69
69
  | `ch32_flash` | writing a CH32 (a RAM loader, page by page) |
70
70
  | `rp2350` | flash and reboot through the RP2350 boot ROM |
@@ -95,10 +95,12 @@ hst.end()
95
95
 
96
96
  ```sh
97
97
  oep dump --port <probe> # what the probe offers (--fake p4-x035: no hardware)
98
- oep config show <probe> # the settings and the live slot / bind state
98
+ oep config show <probe> # the settings, the declarations and the live state
99
+ oep config state <probe> # the live slot / bind / storage state alone (lock-free, for polling)
99
100
  oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
100
101
  oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
101
- oep config save <probe> # kept over a restart (also: remove, erase)
102
+ oep config uart <probe> oep.fixture.uart 115200 --format 8N1 # applied whenever that UART's plan has RX or TX
103
+ oep config save <probe> # kept over a restart (also: remove = unset, erase)
102
104
  ```
103
105
 
104
106
  `<probe>` is a serial port, `tcp://HOST:PORT` or `usb[:VID:PID[:SERIAL]]`. A change takes the lock (owner "oep config") and
@@ -108,8 +110,10 @@ A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`,
108
110
 
109
111
  ## The fake probe (a working spec)
110
112
 
111
- `endpoint.Endpoint` is a fake probe that answers as oep-spec says; ch32rv, this client and the probe firmware are checked
112
- against it (when the spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
113
+ `endpoint.Endpoint` is a fake probe that answers as oep-spec says (2026-10-01: every answer carries the length of its
114
+ data or list, TLVs may follow anything, rejected `expired` after a lapse, one resource number space, describe = declarations
115
+ and `state` for the rest, capture generations); ch32rv, this client and the probe firmware are checked against it (when the
116
+ spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
113
117
  `p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats, `rp2350-pins` = a wire whose pins the host
114
118
  chooses), `fake_serial` the byte side of a serial port (COBS candidates, raw bytes and binds, held during a session and
115
119
  resumed after it).
@@ -29,16 +29,16 @@ X.Y.Z(v1 の凍結までは、どのリリースも wire を壊しうるので
29
29
 
30
30
  | モジュール | 中身 |
31
31
  |---|---|
32
- | `host` | 要求と結果、session_id とロック、`call()`(失敗なら例外)、pipeline、エラーの階層(`OepError` / `Rejected` / `Failed`) |
32
+ | `host` | 要求と結果、session_id とロック、`call()`(失敗なら例外)、pipeline、エラーの階層(`OepError` / `Rejected` / `Failed`。lease 切れなら `Expired`。黙って open し直さず、`Host.epoch` が進む) |
33
33
  | `link` | transport: シリアルの口(常に COBS + CRC、`0x00 <COBS> 0x00`、フレームの外は雑音として捨てる、排他で開く)、USB vendor bulk / HID と TCP(長さつきフレーム、§5.1 の立て直し)、corr による照合と送り直し、`open_host(target)` |
34
- | `core` | インターフェースを名前で探す(キャッシュつき)、confirm、probe の describe(ラベル、transport の一覧)、ロックの取り方(`take`)、ピンの割り当て(plan)、`Interface` の土台 |
35
- | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`、`oep.target.riscv-dm`、リセット線の探索、GPIO 経由の attach |
36
- | `console` | `oep.target.console`(位置つきのストリーム)と、バイト列として読む `ConsoleIO` |
37
- | `fixture` | `oep.fixture.gpio` / `uart` / `i2c-target` / `spi-target`(revision 1) |
38
- | `config` | `oep.probe.config`(スロット、bind、plan / label / idle の項目、get / set / save / erase、スロットと bind の今の状態) |
39
- | `capture` | `oep.fixture.logic`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
34
+ | `core` | インターフェースを名前で探す(キャッシュつき)、confirm(probe の `boot_id` つき)、probe の describe(宣言だけ。起動の間 cache: ラベル、transport の一覧、`max_op_ms`)、ロックの取り方(`take`)、ピンの割り当て(plan)、`Interface` の土台 |
35
+ | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`(scan、attach: `max_speed` は常に送る、`reset=(channel, hold_ms)` でリセットをかけながら attach、detach、connections)、`oep.target.riscv-dm`(応答は値の数を持つ。`RunResult.not_halted`)、リセット線の探索、GPIO 経由の attach |
36
+ | `console` | `oep.target.console`(位置つきのストリーム: read の応答は長さを持ち、マークは `time_ns`、ロック不要の `streams()`)と、バイト列として読む `ConsoleIO` |
37
+ | `fixture` | `oep.fixture.gpio` / `uart`(ストリームは plan が作る。`status()`)/ `i2c-target` / `spi-target`(revision 1) |
38
+ | `config` | `oep.probe.config`(スロット、bind、plan / label / idle / uart の項目、get / set / unset / save / erase。`describe()` = 宣言、`state()` = 保存・スロット・bind の今の状態、`hash_of(items)` = probe と同じ hash) |
39
+ | `capture` | `oep.fixture.logic` / `analog` / `capture-group`(revision 1、oep-spec の oep-if-capture)。start ごとに世代(`LogicCapture.generation`)が進み、read と release はそれを付ける(`read_segment(segment)` は自分で付ける)。`status()` は `Status` を返す。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
40
40
  | `decode` | キャプチャのチャネルの復号(I2C) |
41
- | `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す) |
41
+ | `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す)。名前からインターフェースの番号を引く公開の入口は `registry.INTERFACES[name]`(`.revision`、`.op`、`.tlv`、`.enum`。例 `INTERFACES["oep.fixture.uart"].enum["role"]`)。`FIXTURE_UART` などのモジュールの名前は同じもの |
42
42
  | `arm` | `oep.wire.swd`、`oep.target.arm-adi`、MEM-AP、Cortex-M の停止と関数呼び出し |
43
43
  | `ch32_flash` | CH32 の書き込み(RAM ローダー、ページ単位の書き直し) |
44
44
  | `rp2350` | RP2350 の boot ROM 経由の flash と reboot |
@@ -69,10 +69,12 @@ hst.end()
69
69
 
70
70
  ```sh
71
71
  oep dump --port <probe> # 能力の一覧(--fake p4-x035 でハードウェアなし)
72
- oep config show <probe> # 設定とスロット / bind の今の状態
72
+ oep config show <probe> # 設定、宣言、今の状態
73
+ oep config state <probe> # スロット / bind / 保存の今の状態だけ(ロック不要。監視用)
73
74
  oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
74
75
  oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
75
- oep config save <probe> # 再起動の後も残す(remove / erase もある)
76
+ oep config uart <probe> oep.fixture.uart 115200 --format 8N1 # その UART の plan に RX / TX が付くたびに掛かる
77
+ oep config save <probe> # 再起動の後も残す(remove = unset、erase もある)
76
78
  ```
77
79
 
78
80
  `<probe>` はシリアルの口、`tcp://HOST:PORT`、`usb[:VID:PID[:SERIAL]]`。変更はロックを取り(owner "oep config")、終わったら
@@ -81,8 +83,9 @@ oep config save <probe> # 再起動の後も残す(remove
81
83
 
82
84
  ## 偽の probe(動く spec)
83
85
 
84
- `endpoint.Endpoint` は oep-spec の規範どおりに答える偽の probe で、ch32rv・この client・probe の firmware を突き合わせる
85
- 「動く spec」として使う(spec が変わったら、probe の firmware より先にここを合わせる)。`fake` は宣言の例(profile:
86
+ `endpoint.Endpoint` は oep-spec の規範どおりに答える偽の probe で(2026-10-01: 応答のデータと並びは長さを持ち、どの応答にも TLV が
87
+ 続けられる、期限切れ後の rejected `expired`、資源番号は 1 つの空間、describe は宣言だけで状態は `state`、キャプチャの世代)、
88
+ ch32rv・この client・probe の firmware を突き合わせる「動く spec」として使う(spec が変わったら、probe の firmware より先にここを合わせる)。`fake` は宣言の例(profile:
86
89
  `p4-x035`、`esp32-v003`、`p4-bench` = スロット 3 か所と席 2 つの架空の治具、`rp2350-pins` = host がピンを選ぶ wire)、`fake_serial` は
87
90
  シリアルの口のバイトの側(COBS の候補、生のバイトと bind、セッション中の停止と再開)。
88
91
 
@@ -33,16 +33,16 @@ oep-client-python X.Y.Z (until the v1 freeze every release may break the wire; t
33
33
 
34
34
  | Module | Contents |
35
35
  |---|---|
36
- | `host` | requests and results, the session id and the lock, `call()` (raises unless it worked), pipelining, the errors (`OepError` / `Rejected` / `Failed`) |
36
+ | `host` | requests and results, the session id and the lock, `call()` (raises unless it worked), pipelining, the errors (`OepError` / `Rejected` / `Failed`; `Expired` when the lease lapsed - the session is never re-opened behind the caller's back, `Host.epoch` moves) |
37
37
  | `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)` |
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 |
39
- | `riscv` | `oep.wire.rvswd` / `oep.wire.swio`, `oep.target.riscv-dm`, finding the reset line, attach through GPIO |
40
- | `console` | `oep.target.console` (position streams) and `ConsoleIO`, read as bytes |
41
- | `fixture` | `oep.fixture.gpio` / `uart` / `i2c-target` / `spi-target` (revision 1) |
42
- | `config` | `oep.probe.config` (slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) |
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) |
38
+ | `core` | interfaces by name (cached), confirm (with the probe's `boot_id`), the probe's describe (declarations only, cached per boot: labels, the transport list, `max_op_ms`), taking the lock (`take`), the pin plan, the `Interface` base |
39
+ | `riscv` | `oep.wire.rvswd` / `oep.wire.swio` (scan, attach - `max_speed` always sent, `reset=(channel, hold_ms)` for an attach under reset -, detach, connections), `oep.target.riscv-dm` (answers count their values; `RunResult.not_halted`), finding the reset line, attach through GPIO |
40
+ | `console` | `oep.target.console` (position streams: read answers carry their length, marks are `time_ns`, the lock-free `streams()` list) and `ConsoleIO`, read as bytes |
41
+ | `fixture` | `oep.fixture.gpio` / `uart` (its stream is the plan's; `status()`) / `i2c-target` / `spi-target` (revision 1) |
42
+ | `config` | `oep.probe.config` (slots, binds, plan / label / idle / uart items, get / set / unset / save / erase; `describe()` = the declarations, `state()` = the live storage / slot / bind state; `hash_of(items)` = the probe's hash) |
43
+ | `capture` | `oep.fixture.logic` / `analog` / `capture-group` (revision 1, oep-spec oep-if-capture). Every start is a generation (`LogicCapture.generation`) that read and release name - `read_segment(segment)` does it by itself; `status()` returns a `Status`. Every segment read goes to the `Host.on_capture` callbacks as a `CaptureRecord` (the hook for run recorders; no wireskein dependency) |
44
44
  | `decode` | decoding capture channels (I2C) |
45
- | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec) |
45
+ | `registry` | generated from oep-spec's number table (never edited; copied again from oep-spec). The public way to reach an interface by name is `registry.INTERFACES[name]` (`.revision`, `.op`, `.tlv`, `.enum`, e.g. `INTERFACES["oep.fixture.uart"].enum["role"]`); the module-level names (`FIXTURE_UART`, ...) are the same objects |
46
46
  | `arm` | `oep.wire.swd`, `oep.target.arm-adi`, MEM-AP, halting and calling functions on a Cortex-M |
47
47
  | `ch32_flash` | writing a CH32 (a RAM loader, page by page) |
48
48
  | `rp2350` | flash and reboot through the RP2350 boot ROM |
@@ -73,10 +73,12 @@ hst.end()
73
73
 
74
74
  ```sh
75
75
  oep dump --port <probe> # what the probe offers (--fake p4-x035: no hardware)
76
- oep config show <probe> # the settings and the live slot / bind state
76
+ oep config show <probe> # the settings, the declarations and the live state
77
+ oep config state <probe> # the live slot / bind / storage state alone (lock-free, for polling)
77
78
  oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
78
79
  oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
79
- oep config save <probe> # kept over a restart (also: remove, erase)
80
+ oep config uart <probe> oep.fixture.uart 115200 --format 8N1 # applied whenever that UART's plan has RX or TX
81
+ oep config save <probe> # kept over a restart (also: remove = unset, erase)
80
82
  ```
81
83
 
82
84
  `<probe>` is a serial port, `tcp://HOST:PORT` or `usb[:VID:PID[:SERIAL]]`. A change takes the lock (owner "oep config") and
@@ -86,8 +88,10 @@ A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`,
86
88
 
87
89
  ## The fake probe (a working spec)
88
90
 
89
- `endpoint.Endpoint` is a fake probe that answers as oep-spec says; ch32rv, this client and the probe firmware are checked
90
- against it (when the spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
91
+ `endpoint.Endpoint` is a fake probe that answers as oep-spec says (2026-10-01: every answer carries the length of its
92
+ data or list, TLVs may follow anything, rejected `expired` after a lapse, one resource number space, describe = declarations
93
+ and `state` for the rest, capture generations); ch32rv, this client and the probe firmware are checked against it (when the
94
+ spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
91
95
  `p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats, `rp2350-pins` = a wire whose pins the host
92
96
  chooses), `fake_serial` the byte side of a serial port (COBS candidates, raw bytes and binds, held during a session and
93
97
  resumed after it).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oep-client-python"
7
- version = "0.0.20"
7
+ version = "0.0.22"
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.20"
5
+ __version__ = "0.0.22"
@@ -1,10 +1,12 @@
1
1
  """The oep command: what a probe offers (dump) and its settings (config).
2
2
 
3
3
  oep dump --port /run/board-identify/by-id/<probe> oep dump --fake p4-x035 --prefix oep.target --json
4
- oep config show <probe>
4
+ oep config show <probe> (the settings and what the probe declares; lock-free)
5
+ oep config state <probe> (the live slot / bind / storage state; lock-free)
5
6
  oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
6
7
  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 plan <probe> oep.fixture.uart#1 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
9
+ oep config uart <probe> oep.fixture.uart#1 115200 --format 8N1
8
10
  oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
9
11
 
10
12
  <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
@@ -52,9 +54,12 @@ def main(argv=None) -> int:
52
54
  def _config_parser(sub) -> None:
53
55
  c = sub.add_parser("config", help="the probe's settings (oep.probe.config): slots, binds, save")
54
56
  cs = c.add_subparsers(dest="action", required=True)
55
- show = cs.add_parser("show", help="the settings and the live slot / bind state")
57
+ show = cs.add_parser("show", help="the settings, what the probe declares, and the live slot / bind state")
56
58
  show.add_argument("probe")
57
59
  show.add_argument("--json", action="store_true")
60
+ state = cs.add_parser("state", help="the live slot / bind / storage state (op state, lock-free)")
61
+ state.add_argument("probe")
62
+ state.add_argument("--json", action="store_true")
58
63
  slot = cs.add_parser("slot", help="register a slot (a place a target is wired to)")
59
64
  slot.add_argument("probe")
60
65
  slot.add_argument("--slot", type=int, default=0, help="the slot number (default 0)")
@@ -62,8 +67,9 @@ def _config_parser(sub) -> None:
62
67
  slot.add_argument("--wire", help="rvswd or swio (or an fn); default: the probe's only wire")
63
68
  slot.add_argument("--pins", help="swdio,swclk (one pin on swio); default: the wire's only pin set")
64
69
  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")
70
+ slot.add_argument("--retry", type=float, default=0, help="at-boot: try again every N s while absent (0: never)")
71
+ slot.add_argument("--mechanism", choices=sorted(config.MECHANISM), default="dmseq",
72
+ help="the console's mechanism, or none (no console on this slot)")
67
73
  slot.add_argument("--max-speed", type=int, default=0,
68
74
  help="the line's ceiling in Hz for the probe's own attach (0: none); the target's, e.g. 1000000")
69
75
  slot.add_argument("--idle-clock", choices=sorted(config.IDLE_CLOCK), default="high",
@@ -79,7 +85,7 @@ def _config_parser(sub) -> None:
79
85
  bind.add_argument("--save", action="store_true")
80
86
  plan = cs.add_parser("plan", help="the pins an interface keeps (its whole plan, kept as a setting)")
81
87
  plan.add_argument("probe")
82
- plan.add_argument("fn", help="an fn, or an interface name with #k for its k-th instance (1 = the first)")
88
+ plan.add_argument("fn", help="an fn, or name#instance (the list instance, 0 = the first; #0 may be left out, core §7.2)")
83
89
  plan.add_argument("roles", nargs="+", help="ROLE=CHANNEL, ROLE a number or the interface's role name (rx, tx, line...)")
84
90
  plan.add_argument("--save", action="store_true")
85
91
  label = cs.add_parser("label", help="name a channel (shown in oep.core's describe)")
@@ -92,9 +98,15 @@ def _config_parser(sub) -> None:
92
98
  idle.add_argument("channel", type=int)
93
99
  idle.add_argument("mode", choices=sorted(config.IDLE))
94
100
  idle.add_argument("--save", action="store_true")
95
- rm = cs.add_parser("remove", help="remove one item: slot N, bind PORT, plan FN, label CH, idle CH")
101
+ uart = cs.add_parser("uart", help="a fixture UART's baud / format, applied whenever its plan has RX or TX")
102
+ uart.add_argument("probe")
103
+ uart.add_argument("fn", help="an fn, or name#instance of the oep.fixture.uart")
104
+ uart.add_argument("baud", type=int)
105
+ uart.add_argument("--format", default="8N1", help="data bits, parity, stop bits: 8N1 (default), 8E1, 7O2 ...")
106
+ uart.add_argument("--save", action="store_true")
107
+ rm = cs.add_parser("remove", help="remove one item (unset): slot N, bind PORT, plan FN, label CH, idle CH, uart FN")
96
108
  rm.add_argument("probe")
97
- rm.add_argument("kind", choices=["slot", "bind", "plan", "label", "idle"])
109
+ rm.add_argument("kind", choices=sorted(config.ITEM))
98
110
  rm.add_argument("key", type=int)
99
111
  rm.add_argument("--save", action="store_true")
100
112
  for name in ("save", "erase"):
@@ -108,7 +120,7 @@ def _pins(hst, fn: int, text: str | None, wire: str) -> tuple[int, int]:
108
120
  groups = []
109
121
  for tag, v in core.describe(hst, fn):
110
122
  if tag & 0x7F == catalog.CHANNEL_GROUP:
111
- roles = {v[1 + 3 * i]: struct.unpack_from("<H", v, 2 + 3 * i)[0] for i in range((len(v) - 1) // 3)}
123
+ roles = dict(catalog.unpack_channel_group(v)[1])
112
124
  groups.append((roles.get(1, 0xFFFF), roles.get(2, 0xFFFF)))
113
125
  if len(groups) != 1:
114
126
  raise SystemExit(f"{wire}: {len(groups)} pin sets on this probe - name one with --pins")
@@ -116,20 +128,21 @@ def _pins(hst, fn: int, text: str | None, wire: str) -> tuple[int, int]:
116
128
 
117
129
 
118
130
  def _plan_fn(hst, spec: str) -> tuple[int, dict[str, int]]:
119
- """fn and its role names (from the registry) for `spec`: an fn number, or name[#k] (k-th instance, 1-based)."""
131
+ """fn and its role names (from the registry) for `spec`: an fn number, or name[#instance] (the list's instance,
132
+ 0-based, core §7.2)."""
120
133
  from . import registry as reg
121
134
  if spec.isdigit():
122
135
  fn = int(spec)
123
136
  name = next((e.name for e in core.list_entries(hst) if e.fn == fn), "")
124
137
  else:
125
138
  name, _, k = spec.partition("#")
126
- fns = core.find_all(hst, name)
127
- if not fns:
139
+ entries = [e for e in core.list_entries(hst, name, exact=True)]
140
+ if not entries:
128
141
  raise SystemExit(f"the probe offers no {name}")
129
- index = int(k) - 1 if k else 0
130
- if not 0 <= index < len(fns):
131
- raise SystemExit(f"{name}: {len(fns)} instance(s), fns {fns}")
132
- fn = fns[index]
142
+ instance = int(k) if k else 0
143
+ fn = next((e.fn for e in entries if e.instance == instance), None)
144
+ if fn is None:
145
+ raise SystemExit(f"{name}: instances {sorted(e.instance for e in entries)}, not {instance}")
133
146
  iface = reg.INTERFACES.get(name)
134
147
  roles = dict(iface.enum.get("role", {})) if iface else {}
135
148
  return fn, roles
@@ -179,6 +192,8 @@ def _config(args) -> int:
179
192
  cfg = config.ProbeConfig(hst)
180
193
  if args.action == "show":
181
194
  return _show(hst, cfg, args.json)
195
+ if args.action == "state":
196
+ return _state(cfg, args.json)
182
197
  if args.action == "slot":
183
198
  fn = _wire_fn(hst, args.wire)
184
199
  args.wire = args.wire or str(fn)
@@ -207,6 +222,15 @@ def _config(args) -> int:
207
222
  raise SystemExit(f"{role}: not a role of fn {fn} (roles: {', '.join(sorted(roles)) or 'numbers only'})")
208
223
  items.append(config.Plan(fn=fn, role=number, channel=int(ch, 0)))
209
224
  _change(hst, cfg, items, args.save)
225
+ elif args.action == "uart":
226
+ fn, _ = _plan_fn(hst, args.fn)
227
+ from .fixture import FixtureUart
228
+ fmt = args.format.upper()
229
+ try:
230
+ byte = FixtureUart.format_byte(int(fmt[0]), fmt[1], int(fmt[2]))
231
+ except (KeyError, ValueError, IndexError):
232
+ raise SystemExit(f"--format {args.format}: want <data bits><parity><stop bits>, e.g. 8N1, 8E2, 7O1") from None
233
+ _change(hst, cfg, [config.Uart(fn=fn, baud=args.baud, format=byte)], args.save)
210
234
  elif args.action == "label":
211
235
  _change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
212
236
  elif args.action == "idle":
@@ -231,42 +255,66 @@ def _config(args) -> int:
231
255
  hst.link.close()
232
256
 
233
257
 
258
+ def _plain(o):
259
+ return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
260
+ else list(o)
261
+
262
+
263
+ def _state_dict(st) -> dict:
264
+ return {**{k: v for k, v in vars(st).items() if k not in ("slots", "binds")},
265
+ "slots": [_plain(s) for s in st.slots], "binds": [_plain(b) for b in st.binds]}
266
+
267
+
268
+ def _state(cfg, as_json: bool) -> int:
269
+ """The live state alone (op state, lock-free): what a monitor polls."""
270
+ st = cfg.state()
271
+ if as_json:
272
+ print(json.dumps(_state_dict(st), indent=2, default=lambda o: o.hex() if isinstance(o, bytes) else str(o)))
273
+ return 0
274
+ why = f" ({st.unreadable})" if st.unreadable else ""
275
+ print(f"storage: {st.storage}{why}, saved hash 0x{st.saved_hash:08x}")
276
+ for s in st.slots:
277
+ print(f" slot {s.slot}: {s.state}" + (f", connection {s.connection}" if s.connection else "")
278
+ + (f", tried at {s.last_try_at_ns / 1e9:.3f} s" if s.last_try_at_ns is not None else "")
279
+ + (f", target_id {s.target_id[::-1].hex()}" if s.target_id else ""))
280
+ for b in st.binds:
281
+ print(f" port {b.port}: {b.mode}, {b.flow}" + (f", carrying {b.selected}" if b.selected is not None else ""))
282
+ return 0
283
+
284
+
234
285
  def _show(hst, cfg, as_json: bool) -> int:
235
286
  h, _ = cfg.get()
236
287
  items = cfg.items()
288
+ decl = cfg.describe()
237
289
  st = cfg.state()
238
290
  kinds = {i: _name(k) for i, k, _ in core.transports(hst)}
239
291
  if as_json:
240
- def plain(o):
241
- return {k: (v.hex() if isinstance(v, bytes) else v) for k, v in vars(o).items()} if hasattr(o, "__dict__") \
242
- else list(o)
243
- out = {"hash": h, "items": [dict(type=type(i).__name__, **plain(i)) if hasattr(i, "__dict__") else plain(i)
292
+ out = {"hash": h, "items": [dict(type=type(i).__name__, **_plain(i)) if hasattr(i, "__dict__") else _plain(i)
244
293
  for i in items],
245
- "state": {**{k: v for k, v in vars(st).items() if k not in ("slots", "binds")},
246
- "slots": [plain(s) for s in st.slots], "binds": [plain(b) for b in st.binds]},
247
- "transports": kinds}
294
+ "declared": _plain(decl), "state": _state_dict(st), "transports": kinds}
248
295
  print(json.dumps(out, indent=2, default=lambda o: o.hex() if isinstance(o, bytes) else str(o)))
249
296
  return 0
250
- print(f"storage: {st.storage} ({st.storage_bytes} bytes), saved hash 0x{st.saved_hash:08x}; now 0x{h:08x}")
297
+ why = f" ({st.unreadable})" if st.unreadable else ""
298
+ print(f"storage: {st.storage}{why} ({decl.storage_bytes} bytes), saved hash 0x{st.saved_hash:08x}; now 0x{h:08x}")
251
299
  print("transports: " + ", ".join(f"{i} {k}" for i, k in kinds.items()))
252
300
  by_slot = {s.slot: s for s in st.slots}
253
- print(f"slots (up to {st.slots_max}):")
301
+ print(f"slots (up to {decl.slots_max}):")
254
302
  for it in items:
255
303
  if isinstance(it, config.Slot):
256
304
  s = by_slot.get(it.slot)
257
305
  pins = f"{it.pins[0]}" if it.pins[1] == 0xFFFF else f"{it.pins[0]},{it.pins[1]}"
258
- retry = f" retry {it.retry_s} s" if it.attach == "at-boot" else ""
306
+ retry = f" retry {it.retry_s:g} s" if it.attach == "at-boot" else ""
259
307
  retry += (f" max {it.max_speed} Hz" if it.max_speed else "") + (" idle-low" if it.idle_clock == "low" else "")
260
308
  lock = (f" lock {int.from_bytes(it.lock[1], 'little'):08x}:{int.from_bytes(it.lock[2], 'little'):08x}"
261
309
  if it.lock else "")
262
310
  live = ""
263
311
  if s:
264
- tried = "never tried" if s.last_try_ms is None else f"tried {s.last_try_ms} ms ago"
312
+ tried = "never tried" if s.last_try_at_ns is None else f"tried at {s.last_try_at_ns / 1e9:.3f} s"
265
313
  tid = f" target_id {s.target_id[::-1].hex()}" if s.target_id else ""
266
314
  live = f" -> {s.state}" + (f" (connection {s.connection})" if s.connection else f" ({tried})") + tid
267
315
  print(f" {it.slot} {it.name}: fn {it.wire_fn} pins {pins} {it.attach}{retry} {it.mechanism}{lock}{live}")
268
316
  by_port = {b.port: b for b in st.binds}
269
- print(f"binds (modes: {', '.join(st.bind_modes) or '-'}):")
317
+ print(f"binds (modes: {', '.join(decl.bind_modes) or '-'}):")
270
318
  names = {it.slot: it.name for it in items if isinstance(it, config.Slot)}
271
319
  for it in items:
272
320
  if isinstance(it, config.Bind):
@@ -29,17 +29,19 @@ class SwdWire(WireBase):
29
29
  self.existing = False
30
30
 
31
31
  def attach(self, targetsel: int | None = None, max_speed: int | None = None,
32
- pins: tuple[int, int] | None = None) -> tuple[int, int, bool]:
33
- """-> (connection, DPIDR, woke from dormant). targetsel (multidrop) and max_speed go as critical TLVs: a probe
34
- that cannot honour them refuses. self.existing: the wire was attached already (its connection returned)."""
35
- body = self._speed_tlv(max_speed) + self._pins_tlv(pins)
32
+ pins: tuple[int, int] | None = None, reset: tuple[int, int] | None = None) -> tuple[int, int, bool]:
33
+ """-> (connection, DPIDR, woke from dormant). The request is method 0 (swd has no halting attach) and TLVs:
34
+ max_speed (required, critical; None: the wire's declared max_clock_hz), targetsel (multidrop, critical), pins,
35
+ reset = (channel, hold_ms) (critical). self.existing: the wire was attached already (its connection returned).
36
+ self.flags: the attach_flags byte."""
37
+ body = bytes([0]) + self._speed_tlv(self._speed_or_default(max_speed)) + self._pins_tlv(pins) + self._reset_tlv(reset)
36
38
  if targetsel is not None:
37
39
  body += m.tlv(self.TAG_TARGETSEL, struct.pack("<I", targetsel), critical=True)
38
40
  rd = m.Reader(self._call(self.ATTACH, body).payload)
39
- conn, dpidr, flags, self.speed_hz = rd.take("HIBI")
40
- self.existing = bool(flags & 2)
41
+ conn, dpidr, self.flags, self.speed_hz = rd.take("HIBI")
42
+ self.existing = bool(self.flags & self.FLAGS["existing"])
41
43
  rd.tail()
42
- return conn, dpidr, bool(flags & 1)
44
+ return conn, dpidr, bool(self.flags & self.FLAGS["dormant_woken"])
43
45
 
44
46
 
45
47
  class AdiError(TargetError):
@@ -82,8 +84,8 @@ class ArmAdi(Interface):
82
84
  reads = transfer_reads(steps)
83
85
  r = self._request(self.TRANSFER, struct.pack("<H", len(reads)) + steps)
84
86
  rd = ran(r)
85
- done, status, self.last_ack = rd.take("HBB")
86
- values = rd.words(sum(reads[:done]))
87
+ done, status, self.last_ack, nvals = rd.take("HBBH") # nvals: the values the answer carries (debug §6)
88
+ values = rd.words(nvals)
87
89
  rd.tail()
88
90
  if status != OK or not r.succeeded or done != len(reads):
89
91
  raise AdiError(f"transfer (ack {self.last_ack:#x})", status, r, done=done, values=values)