Nchu Aircraft Instrument Panel

Nchu Aircraft Instrument Panel

南昌航空大学飞机仪表面板模拟器

C++
EGE
X-Plane
Platform
VS
Version

基于 EGE 图形库(Easy Graphics Engine)的 Windows 桌面应用,用于模拟飞机驾驶舱仪表盘。支持两种机型,通过 Windows 共享内存接收 X-Plane 11 飞行模拟器的实时数据驱动仪表显示,无外部数据时自动运行内置 SIM 仿真模式。


目录

  1. 项目概述
  2. 项目结构
  3. EGE 图形库说明
  4. 核心架构
  5. 数据流与协议
  6. 仪表清单
  7. 音频告警系统
  8. 搭建与构建
  9. X-Plane Monitor 工具
  10. 开发者指南
  11. 开发问题记录
  12. 项目推进记录

1. 项目概述

1.1 项目背景

本项目源自南昌航空大学(Nchu) 飞行器设计与工程相关专业的教学与实践需求。在飞行模拟器系统中,驾驶舱仪表面板是飞行员与飞机之间最核心的人机交互界面——它负责将飞行参数(空速、高度、姿态、发动机状态等)以直观的机械仪表或电子显示方式呈现给飞行员。

传统飞行模拟器通常依赖专用硬件仪表或商业仿真软件(如 Prepar3D、X-Plane 自带的仪表盘),存在成本高、扩展性差、不易定制化等局限。本项目的目标是从零构建一套纯软件实现的、可实时渲染的飞机驾驶舱仪表面板,具备以下能力:

  • 双机型支持:同时模拟传统机械仪表(Cessna 172SP)和现代玻璃座舱(Boeing 737-800)两种典型的飞机仪表布局
  • 双数据源:既能在无外部连接时独立运行内置仿真引擎,也能接入 X-Plane 11 飞行模拟器的实时遥测数据
  • 跨设备通信:支持通过局域网(Windows 移动热点)在多台设备间传输飞行数据,实现"一台跑模拟器,一台跑仪表盘"的分布式部署
  • 低成本可扩展:基于开源图形库(EGE)和 Windows 标准 API,无需商业许可证即可自由扩展仪表种类

1.2 为什么选择 EGE 图形库

考量维度 说明
轻量级 EGE(Easy Graphics Engine)是对 Windows GDI+ 的轻量封装,核心库仅数百 KB,无需安装庞大运行时,生成的 exe 单文件即可运行
易于上手 提供类似 Borland BGI 风格的简洁 API,initgraph() + 绘图函数 + getch() 三步即可构建窗口应用,适合教学场景和快速原型开发
GDI+ 加速 基于 GDI+ 实现,在 Windows 上获得硬件加速的 2D 渲染(抗锯齿、渐变填充、Alpha 混合),无需学习 DirectX 或 OpenGL 的复杂管线
离屏缓冲 原生支持 PIMAGE 离屏缓冲机制,可构建完整的双缓冲渲染管线,消除画面闪烁
社区生态 由 xege.org 维护的开源项目,提供完整的中文文档和示例代码,在中国高校中有广泛使用基础

选择 EGE 而非其他备选方案的原因对比:

方案 优点 缺点 本项目评估
EGE (GDI+) 轻量、易用、无需运行时 无 3D 能力、性能中等 ✅ 选定
Direct2D/Direct3D 高性能、硬件加速 学习曲线陡峭、代码量大 ❌ 过度设计
Qt/QML 跨平台、组件丰富 体积大、许可证复杂、学习成本高 ❌ 太重
Win32 GDI 系统原生、稳定 无抗锯齿、无 Alpha 混合、开发效率低 ❌ 功能不足
OpenGL/Vulkan 跨平台、最高性能 复杂度远超本项目需求 ❌ 过度设计
Web-based (Canvas) 跨平台、易部署 性能受浏览器限制、需要 Web 服务器 ❌ 不适合桌面场景

1.3 数据流设计理念

本项目的核心设计原则是单一数据源(Single Source of Truth)——所有仪表渲染的数据均来自同一个 FlightParams 结构体实例,无论数据源是内置 SIM 仿真引擎还是外部 X-Plane:

SIM 仿真引擎 (sim_tick_default)
        │
        ▼
  FlightParams ──→ 仪表 1 (draw_xx_panel_from_udp)
        │           仪表 2
        │           仪表 3
        │           ...
        │
X-Plane ─→ xplane_monitor ─→ 共享内存 ─→ SharedMemReceiver
                                                    │
                                                    ▼
                                              FlightParams

这种设计的好处:

  • 模式切换平滑:SIM/UDP 模式切换只需改变 FlightParams 的填入来源,15 块仪表/8 个模块的绘制代码完全不需要改动
  • 渲染与数据解耦:仪表绘制函数只读地消费 FlightParams,不关心数据从哪来
  • 跨语言协作清晰:Python 负责网络通信(灵活、生态丰富),C++ 负责实时渲染(性能关键),共享内存作为零拷贝桥梁

1.4 项目技术栈详解

组件 技术 在本项目中的具体作用 选型理由
图形渲染 EGE 25.11(基于 GDI+) 窗口管理、双缓冲渲染管线、抗锯齿 2D 绘制(刻度/指针/弧段/文字)、背景图加载与缩放 轻量级 + 足够性能 + 易于教学
编程语言 C++17(Visual C++,/std:c++17) 核心逻辑:主循环、渲染管线、数据结构、Winsock2 UDP 通信、共享内存读取 高性能 + 系统级 API 访问 + EGE 原生支持
构建系统 MSBuild(Visual Studio 2022 / 2026 Preview) 解决方案(.sln)+ 项目文件(.vcxproj),条件配置 Debug/Release、多架构 x86/x64 VS 生态集成 + EGE 预编译库版本管理
UDP 通信 Winsock2(ws2_32.lib) X-Plane 的 UDP 数据广播接收(DATA 协议 + RREF 订阅响应) 跨平台标准协议 + 低延迟 + 无需 TCP 连接开销
进程间通信 Windows 命名共享内存(CreateFileMapping + MapViewOfFile) Python → C++ 零拷贝数据传输,FlightParams 结构体以 packed 二进制布局写入共享内存 零序列化开销 + 跨语言 + 极低延迟
音频告警 Windows PlaySound API(winmm.lib) 异步 .wav 播放,基于 FlightParams 实时判断触发条件(失速、高度、燃油低等) 零额外依赖 + SND_ASYNC 不阻塞渲染线程
数据采集 Python 3 + Tkinter xplane_monitor:UDP 监听、RREF 订阅管理、NIC 嗅探、CSV 录制/回放、共享内存写入、仪表盘 UI Python 网络生态丰富 + Tkinter 内嵌 GUI
网络接口枚举 psutil 枚举本机所有网卡 IP,实现多 IP 绑定策略(热点 + Wi-Fi + 回环) 跨平台网络接口信息获取
抓包分析 Scapy + Npcap 可选:二层网卡嗅探,旁路分析 X-Plane UDP 流量 无需修改 X-Plane 配置即可监控网络通信
打包部署 PyInstaller 将 xplane_monitor.py 打包为独立 exe,onefile/onedir 两种模式 零 Python 运行时依赖部署

1.5 支持的机型

机型 仪表布局 说明
Cessna 172SP(水上型) 6 块主仪表(中间 "六块")+ 9 块辅助仪表(左右两侧) 传统机械仪表布局,共 15 个槽位,涵盖中间 6 块主仪表和左右 9 块辅助仪表
Boeing 737-800 PFD + ND + CLOCK + ISFD + Upper/Lower DU + FMC/CDU 现代玻璃座舱布局,共 8 个显示单元模块

1.6 数据来源

模式 说明
SIM 模式(默认) 内置 sim_tick_default() 自动生成仿真飞行数据,无需外部连接即可独立演示
UDP 模式 通过 Windows 命名共享内存接收 X-Plane 11 飞行模拟器实时数据,驱动仪表真实反映飞行状态

按 Space 键可在两种模式间切换。两种模式共享同一套 FlightParams 数据结构和渲染管线。


2. 项目结构

AircraftInstrumentPanel/
│
├── AircraftInstrumentPanel.sln           ← Visual Studio 解决方案文件
├── AircraftInstrumentPanel.vcxproj       ← VS 项目文件(x64 Debug/Release)
├── AircraftInstrumentPanel.vcxproj.filters
├── AircraftInstrumentPanel.vcxproj.user  ← VS 用户设置(调试工作目录等)
├── COMMIT_CONVENTION.md                  ← 提交规范文档(Conventional Commits)
├── git-stats.sh                          ← Git 提交统计脚本
│
├── ege/                                  ★ EGE 图形库 v25.11(核心依赖)
│   ├── include/
│   │   ├── ege.h                         ← EGE 主头文件(自动导入 graphics.h)
│   │   ├── graphics.h                    ← 图形 API(项目直接引用)
│   │   ├── ege.zh_CN.h                   ← 中文资源文件
│   │   └── ege/                          ← 子模块(button/fps/label/sys_edit/types 等)
│   └── lib/
│       ├── vs2022/x64/graphics.lib       ← Release 静态库
│       ├── vs2022/x64/graphicsd.lib      ← Debug 静态库
│       └── vs2010~vs2026/                ← 各版本预编译库(含 x86/x64)
│
├── Nchu_Aircraft_instrument_panel/       ★ 项目源码根目录
│   │
│   ├── launcher.cpp                      ← 启动器入口:创建选择菜单窗口
│   ├── panel_interface.h                 ← PanelInterface 抽象接口定义
│   ├── panel_runtime.h / .cpp            ← 共享运行时:EGE 主循环、面板切换
│   │
│   ├── Cessna  Skyhawk(Floats)/          ★ Cessna 172SP(水上型)机型
│   │   ├── main.cpp                      ← 面板入口,导出 cessna_panel
│   │   ├── demo3.png                     ← 1920×1200 面板背景图
│   │   ├── gauge_common/                 ← 公共仪表基础设施
│   │   │   ├── headers/gauge_common.h    ← FlightParams(60 个字段)、数学/颜色工具
│   │   │   └── sources/gauge_common.cpp  ← SIM 仿真器、UDP 线程、绘图基元
│   │   ├── image_panel/                  ← 面板渲染管线 + 15 槽位布局
│   │   │   ├── headers/image_panel.h
│   │   │   └── sources/image_panel.cpp   ← 槽位定义、层合成、鼠标交互、预览
│   │   │
│   │   ├── Mid_1_1_Airspeed_Indicator/   ← 空速表
│   │   ├── Mid_1_2_Attitude_Indicator/   ← 姿态仪
│   │   ├── Mid_1_3_Altimeter/            ← 高度表
│   │   ├── Mid_2_1_Turn_Coordinator/     ← 转弯协调仪
│   │   ├── Mid_2_2_Heading_Indicator/    ← 航向指示器
│   │   ├── Mid_2_3_Vertical_Speed_Indicator/  ← 升降速度表
│   │   ├── Left_1_1_Clock_OAT_Volt/      ← 时钟/气温/电压表
│   │   ├── Left_2_1_Fuel_Quantity/       ← 燃油量表
│   │   ├── Left_2_2_EGT_Fuel_Flow/       ← 排气温度/燃油流量表
│   │   ├── Left_3_1_Oil_Temp_Pressure/   ← 油温/油压表
│   │   ├── Left_3_2_Vacuum_Ammeter/      ← 真空/电流表
│   │   ├── Right_1_1_VOR1_ILS_CDI/       ← VOR1/ILS CDI
│   │   ├── Right_2_1_VOR2_CDI/           ← VOR2 CDI
│   │   ├── Right_3_1_Tachometer_Hobbs/   ← 转速表/霍布斯计时
│   │   ├── Right_3_2_ADF_Indicator/      ← ADF 方位指示器
│   │   └── docs/
│   │       ├── gauge_integration_guide.md ← 仪表集成指南(比例映射、坐标系统)
│   │       └── purple_issue_fix.md        ← 紫色锯齿问题排查与修复
│   │
│   ├── Boeing737-800/                    ★ Boeing 737-800 机型
│   │   ├── main.cpp                      ← 面板入口,导出 boeing_panel
│   │   ├── Boeing737-800.png             ← 1920×1200 面板背景图
│   │   ├── Boeing737-800.txt             ← SVG 仪表槽位多边形标定数据
│   │   ├── B737_image_panel/             ← B737 面板渲染管理
│   │   ├── CLOCK/                        ← 飞行时钟
│   │   ├── MIP_PFD/                      ← 主飞行显示器(PFD)
│   │   ├── MIP_ND/                       ← 导航显示器(ND)
│   │   ├── ISFD/                         ← 集成备用飞行显示器
│   │   ├── UPPER_DU/                     ← 上部显示单元
│   │   ├── LOWER_DU/                     ← 下部显示单元
│   │   ├── FMC_CDU/                      ← 飞行管理计算机/CDU
│   │   └── B737_instrument_integration_guide.md ← B737 集成指南
│   │
│   ├── udp_module/                       ★ 数据通信模块
│   │   ├── headers/
│   │   │   ├── fleet_manager.h           ← 机队管理器(多机数据统一管理)
│   │   │   ├── shared_mem_receiver.h     ← 共享内存接收器(Python→C++ 桥接)
│   │   │   └── aircraft_config.h         ← 飞机配置数据结构
│   │   └── sources/
│   │       ├── fleet_manager.cpp
│   │       └── shared_mem_receiver.cpp
│   │
│   ├── audio_alert/                      ★ 音频告警模块
│   │   ├── headers/
│   │   │   ├── alert_rules.h             ← 告警规则引擎(C172 / B737 双规则表)
│   │   │   └── alert_sound.h             ← 异步 .wav 播放器(PlaySound API)
│   │   └── sources/
│   │       ├── alert_rules.cpp
│   │       └── alert_sound.cpp
│   │
│   └── alert/                            ← 📁 告警资源目录(.wav 文件等)
│
└── tools/                                ★ Python 辅助工具
    ├── xplane_monitor.py                 ← X-Plane UDP 监听/监控工具(Tkinter GUI)
    ├── build_exe.py                      ← PyInstaller 打包脚本(onefile / onedir)
    └── file_version.txt                  ← 版本信息(用于 exe 文件属性)

