// ============================================================
//  RobotController.java  v3.0  [UI Reskin: Black/White/Pink/Purple]
//
//  Transport: USB Serial (jSerialComm) -> Master ESP32 -> ESP-NOW -> Slave
//
//  SETUP:
//    1. Download jSerialComm JAR:
//       https://github.com/Fazecast/jSerialComm/releases
//       (jSerialComm-2.10.4.jar)
//    2. Compile:
//       Windows: javac -cp .;jSerialComm-2.10.4.jar RobotController.java
//       Linux:   javac -cp .:jSerialComm-2.10.4.jar RobotController.java
//    3. Run:
//       Windows: java -cp .;jSerialComm-2.10.4.jar RobotController
//       Linux:   java -cp .:jSerialComm-2.10.4.jar RobotController
//
//  SERIAL PROTOCOL (Master firmware v2.0):
//    GUI -> Master (outbound, newline-terminated):
//      CMD:F / CMD:B / CMD:L / CMD:R / CMD:S
//      SPEED:0-255
//      MODE:MANUAL / MODE:AUTO / MODE:SUMO / MODE:MAZE
//      AUX:EYES:ON|OFF
//      AUX:CHARGE:ON|OFF   AUX:CHARGE1-3:ON|OFF
//      PING   TEL?
//
//    Master -> GUI (inbound):
//      TEL:key=value     -- one telemetry field per line
//      PONG              -- ping reply
//      MASTER_READY      -- master boot confirmation
//      ERR:...           -- error string
//
//  SLAVE MODE SYNC:
//    On every TEL:mode= line received, the GUI silently updates the
//    mode toggle to match the slave's actual mode WITHOUT sending a
//    MODE: command back.
// ============================================================

import com.fazecast.jSerialComm.SerialPort;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import javax.swing.JFileChooser;
import javax.swing.JProgressBar;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.JToggleButton;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;

public class RobotControllernew extends JFrame {

    // =========================================================================
    // THEME PALETTE (Black / White / Pink / Purple)
    // =========================================================================
    private static final Color BG_DARK = new Color(10, 10, 15);
    private static final Color BG_PANEL = new Color(22, 18, 32);
    private static final Color BG_CARD = new Color(32, 26, 48);
    private static final Color BG_INPUT = new Color(18, 14, 28);
    private static final Color PINK = new Color(255, 85, 170);
    private static final Color PINK_DARK = new Color(140, 30, 90);
    private static final Color PURPLE = new Color(160, 80, 255);
    private static final Color PURPLE_DIM = new Color(110, 50, 200);
    private static final Color PURPLE_DARK = new Color(70, 30, 140);
    private static final Color WHITE = new Color(240, 235, 255);
    private static final Color WHITE_DIM = new Color(170, 160, 200);
    private static final Color BORDER_SUBTLE = new Color(80, 50, 120);
    private static final Color BORDER_GLOW = new Color(120, 60, 180);
    private static final Color GREEN_ACCENT = new Color(80, 255, 160);
    private static final Color RED_ACCENT = new Color(255, 80, 100);
    private static final Color CYAN_ACCENT = new Color(80, 200, 255);

    // THEME FONTS
    private static final Font FONT_TITLE = new Font(Font.SANS_SERIF, Font.BOLD, 13);
    private static final Font FONT_BODY = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
    private static final Font FONT_BOLD = new Font(Font.SANS_SERIF, Font.BOLD, 12);
    private static final Font FONT_MONO = new Font(Font.MONOSPACED, Font.PLAIN, 12);
    private static final Font FONT_MONO_B = new Font(Font.MONOSPACED, Font.BOLD, 12);
    private static final Font FONT_BTN = new Font(Font.SANS_SERIF, Font.BOLD, 12);
    private static final Font FONT_BTN_LG = new Font(Font.SANS_SERIF, Font.BOLD, 13);

    // =========================================================================
    // ROBOT MODE ENUM
    // =========================================================================
    private enum RobotMode {
        MANUAL("Manual/Hockey Mode", "MODE:MANUAL"),
        AUTO("Auto/Unique Mode", "MODE:AUTO"),
        SUMO("Auto/Sumo Bot Mode", "MODE:SUMO"),
        SUMO_NL("Sumo Bot Mode(no line sensor)", "MODE:SUMO_NL"),
        MAZE("Auto/Maze Solver Mode", "MODE:MAZE"),
        OTA("OTA Update Mode", "MODE:OTA");

        final String label;
        final String command;

        RobotMode(String label, String command) {
            this.label = label;
            this.command = command;
        }

        static RobotMode fromFirmwareString(String s) {
            if (s == null)
                return null;
            switch (s.trim().toUpperCase()) {
                case "MANUAL":  return MANUAL;
                case "AUTO":    return AUTO;
                case "SUMO":    return SUMO;
                case "SUMO_NL": return SUMO_NL;
                case "MAZE":    return MAZE;
                case "OTA":     return OTA;
                default:        return null;
            }
        }
    }

    // =========================================================================
    // CONSTANTS
    // =========================================================================
    private static final int BAUD_RATE = 115200;
    private static final int HELD_COMMAND_INTERVAL_MS = 30;
    private static final int TEL_REQUEST_INTERVAL_MS = 800;
    private static final int WINDOW_WIDTH = 1420;
    private static final int WINDOW_HEIGHT = 820;
    private static final String TEL_PREFIX = "TEL:";

    // =========================================================================
    // CONNECTION UI
    // =========================================================================
    private final JComboBox<String> portCombo = new JComboBox<>();
    private final JButton refreshPortsBtn = new JButton("\u21BA");
    private final JButton connectBtn = new JButton("Connect");
    private final JLabel connectedLabel = new JLabel("\u25CF Disconnected");

    // =========================================================================
    // MODE TOGGLE BUTTONS
    // =========================================================================
    private final JToggleButton manualModeButton = new JToggleButton(RobotMode.MANUAL.label);
    private final JToggleButton autoModeButton   = new JToggleButton(RobotMode.AUTO.label);
    private final JToggleButton sumoModeButton   = new JToggleButton(RobotMode.SUMO.label);
    private final JToggleButton sumoNlModeButton = new JToggleButton(RobotMode.SUMO_NL.label);
    private final JToggleButton mazeModeButton   = new JToggleButton(RobotMode.MAZE.label);
    private final JToggleButton otaModeButton    = new JToggleButton(RobotMode.OTA.label);

    // =========================================================================
    // OTA STATE & UI COMPONENTS
    // =========================================================================
    private volatile File      otaBinFile       = null;
    private final JProgressBar otaProgressBar   = new JProgressBar(0, 100);
    private final JLabel       otaStatusLbl     = new JLabel("Waiting for OTA activity...");
    private final JLabel       otaFileLbl       = new JLabel("No file selected");
    private final JLabel       otaPingLbl       = new JLabel("Not checked");
    private final JButton      otaPickBtn       = new JButton("\uD83D\uDCC2  Select .bin");
    private final JButton      otaUploadBtn     = new JButton("\u2B06  Upload Firmware");
    private final JButton      otaPingBtn       = new JButton("\u26A1  Ping Rover");

    // CardLayout to swap center panel between normal controls and OTA panel
    private CardLayout centerCards;
    private JPanel     centerCardHost;

    // =========================================================================
    // SPEED
    // =========================================================================
    private final JSlider speedSlider = new JSlider(0, 255, 180);
    private final JLabel speedValueLabel = new JLabel("180");

    // =========================================================================
    // MOVEMENT BUTTONS
    // =========================================================================
    private final JButton forwardButton = new JButton("\u25B2 Forward");
    private final JButton backwardButton = new JButton("\u25BC Backward");
    private final JButton leftButton = new JButton("\u25C0 Left");
    private final JButton rightButton = new JButton("Right \u25B6");
    private final JButton stopButton = new JButton("\u25A0 STOP");

