> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# ROS 2 وPX4: كيف تطور برمجيات الطائرة المسيرة
- URL: https://arabroot.io/articles/ros-2-و-px4-كيف-تطور-برمجيات-الطائرة-المسيرة/
- Published: 2026-10-12T19:45:00.000Z
- Updated: 2026-10-12T12:15:43.000Z
- Description: كيف تطور كود الطائرة المسيرة من المحاكاة إلى الحقل، وتربط ROS 2 بالطيار الآلي PX4، وتقرأ سجل الطيران عندما لا يطير كما توقعت.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, مجالات متخصصة, الاستضافة الذاتية

في [مقال برمجة الطائرات المسيرة](https://arabroot.io/articles/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%B7%D8%A7%D8%A6%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B3%D9%8A%D8%B1%D8%A9-%D9%85%D8%B9-px4-%D9%88-mavsdk/) كتبنا برنامج Python صغيراً بمكتبة MAVSDK يعطي الطائرة مهمة ويراقبها، وشغلناه على PX4 في محاكاة SITL. وهذا يكفي لمهمة تمر بنقاط، ولكن المشاريع الحقيقية تحتاج أكثر من ذلك: كود يعمل على حاسوب مرافق Companion Computer فوق الطائرة نفسها، يقرأ كاميرا أو LiDAR ويقرر أين تذهب الطائرة عشرات المرات في الثانية، ثم طريقة لتختبر هذا الكود بأمان قبل أن يطير، وطريقة لتعرف ماذا حدث إذا لم يطر كما توقعت. وفي هذا المقال سوف نبني هذه السلسلة كلها على PX4 وROS 2، ثم ننظر كيف يبدو التطوير نفسه في منصة DJI المغلقة.

وسوف نناقش في هذا المقال ما يلي:

- مسار التطوير من المحاكاة SITL إلى HITL إلى الحقل، وما الذي يكشفه كل منها.
- الحاسوب المرافق، ولماذا ROS 2، وكيف يتصل ب PX4 عبر uXRCE-DDS.
- وضع Offboard الذي يتحكم فيه كودك في الطائرة، وما يشترطه PX4 حتى يقبله.
- تجربة عملية: عقدة ROS 2 Node تطير مربعاً في SITL، ثم نوقفها فجأة كأن الحاسوب المرافق تعطل.
- سجل الطيران ULog: كيف تقرؤه بأدوات pyulog وبكود Python صغير.
- اختبار المحاكاة في CI، وشكل التطوير بمكتبة DJI Mobile SDK دون أن نفتح حساباً.

## من المحاكاة إلى الحقل

الحل البسيط أن تكتب الكود وتحمله على الطائرة وتجربه في الحديقة، والمشكلة أن كل خطأ في هذه الطريقة ثمنه طائرة، وأحياناً ما هو أغلى من الطائرة. لذلك يمر كل تعديل بثلاث مراحل، والصورة التالية تبين ما هو حقيقي وما هو محاكاة في كل منها:

![ثلاث بطاقات من اليمين إلى اليسار بأسهم بينها: 1 SITL حاسوب وطائرة وPX4 + Gazebo + your code، كود الطيار الآلي الحقيقي على جهازك وآلاف التشغيلات بلا خطر ويعمل في CI، والحساسات والفيزياء محاكاة ولا يكشف أخطاء العتاد والتوقيت. 2 HITL حاسوب متصل بمتحكم real flight controller, USB، الكود يعمل على المتحكم نفسه ويكشف بطء المعالج والتوقيت والإعداد، والحساسات ما زالت محاكاة وانزع المراوح قبل التشغيل. 3 Field test طائرة فوق أرض خضراء real drone, low and open area، كل شيء حقيقي والسجل ULog يحكي ما حدث، والخطأ هنا طائرة تسقط وطيار بجهاز تحكم يستطيع أن يتدخل](https://arabroot.io/content/images/2026/10/drone-development-01-pipeline.webp)

كل مرحلة تجعل شيئاً إضافياً حقيقياً، والحقل آخرها

- **SITL:** كود PX4 الحقيقي يعمل على جهازك مع محاكي فيزياء، وقد شرحناه في [مقال البرمجة](https://arabroot.io/articles/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%B7%D8%A7%D8%A6%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B3%D9%8A%D8%B1%D8%A9-%D9%85%D8%B9-px4-%D9%88-mavsdk/#sitl). وهنا تكتشف أخطاء المنطق: أمر في غير وقته، أو حالة لم تتوقعها، أو رد فعل الحماية.
- **HITL:** واختصارها Hardware In The Loop، ويقول [توثيق PX4](https://docs.px4.io/v1.17/en/simulation/hitl.html?ref=arabroot.io) إن البرنامج العادي ل PX4 يعمل فيها على المتحكم الحقيقي بوضع HITL، بينما تأتي قراءات الحساسات من محاكي على الحاسوب، وإن المحاكيين المدعومين لها في الإصدار 1.17 هما Gazebo Classic للطائرات متعددة المراوح وVTOL وjMAVSim للرباعية فقط، وإن هذه الميزة يدعمها المجتمع ويصونها. وفائدتها أنها تكشف ما لا يكشفه SITL: معالج المتحكم الأبطأ، وإعداد المنافذ الحقيقي، والتوقيت.
- **الحقل:** طيران حقيقي على ارتفاع منخفض في مكان مفتوح، وطيار يمسك جهاز التحكم ويستطيع أن يأخذ التحكم في أي لحظة، ثم قراءة السجل بعد كل طيران.

## الحاسوب المرافق وإطار ROS 2 للروبوتات

الحاسوب المرافق Companion Computer حاسوب صغير يركب على الطائرة بجانب المتحكم، مثل Raspberry Pi أو NVIDIA Jetson، ويعمل عليه Linux وكودك، ويتصل بالمتحكم بكابل تسلسلي أو Ethernet. والسبب أنك لا تضع منطق الرؤية والتخطيط داخل الطيار الآلي نفسه، فالمتحكم معالج صغير مهمته الأولى أن يبقي الطائرة في الهواء، وكل تعقيد إضافي فيه خطر على هذه المهمة. لذلك يبقى الطيار الآلي مسؤولاً عن الاستقرار والحماية، ويرسل له الحاسوب المرافق «إلى أين» فقط.

وأما ROS 2 فهو إطار Framework مفتوح المصدر لبرمجة الروبوتات، والكود فيه عقد Nodes منفصلة تتبادل الرسائل على مواضيع Topics، فعقدة تقرأ الكاميرا وأخرى تكتشف العوائق وثالثة تقرر المسار، وكلها تتحدث بطريقة واحدة. ويوصي [دليل ROS 2 في توثيق PX4](https://docs.px4.io/v1.17/en/ros2/user%5Fguide.html?ref=arabroot.io) بالإصدار Humble على Ubuntu 22.04.

### كيف تصل رسائل ROS 2 إلى الطيار الآلي

في مقال البرمجة تحدث برنامجنا مع PX4 بلغة MAVLink، أما ROS 2 فيتحدث مع PX4 بطريقة أخرى اسمها uXRCE-DDS، ويقول [توثيقها](https://docs.px4.io/v1.17/en/middleware/uxrce%5Fdds.html?ref=arabroot.io) إنها تجعل رسائل PX4 الداخلية، واسمها uORB، تظهر كمواضيع ROS 2 مباشرة، وإنها من جزأين: عميل Client يعمل داخل PX4، ووكيل Agent يعمل على الحاسوب المرافق، وبينهما رابط تسلسلي أو UDP، والمنفذ الافتراضي في المحاكاة 8888\. والصورة التالية تبين ذلك:

![على اليسار بطاقة Companion computer فيها مربع ROS 2 node: offboard_square يكتب /fmu/in/trajectory_setpoint، وتحته MicroXRCEAgent udp4 -p 8888 الذي يترجم بين DDS وعميل PX4 الصغير، وبينهما سهمان DDS ورقم 1. في الوسط سهمان serial TELEM2 or UDP 8888 ورقم 2. وعلى اليمين بطاقة Flight Controller (PX4) فيها uxrce_dds_client يعمل داخل PX4 ويقرأ dds_topics.yaml وفوقه uORB topics مثل trajectory_setpoint, vehicle_status ورقم 3](https://arabroot.io/content/images/2026/10/drone-development-02-ros2.webp)

عقدتك تكتب على مواضيع ROS 2، والوكيل والعميل ينقلانها إلى رسائل PX4 الداخلية

وأهم ما في هذا الترتيب لك كمطور:

- المواضيع التي يرسلها PX4 تبدأ بـ `/fmu/out/` والتي يستقبلها تبدأ بـ `/fmu/in/`، والملف `dds_topics.yaml` في كود PX4 يحدد أيها يظهر.
- تعريفات الرسائل في حزمة `px4_msgs`، ويجب أن يطابق فرعها إصدار PX4 الذي تعمل عليه، ولهذا نبني في التجربة الفرع `release/1.17`. ويذكر دليل ROS 2 في PX4 أن الإصدار 1.16 أضاف ترقيم إصدارات الرسائل Message Versioning، وأن عقدة الترجمة Message Translation Node تلزمك إذا اختلفت إصدارات الرسائل بين كودك والطيار الآلي.
- هذه الطريقة حلت في PX4 v1.14 محل الجسر القديم Fast-RTPS Bridge الذي كان يحتاج نسخة خاصة من البرنامج، فإذا وجدت مثالاً يذكر `microRTPS` فهو قديم.

### وضع Offboard الذي يقود فيه كودك الطائرة

في وضع Offboard يتبع PX4 الأهداف Setpoints التي يرسلها كودك: موقعاً أو سرعة أو ميلاً، ويبقى هو مسؤولاً عن تثبيت الطائرة والحماية. ولكن له شرطان تذكرهما [صفحة الوضع](https://docs.px4.io/v1.17/en/flight%5Fmodes/offboard.html?ref=arabroot.io): أن يصله دليل حياة Proof of Life بشكل مستمر بمعدل أعلى من 2 هرتز، أي رسالة `OffboardControlMode` أو أهداف متتابعة، وإلا خرج من الوضع، وأن يبدأ هذا البث قبل أن تطلب التحويل إلى Offboard أو التشغيل فيه. وإذا انقطع البث ينتظر PX4 المدة `COM_OF_LOSS_T` ثم ينفذ ما في المعامل `COM_OBL_RC_ACT`، وقيمه تبدأ من وضع تثبيت الموقع Position إلى العودة Return والهبوط Land.

وقد يتساءل البعض: لماذا هذا الشرط؟ والإجابة أن الحاسوب المرافق قد يتجمد أو يعيد التشغيل أو ينقطع كابله، والطائرة في الهواء لا تستطيع أن تنتظر، فإذا لم يسمع الطيار الآلي من كودك أخذ القرار بنفسه، وهذا ما سوف نكسره في التجربة.

## التجربة العملية: عقدة ROS 2 تطير مربعاً

المختبر ثلاثة أجزاء على شبكة Docker واحدة: PX4 v1.17.0 في SITL كما في مقال البرمجة، وContainer فيه ROS 2 Humble والوكيل وتعريفات الرسائل وعقدتنا، ثم أدوات قراءة السجل. والوكيل والعقدة يعملان في شبكة Container ال PX4 نفسها، والسبب أن العميل داخل PX4 في المحاكاة يتصل بالعنوان 127.0.0.1 على المنفذ 8888 كما يظهر في سجله.

وملف Dockerfile يبني الوكيل من مصدره بالإصدار v2.4.3 الذي يذكره دليل PX4، ثم يبني `px4_msgs` من الفرع المطابق لإصدار PX4، ثم يثبت pyulog:

```dockerfile
# ROS 2 Humble + Micro XRCE-DDS Agent + px4_msgs (release/1.17) + our offboard node and log tools
FROM ros:humble-ros-base-jammy
SHELL ["/bin/bash", "-c"]
RUN apt-get update && apt-get install -y --no-install-recommends git cmake g++ python3-pip python3-colcon-common-extensions \
    && rm -rf /var/lib/apt/lists/*

# the agent PX4 talks to (version from the PX4 ROS 2 user guide)
RUN git clone --depth 1 -b v2.4.3 https://github.com/eProsima/Micro-XRCE-DDS-Agent.git /opt/agent \
    && cmake -S /opt/agent -B /opt/agent/build && cmake --build /opt/agent/build -j6 \
    && cmake --install /opt/agent/build && ldconfig /usr/local/lib/

# message definitions that match the PX4 release we fly
WORKDIR /ws
RUN git clone --depth 1 -b release/1.17 https://github.com/PX4/px4_msgs.git src/px4_msgs \
    && source /opt/ros/humble/setup.bash && colcon build --packages-select px4_msgs --parallel-workers 6

RUN pip3 install --no-cache-dir pyulog==1.2.4
COPY offboard_square.py analyze_log.py /lab/
COPY entry.sh /entry.sh
ENTRYPOINT ["/entry.sh"]
```

وبناء هذه Docker Image أخذ على جهازنا أكثر من ساعة، أغلبها في بناء مكتبة Fast DDS التي يبنيها الوكيل معه، فإذا كنت ستعيد التجربة كثيراً فاحتفظ بها. أما ملف `entry.sh` فيحمل بيئة ROS 2 وبيئة الرسائل قبل أي أمر:

```bash
#!/bin/bash
source /opt/ros/humble/setup.bash
source /ws/install/setup.bash
exec "$@"
```

والعقدة `offboard_square.py` قرابة 120 سطراً، تجدها كاملة في البطاقة التالية:

`offboard_square.py` كاملاً 

```python
"""ROS 2 offboard node for PX4 (uXRCE-DDS): arm, switch to Offboard, fly a 10 m square at 5 m, land.

  python3 offboard_square.py            normal run
  python3 offboard_square.py --crash 25 stop every publisher after 25 s in Offboard (a companion computer that hangs)

Topic names come from the PX4 release (px4_msgs release/1.17); versioned messages carry a _vN suffix.
"""
import sys
import time

import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy
from px4_msgs.msg import OffboardControlMode, TrajectorySetpoint, VehicleCommand, VehicleLocalPosition, VehicleStatus

NAV = {0: "MANUAL", 2: "POSCTL", 3: "AUTO_MISSION", 4: "AUTO_LOITER", 5: "AUTO_RTL", 14: "OFFBOARD",
       17: "AUTO_TAKEOFF", 18: "AUTO_LAND"}
SQUARE = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)]   # north, east in metres
ALT = -5.0                                                                  # NED: up is negative

class OffboardSquare(Node):
    def __init__(self, crash_after):
        super().__init__("offboard_square")
        qos = QoSProfile(reliability=ReliabilityPolicy.BEST_EFFORT, durability=DurabilityPolicy.TRANSIENT_LOCAL,
                         history=HistoryPolicy.KEEP_LAST, depth=1)
        self.mode_pub = self.create_publisher(OffboardControlMode, "/fmu/in/offboard_control_mode", qos)
        self.sp_pub = self.create_publisher(TrajectorySetpoint, "/fmu/in/trajectory_setpoint", qos)
        self.cmd_pub = self.create_publisher(VehicleCommand, "/fmu/in/vehicle_command", qos)
        self.create_subscription(VehicleLocalPosition, "/fmu/out/vehicle_local_position_v1", self.on_pos, qos)
        self.create_subscription(VehicleStatus, "/fmu/out/vehicle_status_v1", self.on_status, qos)
        self.pos, self.status, self.nav = None, None, None
        self.ticks, self.wp, self.offboard_since, self.crash_after = 0, 0, None, crash_after
        self.t0 = time.monotonic()
        self.timer = self.create_timer(0.1, self.tick)          # 10 Hz: PX4 wants the heartbeat above 2 Hz

    def log(self, msg):
        print(f"[{time.monotonic() - self.t0:6.1f}s] {msg}", flush=True)

    def now_us(self):
        return int(self.get_clock().now().nanoseconds / 1000)

    def on_pos(self, m):
        self.pos = m

    def on_status(self, m):
        if self.nav != m.nav_state:
            self.log(f"nav_state {NAV.get(m.nav_state, m.nav_state)}  armed={m.arming_state == 2}  failsafe={m.failsafe}")
            self.nav = m.nav_state
        self.status = m

    def command(self, cmd, p1=0.0, p2=0.0):
        m = VehicleCommand()
        m.command, m.param1, m.param2 = cmd, float(p1), float(p2)
        m.target_system, m.target_component, m.source_system, m.source_component = 1, 1, 1, 1
        m.from_external, m.timestamp = True, self.now_us()
        self.cmd_pub.publish(m)

    def tick(self):
        if self.crash_after is not None and self.offboard_since and time.monotonic() - self.offboard_since > self.crash_after:
            if self.timer is not None:
                self.log("simulated hang: no more offboard_control_mode or setpoints")
                self.timer.cancel()
                self.timer = None
                self.create_timer(1.0, self.watch_only)
            return
        hb = OffboardControlMode()
        hb.position, hb.timestamp = True, self.now_us()
        self.mode_pub.publish(hb)

        n, e = SQUARE[self.wp]
        sp = TrajectorySetpoint()
        sp.position = [n, e, ALT]
        sp.velocity = [float("nan")] * 3
        sp.acceleration = [float("nan")] * 3
        sp.yaw, sp.timestamp = 0.0, self.now_us()
        self.sp_pub.publish(sp)

        self.ticks += 1
        if self.ticks == 10:                                     # 1 s of setpoints first, or PX4 refuses Offboard
            self.command(VehicleCommand.VEHICLE_CMD_DO_SET_MODE, 1, 6)   # custom mode, PX4 main mode 6 = Offboard
            self.command(VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM, 1)
            self.log("requested Offboard + arm")
        if self.nav == 14 and self.offboard_since is None:
            self.offboard_since = time.monotonic()
        if self.pos is None or self.nav != 14:
            return
        err = ((self.pos.x - n) ** 2 + (self.pos.y - e) ** 2 + (self.pos.z - ALT) ** 2) ** 0.5
        if self.ticks % 20 == 0:
            self.log(f"wp {self.wp} target ({n:4.1f},{e:4.1f},{-ALT:3.1f})  at ({self.pos.x:5.1f},{self.pos.y:5.1f},{-self.pos.z:4.1f})  err {err:4.1f} m")
        if err < 0.5:
            if self.wp == len(SQUARE) - 1:
                self.log("square done, landing")
                self.command(VehicleCommand.VEHICLE_CMD_NAV_LAND)
                self.timer.cancel()
                self.create_timer(1.0, self.watch_only)
            else:
                self.wp += 1
                self.log(f"reached, next waypoint {self.wp}")

    def watch_only(self):
        if self.status is not None and self.status.arming_state != 2 and self.pos is not None and self.pos.z > -0.3:
            self.log(f"disarmed at ({self.pos.x:5.1f},{self.pos.y:5.1f}), done")
            raise SystemExit(0)

def main():
    crash = float(sys.argv[sys.argv.index("--crash") + 1]) if "--crash" in sys.argv else None
    rclpy.init()
    node = OffboardSquare(crash)
    try:
        rclpy.spin(node)
    except SystemExit:
        pass
    node.destroy_node()
    rclpy.try_shutdown()

if __name__ == "__main__":
    main()
```

في الكود أعلاه لاحظ التالي:

- ملف QoS بموثوقية `BEST_EFFORT` لكل الاشتراكات والنشر، لأن PX4 ينشر مواضيعه بهذا الإعداد، والاشتراك بإعداد Reliable لا يستقبل منها شيئاً.
- المؤقت يعمل 10 مرات في الثانية، وفي كل مرة ينشر `OffboardControlMode` مع `position=True` ثم الهدف `TrajectorySetpoint`، وهذا هو دليل الحياة الذي يشترطه PX4.
- طلب وضع Offboard والتشغيل لا يرسل إلا بعد 10 دورات، أي بعد ثانية من البث، والسبب شرط PX4 أن يصله البث قبل التحويل.
- الأوامر من نوع `VehicleCommand`، والرقم 6 في `DO_SET_MODE` هو الوضع الرئيسي Offboard في PX4.
- المحاور NED: الشمال ثم الشرق ثم الأسفل، لذلك الارتفاع 5 أمتار هو `-5.0`.
- الخيار `--crash` يوقف المؤقت كله بعد عدد من الثواني في Offboard، فلا يصل PX4 أي دليل حياة، كأن الحاسوب المرافق تجمد، ويبقى الاشتراك فقط لنرى ماذا يحدث.

### تشغيل العقدة

الأوامر التالية تشغل PX4، ثم تشغل الوكيل في شبكة Container ال PX4، ثم تعرض المواضيع التي ظهرت في ROS 2:

```bash
docker build -t lab-drone2-ros:1 .
docker network create lab-drone2-net
docker run -dit --name lab-drone2-px4 --network lab-drone2-net jonasvautherin/px4-gazebo-headless:1.17.0
docker exec lab-drone2-px4 bash -c 'cd /root/px4/build; ./bin/px4-param set NAV_DLL_ACT 0'
docker run -d --name lab-drone2-ros --network container:lab-drone2-px4 lab-drone2-ros:1 MicroXRCEAgent udp4 -p 8888
docker exec lab-drone2-ros /entry.sh bash -c 'ros2 topic list | sort'
```

ولاحظ أننا ألغينا فعل فقدان محطة التحكم `NAV_DLL_ACT` للسبب نفسه الذي شرحناه في [مقال التجميع](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%B7%D8%A7%D8%A6%D8%B1%D8%A9-%D9%85%D8%B3%D9%8A%D8%B1%D8%A9-%D8%B1%D8%A8%D8%A7%D8%B9%D9%8A%D8%A9-%D9%85%D9%86-%D8%A7%D9%84%D9%82%D8%B7%D8%B9-%D8%A7%D9%84%D9%89-%D8%A7%D9%88%D9%84-%D8%A7%D9%82%D9%84%D8%A7%D8%B9/#lab-sitl): لا توجد محطة تحكم في هذه التجربة، وPX4 يرفض التشغيل دونها. والمخرج مختصراً على بعض المواضيع:

```text
/fmu/in/offboard_control_mode
/fmu/in/trajectory_setpoint
/fmu/in/vehicle_attitude_setpoint_v1
/fmu/in/vehicle_command
/fmu/in/vehicle_visual_odometry
/fmu/out/battery_status_v1
/fmu/out/failsafe_flags
/fmu/out/vehicle_attitude
/fmu/out/vehicle_local_position_v1
/fmu/out/vehicle_status_v1
/fmu/out/wind
```

ظهر 65 موضوعاً تحت `/fmu`، ولاحظ اللاحقة `_v1` في بعضها، وهي ترقيم إصدار الرسالة الذي ذكرناه، ولهذا يشترك الكود في `vehicle_status_v1` وليس `vehicle_status`، فإذا اشتركت بالاسم القديم من مثال قديم فلن يصلك شيء ولن ترى خطأ. الآن نشغل العقدة:

```bash
docker exec lab-drone2-ros /entry.sh python3 -u /lab/offboard_square.py
```

```text
[   0.1s] nav_state AUTO_LOITER  armed=False  failsafe=False
[   1.0s] requested Offboard + arm
[   1.2s] nav_state OFFBOARD  armed=True  failsafe=False
[   2.0s] wp 0 target ( 0.0, 0.0,5.0)  at ( -0.0, -0.0,-0.5)  err  5.5 m
[   4.0s] wp 0 target ( 0.0, 0.0,5.0)  at ( -0.0, -0.0,-0.6)  err  5.6 m
[   6.0s] wp 0 target ( 0.0, 0.0,5.0)  at ( -0.0,  0.0,-0.6)  err  5.6 m
[   8.0s] wp 0 target ( 0.0, 0.0,5.0)  at (  0.1,  0.0, 1.4)  err  3.6 m
[   9.9s] reached, next waypoint 1
[  10.0s] wp 1 target (10.0, 0.0,5.0)  at ( -0.1,  0.1, 4.6)  err 10.1 m
[  12.0s] wp 1 target (10.0, 0.0,5.0)  at (  6.5, -1.1, 5.2)  err  3.7 m
[  13.4s] reached, next waypoint 2
[  14.0s] wp 2 target (10.0,10.0,5.0)  at ( 10.1,  0.5, 5.4)  err  9.5 m
[  15.9s] reached, next waypoint 3
[  16.0s] wp 3 target ( 0.0,10.0,5.0)  at ( 10.1, 10.0, 5.1)  err 10.1 m
[  18.0s] wp 3 target ( 0.0,10.0,5.0)  at (  3.3, 10.6, 5.0)  err  3.4 m
[  19.0s] reached, next waypoint 4
[  20.0s] wp 4 target ( 0.0, 0.0,5.0)  at ( -0.3,  8.9, 5.3)  err  8.9 m
[  22.0s] wp 4 target ( 0.0, 0.0,5.0)  at ( -0.2,  1.1, 5.1)  err  1.1 m
[  22.4s] square done, landing
[  22.4s] nav_state AUTO_LAND  armed=True  failsafe=False
[  38.6s] nav_state OFFBOARD  armed=False  failsafe=False
[  39.4s] disarmed at (  0.1, -2.8), done
```

في المخرج أعلاه لاحظ التالي:

- بعد ثانية من البث قبل PX4 وضع Offboard والتشغيل معاً، وبدأت الطائرة تصعد إلى 5 أمتار ثم طارت المربع: 10 أمتار شمالاً ثم شرقاً ثم جنوباً ثم غرباً.
- الخطأ يقفز إلى 10 أمتار تقريباً عند كل نقطة جديدة لأن الهدف يتغير دفعة واحدة، فالعقدة ترسل النقطة التالية مباشرة، وPX4 هو الذي يحسب الطريق إليها ويحد السرعة والتسارع.
- بعد آخر نقطة أرسلت العقدة أمر الهبوط، فتحول الوضع إلى `AUTO_LAND` ثم أطفأ PX4 المحركات بعد الهبوط. ولاحظ أن موضع الهبوط بعيد قرابة 2.8 متر غرباً عن نقطة البداية، فأمر الهبوط يهبط حيث الطائرة، ولا يعود إلى نقطة محددة، والطائرة كانت ما زالت تتحرك حين صدر.

### نكسر الإعداد: الحاسوب المرافق يتجمد

قبل الكسر نقرأ ما سوف يفعله PX4 إذا انقطع دليل الحياة، من المعاملات في المحاكاة نفسها:

```bash
docker exec lab-drone2-px4 bash -c 'cd /root/px4/build; for p in COM_OF_LOSS_T COM_OBL_RC_ACT COM_RC_IN_MODE; do
  ./bin/px4-param show $p | grep -E " $p "; done'
```

```text
x   COM_OF_LOSS_T [285,509] : 1.0000
x   COM_OBL_RC_ACT [284,508] : 0
x   COM_RC_IN_MODE [296,520] : 1
```

أي أن PX4 ينتظر ثانية واحدة، ثم ينتقل إلى الوضع 0 وهو تثبيت الموقع Position، وأن التحكم اليدوي من MAVLink فقط لأنه لا يوجد جهاز تحكم في المحاكاة. الآن نعيد التجربة على Container جديد من PX4، ونطلب من العقدة أن تتجمد بعد 12 ثانية في Offboard:

```bash
docker rm -f lab-drone2-ros lab-drone2-px4
docker run -dit --name lab-drone2-px4 --network lab-drone2-net jonasvautherin/px4-gazebo-headless:1.17.0
docker exec lab-drone2-px4 bash -c 'cd /root/px4/build; ./bin/px4-param set NAV_DLL_ACT 0'
docker run -d --name lab-drone2-ros --network container:lab-drone2-px4 lab-drone2-ros:1 MicroXRCEAgent udp4 -p 8888
docker exec lab-drone2-ros /entry.sh python3 -u /lab/offboard_square.py --crash 12
docker logs -t lab-drone2-px4 | grep -E "commander|failsafe|navigator"
```

```text
[   0.1s] nav_state AUTO_LOITER  armed=False  failsafe=False
[   1.0s] requested Offboard + arm
[   1.0s] nav_state OFFBOARD  armed=True  failsafe=False
[   2.0s] wp 0 target ( 0.0, 0.0,5.0)  at ( -0.0,  0.0, 0.0)  err  5.0 m
[   4.0s] wp 0 target ( 0.0, 0.0,5.0)  at ( -0.0, -0.0, 0.0)  err  5.0 m
[   6.0s] wp 0 target ( 0.0, 0.0,5.0)  at (  0.1,  0.0, 1.8)  err  3.2 m
[   8.0s] wp 0 target ( 0.0, 0.0,5.0)  at ( -0.1,  0.1, 4.3)  err  0.7 m
[   8.9s] reached, next waypoint 1
[  10.0s] wp 1 target (10.0, 0.0,5.0)  at (  1.7, -0.5, 4.9)  err  8.3 m
[  11.8s] reached, next waypoint 2
[  12.0s] wp 2 target (10.0,10.0,5.0)  at ( 10.0, -0.2, 4.8)  err 10.2 m
[  13.2s] simulated hang: no more offboard_control_mode or setpoints
[  14.5s] nav_state AUTO_RTL  armed=True  failsafe=True
[  75.7s] nav_state OFFBOARD  armed=False  failsafe=False
[  76.2s] disarmed at (  0.0, -0.0), done

2026-10-12T12:03:33.591011185Z INFO  [commander] Takeoff detected
2026-10-12T12:03:45.039755557Z WARN  [failsafe] Failsafe activated
2026-10-12T12:03:45.111547085Z INFO  [navigator] RTL: start return at 31 m (30 m above destination)
2026-10-12T12:04:08.210004632Z INFO  [navigator] RTL: land at destination
2026-10-12T12:04:42.413621236Z INFO  [commander] Landing detected
2026-10-12T12:04:46.385621037Z INFO  [commander] Disarmed by landing
```

في المخرج أعلاه لاحظ التالي:

- بين توقف العقدة وإعلان الحماية 1.3 ثانية، أي مهلة `COM_OF_LOSS_T` وهي ثانية واحدة، مع تأخر بسيط.
- الإعداد يقول وضع تثبيت الموقع، ولكن PX4 اختار العودة RTL، والسبب أن وضع Position وضع يدوي يحتاج من يقود الطائرة، ولا يوجد جهاز تحكم، فاختار PX4 فعلاً يستطيع أن ينفذه وحده. ولهذا لا تفترض أن القيمة التي في المعامل هي ما سيحدث، واختبرها في SITL بالظروف نفسها التي ستطير بها.
- العودة بدأت بالصعود إلى 30 متراً فوق نقطة الإقلاع، كما رأينا في مقال البرمجة، ثم عادت الطائرة وهبطت في نقطة البداية، وهذا أفضل بكثير من أن تبقى في الهواء تنتظر كوداً لن يعود.

## قراءة سجل الطيران ULog بالكود

كل ما رأيناه في الكسر عرفناه من رسائل PX4 ومن مخرج عقدتنا، ولكن في طائرة حقيقية لا تكون أمامك شاشة وقت المشكلة، وإنما سجل الطيران. وPX4 يكتب سجلاً بصيغة ULog، وفي هذه المحاكاة يبدأ التسجيل من التشغيل حتى الإطفاء لأن المعامل `SDLOG_MODE` قيمته 1، والملف في المجلد `build/rootfs/log` داخل Container. ويذكر [توثيق تحليل السجلات](https://docs.px4.io/v1.17/en/log/flight%5Flog%5Fanalysis.html?ref=arabroot.io) ثلاث أدوات رئيسية: Flight Review على `logs.px4.io` وهو موقع ترفع إليه السجل فيرسم لك تقريراً، وPlotJuggler برنامج سطح مكتب يرسم أي موضوع، وpyulog مكتبة Python مع أوامر جاهزة. ولاحظ أن رفع السجل إلى موقع عام يعني أن بيانات طيرانك ومواقعه خرجت من جهازك، فإذا كان الطيران لجهة عملك فاستخدم الأدوات المحلية.

الأوامر التالية تنسخ سجل الطيرة الأولى من Container ال PX4، ثم تقرأ ملخصه ورسائله بأوامر pyulog:

```bash
docker cp lab-drone2-px4:/root/px4/build/rootfs/log/2026-10-12/11_59_40.ulg normal.ulg
docker cp normal.ulg lab-drone2-ros:/tmp/normal.ulg
# the second run, after the break, is 12_03_11.ulg in the new PX4 container: copy it the same way as hang.ulg
docker exec lab-drone2-ros /entry.sh bash -c 'cd /tmp; ulog_info normal.ulg | head -6; ulog_messages normal.ulg | head -8'
```

```text
Logging start time: 0:00:12, duration: 0:01:04
No Dropouts
SW Version: v1.17.0
Info Messages:
 boot_time_utc_us: 1791806368083657
 metadata_events_sha256: 3e07d215b9591c01d9014acff13d780e310b058438b0d7f0423b23bc9fc1f59d
0:00:12.600 INFO: [logger] logger started (mode=all)
0:00:12.604 INFO: logging: opening log file 2026-10-12/11_59_40.ulg
0:00:12.604 INFO: [logger] Start file log (type: full)
0:00:12.604 INFO: [logger] Opened full log file: ./log/2026-10-12/11_59_40.ulg
0:00:12.616 INFO: [mavlink] MAVLink only on localhost (set param MAV_{i}_BROADCAST = 1 to enable network)
0:00:12.616 INFO: [mavlink] MAVLink only on localhost (set param MAV_{i}_BROADCAST = 1 to enable network)
0:00:12.640 INFO: [px4] Startup script returned successfully
0:00:12.668 WARNING: [health_and_arming_checks] Preflight Fail: No connection to the GCS
```

والأوامر الجاهزة مفيدة للنظرة الأولى، ولكن الفائدة الحقيقية أن السجل بيانات تقرؤها بكود، فتسأله السؤال الذي يهمك. والسكربت `analyze_log.py` يطبع تغيرات الوضع، والتحذيرات، وأي علامات حماية Failsafe Flags ظهرت بعد التشغيل وبأي ترتيب، وأعلى ارتفاع وأبعد مسافة:

```python
"""Reads a PX4 ULog file and prints what happened: mode changes, warnings, which failsafe flags fired, and how
high and far the vehicle went.   python3 analyze_log.py <file.ulg>"""
import math
import sys

from pyulog import ULog

NAV = {0: "MANUAL", 2: "POSCTL", 3: "AUTO_MISSION", 4: "AUTO_LOITER", 5: "AUTO_RTL", 14: "OFFBOARD",
       17: "AUTO_TAKEOFF", 18: "AUTO_LAND"}

ulog = ULog(sys.argv[1])
t0 = int(ulog.start_timestamp)
sec = lambda t: (int(t) - t0) / 1e6
print(f"log length {(int(ulog.last_timestamp) - t0) / 1e6:.1f} s, {len(ulog.data_list)} topics, "
      f"{ulog.get_version_info_str()}, dropouts {len(ulog.dropouts)}")

def topic(name):
    for d in ulog.data_list:
        if d.name == name and d.multi_id == 0:
            return d.data
    raise KeyError(name)

st = topic("vehicle_status")
last = None
print("\nmode changes:")
for i, t in enumerate(st["timestamp"]):
    nav, armed, fs = int(st["nav_state"][i]), int(st["arming_state"][i]) == 2, bool(st["failsafe"][i])
    if (nav, armed, fs) != last:
        print(f"  {sec(t):6.1f}s  {NAV.get(nav, nav):12s} armed={armed} failsafe={fs}")
        last = (nav, armed, fs)

print("\nwarnings and worse written by PX4:")
for m in ulog.logged_messages:
    if m.log_level_str() in ("WARNING", "ERROR", "CRITICAL", "ALERT", "EMERGENCY"):
        print(f"  {sec(m.timestamp):6.1f}s  {m.log_level_str():9s} {m.message}")

ff = topic("failsafe_flags")
armed_from = min(sec(t) for i, t in enumerate(st["timestamp"]) if int(st["arming_state"][i]) == 2)
print(f"\nfailsafe flags that turned on after arming ({armed_from:.1f}s):")
fired = []
for field in ff:
    if field == "timestamp" or not field.endswith(("_lost", "_invalid", "_failure", "_low", "_breached")):
        continue
    for i, t in enumerate(ff["timestamp"]):
        if sec(t) > armed_from and ff[field][i]:
            fired.append((sec(t), field))
            break
for t, field in sorted(fired):
    print(f"  {t:6.1f}s  {field}")

pos = topic("vehicle_local_position")
far = max(math.hypot(x, y) for x, y in zip(pos["x"], pos["y"]))
print(f"\nhighest point {-min(pos['z']):.1f} m, farthest point {far:.1f} m from the start")
```

```bash
docker exec lab-drone2-ros /entry.sh bash -c 'cd /tmp; python3 /lab/analyze_log.py normal.ulg; python3 /lab/analyze_log.py hang.ulg'
```

```text
== normal.ulg
log length 64.3 s, 89 topics, v1.17.0, dropouts 0

mode changes:
    -0.1s  AUTO_LOITER  armed=False failsafe=False
    35.2s  OFFBOARD     armed=True failsafe=False
    50.6s  AUTO_LAND    armed=True failsafe=False
    63.4s  OFFBOARD     armed=False failsafe=False

warnings and worse written by PX4:
     0.1s  WARNING   [health_and_arming_checks] Preflight Fail: No connection to the GCS
     2.7s  ERROR     [sensors] Accel #0 fail:  TIMEOUT!
    25.1s  ERROR     [sensors] Accel #0 fail:  TIMEOUT!

failsafe flags that turned on after arming (35.2s):
    35.7s  gcs_connection_lost
    35.7s  manual_control_signal_lost
    51.7s  offboard_control_signal_lost

highest point 5.4 m, farthest point 14.5 m from the start

== hang.ulg
log length 78.5 s, 90 topics, v1.17.0, dropouts 0

mode changes:
    -0.2s  AUTO_LOITER  armed=False failsafe=False
    19.5s  OFFBOARD     armed=True failsafe=False
    31.2s  AUTO_RTL     armed=True failsafe=True
    77.5s  OFFBOARD     armed=False failsafe=False

warnings and worse written by PX4:
     0.6s  WARNING   [health_and_arming_checks] Preflight Fail: No connection to the GCS
    31.2s  WARNING   [failsafe] Failsafe activated
    78.1s  WARNING   [health_and_arming_checks] Preflight Fail: heading estimate not stable

failsafe flags that turned on after arming (19.5s):
    19.7s  gcs_connection_lost
    19.7s  manual_control_signal_lost
    31.2s  offboard_control_signal_lost
    78.1s  local_position_invalid

highest point 29.8 m, farthest point 15.5 m from the start
```

في المخرج أعلاه لاحظ التالي:

- في السجل الثاني تظهر القصة كاملة دون أي شاشة: تشغيل في Offboard عند 19.5 ثانية، ثم علامة `offboard_control_signal_lost` والعودة RTL مع الحماية عند 31.2 ثانية، ثم الإطفاء، وأعلى ارتفاع قرابة 30 متراً هو ارتفاع العودة.
- علامتا `gcs_connection_lost` و`manual_control_signal_lost` ظهرتا في الطيرتين بعد التشغيل بلحظات، لأنه لا توجد محطة ولا جهاز تحكم في المحاكاة، وفي طائرة حقيقية ظهور أي منهما يستحق أن تتوقف عنده.
- في السجل الأول ظهرت `offboard_control_signal_lost` أيضاً، ولكن بعد أن تحولت الطائرة إلى الهبوط، لأن العقدة توقفت عن البث بعد أمر الهبوط، فهي ليست مشكلة، وهذا مثال على أن العلامة وحدها لا تكفي وأن ترتيبها مع الوضع هو الذي يحكي.
- رسالة `Accel #0 fail: TIMEOUT` شرحناها في مقال الأساسيات، وهي من بطء المحاكاة على جهاز مشغول.

والصورة التالية رسمناها من السجلين نفسيهما بعد تحويل موضوعين منهما إلى CSV بالأمر `ulog2csv`:

![لوحتان: على اليسار Run 1: top view (north up) مربع رمادي متقطع ضلعه 10 أمتار والمسار البرتقالي فوقه يمر بالزوايا الأربع ويبدأ من نقطة خضراء start وينتهي على بعد أمتار غرب البداية، وعلى اليمين Run 2: altitude, node stops رسم للارتفاع مع الزمن يصعد إلى 5 أمتار، ثم خط أحمر متقطع node hangs قرب 10 ثوان، وبعده خط أزرق failsafe: RTL، ثم يصعد الارتفاع إلى 30 متراً ويبقى ثوان ثم ينزل حتى الأرض قرب 55 ثانية](https://arabroot.io/content/images/2026/10/drone-development-03-flight.webp)

المسار والارتفاع من سجل ULog نفسه، كما يراه من يفتح السجل بعد الطيران

### تنظيف المختبر

```bash
docker rm -f lab-drone2-ros lab-drone2-px4
docker network rm lab-drone2-net
docker image rm lab-drone2-ros:1
```

## اختبار آلي بالمحاكاة مع كل تعديل

ما فعلناه في التجربة يمكن أن يتحول إلى اختبار آلي يعمل مع كل تعديل في الكود، فالمحاكاة تعمل بلا شاشة داخل Docker كما رأيت، والسجل يقرأ بكود. وPX4 نفسه يفعل ذلك، فتقول صفحة [اختبارات التكامل](https://docs.px4.io/v1.17/en/test%5Fand%5Fci/integration%5Ftesting%5Fmavsdk.html?ref=arabroot.io) إن اختباراته المبنية على MAVSDK مطورة أساساً لتعمل على SITL، وإنها تعمل في التكامل المستمر CI، ويشغلها الأمر التالي بسرعة محاكاة أعلى من الزمن الحقيقي:

```bash
test/mavsdk_tests/mavsdk_test_runner.py test/mavsdk_tests/configs/sitl.json --speed-factor 10
```

وفي مشروعك تستطيع أن تبني الشيء نفسه بالقطع التي في هذا المقال: Container من PX4 في SITL، وContainer فيه عقدتك والوكيل، ثم سكربت يقرأ السجل ويفشل الاختبار إذا ظهرت حماية Failsafe أو تجاوز الخطأ بين الهدف والموقع حداً تختاره. وقد شرحنا تشغيل خط CI على سيرفرك في [دليل Gitea Actions](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/).

## التطوير لطائرات DJI بمكتبتها الرسمية

في [مقال المنصات](https://arabroot.io/articles/px4-%D9%88-ardupilot-%D8%A7%D9%85-dji-%D8%A7%D9%8A-%D9%85%D9%86%D8%B5%D8%A9-%D8%AA%D9%86%D8%A7%D8%B3%D8%A8-%D8%B7%D8%A7%D8%A6%D8%B1%D8%AA%D9%83/#dji) عرفنا مكتبات DJI الثلاث، وهنا ننظر إلى شكل الكود نفسه في Mobile SDK v5\. ولم ننشئ حساب مطور ولم نشغل الكود، فما يلي من [المستودع الرسمي](https://github.com/dji-sdk/Mobile-SDK-Android-V5?ref=arabroot.io) كما في آخر تعديل عليه في 3 يونيو 2026، والأمثلة فيه برخصة MIT. والخطوات كما يلي:

1. تسجل مطوراً في DJI، وتنشئ مفتاح تطبيق App Key مرتبطاً باسم حزمة تطبيقك.
2. تضع المفتاح في ملف `AndroidManifest.xml` بالاسم `com.dji.sdk.API_KEY`، وفي المثال يأتي من المتغير `AIRCRAFT_API_KEY` في `gradle.properties`.
3. تهيئ المكتبة ثم تسجل التطبيق، والتسجيل يحتاج اتصالاً بالإنترنت، ولهذا يعيد المثال المحاولة عندما تعود الشبكة.
4. تتعامل مع الطائرة من خلال «مفاتيح» Keys تقرؤها أو تستمع إليها أو تنفذها.

والمقطع التالي من الكود التجريبي في المستودع، ملف `MSDKManagerVM.kt`، مختصراً على التهيئة والتسجيل:

```kotlin
SDKManager.getInstance().init(appContext, object : SDKManagerCallback {
    override fun onRegisterSuccess() {
        lvRegisterState.postValue(Pair(true, null))
    }

    override fun onRegisterFailure(error: IDJIError) {
        lvRegisterState.postValue(Pair(false, error))
    }

    override fun onInitProcess(event: DJISDKInitEvent, totalProcess: Int) {
        lvInitProcess.postValue(Pair(event, totalProcess))
        // Don't forget to call the registerApp()
        if (event == DJISDKInitEvent.INITIALIZE_COMPLETE) {
            isInit = true
            SDKManager.getInstance().registerApp()
        }
    }
    // onProductConnect, onProductDisconnect, onProductChanged, onDatabaseDownloadProgress ...
})
```

وأمر الإقلاع في الملف `BasicAircraftControlVM.kt` مفتاح ينفذ:

```kotlin
FlightControllerKey.KeyStartTakeoff.create().action({
    callback.onSuccess(it)
}, { e: IDJIError ->
    callback.onFailure(e)
})
```

في الكود أعلاه لاحظ التالي:

- لا توجد هنا رسائل ولا بروتوكول تراه، فكل شيء خلف المفاتيح، وهذا هو الفرق العملي مع MAVLink وuXRCE-DDS: أنت لا ترى ما يحدث تحت المكتبة، ولا تستطيع أن تضيف ما ليس فيها.
- كل شيء يحتاج طائرة حقيقية مدعومة متصلة بالجوال أو جهاز التحكم، ولا يوجد ما يقابل SITL الذي يشغل كود الطيار الآلي على جهازك. والمثال فيه شاشة للمحاكي من خلال `SimulatorManager.getInstance().enableSimulator(...)`، ولكن هذا المحاكي يعمل والطائرة الحقيقية متصلة، فالبداية تحتاج عتاداً وحساباً.
- التطبيق Android فقط، فإذا كان فريقك يكتب Python أو ROS 2 على Linux فهذه المكتبة لا تناسبه، والأقرب له Payload SDK على Linux أو Cloud API على سيرفر.

## أخطاء شائعة

- **فرع `px4_msgs` لا يطابق إصدار PX4:** المواضيع لا تظهر أو تظهر بأسماء مختلفة، فابن الفرع الذي يطابق الطيار الآلي.
- **طلب Offboard قبل بث الأهداف:** PX4 يرفض التحويل، فابدأ البث ثانية على الأقل قبل الطلب.
- **إرسال الأهداف من مؤقت بطيء:** أقل من 2 هرتز يخرج الطائرة من Offboard، فاجعل البث في مؤقت مستقل عن منطقك الثقيل.
- **QoS لا يطابق PX4:** مواضيع `/fmu/out` تنشر بموثوقية Best Effort، والاشتراك بإعداد Reliable لا يستقبل شيئاً.
- **ترك `COM_OBL_RC_ACT` دون مراجعة:** اختر بنفسك ما تفعله الطائرة إذا تعطل الحاسوب المرافق، ثم اختبره في SITL كما في التجربة.
- **الطيران دون قراءة السجل:** كل طيران تجريبي ينتهي بقراءة ULog، حتى لو بدا كل شيء طبيعياً.

## تدريب عملي

1. غير `COM_OBL_RC_ACT` إلى 4 (الهبوط) وأعد الكسر، وقارن ما يطبعه سكربت السجل بالتشغيل السابق.
2. اجعل العقدة ترسل السرعة بدل الموقع، فتضع `velocity=True` في `OffboardControlMode`، وطر المربع بسرعة ثابتة.

## الخلاصة

- كل تعديل يمر ب SITL ثم HITL ثم الحقل، وكل مرحلة تجعل شيئاً إضافياً حقيقياً.
- الحاسوب المرافق يقرر «إلى أين»، والطيار الآلي يبقى مسؤولاً عن الاستقرار والحماية.
- ROS 2 يتحدث مع PX4 عبر uXRCE-DDS: وكيل على الحاسوب المرافق وعميل داخل PX4، ومواضيع تحت `/fmu/in` و`/fmu/out`.
- وضع Offboard يحتاج بثاً مستمراً أعلى من 2 هرتز، وانقطاعه يشغل حماية تختارها أنت.
- سجل ULog يقرأ بأدوات pyulog أو بكود، ويصلح أساساً لاختبار آلي في CI.
- التطوير مع DJI يكون فوق مفاتيح مكتبة مغلقة، ويحتاج حساباً وطائرة حقيقية من البداية.

[![](https://arabroot.io/content/images/2026/10/drone-programming-cover-cover.webp)دليلبرمجة الطائرات المسيرة مع PX4 وMAVSDKكيف تكتب برنامجاً يقلع بالطائرة المسيرة ويطير مهمة ويعود، وتختبره في محاكاة، وترى كيف يحميها الطيار الآلي عند الأعطال.](https://arabroot.io/articles/%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%B7%D8%A7%D8%A6%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B3%D9%8A%D8%B1%D8%A9-%D9%85%D8%B9-px4-%D9%88-mavsdk/)[![](https://arabroot.io/content/images/2026/10/drone-platforms-cover-cover.webp)دليلPX4 وArduPilot أم DJI: أي منصة تناسب طائرتك؟ما الفرق بين منصات الطائرات المسيرة المفتوحة ومنصة DJI المغلقة ومكتباتها، وما القيود الأمريكية الرسمية التي تدخل في القرار اليوم.](https://arabroot.io/articles/px4-%D9%88-ardupilot-%D8%A7%D9%85-dji-%D8%A7%D9%8A-%D9%85%D9%86%D8%B5%D8%A9-%D8%AA%D9%86%D8%A7%D8%B3%D8%A8-%D8%B7%D8%A7%D8%A6%D8%B1%D8%AA%D9%83/)[![](https://arabroot.io/content/images/2026/10/drone-build-cover-cover.webp)دليلتجميع طائرة مسيرة رباعية: من القطع إلى أول إقلاعكيف تختار قطع طائرة رباعية وتحسب دفعها وزمن طيرانها من جداول المحركات، ثم توصلها وتضبطها بأمان قبل أول إقلاع.](https://arabroot.io/articles/%D8%AA%D8%AC%D9%85%D9%8A%D8%B9-%D8%B7%D8%A7%D8%A6%D8%B1%D8%A9-%D9%85%D8%B3%D9%8A%D8%B1%D8%A9-%D8%B1%D8%A8%D8%A7%D8%B9%D9%8A%D8%A9-%D9%85%D9%86-%D8%A7%D9%84%D9%82%D8%B7%D8%B9-%D8%A7%D9%84%D9%89-%D8%A7%D9%88%D9%84-%D8%A7%D9%82%D9%84%D8%A7%D8%B9/)[![](https://arabroot.io/content/images/2026/10/drone-mapping-cover-cover.webp)دليلرسم الخرائط بالطائرات المسيرة مع OpenDroneMapكيف تتحول عشرات الصور الجوية إلى خريطة مصححة ونموذج ارتفاعات على سيرفرك، وما الذي يحدد دقتها قبل أن تقلع الطائرة.](https://arabroot.io/articles/%D8%B1%D8%B3%D9%85-%D8%A7%D9%84%D8%AE%D8%B1%D8%A7%D8%A6%D8%B7-%D8%A8%D8%A7%D9%84%D8%B7%D8%A7%D8%A6%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B3%D9%8A%D8%B1%D8%A9-%D9%85%D8%B9-opendronemap/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، والتجربة على PX4 v1.17.0 في محاكاة SITL مع Gazebo (jonasvautherin/px4-gazebo-headless:1.17.0)، وROS 2 Humble (rclpy 3.3.22) على Ubuntu 22.04.5 داخل ros:humble-ros-base-jammy، وMicro XRCE-DDS Agent v2.4.3 مع Fast DDS 2.14.7، وpx4\_msgs من الفرع release/1.17، وpyulog 1.2.4 على Python 3.10.12 في Docker Engine 29.8.0، ومكتبة DJI Mobile SDK من مستودعها الرسمي دون تشغيل، والمعلومات من توثيق PX4 كما قرأناه في 12 أكتوبر 2026.