One of our upcoming hardware projects (more on this later) requires a very bright, controllable light source. In the past, we’ve just used bare LEDs with our own cooling, power, control, etc. Not being someone who really understands how electricity works though, it always feels like a hassle to get a reliable setup. For this project, we instead decided to just give a LumeCube a try. They’re far from cheap, but they’re very bright, and hopefully have rock solid reliability.
LumeCube has an iOS app which allows for control over bluetooth. Although it also has a USB port, that’s only used for charging. We wanted to be able to do basic brightness control from within the Python application that will run the rest of the hardware. It doesn’t appear anyone has gone to the trouble of reverse engineering the LumeCube before, so we figured we’d give it a go. There were just two challenges:
- We’ve never reverse engineered a Bluetooth communications protocol.
- We don’t really understand how Bluetooth works.
Sticking with our philosophy of obtaining minimum-viable-knowledge, we started Googling “how to reverse engineer a bluetooth device” and ended up on this Medium post by Uri Shaked. Uri pointed us towards the “nRF Connect” app, which allows you to scan for Bluetooth devices and enumerate all of their characteristics (look at me using the lingo).
Acting on the assumption that LumeCube wasn’t going to go out of their way to secure their Bluetooth connection, we assumed brightness control would be pretty straightforward. It was just a matter of tracking down the characteristic ID and the structure of the data. To do that, we began by listing all of the characteristics in nRF Connect. Then we would disconnect and launch the official app. From there, we could adjust the brightness, then flip back to nRF Connect, rescan the characteristics, and see what had changed.
The relevant characteristic popped out very quickly. The third byte of 33826a4d-486a-11e4-a545-022807469bf0
varied from 0x00
to 0x64
, or 0-100. Writing new values back to that characteristic confirmed that hunch – huzzah!
Once we’d identified the characteristic, it was just a matter of implementing some controls in Python. For that we used Bleak, which offers good documentation and cross-platform support. Below is a quick example that turns brightness to 100 (0x64
).
import asyncio
import logging
from bleak import BleakClient
address = "819083F8-A230-4F61-8F94-CB69FF63D340"
LIGHT_UUID = "33826a4d-486a-11e4-a545-022807469bf0"
#logging.basicConfig(level=logging.DEBUG)
async def run(address):
async with BleakClient(address) as client:
await client.write_gatt_char(LIGHT_UUID, bytearray(b"\xfc\xa1\x64\x00"), True)
loop = asyncio.get_event_loop()
loop.run_until_complete(run(address))
Thx for the tutorial.
Where did you get the address variable from?
My LumeCubeV2 automatically disconnects and I cannot pair with it. I receive an AuthenticationFAiled error and it disconnects.
I believe nRF Connect shows that info as well, though you could probably scan for it using Bleak. Not sure about that other error – haven’t used my LumeCube in a while.