3. EGE 图形库说明

ege/ 目录是 Easy Graphics Engine (EGE) v25.11,由 xege.org 维护的开源 Windows 图形库。EGE 基于 GDI+ 封装,提供简洁易用的 C/C++ 绘图 API,兼具性能与可移植性。官方网站:https://xege.org,GitHub: x-ege/xege。

3.1 在项目中的配置

环节 配置值
头文件包含 #include <graphics.h> 或 #include <ege.h>
附加包含目录 ege\include
Release 链接库 graphics.lib → ege\lib\vs2022\x64\graphics.lib
Debug 链接库 graphicsd.lib → ege\lib\vs2022\x64\graphicsd.lib
系统链接依赖 gdiplus.lib gdi32.lib imm32.lib msimg32.lib ole32.lib oleaut32.lib winmm.lib uuid.lib
网络链接依赖 ws2_32.lib(Winsock2,UDP 通信)

项目根据 Visual Studio 版本自动选择 EGE 库目录:$(VisualStudioVersion)=='17.0' → vs2022\x64,否则 → vs2026\x64,未来可扩展支持更多版本。

3.2 EGE 核心 API 在本项目中的应用

API 用途
initgraph(), closegraph() 窗口创建与销毁(由共享运行时管理)
setinitmode() 初始化模式(如 INIT_RENDERMANUAL 手动控制渲染)
getimage(), putimage() 加载面板背景图、离屏缓冲输出
newimage(), delimage(), resize_f() 离屏缓冲管理(双缓冲消除闪烁)
ege_enable_aa() 开启抗锯齿,提高仪表刻度/指针渲染质量
setcolor(), setfillcolor(), settextcolor() 颜色状态控制
ege_fillcircle(), ege_arc(), ege_fillpoly() 图形基元绘制
outtextxy(), setfont(), setbkmode() 文字渲染
getkey(), random() 键盘输入与随机数
setcaption() 设置窗口标题

3.3 项目特定约定

  • 离屏缓冲:所有面板使用离屏 PIMAGE 缓冲完成全部绘制后,一次性 putimage 输出到窗口,彻底消除画面闪烁
  • 抗锯齿:离屏缓冲均调用 ege_enable_aa(true, buf) 开启抗锯齿
  • 坐标系统:x 向右为正,y 向下为正;角度 0° = 3 点钟方向,逆时针递增。
  • Winsock2 兼容:因 EGE 内部包含 windows.h(会拉入 winsock.h),项目中所有包含 winsock2.h 的源文件必须在包含任何 EGE/Windows 头文件之前先包含 winsock2.h,并定义 WIN32_LEAN_AND_MEAN。gauge_common.h 中对此有详细注释说明。

3.4 EGE 已知问题(紫色/残缺修复)

多个仪表曾出现文字发紫、画面残缺的问题,根因有二:

  1. 文字颜色状态残留:settextcolor() 未显式设置,沿用了前序模块的紫色状态 → 修复:所有 outtextxy() 前加 settextcolor()
  2. 弓形覆盖绘制顺序错误:ege_fillpoly() 绘制的弓形层在指针/文字之后执行,大面积遮盖前景 → 修复:将弓形绘制移至指针之前

详见 docs/purple_issue_fix.md。


4. 核心架构

4.1 软件分层

┌──────────────────────────────────────────────────────┐
│                    Launcher (启动器)                    │
│            launcher.cpp — 选择菜单窗口                  │
├──────────────────────────────────────────────────────┤
│               Shared Runtime (共享运行时)               │
│         panel_runtime.cpp — EGE 主循环、面板切换        │
│         panel_interface.h — PanelInterface 抽象        │
├──────────────────────────────────────────────────────┤
│  ┌─────────────────┐  ┌─────────────────────────────┐ │
│  │  Cessna 172SP    │  │  Boeing 737-800             │ │
│  │  main.cpp        │  │  main.cpp                   │ │
│  │  image_panel/    │  │  B737_image_panel/          │ │
│  │  gauge_common/   │  │  (共用 gauge_common)        │ │
│  │  6+9 块仪表      │  │  8 个 DU 模块               │ │
│  └────────┬─────────┘  └───────────┬─────────────────┘ │
│           │                         │                   │
│           └──────────┬──────────────┘                   │
│                      ▼                                  │
│  ┌──────────────────────────────────────────────────┐   │
│  │           udp_module (数据通信层)                  │   │
│  │  FleetManager → SharedMemReceiver → FlightParams │   │
│  └──────────────────────────────────────────────────┘   │
│                      │                                   │
│  ┌──────────────────────────────────────────────────┐   │
│  │           audio_alert (音频告警层)                 │   │
│  │  AlertRules → AlertSound → PlaySound API          │   │
│  └──────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────┘

4.2 启动流程

启动 AircraftInstrumentPanel.exe
    │
    ▼
┌─────────────────────────────────┐
│   启动器选择窗口                 │
│                                 │
│  [1] Cessna 172SP               │
│  [2] Boeing 737-800             │
│  [3] X-Plane Monitor(外部工具)│
│  [4] Exit                       │
└─────────────────────────────────┘
    │
    ▼ 选择 1 或 2 → DestroyWindow 关闭启动器
panel_main(initial_panel_id)    ← 共享运行时入口(可多次调用)
    │
    ├─ DPI 感知 → 计算缩放后的窗口尺寸
    ├─ initgraph() 创建 EGE 窗口(仅首次调用时)
    ├─ 复用已有 EGE 窗口(非首次调用,恢复显示)
    ├─ 加载面板背景图(getimage)
    ├─ 创建离屏渲染缓冲(newimage + 抗锯齿)
    ├─ 调用面板 init 回调(初始化仪表、FleetManager 等)
    └─ 进入主循环:
       ├─ Win32 消息循环(PeekMessage 非阻塞)
       ├─ 窗口尺寸变化检测与自动缩放
       ├─ F11 全屏切换(GetAsyncKeyState 非阻塞检测)
       ├─ 数字键 1/2 面板切换(仅替换 init/shutdown 回调)
       ├─ 处理鼠标事件
       ├─ update(dt) → 仿真推进 / UDP 数据同步
       ├─ render(w, h) → 绘制到离屏缓冲 → putimage 输出
       └─ 循环直到 Esc → 返回启动器
    │
    ▼ 程序退出
panel_cleanup()    ← 彻底释放 EGE 资源

4.3 PanelInterface 抽象接口

每个机型通过统一的 PanelInterface 结构体向共享运行时注册。运行时通过回调驱动面板生命周期:

typedef struct {
    const char *name;            // 面板名称(显示在窗口标题)
    int logical_w, logical_h;    // 面板逻辑尺寸(用于初始窗口大小)
    int (*init)(int w, int h);   // 初始化:加载资源、创建离屏缓冲、启动 FleetManager
    void (*shutdown)(void);      // 清理:释放图片、关闭共享内存
    void (*update)(double dt);   // 每帧更新:仿真物理推进或 UDP 数据同步
    void (*render)(int w, int h);// 每帧渲染:绘制到 EGE 离屏缓冲
    void (*on_space)(void);      // Space 键:切换 SIM/UDP 模式
    void (*on_mouse)(const mouse_msg *msg);  // 鼠标事件(悬停、双击放大等)
    void (*on_resize)(int w, int h);         // 窗口缩放回调
} PanelInterface;

两个面板实例分别在各自 main.cpp 中定义:extern PanelInterface cessna_panel 和 extern PanelInterface boeing_panel,运行时通过 g_panels[0] / g_panels[1] 引用。

4.4 渲染管线

每帧 render(w, h) 调用:
    │
    ├─ 1. 绘制面板背景图(缩放到窗口尺寸)
    ├─ 2. 遍历 15 个(或 B737 的 8 个)仪表槽位
    │      ├─ 计算槽位中心、半径(比例映射)
    │      ├─ 调用该仪表的 draw_xxx_panel_from_udp()
    │      │     ├─ 绘制静态刻度/数字/标签
    │      │     ├─ 根据 FlightParams 绘制指针/数值
    │      │     └─ 绘制弓形覆盖、塑料边框效果
    │      └─ 下一个槽位
    ├─ 3. 绘制状态指示(模式标签、数据链路状态等)
    └─ 4. putimage() 将离屏缓冲输出到窗口

4.5 运行时快捷键

按键 功能
1 切换到 Cessna 172SP(仅替换面板回调,复用 EGE 窗口)
2 切换到 Boeing 737-800(仅替换面板回调,复用 EGE 窗口)
Space 切换 SIM(仿真)/ UDP(外部数据)模式
F11 切换全屏(WS_POPUP 模式)
Esc 返回启动器选择窗口(panel_main 可多次调用,支持往返)

panel_main() 支持多次调用:首次调用初始化 EGE 窗口,后续调用复用已有窗口。
程序退出前应调用 panel_cleanup() 彻底释放 EGE 资源。panel_main() 与 panel_cleanup() 成对使用。

4.6 共享运行时 API

// 进入仪表面板共享运行时
int panel_main(int initial_panel_id);
//  initial_panel_id: 1 = Cessna 172SP, 2 = Boeing 737-800
//  返回: 0 正常退出, 非 0 错误
//  可多次调用。首次调用初始化 EGE 窗口,后续调用复用已有窗口。

// 彻底关闭 EGE 图形系统(程序退出前调用)
void panel_cleanup(void);
//  与 panel_main() 成对使用。调用后不能再调用 panel_main()。

5. 数据流与协议

5.1 完整数据管道

