목표: 드론이 MQTT로 보내는 raw MAVLink 데이터를 받아 디코딩 및 처리하는 시스템 만들기위해서 그 데이터인 MAVLink 프로토콜 자체를 정리한다. (이전 글에서 "펌웨어가 데이터의 생산자"라고 했는데, 그 데이터가 바로 MAVLink 메시지!)
0. 용어 정리 (프로토콜 / Protobuf / 페이로드)
프로토콜(protocol)이란
통신 규칙(약속)이다. "이런 순서로, 이런 형식으로 데이터를 주고받자"는 합의.
- 비유: 편지 봉투에 "왼쪽 위 = 보내는 사람, 오른쪽 위 = 우표" 같은 약속. 양쪽이 같은 약속을 지켜야 배달된다.
- HTTP, TCP, MAVLink 모두 각자의 "주고받는 규칙"이다.
프로토콜은 층(layer)으로 쌓인다 — TCP/UDP와의 관계
네트워크 수업에서 배운 계층 개념 그대로다.
- TCP/UDP = "데이터를 어떻게 실어 나를까"를 담당하는 운송 계층. 트럭. 안에 뭐가 실렸는지는 신경 쓰지 않는다.
- MAVLink = 그 트럭에 실리는 상자의 내용물 규격. "이 상자엔 위도·경도·고도를 이 순서로 담는다"는 약속. 즉 TCP/UDP보다 위층이다.
[ MAVLink 메시지 ] ← 상자 내용물 규격 (뭘 담을지) ← 내가 디코딩하는 대상
↓ 이걸 실어서
[ TCP / UDP / MQTT ] ← 운송 계층 (어떻게 나를지)
↓
[ IP, 무선 링크 ] ← 실제 물리적 배달
내 시스템의 흐름: 드론이 MAVLink 메시지(내용물)를 만든다 → MQTT/TCP(트럭)에 실어 보낸다 → 내가 받아 트럭에서 상자를 꺼낸다(MQTT 수신) → 상자 내용물(MAVLink)을 규격대로 뜯는 게 디코딩.
페이로드(payload)란
포장·주소·검증용 부가정보를 뺀 진짜 알맹이 데이터
- 비유: 택배에서 송장·완충재·테이프를 뺀 안의 진짜 물건. 편지로 치면 봉투가 아니라 편지 본문
- 네트워크 수업의 "헤더 빼고 남는 게 페이로드"와 정확히 같은 개념
Protobuf(Protocol Buffers)란
구글이 만든 데이터 직렬화 포맷
- 직렬화(serialize): 프로그램 안의 데이터를 네트워크로 보내려고 한 줄의 바이트로 쭉 펴는 것. 받아서 되돌리는 게 역직렬화.
- JSON도 직렬화 포맷이지만, 사람이 읽으라고 글자로 편다({"latitude": 37.12}) → 편하지만 무겁다.
- Protobuf는 가독성을 포기하고 바이너리로 빽빽하게 편다. 대신 미리 스키마(필드 타입·순서 정의서)를 양쪽이 공유해야 복원 가능하다. 글자 라벨을 안 보내고 순서로만 구분하기 때문.
"MAVLink가 Protobuf 같다"는 건 같은 발상이라는 뜻이다: 라벨 없이 바이너리로 빽빽하게, 미리 정한 스키마(= dialect의 메시지 정의)로만 해독
1. MAVLink란
Micro Air Vehicle Link의 약자. 소형 무인기와 통신하기 위한 프로토콜이다.
성격은 우리가 아는 것 중 Protobuf(protocol buffers)에 가장 가깝다.
왜 Protobuf와 비슷한가
- 사람이 읽는 JSON이 아니라, 바이너리로 빽빽하게 압축된 포맷
- 미리 정해진 스키마(메시지 정의)가 있어야 디코딩 가능
- 필드마다 타입과 순서가 정해져 있음
왜 JSON을 안 쓰고 이렇게 만들었나
드론은 대역폭과 자원이 부족한 환경이다. MAVLink는 자원 및 대역폭이 제한된 소형 무인기와 통신하기 위한 효율적 텔레메트리 프로토콜 무선 링크로 초당 수십~수백 개 메시지를 날려야 하는데, {"latitude": 37.1234567} 같은 텍스트를 매번 보내면 너무 무겁고 느리다.
→ 그래서 "위도"를 4바이트 정수 하나에 욱여넣는 식으로 극한까지 압축했다.
2. 메시지가 "종류별로" 나뉜다는 것
MAVLink 통신은 한 덩어리로 흐르지 않는다. 종류가 정해진 메시지들이 각자 따로 날아오는 구조다.
비유: 드론이 지상국에 여러 종류의 엽서를 계속 부친다. 엽서마다 종류(양식)가 정해져 있다.
| 메시지 | 뜻 |
| HEARTBEAT | "나 살아있어요" (심장박동) |
| GLOBAL_POSITION_INT | "제 현재 위경도/고도는 이거예요" |
| ATTITUDE | "제가 지금 이만큼 기울어져 있어요" |
| SYS_STATUS | "배터리 잔량, 센서 상태는 이래요" |
| GPS_RAW_INT | "GPS 원시 데이터예요" |
각 메시지(엽서)에는 고유 번호(message ID)와 정해진 칸(필드)이 있다.
- HEARTBEAT: ID 0번. "기체 타입, 자동조종 종류, 비행 모드, 시스템 상태" 등의 칸.
- GLOBAL_POSITION_INT: ID 33번. "타임스탬프, 위도, 경도, 고도, 속도" 등의 칸.
raw MAVLink를 디코딩한다는 건: "날아온 바이트 덩어리 → 몇 번 엽서인지 알아내고 → 그 양식에 맞춰 칸을 하나씩 꺼내는" 작업 Protobuf 메시지를 .proto 스키마로 파싱하는 것과 같은 개념
3. 메시지 하나가 실제로 어떻게 생겼나 (프레임 구조)
엽서 하나(= 한 메시지)는 이런 봉투에 담겨 날아온다. 아래는 MAVLink v2.0 기준. 앞부분(STX~CHECKSUM)은 항상 있고, 맨 뒤 SIGNATURE는 옵션(서명 켠 경우에만 붙음)이다.

