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,247 @@
|
|
|
1
|
+
# wigidash_usb.py
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import usb.core
|
|
8
|
+
import usb.util
|
|
9
|
+
|
|
10
|
+
from benchlab.wigidash.benchlab_utils import get_logger
|
|
11
|
+
|
|
12
|
+
is_linux = sys.platform.startswith("linux")
|
|
13
|
+
is_windows = sys.platform.startswith("win")
|
|
14
|
+
|
|
15
|
+
logger = get_logger("WigidashUsb")
|
|
16
|
+
|
|
17
|
+
_usb_backend = None
|
|
18
|
+
if is_windows:
|
|
19
|
+
try:
|
|
20
|
+
import libusb_package
|
|
21
|
+
_usb_backend = libusb_package.get_libusb1_backend()
|
|
22
|
+
logger.debug("Using bundled libusb-package backend for pyusb")
|
|
23
|
+
except Exception as e:
|
|
24
|
+
# Fall back to pyusb's normal backend discovery (e.g. a system-wide
|
|
25
|
+
# libusb-1.0.dll on PATH) if libusb-package isn't installed/usable.
|
|
26
|
+
logger.debug(
|
|
27
|
+
"libusb-package backend unavailable, falling back to "
|
|
28
|
+
f"default discovery: {e}")
|
|
29
|
+
|
|
30
|
+
_PERMISSION_ERROR_SIGNALS = (
|
|
31
|
+
"access denied",
|
|
32
|
+
"insufficient permissions",
|
|
33
|
+
"errno 13",
|
|
34
|
+
"operation not permitted",
|
|
35
|
+
"permission denied",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_UDEV_RULES_DIRS = (
|
|
39
|
+
"/etc/udev/rules.d",
|
|
40
|
+
"/usr/lib/udev/rules.d",
|
|
41
|
+
"/lib/udev/rules.d")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _udev_rule_hint(vendor_id, product_id):
|
|
45
|
+
"""Return the udev rule text + setup steps for granting non-root
|
|
46
|
+
USB access."""
|
|
47
|
+
return (
|
|
48
|
+
f'Create /etc/udev/rules.d/99-wigidash.rules with:\n'
|
|
49
|
+
f' SUBSYSTEM=="usb", ATTR{{idVendor}}=="{vendor_id:04x}", '
|
|
50
|
+
f'ATTR{{idProduct}}=="{product_id:04x}", TAG+="uaccess"\n'
|
|
51
|
+
"Then run: sudo udevadm control --reload-rules "
|
|
52
|
+
"&& sudo udevadm trigger"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _udev_rule_exists(vendor_id, product_id, rules_dirs=_UDEV_RULES_DIRS):
|
|
57
|
+
"""Best-effort scan of udev rule files for one referencing this VID:PID."""
|
|
58
|
+
vid_hex = f"{vendor_id:04x}"
|
|
59
|
+
pid_hex = f"{product_id:04x}"
|
|
60
|
+
pattern = re.compile(
|
|
61
|
+
rf'idVendor.{{0,5}}{vid_hex}.{{0,80}}idProduct.{{0,5}}{pid_hex}'
|
|
62
|
+
rf'|idProduct.{{0,5}}{pid_hex}.{{0,80}}idVendor.{{0,5}}{vid_hex}',
|
|
63
|
+
re.IGNORECASE,
|
|
64
|
+
)
|
|
65
|
+
for rules_dir in rules_dirs:
|
|
66
|
+
for path in glob.glob(os.path.join(rules_dir, "*.rules")):
|
|
67
|
+
try:
|
|
68
|
+
with open(path, "r", errors="ignore") as f:
|
|
69
|
+
if pattern.search(f.read()):
|
|
70
|
+
return True
|
|
71
|
+
except OSError:
|
|
72
|
+
continue
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def check_linux_usb_permissions(vendor_id=0x28DA, product_id=0xEF01):
|
|
77
|
+
"""
|
|
78
|
+
Best-effort check that the current user can access the WigiDash USB
|
|
79
|
+
device without root (via an existing udev rule, or an already-accessible
|
|
80
|
+
device node). Linux-only diagnostic — never a hard gate.
|
|
81
|
+
|
|
82
|
+
Returns (True, None) if access looks fine or the check is inconclusive,
|
|
83
|
+
(False, <actionable message>) if it looks like it will fail.
|
|
84
|
+
"""
|
|
85
|
+
if not is_linux:
|
|
86
|
+
return True, None
|
|
87
|
+
|
|
88
|
+
if os.geteuid() == 0:
|
|
89
|
+
return True, None
|
|
90
|
+
|
|
91
|
+
if _udev_rule_exists(vendor_id, product_id):
|
|
92
|
+
return True, None
|
|
93
|
+
|
|
94
|
+
# No matching rule found — see if a device node is nonetheless already
|
|
95
|
+
# accessible (e.g. user is in the right group via some other mechanism).
|
|
96
|
+
try:
|
|
97
|
+
found = usb.core.find(
|
|
98
|
+
idVendor=vendor_id,
|
|
99
|
+
idProduct=product_id,
|
|
100
|
+
find_all=True)
|
|
101
|
+
for dev in found:
|
|
102
|
+
bus, addr = getattr(
|
|
103
|
+
dev, "bus", None), getattr(
|
|
104
|
+
dev, "address", None)
|
|
105
|
+
if bus is None or addr is None:
|
|
106
|
+
continue
|
|
107
|
+
node = f"/dev/bus/usb/{bus:03d}/{addr:03d}"
|
|
108
|
+
if os.path.exists(node) and os.access(node, os.R_OK | os.W_OK):
|
|
109
|
+
return True, None
|
|
110
|
+
except Exception:
|
|
111
|
+
pass # inconclusive — fall through to the actionable warning
|
|
112
|
+
|
|
113
|
+
return False, (
|
|
114
|
+
"No udev rule found granting non-root access to the WigiDash "
|
|
115
|
+
f"(VID:0x{vendor_id:04X}, PID:0x{product_id:04X}). "
|
|
116
|
+
+ _udev_rule_hint(vendor_id, product_id)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _is_permission_error(exc) -> bool:
|
|
121
|
+
msg = str(exc).lower()
|
|
122
|
+
return any(signal in msg for signal in _PERMISSION_ERROR_SIGNALS)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def scan_wigidash(vendor_id=0x28DA, product_id=0xEF01):
|
|
126
|
+
"""
|
|
127
|
+
Scan for all connected Wigidash USB devices.
|
|
128
|
+
Returns a list of USBDevice instances (already connected).
|
|
129
|
+
"""
|
|
130
|
+
if is_linux:
|
|
131
|
+
ok, hint = check_linux_usb_permissions(vendor_id, product_id)
|
|
132
|
+
if not ok:
|
|
133
|
+
logger.warning(f"WigiDash USB access may be misconfigured: {hint}")
|
|
134
|
+
|
|
135
|
+
devices = []
|
|
136
|
+
find_kwargs = {
|
|
137
|
+
"idVendor": vendor_id,
|
|
138
|
+
"idProduct": product_id,
|
|
139
|
+
"find_all": True}
|
|
140
|
+
if _usb_backend is not None:
|
|
141
|
+
find_kwargs["backend"] = _usb_backend
|
|
142
|
+
found = usb.core.find(**find_kwargs)
|
|
143
|
+
for dev in found:
|
|
144
|
+
try:
|
|
145
|
+
dev.set_configuration()
|
|
146
|
+
serial = usb.util.get_string(dev, dev.iSerialNumber)
|
|
147
|
+
usb_dev = USBDevice(vendor_id, product_id, serial=serial)
|
|
148
|
+
usb_dev.dev = dev
|
|
149
|
+
logger.info(
|
|
150
|
+
f"Wigidash device found and configured: VID:0x{
|
|
151
|
+
vendor_id:04X}, PID:0x{
|
|
152
|
+
product_id:04X}, Serial: {serial}")
|
|
153
|
+
devices.append(usb_dev)
|
|
154
|
+
except usb.core.USBError as e:
|
|
155
|
+
if is_linux and _is_permission_error(e):
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"Failed to configure device {dev}: {e}. "
|
|
158
|
+
f"{_udev_rule_hint(vendor_id, product_id)}"
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
logger.warning(f"Failed to configure device {dev}: {e}")
|
|
162
|
+
return devices
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class USBDevice:
|
|
166
|
+
def __init__(self, vendor_id, product_id, serial=None, dev_obj=None):
|
|
167
|
+
self.vendor_id = vendor_id
|
|
168
|
+
self.product_id = product_id
|
|
169
|
+
self.serial = serial
|
|
170
|
+
self.dev = dev_obj
|
|
171
|
+
|
|
172
|
+
def connect(self):
|
|
173
|
+
if self.dev is None:
|
|
174
|
+
raise RuntimeError("USB device object not attached!")
|
|
175
|
+
try:
|
|
176
|
+
self.dev.set_configuration()
|
|
177
|
+
logger.info(
|
|
178
|
+
f"USB device configured successfully, serial: {
|
|
179
|
+
self.serial}")
|
|
180
|
+
except usb.core.USBError as e:
|
|
181
|
+
if is_linux and _is_permission_error(e):
|
|
182
|
+
logger.warning(
|
|
183
|
+
f"USB set_configuration failed (ignored): {e}. "
|
|
184
|
+
f"{_udev_rule_hint(self.vendor_id, self.product_id)}"
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
logger.warning(f"USB set_configuration failed (ignored): {e}")
|
|
188
|
+
|
|
189
|
+
def disconnect(self):
|
|
190
|
+
"""Cleanup USB resources"""
|
|
191
|
+
if self.dev:
|
|
192
|
+
usb.util.dispose_resources(self.dev)
|
|
193
|
+
logger.info("USB device resources disposed")
|
|
194
|
+
self.dev = None
|
|
195
|
+
|
|
196
|
+
def ctrl_transfer_in(
|
|
197
|
+
self,
|
|
198
|
+
cmd,
|
|
199
|
+
wValue=0,
|
|
200
|
+
wIndex=0,
|
|
201
|
+
length=0,
|
|
202
|
+
timeout=2000):
|
|
203
|
+
"""Perform IN control transfer"""
|
|
204
|
+
try:
|
|
205
|
+
data = self.dev.ctrl_transfer(
|
|
206
|
+
0x80 | 0x21, cmd, wValue, wIndex, length, timeout=timeout)
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"IN transfer cmd=0x{
|
|
209
|
+
cmd:02X}, length={length} → {data}")
|
|
210
|
+
return data
|
|
211
|
+
except usb.core.USBError as e:
|
|
212
|
+
logger.error(f"IN transfer failed: {e}")
|
|
213
|
+
raise RuntimeError(f"IN transfer failed: {e}")
|
|
214
|
+
|
|
215
|
+
def ctrl_transfer_out(
|
|
216
|
+
self,
|
|
217
|
+
cmd,
|
|
218
|
+
wValue=0,
|
|
219
|
+
wIndex=0,
|
|
220
|
+
data=None,
|
|
221
|
+
timeout=2000):
|
|
222
|
+
"""Perform OUT control transfer"""
|
|
223
|
+
try:
|
|
224
|
+
self.dev.ctrl_transfer(
|
|
225
|
+
0x00 | 0x21,
|
|
226
|
+
cmd,
|
|
227
|
+
wValue,
|
|
228
|
+
wIndex,
|
|
229
|
+
data,
|
|
230
|
+
timeout=timeout)
|
|
231
|
+
logger.debug(f"OUT transfer cmd=0x{cmd:02X}, data={data}")
|
|
232
|
+
except usb.core.USBError as e:
|
|
233
|
+
logger.error(f"OUT transfer failed: {e}")
|
|
234
|
+
raise RuntimeError(f"OUT transfer failed: {e}")
|
|
235
|
+
|
|
236
|
+
def bulk_write(self, endpoint, data, timeout=2000):
|
|
237
|
+
"""Write data via bulk transfer"""
|
|
238
|
+
try:
|
|
239
|
+
written = self.dev.write(endpoint, data, timeout=timeout)
|
|
240
|
+
logger.debug(
|
|
241
|
+
f"Bulk write to ep=0x{
|
|
242
|
+
endpoint:02X}, len={
|
|
243
|
+
len(data)} bytes")
|
|
244
|
+
return written
|
|
245
|
+
except usb.core.USBError as e:
|
|
246
|
+
logger.error(f"Bulk write failed: {e}")
|
|
247
|
+
raise RuntimeError(f"Bulk write failed: {e}")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# wigidash_widget.py
|
|
2
|
+
|
|
3
|
+
from ctypes import Structure, c_int16, c_uint16, c_uint8, c_uint32
|
|
4
|
+
|
|
5
|
+
from benchlab.wigidash.benchlab_utils import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger("WigidashWidget")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WidgetConfig(Structure):
|
|
11
|
+
_pack_ = 4
|
|
12
|
+
_fields_ = [
|
|
13
|
+
('X', c_int16),
|
|
14
|
+
('Y', c_int16),
|
|
15
|
+
('Width', c_int16),
|
|
16
|
+
('Height', c_int16),
|
|
17
|
+
('BaseClr', c_uint16),
|
|
18
|
+
('DrawAddr', c_uint32),
|
|
19
|
+
('DrawLock', c_uint8),
|
|
20
|
+
('InvalidateFlag', c_uint8),
|
|
21
|
+
('UpdateFromCache', c_uint8)
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return (
|
|
26
|
+
f'X: {self.X}, Y: {self.Y}, Width: {self.Width}, '
|
|
27
|
+
f'Height: {self.Height}, '
|
|
28
|
+
f'BaseClr: {self.BaseClr}, DrawAddr: {self.DrawAddr}, '
|
|
29
|
+
f'DrawLock: {self.DrawLock}, '
|
|
30
|
+
f'InvalidateFlag: {self.InvalidateFlag}, '
|
|
31
|
+
f'UpdateFromCache: {self.UpdateFromCache}')
|
|
32
|
+
|
|
33
|
+
# --- Properties with logging ---
|
|
34
|
+
@property
|
|
35
|
+
def DrawLock(self):
|
|
36
|
+
return self._DrawLock
|
|
37
|
+
|
|
38
|
+
@DrawLock.setter
|
|
39
|
+
def DrawLock(self, value):
|
|
40
|
+
logger.debug(f"DrawLock changed: {self._DrawLock} → {value}")
|
|
41
|
+
self._DrawLock = value
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def InvalidateFlag(self):
|
|
45
|
+
return self._InvalidateFlag
|
|
46
|
+
|
|
47
|
+
@InvalidateFlag.setter
|
|
48
|
+
def InvalidateFlag(self, value):
|
|
49
|
+
logger.debug(
|
|
50
|
+
f"InvalidateFlag changed: {
|
|
51
|
+
self._InvalidateFlag} → {value}")
|
|
52
|
+
self._InvalidateFlag = value
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def UpdateFromCache(self):
|
|
56
|
+
return self._UpdateFromCache
|
|
57
|
+
|
|
58
|
+
@UpdateFromCache.setter
|
|
59
|
+
def UpdateFromCache(self, value):
|
|
60
|
+
logger.debug(
|
|
61
|
+
f"UpdateFromCache changed: {
|
|
62
|
+
self._UpdateFromCache} → {value}")
|
|
63
|
+
self._UpdateFromCache = value
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def create_fullscreen(cls, width=1016, height=592):
|
|
67
|
+
"""Create a fullscreen widget configuration"""
|
|
68
|
+
widget = cls()
|
|
69
|
+
widget.X = 0
|
|
70
|
+
widget.Y = 0
|
|
71
|
+
widget.Width = width
|
|
72
|
+
widget.Height = height
|
|
73
|
+
widget.DrawAddr = 0
|
|
74
|
+
logger.info(f"Created fullscreen widget: {widget}")
|
|
75
|
+
return widget
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def create_custom(cls, x=0, y=0, width=100, height=100, base_color=0):
|
|
79
|
+
"""Create a custom positioned widget"""
|
|
80
|
+
widget = cls()
|
|
81
|
+
widget.X = x
|
|
82
|
+
widget.Y = y
|
|
83
|
+
widget.Width = width
|
|
84
|
+
widget.Height = height
|
|
85
|
+
widget.BaseClr = base_color
|
|
86
|
+
widget.DrawAddr = 0
|
|
87
|
+
logger.info(f"Created custom widget: {widget}")
|
|
88
|
+
return widget
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: benchlab-pytools
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Python control suite for BENCHLAB telemetry devices
|
|
5
|
+
Project-URL: Homepage, https://benchlab.io/
|
|
6
|
+
Project-URL: Repository, https://github.com/BenchLab-io/benchlab-pytools
|
|
7
|
+
Project-URL: Issues, https://github.com/BenchLab-io/benchlab-pytools/issues
|
|
8
|
+
Author: BENCHLAB.io
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: benchlab,hardware-monitoring,telemetry
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: System :: Hardware
|
|
23
|
+
Classifier: Topic :: System :: Monitoring
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: benchlab-pycore>=0.5.1
|
|
26
|
+
Requires-Dist: packaging<26.0,>=23.0
|
|
27
|
+
Requires-Dist: python-dotenv<2.0,>=1.0.0
|
|
28
|
+
Requires-Dist: textual<9.0,>=0.60
|
|
29
|
+
Requires-Dist: windows-curses>=2.4.1; platform_system == 'Windows'
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: amqtt<0.13.0,>=0.12.0; extra == 'all'
|
|
32
|
+
Requires-Dist: benchlab-pycore>=0.5.1; extra == 'all'
|
|
33
|
+
Requires-Dist: blessed>=1.20.0; extra == 'all'
|
|
34
|
+
Requires-Dist: dearpygui>=2.1.1; extra == 'all'
|
|
35
|
+
Requires-Dist: fastapi==0.111.1; extra == 'all'
|
|
36
|
+
Requires-Dist: libusb-package>=1.0.26.1; (sys_platform == 'win32') and extra == 'all'
|
|
37
|
+
Requires-Dist: matplotlib>=3.10.7; extra == 'all'
|
|
38
|
+
Requires-Dist: numpy; extra == 'all'
|
|
39
|
+
Requires-Dist: numpy>=2.2.0; extra == 'all'
|
|
40
|
+
Requires-Dist: paho-mqtt<3.0,>=2.0; extra == 'all'
|
|
41
|
+
Requires-Dist: pillow>=10.0.0; extra == 'all'
|
|
42
|
+
Requires-Dist: pillow>=11.3.0; extra == 'all'
|
|
43
|
+
Requires-Dist: pydantic>=2.0.0; extra == 'all'
|
|
44
|
+
Requires-Dist: pyserial>=3.5; extra == 'all'
|
|
45
|
+
Requires-Dist: python-dotenv==1.1.1; extra == 'all'
|
|
46
|
+
Requires-Dist: pyusb>=1.3.1; extra == 'all'
|
|
47
|
+
Requires-Dist: pyyaml<7.0,>=6.0; extra == 'all'
|
|
48
|
+
Requires-Dist: pyyaml>=6.0; extra == 'all'
|
|
49
|
+
Requires-Dist: requests>=2.31.0; extra == 'all'
|
|
50
|
+
Requires-Dist: ruamel-yaml; extra == 'all'
|
|
51
|
+
Requires-Dist: tornado; extra == 'all'
|
|
52
|
+
Requires-Dist: uvicorn[standard]==0.24.0; extra == 'all'
|
|
53
|
+
Provides-Extra: csv-log
|
|
54
|
+
Requires-Dist: benchlab-pycore>=0.5.1; extra == 'csv-log'
|
|
55
|
+
Requires-Dist: pyserial>=3.5; extra == 'csv-log'
|
|
56
|
+
Provides-Extra: graph
|
|
57
|
+
Requires-Dist: dearpygui>=2.1.1; extra == 'graph'
|
|
58
|
+
Requires-Dist: pyserial>=3.5; extra == 'graph'
|
|
59
|
+
Provides-Extra: hwinfo
|
|
60
|
+
Requires-Dist: pyserial>=3.5; extra == 'hwinfo'
|
|
61
|
+
Provides-Extra: mqtt
|
|
62
|
+
Requires-Dist: amqtt<0.13.0,>=0.12.0; extra == 'mqtt'
|
|
63
|
+
Requires-Dist: paho-mqtt<3.0,>=2.0; extra == 'mqtt'
|
|
64
|
+
Requires-Dist: pyyaml<7.0,>=6.0; extra == 'mqtt'
|
|
65
|
+
Provides-Extra: restapi
|
|
66
|
+
Requires-Dist: fastapi==0.111.1; extra == 'restapi'
|
|
67
|
+
Requires-Dist: pydantic>=2.0.0; extra == 'restapi'
|
|
68
|
+
Requires-Dist: pyserial>=3.5; extra == 'restapi'
|
|
69
|
+
Requires-Dist: python-dotenv==1.1.1; extra == 'restapi'
|
|
70
|
+
Requires-Dist: uvicorn[standard]==0.24.0; extra == 'restapi'
|
|
71
|
+
Provides-Extra: tui
|
|
72
|
+
Requires-Dist: pyserial>=3.5; extra == 'tui'
|
|
73
|
+
Provides-Extra: vu
|
|
74
|
+
Requires-Dist: blessed>=1.20.0; extra == 'vu'
|
|
75
|
+
Requires-Dist: numpy; extra == 'vu'
|
|
76
|
+
Requires-Dist: pillow>=10.0.0; extra == 'vu'
|
|
77
|
+
Requires-Dist: pyserial>=3.5; extra == 'vu'
|
|
78
|
+
Requires-Dist: pyyaml>=6.0; extra == 'vu'
|
|
79
|
+
Requires-Dist: requests>=2.31.0; extra == 'vu'
|
|
80
|
+
Requires-Dist: ruamel-yaml; extra == 'vu'
|
|
81
|
+
Requires-Dist: tornado; extra == 'vu'
|
|
82
|
+
Provides-Extra: wigidash
|
|
83
|
+
Requires-Dist: libusb-package>=1.0.26.1; (sys_platform == 'win32') and extra == 'wigidash'
|
|
84
|
+
Requires-Dist: matplotlib>=3.10.7; extra == 'wigidash'
|
|
85
|
+
Requires-Dist: numpy>=2.2.0; extra == 'wigidash'
|
|
86
|
+
Requires-Dist: pillow>=11.3.0; extra == 'wigidash'
|
|
87
|
+
Requires-Dist: pyserial>=3.5; extra == 'wigidash'
|
|
88
|
+
Requires-Dist: pyusb>=1.3.1; extra == 'wigidash'
|
|
89
|
+
Description-Content-Type: text/markdown
|
|
90
|
+
|
|
91
|
+
# BENCHLAB PyTools
|
|
92
|
+
|
|
93
|
+
BENCHLAB PyTools is the Python-based control suite for BENCHLAB telemetry devices. It provides a shared telemetry pipeline — device discovery, data sourcing, and process management — plus a set of consumer tools built on top of it:
|
|
94
|
+
|
|
95
|
+
- **TUI** — interactive terminal dashboard for live monitoring
|
|
96
|
+
- **CSV Logger** — fleet-wide telemetry logging for offline analysis
|
|
97
|
+
- **FastAPI Server** — REST API for telemetry, exposed for other tools/integrations
|
|
98
|
+
- **Graph** — DearPyGui-based real-time sensor graphing
|
|
99
|
+
- **HWiNFO Export** — exposes sensors as HWiNFO64 custom sensors
|
|
100
|
+
- **MQTT Publisher** — publishes telemetry to a local or remote MQTT broker
|
|
101
|
+
- **Link** — publishes telemetry to the BENCHLAB cloud (SaaS) MQTT broker
|
|
102
|
+
- **VU Dials** — analog-style VU meter dial display and configuration
|
|
103
|
+
- **WigiDash** — telemetry/graph display on a G.SKILL WigiDash panel
|
|
104
|
+
- **Config Tool** — import/export device configuration (fan curves, RGB, etc.) via JSON
|
|
105
|
+
|
|
106
|
+
All tools share a common data-source layer, so the same telemetry can be read directly from a device, through a FastAPI server, over MQTT, or via the Windows BENCHLAB service — locally or remotely — without changing the consumer tool.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Installation
|
|
111
|
+
|
|
112
|
+
Requires Python 3.10+.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install -r requirements.txt
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each tool has its own `requirements.txt` (e.g. `benchlab/graph/requirements.txt`, `benchlab/vu/requirements.txt`). The launcher installs a tool's requirements automatically the first time it's run — you generally don't need to install them by hand.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Usage
|
|
123
|
+
|
|
124
|
+
### Interactive Menu
|
|
125
|
+
|
|
126
|
+
Run with no arguments to enter the interactive launcher:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python benchlab.py
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
or equivalently:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python -m benchlab
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The interactive menu (prompt_toolkit-based, with a plain-input fallback if `prompt_toolkit` isn't installed) lets you pick a data source and one or more tools, then launches them — installing any missing per-tool dependencies along the way.
|
|
139
|
+
|
|
140
|
+
### Command-Line Flags
|
|
141
|
+
|
|
142
|
+
Each tool can also be launched directly with a flag:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python -m benchlab -tui # Interactive terminal dashboard
|
|
146
|
+
python -m benchlab -logfleet # CSV logger (no TUI)
|
|
147
|
+
python -m benchlab -fastapi # FastAPI telemetry server
|
|
148
|
+
python -m benchlab -graph # DearPyGui graph
|
|
149
|
+
python -m benchlab -hwinfo # HWiNFO custom sensor export
|
|
150
|
+
python -m benchlab -mqtt [broker] # MQTT publisher (default broker: localhost)
|
|
151
|
+
python -m benchlab -link # Publish telemetry to BENCHLAB cloud (Link)
|
|
152
|
+
python -m benchlab -vu # VU analog dials
|
|
153
|
+
python -m benchlab -vuconfig # VU dial configuration UI
|
|
154
|
+
python -m benchlab -wigidash # WigiDash display
|
|
155
|
+
python -m benchlab -config ... # Device configuration import/export
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Running with no flags is equivalent to launching the interactive menu.
|
|
159
|
+
|
|
160
|
+
### Data Sources
|
|
161
|
+
|
|
162
|
+
Most tools accept `--source` to choose where telemetry comes from:
|
|
163
|
+
|
|
164
|
+
| Source | Description |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `direct` (default) | Direct USB-serial connection via `benchlab-pycore` |
|
|
167
|
+
| `fastapi` | Local FastAPI server, started automatically if not already running |
|
|
168
|
+
| `fastapi_custom` | Remote FastAPI server — requires `--api-url` |
|
|
169
|
+
| `mqtt` | Local MQTT broker + publisher, started automatically if needed |
|
|
170
|
+
| `mqtt_custom` | Remote/existing MQTT broker — requires `--mqtt-broker`/`--mqtt-port` |
|
|
171
|
+
| `named_pipe` | Windows BENCHLAB service (`BL_Service`) via named pipes — Windows only |
|
|
172
|
+
| `service_http` | Windows BENCHLAB service HTTP API — requires `--service-url` (default `http://localhost:8585`) |
|
|
173
|
+
|
|
174
|
+
Common connection flags:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
--source SOURCE direct | fastapi | fastapi_custom | mqtt | mqtt_custom | named_pipe | service_http
|
|
178
|
+
--api-url URL FastAPI base URL (default: http://127.0.0.1:8000)
|
|
179
|
+
--api-port PORT FastAPI port (default: 8000)
|
|
180
|
+
--mqtt-broker HOST MQTT broker host (default: localhost)
|
|
181
|
+
--mqtt-port PORT MQTT broker port (default: 1883)
|
|
182
|
+
--service-url URL BENCHLAB Windows service HTTP API URL (default: http://localhost:8585)
|
|
183
|
+
-i, --interval SECONDS Refresh interval (default: 1.0)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
When a source needs a background service (`fastapi`, `mqtt`), the launcher starts and health-checks it automatically, and tears it down on exit. Not every tool supports every source — the config tool, for example, only supports `direct` and `named_pipe`. See each tool's README for specifics.
|
|
187
|
+
|
|
188
|
+
### Launch Profiles
|
|
189
|
+
|
|
190
|
+
A named profile can bundle a data source and a set of tools to start together:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
python -m benchlab --profile gskill_ctex26
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Profiles are defined in `benchlab/tools.py` (`LAUNCH_PROFILES`). Each spawns its tools in separate terminal windows and manages them as a group.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Architecture
|
|
201
|
+
|
|
202
|
+
- `benchlab/main.py` — CLI argument parsing and mode dispatch (`launch_mode()`)
|
|
203
|
+
- `benchlab/launcher.py` — in-process and multi-terminal tool launching, process lifecycle
|
|
204
|
+
- `benchlab/sources.py` — data-source detection, startup, and teardown for all supported sources
|
|
205
|
+
- `benchlab/tools.py` — the `CONSUMER_TOOLS` registry (tool metadata, module/function to invoke, dependencies) and `LAUNCH_PROFILES`
|
|
206
|
+
- `benchlab/menu.py` / `benchlab/menu_classic.py` — interactive terminal menu (prompt_toolkit, with a plain-input fallback)
|
|
207
|
+
- `benchlab/core/` — shared internals used by every tool: device discovery, the data-source abstraction, process management, retry logic. See [benchlab/core/README.md](benchlab/core/README.md).
|
|
208
|
+
|
|
209
|
+
Each consumer tool lives in its own subpackage under `benchlab/` with its own README, and (where needed) its own `requirements.txt`.
|
|
210
|
+
|
|
211
|
+
### Adding a New Tool
|
|
212
|
+
|
|
213
|
+
1. Add an entry to `CONSUMER_TOOLS` in `benchlab/tools.py` with `name`, `flag`, `module`, `function`, and `requirements`.
|
|
214
|
+
2. Implement the tool's entry function in its module, accepting an `args` namespace (see `benchlab/launcher.py::_build_args_namespace`).
|
|
215
|
+
3. Add a CLI flag for it in `benchlab/main.py::get_parser()` and dispatch it in `launch_mode()`.
|
|
216
|
+
4. Add a `requirements.txt` in the tool's directory if it has extra dependencies.
|
|
217
|
+
5. Write a `README.md` in the tool's directory following the style of the existing ones.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Tool Documentation
|
|
222
|
+
|
|
223
|
+
- [Config Tool](benchlab/config/README.md) — device configuration import/export
|
|
224
|
+
- [Core](benchlab/core/README.md) — shared internals (data sources, process management, discovery)
|
|
225
|
+
- [CSV Logger](benchlab/csv_log/README.md)
|
|
226
|
+
- [FastAPI Server](benchlab/restapi/readme.md)
|
|
227
|
+
- [Graph](benchlab/graph/README.md)
|
|
228
|
+
- [HWiNFO Export](benchlab/hwinfo/README.md)
|
|
229
|
+
- [Link](benchlab/link/README.md) — publish telemetry to BENCHLAB cloud
|
|
230
|
+
- [MQTT Publisher](benchlab/mqtt/README.md)
|
|
231
|
+
- [TUI](benchlab/tui/README.md)
|
|
232
|
+
- [VU Dials](benchlab/vu/README.md)
|
|
233
|
+
- [WigiDash](benchlab/wigidash/README.md)
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
Part of BENCHLAB PyTools. See main project license for details.
|