syndesi 0.4.1__py3-none-any.whl → 0.4.4__py3-none-any.whl
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/__init__.py +16 -1
- syndesi/adapters/adapter.py +187 -108
- syndesi/adapters/auto.py +26 -15
- syndesi/adapters/backend/adapter_backend.py +57 -39
- syndesi/adapters/backend/adapter_session.py +30 -32
- syndesi/adapters/backend/backend.py +7 -2
- syndesi/adapters/backend/backend_status.py +0 -0
- syndesi/adapters/backend/backend_tools.py +1 -1
- syndesi/adapters/backend/descriptors.py +1 -1
- syndesi/adapters/backend/ip_backend.py +45 -42
- syndesi/adapters/backend/serialport_backend.py +23 -19
- syndesi/adapters/backend/stop_condition_backend.py +54 -30
- syndesi/adapters/backend/visa_backend.py +5 -5
- syndesi/adapters/ip.py +33 -27
- syndesi/adapters/ip_server.py +9 -3
- syndesi/adapters/serialport.py +20 -7
- syndesi/adapters/stop_condition.py +86 -135
- syndesi/adapters/timeout.py +2 -28
- syndesi/adapters/visa.py +11 -3
- syndesi/cli/backend_status.py +7 -9
- syndesi/cli/console.py +0 -53
- syndesi/cli/shell.py +3 -16
- syndesi/cli/shell_tools.py +0 -5
- syndesi/component.py +79 -0
- syndesi/protocols/delimited.py +7 -22
- syndesi/protocols/modbus.py +9 -8
- syndesi/protocols/protocol.py +7 -1
- syndesi/protocols/raw.py +4 -4
- syndesi/protocols/scpi.py +6 -5
- syndesi/scripts/syndesi.py +2 -4
- syndesi/tools/backend_api.py +6 -32
- syndesi/tools/backend_logger.py +0 -1
- syndesi/tools/errors.py +28 -9
- syndesi/tools/log.py +0 -87
- syndesi/tools/types.py +0 -43
- syndesi/version.py +5 -1
- syndesi-0.4.4.dist-info/METADATA +96 -0
- syndesi-0.4.4.dist-info/RECORD +61 -0
- syndesi-0.4.1.dist-info/METADATA +0 -123
- syndesi-0.4.1.dist-info/RECORD +0 -59
- {syndesi-0.4.1.dist-info → syndesi-0.4.4.dist-info}/WHEEL +0 -0
- {syndesi-0.4.1.dist-info → syndesi-0.4.4.dist-info}/entry_points.txt +0 -0
- {syndesi-0.4.1.dist-info → syndesi-0.4.4.dist-info}/licenses/LICENSE +0 -0
- {syndesi-0.4.1.dist-info → syndesi-0.4.4.dist-info}/top_level.txt +0 -0
syndesi/__init__.py
CHANGED
|
@@ -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
|
+
]
|
syndesi/adapters/adapter.py
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
# File : adapters.py
|
|
2
2
|
# Author : Sébastien Deriaz
|
|
3
3
|
# License : GPL
|
|
4
|
-
#
|
|
5
|
-
# Adapters provide a common abstraction for the media layers (physical + data link + network)
|
|
6
|
-
# The following classes are provided, which all are derived from the main Adapter class
|
|
7
|
-
# - IP
|
|
8
|
-
# - Serial
|
|
9
|
-
# - VISA
|
|
10
|
-
#
|
|
11
|
-
# Note that technically VISA is not part of the media layer, only USB is.
|
|
12
|
-
# This is a limitation as it is to this day not possible to communicate "raw"
|
|
13
|
-
# with a device through USB yet
|
|
14
|
-
#
|
|
15
|
-
# An adapter is meant to work with bytes objects but it can accept strings.
|
|
16
|
-
# Strings will automatically be converted to bytes using utf-8 encoding
|
|
17
4
|
|
|
18
|
-
|
|
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
|
+
"""
|
|
19
|
+
|
|
19
20
|
import logging
|
|
21
|
+
import os
|
|
20
22
|
import queue
|
|
21
23
|
import subprocess
|
|
22
24
|
import sys
|
|
@@ -25,93 +27,125 @@ import time
|
|
|
25
27
|
import weakref
|
|
26
28
|
from abc import ABC, abstractmethod
|
|
27
29
|
from collections.abc import Callable
|
|
30
|
+
from enum import Enum
|
|
28
31
|
from multiprocessing.connection import Client, Connection
|
|
29
32
|
from types import EllipsisType
|
|
30
33
|
from typing import Any
|
|
31
|
-
import os
|
|
32
34
|
|
|
33
|
-
from
|
|
34
|
-
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
|
|
35
43
|
|
|
36
44
|
from ..tools.backend_api import (
|
|
37
45
|
BACKEND_PORT,
|
|
46
|
+
DEFAULT_ADAPTER_OPEN_TIMEOUT,
|
|
47
|
+
EXTRA_BUFFER_RESPONSE_TIME,
|
|
38
48
|
Action,
|
|
39
49
|
BackendResponse,
|
|
40
50
|
Fragment,
|
|
41
51
|
default_host,
|
|
42
52
|
raise_if_error,
|
|
43
|
-
EXTRA_BUFFER_RESPONSE_TIME
|
|
44
53
|
)
|
|
45
54
|
from ..tools.log_settings import LoggerAlias
|
|
46
55
|
from .backend.adapter_backend import (
|
|
47
|
-
|
|
48
|
-
AdapterResponseTimeout,
|
|
56
|
+
AdapterDisconnectedSignal,
|
|
49
57
|
AdapterReadPayload,
|
|
58
|
+
AdapterResponseTimeout,
|
|
50
59
|
AdapterSignal,
|
|
51
60
|
)
|
|
61
|
+
from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
52
62
|
from .backend.descriptors import Descriptor
|
|
53
|
-
from .stop_condition import
|
|
63
|
+
from .stop_condition import Continuation, StopCondition, StopConditionType
|
|
54
64
|
from .timeout import Timeout, TimeoutAction, any_to_timeout
|
|
55
65
|
|
|
56
66
|
DEFAULT_STOP_CONDITION = Continuation(time=0.1)
|
|
57
67
|
|
|
58
|
-
DEFAULT_TIMEOUT = Timeout(response=5, action=
|
|
68
|
+
DEFAULT_TIMEOUT = Timeout(response=5, action="error")
|
|
59
69
|
|
|
60
70
|
# Maximum time to let the backend start
|
|
61
71
|
START_TIMEOUT = 2
|
|
62
72
|
# Time to shutdown the backend
|
|
63
73
|
SHUTDOWN_DELAY = 2
|
|
64
74
|
|
|
75
|
+
|
|
65
76
|
class SignalQueue(queue.Queue[AdapterSignal]):
|
|
77
|
+
"""
|
|
78
|
+
A smart queue to hold adapter signals
|
|
79
|
+
"""
|
|
66
80
|
def __init__(self) -> None:
|
|
67
81
|
self._read_payload_counter = 0
|
|
68
82
|
super().__init__(0)
|
|
69
83
|
|
|
70
84
|
def has_read_payload(self) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Return True if the queue contains a read payload
|
|
87
|
+
"""
|
|
71
88
|
return self._read_payload_counter > 0
|
|
72
89
|
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
def put(self, signal: AdapterSignal) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Put a signal in the queue
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
signal : AdapterSignal
|
|
97
|
+
"""
|
|
75
98
|
if isinstance(signal, AdapterReadPayload):
|
|
76
99
|
self._read_payload_counter += 1
|
|
77
|
-
return super().put(signal
|
|
78
|
-
|
|
100
|
+
return super().put(signal)
|
|
79
101
|
|
|
80
102
|
def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
|
|
103
|
+
"""
|
|
104
|
+
Get an AdapterSignal from the queue
|
|
105
|
+
"""
|
|
81
106
|
signal = super().get(block, timeout)
|
|
82
107
|
if isinstance(signal, AdapterReadPayload):
|
|
83
108
|
self._read_payload_counter -= 1
|
|
84
109
|
return signal
|
|
85
110
|
|
|
86
|
-
|
|
87
111
|
def is_backend_running(address: str, port: int) -> bool:
|
|
88
|
-
|
|
112
|
+
"""
|
|
113
|
+
Return True if the backend is running
|
|
114
|
+
"""
|
|
89
115
|
try:
|
|
90
116
|
conn = Client((address, port))
|
|
91
117
|
except ConnectionRefusedError:
|
|
92
118
|
return False
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return True
|
|
119
|
+
conn.close()
|
|
120
|
+
return True
|
|
96
121
|
|
|
97
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
|
+
"""
|
|
98
132
|
arguments = [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
stdin
|
|
133
|
+
sys.executable,
|
|
134
|
+
"-m",
|
|
135
|
+
"syndesi.adapters.backend.backend",
|
|
136
|
+
"-s",
|
|
137
|
+
str(SHUTDOWN_DELAY),
|
|
138
|
+
"-q",
|
|
139
|
+
"-p",
|
|
140
|
+
str(BACKEND_PORT if port is None else port),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
stdin = subprocess.DEVNULL
|
|
110
144
|
stdout = subprocess.DEVNULL
|
|
111
145
|
stderr = subprocess.DEVNULL
|
|
112
146
|
|
|
113
147
|
if os.name == "posix":
|
|
114
|
-
subprocess.Popen(
|
|
148
|
+
subprocess.Popen( #pylint: disable=consider-using-with
|
|
115
149
|
arguments,
|
|
116
150
|
stdin=stdin,
|
|
117
151
|
stdout=stdout,
|
|
@@ -122,14 +156,14 @@ def start_backend(port: int | None = None) -> None:
|
|
|
122
156
|
|
|
123
157
|
else:
|
|
124
158
|
# Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
|
|
125
|
-
|
|
126
|
-
|
|
159
|
+
create_new_process_group = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
|
|
160
|
+
detached_process = 0x00000008 # not exposed by subprocess on all Pythons
|
|
127
161
|
# Optional: CREATE_NO_WINDOW (no window even for console apps)
|
|
128
|
-
|
|
162
|
+
create_no_window = 0x08000000
|
|
129
163
|
|
|
130
|
-
creationflags =
|
|
164
|
+
creationflags = create_new_process_group | detached_process | create_no_window
|
|
131
165
|
|
|
132
|
-
subprocess.Popen(
|
|
166
|
+
subprocess.Popen( #pylint: disable=consider-using-with
|
|
133
167
|
arguments,
|
|
134
168
|
stdin=stdin,
|
|
135
169
|
stdout=stdout,
|
|
@@ -138,13 +172,29 @@ def start_backend(port: int | None = None) -> None:
|
|
|
138
172
|
close_fds=True,
|
|
139
173
|
)
|
|
140
174
|
|
|
175
|
+
|
|
141
176
|
class ReadScope(Enum):
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
"""
|
|
183
|
+
NEXT = "next"
|
|
184
|
+
BUFFERED = "buffered"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Adapter(Component[bytes]):
|
|
188
|
+
"""
|
|
189
|
+
Adapter class
|
|
144
190
|
|
|
145
|
-
|
|
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
|
|
146
195
|
def __init__(
|
|
147
196
|
self,
|
|
197
|
+
*,
|
|
148
198
|
descriptor: Descriptor,
|
|
149
199
|
alias: str = "",
|
|
150
200
|
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
@@ -169,9 +219,7 @@ class Adapter(ABC):
|
|
|
169
219
|
encoding : str
|
|
170
220
|
Which encoding to use if str has to be encoded into bytes
|
|
171
221
|
"""
|
|
172
|
-
|
|
173
|
-
super().__init__()
|
|
174
|
-
self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
|
|
222
|
+
super().__init__(LoggerAlias.ADAPTER)
|
|
175
223
|
self.encoding = encoding
|
|
176
224
|
self._signal_queue: SignalQueue = SignalQueue()
|
|
177
225
|
self.event_callback: Callable[[AdapterSignal], None] | None = event_callback
|
|
@@ -179,17 +227,12 @@ class Adapter(ABC):
|
|
|
179
227
|
self._backend_connection_lock = threading.Lock()
|
|
180
228
|
self._make_backend_request_queue: queue.Queue[BackendResponse] = queue.Queue()
|
|
181
229
|
self._make_backend_request_flag = threading.Event()
|
|
182
|
-
self.
|
|
230
|
+
self._opened = False
|
|
183
231
|
self._alias = alias
|
|
184
232
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
else
|
|
188
|
-
self._backend_address = backend_address
|
|
189
|
-
if backend_port is None:
|
|
190
|
-
self._backend_port = BACKEND_PORT
|
|
191
|
-
else:
|
|
192
|
-
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
|
|
193
236
|
|
|
194
237
|
# There a two possibilities here
|
|
195
238
|
# A) The descriptor is fully initialized
|
|
@@ -197,9 +240,6 @@ class Adapter(ABC):
|
|
|
197
240
|
# B) The descriptor is not fully initialized
|
|
198
241
|
# -> Wait for the protocol to set defaults and then connect the adapter
|
|
199
242
|
|
|
200
|
-
assert isinstance(
|
|
201
|
-
descriptor, Descriptor
|
|
202
|
-
), "descriptor must be a Descriptor class"
|
|
203
243
|
self.descriptor = descriptor
|
|
204
244
|
self.auto_open = auto_open
|
|
205
245
|
|
|
@@ -215,7 +255,7 @@ class Adapter(ABC):
|
|
|
215
255
|
elif isinstance(stop_conditions, list):
|
|
216
256
|
self._stop_conditions = stop_conditions
|
|
217
257
|
else:
|
|
218
|
-
raise ValueError(
|
|
258
|
+
raise ValueError("Invalid stop_conditions")
|
|
219
259
|
|
|
220
260
|
# Set the timeout
|
|
221
261
|
self.is_default_timeout = False
|
|
@@ -239,7 +279,6 @@ class Adapter(ABC):
|
|
|
239
279
|
self.connect()
|
|
240
280
|
|
|
241
281
|
weakref.finalize(self, self._cleanup)
|
|
242
|
-
self._init_ok = True
|
|
243
282
|
|
|
244
283
|
# We can auto-open only if auto_open is enabled and if
|
|
245
284
|
# connection with the backend has been made (descriptor initialized)
|
|
@@ -247,6 +286,9 @@ class Adapter(ABC):
|
|
|
247
286
|
self.open()
|
|
248
287
|
|
|
249
288
|
def connect(self) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Connect to the backend
|
|
291
|
+
"""
|
|
250
292
|
if self.backend_connection is not None:
|
|
251
293
|
# No need to connect, everything has been done already
|
|
252
294
|
return
|
|
@@ -272,7 +314,7 @@ class Adapter(ABC):
|
|
|
272
314
|
try:
|
|
273
315
|
self.backend_connection = Client((default_host, BACKEND_PORT))
|
|
274
316
|
except ConnectionRefusedError as err:
|
|
275
|
-
raise
|
|
317
|
+
raise BackendCommunicationError("Failed to connect to backend") from err
|
|
276
318
|
self._read_thread = threading.Thread(
|
|
277
319
|
target=self.read_thread,
|
|
278
320
|
args=(self._signal_queue, self._make_backend_request_queue),
|
|
@@ -289,7 +331,12 @@ class Adapter(ABC):
|
|
|
289
331
|
if self.auto_open:
|
|
290
332
|
self.open()
|
|
291
333
|
|
|
292
|
-
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:
|
|
293
340
|
"""
|
|
294
341
|
Send a request to the backend and return the arguments
|
|
295
342
|
"""
|
|
@@ -300,11 +347,9 @@ class Adapter(ABC):
|
|
|
300
347
|
|
|
301
348
|
self._make_backend_request_flag.set()
|
|
302
349
|
try:
|
|
303
|
-
response = self._make_backend_request_queue.get(
|
|
304
|
-
timeout=BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
305
|
-
)
|
|
350
|
+
response = self._make_backend_request_queue.get(timeout=timeout)
|
|
306
351
|
except queue.Empty as err:
|
|
307
|
-
raise
|
|
352
|
+
raise BackendCommunicationError(
|
|
308
353
|
f"Failed to receive response from backend to {action}"
|
|
309
354
|
) from err
|
|
310
355
|
|
|
@@ -320,24 +365,33 @@ class Adapter(ABC):
|
|
|
320
365
|
signal_queue: SignalQueue,
|
|
321
366
|
request_queue: queue.Queue[BackendResponse],
|
|
322
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
|
+
"""
|
|
323
374
|
while True:
|
|
324
375
|
try:
|
|
325
376
|
if self.backend_connection is None:
|
|
326
377
|
raise RuntimeError("Backend connection wasn't initialized")
|
|
327
378
|
response: tuple[Any, ...] = self.backend_connection.recv()
|
|
328
379
|
except (EOFError, TypeError, OSError):
|
|
329
|
-
signal_queue.put(
|
|
380
|
+
signal_queue.put(AdapterDisconnectedSignal())
|
|
330
381
|
request_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
|
|
331
382
|
break
|
|
332
383
|
else:
|
|
333
384
|
if not isinstance(response, tuple):
|
|
334
|
-
raise
|
|
385
|
+
raise BackendCommunicationError(
|
|
386
|
+
f"Invalid response from backend : {response}"
|
|
387
|
+
)
|
|
335
388
|
action = Action(response[0])
|
|
336
389
|
|
|
337
390
|
if action == Action.ADAPTER_SIGNAL:
|
|
338
|
-
#if is_event(action):
|
|
339
391
|
if len(response) <= 1:
|
|
340
|
-
raise
|
|
392
|
+
raise BackendCommunicationError(
|
|
393
|
+
f"Invalid event response : {response}"
|
|
394
|
+
)
|
|
341
395
|
signal: AdapterSignal = response[1]
|
|
342
396
|
if self.event_callback is not None:
|
|
343
397
|
self.event_callback(signal)
|
|
@@ -361,7 +415,8 @@ class Adapter(ABC):
|
|
|
361
415
|
|
|
362
416
|
def set_default_timeout(self, default_timeout: Timeout | None) -> None:
|
|
363
417
|
"""
|
|
364
|
-
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
|
|
365
420
|
|
|
366
421
|
Parameters
|
|
367
422
|
----------
|
|
@@ -371,7 +426,9 @@ class Adapter(ABC):
|
|
|
371
426
|
self._logger.debug(f"Setting default timeout to {default_timeout}")
|
|
372
427
|
self._timeout = default_timeout
|
|
373
428
|
|
|
374
|
-
def set_stop_conditions(
|
|
429
|
+
def set_stop_conditions(
|
|
430
|
+
self, stop_conditions: StopCondition | None | list[StopCondition]
|
|
431
|
+
) -> None:
|
|
375
432
|
"""
|
|
376
433
|
Overwrite the stop-condition
|
|
377
434
|
|
|
@@ -399,7 +456,7 @@ class Adapter(ABC):
|
|
|
399
456
|
if self._default_stop_condition:
|
|
400
457
|
self.set_stop_conditions(stop_condition)
|
|
401
458
|
|
|
402
|
-
def
|
|
459
|
+
def flush_read(self) -> None:
|
|
403
460
|
"""
|
|
404
461
|
Flush the input buffer
|
|
405
462
|
"""
|
|
@@ -411,7 +468,6 @@ class Adapter(ABC):
|
|
|
411
468
|
self._signal_queue.get(block=False)
|
|
412
469
|
except queue.Empty:
|
|
413
470
|
break
|
|
414
|
-
|
|
415
471
|
|
|
416
472
|
def previous_read_buffer_empty(self) -> bool:
|
|
417
473
|
"""
|
|
@@ -427,9 +483,20 @@ class Adapter(ABC):
|
|
|
427
483
|
"""
|
|
428
484
|
Start communication with the device
|
|
429
485
|
"""
|
|
430
|
-
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
|
+
)
|
|
431
491
|
self._logger.info("Adapter opened")
|
|
432
|
-
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
|
|
433
500
|
|
|
434
501
|
def close(self, force: bool = False) -> None:
|
|
435
502
|
"""
|
|
@@ -445,9 +512,19 @@ class Adapter(ABC):
|
|
|
445
512
|
if self.backend_connection is not None:
|
|
446
513
|
self.backend_connection.close()
|
|
447
514
|
|
|
448
|
-
self.
|
|
515
|
+
self._opened = False
|
|
449
516
|
|
|
450
|
-
def
|
|
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
|
|
526
|
+
|
|
527
|
+
def write(self, data: bytes) -> None:
|
|
451
528
|
"""
|
|
452
529
|
Send data to the device
|
|
453
530
|
|
|
@@ -464,7 +541,7 @@ class Adapter(ABC):
|
|
|
464
541
|
self,
|
|
465
542
|
timeout: Timeout | EllipsisType | None = ...,
|
|
466
543
|
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
467
|
-
scope
|
|
544
|
+
scope: str = ReadScope.BUFFERED.value,
|
|
468
545
|
) -> AdapterReadPayload:
|
|
469
546
|
"""
|
|
470
547
|
Read data from the device
|
|
@@ -490,13 +567,13 @@ class Adapter(ABC):
|
|
|
490
567
|
read_timeout = self._timeout
|
|
491
568
|
else:
|
|
492
569
|
read_timeout = any_to_timeout(timeout)
|
|
493
|
-
|
|
570
|
+
|
|
494
571
|
if read_timeout is None:
|
|
495
572
|
raise RuntimeError("Cannot read without setting a timeout")
|
|
496
573
|
|
|
497
574
|
if stop_conditions is not ...:
|
|
498
575
|
if isinstance(stop_conditions, StopCondition):
|
|
499
|
-
stop_conditions = [stop_conditions]
|
|
576
|
+
stop_conditions = [stop_conditions]
|
|
500
577
|
self._make_backend_request(Action.SET_STOP_CONDITIONs, stop_conditions)
|
|
501
578
|
|
|
502
579
|
# First, we check if data is in the buffer and if the scope if set to BUFFERED
|
|
@@ -525,52 +602,54 @@ class Adapter(ABC):
|
|
|
525
602
|
# Wait for the response time + a bit more
|
|
526
603
|
read_stop_timestamp = read_init_time + _response
|
|
527
604
|
|
|
528
|
-
# else:
|
|
529
|
-
# start_read_id = None
|
|
530
|
-
# read_init_time = None
|
|
531
|
-
|
|
532
|
-
|
|
533
605
|
while True:
|
|
534
606
|
try:
|
|
535
607
|
if read_stop_timestamp is None:
|
|
536
608
|
queue_timeout = None
|
|
537
609
|
else:
|
|
538
|
-
queue_timeout = max(
|
|
610
|
+
queue_timeout = max(
|
|
611
|
+
0,
|
|
612
|
+
read_stop_timestamp
|
|
613
|
+
- time.time()
|
|
614
|
+
+ EXTRA_BUFFER_RESPONSE_TIME,
|
|
615
|
+
)
|
|
539
616
|
|
|
540
617
|
signal = self._signal_queue.get(timeout=queue_timeout)
|
|
541
|
-
except queue.Empty:
|
|
542
|
-
raise
|
|
618
|
+
except queue.Empty as e:
|
|
619
|
+
raise BackendCommunicationError(
|
|
620
|
+
"Failed to receive response from backend"
|
|
621
|
+
) from e
|
|
543
622
|
if isinstance(signal, AdapterReadPayload):
|
|
544
623
|
output_signal = signal
|
|
545
624
|
break
|
|
546
|
-
|
|
547
|
-
raise
|
|
548
|
-
|
|
625
|
+
if isinstance(signal, AdapterDisconnectedSignal):
|
|
626
|
+
raise AdapterDisconnected()
|
|
627
|
+
if isinstance(signal, AdapterResponseTimeout):
|
|
549
628
|
if start_read_id == signal.identifier:
|
|
550
629
|
output_signal = None
|
|
551
630
|
break
|
|
552
631
|
# Otherwise ignore it
|
|
553
632
|
|
|
554
633
|
if output_signal is None:
|
|
555
|
-
# TODO : Make read_timeout always Timeout, never None ?
|
|
556
634
|
match read_timeout.action:
|
|
557
635
|
case TimeoutAction.RETURN_EMPTY:
|
|
558
636
|
t = time.time()
|
|
559
637
|
return AdapterReadPayload(
|
|
560
|
-
fragments=[Fragment(b
|
|
638
|
+
fragments=[Fragment(b"", t)],
|
|
561
639
|
stop_timestamp=t,
|
|
562
640
|
stop_condition_type=StopConditionType.TIMEOUT,
|
|
563
641
|
previous_read_buffer_used=False,
|
|
564
642
|
response_timestamp=None,
|
|
565
|
-
response_delay=None
|
|
643
|
+
response_delay=None,
|
|
566
644
|
)
|
|
567
645
|
case TimeoutAction.ERROR:
|
|
568
|
-
|
|
569
|
-
|
|
646
|
+
timeout_value = read_timeout.response()
|
|
647
|
+
raise AdapterTimeoutError(
|
|
648
|
+
float("nan") if timeout_value is None else timeout_value
|
|
570
649
|
)
|
|
571
650
|
case _:
|
|
572
651
|
raise NotImplementedError()
|
|
573
|
-
|
|
652
|
+
|
|
574
653
|
else:
|
|
575
654
|
return output_signal
|
|
576
655
|
|
|
@@ -583,7 +662,7 @@ class Adapter(ABC):
|
|
|
583
662
|
return signal.data()
|
|
584
663
|
|
|
585
664
|
def _cleanup(self) -> None:
|
|
586
|
-
if self.
|
|
665
|
+
if self._opened:
|
|
587
666
|
self.close()
|
|
588
667
|
|
|
589
668
|
def query_detailed(
|
|
@@ -598,7 +677,7 @@ class Adapter(ABC):
|
|
|
598
677
|
- write
|
|
599
678
|
- read
|
|
600
679
|
"""
|
|
601
|
-
self.
|
|
680
|
+
self.flush_read()
|
|
602
681
|
self.write(data)
|
|
603
682
|
return self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
|
|
604
683
|
|
syndesi/adapters/auto.py
CHANGED
|
@@ -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}")
|