syndesi 0.1.4__py3-none-any.whl → 0.1.6__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.
@@ -1,4 +1,7 @@
1
- from .iadapter import IAdapter
1
+ from .adapter import Adapter
2
2
  from .ip import IP
3
- from .serial import Serial
4
- from .visa import VISA
3
+ from .serialport import SerialPort
4
+ from .visa import VISA
5
+
6
+ from .timeout import Timeout
7
+ from .stop_conditions import Termination, Length, StopCondition
@@ -0,0 +1,304 @@
1
+ # adapters.py
2
+ # Sébastien Deriaz
3
+ # 06.05.2023
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
17
+ #
18
+
19
+ from abc import abstractmethod, ABC
20
+ from .timed_queue import TimedQueue
21
+ from threading import Thread
22
+ from typing import Union
23
+ from enum import Enum
24
+ from .stop_conditions import StopCondition, Termination, Length
25
+ from .timeout import Timeout, TimeoutException, timeout_fuse
26
+ from typing import Union
27
+ from ..tools.types import is_number
28
+ from ..tools.log import LoggerAlias
29
+ import logging
30
+ from time import time
31
+ from dataclasses import dataclass
32
+ from ..tools.others import default_argument, is_default_argument
33
+
34
+ DEFAULT_TIMEOUT = default_argument(Timeout(response=1, continuation=100e-3, total=None))
35
+ DEFAULT_STOP_CONDITION = default_argument(StopCondition())
36
+
37
+ STOP_DESIGNATORS = {
38
+ 'timeout' : {
39
+ Timeout.TimeoutType.RESPONSE : 'TR',
40
+ Timeout.TimeoutType.CONTINUATION : 'TC',
41
+ Timeout.TimeoutType.TOTAL : 'TT'
42
+ },
43
+ 'stop_condition' : {
44
+ Termination : 'ST',
45
+ Length : 'SL'
46
+ },
47
+ 'previous-read-buffer' : 'RB'
48
+ }
49
+
50
+ class Origin(Enum):
51
+ TIMEOUT = 'timeout'
52
+ STOP_CONDITION = 'stop_condition'
53
+
54
+ @dataclass
55
+ class ReturnMetrics:
56
+ read_duration : float
57
+ origin : Origin
58
+ timeout_type : Timeout.TimeoutType
59
+ stop_condition : StopCondition
60
+ previous_read_buffer_used : bool
61
+ n_fragments : int
62
+ response_time : float
63
+ continuation_times : list
64
+ total_time : float
65
+
66
+ class Adapter(ABC):
67
+ class Status(Enum):
68
+ DISCONNECTED = 0
69
+ CONNECTED = 1
70
+
71
+ def __init__(self,
72
+ alias : str = '',
73
+ timeout : Union[float, Timeout] = DEFAULT_TIMEOUT,
74
+ stop_condition : Union[StopCondition, None] = DEFAULT_STOP_CONDITION):
75
+ """
76
+ Adapter instance
77
+
78
+ Parameters
79
+ ----------
80
+ alias : str
81
+ The alias is used to identify the class in the logs
82
+ timeout : float or Timeout instance
83
+ Default timeout is Timeout(response=1, continuation=0.1, total=None)
84
+ stop_condition : StopCondition or None
85
+ Default to None
86
+ """
87
+
88
+ if is_number(timeout):
89
+ self._timeout = Timeout(response=timeout, continuation=100e-3)
90
+ elif isinstance(timeout, Timeout):
91
+ self._timeout = timeout
92
+ else:
93
+ raise ValueError(f"Invalid timeout type : {type(timeout)}")
94
+
95
+ self._stop_condition = stop_condition
96
+ self._read_queue = TimedQueue()
97
+ self._thread : Union[Thread, None] = None
98
+ self._status = self.Status.DISCONNECTED
99
+ # Buffer for data that has been pulled from the queue but
100
+ # not used because of termination or length stop condition
101
+ self._previous_read_buffer = b''
102
+
103
+ self._alias = alias
104
+ self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
105
+
106
+ def set_default_timeout(self, default_timeout : Union[Timeout, tuple, float]):
107
+ """
108
+ Set the default timeout for this adapter. If a previous timeout has been set, it will be fused
109
+
110
+ Parameters
111
+ ----------
112
+ default_timeout : Timeout or tuple or float
113
+ """
114
+ self._timeout = timeout_fuse(self._timeout, default_timeout)
115
+
116
+ def set_default_stop_condition(self, stop_condition):
117
+ """
118
+ Set the default stop condition for this adapter.
119
+
120
+ Parameters
121
+ ----------
122
+ stop_condition : StopCondition
123
+ """
124
+ if is_default_argument(self._stop_condition):
125
+ self._stop_condition = stop_condition
126
+
127
+
128
+ def flushRead(self):
129
+ """
130
+ Flush the input buffer
131
+ """
132
+ self._read_queue.clear()
133
+ self._previous_read_buffer = b''
134
+
135
+ @abstractmethod
136
+ def open(self):
137
+ """
138
+ Start communication with the device
139
+ """
140
+ pass
141
+
142
+ @abstractmethod
143
+ def close(self):
144
+ """
145
+ Stop communication with the device
146
+ """
147
+ pass
148
+
149
+ @abstractmethod
150
+ def write(self, data : Union[bytes, str]):
151
+ """
152
+ Send data to the device
153
+
154
+ Parameters
155
+ ----------
156
+ data : bytes or str
157
+ """
158
+ pass
159
+
160
+ @abstractmethod
161
+ def _start_thread(self):
162
+ """
163
+ Initiate the read thread
164
+ """
165
+ pass
166
+
167
+ def read(self, timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
168
+ """
169
+ Read data from the device
170
+
171
+ Parameters
172
+ ----------
173
+ timeout : Timeout or None
174
+ Set a custom timeout, if None (default), the adapter timeout is used
175
+ stop_condition : StopCondition or None
176
+ Set a custom stop condition, if None (Default), the adapater stop condition is used
177
+ return_metrics : ReturnMetrics class
178
+ """
179
+ read_start = time()
180
+ if self._status == self.Status.DISCONNECTED:
181
+ self.open()
182
+
183
+ # Use adapter values if no custom value is specified
184
+ if timeout is None:
185
+ timeout = self._timeout
186
+ elif isinstance(timeout, float):
187
+ timeout = Timeout(timeout)
188
+
189
+
190
+ if stop_condition is None:
191
+ stop_condition = self._stop_condition
192
+
193
+ # If the adapter is closed, open it
194
+ if self._status == self.Status.DISCONNECTED:
195
+ self.open()
196
+
197
+ if self._thread is None or not self._thread.is_alive():
198
+ self._start_thread()
199
+
200
+ timeout_ms = timeout.initiate_read(len(self._previous_read_buffer) > 0)
201
+
202
+ if stop_condition is not None:
203
+ stop_condition.initiate_read()
204
+
205
+ deferred_buffer = b''
206
+
207
+ # Start with the deferred buffer
208
+ # TODO : Check if data could be lost here, like the data is put in the previous_read_buffer and is never
209
+ # read back again because there's no stop condition
210
+ if len(self._previous_read_buffer) > 0 and stop_condition is not None:
211
+ stop, output, self._previous_read_buffer = stop_condition.evaluate(self._previous_read_buffer)
212
+ previous_read_buffer_used = True
213
+ else:
214
+ stop = False
215
+ output = b''
216
+ previous_read_buffer_used = False
217
+
218
+ n_fragments = 0
219
+ # If everything is used up, read the queue
220
+ if not stop:
221
+ while True:
222
+ (timestamp, fragment) = self._read_queue.get(timeout_ms)
223
+ n_fragments += 1
224
+
225
+ # 1) Evaluate the timeout
226
+ stop, timeout_ms = timeout.evaluate(timestamp)
227
+ if stop:
228
+ data_strategy, origin = timeout.dataStrategy()
229
+ if data_strategy == Timeout.OnTimeoutStrategy.DISCARD:
230
+ # Trash everything
231
+ output = b''
232
+ elif data_strategy == Timeout.OnTimeoutStrategy.RETURN:
233
+ # Return the data that has been read up to this point
234
+ output += deferred_buffer
235
+ if fragment is not None:
236
+ output += fragment
237
+ elif data_strategy == Timeout.OnTimeoutStrategy.STORE:
238
+ # Store the data
239
+ self._previous_read_buffer = output
240
+ output = b''
241
+ elif data_strategy == Timeout.OnTimeoutStrategy.ERROR:
242
+ raise TimeoutException(origin)
243
+ break
244
+ else:
245
+ origin = None
246
+
247
+
248
+
249
+ # Add the deferred buffer
250
+ if len(deferred_buffer) > 0:
251
+ fragment = deferred_buffer + fragment
252
+
253
+ # 2) Evaluate the stop condition
254
+ if stop_condition is not None:
255
+ stop, kept_fragment, deferred_buffer = stop_condition.evaluate(fragment)
256
+ output += kept_fragment
257
+ if stop:
258
+ self._previous_read_buffer = deferred_buffer
259
+ else:
260
+ output += fragment
261
+ if stop:
262
+ break
263
+
264
+ if origin is not None:
265
+ # The stop originates from the timeout
266
+ designator = STOP_DESIGNATORS['timeout'][origin]
267
+ else:
268
+ designator = STOP_DESIGNATORS['stop_condition'][type(stop_condition)]
269
+ else:
270
+ designator = STOP_DESIGNATORS['previous-read-buffer']
271
+
272
+ read_duration = time() - read_start
273
+ if self._previous_read_buffer:
274
+ self._logger.debug(f'Read [{designator}, {read_duration*1e3:.3f}ms] : {output} , previous read buffer : {self._previous_read_buffer}')
275
+ else:
276
+ self._logger.debug(f'Read [{designator}, {read_duration*1e3:.3f}ms] : {output}')
277
+
278
+ if return_metrics:
279
+ return output, ReturnMetrics(
280
+ read_duration=read_duration,
281
+ origin=Origin.TIMEOUT if origin is not None else Origin.STOP_CONDITION,
282
+ timeout_type=origin if origin is not None else None,
283
+ stop_condition=type(stop_condition) if origin is None else None,
284
+ previous_read_buffer_used=previous_read_buffer_used,
285
+ n_fragments=n_fragments,
286
+ response_time=timeout.response_time,
287
+ continuation_times=timeout.continuation_times,
288
+ total_time=timeout.total_time
289
+ )
290
+ else:
291
+ return output
292
+
293
+ @abstractmethod
294
+ def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
295
+ """
296
+ Shortcut function that combines
297
+ - flush_read
298
+ - write
299
+ - read
300
+ """
301
+ pass
302
+
303
+ def __del__(self):
304
+ self.close()
@@ -0,0 +1,45 @@
1
+ # auto.py
2
+ # Sébastien Deriaz
3
+ # 24.06.2024
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 simply 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
+
16
+ from typing import Union
17
+ import re
18
+ from . import Adapter, IP, SerialPort
19
+
20
+ IP_PATTERN = '([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(:[0-9]+)*'
21
+
22
+ WINDOWS_SERIAL_PATTERN = '(COM[0-9]+)(:[0-9]+)*'
23
+ LINUX_SERIAL_PATTERN = '(/dev/tty[a-zA-Z0-9]+)(:[0-9]+)*'
24
+
25
+ def auto_adapter(adapter_or_string : Union[Adapter, str]):
26
+ if isinstance(adapter_or_string, Adapter):
27
+ # Simply return it
28
+ return adapter_or_string
29
+ elif isinstance(adapter_or_string, str):
30
+ # Parse it
31
+ ip_match = re.match(IP_PATTERN, adapter_or_string)
32
+ if ip_match:
33
+ # Return an IP adapter
34
+ return IP(address=ip_match.groups(0), port=ip_match.groups(1))
35
+ elif re.match(WINDOWS_SERIAL_PATTERN, adapter_or_string):
36
+ port, baudrate = re.match(WINDOWS_SERIAL_PATTERN, adapter_or_string).groups()
37
+ return SerialPort(port=port, baudrate=int(baudrate))
38
+ elif re.match(LINUX_SERIAL_PATTERN, adapter_or_string):
39
+ port, baudrate = re.match(LINUX_SERIAL_PATTERN, adapter_or_string)
40
+ return SerialPort(port=port, baudrate=int(baudrate))
41
+ else:
42
+ raise ValueError(f"Couldn't parse adapter description : {adapter_or_string}")
43
+
44
+ else:
45
+ raise ValueError(f"Invalid adapter : {adapter_or_string}")
@@ -9,30 +9,76 @@
9
9
  # - VISA
10
10
  #
11
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 communication "raw"
12
+ # This is a limitation as it is to this day not possible to communicate "raw"
13
13
  # with a device through USB yet
14
14
  #
15
- #
16
- #
17
- #
18
- # Timeout system
19
- # We can differentiate two kinds of timeout :
20
- # - transmission timeout (aka "timeout"): the time it takes for a device to start transmitting what we expect
21
- # - continuation timeout : the time it takes for a device to continue sending the requested data
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
22
17
 
23
18
  from abc import abstractmethod, ABC
19
+ from .timed_queue import TimedQueue
20
+ from threading import Thread
21
+ from typing import Union
22
+ from enum import Enum
23
+ from .stop_conditions import StopCondition
24
+ from .timeout import Timeout, TimeoutException
25
+ from typing import Union
26
+ from ..tools.types import is_number
27
+ from ..tools.logger import AdapterLogger
28
+
29
+ DEFAULT_TIMEOUT = Timeout(response=1, continuation=100e-3, total=None)
30
+ DEFAUT_STOP_CONDITION = None
24
31
 
25
32
  class IAdapter(ABC):
26
- @abstractmethod
27
- def __init__(self, descriptor, *args):
28
- pass
29
33
 
30
- @abstractmethod
34
+ class Status(Enum):
35
+ DISCONNECTED = 0
36
+ CONNECTED = 1
37
+
38
+ def __init__(self,
39
+ timeout : Union[float, Timeout] = DEFAULT_TIMEOUT,
40
+ stop_condition : Union[StopCondition, None] = DEFAUT_STOP_CONDITION,
41
+ log_level : int = 0):
42
+ """
43
+ IAdapter instance
44
+
45
+ Parameters
46
+ ----------
47
+ timeout : float or Timeout instance
48
+ Default timeout is Timeout(response=1, continuation=0.1, total=None)
49
+ stop_condition : StopCondition or None
50
+ Default to None
51
+ log : int
52
+ 0 : basic logging
53
+ 1 : detailed logging
54
+ 2 : complete logging
55
+ """
56
+
57
+ if is_number(timeout):
58
+ self._timeout = Timeout(response=timeout, continuation=100e-3)
59
+ elif isinstance(timeout, Timeout):
60
+ self._timeout = timeout
61
+ else:
62
+ raise ValueError(f"Invalid timeout type : {type(timeout)}")
63
+
64
+ self._stop_condition = stop_condition
65
+ self._read_queue = TimedQueue()
66
+ self._thread : Union[Thread, None] = None
67
+ self._status = self.Status.DISCONNECTED
68
+ # Buffer for data that has been pulled from the queue but
69
+ # not used because of termination or length stop condition
70
+ self._previous_read_buffer = b''
71
+
72
+ self._logger = AdapterLogger('adapter')
73
+ self._logger.set_log_level(log_level)
74
+ self._logger.log_status('Initializing adapter', 0)
75
+
31
76
  def flushRead(self):
32
77
  """
33
78
  Flush the input buffer
34
79
  """
35
- pass
80
+ self._read_queue.clear()
81
+ self._previous_read_buffer = b''
36
82
 
37
83
  @abstractmethod
38
84
  def open(self):
@@ -49,25 +95,117 @@ class IAdapter(ABC):
49
95
  pass
50
96
 
51
97
  @abstractmethod
52
- def write(self, data : bytes):
98
+ def write(self, data : Union[bytes, str]):
53
99
  """
54
100
  Send data to the device
101
+
102
+ Parameters
103
+ ----------
104
+ data : bytes or str
55
105
  """
56
106
  pass
57
-
107
+
58
108
  @abstractmethod
59
- def read(self, timeout=None, continuation_timeout=None, until_char=None) -> bytes:
109
+ def _start_thread(self):
60
110
  """
61
- Read data from the device
111
+ Initiate the read thread
62
112
  """
63
113
  pass
114
+
115
+ def read(self, timeout=None, stop_condition=None) -> bytes:
116
+ """
117
+ Read data from the device
64
118
 
65
- @abstractmethod
66
- def query(self, data : bytes, timeout=None, continuation_timeout=None) -> bytes:
119
+ Parameters
120
+ ----------
121
+ timeout : Timeout or None
122
+ Set a custom timeout, if None (default), the adapter timeout is used
123
+ stop_condition : StopCondition or None
124
+ Set a custom stop condition, if None (Default), the adapater stop condition is used
125
+ """
126
+ if self._status == self.Status.DISCONNECTED:
127
+ self.open()
128
+
129
+ # Use adapter values if no custom value is specified
130
+ if timeout is None:
131
+ timeout = self._timeout
132
+
133
+ if stop_condition is None:
134
+ stop_condition = self._stop_condition
135
+
136
+ # If the adapter is closed, open it
137
+ if self._status == self.Status.DISCONNECTED:
138
+ self.open()
139
+
140
+ if self._thread is None or not self._thread.is_alive():
141
+ self._start_thread()
142
+
143
+ timeout_ms = timeout.initiate_read(len(self._previous_read_buffer) > 0)
144
+
145
+ if stop_condition is not None:
146
+ stop_condition.initiate_read()
147
+
148
+ deferred_buffer = b''
149
+
150
+ # Start with the deferred buffer
151
+ if len(self._previous_read_buffer) > 0 and stop_condition is not None:
152
+ stop, output, self._previous_read_buffer = stop_condition.evaluate(self._previous_read_buffer)
153
+ else:
154
+ stop = False
155
+ output = b''
156
+ # If everything is used up, read the queue
157
+ if not stop:
158
+ while True:
159
+ (timestamp, fragment) = self._read_queue.get(timeout_ms)
160
+
161
+ # 1) Evaluate the timeout
162
+ stop, timeout_ms = timeout.evaluate(timestamp)
163
+ if stop:
164
+ data_strategy, origin = timeout.dataStrategy()
165
+ if data_strategy == Timeout.OnTimeoutStrategy.DISCARD:
166
+ # Trash everything
167
+ output = b''
168
+ elif data_strategy == Timeout.OnTimeoutStrategy.RETURN:
169
+ # Return the data that has been read up to this point
170
+ output += deferred_buffer
171
+ if fragment is not None:
172
+ output += fragment
173
+ elif data_strategy == Timeout.OnTimeoutStrategy.STORE:
174
+ # Store the data
175
+ self._previous_read_buffer = output
176
+ output = b''
177
+ elif data_strategy == Timeout.OnTimeoutStrategy.ERROR:
178
+ raise TimeoutException(origin)
179
+ break
180
+
181
+
182
+ # Add the deferred buffer
183
+ if len(deferred_buffer) > 0:
184
+ fragment = deferred_buffer + fragment
185
+
186
+ # 2) Evaluate the stop condition
187
+ if stop_condition is not None:
188
+ stop, kept_fragment, deferred_buffer = stop_condition.evaluate(fragment)
189
+ output += kept_fragment
190
+ if stop:
191
+ self._previous_read_buffer = deferred_buffer
192
+ else:
193
+ output += fragment
194
+ if stop:
195
+ break
196
+ if self._logger:
197
+ self._logger.log_read(output, self._previous_read_buffer)
198
+
199
+ return output
200
+
201
+ def query(self, data : Union[bytes, str], timeout=None, stop_condition=None) -> bytes:
67
202
  """
68
203
  Shortcut function that combines
69
204
  - flush_read
70
205
  - write
71
206
  - read
72
207
  """
73
- pass
208
+ pass
209
+
210
+ def __del__(self):
211
+ self.close()