leaf-framework 0.10.4__tar.gz → 0.10.6__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.
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/PKG-INFO +1 -1
- leaf_framework-0.10.6/leaf/adapters/core_adapters/streaming_adapter.py +240 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/adapters/equipment_adapter.py +4 -1
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/data/marketplace.yaml +100 -18
- leaf_framework-0.10.6/leaf/modules/phase_modules/measure.py +289 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/start.py +7 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/pyproject.toml +2 -1
- leaf_framework-0.10.4/leaf/modules/phase_modules/measure.py +0 -180
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/LICENSE +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/README.md +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/adapters/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/adapters/core_adapters/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/adapters/core_adapters/continuous_experiment_adapter.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/adapters/core_adapters/discrete_experiment_adapter.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/adapters/core_adapters/upload_adapter.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/config/configuration.yaml +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/error_handler/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/error_handler/error_holder.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/error_handler/exceptions.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/measurement_handler/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/measurement_handler/handler.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/measurement_handler/measurements.yaml +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/measurement_handler/terms.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/.gitkeep +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/ble_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/db_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/event_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/external_api_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/external_event_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/file_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/http_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/modbus_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/mqtt_external_event_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/mqtt_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/opc_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/polling_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/input_modules/simple_watcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/measurement_modules/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/measurement_modules/carbon_dioxide.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/measurement_modules/dissolved_oxygen.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/measurement_modules/fluorescence.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/measurement_modules/measurement_module.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/measurement_modules/optical_density.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/measurement_modules/ph.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/measurement_modules/temperature.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/output_modules/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/output_modules/file.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/output_modules/keydb.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/output_modules/mqtt.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/output_modules/output_module.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/output_modules/sql.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/output_modules/topic_store.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/phase_modules/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/phase_modules/control.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/phase_modules/error.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/phase_modules/external_event_phase.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/phase_modules/initialisation.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/phase_modules/phase.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/phase_modules/start.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/phase_modules/stop.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/process_modules/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/process_modules/continous_module.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/process_modules/discrete_module.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/process_modules/external_event_process.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/process_modules/process_module.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/modules/process_modules/upload_module.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/register/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/register/abstract_leaf.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/register/equipment_actions.yaml +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/register/equipment_data.yaml +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/register/metadata.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/register/py.typed +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/register/topic_utilities.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/registry/discovery.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/registry/loader.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/registry/registry.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/registry/utils.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/adapter_ui_extension.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/components/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/components/header.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/constants.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/desktop_launcher.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/images/icon.icns +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/images/icon.ico +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/images/icon.svg +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/interface.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/markdown/configuration_help.md +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/markdown/documentation_main.md +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/markdown/documentation_protips.md +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/scripts.js +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/styles.css +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/tabs/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/tabs/adapters.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/tabs/configuration.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/tabs/documentation.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/tabs/logs.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/tabs/monitoring.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/ui/utils.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/utility/config_utils.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/utility/logger/__init__.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/utility/logger/logger_utils.py +0 -0
- {leaf_framework-0.10.4 → leaf_framework-0.10.6}/leaf/utility/running_utilities.py +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from typing import Any, NamedTuple
|
|
4
|
+
|
|
5
|
+
from leaf.adapters.equipment_adapter import AbstractInterpreter, EquipmentAdapter
|
|
6
|
+
from leaf.error_handler.error_holder import ErrorHolder
|
|
7
|
+
from leaf.error_handler.exceptions import AdapterBuildError
|
|
8
|
+
from leaf.modules.input_modules.event_watcher import EventWatcher
|
|
9
|
+
from leaf.modules.input_modules.external_event_watcher import ExternalEventWatcher
|
|
10
|
+
from leaf.modules.output_modules.output_module import OutputModule
|
|
11
|
+
from leaf.modules.phase_modules.error import ErrorPhase
|
|
12
|
+
from leaf.modules.phase_modules.initialisation import InitialisationPhase
|
|
13
|
+
from leaf.modules.phase_modules.measure import MeasurePhase
|
|
14
|
+
from leaf.modules.phase_modules.start import StartPhase
|
|
15
|
+
from leaf.modules.phase_modules.stop import StopPhase
|
|
16
|
+
from leaf.modules.process_modules.process_module import ProcessModule
|
|
17
|
+
from leaf.register.metadata import MetadataManager
|
|
18
|
+
from leaf.utility.logger.logger_utils import get_logger
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__, log_file="streaming_adapter.log")
|
|
21
|
+
|
|
22
|
+
# Mirrors the reconnect backoff used by OPCWatcher (opc_watcher.py) for consistency.
|
|
23
|
+
FIRST_RESTART_DELAY = 1
|
|
24
|
+
RESTART_RATE = 2
|
|
25
|
+
MAX_RESTART_DELAY = 60
|
|
26
|
+
# If the previous run stayed up at least this long, treat the next failure
|
|
27
|
+
# as a fresh incident rather than continuing to escalate the backoff.
|
|
28
|
+
STABLE_RUN_THRESHOLD = 60
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _Stream(NamedTuple):
|
|
32
|
+
thread: threading.Thread
|
|
33
|
+
stop_event: threading.Event
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StreamingAdapter(EquipmentAdapter):
|
|
37
|
+
"""
|
|
38
|
+
Adapter for equipment whose interpreter produces measurements
|
|
39
|
+
continuously via its own `stream(filename)` generator, started once per
|
|
40
|
+
detected run, rather than being invoked per discrete measurement event.
|
|
41
|
+
|
|
42
|
+
The interpreter must implement `stream(self, filename) -> Iterator[Any]`,
|
|
43
|
+
yielding parsed measurements at its own pace (e.g. tailing a growing
|
|
44
|
+
file). Use this instead of ContinuousExperimentAdapter when the input
|
|
45
|
+
source changes too fast, or too continuously, for a per-event watcher
|
|
46
|
+
callback to keep up.
|
|
47
|
+
|
|
48
|
+
Unlike measurement(), stream() is not given data on every call - it has
|
|
49
|
+
no per-event trigger. Instead, what to stream is discovered from the
|
|
50
|
+
watcher's start event (e.g. FileWatcher.on_created configured with
|
|
51
|
+
return_data=False, so it carries the created filepath rather than file
|
|
52
|
+
content) and handed to stream() once, when that run begins. A matching
|
|
53
|
+
stop event (e.g. on_deleted) halts it.
|
|
54
|
+
|
|
55
|
+
Multiple filenames can stream concurrently, each on its own thread and
|
|
56
|
+
with independent restart/backoff state: a start event for a filename
|
|
57
|
+
that isn't already active launches a new stream alongside any others; a
|
|
58
|
+
start event for a filename that IS already active restarts just that one.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
instance_data: dict[str, Any],
|
|
64
|
+
watcher: EventWatcher,
|
|
65
|
+
output: OutputModule,
|
|
66
|
+
interpreter: AbstractInterpreter,
|
|
67
|
+
maximum_message_size: int | None = 1,
|
|
68
|
+
error_holder: ErrorHolder | None = None,
|
|
69
|
+
metadata_manager: MetadataManager | None = None,
|
|
70
|
+
experiment_timeout: int | None = None,
|
|
71
|
+
external_watcher: ExternalEventWatcher | None = None,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Initialise the StreamingAdapter.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
instance_data (dict): Instance-level configuration and metadata.
|
|
78
|
+
watcher (EventWatcher): Input module used for start/stop/details
|
|
79
|
+
events. Its start event's data is passed straight through to
|
|
80
|
+
interpreter.stream(filename) - see class docstring.
|
|
81
|
+
output (OutputModule): The output module used to transmit parsed data.
|
|
82
|
+
interpreter (AbstractInterpreter): Must implement stream(filename).
|
|
83
|
+
maximum_message_size (Optional[int]): Max number of measurements per transmission.
|
|
84
|
+
error_holder (Optional[ErrorHolder]): Error tracking object.
|
|
85
|
+
metadata_manager (Optional[MetadataManager]): Optional metadata manager.
|
|
86
|
+
experiment_timeout (Optional[int]): Optional timeout between measurements.
|
|
87
|
+
external_watcher (Optional[ExternalEventWatcher]): Optional listener for external inputs.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
AdapterBuildError: If the interpreter does not implement stream().
|
|
91
|
+
"""
|
|
92
|
+
if not hasattr(interpreter, "stream"):
|
|
93
|
+
raise AdapterBuildError("StreamingAdapter requires an interpreter that implements stream(filename)")
|
|
94
|
+
|
|
95
|
+
start_p = StartPhase(metadata_manager)
|
|
96
|
+
stop_p = StopPhase(metadata_manager)
|
|
97
|
+
details_p = InitialisationPhase(metadata_manager)
|
|
98
|
+
error_p = ErrorPhase(metadata_manager)
|
|
99
|
+
# Not included in _control_process's phases: measurements are driven
|
|
100
|
+
# entirely by stream(), never by the watcher's measurement-topic
|
|
101
|
+
# dispatch, so this is only ever touched directly by _run_stream_loop.
|
|
102
|
+
self._measure_p = MeasurePhase(metadata_manager, maximum_message_size=maximum_message_size)
|
|
103
|
+
|
|
104
|
+
self._control_process = ProcessModule(
|
|
105
|
+
output,
|
|
106
|
+
[start_p, stop_p, details_p, error_p],
|
|
107
|
+
metadata_manager=metadata_manager,
|
|
108
|
+
error_holder=error_holder,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self._streams: dict[Any, _Stream] = {}
|
|
112
|
+
self._streams_guard = threading.Lock()
|
|
113
|
+
|
|
114
|
+
super().__init__(
|
|
115
|
+
instance_data,
|
|
116
|
+
watcher,
|
|
117
|
+
output,
|
|
118
|
+
[self._control_process],
|
|
119
|
+
interpreter,
|
|
120
|
+
metadata_manager=metadata_manager,
|
|
121
|
+
error_holder=error_holder,
|
|
122
|
+
experiment_timeout=experiment_timeout,
|
|
123
|
+
external_watcher=external_watcher,
|
|
124
|
+
)
|
|
125
|
+
# EquipmentAdapter.__init__ calls p.set_interpreter/set_metadata_manager
|
|
126
|
+
# for each phase in _control_process, which no longer includes
|
|
127
|
+
# _measure_p (see above) - wire it up here instead.
|
|
128
|
+
self._measure_p.set_interpreter(interpreter)
|
|
129
|
+
self._measure_p.set_metadata_manager(self._metadata_manager)
|
|
130
|
+
self._measure_p.set_error_holder(error_holder)
|
|
131
|
+
|
|
132
|
+
# Separate from the standard process_input wiring: this callback
|
|
133
|
+
# exists purely to learn the filename to stream from a real start
|
|
134
|
+
# event, and to halt the stream on a real stop event.
|
|
135
|
+
self._watcher.add_callback(self._handle_watcher_event)
|
|
136
|
+
|
|
137
|
+
def stop(self) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Stop the adapter, halting all active streams first.
|
|
140
|
+
"""
|
|
141
|
+
with self._streams_guard:
|
|
142
|
+
filenames = list(self._streams.keys())
|
|
143
|
+
for filename in filenames:
|
|
144
|
+
self._stop_stream(filename)
|
|
145
|
+
super().stop()
|
|
146
|
+
|
|
147
|
+
def _handle_watcher_event(self, topic, data: Any) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Watches for the real start/stop topics dispatched by the watcher
|
|
150
|
+
(as opposed to the process_input wiring used for messaging) purely
|
|
151
|
+
to drive each filename's stream lifecycle.
|
|
152
|
+
|
|
153
|
+
Any other event (e.g. the "measurement" topic FileWatcher uses for
|
|
154
|
+
files that already existed when it started watching, via
|
|
155
|
+
_process_existing_files) is treated as an implicit start too, as
|
|
156
|
+
long as we're not already streaming that filename - otherwise a
|
|
157
|
+
file present before the watcher starts would never get streamed at
|
|
158
|
+
all, since it never produces a real "created" event. Once a stream
|
|
159
|
+
is active for a filename, further non-start/stop events for it are
|
|
160
|
+
no-ops; the running stream() call handles tailing on its own.
|
|
161
|
+
"""
|
|
162
|
+
if topic() == self._metadata_manager.experiment.start():
|
|
163
|
+
self._launch_stream(data)
|
|
164
|
+
elif topic() == self._metadata_manager.experiment.stop():
|
|
165
|
+
self._stop_stream(data)
|
|
166
|
+
else:
|
|
167
|
+
with self._streams_guard:
|
|
168
|
+
already_streaming = data in self._streams
|
|
169
|
+
if not already_streaming:
|
|
170
|
+
self._launch_stream(data)
|
|
171
|
+
|
|
172
|
+
def _launch_stream(self, filename: Any) -> None:
|
|
173
|
+
"""
|
|
174
|
+
(Re)launch the background stream thread for the given filename. Any
|
|
175
|
+
existing stream for this SAME filename is halted first; streams for
|
|
176
|
+
other filenames are left untouched and keep running independently.
|
|
177
|
+
"""
|
|
178
|
+
self._stop_stream(filename)
|
|
179
|
+
|
|
180
|
+
stop_event = threading.Event()
|
|
181
|
+
thread = threading.Thread(
|
|
182
|
+
target=self._run_stream_loop,
|
|
183
|
+
args=(filename, stop_event),
|
|
184
|
+
daemon=True,
|
|
185
|
+
name=f"leaf-stream-{self._metadata_manager.get_instance_id()}-{filename}",
|
|
186
|
+
)
|
|
187
|
+
with self._streams_guard:
|
|
188
|
+
self._streams[filename] = _Stream(thread, stop_event)
|
|
189
|
+
thread.start()
|
|
190
|
+
|
|
191
|
+
def _stop_stream(self, filename: Any) -> None:
|
|
192
|
+
"""
|
|
193
|
+
Signal the given filename's stream generation to stop and wait
|
|
194
|
+
briefly for it to exit. If interpreter.stream() is blocked
|
|
195
|
+
mid-iteration (not suspended at a yield), it cannot be forced to
|
|
196
|
+
stop; the join is best-effort and the thread is a daemon so it
|
|
197
|
+
won't block process shutdown, but it may keep running until that
|
|
198
|
+
blocking call returns.
|
|
199
|
+
"""
|
|
200
|
+
with self._streams_guard:
|
|
201
|
+
stream = self._streams.pop(filename, None)
|
|
202
|
+
if stream is None:
|
|
203
|
+
return
|
|
204
|
+
stream.stop_event.set()
|
|
205
|
+
stream.thread.join(timeout=5)
|
|
206
|
+
|
|
207
|
+
def _run_stream_loop(self, filename: Any, stop_event: threading.Event) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Run interpreter.stream(filename) via MeasurePhase.stream(), restarting
|
|
210
|
+
it with exponential backoff whenever it ends - whether by returning
|
|
211
|
+
normally or by raising - since neither is expected while this run is
|
|
212
|
+
meant to be active. Stops restarting once stop_event is set (either
|
|
213
|
+
by a real stop event for this filename, or by a newer start event
|
|
214
|
+
superseding it).
|
|
215
|
+
|
|
216
|
+
Note: if the interpreter doesn't keep track of its own resume point
|
|
217
|
+
(e.g. a file offset held on the interpreter instance), a restart
|
|
218
|
+
will call stream(filename) fresh and may re-emit data from the beginning.
|
|
219
|
+
"""
|
|
220
|
+
restart_delay = FIRST_RESTART_DELAY
|
|
221
|
+
while not stop_event.is_set():
|
|
222
|
+
started_at = time.monotonic()
|
|
223
|
+
self._measure_p.stream(self._output, filename)
|
|
224
|
+
ran_for = time.monotonic() - started_at
|
|
225
|
+
|
|
226
|
+
if stop_event.is_set():
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if ran_for >= STABLE_RUN_THRESHOLD:
|
|
230
|
+
restart_delay = FIRST_RESTART_DELAY
|
|
231
|
+
|
|
232
|
+
logger.warning(
|
|
233
|
+
f"Interpreter stream({filename!r}) for {self._metadata_manager.get_instance_id()} ended "
|
|
234
|
+
f"after {ran_for:.1f}s; restarting in {restart_delay}s."
|
|
235
|
+
)
|
|
236
|
+
if stop_event.wait(timeout=restart_delay):
|
|
237
|
+
break
|
|
238
|
+
restart_delay = min(restart_delay * RESTART_RATE, MAX_RESTART_DELAY)
|
|
239
|
+
|
|
240
|
+
logger.info(f"Streaming thread for {self._metadata_manager.get_instance_id()} ({filename!r}) exiting.")
|
|
@@ -73,7 +73,10 @@ class AbstractInterpreter(ABC):
|
|
|
73
73
|
data (Any): Raw measurement data.
|
|
74
74
|
|
|
75
75
|
Returns:
|
|
76
|
-
Any: Parsed or transformed measurement data.
|
|
76
|
+
Any: Parsed or transformed measurement data. May also be a
|
|
77
|
+
(finite) generator; MeasurePhase drains it lazily and chunks
|
|
78
|
+
the yielded items into messages of maximum_message_size,
|
|
79
|
+
instead of requiring the full result built up front.
|
|
77
80
|
"""
|
|
78
81
|
self._last_measurement = time.time()
|
|
79
82
|
return data
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
application: Table parser
|
|
12
12
|
requirements:
|
|
13
13
|
folder: "<path>"
|
|
14
|
-
repo_url: https://gitlab.com/
|
|
14
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-table.git
|
|
15
15
|
|
|
16
16
|
- adapter_id: LiveViewUI
|
|
17
|
-
image: "./images/plot.
|
|
17
|
+
image: "./images/plot.jpg"
|
|
18
18
|
description: "A plugin with a graph viewer to show submitted data in real-time." # TODO: Add description
|
|
19
19
|
equipment_data:
|
|
20
20
|
version: "1.0"
|
|
@@ -23,7 +23,20 @@
|
|
|
23
23
|
application: UI Extension Demonstration
|
|
24
24
|
requirements:
|
|
25
25
|
interval: int
|
|
26
|
-
repo_url: https://gitlab.com/
|
|
26
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-live-view.git
|
|
27
|
+
|
|
28
|
+
- adapter_id: Example
|
|
29
|
+
image: ""
|
|
30
|
+
description: "A basic LEAF adapter example. Creates a measurement message every x seconds. Useful as a starting point for developing new adapters."
|
|
31
|
+
equipment_data:
|
|
32
|
+
version: "0.2.1"
|
|
33
|
+
manufacturer: LEAF
|
|
34
|
+
device_type: EXAMPLE
|
|
35
|
+
application: Example application
|
|
36
|
+
requirements:
|
|
37
|
+
interval: int
|
|
38
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-example.git
|
|
39
|
+
name: LEAF-Example
|
|
27
40
|
|
|
28
41
|
# - adapter_id: MockFunctionalAdapter
|
|
29
42
|
# image: ""
|
|
@@ -35,7 +48,7 @@
|
|
|
35
48
|
# application: DEMO application
|
|
36
49
|
# requirements:
|
|
37
50
|
# interval: int
|
|
38
|
-
# repo_url: https://gitlab.com/
|
|
51
|
+
# repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-examples
|
|
39
52
|
# name: LEAF-Examples
|
|
40
53
|
|
|
41
54
|
- adapter_id: BiolectorXT
|
|
@@ -49,7 +62,7 @@
|
|
|
49
62
|
interval: 11
|
|
50
63
|
host: string
|
|
51
64
|
token: string
|
|
52
|
-
repo_url: https://gitlab.com/
|
|
65
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-biolector-xt
|
|
53
66
|
name: LEAF-Biolector XT
|
|
54
67
|
|
|
55
68
|
- adapter_id: BioLector1
|
|
@@ -88,7 +101,7 @@
|
|
|
88
101
|
weight: "10kg"
|
|
89
102
|
requirements:
|
|
90
103
|
write_file: str
|
|
91
|
-
repo_url: https://gitlab.com/
|
|
104
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-biolector1
|
|
92
105
|
name: LEAF-biolector1
|
|
93
106
|
|
|
94
107
|
- adapter_id: leaf_indpensim
|
|
@@ -102,7 +115,7 @@
|
|
|
102
115
|
application: Industrial Penicilin Simulation
|
|
103
116
|
requirements:
|
|
104
117
|
write_file: str
|
|
105
|
-
repo_url: https://gitlab.com/
|
|
118
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-indpensim
|
|
106
119
|
name: LEAF-IndPenSim
|
|
107
120
|
|
|
108
121
|
- adapter_id: maq
|
|
@@ -118,7 +131,7 @@
|
|
|
118
131
|
interval: int
|
|
119
132
|
token: str
|
|
120
133
|
site: int
|
|
121
|
-
repo_url: https://gitlab.com/
|
|
134
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-maq
|
|
122
135
|
name: LEAF-MAQ
|
|
123
136
|
|
|
124
137
|
- adapter_id: MinKNOW
|
|
@@ -134,7 +147,7 @@
|
|
|
134
147
|
host: string
|
|
135
148
|
port: int
|
|
136
149
|
token: string
|
|
137
|
-
repo_url: https://gitlab.com/
|
|
150
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-minknow
|
|
138
151
|
name: LEAF-MinKNOW
|
|
139
152
|
|
|
140
153
|
- adapter_id: iRODS
|
|
@@ -152,7 +165,7 @@
|
|
|
152
165
|
zone: string
|
|
153
166
|
user: string
|
|
154
167
|
password: string
|
|
155
|
-
repo_url: https://gitlab.com/
|
|
168
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-irods
|
|
156
169
|
name: LEAF-iRODS
|
|
157
170
|
|
|
158
171
|
# - adapter_id: HelloWorld
|
|
@@ -165,11 +178,11 @@
|
|
|
165
178
|
# application: DEMO application
|
|
166
179
|
# requirements:
|
|
167
180
|
# interval: int
|
|
168
|
-
# repo_url: https://gitlab.com/
|
|
181
|
+
# repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-hello-world
|
|
169
182
|
# name: LEAF-Hello World
|
|
170
183
|
|
|
171
184
|
- adapter_id: Pioreactor
|
|
172
|
-
image: ./images/pioreactor.
|
|
185
|
+
image: ./images/pioreactor.jpg
|
|
173
186
|
description: "A LEAF adapter for the Pioreactor bioreactor system by Pioreactor. This plugin connects to the Pioreactor MQTT broker to retrieve real-time data on bioreactor conditions and performance."
|
|
174
187
|
equipment_data:
|
|
175
188
|
version: "1.0"
|
|
@@ -182,7 +195,7 @@
|
|
|
182
195
|
port: int
|
|
183
196
|
username: str
|
|
184
197
|
password: str
|
|
185
|
-
repo_url: https://gitlab.com/
|
|
198
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-pioreactor
|
|
186
199
|
name: LEAF-Pioreactor
|
|
187
200
|
|
|
188
201
|
- adapter_id: OPC
|
|
@@ -194,9 +207,78 @@
|
|
|
194
207
|
device_type: OPC sensor module
|
|
195
208
|
application: OPC Monitoring
|
|
196
209
|
requirements:
|
|
197
|
-
port: int
|
|
198
|
-
host: string
|
|
199
|
-
topics: list[string]
|
|
200
|
-
exclude_topics: list[string]
|
|
201
|
-
|
|
210
|
+
port: int # OPC server port
|
|
211
|
+
host: string # OPC server host address
|
|
212
|
+
topics: list[string] # Optional: list of OPC node IDs to subscribe to
|
|
213
|
+
exclude_topics: list[string] # Optional: list of OPC node IDs to exclude
|
|
214
|
+
mapper: # Optional: tag mapping rules applied to each measurement
|
|
215
|
+
- field: node # Tag field to match against (e.g. "node")
|
|
216
|
+
pattern: '^R1_.*' # Regex pattern to match the field value
|
|
217
|
+
tags: # Tags to add when the pattern matches
|
|
218
|
+
entity: 9
|
|
219
|
+
- field: node
|
|
220
|
+
pattern: '^R2_.*'
|
|
221
|
+
tags:
|
|
222
|
+
entity: 10
|
|
223
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-opc-adapter.git
|
|
202
224
|
name: LEAF OPC
|
|
225
|
+
- adapter_id: CAFE
|
|
226
|
+
image: icon:bi-thermometer
|
|
227
|
+
description: "A LEAF adapter for the CAFE system from the PROSE-lab Université Paris-Saclay, INRAE."
|
|
228
|
+
equipment_data:
|
|
229
|
+
version: "1.0.0"
|
|
230
|
+
manufacturer: PROSE
|
|
231
|
+
device_type: CAFE system
|
|
232
|
+
application: Monitoring
|
|
233
|
+
requirements:
|
|
234
|
+
time_column: string
|
|
235
|
+
watch: list[string]
|
|
236
|
+
sep: string
|
|
237
|
+
repo_url: https://gitlab.com/bioindustry-4.0/leafs/leaf-cafe.git
|
|
238
|
+
name: LEAF CAFE
|
|
239
|
+
- adapter_id: RAMSMART
|
|
240
|
+
image: ./images/movesense.png
|
|
241
|
+
description: "A LEAF adapter for the RAMSMART system by Movesense. This plugin detects and retrieves BLE real-time data from RAMSMART sensors for monitoring and analysis."
|
|
242
|
+
equipment_data:
|
|
243
|
+
version: "1.0"
|
|
244
|
+
manufacturer: Movesense
|
|
245
|
+
device_type: RAMSMART sensor
|
|
246
|
+
application: Monitoring
|
|
247
|
+
# requirements:
|
|
248
|
+
repo_url: https://git.wur.nl/leaf/adapters/leaf-ramsmart.git
|
|
249
|
+
name: LEAF-RAMSMART
|
|
250
|
+
- adapter_id: BluetoothScanner
|
|
251
|
+
image: icon:bi-bluetooth
|
|
252
|
+
description: "A LEAF adapter for scanning nearby Bluetooth devices. This plugin retrieves real-time raw-data on detected Bluetooth devices for monitoring and analysis."
|
|
253
|
+
equipment_data:
|
|
254
|
+
version: "1.0"
|
|
255
|
+
manufacturer: LEAF
|
|
256
|
+
device_type: Bluetooth Scanner
|
|
257
|
+
application: Monitoring
|
|
258
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-bluetooth
|
|
259
|
+
name: leaf_bluetooth
|
|
260
|
+
- adapter_id: leaf_modbus
|
|
261
|
+
image: icon:bi-hdd-network
|
|
262
|
+
description: "A LEAF adapter for Modbus TCP devices. Polls configurable holding registers at a fixed interval and publishes measurements for monitoring and analysis."
|
|
263
|
+
equipment_data:
|
|
264
|
+
version: "0.1.0"
|
|
265
|
+
manufacturer: Modbus
|
|
266
|
+
device_type: Modbus TCP device
|
|
267
|
+
application: Modbus Monitoring
|
|
268
|
+
requirements:
|
|
269
|
+
host: string # Modbus TCP host address
|
|
270
|
+
port: int # Modbus TCP port (default 502)
|
|
271
|
+
device_id: int # Modbus unit/device ID (default 1)
|
|
272
|
+
interval: int # Polling interval in seconds
|
|
273
|
+
registers:
|
|
274
|
+
- name: temperature # Key used in the output payload
|
|
275
|
+
address: 40301 # Holding register address in 4xxxx notation
|
|
276
|
+
scale: 0.01 # Optional: multiply raw value by this factor (default 1.0)
|
|
277
|
+
- name: presence
|
|
278
|
+
address: 40803
|
|
279
|
+
- name: last_ir_ts
|
|
280
|
+
address: 42002
|
|
281
|
+
count: 2 # Optional: number of consecutive registers to read (default 1)
|
|
282
|
+
# Multi-register fields are stored as last_ir_ts_0, last_ir_ts_1, ...
|
|
283
|
+
repo_url: https://gitlab.com/leaf-framework/leaf-adapters/leaf-modbus.git
|
|
284
|
+
name: LEAF-Modbus
|