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
@@ -3,12 +3,12 @@
3
3
  # License : GPL
4
4
 
5
5
 
6
- class TerminalCompatible:
7
- name = "..."
8
- description = "..."
9
- help = "..."
10
- aliases = "..."
11
- # prompt settings ? color ?
6
+ # class TerminalCompatible:
7
+ # name = "..."
8
+ # description = "..."
9
+ # help = "..."
10
+ # aliases = "..."
11
+ # # prompt settings ? color ?
12
12
 
13
- def handle_input(self, user_input: str | bytes) -> None:
14
- pass
13
+ # def handle_input(self, user_input: str | bytes) -> None:
14
+ # pass
syndesi/component.py ADDED
@@ -0,0 +1,315 @@
1
+ # File : component.py
2
+ # Author : Sébastien Deriaz
3
+ # License : GPL
4
+ """
5
+ Component is the base of the main syndesi classes : Adapters, Protocols and Drivers
6
+ """
7
+
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from concurrent.futures import Future
11
+ from dataclasses import dataclass, field
12
+ from enum import StrEnum
13
+ from types import EllipsisType
14
+ from typing import Generic, TypeVar
15
+
16
+ from syndesi.adapters.stop_conditions import Fragment, StopCondition, StopConditionType
17
+ from syndesi.adapters.timeout import Timeout
18
+ from syndesi.tools.errors import AdapterOpenError, WorkerThreadError
19
+
20
+ from .tools.log_settings import LoggerAlias
21
+
22
+
23
+ class Event:
24
+ """Generic event, used to move information asynchronously from the adapter worker thread"""
25
+
26
+
27
+ class Descriptor(ABC):
28
+ """
29
+ Descriptor base class. A descriptor is a string to define the main parameters
30
+ of an adapter (ip address, port, baudrate, etc...)
31
+ """
32
+
33
+ DETECTION_PATTERN = ""
34
+
35
+ def __init__(self) -> None:
36
+ return None
37
+
38
+ @staticmethod
39
+ @abstractmethod
40
+ def from_string(string: str) -> "Descriptor":
41
+ """
42
+ Create a Descriptor class from a string
43
+ """
44
+
45
+ @abstractmethod
46
+ def is_initialized(self) -> bool:
47
+ """Return True if the descriptor is initialized"""
48
+
49
+
50
+ T = TypeVar("T")
51
+
52
+
53
+ @dataclass
54
+ class Frame(Generic[T]):
55
+ """
56
+ Adapter signal containing received data
57
+ """
58
+
59
+ stop_timestamp: float | None
60
+ stop_condition_type: StopConditionType | None # None if it's a response timeout
61
+ previous_read_buffer_used: bool
62
+ response_delay: float | None
63
+
64
+ @abstractmethod
65
+ def get_payload(self) -> T:
66
+ """
67
+ Return frame payload
68
+ """
69
+
70
+ @abstractmethod
71
+ def __str__(self) -> str: ...
72
+
73
+
74
+ @dataclass
75
+ class AdapterFrame(Frame[bytes]):
76
+ """
77
+ Adapter frame
78
+ """
79
+
80
+ fragments: list[Fragment] = field(default_factory=lambda: [])
81
+
82
+ def get_payload(self) -> bytes:
83
+ """
84
+ Return all fragement data as a combined bytes array
85
+ """
86
+ return b"".join([f.data for f in self.fragments])
87
+
88
+ def __str__(self) -> str:
89
+ return f"AdapterFrame({self.get_payload()!r})"
90
+
91
+
92
+ R = TypeVar("R")
93
+
94
+
95
+ class ThreadCommand(Future[R]):
96
+ """
97
+ Command object completed by the worker thread.
98
+
99
+ - .future is a concurrent.futures.Future => compatible with asyncio.wrap_future
100
+ - .result() raises WorkerThreadError on command-timeout (worker not responding),
101
+ not on device read timeouts (those are handled in the worker and surfaced as Adapter* errors).
102
+ """
103
+
104
+ def result(self, timeout: float | None = None) -> R:
105
+ """
106
+ Return the result of the thread command
107
+ """
108
+ try:
109
+ return super().result(timeout=timeout)
110
+ except TimeoutError:
111
+ raise WorkerThreadError(
112
+ f"No response from worker thread to {type(self).__name__} within {timeout}s"
113
+ ) from None
114
+
115
+
116
+ class ReadScope(StrEnum):
117
+ """
118
+ Read scope
119
+
120
+ NEXT : Only read data after the start of the read() call
121
+ BUFFERED : Return any data that was present before the read() call
122
+ """
123
+
124
+ NEXT = "next"
125
+ BUFFERED = "buffered"
126
+
127
+
128
+ class Component(ABC, Generic[T]):
129
+ """Syndesi Component
130
+
131
+ A Component is the elementary class of Syndesi. It is the base
132
+ of all classes the user will be using
133
+ """
134
+
135
+ def __init__(self, logger_alias: LoggerAlias) -> None:
136
+ super().__init__()
137
+ self._logger = logging.getLogger(logger_alias.value)
138
+
139
+ # ==== open ====
140
+
141
+ @abstractmethod
142
+ def open(self) -> None:
143
+ """Open the component"""
144
+
145
+ @abstractmethod
146
+ async def aopen(self) -> None:
147
+ """Asynchronously open the component"""
148
+
149
+ # ==== try_open ====
150
+
151
+ async def atry_open(self) -> bool:
152
+ """
153
+ Async try to open communication with the device
154
+ Return True if sucessful and False otherwise
155
+
156
+ Returns
157
+ -------
158
+ success : bool
159
+ """
160
+ try:
161
+ await self.aopen()
162
+ return True
163
+ except AdapterOpenError:
164
+ return False
165
+
166
+ def try_open(self) -> bool:
167
+ """
168
+ Try to open communication with the device
169
+ Return True if sucessful and False otherwise
170
+
171
+ Returns
172
+ -------
173
+ success : bool
174
+ """
175
+ try:
176
+ self.open()
177
+ except AdapterOpenError:
178
+ return False
179
+ return True
180
+
181
+ # ==== close ====
182
+
183
+ @abstractmethod
184
+ def close(self) -> None:
185
+ """Close the component"""
186
+
187
+ @abstractmethod
188
+ async def aclose(self) -> None:
189
+ """Asynchronously close the component"""
190
+
191
+ # ==== read_detailed ====
192
+
193
+ @abstractmethod
194
+ async def aread_detailed(
195
+ self,
196
+ timeout: Timeout | EllipsisType | None = ...,
197
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
198
+ scope: str = ReadScope.BUFFERED.value,
199
+ ) -> Frame[T]:
200
+ """Asynchronously read data from the component and return a Frame object"""
201
+
202
+ @abstractmethod
203
+ def read_detailed(
204
+ self,
205
+ timeout: Timeout | EllipsisType | None = ...,
206
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
207
+ scope: str = ReadScope.BUFFERED.value,
208
+ ) -> Frame[T]:
209
+ """Read data from the component and return a Frame object"""
210
+
211
+ # ==== read ====
212
+
213
+ @abstractmethod
214
+ async def aread(
215
+ self,
216
+ timeout: Timeout | EllipsisType | None = ...,
217
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
218
+ scope: str = ReadScope.BUFFERED.value,
219
+ ) -> T:
220
+ """Asynchronously read data from the component"""
221
+
222
+ @abstractmethod
223
+ def read(
224
+ self,
225
+ timeout: Timeout | EllipsisType | None = ...,
226
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
227
+ scope: str = ReadScope.BUFFERED.value,
228
+ ) -> T:
229
+ """Read data from the component"""
230
+
231
+ # ==== flush_read ====
232
+
233
+ @abstractmethod
234
+ async def aflush_read(self) -> None:
235
+ """Clear input buffer"""
236
+
237
+ @abstractmethod
238
+ def flush_read(self) -> None:
239
+ """Clear input buffer"""
240
+
241
+ # ==== write ====
242
+
243
+ @abstractmethod
244
+ async def awrite(self, data: T) -> None:
245
+ """Asynchronously write data to the component"""
246
+
247
+ @abstractmethod
248
+ def write(self, data: T) -> None:
249
+ """Synchronously write data to the component"""
250
+
251
+ # ==== query_detailed ====
252
+
253
+ @abstractmethod
254
+ async def aquery_detailed(
255
+ self,
256
+ payload: T,
257
+ timeout: Timeout | None | EllipsisType = ...,
258
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
259
+ scope: str = ReadScope.BUFFERED.value,
260
+ ) -> Frame[T]:
261
+ """
262
+ Asynchronously query the component and return a Frame object
263
+ """
264
+
265
+ @abstractmethod
266
+ def query_detailed(
267
+ self,
268
+ payload: T,
269
+ timeout: Timeout | None | EllipsisType = ...,
270
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
271
+ scope: str = ReadScope.BUFFERED.value,
272
+ ) -> Frame[T]:
273
+ """
274
+ Synchronously query the component and return a Frame object
275
+ """
276
+
277
+ # ==== query ====
278
+
279
+ async def aquery(
280
+ self,
281
+ payload: T,
282
+ timeout: Timeout | None | EllipsisType = ...,
283
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
284
+ scope: str = ReadScope.BUFFERED.value,
285
+ ) -> T:
286
+ """Asynchronously query the component"""
287
+ output_frame = await self.aquery_detailed(
288
+ payload=payload,
289
+ timeout=timeout,
290
+ stop_conditions=stop_conditions,
291
+ scope=scope,
292
+ )
293
+ return output_frame.get_payload()
294
+
295
+ def query(
296
+ self,
297
+ payload: T,
298
+ timeout: Timeout | None | EllipsisType = ...,
299
+ stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
300
+ scope: str = ReadScope.BUFFERED.value,
301
+ ) -> T:
302
+ """Query the component"""
303
+ output_frame = self.query_detailed(
304
+ payload=payload,
305
+ timeout=timeout,
306
+ stop_conditions=stop_conditions,
307
+ scope=scope,
308
+ )
309
+ return output_frame.get_payload()
310
+
311
+ # ==== Other ====
312
+
313
+ @abstractmethod
314
+ def is_open(self) -> bool:
315
+ """Return True if the component is open"""
@@ -1,48 +1,75 @@
1
1
  # File : delimited.py
