syndesi 0.4.2__py3-none-any.whl → 0.5.0__py3-none-any.whl

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 (57) hide show
  1. syndesi/__init__.py +22 -2
  2. syndesi/adapters/adapter.py +332 -489
  3. syndesi/adapters/adapter_worker.py +820 -0
  4. syndesi/adapters/auto.py +58 -25
  5. syndesi/adapters/descriptors.py +38 -0
  6. syndesi/adapters/ip.py +203 -71
  7. syndesi/adapters/serialport.py +154 -25
  8. syndesi/adapters/stop_conditions.py +354 -0
  9. syndesi/adapters/timeout.py +58 -21
  10. syndesi/adapters/visa.py +236 -11
  11. syndesi/cli/console.py +51 -16
  12. syndesi/cli/shell.py +95 -47
  13. syndesi/cli/terminal_tools.py +8 -8
  14. syndesi/component.py +315 -0
  15. syndesi/protocols/delimited.py +92 -107
  16. syndesi/protocols/modbus.py +2368 -868
  17. syndesi/protocols/protocol.py +186 -33
  18. syndesi/protocols/raw.py +45 -62
  19. syndesi/protocols/scpi.py +65 -102
  20. syndesi/remote/remote.py +188 -0
  21. syndesi/scripts/syndesi.py +12 -2
  22. syndesi/tools/errors.py +49 -31
  23. syndesi/tools/log_settings.py +21 -8
  24. syndesi/tools/{log.py → logmanager.py} +24 -13
  25. syndesi/tools/types.py +9 -7
  26. syndesi/version.py +5 -1
  27. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/METADATA +1 -1
  28. syndesi-0.5.0.dist-info/RECORD +41 -0
  29. syndesi/adapters/backend/__init__.py +0 -0
  30. syndesi/adapters/backend/adapter_backend.py +0 -438
  31. syndesi/adapters/backend/adapter_manager.py +0 -48
  32. syndesi/adapters/backend/adapter_session.py +0 -346
  33. syndesi/adapters/backend/backend.py +0 -438
  34. syndesi/adapters/backend/backend_status.py +0 -0
  35. syndesi/adapters/backend/backend_tools.py +0 -66
  36. syndesi/adapters/backend/descriptors.py +0 -153
  37. syndesi/adapters/backend/ip_backend.py +0 -149
  38. syndesi/adapters/backend/serialport_backend.py +0 -241
  39. syndesi/adapters/backend/stop_condition_backend.py +0 -219
  40. syndesi/adapters/backend/timed_queue.py +0 -39
  41. syndesi/adapters/backend/timeout.py +0 -252
  42. syndesi/adapters/backend/visa_backend.py +0 -197
  43. syndesi/adapters/ip_server.py +0 -102
  44. syndesi/adapters/stop_condition.py +0 -90
  45. syndesi/cli/backend_console.py +0 -96
  46. syndesi/cli/backend_status.py +0 -274
  47. syndesi/cli/backend_wrapper.py +0 -61
  48. syndesi/scripts/syndesi_backend.py +0 -37
  49. syndesi/tools/backend_api.py +0 -175
  50. syndesi/tools/backend_logger.py +0 -64
  51. syndesi/tools/exceptions.py +0 -16
  52. syndesi/tools/internal.py +0 -0
  53. syndesi-0.4.2.dist-info/RECORD +0 -60
  54. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/WHEEL +0 -0
  55. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/entry_points.txt +0 -0
  56. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/licenses/LICENSE +0 -0
  57. {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/top_level.txt +0 -0
syndesi/adapters/auto.py CHANGED
@@ -1,37 +1,71 @@
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
15
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
+ """
17
+
18
+
19
+ import re
20
+
21
+ from syndesi.component import Descriptor
16
22
 
17
23
  from .adapter import Adapter
18
- from .backend.descriptors import (
19
- IPDescriptor,
24
+ from .ip import IP, IPDescriptor
25
+ from .serialport import SerialPort, SerialPortDescriptor
26
+ from .visa import Visa, VisaDescriptor
27
+
28
+ descriptors: list[type[Descriptor]] = [
20
29
  SerialPortDescriptor,
30
+ IPDescriptor,
21
31
  VisaDescriptor,
22
- adapter_descriptor_by_string,
23
- )
24
- from .ip import IP
25
- from .serialport import SerialPort
26
- from .visa import Visa
32
+ ]
33
+
34
+
35
+ def adapter_descriptor_by_string(string_descriptor: str) -> Descriptor:
36
+ """
37
+ Return a corresponding adapter descriptor from a string
38
+
39
+ Parameters
40
+ ----------
41
+ string_descriptor : str
42
+
43
+ Returns
44
+ -------
45
+ descriptor : Descriptor
46
+ """
47
+ for descriptor in descriptors:
48
+ if re.match(descriptor.DETECTION_PATTERN, string_descriptor):
49
+ x = descriptor.from_string(string_descriptor)
50
+ return x
51
+ raise ValueError(f"Could not parse descriptor string : {string_descriptor}")
27
52
 
28
53
 
29
54
  def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
55
+ """
56
+ Create an adapter from a string or an adapter
57
+
58
+ - <int>.<int>.<int>.<int>[:<int>] -> IP
59
+ - x.y[:<int>] -> IP
60
+ - COM<int> -> SerialPort
61
+ - /dev/tty[ACM|USB]<int> -> SerialPort
62
+
63
+ """
30
64
  if isinstance(adapter_or_string, Adapter):
31
65
  # Simply return it
32
66
  return adapter_or_string
33
67
 
34
- elif isinstance(adapter_or_string, str):
68
+ if isinstance(adapter_or_string, str):
35
69
  descriptor = adapter_descriptor_by_string(adapter_or_string)
36
70
  if isinstance(descriptor, IPDescriptor):
37
71
  return IP(
@@ -39,12 +73,11 @@ def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
39
73
  port=descriptor.port,
40
74
  transport=descriptor.transport.value,
41
75
  )
42
- elif isinstance(descriptor, SerialPortDescriptor):
76
+ if isinstance(descriptor, SerialPortDescriptor):
43
77
  return SerialPort(port=descriptor.port, baudrate=descriptor.baudrate)
44
- elif isinstance(descriptor, VisaDescriptor):
78
+ if isinstance(descriptor, VisaDescriptor):
45
79
  return Visa(descriptor=descriptor.descriptor)
46
- else:
47
- raise RuntimeError(f"Invalid descriptor : {descriptor}")
48
80
 
49
- else:
50
- raise ValueError(f"Invalid adapter : {adapter_or_string}")
81
+ raise RuntimeError(f"Invalid descriptor : {descriptor}")
82
+
83
+ raise ValueError(f"Invalid adapter : {adapter_or_string}")
@@ -0,0 +1,38 @@
1
+ # # File : descriptors.py
2
+ # # Author : Sébastien Deriaz
3
+ # # License : GPL
4
+ # """
5
+ # Descriptors are classes that describe how an adapter is connected to its device.
6
+ # Depending on the protocol, they can hold strings, integers or enums
7
+ # """
8
+
9
+ # import re
10
+ # from abc import abstractmethod
11
+ # from dataclasses import dataclass
12
+ # from enum import Enum
13
+
14
+
15
+ # descriptors: list[type[Descriptor]] = [
16
+ # SerialPortDescriptor,
17
+ # IPDescriptor,
18
+ # VisaDescriptor,
19
+ # ]
20
+
21
+
22
+ # def adapter_descriptor_by_string(string_descriptor: str) -> Descriptor:
23
+ # """
24
+ # Return a corresponding adapter descriptor from a string
25
+
26
+ # Parameters
27
+ # ----------
28
+ # string_descriptor : str
29
+
30
+ # Returns
31
+ # -------
32
+ # descriptor : Descriptor
33
+ # """
34
+ # for descriptor in descriptors:
35
+ # if re.match(descriptor.DETECTION_PATTERN, string_descriptor):
36
+ # x = descriptor.from_string(string_descriptor)
37
+ # return x
38
+ # raise ValueError(f"Could not parse descriptor string : {string_descriptor}")
syndesi/adapters/ip.py CHANGED
@@ -1,110 +1,242 @@
1
1
  # File : ip.py
2
2
  # Author : Sébastien Deriaz
3
3
  # License : GPL
4
- #
5
- # IP adapter, communicates with TCP or UDP
6
-
4
+ """
5
+ IP Adapter, used to communicate with IP targets using the socket module
6
+ """
7
7
 
8
+ import socket
8
9
  from collections.abc import Callable
10
+ from dataclasses import dataclass
11
+ from enum import StrEnum
9
12
  from types import EllipsisType
13
+ from typing import cast
14
+
15
+ import _socket
10
16
 
11
- from syndesi.adapters.backend.adapter_backend import AdapterSignal
12
- from syndesi.tools.types import NumberLike
17
+ from syndesi.adapters.adapter_worker import AdapterEvent, HasFileno
18
+ from syndesi.adapters.stop_conditions import Continuation, StopCondition
19
+ from syndesi.adapters.timeout import Timeout
20
+ from syndesi.component import Descriptor
21
+ from syndesi.tools.errors import AdapterOpenError, AdapterWriteError
13
22
 
14
23
  from .adapter import Adapter
15
- from .backend.descriptors import IPDescriptor
16
- from .stop_condition import StopCondition
17
- from .timeout import Timeout
24
+ from .stop_conditions import Fragment
25
+
26
+
27
+ @dataclass
28
+ class IPDescriptor(Descriptor):
29
+ """
30
+ IP descriptor that holds ip address and port
31
+ """
18
32
 
19
- # TODO : Server ? create an adapter from a socket ?
33
+ class Transport(StrEnum):
34
+ """
35
+ IP Transport protocol
36
+ """
37
+
38
+ TCP = "TCP"
39
+ UDP = "UDP"
40
+
41
+ @classmethod
42
+ def from_str(cls, transport: str) -> "IPDescriptor":
43
+ """
44
+ Create a Transport class from a string
45
+
46
+ Parameters
47
+ ----------
48
+ transport : str
49
+ """
50
+ for member in cls:
51
+ if member.value.lower() == transport.lower():
52
+ return member # type: ignore # TODO : Check this
53
+ raise ValueError(f"{transport} is not a valid {cls.__name__}")
54
+
55
+ DETECTION_PATTERN = r"(\d+.\d+.\d+.\d+|[\w\.]+):\d+:(UDP|TCP)"
56
+ address: str
57
+ transport: Transport
58
+ port: int | None = None
59
+
60
+ @staticmethod
61
+ def from_string(string: str) -> "IPDescriptor":
62
+ parts = string.split(":")
63
+ address = parts[0]
64
+ port = int(parts[1])
65
+ transport = IPDescriptor.Transport(parts[2])
66
+ return IPDescriptor(address, transport, port)
67
+
68
+ def __str__(self) -> str:
69
+ return f"{self.address}:{self.port}:{self.Transport(self.transport).value}"
70
+
71
+ def is_initialized(self) -> bool:
72
+ """
73
+ Return True if all attributes has been defined (not None)
74
+ """
20
75
 
21
- # TODO : Manage opening and closing, modes ? open at instance or at write/read ? close after read ? error if already opened before / strict mode ?
76
+ return self.port is not None and self.transport is not None
22
77
 
23
78
 
24
79
  class IP(Adapter):
25
- DEFAULT_PROTOCOL = IPDescriptor.Transport.TCP
80
+ """
81
+ IP stack adapter. The IP Adapter reads and writes bytes units (frames)
82
+
83
+ Parameters
84
+ ----------
85
+ address : str
86
+ IP address
87
+ port : int or None, default : None
88
+ IP port
89
+ transport : {'TCP', 'UDP'}
90
+ Transport layer
91
+ timeout : Timeout or float
92
+ Specify communication timeout, the time it takes for the target to respond
93
+ stop_conditions : list[StopCondition] or StopCondition
94
+ Stop coniditions are used to decide when a read data block is finished
95
+ and should be returned
96
+
97
+ These include
98
+
99
+ * Termination : stop on a specific sequence like ``\\n`` at the end of the data
100
+ * Length : stop when a specific number of bytes has been received
101
+ * Continuation : stop when no data has been received for a
102
+ specified amount of time
103
+ * Total : stop if the time since the first piece of data received exceeds
104
+ a given amount of time
105
+ * FragmentStopCondition : Return each piece of data individually as received
106
+ by the low-level communication layer
107
+
108
+ Multiple stop conditions can be used to create more complex behaviours
109
+ encoding : str
110
+ Used to convert str to bytes if the user chooses to send
111
+ alias : str
112
+ Name of the adapter, may be removed in the future
113
+ event_callback : f(event : AdapterEvent)
114
+ Function called when an event is received by the adapter worker thread.
115
+ The event can be either one of :
116
+
117
+ * ``AdapterDisconnectedEvent``
118
+ * ``AdapterFrameEvent``
119
+ * ``FirstFragmentEvent``
120
+ auto_open : bool, default to True
121
+ Automatically open the adapter after instanciation
122
+ """
123
+
124
+ BUFFER_SIZE = 65507
26
125
 
27
126
  def __init__(
28
127
  self,
29
128
  address: str,
30
129
  port: int | None = None,
31
- transport: str = DEFAULT_PROTOCOL.value,
32
- timeout: Timeout | NumberLike | None | EllipsisType = ...,
33
- stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
34
- alias: str = "",
130
+ transport: str = IPDescriptor.Transport.TCP.value,
131
+ *,
132
+ timeout: Timeout | float | EllipsisType = ...,
133
+ stop_conditions: list[StopCondition] | StopCondition | EllipsisType = ...,
35
134
  encoding: str = "utf-8",
36
- event_callback: Callable[[AdapterSignal], None] | None = None,
135
+ alias: str = "",
136
+ event_callback: Callable[[AdapterEvent], None] | None = None,
37
137
  auto_open: bool = True,
38
- backend_address: str | None = None,
39
- backend_port: int | None = None,
40
138
  ):
41
- """
42
- IP adapter
43
139
 
44
- Parameters
45
- ----------
46
- address : str
47
- IP description
48
- port : int
49
- IP port
50
- transport : str
51
- 'TCP' or 'UDP'
52
- timeout : Timeout | float
53
- Specify communication timeout
54
- stop_condition : StopCondition
55
- Specify a read stop condition (None by default)
56
- auto_open : bool
57
- Automatically open the adapter
58
- socket : socket.socket
59
- Specify a custom socket, this is reserved for server application
140
+ descriptor = IPDescriptor(
141
+ address=address,
142
+ port=port,
143
+ transport=IPDescriptor.Transport(transport.upper()),
144
+ )
145
+ self._socket: _socket.socket | None = None
60
146
 
61
- """
62
147
  super().__init__(
63
- descriptor=IPDescriptor(
64
- address=address,
65
- port=port,
66
- transport=IPDescriptor.Transport(transport.upper()),
67
- ),
68
- alias=alias,
69
- timeout=timeout,
148
+ descriptor=descriptor,
70
149
  stop_conditions=stop_conditions,
150
+ timeout=timeout,
71
151
  encoding=encoding,
152
+ alias=alias,
72
153
  event_callback=event_callback,
73
154
  auto_open=auto_open,
74
- backend_address=backend_address,
75
- backend_port=backend_port,
76
155
  )
77
- self.descriptor: IPDescriptor
78
-
79
- # if self.descriptor.transport is not None:
80
- self._logger.info(f"Setting up {self.descriptor.transport.value} IP adapter")
81
-
82
- self.set_default_timeout(self._default_timeout())
83
-
84
- def _default_timeout(self) -> Timeout:
85
- return Timeout(response=5, action="error")
156
+ self._descriptor: IPDescriptor
157
+ self._worker_descriptor: IPDescriptor
86
158
 
87
159
  def set_default_port(self, port: int) -> None:
88
160
  """
89
- Sets IP port if no port has been set yet.
90
-
91
- This way, the user can leave the port empty
92
- and the driver/protocol can specify it later
161
+ Set the default port number
93
162
 
94
163
  Parameters
95
164
  ----------
96
165
  port : int
97
166
  """
98
- if self.descriptor.port is None:
99
- self.descriptor.port = port
100
-
101
- def set_default_transport(self, transport: str | IPDescriptor.Transport) -> None:
102
- """
103
- Sets the default IP transport protocol
167
+ if self._descriptor.port is None:
168
+ self._descriptor.port = port
169
+ self._update_descriptor()
170
+
171
+ def _worker_read(self, fragment_timestamp: float) -> Fragment:
172
+ if self._socket is None:
173
+ return Fragment(b"", fragment_timestamp)
174
+
175
+ try:
176
+ data = self._socket.recv(self.BUFFER_SIZE)
177
+ except (ConnectionRefusedError, OSError):
178
+ fragment = Fragment(b"", fragment_timestamp)
179
+ else:
180
+ if data == b"":
181
+ self._logger.warning("Socket disconnected")
182
+ self._worker_close()
183
+ fragment = Fragment(data, fragment_timestamp)
184
+ return fragment
185
+
186
+ def _worker_write(self, data: bytes) -> None:
187
+ super()._worker_write(data)
188
+
189
+ if self._socket is not None:
190
+ if self._socket.send(data) != len(data):
191
+ raise AdapterWriteError(
192
+ f"Adapter {self._worker_descriptor} couldn't write"
193
+ " all of the data to the socket"
194
+ )
195
+
196
+ def _worker_open(self) -> None:
197
+ self._worker_check_descriptor()
198
+
199
+ # Create the socket instance
200
+ if self._worker_descriptor.transport == IPDescriptor.Transport.TCP:
201
+ self._socket = cast(
202
+ _socket.socket,
203
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
204
+ )
205
+ elif self._worker_descriptor.transport == IPDescriptor.Transport.UDP:
206
+ self._socket = cast(
207
+ _socket.socket, socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
208
+ )
209
+ else:
210
+ raise AdapterOpenError("Invalid transport protocol")
211
+
212
+ try:
213
+ self._socket.settimeout(self.WorkerTimeout.OPEN.value)
214
+ self._socket.connect(
215
+ (self._worker_descriptor.address, self._worker_descriptor.port)
216
+ )
217
+ except (OSError, ConnectionRefusedError, socket.gaierror) as e:
218
+ self._opened = False
219
+ msg = f"Failed to open adapter {self._worker_descriptor} : {e}"
220
+ self._logger.error(msg)
221
+ raise AdapterOpenError(msg) from None
222
+
223
+ self._opened = True
224
+ self._logger.info(f"IP Adapter {self._worker_descriptor} opened")
225
+
226
+ def _worker_close(self) -> None:
227
+ if self._socket is not None:
228
+ try:
229
+ self._socket.shutdown(_socket.SHUT_RDWR)
230
+ self._socket.close()
231
+ except OSError:
232
+ pass
233
+ self._socket = None
234
+
235
+ def _selectable(self) -> HasFileno | None:
236
+ return self._socket
237
+
238
+ def _default_stop_conditions(self) -> list[StopCondition]:
239
+ return [Continuation(continuation=0.2)]
104
240
 
105
- Parameters
106
- ----------
107
- transport : str | IPDescriptor.Transport
108
- """
109
- if self.descriptor.transport is None:
110
- self.descriptor.transport = IPDescriptor.Transport(transport)
241
+ def _default_timeout(self) -> Timeout:
242
+ return Timeout(response=1, action="error")
@@ -2,38 +2,93 @@
2
2
  # Author : Sébastien Deriaz
3
3
  # License : GPL
4
4
 
5
+ """
6
+ SerialPort module, allows communication with serial devices using
7
+ the OS layers (COMx, /dev/ttyUSBx or /dev/ttyACMx)
8
+
9
+ """
10
+
5
11
  from collections.abc import Callable
12
+ from dataclasses import dataclass
6
13
  from types import EllipsisType
7
14
 
8
- from syndesi.adapters.backend.adapter_backend import AdapterSignal
15
+ import serial
16
+ from serial.serialutil import PortNotOpenError
17
+
18
+ from syndesi.adapters.adapter_worker import AdapterEvent, HasFileno
19
+ from syndesi.component import Descriptor
20
+ from syndesi.tools.errors import AdapterOpenError, AdapterReadError
9
21
  from syndesi.tools.types import NumberLike
10
22
 
11
23
  from .adapter import Adapter
12
- from .backend.descriptors import SerialPortDescriptor
13
- from .stop_condition import StopCondition
24
+ from .stop_conditions import Continuation, Fragment, StopCondition
14
25
  from .timeout import Timeout
15
26
 
16
27
 
28
+ @dataclass
29
+ class SerialPortDescriptor(Descriptor):
30
+ """
31
+ SerialPort descriptor that holds location (COMx or /dev/ttyx) and baudrate
32
+ """
33
+
34
+ DETECTION_PATTERN = r"(COM\d+|/dev[/\w\d]+):\d+"
35
+ port: str
36
+ baudrate: int | None = None
37
+
38
+ @staticmethod
39
+ def from_string(string: str) -> "SerialPortDescriptor":
40
+ parts = string.split(":")
41
+ port = parts[0]
42
+ baudrate = int(parts[1])
43
+ return SerialPortDescriptor(port, baudrate)
44
+
45
+ def set_default_baudrate(self, baudrate: int) -> bool:
46
+ """
47
+ Set the baudrate if it has not be defined before
48
+
49
+ Parameters
50
+ ----------
51
+ baudrate : int
52
+ """
53
+ if self.baudrate is not None:
54
+ self.baudrate = baudrate
55
+ return True
56
+
57
+ return False
58
+
59
+ def __str__(self) -> str:
60
+ return f"{self.port}:{self.baudrate}"
61
+
62
+ def is_initialized(self) -> bool:
63
+ return self.baudrate is not None
64
+
65
+
17
66
  class SerialPort(Adapter):
67
+ """
68
+ Serial communication adapter
69
+
70
+ Parameters
71
+ ----------
72
+ port : str
73
+ Serial port (COMx or ttyACMx)
74
+ baudrate : int
75
+ Baudrate
76
+ """
77
+
18
78
  def __init__(
19
79
  self,
20
80
  port: str,
21
81
  baudrate: int | None = None,
82
+ *,
22
83
  timeout: Timeout | NumberLike | None | EllipsisType = ...,
23
84
  stop_conditions: StopCondition | list[StopCondition] | EllipsisType = ...,
24
85
  alias: str = "",
25
86
  rts_cts: bool = False, # rts_cts experimental
26
- event_callback: Callable[[AdapterSignal], None] | None = None,
27
- backend_address: str | None = None,
28
- backend_port: int | None = None,
87
+ event_callback: Callable[[AdapterEvent], None] | None = None,
88
+ auto_open: bool = True,
29
89
  ) -> None:
30
90
  """
31
- Serial communication adapter
32
-
33
- Parameters
34
- ----------
35
- port : str
36
- Serial port (COMx or ttyACMx)
91
+ Instanciate new SerialPort adapter
37
92
  """
38
93
  descriptor = SerialPortDescriptor(port, baudrate)
39
94
  super().__init__(
@@ -42,15 +97,18 @@ class SerialPort(Adapter):
42
97
  stop_conditions=stop_conditions,
43
98
  alias=alias,
44
99
  event_callback=event_callback,
45
- backend_address=backend_address,
46
- backend_port=backend_port,
100
+ auto_open=auto_open,
47
101
  )
48
- self.descriptor: SerialPortDescriptor
102
+ self._descriptor: SerialPortDescriptor
103
+ self._worker_descriptor: SerialPortDescriptor
49
104
 
50
105
  self._logger.info(
51
- f"Setting up SerialPort adapter {self.descriptor}, timeout={timeout} and stop_conditions={self._stop_conditions}"
106
+ f"Setting up SerialPort adapter {self._descriptor}, \
107
+ timeout={timeout} and stop_conditions={self._stop_conditions}"
52
108
  )
53
109
 
110
+ self._port: serial.Serial | None = None
111
+
54
112
  self.open()
55
113
 
56
114
  self._rts_cts = rts_cts
@@ -58,7 +116,50 @@ class SerialPort(Adapter):
58
116
  def _default_timeout(self) -> Timeout:
59
117
  return Timeout(response=2, action="error")
60
118
 
61
- def set_baudrate(self, baudrate: int) -> None:
119
+ def _default_stop_conditions(self) -> list[StopCondition]:
120
+ return [Continuation(0.1)]
121
+
122
+ def _worker_open(self) -> None:
123
+ self._worker_check_descriptor()
124
+
125
+ if self._worker_descriptor.baudrate is None:
126
+ raise AdapterOpenError(
127
+ "Descriptor must be fully initialized to open the adapter"
128
+ )
129
+
130
+ if self._port is not None:
131
+ self.close()
132
+
133
+ try:
134
+ self._port = serial.Serial(
135
+ port=self._worker_descriptor.port,
136
+ baudrate=self._worker_descriptor.baudrate,
137
+ rtscts=self._rts_cts,
138
+ )
139
+ except serial.SerialException as e:
140
+ if "No such file" in str(e):
141
+ raise AdapterOpenError(
142
+ f"Port '{self._worker_descriptor.port}' was not found"
143
+ ) from e
144
+ raise AdapterOpenError("Unknown error") from e
145
+
146
+ if self._port.isOpen(): # type: ignore
147
+ self._logger.info(f"Adapter {self._worker_descriptor} opened")
148
+ else:
149
+ self._logger.error(f"Failed to open adapter {self._worker_descriptor}")
150
+ raise AdapterOpenError("Unknown error")
151
+
152
+ def _worker_close(self) -> None:
153
+ if self._port is not None:
154
+ self._port.close()
155
+ self._logger.info(f"Adapter {self._worker_descriptor} closed")
156
+
157
+ async def aflush_read(self) -> None:
158
+ await super().aflush_read()
159
+ if self._port is not None:
160
+ self._port.flush()
161
+
162
+ def set_default_baudrate(self, baudrate: int) -> None:
62
163
  """
63
164
  Set baudrate
64
165
 
@@ -66,15 +167,43 @@ class SerialPort(Adapter):
66
167
  ----------
67
168
  baudrate : int
68
169
  """
69
- if self.descriptor.set_default_baudrate(baudrate):
170
+ if self._descriptor.set_default_baudrate(baudrate):
171
+ self._update_descriptor()
70
172
  self.close()
71
173
  self.open()
72
174
 
73
- def open(self) -> None:
74
- if self.descriptor.baudrate is None:
75
- raise ValueError("Baudrate must be set, please use set_baudrate")
76
- super().open()
175
+ def _worker_write(self, data: bytes) -> None:
176
+ if self._rts_cts: # Experimental
177
+ self._port.setRTS(True) # type: ignore
178
+ if self._port is not None:
179
+ try:
180
+ self._port.write(data)
181
+ except (OSError, PortNotOpenError):
182
+ pass
183
+
184
+ def _worker_read(self, fragment_timestamp: float) -> Fragment:
185
+ if self._port is None:
186
+ raise AdapterReadError("Cannot read from non-initialized port")
187
+
188
+ try:
189
+ data = self._port.read_all()
190
+ except (OSError, PortNotOpenError):
191
+ self._logger.debug('Port error -> b""')
192
+ data = None
193
+
194
+ if data is None or data != b"":
195
+ raise AdapterReadError(
196
+ f"Error while reading from {self._worker_descriptor}"
197
+ )
198
+
199
+ return Fragment(data, fragment_timestamp)
200
+
201
+ def _selectable(self) -> HasFileno | None:
202
+ return self._port
203
+
204
+ # def is_opened(self) -> bool:
205
+ # if self._port is not None:
206
+ # if self._port.isOpen(): # type: ignore
207
+ # return True
77
208
 
78
- def close(self, force: bool = False) -> None:
79
- super().close(force)
80
- self._logger.info("Adapter closed")
209
+ # return False