Files
opi-yolo/drone_controller/base_control.py
T
2026-06-02 11:03:37 +08:00

220 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import time
import math
import threading
from pymavlink import mavutil
class DroneController:
def __init__(self, connection_string='/dev/ttyUSB0', baud=115200):
self.connection_string = connection_string
self.baud = baud
self.master = None
self.is_connected = False
# 飞控实时状态变量(多线程共享)
self.current_mode = None
self.is_armed = False
self.local_x = 0.0
self.local_y = 0.0
self.local_z = 0.0
self.roll = 0.0
self.pitch = 0.0
self.yaw = 0.0
# 线程控制开关
self.running = False
self.telemetry_thread = None
def connect(self):
"""初始化 MAVLink 串口连接,并等待首个心跳包"""
try:
print(f"正在尝试连接飞控 ({self.connection_string})...")
self.master = mavutil.mavlink_connection(self.connection_string, baud=self.baud)
self.master.wait_heartbeat(timeout=5)
self.is_connected = True
print("飞控连接成功,已接收到首个心跳包!")
# 开启后台线程,负责高频监听状态
self.running = True
self.telemetry_thread = threading.Thread(target=self._telemetry_loop, daemon=True)
self.telemetry_thread.start()
return True
except Exception as e:
print(f"连接飞控失败: {e}")
self.is_connected = False
return False
def _telemetry_loop(self):
"""后台线程:高频高吞吐量接收飞控状态反馈,刷新状态变量"""
while self.running and self.is_connected:
try:
# 阻塞式接收,超时 0.1 秒
msg = self.master.recv_match(blocking=True, timeout=0.1)
if not msg:
continue
msg_type = msg.get_type()
# 1. 解析心跳包获取模式与解锁状态
if msg_type == 'HEARTBEAT':
mode_id = msg.custom_mode
self.is_armed = bool(msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED)
# 尝试反向查找模式名称
if self.master:
# 寻找数值对应的模式字符串
for name, num in self.master.mode_mapping().items():
if num == mode_id:
self.current_mode = name
break
# 2. 解析局部 NED 位置坐标
elif msg_type == 'LOCAL_POSITION_NED':
self.local_x = msg.x # 北 (North)
self.local_y = msg.y # 东 (East)
self.local_z = msg.z # 地 (Down)
# 3. 解析姿态角 (弧度转角度)
elif msg_type == 'ATTITUDE':
self.roll = math.degrees(msg.roll)
self.pitch = math.degrees(msg.pitch)
self.yaw = math.degrees(msg.yaw)
except Exception as e:
print(f"\n遥测线程捕获到异常: {e}")
time.sleep(0.1)
def set_mode(self, mode_name):
"""切换飞控模式 (例如 GUIDED 或 OFFBOARD)"""
if not self.is_connected: return False
print(f"正在尝试切换到模式: {mode_name}...")
# 将模式名称转换为飞控可识别的内部数值
if mode_name not in self.master.mode_mapping():
print(f"错误,飞控不支持{mode_name}模式")
return False
mode_id = self.master.mode_mapping()[mode_name]
self.master.set_mode(mode_id)
print(f"已发送模式切换指令 -> {mode_name}")
return True
def arm(self):
"""发送解锁指令"""
if not self.is_connected: return False
print("正在发送解锁指令...")
self.master.mav.command_long_send(
self.master.target_system,
self.master.target_component,
mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
0, 1, 0, 0, 0, 0, 0, 0
)
# 等待并捕获飞控的 ACK 响应,超时时间设为 3 秒
ack = self.master.recv_match(type='COMMAND_ACK', blocking=True, timeout=3.0)
if ack and ack.command == mavutil.mavlink.MAV_COMMAND_ACK:
if ack.result == 0:
print(" ✅ [飞控通信] 飞控解锁成功 (Armed Success)!")
else:
print(f" ❌ [飞控通信] 飞控拒绝了解锁请求,拒绝代码 (Result): {ack.result}")
else:
print(" ⚠️ [飞控通信] 飞控未响应解锁确认,请检查飞控状态或数传指示灯。")
return True
def disarm(self):
"""发送上锁指令"""
if not self.is_connected: return False
print("正在发送强制上锁指令...")
self.master.mav.command_long_send(
self.master.target_system,
self.master.target_component,
mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
0, 0, 0, 0, 0, 0, 0, 0
)
return True
def send_body_velocity(self, vx, vy, vz, yaw_rate=0.0):
"""
核心控制接口:发送机体坐标系(Body Frame)下的速度指令
注意:在下发控制前,如果安全员人为切出了自主模式,则该函数拒绝发送,确保人肉兜底。
yaw_rate:单位(弧度/秒,rad/s),后续使用得转换为角度
"""
if not self.is_connected: return
if self.current_mode not in ['GUIDED', 'OFFBOARD']:
return
# 坐标系掩码定义:使用机体坐标系 (MAV_FRAME_BODY_NED)
# 掩码屏蔽:只保留 vx, vy, vz 和偏航速率 yaw_rate
coordinate_frame = mavutil.mavlink.MAV_FRAME_BODY_NED
type_mask = 0b0000111111000111 # 仅开放速度和偏航率
self.master.mav.set_position_target_local_ned_send(
0, # 系统时间戳(可传0)
self.master.target_system,
self.master.target_component,
coordinate_frame,
type_mask,
0, 0, 0, # 位置 (屏蔽)
vx, vy, vz, # 速度 (机体前后、机体左右、机体上下)
0, 0, 0, # 加速度 (屏蔽)
0, yaw_rate # 偏航,偏航率
)
def print_status(self):
"""利用 \r 实现终端单行丝滑刷新,动态打印 PID 命令与真实状态对比"""
print(
f"\r[Status] Armed: {self.is_armed} | "
f"Pos(N,E,D): ({self.local_x:+.2f}, {self.local_y:+.2f}, {self.local_z:+.2f}) | "
f"Yaw: {self.yaw:+.1f}°\033[K",
end="", flush=True
)
def takeoff(self, altitude):
"""发送起飞指令(仅在 GUIDED 或 OFFBOARD 模式下有效)"""
if not self.is_connected: return False
self.master.mav.command_long_send(
self.master.target_system,
self.master.target_component,
mavutil.mavlink.MAV_CMD_NAV_TAKEOFF,
0, 0, 0, 0, 0, 0, 0, altitude
)
print(f"已下发自动起飞指令 -> 目标高度: {altitude} 米")
return True
def send_target_position(self, x, y, z, yaw):
"""
发送绝对局部位置( NED 坐标系)
x:北向目标位置
y:东向目标位置
z:垂直目标位置
"""
if not self.is_connected: return
# 当人为切出切换模式时,香橙派不再发送命令
if self.current_mode not in ['GUIDED', 'OFFBOARD']: return
# 使用局部 NED 坐标系
coordinate_frame = mavutil.mavlink.MAV_FRAME_NED
# 掩码屏蔽:只保留位置 (x, y, z) 和偏航角 (yaw)
type_mask = 0b0000101111111000
self.master.mav.set_position_target_local_ned_send(
0,
self.master.target_system,
self.master.target_component,
coordinate_frame,
type_mask,
x, y, z,
0, 0, 0,
0, 0, 0,
math.radians(yaw),
0
)
def close(self):
"""释放线程,关闭串口连接"""
self.running = False
if self.telemetry_thread:
self.telemetry_thread.join(timeout=1.0)
print("\n底层控制接口已安全断开。")