Files
sunnypilot/openpilot/common/serial.py
T
Adeeb Shihadeh bf5540c364 rm pyserial (#38311)
* rm pyserial

* lil more

* lil more
2026-07-08 21:06:53 -07:00

273 lines
7.4 KiB
Python

import errno
import fcntl
import os
import select
import struct
import termios
import time
# Modem control lines (linux/termios.h); fall back to common x86_64 values.
TIOCMBIS = getattr(termios, "TIOCMBIS", 0x5416)
TIOCMBIC = getattr(termios, "TIOCMBIC", 0x5417)
TIOCM_DTR = getattr(termios, "TIOCM_DTR", 0x002)
TIOCM_RTS = getattr(termios, "TIOCM_RTS", 0x004)
_TIOCM_DTR = struct.pack("I", TIOCM_DTR)
_TIOCM_RTS = struct.pack("I", TIOCM_RTS)
class SerialException(OSError):
pass
class Serial:
def __init__(self, port: str, baudrate: int = 9600, timeout: float | None = None, *,
rtscts: bool = False, dsrdtr: bool = False, exclusive: bool = False):
self._port = port
self._baudrate = baudrate
self._timeout = timeout
self._rtscts = rtscts
self._dsrdtr = dsrdtr
self._exclusive = exclusive
self._dtr = True
self._fd = -1
self.open()
def __enter__(self):
return self
def __exit__(self, *args) -> None:
self.close()
@property
def fd(self) -> int:
self._ensure_open()
return self._fd
@property
def baudrate(self) -> int:
return self._baudrate
@baudrate.setter
def baudrate(self, value: int) -> None:
self._baudrate = int(value)
if self._fd >= 0:
self._configure()
@property
def dtr(self) -> bool:
return self._dtr
@dtr.setter
def dtr(self, value: bool) -> None:
self._dtr = bool(value)
if self._fd >= 0:
self._set_line(TIOCM_DTR, _TIOCM_DTR, self._dtr)
def open(self) -> None:
if self._fd >= 0:
return
try:
self._fd = os.open(self._port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
except OSError as e:
self._fd = -1
raise SerialException(e.errno, f"could not open port {self._port}: {e}") from e
try:
if self._exclusive:
try:
fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as e:
raise SerialException(e.errno, f"could not exclusively lock port {self._port}: {e}") from e
self._configure()
# When not using hardware DSR/DTR handshaking, drive lines ourselves.
if not self._dsrdtr:
try:
self._set_line(TIOCM_DTR, _TIOCM_DTR, self._dtr)
if not self._rtscts:
self._set_line(TIOCM_RTS, _TIOCM_RTS, True)
except OSError as e:
if e.errno not in (errno.EINVAL, errno.ENOTTY):
raise
self.reset_input_buffer()
except BaseException:
self._close_fd()
raise
def close(self) -> None:
self._close_fd()
def read(self, size: int = 1) -> bytes:
self._ensure_open()
if size <= 0:
return b""
buf = bytearray()
deadline = self._deadline()
while len(buf) < size:
remaining = None if deadline is None else max(0.0, deadline - time.monotonic())
if not self._wait_readable(remaining):
break
try:
chunk = os.read(self._fd, size - len(buf))
except InterruptedError:
continue
except OSError as e:
if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
if self._timeout == 0:
break
continue
raise SerialException(e.errno, f"read failed: {e}") from e
if not chunk:
break
buf.extend(chunk)
return bytes(buf)
def readline(self) -> bytes:
self._ensure_open()
buf = bytearray()
deadline = self._deadline()
while True:
remaining = None if deadline is None else max(0.0, deadline - time.monotonic())
if deadline is not None and remaining == 0.0 and not buf:
# match pyserial: timed-out readline returns empty
if not self._wait_readable(0.0):
return b""
elif not self._wait_readable(remaining):
return bytes(buf)
try:
chunk = os.read(self._fd, 1)
except InterruptedError:
continue
except OSError as e:
if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
if self._timeout == 0:
return bytes(buf)
continue
raise SerialException(e.errno, f"read failed: {e}") from e
if not chunk:
return bytes(buf)
buf.extend(chunk)
if chunk == b"\n":
return bytes(buf)
def write(self, data: bytes) -> int:
self._ensure_open()
if not data:
return 0
view = memoryview(data)
total = 0
while total < len(data):
try:
n = os.write(self._fd, view[total:])
except InterruptedError:
continue
except OSError as e:
if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
select.select([], [self._fd], [], None)
continue
raise SerialException(e.errno, f"write failed: {e}") from e
if n == 0:
raise SerialException("write returned 0")
total += n
return total
def flush(self) -> None:
self._ensure_open()
termios.tcdrain(self._fd)
def reset_input_buffer(self) -> None:
self._ensure_open()
termios.tcflush(self._fd, termios.TCIFLUSH)
def reset_output_buffer(self) -> None:
self._ensure_open()
termios.tcflush(self._fd, termios.TCOFLUSH)
def _close_fd(self) -> None:
if self._fd >= 0:
try:
if self._exclusive:
fcntl.flock(self._fd, fcntl.LOCK_UN)
except OSError:
pass
try:
os.close(self._fd)
except OSError:
pass
self._fd = -1
def _ensure_open(self) -> None:
if self._fd < 0:
raise SerialException("port is not open")
def _deadline(self) -> float | None:
if self._timeout is None:
return None
if self._timeout == 0:
return time.monotonic()
return time.monotonic() + self._timeout
def _wait_readable(self, timeout: float | None) -> bool:
"""Return True if fd is readable. timeout None blocks; 0 polls."""
if timeout is not None and timeout < 0:
timeout = 0.0
try:
ready, _, _ = select.select([self._fd], [], [], timeout)
except InterruptedError:
return False
return bool(ready)
def _baud_constant(self, baudrate: int) -> int:
try:
return getattr(termios, f"B{baudrate}")
except AttributeError as e:
raise ValueError(f"unsupported baud rate: {baudrate}") from e
def _configure(self) -> None:
self._ensure_open()
try:
attrs = termios.tcgetattr(self._fd)
except termios.error as e:
raise SerialException(f"could not get port attributes: {e}") from e
iflag, oflag, cflag, lflag, _ispeed, _ospeed, cc = attrs
# raw binary 8N1
iflag = 0
oflag = 0
lflag = 0
cflag |= termios.CLOCAL | termios.CREAD
cflag &= ~termios.CSIZE
cflag |= termios.CS8
cflag &= ~(termios.PARENB | termios.PARODD | termios.CSTOPB)
if hasattr(termios, "CRTSCTS"):
if self._rtscts:
cflag |= termios.CRTSCTS
else:
cflag &= ~termios.CRTSCTS
speed = self._baud_constant(self._baudrate)
cc = list(cc)
# Non-blocking reads are handled via select + O_NONBLOCK; keep VMIN/VTIME at 0.
cc[termios.VMIN] = 0
cc[termios.VTIME] = 0
try:
termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, speed, speed, cc])
except termios.error as e:
raise SerialException(f"could not configure port: {e}") from e
# Keep the fd non-blocking so timeout=0 and select work consistently.
flags = fcntl.fcntl(self._fd, fcntl.F_GETFL)
fcntl.fcntl(self._fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
def _set_line(self, _bit: int, packed: bytes, enabled: bool) -> None:
request = TIOCMBIS if enabled else TIOCMBIC
fcntl.ioctl(self._fd, request, packed)