برنامج تعليمي تمهيدي لبرمجة الروبوت
نشرت: 2022-03-11دعونا نواجه الأمر ، الروبوتات رائعة. كما أنهم سيديرون العالم يومًا ما ، ونأمل ، في ذلك الوقت ، أنهم سيشعرون بالشفقة على المبدعين الضعفاء الفقراء (المعروفين أيضًا باسم مطوري الروبوتات) ويساعدوننا في بناء مدينة فضاء مليئة بالكثير. أنا أمزح بالطبع ، لكن فقط نوعًا ما.
في طموحي في الحصول على بعض التأثير الصغير على هذه المسألة ، أخذت دورة في نظرية التحكم في الروبوتات المستقلة العام الماضي ، والتي بلغت ذروتها في بناء محاكاة روبوتية تعتمد على بايثون سمحت لي بممارسة نظرية التحكم على روبوت بسيط ومتحرك وقابل للبرمجة .
في هذه المقالة ، سأوضح كيفية استخدام إطار عمل روبوت Python لتطوير برنامج تحكم ، ووصف مخطط التحكم الذي طورته لروبوتي المحاكي ، وتوضيح كيفية تفاعله مع بيئته وتحقيق أهدافه ، ومناقشة بعض التحديات الأساسية لبرمجة الروبوتات التي واجهتها على طول الطريق.
لمتابعة هذا البرنامج التعليمي حول برمجة الروبوتات للمبتدئين ، يجب أن تكون لديك معرفة أساسية بشيئين:
- الرياضيات - سنستخدم بعض الدوال المثلثية والمتجهات
- Python - نظرًا لأن Python من بين لغات برمجة الروبوت الأساسية الأكثر شيوعًا - سنستخدم مكتبات ووظائف Python الأساسية
تعتبر مقتطفات الشفرة الموضحة هنا مجرد جزء من المحاكي بأكمله ، والذي يعتمد على الفئات والواجهات ، لذلك من أجل قراءة الكود مباشرةً ، قد تحتاج إلى بعض الخبرة في Python والبرمجة الموجهة للكائنات.
أخيرًا ، الموضوعات الاختيارية التي ستساعدك على متابعة هذا البرنامج التعليمي بشكل أفضل هي معرفة ماهية آلة الحالة وكيف تعمل مستشعرات النطاق وأجهزة التشفير.
تحدي الروبوت القابل للبرمجة: التصور مقابل الواقع وهشاشة التحكم
التحدي الأساسي لجميع الروبوتات هو: من المستحيل معرفة الحالة الحقيقية للبيئة. يمكن لبرنامج التحكم في الروبوت فقط تخمين حالة العالم الحقيقي بناءً على القياسات التي يتم إرجاعها بواسطة أجهزة الاستشعار الخاصة به. يمكنه فقط محاولة تغيير حالة العالم الحقيقي من خلال توليد إشارات تحكم.
وبالتالي ، فإن إحدى الخطوات الأولى في تصميم التحكم هي الخروج بتجريد للعالم الحقيقي ، يُعرف بالنموذج ، والذي يمكن بواسطته تفسير قراءات أجهزة الاستشعار الخاصة بنا واتخاذ القرارات. طالما أن العالم الحقيقي يتصرف وفقًا لافتراضات النموذج ، فيمكننا عمل تخمينات جيدة وممارسة السيطرة. ومع ذلك ، بمجرد أن ينحرف العالم الحقيقي عن هذه الافتراضات ، لن نتمكن بعد الآن من إجراء تخمينات جيدة ، وسوف نفقد السيطرة. في كثير من الأحيان ، بمجرد فقدان السيطرة ، لا يمكن استعادتها أبدًا. (إلا إذا استعادته قوة خارجية خيّرة).
هذا هو أحد الأسباب الرئيسية التي تجعل برمجة الروبوتات صعبة للغاية. غالبًا ما نشاهد مقاطع فيديو لأحدث روبوت بحثي في المختبر ، وهو يؤدي أعمالاً رائعة من البراعة أو التنقل أو العمل الجماعي ، ونميل إلى طرح السؤال ، "لماذا لا يتم استخدام هذا في العالم الحقيقي؟" حسنًا ، في المرة القادمة التي تشاهد فيها مثل هذا الفيديو ، ألق نظرة على مدى التحكم الشديد في بيئة المعمل. في معظم الحالات ، تكون هذه الروبوتات قادرة فقط على أداء هذه المهام المثيرة للإعجاب طالما ظلت الظروف البيئية ضمن الحدود الضيقة لنموذجها الداخلي. وبالتالي ، فإن أحد مفاتيح تقدم الروبوتات هو تطوير نماذج أكثر تعقيدًا ومرونة وقوة - ويخضع التقدم المذكور لحدود الموارد الحسابية المتاحة.
[ملاحظة جانبية: سيلاحظ الفلاسفة وعلماء النفس على حد سواء أن الكائنات الحية تعاني أيضًا من الاعتماد على تصورها الداخلي لما تخبرهم به حواسهم. تأتي العديد من التطورات في مجال الروبوتات من مراقبة الكائنات الحية ومعرفة كيفية تفاعلها مع المنبهات غير المتوقعة. فكر في الأمر. ما هو نموذجك الداخلي للعالم؟ هل يختلف عن نملة عن نملة وسمكة؟ (نأمل.) ومع ذلك ، مثل النملة والأسماك ، فمن المرجح أن تبالغ في تبسيط بعض حقائق العالم. عندما تكون افتراضاتك حول العالم غير صحيحة ، فقد يعرضك ذلك لخطر فقدان السيطرة على الأشياء. أحيانًا نطلق على هذا "الخطر". بنفس الطريقة التي يكافح بها روبوتنا الصغير للبقاء على قيد الحياة ضد الكون المجهول ، كذلك نحن جميعًا. هذه رؤية قوية لعلماء الروبوتات.]
محاكي الروبوت القابل للبرمجة
تمت كتابة المحاكاة التي قمت بإنشائها بلغة Python وأطلق عليها اسم Sobot Rimulator . يمكنك العثور على v1.0.0 على جيثب. لا يحتوي على الكثير من الأجراس والصفارات ولكنه مصمم للقيام بشيء واحد بشكل جيد للغاية: توفير محاكاة دقيقة لروبوت متنقل وإعطاء الروبوتات الطموح إطارًا بسيطًا لممارسة برمجة برامج الروبوت. في حين أنه من الأفضل دائمًا امتلاك روبوت حقيقي للعب به ، فإن محاكي Python الروبوت الجيد يمكن الوصول إليه بسهولة وهو مكان رائع للبدء.
في الروبوتات الواقعية ، فإن البرنامج الذي يولد إشارات التحكم ("وحدة التحكم") مطلوب للعمل بسرعة عالية جدًا وإجراء عمليات حسابية معقدة. يؤثر هذا على اختيار لغات برمجة الروبوت الأفضل استخدامًا: عادةً ما يتم استخدام C ++ لهذه الأنواع من السيناريوهات ، ولكن في تطبيقات الروبوتات الأبسط ، تعد Python بمثابة حل وسط جيد جدًا بين سرعة التنفيذ وسهولة التطوير والاختبار.
يحاكي البرنامج الذي كتبته روبوتًا بحثيًا واقعيًا يُدعى Khepera ولكن يمكن تكييفه مع مجموعة من الروبوتات المتنقلة ذات الأبعاد والمستشعرات المختلفة. منذ أن حاولت برمجة جهاز المحاكاة قدر الإمكان لإمكانيات الروبوت الحقيقي ، يمكن تحميل منطق التحكم في روبوت Khepera حقيقي مع الحد الأدنى من إعادة البناء ، وسوف يؤدي نفس أداء الروبوت المحاكى. تشير الميزات المحددة المطبقة إلى Khepera III ، ولكن يمكن تكييفها بسهولة مع Khepera IV الجديدة.
وبعبارة أخرى ، فإن برمجة الروبوت المحاكي تشبه برمجة روبوت حقيقي. هذا أمر بالغ الأهمية إذا كان المحاكي سيكون مفيدًا لتطوير وتقييم مناهج برامج التحكم المختلفة.
في هذا البرنامج التعليمي ، سأصف بنية برنامج التحكم في الروبوت الذي يأتي مع الإصدار 1.0.0 من Sobot Rimulator ، وسأقدم مقتطفات من مصدر Python (مع تعديلات طفيفة من أجل الوضوح). ومع ذلك ، فإنني أشجعك على الغوص في المصدر والعبث. تم تشعب جهاز المحاكاة واستخدامه للتحكم في الروبوتات المحمولة المختلفة ، بما في ذلك Roomba2 من iRobot. وبالمثل ، لا تتردد في تفرع المشروع وتحسينه.
منطق التحكم في الروبوت مقيد بفئات / ملفات Python هذه:
-
models/supervisor.py
- هذه الفئة مسؤولة عن التفاعل بين العالم المحاكي حول الروبوت والروبوت نفسه. إنه يطور آلة حالة الروبوت الخاصة بنا ويطلق وحدات التحكم لحساب السلوك المطلوب. -
models/supervisor_state_machine.py
- تمثل هذه الفئة الحالات المختلفة التي يمكن أن يكون فيها الروبوت ، اعتمادًا على تفسيره لأجهزة الاستشعار. - الملفات الموجودة في دليل
models/controllers
- تنفذ هذه الفئات سلوكيات مختلفة للروبوت نظرًا لحالة البيئة المعروفة. على وجه الخصوص ، يتم تحديد وحدة تحكم معينة اعتمادًا على جهاز الحالة.
الهدف
تحتاج الروبوتات ، مثل البشر ، إلى غاية في الحياة. سيكون هدف برنامجنا الذي يتحكم في هذا الروبوت بسيطًا للغاية: سيحاول شق طريقه إلى نقطة هدف محددة مسبقًا. عادة ما تكون هذه هي الميزة الأساسية التي يجب أن يتمتع بها أي روبوت متنقل ، من السيارات المستقلة إلى المكانس الكهربائية الروبوتية. تتم برمجة إحداثيات الهدف في برنامج التحكم قبل تنشيط الروبوت ولكن يمكن إنشاؤها من تطبيق Python الإضافي الذي يشرف على حركات الروبوت. على سبيل المثال ، فكر في الأمر وهو يقودك عبر نقاط طريق متعددة.
ومع ذلك ، ولتعقيد الأمور ، قد تكون بيئة الروبوت مليئة بالعقبات. لا يجوز للروبوت أن يصطدم بعقبة في طريقه إلى الهدف. لذلك ، إذا واجه الروبوت عقبة ، فسيتعين عليه أن يجد طريقه حتى يتمكن من الاستمرار في طريقه إلى الهدف.
الروبوت القابل للبرمجة
كل روبوت يأتي بقدرات مختلفة ومخاوف تتعلق بالتحكم. دعنا نتعرف على روبوتنا المحاكي القابل للبرمجة.
أول شيء يجب ملاحظته هو أنه في هذا الدليل ، سيكون الروبوت الخاص بنا روبوتًا متحركًا مستقلًا . هذا يعني أنه سيتحرك في الفضاء بحرية وأنه سيتحرك تحت سيطرته الخاصة. هذا على عكس ، على سبيل المثال ، روبوت يعمل بالتحكم عن بعد (وهو ليس مستقلاً) أو ذراع روبوت مصنع (وهو ليس متحركًا). يجب أن يكتشف الروبوت الخاص بنا بنفسه كيفية تحقيق أهدافه والبقاء على قيد الحياة في بيئته. يثبت هذا أنه تحد صعب بشكل مدهش لمبرمجي الروبوتات المبتدئين.
مدخلات التحكم: مجسات
هناك العديد من الطرق المختلفة التي يمكن أن يتم بها تجهيز الروبوت لمراقبة بيئته. يمكن أن يشمل ذلك أي شيء من مستشعرات القرب ، وأجهزة استشعار الضوء ، والمصدات ، والكاميرات ، وما إلى ذلك. بالإضافة إلى ذلك ، قد تتواصل الروبوتات مع أجهزة استشعار خارجية تمنحها معلومات لا تستطيع هي نفسها مراقبتها بشكل مباشر.
تم تجهيز الروبوت المرجعي الخاص بنا بتسعة مستشعرات تعمل بالأشعة تحت الحمراء - يحتوي الطراز الأحدث على ثمانية مستشعرات تقارب تعمل بالأشعة تحت الحمراء وخمسة أجهزة استشعار تقارب فوق صوتية - مرتبة في "تنورة" في كل اتجاه. يوجد عدد أكبر من المستشعرات التي تواجه مقدمة الروبوت أكثر من الجزء الخلفي ، لأنه عادة ما يكون أكثر أهمية بالنسبة للروبوت أن يعرف ما هو أمامه من ما هو خلفه.
بالإضافة إلى مستشعرات القرب ، يمتلك الروبوت زوجًا من مؤشرات العجلات التي تتعقب حركة العجلة. تسمح لك هذه بتتبع عدد الدورات التي تقوم بها كل عجلة ، مع دوران واحد كامل للأمام للعجلة يبلغ 2765 نقرة. ينعطف في الاتجاه المعاكس عد إلى الوراء ، ويقلل من العد بدلاً من زيادته. لا داعي للقلق بشأن أرقام محددة في هذا البرنامج التعليمي لأن البرنامج الذي سنكتبه يستخدم المسافة المقطوعة معبرًا عنها بالأمتار. سأوضح لك لاحقًا كيفية حسابها من القراد باستخدام وظيفة Python سهلة.
مخرجات التحكم: التنقل
بعض الروبوتات تتحرك على أرجل. يتدحرج البعض مثل الكرة. حتى أن البعض انزلق مثل الثعبان.
إن الروبوت الخاص بنا هو روبوت ذو محرك تفاضلي ، مما يعني أنه يدور على عجلتين. عندما تدور كلتا العجلتين بنفس السرعة ، يتحرك الروبوت في خط مستقيم. عندما تتحرك العجلات بسرعات مختلفة ، يستدير الروبوت. وبالتالي ، فإن التحكم في حركة هذا الروبوت يؤدي إلى التحكم بشكل صحيح في معدلات دوران كل من هاتين العجلتين.
API
في Sobot Rimulator ، يتم تجسيد الفصل بين "كمبيوتر" الروبوت والعالم المادي (المحاكى) بواسطة ملف robot_supervisor_interface.py
، والذي يحدد واجهة برمجة التطبيقات بالكامل للتفاعل مع أجهزة استشعار ومحركات "الروبوت الحقيقي":
-
read_proximity_sensors()
مصفوفة من تسع قيم بالتنسيق الأصلي لأجهزة الاستشعار -
read_wheel_encoders()
مصفوفة من قيمتين تشير إلى إجمالي العلامات منذ البداية -
set_wheel_drive_rates( v_l, v_r )
قيمتين (بالراديان في الثانية) وتضبط السرعة اليمنى واليسرى للعجلات على هاتين القيمتين
تستخدم هذه الواجهة داخليًا كائنًا آليًا يوفر البيانات من أجهزة الاستشعار وإمكانية تحريك المحركات أو العجلات. إذا كنت ترغب في إنشاء روبوت مختلف ، فعليك ببساطة توفير فئة روبوت Python مختلفة يمكن استخدامها بواسطة نفس الواجهة ، وستعمل بقية الكود (وحدات التحكم والمشرف والمحاكي) خارج الصندوق!
المحاكي
نظرًا لأنك تستخدم روبوتًا حقيقيًا في العالم الحقيقي دون إيلاء الكثير من الاهتمام لقوانين الفيزياء المعنية ، يمكنك تجاهل كيفية محاكاة الروبوت وتخطي مباشرة إلى كيفية برمجة برنامج وحدة التحكم ، حيث سيكون هو نفسه تقريبًا بين العالم الحقيقي والمحاكاة. ولكن إذا كنت تشعر بالفضول ، فسوف أعرضه هنا بإيجاز.
ملف world.py
هو فئة Python التي تمثل العالم المحاكي ، مع وجود روبوتات وعقبات بداخله. تهتم وظيفة الخطوة داخل هذه الفئة بتطوير عالمنا البسيط من خلال:
- تطبيق قواعد الفيزياء على حركات الروبوت
- النظر في الاصطدامات مع العوائق
- توفير قيم جديدة لأجهزة الاستشعار الروبوتية
في النهاية ، تستدعي المشرفين الآليين المسؤولين عن تنفيذ برنامج دماغ الإنسان الآلي.
يتم تنفيذ وظيفة الخطوة في حلقة بحيث robot.step_motion()
بتحريك الروبوت باستخدام سرعة العجلة التي يحسبها المشرف في خطوة المحاكاة السابقة.
# step the simulation through one time interval def step( self ): dt = self.dt # step all the robots for robot in self.robots: # step robot motion robot.step_motion( dt ) # apply physics interactions self.physics.apply_physics() # NOTE: The supervisors must run last to ensure they are observing the "current" world # step all of the supervisors for supervisor in self.supervisors: supervisor.step( dt ) # increment world time self.world_time += dt
تعمل وظيفة apply_physics()
داخليًا على تحديث قيم مستشعرات القرب من الروبوت بحيث يتمكن المشرف من تقدير البيئة في خطوة المحاكاة الحالية. تنطبق نفس المفاهيم على أجهزة التشفير.
نموذج بسيط
أولاً ، سيكون لدى الروبوت نموذج بسيط للغاية. سيقدم العديد من الافتراضات حول العالم. بعض من أهمها ما يلي:
- التضاريس دائمًا مسطحة وحتى
- العوائق ليست مستديرة أبدا
- العجلات لا تنزلق أبدًا
- لا شيء يمكن أن يدفع الروبوت في الأرجاء
- لا تفشل المستشعرات أبدًا أو تعطي قراءات خاطئة
- تدور العجلات دائمًا عندما يُطلب منهم ذلك
على الرغم من أن معظم هذه الافتراضات معقولة داخل بيئة شبيهة بالمنزل ، فقد تكون هناك عقبات مستديرة. يتميز برنامج تجنب العقبات الخاص بنا بتنفيذ بسيط ويتبع حدود العقبات من أجل الالتفاف عليها. سنقوم بتلميح القراء حول كيفية تحسين إطار التحكم في الروبوت الخاص بنا من خلال فحص إضافي لتجنب العوائق الدائرية.
حلقة التحكم
سندخل الآن في جوهر برنامج التحكم الخاص بنا ونوضح السلوكيات التي نريد برمجتها داخل الروبوت. يمكن إضافة سلوكيات إضافية إلى هذا الإطار ، ويجب أن تجرب أفكارك الخاصة بعد الانتهاء من القراءة! تم اقتراح برنامج الروبوتات القائمة على السلوك منذ أكثر من 20 عامًا ولا يزال أداة قوية للروبوتات المتنقلة. على سبيل المثال ، في عام 2007 تم استخدام مجموعة من السلوكيات في DARPA Urban Challenge - أول مسابقة للسيارات ذاتية القيادة!
الروبوت هو نظام ديناميكي. حالة الروبوت ، وقراءات أجهزة الاستشعار الخاصة به ، وتأثيرات إشارات التحكم في تدفق مستمر. يتضمن التحكم في طريقة سير الأحداث الخطوات الثلاث التالية:
- تطبيق إشارات التحكم.
- قس النتائج.
- توليد إشارات تحكم جديدة محسوبة لتقريبنا من هدفنا.
تتكرر هذه الخطوات مرات ومرات حتى نحقق هدفنا. كلما استطعنا القيام بذلك في الثانية ، أصبح لدينا سيطرة أفضل على النظام. يكرر روبوت Sobot Rimulator هذه الخطوات 20 مرة في الثانية (20 هرتز) ، ولكن يجب على العديد من الروبوتات القيام بذلك آلاف أو ملايين المرات في الثانية من أجل الحصول على تحكم مناسب. تذكر مقدمتنا السابقة حول لغات برمجة الروبوتات المختلفة لأنظمة الروبوتات المختلفة ومتطلبات السرعة.
بشكل عام ، في كل مرة يأخذ الروبوت الخاص بنا قياسات باستخدام مستشعراته ، فإنه يستخدم هذه القياسات لتحديث تقديره الداخلي لحالة العالم - على سبيل المثال ، المسافة من هدفه. يقارن هذه الحالة بالقيمة المرجعية لما تريده أن تكون الحالة (بالنسبة للمسافة ، تريدها أن تكون صفراً) ، وتحسب الخطأ بين الحالة المرغوبة والحالة الفعلية. بمجرد معرفة هذه المعلومات ، يمكن تقليل إنشاء إشارات تحكم جديدة إلى مشكلة تقليل الخطأ إلى الحد الأدنى مما سيؤدي في النهاية إلى تحريك الروبوت نحو الهدف.
حيلة ذكية: تبسيط النموذج
للتحكم في الروبوت الذي نريد برمجته ، يتعين علينا إرسال إشارة إلى العجلة اليسرى تخبرها بمدى سرعة الدوران ، وإشارة منفصلة إلى العجلة اليمنى تخبرها بمدى سرعة الدوران. دعنا نسمي هذه الإشارات v L و v R. ومع ذلك ، فإن التفكير المستمر فيما يتعلق بـ v L و v R مرهق للغاية. بدلاً من السؤال ، "ما السرعة التي نريد أن تدور بها العجلة اليسرى ، وما مدى السرعة التي نريد أن تدور بها العجلة اليمنى؟" من الطبيعي أن نسأل ، "ما السرعة التي نريد أن يتحرك بها الروبوت للأمام ، وما السرعة التي نريدها أن يدور بها ، أو يغير اتجاهها؟" دعنا نسمي هذه المعلمات السرعة v والسرعة الزاوية (الدوران) ω (اقرأ "أوميغا"). اتضح أنه يمكننا بناء نموذجنا بالكامل على v و بدلاً من v L و v R ، وفقط بمجرد أن نحدد الطريقة التي نريد أن يتحرك بها الروبوت المبرمج ، قم بتحويل هاتين القيمتين رياضياً إلى v L و v R التي نحتاجها للتحكم الفعلي في عجلات الروبوت. يُعرف هذا بنموذج الدراجة الأحادية للتحكم.
هذا هو كود Python الذي ينفذ التحول النهائي في supervisor.py
. لاحظ أنه إذا كانت تساوي 0 ، فستدور كلتا العجلتين بنفس السرعة:
# generate and send the correct commands to the robot def _send_robot_commands( self ): # ... v_l, v_r = self._uni_to_diff( v, omega ) self.robot.set_wheel_drive_rates( v_l, v_r ) def _uni_to_diff( self, v, omega ): # v = translational velocity (m/s) # omega = angular velocity (rad/s) R = self.robot_wheel_radius L = self.robot_wheel_base_length v_l = ( (2.0 * v) - (omega*L) ) / (2.0 * R) v_r = ( (2.0 * v) + (omega*L) ) / (2.0 * R) return v_l, v_r
تقدير الحالة: روبوت ، اعرف نفسك
باستخدام أجهزة الاستشعار الخاصة به ، يجب أن يحاول الروبوت تقدير حالة البيئة بالإضافة إلى حالتها الخاصة. لن تكون هذه التقديرات مثالية أبدًا ، لكنها يجب أن تكون جيدة إلى حد ما لأن الروبوت سوف يعتمد في جميع قراراته على هذه التقديرات. باستخدام مستشعرات القرب ومؤشرات العجلة وحدها ، يجب أن تحاول تخمين ما يلي:
- الاتجاه إلى العقبات
- المسافة من العوائق
- موقف الروبوت
- عنوان الروبوت
يتم تحديد الخواص الأولى والثانية من خلال قراءات مستشعر القرب وهما واضحان إلى حد ما. تقوم وظيفة API read_proximity_sensors()
بإرجاع مصفوفة من تسع قيم ، واحدة لكل مستشعر. نعلم مسبقًا أن القراءة السابعة ، على سبيل المثال ، تتوافق مع المستشعر الذي يشير 75 درجة إلى يمين الروبوت.
وبالتالي ، إذا أظهرت هذه القيمة قراءة مناظرة لمسافة 0.1 متر ، فإننا نعلم أن هناك عائقًا على بعد 0.1 مترًا ، و 75 درجة جهة اليسار. إذا لم يكن هناك عائق ، فسيرجع المستشعر قراءة لمدى أقصى يبلغ 0.2 متر. وبالتالي ، إذا قرأنا 0.2 متر على المستشعر السابع ، سنفترض أنه لا يوجد في الواقع أي عائق في هذا الاتجاه.
نظرًا للطريقة التي تعمل بها مستشعرات الأشعة تحت الحمراء (قياس انعكاس الأشعة تحت الحمراء) ، فإن الأرقام التي ترجعها هي عبارة عن تحويل غير خطي للمسافة الفعلية المكتشفة. وبالتالي ، فإن دالة بايثون لتحديد المسافة المشار إليها يجب أن تحول هذه القراءات إلى أمتار. يتم ذلك في supervisor.py
النحو التالي:
# update the distances indicated by the proximity sensors def _update_proximity_sensor_distances( self ): self.proximity_sensor_distances = [ 0.02-( log(readval/3960.0) )/30.0 for readval in self.robot.read_proximity_sensors() ]
مرة أخرى ، لدينا نموذج مستشعر محدد في إطار عمل روبوت Python هذا ، بينما في العالم الحقيقي ، تأتي المستشعرات مع البرامج المصاحبة التي يجب أن توفر وظائف تحويل مماثلة من القيم غير الخطية إلى الأمتار.

