技术文档

驾驶舱交互层重做方案:稳定状态接口(Stable Cockpit Interface, SCI)

此页为项目的英文技术文档原文。 docs/COCKPIT_IO_REDESIGN.md

2026-10-31 更新: 产品中已删除 LU737 Host、WebSocket 链路和 XPanel gauge(逻辑与仪表改为 MSFS WASM + HTML 显示单元重写,见 REWRITE_PLAN.md)。本文中 Host/gauge 的实现部分只作历史参考;SCI 契约(data/sci/contract)、Zibo profile 与模型行为保持不变,是重写版要实现的接口;Host 作为测试基准(oracle)保留,--sci-test 脚本继续可用。

状态日期:2026-10-31。适用:LU737 (737-800) MSFS 2024 ↔ xphost ↔ Zibo 2.S1.50(xlua 脚本 + zibomod.xpl)。

实施进度:全部控件与驾驶舱状态已迁移(P0–P6,旧路径已删除),通过离线测试。实现与本设计的差异(事件按名称发送、名称表代替契约哈希、写回 X-Plane 的电气量取自合成模型等)及实测结论见 docs/SCI_IMPLEMENTATION.md。

本文是可直接落地的重做方案,取代当前"MSFS 直接读写 laminar/B738/*"的做法。 前 4 节定义接口与实现,第 5–7 节是验证、分期与 Zibo 2.S1.50 兼容策略。


0. 现状诊断(以本仓库代码为依据)#

