refactor: standardize codebase formatting, optimize import orders, and update GUI resolution to 1080p
This commit is contained in:
@@ -4,8 +4,8 @@ import ISC
|
||||
// ===== App Window Shell =====
|
||||
Window {
|
||||
// ===== Window Dimensions =====
|
||||
width: 1400
|
||||
height: 820
|
||||
width: 1920
|
||||
height: 1080
|
||||
minimumWidth: 1100
|
||||
minimumHeight: 700
|
||||
visible: true
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
from PySide6.QtQml import QQmlApplicationEngine
|
||||
from PySide6.QtQuickControls2 import QQuickStyle
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from pygui.backend.controllers.device_controller import DeviceController
|
||||
from pygui.backend.controllers.session_controller import SessionController
|
||||
from pygui.backend.data.file_browser import FileBrowser
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
from pygui.backend.data.local_settings_model import LocalSettingsModel
|
||||
from pygui.backend.data.file_browser import FileBrowser
|
||||
from pygui.backend.controllers.session_controller import SessionController
|
||||
|
||||
# Importing wafer_map_item registers the @QmlElement WaferMapItem (QML: import ISC.Wafer).
|
||||
from pygui.backend.visualization.wafer_map_item import WaferMapItem
|
||||
|
||||
|
||||
# ===== Application Entry Point =====
|
||||
def main() -> int:
|
||||
|
||||
@@ -4,29 +4,25 @@
|
||||
|
||||
from pygui.backend.controllers.device_controller import DeviceController # noqa: F401
|
||||
from pygui.backend.controllers.session_controller import SessionController # noqa: F401
|
||||
|
||||
from pygui.backend.models.frame import Frame # noqa: F401
|
||||
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data # noqa: F401
|
||||
from pygui.backend.models.frame_stats import Stats, compute_stats # noqa: F401
|
||||
from pygui.backend.models.session_model import SessionModel # noqa: F401
|
||||
from pygui.backend.models.data_model import TemperatureTableModel # noqa: F401
|
||||
from pygui.backend.models.sensor_editor import SensorEditor # noqa: F401
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig # noqa: F401
|
||||
from pygui.backend.models.stability_detector import StabilityDetector # noqa: F401
|
||||
|
||||
from pygui.backend.data.csv_recorder import CsvRecorder # noqa: F401
|
||||
from pygui.backend.data.csv_file_metadata import CSVFileMetadata # noqa: F401
|
||||
from pygui.backend.data.csv_recorder import CsvRecorder # noqa: F401
|
||||
from pygui.backend.data.data_records import read_data_records # noqa: F401
|
||||
from pygui.backend.data.data_segment import DataSegment # noqa: F401
|
||||
from pygui.backend.data.file_browser import FileBrowser # noqa: F401
|
||||
from pygui.backend.data.local_settings import LocalSettings # noqa: F401
|
||||
from pygui.backend.data.local_settings_model import LocalSettingsModel # noqa: F401
|
||||
|
||||
from pygui.backend.visualization.wafer_map_item import WaferMapItem # noqa: F401
|
||||
from pygui.backend.models.data_model import TemperatureTableModel # noqa: F401
|
||||
from pygui.backend.models.frame import Frame # noqa: F401
|
||||
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data # noqa: F401
|
||||
from pygui.backend.models.frame_stats import Stats, compute_stats # noqa: F401
|
||||
from pygui.backend.models.sensor_editor import SensorEditor # noqa: F401
|
||||
from pygui.backend.models.session_model import SessionModel # noqa: F401
|
||||
from pygui.backend.models.stability_detector import StabilityDetector # noqa: F401
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig # noqa: F401
|
||||
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment # noqa: F401
|
||||
from pygui.backend.visualization.graph_view import GraphView # noqa: F401
|
||||
from pygui.backend.visualization.rbf_heatmap import interpolate_field # noqa: F401
|
||||
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment # noqa: F401
|
||||
|
||||
from pygui.backend.wafer.zwafer_models import ZWaferData, Sensor, DataRecord # noqa: F401
|
||||
from pygui.backend.visualization.wafer_map_item import WaferMapItem # noqa: F401
|
||||
from pygui.backend.wafer.wafer_layouts import available_families, load_layout # noqa: F401
|
||||
from pygui.backend.wafer.zwafer_models import DataRecord, Sensor, ZWaferData # noqa: F401
|
||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser # noqa: F401
|
||||
from pygui.backend.wafer.wafer_layouts import load_layout, available_families # noqa: F401
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
|
||||
|
||||
@@ -20,7 +22,7 @@ def average_clusters(values: list[float], clusters: list[list[int]]) -> list[flo
|
||||
val = values[idx]
|
||||
if not math.isnan(val):
|
||||
valid_vals.append(val)
|
||||
|
||||
|
||||
if valid_vals:
|
||||
mean_val = sum(valid_vals) / len(valid_vals)
|
||||
for idx in cluster:
|
||||
|
||||
@@ -3,4 +3,3 @@ from pygui.backend.controllers.device_controller import DeviceController
|
||||
from pygui.backend.controllers.session_controller import SessionController
|
||||
|
||||
__all__ = ["DeviceController", "SessionController"]
|
||||
|
||||
@@ -7,11 +7,12 @@ import threading
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from PySide6.QtCore import QObject, Property, Qt, Signal, Slot
|
||||
from PySide6.QtCore import Property, QObject, Qt, Signal, Slot
|
||||
|
||||
from pygui.backend.controllers.session_controller import SessionController
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
from pygui.backend.models.data_model import TemperatureTableModel
|
||||
from pygui.backend.visualization.graph_view import GraphView
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
from pygui.serialcomm.data_parser import (
|
||||
convert_to_temperatures,
|
||||
parse_binary_data,
|
||||
@@ -20,7 +21,7 @@ from pygui.serialcomm.data_parser import (
|
||||
)
|
||||
from pygui.serialcomm.device_service import DeviceService
|
||||
from pygui.serialcomm.serial_port import WaferInfo
|
||||
from pygui.backend.controllers.session_controller import SessionController
|
||||
|
||||
# import pygui.backend.wafer_map_item
|
||||
|
||||
stream_controller = SessionController()
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
"""QML-facing controller for the live/review wafer dashboard."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import QObject, Property, QTimer, Qt, Signal, Slot
|
||||
from PySide6.QtCore import Property, QObject, Qt, QTimer, Signal, Slot
|
||||
|
||||
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
|
||||
from pygui.backend.data.csv_recorder import CsvRecorder
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data
|
||||
from pygui.backend.models.sensor_editor import SensorEditor
|
||||
from pygui.backend.models.session_model import SessionModel
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser
|
||||
from pygui.serialcomm.stream_reader import StreamReader
|
||||
from pygui.backend.models.sensor_editor import SensorEditor
|
||||
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MODE_LIVE = "live"
|
||||
@@ -24,7 +26,7 @@ MODE_REVIEW = "review"
|
||||
|
||||
class SessionController(QObject):
|
||||
# public signals
|
||||
frameUpdated = Signal()
|
||||
frameUpdated = Signal()
|
||||
modeChanged = Signal()
|
||||
stateChanged = Signal()
|
||||
recordingChanged = Signal()
|
||||
@@ -56,7 +58,7 @@ class SessionController(QObject):
|
||||
self._dirty = False
|
||||
|
||||
self._liveFrame.connect(self._on_live_frame, Qt.ConnectionType.QueuedConnection)
|
||||
|
||||
|
||||
self._sensor_editor = SensorEditor()
|
||||
self._last_raw_frame: Frame | None = None
|
||||
self._loaded_file: str = ""
|
||||
@@ -74,13 +76,13 @@ class SessionController(QObject):
|
||||
def frameTotal(self) -> int: return self._player.total
|
||||
|
||||
@Property(str, notify=stateChanged)
|
||||
def state(self) -> str:
|
||||
def state(self) -> str:
|
||||
if self._last:
|
||||
return self._last.state
|
||||
# IF we dont have data yet, but the reader is running -> Streaming
|
||||
if self._mode == MODE_LIVE and self._reader is not None:
|
||||
return "streaming"
|
||||
|
||||
|
||||
return "idle"
|
||||
|
||||
@Property(bool, notify=recordingChanged)
|
||||
@@ -198,7 +200,12 @@ class SessionController(QObject):
|
||||
@Slot(str)
|
||||
def loadFile(self, file_path: str) -> None:
|
||||
from pathlib import Path
|
||||
from pygui.backend.data.data_records import is_official_csv, read_data_records, read_official_csv
|
||||
|
||||
from pygui.backend.data.data_records import (
|
||||
is_official_csv,
|
||||
read_data_records,
|
||||
read_official_csv,
|
||||
)
|
||||
from pygui.backend.wafer.wafer_layouts import WaferLayout, resolve_shape_and_size
|
||||
|
||||
frames = []
|
||||
@@ -319,10 +326,9 @@ class SessionController(QObject):
|
||||
# ---- live: stream start/stop ----
|
||||
@Slot(str, str)
|
||||
def startStream(self, port: str, family_code: str = "") -> None:
|
||||
from pygui.serialcomm.serial_port import SerialPort # transport open
|
||||
import serial as pyserial
|
||||
|
||||
from pygui.backend.wafer.wafer_layouts import load_layout_for_wafer_id
|
||||
import time
|
||||
from pygui.serialcomm.serial_port import SerialPort # transport open
|
||||
|
||||
if family_code:
|
||||
try:
|
||||
@@ -346,7 +352,7 @@ class SessionController(QObject):
|
||||
high_byte = payload[i * 2]
|
||||
low_byte = payload[i * 2 + 1]
|
||||
val16 = (high_byte << 8) | low_byte
|
||||
|
||||
|
||||
is_negative = (val16 & 0x8000) != 0
|
||||
integer_part = (val16 >> 7) & 0xFF
|
||||
fractional_bits = val16 & 0x7F
|
||||
@@ -355,7 +361,7 @@ class SessionController(QObject):
|
||||
if is_negative:
|
||||
result = -result
|
||||
values.append(result)
|
||||
|
||||
|
||||
return Frame(seq=seq, time=time.monotonic(), values=values)
|
||||
|
||||
# Clear out any old data from the prev sessions
|
||||
@@ -378,7 +384,7 @@ class SessionController(QObject):
|
||||
family_code=family_code or "A")
|
||||
self._reader.start()
|
||||
self._repaint_timer.start()
|
||||
|
||||
|
||||
self.setMode("live")
|
||||
self.stateChanged.emit()
|
||||
|
||||
@@ -389,7 +395,7 @@ class SessionController(QObject):
|
||||
transport = self._reader._transport
|
||||
self._reader.stop()
|
||||
self._reader = None
|
||||
|
||||
|
||||
# Send 'D2S' command padded to 512 bytes to stop the stream
|
||||
if transport and transport.is_open:
|
||||
try:
|
||||
@@ -398,7 +404,7 @@ class SessionController(QObject):
|
||||
transport.flush()
|
||||
except Exception as exc:
|
||||
log.error("Error sending stop command: %s", exc)
|
||||
|
||||
|
||||
self.stateChanged.emit()
|
||||
self.stopRecording()
|
||||
|
||||
@@ -455,6 +461,5 @@ class SessionController(QObject):
|
||||
"""Remove all sensor overrides."""
|
||||
self._sensor_editor.clear()
|
||||
self._reprocess_current()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import secrets
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
|
||||
# ===== Crypto Utilities =====
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ===== Data Sub-package =====
|
||||
from pygui.backend.data.csv_recorder import CsvRecorder
|
||||
from pygui.backend.data.csv_file_metadata import CSVFileMetadata
|
||||
from pygui.backend.data.data_records import read_data_records, is_official_csv, read_official_csv
|
||||
from pygui.backend.data.csv_recorder import CsvRecorder
|
||||
from pygui.backend.data.data_records import is_official_csv, read_data_records, read_official_csv
|
||||
from pygui.backend.data.data_segment import DataSegment
|
||||
from pygui.backend.data.file_browser import FileBrowser
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
"""Append live frames"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, TextIO
|
||||
|
||||
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
|
||||
|
||||
class CsvRecorder:
|
||||
def __init__(self) -> None:
|
||||
self._file_handle: Optional[TextIO] = None
|
||||
self._oath: Optional[str] = None
|
||||
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def is_recording(self) -> bool:
|
||||
return self._file_handle is not None
|
||||
|
||||
|
||||
|
||||
|
||||
def start(self, path: str, sensors:list[Sensor], serial: str = "") -> None:
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
file_handle = open(path, "w", encoding="utf-8", newline ="")
|
||||
@@ -30,14 +31,14 @@ class CsvRecorder:
|
||||
file_handle.write("data\n")
|
||||
file_handle.flush()
|
||||
self._file_handle, self._path = file_handle, path
|
||||
|
||||
|
||||
def write(self, frame: Frame) -> None:
|
||||
if self._file_handle is None:
|
||||
return
|
||||
row = [f"{frame.time:.3f}"] + [f"{v:.3f}" for v in frame.values]
|
||||
self._file_handle.write(",".join(row) + "\n")
|
||||
self._file_handle.flush()
|
||||
|
||||
|
||||
def stop(self) -> Optional[str]:
|
||||
if self._file_handle is None:
|
||||
return None
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Read the data section of a Z-wafer CSV into DataRecords, or the official headerless CSV."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from pygui.backend.wafer.zwafer_models import DataRecord, Sensor
|
||||
|
||||
|
||||
def read_data_records(file_path: str) -> list[DataRecord]:
|
||||
records: list[DataRecord] = []
|
||||
in_data = False
|
||||
|
||||
@@ -5,12 +5,12 @@ import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QObject, Property, QStandardPaths, Signal, Slot
|
||||
from PySide6.QtCore import Property, QObject, QStandardPaths, Signal, Slot
|
||||
from PySide6.QtWidgets import QFileDialog, QMessageBox
|
||||
|
||||
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
|
||||
from pygui.backend.data.csv_file_metadata import CSVFileMetadata
|
||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser
|
||||
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
|
||||
|
||||
|
||||
# ===== File Browser Model =====
|
||||
|
||||
@@ -4,11 +4,10 @@ from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QObject, Property, QDateTime, QStandardPaths, Signal, Slot
|
||||
from PySide6.QtCore import Property, QDateTime, QObject, QStandardPaths, Signal, Slot
|
||||
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
|
||||
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
|
||||
MASTER_FAMILIES = ("A", "B", "C", "D", "E", "F", "P", "X", "Z")
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# ===== Models Sub-package =====
|
||||
from pygui.backend.models.data_model import TemperatureTableModel
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data
|
||||
from pygui.backend.models.frame_stats import Stats, compute_stats
|
||||
from pygui.backend.models.session_model import SessionModel, SessionUpdate
|
||||
from pygui.backend.models.data_model import TemperatureTableModel
|
||||
from pygui.backend.models.sensor_editor import SensorEditor
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig, classify, resolve_bounds
|
||||
from pygui.backend.models.session_model import SessionModel, SessionUpdate
|
||||
from pygui.backend.models.stability_detector import StabilityDetector
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig, classify, resolve_bounds
|
||||
|
||||
__all__ = [
|
||||
"Frame", "FramePlayer", "frames_from_wafer_data",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
from __future__ import annotations
|
||||
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.wafer.zwafer_models import ZWaferData
|
||||
|
||||
|
||||
def frames_from_wafer_data(data: ZWaferData, records) -> list[Frame]:
|
||||
"""Build Frames from parsed DataRecords (records = list[DataRecord].)"""
|
||||
return [Frame(seq=i, time=r.time, values=list(r.values)) for i, r in enumerate(records)]
|
||||
@@ -10,36 +12,36 @@ class FramePlayer:
|
||||
def __init__(self) -> None:
|
||||
self._frames: list[Frame] =[]
|
||||
self._i =0
|
||||
|
||||
|
||||
def load(self, frames: list[Frame]) -> "FramePlayer":
|
||||
self._frames = frames
|
||||
self._i = 0
|
||||
return self
|
||||
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return len(self._frames)
|
||||
|
||||
|
||||
@property
|
||||
def index(self) -> int:
|
||||
return self._i
|
||||
|
||||
return self._i
|
||||
|
||||
@property
|
||||
def at_end(self) -> bool:
|
||||
return self._i >= len(self._frames) -1
|
||||
|
||||
|
||||
def current(self) -> Frame | None:
|
||||
if not self._frames:
|
||||
return None
|
||||
return self._frames[self._i]
|
||||
|
||||
|
||||
def _clamp(self, i:int):
|
||||
return max(0, min(i, len(self._frames) - 1)) if self._frames else 0
|
||||
|
||||
|
||||
def step(self, delta: int) -> "FramePlayer":
|
||||
self._i = self._clamp(self._i + delta)
|
||||
return self
|
||||
|
||||
|
||||
def seek(self, i: int) -> "FramePlayer":
|
||||
self._i = self._clamp(i)
|
||||
return self
|
||||
@@ -48,4 +50,4 @@ class FramePlayer:
|
||||
"""Wall-clock ms until the next frame based on recorded timestamps; 0 if at end."""
|
||||
if self._i + 1 >= len(self._frames):
|
||||
return 0.0
|
||||
return (self._frames[self._i + 1].time - self._frames[self._i].time) * 1000.0
|
||||
return (self._frames[self._i + 1].time - self._frames[self._i].time) * 1000.0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Per-frame descriptive statistics"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@ class SensorEditor:
|
||||
def __init__(self) -> None:
|
||||
self._replacements: dict[int, float] = {}
|
||||
self._offsets: dict[int, float] = {}
|
||||
|
||||
|
||||
|
||||
|
||||
def set_replacement(self, index: int, value: float) -> None:
|
||||
"""Force sensor `index` to read `value` for every frame this session."""
|
||||
self._replacements[index] = value
|
||||
|
||||
|
||||
def set_offset(self, index: int, value: float) -> None:
|
||||
"""Shift sensor `index` by `delta` (applied after any replacement)."""
|
||||
self._offsets[index] = value
|
||||
|
||||
|
||||
def clear(self, index: int | None = None) -> None:
|
||||
"""Remove overrides. Pass an index to clear one sensor; omit to clear all."""
|
||||
if index is None:
|
||||
@@ -27,16 +27,16 @@ class SensorEditor:
|
||||
self._offsets.pop(index, None)
|
||||
def has_overrides(self) -> bool:
|
||||
return bool(self._replacements or self._offsets)
|
||||
|
||||
|
||||
def active_indices(self) -> list[int]:
|
||||
"""Return sorted sensor indices that have any override."""
|
||||
return sorted(set(self._replacements) | set(self._offsets))
|
||||
|
||||
|
||||
|
||||
|
||||
def apply(self, values: list[float]) -> list[float]:
|
||||
"""Return a new list wit all overrides applied (original unchanged)"""
|
||||
if not self.has_overrides():
|
||||
return list(values)
|
||||
return list(values)
|
||||
result = list(values)
|
||||
for i in range(len(result)):
|
||||
v = result[i]
|
||||
@@ -45,4 +45,4 @@ class SensorEditor:
|
||||
if i in self._offsets:
|
||||
v += self._offsets[i]
|
||||
result[i] = v
|
||||
return result
|
||||
return result
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pygui.backend.models.frame import Frame
|
||||
@@ -6,6 +7,7 @@ from pygui.backend.models.frame_stats import Stats, compute_stats
|
||||
from pygui.backend.models.stability_detector import StabilityDetector
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig, classify, resolve_bounds
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SessionUpdate:
|
||||
seq: int
|
||||
@@ -15,18 +17,18 @@ class SessionUpdate:
|
||||
state: str
|
||||
target: float
|
||||
margin: float
|
||||
|
||||
|
||||
class SessionModel:
|
||||
def __init__(self, thresholds: ThresholdConfig | None = None) -> None:
|
||||
self._config = thresholds or ThresholdConfig()
|
||||
self._stability = StabilityDetector()
|
||||
|
||||
|
||||
def set_thresholds(self, config: ThresholdConfig) -> None:
|
||||
self._config = config
|
||||
|
||||
|
||||
def reset(self) -> None:
|
||||
self._stability.reset()
|
||||
|
||||
|
||||
def process(self, frame: Frame) -> SessionUpdate:
|
||||
stats = compute_stats(frame.values)
|
||||
target, margin = resolve_bounds(frame.values, self._config)
|
||||
@@ -34,6 +36,6 @@ class SessionModel:
|
||||
# State always track the user's process set_point, even in auto-color mode.
|
||||
state = self._stability.update(stats.avg, frame.time, self._config.set_point)
|
||||
return SessionUpdate(
|
||||
seq=frame.seq, values=frame.values, bands=bands,
|
||||
seq=frame.seq, values=frame.values, bands=bands,
|
||||
stats=stats, state=state, target=target, margin=margin,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Detect process state (Idle, Ramp, Set) from the running average temp"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
STATE_IDLE = "idle"
|
||||
@@ -7,28 +8,27 @@ STATE_RAMP = "ramp"
|
||||
STATE_SET = "set"
|
||||
|
||||
|
||||
class StabilityDetector:
|
||||
def __init__(self, idle_below: float = 50.0, tolerance: float = 1.0,
|
||||
class StabilityDetector:
|
||||
def __init__(self, idle_below: float = 50.0, tolerance: float = 1.0,
|
||||
settle_seconds: float = 10.0) -> None:
|
||||
self._idle_below = idle_below
|
||||
self._tolerance = tolerance
|
||||
self._settle_seconds = settle_seconds
|
||||
self._near_since: Optional[float] = None # When avg entered the +- tolerance band
|
||||
|
||||
|
||||
|
||||
|
||||
def reset(self) -> None:
|
||||
self._near_since = None
|
||||
|
||||
|
||||
def update(self, avg: float, time: float, set_point: float) ->str:
|
||||
if avg < self._idle_below:
|
||||
self._near_since = None
|
||||
return STATE_IDLE
|
||||
if abs(avg - set_point) <= self._tolerance:
|
||||
if self._near_since is None:
|
||||
self._near_since = time
|
||||
self._near_since = time
|
||||
if time - self._near_since >= self._settle_seconds:
|
||||
return STATE_SET
|
||||
return STATE_RAMP
|
||||
self._near_since = None
|
||||
return STATE_RAMP
|
||||
|
||||
@@ -4,11 +4,12 @@ Auto mode derives target=mean, margin=1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
BAND_IN = "in_range"
|
||||
BAND_HIGH = "high"
|
||||
BAND_HIGH = "high"
|
||||
BAND_LOW = "low"
|
||||
|
||||
|
||||
@@ -39,4 +40,4 @@ def classify(value: float, target: float, margin: float) -> str:
|
||||
|
||||
def classify_all(values: list[float], cfg: ThresholdConfig) -> list[str]:
|
||||
target, margin = resolve_bounds(values, cfg)
|
||||
return [classify(v, target, margin)for v in values ]
|
||||
return [classify(v, target, margin)for v in values ]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# ===== Visualization Sub-package =====
|
||||
from pygui.backend.visualization.wafer_map_item import WaferMapItem
|
||||
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment
|
||||
from pygui.backend.visualization.graph_view import GraphView
|
||||
from pygui.backend.visualization.rbf_heatmap import interpolate_field
|
||||
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment
|
||||
from pygui.backend.visualization.wafer_map_item import WaferMapItem
|
||||
|
||||
__all__ = [
|
||||
"WaferMapItem", "GraphView",
|
||||
|
||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from PySide6.QtCore import QObject, Property, Signal, Slot
|
||||
from PySide6.QtCore import Property, QObject, Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import List, Tuple, Optional
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -13,9 +13,15 @@ from __future__ import annotations
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import Property, QPoint, Signal, Slot, Qt
|
||||
from PySide6.QtCore import Property, QPoint, Qt, Signal, Slot
|
||||
from PySide6.QtGui import (
|
||||
QBrush, QColor, QFont, QImage, QPainter, QPen, QPolygon,
|
||||
QBrush,
|
||||
QColor,
|
||||
QFont,
|
||||
QImage,
|
||||
QPainter,
|
||||
QPen,
|
||||
QPolygon,
|
||||
)
|
||||
from PySide6.QtQml import QmlElement
|
||||
from PySide6.QtQuick import QQuickPaintedItem
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ===== Wafer Sub-package =====
|
||||
from pygui.backend.wafer.zwafer_models import ZWaferData, Sensor, DataRecord
|
||||
from pygui.backend.wafer.wafer_layouts import available_families, load_layout
|
||||
from pygui.backend.wafer.zwafer_models import DataRecord, Sensor, ZWaferData
|
||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser
|
||||
from pygui.backend.wafer.wafer_layouts import load_layout, available_families
|
||||
|
||||
__all__ = [
|
||||
"ZWaferData", "Sensor", "DataRecord",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Tuple, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from pygui.backend.wafer.zwafer_models import ZWaferData, Sensor
|
||||
from pygui.backend.wafer.zwafer_models import Sensor, ZWaferData
|
||||
|
||||
|
||||
# ===== Z-Wafer CSV Parser =====
|
||||
|
||||
@@ -6,7 +6,6 @@ QML via @Slot methods on a QObject controller.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import serial.tools.list_ports
|
||||
@@ -42,8 +41,8 @@ class DeviceService:
|
||||
|
||||
# On macOS/Linux, also detect virtual loopback serial ports (PTYs) opened by socat
|
||||
import platform
|
||||
import subprocess
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
if platform.system() in ("Darwin", "Linux"):
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Continuous serial frame reader (Live-mode source)"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
@@ -13,7 +14,7 @@ log = logging.getLogger(__name__)
|
||||
ParseFrame = Callable[[bytes, int], Frame]
|
||||
|
||||
class StreamReader:
|
||||
|
||||
|
||||
def __init__(self, transport, parse_frame: ParseFrame,
|
||||
on_frame: Callable[[Frame], None],
|
||||
on_error: Callable[[Exception], None] | None = None,
|
||||
@@ -26,7 +27,7 @@ class StreamReader:
|
||||
self._thread: threading.Thread | None = None
|
||||
self._stop = threading.Event()
|
||||
self.error_count = 0
|
||||
|
||||
|
||||
def start(self) -> None:
|
||||
self._stop.clear()
|
||||
self._thread = threading.Thread(target=self._run, daemon = True)
|
||||
|
||||
Reference in New Issue
Block a user