    // =========================================================================
    // LIGHTS BUTTONS
    // =========================================================================
    private final JButton eyesOnButton = new JButton("Eyes ON");
    private final JButton eyesOffButton = new JButton("Eyes OFF");
    private final JButton chargeOnButton = new JButton("All Charge ON");
    private final JButton chargeOffButton = new JButton("All Charge OFF");
    private final JButton charge1OnButton = new JButton("Charge 1 ON");
    private final JButton charge1OffButton = new JButton("Charge 1 OFF");
    private final JButton charge2OnButton = new JButton("Charge 2 ON");
    private final JButton charge2OffButton = new JButton("Charge 2 OFF");
    private final JButton charge3OnButton = new JButton("Charge 3 ON");
    private final JButton charge3OffButton = new JButton("Charge 3 OFF");

    // =========================================================================
    // LIVE SENSOR LABELS
    // =========================================================================
    private final JLabel sonar1Label = new JLabel("-- cm");
    private final JLabel sonar2Label = new JLabel("-- cm");
    private final JLabel sonar3Label = new JLabel("-- cm");
    private final JLabel line1Label = new JLabel("--");
    private final JLabel line2Label = new JLabel("--");
    private final JLabel irLabel = new JLabel("--");

    // =========================================================================
    // STATUS / TELEMETRY / LOG
    // =========================================================================
    private final JLabel connectionStatusLabel = new JLabel("Status: Disconnected");
    private final JTextArea telemetryArea = new JTextArea(12, 30);
    private final JTextArea logArea = new JTextArea(12, 30);

    // =========================================================================
    // SERIAL STATE
    // =========================================================================
    private volatile boolean connected = false;
    private volatile SerialPort activePort = null;

