يحتوي هذا المقال على روابط تسويق بالعمولة مُعلَّمة بعلامة *. بصفتي شريكًا في برنامج Amazon Associates، أربح من عمليات الشراء المؤهلة، دون أي تكلفة إضافية عليك. يساعد ذلك على إبقاء الشروحات مجانية.
OmArm Zero ذراع روبوتية مطبوعة بالطباعة ثلاثية الأبعاد (3D) وتعمل بـ ESP32: خمسة مفاصل للذراع إضافة إلى قابض، ويُتحكَّم فيها من المتصفح عبر واي فاي (WiFi).
سلسلة OmArm Zero
الجزء 1: البناء (هذا المقال) · الجزء 2: URDF + RViz + Gazebo · الجزء 3: MoveIt 2 · الجزء 4: الالتقاط والوضع بالرؤية الحاسوبية


يُحمَّل الفيديو من YouTube (Google)، وقد يضع ملفات تعريف الارتباط (Cookies). سياسة الخصوصية
نظرة سريعة
ما ستبنيه: ذراع روبوتية بست درجات حرية (DOF، أي محاور حركة مستقلة): خمسة مفاصل للذراع إضافة إلى قابض بفكين متوازيين، ولكل منها محرك سيرفو واحد. تتحكم فيها عبر واي فاي (WiFi) من هاتفك أو حاسوبك المحمول. مدى الوصول 303 mm، والحمولة 100 g (وبحد أقصى 150 g مع نابض الكتف)، وجميع الأجزاء الهيكلية مطبوعة بالطباعة ثلاثية الأبعاد (3D)، ومزود طاقة واحد بجهد 5 V.
الوقت اللازم: نحو 12 ساعة من الطباعة، إضافة إلى ما بين 8 و12 ساعة من التجميع والاختبار. التجميع الميكانيكي وحده يستغرق نحو ساعتين إلى ثلاث ساعات إذا عملت على مهل.
التكلفة الإجمالية: نحو $48 ثمنًا للقطع، بما في ذلك خيط الطباعة (Filament).
مستوى الصعوبة: متوسط. ستجمّع وصلات ميكانيكية، وتوصّل الإلكترونيات (مع بعض اللحام)، وترفع البرنامج الثابت (Firmware) إلى اللوحة. الخبرة في Arduino مفيدة لكنها ليست شرطًا.
الأدوات: طقم مفاتيح ألن (Allen)، ومفك براغي صليبي صغير (Phillips)، وقاطعة أسلاك، وكاوية لحام (اختيارية لكنها موصى بها)، وقدمة رقمية (Caliper) لفحص التفاوتات، ولقمة مفتاح (Socket) مقاس 3.8 mm أو 4 mm لأذرع السيرفو. ذراع السيرفو (Horn) هو الذراع الصغير أو القرص الذي يُثبَّت بالبراغي على عمود الخرج في السيرفو.
الملفات: حزمة الجزء 1 التي تضم ملفات STL (ملفات طباعة ثلاثية الأبعاد)، ونموذج STEP (تجميعة CAD بصيغة تفتحها معظم برامج CAD)، والبرنامج الثابت (الرابط في نهاية المقال).
1. لماذا نبني ذراعًا روبوتية من الصفر؟
كانت ذراعي السابقة ذراع روبوتية بست درجات حرية (6-DOF) بتحكم عبر البلوتوث مبنية حول Arduino Uno ووحدة البلوتوث HC-05. كانت تعمل، لكن مدى البلوتوث كان محدودًا، وكانت تحتاج إلى لوحتين لأداء ما تستطيعه شريحة واحدة، فبدأت من الصفر. سعر ESP32 هو $5، وفيه واي فاي (Wi-Fi) مدمج، لذا يحل محل Uno وHC-05 معًا.
وكانت النتيجة OmArm Zero: خمسة مفاصل للذراع إضافة إلى قابض، ووزن 535 g، ومدى وصول 303 mm، ونحو $48 ثمنًا للقطع. وعلى خلاف الأذرع التي تُباع عُدّةً جاهزة (Kit)، يعرض هذا الدليل الأرقام التي يقوم عليها كل قرار تصميمي، بدءًا بحساب عزم الدوران الذي يحدد أي سيرفو يوضع في أي مفصل.
تعمل الذراع بلوحة ESP32 المزودة بواي فاي مدمج. تُفتح واجهة الويب الخاصة بها في متصفح هاتفك أو حاسوبك المحمول، وهي مخزنة في ذاكرة الفلاش داخل الشريحة، لذلك لا تحتاج الذراع إلى موجّه (Router) ولا إلى خدمة سحابية. ويتوفر أيضًا التحكم التسلسلي عبر USB، للاختبار وللأجزاء اللاحقة الخاصة بـ ROS 2 (Robot Operating System 2، وهو إطار برمجي للتحكم في الروبوتات).
خططت لـ ROS 2 منذ البداية: البرنامج الثابت التسلسلي يستقبل زوايا المفاصل أصلًا، لذا فإن ربط ROS 2 في الجزء 3 مجرد طبقة رقيقة فوقه. في الجزء 3 يُرفع إلى اللوحة برنامج ثابت تسلسلي خاص بـ ROS 2 (omarm_zero_ros2_serial.ino)، يستقبل أمر MOVE نفسه (القسم 7).
2. متطلبات النظام والمواصفات الهندسية
قبل أن أرسم أول قطعة في CAD، دوّنت ما يجب أن تفعله الذراع.
المتطلبات الوظيفية
- مدى الوصول: 303 mm من محور الكتف إلى أطراف الفكين.
- الحمولة: 100 g في التشغيل العادي، و150 g كحد أقصى مع مساعدة النابض عند الكتف.
- درجات الحرية: خمسة مفاصل للذراع إضافة إلى قابض: دوران القاعدة، ورفع الكتف، والمرفق، ودوران الساعد (J4)، وإمالة المعصم (J5)، وقابض بفكين متوازيين.
- التحكم: واجهة ويب عبر واي فاي للاستخدام اليومي، وبروتوكول تسلسلي عبر USB للاختبار ولـ ROS 2.
- الحركة: حركة سلسة بلا تذبذب، مع سرعة قابلة للضبط من بطيئة جدًا إلى سريعة.
- القابض: فكّان متوازيان يمسكان جسمًا وزنه 100 g دون أن ينزلق.
المتطلبات غير الوظيفية
- التكلفة: نحو $48 إجمالًا، وأقل من $5 لكل سيرفو.
- قابلية الطباعة: كل قطعة تناسب سرير طباعة مقاسه 220 × 220 mm وتُطبع بخيط PLA القياسي. أطبع على Bambu Lab X1 Carbon؛ وتكفي لوحتا طباعة لجميع القطع.
- التجميع: لا حاجة إلى أدوات غير التي يملكها أي صانع (Maker) أصلًا؛ وكل جزء هيكلي مطبوع بالطباعة ثلاثية الأبعاد (3D).
- الطاقة: مزود طاقة واحد بجهد 5 V، دون منظمات جهد إضافية.
- وقت البناء: ما بين 8 و12 ساعة من التجميع والاختبار (دون احتساب الطباعة) لمن سبق له اللحام.
البنية الحركية
للذراع خمسة مفاصل دوّارة (مفاصل تدور حول محور واحد) إضافة إلى القابض:
- المفصل 1 (القاعدة): يدير الذراع بالكامل حول المحور الرأسي، بمدى حركة 180 درجة. ولأن هذا المحور موازٍ لاتجاه الجاذبية، لا يولّد وزن الذراع أي عزم دوران حوله؛ فالسيرفو يتغلب على الاحتكاك والقصور الذاتي فقط.
- المفصل 2 (الكتف): يرفع العضد (الجزء العلوي من الذراع) من الوضع الأفقي إلى الرأسي. تدور الذراع كلها حول هذه النقطة وتشدّ الجاذبية على طولها الكامل، لذا يحتاج هذا المفصل إلى أقوى سيرفو.
- المفصل 3 (المرفق): يثني الساعد. ويتحمل حملًا أقل من الكتف لأن العضد يقع خلفه.
- المفصل 4 (دوران الساعد): يدير المعصم والقابض حول محور الساعد. حمل خفيف.
- المفصل 5 (إمالة المعصم): يُميل القابض إلى الأعلى والأسفل. حمل خفيف.
- القابض: آلية بفكين متوازيين يحركها سيرفو صغير؛ ينغلق الإصبعان بشكل متماثل.
لماذا خمسة مفاصل للذراع وليس ستة؟ المفصل السادس سيضيف سيرفو آخر، ووزنًا أكبر في نهاية الذراع، وحملًا أكبر على الكتف. خمسة مفاصل تكفي لتحديد موضع القابض وضبط إمالته ودورانه، وهذا يغطي معظم مهام الالتقاط والوضع (Pick and Place). أما ما تتنازل عنه فهو حرية اختيار اتجاه القابض عند كل نقطة.
معاملات Denavit-Hartenberg
جديد في عالم الروبوتات؟ يمكنك تخطي هذا القسم. معاملات Denavit-Hartenberg (DH) هي أربعة أرقام لكل مفصل تصف موضع محور كل مفصل بالنسبة إلى المفصل السابق. لا يستخدمها الجزء 2: إذ يصف ملف URDF الهندسة نفسها بنقاط أصل المفاصل ومحاورها المصدَّرة من CAD.
يستخدم الجدول اصطلاح DH المعدَّل كما وصفه Craig. قيمه تقريبية لهندسة URDF؛ فالساعد مثلًا يبلغ طوله 98.2 mm بين محوري المفصلين في URDF.
| المفصل | إزاحة Theta (درجة) | d (mm) | a (mm) | Alpha (درجة) | ملاحظات |
|---|---|---|---|---|---|
| 1 | 0 | 50.87 | 0 | 0 | ارتفاع القاعدة، محور رأسي |
| 2 | 0 | 0 | 23.14 | -90 | الكتف، ينثني إلى الأسفل |
| 3 | 0 | 0 | 113.11 | 0 | المرفق، يمتد إلى الأمام |
| 4 | 90 | 98.50 | 0 | -90 | دوران الساعد |
| 5 | 0 | 32.05 | 0 | 90 | إمالة المعصم |
في كل صف، a هو طول الوصلة، وalpha زاوية الالتواء من محور المفصل السابق إلى محور هذا المفصل. وتمتد الإزاحة d على طول محور هذا المفصل، أما إزاحة theta فهي موضع الصفر المعتمد في الأوامر.
تحليل عزم الدوران
عزم الدوران الاسمي للسيرفو لا يعني الكثير ما لم تقارنه بالحمل. رسمت لكل مفصل مخطط الجسم الحر (رسم تخطيطي لكل القوى المؤثرة في الجزء) وحسبت عزم الدوران الساكن بهذه المعادلة.
tau_required = m_load * g * L_from_pivot * cos(theta)
m_load هي كتلة كل ما يقع بعد المفصل، وL_from_pivot المسافة من نقطة الارتكاز إلى مركز تلك الكتلة، وg تسارع الجاذبية، وtheta زاوية المفصل. يبلغ عزم الدوران أقصاه عند theta = 0، أي عندما تكون الذراع أفقية.
المفصل 1 (دوران القاعدة): لا يوجد عزم ناتج عن الجاذبية، بل احتكاك وقصور ذاتي فقط. محرك MG996R أقوى مما يحتاجه هذا المفصل، لكنني أستخدمه رغم ذلك ليتوافق مع J2 وJ3. وهو يكلّف نحو $2 أكثر من SG90 ($4 مقابل $2 في BOM، أي قائمة المواد في القسم 4).
المفصل 2 (الكتف): كل ما يتحرك فوق القاعدة يزن 326 g وفق نموذج CAD. ولتقدير بسيط ومتحفظ، أحتسب ذلك كله مع الحمولة وأضع المجموع على مسافة متوسطة تبلغ نحو 180 mm من نقطة الارتكاز. ومع حمولة 150 g يصبح المجموع 476 g:
tau_required = 0.476 kg * 9.81 m/s^2 * 0.180 m * cos(0) = 0.84 N·m ≈ 8.6 kgf·cm
مع الحمولة الاسمية 100 g يحتاج الكتف إلى نحو 7.7 kgf·cm. أما الحساب مفصلًا بمفصل باستخدام كتل CAD، مع وضع الحمولة عند أطراف الفكين على بعد 303 mm من الكتف، فيعطي 8.0 kgf·cm عند 150 g. أي أن التقدير البسيط يبقى في جانب الأمان.
عزم MG996R الاسمي 11 kgf·cm عند 6 V. وعند 5 V، وهو الجهد الذي تعمل به الذراع، يعطي نحو 8.8 kgf·cm. وهذا يعني معامل أمان (عزم الدوران المتاح مقسومًا على عزم الدوران المطلوب) قدره 1.02 عند ذروة الحمل. ولزيادة الهامش أضفت عند الكتف نابض شدّ يسحب العضد إلى الأعلى (التفاصيل في القسم 3).
المفصل 3 (المرفق): يحمل الساعد والمعصم والقابض، وتزن معًا 132 g وفق نموذج CAD. ووزنها وحده يتطلب 0.13 N·m (أي 1.3 kgf·cm). وتتدلى الحمولة عند أطراف الفكين على بعد نحو 190 mm من المرفق. ومع حمولة 150 g:
tau_required = 0.13 N·m + 0.150 kg * 9.81 m/s^2 * 0.190 m = 0.41 N·m ≈ 4.2 kgf·cm
عند 5 V يوفر MG996R نحو 8.8 kgf·cm. معامل الأمان: 2.1.
المفصل 4 (دوران الساعد): يقع وزن القابض قريبًا من محور الدوران، لذا لا يكاد يوجد عزم ناتج عن الجاذبية. ويكفي هنا SG90.
المفصل 5 (إمالة المعصم): يحمل القابض، وكتلته 50 g على بعد 20 mm، إضافة إلى الحمولة عند أطراف الفكين على بعد نحو 60 mm من محور J5. ومع حمولة 150 g:
tau_required = 0.0098 N·m + 0.150 kg * 9.81 m/s^2 * 0.060 m = 0.098 N·m ≈ 1.0 kgf·cm
يوفر سيرفو SG90 الصغير (Micro Servo) عزمًا قدره 1.8 kgf·cm. معامل الأمان: 1.8، وهو كافٍ عند 100 g وضيّق عند 150 g.
الخلاصة:
| المفصل | الحمل (g) | المسافة (mm) | المطلوب (kgf·cm) | Servo | المتاح (kgf·cm) | معامل الأمان | القيد |
|---|---|---|---|---|---|---|---|
| 1 | 476 | 0 | 0 | MG996R | 8.8 | لا ينطبق (لا يوجد حمل جاذبية) | لا يوجد |
| 2 | 476 | 180 | 8.6 | MG996R | 8.8 | 1.02 | حرج |
| 3 | 282 | حتى 190 | 4.2 | MG996R | 8.8 | 2.1 | آمن |
| 4 | 50 | 0 | ~0 | SG90 | 1.8 | لا ينطبق (لا يوجد حمل جاذبية) | آمن |
| 5 | 200 | حتى 60 | 1.0 | SG90 | 1.8 | 1.8 | ضيّق عند 150 g |
المقصود بعزم الدوران المتاح قيمته عند 5 V، وهي 8.8 kgf·cm لمحرك MG996R، و1.8 kgf·cm لمحرك SG90 الذي تذكر ورقة بياناته هذه القيمة عند 4.8 V.
المفصل 2 هو ما يحدّ من قدرة الذراع كلها: فعند حمولة 150 g يحتاج إلى 8.6 من أصل 8.8 kgf·cm المتاحة عند 5 V. ويضيف النابض هامشًا قدره نحو 2.9 kgf·cm. ومن دونه يسخن سيرفو الكتف ويبدأ بالانحراف عن موضعه بعد دقيقة أو دقيقتين، لذا سأقصر الحمولة في هذه الحالة على نحو 75 g.
حد الحمولة. عزم دوران الكتف بدلالة الحمولة P (بوحدة kg):
tau_J2 = (0.326 + P) * 9.81 * 0.180 = 0.576 + P*1.766 N·m
بمساواة هذا المقدار بقيمة 8.8 kgf·cm المتاحة عند 5 V وحلّ المعادلة بالنسبة إلى P نحصل على نحو 0.16 kg. وهذا حد أعلى ساكن: فهو يتجاهل التسارع والاحتكاك والخلوص في التروس (Backlash، أي الحركة الحرة الصغيرة بين أسنان التروس) والحرارة، كما أن العزم المستمر لمحرك MG996R عند 5 V أقل من القيمة المذكورة في ورقة البيانات. لذلك اختبرت أوزانًا حقيقية: حمولة 100 g تتحرك بسلاسة دون أي صوت إجهاد مسموع، و150 g تُمسَك بثبات بمساعدة النابض، أما 200 g فتسبب توقف السيرفو (Stall، أي يتوقف عن الحركة رغم تغذيته بالطاقة) عند الزوايا القصوى. ومن هنا جاءت المواصفة: 100 g حمولةً اسمية (طوال اليوم) و150 g حدًّا أقصى (من حين لآخر، مع النابض). وما يتجاوز 150 g يجعل السيرفو يسخن وقد يحترق.
مساحة العمل ومدى الوصول
أطوال الوصلات من ملف URDF: العضد 113.1 mm، والساعد 98.2 mm، والمعصم 32.1 mm، والقابض نحو 60 mm حتى أطراف الفكين. وعند مدّ الذراع أفقيًا، وبالقياس من محور الكتف:
reach = 113.1 + 98.2 + 32.1 + 60 = 303.4 mm ≈ 303 mm
ارتفاع القاعدة البالغ 50.9 mm رأسي، ولا يضيف شيئًا إلى مدى الوصول الأفقي.
الحيز الذي تبلغه الذراع هو تقريبًا نصف كرة نصف قطرها 303 mm فوق الطاولة، ومركزها القاعدة.
ميزانية الطاقة
كل شيء يعمل من مزود طاقة واحد بجهد 5 V، لذا يجب أن يغطي هذا المزود مجموع التيارات كلها.
- يسحب كل MG996R نحو 500 mA تحت الحمل المعتاد، وحتى 2500 mA عند التوقف (Stall).
- يسحب كل SG90 نحو 200 mA في الوضع المعتاد، وحتى 650 mA عند التوقف.
- يسحب ESP32 نحو 80 mA، وحتى 500 mA في ذروات الإرسال عبر واي فاي.
- تسحب لوحة قيادة السيرفو PCA9685 (القسم 4) نحو 10 mA.
المجموع المعتاد: 500 + 500 + 500 + 200 + 200 + 200 + 80 + 10 = 2190 mA.
مجموع الذروة عند توقف السيرفوهات الستة كلها في آن واحد: 2500 + 2500 + 2500 + 650 + 650 + 650 + 500 + 10 = 9960 mA.
تبقى الذروة النظرية البالغة 9960 mA دون التيار الاسمي البالغ 10 A لمزود الطاقة 5 V 10 A. ولا تحدث إلا إذا توقفت السيرفوهات الستة كلها في آن واحد، وهو ما لا تسببه الحركة العادية. ولو حدث ذلك لانخفض الجهد، وقد يُعاد تشغيل ESP32 (Brown-out: إعادة تشغيل سببها انخفاض جهد التغذية). شغّلت هذه الذراع لساعات بأنماط حركة عادية دون أي مشكلة في التغذية. وبالقياس على ذراعي، تسحب نحو 1.5 A حين تثبت جميع السيرفوهات في مواضعها، ونحو 4 A في الذروات أثناء الحركات السريعة لعدة مفاصل معًا. مزود 5 V 10 A هو الحد الأدنى؛ أما وحدة 15 A فتكلف نحو $3 أكثر وتترك هامشًا أكبر. أوصل مزود الطاقة عبر مقبس أسطواني (Barrel Jack) مع كتلة أطراف لولبية (البند 33)، ومن هناك أمدّ سلكين إلى طرف التغذية في PCA9685. كثير من مقابس 5.5 × 2.1 mm مصنفة لتيار من 3 إلى 5 A فقط، فتحقق من تصنيف مقبسك؛ وللحصول على 10 A كاملة استخدم مزود طاقة بأطراف لولبية.
تنبيه أمان: غذِّ السيرفوهات دائمًا من مزود الطاقة المنفصل 5 V، ولا تغذِّها أبدًا من منفذ USB في ESP32. يوفر منفذ USB بضع مئات من المللي أمبير فقط؛ بينما يسحب محرك MG996R واحد متوقف 2500 mA.
3. التصميم الميكانيكي ونموذج CAD
صممت OmArm Zero في Autodesk Inventor. ومن أجل الجزء 2 استُورد النموذج إلى Fusion 360 وصُدِّر بصيغة URDF (Unified Robot Description Format)، وهي صيغة الملفات التي يصف بها ROS 2 وصلات الروبوت ومفاصله. برنامج Inventor بارامتري: أطوال الوصلات وأبعاد تجاويف السيرفو ومقاسات الثقوب كلها متغيرات، وكل بُعد مرتبط بها يتحدث تلقائيًا عند تغيير أحدها. وهذا يغني عن إعادة رسم القطع عندما يتغير طول الكتف مثلًا.
التصميم معياري (Modular): كل مفصل تجميعة مستقلة، مصممة للطباعة بتقنية FDM (النمذجة بالترسيب المنصهر، أي طابعة الخيوط المعتادة). سماكة معظم الجدران 1.2 mm: ثلاثة محيطات (Perimeters، أي حلقات الحواف الخارجية) تضعها فوهة 0.4 mm. وقطر ثقوب M3 هو 3.2 mm؛ إذ تشق براغي M3 أسنانها بنفسها في PLA (انظر «وسائل التثبيت» في القسم 4).
تجميعة القاعدة
تتكون القاعدة من جزأين: حلقة سفلية ثابتة وقطعة علوية تدور. يستقر MG996R في الجزء السفلي، مثبتًا على رف مقوّى بأربعة براغي No.3-28 برأس عريض مقبب (البند 25). وفي المركز محمل كروي 6806ZZ (30 × 42 × 7 mm). تركب حلقته الداخلية على بروز أسطواني قطره 30 mm (Spigot، أي وتد أسطواني قصير) في base_link. وتستقر حلقته الخارجية في تجويف قطره 42 mm على الجانب السفلي من base_link_upper، ويملأ عرضه البالغ 7 mm ذلك التجويف. يحمل المحمل القاعدة العلوية ويزيل الخلوص (الارتخاء) من دوران القاعدة. ومن دونه لن يحدد موضع القاعدة العلوية سوى ذراع السيرفو، مع خلوص نصف قطري قدره 6 mm من كل الجهات، لذا اعتبر المحمل مكوّنًا إلزاميًا. يُثبَّت ذراع السيرفو بالبراغي على الجزء العلوي، فيتبع الجزء العلوي حركة السيرفو.
قطر القاعدة 85 mm، وهو ما يكفي لوقوف ثابت. وتُطبع على جزأين لتجنب الجسور (Bridging، أي الطباعة في الهواء بين نقطتي ارتكاز) فوق تجويف المحمل.
مفصل الكتف
يتحمل الكتف الحمل الأكبر، لذا نال القدر الأكبر من الاهتمام. يحمل حامل الكتف (Bracket) محرك MG996R بحيث يكون عمود الخرج أفقيًا، فيتأرجح العضد إلى الأعلى والأسفل. ويُثبَّت العضد بالبراغي على ذراع السيرفو.
يسحب نابض شدّ مثبت بين العضد والحامل المفصلَ إلى الأعلى ويعادل نحو نصف وزن الذراع. وعندما يرفع السيرفو الذراع، فإنه يعمل فقط ضد الشد المسبق للنابض إضافة إلى الوزن المتبقي. والنابض هو ما يتيح لمحرك MG996R (8.8 kgf·cm عند 5 V) أن يحمل الذراع بثبات.
يستقر النابض في مجرى على الجانب الداخلي لحامل الكتف. يُعلَّق أحد طرفيه في فتحة على الحامل، والآخر في فتحة على arm_link_1 (العضد). وعندما يكون العضد أفقيًا، وهو أدنى موضع له، يكون النابض في أقصى تمدد له ويولّد نحو 2.9 kgf·cm؛ ومع ارتفاع الذراع يرتخي النابض.
مواصفات النابض: نابض شدّ (وليس نابض ضغط)، قطره الخارجي نحو 4 mm، وسمك سلكه 0.5 mm، وطوله الحر 15 mm، وثابت النابض نحو 0.8 N/mm (القوة اللازمة لكل مليمتر من التمدد). تجده في مجموعات النوابض المتنوعة لدى متاجر العُدد بنحو $1. الثابت الدقيق ليس حاسمًا؛ فأي قيمة بين 0.5 و1.2 N/mm تفي بالغرض. النابض الأكثر صلابة يعني أن السيرفو يبذل جهدًا أقل عند الرفع وأكثر عند الخفض. اختبرت ثلاثة نوابض واستقر اختياري على نابض 0.8 N/mm لأنه بدا متوازنًا في الاتجاهين.
مفصل المرفق
يستخدم المرفق حامل السيرفو نفسه المستخدم في الكتف، لكنه لا يحتاج إلى نابض لأنه يحمل الساعد والمعصم والقابض فقط.
تجميعة المعصم
يتكون المعصم من سيرفوين SG90 متعامدين. الأول (J4) مثبت في نهاية الساعد ويدير حامل المعصم حول محور الساعد. والثاني (J5) مثبت في حامل المعصم ويُميل القابض إلى الأعلى والأسفل. أبعاد كل SG90 هي 23 × 12.2 × 29 mm. ومع سيرفوهات بهذا الصغر، لا يبعد محور J5 عن J4 سوى 32 mm، ما يبقي المعصم قصيرًا وخفيفًا.
تجميع القابض
يحوّل القابض دوران السيرفو إلى حركة إغلاق لإصبعين متوازيين. يدور ترسان مطبوعان في اتجاهين متعاكسين؛ ويحرك كل منهما إصبعًا واحدًا عبر وصلة ميكانيكية. يُثبَّت الترس الأيسر بالبراغي على ذراع السيرفو، بينما يدور الترس الأيمن بحرية على محور ويديره الترس الأيسر. ولأن الإصبعين ينغلقان بشكل متماثل، يبقى الجسم في المنتصف. التقطت به أشياء تتراوح بين أقلام التحديد (Marker) ومحركات التيار المستمر (DC) الصغيرة.
الطباعة ثلاثية الأبعاد (3D): اتجاه الطباعة وقائمة القطع وإعدادات برنامج التقطيع
لكل قطعة اتجاه طباعة محدد يقلل مادة الدعامات (هياكل مؤقتة تُطبع أسفل الأجزاء البارزة) ويجعل خطوط الطبقات تمتد في اتجاه الحمل. تُطبع الذراع العلوية مستلقية بشكل مسطح وبالطول، فتمتد خطوط الطبقات على طول المسافة التي يقع عليها حمل الانحناء؛ ولو طُبعت قائمة لانفصلت الطبقات تحت الحمل. وتُطبع تروس القابض مسطحة أيضًا، لأن الطبقات العمودية تجعل الأسنان تنكسر بالقص.
سُمك الجدران 1.2 mm، إلا في مواضع تركّز الإجهاد حيث يبلغ 2.0 mm. ويحيط بجيب المحمل في base_link_upper جدار بسُمك 5 mm (تجويف قطره 42 mm داخل نتوء قطره 52 mm، وهو الحلقة البارزة حول التجويف) حتى لا يتشقق بفعل التركيب بالضغط. التفاوتات (مثل خلوص 0.2 mm حول كل جيب سيرفو) مدمجة في تصميم CAD، لذا اطبع ملفات STL بمقياس 100 % دون أي تعويض في XY.
تتكون الذراع من 16 قطعة مطبوعة من 12 ملف STL: يُطبع gripper_link أربع مرات، و gripper_sleeve مرتين، وكل ملف آخر مرة واحدة. أرقام البنود تطابق قائمة المواد (BOM) في القسم 4. الاسمان Gear_links و Gear_rechts اسما ملفين بالألمانية: links = يسار، rechts = يمين.
| البند | ملف STL | الكمية | الوصف | اتجاه الطباعة |
|---|---|---|---|---|
| 1 | base_link.stl | 1 | قاعدة ثابتة مع جيب للسيرفو وبروز أسطواني بقطر 30 mm لتركيب المحمل | مسطّحة، والوجه السفلي للأسفل |
| 2 | base_link_upper.stl | 1 | القاعدة العلوية الدوّارة مع جيب محمل بقطر 42 mm، وتُثبَّت على ذراع السيرفو (Horn) | مسطّحة، والوجه السفلي للأسفل |
| 3 | arm_link_1.stl | 1 | الذراع العلوية (من الكتف إلى المرفق) | أفقية، مسطّحة بالطول |
| 4 | arm_link_2.stl | 1 | الساعد (من المرفق إلى المعصم) | أفقية، مسطّحة بالطول |
| 5 | arm_link_3.stl | 1 | حامل المعصم (يحمل سيرفو J5) | قائمة، ووجه القاعدة للأسفل |
| 6 | gripper_base.stl | 1 | هيكل القابض، يُثبَّت على ذراع سيرفو J5 | مسطّحة، والوجه المفتوح للأعلى |
| 7 | Gripper_2.stl | 1 | إصبع القابض الأيمن | مسطّحة |
| 8 | Gear_rechts.stl | 1 | الترس الأيمن، المُقاد، يدور بحرية على محوره | مسطّحة، والأسنان للأعلى |
| 9 | Gear_links.stl | 1 | الترس الأيسر، القائد، يُثبَّت بالبراغي على ذراع السيرفو | مسطّحة، والأسنان للأعلى |
| 10 | Gripper.stl | 1 | إصبع القابض الأيسر | مسطّحة |
| 11 | gripper_link.stl | 4 | وصلة ربط القابض | مسطّحة |
| 12 | gripper_sleeve.stl | 2 | جلبة القابض (بطانة / فاصل) | مسطّحة |
| الإجمالي | 16 |
تكفي لوحتا طباعة بمقاس 220 × 220 mm لكل القطع، لذا تصلح أيضًا الطابعات التي يكون سرير طباعتها أصغر من سرير X1 Carbon. تضم اللوحة 1 القطع الهيكلية (القاعدة ووصلات الذراع)، وتضم اللوحة 2 القابض والقطع الصغيرة.
إعدادات برنامج التقطيع (Bambu Studio):
| المعامل | القيمة |
|---|---|
| المادة | PLA (أي علامة تجارية) |
| الفوهة | 0.4 mm |
| ارتفاع الطبقة | 0.20 mm |
| ارتفاع الطبقة الأولى | 0.12 mm |
| نسبة الملء (Infill) | 15 % بنمط خطوط مستقيمة (Rectilinear) |
| عدد حلقات الجدار | 3 (= جدار بسُمك 1.2 mm) |
| الطبقات العلوية/السفلية | 4 |
| نوع الدعامات | دعامات شجرية (تلقائي) |
| زاوية تفعيل الدعامات | 45 درجة |
| الالتصاق بلوحة الطباعة | حافة Brim (إطار من طبقة واحدة حول القطعة) على base_link فقط |
تستغرق الطباعة نحو 6 ساعات لكل لوحة، أي 12 ساعة إجمالًا، وتستهلك قرابة 200 g من خيط الطباعة (Filament). تصلح هذه الإعدادات لأي طابعة FDM بفوهة 0.4 mm. فعّل الدعامات الشجرية لوصلات الذراع، لأن في جيوب السيرفو أجزاء بارزة بزاوية تقارب 50 درجة.
بعد الطباعة، أزل مادة الدعامات بقاطعة مستوية (Flush Cutter) وافحص كل جيب محمل وجيب سيرفو بالقدمة (Caliper). يجب أن يبلغ قطر البروز الأسطواني للمحمل أعلى base_link مقدار 30.0 mm (بتفاوت -0.1 mm)، وقطر جيب المحمل في الجهة السفلية من base_link_upper مقدار 42.0 mm (+0.1 mm). ويجب أن يدخل جسم MG996R (40.7 × 19.7 mm) في جيوب السيرفو بخلوص يقارب 0.2 mm من كل جانب.
يفتح ملف مشروع Bambu Studio (omarm_zero.3mf) الموجود في حزمة الجزء 1 اللوحتين وعليهما كل القطع في مواضعها.


