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:
- Connect to the device
- Subscribe to notifications
- Accumulate/average data for 15 seconds
- Write them to InfluxDB
- 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:
Code
The Python script is hosted on Gitlab.
Tools
- jadx: Decompile and transform Dex to Java
- Apktool: Decompile/recompile APK
- BLE assigned numbers
Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Pinterest
Email