DEV Community

loading...
Cover image for Reverse Engineering Sphero R2D2 with Javascript

Reverse Engineering Sphero R2D2 with Javascript

astagi profile image Andrea Stagi Updated on ・9 min read

I bought my Sphero R2D2 two years ago, it was a cool toy for a Star Wars fans like me and a great pal for my cat.. anyway after some time I started thinking to make some programming project with this beautiful cluster of electronics! I wanted to find a way to script my robot but I didn’t found anything well documented and maintained.

The only thing I knew about R2D2 is that it works using BLE technology and you can drive it using the official Sphero Droids app (link). I found only this article, a good starting point and on Sphero’s website there’s some documentation about protocol communication but it wasn’t enough, the article and the script attached looked unfinished and documentation had no specification about the messages that make R2D2 moving and dancing.

That’s why I decided to write some Javascript code to discover how to communicate with R2D2! In this article I’ll show you my personal experience on reverse engineering this droid nut you can apply this approach to any BLE device you want to hack.

TL;DR

You can jump to this repository and use the code to communicate with your R2D2. The final result is in this video 📺

Watch the video

Scripting my Sphero R2D2 droid

Setup

For this experiment is necessary:

  • Basic knowledge of BLE protocol (a tutorial for beginners)
  • A computer with BLE support (I’m using a MacBook Pro)
  • An Android phone (I’m using an old Motorola with an Android 6)
  • A Sphero R2D2 droid! (Amazon 📦)

The first thing to do is installing Wireshark and Android Developer tools on the PC:

  • Wireshark is a network protocol analyzer useful for inspecting Bluetooth messages and can be downloaded from the official site
  • Android Developer tools contain adb executable to communicate with your Android phone from the PC, visit the official site for more informations.

On the Android phone install Sphero Droids app and enable Bluetooth HCI Spoofing feature under Developer options.

Using this feature, I’m able to obtain a file with all Bluetooth communication packets sent and received between devices. 

Capturing data

Now, with BLE HCI Spoofing enabled, open the Sphero Droids app, connect R2D2 and play with it for some time. 

After that, close the app and download the file generated on your disk using adb.

adb pull /sdcard/btsnoop_hci.log /dest/path
Enter fullscreen mode Exit fullscreen mode

This file is generally saved under /sdcard/btsnoop_hci.log and can be opened with Wireshark.

Wireshark inspection

This is the most interesting part of the project: opening the file with Wireshark reveals a lot of useful information for reverse engineering the droid. This is what I got after my first session: there are a lot of information request packets sent between the Android device (localhost) and the droid (mine is labeled with the address d7:1b:52:17:7b:d6) and, after some scrolling, there’s the first write request!

The “usetheforce. ..band” message

As you can see in the bytes inspector the payload is quite eloquent: “usetheforce. ..band”. Sounds good :)

Another useful information is Service UUID and Characteristic UUID (handle 0x0015), annotate them to know where to send “usetheforce. ..band” message!

Now it’s time to read some documentation, starting from the Packet structure. This is the schema of a packet in Sphero’s protocol:

Every packet has a SOP (Start of packet) byte and an EOP (End of packet) byte, both equal to 0x8D and 0xD8, so it’s necessary to search for all those packets starting with SOP and ending with EOP.

Other interesting bytes are:

SEQ (Sequence Number): The token used to link commands with responses

DATA (Message Data): Zero or more bytes of payload data

CHK (Checksum): The sum of all bytes (excluding SOP and EOP) mod 256, bit-inverted

The first packet sent from the app is this:

| 0x8D | 0x0A | 0x13 | 0x0D | 0x00 | 0xD5 | 0xD8 |
Enter fullscreen mode Exit fullscreen mode

The SEQ byte here is 0x00 according to the packet structure schema: this is the first packet the app sends to the droid! Let’s call it the Init packet

The first packet: Init packet

As you can see, there’s another Service UUID and another Characteristic UUID (handle 0x001c) that will receive the next messages.

Another useful message to get is the last one at the end of the log file, sent from the app before closing, the packet to turn off the droid:

| 0x8D | 0x0A | 0x13 | 0x01 | 0x20 | 0xC1 | 0xD8 |
Enter fullscreen mode Exit fullscreen mode

The Turn off packet

