syndesi 0.4.0__tar.gz → 0.4.2__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 (69) hide show
  1. syndesi-0.4.2/PKG-INFO +96 -0
  2. syndesi-0.4.2/README.md +74 -0
  3. {syndesi-0.4.0 → syndesi-0.4.2}/pyproject.toml +1 -3
  4. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/adapter.py +91 -158
  5. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/auto.py +1 -1
  6. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/adapter_backend.py +54 -37
  7. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/adapter_session.py +26 -27
  8. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/descriptors.py +3 -2
  9. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/ip_backend.py +1 -0
  10. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/serialport_backend.py +9 -10
  11. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/stop_condition_backend.py +47 -26
  12. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/visa_backend.py +7 -7
  13. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/ip.py +6 -10
  14. syndesi-0.4.2/syndesi/adapters/stop_condition.py +90 -0
  15. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/timeout.py +3 -30
  16. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/visa.py +2 -2
  17. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/backend_status.py +7 -9
  18. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/console.py +1 -54
  19. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/shell.py +1 -14
  20. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/shell_tools.py +0 -5
  21. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/delimited.py +17 -37
  22. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/modbus.py +17 -14
  23. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/raw.py +20 -16
  24. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/scpi.py +18 -15
  25. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/scripts/syndesi.py +1 -3
  26. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/backend_api.py +5 -38
  27. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/backend_logger.py +0 -1
  28. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/errors.py +4 -5
  29. syndesi-0.4.2/syndesi/tools/internal.py +0 -0
  30. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/log.py +0 -88
  31. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/types.py +0 -44
  32. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/version.py +1 -1
  33. syndesi-0.4.2/syndesi.egg-info/PKG-INFO +96 -0
  34. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/SOURCES.txt +1 -0
  35. syndesi-0.4.0/PKG-INFO +0 -123
  36. syndesi-0.4.0/README.md +0 -101
  37. syndesi-0.4.0/syndesi/adapters/stop_condition.py +0 -163
  38. syndesi-0.4.0/syndesi.egg-info/PKG-INFO +0 -123
  39. {syndesi-0.4.0 → syndesi-0.4.2}/LICENSE +0 -0
  40. {syndesi-0.4.0 → syndesi-0.4.2}/setup.cfg +0 -0
  41. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/__init__.py +0 -0
  42. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/__main__.py +0 -0
  43. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/__init__.py +0 -0
  44. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/__init__.py +0 -0
  45. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/adapter_manager.py +0 -0
  46. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/backend.py +0 -0
  47. syndesi-0.4.0/syndesi/cli/__init__.py → syndesi-0.4.2/syndesi/adapters/backend/backend_status.py +0 -0
  48. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/backend_tools.py +1 -1
  49. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/timed_queue.py +0 -0
  50. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/backend/timeout.py +0 -0
  51. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/ip_server.py +0 -0
  52. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/adapters/serialport.py +0 -0
  53. {syndesi-0.4.0/syndesi/protocols → syndesi-0.4.2/syndesi/cli}/__init__.py +0 -0
  54. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/backend_console.py +0 -0
  55. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/backend_wrapper.py +0 -0
  56. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/terminal.py +0 -0
  57. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/terminal_apps.py +0 -0
  58. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/cli/terminal_tools.py +0 -0
  59. {syndesi-0.4.0/syndesi/scripts → syndesi-0.4.2/syndesi/protocols}/__init__.py +0 -0
  60. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/protocols/protocol.py +0 -0
  61. {syndesi-0.4.0/syndesi/tools → syndesi-0.4.2/syndesi/scripts}/__init__.py +0 -0
  62. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/scripts/syndesi_backend.py +0 -0
  63. /syndesi-0.4.0/syndesi/tools/internal.py → /syndesi-0.4.2/syndesi/tools/__init__.py +0 -0
  64. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/exceptions.py +0 -0
  65. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi/tools/log_settings.py +0 -0
  66. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/dependency_links.txt +0 -0
  67. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/entry_points.txt +0 -0
  68. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/requires.txt +0 -0
  69. {syndesi-0.4.0 → syndesi-0.4.2}/syndesi.egg-info/top_level.txt +0 -0
