syndesi 0.3.2__tar.gz → 0.4.1__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 (67) hide show
  1. {syndesi-0.3.2/syndesi.egg-info → syndesi-0.4.1}/PKG-INFO +2 -1
  2. {syndesi-0.3.2 → syndesi-0.4.1}/pyproject.toml +1 -1
  3. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/adapter.py +126 -165
  4. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/auto.py +1 -1
  5. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/adapter_backend.py +96 -63
  6. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/adapter_session.py +53 -153
  7. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/descriptors.py +3 -2
  8. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/ip_backend.py +1 -0
  9. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/serialport_backend.py +19 -13
  10. syndesi-0.4.1/syndesi/adapters/backend/stop_condition_backend.py +198 -0
  11. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/visa_backend.py +7 -7
  12. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/ip.py +6 -10
  13. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/serialport.py +4 -5
  14. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/stop_condition.py +47 -26
  15. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/timeout.py +2 -2
  16. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/visa.py +2 -2
  17. syndesi-0.4.1/syndesi/cli/shell_tools.py +105 -0
  18. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/protocols/delimited.py +16 -21
  19. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/protocols/modbus.py +17 -14
  20. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/protocols/raw.py +20 -16
  21. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/protocols/scpi.py +17 -15
  22. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/tools/backend_api.py +15 -16
  23. syndesi-0.4.1/syndesi/tools/errors.py +51 -0
  24. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/version.py +1 -1
  25. {syndesi-0.3.2 → syndesi-0.4.1/syndesi.egg-info}/PKG-INFO +2 -1
  26. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi.egg-info/requires.txt +1 -0
  27. syndesi-0.3.2/syndesi/adapters/backend/stop_condition_backend.py +0 -342
  28. syndesi-0.3.2/syndesi/cli/shell_tools.py +0 -107
  29. syndesi-0.3.2/syndesi/tools/errors.py +0 -23
  30. {syndesi-0.3.2 → syndesi-0.4.1}/LICENSE +0 -0
  31. {syndesi-0.3.2 → syndesi-0.4.1}/README.md +0 -0
  32. {syndesi-0.3.2 → syndesi-0.4.1}/setup.cfg +0 -0
  33. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/__init__.py +0 -0
  34. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/__main__.py +0 -0
  35. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/__init__.py +0 -0
  36. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/__init__.py +0 -0
  37. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/adapter_manager.py +0 -0
  38. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/backend.py +0 -0
  39. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/backend_tools.py +0 -0
  40. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/timed_queue.py +0 -0
  41. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/backend/timeout.py +0 -0
  42. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/adapters/ip_server.py +0 -0
  43. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/cli/__init__.py +0 -0
  44. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/cli/backend_console.py +0 -0
  45. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/cli/backend_status.py +0 -0
  46. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/cli/backend_wrapper.py +0 -0
  47. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/cli/console.py +0 -0
  48. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/cli/shell.py +0 -0
  49. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/cli/terminal.py +0 -0
  50. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/cli/terminal_apps.py +0 -0
  51. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/cli/terminal_tools.py +0 -0
  52. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/protocols/__init__.py +0 -0
  53. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/protocols/protocol.py +0 -0
  54. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/scripts/__init__.py +0 -0
  55. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/scripts/syndesi.py +0 -0
  56. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/scripts/syndesi_backend.py +0 -0
  57. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/tools/__init__.py +0 -0
  58. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/tools/backend_logger.py +0 -0
  59. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/tools/exceptions.py +0 -0
  60. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/tools/internal.py +0 -0
  61. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/tools/log.py +0 -0
  62. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/tools/log_settings.py +0 -0
  63. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi/tools/types.py +0 -0
  64. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi.egg-info/SOURCES.txt +0 -0
  65. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi.egg-info/dependency_links.txt +0 -0
  66. {syndesi-0.3.2 → syndesi-0.4.1}/syndesi.egg-info/entry_points.txt +0 -0
  67. {syndesi-0.3.2 → syndesi-0.4.1}/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.3.2
3
+ Version: 0.4.1
4
4
  Summary: Syndesi
5
5
  Author-email: Sébastien Deriaz <sebastien.deriaz1@gmail.com>
6
6
  License: GPL
@@ -17,6 +17,7 @@ License-File: LICENSE
17
17
  Requires-Dist: prompt_toolkit
18
18
  Requires-Dist: pyserial
19
19
  Requires-Dist: rich
20
+ Requires-Dist: platformdirs
20
21
  Dynamic: license-file