It’s time to annotate services, characteristics and messages (without SOP, EOP and other bytes) in some constants.

const CONNECT_SERVICE = "00020001574f4f2053706865726f2121";
const CONNECT_CHAR = "00020005574f4f2053706865726f2121";

const MAIN_SERVICE = "00010001574f4f2053706865726f2121";
const MAIN_CHAR = "00010002574f4f2053706865726f2121";

const MSG_CONNECTION = [0x75,0x73,0x65,0x74,0x68,0x65,0x66,0x6F,0x72,0x63,0x65,0x2E,0x2E,0x2E,0x62,0x61,0x6E,0x64];
const MSG_INIT = [0x0A,0x13,0x0D];
const MSG_OFF = [0x0A,0x13,0x01];
Enter fullscreen mode Exit fullscreen mode

Let’s write some code

The final script will be composed by:

  • a function to build a packet
  • a function to connect R2D2 droid
  • a functionto write packets and wait for a response
  • a function to turn off the droid

Building a packet

Building a packet is very straightforward because it’s just an array of bytes, starting with a SOP byte and ending with an EOP byte. There are two bytes that must be generated at runtime:

  • SEQ byte: it’s just a variable initialized to 0x00 and incremented by 1 everytime a packet is built.
  • CHK byte: according to the documentation, CHK byte is the sum of all bytes (excluding SOP & EOP) mod 256, bit-inverted, so it’s really easy to generate.
let calculateChk = (buff) => {
  let ret = 0x00;
  for (let i = 0 ; i < buff.length ; i++) {
    ret += buff[i];
  }
  ret = ret & 255;
  return (ret ^ 255);
}
Enter fullscreen mode Exit fullscreen mode

There are other special bytes used in communication beyond SOP and EOP:

When the ESC, SOP, or EOP bytes are needed in the payload, they are encoded into two-byte escape sequences as follows:

This is the final code to build a valid packet for R2D2:

const ESC = 0xAB;
const SOP = 0x8D;
const EOP = 0xD8;
const ESC_ESC = 0x23;
const ESC_SOP = 0x05;
const ESC_EOP = 0x50;

let seq = 0;

let buildPacket = (init, payload=[]) => {
  let packet = [SOP];
  let body = [];
  let packetEncoded = [];

  body.push(...init);
  body.push(seq);
  body.push(...payload);

  body.push(calculateChk(body));

  for (let i = 0 ; i < body.length ; i++) {
    if (body[i] == ESC) {
      packetEncoded.push(...[ESC, ESC_ESC]);
    }
    else if (body[i] == SOP) {
      packetEncoded.push(...[ESC, ESC_SOP]);
    }
    else if (body[i] == EOP) {
      packetEncoded.push(...[ESC, ESC_EOP]);
    }
    else {
      packetEncoded.push(body[i])
    }
  }

  packet.push(...packetEncoded);
  packet.push(EOP);
  seq++;

  return packet;
}
Enter fullscreen mode Exit fullscreen mode

Connect our droid

In this example to connect R2D2 with the PC using BLE technology I use Noble library. I installed two special forks to make Noble and node-xpc-connection working on MacOS Catalina (for more info have a quick glance at the README)

npm install git://github.com/taoyuan/node-xpc-connection.git
npm install git://github.com/lzever/noble.git
Enter fullscreen mode Exit fullscreen mode

With Noble is really easy implementing a function to get the main characteristic used to communicate with the droid.

const noble = require('noble');

