oep-client-python 0.0.8__tar.gz → 0.0.9__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.8 → oep_client_python-0.0.9}/CHANGELOG.md +6 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/PKG-INFO +12 -4
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/README.ja.md +10 -3
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/README.md +11 -3
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/pyproject.toml +1 -1
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/__init__.py +1 -1
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/endpoint.py +112 -6
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/fake.py +22 -3
- oep_client_python-0.0.9/src/oep_client/fake_capture.py +267 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/fake_serial.py +2 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/fake_serve.py +8 -1
- oep_client_python-0.0.9/tests/test_fake_capture.py +182 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_fake_spec.py +2 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/.gitignore +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/LICENSE +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/__main__.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/arm.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/capture.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/catalog.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/ch32_flash.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/cobs.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/config.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/console.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/core.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/decode.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/dump.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/esp32_targets.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/fixture.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/frames.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/hid_stream.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/host.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/interfaces.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/link.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/message.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/names.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/registry.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/riscv.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/rp2350.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/target.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/uiapduino.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/src/oep_client/usb_stream.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_capabilities.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_capture.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_cobs.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_config.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_esp32_targets.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_flash_console.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_hid_stream.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_interfaces.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_link_host.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_link_serial.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_session.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_target_parts.py +0 -0
- {oep_client_python-0.0.8 → oep_client_python-0.0.9}/tests/test_usb_stream.py +0 -0
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.9
|
|
6
|
+
- (EN) The fake probe has `oep.fixture.capture` (logic, `fake_capture`): one-shot, repeat (a ring and release), streaming (data pushes), level / edge triggers with a pretrigger, events, and a known waveform (sample i = the counter i, channel k = its bit k) in the profile's layout (P4: w 1-16; classic ESP32: w 8). A capture shares pins with other interfaces (it only listens). `Endpoint.pushes()` gives the frames the probe sends by itself; fake_serve sends them on the pty and TCP. `--capture-slipped` sets flags bit2 on every segment.
|
|
7
|
+
- (JA) 偽の probe に `oep.fixture.capture`(ロジック、`fake_capture`)を入れた: ワンショット、リピート(リングと release)、ストリーミング(データの push)、レベル / エッジのトリガとプリトリガ、出来事。取れる波形は決まっている(サンプル i = カウンタ i、チャネル k = そのビット k)。layout は profile のもの(P4: w 1〜16、classic ESP32: w 8)。capture はほかのインターフェースとピンを共有できる(聞くだけ)。`Endpoint.pushes()` が probe の送り出すフレームを返し、fake_serve は pty と TCP に流す。`--capture-slipped` は区画すべてに flags bit2 を立てる。
|
|
8
|
+
- (EN) The fake: an attach without pins joins the wire's one live connection, also on a wire whose pins the host chooses (oep-if-debug §1).
|
|
9
|
+
- (JA) 偽の probe: pins の無い attach は、その線の生きている接続が 1 つならそれに乗る。host がピンを選ぶ線でも同じ(oep-if-debug §1)。
|
|
10
|
+
|
|
5
11
|
## 0.0.8
|
|
6
12
|
- (EN) Wires whose pins the host chooses (oep-if-debug §1, role_channels): the fake takes any allowed free pair, skips the pairs whose pins something holds in a count-0 scan, holds a live connection's pins against plans, and has a profile for it (`rp2350-pins`). `Wire.scan()` goes through the whole list (at most 255 pairs a request; count 0 continues with the new skip TLV until tried = 0).
|
|
7
13
|
- (JA) host がピンを選ぶ wire(oep-if-debug §1、role_channels): 偽の probe は、許された空いている組ならどれでも受け、count = 0 の scan では何かが持っているピンの組を飛ばし、生きている接続のピンを plan から守る。そのための profile(`rp2350-pins`)を足した。`Wire.scan()` は並びを最後まで回る(1 回 255 組まで。count = 0 は新しい skip の TLV で tried = 0 まで続ける)。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: oep-client-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.9
|
|
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
|
|
@@ -107,8 +107,15 @@ A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`,
|
|
|
107
107
|
|
|
108
108
|
`endpoint.Endpoint` is a fake probe that answers as oep-spec says; ch32rv, this client and the probe firmware are checked
|
|
109
109
|
against it (when the spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
|
|
110
|
-
`p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats
|
|
111
|
-
port (COBS candidates, raw bytes and binds, held during a session and
|
|
110
|
+
`p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats, `rp2350-pins` = a wire whose pins the host
|
|
111
|
+
chooses), `fake_serial` the byte side of a serial port (COBS candidates, raw bytes and binds, held during a session and
|
|
112
|
+
resumed after it).
|
|
113
|
+
|
|
114
|
+
`fake_capture` is `oep.fixture.capture` (logic): one-shot, repeat (segments with the clock at the actual rate, a ring,
|
|
115
|
+
release) and streaming (data pushes while subscribed), level / edge triggers with a pretrigger, and events. What it
|
|
116
|
+
captures is known: sample i is the counter i, channel k its bit k (a square wave of period 2^(k+1) samples), in the layout
|
|
117
|
+
the profile allows (`p4-x035`: w 1-16 as the P4's PARLIO, three channels in w 4; `esp32-v003`: w 8 as the classic ESP32's
|
|
118
|
+
sampler, one-shot only). A capture only listens, so it may be planned on pins other interfaces hold.
|
|
112
119
|
|
|
113
120
|
Other programs' tests run `fake_serve` as a child process:
|
|
114
121
|
|
|
@@ -122,4 +129,5 @@ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--f
|
|
|
122
129
|
`--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
|
|
123
130
|
so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
|
|
124
131
|
broken once). `--uart-plan` / `--uart-rx` give the first fixture UART a plan and RX bytes, `--run-hook` a host's own model of
|
|
125
|
-
riscv-dm run.
|
|
132
|
+
riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. Events and data pushes go out on the pty and on TCP
|
|
133
|
+
(both framings). The rest: `--help`.
|
|
@@ -80,8 +80,14 @@ oep config save <probe> # 再起動の後も残す(remove
|
|
|
80
80
|
|
|
81
81
|
`endpoint.Endpoint` は oep-spec の規範どおりに答える偽の probe で、ch32rv・この client・probe の firmware を突き合わせる
|
|
82
82
|
「動く spec」として使う(spec が変わったら、probe の firmware より先にここを合わせる)。`fake` は宣言の例(profile:
|
|
83
|
-
`p4-x035`、`esp32-v003`、`p4-bench` = スロット 3 か所と席 2
|
|
84
|
-
|
|
83
|
+
`p4-x035`、`esp32-v003`、`p4-bench` = スロット 3 か所と席 2 つの架空の治具、`rp2350-pins` = host がピンを選ぶ wire)、`fake_serial` は
|
|
84
|
+
シリアルの口のバイトの側(COBS の候補、生のバイトと bind、セッション中の停止と再開)。
|
|
85
|
+
|
|
86
|
+
`fake_capture` は `oep.fixture.capture`(ロジック): ワンショット、リピート(実際のレートで時計どおりに区画ができ、リング、release)、
|
|
87
|
+
ストリーミング(購読している間のデータの push)、レベル / エッジのトリガとプリトリガ、出来事。取れるものは決まっている: サンプル i は
|
|
88
|
+
カウンタの値 i で、チャネル k はそのビット k(周期 2^(k+1) サンプルの方形波)。layout は profile が許す形(`p4-x035`: P4 の
|
|
89
|
+
PARLIO と同じ w 1〜16、3 本なら w 4。`esp32-v003`: classic ESP32 の sampler と同じ w 8、ワンショットだけ)。capture は聞くだけ
|
|
90
|
+
なので、ほかのインターフェースが持つピンにも plan できる。
|
|
85
91
|
|
|
86
92
|
外のプログラムの試験には `fake_serve` を子プロセスで使う:
|
|
87
93
|
|
|
@@ -93,6 +99,7 @@ uv run python -m oep_client.fake_serve --pty --profile p4-bench --slot x035 --bi
|
|
|
93
99
|
|
|
94
100
|
pty がシリアルの口(host が TIOCEXCL を掛けて開く)、`--tcp PORT` は `--framing cobs`(シリアルの口)か `--framing length`
|
|
95
101
|
(vendor bulk / TCP の形)。故障の注入は `--drop N`(N 番目の答えを 1 回出さない。要求は実行済みなので送り直しは覚えた答えを
|
|
96
|
-
受ける)、`--noise TEXT`(答えの前に雑音)、`--corrupt N`(N 番目の答えの CRC を 1
|
|
102
|
+
受ける)、`--noise TEXT`(答えの前に雑音)、`--corrupt N`(N 番目の答えの CRC を 1 回壊す)。`--capture-slipped` は capture の
|
|
103
|
+
区画すべてに flags bit2 を立てる。出来事とデータの push は pty にも TCP(両方の framing)にも出る。ほかは `--help`。
|
|
97
104
|
|
|
98
105
|
v0 の client(`oep_client.v0`)は 2026-09-26 に消した(git の履歴に残る)。v0 を話す probe はもう無い。
|
|
@@ -85,8 +85,15 @@ A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`,
|
|
|
85
85
|
|
|
86
86
|
`endpoint.Endpoint` is a fake probe that answers as oep-spec says; ch32rv, this client and the probe firmware are checked
|
|
87
87
|
against it (when the spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
|
|
88
|
-
`p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats
|
|
89
|
-
port (COBS candidates, raw bytes and binds, held during a session and
|
|
88
|
+
`p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats, `rp2350-pins` = a wire whose pins the host
|
|
89
|
+
chooses), `fake_serial` the byte side of a serial port (COBS candidates, raw bytes and binds, held during a session and
|
|
90
|
+
resumed after it).
|
|
91
|
+
|
|
92
|
+
`fake_capture` is `oep.fixture.capture` (logic): one-shot, repeat (segments with the clock at the actual rate, a ring,
|
|
93
|
+
release) and streaming (data pushes while subscribed), level / edge triggers with a pretrigger, and events. What it
|
|
94
|
+
captures is known: sample i is the counter i, channel k its bit k (a square wave of period 2^(k+1) samples), in the layout
|
|
95
|
+
the profile allows (`p4-x035`: w 1-16 as the P4's PARLIO, three channels in w 4; `esp32-v003`: w 8 as the classic ESP32's
|
|
96
|
+
sampler, one-shot only). A capture only listens, so it may be planned on pins other interfaces hold.
|
|
90
97
|
|
|
91
98
|
Other programs' tests run `fake_serve` as a child process:
|
|
92
99
|
|
|
@@ -100,4 +107,5 @@ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--f
|
|
|
100
107
|
`--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
|
|
101
108
|
so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
|
|
102
109
|
broken once). `--uart-plan` / `--uart-rx` give the first fixture UART a plan and RX bytes, `--run-hook` a host's own model of
|
|
103
|
-
riscv-dm run.
|
|
110
|
+
riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. Events and data pushes go out on the pty and on TCP
|
|
111
|
+
(both framings). The rest: `--help`.
|
|
@@ -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.9"
|
|
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"
|
|
@@ -39,7 +39,7 @@ from collections import OrderedDict
|
|
|
39
39
|
from dataclasses import dataclass, field
|
|
40
40
|
from typing import Callable
|
|
41
41
|
|
|
42
|
-
from . import catalog, fake, message as m, registry as reg
|
|
42
|
+
from . import catalog, fake, fake_capture, message as m, registry as reg
|
|
43
43
|
|
|
44
44
|
TOY_WRITE, TOY_READ = 0x01, 0x02
|
|
45
45
|
OK, WAIT, LINE, FAULT, TIMEOUT, STATE = (reg.STATUS[k] for k in ("ok", "wait", "line", "fault", "timeout", "state"))
|
|
@@ -275,6 +275,8 @@ class Endpoint:
|
|
|
275
275
|
first = self._role_pairs(fn)[0]
|
|
276
276
|
self.targets[(fn, first)] = FakeTarget()
|
|
277
277
|
self.target = next(iter(self.targets.values()), FakeTarget())
|
|
278
|
+
self.captures: dict[int, fake_capture.FakeCapture] = {
|
|
279
|
+
fn: self._capture_from(self.static[fn]) for fn, name in self.names.items() if name == "oep.fixture.capture"}
|
|
278
280
|
self.mechanisms = set()
|
|
279
281
|
for fn, name in self.names.items():
|
|
280
282
|
if name == "oep.target.console":
|
|
@@ -302,6 +304,9 @@ class Endpoint:
|
|
|
302
304
|
self.dropped = 0 # requests a v0 endpoint dropped (role 0x81)
|
|
303
305
|
self.requests: list[m.Request] = []
|
|
304
306
|
self.subscribed: set[int] = set()
|
|
307
|
+
self.push_seq: dict[int, int] = {} # fn -> the next event / data seq (core §11.2)
|
|
308
|
+
self.outbox: list[bytes] = [] # events and data frames waiting to go out (pushes())
|
|
309
|
+
self.capture_slipped = False # every capture segment says flags bit2 (a pace that fell behind)
|
|
305
310
|
self.plan: set[tuple[int, int, int]] = set() # (fn, role, channel), from plan_apply and the config
|
|
306
311
|
self.plan_from_config: set[int] = set() # fns whose plan came from the config (not a session's)
|
|
307
312
|
self.resend: OrderedDict[int, tuple[int, int, int, bytes | None]] = OrderedDict()
|
|
@@ -340,6 +345,91 @@ class Endpoint:
|
|
|
340
345
|
def target_id(self, value: int | None) -> None:
|
|
341
346
|
self.target.target_id = value
|
|
342
347
|
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _capture_from(tlvs: list[bytes]) -> fake_capture.FakeCapture:
|
|
350
|
+
"""A capture as its describe declares it (modes, the w allowed, the rate range, max_read, the ring)."""
|
|
351
|
+
d = reg.FIXTURE_CAPTURE.tlv["describe"]
|
|
352
|
+
modes, widths, lo, hi, ring, most = set(), {8}, 1, 1_000_000, 1, 1024
|
|
353
|
+
for t in tlvs:
|
|
354
|
+
tag, v = t[0], t[2:2 + t[1]]
|
|
355
|
+
if tag == d["mode"]:
|
|
356
|
+
modes.add(v[0])
|
|
357
|
+
elif tag == d["channels"]:
|
|
358
|
+
widths = {1 << i for i in range(6) if v[1] >> i & 1}
|
|
359
|
+
elif tag == catalog.MIN_CLOCK_HZ:
|
|
360
|
+
lo = struct.unpack("<I", v)[0]
|
|
361
|
+
elif tag == catalog.MAX_CLOCK_HZ:
|
|
362
|
+
hi = struct.unpack("<I", v)[0]
|
|
363
|
+
elif tag == d["segment_ring"]:
|
|
364
|
+
ring = struct.unpack("<H", v)[0]
|
|
365
|
+
elif tag == d["max_read"]:
|
|
366
|
+
most = struct.unpack("<I", v)[0]
|
|
367
|
+
return fake_capture.FakeCapture(modes or {fake_capture.MODE["one_shot"]}, widths, lo, hi, ring, most)
|
|
368
|
+
|
|
369
|
+
def _next_seq(self, fn: int) -> int:
|
|
370
|
+
seq = self.push_seq.get(fn, 0)
|
|
371
|
+
self.push_seq[fn] = (seq + 1) & 0xFFFF
|
|
372
|
+
return seq
|
|
373
|
+
|
|
374
|
+
def _events(self, fn: int, events: list[bytes]) -> None:
|
|
375
|
+
"""Events (kind(u8) payload) of `fn` to go out while it is subscribed; unsubscribed, they are not sent."""
|
|
376
|
+
for e in events:
|
|
377
|
+
if fn in self.subscribed:
|
|
378
|
+
self.outbox.append(bytes([m.ROLE_EVENT]) + struct.pack("<HH", fn, self._next_seq(fn)) + e)
|
|
379
|
+
|
|
380
|
+
def pushes(self) -> list[bytes]:
|
|
381
|
+
"""The frames the probe sends by itself now (core §11): events, and a streaming capture's data. A serving loop
|
|
382
|
+
frames and sends them; a test takes them from here."""
|
|
383
|
+
self.tick()
|
|
384
|
+
for fn, cap in self.captures.items():
|
|
385
|
+
if fn in self.subscribed:
|
|
386
|
+
self.outbox += cap.pushes(fn, lambda fn=fn: self._next_seq(fn), self.probe.max_frame)
|
|
387
|
+
out, self.outbox = self.outbox, []
|
|
388
|
+
return out
|
|
389
|
+
|
|
390
|
+
def _capture(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
391
|
+
cap, O = self.captures[fn], fake_capture.OP
|
|
392
|
+
self._events(fn, cap.tick(self.now())) # what the clock captured up to this request
|
|
393
|
+
roles = sorted(a[1] for a in self.plan if a[0] == fn)
|
|
394
|
+
budget = self.probe.max_frame - m.RESULT_HEADER
|
|
395
|
+
try:
|
|
396
|
+
if op in (O["configure"], O["query"]):
|
|
397
|
+
got, ignored = t.tail(set(fake_capture.TLV.values()) - {fake_capture.TLV["frontend"]})
|
|
398
|
+
settled = cap.settle(got, t.critical, len(roles))
|
|
399
|
+
if op == O["configure"]:
|
|
400
|
+
cap.apply(settled)
|
|
401
|
+
cap.slipped = self.capture_slipped
|
|
402
|
+
return self._answer(cap.answer(settled), ignored)
|
|
403
|
+
if op == O["start"]:
|
|
404
|
+
t.tail()
|
|
405
|
+
self._events(fn, cap.start(self.now()))
|
|
406
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<I", 0)
|
|
407
|
+
if op == O["stop"]:
|
|
408
|
+
t.tail()
|
|
409
|
+
self._events(fn, cap.stop())
|
|
410
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
411
|
+
if op == O["force"]:
|
|
412
|
+
t.tail()
|
|
413
|
+
return m.COMPLETED, m.SUCCESS, b"" # nothing waits: a trigger is found at start
|
|
414
|
+
if op == O["status"]:
|
|
415
|
+
t.tail()
|
|
416
|
+
return m.COMPLETED, m.SUCCESS, cap.status()
|
|
417
|
+
if op == O["read"]:
|
|
418
|
+
position, most = t.take("QI")
|
|
419
|
+
return m.COMPLETED, m.SUCCESS, cap.read(position, most, budget)
|
|
420
|
+
if op == O["segments"]:
|
|
421
|
+
first = t.take("I")
|
|
422
|
+
t.tail()
|
|
423
|
+
return m.COMPLETED, m.SUCCESS, cap.segment_list(first, budget)
|
|
424
|
+
if op == O["release"]:
|
|
425
|
+
serial = t.take("I")
|
|
426
|
+
t.tail()
|
|
427
|
+
cap.release(serial, self.now())
|
|
428
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
429
|
+
except fake_capture.Reject as e:
|
|
430
|
+
raise Reject(e.reason, e.payload)
|
|
431
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
432
|
+
|
|
343
433
|
def _role_pairs(self, fn: int) -> list[tuple[int, int]]:
|
|
344
434
|
"""A role_channels wire's pairs in the count = 0 order (oep-if-debug §1): swdio ascending, then swclk."""
|
|
345
435
|
roles = self.pin_roles[fn]
|
|
@@ -361,7 +451,7 @@ class Endpoint:
|
|
|
361
451
|
def _held(self, fn: int | None = None, pair: tuple[int, int] | None = None) -> set[int]:
|
|
362
452
|
"""Channels something holds (core §8.1): the plan, the slots' pairs, the live connections' pairs - except
|
|
363
453
|
wire `fn`'s own connection on `pair` (attaching there again, or scanning through it)."""
|
|
364
|
-
held = {a[2] for a in self.plan}
|
|
454
|
+
held = {a[2] for a in self.plan if a[0] not in self.captures} # a capture only listens
|
|
365
455
|
held |= {p for s in self.slots.values() for p in s.pair if p != 0xFFFF and not (s.wire_fn == fn and s.pair == pair)}
|
|
366
456
|
held |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF and not (c.fn == fn and c.pair == pair)}
|
|
367
457
|
return held
|
|
@@ -613,6 +703,7 @@ class Endpoint:
|
|
|
613
703
|
if fn != 0 and fn not in self.names:
|
|
614
704
|
return m.REJECTED, m.UNAVAILABLE, b""
|
|
615
705
|
self.subscribed.add(fn)
|
|
706
|
+
self.push_seq[fn] = 0 # seq from 0 at every subscribe (core §11.2)
|
|
616
707
|
return m.COMPLETED, m.SUCCESS, b""
|
|
617
708
|
if op == m.OP_UNSUBSCRIBE:
|
|
618
709
|
fn = t.take("H")
|
|
@@ -644,14 +735,24 @@ class Endpoint:
|
|
|
644
735
|
def _check_plan(self, got: list[tuple[int, int, int]]) -> None:
|
|
645
736
|
"""plan_apply's all-or-nothing check: the roles each fn has, and no pin another fn (or a slot) holds."""
|
|
646
737
|
named = {fn for fn, _, _ in got}
|
|
647
|
-
|
|
738
|
+
# a capture only listens: it shares pins with anything, and nothing is kept from a pin by it (core §8.1)
|
|
739
|
+
kept = {a for a in self.plan if a[0] not in named and a[0] not in self.captures}
|
|
648
740
|
slot_pins = {p for s in self.slots.values() for p in s.pair if p != 0xFFFF}
|
|
649
741
|
slot_pins |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF} # a live connection's pins too
|
|
650
742
|
for fn, role, ch in got:
|
|
743
|
+
if fn in self.captures:
|
|
744
|
+
if role not in self._declared_roles(fn, ch):
|
|
745
|
+
raise Reject(m.UNAVAILABLE)
|
|
746
|
+
continue
|
|
651
747
|
roles = {"oep.fixture.gpio": {1}, "oep.fixture.uart": {1, 2}}.get(self.names.get(fn, ""), set())
|
|
652
748
|
if role not in roles or any(k[2] == ch for k in kept) or ch in slot_pins:
|
|
653
749
|
raise Reject(m.UNAVAILABLE)
|
|
654
750
|
|
|
751
|
+
def _declared_roles(self, fn: int, channel: int) -> set[int]:
|
|
752
|
+
"""The roles fn's describe offers on `channel` (role_channels)."""
|
|
753
|
+
return {t[2] for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS
|
|
754
|
+
and channel in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
|
|
755
|
+
|
|
655
756
|
def _drop_plan(self, fn: int) -> None:
|
|
656
757
|
for a in [a for a in self.plan if a[0] == fn]:
|
|
657
758
|
self.gpio_modes.pop(a[2], None)
|
|
@@ -787,8 +888,11 @@ class Endpoint:
|
|
|
787
888
|
if not self._allows(fn, pair) or set(pair) & self._held(fn, pair):
|
|
788
889
|
raise Reject(m.UNAVAILABLE) # not allowed here, or its pins are held (§8.1)
|
|
789
890
|
return pair
|
|
891
|
+
live = [c.pair for c in self.conns.values() if c.fn == fn]
|
|
892
|
+
if len(live) == 1:
|
|
893
|
+
return live[0] # no pins: the wire's one live connection
|
|
790
894
|
allowed = self._allowed_pairs(fn)
|
|
791
|
-
if len(allowed) != 1:
|
|
895
|
+
if live or len(allowed) != 1:
|
|
792
896
|
raise Reject(m.UNAVAILABLE) # the host chooses among several
|
|
793
897
|
return allowed[0]
|
|
794
898
|
|
|
@@ -1399,9 +1503,11 @@ class Endpoint:
|
|
|
1399
1503
|
self.flows[(port, key)] = Flow(sid, 0)
|
|
1400
1504
|
|
|
1401
1505
|
def tick(self) -> None:
|
|
1402
|
-
"""Time passes: the lease, at-boot retries, mixed lines closed by quiet."""
|
|
1506
|
+
"""Time passes: the lease, at-boot retries, mixed lines closed by quiet, the captures."""
|
|
1403
1507
|
self._lapse()
|
|
1404
1508
|
now = self.now()
|
|
1509
|
+
for fn, cap in self.captures.items():
|
|
1510
|
+
self._events(fn, cap.tick(now))
|
|
1405
1511
|
for n, s in self.slots.items():
|
|
1406
1512
|
rt = self.slot_rt[n]
|
|
1407
1513
|
if (s.attach == SLOT_ATTACH["at_boot"] and s.retry_s and not rt.evicted
|
|
@@ -1549,4 +1655,4 @@ class Endpoint:
|
|
|
1549
1655
|
|
|
1550
1656
|
SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
|
|
1551
1657
|
"oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
|
|
1552
|
-
"oep.probe.config": "config_op"}
|
|
1658
|
+
"oep.probe.config": "config_op", "oep.fixture.capture": "capture"}
|
|
@@ -108,6 +108,21 @@ TRANSPORT = reg.CORE.enum["transport_kind"]
|
|
|
108
108
|
SERIAL_KINDS = {TRANSPORT["uart_bridge"], TRANSPORT["usb_cdc"], TRANSPORT["usb_serial_jtag"]}
|
|
109
109
|
NS = "io.github.ch32-riscv-ug"
|
|
110
110
|
MECHANISMS = reg.TARGET_CONSOLE.tlv["describe"]["mechanisms"]
|
|
111
|
+
_CAPD = reg.FIXTURE_CAPTURE.tlv["describe"]
|
|
112
|
+
_CAPM = reg.FIXTURE_CAPTURE.enum["mode"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _capture_decl(modes: list[str], max_channels: int, widths: list[int], max_samples: int, ring: int,
|
|
116
|
+
max_read: int) -> tuple[bytes, ...]:
|
|
117
|
+
"""oep.fixture.capture's declarations (oep-if-capture §3.5): a mode entry per mode, channels (max, the w allowed as
|
|
118
|
+
bits: bit i = w 2^i), the trigger types (immediate, level, edge) with the most pretrigger, max_read, segment_ring."""
|
|
119
|
+
out = tuple(catalog.tlv(_CAPD["mode"], struct.pack("<BBII", _CAPM[k], 1, max_samples, ring if k == "repeat" else 1))
|
|
120
|
+
for k in modes)
|
|
121
|
+
bits = sum(1 << (w.bit_length() - 1) for w in widths)
|
|
122
|
+
return out + (catalog.tlv(_CAPD["channels"], bytes([max_channels, bits])),
|
|
123
|
+
catalog.tlv(_CAPD["trigger"], struct.pack("<BI", 0b111, max_samples - 1)),
|
|
124
|
+
catalog.u32(_CAPD["max_read"], max_read), catalog.u16(_CAPD["segment_ring"], ring),
|
|
125
|
+
catalog.u32(FEATURES, 0b001))
|
|
111
126
|
MAX_CONNECTIONS = reg.WIRE_RVSWD.tlv["describe"]["max_connections"]
|
|
112
127
|
_CFG = reg.PROBE_CONFIG.tlv["describe"]
|
|
113
128
|
|
|
@@ -165,9 +180,11 @@ def p4_x035() -> FakeProbe:
|
|
|
165
180
|
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
166
181
|
Offered(6, 4, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
|
|
167
182
|
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
168
|
-
|
|
183
|
+
# the P4's PARLIO: w 1-16, one-shot / repeat / streaming
|
|
184
|
+
Offered(7, 5, "oep.fixture.capture", _roles({k: pins for k in range(16)}) + (
|
|
169
185
|
catalog.u32(MAX_CLOCK_HZ, 20_000_000), catalog.u32(MIN_CLOCK_HZ, 1_000),
|
|
170
|
-
catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))
|
|
186
|
+
catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))
|
|
187
|
+
+ _capture_decl(["one_shot", "repeat", "streaming"], 16, [1, 2, 4, 8, 16], 1 << 20, 8, 4096)),
|
|
171
188
|
Offered(8, 6, f"{NS}.esp32.i2c-target", _roles({1: pins, 2: pins}) + (
|
|
172
189
|
catalog.u16(MAX_LENGTH, 128), catalog.u32(MAX_CLOCK_HZ, 1_000_000),
|
|
173
190
|
catalog.u32(FEATURES, 0b11), catalog.u8(IMPLEMENTATION, 2))),
|
|
@@ -194,8 +211,10 @@ def esp32_v003() -> FakeProbe:
|
|
|
194
211
|
Offered(4, 2, "oep.fixture.gpio", _roles({1: wired + [23]})),
|
|
195
212
|
Offered(5, 3, "oep.fixture.uart", _roles({1: wired, 2: wired}) + (
|
|
196
213
|
catalog.u32(MAX_CLOCK_HZ, 115_200), catalog.u8(IMPLEMENTATION, 2))),
|
|
214
|
+
# the classic ESP32's GPIO sampler: a byte a sample (w 8), one-shot
|
|
197
215
|
Offered(6, 4, "oep.fixture.capture", _roles({k: wired for k in range(4)}) + (
|
|
198
|
-
catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))
|
|
216
|
+
catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))
|
|
217
|
+
+ _capture_decl(["one_shot"], 8, [8], 65536, 1, 480)),
|
|
199
218
|
Offered(7, 5, f"{NS}.esp32.i2c-target", _roles({1: wired, 2: wired}) + (
|
|
200
219
|
catalog.u16(MAX_LENGTH, 16), catalog.u32(MAX_CLOCK_HZ, 100_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
201
220
|
# Two fixed pin sets (an example of channel_group; GPIO23 is the DUT's NRST on this jig).
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""oep.fixture.capture in the fake probe (oep-spec docs/oep-if-capture.ja.md, revision 1, logic).
|
|
3
|
+
|
|
4
|
+
What it captures is known in advance, so a receiver can check it: sample i of a capture (counted from its start, across
|
|
5
|
+
segments) is the counter i, and channel k is bit k of it - a square wave of period 2^(k+1) samples. The layout is the
|
|
6
|
+
probe's (§1.1): w is the smallest width the describe allows (channels tag 0x44) that holds the channels, pos[k] = k.
|
|
7
|
+
|
|
8
|
+
Modes: one-shot (the segment is there as soon as start answers), repeat (segments come with the clock at the actual
|
|
9
|
+
rate, up to the ring; a full ring stops the capture, release frees it) and streaming (the bytes come with the clock and
|
|
10
|
+
go out as data pushes while subscribed). Triggers: level and edge on a channel, with a pretrigger. `slipped` sets flags
|
|
11
|
+
bit2 on every segment (a probe whose software pace fell behind).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import struct
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from fractions import Fraction
|
|
19
|
+
|
|
20
|
+
from . import message as m, registry as reg
|
|
21
|
+
|
|
22
|
+
CAP = reg.FIXTURE_CAPTURE
|
|
23
|
+
OP = CAP.op
|
|
24
|
+
TLV, ANSWER = CAP.tlv["configure"], CAP.tlv["configure_answer"]
|
|
25
|
+
MODE, STATE, TRIGGER = CAP.enum["mode"], CAP.enum["state"], CAP.enum["trigger"]
|
|
26
|
+
STOPPED, FLAG = CAP.enum["stopped_reason"], CAP.enum["segment_flag"]
|
|
27
|
+
EVENT = CAP.event
|
|
28
|
+
NONE = 0xFFFFFFFF
|
|
29
|
+
MAX_SAMPLES = 1 << 20 # a fake keeps its captures in memory
|
|
30
|
+
SEGMENT = struct.Struct("<IQIQIB") # serial position samples start_us trigger_index flags (§2)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Reject(Exception):
|
|
34
|
+
def __init__(self, reason: int, payload: bytes = b""):
|
|
35
|
+
self.reason, self.payload = reason, payload
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Segment:
|
|
40
|
+
serial: int
|
|
41
|
+
position: int
|
|
42
|
+
samples: int
|
|
43
|
+
start_us: int
|
|
44
|
+
trigger_index: int = NONE
|
|
45
|
+
flags: int = 0
|
|
46
|
+
|
|
47
|
+
def pack(self) -> bytes:
|
|
48
|
+
return SEGMENT.pack(self.serial, self.position, self.samples, self.start_us, self.trigger_index, self.flags)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class FakeCapture:
|
|
53
|
+
"""One capture interface. `now_ms` is the endpoint's clock; `roles()` the channels the plan gives it (role order)."""
|
|
54
|
+
modes: set[int] # the modes the describe declares
|
|
55
|
+
widths: set[int] # the w the describe allows
|
|
56
|
+
min_hz: int
|
|
57
|
+
max_hz: int
|
|
58
|
+
ring: int = 8 # segment_ring
|
|
59
|
+
max_read: int = 4096
|
|
60
|
+
slipped: bool = False
|
|
61
|
+
state: int = STATE["unconfigured"]
|
|
62
|
+
mode: int = MODE["one_shot"]
|
|
63
|
+
rate: Fraction = Fraction(0)
|
|
64
|
+
samples: int = 0
|
|
65
|
+
segments_max: int = 0
|
|
66
|
+
trigger: tuple[int, int, int] | None = None
|
|
67
|
+
pretrigger: int = 0
|
|
68
|
+
width: int = 0
|
|
69
|
+
channels: int = 0
|
|
70
|
+
data: bytearray = field(default_factory=bytearray) # the stream from `base` on
|
|
71
|
+
base: int = 0 # the byte position data[0] is at
|
|
72
|
+
segs: list[Segment] = field(default_factory=list)
|
|
73
|
+
serial_done: int = 0
|
|
74
|
+
started_ms: int = 0
|
|
75
|
+
produced: int = 0 # samples produced since start (repeat / streaming)
|
|
76
|
+
sent: int = 0 # streaming: the byte position pushed so far
|
|
77
|
+
first_sample: int = 0 # the counter value of the capture's first sample
|
|
78
|
+
gap_next: bool = False # repeat: the capture stopped for want of a segment; the next one says so
|
|
79
|
+
|
|
80
|
+
# ---- configure ------------------------------------------------------------------------------------------------
|
|
81
|
+
def settle(self, got: dict[int, bytes], critical: set[int], channels: int) -> dict:
|
|
82
|
+
"""The actual values for a configure / query request (nothing changed)."""
|
|
83
|
+
mode = got[TLV["mode"]][0] if TLV["mode"] in got else MODE["one_shot"]
|
|
84
|
+
if mode not in self.modes:
|
|
85
|
+
raise Reject(m.UNSUPPORTED, bytes([TLV["mode"] | (m.TAG_CRITICAL if TLV["mode"] in critical else 0)]))
|
|
86
|
+
if TLV["rate"] not in got or len(got[TLV["rate"]]) != 4:
|
|
87
|
+
raise Reject(m.MALFORMED)
|
|
88
|
+
asked = struct.unpack("<I", got[TLV["rate"]])[0]
|
|
89
|
+
if not asked:
|
|
90
|
+
raise Reject(m.MALFORMED)
|
|
91
|
+
# the source clock divided by a whole number: the nearest rate at or under the one asked, inside the range
|
|
92
|
+
div = max(1, -(-self.max_hz // min(max(asked, self.min_hz), self.max_hz)))
|
|
93
|
+
rate = Fraction(self.max_hz, div)
|
|
94
|
+
samples = struct.unpack("<I", got[TLV["samples"]])[0] if TLV["samples"] in got else 4096
|
|
95
|
+
samples = max(1, min(samples, MAX_SAMPLES))
|
|
96
|
+
segs = struct.unpack("<I", got[TLV["segments"]])[0] if TLV["segments"] in got else self.ring
|
|
97
|
+
segs = max(1, min(segs, self.ring)) if mode == MODE["repeat"] else 1
|
|
98
|
+
trigger = None
|
|
99
|
+
if TLV["trigger"] in got:
|
|
100
|
+
kind, role, value = struct.unpack("<BBH", got[TLV["trigger"]])
|
|
101
|
+
if kind not in (TRIGGER["immediate"], TRIGGER["level"], TRIGGER["edge"]) or (kind and role >= channels):
|
|
102
|
+
raise Reject(m.UNSUPPORTED, bytes([TLV["trigger"] | m.TAG_CRITICAL]))
|
|
103
|
+
trigger = (kind, role, value) if kind else None
|
|
104
|
+
pre = struct.unpack("<I", got[TLV["pretrigger"]])[0] if TLV["pretrigger"] in got else 0
|
|
105
|
+
width = min((w for w in self.widths if w >= max(channels, 1)), default=None)
|
|
106
|
+
if width is None or not channels:
|
|
107
|
+
raise Reject(m.UNAVAILABLE) # no channels planned, or more than a sample holds
|
|
108
|
+
if mode != MODE["one_shot"] and width < 8:
|
|
109
|
+
samples = -(-samples // (8 // width)) * (8 // width) # segments end on a byte
|
|
110
|
+
return {"mode": mode, "rate": rate, "samples": samples, "segments": segs, "trigger": trigger,
|
|
111
|
+
"pretrigger": min(pre, samples - 1), "width": width, "channels": channels}
|
|
112
|
+
|
|
113
|
+
def apply(self, s: dict) -> None:
|
|
114
|
+
self.mode, self.rate, self.samples, self.segments_max = s["mode"], s["rate"], s["samples"], s["segments"]
|
|
115
|
+
self.trigger, self.pretrigger, self.width, self.channels = s["trigger"], s["pretrigger"], s["width"], s["channels"]
|
|
116
|
+
self.state = STATE["configured"]
|
|
117
|
+
self._clear()
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def answer(s: dict) -> bytes:
|
|
121
|
+
def tlv(tag: int, v: bytes) -> bytes:
|
|
122
|
+
return bytes([tag, len(v)]) + v
|
|
123
|
+
return (tlv(ANSWER["actual_rate"], struct.pack("<II", s["rate"].numerator, s["rate"].denominator))
|
|
124
|
+
+ tlv(ANSWER["layout"], bytes([s["width"], s["channels"]]) + bytes(range(s["channels"])))
|
|
125
|
+
+ tlv(ANSWER["actual_samples"], struct.pack("<I", s["samples"]))
|
|
126
|
+
+ tlv(ANSWER["actual_segments"], struct.pack("<I", s["segments"]))
|
|
127
|
+
+ tlv(ANSWER["timing"], struct.pack("<BI", 1 if s["rate"].denominator != 1 else 0, 0))
|
|
128
|
+
+ tlv(ANSWER["blocking_ms"], struct.pack("<I", 0)))
|
|
129
|
+
|
|
130
|
+
# ---- the data ---------------------------------------------------------------------------------------------------
|
|
131
|
+
def _clear(self) -> None:
|
|
132
|
+
self.data, self.base, self.segs, self.serial_done = bytearray(), 0, [], 0
|
|
133
|
+
self.produced, self.sent, self.first_sample, self.gap_next = 0, 0, 0, False
|
|
134
|
+
|
|
135
|
+
def _pack(self, first: int, n: int) -> bytes:
|
|
136
|
+
"""Samples first .. first+n-1 of the counter in the layout (w bits each, little endian, low samples first)."""
|
|
137
|
+
w = self.width
|
|
138
|
+
if w >= 8:
|
|
139
|
+
per = w // 8
|
|
140
|
+
mask = (1 << w) - 1
|
|
141
|
+
return b"".join(((first + i) & mask).to_bytes(per, "little") for i in range(n))
|
|
142
|
+
out = bytearray((n * w + 7) // 8)
|
|
143
|
+
mask = (1 << w) - 1
|
|
144
|
+
for i in range(n):
|
|
145
|
+
bit = i * w
|
|
146
|
+
out[bit >> 3] |= ((first + i) & mask) << (bit & 7)
|
|
147
|
+
return bytes(out)
|
|
148
|
+
|
|
149
|
+
def _trigger_at(self) -> int:
|
|
150
|
+
"""The first sample, at or after the pretrigger, where the trigger holds (the counter's bit `role`)."""
|
|
151
|
+
kind, role, value = self.trigger
|
|
152
|
+
period = 1 << (role + 1)
|
|
153
|
+
i = self.pretrigger
|
|
154
|
+
while True: # within two periods of the channel
|
|
155
|
+
now, before = (i >> role) & 1, ((i - 1) >> role) & 1 if i else None
|
|
156
|
+
if kind == TRIGGER["level"] and now == (value & 1):
|
|
157
|
+
return i
|
|
158
|
+
if kind == TRIGGER["edge"] and before is not None and now != before:
|
|
159
|
+
if value == 2 or (value == 0 and now == 1) or (value == 1 and now == 0):
|
|
160
|
+
return i
|
|
161
|
+
i += 1
|
|
162
|
+
if i > self.pretrigger + 2 * period + 1:
|
|
163
|
+
return self.pretrigger
|
|
164
|
+
|
|
165
|
+
def _segment(self, n: int, now_ms: int, trigger_index: int = NONE, flags: int = 0) -> Segment:
|
|
166
|
+
if self.gap_next:
|
|
167
|
+
flags, self.gap_next = flags | FLAG["gap"], False
|
|
168
|
+
seg = Segment(self.serial_done, self.base + len(self.data), n, now_ms * 1000, trigger_index,
|
|
169
|
+
flags | (FLAG["slipped"] if self.slipped else 0))
|
|
170
|
+
self.data += self._pack(self.first_sample + self.produced, n)
|
|
171
|
+
self.produced += n
|
|
172
|
+
self.segs.append(seg)
|
|
173
|
+
self.serial_done += 1
|
|
174
|
+
return seg
|
|
175
|
+
|
|
176
|
+
# ---- the operations ---------------------------------------------------------------------------------------------
|
|
177
|
+
def start(self, now_ms: int) -> list[bytes]:
|
|
178
|
+
"""-> the events it raises (kind(u8) payload), for the endpoint to send when subscribed."""
|
|
179
|
+
if self.state == STATE["unconfigured"]:
|
|
180
|
+
raise Reject(m.UNAVAILABLE)
|
|
181
|
+
self._clear()
|
|
182
|
+
self.started_ms = now_ms
|
|
183
|
+
events = []
|
|
184
|
+
if self.mode == MODE["one_shot"]:
|
|
185
|
+
index = self._trigger_at() if self.trigger else NONE
|
|
186
|
+
if self.trigger:
|
|
187
|
+
events.append(bytes([EVENT["triggered"]]) + struct.pack("<II", 0, index))
|
|
188
|
+
seg = self._segment(self.samples, now_ms, index)
|
|
189
|
+
self.state = STATE["done"]
|
|
190
|
+
events += [bytes([EVENT["segment"]]) + seg.pack(), bytes([EVENT["stopped"], STOPPED["complete"]])]
|
|
191
|
+
else:
|
|
192
|
+
self.state = STATE["capturing"]
|
|
193
|
+
return events
|
|
194
|
+
|
|
195
|
+
def tick(self, now_ms: int) -> list[bytes]:
|
|
196
|
+
"""Time passes: repeat's segments, streaming's bytes. -> events."""
|
|
197
|
+
if self.state != STATE["capturing"]:
|
|
198
|
+
return []
|
|
199
|
+
due = int((now_ms - self.started_ms) * self.rate / 1000) # samples the clock has taken since start
|
|
200
|
+
events = []
|
|
201
|
+
if self.mode == MODE["repeat"]:
|
|
202
|
+
while due - self.produced >= self.samples:
|
|
203
|
+
if len(self.segs) >= self.segments_max: # no free segment: the capture stops
|
|
204
|
+
self.state = STATE["paused"]
|
|
205
|
+
events.append(bytes([EVENT["stopped"], STOPPED["no_free_segment"]]))
|
|
206
|
+
break
|
|
207
|
+
seg = self._segment(self.samples, now_ms)
|
|
208
|
+
events.append(bytes([EVENT["segment"]]) + seg.pack())
|
|
209
|
+
elif self.mode == MODE["streaming"]:
|
|
210
|
+
per_byte = 8 // self.width if self.width < 8 else 1 # whole bytes only
|
|
211
|
+
n = (due - self.produced) // per_byte * per_byte
|
|
212
|
+
if n > 0:
|
|
213
|
+
self.data += self._pack(self.first_sample + self.produced, min(n, MAX_SAMPLES))
|
|
214
|
+
self.produced += min(n, MAX_SAMPLES)
|
|
215
|
+
return events
|
|
216
|
+
|
|
217
|
+
def stop(self) -> list[bytes]:
|
|
218
|
+
if self.state not in (STATE["capturing"], STATE["waiting"], STATE["paused"]):
|
|
219
|
+
return []
|
|
220
|
+
self.state = STATE["done"]
|
|
221
|
+
return [bytes([EVENT["stopped"], STOPPED["host"]])]
|
|
222
|
+
|
|
223
|
+
def status(self) -> bytes:
|
|
224
|
+
return struct.pack("<BIQB", self.state, self.serial_done, self.base + len(self.data), 0)
|
|
225
|
+
|
|
226
|
+
def read(self, position: int, most: int, budget: int) -> bytes:
|
|
227
|
+
flags = 0
|
|
228
|
+
if position < self.base:
|
|
229
|
+
position, flags = self.base, flags | 0x02 # re-used: from what is kept (gap)
|
|
230
|
+
end = self.base + len(self.data)
|
|
231
|
+
take = max(0, min(most, self.max_read, budget - 9, end - position))
|
|
232
|
+
data = bytes(self.data[position - self.base:position - self.base + take])
|
|
233
|
+
if position + take < end:
|
|
234
|
+
flags |= 0x01 # more
|
|
235
|
+
return struct.pack("<QB", position, flags) + data
|
|
236
|
+
|
|
237
|
+
def segment_list(self, first: int, budget: int) -> bytes:
|
|
238
|
+
out = [s.pack() for s in self.segs if s.serial >= first]
|
|
239
|
+
out = out[:max(0, (budget - 1) // SEGMENT.size)][:255]
|
|
240
|
+
return bytes([len(out)]) + b"".join(out)
|
|
241
|
+
|
|
242
|
+
def release(self, serial: int, now_ms: int) -> None:
|
|
243
|
+
if self.mode != MODE["repeat"]:
|
|
244
|
+
return # one-shot: the next start clears; streaming: pushed
|
|
245
|
+
keep = [s for s in self.segs if s.serial > serial]
|
|
246
|
+
drop_to = keep[0].position if keep else self.base + len(self.data)
|
|
247
|
+
del self.data[:drop_to - self.base]
|
|
248
|
+
self.base = drop_to
|
|
249
|
+
self.segs = keep
|
|
250
|
+
if self.state == STATE["paused"]: # it goes on from now; the gap shows (flags bit0)
|
|
251
|
+
self.state, self.gap_next = STATE["capturing"], True
|
|
252
|
+
self.started_ms = now_ms - int(self.produced * 1000 / self.rate)
|
|
253
|
+
|
|
254
|
+
def pushes(self, fn: int, next_seq, budget: int) -> list[bytes]:
|
|
255
|
+
"""Streaming: the bytes not pushed yet, as data frames (position(u64) then data, common §1.5)."""
|
|
256
|
+
if self.mode != MODE["streaming"]:
|
|
257
|
+
return []
|
|
258
|
+
out = []
|
|
259
|
+
end = self.base + len(self.data)
|
|
260
|
+
while self.sent < end:
|
|
261
|
+
n = min(end - self.sent, budget - 13)
|
|
262
|
+
chunk = bytes(self.data[self.sent - self.base:self.sent - self.base + n])
|
|
263
|
+
out.append(bytes([m.ROLE_DATA]) + struct.pack("<HHQ", fn, next_seq(), self.sent) + chunk)
|
|
264
|
+
self.sent += n
|
|
265
|
+
del self.data[:self.sent - self.base] # pushed: the probe re-uses it
|
|
266
|
+
self.base = self.sent
|
|
267
|
+
return out
|
|
@@ -91,6 +91,8 @@ class FakeSerialPort:
|
|
|
91
91
|
def tick(self) -> None:
|
|
92
92
|
self._gap(self.ep.now())
|
|
93
93
|
self.ep.tick()
|
|
94
|
+
for f in self.ep.pushes(): # events and data the probe sends by itself (core §11)
|
|
95
|
+
self.frames.append(cobs.frame(f))
|
|
94
96
|
|
|
95
97
|
def output(self, room: int = 4096) -> bytes:
|
|
96
98
|
"""What the probe sends now: waiting answers first, then raw bytes by the bind (up to `room` in all)."""
|
|
@@ -12,7 +12,7 @@ The first line on stdout says where to open: `PTY /dev/pts/N` or `PORT n`. The p
|
|
|
12
12
|
(so a test's child never stays behind), or with --once when the first TCP connection closes.
|
|
13
13
|
|
|
14
14
|
Options:
|
|
15
|
-
--profile NAME p4-x035 (default), esp32-v003
|
|
15
|
+
--profile NAME p4-x035 (default), esp32-v003, p4-bench or rp2350-pins (p4_x035 style names work too)
|
|
16
16
|
--port-index N which serial port of the profile the pty / cobs TCP is (default: the first one)
|
|
17
17
|
--noise TEXT raw bytes written in front of every answer (the host must skip them)
|
|
18
18
|
--drop N the N-th answer (1-based) is not sent, once (the request did run: a resend gets the
|
|
@@ -25,6 +25,7 @@ Options:
|
|
|
25
25
|
--bind MODE bind the serial port to every --slot: last-reset, manual or mixed
|
|
26
26
|
--target-id HEX the target_id every target's attach reports (wch_dmi_7f)
|
|
27
27
|
--absent N the N-th pin pair of the first wire has no target (repeatable)
|
|
28
|
+
--capture-slipped every oep.fixture.capture segment says flags bit2 (slipped: a pace that fell behind)
|
|
28
29
|
--uart-plan the first oep.fixture.uart gets its RX / TX plan at boot, as if saved (the jig's "DUT TX" /
|
|
29
30
|
"DUT RX" labels when the profile has them, else the first free channels); configure then works
|
|
30
31
|
--uart-rx TEXT what arrives on that UART's RX every --every ms once it is configured (%d = a counter)
|
|
@@ -70,6 +71,7 @@ def build(a: argparse.Namespace) -> endpoint.Endpoint:
|
|
|
70
71
|
hook = _load_hook(a.run_hook)
|
|
71
72
|
for tg in ep.targets.values():
|
|
72
73
|
tg.run_hook = (lambda t: lambda pc, regs: hook(t, pc, regs))(tg)
|
|
74
|
+
ep.capture_slipped = getattr(a, "capture_slipped", False)
|
|
73
75
|
for n in a.absent:
|
|
74
76
|
ep.targets[(wire, ep.pairs[wire][n])].present = False
|
|
75
77
|
items = []
|
|
@@ -294,6 +296,10 @@ def _serve_conn(a, ep, console, conn, watch_stdin) -> bool:
|
|
|
294
296
|
conn.setblocking(False)
|
|
295
297
|
else:
|
|
296
298
|
ep.tick()
|
|
299
|
+
for f in ep.pushes(): # events and data pushes (core §11)
|
|
300
|
+
conn.setblocking(True)
|
|
301
|
+
conn.sendall(struct.pack("<H", len(f)) + f)
|
|
302
|
+
conn.setblocking(False)
|
|
297
303
|
|
|
298
304
|
|
|
299
305
|
def main(argv: list[str] | None = None) -> None:
|
|
@@ -317,6 +323,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
317
323
|
ap.add_argument("--uart-rx")
|
|
318
324
|
ap.add_argument("--run-hook")
|
|
319
325
|
ap.add_argument("--once", action="store_true")
|
|
326
|
+
ap.add_argument("--capture-slipped", action="store_true")
|
|
320
327
|
a = ap.parse_args(argv)
|
|
321
328
|
if a.tcp is None and a.framing == "length":
|
|
322
329
|
ap.error("--framing length is for --tcp")
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""oep.fixture.capture in the fake probe, through oep_client.capture (oep-if-capture): the counter waveform, the layouts,
|
|
2
|
+
triggers, repeat's ring and release, streaming pushes."""
|
|
3
|
+
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from oep_client import capture as c, core, endpoint, fake, fake_capture, host as h, message as m
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Clock:
|
|
12
|
+
t = 0
|
|
13
|
+
|
|
14
|
+
def __call__(self):
|
|
15
|
+
return self.t
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bench(profile=fake.p4_x035):
|
|
19
|
+
clock = Clock()
|
|
20
|
+
ep = endpoint.Endpoint(profile(), clock)
|
|
21
|
+
hst = h.Host(lambda b: ep.handle(b, 0))
|
|
22
|
+
hst.open(3000)
|
|
23
|
+
lc = c.LogicCapture(hst)
|
|
24
|
+
return ep, hst, lc, clock
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def counter_ok(lc, data, samples, first=0):
|
|
28
|
+
"""Channel k of sample i is bit k of the counter i."""
|
|
29
|
+
for k in range(len(lc.config.positions)):
|
|
30
|
+
assert lc.channel(data, k, samples) == [((first + i) >> k) & 1 for i in range(samples)]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_one_shot_three_channels_in_four_bit_samples():
|
|
34
|
+
ep, hst, lc, _ = bench()
|
|
35
|
+
core.plan_apply(hst, [(lc.fn, 0, 20), (lc.fn, 1, 21), (lc.fn, 2, 22)])
|
|
36
|
+
cfg = lc.configure(rate=1_000_000, samples=1000)
|
|
37
|
+
assert (cfg.width, cfg.positions, cfg.samples) == (4, [0, 1, 2], 1000)
|
|
38
|
+
assert cfg.rate == 1_000_000
|
|
39
|
+
lc.start()
|
|
40
|
+
(seg,) = lc.wait()
|
|
41
|
+
assert (seg.serial, seg.samples, seg.trigger_index) == (0, 1000, None)
|
|
42
|
+
data = lc.read_segment(seg)
|
|
43
|
+
assert len(data) == 500
|
|
44
|
+
counter_ok(lc, data, 1000)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_the_classic_esp32_sampler_takes_a_byte_a_sample():
|
|
48
|
+
ep, hst, lc, _ = bench(fake.esp32_v003)
|
|
49
|
+
core.plan_apply(hst, [(lc.fn, 0, 4), (lc.fn, 1, 5)])
|
|
50
|
+
cfg = lc.configure(rate=1_000_000, samples=300)
|
|
51
|
+
assert (cfg.width, cfg.positions) == (8, [0, 1])
|
|
52
|
+
lc.start()
|
|
53
|
+
(seg,) = lc.wait()
|
|
54
|
+
counter_ok(lc, lc.read_segment(seg), 300)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_a_rate_is_the_source_divided_by_a_whole_number():
|
|
58
|
+
ep, hst, lc, _ = bench()
|
|
59
|
+
core.plan_apply(hst, [(lc.fn, 0, 20)])
|
|
60
|
+
assert lc.configure(rate=3_000_000, query=True).rate == c.Fraction(20_000_000, 7) # at or under the one asked
|
|
61
|
+
assert lc.config is None # query changes nothing
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize("kind,role,value,index", [
|
|
65
|
+
(c.EDGE, 1, 0, 10), # bit 1 rises at i % 4 == 2: the first at or after the pretrigger 10 is 10
|
|
66
|
+
(c.EDGE, 2, 1, 16), # bit 2 falls at i % 8 == 0
|
|
67
|
+
(c.LEVEL, 3, 1, 10), # bit 3 is 1 on 8..15
|
|
68
|
+
])
|
|
69
|
+
def test_a_trigger_is_where_the_channel_does_it(kind, role, value, index):
|
|
70
|
+
ep, hst, lc, _ = bench()
|
|
71
|
+
core.plan_apply(hst, [(lc.fn, r, 20 + r) for r in range(4)])
|
|
72
|
+
lc.configure(rate=1_000_000, samples=64, trigger=(kind, role, value), pretrigger=10)
|
|
73
|
+
lc.start()
|
|
74
|
+
(seg,) = lc.wait()
|
|
75
|
+
assert seg.trigger_index == index
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_slipped_segments_say_so():
|
|
79
|
+
ep, hst, lc, _ = bench()
|
|
80
|
+
ep.capture_slipped = True
|
|
81
|
+
core.plan_apply(hst, [(lc.fn, 0, 20)])
|
|
82
|
+
lc.configure(rate=1_000_000, samples=64)
|
|
83
|
+
lc.start()
|
|
84
|
+
(seg,) = lc.wait()
|
|
85
|
+
assert seg.slipped
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_repeat_fills_its_ring_with_the_clock_and_goes_on_after_release():
|
|
89
|
+
ep, hst, lc, clock = bench()
|
|
90
|
+
core.plan_apply(hst, [(lc.fn, 0, 20), (lc.fn, 1, 21)])
|
|
91
|
+
cfg = lc.configure(rate=1_000_000, mode=c.REPEAT, samples=1000, segments=3)
|
|
92
|
+
assert cfg.segments == 3
|
|
93
|
+
lc.start()
|
|
94
|
+
clock.t = 2 # 2 ms = 2000 samples: two segments
|
|
95
|
+
assert lc.status()[1] == 2
|
|
96
|
+
clock.t = 10 # the ring (3) fills and the capture stops
|
|
97
|
+
state, done, _, _ = lc.status()
|
|
98
|
+
assert (state, done) == (c.STATE["paused"], 3)
|
|
99
|
+
segs = lc.segments()
|
|
100
|
+
assert [s.serial for s in segs] == [0, 1, 2]
|
|
101
|
+
whole = b"".join(lc.read_segment(s) for s in segs)
|
|
102
|
+
counter_ok(lc, whole, 3000) # segments follow on without a gap
|
|
103
|
+
lc.release(2)
|
|
104
|
+
clock.t = 11
|
|
105
|
+
(nxt,) = lc.segments(3)
|
|
106
|
+
assert nxt.flags & fake_capture.FLAG["gap"] # it stopped: the next segment says so
|
|
107
|
+
assert ep.captures[lc.fn].read(0, 8, 64)[8] & 0x02 # released bytes are gone (read: gap)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_streaming_pushes_the_bytes_while_subscribed():
|
|
111
|
+
ep, hst, lc, clock = bench()
|
|
112
|
+
core.plan_apply(hst, [(lc.fn, 0, 20), (lc.fn, 1, 21), (lc.fn, 2, 22)])
|
|
113
|
+
lc.configure(rate=1_000_000, mode=c.STREAMING)
|
|
114
|
+
lc.subscribe()
|
|
115
|
+
lc.start()
|
|
116
|
+
clock.t = 3 # 3000 samples x 4 bits = 1500 bytes
|
|
117
|
+
frames = ep.pushes()
|
|
118
|
+
assert frames and all(f[0] == m.ROLE_DATA for f in frames)
|
|
119
|
+
seqs = [struct.unpack_from("<H", f, 3)[0] for f in frames]
|
|
120
|
+
assert seqs == list(range(len(frames)))
|
|
121
|
+
data = b"".join(f[13:] for f in frames)
|
|
122
|
+
assert struct.unpack_from("<Q", frames[0], 5)[0] == 0 and len(data) == 1500
|
|
123
|
+
counter_ok(lc, data, 3000)
|
|
124
|
+
lc.stop()
|
|
125
|
+
assert lc.status()[2] == 1500
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_events_go_out_only_while_subscribed():
|
|
129
|
+
ep, hst, lc, _ = bench()
|
|
130
|
+
core.plan_apply(hst, [(lc.fn, 0, 20)])
|
|
131
|
+
lc.configure(rate=1_000_000, samples=64)
|
|
132
|
+
lc.start()
|
|
133
|
+
assert ep.pushes() == []
|
|
134
|
+
lc.subscribe()
|
|
135
|
+
lc.start()
|
|
136
|
+
kinds = [f[5] for f in ep.pushes()]
|
|
137
|
+
assert kinds == [c.EVENT_SEGMENT, c.EVENT_STOPPED] and ep.outbox == []
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_a_capture_listens_on_pins_other_interfaces_hold():
|
|
141
|
+
ep, hst, lc, _ = bench()
|
|
142
|
+
uart = core.find(hst, "oep.fixture.uart")
|
|
143
|
+
core.plan_apply(hst, [(uart, 1, 12), (uart, 2, 6)])
|
|
144
|
+
core.plan_apply(hst, [(lc.fn, 0, 12)]) # the UART's RX, captured too
|
|
145
|
+
gpio = core.find(hst, "oep.fixture.gpio")
|
|
146
|
+
with pytest.raises(h.Rejected):
|
|
147
|
+
core.plan_apply(hst, [(gpio, 1, 12)]) # the UART still holds it against drivers
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_fake_serve_streams_over_tcp_to_the_client():
|
|
151
|
+
"""The whole path wireskein takes: open_host, take, plan, configure, subscribe, start, stream, stop, finish."""
|
|
152
|
+
import subprocess
|
|
153
|
+
import sys
|
|
154
|
+
|
|
155
|
+
from oep_client import link
|
|
156
|
+
proc = subprocess.Popen([sys.executable, "-m", "oep_client.fake_serve", "--tcp", "0", "--framing", "length",
|
|
157
|
+
"--capture-slipped"],
|
|
158
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
|
|
159
|
+
try:
|
|
160
|
+
port = proc.stdout.readline().split()[1]
|
|
161
|
+
hst = link.open_host(f"tcp://127.0.0.1:{port}", timeout=2.0)
|
|
162
|
+
core.take(hst, 5000, owner="test")
|
|
163
|
+
lc = c.LogicCapture(hst)
|
|
164
|
+
core.plan_apply(hst, [(lc.fn, 0, 20), (lc.fn, 1, 21)])
|
|
165
|
+
lc.configure(rate=100_000, samples=500) # one-shot first
|
|
166
|
+
lc.start()
|
|
167
|
+
(seg,) = lc.wait()
|
|
168
|
+
assert seg.slipped
|
|
169
|
+
counter_ok(lc, lc.read_segment(seg), 500)
|
|
170
|
+
lc.configure(rate=100_000, mode=c.STREAMING) # then streaming: 25 kB/s at w 2
|
|
171
|
+
lc.subscribe()
|
|
172
|
+
lc.start()
|
|
173
|
+
got = lc.stream(hst.link, nbytes=2000, seconds=3)
|
|
174
|
+
lc.stop()
|
|
175
|
+
lc.finish(hst.link, got)
|
|
176
|
+
assert got.start == 0 and not got.gaps and got.seq_lost == 0 and len(got.data) >= 2000
|
|
177
|
+
counter_ok(lc, got.data, len(got.data) * 4)
|
|
178
|
+
hst.end()
|
|
179
|
+
hst.link.close()
|
|
180
|
+
finally:
|
|
181
|
+
proc.stdin.close()
|
|
182
|
+
proc.wait(timeout=5)
|
|
@@ -455,12 +455,14 @@ def test_a_wire_takes_any_free_pair_the_host_names():
|
|
|
455
455
|
assert h.raw(1, 0x02, b"\x01" + pins(1, 2)).detail == m.UNAVAILABLE # GP1 is the live connection's
|
|
456
456
|
assert h.raw(0, m.OP_PLAN_APPLY, m.tlv(0x90, struct.pack("<HBH", 4, 1, 0))).detail == m.UNAVAILABLE
|
|
457
457
|
assert m.Reader(h.ok(1, 0x01, b"\x00")).take("B") == 1 # its one seat is taken: the live pair only
|
|
458
|
+
assert struct.unpack_from("<H", h.ok(1, 0x02, b"\x00"))[0] == conn # no pins: the one live connection
|
|
458
459
|
assert h.raw(1, 0x01, b"\x01" + struct.pack("<HH", 2, 3)).detail == m.UNAVAILABLE
|
|
459
460
|
h.ok(1, 0x03, struct.pack("<H", conn) + m.tlv(0x01, b"")) # detach (force): the pins go back
|
|
460
461
|
h.ok(0, m.OP_PLAN_APPLY, m.tlv(0x90, struct.pack("<HBH", 4, 1, 5)))
|
|
461
462
|
assert h.raw(1, 0x01, b"\x01" + struct.pack("<HH", 5, 6)).detail == m.UNAVAILABLE # GP5 is the plan's
|
|
462
463
|
assert h.raw(1, 0x02, b"\x01" + pins(3, 3)).detail == m.UNAVAILABLE # one channel twice
|
|
463
464
|
assert h.raw(1, 0x02, b"\x01" + pins(19, 3)).detail == m.UNAVAILABLE # not offered (PSRAM CS)
|
|
465
|
+
assert h.raw(1, 0x02, b"\x01").detail == m.UNAVAILABLE # no pins, no live connection: the host chooses
|
|
464
466
|
r = h.raw(1, 0x02, b"\x01" + pins(7, 8)) # a free pair nothing answers on
|
|
465
467
|
assert r.resolution == m.COMPLETED and not r.succeeded
|
|
466
468
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|