syndesi 0.5.0__py3-none-any.whl → 0.5.1__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.
@@ -93,11 +93,12 @@ class Adapter(Component[bytes], AdapterWorker):
93
93
  event_callback: Callable[[AdapterEvent], None] | None = None,
94
94
  auto_open: bool = True,
95
95
  ) -> None:
96
- super().__init__(LoggerAlias.ADAPTER)
97
- self.encoding = encoding
96
+ Component.__init__(self, LoggerAlias.ADAPTER)
97
+ AdapterWorker.__init__(self, encoding)
98
+
98
99
  self._alias = alias
99
100
 
100
- self.descriptor = descriptor
101
+ self._descriptor = descriptor
101
102
  self.auto_open = auto_open
102
103
 
103
104
  self._initial_event_callback = event_callback
@@ -141,22 +142,33 @@ class Adapter(Component[bytes], AdapterWorker):
141
142
  # Serialize read/write/query ordering for async callers.
142
143
  self._async_io_lock = asyncio.Lock()
143
144
 
144
- self._logger.info(f"Setting up {self.descriptor} adapter ")
145
+ self._logger.info(f"Setting up {self._descriptor} adapter ")
145
146
  self._update_descriptor()
146
147
  self.set_stop_conditions(self._initial_stop_conditions)
147
148
  self.set_timeout(self._initial_timeout)
148
149
  self.set_event_callback(self._initial_event_callback)
149
150
 
150
- if self.descriptor.is_initialized() and auto_open:
151
+ if self._descriptor.is_initialized() and auto_open:
151
152
  self.open()
152
153
 
153
154
  weakref.finalize(self, self._cleanup)
154
155
 
156
+ def get_descriptor(self) -> Descriptor:
157
+ """
158
+ Return the adapter's descriptor
159
+
160
+ Returns
161
+ -------
162
+ descriptor : Descriptor
163
+ """
164
+ return self._descriptor
165
+
155
166
  # ┌──────────────────────────┐
156
167
  # │ Defaults / configuration │
157
168
  # └──────────────────────────┘
158
169
 
159
170
  def _stop(self) -> None:
171
+ super()._stop()
160
172
  cmd = StopThreadCommand()
161
173
  self._worker_send_command(cmd)
162
174
  try:
@@ -165,7 +177,7 @@ class Adapter(Component[bytes], AdapterWorker):
165
177
  pass
166
178
 
167
179
  def _update_descriptor(self) -> None:
168
- cmd = SetDescriptorCommand(self.descriptor)
180
+ cmd = SetDescriptorCommand(self._descriptor)
169
181
  self._worker_send_command(cmd)
170
182
  cmd.result(self.WorkerTimeout.IMMEDIATE_COMMAND.value)
171
183
 
@@ -178,27 +190,19 @@ class Adapter(Component[bytes], AdapterWorker):
178
190
  raise NotImplementedError
179
191
 
180
192
  def __str__(self) -> str:
181
- return str(self.descriptor)
193
+ return str(self._descriptor)
182
194
 
183
195
  def __repr__(self) -> str:
184
196
  return self.__str__()
185
197
 
186
198
  def _cleanup(self) -> None:
187
- # Be defensive: finalizers can run at interpreter shutdown.
188
199
  try:
189
200
  if self.is_open():
190
201
  self.close()
191
202
  except AdapterError:
192
203
  pass
193
-
194
204
  self._stop()
195
205
 
196
- try:
197
- self._command_queue_r.close()
198
- self._command_queue_w.close()
199
- except AdapterError:
200
- pass
201
-
202
206
  # ┌────────────┐
203
207
  # │ Public API │
204
208
  # └────────────┘
@@ -344,9 +348,10 @@ class Adapter(Component[bytes], AdapterWorker):
344
348
  scope: str = ReadScope.BUFFERED.value,
345
349
  ) -> AdapterFrame:
346
350
  with self._sync_io_lock:
