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

w6_waypoint_mission.py

W6 專案 1:自動飛正方形並回家(pymavlink),含安全參數與驗收。

檔案位置:scripts/w6_waypoint_mission.py

"""W6 專案 1:自動飛正方形並回家(pymavlink)。

用法:vendor/venv/bin/python -u -I scripts/w6_waypoint_mission.py
先執行 scripts/sitl.sh;本腳本連 tcp:127.0.0.1:5762。
流程:設安全參數(geofence + 低電量 RTL)→ 上傳任務 → arm → AUTO 執行 → 落地後印出驗收結果 → 還原參數。
任務:起飛 10 m → 四個角(北 20 m 起順時針)→ RTL。驗收:落地點離 Home 小於 ACCEPT_M 公尺、任務在 TIMEOUT 內結束。
"""
import math
import time

from pymavlink import mavutil

CONN = "tcp:127.0.0.1:5762"
ALT = 10  # 起飛與航點高度,公尺(相對 home)
SIDE = 20  # 正方形邊長,公尺
M_PER_DEG_LAT = 111_320  # 每度緯度約幾公尺(近似值)
AUTO = 3  # ArduCopter 的 AUTO 模式編號
GUIDED = 4  # ArduCopter 的 GUIDED 模式編號
TIMEOUT = 300  # 任務最多等幾秒
ACCEPT_M = 3  # 驗收:落地點離 Home 的水平距離上限,公尺
# 安全參數:圓柱圍欄(半徑 50 m、高度上限 20 m,越界 RTL)+低電量 RTL
SAFETY = {
    "FENCE_ENABLE": 1,  # 1 = 啟用圍欄
    "FENCE_TYPE": 3,  # bitmask:1 = 高度上限、2 = 以 Home 為圓心的圓;3 = 兩個都開
    "FENCE_ACTION": 1,  # 越界動作:1 = RTL(不行就 LAND)
    "FENCE_RADIUS": 50,  # 圍欄半徑,公尺
    "FENCE_ALT_MAX": 20,  # 圍欄高度上限,公尺
    "BATT_FS_LOW_ACT": 2,  # 低電量動作:2 = RTL
}

T = mavutil.mavlink  # T:MAVLink 常數的命名空間,只為了縮短下面的名稱
# 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, T.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, T.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, mode)
    hb = m.recv_match(type="HEARTBEAT", condition=f"HEARTBEAT.custom_mode=={mode}", blocking=True, timeout=5)
    if hb is None:
        raise SystemExit(f"切換模式 {mode} 失敗")


def get_param(name):
    m.mav.param_request_read_send(m.target_system, m.target_component, name.encode(), -1)
    msg = m.recv_match(type="PARAM_VALUE", condition=f"PARAM_VALUE.param_id=='{name}'", blocking=True, timeout=5)
    if msg is None:
        raise SystemExit(f"讀不到參數 {name}")
    return msg.param_value


def set_param(name, value):
    m.mav.param_set_send(m.target_system, m.target_component, name.encode(), value, T.MAV_PARAM_TYPE_REAL32)
    msg = m.recv_match(type="PARAM_VALUE", condition=f"PARAM_VALUE.param_id=='{name}'", blocking=True, timeout=5)
    if msg is None:
        raise SystemExit(f"設定參數 {name} 沒有回應")


def upload(items):
    """依 MAVLink 任務協定上傳 items:[(command, frame, lat, lon, alt), ...]。"""
    m.mav.mission_clear_all_send(m.target_system, m.target_component)
    m.recv_match(type="MISSION_ACK", blocking=True, timeout=5)
    m.mav.mission_count_send(m.target_system, m.target_component, len(items))
    deadline = time.time() + 30  # 上傳最多等幾秒
    while True:
        msg = m.recv_match(type=["MISSION_REQUEST_INT", "MISSION_REQUEST", "MISSION_ACK"],
                           blocking=True, timeout=5)
        if msg is None or time.time() > deadline:
            raise SystemExit("上傳逾時:飛控沒有回應")
        if msg.get_type() == "MISSION_ACK":
            if msg.type != T.MAV_MISSION_ACCEPTED:
                raise SystemExit(f"上傳失敗:MISSION_ACK type={msg.type}")
            return
        cmd, frame, lat, lon, alt = items[msg.seq]  # msg.seq:飛控現在要第幾項
        m.mav.mission_item_int_send(m.target_system, m.target_component, msg.seq, frame, cmd,
                                    0, 1, 0, 0, 0, 0, lat, lon, alt)  # current=0、autocontinue=1、param1–4=0


def local_xy():
    """回傳 (北, 東) 離 EKF 原點的距離,公尺;收不到就結束腳本。"""
    pos = m.recv_match(type="LOCAL_POSITION_NED", blocking=True, timeout=5)  # pos:LOCAL_POSITION_NED 訊息
    if pos is None:
        raise SystemExit("收不到 LOCAL_POSITION_NED")
    return pos.x, pos.y


