Logo

معمل 04: التحكم عن بعد بلوحة المفاتيح لروبوت المستودع

9 دقائق قراءة
شرائح الدرس
1 / 15

معمل 04: التحكم عن بعد بلوحة المفاتيح لروبوت المستودع

بناء عقدة مخصصة للتحكم في السرعة

المقرر: معمل الروبوتات القسم: علوم الحاسب، كلية الحاسبات والمعلومات، جامعة المنصورة الفصل الدراسي: خريف 2025 إصدار ROS: ROS 2 Jazzy Jalisco إصدار Gazebo: Gazebo Jetty (gz-sim)


نظرة عامة

في هذا المعمل، ستُنشئ node مخصصة للتحكم عن بعد (teleop) بلوحة المفاتيح للتحكم يدويًا في روبوت المستودع الخاص بك. هذا يبني على محاكاة المستودع في المعمل 03 بإضافة تحكم بديهي عبر لوحة المفاتيح.

أهداف التعلّم:

  • إنشاء node مخصصة بلغة Python في ROS 2 لاستقبال إدخال لوحة المفاتيح
  • فهم التحكم في السرعة والنشر إلى topic باسم cmd_vel
  • تنفيذ ميزات أمان وإيقاف طارئ (emergency stop)
  • اختبار والتحقق من تحكم الروبوت في المحاكاة

المتطلبات الأساسية:

  • إتمام المعمل 03 (محاكاة المستودع)
  • حزمة warehouse_simulation تعمل بشكل صحيح
  • الروبوت يستجيب بشكل صحيح لرسائل /cmd_vel

الجزء 1: فهم التحكم عن بعد بلوحة المفاتيح (Keyboard Teleop)

ما هو الـTeleop؟

التشغيل عن بعد (Teleoperation أو Teleop) = تشغيل الروبوت عن بعد بواسطة مُشغِّل بشري.

بالنسبة لروبوت المستودع الخاص بنا:

  • المدخَل (Input): ضغطات لوحة المفاتيح (w، a، s، d، إلخ)
  • المعالجة: node في ROS تحوّل ضغطات المفاتيح إلى أوامر سرعة
  • المُخرَج (Output): رسائل Twist تُنشر إلى topic باسم /cmd_vel
  • النتيجة: الروبوت يتحرك في Gazebo

لماذا نُنشئ teleop مخصصًا؟

توجد حزم teleop قياسية في ROS، لكن إنشاء حزمتنا الخاصة يتيح:

  1. التخصيص: تكييف عناصر التحكم مع احتياجاتنا المحددة
  2. التعلّم: فهم كيفية عمل nodes في ROS
  3. الميزات: إضافة وظائف مخصصة (حدود للسرعة، إيقاف طارئ، إلخ)
  4. الدمج: تكامل أفضل مع حزمة warehouse_simulation الخاصة بنا

الجزء 2: تصميم node الـTeleop

مخطط التحكم

سنستخدم تحكمًا تراكميًا في السرعة (incremental velocity control):

w : زيادة السرعة الأمامية (+0.1 م/ث لكل ضغطة)
x : زيادة السرعة الخلفية (-0.1 م/ث لكل ضغطة)
a : زيادة معدل الدوران لليسار (+0.2 راد/ث لكل ضغطة)
d : زيادة معدل الدوران لليمين (-0.2 راد/ث لكل ضغطة)
s : توقف (ضبط جميع السرعات على صفر)
SPACE : إيقاف طارئ (نفس تأثير 's')
q : الخروج من البرنامج

لماذا تراكمي (incremental)؟

  • تسارع/تباطؤ سلس
  • تحكم دقيق في سرعة الروبوت
  • الروبوت يحافظ على سرعته حتى تُغيّرها أنت

حدود السرعة

من أجل السلامة، سننفّذ حدودًا قصوى:

  • أقصى سرعة خطية (linear velocity): 1.0 م/ث
  • أقصى سرعة زاوية (angular velocity): 2.0 راد/ث

هذا يمنع الروبوت من التحرك بسرعة خطرة.


الجزء 3: التنفيذ

الخطوة 3.1: إنشاء node الـTeleop

أنشئ الملف:

cd ~/Desktop/ROS-2-Practical-Course-Roadmap-2025/ros_ws/warehouse_simulation/warehouse_simulation
nano teleop_key.py

أضف كود node الـteleop الكامل:

