Restructure into src/ layout under pygui package

Move all application source under src/pygui/ and rewire imports,
build config, and QML module path to match.
- Relocate backend/, serialcomm/, and the ISC QML module into
  src/pygui/; convert main.py into pygui/__main__.py with a main()
  entry point (run via `python -m pygui` or the new `isc` script)
- Rewrite absolute imports: backend.* -> pygui.backend.*,
  serialcomm.* -> pygui.serialcomm.* (source + tests)
- Move app icons (isc.ico/icns) into packaging/
- Update README and ISC.qmlproject to the new paths
This commit is contained in:
jack
2026-06-03 11:41:45 -07:00
parent af170666e8
commit 9779baa468
34 changed files with 130 additions and 36 deletions
+152
View File
@@ -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 pygui.backend.local_settings import LocalSettings
from pygui.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)