syndesi 0.1.5__tar.gz → 0.2.0__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 (61) hide show
  1. {syndesi-0.1.5/syndesi.egg-info → syndesi-0.2.0}/PKG-INFO +17 -15
  2. {syndesi-0.1.5 → syndesi-0.2.0}/README.md +16 -14
  3. {syndesi-0.1.5 → syndesi-0.2.0}/setup.py +6 -2
  4. syndesi-0.2.0/syndesi/adapters/__init__.py +8 -0
  5. syndesi-0.2.0/syndesi/adapters/adapter.py +321 -0
  6. syndesi-0.2.0/syndesi/adapters/auto.py +45 -0
  7. syndesi-0.2.0/syndesi/adapters/ip.py +150 -0
  8. syndesi-0.2.0/syndesi/adapters/ip_server.py +108 -0
  9. syndesi-0.2.0/syndesi/adapters/proxy.py +92 -0
  10. syndesi-0.2.0/syndesi/adapters/serialport.py +152 -0
  11. syndesi-0.2.0/syndesi/adapters/stop_conditions.py +161 -0
  12. syndesi-0.2.0/syndesi/adapters/timed_queue.py +32 -0
  13. syndesi-0.2.0/syndesi/adapters/timeout.py +288 -0
  14. {syndesi-0.1.5 → syndesi-0.2.0}/syndesi/adapters/visa.py +10 -8
  15. syndesi-0.2.0/syndesi/protocols/__init__.py +5 -0
  16. {syndesi-0.1.5 → syndesi-0.2.0}/syndesi/protocols/delimited.py +37 -12
  17. syndesi-0.2.0/syndesi/protocols/protocol.py +17 -0
  18. syndesi-0.2.0/syndesi/protocols/raw.py +28 -0
  19. syndesi-0.2.0/syndesi/protocols/scpi.py +83 -0
  20. syndesi-0.2.0/syndesi/protocols/sdp.py +14 -0
  21. {syndesi-0.1.5/syndesi → syndesi-0.2.0/syndesi/tools}/__init__.py +0 -0
  22. syndesi-0.2.0/syndesi/tools/log.py +107 -0
  23. syndesi-0.2.0/syndesi/tools/others.py +1 -0
  24. syndesi-0.2.0/syndesi/tools/shell.py +111 -0
  25. {syndesi-0.1.5 → syndesi-0.2.0}/syndesi/tools/types.py +24 -18
  26. {syndesi-0.1.5 → syndesi-0.2.0/syndesi.egg-info}/PKG-INFO +17 -15
  27. syndesi-0.2.0/syndesi.egg-info/SOURCES.txt +32 -0
  28. syndesi-0.2.0/syndesi.egg-info/entry_points.txt +3 -0
  29. syndesi-0.2.0/syndesi.egg-info/top_level.txt +1 -0
  30. syndesi-0.1.5/bin/syndesi +0 -1
  31. syndesi-0.1.5/syndesi/adapters/__init__.py +0 -4
  32. syndesi-0.1.5/syndesi/adapters/iadapter.py +0 -73
  33. syndesi-0.1.5/syndesi/adapters/ip.py +0 -84
  34. syndesi-0.1.5/syndesi/adapters/serial.py +0 -37
  35. syndesi-0.1.5/syndesi/descriptors/Serial.py +0 -10
  36. syndesi-0.1.5/syndesi/descriptors/__init__.py +0 -1
  37. syndesi-0.1.5/syndesi/descriptors/descriptor.py +0 -9
  38. syndesi-0.1.5/syndesi/descriptors/ip.py +0 -9
  39. syndesi-0.1.5/syndesi/descriptors/syndesi/Syndesi.py +0 -9
  40. syndesi-0.1.5/syndesi/descriptors/syndesi/__init__.py +0 -0
  41. syndesi-0.1.5/syndesi/descriptors/syndesi/_device.py +0 -25
  42. syndesi-0.1.5/syndesi/descriptors/syndesi/devices.py +0 -10
  43. syndesi-0.1.5/syndesi/descriptors/syndesi/frame.py +0 -133
  44. syndesi-0.1.5/syndesi/descriptors/syndesi/network.py +0 -41
  45. syndesi-0.1.5/syndesi/descriptors/syndesi/payload.py +0 -11
  46. syndesi-0.1.5/syndesi/descriptors/syndesi/sdid.py +0 -21
  47. syndesi-0.1.5/syndesi/descriptors/visa.py +0 -31
  48. syndesi-0.1.5/syndesi/protocols/__init__.py +0 -5
  49. syndesi-0.1.5/syndesi/protocols/iprotocol.py +0 -14
  50. syndesi-0.1.5/syndesi/protocols/raw.py +0 -79
  51. syndesi-0.1.5/syndesi/protocols/scpi.py +0 -62
  52. syndesi-0.1.5/syndesi/protocols/sdp.py +0 -14
  53. syndesi-0.1.5/syndesi/tools/__init__.py +0 -0
  54. syndesi-0.1.5/syndesi/tools/stop_conditions.py +0 -148
  55. syndesi-0.1.5/syndesi.egg-info/SOURCES.txt +0 -38
  56. syndesi-0.1.5/syndesi.egg-info/top_level.txt +0 -2
  57. {syndesi-0.1.5 → syndesi-0.2.0}/LICENSE +0 -0
  58. {syndesi-0.1.5 → syndesi-0.2.0}/setup.cfg +0 -0
  59. {syndesi-0.1.5/experiments → syndesi-0.2.0/syndesi}/__init__.py +0 -0
  60. {syndesi-0.1.5 → syndesi-0.2.0}/syndesi/tools/exceptions.py +0 -0
  61. {syndesi-0.1.5 → syndesi-0.2.0}/syndesi.egg-info/dependency_links.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: syndesi
