这个系列用来记录在开发 AirSim 应用过程中遇到的一些问题和解决方案,由于 AirSim 已经停止维护了,因此我实际的开发平台是 Cosys-AirSim,但这个 fork 在编译和部署的时候有不少坑,后续我会找机会补上。
第一篇博客实际上不需要你编译和部署 AirSim 和 Cosys-AirSim,主要是验证一下 Xbox 游戏手柄是否可用以及基本的通讯功能是否正常。
- AirSim 官方 GitHub 仓库:https://github.com/microsoft/AirSim
- Cosys-AirSim 官方 GitHub 仓库:https://github.com/Cosys-Lab/Cosys-AirSim
这篇博客涉及到的代码我都放在 GitHub 仓库中,欢迎大家 Issue Bug:
1. 硬件准备
我这里使用的是 Xbox 无线控制器,但连接方式使用的是 USB 连接,因为主机没有蓝牙收发器还需要额外买一个蓝牙增强模块。

正确连接后手柄的 XBox 指示灯会常亮,如果这个灯一闪一闪的则说明没有正确连接,在Windows平台上通常会自动弹出驱动安装确认,将驱动装上即可。
2. GUI 工具测试手柄
在写代码之前建议先用一些免费工具来测试手柄各个按键是否正常,虽然网上有很多免费工具,但我自己用的惯的还是 Microsoft Stroe 里面的一个工具 Controller Tester
,可以直接在商店里面搜到:

安装完成后直接打开软件可以看到下面的画面,将手柄上的按钮全部按下,每检测到一个有效触发就会将其标绿,两个遥感和后面 LT
和 RT
本质上是线性轴:

