يحتوي هذا المقال على روابط تسويق بالعمولة مُعلَّمة بعلامة *. بصفتي شريكًا في برنامج Amazon Associates، أربح من عمليات الشراء المؤهلة، دون أي تكلفة إضافية عليك. يساعد ذلك على إبقاء الشروحات مجانية.
1. لماذا هذا المستشعر ومقدمة
HC-SR04 وحدة لقياس المسافة بالموجات فوق الصوتية: أسطوانتان معدنيتان (محوّلا الموجات فوق الصوتية) على لوحة صغيرة. تُرسل إحداهما دفعة صوتية أعلى من أن تسمعها الأذن، وتُنصت الأخرى للصدى. يقيس المسافة من نحو 2 cm إلى 400 cm، ويعمل بجهد 5 V، ويُعبّر عن المسافة بعرض نبضة واحدة. سعره نحو يورو واحد، ولهذا تجده في كل مجموعة للمبتدئين تقريبًا. ومن استخداماته المعتادة: مساعد ركن السيارة، وروبوت يتوقف قبل العائق، ومقياس لمنسوب الماء دون تلامس. وقد استخدمته من قبل في رادار Arduino وفي سيارة روبوتية تتفادى العوائق.
وهنا نوصّله بـ ESP32 (لوحة تطوير بمتحكم دقيق مزوّدة بـ Wi-Fi مدمج)، فتعرض المسافة بالسنتيمتر مباشرةً على لوحة معلومات ويب تفتحها في متصفح هاتفك. ويبيّن المقال أيضًا أين تنحرف القراءات ومتى تكون خاطئة، بالاعتماد على أرقام من جلسة اختبار أجريتها بنفسي. وللاطلاع على نسخة Arduino Uno، راجع شرح مستشعر المسافة بالموجات فوق الصوتية مع Arduino. التوصيلات هناك أبسط، لأن اللوحة التي تعمل بجهد 5 V لا تحتاج إلى مقسم جهد على خط Echo.
2. كيف يعمل
2.1 مبدأ الاستشعار
تخيّل أنك تصرخ عبر وادٍ وتعدّ الثواني حتى يعود صوتك. ينتقل الصوت بسرعة ثابتة، لذا يخبرك زمن التأخير بمدى بُعد الجدار المقابل. يفعل HC-SR04 الشيء نفسه على مسافات من 2 cm إلى 4 m، وبتردد لا تستطيع سماعه. عندما يرسل إليه ESP32 نبضة تحفيز مدتها 10 µs، يُطلق من المُرسِل ثماني دورات من صوت بتردد 40 kHz. الميكروثانية (µs) جزء من مليون من الثانية، و40 kHz تعني 40,000 اهتزازة في الثانية. ينتقل الصوت عبر الهواء، ويصطدم بأي جسم أمام المستشعر، ثم يعود إلى المستقبِل. ويقيس المستشعر الزمن الذي استغرقته رحلة الذهاب والإياب.
الزمن المُقاس هو زمن رحلة ذهاب وإياب: ينتقل الصوت إلى الجسم ثم يعود، لذا يقابل هذا الزمن ضعف المسافة التي تريد معرفتها. ولهذا تقسم المعادلة على اثنين. لا يعرف المستشعر المسافة بالسنتيمتر؛ كل ما يعرفه هو مدة زمنية. فيسلّمك هذه المدة على شكل نبضة كهربائية ويترك الحساب لـ ESP32. وقد اختير التردد 40 kHz لأنه ينتقل جيدًا عبر الهواء في هذه المسافات القصيرة، ولأن محوّلات البيزو الرخيصة (عناصر خزفية تحوّل الجهد إلى اهتزاز وبالعكس) يقع ترددها الرنيني عنده.
2.2 المعادلة الأساسية
يعطيك المستشعر زمن الصدى بالميكروثانية. تبلغ سرعة الصوت في الهواء الجاف عند 20 °C نحو 343 m/s، ويسهل استخدامها بصيغة 0.0343 cm لكل ميكروثانية. وبالتالي:
distance (cm) = (echo_time_us * 0.0343) / 2
echo_time_us هو المدة التي بقي فيها الطرف Echo في الحالة العالية (أي عند جهد “التشغيل”)، بالميكروثانية، كما يقيسها ESP32. ويحوّل الرقم 0.0343 زمن انتقال الصوت بالميكروثانية إلى مسافة مقطوعة بالسنتيمتر. أما / 2 فيحذف مسافة العودة، لأن النبضة قاست زمن الذهاب والإياب معًا. وتجمع ورقة البيانات (Datasheet) كل ذلك في صيغة مختصرة، distance_cm = echo_time_us / 58، والصيغتان متفقتان: 1 مقسومًا على (0.0343 مقسومًا على 2) يساوي 58.3.
مثال محلول: يبقى الطرف Echo في الحالة العالية لمدة 1160 µs. اضرب في 0.0343 لتحصل على 39.79 cm هي المسافة الكلية التي قطعها الصوت، ثم اقسم على 2: النتيجة 19.9 cm. وتعطي الصيغة المختصرة 1160 / 58 = 20.0 cm، أي النتيجة نفسها مع فارق التقريب.
ترتفع سرعة الصوت بنحو 0.6 m/s لكل °C، أي قرابة 0.18 % لكل درجة، والمعادلة تفترض 20 °C. عند 30 °C يعود الصدى أسرع، وتكون المسافة الحقيقية أطول بنحو 1.7 % مما تعطيه المعادلة. وعند 89 cm يعني ذلك نحو 1.5 cm، وهو أكبر من التشتت البالغ 9 mm المُقاس في القسم 7. ولهذا السبب تضيف الأنظمة عالية الدقة مستشعرًا لدرجة الحرارة.
2.3 داخل الوحدة، خطوة بخطوة
السلسلة الكاملة من نبضة التحفيز حتى ظهور رقم على الشاشة:
- التحفيز: يُبقي ESP32 الطرف Trig (Trigger، أي التحفيز) في الحالة العالية لمدة 10 µs، وهذه هي إشارة “ابدأ القياس”.
- الإطلاق: تُطلق الوحدة من المُرسِل ثماني دورات من الموجات فوق الصوتية بتردد 40 kHz.
- الانتقال ذهابًا: يقطع الصوت المسافة حتى الجسم.
- الانعكاس: يعكس الجسم جزءًا من الصوت عائدًا نحو المستشعر.
- الاستقبال: يلتقط المستقبِل (الأسطوانة المعدنية الثانية) الصدى العائد.
- نبضة الصدى: ترفع الوحدة الطرف Echo إلى الحالة العالية وتُبقيه فيها طوال المدة التي استغرقتها رحلة الذهاب والإياب بالضبط.
- القياس: يقيس ESP32 مدة تلك النبضة العالية بالميكروثانية.
- الحساب: تطبّق المعادلة أعلاه فتحصل على المسافة بالسنتيمتر.
3. رسم متحرك تفاعلي
يُرسل الرسم المتحرك دفعة صوتية، ويُظهر انتقالها إلى الجسم وعودتها، ويحدّث المسافة وزمن الصدى والمعادلة مباشرةً أثناء سحبك لشريط التمرير. يُطلق زر Ping قياسًا واحدًا، بينما يحرّك Auto sweep الجسم تلقائيًا.
إذا لم يُحمَّل الرسم المتحرك، فافتحه في علامة تبويب جديدة.
4. التوصيلات، المكونات وOmBlock
4.1 قائمة المواد
تحتاج إلى ESP32، وHC-SR04، ومقاومتين لمقسم الجهد، ولوحة تجارب (Breadboard، وهي لوحة لا تحتاج إلى لحام وفيها صفوف متصلة من نقاط التوصيل)، وأسلاك توصيل (Jumper). تتراوح تكلفة المجموعة بين ستة وتسعة يورو تقريبًا، ويذهب معظمها إلى ESP32.
| القطع المستخدمة في هذا المشروع | إعلان · روابط تسويق بالعمولة | |
|---|---|---|
| لوحة ESP32 DevKit V1 | متحكم دقيق (MCU) ثنائي النواة مع Wi-Fi وبلوتوث (BT)، ومنطق 3.3 V، ومنفذ USB | Amazon* · AliExpress |
| مستشعر الموجات فوق الصوتية HC-SR04 | وحدة قياس مسافة بالموجات فوق الصوتية من 2 إلى 400 cm، بجهد 5 V وتردد 40 kHz | Amazon* · AliExpress |
| مجموعة مقاومات متنوعة | مقاومات غشاء كربوني بقدرة 1/4 W، تشمل 1 kΩ و2.2 kΩ لمقسم الجهد على خط Echo | Amazon* · AliExpress |
| لوحة تجارب (Breadboard) بـ 830 نقطة توصيل | لوحة تجارب بدون لحام (Breadboard) | Amazon* · AliExpress |
| مجموعة أسلاك توصيل (Jumper) | أسلاك Dupont بأطراف قابلة للتركيب: ذكر-ذكر (M-M)، ذكر-أنثى (M-F)، أنثى-أنثى (F-F) | Amazon* · AliExpress |
*روابط تسويق بالعمولة: أحصل على عمولة صغيرة إذا اشتريت من خلالها، دون أي تكلفة إضافية عليك.
بصفته شريكًا في برنامج Amazon Associates وشريكًا تسويقيًا لدى AliExpress، يربح موقع OmArTronics من عمليات الشراء المؤهلة. يبقى السعر كما هو بالنسبة إليك، وتساعد العمولة الصغيرة على إبقاء هذه الشروحات مجانية.
4.2 تنفيذ التوصيلات
يحتوي HC-SR04 على أربعة أطراف: VCC وTrig وEcho وGND. توصيل ثلاثة منها بسيط: يتصل VCC بالطرف 5 V في ESP32، ويتصل GND بالطرف GND في ESP32، ويتصل Trig بالطرف GPIO 5 (GPIO: طرف إدخال/إخراج للأغراض العامة). أما الطرف الرابع، Echo، فيحتاج إلى عناية إضافية.
يبقى Echo في وضع الخمول عند المستوى المنخفض، ثم ينبض حتى 5 V ليعبّر عن المسافة. مداخل ESP32 أطراف تعمل بجهد 3.3 V، لذا فإن إشارة 5 V مباشرة على GPIO 18 تُجهد الطرف وقد تتلفه مع مرور الوقت. والحل هو مقسم الجهد: مقاومتان على التوالي تخفّضان جهد 5 V إلى مستوى آمن. تقع R1 (1 kΩ) بين Echo ونقطة وسطى؛ وتقع R2 (2.2 kΩ) بين تلك النقطة والأرضي (GND). يقرأ ESP32 النقطة الوسطى، حيث يكون الجهد 5 × 2.2 / (1 + 2.2) = 3.44 V.
اخترت القيمة الثانية بالصدفة واحتفظت بها. تستخدم معظم الشروحات 1 kΩ و2 kΩ، ما يعطي 3.33 V. لكن مجموعة المقاومات لديّ لم تكن تحتوي على 2 kΩ، فهذه القيمة ليست ضمن سلسلة E12، وهي القيم القياسية الاثنتا عشرة للمقاومات لكل عقد عشري (1.0، 1.2، 1.5، 1.8، 2.2 وهكذا) التي تُبنى منها المجموعات الرخيصة. كانت المجموعة تحتوي على 2.2 kΩ، فاستخدمتها. تستقر النقطة الوسطى عند 3.44 V، أي أعلى قليلًا من جهد التغذية 3.3 V، لكنها دون الحد الأقصى المطلق لـ ESP32 البالغ 3.6 V (الجهد الذي قد تتلف الشريحة إذا تجاوزته). وقد عملت اللوحة لساعات على طاولة العمل لديّ دون مشكلات. إذا أردت البقاء دون 3.3 V، فاستخدم 2.2 kΩ للمقاومة R1 و3.3 kΩ للمقاومة R2 لتحصل على 3.0 V، وكلتا القيمتين موجودة في مجموعات E12 القياسية. أما Trig فلا يحتاج إلى مقسم جهد: إذ يقبل مدخل Trig جهد الخرج 3.3 V من ESP32 بوصفه مستوى منطقيًا عاليًا (الجهد الذي تقرؤه الوحدة على أنه “تشغيل”).
توزيع الأطراف كاملًا:
| طرف HC-SR04 | طرف ESP32 | لون السلك | ملاحظة |
|---|---|---|---|
| VCC | 5 V / VIN | أحمر | جهد 5 V كامل، وليس 3.3 V؛ بعض اللوحات تسمّي هذا الطرف VIN |
| Trig | GPIO 5 | أزرق | جهد القيادة 3.3 V كافٍ |
| Echo | النقطة الوسطى -> GPIO 18 | برتقالي | عبر مقسم الجهد R1/R2 |
| GND | GND | أسود | وصّله أولًا |

