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,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Statistics Module for TUI and other tools
|
|
3
|
+
|
|
4
|
+
Provides thread-safe channel statistics tracking with min/max/average
|
|
5
|
+
calculations that can be used by any tool consuming telemetry data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from typing import Dict, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _make_stat() -> Dict[str, float]:
|
|
14
|
+
"""Create a new statistics dictionary."""
|
|
15
|
+
return {'min': float('inf'), 'max': float('-inf'), 'sum': 0.0, 'count': 0}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ChannelStats:
|
|
19
|
+
"""Thread-safe per-device, per-channel running statistics.
|
|
20
|
+
|
|
21
|
+
Tracks minimum, maximum, and average values for numeric telemetry channels
|
|
22
|
+
across multiple devices. Can be used by TUI, dashboard, or any other tool
|
|
23
|
+
that needs statistical analysis of telemetry data.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize statistics tracker."""
|
|
28
|
+
self._lock = threading.Lock()
|
|
29
|
+
# device -> channel -> stat dict
|
|
30
|
+
self._data: Dict[str, Dict[str, Dict[str, float]]
|
|
31
|
+
] = defaultdict(lambda: defaultdict(_make_stat))
|
|
32
|
+
|
|
33
|
+
def update(self, device: str, channel: str, value: float) -> None:
|
|
34
|
+
"""Update statistics for a device channel.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
device: Device identifier (UID or port)
|
|
38
|
+
channel: Channel name (e.g., 'SYS_Power', 'Chip_Temp')
|
|
39
|
+
value: Numeric value to add to statistics
|
|
40
|
+
"""
|
|
41
|
+
if value is None or not isinstance(value, (int, float)):
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
with self._lock:
|
|
45
|
+
s = self._data[device][channel]
|
|
46
|
+
if value < s['min']:
|
|
47
|
+
s['min'] = value
|
|
48
|
+
if value > s['max']:
|
|
49
|
+
s['max'] = value
|
|
50
|
+
s['sum'] += value
|
|
51
|
+
s['count'] += 1
|
|
52
|
+
|
|
53
|
+
def get(self,
|
|
54
|
+
device: str,
|
|
55
|
+
channel: str) -> Tuple[Optional[float],
|
|
56
|
+
Optional[float],
|
|
57
|
+
Optional[float]]:
|
|
58
|
+
"""Get statistics for a device channel.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
device: Device identifier
|
|
62
|
+
channel: Channel name
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple of (min, max, avg) or (None, None, None) if no data
|
|
66
|
+
"""
|
|
67
|
+
with self._lock:
|
|
68
|
+
s = self._data[device][channel]
|
|
69
|
+
if s['count'] == 0:
|
|
70
|
+
return None, None, None
|
|
71
|
+
return s['min'], s['max'], s['sum'] / s['count']
|
|
72
|
+
|
|
73
|
+
def get_all(self,
|
|
74
|
+
device: str) -> Dict[str,
|
|
75
|
+
Tuple[Optional[float],
|
|
76
|
+
Optional[float],
|
|
77
|
+
Optional[float]]]:
|
|
78
|
+
"""Get statistics for all channels of a device.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
device: Device identifier
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Dictionary mapping channel names to (min, max, avg) tuples
|
|
85
|
+
"""
|
|
86
|
+
with self._lock:
|
|
87
|
+
result = {}
|
|
88
|
+
for channel in self._data[device]:
|
|
89
|
+
s = self._data[device][channel]
|
|
90
|
+
if s['count'] == 0:
|
|
91
|
+
result[channel] = (None, None, None)
|
|
92
|
+
else:
|
|
93
|
+
result[channel] = (
|
|
94
|
+
s['min'], s['max'], s['sum'] / s['count'])
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
def reset(self, device: Optional[str] = None) -> None:
|
|
98
|
+
"""Reset statistics for a device or all devices.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
device: Device identifier to reset, or None to reset all devices
|
|
102
|
+
"""
|
|
103
|
+
with self._lock:
|
|
104
|
+
if device is None:
|
|
105
|
+
# Reset all devices
|
|
106
|
+
self._data.clear()
|
|
107
|
+
else:
|
|
108
|
+
# Reset specific device
|
|
109
|
+
if device in self._data:
|
|
110
|
+
self._data[device].clear()
|
|
111
|
+
|
|
112
|
+
def get_devices(self) -> list[str]:
|
|
113
|
+
"""Get list of all devices with statistics.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of device identifiers with recorded statistics
|
|
117
|
+
"""
|
|
118
|
+
with self._lock:
|
|
119
|
+
return list(self._data.keys())
|
|
120
|
+
|
|
121
|
+
def get_channels(self, device: str) -> list[str]:
|
|
122
|
+
"""Get list of all channels for a device.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
device: Device identifier
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of channel names with recorded statistics for the device
|
|
129
|
+
"""
|
|
130
|
+
with self._lock:
|
|
131
|
+
if device in self._data:
|
|
132
|
+
return list(self._data[device].keys())
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
def has_data(self, device: str, channel: Optional[str] = None) -> bool:
|
|
136
|
+
"""Check if statistics data exists.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
device: Device identifier
|
|
140
|
+
channel: Optional channel name to check specifically
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if statistics data exists
|
|
144
|
+
"""
|
|
145
|
+
with self._lock:
|
|
146
|
+
if device not in self._data:
|
|
147
|
+
return False
|
|
148
|
+
if channel is None:
|
|
149
|
+
return bool(self._data[device])
|
|
150
|
+
return (channel in self._data[device]
|
|
151
|
+
and self._data[device][channel]['count'] > 0)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class StatsFormatter:
|
|
155
|
+
"""Helper class for formatting statistics for display."""
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def format_stat_string(min_val: Optional[float], max_val: Optional[float],
|
|
159
|
+
avg_val: Optional[float], decimals: int = 1,
|
|
160
|
+
unit: str = "", width: int = 10) -> str:
|
|
161
|
+
"""Format statistics as a display string.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
min_val: Minimum value
|
|
165
|
+
max_val: Maximum value
|
|
166
|
+
avg_val: Average value
|
|
167
|
+
decimals: Number of decimal places
|
|
168
|
+
unit: Unit string (e.g., 'W', '°C', 'V')
|
|
169
|
+
width: Fixed width for each stat column
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Formatted string like "↓10.2W ↑15.8W ~13.1W"
|
|
173
|
+
"""
|
|
174
|
+
if min_val is None or max_val is None or avg_val is None:
|
|
175
|
+
return ""
|
|
176
|
+
|
|
177
|
+
lo = f"↓{min_val:.{decimals}f}{unit}"
|
|
178
|
+
hi = f"↑{max_val:.{decimals}f}{unit}"
|
|
179
|
+
av = f"~{avg_val:.{decimals}f}{unit}"
|
|
180
|
+
|
|
181
|
+
return f" {lo:<{width}} {hi:<{width}} {av:<{width}}"
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def format_compact_range(
|
|
185
|
+
min_val: Optional[float],
|
|
186
|
+
max_val: Optional[float],
|
|
187
|
+
avg_val: Optional[float],
|
|
188
|
+
decimals: int = 1,
|
|
189
|
+
unit: str = "") -> str:
|
|
190
|
+
"""Format statistics as a compact range string.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
min_val: Minimum value
|
|
194
|
+
max_val: Maximum value
|
|
195
|
+
avg_val: Average value
|
|
196
|
+
decimals: Number of decimal places
|
|
197
|
+
unit: Unit string
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Formatted string like "10.2-15.8 ~13.1W"
|
|
201
|
+
"""
|
|
202
|
+
if min_val is None or max_val is None or avg_val is None:
|
|
203
|
+
return ""
|
|
204
|
+
|
|
205
|
+
return f"{
|
|
206
|
+
min_val:.{decimals}f}-{
|
|
207
|
+
max_val:.{decimals}f} ~{
|
|
208
|
+
avg_val:.{decimals}f}{unit}"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Convenience function for creating a stats callback
|
|
212
|
+
def create_stats_callback(stats: ChannelStats):
|
|
213
|
+
"""Create a callback function for DataSourceManager.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
stats: ChannelStats instance to update
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Callback function that can be passed to DataSourceManager
|
|
220
|
+
"""
|
|
221
|
+
def callback(device_uid: str, channel: str, value: float):
|
|
222
|
+
stats.update(device_uid, channel, value)
|
|
223
|
+
|
|
224
|
+
return callback
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Enhanced BENCHLAB CSV Fleet Logger
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Enhanced CSV Fleet Logger provides robust, lightweight telemetry logging for BENCHLAB devices with improved error handling, performance optimization, and cross-platform compatibility.
|
|
6
|
+
|
|
7
|
+
### Key Improvements
|
|
8
|
+
|
|
9
|
+
- **Configuration-driven**: Support for config files and environment variables
|
|
10
|
+
- **Enhanced error handling**: Exponential backoff retry logic and auto-reconnection
|
|
11
|
+
- **Performance optimized**: Buffered writes and async file I/O
|
|
12
|
+
- **Cross-platform**: Optimized for both Windows and Linux environments
|
|
13
|
+
- **Multiple formats**: Support for CSV and JSON output formats
|
|
14
|
+
- **Silent operation**: Headless mode for automated deployments
|
|
15
|
+
- **Connection monitoring**: Automatic reconnection for dropped devices
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
| Feature | Description |
|
|
22
|
+
|---------|-------------|
|
|
23
|
+
| **Configurable operation** | File-based and environment variable configuration |
|
|
24
|
+
| **Smart device discovery** | Enhanced error handling during device detection |
|
|
25
|
+
| **Buffered writes** | Configurable buffer size for optimal disk I/O |
|
|
26
|
+
| **Auto-reconnection** | Automatic reconnection for dropped serial connections |
|
|
27
|
+
| **Multiple output formats** | CSV and JSON format support |
|
|
28
|
+
| **Silent mode** | Headless operation for automated scripts |
|
|
29
|
+
| **Cross-platform** | Optimized for Windows, Linux, and embedded systems |
|
|
30
|
+
| **Memory efficient** | Circular buffers and automatic cleanup |
|
|
31
|
+
| **Detailed logging** | Configurable log levels and structured output |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
Install the tool's dependencies (from `benchlab/csv_log/requirements.txt`):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install -r benchlab/csv_log/requirements.txt
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Key dependencies: `pyserial`, `benchlab-pycore`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
### Configuration File
|
|
50
|
+
|
|
51
|
+
Create a `csv_logger.config` file in your working directory. Only the settings below are actually read by `LoggerConfig`/`load_config()`; other keys are ignored:
|
|
52
|
+
|
|
53
|
+
```ini
|
|
54
|
+
[logger]
|
|
55
|
+
interval = 1.0 # Logging interval in seconds
|
|
56
|
+
output_dir = logs # Output directory
|
|
57
|
+
buffer_size = 100 # Rows buffered before a batched write
|
|
58
|
+
format = csv # Output format: csv or json (currently informational; batcher writes CSV)
|
|
59
|
+
silent_mode = false # Silent operation (also auto-selects devices)
|
|
60
|
+
auto_select = false # Auto-select all discovered devices, skip the prompt
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Environment Variables
|
|
64
|
+
|
|
65
|
+
Override select settings with environment variables (read in `load_config()`):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export CSV_LOG_INTERVAL=0.5
|
|
69
|
+
export CSV_LOG_OUTPUT_DIR=/var/log/benchlab
|
|
70
|
+
export CSV_LOG_BUFFER_SIZE=200
|
|
71
|
+
export CSV_LOG_SILENT=true
|
|
72
|
+
export CSV_LOG_AUTO_SELECT=true
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`BENCHLAB_AUTO_SELECT=true` is also honored by `run_enhanced_csv_logger()` (used when launched via `python -m benchlab -logfleet`) to force auto-selection.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
### Command Line Interface
|
|
82
|
+
|
|
83
|
+
Via the main BENCHLAB launcher (recommended):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Basic usage with default settings (direct/serial source, 1s interval)
|
|
87
|
+
python -m benchlab -logfleet
|
|
88
|
+
|
|
89
|
+
# Custom interval
|
|
90
|
+
python -m benchlab -logfleet -i 0.5
|
|
91
|
+
|
|
92
|
+
# Choose a data source (direct | fastapi | fastapi_custom | mqtt | mqtt_custom | named_pipe | service_http)
|
|
93
|
+
python -m benchlab -logfleet --source fastapi --api-url http://127.0.0.1:8000
|
|
94
|
+
python -m benchlab -logfleet --source mqtt --mqtt-broker localhost --mqtt-port 1883
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`-i`/`--interval`, `--source`, `--api-url`, `--api-port`, `--mqtt-broker`, and `--mqtt-port` are the standard `benchlab/main.py` CLI flags; the tool itself has no `-logfleet`-specific flags beyond what `main.py` exposes.
|
|
98
|
+
|
|
99
|
+
Running the module standalone (bypasses the main launcher, uses its own small argparse CLI limited to `direct`/`fastapi`/`mqtt` sources):
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python -m benchlab.csv_log.csv_logger_enhanced -i 0.5 -c my_config.config
|
|
103
|
+
python -m benchlab.csv_log.csv_logger_enhanced --silent --auto-select
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Programmatic Usage
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from benchlab.csv_log.csv_logger_enhanced import EnhancedCSVLogger, LoggerConfig
|
|
110
|
+
import types
|
|
111
|
+
|
|
112
|
+
# Create configuration
|
|
113
|
+
config = LoggerConfig(
|
|
114
|
+
interval=0.5,
|
|
115
|
+
output_dir="custom_logs",
|
|
116
|
+
buffer_size=50,
|
|
117
|
+
silent_mode=True,
|
|
118
|
+
auto_select=True,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# args mirrors the standard benchlab CLI namespace (source/api_url/mqtt_broker/mqtt_port)
|
|
122
|
+
args = types.SimpleNamespace(source="direct", interval=0.5, api_url="http://127.0.0.1:8000",
|
|
123
|
+
mqtt_broker="localhost", mqtt_port=1883)
|
|
124
|
+
|
|
125
|
+
# Create and run logger (use as a context manager so stop_logging() always runs)
|
|
126
|
+
with EnhancedCSVLogger(config, args=args) as logger:
|
|
127
|
+
logger.start_logging()
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Output Format
|
|
133
|
+
|
|
134
|
+
Rows are written as CSV via the batching logger (`benchlab/csv_log/message_batcher.py`):
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
Timestamp,uid,SYS_Power,CPU_Power,GPU_Power,Temp1,Temp2,...
|
|
138
|
+
2025-10-06T10:15:01.123456,BL-1234,120,50,30,65,70,...
|
|
139
|
+
2025-10-06T10:15:02.123456,BL-1234,118,49,31,65,71,...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The `format` config key is accepted by `LoggerConfig` but the batcher currently always writes CSV; JSON output is not implemented.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Behavior Notes
|
|
147
|
+
|
|
148
|
+
- **Buffered writes**: rows are batched (default `buffer_size = 100`) and flushed via `BatchingLogger`, with a background flush every 5 seconds and an explicit flush each polling cycle.
|
|
149
|
+
- **Retry on connect**: initial connection to the data source uses `SmartRetryManager` (3 attempts, exponential backoff) — see `benchlab/csv_log/smart_retry.py`.
|
|
150
|
+
- **Silent/auto-select**: `silent_mode` or `auto_select` (or `BENCHLAB_AUTO_SELECT=true`) skip the interactive device-selection prompt and log at `WARNING` level instead of `INFO`.
|
|
151
|
+
- **Graceful shutdown**: Ctrl+C stops the polling loop, flushes the batcher, and disconnects the data source (`EnhancedCSVLogger.stop_logging()`).
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Troubleshooting
|
|
156
|
+
|
|
157
|
+
### Device Not Detected
|
|
158
|
+
```bash
|
|
159
|
+
python -c "from benchlab_pycore.core import get_benchlab_ports; print(get_benchlab_ports())"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Permission Errors (Linux)
|
|
163
|
+
```bash
|
|
164
|
+
# Add user to dialout group for serial access
|
|
165
|
+
sudo usermod -a -G dialout $USER
|
|
166
|
+
# Log out and back in, or restart
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### No devices selected / logging exits immediately
|
|
170
|
+
Ensure the configured `--source` (direct/fastapi/mqtt/etc.) is actually running and reachable — the logger connects via `DataSourceManager`, and a failed connection or empty device discovery causes it to log an error and stop.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## API Reference
|
|
175
|
+
|
|
176
|
+
### EnhancedCSVLogger Class
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
class EnhancedCSVLogger:
|
|
180
|
+
def __init__(self, config: LoggerConfig, args=None)
|
|
181
|
+
def start_logging(self)
|
|
182
|
+
def stop_logging(self)
|
|
183
|
+
def discover_devices(self) -> List[DeviceConfig]
|
|
184
|
+
def select_devices(self, devices: List[DeviceConfig]) -> List[DeviceConfig]
|
|
185
|
+
def create_batcher(self)
|
|
186
|
+
def log_device_data(self, uid: str) -> bool
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### LoggerConfig Dataclass
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
@dataclass
|
|
193
|
+
class LoggerConfig:
|
|
194
|
+
interval: float = 1.0
|
|
195
|
+
output_dir: str = "logs"
|
|
196
|
+
buffer_size: int = 100
|
|
197
|
+
format: str = "csv" # accepted, but only CSV output is implemented
|
|
198
|
+
silent_mode: bool = False
|
|
199
|
+
auto_select: bool = False
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Support
|
|
205
|
+
|
|
206
|
+
For issues, check the Troubleshooting section above or report bugs via GitHub issues.
|