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

W6 實驗 4:觸發低電量 failsafe(pymavlink),觀察飛控自己切到 RTL 並降落。

檔案位置:scripts/w6_failsafe_battery.py

"""W6 實驗 4:觸發低電量 failsafe(pymavlink),觀察飛控自己切到 RTL 並降落。

用法:vendor/venv/bin/python -u -I scripts/w6_failsafe_battery.py
先執行 scripts/sitl.sh;本腳本連 tcp:127.0.0.1:5762。
做法:起飛飛到北 30 m 後,把 BATT_LOW_VOLT 調到比目前電壓高 1 V,等 BATT_LOW_TIMER 秒後飛控判定低電量,
依 BATT_FS_LOW_ACT 動作。結束時還原三個參數。
"""
import time

from pymavlink import mavutil

CONN = "tcp:127.0.0.1:5762"
ALT = 10  # 起飛高度,公尺(相對起飛點)
FAR = 30  # 觸發前先飛到北幾公尺,這樣才看得出 RTL 飛回來
GUIDED = 4  # ArduCopter 的 GUIDED 模式編號
TIMEOUT = 120  # 每個階段最多等幾秒
FS_LOW_ACT_RTL = 2  # BATT_FS_LOW_ACT:2 = RTL
LOW_TIMER = 2  # BATT_LOW_TIMER:電壓持續低於門檻幾秒才觸發,秒
MODES = {3: "AUTO", 4: "GUIDED", 5: "LOITER", 6: "RTL", 9: "LAND"}  # ArduCopter 模式編號 → 名稱,只用來印 log

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 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} 沒有回應")
    log(f"{name} = {msg.param_value:g}")


def restore(originals):
    """逐一還原參數;某一個失敗不影響其他,最後回報失敗的名稱。"""
    failed = []  # 還原失敗的參數名稱
    for name, value in originals.items():
        try:
            set_param(name, value)
        except SystemExit:
            failed.append(name)
    if failed:
        log(f"警告:這些參數沒有還原成功,請手動檢查:{failed}")


def watch(until):
    """印出模式變化與飛控的 STATUSTEXT,直到 until() 為真;逾時就結束腳本。"""
    end = time.time() + TIMEOUT
    mode = None  # 目前的 custom_mode
    while time.time() < end:
        msg = m.recv_match(type=["HEARTBEAT", "STATUSTEXT"], blocking=True, timeout=1)
        if msg is None:
            continue
        if msg.get_type() == "STATUSTEXT":
            log(f"STATUSTEXT:{msg.text}")
        elif msg.get_srcSystem() == m.target_system:  # 連線上也有 GCS(system 255)的 HEARTBEAT,要略過
            if msg.custom_mode != mode:
                mode = msg.custom_mode
                log(f"模式:{MODES.get(mode, mode)}")
            if until(msg):
                return
    raise SystemExit("逾時")


def disarmed(hb):
    return not hb.base_mode & T.MAV_MODE_FLAG_SAFETY_ARMED


originals = {n: get_param(n) for n in ("BATT_LOW_VOLT", "BATT_LOW_TIMER", "BATT_FS_LOW_ACT")}  # 結束時還原
log(f"原始參數:{originals}")
try:
    set_param("BATT_FS_LOW_ACT", FS_LOW_ACT_RTL)
    set_param("BATT_LOW_TIMER", LOW_TIMER)

    # 1. GUIDED → arm(EKF 還沒好會被拒絕,所以重試)→ takeoff → 飛到北 FAR 公尺
    m.mav.set_mode_send(m.target_system, T.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, GUIDED)
    for _ in range(60):
        if command(T.MAV_CMD_COMPONENT_ARM_DISARM, 1) == 0:
            break
        time.sleep(1)
    else:
        raise SystemExit("arm 失敗")
    log("已 arm")
    command(T.MAV_CMD_NAV_TAKEOFF, 0, 0, 0, 0, 0, 0, ALT)
    end = time.time() + TIMEOUT
    while time.time() < end:
        g = m.recv_match(type="GLOBAL_POSITION_INT", blocking=True, timeout=1)
        if g is not None and g.relative_alt / 1000 >= ALT * 0.95:  # relative_alt 單位 mm → m
            break
    else:
        raise SystemExit("逾時:takeoff")
    log("takeoff 完成")
    m.mav.set_position_target_local_ned_send(
        0, m.target_system, m.target_component, T.MAV_FRAME_LOCAL_NED,
        T.POSITION_TARGET_TYPEMASK_VX_IGNORE | T.POSITION_TARGET_TYPEMASK_VY_IGNORE
        | T.POSITION_TARGET_TYPEMASK_VZ_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,
        FAR, 0, -ALT, 0, 0, 0, 0, 0, 0, 0, 0)
    end = time.time() + TIMEOUT
    while time.time() < end:
        pos = m.recv_match(type="LOCAL_POSITION_NED", blocking=True, timeout=1)  # pos.x:往北距離,公尺
        if pos is not None and abs(pos.x - FAR) < 1.0:
            break
    else:
        raise SystemExit(f"逾時:飛到北 {FAR} m")
    log(f"已飛到北 {FAR} m")

    # 2. 把低電量門檻調到比目前電壓高 1 V
    status = m.recv_match(type="SYS_STATUS", blocking=True, timeout=5)  # status:SYS_STATUS 訊息
    if status is None:
        raise SystemExit("收不到 SYS_STATUS")
    volt = status.voltage_battery / 1000  # mV → V
    log(f"目前電壓 {volt:.2f} V,把 BATT_LOW_VOLT 調到 {volt + 1:.2f} V")
    set_param("BATT_LOW_VOLT", volt + 1)

    # 3. 不再送任何指令,只觀察飛控自己做什麼,直到 disarm
    watch(disarmed)
    log("結束:已 disarm")
finally:
    restore(originals)