OmObiArm: بناء روبوت متحرك بذراع روبوتية مدمجة باستخدام Arduino مع التحكم عبر البلوتوث

يحتوي هذا المقال على روابط تسويق بالعمولة مُعلَّمة بعلامة *. بصفتي شريكًا في برنامج Amazon Associates، أربح من عمليات الشراء المؤهلة، دون أي تكلفة إضافية عليك. يساعد ذلك على إبقاء الشروحات مجانية.

يُحمَّل الفيديو من YouTube (Google)، وقد يضع ملفات تعريف الارتباط (Cookies). سياسة الخصوصية

📋 ملخص سريع

OmObiArm مركبة جوالة (Rover) بأربع عجلات تعلوها ذراع روبوتية بستة محركات سيرفو. للذراع خمسة مفاصل إضافة إلى القابض. تتحكم لوحة Arduino Mega 2560 في الاثنين معًا: في محركات التيار المستمر الأربعة عبر Adafruit Motor Shield V1.2، وفي محركات السيرفو عبر لوحة قيادة السيرفو PCA9685. ويرسل تطبيق Android مبني في MIT App Inventor الأوامر عبر وحدة البلوتوث HC-05.

يجمع OmObiArm بين اثنين من مشاريعي السابقة: OmObi، المركبة الجوالة التي يُتحكَّم فيها عبر البلوتوث، و OmArm، الذراع الروبوتية ذات التحكم عبر البلوتوث. تُثبَّت الذراع على اللوح العلوي للمركبة، وتشغّل لوحة Arduino Mega واحدة الاثنين معًا. تتحرك المركبة إلى الأمام والخلف وتدور في مكانها (التوجيه الانزلاقي Skid Steering: تدور العجلات اليسرى واليمنى في اتجاهين متعاكسين). تحرّك ستة منزلقات في التطبيق مفاصل الذراع، ويسجّل زر Save أوضاع الذراع، ويعيد زر Play تشغيلها. في الاختبار في نهاية الفيديو، حفظت عدة أوضاع ثم بدأت التشغيل، فرفع الروبوت جسمًا ونقله إلى مكان آخر.

OmObiArm picking up a toy car with its gripper

كيف يعمل OmObiArm

لوحة Arduino Mega 2560 هي وحدة التحكم المركزية. تستقبل الأوامر من الهاتف عبر الوحدة HC-05 على أطراف المنفذ التسلسلي العتادي (RX0 وTX0، الطرفان 0 و1). وبحسب الأمر، تقود المركبة أو تحرّك الذراع.

للمركبة، تستخدم لوحة Arduino درع المحركات Adafruit Motor Shield V1.2. يُركَّب الدرع مباشرة فوق Arduino Mega ويقود محركات التيار المستمر الأربعة عبر شرائح L293D. كل شريحة L293D جسر H مزدوج (H-bridge: دائرة ترانزستورات تتيح تشغيل محرك التيار المستمر في الاتجاهين). شرحت طريقة عمل الدرع في درس سيارة الروبوت المتحكَّم فيها عبر البلوتوث باستخدام Adafruit Motor Shield.

التحكم في الذراع والاتصال اللاسلكي

للذراع، تتواصل لوحة Arduino مع لوحة قيادة السيرفو PCA9685 ذات 16 قناة عبر I2C (ناقل بسلكين: SDA ينقل البيانات وSCL ينقل إشارة الساعة). تولّد PCA9685 إشارات PWM (تعديل عرض النبضة: طول النبضة يحدد زاوية السيرفو) لستة محركات سيرفو على القنوات 0 و4 و8 و9 و12 و13. القنوات غير متتالية؛ فالمصفوفة servoChannels في الكود تربط محركات السيرفو من 1 إلى 6 بهذه القنوات. تحرّك ثلاثة محركات سيرفو MG996R القاعدة والكتف والكوع، وهي التي تتحمل الحمل الأكبر، وتحرّك ثلاثة محركات سيرفو مصغّرة SG90 المعصم والقابض.

تحصل محركات السيرفو على طاقتها من محوّل خافض للجهد LM2596 (Buck Converter: وحدة DC-DC صغيرة قابلة للضبط لخفض الجهد) يخفض جهد بطارية LiPo (ليثيوم بوليمر، انظر 4.2) من 7.4 V إلى 6 V. وبذلك لا يمر تيار السيرفو أبدًا عبر منظم الجهد في لوحة Arduino. ولربط الروبوتين كهربائيًا، أضفت LM2596 وPCA9685 إلى إلكترونيات المركبة. لأساسيات السيرفو وPCA9685، راجع ما شرحته في دليل التحكم بمحرك السيرفو مع Arduino.

يرسل تطبيق Android، المبني باستخدام MIT App Inventor، أوامر نصية قصيرة عبر البلوتوث. ويسرد جدول مرجع الأوامر في الخطوة 5 جميع هذه الأوامر.

المكونات

تحتاج إلى القطع المطبوعة بالطابعة ثلاثية الأبعاد (3D) لمركبة OmObi وذراع OmArm، إضافة إلى الإلكترونيات والمحركات وأدوات التثبيت المذكورة أدناه.

نظرة عامة

الفئةالمكونات
وحدة التحكمArduino Mega 2560
وحدة قيادة المحركاتAdafruit Motor Shield V1.2 (L293D)
لوحة قيادة السيرفولوحة قيادة السيرفو PCA9685 بـ 16 قناة عبر I2C
بلوتوثوحدة HC-05
المحركات4 محركات تيار مستمر بعلب تروس، 4 عجلات
محركات السيرفو3 محركات MG996R (عزم دوران عالٍ)، 3 محركات SG90 (مصغّرة)
الطاقةبطارية LiPo بجهد 7.4 V من نوع 2S (5300 mAh)، ومحوّل خافض للجهد LM2596 مضبوط على 6 V، ومفتاح طاقة
القطع المطبوعة بالطابعة ثلاثية الأبعاد (3D)الهيكل، واللوح العلوي، ووصلات الذراع، والقابض مع التروس والجُلَب، ومحور المفصل، والأغطية، وحامل Arduino (من تصميمَي OmObi وOmArm)
الملحقاتأسلاك التوصيل (Jumper)، ومقاومات 1 kΩ و2 kΩ (مقسم الجهد، انظر 4.5)، وبراغي M3 وM2، وحشوات ملولبة حرارية M3، ونابض شد، ومحمل كروي 6806ZZ (30x42x7 mm)، وأذرع السيرفو (Horns: رافعات صغيرة تُثبَّت بالبراغي على أعمدة محركات السيرفو)

قائمة المواد الكاملة (BOM)

القطع التي تحمل علامة “طباعة ثلاثية الأبعاد (3D)” متوفرة بصيغة ملفات STL في متجر OmArTronics. الأرقام هي أرقام البنود في قائمة القطع الخاصة بنموذج CAD.

القطع المطبوعة بالطابعة ثلاثية الأبعاد (3D)

الرقمالكميةالجزءملاحظات
11القاعدة السفلية (الهيكل)طباعة ثلاثية الأبعاد (3D). الجزء السفلي من الهيكل، يحمل المحركات والإلكترونيات
291إطار الهيكلطباعة ثلاثية الأبعاد (3D). إطار الهيكل
311قاعدة سيارة الروبوتقاعدة مطبوعة بالطابعة ثلاثية الأبعاد (3D)
303الغطاءأغطية مطبوعة بالطابعة ثلاثية الأبعاد (3D)
151وصلة قاعدة الذراعطباعة ثلاثية الأبعاد (3D). تربط الذراع بالمركبة
101قاعدة الذراع العلويةطباعة ثلاثية الأبعاد (3D). الجزء العلوي من قاعدة الذراع
31وصلة الذراع 1طباعة ثلاثية الأبعاد (3D)
141وصلة الذراع 2طباعة ثلاثية الأبعاد (3D)
131وصلة الذراع 3طباعة ثلاثية الأبعاد (3D)
94الوصلةقطع وصلات مطبوعة بالطابعة ثلاثية الأبعاد (3D)
81مجموعة القابضمجموعة القابض الكاملة مطبوعة بالطابعة ثلاثية الأبعاد (3D)
41إصبع القابض الأيمنطباعة ثلاثية الأبعاد (3D)
61ترس القابض الأيمنطباعة ثلاثية الأبعاد (3D). الترس الأيمن لآلية القابض
71ترس القابض الأيسرطباعة ثلاثية الأبعاد (3D). الترس الأيسر لآلية القابض
52جُلبة القابضطباعة ثلاثية الأبعاد (3D). جُلبة لمحور مفصل القابض
181محور المفصلطباعة ثلاثية الأبعاد (3D). محور يثبّت المفاصل في الذراع

