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.
- benchlab/__init__.py +14 -0
- benchlab/__main__.py +21 -0
- benchlab/config/README.md +325 -0
- benchlab/config/__init__.py +9 -0
- benchlab/config/config_client.py +631 -0
- benchlab/config/config_manager.py +495 -0
- benchlab/config/config_tool.py +358 -0
- benchlab/config/diff.py +163 -0
- benchlab/config/examples/complete_config.json +56 -0
- benchlab/config/examples/rgb_lighting.json +24 -0
- benchlab/config/examples/simple_fan.json +32 -0
- benchlab/config/schema.py +138 -0
- benchlab/core/README.md +154 -0
- benchlab/core/__init__.py +78 -0
- benchlab/core/config.py +145 -0
- benchlab/core/datasource.py +1209 -0
- benchlab/core/datasource_manager.py +418 -0
- benchlab/core/device_registry.py +206 -0
- benchlab/core/discovery.py +92 -0
- benchlab/core/infrastructure.py +309 -0
- benchlab/core/process_manager.py +356 -0
- benchlab/core/retry.py +85 -0
- benchlab/core/shared_serial.py +235 -0
- benchlab/core/statistics.py +224 -0
- benchlab/csv_log/README.md +206 -0
- benchlab/csv_log/__init__.py +5 -0
- benchlab/csv_log/csv_logger_enhanced.py +416 -0
- benchlab/csv_log/message_batcher.py +361 -0
- benchlab/csv_log/requirements.txt +3 -0
- benchlab/csv_log/smart_retry.py +466 -0
- benchlab/graph/README.md +117 -0
- benchlab/graph/__init__.py +5 -0
- benchlab/graph/app.py +353 -0
- benchlab/graph/requirements.txt +2 -0
- benchlab/graph/runner.py +42 -0
- benchlab/graph/ui.py +288 -0
- benchlab/hwinfo/README.md +135 -0
- benchlab/hwinfo/__init__.py +5 -0
- benchlab/hwinfo/hwinfo_export.py +386 -0
- benchlab/hwinfo/requirements.txt +1 -0
- benchlab/launcher.py +387 -0
- benchlab/link/.env.EXAMPLE +10 -0
- benchlab/link/README.md +172 -0
- benchlab/link/__init__.py +1 -0
- benchlab/link/link_main.py +479 -0
- benchlab/main.py +462 -0
- benchlab/menu.py +729 -0
- benchlab/menu_classic.py +410 -0
- benchlab/mqtt/README.md +201 -0
- benchlab/mqtt/__init__.py +5 -0
- benchlab/mqtt/mqtt.config_template +2 -0
- benchlab/mqtt/mqtt_publisher.py +540 -0
- benchlab/mqtt/requirements.txt +3 -0
- benchlab/prefs.py +67 -0
- benchlab/restapi/.env.example +17 -0
- benchlab/restapi/__init__.py +5 -0
- benchlab/restapi/favicon.ico +0 -0
- benchlab/restapi/readme.md +202 -0
- benchlab/restapi/requirements.txt +5 -0
- benchlab/restapi/telemetry_api.py +808 -0
- benchlab/sources.py +515 -0
- benchlab/tools.py +147 -0
- benchlab/tui/README.md +266 -0
- benchlab/tui/__init__.py +5 -0
- benchlab/tui/config.py +477 -0
- benchlab/tui/requirements.txt +2 -0
- benchlab/tui/tui_core.py +1298 -0
- benchlab/tui/tui_main.py +418 -0
- benchlab/vu/README.md +64 -0
- benchlab/vu/VU-Server/NOTICE.md +13 -0
- benchlab/vu/VU-Server/README.md +80 -0
- benchlab/vu/VU-Server/Running_from_source_on_linux.md +97 -0
- benchlab/vu/VU-Server/__init__.py +0 -0
- benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
- benchlab/vu/VU-Server/community_applications.md +13 -0
- benchlab/vu/VU-Server/config.yaml +9 -0
- benchlab/vu/VU-Server/database.py +256 -0
- benchlab/vu/VU-Server/dial_driver.py +534 -0
- benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +78 -0
- benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +100 -0
- benchlab/vu/VU-Server/dials/__init__.py +0 -0
- benchlab/vu/VU-Server/dials/base_logger.py +100 -0
- benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
- benchlab/vu/VU-Server/installer/install.nsi +148 -0
- benchlab/vu/VU-Server/make_version.py +40 -0
- benchlab/vu/VU-Server/pylintrc +628 -0
- benchlab/vu/VU-Server/requirements.txt +9 -0
- benchlab/vu/VU-Server/serial_driver.py +244 -0
- benchlab/vu/VU-Server/server.py +670 -0
- benchlab/vu/VU-Server/server_config.py +255 -0
- benchlab/vu/VU-Server/server_dial_handler.py +334 -0
- benchlab/vu/VU-Server/vu_notifications.py +24 -0
- benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
- benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
- benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
- benchlab/vu/VU-Server/www/assets/css/demo.css +291 -0
- benchlab/vu/VU-Server/www/assets/css/demo.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +276 -0
- benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +1084 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +1084 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +528 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +528 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.css +10 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +8 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +10 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +8 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +1504 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +1504 -0
- benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +9 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.css +25813 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.min.css +13 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +25777 -0
- benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +13 -0
- benchlab/vu/VU-Server/www/assets/css/theme.min.css +261 -0
- benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
- benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
- benchlab/vu/VU-Server/www/assets/js/dark-theme.js +35 -0
- benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +9 -0
- benchlab/vu/VU-Server/www/assets/js/demo.js +132 -0
- benchlab/vu/VU-Server/www/assets/js/demo.min.js +9 -0
- benchlab/vu/VU-Server/www/assets/js/jquery.min.js +2 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +7482 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +15 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.js +7543 -0
- benchlab/vu/VU-Server/www/assets/js/tabler.min.js +15 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_api.js +68 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +167 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +138 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +179 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +60 -0
- benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +125 -0
- benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
- benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
- benchlab/vu/VU-Server/www/favicon.ico +0 -0
- benchlab/vu/VU-Server/www/favicon.png +0 -0
- benchlab/vu/VU-Server/www/index.html +47 -0
- benchlab/vu/VU-Server/www/views/api_keys.html +151 -0
- benchlab/vu/VU-Server/www/views/dial.html +286 -0
- benchlab/vu/VU-Server/www/views/footer.html +27 -0
- benchlab/vu/VU-Server/www/views/header.html +86 -0
- benchlab/vu/VU-Server/www/views/key_settings.html +132 -0
- benchlab/vu/VU-Server/www/views/start.html +124 -0
- benchlab/vu/__init__.py +5 -0
- benchlab/vu/assets/Barlow-Bold.ttf +0 -0
- benchlab/vu/assets/bl_dial_144x200.png +0 -0
- benchlab/vu/assets/bl_logo_144x200.png +0 -0
- benchlab/vu/devices.py +166 -0
- benchlab/vu/requirements.txt +24 -0
- benchlab/vu/sensors.py +18 -0
- benchlab/vu/vu_dial.config_template +90 -0
- benchlab/vu/vu_logo_gen.py +97 -0
- benchlab/vu/vu_server.config_template +5 -0
- benchlab/vu/vu_server_config.py +40 -0
- benchlab/vu/vu_server_manager.py +192 -0
- benchlab/vu/vu_tui.py +919 -0
- benchlab/vu/vu_updater.py +540 -0
- benchlab/wigidash/README.md +182 -0
- benchlab/wigidash/__init__.py +5 -0
- benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
- benchlab/wigidash/assets/Inter.ttf +0 -0
- benchlab/wigidash/assets/benchlab.png +0 -0
- benchlab/wigidash/benchlab_fleet.py +150 -0
- benchlab/wigidash/benchlab_graph.py +517 -0
- benchlab/wigidash/benchlab_overview.py +426 -0
- benchlab/wigidash/benchlab_telemetry.py +105 -0
- benchlab/wigidash/benchlab_ui.py +190 -0
- benchlab/wigidash/benchlab_utils.py +182 -0
- benchlab/wigidash/requirements.txt +9 -0
- benchlab/wigidash/wigidash_device.py +541 -0
- benchlab/wigidash/wigidash_manager.py +342 -0
- benchlab/wigidash/wigidash_session.py +355 -0
- benchlab/wigidash/wigidash_usb.py +247 -0
- benchlab/wigidash/wigidash_widget.py +88 -0
- benchlab_pytools-3.0.0.dist-info/METADATA +239 -0
- benchlab_pytools-3.0.0.dist-info/RECORD +184 -0
- benchlab_pytools-3.0.0.dist-info/WHEEL +4 -0
- benchlab_pytools-3.0.0.dist-info/entry_points.txt +2 -0
- benchlab_pytools-3.0.0.dist-info/licenses/LICENSE +21 -0
benchlab/menu_classic.py
ADDED
|
@@ -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
|
benchlab/mqtt/README.md
ADDED
|
@@ -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/)
|