Isometric view from the top corner of the system. Shows the top interface with buttons, LCD display, and the switch. Additionally, you can see the load cell where the k-cup goes, the hopper for coffee grounds, and the cables for power. The lid is put down beneath the top of the box to show the hopper.
This project helps me pour grounded coffee beans into my reusable k-cups every morning. Once the device is plugged in, the top switch turns the system on. The right button dispenses a full k-cup worth of coffee grounds, and the button on the left dispenses a half cup. A green LED lights up beneath the button that is pressed. The display shows the whether or not the machine is currently dispensing coffee grounds. The load cell on the bottom senses the weight of the k-cup to determine when to stop dispensing coffee grounds.
Here shows the LCD display up close. Since none of the buttons are pressed, no LEDs are on. The LCD display reads "Idle - Press Btn, Motor: Off". When a button is pressed it will read "Motor On"
This picture shows the wiring for the power. The system uses a wire for power to the motor and a usb for the arduino. This was a challenge because the motor draws a lot of power, so I had to ensure that there was enough voltage for the system when the motor turns on .
This picture depics the complex wiring of all the components within the box. I used two PCBs, one for the components on the top interface (LCD, buttons, LEDs, switch) and one for the bottom (Load cell and motor). This was very difficult because I had to ensure no wires interfered with the gears.
This image provides a close up for the funnel out of the auger to the load cell placement. I had to hand carve out the hole for the load cell since I forgot to include that in my dxf file. This was difficult and will make me think twice about making sure I accounted for all components in my designs next time.
In this video, I show where the coffee grounds are held in the hopper. Once the coffee grounds are shown, I close the lid, flip the on switch and press a button to start dispensing coffee grounds into the k-cup. You can see the green LED light up indicating which button was pressed, and the LCD indicating that the motor is on.
Wiring for the top interface of the box. Figuring out where to place components was not very difficult, but the wiring behind them was very complex. I had to iterate between different wire sizes to make sure they were long enough to reach the PCB, but not too long so that they got in the way of the motor and gears.
Gluing the 3D printed mechanism into place on the inside of the final box. I laser cut a hole in the box for the auger to dispense out of and taped the 3D print into place while the glue was drying. It was very difficult to get the 3D print into place within the box since the motor and wiring weighed a lot relative to the box.
This is from my process of designing the top of the box. There were a lot of components here so I had to make sure I had enough room to place them evenly. A significantly difficult thing I ran into here was how to attach a hinge for the hopper lid. This was challenging to find the right sized hinge and screws to fit through such thin wood without cracking it.
Here is me testing the mechanics of the gear ratios I 3D printed. I faced many challenges with the printing aspects and how I was attaching the motor to the gears and the auger. I ended up using zip ties to lock the motor in place. They kept the motor stable and aligned with the gear ratios.
The overall design process was very complex and often involved a lot of iteration with sizing and design types to find the optimal specifications. For instance, I had to 3D print multiple gear ratios and attachments to the DC motor because the position of the motor and the gears needed to line up perfectly. Another challenging aspect for the design that was not depicted in the images was orienting the components on the box after I had already laser cut. With limited time I improvised by sanding down certain areas of the wood and sawing off the lid for the hopper.
Overall, I am definitely happy with the work and effort I put into this project. I finally got to put my CAD work, 3D printing, and laser cutting towards a meaningful design. I feel like I learned a ton about my strengths and weaknesses in design. Going into this project I already had pretty solid skills with electrical wiring and code, so I wanted to focus more on the design aspect and the mechanics of my project. One comment I got said " I think it looks polished with the LEDs and buttons, and the mechanism for dispensing the coffee grounds was neat". This made me very happy to see because I was really proud of the physical neatness of the interface on the top of the box. Laying out all the electronic components and adding the hinge took a lot of effort, so I am glad it payed off. I also really enjoyed the challenge of getting the auger dispenser to work as intended even if it was stressful at times. At one point I got very frustrated because I needed to 3D print parts, but it takes quite a bit of time to get the parts back and the prints are not guaranteed to be successful. One thing I learned was that I need to focus more on balancing my work time between working on the functionality of the system and the physical display of it. If I were to do this project over again I would tell myself to start earlier, and prototype out a few designs before jumping into everything. In the future I would love to work on an advanced version of my design if I have time. I was given lots of great tips during the project demos. One thing I had not considered was that there are food safe 3D printing material I could have used. This is obviously something I would need in order to be able to use this project for real. I would also like to change the material of the box to a shiny acrylic so that the display is not so bland. One comment I got that I may consider was "Is there a way to incorporate a system to grind whole beans instead of ground coffee". This got me thinking about ways to add additional features to the system. I think this aspect could be very beneficial because grinding up all the coffee beans manually is also another very tedious task that I have to every so often. This would save a lot of time for me and ultimately make this project all the more worth while.
/*
==============================================================================
Project Title: Automatic Coffee Grounds Dispense
Author: Cole Franklin
==============================================================================
Description:
This sketch drives a DC motor based on readings from an HX711 load cell
amplifier, using two user buttons to start a run. When either BIG (D8) or
SMALL (D9) is pressed, the system tares the scale, latches the corresponding
LED, starts the motor (D6), and monitors the scale. Once the filtered reading
meets the stop criterion (REQUIRED_CONSECUTIVE samples <= STOP_THRESHOLD) or a
safety timeout elapses, the motor is stopped. A 16x2 I2C LCD shows state and
live readings (Idle/Running/Stopped).
Pin Mapping (Arduino Uno-style):
HX711:
D2 -> LOADCELL_DOUT_PIN (HX711 DOUT)
D3 -> LOADCELL_SCK_PIN (HX711 SCK)
5V -> HX711 VCC
GND -> HX711 GND
User I/O:
D8 -> BIG_BUTTON_PIN (assumes external pulldown; pressed = HIGH)
D9 -> SMALL_BUTTON_PIN (assumes external pulldown; pressed = HIGH)
D10 -> GREEN_LED_PIN (LED for BIG)
D11 -> SMALL_LED_PIN (LED for SMALL)
D6 -> MOTOR_PIN (to MOSFET/driver input; HIGH = motor ON)
LED_BUILTIN mirrors motor state for quick debug
LCD (I2C backpack, HD44780-compatible):
A4 -> SDA
A5 -> SCL
+5V/GND as required
I2C address typically 0x27 (alternate: 0x3F) — adjust LCD_I2C_ADDR
Operation Summary:
- Startup: HX711 initialized (no auto-tare), LEDs and motor OFF, LCD shows Idle.
- First button press (BIG/SMALL):
* Tare is performed immediately at press time.
* LED for that channel latches ON, motor starts.
* LCD shows Running and the live weight/units reading.
- Running:
* Early "arming" window ignores sensor to avoid spurious stops.
* Stop when REQUIRED_CONSECUTIVE readings are <= STOP_THRESHOLD.
* Or stop on SAFETY_TIMEOUT_MS, whichever comes first.
- Done: Motor remains OFF, latched LED stays ON, LCD shows final reading.
Credits / Attributions:
- HX711 library by Bogdan Necula (bogde) and contributors.
- LiquidCrystal_I2C library (HD44780 over I2C).
- Development assistance: OpenAI ChatGPT (model “GPT-5 Thinking”) was used to
help draft documentation and inline comments; all logic decisions were reviewed
and integrated by the author.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "HX711.h"
// ---------------- LCD config ----------------
#define LCD_I2C_ADDR 0x27 // change to 0x3F if your module uses that
LiquidCrystal_I2C lcd(LCD_I2C_ADDR, 16, 2);
// ---------------- HX711 wiring --------------
const int LOADCELL_DOUT_PIN = 2; // D2
const int LOADCELL_SCK_PIN = 3; // D3
HX711 scale;
// ---------------- I/O pins ------------------
const uint8_t BIG_BUTTON_PIN = 8; // external pulldown -> pressed = HIGH
const uint8_t SMALL_BUTTON_PIN = 9; // external pulldown -> pressed = HIGH
const uint8_t GREEN_LED_PIN = 10; // for big button
const uint8_t SMALL_LED_PIN = 11; // for small button
const uint8_t MOTOR_PIN = 6; // NPN driver, HIGH = ON, LOW = OFF
// ---------------- Behavior config -----------
const float STOP_THRESHOLD = 200; // weight to stop
const unsigned long DEBOUNCE_MS = 30; // button debounce
const unsigned long SAFETY_TIMEOUT_MS = 60000; // 60s fail-safe (0 to disable)
// Robustness & tuning
const unsigned long ARMING_DELAY_MS = 1000; // ignore scale for first 1s after start
const uint8_t REQUIRED_CONSECUTIVE = 3; // need N consecutive samples <= threshold
const unsigned long HX711_WAIT_MS_PER_SAMPLE = 200; // per-sample wait before giving up
// ---------------- State machine -------------
enum State : uint8_t { IDLE, RUNNING, DONE };
State state = IDLE;
// Bookkeeping
bool ledLatchedBig = false;
bool ledLatchedSmall = false;
bool lastBigLevel = LOW;
bool lastSmallLevel = LOW;
unsigned long lastBigChangeMs = 0;
unsigned long lastSmallChangeMs = 0;
int8_t activeChannel = -1; // 0 = big, 1 = small
unsigned long runStartMs = 0;
uint8_t belowCount = 0;
float lastReading = NAN; // latest displayed reading
// --------------- Forward declarations -------
static void motorOn();
static void motorOff();
static int8_t getPressedEdge();
static bool readUnitsAveraged(float &out, uint8_t samples = 5, unsigned long waitMsPerSample = HX711_WAIT_MS_PER_SAMPLE);
static void lcdShowIdle(float reading);
static void lcdShowRunning(float reading, unsigned long elapsedMs);
static void lcdShowStopped(float reading);
void setup() {
Serial.begin(38400);
Serial.println(F("HX711 + Buttons + Motor + LCD"));
// LCD init
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0,0); lcd.print(F("Scale Controller"));
lcd.setCursor(0,1); lcd.print(F("Initializing..."));
// Buttons (external pulldown -> INPUT)
pinMode(BIG_BUTTON_PIN, INPUT);
pinMode(SMALL_BUTTON_PIN, INPUT);
// LEDs
pinMode(GREEN_LED_PIN, OUTPUT);
pinMode(SMALL_LED_PIN, OUTPUT);
digitalWrite(GREEN_LED_PIN, LOW);
digitalWrite(SMALL_LED_PIN, LOW);
// Motor + debug LED
pinMode(MOTOR_PIN, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
motorOff();
// Debounce baselines
lastBigLevel = digitalRead(BIG_BUTTON_PIN);
lastSmallLevel = digitalRead(SMALL_BUTTON_PIN);
lastBigChangeMs = millis();
lastSmallChangeMs = millis();
// HX711 init (no auto tare)
Serial.println(F("Initializing the scale"));
scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
// Optional diagnostics
Serial.print(F("read avg 20:\t")); Serial.println(scale.read_average(20));
Serial.print(F("units(5) pre:\t")); Serial.println(scale.get_units(5), 1);
scale.set_scale(2280.f); // your calibration factor
// scale.tare(); // <-- DISABLED: we will tare on button press
Serial.print(F("units(5) post:\t")); Serial.println(scale.get_units(5), 1);
lcd.clear();
lcdShowIdle(NAN); // show idle screen
state = IDLE;
activeChannel = -1;
}
void loop() {
// Show an idle reading (non-blocking-ish) to keep LCD fresh
if (state == IDLE) {
// Take a light, timed average just for display (ignore failure quietly)
float disp = 0.0f;
if (readUnitsAveraged(disp, 3, 50)) {
lastReading = disp;
lcdShowIdle(lastReading);
} else {
lcdShowIdle(NAN);
}
}
if (state == IDLE) {
int8_t pressed = getPressedEdge(); // 0 = big, 1 = small, -1 = none
if (pressed == 0 && !ledLatchedBig) {
Serial.println(F("BIG pressed -> tare, LED on, motor start"));
scale.tare(); // <-- tare on press
Serial.println(F("Scale tared"));
digitalWrite(GREEN_LED_PIN, HIGH);
ledLatchedBig = true;
activeChannel = 0;
motorOn();
runStartMs = millis();
belowCount = 0;
state = RUNNING;
lcdShowRunning(lastReading, 0);
} else if (pressed == 1 && !ledLatchedSmall) {
Serial.println(F("SMALL pressed -> tare, LED on, motor start"));
scale.tare(); // <-- tare on press
Serial.println(F("Scale tared"));
digitalWrite(SMALL_LED_PIN, HIGH);
ledLatchedSmall = true;
activeChannel = 1;
motorOn();
runStartMs = millis();
belowCount = 0;
state = RUNNING;
lcdShowRunning(lastReading, 0);
}
}
else if (state == RUNNING) {
// Grace/arming period
unsigned long now = millis();
unsigned long elapsed = now - runStartMs;
if (elapsed < ARMING_DELAY_MS) {
if (SAFETY_TIMEOUT_MS > 0 && elapsed > SAFETY_TIMEOUT_MS) {
motorOff(); state = DONE;
Serial.println(F("Safety timeout during arming."));
lcdShowStopped(lastReading);
} else {
lcdShowRunning(lastReading, elapsed);
}
return;
}
// Read scale with timeout protection
float reading = 0.0f;
if (!readUnitsAveraged(reading)) {
motorOff();
state = DONE;
Serial.println(F("HX711 timeout -> motor stopped"));
lcdShowStopped(lastReading);
return;
}
lastReading = reading;
Serial.print(F("Reading: ")); Serial.println(reading, 1);
lcdShowRunning(reading, elapsed);
// Require N consecutive samples below threshold
if (reading <= STOP_THRESHOLD) {
if (++belowCount >= REQUIRED_CONSECUTIVE) {
motorOff();
state = DONE;
Serial.println(F("Threshold reached (consecutive). Motor stopped."));
lcdShowStopped(reading);
}
} else {
belowCount = 0;
}
// Safety timeout
if (SAFETY_TIMEOUT_MS > 0 && elapsed > SAFETY_TIMEOUT_MS) {
motorOff();
state = DONE;
Serial.println(F("Safety timeout. Motor stopped."));
lcdShowStopped(reading);
}
}
// state == DONE: hold display showing final value
}
// ---------------- Helpers ----------------
static void motorOn() { digitalWrite(MOTOR_PIN, HIGH); digitalWrite(LED_BUILTIN, HIGH); }
static void motorOff() { digitalWrite(MOTOR_PIN, LOW); digitalWrite(LED_BUILTIN, LOW); }
// Debounced rising-edge detector (external pulldown -> pressed = HIGH)
static int8_t getPressedEdge() {
unsigned long now = millis();
int8_t result = -1;
bool lvlBig = digitalRead(BIG_BUTTON_PIN);
if (lvlBig != lastBigLevel) {
lastBigLevel = lvlBig;
lastBigChangeMs = now;
} else if ((now - lastBigChangeMs) >= DEBOUNCE_MS) {
if (lvlBig == HIGH && !ledLatchedBig && result < 0) result = 0;
}
bool lvlSmall = digitalRead(SMALL_BUTTON_PIN);
if (lvlSmall != lastSmallLevel) {
lastSmallLevel = lvlSmall;
lastSmallChangeMs = now;
} else if ((now - lastSmallChangeMs) >= DEBOUNCE_MS) {
if (lvlSmall == HIGH && !ledLatchedSmall && result < 0) result = 1;
}
return result; // first qualifying press wins
}
// Read calibrated units with light averaging, with optional timeout
static bool readUnitsAveraged(float &out, uint8_t samples, unsigned long waitMsPerSample) {
float sum = 0.0f;
for (uint8_t i = 0; i < samples; i++) {
if (waitMsPerSample == 0) {
while (!scale.is_ready()) { /* block */ }
} else {
unsigned long t0 = millis();
while (!scale.is_ready()) {
if (millis() - t0 > waitMsPerSample) return false; // give up
yield(); // or delay(1) on AVR
}
}
sum += scale.get_units(1);
}
out = sum / (float)samples;
return true;
}
// ---------------- LCD helpers --------------
static void lcdPrintCleared(const char* line1, const char* line2) {
lcd.clear();
lcd.setCursor(0,0); lcd.print(line1);
lcd.setCursor(0,1); lcd.print(line2);
}
static void lcdShowIdle(float reading) {
lcd.setCursor(0,0);
lcd.print(F("Idle - Press Btn "));
lcd.setCursor(0,1);
lcd.print(F("W: "));
if (isnan(reading)) lcd.print(F("--.- "));
else { lcd.print(reading, 1); lcd.print(F(" ")); }
}
static void lcdShowRunning(float reading, unsigned long elapsedMs) {
lcd.setCursor(0,0);
lcd.print(F("Running "));
lcd.print(elapsedMs/1000);
lcd.print(F("s ")); // pad to clear
lcd.setCursor(0,1);
lcd.print(F("W: "));
if (isnan(reading)) lcd.print(F("--.- "));
else { lcd.print(reading, 1); lcd.print(F(" ")); }
}
static void lcdShowStopped(float reading) {
lcdPrintCleared("Motor Stopped ", "Final W: ");
lcd.setCursor(9,1);
if (isnan(reading)) lcd.print(F("--.-"));
else lcd.print(reading, 1);
}