#!/usr/bin/env python3
"""
Teleop Key Node for Warehouse Robot
Control the robot using keyboard keys:
    w/x : increase/decrease linear velocity
    a/d : increase/decrease angular velocity
    s   : stop
    SPACE : emergency stop
    q   : quit
"""
 
import sys
import tty
import termios
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
 
 
class TeleopKeyNode(Node):
    def __init__(self):
        super().__init__('teleop_key')
 
        # Publisher for cmd_vel
        self.publisher = self.create_publisher(Twist, '/cmd_vel', 10)
 
        # Velocity settings
        self.linear_velocity = 0.0
        self.angular_velocity = 0.0
        self.linear_step = 0.1  # m/s
        self.angular_step = 0.2  # rad/s
        self.max_linear = 1.0
        self.max_angular = 2.0
 
        self.get_logger().info('Teleop Key Node Started!')
        self.get_logger().info('Use w/x for linear, a/d for angular, s to stop, SPACE for emergency stop, q to quit')
 
    def get_key(self):
        """Get a single keypress from the terminal"""
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch
 
    def update_velocity(self, linear_change, angular_change):
        """Update velocity with limits"""
        self.linear_velocity += linear_change
        self.angular_velocity += angular_change
 
        # Apply limits
        self.linear_velocity = max(-self.max_linear, min(self.max_linear, self.linear_velocity))
        self.angular_velocity = max(-self.max_angular, min(self.max_angular, self.angular_velocity))
 
    def publish_velocity(self):
        """Publish the current velocity"""
        msg = Twist()
        msg.linear.x = self.linear_velocity
        msg.angular.z = self.angular_velocity
        self.publisher.publish(msg)
 
        self.get_logger().info(f'Linear: {self.linear_velocity:.2f} m/s, Angular: {self.angular_velocity:.2f} rad/s')
 
    def run(self):
        """Main control loop"""
        print("\n" + "="*60)
        print("Warehouse Robot Teleop Control")
        print("="*60)
        print("Controls:")
        print("  w : Increase forward speed")
        print("  x : Increase backward speed")
        print("  a : Turn left (increase)")
        print("  d : Turn right (increase)")
        print("  s : Stop all movement")
        print("  SPACE : Emergency stop")
        print("  q : Quit")
        print("="*60 + "\n")
 
        try:
            while rclpy.ok():
                key = self.get_key()
 
                if key == 'w':
                    self.update_velocity(self.linear_step, 0)
                    self.publish_velocity()
 
                elif key == 'x':
                    self.update_velocity(-self.linear_step, 0)
                    self.publish_velocity()
 
                elif key == 'a':
                    self.update_velocity(0, self.angular_step)
                    self.publish_velocity()
 
                elif key == 'd':
                    self.update_velocity(0, -self.angular_step)
                    self.publish_velocity()
 
                elif key == 's' or key == ' ':
                    self.linear_velocity = 0.0
                    self.angular_velocity = 0.0
                    self.publish_velocity()
                    if key == ' ':
                        self.get_logger().warn('EMERGENCY STOP!')
 
                elif key == 'q':
                    self.get_logger().info('Quitting...')
                    break
 
                elif key == '\x03':  # Ctrl+C
                    break
 
        except Exception as e:
            self.get_logger().error(f'Error: {e}')
 
        finally:
            # Stop the robot before exiting
            self.linear_velocity = 0.0
            self.angular_velocity = 0.0
            self.publish_velocity()
            self.get_logger().info('Teleop Key Node Stopped')
 
 
def main(args=None):
    rclpy.init(args=args)
 
    teleop_node = TeleopKeyNode()
 
    try:
        teleop_node.run()
    except KeyboardInterrupt:
        pass
    finally:
        teleop_node.destroy_node()
        rclpy.shutdown()
 
 
if __name__ == '__main__':
    main()

احفظ واخرج (Ctrl+O، Enter، Ctrl+X).


الخطوة 3.2: اجعل الملف قابلًا للتنفيذ

chmod +x ~/Desktop/ROS-2-Practical-Course-Roadmap-2025/ros_ws/warehouse_simulation/warehouse_simulation/teleop_key.py

الخطوة 3.3: تحديث setup.py

عدّل ملف setup.py:

cd ~/Desktop/ROS-2-Practical-Course-Roadmap-2025/ros_ws/warehouse_simulation
nano setup.py

حدّث قسم entry_points:

entry_points={
    'console_scripts': [
        'teleop_key = warehouse_simulation.teleop_key:main',
    ],
},

احفظ واخرج.


الخطوة 3.4: بناء الحزمة