现象 位置 根因
959 个 L:LU737_* 一一对应 laminar/B738/* model_io.json (lvars) 由 tools/msfs/generate_behaviors.py 的 Mapper.dref() 生成 MSFS 模型直接依赖 Zibo 内部 DataRef 名;Zibo 改名 → 动画失效,且必须重新生成 glTF 行为 XML
1,879 个 H:LU737_<cmd> / _END / _ONCE 一一对应 Zibo 命令 model_io.json (hevents)、index.ts: onInteractionEvent 输入层也绑定在 Zibo 命令名上;“开关该到哪个位置”的语义不存在,只有“发了一个命令”
点击写 L: 后再发 H:LU737_W<n> 通知 gauge generate_behaviors.py: write_event 位置先在 MSFS 侧被改写(乐观写),Zibo 若拒绝/钳位,模型与逻辑分叉
bus_volts / avionics_on 由“GEN/GPU 开关位置 > 0.5”推出 xphost/src/xpsim.cpp 约 293–317 行 开关位置 ≠ 有电(GEN 开关是弹簧回中的瞬时开关;GPU 开关 ON 但无地面电源也没电);冷暗/上电判断都会错
bridge 中对 LU737_switch_battery_dn/up 特判 xphost/src/bridge.cpp 约 102 行 映射散落在代码里,无法统一测试
关键 DataRef 缺失(如 power_on_buses)→ 整片仪表黑 上层直接读 DataRef,无回退 单点依赖,没有“质量/来源”概念
APU 开关到 START,Zibo 不调用 sim/electrical/APU_start PROGRESS_LOG.md 第二次机上测试 Host 里缺少 X-Plane 自身系统的行为,且没有“命令 → 期望状态 → 超时”的闭环判断

结论:问题不在单个按钮,而在缺少一层语义稳定、带闭环确认的状态模型。


1. 新架构(文字架构图)#

┌──────────────────────────── MSFS 2024 ────────────────────────────┐
│                                                                    │
│  3D 模型 (LU738_INTERIOR.xml,由 SCI 契约生成)                      │
│   MouseRect ──► (>H:LU737_<CTL>_<ACT>)        只发稳定 H: 事件      │
│   Animation  ◄── (L:LU737_<CTL>_POS / _GRD / _BTN)                  │
│   Emissive   ◄── (L:LU737_ANN_* / L:LU737_LT_* / L:LU737_DU_*_PWR)  │
│   Visibility ◄── 同上(只读 SCI L:)                                │
│                                                                    │
│  XPanel Gauge(瘦中继,不含任何 laminar/B738 名称)                 │
│   onInteractionEvent(H:LU737_*) ─► 编码为 CTL_EVENT(id, act, arg)  │
│   收到 STATE_DELTA ─► SimVar.SetSimVarValue(L:LU737_*)             │
│   面板渲染:按 L:LU737_DU_<n>_PWR 决定是否绘制该 DU 的内容          │
│   (现有 SimBridge:MSFS 物理 SimVar ↔ sim/* 保持不变)               │
└──────────────────────────────┬─────────────────────────────────────┘
                               │ ws://127.0.0.1:52737(现有链路,新增 0x1x/0x9x 帧)
┌──────────────────────────────▼─────────────────────────────────────┐
│ LU737Host (xphost)                                                  │
│                                                                     │
│  ┌─ SCI 层(新,版本无关)──────────────────────────────────────┐   │
│  │ ControlRegistry   控件定义:id、类型、位置枚举、护盖关系      │   │
│  │ StateTable        稳定状态:值 + 质量(OK/ALIAS/SYNTH/SHADOW/   │   │
│  │                   MISSING) + 来源 + 最后变化时间              │   │
│  │ EventRouter       事件 → 意图(intent):SET pos / PRESS / ...  │   │
│  │ ClosedLoop        每个意图的期望状态、超时、结果、日志         │   │
│  │ ElecModel         合成电气状态机(总线/供电/DU/灯),与 Zibo   │   │
│  │                   自身状态交叉校验                             │   │
│  └───────────────────────────┬──────────────────────────────────┘   │
│  ┌─ Zibo 适配层(随 Zibo 版本替换)▼────────────────────────────┐   │
│  │ profiles/zibo_2S1_50/*.json  绑定:命令(begin/end/once)、     │   │
│  │                              DataRef(含别名链)、合成表达式     │   │
│  │ ProfileLoader + Probe        启动时解析每个绑定 → 解析报告     │   │
│  └───────────────────────────┬──────────────────────────────────┘   │
│  ┌─ 现有 XPLM 仿真(缩减为插件运行所需的最小集)▼───────────────┐   │
│  │ core / xplm_api / xpsim:DataRef、Command、FlightLoop         │   │
│  │ xpsim 的电气合成改为从 ElecModel 输出 sim/cockpit2/electrical/*│   │
│  └───────────────────────────┬──────────────────────────────────┘   │
│                  xlua.xpl + Zibo Lua ▼  zibomod.xpl(不修改)        │
└─────────────────────────────────────────────────────────────────────┘

单一事实来源(仓库内):
  data/sci/contract/*.json   ← 稳定契约:控件、状态、事件(版本无关)
  data/sci/profiles/zibo_2S1_50/*.json ← Zibo 绑定(版本相关,只有 Host 读)
生成物:
  tools/msfs/generate_behaviors.py --sci   → 模型行为 XML(只引用契约)
  avionics/generated/sci_ids.json          → gauge 用的 id ↔ L: 名称表
  xphost 运行时加载 profile(换 Zibo 版本无需重新编译、无需重新生成 MSFS 包)

1.1 职责划分(硬性规则)#

层 可以知道 禁止知道
MSFS 模型 XML L:LU737_*、H:LU737_*(契约内名称)、MSFS A: 物理量 任何 laminar/B738/*、任何 Zibo 命令
Gauge 契约 id 表、现有 sim/* SimBridge Zibo DataRef/命令(model_io.json 废弃)
Host SCI 层 契约 Zibo 名称(只通过 profile 访问)
Host Zibo 适配层 profile 中的 Zibo 名称 MSFS 名称

1.2 为什么由 Host(而不是 gauge)做适配#

  • Host 与插件在同一进程、同一帧内:发命令、跑一帧、读 DataRef 是原子的,闭环确认不受 WebSocket 延迟影响。
  • 适配表换版本只改 Host 读取的 JSON,MSFS 包(glTF + XML + JS)不需重建。
  • Gauge 只做中继:H: 事件是 MSFS 中只有 gauge 能接收的东西(onInteractionEvent),L: 写入也只有 gauge 能做,所以 gauge 必须保留,但不含逻辑。

2. 稳定状态表#

2.1 数据模型#

每个稳定状态(State):

字段 类型 含义
id u16 契约内编号(生成时分配,追加不重排)
name string 稳定名称,即 L: 名去掉 LU737_ 前缀,如 ELEC_BAT_POS
kind enum pos(离散位置)/ guard / btn / knob(连续或角度)/ ann(指示灯亮度)/ lt(照明亮度)/ bus(总线有电)/ pwr(设备供电)/ val(数值)
enum string[] 仅 pos:位置名,值即下标,如 ["OFF","ON"]、["BAT","OFF","...","APU","GPU"]
range [min,max] knob/val 的单位范围(角度用度,亮度 0..1)
value f64 当前值(L: 中写入的就是它)
quality enum OK 主绑定读取 / ALIAS 别名绑定 / SYNTH 合成 / SHADOW 影子状态(由 Host 记录的已执行意图推出)/ MISSING 无来源(使用安全默认值)
t_change f64 最后变化时间(Host 帧时钟)

所有值统一为 f64,编码规则:

  • pos:整数下标 0..n-1,与 Zibo 的取值无关(例如 Zibo 的 -1/0/1 由 profile 映射为 0/1/2)。
  • guard:0 闭合,1 打开;Host 可输出中间值用于动画(0..1)。
  • btn:0 未按下,1 按下(按钮的机械行程,不是逻辑状态)。
  • ann:0..1 亮度,已包含供电、灯测试(LIGHTS TEST)与 DIM/BRT 的结果——模型只做 EmissiveFactor = (L:LU737_ANN_x)。
  • bus/pwr:0 无电 / 1 有电(合成量)。
  • 角度类旋钮:输出 0..1 的归一化角度,模型动画长度固定按契约中的 anim 映射。

2.2 最小必要集合(Phase 1–2 必须实现,约 140 项)#

A. 电气总线与供电(全部合成量,kind=bus/pwr)#

L: 名 含义 合成规则(ElecModel,见 §4.6)
LU737_BUS_HOT_BAT 热电瓶汇流条 电瓶电量 > 0
LU737_BUS_SW_HOT_BAT 开关热电瓶汇流条 HOT_BAT ∧ BAT 开关 ON
LU737_BUS_BAT 电瓶汇流条 BAT ON ∧ (电瓶 ∨ TRU)
LU737_BUS_DC1 / _DC2 DC 1/2 TRU1 / TRU2 有输入(XFR1 / XFR2),或 TRU3 经交联
LU737_BUS_DC_STBY DC 备用 DC1 ∨ (BAT ON ∧ STBY PWR 非 OFF)
LU737_BUS_AC_STBY AC 备用 XFR1 ∨ (静变流机:BAT 汇流条 ∧ STBY PWR AUTO/BAT)
LU737_BUS_XFR1 / _XFR2 转换汇流条 1/2 本侧或经 BTB 的 AC 源(GPU / APU GEN / IDG)
LU737_BUS_MAIN1 / _MAIN2 主 AC 汇流条 XFR 有电 ∧ 非卸载(GALLEY/CAB UTIL)
LU737_BUS_GND_SVC1 / _2 地面服务汇流条 XFR 有电,或 GPU 可用 ∧ GND SVC 开关
LU737_SRC_GPU_AVAIL 地面电源可用(GRD POWER AVAILABLE 灯逻辑源) EFB 连接 GPU 或 MSFS 外部电源可用
LU737_SRC_APU_GEN_RDY APU 发电机可用 APU N > 95% 且 APU 运行
LU737_SRC_IDG1_RDY / _2 IDG 可用 发动机 N2 > 阈值 ∧ 未脱开
LU737_ELEC_POWERED 机上有任何 DC 电 BAT ∨ DC1 ∨ DC2
LU737_ELEC_AC_POWERED 有任何 AC 主电源 XFR1 ∨ XFR2
LU737_ELEC_STATE 电气状态机(§4.6),pos:COLD_DARK, BAT_ONLY, GPU, APU, ENG, MIXED 合成

B. 仪表 / 屏幕供电(kind=pwr,gauge 与模型据此决定渲染/发光)#

L: 名 含义 主来源 合成回退(有电源总线即视为有电)
LU737_DU_CPT_OUTBD_PWR 机长外侧 DU (PFD) Zibo display_units/odu_cpt 状态 ≥ 0 AC_STBY
LU737_DU_CPT_INBD_PWR 机长内侧 DU (ND) display_units/idu_cpt AC_STBY
LU737_DU_UPPER_PWR 上 DU (EICAS) display_units/udu AC_STBY
LU737_DU_LOWER_PWR 下 DU display_units/ldu XFR2
LU737_DU_FO_INBD_PWR 副驾内侧 DU display_units/idu_fo XFR2
LU737_DU_FO_OUTBD_PWR 副驾外侧 DU display_units/odu_fo XFR2
LU737_DU_<n>_SRC 该 DU 当前显示的格式(pos:PFD/ND/ENG/SYS/BLANK…) display_units/* 的值映射 正常格式
LU737_CDU1_PWR / _CDU2_PWR CDU 屏 electric/mcdu1_on / mcdu2_on XFR1 / XFR2
LU737_ISFD_PWR 备用仪表 (无 Zibo 专用量) DC_STBY 或 HOT_BAT
LU737_CLOCK_PWR / _RADIO1_PWR / _RADIO2_PWR / _ACP_PWR / _XPDR_PWR / _MCP_PWR / _EFIS_CPT_PWR / _EFIS_FO_PWR 其他带显示的面板 profile 中若有对应 DataRef 用之 按 §4.6 的负载表

规则:主来源可用且自洽时用主来源;主来源缺失 → 用合成;二者不一致 → 用主来源并记 MISMATCH 日志。 这样 Zibo 自己的逻辑(例如电瓶下 FO DU 为空白)被完整保留,Zibo 缺量时又不会整片黑屏。

C. 开关/旋钮/护盖位置(以 Phase 1 电气面板为例,其他面板同模式)#

L: 名 kind enum / range Zibo 2.S1.50 主绑定(profile)
LU737_ELEC_BAT_POS pos OFF, ON dref laminar/B738/electric/battery_pos;cmd laminar/B738/switch/battery_up/_dn
LU737_ELEC_BAT_GRD guard 0/1 dref laminar/B738/button_switch/cover_position[i](i 由 Probe 校核);cmd laminar/B738/button_switch_coverNN
LU737_ELEC_STBY_PWR_POS pos BAT, OFF, AUTO dref laminar/B738/electric/standby_bat_pos;cmd laminar/B738/switch/standby_bat_left/_right
LU737_ELEC_STBY_PWR_GRD guard 0/1 护盖 dref/cmd 同上(另一个索引)
LU737_ELEC_DC_METER_POS pos STBY_PWR, BAT_BUS, BAT, AUX_BAT, TR1, TR2, TR3, TEST cmd laminar/B738/knob/dc_power_up/_dn;位置 dref 由 Probe 确认
LU737_ELEC_AC_METER_POS pos STBY_PWR, GRD_PWR, GEN1, APU_GEN, GEN2, INV, TEST cmd laminar/B738/knob/ac_power_up/_dn
LU737_ELEC_GPU_POS pos OFF, NEUTRAL, ON(弹簧回中) dref laminar/B738/electrical/gpu_pos;cmd laminar/B738/toggle_switch/gpu_up/_dn
LU737_ELEC_GEN1_POS / _GEN2_POS pos OFF, NEUTRAL, ON(弹簧回中) dref laminar/B738/electrical/gen1_pos …;cmd …/toggle_switch/gen1_up/_dn
LU737_ELEC_APUGEN1_POS / _APUGEN2_POS pos OFF, NEUTRAL, ON dref laminar/B738/electrical/apu_gen1_pos …;cmd …/toggle_switch/apu_gen1_up/_dn
LU737_ELEC_BUS_TRANS_POS pos OFF, AUTO cmd sim/electrical/cross_tie_on/_off;dref sim/cockpit2/electrical/cross_tie
LU737_ELEC_DRIVE1_DISC_POS / _2 pos NORM, DISC(弹簧) dref laminar/B738/one_way_switch/drive_disconnect1_pos;cmd …/drive_disconnect1 / _off
LU737_ELEC_DRIVE1_GRD / _2 guard 0/1 护盖
LU737_ELEC_CAB_UTIL_POS / _IFE_POS pos OFF, ON cmd laminar/B738/autopilot/cab_util_toggle / ife_pass_seat_toggle
LU737_APU_SW_POS pos OFF, ON, START(START 弹簧回 ON) dref laminar/B738/spring_toggle_switch/APU_start_pos;cmd …/APU_start_pos_up/_dn

D. 关键指示灯(kind=ann,值已含供电与灯测试)#

电气面板:ANN_BAT_DISCHARGE、ANN_TR_UNIT、ANN_ELEC、ANN_STBY_PWR_OFF、ANN_GRD_PWR_AVAIL、ANN_DRIVE1/2、ANN_TRANS_BUS_OFF1/2、ANN_SOURCE_OFF1/2、ANN_GEN_OFF_BUS1/2、ANN_APU_GEN_OFF_BUS、ANN_APU_MAINT/LOW_OIL/FAULT/OVERSPEED。 全局:ANN_MASTER_CAUTION_CPT/FO、ANN_FIRE_WARN_CPT/FO、六组 Six-Pack(ANN_SIXPACK_<ELEC|APU|FUEL|...>)。 MCP:ANN_MCP_<BTN>(每个 MCP 按钮的绿条)。

合成通用式(Host 内):

ann = clamp01( (logic ∨ (lights_test ∧ bus(ann.bus))) ∧ bus(ann.bus) ) × dim(ann.dim_group)

logic 的主来源是 Zibo laminar/B738/annunciator/*(Zibo 已在其中处理测试与亮度时,profile 声明 includes_test=true / includes_dim=true,Host 不重复处理)。

E. 照明(kind=lt)#

LT_PANEL_CPT、LT_PANEL_FO、LT_PANEL_OVHD、LT_PANEL_PED、LT_FLOOD_*、LT_DOME、LT_BACKLIGHT_<group>:值 = 旋钮位置 × 对应总线有电。现有 L:LU737_default_lit 迁移为 LT_*。

2.3 扩展集合(Phase 3–5)#

按面板分组,名称前缀即分组:

前缀 面板 主要条目
FUEL_ 燃油 6 个泵 PUMP_<L_AFT…>_POS、XFEED_POS、XFEED_VALVE(val 0..1 阀门开度,用于 VALVE OPEN 灯)、ANN_FUEL_*
HYD_ 液压 ENG1_PUMP_POS、ELEC2_PUMP_POS…、ANN_HYD_*、QTY_A/B
AIR_ 空调引气 PACK_L_POS(OFF,AUTO,HIGH)、ISOL_POS(CLOSE,AUTO,OPEN)、BLEED1_POS、APU_BLEED_POS、TEMP_<zone>_KNOB、TRIM_AIR_POS、RECIRC_FAN_*、PRESS_MODE_POS、ANN_*
ICE_ 防冰 WINDOW_HEAT_<n>_POS、PROBE_HEAT_A/B_POS、WING_AI_POS、ENG_AI1/2_POS、ANN_*
LTS_ 外部/内部灯光 LAND_L_RET_POS(RET,EXT,ON)、LAND_L_FIX_POS、TAXI_POS、RWY_TURNOFF_L/R_POS、LOGO、POSITION_POS(STEADY,OFF,STROBE_STEADY)、ANTI_COLL、WING、WHEEL_WELL、EMER_EXIT_POS(OFF,ARMED,ON)+_GRD、NO_SMOKING、FASTEN_BELTS(OFF,AUTO,ON)
ENG_ 发动机/启动 START1_POS(GRD,OFF,CONT,FLT)(GRD 由电磁保持,见 §4.3 hold_by_logic)、IGN_SRC_POS、EEC1/2_POS+_GRD、START_LEVER1/2_POS(CUTOFF,IDLE)
FLT_ 飞控面板 FLTCTL_A/B_POS+_GRD、SPOILER_A/B_POS+_GRD、ALT_FLAPS_POS+_GRD、YAW_DAMPER_POS、ANN_*
MCP_ MCP CRS_L/R、IAS、HDG、ALT、VS(val,显示值与旋钮角度分开:MCP_HDG_VAL、MCP_HDG_KNOB)、BANK_POS、FD_L/R_POS、AT_ARM_POS、每个按钮 MCP_<BTN>_BTN(按下行程)+ ANN_MCP_<BTN>,MCP_DISENGAGE_POS
EFIS_CPT_ / EFIS_FO_ EFIS MINS_SEL_POS(RADIO,BARO)、MINS_KNOB、BARO_SEL_POS(IN,HPA)、BARO_KNOB、MODE_POS(APP,VOR,MAP,PLN)、RANGE_POS(5…640)、VOR_ADF_L/R_POS、按钮 `<WXR
GLARE_ 遮光板 主警告/主注意按钮、六组灯按压、CLOCK_*
MAIN_ 主面板 DU_SEL_CPT_POS、DU_SEL_FO_POS、LOWER_DU_POS、AUTOBRAKE_POS(RTO,OFF,1,2,3,MAX)、GEAR_LEVER_POS(UP,OFF,DN)、NOSE_WHEEL_STEER_POS、ISFD_*、ENG_DISP_BTN、N1_SET_POS、SPD_REF_POS、FUEL_FLOW_POS
PED_ 中央操纵台 FLAP_LEVER_POS(0,1,2,5,10,15,25,30,40)、SPEEDBRAKE_POS(val)、PARK_BRAKE_POS、STAB_TRIM_CUTOUT_<n>_POS+_GRD、`FIRE_HANDLE_<1
THR_ 油门台 TOGA_BTN、AT_DISC_BTN、REV_L/R(物理量仍来自 MSFS A:,见 §4.7)
YOKE_ 侧杆/驾驶盘 AP_DISC_BTN_CPT/FO、TRIM_SW_CPT/FO_POS(DN,NEUT,UP)、MIC_*、CHRONO_*
EFB_ EFB 平板 平板按钮保留为通用触摸事件 EFB_TOUCH(x,y)(Zibo 平板按钮是坐标交互,不进入稳定状态表)

数量预估:控件约 420 个(现有 991 个点击区按控件合并后),稳定状态约 900 项(位置+护盖+按钮约 520,指示灯约 210,供电/总线约 50,数值约 120)。


3. 命名规范#

3.1 通用规则#

  • 前缀固定 LU737_;其余全大写,下划线分隔:LU737_<PANEL>_<CONTROL>[_<SUB>]_<SUFFIX>。
  • PANEL 取 §2.3 表中的前缀(ELEC、FUEL、MCP、EFIS_CPT…)。
  • 双侧控件以 _CPT/_FO(人员侧)或 1/2、_L/_R(系统侧)区分,不用 Zibo 的 capt/cpt/pilot 混用写法。
  • 名称一经发布不得改名;废弃名保留到下一大版本,并在契约中标 deprecated。

3.2 L: 变量(Host → MSFS,只读给模型)#

后缀 kind 例
_POS 离散位置下标 LU737_ELEC_BAT_POS
_GRD 护盖 0..1 LU737_ELEC_BAT_GRD
_BTN 按钮行程 0/1 LU737_MCP_LNAV_BTN
_KNOB 旋钮归一化角度 0..1(无限旋钮为 0..1 循环) LU737_MCP_HDG_KNOB
_VAL 物理/显示数值(单位见契约) LU737_MCP_HDG_VAL
LU737_ANN_* 指示灯亮度 0..1 LU737_ANN_SOURCE_OFF1
LU737_LT_* 照明亮度 0..1 LU737_LT_PANEL_CPT
LU737_BUS_* 总线 0/1 LU737_BUS_XFR1
_PWR 设备供电 0/1 LU737_DU_UPPER_PWR
LU737_SYS_* 链路与诊断 LU737_SYS_LINK(0 断开 / 1 连接)、LU737_SYS_SEQ、LU737_SYS_DEGRADED(有 MISSING 项的数量)

MSFS 侧唯一可写的 L::LU737_IN_*(输入暂存,见 3.3 拖动)与用户偏好(LU737_DU_GAIN)。模型绝不写 _POS/_GRD/_BTN/_KNOB。

3.3 H: 事件(MSFS → Host)#

格式:H:LU737_<PANEL>_<CONTROL>_<ACTION>

ACTION 语义 用于
PRESS / RELEASE 按下 / 松开(Host 保持命令 begin→end) 按钮、弹簧开关、测试按钮
TOGGLE 两位开关取反 拨动开关、护盖
INC / DEC 步进一档(离散)或一格(旋钮) 多位选择钮、滚轮
INC_FAST / DEC_FAST 大步进(旋钮外圈/加速) MCP、EFIS 旋钮
SET_<n> 直接到第 n 个位置(n 为 enum 下标) 鼠标点在多位开关的上/下半区
GUARD_OPEN / GUARD_CLOSE / GUARD_TOGGLE 护盖 所有带护盖的开关
PUSH 旋钮按压(MCP 旋钮中心、EFIS 中心按钮) 与旋转共存时
DRAG 读取 L:LU737_IN_<PANEL>_<CONTROL> 的目标值后设置 杆、连续旋钮拖动

示例:H:LU737_ELEC_BAT_TOGGLE、H:LU737_ELEC_GEN1_SET_0、H:LU737_ELEC_GEN1_RELEASE、H:LU737_ELEC_BAT_GUARD_TOGGLE、H:LU737_EFIS_CPT_RANGE_INC、H:LU737_MCP_HDG_INC_FAST、H:LU737_PED_FLAP_LEVER_DRAG。

3.4 鼠标与事件的标准映射(生成器统一套用)#

控件类型 左键 右键 滚轮 松开
两位开关 TOGGLE TOGGLE 上 SET_1 / 下 SET_0 —
多位开关(含弹簧位) 上半区 INC / 下半区 DEC(无分区时左 INC 右 DEC) 同左 INC/DEC 若目标为弹簧位:RELEASE
按钮 PRESS — — RELEASE
旋钮(离散/连续) DEC INC INC/DEC —
带按压的旋钮 中心区 PUSH,外圈同上
护盖 GUARD_TOGGLE(护盖本体单独 MouseRect)
护盖下开关(护盖闭合时) 等价 GUARD_OPEN(与真实飞机一致:需先开盖)
杆(襟翼、减速板) INC/DEC 到下一档 INC/DEC DRAG 仅 MSFS 2024 拖动交互

4. Host 适配层:核心逻辑#

4.1 契约文件(版本无关)data/sci/contract/elec.json 片段#

{
  "controls": [
    {"id": "ELEC_BAT", "type": "switch2", "enum": ["OFF", "ON"],
     "guard": "ELEC_BAT_GRD", "guard_blocks": true},
    {"id": "ELEC_GEN1", "type": "switch3_spring", "enum": ["OFF", "NEUTRAL", "ON"],
     "rest": "NEUTRAL"},
    {"id": "ELEC_STBY_PWR", "type": "switch3", "enum": ["BAT", "OFF", "AUTO"],
     "guard": "ELEC_STBY_PWR_GRD", "guard_blocks": true},
    {"id": "APU_SW", "type": "switch3_spring_end", "enum": ["OFF", "ON", "START"],
     "spring": {"from": "START", "to": "ON"}}
  ],
  "states": [
    {"name": "ELEC_BAT_POS", "kind": "pos", "enum": ["OFF", "ON"], "default": 0},
    {"name": "ELEC_BAT_GRD", "kind": "guard", "default": 0},
    {"name": "BUS_XFR1", "kind": "bus", "default": 0},
    {"name": "DU_FO_OUTBD_PWR", "kind": "pwr", "default": 0},
    {"name": "ANN_SOURCE_OFF1", "kind": "ann", "bus": "BUS_DC1", "test": true, "dim": "ANN"}
  ]
}

4.2 Zibo 绑定文件(版本相关)data/sci/profiles/zibo_2S1_50/elec.json 片段#

{
  "profile": "zibo_2S1_50",
  "bindings": {
    "ELEC_BAT": {
      "read": [{"dref": "laminar/B738/electric/battery_pos", "map": {"0": 0, "1": 1}}],
      "drive": {"mode": "step_pair",
                "inc": "laminar/B738/switch/battery_up",
                "dec": "laminar/B738/switch/battery_dn"}
    },
    "ELEC_BAT_GRD": {
      "read": [{"dref": "laminar/B738/button_switch/cover_position[?]", "probe": "cover_index"}],
      "drive": {"mode": "toggle", "cmd": "laminar/B738/button_switch_cover??"},
      "fallback": "shadow"
    },
    "ELEC_GEN1": {
      "read": [{"dref": "laminar/B738/electrical/gen1_pos", "map": {"-1": 0, "0": 1, "1": 2}}],
      "drive": {"mode": "spring_hold",
                "to": {"OFF": "laminar/B738/toggle_switch/gen1_dn",
                       "ON":  "laminar/B738/toggle_switch/gen1_up"}}
    },
    "APU_SW": {
      "read": [{"dref": "laminar/B738/spring_toggle_switch/APU_start_pos"}],
      "drive": {"mode": "step_pair",
                "inc": "laminar/B738/spring_toggle_switch/APU_start_pos_up",
                "dec": "laminar/B738/spring_toggle_switch/APU_start_pos_dn",
                "hold_last_step": "START"},
      "effects": [{"when": "START held", "expect": "SRC_APU_RUNNING", "within_s": 5,
                   "else": "x_apu_start"}]
    },
    "BUS_XFR1": {
      "read": [{"dref": "laminar/B738/electric/ac_tnsbus1_status", "gt": 0.5}],
      "synth": "elec.xfr1"
    },
    "DU_FO_OUTBD_PWR": {
      "read": [{"dref": "laminar/B738/display_units/odu_fo", "ge": 0}],
      "synth": "elec.load('DU_FO_OUTBD')"
    },
    "ANN_SOURCE_OFF1": {
      "read": [{"dref": "laminar/B738/annunciator/source_off1", "includes_test": true}],
      "synth": "!elec.xfr1_from_side1 && bus('BUS_DC1')"
    }
  }
}

read 是别名链:按顺序尝试,第一个能解析的生效(OK/ALIAS),全部失败再走 synth(SYNTH)或 shadow(SHADOW),都没有则 MISSING + 契约默认值。

4.3 驱动模式(把“意图”翻译成正确的 begin/end/once)#

mode 适用 行为
step_pair 有 up/dn 两个命令的多位开关、旋钮 意图 SET n:每帧比较当前读数与目标,差一档就 once(inc/dec)(begin,跑一帧,end),直到相等或超出 max_steps;INC/DEC 即 SET cur±1
toggle 单个 toggle 命令 SET n:读数 ≠ n 时 once(cmd) 一次,下一帧确认
spring_hold 弹簧回中开关(GEN、GPU、DRIVE) SET OFF/ON:begin(cmd) 并保持到 RELEASE,RELEASE 时 end(cmd);超时 20 s 自动 end(沿用现有 kHoldTimeout)
hold 按钮、测试按钮 PRESS → begin,RELEASE → end;Release 丢失时超时 end
once 纯脉冲(MCP 按钮在 Zibo 中按下即生效) PRESS → once;RELEASE 只更新 _BTN 行程
dref_write 可写 DataRef(温度旋钮、亮度、Zibo 未提供命令的量) 写值,下一帧读回确认(Zibo 若用自己的值覆盖,记 OVERRIDDEN)
hold_by_logic 由系统电磁保持的位置(ENG START GRD) 按 spring_hold 发命令,松开后读取 Zibo 位置:若 Zibo 保持在 GRD 就保持
sim_cmd X-Plane 自带命令(cross_tie、灯光) 同 once;Host 的 xpsim 负责其副作用

护盖联动(guard_blocks=true):护盖闭合时对开关的任何意图先转为 GUARD_OPEN,不直接下发开关命令(真实行为,也避免 Zibo 因护盖闭合忽略命令导致“点了没反应”)。

begin/end 不变量(Host 强制):

  1. 同一命令同一时刻最多一个持有者;重复 begin 被合并(计数,不重复调用插件)。
  2. 每个 begin 必有一个 end:来源可以是 RELEASE、once 的下一帧、超时、链路断开(releaseCommands())。
  3. once 至少跨一个插件帧(begin → frame → end),因为 Zibo 很多命令在 phase==1(continue)或 flight loop 中才处理。

4.4 核心伪代码#

// ---------- 启动 ----------
Contract C = loadContract("data/sci/contract/*.json");      // 版本无关
Profile  P = loadProfile(ini.profile /* "zibo_2S1_50" */);   // 版本相关
for (State &s : C.states) {
    Binding *b = P.find(s.name);
    s.reader = resolveReadChain(b);          // 依次 Host::find(dref);记录哪个别名命中
    s.quality = s.reader ? (s.reader->isPrimary ? OK : ALIAS)
              : b && b->synth ? SYNTH
              : b && b->fallback == "shadow" ? SHADOW : MISSING;
}
for (Control &c : C.controls) c.driver = makeDriver(P.find(c.id));  // 解析命令;缺失 → UNBOUND
writeResolutionReport("LU737Host_sci_report.txt");  // 每项:名称 / 质量 / 命中的 DataRef / 命令

