oep-client-python 0.0.3__tar.gz → 0.0.5__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.3 → oep_client_python-0.0.5}/CHANGELOG.md +16 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/PKG-INFO +2 -2
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/README.ja.md +1 -1
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/README.md +1 -1
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/pyproject.toml +1 -1
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/__init__.py +1 -1
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/__main__.py +68 -3
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/core.py +41 -1
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/endpoint.py +1 -1
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/fake_serial.py +4 -2
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/usb_stream.py +32 -4
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_config.py +43 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_fake_spec.py +26 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_link_serial.py +7 -2
- oep_client_python-0.0.5/tests/test_usb_stream.py +96 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/.gitignore +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/LICENSE +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/arm.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/capture.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/catalog.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/ch32_flash.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/cobs.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/config.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/console.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/decode.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/dump.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/esp32_targets.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/fake.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/fake_serve.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/fixture.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/frames.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/hid_stream.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/host.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/interfaces.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/link.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/message.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/names.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/registry.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/riscv.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/rp2350.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/target.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/src/oep_client/uiapduino.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_capabilities.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_capture.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_cobs.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_esp32_targets.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_flash_console.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_hid_stream.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_interfaces.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_link_host.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_session.py +0 -0
- {oep_client_python-0.0.3 → oep_client_python-0.0.5}/tests/test_target_parts.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.5
|
|
6
|
+
- (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.
|
|
7
|
+
- (JA) USB(python-libusb1)の stream は、終了時にも片付いて閉じる: IN の転送を取り消し、event thread が取り戻してから handle と context を閉じる。link を閉じずに終わったプログラムが終了で固まったり、libusb が abort したりしていた。
|
|
8
|
+
- (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).
|
|
9
|
+
- (JA) `core.plan_apply` が unavailable で断られたとき、設定から分かれば何がピンを持っているかを示す(`core.PinsTaken`: 別の fn の保存した plan、スロット)。
|
|
10
|
+
- (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).
|
|
11
|
+
- (JA) 偽の probe: その kind のマークが無いときの「最後のマークから」の read は、一番古い位置ではなく今から(oep-if-common §1.2)。
|
|
12
|
+
|
|
13
|
+
## 0.0.4
|
|
14
|
+
- (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).
|
|
15
|
+
- (JA) 偽のシリアルの口: 0x00 だけの候補は、200 ms の後も生のバイトにしない(probe の firmware にも同じバグがあり、target のコンソールに 0x00 が届いていた)。
|
|
16
|
+
- (EN) `oep config slot` takes the probe's only RISC-V wire when `--wire` is left out (a SWIO-only probe needed `--wire swio`).
|
|
17
|
+
- (JA) `oep config slot` は `--wire` を省くと、probe の唯一の RISC-V の線を使う(SWIO だけの probe で `--wire swio` が要っていた)。
|
|
18
|
+
- (EN) `oep config plan <probe> <fn | name#k> ROLE=CH ...` (role names from the registry: rx, tx, line...), `oep config label` and `oep config idle`.
|
|
19
|
+
- (JA) `oep config plan <probe> <fn | 名前#k> ROLE=CH ...`(role の名前は registry から: rx、tx、line など)、`oep config label`、`oep config idle`。
|
|
20
|
+
|
|
5
21
|
## 0.0.3
|
|
6
22
|
- (EN) `oep_client.config` (oep.probe.config: slots, binds, plan / label / idle items, get / set / save / erase, the live slot and bind state) and the `oep config show | slot | bind | remove | save | erase` command. An English README.md, also the PyPI page.
|
|
7
23
|
- (JA) `oep_client.config`(oep.probe.config: スロット、bind、plan / label / idle の項目、get / set / save / erase、スロットと bind の今の状態)と、`oep config show | slot | bind | remove | save | erase` の命令。英語の README.md(PyPI のページも)。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: oep-client-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
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.5"
|
|
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"
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
oep config show <probe>
|
|
5
5
|
oep config slot <probe> --name x035 --wire rvswd --pins 2,54 --attach at-boot --retry 1 --mechanism dmseq
|
|
6
6
|
oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
|
|
7
|
+
oep config plan <probe> oep.fixture.uart#2 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
|
|
7
8
|
oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
|
|
8
9
|
|
|
9
10
|
<probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
|
|
@@ -58,7 +59,7 @@ def _config_parser(sub) -> None:
|
|
|
58
59
|
slot.add_argument("probe")
|
|
59
60
|
slot.add_argument("--slot", type=int, default=0, help="the slot number (default 0)")
|
|
60
61
|
slot.add_argument("--name", required=True, help="1-32 of a-z 0-9 - _ (the oep://<probe>/<name> address)")
|
|
61
|
-
slot.add_argument("--wire",
|
|
62
|
+
slot.add_argument("--wire", help="rvswd or swio (or an fn); default: the probe's only wire")
|
|
62
63
|
slot.add_argument("--pins", help="swdio,swclk (one pin on swio); default: the wire's only pin set")
|
|
63
64
|
slot.add_argument("--attach", choices=sorted(config.ATTACH), default="host")
|
|
64
65
|
slot.add_argument("--retry", type=int, default=0, help="at-boot: try again every N s while absent (0: never)")
|
|
@@ -72,6 +73,21 @@ def _config_parser(sub) -> None:
|
|
|
72
73
|
bind.add_argument("--stream", action="append", required=True, help="slot:NAME, slot:N or uart:FN (repeatable)")
|
|
73
74
|
bind.add_argument("--select", type=int, default=0, help="manual: the stream it carries (index in --stream)")
|
|
74
75
|
bind.add_argument("--save", action="store_true")
|
|
76
|
+
plan = cs.add_parser("plan", help="the pins an interface keeps (its whole plan, kept as a setting)")
|
|
77
|
+
plan.add_argument("probe")
|
|
78
|
+
plan.add_argument("fn", help="an fn, or an interface name with #k for its k-th instance (1 = the first)")
|
|
79
|
+
plan.add_argument("roles", nargs="+", help="ROLE=CHANNEL, ROLE a number or the interface's role name (rx, tx, line...)")
|
|
80
|
+
plan.add_argument("--save", action="store_true")
|
|
81
|
+
label = cs.add_parser("label", help="name a channel (shown in oep.core's describe)")
|
|
82
|
+
label.add_argument("probe")
|
|
83
|
+
label.add_argument("channel", type=int)
|
|
84
|
+
label.add_argument("text")
|
|
85
|
+
label.add_argument("--save", action="store_true")
|
|
86
|
+
idle = cs.add_parser("idle", help="the state of a free pin")
|
|
87
|
+
idle.add_argument("probe")
|
|
88
|
+
idle.add_argument("channel", type=int)
|
|
89
|
+
idle.add_argument("mode", choices=sorted(config.IDLE))
|
|
90
|
+
idle.add_argument("--save", action="store_true")
|
|
75
91
|
rm = cs.add_parser("remove", help="remove one item: slot N, bind PORT, plan FN, label CH, idle CH")
|
|
76
92
|
rm.add_argument("probe")
|
|
77
93
|
rm.add_argument("kind", choices=["slot", "bind", "plan", "label", "idle"])
|
|
@@ -95,8 +111,40 @@ def _pins(hst, fn: int, text: str | None, wire: str) -> tuple[int, int]:
|
|
|
95
111
|
return groups[0]
|
|
96
112
|
|
|
97
113
|
|
|
98
|
-
def
|
|
99
|
-
|
|
114
|
+
def _plan_fn(hst, spec: str) -> tuple[int, dict[str, int]]:
|
|
115
|
+
"""fn and its role names (from the registry) for `spec`: an fn number, or name[#k] (k-th instance, 1-based)."""
|
|
116
|
+
from . import registry as reg
|
|
117
|
+
if spec.isdigit():
|
|
118
|
+
fn = int(spec)
|
|
119
|
+
name = next((e.name for e in core.list_entries(hst) if e.fn == fn), "")
|
|
120
|
+
else:
|
|
121
|
+
name, _, k = spec.partition("#")
|
|
122
|
+
fns = core.find_all(hst, name)
|
|
123
|
+
if not fns:
|
|
124
|
+
raise SystemExit(f"the probe offers no {name}")
|
|
125
|
+
index = int(k) - 1 if k else 0
|
|
126
|
+
if not 0 <= index < len(fns):
|
|
127
|
+
raise SystemExit(f"{name}: {len(fns)} instance(s), fns {fns}")
|
|
128
|
+
fn = fns[index]
|
|
129
|
+
iface = reg.INTERFACES.get(name)
|
|
130
|
+
roles = dict(iface.enum.get("role", {})) if iface else {}
|
|
131
|
+
return fn, roles
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _wire_fn(hst, wire: str | None) -> int:
|
|
135
|
+
if wire is None:
|
|
136
|
+
wires = [e for e in core.list_entries(hst, "oep.wire") if e.name in ("oep.wire.rvswd", "oep.wire.swio")]
|
|
137
|
+
if len(wires) != 1:
|
|
138
|
+
raise SystemExit("this probe has " + (", ".join(f"{e.name} (fn {e.fn})" for e in wires) or "no RISC-V wire")
|
|
139
|
+
+ ": name one with --wire")
|
|
140
|
+
return wires[0].fn
|
|
141
|
+
name = wire if wire.startswith("oep.") else f"oep.wire.{wire}"
|
|
142
|
+
if wire.isdigit():
|
|
143
|
+
return int(wire)
|
|
144
|
+
try:
|
|
145
|
+
return core.find(hst, name)
|
|
146
|
+
except LookupError:
|
|
147
|
+
raise SystemExit(f"this probe does not offer {name}") from None
|
|
100
148
|
|
|
101
149
|
|
|
102
150
|
def _stream(spec: str, slots: dict[str, int]) -> tuple[str, int]:
|
|
@@ -129,6 +177,7 @@ def _config(args) -> int:
|
|
|
129
177
|
return _show(hst, cfg, args.json)
|
|
130
178
|
if args.action == "slot":
|
|
131
179
|
fn = _wire_fn(hst, args.wire)
|
|
180
|
+
args.wire = args.wire or str(fn)
|
|
132
181
|
lock = None
|
|
133
182
|
if args.lock:
|
|
134
183
|
mask, _, value = args.lock.partition(":")
|
|
@@ -140,6 +189,22 @@ def _config(args) -> int:
|
|
|
140
189
|
slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
|
|
141
190
|
it = config.Bind(args.port, args.mode, [_stream(s, slots) for s in args.stream], args.select)
|
|
142
191
|
_change(hst, cfg, [it], args.save)
|
|
192
|
+
elif args.action == "plan":
|
|
193
|
+
fn, roles = _plan_fn(hst, args.fn)
|
|
194
|
+
items = []
|
|
195
|
+
for spec in args.roles:
|
|
196
|
+
role, _, ch = spec.partition("=")
|
|
197
|
+
if not ch:
|
|
198
|
+
raise SystemExit(f"{spec}: want ROLE=CHANNEL")
|
|
199
|
+
number = int(role) if role.isdigit() else roles.get(role.lower())
|
|
200
|
+
if number is None:
|
|
201
|
+
raise SystemExit(f"{role}: not a role of fn {fn} (roles: {', '.join(sorted(roles)) or 'numbers only'})")
|
|
202
|
+
items.append(config.Plan(fn, number, int(ch, 0)))
|
|
203
|
+
_change(hst, cfg, items, args.save)
|
|
204
|
+
elif args.action == "label":
|
|
205
|
+
_change(hst, cfg, [config.Label(args.channel, args.text)], args.save)
|
|
206
|
+
elif args.action == "idle":
|
|
207
|
+
_change(hst, cfg, [config.Idle(args.channel, args.mode)], args.save)
|
|
143
208
|
elif args.action == "remove":
|
|
144
209
|
_change(hst, cfg, [config.remove(args.kind, args.key)], args.save)
|
|
145
210
|
else:
|
|
@@ -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:
|
|
@@ -923,7 +923,7 @@ class Endpoint:
|
|
|
923
923
|
pos = s.end
|
|
924
924
|
elif frm == 3:
|
|
925
925
|
hits = [mk for mk in s.marks if arg == 0 or mk[2] == arg]
|
|
926
|
-
pos = hits[-1][1] if hits else s.
|
|
926
|
+
pos = hits[-1][1] if hits else s.end # no such mark: from now (common §1.2)
|
|
927
927
|
else:
|
|
928
928
|
raise Reject(m.UNSUPPORTED)
|
|
929
929
|
flags = 0
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
- a candidate runs from a 0x00 to the next 0x00; if it decodes and its CRC matches it is a request, otherwise it
|
|
7
7
|
(with its leading 0x00) is raw bytes, and the closing 0x00 starts the next candidate; bytes outside a candidate
|
|
8
|
-
are raw at once; a candidate that stops for 200 ms (`tick`) is raw;
|
|
8
|
+
are raw at once; a candidate that stops for 200 ms (`tick`) is raw; a candidate that is only its 0x00 (0x00 0x00,
|
|
9
|
+
or the closing 0x00 of a frame when nothing follows) is nothing
|
|
9
10
|
- raw bytes go to the endpoint's bind for this port (`Endpoint.port_input`)
|
|
10
11
|
- answers go out as 0x00 <COBS> 0x00, ahead of raw chunks (`Endpoint.port_output`); one writer, never a raw byte
|
|
11
12
|
inside a frame
|
|
@@ -80,7 +81,8 @@ class FakeSerialPort:
|
|
|
80
81
|
def _gap(self, now: int) -> None:
|
|
81
82
|
if self.cand is not None and now - self.last_ms >= GAP_MS:
|
|
82
83
|
raw, self.cand = bytes(self.cand), None
|
|
83
|
-
|
|
84
|
+
if len(raw) > 1: # only its 0x00: a delimiter (a frame's closing 0x00), not raw
|
|
85
|
+
self._raw(bytearray(raw))
|
|
84
86
|
|
|
85
87
|
def _raw(self, raw: bytearray) -> None:
|
|
86
88
|
if raw:
|
|
@@ -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()
|
|
@@ -65,3 +65,46 @@ def test_the_command(capsys, monkeypatch):
|
|
|
65
65
|
assert cli.main(["config", "show", "x"]) == 0
|
|
66
66
|
out = capsys.readouterr().out
|
|
67
67
|
assert "0 x035: fn 1 pins 2,3 at-boot retry 1 s dmseq" in out and "port 0 (usb_serial_jtag): last-reset [slot:x035]" in out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_plan_label_idle_from_the_command(capsys, monkeypatch):
|
|
71
|
+
ep, hst = open_bench()
|
|
72
|
+
|
|
73
|
+
class FakeLink:
|
|
74
|
+
def close(self):
|
|
75
|
+
pass
|
|
76
|
+
hst.link = FakeLink()
|
|
77
|
+
hst.end()
|
|
78
|
+
monkeypatch.setattr(cli.link, "open_host", lambda target: hst)
|
|
79
|
+
assert cli.main(["config", "plan", "x", "oep.fixture.uart", "rx=20", "tx=21"]) == 0
|
|
80
|
+
assert cli.main(["config", "label", "x", "20", "DUT TX"]) == 0
|
|
81
|
+
assert cli.main(["config", "idle", "x", "21", "pull-up", "--save"]) == 0
|
|
82
|
+
assert {(5, 1, 20), (5, 2, 21)} <= ep.plan and 5 in ep.plan_from_config
|
|
83
|
+
items = config.ProbeConfig(hst).items()
|
|
84
|
+
assert config.Label(20, "DUT TX") in items and config.Idle(21, "pull-up") in items
|
|
85
|
+
assert ep.saved is not None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_slot_takes_the_only_wire_by_default(monkeypatch):
|
|
89
|
+
ep = endpoint.Endpoint(fake.esp32_v003(), Clock()) # oep.wire.swio only
|
|
90
|
+
hst = h.Host(lambda b: ep.handle(b, 0))
|
|
91
|
+
|
|
92
|
+
class FakeLink:
|
|
93
|
+
def close(self):
|
|
94
|
+
pass
|
|
95
|
+
hst.link = FakeLink()
|
|
96
|
+
monkeypatch.setattr(cli.link, "open_host", lambda target: hst)
|
|
97
|
+
assert cli.main(["config", "slot", "x", "--name", "v003", "--attach", "at-boot", "--retry", "1"]) == 0
|
|
98
|
+
(slot,) = [i for i in config.ProbeConfig(hst).items() if isinstance(i, config.Slot)]
|
|
99
|
+
assert slot.wire_fn == 1 and slot.pins == (16, 0xFFFF)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_a_plan_refused_for_a_saved_plan_names_the_holder():
|
|
103
|
+
import pytest
|
|
104
|
+
from oep_client import core
|
|
105
|
+
ep, hst = open_bench()
|
|
106
|
+
cfg = config.ProbeConfig(hst)
|
|
107
|
+
cfg.set([config.Plan(5, 1, 20), config.Plan(5, 2, 21)]) # fixture.uart keeps 20 / 21 as a setting
|
|
108
|
+
with pytest.raises(core.PinsTaken) as e:
|
|
109
|
+
core.plan_apply(hst, [(4, 1, 21)]) # gpio wants 21
|
|
110
|
+
assert e.value.holders == [(21, "the saved plan of fn 5 (role 2)")] and "fn 5" in str(e.value)
|
|
@@ -370,3 +370,29 @@ def test_fake_serve_uart_plan_and_rx():
|
|
|
370
370
|
finally:
|
|
371
371
|
proc.stdin.close()
|
|
372
372
|
proc.wait(timeout=5)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_the_closing_0x00_of_a_frame_is_not_raw_after_the_gap():
|
|
376
|
+
clock = Clock()
|
|
377
|
+
ep = endpoint.Endpoint(fake.p4_bench(), clock)
|
|
378
|
+
p = ep.pairs[1]
|
|
379
|
+
ep.load_config([slot_item(0, 1, p[0]), bind_item(0, MODE["manual"], [(KIND["slot_console"], 0)])])
|
|
380
|
+
port = fake_serial.FakeSerialPort(ep, 0)
|
|
381
|
+
port.feed(framed(m.Request(1, 0, m.OP_LOCK_STATE, b""))) # lock-free: the port is not held
|
|
382
|
+
clock.t += 250
|
|
383
|
+
port.tick()
|
|
384
|
+
sid = ep.stream_keys[(ep._conn_at(1, p[0]), 2)]
|
|
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
|
|
@@ -82,9 +82,13 @@ def test_open_host_on_a_serial_port_with_console_bytes_and_tiocexcl():
|
|
|
82
82
|
if os.geteuid() != 0:
|
|
83
83
|
with pytest.raises(link.PortBusy):
|
|
84
84
|
link.open_host(where[1]) # exclusive: one host at a time
|
|
85
|
+
import time
|
|
86
|
+
deadline = time.monotonic() + 3 # lock-free reads among the console bytes,
|
|
87
|
+
while hst.link.noise == 0 and time.monotonic() < deadline: # before a session holds the port's raw side
|
|
88
|
+
assert [k for _, k, _ in core.transports(hst)] == [1] # a UART bridge
|
|
89
|
+
time.sleep(0.02)
|
|
85
90
|
opened = core.take(hst, 3000, owner="test") # the only way in: by force at once
|
|
86
91
|
assert opened.lease_ms == 3000
|
|
87
|
-
assert [k for _, k, _ in core.transports(hst)] == [1] # a UART bridge
|
|
88
92
|
for _ in range(20): # requests among the console bytes
|
|
89
93
|
hst.keepalive()
|
|
90
94
|
hst.end()
|
|
@@ -100,7 +104,8 @@ def test_the_same_host_over_tcp_length_frames_and_a_holder_that_keeps_its_lease_
|
|
|
100
104
|
try:
|
|
101
105
|
a = link.open_host(f"tcp://127.0.0.1:{where[1]}", timeout=1.0)
|
|
102
106
|
a.open(60000, owner="ch32rv monitor")
|
|
103
|
-
|
|
107
|
+
locked, remaining, owner = a.lock_owner() # one read: the lease ticks down between two
|
|
108
|
+
assert locked and 0 < remaining <= 60000 and owner == "ch32rv monitor"
|
|
104
109
|
b = h.Host(a.link.send) # another host on the same link (a broker's client)
|
|
105
110
|
b.link = a.link
|
|
106
111
|
b.revision = 1
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|