هذا الدليل جزء من: كاميرا 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 - بدون إنترنت، بدون مفتاح API
شبكة عصبية تعمل على الشريحة - بدون إنترنت، بدون حساب، بدون اشتراك
كشف الوجوه يعمل بالكامل على ESP32-S3. مربعات خضراء حول الوجوه، ونقاط حمراء على العينين والأنف والفم، وعداد معدل إطارات مباشر. افصل جهاز التوجيه الخاص بك وسيستمر في العمل، لأن الشبكة العصبية موجودة في ذاكرة الفلاش الخاصة بالشريحة وتعمل على تعليماتها المتجهية.
لا توجد خدمة سحابية هنا، ولا مفتاح API ولا شيء للتسجيل فيه. لا شيء يغادر الغرفة.
كشف الوجوه دون اتصال يعمل على لوحة MaTouch AI ESP32-S3
كيف يعمل
يستخدم هذا نماذج esp-dl من Espressif: كاشف من مرحلتين حيث يقترح human_face_detect_msr01 الوجوه المرشحة ويؤكدها human_face_detect_mnp01 ويجد النقاط الرئيسية الخمس للوجه. كلا النموذجين يأتيان داخل ESP32 Arduino core 2.x - لا توجد مكتبة لتثبيتها.
هذا هو السبب في أن كل مشروع على هذا الموقع يثبت ESP32 core 2.0.17. تمت إزالة نماذج الوجه في core 3.x، ولن يتم تجميع هذا الكود هناك - ستحصل على human_face_detect_msr01.hpp: No such file or directory.
الصوت وطرف إخراج
تصدر اللوحة صوتًا عبر مكبر الصوت الخاص بها عند ظهور وجه، وتجعل IO15 مرتفعًا بينما يكون الوجه في الإطار - بحيث يمكنها تشغيل مرحل أو مصباح أو قفل. اضبط ENABLE_BEEP أو ACTION_PIN في أعلى الكود لإيقاف أي منهما.
الكاميرا ولوحة اللمس تشتركان في ناقل
تستخدم واجهة التحكم في الكاميرا نفس سلكي I2C مثل لوحة اللمس والساعة ومقياس الوقود. افتراضيًا، يثبت برنامج تشغيل الكاميرا برنامج تشغيل I2C الخاص به على تلك الأطراف وتتوقف لوحة اللمس عن الاستجابة. الإصلاح هو ثلاثة أسطر في إعداد الكاميرا:
config.pin_sccb_sda = -1; // مشاركة الناقل الذي يقوده Wire بالفعل
config.pin_sccb_scl = -1;
config.sccb_i2c_port = 0; // Wire هو منفذ I2C 0مع استدعاء Wire.begin(39, 38) قبل esp_camera_init(). لا يوجد مثال منشور لهذه اللوحة يفعل ذلك، لأن أيا منها لا يقرأ لوحة اللمس بعد بدء تشغيل الكاميرا.
قيود صريحة
يعمل الكاشف على إطار 240×240، لذلك يتم تفويت الوجوه الصغيرة على مسافة.
الكشف ليس تمييزًا - هذا الكود يجد الوجوه، ولا يعرف لمن هي. لذلك، انظر إلى المشروع 03b.
لا يوجد حماية من الانتحال. صورة فوتوغرافية لوجه لا تزال وجهًا.
حول لوحة MaTouch AI ESP32-S3 2.8"
كل مشروع في هذه الصفحة يعمل على MaTouch AI ESP32-S3 2.8" TFT ST7789V من Makerfabs. إنها لوحة متكاملة: شاشة لمس ملونة، كاميرا بدقة 3 ميجابكسل، ميكروفونان ومضخم صوت حقيقي، كلها مدفوعة بواسطة ESP32-S3 مع 8 MB من PSRAM. هذا المزيج هو ما يجعل مشاريع الذكاء الاصطناعي هذه ممكنة على لوحة واحدة دون أي شيء آخر متصل.
ذاكرة PSRAM بسعة 8 MB أكثر أهمية من أي رقم آخر هنا. إنها ما يسمح للوحة باحتواء إطار كاميرا، أو بضع ثوانٍ من الصوت المسجل، أو صورة مشفرة base64 في الذاكرة في نفس الوقت - لا شيء منها يناسب ذاكرة ESP32 العادية.
وثائق الشركة المصنعة: صفحة Makerfabs wiki.
المواصفات الرئيسية
المعالج: 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 (زوج استريو حقيقي)
مكبر الصوت: مضخم فئة D MAX98357A، 3.2 W في 4 Ω
التخزين: فتحة بطاقة microSD (وضع SPI)
الطاقة: USB-C، موصل بطارية JST، شاحن TP4056، مفتاح طاقة
أيضًا على اللوحة: WS2812B RGB LED، ساعة زمنية حقيقية PCF8563T مدعومة بالبطارية، ومقياس وقود بطارية MAX17048 غير مدرج في المواصفات الرسمية
منفذا USB-C ليسا متماثلين. يشارك مكبر صوت اللوحة أطراف الإشارة الخاصة به (IO19 وIO20) مع منفذ USB الأصلي، لأن تلك الأطراف هي خطوط بيانات USB الثابتة في ESP32-S3. قم دائمًا بالرفع والطاقة من خلال منفذ USB-C CH340K (الموجود بجانب زر RESET)، واضبط USB CDC On Boot على Disabled. استخدم المنفذ الخاطئ وسيتعطل الصوت أو ستفشل عمليات الرفع.
إعدادات Arduino IDE
هذه الإعدادات مهمة. معظم المشكلات التي يبلغ عنها الأشخاص مع هذه اللوحة هي أحد هذه الإعدادات خاطئًا، وهي تتم إعادة تعيينها عند تغيير إصدار core، لذا تحقق منها مرة أخرى بعد أي تغيير.
الإعداد | القيمة |
|---|---|
اللوحة | وحدة تطوير ESP32S3 |
إصدار نواة ESP32 | 2.0.17 |
PSRAM | OPI PSRAM |
حجم الفلاش | 16 ميجابايت (128 ميجابت) |
مخطط التقسيم | فلاش 16 ميجا (تطبيق 3 ميجا/نظام ملفات FATFS 9.9 ميجا) |
USB CDC عند التشغيل | معطل |
سرعة الرفع | 921600 |
مسح كل الفلاش قبل الرفع | معطل |
المنفذ | منفذ USB-C الخاص بـ CH340K |
استخدم نواة ESP32 الإصدار 2.0.17، وليس 3.x. أزالت Espressif نماذج اكتشاف الوجوه على الجهاز في النواة 3، لذا لن يتم تجميع مشاريع الوجه هناك. تثبيت الإصدار 2.0.17 يضمن عمل كل مشروع في هذه الصفحة بإعداد واحد. في مدير اللوحات، تتيح لك القائمة المنسدلة للإصدارات التبديل ذهابًا وإيابًا متى شئت.
استخدم مكتبة GFX للأردوينو الإصدار 1.5.6، وليس 1.6.x. إصدارات 1.6 مبنية لنواة ESP32 3 ويمكن أن تعلق عند بدء التشغيل على النواة 2.0.17. إذا بقيت شاشتك سوداء بعد الرفع، فهذا أول شيء يجب التحقق منه.
المكتبات المطلوبة
قم بتثبيتها من خلال أدوات ← إدارة المكتبات في بيئة الأردوينو. أرقام الإصدارات مهمة - يرجى استخدام تلك المدرجة.
المكتبة | الإصدار | المؤلف |
|---|---|---|
مكتبة GFX للأردوينو | 1.5.6 | moononournation |
bb_captouch | 1.3.1 | Larry Bank |
نماذج اكتشاف الوجوه ليست مكتبة - إنها مدمجة في نواة ESP32 2.0.17 نفسها، لذا لا يوجد شيء إضافي لتثبيته.
استكشاف الأخطاء وإصلاحها
العرض | السبب والحل |
|---|---|
تبقى الشاشة سوداء | إصدار خاطئ لمكتبة GFX (استخدم 1.5.6) أو إعدادات لوحة خاطئة. |
|
|
لا شيء يُرفع / لا يوجد منفذ COM | منفذ USB-C خاطئ، أو برنامج تشغيل CH340 غير مثبت. |
تفشل الكاميرا ولا تتعافى أبدًا | خط إعادة تعيين الكاميرا مرتبط بزر إعادة تعيين اللوحة، لذا لا يمكن للبرنامج إعادة تشغيلها. اضغط على زر إعادة التعيين. إذا استمر الفشل، أعد تركيب كابل الكاميرا الشريطي. |
تنزيل الكود
الرسم الكامل للأردوينو لهذا المشروع، مع pins.h وكل ما يحتاجه، متاح للتنزيل مجانًا.
فك ضغطه، افتح ملف .ino في بيئة الأردوينو، تحقق من الإعدادات أعلاه، وارفعه عبر منفذ 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 (Azure + DeepSeek)
- Makerfabs MaTouch ESP32-S3 2.8 بوصة كاميرا ESP32-S3 رؤية ذكاء اصطناعي: وجّه الكاميرا واسأل عمّا تراه
/*
* ===========================================================================
* 03b_Face_Enroll — 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.
* ---------------------------------------------------------------------------
*
* Guided, named face enrollment - fully offline. This is 03_Face_Offline
* grown up: instead of a bare ENROLL button, you manage a roster of names
* over the serial monitor, then enroll each person with an on-screen guide,
* a countdown, and multi-sample capture.
*
* HOW TO USE
* ----------
* 1. Open the Serial Monitor at 115200 and set up your roster ONCE:
* name 0 Ahmad
* name 1 Sara
* list
* Names are stored in flash (NVS) - they survive reboots.
*
* 2. On the board: tap ENROLL. The top-left shows "Enroll: Ahmad".
* Tap again to cycle to the next name. Cycling past the last name
* cancels enrollment.
*
* 3. The chosen person steps in front of the camera, close enough that
* their face fills the guide box. A 3-2-1 countdown runs, three samples
* are captured, and a green "enrolled" banner confirms. Enrollment then
* disarms itself.
*
* 4. From now on the board greets that face BY NAME on screen, SAYS THE
* WELCOME OUT LOUD if the matching clip is on the SD card, and the action
* pin (IO15) goes HIGH only for enrolled faces - face unlock.
*
* SPOKEN WELCOMES (optional - the sketch works fine without them)
* ---------------------------------------------------------------
* Put WAV files in the root of the SD card, one per roster slot:
* /welcome_0.wav plays for whoever is "name 0"
* /welcome_1.wav plays for "name 1" ... up to /welcome_7.wav
* Format: 16-bit PCM WAV (mono preferred; stereo is downmixed). Any sample
* rate works. Missing card or missing file -> the triple chirp plays instead.
*
* Generate them in one go with Makerfabs/make_welcome_wavs.ps1 , which uses
* Azure text-to-speech. Azure returns exactly 16 kHz / 16-bit / mono PCM -
* precisely what this board's amplifier wants. The cloud is used ONCE, when
* you make the files; the board itself never needs the internet.
*
* Test a clip without a face in front of the camera: play 0 (serial)
*
* Serial commands:
* list show the roster and who is enrolled this session
* name <n> <text> set the name for slot n (0-7)
* del <n> forget slot n's face for this session (name kept)
* clear wipe all names from flash + forget all faces
* help this list
*
* HONEST LIMITATION (say it on camera): names persist across reboots,
* enrolled FACES do not - they live in RAM and are re-enrolled per session
* (~10 seconds per person). A permanent installation would add a custom
* flash partition for face storage.
*
* ---------------------------------------------------------------------------
* TUNING KNOBS (the #defines just below this comment)
*
* SAMPLES_PER_FACE (default 3)
* How many face samples are captured per enrollment, ~400 ms apart.
* More samples = more reliable recognition but a longer capture moment.
* 3 is a good balance; use 4-5 if recognition feels unsure (glasses
* on/off, strong side light), 2 if you want the fastest possible demo.
* Each sample consumes one of the MAX_IDS (32) model slots, so
* 8 people x 3 samples = 24 fits; 8 x 5 = 40 does NOT.
*
* GATE_MIN_WIDTH (default 90)
* How close the person must be before capture arms (face box width in
* pixels, out of 240). Raise it to force closer/better enrollments,
* lower it if people cannot get close to the lens in your setup.
*
* ---------------------------------------------------------------------------
* 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 <-- REQUIRED. Face models were removed
* from core 3.x - will not compile there
* PSRAM : OPI PSRAM <-- required, nothing works without
* Flash Size : 16MB (128Mb)
* Partition Scheme : 16M Flash (3MB APP/9.9MB FATFS)
* USB CDC On Boot : Disabled
* Upload Speed : 921600
* Port : the CH340K USB-C port (the one near RESET)
*
* Erase All Flash Contents Before Sketch Upload : DISABLED
* ^^^ If this is Enabled, every upload wipes the non-volatile
* memory where the roster names live, and your names vanish on
* each reflash. This is the usual reason "the names were erased".
*
* LIBRARIES
* GFX Library for Arduino v1.5.6 (NOT 1.6.x - that pairs with core 3)
* bb_captouch v1.3.1
* (face models and Preferences come with the ESP32 core itself)
*
* ---------------------------------------------------------------------------
* FUNCTIONS IN THIS SKETCH
* rosterLoad() load the 8 names from NVS flash at boot
* rosterSaveName(slot) save one name to NVS
* rosterClearAll() wipe names + session faces ("clear" command)
* namedCount() / enrolledCount() / nextNamedSlot(from) roster queries
* printHelp() / printRoster() serial command help + roster table
* handleSerialLine(l) parse one serial command (list/name/del/clear)
* pollSerial() non-blocking serial line reader
* spkInit() I2S speaker output
* playTone(f,ms) synthesised tone
* beepSeen()/beepEnrolled()/beepTick()/beepRecognized() event sounds
* playWavFromSD(path) play ANY WAV file on the SD card, by name
* playWelcome(slot) play /welcome_<slot>.wav, by roster slot number
* drawGreeting() the big latched NAME banner
* touchRaw(&x,&y) raw touch read, mapped to screen coordinates
* touchTapped(&x,&y) true exactly once per physical tap (edge detect)
* cameraInit() camera at 240x240 RGB565, shared I2C bus
* gatePasses(results) quality gate: one face, close, centred
* drawPanel(faces,fps) right-hand info column + ENROLL button
* overlay(msg,col) top strip inside the viewfinder
* bottomBar(msg,col) recognised-name strip at the bottom
* setup() / loop() boot / detect + recognise + enrollment machine
*
* Robojax.com
* ===========================================================================
*/
#define ENABLE_BEEP 1 // speaker chirps on events (IO19/20 - see pins.h)
#define ACTION_PIN 15 // HIGH while an ENROLLED face is in frame; -1 = off
#define ROSTER_SIZE 8
#define NAME_LEN 16
#define SAMPLES_PER_FACE 3 // multi-sample enrollment = reliable recognition
#define GATE_MIN_WIDTH 90 // face box must be at least this wide (come close)
#define ARMED_TIMEOUT_MS 30000
/* How long the big NAME banner stays on screen after the last time that face
* was seen. It refreshes every time recognition succeeds, so while the person
* is in front of the camera the banner simply stays up; when they walk away
* it lingers this long and then clears. */
#define GREET_HOLD_MS 6000
#include <Arduino_GFX_Library.h>
#include <bb_captouch.h>
#include <Wire.h>
#include <Preferences.h>
#include <SPI.h>
#include <SD.h>
#include "esp_camera.h"
#include "pins.h"
#include "human_face_detect_msr01.hpp"
#include "human_face_detect_mnp01.hpp"
#include "face_recognition_tool.hpp"
#include "face_recognition_112_v1_s8.hpp"
#if ENABLE_BEEP
#include "driver/i2s.h"
#endif
/* HWSPI (not ESP32SPI): the SD card holding the welcome clips shares pins
* 13/12/48 with the display, so BOTH must go through the same SPI driver.
* Using two different drivers silently steals the pins from the LCD. */
Arduino_HWSPI *bus = new Arduino_HWSPI(
TFT_DC, TFT_CS, TFT_SCLK, TFT_MOSI, TFT_MISO, &SPI, true);
Arduino_GFX *gfx = new Arduino_ST7789(bus, TFT_RES, 1, true);
BBCapTouch bbct;
HumanFaceDetectMSR01 stage1(0.1F, 0.5F, 10, 0.2F);
HumanFaceDetectMNP01 stage2(0.5F, 0.3F, 5);
FaceRecognition112V1S8 recognizer;
Preferences prefs;
/* --- roster --------------------------------------------------------------- */
char names[ROSTER_SIZE][NAME_LEN]; // loaded from NVS at boot
uint8_t samples_of[ROSTER_SIZE] = {0}; // samples enrolled this session
/* esp-dl hands out sequential ids (0,1,2...) as we enroll. This maps each
* model id back to a roster slot; -1 = forgotten (del command). */
#define MAX_IDS 32
int8_t id2slot[MAX_IDS];
int ids_used = 0;
/* --- enrollment state machine --------------------------------------------- */
enum EnrollState { EN_IDLE, EN_ARMED, EN_COUNTDOWN, EN_CAPTURE, EN_DONE };
EnrollState en_state = EN_IDLE;
int en_slot = -1; // roster slot being enrolled
uint32_t en_t0 = 0; // timer for the current state
int en_count = 0; // countdown value / samples captured
uint32_t en_last_sample = 0;
/* --- layout --------------------------------------------------------------- */
#define BTN_X 242
#define BTN_W 78
#define BTN_ENROLL_Y 140 // ENROLL / NEXT
#define BTN_CLEAR_Y 92 // CLEAR - wipes the banner / cancels enrollment
#define BTN_H 44
/* The top VIEW_TOP rows of the viewfinder are RESERVED for the status text.
* The camera image starts below them, so that text is never erased and
* therefore never flickers. Camera row N still lands on screen row N, so the
* detection boxes need no offset - they are simply clipped at the top. */
#define VIEW_TOP 18
/* --- serial line buffer ---------------------------------------------------- */
char ser_line[48];
int ser_len = 0;
/* --- the "hello <name>" banner --------------------------------------------
* Latched, not drawn per-frame-of-recognition: the detector occasionally
* misses a frame, and without latching the name would flicker on and off as
* the camera repaints over it. */
char greet_name[NAME_LEN] = "";
float greet_conf = 0;
uint32_t greet_until = 0; // millis() deadline; 0 = nothing showing
/* --- spoken welcome clips -------------------------------------------------
* Optional WAV files on the SD card, one per roster slot:
* /welcome_0.wav /welcome_1.wav ... /welcome_7.wav
* Slot 0 is whoever you set with "name 0 Ahmad", so adding a person is:
* set the name, drop the matching file, enroll the face.
* If the card or the file is missing, the triple chirp plays instead -
* the sketch never depends on the SD card being present.
*
* Generate the files with make_welcome_wavs.ps1 (in the Makerfabs folder):
* Azure returns exactly 16 kHz / 16-bit / mono PCM, which is precisely what
* this board's amplifier wants - so the cloud is used ONCE at build time and
* never at run time. */
bool ok_sd = false;
int pending_welcome = -1; // slot whose clip should play after drawing
/* ===========================================================================
* Roster storage — names in NVS, keys "n0".."n7"
* =========================================================================== */
void rosterLoad() {
bool opened = prefs.begin("faces", true); // read-only
int found = 0;
for (int i = 0; i < ROSTER_SIZE; i++) {
char key[4];
snprintf(key, sizeof(key), "n%d", i);
String v = prefs.getString(key, "");
strncpy(names[i], v.c_str(), NAME_LEN - 1);
names[i][NAME_LEN - 1] = 0;
if (names[i][0]) found++;
}
prefs.end();
Serial.printf("NVS \"faces\": %s, %d name(s) restored\n",
opened ? "found" : "not present",
found);
if (!opened || found == 0)
Serial.println(F(" (first run, or the flash was erased - see the note about\n"
" 'Erase All Flash Contents Before Sketch Upload' in Tools)"));
}
/* Writes the name AND reads it straight back, so a silent NVS failure can
* never look like success. */
void rosterSaveName(int slot) {
char key[4];
snprintf(key, sizeof(key), "n%d", slot);
prefs.begin("faces", false); // read-write
size_t written = prefs.putString(key, names[slot]);
String back = prefs.getString(key, "");
prefs.end();
bool ok = (written > 0) && (back == String(names[slot]));
Serial.printf("slot %d = \"%s\" -> flash %s\n",
slot, names[slot], ok ? "OK (survives reboot)" : "WRITE FAILED");
if (!ok)
Serial.println(F(" NVS write failed - is the partition scheme still\n"
" '16M Flash (3MB APP/9.9MB FATFS)'?"));
}
void rosterClearAll() {
prefs.begin("faces", false);
prefs.clear();
prefs.end();
memset(names, 0, sizeof(names));
memset(samples_of, 0, sizeof(samples_of));
for (int i = 0; i < MAX_IDS; i++) id2slot[i] = -1;
}
int namedCount() {
int n = 0;
for (int i = 0; i < ROSTER_SIZE; i++) if (names[i][0]) n++;
return n;
}
int enrolledCount() {
int n = 0;
for (int i = 0; i < ROSTER_SIZE; i++) if (samples_of[i]) n++;
return n;
}
/* next named slot at or after 'from'; -1 if none */
int nextNamedSlot(int from) {
for (int i = from; i < ROSTER_SIZE; i++) if (names[i][0]) return i;
return -1;
}
/* ===========================================================================
* Serial commands
* =========================================================================== */
void printHelp() {
Serial.println(F("\nCommands:"));
Serial.println(F(" list show roster"));
Serial.println(F(" name <n> <text> set name for slot n (0-7)"));
Serial.println(F(" del <n> forget slot n's face (this session; name kept)"));
Serial.println(F(" play <n> test-play /welcome_<n>.wav from the SD card"));
Serial.println(F(" clear wipe all names + faces (reboot for a full reset)"));
Serial.println(F(" help this list\n"));
}
void printRoster() {
Serial.println(F("\nslot name enrolled"));
Serial.println(F("---- --------------- --------"));
for (int i = 0; i < ROSTER_SIZE; i++) {
Serial.printf(" %d %-15s %s\n", i,
names[i][0] ? names[i] : "-",
samples_of[i] ? "YES" : "no");
}
Serial.printf("Names persist in flash. Faces are RAM-only (%d enrolled this session).\n\n",
enrolledCount());
}
void handleSerialLine(char *line) {
while (*line == ' ') line++;
if (strncmp(line, "list", 4) == 0) {
printRoster();
} else if (strncmp(line, "name ", 5) == 0) {
int slot = -1;
char text[NAME_LEN] = {0};
if (sscanf(line + 5, "%d %15[^\n]", &slot, text) == 2 &&
slot >= 0 && slot < ROSTER_SIZE && text[0]) {
strncpy(names[slot], text, NAME_LEN - 1);
rosterSaveName(slot);
Serial.printf("slot %d = \"%s\" (saved to flash)\n", slot, names[slot]);
} else {
Serial.println(F("usage: name <0-7> <text>"));
}
} else if (strncmp(line, "del ", 4) == 0) {
int slot = atoi(line + 4);
if (slot >= 0 && slot < ROSTER_SIZE) {
// esp-dl on core 2.x has no reliable per-id delete, so we unmap instead:
// the samples stay in the model but now report as "unknown".
for (int i = 0; i < MAX_IDS; i++) if (id2slot[i] == slot) id2slot[i] = -1;
samples_of[slot] = 0;
Serial.printf("slot %d face forgotten (name \"%s\" kept)\n", slot, names[slot]);
}
} else if (strncmp(line, "play ", 5) == 0) {
/* Test a welcome clip without needing a face in front of the camera. */
int slot = atoi(line + 5);
if (!playWelcome(slot))
Serial.printf("no /welcome_%d.wav (or no SD card)\n", slot);
} else if (strncmp(line, "clear", 5) == 0) {
rosterClearAll();
Serial.println(F("all names wiped from flash, all faces forgotten."));
Serial.println(F("(reboot to also empty the face model itself)"));
} else {
printHelp();
}
}
void pollSerial() {
while (Serial.available()) {
char c = Serial.read();
if (c == '\n' || c == '\r') {
if (ser_len > 0) {
ser_line[ser_len] = 0;
handleSerialLine(ser_line);
ser_len = 0;
}
} else if (ser_len < (int)sizeof(ser_line) - 1) {
ser_line[ser_len++] = c;
}
}
}
/* ===========================================================================
* Beep — event sounds. One chirp = face seen, rising pair = enrolled.
* =========================================================================== */
#if ENABLE_BEEP
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 = 4,
.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);
}
void playTone(float freq, int ms) {
int n = 16 * ms;
int16_t *buf = (int16_t *)malloc(n * 2);
if (!buf) return;
int fade = n / 8;
for (int i = 0; i < n; i++) {
float env = 1.0f;
if (i < fade) env = (float)i / fade;
else if (i > n - fade) env = (float)(n - i) / fade;
buf[i] = (int16_t)(sinf(2 * PI * freq * i / 16000.0f) * 11000 * env);
}
size_t w = 0;
i2s_write(I2S_SPK_PORT, buf, n * 2, &w, portMAX_DELAY);
free(buf);
}
/* playTone(0, ms) is silence - sin(0) is zero - which is how the triple
* beep gets clean gaps between its chirps. */
void beepSeen() { playTone(1000, 90); } // a face arrived
void beepEnrolled() { playTone(900, 90); playTone(1400, 140); } // enrollment done
void beepTick() { playTone(1200, 45); } // countdown tick
/* THREE quick high chirps = "I know who this is". Deliberately different
* from the single beep that just means "somebody is there". */
void beepRecognized() {
playTone(1600, 80);
playTone(0, 70);
playTone(1600, 80);
playTone(0, 70);
playTone(1600, 80);
}
/* ===========================================================================
* playWavFromSD(path) — play any WAV file on the SD card, by name.
*
* playWavFromSD("/welcome_0.wav");
* playWavFromSD("/doorbell.wav");
*
* Returns true if something was played. Handles 16-bit PCM, mono or stereo
* (stereo is downmixed), at any sample rate - the I2S clock is retuned to
* match the file and put back to 16 kHz afterwards so the beeps still work.
*
* The whole clip is loaded into PSRAM before playing: reading the SD card
* in the middle of playback would stutter, because the card shares the SPI
* bus with the display.
* =========================================================================== */
bool playWavFromSD(const char *path) {
if (!ok_sd) { Serial.printf("no SD card - cannot play %s\n", path); return false; }
File f = SD.open(path, FILE_READ);
if (!f) { Serial.printf("not found: %s\n", path); return false; }
char riff[12];
if (f.read((uint8_t *)riff, 12) != 12 ||
memcmp(riff, "RIFF", 4) || memcmp(riff + 8, "WAVE", 4)) {
Serial.printf("%s is not a WAV file\n", path);
f.close();
return false;
}
uint32_t rate = 16000, data_len = 0;
uint16_t channels = 1, bits = 16;
/* walk the RIFF chunks to find "fmt " and "data" - do not assume the
* header is exactly 44 bytes, some encoders add extra chunks */
while (f.available() >= 8) {
char cid[4];
uint32_t csize = 0;
f.read((uint8_t *)cid, 4);
f.read((uint8_t *)&csize, 4);
if (!memcmp(cid, "fmt ", 4)) {
uint8_t fmt[16] = {0};
uint32_t want = csize < 16 ? csize : 16;
f.read(fmt, want);
channels = fmt[2] | (fmt[3] << 8);
rate = (uint32_t)fmt[4] | ((uint32_t)fmt[5] << 8) |
((uint32_t)fmt[6] << 16) | ((uint32_t)fmt[7] << 24);
bits = fmt[14] | (fmt[15] << 8);
if (csize > want) f.seek(f.position() + (csize - want));
} else if (!memcmp(cid, "data", 4)) {
data_len = csize;
break; // now positioned at the samples
} else {
f.seek(f.position() + csize + (csize & 1)); // chunks are word-aligned
}
}
if (bits != 16 || data_len == 0) {
Serial.printf("%s: need 16-bit PCM (got %u-bit, %u bytes)\n",
path, bits, (unsigned)data_len);
f.close();
return false;
}
const uint32_t CAP = 1024UL * 1024UL; // ~30 s at 16 kHz mono
if (data_len > CAP) data_len = CAP;
uint8_t *pcm = (uint8_t *)ps_malloc(data_len);
if (!pcm) { Serial.println("PSRAM alloc failed"); f.close(); return false; }
uint32_t got = f.read(pcm, data_len);
f.close();
if (got < 64) { free(pcm); return false; }
/* stereo -> mono, in place */
if (channels == 2) {
int16_t *s = (int16_t *)pcm;
uint32_t pairs = got / 4;
for (uint32_t i = 0; i < pairs; i++)
s[i] = (int16_t)(((int32_t)s[i * 2] + s[i * 2 + 1]) / 2);
got = pairs * 2;
}
Serial.printf("playing %s (%u KB, %u Hz, %u ch)\n",
path, (unsigned)(got / 1024), (unsigned)rate, channels);
i2s_set_sample_rates(I2S_SPK_PORT, rate);
static const uint8_t lead_in[320] = {0}; // brief silence softens the pop
size_t w = 0;
i2s_write(I2S_SPK_PORT, lead_in, sizeof(lead_in), &w, portMAX_DELAY);
i2s_write(I2S_SPK_PORT, pcm, got, &w, portMAX_DELAY);
i2s_write(I2S_SPK_PORT, lead_in, sizeof(lead_in), &w, portMAX_DELAY);
delay(120);
i2s_zero_dma_buffer(I2S_SPK_PORT);
i2s_set_sample_rates(I2S_SPK_PORT, 16000); // back to the beep rate
free(pcm);
return true;
}
/* playWelcome(slot) — same thing, addressed by roster slot number.
* Plays /welcome_<slot>.wav . Returns false if there is no such file, so
* the caller can fall back to the chirps. */
bool playWelcome(int slot) {
if (slot < 0 || slot >= ROSTER_SIZE) return false;
char path[24];
snprintf(path, sizeof(path), "/welcome_%d.wav", slot);
return playWavFromSD(path);
}
#else
bool playWavFromSD(const char *path) { (void)path; return false; }
bool playWelcome(int slot) { (void)slot; return false; }
void beepSeen() {}
void beepEnrolled() {}
void beepTick() {}
void beepRecognized() {}
#endif
/* ===========================================================================
* Touch — edge-detected: one event per physical tap
* =========================================================================== */
bool touchRaw(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;
}
/* returns true exactly once per tap, with the tap position */
bool touchTapped(uint16_t *tx, uint16_t *ty) {
static bool down = false;
static uint8_t misses = 0;
uint16_t x, y;
if (touchRaw(&x, &y)) {
misses = 0;
if (!down) { down = true; *tx = x; *ty = y; return true; }
} else if (down) {
if (++misses >= 4) { down = false; misses = 0; }
}
return false;
}
/* ===========================================================================
* Camera — identical to the proven 03 init (shared SCCB on Wire)
* =========================================================================== */
bool cameraInit() {
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;
c.pin_sccb_sda = -1; // share the bus Wire already drives
c.pin_sccb_scl = -1;
c.sccb_i2c_port = 0;
c.pin_pwdn = CAM_PIN_PWDN;
c.pin_reset = CAM_PIN_RESET;
c.xclk_freq_hz = 20000000;
c.frame_size = FRAMESIZE_240X240;
c.pixel_format = PIXFORMAT_RGB565;
c.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
c.fb_location = CAMERA_FB_IN_PSRAM;
c.jpeg_quality = 12;
c.fb_count = 2;
if (esp_camera_init(&c) != ESP_OK) return false;
sensor_t *s = esp_camera_sensor_get();
if (s) { s->set_hmirror(s, 1); s->set_vflip(s, 1); s->set_brightness(s, 1); }
return true;
}
/* ===========================================================================
* Quality gate: exactly one face, close enough, roughly centred
* =========================================================================== */
bool gatePasses(std::list<dl::detect::result_t> &results) {
if (results.size() != 1) return false;
dl::detect::result_t &r = results.front();
int w = r.box[2] - r.box[0];
int cx = (r.box[0] + r.box[2]) / 2;
int cy = (r.box[1] + r.box[3]) / 2;
if (w < GATE_MIN_WIDTH) return false;
if (cx < 48 || cx > 192) return false;
if (cy < 48 || cy > 192) return false;
return true;
}
/* ===========================================================================
* UI
* =========================================================================== */
void drawPanel(int faces, float fps) {
gfx->fillRect(BTN_X, 0, BTN_W, 240, BLACK);
gfx->setTextSize(1);
gfx->setTextColor(CYAN);
gfx->setCursor(BTN_X + 2, 4); gfx->print("OFFLINE");
gfx->setCursor(BTN_X + 2, 14); gfx->print("face enroll");
gfx->setCursor(BTN_X + 2, 24); gfx->print("Robojax.com");
gfx->setTextColor(WHITE);
gfx->setCursor(BTN_X + 2, 44); gfx->printf("faces: %d", faces);
gfx->setCursor(BTN_X + 2, 56); gfx->printf("FPS: %.1f", fps);
gfx->setCursor(BTN_X + 2, 68); gfx->printf("known: %d/%d", enrolledCount(), namedCount());
// CLEAR button - wipes the name banner, or cancels an enrollment in progress
gfx->fillRoundRect(BTN_X, BTN_CLEAR_Y, BTN_W, BTN_H, 6, gfx->color565(110, 35, 35));
gfx->drawRoundRect(BTN_X, BTN_CLEAR_Y, BTN_W, BTN_H, 6, WHITE);
gfx->setTextColor(WHITE);
gfx->setCursor(BTN_X + 18, BTN_CLEAR_Y + 18);
gfx->print("CLEAR");
// ENROLL button
uint16_t col = (en_state == EN_IDLE) ? gfx->color565(0, 90, 160)
: gfx->color565(160, 90, 0);
gfx->fillRoundRect(BTN_X, BTN_ENROLL_Y, BTN_W, BTN_H, 6, col);
gfx->drawRoundRect(BTN_X, BTN_ENROLL_Y, BTN_W, BTN_H, 6, WHITE);
gfx->setTextColor(WHITE);
gfx->setCursor(BTN_X + 14, BTN_ENROLL_Y + 16);
gfx->print(en_state == EN_IDLE ? "ENROLL" : "NEXT >");
gfx->setTextColor(YELLOW);
gfx->setCursor(BTN_X + 2, 196); gfx->print("names kept,");
gfx->setCursor(BTN_X + 2, 206); gfx->print("faces reset");
gfx->setCursor(BTN_X + 2, 216); gfx->print("at boot");
}
/* Top status strip. Redraws ONLY when the text actually changes - the camera
* never covers this strip, so anything drawn here stays put and cannot
* flicker. Call overlay("") to clear it. */
void overlay(const char *msg, uint16_t colour) {
static char last[72] = "\x01"; // impossible value = force first draw
static uint16_t last_col = 0;
if (colour == last_col && strncmp(last, msg, sizeof(last) - 1) == 0) return;
strncpy(last, msg, sizeof(last) - 1);
last[sizeof(last) - 1] = 0;
last_col = colour;
gfx->fillRect(0, 0, 240, VIEW_TOP, BLACK);
if (!msg[0]) return;
gfx->setTextSize(1);
gfx->setTextColor(colour);
gfx->setCursor(4, 5);
gfx->print(msg);
}
void bottomBar(const char *msg, uint16_t colour) {
gfx->fillRect(0, 224, 240, 16, BLACK);
gfx->setTextSize(1);
gfx->setTextColor(colour);
gfx->setCursor(4, 228);
gfx->print(msg);
}
/* The big green NAME banner. Redrawn every frame while the greeting is live,
* so the camera never paints over it. */
void drawGreeting() {
const int bx = 6, by = 152, bw = 228, bh = 62;
gfx->fillRoundRect(bx, by, bw, bh, 8, gfx->color565(0, 120, 45));
gfx->drawRoundRect(bx, by, bw, bh, 8, WHITE);
int len = strlen(greet_name);
int ts = (len <= 11) ? 3 : 2; // shrink for very long names
int tw = len * 6 * ts;
gfx->setTextSize(ts);
gfx->setTextColor(WHITE);
gfx->setCursor(bx + (bw - tw) / 2, by + 10);
gfx->print(greet_name);
gfx->setTextSize(1);
gfx->setTextColor(gfx->color565(200, 255, 200));
gfx->setCursor(bx + 8, by + bh - 16);
gfx->printf("RECOGNIZED %.0f%% offline", greet_conf);
}
/* ===========================================================================
* SETUP
* =========================================================================== */
void setup() {
Serial.begin(115200);
delay(400);
Serial.println(F("\n=== 03b Face Enrollment | Robojax.com ==="));
for (int i = 0; i < MAX_IDS; i++) id2slot[i] = -1;
rosterLoad();
pinMode(TFT_BLK, OUTPUT);
digitalWrite(TFT_BLK, LOW);
pinMode(SD_CS, OUTPUT);
digitalWrite(SD_CS, HIGH);
// one shared SPI bus for the display AND the SD card, started before either
SPI.begin(TFT_SCLK, TFT_MISO, TFT_MOSI);
#if ACTION_PIN >= 0
pinMode(ACTION_PIN, OUTPUT);
digitalWrite(ACTION_PIN, LOW);
#endif
gfx->begin();
gfx->fillScreen(BLACK);
digitalWrite(TFT_BLK, HIGH);
bbct.init(TOUCH_SDA, TOUCH_SCL, TOUCH_RST, TOUCH_INT);
delay(50);
Wire.begin(I2C_SDA, I2C_SCL, 100000);
delay(20);
#if ENABLE_BEEP
spkInit();
#endif
gfx->setTextColor(YELLOW);
gfx->setTextSize(1);
gfx->setCursor(4, 4);
gfx->print("starting camera + neural networks...");
if (!cameraInit()) {
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);
}
/* SD card LAST - after the camera. The camera's DMA buffer must come from
* internal RAM, and mounting the card first can starve it ("cam_dma_config
* failed"). The card is entirely optional here: no card just means the
* triple chirp is used instead of spoken welcomes. */
const uint32_t sd_speeds[] = {40000000, 20000000, 10000000, 4000000};
for (uint8_t i = 0; i < 4 && !ok_sd; i++)
if (SD.begin(SD_CS, SPI, sd_speeds[i])) ok_sd = true;
digitalWrite(SD_CS, HIGH);
Serial.println(ok_sd ? "SD ok - looking for /welcome_<slot>.wav clips"
: "no SD card - using chirps instead of spoken welcomes");
gfx->fillScreen(BLACK);
drawPanel(0, 0);
printRoster();
printHelp();
if (namedCount() == 0)
Serial.println(F(">> Roster is empty. Start with: name 0 YourName"));
}
/* ===========================================================================
* LOOP
* =========================================================================== */
void loop() {
uint32_t t0 = millis();
pollSerial();
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) { delay(10); return; }
std::list<dl::detect::result_t> &candidates =
stage1.infer((uint16_t *)fb->buf, {(int)fb->height, (int)fb->width, 3});
std::list<dl::detect::result_t> &results =
stage2.infer((uint16_t *)fb->buf, {(int)fb->height, (int)fb->width, 3}, candidates);
int faces = results.size();
bool known_face = false;
char who[36] = "";
/* ---- recognition (skip while capturing - the samples ARE the model) ---- */
if (faces > 0 && en_state != EN_CAPTURE && ids_used > 0) {
dl::detect::result_t &best = results.front();
face_info_t info = recognizer.recognize((uint16_t *)fb->buf,
{(int)fb->height, (int)fb->width, 3},
best.keypoint);
if (info.id >= 0 && info.id < MAX_IDS && id2slot[info.id] >= 0) {
known_face = true;
const char *nm = names[id2slot[info.id]];
/* A greeting is "new" if it is a different person, or if the previous
* banner had already expired (they walked away and came back). Only a
* new greeting chirps - otherwise it would beep every frame. */
bool is_new = (strcmp(greet_name, nm) != 0) || (millis() > greet_until);
strncpy(greet_name, nm, NAME_LEN - 1);
greet_name[NAME_LEN - 1] = 0;
greet_conf = info.similarity * 100;
greet_until = millis() + GREET_HOLD_MS;
if (is_new) {
Serial.printf("recognized: %s (%.0f%%)\n", nm, greet_conf);
/* Queue the clip instead of playing it here: the name banner is
* drawn further down the loop, and we want the viewer to SEE the
* name before the board says it. */
pending_welcome = id2slot[info.id];
}
} else if (info.id >= 0) {
snprintf(who, sizeof(who), "forgotten face");
} else {
snprintf(who, sizeof(who), "unknown");
}
}
/* ---- enrollment state machine ---- */
bool gate = gatePasses(results);
switch (en_state) {
case EN_IDLE:
break;
case EN_ARMED:
if (millis() - en_t0 > ARMED_TIMEOUT_MS) {
en_state = EN_IDLE;
Serial.println(F("enrollment timed out"));
} else if (gate) {
en_state = EN_COUNTDOWN;
en_count = 3;
en_t0 = millis();
beepTick();
}
break;
case EN_COUNTDOWN:
if (!gate) { // they moved away - re-arm
en_state = EN_ARMED;
en_t0 = millis();
} else if (millis() - en_t0 > 700) {
en_t0 = millis();
if (--en_count <= 0) {
en_state = EN_CAPTURE;
en_count = 0;
en_last_sample = 0;
} else {
beepTick();
}
}
break;
case EN_CAPTURE:
if (gate && millis() - en_last_sample > 400) {
en_last_sample = millis();
dl::detect::result_t &best = results.front();
recognizer.enroll_id((uint16_t *)fb->buf,
{(int)fb->height, (int)fb->width, 3},
best.keypoint, "", false /* RAM only */);
if (ids_used < MAX_IDS) id2slot[ids_used++] = en_slot;
en_count++;
Serial.printf("sample %d/%d for \"%s\"\n", en_count, SAMPLES_PER_FACE, names[en_slot]);
if (en_count >= SAMPLES_PER_FACE) {
samples_of[en_slot] = en_count;
en_state = EN_DONE;
en_t0 = millis();
beepEnrolled();
Serial.printf("\"%s\" enrolled.\n", names[en_slot]);
}
} else if (!gate && millis() - en_last_sample > 2500) {
en_state = EN_ARMED; // lost them mid-capture
en_t0 = millis();
}
break;
case EN_DONE:
if (millis() - en_t0 > 1500) en_state = EN_IDLE;
break;
}
/* ---- beep on arrival + action pin ---- */
{
static int prev_faces = 0;
static uint32_t last_beep = 0;
/* !known_face: a recognized person already got the triple chirp above -
* without this they would hear three chirps AND this single beep. */
if (faces > 0 && prev_faces == 0 && en_state == EN_IDLE && !known_face &&
millis() - last_beep > 2000) {
last_beep = millis();
beepSeen();
}
prev_faces = faces;
#if ACTION_PIN >= 0
digitalWrite(ACTION_PIN, known_face ? HIGH : LOW);
#endif
}
/* ---- draw ----
* The frame starts at VIEW_TOP so it never covers the status strip. During
* the "enrolled" confirmation the picture is frozen, which makes that
* banner rock solid instead of fighting the camera for the screen. */
if (en_state != EN_DONE) {
gfx->draw16bitBeRGBBitmap(0, VIEW_TOP,
(uint16_t *)fb->buf + VIEW_TOP * fb->width,
fb->width, fb->height - VIEW_TOP);
}
esp_camera_fb_return(fb);
if (en_state != EN_DONE) {
for (auto &r : results) {
int x1 = constrain(r.box[0], 0, 239), y1 = constrain(r.box[1], VIEW_TOP + 1, 239);
int x2 = constrain(r.box[2], 0, 239), y2 = constrain(r.box[3], VIEW_TOP + 1, 239);
uint16_t bc = known_face ? GREEN : (ids_used > 0 ? YELLOW : GREEN);
gfx->drawRect(x1, y1, x2 - x1, y2 - y1, bc);
gfx->drawRect(x1 + 1, y1 + 1, x2 - x1 - 2, y2 - y1 - 2, bc);
if (r.keypoint.size() >= 10)
for (int k = 0; k < 5; k++)
gfx->fillCircle(constrain(r.keypoint[k * 2], 0, 239),
constrain(r.keypoint[k * 2 + 1], VIEW_TOP + 2, 239), 2, RED);
}
}
/* state overlays */
char msg[48];
switch (en_state) {
case EN_ARMED:
// guide box the face has to fill
gfx->drawRect(60, 50, 120, 140, CYAN);
snprintf(msg, sizeof(msg), "Enroll: %s - step close", names[en_slot]);
overlay(msg, CYAN);
break;
case EN_COUNTDOWN:
snprintf(msg, sizeof(msg), "Enroll: %s - hold still... %d", names[en_slot], en_count);
overlay(msg, YELLOW);
gfx->setTextSize(5);
gfx->setTextColor(YELLOW);
gfx->setCursor(105, 95);
gfx->printf("%d", en_count);
gfx->setTextSize(1);
break;
case EN_CAPTURE:
snprintf(msg, sizeof(msg), "capturing %s %d/%d", names[en_slot], en_count, SAMPLES_PER_FACE);
overlay(msg, GREEN);
break;
case EN_DONE:
gfx->fillRect(30, 100, 180, 40, gfx->color565(0, 110, 0));
gfx->drawRect(30, 100, 180, 40, WHITE);
gfx->setTextSize(2);
gfx->setTextColor(WHITE);
gfx->setCursor(40, 112);
gfx->printf("%s OK", names[en_slot]);
gfx->setTextSize(1);
break;
default:
overlay("", CYAN); // idle: clear the strip (once, not per frame)
break;
}
/* Known faces get the big banner; unknown/forgotten keep the small strip.
* Suppressed during enrollment so it cannot collide with the guide box,
* the countdown digit or the "enrolled" confirmation. */
if (en_state == EN_IDLE) {
if (greet_name[0] && millis() < greet_until) drawGreeting();
else if (who[0] && !known_face) bottomBar(who, YELLOW);
}
/* Now that the name is on screen, greet them out loud. Falls back to the
* triple chirp when there is no SD card or no clip for that slot. */
if (pending_welcome >= 0) {
if (!playWelcome(pending_welcome)) beepRecognized();
pending_welcome = -1;
}
/* ---- taps ---- */
uint16_t tx, ty;
if (touchTapped(&tx, &ty)) {
if (tx >= BTN_X && ty >= BTN_CLEAR_Y && ty < BTN_CLEAR_Y + BTN_H) {
/* CLEAR: drop the name banner and cancel any enrollment in progress.
* The camera repaints the picture area, so we only need to clear the
* reserved strip explicitly. */
greet_name[0] = 0;
greet_until = 0;
pending_welcome = -1;
if (en_state != EN_IDLE) {
en_state = EN_IDLE;
Serial.println(F("enrollment cancelled (CLEAR)"));
}
overlay("", CYAN);
drawPanel(0, 0);
} else if (tx >= BTN_X && ty >= BTN_ENROLL_Y && ty < BTN_ENROLL_Y + BTN_H) { // ENROLL / NEXT
if (en_state == EN_IDLE) {
int s = nextNamedSlot(0);
if (s < 0) {
Serial.println(F("Roster empty - set a name first: name 0 YourName"));
} else {
en_slot = s;
en_state = EN_ARMED;
en_t0 = millis();
}
} else {
int s = nextNamedSlot(en_slot + 1); // cycle / cancel
if (s < 0) {
en_state = EN_IDLE;
Serial.println(F("enrollment cancelled"));
} else {
en_slot = s;
en_state = EN_ARMED;
en_t0 = millis();
}
}
}
}
/* ---- panel refresh ---- */
static uint32_t last_panel = 0;
uint32_t dt = millis() - t0;
if (dt < 1) dt = 1;
if (millis() - last_panel > 500) {
last_panel = millis();
drawPanel(faces, 1000.0f / dt);
}
}
الأشياء التي قد تحتاجها
-
آخر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