import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { RefreshCw, Usb, TriangleAlert } from "lucide-react"; import { Input } from "@/components/input"; import { isElectron } from "@/lib/electron"; import { websocketAuthProtocols } from "@/lib/ws-auth"; import type { SerialConfig } from "@/types/ui-types"; import { Select2 } from "@/components/select2"; const BAUD_RATES = [ 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, ]; interface SerialPanelProps { onConnect: (config: SerialConfig) => void; } // In Chrome/Edge the Web Serial API shows its own native port picker on requestPort(), // so getPorts() only returns previously-granted ports. We never need the user to type // a path — the browser handles selection at connect time. const useWebSerial = !isElectron() && "serial" in navigator; const serialUnsupported = !isElectron() && !("serial" in navigator); export function SerialPanel({ onConnect }: SerialPanelProps) { const { t } = useTranslation(); const [path, setPath] = useState(""); const [baudRate, setBaudRate] = useState(115200); const [dataBits, setDataBits] = useState<5 | 6 | 7 | 8>(8); const [stopBits, setStopBits] = useState<1 | 2>(1); const [parity, setParity] = useState<"none" | "even" | "odd">("none"); // Electron only: ports discovered from the backend const [availablePorts, setAvailablePorts] = useState([]); const [loadingPorts, setLoadingPorts] = useState(false); const buildWsUrl = () => { // Serial is always local -- the device is physically attached to this // desktop machine, so it never routes through a remote server. return "ws://127.0.0.1:30011"; }; const refreshPorts = useCallback(() => { if (!isElectron()) return; setLoadingPorts(true); const url = buildWsUrl(); if (!url) { setLoadingPorts(false); return; } const ws = new WebSocket( url, websocketAuthProtocols(localStorage.getItem("jwt")), ); ws.onopen = () => ws.send(JSON.stringify({ type: "list_ports" })); ws.onmessage = (ev) => { try { const msg = JSON.parse(ev.data as string) as { type: string; data?: { path: string }[]; }; if (msg.type === "ports_list" && Array.isArray(msg.data)) { const paths = msg.data.map((p) => p.path); setAvailablePorts(paths); if (!path && paths.length > 0) setPath(paths[0]); } } catch { // ignore } finally { ws.close(); setLoadingPorts(false); } }; ws.onerror = () => setLoadingPorts(false); }, [path]); useEffect(() => { if (isElectron()) refreshPorts(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const connect = () => { // For Web Serial, path is irrelevant — the browser picker handles it. // We pass an empty string; Serial.tsx calls requestPort() itself. if (useWebSerial) { onConnect({ path: "", baudRate, dataBits, stopBits, parity }); return; } const finalPath = path.trim(); if (!finalPath) return; onConnect({ path: finalPath, baudRate, dataBits, stopBits, parity }); }; // ── Not supported (Firefox, Safari, etc.) ───────────────────────────── if (serialUnsupported) { return (
{t("serial.notSupportedTitle")}

{t("serial.notSupported")}

{t("serial.hideHint")}

); } // ── Web Serial (Chrome / Edge) ───────────────────────────────────────── if (useWebSerial) { return (

{t("serial.browserPickerHint")}

setBaudRate(Number(e.target.value))} className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" > {BAUD_RATES.map((r) => ( ))}
setDataBits(Number(e.target.value) as 5 | 6 | 7 | 8) } className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" > {([5, 6, 7, 8] as const).map((b) => ( ))}
setStopBits(Number(e.target.value) as 1 | 2)} className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" >
setParity(e.target.value as "none" | "even" | "odd") } className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" >
); } // ── Electron (backend serialport) ───────────────────────────────────── return (
{availablePorts.length > 0 && ( setPath(e.target.value)} className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" > {availablePorts.map((p) => ( ))} )} setPath(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") connect(); }} className="h-7 text-xs font-mono" />
setBaudRate(Number(e.target.value))} className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" > {BAUD_RATES.map((r) => ( ))}
setDataBits(Number(e.target.value) as 5 | 6 | 7 | 8) } className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" > {([5, 6, 7, 8] as const).map((b) => ( ))}
setStopBits(Number(e.target.value) as 1 | 2)} className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" >
setParity(e.target.value as "none" | "even" | "odd") } className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" >
); }