Sean McCormick
Published © GPL3+

Brick Droid

Using a Particle Photon, a few servos, and some toy building bricks....you can build your own autonomous droid with the kids!

BeginnerFull instructions provided1,725
Brick Droid

Things used in this project

Hardware components

Photon
Particle Photon
×1
Full Rotation Servo
×2
Piezo Speaker
×1
Servos (Tower Pro MG996R)
×1
Breadboard (generic)
Breadboard (generic)
×2
USB Battery Pack
×1
Jumper wires (generic)
Jumper wires (generic)
×1
2 inch wheels
×1
General Purpose Transistor NPN
General Purpose Transistor NPN
×1
Resistor 221 ohm
Resistor 221 ohm
×1
Ball Caster Metal - 3/8"
×4
SparkFun microB USB Breakout
SparkFun microB USB Breakout
×1
USB-A to Micro-USB Cable
USB-A to Micro-USB Cable
×2

Hand tools and fabrication machines

Hot glue gun (generic)
Hot glue gun (generic)

Story

Read more

Schematics

Brick Droid Wiring Diagram

This diagram shows how to connect all wires to drive motors, sensors and speakers for the Brick Droid to be run by the Particle Photon.

Code

BrickDroid.INO

C/C++
This is the code that runs on the Particle Photon that drives the brick droid and its three modes (Manual, Self Drive w Tracking and Cantina). In addition to driving the Droid, it also sets up the Rest APIs to control the Droid via REST calls from mobile devices using the HTML UI or on any communication device that can make REST based calls (ex. pebble watch)
//BRICK DROID AI

//configurable values...change these based on your specifics (ie) if 
// you want to use other pins or if your droid needs longer
// time to turn.
int leftLegPin = D0;
int rightLegPin = D1;
int headPin = D2;
int pingPin = D3;
int beepPin = D4;
int pingRetries = 3;
int distanceThreasholdInCm = 30;
int delayToTurnNinty=750;
int delayForward=100;

//public varibles for the servos
Servo leftLeg;
Servo rightLeg;
Servo head;
//servo movement values for the legs and head
int FORWARD=0;
int BACKWARD=180;
int STOP=90;
int RIGHT=0;
int LEFT=180;
int CENTER=90;
int currentAction=1;  //set initial startup value to be manual/web controlled

//beep/sound definitions...delays in milliseconds of piezo on/off
//              on   off  on  off  on  off  on
int sound1[] = {200, 50, 100, 100, 50, 250, 400};
int sound2[] = {100, 200, 300, 50, 300, 200, 100};
int sound3[] = {100, 200, 100, 50, 300, 400, 100};
int sound4[] = {50, 300, 400, 200, 100, 100, 50};
int sound5[] = {300, 100, 100, 100, 100, 200, 100};
int sound6[] = {200, 50, 100, 400, 100, 100, 200};
int numSounds=6;
int* sounds[7];

//tracking definitions.   Data to keep track where the droid has gone in Self
//      drive mode to report to the REST service / HTML UI.
const int TrackingSize=100;
char tracing[TrackingSize];
int tcount;
int forwardTraceTick=10;
int forwardTraceCounter=0;

//init startup method...runs only once
void setup()
{
  resetTracing();
  //assign sounds to array to pick from
  sounds[0]=sound1;
  sounds[1]=sound2;
  sounds[2]=sound3;
  sounds[3]=sound4;
  sounds[4]=sound5;
  sounds[5]=sound6;

  //setup REST service for mode control
  Spark.function("droidmode",droidMode);

  //setup REST service for manual mode
  Spark.function("droidcontrol",manualControl);

  //set REST service for tracing in self drive mode
  Spark.variable("droidtrack", tracing, STRING);

  //attach pins to servos
  rightLeg.attach(rightLegPin);
  leftLeg.attach(leftLegPin);
  head.attach(headPin);

  //setup piezo buzzer
  pinMode(beepPin, OUTPUT);

  Serial.begin(9600);
  delay(1000);

  //announce that your droid is ready to operate
  beep();
}

//ongoing "loop" method...runs continuously after startup.
void loop()
{
  if(currentAction==1)
  {
    //manual/web controlled mode...do nothing in loop and
    //     wait for rest methods to be called
  }

  if(currentAction==2)
  {
    //self driving robot, that avoids obstancles
    selfDrive();
  }

  if(currentAction==3)
  {
    //cantina mode...dancing around in a small area.
    cantinaMode();
  }
}