تقع المقاومتان في عمود واحد على لوحة التجارب الصغيرة. تمتد المقاومة 1 kΩ من Echo نزولًا إلى الصف الأوسط، والمقاومة 2.2 kΩ من ذلك الصف إلى الأرضي، ويأخذ السلك المتجه إلى GPIO 18 الإشارة من الصف الواقع بينهما.
نفّذ التوصيلات بهذا الترتيب: الأرضي أولًا، ثم VCC، ثم Trig، ثم ركّب مقسم الجهد وصِل النقطة الوسطى بـ GPIO 18. تجنّب الأطراف من GPIO 6 إلى 11، لأن ESP32 يستخدمها لشريحة ذاكرة Flash (الذاكرة التي تحفظ البرنامج). أما GPIO 5 و18 فآمنان.
4.3 حاوية OmBlock
يحصل كل مستشعر في هذه السلسلة على OmBlock، وهو حامل صغير مصنوع بالطباعة ثلاثية الأبعاد (3D) يُثبَّت بالضغط في OmGrid (قاعدة مطبوعة بتقنية 3D فيها نمط منتظم من ثقوب التثبيت). والنتيجة إعداد مرتب على طاولة العمل بدلًا من لوحة تجارب سائبة. وتحمل الشبكة نفسها الذراع الروبوتية OmArm Zero، حيث بدأ هذا النظام. يحتوي OmBlock-HC-SR04 على فتحتين بقطر 16 mm للمحوّلين، وقناة خلفية للأسلاك الأربعة، ويُركَّب بجوار OmBlock-ESP32 وOmBlock-Power. اطبعه بخيط PLA (بلاستيك الطباعة القياسي) بارتفاع طبقة 0.2 mm (سُمك كل طبقة مطبوعة) دون دعامات. وتجد رابط ملف STL (النموذج ثلاثي الأبعاد 3D الذي تحمّله في برنامج التقطيع، وهو البرنامج الذي يجهّز الطباعة) في القسم 9.