21
22
 
22
23
  # Syndesi Python Implementation
@@ -22,7 +22,7 @@ classifiers = [
22
22
  "Operating System :: MacOS :: MacOS X",
23
23
  "Operating System :: Microsoft :: Windows",
24
24
  ]
25
- dependencies = ['prompt_toolkit', 'pyserial', 'rich']
25
+ dependencies = ['prompt_toolkit', 'pyserial', 'rich', 'platformdirs']
26
26
 
27
27
  #[project.optional-dependencies]
28
28
  #extra = [""]
@@ -22,13 +22,12 @@ import subprocess
22
22
  import sys
23
23
  import threading
24
24
  import time
25
- import uuid
26
25
  import weakref
27
26
  from abc import ABC, abstractmethod
28
27
  from collections.abc import Callable
29
28
  from multiprocessing.connection import Client, Connection
30
29
  from types import EllipsisType
31
- from typing import Any, cast
30
+ from typing import Any
32
31
  import os
33
32
 
34
33
  from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
@@ -38,72 +37,51 @@ from ..tools.backend_api import (
38
37
  BACKEND_PORT,
39
38
  Action,
40
39
  BackendResponse,
40
+ Fragment,
41
41
  default_host,
42
- is_event,
43
42
  raise_if_error,
43
+ EXTRA_BUFFER_RESPONSE_TIME
44
44
  )
45
45
  from ..tools.log_settings import LoggerAlias
46
46
  from .backend.adapter_backend import (
47
47
  AdapterDisconnected,
48
- AdapterReadInit,
48
+ AdapterResponseTimeout,
49
49
  AdapterReadPayload,
50
50
  AdapterSignal,
51
51
  )
52
52
  from .backend.descriptors import Descriptor
53
- from .stop_condition import StopCondition, TimeoutStopCondition
53
+ from .stop_condition import StopCondition, Continuation, StopConditionType, Total
54
54
  from .timeout import Timeout, TimeoutAction, any_to_timeout
55
55
 
56
- DEFAULT_STOP_CONDITION = [TimeoutStopCondition(continuation=0.1)]
56
+ DEFAULT_STOP_CONDITION = Continuation(time=0.1)
57
57
 
58
58
  DEFAULT_TIMEOUT = Timeout(response=5, action='error')
59
59
 
60
- SHUTDOWN_DELAY = 2
61
-
62
60
  # Maximum time to let the backend start
63
61
  START_TIMEOUT = 2
62
+ # Time to shutdown the backend
63
+ SHUTDOWN_DELAY = 2
64
64
 
65
- # from enum import Enum, auto
66
-
67
-
68
- # class CallbackEvent(Enum):
69
- # DATA_READY = auto()
70
- # ADAPTER_DISCONNECTED = auto()
71
-
72
- import time
73
- import queue
74
- from typing import TypeVar, Generic
75
-
76
- T = TypeVar("T")
65
+ class SignalQueue(queue.Queue[AdapterSignal]):
66
+ def __init__(self) -> None:
67
+ self._read_payload_counter = 0
68
+ super().__init__(0)
77
69
 
78
- class PeekQueue(queue.Queue, Generic[T]):
79
- def peek(self, block: bool = True, timeout: float | None = None) -> T:
80
- """
81
- Return (without removing) the head item.
70
+ def has_read_payload(self) -> bool:
71
+ return self._read_payload_counter > 0
82
72
 
83
- Args:
84
- block: If False, raise queue.Empty immediately if empty.
85
- timeout: Max seconds to wait if block=True. None means wait forever.
86
73
 
87
- Raises:
88
- queue.Empty: if no item is available within constraints.
89
- """
90
- with self.not_empty:
91
- if not block:
92
- if not self._qsize():
93
- raise queue.Empty
94
- return self.queue[0] # type: ignore[attr-defined]
95
-
96
- end = None if timeout is None else time.monotonic() + timeout
97
- while not self._qsize():
98
- if timeout is None:
99
- self.not_empty.wait()
100
- else:
101
- remaining = end - time.monotonic()
102
- if remaining <= 0:
103
- raise queue.Empty
104
- self.not_empty.wait(remaining)
74
+ def put(self, signal: AdapterSignal, block: bool = True, timeout: float | None = None) -> None:
75
+ if isinstance(signal, AdapterReadPayload):
76
+ self._read_payload_counter += 1
77
+ return super().put(signal, block, timeout)
78
+
105
79
 
