// Voice Keeper — integrated firmware (Final Project) // Board: Seeed XIAO ESP32-C3 (Week 8 custom PCB) // // Subsystems merged from weekly tests: // Week 09 — TTP223B touch (play trigger) // Week 10 — status LEDs on D1 / D2 // Week 11 — TCS34725 colour sensors + DFPlayer Mini UART // Week 15 — Wi-Fi AP + Web UI (adapted for track playback) // // Libraries (Arduino Library Manager): // Adafruit TCS34725 // DFRobotDFPlayerMini // // Wiring (XIAO ESP32-C3): // I2C SDA=D4, SCL=D5 → TCA9548A → 4× TCS34725 (channels 0–3) // UART TX=D6, RX=D7 → DFPlayer Mini (9600 baud) // Touch D0 → TTP223B play sensor // LED D1 → success (green) // LED D2 → error (red) #include #include #include #include #include "DFRobotDFPlayerMini.h" // ---- Pins ---- const int PIN_TOUCH = D0; const int PIN_LED_OK = D1; const int PIN_LED_ERR = D2; // ---- I2C multiplexer (TCA9548A) ---- const uint8_t TCA9548A_ADDR = 0x70; const uint8_t NUM_SENSORS = 4; Adafruit_TCS34725 tcs( TCS34725_INTEGRATIONTIME_50MS, TCS34725_GAIN_4X ); // ---- DFPlayer ---- HardwareSerial dfSerial(1); DFRobotDFPlayerMini dfPlayer; const uint8_t DFPLAYER_VOLUME = 28; // 0–30, tested in integration // ---- Wi-Fi AP + Web UI ---- const char *AP_SSID = "VoiceKeeper"; const char *AP_PASS = "fabacademy"; WebServer server(80); static const char INDEX_HTML[] PROGMEM = R"rawliteral( Voice Keeper

Voice Keeper — Setup UI

