syndesi 0.4.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {syndesi-0.4.2/syndesi.egg-info → syndesi-0.5.0}/PKG-INFO +1 -1
  2. {syndesi-0.4.2 → syndesi-0.5.0}/pyproject.toml +7 -1
  3. syndesi-0.5.0/syndesi/__init__.py +30 -0
  4. syndesi-0.5.0/syndesi/adapters/adapter.py +470 -0
  5. syndesi-0.5.0/syndesi/adapters/adapter_worker.py +820 -0
  6. syndesi-0.5.0/syndesi/adapters/auto.py +83 -0
  7. syndesi-0.5.0/syndesi/adapters/descriptors.py +38 -0
  8. syndesi-0.5.0/syndesi/adapters/ip.py +242 -0
  9. syndesi-0.5.0/syndesi/adapters/serialport.py +209 -0
  10. syndesi-0.5.0/syndesi/adapters/stop_conditions.py +354 -0
  11. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/adapters/timeout.py +58 -21
  12. syndesi-0.5.0/syndesi/adapters/visa.py +269 -0
  13. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/cli/console.py +51 -16
  14. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/cli/shell.py +95 -47
  15. syndesi-0.5.0/syndesi/cli/terminal_tools.py +14 -0
  16. syndesi-0.5.0/syndesi/component.py +315 -0
  17. syndesi-0.5.0/syndesi/protocols/delimited.py +168 -0
  18. syndesi-0.5.0/syndesi/protocols/modbus.py +3101 -0
  19. syndesi-0.5.0/syndesi/protocols/protocol.py +233 -0
  20. syndesi-0.5.0/syndesi/protocols/raw.py +73 -0
  21. syndesi-0.5.0/syndesi/protocols/scpi.py +107 -0
  22. syndesi-0.5.0/syndesi/remote/remote.py +188 -0
  23. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/scripts/syndesi.py +12 -2
  24. syndesi-0.5.0/syndesi/tools/errors.py +68 -0
  25. syndesi-0.5.0/syndesi/tools/log_settings.py +30 -0
  26. syndesi-0.4.2/syndesi/tools/log.py → syndesi-0.5.0/syndesi/tools/logmanager.py +24 -13
  27. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/tools/types.py +9 -7
  28. syndesi-0.5.0/syndesi/version.py +7 -0
  29. {syndesi-0.4.2 → syndesi-0.5.0/syndesi.egg-info}/PKG-INFO +1 -1
  30. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi.egg-info/SOURCES.txt +6 -25
  31. syndesi-0.4.2/syndesi/__init__.py +0 -10
  32. syndesi-0.4.2/syndesi/adapters/adapter.py +0 -627
  33. syndesi-0.4.2/syndesi/adapters/auto.py +0 -50
  34. syndesi-0.4.2/syndesi/adapters/backend/adapter_backend.py +0 -438
  35. syndesi-0.4.2/syndesi/adapters/backend/adapter_manager.py +0 -48
  36. syndesi-0.4.2/syndesi/adapters/backend/adapter_session.py +0 -346
  37. syndesi-0.4.2/syndesi/adapters/backend/backend.py +0 -438
  38. syndesi-0.4.2/syndesi/adapters/backend/backend_status.py +0 -0
  39. syndesi-0.4.2/syndesi/adapters/backend/backend_tools.py +0 -66
  40. syndesi-0.4.2/syndesi/adapters/backend/descriptors.py +0 -153
  41. syndesi-0.4.2/syndesi/adapters/backend/ip_backend.py +0 -149
  42. syndesi-0.4.2/syndesi/adapters/backend/serialport_backend.py +0 -241
  43. syndesi-0.4.2/syndesi/adapters/backend/stop_condition_backend.py +0 -219
  44. syndesi-0.4.2/syndesi/adapters/backend/timed_queue.py +0 -39
  45. syndesi-0.4.2/syndesi/adapters/backend/timeout.py +0 -252
  46. syndesi-0.4.2/syndesi/adapters/backend/visa_backend.py +0 -197
  47. syndesi-0.4.2/syndesi/adapters/ip.py +0 -110
  48. syndesi-0.4.2/syndesi/adapters/ip_server.py +0 -102
  49. syndesi-0.4.2/syndesi/adapters/serialport.py +0 -80
  50. syndesi-0.4.2/syndesi/adapters/stop_condition.py +0 -90
  51. syndesi-0.4.2/syndesi/adapters/visa.py +0 -44
  52. syndesi-0.4.2/syndesi/cli/backend_console.py +0 -96
  53. syndesi-0.4.2/syndesi/cli/backend_status.py +0 -274
  54. syndesi-0.4.2/syndesi/cli/backend_wrapper.py +0 -61
  55. syndesi-0.4.2/syndesi/cli/terminal_tools.py +0 -14
  56. syndesi-0.4.2/syndesi/protocols/delimited.py +0 -183
  57. syndesi-0.4.2/syndesi/protocols/modbus.py +0 -1601
  58. syndesi-0.4.2/syndesi/protocols/protocol.py +0 -80
  59. syndesi-0.4.2/syndesi/protocols/raw.py +0 -90
  60. syndesi-0.4.2/syndesi/protocols/scpi.py +0 -144
  61. syndesi-0.4.2/syndesi/scripts/syndesi_backend.py +0 -37
  62. syndesi-0.4.2/syndesi/tools/__init__.py +0 -0
  63. syndesi-0.4.2/syndesi/tools/backend_api.py +0 -175
  64. syndesi-0.4.2/syndesi/tools/backend_logger.py +0 -64
  65. syndesi-0.4.2/syndesi/tools/errors.py +0 -50
  66. syndesi-0.4.2/syndesi/tools/exceptions.py +0 -16
  67. syndesi-0.4.2/syndesi/tools/internal.py +0 -0
  68. syndesi-0.4.2/syndesi/tools/log_settings.py +0 -17
  69. syndesi-0.4.2/syndesi/version.py +0 -3
  70. {syndesi-0.4.2 → syndesi-0.5.0}/LICENSE +0 -0
  71. {syndesi-0.4.2 → syndesi-0.5.0}/README.md +0 -0
  72. {syndesi-0.4.2 → syndesi-0.5.0}/setup.cfg +0 -0
  73. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/__main__.py +0 -0
  74. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/adapters/__init__.py +0 -0
  75. {syndesi-0.4.2/syndesi/adapters/backend → syndesi-0.5.0/syndesi/cli}/__init__.py +0 -0
  76. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/cli/shell_tools.py +0 -0
  77. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/cli/terminal.py +0 -0
  78. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi/cli/terminal_apps.py +0 -0
  79. {syndesi-0.4.2/syndesi/cli → syndesi-0.5.0/syndesi/protocols}/__init__.py +0 -0
  80. {syndesi-0.4.2/syndesi/protocols → syndesi-0.5.0/syndesi/scripts}/__init__.py +0 -0
  81. {syndesi-0.4.2/syndesi/scripts → syndesi-0.5.0/syndesi/tools}/__init__.py +0 -0
  82. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi.egg-info/dependency_links.txt +0 -0
  83. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi.egg-info/entry_points.txt +0 -0
  84. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi.egg-info/requires.txt +0 -0
  85. {syndesi-0.4.2 → syndesi-0.5.0}/syndesi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syndesi
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Syndesi
5
5
  Author-email: Sébastien Deriaz <sebastien.deriaz1@gmail.com>