// REST service for configuring the "mode" of the droid
int droidMode(String setmode) {
  resetTracing();
  if(setmode=="manual")
  {
    leftLeg.write(STOP);
    rightLeg.write(STOP);
    beep();
    currentAction=1;
    return 1;
  }
  if(setmode=="selfdrive")
  {
    beep();
    currentAction=2;
    return 1;
  }
  if(setmode=="cantina")
  {
    beep();
    currentAction=3;
    return 1;
  }
  return 0;
}

//vv MANUAL DRIVE MODE
//  REST service for controlling the droid in manual mode.
int manualControl(String command) {
  Serial.print("manualControl");
  Serial.print(command);
  Serial.println();
    if (command=="left") {
        leftLeg.write(FORWARD);
        rightLeg.write(FORWARD);
        delay(delayToTurnNinty);
        leftLeg.write(STOP);
        rightLeg.write(STOP);
        return 1;
    }
    if (command=="right") {
        leftLeg.write(BACKWARD);
        rightLeg.write(BACKWARD);
        delay(delayToTurnNinty);
        leftLeg.write(STOP);
        rightLeg.write(STOP);
        return 1;
    }
    if (command=="forward") {
        leftLeg.write(BACKWARD);
        rightLeg.write(FORWARD);
        return 1;
    }
    if (command=="backward") {
        leftLeg.write(FORWARD);
        rightLeg.write(BACKWARD);
        return 1;
    }
    if (command=="stop") {
        leftLeg.write(STOP);
        rightLeg.write(STOP);
        return 1;
    }
    if (command=="headleft") {
        head.write(LEFT);
        return 1;
    }
    if (command=="headright") {
        head.write(RIGHT);
        return 1;
    }
    if (command=="headcenter") {
        head.write(CENTER);
        return 1;
    }
    return 0;
}
//^^ MANUAL DRIVE MODE


//vv SELF DRIVE MODE
void selfDrive()
{
  //self controlled mode, obstacle avoiding movement
  long distance = pingDistance();
  Serial.print("ForwardDistance");
  Serial.print(distance);
  Serial.println();
  if(distance > distanceThreasholdInCm)
  {
    //we have space in front of us, go foward
    if(forwardTraceCounter==forwardTraceTick)
    {
      pushTrace('F');
      forwardTraceCounter=0;
    }
    else
    {
      forwardTraceCounter++;
    }
    leftLeg.write(BACKWARD);
    rightLeg.write(FORWARD);
    delay(delayForward);
  }
  else
  {
    //no space in front, find a way left or right to turn
    leftLeg.write(STOP);
    rightLeg.write(STOP);
    beep();
    forwardTraceCounter=0;
    head.write(RIGHT);
    delay(500);
    //look right
    long openDistanceRight = pingDistance();
    delay(100);
    head.write(LEFT);
    delay(1000);
    //look left
    long openDistanceLeft = pingDistance();
    delay(100);
    head.write(CENTER);
    delay(500);
    Serial.print("openDistanceLeft");
    Serial.print(openDistanceLeft);
    Serial.println();
    Serial.print("openDistanceRight");
    Serial.print(openDistanceRight);
    Serial.println();

    if(openDistanceLeft > openDistanceRight && openDistanceLeft > distanceThreasholdInCm)
    {
      //turn left
      pushTrace('L');
      Serial.println("turn left");
      leftLeg.write(FORWARD);
      rightLeg.write(FORWARD);
      delay(delayToTurnNinty);
    }
    else
    {
      if(openDistanceRight > openDistanceLeft && openDistanceRight > distanceThreasholdInCm)
      {
        //turn right
        pushTrace('R');
        Serial.println("turn right");
        leftLeg.write(BACKWARD);
        rightLeg.write(BACKWARD);
        delay(delayToTurnNinty);
      }
      else
      {
        //neither have good line of sight, turn around
        pushTrace('B');
        Serial.println("turn around");
        leftLeg.write(FORWARD);
        rightLeg.write(FORWARD);
        delay(delayToTurnNinty * 2);
      }
    }
  }
}

long pingDistance()
{
  //grabbed from arduino.cc/en/Tutorial/Ping

    // establish variables for duration of the ping,
    // and the distance result in inches and centimeters:
    long duration, cm;

    //I added a loop thru a set of retries
    //....sometimes my ping ))) comes back with false 0s.
    for(int loop=0;loop<pingRetries;loop++)
    {
      Serial.println("ping loop1");
      // The PING))) is triggered by a HIGH pulse of 2 or more microseconds.
      // Give a short LOW pulse beforehand to ensure a clean HIGH pulse:
      pinMode(pingPin, OUTPUT);
      delayMicroseconds(2);
      digitalWrite(pingPin, LOW);
      delayMicroseconds(2);
      digitalWrite(pingPin, HIGH);
      delayMicroseconds(5);
      digitalWrite(pingPin, LOW);

      // The same pin is used to read the signal from the PING))): a HIGH
      // pulse whose duration is the time (in microseconds) from the sending
      // of the ping to the reception of its echo off of an object.
      pinMode(pingPin, INPUT);
      duration = pulseIn(pingPin, HIGH);

      // convert the time into a distance
      cm = microsecondsToCentimeters(duration);
      //if non-zero, break out of retry loop
      if(cm>0)
      {
        break;
      }
    }
    return cm;
}

long microsecondsToCentimeters(long microseconds) {
  // The speed of sound is 340 m/s or 29 microseconds per centimeter.
  // The ping travels out and back, so to find the distance of the
  // object we take half of the distance travelled.
  return microseconds / 29 / 2;
}
//^^ SELF DRIVE

//vv CANTINA MODE
void cantinaMode()
{
  //randomly pick an action and do it
  int action = random(0,9);
  if(action==0)
  {
    //drive forward
    leftLeg.write(FORWARD);
    rightLeg.write(FORWARD);
    delay(1000);
  }
  if(action==1)
  {
    //turn right
    leftLeg.write(BACKWARD);
    rightLeg.write(FORWARD);
    delay(delayToTurnNinty);
  }
  if(action==2)
  {
    //turn left
    leftLeg.write(FORWARD);
    rightLeg.write(BACKWARD);
    delay(delayToTurnNinty);
  }
  if(action==3)
  {
    //drive backwards
    leftLeg.write(BACKWARD);
    rightLeg.write(BACKWARD);
    delay(1000);
  }
  if(action==4)
  {
    //drive backwards
    leftLeg.write(FORWARD);
    rightLeg.write(BACKWARD);
    delay(delayToTurnNinty*2);
  }
  if(action==5)
  {
    //turn head to left
    head.write(LEFT);
  }
  if(action==6)
  {
    //turn head center
    head.write(CENTER);
  }
  if(action==7)
  {
    //turn head right
    head.write(RIGHT);
  }
  if(action==8)
  {
    //make sound
    beep();
  }
  leftLeg.write(STOP);
  rightLeg.write(STOP);
}

//^^ CANTINA MODE

//vv Misc methods
void beep(){
  int mySoundPick = random(0,numSounds);
  int *mySound = sounds[mySoundPick];
  int current=LOW;
  for (int i = 0; i < 7; i++) {
    if(current==LOW)
    {
      current=HIGH;
    }
    else
    {
      current=LOW;
    }
    digitalWrite(beepPin, current);
    delay(mySound[i]);
  }
  digitalWrite(beepPin, LOW);
}

void resetTracing(){
  forwardTraceCounter=0;

  //zero out tracking array
  for(int tloop = 0; tloop < TrackingSize; tloop++)
  {
    tracing[tloop]=NULL;
  }
  tcount=0;
}

void pushTrace(char direction)
{
  //push the latest value onto the tracing array

  //have not taken more then 100 moves, pop on end
  if(tcount<TrackingSize-1)
  {
    Serial.print("corky here");
    tracing[tcount]=direction;
    tcount=tcount+1;
  }
  else
  {
    //we are up to 100 moves, shift everything forward
    //   then pop on end
    for(int tloop = 0; tloop < TrackingSize-2; tloop++)
    {
      tracing[tloop] = tracing[tloop+1];
    }
    tracing[TrackingSize-1]=direction;
  }
}

//^^ Misc Methods

BrickDroid.HTML

