Final Project

Slide

presentation slide

Video

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

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

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.

way things work

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.

timber

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

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

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.

case

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

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

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.