┌─────────────────────────────────────────────────────────┐
│                    X-Plane 11                            │
│  通过 UDP 端口 49000/49010/49020 广播飞行数据            │
│  协议: DATA (每帧)、RREF (订阅响应)、BEACON (发现)       │
└──────────────────────┬──────────────────────────────────┘
                       │ UDP 网络包
                       ▼
┌─────────────────────────────────────────────────────────┐
│           xplane_monitor.py (Python 监听器)              │
│                                                         │
│  1. UDP 监听线程 → 解析 DATA/RREF/BEACON 包             │
│  2. 维护 live_values 字典 (实时数据缓存)                 │
│  3. 共享内存写入线程 → 序列化 FlightParams → 写 mmap    │
│  4. (可选) NIC 嗅探 → Scapy 抓包分析                    │
│  5. (可选) CSV 录制/回放                                 │
└──────────────────────┬──────────────────────────────────┘
                       │ Windows 命名共享内存
                       │ "Local\AircraftPanelSharedMem"
                       │ 4096 字节, packed 二进制布局
                       ▼
┌─────────────────────────────────────────────────────────┐
│     SharedMemReceiver (C++, udp_module)                  │
│                                                         │
│  1. OpenFileMapping → MapViewOfFile → pData             │
│  2. 检查 data_ready 标志 (volatile LONG)                │
│  3. 读取 FlightParams → 清零 data_ready                 │
│  4. 由 FleetManager 分发到面板                          │
└──────────────────────┬──────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────┐
│     FleetManager → FlightParams → 各仪表绘制函数         │
│     最终由 EGE putimage() 渲染到屏幕                     │
└─────────────────────────────────────────────────────────┘

5.2 共享内存协议

项目 值
共享内存名称 Local\AircraftPanelSharedMem
大小 4096 字节
布局 SharedMemLayout(packed 对齐)
同步方式 volatile LONG data_ready 标志位
#pragma pack(push, 1)
typedef struct {
    volatile LONG data_ready;   // 1 = 有新数据, 读取后置 0
    FlightParams params;        // 60 个字段的飞行数据
} SharedMemLayout;
#pragma pack(pop)

Python 端 (xplane_monitor) 使用 ctypes 构造相同的二进制布局写入,C++ 端通过 SharedMemReceiver 读取。零拷贝、零序列化开销。

5.3 FlightParams 数据结构

FlightParams 结构体包含 60 个字段(46 个 double + 14 个 int),定义于 gauge_common.h:

类别 字段 单位 来源
姿态 theta (俯仰), phi (滚转), psi (航向) 度 X-Plane RREF
飞行 alt_m (高度), ias_kts (空速), vs_fpm (升降率) 米/节/英尺·分⁻¹ X-Plane RREF
Left_1_1 oat_c, bus_volts, clock_seconds °C/V/s X-Plane + 本地仿真
Left_2_1 fuel_left_gal, fuel_right_gal 加仑 X-Plane RREF
Left_2_2 egt_c, fuel_flow_gph °C/gal·h⁻¹ X-Plane RREF
Left_3_1 oil_temp_c, oil_press_psi °C/psi X-Plane RREF
Left_3_2 vacuum_inhg, ammeter_amp inHg/A X-Plane RREF
Right_1_1 nav1_obs_deg, nav1_dev_dot, nav1_gs_dot, nav1_to_from, nav1_nav_flag, nav1_gs_flag, nav_source_mode 度/dot/— X-Plane RREF
Right_2_1 nav2_obs_deg, nav2_dev_dot, nav2_to_from, nav2_nav_flag 度/dot/— X-Plane RREF
Right_3_1 rpm, hobbs_hours RPM/h X-Plane RREF
Right_3_2 adf_bearing_deg, adf_card_deg 度 X-Plane RREF + 本地派生
B737 左发 n1_left, n2_left, egt_left_c, ff_left_kgph, vib_left, oil_press_left_psi, oil_temp_left_c, oil_qty_left %/°C/kg·h⁻¹/— X-Plane RREF
B737 右发 n1_right, n2_right, egt_right_c, ff_right_kgph, vib_right, oil_press_right_psi, oil_temp_right_c, oil_qty_right %/°C/kg·h⁻¹/— X-Plane RREF
B737 中央油箱 fuel_center_gal 加仑 X-Plane RREF
告警系统 radio_alt_ft, gear_deploy_ratio, target_alt_ft 英尺/比率/英尺 X-Plane RREF
X-Plane 告警标志位 annun_stall, annun_overspeed, annun_engine_fire1, annun_engine_fire2, annun_gear_unsafe, annun_oil_pressure, annun_oil_temperature, annun_fuel_quantity 0/1 X-Plane 原生

5.4 X-Plane RREF 映射

xplane_monitor 通过 RREF 协议订阅 X-Plane dataref,按索引映射到 FlightParams:

Index 字段 X-Plane dataref 换算
0 theta sim/cockpit2/gauges/indicators/pitch_deg —
1 phi sim/cockpit2/gauges/indicators/roll_deg —
2 psi sim/cockpit2/gauges/indicators/heading_electric_deg 缠绕修正
3 ias_kts sim/cockpit2/gauges/indicators/airspeed_kts_pilot 指数平滑
4 alt_m sim/cockpit2/gauges/indicators/altitude_ft_pilot ×0.3048
5 vs_fpm sim/cockpit2/gauges/indicators/vvi_fpm_pilot 指数平滑
6 oat_c sim/cockpit2/temperature/outside_air_temp_degc —
7 bus_volts sim/cockpit2/electrical/bus_volts —
8 fuel_left_gal sim/cockpit2/fuel/fuel_quantity_left_gal —
9 fuel_right_gal sim/cockpit2/fuel/fuel_quantity_right_gal —
10 egt_c sim/cockpit2/engine/indicators/egt_deg_C —
11 fuel_flow_gph sim/cockpit2/engine/indicators/fuel_flow_kg_sec ÷2.72×3600
12 oil_temp_c sim/cockpit2/engine/indicators/oil_temperature_deg_C —
13 oil_press_psi sim/cockpit2/engine/indicators/oil_pressure_psi —
14 vacuum_inhg sim/cockpit2/electrical/vacuum_in_hg —
15 ammeter_amp sim/cockpit2/electrical/ammeter_indicated_amps —
16 nav1_obs_deg sim/cockpit2/radios/indicators/nav1_obs_deg —
17 nav1_dev_dot sim/cockpit2/radios/indicators/nav1_deviation_dot —
18 nav1_gs_dot sim/cockpit2/radios/indicators/nav1_gs_deviation_dot —
19 nav1_flag sim/cockpit2/radios/indicators/nav1_flag → to_from/nav_flag/gs_flag
20 nav2_obs_deg sim/cockpit/radios/nav2_obs_degm —
21 nav2_dev_dot sim/cockpit/radios/nav2_hdef_dot —
22 nav2_flag sim/cockpit2/radios/indicators/nav2_flag → to_from/nav_flag
23 rpm sim/cockpit2/engine/indicators/engine_speed_rpm[0] —
24 hobbs_hours sim/cockpit2/engine/indicators/hobbs_hours —
25 adf_bearing_deg sim/cockpit/radios/adf1_bearing_deg —
26 adf_card_deg sim/flightmodel/position/mag_psi 与航向同步
27 n1_left sim/cockpit2/engine/indicators/N1_percent[0] —
28 n1_right sim/cockpit2/engine/indicators/N1_percent[1] —
29 n2_left sim/cockpit2/engine/indicators/N2_percent[0] —
30 n2_right sim/cockpit2/engine/indicators/N2_percent[1] —
31 egt_left_c sim/cockpit2/engine/indicators/EGT_deg_C[0] —
32 egt_right_c sim/cockpit2/engine/indicators/EGT_deg_C[1] —
33 ff_left_kgph sim/cockpit2/engine/indicators/fuel_flow_kg_sec[0] ×3600
34 ff_right_kgph sim/cockpit2/engine/indicators/fuel_flow_kg_sec[1] ×3600
35 fuel_center_gal sim/cockpit2/fuel/fuel_quantity[2] ÷2.85
36 vib_left sim/flightmodel2/engines/vibration[0] —
37 vib_right sim/flightmodel2/engines/vibration[1] —
38 oil_press_left_psi sim/cockpit2/engine/indicators/oil_pressure_psi[0] —
39 oil_press_right_psi sim/cockpit2/engine/indicators/oil_pressure_psi[1] —
40 oil_temp_left_c sim/cockpit2/engine/indicators/oil_temperature_deg_C[0] —
41 oil_temp_right_c sim/cockpit2/engine/indicators/oil_temperature_deg_C[1] —
42 radio_alt_ft sim/cockpit2/gauges/indicators/radio_altimeter_height_ft_pilot —
43 gear_deploy_ratio sim/flightmodel2/gear/deploy_ratio[0] —
44 target_alt_ft sim/cockpit2/autopilot/altitude_dial_ft —
45 annun_stall sim/cockpit2/annunciators/stall_warning —
46 annun_overspeed sim/cockpit2/annunciators/overspeed —
47 annun_engine_fire1 sim/cockpit2/annunciators/engine_fire[0] —
48 annun_engine_fire2 sim/cockpit2/annunciators/engine_fire[1] —
49 annun_gear_unsafe sim/cockpit2/annunciators/gear_unsafe —
50 annun_oil_pressure sim/cockpit2/annunciators/oil_pressure —
51 annun_oil_temperature sim/cockpit2/annunciators/oil_temperature —
52 annun_fuel_quantity sim/cockpit2/annunciators/fuel_quantity —

指数平滑系数 α=0.3 用于平滑姿态、空速等噪声较大的数据。

5.5 SIM 仿真模式

当无 X-Plane 连接时,gauge_common.cpp 中的 sim_tick_default() 自动生成仿真数据:

  • 姿态:小幅正弦振荡模拟湍流
  • 空速/高度:巡航恒定值
  • 燃油:随时间递减
  • 发动机参数:根据推力状态变化
  • 导航:预设 OBS 缓转、偏离振荡

此模式确保项目可在无任何外部依赖的情况下独立演示和开发。


6. 仪表清单

6.1 Cessna 172SP 仪表盘

面板背景图 demo3.png(1920×1200)定义了 15 个仪表槽位,涵盖中间 6 块主仪表和左右 9 块辅助仪表。

槽位 工程名 说明
Mid_1_1 Airspeed_Indicator 空速表,含彩色弧段(白/绿/黄/红区)
Mid_1_2 Attitude_Indicator 姿态仪,蓝天/大地背景,俯仰滚转指示
Mid_1_3 Altimeter 三针高度表,含气压设定窗
Mid_2_1 Turn_Coordinator 转弯协调仪,飞机符号 + 侧滑球
Mid_2_2 Heading_Indicator 航向陀螺,含 OBS 设定指针
Mid_2_3 Vertical_Speed_Indicator 升降速度表,±2000 fpm
Left_1_1 Clock_OAT_Volt 数码组合表(时钟/气温/电压),LCD 数码管显示
Left_2_1 Fuel_Quantity 双油箱双针燃油量表,88° 对称弧段
Left_2_2 EGT_Fuel_Flow 排气温度/燃油流量组合表,浅绿色安全弧带
Left_3_1 Oil_Temp_Pressure 滑油温度/压力双针表,含绿色安全弧带
Left_3_2 Vacuum_Ammeter 真空度/电流组合表,对称弓形弧段
Right_1_1 VOR1_ILS_CDI VOR1/ILS 航道偏离指示器,含下滑道菱形
Right_2_1 VOR2_CDI VOR2 航道偏离指示器,含 TO/FROM 指示器
Right_3_1 Tachometer_Hobbs 转速表/霍布斯计时表,绿色安全弧区
Right_3_2 ADF_Indicator ADF 方位指示器,含黄色方位指针

6.2 Boeing 737-800 仪表盘

面板背景图 Boeing737-800.png(1920×1200),基于 SVG 标定数据定义槽位多边形。