بعد طباعته بخيط PLA برتقالي، يثبّت المحوّلين بشكل عمودي على الهدف. قد يفقد المستشعر الصدى إذا كان مائلًا عن سطح أملس، لأن الصوت ينعكس جانبًا بدلًا من أن يعود إلى المستقبِل. قبل OmBlock كنت أُسند الوحدة إلى حزمة بطاريات وأتساءل لماذا تنقطع القراءات.
4.4 ملاحظات قبل التشغيل
تحقق من هذه النقاط قبل التشغيل:
- زوّد المستشعر بجهد 5 V حقيقي؛ فعند 3.3 V يتقلص المدى وتصبح القراءات مشوشة.
- لا توصل Echo مباشرة بأي طرف GPIO دون مقسم الجهد أبدًا.
- إذا كانت كل القراءات 0 أو قيمًا لا معنى لها، فتحقق من أن VCC وGND غير معكوسين، ومن أن النقطة الوسطى لمقسم الجهد متصلة بـ GPIO 18.
- وجّه المستشعر نحو سطح مستوٍ وصلب؛ فالأهداف اللينة أو المائلة تشتّت الصدى وتسبب انقطاعات (قراءات لا يعود فيها أي صدى، انظر القسم 7).
5. كود الاختبار
قبل لوحة معلومات الويب، احصل على قراءة بسيطة عبر الشاشة التسلسلية (Serial Monitor)، وهي نافذة في Arduino IDE تعرض النص الذي ترسله اللوحة عبر USB. فهذه القراءة تثبت صحة التوصيلات ومقسم الجهد. المنطق قصير: أرسل نبضة تحفيز، وقِس مدة نبضة Echo، وحوّلها إلى سنتيمترات، وارفض أي قيمة خارج النطاق من 2 إلى 400 cm.
برنامج الاختبار (Sketch، أي برنامج Arduino) ملف واحد دون علامات تبويب إضافية (ملفات مصدرية إضافية يعرضها IDE جنبًا إلى جنب). افتحه في Arduino IDE (المحرر الذي يترجم الكود ويرفعه إلى اللوحة) وارفعه إلى اللوحة. جميع الإعدادات التي قد تغيّرها (الأطراف، وفترة القراءة، وإزاحة المعايرة) موجودة في كتلة معنونة في أعلى الملف:
/*
* HC-SR04 Ultrasonic Distance — Test Sketch
* OmArTronics · Project 01 · 2026-07-25 · MIT License
*
* Prints distance in cm over Serial at 115200 baud.
* Wiring: Trig -> GPIO 5, Echo -> GPIO 18 through a 1k/2.2k divider (5V -> 3.44V),
* VCC -> 5V, GND -> GND. See wiring.md.
*
* Single file — just open in the Arduino IDE and upload. Change pins below.
*/
// ===== Settings (edit these) =====
#define TRIG_PIN 5 // ESP32 GPIO 5 -> HC-SR04 Trig
#define ECHO_PIN 18 // ESP32 GPIO 18 <- Echo via 1k/2.2k divider
#define READING_INTERVAL_MS 60 // datasheet suggests >= 60 ms between pings
#define ECHO_TIMEOUT_US 25000 // ~430 cm round trip; pulseIn gives up after this
#define SERIAL_BAUD 115200
#define SOUND_CM_PER_US 0.0343f // speed of sound at 20 C, cm per microsecond
#define DIST_MIN_CM 2.0f // datasheet minimum
#define DIST_MAX_CM 400.0f // datasheet maximum
#define CALIBRATION_OFFSET 0.0f // cm added after conversion (tune vs a ruler)
#define CALIBRATION_SCALE 1.0f // multiply reading (1.0 = no change)
// =================================
// One measurement. Returns distance in cm, or NAN if the reading is invalid.
float readDistanceCm() {
// 10 us trigger pulse tells the module to fire an 8-cycle 40 kHz burst.
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
// pulseIn measures how long Echo stays HIGH, in microseconds.
// It returns 0 if no echo arrives before the timeout.
unsigned long echo_us = pulseIn(ECHO_PIN, HIGH, ECHO_TIMEOUT_US);
if (echo_us == 0) return NAN; // timeout: nothing in range
// Convert time of flight to distance (divide by 2 for the round trip).
float cm = (echo_us * SOUND_CM_PER_US) / 2.0f;
cm = cm * CALIBRATION_SCALE + CALIBRATION_OFFSET;
// Reject readings outside the datasheet range.
if (cm < DIST_MIN_CM || cm > DIST_MAX_CM) return NAN;
return cm;
}
void setup() {
Serial.begin(SERIAL_BAUD);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
digitalWrite(TRIG_PIN, LOW);
delay(50);
Serial.println(F("HC-SR04 test ready. Distance in cm:"));
}
void loop() {
float cm = readDistanceCm();
if (isnan(cm)) {
Serial.println(F("-- out of range / no echo --"));
} else {
Serial.print(cm, 1);
Serial.println(F(" cm"));
}
delay(READING_INTERVAL_MS);
}
توقف الدالة pulseIn(ECHO_PIN, HIGH, ECHO_TIMEOUT_US) تنفيذ البرنامج إلى أن يرتفع Echo إلى الحالة العالية ثم ينخفض. وتُعيد مدة الحالة العالية بالميكروثانية، أو 0 إذا لم يعد أي صدى قبل انتهاء المهلة. أما / 2.0f فهو تصحيح رحلة الذهاب والإياب من القسم 2. وتلتقط isnan(cm) حالتي انتهاء المهلة والخروج عن النطاق بفحص واحد، فتطبع الحلقة سطرًا واضحًا “out of range” (خارج النطاق) بدلًا من رقم لا معنى له. عند غياب الصدى، تُبقي معظم الوحدات Echo في الحالة العالية لنحو 38 ms. والمهلة البالغة 25 ms (ECHO_TIMEOUT_US) تُنهي الانتظار قبل ذلك، لذا يطبع هذا البرنامج سطر الخروج عن النطاق. أما برنامج pulseIn في شرح Arduino Uno الخاص بي فيُبقي المهلة الافتراضية البالغة ثانية واحدة، ويطبع نحو 650 cm في الحالة نفسها.
المخرجات المتوقعة في الشاشة التسلسلية (Serial Monitor) عند 115200 baud (سرعة الاتصال التسلسلي عبر USB بالبت في الثانية)، أثناء تقريب يدك من المستشعر:
HC-SR04 test ready. Distance in cm: 34.2 cm 28.9 cm 19.9 cm 11.3 cm -- out of range / no echo --
إذا كانت مخرجات Serial خاطئة، فالتوصيلات خاطئة. أصلحها هنا قبل أن تنتقل إلى لوحة المعلومات.

