#!/usr/bin/env python3
"""
SCPI Client — Interactive SCPI terminal for NEnG instruments.

Send SCPI commands via USB serial or WiFi/TCP. Supports single-command
and interactive modes with tab-completion, command history, pretty-printed
responses, and OPC synchronization.

Works with all NEnG instrument families: TMP117, 3Dmag, BTS7960,
RelayBank16, RTDMax31865.

Usage (installed via pip):
    # Interactive mode
    scpi-client

    # Single command
    scpi-client "*IDN?"
    scpi-client ":SENS:TEMP?"

    # WiFi/TCP mode
    scpi-client -w 192.168.1.100 "*IDN?"
    scpi-client -w 192.168.1.100  # interactive over WiFi

    # Specify serial port
    scpi-client -p /dev/cu.usbmodem1101 "*IDN?"

(c) 2026 Prof. Flavio ABREU ARAUJO. All rights reserved.
"""

from __future__ import annotations

import argparse
import contextlib
import importlib
import os
import platform
import re
import socket
import struct
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import as_completed as _as_completed

# Conditional import of termios (Unix-only)
try:
    import termios
except ImportError:
    # On Windows, termios doesn't exist. Create a placeholder exception class.
    class termios:  # type: ignore
        class error(OSError):
            """Placeholder for termios.error on Windows"""

            pass


import serial  # type: ignore
import serial.tools.list_ports  # type: ignore

# Conditional import of colorama (for Windows ANSI color support)
try:
    import colorama
    COLORAMA_AVAILABLE = True
except ImportError:
    COLORAMA_AVAILABLE = False

# ---------------------------------------------------------------------------
# ANSI helpers
# ---------------------------------------------------------------------------
CYAN = "\033[96m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
MAGENTA = "\033[95m"
RED = "\033[91m"
BLUE = "\033[94m"
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"

DEFAULT_SCPI_PORT = 5025

# Try to import readline for history / tab-completion
try:
    import readline

    READLINE_AVAILABLE = True
    USING_LIBEDIT = "libedit" in (readline.__doc__ or "")
except ImportError:
    READLINE_AVAILABLE = False
    USING_LIBEDIT = False

HISTORY_FILE = os.path.expanduser("~/.scpi_send_history")

# Try to import prompt_toolkit for better cross-platform tab completion
try:
    from prompt_toolkit import PromptSession
    from prompt_toolkit.completion import Completer, Completion
    from prompt_toolkit.document import Document
    from prompt_toolkit.history import FileHistory

    PROMPT_TOOLKIT_AVAILABLE = True

    class FilteringCompleter(Completer):
        """Custom completer that filters commands as you type."""

        def __init__(self, commands: list[str]):
            self.commands = commands

        def get_completions(self, document: Document, complete_event):
            """Return matching commands based on current text."""
            text = document.text_before_cursor.strip()
            if not text:
                # Show all commands if nothing typed
                for cmd in self.commands:
                    yield Completion(cmd, -len(text))
            else:
                # Filter to commands that start with the typed text (case-insensitive)
                for cmd in self.commands:
                    if cmd.upper().startswith(text.upper()):
                        # Yield the part that needs to be added
                        yield Completion(cmd, -len(text))

except ImportError:
    PROMPT_TOOLKIT_AVAILABLE = False
    FileHistory = None  # type: ignore
    FilteringCompleter = None  # type: ignore


def get_scpi_input(prompt_text: str, commands: list[str]) -> str:
    """Get user input with filtering menu as you type.

    Filters commands dynamically as the user types.
    """
    if PROMPT_TOOLKIT_AVAILABLE:
        try:
            history = FileHistory(HISTORY_FILE)
            session = PromptSession(
                completer=FilteringCompleter(commands),
                history=history,
                complete_while_typing=True,
            )
            return session.prompt(prompt_text)
        except (KeyboardInterrupt, EOFError):
            raise
        except Exception as e:
            # Fall back if prompt_toolkit fails (e.g., stdin is piped)
            import sys
            print(f"Warning: prompt_toolkit failed: {e}", file=sys.stderr)

    if READLINE_AVAILABLE and platform.system() != "Windows":
        # On Unix with readline, use basic input (readline handles everything natively)
        return input(prompt_text)

    # Fallback: basic input
    return input(prompt_text)


# ===================================================================
# TCP Socket wrapper (mirrors serial.Serial interface)
# ===================================================================
class TCPSocket:
    """TCP socket wrapper that mimics ``serial.Serial`` for SCPI over WiFi."""

    def __init__(self, host: str, port: int = DEFAULT_SCPI_PORT, timeout: float = 2.0):
        self.host = host
        self.tcp_port = port
        self.timeout = timeout
        self.sock: socket.socket | None = None
        self._buffer = b""

    # -- connection --------------------------------------------------------
    def connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.connect((self.host, self.tcp_port))

    def close(self):
        if self.sock:
            with contextlib.suppress(Exception):
                self.sock.close()
            self.sock = None

    # -- write / read ------------------------------------------------------
    def write(self, data, sync=True):
        if isinstance(data, str):
            data = data.encode("utf-8")
        if self.sock:
            try:
                self.sock.sendall(data)
            except (BrokenPipeError, ConnectionResetError, OSError) as e:
                raise ConnectionError(f"Connection lost: {e}") from e

    def read(self, size: int = 1) -> bytes:
        if not self.sock:
            return b""
        try:
            if len(self._buffer) >= size:
                data, self._buffer = self._buffer[:size], self._buffer[size:]
                return data
            data = self._buffer
            self._buffer = b""
            with contextlib.suppress(socket.timeout):
                data += self.sock.recv(size - len(data))
            return data
        except (ConnectionResetError, BrokenPipeError, OSError) as e:
            raise ConnectionError(f"Connection lost while reading: {e}") from e

    def readline(self) -> bytes:
        if not self.sock:
            return b""
        try:
            while b"\n" not in self._buffer:
                try:
                    chunk = self.sock.recv(1024)
                    if not chunk:
                        break
                    self._buffer += chunk
                except socket.timeout:
                    break
            if b"\n" in self._buffer:
                line, self._buffer = self._buffer.split(b"\n", 1)
                return line + b"\n"
            line, self._buffer = self._buffer, b""
            return line
        except (ConnectionResetError, BrokenPipeError, OSError) as e:
            raise ConnectionError(f"Connection lost while reading: {e}") from e

    # -- buffer management -------------------------------------------------
    def reset_input_buffer(self):
        self._buffer = b""
        if self.sock:
            self.sock.setblocking(False)
            try:
                while True:
                    chunk = self.sock.recv(1024)
                    if not chunk:
                        break
            except (OSError, BlockingIOError):
                pass
            finally:
                self.sock.setblocking(True)
                self.sock.settimeout(self.timeout)

    def reset_output_buffer(self):
        pass

    def flush(self):
        pass

    @property
    def in_waiting(self) -> int:
        if self._buffer:
            return len(self._buffer)
        if not self.sock:
            return 0
        self.sock.setblocking(False)
        try:
            chunk = self.sock.recv(1024, socket.MSG_PEEK)
            return len(chunk)
        except (OSError, BlockingIOError):
            return 0
        finally:
            self.sock.setblocking(True)
            self.sock.settimeout(self.timeout)


