Phase 2: extract headless core modules (sras_format, scan_geometry, config)
- core/sras_format.py: THE v6 implementation — create_scan_file (writer, byte-identical to the old one, enforced against the Phase-0 goldens), SrasFile parser with frontier/truncation walk, and zero-copy mmap load_angle/load_row views for multi-GB files - core/scan_geometry.py: ScanPlan/AngleGeometry dataclasses, build_plan (rotated-bbox trig from MainWindow._build_scan_params), travel-limit validate_plan (limits now a StageLimits dataclass, not literals buried in the worker), format_eta + EtaEstimator (bounded deque) - core/config.py: ScanDefaults dataclass replaces the module-import-time dict globals. FIXES: editing any main-window port used to rewrite aui_defaults.json without helios_port, silently reverting the Helios port every time (test_helios_port_survives_partial_update covers it). Also drops the inert laser_freq_hz plumbing — scans always used the LASER_FREQ_HZ constant. - hardware/serial_util.py: shared 8N1 open + scored port enumeration (promoted from t3r_control_panel); helios_laser and the panel use it - sc3_aui_app.py and sras_scan_manager.py migrated onto core (three format implementations down to one); ScanWorker now takes a ScanPlan - tests: byte-identical writer vs golden, frontier over every truncation variant, mmap==eager, geometry vs golden fixtures + invariants, config round-trip. 28 passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,13 @@ Helios Laser System Driver
|
||||
Basic implementation for controlling the Helios pulsed laser.
|
||||
"""
|
||||
|
||||
import serial
|
||||
import time
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
|
||||
from hardware.serial_util import open_8n1
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -42,10 +43,9 @@ class HeliosLaser:
|
||||
|
||||
@staticmethod
|
||||
def list_available_ports() -> List[str]:
|
||||
"""List available serial ports"""
|
||||
import serial.tools.list_ports
|
||||
ports = serial.tools.list_ports.comports()
|
||||
return [port.device for port in ports]
|
||||
"""List available serial ports, likeliest devices first."""
|
||||
from hardware.serial_util import list_port_devices
|
||||
return list_port_devices()
|
||||
|
||||
def connect(self, port: str = None) -> bool:
|
||||
"""
|
||||
@@ -65,14 +65,7 @@ class HeliosLaser:
|
||||
return False
|
||||
|
||||
try:
|
||||
self.serial = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=9600,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=self.timeout
|
||||
)
|
||||
self.serial = open_8n1(self.port, baudrate=9600, timeout=self.timeout)
|
||||
time.sleep(0.1) # Allow time for connection to stabilize
|
||||
self.is_connected = True
|
||||
logger.info(f"Connected to Helios laser on {self.port}")
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Shared serial-port helpers: 8N1 open and scored port enumeration.
|
||||
|
||||
Qt-free — GUI code adapts the (device, label) list into its own widgets.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
|
||||
# Substrings that suggest a USB-serial adapter we actually talk to
|
||||
# (ESP32-based T3R, CP210x/CH340 dongles, CDC-ACM devices); matching ports
|
||||
# sort first in pickers.
|
||||
DEVICE_HINTS = ("esp32", "jtag", "espressif", "usb serial", "cp210", "ch340", "cdc")
|
||||
|
||||
|
||||
def open_8n1(port: str, baudrate: int, timeout: float,
|
||||
write_timeout: float | None = None) -> serial.Serial:
|
||||
"""Open a serial port with the 8N1 framing every device here uses."""
|
||||
return serial.Serial(
|
||||
port=port,
|
||||
baudrate=baudrate,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=timeout,
|
||||
write_timeout=write_timeout,
|
||||
)
|
||||
|
||||
|
||||
def scored_ports() -> list[tuple[str, str]]:
|
||||
"""Enumerate serial ports as (device, human label), likeliest-first."""
|
||||
ports = list(list_ports.comports())
|
||||
|
||||
def score(p):
|
||||
text = f"{p.description} {p.manufacturer or ''} {p.product or ''}".lower()
|
||||
return -sum(h in text for h in DEVICE_HINTS)
|
||||
|
||||
ports.sort(key=score)
|
||||
return [(p.device, f"{p.device} — {p.description or p.device}") for p in ports]
|
||||
|
||||
|
||||
def list_port_devices() -> list[str]:
|
||||
"""Plain device-path list, likeliest-first."""
|
||||
return [dev for dev, _ in scored_ports()]
|
||||
Reference in New Issue
Block a user