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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pmd-pycore
3
- Version: 0.2.3
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/read_sensors.py` — a polling/averaging loop that mirrors the original sample script.
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
- - `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`).
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/read_sensors.py` — a polling/averaging loop that mirrors the original sample script.
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
- - `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`).
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.")
@@ -2,4 +2,4 @@
2
2
  Version information for pmd-pycore
3
3
  """
4
4
 
5
- __version__ = "0.2.3"
5
+ __version__ = "0.2.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pmd-pycore
3
- Version: 0.2.3
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/read_sensors.py` — a polling/averaging loop that mirrors the original sample script.
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
- - `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`).
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pmd-pycore"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "PMD PyCore - Python Interface for ElmorLabs PMD2/PMD2X/PMD-USB"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = pmd-pycore
3
- version = 0.2.3
3
+ version = 0.2.4
4
4
  author = Pieter Plaisier
5
5
  author_email = pieter@hanweiconsulting.com
6
6
  description = PMD PyCore - Python Interface for ElmorLabs PMD2/PMD2X/PMD-USB