3
- Version: 0.1.5
3
+ Version: 0.2.0
4
4
  Summary: Syndesi
5
5
  Author: Sebastien Deriaz
6
6
  Author-email: sebastien.deriaz1@gmail.com
@@ -16,6 +16,8 @@ License-File: LICENSE
16
16
 
17
17
  # Syndesi Python Implementation
18
18
 
19
+ Syndesi description is available [here](https://github.com/syndesi-project/Syndesi/README.md)
20
+
19
21
  ## Installation
20
22
 
21
23
  The syndesi Python package can be installed through pip
@@ -25,14 +27,16 @@ The syndesi Python package can be installed through pip
25
27
  The package can also be installed locally by cloning this repository
26
28
 
27
29
  ```bash
28
- git clone ...
30
+ git clone https://github.com/syndesi-project/Syndesi
29
31
  cd Syndesi/Python
30
32
  pip install .
31
33
  ```
32
34
 
33
35
  ## Usage
34
36
 
35
- To instantiate a device / testbench, one must import the device and a suitable adapter
37
+
38
+
39
+ To instantiate a device, one must import the device and a suitable adapter
36
40
 
37
41
  ```python
38
42
  # 1) Import the device
@@ -46,16 +50,6 @@ mm = SDM3055(IP("192.168.1.123"))
46
50
  ## 4) Use
47
51
  voltage = mm.measure_dc_voltage()
48
52
  ```
49
-
50
- The Syndesi Python package provides the user with the necessary tools to control compatible devices
51
-
52
- - drivers : device-specific implementation
53
- - descriptors : Each class represents a particular way of connecting to a device, the user must provide que necessary information (IP, com port, ID, etc...)
54
- - communication wrapper (wrappers) : Wrappers for low-level communication (TCP, UDP, UART, etc...)
55
- - IP (TCP / UDP)
56
- - UART
57
- - USB (?)
58
-
59
53
  ## Layers
60
54
 
61
55
  The first layer is the "Device" base class
@@ -88,6 +82,14 @@ The Syndesi Device Protocol is a light-weight and easy interface to send / recei
88
82
 
89
83
  ## Notes
90
84
 
91
- 15.08.2023 : The adapters must work with bytearray data only
92
-
93
85
  06.09.2023 : bytearray is changed to bytes everywhere
86
+
87
+ 23.10.2023 : continuation timeout isn't suitable for TCP, but it can work for UDP as a UDP server can send multiple response packets after a single packet from the client. This can be handled in different ways by firewalls. Thankfull that's none of our business so continuation timeout can be implemented
88
+
89
+ 22.11.2023 : The timeout and stop conditions strategy is a bit complicated :
90
+
91
+ - What if we receive the message b'ACK\nNCK\n' using a termination stop condition but we receive b'ACK', then a timeout, then b'\nNCK\n' ?
92
+ - Should the first part be kept ? should an error be raised at the timeout because nothing was read ?
93
+ - Two kinds of timeouts ?
94
+ - One where "we read as much as possible during the available time"
95
+ - One where "we expect a response within X otherwise it's trash"
@@ -1,5 +1,7 @@
1
1
  # Syndesi Python Implementation
2
2
 
3
+ Syndesi description is available [here](https://github.com/syndesi-project/Syndesi/README.md)
4
+
3
5
  ## Installation
4
6
 
5
7
  The syndesi Python package can be installed through pip
@@ -9,14 +11,16 @@ The syndesi Python package can be installed through pip
9
11
  The package can also be installed locally by cloning this repository
10
12
 
11
13
  ```bash
12
- git clone ...
14
+ git clone https://github.com/syndesi-project/Syndesi
13
15
  cd Syndesi/Python
14
16
  pip install .
15
17
  ```
16
18
 
17
19
  ## Usage
18
20
 
19
- To instantiate a device / testbench, one must import the device and a suitable adapter
21
+
22
+
23
+ To instantiate a device, one must import the device and a suitable adapter
20
24
 
21
25
  ```python
22
26
  # 1) Import the device
@@ -30,16 +34,6 @@ mm = SDM3055(IP("192.168.1.123"))
30
34
  ## 4) Use
31
35
  voltage = mm.measure_dc_voltage()
32
36
  ```
33
-
34
- The Syndesi Python package provides the user with the necessary tools to control compatible devices
35
-
36
- - drivers : device-specific implementation
37
- - descriptors : Each class represents a particular way of connecting to a device, the user must provide que necessary information (IP, com port, ID, etc...)
38
- - communication wrapper (wrappers) : Wrappers for low-level communication (TCP, UDP, UART, etc...)
39
- - IP (TCP / UDP)
40
- - UART
41
- - USB (?)
42
-
43
37
  ## Layers
44
38
 
45
39
  The first layer is the "Device" base class
@@ -72,6 +66,14 @@ The Syndesi Device Protocol is a light-weight and easy interface to send / recei
72
66
 
73
67
  ## Notes
74
68
 
75
- 15.08.2023 : The adapters must work with bytearray data only
76
-
77
69
  06.09.2023 : bytearray is changed to bytes everywhere
70
+
71
+ 23.10.2023 : continuation timeout isn't suitable for TCP, but it can work for UDP as a UDP server can send multiple response packets after a single packet from the client. This can be handled in different ways by firewalls. Thankfull that's none of our business so continuation timeout can be implemented
72
+
73
+ 22.11.2023 : The timeout and stop conditions strategy is a bit complicated :
74
+
75
+ - What if we receive the message b'ACK\nNCK\n' using a termination stop condition but we receive b'ACK', then a timeout, then b'\nNCK\n' ?
76
+ - Should the first part be kept ? should an error be raised at the timeout because nothing was read ?
77
+ - Two kinds of timeouts ?
78
+ - One where "we read as much as possible during the available time"
79
+ - One where "we expect a response within X otherwise it's trash"
@@ -1,6 +1,6 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
- VERSION = '0.1.5'
3
+ VERSION = '0.2.0'
4
4
  DESCRIPTION = 'Syndesi'
5
5
 
6
6
  with open("README.md", "r", encoding="utf-8") as fh:
@@ -15,7 +15,11 @@ setup(
15
15
  description=DESCRIPTION,
16
16
  long_description_content_type="text/markdown",
17
17
  long_description=long_description,
18
- scripts=['bin/syndesi'],
18
+ entry_points = {
19
+ 'console_scripts': [
20
+ 'syndesi=syndesi.shell.syndesi:main',
21
+ 'syndesi-proxy=syndesi.proxy.proxy:main'],
22
+ },
19
23
  packages=find_packages(),
20
24
  install_requires=[''],
21
25
  keywords=['python', 'syndesi', 'interface', 'ethernet'],
@@ -0,0 +1,8 @@
1
+ from .adapter import Adapter
2
+ from .ip import IP
3
+ from .serialport import SerialPort
4
+ from .visa import VISA
5
+ from .proxy import Proxy
6
+
7
+ from .timeout import Timeout
8
+ from .stop_conditions import Termination, Length, StopCondition
@@ -0,0 +1,321 @@
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
33
+
34
+ DEFAULT_TIMEOUT = Timeout(response=1, continuation=100e-3, total=None)
35
+ DEFAULT_STOP_CONDITION = None
36
+
37
+
38
+ class AdapterDisconnected(Exception):
39
+ pass
40
+
41
+ STOP_DESIGNATORS = {
42
+ 'timeout' : {
43
+ Timeout.TimeoutType.RESPONSE : 'TR',
44
+ Timeout.TimeoutType.CONTINUATION : 'TC',
45
+ Timeout.TimeoutType.TOTAL : 'TT'
46
+ },
47
+ 'stop_condition' : {
48
+ Termination : 'ST',
49
+ Length : 'SL'
50
+ },
51
+ 'previous-read-buffer' : 'RB'
52
+ }
53
+
54
+ class Origin(Enum):
55
+ TIMEOUT = 'timeout'
56
+ STOP_CONDITION = 'stop_condition'
57
+
58
+ @dataclass
59
+ class ReturnMetrics:
60
+ read_duration : float
61
+ origin : Origin
62
+ timeout_type : Timeout.TimeoutType
63
+ stop_condition : StopCondition
64
+ previous_read_buffer_used : bool
65
+ n_fragments : int
66
+ response_time : float
67
+ continuation_times : list
68
+ total_time : float
69
+
70
+ class Adapter(ABC):
71
+ class Status(Enum):
72
+ DISCONNECTED = 0
73
+ CONNECTED = 1
74
+
75
+ def __init__(self, alias : str = '', stop_condition : Union[StopCondition, None] = DEFAULT, timeout : Union[float, Timeout] = DEFAULT) -> None:
76
+ """
77
+ Adapter instance
78
+
79
+ Parameters
80
+ ----------
81
+ alias : str
82
+ The alias is used to identify the class in the logs
83
+ timeout : float or Timeout instance
84
+ Default timeout is Timeout(response=1, continuation=0.1, total=None)
85
+ stop_condition : StopCondition or None
86
+ Default to None
87
+ """
88
+ super().__init__()
89
+ self._alias = alias
90
+
91
+ self._default_stop_condition = stop_condition == DEFAULT
92
+ if self._default_stop_condition:
93
+ self._stop_condition = DEFAULT_STOP_CONDITION
94
+ else:
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
+ self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
100
+
101
+ # Buffer for data that has been pulled from the queue but
102
+ # not used because of termination or length stop condition
103
+ self._previous_read_buffer = b''
104
+
105
+ self._default_timeout = timeout == DEFAULT
106
+ if self._default_timeout:
107
+ self._timeout = DEFAULT_TIMEOUT
108
+ else:
109
+ if is_number(timeout):
110
+ self._timeout = Timeout(response=timeout, continuation=100e-3)
111
+ elif isinstance(timeout, Timeout):
112
+ self._timeout = timeout
113
+ else:
114
+ raise ValueError(f"Invalid timeout type : {type(timeout)}")
115
+
116
+ def set_default_timeout(self, default_timeout : Union[Timeout, tuple, float]):
117
+ """
118
+ Set the default timeout for this adapter. If a previous timeout has been set, it will be fused
119
+
120
+ Parameters
121
+ ----------
122
+ default_timeout : Timeout or tuple or float
123
+ """
124
+ if self._default_timeout:
125
+ self._timeout = default_timeout
126
+ else:
127
+ self._timeout = timeout_fuse(self._timeout, default_timeout)
128
+
129
+ def set_default_stop_condition(self, stop_condition):
130
+ """
131
+ Set the default stop condition for this adapter.
132
+
133
+ Parameters
134
+ ----------
135
+ stop_condition : StopCondition
136
+ """
137
+ if self._default_stop_condition:
138
+ self._stop_condition = stop_condition
139
+
140
+ def flushRead(self):
141
+ """
142
+ Flush the input buffer
143
+ """
144
+ self._read_queue.clear()
145
+ self._previous_read_buffer = b''
146
+
147
+ @abstractmethod
148
+ def open(self):
149
+ """
150
+ Start communication with the device
151
+ """
152
+ pass
153
+
154
+ @abstractmethod
155
+ def close(self):
156
+ """
157
+ Stop communication with the device
158
+ """
159
+ pass
160
+
161
+ @abstractmethod
162
+ def write(self, data : Union[bytes, str]):
163
+ """
164
+ Send data to the device
165
+
166
+ Parameters
167
+ ----------
168
+ data : bytes or str
169
+ """
170
+ pass
171
+
172
+ # TODO : Return None or b'' when read thread is killed while reading
173
+ # This is to detect if a server socket has been closed
174
+
175
+
176
+ def read(self, timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
177
+ """
178
+ Read data from the device
179
+
180
+ Parameters
181
+ ----------
182
+ timeout : Timeout or None
183
+ Set a custom timeout, if None (default), the adapter timeout is used
184
+ stop_condition : StopCondition or None
185
+ Set a custom stop condition, if None (Default), the adapater stop condition is used
186
+ return_metrics : ReturnMetrics class
187
+ """
188
+ read_start = time()
189
+ if self._status == self.Status.DISCONNECTED:
190
+ self.open()
191
+
192
+ # Use adapter values if no custom value is specified
193
+ if timeout is None:
194
+ timeout = self._timeout
195
+ elif isinstance(timeout, float):
196
+ timeout = Timeout(timeout)
197
+
198
+
199
+ if stop_condition is None:
200
+ stop_condition = self._stop_condition
201
+
202
+ # If the adapter is closed, open it
203
+ if self._status == self.Status.DISCONNECTED:
204
+ self.open()
205
+
206
+ if self._thread is None or not self._thread.is_alive():
207
+ self._start_thread()
208
+
209
+ timeout_ms = timeout.initiate_read(len(self._previous_read_buffer) > 0)
210
+
211
+ if stop_condition is not None:
212
+ stop_condition.initiate_read()
213
+
214
+ deferred_buffer = b''
215
+
216
+ # Start with the deferred buffer
217
+ # TODO : Check if data could be lost here, like the data is put in the previous_read_buffer and is never
218
+ # read back again because there's no stop condition
219
+ if len(self._previous_read_buffer) > 0 and stop_condition is not None:
220
+ stop, output, self._previous_read_buffer = stop_condition.evaluate(self._previous_read_buffer)
221
+ previous_read_buffer_used = True
222
+ else:
223
+ stop = False
224
+ output = b''
225
+ previous_read_buffer_used = False
226
+
227
+ n_fragments = 0
228
+ # If everything is used up, read the queue
229
+ if not stop:
230
+ while True:
231
+ (timestamp, fragment) = self._read_queue.get(timeout_ms)
232
+ n_fragments += 1
233
+
234
+ if fragment == b'':
235
+ raise AdapterDisconnected()
236
+
237
+ # 1) Evaluate the timeout
238
+ stop, timeout_ms = timeout.evaluate(timestamp)
239
+ if stop:
240
+ data_strategy, origin = timeout.dataStrategy()
241
+ if data_strategy == Timeout.OnTimeoutStrategy.DISCARD:
242
+ # Trash everything
243
+ output = b''
244
+ elif data_strategy == Timeout.OnTimeoutStrategy.RETURN:
245
+ # Return the data that has been read up to this point
246
+ output += deferred_buffer
247
+ if fragment is not None:
248
+ output += fragment
249
+ elif data_strategy == Timeout.OnTimeoutStrategy.STORE:
250
+ # Store the data
251
+ self._previous_read_buffer = output
252
+ output = b''
253
+ elif data_strategy == Timeout.OnTimeoutStrategy.ERROR:
254
+ raise TimeoutException(origin)
255
+ break
256
+ else:
257
+ origin = None
258
+
259
+
260
+
261
+ # Add the deferred buffer
262
+ if len(deferred_buffer) > 0:
263
+ fragment = deferred_buffer + fragment
264
+
265
+ # 2) Evaluate the stop condition
266
+ if stop_condition is not None:
267
+ stop, kept_fragment, deferred_buffer = stop_condition.evaluate(fragment)
268
+ output += kept_fragment
269
+ if stop:
270
+ self._previous_read_buffer = deferred_buffer
271
+ else:
272
+ output += fragment
273
+ if stop:
274
+ break
275
+
276
+ if origin is not None:
277
+ # The stop originates from the timeout
278
+ designator = STOP_DESIGNATORS['timeout'][origin]
279
+ else:
280
+ designator = STOP_DESIGNATORS['stop_condition'][type(stop_condition)]
281
+ else:
282
+ designator = STOP_DESIGNATORS['previous-read-buffer']
283
+
284
+ read_duration = time() - read_start
285
+ if self._previous_read_buffer:
286
+ self._logger.debug(f'Read [{designator}, {read_duration*1e3:.3f}ms] : {output} , previous read buffer : {self._previous_read_buffer}')
287
+ else:
288
+ self._logger.debug(f'Read [{designator}, {read_duration*1e3:.3f}ms] : {output}')
289
+
290
+ if return_metrics:
291
+ return output, ReturnMetrics(
292
+ read_duration=read_duration,
293
+ origin=Origin.TIMEOUT if origin is not None else Origin.STOP_CONDITION,
294
+ timeout_type=origin if origin is not None else None,
295
+ stop_condition=type(stop_condition) if origin is None else None,
296
+ previous_read_buffer_used=previous_read_buffer_used,
297
+ n_fragments=n_fragments,
298
+ response_time=timeout.response_time,
299
+ continuation_times=timeout.continuation_times,
300
+ total_time=timeout.total_time
301
+ )
302
+ else:
303
+ return output
304
+
305
+ @abstractmethod
306
+ def _start_thread(self):
307
+ pass
308
+
309
+ def __del__(self):
310
+ self.close()
311
+
312
+ @abstractmethod
313
+ def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
314
+ """
315
+ Shortcut function that combines
316
+ - flush_read
317
+ - write
318
+ - read
319
+ """
320
+ pass
321
+
@@ -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}")
@@ -0,0 +1,150 @@
1
+ import socket
2
+ from enum import Enum
3
+ from .adapter import Adapter
4
+ from ..tools.types import to_bytes
5
+ from .timeout import Timeout
6
+ from threading import Thread
7
+ from .timed_queue import TimedQueue
8
+ from typing import Union
9
+ from time import time
10
+ import argparse
11
+ from ..tools import shell
12
+
13
+ class IP(Adapter):
14
+ DEFAULT_RESPONSE_TIMEOUT = 1
15
+ DEFAULT_CONTINUATION_TIMEOUT = 1e-3
16
+ DEFAULT_TOTAL_TIMEOUT = 5
17
+
18
+
19
+ DEFAULT_TIMEOUT = Timeout(
20
+ response=DEFAULT_RESPONSE_TIMEOUT,
21
+ continuation=DEFAULT_CONTINUATION_TIMEOUT,
22
+ total=DEFAULT_TOTAL_TIMEOUT)
23
+ DEFAULT_BUFFER_SIZE = 1024
24
+ class Protocol(Enum):
25
+ TCP = 'TCP'
26
+ UDP = 'UDP'
27
+
28
+ def __init__(self,
29
+ address : str,
30
+ port : int = None,
31
+ transport : str = 'TCP',
32
+ timeout : Union[Timeout, float] = DEFAULT_TIMEOUT,
33
+ stop_condition = None,
34
+ alias : str = '',
35
+ buffer_size : int = DEFAULT_BUFFER_SIZE,
36
+ _socket : socket.socket = None):
37
+ """
38
+ IP stack adapter
39
+
40
+ Parameters
41
+ ----------
42
+ address : str
43
+ IP description
44
+ port : int
45
+ IP port
46
+ transport : str
47
+ 'TCP' or 'UDP'
48
+ timeout : Timeout | float
49
+ Specify communication timeout
50
+ stop_condition : StopCondition
51
+ Specify a read stop condition (None by default)
52
+ alias : str
53
+ Specify an alias for this adapter, '' by default
54
+ buffer_size : int
55
+ Socket buffer size, may be removed in the future
56
+ socket : socket.socket
57
+ Specify a custom socket, this is reserved for server application
58
+ """
59
+ super().__init__(alias=alias, timeout=timeout, stop_condition=stop_condition)
60
+ self._transport = self.Protocol(transport)
61
+ self._is_server = _socket is not None
62
+
63
+ self._logger.info(f"Setting up {self._transport.value} IP adapter ({'server' if self._is_server else 'client'})")
64
+
65
+ if self._is_server:
66
+ # Server
67
+ self._socket = _socket
68
+ self._status = self.Status.CONNECTED
69
+ elif self._transport == self.Protocol.TCP:
70
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
71
+ elif self._transport == self.Protocol.UDP:
72
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
73
+
74
+ self._address = address
75
+ self._port = port
76
+ self._buffer_size = buffer_size
77
+
78
+ def set_default_port(self, port):
79
+ """
80
+ Sets IP port if no port has been set yet.
81
+
82
+ This way, the user can leave the port empty
83
+ and the driver/protocol can specify it later
84
+
85
+ Parameters
86
+ ----------
87
+ port : int
88
+ """
89
+ if self._port is None:
90
+ self._port = port
91
+
92
+ def open(self):
93
+ if self._is_server:
94
+ raise SystemError("Cannot open server socket. It must be passed already opened")
95
+ if self._port is None:
96
+ raise ValueError(f"Cannot open adapter without specifying a port")
97
+
98
+ self._logger.debug(f"Adapter {self._alias} connect to ({self._address}, {self._port})")
99
+ self._socket.connect((self._address, self._port))
100
+ self._status = self.Status.CONNECTED
101
+ self._logger.info(f"Adapter {self._alias} opened !")
102
+
103
+ def close(self):
104
+ if hasattr(self, '_socket'):
105
+ self._socket.close()
106
+ self._logger.info("Adapter closed !")
107
+ self._status = self.Status.DISCONNECTED
108
+
109
+ def write(self, data : Union[bytes, str]):
110
+ data = to_bytes(data)
111
+ if self._status == self.Status.DISCONNECTED:
112
+ self._logger.info(f"Adapter {self._alias} is closed, opening...")
113
+ self.open()
114
+ write_start = time()
115
+ self._socket.send(data)
116
+ write_duration = time() - write_start
117
+ self._logger.debug(f"Written [{write_duration*1e3:.3f}ms]: {repr(data)}")
118
+
119
+ def _start_thread(self):
120
+ self._logger.debug("Starting read thread...")
121
+ self._thread = Thread(target=self._read_thread, daemon=True, args=(self._socket, self._read_queue))
122
+ self._thread.start()
123
+
124
+ # EXPERIMENTAL
125
+ def read_thread_alive(self):
126
+ return self._thread.is_alive()
127
+
128
+
129
+ def _read_thread(self, socket : socket.socket, read_queue : TimedQueue):
130
+ while True: # TODO : Add stop_pipe ? Maybe it was removed ?
131
+ try:
132
+ payload = socket.recv(self._buffer_size)
133
+ if len(payload) == self._buffer_size and self._transport == self.Protocol.UDP:
134
+ self._logger.warning("Warning, inbound UDP data may have been lost (max buffer size attained)")
135
+ except OSError:
136
+ break
137
+ # If payload is empty, it means the socket has been disconnected
138
+ if payload == b'':
139
+ read_queue.put(payload)
140
+ break
141
+ read_queue.put(payload)
142
+
143
+ def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False):
144
+ if self._is_server:
145
+ raise SystemError("Cannot query on server adapters")
146
+ self.flushRead()
147
+ self.write(data)
148
+ return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
149
+
150
+