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,466 @@
1
+ """
2
+ Smart Retry Logic for BENCHLAB CSV Logger
3
+ Provides intelligent retry strategies with exponential backoff, jitter,
4
+ and circuit breaker patterns
5
+ """
6
+
7
+ import time
8
+ import random
9
+ import logging
10
+ import threading
11
+ from typing import Callable, Any, Optional, Dict, List, Union
12
+ from enum import Enum
13
+ from dataclasses import dataclass
14
+ from functools import wraps
15
+
16
+ try:
17
+ import serial
18
+ _SERIAL_EXCEPTIONS: List[type] = [
19
+ OSError, serial.SerialException, TimeoutError]
20
+ except ImportError:
21
+ serial = None
22
+ _SERIAL_EXCEPTIONS = [OSError, TimeoutError]
23
+
24
+
25
+ class RetryStrategy(Enum):
26
+ """Retry strategy types"""
27
+ EXPONENTIAL_BACKOFF = "exponential_backoff"
28
+ LINEAR_BACKOFF = "linear_backoff"
29
+ FIXED_DELAY = "fixed_delay"
30
+ JITTERED_EXPONENTIAL = "jittered_exponential"
31
+
32
+
33
+ class CircuitState(Enum):
34
+ """Circuit breaker states"""
35
+ CLOSED = "closed" # Normal operation
36
+ OPEN = "open" # Circuit is open, failing fast
37
+ HALF_OPEN = "half_open" # Testing if circuit can be closed
38
+
39
+
40
+ @dataclass
41
+ class RetryConfig:
42
+ """Configuration for retry logic"""
43
+ max_retries: int = 3
44
+ base_delay: float = 1.0
45
+ max_delay: float = 60.0
46
+ strategy: RetryStrategy = RetryStrategy.EXPONENTIAL_BACKOFF
47
+ jitter: bool = True
48
+ jitter_factor: float = 0.1
49
+ timeout: Optional[float] = None
50
+ retryable_exceptions: List[type] = None
51
+ circuit_breaker_enabled: bool = True
52
+ circuit_failure_threshold: int = 5
53
+ circuit_recovery_timeout: float = 60.0
54
+
55
+
56
+ class CircuitBreaker:
57
+ """Circuit breaker implementation for preventing cascading failures"""
58
+
59
+ def __init__(self, config: RetryConfig):
60
+ self.config = config
61
+ self.state = CircuitState.CLOSED
62
+ self.failure_count = 0
63
+ self.success_count = 0
64
+ self.last_failure_time = 0.0
65
+ self.lock = threading.Lock()
66
+ self.logger = logging.getLogger(__name__)
67
+
68
+ def call(self, func: Callable, *args, **kwargs) -> Any:
69
+ """Execute function with circuit breaker protection"""
70
+ with self.lock:
71
+ if self.state == CircuitState.OPEN:
72
+ since_failure = time.time() - self.last_failure_time
73
+ if since_failure > self.config.circuit_recovery_timeout:
74
+ self.logger.info(
75
+ "Circuit breaker: transitioning to half-open")
76
+ self.state = CircuitState.HALF_OPEN
77
+ self.success_count = 0
78
+ else:
79
+ raise CircuitBreakerOpenError("Circuit breaker is open")
80
+
81
+ try:
82
+ result = func(*args, **kwargs)
83
+ self._on_success()
84
+ return result
85
+ except Exception as e:
86
+ self._on_failure()
87
+ raise e
88
+
89
+ def _on_success(self):
90
+ """Handle successful operation"""
91
+ if self.state == CircuitState.HALF_OPEN:
92
+ self.success_count += 1
93
+ if self.success_count >= 3: # Require 3 successes to close
94
+ self.logger.info("Circuit breaker: transitioning to closed")
95
+ self.state = CircuitState.CLOSED
96
+ self.failure_count = 0
97
+ self.success_count = 0
98
+
99
+ def _on_failure(self):
100
+ """Handle failed operation"""
101
+ self.failure_count += 1
102
+ self.last_failure_time = time.time()
103
+
104
+ if (self.config.circuit_breaker_enabled and
105
+ self.failure_count >= self.config.circuit_failure_threshold):
106
+ self.logger.warning(
107
+ f"Circuit breaker: opening after {
108
+ self.failure_count} failures")
109
+ self.state = CircuitState.OPEN
110
+
111
+
112
+ class CircuitBreakerOpenError(Exception):
113
+ """Exception raised when circuit breaker is open"""
114
+ pass
115
+
116
+
117
+ class SmartRetryManager:
118
+ """Smart retry manager with multiple retry strategies"""
119
+
120
+ def __init__(self, config: RetryConfig = None):
121
+ self.config = config or RetryConfig()
122
+ self.circuit_breaker = CircuitBreaker(
123
+ self.config) if self.config.circuit_breaker_enabled else None
124
+ self.logger = logging.getLogger(__name__)
125
+
126
+ def execute(self, func: Callable, *args, **kwargs) -> Any:
127
+ """Execute function with smart retry logic"""
128
+ last_exception = None
129
+
130
+ for attempt in range(self.config.max_retries + 1):
131
+ try:
132
+ if self.circuit_breaker:
133
+ return self.circuit_breaker.call(func, *args, **kwargs)
134
+ else:
135
+ return func(*args, **kwargs)
136
+
137
+ except Exception as e:
138
+ last_exception = e
139
+
140
+ # Check if exception is retryable
141
+ if not self._is_retryable_exception(e):
142
+ self.logger.error(f"Non-retryable exception: {e}")
143
+ raise e
144
+
145
+ if attempt == self.config.max_retries:
146
+ self.logger.error(
147
+ f"Max retries ({
148
+ self.config.max_retries}) exceeded for {
149
+ func.__name__}")
150
+ raise e
151
+
152
+ # Calculate delay
153
+ delay = self._calculate_delay(attempt)
154
+
155
+ self.logger.warning(
156
+ f"Attempt {attempt + 1}/{self.config.max_retries + 1} "
157
+ f"failed for {func.__name__}: {e}. "
158
+ f"Retrying in {delay:.2f}s"
159
+ )
160
+
161
+ time.sleep(delay)
162
+
163
+ # This should never be reached, but just in case
164
+ raise last_exception
165
+
166
+ def _is_retryable_exception(self, exception: Exception) -> bool:
167
+ """Check if exception is retryable"""
168
+ if not self.config.retryable_exceptions:
169
+ # If no specific exceptions defined, assume all are retryable
170
+ # except for KeyboardInterrupt and SystemExit
171
+ return not isinstance(exception, (KeyboardInterrupt, SystemExit))
172
+
173
+ return any(isinstance(exception, exc_type)
174
+ for exc_type in self.config.retryable_exceptions)
175
+
176
+ def _calculate_delay(self, attempt: int) -> float:
177
+ """Calculate delay based on retry strategy"""
178
+ if self.config.strategy == RetryStrategy.FIXED_DELAY:
179
+ delay = self.config.base_delay
180
+
181
+ elif self.config.strategy == RetryStrategy.LINEAR_BACKOFF:
182
+ delay = self.config.base_delay * (attempt + 1)
183
+
184
+ elif self.config.strategy in [
185
+ RetryStrategy.EXPONENTIAL_BACKOFF,
186
+ RetryStrategy.JITTERED_EXPONENTIAL]:
187
+ delay = self.config.base_delay * (2 ** attempt)
188
+
189
+ if (self.config.jitter and self.config.strategy ==
190
+ RetryStrategy.JITTERED_EXPONENTIAL):
191
+ # Add jitter
192
+ jitter_amount = delay * self.config.jitter_factor
193
+ delay += random.uniform(-jitter_amount, jitter_amount)
194
+ delay = max(0, delay) # Ensure delay is not negative
195
+
196
+ # Cap delay at max_delay
197
+ return min(delay, self.config.max_delay)
198
+
199
+
200
+ def smart_retry(config: Union[RetryConfig, Dict, None] = None):
201
+ """Decorator for applying smart retry logic to functions"""
202
+ def decorator(func: Callable) -> Callable:
203
+ @wraps(func)
204
+ def wrapper(*args, **kwargs):
205
+ # Create config from dict if provided
206
+ retry_config = config
207
+ if isinstance(config, dict):
208
+ retry_config = RetryConfig(**config)
209
+ elif config is None:
210
+ retry_config = RetryConfig()
211
+
212
+ manager = SmartRetryManager(retry_config)
213
+ return manager.execute(func, *args, **kwargs)
214
+ return wrapper
215
+ return decorator
216
+
217
+
218
+ class AdaptiveRetryManager:
219
+ """Adaptive retry manager that learns from failure patterns"""
220
+
221
+ def __init__(self, config: RetryConfig = None):
222
+ self.config = config or RetryConfig()
223
+ self.failure_history = [] # List of (timestamp, delay_used, succeeded)
224
+ self.logger = logging.getLogger(__name__)
225
+ self.lock = threading.Lock()
226
+
227
+ def execute(self, func: Callable, *args, **kwargs) -> Any:
228
+ """Execute function with adaptive retry logic"""
229
+ last_exception = None
230
+
231
+ for attempt in range(self.config.max_retries + 1):
232
+ try:
233
+ result = func(*args, **kwargs)
234
+ self._record_success(attempt)
235
+ return result
236
+
237
+ except Exception as e:
238
+ last_exception = e
239
+
240
+ if not self._is_retryable_exception(e):
241
+ raise e
242
+
243
+ if attempt == self.config.max_retries:
244
+ self.logger.error(
245
+ f"Max retries ({
246
+ self.config.max_retries}) exceeded for {
247
+ func.__name__}")
248
+ raise e
249
+
250
+ # Calculate adaptive delay
251
+ delay = self._calculate_adaptive_delay(attempt)
252
+
253
+ self.logger.warning(
254
+ f"Attempt {attempt + 1}/{self.config.max_retries + 1} "
255
+ f"failed for {func.__name__}: {e}. "
256
+ f"Using adaptive delay: {delay:.2f}s"
257
+ )
258
+
259
+ self._record_failure(attempt, delay)
260
+ time.sleep(delay)
261
+
262
+ raise last_exception
263
+
264
+ def _calculate_adaptive_delay(self, attempt: int) -> float:
265
+ """Calculate delay based on historical failure patterns"""
266
+ with self.lock:
267
+ if not self.failure_history:
268
+ # No history, use base strategy
269
+ return self._calculate_base_delay(attempt)
270
+
271
+ # Analyze recent failures
272
+ # Last 10 failures
273
+ recent_failures = [
274
+ f for f in self.failure_history[-10:] if not f[2]]
275
+
276
+ if not recent_failures:
277
+ return self._calculate_base_delay(attempt)
278
+
279
+ # Calculate average delay that failed
280
+ avg_failed_delay = sum(
281
+ f[1] for f in recent_failures) / len(recent_failures)
282
+
283
+ # If recent failures had short delays, try longer delays
284
+ if avg_failed_delay < self.config.base_delay * 2:
285
+ base_delay = self.config.base_delay * 3
286
+ else:
287
+ base_delay = self.config.base_delay
288
+
289
+ # Apply exponential backoff with the adapted base
290
+ delay = base_delay * (2 ** attempt)
291
+ return min(delay, self.config.max_delay)
292
+
293
+ def _calculate_base_delay(self, attempt: int) -> float:
294
+ """Calculate base delay using standard strategies"""
295
+ if self.config.strategy == RetryStrategy.FIXED_DELAY:
296
+ return self.config.base_delay
297
+ elif self.config.strategy == RetryStrategy.LINEAR_BACKOFF:
298
+ return self.config.base_delay * (attempt + 1)
299
+ else: # Exponential backoff
300
+ delay = self.config.base_delay * (2 ** attempt)
301
+ if self.config.jitter:
302
+ jitter_amount = delay * self.config.jitter_factor
303
+ delay += random.uniform(-jitter_amount, jitter_amount)
304
+ delay = max(0, delay)
305
+ return min(delay, self.config.max_delay)
306
+
307
+ def _record_success(self, attempt: int):
308
+ """Record a successful operation"""
309
+ with self.lock:
310
+ self.failure_history.append((time.time(), 0, True))
311
+ # Keep only last 100 entries
312
+ if len(self.failure_history) > 100:
313
+ self.failure_history.pop(0)
314
+
315
+ def _record_failure(self, attempt: int, delay: float):
316
+ """Record a failed operation"""
317
+ with self.lock:
318
+ self.failure_history.append((time.time(), delay, False))
319
+ # Keep only last 100 entries
320
+ if len(self.failure_history) > 100:
321
+ self.failure_history.pop(0)
322
+
323
+ def _is_retryable_exception(self, exception: Exception) -> bool:
324
+ """Check if exception is retryable"""
325
+ if not self.config.retryable_exceptions:
326
+ return not isinstance(exception, (KeyboardInterrupt, SystemExit))
327
+
328
+ return any(isinstance(exception, exc_type)
329
+ for exc_type in self.config.retryable_exceptions)
330
+
331
+ def get_stats(self) -> Dict:
332
+ """Get retry statistics"""
333
+ with self.lock:
334
+ total_attempts = len(self.failure_history)
335
+ successes = sum(
336
+ 1 for _, _, success in self.failure_history if success)
337
+ failures = total_attempts - successes
338
+
339
+ if total_attempts == 0:
340
+ return {
341
+ 'total_attempts': 0,
342
+ 'success_rate': 0.0,
343
+ 'avg_delay': 0.0,
344
+ 'recent_failures': 0
345
+ }
346
+
347
+ recent_failures = [
348
+ f for f in self.failure_history[-10:] if not f[2]]
349
+
350
+ return {
351
+ 'total_attempts': total_attempts,
352
+ 'successes': successes,
353
+ 'failures': failures,
354
+ 'success_rate': successes / total_attempts,
355
+ 'avg_delay': sum(
356
+ delay for _,
357
+ delay,
358
+ _ in self.failure_history) / total_attempts,
359
+ 'recent_failures': len(recent_failures)}
360
+
361
+
362
+ # Pre-configured retry managers for common use cases
363
+ SERIAL_RETRY_CONFIG = RetryConfig(
364
+ max_retries=5,
365
+ base_delay=0.5,
366
+ max_delay=10.0,
367
+ strategy=RetryStrategy.JITTERED_EXPONENTIAL,
368
+ jitter_factor=0.2,
369
+ retryable_exceptions=_SERIAL_EXCEPTIONS,
370
+ )
371
+
372
+ NETWORK_RETRY_CONFIG = RetryConfig(
373
+ max_retries=3,
374
+ base_delay=1.0,
375
+ max_delay=30.0,
376
+ strategy=RetryStrategy.EXPONENTIAL_BACKOFF,
377
+ circuit_breaker_enabled=True,
378
+ circuit_failure_threshold=3,
379
+ circuit_recovery_timeout=60.0
380
+ )
381
+
382
+ FILE_RETRY_CONFIG = RetryConfig(
383
+ max_retries=3,
384
+ base_delay=0.1,
385
+ max_delay=5.0,
386
+ strategy=RetryStrategy.LINEAR_BACKOFF,
387
+ retryable_exceptions=[OSError, IOError, PermissionError]
388
+ )
389
+
390
+
391
+ # Convenience functions
392
+ def retry_serial_operation(func: Callable, *args, **kwargs) -> Any:
393
+ """Execute function with serial-specific retry logic"""
394
+ manager = SmartRetryManager(SERIAL_RETRY_CONFIG)
395
+ return manager.execute(func, *args, **kwargs)
396
+
397
+
398
+ def retry_network_operation(func: Callable, *args, **kwargs) -> Any:
399
+ """Execute function with network-specific retry logic"""
400
+ manager = SmartRetryManager(NETWORK_RETRY_CONFIG)
401
+ return manager.execute(func, *args, **kwargs)
402
+
403
+
404
+ def retry_file_operation(func: Callable, *args, **kwargs) -> Any:
405
+ """Execute function with file-specific retry logic"""
406
+ manager = SmartRetryManager(FILE_RETRY_CONFIG)
407
+ return manager.execute(func, *args, **kwargs)
408
+
409
+
410
+ # Decorator shortcuts
411
+ retry_serial = smart_retry(SERIAL_RETRY_CONFIG)
412
+ retry_network = smart_retry(NETWORK_RETRY_CONFIG)
413
+ retry_file = smart_retry(FILE_RETRY_CONFIG)
414
+
415
+
416
+ if __name__ == '__main__':
417
+ # Example usage and testing
418
+ import serial
419
+
420
+ # Example 1: Using the decorator
421
+ @retry_serial
422
+ def read_serial_port(port: str) -> str:
423
+ """Example function that reads from a serial port"""
424
+ ser = serial.Serial(port, timeout=1)
425
+ try:
426
+ return ser.readline().decode('utf-8').strip()
427
+ finally:
428
+ ser.close()
429
+
430
+ # Example 2: Using the manager directly
431
+ def write_to_file_safely(filename: str, data: str):
432
+ """Example function that writes to a file with retry logic"""
433
+ manager = SmartRetryManager(FILE_RETRY_CONFIG)
434
+
435
+ def _write():
436
+ with open(filename, 'w') as f:
437
+ f.write(data)
438
+
439
+ return manager.execute(_write)
440
+
441
+ # Example 3: Using adaptive retry
442
+ def network_request_with_adaptive_retry(url: str):
443
+ """Example function using adaptive retry"""
444
+ manager = AdaptiveRetryManager(NETWORK_RETRY_CONFIG)
445
+
446
+ def _request():
447
+ # Simulate network request
448
+ import requests
449
+ response = requests.get(url, timeout=5)
450
+ return response.text
451
+
452
+ return manager.execute(_request)
453
+
454
+ print("Smart retry logic loaded successfully!")
455
+ print("Available retry managers:")
456
+ print("- SmartRetryManager: Standard smart retry")
457
+ print("- AdaptiveRetryManager: Learning-based retry")
458
+ print("- CircuitBreaker: Circuit breaker pattern")
459
+ print("\nPre-configured retry configs:")
460
+ print("- SERIAL_RETRY_CONFIG: For serial port operations")
461
+ print("- NETWORK_RETRY_CONFIG: For network operations")
462
+ print("- FILE_RETRY_CONFIG: For file operations")
463
+ print("\nDecorators:")
464
+ print("- @retry_serial: Decorate serial operations")
465
+ print("- @retry_network: Decorate network operations")
466
+ print("- @retry_file: Decorate file operations")
@@ -0,0 +1,117 @@
1
+ # BENCHLAB Graph
2
+
3
+ ## Overview
4
+
5
+ The BENCHLAB Graph module provides a **graphical interface** for real-time telemetry visualization.
6
+ It allows monitoring of connected BENCHLAB devices with selectable sensors and dynamic plotting of measurements.
7
+
8
+ Built using [Dear PyGui](https://dearpygui.readthedocs.io/), the module supports:
9
+
10
+ - Real-time device detection and selection
11
+ - Dynamic sensor graphing
12
+ - Live min/max/average value display
13
+ - Multi-device support
14
+
15
+ ---
16
+
17
+ ## Features
18
+
19
+ | Feature | Description |
20
+ |---------|-------------|
21
+ | Device Detection | Automatically scans for connected BENCHLAB devices and lists them in a combo box. |
22
+ | Sensor Selection | Choose which sensor to display from the device. |
23
+ | Real-Time Graph | Plots selected sensor data dynamically with adjustable axes. |
24
+ | Min/Max/Avg Display | Shows live statistics for the selected sensor on the graph. |
25
+ | Thread-Safe Updates | Sensor reading and graph updates run in separate threads with synchronization. |
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ Install the tool's dependencies (from `benchlab/graph/requirements.txt`):
32
+
33
+ ```
34
+ pip install -r benchlab/graph/requirements.txt
35
+ ```
36
+
37
+ **Dependencies:**
38
+
39
+ - `dearpygui>=2.1.1`
40
+ - `pyserial>=3.5`
41
+ - BENCHLAB core modules (`benchlab.core`)
42
+
43
+ ---
44
+
45
+ ## Usage
46
+
47
+ ### Launch Graph Module
48
+
49
+ From the main BENCHLAB launcher:
50
+
51
+ ```bash
52
+ # Default (direct/serial source, 1s sensor poll interval)
53
+ python -m benchlab -graph
54
+
55
+ # Custom interval
56
+ python -m benchlab -graph -i 0.5
57
+
58
+ # Choose a data source (direct | fastapi | fastapi_custom | mqtt | mqtt_custom | named_pipe | service_http)
59
+ python -m benchlab -graph --source fastapi --api-url http://127.0.0.1:8000
60
+ python -m benchlab -graph --source mqtt --mqtt-broker localhost --mqtt-port 1883
61
+ ```
62
+
63
+ `-i`/`--interval` sets `GraphApp.sensor_read_interval` (default 1.0s). `--source`, `--api-url`, `--mqtt-broker`, and `--mqtt-port` are the standard `benchlab/main.py` flags; the graph module has no `-graph`-specific CLI flags of its own.
64
+
65
+ ### Main GUI
66
+
67
+ - **Device Selection**: Select a device from the combo box.
68
+ - **Sensor Selection**: Open the sensor selection window to pick which metric to graph.
69
+ - **Graph Window**: Displays a live line chart of the selected sensor.
70
+ - **Statistics**: Minimum, maximum, and average values are shown and updated in real time.
71
+ - **Auto-Axis**: Y-axis limits adjust dynamically based on sensor data.
72
+
73
+ ---
74
+
75
+ ## Developer Notes
76
+
77
+ ### Class: `GraphApp`
78
+
79
+ Handles the main functionality of the graph module:
80
+
81
+ - **Device Management**
82
+ - `detect_devices()` – scans for devices and updates UI
83
+ - `device_changed()` – updates state when device selection changes
84
+ - `start_sensor_thread()` / `stop_sensor_thread()` – manage sensor polling in a background thread
85
+
86
+ - **Sensor Logic**
87
+ - `get_sensor_value()` – fetches a specific sensor value from the device
88
+
89
+ - **UI Windows**
90
+ - `show_sensor_selection()` – opens the sensor selection window
91
+ - `open_graph_window()` – launches the graph plotting window
92
+
93
+ - **Graph Update Loop**
94
+ - `update_graph_loop()` – continuously updates the graph with new sensor readings
95
+ - Dynamically adjusts axes and updates min/max/avg statistics
96
+
97
+ - **Main Run Loop**
98
+ - `run()` – creates the Dear PyGui context, viewport, and GUI update loop
99
+
100
+ ---
101
+
102
+ ## References
103
+
104
+ - [BENCHLAB core modules](https://github.com/<your-org>/benchlab/tree/main/benchlab/core)
105
+ - Related modules:
106
+ - [TUI](../tui/README.md)
107
+ - [CSV Logger](../csv_log/README.md)
108
+ - [MQTT Publisher](../mqtt/README.md)
109
+ - [VU](../vu/README.md)
110
+
111
+ ---
112
+
113
+ ## Notes
114
+
115
+ - The module is **thread-safe**: sensor reads and GUI updates run in separate threads.
116
+ - Designed for **real-time telemetry monitoring**, suitable for fleet management and lab setups.
117
+ - Y-axis auto-scaling ensures graphs remain readable even with rapidly changing sensor values.
@@ -0,0 +1,5 @@
1
+ """
2
+ BENCHLAB Telemetry Package
3
+ """
4
+
5
+ __version__ = "0.9.0"