#!/usr/bin/env python3
"""
Checkweigher HMI - PyQt6 Production Application
Replaces Weintek EasyBuilder Pro HMI for Checkweigher machine.
Communicates via Modbus RTU (USB-RS485) using minimalmodbus.

Architecture:
  - Theme:        Centralized QSS constants (colors, gradients, fonts)
  - DataModel:    Dataclass holding all machine state
  - ModbusWorker: QThread for non-blocking hardware polling
  - Simulator:    QTimer-based fake data generator (Phase 1 / offline mode)
  - Widgets:      Reusable LCD, StatusBar, StatCard, IndustrialButton
  - MainWindow:   Orchestrates layout and signal/slot wiring
"""

import sys
import random
import struct
import time
from dataclasses import dataclass, field
from enum import IntEnum
from typing import Optional

from PyQt6.QtCore import (
    Qt, QTimer, QThread, pyqtSignal, QObject, QSize, QPoint
)
from PyQt6.QtGui import (
    QFont, QFontDatabase, QPainter, QColor, QLinearGradient,
    QPen, QBrush, QPalette
)
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QLabel, QPushButton,
    QVBoxLayout, QHBoxLayout, QGridLayout, QFrame, QSizePolicy,
    QSpacerItem, QStackedWidget
)

# ─────────────────────────────────────────────────────────────────────────────
# SECTION 1 — MODBUS REGISTER MAP  (derived from weintek_tags CSV)
# ─────────────────────────────────────────────────────────────────────────────
class MB:
    """Modbus register addresses (0-based for minimalmodbus)."""
    # 3x Input Registers — read only
    DEVICE_STATUS              = 8    # device.status
    DEVICE_STATE               = 11   # device.state
    CHECK_WEIGHT_VALUE         = 13   # device.checkWeightValue    (32-bit float, 2 regs)
    CHECK_WEIGHT_DEVIATION     = 15   # device.checkWeightDeviation (32-bit float)
    CHECK_WEIGHT_STATE         = 17   # device.checkWeightState
    WEIGHER_STATUS             = 18   # device.weigher.status
    WEIGHER_STATE              = 21   # device.weigher.state
    WEIGHER_CURRENT_WEIGHT     = 23   # device.weigher.currentWeight (32-bit float)
    REJECTION_STATUS           = 37   # device.rejectionSystem.status
    REJECTION_STATE            = 40   # device.rejectionSystem.state
    UNDERWEIGHT_WORK_CYCLE     = 10547 # device.rejectionSystem.underweightRejectWorkCycle (32-bit)
    OVERWEIGHT_WORK_CYCLE      = 10549 # device.rejectionSystem.overweightRejectWorkCycle  (32-bit)
    HIST_UNDERWEIGHT_COUNT     = 1113  # device.historyRecorder.underweightCount  (32-bit)
    HIST_OVERWEIGHT_COUNT      = 1115  # device.historyRecorder.overweightCount   (32-bit)
    HIST_PROPER_COUNT          = 1119  # device.historyRecorder.properCount       (32-bit)
    HIST_TOTAL_COUNT           = 1121  # device.historyRecorder.totalCount        (32-bit)
    HIST_MEAN_WEIGHT           = 1109  # device.historyRecorder.meanWeight        (32-bit float)
    HIST_STD_WEIGHT            = 1107  # device.historyRecorder.stdWeight         (32-bit float)
    HIST_MIN_WEIGHT            = 1101  # device.historyRecorder.minWeight         (32-bit float)
    HIST_MAX_WEIGHT            = 1103  # device.historyRecorder.maxWeight         (32-bit float)
    KERNEL_STATE               = 5    # kernel.kernelState
    # 4x Holding Registers — read/write
    DESIRED_WEIGHT             = 25   # device.desiredWeight    (32-bit float)
    UPPER_LIMIT_WEIGHT         = 27   # device.upperLimitWeight (32-bit float)
    LOWER_LIMIT_WEIGHT         = 29   # device.lowerLimitWeight (32-bit float)
    CMD_INPUT_BITS_0           = 889  # commandService.commandInputBits[0]


