ikalogic-scanastudio 0.2.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.
- ikalogic_scanastudio-0.2.1/LICENSE +21 -0
- ikalogic_scanastudio-0.2.1/PKG-INFO +145 -0
- ikalogic_scanastudio-0.2.1/README.md +116 -0
- ikalogic_scanastudio-0.2.1/pyproject.toml +77 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/__init__.py +121 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/__init__.py +0 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/errors.py +68 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/frames.py +135 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/gui.py +206 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/state.py +100 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/transport.py +278 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_generated/__init__.py +0 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_generated/protocol.py +2522 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/capture.py +180 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/client.py +306 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/data.py +206 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/decoders.py +260 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/events.py +73 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/export.py +10 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/markers.py +75 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/measures.py +161 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/patterns.py +128 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/py.typed +0 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/scripts.py +193 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/trigger.py +162 -0
- ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/workspace.py +217 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) IKALOGIC S.A.S.
|
|
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,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ikalogic-scanastudio
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Python SDK for ScanaStudio v6 logic analysers and pattern generators
|
|
5
|
+
Keywords: scanastudio,ikalogic,logic-analyzer,protocol-decoder,pattern-generator,instrumentation,sdk,api
|
|
6
|
+
Author: IKALOGIC
|
|
7
|
+
Author-email: IKALOGIC <contact@ikalogic.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Manufacturing
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: pydantic>=2.7
|
|
23
|
+
Requires-Dist: websockets>=13.0
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Project-URL: Documentation, https://www.ikalogic.com/kb/scanastudio-api/
|
|
26
|
+
Project-URL: Homepage, https://www.ikalogic.com/logic-analyzers/
|
|
27
|
+
Project-URL: Repository, https://github.com/ikalogic/scanastudio-v6
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# ikalogic-scanastudio
|
|
31
|
+
|
|
32
|
+
Python SDK for [ScanaStudio v6](https://www.ikalogic.com/logic-analyzers/), the
|
|
33
|
+
software behind Ikalogic logic analysers and pattern generators.
|
|
34
|
+
|
|
35
|
+
ScanaStudio runs a server that owns the devices and speaks one WebSocket
|
|
36
|
+
protocol. The interface is a client of that protocol, and so is this package —
|
|
37
|
+
so everything you can do in the application, you can do from a script: capture,
|
|
38
|
+
decode, measure, generate patterns and export.
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
pip install ikalogic-scanastudio
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Capture and decode
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from ikalogic_scanastudio import ScanaStudio, Trigger
|
|
48
|
+
|
|
49
|
+
with ScanaStudio.connect() as server:
|
|
50
|
+
workspace = server.create("se254") # a demo device: no hardware needed
|
|
51
|
+
|
|
52
|
+
workspace.capture.run(
|
|
53
|
+
samples=1_000_000,
|
|
54
|
+
sample_rate=25_000_000,
|
|
55
|
+
trigger=Trigger.rising(channel=0, position=0.1),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
i2c = workspace.decoders.add(
|
|
59
|
+
"i2c.js",
|
|
60
|
+
{"ch_scl": workspace.channel("I2C SCL"), "ch_sda": workspace.channel("I2C SDA")},
|
|
61
|
+
wait=True,
|
|
62
|
+
)
|
|
63
|
+
for packet in i2c.packets():
|
|
64
|
+
print(f"{workspace.capture.seconds(packet.start):.6f}s {packet.title}: {packet.content}")
|
|
65
|
+
|
|
66
|
+
workspace.export_csv("packets.csv", source="packets", instances=[i2c.instance_id])
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`server.create` takes a demo key such as `se254`, or `hw:<serial>` for real
|
|
70
|
+
hardware; `server.devices()` lists both. `server.open` takes the path of a
|
|
71
|
+
`.scana` instead, and `server.attach` joins a workspace already running.
|
|
72
|
+
|
|
73
|
+
⚠️ A demo device generates pseudo-random noise, not protocol traffic. It runs
|
|
74
|
+
the whole path — capture, decode, measure, export — which is what makes it
|
|
75
|
+
useful for writing and testing a script without hardware, but a decoder run on
|
|
76
|
+
it finds structure by chance.
|
|
77
|
+
|
|
78
|
+
## Read the raw signal
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
for edge in workspace.data.transitions(channel=0):
|
|
82
|
+
print(edge.sample, edge.level)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The iterator pages for you, however long the capture. The first edge it yields
|
|
86
|
+
is the level in force where you started reading, so a window describes itself.
|
|
87
|
+
|
|
88
|
+
## Find out what a decoder takes
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
for option in workspace.decoders.options("i2c.js"):
|
|
92
|
+
print(option)
|
|
93
|
+
# ch_sda: SDA Channel (ch_selector) = None
|
|
94
|
+
# address_format [Advanced options]: Address display format (combo)
|
|
95
|
+
# one of ['HEX', 'Binary', 'Decimal'] = 'HEX'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Pass those by id or by caption, as Python values — a channel number, a combo
|
|
99
|
+
entry by name, a flag as `True`. Anything left out keeps the script's default.
|
|
100
|
+
|
|
101
|
+
## Leave a capture running
|
|
102
|
+
|
|
103
|
+
The server owns the workspace, so disconnecting destroys nothing. Start a long
|
|
104
|
+
acquisition, go away, and come back to it:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
with ScanaStudio.connect() as server:
|
|
108
|
+
workspace = server.create("hw:SP259-000123")
|
|
109
|
+
workspace.capture.start(samples=2_000_000_000)
|
|
110
|
+
print("running as workspace", workspace.id)
|
|
111
|
+
|
|
112
|
+
# later, from anywhere
|
|
113
|
+
with ScanaStudio.connect("ws://bench.local:4911") as server:
|
|
114
|
+
for session in server.workspaces():
|
|
115
|
+
print(session.workspace_id, session.device_name, session.state)
|
|
116
|
+
workspace = server.attach(session.workspace_id)
|
|
117
|
+
workspace.capture.wait()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
⚠️ A server listening beyond loopback has **no authentication**: whoever reaches
|
|
121
|
+
the port has the devices.
|
|
122
|
+
|
|
123
|
+
## What is where
|
|
124
|
+
|
|
125
|
+
| | |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `server.devices()`, `server.workspaces()` | what you can open, and what is already open |
|
|
128
|
+
| `server.capabilities(device)` | the rates, depths and thresholds it offers |
|
|
129
|
+
| `server.scripts` | the decoder library, which belongs to the server |
|
|
130
|
+
| `workspace.capture` | arming, running and waiting |
|
|
131
|
+
| `workspace.data` | raw edges, packets, items, hex |
|
|
132
|
+
| `workspace.decoders` | attaching and configuring decoders |
|
|
133
|
+
| `workspace.markers` | time markers |
|
|
134
|
+
| `workspace.measures` | measurements between two markers |
|
|
135
|
+
| `workspace.pattern` | the pattern generator |
|
|
136
|
+
| `workspace.events` | everything the server pushes |
|
|
137
|
+
|
|
138
|
+
## Versions
|
|
139
|
+
|
|
140
|
+
This SDK speaks protocol 7. `ScanaStudio.connect` refuses a server that speaks
|
|
141
|
+
another, since a mismatch means one of the two needs updating. The protocol
|
|
142
|
+
itself is documented in
|
|
143
|
+
[`api/schema/protocol.md`](https://github.com/ikalogic/scanastudio-v6/blob/main/api/schema/protocol.md).
|
|
144
|
+
|
|
145
|
+
MIT licensed.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# ikalogic-scanastudio
|
|
2
|
+
|
|
3
|
+
Python SDK for [ScanaStudio v6](https://www.ikalogic.com/logic-analyzers/), the
|
|
4
|
+
software behind Ikalogic logic analysers and pattern generators.
|
|
5
|
+
|
|
6
|
+
ScanaStudio runs a server that owns the devices and speaks one WebSocket
|
|
7
|
+
protocol. The interface is a client of that protocol, and so is this package —
|
|
8
|
+
so everything you can do in the application, you can do from a script: capture,
|
|
9
|
+
decode, measure, generate patterns and export.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install ikalogic-scanastudio
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Capture and decode
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from ikalogic_scanastudio import ScanaStudio, Trigger
|
|
19
|
+
|
|
20
|
+
with ScanaStudio.connect() as server:
|
|
21
|
+
workspace = server.create("se254") # a demo device: no hardware needed
|
|
22
|
+
|
|
23
|
+
workspace.capture.run(
|
|
24
|
+
samples=1_000_000,
|
|
25
|
+
sample_rate=25_000_000,
|
|
26
|
+
trigger=Trigger.rising(channel=0, position=0.1),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
i2c = workspace.decoders.add(
|
|
30
|
+
"i2c.js",
|
|
31
|
+
{"ch_scl": workspace.channel("I2C SCL"), "ch_sda": workspace.channel("I2C SDA")},
|
|
32
|
+
wait=True,
|
|
33
|
+
)
|
|
34
|
+
for packet in i2c.packets():
|
|
35
|
+
print(f"{workspace.capture.seconds(packet.start):.6f}s {packet.title}: {packet.content}")
|
|
36
|
+
|
|
37
|
+
workspace.export_csv("packets.csv", source="packets", instances=[i2c.instance_id])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`server.create` takes a demo key such as `se254`, or `hw:<serial>` for real
|
|
41
|
+
hardware; `server.devices()` lists both. `server.open` takes the path of a
|
|
42
|
+
`.scana` instead, and `server.attach` joins a workspace already running.
|
|
43
|
+
|
|
44
|
+
⚠️ A demo device generates pseudo-random noise, not protocol traffic. It runs
|
|
45
|
+
the whole path — capture, decode, measure, export — which is what makes it
|
|
46
|
+
useful for writing and testing a script without hardware, but a decoder run on
|
|
47
|
+
it finds structure by chance.
|
|
48
|
+
|
|
49
|
+
## Read the raw signal
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
for edge in workspace.data.transitions(channel=0):
|
|
53
|
+
print(edge.sample, edge.level)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The iterator pages for you, however long the capture. The first edge it yields
|
|
57
|
+
is the level in force where you started reading, so a window describes itself.
|
|
58
|
+
|
|
59
|
+
## Find out what a decoder takes
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
for option in workspace.decoders.options("i2c.js"):
|
|
63
|
+
print(option)
|
|
64
|
+
# ch_sda: SDA Channel (ch_selector) = None
|
|
65
|
+
# address_format [Advanced options]: Address display format (combo)
|
|
66
|
+
# one of ['HEX', 'Binary', 'Decimal'] = 'HEX'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Pass those by id or by caption, as Python values — a channel number, a combo
|
|
70
|
+
entry by name, a flag as `True`. Anything left out keeps the script's default.
|
|
71
|
+
|
|
72
|
+
## Leave a capture running
|
|
73
|
+
|
|
74
|
+
The server owns the workspace, so disconnecting destroys nothing. Start a long
|
|
75
|
+
acquisition, go away, and come back to it:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
with ScanaStudio.connect() as server:
|
|
79
|
+
workspace = server.create("hw:SP259-000123")
|
|
80
|
+
workspace.capture.start(samples=2_000_000_000)
|
|
81
|
+
print("running as workspace", workspace.id)
|
|
82
|
+
|
|
83
|
+
# later, from anywhere
|
|
84
|
+
with ScanaStudio.connect("ws://bench.local:4911") as server:
|
|
85
|
+
for session in server.workspaces():
|
|
86
|
+
print(session.workspace_id, session.device_name, session.state)
|
|
87
|
+
workspace = server.attach(session.workspace_id)
|
|
88
|
+
workspace.capture.wait()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
⚠️ A server listening beyond loopback has **no authentication**: whoever reaches
|
|
92
|
+
the port has the devices.
|
|
93
|
+
|
|
94
|
+
## What is where
|
|
95
|
+
|
|
96
|
+
| | |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `server.devices()`, `server.workspaces()` | what you can open, and what is already open |
|
|
99
|
+
| `server.capabilities(device)` | the rates, depths and thresholds it offers |
|
|
100
|
+
| `server.scripts` | the decoder library, which belongs to the server |
|
|
101
|
+
| `workspace.capture` | arming, running and waiting |
|
|
102
|
+
| `workspace.data` | raw edges, packets, items, hex |
|
|
103
|
+
| `workspace.decoders` | attaching and configuring decoders |
|
|
104
|
+
| `workspace.markers` | time markers |
|
|
105
|
+
| `workspace.measures` | measurements between two markers |
|
|
106
|
+
| `workspace.pattern` | the pattern generator |
|
|
107
|
+
| `workspace.events` | everything the server pushes |
|
|
108
|
+
|
|
109
|
+
## Versions
|
|
110
|
+
|
|
111
|
+
This SDK speaks protocol 7. `ScanaStudio.connect` refuses a server that speaks
|
|
112
|
+
another, since a mismatch means one of the two needs updating. The protocol
|
|
113
|
+
itself is documented in
|
|
114
|
+
[`api/schema/protocol.md`](https://github.com/ikalogic/scanastudio-v6/blob/main/api/schema/protocol.md).
|
|
115
|
+
|
|
116
|
+
MIT licensed.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ikalogic-scanastudio"
|
|
3
|
+
version = "0.2.1"
|
|
4
|
+
description = "Python SDK for ScanaStudio v6 logic analysers and pattern generators"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "IKALOGIC", email = "contact@ikalogic.com" }]
|
|
9
|
+
keywords = [
|
|
10
|
+
"scanastudio",
|
|
11
|
+
"ikalogic",
|
|
12
|
+
"logic-analyzer",
|
|
13
|
+
"protocol-decoder",
|
|
14
|
+
"pattern-generator",
|
|
15
|
+
"instrumentation",
|
|
16
|
+
"sdk",
|
|
17
|
+
"api",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: Manufacturing",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
30
|
+
"Topic :: Scientific/Engineering",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
requires-python = ">=3.10"
|
|
34
|
+
dependencies = [
|
|
35
|
+
"pydantic>=2.7",
|
|
36
|
+
"websockets>=13.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://www.ikalogic.com/logic-analyzers/"
|
|
41
|
+
Documentation = "https://www.ikalogic.com/kb/scanastudio-api/"
|
|
42
|
+
Repository = "https://github.com/ikalogic/scanastudio-v6"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = ["pytest>=8", "mypy>=1.11", "ruff>=0.6"]
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["uv_build>=0.5,<0.9"]
|
|
49
|
+
build-backend = "uv_build"
|
|
50
|
+
|
|
51
|
+
[tool.uv.build-backend]
|
|
52
|
+
module-name = "ikalogic_scanastudio"
|
|
53
|
+
module-root = "src"
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 100
|
|
57
|
+
target-version = "py310"
|
|
58
|
+
# Generated: it mirrors the Rust contract, and reformatting it would fight the
|
|
59
|
+
# staleness check that keeps the two in step.
|
|
60
|
+
extend-exclude = ["src/ikalogic_scanastudio/_generated"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["E", "F", "I", "UP", "B", "W"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.per-file-ignores]
|
|
66
|
+
# Generated, and it mirrors the Rust contract rather than Python taste.
|
|
67
|
+
"src/ikalogic_scanastudio/_generated/*" = ["E501"]
|
|
68
|
+
|
|
69
|
+
[tool.mypy]
|
|
70
|
+
plugins = ["pydantic.mypy"]
|
|
71
|
+
python_version = "3.10"
|
|
72
|
+
strict = true
|
|
73
|
+
files = ["src/ikalogic_scanastudio"]
|
|
74
|
+
|
|
75
|
+
[[tool.mypy.overrides]]
|
|
76
|
+
module = "ikalogic_scanastudio._generated.*"
|
|
77
|
+
disallow_untyped_defs = false
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Python SDK for ScanaStudio v6.
|
|
2
|
+
|
|
3
|
+
The server owns the workspaces and speaks one WebSocket protocol; this package
|
|
4
|
+
is a typed client for it. Everything the ScanaStudio interface can do, a script
|
|
5
|
+
can do here — capture, decode, measure, generate patterns and export.
|
|
6
|
+
|
|
7
|
+
from ikalogic_scanastudio import ScanaStudio, Trigger
|
|
8
|
+
|
|
9
|
+
with ScanaStudio.connect() as server:
|
|
10
|
+
workspace = server.create("se254")
|
|
11
|
+
workspace.capture.run(
|
|
12
|
+
samples=1_000_000,
|
|
13
|
+
sample_rate=25_000_000,
|
|
14
|
+
trigger=Trigger.rising(channel=0),
|
|
15
|
+
)
|
|
16
|
+
i2c = workspace.decoders.add(
|
|
17
|
+
"i2c.js",
|
|
18
|
+
{"ch_scl": workspace.channel("I2C SCL"), "ch_sda": workspace.channel("I2C SDA")},
|
|
19
|
+
wait=True,
|
|
20
|
+
)
|
|
21
|
+
for packet in i2c.packets():
|
|
22
|
+
print(packet.start, packet.title, packet.content)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from ._core.errors import (
|
|
26
|
+
Busy,
|
|
27
|
+
ConnectionClosed,
|
|
28
|
+
IoError,
|
|
29
|
+
ProtocolError,
|
|
30
|
+
Refused,
|
|
31
|
+
ScanaStudioError,
|
|
32
|
+
Timeout,
|
|
33
|
+
VersionMismatch,
|
|
34
|
+
)
|
|
35
|
+
from ._core.frames import Edge, HexByte, HexPage, Item
|
|
36
|
+
from ._core.gui import Option, UnknownOption
|
|
37
|
+
from ._core.state import RUNNING_STATES
|
|
38
|
+
from ._generated.protocol import (
|
|
39
|
+
PROTOCOL_VERSION,
|
|
40
|
+
CapabilitiesEvent,
|
|
41
|
+
ChannelDesc,
|
|
42
|
+
HexByteDesc,
|
|
43
|
+
ItemRowDesc,
|
|
44
|
+
MarkerDesc,
|
|
45
|
+
MeasureDesc,
|
|
46
|
+
PacketDesc,
|
|
47
|
+
RowFilter,
|
|
48
|
+
ScriptDesc,
|
|
49
|
+
TriggerRequest,
|
|
50
|
+
WorkspaceDesc,
|
|
51
|
+
)
|
|
52
|
+
from .capture import DEFAULT_WAIT, CaptureApi
|
|
53
|
+
from .client import DEFAULT_URL, SUPPORTED_PROTOCOLS, Device, ScanaStudio, ServerInfo
|
|
54
|
+
from .data import EDGE_PAGE, PAGE, DataApi
|
|
55
|
+
from .decoders import Decoder, DecodersApi, ScriptError
|
|
56
|
+
from .events import EventsApi
|
|
57
|
+
from .export import DEFAULT_IO_WAIT, EXPORT_SOURCES
|
|
58
|
+
from .markers import MarkersApi
|
|
59
|
+
from .measures import MEASURE_KINDS, MeasuresApi
|
|
60
|
+
from .patterns import PatternApi
|
|
61
|
+
from .scripts import ScriptOpError, ScriptsApi
|
|
62
|
+
from .trigger import SEQUENCES, Trigger, step
|
|
63
|
+
from .workspace import Workspace
|
|
64
|
+
|
|
65
|
+
__version__ = "0.1.0"
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"DEFAULT_URL",
|
|
69
|
+
"DEFAULT_WAIT",
|
|
70
|
+
"EDGE_PAGE",
|
|
71
|
+
"DEFAULT_IO_WAIT",
|
|
72
|
+
"EXPORT_SOURCES",
|
|
73
|
+
"MEASURE_KINDS",
|
|
74
|
+
"PAGE",
|
|
75
|
+
"PROTOCOL_VERSION",
|
|
76
|
+
"RUNNING_STATES",
|
|
77
|
+
"SEQUENCES",
|
|
78
|
+
"SUPPORTED_PROTOCOLS",
|
|
79
|
+
"Busy",
|
|
80
|
+
"CapabilitiesEvent",
|
|
81
|
+
"CaptureApi",
|
|
82
|
+
"ChannelDesc",
|
|
83
|
+
"ConnectionClosed",
|
|
84
|
+
"DataApi",
|
|
85
|
+
"Decoder",
|
|
86
|
+
"DecodersApi",
|
|
87
|
+
"Device",
|
|
88
|
+
"Edge",
|
|
89
|
+
"EventsApi",
|
|
90
|
+
"HexByte",
|
|
91
|
+
"HexByteDesc",
|
|
92
|
+
"HexPage",
|
|
93
|
+
"IoError",
|
|
94
|
+
"Item",
|
|
95
|
+
"ItemRowDesc",
|
|
96
|
+
"MarkerDesc",
|
|
97
|
+
"MarkersApi",
|
|
98
|
+
"MeasuresApi",
|
|
99
|
+
"MeasureDesc",
|
|
100
|
+
"Option",
|
|
101
|
+
"PacketDesc",
|
|
102
|
+
"PatternApi",
|
|
103
|
+
"ProtocolError",
|
|
104
|
+
"Refused",
|
|
105
|
+
"RowFilter",
|
|
106
|
+
"ScanaStudio",
|
|
107
|
+
"ScanaStudioError",
|
|
108
|
+
"ScriptDesc",
|
|
109
|
+
"ScriptError",
|
|
110
|
+
"ScriptOpError",
|
|
111
|
+
"ScriptsApi",
|
|
112
|
+
"ServerInfo",
|
|
113
|
+
"Timeout",
|
|
114
|
+
"Trigger",
|
|
115
|
+
"TriggerRequest",
|
|
116
|
+
"UnknownOption",
|
|
117
|
+
"VersionMismatch",
|
|
118
|
+
"Workspace",
|
|
119
|
+
"WorkspaceDesc",
|
|
120
|
+
"step",
|
|
121
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""What the SDK raises, and why."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ScanaStudioError(Exception):
|
|
7
|
+
"""Base of every error this package raises."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConnectionClosed(ScanaStudioError):
|
|
11
|
+
"""The server went away while a request was in flight.
|
|
12
|
+
|
|
13
|
+
A workspace survives a disconnect: reconnect and attach to its id to pick
|
|
14
|
+
up where the capture got to.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProtocolError(ScanaStudioError):
|
|
19
|
+
"""The server refused a command.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
code: Stable and matchable — ``bad_message`` or ``no_workspace``.
|
|
23
|
+
message: A sentence written by the server, to show as is.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, code: str, message: str):
|
|
27
|
+
super().__init__(f"{message} ({code})")
|
|
28
|
+
self.code = code
|
|
29
|
+
self.message = message
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Timeout(ScanaStudioError):
|
|
33
|
+
"""No answer arrived in time.
|
|
34
|
+
|
|
35
|
+
The server drops nothing it can read, so this usually means the operation
|
|
36
|
+
is still running rather than lost. Raise the timeout, or wait on the event
|
|
37
|
+
the operation reports through.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VersionMismatch(ScanaStudioError):
|
|
42
|
+
"""The server speaks a protocol this SDK does not.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
spoken: The protocol version the server announced.
|
|
46
|
+
supported: The versions this SDK knows.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, spoken: int, supported: tuple[int, ...]):
|
|
50
|
+
listed = ", ".join(str(v) for v in supported)
|
|
51
|
+
super().__init__(
|
|
52
|
+
f"the server speaks protocol {spoken}, this SDK speaks {listed}. "
|
|
53
|
+
"Update whichever is older."
|
|
54
|
+
)
|
|
55
|
+
self.spoken = spoken
|
|
56
|
+
self.supported = supported
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class IoError(ScanaStudioError):
|
|
60
|
+
"""A save, load or export failed, with the server's own reason."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Refused(ScanaStudioError):
|
|
64
|
+
"""The server declined to start an acquisition, with its written reason."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Busy(ScanaStudioError):
|
|
68
|
+
"""The workspace is frozen by a running export, so the command was refused."""
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Binary frames. They carry the bulk data JSON would make expensive, and their
|
|
2
|
+
layouts live in `api/schema/protocol.md`."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import struct
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import NamedTuple
|
|
9
|
+
|
|
10
|
+
from .._generated.protocol import BINARY_FRAME_MAGIC
|
|
11
|
+
|
|
12
|
+
_HEADER = struct.Struct("<II")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FrameHeader(NamedTuple):
|
|
16
|
+
request_id: int
|
|
17
|
+
body: bytes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def read_header(frame: bytes) -> FrameHeader | None:
|
|
21
|
+
"""Splits `[magic u32 LE][id u32 LE]` off a frame, or None if it is not one."""
|
|
22
|
+
if len(frame) < _HEADER.size:
|
|
23
|
+
return None
|
|
24
|
+
magic, request_id = _HEADER.unpack_from(frame, 0)
|
|
25
|
+
if magic != BINARY_FRAME_MAGIC:
|
|
26
|
+
return None
|
|
27
|
+
return FrameHeader(request_id, frame[_HEADER.size :])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Edge(NamedTuple):
|
|
31
|
+
"""One transition: the channel holds `level` from `sample` until the next."""
|
|
32
|
+
|
|
33
|
+
sample: int
|
|
34
|
+
level: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_EDGE = struct.Struct("<qB")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def read_transitions(body: bytes) -> list[Edge]:
|
|
41
|
+
"""`[n u32 LE]` then n × `[sample i64 LE][level u8]`."""
|
|
42
|
+
(count,) = struct.unpack_from("<I", body, 0)
|
|
43
|
+
at = 4
|
|
44
|
+
edges = []
|
|
45
|
+
for _ in range(count):
|
|
46
|
+
sample, level = _EDGE.unpack_from(body, at)
|
|
47
|
+
edges.append(Edge(sample, level))
|
|
48
|
+
at += _EDGE.size
|
|
49
|
+
return edges
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Item:
|
|
54
|
+
"""One decoded item, as the script emitted it."""
|
|
55
|
+
|
|
56
|
+
channel: int
|
|
57
|
+
start: int
|
|
58
|
+
end: int
|
|
59
|
+
emphasis: int
|
|
60
|
+
contents: list[str]
|
|
61
|
+
"""Longest first: a renderer picks the one that fits the width it has."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def read_items(body: bytes) -> list[Item]:
|
|
65
|
+
"""Item records, then the string table their content indices point into."""
|
|
66
|
+
(count,) = struct.unpack_from("<I", body, 0)
|
|
67
|
+
at = 4
|
|
68
|
+
raw: list[tuple[int, int, int, int, list[int]]] = []
|
|
69
|
+
for _ in range(count):
|
|
70
|
+
channel, emphasis, n_contents = struct.unpack_from("<HBB", body, at)
|
|
71
|
+
at += 4
|
|
72
|
+
start, end = struct.unpack_from("<qq", body, at)
|
|
73
|
+
at += 16
|
|
74
|
+
indices = list(struct.unpack_from(f"<{n_contents}I", body, at))
|
|
75
|
+
at += 4 * n_contents
|
|
76
|
+
raw.append((channel, emphasis, start, end, indices))
|
|
77
|
+
|
|
78
|
+
(n_strings,) = struct.unpack_from("<I", body, at)
|
|
79
|
+
at += 4
|
|
80
|
+
table: list[str] = []
|
|
81
|
+
for _ in range(n_strings):
|
|
82
|
+
(length,) = struct.unpack_from("<I", body, at)
|
|
83
|
+
at += 4
|
|
84
|
+
table.append(body[at : at + length].decode("utf-8", "replace"))
|
|
85
|
+
at += length
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
Item(
|
|
89
|
+
channel=channel,
|
|
90
|
+
start=start,
|
|
91
|
+
end=end,
|
|
92
|
+
emphasis=emphasis,
|
|
93
|
+
contents=[table[i] for i in indices if i < len(table)],
|
|
94
|
+
)
|
|
95
|
+
for channel, emphasis, start, end, indices in raw
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class HexByte:
|
|
101
|
+
"""One byte of a decoder's hex view, with the samples it spans."""
|
|
102
|
+
|
|
103
|
+
channel: int
|
|
104
|
+
value: int
|
|
105
|
+
start: int
|
|
106
|
+
end: int
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class HexPage:
|
|
111
|
+
"""A page of the hex view, and the generation it belongs to.
|
|
112
|
+
|
|
113
|
+
The store is append-only between clears, so a client pulls only the tail.
|
|
114
|
+
A `generation` that moved means the store was cleared: drop what you cached
|
|
115
|
+
and start again from offset 0.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
generation: int
|
|
119
|
+
total: int
|
|
120
|
+
bytes: list[HexByte]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
_HEX = struct.Struct("<HBqq")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def read_hex_page(body: bytes) -> HexPage:
|
|
127
|
+
"""`[generation u32][total u32][n u32]` then n × `[ch u16][byte u8][start i64][end i64]`."""
|
|
128
|
+
generation, total, count = struct.unpack_from("<III", body, 0)
|
|
129
|
+
at = 12
|
|
130
|
+
out = []
|
|
131
|
+
for _ in range(count):
|
|
132
|
+
channel, value, start, end = _HEX.unpack_from(body, at)
|
|
133
|
+
out.append(HexByte(channel, value, start, end))
|
|
134
|
+
at += _HEX.size
|
|
135
|
+
return HexPage(generation, total, out)
|