benchlab-pytools 3.0.0__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.
- benchlab/__init__.py +14 -0
- benchlab/__main__.py +21 -0
- benchlab/config/README.md +325 -0
- benchlab/config/__init__.py +9 -0
- benchlab/config/config_client.py +631 -0
- benchlab/config/config_manager.py +495 -0
- benchlab/config/config_tool.py +358 -0
- benchlab/config/diff.py +163 -0
- benchlab/config/examples/complete_config.json +56 -0
- benchlab/config/examples/rgb_lighting.json +24 -0
- benchlab/config/examples/simple_fan.json +32 -0
- benchlab/config/schema.py +138 -0
- benchlab/core/README.md +154 -0
- benchlab/core/__init__.py +78 -0
- benchlab/core/config.py +145 -0
- benchlab/core/datasource.py +1209 -0
- benchlab/core/datasource_manager.py +418 -0
- benchlab/core/device_registry.py +206 -0
- benchlab/core/discovery.py +92 -0
- benchlab/core/infrastructure.py +309 -0
- benchlab/core/process_manager.py +356 -0
- benchlab/core/retry.py +85 -0
- benchlab/core/shared_serial.py +235 -0
- benchlab/core/statistics.py +224 -0
- benchlab/csv_log/README.md +206 -0
- benchlab/csv_log/__init__.py +5 -0
- benchlab/csv_log/csv_logger_enhanced.py +416 -0
- benchlab/csv_log/message_batcher.py +361 -0
- benchlab/csv_log/requirements.txt +3 -0
- benchlab/csv_log/smart_retry.py +466 -0
- benchlab/graph/README.md +117 -0
- benchlab/graph/__init__.py +5 -0
- benchlab/graph/app.py +353 -0
- benchlab/graph/requirements.txt +2 -0
- benchlab/graph/runner.py +42 -0
- benchlab/graph/ui.py +288 -0
- benchlab/hwinfo/README.md +135 -0
- benchlab/hwinfo/__init__.py +5 -0
- benchlab/hwinfo/hwinfo_export.py +386 -0
- benchlab/hwinfo/requirements.txt +1 -0
- benchlab/launcher.py +387 -0
- benchlab/link/.env.EXAMPLE +10 -0
- benchlab/link/README.md +172 -0
- benchlab/link/__init__.py +1 -0
- benchlab/link/link_main.py +479 -0
- benchlab/main.py +462 -0
- benchlab/menu.py +729 -0
- benchlab/menu_classic.py +410 -0
- benchlab/mqtt/README.md +201 -0
- benchlab/mqtt/__init__.py +5 -0
- benchlab/mqtt/mqtt.config_template +2 -0
- benchlab/mqtt/mqtt_publisher.py +540 -0
- benchlab/mqtt/requirements.txt +3 -0
- benchlab/prefs.py +67 -0
- benchlab/restapi/.env.example +17 -0
- benchlab/restapi/__init__.py +5 -0
- benchlab/restapi/favicon.ico +0 -0
- benchlab/restapi/readme.md +202 -0
- benchlab/restapi/requirements.txt +5 -0
- benchlab/restapi/telemetry_api.py +808 -0
- benchlab/sources.py +515 -0
- benchlab/tools.py +147 -0
- benchlab/tui/README.md +266 -0
- benchlab/tui/__init__.py +5 -0
- benchlab/tui/config.py +477 -0
- benchlab/tui/requirements.txt +2 -0
- benchlab/tui/tui_core.py +1298 -0
- benchlab/tui/tui_main.py +418 -0
- benchlab/vu/README.md +64 -0
- benchlab/vu/VU-Server/NOTICE.md +13 -0
- benchlab/vu/VU-Server/README.md +80 -0
- benchlab/vu/VU-Server/Running_from_source_on_linux.md +97 -0
- benchlab/vu/VU-Server/__init__.py +0 -0
- benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
- benchlab/vu/VU-Server/community_applications.md +13 -0
- benchlab/vu/VU-Server/config.yaml +9 -0
- benchlab/vu/VU-Server/database.py +256 -0
- benchlab/vu/VU-Server/dial_driver.py +534 -0
- benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +78 -0
- benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +100 -0
- benchlab/vu/VU-Server/dials/__init__.py +0 -0
- benchlab/vu/VU-Server/dials/base_logger.py +100 -0
- benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
- benchlab/vu/VU-Server/installer/install.nsi +148 -0
- benchlab/vu/VU-Server/make_version.py +40 -0
- benchlab/vu/VU-Server/pylintrc +628 -0
- benchlab/vu/VU-Server/requirements.txt +9 -0
- benchlab/vu/VU-Server/serial_driver.py +244 -0
- benchlab/vu/VU-Server/server.py +670 -0
- benchlab/vu/VU-Server/server_config.py +255 -0
- benchlab/vu/VU-Server/server_dial_handler.py +334 -0
- benchlab/vu/VU-Server/vu_notifications.py +24 -0
- benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
- benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
- benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
- benchlab/vu/VU-Server/www/assets/css/demo.css +291 -0
- benchlab/vu/VU-Server/www/assets/css/demo.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +276 -0
- benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +1084 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +1084 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +528 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +528 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.css +10 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +8 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +10 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +8 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +1504 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +1504 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.css +25813 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.min.css +13 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +25777 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +13 -0
- benchlab/vu/VU-Server/www/assets/css/theme.min.css +261 -0
- benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
- benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
- benchlab/vu/VU-Server/www/assets/js/dark-theme.js +35 -0
- benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +9 -0
- benchlab/vu/VU-Server/www/assets/js/demo.js +132 -0
- benchlab/vu/VU-Server/www/assets/js/demo.min.js +9 -0
- benchlab/vu/VU-Server/www/assets/js/jquery.min.js +2 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +7482 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +15 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.js +7543 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.min.js +15 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_api.js +68 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +167 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +138 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +179 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +60 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +125 -0
- benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
- benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
- benchlab/vu/VU-Server/www/favicon.ico +0 -0
- benchlab/vu/VU-Server/www/favicon.png +0 -0
- benchlab/vu/VU-Server/www/index.html +47 -0
- benchlab/vu/VU-Server/www/views/api_keys.html +151 -0
- benchlab/vu/VU-Server/www/views/dial.html +286 -0
- benchlab/vu/VU-Server/www/views/footer.html +27 -0
- benchlab/vu/VU-Server/www/views/header.html +86 -0
- benchlab/vu/VU-Server/www/views/key_settings.html +132 -0
- benchlab/vu/VU-Server/www/views/start.html +124 -0
- benchlab/vu/__init__.py +5 -0
- benchlab/vu/assets/Barlow-Bold.ttf +0 -0
- benchlab/vu/assets/bl_dial_144x200.png +0 -0
- benchlab/vu/assets/bl_logo_144x200.png +0 -0
- benchlab/vu/devices.py +166 -0
- benchlab/vu/requirements.txt +24 -0
- benchlab/vu/sensors.py +18 -0
- benchlab/vu/vu_dial.config_template +90 -0
- benchlab/vu/vu_logo_gen.py +97 -0
- benchlab/vu/vu_server.config_template +5 -0
- benchlab/vu/vu_server_config.py +40 -0
- benchlab/vu/vu_server_manager.py +192 -0
- benchlab/vu/vu_tui.py +919 -0
- benchlab/vu/vu_updater.py +540 -0
- benchlab/wigidash/README.md +182 -0
- benchlab/wigidash/__init__.py +5 -0
- benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
- benchlab/wigidash/assets/Inter.ttf +0 -0
- benchlab/wigidash/assets/benchlab.png +0 -0
- benchlab/wigidash/benchlab_fleet.py +150 -0
- benchlab/wigidash/benchlab_graph.py +517 -0
- benchlab/wigidash/benchlab_overview.py +426 -0
- benchlab/wigidash/benchlab_telemetry.py +105 -0
- benchlab/wigidash/benchlab_ui.py +190 -0
- benchlab/wigidash/benchlab_utils.py +182 -0
- benchlab/wigidash/requirements.txt +9 -0
- benchlab/wigidash/wigidash_device.py +541 -0
- benchlab/wigidash/wigidash_manager.py +342 -0
- benchlab/wigidash/wigidash_session.py +355 -0
- benchlab/wigidash/wigidash_usb.py +247 -0
- benchlab/wigidash/wigidash_widget.py +88 -0
- benchlab_pytools-3.0.0.dist-info/METADATA +239 -0
- benchlab_pytools-3.0.0.dist-info/RECORD +184 -0
- benchlab_pytools-3.0.0.dist-info/WHEEL +4 -0
- benchlab_pytools-3.0.0.dist-info/entry_points.txt +2 -0
- benchlab_pytools-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Device discovery utilities using the benchlab-pycore library.
|
|
2
|
+
|
|
3
|
+
All serial‑port discovery is now performed through the official
|
|
4
|
+
``benchlab_pycore`` helpers. This module provides a single public function
|
|
5
|
+
``discover_devices`` that returns a list of dictionaries containing the UID,
|
|
6
|
+
port and firmware version of each connected BenchLab device.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import List, Dict, Any
|
|
13
|
+
|
|
14
|
+
# benchlab-pycore helpers
|
|
15
|
+
from benchlab_pycore.core import get_benchlab_ports
|
|
16
|
+
from benchlab_pycore.core import read_device, read_uid
|
|
17
|
+
|
|
18
|
+
# benchlab_pycore.core.serial_io has no connection-opening helper; use the
|
|
19
|
+
# local wrapper instead (see benchlab.core.shared_serial for why).
|
|
20
|
+
from benchlab.core.shared_serial import open_serial_connection
|
|
21
|
+
|
|
22
|
+
# PyCore >=0.4.1 renamed BENCHLAB_CFE_PRODUCT_ID to BENCHLAB_BL2_PRODUCT_ID
|
|
23
|
+
# (the "CFE" SKU is now marketed as BENCHLAB 2). Imported directly from
|
|
24
|
+
# pycore (not from benchlab.core) to avoid a circular import, since
|
|
25
|
+
# benchlab.core imports this module via datasource_manager.
|
|
26
|
+
try:
|
|
27
|
+
from benchlab_pycore.core import BENCHLAB_BL2_PRODUCT_ID
|
|
28
|
+
except ImportError:
|
|
29
|
+
from benchlab_pycore.core import (
|
|
30
|
+
BENCHLAB_CFE_PRODUCT_ID as BENCHLAB_BL2_PRODUCT_ID)
|
|
31
|
+
from benchlab_pycore.core import BENCHLAB_ORIGINAL_PRODUCT_ID
|
|
32
|
+
|
|
33
|
+
# Re‑use the retry decorator defined in ``benchlab.core.retry``
|
|
34
|
+
from .retry import retry, RetryPolicy
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("benchlab.core.discovery")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@retry(RetryPolicy(max_retries=3, backoff_factor=2.0,
|
|
40
|
+
base_delay=0.5, allowed_exceptions=(Exception,)))
|
|
41
|
+
def discover_devices() -> List[Dict[str, Any]]:
|
|
42
|
+
"""Return a list of connected BenchLab devices.
|
|
43
|
+
|
|
44
|
+
Each entry is a mapping with the keys ``uid``, ``port`` and ``fw``
|
|
45
|
+
(firmware).
|
|
46
|
+
The function:
|
|
47
|
+
1. Calls :func:`benchlab_pycore.core.get_benchlab_ports` to obtain
|
|
48
|
+
candidate ports that match the known hardware ID.
|
|
49
|
+
2. Opens each port, reads the UID and firmware version, then closes the
|
|
50
|
+
connection.
|
|
51
|
+
3. Logs discovery details at INFO level and any failures at DEBUG level.
|
|
52
|
+
"""
|
|
53
|
+
devices: List[Dict[str, Any]] = []
|
|
54
|
+
logger.info("Scanning for BenchLab devices via benchlab-pycore")
|
|
55
|
+
|
|
56
|
+
ports = get_benchlab_ports()
|
|
57
|
+
for port_info in ports:
|
|
58
|
+
port = port_info.get("port")
|
|
59
|
+
if not port:
|
|
60
|
+
continue
|
|
61
|
+
try:
|
|
62
|
+
ser = open_serial_connection(port)
|
|
63
|
+
if ser is None:
|
|
64
|
+
logger.debug("Could not open serial port %s", port)
|
|
65
|
+
continue
|
|
66
|
+
uid = read_uid(ser)
|
|
67
|
+
info = read_device(ser) or {}
|
|
68
|
+
ser.close()
|
|
69
|
+
if uid:
|
|
70
|
+
fw = info.get("FwVersion", "?")
|
|
71
|
+
product_id = info.get(
|
|
72
|
+
"ProductId", BENCHLAB_ORIGINAL_PRODUCT_ID)
|
|
73
|
+
variant = (
|
|
74
|
+
"BL2" if product_id == BENCHLAB_BL2_PRODUCT_ID
|
|
75
|
+
else "ORIGINAL")
|
|
76
|
+
devices.append({"uid": uid, "port": port,
|
|
77
|
+
"fw": fw, "variant": variant})
|
|
78
|
+
logger.info(
|
|
79
|
+
"Discovered BenchLab device UID=%s on %s "
|
|
80
|
+
"(FW=%s, Variant=%s)",
|
|
81
|
+
uid,
|
|
82
|
+
port,
|
|
83
|
+
fw,
|
|
84
|
+
variant)
|
|
85
|
+
else:
|
|
86
|
+
logger.debug("No UID read from port %s", port)
|
|
87
|
+
except Exception as exc: # pragma: no cover – defensive logging
|
|
88
|
+
logger.debug("Error probing port %s: %s", port, exc)
|
|
89
|
+
|
|
90
|
+
if not devices:
|
|
91
|
+
logger.info("No BenchLab devices found via benchlab-pycore discovery")
|
|
92
|
+
return devices
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Infrastructure Manager for BENCHLAB
|
|
3
|
+
|
|
4
|
+
Handles starting and stopping of data provider services (FastAPI, MQTT)
|
|
5
|
+
that can be shared by multiple tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import socket
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("benchlab.core.infrastructure")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InfrastructureManager:
|
|
21
|
+
"""Manages infrastructure services (FastAPI, MQTT) for multi-tool
|
|
22
|
+
scenarios.
|
|
23
|
+
|
|
24
|
+
This class is responsible for:
|
|
25
|
+
- Starting FastAPI server as a subprocess when needed
|
|
26
|
+
- Starting MQTT publisher thread when needed
|
|
27
|
+
- Ensuring services are properly shut down
|
|
28
|
+
- Checking if services are already running
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
fastapi_host: str = "127.0.0.1",
|
|
34
|
+
fastapi_port: int = 8000,
|
|
35
|
+
mqtt_broker: str = "localhost",
|
|
36
|
+
mqtt_port: int = 1883,
|
|
37
|
+
):
|
|
38
|
+
"""Initialize infrastructure manager.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
fastapi_host: Host for FastAPI server
|
|
42
|
+
fastapi_port: Port for FastAPI server
|
|
43
|
+
mqtt_broker: MQTT broker hostname
|
|
44
|
+
mqtt_port: MQTT broker port
|
|
45
|
+
"""
|
|
46
|
+
self.fastapi_host = fastapi_host
|
|
47
|
+
self.fastapi_port = fastapi_port
|
|
48
|
+
self.mqtt_broker = mqtt_broker
|
|
49
|
+
self.mqtt_port = mqtt_port
|
|
50
|
+
|
|
51
|
+
self._fastapi_process: Optional[subprocess.Popen] = None
|
|
52
|
+
self._mqtt_thread: Optional[threading.Thread] = None
|
|
53
|
+
self._stop_event = threading.Event()
|
|
54
|
+
self._lock = threading.Lock()
|
|
55
|
+
|
|
56
|
+
def start_fastapi(self, wait_ready: bool = True,
|
|
57
|
+
timeout: float = 10.0) -> bool:
|
|
58
|
+
"""Start FastAPI server as a subprocess.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
wait_ready: Whether to wait for server to be ready
|
|
62
|
+
timeout: Timeout for waiting (seconds)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if server started successfully
|
|
66
|
+
"""
|
|
67
|
+
with self._lock:
|
|
68
|
+
# Check if already running
|
|
69
|
+
if self._fastapi_process is not None:
|
|
70
|
+
if self._fastapi_process.poll() is None:
|
|
71
|
+
logger.info("FastAPI server already running")
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
# Check if port is already in use (server might be running
|
|
75
|
+
# externally)
|
|
76
|
+
if self._is_port_in_use(self.fastapi_host, self.fastapi_port):
|
|
77
|
+
logger.info(
|
|
78
|
+
f"Port {self.fastapi_port} already in use, "
|
|
79
|
+
f"assuming FastAPI is running")
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
logger.info(
|
|
83
|
+
f"Starting FastAPI server on {
|
|
84
|
+
self.fastapi_host}:{
|
|
85
|
+
self.fastapi_port}")
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
# Start FastAPI server as subprocess
|
|
89
|
+
self._fastapi_process = subprocess.Popen(
|
|
90
|
+
[
|
|
91
|
+
sys.executable,
|
|
92
|
+
"-m",
|
|
93
|
+
"uvicorn",
|
|
94
|
+
"benchlab.restapi.telemetry_api:app",
|
|
95
|
+
"--host",
|
|
96
|
+
self.fastapi_host,
|
|
97
|
+
"--port",
|
|
98
|
+
str(self.fastapi_port),
|
|
99
|
+
"--log-level",
|
|
100
|
+
"warning",
|
|
101
|
+
],
|
|
102
|
+
stdout=subprocess.PIPE,
|
|
103
|
+
stderr=subprocess.PIPE,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if wait_ready:
|
|
107
|
+
if self._wait_for_port(
|
|
108
|
+
self.fastapi_host, self.fastapi_port, timeout):
|
|
109
|
+
logger.info(
|
|
110
|
+
f"FastAPI server started on port {
|
|
111
|
+
self.fastapi_port}")
|
|
112
|
+
return True
|
|
113
|
+
else:
|
|
114
|
+
logger.error("FastAPI server failed to start")
|
|
115
|
+
self.stop_fastapi()
|
|
116
|
+
return False
|
|
117
|
+
else:
|
|
118
|
+
# Give it a moment to start
|
|
119
|
+
time.sleep(0.5)
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f"Failed to start FastAPI server: {e}")
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
def stop_fastapi(self) -> None:
|
|
127
|
+
"""Stop FastAPI server."""
|
|
128
|
+
with self._lock:
|
|
129
|
+
if self._fastapi_process is not None:
|
|
130
|
+
logger.info("Stopping FastAPI server")
|
|
131
|
+
try:
|
|
132
|
+
# Try graceful shutdown first
|
|
133
|
+
self._fastapi_process.terminate()
|
|
134
|
+
try:
|
|
135
|
+
self._fastapi_process.wait(timeout=5.0)
|
|
136
|
+
except subprocess.TimeoutExpired:
|
|
137
|
+
# Force kill if not responding
|
|
138
|
+
self._fastapi_process.kill()
|
|
139
|
+
self._fastapi_process.wait()
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.warning(f"Error stopping FastAPI server: {e}")
|
|
142
|
+
finally:
|
|
143
|
+
self._fastapi_process = None
|
|
144
|
+
|
|
145
|
+
def is_fastapi_running(self) -> bool:
|
|
146
|
+
"""Check if FastAPI server is running.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
True if FastAPI server is running
|
|
150
|
+
"""
|
|
151
|
+
# Check if we started it
|
|
152
|
+
if self._fastapi_process is not None:
|
|
153
|
+
if self._fastapi_process.poll() is None:
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
# Check if port is in use (might be running externally)
|
|
157
|
+
return self._is_port_in_use(self.fastapi_host, self.fastapi_port)
|
|
158
|
+
|
|
159
|
+
def start_mqtt_publisher(self) -> bool:
|
|
160
|
+
"""Start MQTT publisher thread.
|
|
161
|
+
|
|
162
|
+
Note: This starts the MQTT publisher that sends data to an external
|
|
163
|
+
MQTT broker. The broker itself must be running separately.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if MQTT publisher started successfully
|
|
167
|
+
"""
|
|
168
|
+
with self._lock:
|
|
169
|
+
if self._mqtt_thread is not None and self._mqtt_thread.is_alive():
|
|
170
|
+
logger.info("MQTT publisher already running")
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
logger.info(
|
|
174
|
+
f"Starting MQTT publisher to {
|
|
175
|
+
self.mqtt_broker}:{
|
|
176
|
+
self.mqtt_port}")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
self._stop_event.clear()
|
|
180
|
+
self._mqtt_thread = threading.Thread(
|
|
181
|
+
target=self._mqtt_publisher_loop,
|
|
182
|
+
daemon=True,
|
|
183
|
+
)
|
|
184
|
+
self._mqtt_thread.start()
|
|
185
|
+
|
|
186
|
+
# Give it a moment to connect
|
|
187
|
+
time.sleep(1.0)
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Failed to start MQTT publisher: {e}")
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
def stop_mqtt_publisher(self) -> None:
|
|
195
|
+
"""Stop MQTT publisher thread."""
|
|
196
|
+
with self._lock:
|
|
197
|
+
if self._mqtt_thread is not None:
|
|
198
|
+
logger.info("Stopping MQTT publisher")
|
|
199
|
+
self._stop_event.set()
|
|
200
|
+
if self._mqtt_thread.is_alive():
|
|
201
|
+
self._mqtt_thread.join(timeout=5.0)
|
|
202
|
+
self._mqtt_thread = None
|
|
203
|
+
|
|
204
|
+
def is_mqtt_running(self) -> bool:
|
|
205
|
+
"""Check if MQTT publisher is running.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
True if MQTT publisher thread is alive
|
|
209
|
+
"""
|
|
210
|
+
return self._mqtt_thread is not None and self._mqtt_thread.is_alive()
|
|
211
|
+
|
|
212
|
+
def start_all(self, tools: list) -> bool:
|
|
213
|
+
"""Start all required infrastructure based on tool requirements.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
tools: List of tool configurations with 'name' and 'source' keys
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
True if all required infrastructure started successfully
|
|
220
|
+
"""
|
|
221
|
+
# Determine what's needed
|
|
222
|
+
need_fastapi = any(t.get('source') == 'fastapi' for t in tools)
|
|
223
|
+
need_mqtt = any(t.get('source') == 'mqtt' for t in tools)
|
|
224
|
+
|
|
225
|
+
success = True
|
|
226
|
+
|
|
227
|
+
if need_fastapi:
|
|
228
|
+
if not self.start_fastapi():
|
|
229
|
+
logger.error("Failed to start FastAPI server")
|
|
230
|
+
success = False
|
|
231
|
+
|
|
232
|
+
if need_mqtt:
|
|
233
|
+
if not self.start_mqtt_publisher():
|
|
234
|
+
logger.error("Failed to start MQTT publisher")
|
|
235
|
+
success = False
|
|
236
|
+
|
|
237
|
+
return success
|
|
238
|
+
|
|
239
|
+
def stop_all(self) -> None:
|
|
240
|
+
"""Stop all infrastructure services."""
|
|
241
|
+
self.stop_fastapi()
|
|
242
|
+
self.stop_mqtt_publisher()
|
|
243
|
+
|
|
244
|
+
def _is_port_in_use(self, host: str, port: int) -> bool:
|
|
245
|
+
"""Check if a port is in use.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
host: Host to check
|
|
249
|
+
port: Port to check
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if port is in use
|
|
253
|
+
"""
|
|
254
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
255
|
+
try:
|
|
256
|
+
s.settimeout(1.0)
|
|
257
|
+
result = s.connect_ex((host, port))
|
|
258
|
+
return result == 0
|
|
259
|
+
except Exception:
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
def _wait_for_port(
|
|
263
|
+
self,
|
|
264
|
+
host: str,
|
|
265
|
+
port: int,
|
|
266
|
+
timeout: float = 10.0) -> bool:
|
|
267
|
+
"""Wait for a port to become available.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
host: Host to check
|
|
271
|
+
port: Port to check
|
|
272
|
+
timeout: Timeout in seconds
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
True if port becomes available within timeout
|
|
276
|
+
"""
|
|
277
|
+
start_time = time.time()
|
|
278
|
+
while time.time() - start_time < timeout:
|
|
279
|
+
if self._is_port_in_use(host, port):
|
|
280
|
+
return True
|
|
281
|
+
time.sleep(0.2)
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
def _mqtt_publisher_loop(self):
|
|
285
|
+
"""Background thread that publishes data to MQTT broker.
|
|
286
|
+
|
|
287
|
+
This imports and runs the MQTT publisher from benchlab.mqtt.
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
from benchlab.mqtt.mqtt_publisher import run_mqtt_mode
|
|
291
|
+
|
|
292
|
+
# Set environment variables for MQTT configuration
|
|
293
|
+
os.environ.setdefault("MQTT_BROKER", self.mqtt_broker)
|
|
294
|
+
os.environ.setdefault("MQTT_PORT", str(self.mqtt_port))
|
|
295
|
+
|
|
296
|
+
# Run MQTT mode (it handles its own lifecycle)
|
|
297
|
+
run_mqtt_mode(broker_type=f"{self.mqtt_broker}:{self.mqtt_port}")
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(f"MQTT publisher thread error: {e}")
|
|
301
|
+
|
|
302
|
+
def __enter__(self):
|
|
303
|
+
"""Context manager entry."""
|
|
304
|
+
return self
|
|
305
|
+
|
|
306
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
307
|
+
"""Context manager exit - ensure cleanup."""
|
|
308
|
+
self.stop_all()
|
|
309
|
+
return False
|