# ===================================================================
# VXI-11 Socket wrapper (mirrors serial.Serial interface)
# ===================================================================
class VXI11Socket:
    """VXI-11 wrapper that mimics ``serial.Serial`` for SCPI over LAN.

    Uses ``pyvisa`` + ``pyvisa-py`` as the backend (works on Python 3.13+).
    ``python-vxi11`` depends on the removed ``xdrlib`` module and cannot be
    used on Python 3.13 or later.

    Install the dependencies with::

        pip install pyvisa pyvisa-py

    VXI-11 is a synchronous, request/response protocol. After every
    ``write()`` call that contains a query (``?``), the instrument
    response is eagerly read and buffered so that the
    ``send_scpi_command`` polling loop (which checks ``in_waiting``)
    sees data immediately.
    """

    is_vxi11: bool = True  # sentinel used by send_scpi_command

    def __init__(self, host: str, timeout: float = 5.0):
        self.host = host
        self.timeout = timeout
        self._instr = None
        self._buffer = b""

    # -- connection --------------------------------------------------------
    def connect(self):
        try:
            import pyvisa  # type: ignore
        except ImportError as exc:
            raise ImportError(
                "pyvisa and pyvisa-py are required for VXI-11 connections.\n"
                "Install with:  pip install pyvisa pyvisa-py"
            ) from exc
        rm = pyvisa.ResourceManager("@py")
        resource_str = f"TCPIP0::{self.host}::INSTR"
        try:
            self._instr = rm.open_resource(resource_str)
        except pyvisa.errors.VisaIOError as exc:
            raise ConnectionError(f"VXI-11 open failed for {self.host}: {exc}") from exc
        self._instr.timeout = int(self.timeout * 1000)  # pyvisa uses milliseconds
        # Disable response headers for LeCroy/Teledyne scopes (CHDR OFF).
        # Standard SCPI instruments don't support this command and will silently
        # ignore it or return an error — both are harmless.
        with contextlib.suppress(Exception):
            self._instr.write("CHDR OFF")

    def close(self):
        if self._instr:
            with contextlib.suppress(Exception):
                self._instr.close()
            self._instr = None

    # Commands whose responses are IEEE 488.2 binary blocks, not ASCII text.
    # Pattern: the WF? sub-parameter determines the format.
    _BINARY_WF_PARAMS = frozenset({"DAT1", "DAT2", "DTIME"})

    @staticmethod
    def _is_binary_response(cmd: str) -> bool:
        """Return True if this command returns a binary (non-ASCII) block."""
        upper = cmd.upper()
        return any(f"WF? {param}" in upper for param in VXI11Socket._BINARY_WF_PARAMS)

    @staticmethod
    def _sparkline(samples, width: int = 48) -> str:
        """Generate a unicode sparkline from any numeric sequence."""
        _SPARKS = "▁▂▃▄▅▆▇█"
        mn, mx = min(samples), max(samples)
        span = mx - mn or 1
        step = max(1, len(samples) // width)
        subset = list(samples)[::step][:width]
        return "".join(_SPARKS[min(7, int((v - mn) / span * 8))] for v in subset)

    @staticmethod
    def _fmt_phys(v: float, unit: str) -> str:
        """Format a physical value with auto SI-prefix (m, μ)."""
        av = abs(v)
        if unit in ("ADC", "") or av == 0 or av >= 0.1:
            return f"{v:.4g} {unit}"
        if av >= 1e-3:
            return f"{v * 1e3:.4g} m{unit}"
        return f"{v * 1e6:.4g} μ{unit}"

    @staticmethod
    def _fmt_time(t: float) -> str:
        """Format a time value with auto SI-prefix (ms, μs, ns, ps)."""
        at = abs(t)
        if at == 0:
            return "0 s"
        if at >= 1:
            return f"{t:.4g} s"
        if at >= 1e-3:
            return f"{t * 1e3:.4g} ms"
        if at >= 1e-6:
            return f"{t * 1e6:.4g} μs"
        if at >= 1e-9:
            return f"{t * 1e9:.4g} ns"
        return f"{t * 1e12:.4g} ps"

    def _get_waveform_scale(self, channel: str) -> dict:
        """Query waveform scaling parameters for *channel* via INSP? 'WAVEDESC'.

        A single query returns all parameters at once as multi-line ASCII text:
            VERTICAL_GAIN      : 1.6496e-04
            VERTICAL_OFFSET    : -0.0000e+00
            VERTUNIT           : Unit Name = V
            HORIZ_INTERVAL     : 4.0000e-07
            ...
        This works regardless of the CHDR setting.
        Returns a dict with keys: vgain, voffset, unit, dt.
        """
        result: dict = {"vgain": 1.0, "voffset": 0.0, "unit": "ADC", "dt": 0.0}
        if not self._instr or not channel:
            return result
        _want = {
            "VERTICAL_GAIN": ("vgain", float),
            "VERTICAL_OFFSET": ("voffset", float),
            # LeCroy WAVEDESC uses "VERTUNIT" with format "Unit Name = V"
            "VERTUNIT": (
                "unit",
                lambda s: (s.split("=")[-1].strip() if "=" in s else s.strip("\"' ")) or "V",
            ),
            "HORIZ_INTERVAL": ("dt", float),
        }
        try:
            resp = self._instr.query(f"{channel}:INSP? 'WAVEDESC'")
            for line in resp.splitlines():
                if ":" not in line:
                    continue
                key, _, raw_val = line.partition(":")
                key = key.strip()
                if key not in _want:
                    continue
                dest, conv = _want[key]
                with contextlib.suppress(ValueError, TypeError):
                    result[dest] = conv(raw_val.strip())
        except Exception:
            pass
        return result

    @staticmethod
    def _format_binary_block(
        data: bytes,
        vgain: float = 1.0,
        voffset: float = 0.0,
        unit: str = "ADC",
        dt: float = 0.0,
    ) -> str:
        """Parse an IEEE 488.2 definite-length binary block and return a summary.

        Format:  ``#<N><L><payload>``  where N = number of digits in L,
        L = payload byte count (decimal), followed by the raw payload bytes.

        LeCroy scopes prefix the block with "KEYWORD," even with CHDR OFF
        (e.g. ``DAT1,#9000000027<data>``).  We locate ``#`` explicitly.

        Waveform samples are converted to physical values using:
            value = sample * vgain - voffset

        Returns a multi-line string; the VXI-11 query path drains all lines.
        """
        if not data:
            return "(empty binary response)"

        # LeCroy prepends an ASCII label before '#' even with CHDR OFF
        # e.g. b"DAT1,#9000000027<payload>\n"
        prefix = ""
        block_start = data.find(b"#")
        if block_start > 0:
            prefix = data[:block_start].decode("ascii", errors="replace").strip(",; ")
            data = data[block_start:]
        elif block_start < 0:
            # No '#' — try plain text, then hex dump
            try:
                return data.decode("utf-8").strip()
            except UnicodeDecodeError:
                hex_str = " ".join(f"{b:02x}" for b in data)
                return f"[Binary data: {len(data)} bytes]\n  hex: {hex_str}"

        try:
            n_digits = int(chr(data[1]))
            if n_digits > 0:
                byte_count = int(data[2 : 2 + n_digits])
                payload = data[2 + n_digits : 2 + n_digits + byte_count]

                tag = f"{prefix}: " if prefix else ""

                # Single double (8 bytes) — e.g. DTIME (time step in seconds)
                if byte_count == 8:
                    (val,) = struct.unpack("<d", payload)
                    return f"{tag}{val:.6e} s  [Δt per sample]"

                # Single float (4 bytes)
                if byte_count == 4:
                    (val,) = struct.unpack("<f", payload)
                    return f"{tag}{val:.6e}  [binary float]"

                # Waveform array: odd byte_count → int8, even → int16
                if byte_count >= 1:
                    if byte_count % 2 == 0:
                        n_samples = byte_count // 2
                        samples = struct.unpack(f"<{n_samples}h", payload[: n_samples * 2])
                        dtype = "int16"
                    else:
                        n_samples = byte_count
                        samples = struct.unpack(f"{n_samples}b", payload[:n_samples])
                        dtype = "int8"

                    # Convert ADC counts → physical values: v = sample * vgain - voffset
                    scaled = unit != "ADC"
                    values = tuple(s * vgain - voffset for s in samples)
                    mn_s = VXI11Socket._fmt_phys(min(values), unit)
                    mx_s = VXI11Socket._fmt_phys(max(values), unit)
                    mean_s = VXI11Socket._fmt_phys(sum(values) / n_samples, unit)
                    if dt:
                        dt_s = VXI11Socket._fmt_time(dt)
                        win_s = VXI11Socket._fmt_time(n_samples * dt)
                        time_s = f"  Δt={dt_s}  T={win_s}"
                    else:
                        time_s = ""
                    spark = VXI11Socket._sparkline(values)

                    preview_n = min(40, n_samples)
                    if scaled:
                        preview = "  ".join(f"{v:.4g}" for v in values[:preview_n])
                        value_unit = unit
                    else:
                        preview = "  ".join(str(s) for s in samples[:preview_n])
                        value_unit = "ADC counts"
                    ellipsis = "  ..." if n_samples > preview_n else ""

                    return "\n".join(
                        [
                            f"{tag}[{n_samples} {dtype}{time_s} · min={mn_s}  max={mx_s}  mean={mean_s}]",
                            f"  {spark}",
                            f"  [{preview}{ellipsis}]  ({value_unit})",
                        ]
                    )
        except (ValueError, IndexError, struct.error):
            pass

        # Fallback: hex dump
        hex_str = " ".join(f"{b:02x}" for b in data)
        return f"[Binary data: {len(data)} bytes]\n  hex: {hex_str}"

    # -- write / read ------------------------------------------------------
    def write(self, data, sync=True):
        if isinstance(data, bytes):
            data = data.decode("utf-8", errors="replace")
        cmd = data.strip()
        if not cmd or not self._instr:
            return
        try:
            self._instr.write(cmd)
        except Exception as e:
            raise ConnectionError(f"VXI-11 write failed: {e}") from e
        # Eagerly read the instrument response for any query
        if "?" in cmd:
            try:
                if self._is_binary_response(cmd):
                    raw = self._instr.read_raw()
                    # Fetch physical scaling from the scope for the active channel
                    channel = cmd.split(":", 1)[0].upper() if ":" in cmd else ""
                    scale = self._get_waveform_scale(channel)
                    summary = self._format_binary_block(raw, **scale)
                    self._buffer = summary.encode("utf-8") + b"\n"
                else:
                    response = self._instr.read()
                    response = self._strip_response_header(cmd, response)
                    self._buffer = response.strip().encode("utf-8") + b"\n"
            except Exception:
                self._buffer = b""

    @staticmethod
    def _strip_response_header(cmd: str, response: str) -> str:
        """Strip instrument response headers (e.g. LeCroy CHDR SHORT/LONG mode).

        Some instruments prefix query responses with the command keyword::

            *IDN?     → "*IDN LECROY,WP804HD-MS,..."   → "LECROY,WP804HD-MS,..."
            C1:VDIV?  → "C1:VDIV 5.0E-3V"             → "5.0E-3V"

        The keyword is derived as the last ``;``-separated token of the part
        of the command before the first ``?``, then matched case-insensitively
        against the start of the response followed by a space.
        """
        before_q = cmd.split("?")[0]
        keyword = before_q.split(";")[-1].strip()
        if keyword and response.upper().startswith(keyword.upper() + " "):
            return response[len(keyword) + 1 :].lstrip()
        return response

    def readline(self) -> bytes:
        if not self._buffer:
            return b""
        if b"\n" in self._buffer:
            line, self._buffer = self._buffer.split(b"\n", 1)
            return line + b"\n"
        line, self._buffer = self._buffer, b""
        return line

    def read(self, size: int = 1) -> bytes:
        if not self._buffer:
            return b""
        data, self._buffer = self._buffer[:size], self._buffer[size:]
        return data

    # -- buffer management -------------------------------------------------
    def reset_input_buffer(self):
        self._buffer = b""

    def reset_output_buffer(self):
        pass

    def flush(self):
        pass

    @property
    def in_waiting(self) -> int:
        return len(self._buffer)


# ===================================================================
# Utility functions
# ===================================================================
def find_neng_ports() -> list[tuple[str, str]]:
    """Return all serial ports that look like NEnG / CircuitPython devices.

    Returns a list of (device_path, description) tuples.
    """
    matches = []
    for p in serial.tools.list_ports.comports():
        desc = p.description.lower() if p.description else ""
        mfr = p.manufacturer.lower() if p.manufacturer else ""

        # Check USB VID/PID first (most reliable, works across all OS languages)
        # VID 0x239A = Adafruit (CircuitPython), 0x2341 = Arduino, 0x1A86 = CH340
        # VID 0x303A = Espressif (ESP32), 0x2E8A = Raspberry Pi (Pico)
        is_neng = False
        if p.vid is not None and p.vid in (0x239A, 0x2341, 0x1A86, 0x303A, 0x2E8A):
            is_neng = True

        # Fall back to description/manufacturer text matching
        if not is_neng and (
            any(kw in desc for kw in ("circuitpython", "arduino", "esp32", "nano"))
            or "arduino" in mfr
        ):
            is_neng = True

        if is_neng:
            matches.append((p.device, p.description or p.device))
    return matches


def parse_wifi_address(addr_str: str) -> tuple[str, int]:
    """Parse ``"host"`` or ``"host:port"`` into *(host, port)* tuple."""
    if ":" in addr_str:
        host, port_str = addr_str.rsplit(":", 1)
        try:
            port = int(port_str)
        except ValueError:
            print(f"Warning: Invalid port '{port_str}', using default {DEFAULT_SCPI_PORT}")
            port = DEFAULT_SCPI_PORT
    else:
        host = addr_str
        port = DEFAULT_SCPI_PORT
    return host, port


# ===================================================================
# Pretty-print helpers
# ===================================================================
def format_response(command: str, response: str, pretty: bool = True) -> str:
    """Pretty-format known SCPI responses for human readability."""
    if not pretty or not response:
        return response

    cmd = command.strip().upper()

    # Detect SCPI error codes (format: -xxx or -xxx,Message)
    if response.strip().startswith("-") and ("," in response or response.strip()[1:].isdigit()):
        try:
            parts = response.strip().split(",", 1)
            error_code = int(parts[0])
            error_msg = parts[1] if len(parts) > 1 else "Unknown error"

            # Common SCPI error codes
            if error_code == -113:
                return f"{RED}✗ Error:{RESET} Undefined header (command not recognized)"
            elif error_code == -109:
                return f"{RED}✗ Error:{RESET} Missing parameter"
            elif error_code == -108:
                return f"{RED}✗ Error:{RESET} Parameter not allowed"
            elif error_code == -102:
                return f"{RED}✗ Error:{RESET} Syntax error"
            else:
                return f"{RED}✗ Error {error_code}:{RESET} {error_msg}"
        except (ValueError, IndexError):
            pass

    # --- Special error handling ---
    # Read-only filesystem error (common when CIRCUITPY is mounted in DEBUG mode)
    if "Read-only filesystem" in response or "CIRCUITPY mounted" in response:
        if cmd.startswith(":SYST:USB:MODE"):
            return (
                f"{RED}✗ USB mode change failed:{RESET} {YELLOW}Filesystem is read-only{RESET}\n"
                f"{DIM}Note: CIRCUITPY is mounted. USB mode can only be changed in usb_config.txt file directly in CIRCUITPY drive.{RESET}"
            )
        else:
            return (
                f"{RED}✗ Write failed:{RESET} {YELLOW}Filesystem is read-only{RESET}\n"
                f"{DIM}Note: CIRCUITPY is mounted (DEBUG mode).{RESET}"
            )

    # --- PID Status (:PID:STAT?) ---
    if cmd in (":PID:STAT?", ":PID:STATUS?"):
        try:
            parts = response.split(",")
            if len(parts) >= 7:
                en, sp, err, out = int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3])
                p, i, d = float(parts[4]), float(parts[5]), float(parts[6])
                si = f"{GREEN}●{RESET}" if en else f"{DIM}○{RESET}"
                st = f"{GREEN}ENABLED{RESET}" if en else f"{DIM}DISABLED{RESET}"
                oc = RED if abs(out) > 0.8 else YELLOW if abs(out) > 0.5 else GREEN
                ob = "█" * int(abs(out) * 10)
                od = "HEAT" if out > 0 else "COOL" if out < 0 else "OFF"
                ec = RED if abs(err) > 2 else YELLOW if abs(err) > 0.5 else GREEN
                r = f"\n{BOLD}{CYAN}PID Controller Status:{RESET}\n"
                r += f"  {si} Status:      {st}\n"
                r += f"  🎯 Setpoint:    {MAGENTA}{sp:6.2f}°C{RESET}\n"
                r += f"  ⚠️  Error:       {ec}{err:+6.2f}°C{RESET}\n"
                r += f"  ⚡ Output:      {oc}{out:+6.2f}{RESET} ({od}) {oc}{ob}{RESET}\n"
                r += f"  📊 PID Terms:\n     P: {p:+7.4f}\n     I: {i:+7.4f}\n     D: {d:+7.4f}"
                return r
        except (ValueError, IndexError):
            pass

    # --- PID Telemetry (:PID:TELE?) ---
    elif cmd in (":PID:TELE?", ":PID:TELEMETRY?"):
        try:
            parts = response.split(",")
            if len(parts) >= 7:
                run, sens = int(parts[0]), int(parts[1])
                tmp, sp, out = float(parts[2]), float(parts[3]), float(parts[4])
                lc, ramp = int(parts[5]), int(parts[6])
                si = f"{GREEN}●{RESET}" if run else f"{DIM}○{RESET}"
                st = f"{GREEN}RUNNING{RESET}" if run else f"{DIM}STOPPED{RESET}"
                oc = RED if abs(out) > 0.8 else YELLOW if abs(out) > 0.5 else GREEN
                ob = "█" * int(abs(out) * 10)
                od = "HEAT" if out > 0 else "COOL" if out < 0 else "OFF"
                er = tmp - sp
                ec = RED if abs(er) > 2 else YELLOW if abs(er) > 0.5 else GREEN
                r = f"\n{BOLD}{CYAN}PID Controller Telemetry:{RESET}\n"
                r += f"  {si} Status:      {st}\n"
                r += f"  🔢 Sensor:       #{sens}\n"
                r += f"  🌡️  Temperature:  {CYAN}{tmp:7.3f}°C{RESET}\n"
                r += f"  🎯 Setpoint:     {MAGENTA}{sp:7.3f}°C{RESET}\n"
                r += f"  ⚠️  Error:        {ec}{er:+7.3f}°C{RESET}\n"
                r += f"  ⚡ Output:       {oc}{out:+7.3f}{RESET} ({od}) {oc}{ob}{RESET}\n"
                r += f"  🔄 Loop count:   {lc}\n"
                r += (
                    f"  🔥 Ramping:      {YELLOW}ACTIVE{RESET}"
                    if ramp
                    else f"  🔥 Ramping:      {DIM}INACTIVE{RESET}"
                )
                return r
        except (ValueError, IndexError):
            pass

    # --- Ramp Status (:PID:RAMP:STAT?) ---
    elif cmd in (":PID:RAMP:STAT?", ":PID:RAMP:STATUS?"):
        try:
            parts = response.split(",")
            if len(parts) >= 9:
                act, ramp, hold, comp = int(parts[0]), int(parts[1]), int(parts[2]), int(parts[3])
                st_t, tgt, csp = float(parts[4]), float(parts[5]), float(parts[6])
                rate, prog = float(parts[7]), float(parts[8])
                if comp:
                    si, st = f"{GREEN}✓{RESET}", f"{GREEN}COMPLETED{RESET}"
                elif hold:
                    si, st = f"{YELLOW}⏸{RESET}", f"{YELLOW}HOLDING{RESET}"
                elif ramp:
                    si, st = f"{YELLOW}▶{RESET}", f"{YELLOW}RAMPING{RESET}"
                elif act:
                    si, st = f"{CYAN}●{RESET}", f"{CYAN}ACTIVE{RESET}"
                else:
                    si, st = f"{DIM}■{RESET}", f"{DIM}IDLE{RESET}"
                r = f"\n{BOLD}{CYAN}Temperature Ramp Status:{RESET}\n  {si} Phase:      {st}\n"
                if act:
                    r += f"  🎯 Target:     {MAGENTA}{tgt:6.2f}°C{RESET}\n"
                    r += f"  📍 Current SP: {CYAN}{csp:6.2f}°C{RESET}\n"
                    r += f"  📈 Rate:       {CYAN}{rate:6.2f}°C/min{RESET}\n"
                    if rate > 0 and ramp:
                        td = abs(tgt - st_t)
                        total = (td / rate) * 60.0
                        el = (prog / 100.0) * total
                        rem = total - el
                        r += f"  ⏱️  Elapsed:    {el:6.1f}s\n  ⏳ Remaining:  {rem:6.1f}s\n"
                    bw = 20
                    fi = int((prog / 100.0) * bw)
                    bar = "█" * fi + "░" * (bw - fi)
                    r += f"  Progress: [{CYAN}{bar}{RESET}] {prog:5.1f}%"
                else:
                    r += f"  {DIM}No active ramp{RESET}"
                return r
        except (ValueError, IndexError) as e:
            return f"{YELLOW}⚠ Ramp Status Parse Error:{RESET}\n{response}\n{DIM}Error: {e}{RESET}"

    # --- WiFi Comprehensive Status (:WIFI:STAT?) ---
    elif cmd == ":WIFI:STAT?":
        try:
            # Parse pipe-separated key:value format
            # Format: HW:1|EN:1|CONN:1|SSID:MyNet|IP:192.168.1.100|RSSI:-45|CH:6|MAC:AA:BB:CC|HOST:tmp117|AUTO:1|FB:2
            data = {}
            for part in response.split("|"):
                if ":" in part:
                    key, value = part.split(":", 1)
                    data[key] = value

            # Check hardware availability
            if data.get("HW") == "0":
                return f"{RED}WiFi Hardware: NOT AVAILABLE{RESET}"

            # Build pretty output
            enabled = data.get("EN") == "1"
            connected = data.get("CONN") == "1"

            # Status indicators
            if connected:
                si = f"{GREEN}📶{RESET}"
                st = f"{GREEN}CONNECTED{RESET}"
            elif enabled:
                si = f"{YELLOW}📡{RESET}"
                st = f"{YELLOW}ENABLED, NOT CONNECTED{RESET}"
            else:
                si = f"{DIM}📵{RESET}"
                st = f"{DIM}DISABLED{RESET}"

            r = f"\n{BOLD}{CYAN}┌─── WiFi Status ───┐{RESET}\n"
            r += f"  {si} Status:      {st}\n"

            # Connection details
            if connected:
                ssid = data.get("SSID", "Unknown")
                ip = data.get("IP", "Unknown")
                r += f"  🌐 SSID:        {CYAN}{ssid}{RESET}\n"
                r += f"  📍 IP Address:  {MAGENTA}{ip}{RESET}\n"

                # Signal strength (if available)
                if "RSSI" in data:
                    try:
                        rssi = int(data["RSSI"])
                        if rssi > -50:
                            sig_color, sig_qual = GREEN, "Excellent"
                        elif rssi > -60:
                            sig_color, sig_qual = GREEN, "Good"
                        elif rssi > -70:
                            sig_color, sig_qual = YELLOW, "Fair"
                        else:
                            sig_color, sig_qual = RED, "Weak"
                        r += f"  📊 Signal:      {sig_color}{sig_qual}{RESET} ({rssi} dBm)\n"
                    except ValueError:
                        pass

                # Channel (if available)
                if "CH" in data:
                    r += f"  📡 Channel:     {CYAN}{data['CH']}{RESET}\n"
            else:
                # Show configured SSID if not connected
                cfg_ssid = data.get("CFG", "None")
                if cfg_ssid != "None":
                    r += f"  🌐 Configured:  {DIM}{cfg_ssid}{RESET}\n"

            # Device identity
            mac = data.get("MAC", "Unknown")
            hostname = data.get("HOST", "Unknown")
            r += f"  🔖 MAC:         {CYAN}{mac}{RESET}\n"
            r += f"  💻 Hostname:    {CYAN}{hostname}{RESET}\n"

            # Configuration
            auto = data.get("AUTO") == "1"
            auto_str = f"{GREEN}Yes{RESET}" if auto else f"{DIM}No{RESET}"
            r += f"  🔄 Auto-Connect: {auto_str}\n"

            # Fallback networks
            fb_count = data.get("FB", "0")
            r += f"  📋 Fallback Networks: {CYAN}{fb_count}{RESET}\n"

            r += f"{BOLD}{CYAN}└───────────────────┘{RESET}"
            return r
        except (ValueError, IndexError, KeyError):
            pass

    # --- WiFi Status (:WIFI:STATUS?) ---
    elif cmd == ":WIFI:STATUS?":
        try:
            parts = response.split(",")
            if len(parts) >= 4:
                conn = int(parts[0])
                ssid, ip, rssi = parts[1], parts[2], int(parts[3])
                si = f"{GREEN}📶{RESET}" if conn else f"{DIM}📵{RESET}"
                st = f"{GREEN}CONNECTED{RESET}" if conn else f"{DIM}DISCONNECTED{RESET}"
                if rssi > -50:
                    sig = f"{GREEN}Excellent{RESET} ({rssi} dBm)"
                elif rssi > -60:
                    sig = f"{GREEN}Good{RESET} ({rssi} dBm)"
                elif rssi > -70:
                    sig = f"{YELLOW}Fair{RESET} ({rssi} dBm)"
                else:
                    sig = f"{RED}Weak{RESET} ({rssi} dBm)"
                r = f"\n{BOLD}{CYAN}WiFi Status:{RESET}\n  {si} Status:  {st}\n"
                if conn:
                    r += f"  🌐 Network:  {CYAN}{ssid}{RESET}\n"
                    r += f"  📍 Address:  {MAGENTA}{ip}{RESET}\n"
                    r += f"  📊 Signal:   {sig}"
                return r
        except (ValueError, IndexError):
            pass

    # --- WiFi Scan Results ---
    elif cmd in (":WIFI:SCAN?", ":WIFI:NETWORKS?"):
        try:
            # Format: SSID1,RSSI1,Channel1,AuthMode1;SSID2,RSSI2,Channel2,AuthMode2;...
            networks = response.strip().split(";")
            if networks and networks[0]:
                r = f"\n{BOLD}{CYAN}Available WiFi Networks:{RESET}\n{DIM}{'─' * 60}{RESET}\n"
                for idx, net in enumerate(networks, 1):
                    parts = net.split(",", 3)
                    if len(parts) >= 3:
                        ssid = parts[0].strip()
                        try:
                            rssi = int(parts[1].strip())
                        except ValueError:
                            rssi = 0
                        try:
                            channel = int(parts[2].strip())
                        except ValueError:
                            channel = 0
                        auth = parts[3].strip() if len(parts) > 3 else "Unknown"

                        # Signal strength color and quality
                        if rssi > -50:
                            sig_color = GREEN
                            sig_bar = "█████ Excellent"
                        elif rssi > -60:
                            sig_color = GREEN
                            sig_bar = "████░ Good"
                        elif rssi > -70:
                            sig_color = YELLOW
                            sig_bar = "███░░ Fair"
                        elif rssi > -80:
                            sig_color = YELLOW
                            sig_bar = "██░░░ Weak"
                        else:
                            sig_color = RED
                            sig_bar = "█░░░░ Very Weak"

                        # Security indicators
                        auth_clean = (
                            auth.replace("[", "").replace("]", "").replace("wifi.AuthMode.", "")
                        )
                        if "WPA2" in auth_clean:
                            sec_icon = f"{GREEN}🔒{RESET}"
                            sec_level = "Secure (WPA2)"
                        elif "WPA" in auth_clean:
                            sec_icon = f"{YELLOW}🔓{RESET}"
                            sec_level = "WPA"
                        else:
                            sec_icon = f"{RED}⚠️{RESET}"
                            sec_level = "Open"

                        r += f"  {idx:2d}. {CYAN}{ssid:<30}{RESET} {sec_icon} {sec_level:<20}\n"
                        r += f"      Channel: {channel:2d}  Signal: {sig_color}{sig_bar}{RESET} ({rssi} dBm)\n"

                r += f"{DIM}{'─' * 60}{RESET}"
                return r
        except (ValueError, IndexError, AttributeError):
            pass

    # --- Sensor temperatures ---
    elif cmd in (":PID:TEMPS?", ":PID:TEMP:ALL?"):
        try:
            temps = [float(t) for t in response.split(",")]
            r = f"\n{BOLD}{CYAN}Sensor Temperatures:{RESET}\n"
            for idx, t in enumerate(temps, 1):
                tc = RED if t > 40 else YELLOW if t > 30 else CYAN
                r += f"  🌡️  Sensor {idx}: {tc}{t:6.2f}°C{RESET}\n"
            return r.rstrip()
        except (ValueError, IndexError):
            pass

    # --- Simulation status ---
    elif cmd in (":SIM:STAT?", ":SIM:STATUS?"):
        try:
            pairs = {}
            for part in response.split(","):
                if ":" in part:
                    k, v = part.split(":", 1)
                    pairs[k] = float(v)
            if "ACTUAL" in pairs and "TEC" in pairs and "AMBIENT" in pairs:
                ac, tc, am = pairs["ACTUAL"], pairs["TEC"], pairs["AMBIENT"]
                tcc = RED if abs(tc) > 0.8 else YELLOW if abs(tc) > 0.5 else GREEN
                td = "HEATING" if tc > 0 else "COOLING" if tc < 0 else "OFF"
                tb = "█" * int(abs(tc) * 10)
                r = f"\n{BOLD}{CYAN}Thermal Simulation:{RESET}\n"
                r += f"  🌡️  Actual:   {CYAN}{ac:6.2f}°C{RESET}\n"
                r += f"  🌍 Ambient:  {DIM}{am:6.2f}°C{RESET}\n"
                r += f"  ⚡ TEC:      {tcc}{tc:+6.2f}{RESET} ({td}) {tcc}{tb}{RESET}"
                return r
        except (ValueError, KeyError):
            pass

    # --- :SENS:ALL? ---
    elif cmd in (":SENS:ALL?", ":SENS:TEMP:ALL?"):
        try:
            values = [float(t) for t in response.split(",")]
            if len(values) < 2:
                return response  # Not enough data, return as-is

            # First value is timestamp in seconds, rest are sensor temperatures
            timestamp = values[0]
            temps = values[1:]

            r = f"\n{BOLD}{CYAN}All Sensors:{RESET}\n"
            r += f"  ⏱️  Timestamp:  {DIM}{timestamp:.2f}s{RESET}\n"
            for idx, t in enumerate(temps, 1):
                tc = RED if t > 40 else YELLOW if t > 30 else CYAN
                r += f"  🌡️  Sensor {idx}: {tc}{t:6.2f}°C{RESET}\n"
            return r.rstrip()
        except (ValueError, IndexError):
            pass

    # --- Synchronized sensor measurement ---
    elif cmd.startswith(":SENS:SYNC"):
        try:
            # Format: timestamp,T1,T2,skew_ms,DIFF
            parts = response.split(",")
            if len(parts) >= 5:
                timestamp = float(parts[0])
                t1 = float(parts[1])
                t2 = float(parts[2])
                skew_ms = float(parts[3])
                diff = float(parts[4].strip())

                # Color code temperatures
                t1c = RED if t1 > 40 else YELLOW if t1 > 30 else CYAN
                t2c = RED if t2 > 40 else YELLOW if t2 > 30 else CYAN
                diffc = RED if abs(diff) > 1.0 else YELLOW if abs(diff) > 0.1 else GREEN

                r = f"\n{BOLD}{CYAN}Synchronized Sensor Reading:{RESET}\n"
                r += f"  ⏱️  Timestamp:  {DIM}{timestamp:.3f}s{RESET}\n"
                r += f"  🌡️  Sensor 1:   {t1c}{t1:.4f}°C{RESET}\n"
                r += f"  🌡️  Sensor 2:   {t2c}{t2:.4f}°C{RESET}\n"
                r += f"  ⚡ Skew:       {DIM}{skew_ms:.3f}ms{RESET}\n"
                r += f"  📊 Difference: {diffc}{diff:+.4f}°C{RESET} {DIM}(T1 - T2){RESET}"
                return r
        except (ValueError, IndexError):
            pass

    # --- Single temperature ---
    elif cmd in (":SENS:TEMP?", ":MEAS:TEMP?", ":TEMP?") or (
        cmd.startswith(":SENS") and cmd.endswith(":TEMP?")
    ):
        try:
            t = float(response.strip())
            tc = RED if t > 40 else YELLOW if t > 30 else CYAN
            return f"🌡️  {tc}{t:.2f}°C{RESET}"
        except ValueError:
            pass

    # --- TEC Output ---
    elif cmd in (":TEC:OUTP?", ":TEC:OUTPUT?"):
        try:
            o = float(response.strip())
            oc = RED if abs(o) > 0.8 else YELLOW if abs(o) > 0.5 else GREEN
            ob = "█" * int(abs(o) * 10)
            od = "HEATING" if o > 0 else "COOLING" if o < 0 else "OFF"
            r = f"⚡ TEC Output: {oc}{o:+6.2f}{RESET} ({od})"
            if ob:
                r += f" {oc}{ob}{RESET}"
            return r
        except ValueError:
            pass

    # --- PID Tuning ---
    elif cmd == ":PID:TUNE?":
        try:
            parts = response.split(",")
            if len(parts) >= 3:
                kp, ki, kd = float(parts[0]), float(parts[1]), float(parts[2])
                r = f"\n{BOLD}{CYAN}PID Tuning Parameters:{RESET}\n"
                r += f"  Kp: {GREEN}{kp:.6f}{RESET}\n"
                r += f"  Ki: {GREEN}{ki:.6f}{RESET}\n"
                r += f"  Kd: {GREEN}{kd:.6f}{RESET}"
                return r
        except (ValueError, IndexError):
            pass

    # --- Simple one-liners ---
    elif cmd in (":PID:SETP?", ":PID:SETPOINT?"):
        try:
            return f"🎯 Setpoint: {MAGENTA}{float(response.strip()):.2f}°C{RESET}"
        except ValueError:
            pass
    elif cmd in (":PID:ENAB?", ":PID:ENABLE?"):
        try:
            en = int(response.strip())
            return (
                f"⚡ PID Control: {GREEN}ENABLED{RESET}"
                if en
                else f"⚡ PID Control: {DIM}DISABLED{RESET}"
            )
        except ValueError:
            pass
    elif cmd in (":PID:SENS?", ":PID:SENSOR?"):
        try:
            return f"🔢 Selected Sensor: #{CYAN}{int(response.strip())}{RESET}"
        except ValueError:
            pass
    elif cmd in (":PID:SENS:COUN?", ":PID:SENS:COUNT?", ":PID:SENSOR:COUNT?", ":SENS:COUNT?"):
        try:
            return f"🔢 Sensor Count: {CYAN}{int(response.strip())}{RESET}"
        except ValueError:
            pass
    elif cmd == ":PID:RAMP?":
        try:
            return (
                f"🔥 Ramp: {YELLOW}ACTIVE{RESET}"
                if int(response.strip())
                else f"🔥 Ramp: {DIM}INACTIVE{RESET}"
            )
        except ValueError:
            pass
    elif cmd == ":WIFI:IP?":
        return f"📍 IP Address: {MAGENTA}{response.strip()}{RESET}"
    elif cmd == ":WIFI:MAC?":
        return f"🔖 MAC Address: {CYAN}{response.strip()}{RESET}"
    elif cmd == ":WIFI:SSID?":
        return f"🌐 SSID: {CYAN}{response.strip()}{RESET}"
    elif cmd == ":WIFI:RSSI?":
        try:
            rssi = int(response.strip())
            if rssi > -50:
                sig = f"{GREEN}Excellent{RESET}"
            elif rssi > -60:
                sig = f"{GREEN}Good{RESET}"
            elif rssi > -70:
                sig = f"{YELLOW}Fair{RESET}"
            else:
                sig = f"{RED}Weak{RESET}"
            return f"📊 Signal Strength: {sig} ({rssi} dBm)"
        except ValueError:
            pass
    elif cmd in (":WIFI:CONNECTED?",):
        try:
            c = int(response.strip())
            return (
                f"📶 WiFi: {GREEN}CONNECTED{RESET}" if c else f"📵 WiFi: {DIM}DISCONNECTED{RESET}"
            )
        except ValueError:
            pass
    elif cmd in (":WIFI:ENAB?", ":WIFI:ENABLE?"):
        try:
            e = int(response.strip())
            return f"📶 WiFi: {GREEN}ENABLED{RESET}" if e else f"📶 WiFi: {DIM}DISABLED{RESET}"
        except ValueError:
            pass
    elif cmd == ":WIFI:AVAILABLE?":
        try:
            a = int(response.strip())
            return (
                f"📶 WiFi Hardware: {GREEN}AVAILABLE{RESET}"
                if a
                else f"📶 WiFi Hardware: {RED}NOT AVAILABLE{RESET}"
            )
        except ValueError:
            pass
    elif cmd == ":WIFI:NETWORK:LIST?":
        # Format: SSID1,SSID2,SSID3 or NONE
        if response.strip().upper() == "NONE":
            return f"{DIM}No fallback networks configured{RESET}"
        try:
            networks = [n.strip() for n in response.split(",") if n.strip()]
            if not networks:
                return f"{DIM}No fallback networks configured{RESET}"
            r = f"\n{BOLD}{CYAN}Configured Fallback Networks:{RESET}\n"
            for idx, ssid in enumerate(networks, 1):
                r += f"  {idx}. {CYAN}{ssid}{RESET}\n"
            return r.rstrip()
        except Exception:
            pass
    elif cmd.startswith(":WIFI:NETWORK:ADD"):
        # Handle WiFi network add responses (may include debug info)
        resp = response.strip()
        # Check last non-empty line (response may have debug print statements first)
        last_line = [line for line in resp.split("\n") if line.strip()][-1] if resp else ""
        if last_line.startswith("WARNING"):
            # Show warning message (e.g., added to memory but not saved to disk)
            return f"{YELLOW}⚠{RESET} {last_line}"
        elif last_line.startswith("OK"):
            # Extract SSID from command (format: :WIFI:NETWORK:ADD ssid,password)
            parts = cmd.split(None, 1)
            if len(parts) > 1 and "," in parts[1]:
                ssid = parts[1].split(",")[0].strip()
                # Check for debug info in response
                if "[DEBUG:" in last_line:
                    # Extract debug info
                    debug_match = re.search(r"\[DEBUG:.*?\]", last_line)
                    if debug_match:
                        debug_info = debug_match.group(0)
                        return f"{GREEN}✓{RESET} Fallback network added: {CYAN}{ssid}{RESET}\n{DIM}{debug_info}{RESET}"
                return f"{GREEN}✓{RESET} Fallback network added: {CYAN}{ssid}{RESET}"
            return f"{GREEN}✓{RESET} Fallback network added"
        elif last_line.startswith("ERROR"):
            # Show error with debug info if present
            return f"{RED}✗{RESET} {last_line}"
        # Fall through to show raw response
    elif cmd.startswith(":WIFI:NETWORK:REM"):
        # Handle WiFi network remove responses
        resp = response.strip()
        # Check last non-empty line (response may have debug print statements first)
        last_line = [line for line in resp.split("\n") if line.strip()][-1] if resp else ""
        if last_line.startswith("WARNING"):
            # Show warning message (e.g., removed from memory but not saved to disk)
            return f"{YELLOW}⚠{RESET} {last_line}"
        elif last_line.startswith("OK"):
            # Extract SSID from command if present
            parts = cmd.split(None, 1)
            ssid = parts[1] if len(parts) > 1 else ""
            if ssid:
                return f"{GREEN}✓{RESET} Fallback network removed: {CYAN}{ssid}{RESET}"
            return f"{GREEN}✓{RESET} Fallback network removed"
        elif last_line.startswith("ERROR"):
            return f"{RED}✗{RESET} {last_line}"
    elif cmd == ":WIFI:CONFIG:SAVE":
        # Handle WiFi config save responses
        resp = response.strip()
        if resp.upper().startswith("OK"):
            return f"{GREEN}✓{RESET} WiFi configuration {GREEN}saved{RESET}"
        else:
            return f"{RED}✗{RESET} {resp}"
    elif cmd == ":WIFI:CONFIG:LOAD":
        # Handle WiFi config load responses
        resp = response.strip()
        if resp.upper().startswith("OK"):
            return f"{GREEN}✓{RESET} WiFi configuration {GREEN}reloaded{RESET}"
        else:
            return f"{RED}✗{RESET} {resp}"
    elif cmd == ":TEC:STOP":
        rl = response.strip().lower()
        if "ok" in rl or rl == "0":
            return f"🛑 TEC: {RED}STOPPED{RESET} (output set to 0)"
        return f"🛑 TEC Stop: {response}"
    elif cmd in (":TEC:LIM?", ":TEC:LIMIT?", ":TEC:LIMITS?"):
        try:
            parts = response.split(",")
            if len(parts) >= 2:
                return f"🔒 TEC Limits: {CYAN}{float(parts[0]):+.2f}{RESET} to {CYAN}{float(parts[1]):+.2f}{RESET}"
        except (ValueError, IndexError):
            pass
    elif cmd.startswith(":SIM:RES"):
        rl = response.strip().lower()
        return (
            f"🔄 Simulation: {GREEN}RESET{RESET}"
            if "ok" in rl
            else f"🔄 Simulation Reset: {response}"
        )
    elif cmd.startswith(":SYST:LOG"):
        if response.strip():
            r = f"\n{BOLD}{CYAN}System Log:{RESET}\n{DIM}{'─' * 60}{RESET}\n"

            # Response may have concatenated log entries like: "[8.38s] INFO:   - cmd1;[8.38s] INFO:   - cmd2;..."
            # Split by timestamp pattern: look for patterns like "[N.NNs] LEVEL:"
            log_text = response.strip()

            # First, try to split by explicit newlines (if firmware already includes them)
            lines = log_text.split("\n")

            # Check if we have concatenated entries (multiple timestamps within lines)
            # This can happen if: (a) single line with multiple entries, OR (b) multi-line with concatenated entries
            has_concatenated = any(line.count("[") > 1 for line in lines)

            if has_concatenated:
                # We have concatenated entries within one or more lines
                # Split each line by timestamp pattern, then flatten the result
                split_pattern = r"(?=\[\d+\.\d+s\]\s+(INFO|ERROR|WARNING|DEBUG|WARN)\s*:)"
                all_entries = []
                for line in lines:
                    if "[" in line:
                        # This line has log entries, split it
                        entries = re.split(split_pattern, line)
                        all_entries.extend([e.strip() for e in entries if e.strip() and "[" in e])
                lines = all_entries

            for line in lines:
                if not line.strip():
                    continue

                # Color code based on log level
                if "[ERROR]" in line or "ERROR" in line.upper():
                    r += f"{RED}{line}{RESET}\n"
                elif "[WARNING]" in line or "WARN" in line.upper():
                    r += f"{YELLOW}{line}{RESET}\n"
                elif "[INFO]" in line:
                    r += f"{CYAN}{line}{RESET}\n"
                elif "[DEBUG]" in line:
                    r += f"{DIM}{line}{RESET}\n"
                else:
                    r += f"{line}\n"

            r += f"{DIM}{'─' * 60}{RESET}"
            return r
        return f"{DIM}(no log entries){RESET}"
    elif cmd.startswith(":SYST:CLIENTS"):
        if response.strip():
            r = f"\n{BOLD}{CYAN}TCP SCPI Clients:{RESET}\n{DIM}{'─' * 60}{RESET}\n"

            # Response format: count,IP1:Port1,status1;IP2:Port2,status2;...
            # or: count\nIP1:Port1,status1\nIP2:Port2,status2\n...
            lines = response.strip().split("\n")
            first_line = lines[0]

            try:
                # Try to parse count from first line
                if first_line.startswith("ERROR"):
                    return f"{RED}✗ {first_line}{RESET}"

                parts = first_line.split(",")
                try:
                    count = int(parts[0])
                except ValueError:
                    # If first part is not an int, treat the whole response as one line
                    count = len(lines)
                    lines = [response.strip()]

                if count == 0:
                    r += f"  {DIM}No clients connected{RESET}\n"
                else:
                    r += f"  {CYAN}{count} client{'' if count == 1 else 's'} connected:{RESET}\n\n"

                    client_list = []
                    # Prefer clients from subsequent lines (avoid treating the first line as a client)
                    for line in lines[1:] if len(lines) > 1 and count > 0 else []:
                        if ":" in line and ("," in line or ";" not in line):
                            client_list.append(line.strip())
                        elif ";" in line:
                            # Handle semicolon-separated format
                            for client in line.split(";"):
                                if client.strip():
                                    client_list.append(client.strip())

                    # If still empty and count > 0, parse remainder of the first line after the first comma
                    if not client_list and count > 0 and "," in first_line:
                        remainder = first_line.split(",", 1)[1].strip()
                        if remainder:
                            if ";" in remainder:
                                client_list.extend(
                                    [c.strip() for c in remainder.split(";") if c.strip()]
                                )
                            else:
                                client_list.append(remainder)

                    for idx, client in enumerate(client_list[:count], 1):
                        # Parse client info: IP:Port,status or just IP:Port
                        if "," in client:
                            addr, status = client.split(",", 1)
                            status_str = status.strip()
                        else:
                            addr = client
                            status_str = "connected"

                        # Format address
                        if ":" in addr:
                            ip, port = addr.rsplit(":", 1)
                            try:
                                port_num = int(port)
                            except ValueError:
                                port_num = port
                        else:
                            ip = addr
                            port_num = "?"

                        # Status indicator
                        if status_str.lower() in ("active", "connected", "ok", "1"):
                            status_icon = f"{GREEN}●{RESET}"
                            status_text = f"{GREEN}Connected{RESET}"
                        elif status_str.lower() in ("idle", "0"):
                            status_icon = f"{YELLOW}●{RESET}"
                            status_text = f"{YELLOW}Idle{RESET}"
                        else:
                            status_icon = f"{DIM}●{RESET}"
                            status_text = f"{DIM}{status_str}{RESET}"

                        r += f"  {idx}. {status_icon} {MAGENTA}{ip}{RESET}:{CYAN}{port_num}{RESET} {status_text}\n"

                r += f"{DIM}{'─' * 60}{RESET}"
                return r
            except (ValueError, IndexError) as e:
                return (
                    f"{YELLOW}⚠ Client List Parse Error:{RESET}\n{response}\n{DIM}Error: {e}{RESET}"
                )
        return f"{DIM}(no clients connected){RESET}"
    elif cmd in (":SYST:CAL:SAVE",):
        rl = response.strip().lower()
        return (
            f"💾 Calibration: {GREEN}SAVED{RESET}"
            if ("ok" in rl or "saved" in rl)
            else f"💾 Calibration Save: {response}"
        )
    elif cmd in (":SYST:CAL:LOAD",):
        rl = response.strip().lower()
        return (
            f"📂 Calibration: {GREEN}LOADED{RESET}"
            if ("ok" in rl or "loaded" in rl)
            else f"📂 Calibration Load: {response}"
        )
    elif cmd in (":SYST:CAL:DEL", ":SYST:CAL:DELETE"):
        rl = response.strip().lower()
        return (
            f"🗑️  Calibration: {YELLOW}DELETED{RESET}"
            if ("ok" in rl or "deleted" in rl)
            else f"🗑️  Calibration Delete: {response}"
        )
    elif cmd in (":SYST:MODE?",):
        mode = response.strip().upper()
        if mode == "SIM":
            return f"🔧 System Mode: {YELLOW}SIMULATION{RESET}"
        elif mode == "REAL":
            return f"🔧 System Mode: {GREEN}REAL HARDWARE{RESET}"
        return f"🔧 System Mode: {CYAN}{mode}{RESET}"
    elif cmd.upper().startswith(":SYST:MODE") and not cmd.endswith("?"):
        # Handle mode switch command responses
        resp = response.strip()
        # Check if it's an error code (format: "code,message")
        if resp.startswith("-") or ("," in resp and resp.split(",")[0].lstrip("-").isdigit()):
            # Parse SCPI error
            try:
                parts = resp.split(",", 1)
                code = int(parts[0])
                msg = parts[1] if len(parts) > 1 else "Unknown error"
                if code == -109:
                    return f"{RED}✗ Mode switch failed:{RESET} {YELLOW}Missing or incorrect password{RESET}"
                else:
                    return f"{RED}✗ Mode switch failed:{RESET} {msg} (error {code})"
            except (ValueError, IndexError):
                return f"{RED}✗ Mode switch failed:{RESET} {resp}"
        # If it contains log messages with timestamps, it's a successful mode switch
        elif "[" in resp and "s]" in resp and "INFO" in resp:
            return f"{GREEN}✓{RESET} Mode switch in progress:\n{CYAN}{resp}{RESET}"
        # Otherwise, pass through as-is
        return resp
    # :SYST:USB:MODE — device now returns OK,REBOOT_REQUIRED or error
    elif cmd.upper().startswith(":SYST:USB:MODE") and not cmd.endswith("?"):
        resp = response.strip()
        # Extract mode from the command for display
        cmd_parts = cmd.strip().split()
        mode_part = cmd_parts[1].upper() if len(cmd_parts) > 1 else "?"
        if resp == "OK,REBOOT_REQUIRED":
            return f"{GREEN}✓{RESET} USB mode set to {CYAN}{mode_part}{RESET} {DIM}(reboot required){RESET}"
        # Check if it's an error code
        if resp.startswith("-") or ("," in resp and resp.split(",")[0].lstrip("-").isdigit()):
            try:
                parts = resp.split(",", 1)
                code = int(parts[0])
                msg = parts[1] if len(parts) > 1 else "Unknown error"
                if code == -109:
                    return f"{RED}✗ USB mode change failed:{RESET} {YELLOW}Missing password{RESET}"
                elif code == -220:
                    return f"{RED}✗ USB mode change failed:{RESET} {YELLOW}Invalid password{RESET}"
                elif code == -108:
                    return f"{RED}✗ USB mode change failed:{RESET} {YELLOW}Invalid mode (use PROD/DEV/DEBUG){RESET}"
                elif code == -350:
                    return f"{RED}✗ USB mode change failed:{RESET} {YELLOW}Filesystem write failed{RESET}"
                else:
                    return f"{RED}✗ USB mode change failed:{RESET} {msg} (error {code})"
            except (ValueError, IndexError):
                return f"{RED}✗ USB mode change failed:{RESET} {resp}"
        return resp
    elif cmd in (":SYST:RES", ":SYST:RESET", "*RST") or response == "RESET_COMMAND_SENT":
        return f"🔄 System: {YELLOW}RESETTING...{RESET}"
    elif cmd == "*IDN?":
        parts = response.strip().split(",")
        if len(parts) >= 4:
            r = f"\n{BOLD}{CYAN}Device Identification:{RESET}\n"
            r += f"  Manufacturer: {GREEN}{parts[0].strip()}{RESET}\n"
            r += f"  Model:        {GREEN}{parts[1].strip()}{RESET}\n"
            r += f"  Serial:       {MAGENTA}{parts[2].strip()}{RESET}\n"
            r += f"  Version:      {CYAN}{parts[3].strip()}{RESET}"
            return r
    elif cmd.startswith(":HELP") or cmd.startswith("HELP"):
        return f"\n{BOLD}{CYAN}Help:{RESET}\n{response}"

    # --- OTA Update Status (:SYST:UPD:STATUS?) ---
    elif cmd == ":SYST:UPD:STATUS?":
        try:
            # Parse comma-separated key:value format
            # Format: state:IDLE,authenticated:0,target:none,received:0,expected:0,progress:0.0%
            data = {}
            for part in response.split(","):
                if ":" in part:
                    key, value = part.split(":", 1)
                    data[key] = value.strip()

            state = data.get("state", "UNKNOWN")
            authenticated = data.get("authenticated", "0") == "1"
            target = data.get("target", "none")
            received = data.get("received", "0")
            expected = data.get("expected", "0")
            progress = data.get("progress", "0.0%")

            # State indicator and color
            if state == "IDLE":
                state_icon = f"{DIM}○{RESET}"
                state_str = f"{DIM}IDLE{RESET}"
            elif state == "AUTHENTICATED":
                state_icon = f"{CYAN}●{RESET}"
                state_str = f"{CYAN}AUTHENTICATED{RESET}"
            elif state == "RECEIVING":
                state_icon = f"{YELLOW}▶{RESET}"
                state_str = f"{YELLOW}RECEIVING{RESET}"
            elif state == "VERIFYING":
                state_icon = f"{YELLOW}⏳{RESET}"
                state_str = f"{YELLOW}VERIFYING{RESET}"
            elif state == "COMMITTED":
                state_icon = f"{GREEN}✓{RESET}"
                state_str = f"{GREEN}COMMITTED{RESET}"
            elif state == "FAILED":
                state_icon = f"{RED}✗{RESET}"
                state_str = f"{RED}FAILED{RESET}"
            else:
                state_icon = "?"
                state_str = state

            # Authentication status
            auth_icon = f"{GREEN}✓{RESET}" if authenticated else f"{DIM}✗{RESET}"
            auth_str = f"{GREEN}Yes{RESET}" if authenticated else f"{DIM}No{RESET}"

            # Format output
            r = f"\n{BOLD}{CYAN}OTA Update Status:{RESET}\n"
            r += f"  {state_icon} State:         {state_str}\n"
            r += f"  {auth_icon} Authenticated: {auth_str}\n"
            r += f"  📄 Target:        {MAGENTA}{target}{RESET}\n"
            r += f"  📥 Received:      {CYAN}{received}{RESET} bytes\n"
            r += f"  📊 Expected:      {CYAN}{expected}{RESET} bytes\n"
            r += f"  ⏱️  Progress:      {YELLOW}{progress}{RESET}"

            return r
        except (ValueError, IndexError, KeyError):
            pass

    return response