originals = {n: get_param(n) for n in SAFETY}  # 結束時還原
try:
    # 1. 安全參數
    for name, value in SAFETY.items():
        set_param(name, value)
    log("安全參數已設定:" + "、".join(f"{k}={v}" for k, v in SAFETY.items()))

    # 2. 任務:以目前位置為 home
    g = m.recv_match(type="GLOBAL_POSITION_INT", blocking=True, timeout=10)  # g:GLOBAL_POSITION_INT 訊息
    if g is None:
        raise SystemExit("收不到 GLOBAL_POSITION_INT")
    lat0, lon0 = g.lat / 1e7, g.lon / 1e7  # home 的緯度、經度,度
    alt0 = g.alt / 1000  # home 的海拔(AMSL),公尺

    def offset(north, east):
        """home 往北 north 公尺、往東 east 公尺的 (緯度, 經度),單位 1e7 度。"""
        lat = lat0 + north / M_PER_DEG_LAT
        lon = lon0 + east / (M_PER_DEG_LAT * math.cos(math.radians(lat0)))
        return int(lat * 1e7), int(lon * 1e7)

    corners = [offset(SIDE, 0), offset(SIDE, SIDE), offset(0, SIDE), offset(0, 0)]
    home_lat, home_lon = offset(0, 0)
    items = [
        (T.MAV_CMD_NAV_WAYPOINT, T.MAV_FRAME_GLOBAL, home_lat, home_lon, alt0),  # 第 0 項:home
        (T.MAV_CMD_NAV_TAKEOFF, T.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, 0, 0, ALT),
    ]
    items += [(T.MAV_CMD_NAV_WAYPOINT, T.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, la, lo, ALT) for la, lo in corners]
    items += [(T.MAV_CMD_NAV_RETURN_TO_LAUNCH, T.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT, 0, 0, 0)]
    upload(items)
    log(f"任務已上傳({len(items)} 項)")

    # 3. arm(EKF 還沒準備好會被拒絕,所以重試)→ AUTO → MISSION_START
    set_mode(GUIDED)
    for _ in range(60):
        if command(T.MAV_CMD_COMPONENT_ARM_DISARM, 1) == 0:
            break
        time.sleep(1)
    else:
        raise SystemExit("arm 失敗")
    home_n, home_e = local_xy()  # 起飛點離 EKF 原點的距離,公尺(驗收用)
    log("已 arm")
    set_mode(AUTO)
    log(f"AUTO;MISSION_START 結果:{command(T.MAV_CMD_MISSION_START, 0, 0)}")

    # 4. 觀察:記錄到達的項目與最遠距離,直到 disarm
    start = time.time()  # 任務開始時刻
    end = start + TIMEOUT
    max_dist = 0.0  # 離 Home 的最大水平距離,公尺
    reached = []  # 到達的項目編號
    while time.time() < end:
        msg = m.recv_match(type=["MISSION_ITEM_REACHED", "LOCAL_POSITION_NED", "HEARTBEAT", "STATUSTEXT"],
                           blocking=True, timeout=1)
        if msg is None:
            continue
        kind = msg.get_type()  # kind:訊息種類
        if kind == "MISSION_ITEM_REACHED":
            reached.append(msg.seq)
            log(f"到達第 {msg.seq} 項")
        elif kind == "STATUSTEXT":
            log(f"STATUSTEXT:{msg.text}")
        elif kind == "LOCAL_POSITION_NED":
            max_dist = max(max_dist, math.hypot(msg.x - home_n, msg.y - home_e))
        elif msg.get_srcSystem() == m.target_system \
                and not msg.base_mode & T.MAV_MODE_FLAG_SAFETY_ARMED:  # 連線上也有 GCS 的 HEARTBEAT,要略過
            break
    else:
        raise SystemExit("逾時:任務沒有在時限內結束")
    duration = time.time() - start  # 任務耗時,秒

    # 5. 驗收
    n, e = local_xy()
    land_dist = math.hypot(n - home_n, e - home_e)  # 落地點離 Home 的水平距離,公尺
    ok = land_dist <= ACCEPT_M and all(i in reached for i in range(2, 6))
    print("—— 驗收 ——")
    print(f"耗時 {duration:.0f} 秒;到達項目 {reached};最遠離 Home {max_dist:.1f} m;落地點離 Home {land_dist:.1f} m")
    print("結果:" + ("通過" if ok else "未通過"))
finally:
    failed = []  # 還原失敗的參數名稱
    for name, value in originals.items():
        try:
            set_param(name, value)
        except SystemExit:
            failed.append(name)
    log(f"警告:這些參數沒有還原成功,請手動檢查:{failed}" if failed else "參數已還原")