Technical reference

Engine start and engine dynamics (CFM56-7B26): specification for the rewrite

docs/specs/engine_start_and_dynamics.md

Milestone M2. Subsystem: engine start switches, starter, ignition, start levers, light-off and start EGT, N1/N2 spool-up and spool-down, idle modes, thrust lever → requested N1/throttle, thrust, fuel flow, EGT, oil, vibration, failures/flameout, EEC, reversers (engine side), and the coupling to the simulator's engine model.

Source: zibomod.xpl (Zibo 2.S1.50, Linux build with symbols), read with tools/re/decompile.py (Ghidra); XLua scripts; 737_80NG.acf; the SCI contract and Zibo profile; the oracle (xphost). Everything below is described in our own words; constants, tables and timings are Zibo's. Function and global names are the symbol names in zibomod.xpl (e.g. Starter, engine_N2_anim), so a reader can find the original.

Conventions: index i = engine (0 = left/1, 1 = right/2). dt = sim_period (see Appendix A: frame time clamped to (0, 0.1] s, 0.01 s on the first frame). Helper functions (Rescale, Limit, Anim_value, B738_set_animation_rate, Anim_linear_value2) are defined in §5.1; the CFM56 table functions (cfm56_7b_*, corrected_*) are reproduced by a separate work package and are used here as black boxes with the arguments given.

1 Scope and sources#

In scope: everything zibomod computes for the two CFM56-7B26 engines except the pure table functions: start switches and their knob animation, the solenoid release, start valves, starter air → N2, ignition (L/R/BOTH, igniter power, auto-relight, windmill and cross-bleed start envelopes), start levers → X-Plane mixture, light-off EGT, hung/hot/wet starts, N1/N2 during start, spool-down and windmilling, idle (ground/flight/approach, EEC modes), thrust lever → throttle use → N1, EGT, fuel flow (indication and to the simulator), oil pressure/temperature, vibration, fan rotation, failures and flameouts, reverser engine side, and the forces written to X-Plane.

Not in scope: the CFM56 tables (separate work package), duct pressure and bleed valves (pneumatics spec: this spec lists what start needs from it), fuel tanks/pumps/valves (fuel spec), A/T and N1 limit computation (B738_N1_thrust_manage_new2, B738_eng_n1_set, B738_eng_out are called from Glareshield; AFDS/AT spec), EICAS drawing (display spec; it reads the outputs of §6), sound.

Sources (zibomod function → section): MainFlightLoopCallback (App. A), Init_engine_param (4.1), Idle_mode_start_flight, Systems_start_flight (4.2), Idle_mode (4.3), Engine_n1_n2 (4.4), Thr_shift (4.5), thr_use_to_n1, n1_to_thr_use, Thr_lever_to_use, Eicas_thrust (4.6), Xbleed_start (4.7), Starter, Gauge_N1_N2_EGT, n1_shutdown, n2_shutdown (4.8), Engine_calculation, egt_start, calc_start_egt (4.9), Engine_model (4.10), Systems (start part, 4.11), CMD_engN_start_*, CMD_eng_start_lft/rgt, CMD_mixtureN_*, manage_eng_failure, eng_flameout_effect, engine_fail (4.12), N1_N2_idle (4.13), CMD_reverse_*, reverse_leverN (4.14); Register_*DR for dataref names; Xplane_datarefs/Read_Xplane_DR for the X-Plane inputs. XLua: B738.tablet owns laminar/B738/engine/engine_air_start (EFB ground air cart), B738.a_fms owns laminar/B738/engine_start_auto (option, default 0). ACF: CFM56 parameters below are not used by Zibo's model except where noted in §7. Oracle runs: xphost/test/sci/start_spec_eng1_ground.json (§8).

2 Inputs#

2.1 Cockpit (SCI controls → Zibo commands/datarefs)#

SCI control (actions) SCI state Zibo Notes
ENGINE_1_STARTER / ENGINE_2_STARTER (INC, DEC) ENGINE_STARTER1_POS, _POS2 (ENGINE_STARTER2_*) cmd laminar/B738/knob/engN_start_right / _left; dref laminar/B738/engine/starterN_pos (0 GRD, 1 OFF, 2 CONT, 3 FLT), starterN_pos2 §4.11.1; GRD released by logic at 55.3–58 % N2 (contract §4.3 hold_by_logic)
(no SCI control) – cmds laminar/B738/rotary/engN_start_grd/off/cont/flt direct positions
ENGINE_START_SOURCE (INC, DEC) TOGGLE_SWITCH_ENG_START_SOURCE toggle_switch/eng_start_source_right/_left; −1 IGN L, 0 BOTH, 1 IGN R default IGN R
MIXTURE_ENGINE_1/2 (INC/DEC ±0.1), MIXTURE_ENGINE_1_2/_2_2 (TOGGLE 0↔1) ENGINE_MIXTURE_RATIO1/2 dref laminar/B738/engine/mixture_ratioN; cmds laminar/B738/engine/mixtureN_idle/_cutoff/_toggle/_hold start lever CUTOFF 0 / IDLE 1; Zibo tests > 0.5
EEC_1/2 (TOGGLE), guard EEC_1/2_GATE SIM_COCKPIT2_ENGINE_ACTUATORS_FADEC_ON_0/1, BUTTON_SWITCH_COVER_POSITION_0/1 X-Plane sim/cockpit2/engine/actuators/fadec_on[i] (1 NORMAL, 0 ALTN) §4.3
THROTTLE_ENGINE_1/2, THROTTLE_ALL (INC/DEC) ENGINE_THRUST1/2/12_LEVELER laminar/B738/engine/thrust1/2/12_leveler (0…1) also moved by the A/T
REVERSE_THROTTLE_1/2/BOTH FLT_CTRLS_REVERSE_LEVER1/2/12 laminar/B738/flt_ctrls/reverse_lever1/2/12 §4.14
ENGAGE_/DISENGAGE_REVERSE_1/2/BOTH (PRESS) SIM_COCKPIT2_ENGINE_ACTUATORS_PROP_MODE_0/1 sim/engines/thrust_reverse_toggle_1/2, …toggle handled by Zibo commands CMD_reverse_toggle*
N1_SET, N1_SET_ADJUST TOGGLE_SWITCH_N1_SET_* n1_set_source, n1_set_adjust AFDS/EICAS spec (B738_eng_n1_set)
ENGINE_1/2_ANTI_ICE ICE_ENG1/2_HEAT_POS B738DR_eng1/2_heat_pos approach idle, auto-ignition
fire handles, bleed switches, packs, isolation, APU bleed (fire, pneumatics specs) – inputs listed in 2.3

2.2 Simulator inputs (X-Plane datarefs Zibo reads → MSFS source)#

X-Plane dataref (Zibo global) Use MSFS source (recommendation)
sim/weather/aircraft/barometer_current_pas (simDR_current_pa), sim/weather/barometer_current_inhg AmbPR, oil pressure offset AMBIENT PRESSURE (inHg → Pa)
sim/cockpit2/temperature/outside_air_LE_temp_degc (simDR_TAT) θ, EGT floor TOTAL AIR TEMPERATURE
sim/cockpit2/temperature/outside_air_temp_degc (simDR_OAT) idle FF correction, EEC soft ALTN AMBIENT TEMPERATURE
sim/weather/sigma, sim/weather/rho rates, flight idle AMBIENT DENSITY / 1.225
sim/flightmodel/misc/machno, sim/flightmodel/position/true_airspeed tables, DynP AIRSPEED MACH, AIRSPEED TRUE
sim/flightmodel2/position/pressure_altitude, sim/flightmodel/position/elevation tables, windmill, flameout PRESSURE ALTITUDE, PLANE ALTITUDE (m)
radio_altimeter_height_ft_pilot, on_ground, groundspeed, IAS pilot idle/airborne latch, start envelopes, ground thrust RADIO HEIGHT, SIM ON GROUND, GROUND VELOCITY, AIRSPEED INDICATED
sim/flightmodel/weight/m_total, wind speed/direction, magnetic heading ground thrust term, windmill TOTAL WEIGHT, AMBIENT WIND VELOCITY/DIRECTION, PLANE HEADING DEGREES MAGNETIC
sim/weather/precipitation_on_aircraft_ratio, hail rain flameout, light-off rate AMBIENT PRECIP RATE (scaled) — see §9
sim/flightmodel2/engines/N1_percent, N2_percent read back of Zibo's own writes; flight start Zibo's own values (no MSFS read)
sim/flightmodel/engine/ENGN_running, sim/flightmodel2/engines/engine_is_burning_fuel light-off/"running" must be modelled by the rewrite (§7.1)
sim/flightmodel/engine/ENGN_thro_use Zibo's own throttle use (written by Eicas_thrust) internal state
sim/cockpit2/engine/indicators/fuel_flow_kg_sec Zibo's own ENGN_FF_ internal state
sim/cockpit2/engine/indicators/oil_temperature_deg_C, sim/flightmodel/engine/ENGN_oil_quan oil temperature base, quantity must be modelled (§7.2)
sim/flightmodel2/engines/engine_rotation_speed_rad_sec, …angle_deg fan animation Zibo's own (rot_spd/ang)
sim/flightmodel/failures/inlet_ice_per_engine (rel_eng_ice) ice factor on throttle use MSFS engine icing if exposed, else 0
sim/operation/failures/rel_engfai0/1, rel_engsep0/1, rel_engfir0/1, rel_fadec_0/1, rel_oilpmp0/1, rel_N2_ind0/1, bird strike failure states (Zibo writes most of them itself) internal state
sim/cockpit2/engine/actuators/fadec_on[i], prop_mode[i], ignition_key[i], igniter_on[i], mixture_ratio[i] written and read back by Zibo internal state
sim/operation/prefs/startup_running cold vs running start MSFS flight state (cold & dark vs engines running)
sim/time/paused, sim/time/is_in_replay frame gating SIM PAUSED/replay state

2.3 Other subsystems (Zibo datarefs/globals)#

Input From
B738DR_real_duct_pressure_L/R (psi), B738DR_apu_bleed_air_valve_pos, B738DR_isolation_bleed_air_valve_pos, B738DR_lpack/rpack_bleed_air_valve_pos, apu_establish_time, ext_air_duct, laminar/B738/engine/engine_air_start pneumatics
B738DR_spar_valve_N_pos, B738DR_eng_valve_N_pos, tank pump status L1/L2/C1/R1/R2/C2, cross_feed_valve, X-Plane fuel_selector_l/r, tank weights fuel
B738DR_dir_batbus_status (battery bus), ac_tnsbus1/2_status, ac_stdbus_status, batbus_status electrics
B738DR_hyd_A/B/stdby_status, fire handle arm, simDR_engN_fire hydraulics, fire
B738DR_flaps handle, cowl anti-ice (ice_inlet_heat_on_per_engine), B738DR_engN_heat_pos flight controls, anti-ice
B738DR_fms_N1_thrust[0..1] (N1 limit ratio), A/T lever writes FMS / AFDS
laminar/B738/failure/engine[0..69] EFB failures
B738DR_sfp/sfp_variant, laminar/B738/engine_start_auto options

3 State#

Per engine unless noted (names = zibomod globals; reset values from §4.1–4.2):

Group State
Start switch knob[i] (engineN_starter_pos), engine_starter_tgt[i], B738DR_engineN_starter_pos/_pos2
Valves/ignition B738DR_start_valveN, engineN_starter_valve, ignition[i], real_ignition[i], engine_on_old2, engine_start_enable_timer, fuel_selector_enable, TMR_energize, egt_redline, TMR_egt_redline, hot_start_warn, eec_hotstart, eng_egt_old
Spool engine_N1_anim, engine_N2_anim, engine_N2_old, B738DR_engN_N1/N2, eng_N1_idx, eng_N2_idx, gauge_n1/n2/egt, was_below50, eng_was_off, was_hung_start, was_hot_start, ENGN_thro_use (as written), T/T95 (per frame)
Idle/EEC idle_mode[i], eec_mode[i], eec_tmr, eec_rst, eec_last_oat/slt, max_thrust_use[i], shift_thr[i], eec_n1_lim_thr_use[i], global idle_n1, idle_n2, TotTR, TotTR_n2, TotTR_egt, AmbPR, aircraft_was_on_air2, TMR_mode_ground_timer, mixture_cur[i]
Fuel/EGT/oil metering_valve, B738DR_fuel_flow_kg_sec, fuel_flow_choke, start_ff, engine_running_old, fuel_valve_open_old, engine_lost, eng_egt_lost_tgt, eng_n2_lost_tgt, saved_egt, motoring_eng_egt, coef_egt, ws_coef_egt, eng_accel_egt, B738DR_engN_egt, B738DR_engN_oil_press, B738DR_engN_oil_temp
Random egt_rnd[i] (±5 °C per flight), eng_diff_stab[0], RNG state
Failures eng1/2_max_timer, eng1/2_flameout_timer, …timer2, TMR_fail_engine, TMR_eng_vib, eng_vib_active, eng_vib_tick, TMR_eng_high_temp, TMR_eng_low_press, eng_flameout_timer/2, B738DR_eng_flameout
Vibration/fan B738DR_engN_vib, rot_spd, rot_ang
Reverser B738DR_reverse_thrust[i], B738DR_reverse_deploy[i], reverse_max_enable/on
Simulator-side (must exist in the rewrite) running/burning flag, mixture, ignition key, igniter, fadec_on, prop_mode, oil temperature/quantity

4 Per-frame logic in execution order#

Order of the engine functions within a frame (Appendix A.4.4.2 and A.6): Idle_mode → Engine_n1_n2 → Thr_shift → (Glareshield: A/T moves the thrust levers) → Eicas_thrust → Xbleed_start → Starter (calls Gauge_N1_N2_EGT first) → Engine_calculation → Engine_model → … → Systems (start switches, start valves, ignition, EICAS energise) → … → manage_eng_failure → eng_flameout_effect → (2 Hz) N1_N2_idle, engine_fail. Commands (start switches, start levers, reversers) run between frames, when X-Plane delivers them.