الإلكترونيات

الرقمالكميةالجزءملاحظات
321Arduino Mega 2560وحدة التحكم
331وحدة قيادة المحركات L293D (Adafruit Motor Shield V1.2)درع محركات بجسر H مزدوج لمحركات التيار المستمر الأربعة
431لوحة قيادة السيرفو PCA9685لوحة قيادة سيرفو بـ 16 قناة عبر I2C
401وحدة البلوتوث HC-05الاتصال اللاسلكي بالهاتف
421محوّل خافض للجهد LM2596يخفض جهد البطارية إلى 6 V لمحركات السيرفو
341بطارية LiPo 2S بجهد 7.4 V، بسعة 5300 mAhمزود الطاقة الرئيسي
361المفتاحمفتاح الطاقة الرئيسي

المحركات ومحركات السيرفو والعجلات

الرقمالكميةالجزءملاحظات
444محرك تيار مستمر بعلبة تروسنظام دفع المركبة
264ترس المحركترس بلاستيكي لعمود المحرك
284عجلة عربة 4WDعجلة بلاستيكية
274إطار 4WDإطار مطاطي
123سيرفو MG996Rالقاعدة، الكتف، الكوع
113سيرفو مصغّر SG90ميل المعصم (Pitch)، دوران المعصم (Roll)، القابض
203ذراع سيرفو MG955 (Horn)أذرع السيرفو (Horns) لمحركات MG996R (القطعة في نموذج CAD مسمّاة باسم MG995، الذي يستخدم ذراع السيرفو نفسه ذا 25 سنًّا)
213ذراع سيرفو SG90 (Horn)أذرع (Horns) لمحركات SG90

أدوات التثبيت والمحامل والقطع الصغيرة

الرقمالكميةالجزءملاحظات
21JIS B 1521 6806, 30x42x7 mmمحمل كروي ذو أخدود عميق لدوران قاعدة الذراع
171نابض شديوفّر الشد لآلية الوصلات
3516حشوات M3حشوات ملولبة حرارية للقطع المطبوعة
167AS 1420-1973 M3x16برغي برأس أسطواني وتجويف سداسي
3716AS 1420-1973 M3x6براغي مترية ISO برأس أسطواني وتجويف سداسي
384ANSI B18.3.1M M3x0.5×30برغي متري برأس أسطواني وتجويف سداسي مُشكَّل بالسحب (Broached)
394ANSI B18.3.1M M3x0.5×25برغي متري برأس أسطواني وتجويف سداسي مُشكَّل بالسحب (Broached)
242DIN 7985 (Z) M3x6-Zبراغي برأس أسطواني مرتفع (Cheese Head) وتجويف صليبي، النوع Z
253DIN 7985 (Z) M2x6-Zبراغي برأس أسطواني مرتفع (Cheese Head) وتجويف صليبي، النوع Z
191ANSI B18.2.4.2M M3x0.5صامولة سداسية (لولب متري قياسي)
228ANSI B18.6.4 No. 3-28 x 1/2برغي ذاتي اللولبة برأس مقبّب عريض (Truss Head) وتجويف صليبي، النوع AB، النوع I
2316ANSI B18.6.4 No. 2-32 x 3/8برغي ذاتي اللولبة برأس مقبّب عريض (Truss Head) وتجويف صليبي، النوع AB، النوع I
414ANSI B18.6.4 No. 0-48 x 1/4برغي ذاتي اللولبة برأس مقبّب عريض (Truss Head) وتجويف صليبي، النوع AB، النوع I

الخطوة 1: تصميم النموذج ثلاثي الأبعاد (3D)

يجمع OmObiArm بين تصميمَي مركبة OmObi وذراع OmArm. في Autodesk Inventor عدّلت قاعدة OmArm ودمجتها مع اللوح العلوي لمركبة OmObi، فاللوح العلوي هو أيضًا قاعدة الذراع. ولكابلات السيرفو صنعت فتحة في القاعدة، فتمر الكابلات نزولًا إلى داخل المركبة. إذا كنت جديدًا على التصميم ثلاثي الأبعاد (3D) للروبوتات، فابدأ بدرسي مقدمة إلى الطباعة ثلاثية الأبعاد (3D) وAutodesk Inventor. يوضح الرسم الهندسي أدناه الأبعاد الرئيسية ومناظر المقاطع للتجميع.

Technical drawing of OmObiArm with dimensions and sectional views of the chassis and the arm base

الخطوة 2: الطباعة ثلاثية الأبعاد (3D) للقطع

اطبع كل القطع التي تحمل علامة “طباعة ثلاثية الأبعاد (3D)” في قائمة المواد (BOM). يناسب خيط الطباعة PLA معظم القطع. إعدادات الطابعة واختيار المادة مشروحة في درس المقدمة المذكور رابطه في الخطوة 1.

الخطوة 3: تجميع OmObiArm

3.1 تركيب المحركات والعجلات

يحتوي الهيكل على حشوات ملولبة حرارية M3 (جُلَب معدنية ذات لولب داخلي تغرسها في البلاستيك بالحرارة). عندما بنيت مركبة OmObi سخّنتها وضغطتها داخل القطع المطبوعة، فصارت البراغي تُثبَّت في لولب معدني. ثبّت كل محرك من محركات التيار المستمر الأربعة على حوامل المحركات المطبوعة بالبراغي والصواميل، ثم ركّب عجلة على عمود كل محرك.

3.2 تركيب Arduino Mega ودرع المحركات وPCA9685 وLM2596 وHC-05

ثبّت حامل Arduino المطبوع على الهيكل بالبراغي، ثم ثبّت Arduino Mega في الحامل. ركّب Adafruit Motor Shield فوق أطراف التوصيل في Arduino؛ فهو لا يحتاج إلى أسلاك إضافية. تأكد من أن كل طرف (Pin) مستقر في مقبسه قبل أن تضغط الدرع إلى الأسفل. ثبّت PCA9685 وLM2596 وHC-05 على الهيكل بأربطة بلاستيكية (Zip Ties) أو بشريط لاصق مزدوج الوجه، متقاربة بما يكفي لتبقى الأسلاك قصيرة.

3.3 تثبيت اللوح العلوي

ثبّت اللوح العلوي الذي يحمل الذراع على الهيكل بالبراغي بإحكام، حتى لا تهتز الذراع أثناء سير المركبة.

3.4 تجميع الذراع

تُجمَّع الذراع كما هو موضح في دليل بناء OmArm. مقاسات البراغي في هذا القسم مطابقة لأدلة OmArm الخاصة بي. أما قائمة قطع CAD أعلاه فتستخدم براغي قياسية من مكتبة Inventor، لذلك لن تجد هذه المقاسات فيها.

الخطوة الفرعية 1: تثبيت السيرفو على القاعدة. ثبّت أول محرك سيرفو MG996R على قاعدة الذراع بالبراغي المرفقة مع السيرفو، أو ببراغي ذاتية اللولبة M3x12 (براغي تشق لولبها بنفسها في البلاستيك). أضف المحمل الكروي 6806ZZ (30x42x7 mm) لدوران القاعدة؛ فهو يقلل الاحتكاك لتدور القاعدة بسلاسة. ثم ثبّت ذراع السيرفو (Horn) على الجزء العلوي الدوّار ببراغي ذاتية اللولبة M2x12.

الخطوة الفرعية 2: توسيط محركات السيرفو. اضبط كل سيرفو على وضعه المركزي قبل تركيب ذراعه (Horn). ولهذا الغرض، وصّل السيرفو بـ PCA9685 كما في الخطوة 4، وارفع برنامج Arduino (Sketch) من الخطوة 5، وشغّل الطاقة مرة واحدة: تحرّك الدالة setup() كل محرك سيرفو إلى قيمة PWM 375 (90°). ثم ركّب ذراع السيرفو بحيث تكون الوصلة في وضعها الأوسط، وثبّته ببرغي الذراع. أما ذراع السيرفو المركّب بزاوية منحرفة فيدفع المفصل نحو مصدّه الميكانيكي عند التشغيل التالي.

