Introduction: WowWee RevCar Command Line Remote

Introduction

This Instructable is about the process of building a command line app to control WowWee RevCar devices, which were these clever RC cars that were usually sold in pairs and could be controlled through a phone app that uses Bluetooth Low Energy (BLE) to send commands and receive events from the cars.

Although were discussing WowWee RevCars here, the process can be applied to any other RC car that is controlled through BLE or indeed anything else that communicates over BLE.

See the video for a quick demo.


Why CLI App?

The CLI app itself is not the end-goal here, but once we know how to control the car and test the logic involved over the command line, then it would be easy to design the next step or higher level applications like a Raspberry Pi remote control (with a touch screen) or perhaps a console Controller with Arduino+HC05 combo embedded or anything else.

The CLI application was built in Python and tested on Linux. The capture happened on an Android phone to record the HCI Snoop data.

The rest of this Instructable will walk through the process of finding out what that communication protocol looks like and re-creating it in python. If you're interested in the details of the protocol used by the app/car and sample communication capture, those are also on the github repo.

Supplies

What you'll need to re-create this Instructable with your own device:

  • A RevCar (or another phone-controlled BLE device)
  • Android Phone with device's App
  • Linux PC (the only way really)
  • Wireshark (for reading packet captures)
  • Android Platform Tools (to use ADB to download captures)
  • Optionally, you can use the BLE-LS python script to read the attributes of your device -

    https://github.com/madkaye/ble-ls

What you'll need to run the CLI app (If you're one of the few with a RevCar, or interested in the code for CLI or BLE)

Step 1: Analysing the Device's BLE Services

First step to find out what's going on is to fire up the RevCar and see what BLE services are advertised by it.

For this step there are plenty of tools available on PC or phone. I used a combination of bluetoothctl and my own python scripts.

