Reverse an BLE Light
Reverse one BLE LED
I have bought one BLE(Bluetooth Low Energy) LED and want to control it with Home Assistant.
There are many blogs about BLE sniffing for these types of lights, for example BLE_LED_strip. Although I did not actually use this method, but thanks to this comprehensive guide.
I tried the method mentioned above, but encoutered the following issues:
- The Bluetooth HCI log was not included in the bug report when my android phone is not rooted
- After rooting, the btsnoop_hci.log did not contain much information. Sepcifically, there were no Bluetooth records sent to the remote device.
However, there is still one method available: decompiling the Android application. These little brands’ Bluetooth LED applications are also small and often without obfuscation. For example, this light uses the Allbest Home.
APK decompile
Jadx is an excellent DEX to Java decompiler, well-suited for our purpose.
The decompiled result is absolutely great. I simply opened the APK file without needing any other operations, and the generated code is highly readable.
I searched for classes and functions containing the keywords light
Send
ble
and bluetooth
After some searching, I found methods in CommandUtil
class that fit the requirements.
1
2
3
4
5
// class com.th.qc.command.CommandUtil
// other methods such as sendLightMode sendLightBright sendLightColor... can also been found here
public final void sendLightSwitch(boolean z) {
BlueConnectionUtil.getInstance().writeCmd(pack((byte) 17, z ? (byte) 1 : (byte) 0));
}
Now we just need to figure out how the writeCmd
method sends BLE commands and how the pack
method generates the commands been sent.
pack
First, let us see the implementation of pack
function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final byte[] pack(byte b, byte... data) {
Intrinsics.checkNotNullParameter(data, "data");
ArrayList arrayList = new ArrayList();
arrayList.add((byte) -96);
arrayList.add(Byte.valueOf(b));
arrayList.add(Byte.valueOf((byte) (data.length + 3)));
for (byte b2 : data) {
arrayList.add(Byte.valueOf(b2));
}
ArrayList arrayList2 = arrayList;
byte[] shortToByte = BleUtils.getShortToByte(BlockUtils.crc16Check(CollectionsKt.toByteArray(arrayList2)));
Intrinsics.checkNotNullExpressionValue(shortToByte, "getShortToByte(crc16)");
arrayList.add(Byte.valueOf(shortToByte[1]));
arrayList.add(Byte.valueOf(shortToByte[0]));
return CollectionsKt.toByteArray(arrayList2);
}
public static int calculateCRC16(byte[] bArr) {
int i = 65535;
for (byte b : bArr) {
i ^= b & UByte.MAX_VALUE;
for (int i2 = 0; i2 < 8; i2++) {
i = (i & 1) != 0 ? (i >> 1) ^ 40961 : i >> 1;
}
}
return i;
}
The method is very straightforward and easy to read. Let’s convert it to python :happy:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# python version
def crc16(in_) -> int:
i = 65535
for b in in_:
i ^= b & 0xff
for i2 in range(0,8):
if (i&1) != 0:
i = (i>>1) ^ 40961
else:
i = i >> 1
return i
def getshortToByte(i) -> list:
return [i&0xff,(i>>8&0xff)]
def pack(b, data) -> list:
ret = []
ret.append(0xA0)
ret.append(b)
ret.append(len(data) + 3)
for i in data:
ret.append(i)
ret += getshortToByte(crc16(ret))
return ret
writeCmd
Secondly, let’s dig into the writeCmd
method. Keep in mind that we don’t need to understand every detail about the implementation. We only need to stop when the method and parameter names seems familiar to us.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// in order to simplify the code, I delete some unimportant codes
public wirteCmd(byte[] bArr) {
BleDevice connectDevice = BleManager.getInstance().getConnectDevice(str);
if (connectDevice != null) {
writeShakeBle(connectDevice, bArr, this.mWriteCallback);
}
}
private void writeShakeBle(final BleDevice bleDevice, final byte[] bArr, final BleWriteCallback bleWriteCallback) {
BleManager bleManager = BleManager.getInstance();
String serviceUUID = bleDevice.getFilter().getServiceUUID();
String writeUUID = bleDevice.getFilter().getWriteUUID();
if (bleWriteCallback == null) {
bleWriteCallback = this.mWriteCallback;
}
bleManager.write(bleDevice, serviceUUID, writeUUID, bArr, bleWriteCallback);
return null;
}
public void write(BleDevice bleDevice, String serviceUUID, String writeUUID, byte[] bArr, BleWriteCallback bleWriteCallback) {
bleBluetooth.newBleConnector().withUUIDString(serviceUUID, writeUUID).writeCharacteristic(bArr, bleWriteCallback, writeUUID);
}
the UUID
and writeCharacteristic
terms seem familiar to me because these words are inside the BLE protocol definition, which you can find in the manual. I will also explain them in the next section too.
BLE
In Bluetooth Low Energy (BLE), a UUID (Universally Unique Identifier) is a 128-bit number used to identify and distinguish different BLE services, characteristics, and descriptors. UUIDs are used to define the structure and functionality of BLE data.
A characteristic is a basic data entity in BLE that carries the actual data payload. Characteristics are grouped under services, which are collections of related characteristics. Each characteristic has:
- UUID: A unique 128-bit identifier that defines the type of data the characteristic represents (e.g., heart rate, temperature, etc.).
- Properties: Bit flags that define how the characteristic can be used and accessed. These include:
- Read: Allows the characteristic value to be read.
- Write: Allows the characteristic value to be written.
- Notify: Allows the server to send updates to the client when the characteristic value changes.
- Indicate: Similar to Notify, but with an acknowledgment from the client.
- Value: The actual data payload of the characteristic.
- Descriptors (optional): Additional metadata or attributes related to the characteristic, such as value ranges, units, or descriptions.
For example, a Heart Rate Service might have a Heart Rate Measurement Characteristic with a UUID of 0x2A37. This characteristic could have the Read and Notify properties enabled, allowing a client to read the current heart rate value and receive updates when it changes.
In summary, UUIDs are used to identify BLE entities, characteristics contain the actual data, and properties define how the characteristic can be accessed and used within the BLE protocol.
Bleak
bleak is python library to communicate with BLE protocol. Our implementation of light switch control will depend on it.
A Bluetooth peripheral may have several characteristics with the same UUID, so the means of specifying characteristics by UUID or string representation of it might not always work in bleak version > 0.7.0. One can now also use the characteristic’s handle or even the
BleakGATTCharacteristic
object itself inread_gatt_char
,write_gatt_char
,start_notify
, andstop_notify
.
One can use the BleakClient
to connect to a Bluetooth device and read all services via the asynchronous context manager like this:
1
2
3
4
5
6
7
8
9
10
11
import asyncio
from bleak import BleakClient
address = "xx:xx:xx:xx:xx:xx"
async with BleakClient(address) as client:
for service in client.services:
print(f"Service UUID: {service.uuid}")
characteristics = service.characteristics
for characteristic in characteristics:
print(f" Characteristic UUID: {characteristic.uuid}")
print(f" Properties: {characteristic.properties}")
It is recommended to use the asyncio library with BleakClient.
For our test case, the script will output all services and corresponding characteristics like this:
1
2
3
4
5
Service UUID: 0000ff10-0000-1000-8000-00805f9b34fb
Characteristic UUID: 0000ff11-0000-1000-8000-00805f9b34fb
Properties: notify
Characteristic UUID: 0000ff12-0000-1000-8000-00805f9b34fb
Properties: read, write
The one we need to focus on is the UUID with the write
property
Script
Now, with the knowledge we gained from the previous sections, we can write the code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from bleak import BleakClient
def crc16(in_) -> int:
i = 65535
for b in in_:
i ^= b & 0xff
for i2 in range(0,8):
if (i&1) != 0:
i = (i>>1) ^ 40961
else:
i = i >> 1
return i
def getshortToByte(i) -> list:
return [i&0xff,(i>>8&0xff)]
def pack(b, data) -> list:
ret = []
ret.append(0xA0)
ret.append(b)
ret.append(len(data) + 3)
for i in data:
ret.append(i)
ret += getshortToByte(crc16(ret))
return ret
LIGHT_ON_CMD = bytes(pack(17,[1]))
address = "90:00:00:51:8E:27"
LIGHT_UUID = # test your uuid here
async with BleakClient(address) as client:
await client.write_gatt_char(LIGHT_UUID, LIGHT_ON_CMD)
The python script needs the MAC address first. You can obtain it from your phone using nRF Connect. For instructions on using nRF Connect, refer to this manual. Match the name with the MAC address yourself.
Choose the characteristic with the write property and configure it as
LIGHT_UUID
Run the script
Success! the light is on.
You can explore other services on your own.
TODO
Add the device as HACS integrations to home assistant