الخطوة الفرعية 3: تركيب الوصلات. ثبّت كل سيرفو على وصلة الذراع الخاصة به بالبراغي، واربط الوصلات عبر أذرع السيرفو. تأكد من أن المفاصل تتحرك بسلاسة وأنها مثبتة بإحكام.

الخطوة الفرعية 4: تجميع القابض. ثبّت القابض على الوصلة الأخيرة ببراغي M3x20. تأكد من أنه مثبت بإحكام على الذراع. ستختبر فتحه وإغلاقه بالتطبيق في 7.3.

3.5 الفحص الميكانيكي

قبل توصيل بقية الروبوت، أدر كل عجلة باليد وتأكد من أنها لا تحتك بالهيكل، وتأكد من عدم وجود رأس برغي أو كابل يعيق أي مفصل في الذراع.

الخطوة 4: التوصيلات

قد يؤدي التوصيل الخاطئ إلى إتلاف اللوحات. وصّل كل مجموعة أدناه وقارنها بمخطط التوصيل في نهاية هذه الخطوة.

4.1 توصيل محركات التيار المستمر بدرع المحركات

المحركطرف درع المحركاتالجانب
المحرك الأمامي الأيسرM1يسار
المحرك الخلفي الأيسرM2يسار
المحرك الأمامي الأيمنM3يمين
المحرك الخلفي الأيمنM4يمين

وصّل المحركين الأيسرين بالمخرجين M1 وM2، والمحركين الأيمنين بالمخرجين M3 وM4. تحدد قطبية كل محرك اتجاه دورانه؛ وستتحقق منها في 7.2.

4.2 البطارية ومسار الطاقة الرئيسي

أستخدم بطارية LiPo من نوع 2S بجهد 7.4 V (خليتان على التوالي، الجهد الاسمي لكل منهما 3.7 V) بسعة 5300 mAh وموصل EC3. استخدم حزمة بطاريات لا تقل سعتها عن ذلك. الحَمْ على مدخل الدائرة القابسَ المناسب لبطاريتك: EC3 مثل قابسي (قابس بطرفين شائع في بطاريات نماذج RC)، أو XT60، أو قابس T. ضع مفتاح الطاقة بين البطارية والدائرة.

اشحن حزمة البطاريات فقط بشاحن موازنة مخصص لبطاريات LiPo (Balance Charger). يشحن هذا الشاحن كل خلية إلى الجهد نفسه عبر قابس الموازنة الصغير في الحزمة. لا تترك الحزمة أبدًا تُشحن دون مراقبة. توقف عن القيادة عندما ينخفض جهد الحزمة إلى نحو 6.6 V (أي 3.3 V لكل خلية)؛ فتحت 3.0 V لكل خلية قد تتلف الخلايا تلفًا دائمًا. يُنبّهك منبّه جهد LiPo موصول بقابس الموازنة في الوقت المناسب. إذا خزّنت الحزمة لأكثر من بضعة أيام، فاشحنها أو فرّغها إلى نحو 7.6 V (أي 3.8 V لكل خلية)، وهو جهد التخزين؛ ومعظم شواحن الموازنة فيها وضع تخزين لهذا الغرض.

تغذي البطارية طرف EXT_PWR في درع المحركات (طرف التوصيل اللولبي لتغذية المحركات)، وتغذي LM2596 بالتوازي. وعند تركيب وصلة PWR (Jumper) على الدرع، يمرّر الدرع جهد البطارية إلى الطرف VIN في Arduino (مدخل التغذية الخارجية)، ويُنتج منظم الجهد الخاص بلوحة Arduino منه 5 V. ركّب هذه الوصلة: ففي مخطط التوصيل هي مسار الطاقة الوحيد للوحة Arduino بعد فصل كابل USB.

4.3 محوّل خفض الجهد LM2596

طرف LM2596التوصيل
IN+القطب الموجب للبطارية (7.4 V، عبر المفتاح)
IN−أرضي البطارية
OUT+طرف التوصيل اللولبي V+ في PCA9685 (تغذية السيرفو، مضبوطة على 6 V)
OUT−طرف GND اللولبي في PCA9685

قبل توصيل PCA9685، أدر المقاومة المتغيرة (Potentiometer: مقاومة صغيرة قابلة للضبط ببرغي في أعلاها) على LM2596 حتى يُظهر جهاز القياس المتعدد 6 V تمامًا عند OUT+. لحمت أسلاك المحوّل أولًا. ومع البطارية المشحونة كان جهد الدخل نحو 8 V، فأدرت البرغي وأنا أقيس الخرج حتى ظهرت القيمة 6 V. يغذي خط 6 V هذا طرف السيرفو في PCA9685 ومحركات السيرفو الستة كلها.

يوفّر LM2596 نحو 2 إلى 3 A من دون مشتت حراري، وقد يسحب محرك MG996R واحد نحو 2.5 A عند التوقف (Stall). إذا أُعيد تشغيل Arduino أو ارتعشت محركات السيرفو عندما تتحرك عدة مفاصل في وقت واحد، فاستبدل الوحدة بمحوّل خافض للجهد XL4016 (حتى 8 A). تبقى التوصيلات وضبط الجهد على 6 V كما هي.

4.4 لوحة قيادة السيرفو PCA9685 ومحركات السيرفو

طرف PCA9685التوصيل
V+ (طرف توصيل لولبي، تغذية السيرفو)LM2596 OUT+ (6 V)
GND (طرف توصيل لولبي)LM2596 OUT−
SDAArduino Mega SDA (الطرف 20)
SCLArduino Mega SCL (الطرف 21)
VCC (طرف تغذية المنطق)Arduino 5 V
GND (أرضي المنطق)Arduino GND

لا توصل مصدر 6 V أبدًا بطرف المنطق VCC؛ فشريحة PCA9685 مصممة لجهد 5.5 V كحد أقصى. ولا تغذِّ محركات السيرفو من الطرف 5 V في Arduino أيضًا، فستة محركات سيرفو تسحب تيارًا أكبر مما تستطيع اللوحة توفيره، ما يسبب هبوطًا في الجهد واهتزاز السيرفو وإعادة تشغيل Arduino.

ركّب كل سيرفو على قناته بحيث تطابق أطراف الإشارة وV+ وGND العلامات المطبوعة على اللوحة. يستخدم الكود هذه القنوات:

Servoالمفصلقناة PCA9685النوع
السيرفو 1دوران القاعدة0MG996R
السيرفو 2الكتف4MG996R
السيرفو 3الكوع8MG996R
السيرفو 4ميل المعصم9SG90
السيرفو 5دوران المعصم12SG90
السيرفو 6القابض13SG90

4.5 وحدة البلوتوث HC-05

طرف HC-05طرف Arduino Mega
VCC5 V
GNDGND
TXDRX0 (الطرف 0)
RXDTX0 (الطرف 1)

يعمل الطرف RXD في HC-05 بمنطق 3.3 V، بينما يُخرج الطرف TX0 في Mega جهد 5 V. في الروبوت الذي بنيته يتصل RXD مباشرة بالطرف 1 كما في مخطط التوصيل، وقد عمل الروبوت بهذه الطريقة. لكن مقسم الجهد (مقاومتان تقسمان الجهد) هو الخيار الأكثر أمانًا: فمع مقاومة 1 kΩ من TX0 إلى RXD ومقاومة 2 kΩ من RXD إلى GND، يصل إلى RXD جهد 5 V × 2 kΩ / 3 kΩ ≈ 3.3 V.

الطرفان 0 و1: تتصل وحدة HC-05 بالمنفذ التسلسلي العتادي Serial0 في Mega (الطرفان 0 و1). ويستخدم اتصال USB الطرفين نفسيهما عند رفع برنامج Arduino (Sketch). افصل HC-05، أو على الأقل سلكي TXD وRXD، قبل كل عملية رفع؛ فإذا بقيت الوحدة موصولة يفشل الرفع بخطأ في الاتصال. ثم أعد توصيلها بعد ذلك. البديل هو Serial1 (الطرفان 18 و19): وصّل TXD بـ RX1 وRXD بـ TX1 (مع مقسم الجهد). وفي البرنامج أضف Serial1.begin(9600) إلى setup() واقرأ الأوامر من Serial1 بدلًا من Serial. يبقى Serial على اتصال USB، فيعمل الرفع والشاشة التسلسلية (Serial Monitor: نافذة Arduino IDE التي تعرض النص الذي ترسله اللوحة) حتى والوحدة موصولة. شرحتُ التوصيل والاقتران خطوة بخطوة في درس وحدة البلوتوث HC-05/HC-06.

