โมดูล 4/5 · สัปดาห์ 10–12 · 27 ชม.

อ่าน telemetry ด้วย pymavlink

UAT 104 การเขียนโปรแกรมคอมพิวเตอร์

เวลาเรียนประมาณ 90 นาทีร่าง รอตรวจปรับปรุงล่าสุด 26 กันยายน 2569

บทเรียน

เมื่อเรียนจบโมดูลนี้ ผู้เรียนจะสามารถ

  1. อธิบายโครงสร้างเฟรม MAVLink 2 และอ่านส่วนหัวจากไบต์ดิบได้
  2. อ่านไฟล์ telemetry (.tlog) ด้วย pymavlink และนับข้อความแต่ละชนิด
  3. แปลงหน่วยของข้อความ HEARTBEAT, GLOBAL_POSITION_INT, SYS_STATUS และ VFR_HUD ให้ถูกต้อง
  4. หาช่วงลิงก์ขาดจาก heartbeat และคำนวณอัตราแพ็กเก็ตหายจากเลขลำดับ
  5. ส่งออกข้อมูลเป็น CSV และเชื่อมต่อ SITL แบบสดได้อย่างปลอดภัย

ความรู้พื้นฐานที่ควรมี: UAT 104 โมดูล 3 · UAT 311 โมดูล 3 (MAVLink และ C2 link)

ทำไมต้องรู้

โดรนที่ใช้ PX4 หรือ ArduPilot ส่งสถานะของตัวเองให้สถานีภาคพื้นผ่านโพรโทคอล MAVLink และสถานีภาคพื้นส่วนใหญ่บันทึกทุกข้อความที่ได้รับลงไฟล์ .tlog เมื่อเกิดเหตุผิดปกติ ไฟล์นี้คือหลักฐานว่าโดรนรายงานอะไร เมื่อไร และลิงก์ขาดตอนไหน (UAT 312 โมดูล 4) ผู้ที่อ่านไฟล์นี้ด้วยโปรแกรมได้จะวิเคราะห์เหตุได้เร็วและแม่นกว่าการเลื่อนดูกราฟในโปรแกรมสำเร็จรูป

โมดูลนี้ใช้ไฟล์ telemetry_sample.tlog ซึ่งเป็น ข้อมูลสังเคราะห์ จากแบบจำลองเที่ยวบินเดียวกับโมดูล 3 มีลิงก์ขาดที่ตั้งใจใส่ไว้ 8 วินาที

ห้าขั้นตอน อากาศยานหรือ SITL ส่งข้อมูลผ่านลิงก์ UDP หรือ serial ไปยังสถานีภาคพื้น QGC หรือ MAVProxy ซึ่งบันทึกไฟล์ .tlog แล้ว Python ใช้ pymavlink อ่านและสร้าง CSV กราฟ ช่วงลิงก์ขาด และอัตราแพ็กเก็ตหาย มีเส้นประแสดงว่า Python ต่อกับอากาศยานสดผ่าน UDP ได้ด้วย
ภาพที่ 1 เส้นทางข้อมูล telemetry จากอากาศยานถึงโปรแกรม

ข้อความ MAVLink 2 ทุกข้อความเริ่มด้วยไบต์ 0xFD (MAVLink 1 ใช้ 0xFE) ตามด้วยส่วนหัวที่บอกความยาว เลขลำดับ ผู้ส่ง และรหัสข้อความ จากนั้นเป็นข้อมูล (payload) และ checksum

หนึ่งเฟรม MAVLink 2 เรียงไบต์จากซ้ายไปขวา STX 0xFD LEN INC FLAGS CMP FLAGS SEQ SYS ID COMP ID MSG ID สามไบต์ PAYLOAD 0 ถึง 255 ไบต์ CRC สองไบต์ และ SIG สิบสามไบต์ ด้านล่างอธิบายว่าส่วนหัวสิบไบต์บอกผู้ส่ง ข้อความ และลำดับ ส่วนลายเซ็นมีเฉพาะเมื่อเปิดการลงนามข้อความ
ภาพที่ 2 โครงสร้างเฟรม MAVLink 2

