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>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user