| 순서 | 필드 | 크기 | 비유 | 역할 |
| 1 | STX | 1 byte | 봉투 시작 표시 | "여기서부터 새 엽서 시작". v2 = 0xFD (v1 = 0xFE) |
| 2 | LEN | 1 byte | 내용물 분량 | PAYLOAD가 몇 바이트인지 (0~255) |
| 3 | INC FLAGS (incompatible) |
1 byte | 봉인 방식 표시 | 모르면 메시지를 버려야 하는 플래그. 서명 여부 등이 여기 |
| 4 | CMP FLAGS (compatible) |
1 byte | 참고용 메모 | 몰라도 무시하고 읽으면 되는 부가 정보 |
| 5 | SEQ | 1 byte | 엽서 일련번호 | 0~255 순환. 건너뛴 번호로 유실 감지 |
| 6 | SYS ID | 1 byte | 보낸 기체 주소 | 어느 시스템(드론)이 보냈나 |
| 7 | COMP ID | 1 byte | 보낸 부품 주소 | 그 기체 안 어느 부품(FC, 카메라 등)이 보냈나 |
| 8 | MSG ID | 3 bytes | 엽서 종류 번호 | HEARTBEAT(0)인지 GLOBAL_POSITION_INT(33)인지. v2에서 3바이트로 확장 |
| 9 | PAYLOAD | 0~255 bytes | 엽서 본문 | 실제 데이터(위도, 고도 등) = 알맹이 |
| 10 | CHECKSUM | 2 bytes | 봉인 도장 | 중간에 깨지지 않았는지 검증 |
| 11 | SIGNATURE (옵션) | 13 bytes | 인감 도장 | 위조 방지 전자 서명. link id(1) + timestamp(6) + signature(6) |
핵심: 메시지 ID를 봐야 페이로드를 어떻게 해석할지 결정된다. ID가 33번이면 "페이로드를 GLOBAL_POSITION_INT 양식으로 읽어야겠다"가 된다. 봉투를 뜯기 전엔 안쪽 바이트는 그냥 의미 없는 숫자 뭉치다.
INC FLAGS vs CMP FLAGS (v2에서 새로 생긴 부분)
둘 다 "이 플래그를 모르는 디코더가 만났을 때 어떻게 할래?"에 대한 답이다.
- INCompatible flags → 모르면 메시지를 버려라. (모른 채 읽으면 잘못 읽음. 서명이 붙었다는 표시가 대표 예시)
- COMpatible flags → 모르면 무시하고 읽어라. (몰라도 본문 해석엔 지장 없음)
비유(HTTP 헤더): "이해 못 하면 요청 자체를 거부해야 하는 헤더" vs "몰라도 그냥 무시하면 되는 부가 헤더"의 차이.
SIGNATURE (맨 뒤 옵션 13바이트)
메시지 위조 방지용 전자 서명. "진짜 우리 드론이 보냈고 중간 조작이 없었다"를 보증한다. 서명 기능을 켠 경우에만 붙고(그림에서 점선), 안 쓰면 CHECKSUM에서 메시지가 끝난다.
핵심: 메시지 ID를 봐야 페이로드를 어떻게 해석할지 결정된다. ID가 33번이면 "페이로드를 GLOBAL_POSITION_INT 양식으로 읽어야겠다"가 된다. 봉투를 뜯기 전엔 안쪽 바이트는 그냥 의미 없는 숫자 뭉치다.
실무에서 디코딩되는 순서
- STX로 메시지 시작을 찾는다 (0xFD면 v2)
- INC FLAGS로 서명 여부 등 해석 가능 여부를 확인
- MSG ID(3바이트)로 어떤 메시지인지 판별
- PAYLOAD를 해당 dialect 스키마로 해석
- CHECKSUM으로 무결성 검증
실제로는 pymavlink 같은 라이브러리가 이 프레임 해체를 자동으로 해준다. 바이트를 손으로 자를 일은 거의 없고, 라이브러리가 "MSG ID는 33, 필드는 이거"라고 객체로 건네준다. 다만 구조를 알아야 에러(서명 파싱 실패, v1/v2 혼동 등) 원인을 짚을 수 있다.
여기서 dialect가 다시 등장
메시지 ID ↔ 양식의 대응표(어느 ID가 무슨 메시지고 칸이 뭔지)가 바로 dialect에 정의돼 있다. 표준 MAVLink에 공통 메시지가 있고, ardupilotmega 방언에 ArduPilot 고유 메시지가 추가돼 있다. → 디코더에게 방언을 알려줘야 ID→양식 대응표를 갖고 제대로 파싱할 수 있다.
한 줄 요약(실무 연결): MAVLink 디코딩이란 "바이너리 봉투에서 메시지 ID를 읽고, 해당 dialect 스키마로 페이로드 칸을 꺼내는 일"이며, 이게 내 시스템의 심장부다.
Q&A
Q. MAVLink를 JSON이 아니라 Protobuf 같은 바이너리 스키마 포맷으로 만든 이유는?
드론은 자원이 부족한 환경이라 JSON처럼 무거운 텍스트를 쓸 수 없어서, 바이너리 스키마 포맷으로 최대한 경량화했다.
Q. "메시지가 종류별로 나뉜다"는 게 무슨 뜻? (HEARTBEAT / GLOBAL_POSITION_INT)
메시지마다 고유 ID가 있어 구별된다. ID가 0이면 HEARTBEAT 정보가, 다른 ID면 GLOBAL_POSITION_INT 등 해당 메시지의 데이터가 들어온다.
Q. 디코딩할 때 왜 메시지 ID를 먼저 봐야 하나?
메시지마다 담긴 데이터(필드)가 다르기 때문에, ID를 먼저 봐야 어떤 양식으로 칸을 꺼낼지 알 수 있다.
Q. INC FLAGS와 CMP FLAGS의 차이는? (v2)
둘 다 "이 플래그를 모르는 디코더가 만났을 때 어떻게 할지"를 정한다.
- INC(incompatible): 모르면 메시지를 버려야 한다. (서명 여부 등이 여기)
- CMP(compatible): 몰라도 무시하고 읽으면 된다.
참고 자료
- MAVLink 공식 문서: https://mavlink.io/en/
- MAVLink 메시지 정의(common): https://mavlink.io/en/messages/common.html
- MAVLink 메시지 정의(ArduPilot 방언): https://mavlink.io/en/messages/ardupilotmega.html
- pymavlink: https://github.com/ArduPilot/pymavlink
- Protocol Buffers 개요: https://protobuf.dev/overview/
'개발' 카테고리의 다른 글
| [Drone] FC와 Ardupilot/PX4 (0) | 2026.07.13 |
|---|---|
| [NLP] NLP란? (1) | 2026.07.06 |