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_mavsdk_basic.py

W5 實驗 3:Guided 基本流程(MAVSDK-Python 4.0):arm、takeoff、goto、降落。

檔案位置:scripts/w5_mavsdk_basic.py

"""W5 實驗 3:Guided 基本流程(MAVSDK-Python 4.0):arm、takeoff、goto、降落。

用法:vendor/venv/bin/python -u -I scripts/w5_mavsdk_basic.py
與 scripts/w5_guided_basic.py 做同一件事,用來比較兩種寫法。
先執行 scripts/sitl.sh;本腳本聽 udpin://0.0.0.0:14550(MAVProxy 轉送的埠),所以要先關掉 QGC,
否則兩個程式搶同一個埠。MAVSDK 的 goto 吃經緯度與海拔(AMSL),不是 local NED。
"""
import asyncio
import math
import time

from mavsdk.asyncio import ComponentType, Configuration, Mavsdk
from mavsdk.asyncio.plugins.action import ActionAsync, ActionError
from mavsdk.asyncio.plugins.telemetry import TelemetryAsync

CONN = "udpin://0.0.0.0:14550"  # udpin:本機監聽(udp:// 已被 MAVSDK 標為棄用)
TAKEOFF_ALT = 10  # 起飛高度,公尺(相對起飛點)
GOTO_NORTH = 20  # goto 目標:往北幾公尺(相對起飛點)
GOTO_EAST = 20  # goto 目標:往東幾公尺(相對起飛點)
M_PER_DEG_LAT = 111_320  # 每度緯度約幾公尺(近似值)
TIMEOUT = 60  # 每個階段最多等幾秒
t0 = time.time()  # 腳本開始時刻


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


async def wait_for(label, check):
    """每個位置更新就呼叫 check(position);成立就回傳,逾時就結束腳本。"""
    async def loop():
        async for pos in telemetry.subscribe_position():
            if check(pos):
                return pos

    try:
        pos = await asyncio.wait_for(loop(), TIMEOUT)
    except asyncio.TimeoutError:
        raise SystemExit(f"逾時:{label}")
    log(f"{label}:完成")
    return pos


async def main():
    global telemetry
    config = Configuration.create_with_component_type(ComponentType.GROUND_STATION)
    async with Mavsdk(config) as mavsdk:
        await mavsdk.add_any_connection(CONN)
        system = await mavsdk.first_autopilot(timeout_s=10.0)
        if system is None:
            raise SystemExit("找不到飛控(QGC 是否還開著?)")
        log("已連線")
        action = ActionAsync(system)  # action:arm、takeoff、goto、land 等動作
        telemetry = TelemetryAsync(system)  # telemetry:位置、狀態等遙測

        # 1. arm(EKF 還沒準備好會被拒絕,所以重試;MAVSDK 以例外表示失敗)
        for _ in range(30):
            try:
                await action.arm()
                break
            except ActionError as e:
                log(f"arm 被拒絕({e}),1 秒後重試")
                await asyncio.sleep(1)
        else:
            raise SystemExit("arm 失敗")
        log("已 arm")

        # 2. takeoff:高度達到目標的 95% 就算完成
        await action.set_takeoff_altitude(TAKEOFF_ALT)
        await action.takeoff()
        pos = await wait_for("takeoff", lambda p: p.relative_altitude_m >= TAKEOFF_ALT * 0.95)

        # 3. goto:把「北 20 m、東 20 m」換成經緯度;高度維持(absolute_altitude_m 是 AMSL)
        lat0, lon0 = pos.latitude_deg, pos.longitude_deg  # 起點(起飛點上空)
        lat = lat0 + GOTO_NORTH / M_PER_DEG_LAT
        lon = lon0 + GOTO_EAST / (M_PER_DEG_LAT * math.cos(math.radians(lat0)))
        await action.goto_location(lat, lon, pos.absolute_altitude_m, 0)
        log(f"goto:北 {GOTO_NORTH} m、東 {GOTO_EAST} m")

        def reached(p):
            dn = (p.latitude_deg - lat) * M_PER_DEG_LAT  # 距目標:北向,公尺
            de = (p.longitude_deg - lon) * M_PER_DEG_LAT * math.cos(math.radians(lat0))  # 距目標:東向,公尺
            return math.hypot(dn, de) < 1.0

        await wait_for("goto", reached)

        # 4. 降落:落地後飛控自動 disarm;用 armed 狀態判斷(相對高度落地後可能仍讀到 1 m 以上)
        await action.land()
        log("land 指令已送出")
        async def disarmed():
            async for armed in telemetry.subscribe_armed():
                if not armed:
                    return

        try:
            await asyncio.wait_for(disarmed(), TIMEOUT)
        except asyncio.TimeoutError:
            raise SystemExit("逾時:降落")
        log("降落:完成(已 disarm)")


asyncio.run(main())