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,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON Schema and Validation for BENCHLAB Device Configuration
|
|
3
|
+
|
|
4
|
+
Defines Pydantic models for type-safe configuration handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional, Any, Dict, Literal
|
|
8
|
+
from pydantic import BaseModel, Field, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DeviceSelector(BaseModel):
|
|
12
|
+
"""Device selection criteria."""
|
|
13
|
+
type: Literal["guid", "productId", "pipeName", "port", "any"] = Field(
|
|
14
|
+
description="How to identify the device"
|
|
15
|
+
)
|
|
16
|
+
value: Optional[Any] = Field(
|
|
17
|
+
default=None,
|
|
18
|
+
description="Selector value (null for 'any')"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FanConfig(BaseModel):
|
|
23
|
+
"""Fan configuration for a single fan channel."""
|
|
24
|
+
fanId: int = Field(ge=0, le=8, description="Fan ID (0-8)")
|
|
25
|
+
FanMode: int = Field(ge=0, le=2, description="0=Auto, 1=Fixed, 2=Manual")
|
|
26
|
+
TempSource: int = Field(ge=0, le=5, description="Temperature sensor index")
|
|
27
|
+
Temp: List[int] = Field(
|
|
28
|
+
default=[300, 600],
|
|
29
|
+
min_length=2,
|
|
30
|
+
max_length=2,
|
|
31
|
+
description="Temperature curve points (0.1°C units)"
|
|
32
|
+
)
|
|
33
|
+
Duty: List[int] = Field(
|
|
34
|
+
default=[30, 80],
|
|
35
|
+
min_length=2,
|
|
36
|
+
max_length=2,
|
|
37
|
+
description="Duty cycle at each temp point (0-100%)"
|
|
38
|
+
)
|
|
39
|
+
RampStep: int = Field(
|
|
40
|
+
ge=0,
|
|
41
|
+
le=255,
|
|
42
|
+
default=5,
|
|
43
|
+
description="Rate of change")
|
|
44
|
+
FixedDuty: int = Field(
|
|
45
|
+
ge=0,
|
|
46
|
+
le=100,
|
|
47
|
+
default=50,
|
|
48
|
+
description="Fixed duty cycle")
|
|
49
|
+
MinDuty: int = Field(ge=0, le=100, default=20, description="Minimum duty")
|
|
50
|
+
MaxDuty: int = Field(ge=0, le=100, default=100, description="Maximum duty")
|
|
51
|
+
FanStop: int = Field(
|
|
52
|
+
ge=0,
|
|
53
|
+
le=1,
|
|
54
|
+
default=0,
|
|
55
|
+
description="0=disabled, 1=enabled")
|
|
56
|
+
|
|
57
|
+
@field_validator('Duty')
|
|
58
|
+
@classmethod
|
|
59
|
+
def validate_duty(cls, v):
|
|
60
|
+
for duty in v:
|
|
61
|
+
if not 0 <= duty <= 100:
|
|
62
|
+
raise ValueError(f"Duty values must be 0-100, got {duty}")
|
|
63
|
+
return v
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FanProfile(BaseModel):
|
|
67
|
+
"""Fan profile containing multiple fan configurations."""
|
|
68
|
+
profileId: int = Field(ge=0, le=2, description="Profile ID (0-2)")
|
|
69
|
+
fans: List[FanConfig] = Field(description="Fan configurations")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RGBConfig(BaseModel):
|
|
73
|
+
"""RGB profile configuration."""
|
|
74
|
+
profileId: int = Field(ge=0, le=1, description="Profile ID (0-1)")
|
|
75
|
+
Mode: int = Field(ge=0, le=9, description="RGB mode")
|
|
76
|
+
Red: int = Field(ge=0, le=255, description="Red component")
|
|
77
|
+
Green: int = Field(ge=0, le=255, description="Green component")
|
|
78
|
+
Blue: int = Field(ge=0, le=255, description="Blue component")
|
|
79
|
+
Direction: int = Field(ge=0, le=1, description="Animation direction")
|
|
80
|
+
Speed: int = Field(ge=0, le=100, description="Animation speed")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DeviceConfig(BaseModel):
|
|
84
|
+
"""Configuration for a single device."""
|
|
85
|
+
selector: DeviceSelector = Field(description="Device selection criteria")
|
|
86
|
+
deviceName: Optional[str] = Field(
|
|
87
|
+
default=None,
|
|
88
|
+
max_length=31,
|
|
89
|
+
description="Device friendly name"
|
|
90
|
+
)
|
|
91
|
+
fanProfiles: Optional[List[FanProfile]] = Field(
|
|
92
|
+
default=None,
|
|
93
|
+
description="Fan profile configurations"
|
|
94
|
+
)
|
|
95
|
+
rgbProfiles: Optional[List[RGBConfig]] = Field(
|
|
96
|
+
default=None,
|
|
97
|
+
description="RGB profile configurations"
|
|
98
|
+
)
|
|
99
|
+
calibration: Optional[Dict[str, Any]] = Field(
|
|
100
|
+
default=None,
|
|
101
|
+
description="Calibration data"
|
|
102
|
+
)
|
|
103
|
+
saveToFlash: bool = Field(
|
|
104
|
+
default=False,
|
|
105
|
+
description="Save config to flash after applying"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ConfigFile(BaseModel):
|
|
110
|
+
"""Root configuration file schema."""
|
|
111
|
+
version: str = Field(default="1.0", description="Schema version")
|
|
112
|
+
description: str = Field(
|
|
113
|
+
default="BENCHLAB Configuration",
|
|
114
|
+
description="Human-readable description"
|
|
115
|
+
)
|
|
116
|
+
devices: List[DeviceConfig] = Field(description="Device configurations")
|
|
117
|
+
|
|
118
|
+
@field_validator('devices')
|
|
119
|
+
@classmethod
|
|
120
|
+
def validate_devices(cls, v):
|
|
121
|
+
if not v:
|
|
122
|
+
raise ValueError("Configuration must contain at least one device")
|
|
123
|
+
return v
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate_config_file(config_dict: dict) -> ConfigFile:
|
|
127
|
+
"""Validate a configuration dictionary against the schema.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
config_dict: Dictionary loaded from JSON
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Validated ConfigFile object
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ValidationError: If validation fails
|
|
137
|
+
"""
|
|
138
|
+
return ConfigFile(**config_dict)
|
benchlab/core/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# BENCHLAB Core - Shared Internals
|
|
2
|
+
|
|
3
|
+
`benchlab/core/` is the internal library used by every BENCHLAB PyTools tool (TUI, FastAPI server, MQTT publisher, Link, HWiNFO export, VU dials, WigiDash, config tool). It provides the data-source abstraction that lets multiple tools consume telemetry without fighting over a serial port, plus supporting infrastructure: device lifecycle tracking, subprocess management, retry helpers, and statistics.
|
|
4
|
+
|
|
5
|
+
This is a developer-facing internals doc. For CLI usage of individual tools, see their own READMEs (`benchlab/restapi/readme.md`, `benchlab/mqtt/README.md`, `benchlab/config/README.md`, `benchlab/link/README.md`).
|
|
6
|
+
|
|
7
|
+
## Architecture Overview
|
|
8
|
+
|
|
9
|
+
### The Problem
|
|
10
|
+
|
|
11
|
+
Only one process can own a serial port at a time. If every tool connected directly via `benchlab_pycore`, only one tool could ever run at once.
|
|
12
|
+
|
|
13
|
+
### The Solution
|
|
14
|
+
|
|
15
|
+
A **DataSource abstraction layer** (`datasource.py`) with multiple implementations, all exposing the same interface. Tools pick a source type via `--source` and don't need to know how the data actually gets to them.
|
|
16
|
+
|
|
17
|
+
## Components
|
|
18
|
+
|
|
19
|
+
### 1. DataSource classes (`datasource.py`)
|
|
20
|
+
|
|
21
|
+
`DataSource` is an abstract base class (`connect`, `disconnect`, `is_connected`, `list_devices`, `get_telemetry(uid)`, `get_device_info(uid)`, `source_type`). Concrete implementations:
|
|
22
|
+
|
|
23
|
+
| Class | `source_type` | Description |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `DirectDataSource` | `direct` | Connects directly to the serial port via `benchlab_pycore`. Runs its own background polling thread. Exclusive port access — only one tool. |
|
|
26
|
+
| `FastAPIDataSource` | `fastapi` / `fastapi_custom` | HTTP client for the `benchlab.restapi` server. Polls `/health`, `/devices`, `/device/{uid}/telemetry`, `/device/{uid}/info` via `requests`. |
|
|
27
|
+
| `MQTTDataSource` | `mqtt` / `mqtt_custom` | Subscribes to `<topic_prefix>/+/telemetry` and `<topic_prefix>/+/info` on an MQTT broker via `paho-mqtt`. Resolves `MQTT_PROTOCOL` (v3.1 / v3.1.1 / v5) via the module-level `resolve_mqtt_protocol()` helper. |
|
|
28
|
+
| `NamedPipeDataSource` | `named_pipe` | Windows-only. Talks to the C# BENCHLAB Windows service over named pipes (`BenchlabDiscovery` for device listing, per-device `BenchlabSensorPipe_XX_YYY` pipes for telemetry). Normalizes the C# service's `ShortName`-keyed sensor payloads into the same flat key names Python tools expect, via the module-level `_normalise_cs_telemetry()` helper. |
|
|
29
|
+
| `ServiceHttpDataSource` | `service_http` | REST client for the C# BENCHLAB service's HTTP API (default `http://localhost:8585`). Thin client only — does not start/manage the service. Same telemetry normalization as `NamedPipeDataSource`. |
|
|
30
|
+
|
|
31
|
+
Each implementation validates its constructor kwargs through a Pydantic model in `config.py` (`SerialConfig`, `FastAPIConfig`, `MQTTConfig`, `NamedPipeConfig`, `ServiceHttpConfig`).
|
|
32
|
+
|
|
33
|
+
The `create_datasource(source_type, **kwargs)` factory function builds the right class:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from benchlab.core import create_datasource
|
|
37
|
+
|
|
38
|
+
datasource = create_datasource('fastapi', base_url='http://127.0.0.1:8000')
|
|
39
|
+
datasource = create_datasource('direct', port='COM3')
|
|
40
|
+
datasource = create_datasource('mqtt', broker='localhost', port=1883)
|
|
41
|
+
|
|
42
|
+
datasource.connect()
|
|
43
|
+
devices = datasource.list_devices()
|
|
44
|
+
telemetry = datasource.get_telemetry(uid)
|
|
45
|
+
device_info = datasource.get_device_info(uid)
|
|
46
|
+
datasource.disconnect()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. DataSourceManager (`datasource_manager.py`)
|
|
50
|
+
|
|
51
|
+
Wraps any `DataSource` with a consistent, higher-level API used by tools (TUI, Link, etc.): connection management, a background polling thread, device selection, and a single `snapshot()` call for UI/consumer code.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from benchlab.core.datasource_manager import DataSourceManager
|
|
55
|
+
|
|
56
|
+
mgr = DataSourceManager(source_type='fastapi', base_url='http://127.0.0.1:8000')
|
|
57
|
+
mgr.connect()
|
|
58
|
+
mgr.select_device(uid)
|
|
59
|
+
snap = mgr.snapshot()
|
|
60
|
+
# snap: connected, source_type, source_desc, port, uid, device_info,
|
|
61
|
+
# sensor_data, connection_time, last_error, all_devices, all_telemetry
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Also accepts an optional `stats_callback(uid, channel, value)` invoked on every changed numeric telemetry value, typically wired to a `ChannelStats` instance (see below).
|
|
65
|
+
|
|
66
|
+
### 3. DeviceRegistry (`device_registry.py`)
|
|
67
|
+
|
|
68
|
+
Thread-safe singleton (`DeviceRegistry.get_instance()`) that is the single source of truth for "which devices exist right now." Exactly one component per process should own registration — e.g. the FastAPI server registers/unregisters devices as its reader threads start and stop. Other tools only observe via `get_devices()`, `get_device(uid)`, `has_device(uid)`, or subscribe to `on_connect(callback)` / `on_disconnect(callback)` events. `update_telemetry(uid)` stamps a device's `last_telemetry` time.
|
|
69
|
+
|
|
70
|
+
### 4. ProcessManager (`process_manager.py`)
|
|
71
|
+
|
|
72
|
+
Thread-safe singleton (`ProcessManager.get_instance()`) for starting/stopping infrastructure subprocesses (e.g. a spawned FastAPI server or MQTT publisher process) with health checks:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
pm = ProcessManager.get_instance()
|
|
76
|
+
pm.start_service(
|
|
77
|
+
name="fastapi",
|
|
78
|
+
cmd=["python", "-m", "benchlab", "-fastapi"],
|
|
79
|
+
health_check=lambda: ping_http("http://127.0.0.1:8000/health"),
|
|
80
|
+
timeout=20,
|
|
81
|
+
)
|
|
82
|
+
pm.stop_service("fastapi")
|
|
83
|
+
pm.shutdown_all()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Redirects each service's stdout/stderr to `logs/svc_<name>_stdout.log` / `_stderr.log`. Stopping tries graceful termination (SIGTERM / `taskkill`) first, then force-kills after a timeout.
|
|
87
|
+
|
|
88
|
+
### 5. InfrastructureManager (`infrastructure.py`)
|
|
89
|
+
|
|
90
|
+
Higher-level, non-singleton orchestrator used by the multi-tool launcher path: starts a FastAPI server as a `uvicorn` subprocess (`start_fastapi`) or an MQTT publisher as a background thread (`start_mqtt_publisher`), detecting if a compatible service is already listening on the target port before starting a new one. `start_all(tools)` / `stop_all()` take a list of tool configs and start only what's needed. Usable as a context manager.
|
|
91
|
+
|
|
92
|
+
### 6. Discovery (`discovery.py`)
|
|
93
|
+
|
|
94
|
+
`discover_devices()` — the canonical way to enumerate connected BENCHLAB devices over direct serial. Uses `benchlab_pycore.core.get_benchlab_ports()` to find candidate ports, opens each, reads UID/firmware/product ID, and returns `{"uid", "port", "fw", "variant"}` dicts (`variant` is `"ORIGINAL"` or `"BL2"`). Wrapped in the `retry` decorator (3 attempts, exponential backoff).
|
|
95
|
+
|
|
96
|
+
### 7. Retry (`retry.py`)
|
|
97
|
+
|
|
98
|
+
`RetryPolicy` (dataclass: `max_retries`, `backoff_factor`, `base_delay`, `allowed_exceptions`) plus a `retry(policy)` decorator that retries a wrapped callable with exponential backoff, logging each attempt. Used throughout `datasource.py` and `discovery.py` for connection attempts.
|
|
99
|
+
|
|
100
|
+
### 8. Shared serial (`shared_serial.py`)
|
|
101
|
+
|
|
102
|
+
`open_serial_connection(port)` — the one place that actually calls `serial.Serial(port, 115200, timeout=1)`; returns `None` instead of raising on failure. All direct-serial code paths (`DirectDataSource`, `discovery.py`, `telemetry_api.py`) use this instead of talking to `pyserial` directly, so the "None on failure" contract is consistent everywhere.
|
|
103
|
+
|
|
104
|
+
Also provides `SharedSerialManager`, a singleton connection pool keyed by port with reference counting (`acquire_connection` / `release_connection`), so multiple in-process consumers can share one open port instead of each opening their own handle. `SharedConnection` is the context-manager wrapper returned by `acquire_connection`.
|
|
105
|
+
|
|
106
|
+
### 9. Statistics (`statistics.py`)
|
|
107
|
+
|
|
108
|
+
`ChannelStats` — thread-safe per-device, per-channel running min/max/average tracker (`update`, `get`, `get_all`, `reset`, `get_devices`, `get_channels`, `has_data`). `StatsFormatter` provides display helpers (`format_stat_string`, `format_compact_range`). `create_stats_callback(stats)` builds a `(uid, channel, value) -> None` callback suitable for `DataSourceManager(stats_callback=...)`.
|
|
109
|
+
|
|
110
|
+
### 10. Config models (`config.py`)
|
|
111
|
+
|
|
112
|
+
Pydantic models validating each DataSource's constructor kwargs: `SerialConfig`, `FastAPIConfig` (normalizes `base_url` to include a scheme and strips trailing slash), `MQTTConfig`, `NamedPipeConfig`, `ServiceHttpConfig`.
|
|
113
|
+
|
|
114
|
+
## Package exports (`benchlab/core/__init__.py`)
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from benchlab.core import (
|
|
118
|
+
DataSource, DirectDataSource, FastAPIDataSource, MQTTDataSource, create_datasource,
|
|
119
|
+
DataSourceManager,
|
|
120
|
+
ChannelStats, StatsFormatter, create_stats_callback,
|
|
121
|
+
DeviceRegistry, DeviceInfo,
|
|
122
|
+
ProcessManager, ManagedProcess,
|
|
123
|
+
BENCHLAB_ORIGINAL_PRODUCT_ID, BENCHLAB_BL2_PRODUCT_ID,
|
|
124
|
+
)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`NamedPipeDataSource` and `ServiceHttpDataSource` live in `datasource.py` but aren't re-exported from `__init__.py`; import them directly from `benchlab.core.datasource` if needed. `BENCHLAB_ORIGINAL_PRODUCT_ID` / `BENCHLAB_BL2_PRODUCT_ID` are re-exported from `benchlab_pycore` (with a fallback to hardcoded `0x10`/`0x11` if pycore isn't installed) so tools can detect device variant without importing pycore directly.
|
|
128
|
+
|
|
129
|
+
## How tools use this
|
|
130
|
+
|
|
131
|
+
- **`benchlab/main.py`** resolves `--source` into environment variables via `_setup_source_from_args()` and calls `check_and_setup_source()` (in `benchlab/sources.py`) to start/verify the chosen source before launching a tool.
|
|
132
|
+
- **`benchlab/restapi/telemetry_api.py`** is a *producer*: it owns the serial port directly, registers devices in `DeviceRegistry`, and serves `DirectDataSource`-equivalent data to `FastAPIDataSource` clients.
|
|
133
|
+
- **`benchlab/mqtt/mqtt_publisher.py`** is also a producer: reads sensors directly and publishes to an MQTT broker for `MQTTDataSource` clients.
|
|
134
|
+
- **`benchlab/link/link_main.py`** is a consumer: uses `DataSourceManager` against any local source (direct/fastapi/mqtt) and republishes telemetry to a remote/cloud MQTT broker.
|
|
135
|
+
- **`benchlab/tui/`**, **VU dials**, **WigiDash**, **HWiNFO export**, **CSV logger** are consumers via `DataSourceManager` or `create_datasource` directly.
|
|
136
|
+
- The **config tool** (`benchlab/config/`) does not use the DataSource abstraction — it has its own direct/named-pipe client layer (`config_client.py`) for read/write configuration operations, since DataSource is read-only telemetry.
|
|
137
|
+
|
|
138
|
+
## Data Source Comparison
|
|
139
|
+
|
|
140
|
+
| Source | Use case | Pros | Cons |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| `direct` | Single tool, lowest latency | No overhead, no extra process | Exclusive port access, only one tool |
|
|
143
|
+
| `fastapi` / `fastapi_custom` | Multiple tools, remote access | HTTP-based, firewall-friendly, multi-client | Small latency overhead, requires a running server |
|
|
144
|
+
| `mqtt` / `mqtt_custom` | Distributed/IoT integration | Pub/sub, works with existing MQTT infra | Requires a broker, more moving parts |
|
|
145
|
+
| `named_pipe` | Windows, alongside the C# BENCHLAB service | Multiple tools, no direct serial management | Windows only, requires the service + pywin32 |
|
|
146
|
+
| `service_http` | Windows, alongside the C# BENCHLAB service | HTTP-based, multi-client | Windows-oriented, requires the service running |
|
|
147
|
+
|
|
148
|
+
## See Also
|
|
149
|
+
|
|
150
|
+
- [BENCHLAB PyTools Documentation](../../README.md) - Main PyTools documentation
|
|
151
|
+
- [FastAPI Telemetry Server README](../restapi/readme.md)
|
|
152
|
+
- [MQTT Publisher README](../mqtt/README.md)
|
|
153
|
+
- [Config Tool README](../config/README.md)
|
|
154
|
+
- [Link README](../link/README.md)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BenchLab Core - Infrastructure layer
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- DataSource abstraction for telemetry consumption (direct / FastAPI / MQTT)
|
|
6
|
+
- DataSourceManager for unified datasource management with statistics
|
|
7
|
+
- DeviceRegistry singleton for device lifecycle tracking
|
|
8
|
+
- ProcessManager singleton for infrastructure service management
|
|
9
|
+
- ChannelStats for thread-safe telemetry statistics tracking
|
|
10
|
+
- InfrastructureManager for higher-level orchestration
|
|
11
|
+
|
|
12
|
+
PyCore v0.3.0 Multi-Variant Support:
|
|
13
|
+
- ORIGINAL (0x10): 11 power sensors, 4 temperature sensors
|
|
14
|
+
- BL2 / BENCHLAB 2 (0x11): 23 power sensors, 8 temperature sensors
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
from benchlab.core.datasource import (
|
|
20
|
+
DataSource,
|
|
21
|
+
DirectDataSource,
|
|
22
|
+
FastAPIDataSource,
|
|
23
|
+
MQTTDataSource,
|
|
24
|
+
create_datasource,
|
|
25
|
+
)
|
|
26
|
+
from benchlab.core.datasource_manager import DataSourceManager
|
|
27
|
+
from benchlab.core.device_registry import DeviceRegistry, DeviceInfo
|
|
28
|
+
from benchlab.core.process_manager import ProcessManager, ManagedProcess
|
|
29
|
+
from benchlab.core.statistics import (
|
|
30
|
+
ChannelStats,
|
|
31
|
+
StatsFormatter,
|
|
32
|
+
create_stats_callback,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("benchlab.core")
|
|
36
|
+
|
|
37
|
+
# PyCore variant constants for tools that need to detect device capabilities
|
|
38
|
+
# Import from pycore to make them easily accessible to tools.
|
|
39
|
+
# PyCore >=0.4.1 renamed BENCHLAB_CFE_PRODUCT_ID to BENCHLAB_BL2_PRODUCT_ID
|
|
40
|
+
# (the "CFE" SKU is now marketed as BENCHLAB 2); fall back to the old name
|
|
41
|
+
# for older pycore installs.
|
|
42
|
+
try:
|
|
43
|
+
from benchlab_pycore.core import BENCHLAB_ORIGINAL_PRODUCT_ID
|
|
44
|
+
try:
|
|
45
|
+
from benchlab_pycore.core import BENCHLAB_BL2_PRODUCT_ID
|
|
46
|
+
except ImportError:
|
|
47
|
+
from benchlab_pycore.core import (
|
|
48
|
+
BENCHLAB_CFE_PRODUCT_ID as BENCHLAB_BL2_PRODUCT_ID)
|
|
49
|
+
except ImportError:
|
|
50
|
+
logger.warning(
|
|
51
|
+
"benchlab_pycore not installed; using fallback product ID constants")
|
|
52
|
+
BENCHLAB_ORIGINAL_PRODUCT_ID = 0x10
|
|
53
|
+
BENCHLAB_BL2_PRODUCT_ID = 0x11
|
|
54
|
+
|
|
55
|
+
__version__ = "2.0.0"
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
# DataSource layer
|
|
59
|
+
"DataSource",
|
|
60
|
+
"DirectDataSource",
|
|
61
|
+
"FastAPIDataSource",
|
|
62
|
+
"MQTTDataSource",
|
|
63
|
+
"create_datasource",
|
|
64
|
+
# Unified datasource management
|
|
65
|
+
"DataSourceManager",
|
|
66
|
+
# Statistics tracking
|
|
67
|
+
"ChannelStats",
|
|
68
|
+
"StatsFormatter",
|
|
69
|
+
"create_stats_callback",
|
|
70
|
+
# Device and process management
|
|
71
|
+
"DeviceRegistry",
|
|
72
|
+
"DeviceInfo",
|
|
73
|
+
"ProcessManager",
|
|
74
|
+
"ManagedProcess",
|
|
75
|
+
# PyCore v0.3.0 variant constants
|
|
76
|
+
"BENCHLAB_ORIGINAL_PRODUCT_ID",
|
|
77
|
+
"BENCHLAB_BL2_PRODUCT_ID",
|
|
78
|
+
]
|
benchlab/core/config.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Configuration models for Benchlab data sources.
|
|
2
|
+
|
|
3
|
+
This module defines Pydantic ``BaseModel`` classes that validate the
|
|
4
|
+
configuration parameters required by each concrete ``DataSource``
|
|
5
|
+
implementation. Using a strict schema helps catch mis-configurations
|
|
6
|
+
early (e.g. missing serial port, invalid MQTT broker address) and makes
|
|
7
|
+
the library easier to use from external tools.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SerialConfig(BaseModel):
|
|
16
|
+
"""Configuration for :class:`DirectDataSource`.
|
|
17
|
+
|
|
18
|
+
Attributes
|
|
19
|
+
----------
|
|
20
|
+
port: Optional[str]
|
|
21
|
+
The serial port to connect to. ``None`` triggers auto-detection.
|
|
22
|
+
poll_interval: float
|
|
23
|
+
Seconds between successive sensor reads. Must be positive.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
port: Optional[str] = None
|
|
27
|
+
poll_interval: float = Field(
|
|
28
|
+
default=1.0,
|
|
29
|
+
gt=0,
|
|
30
|
+
description="Polling interval in seconds")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FastAPIConfig(BaseModel):
|
|
34
|
+
"""Configuration for :class:`FastAPIDataSource`.
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
base_url: str
|
|
39
|
+
Base URL of the FastAPI server (e.g. ``http://127.0.0.1:8000``).
|
|
40
|
+
timeout: float
|
|
41
|
+
HTTP request timeout in seconds. Must be positive.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
base_url: str = Field(..., description="Base URL of the FastAPI server")
|
|
45
|
+
timeout: float = Field(
|
|
46
|
+
default=5.0,
|
|
47
|
+
gt=0,
|
|
48
|
+
description="Request timeout in seconds")
|
|
49
|
+
|
|
50
|
+
@field_validator("base_url")
|
|
51
|
+
@classmethod
|
|
52
|
+
def _validate_base_url(cls, v: str) -> str:
|
|
53
|
+
"""Ensure base_url has a protocol prefix and no trailing slash."""
|
|
54
|
+
v = v.strip()
|
|
55
|
+
if not v.startswith(('http://', 'https://')):
|
|
56
|
+
v = f"http://{v}"
|
|
57
|
+
return v.rstrip('/')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MQTTConfig(BaseModel):
|
|
61
|
+
"""Configuration for :class:`MQTTDataSource`.
|
|
62
|
+
|
|
63
|
+
Attributes
|
|
64
|
+
----------
|
|
65
|
+
broker: str
|
|
66
|
+
Hostname of the MQTT broker.
|
|
67
|
+
port: int
|
|
68
|
+
Network port of the broker (default 1883).
|
|
69
|
+
topic_prefix: str
|
|
70
|
+
Prefix used for all Benchlab topics.
|
|
71
|
+
timeout: float
|
|
72
|
+
Connection timeout in seconds.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
broker: str = Field(
|
|
76
|
+
default="localhost",
|
|
77
|
+
description="MQTT broker hostname")
|
|
78
|
+
port: int = Field(default=1883, ge=1, le=65535, description="Broker port")
|
|
79
|
+
topic_prefix: str = Field(
|
|
80
|
+
default="benchlab",
|
|
81
|
+
description="Base topic prefix")
|
|
82
|
+
timeout: float = Field(
|
|
83
|
+
default=5.0,
|
|
84
|
+
gt=0,
|
|
85
|
+
description="Connection timeout in seconds")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class NamedPipeConfig(BaseModel):
|
|
89
|
+
"""Configuration for :class:`NamedPipeDataSource`.
|
|
90
|
+
|
|
91
|
+
Windows-only. Connects to the C# BenchLab Windows service via
|
|
92
|
+
named pipes (BenchlabDiscovery + per-device BenchlabSensorPipe_XX_YYY).
|
|
93
|
+
|
|
94
|
+
Attributes
|
|
95
|
+
----------
|
|
96
|
+
timeout: float
|
|
97
|
+
Seconds to wait for a pipe connection before giving up.
|
|
98
|
+
poll_interval: float
|
|
99
|
+
Seconds between successive sensor reads.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
timeout: float = Field(
|
|
103
|
+
default=5.0,
|
|
104
|
+
gt=0,
|
|
105
|
+
description="Pipe connection timeout in seconds")
|
|
106
|
+
poll_interval: float = Field(
|
|
107
|
+
default=1.0,
|
|
108
|
+
gt=0,
|
|
109
|
+
description="Polling interval in seconds")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ServiceHttpConfig(BaseModel):
|
|
113
|
+
"""Configuration for :class:`ServiceHttpDataSource`.
|
|
114
|
+
|
|
115
|
+
Connects to the C# BenchLab service REST HTTP API.
|
|
116
|
+
The service is auto-detected at port 8585 by default.
|
|
117
|
+
|
|
118
|
+
Attributes
|
|
119
|
+
----------
|
|
120
|
+
base_url: str
|
|
121
|
+
Base URL of the C# BenchLab service
|
|
122
|
+
(e.g. ``http://localhost:8585``).
|
|
123
|
+
timeout: float
|
|
124
|
+
HTTP request timeout in seconds. Must be positive.
|
|
125
|
+
poll_interval: float
|
|
126
|
+
Seconds between successive telemetry polls.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
base_url: str = Field(
|
|
130
|
+
default="http://localhost:8585",
|
|
131
|
+
description="Base URL of the C# BenchLab service HTTP API",
|
|
132
|
+
)
|
|
133
|
+
timeout: float = Field(
|
|
134
|
+
default=5.0,
|
|
135
|
+
gt=0,
|
|
136
|
+
description="Request timeout in seconds")
|
|
137
|
+
poll_interval: float = Field(
|
|
138
|
+
default=1.0,
|
|
139
|
+
gt=0,
|
|
140
|
+
description="Polling interval in seconds")
|
|
141
|
+
|
|
142
|
+
@field_validator("base_url")
|
|
143
|
+
@classmethod
|
|
144
|
+
def _strip_trailing_slash(cls, v: str) -> str:
|
|
145
|
+
return v.rstrip('/')
|