cd ~/Desktop/ROS-2-Practical-Course-Roadmap-2025/ros_ws
colcon build --packages-select warehouse_simulation
source install/setup.bash

المُخرَج المتوقَّع:

Starting >>> warehouse_simulation
Finished <<< warehouse_simulation [X.XXs]
 
Summary: 1 package finished [X.XXs]

الجزء 4: اختبار node الـTeleop

الطرفية 1: تشغيل المحاكاة

cd ~/Desktop/ROS-2-Practical-Course-Roadmap-2025/ros_ws
source install/setup.bash
ros2 launch warehouse_simulation warehouse_simulation.launch.py

انتظر حتى:

  • تُفتح نافذة Gazebo
  • يظهر الروبوت في المستودع
  • تظهر رسائل الـbridge الدالة على اتصال الـtopics

الطرفية 2: تشغيل node الـTeleop

cd ~/Desktop/ROS-2-Practical-Course-Roadmap-2025/ros_ws
source install/setup.bash
ros2 run warehouse_simulation teleop_key

يجب أن ترى:

============================================================
Warehouse Robot Teleop Control
============================================================
Controls:
  w : Increase forward speed
  x : Increase backward speed
  a : Turn left (increase)
  d : Turn right (increase)
  s : Stop all movement
  SPACE : Emergency stop
  q : Quit
============================================================
 
[INFO] [teleop_key]: Teleop Key Node Started!
[INFO] [teleop_key]: Use w/x for linear, a/d for angular, s to stop, SPACE for emergency stop, q to quit

تسلسل الاختبار

جرّب هذا التسلسل لاختبار جميع عناصر التحكم:

  1. اضغط w 3 مرات:

    • يجب أن تزيد السرعة: 0.1، 0.2، 0.3 م/ث
    • يجب أن يتحرك الروبوت للأمام في Gazebo
  2. اضغط a مرتين:

    • يجب أن تزيد السرعة الزاوية: 0.2، 0.4 راد/ث
    • يجب أن يبدأ الروبوت بالانعطاف لليسار أثناء تحركه للأمام
  3. اضغط s:

    • يجب أن تنخفض كلتا السرعتين إلى صفر
    • يجب أن يتوقف الروبوت
  4. اضغط x مرتين:

    • يجب أن تصبح السرعة الخطية: -0.1، -0.2 م/ث
    • يجب أن يتحرك الروبوت للخلف
  5. اضغط d 3 مرات:

    • يجب أن تصبح السرعة الزاوية: -0.2، -0.4، -0.6 راد/ث
    • يجب أن ينعطف الروبوت لليمين أثناء تحركه للخلف
  6. اضغط SPACE:

    • يجب أن تظهر رسالة الإيقاف الطارئ
    • يجب أن يتوقف الروبوت فورًا
  7. اضغط q:

    • يجب أن يخرج البرنامج بشكل نظيف

الجزء 5: فهم الكود

المفاهيم الأساسية

1. بنية node في ROS

class TeleopKeyNode(Node):
    def __init__(self):
        super().__init__('teleop_key')
        self.publisher = self.create_publisher(Twist, '/cmd_vel', 10)
  • ترث من الصنف (class) Node
  • تُنشئ publisher لرسائل Twist على topic باسم /cmd_vel

2. التعامل مع إدخال الطرفية (Terminal Input)

def get_key(self):
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        ch = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch
  • تضبط الطرفية على الوضع الخام (raw mode) لقراءة ضغطات مفاتيح فردية
  • تُعيد إعدادات الطرفية بعد القراءة

3. التحكم التراكمي في السرعة (Incremental Velocity Control)

def update_velocity(self, linear_change, angular_change):
    self.linear_velocity += linear_change
    self.angular_velocity += angular_change
 
    # Apply limits
    self.linear_velocity = max(-self.max_linear, min(self.max_linear, self.linear_velocity))
  • تُضيف التغيير إلى السرعة الحالية (تراكمي)
  • تُقيّد القيمة ضمن الحد الأقصى/الأدنى

4. نشر رسائل Twist

def publish_velocity(self):
    msg = Twist()
    msg.linear.x = self.linear_velocity
    msg.angular.z = self.angular_velocity
    self.publisher.publish(msg)
  • تُنشئ رسالة Twist
  • تضبط linear.x (الأمام/الخلف) وangular.z (الدوران)
  • تُنشر إلى /cmd_vel

الجزء 6: المراقبة واستكشاف الأخطاء

مراقبة الرسائل المنشورة

افتح طرفية جديدة:

