syndesi 0.1.4__py3-none-any.whl → 0.3.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.
Files changed (80) hide show
  1. syndesi/__init__.py +3 -0
  2. syndesi/__main__.py +4 -0
  3. syndesi/adapters/__init__.py +3 -3
  4. syndesi/adapters/adapter.py +528 -0
  5. syndesi/adapters/auto.py +49 -0
  6. syndesi/adapters/backend/adapter_backend.py +364 -0
  7. syndesi/adapters/backend/adapter_manager.py +49 -0
  8. syndesi/adapters/backend/adapter_session.py +363 -0
  9. syndesi/adapters/backend/backend.py +369 -0
  10. syndesi/{descriptors/syndesi/__init__.py → adapters/backend/backend_status.py} +0 -0
  11. syndesi/adapters/backend/backend_tools.py +55 -0
  12. syndesi/adapters/backend/descriptors.py +140 -0
  13. syndesi/adapters/backend/ip_backend.py +137 -0
  14. syndesi/adapters/backend/serialport_backend.py +232 -0
  15. syndesi/adapters/backend/stop_condition_backend.py +319 -0
  16. syndesi/adapters/backend/timed_queue.py +39 -0
  17. syndesi/adapters/backend/timeout.py +252 -0
  18. syndesi/adapters/backend/visa_backend.py +179 -0
  19. syndesi/adapters/ip.py +75 -65
  20. syndesi/adapters/ip_server.py +102 -0
  21. syndesi/adapters/serialport.py +74 -0
  22. syndesi/adapters/stop_condition.py +102 -0
  23. syndesi/adapters/timeout.py +99 -0
  24. syndesi/adapters/visa.py +38 -54
  25. {tests → syndesi/cli}/__init__.py +0 -0
  26. syndesi/cli/adapter_shell.py +210 -0
  27. syndesi/cli/backend_console.py +88 -0
  28. syndesi/cli/backend_status.py +212 -0
  29. syndesi/cli/backend_wrapper.py +50 -0
  30. syndesi/cli/shell.py +269 -0
  31. syndesi/protocols/__init__.py +8 -3
  32. syndesi/protocols/delimited.py +145 -45
  33. syndesi/protocols/modbus.py +1561 -0
  34. syndesi/protocols/protocol.py +71 -0
  35. syndesi/protocols/raw.py +45 -60
  36. syndesi/protocols/scpi.py +154 -33
  37. syndesi/scripts/__init__.py +0 -0
  38. syndesi/scripts/syndesi.py +52 -0
  39. syndesi/scripts/syndesi_backend.py +43 -0
  40. syndesi/tools/backend_api.py +190 -0
  41. syndesi/tools/backend_logger.py +53 -0
  42. syndesi/tools/errors.py +18 -0
  43. syndesi/tools/exceptions.py +7 -1
  44. syndesi/tools/internal.py +0 -0
  45. syndesi/tools/log.py +143 -0
  46. syndesi/tools/log_settings.py +16 -0
  47. syndesi/tools/types.py +87 -46
  48. syndesi/version.py +3 -0
  49. {syndesi-0.1.4.dist-info → syndesi-0.3.1.dist-info}/METADATA +24 -22
  50. syndesi-0.3.1.dist-info/RECORD +56 -0
  51. {syndesi-0.1.4.dist-info → syndesi-0.3.1.dist-info}/WHEEL +1 -1
  52. syndesi-0.3.1.dist-info/entry_points.txt +3 -0
  53. {syndesi-0.1.4.dist-info → syndesi-0.3.1.dist-info/licenses}/LICENSE +194 -194
  54. syndesi-0.3.1.dist-info/top_level.txt +1 -0
  55. syndesi/adapters/IP.py +0 -81
  56. syndesi/adapters/VISA.py +0 -50
  57. syndesi/adapters/iadapter.py +0 -73
  58. syndesi/adapters/serial.py +0 -37
  59. syndesi/descriptors/IP.py +0 -9
  60. syndesi/descriptors/Serial.py +0 -10
  61. syndesi/descriptors/VISA.py +0 -31
  62. syndesi/descriptors/__init__.py +0 -1
  63. syndesi/descriptors/descriptor.py +0 -9
  64. syndesi/descriptors/ip.py +0 -9
  65. syndesi/descriptors/syndesi/Syndesi.py +0 -9
  66. syndesi/descriptors/syndesi/_device.py +0 -25
  67. syndesi/descriptors/syndesi/devices.py +0 -10
  68. syndesi/descriptors/syndesi/frame.py +0 -133
  69. syndesi/descriptors/syndesi/network.py +0 -41
  70. syndesi/descriptors/syndesi/payload.py +0 -11
  71. syndesi/descriptors/syndesi/sdid.py +0 -21
  72. syndesi/descriptors/visa.py +0 -31
  73. syndesi/protocols/commands.py +0 -56
  74. syndesi/protocols/iprotocol.py +0 -14
  75. syndesi/protocols/sdp.py +0 -14
  76. syndesi/tools/stop_conditions.py +0 -148
  77. syndesi-0.1.4.data/scripts/syndesi +0 -1
  78. syndesi-0.1.4.dist-info/RECORD +0 -42
  79. syndesi-0.1.4.dist-info/top_level.txt +0 -2
  80. {experiments → syndesi/adapters/backend}/__init__.py +0 -0
