Hey all!
I want to introduce a project I’ve been working on for controlling a 1/10th scale remote control truck with an Android mobile phone. This is just a quick introduction to the project. I will have detailed steps and code once I get everything up and running, so please hang in there and check back soon.
I first got the idea for this project when I stumbled upon JBProjects.net. This is a great website, created by Jonathan Bennett, an electrical engineering student with a passion for robotics and other cool stuff. I highly recommend checking out his web site. Jonathan built a wifi RC truck using a Linksys WRT54GL router and a few other off the shelf components. The Linksys router is such a cool piece of hardware, that I decided to get one in order to build a similar truck to Jonathan’s. The big difference between our two trucks is the control scheme. I am using an Android phone to drive and steer the truck, while Jonathan used a VB6 app he wrote running on a laptop.
The Equipment
- (1) Traxxas Stampede XL-5
- (1) Linksys WRT54GL wireless Router
- (1) Basic Stamp 2 Microcontroller
- (2) 5000mAh batteries
- (1) Nexus One Android phone
- (1) 12VDC Fixed-Voltage regulator 7812
- Misc hardware to hold everything together
Construction
The truck is almost complete. I still need to mount the second battery and the Linksys router. The Basic Stamp has been secured to the truck platform. Since it’s a bare PCB board, I placed risers below the board in order for it to sit flush with the platform. The platform is an 8×12″ 3mm sintra sheet from Solarbotics.com. It’s rigid, but very easy to machine.
Since both the hardware I had chosen and my execution design wasn’t going to be circuit for circuit or code for code to the original wifi truck, I had to ensure that all of the hardware would work together. Specifically, I had to figure out if the Basic Stamp could send compatible signals to the RC truck’s electronic speed controller(ESC) and steering servo. To do this, I removed the housing around the truck’s RC receiver, where the ESC and steering servo connected to, and recorded the signals received from the RC transmitter. I used a digital multimeter connected to a computer to measure and record both the pulse width and duty cycle of the signal. With this information and a little trial and error, I was able to control the speed of the motor and the direction of the steering servo.
Below is some test code I wrote for the Basic Stamp to verify that I could control the speed and direction of the motor.
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 | ' {$STAMP BS2} ' {$PBASIC 2.5} ' {$PORT COM1} counter VAR Word TX CON 0 interval CON 1000 'Motor Brake FOR counter = 1 TO interval PULSOUT TX, 680 PAUSE 17 NEXT 'Motor Full Speed Forward FOR counter = 1 TO interval PULSOUT TX, 930 PAUSE 17 NEXT 'Motor Brake FOR counter = 1 TO interval PULSOUT TX, 680 PAUSE 17 NEXT 'Motor Full Speed Reverse FOR counter = 1 TO interval PULSOUT TX, 425 PAUSE 17 NEXT 'Motor Brake FOR counter = 1 TO interval PULSOUT TX, 680 PAUSE 17 NEXT |
Programming
This is definitely the most difficult part of the whole project. There are three major hardware components, each requiring their own specific program, all using different code languages. The Android phone uses Java, the Linksys router runs C, and the Basic Stamp uses PBasic.
Linksys Router
I have to again give a big thanks to Jonathan. I used his carserver.c code that runs on the Linksys router. This piece of code receives the telemetry from the control device, Android phone, and then relays it to the Basic Stamp. I’ve made a few modifications to Jonathan’s code to fit my needs. I’m also running the openWRT Kamikaze 7.09 firmware on my router instead of WhiteRussian. This required me to compile a new binary for the router, different to the one Jonathan uses. I followed a howto document, written by Eric Bishop, to compile the carserver code for my router.
Android Phone
The Android code is very simple right now. I’ve created a deadman button, so that data will only be sent to the router when the button is depressed. As soon as the user lets go of the button, data will no longer be transmitted to the router. When pressed, the program reads the orientation sensor and sends the pitch and roll data to the Linksys router.
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 | package com.sharethebytes.STBTilt; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.MotionEvent; import android.graphics.PixelFormat; import android.widget.TextView; import android.widget.Button; import android.hardware.Sensor; import android.hardware.SensorManager; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.GeomagneticField; import android.util.Log; import java.lang.String; import java.io.* ; import java.net.* ; import java.net.InetAddress; import java.net.NetworkInterface; import java.lang.Thread; import java.lang.Exception; import java.lang.Character.UnicodeBlock; public class STBTilt extends Activity { static final double PI = 3.14159265358979323; static TextView Azimuth; static TextView Pitch; static TextView Roll; static Button Run; static double orientArray[] = new double[3]; static final double kFilter = 1; private static Sensor sensor; private static SensorManager sm; static OutputStream out; static Socket s = null; static String myName; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getWindow().setFormat(PixelFormat.TRANSLUCENT); requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.main); Azimuth = (TextView) findViewById(R.id.azimuth); Pitch = (TextView) findViewById(R.id.pitch); Roll = (TextView) findViewById(R.id.roll); Azimuth.setText("0"); Pitch.setText("0"); Roll.setText("0"); orientArray[0] = 0; orientArray[1] = 0; orientArray[2] = 0; Run = (Button) findViewById(R.id.run); Run.setOnTouchListener(new View.OnTouchListener(){ public boolean onTouch( View yourButton , MotionEvent theMotion ) { switch ( theMotion.getAction() ) { case MotionEvent.ACTION_DOWN: Sensor accelerometer = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); Sensor magnetometer = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); // Register your SensorListener sm.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_GAME); sm.registerListener(sensorEventListener, magnetometer, SensorManager.SENSOR_DELAY_GAME); //myName = NetworkInterface.getName(); Log.d("Net","***Network Interface: " + myName); break; case MotionEvent.ACTION_UP: sm.unregisterListener(sensorEventListener); if(s != null){ try { s.close(); s=null; } catch (IOException ex) { Log.e("Socket","***Socket Close Error " + ex.toString()); } } break; } return true; } }); sm = (SensorManager) getSystemService(SENSOR_SERVICE); } /** * The listener that listen to events from the orientation listener */ private static SensorEventListener sensorEventListener = new SensorEventListener() { private float[] mGravity; private float[] mGeomagnetic; public void onAccuracyChanged(Sensor sensor, int accuracy) { } public void onSensorChanged(SensorEvent event) { if(event.sensor.getType() == Sensor.TYPE_ACCELEROMETER){ mGravity = event.values.clone(); Log.d("Accel","***X: " + String.valueOf(mGravity[0])); Log.d("Accel","***Y: " + String.valueOf(mGravity[1])); Log.d("Accel","***Z: " + String.valueOf(mGravity[2])); } if(event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD){ mGeomagnetic = event.values.clone(); Log.d("Magnetic","***Compass 1: " + mGeomagnetic[0]); Log.d("Magnetic","***Compass 2: " + mGeomagnetic[1]); Log.d("Magnetic","***Compass 3: " + mGeomagnetic[2]); } if(mGravity != null && mGeomagnetic != null){ float[] R = new float[9]; float[] I = new float[9]; boolean success = SensorManager.getRotationMatrix(R,I, mGravity, mGeomagnetic); if(success){ float orientation[] = new float[3]; SensorManager.getOrientation(R,orientation); //Use a basic low-pass filter to only keep the gravity in the accelerometer values orientArray[0] = orientation[0]*180/PI * kFilter + orientArray[0] * (1.0 - kFilter); orientArray[1] = orientation[1]*180/PI * kFilter + orientArray[1] * (1.0 - kFilter); orientArray[2] = orientation[2]*180/PI * kFilter + orientArray[2] * (1.0 - kFilter); Azimuth.setText("Azimuth: " + Math.round(orientArray[0])+""); Pitch.setText("Pitch: " + Math.round(orientArray[1])+""); Roll.setText("Roll: " + Math.round(orientArray[2])+""); if(s!=null){ String message = Math.round(orientArray[0])+":"+Math.round(orientArray[1])+":"+Math.round(orientArray[2])+""; sendMessage(message); } } } } }; public static void sendMessage(String message){ byte[] b = null; try { b=message.getBytes("UnicodeLittleUnMarked"); out.write(b,0,b.length); //Integer size= b.length; //Log.d("Socket","***" + size.toString()); } catch (IOException ex) { Log.e("Socket","***Socket Send Error " + ex.toString()); } } @Override public void onDestroy(){ super.onDestroy(); Log.d("Destroy","***Destroying***"); sm.unregisterListener(sensorEventListener); finish(); } @Override public void onPause(){ super.onPause(); Log.d("Pause","***Pausing***"); sm.unregisterListener(sensorEventListener); if(s != null){ try { s.close(); s=null; } catch (IOException ex) { Log.e("Socket","***Socket Close Error " + ex.toString()); } } finish(); } @Override public void onResume(){ super.onResume(); Log.d("Resume","***Resuming***"); } } |
That’s it for now. Come back soon and I will have all the steps and all the code that will get this truck moving.



