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.
- syndesi/__init__.py +22 -2
- syndesi/adapters/adapter.py +332 -489
- syndesi/adapters/adapter_worker.py +820 -0
- syndesi/adapters/auto.py +58 -25
- syndesi/adapters/descriptors.py +38 -0
- syndesi/adapters/ip.py +203 -71
- syndesi/adapters/serialport.py +154 -25
- syndesi/adapters/stop_conditions.py +354 -0
- syndesi/adapters/timeout.py +58 -21
- syndesi/adapters/visa.py +236 -11
- syndesi/cli/console.py +51 -16
- syndesi/cli/shell.py +95 -47
- syndesi/cli/terminal_tools.py +8 -8
- syndesi/component.py +315 -0
- syndesi/protocols/delimited.py +92 -107
- syndesi/protocols/modbus.py +2368 -868
- syndesi/protocols/protocol.py +186 -33
- syndesi/protocols/raw.py +45 -62
- syndesi/protocols/scpi.py +65 -102
- syndesi/remote/remote.py +188 -0
- syndesi/scripts/syndesi.py +12 -2
- syndesi/tools/errors.py +49 -31
- syndesi/tools/log_settings.py +21 -8
- syndesi/tools/{log.py → logmanager.py} +24 -13
- syndesi/tools/types.py +9 -7
- syndesi/version.py +5 -1
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/METADATA +1 -1
- syndesi-0.5.0.dist-info/RECORD +41 -0
- syndesi/adapters/backend/__init__.py +0 -0
- syndesi/adapters/backend/adapter_backend.py +0 -438
- syndesi/adapters/backend/adapter_manager.py +0 -48
- syndesi/adapters/backend/adapter_session.py +0 -346
- syndesi/adapters/backend/backend.py +0 -438
- syndesi/adapters/backend/backend_status.py +0 -0
- syndesi/adapters/backend/backend_tools.py +0 -66
- syndesi/adapters/backend/descriptors.py +0 -153
- syndesi/adapters/backend/ip_backend.py +0 -149
- syndesi/adapters/backend/serialport_backend.py +0 -241
- syndesi/adapters/backend/stop_condition_backend.py +0 -219
- syndesi/adapters/backend/timed_queue.py +0 -39
- syndesi/adapters/backend/timeout.py +0 -252
- syndesi/adapters/backend/visa_backend.py +0 -197
- syndesi/adapters/ip_server.py +0 -102
- syndesi/adapters/stop_condition.py +0 -90
- syndesi/cli/backend_console.py +0 -96
- syndesi/cli/backend_status.py +0 -274
- syndesi/cli/backend_wrapper.py +0 -61
- syndesi/scripts/syndesi_backend.py +0 -37
- syndesi/tools/backend_api.py +0 -175
- syndesi/tools/backend_logger.py +0 -64
- syndesi/tools/exceptions.py +0 -16
- syndesi/tools/internal.py +0 -0
- syndesi-0.4.2.dist-info/RECORD +0 -60
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/WHEEL +0 -0
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/entry_points.txt +0 -0
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {syndesi-0.4.2.dist-info → syndesi-0.5.0.dist-info}/top_level.txt +0 -0
syndesi/cli/terminal_tools.py
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
# License : GPL
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class TerminalCompatible:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
# class TerminalCompatible:
|
|
7
|
+
# name = "..."
|
|
8
|
+
# description = "..."
|
|
9
|
+
# help = "..."
|
|
10
|
+
# aliases = "..."
|
|
11
|
+
# # prompt settings ? color ?
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
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"""
|
syndesi/protocols/delimited.py
CHANGED
|
@@ -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.
|
|
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
|
|
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[[
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
81
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return response
|
|
108
|
+
# ┌────────────┐
|
|
109
|
+
# │ Public API │
|
|
110
|
+
# └────────────┘
|
|
99
111
|
|
|
100
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
154
|
+
return frame.get_payload()
|
|
152
155
|
|
|
153
|
-
def
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
167
|
+
if output_event is not None:
|
|
168
|
+
self._event_callback(output_event)
|