https://www.youtube.com/watch?v=GjeKULMOL8E
The 3-wheel car is built with the Makeblock Robot Starter Kit. (Plus some Legos for style.) It has a Bluetooth module. It is controlled by the phone’s gyros via Bluetooth. You drive it like a steering wheel with two hands, or like Darth Vader with one hand.
1. Code for the smartphone below. I saved this as gyroCar.py and transferred it to my phone (LG Optimus Dynamic). Then I execute/run it using SL4A from the phone itself:
#!/usr/bin/env python
import android
import sys
import time
class RoboComm:
def __init__(self, droid, baotooth):
self.droid = droid
self.baotooth = baotooth
self.lastCommand = None
def sendCmd(self, cmd):
if cmd is None:
return
if "fwd" in cmd or "bwd" in cmd \
or "left" in cmd or "right" in cmd \
or "stop" in cmd:
# don't send redundant movement commands to save data
if self.lastCommand == cmd:
return
print cmd
self.lastCommand = cmd
if self.baotooth:
self.droid.bluetoothWrite(cmd + "\n", self.baotooth)
class DroidGyroJoystick:
def __init__(self, droid):
self.droid = droid
self.orient = None
self.joyFwdBwdPos = 0
self.joyLeftRightPos = 0
self.accelZero = 0.4 # deflection of less than this is treated as no deflection
self.turnZero = 0.3 # deflection of less than this is treated as no deflection
self.minSpeed = 0.4
if self.droid:
self.droid.startSensingTimed(1, 200)
self.updateOrient()
def updateOrient(self):
if self.droid:
self.orient = self.droid.sensorsReadOrientation().result
def getJoyCommand(self):
#
# translate gyro reading to joystick input
#
if not self.orient:
return None
if self.orient[0] is None:
return None
turn = self.orient[1] # [1.57=left, 0=stop, -1.57=right]
accel = self.orient[2] # [0=fwd, -1.57=stop, -3.14=bwd]
#if accel > 0 and accel < 0.7:
# # tipping phone too far forward but let's treat as fwd anyway
# accel = 0
if turn > 1.57:
turn = 1.57 # hard left, past horizon = max left
if turn < -1.57:
turn = -1.57 # hard right, past horizon = max right
if accel > 0:
# phone is inverted - send stop signal
return "!stop"
# normalize turn and accel values to [-1.0, 1.0]
turn = (-turn) / 1.57
accel = ((-accel) - 1.57) / 1.57
# filter for neutral pos
if turn > -self.turnZero and turn < self.turnZero:
turn = 0
if accel > -self.accelZero and accel < self.accelZero:
accel = 0
# set min speed
if turn > 0:
turn = max(self.minSpeed, turn)
if turn < 0:
turn = min(-self.minSpeed, turn)
if accel > 0:
accel = max(self.minSpeed, accel)
if accel < 0:
accel = min(-self.minSpeed, accel)
self.joyFwdBwdPos = int(accel * 10)
self.joyLeftRightPos = int(turn * 10)
# decide move cmd based on vectors
fbmag = min(abs(self.joyFwdBwdPos) + 1, 10)
lrmag = min(abs(self.joyLeftRightPos) + 2, 10)
if self.joyFwdBwdPos < 0:
if self.joyLeftRightPos < 0:
return "!fwdleft %d %d" % (fbmag, lrmag)
elif self.joyLeftRightPos > 0:
return "!fwdright %d %d" % (fbmag, lrmag)
else:
return "!fwd %d" % fbmag
elif self.joyFwdBwdPos > 0:
if self.joyLeftRightPos < 0:
return "!bwdleft %d %d" % (fbmag, lrmag)
elif self.joyLeftRightPos > 0:
return "!bwdright %d %d" % (fbmag, lrmag)
else:
return "!bwd %d" % fbmag
elif self.joyLeftRightPos < 0:
return "!left %d" % lrmag
elif self.joyLeftRightPos > 0:
return "!right %d" % lrmag
return "!stop"
class GyroCar:
def __init__(self):
self.droid = None
self.baotooth = None
self.droid = android.Android()
#self.droid = android.Android(('192.168.1.6', 33333))
self.droid.wakeLockAcquireDim()
#self.droid.generateDtmfTones('123')
self.connectBluetooth()
self.roboComm = RoboComm(self.droid, self.baotooth)
self.gyro = DroidGyroJoystick(self.droid)
def __del__(self):
print "Goodbye!"
if self.droid:
if self.baotooth:
self.droid.bluetoothStop(self.baotooth)
self.droid.wakeLockRelease()
def connectBluetooth(self):
uuid = "00001101-0000-1000-8000-00805F9B34FB"
mac = "98:D3:31:B0:E9:37" # makeblock mac
self.droid.bluetoothConnect(uuid, mac)
conns = self.droid.bluetoothActiveConnections()
if not conns.result:
print "Android phone failed to link to Baotooth"
else:
print "Baotooth connected"
self.baotooth = conns.result.keys()[0]
if (self.baotooth):
self.droid.bluetoothWrite("!beep\n", self.baotooth)
def run(self):
# poll gyro, translate to bao commands
# run forever
while True:
self.gyro.updateOrient()
cmd = self.gyro.getJoyCommand()
self.roboComm.sendCmd(cmd)
time.sleep(0.2)
# allow use as a module or standalone script
if __name__ == "__main__":
gyroCar = GyroCar()
gyroCar.run()
If you use the code above, the part that you’ll need to tweak for your own build is the DroidGyroJoystick class. Particularly, the pitch/roll numbers my phone gives lying on one edge may be different from yours. The other things you might want to change are the tuning parameters. For example, I configured a “deadzone” with accelZero and turnZero so that small motions near “centered” position won’t count as joystick inputs.
(If you’re new to SL4A, I show how to get set up, as well as where the android.py in “import android” above comes from, here.)
2. Arduino sketch for the robot car below. Sorry it’s unnecessarily complicated. It’s because it’s code for a 2-in-1 IR or Bluetooth + obstacle-avoidance car. If you connect only an IR sensor to Port 6, it will be IR-controlled. If you connect only a Bluetooth module to Port 7, it will be Bluetooth-controlled. Why did I do such a thing? Because I wanted to be able to switch remotes for my kids just by swapping modules, without uploading sketches.
Anyway, what’s important here is that in Bluetooth mode, it recognizes my silly custom command set (fwd, left, right, bwd, etc). These command strings are how the SL4A Python script on the phone instructs the car what to do, over Bluetooth. The other useful thing is that the Bluetooth commands let me specify the speed, in addition to direction. So the more you push your joystick forward, the faster it’ll go.
#include
#include
#include
#include
#include
MeDCMotor MotorL(M1);
MeDCMotor MotorR(M2);
MeUltrasonicSensor ultrasonic(PORT_3);
MeInfraredReceiver infrared(PORT_6);
MeBluetooth bluetooth(PORT_7);
const int MAXBUFSZ = 128;
char g_lineBuf[MAXBUFSZ];
int g_bufReadIdx = 0;
// movement settings
const boolean g_debug = false;
const int g_stepPeriod = 50;
const int g_maxStepsForward = 3000 / g_stepPeriod;
const int g_maxTurnSteps = 3000 / g_stepPeriod;
const int g_minSpeed = 45;
const int g_maxSpeed = 255;
const int g_speedFactor = 23;
const float g_leftTrim = 1.;
const float g_rightTrim = 1.;
// enums
const int LED_PIN = 13;
const int EBRAKE_STOP = 2; // full stop
const int EBRAKE_WAIT = 1; // wait for "Go" signal
const int EBRAKE_GO = 0; // brakes off and GO!
const int MODE_ULTRASONIC = 0;
const int MODE_IR = 1;
const int MODE_BLUETOOTH = 2;
const int MODE_UNKNOWN = 3;
const int ULTRASONIC_INIT_SPEED = 200;
// robo state machine
int g_moveSpeed = ULTRASONIC_INIT_SPEED;
int g_numStepsForward = 0;
int g_numTurnSteps = 0;
boolean g_rightFlag;
int g_ebrake = EBRAKE_STOP;
long g_timeLastBlocked = 0;
uint8_t g_mode = MODE_UNKNOWN;
uint8_t g_lastMode = MODE_UNKNOWN;
void setup()
{
Serial.begin(9600);
Serial.println("Software serial on");
bluetooth.begin(9600);
Serial.println("Bluetooth on");
infrared.begin();
Serial.println("Infrared on");
g_lineBuf[0] = '\0';
ResetUltrasonic(EBRAKE_STOP);
pinMode(LED_PIN, OUTPUT); // "obstructed" LED
Serial.println("Bao ready");
}
void loop()
{
boolean hasBlue = false;
boolean hasRed = false;
if (g_mode == MODE_UNKNOWN)
{
// first time - detect if we're in IR or Bluetooth mode
hasRed = infrared.available() || infrared.buttonState();
hasBlue = bluetooth.available();
if (hasRed)
SwitchMode(MODE_IR);
if (hasBlue)
SwitchMode(MODE_BLUETOOTH);
}
else if (g_mode == MODE_IR || g_lastMode == MODE_IR)
hasRed = infrared.available() || infrared.buttonState();
else if (g_mode == MODE_BLUETOOTH || g_lastMode == MODE_BLUETOOTH)
hasBlue = bluetooth.available();
char inDat;
if (hasBlue)
{
ResetUltrasonic(EBRAKE_GO);
inDat = bluetooth.read();
Serial.print(inDat);
readIntoLineBuf(inDat);
if (cmdLineReady())
{
String cmd(g_lineBuf);
if (g_debug)
Serial.println("COMMAND " + cmd);
processCommand(cmd);
}
}
else if (hasRed)
{
ResetUltrasonic(EBRAKE_GO);
inDat = infrared.read();
switch (inDat)
{
case IR_BUTTON_PLUS:
Forward(g_moveSpeed);
break;
case IR_BUTTON_MINUS:
Backward(g_moveSpeed);
break;
case IR_BUTTON_NEXT:
TurnRight(g_moveSpeed*2);
break;
case IR_BUTTON_PREVIOUS:
TurnLeft(g_moveSpeed*2);
break;
case IR_BUTTON_TEST:
ForwardAndLeft(g_moveSpeed, g_moveSpeed);
break;
case IR_BUTTON_RETURN:
ForwardAndRight(g_moveSpeed, g_moveSpeed);
break;
case IR_BUTTON_9:
ChangeSpeed(g_speedFactor*9+g_minSpeed);
break;
case IR_BUTTON_8:
ChangeSpeed(g_speedFactor*8+g_minSpeed);
break;
case IR_BUTTON_7:
ChangeSpeed(g_speedFactor*7+g_minSpeed);
break;
case IR_BUTTON_6:
ChangeSpeed(g_speedFactor*6+g_minSpeed);
break;
case IR_BUTTON_5:
ChangeSpeed(g_speedFactor*5+g_minSpeed);
break;
case IR_BUTTON_4:
ChangeSpeed(g_speedFactor*4+g_minSpeed);
break;
case IR_BUTTON_3:
ChangeSpeed(g_speedFactor*3+g_minSpeed);
break;
case IR_BUTTON_2:
ChangeSpeed(g_speedFactor*2+g_minSpeed);
break;
case IR_BUTTON_1:
ChangeSpeed(g_speedFactor*1+g_minSpeed);
break;
case IR_BUTTON_POWER:
buzzerOn();
delay(100);
buzzerOff();
break;
case IR_BUTTON_MENU:
if (g_mode != MODE_ULTRASONIC)
{
// go ultrasonic
buzzerOn();
delay(100);
buzzerOff();
delay(100);
buzzerOn();
delay(100);
buzzerOff();
delay(100);
buzzerOn();
delay(300);
buzzerOff();
SwitchMode(MODE_ULTRASONIC);
g_moveSpeed = ULTRASONIC_INIT_SPEED;
}
else
{
// back to manual remote
buzzerOn();
delay(100);
buzzerOff();
digitalWrite(LED_PIN, LOW);
SwitchMode(MODE_IR);
}
break;
default:
break;
}
}
else
{
if (g_mode == MODE_ULTRASONIC
&& millis() % g_stepPeriod == 0)
doUltrasonicCar();
else if (g_mode == MODE_IR)
Stop();
}
}
bool processCommand(String cmd)
{
if (cmd == "!beep")
{
buzzerOn();
delay(100);
buzzerOff();
}
else if (cmd.startsWith("!fwd "))
Forward(getSpeed(cmd, 0));
else if (cmd.startsWith("!bwd "))
Backward(getSpeed(cmd, 0));
else if (cmd.startsWith("!left "))
TurnLeft(getSpeed(cmd, 0));
else if (cmd.startsWith("!right "))
TurnRight(getSpeed(cmd, 0));
else if (cmd.startsWith("!fwdleft"))
ForwardAndLeft(getSpeed(cmd, 0), getSpeed(cmd, 1));
else if (cmd.startsWith("!fwdright"))
ForwardAndRight(getSpeed(cmd, 0), getSpeed(cmd, 1));
else if (cmd.startsWith("!bwdleft"))
BackwardAndTurnLeft(getSpeed(cmd, 0), getSpeed(cmd, 1));
else if (cmd.startsWith("!bwdright"))
BackwardAndTurnRight(getSpeed(cmd, 0), getSpeed(cmd, 1));
else
Stop();
}
int getSpeed(String cmd, int idx)
{
int mag = getCmdInt(cmd, idx);
return g_speedFactor * mag + g_minSpeed;
}
int getCmdInt(String cmd, int idx)
{
int spaceIdx = cmd.indexOf(' ');
for (int i=0; i < idx; i++)
spaceIdx = cmd.indexOf(' ', spaceIdx+1);
return cmd.substring(spaceIdx+1).toInt();
}
bool cmdLineReady()
{
if (g_bufReadIdx != 0)
return false; // still building line
if (g_lineBuf[0] != '!')
return false; // not a command line
return true;
}
void readIntoLineBuf(char c)
{
if (c == '\n' || c == '\r' || c == '\0')
{
// finish line for processing, set ready for next line
g_bufReadIdx = 0;
return;
}
if (g_bufReadIdx >= MAXBUFSZ - 1)
{
// drop overflow bytes
return;
}
g_lineBuf[g_bufReadIdx] = c;
g_lineBuf[g_bufReadIdx+1] = '\0';
g_bufReadIdx++;
}
void doUltrasonicCar()
{
int distance = ultrasonic.distanceCm();
boolean isBlocked = (distance > 0 && distance < 60);
if (isBlocked)
g_timeLastBlocked = millis();
else if (millis() - g_timeLastBlocked < 100)
isBlocked = true;
if (isBlocked)
digitalWrite(LED_PIN, HIGH);
else
digitalWrite(LED_PIN, LOW);
boolean isStuck
= (g_numTurnSteps > g_maxTurnSteps
|| g_numStepsForward > g_maxStepsForward);
if (g_ebrake == EBRAKE_STOP && !isBlocked)
{
// /unstuck, ready for go signal
g_ebrake = EBRAKE_WAIT;
return;
}
else if (g_ebrake == EBRAKE_WAIT && isBlocked)
{
// "Go!"
ResetUltrasonic(EBRAKE_GO);
return;
}
else if (g_ebrake == EBRAKE_GO && isStuck)
{
// stuck -- yank ebrake
g_ebrake = EBRAKE_STOP;
Stop();
return;
}
if (g_ebrake != EBRAKE_GO)
{
// halt until !blocked and receive "Go" signal
return;
}
if (isBlocked)
{
// evasive maneuvers!
randomSeed(analogRead(A4));
int randnum = random(300);
if (distance > 30)
{
if (randnum > 150 && !g_rightFlag)
TurnLeft(g_moveSpeed);
else
TurnRight(g_moveSpeed);
}
else
{
if (randnum > 150)
BackwardAndTurnLeft(g_moveSpeed, g_moveSpeed);
else
BackwardAndTurnRight(g_moveSpeed, g_moveSpeed);
}
}
else
{
// ONWARD!
Forward(g_moveSpeed);
}
}
void ResetUltrasonic(int brakePos)
{
g_rightFlag = false;
g_numStepsForward = 0;
g_numTurnSteps = 0;
g_ebrake = brakePos;
if (g_debug && g_mode == MODE_ULTRASONIC)
Serial.println("Reset");
}
void Stop()
{
MotorL.stop();
MotorR.stop();
}
void Forward(int speed)
{
g_numStepsForward++;
g_numTurnSteps = 0;
g_rightFlag = false;
MotorL.run(speed * g_leftTrim);
MotorR.run(speed * g_rightTrim);
if (g_debug)
Serial.println("Forward");
}
void ForwardAndLeft(int fspeed, int lspeed)
{
g_numStepsForward++;
g_numTurnSteps++;
g_rightFlag = false;
MotorL.run(fspeed - (lspeed / 2));
MotorR.run(fspeed);
if (g_debug)
Serial.println("Forward+Left");
}
void ForwardAndRight(int fspeed, int rspeed)
{
g_numStepsForward++;
g_numTurnSteps++;
g_rightFlag = true;
MotorL.run(fspeed);
MotorR.run(fspeed - (rspeed / 2));
if (g_debug)
Serial.println("Forward+Right");
}
void Backward(int speed)
{
g_rightFlag = false;
g_numTurnSteps = 0;
MotorL.run(-speed);
MotorR.run(-speed);
if (g_debug)
Serial.println("Backward");
}
void TurnLeft(int speed)
{
g_rightFlag = false;
g_numStepsForward = 0;
g_numTurnSteps++;
MotorL.run(-speed / 2);
MotorR.run(speed / 2);
if (g_debug)
Serial.println("Left");
}
void TurnRight(int speed)
{
g_rightFlag = true;
g_numStepsForward = 0;
g_numTurnSteps++;
MotorL.run(speed / 2);
MotorR.run(-speed / 2);
if (g_debug)
Serial.println("Right");
}
void BackwardAndTurnLeft(int bspeed, int lspeed)
{
g_rightFlag = true;
g_numStepsForward = 0;
g_numTurnSteps++;
MotorL.run(-bspeed + (lspeed / 2));
MotorR.run(-bspeed);
if (g_debug)
Serial.println("Backward+Left");
}
void BackwardAndTurnRight(int bspeed, int rspeed)
{
g_rightFlag = false;
g_numStepsForward = 0;
g_numTurnSteps++;
MotorL.run(-bspeed);
MotorR.run(-bspeed + (rspeed / 2));
if (g_debug)
Serial.println("Backward+Right");
}
void SwitchMode(uint8_t newMode)
{
g_lastMode = g_mode;
g_mode = newMode;
}
void ChangeSpeed(int spd)
{
g_moveSpeed = spd;
}
P.S. I just installed the Wordpress “Crayon Syntax Highlighter” plugin. It prettifies blocks of code within ‹pre› tags, like above. I like it so far.