syndesi 0.4.2__tar.gz → 0.4.4__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/syndesi.egg-info → syndesi-0.4.4}/PKG-INFO +1 -1
- {syndesi-0.4.2 → syndesi-0.4.4}/pyproject.toml +6 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/__init__.py +16 -1
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/adapter.py +147 -72
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/auto.py +26 -15
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/adapter_backend.py +3 -3
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/adapter_session.py +8 -9
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/backend.py +7 -2
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/ip_backend.py +44 -41
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/serialport_backend.py +17 -12
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/stop_condition_backend.py +8 -5
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/visa_backend.py +3 -3
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/ip.py +30 -24
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/ip_server.py +9 -3
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/serialport.py +20 -7
- syndesi-0.4.4/syndesi/adapters/stop_condition.py +114 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/timeout.py +1 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/visa.py +11 -3
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/console.py +1 -1
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/shell.py +2 -2
- syndesi-0.4.4/syndesi/component.py +79 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/delimited.py +2 -2
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/modbus.py +7 -6
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/protocol.py +7 -1
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/raw.py +2 -2
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/scpi.py +1 -1
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/scripts/syndesi.py +1 -1
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/backend_api.py +8 -1
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/errors.py +24 -4
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/log.py +1 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/types.py +1 -0
- syndesi-0.4.4/syndesi/version.py +7 -0
- {syndesi-0.4.2 → syndesi-0.4.4/syndesi.egg-info}/PKG-INFO +1 -1
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi.egg-info/SOURCES.txt +1 -0
- syndesi-0.4.2/syndesi/adapters/stop_condition.py +0 -90
- syndesi-0.4.2/syndesi/version.py +0 -3
- {syndesi-0.4.2 → syndesi-0.4.4}/LICENSE +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/README.md +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/setup.cfg +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/__main__.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/__init__.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/__init__.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/adapter_manager.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/backend_status.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/backend_tools.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/descriptors.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/timed_queue.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/timeout.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/__init__.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/backend_console.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/backend_status.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/backend_wrapper.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/shell_tools.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/terminal.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/terminal_apps.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/terminal_tools.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/__init__.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/scripts/__init__.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/scripts/syndesi_backend.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/__init__.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/backend_logger.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/exceptions.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/internal.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/log_settings.py +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi.egg-info/dependency_links.txt +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi.egg-info/entry_points.txt +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi.egg-info/requires.txt +0 -0
- {syndesi-0.4.2 → syndesi-0.4.4}/syndesi.egg-info/top_level.txt +0 -0
|
@@ -69,3 +69,9 @@ exclude = ["^tests/fixtures/"]
|
|
|
69
69
|
skips = ["B101"]
|
|
70
70
|
exclude = ["tests"]
|
|
71
71
|
|
|
72
|
+
[tool.pylint]
|
|
73
|
+
# Disable too-many-arguments, this is the case for adapters/protocols/drivers
|
|
74
|
+
# All arguments are necessary and it would not make sense to group them / move them
|
|
75
|
+
# too-many-positional-arguments is kept however because it makes sense
|
|
76
|
+
# Also disable W1203 to allow f-strings in logging lines
|
|
77
|
+
disable = ["R0913", "W1203"]
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Syndesi module
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
from .adapters.ip import IP
|
|
2
6
|
from .adapters.serialport import SerialPort
|
|
7
|
+
from .adapters.timeout import Timeout
|
|
3
8
|
from .adapters.visa import Visa
|
|
4
9
|
from .protocols.delimited import Delimited
|
|
5
10
|
from .protocols.modbus import Modbus
|
|
@@ -7,4 +12,14 @@ from .protocols.raw import Raw
|
|
|
7
12
|
from .protocols.scpi import SCPI
|
|
8
13
|
from .tools.log import log
|
|
9
14
|
|
|
10
|
-
__all__ = [
|
|
15
|
+
__all__ = [
|
|
16
|
+
"IP",
|
|
17
|
+
"SerialPort",
|
|
18
|
+
"Visa",
|
|
19
|
+
"Delimited",
|
|
20
|
+
"Modbus",
|
|
21
|
+
"Raw",
|
|
22
|
+
"SCPI",
|
|
23
|
+
"log",
|
|
24
|
+
"Timeout",
|
|
25
|
+
]
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# File : adapters.py
|
|
2
2
|
# Author : Sébastien Deriaz
|
|
3
3
|
# License : GPL
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Adapters provide a common abstraction for the media layers (physical + data link + network)
|
|
7
|
+
The following classes are provided, which all are derived from the main Adapter class
|
|
8
|
+
- IP
|
|
9
|
+
- Serial
|
|
10
|
+
- VISA
|
|
11
|
+
|
|
12
|
+
Note that technically VISA is not part of the media layer, only USB is.
|
|
13
|
+
This is a limitation as it is to this day not possible to communicate "raw"
|
|
14
|
+
with a device through USB yet
|
|
15
|
+
|
|
16
|
+
An adapter is meant to work with bytes objects but it can accept strings.
|
|
17
|
+
Strings will automatically be converted to bytes using utf-8 encoding
|
|
18
|
+
"""
|
|
17
19
|
|
|
18
20
|
import logging
|
|
19
21
|
import os
|
|
@@ -30,10 +32,18 @@ from multiprocessing.connection import Client, Connection
|
|
|
30
32
|
from types import EllipsisType
|
|
31
33
|
from typing import Any
|
|
32
34
|
|
|
33
|
-
from
|
|
35
|
+
from ..component import Component
|
|
36
|
+
from ..tools.errors import (
|
|
37
|
+
AdapterDisconnected,
|
|
38
|
+
AdapterFailedToOpen,
|
|
39
|
+
AdapterTimeoutError,
|
|
40
|
+
BackendCommunicationError,
|
|
41
|
+
)
|
|
42
|
+
from ..tools.types import NumberLike, is_number
|
|
34
43
|
|
|
35
44
|
from ..tools.backend_api import (
|
|
36
45
|
BACKEND_PORT,
|
|
46
|
+
DEFAULT_ADAPTER_OPEN_TIMEOUT,
|
|
37
47
|
EXTRA_BUFFER_RESPONSE_TIME,
|
|
38
48
|
Action,
|
|
39
49
|
BackendResponse,
|
|
@@ -43,7 +53,7 @@ from ..tools.backend_api import (
|
|
|
43
53
|
)
|
|
44
54
|
from ..tools.log_settings import LoggerAlias
|
|
45
55
|
from .backend.adapter_backend import (
|
|
46
|
-
|
|
56
|
+
AdapterDisconnectedSignal,
|
|
47
57
|
AdapterReadPayload,
|
|
48
58
|
AdapterResponseTimeout,
|
|
49
59
|
AdapterSignal,
|
|
@@ -64,39 +74,61 @@ SHUTDOWN_DELAY = 2
|
|
|
64
74
|
|
|
65
75
|
|
|
66
76
|
class SignalQueue(queue.Queue[AdapterSignal]):
|
|
77
|
+
"""
|
|
78
|
+
A smart queue to hold adapter signals
|
|
79
|
+
"""
|
|
67
80
|
def __init__(self) -> None:
|
|
68
81
|
self._read_payload_counter = 0
|
|
69
82
|
super().__init__(0)
|
|
70
83
|
|
|
71
84
|
def has_read_payload(self) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Return True if the queue contains a read payload
|
|
87
|
+
"""
|
|
72
88
|
return self._read_payload_counter > 0
|
|
73
89
|
|
|
74
|
-
def put(
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
def put(self, signal: AdapterSignal) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Put a signal in the queue
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
signal : AdapterSignal
|
|
97
|
+
"""
|
|
77
98
|
if isinstance(signal, AdapterReadPayload):
|
|
78
99
|
self._read_payload_counter += 1
|
|
79
|
-
return super().put(signal
|
|
100
|
+
return super().put(signal)
|
|
80
101
|
|
|
81
102
|
def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
|
|
103
|
+
"""
|
|
104
|
+
Get an AdapterSignal from the queue
|
|
105
|
+
"""
|
|
82
106
|
signal = super().get(block, timeout)
|
|
83
107
|
if isinstance(signal, AdapterReadPayload):
|
|
84
108
|
self._read_payload_counter -= 1
|
|
85
109
|
return signal
|
|
86
110
|
|
|
87
|
-
|
|
88
111
|
def is_backend_running(address: str, port: int) -> bool:
|
|
89
|
-
|
|
112
|
+
"""
|
|
113
|
+
Return True if the backend is running
|
|
114
|
+
"""
|
|
90
115
|
try:
|
|
91
116
|
conn = Client((address, port))
|
|
92
117
|
except ConnectionRefusedError:
|
|
93
118
|
return False
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return True
|
|
97
|
-
|
|
119
|
+
conn.close()
|
|
120
|
+
return True
|
|
98
121
|
|
|
99
122
|
def start_backend(port: int | None = None) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Start the backend in a separate process
|
|
125
|
+
|
|
126
|
+
A custom port can be specified
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
port : int
|
|
131
|
+
"""
|
|
100
132
|
arguments = [
|
|
101
133
|
sys.executable,
|
|
102
134
|
"-m",
|
|
@@ -113,7 +145,7 @@ def start_backend(port: int | None = None) -> None:
|
|
|
113
145
|
stderr = subprocess.DEVNULL
|
|
114
146
|
|
|
115
147
|
if os.name == "posix":
|
|
116
|
-
subprocess.Popen(
|
|
148
|
+
subprocess.Popen( #pylint: disable=consider-using-with
|
|
117
149
|
arguments,
|
|
118
150
|
stdin=stdin,
|
|
119
151
|
stdout=stdout,
|
|
@@ -124,14 +156,14 @@ def start_backend(port: int | None = None) -> None:
|
|
|
124
156
|
|
|
125
157
|
else:
|
|
126
158
|
# Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
create_new_process_group = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
|
|
160
|
+
detached_process = 0x00000008 # not exposed by subprocess on all Pythons
|
|
129
161
|
# Optional: CREATE_NO_WINDOW (no window even for console apps)
|
|
130
|
-
|
|
162
|
+
create_no_window = 0x08000000
|
|
131
163
|
|
|
132
|
-
creationflags =
|
|
164
|
+
creationflags = create_new_process_group | detached_process | create_no_window
|
|
133
165
|
|
|
134
|
-
subprocess.Popen(
|
|
166
|
+
subprocess.Popen( #pylint: disable=consider-using-with
|
|
135
167
|
arguments,
|
|
136
168
|
stdin=stdin,
|
|
137
169
|
stdout=stdout,
|
|
@@ -142,13 +174,27 @@ def start_backend(port: int | None = None) -> None:
|
|
|
142
174
|
|
|
143
175
|
|
|
144
176
|
class ReadScope(Enum):
|
|
177
|
+
"""
|
|
178
|
+
Read scope
|
|
179
|
+
|
|
180
|
+
NEXT : Only read data after the start of the read() call
|
|
181
|
+
BUFFERED : Return any data that was present before the read() call
|
|
182
|
+
"""
|
|
145
183
|
NEXT = "next"
|
|
146
184
|
BUFFERED = "buffered"
|
|
147
185
|
|
|
148
186
|
|
|
149
|
-
class Adapter(
|
|
187
|
+
class Adapter(Component[bytes]):
|
|
188
|
+
"""
|
|
189
|
+
Adapter class
|
|
190
|
+
|
|
191
|
+
An adapter permits communication with a hardware device.
|
|
192
|
+
The adapter is the user interface of the backend adapter
|
|
193
|
+
"""
|
|
194
|
+
#pylint: disable=too-many-instance-attributes
|
|
150
195
|
def __init__(
|
|
151
196
|
self,
|
|
197
|
+
*,
|
|
152
198
|
descriptor: Descriptor,
|
|
153
199
|
alias: str = "",
|
|
154
200
|
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
@@ -173,9 +219,7 @@ class Adapter(ABC):
|
|
|
173
219
|
encoding : str
|
|
174
220
|
Which encoding to use if str has to be encoded into bytes
|
|
175
221
|
"""
|
|
176
|
-
|
|
177
|
-
super().__init__()
|
|
178
|
-
self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
|
|
222
|
+
super().__init__(LoggerAlias.ADAPTER)
|
|
179
223
|
self.encoding = encoding
|
|
180
224
|
self._signal_queue: SignalQueue = SignalQueue()
|
|
181
225
|
self.event_callback: Callable[[AdapterSignal], None] | None = event_callback
|
|
@@ -183,17 +227,12 @@ class Adapter(ABC):
|
|
|
183
227
|
self._backend_connection_lock = threading.Lock()
|
|
184
228
|
self._make_backend_request_queue: queue.Queue[BackendResponse] = queue.Queue()
|
|
185
229
|
self._make_backend_request_flag = threading.Event()
|
|
186
|
-
self.
|
|
230
|
+
self._opened = False
|
|
187
231
|
self._alias = alias
|
|
188
232
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
else
|
|
192
|
-
self._backend_address = backend_address
|
|
193
|
-
if backend_port is None:
|
|
194
|
-
self._backend_port = BACKEND_PORT
|
|
195
|
-
else:
|
|
196
|
-
self._backend_port = backend_port
|
|
233
|
+
# Use custom backend or the default one
|
|
234
|
+
self._backend_address = default_host if backend_address is None else backend_address
|
|
235
|
+
self._backend_port = BACKEND_PORT if backend_port is None else backend_port
|
|
197
236
|
|
|
198
237
|
# There a two possibilities here
|
|
199
238
|
# A) The descriptor is fully initialized
|
|
@@ -201,9 +240,6 @@ class Adapter(ABC):
|
|
|
201
240
|
# B) The descriptor is not fully initialized
|
|
202
241
|
# -> Wait for the protocol to set defaults and then connect the adapter
|
|
203
242
|
|
|
204
|
-
assert isinstance(
|
|
205
|
-
descriptor, Descriptor
|
|
206
|
-
), "descriptor must be a Descriptor class"
|
|
207
243
|
self.descriptor = descriptor
|
|
208
244
|
self.auto_open = auto_open
|
|
209
245
|
|
|
@@ -243,7 +279,6 @@ class Adapter(ABC):
|
|
|
243
279
|
self.connect()
|
|
244
280
|
|
|
245
281
|
weakref.finalize(self, self._cleanup)
|
|
246
|
-
self._init_ok = True
|
|
247
282
|
|
|
248
283
|
# We can auto-open only if auto_open is enabled and if
|
|
249
284
|
# connection with the backend has been made (descriptor initialized)
|
|
@@ -251,6 +286,9 @@ class Adapter(ABC):
|
|
|
251
286
|
self.open()
|
|
252
287
|
|
|
253
288
|
def connect(self) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Connect to the backend
|
|
291
|
+
"""
|
|
254
292
|
if self.backend_connection is not None:
|
|
255
293
|
# No need to connect, everything has been done already
|
|
256
294
|
return
|
|
@@ -276,7 +314,7 @@ class Adapter(ABC):
|
|
|
276
314
|
try:
|
|
277
315
|
self.backend_connection = Client((default_host, BACKEND_PORT))
|
|
278
316
|
except ConnectionRefusedError as err:
|
|
279
|
-
raise
|
|
317
|
+
raise BackendCommunicationError("Failed to connect to backend") from err
|
|
280
318
|
self._read_thread = threading.Thread(
|
|
281
319
|
target=self.read_thread,
|
|
282
320
|
args=(self._signal_queue, self._make_backend_request_queue),
|
|
@@ -293,7 +331,12 @@ class Adapter(ABC):
|
|
|
293
331
|
if self.auto_open:
|
|
294
332
|
self.open()
|
|
295
333
|
|
|
296
|
-
def _make_backend_request(
|
|
334
|
+
def _make_backend_request(
|
|
335
|
+
self,
|
|
336
|
+
action: Action,
|
|
337
|
+
*args: Any,
|
|
338
|
+
timeout: float = BACKEND_REQUEST_DEFAULT_TIMEOUT,
|
|
339
|
+
) -> BackendResponse:
|
|
297
340
|
"""
|
|
298
341
|
Send a request to the backend and return the arguments
|
|
299
342
|
"""
|
|
@@ -304,11 +347,9 @@ class Adapter(ABC):
|
|
|
304
347
|
|
|
305
348
|
self._make_backend_request_flag.set()
|
|
306
349
|
try:
|
|
307
|
-
response = self._make_backend_request_queue.get(
|
|
308
|
-
timeout=BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
309
|
-
)
|
|
350
|
+
response = self._make_backend_request_queue.get(timeout=timeout)
|
|
310
351
|
except queue.Empty as err:
|
|
311
|
-
raise
|
|
352
|
+
raise BackendCommunicationError(
|
|
312
353
|
f"Failed to receive response from backend to {action}"
|
|
313
354
|
) from err
|
|
314
355
|
|
|
@@ -324,24 +365,33 @@ class Adapter(ABC):
|
|
|
324
365
|
signal_queue: SignalQueue,
|
|
325
366
|
request_queue: queue.Queue[BackendResponse],
|
|
326
367
|
) -> None:
|
|
368
|
+
"""
|
|
369
|
+
Main adapter thread, it constantly listens for data coming from the backend
|
|
370
|
+
|
|
371
|
+
- signal -> put only the signal in the signal queue
|
|
372
|
+
- otherwise -> put the whole request in the request queue
|
|
373
|
+
"""
|
|
327
374
|
while True:
|
|
328
375
|
try:
|
|
329
376
|
if self.backend_connection is None:
|
|
330
377
|
raise RuntimeError("Backend connection wasn't initialized")
|
|
331
378
|
response: tuple[Any, ...] = self.backend_connection.recv()
|
|
332
379
|
except (EOFError, TypeError, OSError):
|
|
333
|
-
signal_queue.put(
|
|
380
|
+
signal_queue.put(AdapterDisconnectedSignal())
|
|
334
381
|
request_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
|
|
335
382
|
break
|
|
336
383
|
else:
|
|
337
384
|
if not isinstance(response, tuple):
|
|
338
|
-
raise
|
|
385
|
+
raise BackendCommunicationError(
|
|
386
|
+
f"Invalid response from backend : {response}"
|
|
387
|
+
)
|
|
339
388
|
action = Action(response[0])
|
|
340
389
|
|
|
341
390
|
if action == Action.ADAPTER_SIGNAL:
|
|
342
|
-
# if is_event(action):
|
|
343
391
|
if len(response) <= 1:
|
|
344
|
-
raise
|
|
392
|
+
raise BackendCommunicationError(
|
|
393
|
+
f"Invalid event response : {response}"
|
|
394
|
+
)
|
|
345
395
|
signal: AdapterSignal = response[1]
|
|
346
396
|
if self.event_callback is not None:
|
|
347
397
|
self.event_callback(signal)
|
|
@@ -365,7 +415,8 @@ class Adapter(ABC):
|
|
|
365
415
|
|
|
366
416
|
def set_default_timeout(self, default_timeout: Timeout | None) -> None:
|
|
367
417
|
"""
|
|
368
|
-
Set the default timeout for this adapter. If a previous
|
|
418
|
+
Set the default timeout for this adapter. If a previous
|
|
419
|
+
timeout has been set, it will be fused
|
|
369
420
|
|
|
370
421
|
Parameters
|
|
371
422
|
----------
|
|
@@ -405,7 +456,7 @@ class Adapter(ABC):
|
|
|
405
456
|
if self._default_stop_condition:
|
|
406
457
|
self.set_stop_conditions(stop_condition)
|
|
407
458
|
|
|
408
|
-
def
|
|
459
|
+
def flush_read(self) -> None:
|
|
409
460
|
"""
|
|
410
461
|
Flush the input buffer
|
|
411
462
|
"""
|
|
@@ -432,9 +483,20 @@ class Adapter(ABC):
|
|
|
432
483
|
"""
|
|
433
484
|
Start communication with the device
|
|
434
485
|
"""
|
|
435
|
-
self._make_backend_request(
|
|
486
|
+
self._make_backend_request(
|
|
487
|
+
Action.OPEN,
|
|
488
|
+
self._stop_conditions,
|
|
489
|
+
timeout=BACKEND_REQUEST_DEFAULT_TIMEOUT + DEFAULT_ADAPTER_OPEN_TIMEOUT,
|
|
490
|
+
)
|
|
436
491
|
self._logger.info("Adapter opened")
|
|
437
|
-
self.
|
|
492
|
+
self._opened = True
|
|
493
|
+
|
|
494
|
+
def try_open(self) -> bool:
|
|
495
|
+
try:
|
|
496
|
+
self.open()
|
|
497
|
+
except AdapterFailedToOpen:
|
|
498
|
+
return False
|
|
499
|
+
return True
|
|
438
500
|
|
|
439
501
|
def close(self, force: bool = False) -> None:
|
|
440
502
|
"""
|
|
@@ -450,9 +512,19 @@ class Adapter(ABC):
|
|
|
450
512
|
if self.backend_connection is not None:
|
|
451
513
|
self.backend_connection.close()
|
|
452
514
|
|
|
453
|
-
self.
|
|
515
|
+
self._opened = False
|
|
516
|
+
|
|
517
|
+
def is_opened(self) -> bool:
|
|
518
|
+
"""
|
|
519
|
+
Return True if the adapter is opened and False otherwise
|
|
520
|
+
|
|
521
|
+
Returns
|
|
522
|
+
-------
|
|
523
|
+
opened : bool
|
|
524
|
+
"""
|
|
525
|
+
return self._opened
|
|
454
526
|
|
|
455
|
-
def write(self, data: bytes
|
|
527
|
+
def write(self, data: bytes) -> None:
|
|
456
528
|
"""
|
|
457
529
|
Send data to the device
|
|
458
530
|
|
|
@@ -544,13 +616,15 @@ class Adapter(ABC):
|
|
|
544
616
|
|
|
545
617
|
signal = self._signal_queue.get(timeout=queue_timeout)
|
|
546
618
|
except queue.Empty as e:
|
|
547
|
-
raise
|
|
619
|
+
raise BackendCommunicationError(
|
|
620
|
+
"Failed to receive response from backend"
|
|
621
|
+
) from e
|
|
548
622
|
if isinstance(signal, AdapterReadPayload):
|
|
549
623
|
output_signal = signal
|
|
550
624
|
break
|
|
551
|
-
|
|
552
|
-
raise
|
|
553
|
-
|
|
625
|
+
if isinstance(signal, AdapterDisconnectedSignal):
|
|
626
|
+
raise AdapterDisconnected()
|
|
627
|
+
if isinstance(signal, AdapterResponseTimeout):
|
|
554
628
|
if start_read_id == signal.identifier:
|
|
555
629
|
output_signal = None
|
|
556
630
|
break
|
|
@@ -569,8 +643,9 @@ class Adapter(ABC):
|
|
|
569
643
|
response_delay=None,
|
|
570
644
|
)
|
|
571
645
|
case TimeoutAction.ERROR:
|
|
572
|
-
|
|
573
|
-
|
|
646
|
+
timeout_value = read_timeout.response()
|
|
647
|
+
raise AdapterTimeoutError(
|
|
648
|
+
float("nan") if timeout_value is None else timeout_value
|
|
574
649
|
)
|
|
575
650
|
case _:
|
|
576
651
|
raise NotImplementedError()
|
|
@@ -587,7 +662,7 @@ class Adapter(ABC):
|
|
|
587
662
|
return signal.data()
|
|
588
663
|
|
|
589
664
|
def _cleanup(self) -> None:
|
|
590
|
-
if self.
|
|
665
|
+
if self._opened:
|
|
591
666
|
self.close()
|
|
592
667
|
|
|
593
668
|
def query_detailed(
|
|
@@ -602,7 +677,7 @@ class Adapter(ABC):
|
|
|
602
677
|
- write
|
|
603
678
|
- read
|
|
604
679
|
"""
|
|
605
|
-
self.
|
|
680
|
+
self.flush_read()
|
|
606
681
|
self.write(data)
|
|
607
682
|
return self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
|
|
608
683
|
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# File : auto.py
|
|
2
2
|
# Author : Sébastien Deriaz
|
|
3
3
|
# License : GPL
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Automatic adapter function
|
|
7
|
+
This function is used to automatically choose an adapter based on the user's input
|
|
8
|
+
192.168.1.1 -> IP
|
|
9
|
+
COM4 -> Serial
|
|
10
|
+
/dev/tty* -> Serial
|
|
11
|
+
etc...
|
|
12
|
+
If an adapter class is supplied, it is passed through
|
|
13
|
+
|
|
14
|
+
Additionnaly, it is possible to do COM4:115200 so as to make the life of the user easier
|
|
15
|
+
Same with /dev/ttyACM0:115200
|
|
16
|
+
"""
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
from .adapter import Adapter
|
|
@@ -27,6 +29,15 @@ from .visa import Visa
|
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
|
|
32
|
+
"""
|
|
33
|
+
Create an adapter from a string or an adapter
|
|
34
|
+
|
|
35
|
+
- <int>.<int>.<int>.<int>[:<int>] -> IP
|
|
36
|
+
- x.y[:<int>] -> IP
|
|
37
|
+
- COM<int> -> SerialPort
|
|
38
|
+
- /dev/tty[ACM|USB]<int> -> SerialPort
|
|
39
|
+
|
|
40
|
+
"""
|
|
30
41
|
if isinstance(adapter_or_string, Adapter):
|
|
31
42
|
# Simply return it
|
|
32
43
|
return adapter_or_string
|
|
@@ -39,12 +50,12 @@ def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
|
|
|
39
50
|
port=descriptor.port,
|
|
40
51
|
transport=descriptor.transport.value,
|
|
41
52
|
)
|
|
42
|
-
|
|
53
|
+
if isinstance(descriptor, SerialPortDescriptor):
|
|
43
54
|
return SerialPort(port=descriptor.port, baudrate=descriptor.baudrate)
|
|
44
|
-
|
|
55
|
+
if isinstance(descriptor, VisaDescriptor):
|
|
45
56
|
return Visa(descriptor=descriptor.descriptor)
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
|
|
58
|
+
raise RuntimeError(f"Invalid descriptor : {descriptor}")
|
|
48
59
|
|
|
49
60
|
else:
|
|
50
61
|
raise ValueError(f"Invalid adapter : {adapter_or_string}")
|
|
@@ -61,7 +61,7 @@ class AdapterSignal:
|
|
|
61
61
|
pass
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
class
|
|
64
|
+
class AdapterDisconnectedSignal(AdapterSignal):
|
|
65
65
|
def __str__(self) -> str:
|
|
66
66
|
return "Adapter disconnected"
|
|
67
67
|
|
|
@@ -211,7 +211,7 @@ class AdapterBackend(ABC):
|
|
|
211
211
|
return self._previous_buffer.data == b""
|
|
212
212
|
|
|
213
213
|
@abstractmethod
|
|
214
|
-
def open(self) ->
|
|
214
|
+
def open(self) -> None:
|
|
215
215
|
"""
|
|
216
216
|
Start communication with the device
|
|
217
217
|
"""
|
|
@@ -261,7 +261,7 @@ class AdapterBackend(ABC):
|
|
|
261
261
|
fragment_delta_t = float("nan")
|
|
262
262
|
if fragment.data == b"":
|
|
263
263
|
self.close()
|
|
264
|
-
yield
|
|
264
|
+
yield AdapterDisconnectedSignal()
|
|
265
265
|
else:
|
|
266
266
|
self._logger.debug(
|
|
267
267
|
f"New fragment {fragment_delta_t:+.3f} {fragment}"
|
|
@@ -16,7 +16,7 @@ from typing import Any
|
|
|
16
16
|
from syndesi.adapters.backend.stop_condition_backend import (
|
|
17
17
|
stop_condition_to_backend,
|
|
18
18
|
)
|
|
19
|
-
from syndesi.tools.errors import make_error_description
|
|
19
|
+
from syndesi.tools.errors import AdapterError, make_error_description
|
|
20
20
|
from syndesi.tools.types import NumberLike
|
|
21
21
|
|
|
22
22
|
from ...tools.backend_api import Action, frontend_send
|
|
@@ -99,16 +99,12 @@ class AdapterSession(threading.Thread):
|
|
|
99
99
|
self._shutdown_counter_top = None
|
|
100
100
|
self._shutdown_counter = None
|
|
101
101
|
|
|
102
|
-
# self._timeout_events: list[tuple[TimeoutEvent, float]] = []
|
|
103
|
-
|
|
104
102
|
self._read_init_id = 0
|
|
105
103
|
|
|
106
104
|
def add_connection(self, conn: NamedConnection) -> None:
|
|
107
105
|
with self._connections_lock:
|
|
108
106
|
self.connections.append(conn)
|
|
109
|
-
# os.write(self._new_connection_w, b"\x00")
|
|
110
107
|
self._new_connection_w.send(b"\x00")
|
|
111
|
-
self._logger.info(f"New client : {conn.remote()}")
|
|
112
108
|
|
|
113
109
|
def _remove_connection(self, conn: NamedConnection) -> None:
|
|
114
110
|
with self._connections_lock:
|
|
@@ -272,12 +268,15 @@ class AdapterSession(threading.Thread):
|
|
|
272
268
|
self._adapter.set_stop_conditions(
|
|
273
269
|
[stop_condition_to_backend(sc) for sc in request[1]]
|
|
274
270
|
)
|
|
275
|
-
|
|
271
|
+
try:
|
|
272
|
+
self._adapter.open()
|
|
273
|
+
except AdapterError as e:
|
|
276
274
|
# Success !
|
|
277
|
-
response_action = Action.OPEN
|
|
278
|
-
else:
|
|
279
275
|
response_action = Action.ERROR_FAILED_TO_OPEN
|
|
280
|
-
|
|
276
|
+
extra_arguments = (str(e),)
|
|
277
|
+
else:
|
|
278
|
+
response_action = Action.OPEN
|
|
279
|
+
extra_arguments = ("",)
|
|
281
280
|
case Action.WRITE:
|
|
282
281
|
data = request[1]
|
|
283
282
|
if self._adapter.is_opened():
|
|
@@ -98,6 +98,8 @@ def is_request(x: object) -> TypeGuard[tuple[str, object]]:
|
|
|
98
98
|
|
|
99
99
|
class Backend:
|
|
100
100
|
MONITORING_DELAY = 0.5
|
|
101
|
+
NEW_CLIENT_REQUEST_TIMEOUT = 0.5
|
|
102
|
+
|
|
101
103
|
_session_shutdown_delay: NumberLike | None
|
|
102
104
|
_backend_shutdown_delay: NumberLike | None
|
|
103
105
|
_backend_shutdown_timestamp: NumberLike | None
|
|
@@ -278,7 +280,9 @@ class Backend:
|
|
|
278
280
|
# Wait for adapter
|
|
279
281
|
# ready = wait([client.conn], timeout=0.1)
|
|
280
282
|
# selectors to work on Unix and Windows
|
|
281
|
-
ready, _, _ = select.select(
|
|
283
|
+
ready, _, _ = select.select(
|
|
284
|
+
[client.conn], [], [], self.NEW_CLIENT_REQUEST_TIMEOUT
|
|
285
|
+
)
|
|
282
286
|
if len(ready) == 0:
|
|
283
287
|
client.conn.close()
|
|
284
288
|
return
|
|
@@ -290,6 +294,7 @@ class Backend:
|
|
|
290
294
|
action = Action(adapter_request[0])
|
|
291
295
|
if action == Action.SELECT_ADAPTER:
|
|
292
296
|
adapter_descriptor = adapter_request[1]
|
|
297
|
+
self._logger.info(f"New client for {adapter_descriptor}")
|
|
293
298
|
# If the session exists but it is dead, delete it
|
|
294
299
|
if (
|
|
295
300
|
adapter_descriptor in self.adapter_sessions
|
|
@@ -299,7 +304,7 @@ class Backend:
|
|
|
299
304
|
|
|
300
305
|
if adapter_descriptor not in self.adapter_sessions:
|
|
301
306
|
# Create the adapter backend thread
|
|
302
|
-
self._logger.info(f"Creating adapter session for {adapter_descriptor}")
|
|
307
|
+
# self._logger.info(f"Creating adapter session for {adapter_descriptor}")
|
|
303
308
|
thread = AdapterSession(
|
|
304
309
|
adapter_descriptor, shutdown_delay=self._session_shutdown_delay
|
|
305
310
|
) # TODO : Put another delay here ?
|