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
@@ -1,438 +0,0 @@
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 time
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 typing import Protocol
18
-
19
- from ...tools.backend_api import AdapterBackendStatus, Fragment
20
- from ...tools.log_settings import LoggerAlias
21
- from ..stop_condition import StopConditionType
22
- from .descriptors import Descriptor
23
- from .stop_condition_backend import (
24
- ContinuationBackend,
25
- StopConditionBackend,
26
- TotalBackend,
27
- )
28
-
29
-
30
- class HasFileno(Protocol):
31
- def fileno(self) -> int:
32
- return -1
33
-
34
-
35
- Selectable = 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
- # end_delay : float | None
75
-
76
- # def __str__(self) -> str:
77
- # return f"Read init (end delay : {self.end_delay})"
78
-
79
- # def __repr__(self) -> str:
80
- # return self.__str__()
81
-
82
-
83
- @dataclass
84
- class AdapterResponseTimeout(AdapterSignal):
85
- identifier: int
86
-
87
- def __str__(self) -> str:
88
- return "Response timeout"
89
-
90
- def __repr__(self) -> str:
91
- return self.__str__()
92
-
93
-
94
- @dataclass
95
- class AdapterReadPayload(AdapterSignal):
96
- fragments: list[Fragment]
97
- stop_timestamp: float
98
- stop_condition_type: StopConditionType
99
- previous_read_buffer_used: bool
100
- response_timestamp: float | None
101
- # Only used by client and set by frontend
102
- response_delay: float | None = None
103
-
104
- def data(self) -> bytes:
105
- return b"".join([f.data for f in self.fragments])
106
-
107
- def __str__(self) -> str:
108
- return f"Read payload : {self.data()!r}"
109
-
110
- def __repr__(self) -> str:
111
- return self.__str__()
112
-
113
-
114
- # This class holds a request made by the client to know if a response
115
- # is received without the specified time frame
116
- @dataclass
117
- class ResponseRequest:
118
- timestamp: float
119
- identifier: int
120
-
121
-
122
- def nmin(a: float | None, b: float | None) -> float | None:
123
- if a is None and b is None:
124
- return None
125
- elif a is None:
126
- return b
127
- elif b is None:
128
- return a
129
- else:
130
- return min(a, b)
131
-
132
-
133
- class AdapterBackend(ABC):
134
- class ThreadCommands(Enum):
135
- STOP = b"0"
136
-
137
- class AdapterTimeoutEventOrigin(Enum):
138
- TIMEOUT = 0
139
- RESPONSE_REQUEST = 1
140
-
141
- def __init__(self, descriptor: Descriptor) -> None:
142
- """
143
- Adapter instance
144
-
145
- Parameters
146
- ----------
147
- timeout : float or Timeout instance
148
- Default timeout is Timeout(response=5, continuation=0.2, total=None)
149
- stop_condition : StopCondition or None
150
- Default to None
151
- """
152
- self._logger = logging.getLogger(LoggerAlias.ADAPTER_BACKEND.value)
153
-
154
- super().__init__()
155
-
156
- # TODO : Switch to multiple stop conditios
157
- self._stop_conditions: list[StopConditionBackend] = [
158
- ContinuationBackend(time=0.1)
159
- ]
160
- self.descriptor = descriptor
161
- self._thread: Thread | None = None
162
- self._status = AdapterBackendStatus.DISCONNECTED
163
- self._thread_commands_read, self._thread_commands_write = socket.socketpair()
164
- self.backend_signal: Connection | None = None
165
- self.fragments: list[Fragment] = []
166
- self._next_timeout_timestamp: float | None = None
167
- # _response_time indicates if the frontend asked for a read
168
- # None : No ask
169
- # float : Ask for a response to happen at the specified value at max
170
- self._response_request: ResponseRequest | None = None
171
- self._response_request_start: float | None = None
172
-
173
- self._first_fragment = True
174
-
175
- # Buffer for data that has been pulled from the queue but
176
- # not used because of termination or length stop condition
177
- self._previous_buffer = Fragment(b"", None)
178
-
179
- self._last_write_time = time.time()
180
-
181
- def set_stop_conditions(self, stop_conditions: list[StopConditionBackend]) -> None:
182
- """
183
- Overwrite the stop-condition
184
-
185
- Parameters
186
- ----------
187
- stop_condition : StopCondition
188
- """
189
- self._stop_conditions = stop_conditions
190
-
191
- def flush_read(self) -> bool:
192
- """
193
- Flush the input buffer
194
- """
195
- self._logger.debug("Flush")
196
- self._previous_buffer = Fragment(b"", None)
197
- self._response_request = None
198
- self.fragments = []
199
- for stop_condition in self._stop_conditions:
200
- stop_condition.flush_read()
201
- return True
202
-
203
- def previous_read_buffer_empty(self) -> bool:
204
- """
205
- Check whether the previous read buffer is empty
206
-
207
- Returns
208
- -------
209
- empty : bool
210
- """
211
- return self._previous_buffer.data == b""
212
-
213
- @abstractmethod
214
- def open(self) -> bool:
215
- """
216
- Start communication with the device
217
- """
218
- pass
219
-
220
- def close(self) -> bool:
221
- """
222
- Stop communication with the device
223
- """
224
- self._status = AdapterBackendStatus.DISCONNECTED
225
- return True
226
-
227
- def write(self, data: bytes) -> bool:
228
- """
229
- Send data to the device
230
-
231
- Parameters
232
- ----------
233
- data : bytes or str
234
- """
235
- self._last_write_time = time.time()
236
- self._logger.debug(f"Write {repr(data)}")
237
- return True
238
-
239
- @abstractmethod
240
- def selectable(self) -> HasFileno | None:
241
- """
242
- Return an object with a fileno() method (e.g., socket, Connection) suitable for use with select/poll.
243
- """
244
- raise NotImplementedError
245
-
246
- @abstractmethod
247
- def _socket_read(self) -> Fragment:
248
- raise NotImplementedError
249
-
250
- def _fragments_to_string(self, fragments: list[Fragment]) -> str:
251
- if len(fragments) > 0:
252
- return "+".join(repr(f.data) for f in fragments)
253
- else:
254
- return str([])
255
-
256
- def on_socket_ready(self) -> Generator[AdapterSignal, None, None]:
257
- fragment = self._socket_read()
258
- if fragment.timestamp is not None and self._last_write_time is not None:
259
- fragment_delta_t = fragment.timestamp - self._last_write_time
260
- else:
261
- fragment_delta_t = float("nan")
262
- if fragment.data == b"":
263
- self.close()
264
- yield AdapterDisconnected()
265
- else:
266
- self._logger.debug(
267
- f"New fragment {fragment_delta_t:+.3f} {fragment}"
268
- + (" (first)" if self._first_fragment else "")
269
- )
270
- if self._status == AdapterBackendStatus.CONNECTED:
271
- t = time.time()
272
-
273
- # If there's a response request, disable it if there's a timeout stop condition
274
- # The stop-condition will do the job
275
-
276
- if self._response_request is not None:
277
- for stop_condition in self._stop_conditions:
278
- if isinstance(
279
- stop_condition, (ContinuationBackend, TotalBackend)
280
- ):
281
- self._response_request = None
282
- break
283
-
284
- while True:
285
- if self._first_fragment:
286
- self._first_fragment = False
287
- self._read_start_timestamp = t
288
- for stop_condition in self._stop_conditions:
289
- stop_condition.initiate_read()
290
-
291
- stop = False
292
- kept = fragment
293
-
294
- # Run each stop condition one after the other, if a stop is reached, stop evaluating
295
- stop_condition_type: StopConditionType
296
- for stop_condition in self._stop_conditions:
297
- (
298
- stop,
299
- kept,
300
- self._previous_buffer,
301
- self._next_timeout_timestamp,
302
- ) = stop_condition.evaluate(kept)
303
- if stop:
304
- stop_condition_type = stop_condition.type()
305
- break
306
-
307
- # if kept.data != b'':
308
- # self.fragments.append(kept)
309
-
310
- self.fragments.append(kept)
311
-
312
- if stop:
313
- self._first_fragment = True
314
- self._logger.debug(
315
- f"Payload {self._fragments_to_string(self.fragments)} ({stop_condition_type.value})"
316
- )
317
- if (
318
- self._response_request_start is None
319
- or len(self.fragments) == 0
320
- ):
321
- response_delay = None
322
- else:
323
- if self.fragments[0].timestamp is None:
324
- response_delay = None
325
- else:
326
- response_delay = (
327
- self.fragments[0].timestamp
328
- - self._response_request_start
329
- )
330
- self._response_request_start = None
331
- yield AdapterReadPayload(
332
- fragments=self.fragments,
333
- stop_timestamp=t,
334
- stop_condition_type=stop_condition_type,
335
- previous_read_buffer_used=False,
336
- response_timestamp=self.fragments[0].timestamp,
337
- response_delay=response_delay,
338
- )
339
- self._next_timeout_timestamp = None # Experiment !
340
- self.fragments.clear()
341
-
342
- if len(self._previous_buffer.data) > 0 and stop:
343
- # If there's a previous buffer, put it in the fragment and loop again
344
- # Only loop if there's a stop (oterwise a stop would never happen again)
345
- fragment = self._previous_buffer
346
-
347
- else:
348
- # If not, quit now
349
- break
350
-
351
- return None
352
-
353
- def start_read(self, response_time: float, identifier: int) -> None:
354
- """
355
- Start a read operation. This is a signal from the frontend. The only goal is to set the response time
356
- and tell the frontend if nothing arrives within a set time
357
- """
358
- self._response_request_start = time.time()
359
- self._logger.debug(f"Setup read [{identifier}] in {response_time:.3f} s")
360
- self._response_request = ResponseRequest(
361
- self._response_request_start + response_time, identifier
362
- )
363
-
364
- @abstractmethod
365
- def is_opened(self) -> bool:
366
- """
367
- Return True if adapter is opened, False otherwise
368
- """
369
-
370
- def __str__(self) -> str:
371
- return self.descriptor.__str__()
372
-
373
- def __repr__(self) -> str:
374
- return self.__str__()
375
-
376
- def on_timeout_event(self) -> AdapterSignal | None:
377
- t = time.time()
378
-
379
- if self._next_timeout_origin == self.AdapterTimeoutEventOrigin.TIMEOUT:
380
- self._next_timeout_timestamp = None
381
- self._first_fragment = True
382
- if self._response_request_start is None or len(self.fragments) == 0:
383
- response_delay = None
384
- else:
385
- if self.fragments[0].timestamp is not None:
386
- response_delay = (
387
- self.fragments[0].timestamp - self._response_request_start
388
- )
389
- else:
390
- response_delay = None
391
- self._response_request_start = None
392
-
393
- output = AdapterReadPayload(
394
- stop_timestamp=t,
395
- stop_condition_type=StopConditionType.TIMEOUT,
396
- previous_read_buffer_used=False,
397
- fragments=self.fragments,
398
- response_timestamp=(
399
- self.fragments[0].timestamp if len(self.fragments) > 0 else None
400
- ),
401
- response_delay=response_delay,
402
- )
403
- # Reset response request
404
- if self._response_request is not None:
405
- self._response_request = None
406
- self._response_request_start = None
407
- # Clear all of the fragments
408
- self.fragments = []
409
- return output
410
-
411
- elif (
412
- self._next_timeout_origin == self.AdapterTimeoutEventOrigin.RESPONSE_REQUEST
413
- ):
414
- if self._response_request is not None:
415
- signal = AdapterResponseTimeout(self._response_request.identifier)
416
- self._response_request = None
417
- return signal
418
-
419
- return None
420
-
421
- def get_next_timeout(self) -> float | None:
422
- min_timestamp = None
423
- self._next_timeout_origin = None
424
-
425
- if self._next_timeout_timestamp is not None:
426
- min_timestamp = self._next_timeout_timestamp
427
- self._next_timeout_origin = self.AdapterTimeoutEventOrigin.TIMEOUT
428
-
429
- if self._response_request is not None:
430
- if (
431
- min_timestamp is None
432
- or self._response_request.timestamp < min_timestamp
433
- ):
434
- min_timestamp = self._response_request.timestamp
435
- self._next_timeout_origin = (
436
- self.AdapterTimeoutEventOrigin.RESPONSE_REQUEST
437
- )
438
- return min_timestamp
@@ -1,48 +0,0 @@
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
- # class AdapterManager:
16
- # def __init__(self) -> None:
17
- # self.adapters : Dict[str, AdapterBackend] = {}
18
-
19
- # def get_adapter(self, descriptor: Descriptor) -> AdapterBackend:
20
- # string_descriptor = str(descriptor)
21
- # if string_descriptor not in self.adapters:
22
- # # The adapter doesn't exist, create it
23
- # if isinstance(
24
- # descriptor, SerialPortDescriptor
25
- # ): # Add mandatory timeout and stop_condition here ?
26
- # self.adapters[string_descriptor] = SerialPortBackend(
27
- # descriptor=SerialPortDescriptor(
28
- # port=descriptor.port, baudrate=descriptor.baudrate
29
- # )
30
- # )
31
- # elif isinstance(descriptor, IPDescriptor):
32
- # self.adapters[string_descriptor] = IPBackend(descriptor=descriptor)
33
- # elif isinstance(descriptor, VisaDescriptor):
34
- # self.adapters[string_descriptor] = VisaBackend(descriptor=descriptor)
35
- # else:
36
- # raise ValueError(f"Unsupported descriptor : {descriptor}")
37
-
38
- # return self.adapters[string_descriptor]
39
-
40
- # def close_adapter(self, descriptor: Descriptor):
41
- # string_decriptor = str(descriptor)
42
- # if string_decriptor in self.adapters:
43
- # adapter = self.adapters.pop(string_decriptor)
44
- # adapter.close()
45
-
46
-
47
- # # The singleton instance
48
- # adapterManager = AdapterManager()