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. (1) Traxxas Stampede XL-5
  2. (1) Linksys WRT54GL wireless Router
  3. (1) Basic Stamp 2 Microcontroller
  4. (2) 5000mAh batteries
  5. (1) Nexus One Android phone
  6. (1) 12VDC Fixed-Voltage regulator 7812
  7. 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.

Basic Stamp MountFirst Build

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.

?Download STBTilt.java
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.