模块 说明
CLOCK 飞行时钟,八边形面板,7 段数码管 CHR/ET 双模式
MIP_PFD 主飞行显示器(PFD)— 姿态、空速带、高度带、航向
MIP_ND 导航显示器(ND)— 旋转罗盘弧、航点渲染、距离环
ISFD 集成备用飞行显示器(ADI + 高度 + 速度 + 罗盘)
UPPER_DU 上部显示单元 — N1/EGT/燃油流量/油量,16x 超采样抗锯齿
LOWER_DU 下部显示单元 — N2/滑油压力/温度/振动,含内部仿真引擎
FMC_CDU 飞行管理计算机 — 完整 CDU 键盘与 LCD 屏 UI
B737_image_panel 面板渲染管理 — 离屏缓冲池、槽位合成、双击预览窗口

7. 音频告警系统

音频告警模块位于 audio_alert/,为仪表面板提供基于飞行参数的声音告警能力。

7.1 架构

alert_rules_evaluate(fp, is_boeing, now)
    │
    ├─ 遍历机型对应的告警规则表
    │      ├─ 调用规则 check_fn(fp) 判断条件
    │      ├─ 通过去抖时间(debounce_sec)防频繁触发
    │      └─ 条件满足 → 调用 alert_sound_play(filename)
    │
    └─ AlertRule 结构体
           ├─ id / name              ← 规则标识
           ├─ wav_filename           ← 对应的 .wav 文件
           ├─ debounce_sec           ← 去抖时间
           └─ check_fn               ← 条件判断函数指针

7.2 告警规则

支持 Cessna 172SP 和 Boeing 737-800 两套独立规则表(最多 32 条规则):

规则示例 触发条件 机型
失速告警 空速低于失速门限 C172 + B737
高度告警 低于最低安全高度 C172 + B737
燃油低 燃油量低于设定值 C172 + B737
超速告警 空速超过 Vne C172 + B737
发动机参数超限 EGT/油温/油压越界 C172 (+ B737 双发)

7.3 音频引擎

基于 Windows PlaySound API 实现异步 .wav 播放:

API 说明
alert_sound_init(base_dir) 指定 .wav 文件目录
alert_sound_play(filename) 异步播放,不阻塞渲染线程
alert_sound_stop_all() 停止所有播放
alert_sound_shutdown() 释放资源

依赖 winmm.lib(已在项目链接器中配置)。

7.4 告警音效资源

alert/ 目录包含 37 个 .wav 文件,按功能分类如下:

类别 文件 说明
GPWS 高度呼叫 1000ft.wav 500ft.wav 400ft.wav 300ft.wav 200ft.wav 100ft.wav 50ft.wav 40ft.wav 30ft.wav 20ft.wav 10ft.wav 近地警告系统递减高度语音提示
失速告警 stall.wav 失速警告
超速告警 transonic.wav 跨/超音速警告
起落架警告 gear_warn_1.wav gear_warn_2.wav 起落架未放下警告
火警 fire_bell.wav 发动机火警铃声
下滑道警告 glideslope.wav ILS 下滑道偏离警告
自动驾驶警告 autopilot_disco.wav autopilot_fail.wav 自动驾驶断开/故障警告
TCAS 告警 tcas.wav 交通防撞系统告警
高度提醒 altitude_alert.wav 目标高度接近提醒
风切变 wshr.wav 风切变警告
近地警告 pull.wav sink.wav 拉起/下沉率警告
NAV 标识 dot_INNER.wav dot_MIDDLE.wav dot_OUTER.wav dash_MIDDLE.wav dash_MORSE.wav dash_OUTER.wav 导航台摩尔斯识别码(内/中/外)
系统音 seatbelt.wav mini.wav radar_lock.wav lo_rotor.wav te_climb.wav te_descend.wav alternator_off.wav 安全带/雷达锁定/发电机断开等

所有 .wav 文件由 alert_sound_init() 在面板初始化时加载目录路径,通过 alert_sound_play() 异步播放,不阻塞渲染线程。


8. 搭建与构建

8.1 前置条件

组件 要求
Visual Studio 2022(v143 工具集)或 2026 Preview(v145 工具集)
工作负载 「使用 C++ 的桌面开发」
Windows SDK 10.0(项目目标版本)
操作系统 Windows 10/11 64 位
Python 3.7+(仅运行 xplane_monitor.py 时需要)

8.2 构建步骤

方式一:Visual Studio IDE(推荐)

  1. 双击打开 AircraftInstrumentPanel.sln
  2. 工具栏选择 x64 平台 + Debug / Release 配置
  3. Ctrl+Shift+B 编译
  4. 输出:
    • build/x64/Debug/AircraftInstrumentPanel.exe
    • build/x64/Release/AircraftInstrumentPanel.exe

方式二:命令行 MSBuild

# Debug
msbuild AircraftInstrumentPanel.sln /p:Configuration=Debug /p:Platform=x64

# Release
msbuild AircraftInstrumentPanel.sln /p:Configuration=Release /p:Platform=x64

8.3 运行

直接运行编译生成的 AircraftInstrumentPanel.exe。

⚠️ 工作目录要求:运行时的 CWD 需能访问背景图片(demo3.png / Boeing737-800.png)。建议:

  • 在项目根目录启动 exe
  • 或在 VS 中设置「工作目录」为 $(ProjectDir)..\
  • 启动器会自动搜索 exe 同级目录、项目子目录等多个候选路径

8.4 编译配置详解

配置项 Debug Release
输出目录 build/x64/Debug/ build/x64/Release/
中间目录 build/obj/x64/Debug/... build/obj/x64/Release/...
EGE 链接库 graphicsd.lib graphics.lib
优化 无 (/Od) 全程序优化 (/GL, /LTCG)
运行时库 多线程调试 DLL (/MDd) 多线程 DLL (/MD)
预处理器 _DEBUG NDEBUG
C++ 标准 C++17 (/std:c++17) C++17 (/std:c++17)
字符集 Unicode Unicode
多核编译 启用 (/MP) 启用 (/MP)

8.5 调试提示

  • VS 中按 F5 启动调试,确保工作目录设置为 $(ProjectDir)..\(项目 → 属性 → 调试 → 工作目录)
  • 控制台输出可通过 OutputDebugStringA() 查看(VS 输出窗口的「调试」面板)
  • EGE 窗口模式下 printf 不可见,建议用 OutputDebugStringA 调试

9. X-Plane Monitor 工具

tools/xplane_monitor.py 是系统的 数据中转中枢,承担 X-Plane 飞行模拟器与 C++ 仪表面板之间的实时数据桥接角色。它是一款基于 Tkinter 的 Windows 桌面应用(暗色 VSCode 风格界面),既可以作为独立的 UDP 网络监控调试工具使用,也是连接 X-Plane 与仪表面板程序的必需桥梁。

9.1 核心功能与定位

┌──────────────────────────────────────────────────────────────────┐
│                      xplane_monitor.py                            │
│                                                                  │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────────────┐  │
│  │ UDP 监听引擎  │   │ NIC 网卡嗅探  │   │  RREF 订阅管理器     │  │
│  │ (select轮询)  │   │ (Scapy抓包)   │   │  (预设/手动/自动重发) │  │
│  └──────┬───────┘   └──────┬───────┘   └──────────┬───────────┘  │
│         │                 │                       │              │
│         └────────┬────────┘───────────────────────┘              │
│                  ▼                                                │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              数据汇聚层 (live_values 字典)                  │   │
│  │  多数据源合并: DATA包 + RREF响应 + NIC嗅探 → 统一键值缓存  │   │
│  └──────────────────────┬───────────────────────────────────┘   │
│                         │                                        │
│          ┌──────────────┼──────────────┐                        │
│          ▼              ▼              ▼                         │
│  ┌────────────┐ ┌────────────┐ ┌──────────────────┐            │
│  │ 实时数据显示 │ │ CSV录制/回放 │ │ 共享内存转发       │            │
│  │ (仪表卡片UI) │ │ (Flightdata) │ │ (InstrumentForwarder)│          │
│  └────────────┘ └────────────┘ └────────┬─────────┘            │
│                                         │                        │
│                                         ▼                        │
│                              Windows 命名共享内存                │
│                           "Local\AircraftPanelSharedMem"        │
│                           4096 bytes, packed binary             │
└──────────────────────────────────────────────────────────────────┘
角色 说明
UDP 网关 监听局域网内 X-Plane 广播的 UDP 数据包,解析 X-Plane 私有协议(DATA / RREF / BEACON / DREF / CMND),屏蔽底层网络细节
协议转换器 将 X-Plane 的 UDP 数据报转换为 FlightParams 结构化数据,通过共享内存零拷贝传递给 C++ 面板,实现跨语言、跨进程通信
多源数据融合 同时接收 X-Plane 推模式(DATA 帧周期广播)和拉模式(RREF 订阅响应)数据,合并为统一的实时数据视图,两者互为补充和校验
会话管理层 维护 RREF 订阅生命周期(自动重发过期订阅)、飞行数据录制与回放、多飞机数据隔离
网络诊断工具 提供 NIC 级抓包分析(基于 Scapy)、端口流量统计、数据包类型分类、方向自动判断,辅助排查 X-Plane 网络通信问题

9.2 架构分层详解

9.2.1 UDP 监听引擎 — UDPListenerThread

核心网络层,基于 select.select() 事件驱动模型实现非阻塞 I/O 多路复用:

class UDPListenerThread(threading.Thread):
    def setup_sockets(self):
        # 当 bind_ip == "0.0.0.0" 时,自动绑定所有可用 IPv4 地址
        # 每个 (IP, Port) 对创建独立的 socket
        for bind_ip in get_bindable_ipv4s():
            for port in self.ports:
                s = socket.socket(AF_INET, SOCK_DGRAM)
                s.setsockopt(SOL_SOCKET, SO_RCVBUF, 1024 * 1024)  # 1MB 接收缓冲
                s.bind((bind_ip, port))
                s.setblocking(False)
                self.sockets.append(s)

关键特性:

特性 说明
多 IP 同时绑定 不绑定通配符 0.0.0.0,而是逐一绑定本机所有网卡 IP(含热点 IP 192.168.137.1),精确控制数据来源
大缓冲 设置 1MB 接收缓冲区(SO_RCVBUF),应对高帧率突发数据
非阻塞 + select 50ms 超时轮询,避免忙等待,单线程即可处理数十个 socket
智能发包 send_udp() 方法根据远端的 IP 选择"最佳路由" socket:优先同网段、优先非回环

9.2.2 数据解析层

支持四种 X-Plane UDP 协议:

协议 方向 特征头 解析方式 用途
DATA X-Plane → 客户端 DATA 36 字节定长分组,<i8f 周期广播全量飞行参数
RREF 双向 RREF 请求:413B;响应:8B 定长 按需订阅特定 dataref
BEACON X-Plane → 广播 BEAC\0 44 字节定长 服务发现(识别 X-Plane 实例)
DREF/CMND 客户端 → X-Plane DREF/CMND — 写入 dataref / 发送命令

packet_type_from_payload() 通过前 4 字节 Magic Number 做 O(1) 协议识别:

mapping = {
    b"DATA": "DATA",
    b"RREF": "RREF",
    b"DREF": "DREF",
    b"CMND": "CMND",
    b"BEAC": "BEACON",
    b"ACFN": "ACFN",
}

9.2.3 RREF 订阅管理

RREF(Repeated Request)是 X-Plane 的拉模式数据订阅协议:

请求格式: RREF\0 + freq(4B) + req_id(4B) + dataref_path(400B)
响应格式: RREF\0 + req_id(4B) + value(4B float)

工具内置 28 组预定义 RREF 订阅分组,按机型组织:

分组类别 数量 覆盖范围
通用基础 3 组 姿态、空速、高度、航向、位置
Cessna 172SP 6 组 导航无线电、电气灯光、发动机、控制面、环境仪表
Boeing 737-800 8 组 MCP 自动驾驶、ILS、构型状态、双发参数、燃油系统、顶板、警告灯

