syndesi 0.2.4__py3-none-any.whl → 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syndesi/__init__.py +9 -3
- syndesi/__main__.py +4 -0
- syndesi/adapters/__init__.py +0 -8
- syndesi/adapters/adapter.py +541 -244
- syndesi/adapters/auto.py +28 -23
- syndesi/adapters/backend/adapter_backend.py +387 -0
- syndesi/adapters/backend/adapter_manager.py +48 -0
- syndesi/adapters/backend/adapter_session.py +447 -0
- syndesi/adapters/backend/backend.py +438 -0
- syndesi/adapters/backend/backend_tools.py +66 -0
- syndesi/adapters/backend/descriptors.py +152 -0
- syndesi/adapters/backend/ip_backend.py +148 -0
- syndesi/adapters/backend/serialport_backend.py +236 -0
- syndesi/adapters/backend/stop_condition_backend.py +342 -0
- syndesi/adapters/backend/timed_queue.py +39 -0
- syndesi/adapters/backend/timeout.py +252 -0
- syndesi/adapters/backend/visa_backend.py +197 -0
- syndesi/adapters/ip.py +89 -154
- syndesi/adapters/ip_server.py +87 -93
- syndesi/adapters/serialport.py +56 -178
- syndesi/adapters/stop_condition.py +142 -0
- syndesi/adapters/timeout.py +85 -296
- syndesi/adapters/visa.py +39 -98
- syndesi/cli/backend_console.py +96 -0
- syndesi/cli/backend_status.py +276 -0
- syndesi/cli/backend_wrapper.py +61 -0
- syndesi/cli/console.py +281 -0
- syndesi/cli/shell.py +239 -151
- syndesi/cli/shell_tools.py +107 -0
- syndesi/cli/terminal_tools.py +14 -0
- syndesi/protocols/__init__.py +0 -6
- syndesi/protocols/delimited.py +151 -45
- syndesi/protocols/modbus.py +672 -377
- syndesi/protocols/protocol.py +67 -11
- syndesi/protocols/raw.py +67 -9
- syndesi/protocols/scpi.py +90 -36
- syndesi/{descriptors/syndesi → scripts}/__init__.py +0 -0
- syndesi/scripts/syndesi.py +52 -0
- syndesi/scripts/syndesi_backend.py +37 -0
- syndesi/tools/backend_api.py +209 -0
- syndesi/tools/backend_logger.py +65 -0
- syndesi/tools/errors.py +23 -0
- syndesi/tools/exceptions.py +7 -1
- syndesi/tools/log.py +207 -55
- syndesi/tools/log_settings.py +17 -0
- syndesi/tools/types.py +67 -40
- syndesi/version.py +3 -1
- {syndesi-0.2.4.dist-info → syndesi-0.3.2.dist-info}/METADATA +39 -11
- syndesi-0.3.2.dist-info/RECORD +59 -0
- {syndesi-0.2.4.dist-info → syndesi-0.3.2.dist-info}/WHEEL +1 -1
- syndesi-0.3.2.dist-info/entry_points.txt +3 -0
- syndesi-0.3.2.dist-info/licenses/LICENSE +674 -0
- {syndesi-0.2.4.dist-info → syndesi-0.3.2.dist-info}/top_level.txt +0 -0
- syndesi/_version.py +0 -1
- syndesi/adapters/IP.py +0 -81
- syndesi/adapters/VISA.py +0 -50
- syndesi/adapters/iadapter.py +0 -211
- syndesi/adapters/proxy.py +0 -99
- syndesi/adapters/remote.py +0 -18
- syndesi/adapters/serial.py +0 -37
- syndesi/adapters/stop_conditions.py +0 -210
- syndesi/adapters/timed_queue.py +0 -32
- syndesi/api/api.py +0 -77
- syndesi/cli/adapter.py +0 -134
- syndesi/cli/command.py +0 -29
- syndesi/cli/serial.py +0 -18
- syndesi/cli/syndesi.py +0 -45
- syndesi/descriptors/IP.py +0 -9
- syndesi/descriptors/Serial.py +0 -10
- syndesi/descriptors/VISA.py +0 -31
- syndesi/descriptors/__init__.py +0 -1
- syndesi/descriptors/descriptor.py +0 -9
- syndesi/descriptors/ip.py +0 -9
- syndesi/descriptors/syndesi/Syndesi.py +0 -9
- syndesi/descriptors/syndesi/_device.py +0 -25
- syndesi/descriptors/syndesi/devices.py +0 -10
- syndesi/descriptors/syndesi/frame.py +0 -133
- syndesi/descriptors/syndesi/network.py +0 -41
- syndesi/descriptors/syndesi/payload.py +0 -11
- syndesi/descriptors/syndesi/sdid.py +0 -21
- syndesi/descriptors/visa.py +0 -31
- syndesi/protocols/commands.py +0 -56
- syndesi/protocols/iprotocol.py +0 -14
- syndesi/protocols/sdp.py +0 -14
- syndesi/proxy/__init__.py +0 -0
- syndesi/proxy/proxy.py +0 -136
- syndesi/proxy/proxy_api.py +0 -122
- syndesi/tools/logger.py +0 -113
- syndesi/tools/others.py +0 -2
- syndesi/tools/remote_api.py +0 -113
- syndesi/tools/remote_server.py +0 -133
- syndesi/tools/shell.py +0 -111
- syndesi/tools/stop_conditions.py +0 -148
- syndesi-0.2.4.dist-info/RECORD +0 -73
- syndesi-0.2.4.dist-info/entry_points.txt +0 -3
- tests/__init__.py +0 -0
- {experiments → syndesi/adapters/backend}/__init__.py +0 -0
- /syndesi/{api/__init__.py → cli/terminal.py} +0 -0
- /syndesi/cli/{ip.py → terminal_apps.py} +0 -0
- /syndesi/{cli/modbus.py → tools/internal.py} +0 -0
syndesi/protocols/protocol.py
CHANGED
|
@@ -1,24 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# File : protocol.py
|
|
2
|
+
# Author : Sébastien Deriaz
|
|
3
|
+
# License : GPL
|
|
4
|
+
|
|
4
5
|
import logging
|
|
5
|
-
from
|
|
6
|
+
from abc import abstractmethod
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from types import EllipsisType
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..adapters.adapter import Adapter
|
|
12
|
+
from ..adapters.auto import auto_adapter
|
|
13
|
+
from ..adapters.backend.adapter_backend import AdapterReadPayload, AdapterSignal
|
|
14
|
+
|
|
15
|
+
# from syndesi.adapters.stop_condition import StopCondition
|
|
16
|
+
from ..adapters.timeout import Timeout
|
|
17
|
+
from ..tools.log_settings import LoggerAlias
|
|
18
|
+
|
|
6
19
|
|
|
7
20
|
class Protocol:
|
|
8
|
-
def __init__(
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
adapter: Adapter,
|
|
24
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
25
|
+
event_callback: Callable[[AdapterSignal], None] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
# TODO : Convert the callable from AdapterSignal to ProtocolSignal or something similar
|
|
9
28
|
self._adapter = auto_adapter(adapter)
|
|
10
|
-
|
|
29
|
+
|
|
30
|
+
self._event_callback = event_callback
|
|
31
|
+
self._adapter.set_event_callback(self.event_callback)
|
|
32
|
+
|
|
33
|
+
if timeout is not ...:
|
|
11
34
|
self._adapter.set_default_timeout(timeout)
|
|
12
35
|
self._logger = logging.getLogger(LoggerAlias.PROTOCOL.value)
|
|
13
36
|
|
|
14
|
-
|
|
37
|
+
if timeout is ...:
|
|
38
|
+
self._adapter.set_timeout(self._default_timeout())
|
|
39
|
+
else:
|
|
40
|
+
self._adapter.set_timeout(timeout)
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def _default_timeout(self) -> Timeout | None:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def flushRead(self) -> None:
|
|
15
47
|
self._adapter.flushRead()
|
|
16
48
|
|
|
17
|
-
def
|
|
49
|
+
def event_callback(self, event: AdapterSignal) -> None:
|
|
50
|
+
if self._event_callback is not None:
|
|
51
|
+
self._event_callback(event)
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def _on_data_ready_event(self, data: AdapterReadPayload) -> None:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def write(self, *args: Any, **kwargs: Any) -> None:
|
|
18
59
|
pass
|
|
19
60
|
|
|
20
|
-
def
|
|
61
|
+
def open(self) -> None:
|
|
62
|
+
self._adapter.open()
|
|
63
|
+
|
|
64
|
+
def close(self) -> None:
|
|
65
|
+
self._adapter.close()
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def query(
|
|
69
|
+
self,
|
|
70
|
+
*args: Any,
|
|
71
|
+
**kwargs: Any,
|
|
72
|
+
# timeout: Union[Timeout, EllipsisType, None],
|
|
73
|
+
# stop_condition: Union[StopCondition, None, EllipsisType],
|
|
74
|
+
# full_output : bool,
|
|
75
|
+
) -> Any:
|
|
21
76
|
pass
|
|
22
77
|
|
|
23
|
-
|
|
24
|
-
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def read(self, *args: Any, **kwargs: Any) -> Any:
|
|
80
|
+
pass
|
syndesi/protocols/raw.py
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# File : raw.py
|
|
2
|
+
# Author : Sébastien Deriaz
|
|
3
|
+
# License : GPL
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from types import EllipsisType
|
|
7
|
+
|
|
8
|
+
from syndesi.adapters.backend.adapter_backend import AdapterReadPayload, AdapterSignal
|
|
9
|
+
from syndesi.adapters.stop_condition import StopCondition
|
|
3
10
|
|
|
11
|
+
from ..adapters.adapter import Adapter
|
|
12
|
+
from ..adapters.timeout import Timeout
|
|
13
|
+
from .protocol import Protocol
|
|
4
14
|
|
|
5
15
|
# Raw protocols provide the user with the binary data directly,
|
|
6
16
|
# without converting it to string first
|
|
7
17
|
|
|
18
|
+
|
|
8
19
|
class Raw(Protocol):
|
|
9
|
-
def __init__(
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
adapter: Adapter,
|
|
23
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
24
|
+
event_callback: Callable[[AdapterSignal], None] | None = None,
|
|
25
|
+
) -> None:
|
|
10
26
|
"""
|
|
11
27
|
Raw device, no presentation and application layers
|
|
12
28
|
|
|
@@ -14,15 +30,57 @@ class Raw(Protocol):
|
|
|
14
30
|
----------
|
|
15
31
|
adapter : IAdapter
|
|
16
32
|
"""
|
|
17
|
-
super().__init__(
|
|
33
|
+
super().__init__(
|
|
34
|
+
adapter,
|
|
35
|
+
timeout,
|
|
36
|
+
event_callback,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Connect the adapter if it wasn't done already
|
|
40
|
+
self._adapter.connect()
|
|
18
41
|
|
|
19
|
-
def
|
|
42
|
+
def _default_timeout(self) -> Timeout | None:
|
|
43
|
+
return Timeout(response=2, action="error")
|
|
44
|
+
|
|
45
|
+
def write(self, data: bytes) -> None:
|
|
20
46
|
self._adapter.write(data)
|
|
21
47
|
|
|
22
|
-
def query(
|
|
48
|
+
def query(
|
|
49
|
+
self,
|
|
50
|
+
data: bytes,
|
|
51
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
52
|
+
stop_condition: StopCondition | None | EllipsisType = ...,
|
|
53
|
+
full_output: bool = False,
|
|
54
|
+
) -> bytes | tuple[bytes, AdapterSignal]:
|
|
23
55
|
self._adapter.flushRead()
|
|
24
56
|
self.write(data)
|
|
25
|
-
return self.read(
|
|
57
|
+
return self.read(
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
stop_condition=stop_condition,
|
|
60
|
+
full_output=full_output,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def read(
|
|
64
|
+
self,
|
|
65
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
66
|
+
stop_condition: StopCondition | None | EllipsisType = ...,
|
|
67
|
+
full_output: bool = False,
|
|
68
|
+
) -> bytes | tuple[bytes, AdapterSignal]:
|
|
69
|
+
|
|
70
|
+
return self.read_detailed(timeout=timeout, stop_condition=stop_condition)[0]
|
|
71
|
+
|
|
72
|
+
def read_detailed(
|
|
73
|
+
self,
|
|
74
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
75
|
+
stop_condition: StopCondition | None | EllipsisType = ...,
|
|
76
|
+
) -> tuple[bytes, AdapterSignal | None]:
|
|
77
|
+
return self._adapter.read_detailed(
|
|
78
|
+
timeout=timeout, stop_condition=stop_condition
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def _on_data_ready_event(self, data: AdapterReadPayload) -> None:
|
|
82
|
+
# TODO : Call the callback here ?
|
|
83
|
+
pass
|
|
26
84
|
|
|
27
|
-
def
|
|
28
|
-
return self._adapter
|
|
85
|
+
def __str__(self) -> str:
|
|
86
|
+
return f"Raw({self._adapter})"
|
syndesi/protocols/scpi.py
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
# File : scpi.py
|
|
2
|
+
# Author : Sébastien Deriaz
|
|
3
|
+
# License : GPL
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from types import EllipsisType
|
|
7
|
+
|
|
8
|
+
from syndesi.adapters.backend.adapter_backend import AdapterSignal
|
|
9
|
+
|
|
10
|
+
from ..adapters.adapter import Adapter
|
|
11
|
+
from ..adapters.ip import IP
|
|
12
|
+
from ..adapters.stop_condition import StopCondition, Termination
|
|
13
|
+
from ..adapters.timeout import Timeout, TimeoutAction
|
|
2
14
|
from .protocol import Protocol
|
|
3
|
-
from ..tools.types import is_byte_instance
|
|
4
15
|
|
|
5
|
-
DEFAULT_TIMEOUT = Timeout(response=10, continuation=0.5, total=None, on_response='error', on_continuation='error')
|
|
6
16
|
|
|
7
17
|
class SCPI(Protocol):
|
|
8
18
|
DEFAULT_PORT = 5025
|
|
9
|
-
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
adapter: Adapter,
|
|
23
|
+
send_termination: str = "\n",
|
|
24
|
+
receive_termination: str | None = None,
|
|
25
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
26
|
+
encoding: str = "utf-8",
|
|
27
|
+
) -> None:
|
|
10
28
|
"""
|
|
11
29
|
SDP (Syndesi Device Protocol) compatible device
|
|
12
30
|
|
|
@@ -22,9 +40,7 @@ class SCPI(Protocol):
|
|
|
22
40
|
"""
|
|
23
41
|
self._encoding = encoding
|
|
24
42
|
# Set the default timeout
|
|
25
|
-
|
|
26
|
-
timeout = DEFAULT_TIMEOUT
|
|
27
|
-
|
|
43
|
+
|
|
28
44
|
if receive_termination is None:
|
|
29
45
|
self._receive_termination = send_termination
|
|
30
46
|
else:
|
|
@@ -32,56 +48,94 @@ class SCPI(Protocol):
|
|
|
32
48
|
self._send_termination = send_termination
|
|
33
49
|
# Configure the adapter for stop-condition mode (timeouts will raise errors)
|
|
34
50
|
if not adapter._default_stop_condition:
|
|
35
|
-
raise ValueError(
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"No stop-conditions can be set for an adapter used by SCPI protocol"
|
|
53
|
+
)
|
|
54
|
+
adapter.set_stop_conditions(
|
|
55
|
+
Termination(self._receive_termination.encode(self._encoding))
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# adapter.set_timeout(self.timeout)
|
|
38
59
|
if isinstance(adapter, IP):
|
|
39
60
|
adapter.set_default_port(self.DEFAULT_PORT)
|
|
40
61
|
# Give the adapter to the Protocol base class
|
|
41
62
|
super().__init__(adapter=adapter, timeout=timeout)
|
|
42
63
|
|
|
43
|
-
|
|
64
|
+
# Connect the adapter if it wasn't done already
|
|
65
|
+
self._adapter.connect()
|
|
66
|
+
|
|
67
|
+
def _default_timeout(self) -> Timeout | None:
|
|
68
|
+
return Timeout(response=5, action=TimeoutAction.ERROR.value)
|
|
69
|
+
|
|
70
|
+
def _to_bytes(self, command: str) -> bytes:
|
|
44
71
|
if isinstance(command, str):
|
|
45
|
-
return command.encode(
|
|
72
|
+
return command.encode("ASCII")
|
|
46
73
|
else:
|
|
47
|
-
raise ValueError(f
|
|
48
|
-
|
|
49
|
-
def _from_bytes(self, payload
|
|
50
|
-
if
|
|
51
|
-
return payload.decode(
|
|
74
|
+
raise ValueError(f"Invalid command type : {type(command)}")
|
|
75
|
+
|
|
76
|
+
def _from_bytes(self, payload: bytes) -> str:
|
|
77
|
+
if isinstance(payload, bytes):
|
|
78
|
+
return payload.decode("ASCII")
|
|
52
79
|
else:
|
|
53
80
|
raise ValueError(f"Invalid payload type : {type(payload)}")
|
|
54
81
|
|
|
55
|
-
def _formatCommand(self, command):
|
|
82
|
+
def _formatCommand(self, command: str) -> str:
|
|
56
83
|
return command + self._send_termination
|
|
57
84
|
|
|
58
|
-
def _unformatCommand(self, payload):
|
|
59
|
-
return payload.replace(self._receive_termination,
|
|
60
|
-
|
|
61
|
-
def _checkCommand(self, command
|
|
62
|
-
for c in [
|
|
85
|
+
def _unformatCommand(self, payload: str) -> str:
|
|
86
|
+
return payload.replace(self._receive_termination, "")
|
|
87
|
+
|
|
88
|
+
def _checkCommand(self, command: str) -> None:
|
|
89
|
+
for c in ["\n", "\r"]:
|
|
63
90
|
if c in command:
|
|
64
91
|
raise ValueError(f"Invalid char {repr(c)} in command")
|
|
65
92
|
|
|
66
|
-
def write(self, command
|
|
93
|
+
def write(self, command: str) -> None:
|
|
67
94
|
self._checkCommand(command)
|
|
68
95
|
payload = self._to_bytes(self._formatCommand(command))
|
|
69
96
|
self._adapter.write(payload)
|
|
70
97
|
|
|
71
|
-
def write_raw(self, data
|
|
72
|
-
self._adapter.write(
|
|
98
|
+
def write_raw(self, data: bytes, termination: bool = False) -> None:
|
|
99
|
+
self._adapter.write(
|
|
100
|
+
data
|
|
101
|
+
+ (self._send_termination.encode(self._encoding) if termination else b"")
|
|
102
|
+
)
|
|
73
103
|
|
|
74
|
-
def query(
|
|
104
|
+
def query(
|
|
105
|
+
self,
|
|
106
|
+
command: str,
|
|
107
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
108
|
+
stop_condition: StopCondition | None | EllipsisType = ...,
|
|
109
|
+
) -> str:
|
|
75
110
|
self._adapter.flushRead()
|
|
76
111
|
self.write(command)
|
|
77
|
-
return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
|
|
78
112
|
|
|
79
|
-
|
|
80
|
-
output = self._from_bytes(self._adapter.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics))
|
|
81
|
-
return self._unformatCommand(output)
|
|
113
|
+
return self.read(timeout=timeout, stop_condition=stop_condition)
|
|
82
114
|
|
|
83
|
-
def read_raw(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
def read_raw(
|
|
116
|
+
self,
|
|
117
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
118
|
+
stop_condition: StopCondition | None | EllipsisType = ...,
|
|
119
|
+
) -> tuple[bytes, AdapterSignal | None]:
|
|
120
|
+
return self._adapter.read_detailed(
|
|
121
|
+
timeout=timeout,
|
|
122
|
+
stop_condition=stop_condition,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def read_detailed(
|
|
126
|
+
self,
|
|
127
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
128
|
+
stop_condition: StopCondition | None | EllipsisType = ...,
|
|
129
|
+
) -> tuple[str, AdapterSignal | None]:
|
|
130
|
+
|
|
131
|
+
raw_data, signal = self.read_raw(timeout=timeout, stop_condition=stop_condition)
|
|
132
|
+
data_out = self._unformatCommand(self._from_bytes(raw_data))
|
|
133
|
+
|
|
134
|
+
return data_out, signal
|
|
135
|
+
|
|
136
|
+
def read(
|
|
137
|
+
self,
|
|
138
|
+
timeout: Timeout | None | EllipsisType = ...,
|
|
139
|
+
stop_condition: StopCondition | None | EllipsisType = ...,
|
|
140
|
+
) -> str:
|
|
141
|
+
return self.read_detailed(timeout=timeout, stop_condition=stop_condition)[0]
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# File : syndesi.py
|
|
2
|
+
# Author : Sébastien Deriaz
|
|
3
|
+
# License : GPL
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from ..cli.shell import AdapterShell, AdapterType
|
|
9
|
+
from ..tools.log import log
|
|
10
|
+
from ..version import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SyndesiCommands(Enum):
|
|
14
|
+
SERIAL = "serial"
|
|
15
|
+
IP = "ip"
|
|
16
|
+
MODBUS = "modbus"
|
|
17
|
+
VISA = "visa"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="syndesi", description="Syndesi command line tool", epilog=""
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument("-v", "--verbose", action="store_true")
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"command",
|
|
31
|
+
choices=[x.value for x in SyndesiCommands],
|
|
32
|
+
help="Command, use syndesi <command> -h for help",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
args, remaining_args = parser.parse_known_args()
|
|
36
|
+
command = SyndesiCommands(args.command)
|
|
37
|
+
|
|
38
|
+
if args.verbose:
|
|
39
|
+
log("DEBUG", console=True)
|
|
40
|
+
|
|
41
|
+
if command == SyndesiCommands.SERIAL:
|
|
42
|
+
AdapterShell(AdapterType.SERIAL, remaining_args).run()
|
|
43
|
+
elif command == SyndesiCommands.IP:
|
|
44
|
+
AdapterShell(AdapterType.IP, remaining_args).run()
|
|
45
|
+
elif command == SyndesiCommands.VISA:
|
|
46
|
+
AdapterShell(AdapterType.VISA, remaining_args).run()
|
|
47
|
+
else:
|
|
48
|
+
raise RuntimeError(f"Command '{command.value}' is not supported yet")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# File : syndesi_backend.py
|
|
2
|
+
# Author : Sébastien Deriaz
|
|
3
|
+
# License : GPL
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ..cli.backend_console import BackendConsole
|
|
8
|
+
from ..cli.backend_status import BackendStatus
|
|
9
|
+
from ..cli.backend_wrapper import BackendWrapper
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
argument_parser = argparse.ArgumentParser(prog="syndesi-backend")
|
|
14
|
+
|
|
15
|
+
argument_parser.add_argument(
|
|
16
|
+
"--status", action="store_true", help="Show backend status"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
argument_parser.add_argument(
|
|
20
|
+
"--console", action="store_true", help="Run backend console"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
args, remaining_args = argument_parser.parse_known_args()
|
|
24
|
+
|
|
25
|
+
if args.status:
|
|
26
|
+
status = BackendStatus(remaining_args)
|
|
27
|
+
status.run()
|
|
28
|
+
elif args.console:
|
|
29
|
+
console = BackendConsole(remaining_args)
|
|
30
|
+
console.run()
|
|
31
|
+
else:
|
|
32
|
+
backend = BackendWrapper(remaining_args)
|
|
33
|
+
backend.run()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# File : backend_api.py
|
|
2
|
+
# Author : Sébastien Deriaz
|
|
3
|
+
# License : GPL
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from multiprocessing.connection import Connection, wait
|
|
8
|
+
from typing import Any, Protocol
|
|
9
|
+
|
|
10
|
+
from syndesi.tools.errors import BackendCommunicationError, BackendError
|
|
11
|
+
|
|
12
|
+
BACKEND_PORT = 59677
|
|
13
|
+
LOCALHOST = "127.0.0.1"
|
|
14
|
+
|
|
15
|
+
default_host = LOCALHOST
|
|
16
|
+
# AUTHKEY = b'syndesi'
|
|
17
|
+
|
|
18
|
+
# Backend protocol format
|
|
19
|
+
# (action, arguments)
|
|
20
|
+
# If the command succeeds, it is returned as :
|
|
21
|
+
# (action, other arguments)
|
|
22
|
+
# If the command fails, it is returned as :
|
|
23
|
+
# (error, error_description)
|
|
24
|
+
|
|
25
|
+
# The backend links the client with an adapter when SELECT_ADAPTER is sent along with an adapter descriptor
|
|
26
|
+
|
|
27
|
+
STANDARD_RESPONSE_TIMEOUT = 2
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Action(Enum):
|
|
31
|
+
# All adapters
|
|
32
|
+
SELECT_ADAPTER = "select"
|
|
33
|
+
OPEN = "open" # (descriptor,stop_condition) -> ()
|
|
34
|
+
CLOSE = (
|
|
35
|
+
"close" # (descriptor,) -> () # Notify the backend that this client will close
|
|
36
|
+
)
|
|
37
|
+
FORCE_CLOSE = "force_close" # (descriptor,) -> ()
|
|
38
|
+
WRITE = "write" # (descriptor,data) -> ()
|
|
39
|
+
READ = "read" # (descriptor,full_output,temporary_timeout,temporary_stop_condition) -> (data,metrics)
|
|
40
|
+
SET_STOP_CONDITION = "set_stop_condition" # (descriptor,stop_condition)
|
|
41
|
+
FLUSHREAD = "flushread"
|
|
42
|
+
#START_READ = "start_read" # Start a read (descriptor,response_time)
|
|
43
|
+
GET_BACKEND_TIME = "get_time"
|
|
44
|
+
|
|
45
|
+
# Events
|
|
46
|
+
ADAPTER_EVENT_DATA_READY = "event_adapter_data_ready"
|
|
47
|
+
ADAPTER_EVENT_DISCONNECTED = "event_adapter_disconnected"
|
|
48
|
+
ADAPTER_EVENT_READ_INIT = "event_adapter_read_init"
|
|
49
|
+
|
|
50
|
+
# Other
|
|
51
|
+
SET_ROLE_ADAPTER = (
|
|
52
|
+
"set_role_adapter" # Define the client as an adapter (exchange of data)
|
|
53
|
+
)
|
|
54
|
+
SET_ROLE_MONITORING = "set_role_monitoring" # The client queries for backend info
|
|
55
|
+
SET_ROLE_LOGGER = "set_role_logger" # The client receives logs
|
|
56
|
+
SET_LOG_LEVEL = "set_log_level"
|
|
57
|
+
PING = "ping"
|
|
58
|
+
STOP = "stop"
|
|
59
|
+
|
|
60
|
+
# Backend debugger
|
|
61
|
+
ENUMERATE_ADAPTER_CONNECTIONS = "enumerate_adapter_connections"
|
|
62
|
+
ENUMERATE_MONITORING_CONNECTIONS = "enumerate_monitoring_connections"
|
|
63
|
+
BACKEND_STATS = "backend_stats"
|
|
64
|
+
|
|
65
|
+
# Errors
|
|
66
|
+
ERROR_GENERIC = "error_generic"
|
|
67
|
+
ERROR_UNKNOWN_ACTION = "error_unknown_action"
|
|
68
|
+
ERROR_INVALID_REQUEST = "error_invalid_request"
|
|
69
|
+
ERROR_ADAPTER_NOT_OPENED = "error_adapter_not_opened"
|
|
70
|
+
ERROR_INVALID_ROLE = "error_invalid_role"
|
|
71
|
+
ERROR_ADAPTER_DISCONNECTED = "error_adapter_disconnected"
|
|
72
|
+
ERROR_BACKEND_DISCONNECTED = "error_backend_disconnected"
|
|
73
|
+
ERROR_FAILED_TO_OPEN = "error_failed_to_open"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_event(action: Action) -> bool:
|
|
77
|
+
return action.value.startswith("event_")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_action_error(action: Action) -> bool:
|
|
81
|
+
return action.value.startswith("error_")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class BackendException(Exception):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ValidFragment(Protocol):
|
|
89
|
+
data: bytes
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class Fragment:
|
|
94
|
+
data: bytes
|
|
95
|
+
timestamp: float | None
|
|
96
|
+
|
|
97
|
+
def __str__(self) -> str:
|
|
98
|
+
return f"{self.data!r}"
|
|
99
|
+
|
|
100
|
+
def __repr__(self) -> str:
|
|
101
|
+
return self.__str__()
|
|
102
|
+
|
|
103
|
+
def __getitem__(self, key: slice) -> "Fragment":
|
|
104
|
+
# if self.data is None:
|
|
105
|
+
# raise IndexError('Cannot index invalid fragment')
|
|
106
|
+
return Fragment(self.data[key], self.timestamp)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# def get_conn_addresses(conn: Connection) -> tuple[str, str]:
|
|
110
|
+
# try:
|
|
111
|
+
# fd = conn.fileno()
|
|
112
|
+
# except OSError:
|
|
113
|
+
# return ("closed", "closed")
|
|
114
|
+
# else:
|
|
115
|
+
# sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
|
|
116
|
+
# try:
|
|
117
|
+
# # address, port = sock.getpeername() # (IP, port) tuple
|
|
118
|
+
# peer_address = sock.getpeername()
|
|
119
|
+
# sock_address = sock.getsockname()
|
|
120
|
+
# return sock_address, peer_address
|
|
121
|
+
# except Exception:
|
|
122
|
+
# return ("error", "closed")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
BackendResponse = tuple[object, ...]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def frontend_send(conn: Connection, action: Action, *args: Any) -> bool:
|
|
129
|
+
try:
|
|
130
|
+
conn.send((action.value, *args))
|
|
131
|
+
except (BrokenPipeError, OSError):
|
|
132
|
+
return False
|
|
133
|
+
else:
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def backend_request(
|
|
138
|
+
conn: Connection,
|
|
139
|
+
action: Action,
|
|
140
|
+
*args: Any,
|
|
141
|
+
timeout: float = STANDARD_RESPONSE_TIMEOUT,
|
|
142
|
+
) -> BackendResponse:
|
|
143
|
+
try:
|
|
144
|
+
conn.send((action.value, *args))
|
|
145
|
+
except (BrokenPipeError, OSError) as err:
|
|
146
|
+
raise BackendCommunicationError("Failed to communicate with backend") from err
|
|
147
|
+
else:
|
|
148
|
+
ready = wait([conn], timeout=timeout)
|
|
149
|
+
if conn not in ready:
|
|
150
|
+
raise BackendCommunicationError(
|
|
151
|
+
"Failed to receive backend response in time"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
raw_response: object = conn.recv()
|
|
156
|
+
except (EOFError, ConnectionResetError) as err:
|
|
157
|
+
raise BackendCommunicationError(
|
|
158
|
+
f"Failed to receive backend response to {action.value}"
|
|
159
|
+
) from err
|
|
160
|
+
|
|
161
|
+
# Check if the response is correctly formatted
|
|
162
|
+
if not (isinstance(raw_response, tuple) and isinstance(raw_response[0], str)):
|
|
163
|
+
raise BackendCommunicationError(
|
|
164
|
+
f"Invalid response received from backend : {raw_response}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
response_action: Action = Action(raw_response[0])
|
|
168
|
+
arguments: tuple[Any, ...] = raw_response[1:]
|
|
169
|
+
|
|
170
|
+
if is_action_error(response_action):
|
|
171
|
+
if len(arguments) > 0:
|
|
172
|
+
if isinstance(arguments[0], str):
|
|
173
|
+
error_message: str = arguments[0]
|
|
174
|
+
else:
|
|
175
|
+
error_message = "failed to read error message"
|
|
176
|
+
else:
|
|
177
|
+
error_message = "Missing error message"
|
|
178
|
+
raise BackendError(f"{response_action} : {error_message}")
|
|
179
|
+
return arguments
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
backend_send = frontend_send
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def raise_if_error(response: BackendResponse) -> None:
|
|
186
|
+
action = Action(response[0])
|
|
187
|
+
if is_action_error(action):
|
|
188
|
+
if len(response) > 1:
|
|
189
|
+
description = response[1]
|
|
190
|
+
else:
|
|
191
|
+
description = f"{action}"
|
|
192
|
+
raise BackendException(f"{action.name}/{description}")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class AdapterBackendStatus(Enum):
|
|
197
|
+
DISCONNECTED = 0
|
|
198
|
+
CONNECTED = 1
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ClientStatus(Enum):
|
|
202
|
+
DISCONNECTED = 0
|
|
203
|
+
CONNECTED = 1
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# class StatusSnapshot(TypedDict):
|
|
207
|
+
# type : Literal['snapshot']
|
|
208
|
+
# adapters :
|
|
209
|
+
# #clients : List[str]
|