4. قائمة المواد واختيار المكونات
يُظهر الرسم الهندسي كل قطعة مع رقم بندها؛ ويستخدم جدول قائمة المواد (BOM) الأرقام نفسها.

ملفات STL ونموذج STEP ودليل التجميع بصيغة PDF موجودة في حزمة OmArm Zero الجزء 1 (الرابط في النهاية). البرنامج الثابت (Firmware) متاح مجانًا على GitHub.
قائمة القطع الكاملة
| البند | الكمية | الجزء | الوصف | المصدر | السعر |
|---|---|---|---|---|---|
| 1 | 1 | base_link | منصة القاعدة مع جيب للسيرفو وبروز أسطواني بقطر 30 mm للمحمل | طباعة ثلاثية الأبعاد (3D) | |
| 2 | 1 | base_link_upper | القاعدة العلوية الدوّارة مع جيب محمل بقطر 42 mm، وتُثبَّت على ذراع السيرفو (Horn) | طباعة ثلاثية الأبعاد (3D) | |
| 3 | 1 | arm_link_1 | الذراع العلوية (من الكتف إلى المرفق) | طباعة ثلاثية الأبعاد (3D) | |
| 4 | 1 | arm_link_2 | الساعد (من المرفق إلى المعصم) | طباعة ثلاثية الأبعاد (3D) | |
| 5 | 1 | arm_link_3 | حامل المعصم (يحمل سيرفو J5) | طباعة ثلاثية الأبعاد (3D) | |
| 6 | 1 | gripper_base | هيكل القابض، يُثبَّت على ذراع سيرفو J5 | طباعة ثلاثية الأبعاد (3D) | |
| 7 | 1 | Gripper_2 | إصبع القابض الأيمن | طباعة ثلاثية الأبعاد (3D) | |
| 8 | 1 | Gear_rechts | ترس القابض الأيمن، المُقاد | طباعة ثلاثية الأبعاد (3D) | |
| 9 | 1 | Gear_links | ترس القابض الأيسر، القائد | طباعة ثلاثية الأبعاد (3D) | |
| 10 | 1 | القابض | إصبع القابض الأيسر | طباعة ثلاثية الأبعاد (3D) | |
| 11 | 4 | gripper_link | وصلة ربط القابض | طباعة ثلاثية الأبعاد (3D) | |
| 12 | 2 | gripper_sleeve | جلبة القابض / فاصل | طباعة ثلاثية الأبعاد (3D) | |
| 13 | 3 | سيرفو MG996R | تروس معدنية، 11 kgf·cm عند 6 V، رقمي، J1، J2، J3 | شراء | $12 (3 × $4) |
| 14 | 3 | سيرفو مصغّر SG90 | تروس بلاستيكية، 1.8 kgf·cm، تناظري، J4، J5، القابض | شراء | $6 (3 × $2) |
| 15 | 14 | ANSI B18.6.4 No.2-32 3/8″ | برغي برأس عريض (Truss)، مقاس أمريكي #2، بـ 32 سنًّا لكل إنش، بطول 3/8 إنش | شراء | ضمن طقم البراغي |
| 16 | 2 | ANSI B18.6.4 No.2-32 1/4″ | برغي برأس عريض No.2-32، بطول 1/4 إنش | شراء | ضمن طقم البراغي |
| 17 | 3 | DIN 7985 M1.6×2-Z | برغي M1.6 × 2 برأس صليبي | شراء | ضمن طقم البراغي |
| 18 | 7 | ISO 4762 M3x16 (AS 1420) | برغي M3 × 16 برأس سداسي داخلي | شراء | ضمن طقم البراغي |
| 19 | 12 | ISO 4032 M3 (AS 1112) | صامولة سداسية M3 | شراء | ضمن طقم البراغي |
| 20 | 2 | ANSI B18.6.4 No.2-32 1/2″ | برغي برأس عريض No.2-32، بطول 1/2 إنش | شراء | ضمن طقم البراغي |
| 21 | 3 | ذراع سيرفو MG955 (Horn) | ذراع سيرفو (Horn) لمحرك MG996R (الذراع المرفقة مع السيرفو) | مرفق مع السيرفو | |
| 22 | 3 | DIN 7985 M3x6-Z | برغي M3 × 6 برأس صليبي | شراء | ضمن طقم البراغي |
| 23 | 3 | ذراع سيرفو SG90 | ذراع سيرفو لمحرك SG90 | مرفق مع السيرفو | |
| 24 | 2 | ANSI B18.6.4 No.2-32 3/8″ | برغي برأس عريض No.2-32، بطول 3/8 إنش | شراء | ضمن طقم البراغي |
| 25 | 12 | ANSI B18.6.4 No.3-28 1/2″ | برغي برأس عريض No.3-28، بطول 1/2 إنش | شراء | ضمن طقم البراغي |
| 26 | 1 | محمل كروي 6806ZZ (30 × 42 × 7) | محمل كروي ذو أخدود عميق لدوران القاعدة، قطر داخلي 30 mm، قطر خارجي 42 mm، عرض 7 mm (وليس 686ZZ) | شراء | $2 |
| 27 | 1 | ESP32 DevKit | وحدة WROOM بـ 38 طرفًا، ذاكرة فلاش 4 MB، واي فاي (Wi-Fi) مدمج | شراء | $5 |
| 28 | 1 | لوحة قيادة السيرفو PCA9685 | لوحة قيادة سيرفو بـ 16 قناة | شراء | $2 |
| 29 | 1 | نابض شد | قطر خارجي 4 mm، سلك 0.5 mm، طول حر 15 mm، نحو 0.8 N/mm، لمساعدة مفصل الكتف J2 | شراء | $1 |
| 30 | 1 | مزود طاقة 5 V 10 A | تغذية السيرفوهات، مقبس أسطواني 5.5 × 2.1 mm | شراء | $8 |
| 31 | 1 | أسلاك توصيل Jumper (ذكر-ذكر) | من ESP32 إلى PCA9685 | شراء | $2 |
| 32 | 1 | أسلاك توصيل Jumper (ذكر-أنثى) | التوصيلات العامة | شراء | مع البند 31 |
| 33 | 2 | موصل مقبس أسطواني (Barrel Jack) | 5.5 × 2.1 mm، لوحة توصيل للطاقة (Breakout) | شراء | $1 |
| 34 | 1 | قصدير اللحام، مادة الصهر (Flux)، أنابيب انكماش حراري | 60/40 أو خالٍ من الرصاص، أنواع متنوعة | شراء | $3 |
| 35 | 200 g | خيط طباعة PLA | أي لون | شراء | $3 |
| طقم البراغي (البنود 15-25) | $2 | ||||
| الإجمالي (القطع المشتراة) | ~$47.50 |
البنود 1-12 قطع مطبوعة (16 قطعة من 12 ملف STL)، والبندان 13-14 هما السيرفوهات. البنود 15-25 وسائل تثبيت وأذرع سيرفو: يغطي طقم براغي متنوع واحد كل البراغي، وتأتي الأذرع مع السيرفوهات. البنود 26-35 إلكترونيات وقطع معدنية ومواد استهلاكية.
اختيار محركات السيرفو
MG996R هو السيرفو الرئيسي. تروسه المعدنية لا تنكسر أسنانها تحت الحمل. عزم دورانه المقنن 11 kgf·cm عند 6 V، وهذا بالكاد يكفي الكتف عند 5 V (القسم 2). ويُباع على أنه سيرفو رقمي: أي إن متحكمًا صغيرًا بداخله يقود المحرك بتردد أعلى من السيرفو التناظري، فيثبت على موضعه بقوة أكبر. ويستقبل النوعان إشارة التحكم نفسها بتردد 50 Hz. وبسعر يقارب $4 للقطعة، هو أرخص سيرفو بتروس معدنية وجدته. تتفاوت الجودة بين البائعين، لذا اختبر كل سيرفو باستخدام البرنامج الثابت ذي الأوامر التسلسلية قبل تركيبه.
لماذا لا نستخدم MG996R في كل المفاصل؟ بسبب الوزن والطاقة. يزن كل MG996R مقدار 55 g؛ أي إن الثلاثة تشكل 165 g من وزن الذراع البالغ 535 g. ولو وُضعت سيرفوهات MG996R في المعصم (J4، J5) لأضافت 110 g أخرى عند طرف الذراع. وعلى بعد 180 mm من محور دوران الكتف، يرفع ذلك عزم الدوران المطلوب من J2 بمقدار:
0.110 kg * 9.81 m/s^2 * 0.180 m = 0.194 N·m ≈ 2.0 kgf·cm of additional load
ولأن الكتف يعمل أصلًا عند حده الأقصى، فإن 2 kgf·cm إضافية ستجعله غير موثوق.
يزن سيرفو SG90 المصغّر 9 g؛ وثلاثة منها تزن 27 g بدلًا من 165 g. ويكفي عزمه البالغ 1.8 kgf·cm لإمالة المعصم (1.0 kgf·cm مع حمولة 150 g) ولتدوير الساعد (قريب من الصفر). لكن في المقابل، سيرفوهات SG90 تناظرية وقد ترتجف قليلًا حول الموضع المستهدف.
اختيار ESP32
ESP32 متحكم دقيق ثنائي النواة بمعمارية 32 بت وتردد ساعة 240 MHz، مع ذاكرة فلاش 4 MB وذاكرة RAM بسعة 520 KB، وواي فاي (Wi-Fi) وبلوتوث مدمجين، و16 قناة PWM. وPWM (تعديل عرض النبضة) هي إشارة النبضات التي تحدد زاوية السيرفو؛ ويولّدها ESP32 في عتاد LEDC (متحكم LED) الخاص به. سعره $5. وكانت البدائل لوحة Arduino Mega (بلا واي فاي، وتحتاج إلى وحدة لاسلكية خارجية)، ولوحة Arduino MKR WiFi 1010 (نحو $30)، ولوحة STM32 Blue Pill (بلا واي فاي).
يلبّي ESP32 كل ما أحتاجه: واي فاي دون وحدة لاسلكية منفصلة بسعر $15، وذاكرة RAM كافية لخادم ويب ولتحليل JSON (JavaScript Object Notation، وهي صيغة نصية للبيانات المهيكلة). كان بإمكان قنوات PWM الـ 16 فيه أن تقود السيرفوهات مباشرة، لكنني أستخدم بدلًا من ذلك لوحة قيادة السيرفو PCA9685 (القسم التالي).
لوحة قيادة السيرفو PCA9685
PCA9685 لوحة قيادة سيرفو. تتواصل مع ESP32 عبر I2C، وهو ناقل بسلكين (SDA ينقل البيانات و SCL ينقل إشارة الساعة)، وتولّد إشارات PWM بنفسها. وفي حالة السيرفو، تكون إشارة PWM نبضة كل 20 ms، وعرضها، وهو هنا من 0.6 إلى 2.4 ms، يحدد الزاوية.
أستخدمها لأنها تقود حتى 16 سيرفو عبر سلكَي I2C، وفيها أطراف توصيل للسيرفوهات مع خط تغذية V+ منفصل. وبذلك تبقى أطراف ESP32 حرة، ولا يمر تيار السيرفوهات عبر ESP32 أبدًا. سعر اللوحة $2، وتوفر Adafruit لها مكتبة مُختبرة تتولى صيانتها، ولا يحتاج ناقل I2C إلا إلى GPIO21 و GPIO22 على ESP32.
وسائل التثبيت والإضافات
يجمع التصميم بين وسائل تثبيت مترية وأخرى بالإنش. تثبّت براغي No.2-32 و No.3-28 ذات الرأس العريض (البنود 15، 16، 20، 24، 25) السيرفوهات في حواملها. وتشكّل براغي M3 × 16 ذات الرأس السداسي الداخلي (البند 18) مع صواميل M3 (البند 19) محاور وصلات القابض. وتثبّت براغي M3 × 6 ذات الرأس الصليبي (البند 22) أذرع السيرفو على الوصلات. وتثبّت براغي M1.6 × 2 (البند 17) سيرفوهات SG90. ويغطي طقم براغي متنوع واحد جميع المقاسات، مع قطع احتياطية.
لا حاجة إلى الحشوات الملولبة الحرارية (جلب نحاسية ملولبة تُصهر داخل البلاستيك): فبراغي M3 تُربط مباشرة في الثقوب المطبوعة، وتتشكل اللولبة في PLA بشكل نظيف دون ثقب مسبق. وإن كنت تنوي فك الذراع كثيرًا، فالحشوات تجعل اللولبة أكثر تحملًا.
اطلب المحمل الصحيح: البند 26 هو 6806ZZ (30 × 42 × 7 mm). أما 686ZZ (6 × 13 × 5 mm) فهو محمل مختلف لا يناسب أيًّا من القطعتين.
تصحيح (17 سبتمبر 2026): كانت النسخ السابقة من هذا المقال ودليل PDF وملف README على GitHub وفيديو الجزء 1 تذكر محملًا “686ZZ (6x13x5)” وجيبًا مقاسه “13.0mm”. كان ذلك خطأ نقل من جهتي: فتصميم CAD استخدم دائمًا المحمل 6806ZZ (30 × 42 × 7 mm). إن كنت قد طلبت 686ZZ من القائمة القديمة فلن يناسب، وأعتذر عن هذا الطلب الذي ذهب هدرًا. والشكر للقارئ الذي قاس قطعه المطبوعة واكتشف الخطأ.
5. دليل التجميع
قبل أن تبدأ: يجب أن تكون القطع الـ 16 كلها مطبوعة ومفحوصة بالقدمة، والسيرفوهات ووسائل التثبيت في متناول يدك. ترقيم المفاصل من J1 إلى J5 والقابض يتبع الترتيب الحركي في القسم 2؛ وفي الأوامر التسلسلية يحمل القابض رقم المفصل 6.
الخطوة 0: حرّك كل سيرفو إلى 0°، وهو موضعه الأساسي (نفّذ هذا أولًا)
تحتاج الخطوة 0 إلى الإلكترونيات من القسم 6 والبرنامج الثابت ذي الأوامر التسلسلية من القسم 7. وصّل السيرفوهات الستة بلوحة PCA9685 على طاولة العمل (دون الأذرع)، وحمّل OmArmZeroControl.ino على اللوحة، وافتح الشاشة التسلسلية (Serial Monitor) بسرعة 115200 baud (معدل الباود: سرعة النقل التسلسلي). عند بدء التشغيل يحرّك البرنامج الثابت كل سيرفو إلى 0°، وهو موضعه الأساسي؛ أرسل HOME لتكرار ذلك:
HOME
موضع 0° هذا هو المرجع لكل خطوات التجميع. لماذا قبل أي برغي؟ يعرف السيرفو زاوية عموده، لكنه لا يعرف كيف تجلس الذراع (Horn) على العمود المحزّز (عمود الخرج ذو الأسنان الدقيقة التي تمسك الذراع). إذا ثبّتَّ ذراعًا على وصلة وهي منحرفة بمقدار 45 درجة، فسيكون كل أمر زاوية بعد ذلك خاطئًا بمقدار 45 درجة.
اترك الأذرع منزوعة وانتظر حتى يتوقف كل سيرفو. لا تُدِر سيرفو موصولًا بالطاقة بيدك: فقد تنكسر أسنان تروسه. تُركَّب الأذرع أثناء التجميع: ثبّت كل ذراع على قطعتها أولًا، ثم ادفع القطعة مع الذراع على العمود بحيث تتجه القطعة في الاتجاه الذي تصفه الخطوة.
الخطوة 1: أدخل سيرفو القاعدة (J1) في base_link
ابدأ بـ base_link، وهي القطعة السفلية الثابتة. أدخل أول MG996R في جيب السيرفو المستطيل من الأعلى، وعمود الخرج متجه إلى الأعلى. يجب أن يدخل بإحكام. ثبّته بأربعة براغي No.3-28 بطول 1/2″ ذات رأس عريض (البند 25) عبر ألسنة تثبيت السيرفو. لا تبالغ في الشد: يكفي أن يكون محكمًا.

