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/vu/vu_tui.py
ADDED
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
# benchlab/vu/vu_tui.py
|
|
2
|
+
import curses
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import requests
|
|
6
|
+
import time
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from benchlab.vu import devices
|
|
13
|
+
from benchlab.vu.vu_server_manager import (
|
|
14
|
+
start_vu_server, check_vu_server, terminate_vu_server, forward_logs)
|
|
15
|
+
|
|
16
|
+
# --- Logger setup ---
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
logger.setLevel(logging.DEBUG)
|
|
19
|
+
|
|
20
|
+
# Make sure logs go to console
|
|
21
|
+
ch = logging.StreamHandler(sys.stdout)
|
|
22
|
+
ch.setLevel(logging.INFO)
|
|
23
|
+
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
|
|
24
|
+
ch.setFormatter(formatter)
|
|
25
|
+
logger.addHandler(ch)
|
|
26
|
+
|
|
27
|
+
VU_SERVER_CONFIG = Path(__file__).parent / "vu_server.config"
|
|
28
|
+
VU_DIAL_CONFIG = Path(__file__).parent / "vu_dial.config"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_json(path, default=None):
|
|
32
|
+
if path.exists():
|
|
33
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
34
|
+
return json.load(f)
|
|
35
|
+
return default if default is not None else {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def save_json(path, data):
|
|
39
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
40
|
+
json.dump(data, f, indent=2)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _parse_float_or(value, fallback):
|
|
44
|
+
"""Parse *value* as a float, returning *fallback* on empty/invalid input
|
|
45
|
+
instead of raising — matches the existing guarded-input pattern used
|
|
46
|
+
elsewhere in this TUI (e.g. tab2's int() cast, sensor selection)."""
|
|
47
|
+
if not value:
|
|
48
|
+
return fallback
|
|
49
|
+
try:
|
|
50
|
+
return float(value)
|
|
51
|
+
except ValueError:
|
|
52
|
+
return fallback
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_available_sensors(datasource=None) -> list:
|
|
56
|
+
"""Return sensor keys from a live datasource snapshot.
|
|
57
|
+
Falls back to an empty list if no data is available yet.
|
|
58
|
+
"""
|
|
59
|
+
if datasource is None:
|
|
60
|
+
return []
|
|
61
|
+
try:
|
|
62
|
+
raw = datasource.list_devices()
|
|
63
|
+
if isinstance(raw, dict):
|
|
64
|
+
uids = list(raw.keys())
|
|
65
|
+
else:
|
|
66
|
+
uids = [d.get("uid") for d in raw if d.get("uid")]
|
|
67
|
+
if not uids:
|
|
68
|
+
return []
|
|
69
|
+
datasource.select_device(uids[0])
|
|
70
|
+
snap = datasource.snapshot()
|
|
71
|
+
data = snap.get("sensor_data") or snap.get(
|
|
72
|
+
"all_telemetry", {}).get(uids[0], {})
|
|
73
|
+
return [k for k in data if k.lower() != "timestamp"]
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.warning(f"Could not get sensor list from datasource: {e}")
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class VUTUI:
|
|
80
|
+
def __init__(self, stdscr, server_proc=None, datasource=None):
|
|
81
|
+
self.stdscr = stdscr
|
|
82
|
+
self.running = True
|
|
83
|
+
self.server_proc = server_proc
|
|
84
|
+
self.datasource = datasource
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
curses.curs_set(0)
|
|
88
|
+
except BaseException:
|
|
89
|
+
pass
|
|
90
|
+
self.stdscr.nodelay(False)
|
|
91
|
+
|
|
92
|
+
# Tabs
|
|
93
|
+
self.tab_index = 0
|
|
94
|
+
self.tab2_cursor = 0
|
|
95
|
+
self.tab2_start_line = 0
|
|
96
|
+
self.tab3_cursor = 0
|
|
97
|
+
|
|
98
|
+
# Colors
|
|
99
|
+
curses.start_color()
|
|
100
|
+
curses.use_default_colors()
|
|
101
|
+
curses.init_pair(
|
|
102
|
+
1,
|
|
103
|
+
curses.COLOR_WHITE,
|
|
104
|
+
curses.COLOR_YELLOW) # Active tab
|
|
105
|
+
curses.init_pair(2, curses.COLOR_GREEN, -
|
|
106
|
+
1) # Status OK
|
|
107
|
+
curses.init_pair(3, curses.COLOR_RED, -
|
|
108
|
+
1) # Warnings/errors
|
|
109
|
+
curses.init_pair(4, curses.COLOR_MAGENTA, -
|
|
110
|
+
1) # Temperature
|
|
111
|
+
# Inactive tab
|
|
112
|
+
curses.init_pair(6, curses.COLOR_WHITE, -1)
|
|
113
|
+
curses.init_pair(
|
|
114
|
+
7,
|
|
115
|
+
curses.COLOR_BLACK,
|
|
116
|
+
curses.COLOR_CYAN) # Title bar
|
|
117
|
+
|
|
118
|
+
# Load configs & devices
|
|
119
|
+
self.reload_configs()
|
|
120
|
+
self.SENSORS = get_available_sensors(self.datasource)
|
|
121
|
+
|
|
122
|
+
# -------------------- CLEANUP --------------------
|
|
123
|
+
def cleanup(self):
|
|
124
|
+
"""Close benchlab serial ports. The auto-started VU server process
|
|
125
|
+
(if any) is terminated separately by the caller (run_vu_tui), which
|
|
126
|
+
owns server_proc — not tracked on self.
|
|
127
|
+
"""
|
|
128
|
+
logger.info("Starting cleanup...")
|
|
129
|
+
|
|
130
|
+
# Close benchlab ports
|
|
131
|
+
for b in getattr(self, "benchlabs", []):
|
|
132
|
+
try:
|
|
133
|
+
if hasattr(b, "close") and callable(b.close):
|
|
134
|
+
b.close()
|
|
135
|
+
logger.info(
|
|
136
|
+
f"Closed serial port for benchlab {
|
|
137
|
+
b.get(
|
|
138
|
+
'port', '?')}")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.warning(
|
|
141
|
+
f"Failed to close port {
|
|
142
|
+
b.get(
|
|
143
|
+
'port',
|
|
144
|
+
'?')}: {e}")
|
|
145
|
+
|
|
146
|
+
logger.info("Cleanup complete.")
|
|
147
|
+
|
|
148
|
+
# -------------------- CONFIG / DIALS --------------------
|
|
149
|
+
def reload_configs(self):
|
|
150
|
+
template_server = VU_SERVER_CONFIG.parent / "vu_server.config_template"
|
|
151
|
+
template_dial = VU_DIAL_CONFIG.parent / "vu_dial.config_template"
|
|
152
|
+
|
|
153
|
+
if not VU_SERVER_CONFIG.exists():
|
|
154
|
+
src = template_server if template_server.exists() else None
|
|
155
|
+
cfg = load_json(
|
|
156
|
+
src,
|
|
157
|
+
{
|
|
158
|
+
"vu_server_url": "http://localhost:5340",
|
|
159
|
+
"api_key": "",
|
|
160
|
+
"logo_file": ""}) if src else {
|
|
161
|
+
"vu_server_url": "http://localhost:5340",
|
|
162
|
+
"api_key": "",
|
|
163
|
+
"logo_file": ""}
|
|
164
|
+
save_json(VU_SERVER_CONFIG, cfg)
|
|
165
|
+
logger.info(
|
|
166
|
+
"Created VU server config from template"
|
|
167
|
+
if src else "Created default VU server config")
|
|
168
|
+
|
|
169
|
+
if not VU_DIAL_CONFIG.exists():
|
|
170
|
+
if template_dial.exists():
|
|
171
|
+
entries = load_json(template_dial, [])
|
|
172
|
+
for e in entries:
|
|
173
|
+
e["benchlab_port"] = ""
|
|
174
|
+
e["benchlab_uid"] = ""
|
|
175
|
+
e["sensor"] = ""
|
|
176
|
+
save_json(VU_DIAL_CONFIG, entries)
|
|
177
|
+
logger.info("Created VU dial config from template")
|
|
178
|
+
else:
|
|
179
|
+
save_json(VU_DIAL_CONFIG, [])
|
|
180
|
+
logger.info("Created empty VU dial config")
|
|
181
|
+
|
|
182
|
+
self.server_cfg = load_json(VU_SERVER_CONFIG, {})
|
|
183
|
+
self.dial_cfg = load_json(VU_DIAL_CONFIG, [])
|
|
184
|
+
self.benchlabs = devices.get_benchlab_devices(self.datasource)
|
|
185
|
+
|
|
186
|
+
self.vu_server_running = devices.vu_server_check(
|
|
187
|
+
self.server_cfg.get("vu_server_url", "http://localhost:5340"),
|
|
188
|
+
self.server_cfg.get("api_key", "")
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if self.vu_server_running:
|
|
192
|
+
self.vu_dials = devices.get_vu_dials(
|
|
193
|
+
self.server_cfg.get("vu_server_url", "http://localhost:5340"),
|
|
194
|
+
self.server_cfg.get("api_key", "")
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
self.vu_dials = []
|
|
198
|
+
|
|
199
|
+
# Default config for unconfigured dials
|
|
200
|
+
for uid, name in self.vu_dials:
|
|
201
|
+
if not any(d.get("dial_uid") == uid for d in self.dial_cfg):
|
|
202
|
+
self.dial_cfg.append({
|
|
203
|
+
"dial_uid": uid,
|
|
204
|
+
"dial_name": name,
|
|
205
|
+
"benchlab_port": "Unknown",
|
|
206
|
+
"benchlab_uid": "",
|
|
207
|
+
"sensor": "",
|
|
208
|
+
"min": 0,
|
|
209
|
+
"max": 100,
|
|
210
|
+
"backlight": [100, 100, 100],
|
|
211
|
+
"easing_dial": [50, 5],
|
|
212
|
+
"easing_backlight": [50, 5]
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
def update_dial_name_on_server(self, dial_uid, new_name):
|
|
216
|
+
url = f"{
|
|
217
|
+
self.server_cfg.get(
|
|
218
|
+
'vu_server_url',
|
|
219
|
+
'http://localhost:5340')}/api/v0/dial/{dial_uid}/name"
|
|
220
|
+
params = {
|
|
221
|
+
"key": self.server_cfg.get("api_key", ""),
|
|
222
|
+
"name": new_name
|
|
223
|
+
}
|
|
224
|
+
try:
|
|
225
|
+
resp = requests.get(url, params=params, timeout=3)
|
|
226
|
+
if resp.status_code in (200, 201):
|
|
227
|
+
return True
|
|
228
|
+
else:
|
|
229
|
+
logger.warning(
|
|
230
|
+
f"Failed to update dial {dial_uid} name on server: {
|
|
231
|
+
resp.status_code} {
|
|
232
|
+
resp.text}")
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.warning(
|
|
235
|
+
f"Exception updating dial {dial_uid} name on server: {e}")
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
# -------------------- HELPERS --------------------
|
|
239
|
+
def get_or_create_cfg_entry(self, dial):
|
|
240
|
+
cfg_entry = next(
|
|
241
|
+
(d for d in self.dial_cfg if d['dial_uid'] == dial['uid']), None)
|
|
242
|
+
if cfg_entry:
|
|
243
|
+
return cfg_entry
|
|
244
|
+
cfg_entry = {
|
|
245
|
+
"dial_uid": dial['uid'],
|
|
246
|
+
"dial_name": dial['name'],
|
|
247
|
+
"benchlab_port": dial.get("benchlab_port", "Unknown"),
|
|
248
|
+
"benchlab_uid": dial.get("benchlab_uid", ""),
|
|
249
|
+
"sensor": dial.get("sensor", "Not configured"),
|
|
250
|
+
"min": 0,
|
|
251
|
+
"max": 100,
|
|
252
|
+
"backlight": [100, 100, 100],
|
|
253
|
+
"easing_dial": [50, 5],
|
|
254
|
+
"easing_backlight": [50, 5]
|
|
255
|
+
}
|
|
256
|
+
self.dial_cfg.append(cfg_entry)
|
|
257
|
+
return cfg_entry
|
|
258
|
+
|
|
259
|
+
# -------------------- TABS --------------------
|
|
260
|
+
def draw_tabs(self):
|
|
261
|
+
tabs = ["Overview", "Server Config", "Dial Mapping"]
|
|
262
|
+
x = 0
|
|
263
|
+
y = 2 # always draw tabs on line 2
|
|
264
|
+
self.h, self.w = self.stdscr.getmaxyx()
|
|
265
|
+
|
|
266
|
+
for idx, name in enumerate(tabs):
|
|
267
|
+
if idx == self.tab_index:
|
|
268
|
+
self.stdscr.attron(curses.color_pair(1))
|
|
269
|
+
self.stdscr.addstr(y, x, f" {name} ")
|
|
270
|
+
self.stdscr.attroff(curses.color_pair(1))
|
|
271
|
+
else:
|
|
272
|
+
self.stdscr.attron(curses.color_pair(6))
|
|
273
|
+
self.stdscr.addstr(y, x, f" {name} ")
|
|
274
|
+
self.stdscr.attroff(curses.color_pair(6))
|
|
275
|
+
x += len(name) + 3 # spacing
|
|
276
|
+
|
|
277
|
+
# -------------------- TAB 1 --------------------
|
|
278
|
+
def draw_tab1(self):
|
|
279
|
+
self.stdscr.clear()
|
|
280
|
+
self.h, self.w = self.stdscr.getmaxyx()
|
|
281
|
+
y_start = 4
|
|
282
|
+
y = y_start
|
|
283
|
+
|
|
284
|
+
# --- Header ---
|
|
285
|
+
self.stdscr.attron(curses.color_pair(7))
|
|
286
|
+
self.stdscr.addstr(0, 0, " BENCHLAB VU CONFIGURATION ".center(self.w))
|
|
287
|
+
self.stdscr.attroff(curses.color_pair(7))
|
|
288
|
+
|
|
289
|
+
self.draw_tabs()
|
|
290
|
+
|
|
291
|
+
# --- VU Server Config ---
|
|
292
|
+
self.stdscr.addstr(y, 2, "VU Server Config:", curses.A_UNDERLINE)
|
|
293
|
+
y += 1
|
|
294
|
+
keys = list(self.server_cfg.keys())
|
|
295
|
+
key_col_width = max((len(k) for k in keys), default=0) + 2
|
|
296
|
+
for k, v in self.server_cfg.items():
|
|
297
|
+
try:
|
|
298
|
+
self.stdscr.addstr(y, 4, f"{k}: ")
|
|
299
|
+
self.stdscr.addstr(y, 4 + key_col_width, f"{v}")
|
|
300
|
+
except curses.error:
|
|
301
|
+
break
|
|
302
|
+
y += 1
|
|
303
|
+
y += 1
|
|
304
|
+
|
|
305
|
+
# --- Benchlab devices ---
|
|
306
|
+
self.stdscr.addstr(
|
|
307
|
+
y,
|
|
308
|
+
2,
|
|
309
|
+
"Available Benchlab Devices:",
|
|
310
|
+
curses.A_UNDERLINE)
|
|
311
|
+
y += 1
|
|
312
|
+
for b in self.benchlabs:
|
|
313
|
+
try:
|
|
314
|
+
self.stdscr.addstr(y, 4, f"{b['uid']} - {b['port']}")
|
|
315
|
+
except curses.error:
|
|
316
|
+
break
|
|
317
|
+
y += 1
|
|
318
|
+
y += 1
|
|
319
|
+
|
|
320
|
+
# --- VU Dials ---
|
|
321
|
+
self.stdscr.addstr(y, 2, "Available VU Dials:", curses.A_UNDERLINE)
|
|
322
|
+
y += 1
|
|
323
|
+
if self.vu_server_running:
|
|
324
|
+
for uid, name in self.vu_dials:
|
|
325
|
+
try:
|
|
326
|
+
self.stdscr.addstr(y, 4, f"{uid} - {name}")
|
|
327
|
+
except curses.error:
|
|
328
|
+
break
|
|
329
|
+
y += 1
|
|
330
|
+
else:
|
|
331
|
+
try:
|
|
332
|
+
self.stdscr.addstr(
|
|
333
|
+
y,
|
|
334
|
+
4,
|
|
335
|
+
"N/A: VU server is not running!",
|
|
336
|
+
curses.color_pair(3))
|
|
337
|
+
except curses.error:
|
|
338
|
+
pass
|
|
339
|
+
y += 1
|
|
340
|
+
y += 1
|
|
341
|
+
|
|
342
|
+
# --- VU Dial Config Table ---
|
|
343
|
+
self.stdscr.addstr(y, 2, "VU Dial Config:", curses.A_UNDERLINE)
|
|
344
|
+
y += 1
|
|
345
|
+
|
|
346
|
+
if not self.dial_cfg:
|
|
347
|
+
try:
|
|
348
|
+
self.stdscr.addstr(
|
|
349
|
+
y, 4, "No VU dials configured", curses.color_pair(3))
|
|
350
|
+
except curses.error:
|
|
351
|
+
pass
|
|
352
|
+
y += 1
|
|
353
|
+
else:
|
|
354
|
+
# Table headers
|
|
355
|
+
headers = [
|
|
356
|
+
"UID",
|
|
357
|
+
"Name",
|
|
358
|
+
"Sensor",
|
|
359
|
+
"Min/Max",
|
|
360
|
+
"Benchlab",
|
|
361
|
+
"Backlight",
|
|
362
|
+
"Easing"]
|
|
363
|
+
# widths for alignment only
|
|
364
|
+
col_widths = [24, 10, 10, 11, 12, 15, 10]
|
|
365
|
+
x = 4
|
|
366
|
+
for i, h in enumerate(headers):
|
|
367
|
+
try:
|
|
368
|
+
self.stdscr.addstr(y, x, h, curses.A_UNDERLINE)
|
|
369
|
+
except curses.error:
|
|
370
|
+
pass
|
|
371
|
+
x += col_widths[i] + 1
|
|
372
|
+
y += 1
|
|
373
|
+
|
|
374
|
+
# Table rows
|
|
375
|
+
for dial in self.dial_cfg:
|
|
376
|
+
x = 4
|
|
377
|
+
row = [
|
|
378
|
+
str(dial.get("dial_uid", "")),
|
|
379
|
+
str(dial.get("dial_name", "")),
|
|
380
|
+
str(dial.get("sensor", "")),
|
|
381
|
+
f"{dial.get('min', '')}/{dial.get('max', '')}",
|
|
382
|
+
str(dial.get("benchlab_port", "")),
|
|
383
|
+
str(dial.get("backlight", "")),
|
|
384
|
+
f"{dial.get('easing_dial', '')}",
|
|
385
|
+
]
|
|
386
|
+
for i, cell in enumerate(row):
|
|
387
|
+
try:
|
|
388
|
+
self.stdscr.addstr(y, x, cell.ljust(col_widths[i]))
|
|
389
|
+
except curses.error:
|
|
390
|
+
pass
|
|
391
|
+
x += col_widths[i] + 1
|
|
392
|
+
y += 1
|
|
393
|
+
|
|
394
|
+
# --- Footer with Provisioning ---
|
|
395
|
+
self.stdscr.addstr(
|
|
396
|
+
self.h - 1,
|
|
397
|
+
0,
|
|
398
|
+
"TAB: Switch tabs | r: Reload | p: Provision new devices | "
|
|
399
|
+
"q: Quit")
|
|
400
|
+
|
|
401
|
+
def handle_tab1_input(self, key):
|
|
402
|
+
if key in (ord('r'), ord('R')):
|
|
403
|
+
self.reload_configs()
|
|
404
|
+
elif key in (ord('p'), ord('P')):
|
|
405
|
+
self.stdscr.addstr(
|
|
406
|
+
self.h - 3,
|
|
407
|
+
0,
|
|
408
|
+
"Scanning and provisioning new devices...".ljust(
|
|
409
|
+
self.w))
|
|
410
|
+
self.stdscr.refresh()
|
|
411
|
+
success = devices.provision_vu_dials(
|
|
412
|
+
self.server_cfg.get("vu_server_url", "http://localhost:5340"),
|
|
413
|
+
self.server_cfg.get("api_key", "")
|
|
414
|
+
)
|
|
415
|
+
self.vu_dials = devices.get_vu_dials(
|
|
416
|
+
self.server_cfg.get("vu_server_url", "http://localhost:5340"),
|
|
417
|
+
self.server_cfg.get("api_key", "")
|
|
418
|
+
)
|
|
419
|
+
msg = (
|
|
420
|
+
"Provisioning successful." if success
|
|
421
|
+
else "Provisioning failed or no new devices found.")
|
|
422
|
+
self.stdscr.addstr(self.h - 3, 0, msg.ljust(self.w))
|
|
423
|
+
self.stdscr.refresh()
|
|
424
|
+
curses.napms(1000)
|
|
425
|
+
|
|
426
|
+
# -------------------- TAB 2 --------------------
|
|
427
|
+
def draw_tab2(self):
|
|
428
|
+
self.stdscr.erase() # erase instead of clear; safer
|
|
429
|
+
h, w = self.stdscr.getmaxyx()
|
|
430
|
+
|
|
431
|
+
# --- Header ---
|
|
432
|
+
self.stdscr.attron(curses.color_pair(7))
|
|
433
|
+
self.stdscr.addstr(0, 0, " BENCHLAB VU CONFIGURATION ".center(w))
|
|
434
|
+
self.stdscr.attroff(curses.color_pair(7))
|
|
435
|
+
|
|
436
|
+
# --- Tabs ---
|
|
437
|
+
self.draw_tabs()
|
|
438
|
+
|
|
439
|
+
y = 4
|
|
440
|
+
keys = list(self.server_cfg.keys())
|
|
441
|
+
key_col_width = max((len(k) for k in keys), default=0) + 2
|
|
442
|
+
visible_lines = self.h - y - 4
|
|
443
|
+
|
|
444
|
+
if self.tab2_cursor < self.tab2_start_line:
|
|
445
|
+
self.tab2_start_line = self.tab2_cursor
|
|
446
|
+
elif self.tab2_cursor >= self.tab2_start_line + visible_lines:
|
|
447
|
+
self.tab2_start_line = self.tab2_cursor - visible_lines + 1
|
|
448
|
+
|
|
449
|
+
for idx, key in enumerate(
|
|
450
|
+
keys[self.tab2_start_line:
|
|
451
|
+
self.tab2_start_line + visible_lines]):
|
|
452
|
+
val = str(self.server_cfg[key])
|
|
453
|
+
line_y = y + idx
|
|
454
|
+
key_text = f"{key}:".ljust(key_col_width)
|
|
455
|
+
self.stdscr.addstr(line_y, 4, key_text)
|
|
456
|
+
if idx + self.tab2_start_line == self.tab2_cursor:
|
|
457
|
+
self.stdscr.addstr(line_y, 4 +
|
|
458
|
+
key_col_width, val, curses.color_pair(1))
|
|
459
|
+
else:
|
|
460
|
+
self.stdscr.addstr(line_y, 4 + key_col_width, val)
|
|
461
|
+
|
|
462
|
+
self.stdscr.addstr(
|
|
463
|
+
self.h - 1,
|
|
464
|
+
0,
|
|
465
|
+
"↑/↓: Navigate | Enter: Edit | s: Save | TAB: Switch tabs")
|
|
466
|
+
|
|
467
|
+
def handle_tab2_input(self, key):
|
|
468
|
+
keys = list(self.server_cfg.keys())
|
|
469
|
+
max_idx = len(keys) - 1
|
|
470
|
+
|
|
471
|
+
if key == curses.KEY_UP:
|
|
472
|
+
self.tab2_cursor = max(0, self.tab2_cursor - 1)
|
|
473
|
+
elif key == curses.KEY_DOWN:
|
|
474
|
+
self.tab2_cursor = min(max_idx, self.tab2_cursor + 1)
|
|
475
|
+
elif key in (curses.KEY_ENTER, ord('\n')):
|
|
476
|
+
key_name = keys[self.tab2_cursor]
|
|
477
|
+
old_val = str(self.server_cfg[key_name])
|
|
478
|
+
curses.echo()
|
|
479
|
+
prompt = f"Enter new value for {key_name}: "
|
|
480
|
+
self.stdscr.addstr(self.h - 2, 0, " " * (self.w - 1))
|
|
481
|
+
self.stdscr.addstr(self.h - 2, 0, prompt)
|
|
482
|
+
new_val = self.stdscr.getstr(self.h - 2, len(prompt), 60).decode()
|
|
483
|
+
curses.noecho()
|
|
484
|
+
if new_val:
|
|
485
|
+
if old_val.isdigit():
|
|
486
|
+
try:
|
|
487
|
+
new_val = int(new_val)
|
|
488
|
+
except ValueError:
|
|
489
|
+
new_val = old_val
|
|
490
|
+
self.server_cfg[key_name] = new_val
|
|
491
|
+
elif key in (ord('s'), ord('S')):
|
|
492
|
+
save_json(VU_SERVER_CONFIG, self.server_cfg)
|
|
493
|
+
self.stdscr.addstr(
|
|
494
|
+
self.h - 2,
|
|
495
|
+
0,
|
|
496
|
+
"Config saved!".ljust(
|
|
497
|
+
self.w),
|
|
498
|
+
curses.color_pair(2))
|
|
499
|
+
self.stdscr.refresh()
|
|
500
|
+
time.sleep(1)
|
|
501
|
+
|
|
502
|
+
# -------------------- TAB 3 --------------------
|
|
503
|
+
def draw_tab3(self):
|
|
504
|
+
self.stdscr.erase() # erase instead of clear; safer
|
|
505
|
+
h, w = self.stdscr.getmaxyx()
|
|
506
|
+
|
|
507
|
+
# --- Header ---
|
|
508
|
+
self.stdscr.attron(curses.color_pair(7))
|
|
509
|
+
self.stdscr.addstr(0, 0, " BENCHLAB VU CONFIGURATION ".center(w))
|
|
510
|
+
self.stdscr.attroff(curses.color_pair(7))
|
|
511
|
+
|
|
512
|
+
# --- Tabs ---
|
|
513
|
+
self.draw_tabs()
|
|
514
|
+
|
|
515
|
+
y = 4
|
|
516
|
+
# Benchlab devices
|
|
517
|
+
self.stdscr.addstr(
|
|
518
|
+
y,
|
|
519
|
+
2,
|
|
520
|
+
"Available Benchlab Devices:",
|
|
521
|
+
curses.A_UNDERLINE)
|
|
522
|
+
y += 1
|
|
523
|
+
for b in self.benchlabs:
|
|
524
|
+
self.stdscr.addstr(y, 2, f"{b['port']} - {b['uid']}")
|
|
525
|
+
y += 1
|
|
526
|
+
y += 1
|
|
527
|
+
|
|
528
|
+
# Build combined dial list
|
|
529
|
+
self.all_dials = []
|
|
530
|
+
for uid, name in self.vu_dials:
|
|
531
|
+
cfg_entry = next(
|
|
532
|
+
(d for d in self.dial_cfg if d.get("dial_uid") == uid), None)
|
|
533
|
+
benchlab_port = cfg_entry.get(
|
|
534
|
+
"benchlab_port", "Unknown") if cfg_entry else "Unknown"
|
|
535
|
+
sensor = cfg_entry.get(
|
|
536
|
+
"sensor", "Not configured") if cfg_entry else "Not configured"
|
|
537
|
+
min_val = cfg_entry.get("min", 0) if cfg_entry else 0
|
|
538
|
+
max_val = cfg_entry.get("max", 100) if cfg_entry else 100
|
|
539
|
+
self.all_dials.append({
|
|
540
|
+
"uid": uid,
|
|
541
|
+
"name": name,
|
|
542
|
+
"benchlab_port": benchlab_port,
|
|
543
|
+
"sensor": sensor,
|
|
544
|
+
"min": min_val,
|
|
545
|
+
"max": max_val
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
# Column widths
|
|
549
|
+
col_uid_width = max((len(d["uid"])
|
|
550
|
+
for d in self.all_dials), default=0) + 2
|
|
551
|
+
col_name_width = max((len(d["name"])
|
|
552
|
+
for d in self.all_dials), default=0) + 2
|
|
553
|
+
col_port_width = max((len(d["benchlab_port"])
|
|
554
|
+
for d in self.all_dials), default=0) + 2
|
|
555
|
+
col_min_width = 5
|
|
556
|
+
col_max_width = 5
|
|
557
|
+
|
|
558
|
+
self.stdscr.addstr(
|
|
559
|
+
y, 2, f"{
|
|
560
|
+
'Dial UID':<{col_uid_width}} {
|
|
561
|
+
'Dial Name':<{col_name_width}} " f"{
|
|
562
|
+
'Benchlab':<{col_port_width}} {
|
|
563
|
+
'Min':<{col_min_width}} " f"{
|
|
564
|
+
'Max':<{col_max_width}} Sensor", curses.A_UNDERLINE)
|
|
565
|
+
y += 1
|
|
566
|
+
|
|
567
|
+
for idx, dial in enumerate(self.all_dials):
|
|
568
|
+
line = f"{
|
|
569
|
+
dial['uid']:<{col_uid_width}} {
|
|
570
|
+
dial['name']:<{col_name_width}} " f"{
|
|
571
|
+
dial['benchlab_port']:<{col_port_width}} {
|
|
572
|
+
dial['min']:<{col_min_width}} " f"{
|
|
573
|
+
dial['max']:<{col_max_width}} {
|
|
574
|
+
dial['sensor']}"
|
|
575
|
+
self.stdscr.addstr(
|
|
576
|
+
y + idx,
|
|
577
|
+
2,
|
|
578
|
+
line[:self.w - 3],
|
|
579
|
+
curses.color_pair(1) if idx == self.tab3_cursor else 0)
|
|
580
|
+
|
|
581
|
+
footer_text = (
|
|
582
|
+
"Enter: Configure Dial | ↑/↓: Navigate | TAB: Switch tabs | "
|
|
583
|
+
"r: Reload | q: Quit")
|
|
584
|
+
self.stdscr.addstr(
|
|
585
|
+
self.h - 1,
|
|
586
|
+
0,
|
|
587
|
+
footer_text[:self.w - 1])
|
|
588
|
+
|
|
589
|
+
def handle_tab3_input(self, key):
|
|
590
|
+
if not self.all_dials:
|
|
591
|
+
return
|
|
592
|
+
if key == curses.KEY_UP:
|
|
593
|
+
self.tab3_cursor = max(0, self.tab3_cursor - 1)
|
|
594
|
+
elif key == curses.KEY_DOWN:
|
|
595
|
+
self.tab3_cursor = min(
|
|
596
|
+
len(self.all_dials) - 1, self.tab3_cursor + 1)
|
|
597
|
+
elif key in (ord('\n'), curses.KEY_ENTER):
|
|
598
|
+
self.dial_mapping()
|
|
599
|
+
elif key == ord('r'):
|
|
600
|
+
self.reload_configs()
|
|
601
|
+
|
|
602
|
+
# -------------------- DIAL MAPPING --------------------
|
|
603
|
+
def dial_mapping(self):
|
|
604
|
+
if not self.all_dials:
|
|
605
|
+
h, w = self.stdscr.getmaxyx()
|
|
606
|
+
self.stdscr.addstr(
|
|
607
|
+
h - 2,
|
|
608
|
+
0,
|
|
609
|
+
"No VU dials available!",
|
|
610
|
+
curses.color_pair(3))
|
|
611
|
+
self.stdscr.refresh()
|
|
612
|
+
curses.napms(1000)
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
h, w = self.stdscr.getmaxyx()
|
|
616
|
+
dial = self.all_dials[self.tab3_cursor]
|
|
617
|
+
|
|
618
|
+
# --- Step 1: Dial Name ---
|
|
619
|
+
current_name = dial.get("name", "")
|
|
620
|
+
prompt = f"Set Dial Name [{current_name}]: "
|
|
621
|
+
self.stdscr.move(h - 3, 0)
|
|
622
|
+
self.stdscr.clrtoeol()
|
|
623
|
+
self.stdscr.addstr(h - 3, 0, prompt[:w - 1])
|
|
624
|
+
curses.echo()
|
|
625
|
+
inp = self.stdscr.getstr(h - 3, len(prompt), 60).decode().strip()
|
|
626
|
+
curses.noecho()
|
|
627
|
+
if inp:
|
|
628
|
+
dial["name"] = inp
|
|
629
|
+
|
|
630
|
+
# Ensure cfg_entry exists or update
|
|
631
|
+
cfg_entry = next(
|
|
632
|
+
(d for d in self.dial_cfg if d['dial_uid'] == dial['uid']), None)
|
|
633
|
+
if not cfg_entry:
|
|
634
|
+
cfg_entry = {
|
|
635
|
+
"dial_uid": dial['uid'],
|
|
636
|
+
"dial_name": dial.get("name", ""),
|
|
637
|
+
"benchlab_port": dial.get("benchlab_port", "Unknown"),
|
|
638
|
+
"benchlab_uid": dial.get("benchlab_uid", ""),
|
|
639
|
+
"sensor": dial.get("sensor", "Not configured"),
|
|
640
|
+
"min": 0,
|
|
641
|
+
"max": 100,
|
|
642
|
+
"backlight": [100, 100, 100],
|
|
643
|
+
"easing_dial": [50, 5],
|
|
644
|
+
"easing_backlight": [50, 5]
|
|
645
|
+
}
|
|
646
|
+
self.dial_cfg.append(cfg_entry)
|
|
647
|
+
else:
|
|
648
|
+
if inp:
|
|
649
|
+
cfg_entry["dial_name"] = inp
|
|
650
|
+
|
|
651
|
+
success = self.update_dial_name_on_server(dial['uid'], dial["name"])
|
|
652
|
+
if success:
|
|
653
|
+
self.stdscr.addstr(
|
|
654
|
+
h - 2,
|
|
655
|
+
0,
|
|
656
|
+
"Dial name updated on server!".ljust(w),
|
|
657
|
+
curses.color_pair(2))
|
|
658
|
+
else:
|
|
659
|
+
self.stdscr.addstr(
|
|
660
|
+
h - 2,
|
|
661
|
+
0,
|
|
662
|
+
"Failed to update dial name on server!".ljust(w),
|
|
663
|
+
curses.color_pair(3))
|
|
664
|
+
self.stdscr.refresh()
|
|
665
|
+
curses.napms(1000)
|
|
666
|
+
|
|
667
|
+
# --- Step 2: Benchlab Port ---
|
|
668
|
+
current_port = dial.get("benchlab_port", "")
|
|
669
|
+
ports = [b["port"] for b in self.benchlabs]
|
|
670
|
+
prompt = f"Select Benchlab Port [{current_port}]: "
|
|
671
|
+
self.stdscr.move(h - 3, 0)
|
|
672
|
+
self.stdscr.clrtoeol()
|
|
673
|
+
self.stdscr.addstr(h - 3, 0, prompt[:w - 1])
|
|
674
|
+
curses.echo()
|
|
675
|
+
choice = self.stdscr.getstr(h - 3, len(prompt), 20).decode().strip()
|
|
676
|
+
curses.noecho()
|
|
677
|
+
if choice and choice in ports:
|
|
678
|
+
dial["benchlab_port"] = choice
|
|
679
|
+
dial["benchlab_uid"] = next(
|
|
680
|
+
(b["uid"] for b in self.benchlabs if b["port"] == choice), "")
|
|
681
|
+
cfg_entry["benchlab_port"] = choice
|
|
682
|
+
cfg_entry["benchlab_uid"] = dial["benchlab_uid"]
|
|
683
|
+
|
|
684
|
+
# --- Step 3: Sensor Selection ---
|
|
685
|
+
# Refresh sensor list in case datasource wasn't ready at startup
|
|
686
|
+
if not self.SENSORS:
|
|
687
|
+
self.SENSORS = get_available_sensors(self.datasource)
|
|
688
|
+
|
|
689
|
+
if not self.SENSORS:
|
|
690
|
+
self.stdscr.addstr(
|
|
691
|
+
h - 2,
|
|
692
|
+
0,
|
|
693
|
+
"No sensors available yet — ensure device is connected."
|
|
694
|
+
.ljust(w),
|
|
695
|
+
curses.color_pair(3))
|
|
696
|
+
self.stdscr.refresh()
|
|
697
|
+
curses.napms(2000)
|
|
698
|
+
return
|
|
699
|
+
|
|
700
|
+
num_sensors = len(self.SENSORS)
|
|
701
|
+
col_width = max(len(s) for s in self.SENSORS) + 4
|
|
702
|
+
num_cols = max(1, w // col_width)
|
|
703
|
+
num_rows = (num_sensors + num_cols - 1) // num_cols
|
|
704
|
+
|
|
705
|
+
while True:
|
|
706
|
+
# Clear sensor display area
|
|
707
|
+
for row in range(num_rows):
|
|
708
|
+
self.stdscr.move(h - num_rows - 3 + row, 0)
|
|
709
|
+
self.stdscr.clrtoeol()
|
|
710
|
+
|
|
711
|
+
# Display sensors
|
|
712
|
+
for idx, sensor in enumerate(self.SENSORS):
|
|
713
|
+
row = idx % num_rows
|
|
714
|
+
col = idx // num_rows
|
|
715
|
+
x = col * col_width
|
|
716
|
+
s_str = f"{idx + 1}. {sensor}"
|
|
717
|
+
self.stdscr.addstr(h - num_rows - 3 + row,
|
|
718
|
+
x, s_str[:col_width - 1])
|
|
719
|
+
|
|
720
|
+
prompt = f"Select sensor number for {
|
|
721
|
+
dial['name']} or 'q' to skip: "
|
|
722
|
+
self.stdscr.move(h - 4, 0)
|
|
723
|
+
self.stdscr.clrtoeol()
|
|
724
|
+
self.stdscr.addstr(h - 3, 0, prompt[:w - 1])
|
|
725
|
+
curses.echo()
|
|
726
|
+
inp = self.stdscr.getstr(h - 3, len(prompt), 5).decode().strip()
|
|
727
|
+
curses.noecho()
|
|
728
|
+
|
|
729
|
+
if inp.lower() == 'q':
|
|
730
|
+
break
|
|
731
|
+
|
|
732
|
+
try:
|
|
733
|
+
sel = int(inp) - 1
|
|
734
|
+
if 0 <= sel < len(self.SENSORS):
|
|
735
|
+
dial['sensor'] = self.SENSORS[sel]
|
|
736
|
+
cfg_entry['sensor'] = self.SENSORS[sel]
|
|
737
|
+
break
|
|
738
|
+
except ValueError:
|
|
739
|
+
continue
|
|
740
|
+
|
|
741
|
+
# --- Step 4: Min / Max ---
|
|
742
|
+
current_min = cfg_entry.get("min", 0)
|
|
743
|
+
current_max = cfg_entry.get("max", 100)
|
|
744
|
+
|
|
745
|
+
# MIN
|
|
746
|
+
prompt = f"Set MIN [{current_min}]: "
|
|
747
|
+
self.stdscr.move(h - 3, 0)
|
|
748
|
+
self.stdscr.clrtoeol()
|
|
749
|
+
self.stdscr.addstr(h - 3, 0, prompt[:w - 1])
|
|
750
|
+
curses.echo()
|
|
751
|
+
inp = self.stdscr.getstr(h - 3, len(prompt), 10).decode().strip()
|
|
752
|
+
curses.noecho()
|
|
753
|
+
val_min = _parse_float_or(inp, current_min)
|
|
754
|
+
|
|
755
|
+
# MAX
|
|
756
|
+
prompt = f"Set MAX [{current_max}]: "
|
|
757
|
+
self.stdscr.move(h - 3, 0)
|
|
758
|
+
self.stdscr.clrtoeol()
|
|
759
|
+
self.stdscr.addstr(h - 3, 0, prompt[:w - 1])
|
|
760
|
+
curses.echo()
|
|
761
|
+
inp = self.stdscr.getstr(h - 3, len(prompt), 10).decode().strip()
|
|
762
|
+
curses.noecho()
|
|
763
|
+
val_max = _parse_float_or(inp, current_max)
|
|
764
|
+
|
|
765
|
+
if val_max <= val_min:
|
|
766
|
+
self.stdscr.addstr(
|
|
767
|
+
h - 2,
|
|
768
|
+
0,
|
|
769
|
+
"MAX must be greater than MIN!",
|
|
770
|
+
curses.color_pair(3))
|
|
771
|
+
self.stdscr.refresh()
|
|
772
|
+
curses.napms(1500)
|
|
773
|
+
else:
|
|
774
|
+
cfg_entry["min"] = val_min
|
|
775
|
+
cfg_entry["max"] = val_max
|
|
776
|
+
save_json(VU_DIAL_CONFIG, self.dial_cfg)
|
|
777
|
+
self.stdscr.addstr(
|
|
778
|
+
h - 2,
|
|
779
|
+
0,
|
|
780
|
+
"Dial mapping saved!".ljust(w),
|
|
781
|
+
curses.color_pair(2))
|
|
782
|
+
self.stdscr.refresh()
|
|
783
|
+
curses.napms(1000)
|
|
784
|
+
|
|
785
|
+
# -------------------- MAIN LOOP --------------------
|
|
786
|
+
def run(self):
|
|
787
|
+
while self.running:
|
|
788
|
+
if self.tab_index == 0:
|
|
789
|
+
self.draw_tab1()
|
|
790
|
+
elif self.tab_index == 1:
|
|
791
|
+
self.draw_tab2()
|
|
792
|
+
elif self.tab_index == 2:
|
|
793
|
+
self.draw_tab3()
|
|
794
|
+
|
|
795
|
+
self.stdscr.refresh()
|
|
796
|
+
key = self.stdscr.getch()
|
|
797
|
+
|
|
798
|
+
if key == ord('\t'):
|
|
799
|
+
self.tab_index = (self.tab_index + 1) % 3
|
|
800
|
+
elif key in (ord('q'), ord('Q')):
|
|
801
|
+
break
|
|
802
|
+
else:
|
|
803
|
+
if self.tab_index == 0:
|
|
804
|
+
self.handle_tab1_input(key)
|
|
805
|
+
elif self.tab_index == 1:
|
|
806
|
+
self.handle_tab2_input(key)
|
|
807
|
+
elif self.tab_index == 2:
|
|
808
|
+
self.handle_tab3_input(key)
|
|
809
|
+
|
|
810
|
+
# -------------------- RUN TUI --------------------
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def run_vu_tui(stdscr, server_proc=None, datasource=None):
|
|
814
|
+
tui = VUTUI(stdscr, server_proc=server_proc, datasource=datasource)
|
|
815
|
+
|
|
816
|
+
def sigint_handler(sig, frame):
|
|
817
|
+
tui.running = False
|
|
818
|
+
logger.info("Ctrl+C detected. Exiting TUI...")
|
|
819
|
+
|
|
820
|
+
signal.signal(signal.SIGINT, sigint_handler)
|
|
821
|
+
|
|
822
|
+
try:
|
|
823
|
+
tui.run()
|
|
824
|
+
finally:
|
|
825
|
+
logger.info("Cleaning up TUI and server...")
|
|
826
|
+
if tui:
|
|
827
|
+
tui.cleanup()
|
|
828
|
+
if server_proc:
|
|
829
|
+
terminate_vu_server(server_proc)
|
|
830
|
+
logger.info("VU server terminated.")
|
|
831
|
+
|
|
832
|
+
# -------------------- LAUNCH FUNCTION --------------------
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def launch_vu_config(args=None):
|
|
836
|
+
"""Launch the VU configuration TUI.
|
|
837
|
+
|
|
838
|
+
Parameters
|
|
839
|
+
----------
|
|
840
|
+
args:
|
|
841
|
+
Standard benchlab args namespace. If None, reads from env vars.
|
|
842
|
+
"""
|
|
843
|
+
import os
|
|
844
|
+
import types as _types
|
|
845
|
+
from benchlab.core.datasource_manager import DataSourceManager
|
|
846
|
+
|
|
847
|
+
if args is None:
|
|
848
|
+
args = _types.SimpleNamespace(
|
|
849
|
+
source=os.environ.get(
|
|
850
|
+
"BENCHLAB_DATA_SOURCE",
|
|
851
|
+
"direct"),
|
|
852
|
+
api_url=os.environ.get(
|
|
853
|
+
"BENCHLAB_API_URL",
|
|
854
|
+
"http://127.0.0.1:8000"),
|
|
855
|
+
mqtt_broker=os.environ.get(
|
|
856
|
+
"MQTT_BROKER",
|
|
857
|
+
"localhost"),
|
|
858
|
+
mqtt_port=int(
|
|
859
|
+
os.environ.get(
|
|
860
|
+
"MQTT_PORT",
|
|
861
|
+
"1883")),
|
|
862
|
+
interval=1.0,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
logger.info("Launching the BENCHLAB VU Server & Dials Configuration")
|
|
866
|
+
time.sleep(1)
|
|
867
|
+
logger.info("Launch updater with -vu")
|
|
868
|
+
time.sleep(1)
|
|
869
|
+
logger.info("Checking for VU server...")
|
|
870
|
+
time.sleep(1)
|
|
871
|
+
|
|
872
|
+
server_cfg = load_json(VU_SERVER_CONFIG, {})
|
|
873
|
+
server_url = server_cfg.get("vu_server_url", "http://localhost:5340")
|
|
874
|
+
api_key = server_cfg.get("api_key", "")
|
|
875
|
+
|
|
876
|
+
server_proc = None
|
|
877
|
+
if not check_vu_server(server_url, api_key):
|
|
878
|
+
logger.info("No server detected — starting local VU server...")
|
|
879
|
+
server_proc = start_vu_server()
|
|
880
|
+
if server_proc:
|
|
881
|
+
logger.info(f"Local VU server started at {server_url}")
|
|
882
|
+
threading.Thread(
|
|
883
|
+
target=forward_logs, args=(
|
|
884
|
+
server_proc,), daemon=True).start()
|
|
885
|
+
else:
|
|
886
|
+
logger.warning("Failed to start local VU server.")
|
|
887
|
+
else:
|
|
888
|
+
logger.info(f"VU server already running at {server_url}")
|
|
889
|
+
|
|
890
|
+
time.sleep(1)
|
|
891
|
+
|
|
892
|
+
ds_kwargs = {}
|
|
893
|
+
if args.source in ("fastapi", "fastapi_custom"):
|
|
894
|
+
ds_kwargs["base_url"] = args.api_url
|
|
895
|
+
elif args.source == "mqtt":
|
|
896
|
+
ds_kwargs["broker"] = args.mqtt_broker
|
|
897
|
+
ds_kwargs["port"] = args.mqtt_port
|
|
898
|
+
|
|
899
|
+
try:
|
|
900
|
+
datasource = DataSourceManager(source_type=args.source, **ds_kwargs)
|
|
901
|
+
if not datasource.connect():
|
|
902
|
+
logger.warning(
|
|
903
|
+
f"Could not connect to {
|
|
904
|
+
args.source} datasource — sensor list may be empty")
|
|
905
|
+
except Exception:
|
|
906
|
+
# run_vu_tui's own cleanup (which terminates server_proc) never runs
|
|
907
|
+
# if we fail before reaching curses.wrapper below, so terminate it
|
|
908
|
+
# here instead of leaking the auto-started server process.
|
|
909
|
+
terminate_vu_server(server_proc)
|
|
910
|
+
raise
|
|
911
|
+
|
|
912
|
+
try:
|
|
913
|
+
curses.wrapper(run_vu_tui, server_proc, datasource)
|
|
914
|
+
finally:
|
|
915
|
+
datasource.disconnect()
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
if __name__ == "__main__":
|
|
919
|
+
launch_vu_config()
|