class WeighState(IntEnum):
    IDLE       = 0
    RUNNING    = 1
    OK         = 2
    OVER       = 3
    UNDER      = 4
    TARE       = 5
    ERROR      = 6


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 2 — DATA MODEL
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class MachineData:
    """Single source of truth for all machine state."""
    running:             bool  = False
    current_weight:      float = 0.0
    check_weight:        float = 0.0
    deviation:           float = 0.0
    weigh_state:         int   = WeighState.IDLE
    total_count:         int   = 0
    proper_count:        int   = 0
    overweight_count:    int   = 0
    underweight_count:   int   = 0
    mean_weight:         float = 0.0
    std_weight:          float = 0.0
    min_weight:          float = 0.0
    max_weight:          float = 0.0
    desired_weight:      float = 100.0
    upper_limit:         float = 105.0
    lower_limit:         float = 95.0
    tare_offset:         float = 0.0
    connection_ok:       bool  = False
    last_error:          str   = ""


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 3 — THEME / QSS
# ─────────────────────────────────────────────────────────────────────────────
class Theme:
    # Palette
    BG_MAIN       = "#1A2E3B"
    BG_PANEL      = "#1E3040"
    BG_PANEL_DARK = "#152535"
    BG_CARD       = "#243444"
    BORDER_LIGHT  = "#3A5A6A"
    BORDER_DARK   = "#0F1E2A"
    ACCENT_BLUE   = "#2A7FBC"
    ACCENT_CYAN   = "#00B4D8"
    LCD_GREEN     = "#00FF41"
    LCD_DIM       = "#004010"
    LCD_BG        = "#000A02"
    STATUS_OK     = "#00C853"
    STATUS_REJECT = "#D50000"
    STATUS_WARN   = "#FF6D00"
    STATUS_IDLE   = "#546E7A"
    TEXT_PRIMARY  = "#E8F4FD"
    TEXT_SECONDARY= "#7FB3C8"
    TEXT_DIM      = "#4A6A7A"
    BTN_START_TOP = "#2E7D32"
    BTN_START_BOT = "#1B5E20"
    BTN_STOP_TOP  = "#B71C1C"
    BTN_STOP_BOT  = "#7F0000"
    BTN_TARE_TOP  = "#1565C0"
    BTN_TARE_BOT  = "#0D47A1"
    BTN_RESET_TOP = "#E65100"
    BTN_RESET_BOT = "#BF360C"
    BTN_SETUP_TOP = "#4A148C"
    BTN_SETUP_BOT = "#311B92"

    FONT_MONO = "Courier New"

    @staticmethod
    def global_qss() -> str:
        return f"""
        QWidget {{
            background-color: {Theme.BG_MAIN};
            color: {Theme.TEXT_PRIMARY};
            font-family: 'Segoe UI', Arial, sans-serif;
        }}
        QFrame[frameRole="panel"] {{
            background: qlineargradient(x1:0,y1:0,x2:0,y2:1,
                stop:0 {Theme.BG_PANEL}, stop:1 {Theme.BG_PANEL_DARK});
            border: 1px solid {Theme.BORDER_LIGHT};
            border-radius: 6px;
        }}
        QFrame[frameRole="card"] {{
            background: qlineargradient(x1:0,y1:0,x2:0,y2:1,
                stop:0 {Theme.BG_CARD}, stop:1 #1A2C3A);
            border: 1px solid {Theme.BORDER_LIGHT};
            border-top: 1px solid #4A7A8A;
            border-radius: 5px;
        }}
        QLabel[role="title"] {{
            color: {Theme.TEXT_SECONDARY};
            font-size: 11px;
            font-weight: bold;
            letter-spacing: 2px;
            text-transform: uppercase;
        }}
        QLabel[role="value"] {{
            color: {Theme.TEXT_PRIMARY};
            font-size: 22px;
            font-weight: bold;
        }}
        QLabel[role="unit"] {{
            color: {Theme.TEXT_DIM};
            font-size: 13px;
        }}
        QScrollBar {{ width: 0px; height: 0px; }}
        """

    @staticmethod
    def btn_qss(top: str, bot: str) -> str:
        r, g, b = int(top[1:3],16), int(top[3:5],16), int(top[5:7],16)
        shadow = f"#{max(0,r-40):02x}{max(0,g-40):02x}{max(0,b-40):02x}"
        pressed_top = f"#{max(0,r-50):02x}{max(0,g-50):02x}{max(0,b-50):02x}"
        return f"""
        QPushButton {{
            background: qlineargradient(x1:0,y1:0,x2:0,y2:1,
                stop:0 {top}, stop:0.45 {top}, stop:0.5 {bot}, stop:1 {bot});
            color: white;
            border: 1px solid {shadow};
            border-top: 1px solid rgba(255,255,255,60);
            border-radius: 5px;
            font-size: 14px;
            font-weight: bold;
            letter-spacing: 1px;
            padding: 8px 4px;
        }}
        QPushButton:pressed {{
            background: qlineargradient(x1:0,y1:0,x2:0,y2:1,
                stop:0 {pressed_top}, stop:1 {shadow});
            border-top: 1px solid rgba(0,0,0,80);
            padding-top: 10px;
            padding-bottom: 6px;
        }}
        QPushButton:disabled {{
            background: #2A3A44;
            color: #4A6A7A;
            border: 1px solid #2A3A44;
        }}
        """

    @staticmethod
    def lcd_qss() -> str:
        return f"""
        QLabel {{
            background-color: {Theme.LCD_BG};
            color: {Theme.LCD_GREEN};
            font-family: '{Theme.FONT_MONO}';
            font-size: 72px;
            font-weight: bold;
            border: 2px inset #001505;
            border-radius: 4px;
            padding: 6px 20px;
            letter-spacing: 4px;
        }}
        """


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 4 — CUSTOM WIDGETS
# ─────────────────────────────────────────────────────────────────────────────
class LcdDisplay(QLabel):
    """Glowing green LCD-style weight display."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        self.setStyleSheet(Theme.lcd_qss())
        self.setMinimumHeight(110)
        self.setText("  0.00")
        self._glow_phase = 0
        self._glow_timer = QTimer(self)
        self._glow_timer.timeout.connect(self._pulse)
        self._active = False

    def set_weight(self, value: float):
        self.setText(f"{value:7.2f}")

    def set_active(self, active: bool):
        self._active = active
        if active:
            self._glow_timer.start(80)
        else:
            self._glow_timer.stop()
            self._set_color(Theme.LCD_GREEN)

    def _pulse(self):
        """Subtle brightness pulse when running."""
        import math
        self._glow_phase = (self._glow_phase + 1) % 20
        t = (math.sin(self._glow_phase * 3.14159 / 10) + 1) / 2
        brightness = int(180 + t * 75)
        color = f"#{0:02x}{brightness:02x}{int(brightness * 0.25):02x}"
        self._set_color(color)

    def _set_color(self, color: str):
        self.setStyleSheet(
            Theme.lcd_qss().replace(Theme.LCD_GREEN, color)
        )


class StatusBar(QFrame):
    """Full-width status indicator with gradient flash."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFixedHeight(48)
        self.setProperty("frameRole", "card")
        layout = QHBoxLayout(self)
        layout.setContentsMargins(16, 0, 16, 0)

        self._icon = QLabel("●")
        self._icon.setFixedWidth(28)
        self._icon.setFont(QFont("Arial", 18))

        self._label = QLabel("SYSTEM IDLE")
        self._label.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold))
        self._label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)

        self._right = QLabel("")
        self._right.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        self._right.setFont(QFont("Courier New", 13))
        self._right.setStyleSheet(f"color: {Theme.TEXT_SECONDARY};")

        layout.addWidget(self._icon)
        layout.addWidget(self._label, 1)
        layout.addWidget(self._right)

        self._flash_timer = QTimer(self)
        self._flash_timer.timeout.connect(self._toggle_flash)
        self._flash_visible = True
        self.set_idle()

    def _apply(self, text: str, color: str, icon_color: str, flash=False):
        self._label.setText(text)
        self._label.setStyleSheet(f"color: {icon_color}; letter-spacing: 2px;")
        self._icon.setStyleSheet(f"color: {icon_color};")
        self.setStyleSheet(f"""
            QFrame {{
                background: qlineargradient(x1:0,y1:0,x2:1,y2:0,
                    stop:0 {color}44, stop:0.6 {color}22, stop:1 transparent);
                border-left: 4px solid {color};
                border-bottom: 1px solid {color}66;
                border-top: 1px solid {Theme.BORDER_DARK};
                border-right: 1px solid {Theme.BORDER_DARK};
                border-radius: 4px;
            }}
        """)
        if flash:
            self._flash_timer.start(600)
        else:
            self._flash_timer.stop()
            self._icon.setVisible(True)

    def _toggle_flash(self):
        self._flash_visible = not self._flash_visible
        self._icon.setVisible(self._flash_visible)

    def set_idle(self):
        self._apply("SYSTEM IDLE", Theme.STATUS_IDLE, Theme.STATUS_IDLE)

    def set_running(self, weight_str=""):
        self._right.setText(weight_str)
        self._apply("● RUNNING — WEIGHING", Theme.STATUS_OK, Theme.STATUS_OK)

    def set_ok(self):
        self._apply("✔  WEIGHT OK — PASS", Theme.STATUS_OK, Theme.STATUS_OK)

    def set_overweight(self):
        self._apply("▲  OVERWEIGHT — REJECT", Theme.STATUS_REJECT, Theme.STATUS_REJECT, flash=True)

    def set_underweight(self):
        self._apply("▼  UNDERWEIGHT — REJECT", Theme.STATUS_REJECT, Theme.STATUS_REJECT, flash=True)

    def set_error(self, msg=""):
        self._apply(f"⚠  ERROR  {msg}", Theme.STATUS_WARN, Theme.STATUS_WARN, flash=True)

    def set_tare(self):
        self._apply("TARE IN PROGRESS...", Theme.ACCENT_CYAN, Theme.ACCENT_CYAN)