syndesi-0.4.2/PKG-INFO ADDED
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: syndesi
3
+ Version: 0.4.2
4
+ Summary: Syndesi
5
+ Author-email: Sébastien Deriaz <sebastien.deriaz1@gmail.com>
6
+ License: GPL
7
+ Keywords: python,syndesi,interface,ethernet,serial,visa
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Education
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: Unix
12
+ Classifier: Operating System :: MacOS :: MacOS X
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: prompt_toolkit
18
+ Requires-Dist: pyserial
19
+ Requires-Dist: rich
20
+ Requires-Dist: platformdirs
21
+ Dynamic: license-file
22
+
23
+ # Syndesi Python Implementation
24
+
25
+ Syndesi description is available [here](https://github.com/syndesi-project/Syndesi/README.md)
26
+
27
+ Syndesi is a modular Python framework designed to streamline communication and control of a wide range of electronic instruments and devices. By providing a unified abstraction layer for adapters, protocols, and device drivers, Syndesi enables seamless integration with test equipment such as multimeters, oscilloscopes, power supplies, UART/USB devices, and more. Its flexible architecture supports both high-level and low-level operations, making it ideal for automation, data acquisition, and custom device interfacing in laboratory, industrial, and research environments.
28
+
29
+ ## Installation
30
+
31
+ The syndesi Python package can be installed through pip
32
+
33
+ ``pip install syndesi``
34
+
35
+ The package can also be installed locally by cloning this repository
36
+
37
+ ```bash
38
+ git clone https://github.com/syndesi-project/Syndesi
39
+ cd Syndesi
40
+ pip install .
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ The user can work with any of the three following layers :
46
+
47
+ - Adapters : low-level communication (IP, UART, ...)
48
+ - Protocols : Encapsulated protocols (Delimited, Modbus, ...)
49
+ - Drivers : Device or application specific commands
50
+
51
+ ### Adapters
52
+
53
+ The adapter allows the user to read and write raw data through IP, serial and VISA
54
+
55
+ ```python
56
+ from syndesi import IP
57
+
58
+ my_adapter = IP('192.168.1.12', port=5025)
59
+
60
+ my_adapter.write(b'ping\n')
61
+
62
+ my_adapter.read() # -> b'pong'
63
+ ```
64
+
65
+ ```python
66
+ from syndesi import SerialPort
67
+
68
+ arduino = SerialPort('/dev/ttyUSB0', baudrate=115200) # COMx on Windows
69
+ arduino.query(b'get_temperature\n') # -> 20.5
70
+ ```
71
+
72
+ ### Protocols
73
+
74
+ Protocols encapsulate and format data
75
+
76
+ ```python
77
+ from syndesi import IP, Delimited
78
+
79
+ my_server = Delimited(IP('test.server.local', port=1234))
80
+
81
+ my_server.query('Hello world\n') # -> Hello world (\n is removed by Delimited)
82
+
83
+ ```
84
+
85
+ ### Drivers
86
+
87
+ A driver only requires an adapter, the protocol (if used) is instanciated internally
88
+
89
+ ```python
90
+ from syndesi_drivers.instruments.mutlimeters.siglent.SDM3055 import SDM3055
91
+ from syndesi.adapters import IP
92
+
93
+ mm = SDM3055(IP("192.168.1.123"))
94
+
95
+ voltage = mm.measure_dc_voltage()
96
+ ```
@@ -0,0 +1,74 @@
1
+ # Syndesi Python Implementation
2
+
3
+ Syndesi description is available [here](https://github.com/syndesi-project/Syndesi/README.md)
4
+
5
+ Syndesi is a modular Python framework designed to streamline communication and control of a wide range of electronic instruments and devices. By providing a unified abstraction layer for adapters, protocols, and device drivers, Syndesi enables seamless integration with test equipment such as multimeters, oscilloscopes, power supplies, UART/USB devices, and more. Its flexible architecture supports both high-level and low-level operations, making it ideal for automation, data acquisition, and custom device interfacing in laboratory, industrial, and research environments.
6
+
7
+ ## Installation
8
+
9
+ The syndesi Python package can be installed through pip
10
+
11
+ ``pip install syndesi``
12
+
13
+ The package can also be installed locally by cloning this repository
14
+
15
+ ```bash
16
+ git clone https://github.com/syndesi-project/Syndesi
17
+ cd Syndesi
18
+ pip install .
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ The user can work with any of the three following layers :
24
+
25
+ - Adapters : low-level communication (IP, UART, ...)
26
+ - Protocols : Encapsulated protocols (Delimited, Modbus, ...)
27
+ - Drivers : Device or application specific commands
28
+
29
+ ### Adapters
30
+
31
+ The adapter allows the user to read and write raw data through IP, serial and VISA
32
+
33
+ ```python
34
+ from syndesi import IP
35
+
36
+ my_adapter = IP('192.168.1.12', port=5025)
37
+
38
+ my_adapter.write(b'ping\n')
39
+
40
+ my_adapter.read() # -> b'pong'
41
+ ```
42
+
43
+ ```python
44
+ from syndesi import SerialPort
45
+
46
+ arduino = SerialPort('/dev/ttyUSB0', baudrate=115200) # COMx on Windows
47
+ arduino.query(b'get_temperature\n') # -> 20.5
48
+ ```
49
+
50
+ ### Protocols
51
+
52
+ Protocols encapsulate and format data
53
+
54
+ ```python
55
+ from syndesi import IP, Delimited
56
+
57
+ my_server = Delimited(IP('test.server.local', port=1234))
58
+
59
+ my_server.query('Hello world\n') # -> Hello world (\n is removed by Delimited)
60
+
61
+ ```
62
+
63
+ ### Drivers
64
+
65
+ A driver only requires an adapter, the protocol (if used) is instanciated internally
66
+
67
+ ```python
68
+ from syndesi_drivers.instruments.mutlimeters.siglent.SDM3055 import SDM3055
69
+ from syndesi.adapters import IP
70
+
71
+ mm = SDM3055(IP("192.168.1.123"))
72
+
73
+ voltage = mm.measure_dc_voltage()
74
+ ```
@@ -11,7 +11,7 @@ dynamic = ["version"]
11
11
  description = "Syndesi"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
14
- license = { text = "GPL" } # update if different
14
+ license = { text = "GPL" }
15
15
  authors = [{ name = "Sébastien Deriaz", email = "sebastien.deriaz1@gmail.com" }]
16
16
  keywords = ["python", "syndesi", "interface", "ethernet", "serial", "visa"]
17
17
  classifiers = [
@@ -42,8 +42,6 @@ include = ["syndesi*"]
42
42
  [tool.ruff]
43
43
  target-version = "py311"
44
44
  line-length = 100
45
- select = ["D","E","F","I","B"] # include pydocstyle rules ("D")
46
- #ignore = ["D203","D213"] # pick your preferences
47
45
 
48
46
  [tool.ruff.lint]
49
47
  select = ["E", "F", "W", "I", "UP", "B"]
@@ -15,8 +15,8 @@
15
15
  # An adapter is meant to work with bytes objects but it can accept strings.
16
16
  # Strings will automatically be converted to bytes using utf-8 encoding
17
17
 
18
- from enum import Enum
19
18
  import logging
19
+ import os
20
20
  import queue
21
21
  import subprocess
22
22
  import sys
@@ -25,42 +25,44 @@ import time
25
25
  import weakref
26
26
  from abc import ABC, abstractmethod
27
27
  from collections.abc import Callable
28
+ from enum import Enum
28
29
  from multiprocessing.connection import Client, Connection
29
30
  from types import EllipsisType
30
31
  from typing import Any
31
- import os
32
32
 
33
- from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
34
33
  from syndesi.tools.types import NumberLike, is_number
35
34
 
36
35
  from ..tools.backend_api import (
37
36
  BACKEND_PORT,
37
+ EXTRA_BUFFER_RESPONSE_TIME,
38
38
  Action,
39
39
  BackendResponse,
40
+ Fragment,
40
41
  default_host,
41
42
  raise_if_error,
42
- EXTRA_BUFFER_RESPONSE_TIME
43
43
  )
44
44
  from ..tools.log_settings import LoggerAlias
45
45
  from .backend.adapter_backend import (
46
46
  AdapterDisconnected,
47
- AdapterResponseTimeout,
48
47
  AdapterReadPayload,
48
+ AdapterResponseTimeout,
49
49
  AdapterSignal,
50
50
  )
51
+ from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
51
52
  from .backend.descriptors import Descriptor
52
- from .stop_condition import StopCondition, Continuation, Total
53
+ from .stop_condition import Continuation, StopCondition, StopConditionType
53
54
  from .timeout import Timeout, TimeoutAction, any_to_timeout
54
55
 
55
56
  DEFAULT_STOP_CONDITION = Continuation(time=0.1)
56
57
 
57
- DEFAULT_TIMEOUT = Timeout(response=5, action='error')
58
+ DEFAULT_TIMEOUT = Timeout(response=5, action="error")
58
59
 
59
60
  # Maximum time to let the backend start
60
61
  START_TIMEOUT = 2
61
62
  # Time to shutdown the backend
62
63
  SHUTDOWN_DELAY = 2
63
64
 
65
+
64
66
  class SignalQueue(queue.Queue[AdapterSignal]):
65
67
  def __init__(self) -> None:
66
68
  self._read_payload_counter = 0
@@ -69,12 +71,12 @@ class SignalQueue(queue.Queue[AdapterSignal]):
69
71
  def has_read_payload(self) -> bool:
70
72
  return self._read_payload_counter > 0
71
73
 
72
-
73
- def put(self, signal: AdapterSignal, block: bool = True, timeout: float | None = None) -> None:
74
+ def put(
75
+ self, signal: AdapterSignal, block: bool = True, timeout: float | None = None
76
+ ) -> None:
74
77
  if isinstance(signal, AdapterReadPayload):
75
78
  self._read_payload_counter += 1
76
79
  return super().put(signal, block, timeout)
77
-
78
80
 
79
81
  def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
80
82
  signal = super().get(block, timeout)
@@ -93,19 +95,20 @@ def is_backend_running(address: str, port: int) -> bool:
93
95
  conn.close()
94
96
  return True
95
97
 
98
+
96
99
  def start_backend(port: int | None = None) -> None:
97
100
  arguments = [
98
- sys.executable,
99
- "-m",
100
- "syndesi.adapters.backend.backend",
101
- "-s",
102
- str(SHUTDOWN_DELAY),
103
- "-q",
104
- "-p",
105
- str(BACKEND_PORT if port is None else port),
106
- ]
107
-
108
- stdin = subprocess.DEVNULL
101
+ sys.executable,
102
+ "-m",
103
+ "syndesi.adapters.backend.backend",
104
+ "-s",
105
+ str(SHUTDOWN_DELAY),
106
+ "-q",
107
+ "-p",
108
+ str(BACKEND_PORT if port is None else port),
109
+ ]
110
+
111
+ stdin = subprocess.DEVNULL
109
112
  stdout = subprocess.DEVNULL
110
113
  stderr = subprocess.DEVNULL
111
114
 
@@ -121,8 +124,8 @@ def start_backend(port: int | None = None) -> None:
121
124
 
122
125
  else:
123
126
  # Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
124
- CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
125
- DETACHED_PROCESS = 0x00000008 # not exposed by subprocess on all Pythons
127
+ CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
128
+ DETACHED_PROCESS = 0x00000008 # not exposed by subprocess on all Pythons
126
129
  # Optional: CREATE_NO_WINDOW (no window even for console apps)
127
130
  CREATE_NO_WINDOW = 0x08000000
128
131
 
@@ -137,9 +140,11 @@ def start_backend(port: int | None = None) -> None:
137
140
  close_fds=True,
138
141
  )