The core idea of Zibo's engine model:

  • Zibo owns N1, N2, EGT, fuel flow, thrust, oil pressure. Each frame Starter writes N1/N2 into X-Plane's engine (sim/flightmodel/engine/ENGN_N1_, ENGN_N2_), Engine_model overrides the engine forces (sim/operation/override/override_engine_forces = 1) and writes the thrust as faxil_prop/L/M/N_prop, and overrides fuel flow (override_fuel_flow = 1, writes ENGN_FF_).
  • The steady-state N1 follows a commanded "throttle use". Eicas_thrust turns the thrust lever into an N1 target, rate-limits it in N1 space and converts it into X-Plane's ENGN_thro_use; Gauge_N1_N2_EGT turns ENGN_thro_use back into N1 (thr_use_to_n1) and derives N2 and EGT from the CFM56 tables. X-Plane's own spool dynamics are therefore bypassed: N1 is a deterministic function of Zibo's state (plus the start/shutdown dynamics in Starter).
  • X-Plane still decides "running": ENGN_running and engine_is_burning_fuel (light-off, flameout by fuel starvation or cut-off) come from X-Plane's engine model, driven by Zibo's ignition (ignition_key, igniter_on), mixture (mixture_ratio) and N2 (written by Zibo). See §7.

4.1 Flight load: Init_engine_param (first frame after a load)#

Sets the engine model constants (values in §5.3): max_thrust_use[i] = 1.12, max_rev_thrust_use = 0.69, min_sigma = 0.235, max_sigma = 1.0, the thr_tab (14 lever positions 0…1) and several n1_idle* tables. The n1_idle*/thr_tab tables and the functions Calc_request_n1, Calc_request_thrust, n1_thruse_norm, thruse_n1_norm, min_idle_thrust have no caller in 2.S1.50 (dead code): do not port them. The thrust model type B738DR_curve02_type is 4 (static initial value; only the developer tuning file or zibomod/engine_model/curve02_type changes it), so only branch 4 of Engine_model is live.

4.2 Flight start: Idle_mode_start_flight (A.3)#

Depending on X-Plane's start-up preference sim/operation/prefs/startup_running:

  • Cold (0) and not a reloaded flight (loaded_flight == 0): start levers CUTOFF (B738DR_mixture_ratio1/2 = 0), X-Plane N1 = N2 = 0, mixture 0, ENGN_running = 0.
  • Engines running (1): start levers IDLE (1), X-Plane N1 = 19.5 %, N2 = 58.5 %, mixture 1, ENGN_running = 1 (written to X-Plane).
  • Reloaded flight (loaded_flight != 0): start lever i = IDLE if X-Plane's mixture[i] > 0.2.

Then every internal N1/N2 copy is initialised from X-Plane's N1_percent/N2_percent: engine_N1_anim, engine_N2_anim, engine_N2_old, B738DR_eng1/2_N1/N2, gauge_n1/n2, eng_N1_idx, eng_N2_idx; eng_accel_egt = 0. If X-Plane's igniter_on is 0 for either engine, both are set to 1 (written). Vibration indication initial value from N1 (same breakpoints as §4.8.8 divided by 100); eng_vib_active[i] = false, eng_vib_tick = 0.

Systems_start_flight (A.3) also: start switches to OFF (starter_pos = 1, pos2 = 1, internal knob engineN_starter_pos = 1, engine_starter_tgt = 1), IGN select B738DR_eng_start_source = 1 (= IGN R), and eng_diff_stab[0] = 1 − generate_rnd(0, 0.08) while eng_diff_stab[1] keeps 1.0. (Zibo writes both random values to element 0 — the second store overwrites the first, so engine 1 gets 1 − r and engine 2 is never randomised. Reproduce.) egt_rnd[i] (A.3.1) is drawn just before.

4.3 Idle_mode — idle selection, EEC mode, start lever → X-Plane mixture#

  1. Airborne latch: if radio height > 20 ft and aircraft_was_on_air2 == 0 → aircraft_was_on_air2 = 1.
  2. Ground timer: T = 2.5 s if the SFP option is fitted (sfp_variant or B738DR_sfp != 0), else 5.5 s. If on ground: if never airborne → TMR_mode_ground_timer = 0, ground flag = true; else if the timer is 0 → start it at T. (Annun_timers, inside Annunciators, counts it down by dt and when it reaches 0 clears aircraft_was_on_air2; so flight/approach idle is kept for T s after touchdown.)
  3. EEC input health ok[i] = engine_failure[14 + i] != 1 (failure array laminar/B738/failures/engine, index 14/15 = "EEC normal-mode input").
  4. EEC hardware failure engine_failure[16 + i] == 1 → X-Plane rel_fadec_i = 6 (failed, written).
  5. X-Plane fadec_on[i] (the EEC switch, SCI EEC_1/2) is forced 0 when rel_fadec_i == 6; written if changed.
  6. Per engine, EEC mode eec_mode[i] (−1 failed, 0 hard ALTN, 1 NORMAL, 2 soft ALTN, 3 soft ALTN with lever at idle) with timers eec_tmr[i], eec_rst[i]:
    if rel_fadec_i == 6:            eec_mode = -1; eec_tmr = eec_rst = 0
    elif fadec_on[i] == 0:          eec_mode = 0;  eec_tmr = eec_rst = 0
    elif eec_mode < 2:              eec_mode = 1
    if eec_mode == 1:
        if ok[i]: eec_rst = 0
        elif eec_tmr == 0: eec_tmr = 15
        else: eec_tmr -= dt; if eec_tmr <= 0: eec_tmr = 0; eec_mode = 2
    elif eec_mode >= 2:
        if eec_mode == 3:
            eec_rst += dt
            if eec_rst >= 15: eec_mode = 0; fadec_on[i] = 0 (written); eec_rst = 0
        else:
            eec_rst = 0
            if thrust_lever[i] < 0.01: eec_mode = 3
    
  7. Idle mode idle_mode[i] (0 ground, 1 flight, 2 approach):
    if ground flag: idle_mode = 0
    else:
        idle_mode = 1
        if eec_mode >= 0:
            if not ok[i]:  (EEC lost its inputs)
                if pressure altitude < 15000 ft: idle_mode = 2
            elif flap handle ratio < 0.625:        (below flaps 15)
                if cowl anti-ice on (either engine): idle_mode = 2
            else: idle_mode = 2
        if eec_mode == 1:
            eec_last_oat[i] = OAT; eec_last_slt[i] = 15      (frozen for soft ALTN)
    
    Published as laminar/B738/engine/idle_mode[0..1] (B738DR_idle_mode).
  8. Start lever → X-Plane mixture: mixture_cur[i] = Anim_value(mixture_cur[i], B738DR_mixture_ratio{i+1}, 0, 1, 1) (first-order lag, 1/s), written to sim/cockpit2/engine/actuators/mixture_ratio[i] when it changes. The start lever itself (laminar/B738/engine/mixture_ratio1/2, SCI ENGINE_MIXTURE_RATIO1/2) is 0 = CUTOFF, 1 = IDLE; commands mixtureN_idle (= 1), mixtureN_cutoff (= 0), mixtureN_toggle, mixtureN_hold, and the SCI generic controls write the dataref directly.

4.4 Engine_n1_n2 — ambient ratios and idle N1/N2#

AmbPR   = P_amb[Pa] / 101325
TotTR   = max(0, (TAT + 273.15) / 288.15)
TotTR_n2  = TotTR ^ 0.45          (N2 correction)
TotTR_egt = TotTR ^ 0.16          (EGT correction)
DynP = 0.5 * rho * TAS^2 ; TotP = P_amb + DynP ; TotPR = TotP / 101325
n2_min = 58.4 ; if any engine idle_mode == 1: 72.1 ; elif any == 2: 74.9
if both idle_mode == 0:     (ground idle)
    thr = cfm56_7b_ff_thr(650 / ff_corr_coef(alt_p, OAT), alt_p, M)
    n2  = TotTR_n2 * cfm56_7b_cn1_to_cn2(cfm56_7b_cn1(thr, M), alt_p, M) / 14460 * 100
    idle_n2 = max(n2_min, n2)
else:                        (flight / approach idle)
    same with 440 instead of 650, n2 capped at 79
    idle_n2 = max(n2_min, min(79, n2))
cn2 = idle_n2 * 0.01 * 14460 / TotTR_n2           (if TotTR_n2 != 0)
idle_n1 = corrected_cn1_to_n1(cfm56_7b_cn2_to_cn1(cn2, alt_p, M), TotTR)
if idle_n1 < 19.1: idle_n1 = 19.1
idle_n2 = TotTR_n2 * cfm56_7b_cn1_to_cn2(corrected_n1_to_cn1(idle_n1, alt_p, M, 15), alt_p, M) / 14460 * 100

alt_p = X-Plane pressure altitude (ft), M = Mach, P_amb = sim/weather/aircraft/barometer_current_pas, TAT = sim/cockpit2/temperature/outside_air_LE_temp_degc, rho = sim/weather/rho. idle_n1/idle_n2 are single values for both engines (the mode of either engine raises both). 14460 rpm = 100 % N2.

4.5 Thr_shift — EEC N1 limit and idle throttle#

Per engine; egt_max = max(0, egt_rnd[0], egt_rnd[1]):

if eec_mode < 0:                 n1_lim = 105
elif reverser engaged (prop_mode == 3): n1_lim = 104
else:
    theta = TotTR                                  (eec_mode 1)
    if eec_mode == 0: theta = (calc_tat_by_mach(calc_tamb_by_alt(alt_p), M) + 273.15) / 288.15
    if eec_mode >= 2: theta = (calc_tat_by_mach(eec_last_oat[i], M) + 273.15) / (eec_last_slt[i] + 273.15)
    n1_egt = corrected_cn1_to_n1(cfm56_7b_cegt_to_cn1((948 + egt_max + 273.15) / TotTR_egt, alt_p, M), theta)
    cn2_lim = 15183 / theta^0.45      (15183 if the power is 0; theta<0 → theta itself)
    n1_n2  = corrected_cn1_to_n1(cfm56_7b_cn2_to_cn1(cn2_lim, alt_p, M), theta)
    n1_lim = Limit(min(n1_egt, n1_n2), 90, 104)
eec_n1_lim_thr_use[i] = min(1.12, n1_to_thr_use(n1_lim, -1))
max_thrust_use[i] = eec_n1_lim_thr_use[i]
shift_thr[i] = n1_to_thr_use(idle_n1, i)           (throttle-use at idle)

(For eec_mode == 1 the corrected-N1 conversion of n1_egt uses TotTR; the EGT redline in the limit is 948 °C + the random offset.)

4.6 N1 ↔ throttle-use mapping and Eicas_thrust#

Mapping (thr_use_to_n1, n1_to_thr_use; engine index i, i < 0 = "no engine"):

ice_fac(i) = max(0.3, 1 − 0.65 * rel_eng_ice[i]^2)     (X-Plane inlet ice ratio)
cap(i)     = (i < 0) ? 1.12 * ice_fac : max_thrust_use[i] * ice_fac
thr_use_to_n1(u, i) = 19.0758705 + (99.9930954 − 19.0758705) * Limit(u, 0, cap(i))^(1/1.62)
n1_to_thr_use(n, i) = Limit( ((Limit(n, 19.0758705, 108) − 19.0758705) / 80.9172249)^1.62 , 0, cap(i))

(i < 0 uses ice_fac of engine 0 memory layout: the code indexes rel_eng_ice[-1]; Zibo only calls it with -1 from Thr_shift, where the value is then capped at 1.12 — see §9.)

Lever → throttle use (Thr_lever_to_use(lever, idle_mode, prop_mode, i)):

a  = shift_thr[i]; b = 0.25 * max_thrust_use[i]
if prop_mode == 3 (reverse): Rescale(0.06, a, 1.0, max_rev_thrust_use * ice_fac(i), lever)
elif lever >= 0.25:          Rescale(0.25, b, 1.0, max_thrust_use[i] * ice_fac(i), lever)
else:                        Rescale(0.0, a, 0.25, b, lever)

Eicas_thrust (runs only when the X-Plane ENGN_thro_use, OAT and the Lua eng*_req_n1 datarefs exist), per engine:

lever = thrust_lever[i]  (laminar/B738/engine/thrust1/2_leveler)
if prop_mode[i] == 3:  lever = (reverser deploy[i] >= 0.6) ? reverse_lever[i] : 0.06
n1_cmd  = thr_use_to_n1(Thr_lever_to_use(lever, idle_mode[i], prop_mode[i], i), i)     → eng*_req_n1 = n1_cmd/100
n1_now  = thr_use_to_n1(ENGN_thro_use[i], i)          (current commanded N1)
sig     = 1 / Rescale(0.235, 1.5, 1.0, 1.0, Limit(sigma, 0.235, 1))
if n1_cmd − eng_N1_idx[i] > 0:          (accelerating)
    t = (prop_mode==3) ? 0.06*sig : (n1_now >= 33) ? Rescale(1089, 0.5, 1600, 0.06*sig, n1_now^2) : 0.5*eng_diff_stab[i]
else:                                   (decelerating)
    t = (n1_now >= 33 or prop_mode==3) ? 0.16*sig : 0.25*sig
n1_new = B738_set_animation_rate(n1_now, n1_cmd, 0, 108, t)
ENGN_thro_use[i] = n1_to_thr_use(n1_new, i)            (written for both engines)