class StatCard(QFrame):
    """Numeric statistic card with title, large value, and optional unit."""

    def __init__(self, title: str, unit: str = "", color: str = None, parent=None):
        super().__init__(parent)
        self.setProperty("frameRole", "card")
        self._color = color or Theme.TEXT_PRIMARY

        layout = QVBoxLayout(self)
        layout.setContentsMargins(14, 10, 14, 10)
        layout.setSpacing(2)

        title_label = QLabel(title.upper())
        title_label.setProperty("role", "title")
        title_label.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-size: 10px; font-weight: bold; letter-spacing: 1.5px;")
        title_label.setAlignment(Qt.AlignmentFlag.AlignLeft)

        row = QHBoxLayout()
        self._value_label = QLabel("0")
        self._value_label.setFont(QFont("Courier New", 26, QFont.Weight.Bold))
        self._value_label.setStyleSheet(f"color: {self._color};")
        self._value_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)

        unit_label = QLabel(unit)
        unit_label.setProperty("role", "unit")
        unit_label.setStyleSheet(f"color: {Theme.TEXT_DIM}; font-size: 12px; padding-top: 6px;")
        unit_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBottom)

        row.addWidget(self._value_label, 1)
        row.addWidget(unit_label)

        layout.addWidget(title_label)
        layout.addLayout(row)

    def set_value(self, v):
        if isinstance(v, float):
            self._value_label.setText(f"{v:.2f}")
        else:
            self._value_label.setText(str(v))

    def flash_color(self, color: str):
        self._value_label.setStyleSheet(f"color: {color};")
        QTimer.singleShot(400, lambda: self._value_label.setStyleSheet(f"color: {self._color};"))


class IndustrialButton(QPushButton):
    """Touch-friendly button with 3D gradient and pressed state."""

    def __init__(self, text: str, color_top: str, color_bot: str, icon_text: str = "", parent=None):
        super().__init__(parent)
        self.setMinimumHeight(56)
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
        label = f"{icon_text}  {text}" if icon_text else text
        self.setText(label)
        self.setStyleSheet(Theme.btn_qss(color_top, color_bot))
        self.setCursor(Qt.CursorShape.PointingHandCursor)


