benchlab-pytools 3.0.0__py3-none-any.whl

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