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 强制):
- 同一命令同一时刻最多一个持有者;重复
begin被合并(计数,不重复调用插件)。 - 每个
begin必有一个end:来源可以是RELEASE、once的下一帧、超时、链路断开(releaseCommands())。 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>
生成器的静态检查(构建失败条件):
- 任何行为代码中出现
laminar/、LU737_后跟小写字母的旧名 → 失败。 - 每个可点击节点必须映射到一个控件;每个控件至少有一个可见的
_POS/_GRD/_BTN/_KNOB动画或明确标注no_anim。 - 每个
ANN_*至少驱动一个发光节点;每个带发光的指示灯节点必须由ANN_*/LT_*驱动。 - 未映射的 OBJ 动画 DataRef 列表输出到
build/sci_unmapped.txt(应为空或全部ignore)。
5. 闭环验证清单#
5.1 自动化(无 MSFS,CI 可跑)#
xphost 增加 --sci-selftest(Linux 版 + lin_x64 插件,现有测试方式):
- 解析报告:所有状态
quality != MISSING;ALIAS/SYNTH/SHADOW列表与 profile 预期一致(快照比对文件xphost/test/sci_expected_quality.txt)。 - 逐控件扫遍:对契约中每个控件、每个位置执行
SET n,断言:RES OK(或OK_SHADOW)在 ≤ 3 帧内;_POS等于 n;- profile 声明的
effects在时限内出现。
- 护盖:护盖闭合时
SET→BLOCKED_GUARD且护盖打开;开盖后SET生效。 - 按住语义:
PRESS后保持 N 帧,断言命令held>0持续 N 帧,RELEASE后为 0;丢失 RELEASE 时 20 s 超时释放。 - 冷暗上电脚本(断言每一步的总线与
_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,所有显示与灯熄灭 |
- 缺量注入:用
--sci-hide-dref <pattern>在 Host 中隐藏 DataRef(模拟 Zibo 改名/缺失),重跑 2 和 5:结果必须仍然通过(质量降为ALIAS/SYNTH/SHADOW),不得出现大面积_PWR=0。 - 契约一致性:生成器静态检查(§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 缺失不导致大面积失效”)#
- 逐项解析、逐项降级:一个绑定失败只影响该状态;其余状态照常更新(不再出现一个
find()失败导致整块逻辑跳过)。 - 供电门控永不依赖单一 DataRef:每个
_PWR都有 ElecModel 合成回退;BUS_*本身就是合成量。 - 默认值偏向“可用”而非“全黑”的仅限显示:
_PWR在主来源缺失时取合成值(物理一致),而不是取 0;指示灯缺失时取 0(不假亮)。 - 不一致时信 Zibo,但记录:主来源与合成不一致只记日志(限频,每状态每 10 s 一次),不覆盖 Zibo 的逻辑结果——Zibo 的特殊逻辑(例如电瓶下 FO DU 空白、DU 自检)得以保留。
- 契约哈希 + profile 版本写入日志头:任何用户日志都能看出所用 Zibo profile 与解析质量。
7.4 Zibo 升级流程(目标:MSFS 资源零改动)#
- 新 Zibo 放入
plugins/,运行LU737Host --sci-probe --sci-profile zibo_<new>(新目录extends旧 profile)。 - 按探测报告补别名/合成/影子;运行
--sci-selftest至全绿。 - 只发布 Host 与 profile;MSFS 包无需重建(契约哈希不变)。
- 只有当 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 驱动 |