# ===================================================================
# Command helpers
# ===================================================================
def get_command_confirmation(command: str) -> str:
    """Descriptive confirmation for write (non-query) commands."""
    cu = command.strip().upper()
    parts = cu.split()
    base = parts[0] if parts else cu
    param = " ".join(parts[1:]) if len(parts) > 1 else ""

    # Detect malformed commands (ending with colon indicates incomplete command)
    if base.endswith(":") and not base.startswith("*"):
        return (
            f"{YELLOW}⚠{RESET} Incomplete command: {DIM}{base}{RESET} (missing subcommand or query)"
        )

    if base.startswith(":TEC:OUTP") or base.startswith(":TEC:OUTPUT"):
        if param:
            try:
                v = float(param)
                d = "HEATING" if v > 0 else "COOLING" if v < 0 else "OFF"
                vc = RED if abs(v) > 0.7 else YELLOW if abs(v) > 0.3 else GREEN
                return f"{GREEN}✓{RESET} TEC output set to {vc}{v:+.2f}{RESET} {DIM}({d}){RESET}"
            except ValueError:
                return f"{GREEN}✓{RESET} TEC output set to {CYAN}{param}{RESET}"
        return f"{GREEN}✓{RESET} TEC output configured"
    elif base == ":TEC:STOP":
        return f"{GREEN}✓{RESET} TEC stopped {DIM}(output set to 0){RESET}"
    elif base.startswith(":PID:ENAB"):
        if param in ("1", "ON"):
            return f"{GREEN}✓{RESET} PID control {GREEN}enabled{RESET}"
        elif param in ("0", "OFF"):
            return f"{GREEN}✓{RESET} PID control {YELLOW}disabled{RESET}"
        return f"{GREEN}✓{RESET} PID enable state changed"
    elif base.startswith(":PID:SETP"):
        return (
            f"{GREEN}✓{RESET} PID setpoint set to {MAGENTA}{param}°C{RESET}"
            if param
            else f"{GREEN}✓{RESET} PID setpoint configured"
        )
    elif base == ":PID:KP":
        return (
            f"{GREEN}✓{RESET} Kp set to {CYAN}{param}{RESET}"
            if param
            else f"{GREEN}✓{RESET} Kp configured"
        )
    elif base == ":PID:KI":
        return (
            f"{GREEN}✓{RESET} Ki set to {CYAN}{param}{RESET}"
            if param
            else f"{GREEN}✓{RESET} Ki configured"
        )
    elif base == ":PID:KD":
        return (
            f"{GREEN}✓{RESET} Kd set to {CYAN}{param}{RESET}"
            if param
            else f"{GREEN}✓{RESET} Kd configured"
        )
    elif base.startswith(":PID:TUNE"):
        return (
            f"{GREEN}✓{RESET} PID tuning set: {CYAN}{param}{RESET}"
            if param
            else f"{GREEN}✓{RESET} PID tuning configured"
        )
    elif base.startswith(":PID:RES"):
        return f"{GREEN}✓{RESET} PID controller reset {DIM}(integral term cleared){RESET}"
    elif base.startswith(":PID:SENS") and not base.endswith("?"):
        return (
            f"{GREEN}✓{RESET} PID sensor {CYAN}#{param}{RESET} selected"
            if param
            else f"{GREEN}✓{RESET} PID sensor selected"
        )
    elif base.startswith(":PID:RAMP") and not base.endswith("?"):
        if ":STOP" in base or ":CANC" in base:
            return f"{GREEN}✓{RESET} Temperature ramp {YELLOW}stopped{RESET}"
        if param:
            rp = param.split(",")
            if len(rp) >= 2:
                return f"{GREEN}✓{RESET} Ramp started: target {MAGENTA}{rp[0]}°C{RESET} at {CYAN}{rp[1]}°C/min{RESET} {DIM}(PID auto-enabled){RESET}"
            return f"{GREEN}✓{RESET} Ramp configured: {CYAN}{param}{RESET} {DIM}(PID auto-enabled){RESET}"
        return f"{GREEN}✓{RESET} Temperature ramp configured {DIM}(PID auto-enabled){RESET}"
    elif base.startswith(":SIM:RES"):
        ti = f" at {CYAN}{param}°C{RESET}" if param else f" to {DIM}ambient{RESET}"
        return f"{GREEN}✓{RESET} Thermal simulation reset{ti}"
    elif base.startswith(":SYST:CAL:SAVE"):
        return f"{GREEN}✓{RESET} Calibration {GREEN}saved{RESET}"
    elif base.startswith(":SYST:CAL:LOAD"):
        return f"{GREEN}✓{RESET} Calibration {GREEN}loaded{RESET}"
    elif base.startswith(":SYST:CAL:DEL"):
        return f"{GREEN}✓{RESET} Calibration file {YELLOW}deleted{RESET}"
    elif base.startswith(":SYST:USB:MODE"):
        mode_part = param.split()[0].upper() if param else ""
        return f"{GREEN}✓{RESET} USB mode set to {CYAN}{mode_part or '?'}{RESET} {DIM}(reboot required){RESET}"
    elif base.startswith(":SYST:MODE"):
        # Extract mode from param (e.g., "SIM password" or "REAL password")
        mode_part = param.split()[0].upper() if param else ""
        if mode_part == "SIM":
            return f"{GREEN}✓{RESET} System mode confirmed: {YELLOW}SIMULATION{RESET}"
        elif mode_part == "REAL":
            return f"{GREEN}✓{RESET} System mode confirmed: {GREEN}REAL HARDWARE{RESET}"
        return f"{GREEN}✓{RESET} System mode changed"
    elif base.startswith(":SYST:RES"):
        return f"{GREEN}✓{RESET} System reset {YELLOW}initiated{RESET}"
    elif base == "*RST":
        return f"{GREEN}✓{RESET} Device reset {DIM}(status cleared, sensors reset){RESET}"
    elif base == "*CLS":
        return f"{GREEN}✓{RESET} Status registers cleared"
    elif base == "*OPC":
        return f"{GREEN}✓{RESET} Operation complete flag set"
    elif base.startswith(":WIFI:ENAB"):
        if param in ("1", "ON"):
            return f"{GREEN}✓{RESET} WiFi {GREEN}enabled{RESET}"
        elif param in ("0", "OFF"):
            return f"{GREEN}✓{RESET} WiFi {YELLOW}disabled{RESET}"
        return f"{GREEN}✓{RESET} WiFi state changed"
    elif base.startswith(":WIFI:CONN"):
        return f"{GREEN}✓{RESET} WiFi connection initiated"
    elif base.startswith(":WIFI:DISC"):
        return f"{GREEN}✓{RESET} WiFi {YELLOW}disconnected{RESET}"
    # Note: :WIFI:NETWORK:ADD and :WIFI:NETWORK:REMOVE are handled in format_response()
    # to properly show WARNING/ERROR messages with debug info
    elif base.startswith(":WIFI:CONFIG:SAVE"):
        return f"{GREEN}✓{RESET} WiFi configuration {GREEN}saved{RESET}"
    elif base.startswith(":WIFI:CONFIG:LOAD"):
        return f"{GREEN}✓{RESET} WiFi configuration {GREEN}reloaded{RESET}"
    elif base.startswith(":WIFI:SSID") and not base.endswith("?"):
        if param:
            return f"{GREEN}✓{RESET} WiFi SSID set to: {CYAN}{param}{RESET}"
        return f"{GREEN}✓{RESET} WiFi SSID configured"
    elif base.startswith(":WIFI:HOSTNAME") and not base.endswith("?"):
        if param:
            return f"{GREEN}✓{RESET} Hostname set to: {CYAN}{param}{RESET}"
        return f"{GREEN}✓{RESET} Hostname configured"
    elif base.startswith(":WIFI:PASSWORD") and not base.endswith("?"):
        return f"{GREEN}✓{RESET} WiFi password {DIM}updated{RESET} (not shown for security)"
    elif base.startswith(":WIFI:AUTOCONN") and not base.endswith("?"):
        if param in ("1", "ON"):
            return f"{GREEN}✓{RESET} WiFi auto-connect {GREEN}enabled{RESET}"
        elif param in ("0", "OFF"):
            return f"{GREEN}✓{RESET} WiFi auto-connect {YELLOW}disabled{RESET}"
        return f"{GREEN}✓{RESET} WiFi auto-connect configured"
    return f"{GREEN}✓{RESET} Command completed"