ไฟล์ .tlog เก็บแต่ละข้อความต่อท้ายเวลาที่รับ 8 ไบต์ (ไมโครวินาทีตั้งแต่ปี 1970 เรียงแบบ big-endian) การอ่านไบต์ดิบหนึ่งครั้งช่วยให้เห็นว่าไลบรารีทำอะไรให้เรา

import struct
from datetime import datetime, timezone

with open("telemetry_sample.tlog", "rb") as f:
    raw = f.read(20)

ts_us = struct.unpack(">Q", raw[:8])[0]
print(datetime.fromtimestamp(ts_us / 1e6, tz=timezone.utc).isoformat())
print(" ".join(f"{b:02X}" for b in raw[8:20]))

stx, length, incompat, compat, seq, sysid, compid = raw[8:15]
msgid = int.from_bytes(raw[15:18], "little")
print(f"stx=0x{stx:02X} len={length} seq={seq} sysid={sysid} compid={compid} msgid={msgid}")
2026-03-14T02:30:00+00:00
FD 09 00 00 00 01 01 00 00 00 04 00
stx=0xFD len=9 seq=0 sysid=1 compid=1 msgid=0

รหัสข้อความ 0 คือ HEARTBEAT และ payload ยาว 9 ไบต์ เพราะ MAVLink 2 ตัดไบต์ศูนย์ท้าย payload ออกก่อนส่ง

pymavlink แยกไบต์เป็นข้อความให้ ใช้ mavutil.mavlink_connection() เปิดได้ทั้งไฟล์และการเชื่อมต่อสด

ระวัง: blocking กับไฟล์

เมื่ออ่านจาก ไฟล์ ให้ใช้ recv_match(blocking=False) แล้วหยุดเมื่อได้ None ถ้าใช้ blocking=True โปรแกรมจะค้างรอข้อความใหม่ตลอดไปเมื่ออ่านถึงท้ายไฟล์ ส่วน blocking=True เหมาะกับการเชื่อมต่อสดที่มีข้อความเข้ามาเรื่อย ๆ และควรกำหนด timeout ด้วย

from collections import Counter
from pymavlink import mavutil


def read_all(path, msg_type=None):
    """Yield every message (optionally of one type) from a .tlog file."""
    link = mavutil.mavlink_connection(path)
    while True:
        msg = link.recv_match(type=msg_type, blocking=False)
        if msg is None:
            return
        yield msg


counts = Counter(msg.get_type() for msg in read_all("telemetry_sample.tlog"))
print(counts.most_common())
[('ATTITUDE', 3955), ('GLOBAL_POSITION_INT', 1582), ('VFR_HUD', 1582), ('HEARTBEAT', 396), ('SYS_STATUS', 396)]

ฟังก์ชัน read_all() ใช้ yield ทำให้เป็น generator ซึ่งส่งข้อความออกมาทีละข้อความโดยไม่ต้องเก็บทั้งไฟล์ในหน่วยความจำ เหมาะกับ log ขนาดใหญ่

หน่วยของข้อความสำคัญ

ข้อความ MAVLink เก็บตัวเลขเป็นจำนวนเต็มในหน่วยที่ต่างจากที่คนใช้ ต้องแปลงหน่วยทุกครั้ง ตามเอกสาร common message set

