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 起每个控件照此办理)#
- 实测:写一个 probe 脚本,冷暗/上电各跑一次:
记录:哪个命令往哪个方向、位置 DataRef 的取值、是否需要护盖/电源/在地面、按住是否有区别。{"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}]} - 契约(
data/sci/contract/):加状态(<面板>_<控件>_POS,enum 用位置名)和控件(类型、guard、momentary/rest)。 - profile(
data/sci/profiles/zibo_2S1_50/):read(含map)+drive(step_pair/toggle/pos_cmds/hold/once/dref),必要时gate、effects、synth。 - 模型映射(
data/sci/model_map.json):OBJ 命令 → 事件(<控件>_INC/DEC/TOGGLE/SET_n/GUARD_TOGGLE),OBJ DataRef → 状态(values为各位置在 OBJ 中的原始值)。 - 测试:在
xphost/test/sci/加 sweep 与功能断言;跑一次--sci-hide降级。 - 生成:
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 做法#
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 操作 commandPRESS / RELEASE 按下 begin,松开 end command_switch_*、command_knobINC(第一个命令)/ DEC(第二个),RELEASE;滚轮 = 动作 + RELEASE 同上 toggleTOGGLE DataRef 在两个值之间切换 axis_*、delta、wrapINC / 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的别名)。
- 每个点击区 → 一个通用控件,名称取自节点名(
- Host 的通用驱动(
mode: actions)执行上述操作;同一控件的连续操作排队执行(快速转滚轮不丢步,e2e 测试验证);DataRef 写入已到限位时报 OK(注明 at its limit)。 - 闭环监视列表由 Host 实测(
discover.json,约 4 分钟):地面、电瓶 + 地面电源,所有护盖先打开、护盖控件最后测;每个动作前后各有空闲窗口,并持续记录"动作结束后仍在变化"的量(IRS 对准计时、雨刷、闪烁)为全局噪声;按住期间也取快照(测试按钮)。结果写入91_watch.json:每个控件最多 6 项,先 SCI 状态、后插件 DataRef(包括 CDU 行文字这类字符串)。 - 运行时每次点击:监视项有变化 →
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