الخطوة 2: ركّب المحمل
اضغط المحمل 6806ZZ (البند 26) على البروز الأسطواني بقطر 30 mm في مركز base_link، بادئًا بالحلقة الداخلية. التركيب محكم عن قصد. إن لم يستقر في مكانه، فصنفِر البروز برفق بورق صنفرة بدرجة 120 ثم أعد المحاولة. يجب أن يدخل بضغط قوي من اليد وأن يستقر مستويًا على الدرجة عند قاعدة البروز.

الخطوة 3: ثبّت ذراع السيرفو على base_link_upper
خذ base_link_upper، وهي القطعة العلوية الدوّارة. ثبّت ذراع MG996R الصليبية الشكل (البند 21) في جهتها السفلية ببرغي M3 × 6 ذي رأس صليبي (البند 22)، في المنتصف تمامًا. تربط هذه الذراع القاعدة العلوية بعمود السيرفو.

الخطوة 4: ركّب base_link_upper على base_link
أنزل base_link_upper فوق المحمل بحيث يستقر جيبها بقطر 42 mm فوق الحلقة الخارجية، وتتعشّق الذراع مع حزوز عمود السيرفو المتمركز. إن لم يدخل الجيب فوق الحلقة الخارجية، فصنفِر داخل الجيب برفق. ثبّت الذراع ببرغي المركز الخاص بالسيرفو (المرفق معه). أدر القاعدة العلوية بيدك: يجب أن تدور بسلاسة دون أن تعلق.

