Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2ae3f09

Browse files
committedFeb 19, 2025
Refactor SerialHandler
1 parent bd11b73 commit 2ae3f09

23 files changed

+639
-131
lines changed
 

‎pslab/bus/busio.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from pslab.bus.i2c import _I2CPrimitive
3737
from pslab.bus.spi import _SPIPrimitive
3838
from pslab.bus.uart import _UARTPrimitive
39-
from pslab.serial_handler import SerialHandler
39+
from pslab.connection import ConnectionHandler
4040

4141
__all__ = (
4242
"I2C",
@@ -59,7 +59,12 @@ class I2C(_I2CPrimitive):
5959
Frequency of SCL in Hz.
6060
"""
6161

62-
def __init__(self, device: SerialHandler = None, *, frequency: int = 125e3):
62+
def __init__(
63+
self,
64+
device: ConnectionHandler | None = None,
65+
*,
66+
frequency: int = 125e3,
67+
):
6368
# 125 kHz is as low as the PSLab can go.
6469
super().__init__(device)
6570
self._init()
@@ -199,7 +204,7 @@ class SPI(_SPIPrimitive):
199204
created.
200205
"""
201206

202-
def __init__(self, device: SerialHandler = None):
207+
def __init__(self, device: ConnectionHandler | None = None):
203208
super().__init__(device)
204209
ppre, spre = self._get_prescaler(25e4)
205210
self._set_parameters(ppre, spre, 1, 0, 1)
@@ -412,7 +417,7 @@ class UART(_UARTPrimitive):
412417

413418
def __init__(
414419
self,
415-
device: SerialHandler = None,
420+
device: ConnectionHandler | None = None,
416421
*,
417422
baudrate: int = 9600,
418423
bits: int = 8,

‎pslab/bus/i2c.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from typing import List
2323

2424
import pslab.protocol as CP
25-
from pslab.serial_handler import SerialHandler
25+
from pslab.connection import ConnectionHandler, autoconnect
2626
from pslab.external.sensorlist import sensors
2727

2828
__all__ = (
@@ -54,8 +54,8 @@ class _I2CPrimitive:
5454
_READ = 1
5555
_WRITE = 0
5656

57-
def __init__(self, device: SerialHandler = None):
58-
self._device = device if device is not None else SerialHandler()
57+
def __init__(self, device: ConnectionHandler | None = None):
58+
self._device = device if device is not None else autoconnect()
5959
self._running = False
6060
self._mode = None
6161

@@ -447,7 +447,7 @@ class I2CMaster(_I2CPrimitive):
447447
created.
448448
"""
449449

450-
def __init__(self, device: SerialHandler = None):
450+
def __init__(self, device: ConnectionHandler | None = None):
451451
super().__init__(device)
452452
self._init()
453453
self.configure(125e3) # 125 kHz is as low as the PSLab can go.
@@ -506,7 +506,7 @@ class I2CSlave(_I2CPrimitive):
506506
def __init__(
507507
self,
508508
address: int,
509-
device: SerialHandler = None,
509+
device: ConnectionHandler | None = None,
510510
):
511511
super().__init__(device)
512512
self.address = address

‎pslab/bus/spi.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import pslab.protocol as CP
2525
from pslab.bus import classmethod_
26-
from pslab.serial_handler import SerialHandler
26+
from pslab.connection import ConnectionHandler, autoconnect
2727

2828
__all__ = (
2929
"SPIMaster",
@@ -67,8 +67,8 @@ class _SPIPrimitive:
6767
_clock_edge = _CKE # Clock Edge Select bit (inverse of Clock Phase bit).
6868
_smp = _SMP # Data Input Sample Phase bit.
6969

70-
def __init__(self, device: SerialHandler = None):
71-
self._device = device if device is not None else SerialHandler()
70+
def __init__(self, device: ConnectionHandler | None = None):
71+
self._device = device if device is not None else autoconnect()
7272

7373
@classmethod_
7474
@property
@@ -419,7 +419,7 @@ class SPIMaster(_SPIPrimitive):
419419
created.
420420
"""
421421

422-
def __init__(self, device: SerialHandler = None):
422+
def __init__(self, device: ConnectionHandler | None = None):
423423
super().__init__(device)
424424
# Reset config
425425
self.set_parameters()
@@ -492,7 +492,7 @@ class SPISlave(_SPIPrimitive):
492492
created.
493493
"""
494494

495-
def __init__(self, device: SerialHandler = None):
495+
def __init__(self, device: ConnectionHandler | None = None):
496496
super().__init__(device)
497497

498498
def transfer8(self, data: int) -> int:

‎pslab/bus/uart.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import pslab.protocol as CP
1919
from pslab.bus import classmethod_
20-
from pslab.serial_handler import SerialHandler
20+
from pslab.connection import ConnectionHandler, autoconnect
2121

2222
__all__ = "UART"
2323
_BRGVAL = 0x22 # BaudRate = 460800.
@@ -41,8 +41,8 @@ class _UARTPrimitive:
4141
_brgval = _BRGVAL
4242
_mode = _MODE
4343

44-
def __init__(self, device: SerialHandler = None):
45-
self._device = device if device is not None else SerialHandler()
44+
def __init__(self, device: ConnectionHandler | None = None):
45+
self._device = device if device is not None else autoconnect()
4646

4747
@classmethod_
4848
@property
@@ -227,7 +227,7 @@ class UART(_UARTPrimitive):
227227
Serial connection to PSLab device. If not provided, a new one will be created.
228228
"""
229229

230-
def __init__(self, device: SerialHandler = None):
230+
def __init__(self, device: ConnectionHandler | None = None):
231231
super().__init__(device)
232232
# Reset baudrate and mode
233233
self.configure(self._get_uart_baudrate(_BRGVAL))

‎pslab/connection/__init__.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Interfaces for communicating with PSLab devices."""
2+
3+
from serial.tools import list_ports
4+
5+
from .connection import ConnectionHandler
6+
from ._serial import SerialHandler
7+
8+
9+
def detect() -> list[ConnectionHandler]:
10+
"""Detect PSLab devices.
11+
12+
Returns
13+
-------
14+
devices : list[ConnectionHandler]
15+
Handlers for all detected PSLabs. The returned handlers are disconnected; call
16+
.connect() before use.
17+
"""
18+
regex = []
19+
20+
for vid, pid in zip(SerialHandler._USB_VID, SerialHandler._USB_PID):
21+
regex.append(f"{vid:04x}:{pid:04x}")
22+
23+
regex = "(" + "|".join(regex) + ")"
24+
port_info_generator = list_ports.grep(regex)
25+
pslab_devices = []
26+
27+
for port_info in port_info_generator:
28+
device = SerialHandler(port=port_info.device, baudrate=1000000, timeout=1)
29+
30+
try:
31+
device.connect()
32+
except Exception:
33+
pass # nosec
34+
else:
35+
pslab_devices.append(device)
36+
finally:
37+
device.disconnect()
38+
39+
return pslab_devices
40+
41+
42+
def autoconnect() -> ConnectionHandler:
43+
"""Automatically connect when exactly one device is present.
44+
45+
Returns
46+
-------
47+
device : ConnectionHandler
48+
A handler connected to the detected PSLab device. The handler is connected; it
49+
is not necessary to call .connect before use().
50+
"""
51+
devices = detect()
52+
53+
if not devices:
54+
msg = "device not found"
55+
raise ConnectionError(msg)
56+
57+
if len(devices) > 1:
58+
msg = f"autoconnect failed, multiple devices detected: {devices}"
59+
raise ConnectionError(msg)
60+
61+
device = devices[0]
62+
device.connect()
63+
return device

‎pslab/connection/_serial.py

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""Serial interface for communicating with PSLab devices."""
2+
3+
import os
4+
import platform
5+
6+
import serial
7+
8+
import pslab
9+
from pslab.connection.connection import ConnectionHandler
10+
11+
12+
def _check_serial_access_permission():
13+
"""Check that we have permission to use the tty on Linux."""
14+
if platform.system() == "Linux":
15+
import grp
16+
17+
if os.geteuid() == 0: # Running as root?
18+
return
19+
20+
for group in os.getgroups():
21+
if grp.getgrgid(group).gr_name in (
22+
"dialout",
23+
"uucp",
24+
):
25+
return
26+
27+
udev_paths = [
28+
"/run/udev/rules.d/",
29+
"/etc/udev/rules.d/",
30+
"/lib/udev/rules.d/",
31+
]
32+
for p in udev_paths:
33+
udev_rules = os.path.join(p, "99-pslab.rules")
34+
if os.path.isfile(udev_rules):
35+
return
36+
else:
37+
raise PermissionError(
38+
"The current user does not have permission to access "
39+
"the PSLab device. To solve this, either:"
40+
"\n\n"
41+
"1. Add the user to the 'dialout' (on Debian-based "
42+
"systems) or 'uucp' (on Arch-based systems) group."
43+
"\n"
44+
"2. Install a udev rule to allow any user access to the "
45+
"device by running 'pslab install' as root, or by "
46+
"manually copying "
47+
f"{pslab.__path__[0]}/99-pslab.rules into {udev_paths[1]}."
48+
"\n\n"
49+
"You may also need to reboot the system for the "
50+
"permission changes to take effect."
51+
)
52+
53+
54+
class SerialHandler(ConnectionHandler):
55+
"""Interface for controlling a PSLab over a serial port.
56+
57+
Parameters
58+
----------
59+
port : str
60+
baudrate : int, default 1 MBd
61+
timeout : float, default 1 s
62+
"""
63+
64+
# V5 V6
65+
_USB_VID = [0x04D8, 0x10C4]
66+
_USB_PID = [0x00DF, 0xEA60]
67+
68+
def __init__(
69+
self,
70+
port: str,
71+
baudrate: int = 1000000,
72+
timeout: float = 1.0,
73+
):
74+
self._port = port
75+
self._ser = serial.Serial(
76+
baudrate=baudrate,
77+
timeout=timeout,
78+
write_timeout=timeout,
79+
)
80+
_check_serial_access_permission()
81+
82+
@property
83+
def port(self) -> str:
84+
"""Serial port."""
85+
return self._port
86+
87+
@property
88+
def baudrate(self) -> int:
89+
"""Symbol rate."""
90+
return self._ser.baudrate
91+
92+
@baudrate.setter
93+
def baudrate(self, value: int) -> None:
94+
self._ser.baudrate = value
95+
96+
@property
97+
def timeout(self) -> float:
98+
"""Timeout in seconds."""
99+
return self._ser.timeout
100+
101+
@timeout.setter
102+
def timeout(self, value: float) -> None:
103+
self._ser.timeout = value
104+
self._ser.write_timeout = value
105+
106+
def connect(self) -> None:
107+
"""Connect to PSLab."""
108+
self._ser.port = self.port
109+
self._ser.open()
110+
111+
try:
112+
self.get_version()
113+
except Exception:
114+
self._ser.close()
115+
raise
116+
117+
def disconnect(self):
118+
"""Disconnect from PSLab."""
119+
self._ser.close()
120+
121+
def read(self, number_of_bytes: int) -> bytes:
122+
"""Read bytes from serial port.
123+
124+
Parameters
125+
----------
126+
number_of_bytes : int
127+
Number of bytes to read from the serial port.
128+
129+
Returns
130+
-------
131+
bytes
132+
Bytes read from the serial port.
133+
"""
134+
return self._ser.read(number_of_bytes)
135+
136+
def write(self, data: bytes) -> int:
137+
"""Write bytes to serial port.
138+
139+
Parameters
140+
----------
141+
data : int
142+
Bytes to write to the serial port.
143+
144+
Returns
145+
-------
146+
int
147+
Number of bytes written.
148+
"""
149+
return self._ser.write(data)
150+
151+
def __repr__(self) -> str: # noqa
152+
return (
153+
f"{self.__class__.__name__}"
154+
"["
155+
f"{self.port}, "
156+
f"{self.baudrate} baud, "
157+
f"timeout {self.timeout} s"
158+
"]"
159+
)

0 commit comments

Comments
 (0)
Please sign in to comment.