Play an MP3 by track number (0001.mp3 → track 1). For colour-strip photos, use the touch sensor on the device.



)rawliteral"; // ---- Quinary colour encoding (Black=0, Blue=1, Green=2, Red=3, White=4) ---- enum ColourCode : uint8_t { COLOUR_BLACK = 0, COLOUR_BLUE = 1, COLOUR_GREEN = 2, COLOUR_RED = 3, COLOUR_WHITE = 4, COLOUR_UNKNOWN = 255 }; // Tune these thresholds after calibration under your room lighting. const uint16_t CLEAR_MIN = 80; // below → treat as black const uint16_t CLEAR_WHITE = 900; // above → treat as white // ---- State ---- bool touchLatched = false; uint32_t lastPhotoId = 0; // ---- TCA9548A ---- bool tcaSelect(uint8_t channel) { if (channel > 7) return false; Wire.beginTransmission(TCA9548A_ADDR); Wire.write(1 << channel); return Wire.endTransmission() == 0; } bool tcsBeginOnChannel(uint8_t channel) { if (!tcaSelect(channel)) return false; return tcs.begin(); } // ---- Colour classification ---- ColourCode classifyColour(uint16_t r, uint16_t g, uint16_t b, uint16_t c) { if (c < CLEAR_MIN) return COLOUR_BLACK; if (c > CLEAR_WHITE) return COLOUR_WHITE; // Normalise channels by clear value float rn = (float)r / c; float gn = (float)g / c; float bn = (float)b / c; if (bn > rn + 0.08f && bn > gn + 0.05f) return COLOUR_BLUE; if (gn > rn + 0.05f && gn > bn + 0.03f) return COLOUR_GREEN; if (rn > gn + 0.08f && rn > bn + 0.08f) return COLOUR_RED; // Fallback: brightest channel wins if (r >= g && r >= b) return COLOUR_RED; if (g >= r && g >= b) return COLOUR_GREEN; return COLOUR_BLUE; } bool readQuinaryId(uint32_t &idOut) { uint32_t id = 0; uint16_t weights[] = {125, 25, 5, 1}; for (uint8_t ch = 0; ch < NUM_SENSORS; ch++) { if (!tcsBeginOnChannel(ch)) { Serial.print(F("Sensor channel fail: ")); Serial.println(ch); return false; } uint16_t r, g, b, c; tcs.getRawData(&r, &g, &b, &c); ColourCode code = classifyColour(r, g, b, c); Serial.print(F("Ch")); Serial.print(ch); Serial.print(F(" R:")); Serial.print(r); Serial.print(F(" G:")); Serial.print(g); Serial.print(F(" B:")); Serial.print(b); Serial.print(F(" C:")); Serial.print(c); Serial.print(F(" → ")); Serial.println(code); if (code == COLOUR_UNKNOWN) return false; id += (uint32_t)code * weights[ch]; } idOut = id; return true; } // ---- LEDs ---- void pulseLed(int pin, unsigned long ms = 400) { digitalWrite(pin, HIGH); delay(ms); digitalWrite(pin, LOW); } void signalOk() { pulseLed(PIN_LED_OK); } void signalError() { pulseLed(PIN_LED_ERR); delay(100); pulseLed(PIN_LED_ERR); } // ---- Audio ---- bool playTrack(uint16_t track) { if (track < 1) return false; dfPlayer.play(track); Serial.print(F("Playing track ")); Serial.println(track); return true; } bool playPhotoId(uint32_t photoId) { if (photoId > 624) return false; // SD files named 0001.mp3 … map to track = photoId + 1 return playTrack((uint16_t)(photoId + 1)); } // ---- Touch-triggered playback ---- void handleTouchPlay() { int touch = digitalRead(PIN_TOUCH); if (touch == HIGH && !touchLatched) { touchLatched = true; uint32_t photoId = 0; if (readQuinaryId(photoId)) { lastPhotoId = photoId; Serial.print(F("Photo ID: ")); Serial.println(photoId); if (playPhotoId(photoId)) { signalOk(); } else { signalError(); } } else { Serial.println(F("Colour read failed")); signalError(); } } if (touch == LOW) { touchLatched = false; } } // ---- Web UI handlers ---- void sendJsonOk() { server.send(200, "application/json", "{\"ok\":true}"); } void handleRoot() { server.send_P(200, "text/html", INDEX_HTML); } void handlePlay() { if (!server.hasArg("track")) { server.send(400, "application/json", "{\"ok\":false}"); return; } uint16_t track = constrain(server.arg("track").toInt(), 1, 999); if (playTrack(track)) { sendJsonOk(); } else { server.send(500, "application/json", "{\"ok\":false}"); } } void handleState() { String json = "{\"lastPhotoId\":" + String(lastPhotoId) + "}"; server.send(200, "application/json", json); } void setupWebServer() { server.on("/", HTTP_GET, handleRoot); server.on("/api/play", HTTP_GET, handlePlay); server.on("/api/state", HTTP_GET, handleState); server.begin(); } // ---- Setup ---- void setup() { Serial.begin(115200); delay(200); pinMode(PIN_TOUCH, INPUT); pinMode(PIN_LED_OK, OUTPUT); pinMode(PIN_LED_ERR, OUTPUT); digitalWrite(PIN_LED_OK, LOW); digitalWrite(PIN_LED_ERR, LOW); Wire.begin(); // SDA=D4, SCL=D5 on XIAO ESP32-C3 // Verify first colour sensor on mux channel 0 if (!tcsBeginOnChannel(0)) { Serial.println(F("TCS34725 / TCA9548A init failed — check I2C wiring")); signalError(); } else { Serial.println(F("Colour sensors ready (TCA9548A ch0 OK)")); } dfSerial.begin(9600, SERIAL_8N1, D7, D6); if (!dfPlayer.begin(dfSerial)) { Serial.println(F("DFPlayer init failed — check UART wiring and SD card")); signalError(); } else { dfPlayer.volume(DFPLAYER_VOLUME); Serial.println(F("DFPlayer ready")); } WiFi.mode(WIFI_AP); WiFi.softAP(AP_SSID, AP_PASS); setupWebServer(); Serial.println(F("Voice Keeper firmware ready")); Serial.print(F("Setup UI: http://")); Serial.println(WiFi.softAPIP()); } // ---- Main loop ---- void loop() { server.handleClient(); handleTouchPlay(); delay(20); }