// ---------- 每帧(gauge 的 FRAME 到达时) ----------
void Sci::frame(double dt) {
    for (Event &e : inbox) router.dispatch(e);  // 事件 → 意图(Intent),打 seq
    for (Intent &i : active) i.driver->step(i); // 下发 begin/once/end
    host.frame(dt);                             // 插件跑一帧(现有)
    for (Intent &i : active) i.driver->settle(i);  // 结束本帧的 once
    elec.update(stateTable, dt);                // 合成电气(读开关、源、MSFS 物理量)
    for (State &s : C.states) {
        double v = s.reader ? s.map(s.reader->get())
                 : s.quality == SYNTH  ? eval(s.synth)
                 : s.quality == SHADOW ? shadow[s.name]
                 : s.def;
        if (s.reader && s.synthCheck) crossCheck(s, v, eval(s.synth)); // MISMATCH 日志(限频)
        if (v != s.value) { s.value = v; s.t_change = now; delta.push(s.id, v); }
    }
    for (Intent &i : active) closedLoop.check(i);  // §4.5
    send(STATE_DELTA{seq, delta}, ACKS{...});
}

// ---------- 意图驱动(step_pair 例) ----------
void StepPair::step(Intent &i) {
    int cur = state(i.ctl.posState);
    if (cur == i.target) { i.done = true; return; }
    if (i.steps++ >= i.maxSteps) { i.fail("STUCK", cur); return; }
    Command *c = (i.target > cur) ? inc : dec;
    host.commandBegin(c); endAfterFrame.push(c);      // once = 跨一帧
}