4.6 اختبار توزيع الطاقة

قبل التشغيل لأول مرة، تحقق بجهاز القياس المتعدد في وضع فحص الاستمرارية (Continuity) من عدم وجود دارة قصر بين القطبين الموجب والسالب للبطارية. في هذا الوضع يُصدر الجهاز صفيرًا عند اتصال نقطتين. ثم شغّل الطاقة وقِس: 5 V عند Arduino وHC-05، و6 V عند طرف V+ في PCA9685، وجهد البطارية عند طرف EXT_PWR في الدرع. تحصل المحركات على جهد أقل قليلًا، لأن شرائح القيادة L293D تفقد جزءًا من الجهد.

Wiring diagram of OmObiArm: Arduino Mega with Adafruit Motor Shield, PCA9685 servo driver, HC-05, LM2596, four DC motors and six servos

الخطوة 5: برمجة Arduino

يقرأ البرنامج أوامر البلوتوث، ويقود محركات المركبة، ويحرّك محركات السيرفو خطوة بخطوة، ويتولى حفظ الأوضاع وتشغيلها. الكود الكامل وملف مشروع MIT App Inventor موجودان في مستودع GitHub. إذا كنت جديدًا على Arduino، فاقرأ درسي أساسيات برمجة Arduino أولًا. افصل على الأقل سلكي TXD وRXD في HC-05 قبل الرفع (انظر 4.5). ولرفع الكود نزعت غطاء منفذ USB المطبوع ووصلت الروبوت بحاسوبي.

كود Arduino الكامل

#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
#include <AFMotor.h>

// Constants and definitions
const int numServos = 6; // Number of servos
const int maxConfigurations = 10; // Maximum number of storable poses
const int stepDelay = 10; // Delay between each step to slow down the servo movement
const int stepSize = 1;   // PWM units per step

// Servo channels on the PCA9685
const int servoChannels[numServos] = {0, 4, 8, 9, 12, 13};

// Storage structures
int savedConfigurations[maxConfigurations][numServos];
int currentServoPositions[numServos] = {375, 375, 375, 375, 375, 375}; // Default positions
int configCount = 0;            // Counter for stored poses
bool isPlaying = false;         // Status indicating if poses are being played
bool loopPlayback = false;      // Status indicating if poses should be played in a loop
bool stopPlaying = false;       // Status indicating if playback should be stopped
int currentPoseIndex = 0;       // Index of the current pose during playback

// Motor instances for the rover
AF_DCMotor motor1(1);
AF_DCMotor motor2(2);
AF_DCMotor motor3(3);
AF_DCMotor motor4(4);

Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
String input = ""; // Holds incoming Bluetooth data

void setup() {
  Serial.begin(9600); // Serial communication for Bluetooth

  // Initialize motors
  motor1.setSpeed(255);
  motor2.setSpeed(255);
  motor3.setSpeed(255);
  motor4.setSpeed(255);

  // Initialize servo controller
  pwm.begin();
  pwm.setPWMFreq(50); // Set frequency to 50 Hz for servos

  // Initialize servo positions
  for (int i = 0; i < numServos; i++) {
    pwm.setPWM(servoChannels[i], 0, currentServoPositions[i]);
  }

  Serial.println("Bluetooth Robot Controller ready. Waiting for commands...");
}
void loop() {
  // Process incoming commands
  if (Serial.available()) {
    input = Serial.readStringUntil('\n'); // Read data from Bluetooth
    input.trim();

    Serial.println("Received command: " + input);
    processCommand(input);
  }

  // Check if robot arm is playing poses
  if (isPlaying && !stopPlaying) {
    if (loopPlayback) {
      playPosesInLoop();
    } else {
      playNextPose();
    }
  }
}
void processCommand(String command) {
  // If playing, ignore all rover commands
  if (isPlaying) {
    if (command == "s") { // Allow stop playback command
      stopPlayingPoses();
    } else {
      Serial.println("Playback in progress. Ignoring command: " + command);
    }
    return;
  }

  // Control rover movement
  if (command == "F") {
    forward();
  } else if (command == "B") {
    backward();
  } else if (command == "L") {
    turnLeft();
  } else if (command == "R") {
    turnRight();
  } else if (command == "S") {
    stopMotors(); // Stop motors immediately on "S" command

  // Control robotic arm
  } else if (command.startsWith("1,")) {
    processServoCommand(command, 0); // Control Servo 1
  } else if (command.startsWith("2,")) {
    processServoCommand(command, 1); // Control Servo 2
  } else if (command.startsWith("3,")) {
    processServoCommand(command, 2); // Control Servo 3
  } else if (command.startsWith("4,")) {
    processServoCommand(command, 3); // Control Servo 4
  } else if (command.startsWith("5,")) {
    processServoCommand(command, 4); // Control Servo 5
  } else if (command.startsWith("6,")) {
    processServoCommand(command, 5); // Control Servo 6

  // Save, play, reset poses
  } else if (command == "v") {
    saveCurrentPose();
  } else if (command == "l") {
    startPlayingPoses();
  } else if (command == "r") {
    resetPoses();
  } else if (command == "s") {
    stopPlayingPoses();
  } else if (command == "o") {
    loopPlayback = true;
    Serial.println("Loop playback enabled.");
  } else if (command == "f") {
    loopPlayback = false;
    Serial.println("Loop playback disabled.");
  } else {
    Serial.println("Unknown command: " + command);
  }
}

void processServoCommand(String command, int servoIndex) {
  int commaIndex = command.indexOf(',');
  if (commaIndex > 0) {
    int position = command.substring(commaIndex + 1).toInt();
    int pwmValue = map(position, 0, 180, 150, 600);
    moveToPositionSmoothly(servoIndex, pwmValue);
  }
}

التحكم في المحركات وحركة السيرفو

void forward() {
  motor1.run(FORWARD);
  motor2.run(FORWARD);
  motor3.run(FORWARD);
  motor4.run(FORWARD);
  Serial.println("Moving forward");
}

void backward() {
  motor1.run(BACKWARD);
  motor2.run(BACKWARD);
  motor3.run(BACKWARD);
  motor4.run(BACKWARD);
  Serial.println("Moving backward");
}

void turnLeft() {
  motor1.run(BACKWARD);
  motor2.run(BACKWARD);
  motor3.run(FORWARD);
  motor4.run(FORWARD);
  Serial.println("Turning left");
}

void turnRight() {
  motor1.run(FORWARD);
  motor2.run(FORWARD);
  motor3.run(BACKWARD);
  motor4.run(BACKWARD);
  Serial.println("Turning right");
}

void stopMotors() {
  motor1.run(RELEASE);
  motor2.run(RELEASE);
  motor3.run(RELEASE);
  motor4.run(RELEASE);
  Serial.println("Motors stopped");
}

// Arm servo control
void moveToPositionSmoothly(int servoIndex, int targetPwmValue) {
  int currentPwmValue = currentServoPositions[servoIndex];
  if (targetPwmValue > currentPwmValue) {
    for (int pos = currentPwmValue; pos <= targetPwmValue; pos += stepSize) {
      pwm.setPWM(servoChannels[servoIndex], 0, pos);
      delay(stepDelay);
    }
  } else {
    for (int pos = currentPwmValue; pos >= targetPwmValue; pos -= stepSize) {
      pwm.setPWM(servoChannels[servoIndex], 0, pos);
      delay(stepDelay);
    }
  }
  currentServoPositions[servoIndex] = targetPwmValue;
  Serial.println("Servo " + String(servoIndex + 1) + " set to position: " + String(targetPwmValue));
}

دوال الأوضاع

