في مقال أساسيات الطائرات المسيرة رأينا أن الطائرة لا تطير دون طيار آلي Autopilot يحسب سرعة كل محرك مئات المرات في الثانية، وقرأنا من هذا الطيار أرقام المحركات وهو يعمل في محاكاة. والسؤال الطبيعي بعد ذلك: كيف تكتب أنت كوداً يجعل الطائرة تقلع وتمر بنقاط محددة وتعود، دون أن تلمس عصا التحكم؟ والإجابة لا تبدأ بكتابة كود يحرك المحركات، فهذا عمل الطيار الآلي، وإنما بكود يتحدث مع الطيار الآلي بلغته ويعطيه مهمة، ثم يراقب ما يفعله. وفي هذا المقال سوف نقوم بذلك على طيار آلي حقيقي يعمل في محاكاة على جهازك، ثم نكسر الأشياء عن قصد لنرى كيف يحمي الطيار الآلي الطائرة.
وسوف نناقش في هذا المقال ما يلي:
- الطياران الآليان المفتوحان PX4 وArduPilot: من يطورهما، وما رخصة كل منهما، ومتى تختار أيهما.
- بروتوكول MAVLink الذي تتحدث به الطائرة مع كل شيء حولها.
- محطة التحكم الأرضية QGroundControl، ومكتبة MAVSDK، والتغيير الأخير في حزمتها لـ Python.
- تجربة عملية: PX4 في محاكاة SITL داخل Docker بلا شاشة، وبرنامج Python يقلع ويطير مهمة من أربع نقاط ويعود.
- نكسر الإعداد: سياج جغرافي Geofence تخرج منه الطائرة، ثم فقدان إشارة GPS أثناء المهمة، ونقرأ ردة فعل الطيار الآلي من رسائله.
PX4 وArduPilot: الطيار الآلي المفتوح
الطيار الآلي هو البرنامج الذي يعمل على المتحكم Flight Controller، ويقرأ الحساسات ويثبت الطائرة وينفذ المهام ويطبق الحماية عند الأعطال، وأشهر طيارين مفتوحي المصدر هما PX4 وArduPilot، وكلاهما يدعم الطائرات متعددة المراوح وذات الجناح الثابت والهجينة VTOL وغيرها، ويعمل على كثير من المتحكمات نفسها. والفرق الأهم بينهما للمطور هو الرخصة ومن يقف خلف المشروع:
- PX4: برخصة BSD 3-Clause، ويقول مستودعه إنه مستضاف تحت مؤسسة Dronecode التابعة لمؤسسة Linux Foundation. والرخصة المتساهلة Permissive تعني أن الشركة تستطيع أن تبني منتجاً مغلقاً فوق PX4 دون أن تنشر تعديلاتها، وهذا ما يجعله شائعاً في المنتجات التجارية. وآخر إصدار مستقر وقت كتابة المقال هو v1.17.0 الصادر في 13 مايو 2026.
- ArduPilot: برخصة GPLv3، وتقول صفحة الرخصة في توثيقه إنه مفتوح المصدر بـ GPLv3 ولكنك تستطيع تشغيل كود مغلق على حاسوب مرافق Companion Computer، أي أن تعديل ArduPilot نفسه وتوزيعه يلزمك بنشر التعديل، أما برنامجك المنفصل الذي يتحدث معه فلا. ويذكر تاريخ المشروع أن ArduPilot خرج من Dronecode في سبتمبر 2016 بعد تصويت أعضاء مجلسها على إخراج المشاريع التي رخصتها GPLv3، وأنه بدأ في الشهر نفسه برنامج شركاء ArduPilot Partners. وآخر إصدار لطائرات المراوح Copter وقت الكتابة هو Copter-4.7.1.
وقد يتساءل البعض: أيهما أختار؟ والإجابة أن الاثنين ناضجان، والاختيار في الغالب يأتي من خارج الكود: إذا كنت تبني منتجاً تجارياً وتريد أن تعدل الطيار الآلي نفسه دون نشر التعديل فرخصة PX4 أسهل، وإذا كانت طائرتك أو متحكمك أو المجتمع الذي حولك يعمل بـ ArduPilot فالأفضل أن تبقى معه. والخبر الجيد أن الكود الذي سوف تكتبه في هذا المقال يتحدث MAVLink، والاثنان يتحدثانه.
وقد قارنا هذين الطيارين ببقية المنصات المفتوحة وبمنصة DJI المغلقة في مقال المنصات، وانتقلنا من هذا المقال إلى ROS 2 والحاسوب المرافق وسجلات الطيران في مقال التطوير.
MAVLink: لغة الطائرة
MAVLink بروتوكول رسائل صغير وخفيف، تتبادل به الطائرة مع محطة التحكم ومع الحاسوب المرافق ومع برنامجك رسائل الحالة Telemetry، مثل الموقع والارتفاع والبطارية والوضع، ورسائل الأوامر، مثل اقلع وارجع وهذه نقاط المهمة. ويقول موقعه إن الإصدار الأول MAVLink 1 يضيف 8 بايتات فقط لكل رسالة، وإن MAVLink 2 يضيف 12 بايتاً وهو أكثر أماناً وقابلية للتوسع، وتذكر صفحة الإصدار الثاني أنه وسع رقم الرسالة إلى 24 بت بعد أن كان الإصدار الأول محدوداً بـ 256 رسالة، وأضاف توقيع الرسائل Packet Signing للتأكد من أنها مرسلة من نظام موثوق.
والصورة التالية تبين من يتحدث مع من:

أما رخصة MAVLink فتحتاج انتباهاً، فملف COPYING في مستودعه يقول إن المولد Generator نفسه برخصة (L)GPLv3، ولكن الكود الذي يولده ويدخل في منتجك مرخص باستثناء صريح بـ MIT، ويقول موقع MAVLink إن المكتبات المولدة تستطيع استخدامها في تطبيق مغلق دون نشر كوده.
QGroundControl: محطة التحكم الأرضية
محطة التحكم الأرضية Ground Control Station (واختصاراً GCS) هي البرنامج الذي تخطط فيه المهمة على الخريطة، وتعاير الحساسات، وتضبط المعاملات Parameters، وترى حالة الطائرة أثناء الطيران. وأشهرها QGroundControl، ويقول مستودعه إنه يعمل مع أي طائرة تتحدث MAVLink ومنها PX4 وArduPilot، ورخصته بحسب ملف COPYING.md مزدوجة: Apache 2.0 أو GPLv3 وتختار أيهما، مع ملاحظة في الملف نفسه أن خيار Apache 2.0 يحتاج رخصة تجارية من Qt. وآخر إصدار مستقر وقت الكتابة هو v5.1.5. وفي التجربة لن نستخدمه، لأن المحاكاة تعمل بلا شاشة، ولكنه أول ما تفتحه مع طائرة حقيقية.
MAVSDK: مكتبة تتحدث MAVLink نيابة عنك
تستطيع أن تبني رسائل MAVLink بنفسك، ولكن المهمة الواحدة تحتاج تسلسلاً من الرسائل والتأكيدات وإعادة الإرسال، وهنا يأتي MAVSDK، وهو مكتبة بلغة C++ مع واجهات للغات أخرى، تعطيك دوال مثل arm() وtakeoff() وupload_mission()، وتتولى هي الرسائل. ورخصتها BSD 3-Clause، وهي تحت مظلة مؤسسة Dronecode، ويقول توثيقها إنها تعمل على حاسوب مرافق على الطائرة نفسها أو على الأرض في محطة تحكم أو جهاز محمول.
ويجدر الإشارة هنا إلى تغيير حديث قد يضيعك إذا قرأت أمثلة قديمة: حزمة mavsdk على PyPI كانت حتى الإصدار 3 هي المكتبة القديمة MAVSDK-Python التي تتحدث مع خادم mavsdk_server عبر gRPC، ومنذ الإصدار 4 أصبح الاسم نفسه لمكتبة جديدة تستدعي مكتبة C++ مباشرة دون gRPC ودون خادم منفصل، وبواجهة مختلفة. وتقول صفحة الانتقال إن المكتبة القديمة مستمرة باسم mavsdk-grpc وتستوردها بـ mavsdk_grpc، وإن من يريد البقاء على الاسم القديم يثبت mavsdk<4. ولاحظ الفرق في الكود: في القديمة تكتب drone.action.arm()، وفي الجديدة تنشئ كل إضافة Plugin بنفسك ActionAsync(drone). وفي هذا المقال نستخدم الجديدة mavsdk 4.0.6 بواجهتها غير المتزامنة mavsdk.asyncio.
المحاكاة SITL: طائرة كاملة بلا طائرة
لا تكتب أول كود لك على طائرة حقيقية، والسبب بسيط: الخطأ في الكود هنا يعني طائرة تسقط. والحل أن تشغل الطيار الآلي نفسه على جهازك في محاكاة Software In The Loop (واختصاراً SITL)، فيعمل كود PX4 الحقيقي كما هو، ومحاكي فيزياء مثل Gazebo يعطيه قراءات حساسات مصطنعة ويحرك الطائرة بحسب أوامره للمحركات. وبالتالي ما تراه في المحاكاة هو سلوك PX4 الحقيقي من الوضع والحماية والرسائل، وهذا ما يجعل التجربة مفيدة. وأسهل طريقة لذلك هي Docker Image jonasvautherin/px4-gazebo-headless التي يذكرها توثيق MAVSDK، ويقول مستودعها إن الوسم 1.17.0 هو آخر إصدار مدعوم من PX4، وإنها ترسل MAVLink إلى المنفذ 14550 لمحطة التحكم و14540 لبرامج مثل MAVSDK، وتقبل عنوان IP لترسل إليه بدل الجهاز المضيف.
والصورة التالية تبين المختبر:

التجربة العملية: مهمة كاملة في المحاكاة
المختبر كله في مجلد واحد فيه ملفان: Dockerfile يبني بيئة Python مع MAVSDK، وmission.py الذي يتصل بالطائرة ويعطيها المهمة. وملف Dockerfile كما يلي:
FROM python:3.13.16-slim
RUN pip install --no-cache-dir mavsdk==4.0.6
WORKDIR /lab
COPY mission.py basics.py ./
ENTRYPOINT ["python", "-u", "mission.py"]أما mission.py فهو قرابة 180 سطراً، وتجده كاملاً في البطاقة التالية:
mission.py كاملاً
"""A small PX4 mission with the native MAVSDK Python binding (pip package mavsdk 4.x).
python mission.py normal takeoff, four waypoints, return to launch
python mission.py gps the same mission, then the simulated GPS loses all satellites after waypoint 2
python mission.py fence a 60 m geofence: a mission outside it, then a goto outside it
PX4 SITL sends MAVLink to this container on UDP 14540.
"""
import asyncio
import sys
import time
from mavsdk.asyncio import Mavsdk, Configuration, ComponentType
from mavsdk.asyncio.plugins.action import ActionAsync
from mavsdk.asyncio.plugins.mission import MissionAsync, MissionItem, MissionPlan
from mavsdk.asyncio.plugins.telemetry import TelemetryAsync
from mavsdk.asyncio.plugins.offboard import OffboardAsync, VelocityNedYaw
from mavsdk.asyncio.plugins.param import ParamAsync
T0 = time.monotonic()
def log(msg):
print(f"[{time.monotonic() - T0:6.1f}s] {msg}", flush=True)
def waypoint(lat, lon, alt=20.0, speed=5.0):
item = MissionItem()
item.latitude_deg = lat
item.longitude_deg = lon
item.relative_altitude_m = alt
item.speed_m_s = speed
item.is_fly_through = True
item.gimbal_pitch_deg = float("nan")
item.gimbal_yaw_deg = float("nan")
item.camera_action = MissionItem.CameraAction.NONE
item.loiter_time_s = 0.0
item.camera_photo_interval_s = 1.0
item.acceptance_radius_m = 2.0
item.yaw_deg = float("nan")
item.camera_photo_distance_m = 0.0
item.vehicle_action = MissionItem.VehicleAction.NONE
return item
# about 1e-5 degrees of latitude = 1.1 m; a square of ~45 m around the default PX4 home (Zurich)
def square(home_lat, home_lon):
d = 0.0004
return [waypoint(home_lat + d, home_lon), waypoint(home_lat + d, home_lon + d * 1.5),
waypoint(home_lat, home_lon + d * 1.5), waypoint(home_lat - d * 0.5, home_lon + d * 0.5)]
async def watch(telemetry, mission):
async def modes():
last = None
async for m in telemetry.subscribe_flight_mode():
if m != last:
log(f"flight mode: {m.name}")
last = m
async def texts():
async for s in telemetry.subscribe_status_text():
log(f"PX4 says ({s.type.name}): {s.text}")
async def progress():
async for p in mission.subscribe_mission_progress():
log(f"mission progress: {p.current}/{p.total}")
async def gps():
last = None
async for g in telemetry.subscribe_gps_info():
state = (g.num_satellites, g.fix_type.name)
if state != last:
log(f"gps: {g.num_satellites} satellites, fix {g.fix_type.name}")
last = state
return [asyncio.create_task(c()) for c in (modes, texts, progress, gps)]
async def main(mode):
mavsdk = Mavsdk(Configuration.create_with_component_type(ComponentType.GROUND_STATION))
await mavsdk.add_any_connection("udpin://0.0.0.0:14540")
drone = await mavsdk.first_autopilot(30.0)
if drone is None:
raise SystemExit("no autopilot found on udp 14540")
log("autopilot found")
action, mission = ActionAsync(drone), MissionAsync(drone)
telemetry, param = TelemetryAsync(drone), ParamAsync(drone)
offboard = OffboardAsync(drone)
while True:
h = await telemetry.health()
if h.is_global_position_ok and h.is_home_position_ok:
break
await asyncio.sleep(1)
home = await telemetry.home()
log(f"home: {home.latitude_deg:.6f}, {home.longitude_deg:.6f}, {home.absolute_altitude_m:.1f} m AMSL")
tasks = await watch(telemetry, mission)
items = square(home.latitude_deg, home.longitude_deg)
if mode == "fence":
await param.set_param_float("GF_MAX_HOR_DIST", 60.0)
log(f"GF_MAX_HOR_DIST = {await param.get_param_float('GF_MAX_HOR_DIST')} m, "
f"GF_ACTION = {await param.get_param_int('GF_ACTION')}")
items = [waypoint(home.latitude_deg + 0.0003, home.longitude_deg),
waypoint(home.latitude_deg + 0.0011, home.longitude_deg)] # ~33 m then ~122 m north
if mode == "gps":
log(f"EKF2_NOAID_TOUT = {await param.get_param_int('EKF2_NOAID_TOUT')} us, "
f"COM_POS_FS_EPH = {await param.get_param_float('COM_POS_FS_EPH')} m")
await mission.set_return_to_launch_after_mission(True)
await mission.upload_mission(MissionPlan(items))
log(f"mission uploaded: {len(items)} waypoints, RTL after the last one")
await action.arm()
log("armed")
await mission.start_mission()
log("mission start requested")
if mode == "fence":
# PX4 checks the plan against the fence and refuses to start it; wait for the auto disarm,
# then take off by hand and fly towards a point outside the fence
await asyncio.sleep(15)
await action.arm()
await action.set_takeoff_altitude(20.0)
await action.takeoff()
log("armed again, takeoff to 20 m")
await asyncio.sleep(20)
log("goto 122 m north, outside the 60 m fence")
try:
await action.goto_location(home.latitude_deg + 0.0011, home.longitude_deg,
home.absolute_altitude_m + 20.0, float("nan"))
except Exception as e:
log(f"goto refused: {e}")
await asyncio.sleep(3)
# offboard velocity cannot be checked in advance: fly north at 5 m/s until the fence stops us
await offboard.set_velocity_ned(VelocityNedYaw(5.0, 0.0, 0.0, 0.0))
await offboard.start()
log("offboard: north at 5 m/s")
for _ in range(30):
pos = await telemetry.position()
log(f"distance north of home: {(pos.latitude_deg - home.latitude_deg) * 111320:5.1f} m, "
f"mode {(await telemetry.flight_mode()).name}")
await asyncio.sleep(1)
try:
await offboard.stop()
except Exception as e:
log(f"offboard stop: {e}")
await action.land()
if mode == "gps":
async for p in mission.subscribe_mission_progress():
if p.current >= 2:
break
await asyncio.sleep(3)
# the simulated GPS reports as many satellites as SIM_GPS_USED; 0 means no fix at all
await param.set_param_int("SIM_GPS_USED", 0)
log("SIM_GPS_USED = 0: the simulated GPS loses every satellite")
t_end = time.monotonic() + 240
was_in_air = False
while time.monotonic() < t_end:
in_air = await telemetry.in_air()
was_in_air |= in_air
if was_in_air and not await telemetry.armed():
break
await asyncio.sleep(1)
pos = await telemetry.position()
log(f"landed and disarmed at {pos.latitude_deg:.6f}, {pos.longitude_deg:.6f}, "
f"{pos.relative_altitude_m:.1f} m above home")
for t in tasks:
t.cancel()
if __name__ == "__main__":
asyncio.run(main(sys.argv[1] if len(sys.argv) > 1 else "normal"))
في الكود أعلاه لاحظ التالي:
add_any_connection("udpin://0.0.0.0:14540")يستمع على المنفذ الذي يرسل إليه PX4، ثمfirst_autopilot(30.0)ينتظر حتى 30 ثانية أول طيار آلي يظهر على الاتصال.- كل إضافة Plugin تنشئها بنفسك من الطائرة:
ActionAsyncللأوامر المباشرة، وMissionAsyncللمهام، وTelemetryAsyncللحالة، وParamAsyncللمعاملات، وOffboardAsyncللتحكم بالسرعة من الكود. - الدالة
watchتطبع كل تغير في الوضع Flight Mode وكل رسالة يرسلها PX4، وهذا ما سوف يشرح لنا كل ما يحدث عند الكسر. - المهمة مربع تقريبي حول نقطة البداية على ارتفاع 20 متراً، و
set_return_to_launch_after_mission(True)يطلب العودة إلى نقطة الإقلاع بعد آخر نقطة. ونقطة البداية الافتراضية في هذه المحاكاة قرب زيورخ، ولم نغيرها لأن المكان في المحاكاة لا يؤثر في التجربة.
تشغيل المهمة
الأوامر التالية تنشئ شبكة Docker خاصة، وتشغل PX4 مع Gazebo بلا شاشة، وتعطيه عنوان Container البرنامج ليرسل إليه MAVLink، ثم تبني البرنامج وتشغله:
docker network create --subnet 10.250.77.0/24 lab-drone-net
docker run -dit --name lab-drone-px4 --network lab-drone-net --ip 10.250.77.10 \
jonasvautherin/px4-gazebo-headless:1.17.0 10.250.77.20
docker logs lab-drone-px4 | grep "Startup script returned successfully"
docker build -t lab-drone-mavsdk:1 .
docker run --rm --name lab-drone-mavsdk --network lab-drone-net --ip 10.250.77.20 lab-drone-mavsdk:1 normalولاحظ أن الخيار -it مع -d ضروري، لأن PX4 في هذه الصورة يفتح سطر أوامره التفاعلي pxh>، وبدون Terminal يقرأ نهاية الإدخال باستمرار ويملأ السجل بعشرات الميجابايتات في ثوان، وقد حدث معنا ذلك في أول تشغيل. والمخرج سوف يكون كما يلي (حذفنا رسائل المكتبة الداخلية):
[ 1.3s] autopilot found
[ 2.3s] home: 47.397971, 8.546164, 0.2 m AMSL
[ 2.4s] gps: 10 satellites, fix FIX_3D
[ 2.8s] mission uploaded: 4 waypoints, RTL after the last one
[ 2.8s] armed
[ 2.8s] PX4 says (INFO): Armed by external command
[ 2.8s] mission start requested
[ 2.9s] PX4 says (INFO): Executing Mission
[ 2.9s] PX4 says (INFO): Climb to 20.0 meters above home
[ 3.0s] flight mode: MISSION
[ 3.1s] mission progress: 0/4
[ 5.3s] PX4 says (INFO): Takeoff detected
[ 47.7s] mission progress: 1/4
[ 64.8s] mission progress: 2/4
[ 82.0s] mission progress: 3/4
[ 97.3s] PX4 says (INFO): Mission finished, loitering
[ 97.4s] PX4 says (INFO): Returning to launch
[ 97.5s] PX4 says (INFO): RTL: start return at 31 m (30 m above destination)
[ 97.6s] flight mode: RETURN_TO_LAUNCH
[ 98.2s] mission progress: 4/4
[ 121.9s] PX4 says (INFO): RTL: land at destination
[ 164.7s] PX4 says (INFO): Landing detected
[ 167.9s] PX4 says (INFO): Disarmed by landing
[ 168.6s] flight mode: HOLD
[ 168.7s] landed and disarmed at 47.397971, 8.546165, -0.6 m above homeفي المخرج أعلاه لاحظ التالي:
- الطائرة صعدت أولاً إلى 20 متراً ثم مرت بالنقاط الأربع، ثم عادت وهبطت في نقطة الإقلاع نفسها تقريباً، فالفرق في خط الطول في الخانة السادسة فقط، أي أقل من متر.
- العودة بدأت على ارتفاع 30 متراً فوق نقطة الهبوط وليس على ارتفاع المهمة، والسبب أن المعامل
RTL_RETURN_ALTقيمته 30 متراً كما سوف ترى في جدول المعاملات، فالطيار الآلي يرتفع أولاً ليتجاوز العوائق ثم يعود. - بين الأمر
start_mission()وأول رسالة من PX4 أقل من عشر الثانية، لأن كل ذلك داخل جهاز واحد، أما مع راديو حقيقي فسوف ترى تأخيراً أكبر.
قبل الكسر: ما هي إعدادات الحماية فعلاً؟
صفحة مرجع المعاملات تذكر القيم الافتراضية العامة، ولكن كل طائرة Airframe قد تغيرها، فلا تفترض شيئاً واقرأ القيم من الطائرة نفسها. الأمر التالي يقرأها من المحاكاة:
docker exec lab-drone-px4 bash -c 'cd /root/px4/build; for p in NAV_DLL_ACT COM_DL_LOSS_T NAV_RCL_ACT \
COM_RC_IN_MODE GF_ACTION COM_FAIL_ACT_T EKF2_NOAID_TOUT RTL_RETURN_ALT MPC_TILTMAX_AIR; do
./bin/px4-param show $p | grep -E " $p "; done'x NAV_DLL_ACT [649,1115] : 2
x COM_DL_LOSS_T [260,476] : 10
x NAV_RCL_ACT [657,1123] : 2
x COM_RC_IN_MODE [296,520] : 1
x GF_ACTION [471,824] : 2
x COM_FAIL_ACT_T [261,477] : 5.0000
x EKF2_NOAID_TOUT [398,624] : 5000000
x RTL_RETURN_ALT [807,1323] : 30.0000
x MPC_TILTMAX_AIR [615,1081] : 45.0000في المخرج أعلاه لاحظ التالي:
NAV_DLL_ACT، وهو ما يفعله PX4 إذا انقطع الاتصال بمحطة التحكم بعدCOM_DL_LOSS_Tثوان، قيمته هنا 2 أي العودة Return، بينما القيمة الافتراضية في مرجع المعاملات 0 أي لا شيء، والسبب أن إعداد المحاكاة يغيره. وهذا بالضبط ما نقصده: طائرتك الحقيقية قد لا تعود إذا انقطع الاتصال إلا إذا ضبطت ذلك بنفسك.COM_RC_IN_MODEقيمته 1، أي أن التحكم اليدوي من MAVLink فقط، لأنه لا يوجد جهاز تحكم RC في المحاكاة.GF_ACTIONقيمته 2 أي الانتظار Hold عند الخروج من السياج، وCOM_FAIL_ACT_Tيعني أن PX4 ينتظر 5 ثوان في Hold قبل أغلب أفعال الحماية حتى يتدخل الطيار إن أراد، وEKF2_NOAID_TOUTهو 5 ثوان بالميكروثانية، وهي أقصى مدة يقدر فيها المقدر Estimator الموقع دون أي حساس يصحح السرعة أو الموقع.
الكسر الأول: سياج جغرافي Geofence
السياج الجغرافي حد افتراضي حول الطائرة، وأبسط أشكاله بحسب صفحة الحماية أسطوانة حول نقطة الإقلاع، إذا خرجت منها الطائرة نفذ PX4 الفعل المحدد في GF_ACTION. والخيار fence في البرنامج يضبط نصف قطرها GF_MAX_HOR_DIST على 60 متراً، ثم يجرب ثلاث طرق للخروج منه: مهمة نقطتها الثانية على بعد قرابة 122 متراً، ثم أمر الذهاب المباشر goto_location إلى النقطة نفسها، ثم التحكم بالسرعة من الكود Offboard نحو الشمال بسرعة 5 أمتار في الثانية:
docker rm -f lab-drone-px4
docker run -dit --name lab-drone-px4 --network lab-drone-net --ip 10.250.77.10 \
jonasvautherin/px4-gazebo-headless:1.17.0 10.250.77.20
docker run --rm --name lab-drone-mavsdk --network lab-drone-net --ip 10.250.77.20 lab-drone-mavsdk:1 fenceوالمخرج سوف يكون كما يلي (مختصراً على الأسطر المهمة):
[ 2.8s] GF_MAX_HOR_DIST = 60.0 m, GF_ACTION = 2
[ 3.0s] mission uploaded: 2 waypoints, RTL after the last one
[ 3.0s] PX4 says (CRITICAL): Geofence violation for waypoint 3
[ 3.1s] armed
[ 3.1s] mission start requested
[ 3.2s] PX4 says (CRITICAL): Switching to Mission is currently not available
[ 3.7s] flight mode: HOLD
[ 18.1s] armed again, takeoff to 20 m
[ 18.9s] PX4 says (INFO): Takeoff detected
[ 38.1s] goto 122 m north, outside the 60 m fence
[ 38.1s] PX4 says (CRITICAL): Reposition is outside geofence
[ 38.4s] flight mode: HOLD
[ 41.1s] offboard: north at 5 m/s
[ 42.4s] flight mode: OFFBOARD
[ 45.2s] distance north of home: 11.0 m, mode OFFBOARD
[ 50.2s] distance north of home: 28.9 m, mode OFFBOARD
[ 55.2s] distance north of home: 46.0 m, mode OFFBOARD
[ 59.3s] distance north of home: 59.1 m, mode OFFBOARD
[ 59.8s] PX4 says (CRITICAL): Geofence: exceeding maximum distance to Home
[ 59.8s] PX4 says (CRITICAL): Failsafe activated
[ 60.3s] distance north of home: 62.8 m, mode OFFBOARD
[ 60.4s] flight mode: HOLD
[ 61.3s] distance north of home: 66.1 m, mode HOLD
[ 61.9s] PX4 says (CRITICAL): Failsafe activated
[ 61.9s] PX4 says (INFO): RTL: start return at 31 m (30 m above destination)
[ 63.0s] flight mode: RETURN_TO_LAUNCH
[ 71.3s] PX4 says (INFO): Landing at current position
[ 72.4s] flight mode: LANDفي المخرج أعلاه لاحظ التالي:
- المهمة رفضت قبل الإقلاع: PX4 فحص المهمة مقابل السياج لحظة رفعها وكتب أن النقطة 3 تخرج منه، ثم رفض التحويل إلى وضع المهمة، وبقيت الطائرة على الأرض. ولاحظ أن الرسالة تذكر النقطة 3 ومهمتنا فيها نقطتان فقط، فترقيم PX4 لعناصر المهمة يختلف عن ترقيمك، لذلك لا تعتمد على الرقم وحده. ولاحظ أن رفع المهمة نفسه لم يعط خطأ في البرنامج، فالرفض جاء عند البدء، لذلك يجب أن يراقب برنامجك الوضع والرسائل ولا يكتفي بنجاح الدالة.
- أمر الذهاب رفض أيضاً: «Reposition is outside geofence»، وبقيت الطائرة في Hold.
- Offboard لا يفحص مسبقاً: لأن الكود يعطي سرعة وليس مكاناً، فطارت الطائرة شمالاً حتى تجاوزت 60 متراً، فأعلن PX4 تجاوز السياج وشغل الحماية ودخل Hold بحسب
GF_ACTION، ولأن الطائرة بقيت خارج السياج شغل الحماية مرة ثانية وبدأ العودة. وتوقفت الطائرة على بعد قرابة 67 متراً، أي أنها تجاوزت الحد بقرابة 7 أمتار بسبب سرعتها لحظة الاكتشاف، فاجعل السياج أصغر من الحد الذي لا تريد أن تتجاوزه فعلاً. - الهبوط في مكانه في آخر المخرج جاء من البرنامج نفسه وليس من PX4، فالبرنامج يرسل
land()بعد 30 ثانية من بدء Offboard.
الكسر الثاني: فقدان GPS أثناء المهمة
في المحاكاة يأتي GPS من حساس افتراضي يعلن عدد الأقمار المحدد في المعامل SIM_GPS_USED، فإذا جعلناه صفراً فقدت الطائرة الإشارة كما لو دخلت تحت جسر أو تعطل الهوائي. والخيار gps يفعل ذلك بعد ثلاث ثوان من الوصول إلى النقطة الثانية:
docker rm -f lab-drone-px4
docker run -dit --name lab-drone-px4 --network lab-drone-net --ip 10.250.77.10 \
jonasvautherin/px4-gazebo-headless:1.17.0 10.250.77.20
docker run --rm --name lab-drone-mavsdk --network lab-drone-net --ip 10.250.77.20 lab-drone-mavsdk:1 gps
docker logs -t lab-drone-px4 | grep -E "commander|failsafe|navigator"والمخرج سوف يكون كما يلي (مختصراً):
[ 2.0s] EKF2_NOAID_TOUT = 5000000 us, COM_POS_FS_EPH = 5.0 m
[ 2.0s] gps: 10 satellites, fix FIX_3D
[ 3.4s] flight mode: MISSION
[ 51.8s] mission progress: 1/4
[ 72.3s] mission progress: 2/4
[ 75.3s] SIM_GPS_USED = 0: the simulated GPS loses every satellite
[ 75.4s] gps: 0 satellites, fix NO_GPS
[ 84.6s] flight mode: LAND
[ 84.6s] PX4 says (CRITICAL): Failsafe activated
[ 116.5s] PX4 says (INFO): Landing detected
[ 119.6s] PX4 says (INFO): Disarmed by landing
[ 120.7s] landed and disarmed at 47.397820, 8.545963, 4.0 m above home
07:06:42.264731860Z INFO [navigator] Executing Mission
07:08:04.567783651Z WARN [failsafe] Failsafe activated
07:08:36.475565911Z INFO [commander] Landing detected
07:08:39.602944614Z INFO [commander] Disarmed by landingفي المخرج أعلاه لاحظ التالي:
- بعد فقدان الأقمار لم يتغير شيء لبضع ثوان، لأن المقدر يستمر بالحساب من IMU وحده حتى تنتهي مهلة
EKF2_NOAID_TOUT، ثم أعلن PX4 الحماية. والفرق في المخرج 9 ثوان وليس 5، والسبب أن المحاكاة على هذا الجهاز أبطأ من الزمن الحقيقي. - الفعل كان الهبوط Land وليس العودة، والسبب منطقي: العودة تحتاج أن تعرف الطائرة أين هي وأين نقطة الإقلاع، وبدون موقع لا يوجد طريق للعودة، وهذا ما تصفه صفحة الحماية في قسم فقدان الموقع. والنتيجة العملية أن الطائرة هبطت في مكان لا تعرفه أنت، قد يكون سطحاً أو بحيرة.
- آخر موقع طبعه البرنامج «4.0 m above home» ليس صحيحاً، فالطائرة على الأرض، وإنما هو آخر ما قدره المقدر دون GPS، أي أن الموقع نفسه لم يعد موثوقاً بعد الفقدان.
الكسر الثالث: انقطاع الاتصال بمحطة التحكم
هنا نشغل المهمة العادية في الخلفية، وعندما تصل إلى النقطة الثانية نوقف Container البرنامج كله، كما لو انقطع رابط القياس عن بعد أو أغلق الحاسوب:
docker run -d --name lab-drone-mavsdk --network lab-drone-net --ip 10.250.77.20 lab-drone-mavsdk:1 normal
until docker logs lab-drone-mavsdk 2>&1 | grep -q "mission progress: 2/4"; do sleep 0.5; done
date -u +%H:%M:%S.%N; docker stop -t 1 lab-drone-mavsdk
docker logs -t lab-drone-px4 | grep -E "commander|failsafe|navigator"07:10:44.884462600
...
07:09:33.170565348Z INFO [navigator] Executing Mission
07:09:35.928590114Z INFO [commander] Takeoff detected
07:11:11.764949398Z INFO [commander] Connection to ground station lost
07:11:11.803569307Z WARN [failsafe] Failsafe activated: entering Hold for 5 seconds
07:11:19.456706032Z WARN [failsafe] Failsafe activated
07:11:19.522844822Z INFO [navigator] RTL: start return at 30 m (30 m above destination)
07:11:45.880260712Z INFO [navigator] RTL: land at destination
07:12:30.610270287Z INFO [commander] Landing detected
07:12:34.697824261Z INFO [commander] Disarmed by landingفي المخرج أعلاه لاحظ التالي:
- أعلن PX4 فقدان الاتصال بعد مدة
COM_DL_LOSS_T، ثم دخل Hold لمدةCOM_FAIL_ACT_T، ثم نفذNAV_DLL_ACTوهو العودة، فعادت الطائرة وهبطت في نقطة الإقلاع. والأوقات بالساعة الحقيقية أطول من 10 ثوان ثم 5 ثوان للسبب نفسه: المحاكاة أبطأ من الزمن الحقيقي على هذا الجهاز. - وهذا كله لأن
NAV_DLL_ACTقيمته 2 في هذه المحاكاة، أما بالقيمة الافتراضية 0 فكانت الطائرة سوف تكمل مهمتها دون أن تهتم بانقطاع المحطة.
تنظيف المختبر
docker rm -f lab-drone-px4 lab-drone-mavsdk
docker network rm lab-drone-net
docker image rm lab-drone-mavsdk:1الأوامر تحذف ما أنشأناه بالاسم فقط، ويبقى Docker Image الخاص بالمحاكاة إذا أردت أن تعيد التجربة، وحجمه قرابة 3.3 جيجابايت مضغوطاً.
من المحاكاة إلى طائرة حقيقية
- الاتصال: في الطائرة الحقيقية يتغير عنوان الاتصال فقط، فبدل
udpin://0.0.0.0:14540تستخدم منفذ الراديو التسلسلي مثلserial:///dev/ttyUSB0:57600أو عنوان الحاسوب المرافق، ويبقى باقي الكود كما هو. - اختبر الحماية قبل المهمة: اضبط Geofence وأفعال فقدان الإشارة من QGroundControl، وجربها على ارتفاع منخفض في مكان مفتوح، فالقيم تختلف من طائرة إلى أخرى، ورأينا أن القيمة الافتراضية في مرجع PX4 لا تفعل شيئاً عند فقدان رابط المحطة.
- جهاز التحكم في يد طيار: برنامجك لا يغني عن طيار يستطيع أن يأخذ التحكم في أي لحظة، وهذا ليس شرطاً فنياً فقط وإنما شرط نظامي كما في مقال الأنظمة.
- السجلات: PX4 يكتب سجل كل طيران بصيغة ULog على بطاقة الذاكرة، وهو أول ما تفتحه بعد أي سلوك غريب.
أخطاء شائعة
- استخدام أمثلة MAVSDK-Python القديمة مع
mavsdk4: الواجهة تغيرت، فإماmavsdk-grpcللكود القديم أو الواجهة الجديدة. - البدء على طائرة حقيقية: كل كود جديد يمر على SITL أولاً.
- افتراض أن الطائرة ستعود دائماً: العودة تحتاج موقعاً، وبدون GPS لا يستطيع الطيار الآلي أن يعود، كما رأينا.
- إهمال رسائل الطيار الآلي: الرسائل Status Text هي التي تقول لك لماذا رفض أمراً أو غير الوضع، فاطبعها في برنامجك دائماً.
- رفع مهمة دون التحقق من السياج: PX4 يرفض المهمة التي تخرج من السياج، وبرنامجك يجب أن يتعامل مع هذا الرفض.
تدريب عملي
- غير
GF_ACTIONإلى 3 (Return) وأعد تجربة السياج، ولاحظ الفرق في الرسائل والوضع. - غير
NAV_DLL_ACTإلى 0 كما في مرجع المعاملات، ثم أعد الكسر الثالث، ولاحظ أن PX4 يعلن فقدان الاتصال ويكمل المهمة.
الخلاصة
- الطيار الآلي PX4 برخصة BSD 3-Clause تحت Dronecode، وArduPilot برخصة GPLv3، وكلاهما يتحدث MAVLink.
- MAVLink لغة الرسائل بين الطائرة والمحطة والبرامج، وMAVSDK يتولى رسائلها نيابة عنك.
- حزمة
mavsdkلـ Python أصبحت منذ الإصدار 4 مكتبة جديدة بواجهة مختلفة، والقديمة باسمmavsdk-grpc. - SITL يشغل كود PX4 الحقيقي في محاكاة، فتختبر المهمة والحماية دون أن تخاطر بطائرة.
- القيم الافتراضية للحماية تستحق المراجعة قبل أي طيران حقيقي، وفقدان الموقع أخطر من فقدان الاتصال لأن العودة نفسها تحتاج موقعاً.
دليلالطائرات المسيرة للمبتدئين: كيف تطير ومم تتكونكيف تقف الطائرة المسيرة في الهواء وتميل وتدور بتغيير سرعة محركاتها فقط، ومم تتكون، وأي نوع يناسب أي عمل.
دليلرسم الخرائط بالطائرات المسيرة مع OpenDroneMapكيف تتحول عشرات الصور الجوية إلى خريطة مصححة ونموذج ارتفاعات على سيرفرك، وما الذي يحدد دقتها قبل أن تقلع الطائرة.
دليلأنظمة الطائرات المسيرة في السعودية قبل أن تطيرما الذي تطلبه هيئة الطيران المدني قبل أن تطير بطائرة مسيرة في السعودية، من التسجيل والشهادة إلى المناطق الممنوعة والتصوير.سجل التحديثات
- أكتوبر 2026: كتابة المقال، والتجربة على PX4 v1.17.0 في محاكاة SITL مع Gazebo 8.11.0 (jonasvautherin/px4-gazebo-headless:1.17.0) ومكتبة mavsdk 4.0.6 على Python 3.13.16 في Docker Engine 29.8.0، والمعلومات من توثيق PX4 وArduPilot وMAVLink وMAVSDK ومستودعاتها كما قرأناها في 11 أكتوبر 2026.
- أكتوبر 2026: إضافة روابط إلى مقالي المنصات المفتوحة وDJI وتطوير برمجيات الطائرة مع ROS 2.