syndesi 0.4.0__py3-none-any.whl → 0.4.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/adapters/adapter.py +91 -158
- syndesi/adapters/auto.py +1 -1
- syndesi/adapters/backend/adapter_backend.py +54 -37
- syndesi/adapters/backend/adapter_session.py +26 -27
- syndesi/adapters/backend/backend_status.py +0 -0
- syndesi/adapters/backend/backend_tools.py +1 -1
- syndesi/adapters/backend/descriptors.py +3 -2
- syndesi/adapters/backend/ip_backend.py +1 -0
- syndesi/adapters/backend/serialport_backend.py +9 -10
- syndesi/adapters/backend/stop_condition_backend.py +47 -26
- syndesi/adapters/backend/visa_backend.py +7 -7
- syndesi/adapters/ip.py +6 -10
- syndesi/adapters/stop_condition.py +10 -83
- syndesi/adapters/timeout.py +3 -30
- syndesi/adapters/visa.py +2 -2
- syndesi/cli/backend_status.py +7 -9
- syndesi/cli/console.py +1 -54
- syndesi/cli/shell.py +1 -14
- syndesi/cli/shell_tools.py +0 -5
- syndesi/protocols/delimited.py +17 -37
- syndesi/protocols/modbus.py +17 -14
- syndesi/protocols/raw.py +20 -16
- syndesi/protocols/scpi.py +18 -15
- syndesi/scripts/syndesi.py +1 -3
- syndesi/tools/backend_api.py +5 -38
- syndesi/tools/backend_logger.py +0 -1
- syndesi/tools/errors.py +4 -5
- syndesi/tools/log.py +0 -88
- syndesi/tools/types.py +0 -44
- syndesi/version.py +1 -1
- syndesi-0.4.2.dist-info/METADATA +96 -0
- syndesi-0.4.2.dist-info/RECORD +60 -0
- syndesi-0.4.0.dist-info/METADATA +0 -123
- syndesi-0.4.0.dist-info/RECORD +0 -59
- {syndesi-0.4.0.dist-info → syndesi-0.4.2.dist-info}/WHEEL +0 -0
- {syndesi-0.4.0.dist-info → syndesi-0.4.2.dist-info}/entry_points.txt +0 -0
- {syndesi-0.4.0.dist-info → syndesi-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {syndesi-0.4.0.dist-info → syndesi-0.4.2.dist-info}/top_level.txt +0 -0
syndesi/adapters/adapter.py
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
# An adapter is meant to work with bytes objects but it can accept strings.
|
|
16
16
|
# Strings will automatically be converted to bytes using utf-8 encoding
|
|
17
17
|
|
|
18
|
-
from enum import Enum
|
|
19
18
|
import logging
|
|
19
|
+
import os
|
|
20
20
|
import queue
|
|
21
21
|
import subprocess
|
|
22
22
|
import sys
|
|
@@ -25,42 +25,44 @@ import time
|
|
|
25
25
|
import weakref
|
|
26
26
|
from abc import ABC, abstractmethod
|
|
27
27
|
from collections.abc import Callable
|
|
28
|
+
from enum import Enum
|
|
28
29
|
from multiprocessing.connection import Client, Connection
|
|
29
30
|
from types import EllipsisType
|
|
30
31
|
from typing import Any
|
|
31
|
-
import os
|
|
32
32
|
|
|
33
|
-
from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
34
33
|
from syndesi.tools.types import NumberLike, is_number
|
|
35
34
|
|
|
36
35
|
from ..tools.backend_api import (
|
|
37
36
|
BACKEND_PORT,
|
|
37
|
+
EXTRA_BUFFER_RESPONSE_TIME,
|
|
38
38
|
Action,
|
|
39
39
|
BackendResponse,
|
|
40
|
+
Fragment,
|
|
40
41
|
default_host,
|
|
41
42
|
raise_if_error,
|
|
42
|
-
EXTRA_BUFFER_RESPONSE_TIME
|
|
43
43
|
)
|
|
44
44
|
from ..tools.log_settings import LoggerAlias
|
|
45
45
|
from .backend.adapter_backend import (
|
|
46
46
|
AdapterDisconnected,
|
|
47
|
-
AdapterResponseTimeout,
|
|
48
47
|
AdapterReadPayload,
|
|
48
|
+
AdapterResponseTimeout,
|
|
49
49
|
AdapterSignal,
|
|
50
50
|
)
|
|
51
|
+
from .backend.backend_tools import BACKEND_REQUEST_DEFAULT_TIMEOUT
|
|
51
52
|
from .backend.descriptors import Descriptor
|
|
52
|
-
from .stop_condition import
|
|
53
|
+
from .stop_condition import Continuation, StopCondition, StopConditionType
|
|
53
54
|
from .timeout import Timeout, TimeoutAction, any_to_timeout
|
|
54
55
|
|
|
55
56
|
DEFAULT_STOP_CONDITION = Continuation(time=0.1)
|
|
56
57
|
|
|
57
|
-
DEFAULT_TIMEOUT = Timeout(response=5, action=
|
|
58
|
+
DEFAULT_TIMEOUT = Timeout(response=5, action="error")
|
|
58
59
|
|
|
59
60
|
# Maximum time to let the backend start
|
|
60
61
|
START_TIMEOUT = 2
|
|
61
62
|
# Time to shutdown the backend
|
|
62
63
|
SHUTDOWN_DELAY = 2
|
|
63
64
|
|
|
65
|
+
|
|
64
66
|
class SignalQueue(queue.Queue[AdapterSignal]):
|
|
65
67
|
def __init__(self) -> None:
|
|
66
68
|
self._read_payload_counter = 0
|
|
@@ -69,12 +71,12 @@ class SignalQueue(queue.Queue[AdapterSignal]):
|
|
|
69
71
|
def has_read_payload(self) -> bool:
|
|
70
72
|
return self._read_payload_counter > 0
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
def put(
|
|
75
|
+
self, signal: AdapterSignal, block: bool = True, timeout: float | None = None
|
|
76
|
+
) -> None:
|
|
74
77
|
if isinstance(signal, AdapterReadPayload):
|
|
75
78
|
self._read_payload_counter += 1
|
|
76
79
|
return super().put(signal, block, timeout)
|
|
77
|
-
|
|
78
80
|
|
|
79
81
|
def get(self, block: bool = True, timeout: float | None = None) -> AdapterSignal:
|
|
80
82
|
signal = super().get(block, timeout)
|
|
@@ -93,19 +95,20 @@ def is_backend_running(address: str, port: int) -> bool:
|
|
|
93
95
|
conn.close()
|
|
94
96
|
return True
|
|
95
97
|
|
|
98
|
+
|
|
96
99
|
def start_backend(port: int | None = None) -> None:
|
|
97
100
|
arguments = [
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
stdin
|
|
101
|
+
sys.executable,
|
|
102
|
+
"-m",
|
|
103
|
+
"syndesi.adapters.backend.backend",
|
|
104
|
+
"-s",
|
|
105
|
+
str(SHUTDOWN_DELAY),
|
|
106
|
+
"-q",
|
|
107
|
+
"-p",
|
|
108
|
+
str(BACKEND_PORT if port is None else port),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
stdin = subprocess.DEVNULL
|
|
109
112
|
stdout = subprocess.DEVNULL
|
|
110
113
|
stderr = subprocess.DEVNULL
|
|
111
114
|
|
|
@@ -121,8 +124,8 @@ def start_backend(port: int | None = None) -> None:
|
|
|
121
124
|
|
|
122
125
|
else:
|
|
123
126
|
# Windows: detach from the parent's console so keyboard Ctrl+C won't propagate.
|
|
124
|
-
CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
125
|
-
DETACHED_PROCESS = 0x00000008
|
|
127
|
+
CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
|
|
128
|
+
DETACHED_PROCESS = 0x00000008 # not exposed by subprocess on all Pythons
|
|
126
129
|
# Optional: CREATE_NO_WINDOW (no window even for console apps)
|
|
127
130
|
CREATE_NO_WINDOW = 0x08000000
|
|
128
131
|
|
|
@@ -137,9 +140,11 @@ def start_backend(port: int | None = None) -> None:
|
|
|
137
140
|
close_fds=True,
|
|
138
141
|
)
|
|
139
142
|
|
|
143
|
+
|
|
140
144
|
class ReadScope(Enum):
|
|
141
|
-
NEXT =
|
|
142
|
-
BUFFERED =
|
|
145
|
+
NEXT = "next"
|
|
146
|
+
BUFFERED = "buffered"
|
|
147
|
+
|
|
143
148
|
|
|
144
149
|
class Adapter(ABC):
|
|
145
150
|
def __init__(
|
|
@@ -214,7 +219,7 @@ class Adapter(ABC):
|
|
|
214
219
|
elif isinstance(stop_conditions, list):
|
|
215
220
|
self._stop_conditions = stop_conditions
|
|
216
221
|
else:
|
|
217
|
-
raise ValueError(
|
|
222
|
+
raise ValueError("Invalid stop_conditions")
|
|
218
223
|
|
|
219
224
|
# Set the timeout
|
|
220
225
|
self.is_default_timeout = False
|
|
@@ -334,7 +339,7 @@ class Adapter(ABC):
|
|
|
334
339
|
action = Action(response[0])
|
|
335
340
|
|
|
336
341
|
if action == Action.ADAPTER_SIGNAL:
|
|
337
|
-
|
|
342
|
+
# if is_event(action):
|
|
338
343
|
if len(response) <= 1:
|
|
339
344
|
raise RuntimeError(f"Invalid event response : {response}")
|
|
340
345
|
signal: AdapterSignal = response[1]
|
|
@@ -370,7 +375,9 @@ class Adapter(ABC):
|
|
|
370
375
|
self._logger.debug(f"Setting default timeout to {default_timeout}")
|
|
371
376
|
self._timeout = default_timeout
|
|
372
377
|
|
|
373
|
-
def set_stop_conditions(
|
|
378
|
+
def set_stop_conditions(
|
|
379
|
+
self, stop_conditions: StopCondition | None | list[StopCondition]
|
|
380
|
+
) -> None:
|
|
374
381
|
"""
|
|
375
382
|
Overwrite the stop-condition
|
|
376
383
|
|
|
@@ -385,7 +392,7 @@ class Adapter(ABC):
|
|
|
385
392
|
elif stop_conditions is None:
|
|
386
393
|
self._stop_conditions = []
|
|
387
394
|
|
|
388
|
-
self._make_backend_request(Action.
|
|
395
|
+
self._make_backend_request(Action.SET_STOP_CONDITIONs, self._stop_conditions)
|
|
389
396
|
|
|
390
397
|
def set_default_stop_condition(self, stop_condition: StopCondition) -> None:
|
|
391
398
|
"""
|
|
@@ -410,7 +417,6 @@ class Adapter(ABC):
|
|
|
410
417
|
self._signal_queue.get(block=False)
|
|
411
418
|
except queue.Empty:
|
|
412
419
|
break
|
|
413
|
-
|
|
414
420
|
|
|
415
421
|
def previous_read_buffer_empty(self) -> bool:
|
|
416
422
|
"""
|
|
@@ -462,9 +468,9 @@ class Adapter(ABC):
|
|
|
462
468
|
def read_detailed(
|
|
463
469
|
self,
|
|
464
470
|
timeout: Timeout | EllipsisType | None = ...,
|
|
465
|
-
|
|
466
|
-
scope
|
|
467
|
-
) -> AdapterReadPayload
|
|
471
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
472
|
+
scope: str = ReadScope.BUFFERED.value,
|
|
473
|
+
) -> AdapterReadPayload:
|
|
468
474
|
"""
|
|
469
475
|
Read data from the device
|
|
470
476
|
|
|
@@ -481,9 +487,22 @@ class Adapter(ABC):
|
|
|
481
487
|
data : bytes
|
|
482
488
|
signal : AdapterReadPayload
|
|
483
489
|
"""
|
|
484
|
-
t = time.time()
|
|
485
490
|
_scope = ReadScope(scope)
|
|
486
491
|
output_signal = None
|
|
492
|
+
read_timeout = None
|
|
493
|
+
|
|
494
|
+
if timeout is ...:
|
|
495
|
+
read_timeout = self._timeout
|
|
496
|
+
else:
|
|
497
|
+
read_timeout = any_to_timeout(timeout)
|
|
498
|
+
|
|
499
|
+
if read_timeout is None:
|
|
500
|
+
raise RuntimeError("Cannot read without setting a timeout")
|
|
501
|
+
|
|
502
|
+
if stop_conditions is not ...:
|
|
503
|
+
if isinstance(stop_conditions, StopCondition):
|
|
504
|
+
stop_conditions = [stop_conditions]
|
|
505
|
+
self._make_backend_request(Action.SET_STOP_CONDITIONs, stop_conditions)
|
|
487
506
|
|
|
488
507
|
# First, we check if data is in the buffer and if the scope if set to BUFFERED
|
|
489
508
|
while _scope == ReadScope.BUFFERED and self._signal_queue.has_read_payload():
|
|
@@ -495,42 +514,37 @@ class Adapter(ABC):
|
|
|
495
514
|
else:
|
|
496
515
|
# Nothing was found, ask the backend with a START_READ request. The backend will
|
|
497
516
|
# 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)
|
|
502
517
|
|
|
503
|
-
if
|
|
504
|
-
|
|
505
|
-
raise RuntimeError("Timeout needs to be initialized")
|
|
518
|
+
if not read_timeout.is_initialized():
|
|
519
|
+
raise RuntimeError("Timeout needs to be initialized")
|
|
506
520
|
|
|
507
|
-
|
|
521
|
+
_response = read_timeout.response()
|
|
508
522
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
523
|
+
read_init_time = time.time()
|
|
524
|
+
start_read_id = self._make_backend_request(Action.START_READ, _response)[0]
|
|
518
525
|
|
|
526
|
+
if _response is None:
|
|
527
|
+
# Wait indefinitely
|
|
528
|
+
read_stop_timestamp = None
|
|
519
529
|
else:
|
|
520
|
-
|
|
521
|
-
|
|
530
|
+
# Wait for the response time + a bit more
|
|
531
|
+
read_stop_timestamp = read_init_time + _response
|
|
522
532
|
|
|
523
|
-
|
|
524
533
|
while True:
|
|
525
534
|
try:
|
|
526
535
|
if read_stop_timestamp is None:
|
|
527
536
|
queue_timeout = None
|
|
528
537
|
else:
|
|
529
|
-
queue_timeout = max(
|
|
538
|
+
queue_timeout = max(
|
|
539
|
+
0,
|
|
540
|
+
read_stop_timestamp
|
|
541
|
+
- time.time()
|
|
542
|
+
+ EXTRA_BUFFER_RESPONSE_TIME,
|
|
543
|
+
)
|
|
530
544
|
|
|
531
545
|
signal = self._signal_queue.get(timeout=queue_timeout)
|
|
532
|
-
except queue.Empty:
|
|
533
|
-
raise RuntimeError(
|
|
546
|
+
except queue.Empty as e:
|
|
547
|
+
raise RuntimeError("Failed to receive response from backend") from e
|
|
534
548
|
if isinstance(signal, AdapterReadPayload):
|
|
535
549
|
output_signal = signal
|
|
536
550
|
break
|
|
@@ -542,113 +556,35 @@ class Adapter(ABC):
|
|
|
542
556
|
break
|
|
543
557
|
# Otherwise ignore it
|
|
544
558
|
|
|
545
|
-
|
|
546
559
|
if output_signal is None:
|
|
547
|
-
# TODO : Make read_timeout always Timeout, never None ?
|
|
548
560
|
match read_timeout.action:
|
|
549
|
-
case TimeoutAction.
|
|
550
|
-
|
|
561
|
+
case TimeoutAction.RETURN_EMPTY:
|
|
562
|
+
t = time.time()
|
|
563
|
+
return AdapterReadPayload(
|
|
564
|
+
fragments=[Fragment(b"", t)],
|
|
565
|
+
stop_timestamp=t,
|
|
566
|
+
stop_condition_type=StopConditionType.TIMEOUT,
|
|
567
|
+
previous_read_buffer_used=False,
|
|
568
|
+
response_timestamp=None,
|
|
569
|
+
response_delay=None,
|
|
570
|
+
)
|
|
551
571
|
case TimeoutAction.ERROR:
|
|
552
572
|
raise TimeoutError(
|
|
553
573
|
f"No response received from device within {read_timeout.response()} seconds"
|
|
554
574
|
)
|
|
555
575
|
case _:
|
|
556
576
|
raise NotImplementedError()
|
|
557
|
-
|
|
577
|
+
|
|
558
578
|
else:
|
|
559
579
|
return output_signal
|
|
560
580
|
|
|
561
|
-
|
|
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
|
|
567
|
-
|
|
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])
|
|
571
|
-
|
|
572
|
-
# If not timeout is specified, use the default one
|
|
573
|
-
|
|
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
|
|
641
|
-
|
|
642
581
|
def read(
|
|
643
582
|
self,
|
|
644
583
|
timeout: Timeout | EllipsisType | None = ...,
|
|
645
|
-
|
|
584
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
646
585
|
) -> bytes:
|
|
647
|
-
signal = self.read_detailed(timeout=timeout,
|
|
648
|
-
|
|
649
|
-
return None
|
|
650
|
-
else:
|
|
651
|
-
return signal.data()
|
|
586
|
+
signal = self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
|
|
587
|
+
return signal.data()
|
|
652
588
|
|
|
653
589
|
def _cleanup(self) -> None:
|
|
654
590
|
if self._init_ok and self.opened:
|
|
@@ -658,8 +594,8 @@ class Adapter(ABC):
|
|
|
658
594
|
self,
|
|
659
595
|
data: bytes | str,
|
|
660
596
|
timeout: Timeout | EllipsisType | None = ...,
|
|
661
|
-
|
|
662
|
-
) ->
|
|
597
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
598
|
+
) -> AdapterReadPayload:
|
|
663
599
|
"""
|
|
664
600
|
Shortcut function that combines
|
|
665
601
|
- flush_read
|
|
@@ -668,21 +604,18 @@ class Adapter(ABC):
|
|
|
668
604
|
"""
|
|
669
605
|
self.flushRead()
|
|
670
606
|
self.write(data)
|
|
671
|
-
return self.read_detailed(timeout=timeout,
|
|
607
|
+
return self.read_detailed(timeout=timeout, stop_conditions=stop_conditions)
|
|
672
608
|
|
|
673
609
|
def query(
|
|
674
610
|
self,
|
|
675
611
|
data: bytes | str,
|
|
676
612
|
timeout: Timeout | EllipsisType | None = ...,
|
|
677
|
-
|
|
613
|
+
stop_conditions: StopCondition | EllipsisType | list[StopCondition] = ...,
|
|
678
614
|
) -> bytes:
|
|
679
615
|
signal = self.query_detailed(
|
|
680
|
-
data=data, timeout=timeout,
|
|
616
|
+
data=data, timeout=timeout, stop_conditions=stop_conditions
|
|
681
617
|
)
|
|
682
|
-
|
|
683
|
-
return None
|
|
684
|
-
else:
|
|
685
|
-
return signal.data()
|
|
618
|
+
return signal.data()
|
|
686
619
|
|
|
687
620
|
def set_event_callback(self, callback: Callable[[AdapterSignal], None]) -> None:
|
|
688
621
|
self.event_callback = callback
|
syndesi/adapters/auto.py
CHANGED
|
@@ -37,7 +37,7 @@ def auto_adapter(adapter_or_string: Adapter | str) -> Adapter:
|
|
|
37
37
|
return IP(
|
|
38
38
|
address=descriptor.address,
|
|
39
39
|
port=descriptor.port,
|
|
40
|
-
transport=descriptor.transport,
|
|
40
|
+
transport=descriptor.transport.value,
|
|
41
41
|
)
|
|
42
42
|
elif isinstance(descriptor, SerialPortDescriptor):
|
|
43
43
|
return SerialPort(port=descriptor.port, baudrate=descriptor.baudrate)
|
|
@@ -7,19 +7,18 @@
|
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
import socket
|
|
10
|
+
import time
|
|
10
11
|
from abc import ABC, abstractmethod
|
|
11
12
|
from collections.abc import Generator
|
|
12
13
|
from dataclasses import dataclass
|
|
13
14
|
from enum import Enum
|
|
14
15
|
from multiprocessing.connection import Connection
|
|
15
16
|
from threading import Thread
|
|
16
|
-
import
|
|
17
|
-
from typing import Protocol, cast
|
|
18
|
-
|
|
19
|
-
from syndesi.tools.types import NumberLike
|
|
17
|
+
from typing import Protocol
|
|
20
18
|
|
|
21
19
|
from ...tools.backend_api import AdapterBackendStatus, Fragment
|
|
22
20
|
from ...tools.log_settings import LoggerAlias
|
|
21
|
+
from ..stop_condition import StopConditionType
|
|
23
22
|
from .descriptors import Descriptor
|
|
24
23
|
from .stop_condition_backend import (
|
|
25
24
|
ContinuationBackend,
|
|
@@ -27,8 +26,6 @@ from .stop_condition_backend import (
|
|
|
27
26
|
TotalBackend,
|
|
28
27
|
)
|
|
29
28
|
|
|
30
|
-
from ..stop_condition import Continuation, StopConditionType
|
|
31
|
-
|
|
32
29
|
|
|
33
30
|
class HasFileno(Protocol):
|
|
34
31
|
def fileno(self) -> int:
|
|
@@ -82,12 +79,13 @@ class AdapterDisconnected(AdapterSignal):
|
|
|
82
79
|
# def __repr__(self) -> str:
|
|
83
80
|
# return self.__str__()
|
|
84
81
|
|
|
82
|
+
|
|
85
83
|
@dataclass
|
|
86
84
|
class AdapterResponseTimeout(AdapterSignal):
|
|
87
|
-
identifier
|
|
85
|
+
identifier: int
|
|
88
86
|
|
|
89
87
|
def __str__(self) -> str:
|
|
90
|
-
return
|
|
88
|
+
return "Response timeout"
|
|
91
89
|
|
|
92
90
|
def __repr__(self) -> str:
|
|
93
91
|
return self.__str__()
|
|
@@ -99,9 +97,9 @@ class AdapterReadPayload(AdapterSignal):
|
|
|
99
97
|
stop_timestamp: float
|
|
100
98
|
stop_condition_type: StopConditionType
|
|
101
99
|
previous_read_buffer_used: bool
|
|
102
|
-
response_timestamp
|
|
100
|
+
response_timestamp: float | None
|
|
103
101
|
# Only used by client and set by frontend
|
|
104
|
-
response_delay
|
|
102
|
+
response_delay: float | None = None
|
|
105
103
|
|
|
106
104
|
def data(self) -> bytes:
|
|
107
105
|
return b"".join([f.data for f in self.fragments])
|
|
@@ -118,9 +116,10 @@ class AdapterReadPayload(AdapterSignal):
|
|
|
118
116
|
@dataclass
|
|
119
117
|
class ResponseRequest:
|
|
120
118
|
timestamp: float
|
|
121
|
-
identifier
|
|
119
|
+
identifier: int
|
|
120
|
+
|
|
122
121
|
|
|
123
|
-
def nmin(a
|
|
122
|
+
def nmin(a: float | None, b: float | None) -> float | None:
|
|
124
123
|
if a is None and b is None:
|
|
125
124
|
return None
|
|
126
125
|
elif a is None:
|
|
@@ -130,6 +129,7 @@ def nmin(a : float | None, b : float | None) -> float | None:
|
|
|
130
129
|
else:
|
|
131
130
|
return min(a, b)
|
|
132
131
|
|
|
132
|
+
|
|
133
133
|
class AdapterBackend(ABC):
|
|
134
134
|
class ThreadCommands(Enum):
|
|
135
135
|
STOP = b"0"
|
|
@@ -168,7 +168,7 @@ class AdapterBackend(ABC):
|
|
|
168
168
|
# None : No ask
|
|
169
169
|
# float : Ask for a response to happen at the specified value at max
|
|
170
170
|
self._response_request: ResponseRequest | None = None
|
|
171
|
-
self._response_request_start
|
|
171
|
+
self._response_request_start: float | None = None
|
|
172
172
|
|
|
173
173
|
self._first_fragment = True
|
|
174
174
|
|
|
@@ -246,10 +246,10 @@ class AdapterBackend(ABC):
|
|
|
246
246
|
@abstractmethod
|
|
247
247
|
def _socket_read(self) -> Fragment:
|
|
248
248
|
raise NotImplementedError
|
|
249
|
-
|
|
250
|
-
def _fragments_to_string(self, fragments
|
|
249
|
+
|
|
250
|
+
def _fragments_to_string(self, fragments: list[Fragment]) -> str:
|
|
251
251
|
if len(fragments) > 0:
|
|
252
|
-
return
|
|
252
|
+
return "+".join(repr(f.data) for f in fragments)
|
|
253
253
|
else:
|
|
254
254
|
return str([])
|
|
255
255
|
|
|
@@ -260,11 +260,13 @@ class AdapterBackend(ABC):
|
|
|
260
260
|
else:
|
|
261
261
|
fragment_delta_t = float("nan")
|
|
262
262
|
if fragment.data == b"":
|
|
263
|
-
self._logger.debug('Empty data -> close')
|
|
264
263
|
self.close()
|
|
265
264
|
yield AdapterDisconnected()
|
|
266
265
|
else:
|
|
267
|
-
self._logger.debug(
|
|
266
|
+
self._logger.debug(
|
|
267
|
+
f"New fragment {fragment_delta_t:+.3f} {fragment}"
|
|
268
|
+
+ (" (first)" if self._first_fragment else "")
|
|
269
|
+
)
|
|
268
270
|
if self._status == AdapterBackendStatus.CONNECTED:
|
|
269
271
|
t = time.time()
|
|
270
272
|
|
|
@@ -273,7 +275,9 @@ class AdapterBackend(ABC):
|
|
|
273
275
|
|
|
274
276
|
if self._response_request is not None:
|
|
275
277
|
for stop_condition in self._stop_conditions:
|
|
276
|
-
if isinstance(
|
|
278
|
+
if isinstance(
|
|
279
|
+
stop_condition, (ContinuationBackend, TotalBackend)
|
|
280
|
+
):
|
|
277
281
|
self._response_request = None
|
|
278
282
|
break
|
|
279
283
|
|
|
@@ -288,15 +292,18 @@ class AdapterBackend(ABC):
|
|
|
288
292
|
kept = fragment
|
|
289
293
|
|
|
290
294
|
# Run each stop condition one after the other, if a stop is reached, stop evaluating
|
|
291
|
-
stop_condition_type
|
|
295
|
+
stop_condition_type: StopConditionType
|
|
292
296
|
for stop_condition in self._stop_conditions:
|
|
293
|
-
|
|
294
|
-
|
|
297
|
+
(
|
|
298
|
+
stop,
|
|
299
|
+
kept,
|
|
300
|
+
self._previous_buffer,
|
|
301
|
+
self._next_timeout_timestamp,
|
|
302
|
+
) = stop_condition.evaluate(kept)
|
|
295
303
|
if stop:
|
|
296
304
|
stop_condition_type = stop_condition.type()
|
|
297
305
|
break
|
|
298
306
|
|
|
299
|
-
|
|
300
307
|
# if kept.data != b'':
|
|
301
308
|
# self.fragments.append(kept)
|
|
302
309
|
|
|
@@ -304,14 +311,22 @@ class AdapterBackend(ABC):
|
|
|
304
311
|
|
|
305
312
|
if stop:
|
|
306
313
|
self._first_fragment = True
|
|
307
|
-
self._logger.debug(
|
|
308
|
-
|
|
314
|
+
self._logger.debug(
|
|
315
|
+
f"Payload {self._fragments_to_string(self.fragments)} ({stop_condition_type.value})"
|
|
316
|
+
)
|
|
317
|
+
if (
|
|
318
|
+
self._response_request_start is None
|
|
319
|
+
or len(self.fragments) == 0
|
|
320
|
+
):
|
|
309
321
|
response_delay = None
|
|
310
322
|
else:
|
|
311
323
|
if self.fragments[0].timestamp is None:
|
|
312
324
|
response_delay = None
|
|
313
325
|
else:
|
|
314
|
-
response_delay =
|
|
326
|
+
response_delay = (
|
|
327
|
+
self.fragments[0].timestamp
|
|
328
|
+
- self._response_request_start
|
|
329
|
+
)
|
|
315
330
|
self._response_request_start = None
|
|
316
331
|
yield AdapterReadPayload(
|
|
317
332
|
fragments=self.fragments,
|
|
@@ -319,9 +334,9 @@ class AdapterBackend(ABC):
|
|
|
319
334
|
stop_condition_type=stop_condition_type,
|
|
320
335
|
previous_read_buffer_used=False,
|
|
321
336
|
response_timestamp=self.fragments[0].timestamp,
|
|
322
|
-
response_delay=response_delay
|
|
337
|
+
response_delay=response_delay,
|
|
323
338
|
)
|
|
324
|
-
self._next_timeout_timestamp = None
|
|
339
|
+
self._next_timeout_timestamp = None # Experiment !
|
|
325
340
|
self.fragments.clear()
|
|
326
341
|
|
|
327
342
|
if len(self._previous_buffer.data) > 0 and stop:
|
|
@@ -342,8 +357,9 @@ class AdapterBackend(ABC):
|
|
|
342
357
|
"""
|
|
343
358
|
self._response_request_start = time.time()
|
|
344
359
|
self._logger.debug(f"Setup read [{identifier}] in {response_time:.3f} s")
|
|
345
|
-
self._response_request = ResponseRequest(
|
|
346
|
-
|
|
360
|
+
self._response_request = ResponseRequest(
|
|
361
|
+
self._response_request_start + response_time, identifier
|
|
362
|
+
)
|
|
347
363
|
|
|
348
364
|
@abstractmethod
|
|
349
365
|
def is_opened(self) -> bool:
|
|
@@ -367,7 +383,9 @@ class AdapterBackend(ABC):
|
|
|
367
383
|
response_delay = None
|
|
368
384
|
else:
|
|
369
385
|
if self.fragments[0].timestamp is not None:
|
|
370
|
-
response_delay =
|
|
386
|
+
response_delay = (
|
|
387
|
+
self.fragments[0].timestamp - self._response_request_start
|
|
388
|
+
)
|
|
371
389
|
else:
|
|
372
390
|
response_delay = None
|
|
373
391
|
self._response_request_start = None
|
|
@@ -377,8 +395,10 @@ class AdapterBackend(ABC):
|
|
|
377
395
|
stop_condition_type=StopConditionType.TIMEOUT,
|
|
378
396
|
previous_read_buffer_used=False,
|
|
379
397
|
fragments=self.fragments,
|
|
380
|
-
response_timestamp=
|
|
381
|
-
|
|
398
|
+
response_timestamp=(
|
|
399
|
+
self.fragments[0].timestamp if len(self.fragments) > 0 else None
|
|
400
|
+
),
|
|
401
|
+
response_delay=response_delay,
|
|
382
402
|
)
|
|
383
403
|
# Reset response request
|
|
384
404
|
if self._response_request is not None:
|
|
@@ -389,8 +409,7 @@ class AdapterBackend(ABC):
|
|
|
389
409
|
return output
|
|
390
410
|
|
|
391
411
|
elif (
|
|
392
|
-
self._next_timeout_origin
|
|
393
|
-
== self.AdapterTimeoutEventOrigin.RESPONSE_REQUEST
|
|
412
|
+
self._next_timeout_origin == self.AdapterTimeoutEventOrigin.RESPONSE_REQUEST
|
|
394
413
|
):
|
|
395
414
|
if self._response_request is not None:
|
|
396
415
|
signal = AdapterResponseTimeout(self._response_request.identifier)
|
|
@@ -403,8 +422,6 @@ class AdapterBackend(ABC):
|
|
|
403
422
|
min_timestamp = None
|
|
404
423
|
self._next_timeout_origin = None
|
|
405
424
|
|
|
406
|
-
t = time.time()
|
|
407
|
-
|
|
408
425
|
if self._next_timeout_timestamp is not None:
|
|
409
426
|
min_timestamp = self._next_timeout_timestamp
|
|
410
427
|
self._next_timeout_origin = self.AdapterTimeoutEventOrigin.TIMEOUT
|