ข้อความฟิลด์หน่วยที่เก็บแปลงเป็น
HEARTBEAT (#0)type, autopilot, base_mode, custom_modeรหัส enum และบิตชนิดอากาศยาน ออโตไพลอต สถานะ arm โหมดการบิน
SYS_STATUS (#1)voltage_batterymV÷ 1000 → V
current_batterycA÷ 100 → A
battery_remaining%ค่า −1 หมายถึงไม่ทราบ
ATTITUDE (#30)roll, pitch, yawrad× 180/π → องศา
GLOBAL_POSITION_INT (#33)lat, londegE7÷ 10⁷ → องศา
alt, relative_altmm÷ 1000 → m
vx, vy, vzcm/s÷ 100 → m/s
hdgcdeg÷ 100 → องศา
VFR_HUD (#74)groundspeed, alt, climbm/s, m, m/sใช้ได้เลย

ตัวอย่างที่ 1 ถอดรหัส heartbeat และตำแหน่ง

hb = next(read_all("telemetry_sample.tlog", "HEARTBEAT"))
print("quadrotor:", hb.type == mavutil.mavlink.MAV_TYPE_QUADROTOR)
print("ArduPilot:", hb.autopilot == mavutil.mavlink.MAV_AUTOPILOT_ARDUPILOTMEGA)
print("mode:", mavutil.mode_string_v10(hb))
print("armed:", bool(hb.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED))

pos = next(m for m in read_all("telemetry_sample.tlog", "GLOBAL_POSITION_INT") if m.relative_alt > 29_000)
print(f"lat {pos.lat / 1e7:.6f}, lon {pos.lon / 1e7:.6f}")
print(f"height above home {pos.relative_alt / 1000:.1f} m, heading {pos.hdg / 100:.0f} deg")
print(f"climb rate {-pos.vz / 100:.1f} m/s")
quadrotor: True
ArduPilot: True
mode: GUIDED
armed: False
lat 14.099999, lon 100.599999
height above home 29.5 m, heading 90 deg
climb rate 2.5 m/s

vz ในกรอบ NED เป็นบวกเมื่อเคลื่อนลง จึงต้องกลับเครื่องหมายเพื่อให้ได้อัตราไต่ขึ้น (ทบทวนเรื่องกรอบพิกัดใน UAT 314 โมดูล 5) สถานะ armed เป็นบิตหนึ่งใน base_mode ต้องใช้ตัวดำเนินการ & แยกออกมา

หาช่วงลิงก์ขาดและแพ็กเก็ตหาย

ข้อความแต่ละข้อความมี เลขลำดับ (sequence number) 0–255 ที่วนกลับ ถ้าเลขกระโดด แปลว่ามีแพ็กเก็ตที่ผู้ส่งส่งแต่เราไม่ได้รับ ส่วน heartbeat นิยมส่งราว 1 ครั้งต่อวินาทีบนลิงก์วิทยุ (MAVLink ระบุว่าอัตรานี้ขึ้นกับช่องทาง ไม่ได้กำหนดตายตัว) ช่วงที่ไม่มี heartbeat นานผิดปกติจึงบอกว่าลิงก์ขาด

ตัวอย่างที่ 2 วิเคราะห์คุณภาพลิงก์

t0 = prev_t = prev_seq = None
gaps, modes = [], []
received = lost = 0

for msg in read_all("telemetry_sample.tlog"):
    received += 1
    seq = msg.get_seq()
    if prev_seq is not None:
        lost += (seq - prev_seq - 1) % 256
    prev_seq = seq

    if msg.get_type() == "HEARTBEAT":
        t = msg._timestamp
        t0 = t if t0 is None else t0
        if prev_t is not None and t - prev_t > 3:
            gaps.append((round(prev_t - t0, 1), round(t - prev_t, 1)))
        prev_t = t
        mode = mavutil.mode_string_v10(msg)
        if not modes or modes[-1][1] != mode:
            modes.append((round(t - t0, 1), mode))

print("heartbeat gaps (start s, length s):", gaps)
print(f"lost {lost} of {received + lost} packets ({100 * lost / (received + lost):.2f}%)")
print("mode changes:", modes)
heartbeat gaps (start s, length s): [(149.0, 9.0)]
lost 160 of 8071 packets (1.98%)
mode changes: [(0.0, 'GUIDED'), (22.0, 'AUTO'), (352.0, 'RTL'), (387.0, 'LAND')]

พบช่วงไม่มี heartbeat 9 วินาทีหลังวินาทีที่ 149 ตรงกับลิงก์ที่ขาด 8 วินาทีในแบบจำลอง (heartbeat ส่งทุก 1 วินาที ช่วงห่างที่เห็นจึงยาวกว่าเวลาที่ขาดจริงได้ถึง 1 วินาที) และเลขลำดับบอกว่าแพ็กเก็ตหาย 160 แพ็กเก็ต การใช้ % 256 ทำให้นับถูกแม้เลขลำดับวนจาก 255 กลับเป็น 0

ส่งออกเป็น CSV

import pandas as pd

rows = [
    {
        "time_s": m.time_boot_ms / 1000,
        "lat_deg": m.lat / 1e7,
        "lon_deg": m.lon / 1e7,
        "rel_alt_m": m.relative_alt / 1000,
    }
    for m in read_all("telemetry_sample.tlog", "GLOBAL_POSITION_INT")
]
position = pd.DataFrame(rows)
position.to_csv("position_from_tlog.csv", index=False)
print(position.shape, "max height", position["rel_alt_m"].max(), "m")
(1582, 4) max height 30.0 m

ไฟล์ที่ได้ใช้ต่อด้วยเครื่องมือจากโมดูล 3 ได้ทันที time_boot_ms เป็นเวลานับจากที่ออโตไพลอตเปิดเครื่อง ไม่ใช่เวลาจริง ถ้าต้องการเวลาจริงให้ใช้ _timestamp ที่สถานีภาคพื้นบันทึก

เชื่อมต่อ SITL แบบสด

เมื่อพร้อมแล้วจึงต่อกับ SITL (UAT 312 โมดูล 2) โค้ดเดียวกันใช้ได้ เปลี่ยนแค่ต้นทาง ArduPilot SITL ผ่าน MAVProxy ส่งออกทาง UDP พอร์ต 14550 และ PX4 SITL ส่งให้สถานีภาคพื้นทาง UDP 14550 และให้โปรแกรมควบคุมทาง UDP 14540

from pymavlink import mavutil

link = mavutil.mavlink_connection("udp:127.0.0.1:14550")
link.wait_heartbeat(timeout=10)
print("connected to system", link.target_system)

for _ in range(20):
    att = link.recv_match(type="ATTITUDE", blocking=True, timeout=2)
    if att is not None:
        print(f"roll {att.roll:.3f} rad, pitch {att.pitch:.3f} rad")

ความปลอดภัย

บทนี้ อ่าน telemetry อย่างเดียว การส่งคำสั่งให้โดรนผ่าน pymavlink ต้องทำใน SITL ก่อนเสมอ และห้ามเปิดพอร์ต MAVLink ของอากาศยานจริงออกสู่อินเทอร์เน็ต เพราะใครก็ส่งคำสั่งได้

ปฏิบัติการประจำโมดูล

ปฏิบัติการ: รายงานคุณภาพลิงก์จาก tlog

อ้างอิงแนวทางทดลอง L09 “อ่าน MAVLink telemetry จาก SITL” ในคลังความรู้โดรน

  1. อ่าน telemetry_sample.tlog นับข้อความแต่ละชนิด และคำนวณอัตราข้อความต่อวินาทีของแต่ละชนิด
  2. หาช่วงลิงก์ขาดและอัตราแพ็กเก็ตหาย และรายงานเวลาเปลี่ยนโหมดการบิน
  3. ส่งออกตำแหน่ง แรงดันแบตเตอรี่ (จาก SYS_STATUS) และความเร็ว (จาก VFR_HUD) เป็น CSV แล้วเทียบกับ flight_log_sample.csv ของโมดูล 3
  4. ถ้ามีคอมพิวเตอร์ที่รัน SITL ได้ ต่อแบบสด บินภารกิจสั้น บันทึก tlog แล้วรันโปรแกรมเดิมกับไฟล์นั้น

ข้อผิดพลาดที่พบบ่อย

ระวัง

  • ใช้ blocking=True กับไฟล์ จนโปรแกรมค้างที่ท้ายไฟล์
  • ลืมแปลงหน่วย เช่น นำ lat ที่เป็น degE7 ไปวาดแผนที่ตรง ๆ หรืออ่าน relative_alt เป็นเมตร
  • ลืมกลับเครื่องหมาย vz ในกรอบ NED
  • นับแพ็กเก็ตหายโดยไม่คิดการวนของเลขลำดับ
  • สรุปว่า heartbeat ต้องมาทุก 1 วินาทีตามมาตรฐาน ทั้งที่เป็นเพียงธรรมเนียมบนลิงก์วิทยุ
  • เปิดพอร์ต MAVLink ออกสู่เครือข่ายภายนอก

สรุป

  • เฟรม MAVLink 2 เริ่มด้วย 0xFD มีส่วนหัวบอกความยาว ลำดับ ผู้ส่ง และรหัสข้อความ
  • mavutil.mavlink_connection() เปิดได้ทั้งไฟล์ .tlog และการเชื่อมต่อสด ถ้าอ่านไฟล์ต้องใช้ blocking=False
  • ต้องแปลงหน่วยทุกครั้ง: degE7 → องศา, mm → m, cm/s → m/s, cdeg → องศา
  • ช่วงไม่มี heartbeat บอกลิงก์ขาด และเลขลำดับที่กระโดดบอกจำนวนแพ็กเก็ตที่หาย
  • ส่งออกเป็น CSV แล้ววิเคราะห์ต่อด้วย pandas และทดสอบกับ SITL ก่อนทำงานกับอากาศยานจริง

แบบฝึกตรวจความเข้าใจ

  1. ไบต์แรกของเฟรม MAVLink 2 คืออะไร และ MAVLink 1 คืออะไร
  2. lat = 140999986 ในข้อความ GLOBAL_POSITION_INT คือละติจูดกี่องศา
  3. voltage_battery = 24310 ใน SYS_STATUS คือกี่โวลต์
  4. เลขลำดับข้อความก่อนหน้าคือ 250 ข้อความถัดไปที่ได้รับคือ 3 มีแพ็กเก็ตหายกี่แพ็กเก็ต
  5. ทำไมอ่านไฟล์ .tlog ด้วย recv_match(blocking=True) แล้วโปรแกรมค้าง
เฉลย
  1. MAVLink 2 คือ 0xFD ส่วน MAVLink 1 คือ 0xFE
  2. องศา
  3. V
  4. แพ็กเก็ต
  5. เมื่ออ่านถึงท้ายไฟล์ ฟังก์ชันจะรอข้อความใหม่ที่ไม่มีวันมาถึง ต้องใช้ blocking=False แล้วหยุดเมื่อได้ None

สรุปสูตรสำคัญ

แปลงหน่วยพิกัดของ MAVLink
อัตราแพ็กเก็ตหายจากเลขลำดับ

แหล่งอ้างอิงหลัก

  1. MAVLink Development Team. Packet serialization. MAVLink developer guide. link
  2. MAVLink Development Team. MAVLink common message set (common.xml). link
  3. ArduPilot and MAVLink contributors. (2026). pymavlink (Version 2.4.50) [Computer software]. PyPI. link
  4. ArduPilot Dev Team. SITL simulator (software in the loop). link
  5. PX4 Autopilot. PX4 user and developer guide. link
  6. MAVLink Development Team. MAVLink developer guide. link

อ่านเพิ่มเติม

ศึกษาหน่วยความรู้ที่กำหนดล่วงหน้า ดูสื่อประกอบ และทำ quiz ประจำโมดูล

ในชั้นเรียน / ภาคสนาม

ปฏิบัติการในห้องแล็บหรือภาคสนามตามใบงาน พร้อม checklist ความปลอดภัย

หลักฐานการเรียนรู้: ใบงานที่ผ่านการตรวจและผล quiz

แบบทดสอบประจำโมดูล

แบบทดสอบนี้ใช้ตรวจความเข้าใจ (formative) ไม่ใช่การสอบเก็บคะแนน

โดเมนความรู้: การสื่อสาร เครือข่าย และ IoT · การเขียนโปรแกรมและเทคโนโลยีดิจิทัล