syndesi 0.4.2__tar.gz → 0.4.4__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 (68) hide show
  1. {syndesi-0.4.2/syndesi.egg-info → syndesi-0.4.4}/PKG-INFO +1 -1
  2. {syndesi-0.4.2 → syndesi-0.4.4}/pyproject.toml +6 -0
  3. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/__init__.py +16 -1
  4. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/adapter.py +147 -72
  5. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/auto.py +26 -15
  6. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/adapter_backend.py +3 -3
  7. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/adapter_session.py +8 -9
  8. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/backend.py +7 -2
  9. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/ip_backend.py +44 -41
  10. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/serialport_backend.py +17 -12
  11. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/stop_condition_backend.py +8 -5
  12. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/visa_backend.py +3 -3
  13. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/ip.py +30 -24
  14. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/ip_server.py +9 -3
  15. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/serialport.py +20 -7
  16. syndesi-0.4.4/syndesi/adapters/stop_condition.py +114 -0
  17. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/timeout.py +1 -0
  18. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/visa.py +11 -3
  19. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/console.py +1 -1
  20. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/shell.py +2 -2
  21. syndesi-0.4.4/syndesi/component.py +79 -0
  22. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/delimited.py +2 -2
  23. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/modbus.py +7 -6
  24. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/protocol.py +7 -1
  25. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/raw.py +2 -2
  26. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/scpi.py +1 -1
  27. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/scripts/syndesi.py +1 -1
  28. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/backend_api.py +8 -1
  29. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/errors.py +24 -4
  30. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/log.py +1 -0
  31. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/types.py +1 -0
  32. syndesi-0.4.4/syndesi/version.py +7 -0
  33. {syndesi-0.4.2 → syndesi-0.4.4/syndesi.egg-info}/PKG-INFO +1 -1
  34. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi.egg-info/SOURCES.txt +1 -0
  35. syndesi-0.4.2/syndesi/adapters/stop_condition.py +0 -90
  36. syndesi-0.4.2/syndesi/version.py +0 -3
  37. {syndesi-0.4.2 → syndesi-0.4.4}/LICENSE +0 -0
  38. {syndesi-0.4.2 → syndesi-0.4.4}/README.md +0 -0
  39. {syndesi-0.4.2 → syndesi-0.4.4}/setup.cfg +0 -0
  40. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/__main__.py +0 -0
  41. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/__init__.py +0 -0
  42. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/__init__.py +0 -0
  43. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/adapter_manager.py +0 -0
  44. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/backend_status.py +0 -0
  45. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/backend_tools.py +0 -0
  46. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/descriptors.py +0 -0
  47. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/timed_queue.py +0 -0
  48. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/adapters/backend/timeout.py +0 -0
  49. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/__init__.py +0 -0
  50. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/backend_console.py +0 -0
  51. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/backend_status.py +0 -0
  52. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/backend_wrapper.py +0 -0
  53. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/shell_tools.py +0 -0
  54. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/terminal.py +0 -0
  55. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/terminal_apps.py +0 -0
  56. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/cli/terminal_tools.py +0 -0
  57. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/protocols/__init__.py +0 -0
  58. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/scripts/__init__.py +0 -0
  59. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/scripts/syndesi_backend.py +0 -0
  60. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/__init__.py +0 -0
  61. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/backend_logger.py +0 -0
  62. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/exceptions.py +0 -0
  63. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/internal.py +0 -0
  64. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi/tools/log_settings.py +0 -0
  65. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi.egg-info/dependency_links.txt +0 -0
  66. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi.egg-info/entry_points.txt +0 -0
  67. {syndesi-0.4.2 → syndesi-0.4.4}/syndesi.egg-info/requires.txt +0 -0
  68. {syndesi-0.4.2 → syndesi-0.4.4}/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.4.4
4
4
  Summary: Syndesi
5
5
  Author-email: Sébastien Deriaz <sebastien.deriaz1@gmail.com>
6
6
  License: GPL
@@ -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"]
@@ -1,5 +1,10 @@
1
+ """
2
+ Syndesi module
3
+ """
4
+
1
5
  from .adapters.ip import IP
