Files
xplane-cockpit/web/src/api/useXplane.js
T
karim ebc33a78b7 Initial commit: X-Plane G1000 web cockpit + bridge + Tauri desktop app
- server/: Node bridge (datarefs/commands, navdata, CIFP procedures, flight plan)
- web/: React cockpit (PFD/MFD/Map, VFR six-pack, AFCS, FMS CDU), PWA, collapsible sidebar
- desktop/: Tauri 2 launcher (Bun sidecar, system tray, updater) + Linux build via Docker
- scripts/: prep-desktop, build-linux, Gitea release + latest.json

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:07:03 +02:00

89 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);