โปรเจกต์ Hacking Bluetooth Toy Car
เรากำลังเรียนรู้วิธีการ "hack" Bluetooth Toy Car โดยการสร้าง Custom Remote สำหรับควบคุมมัน โดยใช้ Arduino และ Gyroscope
เรากำลังเรียนรู้วิธีการ "hack" Bluetooth Toy Car โดยการสร้าง Custom Remote สำหรับควบคุมมัน โดยใช้ Arduino และ Gyroscope
▶ กดเพื่อดูวิดีโอสาธิตโปรเจกต์
ในบทช่วยสอนนี้ เราจะสร้างรีโมทคอนโทรลแบบกำหนดเองสำหรับ ZenWheels microcar โดย ZenWheels microcar เป็นรถของเล่นขนาด 5 ซม. ที่สามารถควบคุมผ่านแอปพลิเคชัน Android หรือ Iphone ผมจะแสดงให้คุณเห็นถึงวิธีการทำ Reverse Engineering ของแอปพลิเคชัน Android เพื่อค้นหาโปรโตคอลการสื่อสาร และวิธีที่คุณสามารถสร้างรีโมทคอนโทรลโดยใช้ Arduino และ gyroscope
Parts:
1. รถ ZenWheels microcar
2. Arduino pro mini 328p
3. Breadboard
4. MPU6050 gyroscope
5. แหล่งจ่ายไฟ <=5 v (แบตเตอรี่บางชนิดที่สามารถต่อเข้ากับ Breadboard ได้)
6. สาย Jumper แบบ U-shape (ถ้ามี) ผมใช้สาย Jumper เหล่านี้เพราะมันดูดีกว่าบน Breadboard แต่คุณสามารถใช้สาย Jumper ทั่วไปแทนได้
7. HC-05 bluetooth module (แบบที่มีปุ่มสำหรับเข้าสู่ AT mode)
Tools:
1. ตัวแปลง USB to serial FTDI adapter FT232RL เพื่อใช้โปรแกรม Arduino pro mini
2. Arduino IDE
3. โทรศัพท์ Android
4. Android Studio [Optional]
จำเป็นต้องมีความรู้เกี่ยวกับ Java และ Android บ้างเพื่อทำความเข้าใจในส่วนนี้
เป้าหมายของโปรเจกต์คือการควบคุม microcar โดยใช้ gyroscope สำหรับสิ่งนี้เราจำเป็นต้องค้นหาข้อมูลเพิ่มเติมเกี่ยวกับการสื่อสาร bluetooth ระหว่างของเล่นชิ้นนี้กับแอป Android
ในขั้นตอนนี้ผมจะอธิบายวิธีการทำ Reverse Engineering โปรโตคอลการสื่อสารระหว่าง microcar และแอป Android หากคุณเพียงต้องการสร้างรีโมท ขั้นตอนนี้ไม่จำเป็น วิธีหนึ่งในการค้นหาโปรโตคอลคือการดูที่ Source Code แต่มันไม่ง่ายนัก เพราะแอปพลิเคชัน Android ถูกคอมไพล์มาแล้วและเราติดตั้งไฟล์ APK ผ่าน Google Play
ดังนั้นผมจึงทำคู่มือพื้นฐานสำหรับการทำสิ่งนี้:
1. Download the APK. Android Package Kit (เรียกสั้นๆ ว่า APK) คือรูปแบบไฟล์แพ็กเกจที่ใช้โดยระบบปฏิบัติการ Android สำหรับการแจกจ่ายและติดตั้งแอปมือถือ
ขั้นแรกให้ค้นหาแอปพลิเคชันบน Google play store ในกรณีของเราให้ค้นหาคำว่า "zenwheels" และคุณจะได้ link ของแอปพลิเคชัน
จากนั้นค้นหาใน Google สำหรับคำว่า "online apk downloader" และใช้เครื่องมือเพื่อดาวน์โหลด APK โดยปกติเครื่องมือจะถามหาลิงก์ของแอปพลิเคชัน (อันที่เราได้มาก่อนหน้านี้) จากนั้นเราจะกดปุ่มดาวน์โหลดและบันทึกลงในคอมพิวเตอร์ของเรา
2. Decompile the APK. Decompiler ในสถานการณ์ของเราคือเครื่องมือที่รับไฟล์ APK และสร้าง Java source code ออกมา
วิธีที่ง่ายที่สุดคือการใช้ Online Decompiler ในการทำงาน ผมค้นหา Google สำหรับ "online decompliler" และเลือก http://www.javadecompilers.com/ คุณเพียงแค่ต้องอัปโหลด APK ที่คุณได้รับมาก่อนหน้านี้และ
กดปุ่ม Decompile จากนั้นคุณก็แค่ดาวน์โหลด Source Code มา
3. Try to reverse engineer looking through the code
ในการเปิดโปรเจกต์คุณต้องมี Text Editor หรือ IDE (Integrated Development Environment) ที่ดีกว่านั้น IDE มาตรฐานสำหรับโปรเจกต์ Android คือ Android Studio (https://developer.android.com/studio) หลังจากที่คุณติดตั้ง Android Studio แล้ว ให้เปิดโฟลเดอร์โปรเจกต์
เนื่องจากรถของเราถูกควบคุมด้วย bluetooth ผมจึงเริ่มการค้นหาในโค้ดที่ถูก Decompile ด้วยคีย์เวิร์ด "bluetooth" จากผลที่พบ ผมเจอ "BluetoothSerialService" ซึ่งทำหน้าที่จัดการการสื่อสาร ถ้าคลาสนี้จัดการการสื่อสาร มันก็ต้องมี Method สำหรับการส่งคำสั่ง ปรากฏว่ามี Method สำหรับเขียนข้อมูลหนึ่งตัวที่ส่งข้อมูลผ่านช่องทาง bluetooth:
public void write(byte[] out)นี่เป็นจุดเริ่มต้นที่ดี ผมค้นหาการใช้งาน Method write( และพบคลาส "ZenWheelsMicrocar" ที่สืบทอดมาจาก "BluetoothSerialService" คลาสนี้รวบรวม Logic ส่วนใหญ่ของการสื่อสารผ่าน bluetooth ส่วน Logic อื่นๆ จะอยู่ใน Controller: BaseController และ StandardController
ใน BaseController เราจะพบการกำหนดค่าเริ่มต้นของ Service รวมถึงการกำหนดช่องทางสำหรับ steering (การเลี้ยว) และ throttle (การเร่ง) ซึ่งจริงๆ แล้วช่องทางเหล่านี้คือ Command Prefix เพื่อระบุประเภทของคำสั่งที่จะตามมา:
protected ZenWheelsMicrocar microcar = new ZenWheelsMicrocar(this, this.btHandler);
protected ChannelOutput[] outputs = {new TrimChannelOutput(ZenWheelsMicrocar.STEERING_CHANNEL), new TrimChannelOutput(ZenWheelsMicrocar.THROTTLE_CHANNEL)};ใน StandardController การเลี้ยวจะถูกจัดการใน:
public void handleSteering(TouchEvent touchEvent) {
...
this.microcar.setChannel(steeringOutput.channel, steeringOutput.resolveValue());
}จากการวิเคราะห์ Method นี้ steeringOutput.channel มีค่าเท่ากับ 129 (ช่องทางที่ใช้สำหรับการเลี้ยว) และ steeringOutput.resolveValue() อาจมีค่าระหว่าง -90 ถึง 90 ค่า Channel (129) จะถูกส่งไปโดยตรง และค่าการเลี้ยวจะถูกปรับเปลี่ยนโดยใช้การดำเนินการ Bitwise:
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;
}และยังมี Method ที่คล้ายกันใน StandardController ชื่อว่า
public void handleThrottle(TouchEvent touchEvent)Parts:
1. Arduino pro mini 328p ราคาประมาณ 2 $
2. Breadboard
3. MPU6050 gyroscope ราคาประมาณ 1.2$
4. HC-05 master-slave 6 pin module ราคาประมาณ 3$
5. รางถ่าน AA 4 ก้อน พร้อมถ่าน 4 ก้อน
6. สาย Jumper แบบ U-shape (ถ้ามี) ผมใช้สาย Jumper เหล่านี้เพราะมันดูดีกว่าบน Breadboard และทำให้มองเห็นไฟ Leds ได้ชัดเจนกว่า หากคุณไม่มีสายเหล่านี้สามารถใช้สาย Dupont แทนได้
ราคาด้านบนอ้างอิงจาก eBay
Tools:
1. ตัวแปลง USB to serial FTDI adapter FT232RL เพื่อใช้โปรแกรม Arduino pro mini
2. Arduino IDE
3. Android Studio (ถ้าต้องการทำ Reverse Engineering ด้วยตัวเอง)

การประกอบนั้นง่ายมากเพราะเราทำบน Breadboard :)
โปรดตรวจสอบรูปภาพด้านบนสำหรับรายละเอียดเพิ่มเติม



