syndesi 0.3.1__py3-none-any.whl → 0.4.0__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 +9 -2
- syndesi/adapters/__init__.py +0 -4
- syndesi/adapters/adapter.py +408 -242
- syndesi/adapters/auto.py +4 -3
- syndesi/adapters/backend/adapter_backend.py +151 -94
- syndesi/adapters/backend/adapter_manager.py +0 -1
- syndesi/adapters/backend/adapter_session.py +113 -129
- syndesi/adapters/backend/backend.py +160 -91
- syndesi/adapters/backend/backend_tools.py +30 -19
- syndesi/adapters/backend/descriptors.py +20 -8
- syndesi/adapters/backend/ip_backend.py +24 -13
- syndesi/adapters/backend/serialport_backend.py +44 -34
- syndesi/adapters/backend/stop_condition_backend.py +98 -219
- syndesi/adapters/backend/visa_backend.py +65 -47
- syndesi/adapters/ip.py +36 -16
- syndesi/adapters/serialport.py +18 -12
- syndesi/adapters/stop_condition.py +103 -42
- syndesi/adapters/timeout.py +55 -36
- syndesi/adapters/visa.py +19 -13
- syndesi/cli/backend_console.py +26 -18
- syndesi/cli/backend_status.py +137 -73
- syndesi/cli/backend_wrapper.py +18 -7
- syndesi/cli/console.py +281 -0
- syndesi/cli/shell.py +234 -258
- syndesi/cli/shell_tools.py +105 -0
- syndesi/cli/terminal_apps.py +0 -0
- syndesi/cli/terminal_tools.py +14 -0
- syndesi/protocols/__init__.py +0 -10
- syndesi/protocols/delimited.py +74 -47
- syndesi/protocols/modbus.py +132 -95
- syndesi/protocols/protocol.py +31 -22
- syndesi/protocols/raw.py +43 -21
- syndesi/protocols/scpi.py +42 -84
- syndesi/scripts/syndesi.py +9 -9
- syndesi/scripts/syndesi_backend.py +8 -14
- syndesi/tools/backend_api.py +74 -56
- syndesi/tools/backend_logger.py +19 -7
- syndesi/tools/errors.py +34 -1
- syndesi/tools/log.py +115 -31
- syndesi/tools/log_settings.py +1 -0
- syndesi/tools/types.py +14 -22
- syndesi/version.py +1 -1
- {syndesi-0.3.1.dist-info → syndesi-0.4.0.dist-info}/METADATA +35 -7
- syndesi-0.4.0.dist-info/RECORD +59 -0
- syndesi-0.4.0.dist-info/entry_points.txt +3 -0
- syndesi/cli/adapter_shell.py +0 -210
- syndesi-0.3.1.dist-info/RECORD +0 -56
- syndesi-0.3.1.dist-info/entry_points.txt +0 -3
- /syndesi/{adapters/backend/backend_status.py → cli/terminal.py} +0 -0
- {syndesi-0.3.1.dist-info → syndesi-0.4.0.dist-info}/WHEEL +0 -0
- {syndesi-0.3.1.dist-info → syndesi-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {syndesi-0.3.1.dist-info → syndesi-0.4.0.dist-info}/top_level.txt +0 -0
syndesi/adapters/adapter.py
CHANGED
|
@@ -15,63 +15,86 @@
|
|
|
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
|
|
18
19
|
import logging
|
|
19
20
|
import queue
|
|
20
21
|
import subprocess
|
|
21
22
|
import sys
|
|
22
23
|
import threading
|
|
23
24
|
import time
|
|
24
|
-
from typing import Any, Callable, Literal, Optional, Tuple, Union, overload
|
|
25
|
-
import uuid
|
|
26
25
|
import weakref
|
|
27
26
|
from abc import ABC, abstractmethod
|
|
28
|
-
from
|
|
29
|
-
|
|
30
|
-
from
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
from multiprocessing.connection import Client, Connection
|
|
29
|
+
from types import EllipsisType
|
|
30
|
+
from typing import Any
|
|
31
|
+
import os
|
|
32
|
+
|
|
33
|
+
from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
34
|
+
from syndesi.tools.types import NumberLike, is_number
|
|
35
|
+
|
|
36
|
+
from ..tools.backend_api import (
|
|
37
|
+
BACKEND_PORT,
|
|
38
|
+
Action,
|
|
39
|
+
BackendResponse,
|
|
40
|
+
default_host,
|
|
41
|
+
raise_if_error,
|
|
42
|
+
EXTRA_BUFFER_RESPONSE_TIME
|
|
43
|
+
)
|
|
33
44
|
from ..tools.log_settings import LoggerAlias
|
|
34
|
-
from .backend.adapter_backend import
|
|
35
|
-
|
|
45
|
+
from .backend.adapter_backend import (
|
|
46
|
+
AdapterDisconnected,
|
|
47
|
+
AdapterResponseTimeout,
|
|
48
|
+
AdapterReadPayload,
|
|
49
|
+
AdapterSignal,
|
|
50
|
+
)
|
|
51
|
+
from .backend.descriptors import Descriptor
|
|
52
|
+
from .stop_condition import StopCondition, Continuation, Total
|
|
36
53
|
from .timeout import Timeout, TimeoutAction, any_to_timeout
|
|
37
54
|
|
|
38
|
-
DEFAULT_STOP_CONDITION =
|
|
55
|
+
DEFAULT_STOP_CONDITION = Continuation(time=0.1)
|
|
39
56
|
|
|
40
|
-
DEFAULT_TIMEOUT = 5
|
|
41
|
-
|
|
42
|
-
SHUTDOWN_DELAY = 2
|
|
57
|
+
DEFAULT_TIMEOUT = Timeout(response=5, action='error')
|
|
43
58
|
|
|
44
59
|
# Maximum time to let the backend start
|
|
45
60
|
START_TIMEOUT = 2
|
|
61
|
+
# Time to shutdown the backend
|
|
62
|
+
SHUTDOWN_DELAY = 2
|
|
46
63
|
|
|
47
|
-
|
|
64
|
+
class SignalQueue(queue.Queue[AdapterSignal]):
|
|
65
|
+
def __init__(self) -> None:
|
|
66
|
+
self._read_payload_counter = 0
|
|
67
|
+
super().__init__(0)
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
def has_read_payload(self) -> bool:
|
|
70
|
+
return self._read_payload_counter > 0
|
|
51
71
|
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
def put(self, signal: AdapterSignal, block: bool = True, timeout: float | None = None) -> None:
|
|
74
|
+
if isinstance(signal, AdapterReadPayload):
|
|
75
|
+
self._read_payload_counter += 1
|
|
76
|
+
return super().put(signal, block, timeout)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
|
|
80
|
+
signal = super().get(block, timeout)
|
|
81
|
+
if isinstance(signal, AdapterReadPayload):
|
|
82
|
+
self._read_payload_counter -= 1
|
|
83
|
+
return signal
|
|
56
84
|
|
|
57
85
|
|
|
58
|
-
def is_backend_running(address
|
|
86
|
+
def is_backend_running(address: str, port: int) -> bool:
|
|
59
87
|
|
|
60
88
|
try:
|
|
61
|
-
conn = Client((
|
|
62
|
-
_default_host if address is None else address,
|
|
63
|
-
BACKEND_PORT if port is None else port
|
|
64
|
-
))
|
|
89
|
+
conn = Client((address, port))
|
|
65
90
|
except ConnectionRefusedError:
|
|
66
91
|
return False
|
|
67
92
|
else:
|
|
68
93
|
conn.close()
|
|
69
94
|
return True
|
|
70
95
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
subprocess.Popen(
|
|
74
|
-
[
|
|
96
|
+
def start_backend(port: int | None = None) -> None:
|
|
97
|
+
arguments = [
|
|
75
98
|
sys.executable,
|
|
76
99
|
"-m",
|
|
77
100
|
"syndesi.adapters.backend.backend",
|
|
@@ -79,26 +102,57 @@ def start_backend(port : Optional[int] = None) -> None:
|
|
|
79
102
|
str(SHUTDOWN_DELAY),
|
|
80
103
|
"-q",
|
|
81
104
|
"-p",
|
|
82
|
-
str(BACKEND_PORT if port is None else port)
|
|
105
|
+
str(BACKEND_PORT if port is None else port),
|
|
83
106
|
]
|
|
84
|
-
|
|
107
|
+
|
|
108
|
+
stdin = subprocess.DEVNULL
|
|
109
|
+
stdout = subprocess.DEVNULL
|
|
110
|
+
stderr = subprocess.DEVNULL
|
|
111
|
+
|
|
112
|
+
if os.name == "posix":
|
|
113
|
+
subprocess.Popen(
|
|
114
|
+
arguments,
|
|
115
|
+
stdin=stdin,
|
|
116
|
+
stdout=stdout,
|
|
117
|
+
stderr=stderr,
|
|
118
|
+
start_new_session=True,
|
|
119
|
+
close_fds=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
# Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
|
|
124
|
+
CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
|
|
125
|
+
DETACHED_PROCESS = 0x00000008 # not exposed by subprocess on all Pythons
|
|
126
|
+
# Optional: CREATE_NO_WINDOW (no window even for console apps)
|
|
127
|
+
CREATE_NO_WINDOW = 0x08000000
|
|
128
|
+
|
|
129
|
+
creationflags = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS | CREATE_NO_WINDOW
|
|
130
|
+
|
|
131
|
+
subprocess.Popen(
|
|
132
|
+
arguments,
|
|
133
|
+
stdin=stdin,
|
|
134
|
+
stdout=stdout,
|
|
135
|
+
stderr=stderr,
|
|
136
|
+
creationflags=creationflags,
|
|
137
|
+
close_fds=True,
|
|
138
|
+
)
|
|
85
139
|
|
|
140
|
+
class ReadScope(Enum):
|
|
141
|
+
NEXT = 'next'
|
|
142
|
+
BUFFERED = 'buffered'
|
|
86
143
|
|
|
87
144
|
class Adapter(ABC):
|
|
88
|
-
ADDITIONNAL_RESPONSE_DELAY = 1
|
|
89
|
-
BACKEND_REQUEST_DEFAULT_TIMEOUT = 1
|
|
90
|
-
|
|
91
145
|
def __init__(
|
|
92
146
|
self,
|
|
93
147
|
descriptor: Descriptor,
|
|
94
148
|
alias: str = "",
|
|
95
|
-
|
|
96
|
-
timeout:
|
|
149
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
150
|
+
timeout: Timeout | EllipsisType | NumberLike | None = ...,
|
|
97
151
|
encoding: str = "utf-8",
|
|
98
|
-
event_callback
|
|
99
|
-
auto_open
|
|
100
|
-
backend_address
|
|
101
|
-
backend_port
|
|
152
|
+
event_callback: Callable[[AdapterSignal], None] | None = None,
|
|
153
|
+
auto_open: bool = True,
|
|
154
|
+
backend_address: str | None = None,
|
|
155
|
+
backend_port: int | None = None,
|
|
102
156
|
) -> None:
|
|
103
157
|
"""
|
|
104
158
|
Adapter instance
|
|
@@ -118,61 +172,57 @@ class Adapter(ABC):
|
|
|
118
172
|
super().__init__()
|
|
119
173
|
self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
|
|
120
174
|
self.encoding = encoding
|
|
121
|
-
self.
|
|
122
|
-
self.event_callback
|
|
175
|
+
self._signal_queue: SignalQueue = SignalQueue()
|
|
176
|
+
self.event_callback: Callable[[AdapterSignal], None] | None = event_callback
|
|
177
|
+
self.backend_connection: Connection | None = None
|
|
123
178
|
self._backend_connection_lock = threading.Lock()
|
|
124
|
-
self._make_backend_request_queue
|
|
179
|
+
self._make_backend_request_queue: queue.Queue[BackendResponse] = queue.Queue()
|
|
125
180
|
self._make_backend_request_flag = threading.Event()
|
|
126
181
|
self.opened = False
|
|
182
|
+
self._alias = alias
|
|
127
183
|
|
|
128
|
-
if
|
|
129
|
-
self.
|
|
130
|
-
elif backend_address is not None:
|
|
131
|
-
raise RuntimeError(f"Cannot connect to backend {backend_address}")
|
|
184
|
+
if backend_address is None:
|
|
185
|
+
self._backend_address = default_host
|
|
132
186
|
else:
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
187
|
+
self._backend_address = backend_address
|
|
188
|
+
if backend_port is None:
|
|
189
|
+
self._backend_port = BACKEND_PORT
|
|
190
|
+
else:
|
|
191
|
+
self._backend_port = backend_port
|
|
192
|
+
|
|
193
|
+
# There a two possibilities here
|
|
194
|
+
# A) The descriptor is fully initialized
|
|
195
|
+
# -> The adapter can be connected directly
|
|
196
|
+
# B) The descriptor is not fully initialized
|
|
197
|
+
# -> Wait for the protocol to set defaults and then connect the adapter
|
|
144
198
|
|
|
145
199
|
assert isinstance(
|
|
146
200
|
descriptor, Descriptor
|
|
147
201
|
), "descriptor must be a Descriptor class"
|
|
148
202
|
self.descriptor = descriptor
|
|
203
|
+
self.auto_open = auto_open
|
|
149
204
|
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
self._make_backend_request(Action.SET_ROLE_ADAPTER)
|
|
164
|
-
|
|
165
|
-
# Set the adapter
|
|
166
|
-
self._make_backend_request(Action.SELECT_ADAPTER, str(self.descriptor))
|
|
167
|
-
|
|
168
|
-
self._alias = alias
|
|
205
|
+
# Set the stop-condition
|
|
206
|
+
self._stop_conditions: list[StopCondition]
|
|
207
|
+
if stop_conditions is ...:
|
|
208
|
+
self._default_stop_condition = True
|
|
209
|
+
self._stop_conditions = [DEFAULT_STOP_CONDITION]
|
|
210
|
+
else:
|
|
211
|
+
self._default_stop_condition = False
|
|
212
|
+
if isinstance(stop_conditions, StopCondition):
|
|
213
|
+
self._stop_conditions = [stop_conditions]
|
|
214
|
+
elif isinstance(stop_conditions, list):
|
|
215
|
+
self._stop_conditions = stop_conditions
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError('Invalid stop_conditions')
|
|
169
218
|
|
|
170
219
|
# Set the timeout
|
|
171
220
|
self.is_default_timeout = False
|
|
172
|
-
self._timeout
|
|
221
|
+
self._timeout: Timeout | None
|
|
173
222
|
if timeout is Ellipsis:
|
|
174
223
|
# Not set
|
|
175
224
|
self.is_default_timeout = True
|
|
225
|
+
self._timeout = DEFAULT_TIMEOUT
|
|
176
226
|
elif isinstance(timeout, Timeout):
|
|
177
227
|
self._timeout = timeout
|
|
178
228
|
elif is_number(timeout):
|
|
@@ -180,41 +230,82 @@ class Adapter(ABC):
|
|
|
180
230
|
elif timeout is None:
|
|
181
231
|
self._timeout = timeout
|
|
182
232
|
|
|
183
|
-
|
|
184
|
-
# Set the stop-condition
|
|
185
|
-
self._stop_condition : Optional[StopCondition]
|
|
186
|
-
if stop_condition is DEFAULT:
|
|
187
|
-
self._default_stop_condition = True
|
|
188
|
-
self._stop_condition = DEFAULT_STOP_CONDITION
|
|
189
|
-
else:
|
|
190
|
-
self._default_stop_condition = False
|
|
191
|
-
self._stop_condition = stop_condition
|
|
192
|
-
|
|
193
233
|
# Buffer for data that has been pulled from the queue but
|
|
194
234
|
# not used because of termination or length stop condition
|
|
195
235
|
self._previous_buffer = b""
|
|
196
236
|
|
|
237
|
+
if self.descriptor.is_initialized():
|
|
238
|
+
self.connect()
|
|
239
|
+
|
|
197
240
|
weakref.finalize(self, self._cleanup)
|
|
198
241
|
self._init_ok = True
|
|
199
|
-
|
|
200
|
-
if auto_open
|
|
242
|
+
|
|
243
|
+
# We can auto-open only if auto_open is enabled and if
|
|
244
|
+
# connection with the backend has been made (descriptor initialized)
|
|
245
|
+
if self.auto_open and self.backend_connection is not None:
|
|
201
246
|
self.open()
|
|
202
247
|
|
|
203
|
-
def
|
|
248
|
+
def connect(self) -> None:
|
|
249
|
+
if self.backend_connection is not None:
|
|
250
|
+
# No need to connect, everything has been done already
|
|
251
|
+
return
|
|
252
|
+
if not self.descriptor.is_initialized():
|
|
253
|
+
raise RuntimeError("Descriptor wasn't initialized fully")
|
|
254
|
+
|
|
255
|
+
if is_backend_running(self._backend_address, self._backend_port):
|
|
256
|
+
self._logger.info("Backend already running")
|
|
257
|
+
else:
|
|
258
|
+
self._logger.info("Starting backend...")
|
|
259
|
+
start_backend(self._backend_port)
|
|
260
|
+
start = time.time()
|
|
261
|
+
while time.time() < (start + START_TIMEOUT):
|
|
262
|
+
if is_backend_running(self._backend_address, self._backend_port):
|
|
263
|
+
self._logger.info("Backend started")
|
|
264
|
+
break
|
|
265
|
+
time.sleep(0.1)
|
|
266
|
+
else:
|
|
267
|
+
# Backend could not start
|
|
268
|
+
self._logger.error("Could not start backend")
|
|
269
|
+
|
|
270
|
+
# Create the client to communicate with the backend
|
|
271
|
+
try:
|
|
272
|
+
self.backend_connection = Client((default_host, BACKEND_PORT))
|
|
273
|
+
except ConnectionRefusedError as err:
|
|
274
|
+
raise RuntimeError("Failed to connect to backend") from err
|
|
275
|
+
self._read_thread = threading.Thread(
|
|
276
|
+
target=self.read_thread,
|
|
277
|
+
args=(self._signal_queue, self._make_backend_request_queue),
|
|
278
|
+
daemon=True,
|
|
279
|
+
)
|
|
280
|
+
self._read_thread.start()
|
|
281
|
+
|
|
282
|
+
# Identify ourselves
|
|
283
|
+
self._make_backend_request(Action.SET_ROLE_ADAPTER)
|
|
284
|
+
|
|
285
|
+
# Set the adapter
|
|
286
|
+
self._make_backend_request(Action.SELECT_ADAPTER, str(self.descriptor))
|
|
287
|
+
|
|
288
|
+
if self.auto_open:
|
|
289
|
+
self.open()
|
|
290
|
+
|
|
291
|
+
def _make_backend_request(self, action: Action, *args: Any) -> BackendResponse:
|
|
204
292
|
"""
|
|
205
293
|
Send a request to the backend and return the arguments
|
|
206
294
|
"""
|
|
207
295
|
|
|
208
296
|
with self._backend_connection_lock:
|
|
209
|
-
self.backend_connection
|
|
297
|
+
if self.backend_connection is not None:
|
|
298
|
+
self.backend_connection.send((action.value, *args))
|
|
210
299
|
|
|
211
300
|
self._make_backend_request_flag.set()
|
|
212
301
|
try:
|
|
213
302
|
response = self._make_backend_request_queue.get(
|
|
214
|
-
timeout=
|
|
303
|
+
timeout=BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
215
304
|
)
|
|
216
|
-
except queue.Empty:
|
|
217
|
-
raise RuntimeError(
|
|
305
|
+
except queue.Empty as err:
|
|
306
|
+
raise RuntimeError(
|
|
307
|
+
f"Failed to receive response from backend to {action}"
|
|
308
|
+
) from err
|
|
218
309
|
|
|
219
310
|
assert (
|
|
220
311
|
isinstance(response, tuple) and len(response) > 0
|
|
@@ -223,23 +314,33 @@ class Adapter(ABC):
|
|
|
223
314
|
|
|
224
315
|
return response[1:]
|
|
225
316
|
|
|
226
|
-
def read_thread(
|
|
317
|
+
def read_thread(
|
|
318
|
+
self,
|
|
319
|
+
signal_queue: SignalQueue,
|
|
320
|
+
request_queue: queue.Queue[BackendResponse],
|
|
321
|
+
) -> None:
|
|
227
322
|
while True:
|
|
228
323
|
try:
|
|
229
|
-
|
|
324
|
+
if self.backend_connection is None:
|
|
325
|
+
raise RuntimeError("Backend connection wasn't initialized")
|
|
326
|
+
response: tuple[Any, ...] = self.backend_connection.recv()
|
|
230
327
|
except (EOFError, TypeError, OSError):
|
|
231
|
-
|
|
328
|
+
signal_queue.put(AdapterDisconnected())
|
|
232
329
|
request_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
|
|
330
|
+
break
|
|
233
331
|
else:
|
|
234
332
|
if not isinstance(response, tuple):
|
|
235
333
|
raise RuntimeError(f"Invalid response from backend : {response}")
|
|
236
334
|
action = Action(response[0])
|
|
237
335
|
|
|
238
|
-
if
|
|
336
|
+
if action == Action.ADAPTER_SIGNAL:
|
|
337
|
+
#if is_event(action):
|
|
338
|
+
if len(response) <= 1:
|
|
339
|
+
raise RuntimeError(f"Invalid event response : {response}")
|
|
340
|
+
signal: AdapterSignal = response[1]
|
|
239
341
|
if self.event_callback is not None:
|
|
240
|
-
signal : AdapterSignal = response[1]
|
|
241
342
|
self.event_callback(signal)
|
|
242
|
-
|
|
343
|
+
signal_queue.put(signal)
|
|
243
344
|
else:
|
|
244
345
|
request_queue.put(response)
|
|
245
346
|
|
|
@@ -247,7 +348,7 @@ class Adapter(ABC):
|
|
|
247
348
|
def _default_timeout(self) -> Timeout:
|
|
248
349
|
pass
|
|
249
350
|
|
|
250
|
-
def set_timeout(self, timeout:
|
|
351
|
+
def set_timeout(self, timeout: Timeout | None) -> None:
|
|
251
352
|
"""
|
|
252
353
|
Overwrite timeout
|
|
253
354
|
|
|
@@ -257,7 +358,7 @@ class Adapter(ABC):
|
|
|
257
358
|
"""
|
|
258
359
|
self._timeout = timeout
|
|
259
360
|
|
|
260
|
-
def set_default_timeout(self, default_timeout:
|
|
361
|
+
def set_default_timeout(self, default_timeout: Timeout | None) -> None:
|
|
261
362
|
"""
|
|
262
363
|
Set the default timeout for this adapter. If a previous timeout has been set, it will be fused
|
|
263
364
|
|
|
@@ -269,7 +370,7 @@ class Adapter(ABC):
|
|
|
269
370
|
self._logger.debug(f"Setting default timeout to {default_timeout}")
|
|
270
371
|
self._timeout = default_timeout
|
|
271
372
|
|
|
272
|
-
def
|
|
373
|
+
def set_stop_conditions(self, stop_conditions: StopCondition | None | list[StopCondition]) -> None:
|
|
273
374
|
"""
|
|
274
375
|
Overwrite the stop-condition
|
|
275
376
|
|
|
@@ -277,17 +378,16 @@ class Adapter(ABC):
|
|
|
277
378
|
----------
|
|
278
379
|
stop_condition : StopCondition
|
|
279
380
|
"""
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
381
|
+
if isinstance(stop_conditions, list):
|
|
382
|
+
self._stop_conditions = stop_conditions
|
|
383
|
+
elif isinstance(stop_conditions, StopCondition):
|
|
384
|
+
self._stop_conditions = [stop_conditions]
|
|
385
|
+
elif stop_conditions is None:
|
|
386
|
+
self._stop_conditions = []
|
|
387
|
+
|
|
388
|
+
self._make_backend_request(Action.SET_STOP_CONDITION, self._stop_conditions)
|
|
289
389
|
|
|
290
|
-
def set_default_stop_condition(self, stop_condition
|
|
390
|
+
def set_default_stop_condition(self, stop_condition: StopCondition) -> None:
|
|
291
391
|
"""
|
|
292
392
|
Set the default stop condition for this adapter.
|
|
293
393
|
|
|
@@ -296,7 +396,7 @@ class Adapter(ABC):
|
|
|
296
396
|
stop_condition : StopCondition
|
|
297
397
|
"""
|
|
298
398
|
if self._default_stop_condition:
|
|
299
|
-
self.
|
|
399
|
+
self.set_stop_conditions(stop_condition)
|
|
300
400
|
|
|
301
401
|
def flushRead(self) -> None:
|
|
302
402
|
"""
|
|
@@ -305,6 +405,12 @@ class Adapter(ABC):
|
|
|
305
405
|
self._make_backend_request(
|
|
306
406
|
Action.FLUSHREAD,
|
|
307
407
|
)
|
|
408
|
+
while True:
|
|
409
|
+
try:
|
|
410
|
+
self._signal_queue.get(block=False)
|
|
411
|
+
except queue.Empty:
|
|
412
|
+
break
|
|
413
|
+
|
|
308
414
|
|
|
309
415
|
def previous_read_buffer_empty(self) -> bool:
|
|
310
416
|
"""
|
|
@@ -320,14 +426,7 @@ class Adapter(ABC):
|
|
|
320
426
|
"""
|
|
321
427
|
Start communication with the device
|
|
322
428
|
"""
|
|
323
|
-
|
|
324
|
-
payload = None
|
|
325
|
-
else:
|
|
326
|
-
payload = self._stop_condition.compose_json()
|
|
327
|
-
self._make_backend_request(
|
|
328
|
-
Action.OPEN,
|
|
329
|
-
payload
|
|
330
|
-
)
|
|
429
|
+
self._make_backend_request(Action.OPEN, self._stop_conditions)
|
|
331
430
|
self._logger.info("Adapter opened")
|
|
332
431
|
self.opened = True
|
|
333
432
|
|
|
@@ -335,14 +434,15 @@ class Adapter(ABC):
|
|
|
335
434
|
"""
|
|
336
435
|
Stop communication with the device
|
|
337
436
|
"""
|
|
338
|
-
self._logger.debug("Closing adapter frontend")
|
|
339
|
-
self._make_backend_request(Action.CLOSE)
|
|
340
437
|
if force:
|
|
438
|
+
self._logger.debug("Closing adapter frontend")
|
|
439
|
+
else:
|
|
341
440
|
self._logger.debug("Force closing adapter backend")
|
|
342
|
-
|
|
441
|
+
self._make_backend_request(Action.CLOSE, force)
|
|
343
442
|
|
|
344
443
|
with self._backend_connection_lock:
|
|
345
|
-
self.backend_connection
|
|
444
|
+
if self.backend_connection is not None:
|
|
445
|
+
self.backend_connection.close()
|
|
346
446
|
|
|
347
447
|
self.opened = False
|
|
348
448
|
|
|
@@ -358,29 +458,13 @@ class Adapter(ABC):
|
|
|
358
458
|
if isinstance(data, str):
|
|
359
459
|
data = data.encode(self.encoding)
|
|
360
460
|
self._make_backend_request(Action.WRITE, data)
|
|
361
|
-
|
|
362
|
-
@overload
|
|
363
|
-
def read( #type: ignore (default arguments mess everything up)
|
|
364
|
-
self,
|
|
365
|
-
timeout: Union[Timeout, DefaultType, None] = DEFAULT,
|
|
366
|
-
stop_condition: Union[StopCondition, DefaultType, None] = DEFAULT,
|
|
367
|
-
full_output: Literal[True] = True,
|
|
368
|
-
) -> Tuple[bytes, AdapterSignal]: ...
|
|
369
461
|
|
|
370
|
-
|
|
371
|
-
def read(
|
|
462
|
+
def read_detailed(
|
|
372
463
|
self,
|
|
373
|
-
timeout:
|
|
374
|
-
stop_condition:
|
|
375
|
-
|
|
376
|
-
) ->
|
|
377
|
-
|
|
378
|
-
def read(
|
|
379
|
-
self,
|
|
380
|
-
timeout: Union[None, Timeout, DefaultType] = DEFAULT,
|
|
381
|
-
stop_condition: Optional[Union[StopCondition, DefaultType]] = DEFAULT,
|
|
382
|
-
full_output: bool = False,
|
|
383
|
-
) -> Union[bytes, Tuple[bytes, AdapterSignal]]:
|
|
464
|
+
timeout: Timeout | EllipsisType | None = ...,
|
|
465
|
+
stop_condition: StopCondition | EllipsisType | None = ...,
|
|
466
|
+
scope : str = ReadScope.BUFFERED.value,
|
|
467
|
+
) -> AdapterReadPayload | None:
|
|
384
468
|
"""
|
|
385
469
|
Read data from the device
|
|
386
470
|
|
|
@@ -390,117 +474,192 @@ class Adapter(ABC):
|
|
|
390
474
|
Temporary timeout
|
|
391
475
|
stop_condition : StopCondition
|
|
392
476
|
Temporary stop condition
|
|
393
|
-
|
|
394
|
-
|
|
477
|
+
scope : str
|
|
478
|
+
Return previous data ('buffered') or only future data ('next')
|
|
395
479
|
Returns
|
|
396
480
|
-------
|
|
397
481
|
data : bytes
|
|
398
|
-
|
|
399
|
-
Only if full_output is True
|
|
482
|
+
signal : AdapterReadPayload
|
|
400
483
|
"""
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
484
|
+
t = time.time()
|
|
485
|
+
_scope = ReadScope(scope)
|
|
486
|
+
output_signal = None
|
|
487
|
+
|
|
488
|
+
# First, we check if data is in the buffer and if the scope if set to BUFFERED
|
|
489
|
+
while _scope == ReadScope.BUFFERED and self._signal_queue.has_read_payload():
|
|
490
|
+
signal = self._signal_queue.get()
|
|
491
|
+
if isinstance(signal, AdapterReadPayload):
|
|
492
|
+
output_signal = signal
|
|
493
|
+
break
|
|
494
|
+
# TODO : Implement disconnect ?
|
|
404
495
|
else:
|
|
405
|
-
|
|
496
|
+
# Nothing was found, ask the backend with a START_READ request. The backend will
|
|
497
|
+
# 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)
|
|
406
502
|
|
|
407
|
-
|
|
503
|
+
if read_timeout is not None:
|
|
504
|
+
if not read_timeout.is_initialized():
|
|
505
|
+
raise RuntimeError("Timeout needs to be initialized")
|
|
408
506
|
|
|
409
|
-
|
|
507
|
+
_response = read_timeout.response()
|
|
410
508
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
elif read_timeout.response is None:
|
|
414
|
-
queue_timeout_timestamp = None
|
|
415
|
-
else:
|
|
416
|
-
if read_timeout.response is DEFAULT:
|
|
417
|
-
raise RuntimeError('Timeout needs to be initialized')
|
|
418
|
-
|
|
419
|
-
queue_timeout_timestamp = (
|
|
420
|
-
time.time() + read_timeout.response + self.ADDITIONNAL_RESPONSE_DELAY
|
|
421
|
-
)
|
|
509
|
+
read_init_time = time.time()
|
|
510
|
+
start_read_id = self._make_backend_request(Action.START_READ, _response)[0]
|
|
422
511
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
)
|
|
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
|
|
430
518
|
|
|
431
|
-
while True:
|
|
432
|
-
if queue_timeout_timestamp is None or response_received:
|
|
433
|
-
queue_timeout = None
|
|
434
519
|
else:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
queue_timeout = 0
|
|
520
|
+
start_read_id = None
|
|
521
|
+
read_init_time = None
|
|
438
522
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
523
|
+
|
|
524
|
+
while True:
|
|
525
|
+
try:
|
|
526
|
+
if read_stop_timestamp is None:
|
|
527
|
+
queue_timeout = None
|
|
528
|
+
else:
|
|
529
|
+
queue_timeout = max(0, read_stop_timestamp - time.time() + EXTRA_BUFFER_RESPONSE_TIME)
|
|
530
|
+
|
|
531
|
+
signal = self._signal_queue.get(timeout=queue_timeout)
|
|
532
|
+
except queue.Empty:
|
|
533
|
+
raise RuntimeError('Failed to receive response from backend')
|
|
447
534
|
if isinstance(signal, AdapterReadPayload):
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
break
|
|
451
|
-
elif isinstance(signal, AdapterReadInit):
|
|
452
|
-
#signal: AdapterReadInit = response[1]
|
|
453
|
-
# Check if it's the right read_init with the uuid, otherwise ignore it
|
|
454
|
-
if signal.uuid == start_read_uuid:
|
|
455
|
-
if signal.received_response_in_time:
|
|
456
|
-
response_received = True
|
|
457
|
-
else:
|
|
458
|
-
if self._timeout is None:
|
|
459
|
-
raise RuntimeError('Failed to receive data in time but timeout is None')
|
|
460
|
-
else:
|
|
461
|
-
if self._timeout.action == TimeoutAction.RETURN:
|
|
462
|
-
output = b""
|
|
463
|
-
break
|
|
464
|
-
elif self._timeout.action == TimeoutAction.ERROR:
|
|
465
|
-
raise TimeoutError(
|
|
466
|
-
f"No response received from device within {self._timeout.response} seconds"
|
|
467
|
-
)
|
|
535
|
+
output_signal = signal
|
|
536
|
+
break
|
|
468
537
|
elif isinstance(signal, AdapterDisconnected):
|
|
469
538
|
raise RuntimeError("Adapter disconnected")
|
|
539
|
+
elif isinstance(signal, AdapterResponseTimeout):
|
|
540
|
+
if start_read_id == signal.identifier:
|
|
541
|
+
output_signal = None
|
|
542
|
+
break
|
|
543
|
+
# Otherwise ignore it
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
if output_signal is None:
|
|
547
|
+
# TODO : Make read_timeout always Timeout, never None ?
|
|
548
|
+
match read_timeout.action:
|
|
549
|
+
case TimeoutAction.RETURN:
|
|
550
|
+
return None
|
|
551
|
+
case TimeoutAction.ERROR:
|
|
552
|
+
raise TimeoutError(
|
|
553
|
+
f"No response received from device within {read_timeout.response()} seconds"
|
|
554
|
+
)
|
|
555
|
+
case _:
|
|
556
|
+
raise NotImplementedError()
|
|
557
|
+
|
|
558
|
+
else:
|
|
559
|
+
return output_signal
|
|
560
|
+
|
|
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
|
|
470
641
|
|
|
471
|
-
|
|
472
|
-
|
|
642
|
+
def read(
|
|
643
|
+
self,
|
|
644
|
+
timeout: Timeout | EllipsisType | None = ...,
|
|
645
|
+
stop_condition: StopCondition | EllipsisType | None = ...,
|
|
646
|
+
) -> bytes:
|
|
647
|
+
signal = self.read_detailed(timeout=timeout, stop_condition=stop_condition)
|
|
648
|
+
if signal is None:
|
|
649
|
+
return None
|
|
473
650
|
else:
|
|
474
|
-
return
|
|
651
|
+
return signal.data()
|
|
475
652
|
|
|
476
653
|
def _cleanup(self) -> None:
|
|
477
654
|
if self._init_ok and self.opened:
|
|
478
655
|
self.close()
|
|
479
656
|
|
|
480
|
-
|
|
481
|
-
def query(
|
|
482
|
-
self,
|
|
483
|
-
data: Union[bytes, str],
|
|
484
|
-
timeout: Optional[Union[Timeout, DefaultType]] = DEFAULT,
|
|
485
|
-
stop_condition: Optional[Union[StopCondition, DefaultType]] = DEFAULT,
|
|
486
|
-
full_output: Literal[True] = True,
|
|
487
|
-
) -> Tuple[bytes, AdapterSignal]: ...
|
|
488
|
-
@overload
|
|
489
|
-
def query(
|
|
490
|
-
self,
|
|
491
|
-
data: Union[bytes, str],
|
|
492
|
-
timeout: Optional[Union[Timeout, DefaultType]] = DEFAULT,
|
|
493
|
-
stop_condition: Optional[Union[StopCondition, DefaultType]] = DEFAULT,
|
|
494
|
-
full_output: Literal[False] = False,
|
|
495
|
-
) -> bytes: ...
|
|
496
|
-
|
|
497
|
-
def query(
|
|
657
|
+
def query_detailed(
|
|
498
658
|
self,
|
|
499
|
-
data:
|
|
500
|
-
timeout:
|
|
501
|
-
stop_condition:
|
|
502
|
-
|
|
503
|
-
) -> Union[bytes, Tuple[bytes, AdapterSignal]]:
|
|
659
|
+
data: bytes | str,
|
|
660
|
+
timeout: Timeout | EllipsisType | None = ...,
|
|
661
|
+
stop_condition: StopCondition | EllipsisType | None = ...,
|
|
662
|
+
) -> tuple[bytes, AdapterReadPayload | None]:
|
|
504
663
|
"""
|
|
505
664
|
Shortcut function that combines
|
|
506
665
|
- flush_read
|
|
@@ -509,16 +668,23 @@ class Adapter(ABC):
|
|
|
509
668
|
"""
|
|
510
669
|
self.flushRead()
|
|
511
670
|
self.write(data)
|
|
512
|
-
|
|
513
|
-
timeout=timeout, stop_condition=stop_condition, full_output=True
|
|
514
|
-
)
|
|
671
|
+
return self.read_detailed(timeout=timeout, stop_condition=stop_condition)
|
|
515
672
|
|
|
516
|
-
|
|
517
|
-
|
|
673
|
+
def query(
|
|
674
|
+
self,
|
|
675
|
+
data: bytes | str,
|
|
676
|
+
timeout: Timeout | EllipsisType | None = ...,
|
|
677
|
+
stop_condition: StopCondition | EllipsisType | None = ...,
|
|
678
|
+
) -> bytes:
|
|
679
|
+
signal = self.query_detailed(
|
|
680
|
+
data=data, timeout=timeout, stop_condition=stop_condition
|
|
681
|
+
)
|
|
682
|
+
if signal is None:
|
|
683
|
+
return None
|
|
518
684
|
else:
|
|
519
|
-
return
|
|
685
|
+
return signal.data()
|
|
520
686
|
|
|
521
|
-
def set_event_callback(self, callback
|
|
687
|
+
def set_event_callback(self, callback: Callable[[AdapterSignal], None]) -> None:
|
|
522
688
|
self.event_callback = callback
|
|
523
689
|
|
|
524
690
|
def __str__(self) -> str:
|