def send_scpi_command(
    ser, command: str, timeout: float = 2.0, retry_count: int = 3, sync_mode: bool = True
) -> str | None:
    """Send an SCPI command with optional OPC synchronization, return response."""
    cmd = command.strip()
    # A SCPI query has "?" on the command token, e.g. ":SENS:SYNC? 64" — the token
    # ":SENS:SYNC?" ends with "?" even though the full string does not.
    is_query = cmd.split(None, 1)[0].endswith("?") if cmd else False
    is_reset_command = cmd.upper() in (":SYST:RES", "*RST")
    is_mode_command = cmd.upper().startswith(":SYST:MODE") and not is_query

    # Some commands take longer to execute, use extended timeout
    if cmd.upper().startswith(":WIFI:SCAN"):
        timeout = 8.0  # WiFi scan can take up to 5-7 seconds
    elif is_mode_command:
        timeout = 6.0  # Mode switching can take time (sensor initialization)

    if sync_mode and not is_query and not cmd.startswith("*") and not is_reset_command:
        cmd_to_send = f"{cmd};*OPC?\n"
    else:
        cmd_to_send = cmd + "\n" if not cmd.endswith("\n") else cmd

    for attempt in range(retry_count):
        # Skip buffer reset for reset commands - they need to be sent immediately
        if not is_reset_command:
            try:
                ser.reset_input_buffer()
            except (BrokenPipeError, ConnectionResetError, ConnectionError, OSError, termios.error):
                # Connection lost or device disconnected, cannot continue
                return "ERROR: Connection lost (device disconnected)"

        try:
            ser.write(cmd_to_send.encode("utf-8"))
            ser.flush()
        except (BrokenPipeError, ConnectionResetError, ConnectionError, OSError) as e:
            # Connection lost while writing
            if attempt == retry_count - 1:
                return f"ERROR: Connection lost while sending command: {e}"
            time.sleep(0.5)
            continue

        # For reset commands, the device will disconnect immediately.
        # VXI-11 instruments (e.g. oscilloscopes) stay connected after *RST,
        # so we return a normal confirmation instead of breaking the session.
        if is_reset_command:
            time.sleep(0.2)  # Brief wait for command to be processed
            if getattr(ser, "is_vxi11", False):
                return get_command_confirmation(cmd)
            return "RESET_COMMAND_SENT"

        start = time.time()
        lines: list[str] = []

        while (time.time() - start) < timeout:
            try:
                if ser.in_waiting > 0:
                    try:
                        line = ser.readline().decode("utf-8", errors="replace").strip()
                        if not line:
                            continue
                        if line == cmd or line == cmd_to_send.strip():
                            continue
                        # Special handling for system logs which may span multiple readline() calls
                        if cmd.startswith(":SYST:LOG"):
                            # For logs, collect data and check if more is coming
                            lines.append(line)
                            time.sleep(0.05)
                            if ser.in_waiting > 0:
                                # More data coming, continue reading
                                continue
                            else:
                                # No more data, return everything collected
                                return "\n".join(lines) if len(lines) > 1 else line
                        # Special handling for :SYST:MODE commands - check for errors
                        elif is_mode_command:
                            lines.append(line)
                            # Check if this is an error response
                            if line.startswith("-") or "error" in line.lower():
                                return line
                            # Check if this is an OPC response
                            if line == "1":
                                # Query for errors to see if mode switch actually failed
                                time.sleep(0.1)
                                try:
                                    ser.write(b":SYST:ERR?\n")
                                    time.sleep(0.1)
                                    if ser.in_waiting > 0:
                                        err_resp = (
                                            ser.readline().decode("utf-8", errors="replace").strip()
                                        )
                                        # SCPI error format: "error_code,error_message"
                                        if err_resp and not err_resp.startswith("0,"):
                                            # There was an error, return it instead of success message
                                            return err_resp
                                except Exception:
                                    pass
                                # No error, but might have log messages
                                time.sleep(0.5)  # Wait for potential log messages
                                if ser.in_waiting > 0:
                                    # Collect log messages (filter out internal responses)
                                    while ser.in_waiting > 0:
                                        try:
                                            msg = (
                                                ser.readline()
                                                .decode("utf-8", errors="replace")
                                                .strip()
                                            )
                                            # Filter out OPC response, error query responses, and empty lines
                                            if (
                                                msg
                                                and msg != "1"
                                                and not msg.startswith(":SYST:ERR")
                                                and not msg.startswith("0,")
                                            ):
                                                lines.append(msg)
                                            time.sleep(0.05)
                                        except Exception:
                                            break
                                    # Return collected log messages (skip the first "1" if present)
                                    log_messages = [line for line in lines if line != "1"]
                                    if len(log_messages) > 1:
                                        return "\n".join(log_messages[1:])
                                    elif log_messages:
                                        return log_messages[0]
                                    else:
                                        return get_command_confirmation(cmd)
                                else:
                                    return get_command_confirmation(cmd)
                            # For log messages from mode switch, keep collecting
                            time.sleep(0.2)
                            if ser.in_waiting > 0:
                                continue
                            else:
                                return "\n".join(lines)
                        elif is_query:
                            # VXI-11 binary responses span multiple lines — drain the buffer
                            if getattr(ser, "is_vxi11", False) and ser.in_waiting > 0:
                                collected = [line]
                                while ser.in_waiting > 0:
                                    # Use rstrip only — preserve leading spaces (sparkline indent)
                                    extra = (
                                        ser.readline().decode("utf-8", errors="replace").rstrip()
                                    )
                                    if extra.strip():
                                        collected.append(extra)
                                return "\n".join(collected)
                            return line
                        else:
                            # For commands, check sync mode
                            lines.append(line)
                            if sync_mode:
                                if line == "1":
                                    return get_command_confirmation(cmd)
                                elif line.startswith("-") or "error" in line.lower():
                                    return line
                                elif line.startswith("OK") or line.startswith("ERROR"):
                                    # Device returned explicit OK/ERROR response (may include debug info)
                                    return line
                            else:
                                time.sleep(0.1)
                                if not ser.in_waiting:
                                    return line if lines else get_command_confirmation(cmd)
                    except (BrokenPipeError, ConnectionResetError, ConnectionError, OSError) as e:
                        # Connection lost while reading
                        if attempt == retry_count - 1:
                            return f"ERROR: Connection lost while reading response: {e}"
                        break
            except (BrokenPipeError, ConnectionResetError, ConnectionError, OSError) as e:
                # Connection lost while checking buffer
                if attempt == retry_count - 1:
                    return f"ERROR: Connection lost: {e}"
                break

            time.sleep(0.01)

        if lines:
            return "\n".join(lines)
        if attempt < retry_count - 1:
            time.sleep(0.2)

    return None


