Encoding a LIFX packet

This page will explain how to create a LIFX binary protocol message. Some concepts that will be useful include:

I've decided we want to change my light to be full brightness and green. For this we need a SetColor (102). To achieve this we must craft the bytes for a packet header followed by the payload of our SetColor and then send that to my light.

Note that it's important that the bytes are encoded as "little endian". If you're in an environment where you can only write big endian then you'll need to do some byte swapping on the bytes that you write.

Header

Let's start with the header. If we take the fields mentioned in What is in a LIFX message we'll want the following values:

FieldTypeValue
sizeUint1649
protocolUint16 (stored in 12 bits)1024
addressableBool (stored in 1 bit)true
taggedBool (stored in 1 bit)false
originUint8 (stored in 2 bits)0
sourceUint322
target8 Uint8 integersd073d5001337
reserved6 Reserved bytes0
res_requiredBool (stored in 1 bit)false
ack_requiredBool (stored in 1 bit)true
reserved6 Reserved bits0
sequenceUint81
reserved8 Reserved bytes0
pkt_typeUint16102
reserved2 Reserved bytes0

📘

Partial fields

Some fields above are not the full number of bits in those types, see What is in a LIFX Message for more information.

First is size. A LIFX packet header is 36 bytes and then the payload for a SetColor is an extra 13 bytes, so we must encode 49 as a Uint16. This results in 00110001 00000000.

The next two bytes are split into multiple fields. The first 12 bits are for our protocol field, then we have 1 bit for addressable, a bit for tagged and the final 2 bits is for the origin field.

The protocol value must be set to 1024 for LIFX packets and as a Uint16 turns into 00000000 00000100. The LIFX protocol only uses the first 12 bits of that Uint16, so we leave the 4 upper bits for the next fields.

Next is addressable and that must always be true, and tagged should be false. Note that if we were broadcasting this packet to multiple lights then we'd want tagged to be true. And so we set the next two bits to be 01.

Finally our origin should be left as 0s, giving us 00. This means the 2 bytes that consist of protocol, addressable, tagged and origin become 00000000 00010100.

Next up is source. The values 0 and 1 are reserved. So we'll set 2 here. The source is an arbitrary number and can be used to check whether the replies have come from a source you know about. So 2 as a Uint32 becomes 00000010 00000000 00000000 00000000.

Next up is our target. This field allows 8 bytes of data, but currently we only use the first 6. If we want to send this packet to multiple devices and have all of the respond then we leave this as all 0s and ensure tagged is set to true. In this example we only want to target one device and so we'll set a specific target.

Let's say my device is d073d5001337 which becomes this array of hexadecimal numbers [0xd0, 0x73, 0xd5, 0x00, 0x13, 0x37, 0x00, 0x00]. In base 10, which we'll be a bit more familiar with, that becomes [208, 115, 213, 0, 19, 55, 0, 0]. We want to pack each as a Uint8 to become ['11010000, '01110011', '11010101', '00000000', '00010011', '00110111', '00000000', '00000000'], which concatenates into 11010000 01110011 11010101 00000000 00010011 00110111 00000000 00000000

After target we have 6 reserved bytes. For all reserved bytes we set all 0s, so this one is easy! We write 48 0s here. Then we have another split byte. This time one bit for res_required, one bit for ack_required and 6 reserved bits. You shouldn't need to ever set res_required to true because Get messages will return a reply regardless and replies to Set messages aren't useful. So we'll leave that to 0 and then set ack_required to 1. This allows us to use the absence of a reply acknowledgement message to indicate a retry should occur. This leaves us with 00000010.

Now we have a Uint8 for the sequence. Replies from a device will copy the triplet of (source, sequence, target) from the request packet and so we can use this to determine which request resulted in which reply. You should increment sequence for each message you send and then wrap to 0 after it reaches 255. Let's use 1 here, which as a Uint8 becomes 00000001.

Finally we have a couple reserved fields and then type. Each packet has a different type that tells the device what kind of payload will follow. We're creating a SetColor (102) which has a type of 102. This becomes 01100110 00000000 when we pack it as a Uint16.

That was a lot of information! Here is a table:

FieldValue
size00110001 00000000
protocol00000000 00000100
addressable1
tagged0
origin00
source00000010 00000000 00000000 00000000
target11010000 01110011 11010101 00000000 00010011 00110111 00000000 00000000
reserved00000000 00000000 00000000 00000000 00000000 00000000
res_required0
ack_required1
reserved000000
sequence00000001
reserved00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
pkt_type01100110 00000000
reserved00000000 00000000

Payload

This is the fun part, where we tell the device about my chosen color green!

The SetColor (102) has in it's payload the following fields

FieldType
reserved1 Reserved bytes
hueUint16
saturationUint16
brightnessUint16
kelvinUint16
durationUint32

And we want full saturation green at full brightness, and for it to apply immediately. So in HSBK that's 120 Hue and 100% brightness and saturation. When we have full saturation the kelvin doesn't make much difference so we'll leave that as 3500.

In the LIFX protocol, all four numbers in the HSBK are represented using a Uint16 which means a number between 1 and 65535. But in HSBK, hue is a number between 1 and 360, and saturation and brightness are numbers between 0 and 100. So we must first convert them.

Our page on HSBK explains the algorithms we can use and we end up with

hue = 120
saturation = 1
brightness = 1
kelvin = 3500

uint16_hue = int(round(0x10000 * hue) / 360)) % 0x10000
uint16_saturation = int(round(0xFFFF * saturation))
uint16_brightness = int(round(0xFFFF * saturation))
uint16_kelvin = kelvin

Giving us:

FieldValue
reserved0
hue21845
saturation65535
brightness65535
kelvin3500
duration0

And when we convert those into the appropriate encoding

FieldValue
reserved00000000
hue01010101 01010101
saturation11111111 11111111
brightness11111111 11111111
kelvin10101100 00001101
duration00000000 00000000 00000000 00000000

The entire packet

After all this we end up with the following for the entire packet:

FieldValue
size00110001 00000000
protocol00000000 00000100
addressable1
tagged0
origin00
source00000010 00000000 00000000 00000000
target11010000 01110011 11010101 00000000 00010011 00110111 00000000 00000000
reserved00000000 00000000 00000000 00000000 00000000 00000000
res_required0
ack_required1
reserved000000
sequence00000001
reserved00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
pkt_type01100110 00000000
reserved00000000 00000000
reserved00000000
hue01010101 01010101
saturation11111111 11111111
brightness11111111 11111111
kelvin10101100 00001101
duration00000000 00000000 00000000 00000000

And when we concatenate all that together we end up with

00110001 00000000 00000000 00010100 00000010 00000000 00000000 00000000 11010000 01110011 11010101 00000000 00010011 00110111 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000010 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 01100110 00000000 00000000 00000000 00000000 01010101 01010101 11111111 11111111 11111111 11111111 10101100 00001101 00000000 00000000 00000000 00000000

and as hex

3100001402000000
d073d50013370000
0000000000000201
0000000000000000
66000000005555ff
ffffffac0d000000
00

You then send those bytes to your device and it shall become even more pretty!