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:
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:
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:
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
App
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
Right now, I had:
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:
At that time, I had already experimented with:
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:
And every interaction should:
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:
This made the system much more stable from the beginning.
First Working Behavior
At this stage:
This was the first time I had actual data being generated by the system.
Prompt used on Chatgpt
I am working with a XIAO ESP32C6 board and I want to build on a button interaction system similar to the simple edge-detection code I previously used successfully (where a button press toggled LED behavior reliably). Using that as a reference for stable button handling, I now want to extend the logic. The hardware setup is: a push button connected to pin D8 and a yellow LED connected to pin D2. I want to use the button to simulate coin inputs with three types of interactions: a single press should represent 1 rupee and trigger a single LED blink, a double press should represent 2 rupees and trigger a double blink, and a long press (around 1 second or more) should represent 5 rupees and trigger a longer LED glow. The system should use proper edge detection instead of continuous reading, include basic debounce handling, and use timing logic (millis) to distinguish between single, double, and long presses without conflicts (for example, a long press should not also register as a single press). The code should maintain a running total and print updates to the Serial Monitor only when an 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:
From this, I calculated:
Now the system wasn’t just counting, it had context.
Adding Milestones
To make progress visible, I introduced milestone checks:
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 ``` } }
The First Real Bug
Everything seemed fine, until I crossed the goal.
Example:
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:
So the system would properly stop at 100%.
Issue 2: Remainder Loss (More Subtle Problem)
After fixing overflow, I noticed another issue.
Expected behavior:
Actual behavior:
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:
after goal completion:
So now:
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"); } }
Why This Step Mattered
At this stage, I had:
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:
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"); } }
Introducing Firebase (First Time Use)
Now that the device could connect to the internet, I needed a way to store and share data.
The requirement was specific:
This is where Firebase came in.
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:
Everything passes through Firebase.
Creating the Firebase Project
I created a new Firebase project for this system.
Setting Up Realtime Database
From the Firebase console, I moved to:
Then configured:
Test mode allowed open read/write access, which made initial development easier.
Getting Configuration Details
After setting up the database, I needed the credentials required by the ESP32:
These come from adding an app to the Firebase project.
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:
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:
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:
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
Kodular looked very simple and practical at first.
Two main sections:
Designer - The familiar Part
I started by placing the components I knew I would need.
3 labels:
(not exactly what I wanted visually, but enough to test)
(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
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:
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:
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:
Building the Logic (Piece by Piece)
Receiving the Data
The data comes through:
Firebase.GotValue(tag, value)
At first, this part was confusing.
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:
// -------- 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"); } }
Now things finally started making sense.
Connecting It to the UI
Once I could access the values:
First Working State
At this point, the app showed:
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
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:
Logic:
This part seemed to work.
The value showed up in Firebase.
The First Problem - Updates Not Consistent
Even though the goal was updating, it wasn’t consistent everywhere.
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.
This made a big difference.
Now:
New Problem - Everything Else Broke
As soon as the Clock started working properly:
So fixing one thing broke three others.
Trying to Fix That
I started debugging:
Some things improved.
But then:
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.
So even though everything was connected, it wasn’t coordinated.
Reset and Rebuild
At this point, I stopped trying to patch things.
I went back to Firebase and:
Then I reconnected everything step-by-step.
Re-testing the Flow
I tested in order:
1.Set goal from app
2.Press button
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
2.Clean Firebase Structure
3.Avoiding Overwrites
4.Event-Based Serial Feedback
The Moment It Finally Worked
After all this:
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
Prompt used on Chatgpt
Continue building the Smart Piggy ESP32 system from the previous stage where WiFi and Firebase are already connected and
the device sends total, percentage, and lastCoin values. Now extend the system into a stable two-way integration with a
Kodular app that sets a savings goal via Firebase.
Write complete ESP32 (Arduino) code that:
- Reads the goal value from Firebase (`/piggy/goal`) at fixed intervals using millis (non-blocking), not continuous polling
- Updates the goal variable only when it changes, and prints “Goal updated: X” to Serial only on change
- Handles Firebase data types robustly (int, float, double, string → convert to int)
- Maintains responsive button input logic with debounce:
- Single press = +1
- Double press = +2
- Long press = +5
- Keeps button handling independent of Firebase calls (no blocking delays except short LED feedback)
- Calculates percentage as `(total * 100) / goal`, with a safeguard for goal = 0
- Writes data to Firebase WITHOUT overwriting the entire node:
- Use individual writes (`setInt`) for:
- `/piggy/total`
- `/piggy/percentage`
- `/piggy/lastCoin`
- `/piggy/goal` (to keep it synced)
- Uses the Firebase ESP Client library with proper initialization (API key, database URL, anonymous sign-in)
- Includes Serial debug messages for:
- WiFi connection
- Firebase readiness
- Goal updates (only when changed)
- Amount added and percentage
- Firebase write success
- Uses the following structure in Firebase:
```
piggy/
goal
total
percentage
lastCoin
```
- Keeps the loop efficient and stable so that real-time button presses are never delayed by network operations
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); } }
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.