// ---------- 影子状态(SHADOW) ----------
// 位置 DataRef 缺失但命令存在:Host 以“已执行且未被拒绝的意图”更新影子值,
// 并在启动/冷暗复位时置契约默认值。

4.5 闭环判定与日志#

每个意图(Intent)都有结果,写入日志并通过 ACK 帧回传:

结果 条件
OK 期望状态在 within(默认 3 帧,弹簧/保持类为 RELEASE 后 3 帧)内达到
OK_SHADOW 无读回 DataRef,影子状态已更新
BLOCKED_GUARD 护盖闭合,已转为开盖意图
REJECTED 命令已执行,位置未变(Zibo 拒绝,例如无液压/无电)
STUCK 步进超出 max_steps 未到位
UNBOUND profile 中无可用命令(启动报告也会列出)
EFFECT_MISSING 位置已变,但声明的效果(effects.expect)超时未出现,例如 APU START 后 APU 未运转

日志格式(一行一事件,便于 grep 与自动比对):

[sci] t=123.456 seq=812 EV  ELEC_BAT TOGGLE
[sci] t=123.456 seq=812 CMD laminar/B738/switch/battery_up once
[sci] t=123.489 seq=812 ST  ELEC_BAT_POS 0->1 (OK dref laminar/B738/electric/battery_pos)
[sci] t=123.489 seq=812 ST  BUS_BAT 0->1 (SYNTH elec) ; BUS_AC_STBY 0->1 ; DU_CPT_OUTBD_PWR 0->1 (OK)
[sci] t=123.489 seq=812 RES OK 1 frame

