syndesi 0.1.5__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.
- {syndesi-0.1.5/syndesi.egg-info → syndesi-0.2.0}/PKG-INFO +17 -15
- {syndesi-0.1.5 → syndesi-0.2.0}/README.md +16 -14
- {syndesi-0.1.5 → syndesi-0.2.0}/setup.py +6 -2
- syndesi-0.2.0/syndesi/adapters/__init__.py +8 -0
- syndesi-0.2.0/syndesi/adapters/adapter.py +321 -0
- syndesi-0.2.0/syndesi/adapters/auto.py +45 -0
- syndesi-0.2.0/syndesi/adapters/ip.py +150 -0
- syndesi-0.2.0/syndesi/adapters/ip_server.py +108 -0
- syndesi-0.2.0/syndesi/adapters/proxy.py +92 -0
- syndesi-0.2.0/syndesi/adapters/serialport.py +152 -0
- syndesi-0.2.0/syndesi/adapters/stop_conditions.py +161 -0
- syndesi-0.2.0/syndesi/adapters/timed_queue.py +32 -0
- syndesi-0.2.0/syndesi/adapters/timeout.py +288 -0
- {syndesi-0.1.5 → syndesi-0.2.0}/syndesi/adapters/visa.py +10 -8
- syndesi-0.2.0/syndesi/protocols/__init__.py +5 -0
- {syndesi-0.1.5 → syndesi-0.2.0}/syndesi/protocols/delimited.py +37 -12
- syndesi-0.2.0/syndesi/protocols/protocol.py +17 -0
- syndesi-0.2.0/syndesi/protocols/raw.py +28 -0
- syndesi-0.2.0/syndesi/protocols/scpi.py +83 -0
- syndesi-0.2.0/syndesi/protocols/sdp.py +14 -0
- {syndesi-0.1.5/syndesi → syndesi-0.2.0/syndesi/tools}/__init__.py +0 -0
- syndesi-0.2.0/syndesi/tools/log.py +107 -0
- syndesi-0.2.0/syndesi/tools/others.py +1 -0
- syndesi-0.2.0/syndesi/tools/shell.py +111 -0
- {syndesi-0.1.5 → syndesi-0.2.0}/syndesi/tools/types.py +24 -18
- {syndesi-0.1.5 → syndesi-0.2.0/syndesi.egg-info}/PKG-INFO +17 -15
- syndesi-0.2.0/syndesi.egg-info/SOURCES.txt +32 -0
- syndesi-0.2.0/syndesi.egg-info/entry_points.txt +3 -0
- syndesi-0.2.0/syndesi.egg-info/top_level.txt +1 -0
- syndesi-0.1.5/bin/syndesi +0 -1
- syndesi-0.1.5/syndesi/adapters/__init__.py +0 -4
- syndesi-0.1.5/syndesi/adapters/iadapter.py +0 -73
- syndesi-0.1.5/syndesi/adapters/ip.py +0 -84
- syndesi-0.1.5/syndesi/adapters/serial.py +0 -37
- syndesi-0.1.5/syndesi/descriptors/Serial.py +0 -10
- syndesi-0.1.5/syndesi/descriptors/__init__.py +0 -1
- syndesi-0.1.5/syndesi/descriptors/descriptor.py +0 -9
- syndesi-0.1.5/syndesi/descriptors/ip.py +0 -9
- syndesi-0.1.5/syndesi/descriptors/syndesi/Syndesi.py +0 -9
- syndesi-0.1.5/syndesi/descriptors/syndesi/__init__.py +0 -0
- syndesi-0.1.5/syndesi/descriptors/syndesi/_device.py +0 -25
- syndesi-0.1.5/syndesi/descriptors/syndesi/devices.py +0 -10
- syndesi-0.1.5/syndesi/descriptors/syndesi/frame.py +0 -133
- syndesi-0.1.5/syndesi/descriptors/syndesi/network.py +0 -41
- syndesi-0.1.5/syndesi/descriptors/syndesi/payload.py +0 -11
- syndesi-0.1.5/syndesi/descriptors/syndesi/sdid.py +0 -21
- syndesi-0.1.5/syndesi/descriptors/visa.py +0 -31
- syndesi-0.1.5/syndesi/protocols/__init__.py +0 -5
- syndesi-0.1.5/syndesi/protocols/iprotocol.py +0 -14
- syndesi-0.1.5/syndesi/protocols/raw.py +0 -79
- syndesi-0.1.5/syndesi/protocols/scpi.py +0 -62
- syndesi-0.1.5/syndesi/protocols/sdp.py +0 -14
- syndesi-0.1.5/syndesi/tools/__init__.py +0 -0
- syndesi-0.1.5/syndesi/tools/stop_conditions.py +0 -148
- syndesi-0.1.5/syndesi.egg-info/SOURCES.txt +0 -38
- syndesi-0.1.5/syndesi.egg-info/top_level.txt +0 -2
- {syndesi-0.1.5 → syndesi-0.2.0}/LICENSE +0 -0
- {syndesi-0.1.5 → syndesi-0.2.0}/setup.cfg +0 -0
- {syndesi-0.1.5/experiments → syndesi-0.2.0/syndesi}/__init__.py +0 -0
- {syndesi-0.1.5 → syndesi-0.2.0}/syndesi/tools/exceptions.py +0 -0
- {syndesi-0.1.5 → syndesi-0.2.0}/syndesi.egg-info/dependency_links.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: syndesi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Syndesi
|
|
5
5
|
Author: Sebastien Deriaz
|
|
6
6
|
Author-email: sebastien.deriaz1@gmail.com
|
|
@@ -16,6 +16,8 @@ License-File: LICENSE
|
|
|
16
16
|
|
|
17
17
|
# Syndesi Python Implementation
|
|
18
18
|
|
|
19
|
+
Syndesi description is available [here](https://github.com/syndesi-project/Syndesi/README.md)
|
|
20
|
+
|
|
19
21
|
## Installation
|
|
20
22
|
|
|
21
23
|
The syndesi Python package can be installed through pip
|
|
@@ -25,14 +27,16 @@ The syndesi Python package can be installed through pip
|
|
|
25
27
|
The package can also be installed locally by cloning this repository
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
|
-
git clone
|
|
30
|
+
git clone https://github.com/syndesi-project/Syndesi
|
|
29
31
|
cd Syndesi/Python
|
|
30
32
|
pip install .
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
## Usage
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
To instantiate a device, one must import the device and a suitable adapter
|
|
36
40
|
|
|
37
41
|
```python
|
|
38
42
|
# 1) Import the device
|
|
@@ -46,16 +50,6 @@ mm = SDM3055(IP("192.168.1.123"))
|
|
|
46
50
|
## 4) Use
|
|
47
51
|
voltage = mm.measure_dc_voltage()
|
|
48
52
|
```
|
|
49
|
-
|
|
50
|
-
The Syndesi Python package provides the user with the necessary tools to control compatible devices
|
|
51
|
-
|
|
52
|
-
- drivers : device-specific implementation
|
|
53
|
-
- descriptors : Each class represents a particular way of connecting to a device, the user must provide que necessary information (IP, com port, ID, etc...)
|
|
54
|
-
- communication wrapper (wrappers) : Wrappers for low-level communication (TCP, UDP, UART, etc...)
|
|
55
|
-
- IP (TCP / UDP)
|
|
56
|
-
- UART
|
|
57
|
-
- USB (?)
|
|
58
|
-
|
|
59
53
|
## Layers
|
|
60
54
|
|
|
61
55
|
The first layer is the "Device" base class
|
|
@@ -88,6 +82,14 @@ The Syndesi Device Protocol is a light-weight and easy interface to send / recei
|
|
|
88
82
|
|
|
89
83
|
## Notes
|
|
90
84
|
|
|
91
|
-
15.08.2023 : The adapters must work with bytearray data only
|
|
92
|
-
|
|
93
85
|
06.09.2023 : bytearray is changed to bytes everywhere
|
|
86
|
+
|
|
87
|
+
23.10.2023 : continuation timeout isn't suitable for TCP, but it can work for UDP as a UDP server can send multiple response packets after a single packet from the client. This can be handled in different ways by firewalls. Thankfull that's none of our business so continuation timeout can be implemented
|
|
88
|
+
|
|
89
|
+
22.11.2023 : The timeout and stop conditions strategy is a bit complicated :
|
|
90
|
+
|
|
91
|
+
- What if we receive the message b'ACK\nNCK\n' using a termination stop condition but we receive b'ACK', then a timeout, then b'\nNCK\n' ?
|
|
92
|
+
- Should the first part be kept ? should an error be raised at the timeout because nothing was read ?
|
|
93
|
+
- Two kinds of timeouts ?
|
|
94
|
+
- One where "we read as much as possible during the available time"
|
|
95
|
+
- One where "we expect a response within X otherwise it's trash"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Syndesi Python Implementation
|
|
2
2
|
|
|
3
|
+
Syndesi description is available [here](https://github.com/syndesi-project/Syndesi/README.md)
|
|
4
|
+
|
|
3
5
|
## Installation
|
|
4
6
|
|
|
5
7
|
The syndesi Python package can be installed through pip
|
|
@@ -9,14 +11,16 @@ The syndesi Python package can be installed through pip
|
|
|
9
11
|
The package can also be installed locally by cloning this repository
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
|
-
git clone
|
|
14
|
+
git clone https://github.com/syndesi-project/Syndesi
|
|
13
15
|
cd Syndesi/Python
|
|
14
16
|
pip install .
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
## Usage
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
To instantiate a device, one must import the device and a suitable adapter
|
|
20
24
|
|
|
21
25
|
```python
|
|
22
26
|
# 1) Import the device
|
|
@@ -30,16 +34,6 @@ mm = SDM3055(IP("192.168.1.123"))
|
|
|
30
34
|
## 4) Use
|
|
31
35
|
voltage = mm.measure_dc_voltage()
|
|
32
36
|
```
|
|
33
|
-
|
|
34
|
-
The Syndesi Python package provides the user with the necessary tools to control compatible devices
|
|
35
|
-
|
|
36
|
-
- drivers : device-specific implementation
|
|
37
|
-
- descriptors : Each class represents a particular way of connecting to a device, the user must provide que necessary information (IP, com port, ID, etc...)
|
|
38
|
-
- communication wrapper (wrappers) : Wrappers for low-level communication (TCP, UDP, UART, etc...)
|
|
39
|
-
- IP (TCP / UDP)
|
|
40
|
-
- UART
|
|
41
|
-
- USB (?)
|
|
42
|
-
|
|
43
37
|
## Layers
|
|
44
38
|
|
|
45
39
|
The first layer is the "Device" base class
|
|
@@ -72,6 +66,14 @@ The Syndesi Device Protocol is a light-weight and easy interface to send / recei
|
|
|
72
66
|
|
|
73
67
|
## Notes
|
|
74
68
|
|
|
75
|
-
15.08.2023 : The adapters must work with bytearray data only
|
|
76
|
-
|
|
77
69
|
06.09.2023 : bytearray is changed to bytes everywhere
|
|
70
|
+
|
|
71
|
+
23.10.2023 : continuation timeout isn't suitable for TCP, but it can work for UDP as a UDP server can send multiple response packets after a single packet from the client. This can be handled in different ways by firewalls. Thankfull that's none of our business so continuation timeout can be implemented
|
|
72
|
+
|
|
73
|
+
22.11.2023 : The timeout and stop conditions strategy is a bit complicated :
|
|
74
|
+
|
|
75
|
+
- What if we receive the message b'ACK\nNCK\n' using a termination stop condition but we receive b'ACK', then a timeout, then b'\nNCK\n' ?
|
|
76
|
+
- Should the first part be kept ? should an error be raised at the timeout because nothing was read ?
|
|
77
|
+
- Two kinds of timeouts ?
|
|
78
|
+
- One where "we read as much as possible during the available time"
|
|
79
|
+
- One where "we expect a response within X otherwise it's trash"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from setuptools import setup, find_packages
|
|
2
2
|
|
|
3
|
-
VERSION = '0.
|
|
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
|
-
|
|
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'],
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# adapters.py
|
|
2
|
+
# Sébastien Deriaz
|
|
3
|
+
# 06.05.2023
|
|
4
|
+
#
|
|
5
|
+
# Adapters provide a common abstraction for the media layers (physical + data link + network)
|
|
6
|
+
# The following classes are provided, which all are derived from the main Adapter class
|
|
7
|
+
# - IP
|
|
8
|
+
# - Serial
|
|
9
|
+
# - VISA
|
|
10
|
+
#
|
|
11
|
+
# Note that technically VISA is not part of the media layer, only USB is.
|
|
12
|
+
# This is a limitation as it is to this day not possible to communicate "raw"
|
|
13
|
+
# with a device through USB yet
|
|
14
|
+
#
|
|
15
|
+
# An adapter is meant to work with bytes objects but it can accept strings.
|
|
16
|
+
# Strings will automatically be converted to bytes using utf-8 encoding
|
|
17
|
+
#
|
|
18
|
+
|
|
19
|
+
from abc import abstractmethod, ABC
|
|
20
|
+
from .timed_queue import TimedQueue
|
|
21
|
+
from threading import Thread
|
|
22
|
+
from typing import Union
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from .stop_conditions import StopCondition, Termination, Length
|
|
25
|
+
from .timeout import Timeout, TimeoutException, timeout_fuse
|
|
26
|
+
from typing import Union
|
|
27
|
+
from ..tools.types import is_number
|
|
28
|
+
from ..tools.log import LoggerAlias
|
|
29
|
+
import logging
|
|
30
|
+
from time import time
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from ..tools.others import DEFAULT
|
|
33
|
+
|
|
34
|
+
DEFAULT_TIMEOUT = Timeout(response=1, continuation=100e-3, total=None)
|
|
35
|
+
DEFAULT_STOP_CONDITION = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AdapterDisconnected(Exception):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
STOP_DESIGNATORS = {
|
|
42
|
+
'timeout' : {
|
|
43
|
+
Timeout.TimeoutType.RESPONSE : 'TR',
|
|
44
|
+
Timeout.TimeoutType.CONTINUATION : 'TC',
|
|
45
|
+
Timeout.TimeoutType.TOTAL : 'TT'
|
|
46
|
+
},
|
|
47
|
+
'stop_condition' : {
|
|
48
|
+
Termination : 'ST',
|
|
49
|
+
Length : 'SL'
|
|
50
|
+
},
|
|
51
|
+
'previous-read-buffer' : 'RB'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class Origin(Enum):
|
|
55
|
+
TIMEOUT = 'timeout'
|
|
56
|
+
STOP_CONDITION = 'stop_condition'
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ReturnMetrics:
|
|
60
|
+
read_duration : float
|
|
61
|
+
origin : Origin
|
|
62
|
+
timeout_type : Timeout.TimeoutType
|
|
63
|
+
stop_condition : StopCondition
|
|
64
|
+
previous_read_buffer_used : bool
|
|
65
|
+
n_fragments : int
|
|
66
|
+
response_time : float
|
|
67
|
+
continuation_times : list
|
|
68
|
+
total_time : float
|
|
69
|
+
|
|
70
|
+
class Adapter(ABC):
|
|
71
|
+
class Status(Enum):
|
|
72
|
+
DISCONNECTED = 0
|
|
73
|
+
CONNECTED = 1
|
|
74
|
+
|
|
75
|
+
def __init__(self, alias : str = '', stop_condition : Union[StopCondition, None] = DEFAULT, timeout : Union[float, Timeout] = DEFAULT) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Adapter instance
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
alias : str
|
|
82
|
+
The alias is used to identify the class in the logs
|
|
83
|
+
timeout : float or Timeout instance
|
|
84
|
+
Default timeout is Timeout(response=1, continuation=0.1, total=None)
|
|
85
|
+
stop_condition : StopCondition or None
|
|
86
|
+
Default to None
|
|
87
|
+
"""
|
|
88
|
+
super().__init__()
|
|
89
|
+
self._alias = alias
|
|
90
|
+
|
|
91
|
+
self._default_stop_condition = stop_condition == DEFAULT
|
|
92
|
+
if self._default_stop_condition:
|
|
93
|
+
self._stop_condition = DEFAULT_STOP_CONDITION
|
|
94
|
+
else:
|
|
95
|
+
self._stop_condition = stop_condition
|
|
96
|
+
self._read_queue = TimedQueue()
|
|
97
|
+
self._thread : Union[Thread, None] = None
|
|
98
|
+
self._status = self.Status.DISCONNECTED
|
|
99
|
+
self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
|
|
100
|
+
|
|
101
|
+
# Buffer for data that has been pulled from the queue but
|
|
102
|
+
# not used because of termination or length stop condition
|
|
103
|
+
self._previous_read_buffer = b''
|
|
104
|
+
|
|
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)}")
|
|
115
|
+
|
|
116
|
+
def set_default_timeout(self, default_timeout : Union[Timeout, tuple, float]):
|
|
117
|
+
"""
|
|
118
|
+
Set the default timeout for this adapter. If a previous timeout has been set, it will be fused
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
default_timeout : Timeout or tuple or float
|
|
123
|
+
"""
|
|
124
|
+
if self._default_timeout:
|
|
125
|
+
self._timeout = default_timeout
|
|
126
|
+
else:
|
|
127
|
+
self._timeout = timeout_fuse(self._timeout, default_timeout)
|
|
128
|
+
|
|
129
|
+
def set_default_stop_condition(self, stop_condition):
|
|
130
|
+
"""
|
|
131
|
+
Set the default stop condition for this adapter.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
stop_condition : StopCondition
|
|
136
|
+
"""
|
|
137
|
+
if self._default_stop_condition:
|
|
138
|
+
self._stop_condition = stop_condition
|
|
139
|
+
|
|
140
|
+
def flushRead(self):
|
|
141
|
+
"""
|
|
142
|
+
Flush the input buffer
|
|
143
|
+
"""
|
|
144
|
+
self._read_queue.clear()
|
|
145
|
+
self._previous_read_buffer = b''
|
|
146
|
+
|
|
147
|
+
@abstractmethod
|
|
148
|
+
def open(self):
|
|
149
|
+
"""
|
|
150
|
+
Start communication with the device
|
|
151
|
+
"""
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
def close(self):
|
|
156
|
+
"""
|
|
157
|
+
Stop communication with the device
|
|
158
|
+
"""
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
@abstractmethod
|
|
162
|
+
def write(self, data : Union[bytes, str]):
|
|
163
|
+
"""
|
|
164
|
+
Send data to the device
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
data : bytes or str
|
|
169
|
+
"""
|
|
170
|
+
pass
|
|
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
|
+
|
|
176
|
+
def read(self, timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
|
|
177
|
+
"""
|
|
178
|
+
Read data from the device
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
timeout : Timeout or None
|
|
183
|
+
Set a custom timeout, if None (default), the adapter timeout is used
|
|
184
|
+
stop_condition : StopCondition or None
|
|
185
|
+
Set a custom stop condition, if None (Default), the adapater stop condition is used
|
|
186
|
+
return_metrics : ReturnMetrics class
|
|
187
|
+
"""
|
|
188
|
+
read_start = time()
|
|
189
|
+
if self._status == self.Status.DISCONNECTED:
|
|
190
|
+
self.open()
|
|
191
|
+
|
|
192
|
+
# Use adapter values if no custom value is specified
|
|
193
|
+
if timeout is None:
|
|
194
|
+
timeout = self._timeout
|
|
195
|
+
elif isinstance(timeout, float):
|
|
196
|
+
timeout = Timeout(timeout)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if stop_condition is None:
|
|
200
|
+
stop_condition = self._stop_condition
|
|
201
|
+
|
|
202
|
+
# If the adapter is closed, open it
|
|
203
|
+
if self._status == self.Status.DISCONNECTED:
|
|
204
|
+
self.open()
|
|
205
|
+
|
|
206
|
+
if self._thread is None or not self._thread.is_alive():
|
|
207
|
+
self._start_thread()
|
|
208
|
+
|
|
209
|
+
timeout_ms = timeout.initiate_read(len(self._previous_read_buffer) > 0)
|
|
210
|
+
|
|
211
|
+
if stop_condition is not None:
|
|
212
|
+
stop_condition.initiate_read()
|
|
213
|
+
|
|
214
|
+
deferred_buffer = b''
|
|
215
|
+
|
|
216
|
+
# Start with the deferred buffer
|
|
217
|
+
# TODO : Check if data could be lost here, like the data is put in the previous_read_buffer and is never
|
|
218
|
+
# read back again because there's no stop condition
|
|
219
|
+
if len(self._previous_read_buffer) > 0 and stop_condition is not None:
|
|
220
|
+
stop, output, self._previous_read_buffer = stop_condition.evaluate(self._previous_read_buffer)
|
|
221
|
+
previous_read_buffer_used = True
|
|
222
|
+
else:
|
|
223
|
+
stop = False
|
|
224
|
+
output = b''
|
|
225
|
+
previous_read_buffer_used = False
|
|
226
|
+
|
|
227
|
+
n_fragments = 0
|
|
228
|
+
# If everything is used up, read the queue
|
|
229
|
+
if not stop:
|
|
230
|
+
while True:
|
|
231
|
+
(timestamp, fragment) = self._read_queue.get(timeout_ms)
|
|
232
|
+
n_fragments += 1
|
|
233
|
+
|
|
234
|
+
if fragment == b'':
|
|
235
|
+
raise AdapterDisconnected()
|
|
236
|
+
|
|
237
|
+
# 1) Evaluate the timeout
|
|
238
|
+
stop, timeout_ms = timeout.evaluate(timestamp)
|
|
239
|
+
if stop:
|
|
240
|
+
data_strategy, origin = timeout.dataStrategy()
|
|
241
|
+
if data_strategy == Timeout.OnTimeoutStrategy.DISCARD:
|
|
242
|
+
# Trash everything
|
|
243
|
+
output = b''
|
|
244
|
+
elif data_strategy == Timeout.OnTimeoutStrategy.RETURN:
|
|
245
|
+
# Return the data that has been read up to this point
|
|
246
|
+
output += deferred_buffer
|
|
247
|
+
if fragment is not None:
|
|
248
|
+
output += fragment
|
|
249
|
+
elif data_strategy == Timeout.OnTimeoutStrategy.STORE:
|
|
250
|
+
# Store the data
|
|
251
|
+
self._previous_read_buffer = output
|
|
252
|
+
output = b''
|
|
253
|
+
elif data_strategy == Timeout.OnTimeoutStrategy.ERROR:
|
|
254
|
+
raise TimeoutException(origin)
|
|
255
|
+
break
|
|
256
|
+
else:
|
|
257
|
+
origin = None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Add the deferred buffer
|
|
262
|
+
if len(deferred_buffer) > 0:
|
|
263
|
+
fragment = deferred_buffer + fragment
|
|
264
|
+
|
|
265
|
+
# 2) Evaluate the stop condition
|
|
266
|
+
if stop_condition is not None:
|
|
267
|
+
stop, kept_fragment, deferred_buffer = stop_condition.evaluate(fragment)
|
|
268
|
+
output += kept_fragment
|
|
269
|
+
if stop:
|
|
270
|
+
self._previous_read_buffer = deferred_buffer
|
|
271
|
+
else:
|
|
272
|
+
output += fragment
|
|
273
|
+
if stop:
|
|
274
|
+
break
|
|
275
|
+
|
|
276
|
+
if origin is not None:
|
|
277
|
+
# The stop originates from the timeout
|
|
278
|
+
designator = STOP_DESIGNATORS['timeout'][origin]
|
|
279
|
+
else:
|
|
280
|
+
designator = STOP_DESIGNATORS['stop_condition'][type(stop_condition)]
|
|
281
|
+
else:
|
|
282
|
+
designator = STOP_DESIGNATORS['previous-read-buffer']
|
|
283
|
+
|
|
284
|
+
read_duration = time() - read_start
|
|
285
|
+
if self._previous_read_buffer:
|
|
286
|
+
self._logger.debug(f'Read [{designator}, {read_duration*1e3:.3f}ms] : {output} , previous read buffer : {self._previous_read_buffer}')
|
|
287
|
+
else:
|
|
288
|
+
self._logger.debug(f'Read [{designator}, {read_duration*1e3:.3f}ms] : {output}')
|
|
289
|
+
|
|
290
|
+
if return_metrics:
|
|
291
|
+
return output, ReturnMetrics(
|
|
292
|
+
read_duration=read_duration,
|
|
293
|
+
origin=Origin.TIMEOUT if origin is not None else Origin.STOP_CONDITION,
|
|
294
|
+
timeout_type=origin if origin is not None else None,
|
|
295
|
+
stop_condition=type(stop_condition) if origin is None else None,
|
|
296
|
+
previous_read_buffer_used=previous_read_buffer_used,
|
|
297
|
+
n_fragments=n_fragments,
|
|
298
|
+
response_time=timeout.response_time,
|
|
299
|
+
continuation_times=timeout.continuation_times,
|
|
300
|
+
total_time=timeout.total_time
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
return output
|
|
304
|
+
|
|
305
|
+
@abstractmethod
|
|
306
|
+
def _start_thread(self):
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
def __del__(self):
|
|
310
|
+
self.close()
|
|
311
|
+
|
|
312
|
+
@abstractmethod
|
|
313
|
+
def query(self, data : Union[bytes, str], timeout=None, stop_condition=None, return_metrics : bool = False) -> bytes:
|
|
314
|
+
"""
|
|
315
|
+
Shortcut function that combines
|
|
316
|
+
- flush_read
|
|
317
|
+
- write
|
|
318
|
+
- read
|
|
319
|
+
"""
|
|
320
|
+
pass
|
|
321
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# auto.py
|
|
2
|
+
# Sébastien Deriaz
|
|
3
|
+
# 24.06.2024
|
|
4
|
+
#
|
|
5
|
+
# Automatic adapter function
|
|
6
|
+
# This function is used to automatically choose an adapter based on the user's input
|
|
7
|
+
# 192.168.1.1 -> IP
|
|
8
|
+
# COM4 -> Serial
|
|
9
|
+
# /dev/tty* -> Serial
|
|
10
|
+
# etc...
|
|
11
|
+
# If an adapter class is supplied, it is simply passed through
|
|
12
|
+
#
|
|
13
|
+
# Additionnaly, it is possible to do COM4:115200 so as to make the life of the user easier
|
|
14
|
+
# Same with /dev/ttyACM0:115200
|
|
15
|
+
|
|
16
|
+
from typing import Union
|
|
17
|
+
import re
|
|
18
|
+
from . import Adapter, IP, SerialPort
|
|
19
|
+
|
|
20
|
+
IP_PATTERN = '([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(:[0-9]+)*'
|
|
21
|
+
|
|
22
|
+
WINDOWS_SERIAL_PATTERN = '(COM[0-9]+)(:[0-9]+)*'
|
|
23
|
+
LINUX_SERIAL_PATTERN = '(/dev/tty[a-zA-Z0-9]+)(:[0-9]+)*'
|
|
24
|
+
|
|
25
|
+
def auto_adapter(adapter_or_string : Union[Adapter, str]):
|
|
26
|
+
if isinstance(adapter_or_string, Adapter):
|
|
27
|
+
# Simply return it
|
|
28
|
+
return adapter_or_string
|
|
29
|
+
elif isinstance(adapter_or_string, str):
|
|
30
|
+
# Parse it
|
|
31
|
+
ip_match = re.match(IP_PATTERN, adapter_or_string)
|
|
32
|
+
if ip_match:
|
|
33
|
+
# Return an IP adapter
|
|
34
|
+
return IP(address=ip_match.groups(0), port=ip_match.groups(1))
|
|
35
|
+
elif re.match(WINDOWS_SERIAL_PATTERN, adapter_or_string):
|
|
36
|
+
port, baudrate = re.match(WINDOWS_SERIAL_PATTERN, adapter_or_string).groups()
|
|
37
|
+
return SerialPort(port=port, baudrate=int(baudrate))
|
|
38
|
+
elif re.match(LINUX_SERIAL_PATTERN, adapter_or_string):
|
|
39
|
+
port, baudrate = re.match(LINUX_SERIAL_PATTERN, adapter_or_string)
|
|
40
|
+
return SerialPort(port=port, baudrate=int(baudrate))
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(f"Couldn't parse adapter description : {adapter_or_string}")
|
|
43
|
+
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(f"Invalid adapter : {adapter_or_string}")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from .adapter import Adapter
|
|
4
|
+
from ..tools.types import to_bytes
|
|
5
|
+
from .timeout import Timeout
|
|
6
|
+
from threading import Thread
|
|
7
|
+
from .timed_queue import TimedQueue
|
|
8
|
+
from typing import Union
|
|
9
|
+
from time import time
|
|
10
|
+
import argparse
|
|
11
|
+
from ..tools import shell
|
|
12
|
+
|
|
13
|
+
class IP(Adapter):
|
|
14
|
+
DEFAULT_RESPONSE_TIMEOUT = 1
|
|
15
|
+
DEFAULT_CONTINUATION_TIMEOUT = 1e-3
|
|
16
|
+
DEFAULT_TOTAL_TIMEOUT = 5
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_TIMEOUT = Timeout(
|
|
20
|
+
response=DEFAULT_RESPONSE_TIMEOUT,
|
|
21
|
+
continuation=DEFAULT_CONTINUATION_TIMEOUT,
|
|
22
|
+
total=DEFAULT_TOTAL_TIMEOUT)
|
|
23
|
+
DEFAULT_BUFFER_SIZE = 1024
|
|
24
|
+
class Protocol(Enum):
|
|
25
|
+
TCP = 'TCP'
|
|
26
|
+
UDP = 'UDP'
|
|
27
|
+
|
|
28
|
+
def __init__(self,
|
|
29
|
+
address : str,
|
|
30
|
+
port : int = None,
|
|
31
|
+
transport : str = 'TCP',
|
|
32
|
+
timeout : Union[Timeout, float] = DEFAULT_TIMEOUT,
|
|
33
|
+
stop_condition = None,
|
|
34
|
+
alias : str = '',
|
|
35
|
+
buffer_size : int = DEFAULT_BUFFER_SIZE,
|
|
36
|
+
_socket : socket.socket = None):
|
|
37
|
+
"""
|
|
38
|
+
IP stack adapter
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
address : str
|
|
43
|
+
IP description
|
|
44
|
+
port : int
|
|
45
|
+
IP port
|
|
46
|
+
transport : str
|
|
47
|
+
'TCP' or 'UDP'
|
|
48
|
+
timeout : Timeout | float
|
|
49
|
+
Specify communication timeout
|
|
50
|
+
stop_condition : StopCondition
|
|
51
|
+
Specify a read stop condition (None by default)
|
|
52
|
+
alias : str
|
|
53
|
+
Specify an alias for this adapter, '' by default
|
|
54
|
+
buffer_size : int
|
|
55
|
+
Socket buffer size, may be removed in the future
|
|
56
|
+
socket : socket.socket
|
|
57
|
+
Specify a custom socket, this is reserved for server application
|
|
58
|
+
"""
|
|
59
|
+
super().__init__(alias=alias, timeout=timeout, stop_condition=stop_condition)
|
|
60
|
+
self._transport = self.Protocol(transport)
|
|
61
|
+
self._is_server = _socket is not None
|
|
62
|
+
|
|
63
|
+
self._logger.info(f"Setting up {self._transport.value} IP adapter ({'server' if self._is_server else 'client'})")
|
|
64
|
+
|
|
65
|
+
if self._is_server:
|
|
66
|
+
# Server
|
|
67
|
+
self._socket = _socket
|
|
68
|
+
self._status = self.Status.CONNECTED
|
|
69
|
+
elif self._transport == self.Protocol.TCP:
|
|
70
|
+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
71
|
+
elif self._transport == self.Protocol.UDP:
|
|
72
|
+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
73
|
+
|
|
74
|
+
self._address = address
|
|
75
|
+
self._port = port
|
|
76
|
+
self._buffer_size = buffer_size
|
|
77
|
+
|
|
78
|
+
def set_default_port(self, port):
|
|
79
|
+
"""
|
|
80
|
+
Sets IP port if no port has been set yet.
|
|
81
|
+
|
|
82
|
+
This way, the user can leave the port empty
|
|
83
|
+
and the driver/protocol can specify it later
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
port : int
|
|
88
|
+
"""
|
|
89
|
+
if self._port is None:
|
|
90
|
+
self._port = port
|
|
91
|
+
|
|
92
|
+
def open(self):
|
|
93
|
+
if self._is_server:
|
|
94
|
+
raise SystemError("Cannot open server socket. It must be passed already opened")
|
|
95
|
+
if self._port is None:
|
|
96
|
+
raise ValueError(f"Cannot open adapter without specifying a port")
|
|
97
|
+
|
|
98
|
+
self._logger.debug(f"Adapter {self._alias} connect to ({self._address}, {self._port})")
|
|
99
|
+
self._socket.connect((self._address, self._port))
|
|
100
|
+
self._status = self.Status.CONNECTED
|
|
101
|
+
self._logger.info(f"Adapter {self._alias} opened !")
|
|
102
|
+
|
|
103
|
+
def close(self):
|
|
104
|
+
if hasattr(self, '_socket'):
|
|
105
|
+
self._socket.close()
|
|
106
|
+
self._logger.info("Adapter closed !")
|
|
107
|
+
self._status = self.Status.DISCONNECTED
|
|
108
|
+
|
|
109
|
+
def write(self, data : Union[bytes, str]):
|
|
110
|
+
data = to_bytes(data)
|
|
111
|
+
if self._status == self.Status.DISCONNECTED:
|
|
112
|
+
self._logger.info(f"Adapter {self._alias} is closed, opening...")
|
|
113
|
+
self.open()
|
|
114
|
+
write_start = time()
|
|
115
|
+
self._socket.send(data)
|
|
116
|
+
write_duration = time() - write_start
|
|
117
|
+
self._logger.debug(f"Written [{write_duration*1e3:.3f}ms]: {repr(data)}")
|
|
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
|
+
|
|
129
|
+
def _read_thread(self, socket : socket.socket, read_queue : TimedQueue):
|
|
130
|
+
while True: # TODO : Add stop_pipe ? Maybe it was removed ?
|
|
131
|
+
try:
|
|
132
|
+
payload = socket.recv(self._buffer_size)
|
|
133
|
+
if len(payload) == self._buffer_size and self._transport == self.Protocol.UDP:
|
|
134
|
+
self._logger.warning("Warning, inbound UDP data may have been lost (max buffer size attained)")
|
|
135
|
+
except OSError:
|
|
136
|
+
break
|
|
137
|
+
# If payload is empty, it means the socket has been disconnected
|
|
138
|
+
if payload == b'':
|
|
139
|
+
read_queue.put(payload)
|
|
140
|
+
break
|
|
141
|
+
read_queue.put(payload)
|
|
142
|
+
|
|
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")
|
|
146
|
+
self.flushRead()
|
|
147
|
+
self.write(data)
|
|
148
|
+
return self.read(timeout=timeout, stop_condition=stop_condition, return_metrics=return_metrics)
|
|
149
|
+
|
|
150
|
+
|