syndesi 0.1.5__py3-none-any.whl → 0.2.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/adapters/__init__.py +7 -3
- syndesi/adapters/adapter.py +321 -0
- syndesi/adapters/auto.py +45 -0
- syndesi/adapters/iadapter.py +158 -20
- syndesi/adapters/ip.py +106 -40
- syndesi/adapters/ip_server.py +108 -0
- syndesi/adapters/proxy.py +92 -0
- syndesi/adapters/remote.py +18 -0
- syndesi/adapters/serialport.py +158 -0
- syndesi/adapters/stop_conditions.py +173 -0
- syndesi/adapters/timed_queue.py +32 -0
- syndesi/adapters/timeout.py +297 -0
- syndesi/adapters/visa.py +10 -8
- syndesi/api/__init__.py +0 -0
- syndesi/api/api.py +77 -0
- syndesi/protocols/__init__.py +2 -2
- syndesi/protocols/delimited.py +34 -22
- syndesi/protocols/iprotocol.py +2 -2
- syndesi/protocols/protocol.py +17 -0
- syndesi/protocols/raw.py +9 -60
- syndesi/protocols/scpi.py +37 -16
- syndesi/protocols/sdp.py +4 -4
- syndesi/proxy/__init__.py +0 -0
- syndesi/proxy/proxy.py +136 -0
- syndesi/proxy/proxy_api.py +93 -0
- syndesi/tools/log.py +107 -0
- syndesi/tools/logger.py +113 -0
- syndesi/tools/others.py +1 -0
- syndesi/tools/remote_api.py +113 -0
- syndesi/tools/remote_server.py +133 -0
- syndesi/tools/shell.py +111 -0
- syndesi/tools/types.py +24 -18
- {syndesi-0.1.5.dist-info → syndesi-0.2.0.dist-info}/LICENSE +0 -0
- {syndesi-0.1.5.dist-info → syndesi-0.2.0.dist-info}/METADATA +17 -15
- syndesi-0.2.0.dist-info/RECORD +63 -0
- syndesi-0.2.0.dist-info/entry_points.txt +3 -0
- syndesi-0.2.0.dist-info/top_level.txt +1 -0
- syndesi-0.1.5.data/scripts/syndesi +0 -1
- syndesi-0.1.5.dist-info/RECORD +0 -42
- syndesi-0.1.5.dist-info/top_level.txt +0 -2
- {syndesi-0.1.5.dist-info → syndesi-0.2.0.dist-info}/WHEEL +0 -0
syndesi/adapters/ip.py
CHANGED
|
@@ -1,34 +1,79 @@
|
|
|
1
|
-
|
|
2
1
|
import socket
|
|
3
2
|
from enum import Enum
|
|
4
|
-
from .
|
|
5
|
-
from ..tools.types import
|
|
3
|
+
from .adapter import Adapter
|
|
4
|
+
from ..tools.types import to_bytes
|
|
5
|
+
from .timeout import Timeout
|
|
6
|
+
from threading import Thread
|
|
7
|
+
from .timed_queue import TimedQueue
|
|
8
|
+
from typing import Union
|
|
9
|
+
from time import time
|
|
10
|
+
import argparse
|
|
11
|
+
from ..tools import shell
|
|
12
|
+
|
|
13
|
+
class IP(Adapter):
|
|
14
|
+
DEFAULT_RESPONSE_TIMEOUT = 1
|
|
15
|
+
DEFAULT_CONTINUATION_TIMEOUT = 1e-3
|
|
16
|
+
DEFAULT_TOTAL_TIMEOUT = 5
|
|
17
|
+
|
|
6
18
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
DEFAULT_TIMEOUT = Timeout(
|
|
20
|
+
response=DEFAULT_RESPONSE_TIMEOUT,
|
|
21
|
+
continuation=DEFAULT_CONTINUATION_TIMEOUT,
|
|
22
|
+
total=DEFAULT_TOTAL_TIMEOUT)
|
|
23
|
+
DEFAULT_BUFFER_SIZE = 1024
|
|
11
24
|
class Protocol(Enum):
|
|
12
|
-
TCP =
|
|
13
|
-
UDP =
|
|
14
|
-
|
|
25
|
+
TCP = 'TCP'
|
|
26
|
+
UDP = 'UDP'
|
|
27
|
+
|
|
28
|
+
def __init__(self,
|
|
29
|
+
address : str,
|
|
30
|
+
port : int = None,
|
|
31
|
+
transport : str = 'TCP',
|
|
32
|
+
timeout : Union[Timeout, float] = DEFAULT_TIMEOUT,
|
|
33
|
+
stop_condition = None,
|
|
34
|
+
alias : str = '',
|
|
35
|
+
buffer_size : int = DEFAULT_BUFFER_SIZE,
|
|
36
|
+
_socket : socket.socket = None):
|
|
15
37
|
"""
|
|
16
38
|
IP stack adapter
|
|
17
39
|
|
|
18
40
|
Parameters
|
|
19
41
|
----------
|
|
20
|
-
|
|
42
|
+
address : str
|
|
21
43
|
IP description
|
|
22
44
|
port : int
|
|
23
|
-
port
|
|
24
|
-
transport :
|
|
25
|
-
|
|
45
|
+
IP port
|
|
46
|
+
transport : str
|
|
47
|
+
'TCP' or 'UDP'
|
|
48
|
+
timeout : Timeout | float
|
|
49
|
+
Specify communication timeout
|
|
50
|
+
stop_condition : StopCondition
|
|
51
|
+
Specify a read stop condition (None by default)
|
|
52
|
+
alias : str
|
|
53
|
+
Specify an alias for this adapter, '' by default
|
|
54
|
+
buffer_size : int
|
|
55
|
+
Socket buffer size, may be removed in the future
|
|
56
|
+
socket : socket.socket
|
|
57
|
+
Specify a custom socket, this is reserved for server application
|
|
26
58
|
"""
|
|
27
|
-
|
|
28
|
-
self.
|
|
29
|
-
self.
|
|
59
|
+
super().__init__(alias=alias, timeout=timeout, stop_condition=stop_condition)
|
|
60
|
+
self._transport = self.Protocol(transport)
|
|
61
|
+
self._is_server = _socket is not None
|
|
62
|
+
|
|
63
|
+
self._logger.info(f"Setting up {self._transport.value} IP adapter ({'server' if self._is_server else 'client'})")
|
|
64
|
+
|
|
65
|
+
if self._is_server:
|
|
66
|
+
# Server
|
|
67
|
+
self._socket = _socket
|
|
68
|
+
self._status = self.Status.CONNECTED
|
|
69
|
+
elif self._transport == self.Protocol.TCP:
|
|
70
|
+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
71
|
+
elif self._transport == self.Protocol.UDP:
|
|
72
|
+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
73
|
+
|
|
74
|
+
self._address = address
|
|
30
75
|
self._port = port
|
|
31
|
-
self.
|
|
76
|
+
self._buffer_size = buffer_size
|
|
32
77
|
|
|
33
78
|
def set_default_port(self, port):
|
|
34
79
|
"""
|
|
@@ -44,41 +89,62 @@ class IP(IAdapter):
|
|
|
44
89
|
if self._port is None:
|
|
45
90
|
self._port = port
|
|
46
91
|
|
|
47
|
-
|
|
48
|
-
def flushRead(self):
|
|
49
|
-
return super().flushRead()
|
|
50
|
-
|
|
51
92
|
def open(self):
|
|
52
|
-
|
|
93
|
+
if self._is_server:
|
|
94
|
+
raise SystemError("Cannot open server socket. It must be passed already opened")
|
|
95
|
+
if self._port is None:
|
|
96
|
+
raise ValueError(f"Cannot open adapter without specifying a port")
|
|
97
|
+
|
|
98
|
+
self._logger.debug(f"Adapter {self._alias} connect to ({self._address}, {self._port})")
|
|
99
|
+
self._socket.connect((self._address, self._port))
|
|
53
100
|
self._status = self.Status.CONNECTED
|
|
101
|
+
self._logger.info(f"Adapter {self._alias} opened !")
|
|
54
102
|
|
|
55
103
|
def close(self):
|
|
56
|
-
self
|
|
104
|
+
if hasattr(self, '_socket'):
|
|
105
|
+
self._socket.close()
|
|
106
|
+
self._logger.info("Adapter closed !")
|
|
107
|
+
self._status = self.Status.DISCONNECTED
|
|
57
108
|
|
|
58
|
-
def write(self, data : bytes):
|
|
59
|
-
|
|
109
|
+
def write(self, data : Union[bytes, str]):
|
|
110
|
+
data = to_bytes(data)
|
|
60
111
|
if self._status == self.Status.DISCONNECTED:
|
|
112
|
+
self._logger.info(f"Adapter {self._alias} is closed, opening...")
|
|
61
113
|
self.open()
|
|
114
|
+
write_start = time()
|
|
62
115
|
self._socket.send(data)
|
|
116
|
+
write_duration = time() - write_start
|
|
117
|
+
self._logger.debug(f"Written [{write_duration*1e3:.3f}ms]: {repr(data)}")
|
|
63
118
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
119
|
+
def _start_thread(self):
|
|
120
|
+
self._logger.debug("Starting read thread...")
|
|
121
|
+
self._thread = Thread(target=self._read_thread, daemon=True, args=(self._socket, self._read_queue))
|
|
122
|
+
self._thread.start()
|
|
123
|
+
|
|
124
|
+
# EXPERIMENTAL
|
|
125
|
+
def read_thread_alive(self):
|
|
126
|
+
return self._thread.is_alive()
|
|
67
127
|
|
|
68
|
-
self._socket.settimeout(10)
|
|
69
128
|
|
|
70
|
-
|
|
71
|
-
while True:
|
|
129
|
+
def _read_thread(self, socket : socket.socket, read_queue : TimedQueue):
|
|
130
|
+
while True: # TODO : Add stop_pipe ? Maybe it was removed ?
|
|
72
131
|
try:
|
|
73
|
-
|
|
74
|
-
self.
|
|
75
|
-
|
|
76
|
-
except
|
|
132
|
+
payload = socket.recv(self._buffer_size)
|
|
133
|
+
if len(payload) == self._buffer_size and self._transport == self.Protocol.UDP:
|
|
134
|
+
self._logger.warning("Warning, inbound UDP data may have been lost (max buffer size attained)")
|
|
135
|
+
except OSError:
|
|
77
136
|
break
|
|
78
|
-
|
|
137
|
+
# If payload is empty, it means the socket has been disconnected
|
|
138
|
+
if payload == b'':
|
|
139
|
+
read_queue.put(payload)
|
|
140
|
+
break
|
|
141
|
+
read_queue.put(payload)
|
|
79
142
|
|
|
80
|
-
def query(self, data : bytes):
|
|
143
|
+
def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False):
|
|
144
|
+
if self._is_server:
|
|
145
|
+
raise SystemError("Cannot query on server adapters")
|
|
81
146
|
self.flushRead()
|
|
82
147
|
self.write(data)
|
|
83
|
-
return self.read()
|
|
84
|
-
|
|
148
|
+
return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
|
|
149
|
+
|
|
150
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# ip_server.py
|
|
2
|
+
# Sébastien Deriaz
|
|
3
|
+
# 31.05.2024
|
|
4
|
+
|
|
5
|
+
import socket
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from . import IP, StopCondition, Timeout
|
|
8
|
+
from time import time
|
|
9
|
+
from ..tools.types import to_bytes
|
|
10
|
+
from .timeout import timeout_fuse
|
|
11
|
+
import logging
|
|
12
|
+
from ..tools.log import LoggerAlias
|
|
13
|
+
|
|
14
|
+
# NOTE : The adapter is only meant to work with a single client, thus
|
|
15
|
+
# a IPServer class is made to handle the socket and generate IP adapters
|
|
16
|
+
|
|
17
|
+
class IPServer:
|
|
18
|
+
DEFAULT_BUFFER_SIZE = 1024
|
|
19
|
+
class Protocol(Enum):
|
|
20
|
+
TCP = 'TCP'
|
|
21
|
+
UDP = 'UDP'
|
|
22
|
+
|
|
23
|
+
def __init__(self,
|
|
24
|
+
port : int = None,
|
|
25
|
+
transport : str = 'TCP',
|
|
26
|
+
address : str = None,
|
|
27
|
+
max_clients : int = 5,
|
|
28
|
+
stop_condition = None,
|
|
29
|
+
alias : str = '',
|
|
30
|
+
buffer_size : int = DEFAULT_BUFFER_SIZE):
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
IP server adapter
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
port : int
|
|
38
|
+
IP port. If None, the default port set with set_default_port will be used
|
|
39
|
+
transport : str
|
|
40
|
+
'TCP' or 'UDP'
|
|
41
|
+
address : str
|
|
42
|
+
Custom socket ip, None by default
|
|
43
|
+
max_clients : int
|
|
44
|
+
Maximum number of clients, 5 by default
|
|
45
|
+
stop_condition : StopCondition
|
|
46
|
+
Specify a read stop condition (None by default)
|
|
47
|
+
alias : str
|
|
48
|
+
Specify an alias for this adapter, '' by default
|
|
49
|
+
buffer_size : int
|
|
50
|
+
Socket buffer size, may be removed in the future
|
|
51
|
+
"""
|
|
52
|
+
self._alias = alias
|
|
53
|
+
self._stop_condition = stop_condition
|
|
54
|
+
self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
|
|
55
|
+
self._transport = self.Protocol(transport)
|
|
56
|
+
if self._transport == self.Protocol.TCP:
|
|
57
|
+
self._logger.info("Setting up TCP IP server adapter")
|
|
58
|
+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
59
|
+
elif self._transport == self.Protocol.UDP:
|
|
60
|
+
self._logger.info("Setting up UDP IP server adapter")
|
|
61
|
+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError("Invalid protocol")
|
|
64
|
+
|
|
65
|
+
self._address = socket.gethostname() if address is None else address
|
|
66
|
+
self._port = port
|
|
67
|
+
self._max_clients = max_clients
|
|
68
|
+
self._opened = False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def set_default_port(self, port):
|
|
72
|
+
"""
|
|
73
|
+
Sets IP port if no port has been set yet.
|
|
74
|
+
|
|
75
|
+
This way, the user can leave the port empty
|
|
76
|
+
and the driver/protocol can specify it later
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
port : int
|
|
81
|
+
"""
|
|
82
|
+
if self._port is None:
|
|
83
|
+
self._port = port
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def open(self):
|
|
87
|
+
if self._port is None:
|
|
88
|
+
raise ValueError(f"Cannot open adapter without specifying a port")
|
|
89
|
+
self._logger.info(f"Listening to incoming connections on {self._address}:{self._port}")
|
|
90
|
+
self._socket.bind((self._address, self._port))
|
|
91
|
+
self._socket.listen(self._max_clients)
|
|
92
|
+
self._opened = True
|
|
93
|
+
|
|
94
|
+
def close(self):
|
|
95
|
+
if hasattr(self, '_socket'):
|
|
96
|
+
self._socket.close()
|
|
97
|
+
self._logger.info("Adapter closed !")
|
|
98
|
+
self._opened = False
|
|
99
|
+
|
|
100
|
+
def get_client(self, stop_condition : StopCondition = None, timeout : Timeout = None) -> IP:
|
|
101
|
+
"""
|
|
102
|
+
Wait for a client to connect to the server and return the corresponding adapter
|
|
103
|
+
"""
|
|
104
|
+
if not self._opened:
|
|
105
|
+
raise RuntimeError("open() must be called before getting client")
|
|
106
|
+
client_socket, address = self._socket.accept()
|
|
107
|
+
default_timeout = Timeout(response=None, continuation=IP.DEFAULT_CONTINUATION_TIMEOUT, total=None)
|
|
108
|
+
return IP(_socket=client_socket, address=address, stop_condition=stop_condition, timeout=timeout_fuse(timeout, default_timeout))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# proxy.py
|
|
2
|
+
# Sébastien Deriaz
|
|
3
|
+
# 09.04.2024
|
|
4
|
+
#
|
|
5
|
+
# The proxy adapter allows for commands to be issued on a different device
|
|
6
|
+
# The goal is to istanciate a class as such :
|
|
7
|
+
#
|
|
8
|
+
# Only adapter :
|
|
9
|
+
# # The proxy computer is accessed with 192.168.1.1
|
|
10
|
+
# # The device (connected to the remote computer) is accessed with 192.168.2.1
|
|
11
|
+
# my_adapter = Proxy('192.168.1.1', IP('192.168.2.1'))
|
|
12
|
+
#
|
|
13
|
+
# Protocol :
|
|
14
|
+
# my_protocol = SCPI(Proxy('192.168.1.1', Serial('/dev/ttyUSB0')))
|
|
15
|
+
#
|
|
16
|
+
#
|
|
17
|
+
# Driver :
|
|
18
|
+
# my_device = Driver(Proxy('192.168.1.1', VISA('...')))
|
|
19
|
+
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from typing import Union
|
|
22
|
+
|
|
23
|
+
from .adapter import Adapter
|
|
24
|
+
from . import IP, SerialPort, VISA
|
|
25
|
+
from ..proxy.proxy_api import *
|
|
26
|
+
from ..api.api import parse
|
|
27
|
+
|
|
28
|
+
DEFAULT_PORT = 2608
|
|
29
|
+
|
|
30
|
+
class Proxy(Adapter):
|
|
31
|
+
def __init__(self, proxy_adapter : Adapter, remote_adapter : Adapter):
|
|
32
|
+
"""
|
|
33
|
+
Proxy adapter
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
proxy_adapter : Adapter
|
|
38
|
+
Adapter to connect to the proxy server
|
|
39
|
+
remote_adapter : Adapter
|
|
40
|
+
Adapter to instanciate onto the proxy server
|
|
41
|
+
"""
|
|
42
|
+
super().__init__()
|
|
43
|
+
|
|
44
|
+
self._proxy = proxy_adapter
|
|
45
|
+
self._remote = remote_adapter
|
|
46
|
+
|
|
47
|
+
if isinstance(proxy_adapter, IP):
|
|
48
|
+
proxy_adapter.set_default_port(DEFAULT_PORT)
|
|
49
|
+
|
|
50
|
+
if isinstance(self._remote, IP):
|
|
51
|
+
self._proxy.query(IPInstanciate(
|
|
52
|
+
address=self._remote._address,
|
|
53
|
+
port=self._remote._port,
|
|
54
|
+
transport=self._remote._transport,
|
|
55
|
+
buffer_size=self._remote._buffer_size
|
|
56
|
+
).encode())
|
|
57
|
+
elif isinstance(self._remote, SerialPort):
|
|
58
|
+
self._proxy.query()
|
|
59
|
+
|
|
60
|
+
def check(self, status : ReturnStatus):
|
|
61
|
+
if not status.success:
|
|
62
|
+
# There is an error
|
|
63
|
+
raise ProxyException(status.error_message)
|
|
64
|
+
|
|
65
|
+
def open(self):
|
|
66
|
+
if isinstance(self._remote, IP):
|
|
67
|
+
self._proxy.query(AdapterOpen().encode())
|
|
68
|
+
|
|
69
|
+
def close(self):
|
|
70
|
+
self._proxy.query(AdapterClose().encode())
|
|
71
|
+
|
|
72
|
+
def write(self, data : Union[bytes, str]):
|
|
73
|
+
self._proxy.query(AdapterWrite(data).encode())
|
|
74
|
+
|
|
75
|
+
def read(self, timeout=None, stop_condition=None, return_metrics : bool = False):
|
|
76
|
+
output : AdapterReadReturn
|
|
77
|
+
output = parse(self._proxy.query(AdapterRead().encode()))
|
|
78
|
+
if isinstance(output, AdapterReadReturn):
|
|
79
|
+
return output.data
|
|
80
|
+
elif isinstance(output, ReturnStatus):
|
|
81
|
+
raise ProxyException(output.error_message)
|
|
82
|
+
else:
|
|
83
|
+
raise RuntimeError(f"Invalid return : {type(output)}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False):
|
|
87
|
+
self.check(parse(self._proxy.query(AdapterFlushRead().encode())))
|
|
88
|
+
self.check(parse(self._proxy.query(AdapterWrite(data).encode())))
|
|
89
|
+
return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
|
|
90
|
+
|
|
91
|
+
def _start_thread(self):
|
|
92
|
+
pass
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# remote.py
|
|
2
|
+
# Sébastien Deriaz
|
|
3
|
+
# 09.04.2024
|
|
4
|
+
#
|
|
5
|
+
# The remote adapter allows for commands to be issued on a different device through TCP
|
|
6
|
+
# The goal is to istanciate a class as such :
|
|
7
|
+
#
|
|
8
|
+
# Only adapter :
|
|
9
|
+
# # The remote computer is accessed with 192.168.1.1
|
|
10
|
+
# # The device (connected to the remote computer) is accessed with 192.168.2.1
|
|
11
|
+
# my_adapter = Remote('192.168.1.1', IP('192.168.2.1'))
|
|
12
|
+
#
|
|
13
|
+
# Protocol :
|
|
14
|
+
# my_protocol = SCPI(Remote('192.168.1.1', Serial('/dev/ttyUSB0')))
|
|
15
|
+
#
|
|
16
|
+
#
|
|
17
|
+
# Driver :
|
|
18
|
+
# my_device = Driver(Remote('192.168.1.1', VISA('...')))
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import serial
|
|
3
|
+
from threading import Thread
|
|
4
|
+
from typing import Union
|
|
5
|
+
import select
|
|
6
|
+
import argparse
|
|
7
|
+
#from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from .adapter import Adapter
|
|
10
|
+
from ..tools.types import to_bytes
|
|
11
|
+
from .stop_conditions import *
|
|
12
|
+
from .timeout import Timeout
|
|
13
|
+
from .timed_queue import TimedQueue
|
|
14
|
+
from ..tools import shell
|
|
15
|
+
from ..tools.others import DEFAULT
|
|
16
|
+
|
|
17
|
+
# From pyserial - serialposix.py
|
|
18
|
+
import fcntl
|
|
19
|
+
import termios
|
|
20
|
+
import struct
|
|
21
|
+
if hasattr(termios, 'TIOCINQ'):
|
|
22
|
+
TIOCINQ = termios.TIOCINQ
|
|
23
|
+
else:
|
|
24
|
+
TIOCINQ = getattr(termios, 'FIONREAD', 0x541B)
|
|
25
|
+
TIOCM_zero_str = struct.pack('I', 0)
|
|
26
|
+
|
|
27
|
+
DEFAULT_TIMEOUT = Timeout(response=1, continuation=200e-3, total=None)
|
|
28
|
+
|
|
29
|
+
class SerialPort(Adapter):
|
|
30
|
+
def __init__(self,
|
|
31
|
+
port : str,
|
|
32
|
+
baudrate : int,
|
|
33
|
+
timeout : Union[Timeout, float] = DEFAULT,
|
|
34
|
+
stop_condition : StopCondition = DEFAULT,
|
|
35
|
+
rts_cts : bool = False): # rts_cts experimental
|
|
36
|
+
"""
|
|
37
|
+
Serial communication adapter
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
port : str
|
|
42
|
+
Serial port (COMx or ttyACMx)
|
|
43
|
+
"""
|
|
44
|
+
if timeout == DEFAULT:
|
|
45
|
+
timeout = DEFAULT_TIMEOUT
|
|
46
|
+
|
|
47
|
+
super().__init__(timeout=timeout, stop_condition=stop_condition)
|
|
48
|
+
self._logger.info(f"Setting up SerialPort adapter timeout:{timeout}, stop_condition:{stop_condition}")
|
|
49
|
+
self._port = serial.Serial(port=port, baudrate=baudrate)
|
|
50
|
+
if self._port.isOpen():
|
|
51
|
+
self._status = self.Status.CONNECTED
|
|
52
|
+
else:
|
|
53
|
+
self._status = self.Status.DISCONNECTED
|
|
54
|
+
|
|
55
|
+
self._rts_cts = rts_cts
|
|
56
|
+
|
|
57
|
+
self._stop_event_pipe, self._stop_event_pipe_write = os.pipe()
|
|
58
|
+
|
|
59
|
+
def flushRead(self):
|
|
60
|
+
self._port.flush()
|
|
61
|
+
|
|
62
|
+
def open(self):
|
|
63
|
+
self._port.open()
|
|
64
|
+
# Flush the input buffer
|
|
65
|
+
buf = b'0'
|
|
66
|
+
while buf:
|
|
67
|
+
buf = os.read(self._port.fd)
|
|
68
|
+
self._logger.info("Adapter opened !")
|
|
69
|
+
|
|
70
|
+
def close(self):
|
|
71
|
+
if self._thread is not None and self._thread.is_alive():
|
|
72
|
+
os.write(self._stop_event_pipe_write, b'1')
|
|
73
|
+
self._thread.join()
|
|
74
|
+
if hasattr(self, '_port'):
|
|
75
|
+
self._port.close()
|
|
76
|
+
self._logger.info("Adapter closed !")
|
|
77
|
+
|
|
78
|
+
def write(self, data : bytes):
|
|
79
|
+
if self._rts_cts: # Experimental
|
|
80
|
+
self._port.setRTS(True)
|
|
81
|
+
data = to_bytes(data)
|
|
82
|
+
if self._status == self.Status.DISCONNECTED:
|
|
83
|
+
self.open()
|
|
84
|
+
write_start = time()
|
|
85
|
+
self._port.write(data)
|
|
86
|
+
write_duration = time() - write_start
|
|
87
|
+
self._logger.debug(f"Written [{write_duration*1e3:.3f}ms]: {repr(data)}")
|
|
88
|
+
|
|
89
|
+
def _start_thread(self):
|
|
90
|
+
"""
|
|
91
|
+
Start the read thread
|
|
92
|
+
"""
|
|
93
|
+
self._logger.debug("Starting read thread...")
|
|
94
|
+
if self._thread is None or not self._thread.is_alive():
|
|
95
|
+
self._thread = Thread(target=self._read_thread, daemon=True, args=(self._port, self._read_queue, self._stop_event_pipe))
|
|
96
|
+
self._thread.start()
|
|
97
|
+
|
|
98
|
+
def _read_thread(self, port : serial.Serial , read_queue : TimedQueue, stop_event_pipe):
|
|
99
|
+
while True:
|
|
100
|
+
# It looks like using the raw implementation of port.in_waiting and port.read is better, there's no more warnings
|
|
101
|
+
# Equivalent of port.in_waiting :
|
|
102
|
+
#in_waiting = struct.unpack('I', fcntl.ioctl(port.fd, TIOCINQ, TIOCM_zero_str))[0]
|
|
103
|
+
in_waiting = self._port.in_waiting # This is a temporary fix to get windows compatiblity back, an error might pop up
|
|
104
|
+
if in_waiting == 0:
|
|
105
|
+
ready, _, _ = select.select([port.fd, stop_event_pipe], [], [], None)
|
|
106
|
+
if stop_event_pipe in ready:
|
|
107
|
+
# Stop
|
|
108
|
+
break
|
|
109
|
+
# Else, read as many bytes as possible
|
|
110
|
+
fragment = os.read(port.fd, 1000) # simplified version of port.read()
|
|
111
|
+
if fragment:
|
|
112
|
+
read_queue.put(fragment)
|
|
113
|
+
|
|
114
|
+
def read(self, timeout=None, stop_condition=None, return_metrics: bool = False) -> bytes:
|
|
115
|
+
"""
|
|
116
|
+
Read data from the device
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
timeout : Timeout or None
|
|
121
|
+
Set a custom timeout, if None (default), the adapter timeout is used
|
|
122
|
+
stop_condition : StopCondition or None
|
|
123
|
+
Set a custom stop condition, if None (Default), the adapater stop condition is used
|
|
124
|
+
return_metrics : bool
|
|
125
|
+
Return a dictionary containing information about the read operation like
|
|
126
|
+
'read_duration' : float
|
|
127
|
+
'origin' : 'timeout' or 'stop_condition'
|
|
128
|
+
'timeout' : Timeout.TimeoutType
|
|
129
|
+
'stop_condition' : Length or Termination (StopCondition class)
|
|
130
|
+
'previous_read_buffer_used' : bool
|
|
131
|
+
'n_fragments' : int
|
|
132
|
+
"""
|
|
133
|
+
output = super().read(timeout, stop_condition, return_metrics)
|
|
134
|
+
if self._rts_cts: # Experimental
|
|
135
|
+
self._port.setRTS(False)
|
|
136
|
+
return output
|
|
137
|
+
|
|
138
|
+
def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False):
|
|
139
|
+
self.flushRead()
|
|
140
|
+
self.write(data)
|
|
141
|
+
return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
|
|
142
|
+
|
|
143
|
+
def shell_parse(inp: str):
|
|
144
|
+
parser = argparse.ArgumentParser(
|
|
145
|
+
prog='',
|
|
146
|
+
description='Serial port shell parser',
|
|
147
|
+
epilog='')
|
|
148
|
+
# Parse subcommand
|
|
149
|
+
parser.add_argument('--' + shell.Arguments.PORT.value, type=str)
|
|
150
|
+
parser.add_argument('--' + shell.Arguments.BAUDRATE.value, type=int)
|
|
151
|
+
parser.add_argument('--' + shell.Arguments.ENABLE_RTS_CTS.value, action='store_true')
|
|
152
|
+
args = parser.parse_args(inp.split())
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
'port' : getattr(args, shell.Arguments.PORT.value),
|
|
156
|
+
'baudrate' : getattr(args, shell.Arguments.BAUDRATE.value),
|
|
157
|
+
'rts_cts' : bool(getattr(args, shell.Arguments.ENABLE_RTS_CTS.value))
|
|
158
|
+
}
|