2
2
  # Author : Sébastien Deriaz
3
3
  # License : GPL
4
+ """
5
+ Delimited protocol, formats data when communicating with devices expecting
6
+ command-like formats with specified delimiters (like \\n, \\r, \\r\\n, etc...)
7
+ """
4
8
 
5
9
  from collections.abc import Callable
6
10
  from types import EllipsisType
7
11
 
12
+ from syndesi.adapters.adapter_worker import (
13
+ AdapterDisconnectedEvent,
14
+ AdapterEvent,
15
+ AdapterFrameEvent,
16
+ )
17
+
8
18
  from ..adapters.adapter import Adapter
9
- from ..adapters.backend.adapter_backend import AdapterReadPayload, AdapterSignal
10
- from ..adapters.stop_condition import StopCondition, Termination
19
+ from ..adapters.stop_conditions import StopCondition, Termination
11
20
  from ..adapters.timeout import Timeout
12
- from .protocol import Protocol
21
+ from ..component import AdapterFrame, ReadScope
22
+ from .protocol import (
23
+ Protocol,
24
+ ProtocolDisconnectedEvent,
25
+ ProtocolEvent,
26
+ ProtocolFrame,
27
+ ProtocolFrameEvent,
28
+ )
29
+
30
+
31
+ class DelimitedFrame(ProtocolFrame[str]):
32
+ """Delimited frame"""
13
33
 