2
6
  from .adapters.serialport import SerialPort
7
+ from .adapters.timeout import Timeout
3
8
  from .adapters.visa import Visa
4
9
  from .protocols.delimited import Delimited
5
10
  from .protocols.modbus import Modbus
@@ -7,4 +12,14 @@ from .protocols.raw import Raw
7
12
  from .protocols.scpi import SCPI
8
13
  from .tools.log import log
9
14
 
10
- __all__ = ["IP", "SerialPort", "Visa", "Delimited", "Modbus", "Raw", "SCPI", "log"]
15
+ __all__ = [
16
+ "IP",
17
+ "SerialPort",
18
+ "Visa",
19
+ "Delimited",
20
+ "Modbus",
21
+ "Raw",
22
+ "SCPI",
23
+ "log",
24
+ "Timeout",
25
+ ]
@@ -1,19 +1,21 @@
1
1
  # File : adapters.py
2
2
  # Author : Sébastien Deriaz
3
3
  # License : GPL
4
- #
5
- # Adapters provide a common abstraction for the media layers (physical + data link + network)
6
- # The following classes are provided, which all are derived from the main Adapter class
7
- # - IP
8
- # - Serial
9
- # - VISA
10
- #
11
- # Note that technically VISA is not part of the media layer, only USB is.
12
- # This is a limitation as it is to this day not possible to communicate "raw"
13
- # with a device through USB yet
14
- #
15
- # An adapter is meant to work with bytes objects but it can accept strings.
16
- # Strings will automatically be converted to bytes using utf-8 encoding
4
+
5
+ """
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
+
12
+ Note that technically VISA is not part of the media layer, only USB is.
13
+ This is a limitation as it is to this day not possible to communicate "raw"
14
+ with a device through USB yet
15
+
16
+ An adapter is meant to work with bytes objects but it can accept strings.
17
+ Strings will automatically be converted to bytes using utf-8 encoding
18
+ """
17
19
 
18
20
  import logging
19
21
  import os
@@ -30,10 +32,18 @@ from multiprocessing.connection import Client, Connection
30
32
  from types import EllipsisType
31
33
  from typing import Any
32
34
 
33
- from syndesi.tools.types import NumberLike, is_number
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
34
43
 
