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.
@@ -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')
@@ -0,0 +1,10 @@
1
+ """
2
+ Utility helpers for BENCHLAB
3
+ """
4
+
5
+
6
+ def format_temp(value):
7
+ temp_c = value / 10
8
+ if temp_c > 1000 or temp_c < -1000:
9
+ return None
10
+ return round(temp_c, 1)
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
+ pyserial>=3.5
@@ -0,0 +1,2 @@
1
+ benchlab
2
+ tests
@@ -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,2 @@
1
+ # tests/__init__.py
2
+ # (Empty — required to make tests a package)
@@ -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
+