void saveCurrentPose() {
  if (configCount < maxConfigurations) {
    for (int i = 0; i < numServos; i++) {
      savedConfigurations[configCount][i] = currentServoPositions[i];
    }
    configCount++;
    Serial.println("Pose saved. Total poses: " + String(configCount));
  } else {
    Serial.println("Memory full. Cannot save pose.");
  }
}

void startPlayingPoses() {
  if (configCount > 0) {
    stopMotors(); // Ensure rover is stopped during playback
    isPlaying = true;
    stopPlaying = false;
    currentPoseIndex = 0;
    Serial.println("Starting pose playback");
  } else {
    Serial.println("No poses saved");
  }
}

void playNextPose() {
  if (currentPoseIndex < configCount) {
    for (int i = 0; i < numServos; i++) {
      moveToPositionSmoothly(i, savedConfigurations[currentPoseIndex][i]);
    }
    delay(1000);
    currentPoseIndex++;
  } else {
    isPlaying = false;
    Serial.println("Pose playback finished");
  }
}

void playPosesInLoop() {
  for (int i = 0; i < configCount; i++) {
    if (stopPlaying) break; // Never true here: loop() reads no commands while this runs
    for (int j = 0; j < numServos; j++) {
      moveToPositionSmoothly(j, savedConfigurations[i][j]);
    }
    delay(1000);
  }
}

void stopPlayingPoses() {
  stopPlaying = true;
  isPlaying = false;
  Serial.println("Pose playback stopped");
}

void resetPoses() {
  configCount = 0;
  isPlaying = false;
  loopPlayback = false;
  currentPoseIndex = 0;
  Serial.println("All poses reset");
}

شرح الكود: المكتبات

#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
#include <AFMotor.h>

يحتاج البرنامج إلى ثلاث مكتبات. تتولى Wire.h اتصال I2C مع PCA9685. وتتحكم Adafruit_PWMServoDriver.h في محركات السيرفو عبر PCA9685. أما AFMotor.h فهي مكتبة Adafruit Motor Shield لمحركات التيار المستمر الموصولة بجسور H في شرائح L293D. ثبّت المكتبات الثلاث في Arduino IDE قبل الترجمة (Compile).

شرح الكود: قنوات السيرفو والمتغيرات

const int numServos = 6;
const int maxConfigurations = 10;
const int stepDelay = 10;
const int stepSize = 1;
const int servoChannels[numServos] = {0, 4, 8, 9, 12, 13};
int savedConfigurations[maxConfigurations][numServos];
int currentServoPositions[numServos] = {375, 375, 375, 375, 375, 375};
int configCount = 0;
bool isPlaying = false;
bool loopPlayback = false;
bool stopPlaying = false;
int currentPoseIndex = 0;

قيمة numServos هي 6 لأن الذراع تحتوي على ستة محركات سيرفو. ويحدّد maxConfigurations الحد الأقصى لعدد الوضعيات المحفوظة بـ 10. تُخزَّن الوضعيات في ذاكرة SRAM (ذاكرة العمل التي تُمسح عند انقطاع الطاقة)، لذا تضيع عند إطفاء الروبوت. ويحدّد stepDelay (10 ms) وstepSize (1 وحدة PWM) سرعة حركة محركات السيرفو: وحدة PWM واحدة كل 10 ms. وتربط المصفوفة servoChannels محركات السيرفو من 1 إلى 6 بقنوات PCA9685 ذات الأرقام 0 و4 و8 و9 و12 و13. وتخزّن المصفوفة currentServoPositions الموضع الحالي لكل محرك سيرفو. أما متغيرات الحالة (flags) isPlaying وloopPlayback وstopPlaying فتتحكم في تشغيل الوضعيات المحفوظة.

تخزّن المصفوفة قيم PCA9685 الخام وليس الدرجات. عند تردد 50 Hz تقسم شريحة PCA9685 كل دورة مدتها 20 ms إلى 4096 خطوة. والقيمة هي عدد الخطوات التي تبقى فيها النبضة في المستوى المرتفع: 150 تعادل نحو 0.7 ms، و600 نحو 2.9 ms. أما 375 فهي منتصف هذا المدى وتقابل 90°.

شرح الكود: كائنات المحركات ولوحة قيادة السيرفو

AF_DCMotor motor1(1);
AF_DCMotor motor2(2);
AF_DCMotor motor3(3);
AF_DCMotor motor4(4);
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();

تُنشأ أربعة كائنات لمحركات التيار المستمر، واحد لكل مخرج من مخارج المحركات M1 إلى M4 في درع المحركات. والرقم بين القوسين هو رقم المخرج. ويستخدم الكائن pwm من النوع Adafruit_PWMServoDriver عنوان I2C الافتراضي 0x40 لشريحة PCA9685. وإذا أضفت لوحة PCA9685 ثانية، فمرّر عنوانها هنا، مثل Adafruit_PWMServoDriver(0x41). ويُضبط العنوان عبر وصلات اللحام (Solder Jumpers) من A0 إلى A5 على اللوحة.

شرح الكود: setup()

void setup() {
  Serial.begin(9600);
  motor1.setSpeed(255);
  motor2.setSpeed(255);
  motor3.setSpeed(255);
  motor4.setSpeed(255);
  pwm.begin();
  pwm.setPWMFreq(50);
  for (int i = 0; i < numServos; i++) {
    pwm.setPWM(servoChannels[i], 0, currentServoPositions[i]);
  }
}

تبدأ الدالة setup() تشغيل المنفذ التسلسلي بسرعة 9600 baud (عدد البتات في الثانية على الخط التسلسلي)، وهي السرعة الافتراضية لوحدة HC-05. ثم تضبط المحركات الأربعة على السرعة القصوى (255 من 255)، وتشغّل PCA9685 بتردد PWM قدره 50 Hz، وهو التردد المعتاد لمحركات السيرفو المستخدمة في مشاريع الهواة. بعد ذلك ترسل محركات السيرفو الستة مباشرة إلى قيمة PWM ‏375 (90°). هذه الحركة الأولى غير منعّمة، لذا تقفز الذراع عند التشغيل إلى وضعيتها المركزية بأقصى سرعة للسيرفو. أبعِد يديك وأي أجسام عن مدى وصول الذراع عند تشغيلها.

شرح الكود: loop()

void loop() {
  if (Serial.available()) {
    input = Serial.readStringUntil('\n');
    input.trim();
    processCommand(input);
  }
  if (isPlaying && !stopPlaying) {
    if (loopPlayback) {
      playPosesInLoop();
    } else {
      playNextPose();
    }
  }
}

تتحقق الدالة loop() من وصول بيانات على المنفذ التسلسلي. وعند وجود بيانات، تقرأ readStringUntil(‘\n’) أمرًا كاملًا واحدًا. يحدد حرف السطر الجديد (\n) نهاية الأمر، لذلك يجب أن ينتهي كل أمر يرسله التطبيق بـ \n. وبعد أن تزيل trim() المسافات وفواصل الأسطر، يُمرَّر الأمر إلى processCommand(). أما النصف الثاني من loop() فيتحقق مما إذا كان تشغيل الوضعيات نشطًا، وإن كان كذلك يستدعي playPosesInLoop() أو playNextPose().

شرح الكود: processCommand()

void processCommand(String command) {
  if (isPlaying) {
    if (command == "s") { stopPlayingPoses(); }
    return;
  }
  if (command == "F") forward();
  else if (command == "B") backward();
  else if (command == "L") turnLeft();
  else if (command == "R") turnRight();
  else if (command == "S") stopMotors();
  else if (command.startsWith("1,")) processServoCommand(command, 0);
  else if (command.startsWith("2,")) processServoCommand(command, 1);
  else if (command.startsWith("3,")) processServoCommand(command, 2);
  else if (command.startsWith("4,")) processServoCommand(command, 3);
  else if (command.startsWith("5,")) processServoCommand(command, 4);
  else if (command.startsWith("6,")) processServoCommand(command, 5);
  else if (command == "v") saveCurrentPose();
  else if (command == "l") startPlayingPoses();
  else if (command == "r") resetPoses();
  else if (command == "s") stopPlayingPoses();
  else if (command == "o") { loopPlayback = true; }
  else if (command == "f") { loopPlayback = false; }
}

