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:

Field

Type

Value

size

Uint16

49

protocol

Uint16

1024

addressable

Bool

true

tagged

Bool

false

origin

Uint8

0

source

Uint32

2

target

8 Uint8 integers

d073d5001337

reserved

6 Reserved bytes

0

res_required

Bool

false

ack_required

Bool

true

reserved

6 Reserved bits

0

sequence

Uint8

1

reserved

8 Reserved bytes

0

pkt_type

Uint16

102

reserved

2 Reserved bytes

0

📘

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:

Field

Value

size

00110001 00000000

protocol

00000000 00000100

addressable

1

tagged

0

origin

00

source

00000010 00000000 00000000 00000000

target

11010000 01110011 11010101 00000000 00010011 00110111 00000000 00000000

reserved

00000000 00000000 00000000 00000000 00000000 00000000

res_required

0

ack_required

1

reserved

000000

sequence

00000001

reserved

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

pkt_type

01100110 00000000

reserved

00000000 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

Field

Type

reserved

1 Reserved bytes

hue

Uint16

saturation

Uint16

brightness

Uint16

kelvin

Uint16

duration

Uint32

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:

Field

Value

reserved

0

hue

21845

saturation

65535

brightness

65535

kelvin

3500

duration

0

And when we convert those into the appropriate encoding

Field

Value

reserved

00000000

hue

01010101 01010101

saturation

11111111 11111111

brightness

11111111 11111111

kelvin

10101100 00001101

duration

00000000 00000000 00000000 00000000

The entire packet

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

Field

Value

size

00110001 00000000

protocol

00000000 00000100

addressable

1

tagged

0

origin

00

source

00000010 00000000 00000000 00000000

target

11010000 01110011 11010101 00000000 00010011 00110111 00000000 00000000

reserved

00000000 00000000 00000000 00000000 00000000 00000000

res_required

0

ack_required

1

reserved

000000

sequence

00000001

reserved

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

pkt_type

01100110 00000000

reserved

00000000 00000000

reserved

00000000

hue

01010101 01010101

saturation

11111111 11111111

brightness

11111111 11111111

kelvin

10101100 00001101

duration

00000000 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!