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
benchlab/sources.py ADDED
@@ -0,0 +1,515 @@
1
+ """BENCHLAB PyTools v2 – Data Source Management.
2
+
3
+ Handles detection, startup, and teardown of the five supported
4
+ telemetry sources: direct serial, FastAPI REST server, MQTT,
5
+ named pipe (C# service), and service HTTP API (C# service).
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import socket
12
+ import sys
13
+ import urllib.request
14
+
15
+ from benchlab.core.process_manager import ProcessManager
16
+ from benchlab.core.device_registry import DeviceRegistry
17
+
18
+ logger = logging.getLogger("benchlab.launcher")
19
+
20
+
21
+ # ──────────────────────────────────────────────────────────────
22
+ # Low-level Network Helpers
23
+ # ──────────────────────────────────────────────────────────────
24
+
25
+ def _port_in_use(host: str, port: int) -> bool:
26
+ try:
27
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
28
+ sock.settimeout(1)
29
+ return sock.connect_ex((host, port)) == 0
30
+ except Exception:
31
+ return False
32
+
33
+
34
+ def check_mqtt_running(host: str = "localhost", port: int = 1883) -> bool:
35
+ try:
36
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
37
+ sock.settimeout(2)
38
+ return sock.connect_ex((host, port)) == 0
39
+ except Exception:
40
+ return False
41
+
42
+
43
+ # ──────────────────────────────────────────────────────────────
44
+ # FastAPI Source
45
+ # ──────────────────────────────────────────────────────────────
46
+
47
+ def _fastapi_health(host: str, port: int) -> bool:
48
+ """Return True if the FastAPI server is responding to HTTP requests."""
49
+ try:
50
+ url = f"http://{host}:{port}/health"
51
+ with urllib.request.urlopen(url, timeout=3) as resp:
52
+ if resp.status == 200:
53
+ data = json.loads(resp.read().decode())
54
+ return data.get("status") == "healthy"
55
+ except Exception as e:
56
+ logger.debug(f"FastAPI health check failed: {e}")
57
+ return False
58
+
59
+
60
+ def _fastapi_devices_available(host: str, port: int) -> bool:
61
+ """Return True if the FastAPI server has devices available."""
62
+ try:
63
+ url = f"http://{host}:{port}/devices"
64
+ with urllib.request.urlopen(url, timeout=3) as resp:
65
+ devices = json.loads(resp.read().decode())
66
+ return isinstance(devices, list) and len(devices) > 0
67
+ except Exception:
68
+ return False
69
+
70
+
71
+ def _trigger_fastapi_scan(host: str, port: int) -> None:
72
+ """POST /scan to an already-running FastAPI server to force
73
+ device discovery."""
74
+ try:
75
+ req = urllib.request.Request(
76
+ f"http://{host}:{port}/scan", method="POST", data=b""
77
+ )
78
+ with urllib.request.urlopen(req, timeout=5):
79
+ pass
80
+ except Exception:
81
+ pass
82
+
83
+
84
+ def start_fastapi_source(port: int = 8000) -> bool:
85
+ """Start the FastAPI server as a telemetry source via ProcessManager."""
86
+ pm = ProcessManager.get_instance()
87
+ if pm.is_running("fastapi"):
88
+ return True
89
+
90
+ logger.info(f"Starting FastAPI server on port {port}...")
91
+
92
+ ok = pm.start_service(
93
+ name="fastapi",
94
+ cmd=[sys.executable, "-m", "benchlab", "-fastapi"],
95
+ health_check=lambda: _fastapi_health("127.0.0.1", port),
96
+ timeout=20,
97
+ )
98
+
99
+ if ok:
100
+ devices_available = _fastapi_devices_available("127.0.0.1", port)
101
+ if devices_available:
102
+ logger.info(f"FastAPI server ready on port {port} with device(s)")
103
+ else:
104
+ logger.warning(
105
+ f"FastAPI server ready on port {port} but no devices detected")
106
+ else:
107
+ svc = pm.get_service("fastapi")
108
+ if svc and (svc.stderr_log or svc.stdout_log):
109
+ logger.error("FastAPI failed to start. Server log:")
110
+ for line in (svc.stderr_log or svc.stdout_log).splitlines()[-15:]:
111
+ print(f" > {line}")
112
+ return ok
113
+
114
+
115
+ # ──────────────────────────────────────────────────────────────
116
+ # MQTT Source
117
+ # ──────────────────────────────────────────────────────────────
118
+
119
+ def _mqtt_device_check(broker: str) -> bool:
120
+ """Return True if the MQTT datasource can discover any devices."""
121
+ try:
122
+ from benchlab.core.datasource import MQTTDataSource
123
+ ds = MQTTDataSource(broker=broker, timeout=3)
124
+ if ds.connect():
125
+ devices = ds.list_devices()
126
+ ds.disconnect()
127
+ return len(devices) > 0
128
+ except Exception:
129
+ pass
130
+ return False
131
+
132
+
133
+ def start_mqtt_broker(port: int = 1883) -> bool:
134
+ """Start an embedded amqtt broker if no external broker is detected."""
135
+ if check_mqtt_running("localhost", port):
136
+ return True
137
+
138
+ logger.info(f"Starting embedded MQTT broker on port {port}...")
139
+ pm = ProcessManager.get_instance()
140
+ if pm.is_running("mqtt_broker"):
141
+ return True
142
+
143
+ broker_script = f'''
144
+ import asyncio
145
+ from amqtt.broker import Broker
146
+
147
+ config = {{
148
+ "listeners": {{
149
+ "default": {{
150
+ "type": "tcp",
151
+ "bind": "0.0.0.0:{port}",
152
+ }},
153
+ }},
154
+ "auth": {{"allow-anonymous": True}},
155
+ "topic-check": {{"enabled": False}},
156
+ }}
157
+
158
+ async def main():
159
+ broker = Broker(config)
160
+ await broker.start()
161
+ while True:
162
+ await asyncio.sleep(1)
163
+
164
+ asyncio.run(main())
165
+ '''
166
+
167
+ def _broker_port_check():
168
+ try:
169
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
170
+ sock.settimeout(1)
171
+ return sock.connect_ex(("127.0.0.1", port)) == 0
172
+ except Exception:
173
+ return False
174
+
175
+ ok = pm.start_service(
176
+ name="mqtt_broker",
177
+ cmd=[sys.executable, "-c", broker_script],
178
+ health_check=_broker_port_check,
179
+ timeout=15,
180
+ )
181
+
182
+ if ok:
183
+ logger.info(f"Embedded MQTT broker ready on port {port}")
184
+ else:
185
+ logger.error("Embedded MQTT broker failed to start")
186
+ return ok
187
+
188
+
189
+ def start_mqtt_source(broker: str = "localhost", port: int = 1883) -> bool:
190
+ """Start the MQTT publisher as a telemetry source via ProcessManager."""
191
+ pm = ProcessManager.get_instance()
192
+ if pm.is_running("mqtt_publisher"):
193
+ return True
194
+
195
+ logger.info(f"Starting MQTT publisher to {broker}:{port}...")
196
+
197
+ ok = pm.start_service(
198
+ name="mqtt_publisher",
199
+ cmd=[sys.executable, "-m", "benchlab", "-mqtt", broker],
200
+ health_check=lambda: _mqtt_device_check(broker),
201
+ timeout=20,
202
+ )
203
+
204
+ if ok:
205
+ logger.info("MQTT publisher ready with device(s)")
206
+ else:
207
+ svc = pm.get_service("mqtt_publisher")
208
+ if svc and (svc.stderr_log or svc.stdout_log):
209
+ logger.error("MQTT publisher failed to start. Server log:")
210
+ for line in (svc.stderr_log or svc.stdout_log).splitlines()[-15:]:
211
+ print(f" > {line}")
212
+ return ok
213
+
214
+
215
+ # ──────────────────────────────────────────────────────────────
216
+ # Named Pipe Source (C# BenchLab Windows service)
217
+ # ──────────────────────────────────────────────────────────────
218
+
219
+ def _named_pipe_available() -> bool:
220
+ """Return True if the BenchlabDiscovery pipe exists (service is
221
+ running)."""
222
+ if not sys.platform.startswith("win"):
223
+ return False
224
+ try:
225
+ import win32pipe
226
+ # WaitNamedPipe with a very short timeout just checks existence
227
+ win32pipe.WaitNamedPipe(r"\\.\pipe\BenchlabDiscovery", 1000)
228
+ return True
229
+ except Exception:
230
+ return False
231
+
232
+
233
+ def check_named_pipe_service() -> bool:
234
+ """Check if the C# BenchLab named pipe service is running.
235
+
236
+ Returns True if the discovery pipe is present and responsive.
237
+ Does NOT attempt to start the service — call this to decide
238
+ whether to offer named_pipe as a source option.
239
+ """
240
+ if not sys.platform.startswith("win"):
241
+ logger.debug("Named pipe source is Windows-only")
242
+ return False
243
+
244
+ if not _named_pipe_available():
245
+ logger.debug(
246
+ "BenchlabDiscovery pipe not found — C# service not running")
247
+ return False
248
+
249
+ # Quick smoke-test: open the pipe and send ListDevices
250
+ handle = None
251
+ try:
252
+ import win32file
253
+ import win32pipe
254
+
255
+ path = r"\\.\pipe\BenchlabDiscovery"
256
+ win32pipe.WaitNamedPipe(path, 2000)
257
+ handle = win32file.CreateFile(
258
+ path,
259
+ win32file.GENERIC_READ | win32file.GENERIC_WRITE,
260
+ 0, None,
261
+ win32file.OPEN_EXISTING,
262
+ 0, None,
263
+ )
264
+ win32file.WriteFile(handle, b"ListDevices\n")
265
+ _, data = win32file.ReadFile(handle, 65536)
266
+ result = json.loads(data.split(b"\n")[0].decode("utf-8"))
267
+ return isinstance(result, list)
268
+ except Exception as e:
269
+ logger.debug(f"Named pipe smoke-test failed: {e}")
270
+ return False
271
+ finally:
272
+ if handle is not None:
273
+ try:
274
+ win32file.CloseHandle(handle)
275
+ except Exception:
276
+ pass
277
+
278
+
279
+ # ──────────────────────────────────────────────────────────────
280
+ # Service HTTP Source (C# BenchLab Windows service REST API)
281
+ # ──────────────────────────────────────────────────────────────
282
+
283
+ SERVICE_HTTP_DEFAULT_PORT = 8585
284
+
285
+
286
+ def _service_http_health(host: str = "localhost",
287
+ port: int = SERVICE_HTTP_DEFAULT_PORT) -> bool:
288
+ """Return True if the C# BenchLab service HTTP API is healthy."""
289
+ try:
290
+ url = f"http://{host}:{port}/health"
291
+ with urllib.request.urlopen(url, timeout=3) as resp:
292
+ if resp.status == 200:
293
+ data = json.loads(resp.read().decode())
294
+ # C# service returns {"status":"healthy",...}
295
+ return data.get("status") == "healthy"
296
+ except Exception as e:
297
+ logger.debug(f"Service HTTP health check failed: {e}")
298
+ return False
299
+
300
+
301
+ def _service_http_devices_available(
302
+ host: str = "localhost",
303
+ port: int = SERVICE_HTTP_DEFAULT_PORT) -> bool:
304
+ """Return True if the C# service has at least one device."""
305
+ try:
306
+ url = f"http://{host}:{port}/devices"
307
+ with urllib.request.urlopen(url, timeout=3) as resp:
308
+ devices = json.loads(resp.read().decode())
309
+ return isinstance(devices, list) and len(devices) > 0
310
+ except Exception:
311
+ return False
312
+
313
+
314
+ def check_service_http() -> bool:
315
+ """Check if the C# BenchLab service HTTP API is reachable.
316
+
317
+ Auto-detects at localhost:8585. Does NOT start the service.
318
+ """
319
+ return _service_http_health()
320
+
321
+
322
+ def _direct_device_available() -> bool:
323
+ """Return True if pycore can see a BENCHLAB device on any serial port."""
324
+ try:
325
+ from benchlab_pycore.core import get_benchlab_ports
326
+ return len(get_benchlab_ports()) > 0
327
+ except Exception as e:
328
+ logger.debug(f"Direct device scan failed: {e}")
329
+ return False
330
+
331
+
332
+ # ──────────────────────────────────────────────────────────────
333
+ # Unified Source Setup
334
+ # ──────────────────────────────────────────────────────────────
335
+
336
+ def check_and_setup_source(source_type: str, **kwargs) -> bool:
337
+ """Check if a data source is available and start it if not.
338
+
339
+ Sets the relevant environment variables and returns True when the
340
+ source is ready, False if setup failed.
341
+
342
+ Supported source_type values:
343
+ 'direct' — direct serial via pycore
344
+ 'fastapi' — Python benchlab FastAPI server
345
+ 'mqtt' — MQTT broker + publisher
346
+ 'named_pipe' — C# BenchLab service named pipes (Windows only)
347
+ 'service_http' — C# BenchLab service HTTP API
348
+ """
349
+ if source_type == "direct":
350
+ os.environ["BENCHLAB_DATA_SOURCE"] = "direct"
351
+ if not _direct_device_available():
352
+ logger.warning(
353
+ "No BENCHLAB device detected on any serial port. "
354
+ "Plug in a device or select a different data source."
355
+ )
356
+ return True
357
+
358
+ if source_type == "fastapi":
359
+ port = kwargs.get("port", 8000)
360
+ host = kwargs.get("host", "127.0.0.1")
361
+ api_url = f"http://{host}:{port}"
362
+ os.environ["BENCHLAB_API_URL"] = api_url
363
+ os.environ["API_PORT"] = str(port)
364
+
365
+ if _fastapi_health(host, port):
366
+ devices_msg = "with device(s)" if _fastapi_devices_available(
367
+ host, port) else "but no devices detected"
368
+ logger.info(f"FastAPI already running at {api_url} {devices_msg}")
369
+ os.environ["BENCHLAB_DATA_SOURCE"] = "fastapi"
370
+ return True
371
+
372
+ if _port_in_use(host, port):
373
+ logger.info(
374
+ f"Port {port} is in use — triggering /scan on existing server")
375
+ _trigger_fastapi_scan(host, port)
376
+ if _fastapi_health(host, port):
377
+ devices_msg = "with device(s)" if _fastapi_devices_available(
378
+ host, port) else "but no devices detected"
379
+ logger.info(f"FastAPI at {api_url} is healthy {devices_msg}")
380
+ os.environ["BENCHLAB_DATA_SOURCE"] = "fastapi"
381
+ return True
382
+ logger.error(
383
+ f"Port {port} is occupied but server is not responding "
384
+ "— cannot start")
385
+ return False
386
+
387
+ logger.info(f"FastAPI not detected at {api_url}")
388
+ ok = start_fastapi_source(port)
389
+ if ok:
390
+ devices_msg = "with device(s)" if _fastapi_devices_available(
391
+ host, port) else "but no devices detected"
392
+ logger.info(f"FastAPI server started on port {port} {devices_msg}")
393
+ os.environ["BENCHLAB_DATA_SOURCE"] = "fastapi"
394
+ return ok
395
+
396
+ if source_type == "mqtt":
397
+ host = kwargs.get("broker", "localhost")
398
+ mqtt_port = kwargs.get("mqtt_port", 1883)
399
+ os.environ["MQTT_BROKER"] = host
400
+ os.environ["MQTT_PORT"] = str(mqtt_port)
401
+
402
+ if not check_mqtt_running(host, mqtt_port):
403
+ logger.info(f"MQTT broker not detected at {host}:{mqtt_port}")
404
+ if not start_mqtt_broker(mqtt_port):
405
+ return False
406
+ else:
407
+ logger.info(f"MQTT broker available at {host}:{mqtt_port}")
408
+
409
+ return start_mqtt_source(host, mqtt_port)
410
+
411
+ if source_type == "named_pipe":
412
+ if not sys.platform.startswith("win"):
413
+ logger.error(
414
+ "Named pipe source is only available on Windows. "
415
+ "The C# BenchLab service uses Windows named pipes."
416
+ )
417
+ return False
418
+
419
+ if not _named_pipe_available():
420
+ logger.error(
421
+ "BenchLab named pipe service not detected.\n"
422
+ " → Make sure the BenchLab Windows service (BL_Service) "
423
+ "is running.\n"
424
+ " → You can start it via Windows Services or by "
425
+ "running BL_Service.exe.")
426
+ return False
427
+
428
+ logger.info("BenchLab named pipe service detected and ready")
429
+ os.environ["BENCHLAB_DATA_SOURCE"] = "named_pipe"
430
+ return True
431
+
432
+ if source_type == "service_http":
433
+ port = kwargs.get("port", SERVICE_HTTP_DEFAULT_PORT)
434
+ host = kwargs.get("host", "localhost")
435
+
436
+ if not _service_http_health(host, port):
437
+ logger.error(
438
+ f"BenchLab service HTTP API not detected at "
439
+ f"http://{host}:{port}.\n"
440
+ " → Make sure the BenchLab Windows service (BL_Service) "
441
+ "is running.\n"
442
+ " → The service HTTP API listens on port 8585 by default."
443
+ )
444
+ return False
445
+
446
+ devices_msg = "with device(s)" if _service_http_devices_available(
447
+ host, port) else "but no devices detected"
448
+ logger.info(
449
+ f"BenchLab service HTTP API ready at "
450
+ f"http://{host}:{port} {devices_msg}")
451
+ os.environ["BENCHLAB_DATA_SOURCE"] = "service_http"
452
+ os.environ["BENCHLAB_SERVICE_URL"] = f"http://{host}:{port}"
453
+ return True
454
+
455
+ if source_type == "fastapi_custom":
456
+ base_url = kwargs.get("base_url")
457
+ if not base_url:
458
+ logger.error(
459
+ "fastapi_custom requires a base_url "
460
+ "(e.g., http://192.168.1.100:8000)")
461
+ return False
462
+
463
+ # Parse the URL to get host and port
464
+ from urllib.parse import urlparse
465
+ parsed = urlparse(base_url)
466
+ host = parsed.hostname
467
+ port = parsed.port or 8000
468
+
469
+ os.environ["BENCHLAB_API_URL"] = base_url
470
+ os.environ["BENCHLAB_DATA_SOURCE"] = "fastapi_custom"
471
+
472
+ # Check if the remote server is healthy
473
+ if _fastapi_health(host, port):
474
+ devices_msg = "with device(s)" if _fastapi_devices_available(
475
+ host, port) else "but no devices detected"
476
+ logger.info(
477
+ f"FastAPI server available at {base_url} {devices_msg}")
478
+ return True
479
+ else:
480
+ logger.error(f"FastAPI server not reachable at {base_url}")
481
+ logger.error(
482
+ "Make sure the remote server is running and accessible "
483
+ "from this machine.")
484
+ return False
485
+
486
+ if source_type == "mqtt_custom":
487
+ broker = kwargs.get("broker", "localhost")
488
+ mqtt_port = kwargs.get("mqtt_port", 1883)
489
+ os.environ["MQTT_BROKER"] = broker
490
+ os.environ["MQTT_PORT"] = str(mqtt_port)
491
+
492
+ # Check if the custom MQTT broker is reachable
493
+ if not check_mqtt_running(broker, mqtt_port):
494
+ logger.error(f"MQTT broker not reachable at {broker}:{mqtt_port}")
495
+ logger.error(
496
+ "Make sure the MQTT broker is running and accessible.")
497
+ return False
498
+
499
+ logger.info(f"MQTT broker available at {broker}:{mqtt_port}")
500
+ os.environ["BENCHLAB_DATA_SOURCE"] = "mqtt_custom"
501
+
502
+ # Start the MQTT publisher pointing to the custom broker
503
+ return start_mqtt_source(broker, mqtt_port)
504
+
505
+ return False
506
+
507
+
508
+ # ──────────────────────────────────────────────────────────────
509
+ # Cleanup
510
+ # ──────────────────────────────────────────────────────────────
511
+
512
+ def cleanup_all_services() -> None:
513
+ """Shut down all infrastructure services started during this session."""
514
+ ProcessManager.get_instance().shutdown_all()
515
+ DeviceRegistry.get_instance().clear()
benchlab/tools.py ADDED
@@ -0,0 +1,147 @@
1
+ """BENCHLAB PyTools v2 – Tool Definitions & Dependency Helpers.
2
+
3
+ Defines the CONSUMER_TOOLS registry, LAUNCH_PROFILES, and helpers for
4
+ locating tool modules and installing their dependencies.
5
+ """
6
+
7
+ import importlib
8
+ import importlib.util
9
+ import logging
10
+ from pathlib import Path
11
+
12
+ from bootstrap import install_requirements_file
13
+
14
+ logger = logging.getLogger("benchlab.launcher")
15
+
16
+
17
+ # ──────────────────────────────────────────────────────────────
18
+ # Tool Definitions (consumer tools only)
19
+ # ──────────────────────────────────────────────────────────────
20
+
21
+ CONSUMER_TOOLS = {
22
+ "config": {
23
+ "name": "Config Tool",
24
+ "description": "Import/export device configuration",
25
+ "flag": "-config",
26
+ "module": "benchlab.config.config_tool",
27
+ "function": "main",
28
+ "requirements": "requirements.txt",
29
+ "supported_sources": ["direct", "named_pipe"],
30
+ },
31
+ "csv_log": {
32
+ "name": "CSV Logger",
33
+ "description": "Log device telemetry to CSV files",
34
+ "flag": "-logfleet",
35
+ "module": "benchlab.csv_log.csv_logger_enhanced",
36
+ "function": "run_enhanced_csv_logger",
37
+ "requirements": "requirements.txt",
38
+ },
39
+ "graph": {
40
+ "name": "DearPyGui Graph",
41
+ "description": "Interactive sensor graphing widget",
42
+ "flag": "-graph",
43
+ "module": "benchlab.graph.runner",
44
+ "function": "run_graph_mode",
45
+ "requirements": "requirements.txt",
46
+ },
47
+ "hwinfo": {
48
+ "name": "HWiNFO Export",
49
+ "description": "Export sensors to HWiNFO64 custom sensors",
50
+ "flag": "-hwinfo",
51
+ "module": "benchlab.hwinfo.hwinfo_export",
52
+ "function": "export_all_devices",
53
+ "requirements": "requirements.txt",
54
+ },
55
+ "link": {
56
+ "name": "Link",
57
+ "description": "Publish telemetry to BENCHLAB SaaS",
58
+ "flag": "-link",
59
+ "module": "benchlab.link.link_main",
60
+ "function": "run_link",
61
+ "requirements": "requirements.txt",
62
+ },
63
+ "tui": {
64
+ "name": "TUI",
65
+ "description": "Interactive terminal user interface",
66
+ "flag": "-tui",
67
+ "module": "benchlab.tui.tui_main",
68
+ "function": "tui_main",
69
+ "requirements": "requirements.txt",
70
+ "terminal": {"cols": 220, "rows": 70},
71
+ },
72
+ "vu": {
73
+ "name": "VU Dials",
74
+ "description": "Analog-style VU meter dials",
75
+ "flag": "-vu",
76
+ "module": "benchlab.vu.vu_updater",
77
+ "function": "run_updater",
78
+ "requirements": "requirements.txt",
79
+ },
80
+ "vuconfig": {
81
+ "name": "VU Config",
82
+ "description": "VU meter configuration interface",
83
+ "flag": "-vuconfig",
84
+ "module": "benchlab.vu.vu_tui",
85
+ "function": "launch_vu_config",
86
+ "requirements": "requirements.txt",
87
+ },
88
+ "wigidash": {
89
+ "name": "WigiDash",
90
+ "description": "Display telemetry on G.SKILL WigiDash device",
91
+ "flag": "-wigidash",
92
+ "module": "benchlab.wigidash.wigidash_manager",
93
+ "function": "main",
94
+ "requirements": "requirements.txt",
95
+ },
96
+ }
97
+
98
+
99
+ # ──────────────────────────────────────────────────────────────
100
+ # Launch Profiles
101
+ # ──────────────────────────────────────────────────────────────
102
+
103
+ LAUNCH_PROFILES = {
104
+ "gskill_ctex26": {
105
+ "tools": ["tui", "vu", "wigidash"],
106
+ "source": "fastapi_custom",
107
+ },
108
+ }
109
+
110
+ # ──────────────────────────────────────────────────────────────
111
+ # Tool Dependency Helpers
112
+ # ──────────────────────────────────────────────────────────────
113
+
114
+
115
+ def get_module_dir(module_name: str) -> Path:
116
+ """Return the directory containing the given module."""
117
+ spec = importlib.util.find_spec(module_name)
118
+ if not spec or not spec.origin:
119
+ raise ImportError(f"Cannot locate module: {module_name}")
120
+ return Path(spec.origin).parent
121
+
122
+
123
+ def ensure_tool_dependencies(tool_id: str) -> None:
124
+ """Install a tool's requirements file if present."""
125
+ tool = CONSUMER_TOOLS[tool_id]
126
+ req = tool.get("requirements")
127
+ if not req:
128
+ return
129
+ module_dir = get_module_dir(tool["module"])
130
+ req_file = module_dir / req
131
+ if req_file.exists():
132
+ install_requirements_file(str(req_file), tool["name"])
133
+
134
+
135
+ def ensure_profile_dependencies(profile_id: str) -> None:
136
+ """Install dependencies for all tools in a profile."""
137
+ profile = LAUNCH_PROFILES.get(profile_id)
138
+ if not profile:
139
+ raise ValueError(f"Unknown profile: {profile_id}")
140
+
141
+ for tool_id in profile["tools"]:
142
+ if tool_id in CONSUMER_TOOLS:
143
+ try:
144
+ ensure_tool_dependencies(tool_id)
145
+ except Exception as e:
146
+ logger.warning(
147
+ f"Failed to install dependencies for {tool_id}: {e}")