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.
Files changed (63) hide show
  1. wireskein-0.0.1/.gitignore +20 -0
  2. wireskein-0.0.1/CHANGELOG.md +13 -0
  3. wireskein-0.0.1/LICENSE +21 -0
  4. wireskein-0.0.1/PKG-INFO +167 -0
  5. wireskein-0.0.1/README.ja.md +114 -0
  6. wireskein-0.0.1/README.md +149 -0
  7. wireskein-0.0.1/pyproject.toml +44 -0
  8. wireskein-0.0.1/src/wireskein/__init__.py +10 -0
  9. wireskein-0.0.1/src/wireskein/__main__.py +3 -0
  10. wireskein-0.0.1/src/wireskein/_engine/__init__.py +0 -0
  11. wireskein-0.0.1/src/wireskein/_engine/analyzers/__init__.py +0 -0
  12. wireskein-0.0.1/src/wireskein/_engine/analyzers/i2c.py +101 -0
  13. wireskein-0.0.1/src/wireskein/_engine/analyzers/spi.py +154 -0
  14. wireskein-0.0.1/src/wireskein/_engine/analyzers/uart.py +119 -0
  15. wireskein-0.0.1/src/wireskein/_engine/analyzers/upper.py +146 -0
  16. wireskein-0.0.1/src/wireskein/_engine/declarative.py +591 -0
  17. wireskein-0.0.1/src/wireskein/_engine/devices.py +331 -0
  18. wireskein-0.0.1/src/wireskein/_engine/duplex.py +163 -0
  19. wireskein-0.0.1/src/wireskein/_engine/exclude.py +151 -0
  20. wireskein-0.0.1/src/wireskein/_engine/export.py +250 -0
  21. wireskein-0.0.1/src/wireskein/_engine/features.py +217 -0
  22. wireskein-0.0.1/src/wireskein/_engine/fixture.py +64 -0
  23. wireskein-0.0.1/src/wireskein/_engine/gen.py +312 -0
  24. wireskein-0.0.1/src/wireskein/_engine/kernels.py +196 -0
  25. wireskein-0.0.1/src/wireskein/_engine/markers.py +349 -0
  26. wireskein-0.0.1/src/wireskein/_engine/model.py +55 -0
  27. wireskein-0.0.1/src/wireskein/_engine/pipeline.py +130 -0
  28. wireskein-0.0.1/src/wireskein/_engine/plugins_uartlike.py +50 -0
  29. wireskein-0.0.1/src/wireskein/_engine/rvswd.py +218 -0
  30. wireskein-0.0.1/src/wireskein/_engine/scoring.py +84 -0
  31. wireskein-0.0.1/src/wireskein/_engine/srio.py +47 -0
  32. wireskein-0.0.1/src/wireskein/_engine/stack.py +303 -0
  33. wireskein-0.0.1/src/wireskein/_engine/staged.py +675 -0
  34. wireskein-0.0.1/src/wireskein/_engine/survey.py +205 -0
  35. wireskein-0.0.1/src/wireskein/_engine/synth.py +402 -0
  36. wireskein-0.0.1/src/wireskein/_engine/taxonomy.py +268 -0
  37. wireskein-0.0.1/src/wireskein/_engine/typed.py +548 -0
  38. wireskein-0.0.1/src/wireskein/analyze.py +30 -0
  39. wireskein-0.0.1/src/wireskein/cli.py +153 -0
  40. wireskein-0.0.1/src/wireskein/decl/can.toml +38 -0
  41. wireskein-0.0.1/src/wireskein/decl/devices/README.ja.md +14 -0
  42. wireskein-0.0.1/src/wireskein/decl/devices/i2c/qst/qmp6988.toml +23 -0
  43. wireskein-0.0.1/src/wireskein/decl/devices/i2c/sensirion/sht30.toml +47 -0
  44. wireskein-0.0.1/src/wireskein/decl/devices/spi/flash/spi_nor.toml +66 -0
  45. wireskein-0.0.1/src/wireskein/decl/i2c.toml +31 -0
  46. wireskein-0.0.1/src/wireskein/decl/markers/heading.toml +13 -0
  47. wireskein-0.0.1/src/wireskein/decl/markers/i2cdb.toml +32 -0
  48. wireskein-0.0.1/src/wireskein/decl/rvswd.toml +44 -0
  49. wireskein-0.0.1/src/wireskein/decl/spi.toml +24 -0
  50. wireskein-0.0.1/src/wireskein/decl/swd.toml +40 -0
  51. wireskein-0.0.1/src/wireskein/runlog.py +188 -0
  52. wireskein-0.0.1/src/wireskein/verify.py +537 -0
  53. wireskein-0.0.1/tests/data/real/manifest.json +144 -0
  54. wireskein-0.0.1/tests/data/real/v003-uart-115200-time-base-slip.bin.gz +0 -0
  55. wireskein-0.0.1/tests/data/real/x035-i2c-target-left-mid-byte.bin.gz +0 -0
  56. wireskein-0.0.1/tests/data/real/x035-spi-mode2-one-cs-frame.bin.gz +0 -0
  57. wireskein-0.0.1/tests/data/real/x035-spi-peer-4mhz-mode0.bin.gz +0 -0
  58. wireskein-0.0.1/tests/data/real/x035-uart-brr-below-16.bin.gz +0 -0
  59. wireskein-0.0.1/tests/demo_run.py +100 -0
  60. wireskein-0.0.1/tests/test_real.py +72 -0
  61. wireskein-0.0.1/tests/test_runlog.py +45 -0
  62. wireskein-0.0.1/tests/test_uart.py +99 -0
  63. 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` で、プロトタイプから変えていない。このリリースより前の記録もそのまま照合できる。
@@ -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.
@@ -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"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
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