/*
* 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);
}