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,342 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import types
|
|
5
|
+
|
|
6
|
+
from benchlab.core.datasource_manager import DataSourceManager
|
|
7
|
+
from benchlab.wigidash.benchlab_telemetry import (
|
|
8
|
+
TelemetryHistory, telemetry_step, TelemetryContext
|
|
9
|
+
)
|
|
10
|
+
from benchlab.wigidash.benchlab_utils import get_logger
|
|
11
|
+
from benchlab.wigidash.wigidash_usb import scan_wigidash
|
|
12
|
+
from benchlab.wigidash.wigidash_session import BenchlabWigiSession
|
|
13
|
+
|
|
14
|
+
logger = get_logger("WigidashManager")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WigidashManager:
|
|
18
|
+
"""
|
|
19
|
+
Manages multiple Wigidash sessions and their assigned Benchlab devices.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, vendor_id=0x28DA, product_id=0xEF01, datasource=None):
|
|
23
|
+
self.vendor_id = vendor_id
|
|
24
|
+
self.product_id = product_id
|
|
25
|
+
self.sessions = []
|
|
26
|
+
self.benchlab_devices = {}
|
|
27
|
+
self.telemetry_histories = {}
|
|
28
|
+
self.telemetry_contexts = {}
|
|
29
|
+
self.shutting_down = False
|
|
30
|
+
self.shutdown_event = threading.Event()
|
|
31
|
+
self.shutdown_barrier = None
|
|
32
|
+
self.datasource = datasource # Already-connected DataSourceManager
|
|
33
|
+
self._telemetry_lock = threading.Lock()
|
|
34
|
+
|
|
35
|
+
# ----------------- BENCHLAB DEVICE MANAGEMENT ----------------- #
|
|
36
|
+
def get_available_benchlabs(self, log_info=True):
|
|
37
|
+
"""Query the datasource for available Benchlab devices."""
|
|
38
|
+
devices = []
|
|
39
|
+
query_succeeded = False
|
|
40
|
+
try:
|
|
41
|
+
raw = self.datasource.list_devices()
|
|
42
|
+
if isinstance(raw, dict):
|
|
43
|
+
devices = [{"port": info.get("port", "?"), "uid": uid,
|
|
44
|
+
"firmware": info.get("firmware", "?")}
|
|
45
|
+
for uid, info in raw.items()]
|
|
46
|
+
else:
|
|
47
|
+
devices = list(raw)
|
|
48
|
+
query_succeeded = True
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.warning(f"list_devices failed: {e}")
|
|
51
|
+
|
|
52
|
+
for d in devices:
|
|
53
|
+
port = d["port"]
|
|
54
|
+
uid = d["uid"]
|
|
55
|
+
fw = d.get("firmware", "?")
|
|
56
|
+
if port not in self.benchlab_devices:
|
|
57
|
+
self.benchlab_devices[port] = {
|
|
58
|
+
"uid": uid, "firmware": fw, "in_use": False}
|
|
59
|
+
else:
|
|
60
|
+
self.benchlab_devices[port]["uid"] = uid
|
|
61
|
+
self.benchlab_devices[port]["firmware"] = fw
|
|
62
|
+
|
|
63
|
+
# Prune ports no longer reported by the data source, so a disconnected
|
|
64
|
+
# device doesn't linger forever with stale UID/firmware/in_use state.
|
|
65
|
+
if query_succeeded:
|
|
66
|
+
current_ports = {d["port"] for d in devices}
|
|
67
|
+
for stale_port in set(self.benchlab_devices) - current_ports:
|
|
68
|
+
del self.benchlab_devices[stale_port]
|
|
69
|
+
self.telemetry_contexts.pop(stale_port, None)
|
|
70
|
+
|
|
71
|
+
all_devices = [
|
|
72
|
+
{"port": port, "uid": info["uid"], "in_use": info["in_use"]}
|
|
73
|
+
for port, info in self.benchlab_devices.items()
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
if log_info:
|
|
77
|
+
count = len(all_devices)
|
|
78
|
+
if count:
|
|
79
|
+
logger.info(
|
|
80
|
+
f"{count} available Benchlab device{
|
|
81
|
+
's' if count > 1 else ''}:")
|
|
82
|
+
for dev in all_devices:
|
|
83
|
+
logger.info(f" {dev['port']}: UID {dev['uid']}")
|
|
84
|
+
else:
|
|
85
|
+
logger.info("No Benchlab devices detected.")
|
|
86
|
+
|
|
87
|
+
return all_devices
|
|
88
|
+
|
|
89
|
+
# ----------------- TELEMETRY MANAGEMENT ----------------- #
|
|
90
|
+
def release_port(self, port):
|
|
91
|
+
if port in self.benchlab_devices:
|
|
92
|
+
self.benchlab_devices[port]["in_use"] = False
|
|
93
|
+
|
|
94
|
+
def start_telemetry(self, port, session: BenchlabWigiSession):
|
|
95
|
+
if port not in self.benchlab_devices:
|
|
96
|
+
logger.warning(f"Cannot start telemetry: unknown port {port}")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
with self._telemetry_lock:
|
|
100
|
+
# ------------------------------------------------
|
|
101
|
+
# CASE 1: Telemetry already running for this port
|
|
102
|
+
# ------------------------------------------------
|
|
103
|
+
if port in self.telemetry_contexts:
|
|
104
|
+
ctx = self.telemetry_contexts[port]
|
|
105
|
+
|
|
106
|
+
logger.info(
|
|
107
|
+
f"Selected device {port} is already in use. "
|
|
108
|
+
"Using existing telemetry.")
|
|
109
|
+
|
|
110
|
+
session.ser = ctx.ser
|
|
111
|
+
session.device_info = ctx.device_info
|
|
112
|
+
session.uid = ctx.uid
|
|
113
|
+
session.telemetry_history = ctx.history
|
|
114
|
+
session.history = ctx.history
|
|
115
|
+
session.selected_port = port
|
|
116
|
+
session.telemetry_context = ctx
|
|
117
|
+
|
|
118
|
+
ctx.sessions.append(session)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------
|
|
122
|
+
# CASE 2: First session → start telemetry
|
|
123
|
+
# ------------------------------------------------
|
|
124
|
+
self.benchlab_devices[port]["in_use"] = True
|
|
125
|
+
uid = self.benchlab_devices[port]["uid"]
|
|
126
|
+
history = self.telemetry_histories.setdefault(
|
|
127
|
+
uid, TelemetryHistory())
|
|
128
|
+
|
|
129
|
+
# Get device info from datasource
|
|
130
|
+
device_info = {}
|
|
131
|
+
try:
|
|
132
|
+
self.datasource.select_device(uid)
|
|
133
|
+
snap = self.datasource.snapshot()
|
|
134
|
+
device_info = snap.get("device_info") or snap.get(
|
|
135
|
+
"all_devices", {}).get(uid, {})
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.warning(f"Could not get device info for {uid}: {e}")
|
|
138
|
+
|
|
139
|
+
ctx = TelemetryContext(
|
|
140
|
+
port=port,
|
|
141
|
+
ser=None,
|
|
142
|
+
device_info=device_info,
|
|
143
|
+
uid=uid,
|
|
144
|
+
history=history,
|
|
145
|
+
)
|
|
146
|
+
ctx.sessions = [session]
|
|
147
|
+
self.telemetry_contexts[port] = ctx
|
|
148
|
+
|
|
149
|
+
session.ser = None
|
|
150
|
+
session.device_info = device_info
|
|
151
|
+
session.uid = uid
|
|
152
|
+
session.telemetry_history = history
|
|
153
|
+
session.history = history
|
|
154
|
+
session.selected_port = port
|
|
155
|
+
|
|
156
|
+
def telemetry_loop():
|
|
157
|
+
while not self.shutdown_event.is_set():
|
|
158
|
+
try:
|
|
159
|
+
self.datasource.select_device(uid)
|
|
160
|
+
snap = self.datasource.snapshot()
|
|
161
|
+
data = (snap.get("sensor_data")
|
|
162
|
+
or snap.get("all_telemetry", {}).get(uid)
|
|
163
|
+
or {})
|
|
164
|
+
if data:
|
|
165
|
+
telemetry_step(
|
|
166
|
+
ctx, device_info=device_info,
|
|
167
|
+
sensor_struct=data)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.warning(f"Telemetry error on {port}: {e}")
|
|
170
|
+
time.sleep(0.1)
|
|
171
|
+
|
|
172
|
+
threading.Thread(target=telemetry_loop, daemon=True).start()
|
|
173
|
+
|
|
174
|
+
# ----------------- SESSION MANAGEMENT ----------------- #
|
|
175
|
+
|
|
176
|
+
def detect_and_start_sessions(self):
|
|
177
|
+
logger.info("Looking for WigiDash devices ...")
|
|
178
|
+
time.sleep(1)
|
|
179
|
+
|
|
180
|
+
usb_devices = scan_wigidash(self.vendor_id, self.product_id)
|
|
181
|
+
if not usb_devices:
|
|
182
|
+
logger.warning("No WigiDash devices detected.")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
used_serials = {
|
|
186
|
+
s.usb_device.serial for s in self.sessions if s.usb_device.serial}
|
|
187
|
+
|
|
188
|
+
new_devices = []
|
|
189
|
+
for usb in usb_devices:
|
|
190
|
+
if not usb.serial:
|
|
191
|
+
try:
|
|
192
|
+
usb.serial = usb.util.get_string(
|
|
193
|
+
usb.dev, usb.dev.iSerialNumber)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.warning(f"Failed to read USB serial: {e}")
|
|
196
|
+
continue
|
|
197
|
+
if usb.serial in used_serials:
|
|
198
|
+
continue
|
|
199
|
+
new_devices.append(usb)
|
|
200
|
+
|
|
201
|
+
if not new_devices:
|
|
202
|
+
logger.info("No new WigiDash devices.")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
logger.info("Scanning Benchlab devices ...")
|
|
206
|
+
time.sleep(1)
|
|
207
|
+
self.get_available_benchlabs() # only builds cache, closes ports
|
|
208
|
+
|
|
209
|
+
logger.info("Starting Wigi sessions ...")
|
|
210
|
+
for usb in new_devices:
|
|
211
|
+
session = BenchlabWigiSession(
|
|
212
|
+
usb_device=usb,
|
|
213
|
+
telemetry_history=None,
|
|
214
|
+
manager=self,
|
|
215
|
+
)
|
|
216
|
+
try:
|
|
217
|
+
threading.Thread(target=session.run, daemon=True).start()
|
|
218
|
+
self.sessions.append(session)
|
|
219
|
+
used_serials.add(usb.serial)
|
|
220
|
+
logger.info(f"Started Wigidash session for {usb.serial}")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Failed to start session for {usb.serial}: {e}")
|
|
223
|
+
|
|
224
|
+
def shutdown_manager(self):
|
|
225
|
+
"""Close USB and COM ports safely after all sessions done."""
|
|
226
|
+
logger.info("Manager Shutdown Initiated.")
|
|
227
|
+
|
|
228
|
+
# Close COM ports
|
|
229
|
+
for port in self.benchlab_devices:
|
|
230
|
+
self.release_port(port)
|
|
231
|
+
|
|
232
|
+
self.sessions.clear()
|
|
233
|
+
logger.info("Manager shutdown completed")
|
|
234
|
+
|
|
235
|
+
def graceful_shutdown(self):
|
|
236
|
+
"""Graceful shutdown: synchronize splash, then cleanup."""
|
|
237
|
+
if getattr(self, "shutting_down", False):
|
|
238
|
+
logger.info("Graceful shutdown already in progress.")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
logger.info("Initiating graceful shutdown...")
|
|
242
|
+
self.shutting_down = True
|
|
243
|
+
self.shutdown_event.set()
|
|
244
|
+
|
|
245
|
+
# Disconnect DataSource if we created one
|
|
246
|
+
if self.datasource is not None:
|
|
247
|
+
try:
|
|
248
|
+
self.datasource.disconnect()
|
|
249
|
+
logger.info("DataSource disconnected")
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.warning(f"Error disconnecting DataSource: {e}")
|
|
252
|
+
|
|
253
|
+
active_sessions = [s for s in self.sessions if s.app_running]
|
|
254
|
+
|
|
255
|
+
# Only create a barrier if we want splash synchronization
|
|
256
|
+
if active_sessions:
|
|
257
|
+
self.shutdown_barrier = threading.Barrier(len(active_sessions) + 1)
|
|
258
|
+
|
|
259
|
+
# Trigger shutdown in all sessions
|
|
260
|
+
for session in active_sessions:
|
|
261
|
+
threading.Thread(
|
|
262
|
+
target=session.shutdown_session,
|
|
263
|
+
daemon=True).start()
|
|
264
|
+
|
|
265
|
+
# If barrier exists, wait for synchronized splash
|
|
266
|
+
if self.shutdown_barrier:
|
|
267
|
+
try:
|
|
268
|
+
self.shutdown_barrier.wait()
|
|
269
|
+
except threading.BrokenBarrierError:
|
|
270
|
+
logger.warning("Shutdown barrier broken in manager.")
|
|
271
|
+
self.shutdown_barrier = None
|
|
272
|
+
|
|
273
|
+
# Wait for all sessions to signal cleanup_done
|
|
274
|
+
for session in active_sessions:
|
|
275
|
+
# blocks until each session finishes cleanup
|
|
276
|
+
session.cleanup_done.wait()
|
|
277
|
+
|
|
278
|
+
# Clean up manager
|
|
279
|
+
self.shutdown_manager()
|
|
280
|
+
logger.info("Graceful shutdown completed.")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main(args=None):
|
|
284
|
+
"""Entry point. Accepts standard benchlab args namespace."""
|
|
285
|
+
if args is None:
|
|
286
|
+
args = types.SimpleNamespace(
|
|
287
|
+
source=os.environ.get(
|
|
288
|
+
"BENCHLAB_DATA_SOURCE",
|
|
289
|
+
"direct"),
|
|
290
|
+
api_url=os.environ.get(
|
|
291
|
+
"BENCHLAB_API_URL",
|
|
292
|
+
"http://127.0.0.1:8000"),
|
|
293
|
+
mqtt_broker=os.environ.get(
|
|
294
|
+
"MQTT_BROKER",
|
|
295
|
+
"localhost"),
|
|
296
|
+
mqtt_port=int(
|
|
297
|
+
os.environ.get(
|
|
298
|
+
"MQTT_PORT",
|
|
299
|
+
"1883")),
|
|
300
|
+
interval=1.0,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
source = args.source
|
|
304
|
+
ds_kwargs = {}
|
|
305
|
+
if source in ("fastapi", "fastapi_custom"):
|
|
306
|
+
ds_kwargs["base_url"] = args.api_url
|
|
307
|
+
logger.info(f"Using FastAPI datasource: {args.api_url}")
|
|
308
|
+
elif source == "mqtt":
|
|
309
|
+
ds_kwargs["broker"] = args.mqtt_broker
|
|
310
|
+
ds_kwargs["port"] = args.mqtt_port
|
|
311
|
+
logger.info(
|
|
312
|
+
f"Using MQTT datasource: {
|
|
313
|
+
args.mqtt_broker}:{
|
|
314
|
+
args.mqtt_port}")
|
|
315
|
+
else:
|
|
316
|
+
logger.info("Using direct datasource")
|
|
317
|
+
|
|
318
|
+
datasource = DataSourceManager(source_type=source, **ds_kwargs)
|
|
319
|
+
if not datasource.connect():
|
|
320
|
+
logger.error(f"Failed to connect to {source} datasource")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
manager = WigidashManager(datasource=datasource)
|
|
324
|
+
manager.detect_and_start_sessions()
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
while True:
|
|
328
|
+
if manager.shutting_down:
|
|
329
|
+
break
|
|
330
|
+
if manager.sessions and all(
|
|
331
|
+
s.cleanup_done.is_set() for s in manager.sessions):
|
|
332
|
+
break
|
|
333
|
+
time.sleep(1)
|
|
334
|
+
except KeyboardInterrupt:
|
|
335
|
+
logger.info("Initiating graceful shutdown")
|
|
336
|
+
manager.graceful_shutdown()
|
|
337
|
+
finally:
|
|
338
|
+
datasource.disconnect()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == "__main__":
|
|
342
|
+
main()
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# wigidash_session.py
|
|
2
|
+
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import threading
|
|
8
|
+
|
|
9
|
+
from benchlab.wigidash.benchlab_fleet import BenchlabFleetSelect
|
|
10
|
+
from benchlab.wigidash.benchlab_overview import BenchlabOverview
|
|
11
|
+
from benchlab.wigidash.benchlab_graph import BenchlabGraph
|
|
12
|
+
from benchlab.wigidash.benchlab_ui import UITheme
|
|
13
|
+
from benchlab.wigidash.benchlab_utils import (
|
|
14
|
+
display_image, get_logger, KeepAliveManager
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from benchlab.wigidash.wigidash_device import WigidashDevice
|
|
18
|
+
from benchlab.wigidash.wigidash_widget import WidgetConfig
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = get_logger("WigidashSession")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BenchlabWigiSession:
|
|
25
|
+
"""
|
|
26
|
+
UI session for a single Wigidash device.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
SCREEN_WIDTH = UITheme.SCREEN_WIDTH
|
|
30
|
+
SCREEN_HEIGHT = UITheme.SCREEN_HEIGHT
|
|
31
|
+
SPLASH_TIME = 3.0
|
|
32
|
+
SCREEN_KEEPALIVE_INTERVAL = 5.0
|
|
33
|
+
|
|
34
|
+
def __init__(self, usb_device, telemetry_history=None, manager=None):
|
|
35
|
+
self.usb_device = usb_device
|
|
36
|
+
self.telemetry_history = telemetry_history
|
|
37
|
+
self.history = telemetry_history
|
|
38
|
+
self.telemetry_context = None
|
|
39
|
+
self.manager = manager
|
|
40
|
+
self.shutdown_event = threading.Event()
|
|
41
|
+
|
|
42
|
+
self.device_info = None
|
|
43
|
+
self.ser = None
|
|
44
|
+
self.uid = None
|
|
45
|
+
self.selected_port = None
|
|
46
|
+
self.sensor_data = {}
|
|
47
|
+
|
|
48
|
+
self.wigidash = None
|
|
49
|
+
self.keepalive_manager = None
|
|
50
|
+
self.app_running = True
|
|
51
|
+
self.cleanup_done = threading.Event()
|
|
52
|
+
|
|
53
|
+
self.fleet_page = None
|
|
54
|
+
self.overview_page = None
|
|
55
|
+
self.graph_page = None
|
|
56
|
+
self.graph_metrics = []
|
|
57
|
+
|
|
58
|
+
self.sensor_units = {
|
|
59
|
+
# --- High-level power ---
|
|
60
|
+
"SYS_Power": "W",
|
|
61
|
+
"CPU_Power": "W",
|
|
62
|
+
"GPU_Power": "W",
|
|
63
|
+
"MB_Power": "W",
|
|
64
|
+
|
|
65
|
+
# --- EPS Rails ---
|
|
66
|
+
"EPS1_Voltage": "V",
|
|
67
|
+
"EPS1_Current": "A",
|
|
68
|
+
"EPS1_Power": "W",
|
|
69
|
+
"EPS2_Voltage": "V",
|
|
70
|
+
"EPS2_Current": "A",
|
|
71
|
+
"EPS2_Power": "W",
|
|
72
|
+
|
|
73
|
+
# --- ATX Rails ---
|
|
74
|
+
"12V_Voltage": "V",
|
|
75
|
+
"12V_Current": "A",
|
|
76
|
+
"12V_Power": "W",
|
|
77
|
+
"5V_Voltage": "V",
|
|
78
|
+
"5V_Current": "A",
|
|
79
|
+
"5V_Power": "W",
|
|
80
|
+
"5VSB_Voltage": "V",
|
|
81
|
+
"5VSB_Current": "A",
|
|
82
|
+
"5VSB_Power": "W",
|
|
83
|
+
"3.3V_Voltage": "V",
|
|
84
|
+
"3.3V_Current": "A",
|
|
85
|
+
"3.3V_Power": "W",
|
|
86
|
+
|
|
87
|
+
# --- PCIe Rails ---
|
|
88
|
+
"PCIE8_1_Voltage": "V",
|
|
89
|
+
"PCIE8_1_Current": "A",
|
|
90
|
+
"PCIE8_1_Power": "W",
|
|
91
|
+
"PCIE8_2_Voltage": "V",
|
|
92
|
+
"PCIE8_2_Current": "A",
|
|
93
|
+
"PCIE8_2_Power": "W",
|
|
94
|
+
"PCIE8_3_Voltage": "V",
|
|
95
|
+
"PCIE8_3_Current": "A",
|
|
96
|
+
"PCIE8_3_Power": "W",
|
|
97
|
+
"HPWR1_Voltage": "V",
|
|
98
|
+
"HPWR1_Current": "A",
|
|
99
|
+
"HPWR1_Power": "W",
|
|
100
|
+
"HPWR2_Voltage": "V",
|
|
101
|
+
"HPWR2_Current": "A",
|
|
102
|
+
"HPWR2_Power": "W",
|
|
103
|
+
|
|
104
|
+
# --- VIN ---
|
|
105
|
+
# VIN_0 .. VIN_(SENSOR_VIN_NUM-1)
|
|
106
|
+
# safe default—adjust if SENSOR_VIN_NUM changes
|
|
107
|
+
**{f"VIN_{i}": "V" for i in range(16)},
|
|
108
|
+
|
|
109
|
+
# --- Other voltages ---
|
|
110
|
+
"Vdd": "V",
|
|
111
|
+
"Vref": "V",
|
|
112
|
+
|
|
113
|
+
# --- Temps ---
|
|
114
|
+
"Chip_Temp": "°C",
|
|
115
|
+
"Ambient_Temp": "°C",
|
|
116
|
+
"Humidity": "%",
|
|
117
|
+
|
|
118
|
+
# Temp sensors
|
|
119
|
+
**{f"Temp_Sensor_{i + 1}": "°C" for i in range(16)},
|
|
120
|
+
|
|
121
|
+
# --- Fans ---
|
|
122
|
+
**{f"Fan{i + 1}_Duty": "%" for i in range(16)},
|
|
123
|
+
**{f"Fan{i + 1}_RPM": "RPM" for i in range(16)},
|
|
124
|
+
# logical flag, unitless
|
|
125
|
+
**{f"Fan{i + 1}_Status": "" for i in range(16)},
|
|
126
|
+
|
|
127
|
+
"FanExtDuty": "%"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# ----------------- INIT ----------------- #
|
|
131
|
+
def connect_wigidash(self):
|
|
132
|
+
"""Initialize the Wigidash device and UI."""
|
|
133
|
+
logger.info(
|
|
134
|
+
f"Initializing Wigidash device {
|
|
135
|
+
self.usb_device.serial} ...")
|
|
136
|
+
try:
|
|
137
|
+
self.usb_device.connect()
|
|
138
|
+
self.wigidash = WigidashDevice(self.usb_device)
|
|
139
|
+
self.wigidash.init_device()
|
|
140
|
+
self.wigidash.clear_page(0)
|
|
141
|
+
self.wigidash.change_page(0)
|
|
142
|
+
|
|
143
|
+
widget = WidgetConfig.create_fullscreen()
|
|
144
|
+
self.wigidash.add_widget(widget)
|
|
145
|
+
|
|
146
|
+
self.keepalive_manager = KeepAliveManager(
|
|
147
|
+
self.wigidash, interval=self.SCREEN_KEEPALIVE_INTERVAL)
|
|
148
|
+
self.keepalive_manager.start()
|
|
149
|
+
|
|
150
|
+
logger.info(
|
|
151
|
+
f"Wigidash device {
|
|
152
|
+
self.usb_device.serial} initialized successfully.")
|
|
153
|
+
return True
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(
|
|
156
|
+
f"Failed to initialize Wigidash {
|
|
157
|
+
self.usb_device.serial}: {e}")
|
|
158
|
+
try:
|
|
159
|
+
self.usb_device.disconnect()
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
# ----------------- SPLASH ----------------- #
|
|
165
|
+
|
|
166
|
+
def show_splash(self):
|
|
167
|
+
"""Display splash screen with logo."""
|
|
168
|
+
img = Image.new(
|
|
169
|
+
'RGB', (self.SCREEN_WIDTH, self.SCREEN_HEIGHT), color=(
|
|
170
|
+
41, 39, 38))
|
|
171
|
+
try:
|
|
172
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
173
|
+
assets_path = os.path.join(base_dir, "assets", "benchlab.png")
|
|
174
|
+
logo = Image.open(assets_path).convert('RGBA')
|
|
175
|
+
logo.thumbnail((500, 500), Image.Resampling.LANCZOS)
|
|
176
|
+
x = (self.SCREEN_WIDTH - logo.width) // 2
|
|
177
|
+
y = (self.SCREEN_HEIGHT - logo.height) // 2
|
|
178
|
+
img.paste(logo, (x, y), logo)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning(f"Failed to load splash logo: {e}")
|
|
181
|
+
|
|
182
|
+
display_image(self.wigidash, img)
|
|
183
|
+
logger.info(f"Splash screen displayed on {self.usb_device.serial}.")
|
|
184
|
+
time.sleep(self.SPLASH_TIME)
|
|
185
|
+
|
|
186
|
+
# ----------------- HELPER ----------------- #
|
|
187
|
+
|
|
188
|
+
def create_overview_page(self):
|
|
189
|
+
ov = BenchlabOverview(
|
|
190
|
+
self.wigidash,
|
|
191
|
+
wigi=self,
|
|
192
|
+
telemetry_history=self.telemetry_history,
|
|
193
|
+
telemetry_context=self.telemetry_context,
|
|
194
|
+
manager=self.manager
|
|
195
|
+
)
|
|
196
|
+
ov.start()
|
|
197
|
+
return ov
|
|
198
|
+
|
|
199
|
+
# ----------------- MAIN LOOP ----------------- #
|
|
200
|
+
|
|
201
|
+
def run(self):
|
|
202
|
+
"""Run UI loop for Wigidash pages."""
|
|
203
|
+
logger.info(
|
|
204
|
+
f"Session {
|
|
205
|
+
self.usb_device.serial} started with manager={
|
|
206
|
+
self.manager}")
|
|
207
|
+
if not self.connect_wigidash():
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
self.show_splash()
|
|
211
|
+
self.next_page = "fleet"
|
|
212
|
+
|
|
213
|
+
while self.app_running and not self.shutdown_event.is_set():
|
|
214
|
+
try:
|
|
215
|
+
touch, _, _ = self.wigidash.get_click_info()
|
|
216
|
+
except Exception:
|
|
217
|
+
touch = None
|
|
218
|
+
|
|
219
|
+
if not self.app_running or self.shutdown_event.is_set():
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
# --- Fleet Page ---
|
|
223
|
+
if self.next_page == "fleet":
|
|
224
|
+
if self.fleet_page is None:
|
|
225
|
+
self.fleet_page = BenchlabFleetSelect(
|
|
226
|
+
self.wigidash,
|
|
227
|
+
fleet_manager=self.manager,
|
|
228
|
+
wigi=self
|
|
229
|
+
)
|
|
230
|
+
self.fleet_page.start()
|
|
231
|
+
logger.info(
|
|
232
|
+
f"Fleet page started for {
|
|
233
|
+
self.usb_device.serial}.")
|
|
234
|
+
|
|
235
|
+
self.fleet_page.check_touch(touch)
|
|
236
|
+
self.fleet_page.render_and_display()
|
|
237
|
+
|
|
238
|
+
if (not self.fleet_page.running
|
|
239
|
+
and self.fleet_page.selected_port):
|
|
240
|
+
# Copy the selected port from fleet page into the session
|
|
241
|
+
self.selected_port = self.fleet_page.selected_port
|
|
242
|
+
|
|
243
|
+
# Fetch the telemetry context for that port
|
|
244
|
+
if self.manager:
|
|
245
|
+
self.telemetry_context = (
|
|
246
|
+
self.manager.telemetry_contexts.get(
|
|
247
|
+
self.selected_port))
|
|
248
|
+
|
|
249
|
+
# Now create the overview page with a valid context
|
|
250
|
+
self.overview_page = BenchlabOverview(
|
|
251
|
+
self.wigidash,
|
|
252
|
+
wigi=self,
|
|
253
|
+
telemetry_history=self.telemetry_history,
|
|
254
|
+
telemetry_context=self.telemetry_context,
|
|
255
|
+
manager=self.manager or self
|
|
256
|
+
)
|
|
257
|
+
self.overview_page.start()
|
|
258
|
+
self.next_page = "overview"
|
|
259
|
+
self.fleet_page = None
|
|
260
|
+
logger.info(
|
|
261
|
+
f"Fleet selection done on {
|
|
262
|
+
self.usb_device.serial}, opening Overview.")
|
|
263
|
+
|
|
264
|
+
# --- Overview Page ---
|
|
265
|
+
elif self.next_page == "overview":
|
|
266
|
+
if self.overview_page:
|
|
267
|
+
self.overview_page.check_touch(touch)
|
|
268
|
+
self.overview_page.render_and_display()
|
|
269
|
+
|
|
270
|
+
if not self.overview_page.running:
|
|
271
|
+
requested_metrics = getattr(
|
|
272
|
+
self.overview_page, "requested_graph_metrics", [])
|
|
273
|
+
self.overview_page = None
|
|
274
|
+
|
|
275
|
+
if requested_metrics:
|
|
276
|
+
self.graph_metrics = requested_metrics
|
|
277
|
+
self.next_page = "graph"
|
|
278
|
+
logger.info(
|
|
279
|
+
f"Overview requested graph metrics: {
|
|
280
|
+
self.graph_metrics}")
|
|
281
|
+
else:
|
|
282
|
+
self.next_page = "fleet"
|
|
283
|
+
|
|
284
|
+
# --- Graph Page ---
|
|
285
|
+
elif self.next_page == "graph":
|
|
286
|
+
if self.graph_page is None:
|
|
287
|
+
self.graph_page = BenchlabGraph(
|
|
288
|
+
self.wigidash,
|
|
289
|
+
self,
|
|
290
|
+
metrics=self.graph_metrics,
|
|
291
|
+
telemetry_history=self.telemetry_history,
|
|
292
|
+
telemetry_context=self.telemetry_context,
|
|
293
|
+
manager=self.manager or self
|
|
294
|
+
)
|
|
295
|
+
self.graph_page.start()
|
|
296
|
+
logger.info(
|
|
297
|
+
f"Graph page started for {
|
|
298
|
+
self.usb_device.serial} with metrics {
|
|
299
|
+
self.graph_metrics}.")
|
|
300
|
+
|
|
301
|
+
self.graph_page.check_touch(touch)
|
|
302
|
+
self.graph_page.render_and_display()
|
|
303
|
+
|
|
304
|
+
if not self.graph_page.running:
|
|
305
|
+
self.graph_page = None
|
|
306
|
+
self.next_page = "overview"
|
|
307
|
+
logger.info(
|
|
308
|
+
f"Graph page exited for {
|
|
309
|
+
self.usb_device.serial}, returning to Overview.")
|
|
310
|
+
|
|
311
|
+
time.sleep(0.05)
|
|
312
|
+
|
|
313
|
+
# ----------------- STOP / CLEANUP ----------------- #
|
|
314
|
+
|
|
315
|
+
def cleanup(self):
|
|
316
|
+
"""Stop all pages and keepalive manager."""
|
|
317
|
+
if self.keepalive_manager:
|
|
318
|
+
self.keepalive_manager.stop()
|
|
319
|
+
if self.wigidash:
|
|
320
|
+
try:
|
|
321
|
+
self.wigidash.clear_page(0)
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
if self.usb_device:
|
|
325
|
+
try:
|
|
326
|
+
self.usb_device.disconnect()
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.warning(f"Error disconnecting USB device: {e}")
|
|
329
|
+
logger.info(f"Wigidash session {self.usb_device.serial} cleaned up.")
|
|
330
|
+
|
|
331
|
+
def shutdown_session(self):
|
|
332
|
+
"""Show synchronized shutdown splash, then stop pages/keepalive."""
|
|
333
|
+
logger.info(f"Stopping Wigidash session {self.usb_device.serial}")
|
|
334
|
+
self.app_running = False
|
|
335
|
+
self.shutdown_event.set()
|
|
336
|
+
|
|
337
|
+
# Wait at barrier if it exists
|
|
338
|
+
barrier = getattr(self.manager, "shutdown_barrier", None)
|
|
339
|
+
if barrier:
|
|
340
|
+
try:
|
|
341
|
+
barrier.wait()
|
|
342
|
+
except threading.BrokenBarrierError:
|
|
343
|
+
logger.warning(
|
|
344
|
+
f"Shutdown barrier broken for {
|
|
345
|
+
self.usb_device.serial}")
|
|
346
|
+
|
|
347
|
+
# Show splash
|
|
348
|
+
if self.wigidash:
|
|
349
|
+
try:
|
|
350
|
+
self.show_splash()
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.warning(f"Failed to show shutdown splash: {e}")
|
|
353
|
+
|
|
354
|
+
self.cleanup()
|
|
355
|
+
self.cleanup_done.set()
|