pmd-pycore 0.2.3__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,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: pmd-pycore
3
+ Version: 0.2.3
4
+ Summary: PMD PyCore - Python Interface for ElmorLabs PMD2/PMD2X/PMD-USB
5
+ Home-page: https://github.com/ElmorLabs-P1/pmd-pycore
6
+ Author: Pieter Plaisier
7
+ Author-email: Pieter Plaisier <pieter@hanweiconsulting.com>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/ElmorLabs-P1/pmd-pycore
10
+ Project-URL: Issues, https://github.com/ElmorLabs-P1/pmd-pycore/issues
11
+ Keywords: telemetry,hardware,pmd2,pmd-usb,elmorlabs,sensors
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: pyserial>=3.5
20
+
21
+ # pmd-pycore
22
+
23
+ Python interface for ElmorLabs PMD power measurement devices (PMD2, PMD2X, PMD-USB).
24
+
25
+ ## Supported devices
26
+
27
+ | [PMD-USB](https://elmorlabs.com/product/elmorlabs-pmd-usb-power-measurement-device-with-usb/) *(discontinued)* | [PMD2](https://elmorlabs.com/product/elmorlabs-pmd2/) *(discontinued)* | [PMD2X](https://elmorlabs.com/product/elmorlabs-pmd2x-power-measurement-device-2x/) *(current)* |
28
+ |:---:|:---:|:---:|
29
+ | <img src="https://elmorlabs.com/wp-content/uploads/2022/05/with_case-scaled.jpg" width="220"> | <img src="https://elmorlabs.com/wp-content/uploads/2024/10/IMG_0216-scaled.jpg" width="220"> | <img src="https://elmorlabs.com/wp-content/uploads/2026/05/IMG_7900-scaled.jpg" width="220"> |
30
+ | 4 channels (PCIE1, PCIE2, EPS1, EPS2) over a CH340 USB-serial bridge. No ATX24/motherboard rail, no 12VHPWR. | 2x EPS, 3x PCIe, 1x ATX24, 1x 12VHPWR connector. STM32 USB-serial interface. Superseded by PMD2X. | Same rail set as PMD2, plus per-wire 12VHPWR monitoring and connector thermistors. |
31
+
32
+ PMD-USB was superseded by PMD2, which was in turn superseded by PMD2X -- the only one of the
33
+ three still sold. All three measure voltage/current/power on CPU, GPU, and (PMD2/PMD2X only)
34
+ motherboard power cables, with an OLED display and USB connection to a PC for logging.
35
+
36
+ ## Pre-requisites
37
+ - Python >= 3.11, with pyserial (and numpy for the example scripts)
38
+ - PMD-USB uses a CH340 USB-serial chip. Linux has an in-kernel driver for it (binds
39
+ automatically, nothing to install). **Windows does not ship one** -- if the device shows up
40
+ in Device Manager but never gets a COM port, install the CH340/CH341 driver from the chip
41
+ manufacturer's site ([wch-ic.com](https://www.wch-ic.com/), search "CH340"/"CH341") and
42
+ replug. PMD2/PMD2X use an STM32 VCP instead and don't need a separate driver on either OS.
43
+
44
+ ## Install
45
+
46
+ ```
47
+ pip install -r requirements.txt
48
+ pip install -e .
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ### PMD2 / PMD2X
54
+
55
+ ```python
56
+ from pmd_pycore.core import (
57
+ get_pmd2_ports,
58
+ read_pmd2_welcome_string,
59
+ read_pmd2_device,
60
+ read_pmd2_uid,
61
+ read_pmd2_sensors,
62
+ translate_pmd2_sensor_struct,
63
+ )
64
+ import serial
65
+
66
+ port = get_pmd2_ports()[0]["port"]
67
+ ser = serial.Serial(port, 115200, timeout=1, rtscts=False, dsrdtr=False)
68
+
69
+ device_info = read_pmd2_device(ser)
70
+ sensors = read_pmd2_sensors(ser, device_info["ProductId"])
71
+ readings = translate_pmd2_sensor_struct(sensors, device_info["ProductId"])
72
+ print(readings["12V"]) # {'voltage': 12.0, 'current': 1.0, 'power': 12.0}
73
+ ```
74
+
75
+ Currently implemented are the device's read-only commands:
76
+ `CMD_WELCOME` (`read_pmd2_welcome_string`), `CMD_READ_VENDOR_DATA` (`read_pmd2_device`),
77
+ `CMD_READ_UID` (`read_pmd2_uid`), and `CMD_READ_SENSOR_VALUES` (`read_pmd2_sensors`).
78
+ Writing/action commands (continuous TX, config read/write, reset, bootloader) are not
79
+ implemented.
80
+
81
+ See `examples/read_sensors.py` — a polling/averaging loop that mirrors the original sample script.
82
+
83
+ ### PMD-USB
84
+
85
+ PMD-USB is a different device family -- a CH340 USB-serial bridge rather than PMD2/PMD2X's STM32
86
+ VCP, with its own command set and a simpler sensor set (PCIE1, PCIE2, EPS1, EPS2 only; no ATX24
87
+ rails, no 12VHPWR). There's no firmware source for it, so this was ported from and verified
88
+ against the reference implementations linked below plus a real device.
89
+
90
+ ```python
91
+ from pmd_pycore.core import get_pmdusb_ports, read_pmdusb_id, read_pmdusb_sensors, translate_pmdusb_sensors
92
+ import serial
93
+
94
+ port = get_pmdusb_ports()[0]["port"]
95
+ ser = serial.Serial(port, 115200, timeout=1)
96
+
97
+ device_info = read_pmdusb_id(ser)
98
+ sensors = read_pmdusb_sensors(ser)
99
+ readings = translate_pmdusb_sensors(sensors)
100
+ print(readings["EPS1"]) # {'voltage': 12.0, 'current': 1.0, 'power': 12.0}
101
+ ```
102
+
103
+ Implemented: `CMD_WELCOME` (`read_pmdusb_welcome_string`), `CMD_READ_ID` (`read_pmdusb_id`), and
104
+ `CMD_READ_SENSOR_VALUES` (`read_pmdusb_sensors`/`translate_pmdusb_sensors`). This command already
105
+ returns calibrated readings -- confirmed against the official PMD-USB GUI app's source, which
106
+ uses it directly with no client-side calibration math -- so `CMD_READ_ADC`'s raw path isn't
107
+ needed. `translate_pmdusb_aggregate_power` adds GPU/CPU/Total power (PCIE1+PCIE2 / EPS1+EPS2 /
108
+ sum), computed client-side the same way the official app does; PMD-USB's firmware doesn't
109
+ compute these on-device the way PMD2/PMD2X's does.
110
+
111
+ Note: PMD-USB has no per-unit UID/serial number anywhere -- not in the UART protocol (no command
112
+ for it), and not at the USB level (the CH340 chip's serial number field reads empty on the
113
+ device tested here). If you have more than one, there's no way to tell them apart besides which
114
+ COM port they land on.
115
+
116
+ See [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) for what's deliberately out of scope and why.
117
+
118
+ ## Examples
119
+
120
+ - `examples/quickstart.py` — connect to the first device found, trying PMD2/PMD2X first and
121
+ falling back to PMD-USB, print its identity, and read sensor values once (for PMD2/PMD2X:
122
+ per-rail readings, firmware-computed aggregate power, chip health, and PMD2X's 12VHPWR
123
+ connector temperatures).
124
+ - `examples/read_sensors.py` — a polling/averaging loop, refreshing in place, for whichever
125
+ device family is connected (same PMD2/PMD2X-first-then-PMD-USB detection as `quickstart.py`).
126
+
127
+ ## Status
128
+
129
+ This repository is under active development at
130
+ [ElmorLabs-P1/pmd-pycore](https://github.com/ElmorLabs-P1/pmd-pycore). It provides an installable
131
+ `pmd_pycore` package (serial discovery, UART command helpers, sensor structures, and
132
+ readable-unit translation for the supported devices), a test suite, and CI for tests and tagged
133
+ releases.
134
+
135
+ Tagged releases build an sdist/wheel and attach them to a GitHub Release. A PyPI-publish job is
136
+ wired into the release workflow but not yet active -- it needs PyPI Trusted Publishing configured
137
+ for this repo first (no credentials are stored in GitHub either way). Until that's set up,
138
+ install from the GitHub Release assets or `pip install -e .` from a checkout.
139
+
140
+ ## Related repositories
141
+
142
+ - [PMDLogger](https://github.com/ElmorLabs/PMDLogger) — C# reference implementation for PMD-USB
143
+ (there's no public PMD-USB firmware source); `PMD_USB_Device.cs` has the command set and sensor
144
+ scaling used here.
145
+ - [PMD-USB](https://github.com/ElmorLabs/PMD-USB) — the official PMD-USB GUI app; `FormPMD.cs`
146
+ confirmed `CMD_READ_SENSOR_VALUES` is already calibrated and has the `CMD_READ_CONFIG` struct
147
+ layout.
148
+ - [pmd-usb-serial-interface-fast](https://github.com/ElmorLabs/pmd-usb-serial-interface-fast) /
149
+ [elmorlabs-pmd-usb-serial-interface](https://github.com/bjorntas/elmorlabs-pmd-usb-serial-interface) —
150
+ Python sample scripts for PMD-USB, cross-checked against PMDLogger and a real device.
@@ -0,0 +1,130 @@
1
+ # pmd-pycore
2
+
3
+ Python interface for ElmorLabs PMD power measurement devices (PMD2, PMD2X, PMD-USB).
4
+
5
+ ## Supported devices
6
+
7
+ | [PMD-USB](https://elmorlabs.com/product/elmorlabs-pmd-usb-power-measurement-device-with-usb/) *(discontinued)* | [PMD2](https://elmorlabs.com/product/elmorlabs-pmd2/) *(discontinued)* | [PMD2X](https://elmorlabs.com/product/elmorlabs-pmd2x-power-measurement-device-2x/) *(current)* |
8
+ |:---:|:---:|:---:|
9
+ | <img src="https://elmorlabs.com/wp-content/uploads/2022/05/with_case-scaled.jpg" width="220"> | <img src="https://elmorlabs.com/wp-content/uploads/2024/10/IMG_0216-scaled.jpg" width="220"> | <img src="https://elmorlabs.com/wp-content/uploads/2026/05/IMG_7900-scaled.jpg" width="220"> |
10
+ | 4 channels (PCIE1, PCIE2, EPS1, EPS2) over a CH340 USB-serial bridge. No ATX24/motherboard rail, no 12VHPWR. | 2x EPS, 3x PCIe, 1x ATX24, 1x 12VHPWR connector. STM32 USB-serial interface. Superseded by PMD2X. | Same rail set as PMD2, plus per-wire 12VHPWR monitoring and connector thermistors. |
11
+
12
+ PMD-USB was superseded by PMD2, which was in turn superseded by PMD2X -- the only one of the
13
+ three still sold. All three measure voltage/current/power on CPU, GPU, and (PMD2/PMD2X only)
14
+ motherboard power cables, with an OLED display and USB connection to a PC for logging.
15
+
16
+ ## Pre-requisites
17
+ - Python >= 3.11, with pyserial (and numpy for the example scripts)
18
+ - PMD-USB uses a CH340 USB-serial chip. Linux has an in-kernel driver for it (binds
19
+ automatically, nothing to install). **Windows does not ship one** -- if the device shows up
20
+ in Device Manager but never gets a COM port, install the CH340/CH341 driver from the chip
21
+ manufacturer's site ([wch-ic.com](https://www.wch-ic.com/), search "CH340"/"CH341") and
22
+ replug. PMD2/PMD2X use an STM32 VCP instead and don't need a separate driver on either OS.
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pip install -r requirements.txt
28
+ pip install -e .
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### PMD2 / PMD2X
34
+
35
+ ```python
36
+ from pmd_pycore.core import (
37
+ get_pmd2_ports,
38
+ read_pmd2_welcome_string,
39
+ read_pmd2_device,
40
+ read_pmd2_uid,
41
+ read_pmd2_sensors,
42
+ translate_pmd2_sensor_struct,
43
+ )
44
+ import serial
45
+
46
+ port = get_pmd2_ports()[0]["port"]
47
+ ser = serial.Serial(port, 115200, timeout=1, rtscts=False, dsrdtr=False)
48
+
49
+ device_info = read_pmd2_device(ser)
50
+ sensors = read_pmd2_sensors(ser, device_info["ProductId"])
51
+ readings = translate_pmd2_sensor_struct(sensors, device_info["ProductId"])
52
+ print(readings["12V"]) # {'voltage': 12.0, 'current': 1.0, 'power': 12.0}
53
+ ```
54
+
55
+ Currently implemented are the device's read-only commands:
56
+ `CMD_WELCOME` (`read_pmd2_welcome_string`), `CMD_READ_VENDOR_DATA` (`read_pmd2_device`),
57
+ `CMD_READ_UID` (`read_pmd2_uid`), and `CMD_READ_SENSOR_VALUES` (`read_pmd2_sensors`).
58
+ Writing/action commands (continuous TX, config read/write, reset, bootloader) are not
59
+ implemented.
60
+
61
+ See `examples/read_sensors.py` — a polling/averaging loop that mirrors the original sample script.
62
+
63
+ ### PMD-USB
64
+
65
+ PMD-USB is a different device family -- a CH340 USB-serial bridge rather than PMD2/PMD2X's STM32
66
+ VCP, with its own command set and a simpler sensor set (PCIE1, PCIE2, EPS1, EPS2 only; no ATX24
67
+ rails, no 12VHPWR). There's no firmware source for it, so this was ported from and verified
68
+ against the reference implementations linked below plus a real device.
69
+
70
+ ```python
71
+ from pmd_pycore.core import get_pmdusb_ports, read_pmdusb_id, read_pmdusb_sensors, translate_pmdusb_sensors
72
+ import serial
73
+
74
+ port = get_pmdusb_ports()[0]["port"]
75
+ ser = serial.Serial(port, 115200, timeout=1)
76
+
77
+ device_info = read_pmdusb_id(ser)
78
+ sensors = read_pmdusb_sensors(ser)
79
+ readings = translate_pmdusb_sensors(sensors)
80
+ print(readings["EPS1"]) # {'voltage': 12.0, 'current': 1.0, 'power': 12.0}
81
+ ```
82
+
83
+ Implemented: `CMD_WELCOME` (`read_pmdusb_welcome_string`), `CMD_READ_ID` (`read_pmdusb_id`), and
84
+ `CMD_READ_SENSOR_VALUES` (`read_pmdusb_sensors`/`translate_pmdusb_sensors`). This command already
85
+ returns calibrated readings -- confirmed against the official PMD-USB GUI app's source, which
86
+ uses it directly with no client-side calibration math -- so `CMD_READ_ADC`'s raw path isn't
87
+ needed. `translate_pmdusb_aggregate_power` adds GPU/CPU/Total power (PCIE1+PCIE2 / EPS1+EPS2 /
88
+ sum), computed client-side the same way the official app does; PMD-USB's firmware doesn't
89
+ compute these on-device the way PMD2/PMD2X's does.
90
+
91
+ Note: PMD-USB has no per-unit UID/serial number anywhere -- not in the UART protocol (no command
92
+ for it), and not at the USB level (the CH340 chip's serial number field reads empty on the
93
+ device tested here). If you have more than one, there's no way to tell them apart besides which
94
+ COM port they land on.
95
+
96
+ See [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) for what's deliberately out of scope and why.
97
+
98
+ ## Examples
99
+
100
+ - `examples/quickstart.py` — connect to the first device found, trying PMD2/PMD2X first and
101
+ falling back to PMD-USB, print its identity, and read sensor values once (for PMD2/PMD2X:
102
+ per-rail readings, firmware-computed aggregate power, chip health, and PMD2X's 12VHPWR
103
+ connector temperatures).
104
+ - `examples/read_sensors.py` — a polling/averaging loop, refreshing in place, for whichever
105
+ device family is connected (same PMD2/PMD2X-first-then-PMD-USB detection as `quickstart.py`).
106
+
107
+ ## Status
108
+
109
+ This repository is under active development at
110
+ [ElmorLabs-P1/pmd-pycore](https://github.com/ElmorLabs-P1/pmd-pycore). It provides an installable
111
+ `pmd_pycore` package (serial discovery, UART command helpers, sensor structures, and
112
+ readable-unit translation for the supported devices), a test suite, and CI for tests and tagged
113
+ releases.
114
+
115
+ Tagged releases build an sdist/wheel and attach them to a GitHub Release. A PyPI-publish job is
116
+ wired into the release workflow but not yet active -- it needs PyPI Trusted Publishing configured
117
+ for this repo first (no credentials are stored in GitHub either way). Until that's set up,
118
+ install from the GitHub Release assets or `pip install -e .` from a checkout.
119
+
120
+ ## Related repositories
121
+
122
+ - [PMDLogger](https://github.com/ElmorLabs/PMDLogger) — C# reference implementation for PMD-USB
123
+ (there's no public PMD-USB firmware source); `PMD_USB_Device.cs` has the command set and sensor
124
+ scaling used here.
125
+ - [PMD-USB](https://github.com/ElmorLabs/PMD-USB) — the official PMD-USB GUI app; `FormPMD.cs`
126
+ confirmed `CMD_READ_SENSOR_VALUES` is already calibrated and has the `CMD_READ_CONFIG` struct
127
+ layout.
128
+ - [pmd-usb-serial-interface-fast](https://github.com/ElmorLabs/pmd-usb-serial-interface-fast) /
129
+ [elmorlabs-pmd-usb-serial-interface](https://github.com/bjorntas/elmorlabs-pmd-usb-serial-interface) —
130
+ Python sample scripts for PMD-USB, cross-checked against PMDLogger and a real device.
@@ -0,0 +1,15 @@
1
+ # pmd_pycore/__init__.py
2
+
3
+ from importlib.metadata import version as get_version, PackageNotFoundError
4
+ from .version import __version__
5
+
6
+ try:
7
+ installed_version = get_version("pmd-pycore")
8
+ if installed_version != __version__:
9
+ __version__ = installed_version
10
+ except PackageNotFoundError:
11
+ pass # Use the version from version.py
12
+
13
+ from . import core
14
+
15
+ __all__ = ["core", "__version__"]
@@ -0,0 +1,101 @@
1
+ # pmd_pycore/core/__init__.py
2
+
3
+ from .pmd2 import (
4
+ get_pmd2_ports,
5
+ find_pmd2_devices,
6
+ read_pmd2_uart,
7
+ read_pmd2_welcome_string,
8
+ read_pmd2_device,
9
+ read_pmd2_uid,
10
+ read_pmd2_sensors,
11
+ translate_pmd2_sensor_struct,
12
+ translate_pmd2_aggregate_power,
13
+ translate_pmd2_chip_sensors,
14
+ translate_pmd2x_hpwr_temperatures,
15
+ PMD2_CHANNELS,
16
+ PMD2X_CHANNELS,
17
+ PMD2VendorDataStruct,
18
+ PMD2PowerSensor,
19
+ PMD2SensorStruct,
20
+ PMD2XSensorStruct,
21
+ PMD2_CMD,
22
+ PMD2_VENDOR_ID,
23
+ PMD2_PRODUCT_ID,
24
+ PMD2X_PRODUCT_ID,
25
+ PMD2_PRODUCT_STRING,
26
+ PMD2X_PRODUCT_STRING,
27
+ PMD2_SENSOR_POWER_NUM,
28
+ PMD2X_SENSOR_POWER_NUM,
29
+ PMD2X_SENSOR_TS_NUM,
30
+ PMD2X_TS_IN,
31
+ PMD2X_TS_OUT,
32
+ PMD2X_TS_NOT_CONNECTED,
33
+ )
34
+ from .pmdusb import (
35
+ get_pmdusb_ports,
36
+ read_pmdusb_uart,
37
+ read_pmdusb_welcome_string,
38
+ read_pmdusb_id,
39
+ read_pmdusb_sensors,
40
+ translate_pmdusb_sensors,
41
+ translate_pmdusb_aggregate_power,
42
+ PMDUSBIdStruct,
43
+ PMDUSBChannelValues,
44
+ PMDUSBSensorStruct,
45
+ PMDUSB_CMD,
46
+ PMDUSB_VENDOR_ID,
47
+ PMDUSB_PRODUCT_ID,
48
+ PMDUSB_PRODUCT_STRING,
49
+ PMDUSB_CHANNEL_NUM,
50
+ PMDUSB_CHANNELS,
51
+ )
52
+
53
+ __all__ = [
54
+ # pmd2
55
+ "get_pmd2_ports",
56
+ "find_pmd2_devices",
57
+ "read_pmd2_uart",
58
+ "read_pmd2_welcome_string",
59
+ "read_pmd2_device",
60
+ "read_pmd2_uid",
61
+ "read_pmd2_sensors",
62
+ "translate_pmd2_sensor_struct",
63
+ "translate_pmd2_aggregate_power",
64
+ "translate_pmd2_chip_sensors",
65
+ "translate_pmd2x_hpwr_temperatures",
66
+ "PMD2_CHANNELS",
67
+ "PMD2X_CHANNELS",
68
+ "PMD2VendorDataStruct",
69
+ "PMD2PowerSensor",
70
+ "PMD2SensorStruct",
71
+ "PMD2XSensorStruct",
72
+ "PMD2_CMD",
73
+ "PMD2_VENDOR_ID",
74
+ "PMD2_PRODUCT_ID",
75
+ "PMD2X_PRODUCT_ID",
76
+ "PMD2_PRODUCT_STRING",
77
+ "PMD2X_PRODUCT_STRING",
78
+ "PMD2_SENSOR_POWER_NUM",
79
+ "PMD2X_SENSOR_POWER_NUM",
80
+ "PMD2X_SENSOR_TS_NUM",
81
+ "PMD2X_TS_IN",
82
+ "PMD2X_TS_OUT",
83
+ "PMD2X_TS_NOT_CONNECTED",
84
+ # pmdusb
85
+ "get_pmdusb_ports",
86
+ "read_pmdusb_uart",
87
+ "read_pmdusb_welcome_string",
88
+ "read_pmdusb_id",
89
+ "read_pmdusb_sensors",
90
+ "translate_pmdusb_sensors",
91
+ "translate_pmdusb_aggregate_power",
92
+ "PMDUSBIdStruct",
93
+ "PMDUSBChannelValues",
94
+ "PMDUSBSensorStruct",
95
+ "PMDUSB_CMD",
96
+ "PMDUSB_VENDOR_ID",
97
+ "PMDUSB_PRODUCT_ID",
98
+ "PMDUSB_PRODUCT_STRING",
99
+ "PMDUSB_CHANNEL_NUM",
100
+ "PMDUSB_CHANNELS",
101
+ ]