136 lines
5.1 KiB
Python
136 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import logging
|
|
import time
|
|
import threading
|
|
from typing import Optional, Union
|
|
|
|
import serial
|
|
|
|
from m2m.serial.urc_handler import UrcHandler
|
|
from m2m.serial.mock_serial import MockSerial
|
|
from m2m.exceptions import SerialError
|
|
|
|
logger = logging.getLogger('m2m.serial')
|
|
|
|
class SerialPort(UrcHandler):
|
|
"""
|
|
Serial port class tailored for AT-command communication.
|
|
Thread-safe using RLock.
|
|
"""
|
|
|
|
def __init__(self, port: str, baudrate: int = 9600, timeout: float = 1.0, delimiter: bytes = b'\r', **kwargs):
|
|
super().__init__()
|
|
self._delimiter = delimiter
|
|
self._port_name = port
|
|
self.lock = threading.RLock()
|
|
|
|
try:
|
|
if port.upper() == 'MOCK':
|
|
self._serial = MockSerial(port=port, baudrate=baudrate, timeout=timeout, **kwargs)
|
|
else:
|
|
self._serial = serial.serial_for_url(url=port, baudrate=baudrate, timeout=timeout, **kwargs)
|
|
if port.startswith(('socket://', 'rfc2217://')):
|
|
time.sleep(0.5) # Settling time for network sockets
|
|
except (serial.SerialException, ValueError) as e:
|
|
logger.error(f"Failed to open port {port}: {e}")
|
|
raise SerialError(f"Could not open {port}") from e
|
|
|
|
logger.debug(f"Opened serial port: {port} @ {baudrate}")
|
|
|
|
def __enter__(self): return self
|
|
def __exit__(self, *args): self.close()
|
|
|
|
def close(self):
|
|
"""Closes the serial port."""
|
|
if self._serial and self._serial.is_open:
|
|
self._serial.close()
|
|
logger.debug(f"Closed serial port: {self._port_name}")
|
|
|
|
def send_cmd(self, cmd: Union[bytes, str]) -> None:
|
|
"""Sends a command appended with the delimiter."""
|
|
if isinstance(cmd, str):
|
|
cmd = cmd.encode('ascii')
|
|
full_cmd = cmd + self._delimiter
|
|
with self.lock:
|
|
try:
|
|
logger.debug(f"TX: {full_cmd!r}")
|
|
self._serial.write(full_cmd)
|
|
except (serial.SerialException, ConnectionError, OSError) as e:
|
|
raise SerialError(f"Write failed: {e}") from e
|
|
|
|
def read_until(self, terminator: bytes = b'OK', timeout: float = 5.0) -> bytes:
|
|
"""Reads until the terminator is found or timeout occurs."""
|
|
buffer = bytearray()
|
|
end_time = time.time() + timeout
|
|
|
|
with self.lock:
|
|
while time.time() < end_time:
|
|
# We read whatever is available to catch partial lines or prompts
|
|
try:
|
|
in_waiting = self._serial.in_waiting
|
|
except (AttributeError, TypeError):
|
|
in_waiting = 0
|
|
|
|
if isinstance(in_waiting, int) and in_waiting > 0:
|
|
data = self._serial.read(in_waiting)
|
|
if data:
|
|
buffer.extend(data)
|
|
# Process URCs inside the buffer
|
|
for line in bytes(buffer).splitlines(keepends=True):
|
|
if line.endswith((b'\r', b'\n')):
|
|
self.check_urc(line.strip())
|
|
if terminator in buffer: break
|
|
else:
|
|
time.sleep(0.01)
|
|
|
|
res = bytes(buffer)
|
|
if res: logger.debug(f"RX: {res!r}")
|
|
return res
|
|
|
|
def read_until_notify(self, notification: bytes, timeout: float = 5.0) -> Optional[bytes]:
|
|
"""Waits specifically for a URC notification."""
|
|
end_time = time.time() + timeout
|
|
while time.time() < end_time:
|
|
self._read_line() # Pumping the reader
|
|
if val := self.pop_urc_value(notification):
|
|
return val
|
|
time.sleep(0.01)
|
|
return None
|
|
|
|
def read_any(self, timeout: float = 5.0) -> bytes:
|
|
"""Reads anything available within the timeout."""
|
|
buffer = bytearray()
|
|
end_time = time.time() + timeout
|
|
with self.lock:
|
|
while time.time() < end_time:
|
|
try:
|
|
in_waiting = self._serial.in_waiting
|
|
except (AttributeError, TypeError):
|
|
in_waiting = 0
|
|
|
|
if isinstance(in_waiting, int) and in_waiting > 0:
|
|
buffer.extend(self._serial.read(in_waiting))
|
|
else:
|
|
if timeout < 0.2: break
|
|
time.sleep(0.01)
|
|
return bytes(buffer)
|
|
|
|
def _read_line(self) -> bytes:
|
|
"""Internal method to read a single line and process URCs."""
|
|
try:
|
|
try:
|
|
in_waiting = self._serial.in_waiting
|
|
except (AttributeError, TypeError):
|
|
in_waiting = 0
|
|
|
|
line = self._serial.readline()
|
|
if line:
|
|
logger.debug(f"RX Line: {line!r}")
|
|
if self.check_urc(line.strip()): return b''
|
|
elif self._port_name.startswith(('socket://', 'rfc2217://')) and (isinstance(in_waiting, int) and in_waiting == 0):
|
|
time.sleep(0.01)
|
|
return line
|
|
except (serial.SerialException, ConnectionError, OSError) as e:
|
|
raise SerialError(f"Read failed: {e}") from e
|