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
benchlab/main.py ADDED
@@ -0,0 +1,462 @@
1
+ """BENCHLAB PyTools v2 – Main Launcher.
2
+
3
+ This module implements the command-line entry point for the Benchlab
4
+ telemetry suite. The workflow is split into three steps:
5
+
6
+ 1. **Select a data source** – FastAPI, MQTT, direct serial, named pipe,
7
+ or service HTTP.
8
+ 2. **Choose consumer tools** – one or many tools that will read from the
9
+ selected source.
10
+ 3. **Launch** – start the source (if needed) and then launch the
11
+ selected tools, handling cleanup on exit.
12
+ """
13
+
14
+ import argparse
15
+ import curses
16
+ import logging
17
+ import os
18
+ import sys
19
+ import traceback
20
+
21
+ from .tools import CONSUMER_TOOLS, LAUNCH_PROFILES # noqa: F401
22
+ # CONSUMER_TOOLS re-exported for benchlab.main.CONSUMER_TOOLS consumers
23
+ from .sources import check_and_setup_source, cleanup_all_services
24
+ from .launcher import launch_tools_concurrent
25
+
26
+ logger = logging.getLogger("benchlab.launcher")
27
+
28
+ try:
29
+ from .menu import interactive_loop
30
+ except ImportError as e:
31
+ # prompt_toolkit isn't installed (or failed to import for some other
32
+ # reason) — fall back to the plain numbered-input menu rather than
33
+ # crashing the whole interactive entry point.
34
+ logger.debug(
35
+ f"Falling back to classic menu (prompt_toolkit unavailable: {e})")
36
+ from .menu_classic import interactive_loop
37
+
38
+ # ──────────────────────────────────────────────────────────────
39
+ # Argument Parser
40
+ # ──────────────────────────────────────────────────────────────
41
+
42
+
43
+ def get_parser() -> argparse.ArgumentParser:
44
+ from . import __version__
45
+
46
+ parser = argparse.ArgumentParser(
47
+ description="BENCHLAB PyTools v2 - Device Telemetry Suite"
48
+ )
49
+
50
+ parser.add_argument("--version", action="version",
51
+ version=f"benchlab-pytools {__version__}")
52
+ parser.add_argument("-config", action="store_true",
53
+ help="Device configuration import/export tool")
54
+ parser.add_argument("-fastapi", action="store_true",
55
+ help="Launch FastAPI telemetry API server")
56
+ parser.add_argument("-graph", action="store_true",
57
+ help="Launch GUI graphing mode")
58
+ parser.add_argument("-hwinfo", action="store_true",
59
+ help="Export sensors to HWiNFO custom sensors")
60
+ parser.add_argument("-i", "--interval", type=float, default=1.0,
61
+ help="Refresh interval in seconds")
62
+ parser.add_argument("-logfleet", action="store_true",
63
+ help="Run CSV logger without TUI")
64
+ parser.add_argument("-mqtt", nargs="?", const="localhost",
65
+ help="MQTT publisher to localhost mosquitto")
66
+ parser.add_argument("-link", action="store_true",
67
+ help="Run cloud MQTT link publisher")
68
+ parser.add_argument(
69
+ "--remote-host",
70
+ default=None,
71
+ dest="remote_host",
72
+ help="Cloud MQTT broker hostname (overrides LINK_REMOTE_HOST)")
73
+ parser.add_argument(
74
+ "--remote-port",
75
+ type=int,
76
+ default=None,
77
+ dest="remote_port",
78
+ help="Cloud MQTT broker port (default: 8883)")
79
+ parser.add_argument(
80
+ "--remote-user",
81
+ default=None,
82
+ dest="remote_user",
83
+ help="Cloud MQTT username (overrides LINK_REMOTE_USER)")
84
+ parser.add_argument(
85
+ "--remote-pass",
86
+ default=None,
87
+ dest="remote_pass",
88
+ help="Cloud MQTT password (overrides LINK_REMOTE_PASS)")
89
+ parser.add_argument("--no-tls", action="store_true", dest="no_tls",
90
+ help="Disable TLS for cloud MQTT connection")
91
+ parser.add_argument(
92
+ "--topic-pattern",
93
+ default=None,
94
+ dest="topic_pattern",
95
+ help=(
96
+ "MQTT topic pattern with {uid} token "
97
+ "(overrides LINK_TOPIC_PATTERN)"))
98
+ parser.add_argument("-tui", action="store_true",
99
+ help="Enable TUI (default)")
100
+ parser.add_argument(
101
+ "--source",
102
+ help=(
103
+ "Data source: direct | fastapi | fastapi_custom | mqtt | "
104
+ "mqtt_custom | named_pipe | service_http"),
105
+ choices=[
106
+ "direct",
107
+ "fastapi",
108
+ "fastapi_custom",
109
+ "mqtt",
110
+ "mqtt_custom",
111
+ "named_pipe",
112
+ "service_http"],
113
+ default=None,
114
+ metavar="SOURCE")
115
+ parser.add_argument(
116
+ "--api-url",
117
+ default="http://127.0.0.1:8000",
118
+ dest="api_url",
119
+ help="FastAPI base URL (default: http://127.0.0.1:8000)")
120
+ parser.add_argument("--api-port", type=int, default=8000,
121
+ dest="api_port",
122
+ help="FastAPI port (default: 8000)")
123
+ parser.add_argument("--mqtt-broker", default="localhost",
124
+ dest="mqtt_broker",
125
+ help="MQTT broker host (default: localhost)")
126
+ parser.add_argument("--mqtt-port", type=int, default=1883,
127
+ dest="mqtt_port",
128
+ help="MQTT broker port (default: 1883)")
129
+ parser.add_argument(
130
+ "--service-url",
131
+ default="http://localhost:8585",
132
+ dest="service_url",
133
+ help=(
134
+ "C# BenchLab service HTTP API URL "
135
+ "(default: http://localhost:8585)"))
136
+ parser.add_argument("-vu", action="store_true",
137
+ help="Launch VU analog dials")
138
+ parser.add_argument("-vuconfig", action="store_true",
139
+ help="Launch VU configuration interface")
140
+ parser.add_argument("-wigidash", action="store_true",
141
+ help="Connect to WigiDash")
142
+ parser.add_argument("--profile",
143
+ help="Launch predefined multi-tool profile",
144
+ default=None)
145
+
146
+ return parser
147
+
148
+
149
+ # ──────────────────────────────────────────────────────────────
150
+ # Source Setup from CLI Args
151
+ # ──────────────────────────────────────────────────────────────
152
+
153
+ def _setup_source_from_args(args) -> bool:
154
+ """Resolve and start the data source requested via --source (or env
155
+ fallback).
156
+
157
+ Returns True if the source is ready, False on failure.
158
+ """
159
+ source = args.source or os.environ.get("BENCHLAB_DATA_SOURCE", "direct")
160
+ logger.info(f"Setting up data source: {source}")
161
+
162
+ if source == "fastapi":
163
+ port = getattr(
164
+ args,
165
+ "api_port",
166
+ None) or int(
167
+ os.environ.get(
168
+ "API_PORT",
169
+ "8000"))
170
+ os.environ["API_PORT"] = str(port)
171
+ os.environ["BENCHLAB_API_URL"] = getattr(
172
+ args, "api_url", f"http://127.0.0.1:{port}")
173
+ ready = check_and_setup_source("fastapi", port=port)
174
+
175
+ elif source == "fastapi_custom":
176
+ api_url = getattr(
177
+ args,
178
+ "api_url",
179
+ os.environ.get(
180
+ "BENCHLAB_API_URL",
181
+ "http://127.0.0.1:8000"))
182
+ os.environ["BENCHLAB_API_URL"] = api_url
183
+ ready = check_and_setup_source("fastapi_custom", base_url=api_url)
184
+
185
+ elif source == "mqtt":
186
+ broker = getattr(
187
+ args,
188
+ "mqtt_broker",
189
+ None) or os.environ.get(
190
+ "MQTT_BROKER",
191
+ "localhost")
192
+ mqtt_port = getattr(
193
+ args,
194
+ "mqtt_port",
195
+ None) or int(
196
+ os.environ.get(
197
+ "MQTT_PORT",
198
+ "1883"))
199
+ os.environ["MQTT_BROKER"] = broker
200
+ os.environ["MQTT_PORT"] = str(mqtt_port)
201
+ ready = check_and_setup_source(
202
+ "mqtt", broker=broker, mqtt_port=mqtt_port)
203
+
204
+ elif source == "mqtt_custom":
205
+ broker = getattr(
206
+ args,
207
+ "mqtt_broker",
208
+ None) or os.environ.get(
209
+ "MQTT_BROKER",
210
+ "localhost")
211
+ mqtt_port = getattr(
212
+ args,
213
+ "mqtt_port",
214
+ None) or int(
215
+ os.environ.get(
216
+ "MQTT_PORT",
217
+ "1883"))
218
+ os.environ["MQTT_BROKER"] = broker
219
+ os.environ["MQTT_PORT"] = str(mqtt_port)
220
+ ready = check_and_setup_source(
221
+ "mqtt_custom", broker=broker, mqtt_port=mqtt_port)
222
+
223
+ elif source == "named_pipe":
224
+ ready = check_and_setup_source("named_pipe")
225
+
226
+ elif source == "service_http":
227
+ service_url = getattr(args, "service_url", None) or os.environ.get(
228
+ "BENCHLAB_SERVICE_URL", "http://localhost:8585"
229
+ )
230
+ os.environ["BENCHLAB_SERVICE_URL"] = service_url
231
+ # Parse host/port from URL for the check
232
+ import urllib.parse
233
+ parsed = urllib.parse.urlparse(service_url)
234
+ ready = check_and_setup_source(
235
+ "service_http",
236
+ host=parsed.hostname or "localhost",
237
+ port=parsed.port or 8585,
238
+ )
239
+
240
+ else:
241
+ ready = check_and_setup_source("direct")
242
+
243
+ if ready:
244
+ os.environ["BENCHLAB_DATA_SOURCE"] = source
245
+ else:
246
+ logger.error(f"Could not set up '{source}' data source.")
247
+ return ready
248
+
249
+
250
+ def _export_link_env(args) -> None:
251
+ """Mirror --remote-*/--no-tls/--topic-pattern CLI flags into env vars.
252
+
253
+ link_main.py's _resolve_config already falls back to these env vars
254
+ (REMOTE_MQTT_HOST etc.) when no args object provides a value, so
255
+ exporting here is what lets a spawned/multi-tool `link` process
256
+ (launched via launcher.py's _spawn_tool_in_terminal, which parses
257
+ fresh argv with none of these flags) still pick up the config the
258
+ user passed on the parent process's command line.
259
+ """
260
+ mapping = {
261
+ "remote_host": "REMOTE_MQTT_HOST",
262
+ "remote_port": "REMOTE_MQTT_PORT",
263
+ "remote_user": "REMOTE_MQTT_USER",
264
+ "remote_pass": "REMOTE_MQTT_PASS",
265
+ "topic_pattern": "LINK_TOPIC_PATTERN",
266
+ }
267
+ for attr, env_key in mapping.items():
268
+ val = getattr(args, attr, None)
269
+ if val is not None:
270
+ os.environ[env_key] = str(val)
271
+ if getattr(args, "no_tls", False):
272
+ os.environ["REMOTE_MQTT_TLS"] = "false"
273
+
274
+
275
+ # ──────────────────────────────────────────────────────────────
276
+ # Per-tool CLI Dispatch Helpers
277
+ # ──────────────────────────────────────────────────────────────
278
+
279
+ def _run_with_source(
280
+ args,
281
+ import_path: str,
282
+ func_name: str,
283
+ call_fn,
284
+ tool_label: str) -> None:
285
+ """Set up source, call call_fn, then clean up."""
286
+ from .tools import ensure_tool_dependencies
287
+ tool_id = next((tid for tid, t in CONSUMER_TOOLS.items()
288
+ if t["module"] == import_path), None)
289
+ if tool_id:
290
+ try:
291
+ ensure_tool_dependencies(tool_id)
292
+ except Exception as e:
293
+ logger.warning(
294
+ f"Failed to install dependencies for {tool_label}: {e}")
295
+
296
+ if not _setup_source_from_args(args):
297
+ return
298
+ try:
299
+ mod = __import__(import_path, fromlist=[func_name])
300
+ call_fn(getattr(mod, func_name))
301
+ except ModuleNotFoundError:
302
+ traceback.print_exc()
303
+ print(f"{tool_label} module not available in this build.")
304
+ finally:
305
+ cleanup_all_services()
306
+
307
+
308
+ # ──────────────────────────────────────────────────────────────
309
+ # CLI Entry Point
310
+ # ──────────────────────────────────────────────────────────────
311
+
312
+ def launch_mode() -> None:
313
+ """Parse CLI arguments and dispatch to the appropriate mode."""
314
+ parser = get_parser()
315
+
316
+ # Check if -config is in args - if so, use parse_known_args to allow
317
+ # config tool args through
318
+ if '-config' in sys.argv:
319
+ args, unknown = parser.parse_known_args()
320
+ else:
321
+ args = parser.parse_args()
322
+
323
+ no_flags = not any([args.config,
324
+ args.fastapi,
325
+ args.graph,
326
+ args.hwinfo,
327
+ args.link,
328
+ args.logfleet,
329
+ args.mqtt,
330
+ args.tui,
331
+ args.vu,
332
+ args.vuconfig,
333
+ args.wigidash,
334
+ args.profile,
335
+ ])
336
+ if no_flags:
337
+ interactive_loop()
338
+ return
339
+
340
+ # ── Profile ──────────────────────────────────────────────
341
+ if args.profile:
342
+ profile = LAUNCH_PROFILES.get(args.profile)
343
+ if not profile:
344
+ print(f"Unknown profile: {args.profile}")
345
+ return
346
+ print(f"Launching profile: {args.profile}")
347
+
348
+ from .tools import ensure_profile_dependencies
349
+ ensure_profile_dependencies(args.profile)
350
+
351
+ profile_args = argparse.Namespace(
352
+ source=profile.get("source", "direct"),
353
+ api_url=getattr(args, "api_url", "http://127.0.0.1:8000"),
354
+ api_port=getattr(args, "api_port", 8000),
355
+ mqtt_broker=getattr(args, "mqtt_broker", "localhost"),
356
+ mqtt_port=getattr(args, "mqtt_port", 1883),
357
+ service_url=getattr(
358
+ args, "service_url", "http://localhost:8585"),
359
+ )
360
+ if not _setup_source_from_args(profile_args):
361
+ print("Failed to initialize data source")
362
+ return
363
+ os.environ["BENCHLAB_DATA_SOURCE"] = profile.get("source", "direct")
364
+ launch_tools_concurrent(profile["tools"])
365
+ return
366
+
367
+ # ── Individual tool flags ─────────────────────────────────
368
+ if args.config:
369
+ # Config tool handles its own argument parsing
370
+ # Remove -config from sys.argv so config tool can parse remaining args
371
+ try:
372
+ sys.argv.remove('-config')
373
+ from benchlab.config.config_tool import main as config_main
374
+ sys.exit(config_main())
375
+ except ModuleNotFoundError:
376
+ print("Config tool module not available in this build.")
377
+
378
+ elif args.fastapi:
379
+ try:
380
+ from benchlab.restapi.telemetry_api import run_server
381
+ run_server()
382
+ except ModuleNotFoundError:
383
+ print("FastAPI / Uvicorn not available in this build.")
384
+
385
+ elif args.mqtt:
386
+ try:
387
+ from benchlab.mqtt.mqtt_publisher import run_mqtt_mode
388
+ broker = args.mqtt if args.mqtt else "localhost"
389
+ os.environ.setdefault("MQTT_BROKER", broker)
390
+ run_mqtt_mode(broker)
391
+ except ModuleNotFoundError:
392
+ print("MQTT module not available in this build.")
393
+
394
+ elif args.link:
395
+ _export_link_env(args)
396
+ _run_with_source(args,
397
+ "benchlab.link.link_main", "run_link",
398
+ lambda fn: fn(args), "Link")
399
+
400
+ elif args.graph:
401
+ _run_with_source(args,
402
+ "benchlab.graph.runner", "run_graph_mode",
403
+ lambda fn: fn(args), "Graph")
404
+
405
+ elif args.hwinfo:
406
+ _run_with_source(
407
+ args,
408
+ "benchlab.hwinfo.hwinfo_export",
409
+ "export_all_devices",
410
+ lambda fn: fn(
411
+ update_interval=args.interval),
412
+ "HWiNFO export")
413
+
414
+ elif args.logfleet:
415
+ _run_with_source(
416
+ args,
417
+ "benchlab.csv_log.csv_logger_enhanced",
418
+ "run_enhanced_csv_logger",
419
+ lambda fn: fn(args),
420
+ "Enhanced CSV logger")
421
+
422
+ elif args.vu:
423
+ _run_with_source(args,
424
+ "benchlab.vu.vu_updater", "run_updater",
425
+ lambda fn: fn(args), "VU")
426
+
427
+ elif args.vuconfig:
428
+ _run_with_source(args,
429
+ "benchlab.vu.vu_tui", "launch_vu_config",
430
+ lambda fn: fn(args), "VU configuration")
431
+
432
+ elif args.wigidash:
433
+ _run_with_source(args,
434
+ "benchlab.wigidash.wigidash_manager", "main",
435
+ lambda fn: fn(args), "WigiDash")
436
+
437
+ elif args.tui:
438
+ if not _setup_source_from_args(args):
439
+ return
440
+ try:
441
+ from benchlab.tui.tui_main import tui_main
442
+ curses.wrapper(tui_main, None, args)
443
+ except KeyboardInterrupt:
444
+ pass
445
+ except ModuleNotFoundError:
446
+ logger.error("TUI module not available in this build.")
447
+ finally:
448
+ cleanup_all_services()
449
+
450
+ else:
451
+ logger.info(
452
+ "No specific mode flags provided; launching interactive loop.")
453
+ interactive_loop()
454
+
455
+
456
+ def main() -> None:
457
+ """Entry point."""
458
+ launch_mode()
459
+
460
+
461
+ if __name__ == "__main__":
462
+ main()