// ===================================================== // PENNY PAL // XIAO ESP32-C6 // // B1 = D1 = Rs5 // B2 = D2 = Rs1 // B3 = D5 = Rs10 // B4 = D3 = Rs2 // D8 = IR sensor // D10 = Solenoid // D7 (GPIO17) = MP3 RX <- JQ6500 TX // D6 (GPIO16) = MP3 TX -> JQ6500 RX // // MP3 track map: // Track 1 = coin detected sound (IR beam break) // Track 2 = coin insert sound (button press) // Track 3 = 25% milestone // Track 4 = goal achieved // // Parent App: // writes /piggy/goal // // ESP32: // reads /piggy/goal // writes /piggy/total // writes /piggy/percentage // writes /piggy/lastCoin // // New Goal: // resets savings challenge // // Reboot: // restores progress from Firebase // ===================================================== // ---------------- WIFI ---------------- #include // ---------------- FIREBASE ---------------- #include #include "addons/TokenHelper.h" #include "addons/RTDBHelper.h" // ---------------- MP3 ---------------- #include // ---------------- WIFI CREDENTIALS ---------------- const char* ssid = "MiCho"; const char* password = "Hello1234"; // ---------------- FIREBASE CONFIG ---------------- #define API_KEY "AIzaSyDpefz07KOuYyj0mZ1YtvJkm5g3z5HU0-I" #define DATABASE_URL "https://smart-piggy-effc6-default-rtdb.asia-southeast1.firebasedatabase.app/" FirebaseData fbdo; FirebaseAuth auth; FirebaseConfig config; // ===================================================== // BUTTON PINS // // B1 = D1 = Rs5 // B2 = D2 = Rs1 // B3 = D5 = Rs10 // B4 = D3 = Rs2 // ===================================================== #define BTN_1 D1 // Rs5 #define BTN_2 D2 // Rs1 #define BTN_3 D5 // Rs10 #define BTN_4 D3 // Rs2 // ===================================================== // IR SENSOR PIN // ===================================================== #define IR_SENSOR D8 #define COIN_DETECTED HIGH // ===================================================== // SOLENOID PIN // ===================================================== #define SOLENOID D10 // ===================================================== // MP3 PINS (JQ6500) // ===================================================== #define MP3_RX_PIN 17 // D7 -> JQ6500 TX #define MP3_TX_PIN 16 // D6 -> JQ6500 RX HardwareSerial mp3Serial(1); JQ6500_Serial mp3(mp3Serial); // ===================================================== // SAVINGS DATA // ===================================================== int totalAmount = 0; int goal = 0; int lastGoal = -1; // ===================================================== // MILESTONE FLAGS // ===================================================== bool quarterPlayed = false; bool goalPlayed = false; // ===================================================== // IR SENSOR STATE // ===================================================== bool coinPresent = false; int lastIRState = LOW; unsigned long lastTriggerTime = 0; const int IR_LOCKOUT = 200; // ===================================================== // FIREBASE POLL TIMER // ===================================================== unsigned long lastFirebaseRead = 0; const unsigned long firebaseInterval = 2000; // ===================================================== // BUTTON STATE MEMORY // // INPUT_PULLUP: // HIGH = not pressed // LOW = pressed // ===================================================== bool lastBtn1 = HIGH; bool lastBtn2 = HIGH; bool lastBtn3 = HIGH; bool lastBtn4 = HIGH; // ===================================================== // SETUP // ===================================================== void setup() { Serial.begin(115200); Serial.println(); Serial.println("================================"); Serial.println("PENNY PAL STARTING"); Serial.println("================================"); // Button pins pinMode(BTN_1, INPUT_PULLUP); pinMode(BTN_2, INPUT_PULLUP); pinMode(BTN_3, INPUT_PULLUP); pinMode(BTN_4, INPUT_PULLUP); // IR sensor pinMode(IR_SENSOR, INPUT); lastIRState = digitalRead(IR_SENSOR); // Solenoid pinMode(SOLENOID, OUTPUT); digitalWrite(SOLENOID, LOW); // ---------------- WIFI ---------------- Serial.print("Connecting WiFi"); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.println("WiFi Connected"); Serial.print("IP: "); Serial.println(WiFi.localIP()); // ---------------- FIREBASE ---------------- config.api_key = API_KEY; config.database_url = DATABASE_URL; config.token_status_callback = tokenStatusCallback; Firebase.signUp(&config, &auth, "", ""); Firebase.begin(&config, &auth); Firebase.reconnectWiFi(true); Serial.println("Waiting for Firebase..."); while (!Firebase.ready()) { delay(100); } Serial.println("Firebase Ready"); // Restore previous progress loadSavedData(); // ---------------- MP3 ---------------- mp3Serial.begin(9600, SERIAL_8N1, MP3_RX_PIN, MP3_TX_PIN); delay(3000); mp3.reset(); delay(1000); mp3.setSource(MP3_SRC_BUILTIN); delay(500); mp3.setVolume(30); Serial.println("MP3 Ready"); Serial.println("================================"); } // ===================================================== // LOOP // ===================================================== void loop() { // Check if parent changed goal readGoal(); // ---- IR SENSOR ---- int currentIR = digitalRead(IR_SENSOR); if (lastIRState == LOW && currentIR == COIN_DETECTED) { if (millis() - lastTriggerTime > IR_LOCKOUT) { coinPresent = true; lastTriggerTime = millis(); Serial.println("Coin Detected — waiting for denomination button"); mp3.playFileByIndexNumber(1); } } if (currentIR != COIN_DETECTED) { coinPresent = false; } lastIRState = currentIR; // ---- BUTTONS ---- // Only act if a coin is present in the slot bool b1 = digitalRead(BTN_1); bool b2 = digitalRead(BTN_2); bool b3 = digitalRead(BTN_3); bool b4 = digitalRead(BTN_4); if (coinPresent) { if (lastBtn1 == HIGH && b1 == LOW) { addAmount(5); // B1 = Rs5 coinPresent = false; } if (lastBtn2 == HIGH && b2 == LOW) { addAmount(1); // B2 = Rs1 coinPresent = false; } if (lastBtn3 == HIGH && b3 == LOW) { addAmount(10); // B3 = Rs10 coinPresent = false; } if (lastBtn4 == HIGH && b4 == LOW) { addAmount(2); // B4 = Rs2 coinPresent = false; } } else { if ((lastBtn1 == HIGH && b1 == LOW) || (lastBtn2 == HIGH && b2 == LOW) || (lastBtn3 == HIGH && b3 == LOW) || (lastBtn4 == HIGH && b4 == LOW)) { Serial.println("Button ignored — no coin detected"); } } // Store button states lastBtn1 = b1; lastBtn2 = b2; lastBtn3 = b3; lastBtn4 = b4; delay(50); } // ===================================================== // LOAD SAVED DATA // // Called once during startup // ===================================================== void loadSavedData() { if (!Firebase.ready()) return; if (Firebase.RTDB.getInt(&fbdo, "/piggy/total")) { totalAmount = fbdo.intData(); Serial.print("Loaded Total: "); Serial.println(totalAmount); } if (Firebase.RTDB.get(&fbdo, "/piggy/goal")) { if (fbdo.dataType() == "string") goal = fbdo.stringData().toInt(); else if (fbdo.dataType() == "int") goal = fbdo.intData(); else if (fbdo.dataType() == "float") goal = (int)fbdo.floatData(); else if (fbdo.dataType() == "double") goal = (int)fbdo.doubleData(); lastGoal = goal; Serial.print("Loaded Goal: "); Serial.println(goal); } if (goal > 0) { if (totalAmount >= (goal / 4)) quarterPlayed = true; if (totalAmount >= goal) goalPlayed = true; } } // ===================================================== // READ GOAL // // Runs every 2 seconds // Resets challenge if parent changed goal // ===================================================== void readGoal() { if (!Firebase.ready()) return; if (millis() - lastFirebaseRead < firebaseInterval) return; lastFirebaseRead = millis(); if (Firebase.RTDB.get(&fbdo, "/piggy/goal")) { int newGoal = 0; if (fbdo.dataType() == "string") newGoal = fbdo.stringData().toInt(); else if (fbdo.dataType() == "int") newGoal = fbdo.intData(); else if (fbdo.dataType() == "float") newGoal = (int)fbdo.floatData(); else if (fbdo.dataType() == "double") newGoal = (int)fbdo.doubleData(); if (newGoal != lastGoal) { goal = newGoal; lastGoal = newGoal; Serial.println(); Serial.println("===== NEW GOAL DETECTED ====="); Serial.print("Goal = "); Serial.println(goal); totalAmount = 0; quarterPlayed = false; goalPlayed = false; Firebase.RTDB.setInt(&fbdo, "/piggy/total", 0); Firebase.RTDB.setInt(&fbdo, "/piggy/percentage", 0); Firebase.RTDB.setInt(&fbdo, "/piggy/lastCoin", 0); Serial.println("Savings Reset"); } } } // ===================================================== // ADD MONEY // // Only called after IR confirms coin is present // ===================================================== void addAmount(int value) { mp3.playFileByIndexNumber(2); delay(500); pulseSolenoid(); totalAmount += value; int percentage = 0; if (goal > 0) { percentage = (totalAmount * 100) / goal; } Serial.print("Added Rs"); Serial.print(value); Serial.print(" | Total Rs"); Serial.print(totalAmount); Serial.print(" | "); Serial.print(percentage); Serial.println("%"); sendToFirebase(totalAmount, percentage, value); checkMilestones(); } // ===================================================== // PULSE SOLENOID // ===================================================== void pulseSolenoid() { digitalWrite(SOLENOID, HIGH); delay(100); digitalWrite(SOLENOID, LOW); } // ===================================================== // CHECK MILESTONES // ===================================================== void checkMilestones() { if (goal <= 0) return; if (!quarterPlayed && totalAmount >= (goal / 4)) { quarterPlayed = true; Serial.println("25% Goal Reached!"); mp3.playFileByIndexNumber(3); } if (!goalPlayed && totalAmount >= goal) { goalPlayed = true; Serial.println("GOAL ACHIEVED!"); mp3.playFileByIndexNumber(4); } } // ===================================================== // UPDATE FIREBASE // // ESP32 NEVER WRITES GOAL — app owns it // ===================================================== void sendToFirebase(int total, int percentage, int lastCoin) { if (!Firebase.ready()) return; Firebase.RTDB.setInt(&fbdo, "/piggy/total", total); Firebase.RTDB.setInt(&fbdo, "/piggy/percentage", percentage); Firebase.RTDB.setInt(&fbdo, "/piggy/lastCoin", lastCoin); }