syndesi 0.4.0__tar.gz → 0.4.2__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.
- syndesi-0.4.2/PKG-INFO +96 -0
- syndesi-0.4.2/README.md +74 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/pyproject.toml +1 -3
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/adapter.py +91 -158
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/auto.py +1 -1
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/adapter_backend.py +54 -37
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/adapter_session.py +26 -27
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/descriptors.py +3 -2
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/ip_backend.py +1 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/serialport_backend.py +9 -10
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/stop_condition_backend.py +47 -26
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/visa_backend.py +7 -7
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/ip.py +6 -10
- syndesi-0.4.2/syndesi/adapters/stop_condition.py +90 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/timeout.py +3 -30
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/visa.py +2 -2
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/backend_status.py +7 -9
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/console.py +1 -54
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/shell.py +1 -14
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/shell_tools.py +0 -5
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/delimited.py +17 -37
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/modbus.py +17 -14
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/raw.py +20 -16
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/scpi.py +18 -15
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/scripts/syndesi.py +1 -3
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/backend_api.py +5 -38
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/backend_logger.py +0 -1
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/errors.py +4 -5
- syndesi-0.4.2/syndesi/tools/internal.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/log.py +0 -88
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/types.py +0 -44
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/version.py +1 -1
- syndesi-0.4.2/syndesi.egg-info/PKG-INFO +96 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/SOURCES.txt +1 -0
- syndesi-0.4.0/PKG-INFO +0 -123
- syndesi-0.4.0/README.md +0 -101
- syndesi-0.4.0/syndesi/adapters/stop_condition.py +0 -163
- syndesi-0.4.0/syndesi.egg-info/PKG-INFO +0 -123
- {syndesi-0.4.0 → syndesi-0.4.2}/LICENSE +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/setup.cfg +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/__init__.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/__main__.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/__init__.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/__init__.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/adapter_manager.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/backend.py +0 -0
- syndesi-0.4.0/syndesi/cli/__init__.py → syndesi-0.4.2/syndesi/adapters/backend/backend_status.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/backend_tools.py +1 -1
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/timed_queue.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/timeout.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/ip_server.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/serialport.py +0 -0
- {syndesi-0.4.0/syndesi/protocols → syndesi-0.4.2/syndesi/cli}/__init__.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/backend_console.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/backend_wrapper.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/terminal.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/terminal_apps.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/terminal_tools.py +0 -0
- {syndesi-0.4.0/syndesi/scripts → syndesi-0.4.2/syndesi/protocols}/__init__.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/protocol.py +0 -0
- {syndesi-0.4.0/syndesi/tools → syndesi-0.4.2/syndesi/scripts}/__init__.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/scripts/syndesi_backend.py +0 -0
- /syndesi-0.4.0/syndesi/tools/internal.py → /syndesi-0.4.2/syndesi/tools/__init__.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/exceptions.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/log_settings.py +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/dependency_links.txt +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/entry_points.txt +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/requires.txt +0 -0
- {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/top_level.txt +0 -0
syndesi-0.4.2/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: syndesi
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: Syndesi
|
|
5
|
+
Author-email: Sébastien Deriaz <sebastien.deriaz1@gmail.com>
|
|
6
|
+
License: GPL
|
|
7
|
+
Keywords: python,syndesi,interface,ethernet,serial,visa
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Education
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: Unix
|
|
12
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: prompt_toolkit
|
|
18
|
+
Requires-Dist: pyserial
|
|
19
|
+
Requires-Dist: rich
|
|
20
|
+
Requires-Dist: platformdirs
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Syndesi Python Implementation
|
|
24
|
+
|
|
25
|
+
Syndesi description is available [here](https://github.com/syndesi-project/Syndesi/README.md)
|
|
26
|
+
|
|
27
|
+
Syndesi is a modular Python framework designed to streamline communication and control of a wide range of electronic instruments and devices. By providing a unified abstraction layer for adapters, protocols, and device drivers, Syndesi enables seamless integration with test equipment such as multimeters, oscilloscopes, power supplies, UART/USB devices, and more. Its flexible architecture supports both high-level and low-level operations, making it ideal for automation, data acquisition, and custom device interfacing in laboratory, industrial, and research environments.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
The syndesi Python package can be installed through pip
|
|
32
|
+
|
|
33
|
+
``pip install syndesi``
|
|
34
|
+
|
|
35
|
+
The package can also be installed locally by cloning this repository
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/syndesi-project/Syndesi
|
|
39
|
+
cd Syndesi
|
|
40
|
+
pip install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
The user can work with any of the three following layers :
|
|
46
|
+
|
|
47
|
+
- Adapters : low-level communication (IP, UART, ...)
|
|
48
|
+
- Protocols : Encapsulated protocols (Delimited, Modbus, ...)
|
|
49
|
+
- Drivers : Device or application specific commands
|
|
50
|
+
|
|
51
|
+
### Adapters
|
|
52
|
+
|
|
53
|
+
The adapter allows the user to read and write raw data through IP, serial and VISA
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from syndesi import IP
|
|
57
|
+
|
|
58
|
+
my_adapter = IP('192.168.1.12', port=5025)
|
|
59
|
+
|
|
60
|
+
my_adapter.write(b'ping\n')
|
|
61
|
+
|
|
62
|
+
my_adapter.read() # -> b'pong'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from syndesi import SerialPort
|
|
67
|
+
|
|
68
|
+
arduino = SerialPort('/dev/ttyUSB0', baudrate=115200) # COMx on Windows
|
|
69
|
+
arduino.query(b'get_temperature\n') # -> 20.5
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Protocols
|
|
73
|
+
|
|
74
|
+
Protocols encapsulate and format data
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from syndesi import IP, Delimited
|
|
78
|
+
|
|
79
|
+
my_server = Delimited(IP('test.server.local', port=1234))
|
|
80
|
+
|
|
81
|
+
my_server.query('Hello world\n') # -> Hello world (\n is removed by Delimited)
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Drivers
|
|
86
|
+
|
|
87
|
+
A driver only requires an adapter, the protocol (if used) is instanciated internally
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from syndesi_drivers.instruments.mutlimeters.siglent.SDM3055 import SDM3055
|
|
91
|
+
from syndesi.adapters import IP
|
|
92
|
+
|
|
93
|
+
mm = SDM3055(IP("192.168.1.123"))
|
|
94
|
+
|
|
95
|
+
voltage = mm.measure_dc_voltage()
|
|
96
|
+
```
|
syndesi-0.4.2/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Syndesi Python Implementation
|
|
2
|
+
|
|
3
|
+
Syndesi description is available [here](https://github.com/syndesi-project/Syndesi/README.md)
|
|
4
|
+
|
|
5
|
+
Syndesi is a modular Python framework designed to streamline communication and control of a wide range of electronic instruments and devices. By providing a unified abstraction layer for adapters, protocols, and device drivers, Syndesi enables seamless integration with test equipment such as multimeters, oscilloscopes, power supplies, UART/USB devices, and more. Its flexible architecture supports both high-level and low-level operations, making it ideal for automation, data acquisition, and custom device interfacing in laboratory, industrial, and research environments.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
The syndesi Python package can be installed through pip
|
|
10
|
+
|
|
11
|
+
``pip install syndesi``
|
|
12
|
+
|
|
13
|
+
The package can also be installed locally by cloning this repository
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/syndesi-project/Syndesi
|
|
17
|
+
cd Syndesi
|
|
18
|
+
pip install .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
The user can work with any of the three following layers :
|
|
24
|
+
|
|
25
|
+
- Adapters : low-level communication (IP, UART, ...)
|
|
26
|
+
- Protocols : Encapsulated protocols (Delimited, Modbus, ...)
|
|
27
|
+
- Drivers : Device or application specific commands
|
|
28
|
+
|
|
29
|
+
### Adapters
|
|
30
|
+
|
|
31
|
+
The adapter allows the user to read and write raw data through IP, serial and VISA
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from syndesi import IP
|
|
35
|
+
|
|
36
|
+
my_adapter = IP('192.168.1.12', port=5025)
|
|
37
|
+
|
|
38
|
+
my_adapter.write(b'ping\n')
|
|
39
|
+
|
|
40
|
+
my_adapter.read() # -> b'pong'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from syndesi import SerialPort
|
|
45
|
+
|
|
46
|
+
arduino = SerialPort('/dev/ttyUSB0', baudrate=115200) # COMx on Windows
|
|
47
|
+
arduino.query(b'get_temperature\n') # -> 20.5
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Protocols
|
|
51
|
+
|
|
52
|
+
Protocols encapsulate and format data
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from syndesi import IP, Delimited
|
|
56
|
+
|
|
57
|
+
my_server = Delimited(IP('test.server.local', port=1234))
|
|
58
|
+
|
|
59
|
+
my_server.query('Hello world\n') # -> Hello world (\n is removed by Delimited)
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Drivers
|
|
64
|
+
|
|
65
|
+
A driver only requires an adapter, the protocol (if used) is instanciated internally
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from syndesi_drivers.instruments.mutlimeters.siglent.SDM3055 import SDM3055
|
|
69
|
+
from syndesi.adapters import IP
|
|
70
|
+
|
|
71
|
+
mm = SDM3055(IP("192.168.1.123"))
|
|
72
|
+
|
|
73
|
+
voltage = mm.measure_dc_voltage()
|
|
74
|
+
```
|
|
@@ -11,7 +11,7 @@ dynamic = ["version"]
|
|
|
11
11
|
description = "Syndesi"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.10"
|
|
14
|
-
license = { text = "GPL" }
|
|
14
|
+
license = { text = "GPL" }
|
|
15
15
|
authors = [{ name = "Sébastien Deriaz", email = "sebastien.deriaz1@gmail.com" }]
|
|
16
16
|
keywords = ["python", "syndesi", "interface", "ethernet", "serial", "visa"]
|
|
17
17
|
classifiers = [
|
|
@@ -42,8 +42,6 @@ include = ["syndesi*"]
|
|
|
42
42
|
[tool.ruff]
|
|
43
43
|
target-version = "py311"
|
|
44
44
|
line-length = 100
|
|
45
|
-
select = ["D","E","F","I","B"] # include pydocstyle rules ("D")
|
|
46
|
-
#ignore = ["D203","D213"] # pick your preferences
|
|
47
45
|
|
|
48
46
|
[tool.ruff.lint]
|
|
49
47
|
select = ["E", "F", "W", "I", "UP", "B"]
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
# An adapter is meant to work with bytes objects but it can accept strings.
|
|
16
16
|
# Strings will automatically be converted to bytes using utf-8 encoding
|
|
17
17
|
|
|
18
|
-
from enum import Enum
|
|
19
18
|
import logging
|
|
19
|
+
import os
|
|
20
20
|
import queue
|
|
21
21
|
import subprocess
|
|
22
22
|
import sys
|
|
@@ -25,42 +25,44 @@ import time
|
|
|
25
25
|
import weakref
|
|
26
26
|
from abc import ABC, abstractmethod
|
|
27
27
|
from collections.abc import Callable
|
|
28
|
+
from enum import Enum
|
|
28
29
|
from multiprocessing.connection import Client, Connection
|
|
29
30
|
from types import EllipsisType
|
|
30
31
|
from typing import Any
|
|
31
|
-
import os
|
|
32
32
|
|
|
33
|
-
from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
34
33
|
from syndesi.tools.types import NumberLike, is_number
|
|
35
34
|
|
|
36
35
|
from ..tools.backend_api import (
|
|
37
36
|
BACKEND_PORT,
|
|
37
|
+
EXTRA_BUFFER_RESPONSE_TIME,
|
|
38
38
|
Action,
|
|
39
39
|
BackendResponse,
|
|
40
|
+
Fragment,
|
|
40
41
|
default_host,
|
|
41
42
|
raise_if_error,
|
|
42
|
-
EXTRA_BUFFER_RESPONSE_TIME
|
|
43
43
|
)
|
|
44
44
|
from ..tools.log_settings import LoggerAlias
|
|
45
45
|
from .backend.adapter_backend import (
|
|
46
46
|
AdapterDisconnected,
|
|
47
|
-
AdapterResponseTimeout,
|
|
48
47
|
AdapterReadPayload,
|
|
48
|
+
AdapterResponseTimeout,
|
|
49
49
|
AdapterSignal,
|
|
50
50
|
)
|
|
51
|
+
from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
51
52
|
from .backend.descriptors import Descriptor
|
|
52
|
-
from .stop_condition import
|
|
53
|
+
from .stop_condition import Continuation, StopCondition, StopConditionType
|
|
53
54
|
from .timeout import Timeout, TimeoutAction, any_to_timeout
|
|
54
55
|
|
|
55
56
|
DEFAULT_STOP_CONDITION = Continuation(time=0.1)
|
|
56
57
|
|
|
57
|
-
DEFAULT_TIMEOUT = Timeout(response=5, action=
|
|
58
|
+
DEFAULT_TIMEOUT = Timeout(response=5, action="error")
|
|
58
59
|
|
|
59
60
|
# Maximum time to let the backend start
|
|
60
61
|
START_TIMEOUT = 2
|
|
61
62
|
# Time to shutdown the backend
|
|
62
63
|
SHUTDOWN_DELAY = 2
|
|
63
64
|
|
|
65
|
+
|
|
64
66
|
class SignalQueue(queue.Queue[AdapterSignal]):
|
|
65
67
|
def __init__(self) -> None:
|
|
66
68
|
self._read_payload_counter = 0
|
|
@@ -69,12 +71,12 @@ class SignalQueue(queue.Queue[AdapterSignal]):
|
|
|
69
71
|
def has_read_payload(self) -> bool:
|
|
70
72
|
return self._read_payload_counter > 0
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
def put(
|
|
75
|
+
self, signal: AdapterSignal, block: bool = True, timeout: float | None = None
|
|
76
|
+
) -> None:
|
|
74
77
|
if isinstance(signal, AdapterReadPayload):
|
|
75
78
|
self._read_payload_counter += 1
|
|
76
79
|
return super().put(signal, block, timeout)
|
|
77
|
-
|
|
78
80
|
|
|
79
81
|
def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
|
|
80
82
|
signal = super().get(block, timeout)
|
|
@@ -93,19 +95,20 @@ def is_backend_running(address: str, port: int) -> bool:
|
|
|
93
95
|
conn.close()
|
|
94
96
|
return True
|
|
95
97
|
|
|
98
|
+
|
|
96
99
|
def start_backend(port: int | None = None) -> None:
|
|
97
100
|
arguments = [
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
stdin
|
|
101
|
+
sys.executable,
|
|
102
|
+
"-m",
|
|
103
|
+
"syndesi.adapters.backend.backend",
|
|
104
|
+
"-s",
|
|
105
|
+
str(SHUTDOWN_DELAY),
|
|
106
|
+
"-q",
|
|
107
|
+
"-p",
|
|
108
|
+
str(BACKEND_PORT if port is None else port),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
stdin = subprocess.DEVNULL
|
|
109
112
|
stdout = subprocess.DEVNULL
|
|
110
113
|
stderr = subprocess.DEVNULL
|
|
111
114
|
|
|
@@ -121,8 +124,8 @@ def start_backend(port: int | None = None) -> None:
|
|
|
121
124
|
|
|
122
125
|
else:
|
|
123
126
|
# Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
|
|
124
|
-
CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
125
|
-
DETACHED_PROCESS = 0x00000008
|
|
127
|
+
CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
|
|
128
|
+
DETACHED_PROCESS = 0x00000008 # not exposed by subprocess on all Pythons
|
|
126
129
|
# Optional: CREATE_NO_WINDOW (no window even for console apps)
|
|
127
130
|
CREATE_NO_WINDOW = 0x08000000
|
|
128
131
|
|
|
@@ -137,9 +140,11 @@ def start_backend(port: int | None = None) -> None:
|
|
|
137
140
|
close_fds=True,
|
|
138
141
|
)
|
|
139
142
|
|
|
143
|
+
|
|
140
144
|
class ReadScope(Enum):
|
|
141
|
-
NEXT =
|
|
142
|
-
BUFFERED =
|
|
145
|
+
NEXT = "next"
|
|
146
|
+
BUFFERED = "buffered"
|
|
147
|
+
|
|
143
148
|
|
|
144
149
|
class Adapter(ABC):
|
|
145
150
|
def __init__(
|
|
@@ -214,7 +219,7 @@ class Adapter(ABC):
|
|
|
214
219
|
elif isinstance(stop_conditions, list):
|
|
215
220
|
self._stop_conditions = stop_conditions
|
|
216
221
|
else:
|
|
217
|
-
raise ValueError(
|
|
222
|
+
raise ValueError("Invalid stop_conditions")
|
|
218
223
|
|
|
219
224
|
# Set the timeout
|
|
220
225
|
self.is_default_timeout = False
|
|
@@ -334,7 +339,7 @@ class Adapter(ABC):
|
|
|
334
339
|
action = Action(response[0])
|
|
335
340
|
|
|
336
341
|
if action == Action.ADAPTER_SIGNAL:
|
|
337
|
-
|
|
342
|
+
# if is_event(action):
|
|
338
343
|
if len(response) <= 1:
|
|
339
344
|
raise RuntimeError(f"Invalid event response : {response}")
|
|
340
345
|
signal: AdapterSignal = response[1]
|
|
@@ -370,7 +375,9 @@ class Adapter(ABC):
|
|
|
370
375
|
self._logger.debug(f"Setting default timeout to {default_timeout}")
|
|
371
376
|
self._timeout = default_timeout
|
|
372
377
|
|
|
373
|
-
def set_stop_conditions(
|
|
378
|
+
def set_stop_conditions(
|
|
379
|
+
self, stop_conditions: StopCondition | None | list[StopCondition]
|
|
380
|
+
) -> None:
|
|
374
381
|
"""
|
|
375
382
|
Overwrite the stop-condition
|
|
376
383
|
|
|
@@ -385,7 +392,7 @@ class Adapter(ABC):
|
|
|
385
392
|
elif stop_conditions is None:
|
|
386
393
|
self._stop_conditions = []
|
|
387
394
|
|
|
388
|
-
self._make_backend_request(Action.
|
|
395
|
+
self._make_backend_request(Action.SET_STOP_CONDITIONs, self._stop_conditions)
|
|
389
396
|
|
|
390
397
|
def set_default_stop_condition(self, stop_condition: StopCondition) -> None:
|
|
391
398
|
"""
|
|
@@ -410,7 +417,6 @@ class Adapter(ABC):
|
|
|
410
417
|
self._signal_queue.get(block=False)
|
|
411
418
|
except queue.Empty:
|
|
412
419
|
break
|
|
413
|
-
|
|
414
420
|
|
|
415
421
|
def previous_read_buffer_empty(self) -> bool:
|
|
416
422
|
"""
|
|
@@ -462,9 +468,9 @@ class Adapter(ABC):
|
|
|
462
468
|
def read_detailed(
|
|
463
469
|
self,
|
|
464
470
|
timeout: Timeout | EllipsisType | None = ...,
|
|
465
|
-
|
|
466
|
-
scope
|
|
467
|
-
) -> AdapterReadPayload
|
|
471
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
472
|
+
scope: str = ReadScope.BUFFERED.value,
|
|
473
|
+
) -> AdapterReadPayload:
|
|
468
474
|
"""
|
|
469
475
|
Read data from the device
|
|
470
476
|
|
|
@@ -481,9 +487,22 @@ class Adapter(ABC):
|
|
|
481
487
|
data : bytes
|
|
482
488
|
signal : AdapterReadPayload
|
|
483
489
|
"""
|
|
484
|
-
t = time.time()
|
|
485
490
|
_scope = ReadScope(scope)
|
|
486
491
|
output_signal = None
|
|
492
|
+
read_timeout = None
|
|
493
|
+
|
|
494
|
+
if timeout is ...:
|
|
495
|
+
read_timeout = self._timeout
|
|
496
|
+
else:
|
|
497
|
+
read_timeout = any_to_timeout(timeout)
|
|
498
|
+
|
|
499
|
+
if read_timeout is None:
|
|
500
|
+
raise RuntimeError("Cannot read without setting a timeout")
|
|
501
|
+
|
|
502
|
+
if stop_conditions is not ...:
|
|
503
|
+
if isinstance(stop_conditions, StopCondition):
|
|
504
|
+
stop_conditions = [stop_conditions]
|
|
505
|
+
self._make_backend_request(Action.SET_STOP_CONDITIONs, stop_conditions)
|
|
487
506
|
|
|
488
507
|
# First, we check if data is in the buffer and if the scope if set to BUFFERED
|
|
489
508
|
while _scope == ReadScope.BUFFERED and self._signal_queue.has_read_payload():
|
|
@@ -495,42 +514,37 @@ class Adapter(ABC):
|
|
|
495
514
|
else:
|
|
496
515
|
# Nothing was found, ask the backend with a START_READ request. The backend will
|
|
497
516
|
# respond at most after the response_time with either data or a RESPONSE_TIMEOUT
|
|
498
|
-
if timeout is ...:
|
|
499
|
-
read_timeout = self._timeout
|
|
500
|
-
else:
|
|
501
|
-
read_timeout = any_to_timeout(timeout)
|
|
502
517
|
|
|
503
|
-
if
|
|
504
|
-
|
|
505
|
-
raise RuntimeError("Timeout needs to be initialized")
|
|
518
|
+
if not read_timeout.is_initialized():
|
|
519
|
+
raise RuntimeError("Timeout needs to be initialized")
|
|
506
520
|
|
|
507
|
-
|
|
521
|
+
_response = read_timeout.response()
|
|
508
522
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if _response is None:
|
|
513
|
-
# Wait indefinitely
|
|
514
|
-
read_stop_timestamp = None
|
|
515
|
-
else:
|
|
516
|
-
# Wait for the response time + a bit more
|
|
517
|
-
read_stop_timestamp = read_init_time + _response
|
|
523
|
+
read_init_time = time.time()
|
|
524
|
+
start_read_id = self._make_backend_request(Action.START_READ, _response)[0]
|
|
518
525
|
|
|
526
|
+
if _response is None:
|
|
527
|
+
# Wait indefinitely
|
|
528
|
+
read_stop_timestamp = None
|
|
519
529
|
else:
|
|
520
|
-
|
|
521
|
-
|
|
530
|
+
# Wait for the response time + a bit more
|
|
531
|
+
read_stop_timestamp = read_init_time + _response
|
|
522
532
|
|
|
523
|
-
|
|
524
533
|
while True:
|
|
525
534
|
try:
|
|
526
535
|
if read_stop_timestamp is None:
|
|
527
536
|
queue_timeout = None
|
|
528
537
|
else:
|
|
529
|
-
queue_timeout = max(
|
|
538
|
+
queue_timeout = max(
|
|
539
|
+
0,
|
|
540
|
+
read_stop_timestamp
|
|
541
|
+
- time.time()
|
|
542
|
+
+ EXTRA_BUFFER_RESPONSE_TIME,
|
|
543
|
+
)
|
|
530
544
|
|
|
531
545
|
signal = self._signal_queue.get(timeout=queue_timeout)
|
|
532
|
-
except queue.Empty:
|
|
533
|
-
raise RuntimeError(
|
|
546
|
+
except queue.Empty as e:
|
|
547
|
+
raise RuntimeError("Failed to receive response from backend") from e
|
|
534
548
|
if isinstance(signal, AdapterReadPayload):
|
|
535
549
|
output_signal = signal
|
|
536
550
|
break
|
|
@@ -542,113 +556,35 @@ class Adapter(ABC):
|
|
|
542
556
|
break
|
|
543
557
|
# Otherwise ignore it
|
|
544
558
|
|
|
545
|
-
|
|
546
559
|
if output_signal is None:
|
|
547
|
-
# TODO : Make read_timeout always Timeout, never None ?
|
|
548
560
|
match read_timeout.action:
|
|
549
|
-
case TimeoutAction.
|
|
550
|
-
|
|
561
|
+
case TimeoutAction.RETURN_EMPTY:
|
|
562
|
+
t = time.time()
|
|
563
|
+
return AdapterReadPayload(
|
|
564
|
+
fragments=[Fragment(b"", t)],
|
|
565
|
+
stop_timestamp=t,
|
|
566
|
+
stop_condition_type=StopConditionType.TIMEOUT,
|
|
567
|
+
previous_read_buffer_used=False,
|
|
568
|
+
response_timestamp=None,
|
|
569
|
+
response_delay=None,
|
|
570
|
+
)
|
|
551
571
|
case TimeoutAction.ERROR:
|
|
552
572
|
raise TimeoutError(
|
|
553
573
|
f"No response received from device within {read_timeout.response()} seconds"
|
|
554
574
|
)
|
|
555
575
|
case _:
|
|
556
576
|
raise NotImplementedError()
|
|
557
|
-
|
|
577
|
+
|
|
558
578
|
else:
|
|
559
579
|
return output_signal
|
|
560
580
|
|
|
561
|
-
|
|
562
|
-
# Okay idea : Remove the start read and instead ask for the time of the backend.
|
|
563
|
-
# Then we read whatever payload comes from the backend and compare that to the time
|
|
564
|
-
# If it doesn't match our criteria, we trash it
|
|
565
|
-
# When waiting for the backend payload, we wait +0.5s so make sure we received everything
|
|
566
|
-
# This 0.5s could be changed if we're local or not by the way
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
# # First, ask for the backend time, this is the official start of the read
|
|
570
|
-
# backend_read_start_time = cast(NumberLike, self._make_backend_request(Action.GET_BACKEND_TIME)[0])
|
|
571
|
-
|
|
572
|
-
# If not timeout is specified, use the default one
|
|
573
|
-
|
|
574
|
-
# Calculate last_valid_timestamp, the limit at which a payload is not accepted anymore
|
|
575
|
-
# Calculate the queue timeout (time for a response + small delay)
|
|
576
|
-
#last_valid_timestamp = None
|
|
577
|
-
# queue_timeout_timestamp = None
|
|
578
|
-
# if read_timeout is not None:
|
|
579
|
-
# response_delay = read_timeout.response()
|
|
580
|
-
# else:
|
|
581
|
-
# response_delay = None
|
|
582
|
-
|
|
583
|
-
# if response_delay is not None:
|
|
584
|
-
# #last_valid_timestamp = backend_read_start_time + response_delay
|
|
585
|
-
# queue_timeout_timestamp = time.time() + response_delay + BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
586
|
-
|
|
587
|
-
# output_signal : AdapterReadPayload | None
|
|
588
|
-
# # This delay is given by the backend when a fragment is received. It basically says
|
|
589
|
-
# # "I've received something, wait at most x seconds before raising an error"
|
|
590
|
-
# read_init_end_delay : float | None = None
|
|
591
|
-
|
|
592
|
-
# # Ready to read payloads
|
|
593
|
-
# while True:
|
|
594
|
-
# if read_init_end_delay is not None:
|
|
595
|
-
# # Find the next timeout
|
|
596
|
-
# queue_timeout = read_init_end_delay + 0.1 # TODO : Make this clean, this is the delay to let data arrive to the frontend
|
|
597
|
-
# elif queue_timeout_timestamp is None:
|
|
598
|
-
# queue_timeout = None
|
|
599
|
-
# else:
|
|
600
|
-
# queue_timeout = queue_timeout_timestamp - time.time()
|
|
601
|
-
# if queue_timeout < 0:
|
|
602
|
-
# queue_timeout = 0
|
|
603
|
-
|
|
604
|
-
# try:
|
|
605
|
-
# response = self._signal_queue.peek(block=True, timeout=queue_timeout)
|
|
606
|
-
# signal = response[1]
|
|
607
|
-
|
|
608
|
-
# if isinstance(signal, AdapterReadPayload):
|
|
609
|
-
# if response_delay is not None and signal.response_timestamp - backend_read_start_time > response_delay:
|
|
610
|
-
# # This signal happened after the max response time, act as if a timeout occured
|
|
611
|
-
# # and do not pop it out of the queue
|
|
612
|
-
# # TODO : Make _timeout always Timeout, never None ?
|
|
613
|
-
# output_signal = None
|
|
614
|
-
# break
|
|
615
|
-
|
|
616
|
-
# if _scope == ReadScope.NEXT and signal.response_timestamp < backend_read_start_time:
|
|
617
|
-
# # The payload happened before the read start
|
|
618
|
-
# self._signal_queue.get()
|
|
619
|
-
# continue
|
|
620
|
-
|
|
621
|
-
# if response_delay is not None:
|
|
622
|
-
# if signal.response_timestamp - backend_read_start_time > response_delay:
|
|
623
|
-
# self._signal_queue.get()
|
|
624
|
-
# output_signal = None
|
|
625
|
-
# break
|
|
626
|
-
|
|
627
|
-
# # Other wise the payload is valid
|
|
628
|
-
# self._signal_queue.get()
|
|
629
|
-
# output_signal = signal
|
|
630
|
-
# break
|
|
631
|
-
# elif isinstance(signal, AdapterReadInit):
|
|
632
|
-
# read_init_end_delay = signal.end_delay
|
|
633
|
-
# self._signal_queue.get()
|
|
634
|
-
# elif isinstance(signal, AdapterDisconnected):
|
|
635
|
-
# self._signal_queue.get()
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
# except queue.Empty:
|
|
639
|
-
# output_signal = None
|
|
640
|
-
# break
|
|
641
|
-
|
|
642
581
|
def read(
|
|
643
582
|
self,
|
|
644
583
|
timeout: Timeout | EllipsisType | None = ...,
|
|
645
|
-
|
|
584
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
646
585
|
) -> bytes:
|
|
647
|
-
signal = self.read_detailed(timeout=timeout,
|
|
648
|
-
|
|
649
|
-
return None
|
|
650
|
-
else:
|
|
651
|
-
return signal.data()
|
|
586
|
+
signal = self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
|
|
587
|
+
return signal.data()
|
|
652
588
|
|
|
653
589
|
def _cleanup(self) -> None:
|
|
654
590
|
if self._init_ok and self.opened:
|
|
@@ -658,8 +594,8 @@ class Adapter(ABC):
|
|
|
658
594
|
self,
|
|
659
595
|
data: bytes | str,
|
|
660
596
|
timeout: Timeout | EllipsisType | None = ...,
|
|
661
|
-
|
|
662
|
-
) ->
|
|
597
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
598
|
+
) -> AdapterReadPayload:
|
|
663
599
|
"""
|
|
664
600
|
Shortcut function that combines
|
|
665
601
|
- flush_read
|
|
@@ -668,21 +604,18 @@ class Adapter(ABC):
|
|
|
668
604
|
"""
|
|
669
605
|
self.flushRead()
|
|
670
606
|
self.write(data)
|
|
671
|
-
return self.read_detailed(timeout=timeout,
|
|
607
|
+
return self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
|
|
672
608
|
|
|
673
609
|
def query(
|
|
674
610
|
self,
|
|
675
611
|
data: bytes | str,
|
|
676
612
|
timeout: Timeout | EllipsisType | None = ...,
|
|
677
|
-
|
|
613
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
678
614
|
) -> bytes:
|
|
679
615
|
signal = self.query_detailed(
|
|
680
|
-
data=data, timeout=timeout,
|
|
616
|
+
data=data, timeout=timeout, stop_conditions=stop_conditions
|
|
681
617
|
)
|
|
682
|
-
|
|
683
|
-
return None
|
|
684
|
-
else:
|
|
685
|
-
return signal.data()
|
|
618
|
+
return signal.data()
|
|
686
619
|
|
|
687
620
|
def set_event_callback(self, callback: Callable[[AdapterSignal], None]) -> None:
|
|
688
621
|
self.event_callback = callback
|
|
@@ -37,7 +37,7 @@ def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
|
|
|
37
37
|
return IP(
|
|
38
38
|
address=descriptor.address,
|
|
39
39
|
port=descriptor.port,
|
|
40
|
-
transport=descriptor.transport,
|
|
40
|
+
transport=descriptor.transport.value,
|
|
41
41
|
)
|
|
42
42
|
elif isinstance(descriptor, SerialPortDescriptor):
|
|
43
43
|
return SerialPort(port=descriptor.port, baudrate=descriptor.baudrate)
|