تحدد الدالة processCommand() الدالة التي تعالج كل أمر. وأثناء تشغيل الوضعيات (عندما تكون قيمة isPlaying هي true)، لا تقبل إلا الحرف الصغير s لإيقاف التشغيل وتتجاهل كل الأوامر الأخرى. وبذلك لا تصل أوامر القيادة إلى المحركات أثناء تنفيذ سلسلة الوضعيات، فلا يمكن للمركبة أن تنطلق بينما تعمل الذراع. وفي الحالات الأخرى تستدعي الدالة المطابقة للأمر. تقرأ processServoCommand() الرقم الذي يلي الفاصلة، وتحوّل المدى 0–180° إلى مدى PCA9685 ‏150–600، ثم تستدعي دالة الحركة السلسة.

مرجع الأوامر

الأوامر حساسة لحالة الأحرف: الحرف الكبير S يوقف المحركات، والحرف الصغير s يوقف تشغيل الوضعيات. وأمر التشغيل هو الحرف L الصغير (l)، وليس الرقم 1. وينتهي كل أمر بحرف سطر جديد (\n).

الأمرالإجراءالمجموعة
Fالتحرك إلى الأمامالمركبة
Bالتحرك إلى الخلفالمركبة
Lالدوران يسارًا في المكانالمركبة
Rالدوران يمينًا في المكانالمركبة
Sإيقاف المحركات الأربعة كلها (يرسله التطبيق عندما تحرر زر القيادة)المركبة
1,angleتحريك السيرفو 1 (القاعدة) إلى الزاوية angle ‏(0–180)الذراع
2,angleتحريك السيرفو 2 (الكتف) إلى الزاوية angleالذراع
3,angleتحريك السيرفو 3 (المرفق) إلى الزاوية angleالذراع
4,angleتحريك السيرفو 4 (ميل المعصم) إلى الزاوية angleالذراع
5,angleتحريك السيرفو 5 (دوران المعصم) إلى الزاوية angleالذراع
6,angleتحريك السيرفو 6 (القابض) إلى الزاوية angleالذراع
vحفظ وضعية الذراع الحاليةالوضعيات
lتشغيل الوضعيات المحفوظةالوضعيات
rإعادة ضبط (مسح) جميع الوضعيات المحفوظةالوضعيات
sإيقاف تشغيل الوضعياتالوضعيات
oتفعيل وضع التكرارالوضعيات
fتعطيل وضع التكرارالوضعيات

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

void forward() {
  motor1.run(FORWARD); motor2.run(FORWARD);
  motor3.run(FORWARD); motor4.run(FORWARD);
}
void backward() {
  motor1.run(BACKWARD); motor2.run(BACKWARD);
  motor3.run(BACKWARD); motor4.run(BACKWARD);
}
void turnLeft() {
  motor1.run(BACKWARD); motor2.run(BACKWARD);
  motor3.run(FORWARD); motor4.run(FORWARD);
}
void turnRight() {
  motor1.run(FORWARD); motor2.run(FORWARD);
  motor3.run(BACKWARD); motor4.run(BACKWARD);
}
void stopMotors() {
  motor1.run(RELEASE); motor2.run(RELEASE);
  motor3.run(RELEASE); motor4.run(RELEASE);
}

تضبط كل دالة المحركات الأربعة دفعة واحدة. في الحركة إلى الأمام وإلى الخلف تدور كل المحركات في الاتجاه نفسه. أما عند الالتفاف فيدور المحركان الأيسران في اتجاه والمحركان الأيمنان في الاتجاه المعاكس، فتدور المركبة في مكانها (التوجيه بالانزلاق، Skid Steering). تستخدم stopMotors() القيمة RELEASE التي تقطع الطاقة عن المحركات. والسرعة مثبتة على 255 في setup()؛ اخفض هذه القيمة لقيادة أبطأ وأكثر تحكمًا. ولأساسيات التحكم في المحركات عبر جسر H ‏(H-bridge)، راجع شرح وحدة قيادة المحركات L298N.

شرح الكود: moveToPositionSmoothly()

void moveToPositionSmoothly(int servoIndex, int targetPwmValue) {
  int currentPwmValue = currentServoPositions[servoIndex];
  if (targetPwmValue > currentPwmValue) {
    for (int pos = currentPwmValue; pos <= targetPwmValue; pos += stepSize) {
      pwm.setPWM(servoChannels[servoIndex], 0, pos);
      delay(stepDelay);
    }
  } else {
    for (int pos = currentPwmValue; pos >= targetPwmValue; pos -= stepSize) {
      pwm.setPWM(servoChannels[servoIndex], 0, pos);
      delay(stepDelay);
    }
  }
  currentServoPositions[servoIndex] = targetPwmValue;
}

القفز مباشرة إلى الموضع المستهدف يسبب حركة متقطعة وعنيفة وقد يتلف تروس السيرفو. لذلك تمرّ هذه الدالة على قيم PWM وحدةً تلو الأخرى، مع تأخير 10 ms بين كل خطوة والتي تليها، صعودًا أو نزولًا بحسب موقع الهدف. وفي النهاية تخزّن الموضع الجديد في currentServoPositions.

حلقة for حاجبة (Blocking)، أي أنها تشغل Arduino بالكامل حتى تنتهي. فالمسح الكامل من 150 إلى 600 يستغرق 450 خطوة × 10 ms = 4.5 s، ولا تعالج Arduino خلال هذا الوقت أي أوامر جديدة، ولا حتى S (إيقاف المحركات). تنتظر الأوامر في المخزن المؤقت التسلسلي (Serial Buffer)، وهو ذاكرة صغيرة للأحرف الواردة. فإذا كانت المركبة تسير وأنت تحرّك أحد أشرطة التمرير (Slider)، فستواصل السير حتى يصل السيرفو إلى موضعه. أما النسخة غير الحاجبة (Non-blocking) فتحرّك كل سيرفو خطوة واحدة في كل دورة من loop()، وتضبط توقيت الخطوات باستخدام millis() (عدد الميلي ثانية منذ بدء تشغيل Arduino).

شرح الكود: دوال الوضعيات

void saveCurrentPose() {
  if (configCount < maxConfigurations) {
    for (int i = 0; i < numServos; i++) {
      savedConfigurations[configCount][i] = currentServoPositions[i];
    }
    configCount++;
  }
}
void startPlayingPoses() {
  if (configCount > 0) {
    stopMotors();
    isPlaying = true;
    stopPlaying = false;
    currentPoseIndex = 0;
  }
}
void playNextPose() {
  if (currentPoseIndex < configCount) {
    for (int i = 0; i < numServos; i++) {
      moveToPositionSmoothly(i, savedConfigurations[currentPoseIndex][i]);
    }
    delay(1000);
    currentPoseIndex++;
  } else {
    isPlaying = false;
  }
}
void playPosesInLoop() {
  for (int i = 0; i < configCount; i++) {
    if (stopPlaying) break;
    for (int j = 0; j < numServos; j++) {
      moveToPositionSmoothly(j, savedConfigurations[i][j]);
    }
    delay(1000);
  }
}
void resetPoses() {
  configCount = 0;
  isPlaying = false;
  loopPlayback = false;
  currentPoseIndex = 0;
}

تنسخ saveCurrentPose() قيم PWM الست الحالية إلى المصفوفة savedConfigurations وتزيد configCount بمقدار واحد، حتى 10 وضعيات كحد أقصى. أما startPlayingPoses() فتوقف المحركات وتفعّل isPlaying وتعيد currentPoseIndex إلى 0.

تحرّك playNextPose() محركات السيرفو الستة إلى القيم المخزنة لوضعية واحدة، وتنتظر ثانية واحدة، ثم تنتقل إلى الوضعية التالية. وبعد آخر وضعية ينتهي التشغيل تلقائيًا. أما playPosesInLoop() فتمر على كل الوضعيات المحفوظة في استدعاء واحد، وتعيد loop() استدعاءها ما دامت قيمة isPlaying هي true. وتمسح resetPoses() الوضعيات وكل متغيرات حالة التشغيل.

في وضع التكرار، لا يسري أمر الإيقاف إلا بعد أن تُنهي الذراع سلسلة الوضعيات المحفوظة كاملة. فأثناء تنفيذ playPosesInLoop() لا تقرأ Arduino أي أوامر جديدة، لذا لا يرصد فحص stopPlaying داخل الحلقة أمر الإيقاف أبدًا. ومع عدة وضعيات قد يستغرق ذلك وقتًا طويلًا. أما عند تعطيل وضع التكرار، فيسري الإيقاف بين وضعيتين.

