MAVSDK-Python 프로그래밍 완벽 가이드 [제8편]: OFFBOARD 위치 제어 (NED 기준)

안녕하세요! 자율비행 로봇과 드론 제어 알고리즘 연구에 매진하고 계신 대학생 및 연구원 여러분. MAVSDK-Python 연재 블로그의 여덟 번째 시간에 오신 것을 환영합니다.

지난 **[제7편]**에서는 드론 제어의 핵심인 ‘OFFBOARD(오프보드) 모드’의 통신 원리와 3가지 필수 전제 조건, 그리고 방위 기준이 되는 좌표계에 대한 이론적인 배경을 탄탄하게 다졌습니다. 오프보드 모드는 컴패니언 컴퓨터(외부 기기)가 비행 제어기(FC)에게 초당 수십 번의 제어 명령을 쏘아주어 기체를 정밀하게 조종하는 모드였습니다.

이제 이론을 바탕으로 본격적인 실전 코딩에 들어갈 차례입니다. 이번 **[제8편: OFFBOARD 위치 제어 (NED 기준)]**에서는 Offboard 모드에서 가장 기본이 되는 Local_Position_NED를 이용한 위치 제어 방법과 그 예제 코드를 한 줄 한 줄 깊이 있게 분석해 보겠습니다.

공간을 이동하는 자율비행의 첫걸음, 지금부터 자세히 알아보겠습니다!


1. NED 좌표계 복습 및 PositionNedYaw 클래스 이해

코드를 작성하기 전, 우리가 기체에 내릴 명령의 언어인 NED 좌표계를 다시 한번 상기해 보겠습니다. NED는 **North(북), East(동), Down(하)**의 약자로, 지구의 절대 방위를 기준으로 삼는 로컬 좌표계입니다.

MAVSDK에서는 이 NED 좌표계 기반의 위치 이동을 위해 **PositionNedYaw**라는 클래스를 사용합니다. 이 클래스는 총 4개의 매개변수(Parameter)를 받습니다.

  • north_m: 출발지(또는 홈 위치)를 기준으로 북쪽으로 몇 미터(m) 이동할 것인가? (남쪽은 마이너스 값)
  • east_m: 출발지를 기준으로 동쪽으로 몇 미터(m) 이동할 것인가? (서쪽은 마이너스 값)
  • down_m: 출발지를 기준으로 아래로 몇 미터(m) 이동할 것인가?
  • yaw_deg: 기체의 기수(Nose) 방향을 어느 쪽으로 돌릴 것인가? (북쪽 0도, 동쪽 90도, 남쪽 180도, 서쪽 270도)

💡 [연구원 주의사항] 가장 잦은 실수가 발생하는 부분이 바로 down_m (고도)입니다. 비행 동역학 및 항공 분야에서는 전통적으로 Z축의 양(+)의 방향을 지면을 향하는 Down으로 설정합니다. 따라서 기체를 공중으로 띄우고 싶다면 고도는 반드시 마이너스(-) 값을 입력해야 위쪽으로 이동한다는 점을 절대 잊지 마십시오. (예: 5m 상공으로 이륙 = -5.0)


2. 코드 단계별 핵심 로직 분석

이제 본격적으로 offboard_position_ned.py 샘플 스크립트의 작동 순서와 핵심 로직을 뜯어보겠습니다.

① EKF Origin(원점) 초기화 (Initial Setpoint)

지난 7편에서 오프보드 모드의 필수 전제 조건 중 하나가 바로 “모드 진입 전 최소 1초 이상 초기 설정점(Setpoint)을 지속적으로 주입해야 한다”는 것이었습니다.

Python
print("-- EKF origin 초기화")
# Offboard 모드를 시작하기 전에 반드시 위치 제어 기준점(0,0,0)을 초기화해야 합니다.
await drone.offboard.set_position_ned(PositionNedYaw(0.0, 0.0, 0.0, 0.0))

위 코드를 통해 현재 드론이 있는 위치를 North 0, East 0, Down 0으로, 기수 방향도 0도(북쪽)로 세팅하는 스트림을 비행 제어기에 전송합니다,. 만약 이 과정을 생략하고 바로 start()를 호출하면 드론은 명령을 거부(COMMAND_DENIED)합니다.

② 오프보드 모드 시작 및 안전장치

Python
try:
    await drone.offboard.start()