# ===================================================================
# Help text
# ===================================================================
def print_help(device_type: str | None = None) -> None:
    """Print common SCPI commands, tailored to the connected instrument."""
    _LABELS = {
        "relaybank16": "RelayBank16",
        "tmp117": "TMP117",
        "3dmag": "3Dmag",
        "rtdmax31865": "RTDMax31865",
        "keithley6221": "Keithley 6221",
    }
    device_label = _LABELS.get(device_type or "", "")
    header = f"Common SCPI Commands — {device_label}" if device_label else "Common SCPI Commands"
    print(f"\n{BOLD}{CYAN}{header}:{RESET}")

    print(f"\n  {BOLD}{YELLOW}Standard Commands (IEEE-488.2):{RESET}")
    print(f"    {GREEN}*IDN?{RESET}           - Get device identification")
    print(f"    {GREEN}*RST{RESET}            - Reset device")
    print(f"    {GREEN}*CLS{RESET}            - Clear status")
    print(f"    {GREEN}*OPC?{RESET}           - Operation complete query")

    if device_type == "relaybank16":
        print(f"\n  {BOLD}{YELLOW}Route — Bulk Relay Control:{RESET}")
        print(f"    {GREEN}:ROUT:CLOS <n[,n,...]>{RESET}  - Close (activate) one or more relays")
        print(f"    {GREEN}:ROUT:OPEN <n[,n,...]>{RESET}  - Open (deactivate) one or more relays")
        print(f"    {GREEN}:ROUT:MASK <0xHHHH>{RESET}     - Set all 16 relays from hex bitmask")
        print(f"    {GREEN}:ROUT:MASK?{RESET}             - Query relay states as 16-bit hex")
        print(f"    {GREEN}:ROUT:MASK:ALL?{RESET}         - Query relay states as 16-char binary")
        print(f"    {GREEN}:ROUT:TEST [dur]{RESET}        - Sequence-test all relays (dur s each)")

        print(
            f"\n  {BOLD}{YELLOW}Individual Relay Control ({DIM}n = 1..16{RESET}{BOLD}{YELLOW}):{RESET}"
        )
        print(f"    {GREEN}:RELA<n>:STAT ON|OFF{RESET}    - Activate / deactivate relay n")
        print(f"    {GREEN}:RELA<n>:STAT?{RESET}          - Query relay n state (ON/OFF)")
        print(f"    {GREEN}:RELA<n>:PULS [dur]{RESET}     - Pulse relay n (optional duration, s)")
        print(f"    {GREEN}:RELA:PULS:DUR <s>{RESET}      - Set default pulse duration (s)")
        print(f"    {GREEN}:RELA:PULS:DUR?{RESET}         - Query default pulse duration")

        print(f"\n  {BOLD}{YELLOW}Diagnostics:{RESET}")
        print(f"    {GREEN}:DIAG:PING?{RESET}             - Connectivity check (returns PONG)")

        print(f"\n  {BOLD}{YELLOW}System:{RESET}")
        print(f"    {GREEN}:SYST:ERR?{RESET}              - Get last error")
        print(f"    {GREEN}:SYST:CLIENTS?{RESET}          - Get connected TCP clients")
        print(f"    {GREEN}:SYST:RES{RESET}               - Reboot device")
        print(f"    {GREEN}:SYST:VERS?{RESET}             - Query firmware version")

    elif device_type == "tmp117":
        print(f"\n  {BOLD}{YELLOW}Temperature Measurement:{RESET}")
        print(f"    {GREEN}:SENS:TEMP?{RESET}     - Read current temperature")
        print(f"    {GREEN}:SENS:ALL?{RESET}      - Read all sensors (with timestamp)")
        print(f"    {GREEN}:SENS:SYNC? [n]{RESET} - Synchronized reading (n samples, default 8)")
        print(f"    {GREEN}:SENS:COUNT?{RESET}    - Get sensor count")

        print(f"\n  {BOLD}{YELLOW}PID Control:{RESET}")
        print(f"    {GREEN}:PID:SENS <n>{RESET}   - Select sensor (1-4)")
        print(f"    {GREEN}:PID:SETP <t>{RESET}   - Set target temperature (°C)")
        print(f"    {GREEN}:PID:ENAB <0|1>{RESET} - Enable/disable PID")
        print(f"    {GREEN}:PID:STAT?{RESET}      - Get PID status")
        print(f"    {GREEN}:PID:TELE?{RESET}      - Get telemetry")
        print(f"    {GREEN}:PID:TEMPS?{RESET}     - Get all sensor temperatures")

        print(f"\n  {BOLD}{YELLOW}Temperature Ramps:{RESET}")
        print(f"    {GREEN}:PID:RAMP <targ>,<rate>[,<unit>][,<hold>]{RESET}  - Start ramp")
        print(f"    {DIM}            unit: MIN (default) or SEC{RESET}")
        print(f"    {DIM}            hold: hold time at target (sec){RESET}")
        print(f"    {GREEN}:PID:RAMP:STAT?{RESET}                         - Detailed ramp status")
        print(f"    {GREEN}:PID:RAMP:STOP{RESET}                          - Stop current ramp")

        print(f"\n  {BOLD}{YELLOW}PID Tuning:{RESET}")
        print(f"    {GREEN}:PID:KP <val>{RESET}   - Set proportional gain")
        print(f"    {GREEN}:PID:KI <val>{RESET}   - Set integral gain")
        print(f"    {GREEN}:PID:KD <val>{RESET}   - Set derivative gain")
        print(f"    {GREEN}:PID:TUNE?{RESET}      - Query all tuning parameters")
        print(f"    {GREEN}:PID:RES{RESET}        - Reset PID (clear integral term)")

        print(f"\n  {BOLD}{YELLOW}TEC Control:{RESET}")
        print(f"    {GREEN}:TEC:OUTP <-1..1>{RESET} - Set TEC output (normalized)")
        print(f"    {GREEN}:TEC:OUTP?{RESET}        - Query TEC output")
        print(f"    {GREEN}:TEC:STOP{RESET}         - Stop TEC (set to 0)")

        print(f"\n  {BOLD}{YELLOW}System:{RESET}")
        print(f"    {GREEN}:SYST:MODE?{RESET}             - Query current mode (SIM/REAL)")
        print(f"    {GREEN}:SYST:MODE <m> <pw>{RESET}     - Switch mode (SIM/REAL, password)")
        print(
            f"    {GREEN}:SYST:USB:MODE <m> <pw>{RESET} - Set USB mode (PROD/DEV/DEBUG, password)"
        )
        print(f"    {GREEN}:SYST:ERR?{RESET}              - Get last error")
        print(f"    {GREEN}:SYST:LOG? [n]{RESET}          - Get system log (last n lines)")
        print(f"    {GREEN}:SYST:CLIENTS?{RESET}          - Get connected TCP clients")

    elif device_type == "3dmag":
        print(f"\n  {BOLD}{YELLOW}Magnetic Field Measurement:{RESET}")
        print(f"    {GREEN}:READ?{RESET}          - Read sensor (timestamp, Bx, By, Bz, temp °C)")
        print(f"    {GREEN}:READ:BINary?{RESET}   - Binary read (5 × little-endian float32)")
        print(f"    {GREEN}:DATA:POINts?{RESET}   - Number of fields per reading (5)")
        print(f"    {GREEN}:UNIT:MAGnetic?{RESET} - Field unit (GAUSS)")

        print(f"\n  {BOLD}{YELLOW}Sensor Info:{RESET}")
        print(f"    {GREEN}:SENS:TYPE?{RESET}            - Sensor model (MLX90393, TLV493D, …)")
        print(f"    {GREEN}:SENS:ADDRess?{RESET}         - I2C address")

        print(f"\n  {BOLD}{YELLOW}MLX90393 Measurement Profiles:{RESET}")
        print(f"    {GREEN}:SENS:PROFile?{RESET}         - Query active profile")
        print(f"    {GREEN}:SENS:PROFile FAST{RESET}     - Fast (~1 ms, lower accuracy)")
        print(f"    {GREEN}:SENS:PROFile DEFAULT{RESET}  - Balanced (~10 ms)")
        print(f"    {GREEN}:SENS:PROFile HIACC{RESET}    - High accuracy (~100 ms)")

        print(f"\n  {BOLD}{YELLOW}Diagnostics:{RESET}")
        print(f"    {GREEN}:DIAG:PING?{RESET}            - Connectivity check (returns PONG)")

        print(f"\n  {BOLD}{YELLOW}System:{RESET}")
        print(f"    {GREEN}:SYST:ERR?{RESET}              - Get last error")
        print(f"    {GREEN}:SYST:VERS?{RESET}             - Query firmware version")
        print(f"    {GREEN}:SYST:LOG? [n]{RESET}          - Get system log (last n lines)")
        print(f"    {GREEN}:SYST:CLIENTS?{RESET}          - Get connected TCP clients")
        print(f"    {GREEN}:SYST:RES{RESET}               - Reboot device")
        print(
            f"    {GREEN}:SYST:USB:MODE <m> <pw>{RESET} - Set USB mode (PROD/DEV/DEBUG, password)"
        )

    elif device_type == "rtdmax31865":
        print(
            f"\n  {BOLD}{YELLOW}Temperature Measurement ({DIM}n = sensor index{RESET}{BOLD}{YELLOW}):{RESET}"
        )
        print(f"    {GREEN}:SENS:COUNT?{RESET}       - Number of connected sensors")
        print(f"    {GREEN}:SENS:ALL?{RESET}         - All temperatures (timestamp, T1, T2, …)")
        print(f"    {GREEN}:SENS:ALLD?{RESET}        - All temperatures + difference")
        print(f"    {GREEN}:SENS:DIFF?{RESET}        - Temperature difference T1−T2")
        print(f"    {GREEN}:SENSn:TEMP?{RESET}       - Temperature of sensor n (°C)")
        print(f"    {GREEN}:SENSn:RES?{RESET}        - Resistance of sensor n (Ω)")
        print(f"    {GREEN}:SENSn:RAW?{RESET}        - Raw 16-bit ADC code of sensor n")
        print(f"    {GREEN}:UNIT:TEMPerature?{RESET} - Temperature unit (CELSIUS)")

        print(
            f"\n  {BOLD}{YELLOW}Sensor Configuration ({DIM}n = sensor index{RESET}{BOLD}{YELLOW}):{RESET}"
        )
        print(f"    {GREEN}:CONFn:RTD:WIRE?{RESET}    - Wire count (2 or 4, read-only)")
        print(f"    {GREEN}:CONFn:RTD:RTDN [Ω]{RESET} - RTD nominal resistance (query/set)")
        print(f"    {GREEN}:CONFn:RTD:REFR [Ω]{RESET} - Reference resistor (query/set)")
        print(f"    {GREEN}:CONFn:BIAS [0|1]{RESET}   - Bias current enable (query/set)")
        print(f"    {GREEN}:CONFn:AUTO [0|1]{RESET}   - Auto-convert enable (query/set)")
        print(f"    {GREEN}:CONFn:FILT:FREQ?{RESET}   - Filter frequency (50 or 60 Hz)")

        print(
            f"\n  {BOLD}{YELLOW}Fault Status ({DIM}n = sensor index{RESET}{BOLD}{YELLOW}):{RESET}"
        )
        print(f"    {GREEN}:STATn:FAULT?{RESET}       - Fault description (OK or error string)")
        print(f"    {GREEN}:STATn:FAULT:CODE?{RESET}  - Fault code (numeric)")
        print(f"    {GREEN}:STATn:FAULT:CLEar{RESET}  - Clear fault register")

        print(f"\n  {BOLD}{YELLOW}Calibration:{RESET}")
        print(f"    {GREEN}:SYST:CAL:SAVE{RESET}      - Save calibration to flash")
        print(f"    {GREEN}:SYST:CAL:LOAD{RESET}      - Reload calibration from flash")
        print(f"    {GREEN}:SYST:CAL:DEL{RESET}       - Delete calibration file")

        print(f"\n  {BOLD}{YELLOW}Diagnostics:{RESET}")
        print(f"    {GREEN}:DIAG:PING?{RESET}         - Connectivity check (returns PONG)")

        print(f"\n  {BOLD}{YELLOW}System:{RESET}")
        print(f"    {GREEN}:SYST:ERR?{RESET}              - Get last error")
        print(f"    {GREEN}:SYST:VERS?{RESET}             - Query firmware version")
        print(f"    {GREEN}:SYST:LOG? [n]{RESET}          - Get system log (last n lines)")
        print(f"    {GREEN}:SYST:CLIENTS?{RESET}          - Get connected TCP clients")
        print(f"    {GREEN}:SYST:RES{RESET}               - Reboot device")
        print(
            f"    {GREEN}:SYST:USB:MODE <m> <pw>{RESET} - Set USB mode (PROD/DEV/DEBUG, password)"
        )

    elif device_type == "lecroy":
        print(f"\n  {BOLD}{YELLOW}Communication Header:{RESET}")
        print(
            f"    {GREEN}CHDR OFF{RESET}               - Disable response headers (done automatically on connect)"
        )
        print(f"    {GREEN}CHDR SHORT{RESET}             - Enable short-form headers")
        print(f"    {GREEN}CHDR?{RESET}                  - Query current header mode")

        print(f"\n  {BOLD}{YELLOW}Channel Vertical ({DIM}n = 1..4{RESET}{BOLD}{YELLOW}):{RESET}")
        print(f"    {GREEN}Cn:VDIV?{RESET}               - Query vertical scale (V/div)")
        print(f"    {GREEN}Cn:VDIV <val>{RESET}          - Set vertical scale  (e.g. 50E-3V)")
        print(f"    {GREEN}Cn:OFST?{RESET}               - Query vertical offset")
        print(f"    {GREEN}Cn:OFST <val>{RESET}          - Set vertical offset  (e.g. 0V)")
        print(f"    {GREEN}Cn:COUP?{RESET}               - Query coupling (DC / AC / GND)")
        print(f"    {GREEN}Cn:COUP DC{RESET}             - Set DC coupling")
        print(f"    {GREEN}Cn:ATTN?{RESET}               - Query probe attenuation")
        print(f"    {GREEN}Cn:TRA ON|OFF{RESET}          - Show / hide channel trace")
        print(f"    {GREEN}Cn:INVS ON|OFF{RESET}         - Invert channel")
        print(f"    {GREEN}Cn:BWL OFF|200MHZ{RESET}      - Bandwidth limit")

        print(f"\n  {BOLD}{YELLOW}Timebase:{RESET}")
        print(f"    {GREEN}TDIV?{RESET}                  - Query time/div")
        print(f"    {GREEN}TDIV <val>{RESET}             - Set time/div  (e.g. 500E-6S)")
        print(f"    {GREEN}TRDL?{RESET}                  - Query trigger delay")
        print(f"    {GREEN}TRDL <val>{RESET}             - Set trigger delay")

        print(f"\n  {BOLD}{YELLOW}Trigger:{RESET}")
        print(f"    {GREEN}TRIG_MODE?{RESET}             - Query trigger mode")
        print(f"    {GREEN}TRIG_MODE AUTO|NORM|SINGLE|STOP{RESET}  - Set trigger mode")
        print(f"    {GREEN}TRIG_SELECT EDGE,SR,Cn{RESET} - Edge trigger on channel n")
        print(f"    {GREEN}TRIG_LEVEL?{RESET}            - Query trigger level")
        print(f"    {GREEN}TRIG_LEVEL <val>{RESET}       - Set trigger level")

        print(f"\n  {BOLD}{YELLOW}Acquisition:{RESET}")
        print(f"    {GREEN}ARM_ACQUISITION{RESET}        - Arm (single or normal)")
        print(f"    {GREEN}FRTR{RESET}                   - Force trigger")
        print(f"    {GREEN}STOP{RESET}                   - Stop acquisition")
        print(f"    {GREEN}WAIT{RESET}                   - Wait until idle")
        print(f"    {GREEN}*TRG{RESET}                   - IEEE-488.2 trigger")

        print(f"\n  {BOLD}{YELLOW}Waveform Transfer ({DIM}n = 1..4{RESET}{BOLD}{YELLOW}):{RESET}")
        print(f"    {GREEN}WAVEFORM_SETUP SP,0,NP,0,FP,0,SN,0{RESET} - Transfer all points")
        print(f"    {GREEN}Cn:WF? DAT2{RESET}            - Raw ADC data (binary)")
        print(f"    {GREEN}Cn:WF? DAT1{RESET}            - Processed data")
        print(f"    {GREEN}Cn:WF? DTIME{RESET}           - Delta-time array")
        print(f"    {GREEN}Cn:INSP? 'WAVEDESC'{RESET}    - Wave descriptor (metadata)")

        print(f"\n  {BOLD}{YELLOW}VBS MAUI — Channel ({DIM}n = 1..4{RESET}{BOLD}{YELLOW}):{RESET}")
        print(f"    {DIM}  Queries use 'return=' syntax: VBS? 'return=expr'{RESET}")
        print(f"    {GREEN}VBS? 'return=app.Acquisition.Cn.VerScal'{RESET}  - V/div (SI: V)")
        print(f"    {GREEN}VBS? 'return=app.Acquisition.Cn.VerOffset'{RESET} - Offset (V)")
        print(f"    {GREEN}VBS? 'return=app.Acquisition.Cn.Coupling'{RESET}  - Coupling")
        print(f"    {GREEN}VBS 'app.Acquisition.Cn.VerScal = \"5E-3\"'{RESET} - Set V/div")
        print(f"    {GREEN}VBS 'app.Acquisition.Cn.VerOffset = 0'{RESET}     - Set offset")

        print(f"\n  {BOLD}{YELLOW}VBS MAUI — Timebase:{RESET}")
        print(f"    {GREEN}VBS? 'return=app.Acquisition.Horizontal.HorScale'{RESET}   - s/div")
        print(f"    {GREEN}VBS? 'return=app.Acquisition.Horizontal.SampleRate'{RESET} - S/s")
        print(f"    {GREEN}VBS? 'return=app.Acquisition.Horizontal.NumPoints'{RESET}  - Points")
        print(f"    {GREEN}VBS 'app.Acquisition.Horizontal.HorScale = 1E-3'{RESET}    - Set s/div")

        print(f"\n  {BOLD}{YELLOW}VBS MAUI — Trigger:{RESET}")
        print(f"    {GREEN}VBS? 'return=app.Acquisition.Trigger.Source'{RESET}  - Source")
        print(f"    {GREEN}VBS? 'return=app.Acquisition.Trigger.Level'{RESET}   - Level (V)")
        print(f"    {GREEN}VBS? 'return=app.Acquisition.Trigger.Mode'{RESET}    - Mode")
        print(f"    {GREEN}VBS 'app.Acquisition.Trigger.Mode = \"Auto\"'{RESET}   - Set mode")
        print(f"    {GREEN}VBS 'app.Acquisition.Trigger.Mode = \"Single\"'{RESET} - Set mode")
        print(f"    {GREEN}VBS 'app.Acquisition.Trigger.Level = 0'{RESET}        - Set level")

        print(f"\n  {BOLD}{YELLOW}Measurements ({DIM}n = 1..4{RESET}{BOLD}{YELLOW}):{RESET}")
        print(f"    {GREEN}PAVA? ALL{RESET}              - All parameter values (all channels)")
        print(f"    {GREEN}Cn:PAVA? AMPL{RESET}          - Amplitude")
        print(f"    {GREEN}Cn:PAVA? MEAN{RESET}          - Mean value")
        print(f"    {GREEN}Cn:PAVA? RMS{RESET}           - RMS value")
        print(f"    {GREEN}Cn:PAVA? PKPK{RESET}          - Peak-to-peak")
        print(f"    {GREEN}Cn:PAVA? FREQ{RESET}          - Frequency")
        print(f"    {GREEN}Cn:PAVA? PER{RESET}           - Period")
        print(f"    {GREEN}Cn:PAVA? RISE{RESET}          - Rise time")
        print(f"    {GREEN}Cn:PAVA? FALL{RESET}          - Fall time")
        print(f"    {GREEN}Cn:PAVA? WID{RESET}           - Pulse width")
        print(f"    {GREEN}Cn:PAVA? SDEV{RESET}          - Standard deviation")
        print(f"    {DIM}  Status suffix: OK=valid, NP=not precise, IV=invalid{RESET}")
        print(f"    {GREEN}PAST? CUST,Px{RESET}          - Full statistics for configured param Px")
        print(f"    {DIM}  Returns: AVG, HIGH, LAST, LOW, SIGMA, SWEEPS{RESET}")

        print(f"\n  {BOLD}{YELLOW}VBS MAUI — System:{RESET}")
        print(f"    {GREEN}VBS? 'return=app.SerialNumber'{RESET}      - Serial number")
        print(f"    {GREEN}VBS? 'return=app.FirmwareVersion'{RESET}   - Firmware version string")
        print(
            f"    {GREEN}VBS 'app.WaitUntilIdle(5)'{RESET}          - Wait up to 5 s for acquisition"
        )
        print(f"    {GREEN}VBS 'app.Math.F1.Definition = \"C1+C2\"'{RESET} - Define math trace")

        print(f"\n  {BOLD}{YELLOW}System:{RESET}")
        print(f"    {GREEN}DATE?{RESET}                  - Query date/time")
        print(f"    {GREEN}PANEL_SETUP?{RESET}           - Query full panel setup string")
        print(f"    {GREEN}MEMORY_LIST?{RESET}           - List stored waveforms/setups")

    elif device_type == "keithley6221":
        print(f"\n  {BOLD}{YELLOW}DC Current Output:{RESET}")
        print(f"    {GREEN}OUTP ON|OFF{RESET}                 - Enable / disable output")
        print(f"    {GREEN}OUTP?{RESET}                       - Query output state")
        print(f"    {GREEN}SOUR:CURR <val>{RESET}             - Set DC current  (e.g. 1E-3 A)")
        print(f"    {GREEN}SOUR:CURR?{RESET}                  - Query DC current setpoint")
        print(f"    {GREEN}SOUR:CURR:RANG <val>{RESET}        - Set current range")
        print(f"    {GREEN}SOUR:CURR:RANG:AUTO ON|OFF{RESET}  - Auto-range")
        print(f"    {GREEN}SOUR:CURR:COMP <val>{RESET}        - Compliance voltage (V)")
        print(f"    {GREEN}SOUR:CURR:COMP?{RESET}             - Query compliance")
        print(f"    {GREEN}SOUR:DEL <val>{RESET}              - Output settling delay (s)")

        print(f"\n  {BOLD}{YELLOW}AC Waveform Output:{RESET}")
        print(f"    {GREEN}SOUR:WAVE:FUNC SIN|SQU|RAMP|ARB0..ARB4{RESET}  - Waveform type")
        print(f"    {GREEN}SOUR:WAVE:AMPL <val>{RESET}       - Amplitude (A, pk-pk)")
        print(f"    {GREEN}SOUR:WAVE:FREQ <val>{RESET}       - Frequency (Hz, 1e-3 to 1e5)")
        print(f"    {GREEN}SOUR:WAVE:OFFS <val>{RESET}       - DC offset (A)")
        print(
            f"    {GREEN}SOUR:WAVE:DCYC <val>{RESET}       - Duty cycle, %  (square/ramp only, 0–100)"
        )
        print(f"    {GREEN}SOUR:WAVE:RANG BEST|FIX{RESET}    - Source ranging mode")
        print(f"    {GREEN}SOUR:WAVE:DUR:TIME <val>{RESET}   - Output duration (s, or INF)")
        print(f"    {GREEN}SOUR:WAVE:DUR:CYCL <val>{RESET}   - Output duration in cycles")
        print(f"    {GREEN}SOUR:WAVE:PMAR:STAT ON|OFF{RESET} - Phase marker output enable")
        print(f"    {GREEN}SOUR:WAVE:PMAR <val>{RESET}       - Phase marker phase (-180 to 180 °)")
        print(
            f"    {GREEN}SOUR:WAVE:PMAR:OLIN <n>{RESET}    - Phase marker trigger output line (1–6)"
        )
        print(f"    {GREEN}SOUR:WAVE:ARM{RESET}              - Arm waveform (prepares output)")
        print(f"    {GREEN}SOUR:WAVE:INIT{RESET}             - Start waveform output")
        print(f"    {GREEN}SOUR:WAVE:ABOR{RESET}             - Abort waveform output")

        print(
            f"\n  {BOLD}{YELLOW}Delta Mode  {DIM}(requires Keithley 2182A){RESET}{BOLD}{YELLOW}:{RESET}"
        )
        print(f"    {GREEN}SOUR:DELT:HIGH <val>{RESET}       - High-level current (A)")
        print(f"    {GREEN}SOUR:DELT:LOW <val>{RESET}        - Low-level current (A)")
        print(f"    {GREEN}SOUR:DELT:DELAY <val>{RESET}      - Delay between pulses (s)")
        print(f"    {GREEN}SOUR:DELT:COUNT <n>{RESET}        - Number of delta measurements")
        print(f"    {GREEN}SOUR:DELT:CAB ON|OFF{RESET}       - Cold-switching abort")
        print(f"    {GREEN}SOUR:DELT:ARM{RESET}              - Arm delta mode")
        print(f"    {GREEN}SOUR:DELT:INIT{RESET}             - Start delta measurements")
        print(f"    {GREEN}SOUR:DELT:ABOR{RESET}             - Abort delta mode")

        print(
            f"\n  {BOLD}{YELLOW}Differential Conductance  {DIM}(requires 2182A){RESET}{BOLD}{YELLOW}:{RESET}"
        )
        print(f"    {GREEN}SOUR:DCON:STAR <val>{RESET}       - Start current (A)")
        print(f"    {GREEN}SOUR:DCON:STOP <val>{RESET}       - Stop current (A)")
        print(f"    {GREEN}SOUR:DCON:STEP <val>{RESET}       - Step size (A)")
        print(f"    {GREEN}SOUR:DCON:DELTA <val>{RESET}      - Delta amplitude (A)")
        print(f"    {GREEN}SOUR:DCON:DELAY <val>{RESET}      - Delay (s)")
        print(f"    {GREEN}SOUR:DCON:ARM{RESET}              - Arm")
        print(f"    {GREEN}SOUR:DCON:INIT{RESET}             - Start")
        print(f"    {GREEN}SOUR:DCON:ABOR{RESET}             - Abort")

        print(f"\n  {BOLD}{YELLOW}Current Sweep:{RESET}")
        print(f"    {GREEN}SOUR:SWE:SPAC LIN|LOG{RESET}      - Linear or logarithmic spacing")
        print(f"    {GREEN}SOUR:SWE:POIN <n>{RESET}          - Number of sweep points")
        print(f"    {GREEN}SOUR:SWE:RANG BEST|AUTO{RESET}    - Range mode")
        print(f"    {GREEN}SOUR:SWE:COUN <n>{RESET}          - Sweep repeat count")
        print(f"    {GREEN}SOUR:SWE:CAB ON|OFF{RESET}        - Cold-switching abort")
        print(f"    {GREEN}SOUR:SWE:ARM{RESET}               - Arm sweep")
        print(f"    {GREEN}SOUR:SWE:INIT{RESET}              - Start sweep")
        print(f"    {GREEN}SOUR:SWE:ABOR{RESET}              - Abort sweep")
        print(f"    {GREEN}SOUR:LIST:CURR <list>{RESET}      - Arbitrary current list (comma-sep)")
        print(f"    {GREEN}SOUR:LIST:DELAY <list>{RESET}     - Per-point delays (comma-sep)")

        print(f"\n  {BOLD}{YELLOW}Averaging / Sense:{RESET}")
        print(f"    {GREEN}SENS:AVER:STAT ON|OFF{RESET}      - Enable averaging filter")
        print(f"    {GREEN}SENS:AVER:COUN <n>{RESET}         - Filter count")
        print(f"    {GREEN}SENS:AVER:TCON MOV|REP{RESET}     - Moving or repeat filter")

        print(f"\n  {BOLD}{YELLOW}Display:{RESET}")
        print(f"    {GREEN}DISP:ENAB ON|OFF{RESET}           - Turn front-panel display on/off")
        print(f'    {GREEN}DISP:TEXT "<msg>"{RESET}          - Show message on display')
        print(f"    {GREEN}DISP:TEXT:STAT ON|OFF{RESET}      - Enable / clear display text")

        print(f"\n  {BOLD}{YELLOW}Status / System:{RESET}")
        print(f"    {GREEN}STAT:OPER?{RESET}                 - Operation status event register")
        print(f"    {GREEN}STAT:OPER:COND?{RESET}            - Operation condition register")
        print(f"    {GREEN}STAT:QUES?{RESET}                 - Questionable status")
        print(f"    {GREEN}SYST:ERR?{RESET}                  - Query error queue")
        print(f"    {GREEN}SYST:PRES{RESET}                  - Restore factory settings")
        print(f"    {GREEN}SYST:LSYN ON|OFF{RESET}           - Line (50/60 Hz) synchronization")

    # WiFi section — only shown for NEnG embedded instruments (not scopes)
    if device_type not in ("lecroy", "scope", "keithley6221"):
        print(f"\n  {BOLD}{YELLOW}WiFi (if available):{RESET}")
        print(f"    {GREEN}:WIFI:STAT?{RESET}              - Comprehensive WiFi status")
        print(f"    {GREEN}:WIFI:STATUS?{RESET}            - WiFi connection status (compact)")
        print(f"    {GREEN}:WIFI:CONNECTED?{RESET}         - Check if connected (1/0)")
        print(f"    {GREEN}:WIFI:IP?{RESET}                - Get IP address")
        print(f"    {GREEN}:WIFI:SCAN?{RESET}              - Scan for available networks")
        print(f"    {GREEN}:WIFI:NETWORK:ADD <ssid>,<pw>{RESET} - Add fallback network")
        print(f"    {DIM}      Example: :WIFI:NETWORK:ADD MyWiFi,password123{RESET}")
        print(f"    {GREEN}:WIFI:NETWORK:REMOVE <ssid>{RESET}  - Remove fallback network")
        print(f"    {GREEN}:WIFI:NETWORK:LIST?{RESET}          - List configured networks")
        print(f"    {GREEN}:WIFI:CONFIG:SAVE{RESET}            - Save WiFi configuration")

    print(f"\n  {BOLD}{YELLOW}Local Commands:{RESET}")
    print(f"    {MAGENTA}help{RESET}         - Show this help")
    print(f"    {MAGENTA}list{RESET}         - List tab-completion commands")
    print(f"    {MAGENTA}sync/nosync{RESET}  - Toggle OPC synchronization")
    print(f"    {MAGENTA}pretty/raw_response{RESET} - Toggle pretty formatting")
    print(f"    {MAGENTA}status{RESET}       - Show current settings")
    print(f"    {MAGENTA}raw{RESET}          - Monitor raw serial data (5 s)")
    print(f"    {MAGENTA}exit{RESET}         - Exit interactive mode")

    print(f"\n  {BOLD}{CYAN}OPC Synchronization:{RESET}")
    print(f"    {DIM}When enabled (default), non-query commands wait for *OPC?{RESET}")
    print(f"    {DIM}response to ensure completion before returning.{RESET}")
    print()


