import { useEffect, useRef, useState, useCallback } from 'react'; // Single WebSocket connection to the bridge. Streams dataref values + the // shared flight plan in; sends commands / dataref writes / flight-plan edits // out. Auto-reconnects. export function useXplane() { const [values, setValues] = useState({}); const [flightPlan, setFlightPlan] = useState({ name: 'ACTIVE', waypoints: [] }); const [exportMsg, setExportMsg] = useState(null); const [connected, setConnected] = useState(false); // socket to bridge const [xpConnected, setXpConnected] = useState(false); // bridge <-> X-Plane const wsRef = useRef(null); useEffect(() => { let closed = false; let retry; // Coalesce incoming value bursts into a single React update per animation // frame — keeps the gauges smooth instead of re-rendering ~20×/sec. let pending = null; let raf = 0; const flush = () => { raf = 0; if (pending) { const p = pending; pending = null; setValues((prev) => ({ ...prev, ...p })); } }; const connect = () => { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const ws = new WebSocket(`${proto}://${location.host}/ws`); wsRef.current = ws; ws.onopen = () => setConnected(true); ws.onmessage = (ev) => { const msg = JSON.parse(ev.data); if (msg.type === 'values') { pending = pending ? Object.assign(pending, msg.data) : { ...msg.data }; if (!raf) raf = requestAnimationFrame(flush); } else if (msg.type === 'status') setXpConnected(!!msg.xpConnected); else if (msg.type === 'flightplan') setFlightPlan(msg.data); else if (msg.type === 'fp_export_result') setExportMsg(msg); }; ws.onclose = () => { setConnected(false); setXpConnected(false); if (!closed) retry = setTimeout(connect, 2000); }; ws.onerror = () => ws.close(); }; connect(); return () => { closed = true; clearTimeout(retry); wsRef.current?.close(); }; }, []); const send = useCallback((obj) => { const ws = wsRef.current; if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj)); }, []); const command = useCallback((name, duration = 0) => send({ type: 'command', name, duration }), [send]); const setDataref = useCallback((name, value) => send({ type: 'setDataref', name, value }), [send]); // flight-plan actions const fp = { add: (ident) => send({ type: 'fp_add', ident }), addLatLon: (lat, lon) => send({ type: 'fp_add', ident: `${lat},${lon}` }), remove: (index) => send({ type: 'fp_remove', index }), setActive: (index) => send({ type: 'fp_active', index }), clear: () => send({ type: 'fp_clear' }), set: (plan) => send({ type: 'fp_set', plan }), export: (name) => send({ type: 'fp_export', name }), }; return { values, flightPlan, exportMsg, connected, xpConnected, command, setDataref, fp }; } // Search X-Plane's nav database (waypoints/VOR/NDB/airports) via the bridge. export async function navSearch(q) { if (!q) return []; try { const r = await fetch(`/api/nav/search?q=${encodeURIComponent(q)}`); return r.ok ? r.json() : []; } catch { return []; } } // Convenience: read a numeric value with a fallback. export const num = (v, d = 0) => (typeof v === 'number' && isFinite(v) ? v : d);