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
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}")
|