# ===================================================================
# Interactive REPL
# ===================================================================
# Comprehensive tab-completion list
SCPI_COMMANDS = [
    "*IDN?",
    "*RST",
    "*CLS",
    "*OPC?",
    "*OPC",
    "*ESR?",
    "*ESE",
    "*ESE?",
    "*STB?",
    "*SRE",
    "*SRE?",
    "*TST?",
    "*WAI",
    ":SENS:TEMP?",
    ":SENS:ALL?",
    ":SENS:RAW?",
    ":SENS:SER?",
    ":SENS:MEAS?",
    ":SENS:INIT",
    ":SENS:RESET",
    ":SENS:SYNC?",
    ":SENS:SYNC? 1",
    ":SENS:SYNC? 8",
    ":SENS:SYNC? 32",
    ":SENS:SYNC? 64",
    ":SENS:COUNT?",
    ":SENS1:TEMP?",
    ":SENS1:RAW?",
    ":SENS1:SER?",
    ":SENS1:MEAS?",
    ":SENS2:TEMP?",
    ":SENS2:RAW?",
    ":SENS2:SER?",
    ":SENS2:MEAS?",
    ":SENS3:TEMP?",
    ":SENS3:RAW?",
    ":SENS3:SER?",
    ":SENS3:MEAS?",
    ":SENS4:TEMP?",
    ":SENS4:RAW?",
    ":SENS4:SER?",
    ":SENS4:MEAS?",
    ":CONF:HIGH",
    ":CONF:HIGH?",
    ":CONF:LOW",
    ":CONF:LOW?",
    ":CONF:TEMP:OFFS",
    ":CONF:TEMP:OFFS?",
    ":CONF:ALRT:MODE",
    ":CONF:ALRT:MODE?",
    ":CONF:MEAS:AVG",
    ":CONF:MEAS:AVG?",
    ":CONF:MEAS:DEL",
    ":CONF:MEAS:DEL?",
    ":CONF:MEAS:MODE",
    ":CONF:MEAS:MODE?",
    ":PID:SENS",
    ":PID:SENS?",
    ":PID:SENSOR",
    ":PID:SENSOR?",
    ":PID:SENS:COUN?",
    ":PID:SENS:COUNT?",
    ":PID:SENSOR:COUNT?",
    ":PID:SETP",
    ":PID:SETP?",
    ":PID:SETPOINT",
    ":PID:SETPOINT?",
    ":PID:ENAB",
    ":PID:ENAB?",
    ":PID:ENABLE",
    ":PID:ENABLE?",
    ":PID:ENAB ON",
    ":PID:ENAB OFF",
    ":PID:ENAB 1",
    ":PID:ENAB 0",
    ":PID:STAT?",
    ":PID:STATUS?",
    ":PID:TELE?",
    ":PID:TELEMETRY?",
    ":PID:TEMPS?",
    ":PID:TEMP:ALL?",
    ":PID:KP",
    ":PID:KP?",
    ":PID:KI",
    ":PID:KI?",
    ":PID:KD",
    ":PID:KD?",
    ":PID:TUNE",
    ":PID:TUNE?",
    ":PID:RES",
    ":PID:RESET",
    ":PID:RAMP",
    ":PID:RAMP?",
    ":PID:RAMP:STOP",
    ":PID:RAMP:ABORT",
    ":PID:RAMP:STAT?",
    ":PID:RAMP:STATUS?",
    ":TEC:OUTP",
    ":TEC:OUTP?",
    ":TEC:OUTPUT",
    ":TEC:OUTPUT?",
    ":TEC:STOP",
    ":SIM:STAT?",
    ":SIM:STATUS?",
    ":SIM:RES",
    ":SIM:RESET",
    ":SYST:MODE?",
    ":SYST:MODE SIM",
    ":SYST:MODE REAL",
    ":SYST:ERR?",
    ":SYST:ERROR?",
    ":SYST:VERS?",
    ":SYST:VERSION?",
    ":SYST:USB:MODE?",
    ":SYST:USB:MODE PROD",
    ":SYST:USB:MODE DEV",
    ":SYST:USB:MODE DEBUG",
    ":SYST:RES",
    ":SYST:CAL:SAVE",
    ":SYST:CAL:LOAD",
    ":SYST:CAL:DEL",
    ":SYST:LOG?",
    ":SYST:LOG? 50",
    ":SYST:LOG? 100",
    ":SYST:CLIENTS?",
    ":WIFI:ENAB",
    ":WIFI:ENAB?",
    ":WIFI:ENABLE",
    ":WIFI:ENABLE?",
    ":WIFI:CONNECTED?",
    ":WIFI:CONNECT",
    ":WIFI:DISCONNECT",
    ":WIFI:IP?",
    ":WIFI:MAC?",
    ":WIFI:SSID?",
    ":WIFI:RSSI?",
    ":WIFI:STATUS?",
    ":WIFI:STAT?",
    ":WIFI:SCAN?",
    ":WIFI:CHANNEL?",
    ":WIFI:SSID",
    ":WIFI:PASSWORD",
    ":WIFI:PASS",
    ":WIFI:HOSTNAME",
    ":WIFI:HOSTNAME?",
    ":WIFI:AUTOCONNECT",
    ":WIFI:AUTOCONNECT?",
    ":WIFI:AVAILABLE?",
    ":WIFI:CONFIG:SAVE",
    ":WIFI:CONFIG:LOAD",
    ":WIFI:NETWORK:ADD",
    ":WIFI:NETWORK:REMOVE",
    ":WIFI:NETWORK:LIST?",
    "help",
    "exit",
    "quit",
    "q",
    "raw",
    "list",
    "commands",
    "sync",
    "nosync",
    "pretty",
    "raw_response",
    "status",
]

# RelayBank16-specific command list
_RELA_N = [str(n) for n in range(1, 17)]
SCPI_COMMANDS_RELAYBANK16 = [
    # IEEE-488.2
    "*IDN?",
    "*RST",
    "*CLS",
    "*OPC?",
    "*OPC",
    "*ESR?",
    "*ESE",
    "*ESE?",
    "*STB?",
    "*SRE",
    "*SRE?",
    "*TST?",
    "*WAI",
    # Route (bulk relay control)
    ":ROUT:CLOS",
    ":ROUT:OPEN",
    ":ROUT:MASK",
    ":ROUT:MASK?",
    ":ROUT:MASK:ALL?",
    ":ROUT:TEST",
    # Individual relays RELA1-RELA16
    *[f":RELA{n}:STAT ON" for n in _RELA_N],
    *[f":RELA{n}:STAT OFF" for n in _RELA_N],
    *[f":RELA{n}:STAT?" for n in _RELA_N],
    *[f":RELA{n}:PULS" for n in _RELA_N],
    # Pulse duration
    ":RELA:PULS:DUR",
    ":RELA:PULS:DUR?",
    # Diagnostics
    ":DIAG:PING?",
    # System
    ":SYST:ERR?",
    ":SYST:CLIENTS?",
    ":SYST:RES",
    ":SYST:VERS?",
    # WiFi
    ":WIFI:ENAB",
    ":WIFI:ENAB?",
    ":WIFI:ENABLE",
    ":WIFI:ENABLE?",
    ":WIFI:CONNECTED?",
    ":WIFI:CONNECT",
    ":WIFI:DISCONNECT",
    ":WIFI:IP?",
    ":WIFI:MAC?",
    ":WIFI:SSID?",
    ":WIFI:RSSI?",
    ":WIFI:STATUS?",
    ":WIFI:STAT?",
    ":WIFI:SCAN?",
    ":WIFI:CHANNEL?",
    ":WIFI:SSID",
    ":WIFI:PASSWORD",
    ":WIFI:HOSTNAME",
    ":WIFI:HOSTNAME?",
    ":WIFI:AUTOCONNECT",
    ":WIFI:AUTOCONNECT?",
    ":WIFI:AVAILABLE?",
    ":WIFI:CONFIG:SAVE",
    ":WIFI:CONFIG:LOAD",
    ":WIFI:NETWORK:ADD",
    ":WIFI:NETWORK:REMOVE",
    ":WIFI:NETWORK:LIST?",
    # Local
    "help",
    "exit",
    "quit",
    "q",
    "raw",
    "list",
    "commands",
    "sync",
    "nosync",
    "pretty",
    "raw_response",
    "status",
]


# 3Dmag-specific command list
SCPI_COMMANDS_3DMAG = [
    # IEEE-488.2
    "*IDN?",
    "*RST",
    "*CLS",
    "*OPC?",
    "*OPC",
    "*ESR?",
    "*ESE",
    "*ESE?",
    "*STB?",
    "*SRE",
    "*SRE?",
    "*TST?",
    "*WAI",
    # Measurement
    ":READ?",
    ":READ:BINary?",
    # Data format
    ":DATA:POINts?",
    # Sensor info
    ":SENS:TYPE?",
    ":SENS:ADDRess?",
    # MLX90393 measurement profiles
    ":SENS:PROFile?",
    ":SENS:PROFile FAST",
    ":SENS:PROFile DEFAULT",
    ":SENS:PROFile HIACC",
    # Units
    ":UNIT:MAGnetic?",
    # Diagnostics
    ":DIAG:PING?",
    # System
    ":SYST:ERR?",
    ":SYST:VERS?",
    ":SYST:LOG?",
    ":SYST:LOG? 50",
    ":SYST:RES",
    ":SYST:USB:MODE?",
    ":SYST:USB:MODE PROD",
    ":SYST:USB:MODE DEV",
    ":SYST:USB:MODE DEBUG",
    ":SYST:CLIENTS?",
    # WiFi
    ":WIFI:ENAB",
    ":WIFI:ENAB?",
    ":WIFI:CONNECTED?",
    ":WIFI:CONNECT",
    ":WIFI:DISCONNECT",
    ":WIFI:IP?",
    ":WIFI:MAC?",
    ":WIFI:SSID?",
    ":WIFI:RSSI?",
    ":WIFI:STATUS?",
    ":WIFI:STAT?",
    ":WIFI:SCAN?",
    ":WIFI:CHANNEL?",
    ":WIFI:SSID",
    ":WIFI:PASSWORD",
    ":WIFI:HOSTNAME",
    ":WIFI:HOSTNAME?",
    ":WIFI:AUTOCONNECT",
    ":WIFI:AUTOCONNECT?",
    ":WIFI:AVAILABLE?",
    ":WIFI:CONFIG:SAVE",
    ":WIFI:CONFIG:LOAD",
    ":WIFI:NETWORK:ADD",
    ":WIFI:NETWORK:REMOVE",
    ":WIFI:NETWORK:LIST?",
    # Local
    "help",
    "exit",
    "quit",
    "q",
    "raw",
    "list",
    "commands",
    "sync",
    "nosync",
    "pretty",
    "raw_response",
    "status",
]

# RTDMax31865-specific command list
_SENS_N_RTD = ["1", "2"]
SCPI_COMMANDS_RTDMAX31865 = [
    # IEEE-488.2
    "*IDN?",
    "*RST",
    "*CLS",
    "*OPC?",
    "*OPC",
    "*ESR?",
    "*ESE",
    "*ESE?",
    "*STB?",
    "*SRE",
    "*SRE?",
    "*TST?",
    "*WAI",
    # Multi-sensor aggregates
    ":SENS:COUNT?",
    ":SENS:ALL?",
    ":SENS:ALLD?",
    ":SENS:DIFF?",
    # Per-sensor temperature & resistance
    *[f":SENS{n}:TEMP?" for n in _SENS_N_RTD],
    *[f":SENS{n}:RES?" for n in _SENS_N_RTD],
    *[f":SENS{n}:RAW?" for n in _SENS_N_RTD],
    # Per-sensor configuration
    *[f":CONF{n}:RTD:WIRE?" for n in _SENS_N_RTD],
    *[f":CONF{n}:RTD:RTDN?" for n in _SENS_N_RTD],
    *[f":CONF{n}:RTD:RTDN" for n in _SENS_N_RTD],
    *[f":CONF{n}:RTD:REFR?" for n in _SENS_N_RTD],
    *[f":CONF{n}:RTD:REFR" for n in _SENS_N_RTD],
    *[f":CONF{n}:BIAS?" for n in _SENS_N_RTD],
    *[f":CONF{n}:BIAS 0" for n in _SENS_N_RTD],
    *[f":CONF{n}:BIAS 1" for n in _SENS_N_RTD],
    *[f":CONF{n}:AUTO?" for n in _SENS_N_RTD],
    *[f":CONF{n}:AUTO 0" for n in _SENS_N_RTD],
    *[f":CONF{n}:AUTO 1" for n in _SENS_N_RTD],
    *[f":CONF{n}:FILT:FREQ?" for n in _SENS_N_RTD],
    # Per-sensor fault status
    *[f":STAT{n}:FAULT?" for n in _SENS_N_RTD],
    *[f":STAT{n}:FAULT:CODE?" for n in _SENS_N_RTD],
    *[f":STAT{n}:FAULT:CLEar" for n in _SENS_N_RTD],
    # Data format
    ":DATA:POINts?",
    # Units
    ":UNIT:TEMPerature?",
    # Diagnostics
    ":DIAG:PING?",
    # System (with calibration)
    ":SYST:ERR?",
    ":SYST:VERS?",
    ":SYST:LOG?",
    ":SYST:LOG? 50",
    ":SYST:RES",
    ":SYST:USB:MODE?",
    ":SYST:USB:MODE PROD",
    ":SYST:USB:MODE DEV",
    ":SYST:USB:MODE DEBUG",
    ":SYST:CAL:SAVE",
    ":SYST:CAL:LOAD",
    ":SYST:CAL:DEL",
    ":SYST:CLIENTS?",
    # WiFi
    ":WIFI:ENAB",
    ":WIFI:ENAB?",
    ":WIFI:CONNECTED?",
    ":WIFI:CONNECT",
    ":WIFI:DISCONNECT",
    ":WIFI:IP?",
    ":WIFI:MAC?",
    ":WIFI:SSID?",
    ":WIFI:RSSI?",
    ":WIFI:STATUS?",
    ":WIFI:STAT?",
    ":WIFI:SCAN?",
    ":WIFI:CHANNEL?",
    ":WIFI:SSID",
    ":WIFI:PASSWORD",
    ":WIFI:HOSTNAME",
    ":WIFI:HOSTNAME?",
    ":WIFI:AUTOCONNECT",
    ":WIFI:AUTOCONNECT?",
    ":WIFI:AVAILABLE?",
    ":WIFI:CONFIG:SAVE",
    ":WIFI:CONFIG:LOAD",
    ":WIFI:NETWORK:ADD",
    ":WIFI:NETWORK:REMOVE",
    ":WIFI:NETWORK:LIST?",
    # Local
    "help",
    "exit",
    "quit",
    "q",
    "raw",
    "list",
    "commands",
    "sync",
    "nosync",
    "pretty",
    "raw_response",
    "status",
]


# ---------------------------------------------------------------------------
# LeCroy / Teledyne MAUI oscilloscope command list
# Native remote commands + VBS MAUI interface
# ---------------------------------------------------------------------------
_CH = ["1", "2", "3", "4"]

SCPI_COMMANDS_LECROY = [
    # IEEE-488.2
    "*IDN?",
    "*RST",
    "*CLS",
    "*OPC?",
    "*OPC",
    "*ESR?",
    "*STB?",
    "*SRE",
    "*SRE?",
    "*WAI",
    "*TRG",
    # Communication header
    "CHDR OFF",
    "CHDR SHORT",
    "CHDR LONG",
    "CHDR?",
    # --- Channels: vertical ---
    *[f"C{n}:VDIV?" for n in _CH],
    *[f"C{n}:VDIV" for n in _CH],
    *[f"C{n}:OFST?" for n in _CH],
    *[f"C{n}:OFST" for n in _CH],
    *[f"C{n}:COUP?" for n in _CH],
    *[f"C{n}:COUP DC" for n in _CH],
    *[f"C{n}:COUP AC" for n in _CH],
    *[f"C{n}:COUP GND" for n in _CH],
    *[f"C{n}:ATTN?" for n in _CH],
    *[f"C{n}:ATTN" for n in _CH],
    *[f"C{n}:UNIT?" for n in _CH],
    *[f"C{n}:TRA ON" for n in _CH],
    *[f"C{n}:TRA OFF" for n in _CH],
    *[f"C{n}:INVS ON" for n in _CH],
    *[f"C{n}:INVS OFF" for n in _CH],
    *[f"C{n}:BWL OFF" for n in _CH],
    *[f"C{n}:BWL 200MHZ" for n in _CH],
    # --- Timebase ---
    "TDIV?",
    "TDIV",
    "TRDL?",
    "TRDL",
    # --- Trigger ---
    "TRIG_MODE?",
    "TRIG_MODE AUTO",
    "TRIG_MODE NORM",
    "TRIG_MODE SINGLE",
    "TRIG_MODE STOP",
    "TRIG_SELECT?",
    "TRIG_SELECT EDGE,SR,C1",
    "TRIG_SELECT EDGE,SR,C2",
    "TRIG_SELECT EDGE,SR,C3",
    "TRIG_SELECT EDGE,SR,C4",
    "TRIG_LEVEL?",
    "TRIG_LEVEL",
    # --- Acquisition control ---
    "ARM_ACQUISITION",
    "FRTR",
    "STOP",
    "WAIT",
    # --- Waveform transfer ---
    "WAVEFORM_SETUP SP,0,NP,0,FP,0,SN,0",
    *[f"C{n}:WF? DAT1" for n in _CH],
    *[f"C{n}:WF? DAT2" for n in _CH],
    *[f"C{n}:WF? DTIME" for n in _CH],
    *[f"C{n}:WF? TIME" for n in _CH],
    *[f"C{n}:INSP? 'WAVEDESC'" for n in _CH],
    # --- Measurements (native) ---
    "MEASURE_GENERAL?",
    "PARAMETER_CLR",
    # --- System ---
    "DATE?",
    "TIME?",
    "PANEL_SETUP?",
    "MEMORY_LIST?",
    "SCREEN_DUMP",
    # --- VBS MAUI — Channel queries/setters (use 'return=' for queries) ---
    *[f"VBS? 'return=app.Acquisition.C{n}.VerScal'" for n in _CH],
    *[f"VBS? 'return=app.Acquisition.C{n}.VerOffset'" for n in _CH],
    *[f"VBS? 'return=app.Acquisition.C{n}.Coupling'" for n in _CH],
    *[f"VBS 'app.Acquisition.C{n}.VerScal = \"5E-3\"'" for n in _CH],
    *[f"VBS 'app.Acquisition.C{n}.VerOffset = 0'" for n in _CH],
    # --- VBS MAUI — Timebase queries/setters ---
    "VBS? 'return=app.Acquisition.Horizontal.HorScale'",
    "VBS? 'return=app.Acquisition.Horizontal.SampleRate'",
    "VBS? 'return=app.Acquisition.Horizontal.NumPoints'",
    "VBS 'app.Acquisition.Horizontal.HorScale = 1E-3'",
    # --- VBS MAUI — Trigger queries/setters ---
    "VBS? 'return=app.Acquisition.Trigger.Source'",
    "VBS? 'return=app.Acquisition.Trigger.Level'",
    "VBS? 'return=app.Acquisition.Trigger.Mode'",
    "VBS 'app.Acquisition.Trigger.Mode = \"Auto\"'",
    "VBS 'app.Acquisition.Trigger.Mode = \"Normal\"'",
    "VBS 'app.Acquisition.Trigger.Mode = \"Single\"'",
    "VBS 'app.Acquisition.Trigger.Mode = \"Stop\"'",
    "VBS 'app.Acquisition.Trigger.Level = 0'",
    # --- VBS MAUI — System queries ---
    "VBS? 'return=app.SerialNumber'",
    "VBS? 'return=app.FirmwareVersion'",
    # --- Measurements (native — correct API for this firmware) ---
    "PAVA? ALL",
    *[f"C{n}:PAVA? AMPL" for n in _CH],
    *[f"C{n}:PAVA? MEAN" for n in _CH],
    *[f"C{n}:PAVA? RMS" for n in _CH],
    *[f"C{n}:PAVA? PKPK" for n in _CH],
    *[f"C{n}:PAVA? FREQ" for n in _CH],
    *[f"C{n}:PAVA? PER" for n in _CH],
    *[f"C{n}:PAVA? RISE" for n in _CH],
    *[f"C{n}:PAVA? FALL" for n in _CH],
    *[f"C{n}:PAVA? WID" for n in _CH],
    *[f"C{n}:PAVA? SDEV" for n in _CH],
    *[f"PAST? CUST,P{n}" for n in range(1, 9)],
    # --- VBS MAUI — Math ---
    "VBS 'app.Math.F1.Definition = \"C1+C2\"'",
    # --- VBS MAUI — System ---
    "VBS 'app.WaitUntilIdle(5)'",
    # --- Local ---
    "help",
    "exit",
    "quit",
    "q",
    "list",
    "commands",
    "sync",
    "nosync",
    "pretty",
    "raw_response",
    "status",
]


