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,418 @@
1
+ """
2
+ Refactored TUI Main Entry Point
3
+ Clean main entry point using the refactored architecture with separated
4
+ concerns:
5
+ - DataSourceManager handles all data source operations
6
+ - ChannelStats manages statistics
7
+ - TUICore handles all UI rendering
8
+ - Config centralizes configuration
9
+ This replaces the original monolithic tui_main.py with a much cleaner
10
+ implementation.
11
+ """
12
+ import curses
13
+ import logging
14
+ import sys
15
+ import time
16
+ from typing import List, Dict, Any
17
+
18
+ from benchlab.tui.__init__ import __version__
19
+ from benchlab.core.datasource_manager import DataSourceManager
20
+ from benchlab.core.statistics import ChannelStats, create_stats_callback
21
+ from .tui_core import TUICore
22
+
23
+ logger = logging.getLogger("benchlab.tui.main")
24
+
25
+
26
+ class _DevNullWriter:
27
+ """A file-like object that discards all output."""
28
+
29
+ def write(self, s):
30
+ pass
31
+
32
+ def flush(self):
33
+ pass
34
+
35
+ def isatty(self):
36
+ return False
37
+
38
+
39
+ class _TUIStdoutRedirect:
40
+ """Redirect stdout/stderr to prevent interference with curses display.
41
+
42
+ The pycore library writes INFO messages directly to stdout which messes
43
+ up the curses layout. This class redirects stdout/stderr to a file or
44
+ /dev/null
45
+ during TUI operation.
46
+ """
47
+
48
+ def __init__(self, log_file: str = None):
49
+ self.log_file = log_file
50
+ self._original_stdout = None
51
+ self._original_stderr = None
52
+ self._file_handle = None
53
+
54
+ def __enter__(self):
55
+ self._original_stdout = sys.stdout
56
+ self._original_stderr = sys.stderr
57
+
58
+ if self.log_file:
59
+ self._file_handle = open(self.log_file, 'a')
60
+ sys.stdout = self._file_handle
61
+ sys.stderr = self._file_handle
62
+ else:
63
+ # Discard all output
64
+ sys.stdout = _DevNullWriter()
65
+ sys.stderr = _DevNullWriter()
66
+
67
+ return self
68
+
69
+ def __exit__(self, exc_type, exc_val, exc_tb):
70
+ if self._file_handle:
71
+ self._file_handle.flush()
72
+ self._file_handle.close()
73
+
74
+ sys.stdout = self._original_stdout
75
+ sys.stderr = self._original_stderr
76
+ return False
77
+
78
+
79
+ def get_default_datasource(args) -> str:
80
+ """Get default data source based on command-line arguments."""
81
+ if hasattr(args, 'source') and args.source:
82
+ return args.source
83
+ return 'direct'
84
+
85
+
86
+ def convert_fleet_format(
87
+ devices_dict: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
88
+ """Convert DataSourceManager device dict to fleet list format."""
89
+ fleet = []
90
+ for uid, device_info in devices_dict.items():
91
+ fleet.append({
92
+ 'uid': uid,
93
+ 'port': device_info.get('port', 'unknown'),
94
+ 'firmware': device_info.get(
95
+ 'firmware', device_info.get('FwVersion', 0)),
96
+ 'variant': device_info.get('variant'),
97
+ 'ProductId': device_info.get('ProductId'),
98
+ })
99
+ return sorted(fleet, key=lambda d: d["port"])
100
+
101
+
102
+ class TUIApplication:
103
+ """
104
+ Main TUI application class that coordinates between DataSourceManager,
105
+ statistics tracking, and UI rendering.
106
+ """
107
+
108
+ def __init__(self, args):
109
+ self.args = args
110
+ self.source_type = get_default_datasource(args)
111
+
112
+ self.stats = ChannelStats()
113
+ stats_callback = create_stats_callback(self.stats)
114
+
115
+ datasource_kwargs = {
116
+ 'poll_interval': getattr(args, 'interval', 1.0),
117
+ }
118
+
119
+ if self.source_type == 'fastapi':
120
+ if hasattr(args, 'api_port'):
121
+ datasource_kwargs['base_url'] = f"http://127.0.0.1:{
122
+ args.api_port}"
123
+ else:
124
+ datasource_kwargs['base_url'] = "http://127.0.0.1:8000"
125
+ datasource_kwargs['timeout'] = 5.0
126
+
127
+ elif self.source_type == 'fastapi_custom':
128
+ # Use the custom URL from args.api_url (set from BENCHLAB_API_URL
129
+ # env var)
130
+ datasource_kwargs['base_url'] = getattr(
131
+ args, 'api_url', 'http://127.0.0.1:8000')
132
+ datasource_kwargs['timeout'] = 5.0
133
+
134
+ elif self.source_type == 'mqtt':
135
+ datasource_kwargs['broker'] = getattr(
136
+ args, 'mqtt_broker', 'localhost')
137
+ datasource_kwargs['port'] = getattr(args, 'mqtt_port', 1883)
138
+ datasource_kwargs['timeout'] = 5.0
139
+
140
+ elif self.source_type == 'named_pipe':
141
+ datasource_kwargs['timeout'] = 5.0
142
+
143
+ elif self.source_type == 'service_http':
144
+ service_url = getattr(
145
+ args, 'service_url', None) or 'http://localhost:8585'
146
+ datasource_kwargs['base_url'] = service_url
147
+ datasource_kwargs['timeout'] = 5.0
148
+
149
+ self.datasource_manager = DataSourceManager(
150
+ source_type=self.source_type,
151
+ stats_callback=stats_callback,
152
+ **datasource_kwargs
153
+ )
154
+
155
+ self.tui_core = None
156
+ self.fleet_cache = []
157
+ self.last_fleet_refresh = 0.0
158
+
159
+ def run(self, stdscr):
160
+ self.tui_core = TUICore(stdscr, __version__)
161
+
162
+ # Force full screen redraw to overwrite any stray console output
163
+ stdscr.clearok(True)
164
+
165
+ # Auto-connect for all non-direct sources
166
+ if self.source_type != 'direct':
167
+ self._connect_datasource()
168
+
169
+ self._refresh_fleet_cache()
170
+
171
+ while True:
172
+ snapshot = self.datasource_manager.snapshot()
173
+
174
+ rendered = self.tui_core.render(
175
+ snapshot=snapshot,
176
+ stats=self.stats,
177
+ fleet_devices=self.fleet_cache,
178
+ refresh_interval=getattr(self.args, 'interval', 1.0)
179
+ )
180
+
181
+ if not rendered:
182
+ time.sleep(0.2)
183
+ continue
184
+
185
+ try:
186
+ key = stdscr.getkey()
187
+ action = self.tui_core.handle_key(key)
188
+
189
+ if not self._handle_action(action):
190
+ break
191
+
192
+ except curses.error:
193
+ pass
194
+
195
+ def _handle_action(self, action: Dict[str, Any]) -> bool:
196
+ action_type = action.get('type', 'none')
197
+
198
+ if action_type == 'quit':
199
+ return False
200
+
201
+ elif action_type == 'reset_stats':
202
+ uid = self.datasource_manager.get_selected_uid()
203
+ if uid:
204
+ self.stats.reset(uid)
205
+ self.tui_core.set_status("Stats reset.")
206
+ else:
207
+ self.tui_core.set_status("No device selected.")
208
+
209
+ elif action_type == 'rescan_fleet':
210
+ self._refresh_fleet_cache()
211
+ count = len(self.fleet_cache)
212
+ source_label = (
213
+ self.source_type.upper()
214
+ if self.source_type != 'direct' else 'serial')
215
+ self.tui_core.set_status(
216
+ f"Fleet rescanned ({source_label}) — {count} device(s) found.")
217
+
218
+ elif action_type == 'fleet_nav':
219
+ direction = action.get('direction')
220
+ self.tui_core.update_fleet_index(len(self.fleet_cache), direction)
221
+
222
+ elif action_type == 'fleet_select':
223
+ index = action.get('index', 0)
224
+ if 0 <= index < len(self.fleet_cache):
225
+ selected_device = self.fleet_cache[index]
226
+ self._connect_to_device(selected_device)
227
+
228
+ return True
229
+
230
+ def _connect_datasource(self):
231
+ """Connect via non-direct datasource
232
+ (FastAPI/MQTT/named_pipe/service_http)."""
233
+ try:
234
+ if self.datasource_manager.connect():
235
+ uid = self.datasource_manager.get_selected_uid()
236
+ self.tui_core.set_status(
237
+ f"Connected via {
238
+ self.source_type.upper()}")
239
+ logger.info(
240
+ f"Connected to {
241
+ self.source_type} datasource, selected: {uid}")
242
+ else:
243
+ snapshot = self.datasource_manager.snapshot()
244
+ error_msg = snapshot.get(
245
+ 'last_error', f"Failed to connect to {
246
+ self.source_type}")
247
+ self.tui_core.set_status(
248
+ f"Connection failed: {error_msg}", 5.0)
249
+ logger.error(
250
+ f"Failed to connect to {
251
+ self.source_type}: {error_msg}")
252
+ except Exception as e:
253
+ self.tui_core.set_status(f"Connection error: {str(e)}", 5.0)
254
+ logger.error(
255
+ f"Exception during {
256
+ self.source_type} connection: {e}")
257
+
258
+ def _connect_to_device(self, device: Dict[str, Any]):
259
+ port = device.get('port')
260
+ uid = device.get('uid')
261
+
262
+ if uid == "BUSY":
263
+ self.tui_core.set_status(
264
+ "Device is busy (already connected elsewhere)", 3.0)
265
+ return
266
+
267
+ try:
268
+ if self.source_type == 'direct':
269
+ if self.datasource_manager.connect(port=port):
270
+ self.tui_core.set_status(f"Connected to {port}")
271
+ logger.info(f"Connected to device on {port}")
272
+ else:
273
+ snapshot = self.datasource_manager.snapshot()
274
+ error_msg = snapshot.get('last_error', 'Connection failed')
275
+ self.tui_core.set_status(
276
+ f"Connection failed: {error_msg}", 4.0)
277
+ logger.error(f"Failed to connect to {port}: {error_msg}")
278
+ else:
279
+ # Network/pipe datasource — select different device
280
+ if self.datasource_manager.select_device(uid):
281
+ self.tui_core.set_status(f"Selected device {uid}")
282
+ logger.info(
283
+ f"Selected device {uid} via {
284
+ self.source_type}")
285
+ else:
286
+ self.tui_core.set_status("Failed to select device", 3.0)
287
+ logger.warning(f"Failed to select device {uid}")
288
+
289
+ except Exception as e:
290
+ self.tui_core.set_status(f"Connection error: {str(e)}", 4.0)
291
+ logger.error(f"Exception during device connection: {e}")
292
+
293
+ def _refresh_fleet_cache(self):
294
+ try:
295
+ current_time = time.time()
296
+ if current_time - self.last_fleet_refresh < 1.0:
297
+ return
298
+ self.last_fleet_refresh = current_time
299
+
300
+ if (self.source_type != 'direct'
301
+ and self.datasource_manager.is_connected()):
302
+ devices_dict = self.datasource_manager.list_devices()
303
+ self.fleet_cache = convert_fleet_format(devices_dict)
304
+ else:
305
+ self.fleet_cache = self._scan_local_fleet()
306
+
307
+ if self.tui_core and self.tui_core.fleet_index >= len(
308
+ self.fleet_cache):
309
+ self.tui_core.fleet_index = 0
310
+
311
+ logger.debug(
312
+ f"Fleet cache refreshed: {len(self.fleet_cache)} devices")
313
+
314
+ except Exception as e:
315
+ logger.warning(f"Error refreshing fleet cache: {e}")
316
+ self.fleet_cache = []
317
+
318
+ def _scan_local_fleet(self) -> List[Dict[str, Any]]:
319
+ fleet = []
320
+ try:
321
+ devices = self.datasource_manager.discover_devices()
322
+ for device in devices:
323
+ fleet.append({
324
+ "port": device.get("port", "Unknown"),
325
+ "firmware": device.get("fw", "?"),
326
+ "uid": device.get("uid", "Unknown"),
327
+ "variant": device.get("variant", "ORIGINAL"),
328
+ })
329
+ except Exception as e:
330
+ logger.error(f"Error during local fleet scan: {e}")
331
+
332
+ # discover_devices() probes each port with a fresh serial connection,
333
+ # which fails (and is silently dropped) for the port our own active
334
+ # connection already holds exclusively. Fall back to the connected
335
+ # device's own info so it doesn't vanish from the fleet list while
336
+ # we're connected to it.
337
+ if self.datasource_manager.is_connected():
338
+ uid = self.datasource_manager.get_selected_uid()
339
+ if uid and not any(d["uid"] == uid for d in fleet):
340
+ snapshot = self.datasource_manager.snapshot()
341
+ device_info = snapshot.get('device_info') or {}
342
+ fleet.append({
343
+ "port": snapshot.get("port") or "Unknown",
344
+ "firmware": device_info.get("FwVersion", "?"),
345
+ "uid": uid,
346
+ "variant": device_info.get("variant", "ORIGINAL"),
347
+ })
348
+
349
+ return sorted(fleet, key=lambda d: d["port"])
350
+
351
+ def cleanup(self):
352
+ if self.datasource_manager:
353
+ self.datasource_manager.disconnect()
354
+
355
+
356
+ def tui_main(stdscr, _unused, args):
357
+ """Main TUI entry point."""
358
+ # Redirect stdout/stderr to prevent pycore INFO messages from messing up
359
+ # curses layout
360
+ with _TUIStdoutRedirect():
361
+ app = TUIApplication(args)
362
+
363
+ try:
364
+ app.run(stdscr)
365
+ except Exception as e:
366
+ logger.error(f"TUI application error: {e}")
367
+ raise
368
+ finally:
369
+ app.cleanup()
370
+
371
+
372
+ if __name__ == "__main__":
373
+ import argparse
374
+
375
+ parser = argparse.ArgumentParser(description="BenchLab TUI")
376
+ parser.add_argument(
377
+ '--interval',
378
+ type=float,
379
+ default=1.0,
380
+ help='Telemetry refresh interval')
381
+ parser.add_argument(
382
+ '--source',
383
+ choices=[
384
+ 'direct',
385
+ 'fastapi',
386
+ 'fastapi_custom',
387
+ 'mqtt',
388
+ 'named_pipe',
389
+ 'service_http'],
390
+ default='direct',
391
+ help='Data source type')
392
+ parser.add_argument('--api-port', type=int, default=8000, dest='api_port',
393
+ help='FastAPI server port')
394
+ parser.add_argument(
395
+ '--mqtt-broker',
396
+ default='localhost',
397
+ dest='mqtt_broker',
398
+ help='MQTT broker hostname')
399
+ parser.add_argument(
400
+ '--mqtt-port',
401
+ type=int,
402
+ default=1883,
403
+ dest='mqtt_port',
404
+ help='MQTT broker port')
405
+ parser.add_argument(
406
+ '--service-url',
407
+ default='http://localhost:8585',
408
+ dest='service_url',
409
+ help='C# BenchLab service HTTP API URL')
410
+ args = parser.parse_args()
411
+
412
+ try:
413
+ curses.wrapper(tui_main, None, args)
414
+ except KeyboardInterrupt:
415
+ print("\nTUI interrupted by user")
416
+ except Exception as e:
417
+ print(f"TUI error: {e}")
418
+ sys.exit(1)
benchlab/vu/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # BENCHLAB VU Dials
2
+
3
+ Drives physical analog-style USB "VU dial" gauges (via the bundled [VU-Server](VU-Server/)) using live BENCHLAB telemetry, and provides a terminal UI for mapping dials to sensors.
4
+
5
+ ## Overview
6
+
7
+ This tool has two pieces:
8
+
9
+ - **VU Updater** (`-vu`) — polls BENCHLAB telemetry via the shared data-source layer, resolves each configured dial's mapped sensor value, and pushes it to the VU-Server API so the physical dial needle moves. Also renders and uploads a sensor logo image to each dial's screen.
10
+ - **VU Config** (`-vuconfig`) — a curses TUI (`vu_tui.py`) for discovering connected VU dials and BENCHLAB devices, and mapping each dial to a specific device + sensor.
11
+
12
+ Both talk to a local **VU-Server** process (bundled in [`VU-Server/`](VU-Server/), a fork of [ThePi910FC's VU1 server](VU-Server/README.md)), which the tool starts and manages automatically if it isn't already running.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install -r benchlab/vu/requirements.txt
18
+ ```
19
+
20
+ Key dependencies: `requests`, `PyYAML`, `Pillow` (logo rendering), `pyserial`, `tornado` (VU-Server), plus `blessed` for the legacy curses-adjacent terminal helpers.
21
+
22
+ ## Usage
23
+
24
+ ### Configure dial-to-sensor mappings
25
+
26
+ ```bash
27
+ python -m benchlab -vuconfig
28
+ ```
29
+
30
+ Lists detected VU dials and BENCHLAB devices/sensors, and lets you assign a sensor to each dial. Mappings are written to `benchlab/vu/vu_dial.config`.
31
+
32
+ ### Run the updater
33
+
34
+ ```bash
35
+ python -m benchlab -vu
36
+ ```
37
+
38
+ Starts (or connects to) VU-Server, then continuously polls telemetry and updates each mapped dial. Supports the standard data-source flags (`--source`, `--api-url`, `--mqtt-broker`, etc. — see the [top-level README](../../README.md#data-sources)) and `-i/--interval` for poll frequency.
39
+
40
+ The updater watches `vu_dial.config` for changes and hot-reloads mappings without restarting.
41
+
42
+ ## Configuration
43
+
44
+ - **`vu_server.config`** — VU-Server connection settings:
45
+ ```json
46
+ {
47
+ "vu_server_url": "http://localhost:5340",
48
+ "api_key": "...",
49
+ "logo_file": "assets/bl_logo_144x200.png"
50
+ }
51
+ ```
52
+ - **`vu_dial.config`** — list of dial-to-sensor mappings (`dial_uid`, `dial_name`, `benchlab_uid`, `benchlab_port`, `sensor`), managed by `-vuconfig` or by editing directly.
53
+ - **`VU-Server/config.yaml`** — the underlying VU-Server's own device/serial configuration.
54
+
55
+ ## Troubleshooting
56
+
57
+ - **VU-Server won't start** — check `vu_updater.log` in this directory; the updater forwards VU-Server's stdout/stderr there.
58
+ - **No dials detected** — VU dials enumerate as USB serial devices; verify they're connected and recognized by the OS before running `-vuconfig`.
59
+ - **Dial not updating** — confirm the mapped sensor name still exists in the current telemetry snapshot (sensor keys can change if a device's config changes).
60
+
61
+ ## See Also
62
+
63
+ - [BENCHLAB PyTools Documentation](../../README.md)
64
+ - [VU-Server README](VU-Server/README.md) — the bundled dial server itself
@@ -0,0 +1,13 @@
1
+ # Notice
2
+
3
+ This directory vendors code and assets from the VU Server project by Sasa
4
+ Karanovic: https://github.com/SasaKaranovic/VU-Server
5
+
6
+ The upstream repository does not publish a `LICENSE` file. BENCHLAB.io has
7
+ obtained permission directly from the author to include and redistribute
8
+ this code as part of BENCHLAB PyTools, including in the packaged PyPI
9
+ distribution (`benchlab-pytools[vu]`).
10
+
11
+ This notice documents that redistribution grant. It does not itself
12
+ constitute a license; questions about terms for other uses should be
13
+ directed to the upstream author.
@@ -0,0 +1,80 @@
1
+ # VU Dials - VU Server
2
+
3
+
4
+ ![VU1 Dial](assets/vu1_hello_world.png?raw=true "VU1 Dial")
5
+
6
+ This is the official VU dials repository for the VU Server application and it's source code.
7
+
8
+ You can learn more about VU dials at [VUDials.com](https://vudials.com)
9
+
10
+ The VU Server is a key component of the VU dials.
11
+
12
+ It provides a Web API that can be interfaced by any third party application/script/service, allowing it to easily interact with and control the VU dials.
13
+
14
+ For example, in order to update a dial, any application can make a simple [API request](https://docs.vudials.com/api/dial_UID_set/) to the VU server and that's it.
15
+
16
+
17
+ # Demo application
18
+
19
+ Right now, we have a very simple [VU1 demo application](https://github.com/SasaKaranovic/VU-Demo-App) that runs on Windows and demonstrates how dials can be used for resource usage monitoring.
20
+
21
+ You can download it from [VUDials Download page](https://vudials.com/download/demo_app) (or look at the [source code](https://github.com/SasaKaranovic/VU-Demo-App)).
22
+
23
+ Demo application uses VU Server API to demonstrate how a third party application can interact with VU dials.
24
+
25
+
26
+ # Why not build apps for VU dials?
27
+
28
+ You might ask why build an API server? Why not just build more stand-alone applications for VU dials? It's not that simple, but we believe that we have a good reason for this decision...
29
+
30
+ VU dials can display virtually any information, and we all have our favourite/preferred applications for temperature monitoring, CPU/GPU/MEM load monitoring, weather app, stock monitoring, and the list can go on for days.
31
+
32
+ If we started building new applications specifically for VU dials, we would be forcing our users to choose between their existing favourite application and the new one that works with VU dials.
33
+
34
+ In the long run, this would result in "splitting the community" or forcing users to choose between abandoning perfectly fine software packages or abandoning the option to show some information on the VU dials.
35
+
36
+ Instead, our approach is that we have completely opened the VU dials to the community and any third-party application/script/service running on your PC (or even network).
37
+
38
+ Any application can make a straightforward API request to the VU server and say, "Hey, update dial X to show the value of Y", and that's it.
39
+
40
+ Making a web API request is a very simple thing to do in almost any software package.
41
+
42
+ So how does this benefit the community?
43
+
44
+ This approach should allow existing applications to integrate/support VU dials natively or through extensions/addons/plugins. This means you still get to use your favourite application, but as a bonus, it now works seamlessly with VU dials.
45
+
46
+ We are aware of the big caveat, which is that VU dial adoption won't happen over night and that it will require a lot of love and support from the community and developers.
47
+
48
+ But we strongly believe that in the long run this is a much better approach than creating another closed-source, black-box product that forces you to use prescribed app store if you want to use the product.
49
+
50
+
51
+
52
+ # How to run/install VU Server
53
+
54
+ For Windows we have an installer that can be download from [https://vudials.com/download/server](https://vudials.com/download/server)
55
+
56
+ For Linux and Mac users, we don't have the installer (yet), but you can run the VU Server from the source code.
57
+
58
+ We have a simple example that describes how to run VU Server from source code on a Linux machine.
59
+
60
+
61
+ [Jump to Linux Running From Source Code read me](Running_from_source_on_linux.md)
62
+
63
+
64
+ # Want to contribute?
65
+
66
+ As mentioned, right now, VU Dials adoption is the main hurdle for everyone enjoying VU dials in combination with their favourite game/applications/service.
67
+
68
+ For developers interested in supporting VU dials natively or through extension/addon/plugin, please take a look at [VU Dials API](https://docs.vudials.com/api_messaging/) documentation to learn how you can communicate with the dials through VU server.
69
+
70
+ For non-developers interested in supporting VU dials; you can reach out to your favorite developers and let them know that you would appreciate if they could integrate VU dials in their software.
71
+
72
+ We could use any and all the love that the community has to offer. :)
73
+
74
+
75
+ ---
76
+
77
+ [VU Dials Home page](https://vudials.com)
78
+
79
+
80
+
@@ -0,0 +1,97 @@
1
+ # Running VU Server from source code on Linux
2
+
3
+ You can run VU Server from source code.
4
+
5
+ In order to do this you will need to install Python (programming language used by VU Server) and optionally Git.
6
+
7
+ To do this you can run the following command
8
+
9
+ ```bash
10
+ sudo apt install git python3 python3-pip
11
+ ```
12
+
13
+ Then you can clone the VU Server repository and install project dependencies
14
+
15
+ ```bash
16
+ git clone https://github.com/SasaKaranovic/VU-Server.git
17
+ cd VU-Server
18
+ pip3 install -r requirements.txt
19
+ ```
20
+
21
+
22
+ You will need to run the above steps only once.
23
+
24
+ After that you can start VU server by running `python3 server.py`
25
+
26
+ Now you can access VU server web GUI by navigating to http://localhost:5340 in your browser.
27
+
28
+
29
+ ## How to fix "OSError: [Errno 13] Permission denied: '/dev/ttyUSBx'" error
30
+
31
+ On some distributions you will need to explicitly allow access to `/dev/ttyUSBx` where `x` is usually a number (ie `/dev/ttyUSB0`).
32
+
33
+ There are multiple ways to do this.
34
+
35
+ You can temporarily grant access by running `sudo chmod 666 /dev/ttyUSB0`.
36
+
37
+ But you will have to run this command every time you re-plug in the hub, which is not very practical.
38
+
39
+ More permanent solution involves creating a udev rule
40
+
41
+ ```bash
42
+ # navigate to rules.d directory
43
+ cd /etc/udev/rules.d
44
+ #create a new rule file
45
+ sudo touch vu-rule.rules
46
+ # open the file
47
+ sudo nano vu-rule.rules
48
+ # add the following
49
+ KERNEL=="ttyUSB0", MODE="0666"
50
+ ```
51
+
52
+ Please note that you will have to reboot your machine in order for new udev rules to take effect.
53
+
54
+
55
+ # Example command line usage
56
+
57
+ Ideally you would have an application/service/script that updates VU dials for you.
58
+
59
+ But in some instances you might want to update the VU dials from the command line.
60
+
61
+ This example shows how to do this.
62
+
63
+ Requirements
64
+
65
+ - You have to have VU server running.
66
+ - From the VU Server GUI; Get the UID of the dial you want to update.
67
+ - From the VU Server GUI; Get the API key you want to use for this purpose (create a new one or use existing one).
68
+
69
+
70
+ In order to update the dial, we need to make a [HTTP request](https://docs.vudials.com/api/dial_UID_set/) to the VU server.
71
+
72
+ Let's say we want to set the dial with the UID `3E0075000650564139323920` to `50%` using API key `cTpAWYuRpA2zx75Yh961Cg`
73
+
74
+ We can make a simple GET request using `wget` (or `curl` if you prefer):
75
+
76
+ ```bash
77
+ wget -O- -q "http://localhost:5340/api/v0/dial/3E0075000650564139323920/set?value=50&key=cTpAWYuRpA2zx75Yh961C" ; echo
78
+ ```
79
+
80
+ You can use pipe redirects in your terminal to redirect output of applications to VU dials.
81
+
82
+ Let's say we use following command to retrieve CPU usage on our Ubuntu machine
83
+
84
+ ```bash
85
+ echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]
86
+ ```
87
+
88
+ This command will output an integer that represents CPU usage in percentage. We can then forward this information to VU server using pipe redirects
89
+
90
+ ```bash
91
+ echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')] | xargs -I{} wget -O- -q "http://localhost:5340/api/v0/dial/3E0075000650564139323920/set?value={}&key=cTpAWYuRpA2zx75Yh961C"; echo
92
+ ```
93
+
94
+ Please keep in mind that this is a very "simple" example (and yet looks somewhat complicated), but the main idea is that you can easily redirect output of any application on your system and send it to VU Server to have it displayed on your VU1 dials.
95
+
96
+ Ideally you would not run these commands manually but instead have a script/service/cron that updates your dials periodically.
97
+
File without changes