Final Project
Slide
Video
Presentation link
Here's a link to the presentation day stream that starts when my presentation slot begins.
Overview
My project idea:
A toilet seat that lifts itself and puts itself back down.
But, why??
Since the advent of toilets, men and woman have battled one another over the lid.
Personally, I don't love touching toilet seats, but I always lift it up and put it down.
This, in my mind, is a "frictitious inefficiency".
At scale, I believe that we can change our world by solving one micro-friction at a time.
Why not begin in the loo?
Where it all began...
Before joining Fab Academy I was frustrated by my lack of ability to make my ideas. Below are some images of my first attempts at making the toilet seat lifter (I made these before reaching out to Henk about joining the program).
FAIL VIDEOS OF THE PROTOTYPES
Not great...
... but, in the documentation below you'll see the results of my Fab learnings, over the past 5/6 months. It's the culmination of how I went from knowing how to make very little to learning how to make (almost) anything.
The plan
- Make use of the space between the seat an the wall / cistern
- For stability and positioning, the design of base of the device should incorporate the two bolts that hold (most) toilet seats to the bowl
- Power my device with a stepper motor (or two) for the lifting of the lid
Combine all of those points with a mechanism like this draw bridge.
Mechanics (step one)
Make the lid go up.
I added end caps to a basic structure for a shaft to go through. The wire connecting the stepper and the toilet lid would be pulled over it.
To test, I used a CNC shield and attached one end of a cable to the stepper motor and the other to the front of the toilet seat.
The stepper motor didn't have enough power to lift the lid.
I added a counter balance and the physics of the system worked. However, it wasn't a sustainable option.
After a few beers, Henk convinced me that a gear system would help me more easily lift the lid. The next day, I tested that in the lab using Lego.
I modeled and 3D printed a gear that would fit on the stepper motor shaft. My first print was too small.
During this process I was also introduced to a very special book by David Macaulay. It breaks down a massive range of topics into easily understandable parts through the use of illustrations.
The gear ratio was 24:12 (2:1).
I later changed that to 48:15 (16:5). More torque (but also slower output speed).
LIFTOFF! I was able to show that the basic structure / system would function.
It also goes down smoothly
Design (step two)
I used Fusion to design parts for my project. From there I 3D printed or laser cut them.
First, I made the end plates that would hold the bearings that the middle shaft would go through.
This is how they turned out:
Connected to the middle shaft were the larger gear and the spool. The spool is what the wire to to be wound around. On Saco's advice, the spool was designed including a set screw. The set screw secures the spool in place. This worked well, for a while, but eventually I needed to cut notches in the shaft, so that the spool was extra tight.
Since the larger gear would be on the same (round) shaft as the spool, I fitted it with a set screw as well.
The last part to design was the seat clamp. It includes space for a servo motor and a hole for it's cables to come out of. The wire is fastened using a washer and a bolt, which also holds the clamp tight on the toilet lid.
Production + system integration (step three)
In this section I outline the main components of the project. But first, here's a pic of how it all came together:
Base
The base was very satisfying. With earlier attempts I made a space for suction cups to fit into, but I was short on time, so I opted to save that for future spirals.
This is how it turned out.
The baseplates were superglued together.
This is how the base was designed to secure in place. Unfortunately, the bolts of my test toilet seat were much closer together than the actual toilet I tested on. Which meant that I had to quickly recut a base for making my video.
Stepper motor
The middle shaft holds the spool and larger gear. The shaft is held above the stepper motor in bearings which are in acrylic holders.
The limit switch stops the stepper turning when it's pressed.
Wire / cable (from lid to stepper)
For my project, I used a soft+flexible beading wire. It was held in place by a washer that was tightened by a bolt that held the clamp to the toilet lid.
It worked pretty well, and it's cheap, so I don't have plans to change that.
Below is a decent view of how the wire is connected.
Lid + seat clamp
I used a YouTube tutorial to learn how to make a clamp in Fusion.
There's a servo attached to the clamp so that users can toggle between lifting just the lid, or the lid and the seat.
I thought about using an electromagnet to do that job, but apparently they use a lot of power.
Electronics design + production
Motor board
The board that controlled the stepper and servo was originally designed using a XIAO ESP32c3 as it's MCU. However, once I added the remote control communication over ESP-NOW, the c3 didn't have enough processing power, and it was swapped with a c6.
I had more fun plans for the design of the board, but there were some costly errors that meant I had to use a rectangle outline, in the end.
My first board was a Frankenstein
Button / sensor module
The controller has four buttons. One to lift just the toilet lid. The second is for lifting the lid and seat. The third is to lower the seat. The fourth was useful to have for developing the system.
Initially, I was going to have the toilet seat lifting based on motion sensors. I tested the movements and found that it wasn't so nice to involve the feet in toilet lifting process.
For a later spiral, I would like to add a millimeterwave sensor, to detect when the user has left the bathroom and to then lower the seat. The millimeter wave sensor would sit in the button controller. The button controller would be stuck against a wall using double sided tape or a suction cup.
The controller unit includes a 3.7V battery and a 03962a Battery Charger (micro-USB).
I designed a housing for the button controller, but the 3D print was the wrong size and I didn't have time to make one that fit. In my presentation video I was just holding the board against the controller case, but they weren't actually integrated properly.
The case includes a slot for a micro usb charger. And at the top is a space for adding a millimeter wave sensor in future spirals.
Power supply
The stepper motor requires at least 12V, so I've decided to make the first spiral wall powered (12V / 1.5A wall plug).
Programming
Documented at the end of System integration week.
Final working code (lifting device)
// This is the motor board
#include <ESP32Servo.h>
#include <esp_now.h>
#include <WiFi.h>
#include <AccelStepper.h>
// Pin definitions (change as needed)
#define STEPPER_STEP_PIN D0
#define STEPPER_DIR_PIN D1
#define SERVO_PIN 2
#define LIMIT_SWITCH_PIN D8
AccelStepper stepper(AccelStepper::DRIVER, STEPPER_STEP_PIN, STEPPER_DIR_PIN);
Servo myservo1;
unsigned long servoMoveTime = 0;
bool servoMoving = false;
bool stepperShouldStart = false;
long pendingStepperTarget = 0; // Position-based
int pendingServoPosition = 65;
int lastButtonState = 0;
const unsigned long SERVO_DELAY = 500; // Adjust based on your servo speed
// Limit switch variables
volatile bool limitSwitchTriggered = false;
bool ignoreLimitSwitch = false; // Flag to ignore limit switch
typedef struct test_struct {
int buttonState;
} test_struct;
test_struct myData;
// Interrupt Service Routine for limit switch
void IRAM_ATTR handleLimitSwitch() {
if (!ignoreLimitSwitch) {
limitSwitchTriggered = true;
}
}
void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) {
memcpy(&myData, incomingData, sizeof(myData));
Serial.print("Bytes received: ");
Serial.println(len);
Serial.print("Button number: ");
Serial.println(myData.buttonState);
Serial.println();
}
void setup() {
Serial.begin(115200);
myservo1.attach(SERVO_PIN);
myservo1.write(65); // Initial position
// Configure limit switch pin and interrupt
pinMode(LIMIT_SWITCH_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(LIMIT_SWITCH_PIN), handleLimitSwitch, FALLING);
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
// Stepper configuration
stepper.setMaxSpeed(1000);
stepper.setAcceleration(1000); // Smooth starts/stops
stepper.setCurrentPosition(0);
}
void loop() {
// Limit switch handling (only when not ignored)
if (limitSwitchTriggered && !ignoreLimitSwitch) {
limitSwitchTriggered = false;
stepper.stop(); // Stop stepper immediately
stepper.setCurrentPosition(0); // Reset logical position
Serial.println("Limit switch triggered - Stepper stopped and position reset!");
}
// Check if button state has changed
if (myData.buttonState != lastButtonState) {
lastButtonState = myData.buttonState;
// Determine servo position and stepper target position based on button state
switch (myData.buttonState) {
case 1:
pendingServoPosition = 65;
pendingStepperTarget = 10000; // Arbitrary positive position
ignoreLimitSwitch = false; // Enable limit switch for cases 1 and 2
break;
case 2:
pendingServoPosition = 90;
pendingStepperTarget = 10000;
ignoreLimitSwitch = false; // Enable limit switch for cases 1 and 2
break;
case 3:
pendingServoPosition = 90;
pendingStepperTarget = -3500; // Arbitrary negative position
ignoreLimitSwitch = true; // Ignore limit switch for case 3
Serial.println("Case 3: Limit switch disabled");
break;
case 4:
pendingServoPosition = 65;
pendingStepperTarget = -3500; // Fixed: removed duplicate assignment
ignoreLimitSwitch = true; // Ignore limit switch for case 4
Serial.println("Case 4: Limit switch disabled");
break;
default:
pendingServoPosition = 65;
pendingStepperTarget = stepper.currentPosition(); // Stay in place
ignoreLimitSwitch = false; // Enable limit switch by default
break;
}
// Clear any pending limit switch trigger when switching modes
limitSwitchTriggered = false;
// Start servo movement first
myservo1.write(pendingServoPosition);
servoMoveTime = millis();
servoMoving = true;
stepperShouldStart = false;
// Stop stepper immediately when new command comes
stepper.stop();
}
// Check if servo has finished moving and start stepper
if (servoMoving && (millis() - servoMoveTime >= SERVO_DELAY)) {
servoMoving = false;
stepperShouldStart = true;
}
// Start stepper after servo delay
if (stepperShouldStart) {
stepper.moveTo(pendingStepperTarget); // Position-based movement
stepperShouldStart = false;
}
// Always run stepper (non-blocking)
stepper.run();
}
Final code for remote control
#include <esp_now.h>
#include <WiFi.h>
uint8_t GrannySmith[] = {0xe4, 0xb3, 0x23, 0xb5, 0x9d, 0xd8};
const byte buttonPins[] = {D5, D8, D9, D10};
const byte ledPins[] = {D1, D2, D3, D4}; // LED pins corresponding to buttons
const int numButtons = 4;
unsigned long lastMillis[numButtons] = {0};
byte lastPress[numButtons] = {HIGH, HIGH, HIGH, HIGH};
bool buttonPressed(int buttonIndex)
{
byte currentPress = digitalRead(buttonPins[buttonIndex]);
if(currentPress != lastPress[buttonIndex])
{
if(millis() - lastMillis[buttonIndex] < 200) return false;
lastPress[buttonIndex] = currentPress;
if(currentPress == LOW)
{
Serial.print("Button ");
Serial.print(buttonIndex + 1);
Serial.println(" pressed!");
// Turn off all LEDs first
for(int i = 0; i < numButtons; i++) {
digitalWrite(ledPins[i], LOW);
}
// Turn on only the LED for the pressed button
digitalWrite(ledPins[buttonIndex], HIGH);
Serial.print("LED ");
Serial.print(buttonIndex + 1);
Serial.print(" (Pin D");
Serial.print(buttonIndex + 1);
Serial.println(") turned ON - all others OFF");
lastMillis[buttonIndex] = millis();
return true;
}
}
return false;
}
typedef struct test_struct {
int buttonState;
} test_struct;
test_struct test;
esp_now_peer_info_t peerInfo;
// callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
char macStr[18];
Serial.print("Packet to: ");
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
Serial.print(macStr);
Serial.print(" send status:\t");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}
void setup() {
Serial.begin(115200);
// Initialize button pins
for(int i = 0; i < numButtons; i++)
{
pinMode(buttonPins[i], INPUT_PULLUP);
}
// Initialize LED pins
for(int i = 0; i < numButtons; i++)
{
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW); // Start with all LEDs off
}
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_register_send_cb(OnDataSent);
peerInfo.channel = 0;
peerInfo.encrypt = false;
memcpy(peerInfo.peer_addr, GrannySmith, 6);
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer");
return;
}
Serial.println("Master Board Ready");
Serial.println("LED Control Mapping (Exclusive):");
Serial.println("Button 1 (D5) -> LED1 (D1)");
Serial.println("Button 2 (D8) -> LED2 (D2)");
Serial.println("Button 3 (D9) -> LED3 (D3)");
Serial.println("Button 4 (D10) -> LED4 (D4)");
Serial.println("Only one LED will be on at a time!");
}
void loop() {
bool buttonWasPressed = false;
for(int i = 0; i < numButtons; i++)
{
if(buttonPressed(i))
{
test.buttonState = i + 1;
Serial.print("Sending button state: ");
Serial.println(test.buttonState);
buttonWasPressed = true;
break;
}
}
// Send ESP-NOW message if a button was pressed
if(buttonWasPressed) {
esp_err_t result1 = esp_now_send(GrannySmith, (uint8_t *) &test, sizeof(test_struct));
if (result1 == ESP_OK) {
Serial.println("Sent with success");
}
else {
Serial.println("Error sending the data");
}
}
delay(100);
}
Bill of Materials (BOM)
Component | Quantity | Supplier | Total Price |
---|---|---|---|
Nema 17 stepper motor | 1 | fab inventory | €12.00 |
Daiwa 626ZZ Ball Bearing | 2 | tinytronics.nl | €2.00 |
4mm shaft | 1 | fab inventory | €2.00 |
608ZZ bearing | 2 | fab inventory | €3.00 |
8mm shaft | 1 | fab inventory | €3.00 |
DRV8825 step stick | 1 | tinytronics.nl | €5.00 |
XIAO ESP32C3 MCU | 1 | tinytronics.nl | €6.50 |
XIAO ESP32C6 MCU | 1 | tinytronics.nl | €8.25 |
DC-DC Verstelbare Step-down Buck Converter LM2596 3A | 1 | Sam | €3.00 |
Wire (to servo) | --- | fab inventory | €1.00 |
Knorr Prandell bead thread | --- | fab inventory | €2.75 |
Aluminum extrusions | --- | fab inventory | €5.00 |
220 ohm resistor | --- | fab inventory | €0.09 |
SM-S2309S servo motor | 1 | birthday present | €9.92 |
Omron SMD buttons | 4 | fab inventory | €2.88 |
LEDs | 4 | fab inventory | €0.48 |
FR1 | --- | fab inventory | €1.00 |
Bolts | --- | fab inventory | €1.00 |
Micro Switch V-156-1C25 | 1 | Henk | €0.75 |
01x04 horizontal SMD pin header | 2 | fab inventory | €1.12 |
01x03 horizontal SMD pin header | 1 | fab inventory | €1.12 |
01x02 vertical SMD pin socket | 1 | fab inventory | €0.60 |
PJ-002AH-SMT-TR (power jack) | 1 | fab inventory | €1.21 |
12V / 1.5mAh cable | 1 | fab inventory | €15.00 |
10k ohm resistor | 1 | fab inventory | €0.09 |
100 ohm resistor | 4 | fab inventory | €0.36 |
0 ohm resistor | 3 | fab inventory | €0.27 |
100uF electrolytic capacitors | 2 | fab inventory | €0.92 |
3.7V battery | 1 | Henk | €10.00 |
03962a Battery Charger | 1 | Sam | €2.00 |
TOTAL | €106.65 |
Design files
- Edge Cuts motor board
- F.Cu motor board
- Fusion design files
- Button controller - PCB
- Button controller - Schematic
- Machine board - PCB
- Machine board - Schematic
Answered questions
- Power and control
- Communication protocol?
- What material will the base / clamp / cables be?
- How do I power the device?
- How do I stop moisture getting in?
- How do I get electricity to the servo clamp?
- How do I make the battery detachable? (later spiral?)
- Do I use a CNC shield or make my own circuit?
- What components are needed?
- What do I use for the structure?
License
You just DO WHAT THE FUCK YOU WANT TO.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
What have I learned / A reflection
Work on my final project began in earnest around the end of interface+application week. Since then, my biggest takeaways from the final project process are that I've learned just how important system integration is. I can really appreciate when it's been done well. This, I would say is the aspect of my project that I would most like to work on in future spirals, if I were to do them. The aluminum extrusions were just a temporary solution.
The other thing that stood out to me is that time management theory still confuses me and I'd rather just sprint / spend my weekends working than have to get too stuck into planning. I found that in the beginning of the final phase of the final project I spent a lot of time trying to plan things, but ultimately, it worked better that I swung from vine to vine while keeping my eye on how the project was progressing as a whole. I would say that my favorite project management approach is "parallel juggling".
Lastly, I relearned how much I enjoy storytelling. I hope I can make documentation a part of all my future projects.
As a reflection on the course, it feels like it was just an introduction to making almost anything (which seems obvious now that I've typed it out). There's so much underneath each of the weekly topics, but just knowing that all of these topics exist, and seeing how they're used, gives me a framework to guide my future making processes and the confidence to try.
From here I feel I could make almost anything, it's just a matter of time... and gravity.