Ramblings of an aging IT geek
← Ramblings of an aging IT geek
hardware

poking at a £6 bluetooth thermometer until it talked

Sniffing the BLE traffic of a cheap temperature and humidity sensor with nRF Connect and gatttool to find the characteristic carrying the readings and decode the payload by hand.

A small bluetooth sensor board on a workbench

I bought a handful of cheap Bluetooth temperature and humidity sensors, the little white square ones with an LCD that every marketplace sells under six different brand names. They work fine with their own app, which is precisely the problem: I don't want their app, I want the numbers in my own logging. So the job was to find out how the device hands over a reading, and whether I could pull it without anyone's SDK.

First step is always just to look. nRF Connect on a phone, or bluetoothctl and gatttool on a laptop with a BLE adapter, will scan, connect, and dump the GATT table: every service and characteristic the device advertises, with their UUIDs and properties. Most cheap gadgets don't bother with custom encryption or pairing, they just expose a handful of characteristics and trust that nobody's looking. This one was no exception.

$ gatttool -b A4:C1:38:XX:XX:XX --primary
attr handle: 0x0001, end grp handle: 0x0007 uuid: 00001800-...  # generic access
attr handle: 0x0028, end grp handle: 0x002c uuid: 0000fe95-...  # vendor service
...
$ gatttool -b A4:C1:38:XX:XX:XX --characteristics
handle: 0x0036, char value handle: 0x0037, uuid: ebe0ccc1-...  # notify

The interesting characteristic was the one with the notify property and a vendor-specific UUID rather than a standard one. Standard UUIDs (the short 16-bit ones like 0x2A6E for temperature) mean the device follows the Bluetooth SIG spec and you can just read the value off. A long 128-bit vendor UUID means they've done their own thing and you'll have to decode it yourself. Fine. That's the fun bit anyway.

A close-up of a BLE characteristic notification payload

I subscribed to notifications on that characteristic and watched. Every ten seconds or so a five-byte payload arrived. To decode it I did the oldest trick there is: I breathed on the sensor and watched which bytes moved. Two bytes climbed together as the temperature rose, one byte tracked my breath's humidity, and the last pair sat almost still, which is the giveaway for a battery voltage.

bytes:  c9 09 38 64 0b
        ^^^^^ temp     ^^ humidity
0x09c9 = 2505  ->  25.05 °C   (little-endian, divide by 100)
0x38   = 56    ->  56 % RH
0x0b64 = 2916  ->  2.916 V    battery

Little-endian sixteen-bit, divided by a hundred, for temperature. A single byte for humidity as a whole percent. A voltage trailing on the end. Nothing clever, nothing encrypted, just a manufacturer who saw no reason to hide a thermometer reading and was entirely correct not to.

From there it's a few lines of Python with bleak to subscribe and unpack the bytes, and the readings land straight in my own metrics with no app, no cloud account, and no phone in the loop. The whole exercise took an evening and most of that was the breathing-on-it part. The lesson, if there is one, is that an enormous amount of consumer BLE kit is wide open by default. They assume the only thing that'll ever connect is their own app, and they're nearly always wrong, which is occasionally annoying for security and frequently delightful for me.