diff --git a/src/main/decode.test.ts b/src/main/decode.test.ts index a3ab53b..11f5064 100644 --- a/src/main/decode.test.ts +++ b/src/main/decode.test.ts @@ -41,6 +41,6 @@ describe('Decoder aircraft tracking', () => { describe('Decoder telemetry', () => { it('exposes the five readouts', () => { const ids = new Decoder().telemetrySnapshot().map((r) => r.id) - expect(ids).toEqual(['RPM', 'D_IAS_DEG', 'D_FLAPS_IND', 'D_ALT_NEEDLE', 'D_FUEL']) + expect(ids).toEqual(['RPM', 'D_IAS_DEG', 'D_FLAPS_IND', 'ALT_FT', 'D_FUEL']) }) }) diff --git a/src/main/decode.ts b/src/main/decode.ts index 9f928ca..df86063 100644 --- a/src/main/decode.ts +++ b/src/main/decode.ts @@ -11,13 +11,46 @@ interface TelemetrySpec { unit: string } -// DCS-BIOS gauge outputs are 0..max needle positions, not engineering units, so -// readouts are reported as percent of full scale (honest, no fake knots/feet). +// Three altimeter drum addresses encode digit positions 0–9.999 as 0–65535. +// ALT_FT is a virtual telemetry id computed from them; it has no DCS-BIOS address. +const ALT_DRUM_IDS = new Set(['D_ALT_10K', 'D_ALT_1K', 'D_ALT_100S']) +const ALT_FT_MAX = 50_000 + +// Piecewise-linear IAS calibration: [raw, knots] pairs derived from A-4E gauge face. +// The pitot-static scale is nonlinear (angle ∝ v²); raw=0 is init/parked (0 kt). +// Tunable — fly at known speeds and update the table as needed. +const IAS_KT_LUT: [number, number][] = [ + [0, 0], + [5461, 80], + [10923, 120], + [16384, 150], + [24576, 200], + [32768, 300], + [40960, 400], + [46432, 500], + [49152, 600] +] +const IAS_MAX_KT = 600 + +function iasRawToKnots(raw: number): number { + const lut = IAS_KT_LUT + if (raw <= lut[0][0]) return lut[0][1] + if (raw >= lut[lut.length - 1][0]) return lut[lut.length - 1][1] + for (let i = 1; i < lut.length; i++) { + if (raw <= lut[i][0]) { + const [r0, k0] = lut[i - 1] + const [r1, k1] = lut[i] + return k0 + ((raw - r0) / (r1 - r0)) * (k1 - k0) + } + } + return NaN +} + const TELEMETRY: TelemetrySpec[] = [ { id: 'RPM', label: 'RPM', unit: '% RPM' }, - { id: 'D_IAS_DEG', label: 'IAS', unit: '% FS' }, + { id: 'D_IAS_DEG', label: 'IAS', unit: 'kt' }, { id: 'D_FLAPS_IND', label: 'Flap', unit: '% DN' }, - { id: 'D_ALT_NEEDLE', label: 'Press Alt', unit: '% FS' }, + { id: 'ALT_FT', label: 'Press Alt', unit: 'ft' }, { id: 'D_FUEL', label: 'Fuel', unit: '% QTY' } ] @@ -35,12 +68,15 @@ export class Decoder { private readonly name = new StringRegion(ACFT_NAME_ADDRESS, ACFT_NAME_MAX) private readonly telemetry = new Map() // id -> latest value private readonly telemetryMax = new Map() + private readonly altDrums = new Map() // D_ALT_10K/1K/100S raw values private lastName = '' private sawA4ecAddress = false private aircraftDirty = true constructor() { - for (const t of TELEMETRY) this.telemetry.set(t.id, NaN) + for (const t of TELEMETRY) { + if (t.id !== 'ALT_FT') this.telemetry.set(t.id, NaN) + } } /** Apply one write; returns the decoded log rows it produced (may be empty). */ @@ -70,6 +106,9 @@ export class Decoder { this.telemetry.set(o.id, value) this.telemetryMax.set(o.id, o.max > 0 ? o.max : 0xffff) } + if (ALT_DRUM_IDS.has(o.id)) { + this.altDrums.set(o.id, value) + } } return rows } @@ -89,10 +128,28 @@ export class Decoder { telemetrySnapshot(): TelemetryReadout[] { return TELEMETRY.map((t) => { + if (t.id === 'D_IAS_DEG') { + const raw = this.telemetry.get('D_IAS_DEG') ?? NaN + const kt = Number.isNaN(raw) ? NaN : iasRawToKnots(raw) + const pct = Number.isNaN(kt) ? 0 : Math.max(0, Math.min(1, kt / IAS_MAX_KT)) + return { id: 'D_IAS_DEG', label: t.label, value: kt, pct, unit: t.unit } + } + if (t.id === 'ALT_FT') { + const d10k = this.altDrums.get('D_ALT_10K') + const d1k = this.altDrums.get('D_ALT_1K') + const d100s = this.altDrums.get('D_ALT_100S') + const alt = + d10k !== undefined && d1k !== undefined && d100s !== undefined + ? Math.floor((d10k / 65535) * 10) * 10000 + + Math.floor((d1k / 65535) * 10) * 1000 + + Math.floor((d100s / 65535) * 10) * 100 + : NaN + const pct = Number.isNaN(alt) ? 0 : Math.max(0, Math.min(1, alt / ALT_FT_MAX)) + return { id: 'ALT_FT', label: t.label, value: alt, pct, unit: t.unit } + } const raw = this.telemetry.get(t.id) ?? NaN const max = this.telemetryMax.get(t.id) ?? 0xffff const pct = Number.isNaN(raw) ? 0 : Math.max(0, Math.min(1, raw / max)) - // value = percent of full scale; NaN => "—" in the UI (not exported yet) const value = Number.isNaN(raw) ? NaN : Math.round(pct * 100) return { id: t.id, label: t.label, value, pct, unit: t.unit } }) diff --git a/src/renderer/src/tabs/Connection.tsx b/src/renderer/src/tabs/Connection.tsx index d0b0b85..2b67917 100644 --- a/src/renderer/src/tabs/Connection.tsx +++ b/src/renderer/src/tabs/Connection.tsx @@ -1,5 +1,19 @@ import { fmtUptime, useStore, type SourceMode, type Transport } from '../store' +const REC_DOT = ( + +) + function NodesCard() { const nodes = useStore((x) => x.nodes) const sourceMode = useStore((x) => x.sourceMode) @@ -67,6 +81,7 @@ export function Connection() { const setConfigField = useStore((x) => x.setConfigField) const toggleRelay = useStore((x) => x.toggleRelay) const openReplay = useStore((x) => x.openReplay) + const toggleCapture = useStore((x) => x.toggleCapture) // The device details are live: populated once a SimGateway is actually found. // Before that, only the match target (VID/PID) is shown — not real device data. @@ -272,6 +287,30 @@ export function Connection() { + {s.sourceMode !== 'replay' && ( +
+
+ Record +
+ +
+ {s.recording + ? 'Recording live DCS-BIOS stream. Stop to save — replay the file on any machine with no DCS.' + : s.relaying + ? 'Capture a live session to replay later with no DCS running.' + : 'Start the relay first to enable recording.'} +
+
+ )} +