class DeviationBar(QFrame):
    """Horizontal bar showing weight deviation from target."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFixedHeight(30)
        self._deviation = 0.0
        self._limit = 5.0

    def set_deviation(self, dev: float, limit: float = 5.0):
        self._deviation = dev
        self._limit = limit
        self.update()

    def paintEvent(self, event):
        p = QPainter(self)
        p.setRenderHint(QPainter.RenderHint.Antialiasing)
        w, h = self.width(), self.height()
        mid = w // 2

        # Background track
        p.fillRect(0, h//2-3, w, 6, QColor("#0F1E2A"))
        p.setPen(QPen(QColor(Theme.BORDER_LIGHT), 1))
        p.drawRect(0, h//2-3, w-1, 5)

        # Center mark
        p.setPen(QPen(QColor(Theme.TEXT_DIM), 2))
        p.drawLine(mid, 4, mid, h-4)

        # Limit markers
        if self._limit > 0:
            lp = int(mid + (self._limit / (self._limit * 2)) * mid * 0.9)
            rp = int(mid - (self._limit / (self._limit * 2)) * mid * 0.9)
            p.setPen(QPen(QColor(Theme.STATUS_WARN), 1, Qt.PenStyle.DashLine))
            p.drawLine(lp, 2, lp, h-2)
            p.drawLine(rp, 2, rp, h-2)

        # Deviation indicator
        norm = max(-1.0, min(1.0, self._deviation / (self._limit if self._limit else 1.0)))
        bar_x = int(mid + norm * mid * 0.85)
        color = QColor(Theme.STATUS_OK) if abs(norm) < 0.8 else QColor(Theme.STATUS_REJECT)
        p.setPen(Qt.PenStyle.NoPen)
        p.setBrush(QBrush(color))
        p.drawEllipse(bar_x - 7, h//2 - 7, 14, 14)
        p.end()


class ClockLabel(QLabel):
    """Real-time clock label."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-family: 'Courier New'; font-size: 13px;")
        self.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        t = QTimer(self)
        t.timeout.connect(self._tick)
        t.start(1000)
        self._tick()

    def _tick(self):
        from datetime import datetime
        self.setText(datetime.now().strftime("  %H:%M:%S   %d/%m/%Y"))


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 5 — SIMULATOR (Phase 1 — no hardware needed)
# ─────────────────────────────────────────────────────────────────────────────
class Simulator(QObject):
    """
    Generates realistic checkweigher data using a QTimer.
    Emits new_data signal every 800 ms when running.
    """
    new_data = pyqtSignal(object)  # emits MachineData

    def __init__(self, model: MachineData, parent=None):
        super().__init__(parent)
        self._model = model
        self._timer = QTimer(self)
        self._timer.timeout.connect(self._tick)
        self._sample_counter = 0

    def start(self):
        self._model.running = True
        self._model.connection_ok = True   # sim is always "connected"
        self._timer.start(800)

    def stop(self):
        self._model.running = False
        self._timer.stop()
        self._model.weigh_state = WeighState.IDLE
        self.new_data.emit(self._model)

    def tare(self):
        self._model.tare_offset = random.uniform(-0.5, 0.5)
        self._model.weigh_state = WeighState.TARE
        self.new_data.emit(self._model)
        QTimer.singleShot(1200, self._finish_tare)

    def _finish_tare(self):
        self._model.weigh_state = WeighState.IDLE if not self._model.running else WeighState.RUNNING
        self.new_data.emit(self._model)

    def reset_stats(self):
        self._model.total_count     = 0
        self._model.proper_count    = 0
        self._model.overweight_count= 0
        self._model.underweight_count=0
        self._model.mean_weight     = 0.0
        self._model.std_weight      = 0.0
        self._model.min_weight      = 0.0
        self._model.max_weight      = 0.0
        self._sample_counter        = 0
        self.new_data.emit(self._model)

    def _tick(self):
        # Simulate a normally-distributed weight with occasional outliers
        base = self._model.desired_weight
        sigma = 1.8
        w = random.gauss(base, sigma)
        # 5% chance of deliberate outlier
        if random.random() < 0.05:
            w += random.choice([-1, 1]) * random.uniform(6, 12)
        w = round(w - self._model.tare_offset, 2)

        self._model.check_weight   = w
        self._model.current_weight = w
        self._model.deviation      = w - base
        self._model.weigh_state    = WeighState.RUNNING

        # Classify
        if w > self._model.upper_limit:
            self._model.weigh_state    = WeighState.OVER
            self._model.overweight_count += 1
        elif w < self._model.lower_limit:
            self._model.weigh_state    = WeighState.UNDER
            self._model.underweight_count += 1
        else:
            self._model.weigh_state = WeighState.OK
            self._model.proper_count += 1

        self._model.total_count += 1
        n = self._model.total_count
        prev_mean = self._model.mean_weight
        self._model.mean_weight += (w - prev_mean) / n
        # Welford's online std
        self._sample_counter += 1
        if self._sample_counter == 1:
            self._model.min_weight = w
            self._model.max_weight = w
            self._M2 = 0.0
        else:
            delta = w - prev_mean
            delta2 = w - self._model.mean_weight
            self._M2 = getattr(self, '_M2', 0.0) + delta * delta2
            self._model.std_weight = (self._M2 / (n - 1)) ** 0.5 if n > 1 else 0.0
            self._model.min_weight = min(self._model.min_weight, w)
            self._model.max_weight = max(self._model.max_weight, w)

        self.new_data.emit(self._model)


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 6 — MODBUS WORKER THREAD (Phase 2 — real hardware)
# ─────────────────────────────────────────────────────────────────────────────
class ModbusWorker(QThread):
    """
    Polls the checkweigher controller via Modbus RTU in a background thread.
    Set USE_SIMULATION=True (in main) to bypass this entirely.
    """
    new_data   = pyqtSignal(object)
    error_sig  = pyqtSignal(str)

    def __init__(self, port: str, slave_id: int, model: MachineData, parent=None):
        super().__init__(parent)
        self._port     = port
        self._slave    = slave_id
        self._model    = model
        self._running  = False
        self._inst     = None

    def run(self):
        try:
            import minimalmodbus
            self._inst = minimalmodbus.Instrument(self._port, self._slave)
            self._inst.serial.baudrate = 115200
            self._inst.serial.timeout  = 0.3
            self._inst.mode = minimalmodbus.MODE_RTU
            self._model.connection_ok = True
        except Exception as e:
            self.error_sig.emit(f"Modbus connect failed: {e}")
            self._model.connection_ok = False
            return

        self._running = True
        while self._running:
            try:
                self._poll()
                self.new_data.emit(self._model)
            except Exception as e:
                self._model.connection_ok = False
                self.error_sig.emit(str(e))
            time.sleep(0.1)

    def _read_float(self, reg: int) -> float:
        """Read a 32-bit IEEE float from two consecutive input registers."""
        regs = self._inst.read_registers(reg - 1, 2, functioncode=4)
        raw = (regs[0] << 16) | regs[1]
        return struct.unpack('>f', struct.pack('>I', raw))[0]

    def _read_long(self, reg: int) -> int:
        """Read a 32-bit unsigned int from two consecutive input registers."""
        regs = self._inst.read_registers(reg - 1, 2, functioncode=4)
        return (regs[0] << 16) | regs[1]

    def _read_reg(self, reg: int) -> int:
        return self._inst.read_register(reg - 1, functioncode=4)

    def _poll(self):
        m = self._model
        m.current_weight  = self._read_float(MB.WEIGHER_CURRENT_WEIGHT)
        m.check_weight    = self._read_float(MB.CHECK_WEIGHT_VALUE)
        m.deviation       = self._read_float(MB.CHECK_WEIGHT_DEVIATION)
        m.weigh_state     = self._read_reg(MB.CHECK_WEIGHT_STATE)
        m.total_count     = self._read_long(MB.HIST_TOTAL_COUNT)
        m.proper_count    = self._read_long(MB.HIST_PROPER_COUNT)
        m.overweight_count= self._read_long(MB.HIST_OVERWEIGHT_COUNT)
        m.underweight_count=self._read_long(MB.HIST_UNDERWEIGHT_COUNT)
        m.mean_weight     = self._read_float(MB.HIST_MEAN_WEIGHT)
        m.std_weight      = self._read_float(MB.HIST_STD_WEIGHT)
        m.min_weight      = self._read_float(MB.HIST_MIN_WEIGHT)
        m.max_weight      = self._read_float(MB.HIST_MAX_WEIGHT)
        m.connection_ok   = True

    def stop_worker(self):
        self._running = False
        self.wait(2000)


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 7 — LEFT PANEL
# ─────────────────────────────────────────────────────────────────────────────
class LeftPanel(QFrame):
    """60% panel: LCD weight, status bar, deviation bar, control buttons."""

    sig_start = pyqtSignal()
    sig_stop  = pyqtSignal()
    sig_tare  = pyqtSignal()
    sig_reset = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setProperty("frameRole", "panel")
        self._build()

    def _build(self):
        root = QVBoxLayout(self)
        root.setContentsMargins(12, 12, 8, 12)
        root.setSpacing(8)

        # ── Header row ──────────────────────────────────────────────────────
        hdr = QHBoxLayout()
        title = QLabel("CHECKWEIGHER")
        title.setStyleSheet(f"""
            color: {Theme.ACCENT_CYAN};
            font-size: 13px;
            font-weight: bold;
            letter-spacing: 3px;
        """)
        self._conn_label = QLabel("⬤ SIM")
        self._conn_label.setStyleSheet(f"color: {Theme.STATUS_WARN}; font-size: 11px; font-weight: bold;")
        hdr.addWidget(title)
        hdr.addStretch()
        hdr.addWidget(self._conn_label)
        root.addLayout(hdr)

        # ── Weight label ─────────────────────────────────────────────────────
        wlabel_row = QHBoxLayout()
        wlabel = QLabel("NET WEIGHT")
        wlabel.setStyleSheet(f"color: {Theme.TEXT_DIM}; font-size: 10px; letter-spacing: 2px;")
        self._unit_label = QLabel("g")
        self._unit_label.setStyleSheet(f"color: {Theme.LCD_DIM}; font-size: 14px; font-weight: bold;")
        self._unit_label.setAlignment(Qt.AlignmentFlag.AlignRight)
        wlabel_row.addWidget(wlabel)
        wlabel_row.addStretch()
        wlabel_row.addWidget(self._unit_label)
        root.addLayout(wlabel_row)

        # ── LCD Display ───────────────────────────────────────────────────────
        self.lcd = LcdDisplay()
        root.addWidget(self.lcd)

        # ── Limits row ────────────────────────────────────────────────────────
        limits_row = QHBoxLayout()
        self._lo_label = QLabel("▼ LOW: 95.00 g")
        self._lo_label.setStyleSheet(f"color: {Theme.STATUS_WARN}; font-size: 11px; font-weight: bold;")
        self._tgt_label = QLabel("◎ TGT: 100.00 g")
        self._tgt_label.setStyleSheet(f"color: {Theme.TEXT_SECONDARY}; font-size: 11px;")
        self._tgt_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
        self._hi_label = QLabel("HIGH: 105.00 g ▲")
        self._hi_label.setStyleSheet(f"color: {Theme.STATUS_WARN}; font-size: 11px; font-weight: bold;")
        self._hi_label.setAlignment(Qt.AlignmentFlag.AlignRight)
        limits_row.addWidget(self._lo_label)
        limits_row.addStretch()
        limits_row.addWidget(self._tgt_label)
        limits_row.addStretch()
        limits_row.addWidget(self._hi_label)
        root.addLayout(limits_row)

        # ── Deviation bar ─────────────────────────────────────────────────────
        dev_label = QLabel("DEVIATION FROM TARGET")
        dev_label.setStyleSheet(f"color: {Theme.TEXT_DIM}; font-size: 9px; letter-spacing: 2px;")
        root.addWidget(dev_label)
        self.dev_bar = DeviationBar()
        root.addWidget(self.dev_bar)

        # ── Status bar ────────────────────────────────────────────────────────
        self.status_bar = StatusBar()
        root.addWidget(self.status_bar)

        # ── Mini stats row ────────────────────────────────────────────────────
        mini_row = QHBoxLayout()
        mini_row.setSpacing(6)

        self._mean_card  = self._mini_card("MEAN",   "g",  Theme.ACCENT_CYAN)
        self._std_card   = self._mini_card("STD DEV","g",  Theme.TEXT_SECONDARY)
        self._min_card   = self._mini_card("MIN",    "g",  Theme.STATUS_WARN)
        self._max_card   = self._mini_card("MAX",    "g",  Theme.STATUS_WARN)

        for c in [self._mean_card, self._std_card, self._min_card, self._max_card]:
            mini_row.addWidget(c)
        root.addLayout(mini_row)

        root.addStretch()

        # ── Control buttons ───────────────────────────────────────────────────
        separator = QFrame()
        separator.setFrameShape(QFrame.Shape.HLine)
        separator.setStyleSheet(f"color: {Theme.BORDER_LIGHT};")
        root.addWidget(separator)

        btn_row1 = QHBoxLayout()
        btn_row1.setSpacing(8)
        self.btn_start = IndustrialButton("START",  Theme.BTN_START_TOP, Theme.BTN_START_BOT, "▶")
        self.btn_stop  = IndustrialButton("STOP",   Theme.BTN_STOP_TOP,  Theme.BTN_STOP_BOT,  "■")
        self.btn_stop.setEnabled(False)
        btn_row1.addWidget(self.btn_start)
        btn_row1.addWidget(self.btn_stop)

        btn_row2 = QHBoxLayout()
        btn_row2.setSpacing(8)
        self.btn_tare  = IndustrialButton("TARE",   Theme.BTN_TARE_TOP,  Theme.BTN_TARE_BOT,  "⊖")
        self.btn_reset = IndustrialButton("RESET",  Theme.BTN_RESET_TOP, Theme.BTN_RESET_BOT, "↺")
        btn_row2.addWidget(self.btn_tare)
        btn_row2.addWidget(self.btn_reset)

        root.addLayout(btn_row1)
        root.addLayout(btn_row2)

        # Wire buttons
        self.btn_start.clicked.connect(self.sig_start)
        self.btn_stop.clicked.connect(self.sig_stop)
        self.btn_tare.clicked.connect(self.sig_tare)
        self.btn_reset.clicked.connect(self.sig_reset)

    def _mini_card(self, title: str, unit: str, color: str) -> QFrame:
        f = QFrame()
        f.setProperty("frameRole", "card")
        v = QVBoxLayout(f)
        v.setContentsMargins(8, 6, 8, 6)
        v.setSpacing(0)
        tl = QLabel(title)
        tl.setStyleSheet(f"color: {Theme.TEXT_DIM}; font-size: 9px; letter-spacing: 1px;")
        tl.setAlignment(Qt.AlignmentFlag.AlignCenter)
        vl = QLabel("0.00")
        vl.setStyleSheet(f"color: {color}; font-size: 13px; font-weight: bold; font-family: 'Courier New';")
        vl.setAlignment(Qt.AlignmentFlag.AlignCenter)
        ul = QLabel(unit)
        ul.setStyleSheet(f"color: {Theme.TEXT_DIM}; font-size: 9px;")
        ul.setAlignment(Qt.AlignmentFlag.AlignCenter)
        v.addWidget(tl)
        v.addWidget(vl)
        v.addWidget(ul)
        f._value_label = vl
        return f

    # ── Public update slot ───────────────────────────────────────────────────
    def update_data(self, d: MachineData):
        self.lcd.set_weight(d.current_weight)
        self.dev_bar.set_deviation(d.deviation, d.upper_limit - d.desired_weight)
        self.lcd.set_active(d.running)

        # Connection indicator
        if d.connection_ok and not d.running:
            self._conn_label.setText("⬤ CONNECTED")
            self._conn_label.setStyleSheet(f"color: {Theme.STATUS_OK}; font-size: 11px; font-weight: bold;")
        elif d.running:
            self._conn_label.setText("⬤ LIVE")
            self._conn_label.setStyleSheet(f"color: {Theme.LCD_GREEN}; font-size: 11px; font-weight: bold;")

        # Limits
        self._lo_label.setText(f"▼ LOW: {d.lower_limit:.2f} g")
        self._tgt_label.setText(f"◎ TGT: {d.desired_weight:.2f} g")
        self._hi_label.setText(f"HIGH: {d.upper_limit:.2f} g ▲")

        # Status bar
        state = d.weigh_state
        if state == WeighState.IDLE or not d.running:
            self.status_bar.set_idle()
        elif state == WeighState.TARE:
            self.status_bar.set_tare()
        elif state == WeighState.OK:
            self.status_bar.set_ok()
        elif state == WeighState.OVER:
            self.status_bar.set_overweight()
        elif state == WeighState.UNDER:
            self.status_bar.set_underweight()
        else:
            self.status_bar.set_running()

        # Mini stat cards
        self._mean_card._value_label.setText(f"{d.mean_weight:.2f}")
        self._std_card._value_label.setText(f"{d.std_weight:.2f}")
        self._min_card._value_label.setText(f"{d.min_weight:.2f}" if d.min_weight else "—")
        self._max_card._value_label.setText(f"{d.max_weight:.2f}" if d.max_weight else "—")

        # Button states
        self.btn_start.setEnabled(not d.running)
        self.btn_stop.setEnabled(d.running)
        self.btn_tare.setEnabled(True)

    def set_sim_mode(self, sim: bool):
        if sim:
            self._conn_label.setText("⬤ SIMULATION")
            self._conn_label.setStyleSheet(f"color: {Theme.STATUS_WARN}; font-size: 11px; font-weight: bold;")


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 8 — RIGHT PANEL
# ─────────────────────────────────────────────────────────────────────────────
class RightPanel(QFrame):
    """40% panel: statistics, product info, and Setup button."""

    sig_setup = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setProperty("frameRole", "panel")
        self._build()

    def _build(self):
        root = QVBoxLayout(self)
        root.setContentsMargins(8, 12, 12, 12)
        root.setSpacing(8)

        # ── Header ────────────────────────────────────────────────────────────
        hdr = QHBoxLayout()
        title = QLabel("STATISTICS")
        title.setStyleSheet(f"color: {Theme.ACCENT_CYAN}; font-size: 13px; font-weight: bold; letter-spacing: 3px;")
        self._clock = ClockLabel()
        hdr.addWidget(title)
        hdr.addStretch()
        hdr.addWidget(self._clock)
        root.addLayout(hdr)

        # ── Separator ─────────────────────────────────────────────────────────
        sep = QFrame()
        sep.setFrameShape(QFrame.Shape.HLine)
        sep.setStyleSheet(f"color: {Theme.BORDER_LIGHT};")
        root.addWidget(sep)

        # ── Primary stat cards (2-col grid) ──────────────────────────────────
        grid = QGridLayout()
        grid.setSpacing(6)

        self._total_card = StatCard("Total Samples",   "",  Theme.TEXT_PRIMARY)
        self._ok_card    = StatCard("Pass (OK)",        "",  Theme.STATUS_OK)
        self._over_card  = StatCard("Overweight",       "",  Theme.STATUS_REJECT)
        self._under_card = StatCard("Underweight",      "",  Theme.STATUS_REJECT)

        grid.addWidget(self._total_card, 0, 0)
        grid.addWidget(self._ok_card,    0, 1)
        grid.addWidget(self._over_card,  1, 0)
        grid.addWidget(self._under_card, 1, 1)
        root.addLayout(grid)

        # ── Yield / Reject rate ───────────────────────────────────────────────
        rate_frame = QFrame()
        rate_frame.setProperty("frameRole", "card")
        rate_layout = QHBoxLayout(rate_frame)
        rate_layout.setContentsMargins(14, 8, 14, 8)

        yield_col = QVBoxLayout()
        yield_title = QLabel("YIELD RATE")
        yield_title.setStyleSheet(f"color: {Theme.TEXT_DIM}; font-size: 10px; letter-spacing: 1px;")
        self._yield_label = QLabel("—")
        self._yield_label.setFont(QFont("Courier New", 28, QFont.Weight.Bold))
        self._yield_label.setStyleSheet(f"color: {Theme.STATUS_OK};")
        yield_col.addWidget(yield_title)
        yield_col.addWidget(self._yield_label)

        reject_col = QVBoxLayout()
        reject_col.setAlignment(Qt.AlignmentFlag.AlignRight)
        reject_title = QLabel("REJECT RATE")
        reject_title.setStyleSheet(f"color: {Theme.TEXT_DIM}; font-size: 10px; letter-spacing: 1px; text-align: right;")
        reject_title.setAlignment(Qt.AlignmentFlag.AlignRight)
        self._reject_label = QLabel("—")
        self._reject_label.setFont(QFont("Courier New", 28, QFont.Weight.Bold))
        self._reject_label.setStyleSheet(f"color: {Theme.STATUS_REJECT};")
        self._reject_label.setAlignment(Qt.AlignmentFlag.AlignRight)
        reject_col.addWidget(reject_title)
        reject_col.addWidget(self._reject_label)

        rate_layout.addLayout(yield_col, 1)
        rate_layout.addLayout(reject_col, 1)
        root.addWidget(rate_frame)

        # ── Weight histogram bar (simple) ─────────────────────────────────────
        hist_frame = QFrame()
        hist_frame.setProperty("frameRole", "card")
        hist_layout = QVBoxLayout(hist_frame)
        hist_layout.setContentsMargins(10, 8, 10, 8)
        hist_title = QLabel("WEIGHT DISTRIBUTION")
        hist_title.setStyleSheet(f"color: {Theme.TEXT_DIM}; font-size: 9px; letter-spacing: 2px;")
        self._hist_widget = HistogramWidget()
        hist_layout.addWidget(hist_title)
        hist_layout.addWidget(self._hist_widget)
        root.addWidget(hist_frame)

        # ── Product info card ─────────────────────────────────────────────────
        prod_frame = QFrame()
        prod_frame.setProperty("frameRole", "card")
        prod_layout = QGridLayout(prod_frame)
        prod_layout.setContentsMargins(14, 8, 14, 8)
        prod_layout.setHorizontalSpacing(8)
        prod_layout.setVerticalSpacing(4)

        def _kv(k, v_init="—"):
            kl = QLabel(k)
            kl.setStyleSheet(f"color: {Theme.TEXT_DIM}; font-size: 10px;")
            vl = QLabel(v_init)
            vl.setStyleSheet(f"color: {Theme.TEXT_PRIMARY}; font-size: 12px; font-weight: bold; font-family: 'Courier New';")
            vl.setAlignment(Qt.AlignmentFlag.AlignRight)
            return kl, vl

        k1, self._prd_name   = _kv("Product:")
        k2, self._prd_target = _kv("Target Weight:")
        k3, self._prd_tol    = _kv("Tolerance:")
        k4, self._prd_last   = _kv("Last Weight:")

        prod_layout.addWidget(k1, 0, 0);  prod_layout.addWidget(self._prd_name,   0, 1)
        prod_layout.addWidget(k2, 1, 0);  prod_layout.addWidget(self._prd_target, 1, 1)
        prod_layout.addWidget(k3, 2, 0);  prod_layout.addWidget(self._prd_tol,    2, 1)
        prod_layout.addWidget(k4, 3, 0);  prod_layout.addWidget(self._prd_last,   3, 1)
        root.addWidget(prod_frame)

        root.addStretch()

        # ── Setup button ──────────────────────────────────────────────────────
        sep2 = QFrame()
        sep2.setFrameShape(QFrame.Shape.HLine)
        sep2.setStyleSheet(f"color: {Theme.BORDER_LIGHT};")
        root.addWidget(sep2)

        self.btn_setup = IndustrialButton("SETTING UP", Theme.BTN_SETUP_TOP, Theme.BTN_SETUP_BOT, "⚙")
        self.btn_setup.setMinimumHeight(56)
        root.addWidget(self.btn_setup)
        self.btn_setup.clicked.connect(self.sig_setup)

    # ── Public update slot ───────────────────────────────────────────────────
    def update_data(self, d: MachineData):
        self._total_card.set_value(d.total_count)
        self._ok_card.set_value(d.proper_count)
        self._over_card.set_value(d.overweight_count)
        self._under_card.set_value(d.underweight_count)

        if d.total_count > 0:
            yield_r  = d.proper_count / d.total_count * 100
            reject_r = (d.overweight_count + d.underweight_count) / d.total_count * 100
            self._yield_label.setText(f"{yield_r:.1f}%")
            self._reject_label.setText(f"{reject_r:.1f}%")
        else:
            self._yield_label.setText("—")
            self._reject_label.setText("—")

        self._prd_target.setText(f"{d.desired_weight:.2f} g")
        tol = d.upper_limit - d.desired_weight
        self._prd_tol.setText(f"± {tol:.2f} g")
        self._prd_last.setText(f"{d.check_weight:.2f} g")
        self._hist_widget.push(d.check_weight, d.lower_limit, d.upper_limit)

        # Flash reject cards on new rejects
        if d.weigh_state == WeighState.OVER:
            self._over_card.flash_color(Theme.STATUS_REJECT)
        elif d.weigh_state == WeighState.UNDER:
            self._under_card.flash_color(Theme.STATUS_REJECT)

    def set_product_name(self, name: str):
        self._prd_name.setText(name)