106
- return self.queue[0] # type: ignore[attr-defined]
80
+ def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
81
+ signal = super().get(block, timeout)
82
+ if isinstance(signal, AdapterReadPayload):
83
+ self._read_payload_counter -= 1
84
+ return signal
107
85
 
108
86
 
109
87
  def is_backend_running(address: str, port: int) -> bool:
@@ -128,29 +106,23 @@ def start_backend(port: int | None = None) -> None:
128
106
  str(BACKEND_PORT if port is None else port),
129
107
  ]
130
108
 
131
- # Always sever stdio — if you leave any of these inherited,
132
- # you can keep an implicit console/TTY attachment.
133
109
  stdin = subprocess.DEVNULL
134
110
  stdout = subprocess.DEVNULL
135
111
  stderr = subprocess.DEVNULL
136
112
 
137
113
  if os.name == "posix":
138
- # New session == new process group, no longer the terminal's foreground PG.
139
- # This prevents keyboard SIGINT/SIGTSTP from the parent's TTY.
140
114
  subprocess.Popen(
141
115
  arguments,
142
- # cwd=None,
143
- # env=None,
144
116
  stdin=stdin,
145
117
  stdout=stdout,
146
118
  stderr=stderr,
147
- start_new_session=True, # safer than preexec_fn=os.setsid in threaded parents
148
- close_fds=True, # ensure we don't leak any FDs that tie us to the parent
119
+ start_new_session=True,
120
+ close_fds=True,
149
121
  )
150
122
 
151
123
  else:
152
124
  # Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
153
- CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP
125
+ CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
154
126
  DETACHED_PROCESS = 0x00000008 # not exposed by subprocess on all Pythons
155
127
  # Optional: CREATE_NO_WINDOW (no window even for console apps)
156
128
  CREATE_NO_WINDOW = 0x08000000
@@ -159,13 +131,11 @@ def start_backend(port: int | None = None) -> None:
159
131
 
160
132
  subprocess.Popen(
161
133
  arguments,
162
- # cwd=cwd,
163
- # env=env,
164
134
  stdin=stdin,
165
135
  stdout=stdout,
166
136
  stderr=stderr,
167
137
  creationflags=creationflags,
168
- close_fds=True, # break handle inheritance (important with DETACHED_PROCESS)
138
+ close_fds=True,
169
139
  )
170
140
 
171
141
  class ReadScope(Enum):
@@ -177,7 +147,7 @@ class Adapter(ABC):
177
147
  self,
178
148
  descriptor: Descriptor,
179
149
  alias: str = "",
180
- stop_conditions: StopCondition | EllipsisType | list = ...,
150
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
181
151
  timeout: Timeout | EllipsisType | NumberLike | None = ...,
182
152
  encoding: str = "utf-8",
183
153
  event_callback: Callable[[AdapterSignal], None] | None = None,
@@ -203,7 +173,7 @@ class Adapter(ABC):
203
173
  super().__init__()
204
174
  self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
205
175
  self.encoding = encoding
206
- self._event_queue: PeekQueue[BackendResponse] = PeekQueue()
176
+ self._signal_queue: SignalQueue = SignalQueue()
207
177
  self.event_callback: Callable[[AdapterSignal], None] | None = event_callback
208
178
  self.backend_connection: Connection | None = None
209
179
  self._backend_connection_lock = threading.Lock()
@@ -211,7 +181,6 @@ class Adapter(ABC):
211
181
  self._make_backend_request_flag = threading.Event()
212
182
  self.opened = False
213
183
  self._alias = alias
214
- self._read_buffer = []
215
184
 
216
185
  if backend_address is None:
217
186
  self._backend_address = default_host
@@ -235,10 +204,10 @@ class Adapter(ABC):
235
204
  self.auto_open = auto_open
236
205
 
237
206
  # Set the stop-condition
238
- self._stop_conditions: list[StopCondition | None]
207
+ self._stop_conditions: list[StopCondition]
239
208
  if stop_conditions is ...:
240
209
  self._default_stop_condition = True
241
- self._stop_conditions = DEFAULT_STOP_CONDITION
210
+ self._stop_conditions = [DEFAULT_STOP_CONDITION]
242
211
  else:
243
212
  self._default_stop_condition = False
244
213
  if isinstance(stop_conditions, StopCondition):
@@ -306,7 +275,7 @@ class Adapter(ABC):
306
275
  raise RuntimeError("Failed to connect to backend") from err