34
+ payload: str
35
+
36
+ def __str__(self) -> str:
37
+ return f"DelimitedFrame({self.payload})"
38
+
39
+
40
+ class Delimited(Protocol[str]):
41
+ """
42
+ Protocol with delimiter, like LF, CR, etc... LF is used by default
43
+
44
+ No presentation or application layers
45
+
46
+ Parameters
47
+ ----------
48
+ adapter : Adapter
49
+ termination : bytes
50
+ Command termination, '\\n' by default
51
+ format_response : bool
52
+ Apply formatting to the response (i.e removing the termination), True by default
53
+ encoding : str or None
54
+ If None, delimited will not encode/decode
55
+ timeout : Timeout
56
+ None by default (default timeout)
57
+ receive_termination : bytes
58
+ Termination when receiving only, optional
59
+ if not set, the value of termination is used
60
+ """
14
61
 
15
- class Delimited(Protocol):
16
62
  def __init__(
17
63
  self,
18
64
  adapter: Adapter,
19
65
  termination: str = "\n",
66
+ *,
20
67
  format_response: bool = True,
21
68
  encoding: str = "utf-8",
22
69
  timeout: Timeout | None | EllipsisType = ...,
23
- event_callback: Callable[[AdapterSignal], None] | None = None,
70
+ event_callback: Callable[[ProtocolEvent], None] | None = None,
24
71
  receive_termination: str | None = None,
25
72
  ) -> None:
26
- """
27
- Protocol with delimiter, like LF, CR, etc... LF is used by default
28
-
29
- No presentation or application layers
30
-
31
- Parameters
32
- ----------
33
- adapter : Adapter
34
- termination : bytes
35
- Command termination, '\\n' by default
36
- format_response : bool
37
- Apply formatting to the response (i.e removing the termination), True by default
38
- encoding : str or None
39
- If None, delimited will not encode/decode
40
- timeout : Timeout
41
- None by default (default timeout)
42
- receive_termination : bytes
43
- Termination when receiving only, optional
44
- if not set, the value of termination is used
45
- """
46
73
  if not isinstance(termination, str) or isinstance(termination, bytes):
47
74
  raise ValueError(
48
75
  f"end argument must be of type str or bytes, not {type(termination)}"
@@ -60,76 +87,52 @@ class Delimited(Protocol):
60
87
  )
61
88
  super().__init__(adapter, timeout=timeout, event_callback=event_callback)
62
89
 
63
- # Connect the adapter if it wasn't done already
64
- self._adapter.connect()
90
+ self._adapter.set_event_callback(self._on_event)
65
91
 
66
92
  # TODO : Disable encoding/decoding when encoding==None
67
93
 
68
94
  def __str__(self) -> str:
69
95
  if self._receive_termination == self._termination:
70
96
  return f"Delimited({self._adapter},{repr(self._termination)})"
71
- else:
72
- return f"Delimited({self._adapter},{repr(self._termination)}/{repr(self._receive_termination)})"
73
-
74
- def _default_timeout(self) -> Timeout | None:
75
- return Timeout(response=2, action="error")
97
+ return (
98
+ f"Delimited({self._adapter},{repr(self._termination)}"
99
+ "/{repr(self._receive_termination)})"
100
+ )
76
101
 
77
102
  def __repr__(self) -> str:
78
103
  return self.__str__()
79
104
 
80
- def _to_bytes(self, command: str | bytes) -> bytes:
81
- if isinstance(command, str):
82
- return command.encode("ASCII")
83
- elif isinstance(command, bytes):
84
- return command
85
- else:
86
- raise ValueError(f"Invalid command type : {type(command)}")
87
-
88
- def _from_bytes(self, payload: bytes) -> str:
89
- assert isinstance(payload, bytes)
90
- return payload.decode("ASCII") # TODO : encoding ?
91
-
92
- def _format_command(self, command: str) -> str:
93
- return command + self._termination
105
+ def _default_timeout(self) -> Timeout | None:
106
+ return Timeout(response=2, action="error")
94
107
 
95
- def _format_response(self, response: str) -> str:
96
- if response.endswith(self._receive_termination):
97
- response = response[: -len(self._receive_termination)]
98
- return response
108
+ # ┌────────────┐
109
+ # │ Public API │
110
+ # └────────────┘
99
111
 