class HistogramWidget(QFrame):
    """Mini live histogram of recent 40 weight samples."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFixedHeight(60)
        self._samples = []
        self._lo = 95.0
        self._hi = 105.0

    def push(self, value: float, lo: float, hi: float):
        self._lo = lo
        self._hi = hi
        self._samples.append(value)
        if len(self._samples) > 60:
            self._samples.pop(0)
        self.update()

    def paintEvent(self, event):
        if not self._samples:
            return
        p = QPainter(self)
        p.setRenderHint(QPainter.RenderHint.Antialiasing)
        w, h = self.width(), self.height()
        n = len(self._samples)
        bar_w = max(3, w // n)
        span = (self._hi - self._lo) * 1.5
        center = (self._hi + self._lo) / 2
        vmin = center - span / 2
        vmax = center + span / 2
        rng = vmax - vmin if vmax != vmin else 1.0

        for i, val in enumerate(self._samples):
            bh = int(((val - vmin) / rng) * (h - 6)) + 3
            bh = max(2, min(h - 2, bh))
            x = i * (w / n)
            if val > self._hi:
                color = QColor(Theme.STATUS_REJECT)
            elif val < self._lo:
                color = QColor(Theme.STATUS_WARN)
            else:
                age = i / max(n - 1, 1)
                g = int(60 + age * 195)
                color = QColor(0, g, int(g * 0.25))
            p.fillRect(int(x) + 1, h - bh, max(2, int(bar_w) - 1), bh - 1, color)

        # Limit lines
        def y_for(v):
            return h - int(((v - vmin) / rng) * (h - 6)) - 3

        p.setPen(QPen(QColor(Theme.STATUS_WARN), 1, Qt.PenStyle.DashLine))
        p.drawLine(0, y_for(self._hi), w, y_for(self._hi))
        p.drawLine(0, y_for(self._lo), w, y_for(self._lo))
        p.end()


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 9 — TITLE BAR
# ─────────────────────────────────────────────────────────────────────────────
class TitleBar(QWidget):
    """Custom frameless title bar with machine info."""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFixedHeight(36)
        self.setStyleSheet(f"""
            QWidget {{
                background: qlineargradient(x1:0,y1:0,x2:1,y2:0,
                    stop:0 #0F1E2A, stop:0.5 #162535, stop:1 #0F1E2A);
                border-bottom: 1px solid {Theme.BORDER_LIGHT};
            }}
        """)
        layout = QHBoxLayout(self)
        layout.setContentsMargins(14, 0, 14, 0)

        logo = QLabel("⚖")
        logo.setStyleSheet(f"color: {Theme.ACCENT_CYAN}; font-size: 18px;")

        machine_name = QLabel("CHECKWEIGHER CONTROL SYSTEM  |  CW-2000")
        machine_name.setStyleSheet(f"color: {Theme.TEXT_PRIMARY}; font-size: 12px; font-weight: bold; letter-spacing: 2px;")

        mode_badge = QLabel("● SIMULATION MODE")
        mode_badge.setObjectName("mode_badge")
        mode_badge.setStyleSheet(f"""
            color: {Theme.STATUS_WARN};
            font-size: 10px;
            font-weight: bold;
            background: #1A2E00;
            border: 1px solid {Theme.STATUS_WARN};
            border-radius: 3px;
            padding: 2px 8px;
        """)

        layout.addWidget(logo)
        layout.addSpacing(8)
        layout.addWidget(machine_name)
        layout.addStretch()
        layout.addWidget(mode_badge)

        # Drag support
        self._drag_pos = None

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self._drag_pos = event.globalPosition().toPoint() - self.window().frameGeometry().topLeft()

    def mouseMoveEvent(self, event):
        if self._drag_pos and event.buttons() & Qt.MouseButton.LeftButton:
            self.window().move(event.globalPosition().toPoint() - self._drag_pos)


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 10 — MAIN WINDOW
# ─────────────────────────────────────────────────────────────────────────────
class MainWindow(QMainWindow):

    # ── Toggle this to True for real hardware ───────────────────────────────
    USE_SIMULATION = True
    MODBUS_PORT    = "/dev/ttyUSB0"
    MODBUS_SLAVE   = 1

    def __init__(self):
        super().__init__()
        self._model = MachineData()
        self._model.desired_weight = 100.0
        self._model.upper_limit    = 105.0
        self._model.lower_limit    = 95.0

        self._setup_window()
        self._build_ui()
        self._connect_signals()

        if self.USE_SIMULATION:
            self._sim = Simulator(self._model, self)
            self._sim.new_data.connect(self._on_new_data)
            self.left.set_sim_mode(True)
        else:
            self._worker = ModbusWorker(
                self.MODBUS_PORT, self.MODBUS_SLAVE, self._model, self
            )
            self._worker.new_data.connect(self._on_new_data)
            self._worker.error_sig.connect(self._on_error)
            self._worker.start()

    def _setup_window(self):
        self.setWindowTitle("Checkweigher HMI")
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        self.showFullScreen()
        self.setMinimumSize(QSize(1024, 600))
        self.setStyleSheet(Theme.global_qss())

    def _build_ui(self):
        central = QWidget()
        self.setCentralWidget(central)
        root = QVBoxLayout(central)
        root.setContentsMargins(0, 0, 0, 0)
        root.setSpacing(0)

        # Title bar
        self.title_bar = TitleBar(self)
        root.addWidget(self.title_bar)

        # Main content
        content = QHBoxLayout()
        content.setContentsMargins(8, 8, 8, 8)
        content.setSpacing(8)

        self.left  = LeftPanel(self)
        self.right = RightPanel(self)
        self.right.set_product_name("PRODUCT A")

        content.addWidget(self.left,  60)
        content.addWidget(self.right, 40)
        root.addLayout(content, 1)

    def _connect_signals(self):
        self.left.sig_start.connect(self._on_start)
        self.left.sig_stop.connect(self._on_stop)
        self.left.sig_tare.connect(self._on_tare)
        self.left.sig_reset.connect(self._on_reset)
        self.right.sig_setup.connect(self._on_setup)

    # ── Slots ─────────────────────────────────────────────────────────────────
    def _on_start(self):
        if self.USE_SIMULATION:
            self._sim.start()
        else:
            # In real mode: write START command to Modbus
            pass  # TODO: self._worker.write_command(CMD_START)
        self._model.running = True
        self.left.update_data(self._model)

    def _on_stop(self):
        if self.USE_SIMULATION:
            self._sim.stop()
        self._model.running = False
        self.left.update_data(self._model)

    def _on_tare(self):
        if self.USE_SIMULATION:
            self._sim.tare()

    def _on_reset(self):
        if self.USE_SIMULATION:
            self._sim.reset_stats()
        self.right.update_data(self._model)
        self.left.update_data(self._model)

    def _on_setup(self):
        # Placeholder — open settings dialog in future phases
        self.left.status_bar.set_error("Settings page not yet implemented")
        QTimer.singleShot(3000, self.left.status_bar.set_idle)

    def _on_new_data(self, data: MachineData):
        self.left.update_data(data)
        self.right.update_data(data)

    def _on_error(self, msg: str):
        self._model.connection_ok = False
        self.left.status_bar.set_error(msg[:40])

    def keyPressEvent(self, event):
        # ESC exits fullscreen for development; remove for production
        if event.key() == Qt.Key.Key_Escape:
            self.close()


# ─────────────────────────────────────────────────────────────────────────────
# SECTION 11 — ENTRY POINT
# ─────────────────────────────────────────────────────────────────────────────
def main():
    app = QApplication(sys.argv)
    app.setStyle("Fusion")

    # Force dark palette at Qt level (fallback for any un-styled widgets)
    palette = QPalette()
    palette.setColor(QPalette.ColorRole.Window,      QColor(Theme.BG_MAIN))
    palette.setColor(QPalette.ColorRole.WindowText,  QColor(Theme.TEXT_PRIMARY))
    palette.setColor(QPalette.ColorRole.Base,        QColor(Theme.BG_PANEL))
    palette.setColor(QPalette.ColorRole.Text,        QColor(Theme.TEXT_PRIMARY))
    palette.setColor(QPalette.ColorRole.Button,      QColor(Theme.BG_CARD))
    palette.setColor(QPalette.ColorRole.ButtonText,  QColor(Theme.TEXT_PRIMARY))
    palette.setColor(QPalette.ColorRole.Highlight,   QColor(Theme.ACCENT_BLUE))
    app.setPalette(palette)

    win = MainWindow()
    win.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
