Vision voice code version 1.0

/*
 * Vision Voice — Sign Language Glove  v4
 * XIAO ESP32-C3 | 2× ADS1115 | 5× flex sensors | SSD1306 OLED | DFPlayer Mini
 *
 * v4 changes:
 *   + Two-button system — fully standalone, no Serial Monitor needed
 *       BTN_A (D1) — CYCLE / long-press to enter Capture Mode
 *       BTN_B (D2) — CONFIRM / long-press to Calibrate (recognition mode)
 *                             long-press to Delete (capture mode)
 *
 * ── BUTTON MAP ────────────────────────────────────────────
 *  RECOGNITION MODE (normal):
 *    Long press A  →  Enter Capture Mode
 *    Long press B  →  Start Calibration
 *
 *  CAPTURE MODE:
 *    Short press A →  Cycle letter  (A→B→…→Z→_→A)
 *    Short press B →  Capture displayed letter (trimmed-mean template)
 *    Long  press A →  Exit Capture Mode
 *    Long  press B →  Delete displayed letter (asks confirmation on OLED)
 *                       Then: short A = YES delete / short B = NO cancel
 *
 * Serial commands still work alongside buttons.
 *
 * FIRST RUN: long-press B to calibrate, then enter Capture Mode to record gestures.
 * NOTE: v3 flash data is compatible — no need to clear unless recalibrating.
 */

#include <Wire.h>
#include <Adafruit_ADS1X15.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
#include <DFRobotDFPlayerMini.h>

// ── Hardware ──────────────────────────────────────────────
Adafruit_ADS1115     ads1;
Adafruit_ADS1115     ads2;
HardwareSerial       dfSerial(1);
DFRobotDFPlayerMini  dfPlayer;

#define SCREEN_W  128
#define SCREEN_H   64
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, -1);

bool oledOk = false;
bool dfOk   = false;

// ── Buttons ───────────────────────────────────────────────
#define BTN_A        D1   // CYCLE / enter capture
#define BTN_B        D2   // CONFIRM / calibrate / delete
#define DEBOUNCE_MS  50
#define LONG_MS      800  // hold duration for long press

// Per-button state machine
struct Button {
  int      pin;
  bool     lastRaw;        // raw digitalRead last tick
  bool     pressed;        // currently held down (debounced)
  unsigned long pressTime; // when it went LOW (debounced)
  bool     longFired;      // long-press event already sent this hold
};
Button btnA = { BTN_A, HIGH, false, 0, false };
Button btnB = { BTN_B, HIGH, false, 0, false };

// Return values from pollButton()
#define BTN_NONE  0
#define BTN_SHORT 1
#define BTN_LONG  2

// Call every loop tick for one button.
// Returns BTN_SHORT on release (if held < LONG_MS),
//         BTN_LONG  the moment the hold crosses LONG_MS (fires once),
//         BTN_NONE  otherwise.
int pollButton(Button &b) {
  bool raw = digitalRead(b.pin);

  // Falling edge — start timing
  if (raw == LOW && b.lastRaw == HIGH) {
    b.pressTime  = millis();
    b.pressed    = true;
    b.longFired  = false;
  }

  // Still held — check for long press threshold
  if (b.pressed && !b.longFired && raw == LOW) {
    if (millis() - b.pressTime >= LONG_MS) {
      b.longFired = true;
      b.lastRaw   = raw;
      return BTN_LONG;
    }
  }

  // Rising edge — short press if not already consumed as long
  if (raw == HIGH && b.lastRaw == LOW) {
    b.pressed = false;
    b.lastRaw = raw;
    if (!b.longFired && millis() - b.pressTime >= DEBOUNCE_MS) {
      return BTN_SHORT;
    }
    return BTN_NONE;
  }

  b.lastRaw = raw;
  return BTN_NONE;
}

// ── Tuning constants ──────────────────────────────────────
const int   NUM_SAMPLES    = 8;
const int   CAPTURE_FRAMES = 50;
const int   MAX_LETTERS    = 27;      // 26 letters + '_' space gesture
const int   NUM_TEMPLATES  = 5;
const float WEIGHTS[5]     = { 1.5f, 1.2f, 1.0f, 1.0f, 1.3f }; // T I M R P

float MATCH_THRESHOLD = 350.0f;

const int STABLE_COUNT  = 7;
const int STABLE_NEEDED = 5;

const unsigned long ANNOUNCE_COOLDOWN = 2000;

