"""High-level wafer device communication service. Coordinates port enumeration, detect, read, and erase operations using settings from LocalSettingsModel. Designed to be called from QML via @Slot methods on a QObject controller. """ import logging from pathlib import Path from typing import Optional import serial.tools.list_ports from backend.local_settings import LocalSettings from serialcomm.serial_port import SerialPort, WaferInfo log = logging.getLogger(__name__) class DeviceService: """High-level wafer device communication.""" # Expected hex string lengths for known wafer families # P wafer: 393216 hex chars (196608 bytes) # X wafer: 1310720 hex chars (655360 bytes) EXPECTED_HEX_LENGTHS = { "P": 393216, "X": 1310720, } def __init__(self, settings: LocalSettings) -> None: self._settings = settings # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------ def enumerate_ports(self) -> list[str]: """Return list of available serial port names.""" return [p.device for p in serial.tools.list_ports.comports()] def detect_wafer(self, port: str) -> Optional[WaferInfo]: """Try to detect a wafer on the given port. Opens the port, sends s0 command, parses response. Returns WaferInfo if detected, None otherwise. """ try: sp = SerialPort(port) info = sp.detect(self._settings.wafer_detect_timeout) return info except Exception as exc: log.error("Detect failed on %s: %s", port, exc) return None SCAN_PER_PORT_TIMEOUT_MS = 1500 """Per-port timeout while scanning. Wafer responds fast or not at all.""" def detect_all_ports(self) -> tuple[Optional[str], Optional[WaferInfo]]: """Scan every available serial port until one responds to s0. Mirrors C# chkConnection(): enumerates all ports, tries each one, returns the first that responds with a valid 1024-byte payload. Uses a short per-port timeout (SCAN_PER_PORT_TIMEOUT_MS) so a machine with many serial devices (Bluetooth, debug consoles, etc.) doesn't take 30+ seconds to scan. The configured wafer_detect_timeout is reserved for confirmed-port operations. Returns: (port_name, WaferInfo) on success, (None, None) if nothing found. """ ports = self.enumerate_ports() log.info("Scanning %d port(s) for wafer ...", len(ports)) for port in ports: log.info(" trying %s ...", port) try: sp = SerialPort(port) info = sp.detect(self.SCAN_PER_PORT_TIMEOUT_MS) except Exception as exc: log.warning(" %s: %s", port, exc) info = None if info is not None: log.info(" found wafer on %s (family=%s)", port, info.family_code) return port, info log.info(" no wafer detected on any port") return None, None def read_wafer_data( self, port: str, family_code: str = "", cmd: str = "D1", timeout_ms: int | None = None, retries: int | None = None, ) -> Optional[bytes]: """Full read workflow: open → send command → retry → close. Args: port: Serial port name (e.g. "/dev/ttyUSB0"). family_code: Wafer family ("P", "X", etc.) for size validation. cmd: Command string ("D1" for sensor data, "F1" for debug data). timeout_ms: Read timeout in ms (uses settings default if None). retries: Max retries on bad read (uses settings default if None). Returns: Raw bytes from the wafer, or None on failure. """ if timeout_ms is None: timeout_ms = self._settings.wafer_read_timeout if retries is None: retries = self._settings.wafer_read_retries # Determine expected hex string length. # get_wafer_data_size returns BYTES; hex string is 2× that. if family_code: expected_hex_len = self._settings.get_wafer_data_size(family_code) * 2 else: expected_hex_len = 0 # no validation try: sp = SerialPort(port) data = sp.read_memory( cmd=cmd, expected_hex_len=expected_hex_len, timeout_ms=timeout_ms, retries=retries, ) if data is not None: log.info("Read %d bytes from wafer on %s", len(data), port) else: log.warning("Read returned no data from %s", port) return data except Exception as exc: log.error("Read failed on %s: %s", port, exc) return None def erase_wafer(self, port: str) -> bool: """Send p1 erase command. Wafer takes ~15s to erase. Returns True if command was sent successfully. """ try: sp = SerialPort(port) return sp.erase_memory() except Exception as exc: log.error("Erase failed on %s: %s", port, exc) return False def get_expected_hex_length(self, family_code: str) -> int: """Return expected hex string length for a wafer family.""" return self._settings.get_wafer_data_size(family_code)