Files
pyGUI/serialcomm/device_service.py
T
Jack.Le 7e76cffc88 Add SettingsTab, StatusTab, DataTab and wire up DeviceController
- SettingsTab: chamber ID, master CSV mapping, wafer behavior toggle,
  light/dark mode toggle, Edit CSV Metadata dialog
- StatusTab: connection status, wafer info, activity log (blank until
  Detect Wafer fires)
- DataTab: empty state, ready for parsed data
- HomePage: side rail buttons dispatch device actions and jump to Status
  tab; selectedSideActionIndex defaults to -1 (nothing active on startup)
- DeviceController registered as QML context property in main.py
- LocalSettings: added status persistence fields
- Theme: added subheadingColor and disabledText
- Added serialcomm/ package and backend data/graph modules
- gitignore: added build/, dist/, *.spec
2026-05-28 15:02:58 -07:00

153 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)