let connectTheDroid = (address) => {
  return new Promise((resolve, reject) => {
    noble.on('discover', (peripheral) => {
      if (peripheral.address === address) {
        noble.stopScanning();
        peripheral.connect((e) => {
          peripheral.discoverServices([CONNECT_SERVICE], (error, services) => {
            services[0].discoverCharacteristics([HANDLE_CHAR], (error, characteristics) => {
              characteristics[0].notify(true);
              characteristics[0].subscribe(async (error) => {

              });
              services[0].discoverCharacteristics([CONNECT_CHAR], (error, characteristics) => {
                characteristics[0].write(Buffer.from(MSG_CONNECTION), true, (error) => {
                  peripheral.discoverServices([MAIN_SERVICE], (error, services) => {
                    services[0].discoverCharacteristics([MAIN_CHAR], (error, characteristics) => {
                      resolve(characteristics[0]);
                    });
                  });
                });
              });
            });
          });
        });
      }
    });

    noble.on('stateChange', (state) => {
      if (state === 'poweredOn') {
        noble.startScanning();
      } else {
        noble.stopScanning();
      }
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

This script starts scanning all the devices around and select the device with the specific address provided, gets the connection service and sends “usetheforce. ..band” (MSG_CONNECTION) message to its characteristic (CONNECT_CHAR). After that, it’s time to get the “Main characteristic” to send commands to the droid! To do that, it’s better creating some code for writing and reading because I need to wait for some responses.

Write packets and read responses

This is the core part of the experiment: create a function to write commands and… read the response! When the app sends a message to the droid, it receives one or more response packets, as you can see from logs and/or read from the documentation:

Responses echo the DID, CID, and SEQ to help the sender fully identify the corresponding command packet.

Inspecting the Wireshark log you can see that there are some commands that receive another response after the echo response and other commands that require a timeout (e.g. the bipod/tripod transformation). 

To satisfy all these cases, the final write function have to work in this way:

  • Receives the characteristic, the command, a boolean to specify if it receives another response beyond the echo and a timeout
  • Sends the command to the characteristic
  • Waits for the response, check if there’s some errors and then resolve a promise (after some time if timeout is greater than 0)

To enable ‘data’ receive handler, the function needs to subscribe to the main characteristic and read from it. The data packet has the same structure of a packet used to send commands, but now we have to check if there are some errors in the Error byte.

let writePacket = (characteristic, buff, waitForNotification = false, timeout = 0) => {
  return new Promise(function (resolve, reject) {

    let dataRead = [];
    let dataToCheck = [];
    let eopPosition = -1;

    let checkIsAValidRequest = (dataRead) => {
      if (dataRead[5] != 0x00) {
        characteristic.removeListener('data', listenerForRead);
        reject(dataRead[5]);
      }
    }

    let finish = () => {
      dataRead = [];
      setTimeout(() => {
        characteristic.removeListener('data', listenerForRead);
        resolve(true);
      }, timeout);
    }

    let listenerForRead = (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 (waitForNotification) {
          if (dataToCheck[1] % 2 == 0) {
            finish();
          } else {
            checkIsAValidRequest(dataToCheck);
          }
        } else {
          checkIsAValidRequest(dataToCheck);
          finish();
        }
      }
    };
    characteristic.on('data', listenerForRead);
    characteristic.write(Buffer.from(buff));
  });
}
Enter fullscreen mode Exit fullscreen mode

Supported types for payload data

Following the same process, I tried to know how to rotate the top. There are a lot of messages of this type to make the top rotating

90° top rotation message

I tried to rotate the top to ~90° and I got 32 bit of payload with no value representing a number near to 90. That’s not completely true: “90” may be not represented as an integer! Following the documentation there are other types supported for the payload data

32 bits payload 0x42b23198 is very similar to a number encoded using IEEE754! Converting this value with an online tool I get 89.09686. 

This is the final code to rotate R2D2 top:

const MSG_ROTATE = [0x0A,0x17,0x0F];


let convertDegreeToHex = (degree) => {
  var view = new DataView(new ArrayBuffer(4));
  view.setFloat32(0, degree);
  return Array
    .apply(null, { length: 4 })
    .map((_, i) => view.getUint8(i))
}


let droidAddress = 'd7:1b:52:17:7b:d6';


connectTheDroid(droidAddress).then(characteristic => {
  characteristic.subscribe(async(error) => {
    if (error) {
      console.error('Error subscribing to char.');
    } else {
      console.log("Wait for init!");
      await writePacket(characteristic, buildPacket(MSG_INIT), true, 5000);

      console.log('Rotate the droid!');
      for (let degrees = -160 ; degrees <= 180 ; degrees+=5) {
        await writePacket(
          characteristic,
          buildPacket(MSG_ROTATE, convertDegreeToHex(degrees)),
          false,
        );
      }
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

I tried to make a full rotation of the top but it’s not possible, I get error 0x07 (data parameter invalid, check this link for more errors).

In the next episode I’ll try to move R2D2.

You can check this repository containing some other functions like animations and bipod/tripod transformations.

Cover image: artwork by snowmarite

Discussion (0)

pic
Editor guide