Nchu Aircraft Instrument Panel
Nchu Aircraft Instrument Panel
南昌航空大学飞机仪表面板模拟器
基于 EGE 图形库(Easy Graphics Engine)的 Windows 桌面应用,用于模拟飞机驾驶舱仪表盘。支持两种机型,通过 Windows 共享内存接收 X-Plane 11 飞行模拟器的实时数据驱动仪表显示,无外部数据时自动运行内置 SIM 仿真模式。
目录
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 已知问题(紫色/残缺修复)
多个仪表曾出现文字发紫、画面残缺的问题,根因有二:
- 文字颜色状态残留:
settextcolor()未显式设置,沿用了前序模块的紫色状态 → 修复:所有outtextxy()前加settextcolor() - 弓形覆盖绘制顺序错误:
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(推荐)
- 双击打开
AircraftInstrumentPanel.sln - 工具栏选择
x64平台 +Debug/Release配置 Ctrl+Shift+B编译- 输出:
build/x64/Debug/AircraftInstrumentPanel.exebuild/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 且启用精确模式时,不会直接绑定通配符地址,而是:
- 调用
get_bindable_ipv4s()获取本机所有网卡 IP - 为每个 IP 的每个端口独立创建并绑定 socket
- 宿主机 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 仿真模式。
双机局域网模式(推荐)
- 宿主机 B(X-Plane 端):启动 X-Plane 11,设置 → 网络 → UDP 输出,将本机 IP 设为宿主机 A 的热点 IP(
192.168.137.1),端口49000 - 宿主机 A(显示端):开启 Windows 移动热点,运行
xplane_monitor.exe - 在 xplane_monitor 中点击 「开始监听」(默认
0.0.0.0:49020) - 点击 「扫描热点设备」 自动发现 X-Plane 宿主机 IP(
192.168.137.x) - 选择 飞机配置文件(Cessna 172SP 或 Boeing 737-800),一键订阅对应 RREF 数据集
- 数据通过共享内存自动转发给本机的
AircraftInstrumentPanel.exe - 在仪表面板窗口中按
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)
- 在
Cessna Skyhawk(Floats)/下创建模块目录:<Position>_<Name>/headers/和sources/ - 实现
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); - 在
image_panel.cpp的kInstrumentDefinitions中注册 - 在
image_panel.cpp顶部添加#include头文件引用 - 在
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)教学/研究项目。
浙公网安备 33010602011771号