# Generic oscilloscope/bench-instrument command list (IEEE-488.2 + common SCPI)
SCPI_COMMANDS_SCOPE = [
    # IEEE-488.2
    "*IDN?",
    "*RST",
    "*CLS",
    "*OPC?",
    "*OPC",
    "*ESR?",
    "*ESE",
    "*ESE?",
    "*STB?",
    "*SRE",
    "*SRE?",
    "*TST?",
    "*WAI",
    # Status / error
    ":SYST:ERR?",
    # Acquisition
    ":ACQ:TYPE?",
    ":ACQ:TYPE AVERage",
    ":ACQ:TYPE NORMal",
    ":ACQ:TYPE PEAK",
    ":ACQ:COUN?",
    ":ACQ:COUN",
    ":ACQ:STAT?",
    ":SING",
    ":RUN",
    ":STOP",
    # Trigger
    ":TRIG:MODE?",
    ":TRIG:MODE AUTO",
    ":TRIG:MODE NORM",
    ":TRIG:MODE SING",
    ":TRIG:SOUR?",
    ":TRIG:LEV?",
    ":TRIG:SLOP?",
    # Timebase
    ":TIM:SCAL?",
    ":TIM:SCAL",
    ":TIM:OFFS?",
    ":TIM:OFFS",
    # Channels (C1–C4)
    *[f":CHAN{n}:DISP?" for n in range(1, 5)],
    *[f":CHAN{n}:DISP ON" for n in range(1, 5)],
    *[f":CHAN{n}:DISP OFF" for n in range(1, 5)],
    *[f":CHAN{n}:SCAL?" for n in range(1, 5)],
    *[f":CHAN{n}:SCAL" for n in range(1, 5)],
    *[f":CHAN{n}:OFFS?" for n in range(1, 5)],
    *[f":CHAN{n}:COUP?" for n in range(1, 5)],
    *[f":CHAN{n}:COUP DC" for n in range(1, 5)],
    *[f":CHAN{n}:COUP AC" for n in range(1, 5)],
    *[f":CHAN{n}:COUP GND" for n in range(1, 5)],
    # Measurements
    ":MEAS:VRMS?",
    ":MEAS:VMAX?",
    ":MEAS:VMIN?",
    ":MEAS:VAMP?",
    ":MEAS:VTOP?",
    ":MEAS:VBAS?",
    ":MEAS:FREQ?",
    ":MEAS:PERI?",
    ":MEAS:PWID?",
    ":MEAS:NWID?",
    ":MEAS:PHAS?",
    ":MEAS:RISE?",
    ":MEAS:FALL?",
    # Waveform data
    ":WAV:SOUR?",
    ":WAV:SOUR",
    ":WAV:FORM?",
    ":WAV:FORM BYTE",
    ":WAV:FORM WORD",
    ":WAV:FORM ASC",
    ":WAV:DATA?",
    ":WAV:PRE?",
    ":WAV:Xinc?",
    ":WAV:XINC?",
    ":WAV:XORG?",
    ":WAV:YINC?",
    ":WAV:YORG?",
    # Local
    "help",
    "exit",
    "quit",
    "q",
    "list",
    "commands",
    "sync",
    "nosync",
    "pretty",
    "raw_response",
    "status",
]


# Keithley 6221 AC/DC Current Source command list
SCPI_COMMANDS_KEITHLEY6221 = [
    # IEEE-488.2
    "*IDN?",
    "*RST",
    "*CLS",
    "*OPC?",
    "*OPC",
    "*ESR?",
    "*ESE",
    "*ESE?",
    "*STB?",
    "*SRE",
    "*SRE?",
    "*TRG",
    "*WAI",
    # Output
    "OUTP ON",
    "OUTP OFF",
    "OUTP?",
    "OUTP:LTE ON",
    "OUTP:LTE OFF",
    "OUTP:LTE?",
    "OUTP:ISH OLOW",
    "OUTP:ISH GUARD",
    "OUTP:ISH?",
    # DC Current source
    "SOUR:CURR",
    "SOUR:CURR?",
    "SOUR:CURR:RANG",
    "SOUR:CURR:RANG?",
    "SOUR:CURR:RANG:AUTO ON",
    "SOUR:CURR:RANG:AUTO OFF",
    "SOUR:CURR:COMP",
    "SOUR:CURR:COMP?",
    "SOUR:DEL",
    "SOUR:DEL?",
    # Waveform (AC) output
    "SOUR:WAVE:FUNC SIN",
    "SOUR:WAVE:FUNC SQU",
    "SOUR:WAVE:FUNC RAMP",
    "SOUR:WAVE:FUNC ARB0",
    "SOUR:WAVE:FUNC ARB1",
    "SOUR:WAVE:FUNC ARB2",
    "SOUR:WAVE:FUNC ARB3",
    "SOUR:WAVE:FUNC ARB4",
    "SOUR:WAVE:FUNC?",
    "SOUR:WAVE:AMPL",
    "SOUR:WAVE:AMPL?",
    "SOUR:WAVE:FREQ",
    "SOUR:WAVE:FREQ?",
    "SOUR:WAVE:OFFS",
    "SOUR:WAVE:OFFS?",
    "SOUR:WAVE:DCYC",
    "SOUR:WAVE:DCYC?",
    "SOUR:WAVE:RANG BEST",
    "SOUR:WAVE:RANG FIX",
    "SOUR:WAVE:RANG?",
    "SOUR:WAVE:DUR:TIME",
    "SOUR:WAVE:DUR:TIME?",
    "SOUR:WAVE:DUR:CYCL",
    "SOUR:WAVE:DUR:CYCL?",
    "SOUR:WAVE:PMAR:STAT ON",
    "SOUR:WAVE:PMAR:STAT OFF",
    "SOUR:WAVE:PMAR:STAT?",
    "SOUR:WAVE:PMAR",
    "SOUR:WAVE:PMAR?",
    "SOUR:WAVE:PMAR:OLIN",
    "SOUR:WAVE:PMAR:OLIN?",
    "SOUR:WAVE:ARM",
    "SOUR:WAVE:INIT",
    "SOUR:WAVE:ABOR",
    # Delta mode (requires Keithley 2182A nanovoltmeter)
    "SOUR:DELT:HIGH",
    "SOUR:DELT:LOW",
    "SOUR:DELT:DELAY",
    "SOUR:DELT:COUNT",
    "SOUR:DELT:CAB ON",
    "SOUR:DELT:CAB OFF",
    "SOUR:DELT:ARM",
    "SOUR:DELT:INIT",
    "SOUR:DELT:ABOR",
    # Differential Conductance (requires 2182A)
    "SOUR:DCON:STAR",
    "SOUR:DCON:STOP",
    "SOUR:DCON:STEP",
    "SOUR:DCON:DELTA",
    "SOUR:DCON:DELAY",
    "SOUR:DCON:CAB ON",
    "SOUR:DCON:CAB OFF",
    "SOUR:DCON:ARM",
    "SOUR:DCON:INIT",
    "SOUR:DCON:ABOR",
    # Current Sweep
    "SOUR:SWE:SPAC LIN",
    "SOUR:SWE:SPAC LOG",
    "SOUR:SWE:POIN",
    "SOUR:SWE:RANG BEST",
    "SOUR:SWE:RANG AUTO",
    "SOUR:SWE:COUN",
    "SOUR:SWE:CAB ON",
    "SOUR:SWE:CAB OFF",
    "SOUR:SWE:ARM",
    "SOUR:SWE:INIT",
    "SOUR:SWE:ABOR",
    # List mode
    "SOUR:LIST:CURR",
    "SOUR:LIST:CURR?",
    "SOUR:LIST:DELAY",
    "SOUR:LIST:DELAY?",
    # Sense (averaging)
    "SENS:AVER:STAT ON",
    "SENS:AVER:STAT OFF",
    "SENS:AVER:STAT?",
    "SENS:AVER:COUN",
    "SENS:AVER:COUN?",
    "SENS:AVER:TCON MOV",
    "SENS:AVER:TCON REP",
    "SENS:AVER:TCON?",
    # Display
    "DISP:ENAB ON",
    "DISP:ENAB OFF",
    "DISP:ENAB?",
    "DISP:TEXT",
    "DISP:TEXT:STAT ON",
    "DISP:TEXT:STAT OFF",
    # Status
    "STAT:OPER?",
    "STAT:OPER:COND?",
    "STAT:QUES?",
    "STAT:MEAS:EVEN?",
    # System
    "SYST:ERR?",
    "SYST:PRES",
    "SYST:BEEP",
    "SYST:LSYN ON",
    "SYST:LSYN OFF",
    # Local
    "help",
    "exit",
    "quit",
    "q",
    "list",
    "commands",
    "sync",
    "nosync",
    "pretty",
    "raw_response",
    "status",
]


def _detect_device_type(idn_str: str) -> str:
    """Return a short device-type token from an *IDN? response string."""
    idn_up = idn_str.upper()
    if "RELAYBANK" in idn_up:
        return "relaybank16"
    if "TMP117" in idn_up:
        return "tmp117"
    if "3DMAG" in idn_up:
        return "3dmag"
    if "RTDMAX" in idn_up:
        return "rtdmax31865"
    # Keithley 6221 AC/DC Current Source
    if "6221" in idn_up and "KEITHLEY" in idn_up:
        return "keithley6221"
    # LeCroy / Teledyne scopes — use MAUI native command set
    if any(kw in idn_up for kw in ("LECROY", "WAVEPRO", "WAVERUNNER", "WAVEMASTER", "TELEDYNE")):
        return "lecroy"
    # Other oscilloscopes / bench instruments — generic IEEE 488.2 SCPI
    if any(kw in idn_up for kw in ("TEKTRONIX", "KEYSIGHT", "AGILENT", "ROHDE", "RIGOL")):
        return "scope"
    return "generic"


def _commands_for_device(device_type: str) -> list:
    """Return the appropriate tab-completion command list for a device type."""
    if device_type == "relaybank16":
        return SCPI_COMMANDS_RELAYBANK16
    if device_type == "3dmag":
        return SCPI_COMMANDS_3DMAG
    if device_type == "rtdmax31865":
        return SCPI_COMMANDS_RTDMAX31865
    if device_type == "lecroy":
        return SCPI_COMMANDS_LECROY
    if device_type == "scope":
        return SCPI_COMMANDS_SCOPE
    if device_type == "keithley6221":
        return SCPI_COMMANDS_KEITHLEY6221
    return SCPI_COMMANDS  # TMP117 / generic


def interactive_mode(
    ser,
    *,
    sync_mode: bool = True,
    connection_info: dict | None = None,
    pretty_print: bool = True,
    commands: list | None = None,
    device_type: str | None = None,
) -> None:
    """Interactive SCPI prompt with history, tab-completion, and pretty output."""
    cur_sync = sync_mode
    cur_pretty = pretty_print
    _cmds = commands if commands is not None else SCPI_COMMANDS

    if connection_info is None:
        if hasattr(ser, "port") and hasattr(ser, "baudrate"):
            connection_info = {"type": "serial", "port": ser.port, "baudrate": ser.baudrate}
        elif hasattr(ser, "host") and hasattr(ser, "tcp_port"):
            connection_info = {"type": "tcp", "host": ser.host, "tcp_port": ser.tcp_port}

    # readline setup
    if READLINE_AVAILABLE:

        def completer(text, state):
            opts = [c for c in _cmds if c.upper().startswith(text.upper())] if text else list(_cmds)
            return opts[state] if state < len(opts) else None

        readline.set_completer(completer)
        # On Windows, parse_and_bind doesn't work reliably for tab completion
        # because native Windows readline implementations don't handle it properly.
        # Tab completion will not work on Windows, but history will still be available.
        if platform.system() != "Windows":
            if USING_LIBEDIT:
                readline.parse_and_bind("bind ^I rl_complete")
            else:
                readline.parse_and_bind("tab: complete")
        readline.set_completer_delims("\t\n")  # No space: match full "CMD PARAM" strings
        try:
            readline.read_history_file(HISTORY_FILE)
            readline.set_history_length(500)
        except (FileNotFoundError, Exception):
            pass

    print("\n" + "=" * 60)
    print(f"{BOLD}{CYAN}Interactive SCPI Mode (© 2026 Prof. Flavio ABREU ARAUJO){RESET}")
    print("=" * 60)
    if device_type:
        _device_label = {
            "relaybank16": "RelayBank16",
            "tmp117": "TMP117",
            "3dmag": "3Dmag",
            "rtdmax31865": "RTDMax31865",
            "lecroy": "LeCroy / Teledyne MAUI Oscilloscope",
            "scope": "Oscilloscope / Bench Instrument",
            "keithley6221": "Keithley 6221 AC/DC Current Source",
        }.get(device_type, "")
        if _device_label:
            print(f"{BOLD}Device:{RESET}     {GREEN}{_device_label}{RESET}")
    if connection_info:
        if connection_info.get("type") == "vxi11":
            print(f"{BOLD}Connection:{RESET} {GREEN}VXI-11{RESET} {connection_info.get('host')}")
        elif connection_info.get("type") == "tcp":
            print(
                f"{BOLD}Connection:{RESET} {GREEN}TCP{RESET} {connection_info.get('host')}:{connection_info.get('tcp_port')}"
            )
        else:
            print(
                f"{BOLD}Connection:{RESET} {GREEN}Serial{RESET} {connection_info.get('port')} @ {connection_info.get('baudrate')} baud"
            )
    print(f"{DIM}Type SCPI commands and press Enter.{RESET}")
    print(
        f"{DIM}Commands: 'help', 'list', 'sync', 'nosync', 'pretty', 'raw_response', 'status', 'exit'{RESET}"
    )
    if READLINE_AVAILABLE or PROMPT_TOOLKIT_AVAILABLE:
        print(f"{DIM}Use ↑/↓ arrows for history, Tab to filter commands.{RESET}")
    sync_str = f"{GREEN}ON{RESET}" if cur_sync else f"{YELLOW}OFF{RESET}"
    pretty_str = f"{GREEN}ON{RESET}" if cur_pretty else f"{YELLOW}OFF{RESET}"
    print(f"{BOLD}OPC Sync Mode:{RESET} {sync_str}  {BOLD}Pretty Print:{RESET} {pretty_str}")
    print("=" * 60 + "\n")

    try:
        while True:
            try:
                # drain buffer before prompt (may fail if device disconnected)
                try:
                    if hasattr(ser, "in_waiting") and hasattr(ser, "reset_input_buffer"):
                        time.sleep(0.05)
                        if ser.in_waiting > 0:
                            ser.read(ser.in_waiting)
                        ser.reset_input_buffer()
                except (OSError, termios.error):
                    # Device disconnected (e.g., after reset)
                    print(f"\n{YELLOW}Device disconnected.{RESET}")
                    print(f"{YELLOW}Run 'scpi-client' again to reconnect.{RESET}\n")
                    break

                command = get_scpi_input("SCPI> ", _cmds).strip()
                if command:
                    print(f"\033[1A\033[2K{BOLD}{MAGENTA}SCPI>{RESET} {CYAN}{command}{RESET}")
                if not command:
                    continue

                cl = command.lower()
                if cl in ("exit", "quit", "q"):
                    print(f"{YELLOW}Exiting...{RESET}")
                    break
                if cl == "help":
                    print_help(device_type)
                    continue
                if cl == "raw":
                    print(f"\n{CYAN}Listening for raw serial data (5 seconds)...{RESET}")
                    print("-" * 60)
                    start = time.time()
                    while time.time() - start < 5:
                        if ser.in_waiting > 0:
                            try:
                                data = ser.read(ser.in_waiting)
                                print(data.decode("utf-8", errors="replace"), end="", flush=True)
                            except Exception:
                                pass
                        time.sleep(0.01)
                    print("\n" + "-" * 60)
                    ser.reset_input_buffer()
                    continue
                if cl in ("list", "commands"):
                    print(f"\n{BOLD}{CYAN}Available SCPI commands for tab completion:{RESET}")
                    print("-" * 60)
                    groups: dict[str, list[str]] = {}
                    for c in sorted(_cmds):
                        if c.startswith("*"):
                            key = "IEEE-488.2"
                        elif c.startswith(":"):
                            key = c.split(":")[1] if ":" in c[1:] else "Other"
                        else:
                            key = "Local"
                        groups.setdefault(key, []).append(c)
                    for grp in sorted(groups):
                        print(f"\n  {BOLD}{YELLOW}{grp}:{RESET}")
                        cmds = groups[grp]
                        cw = max(len(x) for x in cmds) + 2
                        cols = 80 // cw or 1
                        for i in range(0, len(cmds), cols):
                            row = cmds[i : i + cols]
                            print(
                                f"    {GREEN}"
                                + f"{RESET}  {GREEN}".join(x.ljust(cw - 2) for x in row)
                                + RESET
                            )
                    print()
                    continue
                if cl == "sync":
                    cur_sync = True
                    print(f"{GREEN}OPC synchronization: ON{RESET}")
                    continue
                if cl == "nosync":
                    cur_sync = False
                    print(f"{YELLOW}OPC synchronization: OFF{RESET}")
                    continue
                if cl == "pretty":
                    cur_pretty = True
                    print(f"{GREEN}Pretty print mode: ON{RESET}")
                    continue
                if cl == "raw_response":
                    cur_pretty = False
                    print(f"{YELLOW}Pretty print mode: OFF{RESET}")
                    continue
                if cl == "status":
                    ss = f"{GREEN}ON{RESET}" if cur_sync else f"{YELLOW}OFF{RESET}"
                    ps = f"{GREEN}ON{RESET}" if cur_pretty else f"{YELLOW}OFF{RESET}"
                    print(f"\n{BOLD}Current Settings:{RESET}")
                    print(f"  {BOLD}OPC Sync Mode:{RESET} {ss}")
                    print(f"  {BOLD}Pretty Print:{RESET}  {ps}")
                    if connection_info:
                        if connection_info.get("type") == "vxi11":
                            print(f"  {BOLD}Connection:{RESET}    {GREEN}VXI-11{RESET}")
                            print(f"  {BOLD}Host:{RESET}          {connection_info.get('host')}")
                        elif connection_info.get("type") == "tcp":
                            print(f"  {BOLD}Connection:{RESET}    {GREEN}TCP{RESET}")
                            print(f"  {BOLD}Host:{RESET}          {connection_info.get('host')}")
                            print(
                                f"  {BOLD}Port:{RESET}          {connection_info.get('tcp_port')}"
                            )
                        else:
                            print(f"  {BOLD}Connection:{RESET}    {GREEN}Serial{RESET}")
                            print(f"  {BOLD}Serial Port:{RESET}   {connection_info.get('port')}")
                            print(
                                f"  {BOLD}Baudrate:{RESET}      {connection_info.get('baudrate')}"
                            )
                    continue

                # send the command
                # print(f"{BLUE}→{RESET} {CYAN}{command}{RESET}")
                # Split on ';' to allow chaining multiple commands on one line.
                # Each sub-command is dispatched and displayed individually.
                sub_commands = [c.strip() for c in command.split(";") if c.strip()]
                is_reset_command = any(
                    c.upper() in (":SYST:RES", ":SYST:RES?", "*RST") for c in sub_commands
                )
                try:
                    if len(sub_commands) > 1:
                        for sub_cmd in sub_commands:
                            sub_resp = send_scpi_command(ser, sub_cmd, sync_mode=cur_sync)
                            fmt = format_response(sub_cmd, sub_resp or "", pretty=cur_pretty)
                            if sub_resp:
                                print(
                                    f"  {DIM}{sub_cmd}{RESET}  {GREEN}←{RESET} {YELLOW}{fmt}{RESET}"
                                )
                            else:
                                print(
                                    f"  {DIM}{sub_cmd}{RESET}"
                                    f"  {RED}←{RESET} {DIM}(no response){RESET}"
                                )
                        continue  # outer response display already done above
                    response = send_scpi_command(ser, command, sync_mode=cur_sync)
                    if response:
                        # Check for reset command special response
                        if response == "RESET_COMMAND_SENT":
                            formatted = format_response(command, response, pretty=cur_pretty)
                            print(f"{YELLOW}←{RESET} {formatted}")
                            print(f"\n{GREEN}{BOLD}Device reset initiated.{RESET}")
                            print(
                                f"{YELLOW}The device is restarting. Connection will be closed.{RESET}"
                            )
                            print(f"{YELLOW}Run 'scpi-client' again to reconnect.{RESET}\n")
                            break
                        # Check for critical connection errors
                        elif (
                            "Connection lost" in response
                            or "broken pipe" in response.lower()
                            or "device disconnected" in response.lower()
                        ):
                            print(f"{RED}←{RESET} {RED}{response}{RESET}")
                            if is_reset_command:
                                print(f"\n{GREEN}{BOLD}Device reset complete.{RESET}")
                                print(
                                    f"{YELLOW}The device is restarting. Connection closed.{RESET}"
                                )
                                print(f"{YELLOW}Run 'scpi-client' again to reconnect.{RESET}\n")
                                break
                            else:
                                print(f"\n{RED}{BOLD}Connection lost!{RESET}")
                                print(
                                    f"{YELLOW}The device connection was interrupted. Please reconnect or exit.{RESET}\n"
                                )
                        else:
                            # Always format responses (including errors) for better presentation
                            formatted = format_response(command, response, pretty=cur_pretty)
                            # Determine color based on content
                            if (
                                "✗" in formatted
                                or formatted.startswith("-")
                                or "error" in formatted.lower()
                                and "ERROR:" in response
                            ):
                                # Error message
                                if "\n" in formatted:
                                    print(f"{RED}←{RESET}{formatted}")
                                else:
                                    print(f"{RED}←{RESET} {formatted}")
                            elif "\n" in formatted:
                                print(f"{GREEN}←{RESET}{formatted}")
                            else:
                                print(f"{GREEN}←{RESET} {YELLOW}{formatted}{RESET}")
                    else:
                        print(
                            f"{RED}←{RESET} {DIM}(no response – try 'raw' to see device output){RESET}"
                        )
                except (
                    BrokenPipeError,
                    ConnectionResetError,
                    ConnectionError,
                    OSError,
                    termios.error,
                ) as e:
                    if is_reset_command:
                        print(f"{GREEN}{BOLD}✓{RESET} {GREEN}Device reset complete.{RESET}")
                        print(f"{YELLOW}The device is restarting. Connection closed.{RESET}")
                        print(f"{YELLOW}Run 'scpi-client' again to reconnect.{RESET}\n")
                        break
                    else:
                        print(f"{RED}←{RESET} {RED}{BOLD}ERROR: Connection lost!{RESET}")
                        print(f"{RED}  {e}{RESET}")
                        print(
                            f"{YELLOW}The device connection was interrupted. Please reconnect or exit.{RESET}\n"
                        )
                print()

            except KeyboardInterrupt:
                print(f"\n\n{YELLOW}Interrupted by user. Exiting...{RESET}")
                break
            except EOFError:
                print(f"\n{YELLOW}EOF received. Exiting...{RESET}")
                break
    finally:
        if READLINE_AVAILABLE:
            with contextlib.suppress(Exception):
                readline.write_history_file(HISTORY_FILE)


