Final Project
Slide
Video
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.
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
Before designing + milling the board, I made an electronics diagram for the project.
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).
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);
}
Who's done what (already)?
- Household hacker -- string lift
- A toilet seat that always stays up + their YouTube
- Self lifting toilet seat (entire seat)
- Toilet seat lifter project - Kochi
And my personal favorite:
- Flush seat goes down
- And this is their website
There's also this idea, that I found to be quite novel.
All but two of the examples require a new seat to be bought. And the other two are focused solely on putting the seat down (not up).
My project allows users to install it without buying a new seat, and it is capable of going up and down.
What worked?
The toilet lifted. The toilet went down. So, the mechanics worked.
The electronics also worked.
The code got the job done, but not exactly how I would like it. But it worked.
Everything worked, I would say, except for the base. And I think that was due to needing to replace the whole part at the last minute (the day before I flew to Barcelona).
What didn't work?
The base. I had designed it around the toilet seat that was set up the lab, but when I moved over to test the device on a real toilet, the gap between the bolts was different. That made it almost impossible to keep the device upright.
The code. It mostly worked, but there were some quirks that needed smoothing out. For instance, the settings that lowered down the seat were based on the stepper's position, which had a tendency to change over time.
The ESP32c3. For controlling the servo and stepper motor (and limit switch), so I used the XIAO ESP32c6.
What did work, but I would improve?
The clamp. When screwed in, the bolt would damage the top of the toilet. I added a leftover piece of acrylic to stop the scratching, but I would like to make something that is permanent.
The clamp again. Even though it actually worked really well, I would have liked to make the design a bit wider, so that the clamp wouldn't wriggle from side to side.
The power supply. I don't like having a cable near the toilet.
The base. I would add suction cups at the back of the base. They were really effective. Combined with the way the base is currently designed, I think it would work well in combination.
The base. I would make the outer arms of the baseplate a bit longer, to improve it's hold / resistance to tipping forward.
The spool. It held the wire well, but sometimes the wire would slip off the spool to the right or left, which would change the amount of steps it took for the lid to be returned back down.
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 | --- |
608ZZ bearing | 2 | fab inventory | €3.00 |
8mm shaft | 1 | fab inventory | --- |
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 | --- |
Knorr Prandell bead thread | Pipoos | fab inventory | 2.75 |
Aluminum extrusions | --- | fab inventory | --- |
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 | --- |
Bolts | --- | fab inventory | --- |
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 | --- |
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 |
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?