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/adapters/auto.py
CHANGED
|
@@ -1,37 +1,71 @@
|
|
|
1
1
|
# File : auto.py
|
|
2
2
|
# Author : Sébastien Deriaz
|
|
3
3
|
# License : GPL
|
|
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 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
4
|
|
|
5
|
+
"""
|
|
6
|
+
Automatic adapter function
|
|
7
|
+
This function is used to automatically choose an adapter based on the user's input
|
|
8
|
+
192.168.1.1 -> IP
|
|
9
|
+
COM4 -> Serial
|
|
10
|
+
/dev/tty* -> Serial
|
|
11
|
+
etc...
|
|
12
|
+
If an adapter class is supplied, it is passed through
|
|
13
|
+
|
|
14
|
+
Additionnaly, it is possible to do COM4:115200 so as to make the life of the user easier
|
|
15
|
+
Same with /dev/ttyACM0:115200
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
from syndesi.component import Descriptor
|
|
16
22
|
|
|
17
23
|
from .adapter import Adapter
|
|
18
|
-
from .
|
|
19
|
-
|
|
24
|
+
from .ip import IP, IPDescriptor
|
|
25
|
+
from .serialport import SerialPort, SerialPortDescriptor
|
|
26
|
+
from .visa import Visa, VisaDescriptor
|
|
27
|
+
|
|
28
|
+
descriptors: list[type[Descriptor]] = [
|
|
20
29
|
SerialPortDescriptor,
|
|
30
|
+
IPDescriptor,
|
|
21
31
|
VisaDescriptor,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def adapter_descriptor_by_string(string_descriptor: str) -> Descriptor:
|
|
36
|
+
"""
|
|
37
|
+
Return a corresponding adapter descriptor from a string
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
string_descriptor : str
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
descriptor : Descriptor
|
|
46
|
+
"""
|
|
47
|
+
for descriptor in descriptors:
|
|
48
|
+
if re.match(descriptor.DETECTION_PATTERN, string_descriptor):
|
|
49
|
+
x = descriptor.from_string(string_descriptor)
|
|
50
|
+
return x
|
|
51
|
+
raise ValueError(f"Could not parse descriptor string : {string_descriptor}")
|
|
27
52
|
|
|
28
53
|
|
|
29
54
|
def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
|
|
55
|
+
"""
|
|
56
|
+
Create an adapter from a string or an adapter
|
|
57
|
+
|
|
58
|
+
- <int>.<int>.<int>.<int>[:<int>] -> IP
|
|
59
|
+
- x.y[:<int>] -> IP
|
|
60
|
+
- COM<int> -> SerialPort
|
|
61
|
+
- /dev/tty[ACM|USB]<int> -> SerialPort
|
|
62
|
+
|
|
63
|
+
"""
|
|
30
64
|
if isinstance(adapter_or_string, Adapter):
|
|
31
65
|
# Simply return it
|
|
32
66
|
return adapter_or_string
|
|
33
67
|
|
|
34
|
-
|
|
68
|
+
if isinstance(adapter_or_string, str):
|
|
35
69
|
descriptor = adapter_descriptor_by_string(adapter_or_string)
|
|
36
70
|
if isinstance(descriptor, IPDescriptor):
|
|
37
71
|
return IP(
|
|
@@ -39,12 +73,11 @@ def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
|
|
|
39
73
|
port=descriptor.port,
|
|
40
74
|
transport=descriptor.transport.value,
|
|
41
75
|
)
|
|
42
|
-
|
|
76
|
+
if isinstance(descriptor, SerialPortDescriptor):
|
|
43
77
|
return SerialPort(port=descriptor.port, baudrate=descriptor.baudrate)
|
|
44
|
-
|
|
78
|
+
if isinstance(descriptor, VisaDescriptor):
|
|
45
79
|
return Visa(descriptor=descriptor.descriptor)
|
|
46
|
-
else:
|
|
47
|
-
raise RuntimeError(f"Invalid descriptor : {descriptor}")
|
|
48
80
|
|
|
49
|
-
|
|
50
|
-
|
|
81
|
+
raise RuntimeError(f"Invalid descriptor : {descriptor}")
|
|
82
|
+
|
|
83
|
+
raise ValueError(f"Invalid adapter : {adapter_or_string}")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# # File : descriptors.py
|
|
2
|
+
# # Author : Sébastien Deriaz
|
|
3
|
+
# # License : GPL
|
|
4
|
+
# """
|
|
5
|
+
# Descriptors are classes that describe how an adapter is connected to its device.
|
|
6
|
+
# Depending on the protocol, they can hold strings, integers or enums
|
|
7
|
+
# """
|
|
8
|
+
|
|
9
|
+
# import re
|
|
10
|
+
# from abc import abstractmethod
|
|
11
|
+
# from dataclasses import dataclass
|
|
12
|
+
# from enum import Enum
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# descriptors: list[type[Descriptor]] = [
|
|
16
|
+
# SerialPortDescriptor,
|
|
17
|
+
# IPDescriptor,
|
|
18
|
+
# VisaDescriptor,
|
|
19
|
+
# ]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# def adapter_descriptor_by_string(string_descriptor: str) -> Descriptor:
|
|
23
|
+
# """
|
|
24
|
+
# Return a corresponding adapter descriptor from a string
|
|
25
|
+
|
|
26
|
+
# Parameters
|
|
27
|
+
# ----------
|
|
28
|
+
# string_descriptor : str
|
|
29
|
+
|
|
30
|
+
# Returns
|
|
31
|
+
# -------
|
|
32
|
+
# descriptor : Descriptor
|
|
33
|
+
# """
|
|
34
|
+
# for descriptor in descriptors:
|
|
35
|
+
# if re.match(descriptor.DETECTION_PATTERN, string_descriptor):
|
|
36
|
+
# x = descriptor.from_string(string_descriptor)
|
|
37
|
+
# return x
|
|
38
|
+
# raise ValueError(f"Could not parse descriptor string : {string_descriptor}")
|
syndesi/adapters/ip.py
CHANGED
|
@@ -1,110 +1,242 @@
|
|
|
1
1
|
# File : ip.py
|
|
2
2
|
# Author : Sébastien Deriaz
|
|
3
3
|
# License : GPL
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
"""
|
|
5
|
+
IP Adapter, used to communicate with IP targets using the socket module
|
|
6
|
+
"""
|
|
7
7
|
|
|
8
|
+
import socket
|
|
8
9
|
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import StrEnum
|
|
9
12
|
from types import EllipsisType
|
|
13
|
+
from typing import cast
|
|
14
|
+
|
|
15
|
+
import _socket
|
|
10
16
|
|
|
11
|
-
from syndesi.adapters.
|
|
12
|
-
from syndesi.
|
|
17
|
+
from syndesi.adapters.adapter_worker import AdapterEvent, HasFileno
|
|
18
|
+
from syndesi.adapters.stop_conditions import Continuation, StopCondition
|
|
19
|
+
from syndesi.adapters.timeout import Timeout
|
|
20
|
+
from syndesi.component import Descriptor
|
|
21
|
+
from syndesi.tools.errors import AdapterOpenError, AdapterWriteError
|
|
13
22
|
|
|
14
23
|
from .adapter import Adapter
|
|
15
|
-
from .
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
from .stop_conditions import Fragment
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class IPDescriptor(Descriptor):
|
|
29
|
+
"""
|
|
30
|
+
IP descriptor that holds ip address and port
|
|
31
|
+
"""
|
|
18
32
|
|
|
19
|
-
|
|
33
|
+
class Transport(StrEnum):
|
|
34
|
+
"""
|
|
35
|
+
IP Transport protocol
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
TCP = "TCP"
|
|
39
|
+
UDP = "UDP"
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_str(cls, transport: str) -> "IPDescriptor":
|
|
43
|
+
"""
|
|
44
|
+
Create a Transport class from a string
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
transport : str
|
|
49
|
+
"""
|
|
50
|
+
for member in cls:
|
|
51
|
+
if member.value.lower() == transport.lower():
|
|
52
|
+
return member # type: ignore # TODO : Check this
|
|
53
|
+
raise ValueError(f"{transport} is not a valid {cls.__name__}")
|
|
54
|
+
|
|
55
|
+
DETECTION_PATTERN = r"(\d+.\d+.\d+.\d+|[\w\.]+):\d+:(UDP|TCP)"
|
|
56
|
+
address: str
|
|
57
|
+
transport: Transport
|
|
58
|
+
port: int | None = None
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def from_string(string: str) -> "IPDescriptor":
|
|
62
|
+
parts = string.split(":")
|
|
63
|
+
address = parts[0]
|
|
64
|
+
port = int(parts[1])
|
|
65
|
+
transport = IPDescriptor.Transport(parts[2])
|
|
66
|
+
return IPDescriptor(address, transport, port)
|
|
67
|
+
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
return f"{self.address}:{self.port}:{self.Transport(self.transport).value}"
|
|
70
|
+
|
|
71
|
+
def is_initialized(self) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Return True if all attributes has been defined (not None)
|
|
74
|
+
"""
|
|
20
75
|
|
|
21
|
-
|
|
76
|
+
return self.port is not None and self.transport is not None
|
|
22
77
|
|
|
23
78
|
|
|
24
79
|
class IP(Adapter):
|
|
25
|
-
|
|
80
|
+
"""
|
|
81
|
+
IP stack adapter. The IP Adapter reads and writes bytes units (frames)
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
address : str
|
|
86
|
+
IP address
|
|
87
|
+
port : int or None, default : None
|
|
88
|
+
IP port
|
|
89
|
+
transport : {'TCP', 'UDP'}
|
|
90
|
+
Transport layer
|
|
91
|
+
timeout : Timeout or float
|
|
92
|
+
Specify communication timeout, the time it takes for the target to respond
|
|
93
|
+
stop_conditions : list[StopCondition] or StopCondition
|
|
94
|
+
Stop coniditions are used to decide when a read data block is finished
|
|
95
|
+
and should be returned
|
|
96
|
+
|
|
97
|
+
These include
|
|
98
|
+
|
|
99
|
+
* Termination : stop on a specific sequence like ``\\n`` at the end of the data
|
|
100
|
+
* Length : stop when a specific number of bytes has been received
|
|
101
|
+
* Continuation : stop when no data has been received for a
|
|
102
|
+
specified amount of time
|
|
103
|
+
* Total : stop if the time since the first piece of data received exceeds
|
|
104
|
+
a given amount of time
|
|
105
|
+
* FragmentStopCondition : Return each piece of data individually as received
|
|
106
|
+
by the low-level communication layer
|
|
107
|
+
|
|
108
|
+
Multiple stop conditions can be used to create more complex behaviours
|
|
109
|
+
encoding : str
|
|
110
|
+
Used to convert str to bytes if the user chooses to send
|
|
111
|
+
alias : str
|
|
112
|
+
Name of the adapter, may be removed in the future
|
|
113
|
+
event_callback : f(event : AdapterEvent)
|
|
114
|
+
Function called when an event is received by the adapter worker thread.
|
|
115
|
+
The event can be either one of :
|
|
116
|
+
|
|
117
|
+
* ``AdapterDisconnectedEvent``
|
|
118
|
+
* ``AdapterFrameEvent``
|
|
119
|
+
* ``FirstFragmentEvent``
|
|
120
|
+
auto_open : bool, default to True
|
|
121
|
+
Automatically open the adapter after instanciation
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
BUFFER_SIZE = 65507
|
|
26
125
|
|
|
27
126
|
def __init__(
|
|
28
127
|
self,
|
|
29
128
|
address: str,
|
|
30
129
|
port: int | None = None,
|
|
31
|
-
transport: str =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
130
|
+
transport: str = IPDescriptor.Transport.TCP.value,
|
|
131
|
+
*,
|
|
132
|
+
timeout: Timeout | float | EllipsisType = ...,
|
|
133
|
+
stop_conditions: list[StopCondition] | StopCondition | EllipsisType = ...,
|
|
35
134
|
encoding: str = "utf-8",
|
|
36
|
-
|
|
135
|
+
alias: str = "",
|
|
136
|
+
event_callback: Callable[[AdapterEvent], None] | None = None,
|
|
37
137
|
auto_open: bool = True,
|
|
38
|
-
backend_address: str | None = None,
|
|
39
|
-
backend_port: int | None = None,
|
|
40
138
|
):
|
|
41
|
-
"""
|
|
42
|
-
IP adapter
|
|
43
139
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
transport : str
|
|
51
|
-
'TCP' or 'UDP'
|
|
52
|
-
timeout : Timeout | float
|
|
53
|
-
Specify communication timeout
|
|
54
|
-
stop_condition : StopCondition
|
|
55
|
-
Specify a read stop condition (None by default)
|
|
56
|
-
auto_open : bool
|
|
57
|
-
Automatically open the adapter
|
|
58
|
-
socket : socket.socket
|
|
59
|
-
Specify a custom socket, this is reserved for server application
|
|
140
|
+
descriptor = IPDescriptor(
|
|
141
|
+
address=address,
|
|
142
|
+
port=port,
|
|
143
|
+
transport=IPDescriptor.Transport(transport.upper()),
|
|
144
|
+
)
|
|
145
|
+
self._socket: _socket.socket | None = None
|
|
60
146
|
|
|
61
|
-
"""
|
|
62
147
|
super().__init__(
|
|
63
|
-
descriptor=
|
|
64
|
-
address=address,
|
|
65
|
-
port=port,
|
|
66
|
-
transport=IPDescriptor.Transport(transport.upper()),
|
|
67
|
-
),
|
|
68
|
-
alias=alias,
|
|
69
|
-
timeout=timeout,
|
|
148
|
+
descriptor=descriptor,
|
|
70
149
|
stop_conditions=stop_conditions,
|
|
150
|
+
timeout=timeout,
|
|
71
151
|
encoding=encoding,
|
|
152
|
+
alias=alias,
|
|
72
153
|
event_callback=event_callback,
|
|
73
154
|
auto_open=auto_open,
|
|
74
|
-
backend_address=backend_address,
|
|
75
|
-
backend_port=backend_port,
|
|
76
155
|
)
|
|
77
|
-
self.
|
|
78
|
-
|
|
79
|
-
# if self.descriptor.transport is not None:
|
|
80
|
-
self._logger.info(f"Setting up {self.descriptor.transport.value} IP adapter")
|
|
81
|
-
|
|
82
|
-
self.set_default_timeout(self._default_timeout())
|
|
83
|
-
|
|
84
|
-
def _default_timeout(self) -> Timeout:
|
|
85
|
-
return Timeout(response=5, action="error")
|
|
156
|
+
self._descriptor: IPDescriptor
|
|
157
|
+
self._worker_descriptor: IPDescriptor
|
|
86
158
|
|
|
87
159
|
def set_default_port(self, port: int) -> None:
|
|
88
160
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
This way, the user can leave the port empty
|
|
92
|
-
and the driver/protocol can specify it later
|
|
161
|
+
Set the default port number
|
|
93
162
|
|
|
94
163
|
Parameters
|
|
95
164
|
----------
|
|
96
165
|
port : int
|
|
97
166
|
"""
|
|
98
|
-
if self.
|
|
99
|
-
self.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
167
|
+
if self._descriptor.port is None:
|
|
168
|
+
self._descriptor.port = port
|
|
169
|
+
self._update_descriptor()
|
|
170
|
+
|
|
171
|
+
def _worker_read(self, fragment_timestamp: float) -> Fragment:
|
|
172
|
+
if self._socket is None:
|
|
173
|
+
return Fragment(b"", fragment_timestamp)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
data = self._socket.recv(self.BUFFER_SIZE)
|
|
177
|
+
except (ConnectionRefusedError, OSError):
|
|
178
|
+
fragment = Fragment(b"", fragment_timestamp)
|
|
179
|
+
else:
|
|
180
|
+
if data == b"":
|
|
181
|
+
self._logger.warning("Socket disconnected")
|
|
182
|
+
self._worker_close()
|
|
183
|
+
fragment = Fragment(data, fragment_timestamp)
|
|
184
|
+
return fragment
|
|
185
|
+
|
|
186
|
+
def _worker_write(self, data: bytes) -> None:
|
|
187
|
+
super()._worker_write(data)
|
|
188
|
+
|
|
189
|
+
if self._socket is not None:
|
|
190
|
+
if self._socket.send(data) != len(data):
|
|
191
|
+
raise AdapterWriteError(
|
|
192
|
+
f"Adapter {self._worker_descriptor} couldn't write"
|
|
193
|
+
" all of the data to the socket"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _worker_open(self) -> None:
|
|
197
|
+
self._worker_check_descriptor()
|
|
198
|
+
|
|
199
|
+
# Create the socket instance
|
|
200
|
+
if self._worker_descriptor.transport == IPDescriptor.Transport.TCP:
|
|
201
|
+
self._socket = cast(
|
|
202
|
+
_socket.socket,
|
|
203
|
+
socket.socket(socket.AF_INET, socket.SOCK_STREAM),
|
|
204
|
+
)
|
|
205
|
+
elif self._worker_descriptor.transport == IPDescriptor.Transport.UDP:
|
|
206
|
+
self._socket = cast(
|
|
207
|
+
_socket.socket, socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
raise AdapterOpenError("Invalid transport protocol")
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
self._socket.settimeout(self.WorkerTimeout.OPEN.value)
|
|
214
|
+
self._socket.connect(
|
|
215
|
+
(self._worker_descriptor.address, self._worker_descriptor.port)
|
|
216
|
+
)
|
|
217
|
+
except (OSError, ConnectionRefusedError, socket.gaierror) as e:
|
|
218
|
+
self._opened = False
|
|
219
|
+
msg = f"Failed to open adapter {self._worker_descriptor} : {e}"
|
|
220
|
+
self._logger.error(msg)
|
|
221
|
+
raise AdapterOpenError(msg) from None
|
|
222
|
+
|
|
223
|
+
self._opened = True
|
|
224
|
+
self._logger.info(f"IP Adapter {self._worker_descriptor} opened")
|
|
225
|
+
|
|
226
|
+
def _worker_close(self) -> None:
|
|
227
|
+
if self._socket is not None:
|
|
228
|
+
try:
|
|
229
|
+
self._socket.shutdown(_socket.SHUT_RDWR)
|
|
230
|
+
self._socket.close()
|
|
231
|
+
except OSError:
|
|
232
|
+
pass
|
|
233
|
+
self._socket = None
|
|
234
|
+
|
|
235
|
+
def _selectable(self) -> HasFileno | None:
|
|
236
|
+
return self._socket
|
|
237
|
+
|
|
238
|
+
def _default_stop_conditions(self) -> list[StopCondition]:
|
|
239
|
+
return [Continuation(continuation=0.2)]
|
|
104
240
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
transport : str | IPDescriptor.Transport
|
|
108
|
-
"""
|
|
109
|
-
if self.descriptor.transport is None:
|
|
110
|
-
self.descriptor.transport = IPDescriptor.Transport(transport)
|
|
241
|
+
def _default_timeout(self) -> Timeout:
|
|
242
|
+
return Timeout(response=1, action="error")
|
syndesi/adapters/serialport.py
CHANGED
|
@@ -2,38 +2,93 @@
|
|
|
2
2
|
# Author : Sébastien Deriaz
|
|
3
3
|
# License : GPL
|
|
4
4
|
|
|
5
|
+
"""
|
|
6
|
+
SerialPort module, allows communication with serial devices using
|
|
7
|
+
the OS layers (COMx, /dev/ttyUSBx or /dev/ttyACMx)
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
5
11
|
from collections.abc import Callable
|
|
12
|
+
from dataclasses import dataclass
|
|
6
13
|
from types import EllipsisType
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
import serial
|
|
16
|
+
from serial.serialutil import PortNotOpenError
|
|
17
|
+
|
|
18
|
+
from syndesi.adapters.adapter_worker import AdapterEvent, HasFileno
|
|
19
|
+
from syndesi.component import Descriptor
|
|
20
|
+
from syndesi.tools.errors import AdapterOpenError, AdapterReadError
|
|
9
21
|
from syndesi.tools.types import NumberLike
|
|
10
22
|
|
|
11
23
|
from .adapter import Adapter
|
|
12
|
-
from .
|
|
13
|
-
from .stop_condition import StopCondition
|
|
24
|
+
from .stop_conditions import Continuation, Fragment, StopCondition
|
|
14
25
|
from .timeout import Timeout
|
|
15
26
|
|
|
16
27
|
|
|
28
|
+
@dataclass
|
|
29
|
+
class SerialPortDescriptor(Descriptor):
|
|
30
|
+
"""
|
|
31
|
+
SerialPort descriptor that holds location (COMx or /dev/ttyx) and baudrate
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
DETECTION_PATTERN = r"(COM\d+|/dev[/\w\d]+):\d+"
|
|
35
|
+
port: str
|
|
36
|
+
baudrate: int | None = None
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def from_string(string: str) -> "SerialPortDescriptor":
|
|
40
|
+
parts = string.split(":")
|
|
41
|
+
port = parts[0]
|
|
42
|
+
baudrate = int(parts[1])
|
|
43
|
+
return SerialPortDescriptor(port, baudrate)
|
|
44
|
+
|
|
45
|
+
def set_default_baudrate(self, baudrate: int) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Set the baudrate if it has not be defined before
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
baudrate : int
|
|
52
|
+
"""
|
|
53
|
+
if self.baudrate is not None:
|
|
54
|
+
self.baudrate = baudrate
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def __str__(self) -> str:
|
|
60
|
+
return f"{self.port}:{self.baudrate}"
|
|
61
|
+
|
|
62
|
+
def is_initialized(self) -> bool:
|
|
63
|
+
return self.baudrate is not None
|
|
64
|
+
|
|
65
|
+
|
|
17
66
|
class SerialPort(Adapter):
|
|
67
|
+
"""
|
|
68
|
+
Serial communication adapter
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
port : str
|
|
73
|
+
Serial port (COMx or ttyACMx)
|
|
74
|
+
baudrate : int
|
|
75
|
+
Baudrate
|
|
76
|
+
"""
|
|
77
|
+
|
|
18
78
|
def __init__(
|
|
19
79
|
self,
|
|
20
80
|
port: str,
|
|
21
81
|
baudrate: int | None = None,
|
|
82
|
+
*,
|
|
22
83
|
timeout: Timeout | NumberLike | None | EllipsisType = ...,
|
|
23
84
|
stop_conditions: StopCondition | list[StopCondition] | EllipsisType = ...,
|
|
24
85
|
alias: str = "",
|
|
25
86
|
rts_cts: bool = False, # rts_cts experimental
|
|
26
|
-
event_callback: Callable[[
|
|
27
|
-
|
|
28
|
-
backend_port: int | None = None,
|
|
87
|
+
event_callback: Callable[[AdapterEvent], None] | None = None,
|
|
88
|
+
auto_open: bool = True,
|
|
29
89
|
) -> None:
|
|
30
90
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Parameters
|
|
34
|
-
----------
|
|
35
|
-
port : str
|
|
36
|
-
Serial port (COMx or ttyACMx)
|
|
91
|
+
Instanciate new SerialPort adapter
|
|
37
92
|
"""
|
|
38
93
|
descriptor = SerialPortDescriptor(port, baudrate)
|
|
39
94
|
super().__init__(
|
|
@@ -42,15 +97,18 @@ class SerialPort(Adapter):
|
|
|
42
97
|
stop_conditions=stop_conditions,
|
|
43
98
|
alias=alias,
|
|
44
99
|
event_callback=event_callback,
|
|
45
|
-
|
|
46
|
-
backend_port=backend_port,
|
|
100
|
+
auto_open=auto_open,
|
|
47
101
|
)
|
|
48
|
-
self.
|
|
102
|
+
self._descriptor: SerialPortDescriptor
|
|
103
|
+
self._worker_descriptor: SerialPortDescriptor
|
|
49
104
|
|
|
50
105
|
self._logger.info(
|
|
51
|
-
f"Setting up SerialPort adapter {self.
|
|
106
|
+
f"Setting up SerialPort adapter {self._descriptor}, \
|
|
107
|
+
timeout={timeout} and stop_conditions={self._stop_conditions}"
|
|
52
108
|
)
|
|
53
109
|
|
|
110
|
+
self._port: serial.Serial | None = None
|
|
111
|
+
|
|
54
112
|
self.open()
|
|
55
113
|
|
|
56
114
|
self._rts_cts = rts_cts
|
|
@@ -58,7 +116,50 @@ class SerialPort(Adapter):
|
|
|
58
116
|
def _default_timeout(self) -> Timeout:
|
|
59
117
|
return Timeout(response=2, action="error")
|
|
60
118
|
|
|
61
|
-
def
|
|
119
|
+
def _default_stop_conditions(self) -> list[StopCondition]:
|
|
120
|
+
return [Continuation(0.1)]
|
|
121
|
+
|
|
122
|
+
def _worker_open(self) -> None:
|
|
123
|
+
self._worker_check_descriptor()
|
|
124
|
+
|
|
125
|
+
if self._worker_descriptor.baudrate is None:
|
|
126
|
+
raise AdapterOpenError(
|
|
127
|
+
"Descriptor must be fully initialized to open the adapter"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if self._port is not None:
|
|
131
|
+
self.close()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
self._port = serial.Serial(
|
|
135
|
+
port=self._worker_descriptor.port,
|
|
136
|
+
baudrate=self._worker_descriptor.baudrate,
|
|
137
|
+
rtscts=self._rts_cts,
|
|
138
|
+
)
|
|
139
|
+
except serial.SerialException as e:
|
|
140
|
+
if "No such file" in str(e):
|
|
141
|
+
raise AdapterOpenError(
|
|
142
|
+
f"Port '{self._worker_descriptor.port}' was not found"
|
|
143
|
+
) from e
|
|
144
|
+
raise AdapterOpenError("Unknown error") from e
|
|
145
|
+
|
|
146
|
+
if self._port.isOpen(): # type: ignore
|
|
147
|
+
self._logger.info(f"Adapter {self._worker_descriptor} opened")
|
|
148
|
+
else:
|
|
149
|
+
self._logger.error(f"Failed to open adapter {self._worker_descriptor}")
|
|
150
|
+
raise AdapterOpenError("Unknown error")
|
|
151
|
+
|
|
152
|
+
def _worker_close(self) -> None:
|
|
153
|
+
if self._port is not None:
|
|
154
|
+
self._port.close()
|
|
155
|
+
self._logger.info(f"Adapter {self._worker_descriptor} closed")
|
|
156
|
+
|
|
157
|
+
async def aflush_read(self) -> None:
|
|
158
|
+
await super().aflush_read()
|
|
159
|
+
if self._port is not None:
|
|
160
|
+
self._port.flush()
|
|
161
|
+
|
|
162
|
+
def set_default_baudrate(self, baudrate: int) -> None:
|
|
62
163
|
"""
|
|
63
164
|
Set baudrate
|
|
64
165
|
|
|
@@ -66,15 +167,43 @@ class SerialPort(Adapter):
|
|
|
66
167
|
----------
|
|
67
168
|
baudrate : int
|
|
68
169
|
"""
|
|
69
|
-
if self.
|
|
170
|
+
if self._descriptor.set_default_baudrate(baudrate):
|
|
171
|
+
self._update_descriptor()
|
|
70
172
|
self.close()
|
|
71
173
|
self.open()
|
|
72
174
|
|
|
73
|
-
def
|
|
74
|
-
if self.
|
|
75
|
-
|
|
76
|
-
|
|
175
|
+
def _worker_write(self, data: bytes) -> None:
|
|
176
|
+
if self._rts_cts: # Experimental
|
|
177
|
+
self._port.setRTS(True) # type: ignore
|
|
178
|
+
if self._port is not None:
|
|
179
|
+
try:
|
|
180
|
+
self._port.write(data)
|
|
181
|
+
except (OSError, PortNotOpenError):
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
def _worker_read(self, fragment_timestamp: float) -> Fragment:
|
|
185
|
+
if self._port is None:
|
|
186
|
+
raise AdapterReadError("Cannot read from non-initialized port")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
data = self._port.read_all()
|
|
190
|
+
except (OSError, PortNotOpenError):
|
|
191
|
+
self._logger.debug('Port error -> b""')
|
|
192
|
+
data = None
|
|
193
|
+
|
|
194
|
+
if data is None or data != b"":
|
|
195
|
+
raise AdapterReadError(
|
|
196
|
+
f"Error while reading from {self._worker_descriptor}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return Fragment(data, fragment_timestamp)
|
|
200
|
+
|
|
201
|
+
def _selectable(self) -> HasFileno | None:
|
|
202
|
+
return self._port
|
|
203
|
+
|
|
204
|
+
# def is_opened(self) -> bool:
|
|
205
|
+
# if self._port is not None:
|
|
206
|
+
# if self._port.isOpen(): # type: ignore
|
|
207
|
+
# return True
|
|
77
208
|
|
|
78
|
-
|
|
79
|
-
super().close(force)
|
|
80
|
-
self._logger.info("Adapter closed")
|
|
209
|
+
# return False
|