lu737host.ini 新增:sci_log = off|events|states|all、sci_profile = zibo_2S1_50、sci_trace = ELEC_*,APU_*(通配过滤)。

4.6 电气状态机(ElecModel,合成,不依赖易变 DataRef)#

输入(全部是稳定状态或 MSFS 物理量):开关位置(BAT、STBY PWR、GPU、GEN1/2、APU GEN1/2、BUS TRANS、DRIVE DISC、CAB UTIL、IFE)、SRC_GPU_AVAIL、APU N%(MSFS APU PCT RPM 或 Zibo APU 量)、发动机 N2(MSFS)、电瓶电量(Host 积分)。

锁存状态(737NG 的 GEN/GPU/APU GEN 是瞬时开关,“谁在供电”是锁存的):

src_xfr1, src_xfr2 ∈ {NONE, GPU, APU, IDG1, IDG2}
on GPU_SW→ON   : if GPU_AVAIL { src_xfr1 = src_xfr2 = GPU }
on APUGEN1→ON  : if APU_GEN_RDY { src_xfr1 = APU; if src_xfr2 == GPU: src_xfr2 = APU }
on APUGEN2→ON  : 对称
on GEN1→ON     : if IDG1_RDY { src_xfr1 = IDG1 }      (GEN2 对称)
on *→OFF       : 对应侧断开 → 若 BUS_TRANS=AUTO 且另一侧有 IDG/APU,经 BTB 由另一侧供电
on 源失效       : (GPU 撤除 / APU 停车 / 发动机停车 / IDG 脱开) 对应 src = NONE
on BAT→OFF 且无 AC : 全部 NONE