الخطوة 5: ركّب سيرفو الكتف (J2)
أدخل MG996R الثاني في حامل الكتف على base_link_upper، وعمود الخرج أفقي ومتجه إلى الجانب. ثبّته بأربعة براغي No.3-28 بطول 1/2″ ذات رأس عريض (البند 25).

الخطوة 6: ثبّت ذراع السيرفو على arm_link_1 (الذراع العلوية)
ثبّت ذراع MG996R (البند 21) على arm_link_1 ببرغي M3 × 6 (البند 22). اضبط اتجاه ذراع السيرفو بحيث تتجه الذراع العلوية إلى الأمام عندما يكون السيرفو عند 0° (الموضع الأساسي).

الخطوة 7: ركّب زنبرك المساعدة
علّق زنبرك الشد (البند 29) بين base_link_upper و arm_link_1. في كلتا القطعتين شق صغير فيه نقطة تعليق على الجهة الداخلية. أدخل أحد طرفي الزنبرك في الشق الموجود على base_link_upper، ثم شُدّ الزنبرك قليلًا وأدخل الطرف الآخر في الشق الموجود على arm_link_1. عندما تكون الذراع العلوية أفقية يجب أن يكون الزنبرك مشدودًا وأن يسحب الذراع إلى الأعلى؛ فهو يخفف الحمل عن سيرفو الكتف بنحو 2.9 kgf·cm.

الخطوة 8: ركّب سيرفو المرفق (J3)
أدخل MG996R الثالث في حامل المرفق عند نهاية arm_link_1، وعمود الخرج متجه إلى الجانب. ثبّته بأربعة براغي No.3-28 بطول 1/2″ ذات رأس عريض (البند 25).

الخطوة 9: ثبّت ذراع السيرفو على arm_link_2 (الساعد)
ثبّت ذراع MG996R الثالثة (البند 21) على arm_link_2 ببرغي M3 × 6 (البند 22). اضبط اتجاهها بحيث يمتد الساعد مستقيمًا عندما يكون السيرفو عند 0°.

الخطوة 10: صِل arm_link_2 بـ arm_link_1
أدخل arm_link_2 على عمود سيرفو المرفق بحيث تتعشّق الذراع مع الحزوز، وثبّتها ببرغي المركز الخاص بالسيرفو (المرفق معه). يجب أن يتأرجح الساعد بحرية على كامل مداه.

الخطوة 11: ركّب سيرفو تدوير الساعد (J4)
أدخل أول SG90 في الجيب الدائري عند نهاية arm_link_2، وعمود الخرج متجه على امتداد الساعد. يثبت في مكانه بالكبس؛ ثم أحكم تثبيته ببرغي M1.6 × 2 (البند 17).

الخطوة 12: ثبّت ذراع السيرفو على arm_link_3 (حامل المعصم)
ثبّت ذراع SG90 (البند 23) على arm_link_3 ببرغيين No.2-32 بطول 3/8″ ذوي رأس عريض (البند 24). تربط هذه الذراع سيرفو J4 بحامل المعصم.

الخطوة 13: صِل arm_link_3 بـ arm_link_2
أدخل arm_link_3 على عمود سيرفو J4 وثبّتها ببرغي المركز الخاص بالسيرفو. يجب أن يدور حامل المعصم بحرية حول محور الساعد.

الخطوة 14: ركّب سيرفو إمالة المعصم (J5)
أدخل SG90 الثاني في الجيب الجانبي لـ arm_link_3، بزاوية قائمة على J4، وأحكم تثبيته ببرغي M1.6 × 2 (البند 17).

الخطوة 15: ثبّت ذراع السيرفو على gripper_base
ثبّت ذراع SG90 (البند 23) على gripper_base. تربط هذه الذراع سيرفو J5 بهيكل القابض.

الخطوة 16: ثبّت gripper_base على arm_link_3
أدخل gripper_base على عمود سيرفو J5 وثبّتها ببرغي المركز الخاص بالسيرفو. يجب أن يميل القابض إلى الأعلى والأسفل بحرية.

الخطوة 17: ركّب سيرفو القابض
أدخل SG90 الثالث في هيكل القابض وأحكم تثبيته ببرغي M1.6 × 2 (البند 17). يفتح هذا السيرفو الفكين ويغلقهما (المفصل 6 في الأوامر التسلسلية).

الخطوة 18: ثبّت ذراع السيرفو على ترس القابض
ثبّت ذراع SG90 الثالثة (البند 23) على الترس الأيسر القائد (Gear_links). يدير السيرفو هذا الترس، ويحرّك الترس الفكين.

الخطوة 19: ركّب تروس القابض في gripper_base
عشّق الترسين Gear_links وGear_rechts في قاعدة القابض. يجب أن تتلامس الأسنان بالكاد: إذا كان التعشيق محكمًا جدًا فلن يستطيع السيرفو الدوران، وإذا كان مرتخيًا جدًا ظهر خلوص (لعب) في القابض.

