wireskein 0.0.1__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.
- wireskein-0.0.1/.gitignore +20 -0
- wireskein-0.0.1/CHANGELOG.md +13 -0
- wireskein-0.0.1/LICENSE +21 -0
- wireskein-0.0.1/PKG-INFO +167 -0
- wireskein-0.0.1/README.ja.md +114 -0
- wireskein-0.0.1/README.md +149 -0
- wireskein-0.0.1/pyproject.toml +44 -0
- wireskein-0.0.1/src/wireskein/__init__.py +10 -0
- wireskein-0.0.1/src/wireskein/__main__.py +3 -0
- wireskein-0.0.1/src/wireskein/_engine/__init__.py +0 -0
- wireskein-0.0.1/src/wireskein/_engine/analyzers/__init__.py +0 -0
- wireskein-0.0.1/src/wireskein/_engine/analyzers/i2c.py +101 -0
- wireskein-0.0.1/src/wireskein/_engine/analyzers/spi.py +154 -0
- wireskein-0.0.1/src/wireskein/_engine/analyzers/uart.py +119 -0
- wireskein-0.0.1/src/wireskein/_engine/analyzers/upper.py +146 -0
- wireskein-0.0.1/src/wireskein/_engine/declarative.py +591 -0
- wireskein-0.0.1/src/wireskein/_engine/devices.py +331 -0
- wireskein-0.0.1/src/wireskein/_engine/duplex.py +163 -0
- wireskein-0.0.1/src/wireskein/_engine/exclude.py +151 -0
- wireskein-0.0.1/src/wireskein/_engine/export.py +250 -0
- wireskein-0.0.1/src/wireskein/_engine/features.py +217 -0
- wireskein-0.0.1/src/wireskein/_engine/fixture.py +64 -0
- wireskein-0.0.1/src/wireskein/_engine/gen.py +312 -0
- wireskein-0.0.1/src/wireskein/_engine/kernels.py +196 -0
- wireskein-0.0.1/src/wireskein/_engine/markers.py +349 -0
- wireskein-0.0.1/src/wireskein/_engine/model.py +55 -0
- wireskein-0.0.1/src/wireskein/_engine/pipeline.py +130 -0
- wireskein-0.0.1/src/wireskein/_engine/plugins_uartlike.py +50 -0
- wireskein-0.0.1/src/wireskein/_engine/rvswd.py +218 -0
- wireskein-0.0.1/src/wireskein/_engine/scoring.py +84 -0
- wireskein-0.0.1/src/wireskein/_engine/srio.py +47 -0
- wireskein-0.0.1/src/wireskein/_engine/stack.py +303 -0
- wireskein-0.0.1/src/wireskein/_engine/staged.py +675 -0
- wireskein-0.0.1/src/wireskein/_engine/survey.py +205 -0
- wireskein-0.0.1/src/wireskein/_engine/synth.py +402 -0
- wireskein-0.0.1/src/wireskein/_engine/taxonomy.py +268 -0
- wireskein-0.0.1/src/wireskein/_engine/typed.py +548 -0
- wireskein-0.0.1/src/wireskein/analyze.py +30 -0
- wireskein-0.0.1/src/wireskein/cli.py +153 -0
- wireskein-0.0.1/src/wireskein/decl/can.toml +38 -0
- wireskein-0.0.1/src/wireskein/decl/devices/README.ja.md +14 -0
- wireskein-0.0.1/src/wireskein/decl/devices/i2c/qst/qmp6988.toml +23 -0
- wireskein-0.0.1/src/wireskein/decl/devices/i2c/sensirion/sht30.toml +47 -0
- wireskein-0.0.1/src/wireskein/decl/devices/spi/flash/spi_nor.toml +66 -0
- wireskein-0.0.1/src/wireskein/decl/i2c.toml +31 -0
- wireskein-0.0.1/src/wireskein/decl/markers/heading.toml +13 -0
- wireskein-0.0.1/src/wireskein/decl/markers/i2cdb.toml +32 -0
- wireskein-0.0.1/src/wireskein/decl/rvswd.toml +44 -0
- wireskein-0.0.1/src/wireskein/decl/spi.toml +24 -0
- wireskein-0.0.1/src/wireskein/decl/swd.toml +40 -0
- wireskein-0.0.1/src/wireskein/runlog.py +188 -0
- wireskein-0.0.1/src/wireskein/verify.py +537 -0
- wireskein-0.0.1/tests/data/real/manifest.json +144 -0
- wireskein-0.0.1/tests/data/real/v003-uart-115200-time-base-slip.bin.gz +0 -0
- wireskein-0.0.1/tests/data/real/x035-i2c-target-left-mid-byte.bin.gz +0 -0
- wireskein-0.0.1/tests/data/real/x035-spi-mode2-one-cs-frame.bin.gz +0 -0
- wireskein-0.0.1/tests/data/real/x035-spi-peer-4mhz-mode0.bin.gz +0 -0
- wireskein-0.0.1/tests/data/real/x035-uart-brr-below-16.bin.gz +0 -0
- wireskein-0.0.1/tests/demo_run.py +100 -0
- wireskein-0.0.1/tests/test_real.py +72 -0
- wireskein-0.0.1/tests/test_runlog.py +45 -0
- wireskein-0.0.1/tests/test_uart.py +99 -0
- wireskein-0.0.1/tests/test_verify.py +64 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
corpus/raw/
|
|
2
|
+
corpus/work/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
.venv/
|
|
6
|
+
research/boundary-bench/target/
|
|
7
|
+
research/plugin-lang-bench/target*/
|
|
8
|
+
research/plugin-lang-bench/wasm-plugin/target/
|
|
9
|
+
research/plugin-lang-bench/build-*.log
|
|
10
|
+
research/plugin-lang-bench/stderr-*.log
|
|
11
|
+
research/plugin-lang-bench/results.jsonl
|
|
12
|
+
research/zerocopy-bench/target/
|
|
13
|
+
.cache/
|
|
14
|
+
|
|
15
|
+
# packaging
|
|
16
|
+
build/
|
|
17
|
+
dist/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
uv.lock
|
|
20
|
+
.pytest_cache/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog / 変更履歴
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.0.1
|
|
6
|
+
- (EN) First beta. The prototype is now the `wireskein` package on PyPI (`pip install --pre wireskein`, Python 3.13+, numpy only). Public modules are `wireskein.runlog` (recording a test run, standard library only), `wireskein.verify` (checking a recorded run) and `wireskein.analyze` (decoding a capture, beta), with a `wireskein` command (`analyze`, `segments`, `verify`). The `wsproto` import and `prototype/ws.py` are gone; evaluation scripts moved to `research/` and are not packaged.
|
|
7
|
+
- (JA) 最初のβ版。プロトタイプを PyPI の `wireskein` package にした(`pip install --pre wireskein`、Python 3.13 以上、依存は numpy だけ)。公開の module は `wireskein.runlog`(テストの実行の記録。標準ライブラリだけ)、`wireskein.verify`(記録の照合)、`wireskein.analyze`(キャプチャの復号。β)。コマンドは `wireskein`(`analyze`、`segments`、`verify`)。`wsproto` の import と `prototype/ws.py` はなくなった。評価のスクリプトは `research/` に移し、package には入れない。
|
|
8
|
+
- (EN) Checks: `square`, `level`, `starts`, `ends`, `only_moving`, `pulses`, `i2c`, `spi` and `uart`. The UART check measures the bit rate from the edges, takes the format (`bits`, `parity`, `stop`), does not count characters cut by the window as errors, and with `baud=None` only measures. An I2C transaction cut at the window end is reported with `complete: false` and `pending_bits`. A capture whose meta has `time_base_slipped: true` keeps its verdict, and a failure's reason names the slip.
|
|
9
|
+
- (JA) 検査は `square`、`level`、`starts`、`ends`、`only_moving`、`pulses`、`i2c`、`spi`、`uart`。UART はエッジからビットレートを測り、フォーマット(`bits`、`parity`、`stop`)を指定でき、窓で切れた文字をエラーにせず、`baud=None` で測るだけにもできる。窓の終わりで切れた I2C の取引は `complete: false` と `pending_bits` 付きで返す。meta に `time_base_slipped: true` があるキャプチャは判定を変えず、NG の理由にそれを書く。
|
|
10
|
+
- (EN) Repeated heading names under one parent keep their own expectations (`name[0]`, `name[1]`). A check whose pins are not captured is unchecked, never NG.
|
|
11
|
+
- (JA) 同じ親の下で繰り返した見出しは、それぞれの期待を持つ(`name[0]`、`name[1]`)。ピンがキャプチャにない検査は、NG ではなく未検査。
|
|
12
|
+
- (EN) Run format: `wireskein-run/0`, unchanged from the prototype, so runs recorded before this release still verify.
|
|
13
|
+
- (JA) 記録の形式は `wireskein-run/0` で、プロトタイプから変えていない。このリリースより前の記録もそのまま照合できる。
|
wireskein-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Open Embedded Probe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
wireskein-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: wireskein
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Logic-analyzer captures to protocol decodes, and waveform checks of recorded hardware test runs
|
|
5
|
+
Project-URL: Homepage, https://github.com/Open-Embedded-Probe/wireskein
|
|
6
|
+
Project-URL: Repository, https://github.com/Open-Embedded-Probe/wireskein
|
|
7
|
+
Author: TANAKA Masayuki
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Requires-Python: >=3.13
|
|
16
|
+
Requires-Dist: numpy>=2.0
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# WireSkein
|
|
20
|
+
|
|
21
|
+
[日本語 README](https://github.com/Open-Embedded-Probe/wireskein/blob/main/README.ja.md)
|
|
22
|
+
|
|
23
|
+
WireSkein reads logic-analyzer captures. It has two uses:
|
|
24
|
+
|
|
25
|
+
- **Checking recorded hardware test runs.** A test records what it sent, the captures, and what each step should look like on the wire (a 1 kHz square wave, an I2C write to 0x42, a UART at F_CPU / BRR). `wireskein verify` checks every capture against these expectations and reports OK / NG with measured values, as text, JSON and JUnit XML.
|
|
26
|
+
- **Decoding unknown captures.** `wireskein analyze` finds which pins carry I2C, SPI, UART, RVSWD / SWIO, SWD or CAN, and decodes them. Upper layers (NMEA, Modbus, known I2C / SPI devices) are tried on top.
|
|
27
|
+
|
|
28
|
+
Status: **beta** (`0.1.0b1`). Breaking changes may still happen; see [Stability](#stability).
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install --pre wireskein # or: uv add --prerelease=allow wireskein
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Python 3.13 or newer. The only dependency is numpy.
|
|
37
|
+
|
|
38
|
+
## Checking a test run
|
|
39
|
+
|
|
40
|
+
A test records a run with `wireskein.runlog`. This module uses only the standard library.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from wireskein.runlog import Recorder, square, level, only_moving
|
|
44
|
+
|
|
45
|
+
rec = Recorder("out/run1", target="x035")
|
|
46
|
+
with rec.section(1, "test_pwm"):
|
|
47
|
+
for duty in (64, 128, 0):
|
|
48
|
+
want = [square("PA1", 1000, duty / 255), only_moving(["PA1"])] if duty else [level("PA1", 0)]
|
|
49
|
+
with rec.section(2, f"duty={duty}", expect=want):
|
|
50
|
+
rec.command(f"PWM {duty}") # what the host sent
|
|
51
|
+
rec.reply(reply_line) # what the device answered
|
|
52
|
+
t = rec.armed() # right after arming the capture
|
|
53
|
+
data = read_capture() # bytes, one sample per byte, bit k = pin k
|
|
54
|
+
rec.capture(data, rate, ["PA1", "PA0"], t, start_us=segment_start_us)
|
|
55
|
+
rec.close()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then check it:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
wireskein verify out/run1 --junit out/run1/report.xml --json out/run1/report.json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
OK test_pwm/duty=64 square c0001.bin
|
|
66
|
+
NG test_pwm/duty=128 square c0002.bin duty 0.6999 vs 0.5020
|
|
67
|
+
OK test_pwm/duty=0 level c0003.bin
|
|
68
|
+
2 ok, 1 ng, 0 unchecked (4 segments, 3 captures)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The exit code is 1 when a check fails. A check whose pins are not in the capture is **unchecked** (`--`). Unchecked results do not fail the run.
|
|
72
|
+
|
|
73
|
+
### Headings and segments
|
|
74
|
+
|
|
75
|
+
Headings split the run into a tree of segments. `#` is a test, `##` is a step, and a heading without a name (`##`) closes that level. `Recorder.section()` opens a heading and closes it when the `with` block ends. A capture belongs to the segment in which it was armed. Expectations are stored under the segment path, for example `test_pwm/duty=64`. When a name repeats under the same parent, each repetition gets an index (`duty=64[0]`, `duty=64[1]`) and keeps its own expectations. A broken structure, such as a skipped level, is always reported as NG.
|
|
76
|
+
|
|
77
|
+
### Checks
|
|
78
|
+
|
|
79
|
+
| Helper | Checks |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `square(pin, freq_hz, duty, tol_freq, tol_duty, max_jitter)` | A steady square wave: frequency (relative tolerance), duty (absolute), period spread |
|
|
82
|
+
| `level(pin, value)` | The pin does not move |
|
|
83
|
+
| `starts({pin: v})` / `ends({pin: v})` | The level at the first / last sample |
|
|
84
|
+
| `only_moving([pins])` | No other captured pin moves |
|
|
85
|
+
| `pulses(pin, count, period_s, tol)` | Number of rising edges and their period |
|
|
86
|
+
| `i2c(scl, sda, transactions, hz, tol_hz, released)` | Transactions (address, direction, bytes, ACK, `complete`), SCL rate, and a released bus at the end |
|
|
87
|
+
| `spi(clk, mosi, miso, cs, mode, mosi_bytes, miso_bytes, hz)` | Mode, bytes on both lines, SCK rate, and CS high at the end |
|
|
88
|
+
| `uart(pin, baud, data, tol_baud, idle, bits, parity, stop, max_errors)` | Bit rate measured from the edges, data, idle level, and framing / parity errors. `baud=None` only measures |
|
|
89
|
+
|
|
90
|
+
Pins and roles are given, so these checks are verification, not discovery. `docs/capture-test-guide.ja.md` explains how to choose capture windows and tolerances, with examples from real runs.
|
|
91
|
+
|
|
92
|
+
If a capture has the meta `time_base_slipped: true` (the probe knows some samples were taken late), the verdict does not change, but a failure's reason says so.
|
|
93
|
+
|
|
94
|
+
### Python API
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from wireskein.verify import verify, junit, dumps, lines, summary_line
|
|
98
|
+
|
|
99
|
+
report = verify("out/run1") # dict: results (with measured values), summary, log
|
|
100
|
+
xml = junit(report) # JUnit XML
|
|
101
|
+
text = dumps(report) # JSON
|
|
102
|
+
print(summary_line(report), *lines(report), sep="\n") # NG and unchecked lines, as the CLI prints them
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For pytest, [pytest-embedded-wireskein](https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein) gives each test a `ws_run` recorder and runs `verify` after the test.
|
|
106
|
+
|
|
107
|
+
## Decoding a capture
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
wireskein analyze capture.sr # sigrok .sr, or a fixture directory
|
|
111
|
+
wireskein analyze capture.sr --hint '{"protocols": ["i2c"]}'
|
|
112
|
+
wireskein analyze capture.sr --mode all --out result.json
|
|
113
|
+
wireskein segments capture.sr --results # a capture with marker lines on a UART
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`--hint` restricts what is tried. It can name protocols, or give pins with their roles and baud rates. The result is still scored by the decoders' own checks.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from wireskein.analyze import load, analyze, export
|
|
120
|
+
|
|
121
|
+
cap = load("capture.sr")
|
|
122
|
+
res = analyze(cap, {"protocols": ["spi"]})
|
|
123
|
+
doc = export(res, cap)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Stability
|
|
127
|
+
|
|
128
|
+
| Part | Promise during the beta |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `wireskein.runlog` (names, arguments and meaning of `Recorder` and the check helpers) | Stable. New arguments get defaults that keep the old meaning |
|
|
131
|
+
| Run format (`run.json`, `FORMAT = "wireskein-run/0"`) | Stable. An incompatible change raises `FORMAT`, and `verify` refuses older runs with a clear error |
|
|
132
|
+
| `wireskein.verify.verify` / `junit`, `wireskein verify` | Stable. Report fields may be added |
|
|
133
|
+
| `wireskein.analyze`, `wireskein analyze` / `segments` output | May change |
|
|
134
|
+
| `wireskein._engine` | Internal |
|
|
135
|
+
|
|
136
|
+
## Repository layout
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
src/wireskein/ the package (runlog, verify, analyze, cli, _engine, decl/ data)
|
|
140
|
+
tests/ pytest
|
|
141
|
+
research/ evaluation scripts, benchmarks and findings (not packaged)
|
|
142
|
+
corpus/ real and synthetic fixtures for research/
|
|
143
|
+
docs/ design notes (Japanese)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
To run the research scripts: `uv sync --group research`, then `uv run python research/evaluate.py --synth 200 --engine staged`. See `research/README.ja.md`.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
uv run pytest
|
|
152
|
+
uv build
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Release
|
|
156
|
+
|
|
157
|
+
Releases use GitHub Actions, the same way as pytest-embedded-arduino-cli.
|
|
158
|
+
|
|
159
|
+
1. Update the `## Unreleased` section of `CHANGELOG.md`.
|
|
160
|
+
2. Run the `Release` workflow manually and enter the version, for example `0.1.0b1`.
|
|
161
|
+
3. The workflow does the rest. It updates the version in `pyproject.toml` and `src/wireskein/__init__.py`, moves the changelog entries under the new version, runs the tests, builds, commits, tags `v<version>`, creates a GitHub Release, and publishes to PyPI.
|
|
162
|
+
|
|
163
|
+
PyPI publishing uses Trusted Publishing.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# WireSkein
|
|
2
|
+
|
|
3
|
+
[English README](https://github.com/Open-Embedded-Probe/wireskein/blob/main/README.md)
|
|
4
|
+
|
|
5
|
+
WireSkein は、ロジックアナライザのキャプチャを読むツールです。用途は 2 つあります。
|
|
6
|
+
|
|
7
|
+
- **実機のテストの記録を照合する。** テストは、送ったコマンド、キャプチャ、各ステップの線の上であるべき姿(1 kHz の方形波、0x42 への I2C の書き込み、F_CPU / BRR の UART など)を記録します。`wireskein verify` は、その期待とキャプチャを照らし合わせます。結果は、測定値付きの OK / NG として、テキスト、JSON、JUnit XML で出します。
|
|
8
|
+
- **中身の分からないキャプチャを復号する。** `wireskein analyze` は、どのピンが I2C、SPI、UART、RVSWD / SWIO、SWD、CAN かを見つけて復号します。その上の層(NMEA、Modbus、既知の I2C / SPI デバイス)も試します。
|
|
9
|
+
|
|
10
|
+
状態: **β 版**(`0.1.0b1`)です。互換のない変更が入ることがあります([安定性](#安定性)を参照)。
|
|
11
|
+
|
|
12
|
+
## 入れ方
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pip install --pre wireskein # または uv add --prerelease=allow wireskein
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Python 3.13 以上が要ります。依存は numpy だけです。
|
|
19
|
+
|
|
20
|
+
## テストの記録を照合する
|
|
21
|
+
|
|
22
|
+
テストは、`wireskein.runlog` で記録します。このモジュールは、標準ライブラリだけで動きます。
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from wireskein.runlog import Recorder, square, level, only_moving
|
|
26
|
+
|
|
27
|
+
rec = Recorder("out/run1", target="x035")
|
|
28
|
+
with rec.section(1, "test_pwm"):
|
|
29
|
+
for duty in (64, 128, 0):
|
|
30
|
+
want = [square("PA1", 1000, duty / 255), only_moving(["PA1"])] if duty else [level("PA1", 0)]
|
|
31
|
+
with rec.section(2, f"duty={duty}", expect=want):
|
|
32
|
+
rec.command(f"PWM {duty}") # PC が送ったもの
|
|
33
|
+
rec.reply(reply_line) # デバイスの応答
|
|
34
|
+
t = rec.armed() # キャプチャを開始した直後
|
|
35
|
+
data = read_capture() # bytes。1 サンプル 1 バイト、ビット k がピン k
|
|
36
|
+
rec.capture(data, rate, ["PA1", "PA0"], t, start_us=segment_start_us)
|
|
37
|
+
rec.close()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
照合は次のコマンドで行います。
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
wireskein verify out/run1 --junit out/run1/report.xml --json out/run1/report.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
NG があれば、終了コードは 1 です。検査に使うピンがキャプチャにないときは、未検査(`--`)になります。未検査は、実行を失敗にしません。
|
|
47
|
+
|
|
48
|
+
- 見出し(`#` がテスト、`##` がステップ、名前のない `##` で閉じる)で、区間の木を作ります。
|
|
49
|
+
- キャプチャは、開始した時刻を含む区間に割り当てます。
|
|
50
|
+
- 期待は、区間のパス(例 `test_pwm/duty=64`)に付けます。同じ親の下で名前が重なると、`duty=64[0]`、`duty=64[1]` のように連番を付け、それぞれが自分の期待を持ちます。
|
|
51
|
+
|
|
52
|
+
検査の一覧、窓と許容誤差の決め方、実機の例は [docs/capture-test-guide.ja.md](docs/capture-test-guide.ja.md) にあります。
|
|
53
|
+
|
|
54
|
+
pytest からは、[pytest-embedded-wireskein](https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein) を使います。test ごとに記録器 `ws_run` を渡し、test の後に照合します。
|
|
55
|
+
|
|
56
|
+
## キャプチャを復号する
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
wireskein analyze capture.sr # sigrok の .sr、または fixture のディレクトリ
|
|
60
|
+
wireskein analyze capture.sr --hint '{"protocols": ["i2c"]}'
|
|
61
|
+
wireskein segments capture.sr --results # UART にマーカーの行を流したキャプチャ
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from wireskein.analyze import load, analyze, export
|
|
66
|
+
|
|
67
|
+
cap = load("capture.sr")
|
|
68
|
+
doc = export(analyze(cap, {"protocols": ["spi"]}), cap)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 安定性
|
|
72
|
+
|
|
73
|
+
| 部分 | β の間の約束 |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `wireskein.runlog`(`Recorder` と検査の helper の名前、引数、意味) | 変えない。足す引数は、既定値で今の意味を保つ |
|
|
76
|
+
| 記録の形式(`run.json`、`FORMAT = "wireskein-run/0"`) | 変えない。互換のない変更をするときは `FORMAT` を上げ、`verify` は古い形式をはっきりしたエラーで断る |
|
|
77
|
+
| `wireskein.verify.verify` / `junit`、`wireskein verify` | 変えない。報告の項目は増えることがある |
|
|
78
|
+
| `wireskein.analyze`、`wireskein analyze` / `segments` の出力 | 変わることがある |
|
|
79
|
+
| `wireskein._engine` | 内部 |
|
|
80
|
+
|
|
81
|
+
## リポジトリの構成
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
src/wireskein/ package(runlog、verify、analyze、cli、_engine、decl/ のデータ)
|
|
85
|
+
tests/ pytest
|
|
86
|
+
research/ 評価のスクリプト、ベンチ、実験の記録(package に入れない)
|
|
87
|
+
corpus/ research/ が使う実記録と合成の fixture
|
|
88
|
+
docs/ 設計のメモ
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 開発
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
uv run pytest
|
|
95
|
+
uv build
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## リリース
|
|
99
|
+
|
|
100
|
+
pytest-embedded-arduino-cli と同じく、GitHub Actions で出します。
|
|
101
|
+
|
|
102
|
+
1. `CHANGELOG.md` の `## Unreleased` を更新します。
|
|
103
|
+
2. `Release` の workflow を手で実行し、版(例 `0.1.0b1`)を入れます。
|
|
104
|
+
3. 残りは workflow が行います。
|
|
105
|
+
- `pyproject.toml` と `src/wireskein/__init__.py` の版を書き換える
|
|
106
|
+
- 変更履歴を新しい版の見出しの下へ移す
|
|
107
|
+
- テストとビルドを行い、コミットして `v<版>` のタグを付ける
|
|
108
|
+
- GitHub Release を作り、PyPI に出す
|
|
109
|
+
|
|
110
|
+
PyPI への公開は Trusted Publishing で行います。
|
|
111
|
+
|
|
112
|
+
## ライセンス
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# WireSkein
|
|
2
|
+
|
|
3
|
+
[日本語 README](https://github.com/Open-Embedded-Probe/wireskein/blob/main/README.ja.md)
|
|
4
|
+
|
|
5
|
+
WireSkein reads logic-analyzer captures. It has two uses:
|
|
6
|
+
|
|
7
|
+
- **Checking recorded hardware test runs.** A test records what it sent, the captures, and what each step should look like on the wire (a 1 kHz square wave, an I2C write to 0x42, a UART at F_CPU / BRR). `wireskein verify` checks every capture against these expectations and reports OK / NG with measured values, as text, JSON and JUnit XML.
|
|
8
|
+
- **Decoding unknown captures.** `wireskein analyze` finds which pins carry I2C, SPI, UART, RVSWD / SWIO, SWD or CAN, and decodes them. Upper layers (NMEA, Modbus, known I2C / SPI devices) are tried on top.
|
|
9
|
+
|
|
10
|
+
Status: **beta** (`0.1.0b1`). Breaking changes may still happen; see [Stability](#stability).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pip install --pre wireskein # or: uv add --prerelease=allow wireskein
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Python 3.13 or newer. The only dependency is numpy.
|
|
19
|
+
|
|
20
|
+
## Checking a test run
|
|
21
|
+
|
|
22
|
+
A test records a run with `wireskein.runlog`. This module uses only the standard library.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from wireskein.runlog import Recorder, square, level, only_moving
|
|
26
|
+
|
|
27
|
+
rec = Recorder("out/run1", target="x035")
|
|
28
|
+
with rec.section(1, "test_pwm"):
|
|
29
|
+
for duty in (64, 128, 0):
|
|
30
|
+
want = [square("PA1", 1000, duty / 255), only_moving(["PA1"])] if duty else [level("PA1", 0)]
|
|
31
|
+
with rec.section(2, f"duty={duty}", expect=want):
|
|
32
|
+
rec.command(f"PWM {duty}") # what the host sent
|
|
33
|
+
rec.reply(reply_line) # what the device answered
|
|
34
|
+
t = rec.armed() # right after arming the capture
|
|
35
|
+
data = read_capture() # bytes, one sample per byte, bit k = pin k
|
|
36
|
+
rec.capture(data, rate, ["PA1", "PA0"], t, start_us=segment_start_us)
|
|
37
|
+
rec.close()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then check it:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
wireskein verify out/run1 --junit out/run1/report.xml --json out/run1/report.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
OK test_pwm/duty=64 square c0001.bin
|
|
48
|
+
NG test_pwm/duty=128 square c0002.bin duty 0.6999 vs 0.5020
|
|
49
|
+
OK test_pwm/duty=0 level c0003.bin
|
|
50
|
+
2 ok, 1 ng, 0 unchecked (4 segments, 3 captures)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The exit code is 1 when a check fails. A check whose pins are not in the capture is **unchecked** (`--`). Unchecked results do not fail the run.
|
|
54
|
+
|
|
55
|
+
### Headings and segments
|
|
56
|
+
|
|
57
|
+
Headings split the run into a tree of segments. `#` is a test, `##` is a step, and a heading without a name (`##`) closes that level. `Recorder.section()` opens a heading and closes it when the `with` block ends. A capture belongs to the segment in which it was armed. Expectations are stored under the segment path, for example `test_pwm/duty=64`. When a name repeats under the same parent, each repetition gets an index (`duty=64[0]`, `duty=64[1]`) and keeps its own expectations. A broken structure, such as a skipped level, is always reported as NG.
|
|
58
|
+
|
|
59
|
+
### Checks
|
|
60
|
+
|
|
61
|
+
| Helper | Checks |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `square(pin, freq_hz, duty, tol_freq, tol_duty, max_jitter)` | A steady square wave: frequency (relative tolerance), duty (absolute), period spread |
|
|
64
|
+
| `level(pin, value)` | The pin does not move |
|
|
65
|
+
| `starts({pin: v})` / `ends({pin: v})` | The level at the first / last sample |
|
|
66
|
+
| `only_moving([pins])` | No other captured pin moves |
|
|
67
|
+
| `pulses(pin, count, period_s, tol)` | Number of rising edges and their period |
|
|
68
|
+
| `i2c(scl, sda, transactions, hz, tol_hz, released)` | Transactions (address, direction, bytes, ACK, `complete`), SCL rate, and a released bus at the end |
|
|
69
|
+
| `spi(clk, mosi, miso, cs, mode, mosi_bytes, miso_bytes, hz)` | Mode, bytes on both lines, SCK rate, and CS high at the end |
|
|
70
|
+
| `uart(pin, baud, data, tol_baud, idle, bits, parity, stop, max_errors)` | Bit rate measured from the edges, data, idle level, and framing / parity errors. `baud=None` only measures |
|
|
71
|
+
|
|
72
|
+
Pins and roles are given, so these checks are verification, not discovery. `docs/capture-test-guide.ja.md` explains how to choose capture windows and tolerances, with examples from real runs.
|
|
73
|
+
|
|
74
|
+
If a capture has the meta `time_base_slipped: true` (the probe knows some samples were taken late), the verdict does not change, but a failure's reason says so.
|
|
75
|
+
|
|
76
|
+
### Python API
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from wireskein.verify import verify, junit, dumps, lines, summary_line
|
|
80
|
+
|
|
81
|
+
report = verify("out/run1") # dict: results (with measured values), summary, log
|
|
82
|
+
xml = junit(report) # JUnit XML
|
|
83
|
+
text = dumps(report) # JSON
|
|
84
|
+
print(summary_line(report), *lines(report), sep="\n") # NG and unchecked lines, as the CLI prints them
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For pytest, [pytest-embedded-wireskein](https://github.com/Open-Embedded-Probe/pytest-embedded-wireskein) gives each test a `ws_run` recorder and runs `verify` after the test.
|
|
88
|
+
|
|
89
|
+
## Decoding a capture
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
wireskein analyze capture.sr # sigrok .sr, or a fixture directory
|
|
93
|
+
wireskein analyze capture.sr --hint '{"protocols": ["i2c"]}'
|
|
94
|
+
wireskein analyze capture.sr --mode all --out result.json
|
|
95
|
+
wireskein segments capture.sr --results # a capture with marker lines on a UART
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`--hint` restricts what is tried. It can name protocols, or give pins with their roles and baud rates. The result is still scored by the decoders' own checks.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from wireskein.analyze import load, analyze, export
|
|
102
|
+
|
|
103
|
+
cap = load("capture.sr")
|
|
104
|
+
res = analyze(cap, {"protocols": ["spi"]})
|
|
105
|
+
doc = export(res, cap)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Stability
|
|
109
|
+
|
|
110
|
+
| Part | Promise during the beta |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `wireskein.runlog` (names, arguments and meaning of `Recorder` and the check helpers) | Stable. New arguments get defaults that keep the old meaning |
|
|
113
|
+
| Run format (`run.json`, `FORMAT = "wireskein-run/0"`) | Stable. An incompatible change raises `FORMAT`, and `verify` refuses older runs with a clear error |
|
|
114
|
+
| `wireskein.verify.verify` / `junit`, `wireskein verify` | Stable. Report fields may be added |
|
|
115
|
+
| `wireskein.analyze`, `wireskein analyze` / `segments` output | May change |
|
|
116
|
+
| `wireskein._engine` | Internal |
|
|
117
|
+
|
|
118
|
+
## Repository layout
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
src/wireskein/ the package (runlog, verify, analyze, cli, _engine, decl/ data)
|
|
122
|
+
tests/ pytest
|
|
123
|
+
research/ evaluation scripts, benchmarks and findings (not packaged)
|
|
124
|
+
corpus/ real and synthetic fixtures for research/
|
|
125
|
+
docs/ design notes (Japanese)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
To run the research scripts: `uv sync --group research`, then `uv run python research/evaluate.py --synth 200 --engine staged`. See `research/README.ja.md`.
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
uv run pytest
|
|
134
|
+
uv build
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Release
|
|
138
|
+
|
|
139
|
+
Releases use GitHub Actions, the same way as pytest-embedded-arduino-cli.
|
|
140
|
+
|
|
141
|
+
1. Update the `## Unreleased` section of `CHANGELOG.md`.
|
|
142
|
+
2. Run the `Release` workflow manually and enter the version, for example `0.1.0b1`.
|
|
143
|
+
3. The workflow does the rest. It updates the version in `pyproject.toml` and `src/wireskein/__init__.py`, moves the changelog entries under the new version, runs the tests, builds, commits, tags `v<version>`, creates a GitHub Release, and publishes to PyPI.
|
|
144
|
+
|
|
145
|
+
PyPI publishing uses Trusted Publishing.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wireskein"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Logic-analyzer captures to protocol decodes, and waveform checks of recorded hardware test runs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.13"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "TANAKA Masayuki" },
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy>=2.0",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
|
23
|
+
"Topic :: Software Development :: Testing",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
wireskein = "wireskein.cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/Open-Embedded-Probe/wireskein"
|
|
31
|
+
Repository = "https://github.com/Open-Embedded-Probe/wireskein"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = ["pytest>=8"]
|
|
35
|
+
research = ["mini-racer>=0.14.1"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/wireskein"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
only-include = ["src/wireskein", "tests", "README.md", "README.ja.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""WireSkein: logic-analyzer captures to protocol decodes, and checks of recorded test runs.
|
|
2
|
+
|
|
3
|
+
Public modules: wireskein.runlog (recording, standard library only),
|
|
4
|
+
wireskein.verify (checking a recorded run), wireskein.analyze (decoding a
|
|
5
|
+
capture). Everything under wireskein._engine may change between versions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = ["__version__"]
|
|
9
|
+
|
|
10
|
+
__version__ = "0.0.1"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""I2C decoder: (scl, sda) edges -> transactions. Proposals: every ordered pair."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .. import kernels as K
|
|
8
|
+
from ..stack import Context, Stream, pairs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class I2c:
|
|
12
|
+
name = "i2c"
|
|
13
|
+
consumes = "capture"
|
|
14
|
+
produces = "i2c.transactions"
|
|
15
|
+
|
|
16
|
+
def propose(self, ctx: Context, parent):
|
|
17
|
+
for scl, sda in pairs(ctx.active):
|
|
18
|
+
yield {"scl": scl, "sda": sda}
|
|
19
|
+
|
|
20
|
+
def probe_channels(self, p):
|
|
21
|
+
return [p["scl"], p["sda"]]
|
|
22
|
+
|
|
23
|
+
def run(self, ctx: Context, parent, p):
|
|
24
|
+
cap = ctx.capture
|
|
25
|
+
scl, sda = cap.channel(p["scl"]), cap.channel(p["sda"])
|
|
26
|
+
roles = {"scl": p["scl"], "sda": p["sda"]}
|
|
27
|
+
ev = K.merge_events(scl.edges, sda.edges)
|
|
28
|
+
if len(ev) == 0:
|
|
29
|
+
return roles, None, {"starts": 0}
|
|
30
|
+
c, d = scl.initial, sda.initial
|
|
31
|
+
txs, cur, bits = [], None, []
|
|
32
|
+
starts = stops = 0
|
|
33
|
+
seg_total = seg_ok = 0
|
|
34
|
+
scl_rise_in = 0
|
|
35
|
+
sda_explained = 0
|
|
36
|
+
misc_sda = 0
|
|
37
|
+
|
|
38
|
+
def close_segment(end_sample, stop):
|
|
39
|
+
nonlocal seg_total, seg_ok
|
|
40
|
+
if cur is None:
|
|
41
|
+
return
|
|
42
|
+
if bits: # START immediately followed by START/STOP carries no bits to check
|
|
43
|
+
seg_total += 1
|
|
44
|
+
seg_ok += len(bits) % 9 == 0
|
|
45
|
+
nbytes = len(bits) // 9
|
|
46
|
+
vals = [int("".join(map(str, bits[i * 9:i * 9 + 8])), 2) for i in range(nbytes)]
|
|
47
|
+
acks = [bits[i * 9 + 8] == 0 for i in range(nbytes)]
|
|
48
|
+
if vals:
|
|
49
|
+
cur.update(addr=vals[0] >> 1, rw="read" if vals[0] & 1 else "write", addr_ack=acks[0],
|
|
50
|
+
bytes=vals[1:], acks=acks[1:])
|
|
51
|
+
cur.update(end=int(end_sample), stop=stop, bits=len(bits))
|
|
52
|
+
txs.append(dict(cur))
|
|
53
|
+
|
|
54
|
+
pending = None # a bit is committed on the SCL fall; a rise followed by START/STOP is not data
|
|
55
|
+
for t, which in ev.tolist():
|
|
56
|
+
if which == 0:
|
|
57
|
+
c ^= 1
|
|
58
|
+
if cur is not None:
|
|
59
|
+
scl_rise_in += c == 1
|
|
60
|
+
if c == 1:
|
|
61
|
+
pending = d
|
|
62
|
+
elif pending is not None:
|
|
63
|
+
bits.append(pending)
|
|
64
|
+
pending = None
|
|
65
|
+
else:
|
|
66
|
+
d ^= 1
|
|
67
|
+
if c == 1:
|
|
68
|
+
pending = None
|
|
69
|
+
if d == 0: # START / repeated START
|
|
70
|
+
if cur is not None:
|
|
71
|
+
close_segment(t, False)
|
|
72
|
+
starts += 1
|
|
73
|
+
cur, bits = {"start": int(t)}, []
|
|
74
|
+
else: # STOP
|
|
75
|
+
if cur is not None:
|
|
76
|
+
close_segment(t, True)
|
|
77
|
+
stops += 1
|
|
78
|
+
cur, bits = None, []
|
|
79
|
+
sda_explained += 1
|
|
80
|
+
elif cur is not None:
|
|
81
|
+
sda_explained += 1
|
|
82
|
+
else:
|
|
83
|
+
misc_sda += 1
|
|
84
|
+
n_rise = int(np.sum((scl.initial ^ ((np.arange(len(scl.edges)) + 1) & 1)) == 1))
|
|
85
|
+
with_addr = [x for x in txs if "addr" in x]
|
|
86
|
+
addrs = [x["addr"] for x in with_addr]
|
|
87
|
+
top_addr = max((addrs.count(a) for a in set(addrs)), default=0)
|
|
88
|
+
metrics = {
|
|
89
|
+
"starts": starts,
|
|
90
|
+
"stops": stops,
|
|
91
|
+
"transactions": len(txs),
|
|
92
|
+
"bytes": sum(1 + len(x["bytes"]) for x in with_addr),
|
|
93
|
+
"segment_ok_rate": seg_ok / seg_total if seg_total else 0.0,
|
|
94
|
+
"scl_coverage": scl_rise_in / n_rise if n_rise else 0.0,
|
|
95
|
+
"sda_explained": sda_explained / len(sda.edges) if len(sda.edges) else 0.0,
|
|
96
|
+
"addr_ack_rate": float(np.mean([x["addr_ack"] for x in with_addr])) if with_addr else 0.0,
|
|
97
|
+
"distinct_addr": len(set(addrs)),
|
|
98
|
+
"top_addr_share": top_addr / len(addrs) if addrs else 0.0,
|
|
99
|
+
"clk_role_score": ctx.features[p["scl"]].scores.get("clock", 0.0),
|
|
100
|
+
}
|
|
101
|
+
return roles, Stream("i2c.transactions", txs), metrics
|