Front view of the smart cup coaster consisting of a load cell, a speaker, and buttons, connected to Arduino and speaker by jumpers and breadboards.
My project is a smart cup coaster that helps me mix drinks based on set proportions. It consists of a load cell and a speaker. First, I will put a cup on the coaster and zero the weight of the cup. Then, I can pick a drink (let's say latte) I want to make by pushing the corresponding button, and start pouring liquid (let's say milk) into a cup, which is placed on the platform attached to a load cell. Once it reaches a certain weight, the speaker will play the "ting" sound to remind me to stop. Then, I can start adding the second liquid (let's say coffee). Similarly, the coaster will remind me when it reaches the perfect proportion for latte.
DFPlayer Mini with SD card inserted
Three buttons for different drinks
speaker attached outside of the acrylic piece
load cell with platform mounted
In this video, I am showing the complete interaction with the smart cup coaster. First, I placed a cup on the coaster and clicked the reset button to zero the weight of the cup. Then, I chose the drink I wanted to mix by clicking the corresponding button. Then, I started pouring drinks into the cup, and once I heard the "ding" sound, I stopped, and I did the same thing for another component. After I heard "ding" again, a perfect mixed drink was done.
connected load cell amplifier to arduino through breadboard
all the mechanics are conncted but not organized in the box yet
a load cell base in Anycubic Slicer
connected buttons for picking drinks
I was building this project based on what I had for domain specific week 2, so the whole process is relatively smooth since I already knew how to wire a speaker and a load cell. what I spent most time on is just thinking about what additional features I should add to it and what the final appearean will be.
I received very helpful comments from my peers. Yutong gives me a comment "Having a smaller enclosed container for the electronics might be helpful in case of drink spillage.", which I think is good point because when I was doing tests and demos, I was worried about spill water on my electronics, if I can have a smaller and more sealed container for them, it would be much safer. I also agree with Hua's comment "Having a LCD screen or something to exhibit the amount of liquid would be nice." Currenly, there is no feedback for the user except the "ding" sound. It would more clear to the user if it is showing what drink we are mixing and if I should start with milk or coffee or whatever.
I am generally happy with the result, and it actually realizes most of the features I want to have. Really glad that there weren’t too many technical difficulties. However, I don’t think I have left enough time for the fabrication part, so I didn’t get to design a prettier coaster cover or embed the mechanics into a cup. Moreover, when i was doing this project, I considered it as a very utilitarian device without thinking about giving it more creative features or even having some person-like characters the critics mentioned, which makes me think about the boundary between a physical assistive design versus an electrical art piece. If i can keep developing this, I would really want to see it being part of a cup, and figure out what I can do to shrink the space needed for the mechanics, such that the cup could look lighter and portable.
/*
smart cup coaster
Maggie Guo
Code is written by ChatGPT. It is for Arduino Uno. I set three different sets of thresholds corresponding to three different drinks. Each set includes a lower trigger and an upper trigger. The soundtrack is stored in the SD card. Once the trigger is met, it will play the soundtrack. There is also an extra handleTareEdge() function for zeroing current reading of the weight.
pin mapping as above!
*/
#include <SoftwareSerial.h>
#include <DFRobotDFPlayerMini.h>
#include "HX711.h"
// ---- DFPlayer on D10/D11 ----
SoftwareSerial mp3Serial(10, 11); // D10=RX, D11=TX
DFRobotDFPlayerMini mp3;
bool dfp_ok = false;
// ---- HX711 on D4/D5 ----
#define HX_DT 4
#define HX_SCK 5
HX711 scale;
// ---- Buttons (wired to GND, use INPUT_PULLUP) ----
#define BTN_A 2 // Profile A: 5g + 100g
#define BTN_B 3 // Profile B: 50g + 100g
#define BTN_C 6 // Profile C: 80g + 100g
// ---- Calibration ----
float CALIBRATION_FACTOR = 7050.0f;
// ---- DFPlayer config ----
const int TRACK = 1; // plays MP3/0001.mp3
const int START_VOLUME = 25; // 0–30 range, 25 is fairly loud
// ---- Profiles ----
struct Profile {
const char* name;
float t1_g;
float t2_g;
};
Profile PROFILES[3] = {
{"A (5g, 100g)", 5.0f, 100.0f},
{"B (50g, 100g)", 50.0f, 100.0f},
{"C (80g, 100g)", 80.0f, 100.0f}
};
int current_profile = 0;
bool armed_t1 = true;
bool armed_t2 = true;
const float REARM_MARGIN_G = 2.0f; // re-arm when below (min threshold − 2 g)
unsigned long lastBtnCheck = 0;
const unsigned long BTN_DEBOUNCE_MS = 80;
// ---------- Helpers ----------
void printProfileBanner() {
Serial.println();
Serial.println(F("========================================"));
Serial.print(F(" Active Profile: "));
Serial.println(PROFILES[current_profile].name);
Serial.println(F(" Lower trigger → plays at first threshold"));
Serial.println(F(" Upper trigger → plays at 100 g"));
Serial.println(F("========================================"));
Serial.println();
}
void selectProfile(int idx) {
if (idx < 0 || idx > 2) return;
if (current_profile == idx) return; // already active
current_profile = idx;
armed_t1 = true;
armed_t2 = true;
printProfileBanner();
}
bool buttonPressed(int pin) {
return digitalRead(pin) == LOW;
}
void checkButtons() {
unsigned long now = millis();
if (now - lastBtnCheck < BTN_DEBOUNCE_MS) return;
lastBtnCheck = now;
if (buttonPressed(BTN_A)) {
selectProfile(0);
} else if (buttonPressed(BTN_B)) {
selectProfile(1);
} else if (buttonPressed(BTN_C)) {
selectProfile(2);
}
}
float rearmThresholdFor(Profile p) {
float base = min(p.t1_g, p.t2_g) - REARM_MARGIN_G;
return base < 0.0f ? 0.0f : base;
}
// ---------- Setup ----------
void setup() {
Serial.begin(115200);
while (!Serial) {}
pinMode(BTN_A, INPUT_PULLUP);
pinMode(BTN_B, INPUT_PULLUP);
pinMode(BTN_C, INPUT_PULLUP);
Serial.println(F("Boot: HX711 + DFPlayer with 3 selectable trigger profiles"));
mp3Serial.begin(9600);
dfp_ok = mp3.begin(mp3Serial);
if (dfp_ok) {
mp3.volume(START_VOLUME);
mp3.EQ(0);
Serial.println(F("DFPlayer online."));
} else {
Serial.println(F("DFPlayer init FAILED (check wiring and SD card)."));
}
scale.begin(HX_DT, HX_SCK);
scale.set_scale(CALIBRATION_FACTOR);
delay(300);
Serial.println(F("Place EMPTY CUP on coaster; taring in 2 s..."));
delay(2000);
scale.tare(15);
Serial.println(F("Tare complete."));
printProfileBanner();
}
// ---------- Main loop ----------
void loop() {
checkButtons();
float grams = scale.get_units(5);
Profile &P = PROFILES[current_profile];
float t1 = P.t1_g;
float t2 = P.t2_g;
float rearmAt = rearmThresholdFor(P);
Serial.print(F("mass[g]=")); Serial.print(grams, 1);
Serial.print(F(" | Profile: ")); Serial.print(P.name);
Serial.print(F(" | t1: ")); Serial.print(armed_t1 ? "armed" : "done");
Serial.print(F(", t2: ")); Serial.println(armed_t2 ? "armed" : "done");
if (armed_t1 && grams >= t1) {
Serial.print(F(">=")); Serial.print(t1, 1); Serial.println(F(" g → PLAY (t1)"));
if (dfp_ok) mp3.play(TRACK);
armed_t1 = false;
}
if (armed_t2 && grams >= t2) {
Serial.print(F(">=")); Serial.print(t2, 1); Serial.println(F(" g → PLAY (t2)"));
if (dfp_ok) mp3.play(TRACK);
armed_t2 = false;
}
if ((!armed_t1 || !armed_t2) && grams <= rearmAt) {
armed_t1 = true;
armed_t2 = true;
Serial.print(F("<= ")); Serial.print(rearmAt, 1); Serial.println(F(" g → re-armed both triggers"));
}
if (dfp_ok && mp3.available()) { mp3.readType(); mp3.read(); }
delay(200);
}