12. Machine Week¶
Mechanical Design (part 1 of 2)
Group assignment: - Design a machine that includes mechanism + actuation + automation + application - Build the mechanical parts and operate it manually - Document the group project
Individual assignment:
- Document your individual contribution
Machine Design (part 2 of 2)
Group assignment:
- Actuate and automate your machine
- Document the group project
Individual assignment:
- Document your individual contribution
Overview¶
For machine week, I worked with Elle Hahn and Cooper Cumbus, and we decided to make a Whack-A-Mole machine this week. I will document my individual contributions below, and you can access our full group documentation here. I was assigned the role of “group leader” this week, so one of my contributions was making sure that everyone stayed on top of their work and documentation, which both Cooper and Elle did a really good job with.
Making the Case¶
While Elle and Cooper worked on getting all the heads to move up and down, I worked on making the case for our game. I wanted to laser cut the case because I feel very comfortable using the laser cutter and thought it would be the easiest thing to use for precise cuts for this project. I designed the file in CorelDraw, cardboard cut it, and then eventually, when all the dimensions were essentially perfect, I laser cut on 1/8” wood. It took several cardboard cuts with different dimensions to finally get the box to fit nicely. Once I laser cut all the pieces, I used hot glue to stick them together, but I left the lid off, so that the Servos could be placed inside.
Cooper recommended that we make it press fit, so I designed a case that was approximately 7 inches tall and 12 inches wide. I made a 1” by 1” hole in the back side for the power source and on the top, I made 5 evenly spaced circles with diameters around 3”. At the back of the top, I made some grooves for the backboard of our game.
Here is the CorelDraw box design:
I laser cut the box 3 times on cardboard before finally getting it to look good enough. One of Andrew Puky’s friends was visiting from Germany and wanted to participate, so he helped hot glue the second and third cardboard tests together.
The first time I laser cut the box, I hot glued it together and it ended up ot fitting together even though I thought I had the dimensions correct. I assumed that the issue was because of my mesy hot gluing, so laser cut the same design again and had some help with the hot gluing to see if someone else could make it fit. However, that did not resolve the issue, so instead of going back to the old CorelDraw file and testing every line to make sure it was the right dimension, I just fully redesigned it because the height needed to be increased as well.
Laser cutting the case:
Raster:
Vector:
Here is the laser cut first rendition of the cardboard case:
I did not glue the lid on because I wanted Elle to be able to test the electronics inside of the case.
Here is the laser cut second rendition of the cardboard case:
After figuring out the motor situation, I needed to resize the casing to make sure the head would be able to come up enough and go down enough.
Here are some pictures and a video of Elle and I measuring the situation with the cardboard:
Here is the laser cut third rendition of the case being assembled:
For the backboard, I found a picture online, removed the background, traced it in CorelDraw, and then rastered it onto a piece of wood using the laser cutter.
This is the picture I found online:
This is the picture with the background removed using remove.bg:
This is the picture after tracing it in CorelDraw:
It took me several tries to get the imaged traced without details being removed. I also added the outline for the sign.
Here is the backboard being rastered:
Here is the laser cut backboard on the assembled cardboard casing:
Programming¶
We decided to use a touch sensor at the top of each head so that when the head is whacked, it knows to go down without too much force being applied. Since I had already tried using the HW-139 touch sensor during Input Week as a potential sensor for my final project, I figured that could work for this project as well. I did some initial tests before we incorporated it into our project just to make sure it would work if not touched by a human finger because our Whack-A-Mole machine will use a mallet-like tool to hit the heads. This worked, but when testing it, I learned that the touch sensor needs to be firmly held down in order to read the signal.
The HW-139 is very simple and only has 3 pins: one to ground, one to power, and one to read signal.
This is the wiring using a XIAO ESP32-C3:
This is the code I used:
const int touchPin = 3; // GPIO3 on ESP32-C3
void setup() {
pinMode(touchPin, INPUT);
Serial.begin(115200);
Serial.println("Touch sensor ready");
}
void loop() {
int touchState = digitalRead(touchPin);
if (touchState == HIGH) {
Serial.println("Touched!");
}
delay(100); // Debounce delay
}
This is the touch sensor working using a taped caliper as the model mallet:
On the computer screen, you can see the Arduino terminal says “Touched!” whenever the touch sensor is hit. The ultimate plan with this touch sensor in the project is to place a touch sensor on top of each of the heads and send a signal when the touch sensor is hit to the servo for that head.
We ended up switching to the built-in touch sensors on the Xiao ESP32-S3, so I worked on making the code for these touch sensors and the heads. The final code ended up being a collaboration between all three of us. Cooper and I made the code for the game part and started the web server, and Elle finalized the web server.
This is the final code we used:
#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>
// Wi-Fi credentials
// Web server
WebServer server(80);
// Pins and thresholds
const int servoPins[] = {7, 44, 43}; // Servo pins
const int touchPins[] = {2, 3, 4}; // Touch input pins
const long touchThresholds[] = {35000, 35000, 60000}; // Adjust if needed
const int numMoles = 3;
Servo servos[numMoles];
// Angles
const int popUpAngle = 130; // Mole visible (servo up)
const int hideAngle = 20; // Mole hidden (servo down)
// Game state
int currentMole = -1;
int score = 0;
int highScore = 0;
bool gameRunning = false; // Track if game is running
bool lastTouchState = false; // Track previous touch state to prevent multiple triggers
// Timer variables
unsigned long gameStartTime = 0;
const unsigned long gameDuration = 30000; // 30 seconds in milliseconds
void setup() {
Serial.begin(115200);
randomSeed(analogRead(0));
// Attach servos and ensure all start hidden
for (int i = 0; i < numMoles; i++) {
servos[i].attach(servoPins[i]);
servos[i].write(hideAngle);
}
delay(500); // Give servos time to move to hide position
// Wi-Fi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.println("\nConnected! IP: " + WiFi.localIP().toString());
// Define routes
server.on("/", handleRoot);
server.on("/start", handleStart);
server.on("/stop", handleStop);
server.begin();
// Initially game is stopped
currentMole = -1;
}
void loop() {
server.handleClient();
if (!gameRunning) return; // Skip mole logic if game stopped
// Check if time is up
unsigned long elapsed = millis() - gameStartTime;
if (elapsed >= gameDuration) {
Serial.println("Time's up! Stopping game.");
stopGame();
return;
}
// Only check for touches if there's an active mole
if (currentMole == -1) return;
// Only check the touch sensor corresponding to the current active mole
long touchVal = touchRead(touchPins[currentMole]);
bool currentTouchState = (touchVal > touchThresholds[currentMole]);
// Debug output to help calibrate thresholds
Serial.printf("Mole %d - Touch value: %ld (threshold: %ld) - %s\n",
currentMole, touchVal, touchThresholds[currentMole],
currentTouchState ? "TOUCHED" : "not touched");
// Only trigger on a new touch (edge detection)
if (currentTouchState && !lastTouchState) {
Serial.printf("Hit mole %d! Score: %d -> %d\n", currentMole, score, score + 1);
// Hide the hit mole immediately
servos[currentMole].write(hideAngle);
score++;
// Update high score
if (score > highScore) {
highScore = score;
}
// Reset touch state
lastTouchState = false;
// Brief delay for visual feedback and debouncing
delay(300);
// Spawn the next mole
spawnNewMole();
} else {
lastTouchState = currentTouchState;
}
delay(50); // Small delay to prevent overwhelming serial output
}
void spawnNewMole() {
// Ensure all servos are hidden first
for (int i = 0; i < numMoles; i++) {
servos[i].write(hideAngle);
}
// Small delay to ensure servos have moved
delay(200);
// Select next mole (different from current one)
int nextMole;
do {
nextMole = random(numMoles);
} while (nextMole == currentMole && numMoles > 1); // Avoid repeating if possible
// Update current mole and pop it up
currentMole = nextMole;
servos[currentMole].write(popUpAngle);
// Reset touch state when spawning new mole
lastTouchState = false;
Serial.printf("Mole %d popped up!\n", currentMole);
}
void hideAllMoles() {
for (int i = 0; i < numMoles; i++) {
servos[i].write(hideAngle);
}
currentMole = -1;
}
void stopGame() {
gameRunning = false;
hideAllMoles();
}
void handleStart() {
if (!gameRunning) {
score = 0;
gameRunning = true;
gameStartTime = millis();
spawnNewMole();
}
server.sendHeader("Location", "/");
server.send(302, "text/plain", "");
}
void handleStop() {
if (gameRunning) {
stopGame();
}
server.sendHeader("Location", "/");
server.send(302, "text/plain", "");
}
void handleRoot() {
unsigned long remaining = 0;
if (gameRunning) {
unsigned long elapsed = millis() - gameStartTime;
if (elapsed < gameDuration) {
remaining = (gameDuration - elapsed) / 1000;
}
}
String html = "<html><head>"
"<meta http-equiv='refresh' content='1'/>"
"<style>"
"body { font-family: sans-serif; text-align: center; }"
"button { padding: 10px 20px; font-size: 20px; margin: 10px; }"
"</style></head><body>";
html += "<h1>Whack-a-Mole</h1>";
html += "<p><strong>Score:</strong> " + String(score) + "</p>";
html += "<p><strong>High Score:</strong> " + String(highScore) + "</p>";
html += "<p><strong>Current Mole:</strong> " + (currentMole == -1 ? "None" : String(currentMole + 1)) + "</p>";
if (gameRunning) {
html += "<p><strong>Time Remaining:</strong> " + String(remaining) + " seconds</p>";
html += "<p><button onclick=\"location.href='/stop'\">Stop Game</button></p>";
} else {
html += "<p><button onclick=\"location.href='/start'\">Start Game</button></p>";
}
html += "</body></html>";
server.send(200, "text/html", html);
}
One of the things I learned while making this code was to always be extremely specific with AI prompts and to not be completely dependent on the AI to achieve your end goal first try. Collectively, our group probably had to test the code at least 10 times (probably more) before getting even a decent outline of our game. ChatGPT was very helpful in making our code, but it also took persistence and thoughtfulness to give it the correct prompt and follow up responses to get the ideal code.
Reflection¶
I really enjoyed this week, and although it was challenging, this week was probably my favorite so far. I feel like we divided the work up pretty evenly, so it was not too overwhelming for any specific person. This week also helped me get a good sense of how long it can take to work on a bigger project which should help me with time management for my final project
Files and AI Help¶
All our files and some AI conversations for this week are located on our group page.