总线方程:

XFR1 = src_xfr1 != NONE || (BUS_TRANS==AUTO && src_xfr2 in {APU,IDG2})
XFR2 = 对称
TRU1 = XFR1, TRU2 = XFR2, TRU3 = XFR2 || (BUS_TRANS==AUTO && XFR1)
DC1  = TRU1 || (TRU3 && BUS_TRANS==AUTO)       DC2 = TRU2 || TRU3
BAT  = BAT_SW && (bat_charge>0 || DC1 || DC2)
HOT_BAT = bat_charge > 0                        SW_HOT_BAT = HOT_BAT && BAT_SW
DC_STBY = DC1 || (BAT && STBY_PWR != OFF && (STBY_PWR==BAT || !DC1))
AC_STBY = XFR1 || (BAT && STBY_PWR != OFF && (STBY_PWR==BAT || !XFR1))   // 静变流机
MAINx   = XFRx && !CAB_UTIL_OFF

状态 ELEC_STATE:COLD_DARK(无任何总线) → BAT_ONLY(仅 BAT/STBY) → GPU / APU / ENG / MIXED(按 src_xfr1/2 组合)。状态切换都写 [sci] ELEC 日志,便于对照冷暗启动流程。

负载表(设备 → 总线,作为 _PWR 的合成回退;主来源仍是 Zibo 量):

设备 总线
DU 机长外侧/内侧、上 DU AC_STBY
DU 副驾外侧/内侧、下 DU XFR2
CDU1 / CDU2 XFR1 / XFR2
ISFD、时钟 DC_STBY / HOT_BAT
MCP、EFIS 控制面板 DC1 ∨ DC2(任一侧)
NAV/COMM1、ACP1 DC_STBY / DC1
NAV/COMM2、ACP2 DC2
指示灯(按组) 契约中 ann.bus

向下兼容 X-Plane 自带量:现有 xpsim.cpp 中按开关位置推 bus_volts 的代码删除,改为由 ElecModel 写:

sim/cockpit2/electrical/bus_volts[0..3]   = 28 × {DC1, DC2, DC_STBY, BAT}
sim/cockpit2/electrical/battery_on[0]     = BAT_SW
sim/cockpit/electrical/avionics_on        = DC1 || DC2 || DC_STBY
sim/cockpit2/electrical/generator_on[i]   = src 中含 IDGi
sim/cockpit2/electrical/APU_generator_on  = src 中含 APU
sim/cockpit/electrical/gpu_on             = src 中含 GPU

插件(Zibo)自己读取这些 X-Plane 量时得到的是物理一致的结果,而不是“开关拨过了”。

4.7 与 MSFS 物理的边界#

不变:飞行物理、推力、燃油量、起落架/襟翼实际位置仍由 MSFS 计算(现有 SimBridge)。SCI 只负责驾驶舱控件与驾驶舱显示状态。 油门杆/驾驶盘/方向舵:位置来自 MSFS 硬件轴(A:),SCI 仅输出其上的按钮(TOGA、A/T DISC、AP DISC、配平电门)。

4.8 线协议(在现有 WebSocket 帧格式上扩展)#

沿用 bridge.h 的小端二进制格式,新增类型,不影响现有 0x01–0x06 / 0x80–0x85:

MSFS → Host

0x10 SCI_HELLO   u32 contract_hash, u16 n_states, u16 n_controls   // 契约版本校验
0x11 SCI_EVENT   u32 seq, u16 control_id, u8 action, f32 arg       // arg: SET 的 n、DRAG 的值、INC 的倍数
0x12 SCI_RESYNC  —                                                 // 请求全量快照(gauge 重载/切换飞机/暂停恢复)

action:1 PRESS, 2 RELEASE, 3 TOGGLE, 4 INC, 5 DEC, 6 INC_FAST, 7 DEC_FAST, 8 SET, 9 GUARD_OPEN, 10 GUARD_CLOSE, 11 GUARD_TOGGLE, 12 PUSH, 13 DRAG。 多个 SCI_EVENT 可在同一 WebSocket 消息中拼接,并随下一 FRAME 一起处理(Host 在 h.frame(dt) 之前分派)。

Host → MSFS(作为 0x80 FRAME 回复中的新 section)

0x90 SCI_SNAPSHOT u32 frame, u16 n, n × (u16 state_id, f64 value)            // HELLO/RESYNC 后
0x91 SCI_DELTA    u32 frame, u16 n, n × (u16 state_id, f64 value)            // 每帧只发变化
0x92 SCI_ACK      u16 n, n × (u32 seq, u8 result, u16 frames)                // §4.5 结果
0x93 SCI_QUALITY  u16 n, n × (u16 state_id, u8 quality)                      // 启动时一次 + 变化时

频率:与现有 FRAME 同步(每个 MSFS 帧一次,30–60 Hz),一帧往返;DELTA 平均 < 20 项。 contract_hash 不一致时 Host 拒绝 SCI(只记日志),gauge 显示 LU737 SCI CONTRACT MISMATCH,避免新旧包混用造成错位。

