هذا الدليل جزء من: كاميرا Makerfabs MaTouch AI ESP32S3 2.8 بوصة
أحدث لوحة MaTouch AI تدمج إدخال صوت I2S/مكبر صوت I2S/ كاميرا بدقة 3 ملايين بكسل OV3660/ شاشة بدقة 320*240، مع معالج ESP32S3 القوي وقدرة WiFi، مما يجعل هذه اللوحة أداة/منصة جيدة لتطوير الذكاء الاصطناعي مع ESP32.
Makerfabs MaTouch ESP32-S3 2.8 بوصة كاميرا ESP32-S3 رؤية ذكاء اصطناعي: وجّه الكاميرا واسأل عمّا تراه
تلتقط اللوحة صورة، ويصف الذكاء الاصطناعي ما يراه، ثم ينطق الإجابة بصوت مسموع
وجّه الكاميرا نحو شيء ما، اضغط زرًا، وسيخبرك الذكاء الاصطناعي بما ينظر إليه - مطبوعًا على الشاشة ومنطوقًا عبر مكبر الصوت. وضعان: تسمية الأشياء في مجال الرؤية، أو وصف الشخص أمام الكاميرا.
رؤية الذكاء الاصطناعي - وجّهها نحو شيء ما تعمل على لوحة MaTouch AI ESP32-S3
كيف تصل الصورة إلى هناك
تنتج الكاميرا صورة JPEG بدقة 800×600 بحجم يتراوح بين 20-40 كيلوبايت تقريبًا. تقوم اللوحة بترميزها بتنسيق base64 في ذاكرة PSRAM، مما يجعلها أكبر بنحو الثلث كنص، وتضمّنها في الطلب كمعرّف بيانات (data URI). ذاكرة PSRAM بسعة 8 ميجابايت هي ما يجعل هذا الأمر مريحًا.
وصف، وليس تحديد هوية أبدًا
وضع الشخص (PERSON) يصف ما يمكنه رؤيته - المزاج، النظارات، ما يبدو أن الشخص يفعله. لن يخبرك من يكون شخص ما، وهذا أمر مقصود. تحديد هوية الأشخاص من خلال الوجه مخالف لسياسات استخدام OpenAI، وخدمة تحديد الوجه المكافئة من Microsoft مقيدة بعملية موافقة لا يمكن للمطورين العاديين التسجيل فيها ببساطة. أي برنامج تعليمي يعدك بتحديد الوجه عبر السحابة يصف شيئًا غير متاح بشكل عام.
إذا كنت تريد لوحة تتعرّف على أشخاص محددين، استخدم المشروع 03b - فهو يعمل دون اتصال بالإنترنت ولا يرسل وجه أي شخص إلى أي مكان.
يجب أن تتوقف الكاميرا من أجل الشبكة
يتم إيقاف تشغيل عدسة الكاميرا المباشرة أثناء طرح السؤال. هذا ليس أمرًا تجميليًا: برنامج تشغيل الكاميرا قيد التشغيل يحتفظ بالذاكرة الداخلية التي يحتاجها الاتصال الآمن، ومع تشغيل المعاينة يفشل الاتصال ببساطة. إيقاف تشغيل الكاميرا يحرر تلك الذاكرة. كما يعني ذلك أن الإجابة تبقى مقروءة بدلاً من إعادة رسمها أكثر من 25 مرة في الثانية.
تبقى الإجابة حتى تقوم بإغلاقها
تبقى الإجابة على الشاشة حتى تضغط على مسح (CLEAR) - لا يوجد مؤقت. أثناء عرضها، يظهر زر إعادة التشغيل (REPLAY) لتشغيل الإجابة المنطوقة مرة أخرى من الذاكرة، دون استدعاء ثانٍ لواجهة برمجة التطبيقات ودون أي تكلفة إضافية.
حول لوحة MaTouch AI ESP32-S3 2.8"
كل مشروع في هذه الصفحة يعمل على لوحة MaTouch AI ESP32-S3 2.8" TFT ST7789V من Makerfabs. إنها لوحة متكاملة: شاشة لمس ملونة، كاميرا بدقة 3 ميجابكسل، ميكروفونان ومضخم صوت حقيقي، كل ذلك مدعوم بمعالج ESP32-S3 مع ذاكرة PSRAM بسعة 8 ميجابايت. هذا المزيج هو ما يجعل مشاريع الذكاء الاصطناعي هذه ممكنة على لوحة واحدة دون أي شيء إضافي متصل بها.
ذاكرة PSRAM بسعة 8 ميجابايت أهم من أي رقم آخر هنا. إنها ما يسمح للوحة بالاحتفاظ بإطار كاميرا، أو بضع ثوانٍ من الصوت المسجل، أو صورة مشفرة بتنسيق base64 في الذاكرة في نفس الوقت - ولا يتسع أي منها لذاكرة ESP32 العادية.
وثائق الشركة المصنعة: صفحة Makerfabs wiki.
المواصفات الرئيسية
المعالج: ESP32-S3، ثنائي النواة بتردد 240 ميجاهرتز، WiFi 2.4 جيجاهرتز + Bluetooth 5.0
الذاكرة: فلاش بسعة 16 ميجابايت، PSRAM بسعة 8 ميجابايت (مطلوبة في كل مشروع تقريبًا هنا)
الشاشة: IPS بحجم 2.8 بوصة، دقة 320×240، مشغل ST7789V، واجهة SPI
اللمس: سعوي GT911، يتتبع 5 أصابع في وقت واحد
الكاميرا: OV3660، بدقة 3 ميجابكسل، حتى 2048×1536
الميكروفونات: ميكروفونان رقميان INMP441 بواجهة I2S (زوج استريو حقيقي)
مكبر الصوت: مضخم فئة D MAX98357A، بقدرة 3.2 واط على مقاومة 4 أوم
التخزين: فتحة بطاقة microSD (وضع SPI)
الطاقة: USB-C، موصل بطارية JST، شاحن TP4056، مفتاح طاقة
مكونات إضافية على اللوحة: مصباح LED RGB WS2812B، ساعة زمن حقيقي PCF8563T ببطارية احتياطية، ومقياس شحن بطارية MAX17048 غير مذكور في المواصفات الرسمية
منفذا USB-C ليسا متطابقين. مكبر صوت اللوحة يشارك منافذ الإشارة (IO19 وIO20) مع منفذ USB الأصلي، لأن تلك المنافذ هي خطوط بيانات USB المدمجة في معالج ESP32-S3. قم دائمًا بالرفع والشحن عبر منفذ USB-C CH340K (الموجود بجانب زر إعادة التعيين RESET)، واضبط خيار USB CDC On Boot على Disabled. استخدام المنفذ الخاطئ سيؤدي إلى خلل في الصوت أو فشل في الرفع.
إعدادات Arduino IDE
هذه الإعدادات مهمة. معظم المشاكل التي يبلغ عنها الأشخاص مع هذه اللوحة هي أحد هذه الإعدادات الخاطئة، وهي تتم إعادة تعيينها عند تغيير إصدار النواة، لذا تحقق منها مرة أخرى بعد أي تغيير.
الإعداد | القيمة |
|---|---|
اللوحة | ESP32S3 Dev Module |
إصدار نواة ESP32 | 2.0.17 |
PSRAM | OPI PSRAM |
حجم الفلاش | 16MB (128Mb) |
مخطط الأقسام | 16M Flash (3MB APP/9.9MB FATFS) |
USB CDC On Boot | Disabled |
سرعة الرفع | 921600 |
مسح كل الفلاش قبل الرفع | Disabled |
المنفذ | منفذ USB-C CH340K |
استخدم نواة ESP32 الإصدار 2.0.17، وليس 3.x. أزالت Espressif نماذج اكتشاف الوجه المدمجة في الجهاز في النواة 3، لذا لن يتم تجميع مشاريع الوجه هناك. تثبيت الإصدار 2.0.17 يبقي كل مشروع في هذه الصفحة يعمل بإعداد واحد. في Boards Manager، تتيح لك القائمة المنسدلة للإصدارات التبديل ذهابًا وإيابًا متى شئت.
استخدم مكتبة GFX Library for Arduino الإصدار 1.5.6، وليس 1.6.x. إصدارات 1.6 مبنية لـ ESP32 core 3 وقد تتعطل عند بدء التشغيل على core 2.0.17. إذا بقيت شاشتك سوداء بعد الرفع، فهذا أول شيء يجب التحقق منه.
المكتبات المطلوبة
قم بتثبيتها من خلال Tools ← Manage Libraries في بيئة Arduino IDE. أرقام الإصدارات مهمة - يرجى استخدام تلك المذكورة.
المكتبة | الإصدار | المؤلف |
|---|---|---|
GFX Library for Arduino | 1.5.6 | moononournation |
bb_captouch | 1.3.1 | Larry Bank |
ArduinoJson | 7.x | Benoit Blanchon |
Adafruit NeoPixel | أي إصدار حديث | Adafruit |
إعداد secrets.h
تفاصيل WiFi الخاصة بك وأي مفاتيح API توضع في secrets.h، المضمن في التنزيل بقيم افتراضية. افتح تلك العلامة التبويبية في بيئة Arduino IDE واستبدلها بقيمك الخاصة.
يجب أن يكون WiFi بتردد 2.4 جيجاهرتز. لا يمكن لـ ESP32-S3 رؤية شبكة 5 جيجاهرتز على الإطلاق. إذا كان جهاز التوجيه الخاص بك يجمع بين النطاقين تحت اسم واحد (تسميه Asus Smart Connect)، فإما أن توقف ذلك أو تعطي نطاق 2.4 جيجاهرتز اسمًا خاصًا به وتستخدمه في secrets.h.
الحصول على مفاتيح API الخاصة بك
يتواصل هذا المشروع مع خدمة ذكاء اصطناعي سحابية، لذا تحتاج إلى مفتاح خاص بك. إذا لم تفعل هذا من قبل، فلا تقلق - إنها نفس فكرة كلمة المرور التي تحدد حسابك للخدمة. يستغرق الأمر بضع دقائق، مرة واحدة.
المفتاح ليس اشتراكًا في موقع ويب. على سبيل المثال، الدفع مقابل ChatGPT Plus لا يمنحك مفتاح API - الاثنان منتجان منفصلان بفوترة منفصلة. تحتاج إلى حساب على منصة المطورين، كما هو موضح أدناه.
OpenAI - العيون
توفر OpenAI نموذج الرؤية الذي ينظر إلى الصور. فقط المشاريع التي ترسل صورة تحتاج إلى هذا.
انتقل إلى platform.openai.com/api-keys وسجل الدخول أو أنشئ حسابًا.
انقر على Create new secret key، وأعطه اسمًا، وأنشئه.
انسخه فورًا - مثل DeepSeek، يظهر مرة واحدة فقط.
افتح Billing وأضف مبلغًا صغيرًا من الرصيد. واجهة API مدفوعة مسبقًا ومنفصلة عن أي اشتراك ChatGPT قد يكون لديك بالفعل.
ضع المفتاح في secrets.h كـ OPENAI_KEY.
Microsoft Azure Speech - للاستماع والتحدث
يحول Azure كلامك إلى نص ويحول الإجابة إلى صوت. الطبقة المجانية سخية بما يكفي لكل شيء في هذه الصفحة.
انتقل إلى portal.azure.com وسجل الدخول بحساب Microsoft (الحساب المجاني جيد).
إذا لم تستخدم Azure من قبل، سترى شاشة Welcome to Azure تقدم ثلاثة خيارات. اختر Start with an Azure free trial - تحتاج إلى اشتراك قبل أن يسمح لك Azure بإنشاء أي شيء. (يجب على الطلاب اختيار Azure for Students بدلاً من ذلك: نفس النتيجة، بدون بطاقة مطلوبة.) تجاهل Manage Microsoft Entra ID، وهو شيء مختلف تمامًا.
انقر على Create a resource، وابحث عن Speech، واختر Speech service المنشور من Microsoft.
املأ النموذج: أي مجموعة موارد، أي اسم، واختر Region قريبًا منك - اكتب تلك المنطقة تمامًا كما تظهر، على سبيل المثال
eastus.بالنسبة لـ Pricing tier اختر F0 (Free). يسمح بحوالي خمس ساعات من تحويل الكلام إلى نص ونصف مليون حرف من تحويل النص إلى كلام كل شهر.
انقر على Review + create، ثم Create. انتظر حوالي دقيقة، ثم انقر على Go to resource.
في القائمة اليسرى افتح Keys and Endpoint. انسخ KEY 1 و Location/Region.
ضع تلك في secrets.h كـ AZURE_SPEECH_KEY و AZURE_REGION. بالنسبة لـ AZURE_STT_HOST، استخدم <region>.stt.speech.microsoft.com - لذا مع المنطقة eastus يكون ذلك eastus.stt.speech.microsoft.com.
بخصوص بطاقة الائتمان. تطلب النسخة التجريبية المجانية من Azure بطاقة للتحقق من هويتك. لا تفرض عليك رسومًا. تحصل على رصيد بقيمة 200 دولار لمدة 30 يومًا، وبعد ذلك ينتقل الحساب إلى الدفع حسب الاستخدام - لكن طبقة F0 Speech تبقى مجانية، شهرًا بعد شهر، وكل شيء في هذه المشاريع يناسبها بشكل مريح. إذا كنت تفضل عدم تقديم بطاقة على الإطلاق وكنت طالبًا، فإن خيار Azure for Students يمنحك رصيدًا بدونها.
يجب أن يكون مورد "Speech service". مفتاح من مورد Translator أو Language أو Cognitive Services عام يبدو متطابقًا وهو صالح تمامًا - لكن كل طلب كلام يعيد خطأ 401. هذا أمسك بنا أثناء الاختبار وكلفنا ساعة. إذا فشل الكلام مع 401 بينما يبدو المفتاح صحيحًا، تحقق من نوع المورد الذي أنشأته.
كم تكلفة التشغيل
قليلة جدًا، لكنها ليست مجانية، ويجب أن تعرف تقريبًا ما تنفقه قبل ترك مشروع يعمل.
الخدمة | التكلفة التقريبية |
|---|---|
Azure Speech | الطبقة المجانية تغطي حوالي 5 ساعات من الاستماع و0.5 مليون حرف من التحدث شهريًا |
DeepSeek | جزء من سنت لكل إجابة - آلاف الردود مقابل بضعة دولارات |
OpenAI vision | حوالي سنت أو سنتان لكل صورة، حسب النموذج |
الأسعار تتغير، لذا تعامل مع هذه كدليل وليس كعرض سعر. كل هذه الخدمات لديها صفحة استخدام حيث يمكنك مراقبة ما أنفقته، وجميعها تتيح لك تعيين حد إنفاق - وهو أمر يستحق القيام به من اليوم الأول.
احتفظ بمفاتيحك سرية. أي شخص يمتلكها يمكنه إنفاق أموالك. لا تضعها في فيديو أو لقطة شاشة أو منشور في منتدى أو مستودع كود عام. إذا تم كشف مفتاح، احذفه على موقع المزود وأنشئ مفتاحًا جديدًا - يستغرق ذلك ثوانٍ، وهو الحل الحقيقي الوحيد.
استكشاف الأخطاء وإصلاحها
العرض | السبب والحل |
|---|---|
تبقى الشاشة سوداء | إصدار خاطئ لمكتبة GFX (استخدم 1.5.6) أو إعدادات لوحة خاطئة. |
|
|
لا شيء يُرفع / لا يوجد منفذ COM | منفذ USB-C خاطئ، أو برنامج تشغيل CH340 غير مثبت. |
تفشل الكاميرا ولا تتعافى أبدًا | خط إعادة تعيين الكاميرا مرتبط بزر RESET على اللوحة، لذا لا يمكن للبرنامج إعادة تشغيلها. اضغط RESET. إذا استمر الفشل، أعد تركيب كابل الشريط الخاص بالكاميرا. |
تنزيل الكود
مخطط Arduino الكامل لهذا المشروع، مع pins.h وكل ما يحتاجه، متاح للتنزيل مجانًا.
فك ضغطه، وافتح ملف .ino في Arduino IDE، وتحقق من الإعدادات أعلاه، وارفعه عبر منفذ CH340K USB-C.
هذا الدليل هو جزء من: كاميرا Makerfabs MaTouch AI ESP32S3 2.8 بوصة
- Makerfabs MaTouch ESP32-S3 شاشة تعمل باللمس وكاميرا بحجم 2.8 بوصة: صور بدقة 3 ميجابكسل إلى بطاقة SD
- Makerfabs MaTouch ESP32-S3 2.8 بوصة كاميرا كشف الوجه دون اتصال على ESP32-S3 - بدون إنترنت، بدون مفتاح API
- Makerfabs MaTouch ESP32-S3 2.8 بوصة كاميرا قم ببناء مساعد صوتي بالذكاء الاصطناعي على ESP32-S3 (Azure + DeepSeek)
/*
* ===========================================================================
* 05_Vision_AI - MaTouch AI ESP32-S3 2.8" TFT ST7789V
* ===========================================================================
*
* ---------------------------------------------------------------------------
* فيديو وتعليمات مكتوبة
*
* شاهد الفيديو: https://youtu.be/Px3FT47RV2M
* المقال الكامل: https://robojax.com/RTJ852
* ("رؤية AI" - الأسلاك، الإعدادات، لقطات الشاشة
* وأجوبة على المشكلات الشائعة)
*
* مشاهدة الفيديو أولاً ستوفر لك الوقت - حيث يوضح إعدادات Arduino IDE
* وإصدارات المكتبات خطوة بخطوة.
* ---------------------------------------------------------------------------
*
* اللوحة ترى. وجه الكاميرا نحو شيء، اضغط على زر، ونموذج رؤية OpenAI يخبرك بما تنظر إليه - مرسوم على الشاشة
* و(اختياريًا) يتم نطقه من مكبر الصوت الخاص باللوحة عبر Azure TTS.
*
* وضعان، زران:
*
* الأشياء "ماذا ترى؟" - يذكر الأشياء أمام العدسة.
* الشخص يصف الشخص في الإطار: النظارات، التعبير، ماذا
* يفعل. الوصف فقط - لن يحاول ولن يجب عليه
* أن يقول من هو شخص ما. تحديد الأشخاص من خلال الوجه هو
* ضد سياسات استخدام OpenAI، وهذا هو القرار الصحيح:
* قل هذا في الفيديو، فهو يستحق 15 ثانية صادقة.
*
* لماذا OpenAI لهذا العرض وليس DEEPSEEK: واجهة برمجة التطبيقات الخاصة بـ DeepSeek نصية فقط.
* لا يمكنها قبول صورة على الإطلاق. يمكن لـ Azure OpenAI القيام بذلك، ولكن
* OpenAI العادي هو نقطة نهاية واحدة دون إعداد نشر - الأسهل للمتابعة.
*
* كيف تنتقل الصورة: توفر لنا OV3660 صورة JPEG مباشرة (800x600،
* ~40 كيلوبايت). نقوم بتشفيرها بـ base64 في PSRAM (~55 كيلوبايت من النص) وندمجها في
* طلب JSON كـ URI للبيانات. الـ 8 ميغابايت من PSRAM تجعل هذا بسيطًا.
*
* ---------------------------------------------------------------------------
* املأ secrets.h قبل الوميض
* (بحاجة إلى WIFI_*، OPENAI_*؛ AZURE_* فقط إذا كانت SPEAK_REPLIES 1).
*
* إعدادات اللوحة (قائمة الأدوات - كل سطر مهم، الخطأ = شاشة سوداء
* أو أخطاء ترجمة. هذه تعيد تعيينها عند تبديل النوى - تحقق منها مرة أخرى!)
*
* اللوحة: ESP32S3 Dev Module
* نواة ESP32: 2.0.17
* PSRAM: OPI PSRAM <-- مطلوب، حيث توجد مخازن الصور
* حجم الوميض: 16 ميغا بايت (128 ميجابت)
* نظام التقسيم: 16M Flash (3MB APP/9.9MB FATFS)
* USB CDC عند التمهيد: معطل <-- يتشارك مكبر الصوت دبابيس مع USB الأصلي
* سرعة التحميل: 921600
* المنفذ: منفذ USB-C CH340K (القريب من RESET)
*
* المكتبات
* مكتبة GFX لآردوينو v1.5.6 (ليس 1.6.x - تلك تتناسب مع النواة 3)
* bb_captouch v1.3.1
* ArduinoJson v7.x
* Adafruit NeoPixel أي إصدار حديث
*
* ---------------------------------------------------------------------------
* الدوال في هذا السكيتش
* led(r,g,b) تعيين لون LED الحالة RGB
* getTouch(&x,&y) قراءة لوحة اللمس، mapped to screen coordinates
* camStart(...) بدء الكاميرا بتنسيق/حجم معين
* camStartPreview() بدء الكاميرا RGB565 بدقة 240x240
* captureJpeg(&len) أخذ صورة JPEG واحدة بدقة 800x600 إلى PSRAM (تبقى الكاميرا مطفأة
* بعد ذلك - المتصل يعيد تشغيل العرض)
* readHttpResponse() قراءة رد HTTPS، وفك تشفيره بشكل صحيح
* askVision(...) إرسال صورة + موجه إلى OpenAI، وإرجاع الإجابة
* spkInit() إعداد مخرج مكبر صوت I2S
* readExact(...) قراءة بالضبط N بايت من اتصال TLS
* speak(text) Azure TTS -> تنزيل الصوت إلى PSRAM -> تشغيله
* playLastAnswer() إعادة تشغيل الصوت المخزن من PSRAM (زر إعادة التشغيل)
* drawButton(...) رسم زر عمود جانبي واحد
* drawWifi() أعمدة إشارة WiFi + قراءة dBm
* drawButtons() عمود جانبي عادي (زرين للسؤال)
* drawClearSide() عمود جانبي وضع الرد (CLEAR + REPLAY)
* showAnswer(text) إجابة ملفوفة على شاشة العرض
* lookAndTell(...) دورة كاملة: الالتقاط -> السؤال -> العرض -> الكلام
* setup() / loop() تسلسل التمهيد / معالجة العرض + لمسة
*
* Robojax.com
* ===========================================================================
*/
#define SPEAK_REPLIES 1 // 1 = اقرأ الإجابة بصوت عالٍ باستخدام TTS من Azure، 0 = الشاشة فقط
/*
* سطوع LED الحالة، من 0 إلى 255. تعمل WS2812 من مصدر الطاقة وتكون ساطعة بشكل مزعج عند القدرة الكاملة - 25 واضحة تمامًا على الكاميرا.
*/
#define LED_BRIGHTNESS 15
/*
* اتجاه الكاميرا
* 1 = الكاميرا تواجه نفس اتجاه الشاشة (كما يشحن اللوح) - الصورة معكوسة بحيث تبدو طبيعية عند توجيهها نحو نفسك.
* 0 = لقد طويت الشريط بحيث تكون العدسة مواجهة بعيدا عن الشاشة
* (على الطراز الهاتفي، الشاشة إليك / الكاميرا نحو الموضوع).
* إذا كانت الصورة تبدو معكوسة من اليسار إلى اليمين، قم بتغيير هذا الرقم.
*/
#define CAMERA_FACES_USER 1
#include <Arduino_GFX_Library.h>
#include <bb_captouch.h>
#include <Adafruit_NeoPixel.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include "mbedtls/base64.h"
#include "esp_camera.h"
#include "pins.h"
#include "secrets.h"
#if SPEAK_REPLIES
#include "driver/i2s.h"
#define WAV_HEADER_LEN 44
#endif
Arduino_ESP32SPI *bus = new Arduino_ESP32SPI(
TFT_DC, TFT_CS, TFT_SCLK, TFT_MOSI, TFT_MISO, HSPI, true);
Arduino_GFX *gfx = new Arduino_ST7789(bus, TFT_RES, 1, true);
BBCapTouch bbct;
Adafruit_NeoPixel rgb(RGB_LED_NUM, RGB_LED_PIN, NEO_GRB + NEO_KHZ800);
/*
* --- مطالب رؤية الاثنان ------------------------------------------------
* اجعل الإجابات قصيرة: يجب أن تتناسب مع شاشة بمقاس 320x240، وإذا تم نطقها، يجب ألا تترك المقدم في حيرة على الكاميرا.
*/
const char *PROMPT_OBJECTS =
"Look at this photo from a small camera. In ONE short sentence of at most "
"20 words, plain text only, name the main object(s) you see.";
const char *PROMPT_PERSON =
"Look at this photo. If there is a person, describe them in ONE short "
"sentence of at most 20 words: mood, glasses or not, what they are doing. "
"Never guess who they are. If no person is visible, say so. Plain text only.";
/*
* --- تخطيط: عدسة رؤية على اليسار، أزرار على اليمين ------------------------------- */
* #define BTN_X 242
* #define BTN_W 78
* #define BTN_H 52
* #define BTN_OBJ_Y 4
* #define BTN_PERSON_Y 62
*
* void led(uint8_t r, uint8_t g, uint8_t b) { rgb.setPixelColor(0, rgb.Color(r, g, b)); rgb.show(); }
*
* /* ===========================================================================
* لمس
* ===========================================================================
*/
bool getTouch(uint16_t *x, uint16_t *y) {
TOUCHINFO ti;
if (!bbct.getSamples(&ti)) return false;
if (ti.count < 1) return false;
*x = ti.y[0];
*y = (ti.x[0] > 240) ? 0 : (240 - ti.x[0]);
return true;
}
/*
* الكاميرا - RGB565 لعرض مباشر؛ يتم التقاط JPEG من خلال إعادة تشغيل البرنامج التعريفي، نفس التقنية المستخدمة في الرسم التخطيطي 02.
*/
bool mirrored = CAMERA_FACES_USER; // انظر التعريف في أعلى الملف
/*
* يكون المنظار مطفأ أثناء تشغيل مكالمة سحابية وأثناء ظهور الرد على الشاشة. هناك سببان، كُلاهما تم تعلمهما أثناء التجربة:
* 1. الذاكرة العشوائية: برنامج تشغيل الكاميرا الحي يستهلك الذاكرة الداخلية التي يحتاجها مصافحة TLS - مع تشغيل المعاينة، يفشل الاتصال بـ OpenAI (HTTP -1).
* 2. قابلية القراءة: يعيد المنظار رسم الصورة 25 مرة في الثانية وقد يمسح الرد.
* يظل الرد على الشاشة حتى يقوم المستخدم بالنقر على مسح - لا يوجد مؤقت.
*/
bool preview_on = false;
/*
* تُحفظ الإجابة الأخيرة المنطوقة في PSRAM حتى يتمكن زر إعادة التشغيل من تشغيلها مرة أخرى دون الحاجة إلى اتصال سحابي آخر. تُستبدل بالإجابة التالية.
*/
uint8_t *last_audio = nullptr;
size_t last_audio_len = 0;
bool camStart(pixformat_t fmt, framesize_t size, int fb_count, int quality) {
camera_config_t c;
c.ledc_channel = LEDC_CHANNEL_0;
c.ledc_timer = LEDC_TIMER_0;
c.pin_d0 = CAM_PIN_D0; c.pin_d1 = CAM_PIN_D1;
c.pin_d2 = CAM_PIN_D2; c.pin_d3 = CAM_PIN_D3;
c.pin_d4 = CAM_PIN_D4; c.pin_d5 = CAM_PIN_D5;
c.pin_d6 = CAM_PIN_D6; c.pin_d7 = CAM_PIN_D7;
c.pin_xclk = CAM_PIN_XCLK;
c.pin_pclk = CAM_PIN_PCLK;
c.pin_vsync = CAM_PIN_VSYNC;
c.pin_href = CAM_PIN_HREF;
/*
* شارك ناقل I2C الذي تدعمه Wire (لوحة اللمس موجودة هناك أيضًا) بدلاً من السماح للكاميرا بتثبيت محرك ثانٍ على نفس الدبابيس - ذلك يقتل اللمس. يجب أن تعمل Wire.begin() قبل هذه الوظيفة.
*/
c.pin_sccb_sda = -1;
c.pin_sccb_scl = -1;
c.sccb_i2c_port = 0; // سلك = منفذ I2C 0
c.pin_pwdn = CAM_PIN_PWDN;
c.pin_reset = CAM_PIN_RESET;
c.xclk_freq_hz = 20000000;
c.frame_size = size;
c.pixel_format = fmt;
c.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
c.fb_location = CAMERA_FB_IN_PSRAM;
c.jpeg_quality = quality;
c.fb_count = fb_count;
if (esp_camera_init(&c) != ESP_OK) return false;
sensor_t *s = esp_camera_sensor_get();
if (s) {
s->set_hmirror(s, mirrored ? 1 : 0);
s->set_vflip(s, mirrored ? 1 : 0);
s->set_brightness(s, 1);
}
return true;
}
bool camStartPreview() { return camStart(PIXFORMAT_RGB565, FRAMESIZE_240X240, 2, 12); }
/*
* التقاط صورة JPEG بدقة SVGA في ذاكرة مؤقتة يمتلكها المتصل. */
* uint8_t *captureJpeg(size_t *len_out) {
* esp_camera_deinit();
* delay(120);
* إذا لم يبدأ الكاميرا(PIXFORMAT_JPEG، FRAMESIZE_SVGA، 1، 12) { *len_out = 0; return nullptr; }
*
* // بعض الإطارات التمهيدية لتستقر التعرض
* for (int i = 0; i < 3; i++) {
* camera_fb_t *w = esp_camera_fb_get();
* إذا كان(w) esp_camera_fb_return(w);
* delay(100);
* }
*
* uint8_t *copy = nullptr;
* len_out = 0;
* camera_fb_t *fb = esp_camera_fb_get();
* إذا كان(fb && fb->len > 0) {
* copy = (uint8_t *)ps_malloc(fb->len);
* إذا كان(copy) { memcpy(copy, fb->buf, fb->len); *len_out = fb->len; }
* }
* إذا كان(fb) esp_camera_fb_return(fb);
*
* /* تعمد ترك الكاميرا في حالة إيقاف هنا. المتصل يعيد بدء المعاينة
* بعد مكالمة السحابة - تشغيل برنامج تشغيل الكاميرا يجوع عملية المصافحة TLS
* من الذاكرة الداخلية (كانت هذه هي خطأ "Vision HTTP -1").
*/
esp_camera_deinit();
delay(120);
return copy;
}
/*
* قارئ استجابة HTTP - مشترك من خلال المكالمات السحابية أدناه. يعود رمز الحالة ويملاً body_out. يعالج تشفير النقل المجزأ بشكل صحيح: يجب إزالة علامات حجم الشظايا، وإلا ستبقى مدمجة داخل JSON وسيفشل التحليل. (تقوم OpenAI بعرض ردودها بشكل جميل لذا تمتد عبر عدة شظايا - وهذا أثر علينا في الاختبار.)
*/
/*
* قم بحظر الاتصال حتى يتوفر لديه بيانات (أو ينفد الميزانية). كل عملية قراءة أدناه تمر عبر هذا، لأن الخادم يمكن أن يظل صامتًا لعدة ثوانٍ بينما يحلل الصورة - وقراءة عارية (read()) ستنتهي ببساطة بمهلة.
*/
static bool waitData(WiFiClientSecure &c, uint32_t ms) {
uint32_t t0 = millis();
while (!c.available()) {
if (!c.connected()) return false;
if (millis() - t0 > ms) return false;
delay(10);
}
return true;
}
static int readHttpResponse(WiFiClientSecure &client, String &body_out, uint32_t idle_ms) {
body_out = "";
if (!waitData(client, idle_ms)) { Serial.println("HTTP: no response at all"); return 0; }
String status_line = client.readStringUntil('\n');
int code = 0;
sscanf(status_line.c_str(), "HTTP/%*s %d", &code);
bool chunked = false;
while (waitData(client, idle_ms)) {
String h = client.readStringUntil('\n');
if (h == "\r" || h.length() <= 1) break; // سطر فارغ = نهاية العناوين
h.toLowerCase();
if (h.startsWith("transfer-encoding:") && h.indexOf("chunked") >= 0) chunked = true;
}
if (chunked) {
int blanks = 0;
while (true) {
/*
* الانتظار هنا - بدلاً من ترك read() ينتهي عند الوقت المحدد - أمر أساسي: فقراءة انتهت عند الوقت المحدد تبدو كـ "0" (الجزء النهائي)، مما يقص الجسم بصمت إلى لا شيء بينما لا يزال الخادم يفكر.
*/
if (!waitData(client, idle_ms)) {
Serial.println("HTTP: timed out waiting for the next chunk");
break;
}
String szline = client.readStringUntil('\n');
szline.trim();
if (szline.length() == 0) {
if (++blanks > 4) break;
continue;
}
blanks = 0;
long sz = strtol(szline.c_str(), NULL, 16);
if (sz <= 0) break; // شريحة نهائية أصلية
long got = 0;
while (got < sz) {
if (!waitData(client, idle_ms)) break;
while (client.available() && got < sz) { body_out += (char)client.read(); got++; }
}
if (waitData(client, 3000)) client.readStringUntil('\n');
if (got < sz) { Serial.println("HTTP: short chunk"); break; }
}
} else {
while (waitData(client, idle_ms))
while (client.available()) body_out += (char)client.read();
}
return code;
}
/*
* استدعاء رؤية OpenAI الطلب مُعد يدويًا في PSRAM بدلاً من استخدام ArduinoJson، لأن تضمين سلسلة base64 بحجم 55 كيلوبايت في مستند JSON سيعني الاحتفاظ بنسختين. لا يحتاج نص base64 إلى هروب JSON، لذا فإن هذا آمن.
*/
bool askVision(const uint8_t *jpg, size_t jpg_len, const char *prompt, String &answer_out) {
// --- تشفير الصورة بتنسيق base64 إلى PSRAM ---
size_t b64_cap = ((jpg_len + 2) / 3) * 4 + 16;
unsigned char *b64 = (unsigned char *)ps_malloc(b64_cap);
if (!b64) return false;
size_t b64_len = 0;
if (mbedtls_base64_encode(b64, b64_cap, &b64_len, jpg, jpg_len) != 0) {
free(b64);
return false;
}
// --- اجمع طلب JSON حوله ---
/*
* ترفض النماذج الحالية من OpenAI الاسم القديم "max_tokens" - يجب أن يكون "max_completion_tokens" (تم التحقق منه ضد واجهة برمجة التطبيقات الحية، يوليو 2026).
*/
const char *head_fmt =
"{\"model\":\"%s\",\"max_completion_tokens\":%d,\"messages\":[{\"role\":\"user\","
"\"content\":[{\"type\":\"text\",\"text\":\"%s\"},"
"{\"type\":\"image_url\",\"image_url\":{\"url\":\"data:image/jpeg;base64,";
const char *tail = "\"}}]}]}";
char head[640];
int head_len = snprintf(head, sizeof(head), head_fmt, OPENAI_MODEL, LLM_MAX_TOKENS, prompt);
size_t body_len = head_len + b64_len + strlen(tail);
char *body = (char *)ps_malloc(body_len + 1);
if (!body) { free(b64); return false; }
memcpy(body, head, head_len);
memcpy(body + head_len, b64, b64_len);
strcpy(body + head_len + b64_len, tail);
free(b64);
// --- أرسلها: HTTP يدوي، تحميل على شكل قطع (نمط مثبت من 04) ---
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(25);
if (!client.connect(OPENAI_HOST, 443)) {
Serial.println("Vision: TLS connect failed (is the camera still running?)");
free(body);
return false;
}
client.print(String("POST /v1/chat/completions HTTP/1.1\r\n"
"Host: " OPENAI_HOST "\r\n"
"Authorization: Bearer " OPENAI_KEY "\r\n"
"Content-Type: application/json\r\n"
"Connection: close\r\n"
"Content-Length: ") + String(body_len) + "\r\n\r\n");
size_t sent = 0;
while (sent < body_len) {
size_t n = min((size_t)4096, body_len - sent);
size_t w = client.write((uint8_t *)body + sent, n);
if (w == 0) {
delay(50);
w = client.write((uint8_t *)body + sent, n);
if (w == 0) break;
}
sent += w;
yield();
}
free(body);
if (sent < body_len) {
Serial.printf("Vision: upload stalled at %u/%u bytes\n",
(unsigned)sent, (unsigned)body_len);
client.stop();
return false;
}
/*
* افتح الرد مع تقسيم صحيح للمحتوى */
* String resp;
* int code = readHttpResponse(client, resp, 30000);
* client.stop();
*
* if (code != 200) {
* Serial.printf("Vision HTTP %d: %s\n", code, resp.c_str());
* return false;
* }
*
* bool ok = false;
* JsonDocument doc;
* if (!deserializeJson(doc, resp)) {
* const char *content = doc["choices"][0]["message"]["content"];
* if (content) {
* answer_out = String(content);
* answer_out.trim();
* ok = answer_out.length() > 0;
* }
* } else {
* Serial.printf("Vision: فشل تحليل JSON، %u بايت تم استلامها\n", resp.length());
* }
* return ok;
* }
*
* /* ===========================================================================
* Azure TTS - نفس حيلة البث كما في المخطط 04: PCM الخام إلى I2S.
* ===========================================================================
*/
#if SPEAK_REPLIES
void spkInit() {
i2s_config_t cfg = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = 256,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0
};
i2s_pin_config_t pins = {
.mck_io_num = I2S_PIN_NO_CHANGE,
.bck_io_num = I2S_SPK_BCLK,
.ws_io_num = I2S_SPK_LRC,
.data_out_num = I2S_SPK_DOUT,
.data_in_num = I2S_PIN_NO_CHANGE
};
i2s_driver_install(I2S_SPK_PORT, &cfg, 0, NULL);
i2s_set_pin(I2S_SPK_PORT, &pins);
i2s_zero_dma_buffer(I2S_SPK_PORT);
}
/*
* اقرأ بالضبط n بايت من عميل (أو حتى انتهاء المهلة). */
* static size_t readExact(WiFiClientSecure &c, uint8_t *dst, size_t n) {
* size_t got = 0;
* uint32_t t0 = millis();
* while (got < n && millis() - t0 < 10000) {
* int r = c.read(dst + got, n - got);
* if (r > 0) { got += r; t0 = millis(); }
* else if (!c.connected() && !c.available()) break;
* else delay(2);
* }
* return got;
* }
*
* void playLastAnswer(); // مُعرّف أدناه؛ نموذج صريح لـ IDE
*
* void speak(const String &text) {
* String safe = text;
* safe.replace("&", "&");
* safe.replace("<", "<");
* safe.replace(">", ">");
* String ssml = "<speak version='1.0' xml:lang='" AZURE_TTS_LANG "'>"
* "<voice name='" AZURE_TTS_VOICE "'>" + safe + "</voice></speak>";
*
* /* HTTP يدوي مع تفكيك صحيح: ترسل Azure هذا الصوت متجزئًا، و
* يتسرب التدفق الخام لـ HTTPClient خطوط حجم التجزئة ASCII إلى PCM -
* يتم تشغيل كل واحدة كقرع مسموع.
*/
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(20);
const char *host = AZURE_REGION ".tts.speech.microsoft.com";
if (!client.connect(host, 443)) { Serial.println("TTS: TLS connect failed"); return; }
client.print(String("POST /cognitiveservices/v1 HTTP/1.1\r\n"
"Host: ") + host + "\r\n"
"Ocp-Apim-Subscription-Key: " AZURE_SPEECH_KEY "\r\n"
"Content-Type: application/ssml+xml\r\n"
"X-Microsoft-OutputFormat: riff-16khz-16bit-mono-pcm\r\n"
"User-Agent: MaTouchRobojax\r\n"
"Connection: close\r\n"
"Content-Length: " + String(ssml.length()) + "\r\n\r\n");
client.print(ssml);
String status_line = client.readStringUntil('\n');
int code = 0;
sscanf(status_line.c_str(), "HTTP/%*s %d", &code);
bool chunked = false;
long content_len = -1;
while (client.connected() || client.available()) {
String h = client.readStringUntil('\n');
if (h == "\r" || h.length() <= 1) break;
h.toLowerCase();
if (h.startsWith("transfer-encoding:") && h.indexOf("chunked") >= 0) chunked = true;
if (h.startsWith("content-length:")) content_len = h.substring(15).toInt();
}
if (code != 200) { Serial.printf("TTS HTTP %d\n", code); client.stop(); return; }
const size_t AUDIO_CAP = 1200 * 1024;
uint8_t *audio = (uint8_t *)ps_malloc(AUDIO_CAP);
if (!audio) { client.stop(); return; }
size_t alen = 0;
if (chunked) {
while (true) {
String szline = client.readStringUntil('\n');
long sz = strtol(szline.c_str(), NULL, 16);
if (sz <= 0) break;
if (alen + sz > AUDIO_CAP) break;
size_t got = readExact(client, audio + alen, sz);
alen += got;
client.readStringUntil('\n');
if (got < (size_t)sz) break;
}
} else if (content_len > 0) {
alen = readExact(client, audio, min((size_t)content_len, AUDIO_CAP));
} else {
uint32_t idle = millis();
while ((client.connected() || client.available()) && millis() - idle < 5000) {
int r = client.read(audio + alen, min((size_t)2048, AUDIO_CAP - alen));
if (r > 0) { alen += r; idle = millis(); }
else delay(5);
}
}
client.stop();
if (alen > WAV_HEADER_LEN) {
/*
* احتفظ بهذا الجواب لزر الإعادة (بدلاً من السابق)، ثم قم بتشغيله.
*/
if (last_audio) free(last_audio);
last_audio = audio;
last_audio_len = alen;
playLastAnswer();
} else {
free(audio);
}
}
/*
* لعب الإجابة المحفوظة من PSRAM - المستخدمة مباشرة بعد التحميل ومن قبل REPLAY. */
* فارغة playLastAnswer() {
* إذا لم تكن last_audio أو كان طول last_audio أقل من أو يساوي WAV_HEADER_LEN، أعد.
* ثابت uint8_t lead_in[640] = {0}; // 20 مللي ثانية من الصمت قبل التشغيل
* حجم w = 0؛
* i2s_write(I2S_SPK_PORT، lead_in، sizeof(lead_in)، &w، portMAX_DELAY)؛
* i2s_write(I2S_SPK_PORT، last_audio + WAV_HEADER_LEN،
* last_audio_len - WAV_HEADER_LEN، &w، portMAX_DELAY)؛
* i2s_write(I2S_SPK_PORT، lead_in، sizeof(lead_in)، &w، portMAX_DELAY)؛
* تأخير(150)؛
* i2s_zero_dma_buffer(I2S_SPK_PORT)؛
* }
* #endif // SPEAK_REPLIES
*
* /* ===========================================================================
* واجهة المستخدم
* ===========================================================================
*/
void drawButton(int y, const char *l1, const char *l2, uint16_t colour) {
gfx->fillRoundRect(BTN_X, y, BTN_W, BTN_H, 6, colour);
gfx->drawRoundRect(BTN_X, y, BTN_W, BTN_H, 6, WHITE);
gfx->setTextSize(1);
gfx->setTextColor(WHITE);
gfx->setCursor(BTN_X + 8, y + 14);
gfx->print(l1);
gfx->setCursor(BTN_X + 8, y + 28);
gfx->print(l2);
}
/*
* أشرطة WiFi + dBm في أسفل العمود الجانبي، تم تحديثها من الحلقة */
* void drawWifi() {
* gfx->fillRect(BTN_X, 188, BTN_W, 52, BLACK);
* bool up = (WiFi.status() == WL_CONNECTED);
* long rssi = up ? WiFi.RSSI() : -100;
* int bars = rssi > -55 ? 4 : rssi > -65 ? 3 : rssi > -75 ? 2 : rssi > -85 ? 1 : 0;
*
* for (int b = 0; b < 4; b++) {
* int bh = 6 + b * 6;
* uint16_t col = (b < bars) ? GREEN : gfx->color565(60, 60, 60);
* gfx->fillRect(BTN_X + 4 + b * 9, 216 - bh, 7, bh, col);
* }
* gfx->setTextSize(1);
* gfx->setCursor(BTN_X + 44, 196);
* if (up) {
* gfx->setTextColor(CYAN);
* gfx->printf("%ld", rssi);
* } else {
* gfx->setTextColor(RED);
* gfx->print("DOWN");
* }
* gfx->setCursor(BTN_X + 44, 208);
* gfx->setTextColor(gfx->color565(120, 120, 120));
* gfx->print("dBm");
* gfx->setCursor(BTN_X + 4, 228);
* gfx->setTextColor(gfx->color565(120, 120, 120));
* gfx->print("إشارة WiFi");
* }
*
* /* العمود الجانبي العادي: زري الطلب الاثنين
*/
void drawButtons() {
gfx->fillRect(BTN_X, 0, BTN_W, 188, BLACK);
drawButton(BTN_OBJ_Y, "WHAT DO", "YOU SEE?", gfx->color565(0, 90, 160));
drawButton(BTN_PERSON_Y, "DESCRIBE", "PERSON", gfx->color565(120, 60, 140));
gfx->setTextColor(CYAN);
gfx->setCursor(BTN_X + 2, 128);
gfx->print("OpenAI eyes");
gfx->setCursor(BTN_X + 2, 140);
gfx->print("Azure voice");
gfx->setCursor(BTN_X + 2, 152);
gfx->print("Robojax.com");
drawWifi();
}
/*
* وضع الإجابة عمود الجانب: مسح (العودة إلى الكاميرا) + إعادة التشغيل (قله مرة أخرى). تبقى الإجابة على الشاشة حتى يتم الضغط على مسح.
*/
void drawClearSide() {
gfx->fillRect(BTN_X, 0, BTN_W, 188, BLACK);
gfx->fillRoundRect(BTN_X, BTN_OBJ_Y, BTN_W, BTN_H, 6, gfx->color565(110, 35, 35));
gfx->drawRoundRect(BTN_X, BTN_OBJ_Y, BTN_W, BTN_H, 6, WHITE);
gfx->setTextSize(2);
gfx->setTextColor(WHITE);
gfx->setCursor(BTN_X + 10, BTN_OBJ_Y + 14);
gfx->print("CLEAR");
#if SPEAK_REPLIES
if (last_audio_len > 0) {
gfx->fillRoundRect(BTN_X, BTN_PERSON_Y, BTN_W, BTN_H, 6, gfx->color565(0, 110, 60));
gfx->drawRoundRect(BTN_X, BTN_PERSON_Y, BTN_W, BTN_H, 6, WHITE);
gfx->setTextSize(1);
gfx->setTextColor(WHITE);
gfx->setCursor(BTN_X + 16, BTN_PERSON_Y + 20);
gfx->print("REPLAY");
}
#endif
gfx->setTextSize(1);
gfx->setTextColor(YELLOW);
gfx->setCursor(BTN_X + 2, 124);
gfx->print("CLEAR = camera");
gfx->setCursor(BTN_X + 2, 136);
gfx->print("REPLAY = again");
drawWifi();
}
/*
* غطاء إجابة ملفوف بالكلمات عبر أسفل إطار الرؤية.
* void showAnswer(const String &text) {
* const int max_chars = 38;
* int lines = (text.length() + max_chars - 1) / max_chars;
* if (lines > 6) lines = 6;
* int h = lines * 10 + 8;
* int y = 240 - h;
*
* gfx->fillRect(0, y, 240, h, gfx->color565(0, 0, 0));
* gfx->drawRect(0, y, 240, h, CYAN);
* gfx->setTextSize(1);
* gfx->setTextColor(WHITE);
* for (int i = 0; i < lines; i++) {
* gfx->setCursor(4, y + 5 + i * 10);
* gfx->print(text.substring(i * max_chars, min((int)text.length(), (i + 1) * max_chars)));
* }
* }
*
* /* ===========================================================================
* دورة كاملة من "انظر وقل "
* ===========================================================================
*/
void lookAndTell(const char *prompt, const char *label) {
led(255, 120, 0); // الكهرمان: العمل
preview_on = false; // تتوقف الكاميرا هنا
gfx->fillRect(0, 0, 240, 240, BLACK);
gfx->setTextColor(YELLOW);
gfx->setTextSize(1);
gfx->setCursor(30, 110);
gfx->printf("capturing photo (%s)...", label);
size_t jpg_len = 0;
uint32_t t_cap = millis();
uint8_t *jpg = captureJpeg(&jpg_len);
t_cap = millis() - t_cap;
if (!jpg || jpg_len == 0) {
if (jpg) free(jpg);
showAnswer("Capture failed - tap CLEAR to retry.");
led(255, 0, 0);
drawClearSide();
return;
}
Serial.printf("Captured %u KB in %lu ms\n", (unsigned)(jpg_len / 1024), (unsigned long)t_cap);
gfx->setCursor(30, 124);
gfx->printf("asking OpenAI (%uKB)...", (unsigned)(jpg_len / 1024));
String answer;
uint32_t t_ai = millis();
bool ok = askVision(jpg, jpg_len, prompt, answer);
t_ai = millis() - t_ai;
free(jpg);
if (!ok) {
showAnswer("No answer - see serial monitor for the reason.");
led(255, 0, 0);
drawClearSide();
return;
}
Serial.printf("Vision (%lu ms): %s\n", (unsigned long)t_ai, answer.c_str());
showAnswer(answer);
drawClearSide(); // CLEAR يحل محل أزرار الطلب
#if SPEAK_REPLIES
led(0, 255, 40); // أخضر: يتحدث
speak(answer); // تبقى الإجابة على الشاشة
#endif
led(0, 0, 0);
/*
* الجواب يبقى الآن حتى يقوم المستخدم بالنقر على مسح
*/
void setup() {
Serial.begin(115200);
delay(400);
Serial.println("\n=== 05 Vision AI | Robojax.com ===");
pinMode(TFT_BLK, OUTPUT);
digitalWrite(TFT_BLK, LOW);
pinMode(SD_CS, OUTPUT);
digitalWrite(SD_CS, HIGH);
gfx->begin();
gfx->fillScreen(BLACK);
digitalWrite(TFT_BLK, HIGH);
bbct.init(TOUCH_SDA, TOUCH_SCL, TOUCH_RST, TOUCH_INT);
delay(50);
// يشارك SCCB الخاص بالكاميرا في هذا الحافلة، لذا يجب أن يكون السلك متصلًا قبل الكاميرا.
Wire.begin(I2C_SDA, I2C_SCL, 100000);
delay(20);
rgb.begin();
rgb.setBrightness(LED_BRIGHTNESS);
led(0, 0, 0);
#if SPEAK_REPLIES
spkInit();
#endif
gfx->setTextSize(1);
gfx->setTextColor(YELLOW);
gfx->setCursor(4, 4);
gfx->printf("Connecting to %s ...", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
uint32_t t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 20000) delay(300);
gfx->setCursor(4, 16);
if (WiFi.status() == WL_CONNECTED) {
gfx->setTextColor(GREEN);
gfx->print("WiFi ok");
} else {
gfx->setTextColor(RED);
gfx->print("WiFi FAILED (2.4GHz only! check secrets.h)");
}
gfx->setCursor(4, 28);
gfx->setTextColor(YELLOW);
gfx->print("starting camera...");
if (!camStartPreview()) {
gfx->fillScreen(RED);
gfx->setTextColor(WHITE);
gfx->setTextSize(2);
gfx->setCursor(20, 100);
gfx->print("CAMERA FAILED");
gfx->setTextSize(1);
gfx->setCursor(20, 130);
gfx->print("Press RESET (camera reset = board reset)");
while (1) delay(1000);
}
delay(400);
gfx->fillScreen(BLACK);
drawButtons();
preview_on = true;
Serial.println("Running. Touch a button to have the AI look through the camera.");
}
/*
* حلقة
*/
void loop() {
static uint32_t last_touch = 0;
if (preview_on) {
camera_fb_t *fb = esp_camera_fb_get();
if (fb) {
gfx->draw16bitBeRGBBitmap(0, 0, (uint16_t *)fb->buf, fb->width, fb->height);
esp_camera_fb_return(fb);
}
} else {
delay(20); // إجابة على الشاشة، في انتظار الوضوح
}
/*
* إشارة WiFi حية، تتجدد كل 2 ثانية في كلا الوضعين */
* /* static uint32_t last_wifi = 0;
* إذا (millis() - last_wifi > 2000) {
* last_wifi = millis();
* drawWifi();
* }
*
* /* لمسة مستشعرة للحواف: يحدث الإجراء فقط عند لمس جديد، ويجب أن ترفع الإصبع بالكامل (4 قراءات فارغة متتالية) قبل أن يمكن لأي شيء أن يحدث مرة أخرى. هذا هو ما يمنع النقر الواحد على CLEAR من تفعيل زر الالتقاط الذي يظهر في نفس المكان بعد لحظة.
*/
static bool touch_down = false;
static uint8_t release_count = 0;
uint16_t x, y;
if (getTouch(&x, &y)) {
release_count = 0;
if (!touch_down) {
touch_down = true; // نقرة جديدة - تنفيذها مرة واحدة فقط
if (!preview_on) {
الأشياء التي قد تحتاجها
-
آخرProduct page for MaTouch AI ESP32S3 2.8" TFT ST7789Vmakerfabs.com
الموارد والمراجع
-
توثيقMakerfabs MaTouch ESP32-S3 2.8" Camera and Touchscreen: User's Manualwiki.makerfabs.com
-
توثيق
-
توثيقProduct page for MaTouch AI ESP32S3 2.8" TFT ST7789Vmakerfabs.com
-
تحميلArduino GFX Library on GitHubgithub.com
ملفات📁
الملف المطلوب (.h)
ملفات أخرى
مخطط
-
MaTouch_AI 2.8" MaTouch AI ESP32S3 2.8" TFT ST7789V مخططأحدث لوحة MaTouch AI تدمج إدخال صوت I2S/مكبر صوت I2S/ كاميرا 3 ملايين بكسل OV3660/ شاشة بدقة 320*240، مع معالج ESP32S3 القوي وقدرة WiFi، لجعل هذه اللوحة أداة/منصة جيدة لتطوير الذكاء الاصطناعي مع ESP32.
MaTouch_AI 2.8“ SPI TFT ST7789V V1.1.PDF0.15 MB