并通过 飞机配置文件(AIRCRAFT_PROFILES) 一键切换仪表盘显示布局和 RREF 订阅集:

AIRCRAFT_PROFILES = {
    "Cessna Skyhawk(Floats)": {
        "preset_names": ["基础飞行组", "单发活塞组", "Cessna导航无线电组", ...],
        "dashboard_keys": ["ias", "alt", "hdg", "rpm1", "egt1", "ff1", ...],
    },
    "Boeing 737-800": {
        "preset_names": ["基础飞行组", "双发喷气组", "B737自动驾驶MCP细化组", ...],
        "dashboard_keys": ["ias", "alt", "n1_1", "n1_2", "n2_1", "vib1", ...],
    },
}

RREF 管理器自动维护订阅表,定时重发过期请求(auto_resend_rref_loop),保证 30 秒内无响应的订阅自动补发。

9.2.4 NIC 网卡嗅探 — NicSnifferManager

基于 Scapy 的二层抓包引擎,提供与 UDP 监听互补的旁路分析能力:

  • 自动检测 Npcap/WinPcap 运行时可用性
  • 动态 BPF 过滤器:根据监听端口和 X-Plane 端口自动构造 udp and (port 49000 or port 49020 or ...)
  • 多网卡并行抓包(每网卡独立线程)
  • 数据包方向智能分类:根据源端口/IP 判断是"来自 X-Plane"还是"发往 X-Plane"

9.2.5 共享内存转发 — InstrumentForwarder

Python → C++ 的零拷贝通信通道:

项目 值
共享内存名称 Local\AircraftPanelSharedMem
大小 4096 字节
数据布局 int32 data_ready + FlightParams(packed 二进制)
同步机制 data_ready 标志位(1 = 有新数据)
写入频率 数据更新即写,平均 ~20 次/秒

写入方法 shm_write_snapshot() 使用 struct.pack_into 按字段偏移精确写入:

偏移  字段              类型    说明
─────────────────────────────────────────────
0     data_ready        int32   先写0,写完数据后写1
4     theta             float64 俯仰角
12    phi               float64 滚转角
20    psi               float64 航向角
28    alt_m             float64 高度(米)
36    ias_kts           float64 空速(节)
44    vs_fpm            float64 升降率
52    oat_c             float64 外界气温
60    bus_volts         float64 总线电压
68    clock_seconds     float64 时钟秒数
76    fuel_left_gal     float64 左燃油量(加仑)
84    fuel_right_gal    float64 右燃油量(加仑)
...   ...               ...     ...
236   n1_left           float64 左发N1 (%)
244   n1_right          float64 右发N1 (%)
...   ...               ...     ...
396   annun_stall       int32   失速告警标志
...   ...               ...     ...

C++ 端通过 SharedMemReceiver 读取,FleetManager 分发给各仪表绘制函数。完整读写路径:

# Python 端写入
buf = bytearray(512)
struct.pack_into('<d', buf, 4,  pitch)    # theta
struct.pack_into('<d', buf, 12, roll)     # phi
struct.pack_into('<i', buf, 0, 1)         # data_ready = 1
self.shm.seek(0)
self.shm.write(bytes(buf))
// C++ 端读取
SharedMemLayout *pData = (SharedMemLayout*)MapViewOfFile(...);
if (InterlockedExchange(&pData->data_ready, 0) == 1) {
    FlightParams fp = pData->params;
    // 使用 fp.theta, fp.phi, fp.ias_kts ...
}

9.2.6 仪表盘 UI

Tkinter 实现的实时数据显示面板,包含 60+ 个 InstrumentCard 仪表卡片:

  • 深色 VSCode 主题:统一暗色配色方案,Theme 类定义完整色彩体系
  • 仪表卡片组件:显示标题、数值(大字)、单位、数据源标签(DATA/RREF 以不同颜色区分)
  • 数值闪烁动画:数值更新时短暂闪烁金色(150ms),提供视觉反馈
  • 悬停高亮:鼠标悬停时边框变为蓝色

9.3 局域网热点多设备 UDP 通信机制

这是 xplane_monitor 最核心的网络设计之一,解决"X-Plane 运行在一台设备上,仪表面板程序运行在另一台设备上"的跨机通信需求。

9.3.1 典型拓扑

┌─────────────────────────┐        Windows 移动热点        ┌──────────────────────────┐
│  宿主机 A (笔记本)       │     192.168.137.0/24           │  宿主机 B (台式机/平板)   │
│                         │◄─────────────────────────────►│                          │
│  ● xplane_monitor.py    │                                │  ● X-Plane 11            │
│  ● AircraftInstrumentPanel│  热点 IP: 192.168.137.1      │  ● (可选) 第二个显示器    │
│  ● 共享内存 (本地)       │                                │                          │
│  ● IP: 192.168.137.1    │                                │  ● IP: 192.168.137.x     │
└─────────────────────────┘                                └──────────────────────────┘

典型场景:

  • 宿主机 A 运行 xplane_monitor + AircraftInstrumentPanel.exe,作为仪表面板显示端
  • 宿主机 B 运行 X-Plane 11,作为飞行模拟数据源
  • A 通过 Windows 移动热点(192.168.137.1)与 B(192.168.137.x)建立局域网连接

9.3.2 多 IP 绑定策略

def get_bindable_ipv4s():
    """枚举本机所有可绑定的 IPv4 地址"""
    ips = set()
    for _, addrs in psutil.net_if_addrs().items():
        for addr in addrs:
            if addr.family == socket.AF_INET and addr.address != "0.0.0.0":
                ips.add(addr.address)
    ips.add("127.0.0.1")
    return sorted(ips)

当在界面中选择绑定 IP 为 0.0.0.0 且启用精确模式时,不会直接绑定通配符地址,而是:

  1. 调用 get_bindable_ipv4s() 获取本机所有网卡 IP
  2. 为每个 IP 的每个端口独立创建并绑定 socket
  3. 宿主机 A 上典型的绑定结果:
    • 127.0.0.1:49020 — 本机回环
    • 192.168.1.x:49020 — 主网卡(路由器 LAN)
    • 192.168.137.1:49020 — 热点网卡(与 X-Plane 通信)

这种设计的好处:

  • 精确知道数据从哪个网卡到达
  • 发送 RREF 请求时可选择"最佳路由"socket
  • 避免 0.0.0.0 在 Windows 多网卡环境下的路由歧义

9.3.3 智能发送路由选择

当 xplane_monitor 需要向 X-Plane 发送 RREF 请求时,send_udp() 方法执行多级 socket 匹配:

send_udp(remote_ip, remote_port, payload, local_ip=None, local_port=None)
    │
    ├─ 1. 精确匹配: 指定了 local_ip+local_port → 直连对应 socket
    │
    ├─ 2. 最佳 IP 匹配: 通过 guess_best_local_ip_for_remote()
    │      → 创建临时 UDP socket 连接远端
    │      → 查询 getsockname() 获得本机最优出口 IP
    │      → 匹配该 IP 对应的 socket
    │
    ├─ 3. 非回环优先: 若远端 IP 非 127.x,优先选非回环 socket
    │
    └─ 4. 兜底: 使用第一个可用 socket

guess_best_local_ip_for_remote() 通过操作系统路由表判断哪个本地 IP 到远端"代价最小":

def guess_best_local_ip_for_remote(remote_ip):
    s = socket.socket(AF_INET, SOCK_DGRAM)
    s.connect((remote_ip, 9))      # 连接远端但不发送数据
    local_ip = s.getsockname()[0]  # 内核自动选择最优源 IP
    s.close()
    return local_ip

例如,当远端为 192.168.137.5 时,操作系统路由表会判定最优源 IP 为 192.168.137.1(热点网卡),而非 192.168.1.x(主网卡)。

9.3.4 热点客户端自动发现

工具在启动时和用户点击"扫描热点设备"时,通过 arp 缓存扫描自动发现连接到 Windows 移动热点的设备:

def get_hotspot_clients():
    """运行 arp -a,扫描 192.168.137.x 网段"""
    output = subprocess.check_output(["arp", "-a"], timeout=5)
    for line in output.splitlines():
        m = re.match(r"\s*(\d+\.\d+\.\d+\.\d+)\s+([0-9a-f-]{17})\s+(\S+)", line)
        if m and m.group(1).startswith("192.168.137."):
            if m.group(1) != "192.168.137.1":  # 跳过网关自身
                clients.append({"ip": ip, "mac": mac, "type": type})

发现的设备 IP 自动填充到 X-Plane IP 下拉列表中,用户一键即可切换目标。

9.3.5 完整通信流程