الخطوة 20: أكمل تجميع القابض
ثبّت إصبعي القابض (Gripper وGripper_2) على وصلات التروس وعلى قطع الربط gripper_link (البند 11) باستخدام براغي M3 × 16 (البند 18) وصواميل M3 (البند 19). والسيرفو غير موصول بالطاقة، أدر ذراع السيرفو (Horn) ببطء باليد: يجب أن ينغلق الإصبعان بشكل متماثل. افتح القابض بالكامل، ثم أغلقه بالكامل، ثم افتحه مرة أخرى، دون أن يعلق.

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

ثبّت ESP32 وPCA9685 بالقرب من القاعدة. وصّل أسلاك السيرفو بقنوات PCA9685 (انظر الجدول في القسم 6)، ووصّل ESP32 بـ PCA9685 عبر I2C (SDA على GPIO21، وSCL على GPIO22)، ثم وصّل مزود الطاقة 5 V بمربط الطاقة في PCA9685. بعد ذلك وصّل كابل USB وحمّل البرنامج الثابت (Firmware) على اللوحة.
6. التوصيلات ودمج الإلكترونيات
يتكون النظام الكهربائي من ثلاثة أجزاء: مزود الطاقة، ولوحة قيادة السيرفو، والمتحكم الدقيق.
بنية النظام

يتواصل ESP32 مع PCA9685 عبر I2C، ومع العالم الخارجي عبر واي فاي (WiFi) أو الاتصال التسلسلي عبر USB. تولّد لوحة PCA9685 إشارات السيرفو الست. تستمد جميع محركات السيرفو طاقتها من مزود الطاقة 5 V عبر لوحة PCA9685، وليس من ESP32.
مخطط التوصيل
- الطاقة: القطب الموجب (الأحمر) لمزود الطاقة (5 V، 10 A) إلى الطرف V+ في PCA9685، والقطب السالب (الأسود) إلى الطرف GND في PCA9685. إذا لم يكن ESP32 يتغذى عبر USB، فمدّ سلكًا من خط 5 V نفسه إلى الطرف VIN في ESP32 (مدخل الطاقة 5 V في اللوحة).
- ناقل I2C: الطرف GPIO21 (SDA) في ESP32 إلى SDA في PCA9685، والطرف GPIO22 (SCL) في ESP32 إلى SCL في PCA9685. وصّل جميع خطوط GND معًا (ESP32 وPCA9685 ومزود الطاقة).
- محركات السيرفو: لكل سيرفو ثلاثة أسلاك (V+ وGND والإشارة)، ويُركَّب على أطراف توصيل قناة واحدة في PCA9685.
| Servo | المفصل | قناة PCA9685 |
|---|---|---|
| MG996R | J1 القاعدة | CH0 |
| MG996R | J2 الكتف | CH3 |
| MG996R | J3 المرفق | CH4 |
| SG90 | J4 دوران الساعد | CH7 |
| SG90 | J5 إمالة المعصم | CH8 |
| SG90 | القابض | CH11 |


مقاس الأسلاك والتوصيلات
تكون أسلاك السيرفو عادةً بمقاس 22 AWG (المقياس الأمريكي للأسلاك؛ كلما زاد الرقم كان السلك أرفع). تولّد شريحة PCA9685 إشارة التحكم فقط، أما تيار السيرفو فيتدفق من مزود الطاقة 5 V عبر الطرف V+ وخط الطاقة في اللوحة. يسحب MG996R عند التوقف (Stall) تيارًا يصل إلى 2.5 A، لذا اجعل أسلاك تمديد السيرفو قصيرة.
وصّل مزود الطاقة 5 V بسلك مقاس 18 AWG أو 16 AWG مباشرةً إلى مربط البراغي V+ وGND في PCA9685، وليس عبر خطوط لوحة التجارب (فهي مصممة لتيار يقارب 1 A). السلك السميك يُبقي هبوط الجهد صغيرًا.
لا تحمل أسلاك I2C بين ESP32 وPCA9685 تيارًا يُذكر، لذا يمكن أن تكون رفيعة (22 AWG). استخدم أسلاكًا منفصلة وليس زوجًا مجدولًا، حتى تبقى السعة الكهربائية منخفضة.
أخطاء شائعة
- غياب الأرضي المشترك: وصّل GND الخاص بـ ESP32 وPCA9685 ومزود الطاقة معًا. من دون أرضي مشترك لا يكون لإشارات I2C والسيرفو مرجع مشترك، فتتحرك محركات السيرفو بشكل عشوائي أو لا تتحرك إطلاقًا.
- تغذية محركات السيرفو من لوحة ESP32: لا تفعل ذلك. الطرف 5V يمرّر طاقة USB فقط (نحو 500 mA من منفذ الحاسوب)، ومنظّم الجهد 3.3 V مصمَّم لتغذية ESP32 نفسه. يسحب MG996R واحد عند التوقف 2500 mA، فينهار جهد ESP32 (Brownout) ويُعاد تشغيله. استخدم دائمًا مزود الطاقة الخارجي 5 V.
- مقاومات الرفع (Pull-up) لـ I2C: يحتاج I2C إلى مقاومات رفع (Pull-up) تُبقي الخطين SDA وSCL في المستوى المرتفع ما دام لا يوجد جهاز يسحبهما إلى المستوى المنخفض. لوحة PCA9685 الجاهزة (Breakout) مزوّدة بها أصلًا، وهي كافية للأسلاك القصيرة. ذراعي تعمل من دون مقاومات إضافية. إذا كان اتصال I2C غير مستقر، فأضف مقاومات رفع بقيمة 4.7 kΩ من SDA ومن SCL إلى 3.3 V.
- عنوان I2C خاطئ: تستخدم PCA9685 عنوان I2C الافتراضي 0x40، ويفترض الكود القيمة 0x40. تغيّر وصلات اللحام A0-A5 على اللوحة هذا العنوان (A0 مغلقة = 0x41). إذا لم تكن أي وصلة مغلقة، فالعنوان هو 0x40.
- هبوط الجهد أثناء التسلسلات الطويلة: إذا انخفض الجهد أثناء تسلسل حركة طويل، فهذا يعني أن مزود الطاقة وصل إلى حدّه. استخدم مزود طاقة أكبر، أو أبطئ الحركات باستخدام الأمر SPEED.
7. البرنامج الثابت (Firmware): التحكم التسلسلي في ESP32
يعمل البرنامج الثابت التسلسلي على ESP32 ويستقبل أوامر نصية عبر الاتصال التسلسلي بـ USB. وهو أسرع طريقة لاختبار زوايا المفاصل قبل إعداد واجهة الويب.
الإعداد
ستحتاج إلى:
- Arduino IDE 2.x (أو PlatformIO).
- حزمة لوحات ESP32: افتح القائمة File > Preferences، والصق الرابط https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json في الحقل “Additional Board Manager URLs”، ثم افتح Tools > Board > Boards Manager، وابحث عن “esp32” وثبّت الحزمة “esp32 by Espressif Systems”.
- مكتبة Adafruit PWM Servo Driver: افتح القائمة Sketch > Include Library > Manage Libraries، وابحث عن “Adafruit PWM Servo Driver Library”، ثم ثبّتها.
- كابل USB-C أو micro-USB (حسب لوحة DevKit لديك) لتوصيل ESP32 بحاسوبك.
اختر Tools > Board > ESP32 Dev Module (وليس “ESP32S2” أو أي إصدار آخر) ومنفذ COM الصحيح. افتح الشاشة التسلسلية Serial Monitor (Ctrl+Shift+M) واضبط معدل الباود (Baud Rate) على 115200.
كود البرنامج الثابت
يوضّح الكود أدناه منطق OmArmZeroControl.ino. ارفع إلى اللوحة برنامج Arduino (Sketch) الموجود في مجلد Firmware ضمن حزمة المشروع (وهو متوفر أيضًا على GitHub)، فهذه هي النسخة المختبرة. تختلف بعض الأسماء والتفاصيل عن هذا الكود، ومنها نطاق عرض النبضة.
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
// Create servo driver object. Address 0x40 is the default
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);
// Servo configuration
// Standard servo: 1000 us = 0 degrees, 2000 us = 180 degrees
// But we're using a wider range for these servos:
// MG996R: 500-2500 us (but safer to limit to 600-2400 us)
// SG90: 500-2400 us
// Define which servo is on which channel
const int SERVO_BASE = 0; // J1: MG996R
const int SERVO_SHOULDER = 3; // J2: MG996R
const int SERVO_ELBOW = 4; // J3: MG996R
const int SERVO_WRIST_PITCH = 7; // J4: SG90
const int SERVO_WRIST_ROLL = 8; // J5: SG90
const int SERVO_GRIPPER = 11; // Gripper: SG90
// Servo angle limits (degrees)
const int ANGLE_MIN = 0;
const int ANGLE_MAX = 180;
// Pulse width range for servos (in microseconds)
// MG996R on 5V is roughly 500-2500 us, but we'll be conservative
const int PULSE_MIN = 600; // 0 degrees (microseconds)
const int PULSE_MAX = 2400; // 180 degrees (microseconds)
// PCA9685 frequency is 50Hz for servos (20ms per cycle)
#define SERVO_FREQ 50
// Smooth movement parameters
int targetAngles[6] = {0, 0, 0, 0, 0, 0}; // Target angles for each servo
int currentAngles[6] = {0, 0, 0, 0, 0, 0}; // Current angles
int moveSpeed = 5; // Degrees per 20ms cycle (5 = 5 deg/20ms = 250 deg/s max)
int moveInterval = 20; // Milliseconds between updates
// Serial buffer
const int BUFFER_SIZE = 128;
char serialBuffer[BUFFER_SIZE];
int bufferIndex = 0;
// Timing for smooth motion
unsigned long lastMoveTime = 0;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("OmArm Zero - 6-DOF (5+1 Gripper) Robotic Arm");
Serial.println("Initializing I2C and servo driver...");
Wire.begin(21, 22); // SDA=GPIO21, SCL=GPIO22
pwm.begin();
pwm.setOscillatorFrequency(27000000);
pwm.setPWMFreq(SERVO_FREQ);
Serial.println("PCA9685 initialized at 50Hz");
Serial.println("Sending all servos to home position (0 degrees)...");
// Move all servos to home
for (int i = 0; i < 6; i++) {
targetAngles[i] = 0;
currentAngles[i] = 0;
setServoAngle(i, 0);
}
delay(1000);
Serial.println("Ready. Send commands:");
Serial.println(" HOME - move all to 0 degrees");
Serial.println(" STATUS - print current positions");
Serial.println(" MOVE:d1,d2,d3,d4,d5,d6 - move to angles");
Serial.println(" 1,90 - move J1 to 90 degrees");
Serial.println(" SPEED:speed,interval - set move speed");
Serial.println();
lastMoveTime = millis();
}
void loop() {
// Handle serial input
if (Serial.available()) {
char c = Serial.read();
// Basic line buffering
if (c == '\n' || c == '\r') {
if (bufferIndex > 0) {
serialBuffer[bufferIndex] = '\0';
processCommand(serialBuffer);
bufferIndex = 0;
}
} else if (bufferIndex < BUFFER_SIZE - 1) {
serialBuffer[bufferIndex++] = c;
}
}
// Update servo positions smoothly
updateServoMovement();
}
void processCommand(const char* cmd) {
String input(cmd);
input.trim();
input.toUpperCase();
if (input == "HOME") {
// Move all servos to home position
for (int i = 0; i < 6; i++) {
targetAngles[i] = 0;
}
Serial.println("HOME command: moving all servos to 0 degrees");
} else if (input.startsWith("STATUS")) {
// Print current angles
Serial.print("Current angles: ");
for (int i = 0; i < 6; i++) {
Serial.print("J");
Serial.print(i + 1);
Serial.print("=");
Serial.print(currentAngles[i]);
if (i < 5) Serial.print(", ");
}
Serial.println();
} else if (input.startsWith("MOVE:")) {
// Parse MOVE:d1,d2,d3,d4,d5,d6
String params = input.substring(5);
parseAngles(params);
Serial.print("Moving to: ");
for (int i = 0; i < 6; i++) {
Serial.print(targetAngles[i]);
if (i < 5) Serial.print(", ");
}
Serial.println();
} else if (input.startsWith("SPEED:")) {
// Parse SPEED:speed,interval
String params = input.substring(6);
int commaIndex = params.indexOf(',');
if (commaIndex > 0) {
moveSpeed = params.substring(0, commaIndex).toInt();
moveInterval = params.substring(commaIndex + 1).toInt();
Serial.print("Speed set to ");
Serial.print(moveSpeed);
Serial.print(" deg/");
Serial.print(moveInterval);
Serial.println(" ms");
}
} else if (input.indexOf(',') > 0) {
// Single joint command: J,angle
int commaIndex = input.indexOf(',');
int jointNum = input.substring(0, commaIndex).toInt() - 1; // Convert 1-based to 0-based
int angle = input.substring(commaIndex + 1).toInt();
if (jointNum >= 0 && jointNum < 6 && angle >= ANGLE_MIN && angle <= ANGLE_MAX) {
targetAngles[jointNum] = angle;
Serial.print("J");
Serial.print(jointNum + 1);
Serial.print(" -> ");
Serial.println(angle);
} else {
Serial.println("Invalid joint or angle");
}
} else {
Serial.println("Unknown command");
}
}
void parseAngles(const String& params) {
// Parse comma-separated angles
int angles[6];
int angleCount = 0;
int lastComma = -1;
for (int i = 0; i <= params.length(); i++) {
if (params[i] == ',' || i == params.length()) {
if (i > lastComma + 1 && angleCount < 6) {
String numStr = params.substring(lastComma + 1, i);
angles[angleCount] = numStr.toInt();
angleCount++;
}
lastComma = i;
}
}
// Apply parsed angles
for (int i = 0; i < angleCount && i < 6; i++) {
if (angles[i] >= ANGLE_MIN && angles[i] <= ANGLE_MAX) {
targetAngles[i] = angles[i];
}
}
}
void updateServoMovement() {
unsigned long now = millis();
// Check if it's time to update servo positions
if (now - lastMoveTime >= moveInterval) {
lastMoveTime = now;
// Move each servo toward its target
for (int i = 0; i < 6; i++) {
if (currentAngles[i] != targetAngles[i]) {
int diff = targetAngles[i] - currentAngles[i];
// Move by up to moveSpeed degrees per interval
if (abs(diff) <= moveSpeed) {
currentAngles[i] = targetAngles[i];
} else if (diff > 0) {
currentAngles[i] += moveSpeed;
} else {
currentAngles[i] -= moveSpeed;
}
// Send new angle to servo
setServoAngle(i, currentAngles[i]);
}
}
}
}
void setServoAngle(int servoIndex, int angle) {
// Clamp angle to valid range
if (angle < ANGLE_MIN) angle = ANGLE_MIN;
if (angle > ANGLE_MAX) angle = ANGLE_MAX;
// Convert angle (0-180) to pulse width (PULSE_MIN to PULSE_MAX)
int pulseWidth = map(angle, 0, 180, PULSE_MIN, PULSE_MAX);
// Convert pulse width (microseconds) to PCA9685 tick count.
// The PCA9685 divides one 20ms cycle into 4096 ticks.
// So we map from the full 20000us period to the 0-4096 range.
// Example: 1500us (90°) → map(1500, 0, 20000, 0, 4096) = 307 ticks.
int pwmValue = map(pulseWidth, 0, 20000, 0, 4096);
// Get the channel for this servo
int channel = getChannelForServo(servoIndex);
// Set the PWM pulse
pwm.setPWM(channel, 0, pwmValue);
}
int getChannelForServo(int servoIndex) {
// Map servo index to channel
const int channels[6] = {
SERVO_BASE, // Index 0 -> J1
SERVO_SHOULDER, // Index 1 -> J2
SERVO_ELBOW, // Index 2 -> J3
SERVO_WRIST_PITCH, // Index 3 -> J4
SERVO_WRIST_ROLL, // Index 4 -> J5
SERVO_GRIPPER // Index 5 -> Gripper
};
return channels[servoIndex];
}
ملاحظة حول التسمية: يسمّي البرنامج الثابت سيرفوي المعصم SERVO_WRIST_PITCH (J4) وSERVO_WRIST_ROLL (J5)، وتعرضهما واجهة الويب باسمَي “J4 Wrist Pitch” و“J5 Wrist Roll”. هذه الأسماء موروثة من مرحلة سابقة؛ فالمفصل J4 يدير الساعد والمفصل J5 يُميل القابض. المهم هو أرقام القنوات.
شرح الكود
- المكتبات وإعداد I2C: تتولى مكتبة Wire اتصال I2C، وتتولى Adafruit_PWMServoDriver التحكم في PCA9685. تبدأ الدالة setup() تشغيل Wire على GPIO21 (SDA) وGPIO22 (SCL)، وتضبط تردد وحدة PWM على 50 Hz.
- قنوات السيرفو: تربط الثوابت SERVO_BASE وSERVO_SHOULDER وغيرها المفاصل (من J1 إلى J5 والقابض) بقنوات PCA9685 التالية: CH0 وCH3 وCH4 وCH7 وCH8 وCH11، بدلًا من 0 إلى 5. توزيع القنوات بفواصل بينها يمنع تكدّس الموصلات الستة ثلاثية الأطراف على جانب واحد من اللوحة. إذا كانت توصيلاتك مختلفة، فغيّر الأرقام في getChannelForServo()، فهذا الربط موجود في هذا المكان وحده.
- تحويل عرض النبضة: عرض النبضة هو ما يحدد الزاوية. القيمة 1500 µs تمثل الوضع الأوسط (90 درجة)؛ ويستخدم البرنامج الثابت 600 µs للزاوية 0 درجة و2400 µs للزاوية 180 درجة. تعدّ PCA9685 بوحداتها الخاصة (من 0 إلى 4095 وحدة عدّ في كل دورة مدتها 20 ms)، لذلك تحوّل الدالة setServoAngle() القيم من الميكروثانية إلى وحدات العدّ (Ticks).
- الحركة السلسة: يحدد الأمر هدفًا جديدًا فقط؛ وكل moveInterval ms (القيمة الافتراضية 20) تحرّك الحلقة الرئيسية كل مفصل بمقدار moveSpeed درجة (القيمة الافتراضية 5) نحو الهدف. أي 250 درجة في الثانية عند سرعة ثابتة.
- البروتوكول التسلسلي: تحلّل الدالة processCommand() الأوامر. الأمر HOME يحرّك جميع محركات السيرفو إلى 0 درجة. الأمر MOVE:a1,a2,a3,a4,a5,a6 يضبط الأهداف الستة كلها. أمر المفصل الواحد مثل “1,45” يحرّك J1 إلى 45 درجة. الأمر SPEED:speed,interval يغيّر سرعة الحركة.
- السلامة: تُرفض الأوامر التي تحتوي على رقم مفصل خارج النطاق 1-6 أو زاوية خارج النطاق 0-180، وتظهر الرسالة “Invalid joint or angle”؛ كما تحصر الدالة setServoAngle() كل قيمة ضمن 0-180 كإجراء حماية ثانٍ.