3. pygame 测试代码
我这里使用的是 conda 管理python包,在运行代码之前需要安装以下依赖:
bash
(airsim) $ pip install pygame
然后执行下面的代码,代码中的 read_gamepad_buttons()
函数参数通常是上面软件显示的 Controller X
后面跟着的数字,即设备索引号。
【Note】:下面的代码只能在本地运行,即便你通过 ssh 过来运行也会显示没有监测到手柄,原因好像是 windows 平台下 USB 需要映射成 IP 端口,后面我整明白了会补充到这里。
python
import pygame, os, time
def read_gamepad_buttons(joy_device_index:int=0):
"""
读取游戏手柄上所有按键值。
:return: 按键状态字典
"""
pygame.init()
pygame.joystick.init()
if pygame.joystick.get_count() == 0:
print("未检测到任何游戏手柄, pygame.joystick.get_count()=0")
return None
joystick = pygame.joystick.Joystick(joy_device_index)
joystick.init()
button_states = {}
try:
while True:
time.sleep(0.1)
pygame.event.pump()
button_states = []
balls_states = []
axes_states = []
hat_states = []
for i in range(joystick.get_numbuttons()):
button_states.append(joystick.get_button(i))
for i in range(joystick.get_numballs()):
balls_states.append(joystick.get_ball(i))
for i in range(joystick.get_numaxes()):
axes_states.append(joystick.get_axis(i))
for i in range(joystick.get_numhats()):
hat_states.append(joystick.get_hat(i))
print('-' * 50)
print(f'Button {len(button_states)}: {button_states}')
print(f'Balls {len(balls_states)}: {balls_states}')
print(f'Axes {len(axes_states)}: {axes_states}')
print(f'Hat {len(hat_states)}: {hat_states}')
except KeyboardInterrupt:
print("游戏手柄读取终止。")
finally:
pygame.quit()
return button_states
if __name__ == '__main__':
read_gamepad_buttons(0)
有下面的输出就说明手柄被正确连接:
bash
--------------------------------------------------
Button 16: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Balls 0: []
Axes 6: [0.0, 0.0, 0.0, 0.0, -1.0, -1.0]
Hat 1: [(0, 0)]
--------------------------------------------------
根据自己业务需要去映射各个按键的功能。
【Note】:在 Windows 和 Linux 下部分按键的定义和索引是不同的,通常情况下代码是不通用的。如 LT
按键 RT
在 Windows 下被定义为 Axes
,但在 Linux 下是 Button
。
4. 类封装
为了方便自己和大家使用,我将上面的代码进行了封装,由于我个人通常需要在异步场景下获取手柄状态,因此这里的封装分为两种形式:异步 & 同步。
4.1 异步封装 XBoxControllerReaderAsync
python
import pygame
import asyncio
from typing import List, Tuple
class XBoxControllerReaderAsync:
def __init__(self, joy_device_index: int = 0, poll_interval: float = 0.05):
self.joy_device_index = joy_device_index
self.poll_interval = poll_interval
self.button_states: List[int] = []
self.ball_states: List[Tuple[int, int]] = []
self.axis_states: List[float] = []
self.hat_states: List[Tuple[int, int]] = []
self._running = False
self._task = None
self._joystick = None
async def start(self):
pygame.init()
pygame.joystick.init()
if pygame.joystick.get_count() == 0:
raise RuntimeError("未检测到任何游戏手柄,pygame.joystick.get_count()=0")
self._joystick = pygame.joystick.Joystick(self.joy_device_index)
self._joystick.init()
self._running = True
self._task = asyncio.create_task(self._poll_loop())
async def stop(self):
self._running = False
if self._task:
await self._task
pygame.quit()
async def _poll_loop(self):
while self._running:
pygame.event.pump() # 处理事件队列
self.button_states = [
self._joystick.get_button(i) for i in range(self._joystick.get_numbuttons())
]
self.ball_states = [
self._joystick.get_ball(i) for i in range(self._joystick.get_numballs())
]
self.axis_states = [
self._joystick.get_axis(i) for i in range(self._joystick.get_numaxes())
]
self.hat_states = [
self._joystick.get_hat(i) for i in range(self._joystick.get_numhats())
]
await asyncio.sleep(self.poll_interval)
def get_button_states(self) -> List[int]:
return self.button_states
def get_axis_states(self) -> List[float]:
return self.axis_states
def get_ball_states(self) -> List[Tuple[int, int]]:
return self.ball_states
def get_hat_states(self) -> List[Tuple[int, int]]:
return self.hat_states
运行下面的代码进行测试:
python
async def main():
reader = AsyncGamepadReader()
await reader.start()
try:
for _ in range(100):
print("Buttons:", reader.get_button_states())
print("Axes: ", reader.get_axis_states())
print("Hats: ", reader.get_hat_states())
print("-" * 40)
await asyncio.sleep(0.1)
finally:
await reader.stop()
asyncio.run(main())
4.2 同步封装 XBoxControllerReaderSync
python
import pygame
import threading
import time
from typing import List, Tuple
class XBoxControllerReaderSync:
def __init__(self, joy_device_index: int = 0, poll_interval: float = 0.05):
self.joy_device_index = joy_device_index
self.poll_interval = poll_interval
self.button_states: List[int] = []
self.ball_states: List[Tuple[int, int]] = []
self.axis_states: List[float] = []
self.hat_states: List[Tuple[int, int]] = []
self._running = False
self._thread = None
self._joystick = None
def start(self):
self._running = True
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
self._thread.start()
def stop(self):
self._running = False
if self._thread:
self._thread.join()
pygame.quit()
print("GamepadReader stopped and pygame quit.")
def _poll_loop(self):
print("Initializing pygame...")
pygame.init()
pygame.joystick.init()
count = pygame.joystick.get_count()
print(f"Detected {count} joystick(s)")
if count == 0:
raise RuntimeError("未检测到任何游戏手柄,pygame.joystick.get_count()=0")
self._joystick = pygame.joystick.Joystick(self.joy_device_index)
self._joystick.init()
print(f"Joystick {self._joystick.get_name()} initialized.")
while self._running:
try:
pygame.event.pump()
self.button_states = [
self._joystick.get_button(i)
for i in range(self._joystick.get_numbuttons())
]
self.ball_states = [
self._joystick.get_ball(i)
for i in range(self._joystick.get_numballs())
]
self.axis_states = [
self._joystick.get_axis(i)
for i in range(self._joystick.get_numaxes())
]
self.hat_states = [
self._joystick.get_hat(i)
for i in range(self._joystick.get_numhats())
]
except pygame.error as e:
print(f"Pygame error during polling: {e}")
time.sleep(self.poll_interval)
def get_button_states(self) -> List[int]:
return self.button_states
def get_axis_states(self) -> List[float]:
return self.axis_states
def get_ball_states(self) -> List[Tuple[int, int]]:
return self.ball_states
def get_hat_states(self) -> List[Tuple[int, int]]:
return self.hat_states
运行下面的代码测试:
python
if __name__ == "__main__":
reader = GamepadReader()
try:
reader.start()
for _ in range(100):
print("Buttons:", reader.get_button_states())
print("Axes: ", reader.get_axis_states())
print("Hats: ", reader.get_hat_states())
print("-" * 40)
time.sleep(0.1)
finally:
reader.stop()
4.3 同时测试
在运行下面的测试代码前需要确保你的文件结构如下,其中 xbox_controller_async.py
存放异步封装类代码; xbox_controller_sync.py
存放同步封装类代码:
bash
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 6/4/2025 1:38 PM __pycache__
-a---- 6/4/2025 1:18 PM 1656 test.py
-a---- 6/4/2025 1:42 PM 2213 xbox_controller_async.py
-a---- 6/4/2025 1:41 PM 2731 xbox_controller_sync.py
然后运行 test.py
的代码,这里会先测试异步后测试同步:
python
import asyncio
from xbox_controller_async import XBoxControllerReaderAsync
from xbox_controller_sync import XBoxControllerReaderSync
import asyncio
import time
async def test_async_reader(duration=5):
print("=== 异步读取开始 ===")
reader = XBoxControllerReaderAsync()
await reader.start()
start = time.time()
try:
while time.time() - start < duration:
print("Async - Buttons:", reader.get_button_states())
print("Async - Axes: ", reader.get_axis_states())
print("Async - Hats: ", reader.get_hat_states())
print("-" * 40)
await asyncio.sleep(0.1)
finally:
await reader.stop()
print("=== 异步读取结束 ===\n")
def test_sync_reader(duration=5):
print("=== 同步读取开始 ===")
reader = XBoxControllerReaderSync()
reader.start()
start = time.time()
try:
while time.time() - start < duration:
print("Sync - Buttons:", reader.get_button_states())
print("Sync - Axes: ", reader.get_axis_states())
print("Sync - Hats: ", reader.get_hat_states())
print("-" * 40)
time.sleep(0.1)
finally:
reader.stop()
print("=== 同步读取结束 ===")
async def main():
await test_async_reader(duration=5)
await asyncio.sleep(1)
test_sync_reader(duration=5)
if __name__ == "__main__":
asyncio.run(main())
5. settings.json 文件解析类
在使用过程中我发现很多地方都需要获取 settings.json
这个文件中的信息,例如获取无人机相机相机信息、获取传感器类型等,但每次都自己写的话又非常麻烦,这里我提供两个文件用来解析该配置内容。
common_utils.py
:通用工具脚本(当前只存储了一个 json IO 的函数);airsim_aux.py
:AirSim 辅助工具脚本(当前只存储了一个解析配置文件的类);
使用时需要将两个文件放在同级目录下:
common_utils.py
python
import json
import re
def remove_json_comments(text):
def replacer(match):
s = match.group(0)
if s.startswith('/') and not s.startswith('http'):
return ''
return s
pattern = re.compile(
r'"(?:\\.|[^"\\])*"' # 匹配字符串(包括转义符)
r'|(/\*[\s\S]*?\*/)' # 匹配多行注释
r'|(//[^\r\n]*)' # 匹配单行注释
)
return re.sub(pattern, replacer, text)
def load_clean_json(file_path):
from pathlib import Path
path = Path(file_path).expanduser().resolve(strict=False)
with open(path, 'r', encoding='utf-8') as f:
raw_text = f.read()
clean_text = remove_json_comments(raw_text)
return json.loads(clean_text)
airsim_aux.py
python
import common_utils
from termcolor import colored
from pathlib import Path
class AirSimSettingContantParaser:
def __init__(self, file_path:str="~/Documents/AirSim/settings.json"):
self.file_contant = {}
self.file_path = Path(file_path).expanduser().resolve(strict=False)
self.__load_settings_file__(file_path)
self.camera_pose_keys = ["X", "Y", "Z", "Pitch", "Roll", "Yaw"]
self.airsim_camera_type = {
0: "Scene",
1: "DepthPlanar",
2: "DepthPerspective",
3: "DepthVis",
4: "DisparityNormalized",
5: "Segmentation",
6: "SurfaceNormals",
7: "Infrared",
8: "OpticalFlow",
9: "OpticalFlowVis"
}
self.airsim_sensor_type = {
0: "Camera",
1: "Barometer",
2: "Imu",
3: "Gps",
4: "Magnetometer",
5: "Distance Sensor",
6: "Lidar"
}
def __load_settings_file__(self, file_path:str):
contant = common_utils.load_clean_json(file_path)
if contant is None:
return False
self.file_contant = contant
def is_sensor_registed(self, vehicles_name:str, sensor_name:str):
if False == self.is_vehicle_registed(vehicles_name):
return False
vehicle_contant = self.file_contant["Vehicles"][vehicles_name]
if "Sensors" not in vehicle_contant:
return False
if sensor_name not in vehicle_contant["Sensors"]:
return False
return True
def get_sensor_type(self, vehicles_name:str, sensor_name:str):
if False == self.is_sensor_registed(vehicles_name, sensor_name):
return ""
sensor_contant = self.file_contant["Vehicles"][vehicles_name]["Sensors"]
if sensor_name not in sensor_contant:
return ""
if "SensorType" not in sensor_contant[sensor_name]:
return ""
return self.airsim_sensor_type.get(sensor_contant[sensor_name]["SensorType"])
def is_vehicle_registed(self, vehicles_name:str):
if "Vehicles" not in self.file_contant:
return False
if vehicles_name not in self.file_contant["Vehicles"]:
return False
return True
def is_camera_registed(self, vehicles_name:str, camera_name:str):
if False == self.is_vehicle_registed(vehicles_name=vehicles_name):
return False
vehicle_contant = self.file_contant["Vehicles"][vehicles_name]
if "Cameras" not in vehicle_contant:
return False
if camera_name not in vehicle_contant["Cameras"]:
return False
return True
def get_view_mode(self):
if "ViewMode" not in self.file_contant:
return ""
return self.file_contant["ViewMode"]
def get_clock_speed(self):
if "ClockSpeed" not in self.file_contant:
return -1
return self.file_contant["ClockSpeed"]
def is_vehicle_allow_api(self, drone_name:str):
if False == self.is_vehicle_registed(drone_name):
return False
if "AllowAPIAlways" not in self.file_contant["Vehicles"][drone_name]:
return False
return self.file_contant["Vehicles"][drone_name]["AllowAPIAlways"]
def get_vehicle_type(self, vehicle_name:str):
if False == self.is_vehicle_registed(vehicle_name):
return ""
if "VehicleType" not in self.file_contant["Vehicles"][vehicle_name]:
return ""
return self.file_contant["Vehicles"][vehicle_name]["VehicleType"]
def get_physics_engine_name(self):
if "PhysicsEngineName" not in self.file_contant:
return ""
return self.file_contant["PhysicsEngineName"]
def get_camera_pose(self, vehicles_name:str, camera_name:str):
if False == self.is_camera_registed(vehicles_name=vehicles_name, camera_name=camera_name):
return [] # 返回一个空list
camera_contant = self.file_contant["Vehicles"][vehicles_name]["Cameras"][camera_name]
if False == all(key in camera_contant for key in self.camera_pose_keys):
return []
return [camera_contant["X"], camera_contant["Y"], camera_contant["Z"], camera_contant["Pitch"], camera_contant["Roll"], camera_contant["Yaw"]]
def get_camera_resulation(self, vehicles_name:str, camera_name:str):
if False == self.is_camera_registed(vehicles_name=vehicles_name, camera_name=camera_name):
return []
cam_info = self.file_contant["Vehicles"][vehicles_name]["Cameras"][camera_name]
if "CaptureSettings" not in cam_info:
return []
if "Width" not in cam_info["CaptureSettings"][0] or "Height" not in cam_info["CaptureSettings"][0]:
return []
return [cam_info["CaptureSettings"][0]["Width"], cam_info["CaptureSettings"][0]["Height"]]
def get_camera_fov(self, vehicles_name:str, camera_name:str):
if False == self.is_camera_registed(vehicles_name=vehicles_name, camera_name=camera_name):
return 0.0
cam_settings = self.file_contant["Vehicles"][vehicles_name]["Cameras"][camera_name]
if "CaptureSettings" not in cam_settings:
return 0.0
if "FOV_Degrees" not in cam_settings["CaptureSettings"][0]:
return 0.0
return cam_settings["CaptureSettings"][0]["FOV_Degrees"]
def self_checking(self):
print('=' * 50)
print("Start to self checking...")
if "Vehicles" not in self.file_contant:
print(colored("No Vehicles item detected"))
return
vehicle_count = len(self.file_contant["Vehicles"])
print(f"Total detected [{vehicle_count}] vehicles")
for index, vehicles in enumerate(self.file_contant["Vehicles"]):
vehicle_contant = self.file_contant["Vehicles"][vehicles]
print(f"[{index+1}/{vehicle_count}] vehicle [{vehicles}] info:")
if "Cameras" not in vehicle_contant:
print(colored("\tThis vehicles is not contain camera.", "yellow"))
continue
camera_count = len(vehicle_contant["Cameras"])
print(f'\t[{camera_count}] camera detected.')
for cam in vehicle_contant["Cameras"]:
if False == all(key in vehicle_contant["Cameras"][cam] for key in self.camera_pose_keys):
print(colored(f'\t\t[{cam}] pose information is not complate.'))
else:
cam_info = vehicle_contant["Cameras"][cam]
print(f'\t\t[{cam}]:')
if "CaptureSettings" not in cam_info:
print(colored("\t\t\tNo CaptureSettings item detected.", 'yellow'))
else:
if "Width" not in cam_info["CaptureSettings"][0] or "Height" not in cam_info["CaptureSettings"][0]:
print(colored("\t\t\tNo Width or Height item detected.", 'yellow'))
else:
print(f'\t\t\tResulation: Width=[{cam_info["CaptureSettings"][0]["Width"]}], Height=[{cam_info["CaptureSettings"][0]["Height"]}]')
print(f'\t\t\tX=[{cam_info["X"]:.2f}], Y=[{cam_info["Y"]:.2f}], Z=[{cam_info["Z"]:.2f}]')
print(f'\t\t\tRoll=[{cam_info["Roll"]:.2f}], Pitch=[{cam_info["Pitch"]:.2f}], Yaw=[{cam_info["Yaw"]:.2f}]')
print(colored("Self checking done.", 'green'))
print('AirSim ImageType and SensorType dictory:')
print("ImageType:")
print('\t', self.airsim_camera_type)
print("SensorType:")
print('\t', self.airsim_sensor_type)
print('=' * 50)