// ── Per-finger calibration ────────────────────────────────
float fingerMin[5] = {     0,     0,     0,     0,     0 };
float fingerMax[5] = { 17000, 17000, 17000, 17000, 17000 };
bool  isCalibrated = false;

// ── Gesture storage ───────────────────────────────────────
struct GestureLetter {
  char  letter;                       // 'A'-'Z' or '_' for space
  float templates[NUM_TEMPLATES][5];
  int   templateCount;
  bool  active;
};
GestureLetter gestures[MAX_LETTERS];
int numStored = 0;
Preferences prefs;

// ── Capture Mode state ────────────────────────────────────
// 27 slots: index 0-25 = A-Z, index 26 = '_'
const int CYCLE_LEN = 27;
int  captureIdx  = 0;    // which letter is currently selected
bool captureMode = false;

char cycleChar(int idx) {
  if (idx < 26) return 'A' + idx;
  return '_';
}

// ── Runtime state ─────────────────────────────────────────
String        builtWord        = "";
char          lastAnnounced    = '?';
unsigned long lastAnnounceTime = 0;
char          bigLetter        = '-';
unsigned long lastStableTime   = 0;
float         lastConfidence   = 0.0f;

// Stability buffer
char stableBuffer[STABLE_COUNT];
int  stableIdx  = 0;
bool stableFull = false;

// ─────────────────────────────────────────────────────────
// Normalisation
// ─────────────────────────────────────────────────────────
float normFinger(float raw, int i) {
  float range = fingerMax[i] - fingerMin[i];
  if (range < 1.0f) return 500.0f;
  return constrain((raw - fingerMin[i]) / range * 1000.0f, 0.0f, 1000.0f);
}
void normAll(float raw[5], float out[5]) {
  for (int i = 0; i < 5; i++) out[i] = normFinger(raw[i], i);
}

// ─────────────────────────────────────────────────────────
// Sensor reading
// ─────────────────────────────────────────────────────────
int readFinger(int finger) {
  long sum = 0;
  for (int s = 0; s < NUM_SAMPLES; s++)
    sum += (finger < 4) ? ads1.readADC_SingleEnded(finger)
                        : ads2.readADC_SingleEnded(0);
  return (int)(sum / NUM_SAMPLES);
}
void readAll(float out[5]) {
  for (int i = 0; i < 5; i++) out[i] = (float)readFinger(i);
}
void clearSerial() {
  while (Serial.available()) Serial.read();
  delay(50);
}

// ─────────────────────────────────────────────────────────
// Weighted Euclidean distance
// ─────────────────────────────────────────────────────────
float normDist(float tmpl[5], float live[5]) {
  float nT[5], nL[5];
  normAll(tmpl, nT); normAll(live, nL);
  float dist = 0;
  for (int i = 0; i < 5; i++) {
    float d = (nT[i] - nL[i]) * WEIGHTS[i];
    dist += d * d;
  }
  return sqrtf(dist);
}

// ─────────────────────────────────────────────────────────
// Flash: calibration
// ─────────────────────────────────────────────────────────
void saveCalib() {
  prefs.begin("signglove", false);
  prefs.putBool("calib", true);
  for (int i = 0; i < 5; i++) {
    char a[6], b[6];
    sprintf(a, "mn%d", i); sprintf(b, "mx%d", i);
    prefs.putFloat(a, fingerMin[i]);
    prefs.putFloat(b, fingerMax[i]);
  }
  prefs.end();
  Serial.println("Calibration saved.");
}

void loadCalib() {
  prefs.begin("signglove", true);
  isCalibrated = prefs.getBool("calib", false);
  if (isCalibrated) {
    for (int i = 0; i < 5; i++) {
      char a[6], b[6];
      sprintf(a, "mn%d", i); sprintf(b, "mx%d", i);
      fingerMin[i] = prefs.getFloat(a, 0.0f);
      fingerMax[i] = prefs.getFloat(b, 17000.0f);
    }
    Serial.println("Calibration loaded.");
  }
  prefs.end();
}

// ─────────────────────────────────────────────────────────
// Flash: gestures
// ─────────────────────────────────────────────────────────
void saveGestures() {
  prefs.begin("signglove", false);
  prefs.putInt("numStored", numStored);
  for (int i = 0; i < numStored; i++) {
    char key[6]; sprintf(key, "g%d", i);
    prefs.putBytes(key, &gestures[i], sizeof(GestureLetter));
  }
  prefs.end();
  Serial.println("Gestures saved.");
}

