After some ideation, I researched some examples on how to animate objects using motors and mechanisms. During my research, I came across animatronics projects. This inspired me to create something similar, building upon my previous ideas.
I still wanted to do something that has character. My first idea was to make a sock puppet animatronic that would react to it’s surroundings. After looking at some examples, this idea turned into making an animal animatronic. More specifically, I decided that I would make a toucan, which is my design portfolio mascot.
Here are some similar projects I found along the way. I’ve looked into them and picked up some features from each.
Just like in the animatronic DIY kit, I want to make my toucan have a similar mechanism. Instead of servo motors, I am thinking of using stepper motors to have greater control over the speed of the movements. This way, I can make movements that have more of a character.
Low-Fi Prototyping
I started by constructing a mock-up from cardboard. This allowed me to experiment with motor placement, different motor wheel sizes, wire configurations and where to drill holes in the head. It gave me a general idea of how the system works, as a proof of concept. After the lo-fi prototyping stage, I was convinced that this design had potential and was worth pursuing.
Manual Motor Turn
Controlling the Motor
Potentiometer Control
I moved on to controlling the stepper motor. Here, I hooked it up to the circuit board I made in output devices week, just to see it work.
Motor Turn with Potentiometer
Interface Control
To have better control over the motor, I decided to utilize a network interface. I used the interface and application programming week to create a motor control system. It sends the motor right and left turn commands.
Motor Turn with Network Interface
Motor Turn with Network Interface
Universal Joint & Steel Wires
In the lo-fi prototype, I used a copper wire loop to simulate a universal joint. Now, it was time to create my own. I modeled a joint that uses M2 & M3 screws and bearings. I printed the first version and tested it on my cardboard prototype. Also, this time I used thick steel wires instead of copper ones - which did not deform as easily. I tested the result with my hands, and it was a much consistent control scheme than the first iteration.
Neck Movement with Universal Joint & Steel Wires
Improving Motor Control
How to Achieve Smooth Movement
Going back to motor control, the next step was figuring out how to achieve smooth movement. In my trials so far, the motor starts instantly and stops suddenly. However, in order to have the natural movement I want to have, I need acceleration and deccelaration.
By following the code in this video, I managed to get acceleration and deacceleration. After a bit of fine tuning, here is the result:
int DIR_PIN = D5;
int STEP_PIN = D4;
int ENABLE_PIN = D6;
int interval=3000;
#define STEPS 400void setup() {
Serial.begin(19200);
pinMode(STEP_PIN,OUTPUT);
pinMode(DIR_PIN,OUTPUT);
pinMode(ENABLE_PIN,OUTPUT);
digitalWrite(ENABLE_PIN, LOW);
}
void loop(){
digitalWrite(DIR_PIN, LOW);
constantAccel();
digitalWrite(DIR_PIN, HIGH);
constantAccel();
while (true);
}
void constantAccel(){
int delays[STEPS];
float angle = 1;
float accel = 0.01;
float c0 = 2000*sqrt(2* angle / accel ) *0.67703;
float lastDelay = 0;
int highSpeed = 100;
for (int i=0; i< STEPS; i++){
float d = c0;
if (i>0)
d = lastDelay - (2* lastDelay)/(4*i+1);
if (d<highSpeed)
d = highSpeed;
delays[i] = d;
lastDelay = d;
}
for (int i= 0; i<STEPS; i++){
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds (delays[i]);
digitalWrite(STEP_PIN, LOW);
}
for (int i= 0; i<STEPS; i++){
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds (delays[STEPS-i-1]);
digitalWrite(STEP_PIN, LOW);
}
}
Accelerating & Deaccelerating
Wireless Speed & Angle Control Through Interface
The next step was to integrate this smooth movement to my network interface. Since I want to have precise control over the angle and speed of movements, I modified the interface accordingly.
By asking ChatGPT, I learned how to create sliders and get values from them in jQuery. I created two sliders: one for angle of movement and one for speed control. When I click the “turn” buttons, the values from the sliders gets sent to the XIAO.
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebSrv.h>
//#define STEPS 400
// Some variables we will need along the way
const char *ssid = "salvatore ferragamo";
const char *password = "macncheese";
const char *PARAM_MESSAGE = "message";
int webServerPort = 80;
int DIR_PIN = D5;
int STEP_PIN = D4;
int ENABLE_PIN = D6;
int interval = 3000;
int stepCount = 10;
// Setting up our webserver
AsyncWebServer server(webServerPort);
// This function will be called when human will try to access undefined endpoint
void notFound(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", "Not found");
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void sendResponse(AsyncWebServerRequest *request, String message) {
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", message);
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void setup() {
pinMode(STEP_PIN, OUTPUT);
pinMode(DIR_PIN, OUTPUT);
pinMode(ENABLE_PIN, OUTPUT);
digitalWrite(ENABLE_PIN, LOW);
Serial.begin(19200);
delay(10);
// We start by connecting to a WiFi network
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
// We want to know the IP address so we can send commands from our computer to the device
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
// Greet human when it tries to access the root / endpoint.
// This is a good place to send some documentation about other calls available if you wish.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
sendResponse(request, "Hello!");
});
server.on("/motor", HTTP_GET, [](AsyncWebServerRequest *request) {
int state; // motor state
int stepValue; // range slider value, which will be turned into stepCount
float accelValue; // range slider value, which will be turned into accel
if (request->hasParam("state")) {
// The incoming params are Strings
String param = request->getParam("state")->value();
// .. so we have to interpret or cast them
if (param =="CWturn") {
state = 2;
} elseif (param =="CCWturn") {
state = 3;
} else {
state = 0;
}
} else {
state = 0;
}
if (request->hasParam("stepValue")){
stepValue = request->getParam("stepValue")->value().toInt();
}
if (request->hasParam("accelValue")){
accelValue = request->getParam("accelValue")->value().toFloat();
}
// Send back message to human
String stateString; // Declare the variable outside the if statement
if (state ==2) {
Serial.println("turningcw");
digitalWrite(DIR_PIN, LOW);
stepCount = stepValue;
constantAccel(accelValue);
stateString = "isTurningCW";
} elseif (state ==3) {
Serial.println("turningCCW");
digitalWrite(DIR_PIN, HIGH);
stepCount = stepValue;
constantAccel(accelValue);
stateString = "isTurningCCW";
} else {
stateString = "notTurning";
}
String responseJSON = "{\"motorState\":\""+ stateString +"\"}";
sendResponse(request, responseJSON);
});
server.on("/params", HTTP_GET, [](AsyncWebServerRequest *request) {
int param1 = random(100);
int param2 = random(100);
int param3 = random(100);
int param4 = random(100);
String responseJSON = "{";
responseJSON +="\"param1\":"+String(param1) +",";
responseJSON +="\"param2\":"+String(param2) +",";
responseJSON +="\"param3\":"+String(param3) +",";
responseJSON +="\"param4\":"+String(param4) +",";
responseJSON +="}";
sendResponse(request, responseJSON);
});
// If human tries endpoint no exist, exec this function
server.onNotFound(notFound);
Serial.print("Starting web server on port ");
Serial.println(webServerPort);
server.begin();
}
void constantAccel(float accelVal){
int delays[stepCount];
float angle = 1;
float accel = accelVal;
float c0 = 2000*sqrt(2* angle / accel ) *0.67703;
float lastDelay = 0;
int highSpeed = 100;
for (int i=0; i< stepCount; i++){
float d = c0;
if (i>0){
d = lastDelay - (2* lastDelay)/(4*i+1);
}
if (d<highSpeed){
d = highSpeed;
}
delays[i] = d;
lastDelay = d;
}
for (int i= 0; i<stepCount; i++){
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds (delays[i]);
digitalWrite(STEP_PIN, LOW);
}
for (int i= 0; i<stepCount; i++){
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds (delays[stepCount-i-1]);
digitalWrite(STEP_PIN, LOW);
}
}
void loop() {
}
After achieving a satisfactory amount of control over a single motor, I moved on to controlling multiple motors. I my final design, I aim to use threee motors; two for the neck and one for the hip.
Breadboard Testing
I tested the circuit on the breadbord. Each motor has it’s own TMC driver, which are connected to the same power & logic source.
At first, I couldn’t get the motors to turn. They were turning when two motors were connected, but when I connect the third one they all stopped. After some debugging we found out that the issue was with the power source. The 12V adapter I was using was supplying 0.4A, which was not enough to power three motors.
By hooking the circuit to a power source, I found out that it was drawing 1.12A when all three were turning at the same time. We ordered new power sources, and the problem was solved.
Three motors at the same time
Circuit Board Design
After making sure the breadboard prototype worked, I started circuit board design.
I milled the board and set up the circuit.
I powered the circuit by a 12V power source that gives max. 1.3A. The motors can all be turned at the same time,
int STEP_PIN_A = D5;
int DIR_PIN_A = D4;
int ENABLE_PIN = D10;
int STEP_PIN_B = D7;
int DIR_PIN_B = D6;
int STEP_PIN_C = D9;
int DIR_PIN_C = D8;
# define STEPS 400void setup() {
Serial.begin(19200);
pinMode(STEP_PIN_A,OUTPUT);
pinMode(DIR_PIN_A,OUTPUT);
pinMode(STEP_PIN_B,OUTPUT);
pinMode(DIR_PIN_B,OUTPUT);
pinMode(STEP_PIN_C,OUTPUT);
pinMode(DIR_PIN_C,OUTPUT);
pinMode(ENABLE_PIN,OUTPUT);
digitalWrite(ENABLE_PIN, LOW);
}
void loop() {
digitalWrite(DIR_PIN_C, HIGH);
digitalWrite(DIR_PIN_A, LOW);
digitalWrite(DIR_PIN_B, LOW);
constantAccel(STEP_PIN_A);
constantAccel(STEP_PIN_B);
constantAccel(STEP_PIN_C);
}
void constantAccel(int chosenPin){
int delays[STEPS];
float angle = 1;
float accel = 0.01;
float c0 = 2000*sqrt(2* angle / accel ) *0.67703;
float lastDelay = 0;
int highSpeed = 100;
for (int i=0; i< STEPS; i++){
float d = c0;
if (i>0)
d = lastDelay - (2* lastDelay)/(4*i+1);
if (d<highSpeed)
d = highSpeed;
delays[i] = d;
lastDelay = d;
}
for (int i= 0; i<STEPS; i++){
digitalWrite(chosenPin, HIGH);
delayMicroseconds (delays[i]);
digitalWrite(chosenPin, LOW);
}
for (int i= 0; i<STEPS; i++){
digitalWrite(chosenPin, HIGH);
delayMicroseconds (delays[STEPS-i-1]);
digitalWrite(chosenPin, LOW);
}
}
Three motors with circuit board - individual control
Second Iteration
Laser Cut Plywood Body
It was time to move past the cardboard prototyping. By using the info I gathered in the lo-fi prototype stage, I modeled a new body and head for the toucan that would be laser cut out of plywood. This time, I used screw-reinforced joints to attach the universal joint. I had learned this method in laser cutting week in the very beginning.
Universal Joint: Further Iterations
The first universal joint I modeled had a few problems:
The long screws in the middle always got caught in something, and prevented it to turn as intented
It was too long, it made the neck of the bird unnaturally long
It was too wide
To solve these, I wanted to make it more compact. I found a smaller bearing that I could use, and designed the joint around it.
However, this iteration could not turn at all. The cylinder was too tight and the screw I was using could not turn freely. In addition, the smaller screw holes in the middle part were too weak. I experimented with different lenghts, widths and bearing placements. I designed and printed a few more iterations, but all ended with dissappointment.
At the end, I decided to go back to the original bearing I was using, and make the joint shorter. I realized that the width was not the problem at all, the length was. I also used smaller screws for the middle part, so that part would be tougher.
Here is a comparison between the first iteration (right) and the last (left).
Here is the finished universal joint, and how it functions together with the laser-cut plywood body.
Universal joint working
Assembly
I assembled it all together with the motors as well. The new laser cut body integrated well with the measurements I laid out in my cardboard prototype. The head movements were acceptable for now.
I also modeled and laser cut some legs for it to stand on. Later on, I will attach the hip motor to them to allow hip movement. I photographed the toucan in it’s natural habitat. Special thanks to Saskia for the genius idea.
Network Interface
After I got the main assembly complete for my second iteration, I started working on the network interface. My aim was to be able to control the speed, steps and turning direction of three stepper motors with a wireless network interface.
You can see the full process of trial-and-error documented here in the Code Dump. Here, I will try to summarize my progress.
As I built on the interfaces from the previous weeks, I updated the type of information that gets transferred between the interface and XIAO. I made the interface send seperate strings of data for “direction”, “speed” and “steps” for each motor. I started with one motor, then increased it to two. I was originally going to add a third motor as well, but did not have time. However, it can be added easily by replicating the code if one wishes to do so.
I made a graphical user interface for the information to be clearer. In the interface below, the user can select the angle and direction for each motor, while the speed remains the same for both.
For the Arduino side, I experimented with both accelerating and non-accelerating setups for the steppers. They too, can be found in the Code Dump. Here are two examples: one of them uses keyboard controls to control a single motor, and the second one utilizes the GUI to it’s full extent by controlling two motors with acceleration. Although the second one has it’s issues, it is the one used in the examples below until the final code comes into play.
CODE: Precise Stepper Motor Control / WiFi/ Keyboard Control [WORKING v1]
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebSrv.h>
// Some variables we will need along the way
const char *ssid = "Fablab";
const char *password = "Fabricationlab1";
const char *PARAM_MESSAGE = "message";
int webServerPort = 80;
int STEP_PIN_A = D5;
int DIR_PIN_A = D4;
int ENABLE_PIN = D10;
int STEP_PIN_B = D7;
int DIR_PIN_B = D6;
int STEP_PIN_C = D9;
int DIR_PIN_C = D8;
int stepCount = 10;
// Setting up our webserver
AsyncWebServer server(webServerPort);
// This function will be called when human will try to access undefined endpoint
void notFound(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", "Not found");
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void sendResponse(AsyncWebServerRequest *request, String message) {
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", message);
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void setup() {
Serial.begin(19200);
pinMode(STEP_PIN_A,OUTPUT);
pinMode(DIR_PIN_A,OUTPUT);
pinMode(STEP_PIN_B,OUTPUT);
pinMode(DIR_PIN_B,OUTPUT);
pinMode(STEP_PIN_C,OUTPUT);
pinMode(DIR_PIN_C,OUTPUT);
pinMode(ENABLE_PIN,OUTPUT);
digitalWrite(ENABLE_PIN, LOW);
delay(10);
// We start by connecting to a WiFi network
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
// We want to know the IP address so we can send commands from our computer to the device
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
// Greet human when it tries to access the root / endpoint.
// This is a good place to send some documentation about other calls available if you wish.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
sendResponse(request, "Hello!");
});
server.on("/motor", HTTP_GET, [](AsyncWebServerRequest *request) {
int state; // motor state
int stepValue; // range slider value, which will be turned into stepCount
float accelValue; // range slider value, which will be turned into accel
if (request->hasParam("state")) {
// The incoming params are Strings
String param = request->getParam("state")->value();
// .. so we have to interpret or cast them
if (param =="CWturn") {
state = 2;
} elseif (param =="CCWturn") {
state = 3;
} else {
state = 0;
}
} else {
state = 0;
}
if (request->hasParam("stepValue")){
stepValue = request->getParam("stepValue")->value().toInt();
}
if (request->hasParam("accelValue")){
accelValue = request->getParam("accelValue")->value().toFloat();
}
// Send back message to human
String stateString; // Declare the variable outside the if statement
if (state ==2) {
Serial.println("turningcw");
digitalWrite(DIR_PIN_A, LOW);
stepCount = stepValue;
constantAccel(accelValue);
stateString = "isTurningCW";
} elseif (state ==3) {
Serial.println("turningCCW");
digitalWrite(DIR_PIN_A, HIGH);
stepCount = stepValue;
constantAccel(accelValue);
stateString = "isTurningCCW";
} else {
stateString = "notTurning";
}
String responseJSON = "{\"motorState\":\""+ stateString +"\"}";
sendResponse(request, responseJSON);
});
server.on("/params", HTTP_GET, [](AsyncWebServerRequest *request) {
int param1 = random(100);
int param2 = random(100);
int param3 = random(100);
int param4 = random(100);
String responseJSON = "{";
responseJSON +="\"param1\":"+String(param1) +",";
responseJSON +="\"param2\":"+String(param2) +",";
responseJSON +="\"param3\":"+String(param3) +",";
responseJSON +="\"param4\":"+String(param4) +",";
responseJSON +="}";
sendResponse(request, responseJSON);
});
// If human tries endpoint no exist, exec this function
server.onNotFound(notFound);
Serial.print("Starting web server on port ");
Serial.println(webServerPort);
server.begin();
}
void constantAccel(float accelVal){
int delays[stepCount];
float angle = 1;
float accel = accelVal;
float c0 = 2000*sqrt(2* angle / accel ) *0.67703;
float lastDelay = 0;
int highSpeed = 100;
for (int i=0; i< stepCount; i++){
float d = c0;
if (i>0){
d = lastDelay - (2* lastDelay)/(4*i+1);
}
if (d<highSpeed){
d = highSpeed;
}
delays[i] = d;
lastDelay = d;
}
for (int i= 0; i<stepCount; i++){
digitalWrite(STEP_PIN_A, HIGH);
delayMicroseconds (delays[i]);
digitalWrite(STEP_PIN_A, LOW);
}
for (int i= 0; i<stepCount; i++){
digitalWrite(STEP_PIN_A, HIGH);
delayMicroseconds (delays[stepCount-i-1]);
digitalWrite(STEP_PIN_A, LOW);
}
}
void loop() {
}
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebSrv.h>
// Some variables we will need along the way
const char *ssid = "Fablab";
const char *password = "Fabricationlab1";
const char *PARAM_MESSAGE = "message";
int webServerPort = 80;
int STEP_PIN_A = D5;
int DIR_PIN_A = D4;
int ENABLE_PIN = D10;
int STEP_PIN_B = D7;
int DIR_PIN_B = D6;
int STEP_PIN_C = D9;
int DIR_PIN_C = D8;
int stepCount = 10;
// Setting up our webserver
AsyncWebServer server(webServerPort);
// This function will be called when human will try to access undefined endpoint
void notFound(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", "Not found");
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void sendResponse(AsyncWebServerRequest *request, String message) {
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", message);
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void setup() {
Serial.begin(19200);
pinMode(STEP_PIN_A,OUTPUT);
pinMode(DIR_PIN_A,OUTPUT);
pinMode(STEP_PIN_B,OUTPUT);
pinMode(DIR_PIN_B,OUTPUT);
pinMode(STEP_PIN_C,OUTPUT);
pinMode(DIR_PIN_C,OUTPUT);
pinMode(ENABLE_PIN,OUTPUT);
digitalWrite(ENABLE_PIN, LOW);
delay(10);
// We start by connecting to a WiFi network
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
// We want to know the IP address so we can send commands from our computer to the device
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
// Greet human when it tries to access the root / endpoint.
// This is a good place to send some documentation about other calls available if you wish.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
sendResponse(request, "Hello!");
});
server.on("/motor", HTTP_GET, [](AsyncWebServerRequest *request) {
int stepValueA;
int stepValueB;
float accelValue;
if (request->hasParam("sentStepValueA")){
stepValueA = request->getParam("sentStepValueA")->value().toInt();
}
if (request->hasParam("sentStepValueB")){
stepValueB = request->getParam("sentStepValueB")->value().toInt();
}
if (request->hasParam("sentAccelValue")){
accelValue = request->getParam("sentAccelValue")->value().toFloat();
}
if (request->hasParam("sentDirA")) {
// The incoming params are Strings
String param = request->getParam("sentDirA")->value();
// .. so we have to interpret or cast them
if (param =="counter-clockwise") {
digitalWrite(DIR_PIN_A, LOW);
Serial.println("dir a ccw");
} elseif (param =="clockwise") {
digitalWrite(DIR_PIN_A, HIGH);
Serial.println("dir a cw");
} else {
stepValueA = 0;
}
}
if (request->hasParam("sentDirB")) {
// The incoming params are Strings
String param = request->getParam("sentDirB")->value();
// .. so we have to interpret or cast them
if (param =="counter-clockwise") {
digitalWrite(DIR_PIN_B, LOW);
Serial.println("dir b ccw");
} elseif (param =="clockwise") {
digitalWrite(DIR_PIN_B, HIGH);
Serial.println("dir b cw");
} else {
stepValueB=0;
}
}
// Send back message to human
String stateString; // Declare the variable outside the if statement
stateString = "done";
Serial.println(stepValueA);
Serial.println(stepValueB);
Serial.println(accelValue);
motorMove(stepValueA, stepValueB, accelValue);
String responseJSON = "{\"motorState\":\""+ stateString +"\"}";
sendResponse(request, responseJSON);
});
server.on("/params", HTTP_GET, [](AsyncWebServerRequest *request) {
int param1 = random(100);
int param2 = random(100);
int param3 = random(100);
int param4 = random(100);
String responseJSON = "{";
responseJSON +="\"param1\":"+String(param1) +",";
responseJSON +="\"param2\":"+String(param2) +",";
responseJSON +="\"param3\":"+String(param3) +",";
responseJSON +="\"param4\":"+String(param4) +",";
responseJSON +="}";
sendResponse(request, responseJSON);
});
// If human tries endpoint no exist, exec this function
server.onNotFound(notFound);
Serial.print("Starting web server on port ");
Serial.println(webServerPort);
server.begin();
}
void motorMove(int stepValueA, int stepValueB, float accelValue){
int delaysA[stepValueA];
float angleA = 1;
float accelA = accelValue;
float c0A = 2000*sqrt(2* angleA / accelA ) *0.67703;
float lastDelayA = 0;
int highSpeedA = 100;
for (int i=0; i< stepValueA; i++){
float d = c0A;
if (i>0){
d = lastDelayA - (2* lastDelayA)/(4*i+1);
}
if (d<highSpeedA){
d = highSpeedA;
}
delaysA[i] = d;
lastDelayA = d;
}
int delaysB[stepValueB];
float angleB = 1;
float accelB = accelValue;
float c0B = 2000*sqrt(2* angleB / accelB ) *0.67703;
float lastDelayB = 0;
int highSpeedB = 100;
for (int i=0; i< stepValueB; i++){
float d = c0B;
if (i>0){
d = lastDelayB - (2* lastDelayB)/(4*i+1);
}
if (d<highSpeedB){
d = highSpeedB;
}
delaysB[i] = d;
lastDelayB = d;
}
int i = 0;
int j = 0;
while (i < stepValueA || j < stepValueB) {
if (i < stepValueA) {
digitalWrite(STEP_PIN_A, HIGH);
delayMicroseconds(delaysA[i]);
digitalWrite(STEP_PIN_A, LOW);
i++;
}
if (j < stepValueB) {
digitalWrite(STEP_PIN_B, HIGH);
delayMicroseconds(delaysB[j]);
digitalWrite(STEP_PIN_B, LOW);
j++;
}
}
i = 0;
j = 0;
while (i < stepValueA || j < stepValueB) {
if (i < stepValueA) {
digitalWrite(STEP_PIN_A, HIGH);
delayMicroseconds(delaysA[stepValueA-i-1]);
digitalWrite(STEP_PIN_A, LOW);
i++;
}
if (j < stepValueB) {
digitalWrite(STEP_PIN_B, HIGH);
delayMicroseconds(delaysB[stepValueB-j-1]);
digitalWrite(STEP_PIN_B, LOW);
j++;
}
}
}
void loop() {
}
After creating the network interface code for the steppers, I started to try it out on moving the toucan’s head. As I started, I saw the first problem: the plywood beak was too heavy for the motor to move. It often fell down to one side, because it was so front-heavy.
Here is a video of me barely moving the beak with the steppers:
Toucan beaks are very heavy
The solution I found at this stage was to cut off the beak. I found it to be much easier to turn the head after that. It could do both the slower and rapid movements clearly.
Toucan beaks are very heavy
But another issue arose after this. Because of the way the wires were angled and put through the holes in the head, they were getting stuck all the time. You can see below how I constantly have to adjust the wire-head connections to get them unstuck. I experimented for a while with different connection styles and hole diameters for the wire-head connections.
Toucan beaks are very heavy
New Head Shape
The solution I came up with to this problem was to modify the head shape to make the top connection hole horizontal. In the previous iterations, the side-to-side movements were achieved by a wire connected to a horizontally drilled hole; while the top-down movement wire was attached to a vertical hole. This was making the wire get stuck due to the turning angles.
Here is the new and improved head shape:
With this new shape, I was able to achieve much smoother movements without the wires getting stuck in the holes. With that out of the way, I moved on to my third iteration.
Third Iteration
Laser Cut Acrylic Body
I decided that the final form of the toucan would be made out of a semi-transparent 3mm acrylic body. That would give it an overall cleaner look. I modified my Fusion file for the necessary adjustments regarding the nut & screw connections, decided on the tolerances & kerf, and started cutting.
After I cut it, I moved on to assembling it with my usual “screw-reinforced joint” method, trying to get a tight fit with the nuts. However, I quickly learned that acryclic is much more brittle than plywood when you force something on it.
I made structure stronger, loosened the tolerances, and re-cut. I used super-glue in the parts that turned out too loose. It is better to glue it than risk breakage.
Component Box
The design also needed a place to put my electronic components in. I laser cut a box with dividers and outlets, that would also act as a stand for the toucan.
In the box, I put my circuit board, 12V adapter and the cabling in-between. The toucan stands on the box with a nuts-and-bolt connection.
Vacuum Forming the Head
Previously I mentioned that the laser-cut beak was too heavy to turn. However, a toucan is not a toucan without a beak.
So the solution I found to make a three dimensional beak while keeping it light, was vacuum forming. I had used the wildcard week to make a proof-of-concept for this design and it worked. So now, I scaled up the design to use in the final product.
I overlayed the design on top of my laser cut files in Fusion, to make sure that it would fit over the laser-cut assembly. This allows me to spot errors before I make a mistake that costs me milling & vacuum forming time (which is a lot of time).
Again, I used Fusion’s CAM interface to plan out the milling paths. In addition to the workflow in the wildcard week, I also put a “Face Milling” step first to make sure that the larger surface I was working with was level.
I milled the vacuum forming mold out of sikablock. It turned out very smooth and just as I wanted.
Milling sikablock
I vacuum formed the head of the same 0.5mm acryclic sheet, and placed it on the top of the head. Here is a few photos of how the toucan looked at this stage, when assembled fully.
Head Movement Testing
As I went on with these stages, I was always testing head movements on the side. After the vacuum forming & assembly, I continued my tests.
The issue now -which had shown itself when I moved on to the acrylic body & new head shape- was the steppers not generating enough torque. In the first video down below, you can see how the head snaps back after it reaches a certain angle and the stepper tries to turn it further. This was an issue with the stepper’s capabilities, since I was able to turn the head at the same angle when I manually turn the stepper with my hands. Rather than the wire connections or angles being a problem, this time the stepper was giving me trouble.
stepper snapping back after reaching a certain angle
I can turn the head to that angle with manual hand turn
I tried altering the code, disconnecting the third stepper entirely and some more additional fixes, but none of them worked to the extent I wanted. Tinkering with the code was my best option in this stage. The previous code used an acceleration setup for the steppers, meaning the turning motion started slow, accelerated, and slowly stopped. I wanted it to be this way to avoid very snappy unnatural movements. However, this code was starting to act strange when two steppers were involved.
Previously, I had tried to make it work by introducing a while loop that ran independently for two steppers within itself. However, this caused the steppers to act strange when the two of them had different step counts, and disrupted the whole motion. The code snippet is down below.
CODE: Motor Move Function with Independently Accelerating Motors [PARTIALLY WORKING]
void motorMove(int stepValueA, int stepValueB, float accelValue){
int delaysA[stepValueA];
float angleA = 1;
float accelA = accelValue;
float c0A = 2000*sqrt(2* angleA / accelA ) *0.67703;
float lastDelayA = 0;
int highSpeedA = 100;
for (int i=0; i< stepValueA; i++){
float d = c0A;
if (i>0){
d = lastDelayA - (2* lastDelayA)/(4*i+1);
}
if (d<highSpeedA){
d = highSpeedA;
}
delaysA[i] = d;
lastDelayA = d;
}
int delaysB[stepValueB];
float angleB = 1;
float accelB = accelValue;
float c0B = 2000*sqrt(2* angleB / accelB ) *0.67703;
float lastDelayB = 0;
int highSpeedB = 100;
for (int i=0; i< stepValueB; i++){
float d = c0B;
if (i>0){
d = lastDelayB - (2* lastDelayB)/(4*i+1);
}
if (d<highSpeedB){
d = highSpeedB;
}
delaysB[i] = d;
lastDelayB = d;
}
int i = 0;
int j = 0;
while (i < stepValueA || j < stepValueB) {
if (i < stepValueA) {
digitalWrite(STEP_PIN_A, HIGH);
delayMicroseconds(delaysA[i]);
digitalWrite(STEP_PIN_A, LOW);
i++;
}
if (j < stepValueB) {
digitalWrite(STEP_PIN_B, HIGH);
delayMicroseconds(delaysB[j]);
digitalWrite(STEP_PIN_B, LOW);
j++;
}
}
i = 0;
j = 0;
while (i < stepValueA || j < stepValueB) {
if (i < stepValueA) {
digitalWrite(STEP_PIN_A, HIGH);
delayMicroseconds(delaysA[stepValueA-i-1]);
digitalWrite(STEP_PIN_A, LOW);
i++;
}
if (j < stepValueB) {
digitalWrite(STEP_PIN_B, HIGH);
delayMicroseconds(delaysB[stepValueB-j-1]);
digitalWrite(STEP_PIN_B, LOW);
j++;
}
}
}
After being unable to solve this code related problem, I turned to simplifying the program. I got rid of acceleration, and simultaneous movements. I programmed the motors to move sequentially, and without any acceleration. This certainly made things easier when trying to figure out the head movements. Here is the final code, including the jQuery and Arduino sides, that I used in the final video:
CODE: Sequential Stepper Motor Control / WiFi/ Interface With Two Motors [WORKING - VIDEO 07.06.23]
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebSrv.h>
// Some variables we will need along the way
const char *ssid = "Fablab";
const char *password = "Fabricationlab1";
const char *PARAM_MESSAGE = "message";
int webServerPort = 80;
int STEP_PIN_A = D5;
int DIR_PIN_A = D4;
int ENABLE_PIN = D10;
int STEP_PIN_B = D7;
int DIR_PIN_B = D6;
//int STEP_PIN_C = D9;
//int DIR_PIN_C = D8;
int stepCount = 10;
// Setting up our webserver
AsyncWebServer server(webServerPort);
// This function will be called when human will try to access undefined endpoint
void notFound(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", "Not found");
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void sendResponse(AsyncWebServerRequest *request, String message) {
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", message);
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void setup() {
Serial.begin(19200);
pinMode(STEP_PIN_A,OUTPUT);
pinMode(DIR_PIN_A,OUTPUT);
pinMode(STEP_PIN_B,OUTPUT);
pinMode(DIR_PIN_B,OUTPUT);
// pinMode(STEP_PIN_C,OUTPUT);
// pinMode(DIR_PIN_C,OUTPUT);
pinMode(ENABLE_PIN,OUTPUT);
digitalWrite(ENABLE_PIN, LOW);
delay(10);
// We start by connecting to a WiFi network
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
// We want to know the IP address so we can send commands from our computer to the device
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
// Greet human when it tries to access the root / endpoint.
// This is a good place to send some documentation about other calls available if you wish.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
sendResponse(request, "Hello!");
});
server.on("/motor", HTTP_GET, [](AsyncWebServerRequest *request) {
int stepValueA;
int stepValueB;
int accelValue;
if (request->hasParam("sentStepValueA")){
stepValueA = request->getParam("sentStepValueA")->value().toInt();
}
if (request->hasParam("sentStepValueB")){
stepValueB = request->getParam("sentStepValueB")->value().toInt();
}
if (request->hasParam("sentAccelValue")){
accelValue = request->getParam("sentAccelValue")->value().toInt();
}
if (request->hasParam("sentDirA")) {
// The incoming params are Strings
String param = request->getParam("sentDirA")->value();
// .. so we have to interpret or cast them
if (param =="counter-clockwise") {
digitalWrite(DIR_PIN_A, HIGH);
Serial.println("dir a ccw");
} elseif (param =="clockwise") {
digitalWrite(DIR_PIN_A, LOW);
Serial.println("dir a cw");
} else {
stepValueA = 0;
}
}
if (request->hasParam("sentDirB")) {
// The incoming params are Strings
String param = request->getParam("sentDirB")->value();
// .. so we have to interpret or cast them
if (param =="counter-clockwise") {
digitalWrite(DIR_PIN_B, HIGH);
Serial.println("dir b ccw");
} elseif (param =="clockwise") {
digitalWrite(DIR_PIN_B, LOW);
Serial.println("dir b cw");
} else {
stepValueB=0;
}
}
// Send back message to human
String stateString; // Declare the variable outside the if statement
stateString = "done";
Serial.println(stepValueA);
Serial.println(stepValueB);
Serial.println(accelValue);
motorMove(stepValueA, stepValueB, accelValue);
String responseJSON = "{\"motorState\":\""+ stateString +"\"}";
sendResponse(request, responseJSON);
});
server.on("/params", HTTP_GET, [](AsyncWebServerRequest *request) {
int param1 = random(100);
int param2 = random(100);
int param3 = random(100);
int param4 = random(100);
String responseJSON = "{";
responseJSON +="\"param1\":"+String(param1) +",";
responseJSON +="\"param2\":"+String(param2) +",";
responseJSON +="\"param3\":"+String(param3) +",";
responseJSON +="\"param4\":"+String(param4) +",";
responseJSON +="}";
sendResponse(request, responseJSON);
});
// If human tries endpoint no exist, exec this function
server.onNotFound(notFound);
Serial.print("Starting web server on port ");
Serial.println(webServerPort);
server.begin();
}
void motorMove(int stepValueA, int stepValueB, float accelValue) {
int i = 0;
int j = 0;
while (i < stepValueA) {
digitalWrite(STEP_PIN_A, HIGH);
delayMicroseconds(accelValue);
digitalWrite(STEP_PIN_A, LOW);
i++;
}
while (j < stepValueB) {
digitalWrite(STEP_PIN_B, HIGH);
delayMicroseconds(accelValue);
digitalWrite(STEP_PIN_B, LOW);
j++;
}
}
void loop() {
}
I also secured the electrical connections before giving the project it’s final shape. In the previous iteration of the componet box, I did not use any security measures against the cables being pulled off or snapping. I introduced a power cable plug to the component box that allowed the 12V adapter to be plugged and unplugged at will. I also connected a button to the line (L) cable between the 12V adapter and the power cable plug, to turn on the system more easily. I then soldered and isolated the connections.
Final Project from Fab Academy Academic Overlay 2023
Below is my final project from the Fab Academy Academic Overlay 2023 I completed as part of Aalto University New Media MA program.
For the Fab Academy 2024 global class, I planned some improvements on my final project:
Mill the electronics box out of wood.
Design inner part of electronics box with better integration. 3D printed
Change stepper motors.
(?) Re-make the electronics board to include screw terminals, and possibly powering up the Xiao through voltage divider.
29 April Week
Model the inner electronics box
6 May Week
Mill electronics box out of wood.
3D print the inner part of box.
Assemble electronics box.
13 May Week
Re-model the body to integrate new steppers.
Laser cut the new body
20 May Week
(?) Re-make the electronics board
Assemble everything
27 May Week
Final video
Re-Designing the Electronics Enclosure
I started the improvements by re-thinking the integration of the electronics. In the previous version, the electronics box was just a collection of devices and cables haphazardly placed inside a container. I wanted to improve it.
I started by tearing down the old box. In the process, I damaged the PCB. I have to do it again, oh well.
I designed the electronics box to fit everything snugly this time. I measured the power supply and PCB and the plugs, and designed the inside of the box to be 3D printed.
The outside of the box, I’m going to mill it out of sikablock. I chose this material because it is easier to mill than wood, and gives a great surface finish. This outer shell will cover the 3D printed inner enclosure on all sides, except the front where the power cable and buttons will be.