35
44
  from ..tools.backend_api import (
36
45
  BACKEND_PORT,
46
+ DEFAULT_ADAPTER_OPEN_TIMEOUT,
37
47
  EXTRA_BUFFER_RESPONSE_TIME,
38
48
  Action,
39
49
  BackendResponse,
@@ -43,7 +53,7 @@ from ..tools.backend_api import (
43
53
  )
44
54
  from ..tools.log_settings import LoggerAlias
45
55
  from .backend.adapter_backend import (
46
- AdapterDisconnected,
56
+ AdapterDisconnectedSignal,
47
57
  AdapterReadPayload,
48
58
  AdapterResponseTimeout,
49
59
  AdapterSignal,
@@ -64,39 +74,61 @@ SHUTDOWN_DELAY = 2
64
74
 
65
75
 
66
76
  class SignalQueue(queue.Queue[AdapterSignal]):
77
+ """
78
+ A smart queue to hold adapter signals
79
+ """
67
80
  def __init__(self) -> None:
68
81
  self._read_payload_counter = 0
69
82
  super().__init__(0)
70
83
 
71
84
  def has_read_payload(self) -> bool:
85
+ """
86
+ Return True if the queue contains a read payload
87
+ """
72
88
  return self._read_payload_counter > 0
73
89
 
74
- def put(
75
- self, signal: AdapterSignal, block: bool = True, timeout: float | None = None
76
- ) -> None:
90
+ def put(self, signal: AdapterSignal) -> None:
91
+ """
92
+ Put a signal in the queue
93
+
94
+ Parameters
95
+ ----------
96
+ signal : AdapterSignal
97
+ """
77
98
  if isinstance(signal, AdapterReadPayload):
78
99
  self._read_payload_counter += 1
79
- return super().put(signal, block, timeout)
100
+ return super().put(signal)
80
101
 
81
102
  def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
103
+ """
104
+ Get an AdapterSignal from the queue
105
+ """
82
106
  signal = super().get(block, timeout)
83
107
  if isinstance(signal, AdapterReadPayload):
84
108
  self._read_payload_counter -= 1
85
109
  return signal
86
110
 
87
-
88
111
  def is_backend_running(address: str, port: int) -> bool:
89
-
112
+ """
113
+ Return True if the backend is running
114
+ """
90
115
  try:
91
116
  conn = Client((address, port))
92
117
  except ConnectionRefusedError:
93
118
  return False
94
- else:
95
- conn.close()
96
- return True
97
-
119
+ conn.close()
120
+ return True
98
121
 
99
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
+ """
100
132
  arguments = [
101
133
  sys.executable,
102
134
  "-m",
@@ -113,7 +145,7 @@ def start_backend(port: int | None = None) -> None:
113
145
  stderr = subprocess.DEVNULL
114
146
 
115
147
  if os.name == "posix":
116
- subprocess.Popen(
148
+ subprocess.Popen( #pylint: disable=consider-using-with
117
149
  arguments,
118
150
  stdin=stdin,
119
151
  stdout=stdout,
@@ -124,14 +156,14 @@ def start_backend(port: int | None = None) -> None:
124
156
 
125
157
  else:
126
158
  # Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
127
- CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
128
- DETACHED_PROCESS = 0x00000008 # not exposed by subprocess on all Pythons
159
+ create_new_process_group = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
160
+ detached_process = 0x00000008 # not exposed by subprocess on all Pythons
129
161
  # Optional: CREATE_NO_WINDOW (no window even for console apps)
130
- CREATE_NO_WINDOW = 0x08000000
162
+ create_no_window = 0x08000000
131
163
 
132
- creationflags = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS | CREATE_NO_WINDOW
164
+ creationflags = create_new_process_group | detached_process | create_no_window
133
165
 
134
- subprocess.Popen(
166
+ subprocess.Popen( #pylint: disable=consider-using-with
135
167
  arguments,
136
168
  stdin=stdin,
137
169
  stdout=stdout,
@@ -142,13 +174,27 @@ def start_backend(port: int | None = None) -> None:
142
174
 
143
175
 
144
176
  class ReadScope(Enum):
177
+ """
178
+ Read scope
179
+
180
+ NEXT : Only read data after the start of the read() call
181
+ BUFFERED : Return any data that was present before the read() call
182
+ """
145
183
  NEXT = "next"
146
184
  BUFFERED = "buffered"
147
185
 
148
186
 
149
- class Adapter(ABC):
187
+ class Adapter(Component[bytes]):
188
+ """
189
+ Adapter class
190
+
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
150
195
  def __init__(
151
196
  self,
197
+ *,
152
198
  descriptor: Descriptor,
153
199
  alias: str = "",
154
200
  stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
@@ -173,9 +219,7 @@ class Adapter(ABC):
173
219
  encoding : str
174
220
  Which encoding to use if str has to be encoded into bytes
175
221
  """
176
- self._init_ok = False
177
- super().__init__()
178
- self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
222
+ super().__init__(LoggerAlias.ADAPTER)
179
223
  self.encoding = encoding
180
224
  self._signal_queue: SignalQueue = SignalQueue()
181
225
  self.event_callback: Callable[[AdapterSignal], None] | None = event_callback
@@ -183,17 +227,12 @@ class Adapter(ABC):
183
227
  self._backend_connection_lock = threading.Lock()
184
228
  self._make_backend_request_queue: queue.Queue[BackendResponse] = queue.Queue()
185
229
  self._make_backend_request_flag = threading.Event()
186
- self.opened = False
230
+ self._opened = False
187
231
  self._alias = alias
188
232
 
189
- if backend_address is None:
190
- self._backend_address = default_host
191
- else:
192
- self._backend_address = backend_address
193
- if backend_port is None:
194
- self._backend_port = BACKEND_PORT
195
- else:
196
- self._backend_port = backend_port
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
197
236
 
198
237
  # There a two possibilities here
199
238
  # A) The descriptor is fully initialized
@@ -201,9 +240,6 @@ class Adapter(ABC):
201
240
  # B) The descriptor is not fully initialized
202
241
  # -> Wait for the protocol to set defaults and then connect the adapter
203
242
 
204
- assert isinstance(
205
- descriptor, Descriptor
206
- ), "descriptor must be a Descriptor class"
207
243
  self.descriptor = descriptor
208
244
  self.auto_open = auto_open
209
245
 
@@ -243,7 +279,6 @@ class Adapter(ABC):
243
279
  self.connect()
244
280
 
245
281
  weakref.finalize(self, self._cleanup)
246
- self._init_ok = True
247
282
 
248
283
  # We can auto-open only if auto_open is enabled and if
249
284
  # connection with the backend has been made (descriptor initialized)
@@ -251,6 +286,9 @@ class Adapter(ABC):
251
286
  self.open()
252
287
 
253
288
  def connect(self) -> None:
289
+ """
290
+ Connect to the backend
291
+ """
254
292
  if self.backend_connection is not None:
255
293
  # No need to connect, everything has been done already
256
294
  return
@@ -276,7 +314,7 @@ class Adapter(ABC):
276
314
  try:
277
315
  self.backend_connection = Client((default_host, BACKEND_PORT))
278
316
  except ConnectionRefusedError as err:
279
- raise RuntimeError("Failed to connect to backend") from err
317
+ raise BackendCommunicationError("Failed to connect to backend") from err
280
318
  self._read_thread = threading.Thread(
281
319
  target=self.read_thread,
282
320
  args=(self._signal_queue, self._make_backend_request_queue),
@@ -293,7 +331,12 @@ class Adapter(ABC):
293
331
  if self.auto_open:
294
332
  self.open()
295
333
 
296
- def _make_backend_request(self, action: Action, *args: Any) -> BackendResponse:
334
+ def _make_backend_request(
335
+ self,
336
+ action: Action,
337
+ *args: Any,
338
+ timeout: float = BACKEND_REQUEST_DEFAULT_TIMEOUT,
339
+ ) -> BackendResponse:
297
340
  """
298
341
  Send a request to the backend and return the arguments
299
342
  """
@@ -304,11 +347,9 @@ class Adapter(ABC):
304
347
 
305
348
  self._make_backend_request_flag.set()
306
349
  try:
307
- response = self._make_backend_request_queue.get(
308
- timeout=BACKEND_REQUEST_DEFAULT_TIMEOUT
309
- )
350
+ response = self._make_backend_request_queue.get(timeout=timeout)
310
351
  except queue.Empty as err:
311
- raise RuntimeError(
352
+ raise BackendCommunicationError(
312
353
  f"Failed to receive response from backend to {action}"
313
354
  ) from err
314
355
 
@@ -324,24 +365,33 @@ class Adapter(ABC):
324
365
  signal_queue: SignalQueue,
325
366
  request_queue: queue.Queue[BackendResponse],
326
367
  ) -> None:
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
+ """
327
374
  while True:
328
375
  try:
329
376
  if self.backend_connection is None:
330
377
  raise RuntimeError("Backend connection wasn't initialized")
331
378
  response: tuple[Any, ...] = self.backend_connection.recv()
332
379
  except (EOFError, TypeError, OSError):
333
- signal_queue.put(AdapterDisconnected())
380
+ signal_queue.put(AdapterDisconnectedSignal())
334
381
  request_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
335
382
  break
336
383
  else:
337
384
  if not isinstance(response, tuple):
338
- raise RuntimeError(f"Invalid response from backend : {response}")
385
+ raise BackendCommunicationError(
386
+ f"Invalid response from backend : {response}"
387
+ )
339
388
  action = Action(response[0])
340
389
 
341
390
  if action == Action.ADAPTER_SIGNAL:
342
- # if is_event(action):
343
391
  if len(response) <= 1:
344
- raise RuntimeError(f"Invalid event response : {response}")
392
+ raise BackendCommunicationError(
393
+ f"Invalid event response : {response}"
394
+ )
345
395
  signal: AdapterSignal = response[1]
346
396
  if self.event_callback is not None:
347
397
  self.event_callback(signal)
@@ -365,7 +415,8 @@ class Adapter(ABC):
365
415
 
366
416
  def set_default_timeout(self, default_timeout: Timeout | None) -> None:
367
417
  """
368
- Set the default timeout for this adapter. If a previous timeout has been set, it will be fused
418
+ Set the default timeout for this adapter. If a previous
419
+ timeout has been set, it will be fused
369
420
 
370
421
  Parameters
371
422
  ----------
@@ -405,7 +456,7 @@ class Adapter(ABC):
405
456
  if self._default_stop_condition:
406
457
  self.set_stop_conditions(stop_condition)
407
458
 
408
- def flushRead(self) -> None:
459
+ def flush_read(self) -> None:
409
460
  """
410
461
  Flush the input buffer
411
462
  """
@@ -432,9 +483,20 @@ class Adapter(ABC):
432
483
  """
433
484
  Start communication with the device
434
485
  """
435
- self._make_backend_request(Action.OPEN, self._stop_conditions)
486
+ self._make_backend_request(
487
+ Action.OPEN,
488
+ self._stop_conditions,
489
+ timeout=BACKEND_REQUEST_DEFAULT_TIMEOUT + DEFAULT_ADAPTER_OPEN_TIMEOUT,
490
+ )
436
491
  self._logger.info("Adapter opened")
437
- self.opened = True
492
+ self._opened = True
493
+
494
+ def try_open(self) -> bool:
495
+ try:
496
+ self.open()
497
+ except AdapterFailedToOpen:
498
+ return False
499
+ return True
438
500
 
439
501
  def close(self, force: bool = False) -> None:
440
502
  """
@@ -450,9 +512,19 @@ class Adapter(ABC):
450
512
  if self.backend_connection is not None:
451
513
  self.backend_connection.close()
452
514
 
453
- self.opened = False
515
+ self._opened = False
516
+
517
+ def is_opened(self) -> bool:
518
+ """
519
+ Return True if the adapter is opened and False otherwise
520
+
521
+ Returns
522
+ -------
523
+ opened : bool
524
+ """
525
+ return self._opened
454
526
 
455
- def write(self, data: bytes | str) -> None:
527
+ def write(self, data: bytes) -> None:
456
528
  """
457
529
  Send data to the device
458
530
 
@@ -544,13 +616,15 @@ class Adapter(ABC):
544
616
 
545
617
  signal = self._signal_queue.get(timeout=queue_timeout)
546
618
  except queue.Empty as e:
547
- raise RuntimeError("Failed to receive response from backend") from e
619
+ raise BackendCommunicationError(
620
+ "Failed to receive response from backend"
621
+ ) from e
548
622
  if isinstance(signal, AdapterReadPayload):
549
623
  output_signal = signal
550
624
  break
551
- elif isinstance(signal, AdapterDisconnected):
552
- raise RuntimeError("Adapter disconnected")
553
- elif isinstance(signal, AdapterResponseTimeout):
625
+ if isinstance(signal, AdapterDisconnectedSignal):
626
+ raise AdapterDisconnected()
627
+ if isinstance(signal, AdapterResponseTimeout):
554
628
  if start_read_id == signal.identifier:
555
629
  output_signal = None
556
630
  break
@@ -569,8 +643,9 @@ class Adapter(ABC):
569
643
  response_delay=None,
570
644
  )
571
645
  case TimeoutAction.ERROR:
572
- raise TimeoutError(
573
- f"No response received from device within {read_timeout.response()} seconds"
646
+ timeout_value = read_timeout.response()
647
+ raise AdapterTimeoutError(
648
+ float("nan") if timeout_value is None else timeout_value
574
649
  )
575
650
  case _:
576
651
  raise NotImplementedError()
@@ -587,7 +662,7 @@ class Adapter(ABC):
587
662
  return signal.data()
588
663
 
589
664
  def _cleanup(self) -> None:
590
- if self._init_ok and self.opened:
665
+ if self._opened:
591
666
  self.close()
592
667
 
593
668
  def query_detailed(
@@ -602,7 +677,7 @@ class Adapter(ABC):
602
677
  - write
603
678
  - read
604
679
  """
605
- self.flushRead()
680
+ self.flush_read()
606
681
  self.write(data)
607
682
  return self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
608
683
 
@@ -1,17 +1,19 @@
1
1
  # File : auto.py
2
2
  # Author : Sébastien Deriaz
3
3
  # License : GPL
4
- #
5
- # Automatic adapter function
6
- # This function is used to automatically choose an adapter based on the user's input
7
- # 192.168.1.1 -> IP
8
- # COM4 -> Serial
9
- # /dev/tty* -> Serial
10
- # etc...
11
- # If an adapter class is supplied, it is passed through
12
- #
13
- # Additionnaly, it is possible to do COM4:115200 so as to make the life of the user easier
14
- # Same with /dev/ttyACM0:115200
4
+
5
+ """
6
+ Automatic adapter function
7
+ This function is used to automatically choose an adapter based on the user's input
8
+ 192.168.1.1 -> IP
9
+ COM4 -> Serial
10
+ /dev/tty* -> Serial
11
+ etc...
12
+ If an adapter class is supplied, it is passed through
13
+
14
+ Additionnaly, it is possible to do COM4:115200 so as to make the life of the user easier
15
+ Same with /dev/ttyACM0:115200
16
+ """
15
17
 
16
18
 
17
19
  from .adapter import Adapter
@@ -27,6 +29,15 @@ from .visa import Visa
27
29
 
28
30
 
29
31
  def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
32
+ """
33
+ Create an adapter from a string or an adapter
34
+
35
+ - <int>.<int>.<int>.<int>[:<int>] -> IP
36
+ - x.y[:<int>] -> IP
37
+ - COM<int> -> SerialPort
38
+ - /dev/tty[ACM|USB]<int> -> SerialPort
39
+
40
+ """
30
41
  if isinstance(adapter_or_string, Adapter):
31
42
  # Simply return it
32
43
  return adapter_or_string
@@ -39,12 +50,12 @@ def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
39
50
  port=descriptor.port,
40
51
  transport=descriptor.transport.value,
41
52
  )
42
- elif isinstance(descriptor, SerialPortDescriptor):
53
+ if isinstance(descriptor, SerialPortDescriptor):
43
54
  return SerialPort(port=descriptor.port, baudrate=descriptor.baudrate)
44
- elif isinstance(descriptor, VisaDescriptor):
55
+ if isinstance(descriptor, VisaDescriptor):
45
56
  return Visa(descriptor=descriptor.descriptor)
46
- else:
47
- raise RuntimeError(f"Invalid descriptor : {descriptor}")
57
+
58
+ raise RuntimeError(f"Invalid descriptor : {descriptor}")
48
59
 
49
60
  else:
50
61
  raise ValueError(f"Invalid adapter : {adapter_or_string}")
@@ -61,7 +61,7 @@ class AdapterSignal:
61
61
  pass
62
62
 
63
63
 
64
- class AdapterDisconnected(AdapterSignal):
64
+ class AdapterDisconnectedSignal(AdapterSignal):
65
65
  def __str__(self) -> str:
66
66
  return "Adapter disconnected"
67
67
 
@@ -211,7 +211,7 @@ class AdapterBackend(ABC):
211
211
  return self._previous_buffer.data == b""
212
212
 
213
213
  @abstractmethod
214
- def open(self) -> bool:
214
+ def open(self) -> None:
215
215
  """
216
216
  Start communication with the device
217
217
  """
@@ -261,7 +261,7 @@ class AdapterBackend(ABC):
261
261
  fragment_delta_t = float("nan")
262
262
  if fragment.data == b"":
263
263
  self.close()
264
- yield AdapterDisconnected()
264
+ yield AdapterDisconnectedSignal()
265
265
  else:
266
266
  self._logger.debug(
267
267
  f"New fragment {fragment_delta_t:+.3f} {fragment}"
@@ -16,7 +16,7 @@ from typing import Any
16
16
  from syndesi.adapters.backend.stop_condition_backend import (
17
17
  stop_condition_to_backend,
18
18
  )
19
- from syndesi.tools.errors import make_error_description
19
+ from syndesi.tools.errors import AdapterError, make_error_description
20
20
  from syndesi.tools.types import NumberLike
21
21
 
22
22
  from ...tools.backend_api import Action, frontend_send
@@ -99,16 +99,12 @@ class AdapterSession(threading.Thread):
99
99
  self._shutdown_counter_top = None
100
100
  self._shutdown_counter = None
101
101
 
102
- # self._timeout_events: list[tuple[TimeoutEvent, float]] = []
103
-
104
102
  self._read_init_id = 0
105
103
 
106
104
  def add_connection(self, conn: NamedConnection) -> None:
107
105
  with self._connections_lock:
108
106
  self.connections.append(conn)
109
- # os.write(self._new_connection_w, b"\x00")
110
107
  self._new_connection_w.send(b"\x00")
111
- self._logger.info(f"New client : {conn.remote()}")
112
108
 
113
109
  def _remove_connection(self, conn: NamedConnection) -> None:
114
110
  with self._connections_lock:
@@ -272,12 +268,15 @@ class AdapterSession(threading.Thread):
272
268
  self._adapter.set_stop_conditions(
273
269
  [stop_condition_to_backend(sc) for sc in request[1]]
274
270
  )
275
- if self._adapter.open():
271
+ try:
272
+ self._adapter.open()
273
+ except AdapterError as e:
276
274
  # Success !
277
- response_action = Action.OPEN
278
- else:
279
275
  response_action = Action.ERROR_FAILED_TO_OPEN
280
- extra_arguments = ("",)
276
+ extra_arguments = (str(e),)
277
+ else:
278
+ response_action = Action.OPEN
279
+ extra_arguments = ("",)
281
280
  case Action.WRITE:
282
281
  data = request[1]
283
282
  if self._adapter.is_opened():
@@ -98,6 +98,8 @@ def is_request(x: object) -> TypeGuard[tuple[str, object]]:
98
98
 
99
99
  class Backend:
100
100
  MONITORING_DELAY = 0.5
101
+ NEW_CLIENT_REQUEST_TIMEOUT = 0.5
102
+
101
103
  _session_shutdown_delay: NumberLike | None
102
104
  _backend_shutdown_delay: NumberLike | None
103
105
  _backend_shutdown_timestamp: NumberLike | None
@@ -278,7 +280,9 @@ class Backend:
278
280
  # Wait for adapter
279
281
  # ready = wait([client.conn], timeout=0.1)
280
282
  # selectors to work on Unix and Windows
281
- ready, _, _ = select.select([client.conn], [], [], 0.1)
283
+ ready, _, _ = select.select(
284
+ [client.conn], [], [], self.NEW_CLIENT_REQUEST_TIMEOUT
285
+ )
282
286
  if len(ready) == 0:
283
287
  client.conn.close()
284
288
  return
@@ -290,6 +294,7 @@ class Backend:
290
294
  action = Action(adapter_request[0])
291
295
  if action == Action.SELECT_ADAPTER:
292
296
  adapter_descriptor = adapter_request[1]
297
+ self._logger.info(f"New client for {adapter_descriptor}")
293
298
  # If the session exists but it is dead, delete it
294
299
  if (
295
300
  adapter_descriptor in self.adapter_sessions
@@ -299,7 +304,7 @@ class Backend:
299
304
 
300
305
  if adapter_descriptor not in self.adapter_sessions:
301
306
  # Create the adapter backend thread
302
- self._logger.info(f"Creating adapter session for {adapter_descriptor}")
307
+ # self._logger.info(f"Creating adapter session for {adapter_descriptor}")
303
308
  thread = AdapterSession(
304
309
  adapter_descriptor, shutdown_delay=self._session_shutdown_delay
305
310
  ) # TODO : Put another delay here ?