void loadGestures() {
  prefs.begin("signglove", true);
  int n = prefs.getInt("numStored", 0);
  if (n < 0 || n > MAX_LETTERS) n = 0;
  numStored = n;
  for (int i = 0; i < numStored; i++) {
    char key[6]; sprintf(key, "g%d", i);
    size_t sz = prefs.getBytes(key, &gestures[i], sizeof(GestureLetter));
    bool valid = (gestures[i].letter >= 'A' && gestures[i].letter <= 'Z')
              || (gestures[i].letter == '_');
    if (sz != sizeof(GestureLetter) || !valid) { numStored = i; break; }
  }
  prefs.end();
  Serial.print("Loaded "); Serial.print(numStored); Serial.println(" gesture(s).");
}

void clearFlash() {
  prefs.begin("signglove", false); prefs.clear(); prefs.end();
  numStored = 0; isCalibrated = false;
  memset(gestures, 0, sizeof(gestures));
  for (int i = 0; i < 5; i++) { fingerMin[i] = 0; fingerMax[i] = 17000; }
  Serial.println("Flash cleared.");
}

// ─────────────────────────────────────────────────────────
// OLED helpers  (forward-declared for calibrate())
// ─────────────────────────────────────────────────────────
void oledUpdate();

void oledMessage(const char* line1, const char* line2 = "",
                 const char* line3 = "", const char* line4 = "") {
  if (!oledOk) return;
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(0, 2);  display.println(line1);
  display.setTextSize(1);
  display.setCursor(0, 26); display.println(line2);
  display.setCursor(0, 37); display.println(line3);
  display.setCursor(0, 50); display.println(line4);
  display.display();
}

// Capture Mode OLED — big letter centred, hint footer
void oledCaptureScreen(char letter, const char* footer1, const char* footer2) {
  if (!oledOk) return;
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);

  // Header
  display.setTextSize(1);
  display.setCursor(20, 0);
  display.print("-- CAPTURE MODE --");

  // Big letter or SPC
  if (letter == '_') {
    display.setTextSize(3);
    display.setCursor((SCREEN_W - 36) / 2, 12);
    display.print("SPC");
  } else {
    display.setTextSize(4);
    display.setCursor((SCREEN_W - 24) / 2, 10);
    display.print(letter);
  }

  // Footer hints
  display.setTextSize(1);
  display.setCursor(0, 48); display.print(footer1);
  display.setCursor(0, 57); display.print(footer2);
  display.display();
}

// ─────────────────────────────────────────────────────────
// Calibration — works with buttons or serial
// ─────────────────────────────────────────────────────────
void calibrate() {
  Serial.println("\n=== CALIBRATION ===");

  // Step 1: open hand
  oledMessage("CALIBRATE", "1. Open hand", "Extend fingers", "Press B to go");
  Serial.println("Step 1 — OPEN hand. Press B (or any serial key)...");
  while (true) {
    if (pollButton(btnB) == BTN_SHORT) break;
    if (Serial.available()) { clearSerial(); break; }
    delay(20);
  }
  delay(300);
  oledMessage("OPEN HAND", "Capturing...", "Hold still!");
  float openAvg[5] = {};
  for (int f = 0; f < 40; f++) {
    float raw[5]; readAll(raw);
    for (int i = 0; i < 5; i++) openAvg[i] += raw[i];
    delay(30);
  }
  for (int i = 0; i < 5; i++) openAvg[i] /= 40.0f;
  Serial.println("Open hand captured.");

  // Step 2: closed hand
  oledMessage("CALIBRATE", "2. Close hand", "Curl fingers", "Press B to go");
  Serial.println("Step 2 — CLOSED hand. Press B (or any serial key)...");
  while (true) {
    if (pollButton(btnB) == BTN_SHORT) break;
    if (Serial.available()) { clearSerial(); break; }
    delay(20);
  }
  delay(300);
  oledMessage("CLOSE HAND", "Capturing...", "Hold still!");
  float closeAvg[5] = {};
  for (int f = 0; f < 40; f++) {
    float raw[5]; readAll(raw);
    for (int i = 0; i < 5; i++) closeAvg[i] += raw[i];
    delay(30);
  }
  for (int i = 0; i < 5; i++) closeAvg[i] /= 40.0f;
  Serial.println("Closed hand captured.");

  // Compute min/max with 5% padding
  for (int i = 0; i < 5; i++) {
    float lo = min(openAvg[i], closeAvg[i]);
    float hi = max(openAvg[i], closeAvg[i]);
    float pad = (hi - lo) * 0.05f;
    fingerMin[i] = lo - pad;
    fingerMax[i] = hi + pad;
  }
  isCalibrated = true;
  saveCalib();

  stableIdx = 0; stableFull = false;
  memset(stableBuffer, '?', sizeof(stableBuffer));

  oledMessage("DONE!", "Calib saved.", "Re-capture all", "gestures now.");
  Serial.println("\nCalibration saved! Re-capture gesture templates.\n");

  const char* fn[] = { "Thumb ", "Index ", "Middle", "Ring  ", "Pinky " };
  for (int i = 0; i < 5; i++) {
    float range = fingerMax[i] - fingerMin[i];
    Serial.print("  "); Serial.print(fn[i]);
    Serial.print("  range: "); Serial.println(range, 0);
  }
  delay(2000);
  oledUpdate();
}

