DEV Community

loading...
Cover image for Reverse Engineering Sphero R2D2 - I like to move it!

Reverse Engineering Sphero R2D2 - I like to move it!

astagi profile image Andrea Stagi ・6 min read

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

Watch the video

Moving Sphero R2D2 droid

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.

R2D2 Sphero App

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 |...
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

Give it a try!

convertDegreeToHex(0)
// => [0x00, 0x00]
convertDegreeToHex(180)
// => [0x00, 0xB4]
convertDegreeToHex(270)
// => [0x01, 0x0E]
convertDegreeToHex(270, CONVERSIONS.FLOAT)
// => [0x43, 0x87, 0x00, 0x00]
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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

Sphero Edu App

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 |
Enter fullscreen mode Exit fullscreen mode

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 |
Enter fullscreen mode Exit fullscreen mode

The common part of these messages is

| 0x8D | 0x00 | 0x18 | 0x02 | 0xFF |
Enter fullscreen mode Exit fullscreen mode

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 |
Enter fullscreen mode Exit fullscreen mode

XYZ

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;
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

It's time to create the main function to activate the accelerometer inspection, enableAccelerometerInspection. This function have to

  • Receive a characteristic and a callback function
  • Write the packet to activate accelerometer inspection
  • Read data and decode them (remember the schema?) Encode/decode
  • 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);
      }
    }
  });
}
Enter fullscreen mode Exit fullscreen mode
enableAccelerometerInspection(characteristic, (x, y, z) => {
  console.log('----------------------')
  console.log("X:" + x)
  console.log("Y:" + y)
  console.log("Z:" + z)
});
Enter fullscreen mode Exit fullscreen mode

Watch this video to see accelerometer in action 📺

Watch the video

Log accelerometer

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 {
Enter fullscreen mode Exit fullscreen mode

so you can receive accelerometer values listening to accelerometer event!

r2.on('accelerometer', (x, y, z) => {

});
Enter fullscreen mode Exit fullscreen mode

If you want to see other funny methods of DYALF, check the examples folder containing some useful scripts.

Cover image: artwork by Susan Murtaugh

Discussion (0)

Forem Open with the Forem app