في لقطة الشاشة لا يتحرك الجدار، ومع ذلك يرسم Serial Plotter (عرض الرسم البياني في Arduino IDE لبيانات Serial نفسها) خطًا متعرجًا. التفاوت بين 88.7 و89.6 cm هو ضوضاء المستشعر، ويحدّده القسم 7 بالأرقام.
6. الراسم البياني الحي على الويب
6.1 كيف يعمل
يحوّل البرنامج hc-sr04_plotter.ino لوحة ESP32 إلى نقطة وصول Wi-Fi وخادم ويب صغير. تفتح نقطة الوصول شبكة Wi-Fi خاصة بها تنضم إليها الأجهزة الأخرى، ويجيب خادم الويب على طلبات المتصفح. ينضم هاتفك إلى الشبكة “OmArTronics-HC-SR04”، ويفتح http://10.10.10.1، ثم تطلب لوحة المعلومات بيانات نقطة نهاية JSON بشكل متكرر (Polling) خمس مرات في الثانية. ونقطة نهاية JSON (Endpoint) هي عنوان URL يُعيد القيم الحالية بصيغة JSON، وهي صيغة بيانات نصية بسيطة. لا يحتاج ذلك إلى اتصال بالإنترنت ولا إلى أي تطبيق: فلوحة المعلومات مخزنة في البرنامج الثابت (Firmware)، أي البرنامج الذي ترفعه إلى ESP32، ويقدّمها ESP32 بنفسه.

6.2 ميزات لوحة المعلومات
تعرض لوحة المعلومات رقمًا حيًا كبيرًا ورسمًا بيانيًا متحركًا لآخر 100 قراءة يضبط مقياس محوره تلقائيًا. وتعرض البطاقات الحد الأدنى والحد الأقصى والمتوسط والعدد الكلي أولًا بأول. يحفظ زر اللقطة الرسم البياني كصورة PNG. ويُنزّل زر التصدير القراءات المخزنة مؤقتًا كملف CSV (قيم مفصولة بفواصل، وهي صيغة نصية تُفتح في Excel أو LibreOffice). ويمسح زر إعادة الضبط الإحصاءات على ESP32. وإذا فشلت ثلاث عمليات استطلاع متتالية، تتحول نقطة في الزاوية إلى اللون الأحمر وتختفي القيمة، ثم تعيد لوحة المعلومات الاتصال تلقائيًا.

زرّا اللقطة وCSV هما السبب الرئيسي لوجود لوحة المعلومات. فعدة قياسات في القسم 7 مأخوذة من ملف CSV صُدِّر بزر CSV. أما زر اللقطة فينتج الصورة أدناه مباشرة من لوحة الرسم في المتصفح (Canvas، أي منطقة رسم المخطط).