100
- def _on_data_ready_event(self, data: AdapterReadPayload) -> None:
101
- # TODO : Call the callback here ?
102
- # output = self._format_read(data.data(), decode=True)
103
- # return output
104
- pass
112
+ # ==== read_detailed ====
105
113
 
106
- def write(self, command: str) -> None:
107
- command = self._format_command(command)
108
- self._adapter.write(self._to_bytes(command))
114
+ def _adapter_to_protocol(self, adapter_frame: AdapterFrame) -> DelimitedFrame:
115
+ data = adapter_frame.get_payload().decode(self._encoding)
116
+ if data.endswith(self._receive_termination):
117
+ data = data[: -len(self._receive_termination)]
109
118
 
110
- def query(self, data: str, timeout: Timeout | None | EllipsisType = ...) -> str:
111
- """
112
- Writes then reads from the device and return the result
119
+ return DelimitedFrame(
120
+ payload=data,
121
+ stop_timestamp=adapter_frame.stop_timestamp,
122
+ stop_condition_type=adapter_frame.stop_condition_type,
123
+ previous_read_buffer_used=adapter_frame.previous_read_buffer_used,
124
+ response_delay=adapter_frame.response_delay,
125
+ )
113
126
 
114
- Parameters
115
- ----------
116
- data : str
117
- Data to send to the device
118
- timeout : Timeout
119
- Custom timeout for this query (optional)
120
- decode : bool
121
- Decode incoming data, True by default
122
- full_output : bool
123
- return metrics on read operation (False by default)
124
- """
125
- self._adapter.flushRead()
126
- self.write(data)
127
- return self.read(timeout=timeout)
127
+ def _protocol_to_adapter(self, protocol_payload: str) -> bytes:
128
+ terminated_payload = protocol_payload + self._termination
129
+ return terminated_payload.encode(self._encoding)
128
130
 
129
131
  def read_raw(
130
132
  self,
131
133
  timeout: Timeout | None | EllipsisType = ...,
132
134
  stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
135
+ scope: str = ReadScope.BUFFERED.value,
133
136
  ) -> bytes:
134
137
  """
135
138
  Reads command and formats it as a str
@@ -145,39 +148,21 @@ class Delimited(Protocol):
145
148
  """
146
149
 
147
150
  # Send up to the termination
148
- signal = self._adapter.read_detailed(
149
- timeout=timeout, stop_conditions=stop_conditions
151
+ frame = self._adapter.read_detailed(
152
+ timeout=timeout, stop_conditions=stop_conditions, scope=scope
150
153
  )
151
- return signal.data()
154
+ return frame.get_payload()
152
155
 
153
- def read_detailed(
154
- self,
155
- timeout: Timeout | None | EllipsisType = ...,
156
- stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
157
- ) -> AdapterReadPayload:
158
- signal = self._adapter.read_detailed(
159
- timeout=timeout, stop_conditions=stop_conditions
160
- )
161
- return signal
156
+ def _on_event(self, event: AdapterEvent) -> None:
162
157
 
163
- def read(
164
- self,
165
- timeout: Timeout | None | EllipsisType = ...,
166
- stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
167
- ) -> str:
168
- signal = self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
169
- return self._decode(signal.data())
170
-
171
- def _decode(self, data: bytes) -> str:
172
- try:
173
- data_string = data.decode(self._encoding)
174
- except UnicodeDecodeError as err:
175
- raise ValueError(
176
- f"Failed to decode {data!r} to {self._encoding} ({err})"
177
- ) from err
178
- else:
179
- if not self._response_formatting:
180
- # Add the termination back in since it was removed by the adapter
181
- data_string += self._receive_termination
158
+ if self._event_callback is not None:
159
+ output_event: ProtocolEvent | None = None
160
+ if isinstance(event, AdapterDisconnectedEvent):
161
+ output_event = ProtocolDisconnectedEvent()
162
+ if isinstance(event, AdapterFrameEvent):
163
+ output_event = ProtocolFrameEvent(
164
+ frame=self._adapter_to_protocol(event.frame)
165
+ )
182
166
 
183
- return data_string
167
+ if output_event is not None:
168
+ self._event_callback(output_event)