Introduction: Baldroid V3 Balancing Robot With Actobotics Parts and IOIO-OTG

Hi, after designing a couple prototypes with an Android phone + IOIO+OTG module I decided to build a finished product using parts and components from Actobotics / Servocity.com.. besides the quality of the Actobotics products, the wide range of parts and components that are compatible and fit together beautifully made it an easy choice, also.. for DIY robotics is also a great choice due to their affordable prices..

Let's start building an awesome balancing robot!!!.. :-)

Step 1: Body of Robot - 12 Inch Aluminum Channel

The body of the robot is a 12 inch Aluminum channel ( servocity.com part # 585454), make sure you draw your design including all internal parts on paper and measure all the internal components (like batteries, circuitry, cabling) in order to make sure that it fits internally, although in my design I went with the 12 inch Aluminum channel, perhaps 15 inches can provide more room for components and room for expansion in case you want to upgrade the battery or add more specialized circuitry..

Step 2: Protecting the Inner Components of the Robot

I wanted to protect the inner components and I also wanted for the robot to look very clean.. so I decided to cover the inner walls of the Aluminum channel with sheet metal, I used chimney vent tube sheet metal that I bought at Ace Hardware for real cheap, around $11.. this sheet metal is really easy to cut with shears and also easy to drill... and matches the color of the aluminum perfectly, here is a picture of the leftover piece so you can have an idea of what I am talking about:

Step 3: Covering the Inner Walls With the Sheet Metal

After cutting the sheet metal to size I used mostly Crazy Glue / Hot Glue to keep the sheet metal in place.. and on the holes that required drilling for the bolts I used a small piece of wood on the other side of the drill to keep the sheet metal pressed against the Aluminum channel while drilling the hole, that way it guarantees a clean hole without warping the sheet metal.

Step 4: Assembling the Inner Components

I used a IOIO-OTG board to interface with the Android phone.. the IOIO-OTG is pure genius!.. it allows you to connect your Android Phone to pretty much any hobby robotic components.. servo motors via PWM, serial ports, you can read or control the individual pins ( to control LEDs, switches... you name it) ... the IOIO Java libraries are very simple to use, and combined with the power of the Android SDK you can get certainly get very creative.

As you can see in the picture I also added a simple homemade power distribution circuit for the servos... I recommend that you use ceramic and electrolytic capacitors to keep the electrical spikes being generated by the servos under control...

To secure the IOIO-OTG board I used Attachment Blocks (servocity.com part # 585400) ... you cannot see them in the picture but you can tell where they are located by how the screws used to secure the board are located

Step 5: More Assembling of the Inner Components

The battery is a Thunder Power RC model tp5400-2spp25 ... the battery was actually a little bit too wide and I had to remove the outer protective plastic to make it fit.. I do not recommend you do that unless you are very familiar with these batteries... if you break the inner protective seal the battery will get damaged beyond repair... there are multiple battery options in the market... just make sure it fits inside the Aluminum channel before you buy it... go to a hobby store with the channel and look for options that fit the Aluminum channel.

In the picture you see the bottom servos.. those are continuous rotation servos, although I used high power Hitec digital servos from servocity.com part # 37955S .. these servos are certainly overkill for the application... you should use relatively fast and torquey servos with metal gears for both the arms and wheels like Hitec HS-625MG Part:32625S ... the bottom / wheel servos are mounted using Servocity Servoblocks Part # 637110.. these ServoBlocks are awesome! .. the servoblocks come with a shaft that connect to the servo and then go through a bearing mount.. that way weight of the robot is supported by the servoblock and not the servo.

To mount the top arm servos I use standard Hitec HS-625MG ... to mount them I used 1 inch long Aluminum standoffs Part # 534-3489 (you can see them in the picture, no servo installed yet)

Step 6: Final Assembly of Internal Components

You can see the inner parts and pretty much the entire robot as a complete assembly in this photo.. notice the arms.. those are servocity 3 inch aluminum Arms part #545392 .. the attachments at the tip of the arm are heavy cabinet doors I bought at the hardware store.. the added weight really improves the balancing mechanism.. and the robot looks cool.. :-)

To mount the arm servos I used the Standard Servo Plate A # 575112 combined with 1 inch long Aluminum standoffs Part # 534-3489 .. perfect fit for any Hitec or Futaba servo.

The wheels are clear 5 inch precision wheels... Part # 59574 .. there are other options... but I believe these wheels match the aluminum frame perfectly.

Step 7: Location of the Power Switch..

To cover the top and bottom of the robot I used Flat channel brackets. Part # 585468 .... I secured them to the frame using the Attachment blocks Product # 585400...

Now.. I took the attachment block to Ace Hardware and I found the perfect power switch for the application as you can see in the picture.

Step 8: Phone Mount

The phone mount is an Arkon brand.. the model is the type that can hold phones up to 6.5 inches.. I like it a lot... it is cheap on Ebay.. and really easy to disassemble / reassemble, to secure it to the frame of the robot I simply drilled holes to it and used nuts bolts to secure it... whatever mount you use... make sure that the nuts / bolts do no interfere with the sliding mechanism..

Step 9: Routing the Servo / Phone Cables

In continuing in maintaining a clean look for the robot I used 1/2 inch plastic computer grommets from Ace Hardware.. they are a perfect fit for the holes... they are difficult to drill since they stick to the drill bit.. if you have a vise much better.. just go very slow with the drilling speed.. another idea is to use a soldering tip or hot wire to melt and open the hole..

Step 10: Covering the Back of the Robot.

For covering the back of the robot I bought and additional 12 inch Aluminum channel Part #585454 ... then I cut one side with a dremel tool... it is a laborious process.. just be patient.. :-) I used Attachment blocks Part # 585400 and 6-32 bolts to attach the part to the back of the robot.. it looks really clean and it protects the inner components..

Step 11: Software and Code

Software and code:

The smartphone runs Android 4.4.2 ... the code is Java and I used Eclipse with the Android Software Development Kit for Android 4.4.2.. the next step contains the main code..

Although my design is perhaps more expensive compared to other balancing robots due to the fact that I use additional servos and parts to build the arms.. I believe the use of arms greatly improves the balancing of the robot and it also simplifies the coding... also.. the robot looks way cooler with the arms moving to regain balance when is pushed.. Here is a link to the video

https://www.youtube.com/watch?v=poZ71SjB56I

As you can see in the video, I actually push the robot pretty hard sometimes.. but the code reacts to combine the wheels and the arms to regain balance fairly quickly..

The video and pictures do not do justice.. This robot looks amazing in person. People actually think I bought it at a store!

The code also includes support to control the robot using the Moga Pro bluetooth Controller.. the code allows to use the Moga Pro controller to change the values of the Angle, and the P, I and D values of the PID algorithm that handles the calculations to balance the robot.. I am working on code that will allow also allow to use the controller to drive the robot around (pretty cool uh?.. :-) ..

Here is a video that shows initial testing of the code that uses the Moga Pro to control the PID values and the arms as well:

https://www.youtube.com/watch?v=fdisyA6ZMHw

Thanks... Jose Rojo

My contact information:

Email address: joebotics@gmail.com

Step 12: Main Code..

Here is the main code.. I can send a ZIP file with the entire Java project upon request.. please send request to joebotics@gmail.com

MAIN CODE:

package ioio.examples.simple;

// BALANCE ACT 4 + MOGA 2

import ioio.lib.api.PwmOutput;

import ioio.lib.api.exception.ConnectionLostException;

import ioio.lib.util.BaseIOIOLooper;

import ioio.lib.util.IOIOLooper;

import ioio.lib.util.android.IOIOActivity;

import android.os.Bundle;

import android.widget.TextView;

import android.hardware.Sensor;

import android.hardware.SensorEvent;

import android.hardware.SensorEventListener;

import android.hardware.SensorManager;

import android.widget.SeekBar;

import com.bda.controller.Controller;

import com.bda.controller.ControllerListener;

import com.bda.controller.KeyEvent;

import com.bda.controller.MotionEvent;

import com.bda.controller.StateEvent;

public class IOIOSimpleApp extends IOIOActivity implements SensorEventListener{

private SensorManager mSensorManager;

private Sensor mRotVectSensor;

private float[] orientationVals=new float[3];

private float[] mRotationMatrix=new float[16];

private TextView textView_Current_Angle;

private TextView textView_Tilt_adjuster;

private TextView textView_kP_adjuster;

private TextView textView_kI_adjuster;

private TextView textView_kD_adjuster;

private SeekBar seekBar_Tilt_adjuster;

private SeekBar seekBar_kP_adjuster;

private SeekBar seekBar_kI_adjuster;

private SeekBar seekBar_kD_adjuster;

Controller mController = null;

final ExampleControllerListener mListener = new ExampleControllerListener();

final ExamplePlayer mPlayer = new ExamplePlayer(0.0f, 1.0f, 0.0f);

@Override

public void onCreate(Bundle savedInstanceState) {

mController = Controller.getInstance(this);

mController.init();

mController.setListener(mListener, null);

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

mRotVectSensor=mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);

textView_Current_Angle = (TextView) findViewById(R.id.TextView_CurrentAngle_Value);

textView_Tilt_adjuster = (TextView) findViewById(R.id.TextView_Tilt_adjusterValue);

seekBar_Tilt_adjuster = (SeekBar) findViewById(R.id.SeekBar_Tilt_adjuster);

textView_kP_adjuster = (TextView) findViewById(R.id.TextView_kP_adjusterValue);

seekBar_kP_adjuster = (SeekBar) findViewById(R.id.SeekBar_kP_adjuster);

textView_kI_adjuster = (TextView) findViewById(R.id.TextView_kI_adjusterValue);

seekBar_kI_adjuster = (SeekBar) findViewById(R.id.SeekBar_kI_adjuster);

textView_kD_adjuster = (TextView) findViewById(R.id.TextView_kD_adjusterValue);

seekBar_kD_adjuster = (SeekBar) findViewById(R.id.SeekBar_kD_adjuster);

seekBar_Tilt_adjuster.setProgress(500);

seekBar_kP_adjuster.setProgress(0);

seekBar_kI_adjuster.setProgress(0);

seekBar_kD_adjuster.setProgress(0);

enableUi(false);

}

public class ExampleControllerListener implements ControllerListener {

@Override

public void onKeyEvent(KeyEvent event) {

switch (event.getKeyCode()) {

case KeyEvent.KEYCODE_BUTTON_X:

seekBar_Tilt_adjuster.setProgress(seekBar_Tilt_adjuster.getProgress() - 2);

break;

case KeyEvent.KEYCODE_BUTTON_B:

seekBar_Tilt_adjuster.setProgress(seekBar_Tilt_adjuster.getProgress() + 2);

break;

case KeyEvent.KEYCODE_BUTTON_Y:

seekBar_kP_adjuster.setProgress(seekBar_kP_adjuster.getProgress() + 2);

break;

case KeyEvent.KEYCODE_BUTTON_A:

seekBar_kP_adjuster.setProgress(seekBar_kP_adjuster.getProgress() - 2);

break;

case KeyEvent.KEYCODE_DPAD_UP:

seekBar_kI_adjuster.setProgress(seekBar_kI_adjuster.getProgress() + 2);

break;

case KeyEvent.KEYCODE_DPAD_DOWN:

seekBar_kI_adjuster.setProgress(seekBar_kI_adjuster.getProgress() - 2);

break;

case KeyEvent.KEYCODE_DPAD_RIGHT:

seekBar_kD_adjuster.setProgress(seekBar_kD_adjuster.getProgress() + 2);

break;

case KeyEvent.KEYCODE_DPAD_LEFT:

seekBar_kD_adjuster.setProgress(seekBar_kD_adjuster.getProgress() - 2);

break;

}

}

@Override

public void onMotionEvent(MotionEvent event) {

mPlayer.mAxisX = event.getAxisValue(MotionEvent.AXIS_X);

mPlayer.mAxisY = event.getAxisValue(MotionEvent.AXIS_Y);

mPlayer.mAxisZ = event.getAxisValue(MotionEvent.AXIS_Z);

mPlayer.mAxisRZ = event.getAxisValue(MotionEvent.AXIS_RZ);

}

@Override

public void onStateEvent(StateEvent event) {

switch (event.getState()) {

case StateEvent.STATE_CONNECTION:

mPlayer.mConnection = event.getAction();

break;

}

}

}

public class ExamplePlayer {

static final float DEFAULT_SCALE = 4.0f;

static final float DEFAULT_X = 0.0f;

static final float DEFAULT_Y = 0.0f;

boolean gotPadVersion = false;

public int mConnection = StateEvent.ACTION_DISCONNECTED;

public int mControllerVersion = StateEvent.STATE_UNKNOWN;

public int mButtonA = KeyEvent.ACTION_UP;

public int mButtonB = KeyEvent.ACTION_UP;

public int mButtonX = KeyEvent.ACTION_UP;

public int mButtonY = KeyEvent.ACTION_UP;

public int DpadUp = KeyEvent.ACTION_UP;

public int DpadDown = KeyEvent.ACTION_UP;

public int DpadLeft = KeyEvent.ACTION_UP;

public int DpadRight = KeyEvent.ACTION_UP;

public float mAxisX = 0.0f;

public float mAxisY = 0.0f;

public float mAxisZ = 0.0f;

public float mAxisRZ = 0.0f;

final float mR;

final float mG;

final float mB;

float mScale = DEFAULT_SCALE;

float mX = DEFAULT_X;

float mY = DEFAULT_Y;

public ExamplePlayer(float r, float g, float b) {

mR = r;

mG = g;

mB = b;

}

}

public class Looper extends BaseIOIOLooper {

private PwmOutput LeftWheel;

private PwmOutput RightWheel;

private PwmOutput LeftArm;

private PwmOutput RightArm;

private int currentAngle;

private int previousAngle;

private int P = 0;

private int I = 0;

private int D = 0;

private int PID = 0;

private int D_delta;

private int I_delta;

public int seekbar_tilt_adjuster_value = 500;

private int seekbar_kP_adjuster_value = 0;

private int seekbar_kI_adjuster_value = 0;

private int seekbar_kD_adjuster_value = 0;

@Override

public void setup() throws ConnectionLostException {

LeftArm = ioio_.openPwmOutput(13, 50);

RightArm = ioio_.openPwmOutput(12, 50);

LeftWheel = ioio_.openPwmOutput(10, 100);

RightWheel = ioio_.openPwmOutput(11, 100);

enableUi(true);

}

@Override

public void loop() throws ConnectionLostException, InterruptedException {

seekbar_tilt_adjuster_value = seekBar_Tilt_adjuster.getProgress() - 500;

seekbar_kP_adjuster_value = seekBar_kP_adjuster.getProgress();

seekbar_kI_adjuster_value = seekBar_kI_adjuster.getProgress();

seekbar_kD_adjuster_value = seekBar_kD_adjuster.getProgress();

//currentAngle = (Math.round(orientationVals[1] * 100)) + seekbar_tilt_adjuster_value - Math.round(mPlayer.mAxisY*10) - Math.round(mPlayer.mAxisRZ*10);

currentAngle = (Math.round(orientationVals[1] * 100)) + seekbar_tilt_adjuster_value;

D_delta = currentAngle - previousAngle;

I_delta = I_delta + D_delta;

P = (seekbar_kP_adjuster_value * currentAngle) / 2000;

I = (seekbar_kI_adjuster_value * I_delta) / 4000;

D = (seekbar_kD_adjuster_value * D_delta) / 4000;

//if (I < -100) {

// I = -100;

// } else if (I>100) {

// I=100;

// }

PID = P + I + D;

previousAngle = currentAngle;

RightArm.setPulseWidth(1590 - Math.round(mPlayer.mAxisY*900));

LeftArm.setPulseWidth(1540 + Math.round(mPlayer.mAxisRZ*900));

RightWheel.setPulseWidth(1500 + Math.round(mPlayer.mAxisY*35) + PID);

LeftWheel.setPulseWidth(1502 - Math.round(mPlayer.mAxisRZ*35) - PID);

//LeftWheel.setPulseWidth(1502 - PID);

//RightWheel.setPulseWidth(1500 + PID);

//setText_current_angle(Integer.toString(currentAngle));

//setText_tilt_adjuster(Integer.toString(seekbar_tilt_adjuster_value));

//setText_kP_adjuster(Integer.toString(seekbar_kP_adjuster_value));

//setText_kI_adjuster(Integer.toString(seekbar_kI_adjuster_value));

//setText_kD_adjuster(Integer.toString(seekbar_kD_adjuster_value));

setText_current_angle(Integer.toString(currentAngle));

setText_tilt_adjuster(Integer.toString(PID));

setText_kP_adjuster(Integer.toString(P));

setText_kI_adjuster(Integer.toString(I));

setText_kD_adjuster(Integer.toString(D));

}

@Override

public void disconnected() {

enableUi(false);

}

}

@Override

protected IOIOLooper createIOIOLooper() {

return new Looper();

}

private void enableUi(final boolean enable) {

runOnUiThread(new Runnable() {

@Override

public void run() {

}

});

}

private void setText_current_angle(final String str) {

runOnUiThread(new Runnable() {

@Override

public void run() {

textView_Current_Angle.setText(str);

}

});

}

private void setText_tilt_adjuster(final String str) {

runOnUiThread(new Runnable() {

@Override

public void run() {

textView_Tilt_adjuster.setText(str);

}

});

}

private void setText_kP_adjuster(final String str) {

runOnUiThread(new Runnable() {

@Override

public void run() {

textView_kP_adjuster.setText(str);

}

});

}

private void setText_kI_adjuster(final String str) {

runOnUiThread(new Runnable() {

@Override

public void run() {

textView_kI_adjuster.setText(str);

}

});

}

private void setText_kD_adjuster(final String str) {

runOnUiThread(new Runnable() {

@Override

public void run() {

textView_kD_adjuster.setText(str);

}

});

}

@Override

public void onSensorChanged(SensorEvent event)

{

if(event.sensor.getType()==Sensor.TYPE_ROTATION_VECTOR)

{

SensorManager.getRotationMatrixFromVector(mRotationMatrix,event.values);

SensorManager.remapCoordinateSystem(mRotationMatrix,SensorManager.AXIS_X, SensorManager.AXIS_Z, mRotationMatrix);

SensorManager.getOrientation(mRotationMatrix, orientationVals);

orientationVals[1]=(float)Math.toDegrees(orientationVals[1]);

//seekBar_kD_adjuster.setProgress(Math.round((orientationVals[1]*4)+500));

}

}

@Override

public void onAccuracyChanged(Sensor sensor, int accuracy) {

}

@Override

protected void onResume() {

super.onResume();

// register this class as a listener for the orientation and

// accelerometer sensors

mSensorManager.registerListener(this,

mRotVectSensor,

10000);

mController.onResume();

mPlayer.mConnection = mController.getState(Controller.STATE_CONNECTION);

mPlayer.mControllerVersion = mController.getState(Controller.STATE_CURRENT_PRODUCT_VERSION); // Get controller version

mPlayer.mButtonA = mController.getKeyCode(Controller.KEYCODE_BUTTON_A);

mPlayer.mButtonB = mController.getKeyCode(Controller.KEYCODE_BUTTON_B);

mPlayer.mButtonX = mController.getKeyCode(Controller.KEYCODE_BUTTON_X);

mPlayer.mButtonY = mController.getKeyCode(Controller.KEYCODE_BUTTON_Y);

mPlayer.DpadUp = mController.getKeyCode(Controller.KEYCODE_DPAD_UP);

mPlayer.DpadDown = mController.getKeyCode(Controller.KEYCODE_DPAD_DOWN);

mPlayer.DpadLeft = mController.getKeyCode(Controller.KEYCODE_DPAD_LEFT);

mPlayer.DpadRight = mController.getKeyCode(Controller.KEYCODE_DPAD_RIGHT);

mPlayer.mAxisY = mController.getAxisValue(Controller.AXIS_Y);

mPlayer.mAxisRZ = mController.getAxisValue(Controller.AXIS_RZ);

}

@Override

protected void onPause() {

// unregister listener

super.onPause();

mSensorManager.unregisterListener(this);

mController.onPause();

}

@Override

protected void onDestroy() {

mController.exit();

super.onDestroy();

}

}

// ----------------- ROBOT DIAGRAM ----------------------------- //

//

// _________

// | Front |

// | Facing |

// | Phone |

// ----------

// | |

// =========

// LeftArm = 13 ||===| |===|| RightArm = 12

// ||===| |===||

// || | | ||

// || | | ||

// | |

// | |

// || | | ||

// LeftWheel = 10 ||=============|| RightWheel = 11

// || ||

//

//