syndesi 0.3.2__tar.gz → 0.4.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 (67) hide show
  1. {syndesi-0.3.2/syndesi.egg-info → syndesi-0.4.0}/PKG-INFO +2 -1
  2. {syndesi-0.3.2 → syndesi-0.4.0}/pyproject.toml +1 -1
  3. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/adapter.py +191 -159
  4. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/adapter_backend.py +97 -63
  5. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/adapter_session.py +51 -151
  6. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/serialport_backend.py +14 -8
  7. syndesi-0.4.0/syndesi/adapters/backend/stop_condition_backend.py +198 -0
  8. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/serialport.py +4 -5
  9. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/stop_condition.py +47 -26
  10. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/timeout.py +1 -1
  11. syndesi-0.4.0/syndesi/cli/shell_tools.py +105 -0
  12. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/backend_api.py +14 -15
  13. syndesi-0.4.0/syndesi/tools/errors.py +51 -0
  14. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/version.py +1 -1
  15. {syndesi-0.3.2 → syndesi-0.4.0/syndesi.egg-info}/PKG-INFO +2 -1
  16. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi.egg-info/requires.txt +1 -0
  17. syndesi-0.3.2/syndesi/adapters/backend/stop_condition_backend.py +0 -342
  18. syndesi-0.3.2/syndesi/cli/shell_tools.py +0 -107
  19. syndesi-0.3.2/syndesi/tools/errors.py +0 -23
  20. {syndesi-0.3.2 → syndesi-0.4.0}/LICENSE +0 -0
  21. {syndesi-0.3.2 → syndesi-0.4.0}/README.md +0 -0
  22. {syndesi-0.3.2 → syndesi-0.4.0}/setup.cfg +0 -0
  23. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/__init__.py +0 -0
  24. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/__main__.py +0 -0
  25. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/__init__.py +0 -0
  26. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/auto.py +0 -0
  27. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/__init__.py +0 -0
  28. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/adapter_manager.py +0 -0
  29. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/backend.py +0 -0
  30. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/backend_tools.py +0 -0
  31. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/descriptors.py +0 -0
  32. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/ip_backend.py +0 -0
  33. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/timed_queue.py +0 -0
  34. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/timeout.py +0 -0
  35. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/visa_backend.py +0 -0
  36. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/ip.py +0 -0
  37. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/ip_server.py +0 -0
  38. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/visa.py +0 -0
  39. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/__init__.py +0 -0
  40. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/backend_console.py +0 -0
  41. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/backend_status.py +0 -0
  42. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/backend_wrapper.py +0 -0
  43. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/console.py +0 -0
  44. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/shell.py +0 -0
  45. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/terminal.py +0 -0
  46. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/terminal_apps.py +0 -0
  47. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/terminal_tools.py +0 -0
  48. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/__init__.py +0 -0
  49. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/delimited.py +0 -0
  50. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/modbus.py +0 -0
  51. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/protocol.py +0 -0
  52. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/raw.py +0 -0
  53. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/scpi.py +0 -0
  54. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/scripts/__init__.py +0 -0
  55. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/scripts/syndesi.py +0 -0
  56. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/scripts/syndesi_backend.py +0 -0
  57. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/__init__.py +0 -0
  58. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/backend_logger.py +0 -0
  59. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/exceptions.py +0 -0
  60. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/internal.py +0 -0
  61. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/log.py +0 -0
  62. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/log_settings.py +0 -0
  63. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/types.py +0 -0
  64. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi.egg-info/SOURCES.txt +0 -0
  65. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi.egg-info/dependency_links.txt +0 -0
  66. {syndesi-0.3.2 → syndesi-0.4.0}/syndesi.egg-info/entry_points.txt +0 -0
  67. {syndesi-0.3.2 → syndesi-0.4.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.3.2
3
+ Version: 0.4.0
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
@@ -39,71 +38,49 @@ from ..tools.backend_api import (
39
38
  Action,
40
39
  BackendResponse,
41
40
  default_host,
42
- is_event,
43
41
  raise_if_error,
42
+ EXTRA_BUFFER_RESPONSE_TIME
44
43
  )
45
44
  from ..tools.log_settings import LoggerAlias
46
45
  from .backend.adapter_backend import (
47
46
  AdapterDisconnected,
48
- AdapterReadInit,
47
+ AdapterResponseTimeout,
49
48
  AdapterReadPayload,
50
49
  AdapterSignal,
51
50
  )
52
51
  from .backend.descriptors import Descriptor
53
- from .stop_condition import StopCondition, TimeoutStopCondition
52
+ from .stop_condition import StopCondition, Continuation, Total
54
53
  from .timeout import Timeout, TimeoutAction, any_to_timeout
55
54
 
56
- DEFAULT_STOP_CONDITION = [TimeoutStopCondition(continuation=0.1)]
55
+ DEFAULT_STOP_CONDITION = Continuation(time=0.1)
57
56
 
58
57
  DEFAULT_TIMEOUT = Timeout(response=5, action='error')
59
58
 
60
- SHUTDOWN_DELAY = 2
61
-
62
59
  # Maximum time to let the backend start
63
60
  START_TIMEOUT = 2
61
+ # Time to shutdown the backend
62
+ SHUTDOWN_DELAY = 2
64
63
 
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
64
+ class SignalQueue(queue.Queue[AdapterSignal]):
65
+ def __init__(self) -> None:
66
+ self._read_payload_counter = 0
67
+ super().__init__(0)
75
68
 
76
- T = TypeVar("T")
69
+ def has_read_payload(self) -> bool:
70
+ return self._read_payload_counter > 0
77
71
 
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.
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
-
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)
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
+
105
78
 