تؤدي الحلقة الرئيسية مهمتين في كل دورة: قراءة المدخلات التسلسلية، ثم تحديث مواضع محركات السيرفو. وتواصل المفاصل حركتها نحو أهدافها سواء وصل أمر جديد أم لا.
اختبار البرنامج الثابت التسلسلي
قبل رفع كود السيرفو، افحص توصيلات I2C باستخدام برنامج مسح (Scanner Sketch):
#include <Wire.h>
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
Serial.println("Scanning I2C bus...");
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.print("Found device at 0x");
Serial.println(addr, HEX);
}
}
Serial.println("Done.");
}
void loop() {}
يجب أن تظهر الرسالة “Found device at 0x40”. إذا لم يظهر شيء، فافحص توصيلات SDA/SCL. وإذا ظهر عنوان مختلف، مثل 0x41، فهذا يعني أن إحدى وصلات العنوان في لوحة PCA9685 لديك مغلقة؛ فغيّر العنوان في كود السيرفو.
بعد أن يعمل I2C، ارفع برنامج OmArmZeroControl وافتح الشاشة التسلسلية Serial Monitor (Ctrl+Shift+M) على سرعة 115200 باود. يجب أن تظهر رسالة بدء التشغيل. بعد ذلك جرّب الأوامر التالية:
HOME
تتحرك جميع محركات السيرفو إلى 0°، أي وضع البداية (Home). إذا لم يتحرك أحدها، فافحص توصيلاته.
1,180
يدور J1 (القاعدة) بالكامل في اتجاه عقارب الساعة.
1,0
يعود J1 بالدوران بالكامل عكس اتجاه عقارب الساعة إلى 0°.
MOVE:90,45,135,90,90,90
يتحرك J2 إلى 45 درجة (ترتفع الذراع)، وJ3 إلى 135 درجة (ينثني المرفق)، وتنتقل بقية المفاصل إلى 90.
STATUS
يطبع الزاوية الحالية لكل مفصل.
SPEED:2,50
يضبط السرعة على 2 درجة لكل تحديث مدته 50 ms، وهي سرعة أبطأ.
إذا لم يستجب أحد محركات السيرفو، فاستعن بجدول استكشاف الأخطاء وإصلاحها في نهاية القسم 9. وبعد أن يعمل التحكم التسلسلي بشكل موثوق، انتقل إلى واجهة الويب.
8. واجهة الويب
الأوامر النصية مناسبة للاختبار، لكنها بطيئة في الاستخدام اليومي. تمنحك واجهة الويب شريط تمرير (Slider) لكل مفصل في متصفح هاتفك أو حاسوبك المحمول.
يشغّل البرنامج الثابت OmArmZero_LittleFS لوحة ESP32 كنقطة وصول واي فاي (Access Point)، أي أن ESP32 ينشئ شبكة واي فاي خاصة به. ويقدّم واجهة الويب من ذاكرة الفلاش عبر LittleFS، وهو نظام ملفات صغير لذاكرة الفلاش. شغّل الذراع، واتصل بشبكة الواي فاي الخاصة بها، وافتح 10.10.10.1 في متصفحك.
البنية
يشغّل ESP32 ثلاث خدمات في الوقت نفسه:
- نقطة وصول واي فاي باسم “OmArmZero”،
- خادم ويب HTTP على المنفذ 80،
- منطق التحكم في السيرفو نفسه المستخدم في البرنامج الثابت التسلسلي.
واجهة الويب تطبيق أحادي الصفحة (SPA): صفحة HTML واحدة تتواصل شيفرة JavaScript فيها مع ESP32 دون إعادة تحميل الصفحة. ملفات HTML وCSS وJavaScript مخزنة في LittleFS. عند فتح 10.10.10.1 يرسل الخادم الملف index.html، وترسل JavaScript الأوامر بطلبات POST إلى واجهة API في ESP32 (العنوان /api/command).

