syndesi 0.4.4__py3-none-any.whl → 0.5.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 +6 -1
- syndesi/adapters/adapter.py +318 -550
- syndesi/adapters/adapter_worker.py +820 -0
- syndesi/adapters/auto.py +32 -10
- syndesi/adapters/descriptors.py +38 -0
- syndesi/adapters/ip.py +197 -71
- syndesi/adapters/serialport.py +136 -20
- syndesi/adapters/stop_conditions.py +354 -0
- syndesi/adapters/timeout.py +57 -21
- syndesi/adapters/visa.py +227 -10
- syndesi/cli/console.py +50 -15
- syndesi/cli/shell.py +95 -47
- syndesi/cli/terminal_tools.py +8 -8
- syndesi/component.py +267 -31
- syndesi/protocols/delimited.py +92 -107
- syndesi/protocols/modbus.py +2370 -871
- syndesi/protocols/protocol.py +184 -37
- syndesi/protocols/raw.py +45 -62
- syndesi/protocols/scpi.py +65 -102
- syndesi/remote/remote.py +188 -0
- syndesi/scripts/syndesi.py +11 -1
- syndesi/tools/errors.py +31 -33
- syndesi/tools/log_settings.py +21 -8
- syndesi/tools/{log.py → logmanager.py} +23 -13
- syndesi/tools/types.py +8 -7
- syndesi/version.py +1 -1
- {syndesi-0.4.4.dist-info → syndesi-0.5.0.dist-info}/METADATA +1 -1
- syndesi-0.5.0.dist-info/RECORD +41 -0
- syndesi/adapters/backend/__init__.py +0 -0
- syndesi/adapters/backend/adapter_backend.py +0 -438
- syndesi/adapters/backend/adapter_manager.py +0 -48
- syndesi/adapters/backend/adapter_session.py +0 -345
- syndesi/adapters/backend/backend.py +0 -443
- syndesi/adapters/backend/backend_status.py +0 -0
- syndesi/adapters/backend/backend_tools.py +0 -66
- syndesi/adapters/backend/descriptors.py +0 -153
- syndesi/adapters/backend/ip_backend.py +0 -152
- syndesi/adapters/backend/serialport_backend.py +0 -246
- syndesi/adapters/backend/stop_condition_backend.py +0 -222
- syndesi/adapters/backend/timed_queue.py +0 -39
- syndesi/adapters/backend/timeout.py +0 -252
- syndesi/adapters/backend/visa_backend.py +0 -197
- syndesi/adapters/ip_server.py +0 -108
- syndesi/adapters/stop_condition.py +0 -114
- syndesi/cli/backend_console.py +0 -96
- syndesi/cli/backend_status.py +0 -274
- syndesi/cli/backend_wrapper.py +0 -61
- syndesi/scripts/syndesi_backend.py +0 -37
- syndesi/tools/backend_api.py +0 -182
- syndesi/tools/backend_logger.py +0 -64
- syndesi/tools/exceptions.py +0 -16
- syndesi/tools/internal.py +0 -0
- syndesi-0.4.4.dist-info/RECORD +0 -61
- {syndesi-0.4.4.dist-info → syndesi-0.5.0.dist-info}/WHEEL +0 -0
- {syndesi-0.4.4.dist-info → syndesi-0.5.0.dist-info}/entry_points.txt +0 -0
- {syndesi-0.4.4.dist-info → syndesi-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {syndesi-0.4.4.dist-info → syndesi-0.5.0.dist-info}/top_level.txt +0 -0
syndesi/adapters/adapter.py
CHANGED
|
@@ -1,702 +1,470 @@
|
|
|
1
|
-
# File :
|
|
1
|
+
# File : adapter.py
|
|
2
2
|
# Author : Sébastien Deriaz
|
|
3
3
|
# License : GPL
|
|
4
4
|
|
|
5
5
|
"""
|
|
6
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
7
|
|
|
12
|
-
|
|
13
|
-
This is a limitation as it is to this day not possible to communicate "raw"
|
|
14
|
-
with a device through USB yet
|
|
8
|
+
The user calls methods of the Adapter class synchronously.
|
|
15
9
|
|
|
16
10
|
An adapter is meant to work with bytes objects but it can accept strings.
|
|
17
11
|
Strings will automatically be converted to bytes using utf-8 encoding
|
|
12
|
+
|
|
13
|
+
Each adapter contains a worker thread that monitors the low-level communication layers.
|
|
14
|
+
This approach allows for precise time management (when each fragment is sent/received) and allows
|
|
15
|
+
for asynchronous events (fragment received).
|
|
16
|
+
|
|
17
|
+
Async facade:
|
|
18
|
+
- aopen/awrite/aread/aread_detailed simply await the SAME underlying worker-thread commands
|
|
19
|
+
using asyncio.wrap_future (no extra threads are spawned).
|
|
18
20
|
"""
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
# NOTE:
|
|
23
|
+
# This version removes the "worker publishes events into a queue that read_detailed consumes".
|
|
24
|
+
# Instead:
|
|
25
|
+
# - The worker continuously assembles AdapterFrame from fragments (as before).
|
|
26
|
+
# - A read_detailed command registers a "pending read" inside the worker.
|
|
27
|
+
# - When a frame completes, the worker either:
|
|
28
|
+
# * completes the pending read future, OR
|
|
29
|
+
# * buffers the frame for later buffered reads, and optionally calls the callback.
|
|
30
|
+
#
|
|
31
|
+
# This avoids having a sync queue AND an async queue, and makes async wrappers trivial.
|
|
32
|
+
|
|
33
|
+
import asyncio
|
|
25
34
|
import threading
|
|
26
|
-
import time
|
|
27
35
|
import weakref
|
|
28
|
-
from abc import
|
|
36
|
+
from abc import abstractmethod
|
|
29
37
|
from collections.abc import Callable
|
|
30
38
|
from enum import Enum
|
|
31
|
-
from multiprocessing.connection import Client, Connection
|
|
32
39
|
from types import EllipsisType
|
|
33
|
-
from typing import Any
|
|
34
|
-
|
|
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
|
|
43
40
|
|
|
44
|
-
from
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
EXTRA_BUFFER_RESPONSE_TIME,
|
|
48
|
-
Action,
|
|
49
|
-
BackendResponse,
|
|
50
|
-
Fragment,
|
|
51
|
-
default_host,
|
|
52
|
-
raise_if_error,
|
|
53
|
-
)
|
|
41
|
+
from syndesi.tools.errors import AdapterError
|
|
42
|
+
|
|
43
|
+
from ..component import AdapterFrame, Component, Descriptor, ReadScope
|
|
54
44
|
from ..tools.log_settings import LoggerAlias
|
|
55
|
-
from .
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
from ..tools.types import NumberLike, is_number
|
|
46
|
+
from .adapter_worker import (
|
|
47
|
+
AdapterEvent,
|
|
48
|
+
AdapterWorker,
|
|
49
|
+
CloseCommand,
|
|
50
|
+
FlushReadCommand,
|
|
51
|
+
IsOpenCommand,
|
|
52
|
+
OpenCommand,
|
|
53
|
+
ReadCommand,
|
|
54
|
+
SetDescriptorCommand,
|
|
55
|
+
SetEventCallbackCommand,
|
|
56
|
+
SetStopConditionsCommand,
|
|
57
|
+
SetTimeoutCommand,
|
|
58
|
+
StopThreadCommand,
|
|
59
|
+
WriteCommand,
|
|
60
60
|
)
|
|
61
|
-
from .
|
|
62
|
-
from .backend.descriptors import Descriptor
|
|
63
|
-
from .stop_condition import Continuation, StopCondition, StopConditionType
|
|
61
|
+
from .stop_conditions import Fragment, StopCondition
|
|
64
62
|
from .timeout import Timeout, TimeoutAction, any_to_timeout
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
DEFAULT_TIMEOUT = Timeout(response=5, action="error")
|
|
69
|
-
|
|
70
|
-
# Maximum time to let the backend start
|
|
71
|
-
START_TIMEOUT = 2
|
|
72
|
-
# Time to shutdown the backend
|
|
73
|
-
SHUTDOWN_DELAY = 2
|
|
64
|
+
fragments: list[Fragment]
|
|
74
65
|
|
|
75
66
|
|
|
76
|
-
|
|
67
|
+
# pylint: disable=too-many-public-methods, too-many-instance-attributes
|
|
68
|
+
class Adapter(Component[bytes], AdapterWorker):
|
|
77
69
|
"""
|
|
78
|
-
|
|
79
|
-
"""
|
|
80
|
-
def __init__(self) -> None:
|
|
81
|
-
self._read_payload_counter = 0
|
|
82
|
-
super().__init__(0)
|
|
83
|
-
|
|
84
|
-
def has_read_payload(self) -> bool:
|
|
85
|
-
"""
|
|
86
|
-
Return True if the queue contains a read payload
|
|
87
|
-
"""
|
|
88
|
-
return self._read_payload_counter > 0
|
|
89
|
-
|
|
90
|
-
def put(self, signal: AdapterSignal) -> None:
|
|
91
|
-
"""
|
|
92
|
-
Put a signal in the queue
|
|
93
|
-
|
|
94
|
-
Parameters
|
|
95
|
-
----------
|
|
96
|
-
signal : AdapterSignal
|
|
97
|
-
"""
|
|
98
|
-
if isinstance(signal, AdapterReadPayload):
|
|
99
|
-
self._read_payload_counter += 1
|
|
100
|
-
return super().put(signal)
|
|
101
|
-
|
|
102
|
-
def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
|
|
103
|
-
"""
|
|
104
|
-
Get an AdapterSignal from the queue
|
|
105
|
-
"""
|
|
106
|
-
signal = super().get(block, timeout)
|
|
107
|
-
if isinstance(signal, AdapterReadPayload):
|
|
108
|
-
self._read_payload_counter -= 1
|
|
109
|
-
return signal
|
|
110
|
-
|
|
111
|
-
def is_backend_running(address: str, port: int) -> bool:
|
|
112
|
-
"""
|
|
113
|
-
Return True if the backend is running
|
|
114
|
-
"""
|
|
115
|
-
try:
|
|
116
|
-
conn = Client((address, port))
|
|
117
|
-
except ConnectionRefusedError:
|
|
118
|
-
return False
|
|
119
|
-
conn.close()
|
|
120
|
-
return True
|
|
121
|
-
|
|
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
|
-
"""
|
|
132
|
-
arguments = [
|
|
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
|
|
144
|
-
stdout = subprocess.DEVNULL
|
|
145
|
-
stderr = subprocess.DEVNULL
|
|
146
|
-
|
|
147
|
-
if os.name == "posix":
|
|
148
|
-
subprocess.Popen( #pylint: disable=consider-using-with
|
|
149
|
-
arguments,
|
|
150
|
-
stdin=stdin,
|
|
151
|
-
stdout=stdout,
|
|
152
|
-
stderr=stderr,
|
|
153
|
-
start_new_session=True,
|
|
154
|
-
close_fds=True,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
else:
|
|
158
|
-
# Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
|
|
159
|
-
create_new_process_group = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
|
|
160
|
-
detached_process = 0x00000008 # not exposed by subprocess on all Pythons
|
|
161
|
-
# Optional: CREATE_NO_WINDOW (no window even for console apps)
|
|
162
|
-
create_no_window = 0x08000000
|
|
163
|
-
|
|
164
|
-
creationflags = create_new_process_group | detached_process | create_no_window
|
|
165
|
-
|
|
166
|
-
subprocess.Popen( #pylint: disable=consider-using-with
|
|
167
|
-
arguments,
|
|
168
|
-
stdin=stdin,
|
|
169
|
-
stdout=stdout,
|
|
170
|
-
stderr=stderr,
|
|
171
|
-
creationflags=creationflags,
|
|
172
|
-
close_fds=True,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
class ReadScope(Enum):
|
|
177
|
-
"""
|
|
178
|
-
Read scope
|
|
70
|
+
Adapter class
|
|
179
71
|
|
|
180
|
-
|
|
181
|
-
BUFFERED : Return any data that was present before the read() call
|
|
72
|
+
An adapter manages communication with a hardware device.
|
|
182
73
|
"""
|
|
183
|
-
NEXT = "next"
|
|
184
|
-
BUFFERED = "buffered"
|
|
185
74
|
|
|
75
|
+
class WorkerTimeout(Enum):
|
|
76
|
+
"""Timeout value for each worker command scenario"""
|
|
186
77
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
78
|
+
OPEN = 2
|
|
79
|
+
STOP = 1
|
|
80
|
+
IMMEDIATE_COMMAND = 0.2
|
|
81
|
+
CLOSE = 0.5
|
|
82
|
+
WRITE = 0.5
|
|
83
|
+
READ = None
|
|
190
84
|
|
|
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
|
|
195
85
|
def __init__(
|
|
196
86
|
self,
|
|
197
87
|
*,
|
|
198
88
|
descriptor: Descriptor,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
89
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition],
|
|
90
|
+
timeout: Timeout | EllipsisType | NumberLike | None,
|
|
91
|
+
alias: str,
|
|
202
92
|
encoding: str = "utf-8",
|
|
203
|
-
event_callback: Callable[[
|
|
93
|
+
event_callback: Callable[[AdapterEvent], None] | None = None,
|
|
204
94
|
auto_open: bool = True,
|
|
205
|
-
backend_address: str | None = None,
|
|
206
|
-
backend_port: int | None = None,
|
|
207
95
|
) -> None:
|
|
208
|
-
"""
|
|
209
|
-
Adapter instance
|
|
210
|
-
|
|
211
|
-
Parameters
|
|
212
|
-
----------
|
|
213
|
-
alias : str
|
|
214
|
-
The alias is used to identify the class in the logs
|
|
215
|
-
timeout : float or Timeout instance
|
|
216
|
-
Default timeout is
|
|
217
|
-
stop_condition : StopCondition or None
|
|
218
|
-
Default to None
|
|
219
|
-
encoding : str
|
|
220
|
-
Which encoding to use if str has to be encoded into bytes
|
|
221
|
-
"""
|
|
222
96
|
super().__init__(LoggerAlias.ADAPTER)
|
|
223
97
|
self.encoding = encoding
|
|
224
|
-
self._signal_queue: SignalQueue = SignalQueue()
|
|
225
|
-
self.event_callback: Callable[[AdapterSignal], None] | None = event_callback
|
|
226
|
-
self.backend_connection: Connection | None = None
|
|
227
|
-
self._backend_connection_lock = threading.Lock()
|
|
228
|
-
self._make_backend_request_queue: queue.Queue[BackendResponse] = queue.Queue()
|
|
229
|
-
self._make_backend_request_flag = threading.Event()
|
|
230
|
-
self._opened = False
|
|
231
98
|
self._alias = alias
|
|
232
99
|
|
|
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
|
|
236
|
-
|
|
237
|
-
# There a two possibilities here
|
|
238
|
-
# A) The descriptor is fully initialized
|
|
239
|
-
# -> The adapter can be connected directly
|
|
240
|
-
# B) The descriptor is not fully initialized
|
|
241
|
-
# -> Wait for the protocol to set defaults and then connect the adapter
|
|
242
|
-
|
|
243
100
|
self.descriptor = descriptor
|
|
244
101
|
self.auto_open = auto_open
|
|
245
102
|
|
|
246
|
-
|
|
247
|
-
|
|
103
|
+
self._initial_event_callback = event_callback
|
|
104
|
+
|
|
105
|
+
# Default stop conditions
|
|
106
|
+
self._initial_stop_conditions: list[StopCondition]
|
|
248
107
|
if stop_conditions is ...:
|
|
249
|
-
self.
|
|
250
|
-
self.
|
|
108
|
+
self._is_default_stop_condition = True
|
|
109
|
+
self._initial_stop_conditions = self._default_stop_conditions()
|
|
251
110
|
else:
|
|
252
|
-
self.
|
|
111
|
+
self._is_default_stop_condition = False
|
|
253
112
|
if isinstance(stop_conditions, StopCondition):
|
|
254
|
-
self.
|
|
113
|
+
self._initial_stop_conditions = [stop_conditions]
|
|
255
114
|
elif isinstance(stop_conditions, list):
|
|
256
|
-
self.
|
|
115
|
+
self._initial_stop_conditions = stop_conditions
|
|
257
116
|
else:
|
|
258
117
|
raise ValueError("Invalid stop_conditions")
|
|
259
118
|
|
|
260
|
-
#
|
|
261
|
-
self.is_default_timeout =
|
|
262
|
-
|
|
119
|
+
# Default timeout
|
|
120
|
+
self.is_default_timeout = timeout is Ellipsis
|
|
121
|
+
|
|
263
122
|
if timeout is Ellipsis:
|
|
264
|
-
|
|
265
|
-
self.is_default_timeout = True
|
|
266
|
-
self._timeout = DEFAULT_TIMEOUT
|
|
123
|
+
self._initial_timeout = self._default_timeout()
|
|
267
124
|
elif isinstance(timeout, Timeout):
|
|
268
|
-
self.
|
|
125
|
+
self._initial_timeout = timeout
|
|
269
126
|
elif is_number(timeout):
|
|
270
|
-
self.
|
|
127
|
+
self._initial_timeout = Timeout(timeout, action=TimeoutAction.ERROR)
|
|
271
128
|
elif timeout is None:
|
|
272
|
-
self.
|
|
129
|
+
self._initial_timeout = Timeout(None)
|
|
130
|
+
else:
|
|
131
|
+
raise ValueError(f"Invalid timeout : {timeout}")
|
|
273
132
|
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
133
|
+
# Worker thread
|
|
134
|
+
self._worker_thread = threading.Thread(
|
|
135
|
+
target=self._worker_thread_method, daemon=True
|
|
136
|
+
)
|
|
137
|
+
self._worker_thread.start()
|
|
277
138
|
|
|
278
|
-
|
|
279
|
-
|
|
139
|
+
# Serialize read/write/query ordering for sync callers.
|
|
140
|
+
self._sync_io_lock = threading.Lock()
|
|
141
|
+
# Serialize read/write/query ordering for async callers.
|
|
142
|
+
self._async_io_lock = asyncio.Lock()
|
|
280
143
|
|
|
281
|
-
|
|
144
|
+
self._logger.info(f"Setting up {self.descriptor} adapter ")
|
|
145
|
+
self._update_descriptor()
|
|
146
|
+
self.set_stop_conditions(self._initial_stop_conditions)
|
|
147
|
+
self.set_timeout(self._initial_timeout)
|
|
148
|
+
self.set_event_callback(self._initial_event_callback)
|
|
282
149
|
|
|
283
|
-
|
|
284
|
-
# connection with the backend has been made (descriptor initialized)
|
|
285
|
-
if self.auto_open and self.backend_connection is not None:
|
|
150
|
+
if self.descriptor.is_initialized() and auto_open:
|
|
286
151
|
self.open()
|
|
287
152
|
|
|
288
|
-
|
|
289
|
-
"""
|
|
290
|
-
Connect to the backend
|
|
291
|
-
"""
|
|
292
|
-
if self.backend_connection is not None:
|
|
293
|
-
# No need to connect, everything has been done already
|
|
294
|
-
return
|
|
295
|
-
if not self.descriptor.is_initialized():
|
|
296
|
-
raise RuntimeError("Descriptor wasn't initialized fully")
|
|
297
|
-
|
|
298
|
-
if is_backend_running(self._backend_address, self._backend_port):
|
|
299
|
-
self._logger.info("Backend already running")
|
|
300
|
-
else:
|
|
301
|
-
self._logger.info("Starting backend...")
|
|
302
|
-
start_backend(self._backend_port)
|
|
303
|
-
start = time.time()
|
|
304
|
-
while time.time() < (start + START_TIMEOUT):
|
|
305
|
-
if is_backend_running(self._backend_address, self._backend_port):
|
|
306
|
-
self._logger.info("Backend started")
|
|
307
|
-
break
|
|
308
|
-
time.sleep(0.1)
|
|
309
|
-
else:
|
|
310
|
-
# Backend could not start
|
|
311
|
-
self._logger.error("Could not start backend")
|
|
153
|
+
weakref.finalize(self, self._cleanup)
|
|
312
154
|
|
|
313
|
-
|
|
155
|
+
# ┌──────────────────────────┐
|
|
156
|
+
# │ Defaults / configuration │
|
|
157
|
+
# └──────────────────────────┘
|
|
158
|
+
|
|
159
|
+
def _stop(self) -> None:
|
|
160
|
+
cmd = StopThreadCommand()
|
|
161
|
+
self._worker_send_command(cmd)
|
|
314
162
|
try:
|
|
315
|
-
self.
|
|
316
|
-
except
|
|
317
|
-
|
|
318
|
-
self._read_thread = threading.Thread(
|
|
319
|
-
target=self.read_thread,
|
|
320
|
-
args=(self._signal_queue, self._make_backend_request_queue),
|
|
321
|
-
daemon=True,
|
|
322
|
-
)
|
|
323
|
-
self._read_thread.start()
|
|
163
|
+
cmd.result(self.WorkerTimeout.STOP.value)
|
|
164
|
+
except AdapterError:
|
|
165
|
+
pass
|
|
324
166
|
|
|
325
|
-
|
|
326
|
-
self.
|
|
167
|
+
def _update_descriptor(self) -> None:
|
|
168
|
+
cmd = SetDescriptorCommand(self.descriptor)
|
|
169
|
+
self._worker_send_command(cmd)
|
|
170
|
+
cmd.result(self.WorkerTimeout.IMMEDIATE_COMMAND.value)
|
|
327
171
|
|
|
328
|
-
|
|
329
|
-
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def _default_timeout(self) -> Timeout:
|
|
174
|
+
raise NotImplementedError
|
|
330
175
|
|
|
331
|
-
|
|
332
|
-
|
|
176
|
+
@abstractmethod
|
|
177
|
+
def _default_stop_conditions(self) -> list[StopCondition]:
|
|
178
|
+
raise NotImplementedError
|
|
333
179
|
|
|
334
|
-
def
|
|
335
|
-
self
|
|
336
|
-
action: Action,
|
|
337
|
-
*args: Any,
|
|
338
|
-
timeout: float = BACKEND_REQUEST_DEFAULT_TIMEOUT,
|
|
339
|
-
) -> BackendResponse:
|
|
340
|
-
"""
|
|
341
|
-
Send a request to the backend and return the arguments
|
|
342
|
-
"""
|
|
180
|
+
def __str__(self) -> str:
|
|
181
|
+
return str(self.descriptor)
|
|
343
182
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
self.backend_connection.send((action.value, *args))
|
|
183
|
+
def __repr__(self) -> str:
|
|
184
|
+
return self.__str__()
|
|
347
185
|
|
|
348
|
-
|
|
186
|
+
def _cleanup(self) -> None:
|
|
187
|
+
# Be defensive: finalizers can run at interpreter shutdown.
|
|
349
188
|
try:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
) from err
|
|
355
|
-
|
|
356
|
-
assert (
|
|
357
|
-
isinstance(response, tuple) and len(response) > 0
|
|
358
|
-
), f"Invalid response received from backend : {response}"
|
|
359
|
-
raise_if_error(response)
|
|
189
|
+
if self.is_open():
|
|
190
|
+
self.close()
|
|
191
|
+
except AdapterError:
|
|
192
|
+
pass
|
|
360
193
|
|
|
361
|
-
|
|
194
|
+
self._stop()
|
|
362
195
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
"""
|
|
374
|
-
while True:
|
|
375
|
-
try:
|
|
376
|
-
if self.backend_connection is None:
|
|
377
|
-
raise RuntimeError("Backend connection wasn't initialized")
|
|
378
|
-
response: tuple[Any, ...] = self.backend_connection.recv()
|
|
379
|
-
except (EOFError, TypeError, OSError):
|
|
380
|
-
signal_queue.put(AdapterDisconnectedSignal())
|
|
381
|
-
request_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
|
|
382
|
-
break
|
|
383
|
-
else:
|
|
384
|
-
if not isinstance(response, tuple):
|
|
385
|
-
raise BackendCommunicationError(
|
|
386
|
-
f"Invalid response from backend : {response}"
|
|
387
|
-
)
|
|
388
|
-
action = Action(response[0])
|
|
389
|
-
|
|
390
|
-
if action == Action.ADAPTER_SIGNAL:
|
|
391
|
-
if len(response) <= 1:
|
|
392
|
-
raise BackendCommunicationError(
|
|
393
|
-
f"Invalid event response : {response}"
|
|
394
|
-
)
|
|
395
|
-
signal: AdapterSignal = response[1]
|
|
396
|
-
if self.event_callback is not None:
|
|
397
|
-
self.event_callback(signal)
|
|
398
|
-
signal_queue.put(signal)
|
|
399
|
-
else:
|
|
400
|
-
request_queue.put(response)
|
|
196
|
+
try:
|
|
197
|
+
self._command_queue_r.close()
|
|
198
|
+
self._command_queue_w.close()
|
|
199
|
+
except AdapterError:
|
|
200
|
+
pass
|
|
401
201
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
202
|
+
# ┌────────────┐
|
|
203
|
+
# │ Public API │
|
|
204
|
+
# └────────────┘
|
|
405
205
|
|
|
406
|
-
def set_timeout(self, timeout: Timeout | None) -> None:
|
|
206
|
+
def set_timeout(self, timeout: Timeout | None | float) -> None:
|
|
407
207
|
"""
|
|
408
|
-
|
|
208
|
+
Set adapter timeout
|
|
409
209
|
|
|
410
210
|
Parameters
|
|
411
211
|
----------
|
|
412
|
-
timeout : Timeout
|
|
212
|
+
timeout : Timeout, float or None
|
|
413
213
|
"""
|
|
414
|
-
|
|
214
|
+
# This is read by the worker when ReadCommand.timeout is ...
|
|
215
|
+
timeout_instance = any_to_timeout(timeout)
|
|
216
|
+
cmd = SetTimeoutCommand(timeout_instance)
|
|
217
|
+
self._worker_send_command(cmd)
|
|
218
|
+
cmd.result(self.WorkerTimeout.IMMEDIATE_COMMAND.value)
|
|
415
219
|
|
|
416
220
|
def set_default_timeout(self, default_timeout: Timeout | None) -> None:
|
|
417
221
|
"""
|
|
418
|
-
|
|
419
|
-
|
|
222
|
+
Configure adapter default timeout. Timeout will only be set if none
|
|
223
|
+
has been configured before
|
|
420
224
|
|
|
421
225
|
Parameters
|
|
422
226
|
----------
|
|
423
|
-
default_timeout : Timeout or
|
|
227
|
+
default_timeout : Timeout or None
|
|
424
228
|
"""
|
|
425
229
|
if self.is_default_timeout:
|
|
426
|
-
|
|
427
|
-
self.
|
|
230
|
+
new_timeout = any_to_timeout(default_timeout)
|
|
231
|
+
self._logger.debug(f"Setting default timeout to {new_timeout}")
|
|
232
|
+
self.set_timeout(new_timeout)
|
|
428
233
|
|
|
429
234
|
def set_stop_conditions(
|
|
430
235
|
self, stop_conditions: StopCondition | None | list[StopCondition]
|
|
431
236
|
) -> None:
|
|
432
237
|
"""
|
|
433
|
-
|
|
238
|
+
Set adapter stop-conditions
|
|
434
239
|
|
|
435
240
|
Parameters
|
|
436
241
|
----------
|
|
437
|
-
|
|
242
|
+
stop_conditions : [StopCondition] or None
|
|
438
243
|
"""
|
|
439
244
|
if isinstance(stop_conditions, list):
|
|
440
|
-
|
|
245
|
+
lst = stop_conditions
|
|
441
246
|
elif isinstance(stop_conditions, StopCondition):
|
|
442
|
-
|
|
247
|
+
lst = [stop_conditions]
|
|
443
248
|
elif stop_conditions is None:
|
|
444
|
-
|
|
249
|
+
lst = []
|
|
250
|
+
else:
|
|
251
|
+
raise ValueError("Invalid stop_conditions")
|
|
445
252
|
|
|
446
|
-
|
|
253
|
+
cmd = SetStopConditionsCommand(lst)
|
|
254
|
+
self._worker_send_command(cmd)
|
|
255
|
+
cmd.result(self.WorkerTimeout.IMMEDIATE_COMMAND.value)
|
|
447
256
|
|
|
448
|
-
def
|
|
257
|
+
def set_default_stop_conditions(self, stop_conditions: list[StopCondition]) -> None:
|
|
449
258
|
"""
|
|
450
|
-
|
|
259
|
+
Configure adapter default stop-condition. Stop-condition will only be set if none
|
|
260
|
+
has been configured before
|
|
451
261
|
|
|
452
262
|
Parameters
|
|
453
263
|
----------
|
|
454
|
-
|
|
264
|
+
stop_conditions : [StopCondition]
|
|
455
265
|
"""
|
|
456
|
-
if self.
|
|
457
|
-
self.set_stop_conditions(
|
|
266
|
+
if self._is_default_stop_condition:
|
|
267
|
+
self.set_stop_conditions(stop_conditions)
|
|
458
268
|
|
|
459
|
-
def
|
|
460
|
-
|
|
461
|
-
|
|
269
|
+
def set_event_callback(
|
|
270
|
+
self, callback: Callable[[AdapterEvent], None] | None
|
|
271
|
+
) -> None:
|
|
462
272
|
"""
|
|
463
|
-
|
|
464
|
-
Action.FLUSHREAD,
|
|
465
|
-
)
|
|
466
|
-
while True:
|
|
467
|
-
try:
|
|
468
|
-
self._signal_queue.get(block=False)
|
|
469
|
-
except queue.Empty:
|
|
470
|
-
break
|
|
273
|
+
Configure event callback. Event callback is called as such :
|
|
471
274
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
275
|
+
callback(event : AdapterEvent)
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
callback : callable
|
|
475
280
|
|
|
476
|
-
Returns
|
|
477
|
-
-------
|
|
478
|
-
empty : bool
|
|
479
281
|
"""
|
|
480
|
-
|
|
282
|
+
cmd = SetEventCallbackCommand(callback)
|
|
283
|
+
self._worker_send_command(cmd)
|
|
284
|
+
cmd.result(self.WorkerTimeout.IMMEDIATE_COMMAND.value)
|
|
285
|
+
|
|
286
|
+
# ==== open ====
|
|
287
|
+
|
|
288
|
+
def _open_future(self) -> OpenCommand:
|
|
289
|
+
cmd = OpenCommand()
|
|
290
|
+
self._worker_send_command(cmd)
|
|
291
|
+
return cmd
|
|
481
292
|
|
|
482
293
|
def open(self) -> None:
|
|
483
294
|
"""
|
|
484
|
-
|
|
295
|
+
Open adapter communication with the target (blocking)
|
|
485
296
|
"""
|
|
486
|
-
self.
|
|
487
|
-
Action.OPEN,
|
|
488
|
-
self._stop_conditions,
|
|
489
|
-
timeout=BACKEND_REQUEST_DEFAULT_TIMEOUT + DEFAULT_ADAPTER_OPEN_TIMEOUT,
|
|
490
|
-
)
|
|
491
|
-
self._logger.info("Adapter opened")
|
|
492
|
-
self._opened = True
|
|
297
|
+
return self._open_future().result(self.WorkerTimeout.OPEN.value)
|
|
493
298
|
|
|
494
|
-
def
|
|
495
|
-
try:
|
|
496
|
-
self.open()
|
|
497
|
-
except AdapterFailedToOpen:
|
|
498
|
-
return False
|
|
499
|
-
return True
|
|
500
|
-
|
|
501
|
-
def close(self, force: bool = False) -> None:
|
|
299
|
+
async def aopen(self) -> None:
|
|
502
300
|
"""
|
|
503
|
-
|
|
301
|
+
Open adapter communication with the target (async)
|
|
504
302
|
"""
|
|
505
|
-
|
|
506
|
-
self._logger.debug("Closing adapter frontend")
|
|
507
|
-
else:
|
|
508
|
-
self._logger.debug("Force closing adapter backend")
|
|
509
|
-
self._make_backend_request(Action.CLOSE, force)
|
|
303
|
+
await asyncio.wrap_future(self._open_future())
|
|
510
304
|
|
|
511
|
-
|
|
512
|
-
if self.backend_connection is not None:
|
|
513
|
-
self.backend_connection.close()
|
|
305
|
+
# ==== close ====
|
|
514
306
|
|
|
515
|
-
|
|
307
|
+
def _close_future(self) -> CloseCommand:
|
|
308
|
+
cmd = CloseCommand()
|
|
309
|
+
self._worker_send_command(cmd)
|
|
310
|
+
return cmd
|
|
516
311
|
|
|
517
|
-
def
|
|
312
|
+
def close(self) -> None:
|
|
518
313
|
"""
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
Returns
|
|
522
|
-
-------
|
|
523
|
-
opened : bool
|
|
314
|
+
Close adapter communication with the target (blocking)
|
|
524
315
|
"""
|
|
525
|
-
|
|
316
|
+
self._close_future().result(self.WorkerTimeout.CLOSE.value)
|
|
526
317
|
|
|
527
|
-
def
|
|
318
|
+
async def aclose(self) -> None:
|
|
528
319
|
"""
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
Parameters
|
|
532
|
-
----------
|
|
533
|
-
data : bytes or str
|
|
320
|
+
Close adapter communication with the target (async)
|
|
534
321
|
"""
|
|
322
|
+
await asyncio.wrap_future(self._close_future())
|
|
535
323
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
324
|
+
# ==== read_detailed ====
|
|
325
|
+
|
|
326
|
+
def _read_detailed_future(
|
|
327
|
+
self,
|
|
328
|
+
timeout: Timeout | EllipsisType | None,
|
|
329
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition],
|
|
330
|
+
scope: str,
|
|
331
|
+
) -> ReadCommand:
|
|
332
|
+
cmd = ReadCommand(
|
|
333
|
+
timeout=timeout,
|
|
334
|
+
stop_conditions=stop_conditions,
|
|
335
|
+
scope=ReadScope(scope),
|
|
336
|
+
)
|
|
337
|
+
self._worker_send_command(cmd)
|
|
338
|
+
return cmd
|
|
539
339
|
|
|
540
340
|
def read_detailed(
|
|
541
341
|
self,
|
|
542
342
|
timeout: Timeout | EllipsisType | None = ...,
|
|
543
343
|
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
544
344
|
scope: str = ReadScope.BUFFERED.value,
|
|
545
|
-
) ->
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
----------
|
|
551
|
-
timeout : tuple, Timeout
|
|
552
|
-
Temporary timeout
|
|
553
|
-
stop_condition : StopCondition
|
|
554
|
-
Temporary stop condition
|
|
555
|
-
scope : str
|
|
556
|
-
Return previous data ('buffered') or only future data ('next')
|
|
557
|
-
Returns
|
|
558
|
-
-------
|
|
559
|
-
data : bytes
|
|
560
|
-
signal : AdapterReadPayload
|
|
561
|
-
"""
|
|
562
|
-
_scope = ReadScope(scope)
|
|
563
|
-
output_signal = None
|
|
564
|
-
read_timeout = None
|
|
565
|
-
|
|
566
|
-
if timeout is ...:
|
|
567
|
-
read_timeout = self._timeout
|
|
568
|
-
else:
|
|
569
|
-
read_timeout = any_to_timeout(timeout)
|
|
345
|
+
) -> AdapterFrame:
|
|
346
|
+
with self._sync_io_lock:
|
|
347
|
+
return self._read_detailed_future(
|
|
348
|
+
timeout=timeout, stop_conditions=stop_conditions, scope=scope
|
|
349
|
+
).result(self.WorkerTimeout.READ.value)
|
|
570
350
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
output_signal = signal
|
|
584
|
-
break
|
|
585
|
-
# TODO : Implement disconnect ?
|
|
586
|
-
else:
|
|
587
|
-
# Nothing was found, ask the backend with a START_READ request. The backend will
|
|
588
|
-
# respond at most after the response_time with either data or a RESPONSE_TIMEOUT
|
|
589
|
-
|
|
590
|
-
if not read_timeout.is_initialized():
|
|
591
|
-
raise RuntimeError("Timeout needs to be initialized")
|
|
592
|
-
|
|
593
|
-
_response = read_timeout.response()
|
|
594
|
-
|
|
595
|
-
read_init_time = time.time()
|
|
596
|
-
start_read_id = self._make_backend_request(Action.START_READ, _response)[0]
|
|
597
|
-
|
|
598
|
-
if _response is None:
|
|
599
|
-
# Wait indefinitely
|
|
600
|
-
read_stop_timestamp = None
|
|
601
|
-
else:
|
|
602
|
-
# Wait for the response time + a bit more
|
|
603
|
-
read_stop_timestamp = read_init_time + _response
|
|
604
|
-
|
|
605
|
-
while True:
|
|
606
|
-
try:
|
|
607
|
-
if read_stop_timestamp is None:
|
|
608
|
-
queue_timeout = None
|
|
609
|
-
else:
|
|
610
|
-
queue_timeout = max(
|
|
611
|
-
0,
|
|
612
|
-
read_stop_timestamp
|
|
613
|
-
- time.time()
|
|
614
|
-
+ EXTRA_BUFFER_RESPONSE_TIME,
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
signal = self._signal_queue.get(timeout=queue_timeout)
|
|
618
|
-
except queue.Empty as e:
|
|
619
|
-
raise BackendCommunicationError(
|
|
620
|
-
"Failed to receive response from backend"
|
|
621
|
-
) from e
|
|
622
|
-
if isinstance(signal, AdapterReadPayload):
|
|
623
|
-
output_signal = signal
|
|
624
|
-
break
|
|
625
|
-
if isinstance(signal, AdapterDisconnectedSignal):
|
|
626
|
-
raise AdapterDisconnected()
|
|
627
|
-
if isinstance(signal, AdapterResponseTimeout):
|
|
628
|
-
if start_read_id == signal.identifier:
|
|
629
|
-
output_signal = None
|
|
630
|
-
break
|
|
631
|
-
# Otherwise ignore it
|
|
632
|
-
|
|
633
|
-
if output_signal is None:
|
|
634
|
-
match read_timeout.action:
|
|
635
|
-
case TimeoutAction.RETURN_EMPTY:
|
|
636
|
-
t = time.time()
|
|
637
|
-
return AdapterReadPayload(
|
|
638
|
-
fragments=[Fragment(b"", t)],
|
|
639
|
-
stop_timestamp=t,
|
|
640
|
-
stop_condition_type=StopConditionType.TIMEOUT,
|
|
641
|
-
previous_read_buffer_used=False,
|
|
642
|
-
response_timestamp=None,
|
|
643
|
-
response_delay=None,
|
|
644
|
-
)
|
|
645
|
-
case TimeoutAction.ERROR:
|
|
646
|
-
timeout_value = read_timeout.response()
|
|
647
|
-
raise AdapterTimeoutError(
|
|
648
|
-
float("nan") if timeout_value is None else timeout_value
|
|
649
|
-
)
|
|
650
|
-
case _:
|
|
651
|
-
raise NotImplementedError()
|
|
351
|
+
async def aread_detailed(
|
|
352
|
+
self,
|
|
353
|
+
timeout: Timeout | EllipsisType | None = ...,
|
|
354
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
355
|
+
scope: str = ReadScope.BUFFERED.value,
|
|
356
|
+
) -> AdapterFrame:
|
|
357
|
+
async with self._async_io_lock:
|
|
358
|
+
return await asyncio.wrap_future(
|
|
359
|
+
self._read_detailed_future(
|
|
360
|
+
timeout=timeout, stop_conditions=stop_conditions, scope=scope
|
|
361
|
+
)
|
|
362
|
+
)
|
|
652
363
|
|
|
653
|
-
|
|
654
|
-
return output_signal
|
|
364
|
+
# ==== read ====
|
|
655
365
|
|
|
656
366
|
def read(
|
|
657
367
|
self,
|
|
658
368
|
timeout: Timeout | EllipsisType | None = ...,
|
|
659
369
|
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
370
|
+
scope: str = ReadScope.BUFFERED.value,
|
|
660
371
|
) -> bytes:
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if self._opened:
|
|
666
|
-
self.close()
|
|
372
|
+
frame = self.read_detailed(
|
|
373
|
+
timeout=timeout, stop_conditions=stop_conditions, scope=scope
|
|
374
|
+
)
|
|
375
|
+
return frame.get_payload()
|
|
667
376
|
|
|
668
|
-
def
|
|
377
|
+
async def aread(
|
|
669
378
|
self,
|
|
670
|
-
data: bytes | str,
|
|
671
379
|
timeout: Timeout | EllipsisType | None = ...,
|
|
672
380
|
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
673
|
-
|
|
381
|
+
scope: str = ReadScope.BUFFERED.value,
|
|
382
|
+
) -> bytes:
|
|
383
|
+
frame = await self.aread_detailed(
|
|
384
|
+
timeout=timeout, stop_conditions=stop_conditions, scope=scope
|
|
385
|
+
)
|
|
386
|
+
return frame.get_payload()
|
|
387
|
+
|
|
388
|
+
# ==== flush_read ====
|
|
389
|
+
|
|
390
|
+
def _flush_read_future(self) -> FlushReadCommand:
|
|
391
|
+
cmd = FlushReadCommand()
|
|
392
|
+
self._worker_send_command(cmd)
|
|
393
|
+
return cmd
|
|
394
|
+
|
|
395
|
+
async def aflush_read(self) -> None:
|
|
396
|
+
"""
|
|
397
|
+
Clear buffered completed frames and reset current fragment assembly (async)
|
|
674
398
|
"""
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
399
|
+
async with self._async_io_lock:
|
|
400
|
+
await asyncio.wrap_future(self._flush_read_future())
|
|
401
|
+
|
|
402
|
+
def flush_read(self) -> None:
|
|
403
|
+
"""
|
|
404
|
+
Clear buffered completed frames and reset current fragment assembly (blocking)
|
|
679
405
|
"""
|
|
680
|
-
self.
|
|
681
|
-
|
|
682
|
-
|
|
406
|
+
with self._sync_io_lock:
|
|
407
|
+
self._flush_read_future().result(self.WorkerTimeout.IMMEDIATE_COMMAND.value)
|
|
408
|
+
|
|
409
|
+
# ==== write ====
|
|
410
|
+
|
|
411
|
+
def _write_future(self, data: bytes | str) -> WriteCommand:
|
|
412
|
+
if isinstance(data, str):
|
|
413
|
+
data = data.encode(self.encoding)
|
|
414
|
+
cmd = WriteCommand(data)
|
|
415
|
+
self._worker_send_command(cmd)
|
|
416
|
+
return cmd
|
|
417
|
+
|
|
418
|
+
def write(self, data: bytes | str) -> None:
|
|
419
|
+
with self._sync_io_lock:
|
|
420
|
+
self._write_future(data).result(self.WorkerTimeout.WRITE.value)
|
|
421
|
+
|
|
422
|
+
async def awrite(self, data: bytes | str) -> None:
|
|
423
|
+
async with self._async_io_lock:
|
|
424
|
+
await asyncio.wrap_future(self._write_future(data))
|
|
425
|
+
|
|
426
|
+
# ==== query ====
|
|
683
427
|
|
|
684
|
-
def
|
|
428
|
+
async def aquery_detailed(
|
|
685
429
|
self,
|
|
686
|
-
|
|
687
|
-
timeout: Timeout |
|
|
430
|
+
payload: bytes,
|
|
431
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
688
432
|
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
433
|
+
scope: str = ReadScope.BUFFERED.value,
|
|
434
|
+
) -> AdapterFrame:
|
|
435
|
+
async with self._async_io_lock:
|
|
436
|
+
await self.aflush_read()
|
|
437
|
+
await self.awrite(payload)
|
|
438
|
+
return await self.aread_detailed(
|
|
439
|
+
timeout=timeout, stop_conditions=stop_conditions, scope=scope
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def query_detailed(
|
|
443
|
+
self,
|
|
444
|
+
payload: bytes,
|
|
445
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
446
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
447
|
+
scope: str = ReadScope.BUFFERED.value,
|
|
448
|
+
) -> AdapterFrame:
|
|
694
449
|
|
|
695
|
-
|
|
696
|
-
|
|
450
|
+
with self._sync_io_lock:
|
|
451
|
+
self.flush_read()
|
|
452
|
+
self.write(payload)
|
|
453
|
+
return self.read_detailed(
|
|
454
|
+
timeout=timeout, stop_conditions=stop_conditions, scope=scope
|
|
455
|
+
)
|
|
697
456
|
|
|
698
|
-
|
|
699
|
-
return str(self.descriptor)
|
|
457
|
+
# ==== Other ====
|
|
700
458
|
|
|
701
|
-
def
|
|
702
|
-
|
|
459
|
+
def _is_open_future(self) -> IsOpenCommand:
|
|
460
|
+
cmd = IsOpenCommand()
|
|
461
|
+
self._worker_send_command(cmd)
|
|
462
|
+
return cmd
|
|
463
|
+
|
|
464
|
+
def is_open(self) -> bool:
|
|
465
|
+
"""Check if the adapter is open"""
|
|
466
|
+
return self._is_open_future().result(self.WorkerTimeout.IMMEDIATE_COMMAND.value)
|
|
467
|
+
|
|
468
|
+
async def ais_open(self) -> bool:
|
|
469
|
+
"""Asynchronously check if the adapter is open"""
|
|
470
|
+
return await asyncio.wrap_future(self._is_open_future())
|