Files
xplane-cockpit/web/src/components/TimerRef.jsx
T
karim 033a9d406a G1000: manual-accurate radios, baro units, declutter, minimums, OBS, audio panel
Aligned to the official X-Plane 1000 manual:
- NAV radio: active RIGHT / standby LEFT (boxed) per S.12 (COM already correct)
- ALT UNIT softkey (IN / HPA) in the PFD submenu, baro readout converts (S.20)
- DCLTR cycles 3 levels (land / +NDB / flight-plan only) with DCLTR-n label (S.56)
- TOPO and TERRAIN are now independent toggles (relief vs awareness overlay) (S.57)
- Barometric MINIMUMS: BARO MIN bug + readout on the altimeter, amber "MINIMUMS"
  annunciation at/below the decision altitude; set via TMR/REF (lifted to App)
- OBS mode: HSI course follows the CRS knob (magenta "OBS"), sequencing suspended
- New Audio Panel tab (COM mic/receive, MKR/DME/ADF, intercom, Display Backup) (S.91)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 05:55:56 +02:00

92 lines
4.0 KiB
React
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 React, { useEffect, useRef, useState } from 'react';
import { num } from '../api/useXplane.js';
// G1000 TMR/REF window (PFD). The real unit shows a generic timer plus the
// reference V-speeds and barometric minimums. This implements the timer
// (count-up or count-down with START/STOP/RESET) and the V-speed / minimums
// references with simple on/off bugs. Self-contained — no sim dependency.
const VSPEEDS = [
{ key: 'vr', label: 'Vr', def: 55 },
{ key: 'vx', label: 'Vx', def: 62 },
{ key: 'vy', label: 'Vy', def: 74 },
{ key: 'vg', label: 'Vg', def: 68 }, // best glide
];
function fmt(sec) {
const s = Math.max(0, Math.floor(sec));
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60;
const pad = (n) => String(n).padStart(2, '0');
return h > 0 ? `${pad(h)}:${pad(m)}:${pad(ss)}` : `${pad(m)}:${pad(ss)}`;
}
export default function TimerRef({ values, onClose, minimums = { on: false, ft: 500 }, onMinimums }) {
const [dir, setDir] = useState('up'); // 'up' | 'dn'
const [running, setRunning] = useState(false);
const [elapsed, setElapsed] = useState(0); // seconds
const [target, setTarget] = useState(300); // count-down start (s)
const [vbugs, setVbugs] = useState({}); // key -> bool (shown on tape, future)
// Minimums are lifted to App so the PFD altimeter can show the BARO MIN bug.
const minsOn = minimums.on, mins = minimums.ft;
const setMins = (fn) => onMinimums && onMinimums((m) => ({ ...m, ft: Math.max(0, typeof fn === 'function' ? fn(m.ft) : fn) }));
const setMinsOn = (fn) => onMinimums && onMinimums((m) => ({ ...m, on: typeof fn === 'function' ? fn(m.on) : fn }));
const tickRef = useRef(null);
useEffect(() => {
if (!running) return;
const t0 = Date.now() - elapsed * 1000;
tickRef.current = setInterval(() => setElapsed((Date.now() - t0) / 1000), 250);
return () => clearInterval(tickRef.current);
}, [running]); // eslint-disable-line
const shown = dir === 'dn' ? Math.max(0, target - elapsed) : elapsed;
const alt = num(values.altitude);
const belowMins = minsOn && alt > 0 && alt <= mins;
const reset = () => { setRunning(false); setElapsed(0); };
return (
<div className="tmr-window">
<div className="nrst-head">
<span className="nrst-title">TIMER / REFERENCES</span>
</div>
<div className="tmr-body">
<div className="tmr-clock">{fmt(shown)}</div>
<div className="tmr-dir">
<button className={dir === 'up' ? 'on' : ''} onClick={() => { setDir('up'); }}>UP</button>
<button className={dir === 'dn' ? 'on' : ''} onClick={() => { setDir('dn'); }}>DN</button>
</div>
<div className="tmr-ctl">
<button className="fbtn add" onClick={() => setRunning((r) => !r)}>{running ? 'STOP' : 'START'}</button>
<button className="fbtn" onClick={reset}>RESET</button>
</div>
{dir === 'dn' && (
<div className="tmr-target">
<label>FROM</label>
<button onClick={() => setTarget((t) => Math.max(60, t - 60))}></button>
<span>{fmt(target)}</span>
<button onClick={() => setTarget((t) => t + 60)}></button>
</div>
)}
<div className="tmr-sec">REFERENCES V-SPEEDS</div>
<div className="tmr-vspeeds">
{VSPEEDS.map((v) => (
<button key={v.key} className={vbugs[v.key] ? 'on' : ''}
onClick={() => setVbugs((b) => ({ ...b, [v.key]: !b[v.key] }))}>
<i>{v.label}</i><b>{v.def}</b><u>KT</u>
</button>
))}
</div>
<div className="tmr-mins">
<button className={minsOn ? 'on' : ''} onClick={() => setMinsOn((m) => !m)}>MINIMUMS</button>
<button onClick={() => setMins((m) => Math.max(0, m - 100))}></button>
<span className={belowMins ? 'alert' : ''}>{mins} FT</span>
<button onClick={() => setMins((m) => m + 100)}></button>
</div>
{belowMins && <div className="tmr-minalert">MINIMUMS</div>}
</div>
</div>
);
}