4.9 Gauge 改动(avionics/src/instruments/xpanel/index.ts)#

  • 删除 model_io.json 的 lvars/writes/hevents 路径与 H:LU737_W<n> 机制。
  • onInteractionEvent:解析 LU737_<CTL>_<ACT> → 查 sci_ids.json → 发送 0x11。不认识的事件记一次控制台警告。
  • 收到 0x90/0x91:SimVar.SetSimVarValue('L:LU737_' + name, 'number', v)(只写变化值)。
  • 链路断开时:L:LU737_SYS_LINK=0,所有 _PWR/ANN_*/BUS_* 置 0(防止断线后假亮),位置类保持最后值。
  • 面板渲染:每个 DU 的绘制区域在 L:LU737_DU_<n>_PWR == 0 时清为黑色并跳过该区域的指令(同时减少负载);DU 材质 EmissiveFactor = PWR × 亮度 × DU_GAIN。

4.10 模型行为生成(tools/msfs/generate_behaviors.py --sci)#

新增一张点击区 → 控件的映射表 data/sci/clickspots.json(由现有 991 个点击区自动初分组,再人工校对):

{"node": "switch_battery", "control": "ELEC_BAT", "region": "full"},
{"node": "cover_battery",  "control": "ELEC_BAT_GRD", "region": "full"},
{"node": "knob_dc_power",  "control": "ELEC_DC_METER", "region": "left_right"}

以及动画 → 稳定状态映射 data/sci/animations.json(OBJ 中驱动动画的 Zibo DataRef → 稳定状态 + 值映射),生成器据此输出:

<Animation Name="laminar_B738_electric_battery_pos" Length="100" Type="Sim" TypeParam="AutoPlay">
  <Parameter><Code>(L:LU737_ELEC_BAT_POS) 100 *</Code><Lag>400</Lag></Parameter>
</Animation>
<MouseRect>
  <Cursor>Hand</Cursor><MouseFlags>LeftSingle+RightSingle+WheelUp+WheelDown</MouseFlags>
  <CallbackCode>
    (M:Event) 'WheelUp' scmp 0 == if{ (>H:LU737_ELEC_BAT_SET_1) quit }
    (M:Event) 'WheelDown' scmp 0 == if{ (>H:LU737_ELEC_BAT_SET_0) quit }
    (>H:LU737_ELEC_BAT_TOGGLE)
  </CallbackCode>
</MouseRect>

生成器的静态检查(构建失败条件):

  1. 任何行为代码中出现 laminar/、LU737_ 后跟小写字母的旧名 → 失败。
  2. 每个可点击节点必须映射到一个控件;每个控件至少有一个可见的 _POS/_GRD/_BTN/_KNOB 动画或明确标注 no_anim。
  3. 每个 ANN_* 至少驱动一个发光节点;每个带发光的指示灯节点必须由 ANN_*/LT_* 驱动。
  4. 未映射的 OBJ 动画 DataRef 列表输出到 build/sci_unmapped.txt(应为空或全部 ignore)。

5. 闭环验证清单#

5.1 自动化(无 MSFS,CI 可跑)#

xphost 增加 --sci-selftest(Linux 版 + lin_x64 插件,现有测试方式):

  1. 解析报告:所有状态 quality != MISSING;ALIAS/SYNTH/SHADOW 列表与 profile 预期一致(快照比对文件 xphost/test/sci_expected_quality.txt)。
  2. 逐控件扫遍:对契约中每个控件、每个位置执行 SET n,断言:
    • RES OK(或 OK_SHADOW)在 ≤ 3 帧内;
    • _POS 等于 n;
    • profile 声明的 effects 在时限内出现。
  3. 护盖:护盖闭合时 SET → BLOCKED_GUARD 且护盖打开;开盖后 SET 生效。
  4. 按住语义:PRESS 后保持 N 帧,断言命令 held>0 持续 N 帧,RELEASE 后为 0;丢失 RELEASE 时 20 s 超时释放。
  5. 冷暗上电脚本(断言每一步的总线与 _PWR):
步骤 期望
冷暗 所有 BUS_*=0,所有 _PWR=0,所有 ANN_*=0,ELEC_STATE=COLD_DARK
BAT ON(护盖先开) BUS_BAT=BUS_DC_STBY=BUS_AC_STBY=1;DU_CPT_OUTBD/INBD/UPPER_PWR=1;DU_FO_*/DU_LOWER_PWR=0;ANN_STBY_PWR_OFF=0
STBY PWR → OFF BUS_AC_STBY=0、DU_CPT_*=0、ANN_STBY_PWR_OFF=1
GPU 可用 + GPU ON BUS_XFR1/2=1,所有 DU _PWR=1,ANN_GRD_PWR_AVAIL=1
APU START → 运转 → APU GEN1/2 ON SRC_APU_GEN_RDY=1,XFR 源为 APU,ANN_APU_GEN_OFF_BUS=0
起动 ENG1/2 → GEN1/2 ON 源为 IDG,ANN_SOURCE_OFF1/2=0
全部关断 → BAT OFF 回到 COLD_DARK,所有显示与灯熄灭
  1. 缺量注入:用 --sci-hide-dref <pattern> 在 Host 中隐藏 DataRef(模拟 Zibo 改名/缺失),重跑 2 和 5:结果必须仍然通过(质量降为 ALIAS/SYNTH/SHADOW),不得出现大面积 _PWR=0。
  2. 契约一致性:生成器静态检查(§4.10)。

5.2 Gauge 侧(avionics 的 vitest + 现有 harness)#

  • 事件编码:每个契约 H: 名 → 正确的 (control_id, action, arg)。
  • DELTA 应用:只写变化值;断线时 _PWR/ANN/BUS 清零。
  • 渲染门控:_PWR=0 的 DU 区域为黑(截图比对现有 harness 输出)。

5.3 机上(MSFS 2024)逐面板验收表#

每个控件一行,测试员只需看三列(日志自动给出前两列):

控件 事件→命令(日志 EV/CMD) 状态(日志 ST、RES) 视觉(人工)
BAT 开关 ELEC_BAT TOGGLE → battery_up once ELEC_BAT_POS 0→1 OK 拨杆到位;DU 机长侧亮;STBY PWR OFF 灯灭

辅助:

  • Host 控制台 sci_trace=* 时,每次点击打印完整闭环一行汇总:[sci] ELEC_BAT TOGGLE → OK 1f | POS 0→1 | BUS_BAT 0→1 | DU_CPT_OUTBD_PWR 0→1。
  • Gauge 调试覆盖层(L:LU737_SYS_DEBUG=1):在 Panel 角落显示最近 8 个 ACK 与非 OK 结果。
  • 判定规则:任何点击若 ACK 不是 OK*/BLOCKED_GUARD,或 ST 变了但无视觉变化(模型映射错)→ 记缺陷,附日志行。

6. 分阶段实施计划#

每阶段完成标准:§5.1 自动化全绿 + §5.3 该阶段面板机上验收通过。旧路径(model_io.json)在过渡期仍可工作:生成器按控件逐个切换,已迁移控件的点击区只发 SCI 事件,未迁移的保持旧事件。