except OffboardError as error:
    print(f"Starting offboard mode failed with error code: {error._result.result}")
    print("-- Disarming")
    await drone.action.disarm()
    return

오프보드 모드 진입 시에는 다양한 이유(센서 불량, GPS 수신 불량 등)로 거부당할 수 있으므로, 항상 try... except 구문을 사용하여 OffboardError 예외 처리를 해주어야 합니다. 에러가 발생하여 진입에 실패하면, 기체의 안전을 위해 즉각 무장 해제(disarm())를 수행하고 프로그램을 종료합니다,.

③ 고도 상승 (Takeoff) 및 위치 이동 로직

오프보드 모드에 성공적으로 진입했다면, 이제 기체를 마음대로 움직일 수 있습니다. Local coordinate 프레임을 기준으로 기체를 위로 5m 이동(이륙)시켜 보겠습니다.

Python
print("-- Go 0m North, 0m East, -5m Down")
await drone.offboard.set_position_ned(PositionNedYaw(0.0, 0.0, -5.0, 0.0))
await asyncio.sleep(10) 

여기서 제어 명령인 set_position_ned()를 내린 직후, 비동기 대기 함수인 await asyncio.sleep(10)을 호출한 것을 볼 수 있습니다. 드론이 물리적인 모터를 돌려 목표 고도 5m까지 상승하는 데에는 당연히 시간이 소요됩니다. 따라서 기체가 목표 지점에 도달할 수 있는 충분한 sleep 타임을 부여하는 것이 핵심입니다.

이어서 특정 좌표로 기체를 이동시키고, 방향도 돌려봅니다.

Python
print("-- Go 5m North, 10m East, -5m Down (기수 방향 동쪽 90도)")
await drone.offboard.set_position_ned(PositionNedYaw(5.0, 10.0, -5.0, 90.0))
await asyncio.sleep(15)

이 한 줄의 명령으로 드론은 출발지 기준 북쪽으로 5m, 동쪽으로 10m 떨어진 위치로 비행하며, 동시에 기체 앞부분이 동쪽(90도)을 바라보게끔 부드럽게 요(Yaw) 회전을 수행합니다,.

④ 오프보드 모드 종료 및 착륙에 대한 고찰

비행 실험이 끝난 후 오프보드 모드를 종료할 때는 await drone.offboard.stop()을 호출합니다. 이때 한 가지 중요한 기술적 이슈(Caveat)를 알고 계셔야 합니다. 실내 모션캡처나 비전 센서를 이용하는 Non-GPS 환경에서 코드를 실행할 경우, stop() 명령이 COMMAND_DENIED 에러를 반환할 수 있습니다. 이는 오프보드 모드를 중지하면 드론이 자동으로 HOLD(위치 유지) 모드로 전환되려 하는데, GPS가 없는 환경에서는 HOLD 모드 전환이 기본적으로 지원되지 않기 때문입니다.


3. 전체 실전 예제 코드 (offboard_position_ned.py)

위의 개념들이 모두 통합된 전체 코드를 살펴보겠습니다. 연구실의 노트북이나 VM 환경에서 스크립트를 생성하여 직접 실행해 보시기 바랍니다.

Python
#!/usr/bin/env python3
import asyncio
from mavsdk import System
from mavsdk.offboard import (OffboardError, PositionNedYaw)