// ─────────────────────────────────────────────────────────
// Gesture capture with trimmed-mean averaging
// ─────────────────────────────────────────────────────────
void captureGesture(char letter) {
  int slot = -1;
  for (int i = 0; i < numStored; i++)
    if (gestures[i].letter == letter) { slot = i; break; }

  if (slot == -1) {
    if (numStored >= MAX_LETTERS) {
      oledMessage("FULL!", "Max gestures", "reached.");
      Serial.println("Storage full!"); delay(1500); return;
    }
    slot = numStored++;
    gestures[slot].letter        = letter;
    gestures[slot].active        = true;
    gestures[slot].templateCount = 0;
  }

  int tSlot = gestures[slot].templateCount % NUM_TEMPLATES;
  char label[4];
  if (letter == '_') strcpy(label, "SPC"); else { label[0]=letter; label[1]=0; }

  Serial.print("\nCapturing '"); Serial.print(label);
  Serial.print("' template "); Serial.print(tSlot+1);
  Serial.print("/"); Serial.print(NUM_TEMPLATES);
  Serial.println(" — hold pose, capturing in 2s...");

  // OLED countdown
  if (oledOk) {
    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    display.setTextSize(1);
    display.setCursor(0, 0); display.print("Capturing: "); display.print(label);
    display.setCursor(0, 12);
    display.print("Tmpl "); display.print(tSlot+1); display.print("/"); display.print(NUM_TEMPLATES);
    display.setTextSize(2);
    display.setCursor(20, 28); display.print("Hold pose");
    display.setTextSize(1);
    display.setCursor(0, 56); display.print("Starting in 2s...");
    display.display();
  }
  delay(2000);

  // Flush stale frames
  { float tmp[5]; for (int i=0;i<5;i++){readAll(tmp);delay(30);} }

  // Progress bar capture
  float frames[CAPTURE_FRAMES][5];
  if (oledOk) {
    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    display.setTextSize(2);
    display.setCursor(20, 4); display.print("HOLD: "); display.print(label);
    display.setTextSize(1);
    display.setCursor(0, 56); display.print("Capturing...");
    display.display();
  }
  for (int f = 0; f < CAPTURE_FRAMES; f++) {
    readAll(frames[f]);
    // Update progress bar every 5 frames
    if (oledOk && f % 5 == 0) {
      int barW = (int)((float)f / CAPTURE_FRAMES * (SCREEN_W - 4));
      display.fillRect(2, 40, barW, 10, SSD1306_WHITE);
      display.display();
    }
    delay(40);
  }

  // Trimmed mean
  const int trim = max(1, (int)(CAPTURE_FRAMES * 0.15f));
  const int used = CAPTURE_FRAMES - 2 * trim;
  float result[5]; bool highVar = false;
  for (int i = 0; i < 5; i++) {
    float v[CAPTURE_FRAMES];
    for (int f = 0; f < CAPTURE_FRAMES; f++) v[f] = frames[f][i];
    for (int a = 1; a < CAPTURE_FRAMES; a++) {
      float key = v[a]; int b = a-1;
      while (b >= 0 && v[b] > key) { v[b+1]=v[b]; b--; }
      v[b+1] = key;
    }
    float sum = 0;
    for (int f = trim; f < CAPTURE_FRAMES - trim; f++) sum += v[f];
    result[i] = sum / used;
    float range = fingerMax[i] - fingerMin[i];
    if (range > 1.0f) {
      float spread = (v[CAPTURE_FRAMES-1-trim] - v[trim]) / range;
      if (spread > 0.08f) highVar = true;
    }
  }

  for (int i = 0; i < 5; i++) gestures[slot].templates[tSlot][i] = result[i];
  gestures[slot].templateCount++;

  float norm[5]; normAll(result, norm);
  Serial.print("Centroid (norm) T:"); Serial.print(norm[0],0);
  Serial.print(" I:"); Serial.print(norm[1],0);
  Serial.print(" M:"); Serial.print(norm[2],0);
  Serial.print(" R:"); Serial.print(norm[3],0);
  Serial.print(" P:"); Serial.println(norm[4],0);

  int filled = min(gestures[slot].templateCount, NUM_TEMPLATES);
  bool done  = (filled >= NUM_TEMPLATES);

  // Result screen
  if (oledOk) {
    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    display.setTextSize(2);
    display.setCursor(0, 2); display.print(done ? "DONE!" : "SAVED!");
    display.setTextSize(1);
    display.setCursor(0, 26);
    display.print("'"); display.print(label); display.print("' ");
    display.print(filled); display.print("/"); display.print(NUM_TEMPLATES);
    display.setCursor(0, 40);
    display.print(highVar ? "!High variance" : "Stable capture");
    display.setCursor(0, 54);
    display.print(done ? "All templates full" : "Press B: next tmpl");
    display.display();
  }
  Serial.println(highVar ? "⚠  High variance." : "✓  Stable.");
  Serial.print("'"); Serial.print(label); Serial.print("' ");
  Serial.print(filled); Serial.print("/"); Serial.println(NUM_TEMPLATES);

  saveGestures();
  delay(1500);
}