source ~/Desktop/ROS-2-Practical-Course-Roadmap-2025/ros_ws/install/setup.bash
ros2 topic echo /cmd_vel

أثناء ضغطك على المفاتيح، سترى:

linear:
  x: 0.3
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 0.2
---

التحقق من معلومات الـNode

ros2 node list
# Should show: /teleop_key
 
ros2 node info /teleop_key
# Shows publishers, subscribers, etc.

تصور (Visualize) النظام

ros2 run rqt_graph rqt_graph

يجب أن ترى:

/teleop_key --> /cmd_vel --> /parameter_bridge --> Gazebo

الجزء 7: تحسينات وتمارين

تمرين 1: إضافة إعدادات سرعة مسبقة (Speed Presets)

عدّل الكود لإضافة مفاتيح أرقام (1-5) لسرعات مسبقة الضبط:

# In the run() method, add:
elif key == '1':
    self.linear_step = 0.05
    self.get_logger().info('Speed: SLOW')
elif key == '2':
    self.linear_step = 0.1
    self.get_logger().info('Speed: NORMAL')
elif key == '3':
    self.linear_step = 0.2
    self.get_logger().info('Speed: FAST')

تمرين 2: إضافة عرض للموضع (Position Display)

اطبع الموضع الحالي من الـodometry:

# Add subscriber in __init__:
self.odom_sub = self.create_subscription(
    Odometry, '/odom', self.odom_callback, 10
)
 
# Add callback:
def odom_callback(self, msg):
    self.position = msg.pose.pose.position

تمرين 3: تحذير من التصادم (Collision Warning)

أضف تحذيرًا عند اكتشاف عوائق:

# Add subscriber for laser scan:
self.scan_sub = self.create_subscription(
    LaserScan, '/scan', self.scan_callback, 10
)
 
def scan_callback(self, msg):
    min_dist = min(msg.ranges)
    if min_dist < 0.5:  # 50cm
        self.get_logger().warn(f'Obstacle detected at {min_dist:.2f}m!')

الجزء 8: خيارات بديلة للـTeleop

الخيار 1: Teleop القياسي في ROS (اضغط باستمرار للتحرك)

sudo apt install ros-jazzy-teleop-twist-keyboard
ros2 run teleop_twist_keyboard teleop_twist_keyboard

الفروقات:

  • يجب الضغط باستمرار على المفتاح للحفاظ على السرعة
  • تخطيط مفاتيح مختلف (i/j/k/l للحركة)
  • بدون تحكم تراكمي

الخيار 2: التحكم عبر يد تحكم (Gamepad)

sudo apt install ros-jazzy-joy ros-jazzy-teleop-twist-joy
 
# Terminal 1
ros2 run joy joy_node
 
# Terminal 2
ros2 run teleop_twist_joy teleop_node

يتطلب: يد تحكم/عصا تحكم USB


الجزء 9: استكشاف الأخطاء وإصلاحها

المشكلة 1: خطأ "Inappropriate ioctl for device"

السبب: التشغيل في طرفية غير تفاعلية (IDE/سكريبت)

الحل: شغّل teleop_key في نافذة طرفية حقيقية

المشكلة 2: الروبوت لا يتحرك

تحقق من:

# Is simulation running?
ros2 topic list | grep cmd_vel
 
# Is teleop publishing?
ros2 topic echo /cmd_vel
 
# Are there multiple publishers?
ros2 topic info /cmd_vel

الحل: تأكد من وجود publisher واحد فقط لـ/cmd_vel

المشكلة 3: الروبوت يتحرك من تلقاء نفسه

تحقق من:

ros2 topic echo /cmd_vel

الحل: هناك عنصر آخر ينشر إلى /cmd_vel. أوقف الـnodes الأخرى.

المشكلة 4: المفاتيح لا تستجيب

تحقق من: أن نافذة الطرفية لديها التركيز (نشطة)

الحل: انقر على الطرفية التي تُشغّل teleop_key قبل الضغط على المفاتيح


متطلبات التسليم