async def run():
    """ Local NED 좌표계를 이용한 Offboard 위치 제어 예제입니다. """
    drone = System()
    # SITL(시뮬레이터) 환경의 기본 포트인 14540으로 연결합니다.
    await drone.connect(system_address="udp://:14540")
    
    print("드론 연결을 기다리는 중...")
    async for state in drone.core.connection_state():
        if state.is_connected:
            print("-- 드론 연결 성공!")
            break

    print("글로벌 위치 정보(GPS)가 안정화되기를 기다립니다...")
    async for health in drone.telemetry.health():
        if health.is_global_position_ok and health.is_home_position_ok:
            print("-- 위치 신호 정상 확보 완료")
            break

    print("-- 모터 무장 (Arming)")
    await drone.action.arm()

    print("-- EKF origin 초기화 (초기 설정점 주입)")
    await drone.offboard.set_position_ned(PositionNedYaw(0.0, 0.0, 0.0, 0.0))

    print("-- Offboard 모드 시작")
    try:
        await drone.offboard.start()
    except OffboardError as error:
        print(f"Offboard 모드 진입 실패: {error._result.result}")
        print("-- 모터 무장 해제 (Disarming)")
        await drone.action.disarm()
        return

    # 1. 5m 고도로 수직 이륙
    print("-- 북 0m, 동 0m, 상공 5m(-5m Down)로 상승")
    await drone.offboard.set_position_ned(PositionNedYaw(0.0, 0.0, -5.0, 0.0))
    await asyncio.sleep(10) # 도달할 때까지 10초 대기

    # 2. 북쪽 5m 위치로 이동하며 기수를 동쪽(90도)으로 회전
    print("-- 북 5m, 동 0m, 상공 5m 위치로 이동, 기수는 동쪽(90도)을 향함")
    await drone.offboard.set_position_ned(PositionNedYaw(5.0, 0.0, -5.0, 90.0))
    await asyncio.sleep(10)

    # 3. 북쪽 5m, 동쪽 10m 위치로 이동
    print("-- 북 5m, 동 10m, 상공 5m 위치로 이동")
    await drone.offboard.set_position_ned(PositionNedYaw(5.0, 10.0, -5.0, 90.0))
    await asyncio.sleep(15)

    # 4. 출발지 상공(동쪽 10m 유지)으로 돌아오며 기수를 남쪽(180도)으로 회전
    print("-- 북 0m, 동 10m, 상공 0m 위치로 복귀 시도, 기수는 남쪽(180도)을 향함")
    await drone.offboard.set_position_ned(PositionNedYaw(0.0, 10.0, 0.0, 180.0))
    await asyncio.sleep(10)

    print("-- Offboard 모드 종료")
    try:
        await drone.offboard.stop()
    except OffboardError as error:
        print(f"Offboard 모드 종료 실패: {error._result.result}")

if __name__ == "__main__":
    # 비동기 루프 실행
    asyncio.run(run())

4. 프로그램 실행 및 결과 확인

터미널에서 python3 offboard_position_ned.py 명령어를 입력하여 위 스크립트를 실행해 봅니다. 동시에 PX4 시뮬레이터(SITL)나 QGroundControl 화면을 띄워 드론의 움직임을 정밀히 관찰해 보십시오.

명령이 실행됨에 따라 기체가 부드럽게 이륙한 후(Z축 제어), 북쪽으로 전진하고 다시 동쪽으로 꺾여 들어가는 모습을 명확하게 확인할 수 있습니다.

또한 yaw_deg 파라미터 변화에 따라, 드론의 이동 방향과 상관없이 카메라 기수 방향이 동쪽(90도)과 남쪽(180도)으로 칼같이 전환되는 부분은 비전 트래킹 기반 자율비행 연구에서 카메라 시야(FOV)를 확보할 때 매우 유용하게 쓰이는 기술입니다.


마치며

이번 제8편에서는 MAVSDK-Python의 Offboard 모드에서 가장 직관적이고 널리 사용되는 Local NED 좌표계 기반의 위치 제어(set_position_ned) 방법을 완벽하게 마스터해 보았습니다.

코드를 분석하면서 Z축(고도)의 음수(-) 표현과 초기 EKF Origin 주입의 중요성, 그리고 비동기 대기(asyncio.sleep())의 역할까지 자율비행 코딩의 디테일한 부분들을 짚어보았습니다,.

하지만 위치 제어만으로는 빠르게 움직이는 목표물을 추적하거나 자연스러운 곡선 회피 기동을 만들어내기엔 한계가 있습니다. 이를 위해 다음 **[제9편]**에서는 로봇 공학과 자율비행 연구에서 훨씬 더 다이내믹하게 활용되는 **’OFFBOARD 속도 제어 (BODY 기준)’**에 대해 깊이 있게 다루어 보겠습니다.

드론이 단순히 좌표로 이동하는 것을 넘어, 현재 바라보는 방향을 기준으로 “앞으로 5m/s로 돌격해!”와 같이 직관적으로 제어하는 짜릿함을 경험하시게 될 것입니다.

실행 중 에러가 발생하거나 코드에 궁금한 점이 있으시다면 언제든 편하게 댓글을 남겨주세요. 연구원 여러분의 버그 없는 매끄러운 비행을 진심으로 응원합니다! 감사합니다.


Author: maponarooo, CEO of QUAD Drone Lab

Date: March 5, 2026

Similar Posts

답글 남기기