In our project, we consulted Mike, who is a Mason at Carnegie Mellon University's Facilities Management Services. In this project, the goal was to create a physical computing device to benefit Mike in his work. In this, we had an initial meeting with Mike about what we could make to assist him. Initially, we built a prototype and concept involving automatically cleaning a paddle, but we pivoted to creating a game board so that while the FMS employees are in the shop, they can see who is going to be playing the game they decide to play.
For more information on our first plan and meeting: https://sites.google.com/andrew.cmu.edu/60-223-s26/project-3-documentation/fms-mason-interview-documentation?authuser=0
Our project is a way for the people at FMS to actually know if there are enough people to play Left Right Center. In this project, there is a digit displayed on a screen. This number will be set with a button, and can be changed before it is set with a dial. The days are connected with our 24-hour day cycle, and the numbers will go down after each day. The light is normally white, but if there are at least five different players in the game, there are lights that light up green. Once the day countdown goes to zero, if there are enough players, the lights become rainbow. If there aren't enough players, the lights become red. Additionally, if the dice are in the designated storage area, there is a light above it that illuminates white. If the dice are not present, the lights become rainbow.
Image showing the days counting down on the gameboard in an expedited manner (for demo purposes).
Image of the final gameboard
Finalized housing for the dice for the game with the rainbow lights above.
Shot showing how the lights turn green once there is enough players to play left right center in anticapation for the game running.
Shot of the entire casing from an angel so the whole width is able to be captured.
Shot of the button and dial and aracde button used to set the days till game
When the employees at FMS want to start setting up for their weekly Left Right Center Game, they will go to the Gameboard. Here, one of the employees will turn the giant dial, and the days until gaming screens will go through different numbers. Once it is appropriately synced to how many days until they will play, they will hit the button above the dial.
While waiting, if any of the employees, like Mike, walk by and want to play, they will move their card into an empty slot. If enough people say they are playing before the countdown hits 0 days, the lights on the gameboard will turn green to tell everyone we are playing left, right, and center! Once it hits 0 days the colors will be rainbow if they are playing, and if there are not enough people, the color will turn red.
This prototype asked whether we can physically make a solution to clean the paddle with brushes.
Our prototype combined a motor that moves up and down and a nylon brush. Here, we powered the motor and used cable ties to hold the nylon brush in place. Thus, once we powered the motor, the nylon brush moved up and down in an attempt to remove the remaining paste on the paddle.
Through our prototyping process, we found that it was not really feasible to clean the paddle with brushes. Since this was really our only idea, this revelation caused us to decide to pivot. The feedback we got from our prototype was that this was an incredibly hard idea, and we should consider pivoting. Ultimately, we took this feedback and talked with Mike as to what other issues related to his work, but not the actual process, could be improved. Through this conversation, we discovered Left, Right, and Center and how they aim to play more but ultimately don't. Through taking this feedback, we ended up ignoring other feedback suggesting possible ways to fix the problem, but even this feedback did say that while there is a potential option, it may be better to scrap the idea completely.
Our biggest surprise during this prototype phase was how difficult this prototype would be to work with. We figured there would be a reason that this would not be solved easily, as otherwise this wouldn't be the only issue Mike the Mason would have after over two decades of being a mason, but the specific ideas we were optimistic about ended up falling short.
Active image of our linear actuator combined with our nylon brush working to scrub the cement off the paddle.
Here is an image of the cable tie holding tofether the linear actuator and brush. we wanted something stable but not permanent, and this worked perfectly for this.
The wiring used to power the linear actuator and nylon brush used.
Video of the paddle prototype in motion (note video is vertical in order to be able to zoom in more and be further away so I did not get splashed on)
Ideation Sketches regarding how we could find a way to clean the paddle
In this photo, we aim to see if at the perfect angle, can the brush clean the paddle. Getting it to this angle would be a different problem, but we wanted to see if it could
Here, Sophie is actively moving around to make sure the brush is strong enough to clean the paddle
This is an image of the connected photoresistor to the dice housing we finished
This is an image of much of the soldering that was finished for our project initally.
This is an image showing the breadboarded dot matrixes working together
Image of the electronics when they ended up being placed in the casing. They were not finalized here, only being connected through a breadboard
Image of the choas that was ensuing when trying to troubleshoot what was wrong with the soldering. Here, we can see the geiger counter, Andy's arduino pulled from his project, and lots of wires from both soldered boards and breadboards present.
Image showing how the casing looked before we input things in the casing
With our project, due to all of us having very different schedules, it was hard to find times to meet up. Thus, we ended up procrastinating more than we would have liked, and we were all scrambling a bit more than we expected at the last minute. While we got the casing and everything in order by 3 pm the day before, for some reason, the wiring was not working. Thus, Andy, Jack, and Rion were scrambling from 9 pm until around 1 am trying to figure out the wiring. Ultimately, despite resoldering it multiple times, trying different arduinos, and a variety of other things, we went in planning to demonstrate the breadboarded components, and then solder it before handoff. Fortunately, Sophie ended up coming in slightly earlier before the final crit and soldering a few things, which somehow worked completely fine, so a good majority of the crit ended up being soldered.
As a result of our inconveniently aligned schedules, the housing section of our Gantt chart wasn't fully followed. For some reason, neither Jack nor Rion had access to the laser cutter machine despite getting clearance, and Sophie and Andy weren't available until Monday. Thus, we ended up pushing back the fabrication of housing parts and assembling the housing until Tuesday, April 21st, in the afternoon. The electronics input wasn't completed as a full team, as Sophie had commitments she shared with us. When creating the, it was lost in the shuffle and wasn't explicitly mentioned in the Gantt chart.
With feedback from our final crit, one thing that was mentioned we really liked was the idea of having “permanent dice in bubbles”. This could be an idea that, if we further iterate on, we would include. Dice are super easily lost, so having the dice be physically attached to the game board would be a nice way to have everything be more connected. Moreover, in our feedback, another thing that was mentioned was that the project “needs a carrying handle”. While we feel the project works decently on its own, the inclusion of a carrying handle could definitely improve the overall experience of the gameboard.
One thing that our project was repeatedly praised for was the “pivot when problems with the profession were too complex to tackle during rapid design and prototyping”. This was an incredibly difficult decision to make as we felt we were throwing away work that we had already done, but in retrospect, this was definitely the right decision. Not only were we able to see the project through to completion, but people far more experienced in this domain seemed to think that this was the correct option to go down as well. Another form of feedback we repeatedly got was how the device was good at “encouraging participation”, and the fact that evaluators noticed this aspect was very satisfying. This was something we really wanted to focus on, as FMS flaking last-minute playing is a problem Mike mentioned, so by incorporating active commitments through moving the piece, we aimed to reduce its occurrence. By having the players physically move a card to say they are playing, we hope that this will help them actively commit to the activity, compared to just stating that they will play.
Working with a client was a different experience than we anticipated. Our client, Mike the Mason, has been in the field for an extremely long time. This made it hard to think of ways to improve their workflow that wouldn't add convoluted steps, as he has basically mastered his own workflow over the years. In our crit, the only thing he really mentioned was finding a way to clean a paddle, and as such, we went with this as our initial goal. This turned out to be quite hard, and definitely not feasible within the given time frame, so we had to abandon this idea after the prototype evaluation. Thus, if we could go back in time, we would not have spent all of this time on this specific concept and instead tried to pivot earlier. Our idea of landing on helping the FMS people play the game that they seemingly all want to play, but end up not playing, was a pivot we are proud of.
Overall, due to our conflicting time schedules and the pivot we made towards the end of our project, we are decently proud of how it all came together. While we could have changed our direction earlier and started working on troubleshooting before the last day, we still managed to complete the project in a way that will hopefully make Mike and the rest of FMS happy. Mike, in particular, during the final crit, was ecstatic and loved it. He stated how amazing it was, and he was so happy with how it turned out. Mike’s opinion trumps our own personal grievances with the project, and his excitement towards the project is something that we are very proud of. Hopefully, this project will be used for semesters to come and enable Mike and the rest of FMS to create a stronger bond together.
Here is our code:
/*
Capstone project with FMS: MIke the Mason
Potentiometer sets day till game and real-time counts down.
Light sensor tracks if dice in house or not.
Switches in card holder track if more than 5 people signed up to play.
All elements work together to show as different LED states.
Code written for an Arduino Uno.
Pin Mapping:
Arduino Pin | Role | Description
-------------------------------------------------------------------
A0 | Input | Potentiometer
A1 | Input | Light sensor for dice housing
2 | Input | 5 buttons for inside card holder
3 | Input | Button to set potentiometer value
6 | Output | LEDs for main housing
8 | Output | LEDs for dice housing
A4 (SDA) | I2C | Data line for RTC & 8x8 Matrices
A5 (SCL) | I2C | Clock line for RTC & 8x8 Matrices
Code released to the public domain by the authors, 04/23/2026
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include "Adafruit_LEDBackpack.h"
#include "RTClib.h"
#include <Adafruit_NeoPixel.h>
const int potPin = A0;
const int setButtonPin = 3;
const int gameButtonPin = 2;
// LED strip for box
const int mainStripPin = 6;
#define NUM_MAIN_LEDS 30
// LED for dice housing
const int ldrPin = A1;
const int ldrStripPin = 8;
#define NUM_LDR_LEDS 8
int ldrThreshold = 500;
Adafruit_8x8matrix matrixTens = Adafruit_8x8matrix();
Adafruit_8x8matrix matrixOnes = Adafruit_8x8matrix();
RTC_DS3231 rtc;
Adafruit_NeoPixel mainStrip(NUM_MAIN_LEDS, mainStripPin, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel ldrStrip(NUM_LDR_LEDS, ldrStripPin, NEO_GRB + NEO_KHZ800);
// --- STATE VARIABLES ---
int daysLeft = 0;
int liveScore = 0;
bool isSettingMode = false;
int lastRawPotValue = -100;
uint16_t mainRainbowOffset = 0;
uint16_t ldrRainbowOffset = 0;
// Key: tracks the actual calendar day (1 through 31)
int lastRecordedDay = -1;
void setup() {
Serial.begin(9600);
pinMode(setButtonPin, INPUT_PULLUP);
pinMode(gameButtonPin, INPUT_PULLUP);
matrixTens.begin(0x70);
matrixOnes.begin(0x71);
matrixTens.setTextSize(1);
matrixTens.setTextWrap(false);
matrixTens.setTextColor(LED_ON);
matrixOnes.setTextSize(1);
matrixOnes.setTextWrap(false);
matrixOnes.setTextColor(LED_ON);
if (!rtc.begin()) {
Serial.println("Couldn't find RTC!");
while (1);
}
// Flash the computer's exact current time onto the clock module
if (rtc.lostPower()) {
Serial.println("RTC lost power, syncing to computer time...");
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
// Initialize Main Strip
mainStrip.begin();
mainStrip.setBrightness(40); // SAFE FOR USB POWER
mainStrip.show();
// Initialize LDR Strip
ldrStrip.begin();
ldrStrip.setBrightness(40); // SAFE FOR USB POWER
ldrStrip.show();
}
void loop() {
// Ask the clock for the exact date and time
DateTime now = rtc.now();
// ==========================================
// 1. LDR & DICE BOX LED
// ==========================================
int lightValue = analogRead(ldrPin);
if (lightValue < ldrThreshold) {
ldrRainbowCycle();
} else {
setLdrWhite();
}
// ==========================================
// 2. COUNTDOWN & MATRIX
// ==========================================
int currentRawPot = analogRead(potPin);
// Wake up if knob is twisted
if (abs(currentRawPot - lastRawPotValue) > 20) {
isSettingMode = true;
lastRawPotValue = currentRawPot;
}
if (isSettingMode) {
liveScore = map(currentRawPot, 0, 1023, 0, 30);
matrixTens.blinkRate(2);
matrixOnes.blinkRate(2);
// Button to confirm
if (digitalRead(setButtonPin) == LOW) {
daysLeft = liveScore;
isSettingMode = false;
lastRawPotValue = currentRawPot;
// Lock in the exact calendar day eg) the 20th
lastRecordedDay = now.day();
delay(300);
}
}
else {
matrixTens.blinkRate(0);
matrixOnes.blinkRate(0);
if (lastRecordedDay != -1 && now.day() != lastRecordedDay) {
if (daysLeft > 0) {
daysLeft--;
}
// Update the recorded day so it waits for tomorrow midnight
lastRecordedDay = now.day();
}
}
// Update Matrix Screens
int displayScore = isSettingMode ? liveScore : daysLeft;
matrixTens.clear();
matrixOnes.clear();
matrixTens.setCursor(1, 0);
matrixOnes.setCursor(1, 0);
matrixTens.print(displayScore / 10);
matrixOnes.print(displayScore % 10);
matrixTens.writeDisplay();
matrixOnes.writeDisplay();
// ==========================================
// 3. MAIN LED & 4-STATE GAME LOGIC
// ==========================================
bool gameButtonsPressed = (digitalRead(gameButtonPin) == LOW);
if (!isSettingMode && daysLeft == 0) {
if (gameButtonsPressed) {
mainRainbowCycle(); // MORE than 5 players AND 0 days till game
} else {
setMainRed(); // LESS than 5 players but 0 days till game
}
}
else {
if (gameButtonsPressed) {
setMainGreen(); // 5 players but MORE than 0 days till game
} else {
setMainWhite(); // DEFAULT STATE
}
}
delay(20);
}
// --- HELPER FUNCTIONS for MAIN LED HOUSING ---
void mainRainbowCycle() {
for (int i = 0; i < mainStrip.numPixels(); i++) {
int pixelHue = mainRainbowOffset + (i * 65536L / mainStrip.numPixels());
mainStrip.setPixelColor(i, mainStrip.gamma32(mainStrip.ColorHSV(pixelHue)));
}
mainStrip.show();
mainRainbowOffset += 256;
}
void setMainWhite() {
for (int i = 0; i < mainStrip.numPixels(); i++) {
mainStrip.setPixelColor(i, mainStrip.Color(255, 255, 255));
}
mainStrip.show();
}
void setMainGreen() {
for (int i = 0; i < mainStrip.numPixels(); i++) {
mainStrip.setPixelColor(i, mainStrip.Color(0, 255, 0));
}
mainStrip.show();
}
void setMainRed() {
for (int i = 0; i < mainStrip.numPixels(); i++) {
mainStrip.setPixelColor(i, mainStrip.Color(255, 0, 0));
}
mainStrip.show();
}
// --- HELPER FUNCTIONS FOR DICE HOUSE LED STRIP ---
void ldrRainbowCycle() {
for (int i = 0; i < ldrStrip.numPixels(); i++) {
int pixelHue = ldrRainbowOffset + (i * 65536L / ldrStrip.numPixels());
ldrStrip.setPixelColor(i, ldrStrip.gamma32(ldrStrip.ColorHSV(pixelHue)));
}
ldrStrip.show();
ldrRainbowOffset += 256;
}
void setLdrWhite() {
for (int i = 0; i < ldrStrip.numPixels(); i++) {
ldrStrip.setPixelColor(i, ldrStrip.Color(255, 255, 255));
}
ldrStrip.show();
}