lab-sync-driver-protocol 0.3.0__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.
- lab_sync_driver_protocol-0.3.0/LICENSE +21 -0
- lab_sync_driver_protocol-0.3.0/MANIFEST.in +2 -0
- lab_sync_driver_protocol-0.3.0/PKG-INFO +84 -0
- lab_sync_driver_protocol-0.3.0/README.md +66 -0
- lab_sync_driver_protocol-0.3.0/__init__.py +27 -0
- lab_sync_driver_protocol-0.3.0/environment.py +17 -0
- lab_sync_driver_protocol-0.3.0/lab_sync_driver_protocol.egg-info/PKG-INFO +84 -0
- lab_sync_driver_protocol-0.3.0/lab_sync_driver_protocol.egg-info/SOURCES.txt +16 -0
- lab_sync_driver_protocol-0.3.0/lab_sync_driver_protocol.egg-info/dependency_links.txt +1 -0
- lab_sync_driver_protocol-0.3.0/lab_sync_driver_protocol.egg-info/top_level.txt +1 -0
- lab_sync_driver_protocol-0.3.0/py.typed +0 -0
- lab_sync_driver_protocol-0.3.0/pyproject.toml +33 -0
- lab_sync_driver_protocol-0.3.0/setup.cfg +4 -0
- lab_sync_driver_protocol-0.3.0/wire.py +65 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lab Sync
|
|
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,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lab-sync-driver-protocol
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Versioned stdio wire contract between the device engine and out-of-process device drivers
|
|
5
|
+
Author: Lab Sync
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# lab-sync-driver-protocol
|
|
20
|
+
|
|
21
|
+
The versioned wire contract between device drivers and the supervisor that
|
|
22
|
+
launches them. A driver runs as a child process and the two talk
|
|
23
|
+
newline-delimited JSON over the child's stdin and stdout: stdout is the
|
|
24
|
+
protocol channel and nothing else may write to it, stderr is the driver's log
|
|
25
|
+
channel. The package is importable by both sides and depends on nothing, so a
|
|
26
|
+
driver speaks the protocol without importing anything of the supervisor's.
|
|
27
|
+
|
|
28
|
+
Most driver authors want [`lab-sync-driver-sdk`](https://pypi.org/project/lab-sync-driver-sdk/),
|
|
29
|
+
which implements the driver side of this protocol from a decorated Python
|
|
30
|
+
class. Depend on `lab-sync-driver-protocol` directly only to implement the protocol
|
|
31
|
+
yourself, in Python or as a reference for another language.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install lab-sync-driver-protocol
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Frames
|
|
38
|
+
|
|
39
|
+
One JSON object per line. Every request carries an `id`, and every frame a
|
|
40
|
+
driver sends in answer carries the same `id` and a `type`:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
Request (supervisor -> driver): {"id": <int>, "method": <str>, "params": {...}}
|
|
44
|
+
Reply (driver -> supervisor): {"id": <int>, "type": "reply", "result": {...}}
|
|
45
|
+
Execute stream: {"id", "type": "progress", "fraction", "message"}
|
|
46
|
+
... -> {"id", "type": "result", "output_json"}
|
|
47
|
+
or {"id", "type": "error", "code", "message"}
|
|
48
|
+
Telemetry (driver -> supervisor): {"type": "telemetry", "channel": <str>, "payload": {...}}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`lab_sync_driver_protocol.wire` holds the framing (`encode`, `decode`, `read_frame`,
|
|
52
|
+
`write_frame`), the protocol version (`PROTOCOL_VERSION`, currently `v0`) and
|
|
53
|
+
`STREAM_LIMIT`, the line length a reader should accept.
|
|
54
|
+
|
|
55
|
+
## Methods
|
|
56
|
+
|
|
57
|
+
The request vocabulary is `handshake`, `describe`, `connect`,
|
|
58
|
+
`set_simulation`, `ping`, `execute` (streaming progress/result/error frames),
|
|
59
|
+
`abort`, `describe_options`, `estimate_time` and `set_telemetry_demand`.
|
|
60
|
+
|
|
61
|
+
`describe_options` is optional and additive (the protocol version stays `v0`):
|
|
62
|
+
request `{"command": name, "parameters": {…}}`, reply
|
|
63
|
+
`{"options": {param_name: [choice, …]}}` — the runtime choices for the
|
|
64
|
+
command's `dynamic_options` parameters, best-effort per parameter. A driver
|
|
65
|
+
predating it answers with the standard `unknown_method` error frame, which a
|
|
66
|
+
supervisor reads as "no dynamic options" rather than a fault.
|
|
67
|
+
|
|
68
|
+
Telemetry is demand-gated: a driver samples nothing until the supervisor sends
|
|
69
|
+
`set_telemetry_demand` with the complete set of channels that should be
|
|
70
|
+
running. See the `lab_sync_driver_protocol.wire` module docstring.
|
|
71
|
+
|
|
72
|
+
## Environment
|
|
73
|
+
|
|
74
|
+
The supervisor passes per-instance values in the driver process's environment,
|
|
75
|
+
since the launch command is shared by every instance of a driver type:
|
|
76
|
+
|
|
77
|
+
- `STUDIO_DEVICE_CONFIGURATION` (`CONFIGURATION_ENV`): the hardware variant
|
|
78
|
+
the device was added as, one of the `configurations` the driver describes.
|
|
79
|
+
- `STUDIO_DEVICE_DATA_DIR` (`DATA_DIR_ENV`): the instance's own writable
|
|
80
|
+
directory, which is also the process's working directory.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# lab-sync-driver-protocol
|
|
2
|
+
|
|
3
|
+
The versioned wire contract between device drivers and the supervisor that
|
|
4
|
+
launches them. A driver runs as a child process and the two talk
|
|
5
|
+
newline-delimited JSON over the child's stdin and stdout: stdout is the
|
|
6
|
+
protocol channel and nothing else may write to it, stderr is the driver's log
|
|
7
|
+
channel. The package is importable by both sides and depends on nothing, so a
|
|
8
|
+
driver speaks the protocol without importing anything of the supervisor's.
|
|
9
|
+
|
|
10
|
+
Most driver authors want [`lab-sync-driver-sdk`](https://pypi.org/project/lab-sync-driver-sdk/),
|
|
11
|
+
which implements the driver side of this protocol from a decorated Python
|
|
12
|
+
class. Depend on `lab-sync-driver-protocol` directly only to implement the protocol
|
|
13
|
+
yourself, in Python or as a reference for another language.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install lab-sync-driver-protocol
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Frames
|
|
20
|
+
|
|
21
|
+
One JSON object per line. Every request carries an `id`, and every frame a
|
|
22
|
+
driver sends in answer carries the same `id` and a `type`:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
Request (supervisor -> driver): {"id": <int>, "method": <str>, "params": {...}}
|
|
26
|
+
Reply (driver -> supervisor): {"id": <int>, "type": "reply", "result": {...}}
|
|
27
|
+
Execute stream: {"id", "type": "progress", "fraction", "message"}
|
|
28
|
+
... -> {"id", "type": "result", "output_json"}
|
|
29
|
+
or {"id", "type": "error", "code", "message"}
|
|
30
|
+
Telemetry (driver -> supervisor): {"type": "telemetry", "channel": <str>, "payload": {...}}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`lab_sync_driver_protocol.wire` holds the framing (`encode`, `decode`, `read_frame`,
|
|
34
|
+
`write_frame`), the protocol version (`PROTOCOL_VERSION`, currently `v0`) and
|
|
35
|
+
`STREAM_LIMIT`, the line length a reader should accept.
|
|
36
|
+
|
|
37
|
+
## Methods
|
|
38
|
+
|
|
39
|
+
The request vocabulary is `handshake`, `describe`, `connect`,
|
|
40
|
+
`set_simulation`, `ping`, `execute` (streaming progress/result/error frames),
|
|
41
|
+
`abort`, `describe_options`, `estimate_time` and `set_telemetry_demand`.
|
|
42
|
+
|
|
43
|
+
`describe_options` is optional and additive (the protocol version stays `v0`):
|
|
44
|
+
request `{"command": name, "parameters": {…}}`, reply
|
|
45
|
+
`{"options": {param_name: [choice, …]}}` — the runtime choices for the
|
|
46
|
+
command's `dynamic_options` parameters, best-effort per parameter. A driver
|
|
47
|
+
predating it answers with the standard `unknown_method` error frame, which a
|
|
48
|
+
supervisor reads as "no dynamic options" rather than a fault.
|
|
49
|
+
|
|
50
|
+
Telemetry is demand-gated: a driver samples nothing until the supervisor sends
|
|
51
|
+
`set_telemetry_demand` with the complete set of channels that should be
|
|
52
|
+
running. See the `lab_sync_driver_protocol.wire` module docstring.
|
|
53
|
+
|
|
54
|
+
## Environment
|
|
55
|
+
|
|
56
|
+
The supervisor passes per-instance values in the driver process's environment,
|
|
57
|
+
since the launch command is shared by every instance of a driver type:
|
|
58
|
+
|
|
59
|
+
- `STUDIO_DEVICE_CONFIGURATION` (`CONFIGURATION_ENV`): the hardware variant
|
|
60
|
+
the device was added as, one of the `configurations` the driver describes.
|
|
61
|
+
- `STUDIO_DEVICE_DATA_DIR` (`DATA_DIR_ENV`): the instance's own writable
|
|
62
|
+
directory, which is also the process's working directory.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Shared wire contract for the driver protocol.
|
|
2
|
+
|
|
3
|
+
Hand-written newline-delimited JSON framing -- no codegen. Importable by the
|
|
4
|
+
supervisor AND by out-of-process drivers, so a driver speaks the protocol without
|
|
5
|
+
importing anything of the supervisor's. Transport is the child process's
|
|
6
|
+
stdin/stdout; see `wire` for the frame schema.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from lab_sync_driver_protocol.environment import CONFIGURATION_ENV
|
|
10
|
+
from lab_sync_driver_protocol.environment import DATA_DIR_ENV
|
|
11
|
+
from lab_sync_driver_protocol.wire import PROTOCOL_VERSION
|
|
12
|
+
from lab_sync_driver_protocol.wire import STREAM_LIMIT
|
|
13
|
+
from lab_sync_driver_protocol.wire import decode
|
|
14
|
+
from lab_sync_driver_protocol.wire import encode
|
|
15
|
+
from lab_sync_driver_protocol.wire import read_frame
|
|
16
|
+
from lab_sync_driver_protocol.wire import write_frame
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CONFIGURATION_ENV",
|
|
20
|
+
"DATA_DIR_ENV",
|
|
21
|
+
"PROTOCOL_VERSION",
|
|
22
|
+
"STREAM_LIMIT",
|
|
23
|
+
"decode",
|
|
24
|
+
"encode",
|
|
25
|
+
"read_frame",
|
|
26
|
+
"write_frame",
|
|
27
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Per-instance values the supervisor hands a driver process through its environment.
|
|
2
|
+
|
|
3
|
+
The launch command is a driver *type's* (shared by every instance), and stdin is the protocol channel, so anything the supervisor knows about
|
|
4
|
+
one *instance* before the process starts rides the environment. Both are set on every spawn; a driver that does not read them is unaffected.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
#: The hardware variant this device was added as -- one of the names the driver
|
|
8
|
+
#: lists as its ``configurations`` in ``describe`` -- or unset for a driver that
|
|
9
|
+
#: declares none, or a device added before the driver declared any (the driver's
|
|
10
|
+
#: first configuration is then the one it runs as).
|
|
11
|
+
CONFIGURATION_ENV = "STUDIO_DEVICE_CONFIGURATION"
|
|
12
|
+
|
|
13
|
+
#: This instance's own writable directory (also the process's working
|
|
14
|
+
#: directory). The supervisor searches it when serving a relative ``model_path`` or
|
|
15
|
+
#: ``top_view_image_path``, so a driver that generates a file per instance --
|
|
16
|
+
#: a 3D model assembled for the configuration it runs as -- writes it here.
|
|
17
|
+
DATA_DIR_ENV = "STUDIO_DEVICE_DATA_DIR"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lab-sync-driver-protocol
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Versioned stdio wire contract between the device engine and out-of-process device drivers
|
|
5
|
+
Author: Lab Sync
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# lab-sync-driver-protocol
|
|
20
|
+
|
|
21
|
+
The versioned wire contract between device drivers and the supervisor that
|
|
22
|
+
launches them. A driver runs as a child process and the two talk
|
|
23
|
+
newline-delimited JSON over the child's stdin and stdout: stdout is the
|
|
24
|
+
protocol channel and nothing else may write to it, stderr is the driver's log
|
|
25
|
+
channel. The package is importable by both sides and depends on nothing, so a
|
|
26
|
+
driver speaks the protocol without importing anything of the supervisor's.
|
|
27
|
+
|
|
28
|
+
Most driver authors want [`lab-sync-driver-sdk`](https://pypi.org/project/lab-sync-driver-sdk/),
|
|
29
|
+
which implements the driver side of this protocol from a decorated Python
|
|
30
|
+
class. Depend on `lab-sync-driver-protocol` directly only to implement the protocol
|
|
31
|
+
yourself, in Python or as a reference for another language.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install lab-sync-driver-protocol
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Frames
|
|
38
|
+
|
|
39
|
+
One JSON object per line. Every request carries an `id`, and every frame a
|
|
40
|
+
driver sends in answer carries the same `id` and a `type`:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
Request (supervisor -> driver): {"id": <int>, "method": <str>, "params": {...}}
|
|
44
|
+
Reply (driver -> supervisor): {"id": <int>, "type": "reply", "result": {...}}
|
|
45
|
+
Execute stream: {"id", "type": "progress", "fraction", "message"}
|
|
46
|
+
... -> {"id", "type": "result", "output_json"}
|
|
47
|
+
or {"id", "type": "error", "code", "message"}
|
|
48
|
+
Telemetry (driver -> supervisor): {"type": "telemetry", "channel": <str>, "payload": {...}}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`lab_sync_driver_protocol.wire` holds the framing (`encode`, `decode`, `read_frame`,
|
|
52
|
+
`write_frame`), the protocol version (`PROTOCOL_VERSION`, currently `v0`) and
|
|
53
|
+
`STREAM_LIMIT`, the line length a reader should accept.
|
|
54
|
+
|
|
55
|
+
## Methods
|
|
56
|
+
|
|
57
|
+
The request vocabulary is `handshake`, `describe`, `connect`,
|
|
58
|
+
`set_simulation`, `ping`, `execute` (streaming progress/result/error frames),
|
|
59
|
+
`abort`, `describe_options`, `estimate_time` and `set_telemetry_demand`.
|
|
60
|
+
|
|
61
|
+
`describe_options` is optional and additive (the protocol version stays `v0`):
|
|
62
|
+
request `{"command": name, "parameters": {…}}`, reply
|
|
63
|
+
`{"options": {param_name: [choice, …]}}` — the runtime choices for the
|
|
64
|
+
command's `dynamic_options` parameters, best-effort per parameter. A driver
|
|
65
|
+
predating it answers with the standard `unknown_method` error frame, which a
|
|
66
|
+
supervisor reads as "no dynamic options" rather than a fault.
|
|
67
|
+
|
|
68
|
+
Telemetry is demand-gated: a driver samples nothing until the supervisor sends
|
|
69
|
+
`set_telemetry_demand` with the complete set of channels that should be
|
|
70
|
+
running. See the `lab_sync_driver_protocol.wire` module docstring.
|
|
71
|
+
|
|
72
|
+
## Environment
|
|
73
|
+
|
|
74
|
+
The supervisor passes per-instance values in the driver process's environment,
|
|
75
|
+
since the launch command is shared by every instance of a driver type:
|
|
76
|
+
|
|
77
|
+
- `STUDIO_DEVICE_CONFIGURATION` (`CONFIGURATION_ENV`): the hardware variant
|
|
78
|
+
the device was added as, one of the `configurations` the driver describes.
|
|
79
|
+
- `STUDIO_DEVICE_DATA_DIR` (`DATA_DIR_ENV`): the instance's own writable
|
|
80
|
+
directory, which is also the process's working directory.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
__init__.py
|
|
5
|
+
environment.py
|
|
6
|
+
py.typed
|
|
7
|
+
pyproject.toml
|
|
8
|
+
wire.py
|
|
9
|
+
./__init__.py
|
|
10
|
+
./environment.py
|
|
11
|
+
./py.typed
|
|
12
|
+
./wire.py
|
|
13
|
+
lab_sync_driver_protocol.egg-info/PKG-INFO
|
|
14
|
+
lab_sync_driver_protocol.egg-info/SOURCES.txt
|
|
15
|
+
lab_sync_driver_protocol.egg-info/dependency_links.txt
|
|
16
|
+
lab_sync_driver_protocol.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lab_sync_driver_protocol
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "lab-sync-driver-protocol"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Versioned stdio wire contract between the device engine and out-of-process device drivers"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Lab Sync" }]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Intended Audience :: Science/Research",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
"Topic :: Scientific/Engineering",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
# 77: PEP 639 license expressions (`license = "MIT"`, `license-files`).
|
|
23
|
+
requires = ["setuptools>=77.0"]
|
|
24
|
+
build-backend = "setuptools.build_meta"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
packages = ["lab_sync_driver_protocol"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-dir]
|
|
30
|
+
lab_sync_driver_protocol = "."
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
lab_sync_driver_protocol = ["py.typed"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Newline-delimited JSON framing for the driver protocol.
|
|
2
|
+
|
|
3
|
+
One JSON object per line over the child process's stdin/stdout; stdout carries
|
|
4
|
+
protocol frames only, stderr is the driver's log channel. The frame schema is
|
|
5
|
+
transport-shaped -- a request `id` plus a `type` discriminator -- so unary
|
|
6
|
+
replies and the Execute progress stream share one duplex pipe, and a driver may
|
|
7
|
+
later push un-correlated frames (telemetry) without a pending request.
|
|
8
|
+
|
|
9
|
+
Request (supervisor -> driver): {"id": <int>, "method": <str>, "params": {...}}
|
|
10
|
+
Reply (driver -> supervisor): {"id": <int>, "type": "reply", "result": {...}}
|
|
11
|
+
Execute stream: {"id", "type": "progress", "fraction", "message"}
|
|
12
|
+
... -> {"id", "type": "result", "output_json"}
|
|
13
|
+
or {"id", "type": "error", "code", "message"}
|
|
14
|
+
Telemetry (driver -> supervisor): {"type": "telemetry", "channel": <str>, "payload": {...}}
|
|
15
|
+
|
|
16
|
+
Telemetry is the un-correlated case this framing was shaped for: the frame
|
|
17
|
+
carries no `id` because nothing requested that particular reading, and a reader
|
|
18
|
+
must route on `type` before looking for one. It is **demand-gated rather than
|
|
19
|
+
free-running** -- a driver samples nothing until the supervisor says to:
|
|
20
|
+
|
|
21
|
+
supervisor -> driver: {"id", "method": "set_telemetry_demand",
|
|
22
|
+
"params": {"channels": [<str>, ...]}}
|
|
23
|
+
driver -> supervisor: {"id", "type": "reply", "result": {"channels": [...]}}
|
|
24
|
+
|
|
25
|
+
`channels` is the complete set that should be running, not a delta, so a lost
|
|
26
|
+
or reordered frame cannot leave a sampler stuck on. A driver that declares no
|
|
27
|
+
telemetry channels never sees the method, and one that receives a channel it
|
|
28
|
+
does not know ignores it rather than erroring -- both keep a supervisor and a driver
|
|
29
|
+
at different versions working together.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
from typing import TYPE_CHECKING
|
|
36
|
+
from typing import cast
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
import asyncio
|
|
40
|
+
|
|
41
|
+
PROTOCOL_VERSION = "v0"
|
|
42
|
+
|
|
43
|
+
# Bump asyncio's 64 KiB line cap: a single Execute params/output frame may be
|
|
44
|
+
# larger than the default StreamReader limit, and readline() would raise.
|
|
45
|
+
STREAM_LIMIT = 2**24
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def encode(frame: dict[str, object]) -> bytes:
|
|
49
|
+
return (json.dumps(frame) + "\n").encode("utf-8")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def decode(line: bytes) -> dict[str, object]:
|
|
53
|
+
return cast("dict[str, object]", json.loads(line))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def read_frame(reader: asyncio.StreamReader) -> dict[str, object] | None:
|
|
57
|
+
line = await reader.readline()
|
|
58
|
+
if not line:
|
|
59
|
+
return None
|
|
60
|
+
return decode(line)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def write_frame(writer: asyncio.StreamWriter, frame: dict[str, object]) -> None:
|
|
64
|
+
writer.write(encode(frame))
|
|
65
|
+
await writer.drain()
|