2323logger = logging .getLogger (__name__ )
2424
2525
26+ def detect ():
27+ """Detect connected PSLab devices.
28+
29+ Returns
30+ -------
31+ devices : dict of str: str
32+ Dictionary containing port name as keys and device version on that
33+ port as values.
34+ """
35+ regex = []
36+
37+ for vid , pid in zip (SerialHandler ._USB_VID , SerialHandler ._USB_PID ):
38+ regex .append (f"{ vid :04x} :{ pid :04x} " )
39+
40+ regex = "(" + "|" .join (regex ) + ")"
41+ port_info_generator = list_ports .grep (regex )
42+ pslab_devices = {}
43+
44+ for port_info in port_info_generator :
45+ version = _get_version (port_info .device )
46+ if any (expected in version for expected in ["PSLab" , "CSpark" ]):
47+ pslab_devices [port_info .device ] = version
48+
49+ return pslab_devices
50+
51+
52+ def _get_version (port : str ) -> str :
53+ interface = serial .Serial (port = port , baudrate = 1e6 , timeout = 1 )
54+ interface .write (CP .COMMON )
55+ interface .write (CP .GET_VERSION )
56+ version = interface .readline ()
57+ return version .decode ("utf-8" )
58+
59+
2660class SerialHandler :
2761 """Provides methods for communicating with the PSLab hardware.
2862
@@ -98,9 +132,11 @@ def connect(
98132 Parameters
99133 ----------
100134 port : str, optional
101- The name of the port to which the PSLab is connected as a string. On
102- Posix this is a path, e.g. "/dev/ttyACM0". On Windows, it's a numbered
103- COM port, e.g. "COM5". Will be autodetected if not specified.
135+ The name of the port to which the PSLab is connected as a string.
136+ On Posix this is a path, e.g. "/dev/ttyACM0". On Windows, it's a
137+ numbered COM port, e.g. "COM5". Will be autodetected if not
138+ specified. If multiple PSLab devices are connected, port must be
139+ specified.
104140 baudrate : int, optional
105141 Symbol rate in bit/s. The default value is 1000000.
106142 timeout : float, optional
@@ -111,6 +147,8 @@ def connect(
111147 ------
112148 SerialException
113149 If connection could not be established.
150+ RuntimeError
151+ If ultiple devices are connected and no port was specified.
114152 """
115153 # serial.Serial opens automatically if port is not None.
116154 self .interface = serial .Serial (
@@ -119,28 +157,31 @@ def connect(
119157 timeout = timeout ,
120158 write_timeout = timeout ,
121159 )
160+ pslab_devices = detect ()
122161
123162 if self .interface .is_open :
124163 # User specified a port.
125164 version = self .get_version ()
126165 else :
127- regex = []
128- for vid , pid in zip (self ._USB_VID , self ._USB_PID ):
129- regex .append (f"{ vid :04x} :{ pid :04x} " )
130-
131- regex = "(" + "|" .join (regex ) + ")"
132- port_info_generator = list_ports .grep (regex )
133-
134- for port_info in port_info_generator :
135- self .interface .port = port_info .device
166+ if len (pslab_devices ) == 1 :
167+ self .interface .port = list (pslab_devices .keys ())[0 ]
136168 self .interface .open ()
137169 version = self .get_version ()
138- if any (expected in version for expected in ["PSLab" , "CSpark" ]):
139- break
170+ elif len (pslab_devices ) > 1 :
171+ found = ""
172+
173+ for port , version in pslab_devices .items ():
174+ found += f"{ port } : { version } "
175+
176+ raise RuntimeError (
177+ "Multiple PSLab devices found:\n "
178+ f"{ found } "
179+ "Please choose a device by specifying a port."
180+ )
140181 else :
141182 version = ""
142183
143- if any ( expected in version for expected in [ "PSLab" , "CSpark" ]) :
184+ if self . interface . port in pslab_devices :
144185 self .version = version
145186 logger .info (f"Connected to { self .version } on { self .interface .port } ." )
146187 else :
@@ -174,13 +215,11 @@ def reconnect(
174215 port = self .interface .port if port is None else port
175216 timeout = self .interface .timeout if timeout is None else timeout
176217
177- self .interface = serial . Serial (
218+ self .connect (
178219 port = port ,
179220 baudrate = baudrate ,
180221 timeout = timeout ,
181- write_timeout = timeout ,
182222 )
183- self .connect ()
184223
185224 def get_version (self ) -> str :
186225 """Query PSLab for its version and return it as a decoded string.
0 commit comments