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,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")
|
benchlab/graph/README.md
ADDED
|
@@ -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.
|