# MAVLink 原理及開發指南
## I. 範例程式及使用說明
> [GitHub](https://github.com/shengwen-tw/mavlink_example)
```bash=
git clone https://github.com/shengwen-tw/mavlink_example
cd mavlink_example/
make all
```
### 1-1. 使用 USB-TTL 模組進行 Loopback 測試:
將 TXD, RXD 短路:
![](https://hackmd.io/_uploads/HkQyHmz23.jpg)
操作:
```bash=
./mavlink -v -s /dev/ttyUSBX -b 115200
```
### 1-2.作為 TCP/IP Client 連線:
```bash=
./mavlink -v -i ip-address -p port-number
```
`-n` 用於提示使用 TCP/IP 連線
`-v` 用於是否印出訊息提示接收到的封包。
`-s` 後接 USB-TTL 裝置的路徑
`-b` 後接序列埠的 Baudrate
`-i` 後接 IP 位址
`-b` 後接網路 Port number (埠號)
## II. 使用 Codegen 產生 C 語言標頭檔
由於 MAVLink 使用 XML 定義並以 Codegen 產生程式碼,因此各個飛控專案/廠商可以逕行擴充。在產生程式碼前必須先取得目標平台的 XML 檔。基本上大型的 Open-source 專案皆可在 MAVLink 官方 [GitHub](https://github.com/mavlink/mavlink/tree/master/message_definitions/v1.0) 頁面找到。
以下給出 Codegen 產生程式碼的操作流程:
```bash=
sudo apt-get install python3-lxml
git clone https://github.com/mavlink/mavlink.git --recursive
cd mavlink/
python3 ./pymavlink/tools/mavgen.py \
--lang=C --wire-protocol=2.0 \
--output=generated/include/mavlink/v2.0 \
message_definitions/v1.0/common.xml
```
這裡我們指定了 `common.xml`,即 PX4 的封包定義。而 pymavlink 中的 `mavgen.py` 即是 Codegen 程式本體。另外這裡也提供預先產生好的[結果](https://github.com/Tenok-RTOS/mavlink)。
註: 下述的範例程式已包含了 MAVLink 程式碼,不需要再額外產生。
## III. 基本原理
### 3-1. MAVLink 封包格式
在 MAVLink 中無論是發送或是接收,都僅是操作以下的資料結構:
![](https://hackmd.io/_uploads/Hy-MU8M32.png)
以下幾個部分值得注意:
* SYS ID: System ID,用於標註**發送者**,0 表示 Broadcasting, 一般使用 1-255。
* COMP ID: Component ID, 標示**發送者**具體是什麼元件,例如可以設定為 Autopilot 加上 Camera (見 [MAV_COMPONENT](https://mavlink.io/en/messages/common.html#MAV_COMPONENT))
* MSG ID: 封包的編號,例如 0 是代表 Heatbeat
* Payload: 數據區塊
**Remark:** 透過檢查 SYS ID 和 COMP ID 可以確定封包的來源。
詳細說明可見: [Packet Serialization
](https://mavlink.io/en/guide/serialization.html)
---
範例程式使用了兩個執行緒處理 MAVLink 封包 (Message) 的收發。
### 3-2. mavlink_rx_thread()
* 不斷嘗試讀取新的 Byte 並傳入 `mavlink_parse_char()` 函式進行解碼
* 將解碼成功後的 `recvd_msg` 透過 Message queue 傳至 `mavlink_tx_thread()` 中做出回應
```c=
void *mavlink_rx_thread(void *arg)
{
uint8_t c;
mavlink_status_t status;
mavlink_message_t recvd_msg;
while (1) {
read(mavlink_fd, &c, 1);
/* Try parsing the message with new income byte */
if (mavlink_parse_char(MAVLINK_COMM_1, c, &recvd_msg, &status) == 1) {
/* Notify the tx thread */
write(msg_fifo_tx, &recvd_msg, sizeof(recvd_msg));
}
}
}
```
### 3-3. mavlink_tx_thread()
* 固定時間發送特定封包 (由 `MSG_SCHEDULER_INIT()` 和 `MSG_SEND_HZ()` 巨集函式控制)
* 從 Message queue 取出在 `mavlink_rx_thread` 接收到 (解出) 的封包並進行回應。
* 回應的程式最簡單的只要回應 Ack 封包,較複雜的 (即 [Microservice](https://mavlink.io/en/services/)) 則要根據功能維護一個狀態機並進行合適的回覆。
* `parse_mavlink_msg()` 是用於根據接收到的 Message ID 觸發對應的 MAVLink Handler,若是跟 Microservice 有關則必須再改變對應的狀態機狀態。
* 狀態機改變後,Microservice handler 必須再做出回應。
* `mavlink_tx_thread()` 每 10ms 僅能觸發一次,用以限制 CPU 資源使用 (經驗上來說 100Hz 以足夠充分使用)
```c=
void *mavlink_tx_thread(void *arg)
{
MSG_SCHEDULER_INIT(1); // 1Hz
mavlink_message_t recvd_msg;
while (1) {
/* clang-format off */
MSG_SEND_HZ(1,
mavlink_send_play_tune();
mavlink_send_heartbeat();
);
/* clang-format on */
/* Trigger the command parser if received new message from the queue */
if (read(msg_fifo_rx, &recvd_msg, sizeof(recvd_msg)) ==
sizeof(recvd_msg)) {
parse_mavlink_msg(&recvd_msg);
}
/* Limit CPU usage of the thread with execution frequency of 100Hz */
usleep(10000); // 10000us = 10ms
}
}
```
### 3-4. parse_mavlink_msg()
* 在 `mavlink_parser.c` 中新增 MAVLink handler function 用於回應接收到的封包
* 例: ID = 0 的 Heatbeat 封包要呼叫 `mav_heartbeat()` 處理
* 可以在官網查詢封包的 ID 編號,例如: [Heartbeat (#0)](https://mavlink.io/en/messages/common.html#HEARTBEAT).
```c=
enum ENUM_MAV_CMDS {
ENUM_MAVLINK_HANDLER(mav_heartbeat),
ENUM_MAVLINK_HANDLER(mav_command_long),
...
MAV_CMD_CNT
};
struct mavlink_cmd cmd_list[] = {
DEF_MAVLINK_CMD(mav_heartbeat, 0),
DEF_MAVLINK_CMD(mav_command_long, 76),
...
};
void parse_mavlink_msg(mavlink_message_t *msg)
{
for (int i = 0; i < CMD_LEN(cmd_list); i++) {
if (msg->msgid == cmd_list[i].msg_id) {
cmd_list[i].handler(msg);
return;
}
}
...
}
void mav_heartbeat(mavlink_message_t *recvd_msg)
{
...
}
void mav_command_long(mavlink_message_t *recvd_msg)
{
...
}
```
### 3-5. 封包的發送:
我們以 `mavlink_send_play_tune()` 說明封包的發送過程:
```c=
void mavlink_send_play_tune(void)
{
uint8_t sys_id = 1;
uint8_t component_id = 191;
uint8_t target_system = 0;
uint8_t target_component = 0;
uint8_t format = TUNE_FORMAT_MML_MODERN;
const char *tune = "T200 L16 O5 A C O6 E O5 A C O6 E O5 A C O6 E";
mavlink_message_t msg;
mavlink_msg_play_tune_v2_pack(sys_id, component_id, &msg, target_system,
target_component, format, tune);
mavlink_send_msg(&msg);
...
}
```
以下拆成三個步驟說明:
1. 行3至8設定為 `play_tune` 封包所需參數。
2. 行11呼叫 `mavlink_msg_play_tune_v2_pack()` 將參數設定至 `mavlink_message_t msg` 並在進行了諸如設定 Message ID、計算 Checksu 等工作。
3. 透過 `mavlink_send_msg()` 發送至目標裝置。
而 `mavlink_send_msg()` 則是透過 `mavlink_msg_to_send_buffer()` 將 `mavlink_message_t msg` 序列化 (Serialization) 轉為 Byte array 後以 `write()` 送出資料:
```c=
void mavlink_send_msg(mavlink_message_t *msg)
{
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
size_t len = mavlink_msg_to_send_buffer(buf, msg);
write(mavlink_fd, buf, len);
}
```
另外 `mavlink_send_play_tune()` 將 `format` 設定為 `TUNE_FORMAT_MML_MODERN (1)`, 因此 tune 的字串必須以 [Music Macro Language](https://zh.wikipedia.org/zh-tw/Music_Macro_Language) 格式表達。
繁中[維基百科的條目](https://zh.wikipedia.org/zh-tw/Music_Macro_Language)給出了詳細易懂的說明:
* `CDEFGAB`: 依序表示 Do Re Mi Fa So La Si,後接 `#` 或 `+` 為升記號、`-` 為降記號。
* `R`: 休止符。
* `O`: 指定在哪個八度演奏,影響音調高低。
* `>`、`<`: 控制樂譜高八度(`>`)或低八度(`<`)。
* `L`: 音符時值 (四分音符等)。
* `V`: 指定音量大小,後接的數字指定演奏樂器之音量大小。
* `T`: 指定樂器的速度。例如「T120」表示以120BPM來演奏。
最後,關於封包的詳細定義請見: [PLAY_TUNE_V2 (#400)](https://mavlink.io/en/messages/common.html#PLAY_TUNE_V2)。
## IV. Microservice 的原理和設計
MAVLink 定義了許多 [Microservice](https://mavlink.io/en/services/) (即需要時序控制的通訊)。本章節錄並以 [Gimbal Protocol (v2)](https://mavlink.io/en/services/gimbal_v2.html) 為例說明開發一個 Microservice 程式的大致流程。
### 4-1. Gimbal Protocol (v2) 基本概念
基本術語:
* Ground Station: 地面站軟體
* Gimbal Manager: 雲台管理/控制軟體,可以是運作在:
* 飛控軟體 (Autopilot) 上
* 協同電腦 (Companion Computer) 上,即 Raspberry Pi, Qualcomm RB5 等
* 雲台本體 (Gimbal device) 上
* Gimbal Device: 雲台本體
雲台的連接有三種可能的拓樸結構:
**1. 飛控直連雲台 架構:**
![](https://hackmd.io/_uploads/HJSa0Ffhn.png)
**2. 獨立式雲台 (Standalone Integrated Camera / Gimbal) 架構:**
![](https://hackmd.io/_uploads/H1TpRYM23.png)
**3. 飛控 - 協同電腦 - 雲台裝置 架構:**
![](https://hackmd.io/_uploads/Hym00YM33.png)
其中:
* `Gimbal Manager Message` 是給 Gimbal 管理/控制程式之指令封包
* `Gimbal Device Message` 是給 Gimbal 裝置本體之指令封包
以下第三種拓樸結構 (飛控 - 協同電腦 - 雲台裝置) 作為說明。
### 4-3. 封包流程 (Sequence)
如上所述,考慮第三種拓樸,假設要設計協同電腦上的 MAVLink 程式:
* Gimbal Manager 運作在協同電腦上,對 Ground Station 自然是透過 MAVLink 的 `Gimbal Manager Message` 協議交換資訊。
* Gimbal Device 考慮以下兩種情形:
* 支援 MAVLink: Manager 對 Device 使用 `Gimbal Device Message` 通訊
* 不支援 MAVLink: Manager 必須透過其他方式 (TCP/IP, RTPS, Serial 等) 通訊
以下為 Gimbal Protocol (v2) 定義的四種流程 (Sequence):
**1. 裝置探索 (Discovery):**
![](https://hackmd.io/_uploads/H1pHz9fnh.png)
* [COMMAND_LONG (#76)](https://mavlink.io/en/messages/common.html#COMMAND_LONG) --- 控制指令,具體是哪一個行為要設定
* [MAV_CMD_REQUEST_MESSAGE (512)](https://mavlink.io/en/messages/common.html#MAV_CMD_REQUEST_MESSAGE) --- 代入 COMMAND_LONG 中使用
* [GIMBAL_MANAGER_INFORMATION (#280)](https://mavlink.io/en/messages/common.html#GIMBAL_MANAGER_INFORMATION)
* [GIMBAL_DEVICE_INFORMATION (#283)](https://mavlink.io/en/messages/common.html#GIMBAL_DEVICE_INFORMATION)
* [COMMAND_ACK (#77)](https://mavlink.io/en/messages/common.html#COMMAND_ACK)
**2. 一般手動操作 (Normal Manual Control):**
指的是透過操作者透過地面站或是遙控器 (RC, Remote Controller) 控制雲台。
![](https://hackmd.io/_uploads/r1zUzcz3h.png)
* [GIMBAL_MANAGER_SET_ATTITUDE (#282)](https://mavlink.io/en/messages/common.html#GIMBAL_MANAGER_SET_ATTITUDE)
* [GIMBAL_DEVICE_SET_ATTITUDE (#284)](https://mavlink.io/en/messages/common.html#GIMBAL_DEVICE_SET_ATTITUDE)
* [GIMBAL_DEVICE_ATTITUDE_STATUS (#285)](https://mavlink.io/en/messages/common.html#GIMBAL_DEVICE_ATTITUDE_STATUS)
**3. 透過地面站設定感興趣區間 (ROI, Region of Interest)**
![](https://hackmd.io/_uploads/BJ5379G2h.png)
* [COMMAND_LONG (#76)](https://mavlink.io/en/messages/common.html#COMMAND_LONG) --- 控制指令,具體是哪一個行為要設定
* [MAV_CMD_DO_SET_ROI_LOCATION (195)](https://mavlink.io/en/messages/common.html#MAV_CMD_DO_SET_ROI_LOCATION) --- 代入 COMMAND_LONG 中使用
* [GIMBAL_DEVICE_SET_ATTITUDE (#284)](https://mavlink.io/en/messages/common.html#GIMBAL_DEVICE_SET_ATTITUDE)
* [GIMBAL_DEVICE_ATTITUDE_STATUS (#285)](https://mavlink.io/en/messages/common.html#GIMBAL_DEVICE_ATTITUDE_STATUS)
* [MAV_CMD_DO_SET_ROI_NONE (197)](https://mavlink.io/en/messages/common.html#MAV_CMD_DO_SET_ROI_NONE) --- 代入 COMMAND_LONG 中使用
* [COMMAND_ACK (#77)](https://mavlink.io/en/messages/common.html#COMMAND_ACK)
**4. 透過自動飛行任務排程的姿態設定 (Attitude Set During Mission):**
**Caveat:** 官方給的圖應該有誤: 沒有 `CMD_DO_GIMBAL_MANAGER_ATTITUDE`,只有 `CMD_DO_GIMBAL_MANAGER_PITCHYAW`。
不過根據圖的繪製來說,此封包的發送應可不用實作出來。
![](https://hackmd.io/_uploads/Sk6If5z2h.png)
* [COMMAND_LONG (#76)](https://mavlink.io/en/messages/common.html#COMMAND_LONG) --- 控制指令,具體是哪一個行為要設定
* [MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW (1000)](https://mavlink.io/en/messages/common.html#MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW) --- 代入 COMMAND_LONG 中使用
* [GIMBAL_DEVICE_SET_ATTITUDE (#284)](https://mavlink.io/en/messages/common.html#GIMBAL_DEVICE_SET_ATTITUDE)
* [GIMBAL_DEVICE_ATTITUDE_STATUS (#285)](https://mavlink.io/en/messages/common.html#GIMBAL_DEVICE_ATTITUDE_STATUS)
### 4-3. COMMAND_LONG 的接收處理
MAVLink 的 Command 有分 [COMMAND_LONG (#76)](https://mavlink.io/en/messages/common.html#COMMAND_LONG) 和 [COMMAND_INT (#75)](https://mavlink.io/en/messages/common.html#COMMAND_INT)。前者的7個參數欄位都是 `float` 型態,後者的第1, 2, 3, 4, 7欄是float,5和6是 `int32_t` 型態。
[COMMAND_INT (#75)](https://mavlink.io/en/messages/common.html#COMMAND_INT) 主要用於 GPS 相關的指令,因為在儲存經緯度上,`int32_t` 較 `float` 有更高的精度優勢。除此之外絕大多數的功能都是使用 [COMMAND_LONG (#76)](https://mavlink.io/en/messages/common.html#COMMAND_LONG)。
COMMAND_LONG 的接收處理程式如下:
```c=
void mav_command_long(mavlink_message_t *recvd_msg)
{
...
/* Decode command_long message */
mavlink_command_long_t mav_cmd_long;
mavlink_msg_command_long_decode(recvd_msg, &mav_cmd_long);
/* Ignore the message if the target id does not match the system id */
if (get_sys_id() != mav_cmd_long.target_system) {
return;
}
switch (mav_cmd_long.command) {
case MAV_CMD_DO_SET_ROI_LOCATION: /* 195 */
// mav_cmd_do_set_roi_location_handler(&mav_cmd_long);
break;
case MAV_CMD_DO_SET_ROI_NONE: /* 197 */
// mav_cmd_do_set_roi_none_handler(&mav_cmd_long);
break;
case MAV_CMD_REQUEST_MESSAGE: /* 512 */
// mav_cmd_request_msg_handler(&mav_cmd_long);
break;
}
}
```
實際上就是經 3-4 章介紹之 `parse_mavlink_msg()` 函式觸發 `mav_command_long()` 後再根據`mav_cmd_long.command` 的數值選擇對應的處理函式。
### 4-4. Microservice 的程式設計
我們以上述 **透過地面站設定感興趣區間 (ROI, Region of Interest)** 的協議進行說明
![](https://hackmd.io/_uploads/BJ5379G2h.png)
首先根據圖示可知 `mavlink_parser.c` 中必須先增加以下幾個封包的處理函式:
* `COMMAND_LONG (#76)` → `MAV_CMD_DO_SET_ROI_LOCATION (195)`
* `GIMBAL_DEVICE_ATTITUDE_STATUS (#285)`
* `COMMAND_LONG (#76)` → `MAV_CMD_DO_SET_ROI_NONE (197)`
接著便可採 Event-driven 架構撰寫程式並做出適當的回應 (如發送封包回應)。
但實做上可能還須考量:
1. 是否加上狀態機追蹤
2. 目標沒有正確收到 `COMMAND_ACK (#77)` 時的處理
**情況1** 須根據開發者自行斟酌,以下說明 **情況2**:
由於 `COMMAND_ACK (#77)` 的回傳值包含 `ACCEPTED`、`DENIED` 等 (見[MAV_RESULT](https://mavlink.io/en/messages/common.html#MAV_RESULT)),而目標程式可能因資料遺失等重新要求,因此會需要保存特定資料。
顯然的,在此處 Gimbal 的案例中 `COMMAND_ACK (#77)` 的結果是依據`GIMBAL_DEVICE_ATTITUDE_STATUS (#285)` 而定的:
![](https://hackmd.io/_uploads/Sk4Iz3M23.png)
即使 `MAV_CMD_DO_SET_ROI_NONE (197)` 被多次觸發,Gimbal Manager 也應該有辦法正確回應 `COMMAND_ACK (#77)` 封包。