benchlab-pytools 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. benchlab/__init__.py +14 -0
  2. benchlab/__main__.py +21 -0
  3. benchlab/config/README.md +325 -0
  4. benchlab/config/__init__.py +9 -0
  5. benchlab/config/config_client.py +631 -0
  6. benchlab/config/config_manager.py +495 -0
  7. benchlab/config/config_tool.py +358 -0
  8. benchlab/config/diff.py +163 -0
  9. benchlab/config/examples/complete_config.json +56 -0
  10. benchlab/config/examples/rgb_lighting.json +24 -0
  11. benchlab/config/examples/simple_fan.json +32 -0
  12. benchlab/config/schema.py +138 -0
  13. benchlab/core/README.md +154 -0
  14. benchlab/core/__init__.py +78 -0
  15. benchlab/core/config.py +145 -0
  16. benchlab/core/datasource.py +1209 -0
  17. benchlab/core/datasource_manager.py +418 -0
  18. benchlab/core/device_registry.py +206 -0
  19. benchlab/core/discovery.py +92 -0
  20. benchlab/core/infrastructure.py +309 -0
  21. benchlab/core/process_manager.py +356 -0
  22. benchlab/core/retry.py +85 -0
  23. benchlab/core/shared_serial.py +235 -0
  24. benchlab/core/statistics.py +224 -0
  25. benchlab/csv_log/README.md +206 -0
  26. benchlab/csv_log/__init__.py +5 -0
  27. benchlab/csv_log/csv_logger_enhanced.py +416 -0
  28. benchlab/csv_log/message_batcher.py +361 -0
  29. benchlab/csv_log/requirements.txt +3 -0
  30. benchlab/csv_log/smart_retry.py +466 -0
  31. benchlab/graph/README.md +117 -0
  32. benchlab/graph/__init__.py +5 -0
  33. benchlab/graph/app.py +353 -0
  34. benchlab/graph/requirements.txt +2 -0
  35. benchlab/graph/runner.py +42 -0
  36. benchlab/graph/ui.py +288 -0
  37. benchlab/hwinfo/README.md +135 -0
  38. benchlab/hwinfo/__init__.py +5 -0
  39. benchlab/hwinfo/hwinfo_export.py +386 -0
  40. benchlab/hwinfo/requirements.txt +1 -0
  41. benchlab/launcher.py +387 -0
  42. benchlab/link/.env.EXAMPLE +10 -0
  43. benchlab/link/README.md +172 -0
  44. benchlab/link/__init__.py +1 -0
  45. benchlab/link/link_main.py +479 -0
  46. benchlab/main.py +462 -0
  47. benchlab/menu.py +729 -0
  48. benchlab/menu_classic.py +410 -0
  49. benchlab/mqtt/README.md +201 -0
  50. benchlab/mqtt/__init__.py +5 -0
  51. benchlab/mqtt/mqtt.config_template +2 -0
  52. benchlab/mqtt/mqtt_publisher.py +540 -0
  53. benchlab/mqtt/requirements.txt +3 -0
  54. benchlab/prefs.py +67 -0
  55. benchlab/restapi/.env.example +17 -0
  56. benchlab/restapi/__init__.py +5 -0
  57. benchlab/restapi/favicon.ico +0 -0
  58. benchlab/restapi/readme.md +202 -0
  59. benchlab/restapi/requirements.txt +5 -0
  60. benchlab/restapi/telemetry_api.py +808 -0
  61. benchlab/sources.py +515 -0
  62. benchlab/tools.py +147 -0
  63. benchlab/tui/README.md +266 -0
  64. benchlab/tui/__init__.py +5 -0
  65. benchlab/tui/config.py +477 -0
  66. benchlab/tui/requirements.txt +2 -0
  67. benchlab/tui/tui_core.py +1298 -0
  68. benchlab/tui/tui_main.py +418 -0
  69. benchlab/vu/README.md +64 -0
  70. benchlab/vu/VU-Server/NOTICE.md +13 -0
  71. benchlab/vu/VU-Server/README.md +80 -0
  72. benchlab/vu/VU-Server/Running_from_source_on_linux.md +97 -0
  73. benchlab/vu/VU-Server/__init__.py +0 -0
  74. benchlab/vu/VU-Server/assets/vu1_hello_world.png +0 -0
  75. benchlab/vu/VU-Server/community_applications.md +13 -0
  76. benchlab/vu/VU-Server/config.yaml +9 -0
  77. benchlab/vu/VU-Server/database.py +256 -0
  78. benchlab/vu/VU-Server/dial_driver.py +534 -0
  79. benchlab/vu/VU-Server/dials/Comms_Hub_Gauge.py +78 -0
  80. benchlab/vu/VU-Server/dials/Comms_Hub_Server.py +100 -0
  81. benchlab/vu/VU-Server/dials/__init__.py +0 -0
  82. benchlab/vu/VU-Server/dials/base_logger.py +100 -0
  83. benchlab/vu/VU-Server/installer/inc/icon.ico +0 -0
  84. benchlab/vu/VU-Server/installer/install.nsi +148 -0
  85. benchlab/vu/VU-Server/make_version.py +40 -0
  86. benchlab/vu/VU-Server/pylintrc +628 -0
  87. benchlab/vu/VU-Server/requirements.txt +9 -0
  88. benchlab/vu/VU-Server/serial_driver.py +244 -0
  89. benchlab/vu/VU-Server/server.py +670 -0
  90. benchlab/vu/VU-Server/server_config.py +255 -0
  91. benchlab/vu/VU-Server/server_dial_handler.py +334 -0
  92. benchlab/vu/VU-Server/vu_notifications.py +24 -0
  93. benchlab/vu/VU-Server/www/android-chrome-192x192.png +0 -0
  94. benchlab/vu/VU-Server/www/android-chrome-512x512.png +0 -0
  95. benchlab/vu/VU-Server/www/apple-touch-icon.png +0 -0
  96. benchlab/vu/VU-Server/www/assets/css/demo.css +291 -0
  97. benchlab/vu/VU-Server/www/assets/css/demo.min.css +9 -0
  98. benchlab/vu/VU-Server/www/assets/css/demo.rtl.css +276 -0
  99. benchlab/vu/VU-Server/www/assets/css/demo.rtl.min.css +9 -0
  100. benchlab/vu/VU-Server/www/assets/css/tabler-flags.css +1084 -0
  101. benchlab/vu/VU-Server/www/assets/css/tabler-flags.min.css +9 -0
  102. benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.css +1084 -0
  103. benchlab/vu/VU-Server/www/assets/css/tabler-flags.rtl.min.css +9 -0
  104. benchlab/vu/VU-Server/www/assets/css/tabler-payments.css +528 -0
  105. benchlab/vu/VU-Server/www/assets/css/tabler-payments.min.css +9 -0
  106. benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.css +528 -0
  107. benchlab/vu/VU-Server/www/assets/css/tabler-payments.rtl.min.css +9 -0
  108. benchlab/vu/VU-Server/www/assets/css/tabler-social.css +10 -0
  109. benchlab/vu/VU-Server/www/assets/css/tabler-social.min.css +8 -0
  110. benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.css +10 -0
  111. benchlab/vu/VU-Server/www/assets/css/tabler-social.rtl.min.css +8 -0
  112. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.css +1504 -0
  113. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.min.css +9 -0
  114. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.css +1504 -0
  115. benchlab/vu/VU-Server/www/assets/css/tabler-vendors.rtl.min.css +9 -0
  116. benchlab/vu/VU-Server/www/assets/css/tabler.css +25813 -0
  117. benchlab/vu/VU-Server/www/assets/css/tabler.min.css +13 -0
  118. benchlab/vu/VU-Server/www/assets/css/tabler.rtl.css +25777 -0
  119. benchlab/vu/VU-Server/www/assets/css/tabler.rtl.min.css +13 -0
  120. benchlab/vu/VU-Server/www/assets/css/theme.min.css +261 -0
  121. benchlab/vu/VU-Server/www/assets/img/header.png +0 -0
  122. benchlab/vu/VU-Server/www/assets/img/logo.png +0 -0
  123. benchlab/vu/VU-Server/www/assets/js/dark-theme.js +35 -0
  124. benchlab/vu/VU-Server/www/assets/js/dark-theme.min.js +9 -0
  125. benchlab/vu/VU-Server/www/assets/js/demo.js +132 -0
  126. benchlab/vu/VU-Server/www/assets/js/demo.min.js +9 -0
  127. benchlab/vu/VU-Server/www/assets/js/jquery.min.js +2 -0
  128. benchlab/vu/VU-Server/www/assets/js/tabler.esm.js +7482 -0
  129. benchlab/vu/VU-Server/www/assets/js/tabler.esm.min.js +15 -0
  130. benchlab/vu/VU-Server/www/assets/js/tabler.js +7543 -0
  131. benchlab/vu/VU-Server/www/assets/js/tabler.min.js +15 -0
  132. benchlab/vu/VU-Server/www/assets/js/vu1_api.js +68 -0
  133. benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_key_settings.js +167 -0
  134. benchlab/vu/VU-Server/www/assets/js/vu1_gui_api_keys.js +138 -0
  135. benchlab/vu/VU-Server/www/assets/js/vu1_gui_dial.js +179 -0
  136. benchlab/vu/VU-Server/www/assets/js/vu1_gui_home.js +60 -0
  137. benchlab/vu/VU-Server/www/assets/js/vu1_gui_root.js +125 -0
  138. benchlab/vu/VU-Server/www/favicon-16x16.png +0 -0
  139. benchlab/vu/VU-Server/www/favicon-32x32.png +0 -0
  140. benchlab/vu/VU-Server/www/favicon.ico +0 -0
  141. benchlab/vu/VU-Server/www/favicon.png +0 -0
  142. benchlab/vu/VU-Server/www/index.html +47 -0
  143. benchlab/vu/VU-Server/www/views/api_keys.html +151 -0
  144. benchlab/vu/VU-Server/www/views/dial.html +286 -0
  145. benchlab/vu/VU-Server/www/views/footer.html +27 -0
  146. benchlab/vu/VU-Server/www/views/header.html +86 -0
  147. benchlab/vu/VU-Server/www/views/key_settings.html +132 -0
  148. benchlab/vu/VU-Server/www/views/start.html +124 -0
  149. benchlab/vu/__init__.py +5 -0
  150. benchlab/vu/assets/Barlow-Bold.ttf +0 -0
  151. benchlab/vu/assets/bl_dial_144x200.png +0 -0
  152. benchlab/vu/assets/bl_logo_144x200.png +0 -0
  153. benchlab/vu/devices.py +166 -0
  154. benchlab/vu/requirements.txt +24 -0
  155. benchlab/vu/sensors.py +18 -0
  156. benchlab/vu/vu_dial.config_template +90 -0
  157. benchlab/vu/vu_logo_gen.py +97 -0
  158. benchlab/vu/vu_server.config_template +5 -0
  159. benchlab/vu/vu_server_config.py +40 -0
  160. benchlab/vu/vu_server_manager.py +192 -0
  161. benchlab/vu/vu_tui.py +919 -0
  162. benchlab/vu/vu_updater.py +540 -0
  163. benchlab/wigidash/README.md +182 -0
  164. benchlab/wigidash/__init__.py +5 -0
  165. benchlab/wigidash/assets/Inter-Bold.ttf +0 -0
  166. benchlab/wigidash/assets/Inter.ttf +0 -0
  167. benchlab/wigidash/assets/benchlab.png +0 -0
  168. benchlab/wigidash/benchlab_fleet.py +150 -0
  169. benchlab/wigidash/benchlab_graph.py +517 -0
  170. benchlab/wigidash/benchlab_overview.py +426 -0
  171. benchlab/wigidash/benchlab_telemetry.py +105 -0
  172. benchlab/wigidash/benchlab_ui.py +190 -0
  173. benchlab/wigidash/benchlab_utils.py +182 -0
  174. benchlab/wigidash/requirements.txt +9 -0
  175. benchlab/wigidash/wigidash_device.py +541 -0
  176. benchlab/wigidash/wigidash_manager.py +342 -0
  177. benchlab/wigidash/wigidash_session.py +355 -0
  178. benchlab/wigidash/wigidash_usb.py +247 -0
  179. benchlab/wigidash/wigidash_widget.py +88 -0
  180. benchlab_pytools-3.0.0.dist-info/METADATA +239 -0
  181. benchlab_pytools-3.0.0.dist-info/RECORD +184 -0
  182. benchlab_pytools-3.0.0.dist-info/WHEEL +4 -0
  183. benchlab_pytools-3.0.0.dist-info/entry_points.txt +2 -0
  184. benchlab_pytools-3.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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.