106
- return self.queue[0] # type: ignore[attr-defined]
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
107
84
 
108
85
 
109
86
  def is_backend_running(address: str, port: int) -> bool:
@@ -128,29 +105,23 @@ def start_backend(port: int | None = None) -> None:
128
105
  str(BACKEND_PORT if port is None else port),
129
106
  ]
130
107
 
131
- # Always sever stdio — if you leave any of these inherited,
132
- # you can keep an implicit console/TTY attachment.
133
108
  stdin = subprocess.DEVNULL
134
109
  stdout = subprocess.DEVNULL
135
110
  stderr = subprocess.DEVNULL
136
111
 
137
112
  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
113
  subprocess.Popen(
141
114
  arguments,
142
- # cwd=None,
143
- # env=None,
144
115
  stdin=stdin,
145
116
  stdout=stdout,
146
117
  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
118
+ start_new_session=True,
119
+ close_fds=True,
149
120
  )
150
121
 
151
122
  else:
152
123
  # Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
153
- CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP
124
+ CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
154
125
  DETACHED_PROCESS = 0x00000008 # not exposed by subprocess on all Pythons
155
126
  # Optional: CREATE_NO_WINDOW (no window even for console apps)
156
127
  CREATE_NO_WINDOW = 0x08000000
@@ -159,13 +130,11 @@ def start_backend(port: int | None = None) -> None:
159
130
 
160
131
  subprocess.Popen(
161
132
  arguments,
162
- # cwd=cwd,
163
- # env=env,
164
133
  stdin=stdin,
165
134
  stdout=stdout,
166
135
  stderr=stderr,
167
136
  creationflags=creationflags,
168
- close_fds=True, # break handle inheritance (important with DETACHED_PROCESS)
137
+ close_fds=True,
169
138
  )
170
139
 
171
140
  class ReadScope(Enum):
@@ -177,7 +146,7 @@ class Adapter(ABC):
177
146
  self,
178
147
  descriptor: Descriptor,
179
148
  alias: str = "",
180
- stop_conditions: StopCondition | EllipsisType | list = ...,
149
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
181
150
  timeout: Timeout | EllipsisType | NumberLike | None = ...,
182
151
  encoding: str = "utf-8",
183
152
  event_callback: Callable[[AdapterSignal], None] | None = None,
@@ -203,7 +172,7 @@ class Adapter(ABC):
203
172
  super().__init__()
204
173
  self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
205
174
  self.encoding = encoding
206
- self._event_queue: PeekQueue[BackendResponse] = PeekQueue()
175
+ self._signal_queue: SignalQueue = SignalQueue()
207
176
  self.event_callback: Callable[[AdapterSignal], None] | None = event_callback
208
177
  self.backend_connection: Connection | None = None
209
178
  self._backend_connection_lock = threading.Lock()
@@ -211,7 +180,6 @@ class Adapter(ABC):
211
180
  self._make_backend_request_flag = threading.Event()
212
181
  self.opened = False
213
182
  self._alias = alias
214
- self._read_buffer = []
215
183
 
