pmd-pycore 0.2.3__tar.gz → 0.2.4__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.
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/PKG-INFO +22 -8
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/README.md +21 -7
- pmd_pycore-0.2.4/pmd_pycore/examples/__init__.py +0 -0
- pmd_pycore-0.2.4/pmd_pycore/examples/quickstart.py +190 -0
- pmd_pycore-0.2.4/pmd_pycore/examples/read_sensors.py +241 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore/version.py +1 -1
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore.egg-info/PKG-INFO +22 -8
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore.egg-info/SOURCES.txt +3 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pyproject.toml +1 -1
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/setup.cfg +1 -1
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore/__init__.py +0 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore/core/__init__.py +0 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore/core/pmd2.py +0 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore/core/pmdusb.py +0 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore.egg-info/dependency_links.txt +0 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore.egg-info/requires.txt +0 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/pmd_pycore.egg-info/top_level.txt +0 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/tests/test_pmd2.py +0 -0
- {pmd_pycore-0.2.3 → pmd_pycore-0.2.4}/tests/test_pmdusb.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pmd-pycore
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: PMD PyCore - Python Interface for ElmorLabs PMD2/PMD2X/PMD-USB
|
|
5
5
|
Home-page: https://github.com/ElmorLabs-P1/pmd-pycore
|
|
6
6
|
Author: Pieter Plaisier
|
|
@@ -43,11 +43,20 @@ motherboard power cables, with an OLED display and USB connection to a PC for lo
|
|
|
43
43
|
|
|
44
44
|
## Install
|
|
45
45
|
|
|
46
|
+
```
|
|
47
|
+
pip install pmd-pycore
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or from a checkout:
|
|
51
|
+
|
|
46
52
|
```
|
|
47
53
|
pip install -r requirements.txt
|
|
48
54
|
pip install -e .
|
|
49
55
|
```
|
|
50
56
|
|
|
57
|
+
This also installs the example scripts (see [Examples](#examples) below) as part of the package,
|
|
58
|
+
runnable with `python -m pmd_pycore.examples.quickstart`.
|
|
59
|
+
|
|
51
60
|
## Usage
|
|
52
61
|
|
|
53
62
|
### PMD2 / PMD2X
|
|
@@ -78,7 +87,8 @@ Currently implemented are the device's read-only commands:
|
|
|
78
87
|
Writing/action commands (continuous TX, config read/write, reset, bootloader) are not
|
|
79
88
|
implemented.
|
|
80
89
|
|
|
81
|
-
See `examples
|
|
90
|
+
See `python -m pmd_pycore.examples.read_sensors` (below) — a polling/averaging loop that mirrors
|
|
91
|
+
the original sample script.
|
|
82
92
|
|
|
83
93
|
### PMD-USB
|
|
84
94
|
|
|
@@ -117,12 +127,16 @@ See [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) for what's deliberately out of
|
|
|
117
127
|
|
|
118
128
|
## Examples
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
Both examples ship as part of the installed package -- run them with `python -m` after
|
|
131
|
+
`pip install pmd-pycore`, no checkout needed.
|
|
132
|
+
|
|
133
|
+
- `python -m pmd_pycore.examples.quickstart` — connect to the first device found, trying
|
|
134
|
+
PMD2/PMD2X first and falling back to PMD-USB, print its identity, and read sensor values once
|
|
135
|
+
(for PMD2/PMD2X: per-rail readings, firmware-computed aggregate power, chip health, and
|
|
136
|
+
PMD2X's 12VHPWR connector temperatures).
|
|
137
|
+
- `python -m pmd_pycore.examples.read_sensors` — a polling/averaging loop, refreshing in place,
|
|
138
|
+
for whichever device family is connected (same PMD2/PMD2X-first-then-PMD-USB detection as
|
|
139
|
+
`quickstart`).
|
|
126
140
|
|
|
127
141
|
## Status
|
|
128
142
|
|
|
@@ -23,11 +23,20 @@ motherboard power cables, with an OLED display and USB connection to a PC for lo
|
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
26
|
+
```
|
|
27
|
+
pip install pmd-pycore
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or from a checkout:
|
|
31
|
+
|
|
26
32
|
```
|
|
27
33
|
pip install -r requirements.txt
|
|
28
34
|
pip install -e .
|
|
29
35
|
```
|
|
30
36
|
|
|
37
|
+
This also installs the example scripts (see [Examples](#examples) below) as part of the package,
|
|
38
|
+
runnable with `python -m pmd_pycore.examples.quickstart`.
|
|
39
|
+
|
|
31
40
|
## Usage
|
|
32
41
|
|
|
33
42
|
### PMD2 / PMD2X
|
|
@@ -58,7 +67,8 @@ Currently implemented are the device's read-only commands:
|
|
|
58
67
|
Writing/action commands (continuous TX, config read/write, reset, bootloader) are not
|
|
59
68
|
implemented.
|
|
60
69
|
|
|
61
|
-
See `examples
|
|
70
|
+
See `python -m pmd_pycore.examples.read_sensors` (below) — a polling/averaging loop that mirrors
|
|
71
|
+
the original sample script.
|
|
62
72
|
|
|
63
73
|
### PMD-USB
|
|
64
74
|
|
|
@@ -97,12 +107,16 @@ See [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) for what's deliberately out of
|
|
|
97
107
|
|
|
98
108
|
## Examples
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
Both examples ship as part of the installed package -- run them with `python -m` after
|
|
111
|
+
`pip install pmd-pycore`, no checkout needed.
|
|
112
|
+
|
|
113
|
+
- `python -m pmd_pycore.examples.quickstart` — connect to the first device found, trying
|
|
114
|
+
PMD2/PMD2X first and falling back to PMD-USB, print its identity, and read sensor values once
|
|
115
|
+
(for PMD2/PMD2X: per-rail readings, firmware-computed aggregate power, chip health, and
|
|
116
|
+
PMD2X's 12VHPWR connector temperatures).
|
|
117
|
+
- `python -m pmd_pycore.examples.read_sensors` — a polling/averaging loop, refreshing in place,
|
|
118
|
+
for whichever device family is connected (same PMD2/PMD2X-first-then-PMD-USB detection as
|
|
119
|
+
`quickstart`).
|
|
106
120
|
|
|
107
121
|
## Status
|
|
108
122
|
|
|
File without changes
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minimal example: connect to the first supported device found -- trying
|
|
3
|
+
PMD2/PMD2X first, then PMD-USB -- print its identity, and read sensors once.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from typing import Dict, Iterator, Optional
|
|
9
|
+
|
|
10
|
+
import serial
|
|
11
|
+
|
|
12
|
+
from pmd_pycore.core import (
|
|
13
|
+
get_pmd2_ports,
|
|
14
|
+
read_pmd2_device,
|
|
15
|
+
read_pmd2_uid,
|
|
16
|
+
read_pmd2_sensors,
|
|
17
|
+
translate_pmd2_sensor_struct,
|
|
18
|
+
translate_pmd2_aggregate_power,
|
|
19
|
+
translate_pmd2_chip_sensors,
|
|
20
|
+
translate_pmd2x_hpwr_temperatures,
|
|
21
|
+
PMD2_PRODUCT_ID,
|
|
22
|
+
PMD2X_PRODUCT_ID,
|
|
23
|
+
get_pmdusb_ports,
|
|
24
|
+
read_pmdusb_id,
|
|
25
|
+
read_pmdusb_sensors,
|
|
26
|
+
translate_pmdusb_sensors,
|
|
27
|
+
translate_pmdusb_aggregate_power,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
PRODUCT_NAMES = {PMD2_PRODUCT_ID: "PMD2", PMD2X_PRODUCT_ID: "PMD2X"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- PMD2 / PMD2X ---
|
|
34
|
+
|
|
35
|
+
@contextmanager
|
|
36
|
+
def connect_pmd(port: str) -> Iterator[serial.Serial]:
|
|
37
|
+
try:
|
|
38
|
+
# rtscts/dsrdtr disabled: the device uses DTR as its "is a host listening"
|
|
39
|
+
# signal, so pyserial's automatic flow-control handling must stay out of the way.
|
|
40
|
+
with serial.Serial(
|
|
41
|
+
port, 115200, timeout=1, write_timeout=1, rtscts=False, dsrdtr=False
|
|
42
|
+
) as ser:
|
|
43
|
+
ser.dtr = True
|
|
44
|
+
# The device pushes an unsolicited welcome string on DTR assert, but
|
|
45
|
+
# the push can arrive after this point rather than before it. Give it
|
|
46
|
+
# a moment to land, then flush -- read_pmd2_uart() also flushes before each
|
|
47
|
+
# command, so a late push can never corrupt the next real response.
|
|
48
|
+
time.sleep(0.1)
|
|
49
|
+
ser.reset_input_buffer()
|
|
50
|
+
yield ser
|
|
51
|
+
except serial.SerialException as e:
|
|
52
|
+
raise SystemExit(
|
|
53
|
+
f"Failed to open {port}: {e}\n"
|
|
54
|
+
"(is another program -- a monitoring app, another instance of this "
|
|
55
|
+
"script -- already using the port?)"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def print_pmd_device_info(device_info: Dict[str, int], uid: Optional[str]) -> None:
|
|
60
|
+
product_name = PRODUCT_NAMES.get(device_info["ProductId"], "unknown")
|
|
61
|
+
print("--- Device ---")
|
|
62
|
+
print(f"Product: {product_name}")
|
|
63
|
+
print(f"Vendor ID: {hex(device_info['VendorId'])}")
|
|
64
|
+
print(f"Product ID: {hex(device_info['ProductId'])}")
|
|
65
|
+
print(f"Firmware: {hex(device_info['FwVersion'])}")
|
|
66
|
+
print(f"UID: {uid or 'unavailable'}")
|
|
67
|
+
print()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_rail_readings(readings: Dict[str, Dict[str, float]]) -> None:
|
|
71
|
+
print("--- Rails ---")
|
|
72
|
+
print(f'{"Rail":<10}{"Voltage":>9}{"Current":>9}{"Power":>9}')
|
|
73
|
+
for rail, reading in readings.items():
|
|
74
|
+
print(
|
|
75
|
+
f'{rail:<10}{reading["voltage"]:>8.3f}V{reading["current"]:>8.3f}A'
|
|
76
|
+
f'{reading["power"]:>8.3f}W'
|
|
77
|
+
)
|
|
78
|
+
print()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def print_aggregate_power(aggregates: Dict[str, float]) -> None:
|
|
82
|
+
print("--- Aggregate power ---")
|
|
83
|
+
print(f'EPS (CPU): {aggregates["eps"]:>8.3f} W')
|
|
84
|
+
print(f'PCIe (GPU): {aggregates["pcie"]:>8.3f} W')
|
|
85
|
+
print(f'Motherboard: {aggregates["mb"]:>8.3f} W')
|
|
86
|
+
print(f'Total: {aggregates["total"]:>8.3f} W')
|
|
87
|
+
print()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def print_pmd_device_health(chip: Dict[str, float], hpwr_temps: Optional[Dict[str, Optional[float]]]) -> None:
|
|
91
|
+
def fmt_temp(value: Optional[float]) -> str:
|
|
92
|
+
return f"{value:>8.1f} C" if value is not None else f"{'n/c':>8} C"
|
|
93
|
+
|
|
94
|
+
print("--- Device health ---")
|
|
95
|
+
print(f'{"Supply (Vdd):":<17}{chip["vdd"]:>8.3f} V')
|
|
96
|
+
print(f'{"Chip temp:":<17}{chip["tchip"]:>8.1f} C')
|
|
97
|
+
if hpwr_temps is not None:
|
|
98
|
+
print(f'{"12VHPWR in:":<17}{fmt_temp(hpwr_temps["ts_in"])}')
|
|
99
|
+
print(f'{"12VHPWR out:":<17}{fmt_temp(hpwr_temps["ts_out"])}')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_pmd(port: str) -> None:
|
|
103
|
+
with connect_pmd(port) as ser:
|
|
104
|
+
device_info = read_pmd2_device(ser)
|
|
105
|
+
if device_info is None:
|
|
106
|
+
raise SystemExit("Failed to read device info -- is this a PMD2/PMD2X device?")
|
|
107
|
+
|
|
108
|
+
product_id = device_info["ProductId"]
|
|
109
|
+
if product_id not in PRODUCT_NAMES:
|
|
110
|
+
raise SystemExit(f"Unrecognized ProductId {hex(product_id)} -- not a PMD2/PMD2X device?")
|
|
111
|
+
|
|
112
|
+
print_pmd_device_info(device_info, read_pmd2_uid(ser))
|
|
113
|
+
|
|
114
|
+
sensors = read_pmd2_sensors(ser, product_id)
|
|
115
|
+
if sensors is None:
|
|
116
|
+
raise SystemExit("Failed to read sensor values.")
|
|
117
|
+
|
|
118
|
+
print_rail_readings(translate_pmd2_sensor_struct(sensors, product_id))
|
|
119
|
+
print_aggregate_power(translate_pmd2_aggregate_power(sensors))
|
|
120
|
+
hpwr_temps = translate_pmd2x_hpwr_temperatures(sensors) if product_id == PMD2X_PRODUCT_ID else None
|
|
121
|
+
print_pmd_device_health(translate_pmd2_chip_sensors(sensors), hpwr_temps)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# --- PMD-USB ---
|
|
125
|
+
|
|
126
|
+
def print_pmdusb_device_info(device_info: Dict[str, int]) -> None:
|
|
127
|
+
print("--- Device ---")
|
|
128
|
+
print("Product: PMD-USB")
|
|
129
|
+
print(f"Vendor ID: {hex(device_info['VendorId'])}")
|
|
130
|
+
print(f"Product ID: {hex(device_info['ProductId'])}")
|
|
131
|
+
print(f"Firmware: {hex(device_info['FwVersion'])}")
|
|
132
|
+
print()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_pmdusb(port: str) -> None:
|
|
136
|
+
try:
|
|
137
|
+
ser = serial.Serial(port, 115200, timeout=1, write_timeout=1)
|
|
138
|
+
except serial.SerialException as e:
|
|
139
|
+
raise SystemExit(f"Failed to open {port}: {e}")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
device_info = read_pmdusb_id(ser)
|
|
143
|
+
if device_info is None:
|
|
144
|
+
raise SystemExit("Failed to read device info -- is this a PMD-USB device?")
|
|
145
|
+
print_pmdusb_device_info(device_info)
|
|
146
|
+
|
|
147
|
+
sensors = read_pmdusb_sensors(ser)
|
|
148
|
+
if sensors is None:
|
|
149
|
+
raise SystemExit("Failed to read sensor values.")
|
|
150
|
+
|
|
151
|
+
print_rail_readings(translate_pmdusb_sensors(sensors))
|
|
152
|
+
|
|
153
|
+
aggregates = translate_pmdusb_aggregate_power(sensors)
|
|
154
|
+
print("--- Aggregate power ---")
|
|
155
|
+
print(f'GPU (PCIE1+2): {aggregates["gpu"]:>8.3f} W')
|
|
156
|
+
print(f'CPU (EPS1+2): {aggregates["cpu"]:>8.3f} W')
|
|
157
|
+
print(f'Total: {aggregates["total"]:>8.3f} W')
|
|
158
|
+
finally:
|
|
159
|
+
ser.close()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# --- Entry point ---
|
|
163
|
+
|
|
164
|
+
def main() -> None:
|
|
165
|
+
pmd_ports = get_pmd2_ports()
|
|
166
|
+
if pmd_ports:
|
|
167
|
+
if len(pmd_ports) > 1:
|
|
168
|
+
print(f"Found {len(pmd_ports)} PMD2/PMD2X devices, using the first: {pmd_ports[0]['port']}")
|
|
169
|
+
port = pmd_ports[0]["port"]
|
|
170
|
+
print(f"Connecting to {port} (PMD2/PMD2X)...")
|
|
171
|
+
run_pmd(port)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
pmdusb_ports = get_pmdusb_ports()
|
|
175
|
+
if pmdusb_ports:
|
|
176
|
+
if len(pmdusb_ports) > 1:
|
|
177
|
+
print(f"Found {len(pmdusb_ports)} PMD-USB devices, using the first: {pmdusb_ports[0]['port']}")
|
|
178
|
+
port = pmdusb_ports[0]["port"]
|
|
179
|
+
print(f"Connecting to {port} (PMD-USB)...")
|
|
180
|
+
run_pmdusb(port)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
raise SystemExit("No PMD2/PMD2X or PMD-USB device found.")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
try:
|
|
188
|
+
main()
|
|
189
|
+
except KeyboardInterrupt:
|
|
190
|
+
print("\nStopped.")
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connect to the first supported device found -- trying PMD2/PMD2X first, then
|
|
3
|
+
PMD-USB -- and print averaged readings in a loop, refreshing in place.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import statistics
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import serial
|
|
10
|
+
|
|
11
|
+
from pmd_pycore.core import (
|
|
12
|
+
get_pmd2_ports,
|
|
13
|
+
read_pmd2_device,
|
|
14
|
+
read_pmd2_sensors,
|
|
15
|
+
translate_pmd2_sensor_struct,
|
|
16
|
+
translate_pmd2_aggregate_power,
|
|
17
|
+
translate_pmd2_chip_sensors,
|
|
18
|
+
translate_pmd2x_hpwr_temperatures,
|
|
19
|
+
PMD2X_PRODUCT_ID,
|
|
20
|
+
get_pmdusb_ports,
|
|
21
|
+
read_pmdusb_id,
|
|
22
|
+
read_pmdusb_sensors,
|
|
23
|
+
translate_pmdusb_sensors,
|
|
24
|
+
translate_pmdusb_aggregate_power,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Number of sensor reads averaged into each displayed update.
|
|
28
|
+
NUM_SAMPLES = 20
|
|
29
|
+
|
|
30
|
+
# Target time between displayed updates. On PMD2/PMD2X, read_pmd2_uart()'s 5ms
|
|
31
|
+
# inter-command delay dominates per-sample cost, so NUM_SAMPLES * 5ms
|
|
32
|
+
# (~100ms) alone is already close to this budget -- actual cycles land
|
|
33
|
+
# around 110-125ms there, not exactly 100ms. PMD-USB's 2ms delay leaves more
|
|
34
|
+
# headroom. Either way this is a target to approach, not a hard cap.
|
|
35
|
+
UPDATE_INTERVAL_S = 0.1
|
|
36
|
+
|
|
37
|
+
CLEAR_SCREEN = "\x1b[2J\x1b[H"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _average_dicts(dicts, fields):
|
|
41
|
+
return {field: statistics.mean(d[field] for d in dicts) for field in fields}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _rail_lines(averaged_rails):
|
|
45
|
+
lines = [f'{"Rail":<10}{"Voltage":>10}{"Current":>10}{"Power":>10}']
|
|
46
|
+
for rail, reading in averaged_rails.items():
|
|
47
|
+
lines.append(
|
|
48
|
+
f'{rail:<10}{reading["voltage"]:>10.3f}'
|
|
49
|
+
f'{reading["current"]:>10.3f}{reading["power"]:>10.3f}'
|
|
50
|
+
)
|
|
51
|
+
lines.append("")
|
|
52
|
+
return lines
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- PMD2 / PMD2X ---
|
|
56
|
+
|
|
57
|
+
def run_pmd(port):
|
|
58
|
+
try:
|
|
59
|
+
ser = serial.Serial(port, 115200, timeout=1, write_timeout=1, rtscts=False, dsrdtr=False)
|
|
60
|
+
except serial.SerialException as e:
|
|
61
|
+
print(f"Failed to open {port}: {e}")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
ser.dtr = True
|
|
66
|
+
# The device pushes an unsolicited welcome string on DTR assert, but it
|
|
67
|
+
# can arrive after this point rather than before it. Give it a moment to
|
|
68
|
+
# land, then flush -- read_pmd2_uart() also flushes before each command, so a
|
|
69
|
+
# late push can never corrupt the next real response.
|
|
70
|
+
time.sleep(0.1)
|
|
71
|
+
ser.reset_input_buffer()
|
|
72
|
+
|
|
73
|
+
print("Reading vendor data...")
|
|
74
|
+
device_info = read_pmd2_device(ser)
|
|
75
|
+
if device_info is None:
|
|
76
|
+
print("Failed to read vendor data from device.")
|
|
77
|
+
return
|
|
78
|
+
print(f"Result: {device_info}\n")
|
|
79
|
+
time.sleep(1)
|
|
80
|
+
|
|
81
|
+
product_id = device_info["ProductId"]
|
|
82
|
+
is_pmd2x = product_id == PMD2X_PRODUCT_ID
|
|
83
|
+
|
|
84
|
+
while True:
|
|
85
|
+
cycle_start = time.monotonic()
|
|
86
|
+
rail_samples = []
|
|
87
|
+
aggregate_samples = []
|
|
88
|
+
chip_samples = []
|
|
89
|
+
hpwr_temp_samples = []
|
|
90
|
+
|
|
91
|
+
for _ in range(NUM_SAMPLES):
|
|
92
|
+
sensors = None
|
|
93
|
+
while sensors is None:
|
|
94
|
+
try:
|
|
95
|
+
sensors = read_pmd2_sensors(ser, product_id)
|
|
96
|
+
except serial.SerialException:
|
|
97
|
+
time.sleep(0.1)
|
|
98
|
+
rail_samples.append(translate_pmd2_sensor_struct(sensors, product_id))
|
|
99
|
+
aggregate_samples.append(translate_pmd2_aggregate_power(sensors))
|
|
100
|
+
chip_samples.append(translate_pmd2_chip_sensors(sensors))
|
|
101
|
+
if is_pmd2x:
|
|
102
|
+
hpwr_temp_samples.append(translate_pmd2x_hpwr_temperatures(sensors))
|
|
103
|
+
|
|
104
|
+
rail_names = rail_samples[0].keys()
|
|
105
|
+
averaged_rails = {
|
|
106
|
+
rail: _average_dicts([s[rail] for s in rail_samples], ("voltage", "current", "power"))
|
|
107
|
+
for rail in rail_names
|
|
108
|
+
}
|
|
109
|
+
averaged_aggregates = _average_dicts(aggregate_samples, ("eps", "pcie", "mb", "total"))
|
|
110
|
+
averaged_chip = _average_dicts(chip_samples, ("vdd", "tchip"))
|
|
111
|
+
|
|
112
|
+
lines = _rail_lines(averaged_rails)
|
|
113
|
+
|
|
114
|
+
lines.append("--- Aggregate power ---")
|
|
115
|
+
lines.append(f'EPS (CPU): {averaged_aggregates["eps"]:>8.3f} W')
|
|
116
|
+
lines.append(f'PCIe (GPU): {averaged_aggregates["pcie"]:>8.3f} W')
|
|
117
|
+
lines.append(f'Motherboard: {averaged_aggregates["mb"]:>8.3f} W')
|
|
118
|
+
lines.append(f'Total: {averaged_aggregates["total"]:>8.3f} W')
|
|
119
|
+
lines.append("")
|
|
120
|
+
|
|
121
|
+
lines.append("--- Device health ---")
|
|
122
|
+
lines.append(f'{"Supply (Vdd):":<17}{averaged_chip["vdd"]:>8.3f} V')
|
|
123
|
+
lines.append(f'{"Chip temp:":<17}{averaged_chip["tchip"]:>8.1f} C')
|
|
124
|
+
if is_pmd2x:
|
|
125
|
+
def fmt_temp(samples, key):
|
|
126
|
+
values = [s[key] for s in samples if s[key] is not None]
|
|
127
|
+
return f"{statistics.mean(values):>8.1f} C" if values else f"{'n/c':>8} C"
|
|
128
|
+
|
|
129
|
+
lines.append(f'{"12VHPWR in:":<17}{fmt_temp(hpwr_temp_samples, "ts_in")}')
|
|
130
|
+
lines.append(f'{"12VHPWR out:":<17}{fmt_temp(hpwr_temp_samples, "ts_out")}')
|
|
131
|
+
|
|
132
|
+
print(CLEAR_SCREEN + "\n".join(lines), flush=True)
|
|
133
|
+
|
|
134
|
+
elapsed = time.monotonic() - cycle_start
|
|
135
|
+
time.sleep(max(0.0, UPDATE_INTERVAL_S - elapsed))
|
|
136
|
+
finally:
|
|
137
|
+
ser.close()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- PMD-USB ---
|
|
141
|
+
|
|
142
|
+
def run_pmdusb(port):
|
|
143
|
+
try:
|
|
144
|
+
ser = serial.Serial(port, 115200, timeout=1, write_timeout=1)
|
|
145
|
+
except serial.SerialException as e:
|
|
146
|
+
print(f"Failed to open {port}: {e}")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
print("Reading device id...")
|
|
151
|
+
device_info = read_pmdusb_id(ser)
|
|
152
|
+
if device_info is None:
|
|
153
|
+
print("Failed to read device id.")
|
|
154
|
+
return
|
|
155
|
+
print(f"Result: {device_info}\n")
|
|
156
|
+
time.sleep(1)
|
|
157
|
+
|
|
158
|
+
while True:
|
|
159
|
+
cycle_start = time.monotonic()
|
|
160
|
+
channel_samples = []
|
|
161
|
+
aggregate_samples = []
|
|
162
|
+
|
|
163
|
+
for _ in range(NUM_SAMPLES):
|
|
164
|
+
sensors = None
|
|
165
|
+
while sensors is None:
|
|
166
|
+
try:
|
|
167
|
+
sensors = read_pmdusb_sensors(ser)
|
|
168
|
+
except serial.SerialException:
|
|
169
|
+
time.sleep(0.1)
|
|
170
|
+
channel_samples.append(translate_pmdusb_sensors(sensors))
|
|
171
|
+
aggregate_samples.append(translate_pmdusb_aggregate_power(sensors))
|
|
172
|
+
|
|
173
|
+
channel_names = channel_samples[0].keys()
|
|
174
|
+
averaged_channels = {
|
|
175
|
+
channel: _average_dicts(
|
|
176
|
+
[s[channel] for s in channel_samples], ("voltage", "current", "power")
|
|
177
|
+
)
|
|
178
|
+
for channel in channel_names
|
|
179
|
+
}
|
|
180
|
+
averaged_aggregates = _average_dicts(aggregate_samples, ("gpu", "cpu", "total"))
|
|
181
|
+
|
|
182
|
+
lines = _rail_lines(averaged_channels)
|
|
183
|
+
lines.append("--- Aggregate power ---")
|
|
184
|
+
lines.append(f'GPU (PCIE1+2): {averaged_aggregates["gpu"]:>8.3f} W')
|
|
185
|
+
lines.append(f'CPU (EPS1+2): {averaged_aggregates["cpu"]:>8.3f} W')
|
|
186
|
+
lines.append(f'Total: {averaged_aggregates["total"]:>8.3f} W')
|
|
187
|
+
print(CLEAR_SCREEN + "\n".join(lines), flush=True)
|
|
188
|
+
|
|
189
|
+
elapsed = time.monotonic() - cycle_start
|
|
190
|
+
time.sleep(max(0.0, UPDATE_INTERVAL_S - elapsed))
|
|
191
|
+
finally:
|
|
192
|
+
ser.close()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# --- Entry point ---
|
|
196
|
+
|
|
197
|
+
def main():
|
|
198
|
+
print("\nFinding potential PMD2/PMD2X Serial Ports...\n")
|
|
199
|
+
pmd_ports = get_pmd2_ports()
|
|
200
|
+
for i, p in enumerate(pmd_ports):
|
|
201
|
+
print(f"{i} - {p['port']}")
|
|
202
|
+
print()
|
|
203
|
+
|
|
204
|
+
if pmd_ports:
|
|
205
|
+
port_index = 0
|
|
206
|
+
if len(pmd_ports) > 1:
|
|
207
|
+
print("Multiple PMD2/PMD2X serial ports found. Input which port to use:")
|
|
208
|
+
port_index = int(input())
|
|
209
|
+
if port_index >= len(pmd_ports):
|
|
210
|
+
print("Invalid port index.")
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
port = pmd_ports[port_index]["port"]
|
|
214
|
+
print(f"Using port index {port_index} ({port})\n")
|
|
215
|
+
run_pmd(port)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
print("No PMD2/PMD2X serial ports found. Checking for PMD-USB...\n")
|
|
219
|
+
pmdusb_ports = get_pmdusb_ports()
|
|
220
|
+
if not pmdusb_ports:
|
|
221
|
+
print("No PMD2/PMD2X or PMD-USB serial ports found.")
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
port_index = 0
|
|
225
|
+
if len(pmdusb_ports) > 1:
|
|
226
|
+
print("Multiple PMD-USB serial ports found. Input which port to use:")
|
|
227
|
+
port_index = int(input())
|
|
228
|
+
if port_index >= len(pmdusb_ports):
|
|
229
|
+
print("Invalid port index.")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
port = pmdusb_ports[port_index]["port"]
|
|
233
|
+
print(f"Using port index {port_index} ({port})\n")
|
|
234
|
+
run_pmdusb(port)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
try:
|
|
239
|
+
main()
|
|
240
|
+
except KeyboardInterrupt:
|
|
241
|
+
print("\nStopped.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pmd-pycore
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: PMD PyCore - Python Interface for ElmorLabs PMD2/PMD2X/PMD-USB
|
|
5
5
|
Home-page: https://github.com/ElmorLabs-P1/pmd-pycore
|
|
6
6
|
Author: Pieter Plaisier
|
|
@@ -43,11 +43,20 @@ motherboard power cables, with an OLED display and USB connection to a PC for lo
|
|
|
43
43
|
|
|
44
44
|
## Install
|
|
45
45
|
|
|
46
|
+
```
|
|
47
|
+
pip install pmd-pycore
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or from a checkout:
|
|
51
|
+
|
|
46
52
|
```
|
|
47
53
|
pip install -r requirements.txt
|
|
48
54
|
pip install -e .
|
|
49
55
|
```
|
|
50
56
|
|
|
57
|
+
This also installs the example scripts (see [Examples](#examples) below) as part of the package,
|
|
58
|
+
runnable with `python -m pmd_pycore.examples.quickstart`.
|
|
59
|
+
|
|
51
60
|
## Usage
|
|
52
61
|
|
|
53
62
|
### PMD2 / PMD2X
|
|
@@ -78,7 +87,8 @@ Currently implemented are the device's read-only commands:
|
|
|
78
87
|
Writing/action commands (continuous TX, config read/write, reset, bootloader) are not
|
|
79
88
|
implemented.
|
|
80
89
|
|
|
81
|
-
See `examples
|
|
90
|
+
See `python -m pmd_pycore.examples.read_sensors` (below) — a polling/averaging loop that mirrors
|
|
91
|
+
the original sample script.
|
|
82
92
|
|
|
83
93
|
### PMD-USB
|
|
84
94
|
|
|
@@ -117,12 +127,16 @@ See [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) for what's deliberately out of
|
|
|
117
127
|
|
|
118
128
|
## Examples
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
Both examples ship as part of the installed package -- run them with `python -m` after
|
|
131
|
+
`pip install pmd-pycore`, no checkout needed.
|
|
132
|
+
|
|
133
|
+
- `python -m pmd_pycore.examples.quickstart` — connect to the first device found, trying
|
|
134
|
+
PMD2/PMD2X first and falling back to PMD-USB, print its identity, and read sensor values once
|
|
135
|
+
(for PMD2/PMD2X: per-rail readings, firmware-computed aggregate power, chip health, and
|
|
136
|
+
PMD2X's 12VHPWR connector temperatures).
|
|
137
|
+
- `python -m pmd_pycore.examples.read_sensors` — a polling/averaging loop, refreshing in place,
|
|
138
|
+
for whichever device family is connected (same PMD2/PMD2X-first-then-PMD-USB detection as
|
|
139
|
+
`quickstart`).
|
|
126
140
|
|
|
127
141
|
## Status
|
|
128
142
|
|
|
@@ -11,5 +11,8 @@ pmd_pycore.egg-info/top_level.txt
|
|
|
11
11
|
pmd_pycore/core/__init__.py
|
|
12
12
|
pmd_pycore/core/pmd2.py
|
|
13
13
|
pmd_pycore/core/pmdusb.py
|
|
14
|
+
pmd_pycore/examples/__init__.py
|
|
15
|
+
pmd_pycore/examples/quickstart.py
|
|
16
|
+
pmd_pycore/examples/read_sensors.py
|
|
14
17
|
tests/test_pmd2.py
|
|
15
18
|
tests/test_pmdusb.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|