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 (stored in 12 bits) | 1024 |
addressable | Bool (stored in 1 bit) | true |
tagged | Bool (stored in 1 bit) | false |
origin | Uint8 (stored in 2 bits) | 0 |
source | Uint32 | 2 |
target | 8 Uint8 integers | d073d5001337 |
reserved | 6 Reserved bytes | 0 |
res_required | Bool (stored in 1 bit) | false |
ack_required | Bool (stored in 1 bit) | 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 0
s, 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 0
s 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 0
s, so this one is easy! We write 48 0
s 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
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!
Updated 6 months ago