عند سحب شريط التمرير، يرسل المتصفح طلب HTTP POST يحمل رسالة JSON صغيرة. يحلّل البرنامج الثابت الرسالة ويضبط زاوية الهدف؛ ثم تحرّك حلقة الحركة السلسة نفسها المستخدمة في البرنامج الثابت التسلسلي المفصلَ نحو الهدف خطوة بخطوة. ويطلب المتصفح /api/status كل ثانية ليعرض ما إذا كان الاتصال بالذراع لا يزال قائمًا.
أخزّن الصفحة في LittleFS بدلًا من وضعها كسلسلة نصية داخل البرنامج الثابت، لأن ملفات الويب تبقى بذلك منفصلة عن كود التحكم ويمكن أن تتضمن صورًا.
إعداد واجهة الويب
تحتاج إلى إضافة رفع LittleFS لبيئة Arduino IDE 2.x:
- نزّل ملف .vsix من صفحة الإصدار.
- انسخ ملف .vsix إلى المجلد
.arduinoIDE/pluginsفي مجلد المستخدم لديك (على WindowsC:\Users\YourName\.arduinoIDE\plugins). أنشئ المجلد إذا لم يكن موجودًا، ثم أعد تشغيل IDE. - اضغط Ctrl+Shift+P ونفّذ الأمر “Upload LittleFS to Pico/ESP8266/ESP32”.
قبل الرفع، أنشئ مجلدًا باسم “data” بجوار ملف .ino. ضع فيه index.html وstyle.css وapp.js وأي ملفات صور اختيارية. يحزم أمر الرفع مجلد data في صورة نظام ملفات LittleFS ويحمّلها على ESP32.
استكشاف أخطاء رفع LittleFS وإصلاحها.
- يفشل الرفع برسالة “Wrong board” أو يتوقف عند “Connecting…”: اختر اللوحة “ESP32 Dev Module”، لا لوحة ESP32 عامة، وتحقق من منفذ COM، وأغلق الشاشة التسلسلية (فهي تحجز المنفذ).
- ينجح الرفع لكن الصفحة تعرض “index.html not found”: مخطط التقسيم (Partition Scheme) خاطئ. افتح Tools > Partition Scheme واختر “Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)”. رغم الاسم، يحجز هذا المخطط 1.5 MB من ذاكرة الفلاش يستخدمها LittleFS لملفات الويب؛ فقد حلّ LittleFS محل SPIFFS كنظام الملفات الموصى به، لكن اسم القسم لم يُغيَّر قط. أعد الرفع بعد تغيير هذا الإعداد.
- لا يُعثر على مجلد data: يجب أن يكون اسمه “data” بالضبط (بأحرف صغيرة)، وأن يوجد في المجلد نفسه الذي فيه ملف .ino، لا في المستوى الأعلى ولا في مجلد فرعي.
ملفات واجهة الويب
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OmArm Zero Control</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>OmArm Zero</h1>
<div id="status" class="status disconnected">Disconnected</div>
</header>
<main>
<section class="control-panel">
<h2>Joint Control</h2>
<div class="joint-group">
<label for="j1">J1 Base (0-180°)</label>
<input type="range" id="j1" min="0" max="180" value="0" class="slider">
<span id="j1-value">0</span>°
</div>
<div class="joint-group">
<label for="j2">J2 Shoulder (0-180°)</label>
<input type="range" id="j2" min="0" max="180" value="0" class="slider">
<span id="j2-value">0</span>°
</div>
<div class="joint-group">
<label for="j3">J3 Elbow (0-180°)</label>
<input type="range" id="j3" min="0" max="180" value="0" class="slider">
<span id="j3-value">0</span>°
</div>
<div class="joint-group">
<label for="j4">J4 Wrist Pitch (0-180°)</label>
<input type="range" id="j4" min="0" max="180" value="0" class="slider">
<span id="j4-value">0</span>°
</div>
<div class="joint-group">
<label for="j5">J5 Wrist Roll (0-180°)</label>
<input type="range" id="j5" min="0" max="180" value="0" class="slider">
<span id="j5-value">0</span>°
</div>
<div class="joint-group">
<label for="gripper">Gripper (0-180°)</label>
<input type="range" id="gripper" min="0" max="180" value="0" class="slider">
<span id="gripper-value">0</span>°
</div>
</section>
<section class="settings">
<h2>Settings</h2>
<div class="setting-group">
<label for="speed">Movement Speed (deg/update)</label>
<input type="number" id="speed" min="1" max="20" value="5">
</div>
<button id="homeButton" class="button primary">Home Position</button>
</section>
<section class="sequences">
<h2>Sequences</h2>
<button id="recordButton" class="button">Record Sequence</button>
<button id="playButton" class="button" disabled>Play Sequence</button>
<button id="clearButton" class="button">Clear Sequence</button>
<textarea id="sequenceJSON" placeholder="Recorded sequence will appear here"></textarea>
<input type="file" id="loadSequence" accept=".json" style="display:none">
<button id="loadButton" class="button">Load Sequence</button>
</section>
</main>
<footer>
<p>OmArm Zero v1.0 - 6-DOF (5+1 Gripper) Robotic Arm</p>
</footer>
</div>
<script src="app.js"></script>
</body>
</html>
style.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 10px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 100vh;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.status {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.status.connected {
background: #4caf50;
color: white;
}
.status.disconnected {
background: #f44336;
color: white;
}
main {
flex: 1;
padding: 20px;
overflow-y: auto;
}
section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
section:last-child {
border-bottom: none;
}
section h2 {
font-size: 18px;
margin-bottom: 15px;
color: #333;
}
.joint-group {
display: flex;
align-items: center;
margin-bottom: 15px;
gap: 10px;
}
.joint-group label {
flex: 0 0 150px;
font-size: 14px;
color: #555;
}
.joint-group input[type="range"] {
flex: 1;
height: 6px;
border-radius: 3px;
background: #ddd;
outline: none;
-webkit-appearance: none;
appearance: none;
}
.joint-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.joint-group input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.joint-group span {
flex: 0 0 40px;
text-align: right;
font-weight: bold;
color: #333;
}
.setting-group {
margin-bottom: 15px;
}
.setting-group label {
display: block;
font-size: 14px;
margin-bottom: 5px;
color: #555;
}
.setting-group input[type="number"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.button {
display: inline-block;
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
background: #f0f0f0;
color: #333;
margin-right: 10px;
margin-bottom: 10px;
}
.button:hover {
background: #e0e0e0;
}
.button.primary {
background: #667eea;
color: white;
}
.button.primary:hover {
background: #5568d3;
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#sequenceJSON {
width: 100%;
height: 120px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
resize: vertical;
margin: 10px 0;
}
footer {
background: #f5f5f5;
color: #999;
text-align: center;
padding: 15px;
font-size: 12px;
border-top: 1px solid #eee;
}
@media (max-width: 600px) {
header h1 {
font-size: 22px;
}
main {
padding: 15px;
}
.joint-group label {
flex: 0 0 120px;
}
}
app.js:
// OmArm Zero Control Web App
const API_BASE = "/api";
const STATUS_CHECK_INTERVAL = 1000;
const sliders = {
j1: document.getElementById("j1"),
j2: document.getElementById("j2"),
j3: document.getElementById("j3"),
j4: document.getElementById("j4"),
j5: document.getElementById("j5"),
gripper: document.getElementById("gripper")
};
const sliderValues = {
j1: document.getElementById("j1-value"),
j2: document.getElementById("j2-value"),
j3: document.getElementById("j3-value"),
j4: document.getElementById("j4-value"),
j5: document.getElementById("j5-value"),
gripper: document.getElementById("gripper-value")
};
const statusDisplay = document.getElementById("status");
const speedInput = document.getElementById("speed");
const homeButton = document.getElementById("homeButton");
const recordButton = document.getElementById("recordButton");
const playButton = document.getElementById("playButton");
const clearButton = document.getElementById("clearButton");
const loadButton = document.getElementById("loadButton");
const sequenceJSON = document.getElementById("sequenceJSON");
const loadSequenceInput = document.getElementById("loadSequence");
let isConnected = false;
let isRecording = false;
let recordedSequence = [];
// Initialize
document.addEventListener("DOMContentLoaded", () => {
// Slider listeners
for (const [id, slider] of Object.entries(sliders)) {
slider.addEventListener("input", (e) => {
const angle = e.target.value;
sliderValues[id].textContent = angle;
sendCommand(id, angle);
});
}
// Button listeners
homeButton.addEventListener("click", () => {
sendCommand("HOME", null);
// Reset sliders to home position
for (const slider of Object.values(sliders)) {
slider.value = 0;
}
for (const value of Object.values(sliderValues)) {
value.textContent = 0;
}
});
recordButton.addEventListener("click", () => {
if (!isRecording) {
recordedSequence = [];
isRecording = true;
recordButton.textContent = "Stop Recording";
recordButton.classList.add("recording");
} else {
isRecording = false;
recordButton.textContent = "Record Sequence";
recordButton.classList.remove("recording");
saveSequenceToJSON();
}
});
playButton.addEventListener("click", () => {
playRecordedSequence();
});
clearButton.addEventListener("click", () => {
recordedSequence = [];
sequenceJSON.value = "";
playButton.disabled = true;
});
loadButton.addEventListener("click", () => {
loadSequenceInput.click();
});
loadSequenceInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
try {
recordedSequence = JSON.parse(event.target.result);
saveSequenceToJSON();
playButton.disabled = false;
alert("Sequence loaded successfully");
} catch (err) {
alert("Failed to parse sequence file: " + err.message);
}
};
reader.readAsText(file);
}
});
speedInput.addEventListener("change", () => {
sendCommand("SPEED", speedInput.value);
});
// Check connection status periodically
checkConnection();
setInterval(checkConnection, STATUS_CHECK_INTERVAL);
});
function sendCommand(cmd, value) {
let fullCmd = cmd;
if (cmd === "HOME") {
fullCmd = "HOME";
} else if (cmd === "SPEED") {
fullCmd = "SPEED:" + value + ",20";
} else if (["j1", "j2", "j3", "j4", "j5", "gripper"].includes(cmd)) {
// Single joint command
const jointNum = cmd === "gripper" ? 6 : parseInt(cmd.slice(1));
fullCmd = jointNum + "," + value;
// Record if recording
if (isRecording) {
recordedSequence.push({
j1: sliders.j1.value,
j2: sliders.j2.value,
j3: sliders.j3.value,
j4: sliders.j4.value,
j5: sliders.j5.value,
gripper: sliders.gripper.value,
timestamp: Date.now()
});
}
}
fetch(API_BASE + "/command", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cmd: fullCmd })
}).catch(err => console.log("Command send error:", err));
}
function checkConnection() {
fetch(API_BASE + "/status")
.then(response => response.json())
.then(data => {
isConnected = true;
statusDisplay.textContent = "Connected";
statusDisplay.classList.remove("disconnected");
statusDisplay.classList.add("connected");
})
.catch(err => {
isConnected = false;
statusDisplay.textContent = "Disconnected";
statusDisplay.classList.remove("connected");
statusDisplay.classList.add("disconnected");
});
}
function saveSequenceToJSON() {
sequenceJSON.value = JSON.stringify(recordedSequence, null, 2);
playButton.disabled = recordedSequence.length === 0;
}
function playRecordedSequence() {
if (recordedSequence.length === 0) return;
playButton.disabled = true;
let index = 0;
const playStep = () => {
if (index >= recordedSequence.length) {
playButton.disabled = false;
return;
}
const step = recordedSequence[index];
const delay = index > 0 ? recordedSequence[index].timestamp - recordedSequence[index - 1].timestamp : 0;
setTimeout(() => {
for (const joint of ["j1", "j2", "j3", "j4", "j5", "gripper"]) {
sliders[joint].value = step[joint];
sliderValues[joint].textContent = step[joint];
sendCommand(joint, step[joint]);
}
index++;
playStep();
}, Math.max(delay, 100));
};
playStep();
}
البرنامج الثابت للتحكم عبر الويب
يشبه OmArmZero_LittleFS.ino البرنامج الثابت التسلسلي، لكنه يضيف واي فاي وخادم HTTP. الكود المعروض مختصر؛ ارفع إلى اللوحة البرنامج الكامل من مجلد Firmware في حزمة المشروع:
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
#include <WiFi.h>
#include <WebServer.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);
WebServer server(80);
// WiFi AP credentials
const char* SSID = "OmArmZero";
const char* PASSWORD = "omarmcontrol";
// Same servo configuration as serial version
const int SERVO_BASE = 0;
const int SERVO_SHOULDER = 3;
const int SERVO_ELBOW = 4;
const int SERVO_WRIST_PITCH = 7;
const int SERVO_WRIST_ROLL = 8;
const int SERVO_GRIPPER = 11;
const int ANGLE_MIN = 0;
const int ANGLE_MAX = 180;
const int PULSE_MIN = 600;
const int PULSE_MAX = 2400;
#define SERVO_FREQ 50
int targetAngles[6] = {0, 0, 0, 0, 0, 0};
int currentAngles[6] = {0, 0, 0, 0, 0, 0};
int moveSpeed = 5;
int moveInterval = 20;
unsigned long lastMoveTime = 0;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("OmArm Zero - Web Control Interface");
// Initialize LittleFS
if (!LittleFS.begin()) {
Serial.println("LittleFS mount failed");
return;
}
Serial.println("LittleFS mounted");
// Initialize I2C and servo driver
Wire.begin(21, 22);
pwm.begin();
pwm.setOscillatorFrequency(27000000);
pwm.setPWMFreq(SERVO_FREQ);
// Move all to home
for (int i = 0; i < 6; i++) {
setServoAngle(i, 0);
}
// Start WiFi AP
WiFi.mode(WIFI_AP);
WiFi.softAP(SSID, PASSWORD);
delay(500); // Wait for AP interface to fully initialize
// Set static IP (must be AFTER softAP on ESP32)
IPAddress local_IP(10, 10, 10, 1);
IPAddress gateway(10, 10, 10, 1);
IPAddress subnet(255, 255, 255, 0);
WiFi.softAPConfig(local_IP, gateway, subnet);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
// Set up web server routes
server.on("/", handleRoot);
server.on("/api/command", HTTP_POST, handleCommand);
server.on("/api/status", HTTP_GET, handleStatus);
server.onNotFound(handleNotFound);
server.begin();
Serial.println("Web server started");
lastMoveTime = millis();
}
void loop() {
server.handleClient();
updateServoMovement();
}
void handleRoot() {
if (LittleFS.exists("/index.html")) {
File file = LittleFS.open("/index.html", "r");
server.streamFile(file, "text/html");
file.close();
} else {
server.send(404, "text/plain", "index.html not found");
}
}
void handleCommand() {
if (server.hasArg("plain")) {
String body = server.arg("plain");
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, body);
if (!error) {
String cmd = doc["cmd"];
processCommand(cmd.c_str());
server.send(200, "application/json", "{\"status\":\"ok\"}");
} else {
server.send(400, "application/json", "{\"error\":\"JSON parse error\"}");
}
} else {
server.send(400, "application/json", "{\"error\":\"No command\"}");
}
}
void handleStatus() {
StaticJsonDocument<256> doc;
doc["connected"] = true;
doc["angles"][0] = currentAngles[0];
doc["angles"][1] = currentAngles[1];
doc["angles"][2] = currentAngles[2];
doc["angles"][3] = currentAngles[3];
doc["angles"][4] = currentAngles[4];
doc["angles"][5] = currentAngles[5];
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
void handleNotFound() {
// Try to serve the requested file from LittleFS
String path = server.uri();
if (path.endsWith("/")) {
path += "index.html";
}
if (LittleFS.exists(path)) {
File file = LittleFS.open(path, "r");
String contentType = "application/octet-stream";
if (path.endsWith(".html")) contentType = "text/html";
else if (path.endsWith(".css")) contentType = "text/css";
else if (path.endsWith(".js")) contentType = "application/javascript";
else if (path.endsWith(".png")) contentType = "image/png";
else if (path.endsWith(".jpg")) contentType = "image/jpeg";
server.streamFile(file, contentType);
file.close();
} else {
server.send(404, "text/plain", "File not found");
}
}
void processCommand(const char* cmd) {
String input(cmd);
input.trim();
input.toUpperCase();
if (input == "HOME") {
for (int i = 0; i < 6; i++) {
targetAngles[i] = 0;
}
} else if (input.startsWith("MOVE:")) {
String params = input.substring(5);
parseAngles(params);
} else if (input.startsWith("SPEED:")) {
String params = input.substring(6);
int commaIndex = params.indexOf(',');
if (commaIndex > 0) {
moveSpeed = params.substring(0, commaIndex).toInt();
moveInterval = params.substring(commaIndex + 1).toInt();
}
} else if (input.indexOf(',') > 0) {
int commaIndex = input.indexOf(',');
int jointNum = input.substring(0, commaIndex).toInt() - 1;
int angle = input.substring(commaIndex + 1).toInt();
if (jointNum >= 0 && jointNum < 6 && angle >= ANGLE_MIN && angle <= ANGLE_MAX) {
targetAngles[jointNum] = angle;
}
}
}
void parseAngles(const String& params) {
int angles[6];
int angleCount = 0;
int lastComma = -1;
for (int i = 0; i <= params.length(); i++) {
if (params[i] == ',' || i == params.length()) {
if (i > lastComma + 1 && angleCount < 6) {
String numStr = params.substring(lastComma + 1, i);
angles[angleCount] = numStr.toInt();
angleCount++;
}
lastComma = i;
}
}
for (int i = 0; i < angleCount && i < 6; i++) {
if (angles[i] >= ANGLE_MIN && angles[i] <= ANGLE_MAX) {
targetAngles[i] = angles[i];
}
}
}
void updateServoMovement() {
unsigned long now = millis();
if (now - lastMoveTime >= moveInterval) {
lastMoveTime = now;
for (int i = 0; i < 6; i++) {
if (currentAngles[i] != targetAngles[i]) {
int diff = targetAngles[i] - currentAngles[i];
if (abs(diff) <= moveSpeed) {
currentAngles[i] = targetAngles[i];
} else if (diff > 0) {
currentAngles[i] += moveSpeed;
} else {
currentAngles[i] -= moveSpeed;
}
setServoAngle(i, currentAngles[i]);
}
}
}
}
void setServoAngle(int servoIndex, int angle) {
if (angle < ANGLE_MIN) angle = ANGLE_MIN;
if (angle > ANGLE_MAX) angle = ANGLE_MAX;
int pulseWidth = map(angle, 0, 180, PULSE_MIN, PULSE_MAX);
int pwmValue = map(pulseWidth, 0, 20000, 0, 4096);
int channel = getChannelForServo(servoIndex);
pwm.setPWM(channel, 0, pwmValue);
}
int getChannelForServo(int servoIndex) {
const int channels[6] = {
SERVO_BASE,
SERVO_SHOULDER,
SERVO_ELBOW,
SERVO_WRIST_PITCH,
SERVO_WRIST_ROLL,
SERVO_GRIPPER
};
return channels[servoIndex];
}
استخدام واجهة الويب
بعد رفع البرنامج الثابت وملفات LittleFS:
- شغّل الذراع.
- على هاتفك أو حاسوبك المحمول، افتح قائمة شبكات الواي فاي واتصل بالشبكة “OmArmZero”. كلمة المرور هي “omarmcontrol” (وكلاهما محدد في سطري البرنامج الثابت const char* SSID وconst char* PASSWORD).
- افتح 10.10.10.1 في المتصفح. تعرض الصفحة ستة أشرطة تمرير، وحقلًا للسرعة، وأزرارًا لوضع البداية والتسجيل والتشغيل.
- اسحب أحد أشرطة التمرير: تتحرك الذراع فورًا.
- اضغط Home: تتحرك جميع المفاصل إلى 0°، أي وضع البداية.
- اضغط Record، وحرّك الذراع عبر تسلسل من الحركات، ثم اضغط Stop Recording. يظهر التسلسل بصيغة JSON في مربع النص أدناه.
- اضغط Play لتكرار التسلسل.
- لتصدير التسلسل، انسخ نص JSON من مربع النص إلى ملف .json. ويقرأ الزر Load Sequence هذا الملف لاحقًا.
في اختباراتي تستجيب واجهة الويب دون تأخير ملحوظ. أستخدم الهاتف في الغالب: أمسكه بيد واحدة وأقف بجوار الذراع مباشرةً أثناء حركتها.
تُظهر لقطات الشاشة واجهة الويب الموجودة في حزمة المشروع (والمتوفرة أيضًا على GitHub). تبدو مختلفة عن الكود المختصر أعلاه، لكنها ترسل الأوامر نفسها. على الحاسوب المكتبي تتكون من ثلاثة أعمدة: صورة الذراع على اليسار، وأشرطة التمرير وزر HOME في الوسط، ومحرر التسلسلات على اليمين. في محرر التسلسلات تحفظ الوضعيات بالزر “+ Add Current Pose” ثم تعيد تشغيلها؛ وتعمل أيقونتا السهمين على تصدير التسلسل واستيراده كملف JSON.

على الهاتف يتكيف التصميم مع حجم الشاشة: تترتب أشرطة التمرير عموديًا، وينتقل محرر التسلسلات إلى أسفل عناصر التحكم. لقطة شاشة الهاتف مأخوذة من إصدار سابق من البرنامج الثابت كان يستخدم العنوان الافتراضي لـ ESP32 وهو 192.168.4.1؛ أما البرنامج الثابت الحالي فيستخدم 10.10.10.1.

9. الاختبار والتحقق
اختبار نطاق حركة السيرفو
لكل سيرفو نطاق اسمي يبلغ 0-180 درجة، لكن الحدود الميكانيكية وتفاوتات الطباعة قد تقلّصه. أرسل كل مفصل إلى 0 ثم إلى 180، وتحقق من أنه يصل إلى الطرفين دون أن يعلق:
Serial commands: 1,0 (J1 fully counterclockwise) 1,180 (J1 fully clockwise) 2,0 (J2 lowered) 2,180 (J2 raised) 3,0 (J3 elbow down) 3,180 (J3 elbow up) 4,0 (J4 forearm rotation, one end stop) 4,180 (J4 forearm rotation, other end stop) 5,0 (J5 wrist tilt, one end stop) 5,180 (J5 wrist tilt, other end stop) 6,0 (Gripper open) 6,180 (Gripper closed)
بعد كل أمر، أنصت لأي صوت طحن، وتحسّس المفصل بيدك للتأكد من أنه لا يعلق. إذا لم يصل أحد المفاصل إلى حدّه بسلاسة، فأرخِ عناصر التثبيت وحاول مرة أخرى، وتأكد من أن أذرع السيرفو مركّبة بالكامل على المحاور.
اختبار الدقة
أرسل أمرًا بزاوية معينة، ثم قِس الزاوية الفعلية بمنقلة. يجب أن يستقر المفصل ضمن هامش 5 درجات. إذا كان الانحراف ثابتًا بالمقدار نفسه دائمًا، فهذا يعني أن ذراع السيرفو (Horn) مركّب بزاوية خاطئة: كرّر الخطوة 0 لهذا السيرفو.
اختبار السرعة
أرسل حركة مسح من 0 إلى 180 وقِس زمنها:
1,0 (note the time) 1,180 (note the new time)
صوّر حركة المسح بهاتفك (بالتصوير البطيء إن توفر) وعُدّ الإطارات من البداية حتى التوقف. عند السرعة الافتراضية (5 درجات كل 20 ms = 250 درجة في الثانية) تستغرق حركة مسح بمقدار 180 درجة نحو 720 ms؛ وقد قست 750 ms. إذا اختلفت نتيجتك كثيرًا، فاضبط moveSpeed باستخدام الأمر SPEED.
اختبار الحمولة
يزن القابض نفسه 50 g. ضع أوزان اختبار في القابض وتحقق من أن الذراع تحافظ على وضعها.
- أرسل الأمر HOME. عند 0° (وضع البداية) يتجه العضد إلى الأمام ويكون الساعد مستقيمًا (الخطوتان 6 و9): أي امتداد أفقي كامل، حيث يبلغ عزم الدوران على الكتف أعلى قيمة له.
- ثبّت وزنًا مقداره 50 g في القابض وراقب أي انحراف تدريجي. يجب أن تحافظ الذراع على وضعها دون اهتزاز.
- كرّر الاختبار بأوزان 100 g و150 g (مع تركيب نابض المساعدة) و200 g، وقارن النتائج بجدول النتائج أدناه. لا تشغّل الذراع بحمولة 200 g: فهي تتجاوز المواصفات.
اختبار مدى الواي فاي
اختبر واجهة الويب من مسافات مختلفة وعبر الجدران:
- 2 m، مع خط رؤية مباشر: لا يوجد تأخير محسوس.
- 5 m، عبر جدار: لا يزال الأداء جيدًا.
- 10 m: التأخير ملحوظ، لكن الذراع لا تزال تستجيب.
- في مكان مفتوح: نحو 15 m مع خط رؤية مباشر (حسب قياسي).
- لا يعمل الاتصال عبر المنزل بأكمله. يشغّل ESP32 نقطة وصول خاصة به ولا يستخدم جهاز التوجيه (الراوتر) لديك.
اختبار قابلية التكرار
أرسل الذراع إلى وضع معين (مثلًا 45, 90, 90, 90, 90, 90)، ثم أعدها إلى وضع البداية، ثم أرجعها إلى الوضع نفسه. قِس الخطأ بمنقلة أو بالعين. كرّر ذلك 10 مرات. إذا تزايد الخطأ، فهذا يشير إلى مشكلة في السيرفو أو إلى عنصر تثبيت مرتخٍ.
اختبار تشغيل التسلسل
سجّل تسلسل التقاط ووضع بسيطًا: أنزل الذراع، وأغلق القابض، وارفع الذراع، وحرّكها إلى الجانب، وأنزلها، وافتح القابض، ثم أعِدها إلى وضعها الأول. شغّل التسلسل 10 مرات. يشير تخطّي بعض الخطوات أو الارتجاف إلى مشكلة في البرنامج الثابت (غالبًا تجاوز سعة المخزن المؤقت إذا كان التسلسل طويلًا جدًا).
ملخص نتائج الاختبارات
القياسات على ذراع OmArm Zero العاملة لدي:
| الاختبار | النتيجة | ملاحظات |
|---|---|---|
| نطاق السيرفو | حركة كاملة 0-180° | حركة سلسة دون تعليق |
| الدقة (السكونية) | ±3 درجات | ضمن المواصفات |
| السرعة (مسح 0-180) | 750 ms | يطابق القيمة النظرية 5 درجات/20 ms |
| حمولة 50 g | تحافظ على وضعها | لا يوجد انثناء مرئي |
| حمولة 100 g | تحافظ على وضعها | انثناء طفيف عند أقصى مدى للوصول |
| حمولة 150 g | تحافظ على وضعها مع النابض | اهتزاز طفيف، مقبول |
| حمولة 200 g | السيرفو يعمل تحت إجهاد | غير موصى به |
| مدى الواي فاي | 15 m مع خط رؤية مباشر | معتاد في وضع نقطة الوصول (AP) |
| قابلية التكرار | ±2 درجة | ثابتة على مدى 10 دورات |
| تشغيل التسلسل | 10/10 مرات تشغيل متطابقة | دون تخطّي أي خطوة |
إذا فشل أحد الاختبارات، فاستعن بهذا الجدول:
| العَرَض | الفحص | الحل |
|---|---|---|
| لا يتحرك أي سيرفو | قِس وجود 5 V على الطرف V+ في PCA9685؛ وشغّل برنامج مسح I2C | أصلح توصيلات مزود الطاقة؛ وأصلح SDA/SCL أو عنوان I2C |
| سيرفو واحد لا يتحرك، والبقية تتحرك | وصّل سيرفو آخر بالقناة نفسها؛ وتحقق من رقم القناة في getChannelForServo() | إذا تحرك السيرفو الثاني، فالأول تالف: استبدله. وإذا لم يتحرك هو أيضًا، فصحّح رقم القناة؛ وإذا كان الرقم صحيحًا، فهذه القناة لا تُخرج إشارة PWM |
| السيرفو يتحرك لكنه ضعيف | قِس جهد مزود الطاقة بجهاز ملتيميتر أثناء الحركة | إذا هبط الجهد نحو 4 V، فمزود الطاقة أصغر من اللازم: استخدم مزودًا أكبر |
| السيرفو يرتجف | المكثف على V+ وGND؛ سرعة الحركة | الحم مكثفًا كهروليتيًا بسعة 470-1000 µF بين V+ وGND على PCA9685 (في اللوحة مكان مخصص له)، وخفّض السرعة (SPEED:2,20) |
| الذراع تتحرك إلى زوايا خاطئة | موضع ذراع السيرفو (Horn) على محور السيرفو | كرّر الخطوة 0 لهذا السيرفو |
| الواي فاي بطيء | أجسام معدنية قريبة من الذراع أو من هوائي ESP32 | أبعد الهوائي (إذا كان خارجيًا) أو الذراع عن الأجسام المعدنية؛ فالمعدن يحجب إشارة الواي فاي على 2.4 GHz |
10. ما القادم
الجزء 2 يصدّر نموذج CAD إلى URDF ويعرض الذراع في RViz (عارض 3D الخاص بـ ROS 2) وفي محاكي الروبوتات Gazebo. الجزء 3 يضيف تخطيط الحركة باستخدام MoveIt 2. تضبط أشرطة التمرير زوايا المفاصل؛ أما في المهام الحقيقية فتحدد موضعًا بدلًا من ذلك، مثل (200, 100, 50) mm، ويحسب MoveIt زوايا المفاصل (الحركيات العكسية). الجزء 4 يضيف كاميرا وعلامات ArUco (مربعات مطبوعة بالأبيض والأسود يمكن للكاميرا تحديد موقعها) لمهام الالتقاط والوضع.
تنزيل ملفات المشروع
تحتوي حزمة OmArm Zero الجزء 1 (14.90 €) على ملفات STL وSTEP و3MF (ملف 3MF هو مشروع Bambu Studio)، والبرنامج الثابت، ومخطط التوصيل، ودليل التجميع بصيغة PDF. أما البرنامج الثابت وحده فمتاح مجانًا على GitHub.
احصل على ملفات مشروع OmArm Zero
احصل على حزمة OmArm Zero الجزء 1 (14.90 €) • البرنامج الثابت ونظرة عامة على السلسلة على GitHub
البرنامج الثابت مرخّص بموجب رخصة MIT. ملفات CAD ودليل PDF مخصصة للاستخدام الشخصي: اطبع الذراع وجمّعها وعدّلها لنفسك، لكن يُرجى عدم مشاركة الملفات أو بيعها.
ملاحظات ختامية
إذا واجهتك مشكلة، فراجع أولًا قسم Issues على GitHub؛ وإذا لم تجد سؤالك هناك، فافتح Issue جديدًا. وصلتني مئات الأسئلة حول الذراع الأصلية بست درجات حرية (6-DOF)، وأحاول الإجابة عنها جميعًا. إذا بنيت نسختك الخاصة أو عدّلت التصميم، فاترك تعليقًا أدناه أو أشِر إليّ (Tag) في منشورك.
مقارنة بين كل أذرعي الروبوتية: كيف تبني ذراعًا روبوتية بست محاور في المنزل يشرح ماذا يعني ست محاور، ويضع التصاميم الأربعة في جدول واحد، ويخبرك أيها تبني أولًا.
أي كاميرا Raspberry Pi يمكن استخدامها