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,190 @@
1
+ from PIL import Image, ImageFont
2
+ import os
3
+
4
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
5
+ ASSETS_DIR = os.path.join(BASE_DIR, "assets")
6
+
7
+
8
+ class UITheme:
9
+ SCREEN_WIDTH = 1016
10
+ SCREEN_HEIGHT = 592
11
+ HEADER_HEIGHT = 60
12
+ FOOTER_HEIGHT = 56
13
+ PADDING = 8
14
+
15
+ COLOR_SECTION = (252, 228, 119)
16
+ COLOR_BG = (0, 0, 0)
17
+ COLOR_CARD = (15, 15, 15)
18
+ COLOR_TEXT = (238, 238, 238)
19
+ COLOR_MUTED = (128, 128, 128)
20
+ COLOR_BORDER = (200, 200, 200)
21
+
22
+
23
+ class UIButton:
24
+ SELECT_DEVICE = "select_device"
25
+ OVERVIEW = "overview"
26
+ SHUTDOWN = "shutdown"
27
+ GRAPH_METRIC = "graph_metric"
28
+
29
+
30
+ BUTTON_DEFS = {
31
+ UIButton.SELECT_DEVICE: {
32
+ "text": "Select Device",
33
+ "color": (252, 228, 119),
34
+ },
35
+ UIButton.OVERVIEW: {
36
+ "text": "Overview",
37
+ "color": (252, 228, 119),
38
+ },
39
+ UIButton.SHUTDOWN: {
40
+ "text": "Shutdown",
41
+ "color": (255, 99, 71),
42
+ },
43
+ UIButton.GRAPH_METRIC: {
44
+ "color": (0, 0, 0),
45
+ },
46
+ }
47
+
48
+
49
+ def bind_button(button_id, callback):
50
+ spec = BUTTON_DEFS[button_id]
51
+ return {
52
+ "id": button_id,
53
+ "text": spec.get("text", ""),
54
+ "color": spec["color"],
55
+ "callback": callback,
56
+ }
57
+
58
+
59
+ def load_fonts():
60
+ try:
61
+ return {
62
+ "header": ImageFont.truetype(
63
+ os.path.join(
64
+ ASSETS_DIR,
65
+ "Inter-Bold.ttf"),
66
+ 30),
67
+ "title": ImageFont.truetype(
68
+ os.path.join(
69
+ ASSETS_DIR,
70
+ "Inter.ttf"),
71
+ 18),
72
+ "text": ImageFont.truetype(
73
+ os.path.join(
74
+ ASSETS_DIR,
75
+ "Inter.ttf"),
76
+ 14),
77
+ "button": ImageFont.truetype(
78
+ os.path.join(
79
+ ASSETS_DIR,
80
+ "Inter.ttf"),
81
+ 14),
82
+ }
83
+ except Exception:
84
+ default = ImageFont.load_default()
85
+ return dict(
86
+ header=default,
87
+ title=default,
88
+ text=default,
89
+ button=default)
90
+
91
+
92
+ def load_logo(max_size=(60, 60)):
93
+ logo_path = os.path.join(ASSETS_DIR, "benchlab.png")
94
+ try:
95
+ logo_img = Image.open(logo_path).convert("RGBA")
96
+ logo_img.thumbnail(max_size, Image.Resampling.LANCZOS)
97
+ return logo_img
98
+ except Exception:
99
+ # Optionally log warning here if you have a logger
100
+ return None
101
+
102
+
103
+ def draw_header(draw, img, fonts, title, logo=None):
104
+ draw.rectangle([0,
105
+ 0,
106
+ UITheme.SCREEN_WIDTH,
107
+ UITheme.HEADER_HEIGHT],
108
+ fill=UITheme.COLOR_SECTION)
109
+
110
+ if logo:
111
+ y = (UITheme.HEADER_HEIGHT - logo.height) // 2
112
+ img.paste(logo, (10, y), mask=logo)
113
+ img.paste(logo, (UITheme.SCREEN_WIDTH - logo.width - 10, y), mask=logo)
114
+
115
+ bbox = draw.textbbox((0, 0), title, font=fonts["header"])
116
+ x = (UITheme.SCREEN_WIDTH - (bbox[2] - bbox[0])) // 2
117
+ y = (UITheme.HEADER_HEIGHT - (bbox[3] - bbox[1])) // 2
118
+ draw.text((x, y), title, fill=(0, 0, 0), font=fonts["header"])
119
+
120
+
121
+ def draw_footer(draw, fonts, info_text, buttons):
122
+ hitboxes = []
123
+ pad = UITheme.PADDING
124
+
125
+ footer_top = UITheme.SCREEN_HEIGHT - UITheme.FOOTER_HEIGHT
126
+ footer_bottom = UITheme.SCREEN_HEIGHT
127
+
128
+ # Inner content area (accounts for padding + border)
129
+ inner_top = footer_top + pad
130
+ inner_bottom = footer_bottom - pad
131
+ inner_height = inner_bottom - inner_top
132
+
133
+ # ---- Layout calculations ----
134
+ btn_w = 120
135
+ btn_h = inner_height # EXACT same height as info box
136
+ total_btn_width = len(buttons) * btn_w + (len(buttons) - 1) * pad
137
+
138
+ info_right = UITheme.SCREEN_WIDTH - pad - total_btn_width - pad
139
+
140
+ # ---- Info background ----
141
+ draw.rectangle(
142
+ [pad, inner_top, info_right, inner_bottom],
143
+ fill=(20, 20, 20),
144
+ outline=UITheme.COLOR_BORDER
145
+ )
146
+
147
+ # ---- Info text (vertically centered in inner area) ----
148
+ tb = fonts["text"].getbbox(info_text)
149
+ text_h = tb[3] - tb[1]
150
+ text_y = inner_top + (inner_height - text_h) // 2
151
+
152
+ draw.text(
153
+ (pad + 8, text_y),
154
+ info_text,
155
+ fill=UITheme.COLOR_TEXT,
156
+ font=fonts["text"]
157
+ )
158
+
159
+ # ---- Buttons ----
160
+ x = UITheme.SCREEN_WIDTH - pad - btn_w
161
+
162
+ for btn in buttons:
163
+ y = inner_top
164
+
165
+ draw.rectangle(
166
+ [x, y, x + btn_w, y + btn_h],
167
+ fill=btn["color"],
168
+ outline=UITheme.COLOR_BORDER
169
+ )
170
+
171
+ # Center text inside button
172
+ tb = fonts["button"].getbbox(btn["text"])
173
+ tx = x + (btn_w - (tb[2] - tb[0])) // 2
174
+ ty = y + (btn_h - (tb[3] - tb[1])) // 2
175
+
176
+ draw.text((tx, ty), btn["text"], fill=(0, 0, 0), font=fonts["button"])
177
+
178
+ # Hitbox == button rectangle
179
+ hitboxes.append({
180
+ "x0": x,
181
+ "y0": y,
182
+ "x1": x + btn_w,
183
+ "y1": y + btn_h,
184
+ "callback": btn["callback"],
185
+ "text": btn["text"]
186
+ })
187
+
188
+ x -= btn_w + pad
189
+
190
+ return hitboxes
@@ -0,0 +1,182 @@
1
+ # benchlab_utils.py
2
+
3
+ from PIL import Image
4
+ import logging
5
+ import numpy as np
6
+ import sys
7
+ import time
8
+ import threading
9
+
10
+ # ----------------------------------------
11
+ # Logging setup
12
+ # ----------------------------------------
13
+
14
+ # shared logger for all benchlab modules
15
+
16
+
17
+ def get_logger(name="BenchlabUtils", level=logging.INFO):
18
+ logger = logging.getLogger(name)
19
+ logger.setLevel(level)
20
+
21
+ # Remove any duplicate handlers
22
+ if logger.hasHandlers():
23
+ logger.handlers.clear()
24
+
25
+ ch = logging.StreamHandler(sys.stdout)
26
+ ch.setFormatter(logging.Formatter(
27
+ '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s',
28
+ datefmt='%H:%M:%S'
29
+ ))
30
+ logger.addHandler(ch)
31
+ logger.propagate = False # prevent double logging via root logger
32
+ return logger
33
+
34
+
35
+ logger = get_logger("BenchlabUtils")
36
+
37
+
38
+ def setup_logging(module_levels=None, default_level=logging.INFO):
39
+ handler = logging.StreamHandler(sys.stdout)
40
+ handler.setFormatter(logging.Formatter(
41
+ '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s',
42
+ datefmt='%H:%M:%S'
43
+ ))
44
+
45
+ root = logging.getLogger()
46
+ root.setLevel(default_level)
47
+
48
+ if not root.hasHandlers():
49
+ root.addHandler(handler)
50
+
51
+ if module_levels:
52
+ for name, level in module_levels.items():
53
+ lg = logging.getLogger(name)
54
+ lg.setLevel(level)
55
+ if not lg.hasHandlers():
56
+ lg.addHandler(handler)
57
+ lg.propagate = False
58
+
59
+
60
+ # ----------------------------------------
61
+ # Display / Image helpers
62
+ # ----------------------------------------
63
+
64
+ def image_to_rgb565(img: Image.Image) -> bytes:
65
+ """Convert PIL image to raw RGB565 bytes using NumPy (faster)."""
66
+ img = img.convert('RGB')
67
+ arr = np.array(img, dtype=np.uint8) # shape: (H, W, 3)
68
+
69
+ r = (arr[..., 0] >> 3).astype(np.uint16) # 5 bits
70
+ g = (arr[..., 1] >> 2).astype(np.uint16) # 6 bits
71
+ b = (arr[..., 2] >> 3).astype(np.uint16) # 5 bits
72
+
73
+ rgb565 = (r << 11) | (g << 5) | b # shape: (H, W)
74
+ return rgb565.flatten().tobytes()
75
+
76
+
77
+ def display_image(wigidash, img: Image.Image, page=0, widget_id=0):
78
+ """Display an image on the WigiDash widget."""
79
+ if not wigidash:
80
+ logging.getLogger(__name__).warning(
81
+ "display_image: no wigidash device")
82
+ return
83
+
84
+ rgb565 = image_to_rgb565(img)
85
+ wigidash.write_to_widget(
86
+ page=page,
87
+ widget_id=widget_id,
88
+ offset=0,
89
+ data=rgb565)
90
+
91
+
92
+ def clear_display(wigidash, width, height):
93
+ """Fill the screen with black."""
94
+ img = Image.new('RGB', (width, height), (0, 0, 0))
95
+ display_image(wigidash, img)
96
+ logging.getLogger(__name__).info("Display cleared.")
97
+
98
+
99
+ # ----------------------------------------
100
+ # Screen keepalive
101
+ # ----------------------------------------
102
+
103
+ class KeepAliveManager:
104
+ """Keeps the WigiDash screen from going to sleep."""
105
+
106
+ def __init__(self, wigidash, interval=5.0):
107
+ self.wigidash = wigidash
108
+ self.interval = interval
109
+ self.running = False
110
+ self.thread = None
111
+
112
+ def start(self):
113
+ if self.running:
114
+ return
115
+ self.running = True
116
+ self.thread = threading.Thread(target=self.loop, daemon=True)
117
+ self.thread.start()
118
+
119
+ def stop(self):
120
+ self.running = False
121
+ if self.thread:
122
+ self.thread.join(timeout=1.0)
123
+ self.thread = None
124
+
125
+ def mark_active(self):
126
+ try:
127
+ if self.wigidash and hasattr(
128
+ self.wigidash, "clear_screen_timeout"):
129
+ self.wigidash.clear_screen_timeout()
130
+ except Exception as e:
131
+ logger.warning("Keepalive error: %s", e)
132
+
133
+ def loop(self):
134
+ step = 0.1
135
+ while self.running:
136
+ try:
137
+ if self.wigidash:
138
+ self.wigidash.clear_screen_timeout()
139
+ except Exception as e:
140
+ logger.warning(f"Keepalive error: {e}")
141
+
142
+ for _ in range(int(self.interval / step)):
143
+ if not self.running:
144
+ break
145
+ time.sleep(step)
146
+
147
+
148
+ # ----------------------------------------
149
+ # Shutdown function
150
+ # ----------------------------------------
151
+
152
+ def shutdown_wigidash(wigi_instance):
153
+ """
154
+ Safely shut down a WigiDash session.
155
+
156
+ wigi_instance: instance of BenchlabWigi or any object
157
+ with .keepalive_manager, .wigidash, .usb_dev attributes.
158
+ """
159
+ logger.info("Shutting down WigiDash session...")
160
+
161
+ # Stop keepalive
162
+ if getattr(wigi_instance, "keepalive_manager", None):
163
+ wigi_instance.keepalive_manager.stop()
164
+
165
+ # Clear / snooze display
166
+ wigidash = getattr(wigi_instance, "wigidash", None)
167
+ if wigidash:
168
+ try:
169
+ wigidash.clear_page(0)
170
+ wigidash.snooze_device()
171
+ except Exception as e:
172
+ logger.warning(f"Error shutting down display: {e}")
173
+
174
+ # Disconnect USB
175
+ usb_dev = getattr(wigi_instance, "usb_device", None)
176
+ if usb_dev:
177
+ try:
178
+ logger.info("Disconnecting USB device...")
179
+ usb_dev.disconnect()
180
+ logger.info("USB device disconnected.")
181
+ except Exception as e:
182
+ logger.warning(f"Error disconnecting USB: {e}")
@@ -0,0 +1,9 @@
1
+ Pillow>=11.3.0
2
+ matplotlib>=3.10.7
3
+ numpy>=2.2.0
4
+ pyusb>=1.3.1
5
+ pyserial>=3.5
6
+
7
+ # Provides a prebuilt libusb-1.0 backend for pyusb on Windows (no conda
8
+ # required) — pyusb auto-discovers it once installed.
9
+ libusb-package>=1.0.26.1; sys_platform == "win32"