// ─────────────────────────────────────────────────────────
// Delete gesture — with OLED confirmation
// ─────────────────────────────────────────────────────────
void deleteGestureWithConfirm(char letter) {
  char label[4];
  if (letter == '_') strcpy(label, "SPC"); else { label[0]=letter; label[1]=0; }

  // Check if it even exists
  int slot = -1;
  for (int i = 0; i < numStored; i++)
    if (gestures[i].letter == letter && gestures[i].active) { slot = i; break; }

  if (slot == -1) {
    oledMessage("NOT FOUND", label, "No templates", "stored yet.");
    Serial.print("Delete: '"); Serial.print(label); Serial.println("' not found.");
    delay(1500);
    return;
  }

  // Confirmation screen
  oledMessage("DELETE?", label, "[A] YES  delete", "[B] NO   cancel");
  Serial.print("Delete '"); Serial.print(label); Serial.println("'? A=YES B=NO");

  while (true) {
    int evA = pollButton(btnA);
    int evB = pollButton(btnB);

    if (evA == BTN_SHORT) {
      // Confirmed — delete
      gestures[slot].active = false;
      saveGestures();
      oledMessage("DELETED!", label, "", "");
      Serial.print("Deleted '"); Serial.print(label); Serial.println("'.");
      delay(1200);
      return;
    }
    if (evB == BTN_SHORT) {
      // Cancelled
      oledMessage("CANCELLED", "", "", "");
      Serial.println("Delete cancelled.");
      delay(800);
      return;
    }
    // Also allow long-press B to cancel (feels natural)
    if (evB == BTN_LONG || evA == BTN_LONG) {
      oledMessage("CANCELLED", "", "", "");
      Serial.println("Delete cancelled.");
      delay(800);
      return;
    }
    delay(20);
  }
}

// ─────────────────────────────────────────────────────────
// Recognition
// ─────────────────────────────────────────────────────────
char recognizeGesture(float live[5]) {
  if (numStored == 0) return '?';
  float best1 = 1e9f, best2 = 1e9f; char bestLetter = '?';
  for (int i = 0; i < numStored; i++) {
    if (!gestures[i].active) continue;
    int cnt = min(gestures[i].templateCount, NUM_TEMPLATES);
    for (int t = 0; t < cnt; t++) {
      float d = normDist(gestures[i].templates[t], live);
      if (d < best1) { best2=best1; best1=d; bestLetter=gestures[i].letter; }
      else if (d < best2) { best2=d; }
    }
  }
  if (best1 >= MATCH_THRESHOLD) { lastConfidence=0; return '?'; }
  if (numStored > 1 && best2 < MATCH_THRESHOLD * 1.5f) {
    float ratio = best1 / (best2 + 1e-6f);
    if (ratio > 0.75f) { lastConfidence=0; return '?'; }
    lastConfidence = (1.0f-ratio)*(1.0f-best1/MATCH_THRESHOLD)*100.0f;
  } else {
    lastConfidence = (1.0f - best1/MATCH_THRESHOLD)*100.0f;
  }
  lastConfidence = constrain(lastConfidence, 0.0f, 100.0f);
  return bestLetter;
}

