Introduction: Hackable Remote Control for ZenWheels Microcar
In this tutorial we're going to build a custom remote control for the ZenWheels microcar. The ZenWheels microcar is a 5 cm toy car that is controllable through an Android or Iphone application. I'm going to show you how to reverse engineer the Android application to find out about the communication protocol and how you can build a remote control using arduino and a gyroscope.
Step 1: Components & Tools
Parts:
1. The ZenWheels microcar
2. Arduino pro mini 328p
3. Breadboard
4. MPU6050 gyroscope
5. power source <=5 v (some battery that we can attach to the breadboard)
6. U-shape jumper cables (optional). I've used these jumper cables because they look better on the breadboard. Regular jumper cables can be used instead
7. HC-05 bluetooth module (with a button for entering AT mode)
Tools:
1. USB to serial FTDI adapter FT232RL to programm the Arduino pro mini
2. Arduino IDE
3. Android Phone
4. Android Studio [Optional]
Step 2: Reverse Engineering the ZenWheels Android Application [optional]
Some knowledge of Java and Android is required to understand this part.
The project goal is to control the microcar using a gyroscope. For this we need to find out more about the bluetooth communication between this toy and the android app.
In this step i will explain how to reverse engineer the communication protocol between the microcar and the android app. If you just want to build the remote this step is not necessary. One way to discover the protocol is to look at the source code. Hmm but this is not straight forward, android applications are compiled and one can install the apk through google play.
So i've made a basic guide for doing this:
1. Download the APK. An Android Package Kit (APK for short) is the package file format used by the Android operating system for distribution and installation of mobile apps
First search the application on google play store, in our case search "zenwheels" and you'll obtain the application link
Then search on google for "online apk downloader" and use one to download the apk. Usually they will ask for the application link (the one we've obtained earlier), then we'll press a download button and save it in our computer.
2. Decompile the APK. A decompiler in our situation is a tool that takes the APK and produce Java source code.
The simplest solution is to use an online decompiler to do the job. I've searched google for "online decompliler" and i've chosen http://www.javadecompilers.com/. You just need to upload the APK you've obtained earlier and
press the decompile. Then you just download the sources.
3. Try to reverse engineer looking through the code
To open the project you need a text editor or better an IDE (integrated development environment). The default IDE for Android Projects is Android Studio (https://developer.android.com/studio). After you've installed the Android Studio open the project folder.
Because our car is controlled by bluetooth i started my search in the decompiled code with the keyword "bluetooth", from the occurrences i've found "BluetoothSerialService" was in handle of the communication. If this class handles the communication then it must have a send command method. Turns out there is one write method that sends data through the bluetooth channel:
public void write(byte[] out)
This is a good start, i've searched for the .write( method being used and there is a class "ZenWheelsMicrocar" that extends our "BluetoothSerialService". This class contains most of the logic of our communication over Bluetooth. The other part of the logic is in the controllers: BaseController and StandardController.
In the BaseController we have the service initialization, and also definitions of the steering and throttle channels, channels are in fact command prefixes to specify that some type of command will follow:
protected ZenWheelsMicrocar microcar = new ZenWheelsMicrocar(this, this.btHandler); protected ChannelOutput[] outputs = {new TrimChannelOutput(ZenWheelsMicrocar.STEERING_CHANNEL), new TrimChannelOutput(ZenWheelsMicrocar.THROTTLE_CHANNEL)};
In the StandardController the steering is handled in:
public void handleSteering(TouchEvent touchEvent) { ... this.microcar.setChannel(steeringOutput.channel, steeringOutput.resolveValue()); }
Analizing the method, the steeringOutput.channel has the value 129 (channel used for steering) and steeringOutput.resolveValue() may have a value between -90 and 90. The channel value (129) is sent directly, and the steering value is modified by applying bitwise operations:
private final int value_convert_out(int value) { boolean negative = false; if (value < 0) { negative = f6D; } int value2 = value & 63; if (negative) { return value2 | 64; } return value2; }
There is a similar method in the StandardController called
public void handleThrottle(TouchEvent touchEvent)
Step 3: Components
Parts:
1. Arduino pro mini 328p 2 $
2. Breadboard
3. MPU6050 gyroscope 1.2$
4. HC-05 master-slave 6 pin module 3$
5. 4 x AA battery pack with 4 batteries
6. U-shape jumper cables (optional). I've used these jumper cables because they look better on the breadboard, and the leds are more visible this way. If you don't have these cables you can replace them with dupont wires.
The above prices are taken from eBay.
Tools:
1. USB to serial FTDI adapter FT232RL to program the arduino pro mini
2. Arduino IDE
3. Android Studio (optional if you want to reverse engineer yourself)
Step 4: Assembly
The assembly is very simple because we're doing it on a breadboard :)
- first we place our components on the breadboard: the microcontroller, bluetooth module and gyroscope
- connect the HC-05 bluetooth RX and TX pins to arduino 10 and 11 pins. The gyroscope SDA and SCL should be connected to the arduino A4 and A5 pins
- connect the power pins to the bluetooth, gyro and the arduino. the pins should be connected to the + and - on the side of the breadboard
- last connect a power supply (between 3.3V to 5V) to the breadboard, i've used a small LiPo one cell battery but any will do as long as it's in the power range
Please check the pictures above to for more details
Step 5: Pair the HC-05 Bluetooth to the Microcar
For this you'll need an Android phone, the bluetooth HC-05 module and the serial FTDI adapter with wires. Also we'll be using the Arduino IDE to communicate with the bluetooth module.
First we need to find out the microcar bluetooth address:
- enable bluetooth on your phone
- power on the car and go to the bluetooth section of your settings in Android
- search for new devices and some device called "Microcar" should appear
- pair with this device
- then to extract the bluetooth MAC, i've used this app from google play Serial Bluetooth Terminal
After installing this app, go to menu -> devices and there you'll have a list with all bluetooth paired deveices. We're only interested in the code below the "Microcar" mine is 00:06:66:49:A0:4B
Next connect the FTDI adapter to the bluetooth module. First VCC and GROUND pins and then FTDI RX to bluetooth TX and FTDI TX to bluetooth RX. Also there should be a pin on the bluetooth module that should be connected to the VCC. Doing this the bluetooth module enters a "programmable mode". My module has a button that connects the VCC to that special pin. When you plug in the FTDI into the USB it should be with the pin connected / button pressed to enter in this special programmable mode. The bluetooth confirms entering this mode of operation by blinking slowly every 2 seconds.
In the Arduino IDE select the serial port, then open the serial monitor (Both NL and CR with 9600 baud rate). Type AT and the module should confirm with "OK".
Type "AT+ROLE=1" to put the module in master mode. To pair with your bluetooh module write: "AT+BIND=0006,66,49A04B", Notice how our "00:06:66:49:A0:4B" is transformed into "0006,66,49A04B". Well you should do the same transformation for your bluetooh MAC.
Now power on the Zenwheels car then unplug the FTDI and plug it again it without the button pressed / special pin connected. After a while it should connect to the car and you'll notice the car making a specific connection successful sound.
Troubleshooting:
- I found that from all the Bluetooth modules i had, only the one with a button worked as a master!
- ensure the car is charged fully
- make sure the car is not connected to the phone
- if the Bluetooth enters in the AT mode (blinks slowly) but it doesn't respond to command ensure that you have BOTH NL & CR, and also experiment with other BAUD rates
- double check the RX is connected to TX and vice versa
- try this tutorial
Step 6: Code & Usage
First you need to download and install two libraries:
1. MPU6050 library for the gyroscope
2. I2CDev library source
Then download and install my library from here or copy it from below:
/**
* Libraries: * https://github.com/jrowberg/i2cdevlib * https://github.com/jrowberg/i2cdevlib */ #include "I2Cdev.h" #include "MPU6050_6Axis_MotionApps20.h" #include "Wire.h" #include "SoftwareSerial.h"const int MAX_ANGLE = 45; const byte commandStering = 129; const byte commandSpeed = 130;
bool initialization = false; // set true if DMP init was successful uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU uint8_t devStatus; // return status after each device operation (0 = success, !0 = error) uint16_t packetSize; // expected DMP packet size (default is 42 bytes) uint16_t fifoCount; // count of all bytes currently in FIFO uint8_t fifoBuffer[64]; // FIFO storage buffer Quaternion q; // [w, x, y, z] quaternion container VectorFloat gravity; // [x, y, z] gravity vector float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector volatile bool mpuInterrupt = false; // indicates whether MPU interrupt pin has gone high
unsigned long lastPrintTime, lastMoveTime = 0;
SoftwareSerial BTserial(10, 11); MPU6050 mpu;
void setup() { Serial.begin(9600); BTserial.begin(38400); Serial.println("Program started"); initialization = initializeGyroscope(); }
void loop() { if (!initialization) { return; } mpuInterrupt = false; mpuIntStatus = mpu.getIntStatus(); fifoCount = mpu.getFIFOCount(); if (hasFifoOverflown(mpuIntStatus, fifoCount)) { mpu.resetFIFO(); return; } if (mpuIntStatus & 0x02) { while (fifoCount < packetSize) { fifoCount = mpu.getFIFOCount(); } mpu.getFIFOBytes(fifoBuffer, packetSize); fifoCount -= packetSize; mpu.dmpGetQuaternion(&q, fifoBuffer); mpu.dmpGetGravity(&gravity, &q); mpu.dmpGetYawPitchRoll(ypr, &q, &gravity); steer(ypr[0] * 180/M_PI, ypr[1] * 180/M_PI, ypr[2] * 180/M_PI); } }
/* * Receives angle from 0 to 180 where 0 is max left and 180 is max right * Receives speed from -90 to 90 where -90 is max backwards and 90 is max forward */ void moveZwheelsCar(byte angle, int speed) { if (millis() - lastMoveTime < 100) { return; } byte resultAngle; int resultSpeed; if (angle >= 90) { resultAngle = map(angle, 91, 180, 1, 60); } else if (angle < 90) { resultAngle = map(angle, 1, 90, 60, 120); } if (speed > 0) { resultSpeed = map(speed, 0, 90, 0, 60); } else if (speed < 0) { resultSpeed = map(speed, 0, -90, 120, 60); } Serial.print("actualAngle=");Serial.print(angle);Serial.print("; "); Serial.print("actualSpeed=");Serial.print(resultSpeed);Serial.println("; "); BTserial.write(commandStering); BTserial.write(resultAngle); BTserial.write(commandSpeed); BTserial.write((byte) resultSpeed); lastMoveTime = millis(); }
void steer(int x, int y, int z) { x = constrain(x, -1 * MAX_ANGLE, MAX_ANGLE); y = constrain(y, -1 * MAX_ANGLE, MAX_ANGLE); z = constrain(z, -MAX_ANGLE, MAX_ANGLE); int angle = map(y, -MAX_ANGLE, MAX_ANGLE, 0, 180); int speed = map(z, -MAX_ANGLE, MAX_ANGLE, 90, -90); printDebug(x, y, z, angle, speed); moveZwheelsCar(angle, speed); }
void printDebug(int x, int y, int z, int angle, int speed) { if (millis() - lastPrintTime < 1000) { return; } Serial.print("z=");Serial.print(x);Serial.print("; "); Serial.print("y=");Serial.print(y);Serial.print("; "); Serial.print("z=");Serial.print(z);Serial.print("; "); Serial.print("angle=");Serial.print(angle);Serial.print("; "); Serial.print("speed=");Serial.print(speed);Serial.println("; "); lastPrintTime = millis(); }
bool initializeGyroscope() { Wire.begin(); mpu.initialize(); Serial.println(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed")); devStatus = mpu.dmpInitialize(); mpu.setXGyroOffset(220); mpu.setYGyroOffset(76); mpu.setZGyroOffset(-85); mpu.setZAccelOffset(1788); if (devStatus != 0) { Serial.print(F("DMP Initialization failed (code "));Serial.println(devStatus); return false; } mpu.setDMPEnabled(true); Serial.println(F("Enabling interrupt detection (Arduino external interrupt 0)...")); attachInterrupt(0, dmpDataReady, RISING); mpuIntStatus = mpu.getIntStatus(); Serial.println(F("DMP ready! Waiting for first interrupt...")); packetSize = mpu.dmpGetFIFOPacketSize(); return true; }
void dmpDataReady() { mpuInterrupt = true; }
boolean hasFifoOverflown(int mpuIntStatus, int fifoCount) { return mpuIntStatus & 0x10 || fifoCount == 1024; }
Upload the code using the FTDI adapter to the arduino then connect the batteries.
Using the remote:
After the arduino is powered on, also power on the car. The HC-05 module should connect to the car, when that happens the car will emit a sound. If it doesn't work please check the previous step and the troubleshooting section.
If you incline the breadboard forward the car should move forward, right and the car should move right. It also performs more gradual movements like inclining a bit forward and a bit left in this case the car would go slowly to the left.
If the car goes some different way when inclining the breadboard first hold the breadboard in different directions.
How it works:
The sketch gets the gyroscope coordinates every 100 ms, makes computations and then transmit over bluetooth the car commands. First there is a "steer" method that gets called with the raw x, y and z angles. This method transforms the steering between 0 and 180 degrees and the acceleration between -90 and 90. This method calls
void moveZwheelsCar(byte angle, int speed) that converts the steering and acceleration to ZenWheels specifications and then transmits the commands using bluetooth.
The reason i've did the transformation in two steps, is reusability. if i would need to adapt this sketch to remote control some other device i would start from the base method "steer" that already maps the speed and steering to some useful values.
Step 7: Alternatives
An alternative to the "reverse engineering". I've talked about how to reverse engineer the project by starting with the Android application. But there is an alternative to this you can set up a serial FTDI + bluetooth slave (regular HC-05 without specifying the master settings). Then from the ZenWheels app connect to the HC-05 instead of the "microcar".
To decode the commands you'll need to hold the steering wheel in some position, then using a python script analyze the serial communication. I'm suggesting a python script because there are non printable characters and Arduino IDE it's not suitable for that. You'll observe that if you hold the wheel in one position the app will transmit regularly the same two bytes. If you vary the wheel position the fist byte will remain the same the second will change. After many trials you can come up with the steering algorithm, then reverse engineer throttle etc.
An alternative to the arduino based remote would be a RaspberryPi remote. The raspberry pi has an embeded bluetooth module that is painless to set up in the "master" mode and the python bluetooth library works like a charm. Also some more interesting projects are possible like controlling the car using Alexa echo :)
I hope you enjoyed the project and please leave comments below!