ما الذي يجب تسليمه

  1. حزمة الكود:

    cd ~/Desktop/ROS-2-Practical-Course-Roadmap-2025/ros_ws
    tar -czf lab04_teleop.tar.gz warehouse_simulation/
  2. عرض توضيحي بالفيديو (2-3 دقائق):

    • أظهِر واجهة التحكم بالـteleop
    • وضّح جميع أوامر الحركة (w، x، a، d، s)
    • أظهِر الروبوت يتحرك في Gazebo استجابةً للمفاتيح
    • أظهِر التغذية الراجعة للسرعة في الطرفية
    • وضّح الإيقاف الطارئ (SPACE)
  3. تقرير المعمل (بصيغة PDF):

    • مقدمة
    • اشرح كيفية عمل node الـteleop
    • لقطات شاشة تُظهر:
      • تشغيل الـteleop مع رسائل التحكم
      • الروبوت يتحرك في Gazebo
      • مُخرَج topic echo
      • مخطط الـnode (rqt_graph)
    • أجِب عن أسئلة النقاش
    • صف تحسينًا واحدًا نفّذته (اختياري)
    • الخاتمة

أسئلة للنقاش

أجِب عن هذه الأسئلة في تقريرك:

  1. اشرح الفرق بين التحكم التراكمي في السرعة (teleop الخاص بنا) والتحكم المباشر في السرعة (teleop القياسي). ما مزايا كلٍّ منهما؟

  2. لماذا نحتاج إلى تقييد (clamp) السرعات ضمن حد أقصى/أدنى؟ ماذا قد يحدث لو لم نفعل ذلك؟

  3. دالة get_key() تستخدم tty.setraw() ثم تُعيد الإعدادات. لماذا هذا ضروري؟

  4. في رسالة Twist، نضبط linear.x وangular.z. لماذا لا نستخدم linear.y أو angular.x/y لروبوت أرضي؟

  5. كيف يمكنك تعديل node الـteleop ليجعل الروبوت يتبع مسارًا محددًا مسبقًا (مثل مربع) تلقائيًا؟

  6. ماذا يحدث إذا تعطّل node الـteleop أثناء تحرك الروبوت؟ كيف يمكنك تنفيذ ميزة أمان للتعامل مع ذلك؟


معايير التقييم

العنصرالدرجةالمعايير
تنفيذ الـTeleop30node تعمل بشكل صحيح، جميع المفاتيح فعّالة
جودة الكود20نظيف، موثَّق بتعليقات جيدة، يتبع أفضل الممارسات
الاختبار20اختبار شامل موثَّق
العرض بالفيديو15واضح، يُظهر جميع الميزات
تقرير المعمل15مكتوب بعناية، يجيب عن جميع الأسئلة
الإجمالي100

تحديات إضافية (+10 درجات لكل تحدٍّ، بحد أقصى +30)

  1. تحكم متعدد السرعات: نفّذ 5 مستويات سرعة (المفاتيح 1-5) مع تغذية راجعة بصرية

  2. تسارع سلس (Smooth Ramping): أضِف تسارعًا/تباطؤًا تدريجيًا بدلًا من تغييرات السرعة الفورية

  3. تسجيل المسار: سجّل مسار الروبوت واحفظه في ملف لإعادة تشغيله لاحقًا

  4. منطقة أمان: إيقاف تلقائي عند اكتشاف عائق ضمن مسافة 30 سم

  5. لوحة حالة (Status Dashboard): اطبع لوحة حالة لحظية تُظهر السرعة والموضع والعوائق القريبة


مرجع سريع

بنية الملفات

warehouse_simulation/
├── warehouse_simulation/
│   ├── __init__.py
│   └── teleop_key.py          ← Created in this lab
├── config/
│   └── bridge.yaml
├── launch/
│   └── warehouse_simulation.launch.py
├── package.xml
└── setup.py                   ← Updated in this lab

الأوامر الأساسية

# Build
colcon build --packages-select warehouse_simulation
 
# Launch simulation (Terminal 1)
ros2 launch warehouse_simulation warehouse_simulation.launch.py
 
# Run teleop (Terminal 2)
ros2 run warehouse_simulation teleop_key
 
# Monitor velocity (Terminal 3)
ros2 topic echo /cmd_vel
 
# Check nodes
ros2 node list
ros2 node info /teleop_key
 
# Visualize
ros2 run rqt_graph rqt_graph

ملخّص عناصر التحكم

w     - Forward speed +0.1 m/s
x     - Backward speed -0.1 m/s
a     - Turn left +0.2 rad/s
d     - Turn right -0.2 rad/s
s     - Stop
SPACE - Emergency stop
q     - Quit

تحكّم عن بعد سعيد! استمتع بالتحكم في روبوت المستودع الخاص بك! 🎮🤖


تم إعداد محتوى هذا المعمل لكلية الحاسبات والمعلومات، جامعة المنصورة - خريف 2025 تم تحديثه ليتوافق مع Gazebo Jetty وROS 2 Jazzy