// ─────────────────────────────────────────────────────────
// Stability vote filter
// ─────────────────────────────────────────────────────────
char getStable(char g) {
  stableBuffer[stableIdx % STABLE_COUNT] = g;
  stableIdx++;
  if (stableIdx >= STABLE_COUNT) stableFull = true;
  if (!stableFull) return '?';
  int counts[27] = {};
  for (int i = 0; i < STABLE_COUNT; i++) {
    char c = stableBuffer[i];
    if (c >= 'A' && c <= 'Z') counts[c-'A']++;
    else if (c == '_')         counts[26]++;
  }
  int best=0; char bestC='?';
  for (int i = 0; i < 26; i++)
    if (counts[i] > best) { best=counts[i]; bestC='A'+i; }
  if (counts[26] > best) { best=counts[26]; bestC='_'; }
  return (best >= STABLE_NEEDED) ? bestC : '?';
}

// ─────────────────────────────────────────────────────────
// OLED: splash + recognition screen
// ─────────────────────────────────────────────────────────
void oledSplash() {
  if (!oledOk) return;
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(14, 6);  display.println("VISION");
  display.setCursor(14, 26); display.println("VOICE");
  display.setTextSize(1);
  display.setCursor(8, 52);  display.println("Sign Language Glove");
  display.display();
  delay(2000);
}

void oledUpdate() {
  if (!oledOk) return;
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);

  if (bigLetter >= 'A' && bigLetter <= 'Z') {
    display.setTextSize(4);
    display.setCursor((SCREEN_W-24)/2, 2);
    display.print(bigLetter);
  } else if (bigLetter == '_') {
    display.setTextSize(2);
    display.setCursor((SCREEN_W-36)/2, 8);
    display.print("SPC");
  } else {
    display.setTextSize(2);
    display.setCursor(44, 10);
    display.print("--");
  }

  display.drawLine(0, 37, SCREEN_W-1, 37, SSD1306_WHITE);

  display.setTextSize(1);
  display.setCursor(2, 41);
  String shown = builtWord;
  if (shown.length() > 20) shown = shown.substring(shown.length()-20);
  display.print(shown);
  if ((millis()/500)%2 == 0) display.print("_");

  display.setCursor(2, 55);
  display.print(numStored); display.print("L ");
  if (!isCalibrated) {
    display.print("!CALIB HoldB");
  } else {
    display.print("T:"); display.print((int)MATCH_THRESHOLD);
    display.print(" C:"); display.print((int)lastConfidence); display.print("%");
  }
  display.display();
}

// ─────────────────────────────────────────────────────────
// Misc helpers
// ─────────────────────────────────────────────────────────
void printStored() {
  Serial.println("\n--- Stored Gestures ---");
  if (numStored == 0) { Serial.println("None."); }
  for (int i = 0; i < numStored; i++) {
    if (!gestures[i].active) continue;
    int cnt = min(gestures[i].templateCount, NUM_TEMPLATES);
    Serial.print("  ");
    Serial.print(gestures[i].letter=='_' ? "SPACE(_)" : String(gestures[i].letter));
    Serial.print(" → "); Serial.print(cnt);
    Serial.print("/"); Serial.println(NUM_TEMPLATES);
  }
  Serial.print("Threshold : "); Serial.println(MATCH_THRESHOLD);
  Serial.print("Calibrated: "); Serial.println(isCalibrated ? "Yes" : "No");
  Serial.println("-----------------------\n");
}

int letterToTrack(char letter) { return (letter - 'A') + 1; }