307
276
  self._read_thread = threading.Thread(
308
277
  target=self.read_thread,
309
- args=(self._event_queue, self._make_backend_request_queue),
278
+ args=(self._signal_queue, self._make_backend_request_queue),
310
279
  daemon=True,
311
280
  )
312
281
  self._read_thread.start()
@@ -348,7 +317,7 @@ class Adapter(ABC):
348
317
 
349
318
  def read_thread(
350
319
  self,
351
- event_queue: queue.Queue[BackendResponse],
320
+ signal_queue: SignalQueue,
352
321
  request_queue: queue.Queue[BackendResponse],
353
322
  ) -> None:
354
323
  while True:
@@ -357,7 +326,7 @@ class Adapter(ABC):
357
326
  raise RuntimeError("Backend connection wasn't initialized")
358
327
  response: tuple[Any, ...] = self.backend_connection.recv()
359
328
  except (EOFError, TypeError, OSError):
360
- event_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
329
+ signal_queue.put(AdapterDisconnected())
361
330
  request_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
362
331
  break
363
332
  else:
@@ -365,13 +334,14 @@ class Adapter(ABC):
365
334
  raise RuntimeError(f"Invalid response from backend : {response}")
366
335
  action = Action(response[0])
367
336
 
368
- if is_event(action):
337
+ if action == Action.ADAPTER_SIGNAL:
338
+ #if is_event(action):
369
339
  if len(response) <= 1:
370
340
  raise RuntimeError(f"Invalid event response : {response}")
341
+ signal: AdapterSignal = response[1]
371
342
  if self.event_callback is not None:
372
- signal: AdapterSignal = response[1]
373
343
  self.event_callback(signal)
374
- event_queue.put(response)
344
+ signal_queue.put(signal)
375
345
  else:
376
346
  request_queue.put(response)
377
347
 
@@ -401,7 +371,7 @@ class Adapter(ABC):
401
371
  self._logger.debug(f"Setting default timeout to {default_timeout}")
402
372
  self._timeout = default_timeout
403
373
 
404
- def set_stop_conditions(self, stop_conditions: StopCondition | None | list) -> None:
374
+ def set_stop_conditions(self, stop_conditions: StopCondition | None | list[StopCondition]) -> None:
405
375
  """
406
376
  Overwrite the stop-condition
407
377
 
@@ -416,11 +386,7 @@ class Adapter(ABC):
416
386
  elif stop_conditions is None:
417
387
  self._stop_conditions = []
418
388
 
419
- # if self._stop_conditions is None:
420
- # payload = None
421
- # else:
422
- # payload = self._stop_conditions.compose_json()
423
- self._make_backend_request(Action.SET_STOP_CONDITION, self._stop_conditions)
389
+ self._make_backend_request(Action.SET_STOP_CONDITIONs, self._stop_conditions)
424
390
 
425
391
  def set_default_stop_condition(self, stop_condition: StopCondition) -> None:
426
392
  """
@@ -442,7 +408,7 @@ class Adapter(ABC):
442
408
  )
443
409
  while True:
444
410
  try:
445
- self._event_queue.get(block=False)
411
+ self._signal_queue.get(block=False)
446
412
  except queue.Empty:
447
413
  break
448
414
 
@@ -469,11 +435,11 @@ class Adapter(ABC):
469
435
  """
470
436
  Stop communication with the device
471
437
  """
472
- self._logger.debug("Closing adapter frontend")
473
- self._make_backend_request(Action.CLOSE)
474
438
  if force:
439
+ self._logger.debug("Closing adapter frontend")
440
+ else:
475
441
  self._logger.debug("Force closing adapter backend")
476
- self._make_backend_request(Action.FORCE_CLOSE)
442
+ self._make_backend_request(Action.CLOSE, force)
477
443
 
478
444
  with self._backend_connection_lock:
479
445
  if self.backend_connection is not None:
@@ -497,9 +463,9 @@ class Adapter(ABC):
497
463
  def read_detailed(
498
464
  self,
499
465
  timeout: Timeout | EllipsisType | None = ...,
500
- stop_condition: StopCondition | EllipsisType | None = ...,
466
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
501
467
  scope : str = ReadScope.BUFFERED.value,
502
- ) -> tuple[bytes, AdapterReadPayload | None]:
468
+ ) -> AdapterReadPayload:
503
469
  """
504
470
  Read data from the device
505
471
 
@@ -517,110 +483,104 @@ class Adapter(ABC):
517
483
  signal : AdapterReadPayload
518
484
  """
519
485
  _scope = ReadScope(scope)
