Skip to content

Commit e70d01d

Browse files
committed
Refactor SerialHandler
1 parent a3d88bb commit e70d01d

23 files changed

+639
-131
lines changed

pslab/bus/busio.py

Lines changed: 9 additions & 4 deletions
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

Lines changed: 5 additions & 5 deletions
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

Lines changed: 5 additions & 5 deletions
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

Lines changed: 4 additions & 4 deletions
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

Lines changed: 63 additions & 0 deletions
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

Lines changed: 159 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy