Final Project

Source Files

Final Project Source Files

Initial Ideas for Final Project

Animatronics

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.

Animatronic raven DIY kit

How to achieve smoother movements with servo motors

Uncanny eye animatronic

Uncanny eye animatronic 2

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.

toucan sketch

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.

lo-fi motors

motors motors

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.

universal joint model

neck joint neck joint

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:

CODE: Smooth Movement
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
int DIR_PIN = D5;
int STEP_PIN = D4;
int ENABLE_PIN = D6;
int interval=3000;

#define STEPS 400

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

CODE: Arduino IDE

Arduino IDE

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#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;
      } else if (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";
    } else if (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() {

}
CODE: jQuery

jQuery

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>BootStrap Rest DEMO</title>
  <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css">
</head>

<body>

  <header class="container mt-3">
    <div id="status-bar" class="alert alert-light" role="alert">
      API Status: <span id="status-api">Undefined</span>
    </div>
  </header>

  <main class="container">
    <h1>stepper control test</h1>
    <div class="form-group">
      <label for="rangeInput">Angle Input:</label>
      <input type="range" class="form-range" id="rangeInput" min="0" max="192" value="50"> 
    </div>
    <p>Selected value: <span id="rangeValue"> 50 </span></p>

    <div class="form-group">
      <label for="speedInput">Speed Input:</label>
      <input type="range" class="form-range" id="speedInput" min="0.0001" max="0.03"  step="0.0001" value="0.01"> 
    </div>
    <p>Selected value: <span id="speedValue"> 0.01 </span></p>
    
    <div id="motor-sign">Motor Rotation</div>
    <button id="button-counter-clockwise" class="btn btn-warning">Turn Counter-clockwise</button>
    <button id="button-clockwise" class="btn btn-info">Turn Clockwise</button>
  </main>

  <script src="lib/bootstrap/js/bootstrap.bundle.min.js"></script>
  <script src="lib/jquery/jquery-3.6.4.min.js"></script>
  <script>
    function checkAPIStatus() {
      $.ajax({
        url: "http://192.168.223.93/",
        timeout: 5000
      })
        .done(function () {
          $("#status-api").text("Connected");
          if ($("#status-bar").hasClass("alert-light")) {
            $("#status-bar").removeClass("alert-light");
          }
          $("#status-bar").addClass("alert-success");
        })
        .fail(function () {
          $("#status-api").text("Not connected");
        })
    }

    function triggerTurn(direction) {
      if (direction == 2) {
        $("#motor-sign").data("motorState", "CWturn");
      } else if (direction == 3) {
        $("#motor-sign").data("motorState", "CCWturn");
      } else {
        $("#motor-sign").data("motorState", "noAction");
      }

      const buttonState = $("#motor-sign").data("motorState");
      const rangeValue = $('#rangeValue').text(); // Get the value of #rangeValue span
      const speedValue = $('#speedValue').text();

      console.log("sent: " + buttonState);
      console.log("sent: " + rangeValue);
      console.log("sent: " + speedValue);

      $.ajax({
        url: "http://192.168.223.93/motor",
        data: {
          state: buttonState,
          stepValue: rangeValue, // Include the range value in the AJAX request
          accelValue: speedValue
        },
        timeout: 5000
      })
        .done(function (response) {
          const responseJSON = JSON.parse(response); //we convert this into an object so that:
          console.log("received: " + responseJSON.motorState);
          if (responseJSON.motorState == "isTurningCW") {
            $("#motor-sign").css("background", "cyan");
          } else if (responseJSON.motorState == "isTurningCCW") {
            $("#motor-sign").css("background", "orange");
          } else {
            $("#motor-sign").css("background", "gray");
          }
        })
        .fail(function () {
          console.log("motor trigger call failed.");
        })
    }

    $(document).ready(function () {
      console.log("Document has loaded!");

      setInterval(checkAPIStatus, 2000);

      $("#button-clockwise").click(function () {
        triggerTurn(2);
      });

      $("#button-counter-clockwise").click(function () {
        triggerTurn(3);
      });

      $('#rangeInput').on('input', function() {
    var value = $(this).val();
    $('#rangeValue').text(value);
      });

      $('#speedInput').on('input', function() {
    var value = $(this).val();
    $('#speedValue').text(value);
      });

    });
  </script>
</body>

</html>

Slider controls the angle of movement (in steps)

Sliders control angle & speed of movement

Multiple Motor Control

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.

rat nest three motors

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.

circuit schematic

I milled the board and set up the circuit.

three motor circuit three motor 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,

Three motors with circuit board

Or they can be controlled individually like this,

CODE: Three Motor Smooth Control
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

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 400

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);
}

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.

toucan model laser cut toucan

Universal Joint: Further Iterations

The first universal joint I modeled had a few problems:

uj v1

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.

uj v2

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.

uj v3

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

comparison

Here is the finished universal joint, and how it functions together with the laser-cut plywood body.

uj v4 uj v4

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.

assembling

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.

second iteration second iteration

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.

I built this interface on my previous codes from the networking and interface and application programming weeks.

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.

GUI

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]

Arduino IDE

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#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;
      } else if (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";
    } else if (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() {

}

jQuery

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>BootStrap Rest DEMO</title>
  <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css">
</head>

<body>

  <header class="container mt-3">
    <div id="status-bar" class="alert alert-light" role="alert">
      API Status: <span id="status-api">Undefined</span>
    </div>
  </header>

  <main class="container">
    <h1>stepper control test</h1>
    <div class="form-group">
      <label for="rangeInput">Angle Input:</label>
      <input type="range" class="form-range" id="rangeInput" min="0" max="192" value="50"> 
    </div>
    <p>Selected value: <span id="rangeValue"> 50 </span></p>

    <div class="form-group">
      <label for="speedInput">Speed Input:</label>
      <input type="range" class="form-range" id="speedInput" min="0.0001" max="0.03"  step="0.0001" value="0.01"> 
    </div>
    <p>Selected value: <span id="speedValue"> 0.01 </span></p>
    
    <div id="motor-sign">Motor Rotation</div>
    <button id="button-counter-clockwise" class="btn btn-warning">Turn Counter-clockwise</button>
    <button id="button-clockwise" class="btn btn-info">Turn Clockwise</button>
  </main>

  <script src="lib/bootstrap/js/bootstrap.bundle.min.js"></script>
  <script src="lib/jquery/jquery-3.6.4.min.js"></script>
  <script>
    function checkAPIStatus() {
      $.ajax({
        url: "http://193.167.5.175/",
        timeout: 5000
      })
        .done(function () {
          $("#status-api").text("Connected");
          if ($("#status-bar").hasClass("alert-light")) {
            $("#status-bar").removeClass("alert-light");
          }
          $("#status-bar").addClass("alert-success");
        })
        .fail(function () {
          $("#status-api").text("Not connected");
        })
    }

    function triggerTurn(direction) {
      if (direction == 2) {
        $("#motor-sign").data("motorState", "CWturn");
      } else if (direction == 3) {
        $("#motor-sign").data("motorState", "CCWturn");
      } else {
        $("#motor-sign").data("motorState", "noAction");
      }

      const buttonState = $("#motor-sign").data("motorState");
      const rangeValue = $('#rangeValue').text(); // Get the value of #rangeValue span
      const speedValue = $('#speedValue').text();

      console.log("sent: " + buttonState);
      console.log("sent: " + rangeValue);
      console.log("sent: " + speedValue);

      $.ajax({
        url: "http://193.167.5.175/motor",
        data: {
          state: buttonState,
          stepValue: rangeValue, // Include the range value in the AJAX request
          accelValue: speedValue
        },
        timeout: 5000
      })
        .done(function (response) {
          const responseJSON = JSON.parse(response); //we convert this into an object so that:
          console.log("received: " + responseJSON.motorState);
          if (responseJSON.motorState == "isTurningCW") {
            $("#motor-sign").css("background", "cyan");
          } else if (responseJSON.motorState == "isTurningCCW") {
            $("#motor-sign").css("background", "orange");
          } else {
            $("#motor-sign").css("background", "gray");
          }
        })
        .fail(function () {
          console.log("motor trigger call failed.");
        })
    }

    $(document).ready(function () {
      console.log("Document has loaded!");

        // Keyboard event listener
  $(document).on('keypress', function (event) {
    var keyPressed = event.key;
    if (keyPressed === 'd' || keyPressed === 'D') {
      triggerTurn(2); // Simulate Turn Clockwise button click
    } else if (keyPressed === 'a' || keyPressed === 'A') {
      triggerTurn(3); // Simulate Turn Counter-clockwise button click
    }
      });

      setInterval(checkAPIStatus, 2000);

      $("#button-clockwise").click(function () {
        triggerTurn(2);
      });

      $("#button-counter-clockwise").click(function () {
        triggerTurn(3);
      });

      $('#rangeInput').on('input', function() {
    var value = $(this).val();
    $('#rangeValue').text(value);
      });

      $('#speedInput').on('input', function() {
    var value = $(this).val();
    $('#speedValue').text(value);
      });

    });
  </script>
</body>

</html>
CODE: Precise Stepper Motor Control / WiFi/ Interface With Two Motors [PARTIALLY WORKING v3]

Precise Stepper Motor Control / WiFi/ Interface With Two Motors [PARTIALLY WORKING v3]

Arduino IDE

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#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");
      } else if (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");
      } else if (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() {

}

jQuery

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Toucan Motor Control Interface</title>
  <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css">
</head>

<body>

  <header class="container mt-3">
    <div id="status-bar" class="alert alert-light" role="alert">
      API Status: <span id="status-api">Undefined</span>
    </div>
  </header>

  <main class="container">
    <h1>Toucan Motor Control Interface</h1>

    <div class="form-group">
      <label for="speedInput">Speed Input:</label>
      <input type="range" class="form-range" id="speedInput" min="0.0001" max="0.03" step="0.0001" value="0.01">
    </div>
    <p>Selected value: <span id="speedValue"> 0.01 </span></p>

    <h2>Motor A</h2>
    <div class="form-group">
      <label for="stepInputB">Step Input A:</label>
      <input type="range" class="form-range" id="stepInputA" min="0" max="192" value="50">
    </div>
    <p>Selected value: <span id="step-value-A"> 50 </span></p>
    
    <button id="button-motor-A-left" type="button" class="btn btn-outline-danger">counter-clockwise</button>
    <button id="button-motor-A-none" type="button" class="btn btn-outline-danger">none</button>
    <button id="button-motor-A-right" type="button" class="btn btn-outline-danger">clockwise</button>
    <p>Motor A Direction: <span id="direction-motor-A"> none </span></p>

    <h2>Motor B</h2>
    <div class="form-group">
      <label for="stepInputB">Step Input B:</label>
      <input type="range" class="form-range" id="stepInputB" min="0" max="192" value="50">
    </div>
    <p>Selected value: <span id="step-value-B"> 50 </span></p>

    <button id="button-motor-B-left" type="button" class="btn btn-outline-danger">counter-clockwise</button>
    <button id="button-motor-B-none" type="button" class="btn btn-outline-danger">none</button>
    <button id="button-motor-B-right" type="button" class="btn btn-outline-danger">clockwise</button>
    <p>Motor B Direction: <span id="direction-motor-B"> none </span></p>

    <br>
    <button id="button-turn" type="button" class="btn btn-primary btn-lg">TURN!</button>


  </main>

  <script src="lib/bootstrap/js/bootstrap.bundle.min.js"></script>
  <script src="lib/jquery/jquery-3.6.4.min.js"></script>
  <script>
    function checkAPIStatus() {
      $.ajax({
        url: "http://193.167.5.175/",
        timeout: 5000
      })
        .done(function () {
          $("#status-api").text("Connected");
          if ($("#status-bar").hasClass("alert-light")) {
            $("#status-bar").removeClass("alert-light");
          }
          $("#status-bar").addClass("alert-success");
        })
        .fail(function () {
          $("#status-api").text("Not connected");
        })
    }

    function triggerTurn() {
      const directionMotorA = $("#direction-motor-A").text();
      const directionMotorB = $("#direction-motor-B").text();
      const stepValueA = $('#step-value-A').text();
      const stepValueB = $('#step-value-B').text();
      const speedValue = $('#speedValue').text();

      console.log("DirA: " + directionMotorA);
      console.log("DirB: " + directionMotorB);
      console.log("StepA: " + stepValueA);
      console.log("StepB: " + stepValueB);
      console.log("Speed: " + speedValue);

      $.ajax({
        url: "http://193.167.5.175/motor",
        data: {
          sentDirA: directionMotorA,
          sentDirB: directionMotorB,
          sentStepValueA: stepValueA,
          sentStepValueB: stepValueB,
          sentAccelValue: speedValue
        },
        timeout: 5000
      })
        .done(function (response) {
          const responseJSON = JSON.parse(response); //we convert this into an object so that:
          console.log("received: " + responseJSON.motorState);
        })
        .fail(function () {
          console.log("motor trigger call failed.");
        })
    }

    $(document).ready(function () {
      console.log("Document has loaded!");

      // Keyboard event listener
      $(document).on('keypress', function (event) {
        var keyPressed = event.key;
        if (keyPressed === 'd' || keyPressed === 'D') {
          triggerTurn(2); // Simulate Turn Clockwise button click
        } else if (keyPressed === 'a' || keyPressed === 'A') {
          triggerTurn(3); // Simulate Turn Counter-clockwise button click
        }
      });

      setInterval(checkAPIStatus, 2000);

      // Button click event
  // Motor A buttons
  $("#button-motor-A-left, #button-motor-A-none, #button-motor-A-right").click(function () {
    var direction = $(this).text();
    $("#direction-motor-A").text(direction);
  });

  // Motor B buttons
  $("#button-motor-B-left, #button-motor-B-none, #button-motor-B-right").click(function () {
    var direction = $(this).text();
    $("#direction-motor-B").text(direction);
  });

      $("#button-turn").click(function () {
        triggerTurn();
      });

      $('#stepInputA').on('input', function () {
        var value = $(this).val();
        $('#step-value-A').text(value);
      });

      $('#stepInputB').on('input', function () {
        var value = $(this).val();
        $('#step-value-B').text(value);
      });

      $('#speedInput').on('input', function () {
        var value = $(this).val();
        $('#speedValue').text(value);
      });

    });
  </script>
</body>

</html>

Head Movement Testing

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

wire experiments wire experiments

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:

new head shape new head shape

new head shape new 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.

new body

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.

new body

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.

third iteration body third iteration body

third iteration body third iteration body

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.

component box component box

component box component box

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

overlaying overlaying

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.

Fusion CAM

I milled the vacuum forming mold out of sikablock. It turned out very smooth and just as I wanted.

Milling sikablock

vacuum form mold

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.

final form of the toucan final form of the toucan

final form of the toucan final form of the toucan

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]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

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]

Arduino IDE

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#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");
      } else if (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");
      } else if (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() {

}

jQuery

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Toucan Motor Control Interface</title>
  <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css">
</head>

<body>

  <header class="container mt-3">
    <div id="status-bar" class="alert alert-light" role="alert">
      API Status: <span id="status-api">Undefined</span>
    </div>
  </header>

  <main class="container">
    <h1>Toucan Motor Control Interface</h1>

    <div class="form-group">
      <label for="speedInput">Speed Input:</label>
      <input type="range" class="form-range" id="speedInput" min="1000" max="30000" value="5000">
    </div>
    <p>Selected value: <span id="speedValue"> 5000 </span></p>

    <h2>Motor A</h2>
    <div class="form-group">
      <label for="stepInputB">Step Input A:</label>
      <input type="range" class="form-range" id="stepInputA" min="0" max="192" value="50">
    </div>
    <p>Selected value: <span id="step-value-A"> 50 </span></p>
    
    <button id="button-motor-A-left" type="button" class="btn btn-outline-danger">counter-clockwise</button>
    <button id="button-motor-A-none" type="button" class="btn btn-outline-danger">none</button>
    <button id="button-motor-A-right" type="button" class="btn btn-outline-danger">clockwise</button>
    <p>Motor A Direction: <span id="direction-motor-A"> none </span></p>

    <h2>Motor B</h2>
    <div class="form-group">
      <label for="stepInputB">Step Input B:</label>
      <input type="range" class="form-range" id="stepInputB" min="0" max="192" value="50">
    </div>
    <p>Selected value: <span id="step-value-B"> 50 </span></p>

    <button id="button-motor-B-left" type="button" class="btn btn-outline-danger">counter-clockwise</button>
    <button id="button-motor-B-none" type="button" class="btn btn-outline-danger">none</button>
    <button id="button-motor-B-right" type="button" class="btn btn-outline-danger">clockwise</button>
    <p>Motor B Direction: <span id="direction-motor-B"> none </span></p>

    <br>
    <button id="button-turn" type="button" class="btn btn-primary btn-lg">TURN!</button>


  </main>

  <script src="lib/bootstrap/js/bootstrap.bundle.min.js"></script>
  <script src="lib/jquery/jquery-3.6.4.min.js"></script>
  <script>
    function checkAPIStatus() {
      $.ajax({
        url: "http://193.167.5.175/",
        timeout: 5000
      })
        .done(function () {
          $("#status-api").text("Connected");
          if ($("#status-bar").hasClass("alert-light")) {
            $("#status-bar").removeClass("alert-light");
          }
          $("#status-bar").addClass("alert-success");
        })
        .fail(function () {
          $("#status-api").text("Not connected");
        })
    }

    function triggerTurn() {
      const directionMotorA = $("#direction-motor-A").text();
      const directionMotorB = $("#direction-motor-B").text();
      const stepValueA = $('#step-value-A').text();
      const stepValueB = $('#step-value-B').text();
      const speedValue = $('#speedValue').text();

      console.log("DirA: " + directionMotorA);
      console.log("DirB: " + directionMotorB);
      console.log("StepA: " + stepValueA);
      console.log("StepB: " + stepValueB);
      console.log("Speed: " + speedValue);

      $.ajax({
        url: "http://193.167.5.175/motor",
        data: {
          sentDirA: directionMotorA,
          sentDirB: directionMotorB,
          sentStepValueA: stepValueA,
          sentStepValueB: stepValueB,
          sentAccelValue: speedValue
        },
        timeout: 5000
      })
        .done(function (response) {
          const responseJSON = JSON.parse(response); //we convert this into an object so that:
          console.log("received: " + responseJSON.motorState);
        })
        .fail(function () {
          console.log("motor trigger call failed.");
        })
    }

    $(document).ready(function () {
      console.log("Document has loaded!");

      // Keyboard event listener
      $(document).on('keypress', function (event) {
        var keyPressed = event.key;
        if (keyPressed === 'd' || keyPressed === 'D') {
          triggerTurn(2); // Simulate Turn Clockwise button click
        } else if (keyPressed === 'a' || keyPressed === 'A') {
          triggerTurn(3); // Simulate Turn Counter-clockwise button click
        }
      });

      setInterval(checkAPIStatus, 2000);

      // Button click event
  // Motor A buttons
  $("#button-motor-A-left, #button-motor-A-none, #button-motor-A-right").click(function () {
    var direction = $(this).text();
    $("#direction-motor-A").text(direction);
  });

  // Motor B buttons
  $("#button-motor-B-left, #button-motor-B-none, #button-motor-B-right").click(function () {
    var direction = $(this).text();
    $("#direction-motor-B").text(direction);
  });

      $("#button-turn").click(function () {
        triggerTurn();
      });

      $('#stepInputA').on('input', function () {
        var value = $(this).val();
        $('#step-value-A').text(value);
      });

      $('#stepInputB').on('input', function () {
        var value = $(this).val();
        $('#step-value-B').text(value);
      });

      $('#speedInput').on('input', function () {
        var value = $(this).val();
        $('#speedValue').text(value);
      });

    });
  </script>
</body>

</html>

Sequential movement

Securing Electrical Connections

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.

secure box secure box secure box

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.

final gallery final gallery

final gallery final gallery

final gallery final gallery

final gallery final gallery

final slide

Click here for my documentation website from Fab Academy Academic Overlay 2023

Fab Academy 2024

Improvements Planned

For the Fab Academy 2024 global class, I planned some improvements on my final project:

29 April Week

6 May Week

13 May Week

20 May Week

27 May Week

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.

old electronics box

I started by tearing down the old box. In the process, I damaged the PCB. I have to do it again, oh well.

old electronics box old electronics box

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.

3d printed box 3d printed box

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.

3d printed box