สำหรับการขั้นตอนนี้ คุณต้องมีโทรศัพท์ Android, Bluetooth HC-05 Module และ Serial FTDI Adapter พร้อมสายไฟ นอกจากนี้เราจะใช้ Arduino IDE เพื่อสื่อสารกับ Bluetooth Module
อันดับแรก เราต้องค้นหา bluetooth address ของ microcar ก่อน:
หลังจากติดตั้งแอปนี้แล้ว ให้ไปที่ Menu -> Devices คุณจะเห็นรายการอุปกรณ์ Bluetooth ที่ Pair ไว้ทั้งหมด เราสนใจเฉพาะโค้ดที่อยู่ใต้ชื่อ "Microcar" ของผมคือ 00:06:66:49:A0:4B
ถัดมา ให้เชื่อมต่อตัวแปลง FTDI เข้ากับ Bluetooth Module เริ่มจาก Pins VCC และ GROUND จากนั้นต่อ FTDI RX เข้ากับ Bluetooth TX และ FTDI TX เข้ากับ Bluetooth RX นอกจากนี้ควรมี Pin บน Bluetooth Module ที่ต้องเชื่อมต่อกับ VCC การทำเช่นนี้จะทำให้ Bluetooth Module เข้าสู่ "programmable mode" โมดูลของผมมีปุ่มที่เชื่อมต่อ VCC เข้ากับ Pin พิเศษนั้น เมื่อคุณเสียบ FTDI เข้ากับ USB คุณควรเสียบโดยที่ Pin นั้นเชื่อมต่ออยู่หรือกดปุ่มค้างไว้เพื่อเข้าสู่โหมดตั้งโปรแกรมพิเศษนี้ Bluetooth จะยืนยันการเข้าสู่โหมดนี้โดยการกะพริบช้าๆ ทุกๆ 2 วินาที
ใน Arduino IDE ให้เลือก Serial Port จากนั้นเปิด Serial Monitor (เลือกทั้ง NL และ CR ด้วย Baud Rate 9600) พิมพ์ AT และโมดูลควรตอบกลับว่า "OK"
พิมพ์ "AT+ROLE=1" เพื่อตั้งค่าโมดูลให้อยู่ใน Master Mode สำหรับการ Pair กับ Bluetooth Module ของคุณ ให้เขียน: "AT+BIND=0006, 66, 49A04B" สังเกตว่าค่า "00:06:66:49:A0:4B" ของเราถูกเปลี่ยนเป็น "0006, 66, 49A04B" คุณควรทำการแปลงค่า Bluetooth MAC ของคุณในลักษณะเดียวกัน
ตอนนี้ให้เปิดสวิตช์ รถ ZenWheels จากนั้นถอด FTDI ออกแล้วเสียบใหม่อีกครั้งโดยไม่ต้องกดปุ่มหรือต่อ Pin พิเศษ หลังจากนั้นครู่หนึ่งมันควรจะเชื่อมต่อกับรถ และคุณจะสังเกตเห็นว่ารถส่งเสียงเตือนเมื่อเชื่อมต่อสำเร็จ
การแก้ไขปัญหา (Troubleshooting):
ก่อนอื่น คุณต้องดาวน์โหลดและติดตั้ง Library สองตัวนี้:
1. MPU6050 Library สำหรับ gyroscope
2. I2CDev Library source
จากนั้น ดาวน์โหลดและติดตั้ง Library ของผมจาก ที่นี่ หรือคัดลอกได้จากด้านล่าง:
/** * 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 highunsigned 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;
}อัปโหลด Code โดยใช้ตัวแปลง FTDI ไปยัง Arduino จากนั้นเชื่อมต่อแบตเตอรี่
การใช้รีโมท:
หลังจากเปิดสวิตช์ Arduino แล้ว ให้เปิดสวิตช์ที่รถด้วย HC-05 Module ควรจะเชื่อมต่อกับรถ เมื่อเชื่อมต่อแล้วรถจะส่งเสียงออกมา หากใช้งานไม่ได้โปรดตรวจสอบขั้นตอนก่อนหน้าและส่วนการแก้ไขปัญหา
หากคุณเอียง Breadboard ไปข้างหน้า รถควรจะเคลื่อนที่ไปข้างหน้า เอียงไปทางขวารถควรจะเลี้ยวขวา นอกจากนี้ยังสามารถควบคุมการเคลื่อนที่แบบละเอียดได้ เช่น เอียงไปข้างหน้าเล็กน้อยพร้อมกับเอียงไปทางซ้ายเล็กน้อย ในกรณีนี้รถจะค่อยๆ เคลื่อนที่ไปทางซ้ายช้าๆ
ถ้ารถเคลื่อนที่ไปในทิศทางที่ต่างออกไปเมื่อคุณเอียง Breadboard ให้ลองถือ Breadboard ในทิศทางต่างๆ ก่อน
วิธีการทำงาน:
Sketch นี้จะอ่านค่าพิกัดจาก Gyroscope ทุกๆ 100 ms ทำการคำนวณ แล้วส่งคำสั่งควบคุมรถผ่าน bluetooth ขั้นแรกจะมี Method "steer" ที่ถูกเรียกใช้พร้อมกับมุมดิบ x, y และ z Method นี้จะแปลงการเลี้ยวให้อยู่ระหว่าง 0 ถึง 180 องศา และการเร่งให้อยู่ระหว่าง -90 ถึง 90 จากนั้นจะเรียกใช้
void moveZwheelsCar(byte angle, int speed) เพื่อแปลงค่าการเลี้ยวและการเร่งให้เป็นไปตามข้อกำหนดของ ZenWheels และส่งคำสั่งผ่าน bluetooth
เหตุผลที่ผมทำการแปลงค่าเป็นสองขั้นตอนคือเพื่อให้สามารถนำ Code ไปใช้ซ้ำได้ หากผมต้องการปรับ Sketch นี้เพื่อใช้ควบคุมอุปกรณ์อื่นๆ ผมสามารถเริ่มจาก Method พื้นฐานอย่าง "steer" ที่ทำการ Map ค่าความเร็วและการเลี้ยวให้เป็นค่าที่ใช้งานได้อยู่แล้ว
ทางเลือกแทนการ "Reverse Engineering" ผมได้พูดถึงวิธีการทำ Reverse Engineering โปรเจกต์โดยเริ่มจากแอปพลิเคชัน Android แต่ยังมีอีกทางเลือกหนึ่งคือคุณสามารถตั้งค่า FTDI + Bluetooth Slave (ใช้ HC-05 ปกติโดยไม่ต้องตั้งค่า Master) จากนั้นเชื่อมต่อแอป ZenWheels เข้ากับ HC-05 แทนที่จะเชื่อมต่อกับ "microcar"
ในการถอดรหัสคำสั่ง คุณต้องถือพวงมาลัยในตำแหน่งใดตำแหน่งหนึ่งค้างไว้ จากนั้นใช้สคริปต์ Python วิเคราะห์การสื่อสารผ่าน Serial ผมแนะนำสคริปต์ Python เพราะมีตัวอักษรที่พิมพ์ออกมาไม่ได้ (Non-printable characters) ซึ่ง Arduino IDE ไม่เหมาะกับงานนี้ คุณจะพบว่าถ้าคุณถือพวงมาลัยค้างไว้ในตำแหน่งหนึ่ง แอปจะส่ง Byte เดิมสองตัวเป็นระยะๆ หากคุณเปลี่ยนตำแหน่งพวงมาลัย Byte แรกจะยังคงเดิมแต่ Byte ที่สองจะเปลี่ยนไป หลังจากทดลองหลายครั้งคุณจะสรุปอัลกอริทึมการเลี้ยวได้ จากนั้นก็ทำแบบเดียวกันกับการเร่งความเร็ว ฯลฯ
อีกทางเลือกสำหรับ รีโมทที่ใช้ Arduino คือการใช้ RaspberryPi รีโมท เนื่องจาก RaspberryPi มี Bluetooth Module ในตัวซึ่งตั้งค่าในโหมด "Master" ได้ง่ายมาก และ python bluetooth library ก็ทำงานได้ยอดเยี่ยม นอกจากนี้ยังเป็นไปได้ที่จะทำโปรเจกต์ที่น่าสนใจกว่าเดิม เช่น การควบคุมรถโดยใช้ Alexa echo :)
ผมหวังว่าคุณจะสนุกกับโปรเจกต์นี้ และโปรดแสดงความคิดเห็นไว้ด้านล่างได้เลยครับ!
สนับสนุนเพื่อรับ Source Code หรือแอปพลิเคชันสำหรับโปรเจกต์นี้