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.
Files changed (184) hide show
  1. benchlab/__init__.py +14 -0
  2. benchlab/__main__.py +21 -0
  3. benchlab/config/README.md +325 -0
  4. benchlab/config/__init__.py +9 -0
  5. benchlab/config/config_client.py +631 -0
  6. benchlab/config/config_manager.py +495 -0
  7. benchlab/config/config_tool.py +358 -0
  8. benchlab/config/diff.py +163 -0
  9. benchlab/config/examples/complete_config.json +56 -0
  10. benchlab/config/examples/rgb_lighting.json +24 -0
  11. benchlab/config/examples/simple_fan.json +32 -0
  12. benchlab/config/schema.py +138 -0
  13. benchlab/core/README.md +154 -0
  14. benchlab/core/__init__.py +78 -0
  15. benchlab/core/config.py +145 -0
  16. benchlab/core/datasource.py +1209 -0
  17. benchlab/core/datasource_manager.py +418 -0
  18. benchlab/core/device_registry.py +206 -0
  19. benchlab/core/discovery.py +92 -0
  20. benchlab/core/infrastructure.py +309 -0
  21. benchlab/core/process_manager.py +356 -0
  22. benchlab/core/retry.py +85 -0
  23. benchlab/core/shared_serial.py +235 -0
  24. benchlab/core/statistics.py +224 -0
  25. benchlab/csv_log/README.md +206 -0
  26. benchlab/csv_log/__init__.py +5 -0
  27. benchlab/csv_log/csv_logger_enhanced.py +416 -0
  28. benchlab/csv_log/message_batcher.py +361 -0
  29. benchlab/csv_log/requirements.txt +3 -0
  30. benchlab/csv_log/smart_retry.py +466 -0
  31. benchlab/graph/README.md +117 -0
  32. benchlab/graph/__init__.py +5 -0
  33. benchlab/graph/app.py +353 -0
  34. benchlab/graph/requirements.txt +2 -0
  35. benchlab/graph/runner.py +42 -0
  36. benchlab/graph/ui.py +288 -0
  37. benchlab/hwinfo/README.md +135 -0
  38. benchlab/hwinfo/__init__.py +5 -0
  39. benchlab/hwinfo/hwinfo_export.py +386 -0
  40. benchlab/hwinfo/requirements.txt +1 -0
  41. benchlab/launcher.py +387 -0
  42. benchlab/link/.env.EXAMPLE +10 -0
  43. benchlab/link/README.md +172 -0
  44. benchlab/link/__init__.py +1 -0
  45. benchlab/link/link_main.py +479 -0
  46. benchlab/main.py +462 -0
  47. benchlab/menu.py +729 -0
  48. benchlab/menu_classic.py +410 -0
  49. benchlab/mqtt/README.md +201 -0
  50. benchlab/mqtt/__init__.py +5 -0
  51. benchlab/mqtt/mqtt.config_template +2 -0
  52. benchlab/mqtt/mqtt_publisher.py +540 -0
  53. benchlab/mqtt/requirements.txt +3 -0
  54. benchlab/prefs.py +67 -0
  55. benchlab/restapi/.env.example +17 -0
  56. benchlab/restapi/__init__.py +5 -0
  57. benchlab/restapi/favicon.ico +0 -0
  58. benchlab/restapi/readme.md +202 -0
  59. benchlab/restapi/requirements.txt +5 -0
  60. benchlab/restapi/telemetry_api.py +808 -0
  61. benchlab/sources.py +515 -0
  62. benchlab/tools.py +147 -0
  63. benchlab/tui/README.md +266 -0
  64. benchlab/tui/__init__.py +5 -0
  65. benchlab/tui/config.py +477 -0
  66. benchlab/tui/requirements.txt +2 -0
  67. benchlab/tui/tui_core.py +1298 -0
  68. benchlab/tui/tui_main.py +418 -0
  69. benchlab/vu/README.md +64 -0
  70. benchlab/vu/VU-Server/NOTICE.md +13 -0
  71. benchlab/vu/VU-Server/README.md +80 -0
  72. benchlab/vu/VU-Server/Running_from_source_on_linux.md +97 -0
  73. benchlab/vu/VU-Server/__init__.py +0 -0
  74. benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
  75. benchlab/vu/VU-Server/community_applications.md +13 -0
  76. benchlab/vu/VU-Server/config.yaml +9 -0
  77. benchlab/vu/VU-Server/database.py +256 -0
  78. benchlab/vu/VU-Server/dial_driver.py +534 -0
  79. benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +78 -0
  80. benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +100 -0
  81. benchlab/vu/VU-Server/dials/__init__.py +0 -0
  82. benchlab/vu/VU-Server/dials/base_logger.py +100 -0
  83. benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
  84. benchlab/vu/VU-Server/installer/install.nsi +148 -0
  85. benchlab/vu/VU-Server/make_version.py +40 -0
  86. benchlab/vu/VU-Server/pylintrc +628 -0
  87. benchlab/vu/VU-Server/requirements.txt +9 -0
  88. benchlab/vu/VU-Server/serial_driver.py +244 -0
  89. benchlab/vu/VU-Server/server.py +670 -0
  90. benchlab/vu/VU-Server/server_config.py +255 -0
  91. benchlab/vu/VU-Server/server_dial_handler.py +334 -0
  92. benchlab/vu/VU-Server/vu_notifications.py +24 -0
  93. benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
  94. benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
  95. benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
  96. benchlab/vu/VU-Server/www/assets/css/demo.css +291 -0
  97. benchlab/vu/VU-Server/www/assets/css/demo.min.css +9 -0
  98. benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +276 -0
  99. benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +9 -0
  100. benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +1084 -0
  101. benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +9 -0
  102. benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +1084 -0
  103. benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +9 -0
  104. benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +528 -0
  105. benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +9 -0
  106. benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +528 -0
  107. benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +9 -0
  108. benchlab/vu/VU-Server/www/assets/css/tabler-social.css +10 -0
  109. benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +8 -0
  110. benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +10 -0
  111. benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +8 -0
  112. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +1504 -0
  113. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +9 -0
  114. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +1504 -0
  115. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +9 -0
  116. benchlab/vu/VU-Server/www/assets/css/tabler.css +25813 -0
  117. benchlab/vu/VU-Server/www/assets/css/tabler.min.css +13 -0
  118. benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +25777 -0
  119. benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +13 -0
  120. benchlab/vu/VU-Server/www/assets/css/theme.min.css +261 -0
  121. benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
  122. benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
  123. benchlab/vu/VU-Server/www/assets/js/dark-theme.js +35 -0
  124. benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +9 -0
  125. benchlab/vu/VU-Server/www/assets/js/demo.js +132 -0
  126. benchlab/vu/VU-Server/www/assets/js/demo.min.js +9 -0
  127. benchlab/vu/VU-Server/www/assets/js/jquery.min.js +2 -0
  128. benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +7482 -0
  129. benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +15 -0
  130. benchlab/vu/VU-Server/www/assets/js/tabler.js +7543 -0
  131. benchlab/vu/VU-Server/www/assets/js/tabler.min.js +15 -0
  132. benchlab/vu/VU-Server/www/assets/js/vu1_api.js +68 -0
  133. benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +167 -0
  134. benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +138 -0
  135. benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +179 -0
  136. benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +60 -0
  137. benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +125 -0
  138. benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
  139. benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
  140. benchlab/vu/VU-Server/www/favicon.ico +0 -0
  141. benchlab/vu/VU-Server/www/favicon.png +0 -0
  142. benchlab/vu/VU-Server/www/index.html +47 -0
  143. benchlab/vu/VU-Server/www/views/api_keys.html +151 -0
  144. benchlab/vu/VU-Server/www/views/dial.html +286 -0
  145. benchlab/vu/VU-Server/www/views/footer.html +27 -0
  146. benchlab/vu/VU-Server/www/views/header.html +86 -0
  147. benchlab/vu/VU-Server/www/views/key_settings.html +132 -0
  148. benchlab/vu/VU-Server/www/views/start.html +124 -0
  149. benchlab/vu/__init__.py +5 -0
  150. benchlab/vu/assets/Barlow-Bold.ttf +0 -0
  151. benchlab/vu/assets/bl_dial_144x200.png +0 -0
  152. benchlab/vu/assets/bl_logo_144x200.png +0 -0
  153. benchlab/vu/devices.py +166 -0
  154. benchlab/vu/requirements.txt +24 -0
  155. benchlab/vu/sensors.py +18 -0
  156. benchlab/vu/vu_dial.config_template +90 -0
  157. benchlab/vu/vu_logo_gen.py +97 -0
  158. benchlab/vu/vu_server.config_template +5 -0
  159. benchlab/vu/vu_server_config.py +40 -0
  160. benchlab/vu/vu_server_manager.py +192 -0
  161. benchlab/vu/vu_tui.py +919 -0
  162. benchlab/vu/vu_updater.py +540 -0
  163. benchlab/wigidash/README.md +182 -0
  164. benchlab/wigidash/__init__.py +5 -0
  165. benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
  166. benchlab/wigidash/assets/Inter.ttf +0 -0
  167. benchlab/wigidash/assets/benchlab.png +0 -0
  168. benchlab/wigidash/benchlab_fleet.py +150 -0
  169. benchlab/wigidash/benchlab_graph.py +517 -0
  170. benchlab/wigidash/benchlab_overview.py +426 -0
  171. benchlab/wigidash/benchlab_telemetry.py +105 -0
  172. benchlab/wigidash/benchlab_ui.py +190 -0
  173. benchlab/wigidash/benchlab_utils.py +182 -0
  174. benchlab/wigidash/requirements.txt +9 -0
  175. benchlab/wigidash/wigidash_device.py +541 -0
  176. benchlab/wigidash/wigidash_manager.py +342 -0
  177. benchlab/wigidash/wigidash_session.py +355 -0
  178. benchlab/wigidash/wigidash_usb.py +247 -0
  179. benchlab/wigidash/wigidash_widget.py +88 -0
  180. benchlab_pytools-3.0.0.dist-info/METADATA +239 -0
  181. benchlab_pytools-3.0.0.dist-info/RECORD +184 -0
  182. benchlab_pytools-3.0.0.dist-info/WHEEL +4 -0
  183. benchlab_pytools-3.0.0.dist-info/entry_points.txt +2 -0
  184. 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