宿主机 A (热点主机, 192.168.137.1)                   宿主机 B (X-Plane, 192.168.137.5)
                        │                                          │
                        │  [阶段1: 服务发现]                         │
                        │◄══════════════════ BEACON 广播══════════ │
                        │   (UDP 49707, 含版本/机场/机型信息)       │
                        │                                          │
                        │  [阶段2: 接收 DATA 广播]                   │
                        │◄══════════════════ DATA 包═══════════════ │
                        │   (UDP 49000, 36B×N 定长分组, 每帧广播)   │
                        │   解析出: IAS/ALT/姿态/位置等 8×N 字段     │
                        │                                          │
                        │  [阶段3: RREF 订阅]                        │
                        │  ───────────────── RREF 请求 ──────────► │
                        │   (UDP 49000, 413B, 含 dataref 路径)     │
                        │◄══════════════════ RREF 响应════════════ │
                        │   (UDP 49000, 8B/条, req_id+float value)  │
                        │   响应以指定频率持续推送直到连接断开        │
                        │                                          │
                        │  [阶段4: 数据融合与转发]                    │
                        │  live_values 字典:                         │
                        │    { "ias": 120.5, "alt": 1500, ... }     │
                        │      │                                    │
                        │      ▼                                    │
                        │  shm_write_snapshot(values)               │
                        │      │                                    │
                        │      ▼                                    │
                        │  共享内存 "Local\AircraftPanelSharedMem"   │
                        │      │                                    │
                        │      ▼                                    │
                        │  C++ FleetManager → FlightParams          │
                        │      → 各仪表 draw_xxx() → EGE 渲染       │
                        │                                          │
                        │  [可选: NIC 嗅探]                          │
                        │  Scapy 抓取网卡原始二层帧                  │
                        │  旁路分析所有 UDP 流量, 不干扰主数据流      │
                        │                                          │
                        │  [可选: CSV 录制]                          │
                        │  tools/Flightdata/*.csv                   │
                        │  后续可离线回放分析                        │

9.3.6 多设备扩展

当存在 3 台以上设备时(如 X-Plane 宿主机 + 仪表面板显示端 + 第三方遥测端):

  X-Plane (192.168.137.5)
       │
       │ UDP 49000/49020 (DATA + RREF)
       │
  ┌────┴────────────────────┐
  │  xplane_monitor          │  ← 运行在热点宿主机 (192.168.137.1)
  │  ─ 多 IP 绑定全部网卡     │
  │  ─ 集中数据融合 + 转发    │
  └────┬────────────────────┘
       │
       ├── 共享内存 → C++ Panel (本地进程)
       ├── (可扩展) TCP/HTTP → 远端遥测仪表
       └── CSV 文件 → 离线分析

FleetManager 的数据结构天然支持多架飞机(MAX_AIRCRAFT),共享内存写入器同样可扩展为多路输出。

9.4 界面布局

┌──────────────────────────────────────────────────────────────────┐
│ X-Plane 11 UDP Monitor · v3.1-ui-vscode                         │
├────┬─────────────────────────────────────────────────────────────┤
│ 📡 │  ┌──────────────────────────────────────────────────────┐  │
│ 监  │  │  监听控制:  [▲ 开始监听]  [■ 停止监听]              │  │
│ 听  │  │  绑定IP: [0.0.0.0 ▼]  端口: [49020]                │  │
│     │  │  X-Plane IP: [192.168.137.5 ▼]  端口: [49000]      │  │
│     │  │  [🔍 扫描热点设备] — 检测到 2 个客户端              │  │
│     │  └──────────────────────────────────────────────────────┘  │
│     │  ┌──────────────────────────────────────────────────────┐  │
│     │  │  Tab1: 实时数据  Tab2: 仪表盘  Tab3: NIC嗅探         │  │
│  活动  │  Tab4: 录制/回放  Tab5: 日志                         │  │
│  栏   │  ──────────────────────────────────────────────────  │  │
│     │  │  [实时数据]                                         │  │
│     │  │  时间        源IP          类型     字段     值      │  │
│     │  │  12:34:56  192.168.137.5  DATA     IAS    120.5 kt  │  │
│     │  │  12:34:56  192.168.137.5  RREF    theta   -0.02°   │  │
│     │  │  ...                                               │  │
│ 📊  │  └──────────────────────────────────────────────────────┘  │
│ 仪  │  ┌──────────────────────────────────────────────────────┐  │
│ 表  │  │  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐      │  │
│ 盘  │  │  │ IAS   │ │ ALT  │ │ VS   │ │ HDG  │ │ PITCH│      │  │
│     │  │  │ 120.5 │ │ 1500 │ │ -200 │ │ 183.4│ │ -0.02│      │  │
│     │  │  │  kt   │ │  ft  │ │ fpm  │ │ deg  │ │ deg  │      │  │
│     │  │  └──────┘ └──────┘ └──────┘ └──────┘ └──────┘      │  │
│ 🎬  │  └──────────────────────────────────────────────────────┘  │
│ 录  │  ┌──────────────────────────────────────────────────────┐  │
│ 制  │  │  录制: [● 开始录制] [■ 停止]  回放: [▶ 播放] [⏸]   │  │
│     │  │  文件: tools/Flightdata/flight_20260713.csv          │  │
│ 📋  │  └──────────────────────────────────────────────────────┘  │
│ 日  │  ┌──────────────────────────────────────────────────────┐  │
│ 志  │  │  [状态栏: 📡 已连接 🟢 共享内存 OK  📊 PPS: 45.2]   │  │
│     │  └──────────────────────────────────────────────────────┘  │
└────┴────────────────────────────────────────────────────────────┘

左侧 Activity Bar 四个功能区:

图标 名称 功能
📡 监听 (Monitor) UDP 监听启停、IP/端口配置、热点设备扫描、RREF 订阅管理
📊 仪表 (Instruments) 60+ 个仪表卡片的实时数据显示面板,支持三种飞机配置文件一键切换
🎬 录制 (Recording) CSV 录制/回放,录制数据存储在 tools/Flightdata/*.csv,回放速度 0.1x~5x
📋 日志 (Log) 带颜色标签的运行日志,支持关键字过滤

9.5 典型使用流程

单机模式(无 X-Plane,仅调试)

直接在运行 xplane_monitor 的机器上启动 AircraftInstrumentPanel.exe,xplane_monitor 即使未收到 X-Plane 数据也不会影响 C++ 面板运行——面板会自动切换到 SIM 仿真模式。

双机局域网模式(推荐)

  1. 宿主机 B(X-Plane 端):启动 X-Plane 11,设置 → 网络 → UDP 输出,将本机 IP 设为宿主机 A 的热点 IP(192.168.137.1),端口 49000
  2. 宿主机 A(显示端):开启 Windows 移动热点,运行 xplane_monitor.exe
  3. 在 xplane_monitor 中点击 「开始监听」(默认 0.0.0.0:49020)
  4. 点击 「扫描热点设备」 自动发现 X-Plane 宿主机 IP(192.168.137.x)
  5. 选择 飞机配置文件(Cessna 172SP 或 Boeing 737-800),一键订阅对应 RREF 数据集
  6. 数据通过共享内存自动转发给本机的 AircraftInstrumentPanel.exe
  7. 在仪表面板窗口中按 Space 切换到 UDP 模式,仪表开始实时反映 X-Plane 飞行状态

数据录制与回放

录制和回放均基于 CSV 格式:

# 录制文件保存在
tools/Flightdata/*.csv
# CSV 格式: 时间戳, 字段名, 值, 单位, 数据源
  • 录制:点击「开始录制」→ 选择保存路径 → 所有实时数据逐条追加到 CSV
  • 回放:加载 CSV 文件 → 点击「播放」→ 速度可调(0.1x ~ 5.0x)→ 支持暂停/继续/跳转

9.6 安装与构建

依赖安装

# 创建虚拟环境(可选但推荐)
python -m venv venv
venv\Scripts\activate

# 安装核心依赖
pip install psutil                    # 网络接口枚举(多IP绑定必需)
pip install scapy                     # NIC 网卡抓包(可选,需 Npcap 驱动)
pip install ujson                     # 更快 JSON 解析(可选)

# 如需打包为独立 exe
pip install pyinstaller               # 用于 build_exe.py 打包

⚠️ 关于 psutil:psutil 是多 IP 绑定功能的核心依赖。没有它,get_bindable_ipv4s() 将无法枚举本机网卡 IP,热点通信将受限。

关于 Npcap:NIC 嗅探功能需要安装 Npcap,安装时务必勾选 "Install Npcap in WinPcap API-compatible Mode"。如果不使用 NIC 嗅探,Scapy 和 Npcap 均非必需,UDP 监听和共享内存转发不受影响。

打包为独立 exe

cd tools
python build_exe.py              # 打包为单个 exe(onefile 模式)
# 或
python build_exe.py --onedir     # 目录模式(启动更快,便于调试)

打包产物:

  • tools/dist/onefile/xplane_monitor.exe(单文件模式,~30MB)
  • tools/dist/onedir/xplane_monitor.exe(目录模式,启动更快)

启动器菜单按 [3] 可自动定位并启动 xplane_monitor.exe。共享内存功能在 exe 模式下完全正常——mmap 调用的是 Windows 内核 API,与进程是 Python 脚本还是 exe 无关。


10. 开发者指南

10.1 添加一个新仪表(Cessna 172SP)

  1. 在 Cessna Skyhawk(Floats)/ 下创建模块目录:<Position>_<Name>/headers/ 和 sources/
  2. 实现 draw_xxx_panel_from_udp() 函数,遵循比例映射原则:
    void draw_xxx_panel_from_udp(int window_w, int window_h,
        int cx, int cy, double panel_radius, FlightParams *data);
    
  3. 在 image_panel.cpp 的 kInstrumentDefinitions 中注册
  4. 在 image_panel.cpp 顶部添加 #include 头文件引用
  5. 在 vcxproj 中添加新的 .cpp 和 .h 文件引用

10.2 比例映射核心原则

所有独立编写的仪表基于固定设计尺寸(通常半径 220px),集成到项目时不能直接缩放偏移,而应采用比例映射:

// ✅ 正确:一步到位,仅在画像素前四舍五入
int x = (int)(cx + dx / 220.0 * panel_radius + 0.5);
int r = (int)(v / 220.0 * panel_radius + 0.5);

// ❌ 错误:两步取整导致精度损失叠加
double scale = panel_radius / 220.0;
int x = cx + irnd(dx * scale);

10.3 EGE 渲染注意事项

  • settextcolor() 必须显式设置:outtextxy() 的文字颜色受 settextcolor() 控制,而非 setcolor()。忘记设置会导致颜色状态残留(如紫色文字问题)
  • 绘制顺序:背景 → 刻度/数字 → 指针 → 弓形覆盖(塑料边框效果)。弓形覆盖必须在指针之前绘制,否则会遮盖前景
  • 线宽恢复:setlinewidth() 修改线宽后记得恢复为 1.0,避免污染后续仪表
  • 离屏缓冲:始终在离屏 PIMAGE 上完成全部绘制再 putimage,不要直接画到窗口

10.4 代码规范

  • 命名:变量/函数使用英文,注释使用中文(关键逻辑用中文注释,API 调用用英文)
  • Winsock2 顺序:任何包含 winsock2.h 的文件必须在 EGE/Windows 头之前包含,并定义 WIN32_LEAN_AND_MEAN
  • UTF-8:项目使用 /utf-8 编译选项,所有源文件保存为 UTF-8 编码

11. 开发问题记录

本项目在开发过程中遇到并解决了大量实际问题,按类别记录如下,供后续开发者参考。

11.1 EGE 渲染问题

问题 1:文字发紫(紫色/洋红色文字)

项目 内容
现象 数字和部分文字呈现紫色/洋红色,但背景和图形颜色正常
根因 outtextxy() 的文字颜色由 settextcolor() 控制,而非 setcolor()。前序模块设置了紫色文字后未恢复,后续模块未显式调用 settextcolor() 导致颜色状态残留
影响 Left_2_1 Left_2_2 Left_3_2 等多个仪表
修复 所有 outtextxy() 调用前必须三件套:setcolor() + setlinecolor() + settextcolor(),缺一不可
教训 EGE 是全局状态机,每个绘制函数必须自包含全部颜色状态设置,不能依赖前序函数残留的状态

问题 2:画面残缺(斜切/菱形残缺区域)

项目 内容
现象 画面出现大块斜切、菱形残缺区域,部分刻度/指针被遮挡
根因 ege_fillpoly() 绘制的弓形覆盖层(塑料边框效果)在指针和文字之后执行,大面积遮盖前景
影响 多个含弓形覆盖的仪表
修复 将弓形覆盖绘制移到背景之后、刻度之前,作为背景装饰而非前景元素
教训 绘制顺序至关重要:背景 → 弓形覆盖 → 刻度/数字 → 指针 → 标签。大面积填充多边形必须在前景元素之前绘制

问题 3:线宽未恢复导致污染后续仪表

项目 内容
现象 切换仪表后,线条粗细异常(如本该 1px 的刻度线变成 4px)
根因 setlinewidth(4.0f) 修改线宽后未在函数结尾恢复为 1.0f,EGE 全局状态泄漏到下一个仪表
影响 所有修改过线宽的仪表
修复 每个绘制函数出口处执行 setlinewidth(1.0f) 恢复默认
教训 EGE 全局状态(颜色、线宽、字体、背景模式)必须在函数结束时复原,原则是"谁修改谁恢复"

问题 4:fillpoly 与 ege_fillpoly 混用

项目 内容
现象 弓形或指针在离屏缓冲 PIMAGE 上不渲染,但在主窗口正常
根因 旧版 fillpoly(无 ege_ 前缀)在 PIMAGE 上可能调用 GDI legacy 路径,与 EGE 的 GDI+ 渲染不兼容
影响 多个仪表的弓形和指针
修复 统一使用 ege_fillpoly(),兼容主窗口和离屏缓冲
教训 在离屏缓冲上绘图时,始终使用 ege_ 前缀的 API

问题 5:多边形顶点数组越界

项目 内容
现象 极端角度下程序崩溃或绘制异常
根因 弓形分段数 steps = (a1 - a0) * 2 无上限,当角度差很大时 steps 超过数组 ep[1024] 容量
影响 fq_draw_segment() va_draw_segment() ef_draw_segment() 等函数
修复 添加 steps 上限 720,数组大小改为 ep[800] 匹配上限
教训 动态计算数组索引必须有安全上限校验

11.2 架构设计问题

问题 6:仪表控件超出槽位裁切半径

项目 内容
现象 VOR1/ILS 的 OBS 旋钮、空速表的设定旋钮等位于表盘背景圆之外的控件在运行时不可见
根因 默认渲染模式使用 composite_circle_to_target 在槽位半径处做圆形裁切,超出部分被裁掉。控件设计偏移 (-190, 190) 距圆心约 269px,超出设计半径 250
影响 Right_1_1 Mid_1_1 等含外部控件的仪表
修复 在 InstrumentDefinition 中设 preserve_background = true,跳过圆形裁切,改用 putimage 整层回贴。或设置 clip_radius_override 扩大裁切半径
教训 仪表面板不是所有元素都在圆形槽位内,超出控件需要特殊渲染模式

问题 7:渲染状态泄漏(状态机污染)

项目 内容
现象 仪表 A 的字体/颜色/背景模式影响仪表 B 的渲染效果
根因 EGE 是全局状态机,各仪表绘制函数共享同一套全局状态。一个函数修改了状态但未恢复,影响后续调用的函数
影响 所有仪表
修复 每个绘制函数自包含全部状态设置(颜色、字体、背景模式、线宽、对齐方式),不依赖任何外部状态
教训 防御性编程:每个 public 绘制函数开头设置自己需要的状态,结尾恢复默认值

问题 8:FlightParams 全局变量默认零初始化

项目 内容
现象 新添加的仪表指针永远停在零位不动,看起来像"死掉了"
根因 C/C++ 全局变量默认零初始化。FlightParams 新增字段(如 fuel_left_gal)默认值为 0,未在 main.cpp 中赋初始值
影响 新增仪表的仿真模式
修复 在 main.cpp 的初始化部分显式设置新字段的默认值
教训 全局结构体的新字段必须显式初始化,零值对飞行参数来说通常是非法/极端值

11.3 编译与构建问题

问题 9:winsock.h / winsock2.h 冲突

项目 内容
现象 编译报宏重定义、类型重定义错误,集中在 fd_set timeval 等 Winsock 类型
根因 EGE 的 graphics.h 内部包含 windows.h,而 windows.h 默认拉入 winsock.h。winsock2.h 与 winsock.h 不兼容,两者不能同时包含
影响 所有包含 winsock2.h 的源文件
修复 固定包含顺序:① #define WIN32_LEAN_AND_MEAN → ② #include <winsock2.h> → ③ #include <graphics.h>。winsock2.h 在内部定义 _WINSOCKAPI_ 阻止 windows.h 再拉入 winsock.h
教训 在 EGE 项目中使用 Winsock2,包含顺序是硬性约束,不可颠倒

问题 10:graphics.h 找不到

项目 内容
现象 编译报错 fatal error C1083: Cannot open include file: 'graphics.h': No such file or directory
根因 ege/include 目录未添加到 VC++ 附加包含目录
修复 项目已自动配置 ege/include,直接打开 .sln 编译即可。如需手动配置:项目属性 → VC++ 目录 → 包含目录 → 添加 ege/include
教训 确保 .vcxproj 中的 AdditionalIncludeDirectories 包含正确的 EGE 头文件路径

问题 11:LNK2019 链接错误找不到 EGE 函数

项目 内容
现象 链接报 LNK2019: unresolved external symbol _initgraph 等 EGE 函数未解析
根因 EGE 库文件路径不对,或链接器找不到对应库。Debug/Release 配置使用了不同的库文件(graphicsd.lib vs graphics.lib)
修复 项目已自动根据配置选择库文件。vcxproj 中条件判断 $(VisualStudioVersion) 自动映射到对应的 EGE 预编译库目录
教训 不同 VS 版本使用不同 EGE 库目录,确保 vcxproj 中的库路径条件映射正确

问题 12:EGE 库的 Visual Studio 版本兼容

项目 内容
现象 VS2022 编译正常,VS2026 Preview 链接报错
根因 EGE 为不同 VS 版本提供了独立的预编译库,但 VS2026 的库路径在 vcxproj 中需要额外配置
修复 vcxproj 中使用 $(VisualStudioVersion) 判断:'17.0' → vs2022,否则 → vs2026,未来可扩展更多版本
教训 EGE 预编译库与 VS 工具集版本绑定,升级 VS 时需同步更新库路径

11.4 运行与数据问题

问题 13:工作目录找不到背景图片

项目 内容
现象 程序启动后窗口白屏或闪退
根因 运行时的当前工作目录(CWD)不包含 demo3.png(Cessna)或 Boeing737-800.png(Boeing),getimage() 加载失败
修复 启动器自动搜索 exe 同级目录、项目子目录等多个候选路径。建议在项目根目录启动 exe,或在 VS 中设置工作目录为 $(ProjectDir)..\
教训 资源文件加载路径应设计为相对路径 + 多候选目录搜索,而非硬编码路径

问题 14:X-Plane UDP 数据收不到

项目 内容
现象 xplane_monitor 启动后无任何数据包显示
根因 ① X-Plane 未开启 UDP 输出 ② 端口不匹配(X-Plane 默认 49000,xplane_monitor 默认监听 49020) ③ Windows 防火墙拦截 ④ 双机不在同一网段
修复 ① X-Plane 设置 → 网络 → 勾选 UDP 输出 ② 确保监听端口覆盖 X-Plane 输出端口 ③ 检查防火墙规则 ④ 确保热点/LAN 连通
教训 网络调试时先用 xplane_monitor 的 NIC 嗅探功能查看是否有原始 UDP 包到达,确认网络连通性后再排查上层解析

问题 15:共享内存连接失败

项目 内容
现象 C++ 面板读取到的 data_ready 始终为 0,仪表不更新
根因 ① 未先启动 xplane_monitor(负责 CreateFileMapping) ② 共享内存名称不匹配 ③ 进程权限不足
修复 确保先启动 xplane_monitor,共享内存名称两端统一为 Local\AircraftPanelSharedMem
教训 共享内存创建端(Python)必须比读取端(C++)先运行。exe 打包不影响 mmap 功能(调用的是 Windows 内核 API)

问题 16:xplane_monitor 启动报 "Scapy 不可用"

项目 内容
现象 启动时弹出 "Npcap/Scapy 抓包不可用" 警告
根因 Scapy 未安装或 Npcap 驱动未安装。这是可选依赖,仅 NIC 嗅探功能需要
修复 v3.1.0 已移除启动时的 Npcap 自动检测逻辑,不再弹出警告。如需 NIC 抓包,在 NIC 嗅探标签页手动启用,安装 Npcap 并勾选 WinPcap 兼容模式
教训 区分核心依赖和可选依赖,可选依赖缺失不应阻断主功能。启动阶段的依赖检查应限定于核心功能

问题 17:高帧率下 UDP 丢包

项目 内容
现象 X-Plane 以 20+ 帧/秒广播 DATA 包时,xplane_monitor 偶有数据跳变或缺失
根因 默认 UDP 接收缓冲区太小,select 轮询 + Python GIL 导致处理不及时
修复 设置 SO_RCVBUF = 1MB,非阻塞模式 + 循环读取直到 EAGAIN,确保每次 select 返回后清空所有待读数据包
教训 UDP 监听必须使用大缓冲区 + 非阻塞循环读取模式,不能假设每次 select 只收到一个包

11.5 EGE 特定问题

问题 18:高 DPI 屏幕下 EGE 窗口模糊

项目 内容
现象 在 150%+ 缩放的高分屏上,EGE 窗口内容模糊
根因 EGE 基于 GDI+,默认不感知 DPI,Windows 会对非 DPI 感知应用做拉伸缩放
修复 panel_runtime.cpp 中调用 SetProcessDPIAware() 启用 DPI 感知,按实际物理像素创建窗口
教训 Windows 桌面应用必须处理 DPI 感知,否则在高分屏上渲染质量严重下降

问题 19:INIT_RENDERMANUAL 模式使用

项目 内容
现象 窗口尺寸变化时画面撕裂或闪烁
根因 EGE 默认自动渲染模式在窗口消息循环中可能插入额外渲染,破坏手动双缓冲逻辑
修复 setinitmode(INIT_RENDERMANUAL) 关闭自动渲染,完全由主循环控制渲染时机
教训 使用离屏双缓冲时,必须关闭 EGE 的自动渲染,否则两个渲染路径互相干扰

问题 20:PIMAGE 离屏缓冲内存泄漏

项目 内容
现象 长时间运行后内存持续增长
根因 newimage() 创建的 PIMAGE 未在面板切换或退出时 delimage() 释放
修复 在 shutdown 回调中遍历释放所有 PIMAGE,并在 render 中复用已有缓冲而非每次新建
教训 EGE 的 PIMAGE 需要手动管理生命周期,建议在 init 中创建、shutdown 中释放

11.6 X-Plane Monitor 特定问题

问题 21:RREF 订阅超时未响应

项目 内容
现象 订阅的 dataref 长时间无返回值
根因 X-Plane RREF 订阅有超时机制,网络中断或 X-Plane 重启后订阅不自动恢复
修复 auto_resend_rref_loop 定时检查,30 秒内无响应的订阅自动重发
教训 拉模式数据订阅必须设计超时重发机制,不能假设一次订阅永久有效

问题 22:多网卡环境下 UDP 包路由歧义

项目 内容
现象 笔记本电脑同时连接 Wi-Fi(192.168.1.x)和热点(192.168.137.1)时,RREF 请求从错误的网卡发出,X-Plane 收不到
根因 0.0.0.0 绑定在 Windows 多网卡环境下路由不确定,系统可能选择与 X-Plane 不在同一网段的源 IP
修复 不绑 0.0.0.0,逐一绑定所有网卡 IP。send_udp() 通过 guess_best_local_ip_for_remote() 查询系统路由表选择最优源 IP
教训 多网卡环境下 UDP 通信必须精确控制源 IP,不能依赖系统自动选择

问题 23:Windows 热点客户端列表获取

项目 内容
现象 get_hotspot_clients() 返回空列表
根因 ① 热点未开启 ② arp -a 命令输出格式因系统语言而异(中文"动态/静态" vs 英文"dynamic/static") ③ 权限不足
修复 正则匹配兼容中英文输出;添加超时处理;捕获 FileNotFoundError(arp 命令不存在)
教训 调用外部命令时需考虑跨语言输出格式、超时、权限、异常等多个维度

问题 24:PyInstaller 打包后共享内存路径问题

项目 内容
现象 打包为 exe 后 Flightdata 目录出现在临时解压路径而非 exe 同级目录
根因 PyInstaller 打包后 __file__ 指向临时 _MEIPASS 解压路径,os.path.dirname(__file__) 得到的是临时目录
修复 使用 sys.executable 定位 exe 所在目录,而非 __file__
教训 PyInstaller 打包的 exe 中,__file__ 不可用于定位数据文件,始终使用 sys.executable 或 sys.argv[0]

11.7 数据对齐与精度问题

问题 25:共享内存 FlightParams 布局不对齐

项目 内容
现象 C++ 端读取的某些字段值异常(如 theta 读到 phi 的值)
根因 Python struct.pack_into 使用默认对齐(自然对齐),而 C++ FlightParams 使用 #pragma pack(push, 1) 紧凑对齐,两者不一致
修复 Python 端不对齐假设,严格按字段顺序和大小逐字节 pack;C++ 端保持 pack(1) 紧凑布局
教训 跨语言二进制通信必须在两端显式约定对齐方式,或使用逐字段序列化而非一次性结构体拷贝

问题 26:浮点数单位换算精度

项目 内容
现象 燃油量、燃油流量等换算后的数值与 X-Plane 实际值有偏差
根因 X-Plane 的 dataref 单位与 FlightParams 要求单位不同(如 fuel_quantity 为 kg,FlightParams 为 gal),换算系数(如 AvGas 2.72 kg/gal)为近似值
修复 在 shm_write_snapshot() 中做单位换算,调试输出验证;B737 使用 Jet-A 3.04 kg/gal 系数
教训 单位换算是常见的错误来源,每个换算系数都应注明来源,并通过实测验证

问题 27:X-Plane 数据平滑与滤波

项目 内容
现象 姿态、空速等数据显示抖动剧烈,指针来回跳动
根因 X-Plane 原始数据含噪声,直接使用会导致指针抖动
修复 xplane_monitor 和 C++ 端均实现指数平滑(α=0.3):smoothed = α × raw + (1-α) × smoothed
教训 飞行模拟器的原始遥测数据需要滤波后才能用于仪表显示,平滑系数需在响应速度和稳定性之间平衡

11.8 渲染引擎问题

问题 28:EGE 矩阵变换下字体与线条渲染模糊

项目 内容
现象 在 EGE 矩阵变换(ege_translate / ege_rotate / ege_scale)作用下,outtextxy() 输出的文字和 line() 绘制的线条出现模糊/锯齿
根因 旧版 EGE 绘图 API 在矩阵变换上下文中使用 GDI legacy 路径渲染,不支持变换矩阵,导致像素对齐异常
修复 统一替换为 ege_ 前缀的新版绘图接口(ege_line()、ege_rectangle() 等),旧版 API 在矩阵变换下调用 GDI 路径而非 GDI+ 路径
教训 使用 EGE 矩阵变换功能时,必须全程使用 ege_ 前缀的 API 以确保走 GDI+ 渲染管线,新旧 API 混用会导致不可预测的视觉异常

问题 29:面板背景填充白色导致视觉突兀

项目 内容
现象 面板窗口背景为白色,与深色驾驶舱仪表盘风格不协调,窗口缩放时白色边缘暴露
根因 cleardevice() / setbkcolor() 使用白色背景填充,未根据面板主题色设置
修复 背景填充改为黑色(EGERGB(0, 0, 0)),并在 render() 函数开头以黑色填充整个窗口,确保缩放时边缘始终为黑色
教训 驾驶舱仪表盘模拟应默认使用深色背景,白色背景在高对比度显示环境下会造成视觉疲劳,且不符合真实驾驶舱的视觉风格

问题 30:面板切换时 EGE 窗口重复初始化

项目 内容
现象 按 1/2 切换面板时,EGE 窗口销毁并重新创建,导致短暂黑屏、窗口位置重置、资源泄漏
根因 panel_main() 每次调用都执行 initgraph() + closegraph(),EGE 窗口的生命周期与面板生命周期绑定而非与程序生命周期绑定
修复 引入 g_ege_inited 静态标志,仅在首次调用时 initgraph(),后续切换面板仅执行 shutdown() + init() 回调,复用窗口
教训 图形窗口的生命周期应独立于面板/场景的生命周期。窗口创建一次,面板切换仅替换渲染内容和事件处理逻辑

12. 项目推进记录

12.1 参与人员

角色 提交次数 主要贡献领域
成员 1 82 项目架构、核心渲染、UDP通信、xplane_monitor、配置管理
成员 2 15 B737 仪表布局、位置微调、视觉优化
成员 3 8 PFD/ND 显示问题修复、刻度闪烁修复
成员 4 6 机械仪表表盘颜色调整、渲染修复、文件清理
成员 5 1 B737 FMC_CDU 旋钮标签移除

12.2 版本演进时间线

2026-04-29 ~ 2026-05-02:项目初始化

58cc51e 测试上传(首次仓库初始化)
9e81f6c 第一次上传
3cb02c6 提交

项目仓库建立,初始文件结构搭建。此阶段主要为 VS 项目配置和基础骨架代码。

2026-05-08 ~ 2026-05-20:Cessna 172SP 六块主仪表

7111b25 ~ 2ded47f  成员学习 EGE 绘图,提交测试代码
6820d44             第一次合表结果
8ec0b3d             第一次合表结果
fd5503c             测试11
7259812             创建其余九个仪表的文件夹

在此阶段:

  • 核心仪表(空速表、姿态仪、高度表、转弯协调仪、航向指示器、升降速度表)逐个独立绘制完成
  • 左右辅助仪表(时钟/气温/电压、燃油量、EGT/燃油流量、油温/油压、真空/电流、VOR1/ILS CDI、VOR2 CDI、转速表/霍布斯、ADF)陆续添加
  • 通过 image_panel.cpp 的 kInstrumentDefinitions 统一注册管理
  • 5月20日完成首次合表——15 块仪表在面板背景图上正确渲染

2026-05-20 ~ 2026-05-21:UDP 通信模块

60d51f5  创建 udp_module/ 目录结构
13aa816  新增 Python 程序检测 X-Plane UDP 数据
  • 创建 udp_module/,实现 Winsock2 UDP 数据接收
  • 第一版 xplane_monitor.py 诞生,用于监听 X-Plane 11 的 UDP 广播
  • 确立 Python(数据采集)+ C++(渲染)的跨语言协作架构

2026-05-26 ~ 2026-06-01:渲染优化与 Bug 修复

e6c4752  修复仪表文字颜色状态污染和弓形覆盖绘制顺序错误
05f79a1  修复紫色文字/残缺问题,详细记录至 purple_issue_fix.md
9187dfa  调整机械表盘颜色亮度
d0c26cc  添加仪表黑色边框绘制

关键修复:

  • 紫色文字问题(根因:settextcolor() 未显式设置,颜色状态残留)
  • 画面残缺问题(根因:弓形覆盖绘制顺序错误,大面积遮盖前景)
  • 离屏缓冲颜色键透明改用直接像素拷贝,消除边缘混色
  • 添加高 DPI 缩放支持

2026-06-01 ~ 2026-06-16:数据架构重构

0417bd4  集成 RREF 订阅管理器,支持 DATA/RREF 双模式
c9f09f8  重构为 Cessna172SP 专用版本,数据源改用共享内存
84aa4af  添加 xplane_monitor 的 PyInstaller 构建脚本
  • 引入 FleetManager 和 SharedMemReceiver,数据源从直接 UDP 改为共享内存
  • xplane_monitor 从简单的 UDP 监听升级为完整的数据中转中枢
  • 支持 PyInstaller 打包为独立 exe

2026-06-16 ~ 2026-06-23:Boeing 737-800 玻璃座舱

5e15120  添加 Boeing737-800 骨架项目
71993f8  整合 Cessna172SP 和 Boeing737-800 项目,添加启动选择器
062fe78  集成七块仪表模块至面板管理器
e7f76d5  实现像素级八边形裁剪与面板透明合成
9613fa8  ND/PFD 背景绘制
0b189de  初始 B737 三个仪表布局

多成员并行开发:

  • 成员 1:架构集成、共享内存管道、SIM/UDP 模式切换、UPPER_DU/LOWER_DU/FMC_CDU
  • 成员 2:PFD/ND/ISFD 位置标定、SVG 标定图调整、大小微调
  • 成员 3:PFD 显示问题修复、刻度闪烁修复、ND 显示修正

里程碑:9f2d75e(6月22日)——B737 八模块全部集成至统一面板管理器。

2026-06-24 ~ 2026-07-02:共享内存与文档

21975a2  补全 UDP 模式下时钟刻度盘
9b3cb5f  修正燃油流量与油量单位换算
6f47a7e  引入仪表面板共享运行时,支持运行时面板切换
96cfd75  添加提交信息约定文档
f761738  添加 .gitignore 规则
  • panel_runtime 共享运行时抽象——按 1/2 键运行时切换 C172/B737
  • 统一 PanelInterface 接口,两个面板通过 g_panels[] 数组注册
  • COMMIT_CONVENTION.md 确立 Conventional Commits 规范

2026-07-09 ~ 2026-07-10:音频告警系统

d7e8d78  新增音频告警引擎及告警规则!
  • 完整的音频告警系统上线:alert_rules(双机型规则引擎)+ alert_sound(PlaySound 异步播放)
  • 37 个 .wav 音效文件
  • 集成 GPWS 高度离散步进呼叫逻辑

2026-07-10:录制回放与热点发现

7aabc63  新增录制与回放功能
b544bf1  修正燃油流量/油量单位转换
18654ea  添加热点设备检测功能
  • CSV 录制/回放系统上线
  • 热点客户端自动检测(arp -a 扫描 192.168.137.x)
  • 单位换算修正(燃油 kg → gal,流量 kg/sec → gal/hour)

2026-07-11 ~ 2026-07-12:PFD 重构与视觉优化

eaf8aa8  重写空速表与高度表
132f6d1  修正速度表与高度表功能颠倒
9fb4635  添加 README 自述文件
7c455d1  移除姿态仪底部俯仰数字
  • ISFD 空速/高度表重写,加入动态平滑与离屏渲染
  • PFD 速度带/高度带功能修正

2026-07-13:DPI 感知与启动页

ca175c9  自绘启动页,卡片布局替换按钮
ed6b08c  添加高 DPI 感知与动态 DPI 缩放支持
  • 启动器重绘为卡片式自绘 UI
  • 全窗口高 DPI 感知,动态缩放适配不同分辨率

2026-07-13 ~ 2026-07-16:渲染修复与窗口生命周期重构

aad92bc  重构窗口生命周期,支持多次调用panel_main并正确释放资源
2c2dff3  修改Right_3_2 ADF部分绘制逻辑,使更贴近真实表盘
e4a02e5  将面板背景从白色修正为黑色并调整缩放策略以填充窗口
5a31c59  将仪表预览窗口初始位置从主窗口级联改为屏幕居中
26e1799  修正真空安培表指针角度以正确指示仪表值
d311943  修正空速指针角度并支持显示0节
796ed81  修正姿态指示仪俯仰方向
0179a91  修正矩阵变换下字体与线条的视觉渲染问题
2561159  修正文本在矩阵变换下的模糊问题并统一使用新版EGE绘图接口

关键修复:

  • 窗口生命周期重构:panel_main() 支持多次调用,新增 panel_cleanup() 函数,首次调用初始化 EGE 窗口,后续调用复用已有窗口,消除反复创建销毁的开销
  • 面板背景黑色化:将面板背景填充从白色改为黑色,调整缩放策略以完整填充窗口,改善视觉沉浸感
  • 预览窗口居中:仪表预览窗口初始位置从主窗口级联改为屏幕居中,避免在屏幕角落打开
  • 指针角度修正:空速表、真空安培表、姿态仪等多处指针角度修正,确保指示值与实际数据一致
  • 矩阵变换渲染修复:修正 EGE 矩阵变换下字体模糊和线条视觉渲染问题,统一使用新版 EGE 绘图接口,提升跨版本兼容性
  • ADF 表盘优化:Right_3_2 ADF 方位指示器绘制逻辑改进,使表盘更贴近真实飞机仪表外观

12.3 提交统计

统计项 数值
总提交数 112
参与人数 5
最早提交 2026-04-29
最新提交 2026-07-16
项目周期 78 天

按 Conventional Commits 类型分布(仅统计遵循规范的提交):

类型 数量 占比
feat 23 34.3%
fix 20 29.9%
build 8 11.9%
refactor 6 9.0%
docs 5 7.5%
chore 3 4.5%
revert 3 4.5%

12.4 关键里程碑

日期 里程碑 提交
2026-04-29 仓库初始化 58cc51e
2026-05-20 Cessna 172SP 15 块仪表首次合表 8ec0b3d
2026-05-20 首个 xplane_monitor Python 监听器 13aa816
2026-06-01 共享内存数据管道 + RREF 订阅 0417bd4 c9f09f8
2026-06-16 xplane_monitor 可打包为独立 exe 84aa4af
2026-06-22 Boeing 737-800 八模块全部集成 9f2d75e 062fe78
2026-07-02 共享运行时支持面板切换 6f47a7e
2026-07-09 音频告警系统上线 d7e8d78
2026-07-10 录制回放 + 热点设备检测 7aabc63 18654ea
2026-07-11 首个完整 README 9fb4635
2026-07-13 自绘启动页 + 高 DPI 感知 ca175c9 ed6b08c
2026-07-14 面板背景黑色化 + 预览窗口居中 e4a02e5 5a31c59
2026-07-15 矩阵变换渲染修复 + 指针角度修正 0179a91 d311943 26e1799 796ed81
2026-07-16 窗口生命周期重构 + 新版 EGE 接口 aad92bc 2561159

许可证

本项目为南昌航空大学(Nchu)教学/研究项目。

posted @ 2026-07-11 18:53  胡昇  阅读(98)  评论(0)    收藏  举报