347
- return self._read_detailed_future(
351
+ result = self._read_detailed_future(
348
352
  timeout=timeout, stop_conditions=stop_conditions, scope=scope
349
353
  ).result(self.WorkerTimeout.READ.value)
354
+ return result
350
355
 
351
356
  async def aread_detailed(
352
357
  self,
@@ -433,10 +438,12 @@ class Adapter(Component[bytes], AdapterWorker):
433
438
  scope: str = ReadScope.BUFFERED.value,
434
439
  ) -> AdapterFrame:
435
440
  async with self._async_io_lock:
436
- await self.aflush_read()
437
- await self.awrite(payload)
438
- return await self.aread_detailed(
439
- timeout=timeout, stop_conditions=stop_conditions, scope=scope
441
+ await asyncio.wrap_future(self._flush_read_future())
442
+ await asyncio.wrap_future(self._write_future(payload))
443
+ return await asyncio.wrap_future(
444
+ self._read_detailed_future(
445
+ timeout=timeout, stop_conditions=stop_conditions, scope=scope
446
+ )
440
447
  )
441
448
 
442
449
  def query_detailed(
@@ -448,11 +455,12 @@ class Adapter(Component[bytes], AdapterWorker):
448
455
  ) -> AdapterFrame:
449
456
 
450
457
  with self._sync_io_lock:
451
- self.flush_read()
452
- self.write(payload)
453
- return self.read_detailed(
458
+ self._flush_read_future().result(self.WorkerTimeout.IMMEDIATE_COMMAND.value)
459
+ self._write_future(payload).result(self.WorkerTimeout.WRITE.value)
460
+ output = self._read_detailed_future(
454
461
  timeout=timeout, stop_conditions=stop_conditions, scope=scope
455
- )
462
+ ).result(self.WorkerTimeout.READ.value)
463
+ return output
456
464
 
457
465
  # ==== Other ====
458
466
 
@@ -38,6 +38,7 @@ from .stop_conditions import (
38
38
  Total,
39
39
  )
40
40
  from .timeout import Timeout, TimeoutAction, any_to_timeout
41
+ from .tracehub import tracehub
41
42
 
42
43
 
43
44
  def nmin(a: float | None, b: float | None) -> float | None:
@@ -71,22 +72,18 @@ class HasFileno(Protocol):
71
72
  # │ Adapter events │
72
73
  # └────────────────┘
73
74
 
74
-
75
75
  class AdapterEvent(Event):
76
76
  """Adapter event"""
77
77
 
78
-
79
78
  class AdapterDisconnectedEvent(AdapterEvent):
80
79
  """Adapter disconnected event"""
81
80
 
82
-
83
81
  @dataclass
84
82
  class AdapterFrameEvent(AdapterEvent):
85
83
  """Adapter frame event, emitted when new data is available"""
86
84
 
87
85
  frame: AdapterFrame
88
86
 
89
-
90
87
  @dataclass
91
88
  class FirstFragmentEvent(AdapterEvent):
92
89
  """Adapter first fragment event"""
@@ -238,7 +235,8 @@ class AdapterWorker:
238
235
  _FRAME_BUFFER_MAX = 256
239
236
  _COMMAND_READY = b"\x00"
240
237
 
241
- def __init__(self) -> None:
238
+ def __init__(self, encoding : str) -> None:
239
+ self.encoding = encoding
242
240
  # Command queue (worker input)
243
241
  self._command_queue_r, self._command_queue_w = socket.socketpair()
244
242
  self._command_queue_r.setblocking(False)
@@ -270,12 +268,16 @@ class AdapterWorker:
270
268
  self._first_fragment_timestamp: float | None = None
271
269
  self._last_fragment_timestamp: float | None = None
272
270
  self._last_write_timestamp: float | None = None
273
- self._timeout_origin: StopConditionType | None = None
271
+ self._timeout_origin: StopConditionType = StopConditionType.TIMEOUT
274
272
  self._next_stop_condition_timeout_timestamp: float | None = None
275
273
  self._read_start_timestamp: float | None = None
276
274
 
277
275
  self._event_callback: Callable[[AdapterEvent], None] | None = None
278
276
 
277
+ def _stop(self) -> None:
278
+ self._command_queue_r.close()
279
+ self._command_queue_w.close()
280
+
279
281
  # ┌─────────────────┐
280
282
  # │ Worker plumbing │
281
283
  # └─────────────────┘
@@ -323,12 +325,18 @@ class AdapterWorker:
323
325
  self._worker_open()
324
326
  if not self._opened:
325
327
  raise AdapterWriteError("Adapter not opened")
328
+ if self._worker_descriptor is not None:
329
+ tracehub.emit_write(str(self._worker_descriptor), data)
326
330
 
327
331
  @abstractmethod
328
- def _worker_open(self) -> None: ...
332
+ def _worker_open(self) -> None:
333
+ if self._worker_descriptor is not None:
334
+ tracehub.emit_open(str(self._worker_descriptor))
329
335
 
330
336
  @abstractmethod
331
- def _worker_close(self) -> None: ...
337
+ def _worker_close(self) -> None:
338
+ if self._worker_descriptor is not None:
339
+ tracehub.emit_close(str(self._worker_descriptor))
332
340
 
333
341
  # ┌──────────────────────────┐
334
342
  # │ Worker: command handling │
@@ -469,6 +477,9 @@ class AdapterWorker:
469
477
  - always emit callback event (if configured)
470
478
  """
471
479
  self._worker_emit_event(AdapterFrameEvent(frame))
480
+ if self._worker_descriptor is not None:
481
+ payload = frame.get_payload()
482
+ tracehub.emit_read(str(self._worker_descriptor), payload, frame.stop_condition_type)
472
483
 
473
484
  pr = self._pending_read
474
485
  if pr is not None:
@@ -516,7 +527,7 @@ class AdapterWorker:
516
527
  AdapterFrame(
517
528
  fragments=[Fragment(b"", time.time())],
518
529
  stop_timestamp=None,
519
- stop_condition_type=None,
530
+ stop_condition_type=StopConditionType.TIMEOUT,
520
531
  previous_read_buffer_used=False,
521
532
  response_delay=None,
522
533
  )
@@ -640,7 +651,13 @@ class AdapterWorker:
640
651
 
641
652
  self.fragments.append(kept)
642
653
 
654
+ # If there's no stop, break here
643
655
  if stop_condition_type is None:
656
+ # Only upload emit a fragment event if there's no frame
657
+ if self._worker_descriptor is not None:
658
+ tracehub.emit_fragment(str(self._worker_descriptor), kept.data)
659
+
660
+
644
661
  break
645
662
 
646
663
  # frame complete
@@ -708,7 +725,7 @@ class AdapterWorker:
708
725
  self._last_write_timestamp = None
709
726
  self.fragments = []
710
727
  self._next_stop_condition_timeout_timestamp = None
711
- self._timeout_origin = None
728
+ self._timeout_origin = StopConditionType.TIMEOUT
712
729
 
713
730
  def _worker_next_timeout_timestamp(self) -> float | None:
714
731
  stop_conditions = self._stop_conditions
@@ -803,6 +820,7 @@ class AdapterWorker:
803
820
 
804
821
  # Timeout wakeup: decide what timed out
805
822
  # 1) pending read response timeout (before qualifying first fragment)
823
+ print('Pending read')
806
824
  if (
807
825
  self._pending_read is not None
808
826
  and not self._pending_read.first_fragment_seen
syndesi/adapters/ip.py CHANGED
@@ -10,9 +10,6 @@ from collections.abc import Callable
10
10
  from dataclasses import dataclass
11
11
  from enum import StrEnum
12
12
  from types import EllipsisType
13
- from typing import cast
14
-
15
- import _socket
16
13
 
17
14
  from syndesi.adapters.adapter_worker import AdapterEvent, HasFileno
18
15
  from syndesi.adapters.stop_conditions import Continuation, StopCondition
@@ -142,7 +139,8 @@ class IP(Adapter):
142
139
  port=port,
143
140
  transport=IPDescriptor.Transport(transport.upper()),
144
141
  )
145
- self._socket: _socket.socket | None = None
142
+ #self._socket: _socket.socket | None = None
143
+ self._socket : socket.socket | None = None
146
144
 
147
145
  super().__init__(
148
146
  descriptor=descriptor,
@@ -194,21 +192,16 @@ class IP(Adapter):
194
192
  )
195
193
 
196
194
  def _worker_open(self) -> None:
195
+ super()._worker_open()
197
196
  self._worker_check_descriptor()
198
197
 
199
198
  # Create the socket instance
200
199
  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
- )
200
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
205
201
  elif self._worker_descriptor.transport == IPDescriptor.Transport.UDP:
206
- self._socket = cast(
207
- _socket.socket, socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
208
- )
202
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
209
203
  else:
210
204
  raise AdapterOpenError("Invalid transport protocol")
211
-
212
205
  try:
213
206
  self._socket.settimeout(self.WorkerTimeout.OPEN.value)
214
207
  self._socket.connect(
@@ -224,9 +217,10 @@ class IP(Adapter):
224
217
  self._logger.info(f"IP Adapter {self._worker_descriptor} opened")
225
218
 
226
219
  def _worker_close(self) -> None:
220
+ super()._worker_close()
227
221
  if self._socket is not None:
228
222
  try:
229
- self._socket.shutdown(_socket.SHUT_RDWR)
223
+ self._socket.shutdown(socket.SHUT_RDWR)
230
224
  self._socket.close()
231
225
  except OSError:
232
226
  pass
@@ -8,8 +8,10 @@ the OS layers (COMx, /dev/ttyUSBx or /dev/ttyACMx)
8
8
 
9
9
  """
10
10
 
11
+ import threading
11
12
  from collections.abc import Callable
12
13
  from dataclasses import dataclass
14
+ from enum import StrEnum
13
15
  from types import EllipsisType
14
16
 
15
17
  import serial
@@ -25,6 +27,18 @@ from .stop_conditions import Continuation, Fragment, StopCondition
25
27
  from .timeout import Timeout
26
28
 
27
29
 
30
+ class Parity(StrEnum):
31
+ """
32
+ SerialPort parity setting, copied from pyserial
33
+ """
34
+ NONE = "N"
35
+ EVEN = "E"
36
+ ODD = "O"
37
+ MARK = "M"
38
+ SPACE = "S"
39
+
40
+
41
+ # pylint: disable=too-many-instance-attributes
28
42
  @dataclass
29
43
  class SerialPortDescriptor(Descriptor):
30
44
  """
@@ -34,6 +48,12 @@ class SerialPortDescriptor(Descriptor):
34
48
  DETECTION_PATTERN = r"(COM\d+|/dev[/\w\d]+):\d+"
35
49
  port: str
36
50
  baudrate: int | None = None
51
+ bytesize: int = 8
52
+ stopbits: int = 1
53
+ parity: str = Parity.NONE.value
54
+ rts_cts: bool = False
55
+ dsr_dtr: bool = False
56
+ xon_xoff: bool = False
37
57
 
38
58
  @staticmethod
39
59
  def from_string(string: str) -> "SerialPortDescriptor":
@@ -50,7 +70,7 @@ class SerialPortDescriptor(Descriptor):
50
70
  ----------
51
71
  baudrate : int
52
72
  """
53
- if self.baudrate is not None:
73
+ if self.baudrate is None:
54
74
  self.baudrate = baudrate
55
75
  return True
56
76
 
@@ -75,6 +95,9 @@ class SerialPort(Adapter):
75
95
  Baudrate
76
96
  """
77
97
 
98
+ _open_ports: set[str] = set()
99
+ _open_ports_lock = threading.Lock()
100
+
78
101
  def __init__(
79
102
  self,
80
103
  port: str,
@@ -83,14 +106,29 @@ class SerialPort(Adapter):
83
106
  timeout: Timeout | NumberLike | None | EllipsisType = ...,
84
107
  stop_conditions: StopCondition | list[StopCondition] | EllipsisType = ...,
85
108
  alias: str = "",
86
- rts_cts: bool = False, # rts_cts experimental
109
+ bytesize: int = 8,
110
+ stopbits: int = 1,
111
+ parity: str = Parity.NONE.value,
112
+ rts_cts: bool = False,
113
+ xon_xoff: bool = False,
114
+ dsr_dtr: bool = False,
87
115
  event_callback: Callable[[AdapterEvent], None] | None = None,
88
116
  auto_open: bool = True,
89
117
  ) -> None:
90
118
  """
91
119
  Instanciate new SerialPort adapter
92
120
  """
93
- descriptor = SerialPortDescriptor(port, baudrate)
121
+ self._port: serial.Serial | None = None
122
+ descriptor = SerialPortDescriptor(
123
+ port=port,
124
+ baudrate=baudrate,
125
+ bytesize=bytesize,
126
+ stopbits=stopbits,
127
+ parity=parity,
128
+ rts_cts=rts_cts,
129
+ dsr_dtr=dsr_dtr,
130
+ xon_xoff=xon_xoff,
131
+ )
94
132
  super().__init__(
95
133
  descriptor=descriptor,
96
134
  timeout=timeout,
@@ -107,11 +145,11 @@ class SerialPort(Adapter):
107
145
  timeout={timeout} and stop_conditions={self._stop_conditions}"
108
146
  )
109
147
 
110
- self._port: serial.Serial | None = None
111
-
112
- self.open()
113
-
114
- self._rts_cts = rts_cts
148
+ # self._bytesize = bytesize
149
+ # self._stopbits = stopbits
150
+ # self._parity = Parity(parity)
151
+ # self._xonxoff = xon_xoff
152
+ # self._dsrdtr = dsr_dtr
115
153
 
116
154
  def _default_timeout(self) -> Timeout:
117
155
  return Timeout(response=2, action="error")
@@ -120,6 +158,7 @@ class SerialPort(Adapter):
120
158
  return [Continuation(0.1)]
121
159
 
122
160
  def _worker_open(self) -> None:
161
+ super()._worker_open()
123
162
  self._worker_check_descriptor()
124
163
 
125
164
  if self._worker_descriptor.baudrate is None:
@@ -130,13 +169,26 @@ class SerialPort(Adapter):
130
169
  if self._port is not None:
131
170
  self.close()
132
171
 
172
+ port_name = self._worker_descriptor.port
173
+ with self._open_ports_lock:
174
+ if port_name in self._open_ports:
175
+ raise AdapterOpenError(f"Port '{port_name}' is already in use")
176
+ self._open_ports.add(port_name)
177
+
133
178
  try:
134
179
  self._port = serial.Serial(
135
180
  port=self._worker_descriptor.port,
136
181
  baudrate=self._worker_descriptor.baudrate,
137
- rtscts=self._rts_cts,
182
+ rtscts=self._worker_descriptor.rts_cts,
183
+ bytesize=self._worker_descriptor.bytesize,
184
+ parity=self._worker_descriptor.parity,
185
+ stopbits=self._worker_descriptor.stopbits,
186
+ xonxoff=self._worker_descriptor.xon_xoff,
187
+ dsrdtr=self._worker_descriptor.dsr_dtr
138
188
  )
139
189
  except serial.SerialException as e:
190
+ with self._open_ports_lock:
191
+ self._open_ports.discard(port_name)
140
192
  if "No such file" in str(e):
141
193
  raise AdapterOpenError(
142
194
  f"Port '{self._worker_descriptor.port}' was not found"
@@ -146,13 +198,19 @@ class SerialPort(Adapter):
146
198
  if self._port.isOpen(): # type: ignore
147
199
  self._logger.info(f"Adapter {self._worker_descriptor} opened")
148
200
  else:
201
+ with self._open_ports_lock:
202
+ self._open_ports.discard(port_name)
149
203
  self._logger.error(f"Failed to open adapter {self._worker_descriptor}")
150
204
  raise AdapterOpenError("Unknown error")
151
205
 
152
206
  def _worker_close(self) -> None:
207
+ super()._worker_close()
153
208
  if self._port is not None:
154
209
  self._port.close()
155
210
  self._logger.info(f"Adapter {self._worker_descriptor} closed")
211
+ self._port = None
212
+ with self._open_ports_lock:
213
+ self._open_ports.discard(self._worker_descriptor.port)
156
214
 
157
215
  async def aflush_read(self) -> None:
158
216
  await super().aflush_read()
@@ -173,7 +231,8 @@ class SerialPort(Adapter):
173
231
  self.open()
174
232
 
175
233
  def _worker_write(self, data: bytes) -> None:
176
- if self._rts_cts: # Experimental
234
+ super()._worker_write(data)
235
+ if self._worker_descriptor.rts_cts: # Experimental
177
236
  self._port.setRTS(True) # type: ignore
178
237
  if self._port is not None:
179
238
  try:
@@ -188,10 +247,9 @@ class SerialPort(Adapter):
188
247
  try:
189
248
  data = self._port.read_all()
190
249
  except (OSError, PortNotOpenError):
191
- self._logger.debug('Port error -> b""')
192
250
  data = None
193
251
 
194
- if data is None or data != b"":
252
+ if data is None or data == b"":
195
253
  raise AdapterReadError(
196
254
  f"Error while reading from {self._worker_descriptor}"
197
255
  )
@@ -39,12 +39,12 @@ class StopConditionType(Enum):
39
39
  """
40
40
  Stop-condition type
41
41
  """
42
-
43
42
  TERMINATION = "termination"
44
43
  LENGTH = "length"
45
44
  CONTINUATION = "continuation"
46
45
  TOTAL = "total"
47
46
  FRAGMENT = "fragment"
47
+ TIMEOUT = "timeout"
48
48
 
49
49
 
50
50
  class StopCondition:
@@ -98,21 +98,13 @@ class Termination(StopCondition):
98
98
  self._sequence = sequence.encode("utf-8")
99
99
  else:
100
100
  self._sequence = sequence
101
- self._sequence_found_length = 0
102
-
103
- # TYPE = StopConditionType.TERMINATION
104
101
 
105
- # def __init__(self, sequence: bytes | str) -> None:
106
- # """
107
- # Instanciate a new Termination class
108
- # """
109
- # self.sequence: bytes
110
- # if isinstance(sequence, str):
111
- # self.sequence = sequence.encode("utf-8")
112
- # elif isinstance(sequence, bytes):
113
- # self.sequence = sequence
114
- # else:
115
- # raise ValueError(f"Invalid termination sequence type : {type(sequence)}")
102
+ if self._sequence == b"":
103
+ raise ValueError(
104
+ "Empty termination isn't allowed. If you wish "
105
+ "to stop on any received data, use Datagram stop-conditions instead"
106
+ )
107
+ self._sequence_found_length = 0
116
108
 
117
109
  def __str__(self) -> str:
118
110
  return f"Termination({repr(self._sequence)})"
@@ -202,7 +194,6 @@ class Length(StopCondition):
202
194
  deferred_fragment = raw_fragment[remaining_bytes:]
203
195
  self._counter += len(kept_fragment.data)
204
196
  remaining_bytes = self._n - self._counter
205
- # TODO : remaining_bytes <= 0 ? Alongside above TODO maybe
206
197
  return remaining_bytes == 0, kept_fragment, deferred_fragment, None
207
198
 
208
199
 
@@ -250,6 +241,8 @@ class Continuation(StopCondition):
250
241
  stop = False
251
242
  next_event_timeout = None
252
243
 
244
+ self._last_fragment = raw_fragment.timestamp
245
+
253
246
  return stop, kept, deferred, next_event_timeout
254
247
 
255
248
  def type(self) -> StopConditionType:
@@ -17,12 +17,10 @@ class TimeoutAction(Enum):
17
17
  """
18
18
  Action on timeout expiration
19
19
  """
20
-
21
20
  ERROR = "error"
22
21
  RETURN_EMPTY = "return_empty"
23
22
  RETURN_NONE = "return_none"
24
23
 
25
-
26
24
  class Timeout:
27
25
  """
28
26
  This class holds timeout information
@@ -60,7 +58,7 @@ class Timeout:
60
58
  self._response: EllipsisType | NumberLike | None = response
61
59
 
62
60
  def __str__(self) -> str:
63
- if self._response is ...:
61
+ if self._response is ... or self._response is None:
64
62
  r = "..."
65
63
  else:
66
64
  r = f"{self._response:.3f}"