Also written: eng*_req_n1, eng*_req_pie, thr_lvr* (values from the same loop), and the N1 limit bugs B738DR_engN_N1_lim = n1_to_thr_use(100 * fms_N1_thrust[i]), _lim2 = n1_to_thr_use(94 * fms_N1_thrust[i]). B738_set_animation_rate(cur, tgt, lo, hi, t) moves by dt / t per frame (t seconds per 1 % N1). At sea level (sig = 1): acceleration 2 %/s below 33 % N1 (divided by eng_diff_stab), rising with N1² from 2 %/s at 33 % to 16.7 %/s at 40 % N1 and above; deceleration 4 %/s below 33 % and 6.25 %/s above (and in reverse). sig is 1 at sigma ≥ 1 and 1/1.5 at sigma ≤ 0.235, i.e. the times shrink (faster spool) at altitude, except the sub-33 % acceleration which does not use sig.

4.7 Xbleed_start — in-flight start envelope#

alt = Limit(elevation_m * 3.281, 0, 24000)
if alt >= 24000: xbld_start1 = xbld_start2 = wms = 0
else:
    vmin = (alt < 15000) ? 140 : (alt >= 20000) ? 220 : Rescale(15000,140,20000,220,alt)
    vmax = (alt < 15000) ? 300 : Rescale(15000,300,24000,281,alt)
    if radio_alt <= 50: xbld_start1 = xbld_start2 = 0
    elif vmin <= IAS <= vmax:
        xbld_start1 = (eng_N1_idx[0] < 17 and eng_N1_idx[1] > 17 and running[1]) ? 1 : 0
        xbld_start2 = symmetric
    wms = (IAS > vmax) ? 1 : 0            (windmill start possible)
B738DR_xbld_start1/2, B738DR_wms ← values

(Outside vmin…vmax the X-BLD flags keep their previous value.)

4.8 Starter — starter air, N2/N1 dynamics, windmilling, vibration, fan#

Runs only if X-Plane's elevation, starter torque, starter max rpm and N2 datarefs exist.

4.8.1 Ambient-dependent rates (s = Limit(sigma, 0.635, 1)): r_start = Rescale(0.635, 1.10, 1, 1.20, s) (starter N2 rate, %/s), r_lightoff = Rescale(0.635, 1.45, 1, 1.55, s) (post-light-off N2 rate, %/s; × Rescale(0, 1, 1, 0.9, precip) when precipitation on the aircraft > 0.1), n2_motor = Rescale(0.635, 26.1, 1, 29.6, s) (maximum motoring N2, %).

4.8.2 Starter air → maximum motoring N2 per engine (pneumatics inputs defined by the pneumatics spec; f(p) = Rescale(10.5, 0, 21.6, 1, p) on duct pressure in psi):

apu   = f(duct_L) * apu_bleed_valve * apu_establish_time
src0  = apu * n2_motor                      src1 = iso * apu * n2_motor
ext   = engine_air_start ? (ext_air_duct > 21.6 ? 1 : Rescale(0,0,21.6,1,ext_air_duct)) : 0
src2  = iso * ext * 29.6                    src3 = ext * 29.6
src4  = f(duct_L) * n2_motor * iso          src5 = f(duct_R) * n2_motor * iso
load0 = lpack + iso*rpack ; load1 = rpack + iso*lpack     (pack valve positions 0..1)
xs0   = iso * start_valve2 ; xs1 = iso * start_valve1     (other starter via isolation valve)
for i in 0,1:
    loss = (load_i > 1) ? Rescale(1, 0.33, 2, 0.40, load_i) : Rescale(0, 0, 1, 0.33, load_i)
    loss += 0.42 * xs_i
    T[i]  = max(0, max(src_i, src_{2+i}, src_{4+i}) * (1 − loss))   (engine 0: src0,src2,src4; engine 1: src1,src3,src5)
    T95[i] = 0.946 * T[i]
    starter_is_running[i] = (T[i] != 0)
B738DR_starter = 1 if any T != 0

iso = isolation valve position, duct_L/R = B738DR_real_duct_pressure_L/R.

4.8.3 Gauge_N1_N2_EGT() (steady-state targets, per engine):

if eng_N2_idx[i] <= 58:    (start / sub-idle)
    gauge_n1 = thr_use_to_n1(shift_thr[i], i)
    cn1 = corrected_n1_to_cn1_tat(idle_n1, TAT, 15)
    gauge_n2 = TotTR_n2 * cfm56_7b_cn1_to_cn2(cn1, alt_p, M) / 14460 * 100
    gauge_egt = Rescale(0, TAT, 58, TotTR_egt * cfm56_7b_cn1_to_cegt(cn1, alt_p, M) − 273.15, eng_N2_idx)
else:
    gauge_n1 = thr_use_to_n1(ENGN_thro_use[i], i)
    cn1 = corrected_n1_to_cn1_tat(gauge_n1, TAT, 15)
    gauge_n2 = TotTR_n2 * cfm56_7b_cn1_to_cn2(cn1, alt_p, M) / 14460 * 100
    gauge_egt = TotTR_egt * cfm56_7b_cn1_to_cegt(cn1, alt_p, M) − 273.15
gauge_egt = Limit(gauge_egt, −56.5, 1200)
(on the very first frame gauge_egt starts at Limit(TAT, −30, 30))

4.8.4 N2 per engine (pos = start switch position, run = ENGN_running[i], ign_ok = real_ignition[i] != 0 or eng_N2_idx[i] >= 56, N2a = engine_N2_anim[i], driven[i] = "N2 set by starter/shutdown" flag):

lit = run and ign_ok
case pos == GRD (0):
    if not lit:
        if T < N2a:            N2a = n2_shutdown(N2a, T)
        elif N2a >= T95:       N2a = Anim_linear_value2(N2a, T, 0, 100, 0.025 * r_start)
        else:                  N2a = Anim_linear_value2(N2a, T95, 0, 100, r_start)
        driven = true
    else: POST_LIGHTOFF
case pos == FLT (3) and eng_N2_idx >= 11 and IAS > 180:      (windmill/assisted start)
    if not lit:
        if T <= N2a:           N2a = n2_shutdown(N2a, T)
        else:                  N2a = Anim_linear_value2(N2a, T, 0, 100, 0.85 * r_start)
        driven = true
    else: POST_LIGHTOFF
otherwise (OFF, CONT, FLT outside the condition):
    if not lit:                N2a = n2_shutdown(N2a, 0)          (not flagged driven)
    else: POST_LIGHTOFF (without setting driven in the "follow gauge" case)

POST_LIGHTOFF:
    if eng_N2_idx >= idle_n2 + 3 or not was_below50[i]: N2a = gauge_n2[i]
    else: N2a = Anim_linear_value2(N2a, idle_n2 + 3, 0, 100, r_lightoff); driven = true (GRD/FLT cases only)

Then:

  • Hung start (failure engine_failure[23 + i]): if 50 < N2a < 54 and eng_was_off[i] → N2a = 50, was_hung_start[i] = 1.
  • If eng_N2_idx <= 56: if not running → eng_was_off = 1, and clear a latched hung-start ([23+i]) or hot-start ([25+i]) failure (written back to the failure array). If eng_N2_idx > 56 → eng_was_off = 0.
  • was_below50[i] = 1 if N2a < 50 or not running; = 0 once N2a > idle_n2 + 2.8.

n2_shutdown(n, floor) and n1_shutdown(n) (spool-down): below 59.8 % N2 (19.2 % N1) the value follows a time table (seconds since shutdown, ×1.3) — the current value is converted to an elapsed time on the table, dt is added, and the value at the new time is returned (tables §5.4). Above it, N2 is converted to an N1-like value 100*(n/100)^3.11, decreased by B738_set_animation_rate with 0.012 s/% (≥ 33) or 0.04 s/% (< 33), and converted back with exponent 1/3.1; N1 above 19.2 % decreases the same way directly. The result is never below floor.

4.8.5 N1 per engine (target written to X-Plane):

if not run:
    if not driven: N1t = n1_shutdown(engine_N1_anim)
    else: N1t = 100*(N2a/100)^3.11 ; if N1t < eng_N1_idx: N1t = n1_shutdown(engine_N1_anim)
elif engine_N1_anim < idle_n1 − 0.1 or was_below50:
    if N2a <= idle_n2 − 0.3 or was_below50: N1t = 100*(N2a/100)^3.09
    else: N1t = corrected_cn1_to_n1(cfm56_7b_cn2_to_cn1(gauge_n2*0.01*14460/TotTR_n2, alt_p, M), TotTR)
else: N1t = gauge_n1

4.8.6 Windmilling floor (only if either engine is not running): Mach M' = M (0 below 0.01); band index k from 0.1 steps (1…9, 9 for M ≥ 0.8). For k == 1 (Mach < 0.1) the lowest altitude band uses 0.01 instead of 0 as the first Mach breakpoint for the N1 table, and the head-wind component w = |wind_kt * cos(wind_from + 180 − heading_mag)| * 0.514 / 330 (Mach-equivalent, 0 below 0.01, ≤ 0.1) is used instead of M' when larger (N1 table only). Altitude h = Limit(3.28 * elevation_m, 0, 45000) interpolates between table rows at 0, 15000, 25000, 35000, 45000 ft (tables §5.5; note the row pairs: 0–15000 uses rows 0/1, 15000–25000 rows 1/2, … and the interpolation is Rescale(0, rowA, X, rowB, h) with X = upper bound, i.e. always from 0 ft). For every non-running engine: N1t = max(N1t, wm_n1), and if N2a < wm_n2: N2a = wm_n2 (also stored in engine_N2_anim).

