syndesi 0.1.6__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {syndesi-0.1.6/syndesi.egg-info → syndesi-0.2.0}/PKG-INFO +1 -1
  2. {syndesi-0.1.6 → syndesi-0.2.0}/setup.py +6 -2
  3. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/adapters/__init__.py +2 -1
  4. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/adapters/adapter.py +47 -30
  5. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/adapters/ip.py +52 -26
  6. syndesi-0.2.0/syndesi/adapters/ip_server.py +108 -0
  7. syndesi-0.2.0/syndesi/adapters/proxy.py +92 -0
  8. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/adapters/serialport.py +27 -3
  9. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/adapters/stop_conditions.py +7 -1
  10. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/adapters/timeout.py +6 -5
  11. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/protocols/scpi.py +3 -2
  12. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/tools/log.py +1 -0
  13. syndesi-0.2.0/syndesi/tools/others.py +1 -0
  14. syndesi-0.2.0/syndesi/tools/shell.py +111 -0
  15. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/tools/types.py +1 -1
  16. {syndesi-0.1.6 → syndesi-0.2.0/syndesi.egg-info}/PKG-INFO +1 -1
  17. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi.egg-info/SOURCES.txt +4 -2
  18. syndesi-0.2.0/syndesi.egg-info/entry_points.txt +3 -0
  19. syndesi-0.1.6/bin/syndesi +0 -57
  20. syndesi-0.1.6/syndesi/adapters/remote.py +0 -18
  21. syndesi-0.1.6/syndesi/tools/others.py +0 -10
  22. {syndesi-0.1.6 → syndesi-0.2.0}/LICENSE +0 -0
  23. {syndesi-0.1.6 → syndesi-0.2.0}/README.md +0 -0
  24. {syndesi-0.1.6 → syndesi-0.2.0}/setup.cfg +0 -0
  25. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/__init__.py +0 -0
  26. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/adapters/auto.py +0 -0
  27. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/adapters/timed_queue.py +0 -0
  28. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/adapters/visa.py +0 -0
  29. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/protocols/__init__.py +0 -0
  30. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/protocols/delimited.py +0 -0
  31. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/protocols/protocol.py +0 -0
  32. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/protocols/raw.py +0 -0
  33. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/protocols/sdp.py +0 -0
  34. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/tools/__init__.py +0 -0
  35. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi/tools/exceptions.py +0 -0
  36. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi.egg-info/dependency_links.txt +0 -0
  37. {syndesi-0.1.6 → syndesi-0.2.0}/syndesi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: syndesi
3
- Version: 0.1.6
3
+ Version: 0.2.0
4
4
  Summary: Syndesi
5
5
  Author: Sebastien Deriaz
6
6
  Author-email: sebastien.deriaz1@gmail.com
@@ -1,6 +1,6 @@
1
1
  from setuptools import setup, find_packages
2
2
 
3
- VERSION = '0.1.6'
3
+ VERSION = '0.2.0'
4
4
  DESCRIPTION = 'Syndesi'
5
5
 
6
6
  with open("README.md", "r", encoding="utf-8") as fh:
@@ -15,7 +15,11 @@ setup(
15
15
  description=DESCRIPTION,
16
16
  long_description_content_type="text/markdown",
17
17
  long_description=long_description,
18
- scripts=['bin/syndesi'],
18
+ entry_points = {
19
+ 'console_scripts': [
20
+ 'syndesi=syndesi.shell.syndesi:main',
21
+ 'syndesi-proxy=syndesi.proxy.proxy:main'],
22
+ },
19
23
  packages=find_packages(),
20
24
  install_requires=[''],
21
25
  keywords=['python', 'syndesi', 'interface', 'ethernet'],
@@ -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
@@ -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 default_argument, is_default_argument
32
+ from ..tools.others import DEFAULT
33
33
 
34
- DEFAULT_TIMEOUT = default_argument(Timeout(response=1, continuation=100e-3, total=None))
35
- DEFAULT_STOP_CONDITION = default_argument(StopCondition())
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
- if is_number(timeout):
89
- self._timeout = Timeout(response=timeout, continuation=100e-3)
90
- elif isinstance(timeout, Timeout):
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
- raise ValueError(f"Invalid timeout type : {type(timeout)}")
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._alias = alias
104
- self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
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
- self._timeout = timeout_fuse(self._timeout, default_timeout)
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 is_default_argument(self._stop_condition):
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
+
@@ -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
- DEFAULT_RESPONSE_TIMEOUT = 1
12
- DEFAULT_CONTINUATION_TIMEOUT = 1e-3
13
- DEFAULT_TOTAL_TIMEOUT = 5
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
- class IP(Adapter):
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
- if self._transport == self.Protocol.TCP:
57
- self._logger.info("Setting up TCP IP adapter")
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
- else:
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
- if not payload:
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,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
@@ -7,6 +7,9 @@ from .timed_queue import TimedQueue
7
7
  from threading import Thread
8
8
  from typing import Union
9
9
  import select
10
+ import argparse
11
+ from ..tools import shell
12
+ from collections.abc import Sequence
10
13
 
11
14
  # From pyserial - serialposix.py
12
15
  import fcntl
@@ -60,10 +63,11 @@ class SerialPort(Adapter):
60
63
  self._logger.info("Adapter opened !")
61
64
 
62
65
  def close(self):
63
- if self._thread.is_alive():
66
+ if self._thread is not None and self._thread.is_alive():
64
67
  os.write(self._stop_event_pipe_write, b'1')
65
68
  self._thread.join()
66
- self._port.close()
69
+ if hasattr(self, '_port'):
70
+ self._port.close()
67
71
  self._logger.info("Adapter closed !")
68
72
 
69
73
  def write(self, data : bytes):
@@ -78,6 +82,9 @@ class SerialPort(Adapter):
78
82
  self._logger.debug(f"Written [{write_duration*1e3:.3f}ms]: {repr(data)}")
79
83
 
80
84
  def _start_thread(self):
85
+ """
86
+ Start the read thread
87
+ """
81
88
  self._logger.debug("Starting read thread...")
82
89
  if self._thread is None or not self._thread.is_alive():
83
90
  self._thread = Thread(target=self._read_thread, daemon=True, args=(self._port, self._read_queue, self._stop_event_pipe))
@@ -125,4 +132,21 @@ class SerialPort(Adapter):
125
132
  def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False):
126
133
  self.flushRead()
127
134
  self.write(data)
128
- return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
135
+ return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
136
+
137
+ def shell_parse(inp: str):
138
+ parser = argparse.ArgumentParser(
139
+ prog='',
140
+ description='Serial port shell parser',
141
+ epilog='')
142
+ # Parse subcommand
143
+ parser.add_argument('--' + shell.Arguments.PORT.value, type=str)
144
+ parser.add_argument('--' + shell.Arguments.BAUDRATE.value, type=int)
145
+ parser.add_argument('--' + shell.Arguments.ENABLE_RTS_CTS.value, action='store_true')
146
+ args = parser.parse_args(inp.split())
147
+
148
+ return {
149
+ 'port' : getattr(args, shell.Arguments.PORT.value),
150
+ 'baudrate' : getattr(args, shell.Arguments.BAUDRATE.value),
151
+ 'rts_cts' : bool(getattr(args, shell.Arguments.ENABLE_RTS_CTS.value))
152
+ }
@@ -60,7 +60,13 @@ class Termination(StopCondition):
60
60
  sequence : bytes