阶段 内容 产出 / 涉及文件 验收
P0 骨架(1 周) 契约与 profile 格式、Host Sci 模块(StateTable、EventRouter、ClosedLoop、日志、解析报告)、线协议 0x10–0x12 / 0x90–0x93、gauge 中继、生成器 --sci 与静态检查 xphost/src/sci/*.{h,cpp}、data/sci/**、bridge.cpp(接入)、index.ts、generate_behaviors.py --sci-selftest 可运行;契约哈希校验
P1 电气 + 供电 + 显示门控(1–2 周) §2.2 A/B/C(电气)/D(电气灯);ElecModel;xpsim.cpp 电气合成改由 ElecModel 输出;删除 bridge.cpp 中电瓶特例;DU/CDU 渲染门控 冷暗 → BAT → GPU → APU → ENG 全流程 §5.1 第 5 步全绿;机上:冷暗全黑、BAT 机长侧亮、GPU/APU 全亮
P2 APU + 发动机启动 + 燃油 + 主警告/六组灯 + 灯测试 APU 开关(含 START 效果超时与回退)、ENG START、点火、启动手柄、燃油泵/交输、Master Caution/Fire Warn、LIGHTS TEST/DIM 电气指示灯已有,扩展到全部 ANN_* 的通用合成式 冷暗到双发运转全流程无“点了没反应”
P3 MCP + EFIS + 遮光板 全部 MCP 按钮/旋钮(值与角度分离)、两侧 EFIS、时钟、主警告按钮 MCP_*、EFIS_*、GLARE_* 每个旋钮 INC/DEC/FAST/PUSH、每个按钮 PRESS/RELEASE + 绿条灯
P4 顶板其余(液压、空调引气、防冰、外部/内部灯光、增压、飞控、IRS、GPWS 护盖、FDR、厕所/呼叫) 按面板批量迁移,模式与 P1 相同 每个控件扫遍 + 机上验收表
P5 主面板 + 中央操纵台 + 油门台 + 驾驶盘 自动刹车、起落架手柄、DU 选择、襟翼/减速板/停留刹车、无线电/ACP/XPDR、火警手柄、配平切断、TOGA/A/T DISC/AP DISC/配平电门 同上;起落架/襟翼与 MSFS 物理一致
P6 清理 删除 model_io.json 与 lvars/hevents/writes 路径、H:LU737_W<n>、旧 L: 名;xpsim.cpp 去掉零散的 Zibo 名称;文档更新 生成器静态检查:0 个 laminar/ 引用

优先打通的 12 个“验证样板”控件(P1 第一天):BAT(含护盖)、STBY PWR(含护盖)、GPU、APU GEN1、GEN1、BUS TRANS、DC/AC 表选择钮、APU 开关、LIGHTS TEST、MCP CMD A、EFIS CPT RANGE、襟翼手柄。它们覆盖全部驱动模式(toggle、step_pair、spring_hold、hold、once、护盖、多位、杆),模式打通后其余控件只是填表。


7. Zibo 2.S1.50 已知缺失/改名 DataRef 的处理策略#

7.1 先测量,不靠猜:Probe#

Host 启动(或 --sci-probe)时,在插件加载完、跑 N 帧之后输出:

  • profile 中每个 DataRef/命令:FOUND / NOT_FOUND / FOUND_BUT_STATIC(N 帧内从未变化且等于 0,疑似“定义了但不再更新”)。
  • 插件实际注册的、名称与缺失项相似(编辑距离/同目录)的候选,供人工补别名。
  • 对点击区扫遍时“命令执行了但没有任何 DataRef 变化”的命令列表(疑似废弃命令)。

输出 LU737Host_sci_probe.txt,作为 profile 更新的依据。每个 Zibo 版本一个 profile 目录,可以继承:"extends": "zibo_2S1_40",只写差异。

7.2 分级策略#

类别 例 处理
A. 改名/移动 同一含义换了目录(electric/ ↔ electrical/)、capt/cpt 混用 read 别名链;质量 ALIAS;启动报告列出
B. 删除但可推导 power_on_buses(用户报告缺失)、各 *_status 汇流条量、DU 供电 ElecModel 合成;质量 SYNTH;仍存在的相关 Zibo 量(如 ac_tnsbus1_status、display_units/*)用来交叉校验
C. 位置量缺失、命令仍在 部分护盖 cover_position[i]、弹簧开关位置 影子状态 SHADOW:Host 记录已执行意图;冷暗/加载状态时复位为默认;若后来出现读回则自动升级为 OK
D. 命令缺失、位置可写 某些旋钮只有 DataRef 无命令 dref_write 驱动;写后读回,Zibo 覆盖则记 OVERRIDDEN
E. 由 X-Plane 自身系统产生,Host 中无人计算 sim/cockpit2/electrical/bus_volts、avionics_on、generator_on、APU_running、APU_N1_percent ElecModel / SimBridge 提供(§4.6),不再由开关位置直接推导
F. 插件期待 X-Plane 行为而 Host 未提供 APU START 后 Zibo 不调用 sim/electrical/APU_start(PROGRESS_LOG.md) profile effects:START 保持 ≥ 1 s 且 Zibo APU 未开始 → x_apu_start(Host 内置动作:触发 MSFS APU_STARTER 并把 APU N 回灌给插件);记 EFFECT_FALLBACK 日志,便于以后找出 Zibo 缺的启动条件后删除
G. 纯装饰、不可推导 振动、少量外观量 契约默认值,质量 MISSING,只影响该项;LU737_SYS_DEGRADED 计数

7.3 隔离原则(保证“单个 DataRef 缺失不导致大面积失效”)#

  1. 逐项解析、逐项降级:一个绑定失败只影响该状态;其余状态照常更新(不再出现一个 find() 失败导致整块逻辑跳过)。
  2. 供电门控永不依赖单一 DataRef:每个 _PWR 都有 ElecModel 合成回退;BUS_* 本身就是合成量。
  3. 默认值偏向“可用”而非“全黑”的仅限显示:_PWR 在主来源缺失时取合成值(物理一致),而不是取 0;指示灯缺失时取 0(不假亮)。
  4. 不一致时信 Zibo,但记录:主来源与合成不一致只记日志(限频,每状态每 10 s 一次),不覆盖 Zibo 的逻辑结果——Zibo 的特殊逻辑(例如电瓶下 FO DU 空白、DU 自检)得以保留。
  5. 契约哈希 + profile 版本写入日志头:任何用户日志都能看出所用 Zibo profile 与解析质量。

7.4 Zibo 升级流程(目标:MSFS 资源零改动)#

  1. 新 Zibo 放入 plugins/,运行 LU737Host --sci-probe --sci-profile zibo_<new>(新目录 extends 旧 profile)。
  2. 按探测报告补别名/合成/影子;运行 --sci-selftest 至全绿。
  3. 只发布 Host 与 profile;MSFS 包无需重建(契约哈希不变)。
  4. 只有当 Zibo 新增了真实存在的新控件时才扩展契约(追加 id,不重排),此时才需要重新生成模型行为。

附:与现有代码的对照#

现有 新方案中的去向
generate_behaviors.py: Mapper.dref/dref_lvar/cmd/write_event 由 --sci 路径替代;旧路径在 P6 删除
model_io.json (lvars/writes/hevents) avionics/generated/sci_ids.json(只有稳定 id)+ Host profile
index.ts: onInteractionEvent、lvarSlots 刷新 0x11 事件编码、0x90/0x91 写 L:
bridge.cpp 中命令持有/超时(held_、endAfterFrame_、kHoldTimeout) 移入 SCI 驱动器(规则不变,保留实现)
bridge.cpp 中 LU737_switch_battery_* 特例 删除,由 profile 绑定
xpsim.cpp 按开关位置推 bus_volts/avionics_on 由 ElecModel 输出(§4.6)
simbridge.ts(MSFS 物理 ↔ sim/*) 保留;APU/电源相关回灌改由 ElecModel 驱动