@@ -0,0 +1,364 @@
1
+ # File : adapterbackend.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+ #
5
+ # The adapter backend is the background class that manages communication
6
+ # with one particular device. It is always instanciated in a backend client
7
+
8
+ import logging
9
+ import socket
10
+ import uuid
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import Generator
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+ from multiprocessing.connection import Connection
16
+ from threading import Thread
17
+ from time import time
18
+ from typing import Union, Optional, List, Protocol
19
+
20
+ from ...tools.backend_api import AdapterBackendStatus, Fragment
21
+ from ...tools.log_settings import LoggerAlias
22
+ from .descriptors import Descriptor
23
+ from .stop_condition_backend import (
24
+ LengthBackend,
25
+ StopConditionBackend,
26
+ StopConditionType,
27
+ TerminationBackend,
28
+ TimeoutStopConditionBackend
29
+ )
30
+
31
+ class HasFileno(Protocol):
32
+ def fileno(self) -> int:
33
+ return -1
34
+
35
+ Selectable = Union[HasFileno, int]
36
+
37
+ DEFAULT_STOP_CONDITION = None
38
+
39
+
40
+ class SocketReadException(Exception):
41
+ pass
42
+
43
+
44
+ STOP_DESIGNATORS = {
45
+ # "timeout": {
46
+ # TimeoutType.RESPONSE: "TR",
47
+ # TimeoutType.CONTINUATION: "TC",
48
+ # TimeoutType.TOTAL: "TT",
49
+ # },
50
+ "stop_condition": {TerminationBackend: "ST", LengthBackend: "SL"},
51
+ "previous-buffer": "PB",
52
+ }
53
+
54
+
55
+ class Origin(Enum):
56
+ TIMEOUT = "timeout"
57
+ STOP_CONDITION = "stop_condition"
58
+
59
+
60
+ class AdapterSignal:
61
+ pass
62
+
63
+
64
+ class AdapterDisconnected(AdapterSignal):
65
+ def __str__(self) -> str:
66
+ return "Adapter disconnected"
67
+
68
+ def __repr__(self) -> str:
69
+ return self.__str__()
70
+
71
+
72
+ @dataclass
73
+ class AdapterReadInit(AdapterSignal):
74
+ received_response_in_time: bool
75
+ uuid: uuid.UUID
76
+
77
+ def __str__(self) -> str:
78
+ return f"Read init {str(self.uuid)[:5]}... {'in time' if self.received_response_in_time else 'not in time'}"
79
+
80
+ def __repr__(self) -> str:
81
+ return self.__str__()
82
+
83
+
84
+ @dataclass
85
+ class AdapterReadPayload(AdapterSignal):
86
+ fragments: list[Fragment]
87
+ stop_timestamp: float
88
+ stop_condition_type: StopConditionType
89
+ previous_read_buffer_used: bool
90
+ # Only used by client
91
+ response_time: float | None = None
92
+
93
+ def data(self) -> bytes:
94
+ return b"".join([f.data for f in self.fragments])
95
+
96
+ def __str__(self) -> str:
97
+ return f"Read payload : {self.data()!r}"
98
+
99
+ def __repr__(self) -> str:
100
+ return self.__str__()
101
+
102
+ # This class holds a request made by the client to know if a response
103
+ # is received without the specified time frame
104
+ @dataclass
105
+ class ResponseRequest:
106
+ timestamp : float
107
+ uuid : uuid.UUID
108
+
109
+ class AdapterBackend(ABC):
110
+ class ThreadCommands(Enum):
111
+ STOP = b"0"
112
+
113
+ class AdapterTimeoutEventOrigin(Enum):
114
+ TIMEOUT = 0
115
+ RESPONSE_READ_INIT = 1
116
+
117
+ def __init__(self, descriptor: Descriptor) -> None:
118
+ """
119
+ Adapter instance
120
+
121
+ Parameters
122
+ ----------
123
+ timeout : float or Timeout instance
124
+ Default timeout is Timeout(response=5, continuation=0.2, total=None)
125
+ stop_condition : StopCondition or None
126
+ Default to None
127
+ """
128
+ self._logger = logging.getLogger(LoggerAlias.ADAPTER_BACKEND.value)
129
+
130
+ super().__init__()
131
+
132
+ # TODO : Switch to multiple stop conditios
133
+ self._stop_condition: StopConditionBackend = TimeoutStopConditionBackend(continuation=0.1, total=None)
134
+ self.descriptor = descriptor
135
+ self._thread: Thread | None = None
136
+ self._status = AdapterBackendStatus.DISCONNECTED
137
+ self._thread_commands_read, self._thread_commands_write = socket.socketpair()
138
+ self.backend_signal: Optional[Connection] = None
139
+ self.fragments : List[Fragment] = []
140
+ self._start_read_timestamp : Optional[float] = None
141
+ # self._data_out_queue = []
142
+ self._next_timeout_timestamp : Optional[float] = None
143
+ # _response_time indicates if the frontend asked for a read
144
+ # None : No ask
145
+ # float : Ask for a response to happen at the specified value at max
146
+ self._response_request : Optional[ResponseRequest] = None
147
+
148
+ self._first_fragment = True
149
+
150
+ # Buffer for data that has been pulled from the queue but
151
+ # not used because of termination or length stop condition
152
+ self._previous_buffer = Fragment(b"", None)
153
+
154
+ self._last_write_time = time()
155
+
156
+ def set_stop_condition(self, stop_condition : StopConditionBackend) -> None:
157
+ """
158
+ Overwrite the stop-condition
159
+
160
+ Parameters
161
+ ----------
162
+ stop_condition : StopCondition
163
+ """
164
+ self._stop_condition = stop_condition
165
+
166
+ def flush_read(self) -> bool:
167
+ """
168
+ Flush the input buffer
169
+ """
170
+ self._logger.debug("Flush")
171
+ self._previous_buffer = Fragment(b"", None)
172
+ self._response_request = None
173
+ self.fragments = []
174
+ if self._stop_condition is not None:
175
+ self._stop_condition.flush_read()
176
+ return True
177
+
178
+ def previous_read_buffer_empty(self) -> bool:
179
+ """
180
+ Check whether the previous read buffer is empty
181
+
182
+ Returns
183
+ -------
184
+ empty : bool
185
+ """
186
+ return self._previous_buffer.data == b""
187
+
188
+ @abstractmethod
189
+ def open(self) -> bool:
190
+ """
191
+ Start communication with the device
192
+ """
193
+ pass
194
+
195
+ def close(self) -> bool:
196
+ """
197
+ Stop communication with the device
198
+ """
199
+ self._logger.debug("Closing adapter and stopping read thread")
200
+ # self._thread_commands_write.send(self.ThreadCommands.STOP.value)
201
+ # if self._thread is not None and self._thread.is_alive():
202
+ # try:
203
+ # self._thread.join()
204
+ # except RuntimeError:
205
+ # # If the thread cannot be joined, then so be it
206
+ # pass
207
+ self._status = AdapterBackendStatus.DISCONNECTED
208
+ return True
209
+
210
+ def write(self, data: bytes) -> bool:
211
+ """
212
+ Send data to the device
213
+
214
+ Parameters
215
+ ----------
216
+ data : bytes or str
217
+ """
218
+ self._last_write_time = time()
219
+ self._logger.debug(f"Write {repr(data)}")
220
+ return True
221
+
222
+ @abstractmethod
223
+ def selectable(self) -> Optional[HasFileno]:
224
+ """
225
+ Return an object with a fileno() method (e.g., socket, Connection) suitable for use with select/poll.
226
+ """
227
+ raise NotImplementedError
228
+
229
+ @abstractmethod
230
+ def _socket_read(self) -> Fragment:
231
+ raise NotImplementedError
232
+
233
+ def on_socket_ready(self) -> Generator[AdapterSignal, None, None]:
234
+ fragment = self._socket_read()
235
+ if fragment.timestamp is not None and self._last_write_time is not None:
236
+ fragment_delta_t = fragment.timestamp - self._last_write_time
237
+ else:
238
+ fragment_delta_t = float("nan")
239
+ self._logger.debug(f"New fragment {fragment_delta_t:+.3f} {fragment}")
240
+ if fragment.data == b"":
241
+ self.close()
242
+ yield AdapterDisconnected()
243
+
244
+ else:
245
+ if self._status == AdapterBackendStatus.CONNECTED:
246
+ t = time()
247
+
248
+ while True:
249
+ if self._first_fragment:
250
+ self._logger.debug('First fragment')
251
+ self._read_start_time = t
252
+ self._stop_condition.initiate_read()
253
+ self._first_fragment = False
254
+ if self._response_request is not None:
255
+ received_response_in_time = t < self._response_request.timestamp
256
+ # The frontend asked for a response, tell it
257
+ yield AdapterReadInit(
258
+ received_response_in_time, self._response_request.uuid
259
+ )
260
+ self._response_request = None
261
+
262
+ stop, kept, self._previous_buffer, self._next_timeout_timestamp = (
263
+ self._stop_condition.evaluate(fragment, t)
264
+ )
265
+
266
+ self.fragments.append(kept)
267
+
268
+ if stop:
269
+ self._logger.debug('Stop')
270
+ self._first_fragment = True
271
+ yield AdapterReadPayload(
272
+ fragments=self.fragments,
273
+ stop_timestamp=t,
274
+ stop_condition_type=self._stop_condition._TYPE,
275
+ previous_read_buffer_used=False,
276
+ response_time=(
277
+ t - self._start_read_timestamp
278
+ if self._start_read_timestamp is not None
279
+ else None
280
+ ),
281
+ )
282
+ self.fragments.clear()
283
+
284
+ if len(self._previous_buffer.data) > 0 and stop:
285
+ # If there's a previous buffer, put it in the fragment and loop again
286
+ # Only loop if there's a stop (oterwise a stop would never happen again)
287
+ fragment = self._previous_buffer
288
+
289
+ else:
290
+ # If not, quit now
291
+ break
292
+
293
+ return None
294
+
295
+ def start_read(self, response_time: float, uuid: uuid.UUID) -> None:
296
+ """
297
+ Start a read operation. This is a signal from the frontend. The only goal is to set the response time
298
+ and tell the frontend if nothing arrives within a set time
299
+ """
300
+ t = time()
301
+ self._start_read_timestamp = t
302
+ self._logger.debug(f"Setup read {str(uuid)[:5]}... in {response_time:.3f} s")
303
+ self._response_request = ResponseRequest(t + response_time, uuid)
304
+
305
+ @abstractmethod
306
+ def is_opened(self) -> bool:
307
+ """
308
+ Return True if adapter is opened, False otherwise
309
+ """
310
+
311
+ def __str__(self) -> str:
312
+ return self.descriptor.__str__()
313
+
314
+ def __repr__(self) -> str:
315
+ return self.__str__()
316
+
317
+ def on_timeout_event(self) -> Optional[AdapterSignal]:
318
+ t = time()
319
+
320
+ if self._next_timeout_origin == self.AdapterTimeoutEventOrigin.TIMEOUT:
321
+ self._next_timeout_timestamp = None
322
+ self._first_fragment = True
323
+ output = AdapterReadPayload(
324
+ stop_timestamp=t,
325
+ stop_condition_type=self._stop_condition._TYPE,
326
+ previous_read_buffer_used=False,
327
+ fragments=self.fragments,
328
+ response_time=(
329
+ t - self._start_read_timestamp
330
+ if self._start_read_timestamp is not None
331
+ else None
332
+ ),
333
+ )
334
+ # Clear all of the fragments
335
+ self.fragments = []
336
+ return output
337
+
338
+ elif (
339
+ self._next_timeout_origin
340
+ == self.AdapterTimeoutEventOrigin.RESPONSE_READ_INIT
341
+ ):
342
+ if self._response_request is not None:
343
+ uuid = self._response_request.uuid
344
+ self._response_request = None
345
+ return AdapterReadInit(False, uuid)
346
+
347
+ return None
348
+
349
+ def get_next_timeout(self) -> Optional[float]:
350
+ min_timestamp = None
351
+ self._next_timeout_origin = None
352
+
353
+ if self._next_timeout_timestamp is not None:
354
+ min_timestamp = self._next_timeout_timestamp
355
+ self._next_timeout_origin = self.AdapterTimeoutEventOrigin.TIMEOUT
356
+
357
+ if self._response_request is not None:
358
+ if min_timestamp is None or self._response_request.timestamp < min_timestamp:
359
+ min_timestamp = self._response_request.timestamp
360
+ self._next_timeout_origin = (
361
+ self.AdapterTimeoutEventOrigin.RESPONSE_READ_INIT
362
+ )
363
+
364
+ return min_timestamp
@@ -0,0 +1,49 @@
1
+ # File : adapter_manager.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+ #
5
+ # The adapter manager instanciates adapters based on a given descriptor
6
+ # It is used by the backend to create adapters
7
+
8
+ # from .adapter_backend import AdapterBackend
9
+ # from .descriptors import Descriptor, IPDescriptor, SerialPortDescriptor, VisaDescriptor
10
+ # from .ip_backend import IPBackend
11
+ # from .serialport_backend import SerialPortBackend
12
+ # from .visa_backend import VisaBackend
13
+
14
+
15
+
16
+ # class AdapterManager:
17
+ # def __init__(self) -> None:
18
+ # self.adapters : Dict[str, AdapterBackend] = {}
19
+
20
+ # def get_adapter(self, descriptor: Descriptor) -> AdapterBackend:
21
+ # string_descriptor = str(descriptor)
22
+ # if string_descriptor not in self.adapters:
23
+ # # The adapter doesn't exist, create it
24
+ # if isinstance(
25
+ # descriptor, SerialPortDescriptor
26
+ # ): # Add mandatory timeout and stop_condition here ?
27
+ # self.adapters[string_descriptor] = SerialPortBackend(
28
+ # descriptor=SerialPortDescriptor(
29
+ # port=descriptor.port, baudrate=descriptor.baudrate
30
+ # )
31
+ # )
32
+ # elif isinstance(descriptor, IPDescriptor):
33
+ # self.adapters[string_descriptor] = IPBackend(descriptor=descriptor)
34
+ # elif isinstance(descriptor, VisaDescriptor):
35
+ # self.adapters[string_descriptor] = VisaBackend(descriptor=descriptor)
36
+ # else:
37
+ # raise ValueError(f"Unsupported descriptor : {descriptor}")
38
+
39
+ # return self.adapters[string_descriptor]
40
+
41
+ # def close_adapter(self, descriptor: Descriptor):
42
+ # string_decriptor = str(descriptor)
43
+ # if string_decriptor in self.adapters:
44
+ # adapter = self.adapters.pop(string_decriptor)
45
+ # adapter.close()
46
+
47
+
48
+ # # The singleton instance
49
+ # adapterManager = AdapterManager()