يعد تحديد موضع وعنوان الروبوت (المعروف معًا باسم الوضع في برمجة الروبوتات) أكثر صعوبة إلى حد ما. يستخدم الروبوت الخاص بنا قياس المسافات لتقدير شكله. هذا هو المكان الذي تأتي فيه مؤشرات العجلة. من خلال قياس مقدار دوران كل عجلة منذ آخر تكرار لحلقة التحكم ، من الممكن الحصول على تقدير جيد لكيفية تغير وضع الروبوت - ولكن فقط إذا كان التغيير صغيرًا .
هذا هو أحد الأسباب التي تجعل من المهم تكرار حلقة التحكم بشكل متكرر في إنسان آلي حقيقي ، حيث قد لا تكون المحركات التي تحرك العجلات مثالية. إذا انتظرنا وقتًا طويلاً لقياس مؤشرات العجلة ، كان من الممكن أن تقوم كلتا العجلتين بالكثير ، وسيكون من المستحيل تقدير المكان الذي انتهينا إليه.
بالنظر إلى برنامج المحاكاة الحالي لدينا ، يمكننا تحمل تشغيل حساب قياس المسافات عند 20 هرتز - نفس التردد مثل وحدات التحكم. ولكن قد تكون فكرة جيدة أن يكون لديك مؤشر ترابط Python منفصل يعمل بشكل أسرع للقبض على الحركات الأصغر للمؤشرات.
يوجد أدناه وظيفة قياس المسافات الكاملة في supervisor.py
الذي يقوم بتحديث تقدير وضع الروبوت. لاحظ أن وضع الروبوت يتكون من الإحداثيين x
و y
، والعنوان theta
، والذي يتم قياسه بالراديان من المحور X الموجب. موجب x
في الشرق وموجب y
في الشمال. وهكذا فإن العنوان 0
يشير إلى أن الروبوت يواجه الشرق مباشرة. يفترض الروبوت دائمًا أن وضعه الأولي هو (0, 0), 0
.
# update the estimated position of the robot using it's wheel encoder readings def _update_odometry( self ): R = self.robot_wheel_radius N = float( self.wheel_encoder_ticks_per_revolution ) # read the wheel encoder values ticks_left, ticks_right = self.robot.read_wheel_encoders() # get the difference in ticks since the last iteration d_ticks_left = ticks_left - self.prev_ticks_left d_ticks_right = ticks_right - self.prev_ticks_right # estimate the wheel movements d_left_wheel = 2*pi*R*( d_ticks_left / N ) d_right_wheel = 2*pi*R*( d_ticks_right / N ) d_center = 0.5 * ( d_left_wheel + d_right_wheel ) # calculate the new pose prev_x, prev_y, prev_theta = self.estimated_pose.scalar_unpack() new_x = prev_x + ( d_center * cos( prev_theta ) ) new_y = prev_y + ( d_center * sin( prev_theta ) ) new_theta = prev_theta + ( ( d_right_wheel - d_left_wheel ) / self.robot_wheel_base_length ) # update the pose estimate with the new values self.estimated_pose.scalar_update( new_x, new_y, new_theta ) # save the current tick count for the next iteration self.prev_ticks_left = ticks_left self.prev_ticks_right = ticks_right
الآن وقد أصبح الروبوت الخاص بنا قادرًا على إنشاء تقدير جيد للعالم الحقيقي ، فلنستخدم هذه المعلومات لتحقيق أهدافنا.
طرق برمجة Python Robot: Go-to-Goal Behavior
الغرض الأسمى من وجود الروبوت الصغير الخاص بنا في هذا البرنامج التعليمي للبرمجة هو الوصول إلى نقطة الهدف. فكيف نجعل العجلات تدور لتصل إلى هناك؟ لنبدأ بتبسيط نظرتنا للعالم قليلاً ونفترض أنه لا توجد عقبات في الطريق.
تصبح هذه بعد ذلك مهمة بسيطة ويمكن برمجتها بسهولة في Python. إذا تقدمنا إلى الأمام أثناء مواجهة الهدف ، سنصل إلى هناك. بفضل قياس المسافات لدينا ، نحن نعرف ما هي إحداثياتنا الحالية والعنوان. نعلم أيضًا ما هي إحداثيات الهدف لأنها تمت برمجتها مسبقًا. لذلك ، باستخدام القليل من الجبر الخطي ، يمكننا تحديد المتجه من موقعنا إلى الهدف ، كما في go_to_goal_controller.py
:
# return a go-to-goal heading vector in the robot's reference frame def calculate_gtg_heading_vector( self ): # get the inverse of the robot's pose robot_inv_pos, robot_inv_theta = self.supervisor.estimated_pose().inverse().vector_unpack() # calculate the goal vector in the robot's reference frame goal = self.supervisor.goal() goal = linalg.rotate_and_translate_vector( goal, robot_inv_theta, robot_inv_pos ) return goal
لاحظ أننا نحصل على المتجه إلى الهدف في الإطار المرجعي للروبوت ، وليس في إحداثيات العالم. إذا كان الهدف على المحور X في الإطار المرجعي للروبوت ، فهذا يعني أنه أمام الروبوت مباشرة. وبالتالي ، فإن زاوية هذا المتجه من المحور X هي الفرق بين العنوان والعنوان الذي نريد أن نكون عليه. بمعنى آخر ، إنه الخطأ بين حالتنا الحالية وما نريد أن تكون عليه حالتنا الحالية. لذلك ، نريد تعديل معدل الدوران الخاص بنا ω بحيث تتغير الزاوية بين العنوان والهدف باتجاه 0. نريد تقليل الخطأ إلى الحد الأدنى:
# calculate the error terms theta_d = atan2( self.gtg_heading_vector[1], self.gtg_heading_vector[0] ) # calculate angular velocity omega = self.kP * theta_d
self.kP
في المقتطف أعلاه لتطبيق Python للتحكم هو كسب تحكم. إنه معامل يحدد مدى السرعة التي نتحول بها بما يتناسب مع بُعدنا عن الهدف الذي نواجهه. إذا كان الخطأ في العنوان 0
، فإن معدل الدوران هو أيضًا 0
. في دالة Python الحقيقية داخل الملف go_to_goal_controller.py
، سترى المزيد من المكاسب المماثلة ، نظرًا لأننا استخدمنا وحدة تحكم PID بدلاً من معامل نسبي بسيط.
الآن بعد أن أصبح لدينا السرعة الزاوية ω ، كيف نحدد السرعة الأمامية v ؟ القاعدة العامة الجيدة هي تلك التي ربما تعرفها غريزيًا: إذا لم نقم بإجراء منعطف ، فيمكننا المضي قدمًا بأقصى سرعة ، وبعد ذلك كلما قمنا بالتدوير بشكل أسرع ، يجب أن نتباطأ أكثر. يساعدنا هذا بشكل عام في الحفاظ على استقرار نظامنا والعمل ضمن حدود نموذجنا. وبالتالي ، v هي دالة ω . في go_to_goal_controller.py
المعادلة هي:
# calculate translational velocity # velocity is v_max when omega is 0, # drops rapidly to zero as |omega| rises v = self.supervisor.v_max() / ( abs( omega ) + 1 )**0.5
اقتراح لتوضيح هذه الصيغة هو اعتبار أننا عادة ما نبطئ عندما نقترب من الهدف من أجل الوصول إليه بسرعة صفر. كيف ستتغير هذه الصيغة؟ يجب أن تتضمن بطريقة ما استبدال v_max()
بشيء يتناسب مع المسافة. حسنًا ، لقد أكملنا تقريبًا حلقة تحكم واحدة. الشيء الوحيد المتبقي هو تحويل هاتين المعلمتين لنموذج الدراجة الأحادية إلى سرعات عجلة تفاضلية ، وإرسال الإشارات إلى العجلات. إليك مثال على مسار الروبوت تحت وحدة التحكم في Go-to-Go ، بدون عوائق:
كما نرى ، فإن المتجه إلى الهدف هو مرجع فعال لنا لنبني عليه حسابات التحكم الخاصة بنا. إنه تمثيل داخلي لـ "أين نريد أن نذهب". كما سنرى ، فإن الاختلاف الرئيسي الوحيد بين سلوكيات go-to-target والسلوكيات الأخرى هو أن الذهاب نحو الهدف أحيانًا يكون فكرة سيئة ، لذلك يجب علينا حساب متجه مرجعي مختلف.
طرق برمجة روبوت بايثون: سلوك تجنب العقبات
إن التوجه نحو الهدف عندما يكون هناك عائق في هذا الاتجاه هو مثال على ذلك. بدلاً من الانشغال بالأشياء التي تعترض طريقنا ، دعونا نحاول برمجة قانون تحكم يجعل الروبوت يتجنبها.
لتبسيط السيناريو ، دعنا ننسى الآن نقطة الهدف تمامًا ونجعل هدفنا التالي: عندما لا تكون هناك عقبات أمامنا ، المضي قدمًا. عند مواجهة عقبة ، ابتعد عنها حتى تختفي أمامنا.
وفقًا لذلك ، عندما لا يكون هناك عائق أمامنا ، نريد أن يشير متجهنا المرجعي ببساطة إلى الأمام. إذن ω ستكون صفرًا و v ستكون السرعة القصوى. ومع ذلك ، بمجرد اكتشاف عقبة مع مستشعرات القرب الخاصة بنا ، نريد أن يشير المتجه المرجعي إلى أي اتجاه بعيد عن العائق. سيؤدي ذلك إلى ω إطلاق النار لإبعادنا عن العائق ، ويسبب v إلى السقوط للتأكد من أننا لا نصطدم عن طريق الخطأ بالعائق في العملية.
هناك طريقة رائعة لإنشاء المتجه المرجعي المرغوب فيه وهي تحويل قراءات القرب التسعة إلى متجهات ، وأخذ مجموع مرجح. عندما لا يتم اكتشاف أي عوائق ، سيتم جمع المتجهات بشكل متماثل ، مما يؤدي إلى متجه مرجعي يشير إلى الأمام مباشرة حسب الرغبة. ولكن إذا التقط المستشعر الموجود على الجانب الأيمن ، على سبيل المثال ، عقبة ، فسيساهم متجهًا أصغر في المجموع ، وستكون النتيجة متجهًا مرجعيًا يتم إزاحته نحو اليسار.
بالنسبة للروبوت العام مع وضع مختلف لأجهزة الاستشعار ، يمكن تطبيق نفس الفكرة ولكنها قد تتطلب تغييرات في الأوزان و / أو عناية إضافية عندما تكون المستشعرات متماثلة أمام الروبوت وخلفه ، حيث يمكن أن يصبح المجموع المرجح صفرًا .
إليك الكود الذي يفعل ذلك في avoid_obstacles_controller.py
:
# sensor gains (weights) self.sensor_gains = [ 1.0+( (0.4*abs(p.theta)) / pi ) for p in supervisor.proximity_sensor_placements() ] # ... # return an obstacle avoidance vector in the robot's reference frame # also returns vectors to detected obstacles in the robot's reference frame def calculate_ao_heading_vector( self ): # initialize vector obstacle_vectors = [ [ 0.0, 0.0 ] ] * len( self.proximity_sensor_placements ) ao_heading_vector = [ 0.0, 0.0 ] # get the distances indicated by the robot's sensor readings sensor_distances = self.supervisor.proximity_sensor_distances() # calculate the position of detected obstacles and find an avoidance vector robot_pos, robot_theta = self.supervisor.estimated_pose().vector_unpack() for i in range( len( sensor_distances ) ): # calculate the position of the obstacle sensor_pos, sensor_theta = self.proximity_sensor_placements[i].vector_unpack() vector = [ sensor_distances[i], 0.0 ] vector = linalg.rotate_and_translate_vector( vector, sensor_theta, sensor_pos ) obstacle_vectors[i] = vector # store the obstacle vectors in the robot's reference frame # accumulate the heading vector within the robot's reference frame ao_heading_vector = linalg.add( ao_heading_vector, linalg.scale( vector, self.sensor_gains[i] ) ) return ao_heading_vector, obstacle_vectors
باستخدام ao_heading_vector
الناتج كمرجع لدينا لمحاولة الروبوت المطابقة ، فيما يلي نتائج تشغيل برنامج الروبوت في المحاكاة باستخدام وحدة التحكم في تجنب العوائق فقط ، وتجاهل نقطة الهدف تمامًا. يقفز الروبوت بلا هدف ، لكنه لا يصطدم أبدًا بأي عائق ، بل إنه قادر على التنقل في بعض المساحات الضيقة جدًا:
طرق برمجة Python Robot: Hybrid Automata (آلة حالة السلوك)
لقد وصفنا حتى الآن سلوكين - الذهاب إلى الهدف وتجنب العقبات - في عزلة. كلاهما يؤدي وظيفتهما بشكل رائع ، ولكن من أجل الوصول إلى الهدف بنجاح في بيئة مليئة بالعقبات ، نحتاج إلى الجمع بينهما.
يكمن الحل الذي سنطوره في فئة من الآلات تتميز بالتصنيف الرائع للغاية للأوتوماتا الهجينة . تمت برمجة الآلة الهجينة بالعديد من السلوكيات ، أو الأنماط المختلفة ، بالإضافة إلى آلة الدولة الإشرافية. تنتقل آلة الحالة المشرفة من وضع إلى آخر في أوقات منفصلة (عندما تتحقق الأهداف أو تتغير البيئة كثيرًا بشكل مفاجئ) ، بينما يستخدم كل سلوك أجهزة استشعار وعجلات للتفاعل بشكل مستمر مع التغيرات البيئية. كان يسمى المحلول الهجين لأنه يتطور بطريقة منفصلة ومستمرة.
يقوم إطار عمل روبوت Python الخاص بنا بتنفيذ آلة الحالة في ملف supervisor_state_machine.py
.
مجهزًا بسلوكين مفيدين لدينا ، يقترح منطق بسيط نفسه: عندما لا يتم اكتشاف عقبة ، استخدم سلوك go-to-target. عند اكتشاف عائق ، قم بالتبديل إلى سلوك تجنب العوائق حتى يتوقف اكتشاف العائق.
لكن كما اتضح ، فإن هذا المنطق سوف ينتج عنه الكثير من المشاكل. ما سيفعله هذا النظام عندما يواجه عقبة هو الابتعاد عنه ، وبمجرد أن يبتعد عنه ، استدر يمينًا وركض نحوه مرة أخرى. والنتيجة هي حلقة لا نهاية لها من التبديل السريع التي تجعل الروبوت عديم الفائدة. In the worst case, the robot may switch between behaviors with every iteration of the control loop—a state known as a Zeno condition .
There are multiple solutions to this problem, and readers that are looking for deeper knowledge should check, for example, the DAMN software architecture.
What we need for our simple simulated robot is an easier solution: One more behavior specialized with the task of getting around an obstacle and reaching the other side.
Python Robot Programming Methods: Follow-Wall Behavior
Here's the idea: When we encounter an obstacle, take the two sensor readings that are closest to the obstacle and use them to estimate the surface of the obstacle. Then, simply set our reference vector to be parallel to this surface. Keep following this wall until A) the obstacle is no longer between us and the goal, and B) we are closer to the goal than we were when we started. Then we can be certain we have navigated the obstacle properly.
With our limited information, we can't say for certain whether it will be faster to go around the obstacle to the left or to the right. To make up our minds, we select the direction that will move us closer to the goal immediately. To figure out which way that is, we need to know the reference vectors of the go-to-goal behavior and the avoid-obstacle behavior, as well as both of the possible follow-wall reference vectors. Here is an illustration of how the final decision is made (in this case, the robot will choose to go left):
Determining the follow-wall reference vectors turns out to be a bit more involved than either the avoid-obstacle or go-to-goal reference vectors. Take a look at the Python code in follow_wall_controller.py
to see how it's done.
Final Control Design
The final control design uses the follow-wall behavior for almost all encounters with obstacles. However, if the robot finds itself in a tight spot, dangerously close to a collision, it will switch to pure avoid-obstacles mode until it is a safer distance away, and then return to follow-wall. Once obstacles have been successfully negotiated, the robot switches to go-to-goal. Here is the final state diagram, which is programmed inside the supervisor_state_machine.py
:
Here is the robot successfully navigating a crowded environment using this control scheme:
An additional feature of the state machine that you can try to implement is a way to avoid circular obstacles by switching to go-to-goal as soon as possible instead of following the obstacle border until the end (which does not exist for circular objects!)
Tweak, Tweak, Tweak: Trial and Error
The control scheme that comes with Sobot Rimulator is very finely tuned. It took many hours of tweaking one little variable here, and another equation there, to get it to work in a way I was satisfied with. Robotics programming often involves a great deal of plain old trial-and-error. Robots are very complex and there are few shortcuts to getting them to behave optimally in a robot simulator environment…at least, not much short of outright machine learning, but that's a whole other can of worms.
I encourage you to play with the control variables in Sobot Rimulator and observe and attempt to interpret the results. Changes to the following all have profound effects on the simulated robot's behavior:
- The error gain
kP
in each controller - The sensor gains used by the avoid-obstacles controller
- The calculation of v as a function of ω in each controller
- The obstacle standoff distance used by the follow-wall controller
- The switching conditions used by
supervisor_state_machine.py
- Pretty much anything else
When Programmable Robots Fail
We've done a lot of work to get to this point, and this robot seems pretty clever. Yet, if you run Sobot Rimulator through several randomized maps, it won't be long before you find one that this robot can't deal with. Sometimes it drives itself directly into tight corners and collides. Sometimes it just oscillates back and forth endlessly on the wrong side of an obstacle. Occasionally it is legitimately imprisoned with no possible path to the goal. After all of our testing and tweaking, sometimes we must come to the conclusion that the model we are working with just isn't up to the job, and we have to change the design or add functionality.
In the mobile robot universe, our little robot's “brain” is on the simpler end of the spectrum. Many of the failure cases it encounters could be overcome by adding some more advanced software to the mix. More advanced robots make use of techniques such as mapping , to remember where it's been and avoid trying the same things over and over; heuristics , to generate acceptable decisions when there is no perfect decision to be found; and machine learning , to more perfectly tune the various control parameters governing the robot's behavior.
A Sample of What's to Come
Robots are already doing so much for us, and they are only going to be doing more in the future. While even basic robotics programming is a tough field of study requiring great patience, it is also a fascinating and immensely rewarding one.
In this tutorial, we learned how to develop reactive control software for a robot using the high-level programming language Python. But there are many more advanced concepts that can be learned and tested quickly with a Python robot framework similar to the one we prototyped here. I hope you will consider getting involved in the shaping of things to come!
Acknowledgement: I would like to thank Dr. Magnus Egerstedt and Jean-Pierre de la Croix of the Georgia Institute of Technology for teaching me all this stuff, and for their enthusiasm for my work on Sobot Rimulator.