4.8.7 Publish N1/N2:

  • Engine vibration event (eng_vib_active[i], set by bird strike/flameout, §4.12) while eng_vib_tick <= 0.1: N2a −= generate_rnd(0, 0.3 * N2a) (≥ 0), also into X-Plane's N2 reading. eng_vib_tick -= dt; when ≤ 0 it restarts at generate_rnd(2, 3) s.
  • B738DR_engN_N2 = Anim_value(prev, N2a, 0, 108, 1) → eng_N2_idx; write ENGN_N2_ = engine_N2_anim.
  • Engine separated or failed (rel_engsep/rel_engfai = 6): N1 target 0 and indicated N1 = 0.
  • B738DR_engN_N1 = Anim_value(prev, N1t, 0, 108, 1) → eng_N1_idx; engine_N1_anim = N1t; write ENGN_N1_ = engine_N1_anim.
  • Write acf_starter_torque_ratio = 0.07 and acf_starter_max_rpm_ratio = 0.09 every frame (keeps X-Plane's own starter negligible).
  • engine_N2_old = engine_N2_anim.

So the indicated N1/N2 (laminar/B738/engine/indicators/N1_percent_1 etc.) are first-order lags (time constant 1 s) of the values written to X-Plane.

4.8.8 Vibration indication (B738DR_eng1/2_vib, 0…5 units): target v = Rescale(0,0,10,181,N1) below 10 % N1, Rescale(10,181,37,121,N1) to 37 %, Rescale(37,121,105,147,N1) above (in 1/100 units); during a vibration event v = min(500, 3 v); indication = Anim_value(100*vib, v, 0, 500, 3) / 100; separated engine → 0.

4.8.9 Fan rotation (laminar/B738/engine/rot_spd0/1, rot_ang0/1, exterior/fan animation): w = eng_N1_idx / 0.1845273 (rad/s); if X-Plane's engine_rotation_speed_rad_sec >= w use X-Plane's speed and angle; else w = 0 below 0.04, rot_spd = Anim_value(rot_spd, w, 0, 45, 0.65), rot_ang = fmod(rot_ang + dt * 57.3 * rot_spd, 360); separated → speed 0.

4.9 Engine_calculation — fuel metering, fuel flow, EGT, oil#

Per engine, fuel_open[i] = spar_valve_i >= 0.95 and eng_valve_i >= 0.95 (fuel spar and engine fuel valves, fuel spec). First frame: initialise the memories (metering_valve = 0, coef_egt = ws_coef_egt = 1, …).

Fuel flow indication (laminar/B738/engine/fuel_flow_kg_sec[i]), not in replay:

if running went 0 → 1 this frame: fuel_flow_choke = 1
if not running: ff = 0
target_valve = (eng_N2_idx > 19 and fuel_open) ? 1 : 0
metering_valve = B738_set_animation_rate(metering_valve, target_valve, 0, 1, 2)   (2 s full travel)
if engine separated or not running: ff = 0
elif metering_valve < 0.99 or not running: ff → 0 with Anim_value(k = 1/s)   (in 0.01 kg/s units)
elif fuel_flow_choke == 1:        (light-off surge)
    tgt = max(0.09722, start_ff + 0.01389); ff → tgt at 0.154 s per 0.01 kg/s; when reached: choke = 2
elif fuel_flow_choke == 2:
    tgt = max(0.082177, start_ff);         ff → tgt at 0.333 s per 0.01 kg/s; when reached: choke = 0
else:
    tgt = ENGN fuel flow from X-Plane (fuel_flow_kg_sec, which is Zibo's own ENGN_FF_ written by Engine_model)
    if eng_N2_idx < 58.5: if eng_N2_idx > 46.2: tgt = Rescale(46.2, start_ff, 58.5, start_ff + 0.02778, N2)
                          tgt = max(tgt, 0.082177)
    ff = Anim_value(ff, tgt, k = 1/s)
replay: ff = X-Plane fuel flow, fuel_open treated as true.

start_ff[i] = idle fuel flow computed by Engine_model (§4.10).

Fuel available per side for the EGT/flameout logic: X-Plane fuel_selector_l/r 0 → none; 1/2/3 → tank 1/2/3 must hold > 150 kg; other → any tank > 150 kg.

Engine lost (fuel starvation): if the engine was running last frame, is not running now, eng_N2_idx > 85, and fuel is open and available → engine_lost = 1, eng_egt_lost_tgt = min(1000, EGT + Rescale(57, 100, 101.4, 140, N2)), eng_n2_lost_tgt = eng_N2_idx. Cleared when running or N2 < 42.

EGT (laminar/B738/engine/eng1_egt/eng2_egt, °C): target E*, rate coefficient c:

c = Rescale(0, 1, 0.82, 2, Limit(M, 0, 0.82))
if eng_N2_idx > 58: ws_coef_egt = 1
coef_egt, ws_coef_egt clamped to [1,4] and [1,3]
if not running:
    E* = TAT, except during motoring between 28 and 29.8 % N2 with N2 rising and fuel closed:
         E* = Limit(EGT − egt_rnd + Rescale(28,0,29.7,5,N2), TAT, TAT + 123 − egt_rnd)
    saved_egt = 0 ; coef_egt → 1 (Anim_value k=1)
    fuel closed: ws_coef_egt −= dt * (N2 < 25 ? 0.01 : 0.03) while > 1   (cools)
    fuel open:   ws_coef_egt += dt * Rescale(225, 0.25, 625, 0, N2^2) while < 3   (wet start: fuel without light-off)
elif eng_N2_idx >= 56.5 or X-Plane fuel flow >= 0.1:   E* = gauge_egt ; saved_egt = 0 ; coef_egt → 1
elif fuel closed or (N2 < 28 and X-Plane FF < 0.0279):
    if 28 < N2 <= 29.7 and N2 rising: E* = Limit(EGT − egt_rnd + Rescale(28,0,29.7,4,N2), TAT, TAT + 123 − egt_rnd)
    saved_egt = 0 ; coef_egt → 1
elif N2 < 28 or N2 falling: E* = TAT ; saved_egt = 0 ; coef_egt → 1
else (lit, accelerating, N2 28…56.5):
    if not saved_egt: motoring_eng_egt = max(TAT, calc_start_egt(N2, EGT)); saved_egt = 1
    E* = max(egt_start(N2, motoring_eng_egt, i), EGT − egt_rnd)
E* = max(E*, TAT − egt_rnd, −egt_rnd) + egt_rnd ; E* = max(E*, TAT)
c *= (N2 < 30) ? Rescale(0,1,58,1.2,N2) : (N2 < 58) ? Rescale(0,1.2,58,2,N2) : Rescale(58,2,100,3,N2) ; c = Limit(c, 1, 3.5)

calc_start_egt(N2, …) / egt_start(N2, base, i): piecewise-linear start EGT rise above the motoring EGT against N2 (tables §5.6). egt_start scales the table by coef_egt[i], which rises (k = 2/s up, 1/s down, range 1…4) towards Limit(m * Rescale(ws_lo, 0, ws_hi, ws, N2), 1, 4) with ws = ws_coef_egt, ws_lo = Rescale(1,28,3,38,ws), ws_hi = Rescale(1,28,3,49,ws), m = Rescale(0, 1, 1, 4, max(thrust lever, reverse lever)) (opening the thrust lever during the start raises EGT up to ×4), and m = 3 with the hot-start failure (engine_failure[25 + i], sets was_hot_start).

EGT update:

if separated: EGT = 0
elif engine_lost == 1:
    if EGT >= eng_egt_lost_tgt − 45: engine_lost = 2
    else: E* = eng_egt_lost_tgt; X-Plane N2, ENGN_N2_ and engine_N2_anim held at eng_n2_lost_tgt
    EGT = Anim_value(EGT, E*, 0, 1000, 0.3)
elif EGT <= E* or ENGN burning fuel:
    if N2 >= 56.5 or N2 falling or E* <= TAT or E* < EGT:
        k = (EGT <= 180) ? Rescale(100, 0.0009c, 180, 0.0141c, EGT) : Rescale(180, 0.0141c, 900, 0.2c, EGT)
        EGT = Animate_physical3(EGT, E*, 0, 1000, eng_accel_egt, 0.1, 100, 0.3, k)   (second-order, §5.1)
    elif fuel open or N2 <= 28 or N2 > 29.8 or N2 falling: EGT = Anim_value(EGT, E*, 0, 1000, 200)
    else: EGT = Anim_value(EGT, E*, 0, 1000, 0.00155 c)
else:   (cooling, not burning)
    k as above (two ranges); EGT = Anim_value(EGT, E*, 0, 1000, k)
if the second-order branch was not used: eng_accel_egt = 0
first frame: EGT = E*

Oil pressure (laminar/B738/engine/eng1_oil_press, psi), p0 = current_inHg / 2.03602 (ambient psi):

N2 < 20.2:  Rescale(0, 0, 20.2, 15.7, N2) − p0
N2 < 30.7:  Rescale(20.2, 15.7, 30.7, 20.7, N2) − p0
N2 < 38.2:  Rescale(30.7, 20.7, 38.2, 27.7, N2) − p0
N2 < 58.9:  Rescale(38.2, 27.7, 58.9, 39.7, N2) − p0
else:       39.7 + 1.3223529 * (N2 − 58.9) − p0
≥ 0; oil pump failed (rel_oilpmp) or engine separated → 0
oil quantity ratio < 0.55: × Rescale(0.05, 0, 0.55, 1, qty) (linear fade to 0 at 5 %)

Oil temperature (°C) from X-Plane's oil temperature T_xp: T* = (T_xp >= 31) ? 69 + 0.66 (T_xp − 31) : Rescale(0, 0, 31, 69, T_xp), T* = max(T*, TAT, 0); if burning fuel and oil pressure ≤ 14.5 psi: T* = Rescale(0, 200, 14.5, T*, p) and T = Anim_linear_value2(T, T*, 0, 200, 0.26); else T = Anim_value(T, T*, 0, 200, 0.4); separated → 0.

4.10 Engine_model — thrust and fuel flow to X-Plane (branch 4)#

Writes once: override_engine_forces = 1, override_fuel_flow = 1.

for i:
    u = ENGN_thro_use[i]
    if u < 0.25 * max_thrust_use[i]:
        flt_idle = idle_n1 * (1 + Rescale(sigma_lo, f_lo, sigma_hi, f_hi, sigma))   (flight-idle sigma table, §5.3)
        u = Rescale(shift_thr[i], n1_to_thr_use(flt_idle, i), 0.25*max_thrust_use[i], 0.25*max_thrust_use[i], u)
    n1c = thr_use_to_n1(u, i)
    F = burning[i] * cfm56_7b_thrust(corrected_n1_to_cn1_tat(n1c, TAT, 15), M)          (lbf, corrected)
    F = (static_thrust / 27000) * (F + 3700) − 3700 ; F = AmbPR * F * 4.44822162       (N)
    ff = burning * corrected_ff2(cfm56_7b_fuel_flow(burning * cfm56_7b_thrust(corrected_n1_to_cn1_tat(eng_N1_idx, TAT, 15), M), alt_p, M) * 0.45359237 / 3600, alt_p, OAT)
    if ff < 0.0415793 and burning and airborne: ff = 0.0415793     (kg/s floor = 330 lb/h)
    ENGN_FF_[i] = ff
    start_ff[i] = same chain with idle_n1 instead of eng_N1_idx                          (idle fuel flow)
on ground (per engine, uses indicated N1 eng_N1_idx):
    F += burning * ( N1<20: Rescale(0,0,20,1500,N1) ; N1<40: Rescale(20,1500,40,11800,N1) ;
                     N1<50: 11800 ; N1<90: Rescale(50,11800,90,0,N1) ; else 0 )
    if N1 < 35: F −= F * Rescale(19, Rescale(40000, 0, 70000, 0.89 * Rescale(0,1,5,0,GS_kt), weight_kg), 35, 0, N1)
reverse multiplier R = reverse_coef * (GS_kt < 60 ? 1 : GS_kt >= 80 ? 1.3 : Rescale(60,1,80,1.3,GS_kt))
per engine, d = reverser deploy ratio:
    Fx[i] = (d >= 0.5) ? Rescale(0.5, 0, 1, F*R, d) : Rescale(0, −F, 0.5, 0, d)     (−F = forward thrust along −x)
faxil_prop = Fx[0] + Fx[1]
fnrml_prop = −faxil_prop * 0.0874886627          (thrust line inclination, ~5°)
fside_prop = 0.06139626 * (Fx[1] − Fx[0])
L_prop = 5.498948495 * fside_prop ; M_prop = −1.69597236585 * faxil_prop ; N_prop = 82.23832144 * fside_prop
write faxil/fnrml/fside/L/M/N_prop and ENGN_FF_[0..1]

(burning = X-Plane engine_is_burning_fuel[i]; GS_kt = groundspeed × 1.94384449244; weight_kg = total weight. The ground term models taxi thrust/ground effects of Zibo's taxi tuning; values exactly as given.) The X-Plane axis convention: faxil positive aft, so forward thrust is negative.

4.11 Systems — start switches, start valves, ignition, EICAS energise#

Runs at step 19 of the systems block (after Starter).

4.11.1 Start switch knob animation (per engine): an internal knob angle knob[i] (0 GRD, 1 OFF, 2 CONT) moves towards engine_starter_tgt[i] with B738_set_animation_rate(…, 0.15) (0.15 s per position) and produces the published starter_pos (0 GRD, 1 OFF, 2 CONT, 3 FLT) and starter_pos2 (OFF-detent helper, 1 at OFF):

if tgt == knob: pos = knob
elif tgt == 0: knob → 0 (range 0..1);  pos, pos2 = (knob >= 0.5) ? (1, 1) : (2*knob, 0)
elif tgt == 1:
    knob < 0.2:   knob → 1 (range 0..1); pos = 5*knob ; pos2 = 0
    knob < 1:     knob → 1 (range 0..1); pos = 1 ; pos2 = 1
    knob < 1.2:   knob → 1 (range 1..2); pos = 1 ; pos2 = Rescale(1, 1, 1.2, 0, knob)
    else:         knob → 1 (range 1..2); pos = Rescale(1.2, 1, 2, 2, knob) ; pos2 = 0
elif tgt == 2: knob → 2 (range 1..2); pos, pos2 = (knob >= 1.5) ? (Rescale(1.5,1,2,2,knob), 0) : (1, Rescale(1,1,1.5,0,knob))
(tgt == 3 has no branch: pos keeps what the command wrote)

Commands (on command begin; engine 2 symmetric): knob/eng1_start_left (SCI ENGINE_1_STARTER DEC): pos 1 → tgt 0; pos 2 → tgt 1; pos 3 → pos = 2, pos2 = 0, tgt = knob = 2 at once. knob/eng1_start_right (INC): pos 0 → tgt 1; pos 1 → tgt 2; pos 2 → pos = 3, pos2 = 0, tgt = knob = 3 at once. rotary/eng1_start_grd|off|cont|flt: pos = 0|1|2|3, pos2 = 0|1|0|0, tgt = same (the knob angle is not changed, so the next Systems frame re-animates pos from the knob for GRD/OFF/CONT). IGN select toggle_switch/eng_start_source_left/right steps B738DR_eng_start_source −1 (IGN L) / 0 (BOTH) / 1 (IGN R).

4.11.2 Per engine logic (pos, N2 = eng_N2_idx, lever = start lever, src = 1 for IGN L, 3 for BOTH, 2 for IGN R; ignition[i] 0 none, 1 left igniter, 2 right igniter, 3 both):

first frame: engine_on_old2 = burning ; engine_start_enable_timer = 0 ; fuel_selector_enable = 1
starter_engaged = false
pos GRD (0):
    starter_engaged = true
    if 55.3 < N2 < 58: engine_starter_tgt = 1            ← solenoid release: knob returns to OFF
    if lever > 0.5: ignition = airborne ? 3 : src
    if N2 < 50: egt_redline = 1 ; TMR_egt_redline = 0
pos OFF (1):
    ignition = 0
    if burning and lever > 0.5:
        if 50 < N2 < 57: ignition = 3
        if airborne and 5 < N2 < 54: ignition = 3        (auto-relight)
    if engine_start_auto option and (burning or N2 > 25):
        if altitude >= 18000 or flaps == 0: if engine anti-ice on: ignition = src
        else: ignition = src
pos CONT (2):  ignition = 0 ; starter_engaged = true ; if lever > 0.5: ignition = airborne ? (N2 < 54 ? 3 : 0) : src
pos FLT (3):   ignition = 0 ; starter_engaged = true ; if lever > 0.5: ignition = 3
engine fire, failure or separation (= 6): ignition = 0
restart lock-out:
    if not burning and engine_on_old2 and N2 <= 40: engine_start_enable_timer = 5 ; engine_on_old2 = 0
    elif lever < 0.5 and timer != 0: timer −= dt (to 0)       ← counts only with the start lever in CUTOFF
    if timer == 0: fuel_selector_enable = 1 ; if burning and N2 >= 54: engine_on_old2 = 1
    else: ignition = 0 ; if ENGN_running: ENGN_running = 0 (written) ; fuel_selector_enable = 0

4.11.3 Igniter power: left igniter of engine 1 from AC transfer bus 1 (engine 2: transfer bus 2), right igniters from the AC standby bus. If the battery bus (B738DR_dir_batbus_status) is unpowered: ignition = 2 if the selection includes the right igniter and AC standby is powered, else 0. Otherwise remove the unpowered igniter (3 → 2 or 1, 1 → 0, 2 → 0).

4.11.4 EGT start limit and hot start (per engine, ff = EICAS FF energised flag B738DR_eicas_ffN, egt = indicated EGT):

if ff == 0: TMR_egt_redline = 0 ; egt_redline = 0
elif N2 <= 59 or egt >= 725:
    if pos != GRD and N2 < 5 and egt_redline and TMR_egt_redline == 0: TMR_egt_redline = 4
elif egt_redline and TMR_egt_redline == 0: TMR_egt_redline = 2
B738DR_egt_redline1/2 = egt_redline        (start-limit red line shown on the EGT gauge)
hot_start_warn = 1 if egt > 585 and egt rising and N2 < 58 and burning;
    = 0 if not burning, or egt < 725 and falling (while burning); otherwise unchanged
      (above 725 C it stays set)   [corrected 2026-11-03 from zibomod Systems]
if hot_start_warn: egt_hotstart = (egt < 725) ? blink : 2 ;
                   if eng_flameout_timer == 0: eng_flameout_timer = generate_rnd(1, 3)
eec_hotstart (a latch; lever = start lever):
    = 0 if EEC failed (6) or airborne or lever == 0 or N2 < 23
    = 1 elif hot_start_warn and egt > 710 ; otherwise unchanged
    (oracle: wet start aborted at EGT 711 C, ignition off, N2 hangs at 29.6 %, EGT falls)
if eec_hotstart: ignition = 0                       (EEC auto-aborts a ground hot start)
B738DR_egt_hotstart1/2 = egt_hotstart

4.11.5 Ground-air start annunciation B738DR_air_starter (0…5; ASU connected and ext_air_duct > 25): with isolation valve ≥ 0.05: only engine 1 in GRD → 1 (packs off) / 4 (a pack on); only engine 2 in GRD → 2 / 5; both in GRD → 3; with isolation closed: engine 2 in GRD → 2 (right pack off) / 5.

4.11.6 Ignition to X-Plane:

real_ignition[i] = (N2 >= 25) ? ignition[i] : 0
if airborne and B738DR_wms and pos == FLT: real_ignition[i] = ignition[i]     (windmill start)
sim/cockpit2/engine/actuators/ignition_key[i] = real_ignition[i]   (written on change)
igniter_on[i] = 1 if real_ignition != 0 ; = 0 if real_ignition == 0 and starter-N2 < 54 ; else unchanged
auto_ignite_on[] forced 0

4.11.7 Start valves (laminar/B738/engine/start_valve1/2, 0…1, drive the START VALVE OPEN indication and the pneumatic load): if pos == GRD and the battery bus is powered → open linearly in 2.5 s (B738_set_anim_time(v, 1, 0, 1, dir 1, 2.5)), else close in 2.5 s; engineN_starter_valve = Anim_value(·, 0/1, 0, 1, 1) (first-order copy used by the pneumatics). The duct pressure drop caused by the starters and the APU bleed deficit while a start valve is open (−25 psi × (1 − apu_establish_time) when start_valve > 0.1) belong to the pneumatics spec.

4.11.8 EICAS energise (oil pressure/temperature/fuel flow readouts appear when an engine is started): energise[i] = starter_engaged or running or ignition != 0. If energised: TMR_energize += dt up to 4 s; < 1.6 s: oil pressure, oil temperature, FF readouts off; 1.6…2 s: oil pressure and FF on; ≥ 2 s: all on. If not energised: TMR_energize −= dt (only while N2 < 6, to 0); < 0.3 s: all off; 0.3…0.7 s: oil temperature off. Flags B738DR_eicas_oil_press1/2, _oil_temp1/2, _ff1/2.

4.12 Failures and flameout (manage_eng_failure, eng_flameout_effect, engine_fail)#

The EFB/instructor failures live in laminar/B738/failure/engine (70 floats, 1 = active). Engine-related indices:

Index Failure Effect in Zibo
0 / 1 engine 1/2 fire X-Plane rel_engfir0/1 = 6
2 / 3 engine 1/2 failure (flameout) if running: flicker timer generate_rnd(3, 7) s; X-Plane rel_engfai = 6; after generate_rnd(30, 90) s the failure clears itself (index and X-Plane failure back to 0)
4 / 5 engine 1/2 severe vibration only while not running and not failed: with probability 95 % (rnd(0,200) >= 10) a vibration phase of rnd(15, 240) s (eng_vib_active, N2 dips §4.8.7), counted down faster with higher vibration (dt × Rescale(0, r, 3.7, 1, vib) below 3.7 units, × Rescale(3.7, 1, 5, 3, vib) above; r = 0.5 below 50 s left, Rescale(50, 0.5, 150, 0, t) below 150 s, else 0), then (or immediately in 5 %) rel_engfai = 6
6 / 7 engine 1/2 separation / seizure X-Plane rel_engsep = 6; N1, N2 (X-Plane, internal and indicated) forced 0 every frame
8 / 9 generator 1/2 X-Plane generator failure (electrics spec)
10 / 11 oil pump 1/2 X-Plane rel_oilpmp = 6 → oil pressure 0
12 / 13 oil leak 1/2 X-Plane oil quantity ratio −0.00625/s (written)
14 / 15 EEC normal-mode inputs soft ALTN after 15 s, approach idle below 15000 ft (§4.3)
16 / 17 EEC failure X-Plane rel_fadec = 6 → EEC mode −1, EEC switch forced OFF
23 / 24 hung start N2 stopped at 50 % (§4.8.4); cleared when N2 ≤ 56 and not running
25 / 26 hot start start EGT coefficient 3 (§4.9); cleared like the hung start
27 / 28 bird strike 1/2 X-Plane rel_bird_strike_engN = 6

manage_eng_failure also turns sustained abnormal oil into seizure (index 6/7): oil temperature ≥ 165 °C while burning for 180 s cumulative, or oil pressure ≤ 13 psi while burning for 300 s cumulative (timers count back down at 1 s/s when the condition clears).

eng_flameout_effect (bird strike with engine failed and N2 > 45 → clear the X-Plane bird-strike failure, flicker timer generate_rnd(5, 10) s). While a flicker timer runs, laminar/B738/engine/eng_flameout[i] (exterior flame / sound effect) toggles: on for rnd(0.3, 0.7) s, off for rnd(0.5, 2.5) s.

engine_fail(t) (2 Hz, t = accumulated time since the last call ≈ 0.5 s):

  • N1 overspeed: timer += t above 103.5 % N1, += t × Rescale(102.4, 0.5, 103.5, 1, N1) from 102.4 % (engine 2: from 102.1 %, Zibo asymmetry), −= 0.33 t below; > 450 s → X-Plane engine failure rel_engfai = 6.
  • Suction-feed flameout (airborne, elevation > 8500 m, not start_sim_time): a running engine whose side has no operating boost pump on a non-empty tank (L1/L2/C1 for engine 1; with crossfeed open also R1/R2/C2, and symmetric) starts T = Rescale(8500, 120, 12500, 12, elevation_m) + 30 × rnd s; while 0 < T <= 10 the engine is forced off (ENGN_running = 0, written) and B738DR_fuel_selector_l/r = 0. Not running with the start lever in CUTOFF clears the timer.
  • Heavy rain / hail flameout: precipitation or hail > 0.7, or > 0.2 with −12 °C < TAT < 0: an unprotected running engine gets T = rnd(80, 180) × Rescale(0.2, 1.8, 1, 0.7, precip) s (+20 s if within 16 s of the other engine's timer) and is forced off while 0 < T <= 10. Protected = start switch CONT/FLT, or ≥ OFF when the auto-start option or OFF itself is selected — in effect only GRD is unprotected (Zibo's condition; engine 2 tests engine 1's switch for the OFF case, and its 16 s test compares its own timer with itself, so it always gets +20 s). Reproduce as written.

4.13 N1_N2_idle (2 Hz)#

When X-Plane's sigma, low_idle_ratio and high_idle_ratio exist, forces X-Plane's idle ratios to low_idle_ratio = 0.60, high_idle_ratio = 0.67 (re-written whenever they differ). Irrelevant for the MSFS rewrite except as a statement that X-Plane's idle is not used.

4.14 Reversers (engine side)#

  • Commands (SCI ENGAGE/DISENGAGE_REVERSE_1/2/BOTH, sim/engines/ thrust_reverse_toggle*, …_hold*; ignored while throttle_override): toggle: if prop_mode == 1 (normal) and thrust lever < 0.01 → prop_mode = 3, thrust lever 0; if prop_mode == 3, reverse lever ≤ 0.01 and not max-hold → prop_mode = 1, reverse lever 0. Hold: same engagement, sets reverse_max_enable/on (released on command end).
  • Reverse lever (laminar/B738/flt_ctrls/reverse_lever1/2, SCI REVERSE_THROTTLE_1/2): writing it runs reverse_levelN: lever 0 with prop_mode 3 → prop_mode 1 and X-Plane throttle 0; lever ≥ 0.06 with prop_mode != 3 → prop_mode 3; lever clamped to 0.06 while Zibo's own reverser state is not 3; in reverse, X-Plane throttle_ratio = Rescale(0.06, 0, 1, 1, lever).
  • Systems: Zibo's reverser state follows X-Plane prop_mode (stowing always, deploying only on ground or radio height < 10 ft) when hydraulic system A (engine 1; B for engine 2) or standby has pressure and the fire switch is not pulled. Sleeve B738DR_reverse_deploy (published as laminar/B738/engines/thrust_reverser_deploy_ratio): deploy 1.8 s / stow 2.0 s on system A/B, 9.6 s / 12 s on standby only (linear).
  • Thrust: reverse thrust only through Engine_model (§4.10, deploy ≥ 0.5, reverse_coef = 0.23, ×1.3 above 80 kt), and Thr_lever_to_use in reverse (§4.6, max_rev_thrust_use; Zibo's .data initial value 0.57 is replaced by 0.69 in Init_engine_param).

5 Constants, tables, timings#

5.1 Helper functions (exact semantics)#

  • Rescale(x0, y0, x1, y1, x): linear interpolation from (x0,y0) to (x1,y1), clamped to y0/y1 outside [x0, x1] (works for x1 < x0 too; returns y0 if x0 == x1).
  • Limit(x, lo, hi): clamp (if lo > hi and x > hi returns hi).
  • Anim_value(cur, tgt, lo, hi, k): c = Limit(cur); if tgt >= hi − 0.001 and c >= hi − 0.01 → hi; if tgt <= lo + 0.001 and c <= lo + 0.01 → lo; else c + dt · k · (tgt − c) without overshooting tgt, clamped.
  • Anim_linear_value2(cur, tgt, lo, hi, rate): same end snapping; else moves linearly by rate · dt towards tgt.
  • B738_set_animation_rate(cur, tgt, lo, hi, t): t <= 0 → 1. If tgt >= hi − 0.001 and cur >= hi − 0.01 → hi; if tgt <= lo + 0.001 and cur <= lo + 0.01 → lo; if cur > hi → hi; if cur < lo → lo; else moves by dt / t towards tgt without overshoot.
  • B738_set_anim_time(cur, tgt, lo, hi, dir, T): moves by dt/T · |lo − hi| towards tgt (down if dir == 0, up otherwise), clamped to [lo, hi].
  • generate_rnd(a, b) = a + (b − a) · (rand() % 100000) · 1e−5 (C rand(), seeded with srand(time) on the first frame).
  • Animate_physical3(cur, tgt, lo, hi, a&, p5, p6, p7, p8): second-order EGT follower used in §4.9 (state a = eng_accel_egt[i], called with p5 0.1, p6 100, p7 0.3, p8 the rate k); specified in §10.2 (M3c).
  • CFM56 building blocks (separate work package): cfm56_7b_cn1, cfm56_7b_cn1_to_cn2, cfm56_7b_cn2_to_cn1, cfm56_7b_cn1_to_cegt, cfm56_7b_cegt_to_cn1, cfm56_7b_thrust, cfm56_7b_fuel_flow, cfm56_7b_ff_thr, corrected_n1_to_cn1, corrected_n1_to_cn1_tat, corrected_cn1_to_n1, ff_corr_coef, corrected_ff2, calc_tamb_by_alt, calc_tat_by_mach. Call sites and arguments are those in §4.

5.2 Ambient and scaling#

Quantity Value
N2 100 % 14460 rpm (corrected N2 = N2 %·144.6/θ^0.45)
θ exponents N2 0.45, EGT 0.16
N2 limit (EEC) 15183 rpm corrected / θ^0.45
EGT limit (EEC) 948 °C + max(0, egt_rnd)
EEC N1 limit 90…104 % (104 in reverse, 105 with EEC failed)
max_thrust_use 1.12 (then the EEC limit), max_rev_thrust_use 0.69
N1 ↔ throttle N1 = 19.0758705 + 80.9172249 · u^(1/1.62)
Idle N2 floors ground 58.4 %, flight 72.1 %, approach 74.9 %, flight cap 79 %
Idle fuel flows into cfm56_7b_ff_thr ground 650, flight 440 (divided by ff_corr_coef)
Idle N1 floor 19.1 %
sigma clamp 0.235…1.0
Static thrust B738DR_static_thrust 27300 lbf (scaling (F+3700)·27300/27000−3700)
Reverse coefficient 0.23 (×1.3 above 80 kt)
Ground-idle hold after touchdown 5.5 s (2.5 s SFP)
EEC soft→hard ALTN 15 s
Starter knob 0.15 s per position; release window 55.3 < N2 < 58 %
Start valve travel 2.5 s
Restart lock-out 5 s with start lever in CUTOFF after the engine ran down below 40 % N2
Igniters to X-Plane only from 25 % N2 (FLT windmill start: any N2)
Metering valve 2 s full travel, opens at N2 > 19 % with fuel valves open
Fuel flow minimum (indication) 0.082177 kg/s; light-off surge start_ff + 0.01389 (≥ 0.09722)
Fuel flow floor to X-Plane 0.0415793 kg/s when burning and airborne
Starter torque/rpm to X-Plane 0.07 / 0.09
Windmill start envelope §4.7
Indicated N1/N2 lag Anim_value k = 1/s
Engine model flight-idle increment vs sigma σ 0.247144: 0.085, 0.310649: 0.035, 0.374827: 0.02, 0.448579: 0.02, 0.533304: 0.02, 0.629423: 0.034, 0.738808: 0.05, 1.0: 0.12 (flt_idle_* datarefs; linear between, clamped)
Engine model .data defaults sfc 0.038, idle_sfc 0.178, sfc_coef 0.008, bypass 0.8, inlet 0.8, shock_mach 0.9, curve02_coef1/2 2.6/2.75 (used by the inactive branches 2/3/else only)

5.3 Starter#

Quantity sigma 0.635 sigma 1.0
Starter N2 rate r_start (%/s) 1.10 1.20
Post-light-off N2 rate r_lightoff (%/s, ×0.9 in heavy rain) 1.45 1.55
Max motoring N2 n2_motor (%) 26.1 29.6

Duct pressure factor 0 at 10.5 psi → 1 at 21.6 psi. Ground air (ASU) motoring N2 29.6 % at ≥ 21.6 psi ext_air_duct. Pack load loss 0…0.33 (one pack) …0.40 (two packs); other starter via the isolation valve 0.42. Approach factor: N2 rises at the full rate to 94.6 % of the target, then at 2.5 % of the rate.

5.4 Spool-down tables (time × 1.3 s)#

N2 % 59.8 40.7 37.5 32.1 25.9 22.3 18.6 14.8 10.4 4.2 1.7 0
t (s, before ×1.3) 0 6 7 10 15 19 24 31 43 62 70 103
N1 % 19.2 12.1 11 9 7 5.9 4.9 4 3 2 1.7 0
t (s, before ×1.3) 0 6 7 10 15 19 24 31 43 62 70 106

Above the first column: N2 → 100(n/100)^3.11, rate 0.012 s/% (≥ 33) or 0.04 s/% (< 33), back with exponent 1/3.1; N1 the same rates directly. A value of exactly 0 maps to the end of the table.

5.5 Windmilling N2/N1 (non-running engine), Mach 0.0 … 0.9#

N1 (fan) % \ Mach 0 .1 .2 .3 .4 .5 .6 .7 .8 .9
0 ft 0 7.51 10.96 13.45 16.13 18.34 20.52 22.30 21.98 22.28
15000 ft 0 7.13 10.41 12.77 15.32 17.43 19.50 21.19 23.13 23.45
25000 ft 0 6.87 10.03 12.30 14.76 16.78 18.78 20.41 22.27 22.59
35000 ft 0 6.60 9.63 11.81 14.17 16.11 18.03 19.59 21.39 21.69
45000 ft 0 6.34 9.25 11.34 13.60 15.47 17.31 18.81 20.54 20.82
N2 (core) % \ Mach 0 .1 .2 .3 .4 .5 .6 .7 .8 .9
0 ft 0 0 3.64 6.95 10.27 12.08 15.24 17.23 19.83 20.12
15000 ft 0 0 3.45 6.59 9.72 11.44 14.43 16.32 18.78 19.06
25000 ft 0 0 3.32 6.33 9.34 10.99 13.87 15.68 18.04 18.31
35000 ft 0 0 3.18 6.06 8.95 10.53 13.28 15.02 17.28 17.54
45000 ft 0 0 3.05 5.82 8.58 15.47 17.31 18.81 20.54 20.82

The 45000-ft N2 row repeats the 45000-ft N1 values from Mach 0.5 on (Zibo data, reproduce). Altitude interpolation as described in §4.8.6; the head-wind Mach replaces Mach in the first band (fan N1 only: the fan windmills in wind on the ground).

Caution (reading of the interpolation): between 15000 and 25000 ft Zibo interpolates the row pair (15000, 25000) with Rescale(0, row15k, 25000, row25k, h), i.e. the interpolation variable starts at 0 ft, not at the lower band edge. Reproduce literally.

5.6 Start EGT tables (N2 % → ΔEGT °C)#

N2 % 28.1 29.3 30.1 32.1 34.1 36.1 38.1 42.1 44.1 51.1 55 57 58.1 59.4 59.7
calc_start_egt (subtracted to get the base) 0 2 30 60 95 133 165 193 219 251 311 335 345 361 366
egt_start (added, × coef_egt) 0 2 30 52 74 96 118 162 184 261 305 327 338 349 360

Below 28.1 %: 0; above 59.7 %: the last value. Motoring EGT approach: TAT + Rescale(28, 0, 29.7, 5 (not running) / 4 (running, fuel closed), N2), capped at TAT + 123 − egt_rnd.

5.7 Vibration, oil, EICAS#

Vibration targets (1/100 units): N1 0 → 0, 10 → 181, 37 → 121, 105 → 147; event ×3 (≤ 500); lag k = 3/s. Oil pressure breakpoints (psi above ambient): N2 0 → 0, 20.2 → 15.7, 30.7 → 20.7, 38.2 → 27.7, 58.9 → 39.7, then +1.3223529 psi/%. Oil temperature map 31 °C (X-Plane) → 69 °C, slope 0.66 above; low-oil overheat to 200 °C at 0 psi. EICAS energise 1.6 s / 2.0 s / 4 s (§4.11.8). Start EGT red line timers 2 s / 4 s. Hot start warning above 585 °C rising, auto-abort above 710 °C on ground, 725 °C hard.

6 Outputs#

6.1 Zibo datarefs (read by Lua, displays, SCI)#

Dataref Meaning SCI state
laminar/B738/engine/starter1_pos, starter2_pos, …_pos2 start switch 0…3, OFF detent helper ENGINE_STARTER1/2_POS, _POS2
laminar/B738/toggle_switch/eng_start_source IGN select −1/0/1 TOGGLE_SWITCH_ENG_START_SOURCE
laminar/B738/engine/mixture_ratio1/2 start levers ENGINE_MIXTURE_RATIO1/2
laminar/B738/engine/start_valve1/2 start valve 0…1 → EICAS START VALVE OPEN (display)
laminar/B738/engine/starter, engine_air_starter any starter air / ground-air start code 0…5 –
laminar/B738/engine/indicators/N1_percent_1/2, N2_percent_1/2 indicated N1/N2 (lagged) (display)
laminar/B738/engine/eng1_egt, eng2_egt EGT °C (display)
laminar/B738/engine/eng1/2_oil_press, eng1/2_oil_temp oil psi, °C (display)
laminar/B738/engine/fuel_flow_kg_sec[0..1] indicated fuel flow (display)
laminar/B738/engine/eicas_oil_press1/2, eicas_oil_temp1/2, eicas_ff1/2 readouts energised (display)
laminar/B738/engine/idle_mode[0..1] 0 ground, 1 flight, 2 approach (display: no direct SCI)
laminar/B738/engine/indicators/eng1/2_vib vibration units (display)
laminar/B738/engine/rot_ang0/1, rot_spd0/1 fan angle (deg) / speed ENGINE_ROT_ANG0/1
laminar/B738/engines/thrust_reverser_deploy_ratio[0..1] sleeves (model animation)
laminar/B738/engines/actuators/prop_mode, laminar/B738/engine/prop_mode_sync reverser state SIM_COCKPIT2_ENGINE_ACTUATORS_PROP_MODE_0/1 (X-Plane name)
laminar/B738/engine/eng1_req_n1, eng2_req_n1, …_req_pie, thr_lvr1/2 commanded N1 / lever (Lua, EICAS) –
laminar/B738/engine/eng1/2_N1_lim, _lim2 N1 limit bugs in throttle-use units –
laminar/B738/engine/fuel_selector_l/r 0 during a forced flameout –
B738DR_egt_redline1/2, B738DR_egt_hotstart1/2, B738DR_xbld_start1/2, B738DR_wms, B738DR_eng_flameout[0..1] EGT start limit, hot start blink, X-BLD / windmill start cues, flame effect –
laminar/B738/engine/ignition1/2, eng1/2_start_disable registered but never written (always 0 in 2.S1.50) –
laminar/B738/failure/engine[…] cleared by the logic (hung/hot start, timed failure) –
zibomod/engine_model/* model parameters (writable, §5.2) –

6.2 Simulator outputs (X-Plane datarefs Zibo writes → MSFS meaning)#

X-Plane dataref When MSFS equivalent / use in the rewrite
sim/flightmodel/engine/ENGN_N1_, ENGN_N2_ every frame (Starter) the engine N1/N2 the simulator must show: drive MSFS N1/N2 (§7.3)
sim/flightmodel/engine/ENGN_thro_use every frame (Eicas_thrust) internal (commanded throttle use)
sim/flightmodel/engine/ENGN_FF_ + override_fuel_flow = 1 every frame (Engine_model) fuel burn: MSFS fuel flow must equal this (§7.3)
sim/flightmodel/forces/faxil/fnrml/fside_prop, L/M/N_prop + override_engine_forces = 1 every frame thrust force/moment on the airframe (§7.3)
sim/cockpit2/engine/actuators/ignition_key[i], igniter_on[i], auto_ignite_on[i] = 0 on change light-off model input (§7.1)
sim/cockpit2/engine/actuators/mixture_ratio[i] on change light-off / fuel-on input (§7.1)
sim/flightmodel/engine/ENGN_running[i] = 0 restart lock-out, flameouts forces "not running" (§7.1)
sim/aircraft/engine/acf_starter_torque_ratio = 0.07, acf_starter_max_rpm_ratio = 0.09 every frame none (keeps X-Plane's starter inert)
sim/aircraft2/engine/low_idle_ratio = 0.60, high_idle_ratio = 0.67 2 Hz none
sim/cockpit2/engine/actuators/fadec_on[i] EEC failures EEC state (internal)
sim/cockpit2/engine/actuators/prop_mode[i], throttle_ratio[i] (reverse) reverser commands reverser state; MSFS reverser via THROTTLEn_SET negative range or K events — not used for thrust since forces are overridden
sim/operation/failures/rel_* (fire, engfai, engsep, fadec, oilpmp, bird strike, generators) failures internal failure state
sim/flightmodel/engine/ENGN_oil_quan oil leak failure internal oil quantity (§7.2)

7 X-Plane-provided behaviour to supply, and the MSFS coupling#

7.1 Light-off and "running" (X-Plane's engine logic)#

Zibo never sets ENGN_running = 1 (except at flight start with engines running); it only drives the inputs X-Plane uses and forces it to 0. In X-Plane a turbofan becomes running/burning when it has fuel (mixture > 0, tank selector with fuel, no fuel-starvation), an igniter is on, and the core turns fast enough; it stops when the mixture goes to 0, fuel runs out or the engine fails. X-Plane's exact threshold and delay could not be measured here (the oracle has no X-Plane engine; §9). Because Zibo only energises the igniters from 25 % N2 (real_ignition, §4.11.6) and its own N2 is written into X-Plane every frame, the observable rule in Zibo+X-Plane is:

burning[i] becomes 1 when: mixture_ratio[i] > 0 (Zibo's lagged start lever)
                           and ignition_key[i] != 0 (i.e. N2 >= 25 %, or FLT windmill start)
                           and fuel available on that side (fuel spec)
                           and no engine failure/separation/fire-out
burning[i] becomes 0 when: mixture_ratio[i] == 0, or fuel starvation, or rel_engfai/engsep,
                           or Zibo forces ENGN_running = 0 (lock-out, flameout timers)
running[i] = burning[i]   (X-Plane keeps both equal for a jet)

Recommendation: implement this as a small "SimEngine" model inside src/core (the part of X-Plane the engine logic depends on), with the threshold "mixture > 0" taken literally (Zibo's mixture lag reaches 0.63 after 1 s). Verify against X-Plane once (§9: measure light-off delay with a real X-Plane session) and keep the rule in one function so it can be tuned.

7.2 Oil temperature and quantity#

Engine_calculation derives the indicated oil temperature from X-Plane's oil temperature (oil_temperature_deg_C, X-Plane's own heat model) and fades oil pressure with X-Plane's oil quantity ratio (ENGN_oil_quan, starts at the acf value, decreased only by Zibo's leak failure). The rewrite must supply both: oil quantity = 1.0 per engine (reduced by the leak failure); oil temperature: recommended first-order model towards an N2-dependent equilibrium tuned to X-Plane's behaviour (measure in X-Plane: cold soak → idle → take-off), or the MSFS GENERAL ENG OIL TEMPERATURE of the MSFS engine model (which is running in parallel, §7.3). Until measured, use MSFS's.

7.3 MSFS coupling: making MSFS thrust follow Zibo's N1#

Zibo computes thrust in lbf/N itself (Engine_model, §4.10) and replaces X-Plane's engine forces. In MSFS 2024 the rewrite has three options:

  1. Own forces (recommended, closest to Zibo): compute Fx[i] exactly as §4.10 and apply it through the MSFS flight model: set the MSFS engines' thrust to Zibo's value with TURB ENG THRUST is read-only, so use the 2024 ENGINE CONTROL / external force interface if available to WASM (SimConnect SimObject force application is not exposed for the user aircraft in 2024 — to be confirmed in M3), otherwise option 2.
  2. Closed-loop N1 follow (practical default): keep MSFS's turbine engine model running with an FDE whose static thrust and N1 table approximate the CFM56-7B26 (engines.cfg: static_thrust 26300 lbf, N1/thrust table from cfm56_7b_thrust), and drive the MSFS throttle per engine every frame with a PI controller so that TURB ENG N1:i tracks Zibo's engine_N1_anim (feed-forward: throttle = n1_to_thr_use(N1_zibo) mapped through the FDE's N1-vs-throttle table; feedback on the N1 error with a time constant well below Zibo's spool rates, i.e. MSFS's own spool lag must be shorter than Zibo's: set engines.cfg n1_and_n2_spool_up / spool_time fast). Thrust then follows N1 through MSFS's table; compare the resulting thrust with Fx in the differential test and tune the FDE table until they match within 2 %. Reverse: MSFS reverser state from prop_mode, reverse throttle range from the reverse lever.
  3. MSFS TURB ENG N1 override: write TURB ENG CORRECTED N1/TURB ENG N1 directly (writable in MSFS 2024 for some engine types when the engine is "simulated externally"); only if option 1 is unavailable and option 2 cannot reach the required accuracy.

Independently of the option:

  • Combustion/start: start and stop the MSFS engine with the §7.1 rule (GENERAL ENG COMBUSTION:i follows Zibo's burning[i]; use ENGINE_AUTO_START-free paths: set GENERAL ENG STARTER/TURB ENG IGNITION SWITCH EX1 and the mixture/fuel valve so MSFS lights exactly when Zibo's rule says; force TURB ENG N1/N2 during the start from Zibo's values so MSFS never runs its own starter dynamics).
  • Fuel flow: MSFS must burn ENGN_FF_[i] (kg/s) — set the FDE fuel flow scalar to 0 and remove fuel from the tanks in the fuel subsystem with Zibo's flow, or tune fuel_flow_scalar so TURB ENG FUEL FLOW PPH equals Zibo's flow (verify in the differential test).
  • Indications never come from MSFS: all displayed engine values are Zibo's (§6.1).
  • Exterior fan: L:LU737_fan_angle_1/2 from Zibo's rot_ang0/1 (replaces the old gauge integration from MSFS N1).

8 Differential test scenarios#

All scenarios fix dt = 1/30 s, ISA sea level unless stated, set the simulator inputs every frame (oracle set), and compare the outputs of §6.1 with the values below (tolerance: N1/N2 0.05 %, EGT 1 °C plus the random offset ±5 °C, fuel flow 0.001 kg/s, timings ±1 frame). Random numbers: the oracle seeds rand() with time(), so egt_rnd (±5 °C) and eng_diff_stab[0] (0.92…1) differ per run; the comparison must either read them back from the oracle (not exported today) or allow ±5 °C on EGT and compare engine 2 for sub-33 % acceleration timing.

8.1 Ground start on ground air (oracle script, measured)#

xphost/test/sci/start_spec_eng1_ground.json (12/12 checks pass): cold & dark, battery + GPU, ASU connected (laminar/B738/engine/engine_air_start = 1 held → ext_air_duct 48 psi), isolation valve open (AUTO, packs off), engine 1 start switch DEC → GRD at t ≈ 13 s; start lever IDLE at t ≈ 28 s; X-Plane light-off emulated at t ≈ 37.3 s (N2 ≈ 27.3 %) by setting ENGN_running[0] = engine_is_burning_fuel[0] = 1; start lever CUTOFF at t ≈ 120 s. Inputs held: sigma 1, rho 1.225, TAT = OAT = 15 °C, 101325 Pa, 29.92 inHg, X-Plane oil temperature 15 °C, oil quantity 1. Measured (indicated values; xN2 = value written to X-Plane):

t (s) Start sw Start valve N2 % N1 % xN2 % EGT °C Oil psi FF kg/s ign key Note
12.4 OFF 0 0 0 0 17.4 0 0 0 cold (EGT = TAT + egt_rnd)
17.0 GRD 1.0 4.21 0.01 5.36 17.4 0 0 0 N2 +1.20 %/s
27.0 GRD 1.0 16.20 0.35 17.36 17.4 0 0 0
36.3 GRD 1.0 27.28 1.77 28.01 17.4 4.38 0 2 at 94.6 % of 29.6 % motoring; IGN R from 25 %
38.3 GRD 1.0 28.54 2.06 29.59 20.5 4.97 0.065 2 lit: +1.55 %/s, FF surge
42.3 GRD 1.0 34.30 3.69 35.79 125.7 9.37 0.082 2
48.3 GRD 1.0 43.60 7.72 45.09 270.8 16.13 0.082 2
54.3 GRD 1.0 52.90 14.01 54.39 415.4 21.52 0.094 2
56.3 OFF 0.83 56.00 16.70 57.49 464.2 23.32 0.101 3 released at 55.3 %; OFF + 50…57 % → both igniters
58.3 OFF 0.03 59.10 19.73 60.59 460.0 25.26 0.098 0
60.3 OFF 0 60.43 20.48 59.78 451.3 27.03 0.092 0 overshoot to idle + 3 then gauge
66.3 OFF 0 59.78 19.27 59.78 438.1 26.16 0.082 0 stabilised idle N1/N2
117.3 OFF 0 59.78 19.27 59.78 430.5 26.16 0.082 0 idle EGT settles
121.4 OFF 0 52.31 15.70 49.98 281.6 21.18 0 0 1.3 s after CUTOFF + X-Plane stop
133.4 OFF 0 30.17 8.13 29.23 178.5 5.75 0 0 spool-down table
149.4 OFF 0 18.73 4.85 18.26 149.8 0 0 0
177.4 OFF 0 9.61 2.84 9.37 125.7 0 0 0

Oil temperature: 33.4 °C throughout (X-Plane 15 °C mapped by §4.9). EICAS oil pressure/FF readouts energised 1.6 s after GRD. The oracle without the ambient set runs at sigma 0, TAT 0 °C, 0 Pa (host does not synthesise weather): N2 then rises at 1.10 %/s and the oil pressure offset is 0; always set the ambient inputs in engine scenarios.

8.2 Further scenarios (to script in M3)#

  1. Start on APU bleed (needs the pneumatics spec's APU bleed conditions: APU running, APU bleed switch ON, bleedAir_apu_psi > 8, apu_duct_enable): duct pressure 10.5–21.6 psi scales the motoring N2 (26.1…29.6 %); with both packs on the target drops by 40 %.
  2. Cross-bleed start (engine 2 running at ≥ 75 % N2 via isolation valve, engine 1 GRD): motoring from f(duct_R) · n2_motor · iso.
  3. Start lever IDLE before 25 % N2: ignition key stays 0 until 25 %; mixture lag 1/s; light-off at 25 % by the §7.1 rule.
  4. Hung start (laminar/B738/failure/engine[23] = 1): N2 stops at 50 %; failure cleared after shutdown below 56 % N2.
  5. Hot start ([25] = 1): coef_egt → 3, EGT > 585 rising → egt_hotstart blinks; > 710 °C on ground → EEC abort (ignition 0).
  6. Thrust lever steps at idle → 0.25 → 1.0 → 0, on ground, sigma 1: N1 command, ENGN_thro_use, N1/N2/EGT/FF/thrust over time (acceleration 2 %/s below 33 %, 16.7 %/s above 40 %; deceleration 6.25/4 %/s).
  7. Idle modes: flaps 15 / engine anti-ice / airborne → approach idle N2 ≥ 74.9 %; touchdown → ground idle after 5.5 s.
  8. EEC: EEC switch to ALTN (fadec_on = 0) → mode 0 N1 limit; EEC input failure [14] → soft ALTN after 15 s, lever to idle → hard ALTN 15 s later.
  9. In-flight windmill: engine 1 CUTOFF at FL350 M0.78 → N1/N2 follow the §5.5 floors; FLT start above the X-BLD speed band with B738DR_wms.
  10. Restart lock-out: shutdown, then start lever IDLE immediately → no ignition for 5 s with the lever back in CUTOFF.
  11. Reverser: on ground, ENGAGE_REVERSE_1, reverse lever 1.0 → sleeve 1.8 s, faxil_prop sign change, ×1.3 above 80 kt.
  12. Failures: overspeed timer (N1 103.5 % for 450 s), oil leak, engine failure auto-clear (30–90 s), separation → N1/N2 0.

Each runs on the oracle with --sci-test and on the native build of wasm/src/core with the same inputs; the comparison uses the SCI state names and the Zibo datarefs listed in §6.1.

9 Open questions#

  1. X-Plane light-off rule (§7.1): exact N2 threshold, fuel-flow build-up and delay of ENGN_running/engine_is_burning_fuel in X-Plane 12 with Zibo's inputs. Needs one measurement in real X-Plane (igniter on at 25 % N2, mixture lag): log ENGN_running, ENGN_N2_, mixture_ratio per frame.
  2. X-Plane oil temperature model feeding §4.9 (not part of Zibo).
  3. Animate_physical3: resolved in M3c, §10.2.
  4. thr_use_to_n1(u, -1): Zibo reads rel_eng_ice[-1] (the word before the array) in Thr_shift; in practice the memory holds another float. Decide whether to reproduce (read the neighbouring global) or use 0 ice; the result is capped at 1.12 anyway.
  5. Wind in the windmill N1 band 1 uses a Mach breakpoint 0.01 instead of 0 in the lowest altitude band only — reproduce, but confirm intended.
  6. Precipitation input scaling: X-Plane precipitation_on_aircraft_ratio (0…1) vs MSFS AMBIENT PRECIP RATE/state: a mapping must be chosen.
  7. MSFS force injection for option 7.3.1: whether a WASM module can apply an external force to the user aircraft in MSFS 2024; otherwise option 7.3.2 with an FDE tuned against Fx.
  8. Random numbers: the oracle seeds with wall-clock time; for frame-exact differential tests a hook to fix the seed (or export egt_rnd, eng_diff_stab) would be needed in xphost (not modified here).
  9. One-frame latency (App. A.8): Starter/Engine_calculation use the ignition and start valve computed by Systems in the previous frame; the rewrite must keep the order to stay frame-exact.
  10. Ground thrust term in Engine_model (adds up to 11800 N at 40–50 % N1 on the ground and removes thrust below 35 % N1 depending on weight and groundspeed): Zibo's taxi tuning for X-Plane's ground model. With MSFS ground physics it may need retuning (fidelity note, not a logic change).

10 Corrections and details found in M3c (implementation, oracle trace)#

Read from the disassembly where the sections above were open or approximate; the rewrite (wasm/src/core/engine/) follows these and matches the oracle frame by frame (xphost/test/sci/start_trace.json). The helpers are compared bit for bit with the original functions (wasm/test/test_engine_zibo.cpp).

10.1 Helpers (§5.1)#

  • Anim_value: the end snapping compares in double: `(double)tgt >= hi − 0.001 and (double)cur

    = hi − 0.01(likewise atlo); otherwise v = sim_period·k·(tgt − c) + c, clipped to tgtwhen it overshoots, thenLimit(lo, hi)`.

  • B738_set_anim_time(cur, tgt, lo, hi, dir, T): step = sim_period / T · |lo − hi|; dir == 0: only moves down if cur > tgt (not below tgt), then raised to lo; otherwise only moves up if tgt > cur (not above tgt), then capped at hi.
  • n2_shutdown(n, floor): above 59.8 % the N2 is converted to (float)(100·(n·0.01)^3.11) (double pow), moved by B738_set_animation_rate(x, 0, 0, 108, 33 > x ? 0.04 : 0.012) and converted back with exponent 1/3.1. At or below 59.8 %: the elapsed time is interpolated on the table (0 at ≥ 59.8, end time at exactly 0), sim_period added, the value read back at the new time (0 at or after the end, 59.8 at time 0). The result is raised to floor. n1_shutdown(n) is the same with the N1 table and no conversion (B738_set_animation_rate directly above 19.2 %); its "time 0" value is 59.8 (copied from the N2 function; unreachable).
  • calc_start_egt(N2, EGT) = EGT − table(N2), egt_start(N2, base, i) = base + scaled table(N2); the table value is the first/last breakpoint outside 28.1 … 59.7 % and the Rescale of the segment inside. In egt_start the breakpoints are multiplied by coef_egt[i] before interpolating (not the interpolated value). The coefficient update: m = Rescale(0, 1, 1, 4, max(thrust lever, reverse lever)), 3 with the hot-start failure (was_hot_start set); target Limit(m·Rescale(ws_lo, 0, ws_hi, ws, N2), 1, 4); coef = Anim_value(coef, target, 1, 4, 2 if target ≥ coef else 1), then Limit(1, 4).
  • ff_corr_coef(alt, OAT): d = OAT − calc_tamb_by_alt(alt), Rescale(0, Rescale(−20, 0.956, 20, 1.044, d), 36000, Rescale(−20, 0.946, 20, 1.054, d), alt); corrected_ff2(ff, alt, OAT) = ff_corr_coef(alt, OAT)·ff.
  • thr_use_to_n1(u, i): ice factor (float)(1 − 0.65·(double)(ice²)), at least 0.3; cap 1.12·ice (in double) for i < 0, else max_thrust_use[i]·ice; u = Limit(u, 0, cap) (0 if ≤ 0); result u^(1/1.62f)·(99.9930954 − 19.0758705) + 19.0758705 (std::pow(float, float)). n1_to_thr_use(n, i): x = (Limit(n, 19.0758705, 108) − 19.0758705)/80.9172… (0 if ≤ 0), Limit(x^1.62, 0, cap).

10.2 Animate_physical3(c, t, lo, hi, a, p5, p6, p7, p8)#

old = a. Desired rate w = (t − c)·p5 / sim_period; if a == 0 and t > c, w is at least ((t − c)·p7·sim_period + c − c) / sim_period. a += (w − a)·(t ≥ c ? p7 : p8)·sim_period, a = Limit(a, −p6, p6), result r = a·sim_period + c. Then:

  • if t > 550 or not plane_on_ground: a = 0, first order instead: r = (t − c)·(t ≥ c ? p7 : p8)·sim_period + c, not past t;
  • elif old ≥ 0 and a < 0: a = 0, the same first-order step (when falling, not below t);
  • elif r > t + 100 and a > 0: a = (float)(a − sim_period·0.5·a) (double), r = a·sim_period + c;
  • elif a < 0 and t > r: r = t, a = 0. Finally r limited to [lo, hi].

10.3 Engine_calculation (§4.9)#

  • Indicated fuel flow, in 0.01 kg/s inside the animations (ff·100, range 0…1000, result ·0.01 in double): separated or not running → 0; metering valve < 0.99 (double) → Anim_value(…, 0, k 1); choke == 1: target (float)(start_ff + 0.01389), at least 0.09722, B738_set_animation_rate 0.154 s per unit, choke = 2 when the result equals the target; choke == 2: target start_ff (at least 0.082177), 0.333 s, choke = 0 on equality; else target X-Plane's fuel flow, below 58.5 % N2: Rescale(46.2, start_ff, 58.5, start_ff + 0.02778, N2) above 46.2 %, at least 0.082177; Anim_value(…, k 1). engine_running_old is updated after the engine-lost test.
  • "N2 rising" everywhere in the EGT logic is engine_N2_anim ≥ eng_N2_old (written to X-Plane last frame; eng_N2_old = engine_N2_anim at the end of the function); N2 is eng_N2_idx.
  • EGT target (default TAT): not running: the motoring rise Limit(Rescale(28, 0, 29.7, 5, N2) + EGT − rnd, TAT, TAT + 123 − rnd) only with fuel closed, 28 < N2 ≤ 29.7 and N2 rising; running with N2 < 56.5 and X-Plane FF < 0.1: (a) fuel closed or (N2 < 28 and FF < 0.0279): the same rise with 4 instead of 5 (same conditions), else TAT; (b) else N2 < 28 or falling: TAT; (c) else (lit, rising): motoring_eng_egt = max(TAT, calc_start_egt(N2, EGT)) once, target max(egt_start(…), EGT − rnd); otherwise gauge_egt. coef_egt relaxes to 1 (Anim_value k 1) in every branch except (c). Then max(target, TAT − rnd, −rnd) + rnd, at least TAT.
  • ws_coef_egt (not running): fuel open: += sim_period·Rescale(225, 0.25, 625, 0, N2²) while < 3; fuel closed: −= sim_period·0.03 (N2 ≥ 25) or 0.01 (below) while > 1 (double), then Limit(1, 3). With the start lever at IDLE from the beginning of a start this reaches 3 (wet start): the start EGT triples, hot start, EEC ignition cut-off above 710 °C.
  • EGT update: EGT > target and not burning: Anim_value(EGT, target, 0, 1000, k) with k = Rescale(180, 0.0141c, 900, 0.2c, EGT) above 180 °C, Rescale(100, 0.0009c, 180, 0.0141c, EGT) below (products in double); elif N2 < 56.5, rising, target > TAT and target ≥ EGT: Anim_value with k = 0.00155c (fuel closed, 28 < N2 ≤ 29.7, rising) or 200; else Animate_physical3(EGT, target, 0, 1000, eng_accel_egt, 0.1, 100, 0.3, k) (same k). The acceleration is reset to 0 when the second-order branch was not used.
  • Fuel spar and engine valves (computed in Annunciators, i.e. after Engine_calculation): with the fire handle in and the hot battery bus powered: spar valve target 1 once the start lever has been above 0.5 for more than 0.5 s (delay counted with sim_period), engine valve target 1 with the lever above 0.5 unless eec_hotstart; both B738_set_animation_rate(…, 2).

10.4 Systems start part (§4.11)#

  • Knob animation (§4.11.1): the positions are Rescales, i.e. clamped: toward GRD pos = Rescale(0, 0, 0.5, 1, knob) below 0.5 (else 1); toward OFF from below 0.2 pos = Rescale(0, 0, 0.2, 1, knob) — the solenoid release therefore shows OFF on the first frame; from 1.2 … 2 Rescale(1.2, 1, 2, 2, knob); toward CONT from 1.5 Rescale(1.5, 1, 2, 2, knob). tgt == knob: pos = knob (also 3). The OFF-detent helper pos2 differs per engine: engine 1 uses Rescale(0, 0, 0.5, 1, knob) (toward GRD) and Rescale(0, 0, 0.2, 1, knob) (toward OFF), engine 2 Rescale(0.5, 0, 1, 1, knob) and Rescale(0.2, 0, 1, 1, knob). The test 0.2 > knob (and 1.2 > knob) is done in double.
  • igniter_on (§4.11.6) is cleared when real_ignition == 0 and the indicated N2 (eng_N2_idx) is below 54 %.

Appendix A: per-frame call order of MainFlightLoopCallback#

MainFlightLoopCallback(elapsed, …) is zibomod's only flight loop; it returns -1.0 (called every frame). Derived from the disassembly of the function (0x164776, 111 KB) by following the branches around each call, cross-checked with its decompilation; conditions are given as Zibo tests them. Calls into pure helpers (Rescale, Limit, Anim_value, string handling, XPLMGet/SetData*) are omitted. Subsystems other than the engine keep their own internal order; this appendix is the frame skeleton every subsystem spec refers to.

A.1 Frame prologue (every frame)#

  1. If first_time == 0: Init_variables() (once per plugin start).

  2. Reset sim_bus_load[0..2] = 0 (each subsystem adds its electrical load during the frame; written to X-Plane at step A.4.6).

  3. Livery change check; on change livery_checked = 0; then once check_model(), Compare_crc() (model identification / protection).

  4. Interop datarefs of other plugins (JAR ground handling, AviTab, BetterPushback, Honeycomb, SAM jetway power, JBR options, developer tuning). Not part of the systems logic.

  5. B738DR_load_save ← laminar/B738/...load_save (EFB "save/load situation" state, values 3 and 4 = loading).

  6. If first_time_loaded == 0 (first frame after an aircraft/flight load; XPluginReceiveMessage resets it to 0 on plane-loaded/airport-loaded):

    • read simDR_int_ver (X-Plane version);
    • sim_period = 0.01;
    • force X-Plane bus_battery_on[] = 1;
    • read sim/aircraft2/engine/low_idle_ratio and high_idle_ratio;
    • Init_engine_param() (engine model parameters, §4.1);
    • srand(time); read laminar/B738/release_ver;
    • Read_B738_dat() if the B738X data file is pending.

    Else sim_period = elapsed (the X-Plane frame time).

  7. Clamp: if sim_period > 0.1 → 0.1; else if sim_period <= 0 → 0.01.

  8. If any of the handles sim/time/paused, sim/time/is_in_replay or Zibo's own offset dataref is missing → skip everything up to A.7 (return −1).

  9. Read_Xplane_DR() — reads all X-Plane datarefs used by the plugin into simDR_* globals (engine-related ones listed in §2.2).

  10. Read_B738_DR() — reads datarefs of the XLua side (laminar/B738/... owned by Lua) into B738DR_* globals.

A.2 Per-frame preparation#

  1. If load_save is 3 or 4: pause_load_flt = 1. If first_time_loaded == 0: copy TAS/speed of sound/TAT for the displays; unless loading a situation, set X-Plane IAS mode and MCP speed 100 kt (update_mcp_speed_dial); take the reverser state (B738DR_reverse_deploy/thrust) from X-Plane.
  2. init_flght_ctrl(), update_flght_ctrl(), TerrainRadar_warning(), err_debug().
  3. Read FMS datarefs owned by Lua (legs, holds, approach indices, transition altitude, …) and the current barometer (pow for pressure altitude).

A.3 Flight start (once per loaded flight)#

If simDR_pause == 0 and first_time_loaded == 0:

  1. egt_rnd[i] = Limit(generate_rnd(…) − 5, −5, 5) for each engine: a random per-engine EGT offset of ±5 °C used by the EGT model (§4.9).
  2. Idle_mode_start_flight() (§4.2) — engine state at flight start.
  3. In this order: Glareshield_load_plane, Glareshield_start_flight, Annunciators_load_plane, Annunciators_start_flight, Anti_ice_load_plane, Anti_ice_start_flight, Comm_load_plane, Comm_start_flight, Chrono_load_plane, Chrono_start_flight, Fire_load_plane, Fire_start_flight, Switches_load_plane, Switches_start_flight, Lighting_load_plane, Lighting_start_flight, Cab_lighting_start_flight, Systems_start_flight, WXR_start_flight, calc_position_startup.
  4. Clear and publish the failure arrays (laminar/B738/failures/...: electrical, hydraulic, system, pressurisation, engine; 70 floats each).

Then, if first_time_loaded <= 1: first_time_loaded = 1.

A.4 EFB situation state and the systems block#

  1. DR738_state_cmd / state_cmd2 handling (EFB "save/restore state" command strings) — not engine-related.
  2. If B738DR_load_save is 3 or 4 (a saved situation is being loaded): remove ground equipment objects (disableGPU, disableASU, fuel trucks, cones, de-ice trucks, stairs, catering, belts, buses, ambulance, fire trucks), then Glareshield_load, Anti_ice_load, Fire_load, Switches_load, Lighting_load, Systems_load (restore the saved panel state; Systems_load restores starter positions, N1/N2, mixture), and memorize_nav_tuner twice.
  3. Else loaded_flight_timer count-down (-= dt while > 0). After a situation load (load_save == 4) or on the frame after the flight start (first_time_loaded == 1): first_time_loaded = 2, direct_N1_req_bug[] = 1, idle_mode_old[] = 0.
  4. If simDR_replay == 0:
    1. If was_replay != 0 (first frame after leaving replay): restore all memorised states (CWS, A/T, F/D, yoke, thrust levers, flaps, gear, speed brake, trim, reversers, hydraulics, slats, spoilers, control surfaces, simDR_eng_N1/N2, B738DR_eng1/2_N1/N2).

    2. If simDR_pause == 0, in this order (the systems block):

      # Function Subsystem
      1 Idle_mode engine: idle mode, EEC/FADEC failure handling, mixture → X-Plane (§4.3)
      2 Engine_n1_n2 engine: ambient ratios, idle N1/N2 (§4.4)
      3 Thr_shift engine: per-engine throttle offset (idle shift) (§4.5)
      4 Glareshield MCP/AFDS panel, A/T (writes thrust lever positions)
      5 Eicas_thrust engine: thrust lever → N1 command → X-Plane throttle (§4.6)
      6 Xbleed_start engine: cross-bleed start assist (§4.7)
      7 Starter engine: starter air, N2/N1 during start and spool-down, windmilling, vibration, fan rotation (§4.8)
      8 Engine_calculation engine: fuel metering, light-off, EGT, fuel flow, oil (§4.9)
      9 Engine_model engine: thrust and fuel flow written to X-Plane (§4.10)
      10 EICAS_warning EICAS alerts
      11 Annunciators annunciators
      12 Anti_ice anti-ice
      13 Comm radios
      14 Chrono clocks
      15 Fire fire protection
      16 Switches switches
      17 Lighting lighting
      18 Cab_lighting cabin lighting
      19 Systems electrics, APU, pneumatics, fuel, hydraulics, engine start switches, start valves, ignition, EICAS fuel flow/oil indications (§4.11)
      20 WXR weather radar

      Then, unless loading a situation (load_save 3 or 4): Glareshield_save, Anti_ice_save, Fire_save, Switches_save, Lighting_save, Systems_save (state memory for the next "load"); then, if sim_bus_load[0..2] differs from X-Plane's bus_load, write it.

  5. Else (replay): was_replay = 1; Idle_mode_replay, Lighting_replay, Systems_replay, Glareshield_replay (follow the recorded X-Plane values).
  6. If not in replay: PFD_stall_system().

A.5 FMS, displays, AFDS (if simDR_pause == 0)#

  1. Direct-to track drawing, legs data refresh, FMS path computation (fmc_calc, turns, holds, offsets, abeam points, calc_direct_trk, …).
  2. ND: nd_earth_perf, nd_apt_perf, nd_*_display / nd_*_calc, NDRender_xplane, NDRender_xplane2.
  3. ASXP_metar, fms_turn, calc_vortex.

A.6 Flight systems and failures#

  1. ap_speed_accel() (every frame, also when paused).
  2. If simDR_pause == 0:
    1. first_timer counts up to the start-up delay (used by B738_gpu_power).
    2. de_ice(), Pressurization().
    3. If not replay: flight_director_new(), autopilot_servo(), B738_yoke(), lnav_vnav_horiz_vert(), calc_position().
    4. Touchdown_effect(); ski_effect() (conditional, ski option).
    5. If not replay: shift_cg(), stab_filter(), sts().
    6. rst(), manage_failure().
    7. If not replay: manage_eng_failure(), eng_flameout_effect() (§4.12).
    8. If not replay: check_g_limit(), B738_ail_spoil_spdbrk().
    9. Reverser deploy bookkeeping (B738DR_reverse_deploy max of both).
  3. If not paused: electrical bus status bookkeeping (battery bus).
  4. Twice a second: sim_time1 += dt; when sim_time1 > 0.5: N1_N2_idle() (X-Plane idle ratios, §4.13, also while paused); if not paused engine_fail(sim_time1) (§4.12), detect_center_line(), and if first_timer > 2: B738_gpu_power(); then sim_time1 = 0.
  5. If not paused: Neutral_shift(); if not replay B738_flight_controls(), code_mem2() (memorise for replay), else decode_mem2(); B738_vib_flaps(); tune_fm_extend().
  6. If first_time != 0 and not replay: art_control() (artificial stability / handling), plus B738DR_xfirst_time2 bookkeeping.

A.7 Epilogue#

  1. first_time = 1.
  2. Pause-after-load: if a situation load requested a pause (pause_load_flt and B738DR_pause_load_flt != 0) and the sim is not paused → command sim/operation/pause_toggle once.
  3. sim_paused_old = simDR_pause; replay_mod_old = simDR_replay; open_gl_active = 1.
  4. Return -1.0.

A.8 Consequences for the rewrite#

  • The engine subsystem runs at A.4.4.2 steps 1–3 and 5–9, with the start switches/valves/ignition part inside Systems (step 19), i.e. after Starter and Engine_calculation in the same frame: the starter and ignition values used by Starter/Engine_calculation are those computed by Systems in the previous frame (one-frame latency that the rewrite must keep for frame-exact comparison).
  • engine_fail and N1_N2_idle run at 2 Hz, not every frame.
  • Everything in A.4–A.6 is frozen while X-Plane is paused; MSFS pause must gate the same calls.
  • sim_period is clamped to 0.1 s: at frame rates below 10 fps the engine model runs slower than real time, exactly like Zibo.