Skip to content

Commit 8dfb156

Browse files
dccoteclaude
andcommitted
Discover the Millennia eV port by its STM32 USB-CDC VID/PID
Problem: MillenniaEv25Device could only be constructed with an explicit portPath; doInitializeDevice opened SerialPort(portPath=...) and never fell back to VID/PID discovery, so a caller (e.g. the MillenniaUI app) had to know the OS port name up front. The class did not set classIdVendor/classIdProduct, so nothing identified the device by its USB descriptor. Solution: Set classIdVendor = 0x0483 / classIdProduct = 0x5740 (the STM32 Virtual COM Port identity the lab eV25s enumerates as) and have doInitializeDevice discover the port by that identity when no portPath is given, mirroring FieldMasterDevice: it builds SerialPort(idVendor, idProduct, serialNumber) -> matchAnyPort over pyserial's comports(), and raises a clear UnableToInitialize naming the identity when nothing matches. An explicit portPath still wins; a serialNumber narrows discovery when several STM32 CDC ports are present. serialNumber is passed only when it actually narrows, so matchPorts stays on its vid/pid path and never regex-matches a possibly-None descriptor serial. The generic-STM32-VCP caveat is documented in the code. DebugMillenniaEv25Device is unaffected: it overrides doInitializeDevice and keeps its own fake identity. testMillennia.py passes (14 passed, 6 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d4c4a5b commit 8dfb156

1 file changed

Lines changed: 28 additions & 9 deletions

File tree

hardwarelibrary/sources/millennia.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,24 @@ class MillenniaEv25Device(LaserSourceDevice, OnOffControl, ShutterControl, Power
3131

3232
# The lab eV25s enumerates its back-panel USB port as a native STM32
3333
# USB-CDC device: VID 0x0483, PID 0x5740 (confirmed on the bench, firmware
34-
# SW214-00.004.096). MillenniaEv25Device is still instantiated by portPath
35-
# (like CoboltDevice) rather than discovered by VID/PID, because 0x0483:0x5740
36-
# is STMicroelectronics' *generic* STM32 Virtual COM Port identity, shared by
37-
# many unrelated STM32-based USB-CDC boards; matching on it via
38-
# SerialPort.matchAnyPort would risk binding to the wrong device. If a host
39-
# only ever has this one STM32 CDC port, set classIdVendor = 0x0483 and
40-
# classIdProduct = 0x5740 to enable discovery; otherwise pin the portPath.
41-
# To re-derive on another unit:
34+
# SW214-00.004.096). doInitializeDevice discovers the port by this identity
35+
# when no portPath is given.
36+
#
37+
# Caveat: 0x0483:0x5740 is STMicroelectronics' *generic* STM32 Virtual COM
38+
# Port identity, shared by many unrelated STM32-based USB-CDC boards. On a
39+
# host that also has another STM32 CDC device, discovery could bind to the
40+
# wrong port; pin it with an explicit portPath (or narrow with a
41+
# serialNumber) in that case. A given portPath always wins over discovery.
42+
# To re-derive the identity on another unit:
4243
#
4344
# system_profiler SPUSBDataType | grep -A 12 -i millennia
4445
# .venv/bin/python -c "from serial.tools.list_ports import comports; \
4546
# [print(p.device, hex(p.vid or 0), hex(p.pid or 0), p.serial_number) \
4647
# for p in comports()]"
4748

49+
classIdVendor = 0x0483 # STMicroelectronics
50+
classIdProduct = 0x5740 # STM32 Virtual COM Port (generic STM32 CDC identity)
51+
4852
defaultBaudRate = 115200
4953
commandTerminator = "\r" # the eV accepts <CR>, <LF>, or both
5054
minPower = 0.05 # documented eV/Pro-s lower bound
@@ -76,8 +80,23 @@ def __init__(self, portPath=None, serialNumber=None, idProduct=None, idVendor=No
7680
self.laserSerialNumber = None
7781

7882
def doInitializeDevice(self):
79-
try:
83+
if self.portPath is not None:
8084
self.port = SerialPort(portPath=self.portPath)
85+
else:
86+
# Discover by the STM32 USB-CDC identity. serialNumber is a regex
87+
# (PhysicalDevice turns the default into ".*"); pass it only when it
88+
# actually narrows, so SerialPort.matchPorts stays on its vid/pid
89+
# path and never regex-matches against a possibly-None descriptor
90+
# serial.
91+
serialNumber = None if self.serialNumber in (None, ".*") else self.serialNumber
92+
self.port = SerialPort(idVendor=self.idVendor, idProduct=self.idProduct,
93+
serialNumber=serialNumber)
94+
if self.port.portPath is None:
95+
raise PhysicalDevice.UnableToInitialize(
96+
"No Millennia eV found by USB identity {0:#06x}:{1:#06x}. "
97+
"Pass an explicit portPath if several STM32 USB-CDC ports "
98+
"are present.".format(self.idVendor, self.idProduct))
99+
try:
81100
self.port.open(baudRate=self.defaultBaudRate)
82101
self.readIdentity()
83102
except Exception as error:

0 commit comments

Comments
 (0)