Oliv'

Introduction

I recently bought a Cubii under desk elliptical bike, I have two use cases for it:

  • Physiotherapy after a foot surgery
  • Help me to warm-up in my cold home office in the winter

Added bonus, doing light exercise. I definitely not find it as enjoyable as using a full size machine, or doing “real” sports, but this is just my preference and I am stuck at home with crutches anyway 😅

So my second hand model is a Cubii pro: no screen to display statistics, but with Bluetooth connectivity, which means that I had to install yet another proprietary app. I doubt usefulness of collecting those data but as it is BLE communications I wondered if it would be easy to reverse engineer the protocol by spying on the frames…
It appeared to be even easier to look into the Android application to find everything I need to dump the data into InfluxDB, and a fun weekend project.

Decompiling the Android application

I never decompiled any Android application but I knew that it was not hard, especially as Java is interpreted and not a compiled language. A quick search pointed me to Jadx, which did all the magic to extract the code from the Android APK to human readable Java files. Then it took only few minutes to get to the interesting code:

Calories conversion

From the class CubiiBleManager:

calories = this.this$0.getCalories(this.$revolutions, (latestCubiiNoLive == null || (cubiiResistance = latestCubiiNoLive.getCubiiResistance()) == null) ? 1 : cubiiResistance.intValue());
distance = this.this$0.getDistance(this.$revolutions);

public final double getCalories(int i, int i2) {
    // Oliv': i=revolutions, i2=resistance selector
    double d = 0.717d;
    switch (i2) {
        case 2:
            d = 0.733d;
            break;
        case 3:
            d = 0.767d;
            break;
        case 4:
            d = 0.833d;
            break;
        case 5:
            d = 0.9d;
            break;
        case 6:
            d = 1.033d;
            break;
        case 7:
            d = 1.267d;
            break;
        case 8:
            d = 1.483d;
            break;
    }
    return i * 0.03d * d;
}

public final double getDistance(int i) {
    // Oliv': i=revolutions
    return i * 1.93E-4d;
}

BLE UUID

From BLEConstants:

Name UUID BLE defined
RPM 00001731-2927-efef-cdab-eba0fe583711 No
Battery 00001732-2927-efef-cdab-eba0fe583711 No
Break position 00001733-2927-efef-cdab-eba0fe583711 No
Revolutions 00001734-2927-efef-cdab-eba0fe583711 No
Device info 0000180a-0000-1000-8000-00805f9b34fb Yes
Software version 00002a28-0000-1000-8000-00805f9b34fb Yes
Hardware version 00002a27-0000-1000-8000-00805f9b34fb Yes
Cubii BLE service 00001730-2927-efef-cdab-eba0fe583711 No
Cubii motorized BLE service 6e400001-b5a3-f393-e0a9-e50e9ecadc24 No
Cubii + BLE service 6e400001-b5a3-f393-e0a9-e50e24dcca9e No
Cubii+ OTA BLe service 0000fe59-0000-1000-8000-00805f9b34fb Yes

Read BLE value from broadcast

From broadcastUpdate() in file BLEService:

GetIntValue() documentation, constant list here:

Type Value
Float 52
Sfloat 50
Sint16 34
Sint32 36
Sint8 33
Uint16 18
Uint32 20
Uint8 17
Integer revolutions = bluetoothGattCharacteristic.getIntValue(18, 0);
Integer rpm = bluetoothGattCharacteristic.getIntValue((bluetoothGattCharacteristic.getProperties() & 1) == 0 ? 17 : 18, 0);
Integer battery = bluetoothGattCharacteristic.getIntValue((bluetoothGattCharacteristic.getProperties() & 1) == 0 ? 17 : 18, 0);
// Hardware revision
byte[] value = bluetoothGattCharacteristic.getValue();
// Software revision
byte[] value2 = bluetoothGattCharacteristic.getValue();

Read value from BLE service

From file BLEService:

    private final void readRPM() {
        BluetoothGatt bluetoothGatt = this.mBluetoothGatt;
        BluetoothGattService service = bluetoothGatt == null ? null : bluetoothGatt.getService(UUID.fromString(BLEConstants.CUBII_BLE_SERVICE));
        if (service == null) {
            Logger.INSTANCE.d(this.TAG, "Cubii service not found!");
            return;
        }
        BluetoothGattCharacteristic characteristic = service.getCharacteristic(BLEConstants.Companion.getRPM());
        if (characteristic == null) {
            Logger.INSTANCE.d(this.TAG, "Battery level not found!");
        } else {
            readCharacteristic(characteristic);
        }
    }

Easter egg

There is a small copy/paste typo in BLEService. If the RPM BLE service cannot be found the warning is about the battery :-)

    BluetoothGattCharacteristic characteristic = service.getCharacteristic(BLEConstants.Companion.getRPM());
    if (characteristic == null) {
        Logger.INSTANCE.d(this.TAG, "Battery level not found!");
    } else {
        readCharacteristic(characteristic);
    }

BLE connection mode

Cubii is using connected BLE mode, then either notify you with new data or accumulate them between two consecutive read operations.

Reading data and sending them to InfluxDB

Thanks to Python, the libraries Bleak (BLE) and InfluxDB it was fairly easy:

  1. Connect to the device
  2. Subscribe to notifications
  3. Accumulate/average data for 15 seconds
  4. Write them to InfluxDB
  5. In case of disconnection, restart in 1.

Cubii is sending the data every 3 seconds but I am sending them only every 15 seconds to avoid extra disk space. It could easily be down sampled inside InfluxDB thanks to tasks but yeah, it was not really required in the beginning for the small amount of data, and all the actions are done in the Python code, easier to manager for such small project

Dashboard

As expected I have no real use of the data but it was fun, so I draw the data in Grafana:

Grafana dashboard with the collected data

Grafana dashboard with the collected data

Code

The Python script is hosted on Gitlab.

Tools

  1. jadx: Decompile and transform Dex to Java
  2. Apktool: Decompile/recompile APK
  3. BLE assigned numbers
comments powered by Disqus