Source code for pycsamt.iot.protocols.serial

"""Serial (UART) telemetry transport (optional dependency: ``pyserial``).

:class:`SerialTelemetryClient` writes newline-delimited JSON to a serial
port and reads it back. ``pyserial`` is imported lazily so the subpackage
imports without it.
"""

from __future__ import annotations

import json
from typing import Any

from ..core import TelemetryPacket
from .base import (
    BaseTelemetryClient,
    IoTProtocol,
    TelemetryError,
)

__all__ = ["SerialTelemetryClient"]


def _import_pyserial():
    try:
        import serial  # type: ignore
    except Exception as exc:  # noqa: BLE001
        raise TelemetryError(
            "Serial transport requires the 'pyserial' package. Install it "
            "with `pip install pyserial`."
        ) from exc
    return serial


[docs] class SerialTelemetryClient(BaseTelemetryClient): """Send/receive telemetry over a serial port.""" protocol = IoTProtocol.SERIAL def __init__( self, endpoint: str | None = None, *, dry_run: bool = False, baudrate: int = 115200, timeout: float = 1.0, **options: Any, ) -> None: super().__init__( endpoint, dry_run=dry_run, baudrate=baudrate, timeout=timeout, **options, ) def _connect(self) -> None: serial = _import_pyserial() port = self._require_endpoint() self._handle = serial.Serial( port, baudrate=int(self.options.get("baudrate", 115200)), timeout=float(self.options.get("timeout", 1.0)), ) def _disconnect(self) -> None: if self._handle is not None: try: self._handle.close() finally: self._handle = None def _transport_send(self, packet: TelemetryPacket) -> str: if self._handle is None: raise TelemetryError("Serial client is not connected.") self._handle.write(self._payload_bytes(packet) + b"\n") self._handle.flush() return f"wrote {packet.topic} to serial" def _transport_receive( self, *, timeout: float | None = None ) -> dict[str, Any] | None: if self._handle is None: raise TelemetryError("Serial client is not connected.") line = self._handle.readline() if not line: return None text = line.decode("utf-8", errors="replace").strip() if not text: return None try: return json.loads(text) except json.JSONDecodeError: return {"raw": text} def _transport_healthcheck(self) -> bool: return self._handle is not None and getattr( self._handle, "is_open", False )