home

Reverse engineering a LED bulb Bluetooth protocol

I was gifted a "Lunvon Smart LED Bulb" (like this one) and, as one can expect from these cheap bluetooth-controlled RGB lights, both the iOS and Android apps are garbage. Analysing the bluetooth data from the Android app it is possible to decode the protocol used to set the light color and turn it on/off. The bulb exposes a few BLE services, but I wasn't able to get anything useful from those.

Below the source code of a python script that test the bulb RGB capabilities, tested on a Raspberry Pi 3.

import bluetooth
from string import hexdigits
from time import sleep

LAMP_ADDR   = "f4:4e:fd:00:00:00"
LAMP_UUID   = "00001101-0000-1000-8000-00805f9b34fb"
LAMP_LOGIN  = "3031323334353637" # '01234567'

CMD_BASE    = "01fe000053831000"
CMD_TURNON  = "0000000050ff0000"
CMD_TURNOFF = "0000000050000000"
# RGB: BASE + "GGBBRR0050000000" where GG is green hex, etc.

# Bluetooth connection socket
btsock = None

def bt_send(data, debug=False):
    cleanPayload = data.replace(":", "")
    if debug:
        print("> ", cleanPayload)

    try:
        btsock.send(bluetooth.binascii.unhexlify(cleanPayload))
    except bluetooth.btcommon.BluetoothError as e:
        print("! lamp disconnected")
        lamp_connect()

def bt_read(n, debug=False):
    d = bluetooth.binascii.hexlify(btsock.recv(n))
    if debug:
        print("< ", str(d))
    return d

def lamp_turnon():
    bt_send(CMD_BASE + CMD_TURNON)

def lamp_turnoff():
    bt_send(CMD_BASE + CMD_TURNOFF)

def lamp_setrgb(rgb):
    assert len(rgb) == 6
    assert all(c in hexdigits for c in rgb)

    hexR, hexG, hexB = rgb[0:2], rgb[2:4], rgb[4:6]
    gbr = hexG + hexB + hexR + "0050000000"
    bt_send(CMD_BASE + gbr)

def hsv2rgb(h,s,v):
    assert 0 <= h < 360
    assert 0 <= s <= 1
    assert 0 <= v <= 1

    c = s*v
    x = c * (1 - abs((h/60) % 2 - 1))
    m = v - c

    r = g = b = 0

    if 0 <= h < 60:
        r, g = c, x
    elif 60 <= h < 120:
        r, g = x, c
    elif 120 <= h < 180:
        g, b = c, x
    elif 180 <= h < 240:
        g, b = x, c
    elif 240 <= h < 300:
        r, b = x, c
    else:
        r, b = c, x

    return [ (r+m)*255, (g+m)*255, (b+m)*255 ]

def lamp_sethsv(h,s,v):
    r, g, b = hsv2rgb(h,s,v)
    rgbStr = "%.2X" % r + "%.2X" % g + "%.2X" % b
    lamp_setrgb(rgbStr)

def lamp_connect(rgbTest=False):
    global btsock
    services = []
    print("? searching for compatible devices ...")

    # wait for the lamp to be avaible
    while True:
        services = bluetooth.find_service(
            uuid=LAMP_UUID, address=LAMP_ADDR)
        if len(services) > 0: break

        print("? device not found, retrying in 5 seconds ...")
        sleep(2)

    lamp = services[0]

    print("> lamp found [{}], connecting ...".format(LAMP_ADDR))
    btsock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
    btsock.connect((lamp['host'], lamp['port']))

    # Login
    print("> lamp 'login' ...")
    bt_send(LAMP_LOGIN)
    sleep(0.5)

    if rgbTest:
        print("> RGB test")

        lamp_setrgb("220000")
        sleep(0.4)
        lamp_setrgb("002200")
        sleep(0.4)
        lamp_setrgb("000022")
        sleep(0.4)


def lamp_color_loop():
    print("> color loop ...")
    while True: 
        for i in range(1, 360):
            lamp_sethsv(i, 1, 1)
            sleep(0.25)

if __name__ == "__main__":
    lamp_connect(rgbTest=True)
    lamp_color_loop()