| name | aircraft-instruments |
| description | Simulate, render, and model aircraft flight instruments and sensors in software — including real-time display widgets, physics-accurate instrument behavior, sensor error modeling, and failure simulation. Use this skill whenever the user asks to build, design, or model any aircraft instrument or avionics display, including: attitude indicator, airspeed indicator, altimeter, VSI, turn coordinator, heading indicator, HSI, CDI, NAV/COM radios, transponder, EGT/CHT gauges, fuel quantity, oil pressure, ammeter, vacuum gauge, glass cockpit displays (PFD/MFD/EFIS), autopilot annunciators, engine instruments, or any cockpit sensor. Also trigger for: "simulate instrument lag", "model pitot-static errors", "build a six-pack widget", "render a gyro horizon", "instrument failure injection", "simulate icing on pitot tube", "gyroscopic precession model", "VOR needle simulation", "ILS CDI/glideslope", "build an EFIS", "model gyro tumble", "ADC (air data computer) simulation", or any request to render instruments as canvas/SVG/WebGL graphics in a browser or sim environment.
|
Aircraft Instruments & Sensor Simulation Skill
Build accurate, visually faithful instrument simulations — from individual
canvas-rendered gauges to full glass cockpit EFIS displays. Covers physics
modeling, error behavior, failure modes, and rendering technique.
Architecture Overview
Every instrument simulation has three layers:
┌─────────────────────────────────────────┐
│ SENSOR / PHYSICS MODEL │ ← computes "true" value + errors
│ (atmosphere, gyro dynamics, radio nav) │
├─────────────────────────────────────────┤
│ INSTRUMENT MODEL │ ← applies lag, precession, limits
│ (lag filter, error accumulation, │
│ failure state machine) │
├─────────────────────────────────────────┤
│ DISPLAY / RENDERER │ ← canvas/SVG drawing, animations
│ (needle positions, tape scrolling, │
│ annunciators, color bands) │
└─────────────────────────────────────────┘
Never skip the instrument model layer — raw physics values fed directly to
the display produce unrealistically responsive instruments.
§1 — Pitot-Static Instruments
Shared Data Source
function computeADC(state, environment) {
const { altitude, verticalSpeed, trueAirspeed, pitch, roll } = state;
const { rho, rho0 } = atmosphere(altitude);
const dynamicPressure = 0.5 * rho * trueAirspeed ** 2;
const ias = Math.sqrt(2 * dynamicPressure / rho0);
const cas = ias;
return { ias, cas, altitude, verticalSpeed };
}
Airspeed Indicator (ASI)
Physics: reads dynamic pressure (pitot minus static). Display in knots or MPH.
class AirspeedIndicator {
constructor(config) {
this.config = {
Vso: config.Vso || 44,
Vs1: config.Vs1 || 50,
Vfe: config.Vfe || 85,
Vno: config.Vno || 129,
Vne: config.Vne || 163,
maxDisplay: config.maxDisplay || 200,
...config
};
this.indicated = 0;
this.lag = 0.15;
}
update(ias, dt) {
const tau = this.lag;
this.indicated += (ias - this.indicated) * (1 - Math.exp(-dt / tau));
}
}
Rendering — color arcs are the signature feature:
function renderASI(ctx, instrument, cx, cy, radius) {
const { Vso, Vs1, Vfe, Vno, Vne, maxDisplay } = instrument.config;
const toAngle = v => (v / maxDisplay) * 270 - 135;
drawArc(ctx, cx, cy, radius * 0.88, toAngle(Vso), toAngle(Vfe), '#ffffff', 6);
drawArc(ctx, cx, cy, radius * 0.88, toAngle(Vs1), toAngle(Vno), '#00cc44', 6);
drawArc(ctx, cx, cy, radius * 0.88, toAngle(Vno), toAngle(Vne), '#ffcc00', 6);
drawRadialMark(ctx, cx, cy, radius * 0.82, radius * 0.94, toAngle(Vne), '#ff2200', 3);
const needleAngle = toAngle(instrument.indicated);
drawNeedle(ctx, cx, cy, radius * 0.75, needleAngle, '#ffffff');
}
Altimeter
Physics: reads static pressure, converts to altitude via ISA model.
class Altimeter {
constructor() {
this.indicated = 0;
this.kollsman = 29.92;
this.lag = 1.5;
this.error = 0;
}
update(pressureAltitude, dt) {
const actualAltitude = pressureAltitude + (this.kollsman - 29.92) * 1000;
this.indicated += (actualAltitude - this.indicated) * (1 - Math.exp(-dt / this.lag));
}
setKollsman(setting) {
this.kollsman = Math.max(28.0, Math.min(31.0, setting));
}
}
Rendering — three concentric needles:
function renderAltimeter(ctx, instrument, cx, cy, radius) {
const alt = instrument.indicated;
const hand100 = ((alt % 1000) / 1000) * 360 - 90;
const hand1000 = ((alt % 10000) / 10000) * 360 - 90;
const hand10000 = ((alt % 100000)/ 100000)* 360 - 90;
drawNeedle(ctx, cx, cy, radius * 0.65, hand100, '#ffffff', 2.5);
drawNeedle(ctx, cx, cy, radius * 0.75, hand1000, '#ffffff', 2);
drawNeedle(ctx, cx, cy, radius * 0.55, hand10000, '#ffffff', 1.5);
}
Vertical Speed Indicator (VSI)
Physics: measures rate of static pressure change. Significant lag (~6–9 seconds).
class VSI {
constructor() {
this.indicated = 0;
this.lag = 7.0;
this.range = 2000;
}
update(trueVS, dt) {
this.indicated += (trueVS - this.indicated) * (1 - Math.exp(-dt / this.lag));
this.indicated = Math.max(-this.range, Math.min(this.range, this.indicated));
}
}
§2 — Gyroscopic Instruments
Attitude Indicator (AI / Artificial Horizon)
Physics: vacuum-driven gyro. Key behaviors to model:
- Precession: gyro axis drifts ~3°/min, more during maneuvers
- Erection: vacuum system slowly re-erects gyro (~3 min to stabilize)
- Tumble limits: older instruments tumble beyond ±60° pitch / ±100° bank
- Power loss: gyro spools down over ~15 min, instrument gradually topples
class AttitudeIndicator {
constructor() {
this.displayPitch = 0;
this.displayRoll = 0;
this.gyroPitch = 0;
this.gyroRoll = 0;
this.precessionRate = (3 * Math.PI / 180) / 60;
this.erectionRate = (3 * Math.PI / 180) / 60;
this.failed = false;
this.vacuum = 1.0;
}
update(truePitch, trueRoll, dt) {
if (this.failed) return;
const precessNoise = this.precessionRate * dt;
this.gyroPitch += (Math.random() - 0.5) * precessNoise;
this.gyroRoll += (Math.random() - 0.5) * precessNoise;
const erectionK = this.erectionRate * this.vacuum * dt;
this.gyroPitch += (truePitch - this.gyroPitch) * erectionK;
this.gyroRoll += (trueRoll - this.gyroRoll) * erectionK;
this.displayPitch += (this.gyroPitch - this.displayPitch) * (1 - Math.exp(-dt / 0.1));
this.displayRoll += (this.gyroRoll - this.displayRoll) * (1 - Math.exp(-dt / 0.1));
}
fail() { this.failed = true; }
setVacuum(v) { this.vacuum = Math.max(0, Math.min(1, v)); }
}
Rendering — rotating sky/ground horizon:
function renderAI(ctx, instrument, cx, cy, radius) {
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.clip();
ctx.translate(cx, cy);
ctx.rotate(-instrument.displayRoll);
const pitchPx = instrument.displayPitch * (radius / (30 * Math.PI / 180));
ctx.fillStyle = '#1a6fb5';
ctx.fillRect(-radius, -radius * 2 + pitchPx, radius * 2, radius * 2);
ctx.fillStyle = '#8B5e3c';
ctx.fillRect(-radius, pitchPx, radius * 2, radius * 2);
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-radius, pitchPx);
ctx.lineTo(radius, pitchPx);
ctx.stroke();
ctx.restore();
drawAircraftSymbol(ctx, cx, cy);
}
Heading Indicator (HI / Directional Gyro)
Physics: horizontal gyro. Precesses up to 3°/15 min — must be reset to compass.
class HeadingIndicator {
constructor() {
this.displayHeading = 0;
this.gyroHeading = 0;
this.precessionRate = (3 * Math.PI / 180) / 900;
this.vacuum = 1.0;
}
update(trueHeading, dt) {
this.gyroHeading += this.precessionRate * dt * (this.vacuum > 0.5 ? 1 : 3);
this.displayHeading = this.gyroHeading;
}
syncToCompass(magneticHeading) {
this.gyroHeading = magneticHeading;
}
}
Turn Coordinator
Physics: electric gyro, senses roll rate and yaw rate. Separate electrical bus.
class TurnCoordinator {
constructor() {
this.bankAngle = 0;
this.ballOffset = 0;
this.lag = 0.3;
this.powered = true;
}
update(rollRate, yawRate, lateralAccel, dt) {
if (!this.powered) { this.bankAngle = 0; return; }
const sensitivityRoll = 20 / (3 * Math.PI / 180);
const target = Math.max(-30, Math.min(30,
rollRate * sensitivityRoll + yawRate * 5
));
this.bankAngle += (target - this.bankAngle) * (1 - Math.exp(-dt / this.lag));
const ballTarget = -lateralAccel * 40;
this.ballOffset += (ballTarget - this.ballOffset) * (1 - Math.exp(-dt / 0.8));
this.ballOffset = Math.max(-20, Math.min(20, this.ballOffset));
}
}
§3 — Navigation Instruments
VOR / CDI (Course Deviation Indicator)
class VORReceiver {
constructor() {
this.obsSetting = 0;
this.radialFrom = null;
this.cdiDeflection = 0;
this.toFrom = null;
this.signalValid = false;
this.flagShown = true;
}
update(aircraftPos, stations) {
const station = this.findStation(aircraftPos, stations);
if (!station || station.distanceNm > 130) {
this.flagShown = true; this.signalValid = false; return;
}
this.signalValid = true;
this.flagShown = false;
this.radialFrom = bearing(station.pos, aircraftPos);
const courseError = normalizeAngle(this.obsSetting - this.radialFrom);
const inboundError = normalizeAngle(courseError);
this.toFrom = Math.abs(inboundError) < 90 ? 'TO' : 'FROM';
const deviation = normalizeAngle(this.radialFrom - this.obsSetting);
this.cdiDeflection = Math.max(-1, Math.min(1, deviation / 10));
}
}
ILS (Localizer + Glideslope)
class ILSReceiver {
constructor() {
this.locDeflection = 0;
this.gsDeflection = 0;
this.locValid = false;
this.gsValid = false;
}
update(aircraftPos, runwayThreshold, runwayCourse, gsAngleDeg) {
const bearingToThreshold = bearing(aircraftPos, runwayThreshold);
const distNm = distance(aircraftPos, runwayThreshold);
const locErrorDeg = normalizeAngle(bearingToThreshold - runwayCourse);
this.locDeflection = Math.max(-1, Math.min(1, locErrorDeg / 2.5));
this.locValid = distNm < 18;
const altAgl = aircraftPos.altMsl - runwayThreshold.elevFt;
const actualGsAngle = Math.atan2(altAgl, distNm * 6076) * 180 / Math.PI;
const gsErrorDeg = actualGsAngle - gsAngleDeg;
this.gsDeflection = Math.max(-1, Math.min(1, gsErrorDeg / 0.7));
this.gsValid = distNm < 10 && altAgl < 5000;
}
}
§4 — Engine Instruments
class EngineInstruments {
constructor(config) {
this.rpm = 0;
this.mp = 29.92;
this.egt = 0;
this.cht = 0;
this.oilTemp = 0;
this.oilPres = 0;
this.fuelFlow = 0;
this.fuelQty = { left: config.fuelLeft || 25, right: config.fuelRight || 25 };
this.lags = { rpm: 0.5, mp: 0.3, egt: 15, cht: 45, oilTemp: 60, oilPres: 2 };
this.raw = { ...this };
}
update(trueRPM, throttle, mixture, dt) {
this.raw.rpm = trueRPM;
this.raw.egt = 1200 + (throttle * 200) + ((mixture - 0.5) * -400);
this.raw.cht = 300 + (throttle * 150);
this.raw.oilTemp = 180 + (throttle * 40);
this.raw.oilPres = trueRPM > 500 ? 60 - (trueRPM / 100) : 0;
this.raw.fuelFlow = throttle * mixture * 10;
for (const key of ['rpm', 'egt', 'cht', 'oilTemp', 'oilPres']) {
const tau = this.lags[key];
this[key] += (this.raw[key] - this[key]) * (1 - Math.exp(-dt / tau));
}
this.fuelQty.left -= (this.fuelFlow / 2) * (dt / 3600);
this.fuelQty.right -= (this.fuelFlow / 2) * (dt / 3600);
}
}
§5 — Failure Injection System
Every instrument should connect to a central failure manager:
const FailureManager = {
failures: new Set(),
inject(systemId) {
this.failures.add(systemId);
this.applyFailure(systemId);
},
clear(systemId) {
this.failures.delete(systemId);
},
applyFailure(id) {
switch (id) {
case 'pitot_ice':
instruments.asi.pitotBlocked = true;
break;
case 'static_blocked':
instruments.asi.staticBlocked = true;
instruments.altimeter.staticBlocked = true;
instruments.vsi.staticBlocked = true;
break;
case 'vacuum_failure':
instruments.ai.setVacuum(0);
instruments.hi.vacuum = 0;
break;
case 'ai_tumble':
instruments.ai.fail();
break;
case 'electrical_failure':
instruments.tc.powered = false;
instruments.ilsReceiver.powered = false;
break;
case 'vor_flag':
instruments.vor.flagShown = true;
instruments.vor.signalValid = false;
break;
}
},
};
§6 — Glass Cockpit / EFIS Display
PFD Layout (HTML + CSS approach)
For a G1000-style PFD, use HTML elements with CSS transforms — easier
than canvas for tape-style displays:
<div class="pfd">
<canvas id="adi-canvas" class="pfd-adi"></canvas>
<div class="airspeed-tape-window">
<div id="airspeed-tape" class="airspeed-tape">
</div>
<div class="airspeed-bug"></div>
</div>
<div class="altitude-tape-window">
<div id="altitude-tape" class="altitude-tape"></div>
</div>
<canvas id="vsi-canvas" class="pfd-vsi"></canvas>
<canvas id="hsi-canvas" class="pfd-hsi"></canvas>
<div id="annunciators" class="pfd-annunciators"></div>
</div>
Tape scrolling via CSS transform:
function updateAirspeedTape(ias) {
const PX_PER_KT = 6;
const offset = ias * PX_PER_KT;
document.getElementById('airspeed-tape').style.transform
= `translateY(${offset}px)`;
}
Annunciator Colors
const ANNUNCIATOR_LEVELS = {
warning: { color: '#ff2200', flash: true },
caution: { color: '#ffcc00', flash: false },
advisory: { color: '#00aaff', flash: false },
normal: { color: '#00cc44', flash: false },
};
§7 — Rendering Utilities
Common drawing helpers used across all instruments:
function drawArc(ctx, cx, cy, r, startDeg, endDeg, color, width) {
ctx.beginPath();
ctx.arc(cx, cy, r, startDeg * Math.PI/180, endDeg * Math.PI/180);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
}
function drawNeedle(ctx, cx, cy, length, angleDeg, color, width = 2) {
const rad = angleDeg * Math.PI / 180;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + length * Math.cos(rad), cy + length * Math.sin(rad));
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
}
function normalizeAngle(deg) {
let a = deg % 360;
if (a > 180) a -= 360;
if (a < -180) a += 360;
return a;
}
function lagFilter(current, target, tau, dt) {
return current + (target - current) * (1 - Math.exp(-dt / tau));
}
Output Notes
When producing instrument simulations:
- Default output: single HTML file with all instruments, demo flight state,
and controls to manually input values
- For integration into an existing sim: export as a JS module with a clean
update(state, dt) / render(ctx) interface per instrument
- Always include a failure injection panel in demo outputs — it's the most
educational feature
- Label every instrument with its name, and show the raw vs. indicated value
during development (toggle-able in production)