Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

w5_velocity_square.py

W5 實驗 2:Guided 模式速度控制(pymavlink):用速度指令飛正方形,或停止發送觀察逾時。

檔案位置:scripts/w5_velocity_square.py

"""W5 實驗 2:Guided 模式速度控制(pymavlink):用速度指令飛正方形。

用法:vendor/venv/bin/python -u -I scripts/w5_velocity_square.py [square|timeout]
  square(預設):起飛後依序往北、東、南、西各飛一邊,回到起點上空後降落。
  timeout:只往東飛 SIDE_SECONDS 秒,然後「停止發送」速度指令 SILENT_SECONDS 秒,
           觀察飛機什麼時候停下來(ArduCopter 的 GUID_TIMEOUT,預設 3 秒)。
先執行 scripts/sitl.sh;本腳本連 tcp:127.0.0.1:5762。
速度指令(SET_POSITION_TARGET_LOCAL_NED 的 vx, vy, vz)必須持續發送,約 10 Hz。
"""
import math
import sys
import time

from pymavlink import mavutil

MODE = sys.argv[1] if len(sys.argv) > 1 else "square"
CONN = "tcp:127.0.0.1:5762"
TAKEOFF_ALT = 10  # 起飛高度,公尺(相對起飛點)
SPEED = 2.0  # 飛行速度,m/s
SIDE_SECONDS = 10  # 每邊飛幾秒(邊長約 SPEED * SIDE_SECONDS = 20 m)
SILENT_SECONDS = 8  # timeout 模式:停止發送後觀察幾秒
SEND_HZ = 10  # 速度指令發送頻率,Hz
GUIDED = 4  # ArduCopter 的 GUIDED 模式編號
LAND = 9  # ArduCopter 的 LAND 模式編號
TIMEOUT = 60  # 起飛與降落最多等幾秒

T = mavutil.mavlink  # T:MAVLink 常數的命名空間,只為了縮短下面的名稱
# type_mask 只啟用速度(vx, vy, vz),忽略位置、加速度、yaw、yaw 速率
VEL_ONLY = (T.POSITION_TARGET_TYPEMASK_X_IGNORE | T.POSITION_TARGET_TYPEMASK_Y_IGNORE
            | T.POSITION_TARGET_TYPEMASK_Z_IGNORE | T.POSITION_TARGET_TYPEMASK_AX_IGNORE
            | T.POSITION_TARGET_TYPEMASK_AY_IGNORE | T.POSITION_TARGET_TYPEMASK_AZ_IGNORE
            | T.POSITION_TARGET_TYPEMASK_YAW_IGNORE | T.POSITION_TARGET_TYPEMASK_YAW_RATE_IGNORE)

# m:MAVLink 連線;m.mav.*_send 送出訊息,m.recv_match 接收訊息
m = mavutil.mavlink_connection(CONN)
m.wait_heartbeat(timeout=10)
m.mav.request_data_stream_send(m.target_system, m.target_component,
                               mavutil.mavlink.MAV_DATA_STREAM_ALL, 10, 1)
t0 = time.time()  # 腳本開始時刻


def log(msg):
    print(f"[{time.time() - t0:5.1f}s] {msg}")


def command(cmd, *params):
    """送 COMMAND_LONG 並等 COMMAND_ACK;回傳 result(0 = ACCEPTED)。"""
    p = list(params) + [0] * (7 - len(params))  # p:補滿 7 個的 COMMAND_LONG 參數(param1–param7)
    m.mav.command_long_send(m.target_system, m.target_component, cmd, 0, *p)
    ack = m.recv_match(type="COMMAND_ACK", condition=f"COMMAND_ACK.command=={cmd}",
                       blocking=True, timeout=5)
    return None if ack is None else ack.result


def set_mode(mode):
    m.mav.set_mode_send(m.target_system, mavutil.mavlink.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, mode)


