In collaboration with CMU's Facilities Management Services, our final project was to create an assistive, functioning device for an FMS staff member that would be particularly useful and relevant to the client personally.
Our project, the Paint Coverage Tool, was created for and in collaboration with Rodney, a painter at CMU. We worked closely with Rodney to research, prototype, and build hardware solutions to support his daily tasks. For additional context, please refer to our earlier documentation, where we interviewed Rodney to understand the challenges he faces on job sites.
This final documentation summarizes the completed prototype, its purpose, and our design decisions.
We built a countertop device that helps Rodney quickly figure out how much paint he has left and the approximate area that the leftover paint can cover. When he inputs the specific paint into the device, when he places the bucket on, the screen will show the percentage remaining volume, the paint type, estimated coverage area, and a place for him to write down the location that the paint was last used. With a button press, the device also prints a small receipt that Rodney can attach to the paint can so that he has an updated note to keep records of what is in the paint bucket.
Full Device Overview: This image shows the entire paint coverage tool from a high angle, including the load-cell platform, LCD display, print button, and thermal recipt printer.
An angled overview of the device.
Power:
A close-up of the power cord connection on the back panel of the device, showing how the device receives stable power for the scale, screen, and thermal printer.
A close-up view of the LCD, with five readouts: paint type, base type, remaining paint, and leftover coverage.
Thermal Receipt Printer:
A close-up showing the thermal printer integrated into the device + red print button.
Receipt: detailed view of what is printed from the device.
Bottom-up view of the electronics configuration.
Device in use:
A short demonstrating the device measuring a paint bucket, displaying remaining paint and coverage, and printing a receipt note (in a real setting, Rodney would place his paint bucket where the tape is).
Rodney just came back after finishing a paint job. Before he puts the half-used paint bucket back in storage, he places it on the paint coverage tool. Based on the paint type, he selects "Eggshell" and "Midtone". Then, as he places the paint bucket onto the device, the screen updates: "Left: 40%, Area Left: 90 sq ft." Following the instructions on screen, he taps the print button. A small receipt feeds out with the same information displayed on screen. He tears off the slip and sticks it onto the lid of the can. Next week, when he reaches for paint, he doesn't have to shake the can, guess its coverage and location used.
This project was designed to answer the design question: "How can we reduce guesswork in Rodney's workflow, and give him an easy, reliable way to measure and keep track of his leftover paints. "
Our early prototypes were rough but functional versions of the final device. We included the basic hardware and software to output the amount of paint left & its coverage; however, we experimented with multiple input methods and considered different ways of measuring.
Our initial prototypes used minimal hardware & fabrication: an Arduino Uno, a 10kg load cell, a 3D-printed platform, an LCD screen, an on/off button, and control buttons. Our main objective was to figure out the basic workflow that we can build on later.
We laid out the basic hardware to make the device functional. On the whiteboard, we mapped out the specific function of each component.
We add a load cell amplifier to convert the signals from the load cell to a standard output format for the LCD display
We wired all the components with the addition of an on/off button, and uploaded the code to the Arduino to make the display and load cell part of the prototype functional.
Each button controls a different preset paint option, which measures the coverage area differently accroding to the viscosity of the paint.
After presenting our initial prototype, we updated our prototype to better accommodate Rodney's needs. In this image, we mapped out the configuration consisting of the new/final electrical components.
We made a couple of key changes from version 1 to 2 prototype:
Button vs Potentiometer Knob
We learned that there are multiple paints Rodney commonly uses, and each paint has a different paint base that corresponds to a different paint coverage amount. To accommodate the variety of paint choices, we changed the input format from button presets to a potentiometer knob where Rodney can turn to select the paint option on the LCD.
LCD Display + Physical Receipt
Our conversation with Rodney revealed that he would like to have a physical note to help him track the measurement information for organizational purposes. This led us to incorporate an additional thermal printer into the system, which would print out a physical receipt of the information and an area for Rodney to note down the location of the paint used.
Battery vs. Plug-in Power Supply
We decided to use a plug-in (corded) power supply as Rodney mentioned that he would ideally have this device sit stationarily in his office. Having a plug-in cord would save Rodney the trouble of switching the battery every so often.
A short demo showing the prototype functioning, controlled with a potentiometer knob on the screen and a print button.
The enclosure is temporarily held together with tape.
Feedback session with Rodney. We created rapid sketches and prototypes to revise our prototype to better accommodate Rodney's needs.
Building stage: assembling the container for the device. The container is made of laser-cut 3mm plywood and acrylic, put together with finger joints.
Assembling the electronics inside the laser-cut enclosure. We struggled with managing the wire routing and maintaining stable connections while securing each component into its final position.
Throughout our prototyping process, our main goal was to answer the question of whether we can build a sturdy and reliable device that helps Rodney get more accurate measurements and work more efficiently. Our first prototype helped us confirm that a 10kg load cell and a load cell amplifier could provide consistent readings to display on the LCD. However, throughout the process, we often struggled with unstable connections, such as a flickering LCD display and noisy readings from the load cell. Because of the number of connections we have in the system, each bug required multiple rounds of hardware calibration and code refinement to pinpoint the exact cause. One surprise we encountered was how important soldering is in ensuring stable connections within each connection, as we ultimately realized that many of our challenges came from loose wire connections.
Direct feedback from Rodney shaped some of our key design decisions, including integrating a physical output format. We added the thermal receipt printer after Rodney pointed out that the painters have no organized way of storing the buckets of paint and could benefit from printed labels.
Overall, the prototyping process reinforced how crucial direct user involvement is, as we realized that the most important breakthroughs came from listening carefully and engaging in conversations with Rodney.
Soldering process for the loadcell
Cut the enclosure with a laser cutter
Use Fusion 360 to model the enclosure
Since the printing section require a 9V voltage while the rest of the electric components require 5V, a DC-DC Converter voltage conversion module was added to distribute the different voltages accordingly.
Perform DC-DC converter voltage testing to ensure a 5V output.
Enclosure assembly after laser cut and test whether it fit the 3D print part of load cell base unit.
Lessons:
There are several reviewers pointed out that we should attention to the waterproofing problem, so that the electronic components remain protected in case paint accidentally spills.
In addition, we also received feedback that the printed receipt should be longer to provide enough space for Rodney to write notes.
In the future, to ensure that our product works for the client, we would have to let them test out a prototype on the job. For our scale, we calibrated it using 1-pound weights, then did rough calculations, which might not transfer well when an actual paint bucket is placed on it.
Major takeaways from the experience of working with a client:
The opportunity to speak directly with customers or end users is very valuable. They are the person who actually use the products and the ones who truly face the problems that need solving. By carefully listening to the details of their daily work, thinking actively, and asking insightful questions, we can better understand their needs and help them effectively resolve their issues.
/*
Project Title: Paint Scale
Author: Devin, Yutong, Leaf
Credits:
Code generated by Copilot
Description: This code uses an HX711 load-cell amplifier connected to a scale to measure the total weight of a PPG paint bucket. The weight is then converted into a percentage of paint left and an estimated area coverage in square feet. The code divides the measured weight by the known full paint bucket weight to calculate the percentage of paint left. Then, by multiplying the percentage of paint left by the known total area coverage, the code calculates the area coverage with the paint left in the bucket. A rotary encoder with a push button allows the user to navigate the menu to select the paint type (Eggshell, Satin, Semi-Gloss) and the base (Neutral, Pastel, Midtone, Deeptone). A 20x4 I2C LCD displays the selected paint type, base, percentage, coverage, and a box to write where the paint was used. A separate push button triggers a thermal printer to print a receipt of the recorded paint information.
Pin Mapping:
LCD (I2C) → SDA/SCL (default I2C pins)
Load Cell Data → D9 (HX711 DOUT)
Load Cell Clock → D8 (HX711 SCK)
Rotary Encoder CLK → D3
Rotary Encoder DT → D4
Rotary Encoder SW → D7 (push button)
Thermal Printer RX → D5
Thermal Printer TX → D6
Print Button → D2
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "HX711.h"
#include "SoftwareSerial.h"
//LCD setup
LiquidCrystal_I2C lcd(0x27, 20, 4);
//HX711 setup
#define LOADCELL_DOUT_PIN 9
#define LOADCELL_SCK_PIN 8
HX711 scale;
//Rotary encoder pins
#define ENCODER_CLK 3
#define ENCODER_DT 4
#define ENCODER_SW 7
//Thermal printer pins
#define PRINTER_RX 5
#define PRINTER_TX 6
SoftwareSerial printer(PRINTER_RX, PRINTER_TX); //Printer serial connection
//Printer activation button
#define PRINT_BUTTON 2 //Separate button to trigger printing
//Paint info (weights in grams)
float emptyBucketWeight = 272.2; //Empty bucket weight (0.6 lb in grams)
float fullCanWeight = 4581.3; //Full bucket weight (10.1 lb in grams)
float coverageEggshell = 400.0; //Coverage area for Eggshell paint
float coverageSatin = 350.0; //Coverage area for Satin paint
float coverageSemiGloss= 300.0; //Coverage area for Semi-Gloss paint
//Paint options
String paints[] = {"Eggshell", "Satin", "Semi-Gloss"};
int numPaints = 3;
int paintIndex = 0; //Current paint selection index
//Suboptions for each paint type
String eggshellSubs[] = {"Neutral Base", "Pastel Base", "Midtone Base"};
String satinSubs[] = {"Pastel Base", "Midtone Base", "Deeptone Base"};
String semiSubs[] = {"Pastel Base", "Midtone Base", "Deeptone Base", "Neutral Base"};
int subIndex = 0; //Current base selection index
//Encoder state
int lastClk = HIGH;
int lastButtonState = HIGH;
//Menu state
int screen = 1; //Tracks which screen is active
bool needsUpdate = true; //Flag to refresh LCD
//Stored values for printing
int lastPercentRounded = 0; //Last calculated % paint left
int lastCoverageRounded = 0; //Last calculated coverage area
//Noise filter (grams)
float noiseFloor = 20.0; //Ignore readings under 20 g
//Display Function
void printLine(int row, String text) { //Prints text on LCD row and clears remainder
lcd.setCursor(0, row);
lcd.print(text);
int len = text.length();
for (int i = len; i < 20; i++) lcd.print(" ");
}
//Setup Function
void setup() {
lcd.init();
lcd.backlight();
scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
scale.set_scale(200); //Estimated scale factor (grams)
scale.tare(); //Zero the scale at startup
pinMode(ENCODER_CLK, INPUT); //Encoder pins
pinMode(ENCODER_DT, INPUT);
pinMode(ENCODER_SW, INPUT_PULLUP);
pinMode(PRINT_BUTTON, INPUT_PULLUP); //Print button
printer.begin(19200); //Initialize thermal printer
lcd.clear(); //Show initial menu
printLine(0, "Select Paint:");
printLine(1, paints[paintIndex]);
}
//Main Loop
void loop() {
//Encoder Rotation
int clkState = digitalRead(ENCODER_CLK);
if (lastClk == HIGH && clkState == LOW) { //Detect rotation
if (digitalRead(ENCODER_DT) == HIGH) { //Clockwise
if (screen == 1) { paintIndex = (paintIndex + 1) % numPaints; needsUpdate = true; }
else if (screen == 2) { //Cycle through bases
if (paints[paintIndex] == "Eggshell") subIndex = (subIndex + 1) % 3;
else if (paints[paintIndex] == "Satin") subIndex = (subIndex + 1) % 3;
else subIndex = (subIndex + 1) % 4;
needsUpdate = true;
}
} else { //Counter-clockwise
if (screen == 1) { paintIndex = (paintIndex - 1 + numPaints) % numPaints; needsUpdate = true; }
else if (screen == 2) {
if (paints[paintIndex] == "Eggshell") subIndex = (subIndex - 1 + 3) % 3;
else if (paints[paintIndex] == "Satin") subIndex = (subIndex - 1 + 3) % 3;
else subIndex = (subIndex - 1 + 4) % 4;
needsUpdate = true;
}
}
}
lastClk = clkState;
//Encoder Button Handling
int buttonState = digitalRead(ENCODER_SW);
if (lastButtonState == HIGH && buttonState == LOW) { //Button press detected
if (screen == 1) screen = 2; //Advance to next screen
else if (screen == 2) screen = 3;
else if (screen == 3) screen = 4;
else if (screen == 4) { //Reset to start
screen = 1;
paintIndex = 0;
subIndex = 0;
}
needsUpdate = true;
delay(200); //Debounce
}
lastButtonState = buttonState;
//LCD menus
if (needsUpdate) {
lcd.clear();
if (screen == 1) { //Paint selection screen
printLine(0, "Select Paint:");
printLine(1, paints[paintIndex]);
}
else if (screen == 2) { //Base selection screen
printLine(0, "Select Base:");
String subName = (paints[paintIndex] == "Eggshell") ? eggshellSubs[subIndex] :
(paints[paintIndex] == "Satin") ? satinSubs[subIndex] :
semiSubs[subIndex];
printLine(1, subName);
}
else if (screen == 3) { //Measurement screen
float bucketWeight = scale.get_units(5); //Read HX711 (grams)
if (bucketWeight < noiseFloor) bucketWeight = 0; //Ignore small noise
if (bucketWeight < 0) bucketWeight = 0;
float paintWeight = bucketWeight - emptyBucketWeight; //Subtract empty bucket
if (paintWeight < 0) paintWeight = 0;
float coveragePerCan = (paints[paintIndex] == "Eggshell") ? coverageEggshell :
(paints[paintIndex] == "Satin") ? coverageSatin :
coverageSemiGloss;
float percentLeft = (paintWeight / fullCanWeight) * 100.0; //% paint left
percentLeft = constrain(percentLeft, 0, 100);
float coverageFt2 = (percentLeft / 100.0) * coveragePerCan; //Area coverage
coverageFt2 = constrain(coverageFt2, 0, coveragePerCan);
lastPercentRounded = round(percentLeft); //Store for printing
lastCoverageRounded = round(coverageFt2);
printLine(0, "Paint: " + paints[paintIndex]); //Display results
String subName = (paints[paintIndex] == "Eggshell") ? eggshellSubs[subIndex] :
(paints[paintIndex] == "Satin") ? satinSubs[subIndex] :
semiSubs[subIndex];
printLine(1, "Base: " + subName);
printLine(2, "Left: " + String(lastPercentRounded) + "%");
printLine(3, "Area: " + String(lastCoverageRounded) + " sq ft");
}
else if (screen == 4) { //Menu screen
printLine(0, "---- Menu ----");
printLine(1, "Red button: Print");
printLine(2, "Knob click: Reset");
printLine(3, "--------------");
}
needsUpdate = false; //Reset
}
//Print Receipt Block
if (screen == 4 && digitalRead(PRINT_BUTTON) == LOW) { //Print button pressed
printer.println("------ Paint Receipt ------");
printer.println("Paint: " + paints[paintIndex]);
String subName =