syndesi 0.1.6__py3-none-any.whl → 0.2.1__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 +2 -1
- syndesi/adapters/adapter.py +47 -30
- syndesi/adapters/ip.py +52 -26
- syndesi/adapters/proxy.py +92 -0
- syndesi/adapters/serialport.py +43 -13
- syndesi/adapters/stop_conditions.py +20 -2
- syndesi/adapters/timeout.py +15 -5
- syndesi/api/__init__.py +0 -0
- syndesi/api/api.py +77 -0
- syndesi/protocols/delimited.py +23 -36
- syndesi/protocols/scpi.py +3 -2
- syndesi/proxy/__init__.py +0 -0
- syndesi/proxy/proxy.py +136 -0
- syndesi/proxy/proxy_api.py +93 -0
- syndesi/tools/log.py +1 -0
- syndesi/tools/others.py +1 -10
- syndesi/tools/types.py +1 -1
- {syndesi-0.1.6.dist-info → syndesi-0.2.1.dist-info}/LICENSE +0 -0
- {syndesi-0.1.6.dist-info → syndesi-0.2.1.dist-info}/METADATA +1 -1
- {syndesi-0.1.6.dist-info → syndesi-0.2.1.dist-info}/RECORD +23 -17
- syndesi-0.2.1.dist-info/entry_points.txt +3 -0
- syndesi-0.1.6.data/scripts/syndesi +0 -57
- {syndesi-0.1.6.dist-info → syndesi-0.2.1.dist-info}/WHEEL +0 -0
- {syndesi-0.1.6.dist-info → syndesi-0.2.1.dist-info}/top_level.txt +0 -0
syndesi/adapters/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@ from .adapter import Adapter
|
|
|
2
2
|
from .ip import IP
|
|
3
3
|
from .serialport import SerialPort
|
|
4
4
|
from .visa import VISA
|
|
5
|
+
from .proxy import Proxy
|
|
5
6
|
|
|
6
7
|
from .timeout import Timeout
|
|
7
|
-
from .stop_conditions import Termination, Length, StopCondition
|
|
8
|
+
from .stop_conditions import Termination, Length, StopCondition
|
syndesi/adapters/adapter.py
CHANGED
|
@@ -29,10 +29,14 @@ from ..tools.log import LoggerAlias
|
|
|
29
29
|
import logging
|
|
30
30
|
from time import time
|
|
31
31
|
from dataclasses import dataclass
|
|
32
|
-
from ..tools.others import
|
|
32
|
+
from ..tools.others import DEFAULT
|
|
33
33
|
|
|
34
|
-
DEFAULT_TIMEOUT =
|
|
35
|
-
DEFAULT_STOP_CONDITION =
|
|
34
|
+
DEFAULT_TIMEOUT = Timeout(response=1, continuation=100e-3, total=None)
|
|
35
|
+
DEFAULT_STOP_CONDITION = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AdapterDisconnected(Exception):
|
|
39
|
+
pass
|
|
36
40
|
|
|
37
41
|
STOP_DESIGNATORS = {
|
|
38
42
|
'timeout' : {
|
|
@@ -68,10 +72,7 @@ class Adapter(ABC):
|
|
|
68
72
|
DISCONNECTED = 0
|
|
69
73
|
CONNECTED = 1
|
|
70
74
|
|
|
71
|
-
def __init__(self,
|
|
72
|
-
alias : str = '',
|
|
73
|
-
timeout : Union[float, Timeout] = DEFAULT_TIMEOUT,
|
|
74
|
-
stop_condition : Union[StopCondition, None] = DEFAULT_STOP_CONDITION):
|
|
75
|
+
def __init__(self, alias : str = '', stop_condition : Union[StopCondition, None] = DEFAULT, timeout : Union[float, Timeout] = DEFAULT) -> None:
|
|
75
76
|
"""
|
|
76
77
|
Adapter instance
|
|
77
78
|
|
|
@@ -84,24 +85,33 @@ class Adapter(ABC):
|
|
|
84
85
|
stop_condition : StopCondition or None
|
|
85
86
|
Default to None
|
|
86
87
|
"""
|
|
88
|
+
super().__init__()
|
|
89
|
+
self._alias = alias
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
self._timeout = timeout
|
|
91
|
+
self._default_stop_condition = stop_condition == DEFAULT
|
|
92
|
+
if self._default_stop_condition:
|
|
93
|
+
self._stop_condition = DEFAULT_STOP_CONDITION
|
|
92
94
|
else:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
self._stop_condition = stop_condition
|
|
95
|
+
self._stop_condition = stop_condition
|
|
96
96
|
self._read_queue = TimedQueue()
|
|
97
97
|
self._thread : Union[Thread, None] = None
|
|
98
98
|
self._status = self.Status.DISCONNECTED
|
|
99
|
+
self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
|
|
100
|
+
|
|
99
101
|
# Buffer for data that has been pulled from the queue but
|
|
100
102
|
# not used because of termination or length stop condition
|
|
101
103
|
self._previous_read_buffer = b''
|
|
102
104
|
|
|
103
|
-
self.
|
|
104
|
-
self.
|
|
105
|
+
self._default_timeout = timeout == DEFAULT
|
|
106
|
+
if self._default_timeout:
|
|
107
|
+
self._timeout = DEFAULT_TIMEOUT
|
|
108
|
+
else:
|
|
109
|
+
if is_number(timeout):
|
|
110
|
+
self._timeout = Timeout(response=timeout, continuation=100e-3)
|
|
111
|
+
elif isinstance(timeout, Timeout):
|
|
112
|
+
self._timeout = timeout
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError(f"Invalid timeout type : {type(timeout)}")
|
|
105
115
|
|
|
106
116
|
def set_default_timeout(self, default_timeout : Union[Timeout, tuple, float]):
|
|
107
117
|
"""
|
|
@@ -111,7 +121,10 @@ class Adapter(ABC):
|
|
|
111
121
|
----------
|
|
112
122
|
default_timeout : Timeout or tuple or float
|
|
113
123
|
"""
|
|
114
|
-
|
|
124
|
+
if self._default_timeout:
|
|
125
|
+
self._timeout = default_timeout
|
|
126
|
+
else:
|
|
127
|
+
self._timeout = timeout_fuse(self._timeout, default_timeout)
|
|
115
128
|
|
|
116
129
|
def set_default_stop_condition(self, stop_condition):
|
|
117
130
|
"""
|
|
@@ -121,9 +134,8 @@ class Adapter(ABC):
|
|
|
121
134
|
----------
|
|
122
135
|
stop_condition : StopCondition
|
|
123
136
|
"""
|
|
124
|
-
if
|
|
137
|
+
if self._default_stop_condition:
|
|
125
138
|
self._stop_condition = stop_condition
|
|
126
|
-
|
|
127
139
|
|
|
128
140
|
def flushRead(self):
|
|
129
141
|
"""
|
|
@@ -145,7 +157,7 @@ class Adapter(ABC):
|
|
|
145
157
|
Stop communication with the device
|
|
146
158
|
"""
|
|
147
159
|
pass
|
|
148
|
-
|
|
160
|
+
|
|
149
161
|
@abstractmethod
|
|
150
162
|
def write(self, data : Union[bytes, str]):
|
|
151
163
|
"""
|
|
@@ -156,14 +168,11 @@ class Adapter(ABC):
|
|
|
156
168
|
data : bytes or str
|
|
157
169
|
"""
|
|
158
170
|
pass
|
|
159
|
-
|
|
160
|
-
@abstractmethod
|
|
161
|
-
def _start_thread(self):
|
|
162
|
-
"""
|
|
163
|
-
Initiate the read thread
|
|
164
|
-
"""
|
|
165
|
-
pass
|
|
166
171
|
|
|
172
|
+
# TODO : Return None or b'' when read thread is killed while reading
|
|
173
|
+
# This is to detect if a server socket has been closed
|
|
174
|
+
|
|
175
|
+
|
|
167
176
|
def read(self, timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
|
|
168
177
|
"""
|
|
169
178
|
Read data from the device
|
|
@@ -222,6 +231,9 @@ class Adapter(ABC):
|
|
|
222
231
|
(timestamp, fragment) = self._read_queue.get(timeout_ms)
|
|
223
232
|
n_fragments += 1
|
|
224
233
|
|
|
234
|
+
if fragment == b'':
|
|
235
|
+
raise AdapterDisconnected()
|
|
236
|
+
|
|
225
237
|
# 1) Evaluate the timeout
|
|
226
238
|
stop, timeout_ms = timeout.evaluate(timestamp)
|
|
227
239
|
if stop:
|
|
@@ -290,6 +302,13 @@ class Adapter(ABC):
|
|
|
290
302
|
else:
|
|
291
303
|
return output
|
|
292
304
|
|
|
305
|
+
@abstractmethod
|
|
306
|
+
def _start_thread(self):
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
def __del__(self):
|
|
310
|
+
self.close()
|
|
311
|
+
|
|
293
312
|
@abstractmethod
|
|
294
313
|
def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
|
|
295
314
|
"""
|
|
@@ -299,6 +318,4 @@ class Adapter(ABC):
|
|
|
299
318
|
- read
|
|
300
319
|
"""
|
|
301
320
|
pass
|
|
302
|
-
|
|
303
|
-
def __del__(self):
|
|
304
|
-
self.close()
|
|
321
|
+
|
syndesi/adapters/ip.py
CHANGED
|
@@ -7,22 +7,24 @@ from threading import Thread
|
|
|
7
7
|
from .timed_queue import TimedQueue
|
|
8
8
|
from typing import Union
|
|
9
9
|
from time import time
|
|
10
|
+
import argparse
|
|
11
|
+
from ..tools import shell
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
DEFAULT_BUFFER_SIZE = 1024
|
|
13
|
+
class IP(Adapter):
|
|
14
|
+
DEFAULT_RESPONSE_TIMEOUT = 1
|
|
15
|
+
DEFAULT_CONTINUATION_TIMEOUT = 1e-3
|
|
16
|
+
DEFAULT_TOTAL_TIMEOUT = 5
|
|
16
17
|
|
|
17
|
-
DEFAULT_TIMEOUT = Timeout(
|
|
18
|
-
response=DEFAULT_RESPONSE_TIMEOUT,
|
|
19
|
-
continuation=DEFAULT_CONTINUATION_TIMEOUT,
|
|
20
|
-
total=DEFAULT_TOTAL_TIMEOUT)
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
DEFAULT_TIMEOUT = Timeout(
|
|
20
|
+
response=DEFAULT_RESPONSE_TIMEOUT,
|
|
21
|
+
continuation=DEFAULT_CONTINUATION_TIMEOUT,
|
|
22
|
+
total=DEFAULT_TOTAL_TIMEOUT)
|
|
23
|
+
DEFAULT_BUFFER_SIZE = 1024
|
|
23
24
|
class Protocol(Enum):
|
|
24
25
|
TCP = 'TCP'
|
|
25
26
|
UDP = 'UDP'
|
|
27
|
+
|
|
26
28
|
def __init__(self,
|
|
27
29
|
address : str,
|
|
28
30
|
port : int = None,
|
|
@@ -30,7 +32,8 @@ class IP(Adapter):
|
|
|
30
32
|
timeout : Union[Timeout, float] = DEFAULT_TIMEOUT,
|
|
31
33
|
stop_condition = None,
|
|
32
34
|
alias : str = '',
|
|
33
|
-
buffer_size : int = DEFAULT_BUFFER_SIZE
|
|
35
|
+
buffer_size : int = DEFAULT_BUFFER_SIZE,
|
|
36
|
+
_socket : socket.socket = None):
|
|
34
37
|
"""
|
|
35
38
|
IP stack adapter
|
|
36
39
|
|
|
@@ -50,17 +53,24 @@ class IP(Adapter):
|
|
|
50
53
|
Specify an alias for this adapter, '' by default
|
|
51
54
|
buffer_size : int
|
|
52
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
|
|
53
58
|
"""
|
|
54
59
|
super().__init__(alias=alias, timeout=timeout, stop_condition=stop_condition)
|
|
55
60
|
self._transport = self.Protocol(transport)
|
|
56
|
-
|
|
57
|
-
|
|
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:
|
|
58
70
|
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
59
71
|
elif self._transport == self.Protocol.UDP:
|
|
60
|
-
self._logger.info("Setting up UDP IP adapter")
|
|
61
72
|
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
62
|
-
|
|
63
|
-
raise ValueError("Invalid protocol")
|
|
73
|
+
|
|
64
74
|
self._address = address
|
|
65
75
|
self._port = port
|
|
66
76
|
self._buffer_size = buffer_size
|
|
@@ -80,45 +90,61 @@ class IP(Adapter):
|
|
|
80
90
|
self._port = port
|
|
81
91
|
|
|
82
92
|
def open(self):
|
|
93
|
+
if self._is_server:
|
|
94
|
+
raise SystemError("Cannot open server socket. It must be passed already opened")
|
|
83
95
|
if self._port is None:
|
|
84
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})")
|
|
85
99
|
self._socket.connect((self._address, self._port))
|
|
86
100
|
self._status = self.Status.CONNECTED
|
|
87
|
-
self._logger.info("Adapter opened !")
|
|
101
|
+
self._logger.info(f"Adapter {self._alias} opened !")
|
|
88
102
|
|
|
89
103
|
def close(self):
|
|
90
104
|
if hasattr(self, '_socket'):
|
|
91
105
|
self._socket.close()
|
|
92
106
|
self._logger.info("Adapter closed !")
|
|
107
|
+
self._status = self.Status.DISCONNECTED
|
|
93
108
|
|
|
94
109
|
def write(self, data : Union[bytes, str]):
|
|
95
110
|
data = to_bytes(data)
|
|
96
111
|
if self._status == self.Status.DISCONNECTED:
|
|
97
|
-
self._logger.info("Adapter is closed, opening...")
|
|
112
|
+
self._logger.info(f"Adapter {self._alias} is closed, opening...")
|
|
98
113
|
self.open()
|
|
99
114
|
write_start = time()
|
|
100
115
|
self._socket.send(data)
|
|
101
116
|
write_duration = time() - write_start
|
|
102
117
|
self._logger.debug(f"Written [{write_duration*1e3:.3f}ms]: {repr(data)}")
|
|
103
118
|
|
|
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()
|
|
127
|
+
|
|
128
|
+
|
|
104
129
|
def _read_thread(self, socket : socket.socket, read_queue : TimedQueue):
|
|
105
|
-
while True:
|
|
130
|
+
while True: # TODO : Add stop_pipe ? Maybe it was removed ?
|
|
106
131
|
try:
|
|
107
132
|
payload = socket.recv(self._buffer_size)
|
|
108
133
|
if len(payload) == self._buffer_size and self._transport == self.Protocol.UDP:
|
|
109
134
|
self._logger.warning("Warning, inbound UDP data may have been lost (max buffer size attained)")
|
|
110
135
|
except OSError:
|
|
111
136
|
break
|
|
112
|
-
|
|
137
|
+
# If payload is empty, it means the socket has been disconnected
|
|
138
|
+
if payload == b'':
|
|
139
|
+
read_queue.put(payload)
|
|
113
140
|
break
|
|
114
141
|
read_queue.put(payload)
|
|
115
142
|
|
|
116
|
-
def _start_thread(self):
|
|
117
|
-
self._logger.debug("Starting read thread...")
|
|
118
|
-
self._thread = Thread(target=self._read_thread, daemon=True, args=(self._socket, self._read_queue))
|
|
119
|
-
self._thread.start()
|
|
120
|
-
|
|
121
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")
|
|
122
146
|
self.flushRead()
|
|
123
147
|
self.write(data)
|
|
124
|
-
return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
|
|
148
|
+
return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
|
|
149
|
+
|
|
150
|
+
|
|
@@ -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
|
syndesi/adapters/serialport.py
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import os
|
|
2
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
|
|
3
10
|
from ..tools.types import to_bytes
|
|
4
11
|
from .stop_conditions import *
|
|
5
12
|
from .timeout import Timeout
|
|
6
13
|
from .timed_queue import TimedQueue
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
import select
|
|
14
|
+
from ..tools import shell
|
|
15
|
+
from ..tools.others import DEFAULT
|
|
10
16
|
|
|
11
17
|
# From pyserial - serialposix.py
|
|
12
18
|
import fcntl
|
|
@@ -17,16 +23,15 @@ if hasattr(termios, 'TIOCINQ'):
|
|
|
17
23
|
else:
|
|
18
24
|
TIOCINQ = getattr(termios, 'FIONREAD', 0x541B)
|
|
19
25
|
TIOCM_zero_str = struct.pack('I', 0)
|
|
20
|
-
import os
|
|
21
26
|
|
|
22
|
-
DEFAULT_TIMEOUT = Timeout(response=
|
|
27
|
+
DEFAULT_TIMEOUT = Timeout(response=1, continuation=200e-3, total=None)
|
|
23
28
|
|
|
24
29
|
class SerialPort(Adapter):
|
|
25
30
|
def __init__(self,
|
|
26
31
|
port : str,
|
|
27
32
|
baudrate : int,
|
|
28
|
-
timeout : Union[Timeout, float] =
|
|
29
|
-
stop_condition : StopCondition =
|
|
33
|
+
timeout : Union[Timeout, float] = DEFAULT,
|
|
34
|
+
stop_condition : StopCondition = DEFAULT,
|
|
30
35
|
rts_cts : bool = False): # rts_cts experimental
|
|
31
36
|
"""
|
|
32
37
|
Serial communication adapter
|
|
@@ -36,8 +41,11 @@ class SerialPort(Adapter):
|
|
|
36
41
|
port : str
|
|
37
42
|
Serial port (COMx or ttyACMx)
|
|
38
43
|
"""
|
|
44
|
+
if timeout == DEFAULT:
|
|
45
|
+
timeout = DEFAULT_TIMEOUT
|
|
46
|
+
|
|
39
47
|
super().__init__(timeout=timeout, stop_condition=stop_condition)
|
|
40
|
-
self._logger.info("Setting up SerialPort adapter")
|
|
48
|
+
self._logger.info(f"Setting up SerialPort adapter timeout:{timeout}, stop_condition:{stop_condition}")
|
|
41
49
|
self._port = serial.Serial(port=port, baudrate=baudrate)
|
|
42
50
|
if self._port.isOpen():
|
|
43
51
|
self._status = self.Status.CONNECTED
|
|
@@ -60,10 +68,11 @@ class SerialPort(Adapter):
|
|
|
60
68
|
self._logger.info("Adapter opened !")
|
|
61
69
|
|
|
62
70
|
def close(self):
|
|
63
|
-
if self._thread.is_alive():
|
|
71
|
+
if self._thread is not None and self._thread.is_alive():
|
|
64
72
|
os.write(self._stop_event_pipe_write, b'1')
|
|
65
73
|
self._thread.join()
|
|
66
|
-
self
|
|
74
|
+
if hasattr(self, '_port'):
|
|
75
|
+
self._port.close()
|
|
67
76
|
self._logger.info("Adapter closed !")
|
|
68
77
|
|
|
69
78
|
def write(self, data : bytes):
|
|
@@ -78,6 +87,9 @@ class SerialPort(Adapter):
|
|
|
78
87
|
self._logger.debug(f"Written [{write_duration*1e3:.3f}ms]: {repr(data)}")
|
|
79
88
|
|
|
80
89
|
def _start_thread(self):
|
|
90
|
+
"""
|
|
91
|
+
Start the read thread
|
|
92
|
+
"""
|
|
81
93
|
self._logger.debug("Starting read thread...")
|
|
82
94
|
if self._thread is None or not self._thread.is_alive():
|
|
83
95
|
self._thread = Thread(target=self._read_thread, daemon=True, args=(self._port, self._read_queue, self._stop_event_pipe))
|
|
@@ -87,7 +99,8 @@ class SerialPort(Adapter):
|
|
|
87
99
|
while True:
|
|
88
100
|
# It looks like using the raw implementation of port.in_waiting and port.read is better, there's no more warnings
|
|
89
101
|
# Equivalent of port.in_waiting :
|
|
90
|
-
in_waiting = struct.unpack('I', fcntl.ioctl(port.fd, TIOCINQ, TIOCM_zero_str))[0]
|
|
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
|
|
91
104
|
if in_waiting == 0:
|
|
92
105
|
ready, _, _ = select.select([port.fd, stop_event_pipe], [], [], None)
|
|
93
106
|
if stop_event_pipe in ready:
|
|
@@ -125,4 +138,21 @@ class SerialPort(Adapter):
|
|
|
125
138
|
def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False):
|
|
126
139
|
self.flushRead()
|
|
127
140
|
self.write(data)
|
|
128
|
-
return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
|
|
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
|
+
}
|
|
@@ -60,7 +60,13 @@ class Termination(StopCondition):
|
|
|
60
60
|
sequence : bytes
|
|
61
61
|
"""
|
|
62
62
|
super().__init__()
|
|
63
|
-
|
|
63
|
+
if isinstance(sequence, str):
|
|
64
|
+
self._termination = sequence.encode('utf-8')
|
|
65
|
+
elif isinstance(sequence, bytes):
|
|
66
|
+
self._termination = sequence
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Invalid termination sequence type : {type(sequence)}")
|
|
69
|
+
|
|
64
70
|
#self._fragment_store = b''
|
|
65
71
|
#self._sequence_index = 0
|
|
66
72
|
|
|
@@ -128,6 +134,12 @@ class Termination(StopCondition):
|
|
|
128
134
|
self._fragment_store = b''
|
|
129
135
|
return output
|
|
130
136
|
|
|
137
|
+
def __repr__(self) -> str:
|
|
138
|
+
return self.__str__()
|
|
139
|
+
|
|
140
|
+
def __str__(self) -> str:
|
|
141
|
+
return f'Termination({repr(self._termination)})'
|
|
142
|
+
|
|
131
143
|
class Length(StopCondition):
|
|
132
144
|
def __init__(self, N : int) -> None:
|
|
133
145
|
"""
|
|
@@ -152,4 +164,10 @@ class Length(StopCondition):
|
|
|
152
164
|
deferred_fragment = data[remaining_bytes:]
|
|
153
165
|
self._counter += len(kept_fragment)
|
|
154
166
|
remaining_bytes = self._N - self._counter
|
|
155
|
-
return remaining_bytes == 0, kept_fragment, deferred_fragment
|
|
167
|
+
return remaining_bytes == 0, kept_fragment, deferred_fragment
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
return self.__str__()
|
|
171
|
+
|
|
172
|
+
def __str__(self) -> str:
|
|
173
|
+
return f'Length({self._N})'
|
syndesi/adapters/timeout.py
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from typing import Union, Tuple
|
|
7
7
|
from time import time
|
|
8
|
-
from ..tools.others import is_default_argument
|
|
8
|
+
#from ..tools.others import is_default_argument
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Timeout():
|
|
@@ -235,6 +235,15 @@ class Timeout():
|
|
|
235
235
|
"""
|
|
236
236
|
return self._data_strategy, self._last_data_strategy_origin
|
|
237
237
|
|
|
238
|
+
def __str__(self) -> str:
|
|
239
|
+
response = f'r:{self._response:.3f}ms/{self._on_response},' if self._response is not None else ''
|
|
240
|
+
continuation = f'c:{self._continuation:.3f}ms/{self._on_continuation},' if self._continuation is not None else ''
|
|
241
|
+
total = f't:{self._total:.3f}ms/{self._on_total}' if self._total is not None else ''
|
|
242
|
+
return f'Timeout({response}{continuation}{total})'
|
|
243
|
+
|
|
244
|
+
def __repr__(self) -> str:
|
|
245
|
+
return self.__str__()
|
|
246
|
+
|
|
238
247
|
|
|
239
248
|
class TimeoutException(Exception):
|
|
240
249
|
def __init__(self, type : Timeout.TimeoutType) -> None:
|
|
@@ -264,10 +273,11 @@ def timeout_fuse(high_priority, low_priority):
|
|
|
264
273
|
low = low_priority if isinstance(low_priority, Timeout) else Timeout(low_priority)
|
|
265
274
|
|
|
266
275
|
# 3) If one is the default, take the other
|
|
267
|
-
if is_default_argument(high):
|
|
268
|
-
|
|
269
|
-
if is_default_argument(low):
|
|
270
|
-
|
|
276
|
+
# if is_default_argument(high):
|
|
277
|
+
# return low
|
|
278
|
+
# if is_default_argument(low):
|
|
279
|
+
# return high
|
|
280
|
+
# 05.06.2024 : Removed because is_default_argument is obsolete, use DEFAULT is necessary
|
|
271
281
|
|
|
272
282
|
new_attr = {}
|
|
273
283
|
# 4) Select with parameter to keep based on where it has been set
|
syndesi/api/__init__.py
ADDED
|
File without changes
|
syndesi/api/api.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import quopri
|
|
2
|
+
from typing import Tuple, Union, Dict
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from dataclasses import dataclass, fields, Field
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
APIS = {}
|
|
8
|
+
|
|
9
|
+
def register_api(apis : dict):
|
|
10
|
+
"""
|
|
11
|
+
Register apis
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
apis : dict
|
|
16
|
+
{'action' : APICall} class dictionary
|
|
17
|
+
"""
|
|
18
|
+
APIS.update(apis)
|
|
19
|
+
|
|
20
|
+
ACTION_ATTRIBUTE = 'action'
|
|
21
|
+
|
|
22
|
+
class APIItem:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
class APICall:
|
|
26
|
+
action = ''
|
|
27
|
+
_keyword = ''
|
|
28
|
+
|
|
29
|
+
def encode(self) -> bytes:
|
|
30
|
+
cls_fields: Tuple[Field, ...] = fields(self)
|
|
31
|
+
data = {}
|
|
32
|
+
|
|
33
|
+
# Add action field
|
|
34
|
+
data[ACTION_ATTRIBUTE] = self.action
|
|
35
|
+
# Add other fields
|
|
36
|
+
for field in cls_fields:
|
|
37
|
+
field_data = getattr(self, field.name)
|
|
38
|
+
if isinstance(field_data, Enum):
|
|
39
|
+
entry = field_data.value
|
|
40
|
+
elif isinstance(field_data, bytes):
|
|
41
|
+
entry = quopri.encodestring(field_data).decode('ASCII')
|
|
42
|
+
elif isinstance(field_data, str):
|
|
43
|
+
entry = quopri.encodestring(field_data.encode('utf-8')).decode('ASCII')
|
|
44
|
+
else:
|
|
45
|
+
entry = field_data
|
|
46
|
+
|
|
47
|
+
data[field.name] = entry
|
|
48
|
+
return json.dumps(data)
|
|
49
|
+
|
|
50
|
+
def parse(data : Union[str, bytes]) -> APICall:
|
|
51
|
+
json_data = json.loads(data)
|
|
52
|
+
action = json_data[ACTION_ATTRIBUTE]
|
|
53
|
+
json_data.pop(ACTION_ATTRIBUTE)
|
|
54
|
+
arguments = json_data
|
|
55
|
+
# Find the right API call and return it
|
|
56
|
+
if action not in APIS:
|
|
57
|
+
raise RuntimeError(f"API action '{action}' not registered")
|
|
58
|
+
|
|
59
|
+
converted_arguments = {}
|
|
60
|
+
# Convert each argument according to the class field types
|
|
61
|
+
api_fields: Tuple[Field, ...] = fields(APIS[action])
|
|
62
|
+
|
|
63
|
+
for field in api_fields:
|
|
64
|
+
if field.name not in arguments:
|
|
65
|
+
raise RuntimeError(f"Field '{field.name}' missing from arguments")
|
|
66
|
+
|
|
67
|
+
if field.type == bytes:
|
|
68
|
+
# Convert back
|
|
69
|
+
converted_arguments[field.name] = quopri.decodestring(arguments[field.name])
|
|
70
|
+
elif field.type == str:
|
|
71
|
+
converted_arguments[field.name] = quopri.decodestring(arguments[field.name]).decode('utf-8')
|
|
72
|
+
elif field.type == Enum:
|
|
73
|
+
converted_arguments[field.name] = field.type(arguments[field.name])
|
|
74
|
+
else:
|
|
75
|
+
converted_arguments[field.name] = arguments[field.name]
|
|
76
|
+
|
|
77
|
+
return APIS[action](**converted_arguments)
|
syndesi/protocols/delimited.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from .protocol import Protocol
|
|
2
|
-
from ..adapters import Adapter
|
|
2
|
+
from ..adapters import Adapter, Timeout, Termination
|
|
3
3
|
from ..tools.types import assert_byte_instance, assert_byte_instance
|
|
4
4
|
from time import time
|
|
5
5
|
import warnings
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Delimited(Protocol):
|
|
9
|
-
def __init__(self, adapter : Adapter, termination='\n', format_response=True) -> None:
|
|
9
|
+
def __init__(self, adapter : Adapter, termination='\n', format_response=True, encoding : str = 'utf-8', timeout : Timeout = None) -> None:
|
|
10
10
|
"""
|
|
11
11
|
Protocol with delimiter, like LF, CR, etc... '\\n' is used by default
|
|
12
12
|
|
|
@@ -15,18 +15,21 @@ class Delimited(Protocol):
|
|
|
15
15
|
Parameters
|
|
16
16
|
----------
|
|
17
17
|
adapter : IAdapter
|
|
18
|
-
|
|
18
|
+
termination : bytes
|
|
19
19
|
Command termination, '\\n' by default
|
|
20
20
|
format_response : bool
|
|
21
|
-
Apply formatting to the response (i.e removing the termination)
|
|
21
|
+
Apply formatting to the response (i.e removing the termination), True by default
|
|
22
|
+
encoding : str
|
|
23
|
+
timeout : Timeout
|
|
24
|
+
None by default (default timeout)
|
|
22
25
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# Temporary solution before implementing stop conditions
|
|
26
|
-
self._buffer = ''
|
|
26
|
+
adapter.set_default_stop_condition(stop_condition=Termination(sequence=termination))
|
|
27
|
+
super().__init__(adapter, timeout=timeout)
|
|
27
28
|
|
|
28
29
|
if not isinstance(termination, str):
|
|
29
30
|
raise ValueError(f"end argument must be of type str, not {type(termination)}")
|
|
31
|
+
|
|
32
|
+
self._encoding = encoding
|
|
30
33
|
self._termination = termination
|
|
31
34
|
self._response_formatting = format_response
|
|
32
35
|
|
|
@@ -62,40 +65,24 @@ class Delimited(Protocol):
|
|
|
62
65
|
self.write(command)
|
|
63
66
|
return self.read()
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
# adapter will take care of that using the stop conditions
|
|
67
|
-
#
|
|
68
|
-
# For now the delimited module will take care of it
|
|
69
|
-
#
|
|
70
|
-
# Stop conditions should also be added inside the delimited module (unclear yet how)
|
|
71
|
-
|
|
72
|
-
def read(self, timeout=2) -> str:
|
|
68
|
+
def read(self, timeout : Timeout = None, decode : str = True) -> str:
|
|
73
69
|
"""
|
|
74
70
|
Reads command and formats it as a str
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
timeout : Timeout
|
|
75
|
+
decode : bool
|
|
76
|
+
Decode incoming data, True by default
|
|
75
77
|
"""
|
|
76
|
-
if self._termination not in self._buffer:
|
|
77
|
-
# Read the adapter only if there isn't a fragment already in the buffer
|
|
78
|
-
start = time()
|
|
79
|
-
while True:
|
|
80
|
-
# Continuously read the adapter as long as no termination is caught
|
|
81
|
-
data = self._from_bytes(self._adapter.read())
|
|
82
|
-
self._buffer += data
|
|
83
|
-
if self._termination in data or time() > start + timeout:
|
|
84
|
-
break
|
|
85
78
|
|
|
86
79
|
# Send up to the termination
|
|
87
|
-
|
|
80
|
+
data = self._adapter.read(timeout=timeout)
|
|
81
|
+
if decode:
|
|
82
|
+
data = data.decode(self._encoding)
|
|
88
83
|
if self._response_formatting:
|
|
89
84
|
# Only send the fragment (no termination)
|
|
90
|
-
return
|
|
85
|
+
return data
|
|
91
86
|
else:
|
|
92
87
|
# Add the termination back in
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
def read_raw(self) -> bytes:
|
|
96
|
-
"""
|
|
97
|
-
Returns the raw bytes instead of str
|
|
98
|
-
"""
|
|
99
|
-
if len(self._buffer) > 0:
|
|
100
|
-
warnings.warn("Warning : The buffer wasn't empty, standard (non raw) data is still in it")
|
|
101
|
-
return self._adapter.read()
|
|
88
|
+
return data + self._termination
|
syndesi/protocols/scpi.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from ..adapters import Adapter, IP, Timeout, Termination, StopCondition
|
|
2
2
|
from .protocol import Protocol
|
|
3
3
|
from ..tools.types import is_byte_instance
|
|
4
|
-
from ..tools.others import is_default_argument
|
|
4
|
+
#from ..tools.others import is_default_argument
|
|
5
|
+
from ..tools.others import DEFAULT
|
|
5
6
|
|
|
6
7
|
class SCPI(Protocol):
|
|
7
8
|
DEFAULT_PORT = 5025
|
|
@@ -32,7 +33,7 @@ class SCPI(Protocol):
|
|
|
32
33
|
self._adapter.set_default_port(self.DEFAULT_PORT)
|
|
33
34
|
|
|
34
35
|
self._adapter.set_default_timeout(timeout)
|
|
35
|
-
if
|
|
36
|
+
if self._adapter._stop_condition != DEFAULT:
|
|
36
37
|
raise ValueError('A conflicting stop-condition has been set for this adapter')
|
|
37
38
|
self._adapter._stop_condition = Termination(self._receive_termination.encode(encoding=encoding))
|
|
38
39
|
|
|
File without changes
|
syndesi/proxy/proxy.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# proxy.py
|
|
2
|
+
# Sébastien Deriaz
|
|
3
|
+
# 28.05.2024
|
|
4
|
+
import argparse
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from ..adapters import SerialPort
|
|
7
|
+
from ..adapters.adapter import AdapterDisconnected
|
|
8
|
+
from ..adapters.proxy import DEFAULT_PORT
|
|
9
|
+
from ..adapters.ip_server import IPServer
|
|
10
|
+
from typing import Union
|
|
11
|
+
from .proxy_api import *
|
|
12
|
+
from ..api.api import *
|
|
13
|
+
import logging
|
|
14
|
+
from ..tools.log import LoggerAlias, set_log_stream
|
|
15
|
+
|
|
16
|
+
class AdapterType(Enum):
|
|
17
|
+
SERIAL = 'serial'
|
|
18
|
+
IP = 'ip'
|
|
19
|
+
|
|
20
|
+
DEFAULT_BAUDRATE = 115200
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
prog='syndesi-proxy',
|
|
25
|
+
description='Syndesi proxy server',
|
|
26
|
+
epilog='')
|
|
27
|
+
# Parse subcommand
|
|
28
|
+
parser.add_argument('-t', '--adapter_type', choices=[x.value for x in AdapterType], default=AdapterType.IP)
|
|
29
|
+
parser.add_argument('-p', '--port', type=int, default=DEFAULT_PORT, help='IP port')
|
|
30
|
+
parser.add_argument('-a', '--address', default=None, type=str, help='IP address or serial port')
|
|
31
|
+
parser.add_argument('-b', '--baudrate', type=int, default=DEFAULT_BAUDRATE, help='Serial baudrate')
|
|
32
|
+
parser.add_argument('-v', '--verbose', action='store_true')
|
|
33
|
+
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
|
|
36
|
+
if args.verbose:
|
|
37
|
+
set_log_stream(True, 'DEBUG')
|
|
38
|
+
|
|
39
|
+
proxy_server = ProxyServer(adapter_type=args.adapter_type, port=args.port, address=args.address, baudrate=args.baudrate)
|
|
40
|
+
|
|
41
|
+
proxy_server.start()
|
|
42
|
+
|
|
43
|
+
class ProxyServer:
|
|
44
|
+
def __init__(self, adapter_type : AdapterType, port : Union[str, int], address : str, baudrate : int) -> None:
|
|
45
|
+
self._adapter_type = AdapterType(adapter_type)
|
|
46
|
+
self._adapter = None
|
|
47
|
+
self._port = port
|
|
48
|
+
self._address = address
|
|
49
|
+
self._baudrate = baudrate
|
|
50
|
+
self._logger = logging.getLogger(LoggerAlias.PROXY_SERVER.value)
|
|
51
|
+
self._logger.info('Initializing proxy server')
|
|
52
|
+
|
|
53
|
+
def start(self):
|
|
54
|
+
self._logger.info(f"Starting proxy server with {self._adapter_type.value} adapter")
|
|
55
|
+
|
|
56
|
+
if self._adapter_type == AdapterType.SERIAL:
|
|
57
|
+
# If adapter type is serial, create the adapter directly
|
|
58
|
+
self._master_adapter = SerialPort(self._address, baudrate=self._baudrate)
|
|
59
|
+
elif self._adapter_type == AdapterType.IP:
|
|
60
|
+
# Otherwise, create a server to get IP clients
|
|
61
|
+
server = IPServer(port=self._port, transport='TCP', address=self._address, max_clients=1, stop_condition=None)
|
|
62
|
+
server.open()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# If the adapter type is IP, use the external while loop to get clients
|
|
66
|
+
while True:
|
|
67
|
+
self._master_adapter = server.get_client()
|
|
68
|
+
self._logger.info(f'Client connected : {self._master_adapter._address}:{self._master_adapter._port}')
|
|
69
|
+
|
|
70
|
+
while True:
|
|
71
|
+
try:
|
|
72
|
+
call_raw = self._master_adapter.read()
|
|
73
|
+
except AdapterDisconnected:
|
|
74
|
+
self._logger.info('Client disconnected')
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
api_call = parse(call_raw)
|
|
78
|
+
|
|
79
|
+
self._logger.debug(f'Received {type(api_call)}')
|
|
80
|
+
|
|
81
|
+
output = self.manage_call(api_call)
|
|
82
|
+
|
|
83
|
+
self._master_adapter.write(output.encode())
|
|
84
|
+
|
|
85
|
+
if self._adapter_type == AdapterType.IP:
|
|
86
|
+
if not self._master_adapter.read_thread_alive():
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
# Loop only if we need to get a new client
|
|
90
|
+
if self._adapter_type != AdapterType.IP:
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
def manage_call(self, c : APICall) -> APICall:
|
|
94
|
+
output = None
|
|
95
|
+
# IP Specific
|
|
96
|
+
if isinstance(c, IPInstanciate):
|
|
97
|
+
self._adapter = IP(
|
|
98
|
+
address=c.address,
|
|
99
|
+
port=c.port)
|
|
100
|
+
output = ReturnStatus(True)
|
|
101
|
+
# Serial specific
|
|
102
|
+
if isinstance(c, SerialPortInstanciate):
|
|
103
|
+
self._adapter = SerialPort(
|
|
104
|
+
port=c.port,
|
|
105
|
+
baudrate=c.baudrate
|
|
106
|
+
)
|
|
107
|
+
# Adapter
|
|
108
|
+
elif isinstance(c, AdapterOpen):
|
|
109
|
+
if self._adapter is None:
|
|
110
|
+
output = ReturnStatus(False, 'Cannot open uninstanciated adapter')
|
|
111
|
+
self._adapter.open()
|
|
112
|
+
output = ReturnStatus(True)
|
|
113
|
+
elif isinstance(c, AdapterClose):
|
|
114
|
+
if self._adapter is None:
|
|
115
|
+
output = ReturnStatus(False, 'Cannot close uninstanciated adapter')
|
|
116
|
+
else:
|
|
117
|
+
self._adapter.close()
|
|
118
|
+
output = ReturnStatus(True)
|
|
119
|
+
elif isinstance(c, AdapterWrite):
|
|
120
|
+
if self._adapter is None:
|
|
121
|
+
output = ReturnStatus(False, 'Cannot write to uninstanciated adapter')
|
|
122
|
+
else:
|
|
123
|
+
self._adapter.write(c.data)
|
|
124
|
+
output = ReturnStatus(True)
|
|
125
|
+
elif isinstance(c, AdapterFlushRead):
|
|
126
|
+
self._adapter.flushRead()
|
|
127
|
+
output = ReturnStatus(True)
|
|
128
|
+
elif isinstance(c, AdapterRead):
|
|
129
|
+
# TODO : Implement return_metrics
|
|
130
|
+
data = self._adapter.read()
|
|
131
|
+
output = AdapterReadReturn(data=data)
|
|
132
|
+
|
|
133
|
+
return output
|
|
134
|
+
|
|
135
|
+
if __name__ == '__main__':
|
|
136
|
+
main()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# proxy_api.py
|
|
2
|
+
# Sébastien Deriaz
|
|
3
|
+
# 29.05.2024
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from ..api.api import APICall, ACTION_ATTRIBUTE, register_api, APIItem
|
|
9
|
+
|
|
10
|
+
class ProxyException(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
# IP specific
|
|
14
|
+
@dataclass
|
|
15
|
+
class IPInstanciate(APICall):
|
|
16
|
+
action = 'ip_adapter_inst'
|
|
17
|
+
address : str
|
|
18
|
+
port : int
|
|
19
|
+
transport : str
|
|
20
|
+
buffer_size : int
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class TimeoutAPI(APIItem):
|
|
24
|
+
name = 'timeout'
|
|
25
|
+
response : float
|
|
26
|
+
continuation : float
|
|
27
|
+
total : float
|
|
28
|
+
on_response : str
|
|
29
|
+
on_continuation : str
|
|
30
|
+
on_total : str
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class StopConditionAPI(APIItem):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class TerminationAPI(StopConditionAPI):
|
|
38
|
+
name = 'termination'
|
|
39
|
+
sequence : bytes
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class LengthAPI(StopConditionAPI):
|
|
43
|
+
name = 'length'
|
|
44
|
+
length : int
|
|
45
|
+
|
|
46
|
+
# Serial specific
|
|
47
|
+
@dataclass
|
|
48
|
+
class SerialPortInstanciate(APICall):
|
|
49
|
+
action = 'serial_adapter_inst'
|
|
50
|
+
port : str
|
|
51
|
+
baudrate : int
|
|
52
|
+
timeout : TimeoutAPI
|
|
53
|
+
stop_condition : StopConditionAPI
|
|
54
|
+
rts_cts : bool
|
|
55
|
+
|
|
56
|
+
# Adapters common
|
|
57
|
+
@dataclass
|
|
58
|
+
class AdapterOpen(APICall):
|
|
59
|
+
action = 'adapter_open'
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class AdapterClose(APICall):
|
|
63
|
+
action = 'adapter_close'
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class AdapterWrite(APICall):
|
|
67
|
+
action = 'adapter_write'
|
|
68
|
+
data : bytes
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class AdapterFlushRead(APICall):
|
|
72
|
+
action = 'adapter_flush_read'
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class AdapterRead(APICall):
|
|
76
|
+
action = 'adapter_read'
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class AdapterReadReturn(APICall):
|
|
80
|
+
action = 'adapter_read_return'
|
|
81
|
+
data : bytes
|
|
82
|
+
return_metrics : dict = None
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class ReturnStatus(APICall):
|
|
86
|
+
action = 'return_status'
|
|
87
|
+
success : bool
|
|
88
|
+
error_message : str = ''
|
|
89
|
+
|
|
90
|
+
# Register apis
|
|
91
|
+
current_module = sys.modules[__name__]
|
|
92
|
+
API_CALLS_PER_ACTION = {getattr(obj, ACTION_ATTRIBUTE) : obj for obj in current_module.__dict__.values() if hasattr(obj, ACTION_ATTRIBUTE)}
|
|
93
|
+
register_api(API_CALLS_PER_ACTION)
|
syndesi/tools/log.py
CHANGED
syndesi/tools/others.py
CHANGED
|
@@ -1,10 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def default_argument(instance):
|
|
5
|
-
setattr(instance, DEFAULT_ATTRIBUTE_FLAG_NAME, True)
|
|
6
|
-
return instance
|
|
7
|
-
|
|
8
|
-
def is_default_argument(instance):
|
|
9
|
-
return hasattr(instance, DEFAULT_ATTRIBUTE_FLAG_NAME)
|
|
10
|
-
|
|
1
|
+
DEFAULT = 'default'
|
syndesi/tools/types.py
CHANGED
|
File without changes
|
|
@@ -2,19 +2,22 @@ experiments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
2
2
|
syndesi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
syndesi/adapters/IP.py,sha256=4QFxFZeZLDHCpPqtpGQuqmT9pjLiKUNT6zjqkoCZBfE,2093
|
|
4
4
|
syndesi/adapters/VISA.py,sha256=GW4ZaW2cQ-0AmAOTeFcxZZz9E2V8URozR4a0Wpqpvgs,1119
|
|
5
|
-
syndesi/adapters/__init__.py,sha256=
|
|
6
|
-
syndesi/adapters/adapter.py,sha256=
|
|
5
|
+
syndesi/adapters/__init__.py,sha256=Ya9cfzEubB35225S3HD-NI8UkfzS0P6uVZkgG_tTDTI,225
|
|
6
|
+
syndesi/adapters/adapter.py,sha256=Y3P_uGHZEEsJQw7gME3oEhzYaPhClPxq67j5FKXsvbU,10824
|
|
7
7
|
syndesi/adapters/auto.py,sha256=ONniaTqYuotdLg88PdyUY87gzLglWzejEURqDw3Q4dE,1688
|
|
8
8
|
syndesi/adapters/iadapter.py,sha256=45scoY1khaSdZdIPETb0IuColPuEEE6LRw9OtHGl5v0,6884
|
|
9
|
-
syndesi/adapters/ip.py,sha256=
|
|
9
|
+
syndesi/adapters/ip.py,sha256=GWWxaRgmlcqdA2vb7OAYBAdjJdXimE0l3y3PE31AwjE,5422
|
|
10
10
|
syndesi/adapters/ip_server.py,sha256=wFMF_uPvVnYHPqAkAOvIvRTrMh_WMtK53hUorzD9L5s,3802
|
|
11
|
+
syndesi/adapters/proxy.py,sha256=Vdfh7UZLf3tJ68MItVkQijOnOT5iQaqDbvmKH9P7nhU,3011
|
|
11
12
|
syndesi/adapters/remote.py,sha256=XaaMGVSq7ygEijbETlsy-2cHSmMhZmmjA6UCm7hBTmE,552
|
|
12
13
|
syndesi/adapters/serial.py,sha256=cDp4lFt1f6Z6pRTWRXn0mjT_p0mZgAxqiJgtoXnEHDk,877
|
|
13
|
-
syndesi/adapters/serialport.py,sha256=
|
|
14
|
-
syndesi/adapters/stop_conditions.py,sha256
|
|
14
|
+
syndesi/adapters/serialport.py,sha256=Uh64McFdvHqZJJfZXIZnqgU-cPvUhRnz91gLecEQJRg,6025
|
|
15
|
+
syndesi/adapters/stop_conditions.py,sha256=-dWUpgXbFEtONBnvk04gjFUhcprf_KJb28aFMgsps4A,5599
|
|
15
16
|
syndesi/adapters/timed_queue.py,sha256=F-Bwj91cR_iLN2TdCkFL4WfvlJXovhyRF7K3dm30g50,912
|
|
16
|
-
syndesi/adapters/timeout.py,sha256=
|
|
17
|
+
syndesi/adapters/timeout.py,sha256=fH5MIJIUEqgDXtF9EgxZgjgAQ0mdeoybcmFznutXINo,11656
|
|
17
18
|
syndesi/adapters/visa.py,sha256=CHOGT6ZXhW3bROdFDYVEI9UXrXQbKgGnudxi14nFRaU,1304
|
|
19
|
+
syndesi/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
syndesi/api/api.py,sha256=OvoAkE__r9-VVdVgZs88707AC36d5R5J36DkQMdJgfs,2352
|
|
18
21
|
syndesi/descriptors/IP.py,sha256=vr7UbD9B11QJLbpIMycqs2Aqq41IY_WWD_JjeZ5b1FY,124
|
|
19
22
|
syndesi/descriptors/Serial.py,sha256=pO3MtQpWxV_gdSWsGLQ6nf-cANB1rLy8YSEDAK2Tb5I,127
|
|
20
23
|
syndesi/descriptors/VISA.py,sha256=-OSp7ll2ZHme-Oj736h1wd96HHYGNy4EtGtajzNNvVE,706
|
|
@@ -32,26 +35,29 @@ syndesi/descriptors/syndesi/payload.py,sha256=SplKy_Fu4XXcgMY12ed162gS--otUKkPtA
|
|
|
32
35
|
syndesi/descriptors/syndesi/sdid.py,sha256=0B_gHdRhjauW1aY-JF8B60gEY9Cz1TYPiKE3QnhePTM,346
|
|
33
36
|
syndesi/protocols/__init__.py,sha256=0lLF9oH-K_nMp8OoztFfsidFmed2TBBffnN_zJT3f-4,145
|
|
34
37
|
syndesi/protocols/commands.py,sha256=iZusNzht5zK31Uxdx3e2S4mXIdqcz5jBNuozajWuNQM,1898
|
|
35
|
-
syndesi/protocols/delimited.py,sha256
|
|
38
|
+
syndesi/protocols/delimited.py,sha256=-F36ZvyIrAqO4bpxY-ZAXFM_zXi-CJluZj5M2tXGq_A,2997
|
|
36
39
|
syndesi/protocols/iprotocol.py,sha256=TwAiu1Fxq9-vpxGu8f7WuADgeBvTORSIIwJ1vABqhuo,247
|
|
37
40
|
syndesi/protocols/protocol.py,sha256=yRgy_5Sj4LFA-CAAdFX0ZkC7m8GE5NuLv2aSGI5rwKQ,402
|
|
38
41
|
syndesi/protocols/raw.py,sha256=wqo2sIud1eWqQ-akGhwhOT8aI3SLiu4jlKJ5MgmhPbU,1019
|
|
39
|
-
syndesi/protocols/scpi.py,sha256=
|
|
42
|
+
syndesi/protocols/scpi.py,sha256=gU38dfaOhBJVmgDzLkXSqBqjP32uKLzRM5_jZCdCtOQ,3462
|
|
40
43
|
syndesi/protocols/sdp.py,sha256=PQe8zwR2xRiKPVvZL8BqHKvxflyg5dJeV4BAaDMnNw8,313
|
|
44
|
+
syndesi/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
+
syndesi/proxy/proxy.py,sha256=hsxRru-uJ_L9TIfTPqIFG_X1eOn3SR2forFrL70q0DE,5028
|
|
46
|
+
syndesi/proxy/proxy_api.py,sha256=4dhprayG-iWIxEOdnpYwGhfi1cBA_mhuE1FilXpk8X0,1886
|
|
41
47
|
syndesi/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
48
|
syndesi/tools/exceptions.py,sha256=KXMqrmwoTxfcGFUtjzQLlFS-QhKqwMTsnhDfQveDirc,303
|
|
43
|
-
syndesi/tools/log.py,sha256=
|
|
49
|
+
syndesi/tools/log.py,sha256=zOPQA4ZSDU8qCwomS4w61vd9KJ3yTprAnOWYyV7tliM,3542
|
|
44
50
|
syndesi/tools/logger.py,sha256=2Li7mRBtf06BXdGBa-I5aC4y4r2CxWEI8nmC6wh4-GE,3184
|
|
45
|
-
syndesi/tools/others.py,sha256=
|
|
51
|
+
syndesi/tools/others.py,sha256=y7sCWrnsgA3ih1eauucmOUbxHW6lVA120_g3co0MxDI,19
|
|
46
52
|
syndesi/tools/remote_api.py,sha256=crokpha5PQQI5v5KtYe34X7N-rfKi4JGjN-Z6vyCSmo,3003
|
|
47
53
|
syndesi/tools/remote_server.py,sha256=jSZZYzr6_kUo5Nb8_tA_whQceAcuKJrvSO3IdU94fBY,4830
|
|
48
54
|
syndesi/tools/shell.py,sha256=A13htgZ0FsFfe8GMlv-4P_w0pv44ca5r4yshz990uqs,3556
|
|
49
55
|
syndesi/tools/stop_conditions.py,sha256=_H-GPczrLTbPRHYpoPIItOqmPnemwn34QbKFn5GD6Uc,4824
|
|
50
|
-
syndesi/tools/types.py,sha256=
|
|
51
|
-
syndesi-0.1.6.data/scripts/syndesi,sha256=1y5vfiQPEissF1XsH7pfJYIKf1Lp3ScZWFtPdGELNws,956
|
|
56
|
+
syndesi/tools/types.py,sha256=rEpz5wEYGcJgYkRffRP9sYAhKv8X6DNCXIeI8Zcsvfc,1630
|
|
52
57
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
|
-
syndesi-0.1.
|
|
54
|
-
syndesi-0.1.
|
|
55
|
-
syndesi-0.1.
|
|
56
|
-
syndesi-0.1.
|
|
57
|
-
syndesi-0.1.
|
|
58
|
+
syndesi-0.2.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
59
|
+
syndesi-0.2.1.dist-info/METADATA,sha256=alER12YD9J_KdOjJSycLYl3zkovbPhxX4bf8M2K6geA,3468
|
|
60
|
+
syndesi-0.2.1.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
|
61
|
+
syndesi-0.2.1.dist-info/entry_points.txt,sha256=Q_38BGDXKEFxNPSXtkT8brn6f8lmOrkdkEv0IOV-YOM,96
|
|
62
|
+
syndesi-0.2.1.dist-info/top_level.txt,sha256=HrY36JU6hFYp_6qv-GuVBBtHYYemn8qhCrqpvXBd1Lg,8
|
|
63
|
+
syndesi-0.2.1.dist-info/RECORD,,
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
#!python
|
|
2
|
-
|
|
3
|
-
# Syndesi CLI
|
|
4
|
-
import argparse
|
|
5
|
-
from cmd import Cmd
|
|
6
|
-
from enum import Enum
|
|
7
|
-
|
|
8
|
-
class MyPrompt(Cmd):
|
|
9
|
-
prompt = '❯ '
|
|
10
|
-
intro = "Welcome! Type ? to list commands"
|
|
11
|
-
|
|
12
|
-
def do_exit(self, inp):
|
|
13
|
-
print("Bye !")
|
|
14
|
-
return True
|
|
15
|
-
|
|
16
|
-
def do_connect(self, inp):
|
|
17
|
-
print("Device")
|
|
18
|
-
|
|
19
|
-
def default(self, inp):
|
|
20
|
-
print(f"Entered : {inp}")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
do_EOF = do_exit # Allow CTRL+d to exit
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class SubCommands(Enum):
|
|
27
|
-
SHELL = 'shell'
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def connect():
|
|
33
|
-
print("Entering connect subcommand...")
|
|
34
|
-
|
|
35
|
-
p = MyPrompt()
|
|
36
|
-
p.cmdloop()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def main():
|
|
42
|
-
parser = argparse.ArgumentParser(
|
|
43
|
-
prog='syndesi',
|
|
44
|
-
description='Syndesi command line interface',
|
|
45
|
-
epilog='')
|
|
46
|
-
# Parse subcommand
|
|
47
|
-
parser.add_argument('subcommand', choices=[SubCommands.SHELL.value])
|
|
48
|
-
|
|
49
|
-
args = parser.parse_args()
|
|
50
|
-
|
|
51
|
-
if args.subcommand == SubCommands.SHELL.value:
|
|
52
|
-
connect()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if __name__ == '__main__':
|
|
57
|
-
main()
|
|
File without changes
|
|
File without changes
|