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,426 @@
1
+ # benchlab_overview.py
2
+
3
+ import time
4
+
5
+ from PIL import Image, ImageDraw
6
+
7
+ from benchlab.wigidash.benchlab_ui import (
8
+ load_fonts, draw_header, draw_footer, bind_button, load_logo, UIButton,
9
+ UITheme
10
+ )
11
+ from benchlab.wigidash.benchlab_utils import (
12
+ display_image, KeepAliveManager, get_logger
13
+ )
14
+
15
+ logger = get_logger("BenchlabOverview")
16
+
17
+
18
+ class BenchlabOverview:
19
+ SCREEN_WIDTH = UITheme.SCREEN_WIDTH
20
+ SCREEN_HEIGHT = UITheme.SCREEN_HEIGHT
21
+ HEADER_HEIGHT = UITheme.HEADER_HEIGHT
22
+ FOOTER_HEIGHT = UITheme.FOOTER_HEIGHT
23
+ PADDING = UITheme.PADDING
24
+ COLOR_SECTION = UITheme.COLOR_SECTION
25
+ COLOR_TEXT = UITheme.COLOR_TEXT
26
+
27
+ def __init__(
28
+ self,
29
+ wigidash,
30
+ wigi=None,
31
+ telemetry_history=None,
32
+ telemetry_context=None,
33
+ manager=None):
34
+ if not wigidash or not wigi:
35
+ raise ValueError("wigidash and wigi cannot be None")
36
+
37
+ # Wigi device & session
38
+ self.wigidash = wigidash
39
+ self.wigi = wigi
40
+ self.telemetry_history = telemetry_history
41
+ self.telemetry_context = telemetry_context
42
+ self.manager = manager
43
+ self.keepalive = KeepAliveManager(self.wigidash)
44
+ self.running = False
45
+
46
+ # Fonts and logo from central UI
47
+ self.ui_fonts = load_fonts()
48
+ self.ui_logo = load_logo()
49
+
50
+ # Device info & telemetry
51
+ if self.telemetry_history:
52
+ # Pull the latest snapshot from the shared telemetry
53
+ self.sensor_data = self.telemetry_history.latest_snapshot()
54
+ else:
55
+ self.sensor_data = {}
56
+ self.device_info = getattr(wigi, "device_info", {})
57
+ self.uid = getattr(wigi, "uid", "N/A")
58
+ self.ser = getattr(wigi, "ser", None)
59
+ self.history = getattr(wigi, "history", None)
60
+
61
+ # Touch controls
62
+ self.footer_btn_config = None
63
+ self.requested_graph_metrics = None
64
+ self.x1 = self.x2 = self.x3 = 0
65
+ self.col1_width = self.col2_width = self.col3_width = 0
66
+ self.last_touch_time = 0
67
+
68
+ # -------------------------------
69
+ # Page Handling
70
+ # -------------------------------
71
+
72
+ def start(self):
73
+ logger.info("Starting Overview page")
74
+ self.running = True
75
+
76
+ # -------------------------------
77
+ # Touch Handling
78
+ # -------------------------------
79
+
80
+ def check_touch(self, touch):
81
+ if not getattr(self, "running", False):
82
+ return
83
+
84
+ now = int(time.monotonic() * 1000)
85
+ if now - self.last_touch_time < 150:
86
+ return
87
+
88
+ if touch is None or getattr(touch, "Type", 0) == 0:
89
+ return
90
+
91
+ self.last_touch_time = now
92
+
93
+ x, y = getattr(touch, "X", 0), getattr(touch, "Y", 0)
94
+
95
+ # -------------------------------------------------------
96
+ # 1) CARD TOUCH HANDLING (Graph Page)
97
+ # -------------------------------------------------------
98
+ padding = self.PADDING
99
+ header_h = self.HEADER_HEIGHT
100
+
101
+ # --- Top cards ---
102
+ top_y = header_h + padding
103
+ top_h = 160
104
+
105
+ cards = [
106
+ (self.x1, top_y, self.x1 + self.col1_width, top_y + top_h,
107
+ ["SYS_Power", "CPU_Power", "GPU_Power", "MB_Power"]),
108
+ (self.x2, top_y, self.x2 + self.col2_width, top_y + top_h,
109
+ ["Chip_Temp", "Ambient_Temp", "Humidity",
110
+ "Temp_Sensor_1", "Temp_Sensor_2", "Temp_Sensor_3",
111
+ "Temp_Sensor_4"]),
112
+ (self.x3, top_y, self.x3 + self.col3_width, top_y + top_h,
113
+ [k for k in self.sensor_data.keys() if k.startswith("Fan")]),
114
+ ]
115
+
116
+ # --- Bottom cards ---
117
+ bottom_y = top_y + top_h + padding
118
+ bottom_h = 305
119
+ col_width = (self.SCREEN_WIDTH - 5 * padding) // 4
120
+
121
+ power_x = padding
122
+ current_x = padding + col_width + padding
123
+ voltage_x = padding + 2 * (col_width + padding)
124
+
125
+ all_rails = [
126
+ 'EPS1',
127
+ 'EPS2',
128
+ '12V',
129
+ '5V',
130
+ '5VSB',
131
+ '3.3V',
132
+ 'PCIE8_1',
133
+ 'PCIE8_2',
134
+ 'PCIE8_3',
135
+ 'HPWR1',
136
+ 'HPWR2']
137
+
138
+ cards += [(power_x,
139
+ bottom_y,
140
+ power_x + col_width,
141
+ bottom_y + bottom_h,
142
+ [f"{r}_Power" for r in all_rails
143
+ if f"{r}_Power" in self.sensor_data]),
144
+ (current_x,
145
+ bottom_y,
146
+ current_x + col_width,
147
+ bottom_y + bottom_h,
148
+ [f"{r}_Current" for r in all_rails
149
+ if f"{r}_Current" in self.sensor_data]),
150
+ (voltage_x,
151
+ bottom_y,
152
+ voltage_x + col_width,
153
+ bottom_y + bottom_h,
154
+ [f"{r}_Voltage" for r in all_rails
155
+ if f"{r}_Voltage" in self.sensor_data]),
156
+ ]
157
+
158
+ vins_x = padding + 3 * (col_width + padding)
159
+
160
+ vin_keys = [
161
+ k for k in self.sensor_data.keys() if k.startswith("VIN_")
162
+ ]
163
+ cards += [
164
+ (vins_x, bottom_y, vins_x + col_width, bottom_y + bottom_h,
165
+ vin_keys + ["Vdd", "Vref"])
166
+ ]
167
+
168
+ # --- Detect card hit ---
169
+ for (x0, y0, x1, y1, metrics) in cards:
170
+ if x0 <= x <= x1 and y0 <= y <= y1:
171
+ logger.info(f"Card pressed, opening graph for: {metrics}")
172
+ self.running = False
173
+ self.requested_graph_metrics = metrics
174
+ return
175
+
176
+ # -------------------------------------------------------
177
+ # 2) FOOTER BUTTON HANDLING
178
+ # -------------------------------------------------------
179
+ if not self.footer_btn_config:
180
+ return
181
+
182
+ for btn in self.footer_btn_config:
183
+ if btn["x0"] <= x <= btn["x1"] and btn["y0"] <= y <= btn["y1"]:
184
+ logger.info(f"Footer button pressed: {btn.get('text', '')}")
185
+ if btn.get("callback"):
186
+ btn["callback"]()
187
+
188
+ return
189
+
190
+ def return_to_fleet(self):
191
+ """Signal launcher to switch back to fleet."""
192
+ logger.info("Switching back to fleetSelect.")
193
+ self.running = False
194
+
195
+ # -------------------------------
196
+ # Rendering
197
+ # -------------------------------
198
+
199
+ def render_and_display(self):
200
+ """Main-thread-safe rendering call"""
201
+
202
+ if self.telemetry_history:
203
+ # Always use the latest sensor data from the shared telemetry
204
+ # history
205
+ self.sensor_data = self.telemetry_history.latest_snapshot()
206
+ else:
207
+ self.sensor_data = {}
208
+
209
+ self.device_info = getattr(self.wigi, "device_info", {})
210
+ self.uid = getattr(self.wigi, "uid", "N/A")
211
+ img = self.render_overview()
212
+ display_image(self.wigidash, img)
213
+
214
+ def render_overview(self):
215
+ img = Image.new(
216
+ 'RGB', (self.SCREEN_WIDTH, self.SCREEN_HEIGHT), color=(
217
+ 0, 0, 0))
218
+ draw = ImageDraw.Draw(img)
219
+
220
+ padding = 8
221
+ header_height = 60
222
+ title_spacing = 2
223
+ line_height = 18
224
+ data = self.sensor_data or {}
225
+
226
+ self.padding = padding
227
+ self.header_height = header_height
228
+
229
+ total_top_width = self.SCREEN_WIDTH - 4 * padding
230
+ self.col1_width = self.col2_width = int(total_top_width * 0.25) - 3
231
+ self.col3_width = (
232
+ total_top_width - self.col1_width - self.col2_width - 1)
233
+
234
+ self.x1 = padding
235
+ self.x2 = self.x1 + self.col1_width + padding
236
+ self.x3 = self.x2 + self.col2_width + padding
237
+
238
+ # ---- Helper: draw rounded card ----
239
+ def rounded_card(x, y, w, h, title):
240
+ draw.rounded_rectangle(
241
+ [x, y, x + w, y + h], radius=12,
242
+ outline=(200, 200, 200), width=1, fill=(15, 15, 15)
243
+ )
244
+ draw.text((x + 8, y + 8),
245
+ title,
246
+ fill=self.COLOR_SECTION,
247
+ font=self.ui_fonts["title"])
248
+
249
+ # ---- Header ----
250
+ draw_header(
251
+ draw,
252
+ img,
253
+ self.ui_fonts,
254
+ "BENCHLAB TELEMETRY",
255
+ self.ui_logo)
256
+
257
+ # ---- Top cards ----
258
+ top_y = header_height + padding
259
+ top_height = 160
260
+
261
+ x1, x2, x3 = self.x1, self.x2, self.x3
262
+ y1 = y2 = y3 = top_y
263
+
264
+ # Summary
265
+ rounded_card(self.x1, top_y, self.col1_width, top_height, "SUMMARY")
266
+ ly = y1 + 28 + title_spacing
267
+ for key in ["SYS_Power", "CPU_Power", "GPU_Power", "MB_Power"]:
268
+ draw.text((x1 + 10, ly),
269
+ f"{key.split('_')[0]}: {data.get(key, 0):.1f} W",
270
+ fill=self.COLOR_TEXT,
271
+ font=self.ui_fonts["text"])
272
+ ly += line_height
273
+
274
+ # Temperatures
275
+ rounded_card(
276
+ self.x2,
277
+ top_y,
278
+ self.col2_width,
279
+ top_height,
280
+ "TEMPERATURES")
281
+ ly = y2 + 28 + title_spacing
282
+ for key in ["Chip_Temp", "Ambient_Temp", "Humidity"]:
283
+ val = data.get(key, 0)
284
+ if key == "Humidity":
285
+ draw.text((x2 + 10, ly),
286
+ f"{key}: {val:.1f}%",
287
+ fill=self.COLOR_TEXT,
288
+ font=self.ui_fonts["text"])
289
+ else:
290
+ draw.text((x2 + 10, ly),
291
+ f"{key}: {val}°C",
292
+ fill=self.COLOR_TEXT,
293
+ font=self.ui_fonts["text"])
294
+ ly += line_height
295
+ for i in range(1, 5):
296
+ draw.text((x2 + 10, ly),
297
+ f"S{i}: {data.get(f'Temp_Sensor_{i}', 'N/A')}",
298
+ fill=self.COLOR_TEXT,
299
+ font=self.ui_fonts["text"])
300
+ ly += line_height
301
+
302
+ # Fans
303
+ rounded_card(self.x3, top_y, self.col3_width, top_height, "FANS")
304
+ fans = [
305
+ "Fan1",
306
+ "Fan2",
307
+ "Fan3",
308
+ "Fan4",
309
+ "Fan5",
310
+ "Fan6",
311
+ "Fan7",
312
+ "Fan8",
313
+ "Fan9",
314
+ "Ext Fan Duty"]
315
+ half = (len(fans) + 1) // 2
316
+ col_spacing = self.col3_width // 2
317
+ for idx, name in enumerate(fans):
318
+ ly = y3 + 28 + line_height * (idx % half) + title_spacing
319
+ x_text = x3 + 10 if idx < half else x3 + 10 + col_spacing
320
+ if name.startswith("Fan") and name != "Ext Fan Duty":
321
+ duty = data.get(f"{name}_Duty", 0)
322
+ rpm = data.get(f"{name}_RPM", 0)
323
+ status = "ON" if data.get(f"{name}_Status", 0) else "OFF"
324
+ draw.text((x_text, ly),
325
+ f"{name}: {duty}% | {rpm} RPM | {status}",
326
+ fill=self.COLOR_TEXT,
327
+ font=self.ui_fonts["text"])
328
+ else:
329
+ draw.text((x_text, ly),
330
+ f"{name}: {data.get('FanExtDuty', 0)}",
331
+ fill=self.COLOR_TEXT,
332
+ font=self.ui_fonts["text"])
333
+
334
+ # ---- Bottom cards ----
335
+ bottom_y = top_y + top_height + padding
336
+ bottom_height = 305
337
+ col_width = (self.SCREEN_WIDTH - 5 * padding) // 4
338
+
339
+ all_rails = [
340
+ 'EPS1',
341
+ 'EPS2',
342
+ '12V',
343
+ '5V',
344
+ '5VSB',
345
+ '3.3V',
346
+ 'PCIE8_1',
347
+ 'PCIE8_2',
348
+ 'PCIE8_3',
349
+ 'HPWR1',
350
+ 'HPWR2']
351
+ sections = [("POWER", "Power"), ("CURRENT", "Current"),
352
+ ("VOLTAGE", "Voltage")]
353
+
354
+ for i, (title, key_suffix) in enumerate(sections):
355
+ x = padding + i * (col_width + padding)
356
+ rounded_card(x, bottom_y, col_width, bottom_height, title)
357
+ ly = bottom_y + 32
358
+
359
+ # dynamically get rails for this section
360
+ for r in all_rails:
361
+ # ensure we build the full key
362
+ v = data.get(f"{r}_{key_suffix}", 0.0)
363
+ unit = "W" if key_suffix == "Power" else (
364
+ "A" if key_suffix == "Current" else "V")
365
+ draw.text((x + 12, ly),
366
+ f"{r}: {v:.2f} {unit}",
367
+ fill=self.COLOR_TEXT,
368
+ font=self.ui_fonts["text"])
369
+ ly += line_height
370
+
371
+ # VINs
372
+ x4 = padding + 3 * (col_width + padding)
373
+ rounded_card(x4, bottom_y, col_width, bottom_height, "VOLTAGE INPUTS")
374
+ vin_keys = [f"VIN_{i}" for i in range(13)]
375
+ ly = bottom_y + 28 + title_spacing
376
+ for k in vin_keys:
377
+ val = data.get(k, 0.0)
378
+ draw.text((x4 + 10, ly),
379
+ f"{k}: {val:.3f} V",
380
+ fill=self.COLOR_TEXT,
381
+ font=self.ui_fonts["text"])
382
+ ly += line_height
383
+
384
+ # Always draw Vdd and Vref
385
+ draw.text((x4 + 10, ly),
386
+ f"Vdd: {data.get('Vdd', 0.0):.3f} V",
387
+ fill=self.COLOR_TEXT,
388
+ font=self.ui_fonts["text"])
389
+ draw.text((x4 + 10, ly + line_height),
390
+ f"Vref: {data.get('Vref', 0.0):.3f} V",
391
+ fill=self.COLOR_TEXT,
392
+ font=self.ui_fonts["text"])
393
+
394
+ # ---- Footer ----
395
+ btns = [
396
+ bind_button(UIButton.SHUTDOWN, self.manager.graceful_shutdown),
397
+ bind_button(UIButton.SELECT_DEVICE, self.return_to_fleet),
398
+ ]
399
+
400
+ # Safely determine context for port/UID
401
+ ctx = getattr(self, "telemetry_context", None)
402
+
403
+ if ctx:
404
+ port = getattr(ctx, "port", "N/A")
405
+ uid = getattr(ctx, "uid", "N/A")
406
+ device_info = getattr(ctx, "device_info", {})
407
+ vid = device_info.get("VendorId", 0)
408
+ pid = device_info.get("ProductId", 0)
409
+ fw = device_info.get("FwVersion", 0)
410
+
411
+ info_parts = [
412
+ f"Port: {port}",
413
+ f"Vendor ID: 0x{vid:02X}",
414
+ f"Product ID: 0x{pid:02X}",
415
+ f"FW: 0x{fw:02X}",
416
+ f"UID: {uid or 'N/A'}"
417
+ ]
418
+ info_line = " | ".join(info_parts)
419
+ else:
420
+ info_line = "Port: N/A | UID: N/A"
421
+
422
+ # Draw footer using correct fonts
423
+ self.footer_btn_config = draw_footer(
424
+ draw, self.ui_fonts, info_line, btns)
425
+
426
+ return img
@@ -0,0 +1,105 @@
1
+ # benchlab_telemetry.py
2
+
3
+ from collections import defaultdict, deque
4
+
5
+ import time
6
+ import threading
7
+
8
+ from benchlab.wigidash.benchlab_utils import get_logger
9
+
10
+ logger = get_logger("BenchlabTelemetry")
11
+
12
+
13
+ class TelemetryHistory:
14
+ """
15
+ Keep historical data for all telemetry points dynamically, with timestamps.
16
+ """
17
+
18
+ def __init__(self, max_samples=1000):
19
+ self.max_samples = max_samples
20
+ self.data = defaultdict(lambda: deque(maxlen=self.max_samples))
21
+ self.lock = threading.Lock()
22
+
23
+ def add_sample(self, sample: dict):
24
+ timestamped = {"timestamp": time.time(), **sample}
25
+ with self.lock:
26
+ for key, value in timestamped.items():
27
+ self.data[key].append(value)
28
+
29
+ def get_history(self, key):
30
+ with self.lock:
31
+ return list(self.data.get(key, []))
32
+
33
+ def latest_snapshot(self):
34
+ with self.lock:
35
+ return {k: v[-1] if v else None for k, v in self.data.items()}
36
+
37
+
38
+ class TelemetryState:
39
+ """
40
+ Mutable telemetry state owned by the manager but consumed by UI.
41
+ """
42
+ __slots__ = ("device_info", "uid", "sensor_data")
43
+
44
+ def __init__(self, uid=None):
45
+ self.device_info = None
46
+ self.uid = uid
47
+ self.sensor_data = None
48
+
49
+
50
+ class TelemetryContext:
51
+ def __init__(self, port, ser, device_info, uid, history):
52
+ self.port = port
53
+ self.ser = ser
54
+ self.device_info = device_info
55
+ self.uid = uid
56
+ self.history = history
57
+ self.sessions = [] # attached WigiSessions
58
+
59
+
60
+ def telemetry_step(app, device_info=None, sensor_struct=None):
61
+ """
62
+ Process one telemetry iteration.
63
+ - device_info: optional dict already read from serial
64
+ - sensor_struct: optional raw sensor data (from serial) or already
65
+ translated dict (from DataSource)
66
+ """
67
+ try:
68
+ # Use device info if provided
69
+ if device_info and app.device_info is None:
70
+ app.device_info = device_info
71
+ if 'UID' in device_info and app.uid is None:
72
+ app.uid = device_info['UID']
73
+
74
+ if sensor_struct is None:
75
+ logger.warning("No sensor data provided to telemetry_step")
76
+ return
77
+
78
+ # sensor_struct is always a pre-translated dict from DataSourceManager
79
+ if not isinstance(sensor_struct, dict):
80
+ logger.warning(
81
+ "Unexpected sensor_struct type: %s",
82
+ type(sensor_struct))
83
+ return
84
+ data = dict(sensor_struct)
85
+
86
+ # --- Cleanup & normalization ---
87
+ # Copy Fans/Vin too so the in-place mutations below don't leak back
88
+ # into the caller's original lists/dicts.
89
+ data["Fans"] = [dict(fan) for fan in data.get("Fans", [])]
90
+ data["Vin"] = list(data.get("Vin", []))
91
+ for fan in data["Fans"]:
92
+ fan.setdefault("RPM", 0)
93
+ fan.setdefault("Duty", 0)
94
+ fan.setdefault("Status", 0)
95
+ for i, vin in enumerate(data["Vin"]):
96
+ if vin is None:
97
+ data["Vin"][i] = 0.0
98
+
99
+ # --- Store telemetry ---
100
+ app.sensor_data = data
101
+ app.history.add_sample(data)
102
+ logger.debug(f"Telemetry step recorded with {len(data)} keys")
103
+
104
+ except Exception as e:
105
+ logger.exception(f"Telemetry step error: {e}")