6
6
  License: GPL
@@ -50,7 +50,7 @@ ignore = ["E501"]
50
50
  [tool.ruff.format]
51
51
 
52
52
  [tool.ruff.lint.pydocstyle]
53
- convention = "numpy" # or "google"
53
+ convention = "numpy"
54
54
 
55
55
  [tool.isort]
56
56
  profile = "black"
@@ -69,3 +69,9 @@ exclude = ["^tests/fixtures/"]
69
69
  skips = ["B101"]
70
70
  exclude = ["tests"]
71
71
 
72
+ [tool.pylint]
73
+ # Disable too-many-arguments, this is the case for adapters/protocols/drivers
74
+ # All arguments are necessary and it would not make sense to group them / move them
75
+ # too-many-positional-arguments is kept however because it makes sense
76
+ # Also disable W1203 to allow f-strings in logging lines
77
+ disable = ["R0913", "W1203", "W0511", "R0903"]
@@ -0,0 +1,30 @@
1
+ """
2
+ Syndesi module
3
+ """
4
+
5
+ from .adapters.ip import IP
6
+ from .adapters.serialport import SerialPort
7
+ from .adapters.stop_conditions import Continuation, Length, Termination, Total
8
+ from .adapters.timeout import Timeout
9
+ from .adapters.visa import Visa
10
+ from .protocols.delimited import Delimited
11
+ from .protocols.modbus import Modbus
12
+ from .protocols.raw import Raw
13
+ from .protocols.scpi import SCPI
14
+ from .tools.logmanager import log
15
+
16
+ __all__ = [
17
+ "IP",
18
+ "SerialPort",
19
+ "Visa",
20
+ "Delimited",
21
+ "Modbus",
22
+ "Raw",
23
+ "SCPI",
24
+ "log",
25
+ "Timeout",
26
+ "Continuation",
27
+ "Length",
28
+ "Termination",
29
+ "Total",
30
+ ]
@@ -0,0 +1,470 @@
1
+ # File : adapter.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+
5
+ """
6
+ Adapters provide a common abstraction for the media layers (physical + data link + network)
7
+
8
+ The user calls methods of the Adapter class synchronously.
9
+
10
+ An adapter is meant to work with bytes objects but it can accept strings.
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).
20
+ """
21
+
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
34
+ import threading
35
+ import weakref
36
+ from abc import abstractmethod
37
+ from collections.abc import Callable
38
+ from enum import Enum
39
+ from types import EllipsisType
40
+
41
+ from syndesi.tools.errors import AdapterError
42
+
43
+ from ..component import AdapterFrame, Component, Descriptor, ReadScope
44
+ from ..tools.log_settings import LoggerAlias
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
+ )
61
+ from .stop_conditions import Fragment, StopCondition
62
+ from .timeout import Timeout, TimeoutAction, any_to_timeout
63
+
64
+ fragments: list[Fragment]
65
+
66
+
67
+ # pylint: disable=too-many-public-methods, too-many-instance-attributes
68
+ class Adapter(Component[bytes], AdapterWorker):
69
+ """
70
+ Adapter class
71
+
72
+ An adapter manages communication with a hardware device.
73
+ """
74
+
75
+ class WorkerTimeout(Enum):
76
+ """Timeout value for each worker command scenario"""
77
+
78
+ OPEN = 2
79
+ STOP = 1
80
+ IMMEDIATE_COMMAND = 0.2
81
+ CLOSE = 0.5
82
+ WRITE = 0.5
83
+ READ = None
84
+
85
+ def __init__(
86
+ self,
87
+ *,
88
+ descriptor: Descriptor,
89
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition],
90
+ timeout: Timeout | EllipsisType | NumberLike | None,
91
+ alias: str,
92
+ encoding: str = "utf-8",
93
+ event_callback: Callable[[AdapterEvent], None] | None = None,
94
+ auto_open: bool = True,
95
+ ) -> None:
96
+ super().__init__(LoggerAlias.ADAPTER)
97
+ self.encoding = encoding
98
+ self._alias = alias
99
+
100
+ self.descriptor = descriptor
101
+ self.auto_open = auto_open
102
+
103
+ self._initial_event_callback = event_callback
104
+
105
+ # Default stop conditions
106
+ self._initial_stop_conditions: list[StopCondition]
107
+ if stop_conditions is ...:
108
+ self._is_default_stop_condition = True
109
+ self._initial_stop_conditions = self._default_stop_conditions()
110
+ else:
111
+ self._is_default_stop_condition = False
112
+ if isinstance(stop_conditions, StopCondition):
113
+ self._initial_stop_conditions = [stop_conditions]
114
+ elif isinstance(stop_conditions, list):
115
+ self._initial_stop_conditions = stop_conditions
116
+ else:
117
+ raise ValueError("Invalid stop_conditions")
118
+
119
+ # Default timeout
120
+ self.is_default_timeout = timeout is Ellipsis
121
+
122
+ if timeout is Ellipsis:
123
+ self._initial_timeout = self._default_timeout()
124
+ elif isinstance(timeout, Timeout):
125
+ self._initial_timeout = timeout
126
+ elif is_number(timeout):
127
+ self._initial_timeout = Timeout(timeout, action=TimeoutAction.ERROR)
128
+ elif timeout is None:
129
+ self._initial_timeout = Timeout(None)
130
+ else:
131
+ raise ValueError(f"Invalid timeout : {timeout}")
132
+
133
+ # Worker thread
134
+ self._worker_thread = threading.Thread(
135
+ target=self._worker_thread_method, daemon=True
136
+ )
137
+ self._worker_thread.start()
138
+
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()
143
+
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)
149
+
150
+ if self.descriptor.is_initialized() and auto_open:
151
+ self.open()
152
+
153
+ weakref.finalize(self, self._cleanup)
154
+
155
+ # ┌──────────────────────────┐
156
+ # │ Defaults / configuration │
157
+ # └──────────────────────────┘
158
+
159
+ def _stop(self) -> None:
160
+ cmd = StopThreadCommand()
161
+ self._worker_send_command(cmd)
162
+ try:
163
+ cmd.result(self.WorkerTimeout.STOP.value)
164
+ except AdapterError:
165
+ pass
166
+
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)
171
+
172
+ @abstractmethod
173
+ def _default_timeout(self) -> Timeout:
174
+ raise NotImplementedError
175
+
176
+ @abstractmethod
177
+ def _default_stop_conditions(self) -> list[StopCondition]:
178
+ raise NotImplementedError
179
+
180
+ def __str__(self) -> str:
181
+ return str(self.descriptor)
182
+
183
+ def __repr__(self) -> str:
184
+ return self.__str__()
185
+
186
+ def _cleanup(self) -> None:
187
+ # Be defensive: finalizers can run at interpreter shutdown.
188
+ try:
189
+ if self.is_open():
190
+ self.close()
191
+ except AdapterError:
192
+ pass
193
+
194
+ self._stop()
195
+
196
+ try:
197
+ self._command_queue_r.close()
198
+ self._command_queue_w.close()
199
+ except AdapterError:
200
+ pass
201
+
202
+ # ┌────────────┐
203
+ # │ Public API │
204
+ # └────────────┘
205
+
206
+ def set_timeout(self, timeout: Timeout | None | float) -> None:
207
+ """
208
+ Set adapter timeout
209
+
210
+ Parameters
211
+ ----------
212
+ timeout : Timeout, float or None
213
+ """
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)
219
+
220
+ def set_default_timeout(self, default_timeout: Timeout | None) -> None:
221
+ """
222
+ Configure adapter default timeout. Timeout will only be set if none
223
+ has been configured before
224
+
225
+ Parameters
226
+ ----------
227
+ default_timeout : Timeout or None
228
+ """
229
+ if self.is_default_timeout:
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)
233
+
234
+ def set_stop_conditions(
235
+ self, stop_conditions: StopCondition | None | list[StopCondition]
236
+ ) -> None:
237
+ """
238
+ Set adapter stop-conditions
239
+
240
+ Parameters
241
+ ----------
242
+ stop_conditions : [StopCondition] or None
243
+ """
244
+ if isinstance(stop_conditions, list):
245
+ lst = stop_conditions
246
+ elif isinstance(stop_conditions, StopCondition):
247
+ lst = [stop_conditions]
248
+ elif stop_conditions is None:
249
+ lst = []
250
+ else:
251
+ raise ValueError("Invalid stop_conditions")
252
+
253
+ cmd = SetStopConditionsCommand(lst)
254
+ self._worker_send_command(cmd)
255
+ cmd.result(self.WorkerTimeout.IMMEDIATE_COMMAND.value)
256
+
257
+ def set_default_stop_conditions(self, stop_conditions: list[StopCondition]) -> None:
258
+ """
259
+ Configure adapter default stop-condition. Stop-condition will only be set if none
260
+ has been configured before
261
+
262
+ Parameters
263
+ ----------
264
+ stop_conditions : [StopCondition]
265
+ """
266
+ if self._is_default_stop_condition:
267
+ self.set_stop_conditions(stop_conditions)
268
+
269
+ def set_event_callback(
270
+ self, callback: Callable[[AdapterEvent], None] | None
271
+ ) -> None:
272
+ """
273
+ Configure event callback. Event callback is called as such :
274
+
275
+ callback(event : AdapterEvent)
276
+
277
+ Parameters
278
+ ----------
279
+ callback : callable
280
+
281
+ """
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
292
+
293
+ def open(self) -> None:
294
+ """
295
+ Open adapter communication with the target (blocking)
296
+ """
297
+ return self._open_future().result(self.WorkerTimeout.OPEN.value)
298
+
299
+ async def aopen(self) -> None:
300
+ """
301
+ Open adapter communication with the target (async)
302
+ """
303
+ await asyncio.wrap_future(self._open_future())
304
+
305
+ # ==== close ====
306
+
307
+ def _close_future(self) -> CloseCommand:
308
+ cmd = CloseCommand()
309
+ self._worker_send_command(cmd)
310
+ return cmd
311
+
312
+ def close(self) -> None:
313
+ """
314
+ Close adapter communication with the target (blocking)
315
+ """
316
+ self._close_future().result(self.WorkerTimeout.CLOSE.value)
317
+
318
+ async def aclose(self) -> None:
319
+ """
320
+ Close adapter communication with the target (async)
321
+ """
322
+ await asyncio.wrap_future(self._close_future())
323
+
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
339
+
340
+ def read_detailed(
341
+ self,
342
+ timeout: Timeout | EllipsisType | None = ...,
343
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
344
+ scope: str = ReadScope.BUFFERED.value,
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)
350
+
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
+ )
363
+
364
+ # ==== read ====
365
+
366
+ def read(
367
+ self,
368
+ timeout: Timeout | EllipsisType | None = ...,
369
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
370
+ scope: str = ReadScope.BUFFERED.value,
371
+ ) -> bytes:
372
+ frame = self.read_detailed(
373
+ timeout=timeout, stop_conditions=stop_conditions, scope=scope
374
+ )
375
+ return frame.get_payload()
376
+
377
+ async def aread(
378
+ self,
379
+ timeout: Timeout | EllipsisType | None = ...,
380
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
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)
398
+ """
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)
405
+ """
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 ====
427
+
428
+ async def aquery_detailed(
429
+ self,
430
+ payload: bytes,
431
+ timeout: Timeout | None | EllipsisType = ...,
432
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
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:
449
+
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
+ )
456
+
457
+ # ==== Other ====
458
+
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())