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.
Files changed (26) hide show
  1. ikalogic_scanastudio-0.2.1/LICENSE +21 -0
  2. ikalogic_scanastudio-0.2.1/PKG-INFO +145 -0
  3. ikalogic_scanastudio-0.2.1/README.md +116 -0
  4. ikalogic_scanastudio-0.2.1/pyproject.toml +77 -0
  5. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/__init__.py +121 -0
  6. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/__init__.py +0 -0
  7. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/errors.py +68 -0
  8. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/frames.py +135 -0
  9. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/gui.py +206 -0
  10. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/state.py +100 -0
  11. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_core/transport.py +278 -0
  12. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_generated/__init__.py +0 -0
  13. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/_generated/protocol.py +2522 -0
  14. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/capture.py +180 -0
  15. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/client.py +306 -0
  16. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/data.py +206 -0
  17. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/decoders.py +260 -0
  18. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/events.py +73 -0
  19. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/export.py +10 -0
  20. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/markers.py +75 -0
  21. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/measures.py +161 -0
  22. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/patterns.py +128 -0
  23. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/py.typed +0 -0
  24. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/scripts.py +193 -0
  25. ikalogic_scanastudio-0.2.1/src/ikalogic_scanastudio/trigger.py +162 -0
  26. 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
+ ]
@@ -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)