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:
@@ -0,0 +1,47 @@
|
||||
"""Persisted user defaults (ports, scope IP, save directory).
|
||||
|
||||
One flat JSON file, one dataclass. ``save()`` always writes every field, so
|
||||
a partial UI update can never silently drop another field's saved value
|
||||
(which is exactly what the old dict-based writer did to helios_port).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import asdict, dataclass, fields
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULTS_PATH = Path(__file__).resolve().parent.parent / "aui_defaults.json"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScanDefaults:
|
||||
t3r_port: str = "/dev/ttyUSB0"
|
||||
bbd_port: str = "/dev/ttyUSB1"
|
||||
oscope_ip: str = "192.168.0.1"
|
||||
save_dir: str = str(DEFAULTS_PATH.parent / "scans")
|
||||
helios_port: str = "/dev/ttyUSB2"
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path = DEFAULTS_PATH) -> "ScanDefaults":
|
||||
"""Load defaults, tolerating a missing/corrupt file and unknown keys."""
|
||||
if path.exists():
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
known = {f.name for f in fields(cls)}
|
||||
return cls(**{k: v for k, v in data.items() if k in known})
|
||||
except (OSError, ValueError, TypeError) as e:
|
||||
logger.warning("Could not load %s (%s); using fallback defaults", path, e)
|
||||
inst = cls()
|
||||
inst.save(path)
|
||||
return inst
|
||||
|
||||
def save(self, path: Path = DEFAULTS_PATH) -> None:
|
||||
try:
|
||||
with open(path, "w") as f:
|
||||
json.dump(asdict(self), f, indent=2)
|
||||
except OSError as e:
|
||||
logger.warning("Could not save defaults to %s: %s", path, e)
|
||||
Reference in New Issue
Block a user