Technical reference

稳定驾驶舱接口(SCI)实施说明

docs/SCI_IMPLEMENTATION.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。对应设计:docs/COCKPIT_IO_REDESIGN.md。 已完成:全部 991 个驾驶舱点击区、模型用到的全部 959 个驾驶舱状态都走稳定接口;旧路径(model_io.json、L:LU737_<dataref>、H:LU737_<command>、H:LU737_W<n>)已删除(设计 §6 的 P6)。 其中电气面板、APU 开关、总线/供电/显示门控为手工整理(§3、§4),其余为按模型生成(§8)。尚未在 MSFS 中实机运行(离线测试见 §5、§8)。


1. 文件一览#

位置 内容
data/sci/contract/*.json 契约(版本无关):状态(名称、kind、enum、默认值)与控件(类型、护盖、瞬时位置)
data/sci/profiles/zibo_2S1_50/*.json Zibo 2.S1.50 绑定:读取别名链、合成表达式、影子状态、驱动模式、门控、效果检查
data/sci/model_map.json 模型映射(构建期):LevelUp OBJ 的命令/DataRef → SCI 事件/状态
xphost/src/sci.{h,cpp} Host SCI 核心:加载、解析、意图驱动、闭环判定、合成电气模型、协议输出
xphost/src/sci_test.cpp --sci-test 测试脚本执行器(事件、断言、sweep、命令 probe)
xphost/src/json.{h,cpp} 最小 JSON 读取器(允许 // 注释)
xphost/src/bridge.{h,cpp} 协议接入:0x10–0x12 输入,0x90–0x92 输出
xphost/src/xpsim.cpp X-Plane 自带电气量(bus_volts、avionics_on、电瓶电压)改由合成模型输出
xphost/test/sci/*.json 冷暗上电闭环测试、缺量注入(降级)测试
avionics/src/xpanel/sci.ts gauge 中继:事件按名转发、状态写 L:、显示门控、断线熄灭
avionics/src/xpanel/hostlink.ts 协议编解码(hello、event、names、values、acks)
avionics/harness/sci-e2e-entry.ts 端到端:gauge 的链路代码 ↔ 真实 Host + 原版插件
tools/msfs/generate_behaviors.py --sci 模型行为生成:只用 SCI 名,并对照契约静态检查;还有插件名或未知 L: 时构建失败
tools/sci/generate_cockpit_sci.py 从模型清单生成其余全部控件与状态(90_cockpit.json、model_generic.json)
data/sci/profiles/zibo_2S1_50/91_watch.json Host 实测的闭环监视列表(xphost/test/sci/discover.json 生成)
data/sci/profiles/zibo_2S1_50/95_fixes.json 手工修正:原版 OBJ 拼写错误、改名的 DataRef、本版 Zibo 未建模的控件
xphost/test/sci/all_controls.json 全部通用控件每个动作一次的闭环测试

Host 运行时从 可执行文件旁的 sci/ 读取契约与 profile(build.sh 从 data/sci 复制;xphost/dist/windows/sci/ 已随二进制提交)。

2. 与设计文档的差异(实现中做出的调整)#

设计 实现 原因
事件以 u16 control_id + u8 action 编码,契约哈希校验 事件按名称发送(0x11: u32 seq, f64 arg, utf8 name),Host 解析 <CONTROL>_<ACTION> 点击频率低,按名称发送最稳:gauge 不需要任何生成的 id 表,名字不认识时 Host 回 UNKNOWN
状态 id 由契约生成 Host 在 hello/resync 后发送 名称表(0x90),之后按序号发增量(0x91) gauge 与契约解耦;新增状态无需重建 gauge
contract_hash 未实现 名称表已覆盖其作用
0x93 质量帧 未发送;质量写入 Host 日志与 sci_report MSFS 侧暂不需要
DU 门控按物理 DU 区域 按面板顶层分组门控(PFD Captain→DU_CPT_OUTBD_PWR 等,见 sci.ts DISPLAY_GATES) 所有显示面共用一个材质/节点,无法按 DU 拆分发光;顶层分组与设备一一对应
写回 X-Plane 电气量取自"稳定总线状态" 取自 合成电气模型(E_*),不取 Zibo 的总线输出 实测:Zibo 由电瓶电压推导自己的汇流条,回写会形成逐帧 24.5/0 振荡

3. 实测得到的 Zibo 2.S1.50 行为(profile 的依据)#

全部在 Linux Host + 原版 lin_x64 插件上用 --sci-test 的 probe 测得:

项目 结论
power_on_buses 2.S1.50 中不存在,仓库中也无任何代码使用;电源状态改由 electric/*_status 与合成模型提供
电瓶 battery_dn = ON、battery_up = OFF;护盖合上会把电瓶推到 ON;冷暗时护盖是开的(cover_position[2]=1,1 = 开)
STBY PWR standby_bat_pos −1 BAT / 0 OFF / 1 AUTO;_left 向 BAT;护盖 cover_position[3],护盖合上时命令无效
GRD PWR / GEN / APU GEN *_pos −1 OFF(up 命令)/ 0 中位 / 1 ON(dn 命令),按住期间保持,松开回中
GPU 可用 laminar/B738/gpu_toggle(EFB),要求 on_ground 且地速 < 0.08;gpu_available 只读
DC/AC 表 均为 7 档(DC 表无 TEST 档)
BUS TRANSFER 未建模:zibomod 每帧把 sim/cockpit2/electrical/cross_tie 写回 1 → SCI 影子状态
display_units/* −1 = 空白,≥0 = 显示格式;冷暗时 udu=0,所以它不是供电指示 → DU 供电 = 总线 ∧ 非空白
APU Zibo 在 START 松开时调用 X-Plane 的 sim/electrical/APU_start,前提是开关已在 ON 位停留约 5–6 秒(4 秒不行,6 秒可以,与 START 按住时长无关);若此时 MSFS APU N1 已达约 20%,Zibo 不再起动。原先"APU 永远起不来"的根因即在此
旧 xpsim 合成 冷暗时 bus_volts=24.5、avionics_on=1;GPU 开关拨到 ON(无地面电源)时 bus_volts=28

4. Host 运行要点#

  • 延迟加载:插件跑满 3 帧后才解析 profile(部分 DataRef 在首帧才注册),第 300 帧再次尝试解析未命中的读取项。
  • 命令语义:once = begin → 跑一帧 → end;瞬时位置(弹簧)= 按下 begin、RELEASE 时 end(并受 min_hold_s 约束);同一控件的新意图会结束旧意图(REPLACED);按住 20 秒未松开自动 end。
  • 护盖:护盖闭合时,对开关的任何操作先变为 GUARD_OPEN,结果为 BLOCKED_GUARD。
  • 门控(gate):进入某位置前等待条件成立(APU START 等待 T_APU_SW_POS >= 7),超时(12 s)后仍下发并记录日志。
  • 效果检查(effects):进入某位置后在时限内检查期望(APU START 后 15 秒内 Z_APU_N1 > 1),超时记录 EFFECT_MISSING 并执行兜底动作(x_apu_start:Host 自己发出 APU_start → gauge → MSFS)。
  • 影子状态:位置读不到但命令存在 → 自动 SHADOW(由 Host 记录已执行的意图);BUS TRANSFER 明确为影子状态。
  • 合成电气模型:锁存各侧电源(GPU/APU/IDG,开关上升沿 + 可用性),推导各汇流条与 ELEC_STATE;Zibo 某侧持续有电 2 秒而模型无源时,按 elec_init 采纳 Zibo 的电源(暖启动)。
  • 表达式中可用的名称:所有状态名;E_<总线>(合成模型输出,如 E_XFR1、E_SRC1);T_<状态>(该状态距上次变化的秒数);Z_*(profile 内部状态,不发往 MSFS)。

日志示例(sci_log = 2):

[sci] t=5.033 seq=10 EV  APU_SW INC
[sci] t=5.033 seq=10 APU_SW waits for APU_SW_POS == 1 && T_APU_SW_POS >= 7
[sci] t=10.000 CMD laminar/B738/spring_toggle_switch/APU_start_pos_dn begin (held)
[sci] t=10.033 ST  APU_SW_POS 1->2 (OK)
[sci] t=10.533 CMD laminar/B738/spring_toggle_switch/APU_start_pos_dn end
[sci] t=10.567 seq=10 RES APU_SW INC OK -> START pos=ON 166 frames ; released
[sci] t=13.267 EFFECT APU_SW START: Z_APU_N1 > 1 OK after 3.2 s

5. 测试(全部离线,Linux Host + 原版插件)#

测试 内容 结果
xphost/test/sci/elec_cold_start.json 冷暗 → 电瓶 → STBY PWR(含护盖)→ GPU → APU(快速点 START)→ APU GEN → 9 个控件全位置 sweep → 回到冷暗;同时检查 X-Plane 电气量与 APU_start 是否转发给 MSFS 54/54 通过
xphost/test/sci/elec_degraded.json + --sci-hide "laminar/B738/electric/*" --sci-hide "laminar/B738/display_units/*" 同一流程,隐藏 Zibo 的全部电气与显示 DataRef(模拟改名/缺失) 53/53 通过(总线/DU 由合成模型给出,位置降级为影子状态)
avionics/harness/sci-e2e-entry.ts gauge 自己的 HostLink + SciClient 经 WebSocket 连接真实 Host:事件 → L: 变量 → 显示门控 → 断线熄灭 11/11 通过
avionics vitest 事件解析、门控、协议编解码(新增 7 项) 21/21 通过

运行命令见 docs/BUILD_AND_INSTALL.md(Tests without MSFS)。

6. 如何迁移一个新控件(P2 起每个控件照此办理)#

  1. 实测:写一个 probe 脚本,冷暗/上电各跑一次:
    {"only": "关键字1,关键字2", "steps": [{"frames": 30},
      {"probe": "laminar/B738/xxx_up", "frames": 10},
      {"probe": "laminar/B738/xxx_dn", "phase": "begin", "frames": 10},
      {"probe": "laminar/B738/xxx_dn", "phase": "end", "frames": 10}]}
    
    记录:哪个命令往哪个方向、位置 DataRef 的取值、是否需要护盖/电源/在地面、按住是否有区别。
  2. 契约(data/sci/contract/):加状态(<面板>_<控件>_POS,enum 用位置名)和控件(类型、guard、momentary/rest)。
  3. profile(data/sci/profiles/zibo_2S1_50/):read(含 map)+ drive(step_pair/toggle/pos_cmds/hold/once/dref),必要时 gate、effects、synth。
  4. 模型映射(data/sci/model_map.json):OBJ 命令 → 事件(<控件>_INC/DEC/TOGGLE/SET_n/GUARD_TOGGLE),OBJ DataRef → 状态(values 为各位置在 OBJ 中的原始值)。
  5. 测试:在 xphost/test/sci/ 加 sweep 与功能断言;跑一次 --sci-hide 降级。
  6. 生成:python3 tools/build_package.py --skip-models(或单独运行 generate_behaviors.py --sci),cd avionics && npm run build。生成器会拒绝契约中不存在的名字。

7. 未完成 / 已知限制#

  • 未在 MSFS 中运行;Windows 版 Host 已用新源码重新编译,但本环境没有 wine,只运行过 Linux 版。机上验证步骤见 TEST_PLAN.md §4b。
  • 暖启动(发动机运转)只能离线验证到"Zibo 与合成模型一致",发动机发电机供电需在 MSFS 中验证(SRC_IDG* 依赖 Zibo 的 N2)。
  • DU 门控按内容分组而非物理 DU:DU 选择钮把 PFD 切到内侧屏时,门控仍跟随原设备的供电(Zibo 自身的空白逻辑仍然生效)。
  • ANN_* 直接透传 Zibo 的亮度值(例如电瓶状态下 GRD POWER AVAILABLE 为 0.1);若机上偏暗,再在 profile 中加缩放。

8. 全部控件(生成部分)#

鼠标交互(滚轮、拖动、单击;事件名不变)见 docs/COCKPIT_CONTROLS.md。

8.1 做法#

  1. tools/sci/generate_cockpit_sci.py 读取模型清单(build/LU738_*.manifest.json),跳过 model_map.json 中已手工整理的部分:
    • 每个点击区 → 一个通用控件,名称取自节点名(BCOCKPIT_M_Fuel_Tank_L1_Pump → FUEL_TANK_L1_PUMP),动作与原 X-Plane 点击一致:
      原点击类型 动作 Host 操作
      command PRESS / RELEASE 按下 begin,松开 end
      command_switch_*、command_knob INC(第一个命令)/ DEC(第二个),RELEASE;滚轮 = 动作 + RELEASE 同上
      toggle TOGGLE DataRef 在两个值之间切换
      axis_*、delta、wrap INC / DEC DataRef 加减一步(限位或循环)
      drag_* INC / DEC 下一个档位(关键帧),或范围的 10%
    • 模型动画/显隐/发光用到的每个 DataRef → 一个状态,名称取自 DataRef(laminar/B738/toggle_switch/eng_start_source → TOGGLE_SWITCH_ENG_START_SOURCE),单位保持原值。
    • 名称一旦提交即为契约:以后 Zibo 改名只改 profile(如 95_fixes.json 的别名)。
  2. Host 的通用驱动(mode: actions)执行上述操作;同一控件的连续操作排队执行(快速转滚轮不丢步,e2e 测试验证);DataRef 写入已到限位时报 OK(注明 at its limit)。
  3. 闭环监视列表由 Host 实测(discover.json,约 4 分钟):地面、电瓶 + 地面电源,所有护盖先打开、护盖控件最后测;每个动作前后各有空闲窗口,并持续记录"动作结束后仍在变化"的量(IRS 对准计时、雨刷、闪烁)为全局噪声;按住期间也取快照(测试按钮)。结果写入 91_watch.json:每个控件最多 6 项,先 SCI 状态、后插件 DataRef(包括 CDU 行文字这类字符串)。
  4. 运行时每次点击:监视项有变化 → OK;没有监视项(该状态下不可观察)→ OK_SHADOW;有监视项但未变化 → REJECTED(插件没有响应,例如无电、无液压、到了命令旋钮的端点、页面不对)。

8.2 适配层修正(95_fixes.json)#

项目 处理
原版 OBJ 命令拼写 sim4flight_controls/door_open_4(4 个点击区) 改为 sim/flight_controls/door_open_4
OBJ 的 comm/obs_audio_vol_marker(MARKER 音量) Zibo 为 comm/obs_audio_vol_mark:别名 + 改写目标
HF SENS 第三部无线电(rtp_D),Zibo 只有 rtp_L/R 影子状态:旋钮能转,无逻辑
gauges/standby_altitude_baro 别名 gauges/standby_altimeter_baro
display/cat3 别名 placard/cat3
tab/rot_z、tab/fo_rot_z(EFB 旋转) 本版不存在,保持默认 0(与原版行为相同)
YAW_DAMPER(控件与状态同名) 控件改名 YAW_DAMPER_SW
外部着陆灯(xp_to_msfs_map.json 中引用旧 L: 名) 改为 SCI 状态 SWITCH_LAND_LIGHTS_*_POS

解析结果:1001 个状态(OK 974、ALIAS 3、SYNTH 19、SHADOW 3、MISSING 2),991 个控件中 990 个绑定(唯一未绑定的是有意为之的 BUS TRANSFER 影子状态)。

8.3 全部控件的闭环结果(all_controls.json,地面、电瓶 + 地面电源)#

项目 结果
实测到可观察效果的控件(discover.json) 709 / 974
全部动作(每个控件的每个动作执行一次) 1190
OK(监视项按预期变化) 863
OK_SHADOW(已下发;该状态下没有可观察量) 287
REJECTED(有监视项但插件这次没有响应) 40
UNBOUND / UNKNOWN 0

40 个 REJECTED 基本是端点或模式:命令旋钮已在端点(应答机某位为 0 时 DEC、ADF 细调)、V/S/速度窗口未激活时转旋钮、IRS 已在 OFF 时再 DEC、交输/隔离阀已在端点等——即插件确实没有响应这一下,正是闭环要报告的情况。

8.4 已知限制#

  • OK_SHADOW 的控件(实测时没有可观察效果)主要是:MSFS 侧生效的舱门命令、依赖当前页面的 EFB/FMC/HUD 按键、需要发动机或 A/T 的按钮(TO/GA、反推)。它们照常下发,只是无法闭环确认;在相应状态下重新运行 discover.json 可补充。
  • 外部风扇:L:LU737_fan_angle_1/2 由 gauge 按 MSFS 发动机 N1 在模拟时钟上逐帧积分(avionics/src/xpanel/fans.ts;暂停时停转;显示转速随 N1 增加、封顶 5 转/秒以避免频闪倒转)。此前没有任何写入方,风扇一直不转。
  • Host 每帧多读约 1000 个状态:Linux 上每帧约 3.1 ms(之前约 2.2 ms)。
  • 插件缺少某个写入目标 DataRef 时,该控件自动改为写对应的 SCI 状态(影子),开关/旋钮仍然能动并记日志;降级测试中 990/991 个控件仍可用。
  • 舱门/窗户命令的效果在 MSFS 一侧(Host 不可观察),报 OK_SHADOW。

8.5 重新生成(模型或 Zibo 版本变化后)#

python3 tools/sci/generate_cockpit_sci.py            # 契约/绑定/模型映射(名称已存在则保持)
xphost/build.sh linux
xphost/out/linux/lu737host --aircraft . --cold --no-sound --sci-log 0 --sci-test $PWD/xphost/test/sci/discover.json
xphost/out/linux/lu737host --aircraft . --cold --no-sound --sci-log 0 --sci-test $PWD/xphost/test/sci/all_controls.json
python3 tools/build_package.py --skip-models