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,210 @@
|
||||
import React, { useState } from 'react';
|
||||
import { num } from '../api/useXplane.js';
|
||||
import MapView from './MapView.jsx';
|
||||
|
||||
const arr = (v, i = 0, d = 0) => (Array.isArray(v) ? num(v[i], d) : num(v, d));
|
||||
const KG_PER_GAL = 2.72; // avgas
|
||||
const navF = (v) => (num(v) / 100).toFixed(2);
|
||||
const comF = (v) => (num(v) / 100).toFixed(3);
|
||||
|
||||
// G1000 MFD — full-width NAV/COM bar on top, the engine instrument strip (EIS)
|
||||
// down the left as real bar gauges, and the moving map (X-Plane nav data) with
|
||||
// G1000 chrome (compass rose, range, NORTH UP, mode) filling the rest.
|
||||
export default function MFD({ values: V, flightPlan, fp, mapMode }) {
|
||||
const [rangeNm, setRangeNm] = useState(8);
|
||||
return (
|
||||
<div className="mfd-g1000">
|
||||
<MfdTopBar V={V} />
|
||||
<div className="mfd-body">
|
||||
<EisStrip V={V} />
|
||||
<div className="mfd-map">
|
||||
<MapView values={V} flightPlan={flightPlan} fp={fp} hud={false}
|
||||
mapMode={mapMode} dcltr={mapMode?.dcltr || 0} onView={({ rangeNm }) => setRangeNm(rangeNm)} />
|
||||
<MapChrome V={V} rangeNm={rangeNm} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- top NAV/COM bar ---------------- */
|
||||
function MfdTopBar({ V }) {
|
||||
const gs = Math.round(num(V.groundspeed) * 1.94384);
|
||||
const trk = String(Math.round(num(V.track)) % 360).padStart(3, '0');
|
||||
const swap = (x, y) => <text x={x} y={y} fill="#0ff" fontSize="16" textAnchor="middle">⇔</text>;
|
||||
return (
|
||||
<svg className="mfd-topbar" viewBox="0 0 1000 70" preserveAspectRatio="none" fontFamily="monospace">
|
||||
<rect x="0" y="0" width="1000" height="70" fill="#000" />
|
||||
{[300, 660].map((x) => <line key={x} x1={x} y1="2" x2={x} y2="68" stroke="#333" strokeWidth="1.5" />)}
|
||||
<line x1="0" y1="70" x2="1000" y2="70" stroke="#3a3a3a" strokeWidth="2" />
|
||||
{/* NAV1 / NAV2 */}
|
||||
<text x="10" y="27" fill="#fff" fontSize="13">NAV1</text>
|
||||
<rect x="50" y="11" width="80" height="21" fill="none" stroke="#0ff" strokeWidth="1.3" />
|
||||
<text x="126" y="27" fill="#0ff" fontSize="17" textAnchor="end">{navF(V.nav1)}</text>
|
||||
{swap(150, 27)}
|
||||
<text x="174" y="27" fill="#fff" fontSize="17">{navF(V.nav1Sb)}</text>
|
||||
<text x="10" y="58" fill="#fff" fontSize="13">NAV2</text>
|
||||
<text x="126" y="58" fill="#fff" fontSize="17" textAnchor="end">{navF(V.nav2)}</text>
|
||||
<text x="174" y="58" fill="#fff" fontSize="17">{navF(V.nav2Sb)}</text>
|
||||
{/* centre: GS/DTK/TRK/ETE + active mode line */}
|
||||
<text x="312" y="27" fill="#fff" fontSize="13">GS</text>
|
||||
<text x="350" y="27" fill="#e040fb" fontSize="15" fontWeight="bold">{gs}</text>
|
||||
<text x="378" y="27" fill="#0c9" fontSize="11">KT</text>
|
||||
<text x="410" y="27" fill="#fff" fontSize="13">DTK</text>
|
||||
<text x="520" y="27" fill="#fff" fontSize="13">TRK</text>
|
||||
<text x="560" y="27" fill="#e040fb" fontSize="15" fontWeight="bold">{trk}°</text>
|
||||
<text x="610" y="27" fill="#fff" fontSize="13">ETE</text>
|
||||
<text x="480" y="58" fill="#0ff" fontSize="15" textAnchor="middle">NAV – DEFAULT NAV</text>
|
||||
{/* COM1 / COM2 */}
|
||||
<text x="690" y="27" fill="#0f0" fontSize="17">{comF(V.com1)}</text>
|
||||
{swap(818, 27)}
|
||||
<rect x="846" y="11" width="92" height="21" fill="none" stroke="#0ff" strokeWidth="1.3" />
|
||||
<text x="936" y="27" fill="#0ff" fontSize="17" textAnchor="end">{comF(V.com1Sb)}</text>
|
||||
<text x="994" y="27" fill="#fff" fontSize="12" textAnchor="end">COM1</text>
|
||||
<text x="690" y="58" fill="#fff" fontSize="17">{comF(V.com2)}</text>
|
||||
<text x="936" y="58" fill="#fff" fontSize="17" textAnchor="end">{comF(V.com2Sb)}</text>
|
||||
<text x="994" y="58" fill="#fff" fontSize="12" textAnchor="end">COM2</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- engine instrument strip (EIS) ---------------- */
|
||||
function EisStrip({ V }) {
|
||||
const rpm = arr(V.engRpm);
|
||||
const ffGph = (arr(V.fuelFlow) * 3600) / KG_PER_GAL;
|
||||
const oilPsi = arr(V.oilPress);
|
||||
const oilF = arr(V.oilTemp) * 9 / 5 + 32;
|
||||
const egtF = arr(V.egt) * 9 / 5 + 32;
|
||||
const fuelL = arr(V.fuelQty, 0) / KG_PER_GAL;
|
||||
const fuelR = arr(V.fuelQty, 1) / KG_PER_GAL;
|
||||
const volts = arr(V.volts, 0, 28);
|
||||
const amps = arr(V.amps);
|
||||
return (
|
||||
<svg className="eis-svg" viewBox="0 0 190 540" preserveAspectRatio="xMidYMin meet" fontFamily="monospace">
|
||||
<rect x="0" y="0" width="190" height="540" fill="#0a0a0a" />
|
||||
<RpmArc rpm={rpm} />
|
||||
<Bar y={132} label="FFLOW GPH" val={ffGph.toFixed(1)} min={0} max={20} value={ffGph}
|
||||
zones={[{ from: 0, to: 17, c: '#0c0' }, { from: 17, to: 20, c: '#c00' }]} />
|
||||
<Bar y={170} label="OIL PSI" val={Math.round(oilPsi)} min={0} max={100} value={oilPsi}
|
||||
zones={[{ from: 0, to: 20, c: '#c00' }, { from: 20, to: 100, c: '#0c0' }]} />
|
||||
<Bar y={208} label="OIL °F" val={Math.round(oilF)} min={75} max={250} value={oilF}
|
||||
zones={[{ from: 100, to: 245, c: '#0c0' }]} />
|
||||
<Bar y={246} label="EGT °F" val={Math.round(egtF)} min={800} max={1650} value={egtF} zones={[]} />
|
||||
<Bar y={284} label="VAC" min={0} max={10} value={5}
|
||||
zones={[{ from: 4.5, to: 5.5, c: '#0c0' }]} />
|
||||
<FuelBar y={330} left={fuelL} right={fuelR} />
|
||||
<text x="8" y="412" fill="#39d3c0" fontSize="12">ENG</text>
|
||||
<text x="182" y="412" fill="#fff" fontSize="14" textAnchor="end">0.0 HRS</text>
|
||||
<text x="95" y="438" fill="#39d3c0" fontSize="12" textAnchor="middle">– ELECTRICAL –</text>
|
||||
<text x="20" y="462" fill="#fff" fontSize="12">M</text>
|
||||
<text x="95" y="462" fill="#39d3c0" fontSize="12" textAnchor="middle">BUS</text>
|
||||
<text x="170" y="462" fill="#fff" fontSize="12" textAnchor="end">E</text>
|
||||
<text x="18" y="482" fill="#fff" fontSize="15">{volts.toFixed(1)}</text>
|
||||
<text x="95" y="482" fill="#39d3c0" fontSize="11" textAnchor="middle">VOLTS</text>
|
||||
<text x="172" y="482" fill="#fff" fontSize="15" textAnchor="end">{volts.toFixed(1)}</text>
|
||||
<text x="20" y="506" fill="#fff" fontSize="12">M</text>
|
||||
<text x="95" y="506" fill="#39d3c0" fontSize="12" textAnchor="middle">BATT</text>
|
||||
<text x="170" y="506" fill="#fff" fontSize="12" textAnchor="end">S</text>
|
||||
<text x="18" y="526" fill="#fff" fontSize="15">{amps >= 0 ? '+' : ''}{amps.toFixed(1)}</text>
|
||||
<text x="95" y="526" fill="#39d3c0" fontSize="11" textAnchor="middle">AMPS</text>
|
||||
<text x="172" y="526" fill="#fff" fontSize="15" textAnchor="end">+0.0</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({ y, label, val, min, max, value, zones }) {
|
||||
const x0 = 8, x1 = 182, bw = x1 - x0;
|
||||
const px = (v) => x0 + bw * Math.max(0, Math.min(1, (v - min) / (max - min)));
|
||||
const p = px(value);
|
||||
return (
|
||||
<g>
|
||||
<text x={x0} y={y} fill="#39d3c0" fontSize="12">{label}</text>
|
||||
{val != null && <text x={x1} y={y} fill="#fff" fontSize="16" fontWeight="bold" textAnchor="end">{val}</text>}
|
||||
<rect x={x0} y={y + 9} width={bw} height="5" fill="#2a2a2a" />
|
||||
{zones.map((z, i) => <rect key={i} x={px(z.from)} y={y + 9} width={Math.max(0, px(z.to) - px(z.from))} height="5" fill={z.c} />)}
|
||||
<polygon points={`${p},${y + 9} ${p - 5},${y + 1} ${p + 5},${y + 1}`} fill="#fff" stroke="#000" strokeWidth="0.5" />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
// Fuel quantity: one bar per the C172's two tanks, with L and R pointers on a
|
||||
// shared 0–10–20–F (gal) scale; yellow/red caution zone at the low end.
|
||||
function FuelBar({ y, left, right }) {
|
||||
const x0 = 8, x1 = 182, bw = x1 - x0, max = 26.5;
|
||||
const px = (g) => x0 + bw * Math.max(0, Math.min(1, g / max));
|
||||
const tick = (g, lbl) => (
|
||||
<g key={lbl}>
|
||||
<line x1={px(g)} y1={y + 16} x2={px(g)} y2={y + 20} stroke="#777" strokeWidth="1" />
|
||||
<text x={px(g)} y={y + 31} fill="#aaa" fontSize="10" textAnchor="middle">{lbl}</text>
|
||||
</g>
|
||||
);
|
||||
const ptr = (g, lbl) => (
|
||||
<g>
|
||||
<polygon points={`${px(g)},${y + 8} ${px(g) - 5},${y} ${px(g) + 5},${y}`} fill="#fff" stroke="#000" strokeWidth="0.5" />
|
||||
<text x={px(g)} y={y - 2} fill="#fff" fontSize="9" textAnchor="middle">{lbl}</text>
|
||||
</g>
|
||||
);
|
||||
return (
|
||||
<g>
|
||||
<text x={x0} y={y - 6} fill="#39d3c0" fontSize="12">FUEL QTY GAL</text>
|
||||
<rect x={x0} y={y + 8} width={bw} height="6" fill="#2a2a2a" />
|
||||
<rect x={px(0)} y={y + 8} width={px(2.5) - px(0)} height="6" fill="#c00" />
|
||||
<rect x={px(2.5)} y={y + 8} width={px(5) - px(2.5)} height="6" fill="#dd0" />
|
||||
<rect x={px(5)} y={y + 8} width={px(max) - px(5)} height="6" fill="#0c0" />
|
||||
{tick(0, '0')}{tick(8.83, '10')}{tick(17.66, '20')}{tick(max, 'F')}
|
||||
{ptr(left, 'L')}{ptr(right, 'R')}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function RpmArc({ rpm }) {
|
||||
const max = 2700, frac = Math.max(0, Math.min(1, rpm / max));
|
||||
const a0 = -210, a1 = 30, ang = a0 + (a1 - a0) * frac;
|
||||
const cx = 95, cy = 62, r = 42;
|
||||
const pt = (deg, rr) => [cx + rr * Math.cos((deg * Math.PI) / 180), cy + rr * Math.sin((deg * Math.PI) / 180)];
|
||||
const arc = (s, e, color, w) => {
|
||||
const [x0, y0] = pt(s, r), [x1, y1] = pt(e, r);
|
||||
return <path d={`M${x0} ${y0} A${r} ${r} 0 ${e - s > 180 ? 1 : 0} 1 ${x1} ${y1}`} fill="none" stroke={color} strokeWidth={w} />;
|
||||
};
|
||||
const [nx, ny] = pt(ang, r - 2);
|
||||
return (
|
||||
<g fontFamily="monospace">
|
||||
{arc(a0, a1, '#2a2a2a', 7)}
|
||||
{arc(a0, -30, '#0c0', 7)}
|
||||
{arc(0, a1, '#c00', 7)}
|
||||
<line x1={cx} y1={cy} x2={nx} y2={ny} stroke="#fff" strokeWidth="2.5" />
|
||||
<circle cx={cx} cy={cy} r="3" fill="#fff" />
|
||||
<text x={cx} y={cy + 14} fill="#39d3c0" fontSize="12" textAnchor="middle">RPM</text>
|
||||
<text x={cx} y={cy + 40} fill="#fff" fontSize="26" fontWeight="bold" textAnchor="middle">{Math.round(rpm)}</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- map chrome overlay (compass rose / range / mode) ---------------- */
|
||||
const NICE = [0.5, 1, 1.5, 2, 2.5, 4, 5, 7.5, 10, 15, 20, 25, 40, 50, 75, 100, 150, 200, 250, 500];
|
||||
function niceRange(nm) { let r = NICE[0]; for (const s of NICE) if (nm >= s) r = s; return r; }
|
||||
|
||||
function MapChrome({ V, rangeNm }) {
|
||||
const gs = Math.round(num(V.groundspeed) * 1.94384);
|
||||
const rng = niceRange(rangeNm);
|
||||
const cx = 160, cy = 160, r = 150;
|
||||
const ticks = [];
|
||||
for (let d = 0; d < 360; d += 10) {
|
||||
const a = ((d - 90) * Math.PI) / 180;
|
||||
const big = d % 30 === 0;
|
||||
const r2 = r - (big ? 12 : 7);
|
||||
ticks.push(<line key={d} x1={cx + r * Math.cos(a)} y1={cy + r * Math.sin(a)} x2={cx + r2 * Math.cos(a)} y2={cy + r2 * Math.sin(a)} stroke="#cfd6dd" strokeWidth={big ? 2 : 1} />);
|
||||
if (big) {
|
||||
const lbl = d === 0 ? 'N' : d === 90 ? 'E' : d === 180 ? 'S' : d === 270 ? 'W' : d / 10;
|
||||
ticks.push(<text key={'l' + d} x={cx + (r - 26) * Math.cos(a)} y={cy + (r - 26) * Math.sin(a) + 5} fill="#fff" fontSize="15" textAnchor="middle" fontFamily="monospace">{lbl}</text>);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="map-chrome">
|
||||
<svg className="map-rose" viewBox="0 0 320 320">{ticks}</svg>
|
||||
<div className="mc-tr"><b>{gs} KT</b><span>NORTH UP</span></div>
|
||||
<div className="mc-range">{rng} NM</div>
|
||||
<div className="mc-mode">NAV <em className="on" /><em /><em /><em /><em /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user