216
184
  if backend_address is None:
217
185
  self._backend_address = default_host
@@ -235,10 +203,10 @@ class Adapter(ABC):
235
203
  self.auto_open = auto_open
236
204
 
237
205
  # Set the stop-condition
238
- self._stop_conditions: list[StopCondition | None]
206
+ self._stop_conditions: list[StopCondition]
239
207
  if stop_conditions is ...:
240
208
  self._default_stop_condition = True
241
- self._stop_conditions = DEFAULT_STOP_CONDITION
209
+ self._stop_conditions = [DEFAULT_STOP_CONDITION]
242
210
  else:
243
211
  self._default_stop_condition = False
244
212
  if isinstance(stop_conditions, StopCondition):
@@ -306,7 +274,7 @@ class Adapter(ABC):
306
274
  raise RuntimeError("Failed to connect to backend") from err
307
275
  self._read_thread = threading.Thread(
308
276
  target=self.read_thread,
309
- args=(self._event_queue, self._make_backend_request_queue),
277
+ args=(self._signal_queue, self._make_backend_request_queue),
310
278
  daemon=True,
311
279
  )
312
280
  self._read_thread.start()
@@ -348,7 +316,7 @@ class Adapter(ABC):
348
316
 
349
317
  def read_thread(
350
318
  self,
351
- event_queue: queue.Queue[BackendResponse],
319
+ signal_queue: SignalQueue,
352
320
  request_queue: queue.Queue[BackendResponse],
353
321
  ) -> None:
354
322
  while True:
@@ -357,7 +325,7 @@ class Adapter(ABC):
357
325
  raise RuntimeError("Backend connection wasn't initialized")
358
326
  response: tuple[Any, ...] = self.backend_connection.recv()
359
327
  except (EOFError, TypeError, OSError):
360
- event_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
328
+ signal_queue.put(AdapterDisconnected())
361
329
  request_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
362
330
  break
363
331
  else:
@@ -365,13 +333,14 @@ class Adapter(ABC):
365
333
  raise RuntimeError(f"Invalid response from backend : {response}")
366
334
  action = Action(response[0])
367
335
 
368
- if is_event(action):
336
+ if action == Action.ADAPTER_SIGNAL:
337
+ #if is_event(action):
369
338
  if len(response) <= 1:
370
339
  raise RuntimeError(f"Invalid event response : {response}")
340
+ signal: AdapterSignal = response[1]
371
341
  if self.event_callback is not None:
372
- signal: AdapterSignal = response[1]
373
342
  self.event_callback(signal)
374
- event_queue.put(response)
343
+ signal_queue.put(signal)
375
344
  else:
376
345
  request_queue.put(response)
377
346
 
@@ -401,7 +370,7 @@ class Adapter(ABC):
401
370
  self._logger.debug(f"Setting default timeout to {default_timeout}")
402
371
  self._timeout = default_timeout
403
372
 
404
- def set_stop_conditions(self, stop_conditions: StopCondition | None | list) -> None:
373
+ def set_stop_conditions(self, stop_conditions: StopCondition | None | list[StopCondition]) -> None:
405
374
  """
406
375
  Overwrite the stop-condition
407
376
 
@@ -416,10 +385,6 @@ class Adapter(ABC):
416
385
  elif stop_conditions is None:
417
386
  self._stop_conditions = []
418
387
 
419
- # if self._stop_conditions is None:
420
- # payload = None
421
- # else:
422
- # payload = self._stop_conditions.compose_json()
423
388
  self._make_backend_request(Action.SET_STOP_CONDITION, self._stop_conditions)
424
389
 
425
390
  def set_default_stop_condition(self, stop_condition: StopCondition) -> None:
@@ -442,7 +407,7 @@ class Adapter(ABC):
442
407
  )
443
408
  while True:
444
409
  try:
445
- self._event_queue.get(block=False)
410
+ self._signal_queue.get(block=False)
446
411
  except queue.Empty:
447
412
  break
448
413
 
@@ -469,11 +434,11 @@ class Adapter(ABC):
469
434
  """
470
435
  Stop communication with the device
