یہ ٹیوٹوریل کا حصہ ہے: Makerfabs MaTouch AI ESP32S3 2.8" Camera
The latest MaTouch AI board integrate I2S voice input/I2S speaker/ 3 million camera OV3660/ 320*240 resolution display, with ESP32S3 strong processor& Wifi ability, to make this board a good tool/platform for AI development with ESP32.
Makerfabs MaTouch ESP32-S3 2.8" کیمرہ ESP32-S3 AI ویژن: کیمرہ اشارہ کریں اور پوچھیں کہ یہ کیا دیکھتا ہے
بورڈ تصویر لیتا ہے، ایک AI دیکھتا ہے کہ اسے کیا نظر آتا ہے، اور وہ جواب بلند آواز میں کہتا ہے
کیمرے کو کسی چیز کی طرف کریں، ایک بٹن دبائیں، اور ایک AI آپ کو بتاتا ہے کہ وہ کیا دیکھ رہا ہے - اسکرین پر چھپا ہوا اور اسپیکر کے ذریعے بولا ہوا۔ دو طریقے: منظر میں موجود اشیاء کے نام بتائیں، یا کیمرے کے سامنے موجود شخص کو بیان کریں۔
AI ویژن - پوائنٹ اٹ ایٹ سم تھنگ جو MaTouch AI ESP32-S3 بورڈ پر چل رہا ہے
تصویر وہاں کیسے پہنچتی ہے
کیمرہ تقریباً 20-40 KB کا 800×600 JPEG تیار کرتا ہے۔ بورڈ اسے PSRAM میں بیس64-انکوڈ کرتا ہے، جو اسے بطور متن تقریباً ایک تہائی بڑا بنا دیتا ہے، اور اسے درخواست میں ڈیٹا URI کے طور پر شامل کرتا ہے۔ 8 MB کی PSRAM ہی وہ چیز ہے جو اسے آرام دہ بناتی ہے۔
تفصیل، شناخت کبھی نہیں
پرسن موڈ بیان کرتا ہے کہ یہ کیا دیکھ سکتا ہے - مزاج، چشمے، کوئی شخص کیا کر رہا ہے۔ یہ آپ کو نہیں بتائے گا کہ کوئی کون ہے، اور یہ جان بوجھ کر ہے۔ چہرے سے لوگوں کی شناخت کرنا OpenAI کی استعمال کی پالیسیوں کے خلاف ہے، اور Microsoft کی مساوی چہرے کی شناخت کی سروس ایک منظوری کے عمل کے پیچھے بند ہے جس کے لیے عام ڈویلپر آسانی سے سائن اپ نہیں کر سکتے۔ کوئی بھی ٹیوٹوریل جو آپ سے کلاؤڈ چہرے کی شناخت کا وعدہ کرتا ہے وہ کسی ایسی چیز کو بیان کر رہا ہے جو عام طور پر دستیاب نہیں ہے۔
اگر آپ ایک ایسا بورڈ چاہتے ہیں جو مخصوص لوگوں کو پہچانتا ہو، تو پروجیکٹ 03b استعمال کریں - یہ اسے آف لائن کرتا ہے اور یہ کبھی کسی کے چہرے کو کہیں نہیں بھیجتا۔
نیٹ ورک کے لیے کیمرے کو رکنا پڑتا ہے
سوال پوچھے جانے کے دوران لائیو ویو فائنڈر بند کر دیا جاتا ہے۔ یہ صرف ظاہری نہیں ہے: چلنے والا کیمرہ ڈرائیور اندرونی میموری رکھتا ہے جس کی ایک محفوظ کنکشن کو ضرورت ہوتی ہے، اور پیش نظارہ چلنے کے ساتھ کنکشن صرف ناکام ہو جاتا ہے۔ کیمرے کو بند کرنے سے یہ آزاد ہو جاتی ہے۔ اس کا مطلب یہ بھی ہے کہ جواب پڑھنے کے قابل رہتا ہے بجائے اس کے کہ اسے 25 بار فی سیکنڈ دوبارہ پینٹ کیا جائے۔
جواب اس وقت تک رہتا ہے جب تک آپ اسے ختم نہ کریں
جواب اسکرین پر اس وقت تک رہتا ہے جب تک آپ CLEAR کو نہ دبائیں - کوئی ٹائمر نہیں ہے۔ جب یہ دکھا رہا ہوتا ہے، ایک REPLAY بٹن بولا ہوا جواب میموری سے دوبارہ چلاتا ہے، بغیر کسی دوسری API کال اور بغیر کسی اضافی لاگت کے۔
MaTouch AI ESP32-S3 2.8" بورڈ کے بارے میں
اس صفحے کا ہر پروجیکٹ Makerfabs کے MaTouch AI ESP32-S3 2.8" TFT ST7789V پر چلتا ہے۔ یہ ایک آل ان ون بورڈ ہے: ایک رنگین ٹچ اسکرین، ایک 3 میگا پکسل کیمرہ، دو مائیکروفون اور ایک حقیقی اسپیکر ایمپلیفائر، سب کچھ 8 MB PSRAM کے ساتھ ESP32-S3 کے ذریعے چلایا جاتا ہے۔ یہ مجموعہ ہی ان AI پروجیکٹس کو ایک ہی بورڈ پر ممکن بناتا ہے جس میں کچھ اور منسلک نہ ہو۔
8 MB کی PSRAM یہاں کسی بھی دوسرے نمبر سے زیادہ اہم ہے۔ یہ وہی ہے جو بورڈ کو ایک کیمرہ فریم، چند سیکنڈ کی ریکارڈ شدہ آڈیو، یا بیس64-انکوڈ شدہ تصویر کو ایک ہی وقت میں میموری میں رکھنے دیتا ہے - ان میں سے کوئی بھی ESP32 کی عام RAM میں فٹ نہیں ہوتا۔
مینوفیکچرر دستاویزات: Makerfabs وکی صفحہ۔
اہم خصوصیات
پروسیسر: ESP32-S3، ڈوئل کور 240 MHz، WiFi 2.4 GHz + Bluetooth 5.0
میموری: 16 MB فلیش، 8 MB PSRAM (یہاں تقریباً ہر پروجیکٹ کے لیے ضروری)
ڈسپلے: 2.8" IPS، 320×240، ST7789V ڈرائیور، SPI
ٹچ: GT911 کیپسیٹو، ایک ساتھ 5 انگلیوں کو ٹریک کرتا ہے
کیمرہ: OV3660، 3 میگا پکسل، 2048×1536 تک
مائیکروفون: دو INMP441 I2S ڈیجیٹل مائکس (ایک حقیقی سٹیریو جوڑا)
اسپیکر: MAX98357A کلاس-D ایمپلیفائر، 4 Ω میں 3.2 W
اسٹوریج: مائیکرو ایس ڈی کارڈ سلاٹ (SPI موڈ)
پاور: USB-C، JST بیٹری کنیکٹر، TP4056 چارجر، پاور سوئچ
بورڈ پر بھی: WS2812B RGB LED، PCF8563T بیٹری بیکڈ ریئل ٹائم کلاک، اور ایک MAX17048 بیٹری فیول گیج جو سرکاری خصوصیات میں درج نہیں ہے
دو USB-C پورٹس ایک جیسے نہیں ہیں۔ بورڈ کا اسپیکر اپنے سگنل پن (IO19 اور IO20) کو نیٹیو USB پورٹ کے ساتھ شیئر کرتا ہے، کیونکہ وہ پن ESP32-S3 کی ہارڈ وائرڈ USB ڈیٹا لائنیں ہیں۔ ہمیشہ CH340K USB-C پورٹ (وہ جو 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 |
پورٹ | CH340K USB-C پورٹ |
ESP32 کور 2.0.17 استعمال کریں، 3.x نہیں۔ Espressif نے کور 3 میں ڈیوائس پر چہرے کی شناخت کے ماڈلز ہٹا دیے ہیں، لہذا چہرے کے پروجیکٹ وہاں کمپائل نہیں ہوں گے۔ 2.0.17 کو پن کرنے سے اس صفحے کا ہر پروجیکٹ ایک کنفیگریشن کے ساتھ کام کرتا رہتا ہے۔ بورڈز مینیجر میں، ورژن ڈراپ ڈاؤن آپ کو جب چاہیں آگے پیچھے سوئچ کرنے دیتا ہے۔
Arduino کے لیے GFX لائبریری ورژن 1.5.6 استعمال کریں، 1.6.x نہیں۔ 1.6 ریلیزز ESP32 کور 3 کے لیے بنائی گئی ہیں اور کور 2.0.17 پر شروع ہوتے وقت رک سکتی ہیں۔ اگر اپ لوڈ کرنے کے بعد آپ کی اسکرین سیاہ رہتی ہے، تو یہ سب سے پہلی چیز ہے جسے جانچنا چاہیے۔
مطلوبہ لائبریریاں
انہیں Arduino IDE میں Tools → Manage Libraries کے ذریعے انسٹال کریں۔ ورژن نمبر اہم ہیں - براہ کرم درج کردہ ورژن استعمال کریں۔
لائبریری | ورژن | مصنف |
|---|---|---|
GFX لائبریری برائے 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 GHz ہونا ضروری ہے۔ ESP32-S3 5 GHz نیٹ ورک بالکل نہیں دیکھ سکتا۔ اگر آپ کا راؤٹر دونوں بینڈز کو ایک نام کے تحت یکجا کرتا ہے (Asus اسے Smart Connect کہتا ہے)، تو یا تو اسے بند کریں یا 2.4 GHz بینڈ کو اپنا الگ نام دیں اور اسے secrets.h میں استعمال کریں۔
اپنی API کلید حاصل کرنا
یہ پروجیکٹ کلاؤڈ AI سروس سے بات کرتا ہے، اس لیے آپ کو اپنی کلید کی ضرورت ہے۔ اگر آپ نے پہلے کبھی ایسا نہیں کیا، تو پریشان نہ ہوں - یہ ایک پاس ورڈ کی طرح ہے جو آپ کے اکاؤنٹ کو سروس سے پہچانتا ہے۔ اس میں ایک بار چند منٹ لگتے ہیں۔
کلید ویب سائٹ کی سبسکرپشن نہیں ہے۔ مثال کے طور پر، 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 تلاش کریں، اور Microsoft کی شائع کردہ Speech service منتخب کریں۔
فارم بھریں: کوئی بھی ریسورس گروپ، کوئی بھی نام، اور اپنے قریب ایک 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 مفت آزمائش آپ کی شناخت کی تصدیق کے لیے کارڈ مانگتا ہے۔ یہ آپ سے چارج نہیں کرتا۔ آپ کو 30 دن کے لیے $200 کریڈٹ ملتا ہے، اور اس کے بعد اکاؤنٹ Pay-As-You-Go پر منتقل ہو جاتا ہے - لیکن F0 Speech درجہ مفت رہتا ہے، مہینے بہ مہینے، اور ان پروجیکٹس کی ہر چیز اس میں آرام سے فٹ ہوتی ہے۔ اگر آپ بالکل کارڈ نہیں دینا چاہتے اور آپ طالب علم ہیں، تو Azure for Students آپشن آپ کو بغیر کارڈ کے کریڈٹ دیتا ہے۔
یہ "Speech service" ریسورس ہونا ضروری ہے۔ Translator، Language، یا عام Cognitive Services ریسورس کی کلید بالکل ایک جیسی نظر آتی ہے اور مکمل طور پر درست ہوتی ہے - لیکن ہر تقریر کی درخواست پر 401 ایرر آتا ہے۔ یہ ہمیں جانچ کے دوران ملا اور ایک گھنٹہ ضائع کیا۔ اگر تقریر 401 کے ساتھ ناکام ہوتی ہے جبکہ کلید درست نظر آتی ہے، تو چیک کریں کہ آپ نے کس قسم کا ریسورس بنایا ہے۔
چلانے کی لاگت کیا ہے
بہت کم، لیکن یہ مفت نہیں ہے، اور آپ کو تقریباً معلوم ہونا چاہیے کہ پروجیکٹ چھوڑنے سے پہلے آپ کتنا خرچ کر رہے ہیں۔
خدمت | تقریبی لاگت |
|---|---|
Azure اسپیچ | مفت درجہ تقریباً 5 گھنٹے سننے اور 0.5 ملین حروف بولنے ماہانہ کا احاطہ کرتا ہے |
DeepSeek | ہر جواب کا ایک فیصد کا ایک حصہ - چند ڈالرز میں ہزاروں جوابات |
OpenAI ویژن | ہر تصویر پر تقریباً ایک یا دو سینٹ، ماڈل کے لحاظ سے |
قیمتیں بدلتی رہتی ہیں، لہٰذا انہیں حتمی نرخ کے بجائے رہنما کے طور پر لیں۔ ان میں سے ہر خدمت کا ایک استعمال کا صفحہ ہے جہاں آپ اپنے خرچ کو دیکھ سکتے ہیں، اور سب میں آپ خرچ کی حد مقرر کر سکتے ہیں - جو پہلے دن کرنا قابلِ قدر ہے۔
اپنی چابیاں نجی رکھیں۔ جس کے پاس بھی یہ ہوں، وہ آپ کے پیسے خرچ کر سکتا ہے۔ انہیں ویڈیو، اسکرین شاٹ، فورم پوسٹ یا عوامی کوڈ ریپوزٹری میں نہ ڈالیں۔ اگر کوئی چابی کبھی ظاہر ہو جائے، تو اسے فراہم کنندہ کی ویب سائٹ پر حذف کر کے نئی بنائیں - اس میں سیکنڈ لگتے ہیں، اور یہی واحد حقیقی حل ہے۔
مسائل کا حل
علامت | وجہ اور حل |
|---|---|
اسکرین سیاہ رہتی ہے | غلط GFX لائبریری ورژن (1.5.6 استعمال کریں) یا غلط بورڈ سیٹنگز۔ |
|
|
کچھ اپ لوڈ نہیں ہوتا / کوئی COM پورٹ نہیں | غلط USB-C پورٹ، یا CH340 ڈرائیور انسٹال نہیں ہے۔ |
کیمرہ ناکام ہو جاتا ہے اور کبھی بحال نہیں ہوتا | کیمرے کی ری سیٹ لائن بورڈ کے RESET بٹن سے منسلک ہے، لہٰذا سافٹ ویئر اسے دوبارہ شروع نہیں کر سکتا۔ RESET دبائیں۔ اگر پھر بھی ناکام ہو، تو کیمرے کی ربن کیبل کو دوبارہ لگائیں۔ |
کوڈ ڈاؤن لوڈ کریں
اس پروجیکٹ کے لیے مکمل Arduino اسکیچ، pins.h اور باقی تمام ضروری چیزیں، مفت ڈاؤن لوڈ کے لیے دستیاب ہیں۔
اسے ان زپ کریں، Arduino IDE میں .ino فائل کھولیں، اوپر دی گئی سیٹنگز چیک کریں، اور CH340K USB-C پورٹ کے ذریعے اپ لوڈ کریں۔
یہ ٹیوٹوریل کا حصہ ہے: Makerfabs MaTouch AI ESP32S3 2.8" Camera
/*
* ===========================================================================
* 05_Vision_AI — MaTouch AI ESP32-S3 2.8" TFT ST7789V
* ===========================================================================
*
----------
* ROBOJAX.COM - MaTouch AI ESP32-S3 2.8" project series
*
* WATCH THE VIDEO
* https://youtu.be/6AL3g3tC_Hk
*
* WRITTEN TUTORIALS - every project, with photos and full explanation
* Camera and touchscreen.... https://robojax.com/RTJ849
* Offline face recognition.. https://robojax.com/RTJ850
* AI voice assistant........ https://robojax.com/RTJ851
* AI vision................. https://robojax.com/RTJ852
*
* GET THE BOARD - SAVE $5 with coupon code: Robojax_Makerfab
* https://www.makerfabs.com/matouch-ai-esp32s3-2-8-tft-st7789v.html
* (enter the code at checkout)
*
* All of this code is free. If it helped you, a subscribe on YouTube is
* the best way to support more of it.
*
*
* Watching the video first will save you time - it shows the Arduino IDE
* settings and the library versions being set up step by step.
* ---------------------------------------------------------------------------
*
* The board SEES. Point the camera at something, touch a button, and an
* OpenAI vision model tells you what it is looking at - drawn on the screen
* and (optionally) spoken out of the board's own speaker via Azure TTS.
*
* Two modes, two buttons:
*
* OBJECTS "What do you see?" - names the things in front of the lens.
* PERSON Describes the person in frame: glasses, expression, what
* they are doing. DESCRIPTION ONLY - it will not and must not
* try to say WHO someone is. Identifying people by face is
* against OpenAI's usage policies, and it is the right call:
* say this in the video, it is worth 15 honest seconds.
*
* WHY OPENAI FOR THIS DEMO AND NOT DEEPSEEK: DeepSeek's API is text-only.
* It cannot accept an image at all. Azure OpenAI could do it, but plain
* OpenAI is one endpoint with no deployment setup - simplest to follow.
*
* HOW THE IMAGE TRAVELS: the OV3660 gives us a JPEG directly (800x600,
* ~40 KB). We base64-encode it in PSRAM (~55 KB of text) and embed it in
* the JSON request as a data: URI. The 8 MB PSRAM makes this trivial.
*
* ---------------------------------------------------------------------------
* FILL IN secrets.h BEFORE FLASHING
* (needs WIFI_*, OPENAI_*; AZURE_* only if SPEAK_REPLIES is 1).
*
* BOARD SETTINGS (Tools menu - EVERY line matters, wrong = black screen
* or compile errors. These reset when you switch cores - recheck them!)
*
* Board : ESP32S3 Dev Module
* ESP32 core : 2.0.17
* PSRAM : OPI PSRAM <-- required, image buffers live there
* Flash Size : 16MB (128Mb)
* Partition Scheme : 16M Flash (3MB APP/9.9MB FATFS)
* USB CDC On Boot : Disabled <-- speaker shares pins with native USB
* Upload Speed : 921600
* Port : the CH340K USB-C port (the one near RESET)
*
* LIBRARIES
* GFX Library for Arduino v1.5.6 (NOT 1.6.x - that pairs with core 3)
* bb_captouch v1.3.1
* ArduinoJson v7.x
* Adafruit NeoPixel any recent
*
* ---------------------------------------------------------------------------
* FUNCTIONS IN THIS SKETCH
* led(r,g,b) set the RGB status LED colour
* getTouch(&x,&y) read the touch panel, mapped to screen coordinates
* camStart(...) start the camera in a given format/size
* camStartPreview() start the 240x240 RGB565 live-view camera
* captureJpeg(&len) take one 800x600 JPEG into PSRAM (camera stays OFF
* afterwards - caller restarts the preview)
* readHttpResponse() read an HTTPS reply, de-chunking it properly
* askVision(...) send photo + prompt to OpenAI, return the answer
* spkInit() configure the I2S speaker output
* readExact(...) read exactly N bytes from a TLS connection
* speak(text) Azure TTS -> download voice to PSRAM -> play it
* playLastAnswer() replay the kept voice from PSRAM (REPLAY button)
* drawButton(...) draw one side-column button
* drawWifi() WiFi signal bars + dBm readout
* drawButtons() normal side column (the two ask buttons)
* drawClearSide() answer-mode side column (CLEAR + REPLAY)
* showAnswer(text) word-wrapped answer overlay on the viewfinder
* lookAndTell(...) one full cycle: capture -> ask -> show -> speak
* setup() / loop() boot sequence / viewfinder + touch handling
*
* Robojax.com
* ===========================================================================
*/
#define SPEAK_REPLIES 1 // 1 = read the answer aloud with Azure TTS, 0 = screen only
/* Status LED brightness, 0-255. The WS2812 runs from the power rail and is
* uncomfortably bright at full power - 25 is plenty visible on camera. */
#define LED_BRIGHTNESS 15
/* CAMERA ORIENTATION
* 1 = camera faces the SAME way as the screen (how the board ships) - the
* image is mirrored so it looks natural when you point it at yourself.
* 0 = you folded the ribbon so the lens faces AWAY from the screen
* (phone-style, screen to you / camera to the subject).
* If the picture looks left-right reversed, flip this number. */
#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);
/* --- the two vision prompts ------------------------------------------------
* Keep answers short: they must fit a 320x240 screen and, if spoken, must not
* leave the presenter waiting awkwardly on camera. */
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.";
/* --- layout: viewfinder left, buttons right ------------------------------- */
#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(); }
/* ===========================================================================
* Touch
* =========================================================================== */
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;
}
/* ===========================================================================
* Camera — RGB565 for the live view; JPEG capture happens by restarting
* the driver, same technique as sketch 02.
* =========================================================================== */
bool mirrored = CAMERA_FACES_USER; // see the define at the top of the file
/* The viewfinder is OFF while a cloud call runs and while the answer is on
* screen. Two reasons, both learned on the bench:
* 1. RAM: the live camera driver eats the internal memory a TLS handshake
* needs - with the preview running, connecting to OpenAI fails (HTTP -1).
* 2. Readability: the viewfinder repaints 25x/s and would wipe the answer.
* The answer STAYS on screen until the user taps CLEAR - no timer. */
bool preview_on = false;
/* The last spoken answer is KEPT in PSRAM so the REPLAY button can play it
* again without another cloud call. Overwritten by the next answer. */
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;
/* Share the I2C bus Wire already drives (the touch panel lives there too)
* instead of letting the camera install a second driver on the same pins -
* that kills touch. Wire.begin() must run before this function. */
c.pin_sccb_sda = -1;
c.pin_sccb_scl = -1;
c.sccb_i2c_port = 0; // Wire = I2C port 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); }
/* Capture one SVGA JPEG into a PSRAM buffer the caller owns. */
uint8_t *captureJpeg(size_t *len_out) {
esp_camera_deinit();
delay(120);
if (!camStart(PIXFORMAT_JPEG, FRAMESIZE_SVGA, 1, 12)) { *len_out = 0; return nullptr; }
// a few warm-up frames so exposure settles
for (int i = 0; i < 3; i++) {
camera_fb_t *w = esp_camera_fb_get();
if (w) esp_camera_fb_return(w);
delay(100);
}
uint8_t *copy = nullptr;
*len_out = 0;
camera_fb_t *fb = esp_camera_fb_get();
if (fb && fb->len > 0) {
copy = (uint8_t *)ps_malloc(fb->len);
if (copy) { memcpy(copy, fb->buf, fb->len); *len_out = fb->len; }
}
if (fb) esp_camera_fb_return(fb);
/* Deliberately leave the camera OFF here. The caller restarts the preview
* after the cloud call - a running camera driver starves the TLS handshake
* of internal RAM (that was the "Vision HTTP -1" bug). */
esp_camera_deinit();
delay(120);
return copy;
}
/* ===========================================================================
* HTTP response reader — shared by the cloud calls below.
* Returns the status code and fills body_out. Handles chunked transfer
* encoding PROPERLY: the chunk-size markers must be stripped, or they end
* up embedded inside the JSON and the parse fails. (OpenAI pretty-prints
* its replies so they span several chunks - this bit us on the bench.)
* =========================================================================== */
/* Block until the connection has data (or the budget runs out). Every read
* below goes through this, because the server can go quiet for many seconds
* while it analyses the image - and a bare read() would simply time out. */
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; // blank line = end of headers
h.toLowerCase();
if (h.startsWith("transfer-encoding:") && h.indexOf("chunked") >= 0) chunked = true;
}
if (chunked) {
int blanks = 0;
while (true) {
/* Waiting here - instead of letting read() time out - is essential: a
* timed-out read looks exactly like "0" (final chunk), which silently
* truncates the body to nothing while the server is still thinking. */
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; // genuine final chunk
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 vision call
* The request body is built by hand in PSRAM rather than through ArduinoJson,
* because embedding a 55 KB base64 string in a JSON document would mean
* holding two copies. Base64 text never needs JSON escaping, so this is safe.
* =========================================================================== */
bool askVision(const uint8_t *jpg, size_t jpg_len, const char *prompt, String &answer_out) {
// --- base64 encode the image into 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;
}
// --- assemble the JSON request around it ---
/* Current OpenAI models reject the old "max_tokens" name - it must be
* "max_completion_tokens" (verified against the live API, July 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);
// --- send it: manual HTTP, chunk-wise upload (proven pattern from 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;
}
/* read the reply with proper de-chunking */
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 parse failed, %u bytes received\n", resp.length());
}
return ok;
}
/* ===========================================================================
* Azure TTS — same streaming trick as sketch 04: raw PCM into 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);
}
/* Read exactly n bytes from a client (or until timeout). */
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(); // defined below; explicit prototype for the 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>";
/* Manual HTTP with proper de-chunking: Azure sends this audio chunked, and
* HTTPClient's raw stream leaks the ASCII chunk-size lines into the PCM -
* each one plays as an audible KNOCK. */
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) {
/* keep this answer for the REPLAY button (replacing the previous one),
* then play it */
if (last_audio) free(last_audio);
last_audio = audio;
last_audio_len = alen;
playLastAnswer();
} else {
free(audio);
}
}
/* Play the kept answer from PSRAM - used right after download AND by REPLAY. */
void playLastAnswer() {
if (!last_audio || last_audio_len <= WAV_HEADER_LEN) return;
static const uint8_t lead_in[640] = {0}; // 20 ms silence pre-roll
size_t 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);
delay(150);
i2s_zero_dma_buffer(I2S_SPK_PORT);
}
#endif // SPEAK_REPLIES
/* ===========================================================================
* UI
* =========================================================================== */
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 bars + dBm at the bottom of the side column, refreshed from the loop */
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 signal");
}
/* normal side column: the two ask buttons */
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();
}
/* answer-mode side column: CLEAR (back to camera) + REPLAY (say it again).
* The answer stays on screen until CLEAR is tapped. */
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();
}
/* Word-wrapped answer overlay across the bottom of the viewfinder. */
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)));
}
}
/* ===========================================================================
* One full "look and tell" cycle
* =========================================================================== */
void lookAndTell(const char *prompt, const char *label) {
led(255, 120, 0); // amber: working
preview_on = false; // camera goes OFF here
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 replaces the ask buttons
#if SPEAK_REPLIES
led(0, 255, 40); // green: speaking
speak(answer); // answer stays on screen
#endif
led(0, 0, 0);
/* the answer now stays until the user taps CLEAR */
}
/* ===========================================================================
* SETUP
* =========================================================================== */
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);
// The camera's SCCB shares this bus, so Wire must be up before the camera
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.");
}
/* ===========================================================================
* LOOP
* =========================================================================== */
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); // answer on screen, waiting for CLEAR
}
/* live WiFi signal, refreshed every 2 s in both modes */
static uint32_t last_wifi = 0;
if (millis() - last_wifi > 2000) {
last_wifi = millis();
drawWifi();
}
/* Edge-detected touch: an action fires only on a NEW finger-down, and the
* finger must fully lift (4 consecutive empty reads) before anything can
* fire again. This is what stops one tap on CLEAR from also triggering the
* capture button that appears in the same spot a moment later. */
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; // new tap - act exactly once
if (!preview_on) {
/* answer mode: CLEAR returns to the camera, REPLAY says it again */
if (x >= BTN_X && y >= BTN_OBJ_Y && y < BTN_OBJ_Y + BTN_H) {
if (camStartPreview()) {
preview_on = true;
drawButtons();
} else {
showAnswer("Camera restart failed - press RESET.");
}
}
#if SPEAK_REPLIES
else if (x >= BTN_X && y >= BTN_PERSON_Y && y < BTN_PERSON_Y + BTN_H) {
led(0, 255, 40); // green while speaking
playLastAnswer();
led(0, 0, 0);
}
#endif
} else if (x >= BTN_X && y >= BTN_OBJ_Y && y < BTN_OBJ_Y + BTN_H) {
lookAndTell(PROMPT_OBJECTS, "objects");
} else if (x >= BTN_X && y >= BTN_PERSON_Y && y < BTN_PERSON_Y + BTN_H) {
lookAndTell(PROMPT_PERSON, "person");
}
}
} else if (touch_down) {
if (++release_count >= 4) { touch_down = false; release_count = 0; }
}
(void)last_touch;
}
آپ کو جن چیزوں کی ضرورت ہو سکتی ہے
-
ایمیزون
-
دیگر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 schematicThe latest MaTouch AI board integrate I2S voice input/I2S speaker/ 3 million camera OV3660/ 320*240 resolution display, with ESP32S3 strong processor& Wifi ability, to make this board a good tool/platform for AI development with ESP32.
MaTouch_AI 2.8“ SPI TFT ST7789V V1.1.PDF0.15 MB