Technical reference

APU (737-800, GTCP131-9B): specification for the rewrite

docs/specs/apu.md

Status: M2 draft, 2026-11-01. Subject: Zibo 2.S1.50 (zibomod lin_x64 + XLua scripts) as run by the oracle (xphost/out/linux/lu737host). Everything here is described in our own words from reading the decompiled zibomod, the Lua and oracle runs; no decompiled code is reproduced.

Notation as in electrics.md: Z: = Zibo dataref (laminar/B738/...), X: = X-Plane dataref/command (sim/...), SCI: = stable cockpit interface state/control (data/sci/contract). Rescale(x1→y1, x2→y2, v) is Zibo's clamped linear interpolation (outside [x1,x2] the end value), Limit(v, lo, hi) a clamp, and the three animation helpers are defined in §5.1. dt = sim_period (frame time).

Key findings

  1. Zibo does not spool the APU. It runs an APU controller (start sequencing, EGT, LOW OIL / FAULT / MAINT / OVERSPEED logic, generator and bleed availability, door, cool-down, fuel burn) around X-Plane's APU, which it only commands (X:electrical/APU_start held 1 s, then APU_on; APU_off) and reads (X:cockpit2/electrical/APU_N1_percent, X:cockpit/engine/APU_running).
  2. The "START only works after ~5–6 s at ON" observation (SCI_IMPLEMENTATION §3) is a start delay, not a rejection. On START release Zibo waits T_start before calling APU_start; T_start is 17.5 s at TAT 15 °C / sea level, but is divided by 8 (2.2 s) when the APU LOW OIL PRESSURE light is already lit — and that light comes on 5.5 s after the switch reaches ON. A START given after 4 s at ON therefore calls APU_start ~17.5–21 s later, outside the 15 s window that was used for the measurement. Oracle: xphost/test/sci/apu_spec_delay_fault.json. The SCI profile's 7 s gate on START is not needed in the rewrite.
  3. Zibo refuses to start when X-Plane's APU N1 is already ≥ 7 % (this is the "MSFS APU N1 ~20 % → Zibo never starts" effect of the old host). The rewrite must own the APU N1 (§7).
  4. APU generator availability (Z:electrical/apu_bus_enable, → SCI:SRC_APU_GEN_RDY) comes 2 s after the APU has settled: EGT (Zibo's own) between 46 and 52·load, X-Plane running, N1 > 94 % — about 25 s after N1 reaches 100 % with the emulated spool of §8.

1 Scope and sources#

In scope: APU switch OFF/ON/START, start availability and sequence, LOW OIL PRESSURE / FAULT / MAINT / OVERSPEED lights and the APU six-pack inputs, Zibo's APU EGT and the overhead EGT gauge, APU door, cool-down and shutdown, fuel availability and fuel burn, APU bleed supply pressure and APU bleed valve, APU generator availability (apu_bus_enable), APU failures and fire hooks, and the X-Plane APU the rewrite must replace. Out of scope (other specs): switching the APU generator onto the buses, APU GEN OFF BUS light, AC meter (electrics.md §4.2, §4.4, §4.5, §4.6, §4.7); duct pressures, packs, isolation valve, apu_leak (pneumatics spec); engine start air (engine start spec); fire detection/bottles in general and the master-caution latch mechanism (fire / annunciator specs; the APU-specific parts are summarised here).

Source Function / script APU content
zibomod Systems (block after the fuel-transfer code) start availability, switch-state machine, start delay, EGT, enable timers, failures, fuel-pump status, abort, shutdown, door, EGT gauge, light memories
Systems (early, after Electric_bus) apu_establish_time, APU bleed supply pressure, APU bleed demand (apu_mode), EGT load factor, APU fuel flow, APU bleed valve target/position
Annun_timers (start of Annunciators) count-down timers: start delay → APU_start, start-stop (1 s) → APU_on, LOW OIL (5.5 s), normal (8.5 s), cool-down, 5-min; fuel burn
Annunciators lights apu_low_oil, apu_fault, maint_apu, over_speed_apu; APU six-pack inputs; APU fire light
CMD_apu_starter_switch_up/dn, CMD_bleed_air_apu switch handlers
Fire, manage_eng_failure fire handle, extinguishing, X-Plane failure hooks
Pressurization X:cockpit2/bleedair/actuators/apu_bleed
Lighting adds apu_start_load to both AC bus loads (electrics)
Systems_start_flight, Systems_load/_save, MainFlightLoopCallback (panel-state string) initial values, situation restore
Lua B738.zz.sim_cockpit Z:switches/apu_start (0/1/2) convenience dataref → switch commands
B738.tablet EFB failure pages: Z:failure/engine[18..22] (APU), failure/electric[19], failure/pressurisation[6, 15]
B738.a_AXP_4_FMOD_logic sound only (APU bleed when N1 > 80 %): not in phase 1
acf 737_80NG.acf _APU_spoolup_time 53, _APU_spooldn_time 18, _APU_cooldown_time 7, _APU_door_time 9, _APU_ff_kgh 0, _APU_fuel_src 0, _has_APU_bleed_air/_generator/_switch 1, _red_hi_N1_apu 105
oracle xphost/test/sci/apu_spec_start.json, apu_spec_delay_fault.json, apu_trace.json (frame trace) pass (27 + 12 checks); the rewrite passes the same scripts and matches the trace frame by frame (wasm/test/sci_script_runner.cpp)

Frame order (whole plugin, see electrics.md §4.1): command handlers → reads → engines/starter → Annunciators (Annun_timers first, then lights from the previous frame's APU indicator values) → Fire → Switches → Lighting → Systems (Electric_bus, then the APU parts in the order of §4) → Pressurization → manage_failure → manage_eng_failure.

2 Inputs#

2.1 Cockpit controls#

SCI control Zibo command (phase) Effect
APU_SW INC (to ON / START), APU_SW RELEASE from START Z:spring_toggle_switch/APU_start_pos_dn begin; end §4.1
APU_SW DEC …/APU_start_pos_up begin ON → OFF only
APU_BLEED_AIR PRESS Z:toggle_switch/bleed_air_apu bleed_air_apu_pos 0 ↔ 1
ELEC_APUGEN1/2 Z:toggle_switch/apu_gen1/2_dn/up electrics.md §4.2 (needs apu_bus_enable)
APU_FIRE_HANDLE_ARM PRESS, FIRE_HANDLE_DISCHARGE_3 INC/DEC Z:fire/apu/ext_switch_arm, …/ext_switch_L/_R fire handle pulled (Z:fire/apu/ext_switch/pos_arm, animated 0..1 at rate 10) and turned (pos_disch −1..1)
(hardware) Z:switches/apu_start written 0/1/2 Lua relays a change into the switch commands: from OFF: 1 → dn once, 2 → dn once + dn held; from ON: 0 → up once, 2 → dn held; from START: 0 → release dn + up once, 1 → release dn

2.2 Simulator and other-system inputs#

Input Use
X:cockpit2/electrical/APU_N1_percent (N1) everything (§7: rewrite provides it)
X:cockpit/engine/APU_running (RUN, 0/1) everything (§7)
X:cockpit2/electrical/APU_generator_amps APU GEN OFF BUS (electrics)
Z:electric/batbus_status APU controller power; start condition; abort when 0
Z:electric/dc_bus2_status keeps apu_bus_enable when the battery bus is off
X:weather/aircraft/temperature_leadingedge_deg_c (XP12; XP11: X:weather/temperature_le_c) = TAT start delay, EGT targets, establish time
X:flightmodel/position/elevation (m, elev_m) fuel availability (7620 m), start delay altitude factor (×3.281 → ft), fuel flow (×3.28)
X:weather/aircraft/barometer_current_pas (XP12, Pa; as inHg p = Pa/3386.39) → alt_p = (1 − (p/29.92)^0.190284)·145366.45 ft APU bleed pressure. Correction (M3, oracle apu_trace.json): Zibo 2.S1.50 on XP12 reads the Pa dataref; with only barometer_current_inhg set, apu_psi_raw stays 0 and the bleed valve never opens
X:flightmodel/weight/m_fuel[0..2] (L, C, R kg), X:cockpit2/fuel/fuel_tank_pump_on[0..2] fuel availability and burn
Z:annunciator/low_fuel_press_l1/l2/c1/c2/r1/r2, Z:knob/cross_feed… (cross_feed_selector_knob), tank_c1_status APU fuel-pump status (fuel spec computes them)
Z:…/l_pack_pos, r_pack_pos (0 OFF / 1 AUTO / 2 HIGH), engine starter valves, isolation valve, start_valve1/2, apu_leak APU bleed demand and pressure (pneumatics/engine-start specs)
ac_power_bus[k] (3 = APU), X:cockpit2/electrical/bus_load_amps[k] EGT load factor (electrics)
Z:failure/engine[18..22], failure/electric[19], failure/pressurisation[6], [15] §4.8
X:operation/failures/rel_apu_fire, rel_apu, rel_APU_press fire state, APU failed (read back)
X:operation/prefs/startup_running initial switch positions (§3.2)

3 State#

3.1 Variables#

Name (Zibo global) Type / values Meaning
sw = Z:spring_toggle_switch/APU_start_pos 0 OFF, 1 ON, 2 START switch
sw_old 0/1/2 switch value at the end of the previous APU block (also set to 1 by the START-release handler)
start_active 0 idle, 1 start delay, 2 X-Plane starting, 3 running/established start state machine
shutdown_active 0, 1 cool-down, 2 send APU_off shutdown state machine
cool_idle 0/1 cool-down in progress
start_avail, fuel_pump_status, fuel_tank_status 0/1 fuel/start availability (tank = previous frame's pump)
T_start, T_start_stop, T_low_oil, T_normal, T_cool, T_5min, T_temp_init s, count down to 0 timers (§5.3)
start_meter s, counts up time since START without reaching "running"
bus_enable_tmr, duct_enable_tmr s 2 s delays
Z:electrical/apu_bus_enable 0/1 APU generator available
duct_enable 0/1 APU bleed allowed
egt (apu_temp) 0..100 (≈ °C/10) Zibo's APU EGT
egt_tgt (apu_temp_target) persisted target (used by one branch, §4.6)
Z:electrical/apu_temp (gauge) 0..100 EGT needle (OBJ: 0..100 → 0..168°)
temp_init 0..3 EGT-gauge power-up sweep state
door_tgt, Z:electrical/apu_door 0/1, 0..1 APU inlet door
run_old 0/1 RUN of the previous frame
hot_start 0/1 only non-zero after a situation restore (§3.2)
establish (apu_establish_time) 0..1 APU thermally established
fault/maint/overspeed/low_oil × {raw, ind, tmr, tmr2} light logic (§4.7)
apu_mode, T_idle, both_packs_old, air_load bleed demand (§4.9)
load_egt, apu_ff EGT load factor, fuel flow kg/s
max_psi (rndm_max_apu_bleed_psi) uniform [45, 46] drawn at flight start
Z:air/apu/bleed_valve_pos, Z:air/apu_psi 0..1, psi bleed valve, APU bleed pressure
T_fuel s fuel-burn accumulator

3.2 Initial values#

  • Flight start (Systems_start_flight): sw = 0, bleed_air_apu_pos = 0, APU GEN switches 0, X-Plane APU generator off, apu_power_bus1/2 = 0 (both for cold-and-dark and engines-running starts); start_active, door_tgt, start_avail, fuel_pump_status, establish, cool_idle, hot_start, apu_mode, T_fuel, all light ind = 0; X:rel_APU_press := 0; max_psi drawn; .data initial values fuel_tank_status = 1, T_temp_init = 1.5. On the very first frame egt = max(0, TAT/10).
  • Situation restore (Systems_load): start_active, cool_idle, hot_start, T_cool, T_start, T_normal, establish, switch positions and X:APU_running are restored (Zibo writes RUN!). The rewrite restores the same fields; with hot_start = 1 the EGT window check of §4.5 is skipped.
  • Panel-state string (Zibo's saved switch positions): APU character '0' → sw 0; '1' → sw 1, X-Plane APU_on, T_low_oil = 5.5; anything else → sw 2.

4 Per-frame logic in execution order#

4.1 Switch handlers (between frames)#

  • APU_start_pos_up (begin): if sw = 1 → 0. (No effect from START.)
  • APU_start_pos_dn begin: 0 → 1, 1 → 2.
  • APU_start_pos_dn end (release from START): if sw = 2: sw := 1, then if batbus ∧ shutdown_active = 0 ∧ start_active = 0 ∧ RUN = 0: { if start_avail = 1: start_active := 1; if T_low_oil = 0 ∧ low_oil_raw = 0: T_low_oil := 5.5; sw_old := 1 }. (Holding START has no other effect; the release is what starts the APU.)
  • bleed_air_apu: toggles bleed_air_apu_pos.

4.2 Annun_timers (start of Annunciators)#

Each timer: if ≠ 0, subtract dt, floor at 0, and act when it reaches 0 in this frame:

  • T_cool → cool_idle := 0.
  • T_start_stop → X-Plane APU_start end, then APU_on once.
  • T_start → if start_avail = 1: X-Plane APU_start begin, start_active := 2, shutdown_active := 0, T_start_stop := 1; else start_active := 0, T_start_stop := 0.
  • T_low_oil → if N1 < 40: low_oil_raw := 1.
  • T_normal → start_active := 3.
  • T_5min → (only counts down).
  • Fuel burn (§4.12).

4.3 Early part of Systems (after Electric_bus)#

  1. Established time: if N1 > 90: target 1 over Rescale(−30→180 s, 40→60 s, Limit(TAT,−30,40)) (102.9 s at 15 °C), else target 0 over 500 s; establish moves linearly (set_animation_rate, §5.1).
  2. APU bleed supply pressure: base = max(0, max_psi·sin(Rescale(10000→90°, 80000→180°, alt_p)) − apu_leak); if (start_valve1 > 0.1 ∨ start_valve2 > 0.1) ∧ establish < 1: base −= (1 − establish)·25, floor 0; apu_psi_raw (bleedAir_apu_psi) = Rescale(0→0, 100→base, N1).
  3. Demand and output pressure (§4.9), EGT load factor load_egt (§4.10), fuel flow apu_ff (§4.12).
  4. APU bleed valve (§4.9).

4.4 Start availability, light-memory gates (main APU block of Systems)#

  1. If sw = 0 ∨ batbus = 0: gates G = false; if batbus = 0 all four ind and tmr := 0; for each light: tmr := 0 if ind = 0, else count down. Otherwise: G = true and all four tmr := 300.
  2. T_amb = max(0, TAT/10).
  3. start_avail := 1 iff fire handle pos_arm = 0 ∧ batbus = 1 ∧ failure/pressurisation[15] ≠ 1 ∧ N1 < 7 ∧ ( (elev_m < 7620 ∧ L tank > 0) ∨ (fuel_pump_status = 1 ∧ fuel_tank_status = 1) ).
  4. If batbus = 0: all raw, ind := 0; fault/overspeed/low-oil tmr2 := 0.

4.5 Switch-state machine (first matching branch)#

  • sw = 0: if sw_old ≠ 0 ∧ low_oil_ind ∧ no oil failure (engine[21], [22] ≠ 1): low_oil_ind := 0 (LOW OIL goes out at once). All raw := ind; tmr2 := 0. If RUN = 1: if shutdown_active = 0: shutdown_active := 1 and, if gauge ≤ 25: cool_idle := 0, T_cool := 0, else cool_idle := 1 and T_cool := 58 if it was 0. If RUN = 0: shutdown_active, cool_idle, T_cool := 0. Then apu_bus_enable, both enable timers, duct_enable, T_low_oil, hot_start, start_active, T_start, T_start_stop := 0.
  • sw = 1 ∧ sw_old = 2 (START released, seen in Systems): if batbus ∧ shutdown_active = 0 ∧ start_active = 0 ∧ RUN = 0: start_active := 1 if start_avail; T_low_oil := 5.5 if it is 0 and low_oil_raw = 0. (Duplicate of the handler path; normally the handler already did it.)
  • sw = 1 ∧ RUN ∧ shutdown_active ≠ 0 (ON again during cool-down): start_active := 3, shutdown_active, cool_idle, T_cool, hot_start := 0.
  • sw = 1 ∧ start_active = 0 ∧ batbus ∧ shutdown_active = 0 ∧ RUN = 0 ∧ T_low_oil = 0 ∧ low_oil_raw = 0: T_low_oil := 5.5 (this is what lights LOW OIL 5.5 s after selecting ON).

Then sw_old := sw; if start_active < 3: apu_bus_enable, both enable timers, duct_enable := 0.

4.6 Start phases and EGT while starting/running#

  • start_active = 1 ∧ start_avail: if T_start = 0: T_start = Rescale(0→28, 60→14, Limit(TAT+30, 0, 60)) · Rescale(0→1, 28000→1.3, Limit(elev_m·3.281, 0, 28000)); if low_oil_raw = 0: T_low_oil := 5.5, else T_start /= 8. Always: shutdown_active, cool_idle, T_cool, hot_start := 0. (egt is not updated in this state.)
  • = 2: egt_tgt = max(egt, N1); egt := AnimValue(egt → egt_tgt, 0..100, rate 8); if egt > 95 ∧ T_normal = 0: T_normal := 8.5. T_cool, cool_idle, hot_start := 0. LOW OIL clear rule (below).
  • = 3 ∧ RUN: egt_tgt = Rescale(0→47, 4→51, Limit(T_amb, 0, 4)); if egt_tgt ≤ egt ∨ egt ≥ 46: egt := AnimValue(→ load_egt·egt_tgt, rate 0.2) else AnimValue(→ egt_tgt, rate 0.5). If hot_start = 0: when 46 < egt < 52·load_egt ∧ RUN = 1 ∧ N1 > 94: start bus_enable_tmr = 2 (if apu_bus_enable = 0 and the timer is 0) and duct_enable_tmr = 2 (if duct_enable = 0 and timer 0). If hot_start = 1: start both timers unconditionally. LOW OIL clear rule; T_cool, cool_idle := 0.
  • LOW OIL clear rule (states 2 and 3): egt > 40 ∧ RUN = 1 ∧ N1 > 40 → low_oil_raw := 0.
  • If batbus = 0 ∧ dc_bus2 = 0: apu_bus_enable := 0, timer 0. bus_enable_tmr counts down; at ≤ 0: apu_bus_enable := 1.

4.7 Failures, start load, fuel status, FAULT#

  1. MAINT raw := 1 if (apu_bus_enable = 1 ∧ failure/electric[19] = 1) or (establish = 1 ∧ failure/pressurisation[15] = 1). (MAINT raw is otherwise only cleared through raw := ind with the switch OFF / battery off.) If pressurisation[15] = 1: X:rel_APU_press := 6.
  2. OVERSPEED (engine[20] = 1): if RUN = 1 ∧ N1 > 99: overspeed_raw := 1, tmr2 := 30, abort; else if RUN = 0 ∧ tmr2 = 0: overspeed_raw := 0.
  3. LOW OIL PRESSURE failure (engine[22] = 1): if (RUN = 0 ∧ start_active = 0 ∧ shutdown_active = 0) ∨ T_low_oil ≠ 0: low_oil_raw := 0 when its tmr2 = 0; else low_oil_raw := 1 and abort if RUN ∧ N1 > 99.
  4. LOW OIL QUANTITY failure (engine[21] = 1) ∧ sw ≠ 0: low_oil_raw := 1; abort if RUN ∧ N1 > 99.
  5. Z:electric/apu_start_load = 60 while T_start ≠ 0, else 0 (A, added to both AC bus loads).
  6. fuel_pump_status := 1 if any of: (L1 or L2 low-pressure light out) ∧ L > 0; C1 light out ∧ C > 100 kg (crossfeed closed) or (C1 or C2 out) ∧ C > 100 (crossfeed open); (crossfeed open only) (R1 or R2 out) ∧ R > 0; elev_m < 7620 ∧ L > 0; RUN = 1 ∧ L > 0. If 0 → abort.
  7. Start meter: if start_active = 0: 0; else if start_active < 3 ∨ RUN = 0: if ≤ 120 add dt, else { fault_raw := 1, fault_tmr2 := 30 unless (start_active = 3 ∧ run_old = 1); abort }; else 0.
  8. If start_active = 3 ∧ run_old = 1 ∧ RUN = 0 (APU stopped by itself): fault_raw := 1, tmr2 := 30; else if start_active = 0 ∧ shutdown_active = 0 ∧ fault_tmr2 = 0: fault_raw := 0.
  9. run_old := RUN; tmr2 of fault/low-oil/overspeed count down; fuel_tank_status := fuel_pump_status.

4.8 Abort, shutdown, door, enables#

  1. Abort if any "abort" above, or fire handle pos_arm = 1, or batbus = 0: shutdown_active, cool_idle, T_cool, start_active, hot_start, T_start, T_start_stop, T_low_oil := 0; low_oil_raw := 0 if start_meter > 120 or batbus = 0; apu_bus_enable, its timer, door_tgt := 0; if RUN = 1: X-Plane APU_off.
  2. Shutdown: if shutdown_active = 1 ∧ cool_idle = 0 → 2; else if = 2 → 0, X-Plane APU_off, hot_start := 0. (So APU_off goes out one frame after the cool-down ends, or two frames after OFF when the gauge EGT was ≤ 25.)
  3. door_tgt := 0 if RUN = 0 ∧ start_active = 0, else 1.
  4. If shutdown_active ≠ 0: start_active, hot_start, T_start, T_start_stop, T_low_oil, low_oil_raw, apu_bus_enable, its timer := 0. If start_active ≠ 0: shutdown_active, cool_idle, T_cool := 0.
  5. If RUN = 0 ∨ shutdown_active ≠ 0: duct_enable, its timer := 0; else duct_enable_tmr counts down, at ≤ 0 duct_enable := 1.
  6. If sw ≠ 0: T_5min := 300.

Note: switching OFF while start_active = 2 and X-Plane not yet running resets start_active but sends no APU_off; X-Plane keeps spooling (it got APU_on) until RUN = 1, when the OFF branch starts a normal cool-down. Reproduce as is.

4.9 APU bleed (demand, pressure, valve, X-Plane actuator)#

starter = max(engine-1 starter valve, isolation valve · engine-2 starter valve). Pack combination: both OFF → code 0, both AUTO → 1, else 2. Mode (first match): starter > 0 ∨ APU bleed switch OFF ∨ a pack HIGH → mode 0 (factor 1, T_idle := 0); both AUTO → mode 1; both OFF → mode 2 (T_idle := 0 when the pack code changed); otherwise mode 3 (factor 1, T_idle := 0). In modes 1/2 T_idle += dt up to 15 s and, if apu_psi_raw − apu_leak ≥ 0.1 (else factor 1):

  • mode 1: factor 1 for T_idle < 2.5 s, then (21 + apu_leak)/apu_psi_raw;
  • mode 2: (38 + leak)/raw for < 2 s, (33 + leak)/raw for < 9.5 s, then (21 + leak)/raw.

air_load moves linearly toward the factor (0..1) at 0.2/s up, 0.06/s down. Z:air/apu_psi = min(41, max(0, air_load·apu_psi_raw)) (the pneumatics spec uses it times the valve).

Valve target 1 iff APU bleed switch ON ∧ apu_psi_raw > 8 (bleed_valve_min_act_press) and none of: pressurisation[15] = 1, fire handle pos_arm = 1, sw = 0, duct_enable = 0. Position: AnimValue(→ target, 0..1, rate 10) unless failure/pressurisation[6] = 1 (valve frozen). In Pressurization (after Systems): X:cockpit2/bleedair/actuators/apu_bleed = (valve > 0.05 ∧ switch ON). Downstream users: duct pressures/packs/dual bleed (pneumatics), engine start air uses valve · establish (engine start spec).

4.10 EGT load factor#

load_bleed = Rescale(16→1, max_psi→1.14, v) if v = valve·apu_psi ≥ 16, else 1; load_elec = 1 + Σ over transfer buses on APU (ac_power_bus[k] = 3) of Rescale(0→0, 100→0.07, Limit(bus_load_amps[k], 0, 100)); load_egt = max(load_bleed, load_elec).

4.11 EGT when not starting, gauge, light outputs#

egt when start_active = 0 (AV(rate) = AnimValue to the stated target, 0..100):

  • sw = 0: shutdown_active = 0 → toward the persisted egt_tgt, rate 0.008 if egt ≤ 21 else 0.15; = 1 → egt_tgt := Rescale(0→47, 4→51, Limit(T_amb,0,4)), rate 0.5; = 2 → egt_tgt := T_amb, rates 0.008/0.15.
  • sw ≠ 0: shutdown_active = 0 → if temp_init = 0 ∧ T_temp_init = 0: temp_init := 1; egt_tgt := T_amb, rates 0.008/0.15; = 1 and = 2 as above.

T_temp_init: reset to 1.5 (and temp_init := 0) when batbus = 0 or sw = 0, else counts down; batbus = 0 also zeroes T_5min. Door: apu_door moves linearly toward door_tgt, full travel 6 s. Gauge (first match): batbus = 0 ∨ T_5min = 0 → AV(0, 8); start_active > 1 ∨ shutdown_active ≠ 0 ∨ sw = 0 → AV(egt, 8); temp_init 0 → AV(egt, 3); 1 → AV(egt + 20, 8), → 2 once within 5 of it; 2 → AV(0, 7), → 3 below 1.5; 3 → AV(egt, 3). (Power-up sweep of the needle ~1.5 s after ON.)

Light memories: for each of FAULT, MAINT, OVERSPEED, LOW OIL: if G: ind := raw; else if tmr = 0: ind := 0 (so FAULT/MAINT/OVERSPEED stay lit up to 300 s after OFF; LOW OIL is cleared on OFF, §4.5).

Annunciators (next frame), with B = battery bus · generic_lights_brightness_ratio[14] · day factor (electrics.md §4.7; lights test overrides as there): Z:annunciator/apu_low_oil = low_oil_ind·B, apu_fault = fault_ind·B, maint_apu = maint_ind·B, over_speed_apu = overspeed_ind·B. APU six-pack inputs (rising edge latches six_pack_apu, master-caution reset clears; mechanism in the annunciator spec): FAULT ind, MAINT ind, OVERSPEED ind, LOW OIL ind ∧ engine[22], LOW OIL ind ∧ engine[21] (a LOW OIL light without an oil failure does not trigger it). apu_gen_off_bus: electrics.md §4.7.

4.12 Fuel flow and burn#

ff0 = Rescale(0→225, 35000→105, elev_m·3.28) lb/h · 0.45359 / 3600 (kg/s); apu_ff = Rescale(1→ff0, 1.14→1.15·ff0, load_egt). T_fuel += dt; when > 1 s, if RUN = 1 burn b = T_fuel·apu_ff, then T_fuel := 0:

  • no tank pump on (fuel_tank_pump_on[0]+[1]+[2] = 0): L −= b;
  • crossfeed closed: if tank_c1_status = 1 ∧ C pump on ∧ C > 100: take from C down to 100 kg, the rest from L if the L pump is on; else L −= b if the L pump is on;
  • crossfeed open: if C pump on ∧ C > 100: take from C down to 100, the excess as below; else if L pump on ∧ tank_c1_status = 1: as below with b; else L −= b if L pump on and R −= b if R pump on. "As below" = take from the heavier wing tank up to the L/R difference, the remainder half from each.
  • floor L, R at 0; write X:m_fuel[0..2]. (X-Plane's own APU burns nothing: _APU_ff_kgh = 0.)

4.13 Fire and X-Plane failure hooks#

  • manage_eng_failure: engine[18] = 1 → X:rel_apu_fire := 6; engine[19] = 1 → X:rel_apu := 6 (X-Plane then fails the APU: no start / it stops → FAULT via §4.7 7/8).
  • Fire: handle discharged (pos_disch < −0.95 or > 0.95) → fire/apu/fire_ext := 1, was_fire := 1, X:rel_apu := 6 (APU unusable for the rest of the flight unless failures are reset); circuit L/R flag; bottle fire/apu/ext_bottle/psi falls 100 psi/s while the amount discharged accumulates; when rel_apu_fire = 6 and the discharged amount > random(400..1000) (drawn when the fire starts) the fire is cleared; burning 600 s → cockpit smoke.
  • Lights: apu_fire = fire (or fire test)·B_fire, apu_bottle_discharge = bottle empty, extinguisher circuit test, fire_apu_det_inop on FAULT test (fire spec).
  • Handle pulled (pos_arm = 1): no start, abort (APU_off), bleed valve closed.

5 Constants, tables, timings#

5.1 Helpers#

  • AnimValue(cur → tgt, lo..hi, k): c = clamp(cur); if tgt ≥ hi − 0.001 ∧ c ≥ hi − 0.01 → hi; if tgt ≤ lo + 0.001 ∧ c ≤ lo + 0.01 → lo; else c + (tgt − c)·k·dt without overshoot, clamped (first order, time constant 1/k).
  • AnimLinear(cur → tgt, lo..hi, r) (Anim_linear_value2): same end snaps, moves r·dt toward tgt.
  • set_animation_rate(cur → tgt, lo..hi, T): same snaps, moves dt/T toward tgt (T ≤ 0 → 1).

5.2 Thresholds#

Value Use
N1 < 7 % start available
7620 m below: gravity/DC feed from L tank enough to start
N1 < 40 % at T_low_oil expiry LOW OIL raw
egt > 40 ∧ N1 > 40 ∧ RUN LOW OIL clears
egt > 95 starts T_normal
46 < egt < 52·load_egt, N1 > 94, RUN generator/duct enable timers
EGT target 47..51 (TAT 0..40 °C) running EGT (×load_egt ≤ 1.14)
gauge ≤ 25 at OFF no cool-down
N1 > 99 overspeed / oil failures cause shutdown
N1 > 90 establish ramps up
apu_psi_raw > 8 psi bleed valve may open; output clamp 41 psi; demand 38/33/21 psi
max_psi U[45, 46]
fuel C > 100 kg centre tank usable for APU

5.3 Timers#

Timer Value
T_low_oil 5.5 s
T_start Rescale(0→28, 60→14, TAT+30) × Rescale(0→1, 28000→1.3, alt ft); ÷ 8 if LOW OIL raw set. TAT 15 °C, SL: 17.5 s / 2.19 s; TAT 0: 21 s / 2.63 s
APU_start held 1 s (T_start_stop), then APU_on
T_normal 8.5 s after egt > 95
generator / duct enable 2 s
start meter 120 s → FAULT; tmr2 30 s
T_cool 58 s
light memory tmr, T_5min 300 s
T_temp_init 1.5 s
door 6 s full travel
establish 180 s (−30 °C) … 60 s (40 °C), 500 s back
fuel burn applied each > 1 s; 225 lb/h SL … 105 lb/h at 35 000 ft, +15 % at full load

5.4 Failure items#

Item Effect
engine[18] APU FIRE X:rel_apu_fire = 6 → fire
engine[19] APU STARTING/RUNNING X:rel_apu = 6 → X-Plane APU dead → FAULT
engine[20] OVERSPEED OVERSPEED light + shutdown at N1 > 99
engine[21] LOW OIL QUANTITY LOW OIL whenever switch ≠ OFF; shutdown at N1 > 99
engine[22] LOW OIL PRESSURE LOW OIL while starting/running; shutdown at N1 > 99
electric[19] APU GENERATOR MAINT when available; no APU GEN (electrics)
pressurisation[6] APU bleed valve valve frozen
pressurisation[15] APU pressurization no start, valve closed, MAINT when established, X:rel_APU_press = 6

6 Outputs#

6.1 Zibo datarefs#

spring_toggle_switch/APU_start_pos, toggle_switch/bleed_air_apu_pos, electrical/apu_temp (gauge), electrical/apu_door, electrical/apu_bus_enable, electric/apu_start_load, air/apu_psi, air/apu/bleed_valve_pos, annunciator/apu_low_oil, apu_fault, maint_apu, over_speed_apu, six_pack_apu, apu_fire, apu_bottle_discharge, extinguisher_circuit_annun_apu, fire_apu_det_inop, fire/apu/fire, fire_ext, was_fire, ext_bottle/psi, ext_switch/pos_arm, pos_disch, light/spill/ratio/apu_avail_lights_spill[3] (= apu_gen_off_bus), …extinguisher_*_spill_apu[3]. X-Plane writes: commands APU_start (begin/end), APU_on, APU_off; rel_apu, rel_apu_fire, rel_APU_press; cockpit2/bleedair/actuators/apu_bleed; m_fuel[0..2]; cockpit/engine/APU_running (restore only).

6.2 SCI states#

SCI state Source
APU_SW_POS (OFF, ON, START) APU_start_pos
SRC_APU_RUNNING profile: N1 > 95 (keep; it is an SCI convenience, Zibo has no such flag)
SRC_APU_GEN_RDY apu_bus_enable
ANN_APU_LOW_OIL, ANN_APU_FAULT, ANN_APU_MAINT, ANN_APU_OVERSPEED, ANN_SIXPACK_APU, ANN_APU_GEN_OFF_BUS the annunciator datarefs above
ELECTRICAL_APU_TEMP, ELECTRICAL_APU_DOOR apu_temp (gauge), apu_door
TOGGLE_SWITCH_BLEED_AIR_APU_POS bleed_air_apu_pos
FIRE_APU_EXT_SWITCH_POS_ARM/_DISCH, ANNUNCIATOR_APU_FIRE, …_APU_BOTTLE_DISCHARGE, …_EXTINGUISHER_CIRCUIT_ANNUN_APU fire datarefs

Profile note: the APU_SW gate (T_APU_SW_POS >= 7) and the x_apu_start effect fallback exist only because the old host did not model X-Plane's APU; the rewrite must not delay START or start the APU itself.

7 X-Plane-provided behaviour to supply, with a recommendation#

What X-Plane provides and Zibo consumes: the APU N1 trajectory, the APU_running flag, response to APU_start/APU_on/APU_off and to rel_apu (failed), generator amps (electrics), its own APU bleed and generator (Zibo overrides/does not use them: bleed pressure, EGT, availability and door are Zibo's). X-Plane 12 parameters for this aircraft (acf): spool-up 53 s, spool-down 18 s, cool-down 7 s, door 9 s, fuel flow 0. As understood from X-Plane's model (not verifiable in the oracle): the starter switch is START while APU_start is held and ON after; the inlet door opens first (9 s); N1 rises to 100 % in about the spool-up time; APU_running is set when the APU becomes self-sustaining; after APU_off the APU runs the cool-down time, then spools down in ~18 s, APU_running clears and the door closes. X-Plane needs DC power to start and stops the APU when it fails (rel_apu = 6).

Requirements Zibo puts on this model (parity-relevant): N1 < 7 % until Zibo calls APU_start; RUN = 1 and N1 > 94 % reached within 120 s of the START release (else FAULT); RUN must not drop while running (else FAULT); N1 ≥ 40 % with RUN = 1 to clear LOW OIL; APU_off must stop it (RUN → 0) or Zibo keeps door_tgt = 1.

MSFS candidates: SimVars APU PCT RPM, APU PCT STARTER, APU SWITCH, APU GENERATOR SWITCH, APU GENERATOR ACTIVE, APU VOLTS, APU ON FIRE DETECTED, APU BLEED PRESSURE RECEIVED BY ENGINE; events APU_STARTER, APU_OFF_SWITCH, APU_GENERATOR_SWITCH_TOGGLE/SET, APU_BLEED_AIR_SOURCE_TOGGLE/SET.

Recommendation: model the APU spool in wasm/src/core (a small ApuSpool behind the same interface Zibo's controller uses: start_begin/end, on, off, failed → N1, running, door_xp), and keep the MSFS APU off. Reasons: (1) the MSFS APU's spool rate comes from the package cfg and is unrelated to X-Plane's 53/18/7/9 s; (2) anything else that starts it (a default binding, a state load) pushes N1 ≥ 7 % and Zibo then never starts; (3) MSFS's APU would burn fuel and feed MSFS's electrical/bleed while Zibo already burns APU fuel itself and owns the buses; (4) a core model is deterministic and runs in the native differential tests. Proposed model (to be checked against an X-Plane capture, §9): on APU_start begin: door opens over 9 s; N1 then rises linearly 0→100 in 53 s; running from N1 ≥ 50 % (assumption); on APU_off: hold 7 s, N1 falls linearly 100→0 in 18 s, running := 0 below the same threshold, door closes over 9 s; failed → as APU_off without cool-down and no restart. MSFS APU events may later be mirrored only for exterior effects/sound (phase 2).

For the differential tests, both sides receive the same injected N1/RUN sequence (as the oracle scripts do), so the Zibo controller is compared exactly and the spool model is tested separately.

8 Differential test scenarios#

Harness: dt = 1/30 s; inputs as in the oracle scripts (ground, L/R 3000 kg, barometer 29.92 inHg, TAT 15 °C on both TAT datarefs, generic_lights_brightness_ratio[14] = 1, sun pitch 40°); X-Plane APU quantities injected identically. Compare by name: switch, start_active-visible outputs (apu_start_load), commands sent (APU_start begin/end, APU_on, APU_off) with frame numbers (±1 frame), light values exactly, apu_bus_enable edge ±1 frame, apu_temp/apu_door/apu_psi/valve within 1e-3.

# Sequence Expected (oracle)
S1 apu_spec_start.json: BAT; ON; START after 7 s; N1 +10 %/3 s, RUN from 60 %; N1 100; APU bleed ON (packs off); OFF LOW OIL at 5.5 s after ON; start load 60 for 2.19 s then APU_start begin, APU_on 1 s later; door 1 within 6 s; EGT = N1 during start; LOW OIL out at N1 60 + RUN; EGT 100 for ~8.5 s, then decays to ~48.5; apu_bus_enable 19–25 s after N1 100; GEN OFF BUS lit; bleed valve 1, apu_bleed 1, apu_psi 41 → 33 → 21; OFF: bus enable 0 at once, valve 0, APU_off 58 s later; door 0 after RUN 0 (27 checks, passes)
S2 apu_spec_delay_fault.json part 1: ON 2 s, START start load 60 for 17.5 s; LOW OIL 5.5 s after START (timer re-armed)
S3 same, no N1 FAULT at 120 s after START, LOW OIL cleared, six-pack APU lit, door closes; FAULT still lit 10 s after OFF (12 checks, passes)
S4 S1 to running, BAT OFF abort: APU_off, all lights 0, gauge → 0 at rate 8
S5 OFF during start (state 2, RUN 0), then inject RUN 1 no APU_off until RUN 1; then cool-down 58 s
S6 running, OFF with gauge ≤ 25 (inject low EGT via restore or cold APU) APU_off within 2 frames
S7 failures: electric[19]; pressurisation[15] (start unavailable; MAINT after establish); engine[20], [21], [22] at N1 100 MAINT / OVERSPEED / LOW OIL, shutdown, six-pack inputs as §4.11
S8 fire: engine[18]; pull handle; discharge APU fire light, abort, rel_apu 6, extinguish after random amount
S9 elev_m 8000, all pumps off START gives no start (start_avail 0); pumps on → start
S10 ON, observe gauge 0–3 s needle sweep to egt+20 and back from 1.5 s
S11 running 120 s with/without bleed and APU GEN load, various tank-pump/crossfeed settings fuel removed per §4.12
S12 ON during cool-down start_active 3, cool-down cancelled, bus enable returns after 2 s
S13 TAT −30/40 °C, 28 000 ft T_start 28 s·1.3 / 14 s; EGT target 47/51

9 Open questions#

  1. X-Plane's actual APU trajectory (N1 shape, door gating, APU_running threshold, cool-down behaviour after APU_off) cannot be measured in the oracle. Needed: one capture in X-Plane 12 with the aircraft (DataRefTool, 10 Hz: APU_N1_percent, APU_running, cockpit2/electrical/APU_door, APU_starter_switch) for a start and a shutdown. Until then §7's model is a proposal.
  2. Does X-Plane's own 7 s cool-down add to Zibo's 58 s (total run after OFF ≈ 65 s + 18 s spool-down)?
  3. apu_leak and the duct-pressure side of the APU bleed are left to the pneumatics spec; S1's bleed numbers assume no leak (isolation valve, packs off).
  4. Whether SRC_APU_RUNNING should follow Zibo's RUN instead of N1 > 95 (contract semantics). M3: kept at N1 > 95 (profile); X-Plane's RUN is published as the diagnostic L:LU737_APU_RUNNING.
  5. The fuel-burn crossfeed-open branch uses tank_c1_status in a way that looks unintended (it gates the wing balancing); reproduce literally unless the owner decides otherwise.
  6. MSFS: confirm the package cfg defines no active APU (or that leaving it off has no side effects such as MSFS's own APU-generator bus feed).