520
- # Okay idea : Remove the start read and instead ask for the time of the backend.
521
- # Then we read whatever payload comes from the backend and compare that to the time
522
- # If it doesn't match our criteria, we trash it
523
- # When waiting for the backend payload, we wait +0.5s so make sure we received everything
524
- # This 0.5s could be changed if we're local or not by the way
525
-
486
+ output_signal = None
487
+ read_timeout = None
526
488
 
527
- # First, ask for the backend time, this is the official start of the read
528
- backend_read_start_time = cast(NumberLike, self._make_backend_request(Action.GET_BACKEND_TIME)[0])
529
-
530
- # If not timeout is specified, use the default one
531
489
  if timeout is ...:
532
490
  read_timeout = self._timeout
533
491
  else:
534
492
  read_timeout = any_to_timeout(timeout)
493
+
494
+ if read_timeout is None:
495
+ raise RuntimeError("Cannot read without setting a timeout")
496
+
497
+ if stop_conditions is not ...:
498
+ if isinstance(stop_conditions, StopCondition):
499
+ stop_conditions = [stop_conditions]
500
+ self._make_backend_request(Action.SET_STOP_CONDITIONs, stop_conditions)
501
+
502
+ # First, we check if data is in the buffer and if the scope if set to BUFFERED
503
+ while _scope == ReadScope.BUFFERED and self._signal_queue.has_read_payload():
504
+ signal = self._signal_queue.get()
505
+ if isinstance(signal, AdapterReadPayload):
506
+ output_signal = signal
507
+ break
508
+ # TODO : Implement disconnect ?
509
+ else:
510
+ # Nothing was found, ask the backend with a START_READ request. The backend will
511
+ # respond at most after the response_time with either data or a RESPONSE_TIMEOUT
535
512
 
536
- if read_timeout is not None:
537
513
  if not read_timeout.is_initialized():
538
514
  raise RuntimeError("Timeout needs to be initialized")
539
515
 
540
- # Calculate last_valid_timestamp, the limit at which a payload is not accepted anymore
541
- # Calculate the queue timeout (time for a response + small delay)
542
- #last_valid_timestamp = None
543
- queue_timeout_timestamp = None
544
- if read_timeout is not None:
545
- response_delay = read_timeout.response()
546
- else:
547
- response_delay = None
548
-
549
- if response_delay is not None:
550
- #last_valid_timestamp = backend_read_start_time + response_delay
551
- queue_timeout_timestamp = time.time() + response_delay + BACKEND_REQUEST_DEFAULT_TIMEOUT
516
+ _response = read_timeout.response()
552
517
 
553
- output_signal : AdapterReadPayload | None
518
+ read_init_time = time.time()
519
+ start_read_id = self._make_backend_request(Action.START_READ, _response)[0]
554
520
 
555
- # Ready to read payloads
556
- while True:
557
- if queue_timeout_timestamp is None:
558
- queue_timeout = None
521
+ if _response is None:
522
+ # Wait indefinitely
523
+ read_stop_timestamp = None
559
524
  else:
560
- queue_timeout = queue_timeout_timestamp - time.time()
561
- if queue_timeout < 0:
562
- queue_timeout = 0
525
+ # Wait for the response time + a bit more
526
+ read_stop_timestamp = read_init_time + _response
563
527
 
564
- try:
565
- response = self._event_queue.peek(block=True, timeout=queue_timeout)
566
- signal = response[1]
528
+ # else:
529
+ # start_read_id = None
530
+ # read_init_time = None
567
531
 
532
+
533
+ while True:
534
+ try:
535
+ if read_stop_timestamp is None:
536
+ queue_timeout = None
537
+ else:
538
+ queue_timeout = max(0, read_stop_timestamp - time.time() + EXTRA_BUFFER_RESPONSE_TIME)
539
+
540
+ signal = self._signal_queue.get(timeout=queue_timeout)
541
+ except queue.Empty:
542
+ raise RuntimeError('Failed to receive response from backend')
568
543
  if isinstance(signal, AdapterReadPayload):
569
- if response_delay is not None and signal.response_timestamp - backend_read_start_time > response_delay:
570
- # This signal happened after the max response time, act as if a timeout occured
571
- # and do not pop it out of the queue
572
- # TODO : Make _timeout always Timeout, never None ?
573
- output_signal = None
574
- break
575
-
576
- if _scope == ReadScope.NEXT and signal.response_timestamp < backend_read_start_time:
577
- # The payload happened before the read start
578
- self._event_queue.get()
579
- continue
580
-
581
- if response_delay is not None:
582
- if signal.response_timestamp - backend_read_start_time > response_delay:
583
- self._event_queue.get()
584
- output_signal = None
585
- break
586
-
587
- # Other wise the payload is valid
588
- self._event_queue.get()
589
544
  output_signal = signal
