labbench-comm 0.1.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.
- labbench_comm-0.1.1/LICENSE +21 -0
- labbench_comm-0.1.1/PKG-INFO +97 -0
- labbench_comm-0.1.1/README.md +46 -0
- labbench_comm-0.1.1/pyproject.toml +65 -0
- labbench_comm-0.1.1/setup.cfg +4 -0
- labbench_comm-0.1.1/src/labbench_comm/__init__.py +3 -0
- labbench_comm-0.1.1/src/labbench_comm/_version.py +1 -0
- labbench_comm-0.1.1/src/labbench_comm/cli.py +18 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/__init__.py +0 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/__init__.py +92 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/codec.py +50 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/cpar_central.py +117 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/definitions.py +88 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/functions/__init__.py +13 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/functions/clear_waveform_programs.py +20 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/functions/set_operating_mode.py +42 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/functions/set_waveform_program.py +142 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/functions/start_stimulation.py +84 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/functions/stop_stimulation.py +28 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/instruction_codec.py +151 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/messages/__init__.py +7 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/messages/event_message.py +44 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/messages/status_message.py +263 -0
- labbench_comm-0.1.1/src/labbench_comm/devices/cpar/waveform.py +84 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/__init__.py +96 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/bus_central.py +195 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/destuffer.py +130 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/device.py +185 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/device_function.py +206 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/device_message.py +108 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/error_codes.py +12 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/exceptions.py +88 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/frame.py +44 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/function_dispatcher.py +35 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/functions/__init__.py +22 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/functions/device_identification.py +125 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/functions/ping.py +40 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/manufacturer.py +35 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/message_dispatcher.py +37 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/messages/__init__.py +20 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/messages/printf_message.py +57 -0
- labbench_comm-0.1.1/src/labbench_comm/protocols/packet.py +270 -0
- labbench_comm-0.1.1/src/labbench_comm/serial/__init__.py +0 -0
- labbench_comm-0.1.1/src/labbench_comm/serial/async_connection.py +29 -0
- labbench_comm-0.1.1/src/labbench_comm/serial/async_serial_connection.py +124 -0
- labbench_comm-0.1.1/src/labbench_comm/serial/base.py +46 -0
- labbench_comm-0.1.1/src/labbench_comm/serial/connection.py +164 -0
- labbench_comm-0.1.1/src/labbench_comm/utils/__init__.py +0 -0
- labbench_comm-0.1.1/src/labbench_comm/utils/additive_checksum.py +22 -0
- labbench_comm-0.1.1/src/labbench_comm/utils/crc8_ccitt.py +38 -0
- labbench_comm-0.1.1/src/labbench_comm.egg-info/PKG-INFO +97 -0
- labbench_comm-0.1.1/src/labbench_comm.egg-info/SOURCES.txt +54 -0
- labbench_comm-0.1.1/src/labbench_comm.egg-info/dependency_links.txt +1 -0
- labbench_comm-0.1.1/src/labbench_comm.egg-info/entry_points.txt +2 -0
- labbench_comm-0.1.1/src/labbench_comm.egg-info/requires.txt +7 -0
- labbench_comm-0.1.1/src/labbench_comm.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 LabBench Society
|
|
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,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: labbench_comm
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Communication with LabBench devices
|
|
5
|
+
Author: Kristian Hennings
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 LabBench Society
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://labbench.io/
|
|
29
|
+
Project-URL: Repository, https://github.com/LabBench-Society/LabBench.Python
|
|
30
|
+
Project-URL: Issues, https://github.com/LabBench-Society/LabBench.Python/issues
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Science/Research
|
|
33
|
+
Classifier: Topic :: Scientific/Engineering
|
|
34
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Requires-Python: >=3.12
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: pyserial>=3.5
|
|
45
|
+
Requires-Dist: typing-extensions>=4.5
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
48
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
49
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
50
|
+
Dynamic: license-file
|
|
51
|
+
|
|
52
|
+
# labbench-comm
|
|
53
|
+
|
|
54
|
+
**labbench-comm** is an asynchronous Python framework for communicating with LabBench hardware devices over serial connections.
|
|
55
|
+
It provides a robust, testable, and extensible protocol stack with concrete device implementations, starting with the **CPAR+** device.
|
|
56
|
+
|
|
57
|
+
The package is designed for:
|
|
58
|
+
- Scientific and clinical research setups
|
|
59
|
+
- Hardware control and automation
|
|
60
|
+
- Deterministic, protocol-driven device communication
|
|
61
|
+
- Async-first Python applications (`asyncio`)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
- **Async-first architecture** using `asyncio`
|
|
68
|
+
- **Robust serial communication** built on `pyserial`
|
|
69
|
+
- **Protocol abstraction** (framing, packets, checksums, dispatch)
|
|
70
|
+
- **Typed device functions and messages**
|
|
71
|
+
- **Extensible device model** for adding new LabBench devices
|
|
72
|
+
- **Unit tests + hardware integration tests**
|
|
73
|
+
- **CPAR+ device support** (functions, messages, waveform control)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Supported Devices
|
|
78
|
+
|
|
79
|
+
- **CPAR+** (pressure stimulation device)
|
|
80
|
+
|
|
81
|
+
Additional devices can be added by implementing new `Device`, `DeviceFunction`, and `DeviceMessage` classes.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Requirements
|
|
86
|
+
|
|
87
|
+
- Python **3.12+**
|
|
88
|
+
- Supported platforms: Windows, Linux, macOS (serial access required)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
Install from PyPI:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install labbench-comm
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# labbench-comm
|
|
2
|
+
|
|
3
|
+
**labbench-comm** is an asynchronous Python framework for communicating with LabBench hardware devices over serial connections.
|
|
4
|
+
It provides a robust, testable, and extensible protocol stack with concrete device implementations, starting with the **CPAR+** device.
|
|
5
|
+
|
|
6
|
+
The package is designed for:
|
|
7
|
+
- Scientific and clinical research setups
|
|
8
|
+
- Hardware control and automation
|
|
9
|
+
- Deterministic, protocol-driven device communication
|
|
10
|
+
- Async-first Python applications (`asyncio`)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Async-first architecture** using `asyncio`
|
|
17
|
+
- **Robust serial communication** built on `pyserial`
|
|
18
|
+
- **Protocol abstraction** (framing, packets, checksums, dispatch)
|
|
19
|
+
- **Typed device functions and messages**
|
|
20
|
+
- **Extensible device model** for adding new LabBench devices
|
|
21
|
+
- **Unit tests + hardware integration tests**
|
|
22
|
+
- **CPAR+ device support** (functions, messages, waveform control)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Supported Devices
|
|
27
|
+
|
|
28
|
+
- **CPAR+** (pressure stimulation device)
|
|
29
|
+
|
|
30
|
+
Additional devices can be added by implementing new `Device`, `DeviceFunction`, and `DeviceMessage` classes.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Python **3.12+**
|
|
37
|
+
- Supported platforms: Windows, Linux, macOS (serial access required)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
Install from PyPI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install labbench-comm
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "labbench_comm"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Communication with LabBench devices"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Kristian Hennings" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"pyserial>=3.5",
|
|
18
|
+
"typing-extensions>=4.5",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Science/Research",
|
|
24
|
+
"Topic :: Scientific/Engineering",
|
|
25
|
+
"Topic :: System :: Hardware :: Hardware Drivers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
labbench-comm = "labbench_comm.cli:main"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=8.0",
|
|
40
|
+
"pytest-asyncio>=0.23",
|
|
41
|
+
"pytest-cov>=5.0"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://labbench.io/"
|
|
46
|
+
Repository = "https://github.com/LabBench-Society/LabBench.Python"
|
|
47
|
+
Issues = "https://github.com/LabBench-Society/LabBench.Python/issues"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools]
|
|
50
|
+
package-dir = { "" = "src" }
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
where = ["src"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
addopts = "-ra"
|
|
58
|
+
markers = [
|
|
59
|
+
"cpar: integration tests for the LabBench CPAR+ device",
|
|
60
|
+
"hardware: tests that require real hardware",
|
|
61
|
+
"unittest: unit tests of individual classes"
|
|
62
|
+
]
|
|
63
|
+
log_cli = true
|
|
64
|
+
log_cli_level = "DEBUG"
|
|
65
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from labbench_comm import __version__
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
prog="labbench-comm",
|
|
9
|
+
description="LabBench device communication toolkit",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"--version",
|
|
14
|
+
action="version",
|
|
15
|
+
version=f"%(prog)s {__version__}",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
parser.parse_args()
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CPAR+ device support.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
- The CPARplusCentral device implementation
|
|
6
|
+
- CPAR-specific functions and messages
|
|
7
|
+
- Waveform and instruction utilities
|
|
8
|
+
- Device-specific enums and definitions
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# ----------------------------------------------------------------------
|
|
12
|
+
# Device
|
|
13
|
+
# ----------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
from .cpar_central import CPARplusCentral
|
|
16
|
+
|
|
17
|
+
# ----------------------------------------------------------------------
|
|
18
|
+
# Definitions / enums
|
|
19
|
+
# ----------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
from .definitions import (
|
|
22
|
+
DeviceChannelID,
|
|
23
|
+
DeviceState,
|
|
24
|
+
EcpError,
|
|
25
|
+
EventID,
|
|
26
|
+
OperatingMode,
|
|
27
|
+
StopCondition,
|
|
28
|
+
StopCriterion,
|
|
29
|
+
WaveformInstructionType,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# ----------------------------------------------------------------------
|
|
33
|
+
# Waveform
|
|
34
|
+
# ----------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
from .waveform import WaveformInstruction
|
|
37
|
+
from .instruction_codec import InstructionCodec
|
|
38
|
+
|
|
39
|
+
# ----------------------------------------------------------------------
|
|
40
|
+
# Functions
|
|
41
|
+
# ----------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
from .functions import (
|
|
44
|
+
SetWaveformProgram,
|
|
45
|
+
StartStimulation,
|
|
46
|
+
StopStimulation,
|
|
47
|
+
SetOperatingMode,
|
|
48
|
+
ClearWaveformPrograms,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# ----------------------------------------------------------------------
|
|
52
|
+
# Messages
|
|
53
|
+
# ----------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
from .messages import (
|
|
56
|
+
StatusMessage,
|
|
57
|
+
EventMessage,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ----------------------------------------------------------------------
|
|
61
|
+
# Public API
|
|
62
|
+
# ----------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
# Device
|
|
66
|
+
"CPARplusCentral",
|
|
67
|
+
|
|
68
|
+
# Enums / definitions
|
|
69
|
+
"DeviceChannelID",
|
|
70
|
+
"DeviceState",
|
|
71
|
+
"EcpError",
|
|
72
|
+
"EventID",
|
|
73
|
+
"OperatingMode",
|
|
74
|
+
"StopCondition",
|
|
75
|
+
"StopCriterion",
|
|
76
|
+
"WaveformInstructionType",
|
|
77
|
+
|
|
78
|
+
# Waveform
|
|
79
|
+
"WaveformInstruction",
|
|
80
|
+
"InstructionCodec",
|
|
81
|
+
|
|
82
|
+
# Functions
|
|
83
|
+
"SetWaveformProgram",
|
|
84
|
+
"StartStimulation",
|
|
85
|
+
"StopStimulation",
|
|
86
|
+
"SetOperatingMode",
|
|
87
|
+
"ClearWaveformPrograms",
|
|
88
|
+
|
|
89
|
+
# Messages
|
|
90
|
+
"StatusMessage",
|
|
91
|
+
"EventMessage",
|
|
92
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from labbench_comm.devices.cpar.instruction_codec import InstructionCodec
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CPARplusCodec:
|
|
6
|
+
class PressureType(Enum):
|
|
7
|
+
SUPPLY_PRESSURE = 0
|
|
8
|
+
STIMULATING_PRESSURE = 1
|
|
9
|
+
|
|
10
|
+
MAX_SUPPLY_PRESSURE = 1000.0
|
|
11
|
+
MAX_SCORE = 10.0
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def time_to_rate(time: float) -> int:
|
|
15
|
+
return round(time * InstructionCodec.UPDATE_RATE)
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def binary_to_pressure(
|
|
19
|
+
value: int,
|
|
20
|
+
pressure_type: "CPARplusCodec.PressureType" = PressureType.STIMULATING_PRESSURE,
|
|
21
|
+
) -> float:
|
|
22
|
+
if pressure_type is CPARplusCodec.PressureType.SUPPLY_PRESSURE:
|
|
23
|
+
return CPARplusCodec.MAX_SUPPLY_PRESSURE * value / 4095
|
|
24
|
+
return InstructionCodec.MAX_PRESSURE * value / 4095
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def binary_to_score(value: int) -> float:
|
|
28
|
+
return CPARplusCodec.MAX_SCORE * value / 255
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def pressure_to_binary(pressure: float) -> int:
|
|
32
|
+
return InstructionCodec._pressure_to_binary(pressure)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def delta_pressure_to_binary(delta: float) -> int:
|
|
36
|
+
return InstructionCodec._pressure_to_binary(
|
|
37
|
+
delta / InstructionCodec.UPDATE_RATE
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def count_to_time(count: int) -> float:
|
|
42
|
+
return count / InstructionCodec.UPDATE_RATE
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def time_to_count(time: float) -> int:
|
|
46
|
+
return int((time * InstructionCodec.UPDATE_RATE) + 0.9999)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def get_time(samples: int) -> float:
|
|
50
|
+
return CPARplusCodec.count_to_time(samples)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from labbench_comm.protocols.device import Device
|
|
7
|
+
from labbench_comm.protocols.device_function import DeviceFunction
|
|
8
|
+
from labbench_comm.protocols.functions.device_identification import DeviceIdentification
|
|
9
|
+
from labbench_comm.protocols.functions.ping import Ping
|
|
10
|
+
from labbench_comm.devices.cpar.messages import (
|
|
11
|
+
StatusMessage,
|
|
12
|
+
EventMessage,
|
|
13
|
+
)
|
|
14
|
+
from labbench_comm.devices.cpar.definitions import (
|
|
15
|
+
DeviceState,
|
|
16
|
+
EcpError,
|
|
17
|
+
)
|
|
18
|
+
from labbench_comm.devices.cpar.instruction_codec import InstructionCodec
|
|
19
|
+
from labbench_comm.protocols.manufacturer import Manufacturer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CPARplusCentral(Device):
|
|
23
|
+
# ------------------------------------------------------------------
|
|
24
|
+
# Construction
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def __init__(self, bus) -> None:
|
|
28
|
+
super().__init__(bus)
|
|
29
|
+
|
|
30
|
+
self.baudrate = 38400
|
|
31
|
+
self.retries = 3
|
|
32
|
+
|
|
33
|
+
# --- CPAR+ messages ---
|
|
34
|
+
self.add_message(EventMessage())
|
|
35
|
+
self.add_message(StatusMessage())
|
|
36
|
+
|
|
37
|
+
# --- Runtime state ---
|
|
38
|
+
self.state: Optional[DeviceState] = None
|
|
39
|
+
|
|
40
|
+
self.actual_pressure_01 = 0.0
|
|
41
|
+
self.target_pressure_01 = 0.0
|
|
42
|
+
self.final_pressure_01 = 0.0
|
|
43
|
+
|
|
44
|
+
self.actual_pressure_02 = 0.0
|
|
45
|
+
self.target_pressure_02 = 0.0
|
|
46
|
+
self.final_pressure_02 = 0.0
|
|
47
|
+
|
|
48
|
+
self.response_connected = False
|
|
49
|
+
self.vas_is_low = False
|
|
50
|
+
self.vas_score = 0.0
|
|
51
|
+
self.final_vas_score = 0.0
|
|
52
|
+
|
|
53
|
+
self.status_received = []
|
|
54
|
+
self.event_received = []
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# Error handling
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def get_peripheral_error_string(self, error_code: int) -> str:
|
|
61
|
+
try:
|
|
62
|
+
return str(EcpError(error_code))
|
|
63
|
+
except ValueError:
|
|
64
|
+
return f"Unknown CPAR error ({error_code})"
|
|
65
|
+
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
# Message handlers
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def on_status_message(self, message: StatusMessage) -> None:
|
|
71
|
+
if message is None:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
self.actual_pressure_01 = message.actual_pressure_01
|
|
75
|
+
self.target_pressure_01 = message.target_pressure_01
|
|
76
|
+
self.final_pressure_01 = message.final_pressure_01
|
|
77
|
+
|
|
78
|
+
self.actual_pressure_02 = message.actual_pressure_02
|
|
79
|
+
self.target_pressure_02 = message.target_pressure_02
|
|
80
|
+
self.final_pressure_02 = message.final_pressure_02
|
|
81
|
+
|
|
82
|
+
self.response_connected = message.vas_connected
|
|
83
|
+
self.vas_is_low = message.vas_is_low
|
|
84
|
+
self.vas_score = message.vas_score
|
|
85
|
+
self.final_vas_score = message.final_vas_score
|
|
86
|
+
|
|
87
|
+
self.state = message.system_state
|
|
88
|
+
|
|
89
|
+
for cb in self.status_received:
|
|
90
|
+
cb(self, message)
|
|
91
|
+
|
|
92
|
+
def on_event_message(self, message: EventMessage) -> None:
|
|
93
|
+
if message is None:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
for cb in self.event_received:
|
|
97
|
+
cb(self, message)
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# Compatibility
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def is_compatible(self, function: DeviceFunction) -> bool:
|
|
104
|
+
if not isinstance(function, DeviceIdentification):
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
function.manufacturer_id == Manufacturer.InventorsWay
|
|
109
|
+
and function.device_id == 4
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# Representation
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def __str__(self) -> str:
|
|
117
|
+
return "CPAR+ Device"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DeviceChannelID(IntEnum):
|
|
5
|
+
CH01 = 0
|
|
6
|
+
CH02 = 1
|
|
7
|
+
NONE = 2
|
|
8
|
+
|
|
9
|
+
class DeviceState(IntEnum):
|
|
10
|
+
STATE_NOT_CONNECTED = 0
|
|
11
|
+
STATE_IDLE = 1
|
|
12
|
+
STATE_STIMULATING = 2
|
|
13
|
+
STATE_EMERGENCY = 3
|
|
14
|
+
STATE_PENDING = 4
|
|
15
|
+
|
|
16
|
+
class EcpError(IntEnum):
|
|
17
|
+
NO_ERROR = 0
|
|
18
|
+
UNKNOWN_FUNCTION_ERR = 1
|
|
19
|
+
INVALID_REQUEST_LENGTH_ERR = 2
|
|
20
|
+
SYSTEM_NOT_IDLE_ERR = 3
|
|
21
|
+
INCORRECT_CHANNEL_ERR = 4
|
|
22
|
+
NO_REPONSE_DEVICE_CONNECTED_ERR = 5
|
|
23
|
+
RATING_IS_NOT_ZERO_ERR = 6
|
|
24
|
+
NO_SUPPLY_PRESSURE_ERR = 7
|
|
25
|
+
NO_POWER_ERR = 8
|
|
26
|
+
INVALID_START_CONFIGURATION = 9
|
|
27
|
+
STOP_NOT_POSSIBLE_ERR = 10
|
|
28
|
+
INVALID_EVENT_ADDRESS_ERR = 11
|
|
29
|
+
MAX_WAVEFORM_DURATION_EXCEEDED_ERR = 12
|
|
30
|
+
INVALID_ADDRESS_ERR = 13
|
|
31
|
+
COULD_NOT_SAVE_TO_EEPROM_ERR = 14
|
|
32
|
+
INVALID_CHANNEL_ID_ERR = 15
|
|
33
|
+
CANNOT_SET_BUILT_EVENT_ERR = 16
|
|
34
|
+
INVALID_RUN_LEVEL_ERR = 17
|
|
35
|
+
|
|
36
|
+
class EventID(IntEnum):
|
|
37
|
+
EVT_NO_EVENT = 0
|
|
38
|
+
EVT_STATUS_UPDATE = 1
|
|
39
|
+
EVT_START_STIMULATION = 2
|
|
40
|
+
EVT_STOP_STIMULATION = 3
|
|
41
|
+
EVT_STOP_BUTTON01_PRESSED = 4
|
|
42
|
+
EVT_STOP_BUTTON01_RELEASED = 5
|
|
43
|
+
EVT_STOP_BUTTON02_PRESSED = 6
|
|
44
|
+
EVT_STOP_BUTTON02_RELEASED = 7
|
|
45
|
+
EVT_MAX_VAS_SCORED = 8
|
|
46
|
+
EVT_RESPONSE_DISCONNECTED = 9
|
|
47
|
+
EVT_RESPONSE_CONNECTED = 10
|
|
48
|
+
EVT_EMERGENCY_ACTIVATED = 11
|
|
49
|
+
EVT_EMERGENCY_DEACTIVATED = 12
|
|
50
|
+
EVT_WAVEFORMS_COMPLETED = 13
|
|
51
|
+
EVT_TIMELIMIT_EXCEEDED = 14
|
|
52
|
+
EVT_COMM_WATCHDOG_TRIGGERED = 15
|
|
53
|
+
EVT_CUFF01_OUT_OF_COMPLIANCE = 16
|
|
54
|
+
EVT_CUFF02_OUT_OF_COMPLIANCE = 17
|
|
55
|
+
EVT_SUPPLY_PRESSURE_LOW = 18
|
|
56
|
+
EVT_12V_POWER_OFF = 19
|
|
57
|
+
EVT_12V_POWER_ON = 20
|
|
58
|
+
EVT_AIR_LEAK = 21
|
|
59
|
+
EVT_TRIG_IN = 22
|
|
60
|
+
|
|
61
|
+
class OperatingMode(IntEnum):
|
|
62
|
+
RESPONSE_ENABLED = 0
|
|
63
|
+
RESPONSE_DISABLED = 1
|
|
64
|
+
|
|
65
|
+
class StopCondition(IntEnum):
|
|
66
|
+
STOPCOND_NO_CONDITION = 0
|
|
67
|
+
STOPCOND_STOP_BUTTON_PRESSED = 1
|
|
68
|
+
STOPCOND_MAXIMAL_VAS_SCORED = 2
|
|
69
|
+
STOPCOND_STIMULATION_COMPLETED = 3
|
|
70
|
+
STOPCOND_MAXIMAL_TIME_EXCEEDED = 4
|
|
71
|
+
STOPCOND_VASMETER_DISCONNECTED = 5
|
|
72
|
+
STOPCOND_EMERGENCY_STOP_ACTIVATED = 6
|
|
73
|
+
STOPCOND_CONTROL_SOFTWARE = 7
|
|
74
|
+
STOPCOND_OUT_OF_COMPLIANCE = 8
|
|
75
|
+
STOPCOND_COMM_WATCHDOG = 9
|
|
76
|
+
STOPCOND_12V_POWER_OFF = 10
|
|
77
|
+
STOPCOND_SUPPLY_PRESSURE_LOW = 11
|
|
78
|
+
|
|
79
|
+
class StopCriterion(IntEnum):
|
|
80
|
+
STOP_CRITERION_ON_BUTTON_VAS = 0
|
|
81
|
+
STOP_CRITERION_ON_BUTTON_PRESSED = 1
|
|
82
|
+
STOP_CRITERION_ON_BUTTON_RELEASED = 2
|
|
83
|
+
|
|
84
|
+
class WaveformInstructionType(IntEnum):
|
|
85
|
+
TRIG = 0x00
|
|
86
|
+
INC = 0x01
|
|
87
|
+
DEC = 0x02
|
|
88
|
+
STEP = 0x03
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .set_waveform_program import SetWaveformProgram
|
|
2
|
+
from .start_stimulation import StartStimulation
|
|
3
|
+
from .stop_stimulation import StopStimulation
|
|
4
|
+
from .set_operating_mode import SetOperatingMode
|
|
5
|
+
from .clear_waveform_programs import ClearWaveformPrograms
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SetWaveformProgram",
|
|
9
|
+
"StartStimulation",
|
|
10
|
+
"StopStimulation",
|
|
11
|
+
"SetOperatingMode",
|
|
12
|
+
"ClearWaveformPrograms",
|
|
13
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from labbench_comm.protocols.device_function import DeviceFunction
|
|
2
|
+
from labbench_comm.protocols.function_dispatcher import FunctionDispatcher
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ClearWaveformPrograms(DeviceFunction):
|
|
6
|
+
@property
|
|
7
|
+
def code(self) -> int:
|
|
8
|
+
return 0x21
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
super().__init__(request_length=0, response_length=0)
|
|
12
|
+
|
|
13
|
+
def create_dispatcher(self) -> FunctionDispatcher:
|
|
14
|
+
return FunctionDispatcher(self.code, lambda: ClearWaveformPrograms())
|
|
15
|
+
|
|
16
|
+
def dispatch(self, listener):
|
|
17
|
+
return listener.accept(self)
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
return "[0x21] Clear Waveform Programs"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from labbench_comm.protocols.device_function import DeviceFunction
|
|
2
|
+
from labbench_comm.protocols.function_dispatcher import FunctionDispatcher
|
|
3
|
+
from labbench_comm.devices.cpar.definitions import OperatingMode
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SetOperatingMode(DeviceFunction):
|
|
7
|
+
@property
|
|
8
|
+
def code(self) -> int:
|
|
9
|
+
return 0x20
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
super().__init__(request_length=1, response_length=0)
|
|
13
|
+
self.mode = OperatingMode.RESPONSE_ENABLED
|
|
14
|
+
|
|
15
|
+
# ------------------------------------------------------------------
|
|
16
|
+
# Dispatcher
|
|
17
|
+
# ------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
def create_dispatcher(self) -> FunctionDispatcher:
|
|
20
|
+
return FunctionDispatcher(self.code, lambda: SetOperatingMode())
|
|
21
|
+
|
|
22
|
+
def dispatch(self, listener):
|
|
23
|
+
return listener.accept(self)
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
# Properties
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def mode(self) -> OperatingMode:
|
|
31
|
+
return OperatingMode(self.request.get_byte(0))
|
|
32
|
+
|
|
33
|
+
@mode.setter
|
|
34
|
+
def mode(self, value: OperatingMode) -> None:
|
|
35
|
+
self.request.insert_byte(0, int(value))
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
# Representation
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return "[0x20] Set Operating Mode"
|