// ─────────────────────────────────────────────────────────
// Setup
// ─────────────────────────────────────────────────────────
void setup() {
  Serial.begin(115200);
  Wire.begin(D4, D5);

  pinMode(BTN_A, INPUT_PULLUP);
  pinMode(BTN_B, INPUT_PULLUP);

  if (display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    oledOk = true;
    Serial.println("OLED OK.");
    oledSplash();
  } else {
    Serial.println("OLED not found.");
  }

  dfSerial.begin(9600, SERIAL_8N1, D7, D6);
  delay(1000);
  if (dfPlayer.begin(dfSerial)) {
    dfOk = true;
    dfPlayer.volume(30);
    Serial.println("DFPlayer OK — vol 30.");
  } else {
    Serial.println("DFPlayer not found.");
  }

  if (!ads1.begin(0x48)) { Serial.println("ADS1 not found!"); while(1); }
  if (!ads2.begin(0x49)) { Serial.println("ADS2 not found!"); while(1); }
  ads1.setGain(GAIN_ONE);
  ads2.setGain(GAIN_ONE);

  loadCalib();
  loadGestures();

  if (!isCalibrated)
    Serial.println("⚠  Not calibrated — long-press B to calibrate.");

  Serial.println("\n=== Vision Voice Glove v4 ===");
  Serial.println("── BUTTONS ──────────────────");
  Serial.println("Recognition mode:");
  Serial.println("  Long-press A  → Enter Capture Mode");
  Serial.println("  Long-press B  → Calibrate");
  Serial.println("Capture Mode:");
  Serial.println("  Short A       → Cycle letter");
  Serial.println("  Short B       → Capture template");
  Serial.println("  Long  A       → Exit Capture Mode");
  Serial.println("  Long  B       → Delete letter (confirm on OLED)");
  Serial.println("── SERIAL ───────────────────");
  Serial.println("b  → Calibrate   c  → Capture");
  Serial.println("p  → Print       d  → Delete");
  Serial.println("x  → Clear all   l  → Live readings");
  Serial.println("s  → Space       r  → Backspace");
  Serial.println("n  → Clear word  +/-→ Threshold ±50");
  Serial.println("=============================\n");

  oledUpdate();
}

