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
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BENCHLAB Configuration Tool - CLI Entry Point
|
|
3
|
+
|
|
4
|
+
Command-line interface for importing/exporting device configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .config_manager import ConfigManager
|
|
13
|
+
|
|
14
|
+
# Setup logging
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
format='%(levelname)s: %(message)s'
|
|
18
|
+
)
|
|
19
|
+
logger = logging.getLogger("benchlab.config.tool")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cmd_export(args):
|
|
23
|
+
"""Handle export command."""
|
|
24
|
+
manager = ConfigManager(source=args.source)
|
|
25
|
+
|
|
26
|
+
# Discover devices
|
|
27
|
+
devices = manager.discover_devices()
|
|
28
|
+
if not devices:
|
|
29
|
+
print("ERROR: No devices found")
|
|
30
|
+
return 1
|
|
31
|
+
|
|
32
|
+
# Select device
|
|
33
|
+
if args.device:
|
|
34
|
+
identifier = args.device
|
|
35
|
+
else:
|
|
36
|
+
# Use first device
|
|
37
|
+
if args.source == 'direct':
|
|
38
|
+
identifier = devices[0].get('port')
|
|
39
|
+
else:
|
|
40
|
+
identifier = devices[0].get('pipe')
|
|
41
|
+
print(f"Using device: {identifier}")
|
|
42
|
+
|
|
43
|
+
# Export configuration
|
|
44
|
+
if manager.export_config(identifier, args.output):
|
|
45
|
+
print(f"SUCCESS: Configuration exported to {args.output}")
|
|
46
|
+
return 0
|
|
47
|
+
else:
|
|
48
|
+
print("ERROR: Export failed")
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cmd_import(args):
|
|
53
|
+
"""Handle import command."""
|
|
54
|
+
if not Path(args.config_file).exists():
|
|
55
|
+
print(f"ERROR: Config file not found: {args.config_file}")
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
manager = ConfigManager(source=args.source)
|
|
59
|
+
|
|
60
|
+
if args.dry_run:
|
|
61
|
+
print("DRY RUN MODE - showing what would change, "
|
|
62
|
+
"no changes will be applied")
|
|
63
|
+
|
|
64
|
+
if manager.import_config(
|
|
65
|
+
args.config_file,
|
|
66
|
+
dry_run=args.dry_run,
|
|
67
|
+
auto_confirm=args.yes):
|
|
68
|
+
if not args.dry_run:
|
|
69
|
+
print("SUCCESS: Configuration applied")
|
|
70
|
+
return 0
|
|
71
|
+
else:
|
|
72
|
+
print("ERROR: Import failed")
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def cmd_list(args):
|
|
77
|
+
"""Handle list command."""
|
|
78
|
+
manager = ConfigManager(source=args.source)
|
|
79
|
+
devices = manager.discover_devices()
|
|
80
|
+
|
|
81
|
+
if not devices:
|
|
82
|
+
print("No devices found")
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
print(f"Found {len(devices)} device(s):")
|
|
86
|
+
print()
|
|
87
|
+
|
|
88
|
+
for i, device in enumerate(devices, 1):
|
|
89
|
+
if args.source == 'direct':
|
|
90
|
+
print(f"{i}. Port: {device.get('port')}")
|
|
91
|
+
print(f" UID: {device.get('uid', 'N/A')}")
|
|
92
|
+
print(f" FW: 0x{device.get('firmware', 0):08X}")
|
|
93
|
+
else:
|
|
94
|
+
print(f"{i}. Pipe: {device.get('pipe')}")
|
|
95
|
+
print(f" GUID: {device.get('guid', 'N/A')}")
|
|
96
|
+
print(f" Port: {device.get('port', 'N/A')}")
|
|
97
|
+
print(f" Name: {device.get('deviceName', 'N/A')}")
|
|
98
|
+
print()
|
|
99
|
+
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def interactive_mode(args):
|
|
104
|
+
"""Interactive mode for import/export operations."""
|
|
105
|
+
print("=" * 60)
|
|
106
|
+
print("BENCHLAB Configuration Tool - Interactive Mode")
|
|
107
|
+
print("=" * 60)
|
|
108
|
+
print()
|
|
109
|
+
|
|
110
|
+
# Debug: log the source being used
|
|
111
|
+
logger.info(f"Using data source: {args.source}")
|
|
112
|
+
|
|
113
|
+
# List available devices
|
|
114
|
+
manager = ConfigManager(source=args.source)
|
|
115
|
+
devices = manager.discover_devices()
|
|
116
|
+
|
|
117
|
+
if not devices:
|
|
118
|
+
print("ERROR: No devices found")
|
|
119
|
+
print()
|
|
120
|
+
input("Press Enter to exit...")
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
print(f"Found {len(devices)} device(s):")
|
|
124
|
+
for i, device in enumerate(devices, 1):
|
|
125
|
+
if args.source == 'direct':
|
|
126
|
+
print(f" {i}. {device.get('port')} - {device.get('uid', 'N/A')}")
|
|
127
|
+
else:
|
|
128
|
+
print(
|
|
129
|
+
f" {i}. {device.get('deviceName', 'N/A')} - "
|
|
130
|
+
f"{device.get('guid', 'N/A')}")
|
|
131
|
+
print()
|
|
132
|
+
|
|
133
|
+
# Ask for operation type
|
|
134
|
+
print("What would you like to do?")
|
|
135
|
+
print(" 1. Import configuration (apply JSON to device)")
|
|
136
|
+
print(" 2. Export configuration (save device config to JSON)")
|
|
137
|
+
print()
|
|
138
|
+
operation = input("Choice [1-2]: ").strip()
|
|
139
|
+
|
|
140
|
+
if operation == "1":
|
|
141
|
+
# Import mode
|
|
142
|
+
print()
|
|
143
|
+
print("Enter path to JSON configuration file:")
|
|
144
|
+
print("(or press Enter to cancel)")
|
|
145
|
+
config_file = input("> ").strip()
|
|
146
|
+
|
|
147
|
+
if not config_file:
|
|
148
|
+
print("Cancelled.")
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
if not Path(config_file).exists():
|
|
152
|
+
print(f"ERROR: File not found: {config_file}")
|
|
153
|
+
print()
|
|
154
|
+
input("Press Enter to exit...")
|
|
155
|
+
return 1
|
|
156
|
+
|
|
157
|
+
# Ask about saving to flash up front, so import_config's per-device
|
|
158
|
+
# diff/confirm below is the only remaining approval step.
|
|
159
|
+
print()
|
|
160
|
+
print("Save configuration to device flash memory?")
|
|
161
|
+
print("(If 'no', changes will only be applied to RAM "
|
|
162
|
+
"and lost on device reset)")
|
|
163
|
+
save_flash = input("Save to flash? (yes/no): ").strip().lower()
|
|
164
|
+
|
|
165
|
+
# Read + diff + confirm happens per device inside import_config,
|
|
166
|
+
# which prints the diff and prompts before applying each one.
|
|
167
|
+
print()
|
|
168
|
+
if manager.import_config(
|
|
169
|
+
config_file,
|
|
170
|
+
dry_run=False,
|
|
171
|
+
save_to_flash=(
|
|
172
|
+
save_flash in (
|
|
173
|
+
'yes',
|
|
174
|
+
'y'))):
|
|
175
|
+
print()
|
|
176
|
+
if save_flash in ('yes', 'y'):
|
|
177
|
+
print("SUCCESS: Configuration applied and saved to flash")
|
|
178
|
+
else:
|
|
179
|
+
print("SUCCESS: Configuration applied to RAM only")
|
|
180
|
+
print()
|
|
181
|
+
input("Press Enter to exit...")
|
|
182
|
+
return 0
|
|
183
|
+
else:
|
|
184
|
+
print()
|
|
185
|
+
print("ERROR: Configuration failed")
|
|
186
|
+
print()
|
|
187
|
+
input("Press Enter to exit...")
|
|
188
|
+
return 1
|
|
189
|
+
|
|
190
|
+
elif operation == "2":
|
|
191
|
+
# Export mode
|
|
192
|
+
print()
|
|
193
|
+
|
|
194
|
+
# Select device if multiple available
|
|
195
|
+
if len(devices) > 1:
|
|
196
|
+
print("Select device to export:")
|
|
197
|
+
for i, device in enumerate(devices, 1):
|
|
198
|
+
if args.source == 'direct':
|
|
199
|
+
print(
|
|
200
|
+
f" {i}. {device.get('port')} - "
|
|
201
|
+
f"{device.get('uid', 'N/A')}")
|
|
202
|
+
else:
|
|
203
|
+
print(
|
|
204
|
+
f" {i}. {device.get('deviceName', 'N/A')} - "
|
|
205
|
+
f"{device.get('guid', 'N/A')}")
|
|
206
|
+
print()
|
|
207
|
+
device_choice = input(f"Device [1-{len(devices)}]: ").strip()
|
|
208
|
+
try:
|
|
209
|
+
device_idx = int(device_choice) - 1
|
|
210
|
+
if not (0 <= device_idx < len(devices)):
|
|
211
|
+
print("Invalid device selection.")
|
|
212
|
+
return 1
|
|
213
|
+
except ValueError:
|
|
214
|
+
print("Invalid device selection.")
|
|
215
|
+
return 1
|
|
216
|
+
else:
|
|
217
|
+
device_idx = 0
|
|
218
|
+
|
|
219
|
+
selected_device = devices[device_idx]
|
|
220
|
+
if args.source == 'direct':
|
|
221
|
+
identifier = selected_device.get('port')
|
|
222
|
+
else:
|
|
223
|
+
identifier = selected_device.get('pipe')
|
|
224
|
+
|
|
225
|
+
print()
|
|
226
|
+
print("Enter output filename (e.g., my_config.json):")
|
|
227
|
+
output_file = input("> ").strip()
|
|
228
|
+
|
|
229
|
+
if not output_file:
|
|
230
|
+
print("Cancelled.")
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
# Add .json extension if not present
|
|
234
|
+
if not output_file.endswith('.json'):
|
|
235
|
+
output_file += '.json'
|
|
236
|
+
|
|
237
|
+
# Export configuration
|
|
238
|
+
print()
|
|
239
|
+
print(f"Exporting configuration from {identifier}...")
|
|
240
|
+
if manager.export_config(identifier, output_file):
|
|
241
|
+
print()
|
|
242
|
+
print(f"SUCCESS: Configuration exported to {output_file}")
|
|
243
|
+
print()
|
|
244
|
+
input("Press Enter to exit...")
|
|
245
|
+
return 0
|
|
246
|
+
else:
|
|
247
|
+
print()
|
|
248
|
+
print("ERROR: Export failed")
|
|
249
|
+
print()
|
|
250
|
+
input("Press Enter to exit...")
|
|
251
|
+
return 1
|
|
252
|
+
|
|
253
|
+
else:
|
|
254
|
+
print("Invalid choice.")
|
|
255
|
+
return 1
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def main(args=None):
|
|
259
|
+
"""Main entry point for config tool.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
args: Parsed arguments (if called from launcher)
|
|
263
|
+
"""
|
|
264
|
+
# If called without args, parse from command line
|
|
265
|
+
if args is None:
|
|
266
|
+
parser = argparse.ArgumentParser(
|
|
267
|
+
description='BENCHLAB Device Configuration Tool',
|
|
268
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
269
|
+
epilog="""
|
|
270
|
+
Examples:
|
|
271
|
+
# List devices
|
|
272
|
+
python -m benchlab -config --list
|
|
273
|
+
|
|
274
|
+
# Export configuration
|
|
275
|
+
python -m benchlab -config --export config.json
|
|
276
|
+
python -m benchlab -config --export config.json --device COM4
|
|
277
|
+
|
|
278
|
+
# Import configuration (shows a diff of what would change,
|
|
279
|
+
# then asks to confirm)
|
|
280
|
+
python -m benchlab -config --import config.json
|
|
281
|
+
|
|
282
|
+
# Preview changes without applying anything
|
|
283
|
+
python -m benchlab -config --import config.json --dry-run
|
|
284
|
+
|
|
285
|
+
# Apply without the confirmation prompt (for scripts/automation)
|
|
286
|
+
python -m benchlab -config --import config.json --yes
|
|
287
|
+
|
|
288
|
+
# Use named pipe source (Windows only)
|
|
289
|
+
python -m benchlab -config --list --source named_pipe
|
|
290
|
+
python -m benchlab -config --import config.json --source named_pipe
|
|
291
|
+
"""
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
parser.add_argument('--source', choices=['direct', 'named_pipe'],
|
|
295
|
+
default='direct',
|
|
296
|
+
help='Data source type (default: direct)')
|
|
297
|
+
|
|
298
|
+
# Commands
|
|
299
|
+
parser.add_argument('--list', action='store_true',
|
|
300
|
+
help='List available devices')
|
|
301
|
+
parser.add_argument('--export', metavar='FILE', dest='output',
|
|
302
|
+
help='Export configuration to JSON file')
|
|
303
|
+
parser.add_argument('--import', metavar='FILE', dest='config_file',
|
|
304
|
+
help='Import configuration from JSON file')
|
|
305
|
+
|
|
306
|
+
# Options
|
|
307
|
+
parser.add_argument('--device', metavar='ID',
|
|
308
|
+
help='Device identifier (port or pipe name)')
|
|
309
|
+
parser.add_argument(
|
|
310
|
+
'--dry-run',
|
|
311
|
+
action='store_true',
|
|
312
|
+
help='Show what would change without applying anything '
|
|
313
|
+
'(connects to the device and reads its current '
|
|
314
|
+
'config)')
|
|
315
|
+
parser.add_argument(
|
|
316
|
+
'-y',
|
|
317
|
+
'--yes',
|
|
318
|
+
action='store_true',
|
|
319
|
+
help='Apply changes without the confirmation prompt '
|
|
320
|
+
'(the diff is still shown)')
|
|
321
|
+
|
|
322
|
+
args = parser.parse_args()
|
|
323
|
+
else:
|
|
324
|
+
# Called from launcher - add missing command attributes
|
|
325
|
+
# Ensure source exists (should be set by launcher)
|
|
326
|
+
if not hasattr(args, 'source'):
|
|
327
|
+
args.source = 'direct'
|
|
328
|
+
if not hasattr(args, 'list'):
|
|
329
|
+
args.list = False
|
|
330
|
+
if not hasattr(args, 'output'):
|
|
331
|
+
args.output = None
|
|
332
|
+
if not hasattr(args, 'config_file'):
|
|
333
|
+
args.config_file = None
|
|
334
|
+
if not hasattr(args, 'device'):
|
|
335
|
+
args.device = None
|
|
336
|
+
if not hasattr(args, 'dry_run'):
|
|
337
|
+
args.dry_run = False
|
|
338
|
+
if not hasattr(args, 'yes'):
|
|
339
|
+
args.yes = False
|
|
340
|
+
|
|
341
|
+
# If no command specified, run interactive mode
|
|
342
|
+
if not (args.list or args.output or args.config_file):
|
|
343
|
+
return interactive_mode(args)
|
|
344
|
+
|
|
345
|
+
# Execute command
|
|
346
|
+
if args.list:
|
|
347
|
+
return cmd_list(args)
|
|
348
|
+
elif args.output:
|
|
349
|
+
return cmd_export(args)
|
|
350
|
+
elif args.config_file:
|
|
351
|
+
return cmd_import(args)
|
|
352
|
+
else:
|
|
353
|
+
print("ERROR: No command specified. Use --help for usage.")
|
|
354
|
+
return 1
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
if __name__ == '__main__':
|
|
358
|
+
sys.exit(main())
|
benchlab/config/diff.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Diff
|
|
3
|
+
|
|
4
|
+
Computes and renders a field-level diff between a device's current state
|
|
5
|
+
(as read by ConfigManager._read_current_state) and the desired state from
|
|
6
|
+
a config file, so users see exactly what would change before applying.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class FieldChange:
|
|
15
|
+
field: str
|
|
16
|
+
current: Any
|
|
17
|
+
desired: Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class DiffResult:
|
|
22
|
+
device_name_change: Optional[FieldChange] = None
|
|
23
|
+
# (profileId, fanId) -> list of FieldChange
|
|
24
|
+
fan_changes: Dict[Tuple[int, int], List[FieldChange]
|
|
25
|
+
] = field(default_factory=dict)
|
|
26
|
+
# profileId -> list of FieldChange
|
|
27
|
+
rgb_changes: Dict[int, List[FieldChange]] = field(default_factory=dict)
|
|
28
|
+
calibration_changed: bool = False
|
|
29
|
+
read_errors: List[str] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
def is_empty(self) -> bool:
|
|
32
|
+
return not (
|
|
33
|
+
self.device_name_change
|
|
34
|
+
or self.fan_changes
|
|
35
|
+
or self.rgb_changes
|
|
36
|
+
or self.calibration_changed
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _dict_diff(current: Dict[str, Any], desired: Dict[str,
|
|
41
|
+
Any], skip_keys=()) -> List[FieldChange]:
|
|
42
|
+
"""Return FieldChanges for keys present in *desired* whose value differs
|
|
43
|
+
from *current* (missing-in-current counts as a difference, e.g. first
|
|
44
|
+
import onto a fresh device)."""
|
|
45
|
+
changes = []
|
|
46
|
+
for k, desired_v in desired.items():
|
|
47
|
+
if k in skip_keys:
|
|
48
|
+
continue
|
|
49
|
+
current_v = current.get(k) if current else None
|
|
50
|
+
if current_v != desired_v:
|
|
51
|
+
changes.append(
|
|
52
|
+
FieldChange(
|
|
53
|
+
field=k,
|
|
54
|
+
current=current_v,
|
|
55
|
+
desired=desired_v))
|
|
56
|
+
return changes
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def compute_diff(current_state: Dict[str, Any],
|
|
60
|
+
desired_device_config) -> DiffResult:
|
|
61
|
+
"""Compare a device's current state against a DeviceConfig (pydantic
|
|
62
|
+
model).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
current_state: dict as returned by ConfigManager._read_current_state
|
|
66
|
+
(deviceName, fanProfiles, rgbProfiles, calibration,
|
|
67
|
+
readErrors)
|
|
68
|
+
desired_device_config: schema.DeviceConfig instance from the
|
|
69
|
+
loaded config file
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
DiffResult describing only the fields that differ.
|
|
73
|
+
"""
|
|
74
|
+
result = DiffResult(read_errors=list(current_state.get('readErrors', [])))
|
|
75
|
+
|
|
76
|
+
# Device name
|
|
77
|
+
if desired_device_config.deviceName is not None:
|
|
78
|
+
current_name = current_state.get('deviceName')
|
|
79
|
+
if current_name != desired_device_config.deviceName:
|
|
80
|
+
result.device_name_change = FieldChange(
|
|
81
|
+
field='deviceName',
|
|
82
|
+
current=current_name,
|
|
83
|
+
desired=desired_device_config.deviceName)
|
|
84
|
+
|
|
85
|
+
# Fan profiles: index current by (profileId, fanId)
|
|
86
|
+
current_fans: Dict[Tuple[int, int], Dict[str, Any]] = {}
|
|
87
|
+
for profile in (current_state.get('fanProfiles') or []):
|
|
88
|
+
pid = profile.get('profileId')
|
|
89
|
+
for fan in profile.get('fans', []):
|
|
90
|
+
current_fans[(pid, fan.get('fanId'))] = fan
|
|
91
|
+
|
|
92
|
+
for profile in (desired_device_config.fanProfiles or []):
|
|
93
|
+
for fan in profile.fans:
|
|
94
|
+
key = (profile.profileId, fan.fanId)
|
|
95
|
+
fan_dict = fan.model_dump()
|
|
96
|
+
changes = _dict_diff(
|
|
97
|
+
current_fans.get(
|
|
98
|
+
key, {}), fan_dict, skip_keys=(
|
|
99
|
+
'fanId',))
|
|
100
|
+
if changes:
|
|
101
|
+
result.fan_changes[key] = changes
|
|
102
|
+
|
|
103
|
+
# RGB profiles: index current by profileId
|
|
104
|
+
current_rgb: Dict[int, Dict[str, Any]] = {
|
|
105
|
+
rgb.get('profileId'): rgb
|
|
106
|
+
for rgb in (current_state.get('rgbProfiles') or [])
|
|
107
|
+
}
|
|
108
|
+
for rgb in (desired_device_config.rgbProfiles or []):
|
|
109
|
+
rgb_dict = rgb.model_dump()
|
|
110
|
+
changes = _dict_diff(
|
|
111
|
+
current_rgb.get(
|
|
112
|
+
rgb.profileId, {}), rgb_dict, skip_keys=(
|
|
113
|
+
'profileId',))
|
|
114
|
+
if changes:
|
|
115
|
+
result.rgb_changes[rgb.profileId] = changes
|
|
116
|
+
|
|
117
|
+
# Calibration: whole-blob comparison, not meant to be hand-edited field by
|
|
118
|
+
# field
|
|
119
|
+
if desired_device_config.calibration is not None:
|
|
120
|
+
if current_state.get(
|
|
121
|
+
'calibration') != desired_device_config.calibration:
|
|
122
|
+
result.calibration_changed = True
|
|
123
|
+
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def format_diff(diff: DiffResult, device_label: str) -> str:
|
|
128
|
+
"""Render a DiffResult as human-readable text."""
|
|
129
|
+
lines = [f"Changes to be applied ({device_label}):"]
|
|
130
|
+
has_content = False
|
|
131
|
+
|
|
132
|
+
if diff.device_name_change:
|
|
133
|
+
has_content = True
|
|
134
|
+
c = diff.device_name_change
|
|
135
|
+
lines.append(" Device Name:")
|
|
136
|
+
lines.append(f" {c.current!r} -> {c.desired!r}")
|
|
137
|
+
|
|
138
|
+
for (profile_id, fan_id), changes in sorted(diff.fan_changes.items()):
|
|
139
|
+
has_content = True
|
|
140
|
+
lines.append(f" Fan {fan_id} Profile {profile_id}:")
|
|
141
|
+
for c in changes:
|
|
142
|
+
lines.append(f" {c.field}: {c.current} -> {c.desired}")
|
|
143
|
+
|
|
144
|
+
for profile_id, changes in sorted(diff.rgb_changes.items()):
|
|
145
|
+
has_content = True
|
|
146
|
+
lines.append(f" RGB Profile {profile_id}:")
|
|
147
|
+
for c in changes:
|
|
148
|
+
lines.append(f" {c.field}: {c.current} -> {c.desired}")
|
|
149
|
+
|
|
150
|
+
if diff.calibration_changed:
|
|
151
|
+
has_content = True
|
|
152
|
+
lines.append(" Calibration: (changed)")
|
|
153
|
+
|
|
154
|
+
if not has_content:
|
|
155
|
+
lines.append(" No changes.")
|
|
156
|
+
|
|
157
|
+
if diff.read_errors:
|
|
158
|
+
lines.append("")
|
|
159
|
+
lines.append(" Warning: could not read current state for:")
|
|
160
|
+
for err in diff.read_errors:
|
|
161
|
+
lines.append(f" - {err}")
|
|
162
|
+
|
|
163
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"description": "Complete device configuration - Fan curves, RGB, and device name",
|
|
4
|
+
"devices": [
|
|
5
|
+
{
|
|
6
|
+
"selector": {
|
|
7
|
+
"type": "any",
|
|
8
|
+
"value": null
|
|
9
|
+
},
|
|
10
|
+
"deviceName": "BENCHLAB_GAMING",
|
|
11
|
+
"fanProfiles": [
|
|
12
|
+
{
|
|
13
|
+
"profileId": 0,
|
|
14
|
+
"fans": [
|
|
15
|
+
{
|
|
16
|
+
"fanId": 0,
|
|
17
|
+
"FanMode": 0,
|
|
18
|
+
"TempSource": 0,
|
|
19
|
+
"Temp": [300, 600],
|
|
20
|
+
"Duty": [30, 80],
|
|
21
|
+
"RampStep": 5,
|
|
22
|
+
"FixedDuty": 50,
|
|
23
|
+
"MinDuty": 20,
|
|
24
|
+
"MaxDuty": 100,
|
|
25
|
+
"FanStop": 0
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"fanId": 1,
|
|
29
|
+
"FanMode": 0,
|
|
30
|
+
"TempSource": 0,
|
|
31
|
+
"Temp": [350, 650],
|
|
32
|
+
"Duty": [35, 85],
|
|
33
|
+
"RampStep": 5,
|
|
34
|
+
"FixedDuty": 50,
|
|
35
|
+
"MinDuty": 25,
|
|
36
|
+
"MaxDuty": 100,
|
|
37
|
+
"FanStop": 0
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"rgbProfiles": [
|
|
43
|
+
{
|
|
44
|
+
"profileId": 0,
|
|
45
|
+
"Mode": 2,
|
|
46
|
+
"Red": 255,
|
|
47
|
+
"Green": 255,
|
|
48
|
+
"Blue": 255,
|
|
49
|
+
"Direction": 0,
|
|
50
|
+
"Speed": 75
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"saveToFlash": true
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"description": "RGB lighting - Set to solid blue",
|
|
4
|
+
"devices": [
|
|
5
|
+
{
|
|
6
|
+
"selector": {
|
|
7
|
+
"type": "any",
|
|
8
|
+
"value": null
|
|
9
|
+
},
|
|
10
|
+
"rgbProfiles": [
|
|
11
|
+
{
|
|
12
|
+
"profileId": 0,
|
|
13
|
+
"Mode": 9,
|
|
14
|
+
"Red": 0,
|
|
15
|
+
"Green": 0,
|
|
16
|
+
"Blue": 255,
|
|
17
|
+
"Direction": 0,
|
|
18
|
+
"Speed": 50
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"saveToFlash": true
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"description": "Simple fan control - Set all fans to 50% fixed speed",
|
|
4
|
+
"devices": [
|
|
5
|
+
{
|
|
6
|
+
"selector": {
|
|
7
|
+
"type": "any",
|
|
8
|
+
"value": null
|
|
9
|
+
},
|
|
10
|
+
"fanProfiles": [
|
|
11
|
+
{
|
|
12
|
+
"profileId": 0,
|
|
13
|
+
"fans": [
|
|
14
|
+
{
|
|
15
|
+
"fanId": 0,
|
|
16
|
+
"FanMode": 1,
|
|
17
|
+
"TempSource": 0,
|
|
18
|
+
"Temp": [300, 600],
|
|
19
|
+
"Duty": [30, 80],
|
|
20
|
+
"RampStep": 5,
|
|
21
|
+
"FixedDuty": 50,
|
|
22
|
+
"MinDuty": 20,
|
|
23
|
+
"MaxDuty": 100,
|
|
24
|
+
"FanStop": 0
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"saveToFlash": true
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|