Week 15: Interface and Application Programming

Individual Assignment:

Write an application that interfaces a user with an input &/or output device that you made.

Group Assignment:

Compare as many tool options as possible.


Starting Point: What I Thought I Was Building

At the beginning of this week, the task sounded quite simple: build a parent-facing app for my smart piggy bank.

The idea in my head was very direct:
1.Parent sets a savings goal
2.Kid adds money through the piggy bank
3.App shows how much has been saved
It felt like a straightforward “app problem”.


Where Things Started Getting Unclear

As soon as I tried to think about implementation, things started breaking apart.

I realized I didn’t actually know how the system was supposed to behave behind the scenes:

Where is the goal stored?
Who keeps track of the total amount?
Who calculates the percentage?
How does the app and device even communicate?

At this point, everything existed as one blurred system rather than clearly defined parts.


Breaking It Down (First Real Step)

To get some clarity, I split the system into three components:

ESP32 (device) → handles physical interaction
Mobile App (Kodular) → interface for the parent
Cloud layer (later Firebase) → shared data between both

This was the first moment the project started to feel like a system instead of just an app.


Important Design Decision

One decision that shaped everything after this:

The app should not perform any calculations.

So the responsibility became:

ESP32

  • calculate total
  • calculate percentage
  • handle logic
  • App

  • display values
  • send goal

  • This made the architecture cleaner, but it also meant the ESP32 had to do more than I initially expected.


    Rough Data Flow (At That Time)

    This is how I imagined things would work:

    
    App → sends goal
    ESP32 → reads goal
    User input → adds money
    ESP32 → updates total + percentage
    ESP32 → sends data to cloud
    App → reads and displays
    
    

    At this stage, this was just a conceptual flow — nothing was implemented yet.


    Ground Reality

    Printing process
    The above slide was made on Canva and not AI generated. All elements are editable.


    Printing process

    Right now, I had no working app, no database and no communication between anything. Only a rough idea of how things should work.

    So instead of jumping into everything at once, I decided to start from the most controlled part of the system:

    The button input on the ESP32


    Transition

    Before anything could go to Firebase or the app, I needed one thing to work perfectly:

    a reliable way to convert physical interaction into meaningful input.


    Button Logic - Starting from What I Already Had

    After mapping out the system, I still didn’t have anything functional. So instead of trying to build everything at once, I went back to something I had already made.

    Starting Point: My Previous PCB

    In the PCB production week, I had designed a board with:

  • XIAO ESP32-C6
  • Push button
  • LED
  • At that time, I had already experimented with:

  • button input
  • LED feedback
  • Serial output
  • That setup was stable and something I understood.

    So instead of reinventing everything, I decided to reuse that logic.


    The Shift in Thinking

    At this point, the idea clicked:

    treat the button as a coin input system.

    So I mapped interactions like this:

    Single press → ₹1 → LED blinks once
    Double press → ₹2 → LED blinks twice
    Long press → ₹5 → LED stays on briefly

    And every interaction should:

    print to Serial
    update a running total


    Building on Existing Logic

    I didn’t start from scratch. I reused parts of my earlier working code and extended it to:

  • detect press types (single, double, long)
  • maintain a running total
  • print clean outputs only when an action happens
  • This made the system much more stable from the beginning.


    First Working Behavior

    Printing process

    At this stage:

  • input was reliable
  • feedback was clear (LED + Serial)
  • total was updating correctly
  • This was the first time I had actual data being generated by the system.


    Prompt used on Chatgpt

    I am using a XIAO ESP32-C6 with a push button on D8 and an LED on D2. I already have a simple button-handling program that works reliably, and I want to build on that logic.
    
    Can you help me create a coin simulation system where:
    
    Single press = ₹1 and LED blinks once
    Double press = ₹2 and LED blinks twice
    Long press = ₹5 and LED stays on briefly
    
    The code should use debounce and timing logic to correctly distinguish between single, double, and long presses without conflicts. It should maintain a running total and print updates to the Serial Monitor only when a valid interaction occurs.
    
    

    Code

    // -------- PIN SETUP -------- #define BUTTON_PIN D8 // button input #define LED_PIN D2 // yellow LED (active LOW or HIGH — we'll handle) // -------- TIMING CONFIG -------- const unsigned long debounceDelay = 50; // debounce time const unsigned long longPressTime = 1000; // long press threshold const unsigned long doublePressGap = 400; // max gap for double press // -------- STATE VARIABLES -------- bool buttonState = LOW; // current stable state bool lastButtonReading = LOW; // last raw reading unsigned long lastDebounceTime = 0; unsigned long pressStartTime = 0; unsigned long lastReleaseTime = 0; int pressCount = 0; int totalAmount = 0; // -------- SETUP -------- void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLDOWN); // using your proven setup pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // LED OFF initially } // -------- LOOP -------- void loop() { int reading = digitalRead(BUTTON_PIN); // -------- DEBOUNCE -------- if (reading != lastButtonReading) { lastDebounceTime = millis(); // reset debounce timer } if ((millis() - lastDebounceTime) > debounceDelay) { if (reading != buttonState) { buttonState = reading; // -------- BUTTON PRESSED -------- if (buttonState == HIGH) { pressStartTime = millis(); // start timing } // -------- BUTTON RELEASED -------- else { unsigned long pressDuration = millis() - pressStartTime; // -------- LONG PRESS -------- if (pressDuration >= longPressTime) { handleLongPress(); // ₹5 pressCount = 0; // reset count (important) } else { // short press → count for single/double pressCount++; lastReleaseTime = millis(); } } } } // -------- SINGLE / DOUBLE DECISION -------- if (pressCount > 0 && (millis() - lastReleaseTime) > doublePressGap) { if (pressCount == 1) { handleSinglePress(); // ₹1 } else if (pressCount == 2) { handleDoublePress(); // ₹2 } pressCount = 0; // reset after handling } lastButtonReading = reading; } // -------- ACTIONS -------- // SINGLE PRESS → ₹1 void handleSinglePress() { totalAmount += 1; Serial.print("Single Press | +1 | Total: "); Serial.println(totalAmount); // single blink digitalWrite(LED_PIN, HIGH); delay(120); digitalWrite(LED_PIN, LOW); } // DOUBLE PRESS → ₹2 void handleDoublePress() { totalAmount += 2; Serial.print("Double Press | +2 | Total: "); Serial.println(totalAmount); // double blink for (int i = 0; i < 2; i++) { digitalWrite(LED_PIN, HIGH); delay(120); digitalWrite(LED_PIN, LOW); delay(120); } } // LONG PRESS → ₹5 void handleLongPress() { totalAmount += 5; Serial.print("Long Press | +5 | Total: "); Serial.println(totalAmount); // long glow digitalWrite(LED_PIN, HIGH); delay(400); digitalWrite(LED_PIN, LOW); }


    Extending It: Introducing a Goal

    Once the total was working, I added a simple constraint:

  • Goal = ₹100 (hardcoded for now)
  • From this, I calculated:

  • percentage = (total / goal) × 100
  • Now the system wasn’t just counting, it had context.


    Adding Milestones

    To make progress visible, I introduced milestone checks:

  • 25%
  • 50%
  • 75%
  • 100%
  • Each milestone triggered a Serial message and LED feedback.

    This made the system feel more interactive and state-aware.


    Prompt used on Chatgpt

    Now that the single, double, and long press logic is working reliably with the button on D8 and the LED on D2, I want to build on
    this same system instead of changing anything. Next, I want to introduce a goal into this flow. So the idea is that the device
    keeps adding values (1, 2, or 5 rupees based on interaction), tracks a running total, and compares it to a fixed goal (for now hardcoded).
    From that, it should calculate percentage progress and check for milestones at 25%, 50%, 75%, and 100%. When it reaches 100%, it should
    trigger a clear feedback moment (like a slightly different LED pattern), and then reset everything back to 0 so the cycle can start again.
    I want to keep the same structure and stability from the previous working code, just extend it to include this goal, percentage, milestone
    tracking, and reset behavior without breaking the interaction logic.
    

    Code

    // -------- PIN SETUP -------- #define BUTTON_PIN D8 // button input (INPUT_PULLDOWN) #define LED_PIN D2 // yellow LED // -------- TIMING CONFIG -------- const unsigned long debounceDelay = 50; const unsigned long longPressTime = 1000; const unsigned long doublePressGap = 400; // -------- SYSTEM STATE -------- int totalAmount = 0; int goal = 100; bool milestone25 = false; bool milestone50 = false; bool milestone75 = false; bool milestone100 = false; // -------- BUTTON STATE -------- bool buttonState = LOW; bool lastButtonReading = LOW; unsigned long lastDebounceTime = 0; unsigned long pressStartTime = 0; unsigned long lastReleaseTime = 0; int pressCount = 0; // -------- SETUP -------- void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLDOWN); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // LED OFF initially } // -------- LOOP -------- void loop() { int reading = digitalRead(BUTTON_PIN); // -------- DEBOUNCE -------- if (reading != lastButtonReading) { lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > debounceDelay) { ``` if (reading != buttonState) { buttonState = reading; // -------- BUTTON PRESSED -------- if (buttonState == HIGH) { pressStartTime = millis(); } // -------- BUTTON RELEASED -------- else { unsigned long pressDuration = millis() - pressStartTime; // -------- LONG PRESS -------- if (pressDuration >= longPressTime) { handleLongPress(); pressCount = 0; } else { pressCount++; lastReleaseTime = millis(); } } } ``` } // -------- SINGLE / DOUBLE -------- if (pressCount > 0 && (millis() - lastReleaseTime) > doublePressGap) { ``` if (pressCount == 1) { handleSinglePress(); } else if (pressCount == 2) { handleDoublePress(); } pressCount = 0; ``` } lastButtonReading = reading; } // -------- ACTIONS -------- // Single press → ₹1 void handleSinglePress() { addAmount(1); blinkLED(1); } // Double press → ₹2 void handleDoublePress() { addAmount(2); blinkLED(2); } // Long press → ₹5 void handleLongPress() { addAmount(5); // longer glow digitalWrite(LED_PIN, HIGH); delay(400); digitalWrite(LED_PIN, LOW); } // -------- CORE LOGIC -------- void addAmount(int value) { // prevent adding after goal already reached if (milestone100) return; totalAmount += value; // clamp to goal if (totalAmount > goal) { totalAmount = goal; } int percentage = (totalAmount * 100) / goal; Serial.print("Added: "); Serial.print(value); Serial.print(" | Total: "); Serial.print(totalAmount); Serial.print(" | "); Serial.print(percentage); Serial.println("%"); checkMilestones(percentage); } // -------- LED HELPER -------- void blinkLED(int times) { for (int i = 0; i < times; i++) { digitalWrite(LED_PIN, HIGH); delay(120); digitalWrite(LED_PIN, LOW); delay(120); } } // -------- RESET SYSTEM -------- void resetSystem() { totalAmount = 0; milestone25 = false; milestone50 = false; milestone75 = false; milestone100 = false; Serial.println("System Reset → Back to 0%"); } // -------- MILESTONE LOGIC -------- void checkMilestones(int percentage) { if (!milestone25 && percentage >= 25) { milestone25 = true; Serial.println("Milestone: 25% reached"); } if (!milestone50 && percentage >= 50) { milestone50 = true; Serial.println("Milestone: 50% reached"); } if (!milestone75 && percentage >= 75) { milestone75 = true; Serial.println("Milestone: 75% reached"); } if (!milestone100 && percentage >= 100) { milestone100 = true; ``` Serial.println("Milestone: Goal reached!"); // -------- CELEBRATION -------- for (int i = 0; i < 3; i++) { digitalWrite(LED_PIN, HIGH); delay(150); digitalWrite(LED_PIN, LOW); delay(150); } delay(500); // pause before reset resetSystem(); // restart cycle ``` } }

    Problem found at this stage: Overflow

    The First Real Bug

    Everything seemed fine, until I crossed the goal.

    Example:

  • Total = 96
  • Add ₹5 → becomes 101 → 101%
  • At this point, I ran into the first issue.


    Issue 1: Overflow

    The system was allowing the total to exceed the goal.

    So instead of stopping at 100, it went beyond:

    Total: 101 | 101%

    This didn’t make sense for the system I was trying to build.

    I wanted the goal to act as a hard boundary, not something that could be exceeded.


    Fix: Clamp to Goal

    I first fixed this by restricting the value:

  • if total > goal → set total = goal
  • So the system would properly stop at 100%.

    Problem found at this stage: Remainder loss

    Issue 2: Remainder Loss (More Subtle Problem)

    After fixing overflow, I noticed another issue.

  • Total = 97
  • Add ₹5
  • Expected behavior:

  • 100 completes the goal
  • remaining ₹2 should carry forward
  • Actual behavior:

  • system reset to 0
  • extra ₹2 was lost


  • Printing process

    So even though overflow was visually fixed, logically it was still incorrect.


    Final Fix: Carry Forward the Remainder

    To solve this properly, I changed the reset logic:

  • calculate overflow = total − goal
  • after goal completion:

  • reset milestones
  • set total = overflow
  • So now:

  • 102 → reset → starts next cycle at 2
  • This made the system behave more like a real piggy bank— no money disappears.


    Prompt used on Chatgpt

    Now that the interaction logic, goal tracking, and reset cycle are working properly, I noticed an issue when the total exceeds the
    goal. For example, if the total is 97 and I add 5, it becomes 102, but earlier the system was resetting to 0 and losing the extra 2.
    I want to fix this so that the overflow is not lost. Instead, when the goal is reached or exceeded, the system should calculate the
    remainder (like 102 → 100 + 2), trigger the goal completion feedback, and then reset while carrying forward the leftover amount into
    the next cycle. The rest of the logic should remain the same—single, double, and long press behavior, milestone tracking, and clean
    serial output. I just want to extend the reset logic so it behaves more realistically and doesn’t discard any value.
    

    Code

    // -------- PIN SETUP -------- #define BUTTON_PIN D8 // button input (INPUT_PULLDOWN) #define LED_PIN D2 // yellow LED // -------- TIMING CONFIG -------- const unsigned long debounceDelay = 50; const unsigned long longPressTime = 1000; const unsigned long doublePressGap = 400; // -------- SYSTEM STATE -------- int totalAmount = 0; int goal = 100; bool milestone25 = false; bool milestone50 = false; bool milestone75 = false; bool milestone100 = false; // -------- BUTTON STATE -------- bool buttonState = LOW; bool lastButtonReading = LOW; unsigned long lastDebounceTime = 0; unsigned long pressStartTime = 0; unsigned long lastReleaseTime = 0; int pressCount = 0; // -------- SETUP -------- void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLDOWN); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); } // -------- LOOP -------- void loop() { int reading = digitalRead(BUTTON_PIN); // -------- DEBOUNCE -------- if (reading != lastButtonReading) { lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > debounceDelay) { if (reading != buttonState) { buttonState = reading; // -------- BUTTON PRESSED -------- if (buttonState == HIGH) { pressStartTime = millis(); } // -------- BUTTON RELEASED -------- else { unsigned long pressDuration = millis() - pressStartTime; // -------- LONG PRESS -------- if (pressDuration >= longPressTime) { handleLongPress(); pressCount = 0; } else { pressCount++; lastReleaseTime = millis(); } } } } // -------- SINGLE / DOUBLE -------- if (pressCount > 0 && (millis() - lastReleaseTime) > doublePressGap) { if (pressCount == 1) { handleSinglePress(); } else if (pressCount == 2) { handleDoublePress(); } pressCount = 0; } lastButtonReading = reading; } // -------- ACTIONS -------- // Single press → ₹1 void handleSinglePress() { addAmount(1); blinkLED(1); } // Double press → ₹2 void handleDoublePress() { addAmount(2); blinkLED(2); } // Long press → ₹5 void handleLongPress() { addAmount(5); // long glow digitalWrite(LED_PIN, HIGH); delay(400); digitalWrite(LED_PIN, LOW); } // -------- CORE LOGIC -------- void addAmount(int value) { totalAmount += value; int percentage = (totalAmount * 100) / goal; Serial.print("Added: "); Serial.print(value); Serial.print(" | Total: "); Serial.print(totalAmount); Serial.print(" | "); Serial.print(percentage); Serial.println("%"); // -------- CHECK GOAL WITH REMAINDER -------- if (totalAmount >= goal) { int overflow = totalAmount - goal; // calculate leftover Serial.println("Milestone: Goal reached!"); // celebration pattern for (int i = 0; i < 3; i++) { digitalWrite(LED_PIN, HIGH); delay(150); digitalWrite(LED_PIN, LOW); delay(150); } delay(500); // -------- RESET WITH CARRY FORWARD -------- totalAmount = overflow; milestone25 = false; milestone50 = false; milestone75 = false; milestone100 = false; Serial.print("System Reset → Carry Forward: "); Serial.println(totalAmount); return; // stop further milestone checks } // -------- NORMAL MILESTONE CHECK -------- checkMilestones(percentage); } // -------- LED HELPER -------- void blinkLED(int times) { for (int i = 0; i < times; i++) { digitalWrite(LED_PIN, HIGH); delay(120); digitalWrite(LED_PIN, LOW); delay(120); } } // -------- MILESTONE LOGIC -------- void checkMilestones(int percentage) { if (!milestone25 && percentage >= 25) { milestone25 = true; Serial.println("Milestone: 25% reached"); } if (!milestone50 && percentage >= 50) { milestone50 = true; Serial.println("Milestone: 50% reached"); } if (!milestone75 && percentage >= 75) { milestone75 = true; Serial.println("Milestone: 75% reached"); } }

    Printing process

    Why This Step Mattered

    At this stage, I had:

  • a reliable input system
  • a running total
  • goal-based logic
  • proper handling of edge cases (overflow + remainder)
  • state transitions (milestones + reset)

  • Even though nothing was connected to server or the app yet, this became the core engine of the entire project.


    Transition

    Now that the device could generate structured and reliable data, the next question was:

    how do I get this data out of the ESP32?


    Connecting the System - WiFi and Firebase

    Up to this point, the system could generate data reliably, but it was still confined to the ESP32.

    Every interaction - button press, total update, milestone - was happening internally, visible only through the Serial Monitor.

    The next step was to move that data outside the device.


    Step 1: Getting the ESP32 Online

    I started by testing a basic WiFi connection.

    The goal here wasn’t complexity, it was verification:

  • can the board connect to a network?
  • does it stay connected?
  • can I see a valid IP address?
  • Once the Serial Monitor showed a successful connection and an IP address, it confirmed that the ESP32 was now part of a network, not just a standalone device.

    Wifi Test Code

    // -------- WIFI TEST -------- #include <WiFi.h> const char* ssid = "MY SSID"; const char* password = "PASSWORD"; void setup() { Serial.begin(115200); delay(1000); Serial.println("Connecting to WiFi..."); WiFi.begin(ssid, password); // wait until connected while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nConnected!"); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); } void loop() { // nothing here }


    Integrating WiFi into the Existing System

    After confirming WiFi worked independently, I added it into my existing button logic code.

    Wifi integrated code

    // -------- WIFI -------- #include <WiFi.h> const char* ssid = "MiCho"; const char* password = "Hello1234"; // -------- PIN SETUP -------- #define BUTTON_PIN D8 #define LED_PIN D2 // -------- TIMING CONFIG -------- const unsigned long debounceDelay = 50; const unsigned long longPressTime = 1000; const unsigned long doublePressGap = 400; // -------- SYSTEM STATE -------- int totalAmount = 0; int goal = 100; bool milestone25 = false; bool milestone50 = false; bool milestone75 = false; // -------- BUTTON STATE -------- bool buttonState = LOW; bool lastButtonReading = LOW; unsigned long lastDebounceTime = 0; unsigned long pressStartTime = 0; unsigned long lastReleaseTime = 0; int pressCount = 0; // -------- SETUP -------- void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLDOWN); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // -------- WIFI CONNECT -------- Serial.println("Connecting to WiFi..."); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi Connected!"); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); } // -------- LOOP -------- void loop() { int reading = digitalRead(BUTTON_PIN); // -------- DEBOUNCE -------- if (reading != lastButtonReading) { lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > debounceDelay) { if (reading != buttonState) { buttonState = reading; if (buttonState == HIGH) { pressStartTime = millis(); } else { unsigned long pressDuration = millis() - pressStartTime; if (pressDuration >= longPressTime) { handleLongPress(); pressCount = 0; } else { pressCount++; lastReleaseTime = millis(); } } } } if (pressCount > 0 && (millis() - lastReleaseTime) > doublePressGap) { if (pressCount == 1) { handleSinglePress(); } else if (pressCount == 2) { handleDoublePress(); } pressCount = 0; } lastButtonReading = reading; } // -------- ACTIONS -------- void handleSinglePress() { addAmount(1); blinkLED(1); } void handleDoublePress() { addAmount(2); blinkLED(2); } void handleLongPress() { addAmount(5); digitalWrite(LED_PIN, HIGH); delay(400); digitalWrite(LED_PIN, LOW); } // -------- CORE LOGIC -------- void addAmount(int value) { totalAmount += value; int percentage = (totalAmount * 100) / goal; Serial.print("Added: "); Serial.print(value); Serial.print(" | Total: "); Serial.print(totalAmount); Serial.print(" | "); Serial.print(percentage); Serial.println("%"); // -------- GOAL CHECK WITH REMAINDER -------- if (totalAmount >= goal) { int overflow = totalAmount - goal; Serial.println("Milestone: Goal reached!"); for (int i = 0; i < 3; i++) { digitalWrite(LED_PIN, HIGH); delay(150); digitalWrite(LED_PIN, LOW); delay(150); } delay(500); totalAmount = overflow; milestone25 = false; milestone50 = false; milestone75 = false; Serial.print("System Reset → Carry Forward: "); Serial.println(totalAmount); return; } checkMilestones(percentage); } // -------- LED -------- void blinkLED(int times) { for (int i = 0; i < times; i++) { digitalWrite(LED_PIN, HIGH); delay(120); digitalWrite(LED_PIN, LOW); delay(120); } } // -------- MILESTONES -------- void checkMilestones(int percentage) { if (!milestone25 && percentage >= 25) { milestone25 = true; Serial.println("Milestone: 25% reached"); } if (!milestone50 && percentage >= 50) { milestone50 = true; Serial.println("Milestone: 50% reached"); } if (!milestone75 && percentage >= 75) { milestone75 = true; Serial.println("Milestone: 75% reached"); } }

    Printing process

    Introducing Firebase (First Time Use)

    Now that the device could connect to the internet, I needed a way to store and share data.

    Printing process

    The requirement was specific:

  • the ESP32 should be able to write data
  • the app should be able to read it
  • both should stay in sync
  • This is where Firebase came in.

    I tried watching Youtube videos but it wasn't much helpful,so, eventually, I just kept googling whenever I got stuck and followed what the results said.


    What Firebase Is (In This Context)

    Instead of treating Firebase as a generic backend, I approached it as:

    a shared data layer between the device and the app

    It acts like a central state that both sides can access.

    In this system:

  • ESP32 updates values like total, percentage, and last coin
  • the app reads and displays those values
  • the app writes the goal
  • the ESP32 reads that goal and uses it for calculations
  • Everything passes through Firebase.


    Creating the Firebase Project

    Printing process

    I created a new Firebase project for this system.

  • Project name: Smart Piggy
  • Default setup (Spark plan)

  • Setting Up Realtime Database

    Printing process

    From the Firebase console, I moved to:

  • Build → Realtime Database
  • Then configured:

  • Region: Singapore (asia-southeast1)
  • Mode: Test mode

  • Printing process

    Test mode allowed open read/write access, which made initial development easier.


    Getting Configuration Details

    Printing process

    After setting up the database, I needed the credentials required by the ESP32:

  • API Key
  • Database URL
  • These come from adding an app to the Firebase project.

    Printing process

    This configuration is what allows the ESP32 to authenticate and communicate with Firebase.

    Enabling Authentication

    To allow the ESP32 to connect without user login, I enabled:

  • Anonymous Authentication
  • This step was necessary for the device to perform read/write operations programmatically.


    First Test - ESP32 Writes to Firebase

    Before sending real system data, I tested a minimal case:

  • ESP32 sends a simple value (123)
  • Firebase receives and stores it

  • Printing process

    Firebase Connectivity Test (ESP32)

    // -------- WIFI -------- #include <WiFi.h> // -------- FIREBASE -------- #include // helpers (required by the library) #include "addons/TokenHelper.h" #include "addons/RTDBHelper.h" // your WiFi const char* ssid = "MiCho"; const char* password = "Hello1234"; // your Firebase (from config) #define API_KEY "AIzaSyDpefz07KOuYyj0mZ1YtvJkm5g3z5HU0-I" #define DATABASE_URL "https://smart-piggy-effc6-default-rtdb.asia-southeast1.firebasedatabase.app/" // Firebase objects FirebaseData fbdo; FirebaseAuth auth; FirebaseConfig config; void setup() { Serial.begin(115200); // -------- WIFI CONNECT -------- WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi connected"); Serial.print("IP: "); Serial.println(WiFi.localIP()); // -------- FIREBASE CONFIG -------- config.api_key = API_KEY; config.database_url = DATABASE_URL; // REQUIRED: token handling config.token_status_callback = tokenStatusCallback; // -------- SIGN UP (ANONYMOUS) -------- if (Firebase.signUp(&config, &auth, "", "")) { Serial.println("Firebase signup OK"); } else { Serial.printf("Signup failed: %s\n", config.signer.signupError.message.c_str()); } // -------- START FIREBASE -------- Firebase.begin(&config, &auth); Firebase.reconnectWiFi(true); } void loop() { // only run when Firebase is ready if (Firebase.ready()) { // send test value if (Firebase.RTDB.setInt(&fbdo, "/test/value", 123)) { Serial.println("Sent: 123"); } else { Serial.print("FAILED: "); Serial.println(fbdo.errorReason()); } } delay(5000); }


    Moving from Test Data to System Data

    After confirming communication worked, I replaced the test value with actual values from the system:

  • total
  • percentage
  • percentage
  • These were sent only when a valid interaction occurred.


    Transition

    With Firebase now storing real data, the next step was:

    how to build an interface that reads and displays this data


    Kodular - Turning Data into an Interface

    Printing process Printing process

    Kodular looked very simple and practical at first. I watched this Youtube Tutorial to get a basic idea.

    Printing process

    Two main sections:

  • Designer (felt familiar immediately.)
  • Blocks (… not so much.)

  • Designer - The familiar Part

    I started by placing the components I knew I would need.

    Printing process

    3 labels:

  • total
  • percentage
  • last coin
  • a progress bar
  • (not exactly what I wanted visually, but enough to test)

  • Firebase Database component
  • (invisible, but important)

    This part was smooth. It felt like arranging things on a canvas.

    At this stage, everything looked ready, but nothing actually worked.


    Blocks - Where Everything Changed

    Printing process

    Then I switched to the Blocks section.

    And that’s where everything stopped making sense.

    It looks like a simple grid with draggable tiles, but:

    Printing process

  • I didn’t know what to pick
  • I didn’t know where to place it
  • I didn’t know how anything connects
  • This is where the logic of the app lives, but there was no clear starting point.


    What Made It Difficult

    Kodular works like puzzle pieces:

  • each block is a piece of logic
  • pieces only fit in certain ways
  • and even if they fit, it doesn’t mean they’re correct

  • Printing process

    Sometimes, the hardest part wasn’t just logic, it was finding the right block in the first place

    Sometimes I knew what I wanted to do, but:

  • couldn’t find the block
  • or found it but didn’t know how to use it
  • or connected it and nothing happened

  • Building the Logic (Piece by Piece)

    Receiving the Data

    The data comes through:

    Firebase.GotValue(tag, value)

    At first, this part was confusing.

  • where does this trigger from?
  • what exactly is inside value?

  • Printing process

    It took time to understand that this is the point where Firebase responds.


    Making Sense of the Data

    Initially, I tried to fetch individual values.

    That didn’t work well.

    Then I switched to fetching everything at once.

    tag = ""

    This returns a full dataset.

    From there:

  • value["total"]
  • value["percentage"]
  • value["lastCoin"]

  • // -------- WIFI -------- #include <WiFi.h> // -------- FIREBASE -------- #include <Firebase_ESP_Client.h> #include "addons/TokenHelper.h" #include "addons/RTDBHelper.h" // -------- WIFI -------- 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; // -------- PIN SETUP -------- #define BUTTON_PIN D8 #define LED_PIN D2 // -------- TIMING CONFIG -------- const unsigned long debounceDelay = 50; const unsigned long longPressTime = 1000; const unsigned long doublePressGap = 400; // -------- SYSTEM STATE -------- int totalAmount = 0; int goal = 100; bool milestone25 = false; bool milestone50 = false; bool milestone75 = false; // -------- BUTTON STATE -------- bool buttonState = LOW; bool lastButtonReading = LOW; unsigned long lastDebounceTime = 0; unsigned long pressStartTime = 0; unsigned long lastReleaseTime = 0; int pressCount = 0; // -------- SETUP -------- void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLDOWN); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // -------- WIFI -------- Serial.println("Connecting to WiFi..."); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi Connected!"); Serial.print("IP Address: "); 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("Firebase ready"); } // -------- LOOP -------- void loop() { int reading = digitalRead(BUTTON_PIN); // -------- DEBOUNCE -------- if (reading != lastButtonReading) { lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > debounceDelay) { if (reading != buttonState) { buttonState = reading; if (buttonState == HIGH) { pressStartTime = millis(); } else { unsigned long pressDuration = millis() - pressStartTime; // LONG PRESS → ₹5 if (pressDuration >= longPressTime) { handleLongPress(); pressCount = 0; } else { pressCount++; lastReleaseTime = millis(); } } } } // SINGLE / DOUBLE PRESS DETECTION if (pressCount > 0 && (millis() - lastReleaseTime) > doublePressGap) { if (pressCount == 1) { handleSinglePress(); } else if (pressCount == 2) { handleDoublePress(); } pressCount = 0; } lastButtonReading = reading; } // -------- ACTIONS -------- void handleSinglePress() { addAmount(1); blinkLED(1); } void handleDoublePress() { addAmount(2); blinkLED(2); } void handleLongPress() { addAmount(5); digitalWrite(LED_PIN, HIGH); delay(400); digitalWrite(LED_PIN, LOW); } // -------- FIREBASE (ATOMIC UPDATE) -------- void sendToFirebase(int total, int percentage, int lastCoin) { if (Firebase.ready()) { FirebaseJson json; json.set("total", total); json.set("percentage", percentage); json.set("lastCoin", lastCoin); // single atomic write Firebase.RTDB.setJSON(&fbdo, "/piggy", &json); Serial.println("Data sent to Firebase (JSON)"); } } // -------- CORE LOGIC -------- void addAmount(int value) { totalAmount += value; int percentage = (totalAmount * 100) / goal; Serial.print("Added: "); Serial.print(value); Serial.print(" | Total: "); Serial.print(totalAmount); Serial.print(" | "); Serial.print(percentage); Serial.println("%"); // single Firebase update sendToFirebase(totalAmount, percentage, value); // -------- GOAL LOGIC -------- if (totalAmount >= goal) { int overflow = totalAmount - goal; Serial.println("Milestone: Goal reached!"); for (int i = 0; i < 3; i++) { digitalWrite(LED_PIN, HIGH); delay(150); digitalWrite(LED_PIN, LOW); delay(150); } delay(500); totalAmount = overflow; milestone25 = false; milestone50 = false; milestone75 = false; Serial.print("System Reset → Carry Forward: "); Serial.println(totalAmount); return; } checkMilestones(percentage); } // -------- LED -------- void blinkLED(int times) { for (int i = 0; i < times; i++) { digitalWrite(LED_PIN, HIGH); delay(120); digitalWrite(LED_PIN, LOW); delay(120); } } // -------- MILESTONES -------- void checkMilestones(int percentage) { if (!milestone25 && percentage >= 25) { milestone25 = true; Serial.println("Milestone: 25% reached"); } if (!milestone50 && percentage >= 50) { milestone50 = true; Serial.println("Milestone: 50% reached"); } if (!milestone75 && percentage >= 75) { milestone75 = true; Serial.println("Milestone: 75% reached"); } }


    Printing process

    Now things finally started making sense.


    Connecting It to the UI

    Once I could access the values:

  • labels were updated
  • progress bar was linked to percentage
  • First Working State

    At this point, the app showed:

  • total
  • total
  • last coin


  • Printing process

    And everything matched what the ESP32 was sending.

    This was the first time I could see the entire system working in one place.


    What This Phase Really Tested

  • figuring out a new system
  • being okay with not understanding things immediately
  • staying with it long enough for it to click

  • Printing process

    I just wanted to finish this before dinner.
    I didn’t want to go to sleep thinking about Kodular blocks.


    Transition

    At this stage, the app could read and display data correctly.

    That felt like a big win.

    But the system was still one-directional.

    The next step was to send data back from the app:

    setting the goal from the interface


    Goal Loop - When Everything Broke Again

    Sending Goal from App

    I added:

  • a textbox → to enter goal
  • a button → to send it


  • Printing process

    Logic:

  • take input
  • store in Firebase under piggy/goal
  • This part seemed to work.

    The value showed up in Firebase.

    Printing process

    The First Problem - Updates Not Consistent

    Even though the goal was updating, it wasn’t consistent everywhere.

  • sometimes ESP32 reflected it
  • sometimes it didn’t
  • app wasn’t reliably updating

  • Printing process

    This is where I realized something was missing.


    The Missing Piece - Clock

    Up until now, the app wasn’t actively checking for updates.

    So I added a Clock component.

  • it runs at intervals
  • requests data from Firebase repeatedly


  • Printing process

    This made a big difference.

    Now:

  • goal updates were consistent
  • app stayed in sync

  • New Problem - Everything Else Broke

    Printing process

    As soon as the Clock started working properly:

  • total stopped updating on the app
  • last coin stopped updating
  • percentage stopped updating
  • Printing process

    So fixing one thing broke three others.


    Trying to Fix That

    I started debugging:

  • checked Firebase values
  • checked block connections
  • adjusted logic
  • Some things improved.

    But then:

  • goal stopped updating properly
  • or Firebase didn’t reflect changes
  • or values were inconsistent again

  • Printing process

    I could see the system almost working, but I couldn’t reach it.


    What Was Actually Happening

    At some point, the system started overwriting values.

  • multiple writes happening
  • inconsistent timing
  • data getting replaced unexpectedly
  • So even though everything was connected, it wasn’t coordinated.


    Reset and Rebuild

    Printing process

    At this point, I stopped trying to patch things.

    I went back to Firebase and:

  • deleted existing data
  • rebuilt the structure manually
  • Then I reconnected everything step-by-step.


    Re-testing the Flow

    I tested in order:

    1.Set goal from app

  • → reflected in Firebase
  • → ESP32 reads correctly
  • 2.Press button

  • → total updates
  • → percentage updates
  • → last coin updates
  • 3.App reflects everything correctly

    This time, everything stayed consistent.


    Key Fixes That Made It Work

    Looking back, a few things fixed the system:

    1.Clock for Controlled Updates

  • app actively checks for data
  • keeps UI in sync
  • 2.Clean Firebase Structure

  • removing conflicting/old values
  • rebuilding from scratch
  • 3.Avoiding Overwrites

  • controlled when data is written
  • avoided unnecessary updates
  • 4.Event-Based Serial Feedback

  • ESP32 sends updates only when interaction happens
  • no continuous unnecessary operations

  • The Moment It Finally Worked

    After all this:

  • goal → updates correctly
  • ESP32 → reads correctly
  • button input → updates values
  • Firebase → stays consistent
  • app → reflects everything in real time

  • Printing process

    This is where I finally reached that “end of the tunnel”.


    Final Working Loop

    App → sets goal
    ↓
    Firebase stores goal
    ↓
    ESP32 reads goal
    ↓
    User interaction
    ↓
    ESP32 updates values
    ↓
    Firebase updated
    ↓
    Clock triggers update
    ↓
    App reflects new values
    

    Printing process

    Prompt used on Chatgpt

    I had already connected my ESP32 to WiFi and Firebase and was sending the total savings, percentage completed, and last coin value. I wanted to connect it with my Kodular app so that the savings goal set in the app could be read by the ESP32 through Firebase.
    
    Can you help me modify the code so the ESP32 periodically reads the goal value from Firebase, updates it only when it changes, and keeps the app and device synchronized? The code should also continue handling button inputs, calculate the saving percentage, and update the total, percentage, last coin, and goal values back to Firebase without affecting the responsiveness of the system.
    

    Code

    // -------- WIFI -------- #include <WiFi.h> // -------- FIREBASE -------- #include <Firebase_ESP_Client.h> #include "addons/TokenHelper.h" #include "addons/RTDBHelper.h" // -------- WIFI -------- 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; // -------- PIN SETUP -------- #define BUTTON_PIN D8 #define LED_PIN D2 // -------- TIMING -------- const unsigned long debounceDelay = 50; const unsigned long longPressTime = 1000; const unsigned long doublePressGap = 400; // -------- FIREBASE TIMING -------- unsigned long lastFirebaseRead = 0; const unsigned long firebaseInterval = 2000; // -------- STATE -------- int totalAmount = 0; int goal = 0; int lastGoal = -1; // -------- BUTTON STATE -------- bool buttonState = LOW; bool lastButtonReading = LOW; unsigned long lastDebounceTime = 0; unsigned long pressStartTime = 0; unsigned long lastReleaseTime = 0; int pressCount = 0; // -------- SETUP -------- void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLDOWN); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); Serial.println("Connecting to WiFi..."); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi Connected!"); Serial.println(WiFi.localIP()); 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("Firebase ready"); } // -------- LOOP -------- void loop() { // -------- READ GOAL (ONLY IF CHANGED) -------- if (Firebase.ready() && millis() - lastFirebaseRead > firebaseInterval) { lastFirebaseRead = millis(); if (Firebase.RTDB.get(&fbdo, "/piggy/goal")) { int newGoal = 0; String type = fbdo.dataType(); if (type == "int") newGoal = fbdo.intData(); else if (type == "float") newGoal = (int)fbdo.floatData(); else if (type == "double") newGoal = (int)fbdo.doubleData(); else if (type == "string") newGoal = fbdo.stringData().toInt(); if (newGoal != lastGoal) { goal = newGoal; lastGoal = newGoal; Serial.print("Goal updated: "); Serial.println(goal); } } } // -------- BUTTON LOGIC -------- int reading = digitalRead(BUTTON_PIN); if (reading != lastButtonReading) { lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > debounceDelay) { if (reading != buttonState) { buttonState = reading; if (buttonState == HIGH) { pressStartTime = millis(); } else { unsigned long pressDuration = millis() - pressStartTime; if (pressDuration >= longPressTime) { handleLongPress(); pressCount = 0; } else { pressCount++; lastReleaseTime = millis(); } } } } if (pressCount > 0 && (millis() - lastReleaseTime) > doublePressGap) { if (pressCount == 1) { handleSinglePress(); } else if (pressCount == 2) { handleDoublePress(); } pressCount = 0; } lastButtonReading = reading; } // -------- BUTTON ACTIONS -------- void handleSinglePress() { addAmount(1); blinkLED(1); } void handleDoublePress() { addAmount(2); blinkLED(2); } void handleLongPress() { addAmount(5); digitalWrite(LED_PIN, HIGH); delay(400); digitalWrite(LED_PIN, LOW); } // -------- FIREBASE WRITE (SAFE: NO OVERWRITE) -------- void sendToFirebase(int total, int percentage, int lastCoin) { if (Firebase.ready()) { Firebase.RTDB.setInt(&fbdo, "/piggy/total", total); Firebase.RTDB.setInt(&fbdo, "/piggy/percentage", percentage); Firebase.RTDB.setInt(&fbdo, "/piggy/lastCoin", lastCoin); Firebase.RTDB.setInt(&fbdo, "/piggy/goal", goal); // keep goal synced Serial.println("Data updated (safe write)"); } } // -------- CORE LOGIC -------- void addAmount(int value) { totalAmount += value; int percentage = 0; if (goal > 0) { percentage = (totalAmount * 100) / goal; } Serial.print("Added: "); Serial.print(value); Serial.print(" | Total: "); Serial.print(totalAmount); Serial.print(" | "); Serial.print(percentage); Serial.println("%"); sendToFirebase(totalAmount, percentage, value); } // -------- LED -------- void blinkLED(int times) { for (int i = 0; i < times; i++) { digitalWrite(LED_PIN, HIGH); delay(120); digitalWrite(LED_PIN, LOW); delay(120); } }


    Design Decisions and Development Process

    The button interaction logic was not something that emerged during this week's assignment. The idea had already been established much earlier in the project planning stage. It was inspired by the coin-identification interaction in the JumpStart 1st Grade vending machine game, which I referenced in the Applications and Implications section.

    For my own implementation, I wanted the child to actively identify the coin denomination before it was accepted. Since I already had a PCB from PCB Production Week containing a XIAO ESP32-C6, a push button, and an LED, I used that board as the starting point for developing and testing the interaction logic.

    I had previously written simple button-based programs that detected presses and displayed results through the Serial Monitor. Building on that foundation, I explored how a single button could represent multiple coin denominations. Because only one button was available on the board, I decided to map different button interactions to different values:

    This interaction model was developed before the Embedded Programming assignment began and served as the basis for the later coin-input system. After discussing the concept with the instructor, I was encouraged to continue developing this direction further.

    The mobile application and cloud synchronization approach came from discussions with the instructor. Based on the type of project I was building, Kodular and Firebase were suggested as a practical way to allow parents to set savings goals and monitor progress remotely. At that stage, both tools were completely new to me, so I relied heavily on YouTube tutorials, online documentation, and experimentation to understand how they worked.

    The development process was highly iterative. The button interaction system, Firebase communication, and Kodular blocks were not developed as separate stages. Instead, they evolved together through continuous testing and troubleshooting. Changes in one part of the system often required modifications in the others.

    AI tools, particularly ChatGPT, were used extensively throughout this process. Rather than using generated code unchanged, I used AI to help understand unfamiliar concepts, modify existing programs, troubleshoot errors, and adapt examples to fit the specific requirements of Penny Pal. Many implementation details, including goal tracking behaviour, percentage calculations, reset logic, Firebase data handling, and interaction flow were refined through repeated testing and adjustment until they behaved as intended within the overall system.


    Learnings and Insights

    What helped me most was breaking the system into smaller parts and building step by step, button → logic → WiFi → Firebase → app. Making sure each stage worked before moving forward kept things manageable. Serial output was my constant checkpoint, and switching to event-based updates made the system much more stable. When Firebase started behaving inconsistently, resetting it and rebuilding cleanly turned out to be more effective than trying to fix everything.

    The difficult part was understanding how Kodular works, especially the Blocks section. Nothing was intuitive, and figuring out how blocks connect and where to find them took time. The shift to event-driven thinking was also not immediate. Debugging became harder once everything was connected-when something failed, it was unclear whether the issue was in the app, Firebase, or the ESP32. Synchronization issues and value overwriting made this even more confusing.

    What this project really showed me is that a system is not just about individual components working, but about how well they communicate. Real-time systems need controlled data flow, and event-driven logic behaves very differently from linear code. Also, sometimes it’s more efficient to reset and rebuild than to keep debugging a broken state.

    If I were to take this further, I would improve the app interface, especially the visual feedback. The current progress bar doesn't make any sense, I need to do something about it.


    Group Assignment

    Comparison of Interface and Application Programming Tools

    As part of the group assignment, different interface and application programming approaches were explored. Each project used a different communication method depending on its requirements, ranging from web-based dashboards to game engines and mobile applications. The comparison helped in understanding the strengths and limitations of each approach.


    Printing process

    Project Interface Tool Communication Advantages
    Butterflies HTML / CSS / JavaScript WebSocket Real-time communication
    DO-DO HTML / CSS / JavaScript HTTP REST Simple and browser-based
    Pixel Game Godot Engine USB HID Direct hardware control
    Pixel Plant Pygame Serial (PySerial) Fast local communication
    Smart Piggy Kodular Firebase Real-time cloud sync
    Time Sync Web App BLE + UART Wireless synchronization

    Group Assignment Link: View Group Assignment

    Evaluation

    As part of the group assignment, each of us explored a different way of creating an interface and communicating with hardware. This allowed us to compare several tools and communication methods such as WebSocket, HTTP, USB HID, Serial Communication, BLE, and Firebase.

    For my Smart Piggy project, I chose Firebase Realtime Database with Kodular because it was simple to implement and suited the needs of the project. The piggy bank only needs to update when a goal is set or when a coin is added, so a continuous communication method like WebSocket was not necessary. Firebase also stores the data online, making it easy to synchronize information between the mobile app and the ESP32-C6 while preserving progress even after a restart.

    Comparing the different approaches helped me understand that the choice of interface and communication method depends on the requirements of the project, and Firebase was the most suitable solution for Smart Piggy.


    Source Files

  • Press Logic Module

  • WiFi Test Code
  • WiFi Integrated Code

  • Goal Overflow
  • Goal with Remainder
  • Goal Control test

  • Firebase Integration Mock
  • Firebase Data Sync
  • Firebase Connectivity Test

  • Final Integrated Code

  • Smart_Piggy_App.aia
  • Smart_Piggy_App.apk