System integration
Assignment:
- Design and document the system integration for your final project
Have you answered these questions?
- implemented methods of packaging?
- designed your final project to look like a finished product?
- documented system integration of your final project?
- linked to your system integration documentation from your final project page?
This is the model example of this week's documentation -- as shown in Neil's weekly notes.
BOM
Component | Info | Quantity | Supplier | Product code | Price | Total |
---|---|---|---|---|---|---|
Nema 17 | --- | --- | --- | --- | --- | --- |
Servo motor | --- | --- | --- | --- | --- | --- |
WTOO 624Z Bearings | --- | --- | --- | --- | --- | --- |
4mm shaft | --- | --- | --- | --- | --- | --- |
608ZZ bearing | --- | --- | --- | --- | --- | --- |
8mm shaft | --- | --- | --- | --- | --- | --- |
DRV8825 step stick | --- | --- | --- | --- | --- | --- |
XIAO ESP32C3 MCU | --- | --- | --- | --- | --- | --- |
100 uF capacitor | --- | --- | --- | --- | --- | --- |
HW 411 buck converter | --- | --- | --- | --- | --- | --- |
Wire (to servo) | --- | --- | --- | --- | --- | --- |
Knorr Prandell bead thread | --- | --- | --- | --- | --- | --- |
Aluminum extrusions | --- | --- | --- | --- | --- | --- |
220 ohm resistor | --- | --- | --- | --- | --- | --- |
SM-S2309S servo motor | --- | --- | --- | --- | --- | --- |
button | --- | --- | --- | --- | --- | --- |
LED | --- | --- | --- | --- | --- | --- |
FR1 | --- | --- | --- | --- | --- | --- |
Bolts | --- | --- | --- | --- | --- | --- |
XXX | --- | --- | --- | --- | --- | --- |
XXX | --- | --- | --- | --- | --- | --- |
XXX | --- | --- | --- | --- | --- | --- |
XXX | --- | --- | --- | --- | --- | --- |
XXX | --- | --- | --- | --- | --- | --- |
Mechanical
This week I focused on finalizing how the systems of my project would fit together. Henk and I practiced with Lego to help visualize how pulleys can help make the lifting easier.
Frame
The mechanical design utilizes a frame made of aluminum extrusions that is 250mm tall, 8mm deep, and 200mm wide. The only constraint being that there is often minimal space behind a toilet seat. I measured a few of the recurring toilets in my life and 8mm seemed safe / conservative.
Stepper motor
Connected to the frame will be the NEMA 17 stepper motor. The stepper motor turns a spur gear that rotates another, larger, spur gear, that is connected to a 4mm D-shaft. Along the D-shaft is a spool. The spool is connected to a cable. As the motor turns, it will wind up the cable around the spool.
Clamp
On the other end of the cable is a clamp. The clamp holds onto the toilet lid. On top of the clamp is a servo motor that rotates and latches onto the toilet seat below.
The screw that holds the clamp in place will also be attached to the cable that is being pulled by the servo motor.
Cover
The machine will all be encased with a cover. But more on that later, in wildcard week!
Retainer
On Friday Saco came in. He sat with me and helped me make a few adjustments to the design of my project. He inspired a few new things too!
One of the big questions I was trying to solve was how I was going to get the gears to rotate. On the stepper motor, it has a "D-shaft". Which is basically a normal rod, except that it has a flat edge.
Cutting a flat edge into a metal rod is not easily do-able in our lab. Which is where the retainer comes in.
With this design feature, I can tighten components to the shaft / rod.
Electronics system diagram
In Global Open Time Rico had drafted this for me. It's a rough sketch of my electronics system diagram.
Shout out to Rico.
Here's my version:
Wiring and assembly
Servo (to XIAO): 1x GPIO, 1x 5V / VIN, 1x GND
Stepper (to XIAO): 1x 3V - 5V logic power supply, 1x GND, 2x GPIO
NB. RST and SLP pins?
Stepper (to motor power supply): VMOT, GND, 100uF decoupling capacitor, 4x phase pins
Button to XIAO: 220Ω resistor , 1x logic power supply connection, 1x GND, 1x GPIO
Still to consider...
Battery vs Wall power?
I asked AI about 24W portable power supplies and this is one of the options:
You can make your own portable 12V 24W supply by wiring together ten AA NiMH batteries in series (1.2V x 10 = 12V) or using a small sealed lead-acid or Li-ion battery with a suitable voltage and capacity.
Otherwise, here's a 12V 3A wall socket power supply.
Limit switch to XIAO: 1x VCC (3V3 or 5V), 1x GND, 10k resistor
Buck converter
In order to power the XIAO and stepper motor using the same power supply, a buck converter is needed. The buck converter will convert the power supply to 5V for the XIAO.
Here's a bit about how buck converters work.
This is the buck converter that I'll use for my project:
Here's a decent blog post about the HW-411.
Button read / stepper change
I spent a fair bit of time on Friday, Saturday and Sunday setting up the buck converter AND getting a button to change the direction of the stepper motor.
I had shied away from coding at most opportunities during the course. So, to start, I looked up push button tutorials. Here's another one from the same website that was very helpful.
It took a long time to figure out how to get the code to do exactly what I wanted, but in the end, when it did finally worked, it was extremely satisfying.
Below, in the code, I figured out how to use a bool lastButtonState
and int counter
to toggle the stepper motor between moving forwards and backwards.
#include <AccelStepper.h>
const int buttonPIN = D6;
int counter = 0;
bool lastButtonState = LOW;
AccelStepper stepper(AccelStepper::DRIVER, D0, D1);
void setup() {
Serial.begin(9600);
pinMode(buttonPIN, INPUT_PULLUP);
stepper.setMaxSpeed(1000);
stepper.setSpeed(500);
}
void loop() {
int buttonState = digitalRead(buttonPIN);
if (buttonState != lastButtonState) {
lastButtonState = buttonState;
if (buttonState == HIGH) { // Button pressed
counter++;
if (counter >= 2) counter = 0; // Reset after 2 presses
// Toggle speed/direction
if (counter == 1) {
stepper.setSpeed(-500); // Reverse
} else {
stepper.setSpeed(500); // Forward (original speed)
}
}
}
stepper.runSpeed(); // Always run the stepper
}
Digital files
This is the code for the: Button push, change servo direction .ino.
External controller
After some role playing, I realized that my original idea of having a foot activated toilet would actually be kind of annoying. So, I decided to shift to a wall mounter controller.
The controller will have:
- 3x buttons
- 1x neopixel
- 1x XIAO ESP32C3
- 1x TP4056 battery charger
- 1x 3.7V rechargeable battery
Modes
The three buttons will correspond to three modes that the toilet lifter will operate under:
- One (Piss)
- Two (Lid)
- Henk (EMERGENCY)
Option One will make the servo go to 90. Which will lift the toilet seat and the lid.
Option Two will make the servo go to 65. And will only lift the toilet lid.
Option Henk will make the servo go to 90 AND it will make the toilet seat lift a lot faster.
Stepper movement
In all modes the stepper must turn in the direction that winds the cable in until it hits the limit switch.
I'm still not sure by how much exactly the stepper should wind the other way, when it should start doing that, and what activates that happening.
ESP-NOW
Here is the first code that I got working that sends the bool value of the limitSwitch from one ESP32 to the ESP32 other.
Master
#include <esp_now.h>
#include <WiFi.h>
uint8_t GrannySmith[] = {0x64, 0xe8, 0x33, 0x00, 0x9c, 0xfc};
const int limitSwitchPin = D0;
typedef struct test_struct {
bool switchState;
} 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: ");
// Copies the sender mac address to a string
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);
pinMode(limitSwitchPin, INPUT);
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_register_send_cb(OnDataSent);
// register peer
peerInfo.channel = 0;
peerInfo.encrypt = false;
// register first peer
memcpy(peerInfo.peer_addr, GrannySmith, 6);
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer");
return;
}
}
void loop() {
test.switchState = digitalRead(limitSwitchPin);
Serial.println(digitalRead(limitSwitchPin));
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(2000);
}
Secondary
#include <esp_now.h>
#include <WiFi.h>
typedef struct test_struct {
bool switchState;
} test_struct;
//Create a struct_message called myData
test_struct myData;
//callback function that will be executed when data is received
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("Switch value: ");
Serial.println(myData.switchState);
Serial.println();
}
void setup() {
//Initialize Serial Monitor
Serial.begin(115200);
//Set device as a Wi-Fi Station
WiFi.mode(WIFI_STA);
//Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
// Once ESPNow is successfully Init, we will register for recv CB to
// get recv packer info
esp_now_register_recv_cb(esp_now_recv_cb_t(OnDataRecv));
}
void loop() {
}
Button reading
For the controller, I need the MCU to read three different buttons.
I found this guy's code on an Arduino forum when I was looking up callback functions
#include <MsTimer2.h>
const byte buttonPin = 5;
void setup()
{
Serial.begin(9600);
pinMode(13, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
}
void loop()
{
if(buttonFivePressed())
{
flashLedThirteen(5000);
}
}
bool buttonFivePressed(void)
{
static unsigned long lastMillis = 0;
static byte lastPress = HIGH;
byte currentPress = digitalRead(buttonPin);
if(currentPress != lastPress)
{
if(millis() - lastMillis < 200) return false;
lastPress = currentPress;
if(currentPress == LOW)
{
Serial.println(" button press detected!");
lastMillis = millis();
return true;
}
}
return false;
}
void flashLedThirteen(int ledTime)
{
digitalWrite(13, HIGH);
Serial.println("LED ON");
// sets a new timer callback function
MsTimer2::set(ledTime, [] { // the square brackets define the start of the anonymous callback function which is executed after 5000 milliseconds in this example
digitalWrite(13, LOW);
Serial.println("Timer expired...");
Serial.println("LED OFF");
MsTimer2::stop();
}); // the curly brace defines the end of the anonymous callback function
MsTimer2::start();
}
I wrote this code based on that
const byte buttonPin = 5;
const byte buttonPin2 = 6;
const byte buttonPin3 = 7;
void setup()
{
Serial.begin(9600);
pinMode(13, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
pinMode(buttonPin2, INPUT_PULLUP);
pinMode(buttonPin3, INPUT_PULLUP);
}
void loop()
{
if(buttonFivePressed())
{
digitalWrite(13, HIGH);
}
if(buttonSIXPressed())
{
digitalWrite(13, HIGH);
}
if(buttonSEVENPressed())
{
digitalWrite(13, HIGH);
}
}
bool buttonFivePressed(void)
{
static unsigned long lastMillis = 0;
static byte lastPress = HIGH;
byte currentPress = digitalRead(buttonPin);
if(currentPress != lastPress)
{
if(millis() - lastMillis < 200) return false;
lastPress = currentPress;
if(currentPress == LOW)
{
Serial.println(" button press detected!");
lastMillis = millis();
return true;
}
}
return false;
}
bool buttonSIXPressed(void)
{
static unsigned long lastMillis = 0;
static byte lastPress = HIGH;
byte currentPress = digitalRead(buttonPin2);
if(currentPress != lastPress)
{
if(millis() - lastMillis < 200) return false;
lastPress = currentPress;
if(currentPress == LOW)
{
Serial.println(" button press detected!");
lastMillis = millis();
return true;
}
}
return false;
}
bool buttonSEVENPressed(void)
{
static unsigned long lastMillis = 0;
static byte lastPress = HIGH;
byte currentPress = digitalRead(buttonPin3);
if(currentPress != lastPress)
{
if(millis() - lastMillis < 200) return false;
lastPress = currentPress;
if(currentPress == LOW)
{
Serial.println(" button press detected!");
lastMillis = millis();
return true;
}
}
return false;
}
Then I asked AI to make my code more efficient
const byte buttonPins[] = {5, 6, 7}; // All your button pins in one array
const int numButtons = 3;
// Arrays to track each button's state
unsigned long lastMillis[numButtons] = {0};
byte lastPress[numButtons] = {HIGH, HIGH, HIGH};
void setup()
{
Serial.begin(9600);
pinMode(13, OUTPUT);
// Set up all buttons with a simple loop
for(int i = 0; i < numButtons; i++)
{
pinMode(buttonPins[i], INPUT_PULLUP);
}
}
void loop()
{
// Check all buttons with one loop
for(int i = 0; i < numButtons; i++)
{
if(buttonPressed(i))
{
digitalWrite(13, 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); // Print 1, 2, 3 instead of 0, 1, 2
Serial.println(" pressed!");
lastMillis[buttonIndex] = millis();
return true;
}
}
return false;
}
Made a few minor tweaks to get that working with my XIAO board
const byte buttonPins[] = {D5, D6, D7}; // Changed this to the XIAO button format
const int numButtons = 3;
unsigned long lastMillis[numButtons] = {0};
byte lastPress[numButtons] = {HIGH, HIGH, HIGH};
void setup()
{
Serial.begin(9600);
for(int i = 0; i < numButtons; i++)
{
pinMode(buttonPins[i], INPUT_PULLUP);
}
}
void loop()
{
for(int i = 0; i < numButtons; i++)
{
if(buttonPressed(i))
{
Serial.println(digitalRead(i)); // Checking on Serial if the MCU was reading the correct button pushes
}
}
}
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!");
lastMillis[buttonIndex] = millis();
return true;
}
}
return false;
}
Then I inputted the code for the different button pushes into my earlier ESPNOW code
Master:
#include <esp_now.h>
#include <WiFi.h>
uint8_t GrannySmith[] = {0x64, 0xe8, 0x33, 0x00, 0x9c, 0xfc};
const byte buttonPins[] = {D5, D6, D7};
const int numButtons = 3;
unsigned long lastMillis[numButtons] = {0};
byte lastPress[numButtons] = {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); // Print 1, 2, 3 instead of 0, 1, 2
Serial.println(" pressed!");
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: ");
// Copies the sender mac address to a string
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);
for(int i = 0; i < numButtons; i++)
{
pinMode(buttonPins[i], INPUT_PULLUP);
}
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;
}
}
void loop() {
bool buttonWasPressed = false; // Flag to track if any button was pressed
for(int i = 0; i < numButtons; i++)
{
if(buttonPressed(i))
{
test.buttonState = i + 1; // Store button number (1, 2, 3) not index (0, 1, 2)
Serial.print("Sending button state: ");
Serial.println(test.buttonState);
buttonWasPressed = true;
break; // Exit loop after first button press to avoid conflicts
}
}
// Only send ESP-NOW message if a button was actually 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); // Reduced delay for better responsiveness
}
Secondary
#include <esp_now.h>
#include <WiFi.h>
#include <AccelStepper.h>
AccelStepper stepper(AccelStepper::DRIVER, D0, D1);
typedef struct test_struct {
int buttonState;
} test_struct;
//Create a struct_message called myData
test_struct myData;
//callback function that will be executed when data is received
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);
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.setMaxSpeed(1000);
stepper.setSpeed(500);
}
void loop() {
if (myData.buttonState == 1) {
stepper.setSpeed(-500);
}
if (myData.buttonState == 2) {
stepper.setSpeed(500);
}
if (myData.buttonState == 3) {
stepper.setSpeed(1000);
}
stepper.runSpeed();
}
Non blocking timer
This code adds the stepper
#include <ESP32Servo.h>
#include <esp_now.h>
#include <WiFi.h>
#include <AccelStepper.h>
AccelStepper stepper(AccelStepper::DRIVER, D0, D1);
Servo myservo1;
unsigned long servoMoveTime = 0;
bool servoMoving = false;
bool stepperShouldStart = false;
int pendingStepperSpeed = 0;
const unsigned long SERVO_DELAY = 1000;
typedef struct test_struct {
int buttonState;
} test_struct;
//Create a struct_message called myData
test_struct myData;
//callback function that will be executed when data is received
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(2);
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.setMaxSpeed(1000);
stepper.setSpeed(0);
}
void loop() {
if (myData.buttonState == 1) {
stepper.setSpeed(-500);
myservo1.write(65);
}
else if (myData.buttonState == 2) {
stepper.setSpeed(-1000);
myservo1.write(90);
}
else if (myData.buttonState == 3) {
stepper.setSpeed(1000);
myservo1.write(0);
}
else if (myData.buttonState == 4) {
stepper.setSpeed(0);
myservo1.write(65);
}
else {
stepper.setSpeed(0);
myservo1.write(65);
}
stepper.runSpeed();
}
I asked AI to help write my code above in a way that would make the servo move first and then the stepper motor after, here's the working code for that
#include <ESP32Servo.h>
#include <esp_now.h>
#include <WiFi.h>
#include <AccelStepper.h>
AccelStepper stepper(AccelStepper::DRIVER, D0, D1);
Servo myservo1;
unsigned long servoMoveTime = 0;
bool servoMoving = false;
bool stepperShouldStart = false;
int pendingStepperSpeed = 0;
int pendingServoPosition = 65;
int lastButtonState = 0;
const unsigned long SERVO_DELAY = 500; // Adjust based on your servo speed
typedef struct test_struct {
int buttonState;
} test_struct;
test_struct myData;
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(2);
myservo1.write(65); // Initial position
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.setMaxSpeed(1000);
stepper.setSpeed(0);
}
void loop() {
// Check if button state has changed
if (myData.buttonState != lastButtonState) {
lastButtonState = myData.buttonState;
// Determine servo position and stepper speed based on button state
switch (myData.buttonState) {
case 1:
pendingServoPosition = 65;
pendingStepperSpeed = -500;
break;
case 2:
pendingServoPosition = 90;
pendingStepperSpeed = -1000;
break;
case 3:
pendingServoPosition = 0;
pendingStepperSpeed = 1000;
break;
case 4:
pendingServoPosition = 65;
pendingStepperSpeed = 0;
break;
default:
pendingServoPosition = 65;
pendingStepperSpeed = 0;
break;
}
// Start servo movement first
myservo1.write(pendingServoPosition);
servoMoveTime = millis();
servoMoving = true;
stepperShouldStart = false;
// Stop stepper immediately when new command comes
stepper.setSpeed(0);
}
// 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.setSpeed(pendingStepperSpeed);
stepperShouldStart = false;
}
// Always run stepper (non-blocking)
stepper.runSpeed();
}