6.3 البرنامج الثابت (Firmware)
يعيد برنامج لوحة المعلومات استخدام كود القياس من برنامج الاختبار، ويضيف إليه حساب إحصاءات مستمرة وثلاث نقاط نهاية JSON. لا تستدعي الحلقة delay() أبدًا؛ فهي تجيب على طلبات المتصفح في كل دورة، ولا تأخذ قراءة إلا بعد انقضاء READING_INTERVAL_MS.
لوحة المعلومات كاملةً (HTML وCSS وJavaScript، وهي الأجزاء الثلاثة لصفحة الويب) موجودة داخل البرنامج على شكل ثوابت نصية (String Literals)، وتُرسَل بواسطة server.send_P(). كل ثابت معلَّم بـ PROGMEM، وهي كلمة مفتاحية تُبقي البيانات في ذاكرة Flash على اللوحات التي كانت ستنسخها لولا ذلك إلى RAM (ذاكرة العمل الخاصة بالمتغيرات). أما على ESP32 فالبيانات المعرَّفة بـ const تبقى في Flash على أي حال، لذا فإن PROGMEM يحافظ فقط على توافق الكود مع لوحات Arduino الأخرى. تقرأ send_P() لوحة المعلومات من Flash على دفعات صغيرة وترسلها عبر اتصال الشبكة، فلا تُنسخ أبدًا إلى RAM دفعة واحدة. وهكذا تنتقل لوحة المعلومات مع البرنامج الثابت في عملية رفع واحدة.
/*
* HC-SR04 Live Web-Plotter — ESP32 Access Point, single-sketch build
* OmArTronics · Project 01 · MIT License
*
* ONE upload. That is the whole point of this version.
*
* The dashboard (HTML, CSS, JavaScript) is embedded in the sketch as PROGMEM
* strings, so it travels with the firmware into flash. No LittleFS, no data/
* folder, no IDE plugin, no second upload step. The earlier build kept the
* dashboard on a LittleFS partition and every single failure I hit was in that
* handover: wrong partition scheme, Serial Monitor holding the port, the IDE 2.x
* uploader plugin refusing to appear. The dashboard is 6.9 KB. Flash is 4 MB.
* Carrying it in the sketch costs nothing and removes an entire class of bugs.
*
* Steps:
* 1) Upload this sketch
* 2) Join WiFi "OmArTronics-HC-SR04", password "ultrasonic"
* 3) Open http://10.10.10.1
*
* Wiring: TRIG -> GPIO 5, ECHO -> GPIO 18 through a 1k / 2.2k divider (3.44 V
* at the node), VCC -> 5V, GND -> GND. See the blog post for the schematic.
*
* Boot log runs at 115200 baud.
*/
#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
// ===== Settings (edit these) =====
#define TRIG_PIN 5
#define ECHO_PIN 18
#define READING_INTERVAL_MS 60
#define ECHO_TIMEOUT_US 25000
#define SERIAL_BAUD 115200
#define SOUND_CM_PER_US 0.0343f
#define DIST_MIN_CM 2.0f
#define DIST_MAX_CM 400.0f
#define CALIBRATION_OFFSET 0.0f
#define CALIBRATION_SCALE 1.0f
#define WIFI_SSID "OmArTronics-HC-SR04"
#define WIFI_PASS "ultrasonic"
// =================================
WebServer server(80);
// --- Running statistics ---
float g_last = NAN, g_min = NAN, g_max = NAN;
double g_sum = 0.0;
unsigned long g_count = 0;
unsigned long g_lastReadMs = 0;
// ============================================================================
// The dashboard. Everything between R"rawname( and )rawname" is copied verbatim
// into flash, so you can edit it like a normal web file. The only rule: the
// content must never contain the closing sequence )rawname".
// ============================================================================
static const char INDEX_HTML[] PROGMEM = R"HTMLPAGE(<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0f1115">
<link rel="manifest" href="manifest.json">
<title>HC-SR04 Live Plotter</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>HC-SR04 Live Distance</h1>
<span id="dot" class="dot" title="connection"></span>
</header>
<main>
<section class="live">
<div id="value">--</div><div class="unit">cm</div>
</section>
<canvas id="chart" width="720" height="240" aria-label="Rolling distance chart"></canvas>
<section class="cards">
<div class="card"><div class="lbl">MIN</div><div id="min" class="num">--</div></div>
<div class="card"><div class="lbl">MAX</div><div id="max" class="num">--</div></div>
<div class="card"><div class="lbl">AVG</div><div id="avg" class="num">--</div></div>
<div class="card"><div class="lbl">READINGS</div><div id="count" class="num">0</div></div>
</section>
<section class="btns">
<button id="snap">Snapshot PNG</button>
<button id="csv">Export CSV</button>
<button id="reset" class="warn">Reset stats</button>
</section>
<p class="hint">Access point <b>OmArTronics-HC-SR04</b> · open <b>http://10.10.10.1</b></p>
</main>
<script src="script.js"></script>
</body>
</html>
)HTMLPAGE";
static const char STYLE_CSS[] PROGMEM = R"CSSPAGE(
*{box-sizing:border-box;margin:0;padding:0}
body{background:#0f1115;color:#e8ecf2;font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;max-width:820px;margin:0 auto;padding:14px}
header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
h1{font-size:clamp(16px,4vw,22px);color:#E85D1A;font-weight:800}
.dot{width:14px;height:14px;border-radius:50%;background:#c23b22;box-shadow:0 0 8px #c23b22;transition:.3s}
.dot.ok{background:#39c07a;box-shadow:0 0 8px #39c07a}
.live{display:flex;align-items:baseline;gap:8px;justify-content:center;margin:8px 0}
#value{font-size:clamp(44px,16vw,84px);font-weight:800;color:#1a73e8;transition:color .2s}
.unit{font-size:clamp(16px,5vw,26px);color:#8b95a6}
canvas{width:100%;height:auto;background:#161922;border:1px solid #232838;border-radius:12px;display:block;margin:8px 0}
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:10px 0}
.card{background:#1a1d23;border:1px solid #232838;border-radius:10px;padding:10px 8px;text-align:center}
.lbl{font-size:10.5px;color:#8b95a6;letter-spacing:.5px}
.num{font-size:clamp(15px,4.5vw,20px);font-weight:700;margin-top:3px}
.btns{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0}
button{flex:1 1 130px;min-height:46px;border:none;border-radius:10px;background:#1a73e8;color:#fff;font-size:14px;font-weight:600;cursor:pointer}
button.warn{background:#E85D1A}
button:active{transform:translateY(1px)}
.hint{font-size:12px;color:#8b95a6;text-align:center;margin-top:8px}
@media(max-width:460px){.cards{grid-template-columns:repeat(2,1fr)}}
)CSSPAGE";
static const char SCRIPT_JS[] PROGMEM = R"JSPAGE(
// HC-SR04 live plotter — polls the ESP32 JSON API, draws a rolling chart.
var MAX_POINTS = 100;
var series = []; // {t, v} rolling buffer for chart + CSV
var fails = 0;
var dot = document.getElementById('dot');
var elV = document.getElementById('value');
var elMin = document.getElementById('min'), elMax = document.getElementById('max');
var elAvg = document.getElementById('avg'), elCount = document.getElementById('count');
var cv = document.getElementById('chart'), cx = cv.getContext('2d');
// The sensor resolves to about 3 mm, so printing more than one decimal would be
// inventing precision the hardware does not have. The CSV export rounds the same way.
function fmt(x){ return (x===null||x===undefined||isNaN(x))?'--':(+x).toFixed(1); }
function setConnected(ok){
dot.classList.toggle('ok', ok);
if(!ok){ elV.textContent='--'; elV.style.color='#c23b22'; }
else { elV.style.color='#1a73e8'; }
}
function poll(){
fetch('/api/data',{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
fails = 0; setConnected(true);
elV.textContent = fmt(d.value);
elMin.textContent = fmt(d.min); elMax.textContent = fmt(d.max);
elAvg.textContent = fmt(d.avg); elCount.textContent = d.count||0;
if(d.value!==null && d.value!==undefined && !isNaN(d.value)){
series.push({t:d.timestamp_ms, v:+d.value});
if(series.length>MAX_POINTS) series.shift();
draw();
}
}).catch(function(){
fails++; if(fails>=3) setConnected(false);
});
}
function draw(){
var W=cv.width, H=cv.height, pad=28;
cx.clearRect(0,0,W,H);
cx.strokeStyle='#232838'; cx.lineWidth=1;
for(var i=0;i<=4;i++){ var y=pad+(H-2*pad)*i/4; cx.beginPath(); cx.moveTo(pad,y); cx.lineTo(W-6,y); cx.stroke(); }
if(series.length<2) return;
var vals=series.map(function(p){return p.v;});
var lo=Math.min.apply(null,vals), hi=Math.max.apply(null,vals);
if(hi-lo<1){ hi+=1; lo-=1; } // avoid flat-line divide by zero
cx.fillStyle='#8b95a6'; cx.font='11px sans-serif'; cx.textAlign='right';
for(var k=0;k<=4;k++){ var val=hi-(hi-lo)*k/4; var yy=pad+(H-2*pad)*k/4; cx.fillText(val.toFixed(0),pad-4,yy+3); }
cx.strokeStyle='#1a73e8'; cx.lineWidth=2; cx.beginPath();
series.forEach(function(p,idx){
var x=pad+(W-pad-6)*idx/(MAX_POINTS-1);
var y=pad+(H-2*pad)*(1-(p.v-lo)/(hi-lo));
idx?cx.lineTo(x,y):cx.moveTo(x,y);
});
cx.stroke();
var last=series[series.length-1];
var lx=pad+(W-pad-6)*(series.length-1)/(MAX_POINTS-1);
var ly=pad+(H-2*pad)*(1-(last.v-lo)/(hi-lo));
cx.fillStyle='#E85D1A'; cx.beginPath(); cx.arc(lx,ly,4,0,7); cx.fill();
}
function download(name, url){
var a=document.createElement('a'); a.href=url; a.download=name; a.click();
}
document.getElementById('snap').onclick=function(){
download('hc-sr04_'+Date.now()+'.png', cv.toDataURL('image/png'));
};
document.getElementById('csv').onclick=function(){
var rows=['timestamp_ms,distance_cm'];
series.forEach(function(p){ rows.push(p.t+','+p.v.toFixed(1)); });
download('hc-sr04_'+Date.now()+'.csv', 'data:text/csv,'+encodeURIComponent(rows.join('\n')));
};
document.getElementById('reset').onclick=function(){
fetch('/api/reset',{method:'POST'}).then(function(){ series=[]; draw(); });
};
if('serviceWorker' in navigator){ navigator.serviceWorker.register('sw.js').catch(function(){}); }
setConnected(false);
setInterval(poll, 200);
poll();
)JSPAGE";
// Lets the phone install the page as an app icon. Optional, costs 200 bytes.
static const char MANIFEST_JSON[] PROGMEM = R"MANPAGE({
"name":"HC-SR04 Live Plotter","short_name":"HC-SR04","start_url":"/",
"display":"standalone","background_color":"#0f1115","theme_color":"#0f1115"}
)MANPAGE";
// The service worker only registers itself. Caching the shell would fight the
// live data, and the pages already ship inside the firmware.
static const char SW_JS[] PROGMEM = R"SWPAGE(
self.addEventListener('install', function(){ self.skipWaiting(); });
self.addEventListener('activate', function(e){ e.waitUntil(self.clients.claim()); });
)SWPAGE";
// ============================================================================
// Sensor
// ============================================================================
float readSensor() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long echo_us = pulseIn(ECHO_PIN, HIGH, ECHO_TIMEOUT_US);
if (echo_us == 0) return NAN;
float cm = (echo_us * SOUND_CM_PER_US) / 2.0f;
cm = cm * CALIBRATION_SCALE + CALIBRATION_OFFSET;
if (cm < DIST_MIN_CM || cm > DIST_MAX_CM) return NAN;
return cm;
}
void updateStats(float cm) {
g_last = cm;
if (isnan(g_min) || cm < g_min) g_min = cm;
if (isnan(g_max) || cm > g_max) g_max = cm;
g_sum += cm;
g_count++;
}
// ============================================================================
// HTTP handlers
// ============================================================================
void handleData() {
StaticJsonDocument<256> doc;
doc["sensor"] = "HC-SR04";
doc["unit"] = "cm";
// Raw values, not rounded. The dashboard and its CSV export round to one
// decimal; anyone reading the API directly gets what the sensor actually produced.
if (isnan(g_last)) doc["value"] = nullptr; else doc["value"] = g_last;
if (isnan(g_min)) doc["min"] = nullptr; else doc["min"] = g_min;
if (isnan(g_max)) doc["max"] = nullptr; else doc["max"] = g_max;
doc["avg"] = g_count ? (g_sum / g_count) : 0.0;
doc["count"] = g_count;
doc["timestamp_ms"] = millis();
String out; serializeJson(doc, out);
server.send(200, "application/json", out);
}
void handleStatus() {
StaticJsonDocument<256> doc;
doc["uptime_s"] = millis() / 1000;
doc["clients"] = WiFi.softAPgetStationNum();
doc["readings"] = g_count;
doc["free_heap"] = ESP.getFreeHeap();
String out; serializeJson(doc, out);
server.send(200, "application/json", out);
}
void handleReset() {
g_min = g_max = g_last = NAN;
g_sum = 0.0; g_count = 0;
server.send(200, "application/json", "{\"ok\":true}");
}
// send_P streams straight out of flash, so the page never occupies RAM.
void handleIndex() { server.send_P(200, "text/html", INDEX_HTML); }
void handleCss() { server.send_P(200, "text/css", STYLE_CSS); }
void handleJs() { server.send_P(200, "application/javascript", SCRIPT_JS); }
void handleManifest() { server.send_P(200, "application/json", MANIFEST_JSON); }
void handleSw() { server.send_P(200, "application/javascript", SW_JS); }
// Phones and laptops probe a handful of URLs to decide whether a network has
// internet. Answering them with the dashboard makes the captive-portal popup
// open it directly, which saves typing the IP on a phone.
void handleNotFound() { handleIndex(); }
// ============================================================================
void setup() {
Serial.begin(SERIAL_BAUD);
delay(300);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
digitalWrite(TRIG_PIN, LOW);
Serial.println(F("\n--- OmArTronics HC-SR04 Live Plotter ---"));
Serial.printf("Dashboard in flash: %u bytes (html+css+js)\n",
(unsigned)(sizeof(INDEX_HTML) + sizeof(STYLE_CSS) + sizeof(SCRIPT_JS)));
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(IPAddress(10,10,10,1), IPAddress(10,10,10,1), IPAddress(255,255,255,0));
WiFi.softAP(WIFI_SSID, WIFI_PASS);
Serial.print(F("AP up: ")); Serial.println(WiFi.softAPIP());
Serial.print(F("SSID: ")); Serial.println(F(WIFI_SSID));
server.on("/", HTTP_GET, handleIndex);
server.on("/index.html", HTTP_GET, handleIndex);
server.on("/style.css", HTTP_GET, handleCss);
server.on("/script.js", HTTP_GET, handleJs);
server.on("/manifest.json", HTTP_GET, handleManifest);
server.on("/sw.js", HTTP_GET, handleSw);
server.on("/api/data", HTTP_GET, handleData);
server.on("/api/status", HTTP_GET, handleStatus);
server.on("/api/reset", HTTP_POST, handleReset);
server.onNotFound(handleNotFound);
server.begin();
Serial.println(F("Web server started. Open http://10.10.10.1"));
}
void loop() {
server.handleClient();
unsigned long now = millis();
if (now - g_lastReadMs >= READING_INTERVAL_MS) {
g_lastReadMs = now;
float cm = readSensor();
if (!isnan(cm)) updateStats(cm);
}
}
6.4 الكود المصدري للوحة المعلومات
تعديل HTML داخل سلسلة نصية في C أمر مزعج، لذا توجد الملفات الثلاثة أيضًا كملفات ويب عادية في web_src/ بجوار البرنامج. وهي نسخ مطابقة لكتل PROGMEM: انسخ التغييرات من أحدهما إلى الآخر. تقرّب الدالة fmt() في JavaScript كل قيمة معروضة إلى منزلة عشرية واحدة. أما الواجهة البرمجية API (نقاط نهاية JSON) فتُعيد القيمة الخام، التي تبدو مثل 21.35175 لأنها ناتجة عن قسمة بالفاصلة العائمة (حساب بالأعداد العشرية). خمس منازل عشرية من مستشعر دقة تمييزه المقدَّرة 3 mm هي دقة مختلَقة، لذا تعرض لوحة المعلومات 21.4. ويقرّب تصدير CSV أيضًا إلى منزلة عشرية واحدة (p.v.toFixed(1) في الكود)؛ والـ API وحدها تُعيد الرقم الخام.
6.5 عملية رفع واحدة، واليومان اللذان كلّفاني الوصول إليها
الطريقة البديهية لوضع لوحة معلومات ويب (Dashboard) على ESP32 هي LittleFS، وهو نظام ملفات صغير لذاكرة الفلاش. يذهب برنامج Arduino (Sketch) إلى ذاكرة البرنامج في الفلاش، وتذهب ملفات الويب إلى قسم (Partition) منفصل لنظام الملفات، وتقدّمها اللوحة من هناك. ويحدد مخطط التقسيم (Partition Scheme) كيفية توزيع ذاكرة الفلاش بين البرنامج وتخزين الملفات. هكذا يعمل البرنامج الثابت لذراعي الروبوتية ، وهذا ما يعرضه كل شرح تقريبًا. لذلك بنيت هذا المشروع بالطريقة نفسها.
بعد ذلك حمّلت البرنامج على اللوحة، واتصلت بنقطة الوصول، وفتحت 10.10.10.1، فظهرت لي صفحة تقول إن نظام الملفات مُركَّب (Mounted) ولا يحتوي على أي ملف. كانت قراءات المستشعر صحيحة، وكان العنوان 10.10.10.1/api/data يعيد المسافات مباشرةً. لم يكن ينقص سوى لوحة المعلومات، لأن الرفع الثاني لم يحدث قط.
وذهب معظم الوقت في جعل هذا الرفع الثاني يعمل. فعنصر القائمة في IDE 1.8 ، وهو Tools > ESP32 Sketch Data Upload، غير موجود في IDE 2.x. أما بديله arduino-littlefs-upload، فهو ملف إضافة (Plugin) بصيغة .vsix تنسخه يدويًا إلى المجلد .arduinoIDE/plugins/. وفي عدة إصدارات 2.3.x لا تظهر الإضافة بعد ذلك في لوحة الأوامر (Command Palette، وهي مربع البحث عن الأوامر في الـIDE). استخدمت في هذا المشروع IDE 2.3.7 ثم 2.3.10 لاحقًا. وحتى عندما تعمل، فإنها تفشل دون أي رسالة إذا كانت الشاشة التسلسلية (Serial Monitor) تحجز منفذ COM (اتصال USB التسلسلي الذي يستخدمه الـIDE)، كما أن تغيير مخطط التقسيم بعد ذلك يمحو كل ما حمّلته على اللوحة. وكحلٍّ بديل بنيت في برنامج Arduino نموذج رفع يعمل من المتصفح. نجح ذلك، لكن الخطوة الثانية في الشرح ظلت “والآن ارفع خمسة ملفات أخرى”.
ثم سألت نفسي: لماذا توجد لوحة المعلومات في نظام ملفات أصلًا؟ حجمها 6.9 KB، وذاكرة الفلاش 4 MB. ضعها إذن في برنامج Arduino نفسه.
لذلك أصبحت لوحة المعلومات الآن مخزّنة في PROGMEM، وصار الإجراء كله:
- ارفع البرنامج (Sketch) إلى اللوحة.
- اتصل بشبكة واي فاي (Wi-Fi) “OmArTronics-HC-SR04”، وكلمة المرور ultrasonic.
- افتح http://10.10.10.1.
عملت اللوحة من المحاولة الأولى.


يبقى نظام الملفات الخيار الأفضل للوحة معلومات يتجاوز حجمها بضع عشرات من الكيلوبايت، أو لتحديث لوحة المعلومات دون إعادة ترجمة البرنامج (Compile)، أو للسجلات المخزّنة على الجهاز. أما للوحة معلومات من صفحة واحدة تأتي مع البرنامج الثابت، فإن نظام الملفات يضيف تعقيدًا لا تحتاج إليه. وستستخدم كل المستشعرات في هذه السلسلة نسخة PROGMEM.
7. القيود والحالات الحدّية
هذا هو الجزء الذي يهمني أكثر من غيره في هذه السلسلة: ما تدّعيه ورقة البيانات (Datasheet)، وما قاسه المستشعر الموجود على مكتبي. الأرقام التالية مأخوذة من الراسم التسلسلي (Serial Plotter) وملف CSV المُصدَّر وإحصاءات لوحة المعلومات في جلسة قياس حقيقية.
حتى الهدف الثابت ينتج خطًا متعرجًا. تذكر ورقة البيانات دقة تمييز (Resolution) قدرها 0.3 cm ودقة قياس (Accuracy) تصل في أفضل الأحوال إلى ±3 mm. وجّهت المستشعر نحو جدار يبعد نحو 89 cm، وتركت كل شيء دون أن ألمسه، وراقبت الراسم التسلسلي. على مدى 24 قراءة متتالية خلال 1.55 ثانية تراوحت القيم بين 88.7 و89.6 cm. كان الوسيط (Median، أي القيمة الوسطى للقراءات بعد ترتيبها) 89.1 cm. وكان الانحراف المعياري (Standard Deviation، أي مقدار ابتعاد القراءة عادةً عن المتوسط، وهو معدل جميع القراءات) 1.8 mm، وبلغ التشتت من القمة إلى القمة (Peak-to-Peak، أي من أدنى قراءة إلى أعلاها) 9 mm. هذا التشتت يساوي ثلاثة أضعاف دقة التمييز التي تعلنها ورقة البيانات، مع أن الهدف لم يتحرك.
ويظهر الأمر نفسه على مسافة أقرب. فقد أعطت نافذة زمنية مدتها 2.4 ثانية عند نحو 8 cm متوسطًا قدره 7.67 cm بتشتت قدره 6 mm. لا يوجد أي عطل؛ هذا هو الضجيج (Noise) الطبيعي لنبضة صوتية بتردد 40 kHz يُقاس زمنها بالميكروثانية.
تظهر قيم شاذة منفردة، وهي كبيرة. القيمة الشاذة (Outlier) هي قراءة منفردة بعيدة كثيرًا عن القراءات المجاورة لها. في ملف مُصدَّر من 100 عينة قفزت القراءة من 22.3 cm إلى 32.7 cm ثم عادت إلى 23.5 cm خلال عينتين، مع أن الهدف لم يتحرك بهذا القدر. وفي لقطة شاشة الهاتف يظهر الشيء نفسه على شكل قفزة منفردة إلى 45 cm في منحنى سلس فيما عدا ذلك. إذا كان مشروعك يتخذ قرارات بناءً على المسافة، فخذ الوسيط لثلاث قراءات أو خمس بدلًا من الاعتماد على قراءة واحدة.

الحد الأدنى البالغ 2 cm مفروض مرتين. كانت أدنى قيمة في الجلسة 2.1 cm، لكن برنامج Arduino نفسه يستبعد أي قيمة أقل من DIST_MIN_CM، المضبوطة على 2.0. لذا فإن الحد الأدنى الذي قسته ناتج جزئيًا عن المرشّح (Filter) الذي كتبته أنا وجزئيًا عن الوحدة نفسها. والسبب الفيزيائي: يكون المحوِّل (Transducer) ما زال يهتز من نبضته الخاصة عندما يصل الصدى من جسم قريب جدًا، فلا تستطيع الوحدة التمييز بينهما.
المدى البعيد لم يُختبر. كانت أكبر قيمة في الجلسة 93.4 cm، وبلغت القيمة القصوى في تجربة سابقة 101.4 cm. تذكر ورقة البيانات 400 cm؛ ولم أختبر أهدافًا على هذا البعد.
سجل Serial وملف CSV يستخدمان معدلين مختلفين لأخذ العينات. استغرقت قراءات Serial البالغ عددها 24 في اختبار 89 cm مدة 1.55 s، أي بمتوسط 67.5 ms لكل قراءة مقابل فاصل زمني مضبوط على 60 ms. ومعظم الوقت الإضافي هو زمن انتقال الصدى، نحو 5 ms عند 89 cm. وتتراوح الفجوات المنفردة بين الطوابع الزمنية (Timestamps) من 32 إلى 94 ms (الوسيط 78 ms)، لأن الـIDE يختم كل سطر بوقت وصوله عبر USB، وUSB يسلّم الأسطر على دفعات صغيرة. أما ملف CSV المُصدَّر من لوحة المعلومات فتفصل بين عيناته مدة وسيطها 205 ms، لأن هذا الملف يحتوي على ما استعلم عنه المتصفح، لا على كل قراءة أخذها ESP32. تحقّق من أيّ الاثنين لديك قبل أن تستخدم أيًّا منهما مرجعًا للتوقيت.
لم يُقس بعد (المتوقع وفق ورقة البيانات والفيزياء):
- الأهداف الليّنة أو المائلة: الإسفنج والستائر وأي سطح تصيبه الموجة بزاوية تشتّت الصدى. توقّع انقطاعًا في القراءات بدلًا من أرقام خاطئة لكنها تبدو معقولة.
- عرض الحزمة نحو 15 درجة: يُرجع المستشعر مسافة أقرب جسم داخل مخروط، لا على امتداد خط رفيع. وقد يُرجع مسافة إطار باب عند حافة المخروط بدلًا من الهدف.
- الانحراف بسبب درجة الحرارة: تفترض القيمة الثابتة 0.0343 درجة حرارة 20 °C. وفوق 20 °C يقرأ المستشعر مسافة أقصر من الحقيقية بنحو 0.18 % لكل درجة (انظر القسم 2.2).
- التداخل (Cross-talk): إذا أطلقت وحدتا HC-SR04 نبضاتهما في اللحظة نفسها، فقد تلتقط كلٌّ منهما نبضات الأخرى. إذا استخدمت أكثر من وحدة، فباعِد بين أوقات إطلاقها (Trigger).

عندما يُمسك لوح كهذا عموديًا على المستشعر، تكون الحالة سهلة. أما إذا أُميل كما في الصورة، فالمتوقع أن ينعكس معظم النبضة الصوتية بعيدًا عن المستشعر، وأن تنقطع القراءة.
8. الخلاصة والمستشعر التالي
خمس نقاط لمشروعك القادم:
- اقسم على اثنين دائمًا. تقيس نبضة Echo زمن الذهاب والإياب معًا. cm = echo_us * 0.0343 / 2، أو الاختصار الوارد في ورقة البيانات echo_us / 58.
- مقسم الجهد ليس اختياريًا. يرتفع جهد Echo إلى 5 V، بينما يعمل ESP32 على 3.3 V. تعطي المقاومتان 1 kΩ / 2.2 kΩ جهدًا قدره 3.44 V عند نقطة الالتقاء (Node)، وهو أقل من الحد الأقصى المطلق البالغ 3.6 V، والقيمتان كلتاهما موجودتان في أطقم المقاومات القياسية من سلسلة E12.
- القراءة الواحدة لا يُعتمد عليها. أعطى جدار ثابت على بعد 89 cm تشتتًا قدره 9 mm، وقفزت عينة واحدة 10 cm ثم عادت. طبّق مرشّح الوسيط (Median Filter) على ثلاث إلى خمس قراءات قبل أن تتخذ قرارًا بناءً على الرقم.
- احفظ لوحة المعلومات الصغيرة في PROGMEM. لوحة معلومات حجمها 6.9 KB لا تحتاج إلى قسم LittleFS وإضافة (Plugin) ورفع ثانٍ. يصبح نظام الملفات مجديًا للوحات المعلومات التي يبلغ حجمها بضع عشرات من الكيلوبايت أو للسجلات المخزّنة على الجهاز (القسم 6.5).
- ارسم قراءات المستشعر بيانيًا بدل طباعتها. القفزة الناتجة عن الصدى الكاذب في القسم 7 لا تُرى في الشاشة التسلسلية والأسطر تتدفق فيها باستمرار، لكنها واضحة على الرسم البياني.
بطاقة مرجعية سريعة
| المواصفات | HC-SR04 |
|---|---|
| نوع المستشعر | قياس المسافة بالموجات فوق الصوتية، زمن الطيران (Time-of-Flight) بتردد 40 kHz (حساب المسافة من زمن انتقال الصدى) |
| الواجهة | رقمية: نبضة إطلاق مدتها 10 µs تدخل على Trig، وعرض نبضة الصدى يخرج على Echo |
| التغذية | 5 V، نحو 15 mA؛ ويحتاج مخرج Echo إلى مقسم جهد مع المتحكمات الدقيقة العاملة على 3.3 V مثل ESP32 |
| المدى (ورقة البيانات) | من 2 إلى 400 cm، دقة تمييز 0.3 cm، ±3 mm في أفضل الأحوال |
| المدى (المقيس هنا) | اختُبر من 2.1 إلى 101.4 cm؛ تشتت من القمة إلى القمة قدره 9 mm مع هدف ثابت على بعد 89 cm |
| معدل التحديث | دورة دنيا مدتها 60 ms حسب ورقة البيانات (نحو 16 قراءة/s)؛ المقيس هنا: 67.5 ms لكل قراءة عبر Serial، ولوحة المعلومات تستعلم كل 200 ms |
| الحزمة | مخروط بزاوية نحو 15 درجة؛ يُرجع مسافة أقرب جسم داخل المخروط |
| الأنسب لـ | الأهداف المستوية والصلبة والعمودية: الجدران، والألواح، وأسطح السوائل، وأنظمة المساعدة على الركن |
| تجنّبه إذا | كانت الأهداف ليّنة أو مائلة أو صغيرة جدًا أو أقرب من 2 cm، أو كنت تحتاج إلى دقة بالمليمتر |
المستشعر التالي في السلسلة يستشعر هو الآخر عن بُعد، لكن بفيزياء مختلفة: مستشعر PIR يكشف الأشعة تحت الحمراء الصادرة عن الأجسام الدافئة بدلًا من الصدى. وإذا أردت في هذه الأثناء أن يفعل ESP32 نفسه أكثر من ذلك، فإن مشروع بناء OmArm Zero يضع ستة محركات سيرفو وواجهة ويب على اللوحة نفسها.
9. التنزيلات والروابط
| الملف | الوصف |
|---|---|
| hc-sr04_test.ino | برنامج اختبار Serial من القسم 5 |
| hc-sr04_plotter.ino | برنامج القسم 6 مع لوحة المعلومات مدمجة فيه، برفع واحد |
| web_src/ | لوحة المعلومات بصيغة HTML وCSS وJS قابلة للتعديل |
| schematic.svg | مخطط التوصيل مع مقسم جهد Echo |
| OmBlock HC-SR04.stl | حامل للمستشعر قابل للطباعة ثلاثية الأبعاد (3D) لنظام OmGrid |
| hc-sr04_animation.html | الرسوم المتحركة التفاعلية من القسم 3 |
| HC-SR04_Guide_EN.pdf | دليل قابل للطباعة لطاولة العمل: معلومات سريعة، والتوصيلات، والكود الكامل، واستكشاف الأخطاء وإصلاحها |
| HC-SR04_Guide_AR.pdf | الدليل نفسه باللغة العربية |
| HC-SR04_QuickStart_EN.pdf | بطاقة بدء سريع من صفحة واحدة لطاولة العمل |
| HC-SR04_QuickStart_AR.pdf | بطاقة البدء السريع باللغة العربية |
البرنامج الثابت مرخّص بموجب رخصة MIT، لذا يمكنك استخدامه في أي مشروع. أما ملفات CAD والوثائق فهي للاستخدام الشخصي.
ورقة البيانات: HC-SR04 (نسخة مستضافة لدى SparkFun)
القطع والأطقم: متجر OmArTronics؛ ويحتوي مربع روابط التسويق بالعمولة في القسم 4.1 على روابط للمكونات كلٍّ على حدة.
التالي في السلسلة: مستشعر الحركة PIR طراز HC-SR501 (قريبًا).
احصل على كل شيء في تنزيل واحد مجاني: حزمة HC-SR04 Sensor Lab. تحتوي على البرنامج الثابت، وملفات التوصيل، والمجموعة الكاملة من ملفات STL لنظام OmGrid، ودليلَي PDF، والعرض التقديمي المكوّن من 24 شريحة، والرسوم المتحركة، وبيانات القياس الخام من هذا المقال.
شرح بالفيديو: شاهد خطوات البناء في فيديو مدته 8 دقائق على YouTube.
