/*
* Vision Voice — Sign Language Glove v5.1
* XIAO ESP32-C3 | 2× ADS1115 | 5× flex sensors | SSD1306 OLED | DFPlayer Mini
*
* v5 changes (speed + accuracy improvements):
* + ADS1115 data rate → 860 SPS (was default 128 SPS — ~7x faster ADC)
* + NUM_SAMPLES 4 (was 8 — faster reads, still averaged)
* + NUM_TEMPLATES 8 (was 5 — more pose coverage per gesture)
* + STABLE_COUNT 5 (was 7 — faster gesture lock-on)
* + STABLE_NEEDED 4 (was 5 — faster gesture lock-on)
* + ANNOUNCE_COOLDOWN 1300ms (was 2000ms — snappier announcements)
* + Reject ratio 0.65 (was 0.75 — stricter, fewer wrong letters)
* + bigLetter timeout 800ms (was 1500ms — display resets faster)
* + Loop delay 30ms (was 80ms — tighter recognition loop)
*
* v5.1 changes:
* + Added '~' gesture = "I Love You" (ILY) handshape → plays 027.mp3
* + MAX_LETTERS 27→28, CYCLE_LEN 27→28
* + OLED shows "ILY" label instead of raw '~' character
*
* ⚠ v4 flash data is NOT compatible (NUM_TEMPLATES changed).
* Clear flash (serial 'x') and re-capture all gestures after flashing.
*
* ── 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→_→ILY→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: clear flash (x), long-press B to calibrate, then capture gestures.
*/
#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
struct Button {
int pin;
bool lastRaw;
bool pressed;
unsigned long pressTime;
bool longFired;
};
Button btnA = { BTN_A, HIGH, false, 0, false };
Button btnB = { BTN_B, HIGH, false, 0, false };
#define BTN_NONE 0
#define BTN_SHORT 1
#define BTN_LONG 2
int pollButton(Button &b) {
bool raw = digitalRead(b.pin);
if (raw == LOW && b.lastRaw == HIGH) {
b.pressTime = millis();
b.pressed = true;
b.longFired = false;
}
if (b.pressed && !b.longFired && raw == LOW) {
if (millis() - b.pressTime >= LONG_MS) {
b.longFired = true;
b.lastRaw = raw;
return BTN_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 = 4;
const int CAPTURE_FRAMES = 50;
const int MAX_LETTERS = 28; // v5.1: 26 letters + '_' space + '~' ILY
const int NUM_TEMPLATES = 8;
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 = 5;
const int STABLE_NEEDED = 4;
const unsigned long ANNOUNCE_COOLDOWN = 1300;
// ── 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', '_' space, '~' ILY
float templates[NUM_TEMPLATES][5];
int templateCount;
bool active;
};
GestureLetter gestures[MAX_LETTERS];
int numStored = 0;
Preferences prefs;
// ── Capture Mode state ────────────────────────────────────
// 28 slots: 0-25 = A-Z, 26 = '_', 27 = '~' (ILY)
const int CYCLE_LEN = 28; // v5.1: was 27
int captureIdx = 0;
bool captureMode = false;
char cycleChar(int idx) {
if (idx < 26) return 'A' + idx;
if (idx == 26) return '_';
return '~'; // v5.1: ILY gesture
}
// Human-readable label for a gesture character
void getLabel(char letter, char* label) {
if (letter == '_') strcpy(label, "SPC");
else if (letter == '~') strcpy(label, "ILY"); // v5.1
else { label[0] = letter; label[1] = 0; }
}
// ── 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 == '_')
|| (gestures[i].letter == '~'); // v5.1
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
// ─────────────────────────────────────────────────────────
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 label centred, hint footer
void oledCaptureScreen(char letter, const char* footer1, const char* footer2) {
if (!oledOk) return;
char label[4]; getLabel(letter, label);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
// Header
display.setTextSize(1);
display.setCursor(20, 0);
display.print("-- CAPTURE MODE --");
// Big label: single letter gets size 4, 3-char labels get size 3
if (letter != '_' && letter != '~') {
// Single letter A-Z
display.setTextSize(4);
display.setCursor((SCREEN_W - 24) / 2, 10);
display.print(letter);
} else {
// SPC or ILY — 3 chars at size 3
display.setTextSize(3);
display.setCursor((SCREEN_W - 54) / 2, 12);
display.print(label);
}
// Footer hints
display.setTextSize(1);
display.setCursor(0, 48); display.print(footer1);
display.setCursor(0, 57); display.print(footer2);
display.display();
}
// ─────────────────────────────────────────────────────────
// Calibration
// ─────────────────────────────────────────────────────────
void calibrate() {
Serial.println("\n=== CALIBRATION ===");
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.");
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.");
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]; getLabel(letter, label);
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...");
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);
{ float tmp[5]; for (int i=0;i<5;i++){readAll(tmp);delay(30);} }
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]);
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);
}
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);
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]; getLabel(letter, label);
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;
}
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) {
gestures[slot].active = false;
saveGestures();
oledMessage("DELETED!", label, "", "");
Serial.print("Deleted '"); Serial.print(label); Serial.println("'.");
delay(1200);
return;
}
if (evB == BTN_SHORT) {
oledMessage("CANCELLED", "", "", "");
Serial.println("Delete cancelled.");
delay(800);
return;
}
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.65f) { 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[28] = {}; // v5.1: was 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]++;
else if (c == '~') counts[27]++; // v5.1
}
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='_'; }
if (counts[27] > best) { best=counts[27]; bestC='~'; } // v5.1
return (best >= STABLE_NEEDED) ? bestC : '?';
}
// ─────────────────────────────────────────────────────────
// DFPlayer track mapping
// ─────────────────────────────────────────────────────────
int letterToTrack(char letter) {
if (letter == '~') return 27; // v5.1: 027.mp3 = ILY
if (letter >= 'A' && letter <= 'Z') return (letter - 'A') + 1; // 001-026
return 0; // no track for space/unknown
}
// ─────────────────────────────────────────────────────────
// 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') {
// Single letter — big size 4
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 if (bigLetter == '~') {
// v5.1: ILY recognition display
display.setTextSize(2);
display.setCursor((SCREEN_W-36)/2, 8);
display.print("ILY");
} 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;
char label[4]; getLabel(gestures[i].letter, label);
int cnt = min(gestures[i].templateCount, NUM_TEMPLATES);
Serial.print(" ");
Serial.print(label);
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");
}
// ─────────────────────────────────────────────────────────
// 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);
ads1.setDataRate(RATE_ADS1115_860SPS);
ads2.setDataRate(RATE_ADS1115_860SPS);
loadCalib();
loadGestures();
if (!isCalibrated)
Serial.println("⚠ Not calibrated — long-press B to calibrate.");
Serial.println("\n=== Vision Voice Glove v5.1 ===");
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 (A→Z→SPC→ILY→A)");
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);
if (evA == BTN_SHORT) {
captureIdx = (captureIdx + 1) % CYCLE_LEN;
selected = cycleChar(captureIdx);
oledCaptureScreen(selected, "[A]next [B]save", "[holdA]exit [holdB]del");
}
else if (evB == BTN_SHORT) {
captureGesture(selected);
oledCaptureScreen(selected, "[A]next [B]save", "[holdA]exit [holdB]del");
}
else if (evA == BTN_LONG) {
captureMode = false;
Serial.println("Exited Capture Mode.");
oledUpdate();
}
else if (evB == BTN_LONG) {
deleteGestureWithConfirm(selected);
oledCaptureScreen(selected, "[A]next [B]save", "[holdA]exit [holdB]del");
}
return;
}
// ════════════════════════════════════════════════════════
// RECOGNITION MODE
// ════════════════════════════════════════════════════════
if (evA == BTN_LONG) {
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();
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), _ for space, or ~ for ILY: ");
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 == '_')
|| (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;
char label[4]; getLabel(letter, label);
Serial.print("Deleted '"); Serial.print(label); 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 == '~' || (gesture >= 'A' && gesture <= 'Z')) {
bigLetter = gesture;
lastStableTime = millis();
} else if (millis() - lastStableTime > 800) {
bigLetter = '-';
}
if (stable != '?' &&
stable != lastAnnounced &&
millis() - lastAnnounceTime > ANNOUNCE_COOLDOWN) {
if (stable == '_') {
Serial.println("\n>>> SPACE <<<");
builtWord += ' ';
bigLetter = '_';
} else {
char label[4]; getLabel(stable, label);
Serial.print("\n>>> "); Serial.print(label);
Serial.print(" <<< (conf "); Serial.print(lastConfidence,0); Serial.println("%)");
if (stable != '~') builtWord += stable; // ILY doesn't append a char to word
else builtWord += String("ILY"); // v5.1: append "ILY" text to word
bigLetter = stable;
int track = letterToTrack(stable);
if (dfOk && track > 0) dfPlayer.playFolder(1, track);
}
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(30);
}