# ===================================================================
# main() entry-point
# ===================================================================
def main() -> int:
    """CLI entry-point for the ``scpi-client`` command."""
    from neng_scpi_tools import __version__

    # Enable ANSI colors on Windows terminals
    if COLORAMA_AVAILABLE and platform.system() == "Windows":
        colorama.just_fix_windows_console()

    parser = argparse.ArgumentParser(
        description="Send SCPI commands to NEnG instruments via USB or WiFi",
        epilog=(
            "Examples:\n"
            '  %(prog)s "*IDN?"\n'
            '  %(prog)s ":SENS:TEMP?"\n'
            '  %(prog)s --no-sync ":PID:SETP 25"\n'
            '  %(prog)s -w 192.168.1.100 "*IDN?"   (WiFi)\n'
            "  %(prog)s  (interactive mode)"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
    parser.add_argument("command", nargs="?", help="SCPI command (omit for interactive mode)")

    conn = parser.add_mutually_exclusive_group()
    conn.add_argument("-p", "--port", help="Serial port path (auto-detects if not specified)")
    conn.add_argument(
        "-w",
        "--wifi",
        nargs="?",
        const="AUTO",
        metavar="HOST[:PORT]",
        help=f"WiFi/TCP connection (auto-detects if HOST not specified, default port: {DEFAULT_SCPI_PORT})",
    )
    conn.add_argument(
        "-x",
        "--vxi11",
        metavar="HOST",
        help="VXI-11 LAN connection (e.g. oscilloscope). Requires python-vxi11.",
    )

    parser.add_argument(
        "-b", "--baud", type=int, default=115200, help="Baud rate (default: 115200)"
    )
    parser.add_argument(
        "-t", "--timeout", type=float, default=2.0, help="Timeout in seconds (default: 2.0)"
    )
    parser.add_argument("--no-sync", action="store_true", help="Disable OPC synchronization")
    parser.add_argument("--no-pretty", action="store_true", help="Disable pretty printing")
    parser.add_argument(
        "--large-networks",
        action="store_true",
        help="When using -w auto-detect, also scan networks larger than /24 (>254 hosts). "
        "Skipped by default to keep scans fast.",
    )
    parser.add_argument(
        "-q",
        "--quiet",
        action="store_true",
        help="Suppress connection/status messages; print response only (single-command mode)",
    )
    parser.add_argument(
        "--raw",
        action="store_true",
        help="Print bare response value only, no labels or formatting (implies --quiet and --no-pretty)",
    )

    args = parser.parse_args()
    use_wifi = args.wifi is not None
    use_vxi11 = args.vxi11 is not None

    # --raw implies --quiet and --no-pretty
    if args.raw:
        args.quiet = True
        args.no_pretty = True

    # In single-command quiet mode, swallow all connection/status output by
    # redirecting stdout to a null writer.  The response is printed after
    # stdout is restored, so it reaches the real terminal / pipe.
    _sq = bool(args.command and args.quiet)
    _real_stdout = sys.stdout

    class _NullWriter:
        def write(self, *_): pass
        def flush(self): pass

    _null_out = _NullWriter() if _sq else None
    if _sq:
        sys.stdout = _null_out

    # ---- open connection ------------------------------------------------
    if use_vxi11:
        host = args.vxi11
        print(f"{CYAN}Connecting to {BOLD}{host}{RESET}{CYAN} via VXI-11...{RESET}")
        try:
            ser = VXI11Socket(host, timeout=args.timeout)
            ser.connect()
            print(f"{GREEN}✓ Connected to {BOLD}{host}{RESET} {DIM}(VXI-11){RESET}")
        except ImportError as e:
            print(f"\n{RED}✗ {e}{RESET}")
            return 1
        except Exception as e:
            print(f"\n{RED}✗ VXI-11 connection failed: {e}{RESET}")
            print(
                f"{YELLOW}  Check that the instrument is powered on and reachable at {host}{RESET}"
            )
            return 1
    elif use_wifi:
        # Auto-detect device if --wifi specified without HOST
        if args.wifi == "AUTO":
            # device_scanner is maintained exclusively in neng_wifi_tools.
            _scanner = None
            with contextlib.suppress(ImportError):
                _scanner = importlib.import_module("neng_wifi_tools.device_scanner")

            if _scanner is None or not hasattr(_scanner, "get_all_local_networks"):
                print(
                    f"{RED}✗ WiFi auto-detect unavailable (device_scanner module not found).{RESET}"
                )
                print(f"{YELLOW}  Specify host with --wifi HOST{RESET}")
                return 1

            from ipaddress import IPv4Network as _IPv4Network

            get_all_local_networks = _scanner.get_all_local_networks
            scan_network = _scanner.scan_network
            # Scan all standard SCPI ports so non-NEnG instruments
            # (oscilloscopes, bench instruments, etc.) are discovered too.
            _all_scpi_ports = getattr(_scanner, "ALL_SCPI_PORTS", [DEFAULT_SCPI_PORT])

            print(f"{CYAN}Auto-detecting SCPI devices on network...{RESET}")
            networks = get_all_local_networks()
            if not networks:
                print(f"{RED}✗ Could not auto-detect local networks{RESET}")
                print(f"{YELLOW}  Specify host with --wifi HOST{RESET}")
                return 1

            # Drop networks larger than /24 unless the user opts in
            if not args.large_networks:
                small = [n for n in networks if _IPv4Network(n, strict=False).prefixlen >= 24]
                skipped = len(networks) - len(small)
                if skipped:
                    print(
                        f"{DIM}  Skipping {skipped} large network(s) with prefix < /24 "
                        f"(use --large-networks to include){RESET}"
                    )
                networks = small
                if not networks:
                    print(f"{RED}✗ Only large networks detected (prefix < /24).{RESET}")
                    print(f"{YELLOW}  Use --large-networks to scan them.{RESET}")
                    return 1

            # Smallest network first for quicker results
            networks.sort(key=lambda n: _IPv4Network(n, strict=False).num_addresses)

            if len(networks) > 1:
                print(f"{DIM}  Scanning {len(networks)} networks...{RESET}")

            stop_event = threading.Event()
            devices = []
            try:
                for network in networks:
                    network_devices = scan_network(
                        network=network,
                        ports=_all_scpi_ports,
                        verbose=False,
                        stop_event=stop_event,
                    )
                    devices.extend(network_devices)
                    if stop_event.is_set():
                        break
            except KeyboardInterrupt:
                stop_event.set()

            if stop_event.is_set():
                print(f"\n{YELLOW}Scan interrupted.{RESET}")
                return 130

            if not devices:
                print(f"{RED}✗ No SCPI devices found on any network{RESET}")
                print(f"{YELLOW}  Please check device is powered on and connected{RESET}")
                return 1

            if len(devices) == 1:
                host, port, _idn = devices[0]
                print(f"{GREEN}✓ Found device: {BOLD}{host}:{port}{RESET} {DIM}({_idn}){RESET}")
            else:
                print(f"{GREEN}✓ Found {len(devices)} SCPI devices:{RESET}")
                for idx, (ip, p, idn) in enumerate(devices, 1):
                    print(f"{DIM}  [{idx}] {ip:15s}:{p:<5} - {idn}{RESET}")

                while True:
                    try:
                        choice = input(f"\n{CYAN}Select device [1-{len(devices)}]: {RESET}").strip()
                        idx = int(choice)
                        if 1 <= idx <= len(devices):
                            host, port, _idn = devices[idx - 1]
                            print(f"{GREEN}✓ Selected: {BOLD}{host}:{port}{RESET}")
                            break
                        else:
                            print(f"{YELLOW}  Invalid choice. Enter 1-{len(devices)}{RESET}")
                    except (ValueError, KeyboardInterrupt, EOFError):
                        print(f"\n{RED}✗ Selection cancelled{RESET}")
                        return 1
        else:
            host, port = parse_wifi_address(args.wifi)

        # When a port is given explicitly ("host:port") or from AUTO scan, use
        # only that port.  Otherwise race TCP 5025, TCP 1394 and VXI-11 in
        # parallel so the first working transport wins immediately.
        port_explicit = ":" in args.wifi or args.wifi == "AUTO"

        ser = None
        if port_explicit:
            print(f"{CYAN}Connecting to {BOLD}{host}:{port}{RESET}{CYAN} via TCP...{RESET}")
            try:
                candidate = TCPSocket(host, port, timeout=args.timeout)
                candidate.connect()
                ser = candidate
                print(f"{GREEN}✓ Connected to {BOLD}{host}:{port}{RESET}")
                time.sleep(0.2)
            except (OSError, Exception) as e:
                print(f"\n{RED}✗ Could not connect to {host}:{port}: {e}{RESET}")
                return 1
        else:
            # Race all transports in parallel — first to connect wins.
            print(f"{CYAN}Connecting to {BOLD}{host}{RESET}{CYAN}...{RESET}")

            def _tcp(try_port):
                c = TCPSocket(host, try_port, timeout=args.timeout)
                c.connect()
                return "tcp", try_port, c

            def _vxi11():
                v = VXI11Socket(host, timeout=args.timeout)
                v.connect()
                return "vxi11", None, v

            executor = ThreadPoolExecutor(max_workers=3)
            all_fs = [
                executor.submit(_tcp, 5025),
                executor.submit(_tcp, 1394),
                executor.submit(_vxi11),
            ]
            winning_future = None
            for f in _as_completed(all_fs):
                try:
                    kind, conn_port, conn = f.result()
                    if ser is None:
                        ser = conn
                        winning_future = f
                        if kind == "tcp":
                            port = conn_port
                            print(f"{GREEN}✓ Connected to {BOLD}{host}:{conn_port}{RESET}")
                            time.sleep(0.2)
                        else:
                            use_vxi11 = True
                            use_wifi = False
                            print(f"{GREEN}✓ Connected to {BOLD}{host}{RESET} {DIM}(VXI-11){RESET}")
                        break
                except Exception:
                    pass  # This transport failed — wait for the next one.

            executor.shutdown(wait=False)

            # Close any other futures that happened to also succeed.
            for f in all_fs:
                if f is not winning_future and f.done():
                    with contextlib.suppress(Exception):
                        _, _, extra_conn = f.result()
                        extra_conn.close()

            if ser is None:
                print(f"\n{RED}✗ Could not connect to {host}{RESET}")
                return 1
    else:
        if args.port:
            port_path = args.port
            print(f"{CYAN}Using specified port: {BOLD}{port_path}{RESET}")
        else:
            print(f"{CYAN}Scanning for NEnG devices on USB...{RESET}")
            candidates = find_neng_ports()

            if not candidates:
                print(f"{RED}✗ No NEnG/CircuitPython device found on USB.{RESET}")
                all_ports = serial.tools.list_ports.comports()
                if all_ports:
                    print(f"\n{YELLOW}Available serial ports:{RESET}")
                    for i, p in enumerate(all_ports, 1):
                        print(f"  {CYAN}{i}.{RESET} {BOLD}{p.device}{RESET} - {p.description}")
                print(f"{YELLOW}Specify port with --port or use --wifi HOST.{RESET}")
                return 1

            if len(candidates) == 1:
                port_path = candidates[0][0]
                print(
                    f"{GREEN}✓ Found device: {BOLD}{port_path}{RESET} {DIM}({candidates[0][1]}){RESET}"
                )
            else:
                print(f"{GREEN}✓ Found {len(candidates)} devices:{RESET}")
                for idx, (dev, desc) in enumerate(candidates, 1):
                    print(f"{DIM}  [{idx}] {dev:25s} - {desc}{RESET}")

                while True:
                    try:
                        choice = input(
                            f"\n{CYAN}Select device [1-{len(candidates)}]: {RESET}"
                        ).strip()
                        idx = int(choice)
                        if 1 <= idx <= len(candidates):
                            port_path = candidates[idx - 1][0]
                            print(f"{GREEN}✓ Selected: {BOLD}{port_path}{RESET}")
                            break
                        else:
                            print(f"{YELLOW}  Invalid choice. Enter 1-{len(candidates)}{RESET}")
                    except (ValueError, KeyboardInterrupt, EOFError):
                        print(f"\n{RED}✗ Selection cancelled{RESET}")
                        return 1

        try:
            ser = serial.Serial(
                port=port_path,
                baudrate=args.baud,
                timeout=args.timeout,
                write_timeout=args.timeout,
            )
            print(f"{DIM}Waiting for device to initialize...{RESET}")
            time.sleep(1.5)

            # drain startup messages
            startup: list[str] = []
            while ser.in_waiting > 0:
                try:
                    line = ser.readline().decode("utf-8", errors="replace").strip()
                    if line:
                        startup.append(line)
                except Exception:
                    break
                time.sleep(0.01)
            if startup:
                print(f"\n{CYAN}Device startup messages:{RESET}")
                for ln in startup[-10:]:
                    print(f"  {DIM}{ln}{RESET}")
                print()
            ser.reset_input_buffer()
            ser.reset_output_buffer()
        except serial.SerialException as e:
            print(f"\n{RED}✗ Error opening serial port: {e}{RESET}")
            return 1

    # ---- detect device type (best-effort, interactive mode only) -----------
    device_type = "generic"
    _cmds = SCPI_COMMANDS
    if not args.command:
        try:
            idn = send_scpi_command(ser, "*IDN?", args.timeout, sync_mode=False)
            if idn and not idn.startswith("ERROR"):
                device_type = _detect_device_type(idn)
                _cmds = _commands_for_device(device_type)
                _dev_label = {
                    "relaybank16": "RelayBank16",
                    "tmp117": "TMP117",
                    "3dmag": "3Dmag",
                    "rtdmax31865": "RTDMax31865",
                    "lecroy": "LeCroy / Teledyne MAUI Oscilloscope",
                    "scope": "Oscilloscope",
                    "keithley6221": "Keithley 6221 AC/DC Current Source",
                }.get(device_type, "")
                if _dev_label:
                    print(f"{GREEN}✓ Device identified: {BOLD}{_dev_label}{RESET}")

                # Auto-upgrade TCP → VXI-11 for Teledyne/LeCroy scopes.
                # VXI-11 is required for binary waveform transfer; raw TCP
                # cannot reliably handle IEEE 488.2 binary blocks.
                if use_wifi and device_type == "lecroy":
                    print(f"{CYAN}Teledyne/LeCroy scope detected — upgrading to VXI-11...{RESET}")
                    try:
                        ser.close()
                        vxi_ser = VXI11Socket(host, timeout=args.timeout)
                        vxi_ser.connect()
                        ser = vxi_ser
                        use_vxi11 = True
                        use_wifi = False
                        print(
                            f"{GREEN}✓ Reconnected via VXI-11"
                            f" {DIM}(binary waveforms enabled){RESET}"
                        )
                    except Exception as upgrade_err:
                        print(
                            f"{YELLOW}⚠ VXI-11 upgrade failed"
                            f" ({upgrade_err}) — staying on TCP{RESET}"
                        )
        except Exception:
            pass

    # ---- send command(s) ------------------------------------------------
    # Restore stdout before printing the actual response (quiet mode suppressed
    # connection noise via /dev/null; the response goes to real stdout).
    if _sq:
        sys.stdout = _real_stdout

    try:
        if args.command:
            # Split on ';' so multiple commands/queries are sent individually.
            # Each sub-command gets its own send/receive cycle, which works
            # around devices that only respond to the first query in a compound
            # string.  All paths must be absolute (starting with ':' or '*').
            sub_commands = [s.strip() for s in args.command.split(";") if s.strip()]
            any_error = False

            for sub_cmd in sub_commands:
                if not args.quiet:
                    print(f"\n{CYAN}Sending:{RESET} {BOLD}{sub_cmd}{RESET}")
                response = send_scpi_command(
                    ser, sub_cmd, args.timeout, sync_mode=not args.no_sync
                )
                if response:
                    if response.startswith("ERROR:") or response.startswith("-"):
                        print(f"{RED}Error:{RESET} {RED}{response}{RESET}")
                        any_error = True
                        continue
                    if args.raw:
                        print(response)
                    else:
                        formatted = format_response(sub_cmd, response, pretty=not args.no_pretty)
                        if args.quiet:
                            print(formatted)
                        elif "\n" in formatted:
                            print(f"{GREEN}Response:{RESET}{formatted}")
                        else:
                            print(f"{GREEN}Response:{RESET} {YELLOW}{formatted}{RESET}")
                else:
                    print(f"{RED}(no response){RESET}")
                    any_error = True

            if any_error:
                return 1
        else:
            if use_vxi11:
                ci = {"type": "vxi11", "host": host}
            elif use_wifi:
                ci = {"type": "tcp", "host": host, "tcp_port": port}
            else:
                ci = {"type": "serial", "port": port_path, "baudrate": args.baud}
            interactive_mode(
                ser,
                sync_mode=not args.no_sync,
                connection_info=ci,
                pretty_print=not args.no_pretty,
                commands=_cmds,
                device_type=device_type,
            )
    finally:
        ser.close()

    return 0


if __name__ == "__main__":
    sys.exit(main())