471
436
  """
472
- self._logger.debug("Closing adapter frontend")
473
- self._make_backend_request(Action.CLOSE)
474
437
  if force:
438
+ self._logger.debug("Closing adapter frontend")
439
+ else:
475
440
  self._logger.debug("Force closing adapter backend")
476
- self._make_backend_request(Action.FORCE_CLOSE)
441
+ self._make_backend_request(Action.CLOSE, force)
477
442
 
478
443
  with self._backend_connection_lock:
479
444
  if self.backend_connection is not None:
@@ -499,7 +464,7 @@ class Adapter(ABC):
499
464
  timeout: Timeout | EllipsisType | None = ...,
500
465
  stop_condition: StopCondition | EllipsisType | None = ...,
501
466
  scope : str = ReadScope.BUFFERED.value,
502
- ) -> tuple[bytes, AdapterReadPayload | None]:
467
+ ) -> AdapterReadPayload | None:
503
468
  """
504
469
  Read data from the device
505
470
 
@@ -516,111 +481,174 @@ class Adapter(ABC):
516
481
  data : bytes
517
482
  signal : AdapterReadPayload
518
483
  """
484
+ t = time.time()
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
526
487
 
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
- if timeout is ...:
532
- read_timeout = self._timeout
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 ?
533
495
  else:
534
- read_timeout = any_to_timeout(timeout)
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)
535
502
 
536
- if read_timeout is not None:
537
- if not read_timeout.is_initialized():
538
- raise RuntimeError("Timeout needs to be initialized")
503
+ if read_timeout is not None:
504
+ if not read_timeout.is_initialized():
505
+ raise RuntimeError("Timeout needs to be initialized")
539
506
 
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
507
+ _response = read_timeout.response()
548
508
 
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
509
+ read_init_time = time.time()
510
+ start_read_id = self._make_backend_request(Action.START_READ, _response)[0]
552
511
 
553
- output_signal : AdapterReadPayload | None
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
554
518
 
555
- # Ready to read payloads
556
- while True:
557
- if queue_timeout_timestamp is None:
558
- queue_timeout = None
559
519
  else:
560
- queue_timeout = queue_timeout_timestamp - time.time()
561
- if queue_timeout < 0:
562
- queue_timeout = 0
563
-
564
- try:
565
- response = self._event_queue.peek(block=True, timeout=queue_timeout)
566
- signal = response[1]
520
+ start_read_id = None
521
+ read_init_time = None
567
522
 
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')
568
534
  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 ?
535
+ output_signal = signal
536
+ break
537
+ elif isinstance(signal, AdapterDisconnected):
538
+ raise RuntimeError("Adapter disconnected")
539
+ elif isinstance(signal, AdapterResponseTimeout):
540
+ if start_read_id == signal.identifier:
573
541
  output_signal = None
574
542
  break
543
+ # Otherwise ignore it
575
544
 
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
545
 
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
- output_signal = signal
590
- break
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
591
560
 
592
- elif isinstance(signal, AdapterDisconnected):
593
- self._event_queue.get()
594
- raise RuntimeError("Adapter disconnected")
595
561
 
596
- except queue.Empty:
597
- output_signal = None
598
- break
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
599
567
 
600
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])
601
571
 
602
- 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
572
+ # If not timeout is specified, use the default one
614
573
 
615
-
616
- return data, output_signal
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
617
641
 
618
642
  def read(
619
643
  self,
620
644
  timeout: Timeout | EllipsisType | None = ...,
621
645
  stop_condition: StopCondition | EllipsisType | None = ...,
622
646
  ) -> bytes:
623
- return self.read_detailed(timeout=timeout, stop_condition=stop_condition)[0]
647
+ signal = self.read_detailed(timeout=timeout, stop_condition=stop_condition)
648
+ if signal is None:
649
+ return None
650
+ else:
651
+ return signal.data()
624
652
 
625
653
  def _cleanup(self) -> None:
626
654
  if self._init_ok and self.opened:
@@ -648,9 +676,13 @@ class Adapter(ABC):
648
676
  timeout: Timeout | EllipsisType | None = ...,
649
677
  stop_condition: StopCondition | EllipsisType | None = ...,
650
678
  ) -> bytes:
651
- return self.query_detailed(
679
+ signal = self.query_detailed(
652
680
  data=data, timeout=timeout, stop_condition=stop_condition
653
- )[0]
681
+ )
682
+ if signal is None:
683
+ return None
684
+ else:
685
+ return signal.data()
654
686
 
655
687
  def set_event_callback(self, callback: Callable[[AdapterSignal], None]) -> None:
656
688
  self.event_callback = callback