HTML
This is an HTML page with buttons that make calls (via jQuery) to the Particle API rest services setup by the Brick Droid AI code on startup. This HTML page acts as the client application to the Photons REST services. These services control the droid "mode" as well as the operation when in manual mode, in addition to "drawing" the path of the droid when in self-driving mode.
<html>
    <head>
        <script type="text/javascript" src="http://code.jquery.com/jquery-1.11.3.min.js"></script>

        <script>
          //update these values with the ID of your particle device and the token generated to call rest services
          //  see particle.io documentation on setting up your particle photon and generating a token.
            var particleID = "YOUR PARTICLE PHOTON DEVICE ID HERE";
            var particleToken = "YOUR PARTICLE.IO TOKEN HERE";
            var timer;

            function setDroidMode(newMode) {
                if (timer!=undefined)
                  window.clearInterval(timer);  //make sure to clear the timer so tracking stops when the mode changes

                //call rest service to change the droids mode
                $.post("https://api.particle.io/v1/devices/" + particleID + "/droidmode/", { params: newMode, access_token: particleToken },
                      function(data){
                      });
            }

            function sendManualCommand(newCommand) {
                //call rest service to manually control droid with a command
                $.post("https://api.particle.io/v1/devices/" + particleID + "/droidcontrol/", { params: newCommand, access_token: particleToken },
                      function(data){
                      });
            }

            function startTracing() {
              //setup timer to fire every so often, to call rest service to get the movement of the droid,
              //   and draw those movements on a HTML5 canvas
              timer = window.setInterval(function () {
                      $.get("https://api.particle.io/v1/devices/" + particleID + "/droidtrack?access_token=" + particleToken,
                      function(data){
                        //setup canvas to draw
                        var c = document.getElementById("myCanvas");
                        var ctx = c.getContext("2d");
                        //clear canvas
                        ctx.clearRect(0, 0, c.width, c.height);
                        var startposx=c.width/2;   //keep track of start position X
                        var startposy=c.height/2;  //keep track of start position Y
                        var curposx=c.width/2;   //keep track of ongoing current position X
                        var curposy=c.height/2;  //keep track of ongoing current position Y
                        curdir=1;  //keep track of ongoing direction  1=N  2=S  3=E  4=W
                        var s = data.result;  //set s to be the result of the rest call
                        var moveDist=10  //number of distance to move on each move
                        ctx.moveTo(curposx,curposy);
                        for (var i = 0; i < s.length; i++) {
                          if(s.charAt(i)=="F")
                          {
                            //draw forward line
                            if(curdir==1)//N
                              curposy = curposy-moveDist;
                            if(curdir==2)//S
                              curposy=curposy+moveDist;
                            if(curdir==3)//E
                              curposx=curposx+moveDist;
                            if(curdir==4)//W
                              curposx=curposx-moveDist;

                            ctx.lineTo(curposx, curposy);
                            //no need to change direction...
                          }

                          //turning 180 degrees around
                          if(s.charAt(i)=="B")
                          {
                            //update lines with U turn and draw them
                            if(curdir==1)//N
                            {
                              curposx = curposx+moveDist/2;
                              ctx.lineTo(curposx, curposy);
                              curposy = curposy+moveDist;
                              ctx.lineTo(curposx, curposy);
                            }
                            if(curdir==2)//S
                            {
                              curposx = curposx+moveDist/2;
                              ctx.lineTo(curposx, curposy);
                              curposy = curposy-moveDist;
                              ctx.lineTo(curposx, curposy);
                            }
                            if(curdir==3)//E
                            {
                              curposy = curposy+moveDist/2;
                              ctx.lineTo(curposx, curposy);
                              curposx = curposx-moveDist;
                              ctx.lineTo(curposx, curposy);
                            }
                            if(curdir==4)//W
                            {
                              curposy = curposy+moveDist/2;
                              ctx.lineTo(curposx, curposy);
                              curposx = curposx+moveDist;
                              ctx.lineTo(curposx, curposy);
                            }
                            //update current direction
                            if(curdir==1)//N
                              curdir=2;
                            else if(curdir==2)//S
                              curdir=1;
                            else if(curdir==3)//E
                              curdir=4;
                            else if(curdir==4)//W
                              curdir=3;
                          }

                          //turning right
                          if(s.charAt(i)=="R")
                          {
                            //draw new line based on right turn
                            if(curdir==1)//N
                              curposx = curposx+moveDist;
                            if(curdir==2)//S
                              curposx=curposx-moveDist;
                            if(curdir==3)//E
                              curposy=curposy+moveDist;
                            if(curdir==4)//W
                              curposy=curposy-moveDist;
                            //draw new line
                            ctx.lineTo(curposx, curposy);
                            //update to new direction
                            if(curdir==1)//N
                                curdir=3;
                            else if(curdir==2)//S
                              curdir=4;
                            else if(curdir==3)//E
                              curdir=2;
                            else if(curdir==4)//W
                              curdir=1;
                          }

                          //turning left
                          if(s.charAt(i)=="L")
                          {
                            //update line based on left turn
                            if(curdir==1)//N
                              curposx = curposx-moveDist;
                            if(curdir==2)//S
                              curposx=curposx+moveDist;
                            if(curdir==3)//E
                              curposy=curposy-moveDist;
                            if(curdir==4)//W
                              curposy=curposy+moveDist;

                            //draw new line
                            ctx.lineTo(curposx, curposy);

                            //update direction
                            if(curdir==1)//N
                              curdir=4;
                            else if(curdir==2)//S
                              curdir=3;
                            else if(curdir==3)//E
                              curdir=1;
                            else if(curdir==4)//W
                              curdir=2;
                          }
                        }
                        ctx.stroke();  //draw the lines layed out in loop

                        //draw end circle empty
                        ctx.beginPath();
                        ctx.arc(curposx, curposy, 5, 0, Math.PI*2, true);
                        ctx.closePath();
                        ctx.stroke();
                        //draw start circle  filled
                        ctx.beginPath();
                        ctx.arc(startposx, startposy, 5, 0, Math.PI*2, true);
                        ctx.closePath();
                        ctx.fill();
                      });
                    }, 7000);  //update javascript UI with tracing every XXXX milliseconds
            }

            function stopTracing() {
              window.clearInterval(timer);
            }
        </script>

        <style>
        body, table, td {
            font-family: "Gotham SSm A","Gotham SSm B",proxima-nova,"Helvetica Neue",helvetica,arial,sans-serif;
        }
        </style>
    </head>
    <body>
        <H1>Robot Control</H1>
        <TABLE>
            <TR>
              <TD VALIGN=TOP ALIGN=LEFT>
                Droid Mode:
              </TD>
              <TD ALIGN=LEFT>
                <button style="height:50px;width:100px" onclick="javascript:setDroidMode('manual');">Manual</button>
              </TD>
              <TD>
                <button style="height:50px;width:100px" onclick="javascript:setDroidMode('selfdrive');">Self Driving</button>
              </TD>
              <TD ALIGN=RIGHT>
                <button style="height:50px;width:100px" onclick="javascript:setDroidMode('cantina');">Cantina</button>
              </TD>
          </TR>
          <TR>
            <TD>
              <BR><BR><BR>
            </TD>
          </TR>
          <TR>
            <TD VALIGN=TOP ALIGN=LEFT>
              Manual Mode Controls:
            </TD>
            <TD ALIGN=LEFT COLSPAN=3>
              <TABLE>
                  <TR>
                    <TD ALIGN=LEFT>
                      <button style="height:50px;width:100px" onclick="javascript:sendManualCommand('headleft');">Head Left</button>
                    </TD>
                    <TD>
                      <button style="height:50px;width:100px" onclick="javascript:sendManualCommand('headcenter');">Head Center</button>
                    </TD>
                    <TD ALIGN=RIGHT>
                      <button style="height:50px;width:100px" onclick="javascript:sendManualCommand('headright');">Head Right</button>
                    </TD>
                  </TR>
                  <TR>
                    <TD>
                        <BR><BR>
                    </TD>
                  </TR>
                  <TR>
                    <TD ALIGN=CENTER COLSPAN=3>
                      <button style="height:50px;width:100px" onclick="javascript:sendManualCommand('forward');">Forward</button>
                    </TD>
                  </TR>
                  <TR>
                    <TD ALIGN=LEFT>
                      <button style="height:50px;width:100px" onclick="javascript:sendManualCommand('left');">Left</button>
                    </TD>
                    <TD>
                      <button style="height:50px;width:100px" onclick="javascript:sendManualCommand('stop');">Stop</button>
                    </TD>
                    <TD ALIGN=RIGHT>
                      <button style="height:50px;width:100px" onclick="javascript:sendManualCommand('right');">Right</button>
                    </TD>
                  </TR>
                  <TR>
                    <TD ALIGN=CENTER COLSPAN=3>
                      <button style="height:50px;width:100px" onclick="javascript:sendManualCommand('backward');">Backward</button>
                    </TD>
                  </TR>
              </TABLE>
            </TD>
          </TR>
          <TR>
            <TD COLSPAN=4>
              <BR><BR>
            </TD>
        </TR>
          <TR>
            <TD VALIGN=TOP ALIGN=LEFT>
                Self Driving Tracking:
            </TD>
            <TD>
              <button style="height:50px;width:100px" onclick="javascript:startTracing();">Turn On</button>
            </TD>
            <TD>
              <button style="height:50px;width:100px" onclick="javascript:stopTracing();">Turn Off</button>
            </TD>
            <TD>
            </TD>
        </TR>
        </TABLE>
        <CANVAS id="myCanvas" width="300" height="300">
        </CANVAS>
    </body>
</html>

Credits

Sean McCormick

Sean McCormick

5 projects • 27 followers
20+ year software engineer. #Learn, #Make, #Help

Comments