Final Project

Slide

presentation 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).

prototype fail

prototype fail

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.

caps

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.

test

The stepper motor didn't have enough power to lift the lid.

stutter

I added a counter balance and the physics of the system worked. However, it wasn't a sustainable option.

counter balance

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.

lego test

lego test

I modeled and 3D printed a gear that would fit on the stepper motor shaft. My first print was too small.

lego test

The gear ratio was 24:12 (2:1).

I later changed that to 48:15 (16:5). More torque (but also slower output speed).

lego gears

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.

holder

This is how they turned out:

holders

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.

This is an example of a set screw

set screw

As found on Core77

spool

Since the larger gear would be on the same (round) shaft as the spool, I fitted it with a set screw as well.

gears

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.

clamp

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:

setup

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.

baseplate

This is how it turned out.

baseplate

The baseplates were superglued together.

baseplate

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.

baseplate

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.

gears

stepper mechanism

behind

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.

3D

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.

3D

Electronics design + production

Before designing + milling the board, I made an electronics diagram for the project.

diagram

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.

other-board

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.

other-outline

My first board was a Frankenstein

production

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.

designing

The controller unit includes a 3.7V battery and a 03962a Battery Charger (micro-USB).

production

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).

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)?

And my personal favorite:

There's also this idea, that I found to be quite novel.

spring loaded

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

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