def send_velocity(vx, vy, vz):
    """local NED 速度,m/s:vx 北、vy 東、vz 下(往上要給負數)。"""
    m.mav.set_position_target_local_ned_send(
        0, m.target_system, m.target_component, mavutil.mavlink.MAV_FRAME_LOCAL_NED,
        VEL_ONLY, 0, 0, 0, vx, vy, vz, 0, 0, 0, 0, 0)


def fly(label, vx, vy, seconds, send=True):
    """以固定頻率發送速度 seconds 秒(send=False 則只觀察、不發送),每秒印位置與地速。"""
    log(label)
    end = time.time() + seconds
    next_send = time.time()
    next_print = time.time()
    while time.time() < end:
        if send and time.time() >= next_send:
            send_velocity(vx, vy, 0)
            next_send += 1 / SEND_HZ
        pos = m.recv_match(type="LOCAL_POSITION_NED", blocking=True, timeout=0.05)
        if pos is not None and time.time() >= next_print:
            speed = math.hypot(pos.vx, pos.vy)  # 水平地速,m/s
            log(f"  位置 北 {pos.x:6.1f} 東 {pos.y:6.1f} 高 {-pos.z:5.1f} m,地速 {speed:.2f} m/s")
            next_print += 1.0


def wait_disarmed():
    """等 HEARTBEAT 的 base_mode 不再有 ARMED 旗標(落地後飛控自動 disarm);逾時就結束腳本。
    不用高度判斷:relative_alt 是相對 home 的值,落地後可能仍讀到 1 m 以上。"""
    end = time.time() + TIMEOUT
    while time.time() < end:
        hb = m.recv_match(type="HEARTBEAT", blocking=True, timeout=1)
        if hb is None or hb.get_srcSystem() != m.target_system:  # 連線上也有 GCS(system 255)的 HEARTBEAT,要略過
            continue
        if not hb.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED:
            log("降落:完成(已 disarm)")
            return
    raise SystemExit("逾時:降落")


def wait_alt(check, label):
    end = time.time() + TIMEOUT
    while time.time() < end:
        g = m.recv_match(type="GLOBAL_POSITION_INT", blocking=True, timeout=1)  # g:GLOBAL_POSITION_INT 訊息
        if g is not None and check(g.relative_alt / 1000):  # relative_alt 單位 mm → m
            log(f"{label}:完成")
            return
    raise SystemExit(f"逾時:{label}")


# 1. 切 GUIDED、arm(EKF 還沒準備好會被拒絕,所以重試)
set_mode(GUIDED)
if m.recv_match(type="HEARTBEAT", condition=f"HEARTBEAT.custom_mode=={GUIDED}",
                 blocking=True, timeout=5) is None:
    raise SystemExit("切 GUIDED 失敗")
log("模式:GUIDED")
for _ in range(30):
    result = command(mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, 1)
    if result == 0:
        break
    log(f"arm 被拒絕(result={result}),1 秒後重試")
    time.sleep(1)
else:
    raise SystemExit("arm 失敗")
log("已 arm")

# 2. 起飛
command(mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, 0, 0, 0, 0, 0, 0, TAKEOFF_ALT)
wait_alt(lambda alt: alt >= TAKEOFF_ALT * 0.95, "takeoff")

# 3. 速度控制
if MODE == "square":
    for name, vx, vy in [("往北", SPEED, 0), ("往東", 0, SPEED), ("往南", -SPEED, 0), ("往西", 0, -SPEED)]:
        fly(f"{name} {SPEED} m/s {SIDE_SECONDS} 秒", vx, vy, SIDE_SECONDS)
    fly("煞車:速度 0 持續發送 3 秒", 0, 0, 3)
else:
    fly(f"往東 {SPEED} m/s {SIDE_SECONDS} 秒", 0, SPEED, SIDE_SECONDS)
    fly(f"停止發送 {SILENT_SECONDS} 秒,觀察何時停下來", 0, 0, SILENT_SECONDS, send=False)

# 4. 降落
set_mode(LAND)
log("模式:LAND")
wait_disarmed()