benchlab-pycore 0.1.0__tar.gz
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_pycore-0.1.0/LICENSE +9 -0
- benchlab_pycore-0.1.0/PKG-INFO +62 -0
- benchlab_pycore-0.1.0/README.md +42 -0
- benchlab_pycore-0.1.0/benchlab/__init__.py +12 -0
- benchlab_pycore-0.1.0/benchlab/core/__init__.py +53 -0
- benchlab_pycore-0.1.0/benchlab/core/sensor_translation.py +68 -0
- benchlab_pycore-0.1.0/benchlab/core/serial_io.py +170 -0
- benchlab_pycore-0.1.0/benchlab/core/structures.py +79 -0
- benchlab_pycore-0.1.0/benchlab/core/utils.py +10 -0
- benchlab_pycore-0.1.0/benchlab/version.py +0 -0
- benchlab_pycore-0.1.0/benchlab_pycore.egg-info/PKG-INFO +62 -0
- benchlab_pycore-0.1.0/benchlab_pycore.egg-info/SOURCES.txt +18 -0
- benchlab_pycore-0.1.0/benchlab_pycore.egg-info/dependency_links.txt +1 -0
- benchlab_pycore-0.1.0/benchlab_pycore.egg-info/requires.txt +1 -0
- benchlab_pycore-0.1.0/benchlab_pycore.egg-info/top_level.txt +2 -0
- benchlab_pycore-0.1.0/pyproject.toml +28 -0
- benchlab_pycore-0.1.0/setup.cfg +36 -0
- benchlab_pycore-0.1.0/tests/__init__.py +2 -0
- benchlab_pycore-0.1.0/tests/test_core.py +116 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BENCHLAB
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: benchlab-pycore
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: BENCHLAB PyCore - Python Interface for BENCHLAB
|
|
5
|
+
Home-page: https://github.com/BenchLab-io/benchlab-pycore
|
|
6
|
+
Author: Pieter Plaisier
|
|
7
|
+
Author-email: Pieter Plaisier <contact@benchlab.io>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/BenchLab-io/benchlab-pycore
|
|
10
|
+
Project-URL: Issues, https://github.com/BenchLab-io/benchlab-pycore/issues
|
|
11
|
+
Keywords: telemetry,hardware,benchlab,sensors
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: pyserial>=3.5
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# BENCHLAB Python Core (`benchlab-pycore`)
|
|
22
|
+
|
|
23
|
+
**BENCHLAB PyCore** provides low-level telemetry, sensor IO, and device communication utilities
|
|
24
|
+
for BENCHLAB. It includes standardized interfaces for reading sensors, handling serial communication, and publishing telemetry data.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Read and translate sensor data from BENCHLAB devices
|
|
29
|
+
- Communicate via serial ports
|
|
30
|
+
- Designed to be easily integrated into larger telemetry pipelines
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
pip install benchlab-pycore
|
|
35
|
+
|
|
36
|
+
## Usage Example
|
|
37
|
+
|
|
38
|
+
import benchlab
|
|
39
|
+
from benchlab.core import get_fleet_info, read_sensors
|
|
40
|
+
|
|
41
|
+
info = get_fleet_info()
|
|
42
|
+
print(info)
|
|
43
|
+
|
|
44
|
+
## Project Layout
|
|
45
|
+
|
|
46
|
+
benchlab/
|
|
47
|
+
├── core/
|
|
48
|
+
│ ├── serial_io.py
|
|
49
|
+
│ ├── sensor_translation.py
|
|
50
|
+
│ └── ...
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
Clone and install locally:
|
|
55
|
+
|
|
56
|
+
git clone https://github.com/BenchLab-io/benchlab-pycore.git
|
|
57
|
+
cd benchlab-pycore
|
|
58
|
+
pip install -e .
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT License © 2025 BenchLab Contributors
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# BENCHLAB Python Core (`benchlab-pycore`)
|
|
2
|
+
|
|
3
|
+
**BENCHLAB PyCore** provides low-level telemetry, sensor IO, and device communication utilities
|
|
4
|
+
for BENCHLAB. It includes standardized interfaces for reading sensors, handling serial communication, and publishing telemetry data.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Read and translate sensor data from BENCHLAB devices
|
|
9
|
+
- Communicate via serial ports
|
|
10
|
+
- Designed to be easily integrated into larger telemetry pipelines
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
pip install benchlab-pycore
|
|
15
|
+
|
|
16
|
+
## Usage Example
|
|
17
|
+
|
|
18
|
+
import benchlab
|
|
19
|
+
from benchlab.core import get_fleet_info, read_sensors
|
|
20
|
+
|
|
21
|
+
info = get_fleet_info()
|
|
22
|
+
print(info)
|
|
23
|
+
|
|
24
|
+
## Project Layout
|
|
25
|
+
|
|
26
|
+
benchlab/
|
|
27
|
+
├── core/
|
|
28
|
+
│ ├── serial_io.py
|
|
29
|
+
│ ├── sensor_translation.py
|
|
30
|
+
│ └── ...
|
|
31
|
+
|
|
32
|
+
## Development
|
|
33
|
+
|
|
34
|
+
Clone and install locally:
|
|
35
|
+
|
|
36
|
+
git clone https://github.com/BenchLab-io/benchlab-pycore.git
|
|
37
|
+
cd benchlab-pycore
|
|
38
|
+
pip install -e .
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT License © 2025 BenchLab Contributors
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# benchlab/__init__.py
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("benchlab-pycore")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "0.0.0-dev"
|
|
9
|
+
|
|
10
|
+
from . import core
|
|
11
|
+
|
|
12
|
+
__all__ = ["core", "__version__"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# benchlab/core/__init__.py
|
|
2
|
+
|
|
3
|
+
from .serial_io import (
|
|
4
|
+
get_benchlab_ports,
|
|
5
|
+
find_benchlab_devices,
|
|
6
|
+
get_fleet_info,
|
|
7
|
+
read_uart,
|
|
8
|
+
read_device,
|
|
9
|
+
read_sensors,
|
|
10
|
+
read_uid,
|
|
11
|
+
)
|
|
12
|
+
from .sensor_translation import translate_sensor_struct
|
|
13
|
+
from .structures import (
|
|
14
|
+
VendorDataStruct,
|
|
15
|
+
PowerSensor,
|
|
16
|
+
FanSensor,
|
|
17
|
+
SensorStruct,
|
|
18
|
+
BENCHLAB_CMD,
|
|
19
|
+
BENCHLAB_VENDOR_ID,
|
|
20
|
+
BENCHLAB_PRODUCT_ID,
|
|
21
|
+
BENCHLAB_FIRMWARE_VERSION,
|
|
22
|
+
SENSOR_VIN_NUM,
|
|
23
|
+
SENSOR_POWER_NUM,
|
|
24
|
+
FAN_NUM,
|
|
25
|
+
)
|
|
26
|
+
from .utils import format_temp
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# serial_io"
|
|
30
|
+
"get_benchlab_ports",
|
|
31
|
+
"find_benchlab_devices",
|
|
32
|
+
"get_fleet_info",
|
|
33
|
+
"read_uart",
|
|
34
|
+
"read_device",
|
|
35
|
+
"read_sensors",
|
|
36
|
+
"read_uid",
|
|
37
|
+
# sensor_translation
|
|
38
|
+
"translate_sensor_struct",
|
|
39
|
+
# structures
|
|
40
|
+
"VendorDataStruct",
|
|
41
|
+
"PowerSensor",
|
|
42
|
+
"FanSensor",
|
|
43
|
+
"SensorStruct",
|
|
44
|
+
"BENCHLAB_CMD",
|
|
45
|
+
"BENCHLAB_VENDOR_ID",
|
|
46
|
+
"BENCHLAB_PRODUCT_ID",
|
|
47
|
+
"BENCHLAB_FIRMWARE_VERSION",
|
|
48
|
+
"SENSOR_VIN_NUM",
|
|
49
|
+
"SENSOR_POWER_NUM",
|
|
50
|
+
"FAN_NUM",
|
|
51
|
+
# utils
|
|
52
|
+
"format_temp",
|
|
53
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# benchlab/core/sensor_translation.py
|
|
2
|
+
|
|
3
|
+
from .structures import SENSOR_VIN_NUM
|
|
4
|
+
from .utils import format_temp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def translate_sensor_struct(sensor_struct):
|
|
8
|
+
"""Return a flat dict of interpreted sensor values
|
|
9
|
+
suitable for CSV, graphs, MQTT, etc."""
|
|
10
|
+
data = {}
|
|
11
|
+
|
|
12
|
+
# Power
|
|
13
|
+
power = sensor_struct.PowerReadings
|
|
14
|
+
power_cpu = (power[0].Power + power[1].Power) / 1000
|
|
15
|
+
power_gpu = sum([power[i].Power for i in range(6, 11)]) / 1000
|
|
16
|
+
power_mb = sum([power[i].Power for i in range(2, 6)]) / 1000
|
|
17
|
+
power_system = power_cpu + power_gpu + power_mb
|
|
18
|
+
data.update({
|
|
19
|
+
"SYS_Power": power_system,
|
|
20
|
+
"CPU_Power": power_cpu,
|
|
21
|
+
"GPU_Power": power_gpu,
|
|
22
|
+
"MB_Power": power_mb,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
# EPS, ATX, PCIe
|
|
26
|
+
eps_labels = ["EPS1", "EPS2"]
|
|
27
|
+
atx_labels = ["12V", "5V", "5VSB", "3.3V"]
|
|
28
|
+
pcie_labels = ["PCIE8_1", "PCIE8_2", "PCIE8_3", "HPWR1", "HPWR2"]
|
|
29
|
+
|
|
30
|
+
for i, label in enumerate(eps_labels):
|
|
31
|
+
data[f"{label}_Voltage"] = power[i].Voltage / 1000
|
|
32
|
+
data[f"{label}_Current"] = power[i].Current / 1000
|
|
33
|
+
data[f"{label}_Power"] = power[i].Power / 1000
|
|
34
|
+
|
|
35
|
+
for i, label in enumerate(atx_labels):
|
|
36
|
+
idx = [5, 3, 4, 2][i]
|
|
37
|
+
data[f"{label}_Voltage"] = power[idx].Voltage / 1000
|
|
38
|
+
data[f"{label}_Current"] = power[idx].Current / 1000
|
|
39
|
+
data[f"{label}_Power"] = power[idx].Power / 1000
|
|
40
|
+
|
|
41
|
+
for i, label in enumerate(pcie_labels):
|
|
42
|
+
idx = 6 + i
|
|
43
|
+
data[f"{label}_Voltage"] = power[idx].Voltage / 1000
|
|
44
|
+
data[f"{label}_Current"] = power[idx].Current / 1000
|
|
45
|
+
data[f"{label}_Power"] = power[idx].Power / 1000
|
|
46
|
+
|
|
47
|
+
# Voltage
|
|
48
|
+
vin_names = [f"VIN_{i}" for i in range(SENSOR_VIN_NUM)]
|
|
49
|
+
for name, value in zip(vin_names, sensor_struct.Vin):
|
|
50
|
+
data[name] = value / 1000
|
|
51
|
+
data["Vdd"] = sensor_struct.Vdd / 1000
|
|
52
|
+
data["Vref"] = sensor_struct.Vref / 1000
|
|
53
|
+
|
|
54
|
+
# Temperature
|
|
55
|
+
data["Chip_Temp"] = format_temp(sensor_struct.Tchip)
|
|
56
|
+
data["Ambient_Temp"] = format_temp(sensor_struct.Tamb)
|
|
57
|
+
data["Humidity"] = sensor_struct.Hum / 10
|
|
58
|
+
for i, t in enumerate(sensor_struct.Ts):
|
|
59
|
+
data[f"Temp_Sensor_{i+1}"] = format_temp(t)
|
|
60
|
+
|
|
61
|
+
# Fans
|
|
62
|
+
for i, f in enumerate(sensor_struct.Fans):
|
|
63
|
+
data[f"Fan{i+1}_Duty"] = f.Duty
|
|
64
|
+
data[f"Fan{i+1}_RPM"] = f.Tach
|
|
65
|
+
data[f"Fan{i+1}_Status"] = f.Enable
|
|
66
|
+
data["FanExtDuty"] = sensor_struct.FanExtDuty
|
|
67
|
+
|
|
68
|
+
return data
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serial communication helpers for BENCHLAB devices
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import logging
|
|
9
|
+
import serial
|
|
10
|
+
import serial.tools.list_ports
|
|
11
|
+
from ctypes import sizeof
|
|
12
|
+
from benchlab.core.structures import (
|
|
13
|
+
VendorDataStruct,
|
|
14
|
+
SensorStruct,
|
|
15
|
+
BENCHLAB_CMD,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# --- Logger setup ---
|
|
19
|
+
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
20
|
+
logger = logging.getLogger("mqtt_bridge.serial_io")
|
|
21
|
+
logger.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
|
|
22
|
+
|
|
23
|
+
if not logger.handlers:
|
|
24
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
25
|
+
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
|
|
26
|
+
handler.setFormatter(formatter)
|
|
27
|
+
logger.addHandler(handler)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --- Serial port helpers ---
|
|
31
|
+
def get_benchlab_ports():
|
|
32
|
+
"""Return all Benchlab COM ports without opening them."""
|
|
33
|
+
ports = serial.tools.list_ports.comports()
|
|
34
|
+
benchlab_ports = []
|
|
35
|
+
for p in ports:
|
|
36
|
+
hwid = p.hwid.upper()
|
|
37
|
+
if "VID:PID=0483:5740" in hwid:
|
|
38
|
+
benchlab_ports.append({"port": p.device})
|
|
39
|
+
return benchlab_ports
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_benchlab_devices():
|
|
43
|
+
"""Scan and return all connected BENCHLAB devices."""
|
|
44
|
+
devices = []
|
|
45
|
+
for port, desc, hwid in serial.tools.list_ports.comports():
|
|
46
|
+
if hwid.startswith("USB VID:PID=0483:5740"):
|
|
47
|
+
try:
|
|
48
|
+
ser = serial.Serial(port, baudrate=115200, timeout=1)
|
|
49
|
+
ser.write(b"i\n")
|
|
50
|
+
uid = ser.readline().decode(errors="ignore").strip()
|
|
51
|
+
fw = ser.readline().decode(errors="ignore").strip()
|
|
52
|
+
ser.close()
|
|
53
|
+
logger.info("Found device on %s: UID=%s, FW=%s", port, uid, fw)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
uid, fw = "?", "?"
|
|
56
|
+
logger.warning("Failed to read device on %s: %s", port, e)
|
|
57
|
+
devices.append({"port": port, "uid": uid, "fw": fw})
|
|
58
|
+
return devices
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_fleet_info():
|
|
62
|
+
"""Return a list of all BENCHLAB devices with UID and firmware info."""
|
|
63
|
+
fleet = []
|
|
64
|
+
benchlab_ports = [p["port"] for p in get_benchlab_ports()]
|
|
65
|
+
|
|
66
|
+
for port in benchlab_ports:
|
|
67
|
+
try:
|
|
68
|
+
ser = serial.Serial(port, 115200, timeout=1)
|
|
69
|
+
device_info = read_device(ser)
|
|
70
|
+
uid = read_uid(ser)
|
|
71
|
+
fleet.append(
|
|
72
|
+
{
|
|
73
|
+
"port": port,
|
|
74
|
+
"firmware": (
|
|
75
|
+
device_info.get("FwVersion") if device_info else "?"
|
|
76
|
+
),
|
|
77
|
+
"uid": uid,
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
ser.close()
|
|
81
|
+
logger.debug("Added device to fleet: %s", uid)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.warning("Failed to get device info from %s: %s", port, e)
|
|
84
|
+
return fleet
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def open_serial_connection(port=None):
|
|
88
|
+
"""Open and return a serial connection to the given port."""
|
|
89
|
+
if not port:
|
|
90
|
+
return None
|
|
91
|
+
try:
|
|
92
|
+
ser = serial.Serial(port, baudrate=115200, timeout=1)
|
|
93
|
+
logger.info("Opened serial port %s", port)
|
|
94
|
+
return ser
|
|
95
|
+
except serial.SerialException as e:
|
|
96
|
+
logger.error("Could not open port %s: %s", port, e)
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- UART helpers ---
|
|
101
|
+
def read_uart(ser, cmd, size):
|
|
102
|
+
"""Send a command and read bytes from the device."""
|
|
103
|
+
ser.write(cmd.toByte())
|
|
104
|
+
buffer = ser.read(size)
|
|
105
|
+
if len(buffer) != size:
|
|
106
|
+
logger.warning(
|
|
107
|
+
"UART read incomplete: expected %d, got %d bytes",
|
|
108
|
+
size,
|
|
109
|
+
len(buffer)
|
|
110
|
+
)
|
|
111
|
+
return None
|
|
112
|
+
return buffer
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def read_device(ser):
|
|
116
|
+
"""Read vendor info from the device."""
|
|
117
|
+
try:
|
|
118
|
+
buffer = read_uart(
|
|
119
|
+
ser,
|
|
120
|
+
BENCHLAB_CMD.UART_CMD_READ_VENDOR_DATA,
|
|
121
|
+
sizeof(VendorDataStruct)
|
|
122
|
+
)
|
|
123
|
+
if buffer is None:
|
|
124
|
+
logger.warning("Failed to read vendor data from device")
|
|
125
|
+
return None
|
|
126
|
+
vendor_struct = VendorDataStruct.from_buffer_copy(buffer)
|
|
127
|
+
return {
|
|
128
|
+
"VendorId": vendor_struct.VendorId,
|
|
129
|
+
"ProductId": vendor_struct.ProductId,
|
|
130
|
+
"FwVersion": vendor_struct.FwVersion,
|
|
131
|
+
}
|
|
132
|
+
except (serial.SerialException, OSError, ValueError) as e:
|
|
133
|
+
logger.warning("Failed to read device: %s", e)
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def read_sensors(ser):
|
|
138
|
+
"""Read all sensors from the device."""
|
|
139
|
+
buffer = read_uart(
|
|
140
|
+
ser,
|
|
141
|
+
BENCHLAB_CMD.UART_CMD_READ_SENSORS,
|
|
142
|
+
sizeof(SensorStruct)
|
|
143
|
+
)
|
|
144
|
+
if buffer is None:
|
|
145
|
+
logger.warning("Failed to read sensors from device")
|
|
146
|
+
return None
|
|
147
|
+
return SensorStruct.from_buffer_copy(buffer)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def read_uid(ser, retries=3, delay=0.2):
|
|
151
|
+
"""Read UID from device with optional retries."""
|
|
152
|
+
for attempt in range(1, retries + 1):
|
|
153
|
+
buffer = read_uart(
|
|
154
|
+
ser,
|
|
155
|
+
BENCHLAB_CMD.UART_CMD_READ_UID,
|
|
156
|
+
12
|
|
157
|
+
)
|
|
158
|
+
if buffer:
|
|
159
|
+
uid_hex = buffer.hex().upper()
|
|
160
|
+
logger.debug("Raw UID bytes: %s", buffer)
|
|
161
|
+
logger.debug("UID read: %s", uid_hex)
|
|
162
|
+
return uid_hex
|
|
163
|
+
else:
|
|
164
|
+
logger.warning(
|
|
165
|
+
"Attempt %d: Failed to read UID from device",
|
|
166
|
+
attempt
|
|
167
|
+
)
|
|
168
|
+
time.sleep(delay)
|
|
169
|
+
logger.error("Failed to read UID after %d attempts", retries)
|
|
170
|
+
return None
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core structures, enums, and constants for BENCHLAB
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from ctypes import Structure, c_uint8, c_uint16, c_int16, c_int32
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
|
|
8
|
+
# --- BENCHLAB Constants ---
|
|
9
|
+
BENCHLAB_VENDOR_ID = 0xEE
|
|
10
|
+
BENCHLAB_PRODUCT_ID = 0x10
|
|
11
|
+
BENCHLAB_FIRMWARE_VERSION = 0x01
|
|
12
|
+
SENSOR_VIN_NUM = 13
|
|
13
|
+
SENSOR_POWER_NUM = 11
|
|
14
|
+
FAN_NUM = 9
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# --- Structures ---
|
|
18
|
+
class VendorDataStruct(Structure):
|
|
19
|
+
_fields_ = [
|
|
20
|
+
('VendorId', c_uint8),
|
|
21
|
+
('ProductId', c_uint8),
|
|
22
|
+
('FwVersion', c_uint8),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PowerSensor(Structure):
|
|
27
|
+
_fields_ = [
|
|
28
|
+
('Voltage', c_int16),
|
|
29
|
+
('Current', c_int32),
|
|
30
|
+
('Power', c_int32),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FanSensor(Structure):
|
|
35
|
+
_fields_ = [
|
|
36
|
+
('Enable', c_uint8),
|
|
37
|
+
('Duty', c_uint8),
|
|
38
|
+
('Tach', c_uint16),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SensorStruct(Structure):
|
|
43
|
+
_fields_ = [
|
|
44
|
+
('Vin', c_int16 * SENSOR_VIN_NUM),
|
|
45
|
+
('Vdd', c_uint16),
|
|
46
|
+
('Vref', c_uint16),
|
|
47
|
+
('Tchip', c_int16),
|
|
48
|
+
('Ts', c_int16 * 4),
|
|
49
|
+
('Tamb', c_int16),
|
|
50
|
+
('Hum', c_int16),
|
|
51
|
+
('FanSwitchStatus', c_uint8),
|
|
52
|
+
('RGBSwitchStatus', c_uint8),
|
|
53
|
+
('RGBExtStatus', c_uint8),
|
|
54
|
+
('FanExtDuty', c_uint8),
|
|
55
|
+
('PowerReadings', PowerSensor * SENSOR_POWER_NUM),
|
|
56
|
+
('Fans', FanSensor * FAN_NUM),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --- Enums ---
|
|
61
|
+
class BENCHLAB_CMD(IntEnum):
|
|
62
|
+
UART_CMD_WELCOME = 0
|
|
63
|
+
UART_CMD_READ_SENSORS = 1
|
|
64
|
+
UART_CMD_ACTION = 2
|
|
65
|
+
UART_CMD_READ_NAME = 3
|
|
66
|
+
UART_CMD_WRITE_NAME = 4
|
|
67
|
+
UART_CMD_READ_FAN_PROFILE = 5
|
|
68
|
+
UART_CMD_WRITE_FAN_PROFILE = 6
|
|
69
|
+
UART_CMD_READ_RGB = 7
|
|
70
|
+
UART_CMD_WRITE_RGB = 8
|
|
71
|
+
UART_CMD_READ_CALIBRATION = 9
|
|
72
|
+
UART_CMD_WRITE_CALIBRATION = 10
|
|
73
|
+
UART_CMD_LOAD_CALIBRATION = 11
|
|
74
|
+
UART_CMD_STORE_CALIBRATION = 12
|
|
75
|
+
UART_CMD_READ_UID = 13
|
|
76
|
+
UART_CMD_READ_VENDOR_DATA = 14
|
|
77
|
+
|
|
78
|
+
def toByte(self):
|
|
79
|
+
return self.value.to_bytes(1, 'little')
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: benchlab-pycore
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: BENCHLAB PyCore - Python Interface for BENCHLAB
|
|
5
|
+
Home-page: https://github.com/BenchLab-io/benchlab-pycore
|
|
6
|
+
Author: Pieter Plaisier
|
|
7
|
+
Author-email: Pieter Plaisier <contact@benchlab.io>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/BenchLab-io/benchlab-pycore
|
|
10
|
+
Project-URL: Issues, https://github.com/BenchLab-io/benchlab-pycore/issues
|
|
11
|
+
Keywords: telemetry,hardware,benchlab,sensors
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: pyserial>=3.5
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# BENCHLAB Python Core (`benchlab-pycore`)
|
|
22
|
+
|
|
23
|
+
**BENCHLAB PyCore** provides low-level telemetry, sensor IO, and device communication utilities
|
|
24
|
+
for BENCHLAB. It includes standardized interfaces for reading sensors, handling serial communication, and publishing telemetry data.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Read and translate sensor data from BENCHLAB devices
|
|
29
|
+
- Communicate via serial ports
|
|
30
|
+
- Designed to be easily integrated into larger telemetry pipelines
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
pip install benchlab-pycore
|
|
35
|
+
|
|
36
|
+
## Usage Example
|
|
37
|
+
|
|
38
|
+
import benchlab
|
|
39
|
+
from benchlab.core import get_fleet_info, read_sensors
|
|
40
|
+
|
|
41
|
+
info = get_fleet_info()
|
|
42
|
+
print(info)
|
|
43
|
+
|
|
44
|
+
## Project Layout
|
|
45
|
+
|
|
46
|
+
benchlab/
|
|
47
|
+
├── core/
|
|
48
|
+
│ ├── serial_io.py
|
|
49
|
+
│ ├── sensor_translation.py
|
|
50
|
+
│ └── ...
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
Clone and install locally:
|
|
55
|
+
|
|
56
|
+
git clone https://github.com/BenchLab-io/benchlab-pycore.git
|
|
57
|
+
cd benchlab-pycore
|
|
58
|
+
pip install -e .
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT License © 2025 BenchLab Contributors
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
benchlab/__init__.py
|
|
6
|
+
benchlab/version.py
|
|
7
|
+
benchlab/core/__init__.py
|
|
8
|
+
benchlab/core/sensor_translation.py
|
|
9
|
+
benchlab/core/serial_io.py
|
|
10
|
+
benchlab/core/structures.py
|
|
11
|
+
benchlab/core/utils.py
|
|
12
|
+
benchlab_pycore.egg-info/PKG-INFO
|
|
13
|
+
benchlab_pycore.egg-info/SOURCES.txt
|
|
14
|
+
benchlab_pycore.egg-info/dependency_links.txt
|
|
15
|
+
benchlab_pycore.egg-info/requires.txt
|
|
16
|
+
benchlab_pycore.egg-info/top_level.txt
|
|
17
|
+
tests/__init__.py
|
|
18
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyserial>=3.5
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# 👇 The name people install from PyPI
|
|
7
|
+
name = "benchlab-pycore"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "BENCHLAB PyCore - Python Interface for BENCHLAB"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
license = {text = "MIT"}
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Pieter Plaisier", email = "contact@benchlab.io"}
|
|
15
|
+
]
|
|
16
|
+
keywords = ["telemetry", "hardware", "benchlab", "sensors"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent"
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"pyserial >= 3.5"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/BenchLab-io/benchlab-pycore"
|
|
28
|
+
Issues = "https://github.com/BenchLab-io/benchlab-pycore/issues"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = benchlab-pycore
|
|
3
|
+
version = 0.1.0
|
|
4
|
+
author = Pieter Plaisier
|
|
5
|
+
author_email = contact@benchlab.io
|
|
6
|
+
description = BENCHLAB PyCore - Python Interface for BENCHLAB
|
|
7
|
+
long_description = file: README.md
|
|
8
|
+
long_description_content_type = text/markdown
|
|
9
|
+
license = MIT
|
|
10
|
+
url = https://github.com/BenchLab-io/benchlab-pycore
|
|
11
|
+
project_urls =
|
|
12
|
+
Bug Tracker = https://github.com/BenchLab-io/benchlab-pycore/issues
|
|
13
|
+
classifiers =
|
|
14
|
+
Programming Language :: Python :: 3
|
|
15
|
+
License :: OSI Approved :: MIT License
|
|
16
|
+
Operating System :: OS Independent
|
|
17
|
+
|
|
18
|
+
[options]
|
|
19
|
+
packages = find:
|
|
20
|
+
python_requires = >=3.8
|
|
21
|
+
install_requires =
|
|
22
|
+
pyserial
|
|
23
|
+
|
|
24
|
+
[options.package_data]
|
|
25
|
+
* = *.md, *.txt
|
|
26
|
+
|
|
27
|
+
[options.extras_require]
|
|
28
|
+
dev =
|
|
29
|
+
build
|
|
30
|
+
twine
|
|
31
|
+
pytest
|
|
32
|
+
|
|
33
|
+
[egg_info]
|
|
34
|
+
tag_build =
|
|
35
|
+
tag_date = 0
|
|
36
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# tests/test_core.py
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from ctypes import Structure, sizeof
|
|
5
|
+
from unittest.mock import patch, MagicMock
|
|
6
|
+
|
|
7
|
+
from benchlab.core import (
|
|
8
|
+
get_benchlab_ports,
|
|
9
|
+
find_benchlab_devices,
|
|
10
|
+
get_fleet_info,
|
|
11
|
+
read_uart,
|
|
12
|
+
read_device,
|
|
13
|
+
read_sensors,
|
|
14
|
+
read_uid,
|
|
15
|
+
translate_sensor_struct,
|
|
16
|
+
format_temp,
|
|
17
|
+
VendorDataStruct,
|
|
18
|
+
PowerSensor,
|
|
19
|
+
FanSensor,
|
|
20
|
+
SensorStruct,
|
|
21
|
+
BENCHLAB_CMD,
|
|
22
|
+
SENSOR_VIN_NUM
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# -----------------------
|
|
26
|
+
# Test core imports
|
|
27
|
+
# -----------------------
|
|
28
|
+
def test_imports():
|
|
29
|
+
"""Ensure all core functions and classes exist and structures are correct."""
|
|
30
|
+
# functions
|
|
31
|
+
assert callable(get_benchlab_ports)
|
|
32
|
+
assert callable(find_benchlab_devices)
|
|
33
|
+
assert callable(get_fleet_info)
|
|
34
|
+
assert callable(read_sensors)
|
|
35
|
+
assert callable(read_uid)
|
|
36
|
+
assert callable(read_device)
|
|
37
|
+
assert callable(translate_sensor_struct)
|
|
38
|
+
|
|
39
|
+
# ctypes structures
|
|
40
|
+
assert isinstance(VendorDataStruct(), Structure)
|
|
41
|
+
assert isinstance(PowerSensor(), Structure)
|
|
42
|
+
assert isinstance(FanSensor(), Structure)
|
|
43
|
+
assert isinstance(SensorStruct(), Structure)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# -----------------------
|
|
47
|
+
# Test mocked serial functions
|
|
48
|
+
# -----------------------
|
|
49
|
+
@patch("benchlab.core.serial_io.read_uart")
|
|
50
|
+
def test_mocked_functions(mock_read_uart):
|
|
51
|
+
"""Test core functions using mocks (no hardware needed)."""
|
|
52
|
+
|
|
53
|
+
# Mock read_uart for read_uid
|
|
54
|
+
mock_read_uart.return_value = bytes(range(12))
|
|
55
|
+
uid = read_uid(MagicMock())
|
|
56
|
+
assert uid == bytes(range(12)).hex().upper()
|
|
57
|
+
|
|
58
|
+
# Mock read_uart for read_device
|
|
59
|
+
mock_read_uart.return_value = bytes([0xEE, 0x10, 0x01])
|
|
60
|
+
dev_info = read_device(MagicMock())
|
|
61
|
+
assert dev_info == {
|
|
62
|
+
"VendorId": 0xEE,
|
|
63
|
+
"ProductId": 0x10,
|
|
64
|
+
"FwVersion": 0x01,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Mock read_uart for read_sensors
|
|
68
|
+
mock_read_uart.return_value = bytes([0] * sizeof(SensorStruct))
|
|
69
|
+
sensors = read_sensors(MagicMock())
|
|
70
|
+
assert isinstance(sensors, SensorStruct)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# -----------------------
|
|
74
|
+
# Test translate_sensor_struct
|
|
75
|
+
# -----------------------
|
|
76
|
+
def test_sensor_translation():
|
|
77
|
+
"""Test translate_sensor_struct with a sample SensorStruct."""
|
|
78
|
+
s = SensorStruct()
|
|
79
|
+
# Fill sample values
|
|
80
|
+
for i in range(SENSOR_VIN_NUM):
|
|
81
|
+
s.Vin[i] = i * 10
|
|
82
|
+
s.Vdd = 3300
|
|
83
|
+
s.Tchip = 42
|
|
84
|
+
|
|
85
|
+
translated = translate_sensor_struct(s)
|
|
86
|
+
|
|
87
|
+
# Check keys exist and values are reasonable
|
|
88
|
+
# Adapt these depending on how translate_sensor_struct formats keys
|
|
89
|
+
assert isinstance(translated, dict)
|
|
90
|
+
# Example keys
|
|
91
|
+
assert "12V_Voltage" in translated
|
|
92
|
+
assert translated["12V_Voltage"] == 0.0
|
|
93
|
+
assert "3.3V_Current" in translated
|
|
94
|
+
assert translated["3.3V_Current"] == 0.0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# -----------------------
|
|
98
|
+
# Test format_temp
|
|
99
|
+
# -----------------------
|
|
100
|
+
def test_format_temp():
|
|
101
|
+
"""Test format_temp utility."""
|
|
102
|
+
val = 0
|
|
103
|
+
formatted = format_temp(val)
|
|
104
|
+
assert isinstance(formatted, float)
|
|
105
|
+
assert formatted == 0.0
|
|
106
|
+
|
|
107
|
+
val = 255
|
|
108
|
+
formatted = format_temp(val)
|
|
109
|
+
assert isinstance(formatted, float)
|
|
110
|
+
assert formatted == 25.5
|
|
111
|
+
|
|
112
|
+
val = -42
|
|
113
|
+
formatted = format_temp(val)
|
|
114
|
+
assert isinstance(formatted, float)
|
|
115
|
+
assert formatted == -4.2
|
|
116
|
+
|