    // =========================================================================
    // SCHEDULING
    // =========================================================================
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3, r -> {
        Thread t = new Thread(r, "robot-worker");
        t.setDaemon(true);
        return t;
    });

    private ScheduledFuture<?> telRequestTask;
    private ScheduledFuture<?> heldCommandTask;

    private final AtomicReference<String> heldCommand = new AtomicReference<>("S");
    private final AtomicBoolean sendingHeldCommand = new AtomicBoolean(false);

    // =========================================================================
    // MODE TRACKING
    // =========================================================================
    private volatile RobotMode currentMode = RobotMode.MANUAL;
    private final AtomicBoolean firstTelAfterConnect = new AtomicBoolean(false);

    // =========================================================================
    // CONSTRUCTOR
    // =========================================================================
    public RobotControllernew() {
        super("ESP32 Robot Controller");
        applyGlobalLAF();
        initUi();
        initActions();
        initKeyBindings();
        populatePortList();
    }

    // =========================================================================
    // GLOBAL L&F DEFAULTS
    // =========================================================================
    private void applyGlobalLAF() {
        UIManager.put("Panel.background", BG_PANEL);
        UIManager.put("Label.foreground", WHITE);
        UIManager.put("Label.font", FONT_BODY);
        UIManager.put("ComboBox.background", BG_INPUT);
        UIManager.put("ComboBox.foreground", WHITE);
        UIManager.put("ComboBox.selectionBackground", PURPLE_DIM);
        UIManager.put("ComboBox.selectionForeground", WHITE);
        UIManager.put("ComboBox.font", FONT_BODY);
        UIManager.put("ScrollPane.background", BG_INPUT);
        UIManager.put("SplitPane.background", BG_PANEL);
        UIManager.put("Slider.background", BG_CARD);
        UIManager.put("ToolTip.background", BG_CARD);
        UIManager.put("ToolTip.foreground", PINK);
        UIManager.put("ToolTip.font", FONT_BODY);
    }

    // =========================================================================
    // UI BUILD
    // =========================================================================
    private void initUi() {
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        getContentPane().setBackground(BG_DARK);

        JPanel root = new JPanel(new BorderLayout(12, 12));
        root.setBackground(BG_DARK);
        root.setBorder(new EmptyBorder(14, 14, 10, 14));
        setContentPane(root);

        JPanel topPanel = new JPanel(new BorderLayout(12, 10));
        topPanel.setBackground(BG_DARK);
        topPanel.add(buildConnectionPanel(), BorderLayout.NORTH);
        topPanel.add(buildModeAndSpeedPanel(), BorderLayout.CENTER);

        connectionStatusLabel.setFont(FONT_BOLD);
        connectionStatusLabel.setForeground(WHITE_DIM);
        connectionStatusLabel.setBorder(new EmptyBorder(4, 6, 2, 0));

        telemetryArea.setEditable(false);
        telemetryArea.setFont(FONT_MONO);
        telemetryArea.setBackground(BG_INPUT);
        telemetryArea.setForeground(new Color(180, 255, 200));
        telemetryArea.setCaretColor(PINK);
        telemetryArea.setBorder(new EmptyBorder(6, 8, 6, 8));

        logArea.setEditable(false);
        logArea.setFont(FONT_MONO);
        logArea.setBackground(BG_INPUT);
        logArea.setForeground(WHITE_DIM);
        logArea.setCaretColor(PINK);
        logArea.setBorder(new EmptyBorder(6, 8, 6, 8));

        root.add(topPanel, BorderLayout.NORTH);

        // CardLayout: swap between normal robot controls and OTA panel
        centerCards    = new CardLayout();
        centerCardHost = new JPanel(centerCards);
        centerCardHost.setBackground(BG_DARK);
        centerCardHost.add(buildCenterPanel(), "NORMAL");
        centerCardHost.add(buildOtaPanel(),    "OTA");
        centerCards.show(centerCardHost, "NORMAL");

        root.add(centerCardHost, BorderLayout.CENTER);
        root.add(buildRightPanel(), BorderLayout.EAST);
        root.add(connectionStatusLabel, BorderLayout.SOUTH);

        setPreferredSize(new Dimension(WINDOW_WIDTH, WINDOW_HEIGHT));
        pack();
        setMinimumSize(new Dimension(WINDOW_WIDTH, WINDOW_HEIGHT));
        setLocationRelativeTo(null);
    }

    // =========================================================================
    // CONNECTION PANEL
    // =========================================================================
    private JPanel buildConnectionPanel() {
        JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 6));
        panel.setBackground(BG_CARD);
        panel.setBorder(makeRoundedTitled("Serial Connection", PINK));

        styleComboBox(portCombo);
        portCombo.setPreferredSize(new Dimension(165, 30));

        stylePrimaryButton(refreshPortsBtn, PURPLE, WHITE);
        refreshPortsBtn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 16));
        refreshPortsBtn.setToolTipText("Refresh port list");
        refreshPortsBtn.setPreferredSize(new Dimension(34, 30));

        stylePrimaryButton(connectBtn, PINK, WHITE);
        connectBtn.setPreferredSize(new Dimension(110, 30));

        connectedLabel.setForeground(RED_ACCENT);
        connectedLabel.setFont(FONT_BOLD);

        JLabel portLbl = makeLabel("Port:", WHITE_DIM);
        panel.add(portLbl);
        panel.add(portCombo);
        panel.add(refreshPortsBtn);
        panel.add(connectBtn);
        panel.add(connectedLabel);
        return panel;
    }

    // =========================================================================
    // MODE + SPEED PANEL
    // =========================================================================
    private JPanel buildModeAndSpeedPanel() {
        JPanel panel = new JPanel(new BorderLayout(12, 8));
        panel.setBackground(BG_CARD);
        panel.setBorder(makeRoundedTitled("Mode / Speed", PURPLE));

        ButtonGroup group = new ButtonGroup();
        group.add(manualModeButton);
        group.add(autoModeButton);
        group.add(sumoModeButton);
        group.add(sumoNlModeButton);
        group.add(mazeModeButton);
        group.add(otaModeButton);
        manualModeButton.setSelected(true);

        Dimension d = new Dimension(200, 36);
        styleModeToggle(manualModeButton, d);
        styleModeToggle(autoModeButton, d);
        styleModeToggle(sumoModeButton, d);
        styleModeToggle(sumoNlModeButton, d);
        styleModeToggle(mazeModeButton, d);
        styleModeToggle(otaModeButton, d);

        JPanel modeGrid = new JPanel(new GridLayout(3, 2, 8, 8));
        modeGrid.setBackground(BG_CARD);
        modeGrid.add(manualModeButton);
        modeGrid.add(autoModeButton);
        modeGrid.add(sumoModeButton);
        modeGrid.add(sumoNlModeButton);   // fills former blank cell — 3×2 grid stays balanced
        modeGrid.add(mazeModeButton);
        modeGrid.add(otaModeButton);

        JPanel modePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
        modePanel.setBackground(BG_CARD);
        modePanel.add(makeLabel("Mode:", PURPLE));
        modePanel.add(modeGrid);

        speedSlider.setMajorTickSpacing(51);
        speedSlider.setMinorTickSpacing(5);
        speedSlider.setPaintTicks(true);
        speedSlider.setPaintLabels(true);
        speedSlider.setBackground(BG_CARD);
        speedSlider.setForeground(WHITE_DIM);
        speedSlider.setFont(FONT_BODY);

        speedValueLabel.setFont(new Font(Font.MONOSPACED, Font.BOLD, 16));
        speedValueLabel.setForeground(PINK);

        JPanel speedTop = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0));
        speedTop.setBackground(BG_CARD);
        speedTop.add(makeLabel("Speed:", PINK));
        speedTop.add(speedValueLabel);

        JPanel speedPanel = new JPanel(new BorderLayout(8, 4));
        speedPanel.setBackground(BG_CARD);
        speedPanel.add(speedTop, BorderLayout.NORTH);
        speedPanel.add(speedSlider, BorderLayout.CENTER);

        panel.add(modePanel, BorderLayout.NORTH);
        panel.add(speedPanel, BorderLayout.CENTER);
        return panel;
    }

    // =========================================================================
    // CENTER PANEL
    // =========================================================================
    private JPanel buildCenterPanel() {
        JPanel panel = new JPanel(new BorderLayout(12, 12));
        panel.setBackground(BG_DARK);
        panel.add(buildControlPanel(), BorderLayout.CENTER);
        panel.add(buildAccessoryPanel(), BorderLayout.SOUTH);
        return panel;
    }

    // =========================================================================
    // D-PAD PANEL
    // =========================================================================
    private JPanel buildControlPanel() {
        JPanel panel = new JPanel(new BorderLayout(12, 12));
        panel.setBackground(BG_CARD);
        panel.setBorder(makeRoundedTitled("Manual / Hockey Movement", PINK));

        JPanel dpad = new JPanel(new GridBagLayout());
        dpad.setBackground(BG_CARD);
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(8, 8, 8, 8);
        gbc.fill = GridBagConstraints.BOTH;
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;

        Dimension bs = new Dimension(130, 60);
        styleDpadButton(forwardButton, bs, PINK);
        styleDpadButton(backwardButton, bs, PINK);
        styleDpadButton(leftButton, bs, PINK);
        styleDpadButton(rightButton, bs, PINK);
        styleStopButton(stopButton, bs);

        gbc.gridx = 1;
        gbc.gridy = 0;
        dpad.add(forwardButton, gbc);
        gbc.gridx = 0;
        gbc.gridy = 1;
        dpad.add(leftButton, gbc);
        gbc.gridx = 1;
        gbc.gridy = 1;
        dpad.add(stopButton, gbc);
        gbc.gridx = 2;
        gbc.gridy = 1;
        dpad.add(rightButton, gbc);
        gbc.gridx = 1;
        gbc.gridy = 2;
        dpad.add(backwardButton, gbc);

        JTextArea help = new JTextArea(
                "Keyboard:\nW / \u2191 = Forward\nS / \u2193 = Backward\n" +
                        "A / \u2190 = Left\nD / \u2192 = Right\nSpace = Stop\n\n" +
                        "Hold key/button to keep moving.\nRelease to send STOP.\n\n" +
                        "Controls active in\nManual / Hockey mode only.");
        help.setEditable(false);
        help.setFocusable(false);
        help.setFont(FONT_BODY);
        help.setBackground(BG_INPUT);
        help.setForeground(WHITE_DIM);
        help.setBorder(new CompoundBorder(
                BorderFactory.createLineBorder(BORDER_SUBTLE, 1, true),
                new EmptyBorder(6, 8, 6, 8)));

        panel.add(dpad, BorderLayout.CENTER);
        panel.add(help, BorderLayout.SOUTH);
        return panel;
    }

    // =========================================================================
    // LIGHTS PANEL
    // =========================================================================
    private JPanel buildAccessoryPanel() {
        JPanel panel = new JPanel(new BorderLayout(10, 10));
        panel.setBackground(BG_CARD);
        panel.setBorder(makeRoundedTitled("Manual Lights", PURPLE));

        Dimension bs = new Dimension(170, 40);
        JPanel top = new JPanel(new GridLayout(2, 2, 8, 8));
        top.setBackground(BG_CARD);
        styleLightButton(eyesOnButton, bs, PURPLE, true);
        styleLightButton(eyesOffButton, bs, PURPLE, false);
        styleLightButton(chargeOnButton, bs, PINK, true);
        styleLightButton(chargeOffButton, bs, PINK, false);
        top.add(eyesOnButton);
        top.add(eyesOffButton);
        top.add(chargeOnButton);
        top.add(chargeOffButton);

        JPanel ind = new JPanel(new GridLayout(3, 2, 8, 8));
        ind.setBackground(BG_CARD);
        ind.setBorder(makeRoundedTitled("Individual Charge LEDs", BORDER_GLOW));
        styleLightButton(charge1OnButton, bs, PURPLE, true);
        styleLightButton(charge1OffButton, bs, PURPLE, false);
        styleLightButton(charge2OnButton, bs, PURPLE, true);
        styleLightButton(charge2OffButton, bs, PURPLE, false);
        styleLightButton(charge3OnButton, bs, PURPLE, true);
        styleLightButton(charge3OffButton, bs, PURPLE, false);
        ind.add(charge1OnButton);
        ind.add(charge1OffButton);
        ind.add(charge2OnButton);
        ind.add(charge2OffButton);
        ind.add(charge3OnButton);
        ind.add(charge3OffButton);

        panel.add(top, BorderLayout.NORTH);
        panel.add(ind, BorderLayout.CENTER);
        return panel;
    }

    // =========================================================================
    // OTA PANEL
    // =========================================================================
    private JPanel buildOtaPanel() {
        JPanel panel = new JPanel(new BorderLayout(12, 12));
        panel.setBackground(BG_DARK);
        panel.setBorder(makeRoundedTitled("OTA Firmware Update", PINK));

        // Info block
        JLabel info = new JLabel("<html>" +
            "<b style='color:#a050ff'>How to flash new firmware:</b><br><br>" +
            "1. Click the <b>OTA Update Mode</b> button above<br>" +
            "2. Slave opens Wi-Fi hotspot &nbsp;<b style='color:#ff55aa'>ESP32-Robot-OTA</b>" +
            "&nbsp; password: <b style='color:#ff55aa'>12345678</b><br>" +
            "3. Connect <i>this PC</i> to that hotspot (disconnect from internet is OK)<br>" +
            "4. Ping the rover to confirm the server is up<br>" +
            "5. Select your compiled <b>.bin</b> file and upload<br>" +
            "6. Rover reboots automatically when done</html>");
        info.setFont(FONT_BODY);
        info.setForeground(WHITE_DIM);
        info.setBorder(new CompoundBorder(
                BorderFactory.createLineBorder(BORDER_SUBTLE, 1, true),
                new EmptyBorder(12, 14, 12, 14)));
        info.setBackground(BG_CARD);
        info.setOpaque(true);
        panel.add(info, BorderLayout.NORTH);

        // Controls block
        JPanel controls = new JPanel();
        controls.setLayout(new javax.swing.BoxLayout(controls, javax.swing.BoxLayout.Y_AXIS));
        controls.setBackground(BG_DARK);
        controls.setBorder(new EmptyBorder(10, 6, 6, 6));

        // Ping row
        JPanel pingRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 4));
        pingRow.setBackground(BG_DARK);
        stylePrimaryButton(otaPingBtn, PURPLE, WHITE);
        otaPingBtn.setPreferredSize(new Dimension(170, 34));
        otaPingLbl.setFont(FONT_MONO);
        otaPingLbl.setForeground(WHITE_DIM);
        otaPingBtn.addActionListener(e -> pingOtaRover());
        pingRow.add(otaPingBtn);
        pingRow.add(otaPingLbl);
        controls.add(pingRow);

        // File pick row
        JPanel fileRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 4));
        fileRow.setBackground(BG_DARK);
        stylePrimaryButton(otaPickBtn, PURPLE_DIM, WHITE);
        otaPickBtn.setPreferredSize(new Dimension(170, 34));
        otaFileLbl.setFont(FONT_MONO);
        otaFileLbl.setForeground(WHITE_DIM);
        otaPickBtn.addActionListener(e -> {
            JFileChooser fc = new JFileChooser();
            fc.setDialogTitle("Select firmware .bin");
            fc.setFileFilter(
                new javax.swing.filechooser.FileNameExtensionFilter("Firmware (*.bin)", "bin"));
            if (fc.showOpenDialog(RobotControllernew.this) == JFileChooser.APPROVE_OPTION) {
                otaBinFile = fc.getSelectedFile();
                otaFileLbl.setText(otaBinFile.getName() +
                    "  (" + (otaBinFile.length() / 1024) + " KB)");
                otaFileLbl.setForeground(PINK);
                otaUploadBtn.setEnabled(true);
                appendLog("[OTA] Selected: " + otaBinFile.getAbsolutePath());
            }
        });
        fileRow.add(otaPickBtn);
        fileRow.add(otaFileLbl);
        controls.add(fileRow);

        // Upload button row
        JPanel uploadRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 4));
        uploadRow.setBackground(BG_DARK);
        stylePrimaryButton(otaUploadBtn, PINK, WHITE);
        otaUploadBtn.setPreferredSize(new Dimension(210, 36));
        otaUploadBtn.setEnabled(false);
        otaUploadBtn.addActionListener(e -> startOtaUpload());
        uploadRow.add(otaUploadBtn);
        controls.add(uploadRow);

        // Progress bar
        otaProgressBar.setStringPainted(true);
        otaProgressBar.setString("0%");
        otaProgressBar.setFont(FONT_MONO_B);
        otaProgressBar.setForeground(PINK);
        otaProgressBar.setBackground(BG_INPUT);
        otaProgressBar.setBorder(BorderFactory.createLineBorder(BORDER_SUBTLE, 1, true));
        otaProgressBar.setPreferredSize(new Dimension(420, 28));
        otaProgressBar.setMaximumSize(new Dimension(Integer.MAX_VALUE, 28));
        JPanel progressRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 4));
        progressRow.setBackground(BG_DARK);
        progressRow.add(otaProgressBar);
        controls.add(progressRow);

        // Status label
        otaStatusLbl.setFont(FONT_MONO);
        otaStatusLbl.setForeground(WHITE_DIM);
        JPanel statusRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 2));
        statusRow.setBackground(BG_DARK);
        statusRow.add(otaStatusLbl);
        controls.add(statusRow);

        panel.add(controls, BorderLayout.CENTER);
        return panel;
    }

    // Ping the rover OTA HTTP server over Wi-Fi
    private void pingOtaRover() {
        otaPingLbl.setText("Pinging...");
        otaPingLbl.setForeground(new Color(255, 180, 80));
        appendLog("[OTA] Pinging http://192.168.4.1/update ...");
        new Thread(() -> {
            boolean ok = false;
            try {
                HttpURLConnection conn = (HttpURLConnection)
                    URI.create("http://192.168.4.1/update").toURL().openConnection();
                conn.setConnectTimeout(2500);
                conn.setReadTimeout(2500);
                conn.setRequestMethod("GET");
                ok = conn.getResponseCode() < 500;
                conn.disconnect();
            } catch (Exception ignored) {}
            final boolean reachable = ok;
            SwingUtilities.invokeLater(() -> {
                if (reachable) {
                    otaPingLbl.setText("\u25CF Rover reachable — ready to upload");
                    otaPingLbl.setForeground(GREEN_ACCENT);
                    appendLog("[OTA] Rover OTA server is UP");
                } else {
                    otaPingLbl.setText("\u2717 Not reachable — connect to ESP32-Robot-OTA first");
                    otaPingLbl.setForeground(RED_ACCENT);
                    appendLog("[OTA] Rover OTA server unreachable");
                }
            });
        }, "ota-ping").start();
    }

    // HTTP multipart POST firmware .bin to rover
    private void startOtaUpload() {
        if (otaBinFile == null || !otaBinFile.exists()) {
            appendLog("[OTA] No file selected or file missing");
            return;
        }
        otaUploadBtn.setEnabled(false);
        otaPickBtn.setEnabled(false);
        otaProgressBar.setValue(0);
        otaProgressBar.setString("0%");
        otaStatusLbl.setText("Uploading...");
        otaStatusLbl.setForeground(new Color(255, 180, 80));
        appendLog("[OTA] Starting upload: " + otaBinFile.getName());

        new Thread(() -> {
            try {
                String boundary = "----OTABoundary" + System.currentTimeMillis();
                HttpURLConnection conn = (HttpURLConnection)
                    URI.create("http://192.168.4.1/update").toURL().openConnection();
                conn.setDoOutput(true);
                conn.setRequestMethod("POST");
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(60000);
                conn.setRequestProperty("Content-Type",
                    "multipart/form-data; boundary=" + boundary);

                byte[] header = ("--" + boundary + "\r\n" +
                    "Content-Disposition: form-data; name=\"update\";" +
                    " filename=\"firmware.bin\"\r\n" +
                    "Content-Type: application/octet-stream\r\n\r\n").getBytes();
                byte[] footer = ("\r\n--" + boundary + "--\r\n").getBytes();
                long total = header.length + otaBinFile.length() + footer.length;
                conn.setFixedLengthStreamingMode(total);

                try (OutputStream out = conn.getOutputStream();
                     FileInputStream fis = new FileInputStream(otaBinFile)) {
                    out.write(header);
                    byte[] buf = new byte[4096];
                    long sent = header.length;
                    int n;
                    while ((n = fis.read(buf)) != -1) {
                        out.write(buf, 0, n);
                        sent += n;
                        final int pct = (int) ((sent * 100L) / total);
                        SwingUtilities.invokeLater(() -> {
                            otaProgressBar.setValue(pct);
                            otaProgressBar.setString(pct + "%");
                        });
                    }
                    out.write(footer);
                }

                int code = conn.getResponseCode();
                conn.disconnect();
                final String result = (code == 200)
                    ? "Upload complete — rover rebooting"
                    : "Upload failed (HTTP " + code + ")";
                final Color col = (code == 200) ? GREEN_ACCENT : RED_ACCENT;
                SwingUtilities.invokeLater(() -> {
                    otaStatusLbl.setText(result);
                    otaStatusLbl.setForeground(col);
                    appendLog("[OTA] " + result);
                    otaPickBtn.setEnabled(true);
                    otaUploadBtn.setEnabled(otaBinFile != null);
                });
            } catch (Exception ex) {
                final String err = "Upload error: " + ex.getMessage();
                SwingUtilities.invokeLater(() -> {
                    otaStatusLbl.setText(err);
                    otaStatusLbl.setForeground(RED_ACCENT);
                    appendLog("[OTA] " + err);
                    otaPickBtn.setEnabled(true);
                    otaUploadBtn.setEnabled(otaBinFile != null);
                });
            }
        }, "ota-upload").start();
    }

    // =========================================================================
    // RIGHT PANEL
    // =========================================================================
    private JPanel buildRightPanel() {
        JPanel panel = new JPanel(new BorderLayout(12, 12));
        panel.setBackground(BG_DARK);
        panel.add(buildSensorPanel(), BorderLayout.NORTH);

        JPanel telPanel = new JPanel(new BorderLayout());
        telPanel.setBackground(BG_CARD);
        telPanel.setBorder(makeRoundedTitled("Telemetry", new Color(80, 255, 200)));
        telPanel.add(styledScrollPane(telemetryArea), BorderLayout.CENTER);

        JPanel logPanel = new JPanel(new BorderLayout());
        logPanel.setBackground(BG_CARD);
        logPanel.setBorder(makeRoundedTitled("Log", WHITE_DIM));
        logPanel.add(styledScrollPane(logArea), BorderLayout.CENTER);

        JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, telPanel, logPanel);
        split.setResizeWeight(0.55);
        split.setPreferredSize(new Dimension(390, 400));
        split.setBackground(BG_DARK);
        split.setBorder(null);
        split.setDividerSize(6);

        panel.add(split, BorderLayout.CENTER);
        return panel;
    }

    // =========================================================================
    // SENSOR PANEL
    // =========================================================================
    private JPanel buildSensorPanel() {
        JPanel panel = new JPanel(new GridBagLayout());
        panel.setBackground(BG_CARD);
        panel.setBorder(makeRoundedTitled("Live Sensors", PINK));

        GridBagConstraints gc = new GridBagConstraints();
        gc.insets = new Insets(4, 10, 4, 10);
        gc.anchor = GridBagConstraints.WEST;
        gc.fill = GridBagConstraints.HORIZONTAL;
        gc.weightx = 1.0;

        sonar1Label.setFont(FONT_MONO_B);
        sonar1Label.setForeground(CYAN_ACCENT);
        sonar2Label.setFont(FONT_MONO_B);
        sonar2Label.setForeground(CYAN_ACCENT);
        sonar3Label.setFont(FONT_MONO_B);
        sonar3Label.setForeground(CYAN_ACCENT);
        line1Label.setFont(FONT_MONO_B);
        line1Label.setForeground(GREEN_ACCENT);
        line2Label.setFont(FONT_MONO_B);
        line2Label.setForeground(GREEN_ACCENT);
        irLabel.setFont(FONT_MONO_B);
        irLabel.setForeground(PURPLE);

        addSensorRow(panel, gc, 0, "Sonar Front :", sonar1Label, CYAN_ACCENT);
        addSensorRow(panel, gc, 1, "Sonar Left  :", sonar2Label, CYAN_ACCENT);
        addSensorRow(panel, gc, 2, "Sonar Right :", sonar3Label, CYAN_ACCENT);
        addSensorRow(panel, gc, 3, "Line Front  :", line1Label, GREEN_ACCENT);
        addSensorRow(panel, gc, 4, "Line Back   :", line2Label, GREEN_ACCENT);
        addSensorRow(panel, gc, 5, "IR Front    :", irLabel, PURPLE);
        return panel;
    }

    private void addSensorRow(JPanel p, GridBagConstraints gc, int row,
            String lbl, JLabel val, Color labelColor) {
        JLabel key = makeLabel(lbl, labelColor);
        gc.gridx = 0;
        gc.gridy = row;
        p.add(key, gc);
        gc.gridx = 1;
        p.add(val, gc);
    }

    // =========================================================================
    // STYLE HELPERS
    // =========================================================================
    private Border makeRoundedTitled(String title, Color titleColor) {
        Border line = BorderFactory.createLineBorder(BORDER_SUBTLE, 1, true);
        Border padded = new CompoundBorder(line, new EmptyBorder(8, 10, 8, 10));
        TitledBorder tb = BorderFactory.createTitledBorder(padded, "  " + title + "  ");
        tb.setTitleColor(titleColor);
        tb.setTitleFont(FONT_TITLE);
        return tb;
    }

    private JLabel makeLabel(String text, Color color) {
        JLabel lbl = new JLabel(text);
        lbl.setFont(FONT_BOLD);
        lbl.setForeground(color);
        return lbl;
    }

    private void stylePrimaryButton(JButton btn, Color bg, Color fg) {
        btn.setFont(FONT_BTN);
        btn.setBackground(bg);
        btn.setForeground(fg);
        btn.setFocusPainted(false);
        btn.setBorderPainted(false);
        btn.setOpaque(true);
        btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        btn.setBorder(new CompoundBorder(
                BorderFactory.createLineBorder(bg.darker(), 1, true),
                new EmptyBorder(4, 14, 4, 14)));
        Color hoverBg = bg.brighter();
        btn.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                btn.setBackground(hoverBg);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                btn.setBackground(bg);
            }
        });
    }

    private void styleDpadButton(JButton btn, Dimension size, Color accent) {
        btn.setPreferredSize(size);
        btn.setFont(FONT_BTN_LG);
        Color baseBg = new Color(45, 30, 70);
        btn.setBackground(baseBg);
        btn.setForeground(accent);
        btn.setFocusPainted(false);
        btn.setOpaque(true);
        btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        btn.setBorder(new CompoundBorder(
                BorderFactory.createLineBorder(BORDER_GLOW, 1, true),
                new EmptyBorder(4, 8, 4, 8)));
        btn.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                btn.setBackground(PURPLE_DARK);
                btn.setForeground(WHITE);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                btn.setBackground(baseBg);
                btn.setForeground(accent);
            }
        });
    }

    private void styleStopButton(JButton btn, Dimension size) {
        btn.setPreferredSize(size);
        btn.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 14));
        Color baseBg = new Color(120, 20, 40);
        Color baseFg = new Color(255, 120, 140);
        btn.setBackground(baseBg);
        btn.setForeground(baseFg);
        btn.setFocusPainted(false);
        btn.setOpaque(true);
        btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        btn.setBorder(new CompoundBorder(
                BorderFactory.createLineBorder(RED_ACCENT, 1, true),
                new EmptyBorder(4, 8, 4, 8)));
        btn.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                btn.setBackground(new Color(180, 30, 60));
                btn.setForeground(WHITE);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                btn.setBackground(baseBg);
                btn.setForeground(baseFg);
            }
        });
    }

    private void styleLightButton(JButton btn, Dimension size, Color accent, boolean isOn) {
        btn.setPreferredSize(size);
        btn.setFont(FONT_BTN);
        Color baseBg = isOn ? new Color(40, 25, 65) : new Color(28, 20, 42);
        Color baseFg = isOn ? accent : WHITE_DIM;
        btn.setBackground(baseBg);
        btn.setForeground(baseFg);
        btn.setFocusPainted(false);
        btn.setOpaque(true);
        btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        btn.setBorder(new CompoundBorder(
                BorderFactory.createLineBorder(isOn ? accent.darker() : BORDER_SUBTLE, 1, true),
                new EmptyBorder(4, 8, 4, 8)));
        btn.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                btn.setBackground(PURPLE_DARK);
                btn.setForeground(WHITE);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                btn.setBackground(baseBg);
                btn.setForeground(baseFg);
            }
        });
    }

    private void styleModeToggle(JToggleButton btn, Dimension size) {
        btn.setPreferredSize(size);
        btn.setFont(FONT_BTN);
        btn.setBackground(new Color(38, 28, 60));
        btn.setForeground(WHITE_DIM);
        btn.setFocusPainted(false);
        btn.setOpaque(true);
        btn.setCursor(new Cursor(Cursor.HAND_CURSOR));
        btn.setBorder(new CompoundBorder(
                BorderFactory.createLineBorder(BORDER_SUBTLE, 1, true),
                new EmptyBorder(4, 8, 4, 8)));
        btn.addChangeListener(e -> {
            if (btn.isSelected()) {
                btn.setBackground(PURPLE_DIM);
                btn.setForeground(WHITE);
                btn.setBorder(new CompoundBorder(
                        BorderFactory.createLineBorder(PINK, 1, true),
                        new EmptyBorder(4, 8, 4, 8)));
            } else {
                btn.setBackground(new Color(38, 28, 60));
                btn.setForeground(WHITE_DIM);
                btn.setBorder(new CompoundBorder(
                        BorderFactory.createLineBorder(BORDER_SUBTLE, 1, true),
                        new EmptyBorder(4, 8, 4, 8)));
            }
        });
    }

    private void styleComboBox(JComboBox<String> box) {
        box.setBackground(BG_INPUT);
        box.setForeground(WHITE);
        box.setFont(FONT_BODY);
        box.setBorder(BorderFactory.createLineBorder(BORDER_SUBTLE, 1, true));
    }

    private JScrollPane styledScrollPane(JTextArea area) {
        JScrollPane sp = new JScrollPane(area);
        sp.setBackground(BG_INPUT);
        sp.setBorder(BorderFactory.createLineBorder(BORDER_SUBTLE, 1, true));
        sp.getVerticalScrollBar().setBackground(BG_DARK);
        sp.getHorizontalScrollBar().setBackground(BG_DARK);
        return sp;
    }

    // =========================================================================
    // ACTIONS (logic unchanged)
    // =========================================================================
    private void initActions() {
        refreshPortsBtn.addActionListener(e -> populatePortList());
        connectBtn.addActionListener(e -> toggleConnection());

        manualModeButton.addActionListener(e -> userSwitchMode(RobotMode.MANUAL));
        autoModeButton.addActionListener(e -> userSwitchMode(RobotMode.AUTO));
        sumoModeButton.addActionListener(e -> userSwitchMode(RobotMode.SUMO));
        sumoNlModeButton.addActionListener(e -> userSwitchMode(RobotMode.SUMO_NL));
        mazeModeButton.addActionListener(e -> userSwitchMode(RobotMode.MAZE));
        otaModeButton.addActionListener(e -> userSwitchMode(RobotMode.OTA));

        speedSlider.addChangeListener(e -> {
            int v = speedSlider.getValue();
            speedValueLabel.setText(String.valueOf(v));
            if (!speedSlider.getValueIsAdjusting()) {
                sendSerial("SPEED:" + v);
                appendLog("\u2192 SPEED:" + v);
            }
        });

        installHoldButton(forwardButton, "F");
        installHoldButton(backwardButton, "B");
        installHoldButton(leftButton, "L");
        installHoldButton(rightButton, "R");
        stopButton.addActionListener(e -> runAsync(this::stopHeldMotion));

        eyesOnButton.addActionListener(e -> sendManualOnly("AUX:EYES:ON", "Eyes ON"));
        eyesOffButton.addActionListener(e -> sendManualOnly("AUX:EYES:OFF", "Eyes OFF"));
        chargeOnButton.addActionListener(e -> sendManualOnly("AUX:CHARGE:ON", "All Charge ON"));
        chargeOffButton.addActionListener(e -> sendManualOnly("AUX:CHARGE:OFF", "All Charge OFF"));
        charge1OnButton.addActionListener(e -> sendManualOnly("AUX:CHARGE1:ON", "Charge 1 ON"));
        charge1OffButton.addActionListener(e -> sendManualOnly("AUX:CHARGE1:OFF", "Charge 1 OFF"));
        charge2OnButton.addActionListener(e -> sendManualOnly("AUX:CHARGE2:ON", "Charge 2 ON"));
        charge2OffButton.addActionListener(e -> sendManualOnly("AUX:CHARGE2:OFF", "Charge 2 OFF"));
        charge3OnButton.addActionListener(e -> sendManualOnly("AUX:CHARGE3:ON", "Charge 3 ON"));
        charge3OffButton.addActionListener(e -> sendManualOnly("AUX:CHARGE3:OFF", "Charge 3 OFF"));

        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                shutdownApp();
            }

            @Override
            public void windowLostFocus(WindowEvent e) {
                runAsync(RobotControllernew.this::stopHeldMotion);
            }
        });
        KeyboardFocusManager.getCurrentKeyboardFocusManager()
                .addPropertyChangeListener("focusOwner", evt -> {
                    if (!isFocused())
                        runAsync(this::stopHeldMotion);
                });

        updateManualOnlyControls();
        updateConnectionControls();
    }

    // =========================================================================
    // KEY BINDINGS (logic unchanged)
    // =========================================================================
    private void initKeyBindings() {
        JComponent root = (JComponent) getContentPane();
        InputMap im = root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap am = root.getActionMap();

        bindKey(im, am, "pressed W", "fwdW", () -> startHeldMotion("F"));
        bindKey(im, am, "released W", "fwdWr", this::stopHeldMotion);
        bindKey(im, am, "pressed UP", "fwdUp", () -> startHeldMotion("F"));
        bindKey(im, am, "released UP", "fwdUpr", this::stopHeldMotion);
        bindKey(im, am, "pressed S", "bwdS", () -> startHeldMotion("B"));
        bindKey(im, am, "released S", "bwdSr", this::stopHeldMotion);
        bindKey(im, am, "pressed DOWN", "bwdDn", () -> startHeldMotion("B"));
        bindKey(im, am, "released DOWN", "bwdDnr", this::stopHeldMotion);
        bindKey(im, am, "pressed A", "lftA", () -> startHeldMotion("L"));
        bindKey(im, am, "released A", "lftAr", this::stopHeldMotion);
        bindKey(im, am, "pressed LEFT", "lftLf", () -> startHeldMotion("L"));
        bindKey(im, am, "released LEFT", "lftLfr", this::stopHeldMotion);
        bindKey(im, am, "pressed D", "rgtD", () -> startHeldMotion("R"));
        bindKey(im, am, "released D", "rgtDr", this::stopHeldMotion);
        bindKey(im, am, "pressed RIGHT", "rgtRt", () -> startHeldMotion("R"));
        bindKey(im, am, "released RIGHT", "rgtRtr", this::stopHeldMotion);
        bindKey(im, am, "pressed SPACE", "spc", this::stopHeldMotion);
    }

    private void bindKey(InputMap im, ActionMap am, String ks, String name, Runnable r) {
        im.put(KeyStroke.getKeyStroke(ks), name);
        am.put(name, new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                runAsync(r);
            }
        });
    }

    private void installHoldButton(JButton btn, String cmd) {
        btn.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                runAsync(() -> startHeldMotion(cmd));
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                runAsync(RobotControllernew.this::stopHeldMotion);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != 0)
                    runAsync(RobotControllernew.this::stopHeldMotion);
            }
        });
    }

    // =========================================================================
    // SERIAL — CONNECT / DISCONNECT (logic unchanged)
    // =========================================================================
    private void populatePortList() {
        SerialPort[] ports = SerialPort.getCommPorts();
        DefaultComboBoxModel<String> model = new DefaultComboBoxModel<>();
        for (SerialPort p : ports)
            model.addElement(p.getSystemPortName());
        portCombo.setModel(model);
        if (model.getSize() == 0)
            appendLog("No serial ports found.");
    }

    private void toggleConnection() {
        if (connected) {
            disconnect();
            return;
        }
        String portName = (String) portCombo.getSelectedItem();
        if (portName == null || portName.isBlank()) {
            appendLog("No port selected.");
            return;
        }
        connect(portName);
    }

    private void connect(String portName) {
        SerialPort port = SerialPort.getCommPort(portName);
        port.setBaudRate(BAUD_RATE);
        port.setNumDataBits(8);
        port.setNumStopBits(SerialPort.ONE_STOP_BIT);
        port.setParity(SerialPort.NO_PARITY);
        port.setComPortTimeouts(SerialPort.TIMEOUT_NONBLOCKING, 0, 0);
        port.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED);

        if (!port.openPort()) {
            appendLog("Failed to open " + portName);
            setStatus("Status: Failed to open " + portName);
            return;
        }

        activePort = port;
        connected = true;
        firstTelAfterConnect.set(true);

        appendLog("Connected to " + portName + " @ " + BAUD_RATE);
        setStatus("Status: Connected \u2014 " + portName);

        SwingUtilities.invokeLater(() -> {
            connectedLabel.setText("\u25CF Connected: " + portName);
            connectedLabel.setForeground(GREEN_ACCENT);
            connectBtn.setText("Disconnect");
            connectBtn.setBackground(PINK_DARK);
            portCombo.setEnabled(false);
            refreshPortsBtn.setEnabled(false);
        });

        startSerialReader(port);
        startTelemetryRequestLoop();
        updateConnectionControls();
        sendSerial("TEL?");
    }

    private void disconnect() {
        connected = false;
        stopTelemetryRequestLoop();
        stopHeldMotion();

        SerialPort p = activePort;
        activePort = null;
        if (p != null && p.isOpen())
            p.closePort();

        appendLog("Disconnected.");
        setStatus("Status: Disconnected");

        SwingUtilities.invokeLater(() -> {
            connectedLabel.setText("\u25CF Disconnected");
            connectedLabel.setForeground(RED_ACCENT);
            connectBtn.setText("Connect");
            connectBtn.setBackground(PINK);
            portCombo.setEnabled(true);
            refreshPortsBtn.setEnabled(true);
        });
        updateConnectionControls();
    }

    // =========================================================================
    // SERIAL READER THREAD (logic unchanged)
    // =========================================================================
    private void startSerialReader(SerialPort port) {
        Thread reader = new Thread(() -> {
            StringBuilder lineBuf = new StringBuilder();
            byte[] chunk = new byte[256];

            while (connected && port.isOpen()) {
                int available = port.bytesAvailable();
                if (available <= 0) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException ignored) {
                        break;
                    }
                    continue;
                }
                int read = port.readBytes(chunk, Math.min(available, chunk.length));
                for (int i = 0; i < read; i++) {
                    char c = (char) (chunk[i] & 0xFF);
                    if (c == '\n') {
                        String line = lineBuf.toString().trim();
                        lineBuf.setLength(0);
                        if (!line.isEmpty())
                            dispatchIncoming(line);
                    } else if (c != '\r') {
                        lineBuf.append(c);
                    }
                }
            }
        }, "serial-reader");
        reader.setDaemon(true);
        reader.start();
    }

    private void dispatchIncoming(String line) {
        // STATUS: lines come from slave OTA progress — forward to OTA panel
        if (line.startsWith("STATUS:")) {
            String[] parts = line.split(":", 3);
            if (parts.length == 3) {
                final String msg = parts[1];
                int pct = 0;
                try { pct = Integer.parseInt(parts[2]); } catch (Exception ignored) {}
                final int progress = pct;
                SwingUtilities.invokeLater(() -> {
                    otaProgressBar.setValue(progress);
                    otaProgressBar.setString(progress + "%");
                    otaStatusLbl.setText(msg);
                    otaStatusLbl.setForeground(
                        msg.contains("Done") || msg.contains("OK") ? GREEN_ACCENT :
                        msg.contains("FAIL")                        ? RED_ACCENT   : PINK);
                });
                appendLog("[OTA] " + msg + " (" + pct + "%)");
            }
            return;
        }
        if (line.startsWith(TEL_PREFIX)) {
            processTelemetryLine(line.substring(TEL_PREFIX.length()));
            return;
        }
        if ("PONG".equalsIgnoreCase(line)) {
            appendLog("\u2190 PONG");
            setStatus("Status: Robot reachable");
            return;
        }
        if ("MASTER_READY".equalsIgnoreCase(line)) {
            appendLog("\u2190 Master booted");
            setStatus("Status: Master ready");
            firstTelAfterConnect.set(true);
            sendSerial("TEL?");
            return;
        }
        appendLog("\u2190 " + line);
    }

    // =========================================================================
    // TELEMETRY PROCESSING (logic unchanged)
    // =========================================================================
    private void processTelemetryLine(String kv) {
        int idx = kv.indexOf('=');
        if (idx <= 0 || idx >= kv.length() - 1)
            return;

        String key = kv.substring(0, idx).trim();
        String value = kv.substring(idx + 1).trim();

        final String formatted = String.format("%-12s : %s", key, value);
        SwingUtilities.invokeLater(() -> {
            String cur = telemetryArea.getText();
            int start = cur.indexOf(key + " ");
            if (start < 0)
                start = cur.indexOf(key + "\t");
            if (start >= 0) {
                int end = cur.indexOf('\n', start);
                String updated = cur.substring(0, start) + formatted
                        + (end >= 0 ? cur.substring(end) : "");
                telemetryArea.setText(updated);
            } else {
                telemetryArea.append(formatted + System.lineSeparator());
            }
        });

        switch (key) {
            case "sonar1" -> SwingUtilities.invokeLater(() -> sonar1Label.setText(value + " cm"));
            case "sonar2" -> SwingUtilities.invokeLater(() -> sonar2Label.setText(value + " cm"));
            case "sonar3" -> SwingUtilities.invokeLater(() -> sonar3Label.setText(value + " cm"));
            case "line1" -> SwingUtilities.invokeLater(() -> {
                line1Label.setText(value);
                line1Label.setForeground("DETECTED".equals(value) ? RED_ACCENT : GREEN_ACCENT);
            });
            case "line2" -> SwingUtilities.invokeLater(() -> {
                line2Label.setText(value);
                line2Label.setForeground("DETECTED".equals(value) ? RED_ACCENT : GREEN_ACCENT);
            });
            case "ir" -> SwingUtilities.invokeLater(() -> {
                irLabel.setText(value);
                irLabel.setForeground("DETECTED".equals(value) ? RED_ACCENT : PURPLE);
            });
            case "mode" -> handleIncomingMode(value);
        }
    }

    // =========================================================================
    // MODE SYNC
    // =========================================================================
    private void handleIncomingMode(String modeStr) {
        // The slave's loop returns early in OTA mode and sends no telemetry.
        // Any mode= line arriving while the GUI is in OTA is stale (sent just
        // before the slave entered OTA, still in the serial buffer). Accepting
        // it would immediately reset the GUI back to MANUAL — which is the bug.
        if (currentMode == RobotMode.OTA) return;

        RobotMode slaveMode = RobotMode.fromFirmwareString(modeStr);
        if (slaveMode == null)
            return;

        boolean isFirst = firstTelAfterConnect.getAndSet(false);
        if (isFirst || slaveMode != currentMode) {
            applyModeToGui(slaveMode);
        }
    }

    private void applyModeToGui(RobotMode mode) {
        // Never let a telemetry sync overwrite a user-initiated OTA session.
        // If currentMode is already OTA and the incoming sync says something
        // else (e.g. stale MANUAL from before OTA started), ignore it.
        if (currentMode == RobotMode.OTA && mode != RobotMode.OTA) return;

        currentMode = mode;
        appendLog("[Sync] Slave in " + mode.label + " \u2014 GUI updated, no command sent");
        setStatus("Status: Synced \u2014 " + mode.label);

        SwingUtilities.invokeLater(() -> {
            manualModeButton.setSelected(mode == RobotMode.MANUAL);
            autoModeButton.setSelected(mode == RobotMode.AUTO);
            sumoModeButton.setSelected(mode == RobotMode.SUMO);
            sumoNlModeButton.setSelected(mode == RobotMode.SUMO_NL);
            mazeModeButton.setSelected(mode == RobotMode.MAZE);
            otaModeButton.setSelected(mode == RobotMode.OTA);
            centerCards.show(centerCardHost, mode == RobotMode.OTA ? "OTA" : "NORMAL");
            updateManualOnlyControls();
        });
    }

    // =========================================================================
    // TELEMETRY REQUEST LOOP (logic unchanged)
    // =========================================================================
    private void startTelemetryRequestLoop() {
        telRequestTask = scheduler.scheduleAtFixedRate(() -> {
            if (connected)
                sendSerial("TEL?");
        }, 800, TEL_REQUEST_INTERVAL_MS, TimeUnit.MILLISECONDS);
    }

    private void stopTelemetryRequestLoop() {
        if (telRequestTask != null) {
            telRequestTask.cancel(false);
            telRequestTask = null;
        }
    }

    // =========================================================================
    // MODE CONTROL
    // =========================================================================
    private void userSwitchMode(RobotMode mode) {
        currentMode = mode;
        updateManualOnlyControls();
        SwingUtilities.invokeLater(() ->
            centerCards.show(centerCardHost, mode == RobotMode.OTA ? "OTA" : "NORMAL"));
        runAsync(() -> {
            stopHeldMotion();
            if (mode == RobotMode.OTA) {
                // Slave won't respond to TEL? while serving its OTA web server.
                // Stop polling to avoid serial noise and stale replies.
                stopTelemetryRequestLoop();
            } else {
                // Leaving OTA (or switching any other mode): ensure TEL? loop
                // is running and the next reply is treated as a fresh connect.
                stopTelemetryRequestLoop();
                startTelemetryRequestLoop();
                firstTelAfterConnect.set(true);
            }
            sendSerial(mode.command);
            appendLog("\u2192 " + mode.command);
            setStatus("Status: " + mode.label + " sent");
        });
    }

    // =========================================================================
    // MOTION CONTROL (logic unchanged)
    // =========================================================================
    private void startHeldMotion(String cmd) {
        if (currentMode != RobotMode.MANUAL) {
            setStatus("Status: Movement blocked \u2014 not in Manual mode");
            return;
        }
        heldCommand.set(cmd);
        if (heldCommandTask != null && !heldCommandTask.isCancelled() && !heldCommandTask.isDone())
            return;
        heldCommandTask = scheduler.scheduleAtFixedRate(() -> {
            if (currentMode != RobotMode.MANUAL)
                return;
            if (!sendingHeldCommand.compareAndSet(false, true))
                return;
            try {
                sendSerial("CMD:" + heldCommand.get());
            } finally {
                sendingHeldCommand.set(false);
            }
        }, 0, HELD_COMMAND_INTERVAL_MS, TimeUnit.MILLISECONDS);
    }

    private void stopHeldMotion() {
        heldCommand.set("S");
        if (heldCommandTask != null) {
            heldCommandTask.cancel(false);
            heldCommandTask = null;
        }
        sendSerial("CMD:S");
        setStatus("Status: STOP");
    }

    private void sendManualOnly(String cmd, String label) {
        if (currentMode != RobotMode.MANUAL) {
            setStatus("Status: " + label + " blocked \u2014 not in Manual mode");
            appendLog(label + " ignored \u2014 not in Manual mode");
            return;
        }
        sendSerial(cmd);
        appendLog("\u2192 " + cmd);
        setStatus("Status: " + label);
    }

    // =========================================================================
    // SERIAL SEND (logic unchanged)
    // =========================================================================
    private void sendSerial(String msg) {
        SerialPort p = activePort;
        if (p == null || !p.isOpen())
            return;
        byte[] data = (msg + "\n").getBytes(StandardCharsets.UTF_8);
        p.writeBytes(data, data.length);
    }

    // =========================================================================
    // UI HELPERS (logic unchanged)
    // =========================================================================
    private void updateManualOnlyControls() {
        SwingUtilities.invokeLater(() -> {
            boolean en = connected && (currentMode == RobotMode.MANUAL);
            forwardButton.setEnabled(en);
            backwardButton.setEnabled(en);
            leftButton.setEnabled(en);
            rightButton.setEnabled(en);
            stopButton.setEnabled(en);
            eyesOnButton.setEnabled(en);
            eyesOffButton.setEnabled(en);
            chargeOnButton.setEnabled(en);
            chargeOffButton.setEnabled(en);
            charge1OnButton.setEnabled(en);
            charge1OffButton.setEnabled(en);
            charge2OnButton.setEnabled(en);
            charge2OffButton.setEnabled(en);
            charge3OnButton.setEnabled(en);
            charge3OffButton.setEnabled(en);
        });
    }

    private void updateConnectionControls() {
        SwingUtilities.invokeLater(() -> {
            manualModeButton.setEnabled(connected);
            autoModeButton.setEnabled(connected);
            sumoModeButton.setEnabled(connected);
            mazeModeButton.setEnabled(connected);
            otaModeButton.setEnabled(connected);
            speedSlider.setEnabled(connected);
            updateManualOnlyControls();
        });
    }

    private void setStatus(String text) {
        SwingUtilities.invokeLater(() -> {
            connectionStatusLabel.setText(text);
            if (text.contains("Disconnected") || text.contains("Failed") || text.contains("blocked"))
                connectionStatusLabel.setForeground(RED_ACCENT);
            else if (text.contains("Connected") || text.contains("reachable") || text.contains("Synced"))
                connectionStatusLabel.setForeground(GREEN_ACCENT);
            else if (text.contains("STOP"))
                connectionStatusLabel.setForeground(new Color(255, 180, 80));
            else
                connectionStatusLabel.setForeground(PINK);
        });
    }

    private void appendLog(String text) {
        SwingUtilities.invokeLater(() -> {
            logArea.append(text + System.lineSeparator());
            logArea.setCaretPosition(logArea.getDocument().getLength());
        });
    }

    // =========================================================================
    // SHUTDOWN (logic unchanged)
    // =========================================================================
    private void shutdownApp() {
        try {
            stopHeldMotion();
        } catch (Exception ignored) {
        }
        stopTelemetryRequestLoop();
        if (heldCommandTask != null)
            heldCommandTask.cancel(true);
        scheduler.shutdownNow();
        if (activePort != null && activePort.isOpen())
            activePort.closePort();
    }

    private void runAsync(Runnable task) {
        scheduler.execute(() -> {
            try {
                task.run();
            } catch (Exception ex) {
                appendLog("Task error: " + ex.getMessage());
            }
        });
    }

    // =========================================================================
    // ENTRY POINT
    // =========================================================================
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new RobotControllernew().setVisible(true));
    }
}
