syndesi 0.3.2__tar.gz → 0.4.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.3.2/syndesi.egg-info → syndesi-0.4.0}/PKG-INFO +2 -1
- {syndesi-0.3.2 → syndesi-0.4.0}/pyproject.toml +1 -1
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/adapter.py +191 -159
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/adapter_backend.py +97 -63
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/adapter_session.py +51 -151
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/serialport_backend.py +14 -8
- syndesi-0.4.0/syndesi/adapters/backend/stop_condition_backend.py +198 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/serialport.py +4 -5
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/stop_condition.py +47 -26
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/timeout.py +1 -1
- syndesi-0.4.0/syndesi/cli/shell_tools.py +105 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/backend_api.py +14 -15
- syndesi-0.4.0/syndesi/tools/errors.py +51 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/version.py +1 -1
- {syndesi-0.3.2 → syndesi-0.4.0/syndesi.egg-info}/PKG-INFO +2 -1
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi.egg-info/requires.txt +1 -0
- syndesi-0.3.2/syndesi/adapters/backend/stop_condition_backend.py +0 -342
- syndesi-0.3.2/syndesi/cli/shell_tools.py +0 -107
- syndesi-0.3.2/syndesi/tools/errors.py +0 -23
- {syndesi-0.3.2 → syndesi-0.4.0}/LICENSE +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/README.md +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/setup.cfg +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/__init__.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/__main__.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/__init__.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/auto.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/__init__.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/adapter_manager.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/backend.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/backend_tools.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/descriptors.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/ip_backend.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/timed_queue.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/timeout.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/backend/visa_backend.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/ip.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/ip_server.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/adapters/visa.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/__init__.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/backend_console.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/backend_status.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/backend_wrapper.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/console.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/shell.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/terminal.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/terminal_apps.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/cli/terminal_tools.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/__init__.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/delimited.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/modbus.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/protocol.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/raw.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/protocols/scpi.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/scripts/__init__.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/scripts/syndesi.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/scripts/syndesi_backend.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/__init__.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/backend_logger.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/exceptions.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/internal.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/log.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/log_settings.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi/tools/types.py +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi.egg-info/SOURCES.txt +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi.egg-info/dependency_links.txt +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi.egg-info/entry_points.txt +0 -0
- {syndesi-0.3.2 → syndesi-0.4.0}/syndesi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syndesi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Syndesi
|
|
5
5
|
Author-email: Sébastien Deriaz <sebastien.deriaz1@gmail.com>
|
|
6
6
|
License: GPL
|
|
@@ -17,6 +17,7 @@ License-File: LICENSE
|
|
|
17
17
|
Requires-Dist: prompt_toolkit
|
|
18
18
|
Requires-Dist: pyserial
|
|
19
19
|
Requires-Dist: rich
|
|
20
|
+
Requires-Dist: platformdirs
|
|
20
21
|
Dynamic: license-file
|
|
21
22
|
|
|
22
23
|
# Syndesi Python Implementation
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Operating System :: MacOS :: MacOS X",
|
|
23
23
|
"Operating System :: Microsoft :: Windows",
|
|
24
24
|
]
|
|
25
|
-
dependencies = ['prompt_toolkit', 'pyserial', 'rich']
|
|
25
|
+
dependencies = ['prompt_toolkit', 'pyserial', 'rich', 'platformdirs']
|
|
26
26
|
|
|
27
27
|
#[project.optional-dependencies]
|
|
28
28
|
#extra = [""]
|
|
@@ -22,13 +22,12 @@ import subprocess
|
|
|
22
22
|
import sys
|
|
23
23
|
import threading
|
|
24
24
|
import time
|
|
25
|
-
import uuid
|
|
26
25
|
import weakref
|
|
27
26
|
from abc import ABC, abstractmethod
|
|
28
27
|
from collections.abc import Callable
|
|
29
28
|
from multiprocessing.connection import Client, Connection
|
|
30
29
|
from types import EllipsisType
|
|
31
|
-
from typing import Any
|
|
30
|
+
from typing import Any
|
|
32
31
|
import os
|
|
33
32
|
|
|
34
33
|
from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
@@ -39,71 +38,49 @@ from ..tools.backend_api import (
|
|
|
39
38
|
Action,
|
|
40
39
|
BackendResponse,
|
|
41
40
|
default_host,
|
|
42
|
-
is_event,
|
|
43
41
|
raise_if_error,
|
|
42
|
+
EXTRA_BUFFER_RESPONSE_TIME
|
|
44
43
|
)
|
|
45
44
|
from ..tools.log_settings import LoggerAlias
|
|
46
45
|
from .backend.adapter_backend import (
|
|
47
46
|
AdapterDisconnected,
|
|
48
|
-
|
|
47
|
+
AdapterResponseTimeout,
|
|
49
48
|
AdapterReadPayload,
|
|
50
49
|
AdapterSignal,
|
|
51
50
|
)
|
|
52
51
|
from .backend.descriptors import Descriptor
|
|
53
|
-
from .stop_condition import StopCondition,
|
|
52
|
+
from .stop_condition import StopCondition, Continuation, Total
|
|
54
53
|
from .timeout import Timeout, TimeoutAction, any_to_timeout
|
|
55
54
|
|
|
56
|
-
DEFAULT_STOP_CONDITION =
|
|
55
|
+
DEFAULT_STOP_CONDITION = Continuation(time=0.1)
|
|
57
56
|
|
|
58
57
|
DEFAULT_TIMEOUT = Timeout(response=5, action='error')
|
|
59
58
|
|
|
60
|
-
SHUTDOWN_DELAY = 2
|
|
61
|
-
|
|
62
59
|
# Maximum time to let the backend start
|
|
63
60
|
START_TIMEOUT = 2
|
|
61
|
+
# Time to shutdown the backend
|
|
62
|
+
SHUTDOWN_DELAY = 2
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# DATA_READY = auto()
|
|
70
|
-
# ADAPTER_DISCONNECTED = auto()
|
|
71
|
-
|
|
72
|
-
import time
|
|
73
|
-
import queue
|
|
74
|
-
from typing import TypeVar, Generic
|
|
64
|
+
class SignalQueue(queue.Queue[AdapterSignal]):
|
|
65
|
+
def __init__(self) -> None:
|
|
66
|
+
self._read_payload_counter = 0
|
|
67
|
+
super().__init__(0)
|
|
75
68
|
|
|
76
|
-
|
|
69
|
+
def has_read_payload(self) -> bool:
|
|
70
|
+
return self._read_payload_counter > 0
|
|
77
71
|
|
|
78
|
-
class PeekQueue(queue.Queue, Generic[T]):
|
|
79
|
-
def peek(self, block: bool = True, timeout: float | None = None) -> T:
|
|
80
|
-
"""
|
|
81
|
-
Return (without removing) the head item.
|
|
82
72
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
queue.Empty: if no item is available within constraints.
|
|
89
|
-
"""
|
|
90
|
-
with self.not_empty:
|
|
91
|
-
if not block:
|
|
92
|
-
if not self._qsize():
|
|
93
|
-
raise queue.Empty
|
|
94
|
-
return self.queue[0] # type: ignore[attr-defined]
|
|
95
|
-
|
|
96
|
-
end = None if timeout is None else time.monotonic() + timeout
|
|
97
|
-
while not self._qsize():
|
|
98
|
-
if timeout is None:
|
|
99
|
-
self.not_empty.wait()
|
|
100
|
-
else:
|
|
101
|
-
remaining = end - time.monotonic()
|
|
102
|
-
if remaining <= 0:
|
|
103
|
-
raise queue.Empty
|
|
104
|
-
self.not_empty.wait(remaining)
|
|
73
|
+
def put(self, signal: AdapterSignal, block: bool = True, timeout: float | None = None) -> None:
|
|
74
|
+
if isinstance(signal, AdapterReadPayload):
|
|
75
|
+
self._read_payload_counter += 1
|
|
76
|
+
return super().put(signal, block, timeout)
|
|
77
|
+
|
|
105
78
|
|
|
106
|
-
|
|
79
|
+
def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
|
|
80
|
+
signal = super().get(block, timeout)
|
|
81
|
+
if isinstance(signal, AdapterReadPayload):
|
|
82
|
+
self._read_payload_counter -= 1
|
|
83
|
+
return signal
|
|
107
84
|
|
|
108
85
|
|
|
109
86
|
def is_backend_running(address: str, port: int) -> bool:
|
|
@@ -128,29 +105,23 @@ def start_backend(port: int | None = None) -> None:
|
|
|
128
105
|
str(BACKEND_PORT if port is None else port),
|
|
129
106
|
]
|
|
130
107
|
|
|
131
|
-
# Always sever stdio — if you leave any of these inherited,
|
|
132
|
-
# you can keep an implicit console/TTY attachment.
|
|
133
108
|
stdin = subprocess.DEVNULL
|
|
134
109
|
stdout = subprocess.DEVNULL
|
|
135
110
|
stderr = subprocess.DEVNULL
|
|
136
111
|
|
|
137
112
|
if os.name == "posix":
|
|
138
|
-
# New session == new process group, no longer the terminal's foreground PG.
|
|
139
|
-
# This prevents keyboard SIGINT/SIGTSTP from the parent's TTY.
|
|
140
113
|
subprocess.Popen(
|
|
141
114
|
arguments,
|
|
142
|
-
# cwd=None,
|
|
143
|
-
# env=None,
|
|
144
115
|
stdin=stdin,
|
|
145
116
|
stdout=stdout,
|
|
146
117
|
stderr=stderr,
|
|
147
|
-
start_new_session=True,
|
|
148
|
-
close_fds=True,
|
|
118
|
+
start_new_session=True,
|
|
119
|
+
close_fds=True,
|
|
149
120
|
)
|
|
150
121
|
|
|
151
122
|
else:
|
|
152
123
|
# Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
|
|
153
|
-
CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
124
|
+
CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
|
|
154
125
|
DETACHED_PROCESS = 0x00000008 # not exposed by subprocess on all Pythons
|
|
155
126
|
# Optional: CREATE_NO_WINDOW (no window even for console apps)
|
|
156
127
|
CREATE_NO_WINDOW = 0x08000000
|
|
@@ -159,13 +130,11 @@ def start_backend(port: int | None = None) -> None:
|
|
|
159
130
|
|
|
160
131
|
subprocess.Popen(
|
|
161
132
|
arguments,
|
|
162
|
-
# cwd=cwd,
|
|
163
|
-
# env=env,
|
|
164
133
|
stdin=stdin,
|
|
165
134
|
stdout=stdout,
|
|
166
135
|
stderr=stderr,
|
|
167
136
|
creationflags=creationflags,
|
|
168
|
-
close_fds=True,
|
|
137
|
+
close_fds=True,
|
|
169
138
|
)
|
|
170
139
|
|
|
171
140
|
class ReadScope(Enum):
|
|
@@ -177,7 +146,7 @@ class Adapter(ABC):
|
|
|
177
146
|
self,
|
|
178
147
|
descriptor: Descriptor,
|
|
179
148
|
alias: str = "",
|
|
180
|
-
stop_conditions: StopCondition | EllipsisType | list = ...,
|
|
149
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
181
150
|
timeout: Timeout | EllipsisType | NumberLike | None = ...,
|
|
182
151
|
encoding: str = "utf-8",
|
|
183
152
|
event_callback: Callable[[AdapterSignal], None] | None = None,
|
|
@@ -203,7 +172,7 @@ class Adapter(ABC):
|
|
|
203
172
|
super().__init__()
|
|
204
173
|
self._logger = logging.getLogger(LoggerAlias.ADAPTER.value)
|
|
205
174
|
self.encoding = encoding
|
|
206
|
-
self.
|
|
175
|
+
self._signal_queue: SignalQueue = SignalQueue()
|
|
207
176
|
self.event_callback: Callable[[AdapterSignal], None] | None = event_callback
|
|
208
177
|
self.backend_connection: Connection | None = None
|
|
209
178
|
self._backend_connection_lock = threading.Lock()
|
|
@@ -211,7 +180,6 @@ class Adapter(ABC):
|
|
|
211
180
|
self._make_backend_request_flag = threading.Event()
|
|
212
181
|
self.opened = False
|
|
213
182
|
self._alias = alias
|
|
214
|
-
self._read_buffer = []
|
|
215
183
|
|
|
216
184
|
if backend_address is None:
|
|
217
185
|
self._backend_address = default_host
|
|
@@ -235,10 +203,10 @@ class Adapter(ABC):
|
|
|
235
203
|
self.auto_open = auto_open
|
|
236
204
|
|
|
237
205
|
# Set the stop-condition
|
|
238
|
-
self._stop_conditions: list[StopCondition
|
|
206
|
+
self._stop_conditions: list[StopCondition]
|
|
239
207
|
if stop_conditions is ...:
|
|
240
208
|
self._default_stop_condition = True
|
|
241
|
-
self._stop_conditions = DEFAULT_STOP_CONDITION
|
|
209
|
+
self._stop_conditions = [DEFAULT_STOP_CONDITION]
|
|
242
210
|
else:
|
|
243
211
|
self._default_stop_condition = False
|
|
244
212
|
if isinstance(stop_conditions, StopCondition):
|
|
@@ -306,7 +274,7 @@ class Adapter(ABC):
|
|
|
306
274
|
raise RuntimeError("Failed to connect to backend") from err
|
|
307
275
|
self._read_thread = threading.Thread(
|
|
308
276
|
target=self.read_thread,
|
|
309
|
-
args=(self.
|
|
277
|
+
args=(self._signal_queue, self._make_backend_request_queue),
|
|
310
278
|
daemon=True,
|
|
311
279
|
)
|
|
312
280
|
self._read_thread.start()
|
|
@@ -348,7 +316,7 @@ class Adapter(ABC):
|
|
|
348
316
|
|
|
349
317
|
def read_thread(
|
|
350
318
|
self,
|
|
351
|
-
|
|
319
|
+
signal_queue: SignalQueue,
|
|
352
320
|
request_queue: queue.Queue[BackendResponse],
|
|
353
321
|
) -> None:
|
|
354
322
|
while True:
|
|
@@ -357,7 +325,7 @@ class Adapter(ABC):
|
|
|
357
325
|
raise RuntimeError("Backend connection wasn't initialized")
|
|
358
326
|
response: tuple[Any, ...] = self.backend_connection.recv()
|
|
359
327
|
except (EOFError, TypeError, OSError):
|
|
360
|
-
|
|
328
|
+
signal_queue.put(AdapterDisconnected())
|
|
361
329
|
request_queue.put((Action.ERROR_BACKEND_DISCONNECTED,))
|
|
362
330
|
break
|
|
363
331
|
else:
|
|
@@ -365,13 +333,14 @@ class Adapter(ABC):
|
|
|
365
333
|
raise RuntimeError(f"Invalid response from backend : {response}")
|
|
366
334
|
action = Action(response[0])
|
|
367
335
|
|
|
368
|
-
if
|
|
336
|
+
if action == Action.ADAPTER_SIGNAL:
|
|
337
|
+
#if is_event(action):
|
|
369
338
|
if len(response) <= 1:
|
|
370
339
|
raise RuntimeError(f"Invalid event response : {response}")
|
|
340
|
+
signal: AdapterSignal = response[1]
|
|
371
341
|
if self.event_callback is not None:
|
|
372
|
-
signal: AdapterSignal = response[1]
|
|
373
342
|
self.event_callback(signal)
|
|
374
|
-
|
|
343
|
+
signal_queue.put(signal)
|
|
375
344
|
else:
|
|
376
345
|
request_queue.put(response)
|
|
377
346
|
|
|
@@ -401,7 +370,7 @@ class Adapter(ABC):
|
|
|
401
370
|
self._logger.debug(f"Setting default timeout to {default_timeout}")
|
|
402
371
|
self._timeout = default_timeout
|
|
403
372
|
|
|
404
|
-
def set_stop_conditions(self, stop_conditions: StopCondition | None | list) -> None:
|
|
373
|
+
def set_stop_conditions(self, stop_conditions: StopCondition | None | list[StopCondition]) -> None:
|
|
405
374
|
"""
|
|
406
375
|
Overwrite the stop-condition
|
|
407
376
|
|
|
@@ -416,10 +385,6 @@ class Adapter(ABC):
|
|
|
416
385
|
elif stop_conditions is None:
|
|
417
386
|
self._stop_conditions = []
|
|
418
387
|
|
|
419
|
-
# if self._stop_conditions is None:
|
|
420
|
-
# payload = None
|
|
421
|
-
# else:
|
|
422
|
-
# payload = self._stop_conditions.compose_json()
|
|
423
388
|
self._make_backend_request(Action.SET_STOP_CONDITION, self._stop_conditions)
|
|
424
389
|
|
|
425
390
|
def set_default_stop_condition(self, stop_condition: StopCondition) -> None:
|
|
@@ -442,7 +407,7 @@ class Adapter(ABC):
|
|
|
442
407
|
)
|
|
443
408
|
while True:
|
|
444
409
|
try:
|
|
445
|
-
self.
|
|
410
|
+
self._signal_queue.get(block=False)
|
|
446
411
|
except queue.Empty:
|
|
447
412
|
break
|
|
448
413
|
|
|
@@ -469,11 +434,11 @@ class Adapter(ABC):
|
|
|
469
434
|
"""
|
|
470
435
|
Stop communication with the device
|
|
471
436
|
"""
|
|
472
|
-
self._logger.debug("Closing adapter frontend")
|
|
473
|
-
self._make_backend_request(Action.CLOSE)
|
|
474
437
|
if force:
|
|
438
|
+
self._logger.debug("Closing adapter frontend")
|
|
439
|
+
else:
|
|
475
440
|
self._logger.debug("Force closing adapter backend")
|
|
476
|
-
|
|
441
|
+
self._make_backend_request(Action.CLOSE, force)
|
|
477
442
|
|
|
478
443
|
with self._backend_connection_lock:
|
|
479
444
|
if self.backend_connection is not None:
|
|
@@ -499,7 +464,7 @@ class Adapter(ABC):
|
|
|
499
464
|
timeout: Timeout | EllipsisType | None = ...,
|
|
500
465
|
stop_condition: StopCondition | EllipsisType | None = ...,
|
|
501
466
|
scope : str = ReadScope.BUFFERED.value,
|
|
502
|
-
) ->
|
|
467
|
+
) -> AdapterReadPayload | None:
|
|
503
468
|
"""
|
|
504
469
|
Read data from the device
|
|
505
470
|
|
|
@@ -516,111 +481,174 @@ class Adapter(ABC):
|
|
|
516
481
|
data : bytes
|
|
517
482
|
signal : AdapterReadPayload
|
|
518
483
|
"""
|
|
484
|
+
t = time.time()
|
|
519
485
|
_scope = ReadScope(scope)
|
|
520
|
-
|
|
521
|
-
# Then we read whatever payload comes from the backend and compare that to the time
|
|
522
|
-
# If it doesn't match our criteria, we trash it
|
|
523
|
-
# When waiting for the backend payload, we wait +0.5s so make sure we received everything
|
|
524
|
-
# This 0.5s could be changed if we're local or not by the way
|
|
525
|
-
|
|
486
|
+
output_signal = None
|
|
526
487
|
|
|
527
|
-
# First,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
488
|
+
# First, we check if data is in the buffer and if the scope if set to BUFFERED
|
|
489
|
+
while _scope == ReadScope.BUFFERED and self._signal_queue.has_read_payload():
|
|
490
|
+
signal = self._signal_queue.get()
|
|
491
|
+
if isinstance(signal, AdapterReadPayload):
|
|
492
|
+
output_signal = signal
|
|
493
|
+
break
|
|
494
|
+
# TODO : Implement disconnect ?
|
|
533
495
|
else:
|
|
534
|
-
|
|
496
|
+
# Nothing was found, ask the backend with a START_READ request. The backend will
|
|
497
|
+
# respond at most after the response_time with either data or a RESPONSE_TIMEOUT
|
|
498
|
+
if timeout is ...:
|
|
499
|
+
read_timeout = self._timeout
|
|
500
|
+
else:
|
|
501
|
+
read_timeout = any_to_timeout(timeout)
|
|
535
502
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
503
|
+
if read_timeout is not None:
|
|
504
|
+
if not read_timeout.is_initialized():
|
|
505
|
+
raise RuntimeError("Timeout needs to be initialized")
|
|
539
506
|
|
|
540
|
-
|
|
541
|
-
# Calculate the queue timeout (time for a response + small delay)
|
|
542
|
-
#last_valid_timestamp = None
|
|
543
|
-
queue_timeout_timestamp = None
|
|
544
|
-
if read_timeout is not None:
|
|
545
|
-
response_delay = read_timeout.response()
|
|
546
|
-
else:
|
|
547
|
-
response_delay = None
|
|
507
|
+
_response = read_timeout.response()
|
|
548
508
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
queue_timeout_timestamp = time.time() + response_delay + BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
509
|
+
read_init_time = time.time()
|
|
510
|
+
start_read_id = self._make_backend_request(Action.START_READ, _response)[0]
|
|
552
511
|
|
|
553
|
-
|
|
512
|
+
if _response is None:
|
|
513
|
+
# Wait indefinitely
|
|
514
|
+
read_stop_timestamp = None
|
|
515
|
+
else:
|
|
516
|
+
# Wait for the response time + a bit more
|
|
517
|
+
read_stop_timestamp = read_init_time + _response
|
|
554
518
|
|
|
555
|
-
# Ready to read payloads
|
|
556
|
-
while True:
|
|
557
|
-
if queue_timeout_timestamp is None:
|
|
558
|
-
queue_timeout = None
|
|
559
519
|
else:
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
queue_timeout = 0
|
|
563
|
-
|
|
564
|
-
try:
|
|
565
|
-
response = self._event_queue.peek(block=True, timeout=queue_timeout)
|
|
566
|
-
signal = response[1]
|
|
520
|
+
start_read_id = None
|
|
521
|
+
read_init_time = None
|
|
567
522
|
|
|
523
|
+
|
|
524
|
+
while True:
|
|
525
|
+
try:
|
|
526
|
+
if read_stop_timestamp is None:
|
|
527
|
+
queue_timeout = None
|
|
528
|
+
else:
|
|
529
|
+
queue_timeout = max(0, read_stop_timestamp - time.time() + EXTRA_BUFFER_RESPONSE_TIME)
|
|
530
|
+
|
|
531
|
+
signal = self._signal_queue.get(timeout=queue_timeout)
|
|
532
|
+
except queue.Empty:
|
|
533
|
+
raise RuntimeError('Failed to receive response from backend')
|
|
568
534
|
if isinstance(signal, AdapterReadPayload):
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
535
|
+
output_signal = signal
|
|
536
|
+
break
|
|
537
|
+
elif isinstance(signal, AdapterDisconnected):
|
|
538
|
+
raise RuntimeError("Adapter disconnected")
|
|
539
|
+
elif isinstance(signal, AdapterResponseTimeout):
|
|
540
|
+
if start_read_id == signal.identifier:
|
|
573
541
|
output_signal = None
|
|
574
542
|
break
|
|
543
|
+
# Otherwise ignore it
|
|
575
544
|
|
|
576
|
-
if _scope == ReadScope.NEXT and signal.response_timestamp < backend_read_start_time:
|
|
577
|
-
# The payload happened before the read start
|
|
578
|
-
self._event_queue.get()
|
|
579
|
-
continue
|
|
580
545
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
546
|
+
if output_signal is None:
|
|
547
|
+
# TODO : Make read_timeout always Timeout, never None ?
|
|
548
|
+
match read_timeout.action:
|
|
549
|
+
case TimeoutAction.RETURN:
|
|
550
|
+
return None
|
|
551
|
+
case TimeoutAction.ERROR:
|
|
552
|
+
raise TimeoutError(
|
|
553
|
+
f"No response received from device within {read_timeout.response()} seconds"
|
|
554
|
+
)
|
|
555
|
+
case _:
|
|
556
|
+
raise NotImplementedError()
|
|
557
|
+
|
|
558
|
+
else:
|
|
559
|
+
return output_signal
|
|
591
560
|
|
|
592
|
-
elif isinstance(signal, AdapterDisconnected):
|
|
593
|
-
self._event_queue.get()
|
|
594
|
-
raise RuntimeError("Adapter disconnected")
|
|
595
561
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
562
|
+
# Okay idea : Remove the start read and instead ask for the time of the backend.
|
|
563
|
+
# Then we read whatever payload comes from the backend and compare that to the time
|
|
564
|
+
# If it doesn't match our criteria, we trash it
|
|
565
|
+
# When waiting for the backend payload, we wait +0.5s so make sure we received everything
|
|
566
|
+
# This 0.5s could be changed if we're local or not by the way
|
|
599
567
|
|
|
600
568
|
|
|
569
|
+
# # First, ask for the backend time, this is the official start of the read
|
|
570
|
+
# backend_read_start_time = cast(NumberLike, self._make_backend_request(Action.GET_BACKEND_TIME)[0])
|
|
601
571
|
|
|
602
|
-
|
|
603
|
-
# TODO : Make _timeout always Timeout, never None ?
|
|
604
|
-
if read_timeout.action == TimeoutAction.RETURN:
|
|
605
|
-
data = b""
|
|
606
|
-
output_signal = None
|
|
607
|
-
elif read_timeout.action == TimeoutAction.ERROR:
|
|
608
|
-
raise TimeoutError(
|
|
609
|
-
f"No response received from device within {read_timeout.response()} seconds"
|
|
610
|
-
)
|
|
611
|
-
else:
|
|
612
|
-
data = output_signal.data()
|
|
613
|
-
output_signal.response_delay = output_signal.response_timestamp - backend_read_start_time
|
|
572
|
+
# If not timeout is specified, use the default one
|
|
614
573
|
|
|
615
|
-
|
|
616
|
-
|
|
574
|
+
# Calculate last_valid_timestamp, the limit at which a payload is not accepted anymore
|
|
575
|
+
# Calculate the queue timeout (time for a response + small delay)
|
|
576
|
+
#last_valid_timestamp = None
|
|
577
|
+
# queue_timeout_timestamp = None
|
|
578
|
+
# if read_timeout is not None:
|
|
579
|
+
# response_delay = read_timeout.response()
|
|
580
|
+
# else:
|
|
581
|
+
# response_delay = None
|
|
582
|
+
|
|
583
|
+
# if response_delay is not None:
|
|
584
|
+
# #last_valid_timestamp = backend_read_start_time + response_delay
|
|
585
|
+
# queue_timeout_timestamp = time.time() + response_delay + BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
586
|
+
|
|
587
|
+
# output_signal : AdapterReadPayload | None
|
|
588
|
+
# # This delay is given by the backend when a fragment is received. It basically says
|
|
589
|
+
# # "I've received something, wait at most x seconds before raising an error"
|
|
590
|
+
# read_init_end_delay : float | None = None
|
|
591
|
+
|
|
592
|
+
# # Ready to read payloads
|
|
593
|
+
# while True:
|
|
594
|
+
# if read_init_end_delay is not None:
|
|
595
|
+
# # Find the next timeout
|
|
596
|
+
# queue_timeout = read_init_end_delay + 0.1 # TODO : Make this clean, this is the delay to let data arrive to the frontend
|
|
597
|
+
# elif queue_timeout_timestamp is None:
|
|
598
|
+
# queue_timeout = None
|
|
599
|
+
# else:
|
|
600
|
+
# queue_timeout = queue_timeout_timestamp - time.time()
|
|
601
|
+
# if queue_timeout < 0:
|
|
602
|
+
# queue_timeout = 0
|
|
603
|
+
|
|
604
|
+
# try:
|
|
605
|
+
# response = self._signal_queue.peek(block=True, timeout=queue_timeout)
|
|
606
|
+
# signal = response[1]
|
|
607
|
+
|
|
608
|
+
# if isinstance(signal, AdapterReadPayload):
|
|
609
|
+
# if response_delay is not None and signal.response_timestamp - backend_read_start_time > response_delay:
|
|
610
|
+
# # This signal happened after the max response time, act as if a timeout occured
|
|
611
|
+
# # and do not pop it out of the queue
|
|
612
|
+
# # TODO : Make _timeout always Timeout, never None ?
|
|
613
|
+
# output_signal = None
|
|
614
|
+
# break
|
|
615
|
+
|
|
616
|
+
# if _scope == ReadScope.NEXT and signal.response_timestamp < backend_read_start_time:
|
|
617
|
+
# # The payload happened before the read start
|
|
618
|
+
# self._signal_queue.get()
|
|
619
|
+
# continue
|
|
620
|
+
|
|
621
|
+
# if response_delay is not None:
|
|
622
|
+
# if signal.response_timestamp - backend_read_start_time > response_delay:
|
|
623
|
+
# self._signal_queue.get()
|
|
624
|
+
# output_signal = None
|
|
625
|
+
# break
|
|
626
|
+
|
|
627
|
+
# # Other wise the payload is valid
|
|
628
|
+
# self._signal_queue.get()
|
|
629
|
+
# output_signal = signal
|
|
630
|
+
# break
|
|
631
|
+
# elif isinstance(signal, AdapterReadInit):
|
|
632
|
+
# read_init_end_delay = signal.end_delay
|
|
633
|
+
# self._signal_queue.get()
|
|
634
|
+
# elif isinstance(signal, AdapterDisconnected):
|
|
635
|
+
# self._signal_queue.get()
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
# except queue.Empty:
|
|
639
|
+
# output_signal = None
|
|
640
|
+
# break
|
|
617
641
|
|
|
618
642
|
def read(
|
|
619
643
|
self,
|
|
620
644
|
timeout: Timeout | EllipsisType | None = ...,
|
|
621
645
|
stop_condition: StopCondition | EllipsisType | None = ...,
|
|
622
646
|
) -> bytes:
|
|
623
|
-
|
|
647
|
+
signal = self.read_detailed(timeout=timeout, stop_condition=stop_condition)
|
|
648
|
+
if signal is None:
|
|
649
|
+
return None
|
|
650
|
+
else:
|
|
651
|
+
return signal.data()
|
|
624
652
|
|
|
625
653
|
def _cleanup(self) -> None:
|
|
626
654
|
if self._init_ok and self.opened:
|
|
@@ -648,9 +676,13 @@ class Adapter(ABC):
|
|
|
648
676
|
timeout: Timeout | EllipsisType | None = ...,
|
|
649
677
|
stop_condition: StopCondition | EllipsisType | None = ...,
|
|
650
678
|
) -> bytes:
|
|
651
|
-
|
|
679
|
+
signal = self.query_detailed(
|
|
652
680
|
data=data, timeout=timeout, stop_condition=stop_condition
|
|
653
|
-
)
|
|
681
|
+
)
|
|
682
|
+
if signal is None:
|
|
683
|
+
return None
|
|
684
|
+
else:
|
|
685
|
+
return signal.data()
|
|
654
686
|
|
|
655
687
|
def set_event_callback(self, callback: Callable[[AdapterSignal], None]) -> None:
|
|
656
688
|
self.event_callback = callback
|