139
142
 
143
+
140
144
  class ReadScope(Enum):
141
- NEXT = 'next'
142
- BUFFERED = 'buffered'
145
+ NEXT = "next"
146
+ BUFFERED = "buffered"
147
+
143
148
 
144
149
  class Adapter(ABC):
145
150
  def __init__(
@@ -214,7 +219,7 @@ class Adapter(ABC):
214
219
  elif isinstance(stop_conditions, list):
215
220
  self._stop_conditions = stop_conditions
216
221
  else:
217
- raise ValueError('Invalid stop_conditions')
222
+ raise ValueError("Invalid stop_conditions")
218
223
 
219
224
  # Set the timeout
220
225
  self.is_default_timeout = False
@@ -334,7 +339,7 @@ class Adapter(ABC):
334
339
  action = Action(response[0])
335
340
 
336
341
  if action == Action.ADAPTER_SIGNAL:
337
- #if is_event(action):
342
+ # if is_event(action):
338
343
  if len(response) <= 1:
339
344
  raise RuntimeError(f"Invalid event response : {response}")
340
345
  signal: AdapterSignal = response[1]
@@ -370,7 +375,9 @@ class Adapter(ABC):
370
375
  self._logger.debug(f"Setting default timeout to {default_timeout}")
371
376
  self._timeout = default_timeout
372
377
 
373
- def set_stop_conditions(self, stop_conditions: StopCondition | None | list[StopCondition]) -> None:
378
+ def set_stop_conditions(
379
+ self, stop_conditions: StopCondition | None | list[StopCondition]
380
+ ) -> None:
374
381
  """
375
382
  Overwrite the stop-condition
376
383
 
@@ -385,7 +392,7 @@ class Adapter(ABC):
385
392
  elif stop_conditions is None:
386
393
  self._stop_conditions = []
387
394
 
388
- self._make_backend_request(Action.SET_STOP_CONDITION, self._stop_conditions)
395
+ self._make_backend_request(Action.SET_STOP_CONDITIONs, self._stop_conditions)
389
396
 
390
397
  def set_default_stop_condition(self, stop_condition: StopCondition) -> None:
391
398
  """
@@ -410,7 +417,6 @@ class Adapter(ABC):
410
417
  self._signal_queue.get(block=False)
411
418
  except queue.Empty:
412
419
  break
413
-
414
420
 
415
421
  def previous_read_buffer_empty(self) -> bool:
416
422
  """
@@ -462,9 +468,9 @@ class Adapter(ABC):
462
468
  def read_detailed(
463
469
  self,
464
470
  timeout: Timeout | EllipsisType | None = ...,
465
- stop_condition: StopCondition | EllipsisType | None = ...,
466
- scope : str = ReadScope.BUFFERED.value,
467
- ) -> AdapterReadPayload | None:
471
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
472
+ scope: str = ReadScope.BUFFERED.value,
473
+ ) -> AdapterReadPayload:
468
474
  """
469
475
  Read data from the device
470
476
 
@@ -481,9 +487,22 @@ class Adapter(ABC):
481
487
  data : bytes
482
488
  signal : AdapterReadPayload
483
489
  """
484
- t = time.time()
485
490
  _scope = ReadScope(scope)
486
491
  output_signal = None
492
+ read_timeout = None
493
+
494
+ if timeout is ...:
495
+ read_timeout = self._timeout
496
+ else:
497
+ read_timeout = any_to_timeout(timeout)
498
+
499
+ if read_timeout is None:
500
+ raise RuntimeError("Cannot read without setting a timeout")
501
+
502
+ if stop_conditions is not ...:
503
+ if isinstance(stop_conditions, StopCondition):
504
+ stop_conditions = [stop_conditions]
505
+ self._make_backend_request(Action.SET_STOP_CONDITIONs, stop_conditions)
487
506
 
488
507
  # First, we check if data is in the buffer and if the scope if set to BUFFERED
489
508
  while _scope == ReadScope.BUFFERED and self._signal_queue.has_read_payload():
@@ -495,42 +514,37 @@ class Adapter(ABC):
495
514
  else:
496
515
  # Nothing was found, ask the backend with a START_READ request. The backend will
497
516
  # 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)
502
517
 
503
- if read_timeout is not None:
504
- if not read_timeout.is_initialized():
505
- raise RuntimeError("Timeout needs to be initialized")
518
+ if not read_timeout.is_initialized():
519
+ raise RuntimeError("Timeout needs to be initialized")
506
520
 
507
- _response = read_timeout.response()
521
+ _response = read_timeout.response()
508
522
 
509
- read_init_time = time.time()
510
- start_read_id = self._make_backend_request(Action.START_READ, _response)[0]
511
-
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
523
+ read_init_time = time.time()
524
+ start_read_id = self._make_backend_request(Action.START_READ, _response)[0]
518
525
 
526
+ if _response is None:
527
+ # Wait indefinitely
528
+ read_stop_timestamp = None
519
529
  else:
520
- start_read_id = None
521
- read_init_time = None
530
+ # Wait for the response time + a bit more
531
+ read_stop_timestamp = read_init_time + _response
522
532
 
523
-
524
533
  while True:
525
534
  try:
526
535
  if read_stop_timestamp is None:
527
536
  queue_timeout = None
528
537
  else:
529
- queue_timeout = max(0, read_stop_timestamp - time.time() + EXTRA_BUFFER_RESPONSE_TIME)
538
+ queue_timeout = max(
539
+ 0,
540
+ read_stop_timestamp
541
+ - time.time()
542
+ + EXTRA_BUFFER_RESPONSE_TIME,
543
+ )
530
544
 
531
545
  signal = self._signal_queue.get(timeout=queue_timeout)
532
- except queue.Empty:
533
- raise RuntimeError('Failed to receive response from backend')
546
+ except queue.Empty as e:
547
+ raise RuntimeError("Failed to receive response from backend") from e
534
548
  if isinstance(signal, AdapterReadPayload):
535
549
  output_signal = signal
536
550
  break
@@ -542,113 +556,35 @@ class Adapter(ABC):
542
556
  break
543
557
  # Otherwise ignore it
544
558
 
545
-
546
559
  if output_signal is None:
547
- # TODO : Make read_timeout always Timeout, never None ?
548
560
  match read_timeout.action:
549
- case TimeoutAction.RETURN:
550
- return None
561
+ case TimeoutAction.RETURN_EMPTY:
562
+ t = time.time()
563
+ return AdapterReadPayload(
564
+ fragments=[Fragment(b"", t)],
565
+ stop_timestamp=t,
566
+ stop_condition_type=StopConditionType.TIMEOUT,
567
+ previous_read_buffer_used=False,
568
+ response_timestamp=None,
569
+ response_delay=None,
570
+ )
551
571
  case TimeoutAction.ERROR:
552
572
  raise TimeoutError(
553
573
  f"No response received from device within {read_timeout.response()} seconds"
554
574
  )
555
575
  case _:
556
576
  raise NotImplementedError()
557
-
577
+
558
578
  else:
559
579
  return output_signal
560
580
 
561
-
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
567
-
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])
571
-
572
- # If not timeout is specified, use the default one
573
-
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
641
-
642
581
  def read(
643
582
  self,
644
583
  timeout: Timeout | EllipsisType | None = ...,
645
- stop_condition: StopCondition | EllipsisType | None = ...,
584
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
646
585
  ) -> bytes:
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()
586
+ signal = self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
587
+ return signal.data()
652
588
 
653
589
  def _cleanup(self) -> None:
654
590
  if self._init_ok and self.opened:
@@ -658,8 +594,8 @@ class Adapter(ABC):
658
594
  self,
659
595
  data: bytes | str,
660
596
  timeout: Timeout | EllipsisType | None = ...,
661
- stop_condition: StopCondition | EllipsisType | None = ...,
662
- ) -> tuple[bytes, AdapterReadPayload | None]:
597
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
598
+ ) -> AdapterReadPayload:
663
599
  """
664
600
  Shortcut function that combines
665
601
  - flush_read
@@ -668,21 +604,18 @@ class Adapter(ABC):
668
604
  """
669
605
  self.flushRead()
670
606
  self.write(data)
671
- return self.read_detailed(timeout=timeout, stop_condition=stop_condition)
607
+ return self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
672
608
 
673
609
  def query(
674
610
  self,
675
611
  data: bytes | str,
676
612
  timeout: Timeout | EllipsisType | None = ...,
677
- stop_condition: StopCondition | EllipsisType | None = ...,
613
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
678
614
  ) -> bytes:
679
615
  signal = self.query_detailed(
680
- data=data, timeout=timeout, stop_condition=stop_condition
616
+ data=data, timeout=timeout, stop_conditions=stop_conditions
681
617
  )
682
- if signal is None:
683
- return None
684
- else:
685
- return signal.data()
618
+ return signal.data()
686
619
 
687
620
  def set_event_callback(self, callback: Callable[[AdapterSignal], None]) -> None:
688
621
  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)