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,410 @@
1
+ """BENCHLAB PyTools v2 – Interactive Menu System.
2
+
3
+ Implements the three-step interactive menu flow:
4
+ 1. Mode selection (provider / single tool / multi-tool)
5
+ 2. Tool selection
6
+ 3. Data source selection + launch confirmation
7
+ """
8
+
9
+ import logging
10
+ import os
11
+ import sys
12
+ from typing import List, Optional
13
+
14
+ from bootstrap import clear_screen
15
+ from .tools import CONSUMER_TOOLS
16
+ from .sources import (
17
+ check_and_setup_source,
18
+ check_mqtt_running,
19
+ start_mqtt_broker,
20
+ start_mqtt_source,
21
+ cleanup_all_services,
22
+ SERVICE_HTTP_DEFAULT_PORT,
23
+ )
24
+ from .launcher import launch_single_tool, launch_tools_concurrent
25
+
26
+ logger = logging.getLogger("benchlab.launcher")
27
+
28
+
29
+ # ──────────────────────────────────────────────────────────────
30
+ # Banner
31
+ # ──────────────────────────────────────────────────────────────
32
+
33
+ def print_banner() -> None:
34
+ print(r"""
35
+ ██████╗ ███████╗███╗ ██╗ ██████╗██╗ ██╗██╗ █████╗ ██████╗
36
+ ██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██║██║ ██╔══██╗██╔══██╗
37
+ ██████╔╝█████╗ ██╔██╗ ██║██║ ███████║██║ ███████║██████╔╝
38
+ ██╔══██╗██╔══╝ ██║╚██╗██║██║ ██╔══██║██║ ██╔══██║██╔══██╗
39
+ ██████╔╝███████╗██║ ╚████║╚██████╗██║ ██║███████╗██║ ██║██████╔╝
40
+ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═════╝
41
+
42
+ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗ ██╗ ███████╗
43
+ ██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██╔════╝
44
+ ██████╔╝ ╚████╔╝ ██║ ██║ ██║██║ ██║██║ ███████╗
45
+ ██╔═══╝ ╚██╔╝ ██║ ██║ ██║██║ ██║██║ ╚════██║
46
+ ██║ ██║ ██║ ╚██████╔╝╚██████╔╝███████╗███████║
47
+ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝
48
+ """)
49
+
50
+
51
+ # ──────────────────────────────────────────────────────────────
52
+ # Step 1 – Mode Selection
53
+ # ──────────────────────────────────────────────────────────────
54
+
55
+ def show_step1_menu() -> Optional[str]:
56
+ """Display mode selection.
57
+
58
+ Returns 'provider', 'single', 'multi', or None.
59
+ """
60
+ print("What would you like to do?\n")
61
+ print(" 1. Data Provider - Run FastAPI or MQTT server for other tools")
62
+ print(" 2. Single Tool - Run one tool with a data source")
63
+ print(
64
+ " 3. Multi-Tool - Run multiple tools with shared data "
65
+ "(Experimental!)")
66
+ print()
67
+ print(" q. Quit")
68
+ print()
69
+
70
+ try:
71
+ choice = input("Choice: ").strip().lower()
72
+ return {"1": "provider", "2": "single", "3": "multi"}.get(choice) or (
73
+ None if choice in ("q", "quit", "exit")
74
+ else _invalid("Enter 1, 2, 3, or q.")
75
+ )
76
+ except (EOFError, KeyboardInterrupt):
77
+ return None
78
+
79
+
80
+ def _invalid(msg: str) -> None:
81
+ print(msg)
82
+ return None
83
+
84
+
85
+ # ──────────────────────────────────────────────────────────────
86
+ # Step 2a – Data Provider
87
+ # ──────────────────────────────────────────────────────────────
88
+
89
+ def step2_data_provider() -> None:
90
+ """Select and start a standalone data provider."""
91
+ print()
92
+ print("=== Data Provider ===")
93
+ print("1. FastAPI Server - REST API + WebSocket on port 8000")
94
+ print("2. MQTT Publisher - Publish telemetry to MQTT broker")
95
+ print()
96
+
97
+ choice = input("Choice [1-2]: ").strip()
98
+
99
+ if choice == "1":
100
+ port_input = input(" Port [8000]: ").strip()
101
+ try:
102
+ port = int(port_input) if port_input else 8000
103
+ except ValueError:
104
+ print(" Invalid port number.")
105
+ return
106
+ os.environ["API_PORT"] = str(port)
107
+ if not check_and_setup_source("fastapi", port=port):
108
+ logger.error("Could not start FastAPI server.")
109
+ return
110
+ print("FastAPI server running. Press Ctrl+C to stop the provider.")
111
+ input(" (Press Enter to return to menu after verifying...) ")
112
+
113
+ elif choice == "2":
114
+ host = input(" Broker host [localhost]: ").strip() or "localhost"
115
+ port_input = input(" Broker port [1883]: ").strip()
116
+ try:
117
+ port = int(port_input) if port_input else 1883
118
+ except ValueError:
119
+ print(" Invalid port number.")
120
+ return
121
+ if not check_mqtt_running(host, port):
122
+ logger.warning(f"No MQTT broker at {host}:{port}")
123
+ logger.info("Starting embedded broker...")
124
+ if not start_mqtt_broker(port):
125
+ logger.error("Could not start MQTT broker.")
126
+ return
127
+ else:
128
+ logger.info(f"MQTT broker available at {host}:{port}")
129
+ os.environ["MQTT_BROKER"] = host
130
+ os.environ["MQTT_PORT"] = str(port)
131
+ start_mqtt_source(host, port)
132
+ logger.info("Press Ctrl+C to stop the provider.")
133
+ input(" (Press Enter to return to menu after verifying...) ")
134
+
135
+ else:
136
+ logger.error("Invalid choice in data provider selection.")
137
+
138
+
139
+ # ──────────────────────────────────────────────────────────────
140
+ # Step 2b – Single Tool
141
+ # ──────────────────────────────────────────────────────────────
142
+
143
+ def step2_single_tool() -> None:
144
+ """Select one tool and proceed to source selection."""
145
+ print()
146
+ print("=== Select Tool ===")
147
+ consumer_list = list(CONSUMER_TOOLS.items())
148
+ for i, (_, t) in enumerate(consumer_list, 1):
149
+ print(f" {i}. {t['name']} - {t['description']}")
150
+ print()
151
+
152
+ choice = input("Tool number: ").strip()
153
+ try:
154
+ idx = int(choice) - 1
155
+ if not (0 <= idx < len(consumer_list)):
156
+ print(" Invalid selection.")
157
+ return
158
+ tool_id, tool_info = consumer_list[idx]
159
+ except (ValueError, IndexError):
160
+ print(" Invalid selection.")
161
+ return
162
+
163
+ step3_select_source(tool_ids=[tool_id], tool_names=[tool_info["name"]])
164
+
165
+
166
+ # ──────────────────────────────────────────────────────────────
167
+ # Step 2c – Multi-Tool
168
+ # ──────────────────────────────────────────────────────────────
169
+
170
+ def step2_multi_tool() -> None:
171
+ """Select multiple tools and proceed to source selection.
172
+
173
+ (Experimental!)
174
+ """
175
+ print()
176
+ print("=== Select Tools ===")
177
+ print("Enter tool numbers separated by commas (e.g., 1,3,5)")
178
+ print("Or 'all' to select all.")
179
+
180
+ consumer_list = list(CONSUMER_TOOLS.items())
181
+ for i, (_, t) in enumerate(consumer_list, 1):
182
+ print(f" {i}. {t['name']} - {t['description']}")
183
+ print()
184
+
185
+ choice = input("Tools: ").strip().lower()
186
+ if not choice:
187
+ print(" No tools selected.")
188
+ return
189
+
190
+ if choice == "all":
191
+ selected = list(consumer_list)
192
+ else:
193
+ selected = []
194
+ for part in choice.split(","):
195
+ try:
196
+ idx = int(part.strip()) - 1
197
+ if 0 <= idx < len(consumer_list):
198
+ selected.append(consumer_list[idx])
199
+ except ValueError:
200
+ pass
201
+
202
+ if not selected:
203
+ print(" No valid tools selected.")
204
+ return
205
+
206
+ step3_select_source(
207
+ tool_ids=[tid for tid, _ in selected],
208
+ tool_names=[t["name"] for _, t in selected],
209
+ )
210
+
211
+
212
+ # ──────────────────────────────────────────────────────────────
213
+ # Step 3 – Source Selection & Launch
214
+ # ──────────────────────────────────────────────────────────────
215
+
216
+ def _build_source_menu(is_multi: bool,
217
+ supported_sources: Optional[List[str]] = None) -> dict:
218
+ """Build the source selection menu.
219
+
220
+ Args:
221
+ is_multi: Whether multiple tools are being launched
222
+ supported_sources: Optional list of source types to include
223
+ (e.g., ["direct", "named_pipe"])
224
+ If None, all sources are shown.
225
+
226
+ Returns a dict mapping menu key → (label, source_type).
227
+ All sources are always listed unless filtered by supported_sources.
228
+ OS-unsupported ones get a note.
229
+ """
230
+ is_windows = sys.platform.startswith("win")
231
+ os_name = "Windows" if is_windows else (
232
+ "macOS" if sys.platform == "darwin" else "Linux")
233
+
234
+ # Build all possible sources
235
+ all_sources = []
236
+
237
+ if not is_multi:
238
+ all_sources.append(("Direct (serial port)", "direct"))
239
+
240
+ all_sources.append(("FastAPI server (Python)", "fastapi"))
241
+ all_sources.append(("FastAPI server (custom URL)", "fastapi_custom"))
242
+ all_sources.append(("MQTT (Python, experimental)", "mqtt"))
243
+ all_sources.append(("MQTT (custom)", "mqtt_custom"))
244
+
245
+ pipe_note = "" if is_windows else f" (not available on {os_name})"
246
+ all_sources.append(
247
+ (f"BenchLab service - named pipe{pipe_note}", "named_pipe"))
248
+ all_sources.append(
249
+ (f"BenchLab service - HTTP API (port {SERVICE_HTTP_DEFAULT_PORT})",
250
+ "service_http"))
251
+
252
+ # Filter by supported_sources if provided
253
+ if supported_sources is not None:
254
+ all_sources = [(label, src_type) for label, src_type in all_sources
255
+ if src_type in supported_sources]
256
+
257
+ # Build numbered menu
258
+ sources = {}
259
+ for key, (label, src_type) in enumerate(all_sources, 1):
260
+ sources[str(key)] = (label, src_type)
261
+
262
+ return sources
263
+
264
+
265
+ def step3_select_source(tool_ids: List[str], tool_names: List[str]) -> None:
266
+ """Select data source, verify/start it, confirm, then launch tools."""
267
+ is_multi = len(tool_ids) > 1
268
+ print()
269
+ print("=== Data Source ===")
270
+
271
+ if is_multi:
272
+ print(f"Tools: {', '.join(tool_names)}")
273
+ print(" Note: Direct mode is not available for multi-tool")
274
+ print(" because the serial port can only be used by one process.")
275
+ else:
276
+ print(f"Tool: {tool_names[0]}")
277
+
278
+ print()
279
+
280
+ # Get supported sources for single tool (if specified)
281
+ supported_sources = None
282
+ if not is_multi and len(tool_ids) == 1:
283
+ tool_info = CONSUMER_TOOLS.get(tool_ids[0])
284
+ if tool_info:
285
+ supported_sources = tool_info.get("supported_sources")
286
+
287
+ sources = _build_source_menu(is_multi, supported_sources)
288
+
289
+ for key, (label, _) in sorted(sources.items()):
290
+ print(f" {key}. {label}")
291
+
292
+ print()
293
+ default_key = min(sources.keys())
294
+ choice = input(f"Choice (default: {default_key}): ").strip() or default_key
295
+
296
+ if choice not in sources:
297
+ print(" Invalid choice.")
298
+ return
299
+
300
+ label, source_type = sources[choice]
301
+
302
+ logger.info(f"Setting up {source_type} data source...")
303
+
304
+ # Build kwargs for check_and_setup_source
305
+ setup_kwargs: dict = {}
306
+ if source_type == "fastapi":
307
+ port = int(os.environ.get("API_PORT", "8000"))
308
+ setup_kwargs = {"port": port}
309
+ elif source_type == "fastapi_custom":
310
+ # Prompt for custom FastAPI server URL
311
+ host = input(" Host/IP [127.0.0.1]: ").strip() or "127.0.0.1"
312
+ port_input = input(" Port [8000]: ").strip()
313
+ try:
314
+ port = int(port_input) if port_input else 8000
315
+ except ValueError:
316
+ print(" Invalid port number.")
317
+ return
318
+ base_url = f"http://{host}:{port}"
319
+ setup_kwargs = {"base_url": base_url}
320
+ # Store for later use by datasource manager
321
+ os.environ["BENCHLAB_FASTAPI_CUSTOM_URL"] = base_url
322
+ print(f" → Using FastAPI server at {base_url}")
323
+ elif source_type == "mqtt":
324
+ broker = os.environ.get("MQTT_BROKER", "localhost")
325
+ mqtt_port = int(os.environ.get("MQTT_PORT", "1883"))
326
+ setup_kwargs = {"broker": broker, "mqtt_port": mqtt_port}
327
+ elif source_type == "mqtt_custom":
328
+ # Prompt for custom MQTT broker
329
+ broker = input(" Broker host [localhost]: ").strip() or "localhost"
330
+ port_input = input(" Broker port [1883]: ").strip()
331
+ try:
332
+ mqtt_port = int(port_input) if port_input else 1883
333
+ except ValueError:
334
+ print(" Invalid port number.")
335
+ return
336
+ setup_kwargs = {"broker": broker, "mqtt_port": mqtt_port}
337
+ print(f" → Using MQTT broker at {broker}:{mqtt_port}")
338
+ elif source_type == "service_http":
339
+ import urllib.parse
340
+ svc_url = os.environ.get(
341
+ "BENCHLAB_SERVICE_URL",
342
+ f"http://localhost:{SERVICE_HTTP_DEFAULT_PORT}")
343
+ parsed = urllib.parse.urlparse(svc_url)
344
+ setup_kwargs = {"host": parsed.hostname or "localhost",
345
+ "port": parsed.port or SERVICE_HTTP_DEFAULT_PORT}
346
+
347
+ source_ready = check_and_setup_source(source_type, **setup_kwargs)
348
+
349
+ if not source_ready:
350
+ print(f"\n ✗ Could not set up {source_type} data source.")
351
+ if source_type in ("named_pipe", "service_http"):
352
+ print(
353
+ " → Start the BenchLab Windows service (BL_Service.exe) "
354
+ "and try again.")
355
+ return
356
+
357
+ print()
358
+ print("=== Launch Summary ===")
359
+ print(f"Tools: {', '.join(tool_names)}")
360
+ print(f"Data source: {source_type}")
361
+ print()
362
+
363
+ if input("Launch? (Y/n): ").strip().lower() in ("n", "no"):
364
+ print("Aborted.")
365
+ return
366
+
367
+ os.environ["BENCHLAB_DATA_SOURCE"] = source_type
368
+
369
+ try:
370
+ if is_multi:
371
+ launch_tools_concurrent(tool_ids)
372
+ else:
373
+ launch_single_tool(tool_ids[0])
374
+ except KeyboardInterrupt:
375
+ print("\n Interrupted.")
376
+ finally:
377
+ cleanup_all_services()
378
+
379
+
380
+ # ──────────────────────────────────────────────────────────────
381
+ # Main Interactive Loop
382
+ # ──────────────────────────────────────────────────────────────
383
+
384
+ def interactive_loop() -> None:
385
+ """Drive the top-level interactive menu until the user quits."""
386
+ clear_screen()
387
+ print_banner()
388
+
389
+ while True:
390
+ try:
391
+ mode = show_step1_menu()
392
+ if mode is None:
393
+ print("Goodbye!")
394
+ cleanup_all_services()
395
+ return
396
+
397
+ if mode == "provider":
398
+ step2_data_provider()
399
+ elif mode == "single":
400
+ step2_single_tool()
401
+ elif mode == "multi":
402
+ step2_multi_tool()
403
+
404
+ input("\n Press Enter to continue... ")
405
+ clear_screen()
406
+
407
+ except (EOFError, KeyboardInterrupt):
408
+ print("Goodbye!")
409
+ cleanup_all_services()
410
+ return
@@ -0,0 +1,201 @@
1
+ # BENCHLAB MQTT Publisher
2
+
3
+ ## Overview
4
+
5
+ The MQTT Publisher module provides real-time telemetry from all connected Benchlab devices to an MQTT broker.
6
+
7
+ It automatically:
8
+
9
+ - Detects connected devices via serial ports.
10
+ - Reads sensor data from each device.
11
+ - Translates sensor data into structured JSON payloads.
12
+ - Publishes telemetry and device info to a configured MQTT broker.
13
+ - Supports multiple transport options (TCP, TLS, WebSockets) and authentication.
14
+ - Handles graceful shutdown and logging.
15
+
16
+ Designed for integration with dashboards, monitoring platforms, or other MQTT consumers.
17
+
18
+ ---
19
+
20
+ ## Features
21
+
22
+ | Feature | Description |
23
+ |---------|-------------|
24
+ | Automatic device discovery | Uses `get_fleet_info()` to list all connected devices. |
25
+ | Sensor reading & translation | Reads raw sensor data and converts it into JSON-ready format via `translate_sensor_struct()`. |
26
+ | MQTT publishing | Publishes telemetry and device info on separate topics for each device. |
27
+ | Multi-device support | Runs each device in its own thread for parallel publishing. |
28
+ | TLS / WebSocket support | Supports secure and custom MQTT transports. |
29
+ | Graceful shutdown | Stops all threads and disconnects cleanly on Ctrl+C. |
30
+ | Structured logging | Logs events and errors in JSON format or standard stdout. |
31
+
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ Install the required dependencies for the MQTT module:
37
+
38
+ ```
39
+ pip install -r requirements.txt
40
+ ```
41
+
42
+ Dependencies (see `requirements.txt`):
43
+
44
+ ```
45
+ paho-mqtt>=1.6.1
46
+ amqtt>=0.12.0,<0.13.0
47
+ pyyaml>=6.0,<7.0
48
+ ```
49
+
50
+ Plus the `benchlab_pycore` core modules and the rest of the `benchlab` package (installed as part of the overall project).
51
+
52
+ ---
53
+
54
+ ## Configuration
55
+
56
+ Configure the MQTT publisher using **environment variables**.
57
+
58
+ ### Example
59
+
60
+ ```
61
+ export MQTT_BROKER=localhost
62
+ export MQTT_PORT=1883
63
+ export MQTT_TRANSPORT=tcp
64
+ export MQTT_USERNAME=user
65
+ export MQTT_PASSWORD=secret
66
+ export MQTT_PROTOCOL=MQTTv311
67
+ export MQTT_QOS=0
68
+ export MQTT_PATH=/mqtt
69
+ ```
70
+
71
+ ### Environment Variables
72
+
73
+ | Variable | Default | Description |
74
+ |----------|---------|-------------|
75
+ | `MQTT_BROKER` | `localhost` | Hostname or IP of the MQTT broker |
76
+ | `MQTT_PORT` | `1883` | Port of the broker |
77
+ | `MQTT_TRANSPORT` | `tcp` | Transport protocol (`tcp` or `websockets`) |
78
+ | `MQTT_USERNAME` | None | MQTT username if authentication is required |
79
+ | `MQTT_PASSWORD` | None | MQTT password |
80
+ | `MQTT_PROTOCOL` | `MQTTv311` | MQTT protocol version. Accepts (case-insensitive): `MQTTv31`/`v3.1`/`3.1`/`3` for v3.1, `MQTTv311`/`v3.1.1`/`3.1.1`/`4` for v3.1.1 (default), `MQTTv5`/`v5`/`5` for v5. An unrecognized value raises an error at startup rather than failing silently later. |
81
+ | `MQTT_QOS` | `0` | Quality of Service (0, 1, or 2) |
82
+ | `MQTT_PATH` | None | WebSocket path (if transport is `websockets`) |
83
+ | `MQTT_TOPIC_PREFIX` | `benchlab` | Prefix for published topics, e.g. `<prefix>/<device_uid>/telemetry` |
84
+ | `MQTT_POLL_RATE` | `1` (or `mqtt.config`'s `poll_rate`) | Seconds between telemetry publishes per device |
85
+
86
+ ### Poll Rate via Config File
87
+
88
+ The poll rate can also be set via a local `mqtt.config` file instead of `MQTT_POLL_RATE`. Copy `mqtt.config_template` to `mqtt.config` in this directory and edit the `poll_rate` value:
89
+
90
+ ```ini
91
+ [settings]
92
+ poll_rate = 0.5
93
+ ```
94
+
95
+ `MQTT_POLL_RATE`, if set, takes precedence over the config file.
96
+
97
+ ---
98
+
99
+ ## Usage
100
+
101
+ ### Run MQTT Mode
102
+
103
+ ```
104
+ python -m benchlab -mqtt
105
+ ```
106
+
107
+ `-mqtt` takes an optional positional broker hostname. If omitted, it defaults to `localhost`:
108
+
109
+ ```
110
+ python -m benchlab -mqtt mybroker.local
111
+ ```
112
+
113
+ The broker hostname can also be set via the `MQTT_BROKER` environment variable, or overridden with `--mqtt-broker`/`--mqtt-port` (used when this tool is launched together with other tools that consume the same MQTT source, e.g. via `--source mqtt`).
114
+
115
+ Behavior:
116
+
117
+ - Detects all connected Benchlab devices.
118
+ - Starts a separate thread for each device.
119
+ - Publishes telemetry continuously until interrupted (`Ctrl+C`).
120
+
121
+ ### MQTT Topics
122
+
123
+ Topics are published under a configurable prefix (`MQTT_TOPIC_PREFIX`, default `benchlab`):
124
+
125
+ #### Device Info
126
+
127
+ ```
128
+ <topic_prefix>/<device_uid>/info
129
+ ```
130
+
131
+ Published with `retain=True` so late subscribers can discover the device without waiting for the next info publish.
132
+
133
+ Payload:
134
+
135
+ ```
136
+ {
137
+ "uid": "<device_uid>",
138
+ "com_port": "<serial_port>",
139
+ "firmware": "<firmware_version>"
140
+ }
141
+ ```
142
+
143
+ #### Telemetry
144
+
145
+ ```
146
+ <topic_prefix>/<device_uid>/telemetry
147
+ ```
148
+
149
+ Payload:
150
+
151
+ ```
152
+ {
153
+ "timestamp": 1234567890.123,
154
+ "sensor1": 42.0,
155
+ "sensor2": 3.14,
156
+ ...
157
+ }
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Developer Notes
163
+
164
+ ### Threading
165
+
166
+ - Each device runs in its own thread (`device_thread`).
167
+ - Telemetry publishes at a configurable interval (`publish_interval`).
168
+ - Periodic logging of all connected devices runs in a separate thread (`log_connected_devices_periodically`).
169
+
170
+ ### Graceful Shutdown
171
+
172
+ - Controlled with the global `stop_event` (`threading.Event`).
173
+ - `Ctrl+C` triggers cleanup:
174
+ - All device threads join.
175
+ - Serial connections close.
176
+ - MQTT clients disconnect and stop loop.
177
+
178
+ ### Logging
179
+
180
+ - Supports JSON and plain logging via `JsonFormatter`.
181
+ - Logs contain timestamps, log level, messages, and optional exception info.
182
+
183
+ ### Error Handling
184
+
185
+ - Lost serial connection: retries every second.
186
+ - MQTT publish errors: logged with reason codes.
187
+ - Sensor translation errors: skipped with errors logged.
188
+
189
+ ---
190
+
191
+ ## Extending the Module
192
+
193
+ 1. **Add telemetry fields:** Extend `translate_sensor_struct()` in `benchlab.core.sensor_translation`.
194
+ 2. **Custom topics:** Modify `topic_info` and `topic_telemetry` in `device_thread`.
195
+ 3. **Additional MQTT features:** Implement `on_message` or `subscribe` callbacks if required.
196
+
197
+ ---
198
+
199
+ ## References
200
+
201
+ - [Paho-MQTT Python client](https://pypi.org/project/paho-mqtt/)
@@ -0,0 +1,5 @@
1
+ """
2
+ BENCHLAB Telemetry Package
3
+ """
4
+
5
+ __version__ = "0.9.0"
@@ -0,0 +1,2 @@
1
+ [settings]
2
+ poll_rate = 0.5