// ─────────────────────────────────────────────────────────
// Loop
// ─────────────────────────────────────────────────────────
void loop() {

  int evA = pollButton(btnA);
  int evB = pollButton(btnB);

  // ════════════════════════════════════════════════════════
  // CAPTURE MODE
  // ════════════════════════════════════════════════════════
  if (captureMode) {

    char selected = cycleChar(captureIdx);

    // Refresh OLED — show current selected letter
    // (only redraw on events to avoid flicker; initial draw happens on mode entry)
    if (evA == BTN_SHORT) {
      // Cycle to next letter
      captureIdx = (captureIdx + 1) % CYCLE_LEN;
      selected = cycleChar(captureIdx);
      oledCaptureScreen(selected, "[A]next  [B]save", "[holdA]exit [holdB]del");
    }

    else if (evB == BTN_SHORT) {
      // Capture the selected letter
      captureGesture(selected);
      // Return to capture screen after
      oledCaptureScreen(selected, "[A]next  [B]save", "[holdA]exit [holdB]del");
    }

    else if (evA == BTN_LONG) {
      // Exit capture mode
      captureMode = false;
      Serial.println("Exited Capture Mode.");
      oledUpdate();
    }

    else if (evB == BTN_LONG) {
      // Delete with confirmation
      deleteGestureWithConfirm(selected);
      oledCaptureScreen(selected, "[A]next  [B]save", "[holdA]exit [holdB]del");
    }

    return;  // skip recognition while in capture mode
  }

  // ════════════════════════════════════════════════════════
  // RECOGNITION MODE
  // ════════════════════════════════════════════════════════

  if (evA == BTN_LONG) {
    // Enter capture mode
    captureMode = true;
    captureIdx  = 0;
    Serial.println("Entered Capture Mode. A=cycle B=capture.");
    oledCaptureScreen(cycleChar(captureIdx),
                      "[A]next  [B]save",
                      "[holdA]exit [holdB]del");
    return;
  }

  if (evB == BTN_LONG) {
    // Calibrate
    calibrate();
    return;
  }

  // ── Serial commands ───────────────────────────────────────
  if (Serial.available()) {
    char cmd = Serial.read(); clearSerial();
    if (cmd >= 'A' && cmd <= 'Z') cmd += 32;

    if (cmd == 'b') {
      calibrate();

    } else if (cmd == 'c') {
      Serial.print("Enter letter (A-Z) or _ for space: ");
      unsigned long t = millis();
      while (!Serial.available() && millis()-t < 10000) delay(10);
      if (!Serial.available()) { Serial.println("Timeout."); return; }
      char letter = Serial.read(); clearSerial();
      if (letter >= 'a' && letter <= 'z') letter -= 32;
      bool valid = (letter >= 'A' && letter <= 'Z') || (letter == '_');
      if (!valid) { Serial.println("Invalid."); return; }
      captureGesture(letter);
      oledUpdate();

    } else if (cmd == 'p') {
      printStored();

    } else if (cmd == 'd') {
      Serial.print("Delete which letter? (A-Z or _): ");
      unsigned long t = millis();
      while (!Serial.available() && millis()-t < 10000) delay(10);
      if (!Serial.available()) { Serial.println("Timeout."); return; }
      char letter = Serial.read(); clearSerial();
      if (letter >= 'a' && letter <= 'z') letter -= 32;
      bool found = false;
      for (int i = 0; i < numStored; i++) {
        if (gestures[i].letter == letter) {
          gestures[i].active = false;
          Serial.print("Deleted '");
          Serial.print(letter=='_' ? "SPACE" : String(letter));
          Serial.println("'");
          saveGestures(); found = true; break;
        }
      }
      if (!found) Serial.println("Not found.");

    } else if (cmd == 'x') {
      Serial.print("Clear ALL? (y/n): ");
      unsigned long t = millis();
      while (!Serial.available() && millis()-t < 5000) delay(10);
      if (Serial.available() && Serial.read() == 'y') clearFlash();
      else Serial.println("Cancelled.");

    } else if (cmd == 'l') {
      Serial.println("Live mode — press 'q' to exit");
      while (true) {
        if (Serial.available() && Serial.read() == 'q') break;
        float raw[5], norm[5];
        readAll(raw); normAll(raw, norm);
        Serial.print("raw  T:"); Serial.print(raw[0],0);
        Serial.print(" I:"); Serial.print(raw[1],0);
        Serial.print(" M:"); Serial.print(raw[2],0);
        Serial.print(" R:"); Serial.print(raw[3],0);
        Serial.print(" P:"); Serial.println(raw[4],0);
        Serial.print("norm T:"); Serial.print(norm[0],0);
        Serial.print(" I:"); Serial.print(norm[1],0);
        Serial.print(" M:"); Serial.print(norm[2],0);
        Serial.print(" R:"); Serial.print(norm[3],0);
        Serial.print(" P:"); Serial.println(norm[4],0);
        Serial.println("---");
        delay(200);
      }

    } else if (cmd == 's') {
      builtWord += ' '; oledUpdate();
    } else if (cmd == 'r') {
      if (builtWord.length() > 0) builtWord.remove(builtWord.length()-1);
      oledUpdate();
    } else if (cmd == 'n') {
      builtWord = ""; bigLetter = '-'; oledUpdate();
    } else if (cmd == '+') {
      MATCH_THRESHOLD += 50;
      Serial.print("Threshold: "); Serial.println(MATCH_THRESHOLD);
    } else if (cmd == '-') {
      MATCH_THRESHOLD = max(50.0f, MATCH_THRESHOLD - 50);
      Serial.print("Threshold: "); Serial.println(MATCH_THRESHOLD);
    }
    return;
  }

  // ── Auto recognition ──────────────────────────────────────
  float raw[5];
  readAll(raw);
  char gesture = recognizeGesture(raw);
  char stable  = getStable(gesture);

  if (gesture == '_' || (gesture >= 'A' && gesture <= 'Z')) {
    bigLetter      = gesture;
    lastStableTime = millis();
  } else if (millis() - lastStableTime > 1500) {
    bigLetter = '-';
  }

  if (stable != '?' &&
      stable != lastAnnounced &&
      millis() - lastAnnounceTime > ANNOUNCE_COOLDOWN) {

    if (stable == '_') {
      Serial.println("\n>>> SPACE <<<");
      builtWord += ' ';
      bigLetter  = '_';
    } else {
      Serial.print("\n>>> "); Serial.print(stable);
      Serial.print(" <<< (conf "); Serial.print(lastConfidence,0); Serial.println("%)");
      builtWord += stable;
      bigLetter  = stable;
      if (dfOk) dfPlayer.playFolder(1, letterToTrack(stable));
    }
    lastAnnounced    = stable;
    lastAnnounceTime = millis();
    oledUpdate();
  }

  // Periodic OLED refresh (blinking cursor)
  static unsigned long lastOledMs = 0;
  if (millis() - lastOledMs > 500) { oledUpdate(); lastOledMs = millis(); }

  // Debug output
  float norm[5]; normAll(raw, norm);
  Serial.print("T:"); Serial.print(norm[0],0);
  Serial.print(" I:"); Serial.print(norm[1],0);
  Serial.print(" M:"); Serial.print(norm[2],0);
  Serial.print(" R:"); Serial.print(norm[3],0);
  Serial.print(" P:"); Serial.print(norm[4],0);
  Serial.print(" → "); Serial.print(gesture);
  Serial.print(" ("); Serial.print(lastConfidence,0); Serial.println("%)");

  delay(80);
}