الخطوة 6: تطبيق OmObiArmControl

OmObiArmControl هو تطبيق Android الخاص بالروبوت، وقد بُني باستخدام MIT App Inventor. ويوجد ملف المشروع ‎.aia (صيغة المشروع التي تستوردها إلى MIT App Inventor) في مستودع GitHub.

OmObiArmControl app with the Select Device button, six joint sliders, drive buttons and pose buttons

استخدام التطبيق

  • الاتصال: أقرِن وحدة HC-05 بهاتفك من إعدادات البلوتوث قبل فتح التطبيق (رمز PIN هو 1234، أو 0000 في بعض الوحدات). وفي التطبيق، اضغط على “Select Device” واختر HC-05 من قائمة الأجهزة المقترنة. يتحول لون النص المجاور للزر إلى الأخضر وتظهر عبارة “Connected”. أما اللون الأحمر مع “Not Connected” فيعني أن الاتصال فشل.
  • حرّك الذراع: ستة أشرطة تمرير، واحد لكل سيرفو من القاعدة حتى القابض. يرسل كل شريط قيمته إلى السيرفو المطابق عند تحريكه.
  • القيادة: تحرّك أزرار الأسهم الأربعة المركبة إلى الأمام والخلف واليسار واليمين. ولا تتحرك المركبة إلا ما دمت تضغط على الزر. وعند رفع إصبعك يرسل التطبيق S فتتوقف المحركات، لذا لا يوجد زر إيقاف منفصل للقيادة.
  • الوضعيات: يحفظ زر “Save” موضع الذراع الحالي كوضعية، حتى 10 وضعيات. ويعيد “Play” تشغيلها بالترتيب. ويكرر مفتاح LoopPlayback السلسلة حتى توقفها. وينهي “Stop” التشغيل؛ وفي وضع التكرار تُكمل الذراع أولًا الدورة الحالية عبر كل الوضعيات. ويمسح “Reset” كل الوضعيات المحفوظة. وفي الكود، تُخزَّن الوضعيات المحفوظة في المصفوفة savedConfigurations.
  • الحالة: يعدّ PoseCount، الموجود بجوار مفتاح LoopPlayback، عدد مرات ضغطك على Save. ولا تحتفظ Arduino إلا بأول 10 وضعيات، لذا قد يكون العدد المعروض أكبر من عدد الوضعيات المخزنة فعلًا.

اتصال البلوتوث

استخدم مكوّن ListPicker. في حدث BeforePicking الخاص به، املأ القائمة بأجهزة البلوتوث المقترنة. وفي AfterPicking، اتصل بالجهاز المختار واضبط نص الحالة على “Connected” باللون الأخضر، أو على “Not Connected” باللون الأحمر إذا فشل الاتصال. وتجد شرح كتل البلوتوث (Blocks) بالتفصيل في شرح وحدة البلوتوث HC-05.

أشرطة تمرير الذراع

أضف ستة مكوّنات Slider، واحدًا لكل سيرفو، مع MinValue بقيمة 0 وMaxValue بقيمة 180. وفي حدث PositionChanged لكل شريط تمرير، أرسل نص الأمر عبر البلوتوث: يرسل Slider 1 الأمر 1,value\n، ويرسل Slider 2 الأمر 2,value\n، وهكذا حتى Slider 6.

MIT App Inventor blocks: ListPicker connection and the six slider PositionChanged events that send the servo commands over Bluetooth

أزرار القيادة

أضف أربعة مكوّنات Button للحركة إلى الأمام والخلف واليسار واليمين. وفي حدث TouchDown لكل زر (عند وضع الإصبع على الزر)، أرسل F\n أو B\n أو L\n أو R\n. وفي حدث TouchUp (عند رفع الإصبع)، أرسل S\n. وبهذه الطريقة يعمل التطبيق الموجود في المستودع.

MIT App Inventor blocks for the four drive buttons: TouchDown sends F, B, L or R, TouchUp sends S

أزرار الوضعيات ومفتاح التكرار (Loop)

أضف أزرارًا لـ Save (v\n) وPlay (l\n) وReset (r\n) وStop (s\n)، إضافة إلى مكوّن Switch لوضع التكرار يرسل o\n عند تشغيله وf\n عند إيقافه.

نصوص الحالة

أضف مكوّنات Label لعرض حالة الاتصال وعدد الوضعيات. وحدّثها عندما يتصل التطبيق أو يحفظ وضعية أو يعيد الضبط.

MIT App Inventor blocks for Save, Play, Reset, Stop and the Loop switch, with the PoseCount label update

الخطوة 7: الاختبار والمعايرة

ضع المركبة على سطح مستوٍ، وشغّلها والذراع في وضعية مرتخية، واختبر كل نظام فرعي على حدة قبل تشغيل كل شيء معًا. عند التشغيل تقفز الذراع إلى وضعيتها المركزية بأقصى سرعة للسيرفو، لذا أبعِد يديك عنها.

7.1 فحص الاتصال

أقرِن الوحدة واتصل بها كما هو موضح في الخطوة 6. في إصدار ZS-040 الشائع من وحدة HC-05، يومض LED بسرعة أثناء انتظار الاتصال، ثم يتحول إلى وميض مزدوج كل ثانيتين بعد الاتصال. وإذا فشل الاتصال، فتأكد من وصول الطاقة إلى HC-05 ومن تفعيل البلوتوث في هاتفك.

7.2 قيادة المركبة

اختبر كل اتجاه على حدة: الأمام، والخلف، واليسار، واليمين. إذا دار أحد المحركات في الاتجاه الخاطئ، فبدّل سلكيه عند مخرج درع المحركات؛ ولا حاجة إلى تغيير الكود. وتأكد من توقف المحركات الأربعة كلها عند تحرير الزر.

7.3 معايرة السيرفو

حرّك كل شريط تمرير ببطء، واحدًا تلو الآخر، وتأكد من أن السيرفو المطابق يتحرك. إذا لم يستجب أحد محركات السيرفو، فافحص توصيلاته وتأكد من أن قناته تطابق الكود (0، 4، 8، 9، 12، 13). وإذا كان يرتجف أو يطنّ دون أن يتحرك، فافحص جهد 6 V القادم من LM2596. وإذا وصل السيرفو إلى حدّه الميكانيكي قبل أن يبلغ شريط التمرير 0 أو 180، فقيّد مداه. يطبّق سطر map() في processServoCommand() المدى نفسه على محركات السيرفو الستة كلها. ولتقييد سيرفو واحد فقط، اضبط MinValue وMaxValue لشريط التمرير الخاص به في App Inventor (مثلًا 20 و160)، أو خصّص لكل سيرفو قيمة PWM دنيا وقصوى خاصة به في الكود.

7.4 حفظ الوضعيات وتشغيلها

حرّك الذراع إلى موضع ما واضغط Save. ثم حرّكها إلى موضع ثانٍ واضغط Save مرة أخرى. اضغط Play: يجب أن تعيد الذراع تنفيذ الوضعيتين بالترتيب. فعّل وضع التكرار وابدأ التشغيل: تتنقل الذراع بين الوضعيات باستمرار حتى تضغط Stop، ويسري الإيقاف بعد انتهاء الدورة الحالية. وأخيرًا اضغط Reset ثم Play: لن يحدث شيء، لأنه لا توجد أي وضعية محفوظة.

7.5 المركبة والذراع معًا

قُد المركبة أثناء تحريك أشرطة تمرير الذراع، وراقب جهد البطارية. إذا أصبح الروبوت بطيئًا، أو ارتجفت محركات السيرفو، أو أعادت Arduino تشغيل نفسها، فالسبب المرجح هو مزود الطاقة (راجع قسم استكشاف الأخطاء وإصلاحها). وفي هذه الحالة استخدم بطارية بسعة أعلى أو اخفض سرعة المحركات في setup().

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

يقترن HC-05 لكن الروبوت لا يستجيب: تأكد من توصيل TXD بـ RX0 (الطرف 0) وRXD بـ TX0 (الطرف 1)، ومن أن برنامج Arduino يستخدم 9600 baud. ولرؤية مخرجات التصحيح في الشاشة التسلسلية (Serial Monitor)، افصل HC-05 (أو انقله إلى Serial1، راجع 4.5) ثم افتح الشاشة التسلسلية. يجب أن يطبع البرنامج “Bluetooth Robot Controller ready. Waiting for commands…”.

