In the first part of Reverse Engineering Sphero R2D2 I made a deep look inside Sphero documentation and used Wireshark to catch all the BLE messages between the phone and the droid, replicating them using Node.js. At the end of the first part we were able to animate the droid and rotate the top, now it's time to make our droid move in any direction and play with the accelerometer!
The final result is in this video 📺 Check the final code in this repository
R2D2 Movement
Using the Official Sphero App in "driving mode" you can find a big circle on the left with a small lighting blue point in its center.
Moving the blue point inside the big circle allows you to move R2D2 around, at a certain speed. R2D2 is also able to move forward and backward. During BLE packets analysis I expect to find packets with these information:
- The heading (from 0° to 360°)
- Direction (forward or backward)
- Speed
That's my scanning result after driving my droid around the room
...| 0x0A | 0x16 | 0x07 | 0xB0 | 0x00 | 0xB4 | 0x00 |...
...| 0x0A | 0x16 | 0x07 | 0xC2 | 0x00 | 0xB4 | 0x00 |...
...| 0x0A | 0x16 | 0x07 | 0xFF | 0x00 | 0xB4 | 0x00 |...
...
...| 0x0A | 0x16 | 0x07 | 0x32 | 0x01 | 0x0E | 0x01 |...
...| 0x0A | 0x16 | 0x07 | 0x6A | 0x01 | 0x0E | 0x01 |...
...| 0x0A | 0x16 | 0x07 | 0xA1 | 0x01 | 0x0E | 0x01 |...
As you can see, the common part of these messages is 0x0A, 0x16, 0x07 so we can define the const value
const MSG_MOVE = [0x0A, 0x16, 0x07]
The next byte contains a value between 0x00 and 0xFF, it must be the speed.
The following 2 bytes look to be the heading. I expect to find a value in degrees, so I try to convert these bytes using the IEEE-754 Floating Point Converter as we did in the previous article to move the top
0x00B4 => 2.52233723578e-43
As you can see, this is not a valid value for the heading. Let's try to convert it to a decimal value
0x00B4 => 180
Yay, 180 degrees! ✌🏻
As we can easily imagine, the last byte is the direction (0x00 => forward, 0x01 => backward).
Now before start trying to move our droid programmatically, we need a function to convert a degree value to hex. We can modify the existing convertDegreeToHex adding integer support.
const CONVERSIONS = {
INTEGER: 'i',
FLOAT: 'f',
};
let convertDegreeToHex = (degree, format = CONVERSIONS.INTEGER) => {
var view = new DataView(new ArrayBuffer(4));
format === CONVERSIONS.FLOAT ? view.setFloat32(0, degree) : view.setUint16(0, degree)
return Array
.apply(null, {
length: format === CONVERSIONS.FLOAT ? 4 : 2
})
.map((_, i) => view.getUint8(i))
}
Give it a try!
convertDegreeToHex(0)
// => [0x00, 0x00]
convertDegreeToHex(180)
// => [0x00, 0xB4]
convertDegreeToHex(270)
// => [0x01, 0x0E]
convertDegreeToHex(270, CONVERSIONS.FLOAT)
// => [0x43, 0x87, 0x00, 0x00]
Using the writePacket function we can now move our droid with our code 🎉 Let's try to draw a square!
for (let i = 0 ; i < 4 ; i++) {
await writePacket(
characteristic,
buildPacket(
MSG_MOVE,
[0xFF, ...convertDegreeToHex(i * 90), 0x00]
)
);
await new Promise(resolve => setTimeout(resolve, 2000));
}
Remember to set a timeout after sending a MSG_MOVE, these message are executed instantly! Also keep in mind that heading takes some time to execute (~450ms for 180° rotation).
Accelerometer inspection
Accelerometer inspection is the hardest part I found during reverse engineering. Using the official app to move the droid I didn't find anything related to the accelerometer (e.g. collision detection), so I tried to use another app [Sphero Edu] where events like collision detection are supported (https://play.google.com/store/apps/details?id=com.sphero.sprk&hl=en). Using this app we can create simple block scripts to play with our droid!
Let's make a simple script with collision detection enabled and log BLE communication during its execution
Inspecting Wireshark log you can see that there's a special message sent by Sphero Edu App to our droid
| 0x0A | 0x18 | 0x00 | 0x00 | 0x96 | 0x00 | 0x00 | 0x07 | 0xe0 | 0x78 |
This message activates an infinite stream of messages like these
| 0x8D | 0x00 | 0x18 | 0x02 | 0xFF | 0x41 | 0xE8 | 0xBA | 0x70 | 0x41 | 0x35 | 0xB6 | 0x97 | 0xC1 | 0xAB | 0x50 | 0xDB | ... | 0xD8 |
| 0x8D | 0x00 | 0x18 | 0x02 | 0xFF | 0x42 | 0xE2 | 0xAA | 0x60 | 0x41 | 0x35 | 0xB2 | 0x67 | 0xC1 | 0xBB | 0x20 | 0xAB | ... | 0xD8 |
The common part of these messages is
| 0x8D | 0x00 | 0x18 | 0x02 | 0xFF |
I expect to find there X, Y and Z values. At a first glance, the 12 bytes following the common part, look to be 3 IEEE754 numbers
Common part: | 0x8D | 0x00 | 0x18 | 0x02 | 0xFF |
X axis: | 0x41 | 0xE8 | 0xBA | 0x70 |
Y axis: | 0x41 | 0x35 | 0xB6 | 0x97 |
Z axis: | 0xC1 | 0xAB | 0x50 | 0xDB |
We need to modify our code before receiving these data because they may interfere with other data read operations. To avoid this problem use a function to check the "header" of the received packet (isActionResponse)
let isActionResponse = (data) => {
let valid = false;
valid |= data.slice(0, 2).every((v) => [0x8D, 0x09].indexOf(v) >= 0);
valid |= data.slice(0, 2).every((v) => [0x8D, 0x08].indexOf(v) >= 0);
valid |= data.slice(0, 3).every((v) => [0x8D, 0x00, 0x17].indexOf(v) >= 0);
return valid;
}
And add this code before data validation on writePacket
let listenerForRead = (data) => {
// ...
if (eopPosition !== -1) {
// Check if Package is for me
if (isActionResponse(dataToCheck)) {
// Process data
}
}
};
It's time to create the main function to activate the accelerometer inspection, enableAccelerometerInspection. This function have to
- Receive a
characteristicand acallback function - Write the packet to activate accelerometer inspection
- Read data and decode them (remember the schema?)
- Convert X, Y and Z values and send them to the callback
const MSG_ACCELEROMETER = [0x0A, 0x18, 0x00];
let enableAccelerometerInspection = (characteristic, callback) => {
let dataRead = [];
let dataToCheck = [];
let eopPosition = -1;
characteristic.write(Buffer.from(buildPacket(MSG_ACCELEROMETER, [0x00, 0x96, 0x00, 0x00, 0x07, 0xe0, 0x78])));
characteristic.on('data', (data) => {
dataRead.push(...data);
eopPosition = dataRead.indexOf(EOP);
dataToCheck = dataRead.slice(0);
if (eopPosition !== dataRead.length - 1) {
dataRead = dataRead.slice(eopPosition + 1);
} else {
dataRead = [];
}
if (eopPosition !== -1) {
if (dataToCheck.slice(0, 5).every((v) => [0x8D, 0x00, 0x18, 0x02, 0xFF].indexOf(v) >= 0)) {
// Decode packet
let packetDecoded = [];
for (let i = 0; i < dataToCheck.length - 1; i++) {
if (dataToCheck[i] == ESC && dataToCheck[i + 1] == ESC_ESC) {
packetDecoded.push(ESC);
i++;
} else if (dataToCheck[i] == ESC && dataToCheck[i + 1] == ESC_SOP) {
packetDecoded.push(SOP);
i++;
} else if (dataToCheck[i] == ESC && dataToCheck[i + 1] == ESC_EOP) {
packetDecoded.push(EOP);
i++;
} else {
packetDecoded.push(dataToCheck[i])
}
}
let x = Buffer.from(packetDecoded.slice(5, 9)).readFloatBE(0);
let y = Buffer.from(packetDecoded.slice(9, 13)).readFloatBE(0);
let z = Buffer.from(packetDecoded.slice(13, 17)).readFloatBE(0);
callback(x, y, z);
}
}
});
}
enableAccelerometerInspection(characteristic, (x, y, z) => {
console.log('----------------------')
console.log("X:" + x)
console.log("Y:" + y)
console.log("Z:" + z)
});
Watch this video to see accelerometer in action 📺
Every second the callback gets called ~ 7 times. With these values you can program incline detection, check if your droid fall on the ground, write a simple collision detection and so on!
DYALF
It's time to wrap all that we learned during this reverse engineering process in a library to take advantage of OOP and write a better and more reusable code. For this purpose I created the library DYALF (Droids You Are Looking For) containing all the methods to play with R2D2. You can check the code on Github. With DYALF you can write code like this
const dyalf = require('./dyalf');
let main = async () => {
let r2 = new dyalf.R2D2('4bef2b0786334e2fac126c55f7f2d057');
await r2.connect();
await r2.openCarriage();
await r2.sleep(1000);
await r2.animate(7);
for (var i = -160; i < 180; i += 5) {
await r2.rotateTop(i);
}
await r2.off();
dyalf.shutdown();
};
main();
And is made to support other droids extending the base class Droid (BB8 droid support will be ready soon!).
Using the movement is really simple and readable, rewriting the square drawing function with DYALF will look like
console.log('Make a square 🔳');
for (let i = 0; i < 4; i++) {
await r2.move(0xFF, i * 90, 3000);
}
await r2.stop();
DYALF adds the time parameter to move your droid in a specific direction for N milliseconds.
To get accelerometer values we can simply listen to an event! The base class Droid extends EventEmitter to support events
const EventEmitter = require('events');
class Droid extends EventEmitter {
so you can receive accelerometer values listening to accelerometer event!
r2.on('accelerometer', (x, y, z) => {
});
If you want to see other funny methods of DYALF, check the examples folder containing some useful scripts.
Cover image: artwork by Susan Murtaugh




Top comments (1)
I'm trying to revive an original Sphero R2-D2 that has been unused for approximately 5+ years.
I'm looking for someone familiar with the R2-D2/Sphero V2 BLE protocol who can help determine whether this is a firmware/application problem or whether I'm missing an initialization/recovery step.
ROBOT
Name: D2-C5F1
BLE address: D7:86:AC:82:C5:F1
The robot is consistently discoverable and accepts BLE connections.
CURRENT GATT SERVICES
The robot exposes:
00001800-0000-1000-8000-00805f9b34fb
00001801-0000-1000-8000-00805f9b34fb
00020001-574f-4f20-5370-6865726f2121
0000180f-0000-1000-8000-00805f9b34fb
The 00020001 service contains:
00020002 — notify + write
00020003 — write-without-response
00020004 — read
00020005 — write + write-without-response
The following expected V2 API services are completely absent:
00010001-574f-4f20-5370-6865726f2121
00010002-574f-4f20-5370-6865726f2121
00010003-574f-4f20-5370-6865726f2121
This is the main problem.
00020004 READ
Reading 00020004 consistently returns:
12 00 01 00 04 00 02 01
Length: 8 bytes.
FIRMWARE SURVEY
I ran firmware_survey.py from the ccb/sphero-r2d2 project.
The robot is correctly identified as R2D2, but all of the following queries fail:
main_app_version: ERROR
bootloader_version: ERROR
board_revision: ERROR
processor_name: ERROR
secondary_main_app_version: ERROR
secondary_bootloader_version: ERROR
battery_voltage: ERROR
battery_state: ERROR
head_position: ERROR
current_leg_action: ERROR
leg_position: ERROR
The survey does successfully identify:
toy_type: R2D2
led_count: 8
sound_count: 29
animation_count: 18
ANTI-DOS HANDSHAKE
I sent the documented R2-D2 handshake:
usetheforce...band
to:
00020005-574f-4f20-5370-6865726f2121
The write succeeds.
I then disconnected completely, waited, reconnected, and performed a fresh GATT discovery.
The 000100xx services STILL did not appear:
00010001: FALSE
00010002: FALSE
00010003: FALSE
I also performed a handshake-only test with no other commands whatsoever. Same result.
So the handshake does not appear to be the missing step.
NORDIC DFU CHECK
I performed a complete GATT enumeration specifically looking for standard Nordic DFU services.
Legacy Nordic DFU:
00001530-1212-efde-1523-785feabcd123
NOT PRESENT
Secure DFU:
0000fe59-0000-1000-8000-00805f9b34fb
NOT PRESENT
The corresponding legacy DFU characteristics are also absent.
Therefore the robot does not appear to be advertising a standard Nordic DFU service.
FIRMWARE COMMAND TEST
I attempted the R2 firmware-management queries from the ccb/sphero-r2d2 implementation.
DID 29:
CID 13 — get_pending_update_flags
CID 21 — get_current_application_id
CID 22 — get_all_updatable_processors
CID 24 — get_version_for_updatable_processors
CID 27 — get_pending_update_for_processors
Initially there was a packet-framing error in my test: the EOP byte was missing.
Those tests were repeated correctly with the Sphero V2 EOP byte 0xD8.
The packets were:
8D 0A 1D 0D 00 CB D8
8D 0A 1D 15 01 C2 D8
8D 0A 1D 16 02 C0 D8
8D 0A 1D 18 03 BD D8
8D 0A 1D 1B 04 B9 D8
Before sending them, I explicitly verified the CCCD on 00020002.
CCCD:
00002902-0000-1000-8000-00805f9b34fb
Before enabling notifications:
00 00
After start_notify():
01 00
So notifications were definitely enabled.
All five correctly framed packets were successfully written to 00020003.
No notification/response was received.
After the fifth query, the BLE connection was lost.
IMPORTANT CAVEAT:
I don't know whether 00020003 is actually the correct transport for DID-29 commands when the normal 00010002 API is absent. The upstream ccb/sphero-r2d2 implementation normally uses 00010002 for V2 commands, and my R2 does not expose that characteristic.
MSG_INIT
I have NOT successfully performed the real MSG_INIT test.
The documented/captured packet is:
8D 0A 13 0D 00 D5 D8
DID 19 / CID 13.
However, this command is supposed to be sent through the normal 00010002 API characteristic.
Since my R2 does not expose 00010002, I have deliberately NOT treated sending it to 00020003 as a valid MSG_INIT test.
OTHER OBSERVATIONS
CURRENT THEORY
The robot appears to consistently stop at the Sphero-specific 00020001 initialization layer:
BLE
|
+-- 00020001
|
+-- 00020002
+-- 00020003
+-- 00020004
+-- 00020005
but never reaches:
00010001
|
+-- 00010002
+-- 00010003
I do NOT know whether this means:
I don't want to send bootloader/reflash commands blindly.
QUESTIONS
Does anyone recognize this exact GATT state on an R2-D2?
Specifically:
I also have another original R2-D2 arriving in a few days. I plan to perform a completely passive GATT comparison on that unit before doing anything to it, which should give me a known-good reference.
I'm particularly interested in hearing from anyone who has worked with the ccb/sphero-r2d2 project, reverse-engineered the R2-D2 BLE protocol, or has successfully recovered one of these after sitting unused for years.
I can provide the complete Python diagnostic scripts and raw output if useful.