Add SettingsTab, StatusTab, DataTab and wire up DeviceController
- SettingsTab: chamber ID, master CSV mapping (families A-Z), wafer behavior toggle, light/dark mode toggle, Edit CSV Metadata dialog - StatusTab: connection status card, wafer info, activity log (blank until Detect Wafer fires from the side rail) - DataTab: empty state, ready for parsed data - HomePage: side rail buttons dispatch device actions and jump to Status tab; selectedSideActionIndex defaults to -1 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: restore clean version
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user