bluetoothctl is available on most Linux distros as standard and allows you to scan and connect to BLE devices and read their GATT attributes (but the handles are not the same as other libraries, although UUID remains the same if you're familiar with those).

Our goal here is to find out a few basics about the device (Device Name, MAC Address, ...) but primarily we want to see what attributes are available for reading or writing.

Fire up bluetoothctl and start the scanning process:

$ bluetoothctl
[bluetooth]# scan on
Discovery started

Now start your device and wait a few seconds...

Switch off the scanning and review the list of devices reported

* Addresses masked to protect innocent TVs and toys *

[bluetooth]# scan off
Discovery stopped<br>[CHG] Controller 80:2B:F9:00:00:00 Discovering: no<br>[CHG] Device 61:50:00:00:00:00 RSSI is nil<br>[CHG] Device 66:93:00:00:00:00 TxPower is nil<br>[CHG] Device A3:35:00:00:00:00 RSSI is nil<br>

After switching off the scanning, you'll have two sets of device addresses: the address and Device names as they were captured during the scan period. Then a final report after the scan is switched off. You can usually find out your device's address from the first set, it would look something like this:

[NEW] Device 1C:0F:31:00:00:00 REV-900000

You'll see lots of devices nowadays, from phones and speakers to TVs and toys.

You can proceed with bluetoothctl to connect to the device then switch to the GATT menu to list and read attributes, but the formatting is useful for our purpose and takes too much time to go through (reading attributes by UUID individually).

Instead you can run the BLE-LS script I put together to list the services and which characteristics and their types. Clone the repo at: https://github.com/madkaye/ble-ls

Follow the README and run scan and list commands for your device

You should see an output that looks like this...

Listing services...<br>   -- SERVICE: 00001800-0000-1000-8000-00805f9b34fb [Generic Access]
   --   --> CHAR: 00002a00-0000-1000-8000-00805f9b34fb, Handle: 3 (0x0003) - READ WRITE  - [Device Name]
   --   --> CHAR: 00002a01-0000-1000-8000-00805f9b34fb, Handle: 5 (0x0005) - READ  - [Appearance]
   --   --> CHAR: 00002a04-0000-1000-8000-00805f9b34fb, Handle: 7 (0x0007) - READ  - [Peripheral Preferred Connection Parameters]
   -- SERVICE: 00001801-0000-1000-8000-00805f9b34fb [Generic Attribute]
   -- SERVICE: 0000180a-0000-1000-8000-00805f9b34fb [Device Information]
   --   --> CHAR: 00002a23-0000-1000-8000-00805f9b34fb, Handle: 11 (0x000b) - READ  - [System ID]
   --   --> CHAR: 00002a26-0000-1000-8000-00805f9b34fb, Handle: 13 (0x000d) - READ  - [Firmware Revision String]
   --   --> CHAR: 00002a29-0000-1000-8000-00805f9b34fb, Handle: 15 (0x000f) - READ  - [Manufacturer Name String]
   -- SERVICE: 0000ffe0-0000-1000-8000-00805f9b34fb [ffe0]
   --   --> CHAR: 0000ffe4-0000-1000-8000-00805f9b34fb, Handle: 18 (0x0012) - NOTIFY  - [ffe4]
   -- SERVICE: 0000ffe5-0000-1000-8000-00805f9b34fb [ffe5]
   --   --> CHAR: 0000ffe9-0000-1000-8000-00805f9b34fb, Handle: 23 (0x0017) - WRITE NO RESPONSE WRITE  - [ffe9]
... etc ...
Listing descriptors...
   --  DESCRIPTORS: 00002800-0000-1000-8000-00805f9b34fb, [Primary Service Declaration], Handle: 1 (0x0001)
   --  DESCRIPTORS: 00002803-0000-1000-8000-00805f9b34fb, [Characteristic Declaration], Handle: 2 (0x0002)
   --  DESCRIPTORS: 00002a00-0000-1000-8000-00805f9b34fb, [Device Name], Handle: 3 (0x0003)
   --  DESCRIPTORS: 00002803-0000-1000-8000-00805f9b34fb, [Characteristic Declaration], Handle: 4 (0x0004)
   --  DESCRIPTORS: 00002a01-0000-1000-8000-00805f9b34fb, [Appearance], Handle: 5 (0x0005)
   --  DESCRIPTORS: 00002803-0000-1000-8000-00805f9b34fb, [Characteristic Declaration], Handle: 6 (0x0006)
   --  DESCRIPTORS: 00002a04-0000-1000-8000-00805f9b34fb, [Peripheral Preferred Connection Parameters], Handle: 7 (0x0007)
... etc ...
Reading characteristics...
  -- READ: 00002a00-0000-1000-8000-00805f9b34fb [Device Name] (0x0003), None, Value: b'REV-90000'
  -- READ: 00002a01-0000-1000-8000-00805f9b34fb [Appearance] (0x0005), None, Value: b'\x00\x00'
  -- READ: 00002a04-0000-1000-8000-00805f9b34fb [Peripheral Preferred Connection Parameters] (0x0007), None, Value: b'\x10\x00 \x00\x00\x00\x90\x01'
  -- READ: 00002a23-0000-1000-8000-00805f9b34fb [System ID] (0x000b), None, Value: b'\x1c\x0f1\x00\x00\x00\x00!'
  -- READ: 00002a26-0000-1000-8000-00805f9b34fb [Firmware Revision String] (0x000d), None, Value: b'V0.20_V0.0\x00\x00'
  -- READ: 00002a29-0000-1000-8000-00805f9b34fb [Manufacturer Name String] (0x000f), None, Value: b'REV\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
  -- READ: 0000ffe4-0000-1000-8000-00805f9b34fb [ffe4] (0x0012), None, Value: 
... etc ...

From that output, we learn a few things:

  1. Reading the characteristic values, we can learn some of the basics: Device Name, Firmware Version, etc...

  2. Device advertises two characteristics flagged as allowing 'WRITE' (Handle 0x03 and 0x17). This means that we can rename the device over handle 0x03 if we want to, but more importantly that handle 0x17 is likely what is used to tell the device what to do (to drive and shoot in our case)

Now we know what the device advertises and we know to focus on handle 0x17, let's proceed to capture some data from the manufacturer's app.

Step 2: Capturing a Typical Exchange

This step aims to capture a basic exchange between the Device and its App. For this we use Android's own 'btsnoop' file.

Depending on your Android version, enable the Enable Bluetooth HCI snoop log option in the Developer options menu (look up your version if you're not familiar)

With the logging enabled, we want to capture a standard exchange from start to finish, then download that file as a single reference. Rinse and repeat a couple of times until you get it right.

  • Keep your first capture session to the basic steps (e.g. connecting to the car, drive forward once or twice, then disconnect)
  • Download your log file (to keep it easy to read and compare later)
  • Capture further movements (e.g. connect, drive forward several times, fire, drive forward and fire again)
  • Download those logs and switch to Wireshark for analysis

Downloading Bluetooth Logs

If you're not familiar with how to download get the logs, then you want to download Android's platform tools and use adb to download the logs. Downloading the logs is done by issuing a bug report so Android collects all relevant logs and adb downloads them in a zip file to your current directory.

After installation and connecting your device through USB, issue these commands

$ adb devices                    # to check your device is connected
$ adb bugreport capturebug01     # to download capturebug.zip including btsnoop log

Using Wireshark to Analyse the Logs

Wireshark understands so many networking protocols and displays them semantically by frames, packets or datagrams depending on the layers involved in the capture.

For BLE, the GATT commands are being sent over several other layers, so for the relevant packets, you should see a hierarchy that looks like this

  • Frame <-- the most basic physical (or radio) exchange
    • Bluetooth
      • Bluetooth HCI
        • Bluetooth ACL Packet
          • Bluetooth L2CAP Protocol
            • Bluetooth Attribute Protocol <-- this is the application protocol

The app and the car communicate primarily by having the 'central' app read/write to GATT attributes or the device sending notifications (or writing back if you're listening). These will be seen as over attribute commands (labelled ATT, BTATT or Bluetooth Attribute). Managing the connection and how the attributes would be discovered and advertised would fall to GAP and the rest of the Bluetooth protocols.

In our case, were primarily interested in GATT/ATT packets.

  1. Start by filtering for frames carrying ATT packets with 'btatt' in the filter
  2. Use the 'Time' stamp to orient yourself to what event is occurring. In other words, when did the the connection start, when did you press forward once, and so on. The timestamps is usually T+0 based off the start of the capture session (depending on Wireshark settings)
  3. Confirm you have the right capture by reviewing the packets exchanged at the start, they almost always start with 'central' reading all the services available on the device
  4. Analyse the exchange for patterns and recall those against the handles that allow WRITE from the previous step. Those handles will be the ones to expect the movements to be sent through
  5. Dropping to the very end of the exchange, you'll see a constant sequence of WRITE commands over handle 0x17 with the same value 0x001F78 (seen as little-endian sequence 0x781F00)

The repetitiveness of that commands confirms it is what is used for the action we performed.

We can test this out with a quick SCAN-CONNECT-WRITE sequence of actions. If we're lucky, it works and this was too simple. This is usually the case with LED lights or simpler applications.

In the case of RevCars, the players do pick a character, so the App has to inform the cars about those attributes (e.g. speed, hit-accuracy and so on). Without those character settings, the car doesn't respond to any commands. I treat those as a secret handshake and just send the same ones I captured. We can analyse those with different characters, but one is good enough for this exercise.

A sample of the car exchange and a short write up of the protocol used is on the CLI repository:

https://github.com/madkaye/revcar-cli

Step 3: Prototyping the App's Protocol

This step we try to re-create the exchange we captured through Python scripting and a library that can send/receive BLE data. Using the Python shell to interactively send/receive commands is the easiest way to prototype this.

From the analysis in the previous step, we know the following exchange occurs:

  1. The App scans for RevCar devices and measures the RSSI expected to sense the phone being swiped near the car (as there are usually 2 of them), it then connects to it

  2. After the connection is established, the app will enumerate all the services and characteristics. This is a standard part of BLE discovery. Several READ requests receive an error response from the car, so perhaps the App supports additional features that the car doesn't

  3. The App will sends the character information (the secret handshake code, I didn't analyse). This sets up things like the car's speed, hit-accuracy, etc.

  4. Additionally, the app sets up a notification handle (through handle 0x14 to get notifications on handle 0x12) - this is likely for the hit events from the other car

  5. Then it's ready for driving and firing commands over handle 0x17. Driving is usually a 3-byte sequence starting with 0x78 and Firing is a static 4-byte sequence

  6. Until a disconnection happens

One thing to note about #3, I didn't find that out in the first try, but later when the driving commands didn't work, I went back to analyse the middle part of the exchange further. So be prepared to iterate over this and that's why we capture several files in the capture step.

To prototype this, I wrote (and re-wrote) a few functions in a python class, then start the shell to start executing them. This is an example of what the cleaned up version looks like, referring to the committed version of 'carcontrol.py' in the github repo


from carcontrol import *                 # We use bluepy for BLE functions
car = CarControl()

# Step 1. Connect
car.scan()                               # Handy for getting the address
car.connect('1c:0f:31:00:00:00')         # connects to the car (and starts discovering services)

# Step 2. Enumerate
car.listservices()                       # mostly prints them out as they would have been discovered already
car.listdescriptors()                    # Not necessary, but I didn't know that when I started
car.readcharacteristics()                # We know the characteristics, but what are their values?

# Step 3. Sets it up to start driving
car.sendhandshake()

# Step 4. Skipped
# Step 5. Start testing out driving and firing
car.carforward()
car.carreverse()
car.carfiregun()

# Don't forget to say goodbye
car.disconnectcar()

Congratulations if your prototype works, you can now start designing a better application and clean up the code that you wrote in a hurry.

If it didn't work, you may be unlucky that the manufacturer has put a very difficult obfuscation mechanism in there, but always go back to the capture files and look at it differently. Change your filters, review what you thought wasn't relevant and so on. Analyse, prototype and repeat.

Thanks for reading and good luck with your projects.