61
61
  """
62
62
  super().__init__()
63
- self._termination = sequence
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
 
@@ -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():
@@ -264,10 +264,11 @@ def timeout_fuse(high_priority, low_priority):
264
264
  low = low_priority if isinstance(low_priority, Timeout) else Timeout(low_priority)
265
265
 
266
266
  # 3) If one is the default, take the other
267
- if is_default_argument(high):
268
- return low
269
- if is_default_argument(low):
270
- return high
267
+ # if is_default_argument(high):
268
+ # return low
269
+ # if is_default_argument(low):
270
+ # return high
271
+ # 05.06.2024 : Removed because is_default_argument is obsolete, use DEFAULT is necessary
271
272
 
272
273
  new_attr = {}
273
274
  # 4) Select with parameter to keep based on where it has been set
@@ -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 is_default_argument(self._adapter._stop_condition):
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
 
@@ -11,6 +11,7 @@ from typing import List, Union
11
11
  class LoggerAlias(Enum):
12
12
  ADAPTER = 'adapter'
13
13
  PROTOCOL = 'protocol'
14
+ PROXY_SERVER = 'proxy_server'
14
15
 
15
16
  default_formatter = logging.Formatter('%(asctime)s:%(name)s:%(levelname)s:%(message)s')
16
17
 
@@ -0,0 +1 @@
1
+ DEFAULT = 'default'
@@ -0,0 +1,111 @@
1
+ # shell.py
2
+ # Sébastien Deriaz
3
+ # 30.04.2024
4
+ from enum import Enum
5
+ from cmd import Cmd
6
+ from syndesi.adapters import *
7
+ import argparse
8
+ import shlex
9
+ import sys
10
+ import os
11
+ from colorist import ColorRGB
12
+
13
+ VERSION = 0.1
14
+
15
+ class ShellPrompt(Cmd):
16
+ __hiden_methods = ('do_EOF','do_clear','do_cls')
17
+ PROMPT_COLOR = ColorRGB(28, 90, 145)
18
+
19
+ prompt = f'{PROMPT_COLOR}❯ {PROMPT_COLOR.OFF}'
20
+ intro = "Welcome to the Syndesi Shell! Type ? to list commands"
21
+
22
+ def get_names(self):
23
+ return [n for n in dir(self.__class__) if n not in self.__hiden_methods]
24
+
25
+ def do_exit(self, inp):
26
+ """Exit"""
27
+ return True
28
+
29
+ def do_serial(self, inp):
30
+ """Open serial adapter"""
31
+ self._adapter = SerialPort(**SerialPort.shell_parse(inp))
32
+
33
+ def do_ip(self, inp):
34
+ """Open IP adapter
35
+ -p / --port : port number
36
+ --ip : ip address
37
+ -t / --transport : TCP or UDP
38
+ """
39
+ parser = argparse.ArgumentParser()
40
+ parser.add_argument('-p', '--port', type=int, required=True)
41
+ parser.add_argument('--ip', type=str, required=True)
42
+ parser.add_argument('-t', '--transport', type=str, choices=['UDP', 'TCP'], default='TCP', required=False)
43
+ try:
44
+ args = parser.parse_args(shlex.split(inp))
45
+ except SystemExit as e:
46
+ pass
47
+ else:
48
+ self._adapter = IP(address=args.ip, port=args.port, transport=args.transport)
49
+
50
+ def default(self, inp):
51
+ if hasattr(self, '_adapter'):
52
+ cmd = inp + '\n'
53
+ output = self._adapter.query(cmd)
54
+ print(output)
55
+ else:
56
+ print(f"Unknown command : {inp}, type ? to list commands")
57
+
58
+ def do_clear(self, _):
59
+ if sys.platform == "linux" or sys.platform == "linux2" or sys.platform == "darwin":
60
+ # linux
61
+ os.system('clear')
62
+ elif sys.platform == "win32":
63
+ # Windows
64
+ os.system('cls')
65
+
66
+ def do_cls(self, _):
67
+ self.do_clear()
68
+
69
+ def do_help(self, arg: str) -> bool | None:
70
+ if arg:
71
+ # Use Cmd's help
72
+ super().do_help(arg)
73
+ else:
74
+ # Otherwise, print a custom help
75
+ names = self.get_names()
76
+
77
+ cmds_doc = []
78
+ cmds_undoc = []
79
+ docs = []
80
+ topics = set()
81
+ for name in names:
82
+ if name[:5] == 'help_':
83
+ topics.add(name[5:])
84
+ names.sort()
85
+ # There can be duplicates if routines overridden
86
+ prevname = ''
87
+ for name in names:
88
+ if name[:3] == 'do_':
89
+ if name == prevname:
90
+ continue
91
+ prevname = name
92
+ cmd=name[3:]
93
+ if cmd in topics:
94
+ #cmds_doc.append(cmd)
95
+ topics.remove(cmd)
96
+ elif getattr(self, name).__doc__:
97
+ cmds_doc.append(cmd)
98
+ docs.append(getattr(self, name).__doc__)
99
+ else:
100
+ cmds_undoc.append(cmd)
101
+
102
+ print(f"Syndesi shell V{VERSION}")
103
+ print("Available commands :")
104
+ max_width = max([len(cmd) for cmd in cmds_doc])
105
+ for cmd, doc in zip(cmds_doc, docs):
106
+ print(f" {cmd:<{max_width+2}} : {doc}")
107
+
108
+ #self.print_topics(self.misc_header, sorted(topics),15,80)
109
+ #self.print_topics(self.undoc_header, cmds_undoc, 15,80)
110
+
111
+ do_EOF = do_exit # Allow CTRL+d to exit
@@ -72,7 +72,7 @@ def to_bytes(data):
72
72
  elif isinstance(data, str):
73
73
  return data.encode('utf-8')
74
74
  else:
75
- return ValueError(f"Invalid data type : {type(data)}")
75
+ raise ValueError(f"Invalid data type : {type(data)}")
76
76
 
77
77
 
78
78
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: syndesi
3
- Version: 0.1.6
3
+ Version: 0.2.0
4
4
  Summary: Syndesi
5
5
  Author: Sebastien Deriaz
6
6
  Author-email: sebastien.deriaz1@gmail.com
@@ -1,17 +1,18 @@
1
1
  LICENSE
2
2
  README.md
3
3
  setup.py
4
- bin/syndesi
5
4
  syndesi/__init__.py
6
5
  syndesi.egg-info/PKG-INFO
7
6
  syndesi.egg-info/SOURCES.txt
8
7
  syndesi.egg-info/dependency_links.txt
8
+ syndesi.egg-info/entry_points.txt
9
9
  syndesi.egg-info/top_level.txt
10
10
  syndesi/adapters/__init__.py
11
11
  syndesi/adapters/adapter.py
12
12
  syndesi/adapters/auto.py
13
13
  syndesi/adapters/ip.py
14
- syndesi/adapters/remote.py
14
+ syndesi/adapters/ip_server.py
15
+ syndesi/adapters/proxy.py
15
16
  syndesi/adapters/serialport.py
16
17
  syndesi/adapters/stop_conditions.py
17
18
  syndesi/adapters/timed_queue.py
@@ -27,4 +28,5 @@ syndesi/tools/__init__.py
27
28
  syndesi/tools/exceptions.py
28
29
  syndesi/tools/log.py
29
30
  syndesi/tools/others.py
31
+ syndesi/tools/shell.py
30
32
  syndesi/tools/types.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ syndesi = syndesi.shell.syndesi:main
3
+ syndesi-proxy = syndesi.proxy.proxy:main
syndesi-0.1.6/bin/syndesi DELETED
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env 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()
@@ -1,18 +0,0 @@
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('...')))
@@ -1,10 +0,0 @@
1
- DEFAULT_ATTRIBUTE_FLAG_NAME = '_is_default'
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
-
File without changes
File without changes
File without changes
File without changes