oep-client-python 0.0.4__tar.gz → 0.0.6__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.4 → oep_client_python-0.0.6}/CHANGELOG.md +16 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/PKG-INFO +2 -2
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/README.ja.md +1 -1
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/README.md +1 -1
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/pyproject.toml +1 -1
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/__init__.py +1 -1
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/__main__.py +6 -1
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/config.py +12 -7
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/core.py +41 -1
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/endpoint.py +29 -11
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/fake.py +2 -2
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/fake_serve.py +2 -2
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/registry.py +4 -4
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/riscv.py +31 -12
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/usb_stream.py +32 -4
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_config.py +55 -1
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_fake_spec.py +52 -4
- oep_client_python-0.0.6/tests/test_usb_stream.py +96 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/.gitignore +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/LICENSE +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/arm.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/capture.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/catalog.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/ch32_flash.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/cobs.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/console.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/decode.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/dump.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/esp32_targets.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/fake_serial.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/fixture.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/frames.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/hid_stream.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/host.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/interfaces.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/link.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/message.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/names.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/rp2350.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/target.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/src/oep_client/uiapduino.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_capabilities.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_capture.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_cobs.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_esp32_targets.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_flash_console.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_hid_stream.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_interfaces.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_link_host.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_link_serial.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_session.py +0 -0
- {oep_client_python-0.0.4 → oep_client_python-0.0.6}/tests/test_target_parts.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.6
|
|
6
|
+
- (EN) No default reset line (oep-if-debug §3): `Wire.attach_under_reset(channel, ...)` needs its channel, and `Wire.reset_channels()` reads the ones the probe allows (describe role_channels, role reset). The fake takes only those, and not one a plan holds.
|
|
7
|
+
- (JA) 既定の reset 線は無くなった(oep-if-debug §3): `Wire.attach_under_reset(channel, ...)` は channel が必須。probe が許す channel は `Wire.reset_channels()` で読む(describe の role_channels の role reset)。偽の probe はそれだけを受け、plan が持つ channel は断る。
|
|
8
|
+
- (EN) The line's settings are the target's, passed by the host: `attach(..., idle_clock="low")` (rvswd, critical) and the probe.config slot's new max_speed / idle_clock fields (`oep config slot --max-speed --idle-clock`) for the probe's own attach. The slot item's layout changed.
|
|
9
|
+
- (JA) 線の設定は target のもので、host が渡す: `attach(..., idle_clock="low")`(rvswd、critical)と、probe が自分で attach するための probe.config のスロットの max_speed / idle_clock(`oep config slot --max-speed --idle-clock`)。スロットの項目の形が変わった。
|
|
10
|
+
- (EN) The fake probe keeps a plan the settings put in (oep-core §8): plan_release leaves it (n = 0 included) and plan_apply naming its fn is rejected unavailable.
|
|
11
|
+
- (JA) 偽の probe は、設定が入れた plan を設定のものとして扱う(oep-core §8): plan_release はそれを解かず(n = 0 でも)、その fn を挙げた plan_apply は rejected unavailable。
|
|
12
|
+
|
|
13
|
+
## 0.0.5
|
|
14
|
+
- (EN) USB (python-libusb1) streams close cleanly and at exit: the IN transfers are cancelled and taken back by the event thread before the handle and the context go. A program that ended without closing the link hung at exit, or libusb aborted.
|
|
15
|
+
- (JA) USB(python-libusb1)の stream は、終了時にも片付いて閉じる: IN の転送を取り消し、event thread が取り戻してから handle と context を閉じる。link を閉じずに終わったプログラムが終了で固まったり、libusb が abort したりしていた。
|
|
16
|
+
- (EN) `core.plan_apply` refused as unavailable names what holds the pins when the settings say so (`core.PinsTaken`: another fn's saved plan, a slot).
|
|
17
|
+
- (JA) `core.plan_apply` が unavailable で断られたとき、設定から分かれば何がピンを持っているかを示す(`core.PinsTaken`: 別の fn の保存した plan、スロット)。
|
|
18
|
+
- (EN) Fake probe: a read from the last mark of a kind that is not there starts now, not at the oldest byte (oep-if-common §1.2).
|
|
19
|
+
- (JA) 偽の probe: その kind のマークが無いときの「最後のマークから」の read は、一番古い位置ではなく今から(oep-if-common §1.2)。
|
|
20
|
+
|
|
5
21
|
## 0.0.4
|
|
6
22
|
- (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
23
|
- (JA) 偽のシリアルの口: 0x00 だけの候補は、200 ms の後も生のバイトにしない(probe の firmware にも同じバグがあり、target のコンソールに 0x00 が届いていた)。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: oep-client-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
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
|
|
@@ -43,7 +43,7 @@ uv run pytest # in a checkout
|
|
|
43
43
|
`oep-client` is another project, so the distribution is named `oep-client-python`.
|
|
44
44
|
|
|
45
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
|
|
46
|
+
sets the version in pyproject.toml, uv.lock and `oep_client.__version__` and turns CHANGELOG.md's Unreleased into that version; after
|
|
47
47
|
the tests and the build it commits, tags, makes the GitHub Release and publishes to PyPI (Trusted Publishing). Record changes
|
|
48
48
|
under Unreleased in CHANGELOG.md, (EN) and (JA).
|
|
49
49
|
|
|
@@ -18,7 +18,7 @@ uv run pytest # in a
|
|
|
18
18
|
oep-spec から写す。PyPI の `oep-client` は別のプロジェクトなので、配布名は `oep-client-python`。
|
|
19
19
|
|
|
20
20
|
リリースは GitHub Actions の Release(workflow_dispatch、version = X.Y.Z か X.Y.ZbN): `tools/prepare_release.py` が
|
|
21
|
-
pyproject.toml
|
|
21
|
+
pyproject.toml、uv.lock、`oep_client.__version__` を書き換え、CHANGELOG.md の Unreleased をその版にし、試験と build の後に commit と tag、
|
|
22
22
|
GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md の Unreleased に (EN) / (JA) で書き足しておく。
|
|
23
23
|
|
|
24
24
|
## モジュール(`oep_client`)
|
|
@@ -21,7 +21,7 @@ uv run pytest # in a checkout
|
|
|
21
21
|
`oep-client` is another project, so the distribution is named `oep-client-python`.
|
|
22
22
|
|
|
23
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
|
|
24
|
+
sets the version in pyproject.toml, uv.lock and `oep_client.__version__` and turns CHANGELOG.md's Unreleased into that version; after
|
|
25
25
|
the tests and the build it commits, tags, makes the GitHub Release and publishes to PyPI (Trusted Publishing). Record changes
|
|
26
26
|
under Unreleased in CHANGELOG.md, (EN) and (JA).
|
|
27
27
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oep-client-python"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.6"
|
|
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"
|
|
@@ -64,6 +64,10 @@ def _config_parser(sub) -> None:
|
|
|
64
64
|
slot.add_argument("--attach", choices=sorted(config.ATTACH), default="host")
|
|
65
65
|
slot.add_argument("--retry", type=int, default=0, help="at-boot: try again every N s while absent (0: never)")
|
|
66
66
|
slot.add_argument("--mechanism", choices=sorted(config.MECHANISM), default="dmseq")
|
|
67
|
+
slot.add_argument("--max-speed", type=int, default=0,
|
|
68
|
+
help="the line's ceiling in Hz for the probe's own attach (0: none); the target's, e.g. 1000000")
|
|
69
|
+
slot.add_argument("--idle-clock", choices=sorted(config.IDLE_CLOCK), default="high",
|
|
70
|
+
help="rvswd: SWCLK while the line rests (the target's: low on CH32L103 / V203)")
|
|
67
71
|
slot.add_argument("--lock", help="MASK:VALUE (hex u32) the target_id (WCH DMI 0x7F) must match, e.g. ffffff0f:035e0600")
|
|
68
72
|
slot.add_argument("--save", action="store_true", help="save after the change")
|
|
69
73
|
bind = cs.add_parser("bind", help="what a serial port carries")
|
|
@@ -183,7 +187,7 @@ def _config(args) -> int:
|
|
|
183
187
|
mask, _, value = args.lock.partition(":")
|
|
184
188
|
lock = (1, struct.pack("<I", int(mask, 16)), struct.pack("<I", int(value, 16)))
|
|
185
189
|
it = config.Slot(args.slot, fn, _pins(hst, fn, args.pins, args.wire), args.name, args.attach, args.retry,
|
|
186
|
-
args.mechanism, lock)
|
|
190
|
+
args.mechanism, lock, args.max_speed, args.idle_clock)
|
|
187
191
|
_change(hst, cfg, [it], args.save)
|
|
188
192
|
elif args.action == "bind":
|
|
189
193
|
slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
|
|
@@ -250,6 +254,7 @@ def _show(hst, cfg, as_json: bool) -> int:
|
|
|
250
254
|
s = by_slot.get(it.slot)
|
|
251
255
|
pins = f"{it.pins[0]}" if it.pins[1] == 0xFFFF else f"{it.pins[0]},{it.pins[1]}"
|
|
252
256
|
retry = f" retry {it.retry_s} s" if it.attach == "at-boot" else ""
|
|
257
|
+
retry += (f" max {it.max_speed} Hz" if it.max_speed else "") + (" idle-low" if it.idle_clock == "low" else "")
|
|
253
258
|
lock = (f" lock {int.from_bytes(it.lock[1], 'little'):08x}:{int.from_bytes(it.lock[2], 'little'):08x}"
|
|
254
259
|
if it.lock else "")
|
|
255
260
|
live = ""
|
|
@@ -29,6 +29,7 @@ MECHANISM = {k: v for k, v in reg.TARGET_CONSOLE.enum["mechanism"].items()}
|
|
|
29
29
|
SLOT_STATE = {v: k.replace("_", "-") for k, v in _CFG.enum["slot_state"].items()}
|
|
30
30
|
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
|
+
IDLE_CLOCK = dict(reg.WIRE_RVSWD.enum["idle_clock"]) # a slot's idle_clock (oep-if-debug §3)
|
|
32
33
|
STORAGE_STATE = {v: k for k, v in _CFG.enum["storage_state"].items()}
|
|
33
34
|
|
|
34
35
|
|
|
@@ -79,12 +80,15 @@ class Slot:
|
|
|
79
80
|
retry_s: int = 0 # at-boot: try again every retry_s while the target is not there (0: never)
|
|
80
81
|
mechanism: str = "dmseq" # sdi, dmdata, dmseq
|
|
81
82
|
lock: tuple[int, bytes, bytes] | None = None
|
|
83
|
+
max_speed: int = 0 # the line's ceiling in Hz for the probe's own attach (0: none) - the target's
|
|
84
|
+
idle_clock: str = "high" # rvswd: SWCLK while the line rests, high / low - the target's (oep-if-debug §3)
|
|
82
85
|
TAG = ITEM["slot"]
|
|
83
86
|
|
|
84
87
|
def value(self) -> bytes:
|
|
85
88
|
name = self.name.encode()
|
|
86
|
-
v = struct.pack("<
|
|
87
|
-
self.retry_s if self.attach == "at-boot" else 0,
|
|
89
|
+
v = struct.pack("<BHHHBHIBBB", self.slot, self.wire_fn, *self.pins, ATTACH[self.attach],
|
|
90
|
+
self.retry_s if self.attach == "at-boot" else 0, self.max_speed, IDLE_CLOCK[self.idle_clock],
|
|
91
|
+
MECHANISM[self.mechanism], len(name)) + name
|
|
88
92
|
if self.lock is None:
|
|
89
93
|
return v + b"\x00"
|
|
90
94
|
scheme, mask, value = self.lock
|
|
@@ -127,15 +131,16 @@ def decode(tag: int, v: bytes):
|
|
|
127
131
|
return Label(struct.unpack_from("<H", v)[0], v[2:].decode("utf-8", "replace"))
|
|
128
132
|
if tag == ITEM["idle"] and len(v) == 3:
|
|
129
133
|
return Idle(struct.unpack_from("<H", v)[0], _name(IDLE, v[2]))
|
|
130
|
-
if tag == ITEM["slot"] and len(v) >=
|
|
131
|
-
n, wire_fn, swdio, swclk, attach, retry_s, mech, name_len = struct.unpack_from("<
|
|
132
|
-
name = v[
|
|
133
|
-
rest = v[
|
|
134
|
+
if tag == ITEM["slot"] and len(v) >= 18:
|
|
135
|
+
n, wire_fn, swdio, swclk, attach, retry_s, max_speed, idle, mech, name_len = struct.unpack_from("<BHHHBHIBBB", v)
|
|
136
|
+
name = v[17:17 + name_len].decode("ascii", "replace")
|
|
137
|
+
rest = v[17 + name_len:]
|
|
134
138
|
lock = None
|
|
135
139
|
if rest and rest[0]:
|
|
136
140
|
half = (len(rest) - 1) // 2
|
|
137
141
|
lock = (rest[0], rest[1:1 + half], rest[1 + half:])
|
|
138
|
-
return Slot(n, wire_fn, (swdio, swclk), name, _name(ATTACH, attach), retry_s, _name(MECHANISM, mech), lock
|
|
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))
|
|
139
144
|
if tag == ITEM["bind"] and len(v) >= 4:
|
|
140
145
|
port, mode, selected, n = struct.unpack_from("<BBBB", v)
|
|
141
146
|
streams = [(_name(STREAM, v[4 + 3 * k]), struct.unpack_from("<H", v, 5 + 3 * k)[0]) for k in range(n)]
|
|
@@ -109,7 +109,47 @@ def plan_apply(hst: h.Host, assignments: list[tuple[int, int, int]]) -> None:
|
|
|
109
109
|
all interfaces accept their roles or nothing changes. The plan is the
|
|
110
110
|
session's resource: kept over an explicit end, released at a lease lapse or a force takeover (oep-core §9)."""
|
|
111
111
|
tlv = b"".join(bytes([TAG_ROLE_ASSIGNMENT, 5]) + struct.pack("<HBH", fn, role, ch) for fn, role, ch in assignments)
|
|
112
|
-
|
|
112
|
+
try:
|
|
113
|
+
hst.call(m.CORE_FN, OP_PLAN_APPLY, tlv)
|
|
114
|
+
except h.Rejected as e:
|
|
115
|
+
if e.result.detail != m.UNAVAILABLE:
|
|
116
|
+
raise
|
|
117
|
+
why = _pin_holders(hst, assignments)
|
|
118
|
+
if why:
|
|
119
|
+
raise PinsTaken(e.result, why) from None
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class PinsTaken(h.Rejected):
|
|
124
|
+
"""plan_apply refused (unavailable) and the probe's settings say why: a pin kept by another fn's saved plan or by a
|
|
125
|
+
slot (oep.probe.config). `holders`: (channel, what holds it)."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, result, holders: list[tuple[int, str]]):
|
|
128
|
+
super().__init__(result)
|
|
129
|
+
self.holders = holders
|
|
130
|
+
|
|
131
|
+
def __str__(self) -> str:
|
|
132
|
+
return "pins taken: " + "; ".join(f"channel {ch} by {who}" for ch, who in self.holders) + \
|
|
133
|
+
" (oep config show; oep config remove <probe> plan <fn> frees a saved plan)"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _pin_holders(hst: h.Host, assignments: list[tuple[int, int, int]]) -> list[tuple[int, str]]:
|
|
137
|
+
"""What the settings keep on the channels asked for (best effort: an empty list when there is no oep.probe.config
|
|
138
|
+
or it cannot be read)."""
|
|
139
|
+
from . import config
|
|
140
|
+
try:
|
|
141
|
+
items = config.ProbeConfig(hst).items()
|
|
142
|
+
except (LookupError, h.OepError):
|
|
143
|
+
return []
|
|
144
|
+
fns = {fn for fn, _, _ in assignments}
|
|
145
|
+
out = []
|
|
146
|
+
for fn, role, ch in assignments:
|
|
147
|
+
for it in items:
|
|
148
|
+
if isinstance(it, config.Plan) and it.channel == ch and it.fn not in fns:
|
|
149
|
+
out.append((ch, f"the saved plan of fn {it.fn} (role {it.role})"))
|
|
150
|
+
elif isinstance(it, config.Slot) and ch in it.pins:
|
|
151
|
+
out.append((ch, f"slot {it.slot} {it.name}"))
|
|
152
|
+
return out
|
|
113
153
|
|
|
114
154
|
|
|
115
155
|
def plan_release(hst: h.Host, fns: list[int] | tuple[int, ...] = ()) -> None:
|
|
@@ -60,6 +60,7 @@ WIRES = ("oep.wire.rvswd", "oep.wire.swio")
|
|
|
60
60
|
OWNER = reg.CORE.tlv["open"]["owner"]
|
|
61
61
|
SLOT_NAME = re.compile(r"[a-z0-9_-]{1,32}")
|
|
62
62
|
NO_SLOT, NEVER = 0xFF, 0xFFFFFFFF
|
|
63
|
+
PIN_ROLE_RESET = reg.WIRE_RVSWD.enum["pin_role"]["reset"] # the channels attach_under_reset may take
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
class Reject(Exception):
|
|
@@ -180,6 +181,7 @@ class Connection:
|
|
|
180
181
|
speed: int = 4_000_000
|
|
181
182
|
tid: int | None = None
|
|
182
183
|
users: set = field(default_factory=set) # "host" and/or ("slot", n)
|
|
184
|
+
idle_clock: int = 0 # rvswd: SWCLK while the line rests, 0 high / 1 low
|
|
183
185
|
|
|
184
186
|
|
|
185
187
|
@dataclass(frozen=True)
|
|
@@ -189,6 +191,8 @@ class Slot:
|
|
|
189
191
|
pair: tuple[int, int]
|
|
190
192
|
attach: int
|
|
191
193
|
retry_s: int
|
|
194
|
+
max_speed: int # 0: no ceiling (oep-if-debug §3: the target's, the host's to set)
|
|
195
|
+
idle_clock: int
|
|
192
196
|
mechanism: int
|
|
193
197
|
name: str
|
|
194
198
|
lock: tuple[int, bytes, bytes] | None # (scheme, mask, value)
|
|
@@ -249,9 +253,13 @@ class Endpoint:
|
|
|
249
253
|
self.serial_ports = {i for i, k in enumerate(self.transports) if k in fake.SERIAL_KINDS}
|
|
250
254
|
self.pairs: dict[int, list[tuple[int, int]]] = {} # wire fn -> allowed (swdio, swclk), declared order
|
|
251
255
|
self.max_connections: dict[int, int] = {}
|
|
256
|
+
self.reset_channels: dict[int, set[int]] = {} # wire fn -> channels attach_under_reset may take (role 3)
|
|
252
257
|
for fn, name in self.names.items():
|
|
253
258
|
if name in WIRES:
|
|
254
259
|
self.pairs[fn] = [self._group_pair(name, t) for t in self.static[fn] if t[0] == catalog.CHANNEL_GROUP]
|
|
260
|
+
self.reset_channels[fn] = {
|
|
261
|
+
c for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS and t[2] == PIN_ROLE_RESET
|
|
262
|
+
for c in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
|
|
255
263
|
self.max_connections[fn] = next((t[2] for t in self.static[fn] if t[0] == fake.MAX_CONNECTIONS), 1)
|
|
256
264
|
self.targets: dict[tuple[int, tuple[int, int]], FakeTarget] = {
|
|
257
265
|
(fn, p): FakeTarget() for fn in sorted(self.pairs) for p in self.pairs[fn]}
|
|
@@ -575,17 +583,18 @@ class Endpoint:
|
|
|
575
583
|
got.append(struct.unpack_from("<HBH", value))
|
|
576
584
|
elif tag & m.TAG_CRITICAL:
|
|
577
585
|
return m.REJECTED, m.UNSUPPORTED, bytes([tag])
|
|
578
|
-
self._check_plan(got)
|
|
579
586
|
named = {fn for fn, _, _ in got}
|
|
587
|
+
if named & self.plan_from_config: # the settings' plan is the settings' (core §8)
|
|
588
|
+
raise Reject(m.UNAVAILABLE)
|
|
589
|
+
self._check_plan(got)
|
|
580
590
|
self.plan = {a for a in self.plan if a[0] not in named} | set(got)
|
|
581
|
-
self.plan_from_config -= named
|
|
582
591
|
return m.COMPLETED, m.SUCCESS, b""
|
|
583
592
|
if op == m.OP_PLAN_RELEASE: # n(u8) n x fn(u16); n = 0: every fn
|
|
584
593
|
n = t.take("B")
|
|
585
594
|
fns = {t.take("H") for _ in range(n)}
|
|
586
595
|
t.tail()
|
|
587
|
-
for fn in {a[0] for a in self.plan if not fns or a[0] in fns}:
|
|
588
|
-
self._drop_plan(fn)
|
|
596
|
+
for fn in {a[0] for a in self.plan if not fns or a[0] in fns} - self.plan_from_config:
|
|
597
|
+
self._drop_plan(fn) # the settings' plans stay, n = 0 too (core §8)
|
|
589
598
|
return m.COMPLETED, m.SUCCESS, b""
|
|
590
599
|
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
591
600
|
|
|
@@ -657,10 +666,14 @@ class Endpoint:
|
|
|
657
666
|
if method > 1:
|
|
658
667
|
raise Reject(m.UNSUPPORTED)
|
|
659
668
|
else:
|
|
660
|
-
channel, _hold = t.take("HH")
|
|
661
|
-
if channel not in (
|
|
669
|
+
channel, _hold = t.take("HH") # no default reset line (oep-if-debug §3)
|
|
670
|
+
if channel not in self.reset_channels.get(fn, set()) or any(a[2] == channel for a in self.plan):
|
|
662
671
|
raise Reject(m.UNAVAILABLE)
|
|
663
|
-
|
|
672
|
+
rvswd = self.names[fn] == "oep.wire.rvswd"
|
|
673
|
+
got, ignored = t.tail({0x01, 0x03, 0x04} if rvswd else {0x01, 0x03})
|
|
674
|
+
if 0x04 in got and (len(got[0x04]) != 1 or got[0x04][0] > 1):
|
|
675
|
+
raise Reject(m.MALFORMED)
|
|
676
|
+
idle_clock = got[0x04][0] if 0x04 in got else 0
|
|
664
677
|
pair = self._pick_pair(fn, got)
|
|
665
678
|
tg = self.targets[(fn, pair)]
|
|
666
679
|
speed = min(4_000_000, struct.unpack("<I", got[0x01])[0]) if 0x01 in got else 4_000_000
|
|
@@ -676,6 +689,7 @@ class Endpoint:
|
|
|
676
689
|
flags |= 2
|
|
677
690
|
self.conns[cid].speed = min(self.conns[cid].speed, speed)
|
|
678
691
|
c = self.conns[cid]
|
|
692
|
+
c.idle_clock = idle_clock # an existing connection takes the new rest level
|
|
679
693
|
c.users.add("host")
|
|
680
694
|
for n, s in self.slots.items(): # a new connection for an evicted slot: a new cue
|
|
681
695
|
if s.wire_fn == fn and s.pair == pair:
|
|
@@ -923,7 +937,7 @@ class Endpoint:
|
|
|
923
937
|
pos = s.end
|
|
924
938
|
elif frm == 3:
|
|
925
939
|
hits = [mk for mk in s.marks if arg == 0 or mk[2] == arg]
|
|
926
|
-
pos = hits[-1][1] if hits else s.
|
|
940
|
+
pos = hits[-1][1] if hits else s.end # no such mark: from now (common §1.2)
|
|
927
941
|
else:
|
|
928
942
|
raise Reject(m.UNSUPPORTED)
|
|
929
943
|
flags = 0
|
|
@@ -1218,7 +1232,7 @@ class Endpoint:
|
|
|
1218
1232
|
|
|
1219
1233
|
def _parse_slot(self, v: bytes) -> Slot:
|
|
1220
1234
|
t = Take(v)
|
|
1221
|
-
n, wire_fn, swdio, swclk, attach, retry_s, mech, name_len = t.take("
|
|
1235
|
+
n, wire_fn, swdio, swclk, attach, retry_s, max_speed, idle_clock, mech, name_len = t.take("BHHHBHIBBB")
|
|
1222
1236
|
name = t.bytes(name_len)
|
|
1223
1237
|
scheme = t.take("B")
|
|
1224
1238
|
rest = v[t.at:]
|
|
@@ -1226,6 +1240,8 @@ class Endpoint:
|
|
|
1226
1240
|
raise Reject(m.MALFORMED)
|
|
1227
1241
|
if retry_s and attach != SLOT_ATTACH["at_boot"]:
|
|
1228
1242
|
raise Reject(m.MALFORMED)
|
|
1243
|
+
if idle_clock > 1 or (idle_clock and self.names.get(wire_fn) != "oep.wire.rvswd"):
|
|
1244
|
+
raise Reject(m.MALFORMED)
|
|
1229
1245
|
if not SLOT_NAME.fullmatch(name.decode("ascii", "replace")):
|
|
1230
1246
|
raise Reject(m.MALFORMED)
|
|
1231
1247
|
if self.names.get(wire_fn) not in WIRES or (swdio, swclk) not in self.pairs.get(wire_fn, []):
|
|
@@ -1239,7 +1255,7 @@ class Endpoint:
|
|
|
1239
1255
|
lock = (scheme, rest[:len(rest) // 2], rest[len(rest) // 2:])
|
|
1240
1256
|
elif rest:
|
|
1241
1257
|
raise Reject(m.MALFORMED)
|
|
1242
|
-
return Slot(n, wire_fn, (swdio, swclk), attach, retry_s, mech, name.decode(), lock)
|
|
1258
|
+
return Slot(n, wire_fn, (swdio, swclk), attach, retry_s, max_speed, idle_clock, mech, name.decode(), lock)
|
|
1243
1259
|
|
|
1244
1260
|
def _parse_bind(self, v: bytes, slots: dict[int, Slot]) -> Bind:
|
|
1245
1261
|
t = Take(v)
|
|
@@ -1291,7 +1307,9 @@ class Endpoint:
|
|
|
1291
1307
|
if not tg.present:
|
|
1292
1308
|
return
|
|
1293
1309
|
try:
|
|
1294
|
-
|
|
1310
|
+
speed = min(4_000_000, s.max_speed) if s.max_speed else 4_000_000 # the slot's line settings
|
|
1311
|
+
cid = self._seat(s.wire_fn, s.pair, tg, speed, evict=False) # automatic: never evicts
|
|
1312
|
+
self.conns[cid].idle_clock = s.idle_clock
|
|
1295
1313
|
except Reject:
|
|
1296
1314
|
return
|
|
1297
1315
|
tg.havereset = False
|
|
@@ -185,8 +185,8 @@ def esp32_v003() -> FakeProbe:
|
|
|
185
185
|
_core("3.0.0", "esp32-d0wd", bytes.fromhex("0070070d9394"), 40, reserved, f"{NS}.esp32-v003",
|
|
186
186
|
{16: "SWIO", 23: "NRST", 22: "DUT TX", 21: "DUT RX"},
|
|
187
187
|
_transports([(TRANSPORT["uart_bridge"], 0xFF)])),
|
|
188
|
-
Offered(1, 1, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.
|
|
189
|
-
catalog.u8(MAX_CONNECTIONS, 1))),
|
|
188
|
+
Offered(1, 1, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.role_channels(3, [23]),
|
|
189
|
+
catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
|
|
190
190
|
Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b0111), catalog.u8(IMPLEMENTATION, 1),
|
|
191
191
|
catalog.u16(MAX_LENGTH, 40))),
|
|
192
192
|
Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
|
|
@@ -77,8 +77,8 @@ def build(a: argparse.Namespace) -> endpoint.Endpoint:
|
|
|
77
77
|
swdio, swclk = ep.pairs[wire][n]
|
|
78
78
|
mech = 2 if 2 in ep.mechanisms else min(ep.mechanisms)
|
|
79
79
|
raw = name.encode()
|
|
80
|
-
value = struct.pack("<
|
|
81
|
-
mech, len(raw)) + raw + b"\x00"
|
|
80
|
+
value = struct.pack("<BHHHBHIBBB", n, wire, swdio, swclk, reg.PROBE_CONFIG.enum["slot_attach"]["at_boot"], 1,
|
|
81
|
+
0, 0, mech, len(raw)) + raw + b"\x00" # no speed ceiling, rests high, no lock
|
|
82
82
|
items.append(bytes([_ITEM["slot"], len(value)]) + value)
|
|
83
83
|
if a.bind:
|
|
84
84
|
streams = b"".join(struct.pack("<BH", reg.PROBE_CONFIG.enum["bind_stream"]["slot_console"], n)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from types import SimpleNamespace as _NS
|
|
4
4
|
|
|
5
|
-
REGISTRY_HASH = "
|
|
5
|
+
REGISTRY_HASH = "049bd156b4775990"
|
|
6
6
|
SCHEMA = 1
|
|
7
7
|
PROTOCOL_REVISION = 1
|
|
8
8
|
CONFIRM_REQUEST_MAGIC = 'OEP?'
|
|
@@ -22,13 +22,13 @@ TIMING = {"resync_quiet_ms": 0x32, "probe_frame_gap_ms": 0xC8, "host_frame_pause
|
|
|
22
22
|
|
|
23
23
|
INTERFACES = {}
|
|
24
24
|
CORE = _NS(name="oep.core", revision=1, op={"confirm": 0x01, "list": 0x02, "describe": 0x03, "plan_apply": 0x04, "plan_release": 0x05, "open": 0x10, "end": 0x11, "keepalive": 0x12, "lock_state": 0x13, "subscribe": 0x30, "unsubscribe": 0x32, "link_source": 0x40, "link_sink": 0x41}, lock_free={0x01, 0x02, 0x03, 0x10, 0x13, 0x40, 0x41},
|
|
25
|
-
closed_tail={0x40}, tlv={"plan_apply": {"role_assignment": 0x90}, "open": {"owner": 0x01}, "lock_state_answer": {"owner": 0x01}, "locked_payload": {"owner": 0x01}, "describe": {"firmware": 0x40, "model": 0x41, "unit_id": 0x42, "channels": 0x43, "reserved": 0x44, "profile": 0x45, "label": 0x46, "resets_on_open": 0x47, "transport": 0x49, "oep_pid": 0x4A}}, event={"heartbeat": 0x01}, enum={"transport_kind": {"uart_bridge": 0x01, "usb_cdc": 0x02, "usb_serial_jtag": 0x03, "vendor_bulk": 0x04, "hid": 0x05, "tcp": 0x06}}, own={})
|
|
25
|
+
closed_tail={0x40}, tlv={"plan_apply": {"role_assignment": 0x90}, "open": {"owner": 0x01}, "lock_state_answer": {"owner": 0x01}, "locked_payload": {"owner": 0x01}, "describe": {"firmware": 0x40, "model": 0x41, "unit_id": 0x42, "channels": 0x43, "reserved": 0x44, "profile": 0x45, "label": 0x46, "resets_on_open": 0x47, "transport": 0x49, "oep_pid": 0x4A, "plan_roles": 0x4B}}, event={"heartbeat": 0x01}, enum={"transport_kind": {"uart_bridge": 0x01, "usb_cdc": 0x02, "usb_serial_jtag": 0x03, "vendor_bulk": 0x04, "hid": 0x05, "tcp": 0x06}}, own={})
|
|
26
26
|
INTERFACES["oep.core"] = CORE
|
|
27
27
|
WIRE_RVSWD = _NS(name="oep.wire.rvswd", revision=1, op={"scan": 0x01, "attach": 0x02, "detach": 0x03, "attach_under_reset": 0x04, "connections": 0x05}, lock_free={0x05},
|
|
28
|
-
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03}, "attach_answer": {"target_id": 0x10}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "target_id_scheme": {"wch_dmi_7f": 0x01}, "attach_method": {"run": 0x00, "halt": 0x01}, "pin_role": {"swdio": 0x01, "swclk": 0x02}}, own={})
|
|
28
|
+
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03, "idle_clock": 0x04}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03, "idle_clock": 0x04}, "attach_answer": {"target_id": 0x10}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "target_id_scheme": {"wch_dmi_7f": 0x01}, "attach_method": {"run": 0x00, "halt": 0x01}, "pin_role": {"swdio": 0x01, "swclk": 0x02, "reset": 0x03}, "idle_clock": {"high": 0x00, "low": 0x01}}, own={})
|
|
29
29
|
INTERFACES["oep.wire.rvswd"] = WIRE_RVSWD
|
|
30
30
|
WIRE_SWIO = _NS(name="oep.wire.swio", revision=1, op={"scan": 0x01, "attach": 0x02, "detach": 0x03, "attach_under_reset": 0x04, "connections": 0x05}, lock_free={0x05},
|
|
31
|
-
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03}, "attach_answer": {"target_id": 0x10}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "target_id_scheme": {"wch_dmi_7f": 0x01}, "attach_method": {"run": 0x00, "halt": 0x01}, "pin_role": {"swdio": 0x01}}, own={})
|
|
31
|
+
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03}, "attach_answer": {"target_id": 0x10}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "target_id_scheme": {"wch_dmi_7f": 0x01}, "attach_method": {"run": 0x00, "halt": 0x01}, "pin_role": {"swdio": 0x01, "reset": 0x03}}, own={})
|
|
32
32
|
INTERFACES["oep.wire.swio"] = WIRE_SWIO
|
|
33
33
|
WIRE_SWD = _NS(name="oep.wire.swd", revision=1, op={"scan": 0x01, "attach": 0x02, "detach": 0x03, "connections": 0x05}, lock_free={0x05},
|
|
34
34
|
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "targetsel": 0x02, "pins": 0x03}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "pin_role": {"swdio": 0x01, "swclk": 0x02}}, own={})
|
|
@@ -14,8 +14,8 @@ import struct
|
|
|
14
14
|
import time
|
|
15
15
|
from dataclasses import dataclass, field
|
|
16
16
|
|
|
17
|
-
from . import host as h, message as m, registry as reg
|
|
18
|
-
from .core import Interface
|
|
17
|
+
from . import catalog, host as h, message as m, registry as reg
|
|
18
|
+
from .core import Interface, describe
|
|
19
19
|
from .fixture import Gpio
|
|
20
20
|
|
|
21
21
|
STATUS = reg.STATUS
|
|
@@ -109,7 +109,9 @@ class WireBase(Interface):
|
|
|
109
109
|
class Wire(WireBase):
|
|
110
110
|
"""oep.wire.rvswd / oep.wire.swio (CH32 debug links to a RISC-V debug module)."""
|
|
111
111
|
NAME = "oep.wire.rvswd"
|
|
112
|
-
|
|
112
|
+
ROLE_RESET = reg.WIRE_RVSWD.enum["pin_role"]["reset"]
|
|
113
|
+
TAG_IDLE_CLOCK = reg.WIRE_RVSWD.tlv["attach"]["idle_clock"]
|
|
114
|
+
IDLE_CLOCK = reg.WIRE_RVSWD.enum["idle_clock"]
|
|
113
115
|
RUN, HALT = reg.WIRE_RVSWD.enum["attach_method"]["run"], reg.WIRE_RVSWD.enum["attach_method"]["halt"]
|
|
114
116
|
TAG_TARGET_ID = reg.WIRE_RVSWD.tlv["attach_answer"]["target_id"]
|
|
115
117
|
SCHEME_WCH_DMI_7F = reg.WIRE_RVSWD.enum["target_id_scheme"]["wch_dmi_7f"]
|
|
@@ -125,12 +127,28 @@ class Wire(WireBase):
|
|
|
125
127
|
v = tail.get(self.TAG_TARGET_ID)
|
|
126
128
|
self.target_id = (v[0], bytes(v[1:])) if v else None
|
|
127
129
|
|
|
128
|
-
def
|
|
130
|
+
def _idle_tlv(self, idle_clock: str | None) -> bytes:
|
|
131
|
+
# rvswd only, critical: a probe that cannot rest the line that way must refuse (oep-if-debug §3)
|
|
132
|
+
return b"" if idle_clock is None else m.tlv(self.TAG_IDLE_CLOCK, bytes([self.IDLE_CLOCK[idle_clock]]), critical=True)
|
|
133
|
+
|
|
134
|
+
def reset_channels(self) -> list[int]:
|
|
135
|
+
"""The channels attach_under_reset may take (describe role_channels, role reset). There is no default reset
|
|
136
|
+
line: the host names one every time (oep-if-debug §3)."""
|
|
137
|
+
out = set()
|
|
138
|
+
for tag, v in describe(self.host, self.fn):
|
|
139
|
+
if tag & ~m.TAG_CRITICAL == catalog.ROLE_CHANNELS and v[0] == self.ROLE_RESET:
|
|
140
|
+
out.update(catalog.bitmap_to_channels(struct.unpack_from("<H", v, 1)[0], v[3:]))
|
|
141
|
+
return sorted(out)
|
|
142
|
+
|
|
143
|
+
def attach(self, halt: bool = True, max_speed: int | None = None, pins: tuple[int, int] | None = None,
|
|
144
|
+
idle_clock: str | None = None) -> tuple[int, int]:
|
|
129
145
|
"""-> (connection, DMSTATUS). Attaching an attached wire returns its connection as it is (self.existing).
|
|
130
146
|
self.had_reset: a pending havereset was acknowledged first (a V00x's DMSTATUS halt / run bits stay frozen
|
|
131
147
|
until then); self.speed_hz: the speed the probe chose; max_speed: a ceiling the probe must keep (critical);
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
idle_clock: "high" / "low", how rvswd rests SWCLK (critical). Both are the target's, known by the host
|
|
149
|
+
(oep-if-debug §3). self.target_id: (scheme, value) of the target's identity when the probe could read one."""
|
|
150
|
+
body = (bytes([self.HALT if halt else self.RUN]) + self._speed_tlv(max_speed) + self._pins_tlv(pins)
|
|
151
|
+
+ self._idle_tlv(idle_clock))
|
|
134
152
|
rd = m.Reader(self._call(self.ATTACH, body).payload)
|
|
135
153
|
conn, status, flags, self.speed_hz = rd.take("HIBI")
|
|
136
154
|
self.had_reset, self.existing = bool(flags & 1), bool(flags & 2)
|
|
@@ -139,12 +157,13 @@ class Wire(WireBase):
|
|
|
139
157
|
self._take_target_id(tail)
|
|
140
158
|
return conn, status
|
|
141
159
|
|
|
142
|
-
def attach_under_reset(self, channel: int
|
|
143
|
-
|
|
144
|
-
"""Hold the target in reset through `channel` (
|
|
145
|
-
halt it at once - the way back from firmware that turns the
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
def attach_under_reset(self, channel: int, hold_ms: int = 20, max_speed: int | None = None,
|
|
161
|
+
pins: tuple[int, int] | None = None, idle_clock: str | None = None) -> tuple[int, int]:
|
|
162
|
+
"""Hold the target in reset through `channel` (always named: there is no default reset line; the probe
|
|
163
|
+
allows reset_channels()), attach, release and halt it at once - the way back from firmware that turns the
|
|
164
|
+
debug pins into GPIOs. -> (connection, dpc)"""
|
|
165
|
+
body = (struct.pack("<HH", channel, hold_ms) + self._speed_tlv(max_speed) + self._pins_tlv(pins)
|
|
166
|
+
+ self._idle_tlv(idle_clock))
|
|
148
167
|
rd = m.Reader(self._call(self.ATTACH_UNDER_RESET, body).payload)
|
|
149
168
|
conn, dpc, self.speed_hz = rd.take("HII")
|
|
150
169
|
self._take_target_id(rd.tail())
|
|
@@ -10,6 +10,7 @@ transfer; if its timeout hits first, pyusb raises and the bytes already received
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import atexit
|
|
13
14
|
import threading
|
|
14
15
|
import time
|
|
15
16
|
|
|
@@ -114,15 +115,21 @@ class UsbAsyncStream:
|
|
|
114
115
|
self.timeout = 0.05
|
|
115
116
|
self._buffer = bytearray()
|
|
116
117
|
self._cond = threading.Condition()
|
|
117
|
-
self._closed = False
|
|
118
|
+
self._closed = False # no more resubmits (close() started)
|
|
119
|
+
self._stop = False # the event thread ends (every transfer is back)
|
|
118
120
|
self._transfers = []
|
|
121
|
+
self._active = 0 # transfers submitted and not yet back for good
|
|
119
122
|
for _ in range(self.DEPTH):
|
|
120
123
|
t = handle.getTransfer()
|
|
121
124
|
t.setBulk(endpoint_in, self.URB_SIZE, callback=self._done, timeout=0)
|
|
122
125
|
t.submit()
|
|
123
126
|
self._transfers.append(t)
|
|
127
|
+
self._active += 1
|
|
124
128
|
self._events = threading.Thread(target=self._run, daemon=True)
|
|
125
129
|
self._events.start()
|
|
130
|
+
# Closed at exit too: left to usb1's own finalizers, the context went while the event thread was inside
|
|
131
|
+
# handleEventsTimeout with transfers out - the process hung at exit or libusb aborted (usbi_mutex_destroy).
|
|
132
|
+
atexit.register(self.close)
|
|
126
133
|
|
|
127
134
|
@classmethod
|
|
128
135
|
def open(cls, vid: int, pid: int, serial: str | None = None) -> "UsbAsyncStream":
|
|
@@ -158,14 +165,23 @@ class UsbAsyncStream:
|
|
|
158
165
|
self._buffer += data
|
|
159
166
|
self._cond.notify_all()
|
|
160
167
|
if not self._closed and status in (usb1.TRANSFER_COMPLETED, usb1.TRANSFER_TIMED_OUT):
|
|
161
|
-
|
|
168
|
+
try:
|
|
169
|
+
transfer.submit()
|
|
170
|
+
return
|
|
171
|
+
except usb1.USBError:
|
|
172
|
+
pass
|
|
173
|
+
with self._cond:
|
|
174
|
+
self._active -= 1 # back for good (cancelled, failed, or closing)
|
|
175
|
+
self._cond.notify_all()
|
|
162
176
|
|
|
163
177
|
def _run(self) -> None:
|
|
164
|
-
while not self.
|
|
178
|
+
while not self._stop:
|
|
165
179
|
try:
|
|
166
180
|
self.context.handleEventsTimeout(0.1)
|
|
167
181
|
except self._usb1.USBErrorInterrupted:
|
|
168
182
|
continue
|
|
183
|
+
except self._usb1.USBError:
|
|
184
|
+
break
|
|
169
185
|
|
|
170
186
|
@property
|
|
171
187
|
def in_waiting(self) -> int:
|
|
@@ -195,15 +211,27 @@ class UsbAsyncStream:
|
|
|
195
211
|
self._buffer.clear()
|
|
196
212
|
|
|
197
213
|
def close(self) -> None:
|
|
214
|
+
"""Cancel the IN transfers and let the event thread take them back before the handle and the context go
|
|
215
|
+
(closing with transfers still out hung the process or aborted libusb). Safe to call twice (atexit)."""
|
|
216
|
+
if self._closed:
|
|
217
|
+
return
|
|
198
218
|
self._closed = True
|
|
219
|
+
atexit.unregister(self.close)
|
|
199
220
|
for t in self._transfers:
|
|
200
221
|
try:
|
|
201
222
|
t.cancel()
|
|
202
223
|
except self._usb1.USBError:
|
|
203
224
|
pass
|
|
204
|
-
|
|
225
|
+
deadline = time.monotonic() + 1.0
|
|
226
|
+
with self._cond:
|
|
227
|
+
while self._active > 0 and self._events.is_alive() and time.monotonic() < deadline:
|
|
228
|
+
self._cond.wait(0.05)
|
|
229
|
+
self._stop = True
|
|
230
|
+
self._events.join(timeout=1.0)
|
|
205
231
|
try:
|
|
206
232
|
self.handle.releaseInterface(self.interface)
|
|
233
|
+
except self._usb1.USBError:
|
|
234
|
+
pass
|
|
207
235
|
finally:
|
|
208
236
|
self.handle.close()
|
|
209
237
|
self.context.close()
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import struct
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from oep_client import __main__ as cli, config, endpoint, fake, host as h, message as m
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class Clock:
|
|
@@ -97,3 +99,55 @@ def test_slot_takes_the_only_wire_by_default(monkeypatch):
|
|
|
97
99
|
assert cli.main(["config", "slot", "x", "--name", "v003", "--attach", "at-boot", "--retry", "1"]) == 0
|
|
98
100
|
(slot,) = [i for i in config.ProbeConfig(hst).items() if isinstance(i, config.Slot)]
|
|
99
101
|
assert slot.wire_fn == 1 and slot.pins == (16, 0xFFFF)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_a_plan_refused_for_a_saved_plan_names_the_holder():
|
|
105
|
+
import pytest
|
|
106
|
+
from oep_client import core
|
|
107
|
+
ep, hst = open_bench()
|
|
108
|
+
cfg = config.ProbeConfig(hst)
|
|
109
|
+
cfg.set([config.Plan(5, 1, 20), config.Plan(5, 2, 21)]) # fixture.uart keeps 20 / 21 as a setting
|
|
110
|
+
with pytest.raises(core.PinsTaken) as e:
|
|
111
|
+
core.plan_apply(hst, [(4, 1, 21)]) # gpio wants 21
|
|
112
|
+
assert e.value.holders == [(21, "the saved plan of fn 5 (role 2)")] and "fn 5" in str(e.value)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_a_settings_plan_is_not_the_sessions():
|
|
116
|
+
"""core §8: plan_release leaves a plan the settings put in (n = 0 too); plan_apply naming its fn is refused."""
|
|
117
|
+
ep, hst = open_bench()
|
|
118
|
+
config.ProbeConfig(hst).set([config.Plan(5, 1, 20), config.Plan(5, 2, 21)])
|
|
119
|
+
assert 5 in ep.plan_from_config
|
|
120
|
+
with pytest.raises(h.Rejected) as e:
|
|
121
|
+
hst.call(m.CORE_FN, m.OP_PLAN_APPLY, m.tlv(0x90, struct.pack("<HBH", 5, 1, 22)))
|
|
122
|
+
assert e.value.result.detail == m.UNAVAILABLE
|
|
123
|
+
hst.call(m.CORE_FN, m.OP_PLAN_RELEASE, b"\x00")
|
|
124
|
+
assert {(5, 1, 20), (5, 2, 21)} <= ep.plan and 5 in ep.plan_from_config
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_reset_channels_and_a_named_reset_line():
|
|
128
|
+
"""oep-if-debug §3: the host reads the reset channels the probe allows and names one; there is no default."""
|
|
129
|
+
from oep_client import target
|
|
130
|
+
ep = endpoint.Endpoint(fake.esp32_v003(), Clock())
|
|
131
|
+
hst = h.Host(lambda b: ep.handle(b, 0))
|
|
132
|
+
hst.open(3000)
|
|
133
|
+
wire = target.Wire(hst, "oep.wire.swio")
|
|
134
|
+
assert wire.reset_channels() == [23]
|
|
135
|
+
conn, _dpc = wire.attach_under_reset(23)
|
|
136
|
+
assert conn in ep.conns
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_slot_line_settings_from_the_command(capsys, monkeypatch):
|
|
140
|
+
ep, hst = open_bench()
|
|
141
|
+
|
|
142
|
+
class FakeLink:
|
|
143
|
+
def close(self):
|
|
144
|
+
pass
|
|
145
|
+
hst.link = FakeLink()
|
|
146
|
+
hst.end()
|
|
147
|
+
monkeypatch.setattr(cli.link, "open_host", lambda target: hst)
|
|
148
|
+
assert cli.main(["config", "slot", "x", "--name", "l103", "--pins", "2,3", "--attach", "at-boot", "--retry", "1",
|
|
149
|
+
"--max-speed", "1000000", "--idle-clock", "low"]) == 0
|
|
150
|
+
(slot,) = [i for i in config.ProbeConfig(hst).items() if isinstance(i, config.Slot)]
|
|
151
|
+
assert (slot.max_speed, slot.idle_clock) == (1_000_000, "low")
|
|
152
|
+
assert cli.main(["config", "show", "x"]) == 0
|
|
153
|
+
assert "max 1000000 Hz idle-low" in capsys.readouterr().out
|
|
@@ -49,10 +49,10 @@ class Host:
|
|
|
49
49
|
return r.payload
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def slot_item(n, wire, pair, attach=ATTACH["at_boot"], retry=1, mech=2, name=None, lock=None):
|
|
52
|
+
def slot_item(n, wire, pair, attach=ATTACH["at_boot"], retry=1, mech=2, name=None, lock=None, max_speed=0, idle=0):
|
|
53
53
|
raw = (name or f"s{n}").encode()
|
|
54
|
-
value = struct.pack("<
|
|
55
|
-
len(raw)) + raw
|
|
54
|
+
value = struct.pack("<BHHHBHIBBB", n, wire, *pair, attach, retry if attach == ATTACH["at_boot"] else 0, max_speed,
|
|
55
|
+
idle, mech, len(raw)) + raw
|
|
56
56
|
value += b"\x00" if lock is None else bytes([1]) + lock[0] + lock[1]
|
|
57
57
|
return m.tlv(ITEM["slot"], value)
|
|
58
58
|
|
|
@@ -198,7 +198,7 @@ def test_too_many_at_boot_slots_are_refused():
|
|
|
198
198
|
def test_slot_names_are_url_safe(name):
|
|
199
199
|
ep, h = bench()
|
|
200
200
|
assert h.raw(6, 0x02, slot_item(0, 1, ep.pairs[1][0], name=name or None) if name else
|
|
201
|
-
m.tlv(ITEM["slot"], struct.pack("<
|
|
201
|
+
m.tlv(ITEM["slot"], struct.pack("<BHHHBHIBBB", 0, 1, *ep.pairs[1][0], 1, 1, 0, 0, 2, 0) + b"\x00")).detail == m.MALFORMED
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
# ---- binds and the serial port ---------------------------------------------------------------------------
|
|
@@ -383,3 +383,51 @@ def test_the_closing_0x00_of_a_frame_is_not_raw_after_the_gap():
|
|
|
383
383
|
port.tick()
|
|
384
384
|
sid = ep.stream_keys[(ep._conn_at(1, p[0]), 2)]
|
|
385
385
|
assert bytes(ep.streams[sid].written) == b"" # no stray 0x00 at the target
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def test_read_from_a_last_mark_that_is_not_there_is_from_now():
|
|
389
|
+
ep, h = bench()
|
|
390
|
+
p = ep.pairs[1][0]
|
|
391
|
+
r = h.raw(1, 0x02, bytes([0]) + m.tlv(0x03, struct.pack("<HH", *p), critical=True))
|
|
392
|
+
uart_fn = 5
|
|
393
|
+
h.ok(0, m.OP_PLAN_APPLY, m.tlv(0x90, struct.pack("<HBH", uart_fn, 1, 20)) + m.tlv(0x90, struct.pack("<HBH", uart_fn, 2, 21)))
|
|
394
|
+
h.ok(uart_fn, 0x01, struct.pack("<I", 115200))
|
|
395
|
+
ep.uart_rx(uart_fn, b"old output")
|
|
396
|
+
reset = reg.TARGET_CONSOLE.enum["mark_kind"]["reset"]
|
|
397
|
+
got = h.raw(uart_fn, 0x02, struct.pack("<BQH", 3, reset, 64), session=False).payload
|
|
398
|
+
assert struct.unpack_from("<Q", got)[0] == 10 and got[9:] == b"" # no reset mark: from now, not the old bytes
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def test_no_default_reset_line_only_the_declared_channels():
|
|
402
|
+
"""oep-if-debug §3: attach_under_reset names its channel; the probe takes only role 3 (reset) channels, and not
|
|
403
|
+
one a plan holds."""
|
|
404
|
+
ep = endpoint.Endpoint(fake.esp32_v003(), Clock())
|
|
405
|
+
h = Host(ep)
|
|
406
|
+
h.open()
|
|
407
|
+
assert ep.reset_channels[1] == {23}
|
|
408
|
+
for channel in (0xFFFF, 22): # no default; 22 is not a reset line
|
|
409
|
+
assert h.raw(1, 0x04, struct.pack("<HH", channel, 20)).detail == m.UNAVAILABLE
|
|
410
|
+
h.ok(1, 0x04, struct.pack("<HH", 23, 20))
|
|
411
|
+
h.ok(0, m.OP_PLAN_APPLY, m.tlv(0x90, struct.pack("<HBH", 4, 1, 23))) # fixture.gpio takes NRST
|
|
412
|
+
assert h.raw(1, 0x04, struct.pack("<HH", 23, 20)).detail == m.UNAVAILABLE
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def test_idle_clock_is_rvswd_s_and_a_slot_carries_the_line_settings():
|
|
416
|
+
ep, h = bench()
|
|
417
|
+
pair = ep.pairs[1][0]
|
|
418
|
+
idle_low = m.tlv(0x04, b"\x01", critical=True)
|
|
419
|
+
h.ok(1, 0x02, b"\x01" + m.tlv(0x03, struct.pack("<HH", *pair), critical=True) + idle_low)
|
|
420
|
+
assert ep.conns[ep._conn_at(1, pair)].idle_clock == 1
|
|
421
|
+
assert h.raw(1, 0x02, b"\x01" + m.tlv(0x03, struct.pack("<HH", *pair), critical=True)
|
|
422
|
+
+ m.tlv(0x04, b"\x02", critical=True)).detail == m.MALFORMED
|
|
423
|
+
ep2, h2 = bench()
|
|
424
|
+
p2 = ep2.pairs[1][1]
|
|
425
|
+
h2.ok(6, 0x02, slot_item(0, 1, p2, name="l103", max_speed=1_000_000, idle=1))
|
|
426
|
+
c = ep2.conns[ep2._conn_at(1, p2)]
|
|
427
|
+
assert (c.speed, c.idle_clock) == (1_000_000, 1) # the probe's own attach uses the slot's settings
|
|
428
|
+
v003 = endpoint.Endpoint(fake.esp32_v003(), Clock())
|
|
429
|
+
hv = Host(v003)
|
|
430
|
+
hv.open()
|
|
431
|
+
assert hv.raw(9, 0x02, slot_item(0, 1, v003.pairs[1][0], idle=1)).detail == m.MALFORMED # swio has no idle_clock
|
|
432
|
+
r = hv.raw(1, 0x02, b"\x01" + idle_low)
|
|
433
|
+
assert r.resolution == m.REJECTED and r.detail == m.UNSUPPORTED # an unknown critical TLV on swio
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""UsbAsyncStream closes cleanly: the IN transfers are cancelled and taken back by the event thread before the handle and
|
|
2
|
+
the context go (closing with transfers out hung the process at exit or aborted libusb), and close is hooked to exit."""
|
|
3
|
+
|
|
4
|
+
import atexit
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import types
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FakeUsb1(types.ModuleType):
|
|
13
|
+
TRANSFER_COMPLETED, TRANSFER_TIMED_OUT, TRANSFER_CANCELLED = 0, 2, 3
|
|
14
|
+
|
|
15
|
+
class USBError(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class USBErrorInterrupted(USBError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
usb1 = FakeUsb1("usb1")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Transfer:
|
|
26
|
+
def __init__(self, ctx):
|
|
27
|
+
self.ctx, self.status, self.out = ctx, None, False
|
|
28
|
+
|
|
29
|
+
def setBulk(self, ep, size, callback, timeout):
|
|
30
|
+
self.callback = callback
|
|
31
|
+
|
|
32
|
+
def submit(self):
|
|
33
|
+
self.out = True
|
|
34
|
+
|
|
35
|
+
def cancel(self):
|
|
36
|
+
self.ctx.pending.append(self) # comes back as cancelled on a later event pass, like libusb
|
|
37
|
+
|
|
38
|
+
def getStatus(self):
|
|
39
|
+
return self.status
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Context:
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self.pending, self.lock, self.closed = [], threading.Lock(), False
|
|
45
|
+
|
|
46
|
+
def handleEventsTimeout(self, t):
|
|
47
|
+
import time
|
|
48
|
+
time.sleep(0.005)
|
|
49
|
+
while self.pending:
|
|
50
|
+
tr = self.pending.pop(0)
|
|
51
|
+
tr.out, tr.status = False, usb1.TRANSFER_CANCELLED
|
|
52
|
+
tr.callback(tr)
|
|
53
|
+
|
|
54
|
+
def close(self):
|
|
55
|
+
self.closed = True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Handle:
|
|
59
|
+
def __init__(self, ctx):
|
|
60
|
+
self.ctx, self.transfers, self.closed_with_out = ctx, [], None
|
|
61
|
+
|
|
62
|
+
def getTransfer(self):
|
|
63
|
+
t = Transfer(self.ctx)
|
|
64
|
+
self.transfers.append(t)
|
|
65
|
+
return t
|
|
66
|
+
|
|
67
|
+
def releaseInterface(self, n):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def close(self):
|
|
71
|
+
self.closed_with_out = sum(t.out for t in self.transfers)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.fixture
|
|
75
|
+
def stream(monkeypatch):
|
|
76
|
+
monkeypatch.setitem(sys.modules, "usb1", usb1)
|
|
77
|
+
from oep_client import usb_stream
|
|
78
|
+
ctx = Context()
|
|
79
|
+
h = Handle(ctx)
|
|
80
|
+
s = usb_stream.UsbAsyncStream(ctx, h, 0x81, 0x01, 0)
|
|
81
|
+
return s, ctx, h
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_close_takes_every_transfer_back_before_the_handle_goes(stream):
|
|
85
|
+
s, ctx, h = stream
|
|
86
|
+
s.close()
|
|
87
|
+
assert h.closed_with_out == 0 and ctx.closed and not s._events.is_alive()
|
|
88
|
+
s.close() # twice (atexit after an explicit close): nothing more
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_close_is_hooked_to_exit(stream, monkeypatch):
|
|
92
|
+
s, ctx, h = stream
|
|
93
|
+
calls = []
|
|
94
|
+
monkeypatch.setattr(atexit, "unregister", lambda f: calls.append(f))
|
|
95
|
+
s.close()
|
|
96
|
+
assert calls == [s.close] # it was registered, and an explicit close takes it off
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|