فشل رفع برنامج Arduino: لا تزال وحدة HC-05 موصولة بالطرفين 0 و1. افصل على الأقل سلكي TXD وRXD الخاصين بها (راجع 4.5).

محركات السيرفو ترتجف أو تطنّ أو تتوقف عن الحركة: قِس جهد خرج LM2596؛ يجب أن يكون 6 V. وحدة LM2596 مصنّفة لتيار ذروة 3 A ونحو 2 A بشكل مستمر دون مشتت حراري. ومحرك MG996R واحد متوقف تحت الحمل (Stall) يسحب ما يصل إلى نحو 2.5 A عند 6 V. فإذا انخفض الجهد عندما ترتفع الذراع، فاستخدم وحدة خفض جهد (Step-down) مصنّفة لتيار 5 A على الأقل. وقد ينتج الارتجاف أيضًا عن نقص الطاقة عندما تعمل عدة محركات سيرفو في الوقت نفسه. وافحص أيضًا مقابس السيرفو على PCA9685.

المركبة تتحرك لكن الذراع لا تستجيب: افحص SDA وSCL على الطرفين 20 و21، وجهد 6 V عند طرف V+ في PCA9685. يجب أن يكون عنوان I2C هو 0x40؛ شغّل برنامج Arduino لفحص I2C (برنامج اختبار قصير يسرد كل جهاز على ناقل I2C) للتأكد من أن Arduino تتعرّف على PCA9685.

الذراع تتحرك لكن المركبة لا تستجيب: افحص أسلاك المحركات عند M1 إلى M4، وتأكد من أن درع المحركات مثبّت بالكامل على أطراف التوصيل في Arduino، وافحص جهد البطارية عند طرف EXT_PWR.

الوضعيات لا تُحفظ أو لا تُشغَّل: تأكد من أن التطبيق يرسل v للحفظ وl للتشغيل (حرف L صغير). تعرض الشاشة التسلسلية (Serial Monitor) عدد الوضعيات بعد كل عملية حفظ (راجع البند الأول لمعرفة طريقة توصيلها). وبعد 10 وضعيات محفوظة يتجاهل البرنامج أي عملية حفظ إضافية.

التشغيل المتكرر لا يتوقف: يجب أن يرسل التطبيق الحرف الصغير s. وفي وضع التكرار لا يسري الإيقاف إلا بعد انتهاء الدورة الحالية عبر كل الوضعيات المحفوظة (راجع شرح دوال الوضعيات).

انخفاض الجهد والسلوك غير المنتظم تحت الحمل: لا تستطيع البطارية أو وحدة LM2596 توفير تيار كافٍ للمحركات ومحركات السيرفو في الوقت نفسه. اشحن بطارية LiPo بالكامل. وإذا لم يُجدِ ذلك، فغذِّ المحركات ومحركات السيرفو من بطاريتين منفصلتين.

المحركات تدور في الاتجاه الخاطئ: بدّل سلكي ذلك المحرك عند مخرج الدرع. ولا حاجة إلى تغيير الكود.

الأسئلة الشائعة

هل يمكنني استخدام Arduino Uno بدلًا من Mega؟

يتسع البرنامج لذاكرة Uno: فعند ترجمته لهذه اللوحة يستخدم نحو 1.1 KB من أصل 2 KB من ذاكرة SRAM للمتغيرات العامة. وعلى Uno، وصّل SDA وSCL الخاصين بـ PCA9685 بالطرفين A4 وA5. وتبقى وحدة HC-05 على الطرفين 0 و1، لأن Uno لا تحتوي على Serial1. أما Mega فتحتوي على 8 KB من SRAM وأطراف حرة أكثر، وتتيح لك نقل HC-05 إلى Serial1 (راجع 4.5).

لماذا يستخدم الكود PCA9685 بدلًا من أطراف PWM الموجودة في Arduino نفسها؟

يشغل Adafruit Motor Shield عددًا من أطراف PWM في Arduino. أما PCA9685 فلا تحتاج إلا إلى طرفي I2C (SDA وSCL)، وتوفّر 16 قناة PWM. ويتلقى خط الطاقة V+ فيها جهد 6 V المنفصل القادم من LM2596، فلا يمر تيار السيرفو عبر Arduino أبدًا.

هل يمكنني التحكم في OmObiArm من جهاز iPhone؟

تستخدم وحدة HC-05 تقنية Bluetooth Classic مع ملف المنفذ التسلسلي Serial Port Profile (SPP: بلوتوث يعمل كأنه كابل تسلسلي)، وهو ما لا تتيحه أجهزة iPhone لتطبيقات الطرف الثالث. ولنظام iOS، استبدل HC-05 بوحدة BLE (Bluetooth Low Energy)، مثل HM-10 أو ESP32، وعدّل برنامج Arduino والتطبيق.

المصادر والمشاريع التالية

ملفات المشروع

  • مستودع GitHub: برنامج Arduino وملف مشروع MIT App Inventor (.aia).
  • متجر OmArTronics: ملفات الطباعة وملفات CAD (STL للطباعة، وSTP لفتح النموذج في برنامج CAD) للهيكل والصفيحة العلوية ووصلات الذراع والقابض. أما الكود ومخطط التوصيل وقائمة القطع فموجودة في هذه الصفحة.

شروحات ذات صلة

الترقيات التالية

  • تفادي العوائق: أضف أربعة مستشعرات موجات فوق صوتية من نوع HC-SR04 لتتمكن المركبة من التنقل ذاتيًا بينما تعمل الذراع. راجع شرح مستشعر الموجات فوق الصوتية HC-SR04 و مشروع رادار Arduino.
  • تخزين الوضعيات في EEPROM: خزّن الوضعيات في ذاكرة EEPROM (ذاكرة تحتفظ بمحتواها دون طاقة)، كي لا تضيع عند فصل الطاقة.
  • التحكم في السرعة: استبدل السرعة الثابتة 255 بشريط تمرير في التطبيق، ومرّر قيمته إلى setSpeed().
  • الكاميرا والرؤية الحاسوبية: أضف Raspberry Pi أو ESP32-CAM لاكتشاف الأجسام، فلوحة Mega وحدها لا تستطيع معالجة الصور.
  • واي فاي (Wi-Fi): استبدل HC-05 بلوحة ESP32 للتحكم من متصفح الويب أو من نظام أتمتة منزلية.

مشروع الذراع التالي، OmArm Zero، يخطو هذه الخطوة وينتقل من Arduino إلى ESP32. وتتحكم فيه من المتصفح عبر واي فاي (Wi-Fi)، وتضيف الأجزاء اللاحقة ROS 2 وMoveIt 2 وكاميرا.

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

3 رأي حول “OmObiArm: Building a Mobile Robot with an Integrated Robotic Arm Using Arduino and Bluetooth Control”

  1. مرحبًا يا فريق OmArTronics plus،
    أرجو أن تكونوا بخير!
    معكم Ashley من JLCMC، وهي علامة تجارية فرعية تابعة لـ JLCPCB. نعمل على بناء خدمة عالمية متكاملة توفّر كل شيء في مكان واحد، ولدينا آلاف عديدة من المكونات الميكانيكية التي تناسب مختلف سيناريوهات الاستخدام والاحتياجات.

    لقد أعجبنا نهجك العملي ومشاريعك الإبداعية التي تصنعها بنفسك (DIY)، ولا سيما عملك على الأذرع الروبوتية. ويتوافق تركيزك على الحلول العملية والمبتكرة تمامًا مع مهمتنا في JLCMC المتمثلة في توفير مكونات ميكانيكية عالية الجودة. ونعتقد أن التعاون سيعود بالفائدة على الطرفين، إذ يتطابق اهتمام جمهورك بالروبوتات والأتمتة إلى حد كبير مع الحلول التي نقدمها. وستتيح لنا الشراكة معك تقديم قطعنا الدقيقة إلى مجتمع متمرس تقنيًا يتطلع إلى تقنيات ذكية وفعالة.

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

    مع أطيب التحيات،
    Ashely

    رد

أضف تعليق