simconnect-H 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simconnect-H
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 原生 ctypes SimConnect 库 — 零外部依赖,直接加载 SimConnect.dll 与 MSFS 通讯
|
|
5
|
+
Author-email: HuJie <150501351+hjznb887@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hjznb887/simconnect-H
|
|
8
|
+
Project-URL: Repository, https://github.com/hjznb887/simconnect-H
|
|
9
|
+
Keywords: simconnect,msfs,flight-simulator,ctypes,native
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Games/Entertainment :: Simulation
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# simconnect-H
|
|
21
|
+
|
|
22
|
+
**原生 ctypes SimConnect 库 — 零外部依赖,直接加载 SimConnect.dll 与 Microsoft Flight Simulator 通讯。**
|
|
23
|
+
|
|
24
|
+
## 特性
|
|
25
|
+
|
|
26
|
+
- 🚫 **零 Python 依赖** — 仅使用 Python 标准库 `ctypes`,无任何第三方包
|
|
27
|
+
- 🔓 **无 AGPL 污染** — 不依赖 PySimConnect,MIT 许可证
|
|
28
|
+
- 🎯 **完全控制** — 手动定义所有 `argtypes`,无 Enum 类型漏洞
|
|
29
|
+
- 🪶 **轻量** — 单个文件,即插即用
|
|
30
|
+
- 🏗️ **可打包** — 支持 `pip install` 和 PyInstaller 打包
|
|
31
|
+
- 🔒 **线程安全** — dispatch 回调注册使用锁保护,适合多线程场景
|
|
32
|
+
- 📦 **上下文管理器** — 支持 `with sc:` 语法,自动断开连接
|
|
33
|
+
- 🔍 **智能 DLL 查找** — 自动扫描 MSFS SDK 安装目录、site-packages、系统 PATH
|
|
34
|
+
|
|
35
|
+
## 安装
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 从源码安装
|
|
39
|
+
pip install .
|
|
40
|
+
|
|
41
|
+
# 或直接复制 simconnect_native/ 到项目目录
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
需要 `SimConnect.dll`(微软模拟飞行 SDK 可再发行组件),程序会自动在以下位置查找:
|
|
45
|
+
|
|
46
|
+
1. MSFS SDK 安装目录(`Program Files (x86)/Microsoft SDKs/FlightSimulator/`)
|
|
47
|
+
2. 本文件同目录
|
|
48
|
+
3. 当前工作目录
|
|
49
|
+
4. `site-packages/SimConnect/`(PySimConnect 安装路径)
|
|
50
|
+
5. 系统 PATH
|
|
51
|
+
|
|
52
|
+
也可以手动指定路径:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
sc = SimConnect()
|
|
56
|
+
sc.load_dll(r"C:\Path\To\SimConnect.dll")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 快速入门
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from simconnect_native import (
|
|
63
|
+
SimConnect,
|
|
64
|
+
SIMCONNECT_SIMOBJECT_TYPE_USER,
|
|
65
|
+
SIMCONNECT_RECV_ID_SIMOBJECT_DATA_BYTYPE,
|
|
66
|
+
SIMCONNECT_RECV_ID_OPEN,
|
|
67
|
+
SIMCONNECT_RECV_ID_EXCEPTION,
|
|
68
|
+
FULL_SIMOBJECT_DATA, EXCEPTION_MSG, EXCEPTION_NAMES,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# 支持 with 语句,自动 close
|
|
72
|
+
with SimConnect() as sc:
|
|
73
|
+
sc.load_dll()
|
|
74
|
+
sc.open(b"MyApp")
|
|
75
|
+
|
|
76
|
+
# 注册数据定义
|
|
77
|
+
sc.add_to_data_definition(1, b"PLANE ALTITUDE", b"Feet")
|
|
78
|
+
|
|
79
|
+
# 设置 dispatch 回调
|
|
80
|
+
def on_dispatch(pData, cbData, pContext):
|
|
81
|
+
try:
|
|
82
|
+
dwID = pData.contents.dwID
|
|
83
|
+
except Exception:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
if dwID == SIMCONNECT_RECV_ID_OPEN:
|
|
87
|
+
print("✓ 已连接到 MSFS")
|
|
88
|
+
|
|
89
|
+
elif dwID == SIMCONNECT_RECV_ID_EXCEPTION:
|
|
90
|
+
exc = ctypes.cast(pData, ctypes.POINTER(EXCEPTION_MSG)).contents
|
|
91
|
+
name = EXCEPTION_NAMES.get(exc.dwException, f"UNKNOWN({exc.dwException})")
|
|
92
|
+
print(f"⚠ 异常: {name}")
|
|
93
|
+
|
|
94
|
+
elif dwID == SIMCONNECT_RECV_ID_SIMOBJECT_DATA_BYTYPE:
|
|
95
|
+
req_id, val = sc.read_double(pData)
|
|
96
|
+
print(f"📊 req={req_id} value={val}")
|
|
97
|
+
|
|
98
|
+
sc.set_dispatch_cb(on_dispatch)
|
|
99
|
+
|
|
100
|
+
# 请求数据
|
|
101
|
+
sc.request_data_on_simobject_type(1, 1, 0, SIMCONNECT_SIMOBJECT_TYPE_USER)
|
|
102
|
+
|
|
103
|
+
# 轮询
|
|
104
|
+
import time
|
|
105
|
+
for _ in range(100):
|
|
106
|
+
sc.dispatch()
|
|
107
|
+
time.sleep(0.01)
|
|
108
|
+
|
|
109
|
+
# 写入数据
|
|
110
|
+
from ctypes import c_double, cast, c_void_p
|
|
111
|
+
arr = (c_double * 1)(1000.0)
|
|
112
|
+
ptr = cast(arr, c_void_p)
|
|
113
|
+
sc.set_data_on_simobject(1, data_ptr=ptr)
|
|
114
|
+
|
|
115
|
+
# 发送事件
|
|
116
|
+
sc.map_client_event_to_sim_event(100, b"KEY_TOGGLE")
|
|
117
|
+
sc.transmit_client_event(0, 100, 0)
|
|
118
|
+
|
|
119
|
+
# with 块结束自动断开
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> **v0.2.0 迁移说明**:`FULL_SIMOBJECT_DATA` 已精简为数据头部结构体(`SIMOBJECT_DATA_HEADER`),不再包含预分配的 `dwData` 数组。
|
|
123
|
+
> 所有数据读取请改用 `SimConnect.read_data(pData, datatype)` 或 `sc.read_double(pData)`,内部使用指针偏移零拷贝读取。
|
|
124
|
+
> 向后兼容:`FULL_SIMOBJECT_DATA` 名称仍可作为 `SIMOBJECT_DATA_HEADER` 的别名导入。
|
|
125
|
+
|
|
126
|
+
## API 一览
|
|
127
|
+
|
|
128
|
+
### `SimConnect` 类
|
|
129
|
+
|
|
130
|
+
| 方法 | 说明 |
|
|
131
|
+
| ------ | ------ |
|
|
132
|
+
| `load_dll(path=None)` | 加载 SimConnect.dll |
|
|
133
|
+
| `open(app_name, ...)` | 连接 MSFS |
|
|
134
|
+
| `close()` | 断开连接 |
|
|
135
|
+
| `add_to_data_definition(id, name, unit, ...)` | 注册 SimVar |
|
|
136
|
+
| `clear_data_definition(id)` | 清除定义 |
|
|
137
|
+
| `request_data_on_simobject_type(req_id, def_id, ...)` | 请求数据 |
|
|
138
|
+
| `request_data_on_simobject(req_id, def_id, ...)` | 请求持续数据更新 |
|
|
139
|
+
| `add_and_request(req_id, def_id, name, unit, ...)` | 注册+请求一步完成 |
|
|
140
|
+
| `set_data_on_simobject(def_id, *, object_id, flags, ...)` | 写入数据 |
|
|
141
|
+
| `write_double(def_id, value)` | 快捷写入 double 值 |
|
|
142
|
+
| `map_client_event_to_sim_event(ev_id, name)` | 映射事件 |
|
|
143
|
+
| `transmit_client_event(obj_id, ev_id, data, ...)` | 发送事件 |
|
|
144
|
+
| `subscribe_to_system_event(id, name)` | 订阅系统事件 |
|
|
145
|
+
| `dispatch()` | 处理一次消息队列 |
|
|
146
|
+
| `set_dispatch_cb(callback)` | 设置 dispatch 回调 |
|
|
147
|
+
| `call_dispatch(callback)` | 设置并触发 dispatch |
|
|
148
|
+
| `read_double(pData)` | 从回调中解析 float64 值 |
|
|
149
|
+
| `read_data(pData, datatype=0)` | 从回调指针按类型读取数据(静态方法,零拷贝) |
|
|
150
|
+
| `start_background_dispatch(callback=None)` | 启动后台 dispatch 线程 |
|
|
151
|
+
| `stop_background_dispatch()` | 停止后台 dispatch 线程 |
|
|
152
|
+
| `get_last_sent_packet_id()` | 获取最后发送的数据包 ID |
|
|
153
|
+
| `event_data_float(value)` | float → DWORD 位转换(静态方法) |
|
|
154
|
+
|
|
155
|
+
### 属性
|
|
156
|
+
|
|
157
|
+
| 属性 | 说明 |
|
|
158
|
+
| ------ | ------ |
|
|
159
|
+
| `handle` | SimConnect 句柄(HANDLE) |
|
|
160
|
+
| `dll` | 已加载的 WinDLL 对象 |
|
|
161
|
+
| `is_open` | 是否已连接 |
|
|
162
|
+
|
|
163
|
+
### 模块级工具
|
|
164
|
+
|
|
165
|
+
| 函数 | 说明 |
|
|
166
|
+
| ------ | ------ |
|
|
167
|
+
| `find_simconnect_dll()` | 自动搜索 SimConnect.dll 路径 |
|
|
168
|
+
| `read_data_value(pData, datatype=0)` | 从 dispatch 回调中读取指定类型数据 |
|
|
169
|
+
| `__version__` | 当前库版本 `"0.1.0"` |
|
|
170
|
+
|
|
171
|
+
## 许可证
|
|
172
|
+
|
|
173
|
+
MIT License
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
simconnect_h-0.1.0.dist-info/licenses/LICENSE,sha256=4urYmKqf1cRrSVrBSdC7AFM_642n-N67kqvcofKccoE,1083
|
|
2
|
+
simconnect_native/__init__.py,sha256=AJpeEORtym5hYH2DE5eejFcgqoylNodhy3y-KQA3Wjc,32647
|
|
3
|
+
simconnect_h-0.1.0.dist-info/METADATA,sha256=hlEljFeOZLDhR2e4dGi8mRi76j8WGdhzITUT7ZM1pBU,6350
|
|
4
|
+
simconnect_h-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
simconnect_h-0.1.0.dist-info/top_level.txt,sha256=7OUe7MZvBQtexAbGLdwtfxCZ464-9kp2cB107IoQrXo,18
|
|
6
|
+
simconnect_h-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HuJie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simconnect_native
|
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
# simconnect_native.py
|
|
2
|
+
"""原生 ctypes SimConnect 库 — 零外部依赖,可直接加载 SimConnect.dll 与 MSFS 通讯。
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
from simconnect_native import SimConnect, SIMCONNECT_SIMOBJECT_TYPE_USER
|
|
6
|
+
|
|
7
|
+
sc = SimConnect()
|
|
8
|
+
sc.open("MyApp")
|
|
9
|
+
# 注册数据定义
|
|
10
|
+
sc.add_to_data_definition(1, b"PLANE ALTITUDE", b"Feet", 0) # 0=FLOAT64
|
|
11
|
+
# 请求数据
|
|
12
|
+
sc.request_data_on_simobject_type(1, 1, 0, SIMCONNECT_SIMOBJECT_TYPE_USER)
|
|
13
|
+
# dispatch 回调
|
|
14
|
+
def on_dispatch(pData, cbData, pContext):
|
|
15
|
+
dwID = pData.contents.dwID
|
|
16
|
+
...
|
|
17
|
+
# 启动后台线程持续接收(推荐),不再需要手动循环 dispatch
|
|
18
|
+
sc.start_background_dispatch(on_dispatch)
|
|
19
|
+
# 写入(object_id=0=SIMCONNECT_OBJECT_ID_USER)
|
|
20
|
+
sc.set_data_on_simobject(2, object_id=0, data_ptr=data_ptr)
|
|
21
|
+
# 事件
|
|
22
|
+
sc.map_client_event_to_sim_event(100, b"KEY_TOGGLE")
|
|
23
|
+
sc.transmit_client_event(0, 100, 0, 0x19000000, 16)
|
|
24
|
+
sc.close()
|
|
25
|
+
|
|
26
|
+
不依赖 PySimConnect(AGPL)的任何代码。
|
|
27
|
+
"""
|
|
28
|
+
import ctypes
|
|
29
|
+
import os
|
|
30
|
+
import time
|
|
31
|
+
import logging
|
|
32
|
+
import threading
|
|
33
|
+
from ctypes import (c_ulong, c_float, c_char_p, c_double, c_void_p, c_int32, c_int16, c_int8,
|
|
34
|
+
cast, POINTER, sizeof as c_sizeof, Structure, WinDLL, byref)
|
|
35
|
+
from ctypes.wintypes import HANDLE, DWORD, HRESULT
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# 常量
|
|
39
|
+
"SIMCONNECT_UNUSED", "SIMCONNECT_OBJECT_ID_USER",
|
|
40
|
+
"SIMCONNECT_DATATYPE_FLOAT64", "SIMCONNECT_DATATYPE_FLOAT32",
|
|
41
|
+
"SIMCONNECT_DATATYPE_INT32", "SIMCONNECT_DATATYPE_INT16",
|
|
42
|
+
"SIMCONNECT_DATATYPE_INT8", "SIMCONNECT_DATATYPE_STRINGV",
|
|
43
|
+
"SIMCONNECT_SIMOBJECT_TYPE_USER", "SIMCONNECT_SIMOBJECT_TYPE_ALL",
|
|
44
|
+
"SIMCONNECT_SIMOBJECT_TYPE_AIRCRAFT",
|
|
45
|
+
"SIMCONNECT_PERIOD_NEVER", "SIMCONNECT_PERIOD_ONCE",
|
|
46
|
+
"SIMCONNECT_PERIOD_VISUAL_FRAME", "SIMCONNECT_PERIOD_SIM_FRAME",
|
|
47
|
+
"SIMCONNECT_PERIOD_SECOND",
|
|
48
|
+
"SIMCONNECT_RECV_ID_NULL", "SIMCONNECT_RECV_ID_EXCEPTION",
|
|
49
|
+
"SIMCONNECT_RECV_ID_OPEN", "SIMCONNECT_RECV_ID_QUIT",
|
|
50
|
+
"SIMCONNECT_RECV_ID_EVENT", "SIMCONNECT_RECV_ID_SIMOBJECT_DATA",
|
|
51
|
+
"SIMCONNECT_RECV_ID_SIMOBJECT_DATA_BYTYPE",
|
|
52
|
+
"EXCEPTION_NAMES",
|
|
53
|
+
# 结构体
|
|
54
|
+
"SIMCONNECT_RECV", "SIMOBJECT_DATA_HEADER", "SIMOBJECT_DATA_HEADER_SIZE",
|
|
55
|
+
"FULL_SIMOBJECT_DATA", "EXCEPTION_MSG",
|
|
56
|
+
# 事件
|
|
57
|
+
"MSFS_EVENTS",
|
|
58
|
+
# 函数
|
|
59
|
+
"find_simconnect_dll",
|
|
60
|
+
# 类
|
|
61
|
+
"SimConnect",
|
|
62
|
+
# 新增工具
|
|
63
|
+
"read_data_value",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
# ═══════════════════════════════════════════════════
|
|
69
|
+
# 版本
|
|
70
|
+
# ═══════════════════════════════════════════════════
|
|
71
|
+
|
|
72
|
+
__version__ = "0.1.0"
|
|
73
|
+
|
|
74
|
+
# ═══════════════════════════════════════════════════
|
|
75
|
+
# 常量
|
|
76
|
+
# ═══════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
SIMCONNECT_UNUSED = DWORD(0xFFFFFFFF)
|
|
79
|
+
SIMCONNECT_OBJECT_ID_USER = DWORD(0)
|
|
80
|
+
|
|
81
|
+
# SIMCONNECT_DATATYPE
|
|
82
|
+
SIMCONNECT_DATATYPE_FLOAT64 = c_ulong(0)
|
|
83
|
+
SIMCONNECT_DATATYPE_FLOAT32 = c_ulong(1)
|
|
84
|
+
SIMCONNECT_DATATYPE_INT32 = c_ulong(2)
|
|
85
|
+
SIMCONNECT_DATATYPE_INT16 = c_ulong(3)
|
|
86
|
+
SIMCONNECT_DATATYPE_INT8 = c_ulong(4)
|
|
87
|
+
SIMCONNECT_DATATYPE_STRINGV = c_ulong(5)
|
|
88
|
+
|
|
89
|
+
# SIMCONNECT_SIMOBJECT_TYPE
|
|
90
|
+
SIMCONNECT_SIMOBJECT_TYPE_USER = c_ulong(0)
|
|
91
|
+
SIMCONNECT_SIMOBJECT_TYPE_ALL = c_ulong(1)
|
|
92
|
+
SIMCONNECT_SIMOBJECT_TYPE_AIRCRAFT = c_ulong(2)
|
|
93
|
+
|
|
94
|
+
# SIMCONNECT_PERIOD
|
|
95
|
+
SIMCONNECT_PERIOD_NEVER = c_ulong(0)
|
|
96
|
+
SIMCONNECT_PERIOD_ONCE = c_ulong(1)
|
|
97
|
+
SIMCONNECT_PERIOD_VISUAL_FRAME = c_ulong(2)
|
|
98
|
+
SIMCONNECT_PERIOD_SIM_FRAME = c_ulong(3)
|
|
99
|
+
SIMCONNECT_PERIOD_SECOND = c_ulong(4)
|
|
100
|
+
|
|
101
|
+
# SIMCONNECT_RECV_ID
|
|
102
|
+
SIMCONNECT_RECV_ID_NULL = 0
|
|
103
|
+
SIMCONNECT_RECV_ID_EXCEPTION = 1
|
|
104
|
+
SIMCONNECT_RECV_ID_OPEN = 2
|
|
105
|
+
SIMCONNECT_RECV_ID_QUIT = 3
|
|
106
|
+
SIMCONNECT_RECV_ID_EVENT = 4
|
|
107
|
+
SIMCONNECT_RECV_ID_EVENT_OBJECT_ADDREMOVE = 5
|
|
108
|
+
SIMCONNECT_RECV_ID_EVENT_FILENAME = 6
|
|
109
|
+
SIMCONNECT_RECV_ID_EVENT_FRAME = 7
|
|
110
|
+
SIMCONNECT_RECV_ID_SIMOBJECT_DATA = 14
|
|
111
|
+
SIMCONNECT_RECV_ID_SIMOBJECT_DATA_BYTYPE = 15
|
|
112
|
+
SIMCONNECT_RECV_ID_WEATHER_OBSERVATION = 16
|
|
113
|
+
SIMCONNECT_RECV_ID_CLIENT_DATA = 19
|
|
114
|
+
SIMCONNECT_RECV_ID_EVENT_WEATHER_MODE = 20
|
|
115
|
+
SIMCONNECT_RECV_ID_EVENT_MULTIPLAYER_SERVER_STARTED = 27
|
|
116
|
+
SIMCONNECT_RECV_ID_EVENT_MULTIPLAYER_CLIENT_STARTED = 28
|
|
117
|
+
SIMCONNECT_RECV_ID_EVENT_MULTIPLAYER_SESSION_ENDED = 29
|
|
118
|
+
SIMCONNECT_RECV_ID_EVENT_RACE_END = 30
|
|
119
|
+
SIMCONNECT_RECV_ID_EVENT_RACE_LAP = 31
|
|
120
|
+
SIMCONNECT_RECV_ID_SYSTEM_STATE = 33
|
|
121
|
+
|
|
122
|
+
# SIMCONNECT_EXCEPTION
|
|
123
|
+
EXCEPTION_NAMES = {
|
|
124
|
+
0: "NONE", 1: "ERROR", 2: "SIZE_MISMATCH", 3: "UNRECOGNIZED_ID",
|
|
125
|
+
4: "UNOPENED", 5: "VERSION_MISMATCH", 6: "TOO_MANY_GROUPS",
|
|
126
|
+
7: "NAME_UNRECOGNIZED", 8: "TOO_MANY_EVENT_NAMES",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# ═══════════════════════════════════════════════════
|
|
130
|
+
# 消息结构体
|
|
131
|
+
# ═══════════════════════════════════════════════════
|
|
132
|
+
|
|
133
|
+
class SIMCONNECT_RECV(Structure):
|
|
134
|
+
"""SimConnect 消息头部(所有消息的前 16 字节)"""
|
|
135
|
+
_fields_ = [
|
|
136
|
+
("dwID", DWORD),
|
|
137
|
+
("dwSize", DWORD),
|
|
138
|
+
("dwVersion", DWORD),
|
|
139
|
+
("dwSeqNumber", DWORD),
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class SIMOBJECT_DATA_HEADER(Structure):
|
|
144
|
+
"""数据消息头部(元数据部分,不含数据负载),用于 SIMCONNECT_RECV_ID_SIMOBJECT_DATA / _BYTYPE"""
|
|
145
|
+
_fields_ = [
|
|
146
|
+
("dwID", DWORD), ("dwSize", DWORD), ("dwVersion", DWORD), ("dwSeqNumber", DWORD),
|
|
147
|
+
("dwRequestID", DWORD), ("dwObjectID", DWORD), ("dwDefineID", DWORD),
|
|
148
|
+
("dwFlags", DWORD), ("dwentrynumber", DWORD), ("dwoutof", DWORD),
|
|
149
|
+
("dwDefineCount", DWORD),
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
SIMOBJECT_DATA_HEADER_SIZE = c_sizeof(SIMOBJECT_DATA_HEADER)
|
|
154
|
+
|
|
155
|
+
# 向后兼容别名(不含 dwData,所有数据读取改用指针偏移)
|
|
156
|
+
FULL_SIMOBJECT_DATA = SIMOBJECT_DATA_HEADER
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class EXCEPTION_MSG(Structure):
|
|
160
|
+
"""异常消息(含头部)"""
|
|
161
|
+
_fields_ = [
|
|
162
|
+
("dwID", DWORD), ("dwSize", DWORD), ("dwVersion", DWORD), ("dwSeqNumber", DWORD),
|
|
163
|
+
("dwException", DWORD), ("UNKNOWN_SENDID", DWORD),
|
|
164
|
+
("UNKNOWN_INDEX", DWORD), ("dwSendID", DWORD), ("dwIndex", DWORD),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ═══════════════════════════════════════════════════
|
|
169
|
+
# 事件名称查询表
|
|
170
|
+
# ═══════════════════════════════════════════════════
|
|
171
|
+
|
|
172
|
+
MSFS_EVENTS = {
|
|
173
|
+
# 引擎
|
|
174
|
+
"THROTTLE_FULL", "THROTTLE_INCR", "THROTTLE_DECR",
|
|
175
|
+
"THROTTLE_CUT", "MIXTURE_INCR", "MIXTURE_DECR",
|
|
176
|
+
"PROP_PITCH_INCR", "PROP_PITCH_DECR",
|
|
177
|
+
# 飞行控制
|
|
178
|
+
"AP_MASTER", "AP_PANEL_ALTITUDE_HOLD", "AP_PANEL_HEADING_HOLD",
|
|
179
|
+
"AP_PANEL_SPEED_HOLD", "AP_PANEL_ATTITUDE_HOLD",
|
|
180
|
+
# 灯光
|
|
181
|
+
"LANDING_LIGHTS_TOGGLE", "STROBES_TOGGLE",
|
|
182
|
+
"BEACONS_TOGGLE", "NAV_LIGHTS_TOGGLE", "TAXI_LIGHTS_TOGGLE",
|
|
183
|
+
"PANEL_LIGHTS_TOGGLE",
|
|
184
|
+
# 系统
|
|
185
|
+
"GEAR_TOGGLE", "PARKING_BRAKES",
|
|
186
|
+
"SIM_RESET", "SITUATION_RESET", "REPAIR_AND_REFUEL",
|
|
187
|
+
"TOGGLE_ENGINE", "TOGGLE_MASTER_IGNITION",
|
|
188
|
+
"TOGGLE_ALTERNATOR", "TOGGLE_AVIONICS_MASTER",
|
|
189
|
+
# 视图
|
|
190
|
+
"VIEW_RESET", "EYEPOINT_RESET", "PAN_RESET",
|
|
191
|
+
# 襟翼
|
|
192
|
+
"FLAPS_INCR", "FLAPS_DECR", "FLAPS_UP", "FLAPS_DOWN",
|
|
193
|
+
# 配平
|
|
194
|
+
"ELEV_TRIM_UP", "ELEV_TRIM_DN",
|
|
195
|
+
"AILERON_TRIM_LEFT", "AILERON_TRIM_RIGHT",
|
|
196
|
+
"RUDDER_TRIM_LEFT", "RUDDER_TRIM_RIGHT",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ═══════════════════════════════════════════════════
|
|
201
|
+
# DLL 查找
|
|
202
|
+
# ═══════════════════════════════════════════════════
|
|
203
|
+
|
|
204
|
+
def find_simconnect_dll():
|
|
205
|
+
"""查找 SimConnect.dll 位置。
|
|
206
|
+
|
|
207
|
+
搜索顺序:
|
|
208
|
+
1. 本文件同目录
|
|
209
|
+
2. 当前工作目录
|
|
210
|
+
3. site-packages/SimConnect/(PySimConnect 安装路径)
|
|
211
|
+
4. 系统 PATH(默认返回 "SimConnect.dll" 让 Windows 自动搜索)
|
|
212
|
+
"""
|
|
213
|
+
search_dirs = [
|
|
214
|
+
os.path.dirname(__file__),
|
|
215
|
+
os.getcwd(),
|
|
216
|
+
]
|
|
217
|
+
# Program Files (MSFS SDK 安装路径)
|
|
218
|
+
pf = os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")
|
|
219
|
+
sdk_root = os.path.join(pf, "Microsoft SDKs", "FlightSimulator")
|
|
220
|
+
if os.path.isdir(sdk_root):
|
|
221
|
+
for entry in os.listdir(sdk_root):
|
|
222
|
+
candidate = os.path.join(sdk_root, entry, "SimConnect.dll")
|
|
223
|
+
if os.path.exists(candidate):
|
|
224
|
+
search_dirs.insert(0, os.path.dirname(candidate))
|
|
225
|
+
for base in search_dirs:
|
|
226
|
+
p = os.path.join(base, "SimConnect.dll")
|
|
227
|
+
if os.path.exists(p):
|
|
228
|
+
return p
|
|
229
|
+
try:
|
|
230
|
+
import site
|
|
231
|
+
for d in site.getsitepackages():
|
|
232
|
+
p = os.path.join(d, "SimConnect", "SimConnect.dll")
|
|
233
|
+
if os.path.exists(p):
|
|
234
|
+
return p
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
return "SimConnect.dll"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ═══════════════════════════════════════════════════
|
|
241
|
+
# 模块级便利函数
|
|
242
|
+
# ═══════════════════════════════════════════════════
|
|
243
|
+
|
|
244
|
+
def read_data_value(pData, datatype=0):
|
|
245
|
+
"""从 dispatch 回调的 pData 中读取指定类型的数据。
|
|
246
|
+
|
|
247
|
+
用法:
|
|
248
|
+
from simconnect_native import read_data_value
|
|
249
|
+
val = read_data_value(pData, SIMCONNECT_DATATYPE_FLOAT64)
|
|
250
|
+
"""
|
|
251
|
+
return SimConnect.read_data(pData, datatype)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ═══════════════════════════════════════════════════
|
|
255
|
+
# 高层封装
|
|
256
|
+
# ═══════════════════════════════════════════════════
|
|
257
|
+
|
|
258
|
+
class SimConnect:
|
|
259
|
+
"""SimConnect 原生封装 — 直接通过 ctypes WinDLL 调用 SimConnect.dll。
|
|
260
|
+
|
|
261
|
+
特性:
|
|
262
|
+
- 零 Python 依赖(仅 ctypes 标准库)
|
|
263
|
+
- 完全控制 argtypes,无 Enum 类型污染
|
|
264
|
+
- 线程安全的 dispatch 回调注册
|
|
265
|
+
- 自动 DLL 查找
|
|
266
|
+
|
|
267
|
+
用法:
|
|
268
|
+
sc = SimConnect()
|
|
269
|
+
sc.open("MyApp")
|
|
270
|
+
# ... 使用各种方法 ...
|
|
271
|
+
sc.close()
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
def __init__(self):
|
|
275
|
+
self._dll = None
|
|
276
|
+
self._hSimConnect = None
|
|
277
|
+
self._dispatch_cb = None
|
|
278
|
+
self._DispatchProc = None
|
|
279
|
+
# 后台 dispatch 线程
|
|
280
|
+
self._dispatch_thread = None
|
|
281
|
+
self._dispatch_running = False
|
|
282
|
+
self._dispatch_stop_event = threading.Event()
|
|
283
|
+
self._lock = threading.Lock()
|
|
284
|
+
# 断开回调(由 dispatch 在收到 SIMCONNECT_RECV_ID_QUIT 时调用)
|
|
285
|
+
self.on_disconnect = None
|
|
286
|
+
|
|
287
|
+
# ── 属性 ──────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def handle(self):
|
|
291
|
+
"""SimConnect 句柄(HANDLE),未连接时为 None"""
|
|
292
|
+
return self._hSimConnect
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def dll(self):
|
|
296
|
+
"""已加载的 WinDLL 对象,未加载时为 None"""
|
|
297
|
+
return self._dll
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def is_open(self):
|
|
301
|
+
"""是否已成功打开连接"""
|
|
302
|
+
return (self._hSimConnect is not None
|
|
303
|
+
and self._hSimConnect.value is not None
|
|
304
|
+
and self._hSimConnect.value != 0)
|
|
305
|
+
|
|
306
|
+
def __repr__(self):
|
|
307
|
+
status = "已连接" if self.is_open else "未连接"
|
|
308
|
+
dll_status = "已加载" if self._dll else "未加载"
|
|
309
|
+
return f"<SimConnect {status}, DLL {dll_status}>"
|
|
310
|
+
|
|
311
|
+
def __enter__(self):
|
|
312
|
+
"""支持 with 语句 — 返回自身"""
|
|
313
|
+
return self
|
|
314
|
+
|
|
315
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
316
|
+
"""退出 with 块时自动关闭连接"""
|
|
317
|
+
self.close()
|
|
318
|
+
|
|
319
|
+
# ── 初始化 ────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
def load_dll(self, dll_path=None):
|
|
322
|
+
"""加载 SimConnect.dll。
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
dll_path: DLL 路径,为 None 时自动查找。
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
FileNotFoundError: DLL 文件不存在。
|
|
329
|
+
OSError: DLL 加载失败(如架构不匹配、依赖缺失)。
|
|
330
|
+
"""
|
|
331
|
+
path = dll_path or find_simconnect_dll()
|
|
332
|
+
logger.info("加载 SimConnect.dll: %s", path)
|
|
333
|
+
|
|
334
|
+
if not os.path.isfile(path):
|
|
335
|
+
raise FileNotFoundError(
|
|
336
|
+
f"SimConnect.dll 未找到: {path}\n"
|
|
337
|
+
"请确保已安装 MSFS 或从 SDK 获取 SimConnect.dll"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
self._dll = WinDLL(path)
|
|
342
|
+
except OSError as e:
|
|
343
|
+
raise OSError(
|
|
344
|
+
f"加载 SimConnect.dll 失败: {e}\n"
|
|
345
|
+
"请检查:\n"
|
|
346
|
+
" 1. DLL 是 64 位版本(Python 也是 64 位)\n"
|
|
347
|
+
" 2. 已安装 Microsoft Visual C++ Redistributable\n"
|
|
348
|
+
" 3. DLL 未被其他进程独占锁定"
|
|
349
|
+
) from e
|
|
350
|
+
|
|
351
|
+
logger.debug("SimConnect.dll 加载成功")
|
|
352
|
+
self._setup_argtypes()
|
|
353
|
+
|
|
354
|
+
def _setup_argtypes(self):
|
|
355
|
+
"""配置所有 SimConnect API 函数的 argtypes"""
|
|
356
|
+
d = self._dll
|
|
357
|
+
|
|
358
|
+
# SimConnect_Open
|
|
359
|
+
d.SimConnect_Open.restype = HRESULT
|
|
360
|
+
d.SimConnect_Open.argtypes = [POINTER(HANDLE), c_char_p, c_void_p, DWORD, HANDLE, DWORD]
|
|
361
|
+
|
|
362
|
+
# SimConnect_Close
|
|
363
|
+
d.SimConnect_Close.restype = HRESULT
|
|
364
|
+
d.SimConnect_Close.argtypes = [HANDLE]
|
|
365
|
+
|
|
366
|
+
# SimConnect_CallDispatch
|
|
367
|
+
self._DispatchProc = ctypes.WINFUNCTYPE(None, c_void_p, DWORD, c_void_p)
|
|
368
|
+
d.SimConnect_CallDispatch.restype = HRESULT
|
|
369
|
+
d.SimConnect_CallDispatch.argtypes = [HANDLE, self._DispatchProc, c_void_p]
|
|
370
|
+
|
|
371
|
+
# SimConnect_AddToDataDefinition
|
|
372
|
+
d.SimConnect_AddToDataDefinition.restype = HRESULT
|
|
373
|
+
d.SimConnect_AddToDataDefinition.argtypes = [
|
|
374
|
+
HANDLE, DWORD, c_char_p, c_char_p, c_ulong, c_float, DWORD,
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
# SimConnect_RequestDataOnSimObjectType
|
|
378
|
+
d.SimConnect_RequestDataOnSimObjectType.restype = HRESULT
|
|
379
|
+
d.SimConnect_RequestDataOnSimObjectType.argtypes = [
|
|
380
|
+
HANDLE, DWORD, DWORD, DWORD, c_ulong,
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
# SimConnect_RequestDataOnSimObject
|
|
384
|
+
d.SimConnect_RequestDataOnSimObject.restype = HRESULT
|
|
385
|
+
d.SimConnect_RequestDataOnSimObject.argtypes = [
|
|
386
|
+
HANDLE, DWORD, DWORD, DWORD, c_ulong, DWORD, DWORD, DWORD, DWORD,
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
# SimConnect_SetDataOnSimObject
|
|
390
|
+
d.SimConnect_SetDataOnSimObject.restype = HRESULT
|
|
391
|
+
d.SimConnect_SetDataOnSimObject.argtypes = [
|
|
392
|
+
HANDLE, DWORD, c_ulong, DWORD, DWORD, DWORD, c_void_p,
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
# SimConnect_MapClientEventToSimEvent
|
|
396
|
+
d.SimConnect_MapClientEventToSimEvent.restype = HRESULT
|
|
397
|
+
d.SimConnect_MapClientEventToSimEvent.argtypes = [HANDLE, DWORD, c_char_p]
|
|
398
|
+
|
|
399
|
+
# SimConnect_TransmitClientEvent
|
|
400
|
+
d.SimConnect_TransmitClientEvent.restype = HRESULT
|
|
401
|
+
d.SimConnect_TransmitClientEvent.argtypes = [HANDLE, c_ulong, DWORD, DWORD, DWORD, DWORD]
|
|
402
|
+
|
|
403
|
+
# SimConnect_SubscribeToSystemEvent
|
|
404
|
+
d.SimConnect_SubscribeToSystemEvent.restype = HRESULT
|
|
405
|
+
d.SimConnect_SubscribeToSystemEvent.argtypes = [HANDLE, DWORD, c_char_p]
|
|
406
|
+
|
|
407
|
+
# SimConnect_GetLastSentPacketID
|
|
408
|
+
d.SimConnect_GetLastSentPacketID.restype = HRESULT
|
|
409
|
+
d.SimConnect_GetLastSentPacketID.argtypes = [HANDLE, POINTER(DWORD)]
|
|
410
|
+
|
|
411
|
+
# SimConnect_ClearDataDefinition
|
|
412
|
+
d.SimConnect_ClearDataDefinition.restype = HRESULT
|
|
413
|
+
d.SimConnect_ClearDataDefinition.argtypes = [HANDLE, DWORD]
|
|
414
|
+
|
|
415
|
+
# ── 连接管理 ──────────────────────────────────
|
|
416
|
+
|
|
417
|
+
def open(self, app_name=b"SimConnectApp", window_handle=None, fifo_size=0,
|
|
418
|
+
window_event_handle=None, config_index=0):
|
|
419
|
+
"""建立与 MSFS 的 SimConnect 连接。
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
app_name: 应用名称(bytes)。
|
|
423
|
+
window_handle: 窗口句柄,默认为 None。
|
|
424
|
+
fifo_size: FIFO 大小,默认为 0。
|
|
425
|
+
window_event_handle: 窗口事件句柄,默认为 None。
|
|
426
|
+
config_index: 配置索引,默认为 0。
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
HANDLE: SimConnect 句柄。
|
|
430
|
+
|
|
431
|
+
Raises:
|
|
432
|
+
ConnectionError: 连接失败或返回空句柄。
|
|
433
|
+
RuntimeError: DLL 未加载,请先调用 load_dll()。
|
|
434
|
+
"""
|
|
435
|
+
if not self._dll:
|
|
436
|
+
raise RuntimeError("DLL 未加载,请先调用 load_dll()")
|
|
437
|
+
|
|
438
|
+
hSim = HANDLE(0)
|
|
439
|
+
err = self._dll.SimConnect_Open(
|
|
440
|
+
ctypes.byref(hSim), app_name, window_handle, fifo_size,
|
|
441
|
+
window_event_handle, config_index,
|
|
442
|
+
)
|
|
443
|
+
if err != 0:
|
|
444
|
+
raise ConnectionError(
|
|
445
|
+
f"SimConnect_Open 失败: HRESULT=0x{err:08x}"
|
|
446
|
+
)
|
|
447
|
+
if not hSim or hSim.value is None or hSim.value == 0:
|
|
448
|
+
raise ConnectionError(
|
|
449
|
+
"SimConnect_Open 返回空句柄 — MSFS 可能未运行"
|
|
450
|
+
)
|
|
451
|
+
self._hSimConnect = hSim
|
|
452
|
+
logger.info("SimConnect 已连接 (app=%s)", app_name)
|
|
453
|
+
return hSim
|
|
454
|
+
|
|
455
|
+
def close(self):
|
|
456
|
+
"""关闭 SimConnect 连接。"""
|
|
457
|
+
self.stop_background_dispatch()
|
|
458
|
+
if self._dll and self._hSimConnect:
|
|
459
|
+
try:
|
|
460
|
+
self._dll.SimConnect_Close(self._hSimConnect)
|
|
461
|
+
logger.info("SimConnect 已断开")
|
|
462
|
+
except Exception as e:
|
|
463
|
+
logger.debug("SimConnect_Close 异常: %s", e)
|
|
464
|
+
self._hSimConnect = None
|
|
465
|
+
|
|
466
|
+
# ── dispatch ──────────────────────────────────
|
|
467
|
+
|
|
468
|
+
def call_dispatch(self, callback):
|
|
469
|
+
"""设置并调用 dispatch 回调处理 SimConnect 消息。
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
callback: 回调函数,签名 (pData, cbData, pContext) -> None。
|
|
473
|
+
其中 pData 是 c_void_p,指向 SIMCONNECT_RECV 结构体。
|
|
474
|
+
"""
|
|
475
|
+
if not self._dll or not self._hSimConnect:
|
|
476
|
+
return
|
|
477
|
+
with self._lock:
|
|
478
|
+
self._dispatch_cb = self._DispatchProc(callback)
|
|
479
|
+
self._dll.SimConnect_CallDispatch(
|
|
480
|
+
self._hSimConnect, self._dispatch_cb, None
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def dispatch(self):
|
|
484
|
+
"""处理一次 SimConnect 消息队列。需要先通过 set_dispatch_cb() 设置回调。"""
|
|
485
|
+
if not self._dll or not self._hSimConnect:
|
|
486
|
+
return
|
|
487
|
+
with self._lock:
|
|
488
|
+
cb = self._dispatch_cb
|
|
489
|
+
if not cb:
|
|
490
|
+
return
|
|
491
|
+
self._dll.SimConnect_CallDispatch(
|
|
492
|
+
self._hSimConnect, cb, None
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def set_dispatch_cb(self, callback):
|
|
496
|
+
"""设置 dispatch 回调函数(不触发调用)。
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
callback: 回调函数,签名 (pData, cbData, pContext) -> None。
|
|
500
|
+
"""
|
|
501
|
+
with self._lock:
|
|
502
|
+
self._dispatch_cb = self._DispatchProc(callback)
|
|
503
|
+
|
|
504
|
+
# ── 后台 dispatch 线程 ────────────────────────
|
|
505
|
+
|
|
506
|
+
def start_background_dispatch(self, callback=None):
|
|
507
|
+
"""启动后台线程持续 dispatch(适合高频率数据接收场景)。
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
callback: 可选,设置 dispatch 回调;已设置则传 None。
|
|
511
|
+
"""
|
|
512
|
+
if callback:
|
|
513
|
+
self.set_dispatch_cb(callback)
|
|
514
|
+
if not self._dispatch_cb:
|
|
515
|
+
raise RuntimeError("请先通过 set_dispatch_cb 设置回调")
|
|
516
|
+
if self._dispatch_running:
|
|
517
|
+
logger.debug("后台 dispatch 已在运行")
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
self._dispatch_stop_event.clear()
|
|
521
|
+
self._dispatch_running = True
|
|
522
|
+
self._dispatch_thread = threading.Thread(
|
|
523
|
+
target=self._dispatch_loop, daemon=True,
|
|
524
|
+
name="SimConnectDispatch",
|
|
525
|
+
)
|
|
526
|
+
self._dispatch_thread.start()
|
|
527
|
+
logger.debug("后台 dispatch 线程已启动")
|
|
528
|
+
|
|
529
|
+
def stop_background_dispatch(self):
|
|
530
|
+
"""停止后台 dispatch 线程。"""
|
|
531
|
+
self._dispatch_running = False
|
|
532
|
+
self._dispatch_stop_event.set() # 立即唤醒 dispatch 循环
|
|
533
|
+
if self._dispatch_thread and self._dispatch_thread.is_alive():
|
|
534
|
+
self._dispatch_thread.join(timeout=2)
|
|
535
|
+
if self._dispatch_thread.is_alive():
|
|
536
|
+
logger.warning("后台 dispatch 线程在 2 秒内未退出")
|
|
537
|
+
self._dispatch_thread = None
|
|
538
|
+
logger.debug("后台 dispatch 线程已停止")
|
|
539
|
+
|
|
540
|
+
def _dispatch_loop(self):
|
|
541
|
+
"""后台 dispatch 循环。
|
|
542
|
+
|
|
543
|
+
使用 Event.wait() 替代 time.sleep(),支持被 stop_background_dispatch()
|
|
544
|
+
立即唤醒退出,避免线程卡在 sleep 中无法及时响应停止信号。
|
|
545
|
+
"""
|
|
546
|
+
while self._dispatch_running and self.is_open:
|
|
547
|
+
try:
|
|
548
|
+
self.dispatch()
|
|
549
|
+
except Exception as e:
|
|
550
|
+
logger.warning("dispatch 异常: %s,1 秒后重试", e)
|
|
551
|
+
if self._dispatch_stop_event.wait(timeout=1.0):
|
|
552
|
+
break
|
|
553
|
+
continue
|
|
554
|
+
# 等待直到有数据或收到停止信号
|
|
555
|
+
self._dispatch_stop_event.wait(timeout=0.001)
|
|
556
|
+
|
|
557
|
+
if self.on_disconnect and not self.is_open:
|
|
558
|
+
try:
|
|
559
|
+
self.on_disconnect()
|
|
560
|
+
except Exception:
|
|
561
|
+
pass
|
|
562
|
+
|
|
563
|
+
# ── 数据定义 ──────────────────────────────────
|
|
564
|
+
|
|
565
|
+
def add_to_data_definition(self, define_id, simvar_name, unit,
|
|
566
|
+
datatype=0, epsilon=0.0, datasize=0xFFFFFFFF):
|
|
567
|
+
"""注册 SimVar 数据定义。
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
define_id: 定义 ID(整数)。
|
|
571
|
+
simvar_name: SimVar 名称(bytes),如 b"PLANE ALTITUDE"。
|
|
572
|
+
unit: 单位(bytes),如 b"Feet"。
|
|
573
|
+
datatype: 数据类型,默认 0(FLOAT64)。
|
|
574
|
+
epsilon: 误差容限,默认 0.0。
|
|
575
|
+
datasize: 数据大小,默认 SIMCONNECT_UNUSED。
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
HRESULT 错误码,0 表示成功。
|
|
579
|
+
"""
|
|
580
|
+
return self._dll.SimConnect_AddToDataDefinition(
|
|
581
|
+
self._hSimConnect, DWORD(define_id), simvar_name, unit,
|
|
582
|
+
c_ulong(datatype), c_float(epsilon), DWORD(datasize),
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
def clear_data_definition(self, define_id):
|
|
586
|
+
"""清除数据定义。"""
|
|
587
|
+
return self._dll.SimConnect_ClearDataDefinition(
|
|
588
|
+
self._hSimConnect, DWORD(define_id)
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
# ── 数据请求 ──────────────────────────────────
|
|
592
|
+
|
|
593
|
+
def request_data_on_simobject_type(self, request_id, define_id,
|
|
594
|
+
object_id=0, simobject_type=0):
|
|
595
|
+
"""请求指定类型的 SimObject 数据。
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
request_id: 请求 ID(整数)。
|
|
599
|
+
define_id: 定义 ID(整数)。
|
|
600
|
+
object_id: 对象 ID,默认 0。
|
|
601
|
+
simobject_type: SimObject 类型,默认 SIMCONNECT_SIMOBJECT_TYPE_USER。
|
|
602
|
+
"""
|
|
603
|
+
return self._dll.SimConnect_RequestDataOnSimObjectType(
|
|
604
|
+
self._hSimConnect, DWORD(request_id), DWORD(define_id),
|
|
605
|
+
DWORD(object_id), c_ulong(simobject_type),
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
def request_data_on_simobject(self, request_id, define_id, object_id=0,
|
|
609
|
+
period=4, flags=0, origin=0, interval=0, limit=0):
|
|
610
|
+
"""请求指定 SimObject 的持续数据更新(更常用的 API)。
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
request_id: 请求 ID(整数)。
|
|
614
|
+
define_id: 定义 ID(整数)。
|
|
615
|
+
object_id: 目标对象 ID,默认 0(SIMCONNECT_OBJECT_ID_USER)。
|
|
616
|
+
period: 更新周期,默认 SIMCONNECT_PERIOD_SECOND。
|
|
617
|
+
flags: 标志,默认 0。
|
|
618
|
+
origin: 数据来源,默认 0。
|
|
619
|
+
interval: 间隔,默认 0。
|
|
620
|
+
limit: 限制,默认 0。
|
|
621
|
+
"""
|
|
622
|
+
return self._dll.SimConnect_RequestDataOnSimObject(
|
|
623
|
+
self._hSimConnect, DWORD(request_id), DWORD(define_id),
|
|
624
|
+
DWORD(object_id), c_ulong(period), DWORD(flags),
|
|
625
|
+
DWORD(origin), DWORD(interval), DWORD(limit),
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
def add_and_request(self, request_id, define_id, simvar_name, unit,
|
|
629
|
+
datatype=0, period=4):
|
|
630
|
+
"""注册定义并立即发起持续请求(一次性完成两个步骤)。
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
request_id: 定义 ID 和请求 ID 共用(简化管理)。
|
|
634
|
+
define_id: 同上,通常设为相同值。
|
|
635
|
+
simvar_name: SimVar 名称(bytes)。
|
|
636
|
+
unit: 单位(bytes)。
|
|
637
|
+
datatype: 数据类型,默认 0(FLOAT64)。
|
|
638
|
+
period: 更新周期,默认 SIMCONNECT_PERIOD_SECOND。
|
|
639
|
+
"""
|
|
640
|
+
self.add_to_data_definition(define_id, simvar_name, unit, datatype)
|
|
641
|
+
return self.request_data_on_simobject(
|
|
642
|
+
request_id, define_id, object_id=0, period=period,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# ── 数据写入 ──────────────────────────────────
|
|
646
|
+
|
|
647
|
+
def set_data_on_simobject(self, define_id, object_id=0,
|
|
648
|
+
flags=0, array_count=1, unit_size=8, data_ptr=None):
|
|
649
|
+
"""向 SimObject 写入数据。
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
define_id: 定义 ID(整数)。
|
|
653
|
+
object_id: 目标对象 ID,默认 0(SIMCONNECT_OBJECT_ID_USER)。
|
|
654
|
+
flags: 标志,默认 0(SIMCONNECT_DATA_SET_FLAG_DEFAULT)。
|
|
655
|
+
array_count: 数组元素个数,默认 1。
|
|
656
|
+
unit_size: 每个元素大小(字节),默认 8(double)。
|
|
657
|
+
data_ptr: 数据指针(c_void_p),为 None 时跳过。
|
|
658
|
+
"""
|
|
659
|
+
if data_ptr is None:
|
|
660
|
+
return
|
|
661
|
+
return self._dll.SimConnect_SetDataOnSimObject(
|
|
662
|
+
self._hSimConnect, DWORD(define_id), c_ulong(object_id),
|
|
663
|
+
DWORD(flags), DWORD(array_count), DWORD(unit_size), data_ptr,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
def write_double(self, define_id, value):
|
|
667
|
+
"""快捷方法:写入一个 double 值。
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
define_id: 定义 ID(整数)。
|
|
671
|
+
value: 浮点数值。
|
|
672
|
+
"""
|
|
673
|
+
data = c_double(float(value))
|
|
674
|
+
return self.set_data_on_simobject(
|
|
675
|
+
define_id, data_ptr=cast(byref(data), c_void_p),
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
@staticmethod
|
|
679
|
+
def event_data_float(value):
|
|
680
|
+
"""将 float 转换为 DWORD 位表示(用于需要浮点参数的事件)。
|
|
681
|
+
|
|
682
|
+
某些 SimConnect 事件(如襟翼角度)需要将 float 的二进制表示
|
|
683
|
+
作为 DWORD 传入。
|
|
684
|
+
|
|
685
|
+
用法:
|
|
686
|
+
sc.transmit_client_event(0, ev_id, SimConnect.event_data_float(15.5))
|
|
687
|
+
"""
|
|
688
|
+
return ctypes.cast(byref(c_float(float(value))), POINTER(DWORD)).contents.value
|
|
689
|
+
|
|
690
|
+
# ── 事件 ──────────────────────────────────────
|
|
691
|
+
|
|
692
|
+
def map_client_event_to_sim_event(self, event_id, event_name):
|
|
693
|
+
"""将客户端事件 ID 映射到 Sim 事件名称。
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
event_id: 事件 ID(整数)。
|
|
697
|
+
event_name: 事件名称(bytes),如 b"KEY_TOGGLE"。
|
|
698
|
+
"""
|
|
699
|
+
return self._dll.SimConnect_MapClientEventToSimEvent(
|
|
700
|
+
self._hSimConnect, DWORD(event_id), event_name,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
def transmit_client_event(self, object_id=0, event_id=0, data=0,
|
|
704
|
+
group_priority=0x19000000, flags=16):
|
|
705
|
+
"""发送客户端事件到 SimObject。
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
object_id: 目标对象 ID,默认 SIMCONNECT_OBJECT_ID_USER。
|
|
709
|
+
event_id: 事件 ID(整数)。
|
|
710
|
+
data: 事件数据(整数)。
|
|
711
|
+
group_priority: 组优先级,默认 0x19000000(STANDARD)。
|
|
712
|
+
flags: 标志,默认 16(SIMCONNECT_EVENT_FLAG)。
|
|
713
|
+
"""
|
|
714
|
+
return self._dll.SimConnect_TransmitClientEvent(
|
|
715
|
+
self._hSimConnect, c_ulong(object_id), DWORD(event_id),
|
|
716
|
+
DWORD(data), DWORD(group_priority), DWORD(flags),
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
# ── 系统事件 ──────────────────────────────────
|
|
720
|
+
|
|
721
|
+
def subscribe_to_system_event(self, event_id, event_name):
|
|
722
|
+
"""订阅系统事件。
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
event_id: 事件 ID(整数)。
|
|
726
|
+
event_name: 事件名称(bytes),如 b"SimStart"。
|
|
727
|
+
"""
|
|
728
|
+
return self._dll.SimConnect_SubscribeToSystemEvent(
|
|
729
|
+
self._hSimConnect, DWORD(event_id), event_name,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# ── 工具 ──────────────────────────────────────
|
|
733
|
+
|
|
734
|
+
def get_last_sent_packet_id(self):
|
|
735
|
+
"""获取最后发送的数据包 ID。"""
|
|
736
|
+
pid = DWORD(0)
|
|
737
|
+
self._dll.SimConnect_GetLastSentPacketID(
|
|
738
|
+
self._hSimConnect, ctypes.byref(pid)
|
|
739
|
+
)
|
|
740
|
+
return pid.value
|
|
741
|
+
|
|
742
|
+
def read_double(self, pData):
|
|
743
|
+
"""从 dispatch 回调的 pData 中读取 double 值。
|
|
744
|
+
|
|
745
|
+
用于 SIMCONNECT_RECV_ID_SIMOBJECT_DATA / _BYTYPE 消息。
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
pData: c_void_p,指向完整消息的指针。
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
(request_id, float_value) 元组,解析失败返回 (None, None)。
|
|
752
|
+
"""
|
|
753
|
+
try:
|
|
754
|
+
header = cast(pData, POINTER(SIMOBJECT_DATA_HEADER)).contents
|
|
755
|
+
base = ctypes.cast(pData, c_void_p).value
|
|
756
|
+
val = ctypes.cast(base + SIMOBJECT_DATA_HEADER_SIZE, POINTER(c_double)).contents.value
|
|
757
|
+
return header.dwRequestID, float(val)
|
|
758
|
+
except Exception:
|
|
759
|
+
return None, None
|
|
760
|
+
|
|
761
|
+
@staticmethod
|
|
762
|
+
def read_data(pData, datatype=0):
|
|
763
|
+
"""从 dispatch 回调指针中按类型读取数据值。
|
|
764
|
+
|
|
765
|
+
用于 dispatch 回调中解析不同类型的数据。
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
pData: c_void_p,指向消息体的指针(dispatch 回调的第一个参数)。
|
|
769
|
+
datatype: SIMCONNECT_DATATYPE_*,默认 0(FLOAT64)。
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
数值(int/float/bytes),解析失败返回 None。
|
|
773
|
+
"""
|
|
774
|
+
try:
|
|
775
|
+
base = ctypes.cast(pData, c_void_p).value
|
|
776
|
+
data_addr = base + SIMOBJECT_DATA_HEADER_SIZE
|
|
777
|
+
if datatype == 0: # FLOAT64
|
|
778
|
+
return ctypes.cast(data_addr, POINTER(c_double)).contents.value
|
|
779
|
+
elif datatype == 1: # FLOAT32
|
|
780
|
+
return ctypes.cast(data_addr, POINTER(c_float)).contents.value
|
|
781
|
+
elif datatype == 2: # INT32
|
|
782
|
+
return ctypes.cast(data_addr, POINTER(c_int32)).contents.value
|
|
783
|
+
elif datatype == 3: # INT16
|
|
784
|
+
return ctypes.cast(data_addr, POINTER(c_int16)).contents.value
|
|
785
|
+
elif datatype == 4: # INT8
|
|
786
|
+
return ctypes.cast(data_addr, POINTER(c_int8)).contents.value
|
|
787
|
+
elif datatype == 5: # STRINGV
|
|
788
|
+
# 读取 256 字节的字符串
|
|
789
|
+
buf = ctypes.cast(data_addr, POINTER(ctypes.c_char * 256)).contents
|
|
790
|
+
return buf.value.decode('utf-8', errors='replace').rstrip('\x00')
|
|
791
|
+
return None
|
|
792
|
+
except Exception:
|
|
793
|
+
return None
|