590
545
  break
591
-
592
546
  elif isinstance(signal, AdapterDisconnected):
593
- self._event_queue.get()
594
547
  raise RuntimeError("Adapter disconnected")
595
-
596
- except queue.Empty:
597
- output_signal = None
598
- break
599
-
600
-
548
+ elif isinstance(signal, AdapterResponseTimeout):
549
+ if start_read_id == signal.identifier:
550
+ output_signal = None
551
+ break
552
+ # Otherwise ignore it
601
553
 
602
554
  if output_signal is None:
603
- # TODO : Make _timeout always Timeout, never None ?
604
- if read_timeout.action == TimeoutAction.RETURN:
605
- data = b""
606
- output_signal = None
607
- elif read_timeout.action == TimeoutAction.ERROR:
608
- raise TimeoutError(
609
- f"No response received from device within {read_timeout.response()} seconds"
610
- )
611
- else:
612
- data = output_signal.data()
613
- output_signal.response_delay = output_signal.response_timestamp - backend_read_start_time
614
-
555
+ # TODO : Make read_timeout always Timeout, never None ?
556
+ match read_timeout.action:
557
+ case TimeoutAction.RETURN_EMPTY:
558
+ t = time.time()
559
+ return AdapterReadPayload(
560
+ fragments=[Fragment(b'', t)],
561
+ stop_timestamp=t,
562
+ stop_condition_type=StopConditionType.TIMEOUT,
563
+ previous_read_buffer_used=False,
564
+ response_timestamp=None,
565
+ response_delay=None
566
+ )
567
+ case TimeoutAction.ERROR:
568
+ raise TimeoutError(
569
+ f"No response received from device within {read_timeout.response()} seconds"
570
+ )
571
+ case _:
572
+ raise NotImplementedError()
615
573
 
616
- return data, output_signal
574
+ else:
575
+ return output_signal
617
576
 
618
577
  def read(
619
578
  self,
620
579
  timeout: Timeout | EllipsisType | None = ...,
621
- stop_condition: StopCondition | EllipsisType | None = ...,
580
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
622
581
  ) -> bytes:
623
- return self.read_detailed(timeout=timeout, stop_condition=stop_condition)[0]
582
+ signal = self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
583
+ return signal.data()
624
584
 
625
585
  def _cleanup(self) -> None:
626
586
  if self._init_ok and self.opened:
@@ -630,8 +590,8 @@ class Adapter(ABC):
630
590
  self,
631
591
  data: bytes | str,
632
592
  timeout: Timeout | EllipsisType | None = ...,
633
- stop_condition: StopCondition | EllipsisType | None = ...,
634
- ) -> tuple[bytes, AdapterReadPayload | None]:
593
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
594
+ ) -> AdapterReadPayload:
635
595
  """
636
596
  Shortcut function that combines
637
597
  - flush_read
@@ -640,17 +600,18 @@ class Adapter(ABC):
640
600
  """
641
601
  self.flushRead()
642
602
  self.write(data)
643
- return self.read_detailed(timeout=timeout, stop_condition=stop_condition)
603
+ return self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
644
604
 
645
605
  def query(
646
606
  self,
647
607
  data: bytes | str,
648
608
  timeout: Timeout | EllipsisType | None = ...,
649
- stop_condition: StopCondition | EllipsisType | None = ...,
609
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
650
610
  ) -> bytes:
651
- return self.query_detailed(
652
- data=data, timeout=timeout, stop_condition=stop_condition
653
- )[0]
611
+ signal = self.query_detailed(
612
+ data=data, timeout=timeout, stop_conditions=stop_conditions
613
+ )
614
+ return signal.data()
654
615
 
655
616
  def set_event_callback(self, callback: Callable[[AdapterSignal], None]) -> None:
656
617
  self.event_callback = callback
@@ -37,7 +37,7 @@ def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
37
37
  return IP(
38
38
  address=descriptor.address,
39
39
  port=descriptor.port,
40
- transport=descriptor.transport,
40
+ transport=descriptor.transport.value,
41
41
  )
42
42
  elif isinstance(descriptor, SerialPortDescriptor):
43
43
  return SerialPort(port=descriptor.port, baudrate=descriptor.baudrate)