oep-client-python 0.0.9__tar.gz → 0.0.10__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.9 → oep_client_python-0.0.10}/CHANGELOG.md +4 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/PKG-INFO +6 -2
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/README.ja.md +4 -1
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/README.md +5 -1
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/pyproject.toml +1 -1
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/__init__.py +1 -1
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/capture.py +147 -17
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/endpoint.py +75 -7
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/fake.py +29 -1
- oep_client_python-0.0.10/src/oep_client/fake_capture.py +437 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/registry.py +8 -5
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_capabilities.py +3 -3
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_capture.py +1 -1
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_fake_capture.py +74 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_link_serial.py +1 -1
- oep_client_python-0.0.9/src/oep_client/fake_capture.py +0 -267
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/.gitignore +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/LICENSE +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/__main__.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/arm.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/catalog.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/ch32_flash.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/cobs.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/config.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/console.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/core.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/decode.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/dump.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/esp32_targets.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/fake_serial.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/fake_serve.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/fixture.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/frames.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/hid_stream.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/host.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/interfaces.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/link.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/message.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/names.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/riscv.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/rp2350.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/target.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/uiapduino.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/src/oep_client/usb_stream.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_cobs.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_config.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_esp32_targets.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_fake_spec.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_flash_console.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_hid_stream.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_interfaces.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_link_host.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_session.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_target_parts.py +0 -0
- {oep_client_python-0.0.9 → oep_client_python-0.0.10}/tests/test_usb_stream.py +0 -0
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.10
|
|
6
|
+
- (EN) Captures' times are ns on the probe's one clock with an uncertainty (oep-if-capture): `Segment.start_ns` / `start_uncertainty_ns` replace `start_us` (a segment is 33 bytes), the triggered event carries `trigger_ns`, and `Config.rate_measured` / `rate_ppm` say how sure the rate is. Analog captures: `AnalogCapture` (raw `values()`, the probe's `millivolts()`, `calibration()` - factory data and Vrefint, raw), with the answer read per channel (`zero`, `scale_nv`, `skew_ns`, `frontend`) and `reference`; `configure(frontends=)`. `CaptureGroup` binds tracks, starts them together with one trigger, and gives the group's `start_ns` and `trigger_ns`. The fake has them all (p4-x035: an analog on the ADC1 and a group), and describes a `chip`.
|
|
7
|
+
- (JA) キャプチャの時刻は、probe の 1 本の時計の ns と不確かさになった(oep-if-capture): `start_us` は `Segment.start_ns` / `start_uncertainty_ns` になり(区画は 33 byte)、出来事 triggered は `trigger_ns` を持ち、`Config.rate_measured` / `rate_ppm` がレートの確かさを示す。アナログ: `AnalogCapture`(生の `values()`、probe の 1 次式の `millivolts()`、`calibration()`: 出荷時の値と Vrefint を生のまま)。答えはチャネルごとに読む(`zero`、`scale_nv`、`skew_ns`、`frontend`)、`reference` も。`configure(frontends=)`。`CaptureGroup` はトラックを束ね、1 つのトリガで一緒に始め、組の `start_ns` と `trigger_ns` を返す。偽の probe はこれらをすべて持つ(p4-x035: ADC1 のアナログと組)。core の describe の `chip` も出す。
|
|
8
|
+
|
|
5
9
|
## 0.0.9
|
|
6
10
|
- (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
11
|
- (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 を立てる。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: oep-client-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
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
|
|
@@ -115,7 +115,11 @@ resumed after it).
|
|
|
115
115
|
release) and streaming (data pushes while subscribed), level / edge triggers with a pretrigger, and events. What it
|
|
116
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
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.
|
|
118
|
+
sampler, one-shot only). A capture only listens, so it may be planned on pins other interfaces hold. `p4-x035` also has
|
|
119
|
+
`oep.fixture.analog` (4 channels of the P4's ADC1 on GPIO16-23: an even channel k a square wave, an odd one a sine, of
|
|
120
|
+
period 64 (k // 2 + 1) samples, 12-bit values in 16-bit slots; ESP32-style frontends; a made-up two-point calibration and a
|
|
121
|
+
Vrefint) and `oep.fixture.capture-group` binding the logic and the analog: started together, the analog 5 us later
|
|
122
|
+
(+-2 us), the trigger of one marked on both. Times are ns on the probe's one clock.
|
|
119
123
|
|
|
120
124
|
Other programs' tests run `fake_serve` as a child process:
|
|
121
125
|
|
|
@@ -87,7 +87,10 @@ oep config save <probe> # 再起動の後も残す(remove
|
|
|
87
87
|
ストリーミング(購読している間のデータの push)、レベル / エッジのトリガとプリトリガ、出来事。取れるものは決まっている: サンプル i は
|
|
88
88
|
カウンタの値 i で、チャネル k はそのビット k(周期 2^(k+1) サンプルの方形波)。layout は profile が許す形(`p4-x035`: P4 の
|
|
89
89
|
PARLIO と同じ w 1〜16、3 本なら w 4。`esp32-v003`: classic ESP32 の sampler と同じ w 8、ワンショットだけ)。capture は聞くだけ
|
|
90
|
-
なので、ほかのインターフェースが持つピンにも plan
|
|
90
|
+
なので、ほかのインターフェースが持つピンにも plan できる。`p4-x035` には `oep.fixture.analog`(P4 の ADC1 の 4 チャネル、GPIO16〜23。
|
|
91
|
+
偶数のチャネル k は方形波、奇数は正弦波で、周期は 64 (k // 2 + 1) サンプル。16 ビットの枠に 12 ビットの値。ESP32 と同じ形の
|
|
92
|
+
frontend、作り物の 2 点の較正と Vrefint)と、ロジックとアナログを束ねる `oep.fixture.capture-group` もある(一緒に始まり、
|
|
93
|
+
アナログは 5 µs 遅れ(±2 µs)、片方のトリガが両方に印される)。時刻は probe の 1 本の時計の ns。
|
|
91
94
|
|
|
92
95
|
外のプログラムの試験には `fake_serve` を子プロセスで使う:
|
|
93
96
|
|
|
@@ -93,7 +93,11 @@ resumed after it).
|
|
|
93
93
|
release) and streaming (data pushes while subscribed), level / edge triggers with a pretrigger, and events. What it
|
|
94
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
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.
|
|
96
|
+
sampler, one-shot only). A capture only listens, so it may be planned on pins other interfaces hold. `p4-x035` also has
|
|
97
|
+
`oep.fixture.analog` (4 channels of the P4's ADC1 on GPIO16-23: an even channel k a square wave, an odd one a sine, of
|
|
98
|
+
period 64 (k // 2 + 1) samples, 12-bit values in 16-bit slots; ESP32-style frontends; a made-up two-point calibration and a
|
|
99
|
+
Vrefint) and `oep.fixture.capture-group` binding the logic and the analog: started together, the analog 5 us later
|
|
100
|
+
(+-2 us), the trigger of one marked on both. Times are ns on the probe's one clock.
|
|
97
101
|
|
|
98
102
|
Other programs' tests run `fake_serve` as a child process:
|
|
99
103
|
|
|
@@ -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.10"
|
|
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"
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
"""oep.fixture.capture / oep.fixture.analog revision 1 (oep-spec docs/oep-if-capture.ja.md:
|
|
2
|
-
§3 operations). Numbers from `registry`.
|
|
1
|
+
"""oep.fixture.capture / oep.fixture.analog / oep.fixture.capture-group revision 1 (oep-spec docs/oep-if-capture.ja.md:
|
|
2
|
+
§1 layouts, §2 segments, §3 operations, §3.8 calibration, §4 groups). Numbers from `registry`.
|
|
3
|
+
|
|
4
|
+
Times are the probe's one clock (ns since its boot, comparable within one boot_id): estimates with an uncertainty, the
|
|
5
|
+
probe's known corrections applied. Analog values are always raw; the probe's 1st-order scale, its calibration data and
|
|
6
|
+
its reference are for the host to choose from.
|
|
3
7
|
|
|
4
8
|
configure: a TLV the probe cannot honour is refused (rejected unsupported, 0x0B, payload = the tag) when the host marked
|
|
5
9
|
it critical, and otherwise ignored and listed in the answer's ignored TLV (0x7F, oep-core §2.3)."""
|
|
@@ -17,12 +21,17 @@ from .frames import FramingLost
|
|
|
17
21
|
from .core import Interface, confirm
|
|
18
22
|
|
|
19
23
|
_CAP = reg.FIXTURE_CAPTURE
|
|
24
|
+
_ANA = reg.FIXTURE_ANALOG
|
|
25
|
+
_GRP = reg.FIXTURE_CAPTURE_GROUP
|
|
20
26
|
# configure TLVs; bit 7 of a tag = critical (the probe must reject what it cannot do)
|
|
21
27
|
MODE, RATE, SAMPLES, SEGMENTS, TRIGGER, PRETRIGGER, FRONTEND = (
|
|
22
28
|
_CAP.tlv["configure"][k] for k in ("mode", "rate", "samples", "segments", "trigger", "pretrigger", "frontend"))
|
|
23
|
-
ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, TIMING, SCALE, BLOCKING
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, TIMING, SCALE, BLOCKING, SKEW, FRONTEND_USED, REFERENCE, \
|
|
30
|
+
RATE_ACCURACY = (_ANA.tlv["configure_answer"][k] for k in (
|
|
31
|
+
"actual_rate", "layout", "actual_samples", "actual_segments", "timing", "scale", "blocking_ms", "skew",
|
|
32
|
+
"frontend_used", "reference", "rate_accuracy"))
|
|
33
|
+
FACTORY, VREFINT = _ANA.tlv["calibration_answer"]["factory"], _ANA.tlv["calibration_answer"]["vrefint"]
|
|
34
|
+
REFERENCE_SOURCE = {v: k for k, v in _ANA.enum["reference_source"].items()}
|
|
26
35
|
IGNORED = m.TAG_IGNORED
|
|
27
36
|
CRITICAL = m.TAG_CRITICAL
|
|
28
37
|
ONE_SHOT, REPEAT, STREAMING = (_CAP.enum["mode"][k] for k in ("one_shot", "repeat", "streaming"))
|
|
@@ -37,7 +46,8 @@ def tlvs(payload: bytes) -> list[tuple[int, bytes]]:
|
|
|
37
46
|
return m.split_tlvs(payload)
|
|
38
47
|
|
|
39
48
|
|
|
40
|
-
SEGMENT_BYTES =
|
|
49
|
+
SEGMENT_BYTES = 33 # serial u32, position u64, samples u32, start_ns u64, start_uncertainty_ns u32, trigger_index u32,
|
|
50
|
+
# flags u8
|
|
41
51
|
|
|
42
52
|
|
|
43
53
|
@dataclass
|
|
@@ -45,7 +55,8 @@ class Segment:
|
|
|
45
55
|
serial: int
|
|
46
56
|
position: int
|
|
47
57
|
samples: int
|
|
48
|
-
|
|
58
|
+
start_ns: int # the first sample's time on the probe's clock (an estimate)
|
|
59
|
+
start_uncertainty_ns: int # +- of start_ns (a guide, not a promise)
|
|
49
60
|
trigger_index: int | None
|
|
50
61
|
flags: int
|
|
51
62
|
|
|
@@ -56,8 +67,8 @@ class Segment:
|
|
|
56
67
|
|
|
57
68
|
@classmethod
|
|
58
69
|
def unpack(cls, b: bytes) -> "Segment":
|
|
59
|
-
serial, position, samples,
|
|
60
|
-
return cls(serial, position, samples,
|
|
70
|
+
serial, position, samples, start_ns, uncertainty, trig, flags = struct.unpack_from("<IQIQIIB", b)
|
|
71
|
+
return cls(serial, position, samples, start_ns, uncertainty, None if trig == 0xFFFFFFFF else trig, flags)
|
|
61
72
|
|
|
62
73
|
|
|
63
74
|
@dataclass
|
|
@@ -74,9 +85,13 @@ class Config:
|
|
|
74
85
|
segments: int = 0
|
|
75
86
|
jitter_kind: int = 0
|
|
76
87
|
jitter_ns: int = 0
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
rate_measured: bool = False # rate_accuracy: the rate was measured (else computed from a divider)
|
|
89
|
+
rate_ppm: int = 0 # its uncertainty (0: unknown)
|
|
90
|
+
skew_ns: dict[int, int] = field(default_factory=dict) # analog, per channel (role)
|
|
91
|
+
zero: dict[int, int] = field(default_factory=dict) # analog, per channel
|
|
92
|
+
scale_nv: dict[int, int] = field(default_factory=dict) # analog, nV per value, per channel
|
|
93
|
+
frontend: dict[int, int] = field(default_factory=dict) # analog: the frontend each channel took
|
|
94
|
+
reference: tuple[str, int, bool] | None = None # analog: (source, mV, measured)
|
|
80
95
|
blocking_ms: int = 0
|
|
81
96
|
ignored: list[int] = field(default_factory=list)
|
|
82
97
|
|
|
@@ -140,9 +155,19 @@ def _config(payload: bytes, analog: bool) -> Config:
|
|
|
140
155
|
c.segments = struct.unpack("<I", v)[0]
|
|
141
156
|
elif tag == TIMING:
|
|
142
157
|
c.jitter_kind, c.jitter_ns = v[0], struct.unpack_from("<I", v, 1)[0]
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
elif tag == RATE_ACCURACY:
|
|
159
|
+
c.rate_measured, c.rate_ppm = v[0] == 1, struct.unpack_from("<I", v, 1)[0]
|
|
160
|
+
elif tag == SCALE and analog:
|
|
161
|
+
role, zero, scale = struct.unpack("<BII", v)
|
|
162
|
+
c.zero[role], c.scale_nv[role] = zero, scale
|
|
163
|
+
elif tag == SKEW and analog:
|
|
164
|
+
role, ns = struct.unpack("<BI", v)
|
|
165
|
+
c.skew_ns[role] = ns
|
|
166
|
+
elif tag == FRONTEND_USED and analog:
|
|
167
|
+
c.frontend[v[0]] = v[1]
|
|
168
|
+
elif tag == REFERENCE and analog:
|
|
169
|
+
source, mv, how = struct.unpack("<BIB", v)
|
|
170
|
+
c.reference = (REFERENCE_SOURCE.get(source, str(source)), mv, how == 1)
|
|
146
171
|
elif tag == BLOCKING:
|
|
147
172
|
c.blocking_ms = struct.unpack("<I", v)[0]
|
|
148
173
|
elif tag == IGNORED:
|
|
@@ -165,7 +190,7 @@ class LogicCapture(Interface):
|
|
|
165
190
|
|
|
166
191
|
def configure(self, *, rate: int, mode: int = ONE_SHOT, samples: int | None = None, segments: int | None = None,
|
|
167
192
|
trigger: tuple[int, int, int] | None = None, pretrigger: int | None = None, query: bool = False,
|
|
168
|
-
critical: set[int] = frozenset()) -> Config:
|
|
193
|
+
critical: set[int] = frozenset(), frontends: dict[int, int] | None = None) -> Config:
|
|
169
194
|
"""-> the probe's actual values (Config.ignored: tags the probe ignored). `critical`: tags the probe must honour
|
|
170
195
|
or reject (host.Unsupported, .tag = the one it cannot)."""
|
|
171
196
|
def tlv(tag: int, value: bytes) -> bytes:
|
|
@@ -179,6 +204,8 @@ class LogicCapture(Interface):
|
|
|
179
204
|
body += tlv(TRIGGER, struct.pack("<BBH", *trigger))
|
|
180
205
|
if pretrigger is not None:
|
|
181
206
|
body += tlv(PRETRIGGER, struct.pack("<I", pretrigger))
|
|
207
|
+
for role, fe in sorted((frontends or {}).items()): # analog: the input range per channel (describe frontend)
|
|
208
|
+
body += tlv(FRONTEND, bytes([role, fe]))
|
|
182
209
|
# query is its own operation: the lock is decided per operation, before the payload is looked at
|
|
183
210
|
op = self.QUERY_OP if query else self.CONFIGURE
|
|
184
211
|
c = _config(self._call(op, body, locked=not query).payload, self.ANALOG)
|
|
@@ -331,7 +358,8 @@ class LogicCapture(Interface):
|
|
|
331
358
|
"""The segment's bytes; every Host.on_capture callback gets them as a CaptureRecord (a run recorder, e.g.
|
|
332
359
|
pytest-embedded-wireskein, without this package knowing it)."""
|
|
333
360
|
c = self.config
|
|
334
|
-
|
|
361
|
+
n = (segment.samples * c.width + 7) // 8 if c.width else segment.samples * len(c.order) * c.slot // 8
|
|
362
|
+
data = self.read(segment.position, n)
|
|
335
363
|
if self.host.on_capture:
|
|
336
364
|
record = CaptureRecord(self.fn, self.name, c, segment, data, self.armed_s, time.monotonic())
|
|
337
365
|
for callback in list(self.host.on_capture):
|
|
@@ -365,3 +393,105 @@ class LogicCapture(Interface):
|
|
|
365
393
|
z.writestr("version", "2")
|
|
366
394
|
z.writestr("metadata", "\n".join(meta))
|
|
367
395
|
z.writestr("logic-1-1", out)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@dataclass
|
|
399
|
+
class Calibration:
|
|
400
|
+
"""What the probe knows for turning an analog value into a voltage (oep-if-capture §3.8), raw: the probe applies
|
|
401
|
+
none of it. factory: (frontend or None, scheme, raw bytes) - scheme names how to read raw; vrefint: (raw, ns), the
|
|
402
|
+
internal reference measured after the last start."""
|
|
403
|
+
factory: list[tuple[int | None, str, bytes]] = field(default_factory=list)
|
|
404
|
+
vrefint: tuple[int, int] | None = None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class AnalogCapture(LogicCapture):
|
|
408
|
+
"""Basic analog capture (oep.fixture.analog): the same operations as the logic one; values are raw (§1.2)."""
|
|
409
|
+
NAME = "oep.fixture.analog"
|
|
410
|
+
ANALOG = True
|
|
411
|
+
CALIBRATION = _ANA.op["calibration"]
|
|
412
|
+
|
|
413
|
+
def values(self, data: bytes, k: int, samples: int | None = None) -> list[int]:
|
|
414
|
+
"""Channel k's raw values (§1.2: slot s bits little endian, the value in bits o .. o+b-1, frames in `order`)."""
|
|
415
|
+
c = self.config
|
|
416
|
+
width = c.slot // 8
|
|
417
|
+
frame = width * len(c.order)
|
|
418
|
+
n = samples if samples is not None else len(data) // frame
|
|
419
|
+
m_ = c.order.index(k)
|
|
420
|
+
mask = (1 << c.bits) - 1
|
|
421
|
+
return [(int.from_bytes(data[i * frame + m_ * width:i * frame + (m_ + 1) * width], "little") >> c.offset) & mask
|
|
422
|
+
for i in range(n)]
|
|
423
|
+
|
|
424
|
+
def millivolts(self, k: int, value: int) -> float:
|
|
425
|
+
"""The probe's own 1st-order reading of a raw value of channel k, (value - zero) x scale_nv (a nominal
|
|
426
|
+
reference: see reference and calibration() for others)."""
|
|
427
|
+
c = self.config
|
|
428
|
+
return (value - c.zero.get(k, 0)) * c.scale_nv.get(k, 0) / 1_000_000
|
|
429
|
+
|
|
430
|
+
def calibration(self) -> Calibration:
|
|
431
|
+
out = Calibration()
|
|
432
|
+
for tag, v in tlvs(self._call(self.CALIBRATION, locked=False).payload):
|
|
433
|
+
if tag == FACTORY:
|
|
434
|
+
n = v[1]
|
|
435
|
+
out.factory.append((None if v[0] == 0xFF else v[0], v[2:2 + n].decode("utf-8", "replace"), bytes(v[2 + n:])))
|
|
436
|
+
elif tag == VREFINT:
|
|
437
|
+
out.vrefint = struct.unpack("<IQ", v)
|
|
438
|
+
return out
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@dataclass
|
|
442
|
+
class GroupStatus:
|
|
443
|
+
state: int
|
|
444
|
+
start_ns: int | None
|
|
445
|
+
trigger_ns: int | None
|
|
446
|
+
trigger_fn: int | None
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class CaptureGroup(Interface):
|
|
450
|
+
"""oep.fixture.capture-group (§4): tracks (LogicCapture / AnalogCapture, each configured as usual) started together,
|
|
451
|
+
one of them the trigger. Each track is read as usual; a track's offset is its first segment's start_ns minus the
|
|
452
|
+
group's start_ns, and every track's segment marks the trigger's instant (trigger_index)."""
|
|
453
|
+
NAME = "oep.fixture.capture-group"
|
|
454
|
+
REVISION = 1
|
|
455
|
+
BIND, START, STOP, FORCE, STATUS = (_GRP.op[k] for k in ("bind", "start", "stop", "force", "status"))
|
|
456
|
+
TAG_TRIGGER_TRACK = _GRP.tlv["bind"]["trigger_track"]
|
|
457
|
+
EVENT_TRIGGERED, EVENT_STOPPED = _GRP.event["triggered"], _GRP.event["stopped"]
|
|
458
|
+
NO_TIME = 0xFFFFFFFFFFFFFFFF
|
|
459
|
+
|
|
460
|
+
def bind(self, tracks: list[LogicCapture], trigger: LogicCapture | None = None) -> None:
|
|
461
|
+
"""Bind these (configured) tracks; [] unbinds. `trigger`: the track whose configure trigger starts them all."""
|
|
462
|
+
body = struct.pack(f"<B{len(tracks)}H", len(tracks), *(t.fn for t in tracks))
|
|
463
|
+
if trigger is not None:
|
|
464
|
+
body += m.tlv(self.TAG_TRIGGER_TRACK, struct.pack("<H", trigger.fn), critical=True)
|
|
465
|
+
self._call(self.BIND, body)
|
|
466
|
+
|
|
467
|
+
def start(self, tracks: list[LogicCapture] = ()) -> tuple[int, int]:
|
|
468
|
+
"""-> (blocking_ms, the group's start_ns). `tracks`: whose armed_s to set (for records)."""
|
|
469
|
+
blocking, start_ns = m.Reader(self._call(self.START).payload).take("IQ")
|
|
470
|
+
now = time.monotonic()
|
|
471
|
+
for t in tracks:
|
|
472
|
+
t.armed_s = now
|
|
473
|
+
return blocking, start_ns
|
|
474
|
+
|
|
475
|
+
def stop(self) -> None:
|
|
476
|
+
self._call(self.STOP)
|
|
477
|
+
|
|
478
|
+
def force(self) -> None:
|
|
479
|
+
self._call(self.FORCE)
|
|
480
|
+
|
|
481
|
+
def status(self) -> GroupStatus:
|
|
482
|
+
state, start, trig, fn = m.Reader(self._call(self.STATUS, locked=False).payload).take("BQQH")
|
|
483
|
+
none = self.NO_TIME
|
|
484
|
+
return GroupStatus(state, None if start == none else start, None if trig == none else trig, fn or None)
|
|
485
|
+
|
|
486
|
+
def wait(self, timeout: float = 5.0) -> GroupStatus:
|
|
487
|
+
"""Poll until every track is done (one-shot)."""
|
|
488
|
+
deadline = time.monotonic() + timeout
|
|
489
|
+
while time.monotonic() < deadline:
|
|
490
|
+
st = self.status()
|
|
491
|
+
if st.state == STATE["done"]:
|
|
492
|
+
return st
|
|
493
|
+
if st.state == STATE["error"]:
|
|
494
|
+
raise h.Failed(None, "the group's capture stopped with an error")
|
|
495
|
+
time.sleep(0.002)
|
|
496
|
+
raise TimeoutError("the group's capture did not finish")
|
|
497
|
+
|
|
@@ -276,7 +276,10 @@ class Endpoint:
|
|
|
276
276
|
self.targets[(fn, first)] = FakeTarget()
|
|
277
277
|
self.target = next(iter(self.targets.values()), FakeTarget())
|
|
278
278
|
self.captures: dict[int, fake_capture.FakeCapture] = {
|
|
279
|
-
fn: self._capture_from(self.static[fn]) for fn, name in self.names.items()
|
|
279
|
+
fn: self._capture_from(self.static[fn]) for fn, name in self.names.items()
|
|
280
|
+
if name in ("oep.fixture.capture", "oep.fixture.analog")}
|
|
281
|
+
self.groups: dict[int, fake_capture.FakeGroup] = {
|
|
282
|
+
fn: self._group_from(self.static[fn]) for fn, name in self.names.items() if name == "oep.fixture.capture-group"}
|
|
280
283
|
self.mechanisms = set()
|
|
281
284
|
for fn, name in self.names.items():
|
|
282
285
|
if name == "oep.target.console":
|
|
@@ -349,10 +352,13 @@ class Endpoint:
|
|
|
349
352
|
def _capture_from(tlvs: list[bytes]) -> fake_capture.FakeCapture:
|
|
350
353
|
"""A capture as its describe declares it (modes, the w allowed, the rate range, max_read, the ring)."""
|
|
351
354
|
d = reg.FIXTURE_CAPTURE.tlv["describe"]
|
|
352
|
-
modes, widths, lo, hi, ring, most = set(), {8}, 1, 1_000_000, 1, 1024
|
|
355
|
+
modes, widths, lo, hi, ring, most, fronts = set(), {8}, 1, 1_000_000, 1, 1024, {}
|
|
353
356
|
for t in tlvs:
|
|
354
357
|
tag, v = t[0], t[2:2 + t[1]]
|
|
355
|
-
if tag == d["
|
|
358
|
+
if tag == d["frontend"]: # analog: frontend min_mv max_mv attenuation_mdb
|
|
359
|
+
fe, lo_mv, hi_mv, mdb = struct.unpack("<BiiI", v)
|
|
360
|
+
fronts[fe] = (lo_mv, hi_mv, mdb)
|
|
361
|
+
elif tag == d["mode"]:
|
|
356
362
|
modes.add(v[0])
|
|
357
363
|
elif tag == d["channels"]:
|
|
358
364
|
widths = {1 << i for i in range(6) if v[1] >> i & 1}
|
|
@@ -364,7 +370,21 @@ class Endpoint:
|
|
|
364
370
|
ring = struct.unpack("<H", v)[0]
|
|
365
371
|
elif tag == d["max_read"]:
|
|
366
372
|
most = struct.unpack("<I", v)[0]
|
|
367
|
-
return fake_capture.FakeCapture(modes or {fake_capture.MODE["one_shot"]}, widths, lo, hi, ring, most)
|
|
373
|
+
return fake_capture.FakeCapture(modes or {fake_capture.MODE["one_shot"]}, widths, lo, hi, ring, most, fronts)
|
|
374
|
+
|
|
375
|
+
@staticmethod
|
|
376
|
+
def _group_from(tlvs: list[bytes]) -> fake_capture.FakeGroup:
|
|
377
|
+
d = reg.FIXTURE_CAPTURE_GROUP.tlv["describe"]
|
|
378
|
+
tracks, most, budgets = [], 1, []
|
|
379
|
+
for t in tlvs:
|
|
380
|
+
tag, v = t[0], t[2:2 + t[1]]
|
|
381
|
+
if tag == d["tracks"]:
|
|
382
|
+
tracks = list(struct.unpack(f"<{len(v) // 2}H", v))
|
|
383
|
+
elif tag == d["max_tracks"]:
|
|
384
|
+
most = v[0]
|
|
385
|
+
elif tag == d["budget"]:
|
|
386
|
+
budgets.append((struct.unpack_from("<I", v)[0], list(struct.unpack_from(f"<{(len(v) - 4) // 2}H", v, 4))))
|
|
387
|
+
return fake_capture.FakeGroup(tracks, most, budgets)
|
|
368
388
|
|
|
369
389
|
def _next_seq(self, fn: int) -> int:
|
|
370
390
|
seq = self.push_seq.get(fn, 0)
|
|
@@ -394,12 +414,20 @@ class Endpoint:
|
|
|
394
414
|
budget = self.probe.max_frame - m.RESULT_HEADER
|
|
395
415
|
try:
|
|
396
416
|
if op in (O["configure"], O["query"]):
|
|
397
|
-
|
|
398
|
-
|
|
417
|
+
rest = t.data[t.at:]
|
|
418
|
+
analog = self.names[fn] == "oep.fixture.analog"
|
|
419
|
+
known = set(fake_capture.TLV.values()) - (set() if analog else {fake_capture.TLV["frontend"]})
|
|
420
|
+
got, ignored = t.tail(known)
|
|
421
|
+
# the frontend TLV comes once per channel: read them all
|
|
422
|
+
fronts = [tuple(v[:2]) for tag, v in m.split_tlvs(rest) if tag & 0x7F == fake_capture.TLV["frontend"]] \
|
|
423
|
+
if analog else []
|
|
424
|
+
settled = cap.settle(got, t.critical, len(roles), fronts)
|
|
399
425
|
if op == O["configure"]:
|
|
400
426
|
cap.apply(settled)
|
|
401
427
|
cap.slipped = self.capture_slipped
|
|
402
428
|
return self._answer(cap.answer(settled), ignored)
|
|
429
|
+
if op in (O["start"], O["stop"], O["force"]) and cap.group is not None:
|
|
430
|
+
raise Reject(m.UNAVAILABLE) # bound: the group starts and stops it
|
|
403
431
|
if op == O["start"]:
|
|
404
432
|
t.tail()
|
|
405
433
|
self._events(fn, cap.start(self.now()))
|
|
@@ -426,6 +454,45 @@ class Endpoint:
|
|
|
426
454
|
t.tail()
|
|
427
455
|
cap.release(serial, self.now())
|
|
428
456
|
return m.COMPLETED, m.SUCCESS, b""
|
|
457
|
+
if op == fake_capture.ANA.op["calibration"] and cap.analog:
|
|
458
|
+
t.tail()
|
|
459
|
+
return m.COMPLETED, m.SUCCESS, cap.calibration()
|
|
460
|
+
except fake_capture.Reject as e:
|
|
461
|
+
raise Reject(e.reason, e.payload)
|
|
462
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
463
|
+
|
|
464
|
+
def _group(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
465
|
+
grp, O = self.groups[fn], fake_capture.GRP.op
|
|
466
|
+
try:
|
|
467
|
+
if op == O["bind"]:
|
|
468
|
+
n = t.take("B")
|
|
469
|
+
fns = [t.take("H") for _ in range(n)]
|
|
470
|
+
got, ignored = t.tail({fake_capture.GRP.tlv["bind"]["trigger_track"]})
|
|
471
|
+
src = got.get(fake_capture.GRP.tlv["bind"]["trigger_track"])
|
|
472
|
+
grp.bind(self.captures, fns, struct.unpack("<H", src)[0] if src else 0)
|
|
473
|
+
return self._answer(b"", ignored)
|
|
474
|
+
if op == O["start"]:
|
|
475
|
+
t.tail()
|
|
476
|
+
per, own = grp.start(self.captures, self.now())
|
|
477
|
+
for track, events in per:
|
|
478
|
+
self._events(track, events)
|
|
479
|
+
self._events(fn, own)
|
|
480
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<IQ", 0, grp.start_ns)
|
|
481
|
+
if op == O["stop"]:
|
|
482
|
+
t.tail()
|
|
483
|
+
per, own = grp.stop(self.captures)
|
|
484
|
+
for track, events in per:
|
|
485
|
+
self._events(track, events)
|
|
486
|
+
self._events(fn, own)
|
|
487
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
488
|
+
if op == O["force"]:
|
|
489
|
+
t.tail()
|
|
490
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
491
|
+
if op == O["status"]:
|
|
492
|
+
t.tail()
|
|
493
|
+
for track in grp.tracks:
|
|
494
|
+
self._events(track, self.captures[track].tick(self.now()))
|
|
495
|
+
return m.COMPLETED, m.SUCCESS, grp.status(self.captures)
|
|
429
496
|
except fake_capture.Reject as e:
|
|
430
497
|
raise Reject(e.reason, e.payload)
|
|
431
498
|
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
@@ -1655,4 +1722,5 @@ class Endpoint:
|
|
|
1655
1722
|
|
|
1656
1723
|
SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
|
|
1657
1724
|
"oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
|
|
1658
|
-
"oep.probe.config": "config_op", "oep.fixture.capture": "capture"
|
|
1725
|
+
"oep.probe.config": "config_op", "oep.fixture.capture": "capture", "oep.fixture.analog": "capture",
|
|
1726
|
+
"oep.fixture.capture-group": "group"}
|
|
@@ -109,6 +109,27 @@ SERIAL_KINDS = {TRANSPORT["uart_bridge"], TRANSPORT["usb_cdc"], TRANSPORT["usb_s
|
|
|
109
109
|
NS = "io.github.ch32-riscv-ug"
|
|
110
110
|
MECHANISMS = reg.TARGET_CONSOLE.tlv["describe"]["mechanisms"]
|
|
111
111
|
_CAPD = reg.FIXTURE_CAPTURE.tlv["describe"]
|
|
112
|
+
_ANAD = reg.FIXTURE_ANALOG.tlv["describe"]
|
|
113
|
+
_GRPD = reg.FIXTURE_CAPTURE_GROUP.tlv["describe"]
|
|
114
|
+
CORE_CHIP = reg.CORE.tlv["describe"]["chip"]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _analog_decl(frontends: list[tuple[int, int, int, int]], max_samples: int) -> tuple[bytes, ...]:
|
|
118
|
+
"""oep.fixture.analog's declarations: one-shot / repeat / streaming, 4 channels in 16-bit slots, the frontends
|
|
119
|
+
(number, min_mv, max_mv, attenuation_mdb), triggers cross up / down."""
|
|
120
|
+
out = tuple(catalog.tlv(_ANAD["mode"], struct.pack("<BBII", _CAPM[k], 1, max_samples, 8 if k == "repeat" else 1))
|
|
121
|
+
for k in ("one_shot", "repeat", "streaming"))
|
|
122
|
+
out += tuple(catalog.tlv(_ANAD["frontend"], struct.pack("<BiiI", *f)) for f in frontends)
|
|
123
|
+
return out + (catalog.tlv(_ANAD["channels"], bytes([4, 0b100])), # s 16
|
|
124
|
+
catalog.tlv(_ANAD["trigger"], struct.pack("<BI", 0b11001, max_samples - 1)), # immediate, cross up / down
|
|
125
|
+
catalog.u32(_ANAD["max_read"], 4096), catalog.u16(_ANAD["segment_ring"], 8),
|
|
126
|
+
catalog.u32(FEATURES, 0b001))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _group_decl(tracks: list[int], budgets: list[tuple[int, list[int]]], skews: dict[int, int]) -> tuple[bytes, ...]:
|
|
130
|
+
return ((catalog.tlv(_GRPD["tracks"], struct.pack(f"<{len(tracks)}H", *tracks)), catalog.u8(_GRPD["max_tracks"], len(tracks)))
|
|
131
|
+
+ tuple(catalog.tlv(_GRPD["budget"], struct.pack(f"<I{len(f)}H", most, *f)) for most, f in budgets)
|
|
132
|
+
+ tuple(catalog.tlv(_GRPD["start_skew"], struct.pack("<HI", fn, ns)) for fn, ns in skews.items()))
|
|
112
133
|
_CAPM = reg.FIXTURE_CAPTURE.enum["mode"]
|
|
113
134
|
|
|
114
135
|
|
|
@@ -168,7 +189,8 @@ def p4_x035() -> FakeProbe:
|
|
|
168
189
|
_core("3.0.0", "esp32-p4-devkit", bytes.fromhex("30eda0e31108"), 55, reserved, f"{NS}.p4-x035",
|
|
169
190
|
{2: "SWDIO", 54: "SWCLK", 51: "LED"},
|
|
170
191
|
_transports([(TRANSPORT["usb_serial_jtag"], 0xFF), (TRANSPORT["vendor_bulk"], 0),
|
|
171
|
-
(TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)])
|
|
192
|
+
(TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)])
|
|
193
|
+
+ (catalog.u8(CORE_OEP_PID, 1), catalog.text(CORE_CHIP, "esp32p4 v1.0"))),
|
|
172
194
|
Offered(1, 1, "oep.wire.rvswd", (
|
|
173
195
|
catalog.channel_group(1, [(1, 2), (2, 54)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1),
|
|
174
196
|
catalog.u8(MAX_CONNECTIONS, 1))),
|
|
@@ -191,6 +213,12 @@ def p4_x035() -> FakeProbe:
|
|
|
191
213
|
Offered(9, 7, f"{NS}.esp32.spi-target", _roles({1: pins, 2: pins, 3: pins, 4: pins}) + (
|
|
192
214
|
catalog.u16(MAX_LENGTH, 64), catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
193
215
|
_config(10, 8, slots_max=1),
|
|
216
|
+
# the P4's ADC1 (GPIO16-23): one ADC for all its channels, 611 Hz - 83.3 kHz in all; ESP32-style attenuations
|
|
217
|
+
Offered(11, 1, "oep.fixture.analog", _roles({k: list(range(16, 24)) for k in range(4)}) + (
|
|
218
|
+
catalog.u32(MAX_CLOCK_HZ, 83_333), catalog.u32(MIN_CLOCK_HZ, 611), catalog.u8(IMPLEMENTATION, 3))
|
|
219
|
+
+ _analog_decl([(0, 0, 950, 0), (1, 0, 1250, 2500), (2, 0, 1750, 6000), (3, 0, 3100, 12000)], 65536)),
|
|
220
|
+
# the logic (fn 7) and the analog (fn 11) together; the ADC's 83.3 kHz is shared by its channels
|
|
221
|
+
Offered(12, 1, "oep.fixture.capture-group", _group_decl([7, 11], [(83_333, [11])], {11: 5000})),
|
|
194
222
|
])
|
|
195
223
|
|
|
196
224
|
|