Week 12
AI prompt:
“Please generate an image when she started week 12 "Mechanical Design" this week have only a group assignment part, and she works with her groupmate Hrach. We decided to create a self-balancing robot withh a camera and a web interface and need to when detect smile nned to stop Robot. group assignment: • design a machine that includes mechanism+actuation+automation+function+user interface • build the mechanical parts and operate it manually • document the group project and your individual contribution”
mechanical design, machine design n
Smile Detection Part
#include "esp_camera.h"
#include "Arduino.h"
#include <WiFi.h>
#include <WebServer.h>
// ─── WiFi credentials ───────────────────────────────────────────
#define WIFI_SSID "iPhone"
#define WIFI_PASS "Manuela26"
// ─── XIAO ESP32-S3 Sense pinout ─────────────────────────────────
#define CAM_PIN_PWDN -1
#define CAM_PIN_RESET -1
#define CAM_PIN_XCLK 10
#define CAM_PIN_SIOD 40
#define CAM_PIN_SIOC 39
#define CAM_PIN_D7 48
#define CAM_PIN_D6 11
#define CAM_PIN_D5 12
#define CAM_PIN_D4 14
#define CAM_PIN_D3 16
#define CAM_PIN_D2 18
#define CAM_PIN_D1 17
#define CAM_PIN_D0 15
#define CAM_PIN_VSYNC 38
#define CAM_PIN_HREF 47
#define CAM_PIN_PCLK 13
#define SIGNAL_PIN 43
#define SMILE_HOLD_MS 800 // how long GPIO stays HIGH after smile
#define COOLDOWN_MS 2500 // min time between two smile triggers
#define SMILE_FRAMES 4 // consecutive frames needed to confirm
// ── Relative detection thresholds ───────────────────────────────
// Mouth must be this much brighter than forehead to count as smile
#define RELATIVE_THRESH 22 /// 22
// Local contrast in mouth zone (teeth vs lips boundary)
#define CONTRAST_THRESH 100 ////50
static camera_config_t camera_config = {
.pin_pwdn = CAM_PIN_PWDN, .pin_reset = CAM_PIN_RESET,
.pin_xclk = CAM_PIN_XCLK,
.pin_sscb_sda = CAM_PIN_SIOD, .pin_sscb_scl = CAM_PIN_SIOC,
.pin_d7=CAM_PIN_D7, .pin_d6=CAM_PIN_D6,
.pin_d5=CAM_PIN_D5, .pin_d4=CAM_PIN_D4,
.pin_d3=CAM_PIN_D3, .pin_d2=CAM_PIN_D2,
.pin_d1=CAM_PIN_D1, .pin_d0=CAM_PIN_D0,
.pin_vsync=CAM_PIN_VSYNC, .pin_href=CAM_PIN_HREF, .pin_pclk=CAM_PIN_PCLK,
.xclk_freq_hz = 10000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_GRAYSCALE,
.frame_size = FRAMESIZE_QVGA,
.jpeg_quality = 12,
.fb_count = 2,
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_LATEST,
};
WebServer server(80);
int currentMean = 0;
int currentContrast = 0;
int currentRelative = 0;
uint32_t smileHoldUntil = 0;
uint32_t smileCooldownUntil = 0;
int consecutiveSmiles = 0;
uint32_t totalSmiles = 0;
uint32_t frameCount = 0;
bool lastSmileState = false;
bool newSmileFlag = false;
// ────────────────────────────────────────────────────────────────
// RELATIVE smile detection
// Compares mouth zone brightness against forehead zone.
// If the room gets brighter, BOTH zones rise equally → relative
// stays the same → no false trigger.
// Only teeth showing (mouth brighter than forehead) fires a smile.
// ────────────────────────────────────────────────────────────────
bool detectSmile(uint8_t* buf, int w, int h) {
int colS = w / 4;
int colE = (w * 3) / 4;
int foreS = h / 10;
int foreE = h / 4;
int mouthS = (h * 62) / 100;
int mouthE = (h * 85) / 100;
long sumF=0, cntF=0;
long sumM=0, cntM=0;
long globalSum=0;
int maxM=0, minM=255;
// --- Global brightness ---
for (int i=0; i maxM) maxM = p;
if (p < minM) minM = p;
}
if (!cntF || !cntM) return false;
int meanFore = sumF / cntF;
int meanMouth = sumM / cntM;
// --- Normalize against global brightness ---
int relative = (meanMouth - meanFore) - (globalMean / 12);
int contrast = maxM - minM;
// --- Face presence check ---
if (meanFore < 40 || meanFore > 200) return false;
// --- Dynamic thresholds ---
int dynamicRelThresh = max(15, meanMouth / 10);
int dynamicContThresh = max(60, contrast / 2);
// --- Smooth values (low-pass filter) ---
static float smoothRel = 0;
static float smoothCont = 0;
smoothRel = 0.7 * smoothRel + 0.3 * relative;
smoothCont = 0.7 * smoothCont + 0.3 * contrast;
currentMean = meanMouth;
currentContrast = (int)smoothCont;
currentRelative = (int)smoothRel;
bool isSmile = (smoothRel > dynamicRelThresh && smoothCont > dynamicContThresh);
Serial.printf("Fore:%3d Mouth:%3d Rel:%+3d Cont:%3d ThrR:%d %s\n",
meanFore, meanMouth,
(int)smoothRel, (int)smoothCont,
dynamicRelThresh,
isSmile ? "<<< SMILE" : "");
return isSmile;
}
// ─── Web page ───────────────────────────────────────────────────
const char INDEX_HTML[] PROGMEM = R"rawhtml(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Smile Detector</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;700;800&display=swap" rel="stylesheet">
>style>>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0a0a0f;--surface:#12121a;--border:#1e1e2e;--muted:#3a3a50;
--text:#e8e8f0;--subtle:#7070a0;--smile:#c8f060;--neutral:#4060f0;--accent:#f060c0;
}
body{background:var(--bg);color:var(--text);font-family:'Syne',sans-serif;min-height:100vh;display:grid;grid-template-rows:auto 1fr auto;}
body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(var(--border) 1px,transparent 1px),linear-gradient(90deg,var(--border) 1px,transparent 1px);background-size:40px 40px;opacity:0.4;pointer-events:none;z-index:0;}
header{position:relative;z-index:1;padding:1.4rem 2rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;}
.grandma-panel { display: flex; justify-content: space-between; position: fixed; top: 150px;width: 100%; }
.grandma-panel img { width: 20%; border-radius: 50%; }
.logo{font-size:1rem;font-weight:800;letter-spacing:0.15em;text-transform:uppercase;color:var(--smile);}
.logo span{color:var(--subtle);font-weight:400;}
.pill{font-family:'DM Mono',monospace;font-size:0.65rem;padding:0.25rem 0.7rem;border-radius:99px;border:1px solid var(--muted);color:var(--subtle);letter-spacing:0.1em;transition:all .3s;}
.pill.online{border-color:var(--smile);color:var(--smile);}
main{position:relative;z-index:1;display:grid;grid-template-columns:1fr 290px;gap:1.2rem;padding:1.2rem 2rem;max-width:1050px;width:100%;margin:0 auto;}
.left{display:flex;flex-direction:column;gap:1.2rem;}
.face{background:var(--surface);border:1px solid var(--border);border-radius:1.4rem;padding:2rem;display:flex;flex-direction:column;align-items:center;gap:1rem;transition:border-color .3s,box-shadow .3s;position:relative;overflow:hidden;}
.face.smiling{border-color:var(--smile);box-shadow:0 0 50px -10px rgba(200,240,96,.3);}
.face::after{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0%,rgba(200,240,96,.07) 0%,transparent 70%);opacity:0;transition:opacity .4s;pointer-events:none;}
.face.smiling::after{opacity:1;}
.emoji{font-size:5.5rem;line-height:1;user-select:none;transition:transform .3s cubic-bezier(.34,1.56,.64,1),filter .3s;}
.face.smiling .emoji{transform:scale(1.15);filter:drop-shadow(0 0 18px rgba(200,240,96,.5));animation:pop .45s cubic-bezier(.34,1.56,.64,1);}
@keyframes pop{0%{transform:scale(1)}50%{transform:scale(1.22)}100%{transform:scale(1.15)}}
.slabel{font-size:1.3rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--subtle);transition:color .3s;}
.face.smiling .slabel{color:var(--smile);}
.sigbadge{display:flex;align-items:center;gap:.5rem;font-family:'DM Mono',monospace;font-size:.68rem;color:var(--subtle);padding:.35rem .9rem;border:1px solid var(--muted);border-radius:99px;transition:all .3s;}
.sigbadge.active{color:var(--smile);border-color:var(--smile);background:rgba(200,240,96,.06);}
.dot{width:6px;height:6px;border-radius:50%;background:var(--muted);transition:background .3s;}
.sigbadge.active .dot{background:var(--smile);animation:blink 1s ease-in-out infinite;}
@keyframes blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.5)}}
.chart-card{background:var(--surface);border:1px solid var(--border);border-radius:1.3rem;padding:1.2rem 1.3rem;}
.ctitle{font-family:'DM Mono',monospace;font-size:.65rem;letter-spacing:.15em;text-transform:uppercase;color:var(--subtle);margin-bottom:.7rem;}
canvas{width:100%!important;height:90px!important;display:block;}
.tl{font-family:'DM Mono',monospace;font-size:.6rem;color:var(--accent);margin-top:.35rem;opacity:.7;}
.right{display:flex;flex-direction:column;gap:.9rem;}
.sc{background:var(--surface);border:1px solid var(--border);border-radius:1rem;padding:1rem 1.2rem;}
.slb{font-family:'DM Mono',monospace;font-size:.6rem;letter-spacing:.15em;text-transform:uppercase;color:var(--subtle);margin-bottom:.25rem;}
.sv{font-size:1.8rem;font-weight:800;line-height:1;}
.su{font-family:'DM Mono',monospace;font-size:.65rem;color:var(--subtle);margin-top:.15rem;}
.meter{height:3px;background:var(--muted);border-radius:2px;overflow:hidden;margin-top:.5rem;}
.mf{height:100%;border-radius:2px;background:var(--neutral);transition:width .3s,background .3s;}
.mf.hi{background:var(--smile);}
/* Relative meter: negative=blue, positive=green */
.rel-bar-wrap{height:6px;background:var(--muted);border-radius:3px;overflow:hidden;margin-top:.5rem;position:relative;}
.rel-bar{position:absolute;height:100%;border-radius:3px;transition:all .2s;}
.log-card{background:var(--surface);border:1px solid var(--border);border-radius:1rem;padding:1rem 1.2rem;flex:1;display:flex;flex-direction:column;}
.le{font-family:'DM Mono',monospace;font-size:.65rem;line-height:1.9;color:var(--subtle);overflow-y:auto;max-height:300px;}
.lr{display:flex;gap:.6rem;}
.lt{color:var(--muted);flex-shrink:0;}
.es{color:var(--smile);}
footer{position:relative;z-index:1;padding:.8rem 2rem;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-family:'DM Mono',monospace;font-size:.6rem;color:var(--muted);}
@media(max-width:700px){main{grid-template-columns:1fr;padding:1rem;}header,footer{padding:1rem;}}
</style>
</head>
<body>
<header>
<div class="logo">Smile<span>Detector</span></div>
<div class="pill" id="pill">CONNECTING...</div>
</header>
<div class="grandma-panel">
<img src="https://fabacademy.org/2026/labs/dilijan/students/mariam-daghbashyan/images/week_12/Manuela_SmileDetection.jpg" style="transform: scaleX(-1);" class="img-fluid d-flex align-center">
<img src="https://fabacademy.org/2026/labs/dilijan/students/mariam-daghbashyan/images/week_12/Manuela_SmileDetection.jpg" class="img-fluid d-flex align-center">
</div>
<main>
<div class="left">
<div class="face" id="face">
<div class="emoji" id="emoji">😐</div>
<div class="slabel" id="slabel">No smile</div>
<div class="sigbadge" id="sig"><div class="dot"></div><span>GPIO 43 → ESP32-C3</span></div>
</div>
<div class="chart-card">
<div class="ctitle">Relative brightness (mouth − forehead) — last 60 frames</div>
<canvas id="chart"></canvas>
<div class="tl">── smile threshold: +22</div>
</div>
</div>
<div class="right">
<div class="sc">
<div class="slb">Relative brightness</div>
<div class="sv" id="v-rel">—</div>
<div class="su">mouth minus forehead</div>
<div class="rel-bar-wrap"><div class="rel-bar" id="rel-bar"></div></div>
</div>
<div class="sc">
<div class="slb">Contrast</div>
<div class="sv" id="v-con">—</div>
<div class="su">max − min in mouth zone</div>
<div class="meter"><div class="mf" id="m-con"></div></div>
</div>
<div class="sc">
<div class="slb">Total smiles</div>
<div class="sv" id="v-total">0</div>
<div class="su">confirmed this session</div>
</div>
<div class="sc">
<div class="slb">Frames analysed</div>
<div class="sv" id="v-frames">0</div>
<div class="su">since boot</div>
</div>
<div class="log-card">
<div class="ctitle">Event log</div>
<div class="le" id="log"></div>
</div>
</div>
</main>
<footer>
<span>XIAO ESP32-S3 Sense</span>
<span id="fps">— fps</span>
<span>Self-balancing robot</span>
</footer>
<script>
const REL_THRESH = 22;
let hist = new Array(60).fill(0);
let lastT = Date.now();
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
function drawChart() {
const dpr=devicePixelRatio||1, W=canvas.offsetWidth*dpr, H=90*dpr;
canvas.width=W; canvas.height=H; ctx.clearRect(0,0,W,H);
// zero line
const zy=H/2;
ctx.strokeStyle='rgba(112,112,160,0.35)';ctx.lineWidth=dpr;
ctx.beginPath();ctx.moveTo(0,zy);ctx.lineTo(W,zy);ctx.stroke();
// threshold line
const range=80, ty=H/2-(REL_THRESH/range)*(H/2);
ctx.strokeStyle='rgba(240,96,192,0.5)';
ctx.setLineDash([4*dpr,4*dpr]);
ctx.beginPath();ctx.moveTo(0,ty);ctx.lineTo(W,ty);ctx.stroke();
ctx.setLineDash([]);
// bars
const bw=W/hist.length;
hist.forEach((v,i)=>{
const clamped=Math.max(-range,Math.min(range,v));
const barH=Math.abs(clamped/range)*(H/2);
const y=clamped>=0?H/2-barH:H/2;
ctx.fillStyle=clamped>REL_THRESH?`rgba(200,240,96,${0.5+0.4*(clamped/range)})`:`rgba(64,96,240,${0.3+0.3*(Math.abs(clamped)/range)})`;
ctx.fillRect(i*bw+1,y,bw-2,barH);
});
}
async function poll() {
try {
const d=await fetch('/data',{cache:'no-store'}).then(r=>r.json());
const now=Date.now();
document.getElementById('fps').textContent=Math.round(1000/Math.max(1,now-lastT))+' fps';
lastT=now;
const s=d.smile;
document.getElementById('face').classList.toggle('smiling',s);
document.getElementById('emoji').textContent=s?'😄':'😐';
document.getElementById('slabel').textContent=s?'Smile detected!':'No smile';
document.getElementById('sig').classList.toggle('active',s);
document.getElementById('v-rel').textContent=(d.relative>=0?'+':'')+d.relative;
document.getElementById('v-rel').style.color=d.relative>REL_THRESH?'var(--smile)':d.relative>0?'var(--text)':'var(--subtle)';
document.getElementById('v-con').textContent=d.contrast;
document.getElementById('v-total').textContent=d.total;
document.getElementById('v-frames').textContent=d.frames;
// relative bar: centre=0, right=positive, left=negative
const rb=document.getElementById('rel-bar');
const pct=Math.min(50,Math.abs(d.relative)/80*50);
if(d.relative>=0){rb.style.left='50%';rb.style.width=pct+'%';rb.style.background=d.relative>REL_THRESH?'var(--smile)':'var(--neutral)';}
else{rb.style.left=(50-pct)+'%';rb.style.width=pct+'%';rb.style.background='var(--accent)';}
const cm=document.getElementById('m-con');
cm.style.width=Math.min(100,d.contrast/255*100)+'%';
cm.classList.toggle('hi',d.contrast>50);
hist.push(d.relative);
if(hist.length>60)hist.shift();
drawChart();
if(d.new_smile)addLog('Smile confirmed — GPIO 43 HIGH',true);
document.getElementById('pill').textContent='ONLINE';
document.getElementById('pill').classList.add('online');
} catch(e){
document.getElementById('pill').textContent='OFFLINE';
document.getElementById('pill').classList.remove('online');
}
setTimeout(poll,50);
}
function addLog(msg,smile){
const el=document.createElement('div');el.className='lr';
const t=new Date().toTimeString().slice(0,8);
el.innerHTML=`<span class="lt">${t}</span><span class="${smile?'es':''}">${msg}</span>`;
const lg=document.getElementById('log');lg.prepend(el);
while(lg.children.length>50)lg.removeChild(lg.lastChild);
}
addLog('Dashboard connected',false);
poll();
window.addEventListener('resize',drawChart);
</script>
</body>
</html>
)rawhtml";
void handleRoot() { server.send(200,"text/html",INDEX_HTML); }
void handleData() {
bool smile=(millis()set_brightness(s,1); s->set_contrast(s,1); s->set_saturation(s,-1);
s->set_whitebal(s,1); s->set_awb_gain(s,1);
s->set_exposure_ctrl(s,1); s->set_aec2(s,1);
s->set_gainceiling(s,GAINCEILING_4X);
}
Serial.printf("Connecting to %s",WIFI_SSID);
WiFi.begin(WIFI_SSID,WIFI_PASS);
for(int i=0;i<40&&WiFi.status()!=WL_CONNECTED;i++){delay(500);Serial.print(".");}
if(WiFi.status()==WL_CONNECTED)
Serial.printf("\n>>> Open: http://%s\n",WiFi.localIP().toString().c_str());
else
Serial.println("\nWiFi failed — running headless");
server.on("/", handleRoot);
server.on("/data",handleData);
server.begin();
Serial.println("Ready! Watch Rel value — smile when >+22");
}
void loop() {
server.handleClient();
digitalWrite(SIGNAL_PIN,millis()buf,fb->width,fb->height);
esp_camera_fb_return(fb);
if(smileNow){
if(++consecutiveSmiles>=SMILE_FRAMES){
if(millis()>smileCooldownUntil){
Serial.println(">>> SMILE CONFIRMED");
smileHoldUntil =millis()+SMILE_HOLD_MS;
smileCooldownUntil =millis()+COOLDOWN_MS;
totalSmiles++;
newSmileFlag=true;
}
consecutiveSmiles=0;
}
} else {
consecutiveSmiles=0;
}
}
AI prompt:
“And when they finished this week”

