Continuing the discussion from Controlling lights with Bash:
So I’ve had a few questions about how I built the packets for the bash example. I thought it might be a good tutorial for those who havent had much experience with binary protocols before. Hopefully it gets a few more people over that hurdle and lets them talk directly to the bulbs over LAN.
Before I start there are a few concepts that you will need to be familiar with. Here are a few wikipedia articles you may wish to review:
Binary protocols
Binary protocols usually exist as several layers applied one after the other, with each layer describing details of the next. When all combined together these make up what is called a frame or a packet. The LIFX protocol looks like this too. In the LIFX protocol there are two major sections, the headers and the payload. The headers describe the action that is being taken and the way in which it will work and the payload provides the data for that specific action. Sometimes the action requires no data, and in these cases the payload is not included.
In the LIFX protocol there are three headers that are included in every message.
- The frame header includes details about how to process the frame itself. This includes the protocol version and the size of the frame.
- The frame address header includes details about the destination and the processing of the frame.
- The protocol header describes the type of the payload.
These headers are included in the order above, with the payload included at the end if required.
An example packet
Lets build a packet to change all of the lights on our network to green. We will be representing the packet in hexidecimal format, which means every character corresponds to 4 bits, we will group them in groups of two, representing a full byte.
To do this we first need to build the frame header. You can find the description of what is included in the frame header here. The documentation says we start with 16 bits which indicate the size of the message. Since we don’t have the frame yet we cant possibly know its size. But we do know that the size is stored in two bytes. So lets write them as ?
for now. So our packet starts out as:
?? ??
A Binary Field
Next up is a byte split up into several fields, so we will have to manually assemble it ourselves. You will need to do this whenever you encounter fields that are partial bytes. So lets start with two zero bytes:
???? ???? ???? ????
The documentation says that the first two bits here are the message origin indicator and must be zero.
00?? ???? ???? ????
The next bit represents the tagged
field. We want this packet to be processed by all devices that receive it so we need to set it to one (1).
001? ???? ???? ????
The next bit represents the addressable
field. This indicates that the next header will be a frame address header. Since all of our frames require this it will always be set to 1.
0011 ???? ???? ????
The final 12 bits are the protocol
field. This must be set to 1024 which is 0100 0000 0000
in binary. Now our two bytes are complete.
0011 0100 0000 0000
Lets look at them in hexidecimal:
34 00
Back to the packet
Before we add these to the packet there is one final thing to note. The protocol is specified to be little endian. We have been working in big endian for ease of explanation, so we need to switch the two bytes around before we add them to the frame. Now the frame looks like this:
?? ?? 00 34
The next 32 bit (4 bytes) are the source, which are unique to the client and used to identify broadcasts that it cares about. Since we are a dumb client and don’t care about the response, lets set this all to zero (0). If you are writing a client program you’ll want to use a unique value here.
?? ?? 00 34 00 00 00 00
That finishes off the frame header, and we move onto the frame address. The frame address starts with 64 bits (8 bytes) of the target
field. Since we want this message to be processed by all device we will set it to zero.
?? ?? 00 34 00 00 00 00 00 00 00 00 00 00 00 00
This is followed by a reserved section of 48 bits (6 bytes) that must be all zeros.
?? ?? 00 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
The next byte is another field so follow the steps above to build the binary then hex representations. In this example we will be setting the ack_required
and res_required
fields to zero (0) because our bash script wont be listening for a response. This leads to a byte of zero being added.
?? ?? 00 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Since we aren’t processing or creating responses the sequence number is irrelevant so lets also set it to zero (0).
?? ?? 00 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Next we include the protocol header. which begins with 64 reserved bits (8 bytes). Set these all to zero.
?? ?? 00 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Now we have to indicate the message type. The different message types are listed in the Device Messages and Light Messages pages of the documentation. We are changing the color of our lights, so lets use SetColor, which is message type 0x66 (102 in decimal). Remember to represent this in little endian.
?? ?? 00 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 66 00
Finally another reserved field of 16 bits (2 bytes).
?? ?? 00 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 66 00 00 00
Now we have finished with the headers. All that’s left now is the payload. Since our packet is getting quite long I’m going to build the payload separately for now. But imagine it is included at the end of the packet we have been building so far.
The payload starts with a reserved field of 8 bits (1 bytes).
00
Next up is the color described in HSBK. The HSBK format is described at the top of the light messages page. It starts with a 16 bit (2 byte) integer representing the Hue. The hue of green is 120 degrees. Our scale however goes from 1-65535 instead of the traditional 1-360 hue scale. To represent this we use a simple formula to find the hue in our range. 120 / 360 * 65535
which yields a result of 21845
. This is 0x5555 in hex. In our case it isn’t important, but remember to represent this number in little endian.
00 55 55
We want maximum saturation which in a 16bit (2 byte) value is represented as 0xFFFF.
00 55 55 FF FF
We also want maximum brightness.
00 55 55 FF FF FF FF
Finally we set the Kelvin to mid-range which is 3500 in decimal or 0x0DAC.
00 55 55 FF FF FF FF AC 0D
The final part of our payload is the number of milliseconds over which to perform the transition. Lets set it to 1024ms because its an easy number and this is getting complicated. 1024 is 0x00000400 in hex.
00 55 55 FF FF FF FF AC 0D 00 04 00 00
So there we are, with 36 bytes of header information and 13 bytes of payload we have a frame that is 49 bytes big (or 0x31 in hex). So lets combine the two and fill in the size field.
31 00 00 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 66 00 00 00 00 55 55 FF FF FF FF AC 0D 00 04 00 00
And we have a packet! If you add the escaping to it that was on the bash post you get:
allgreen.echo
\x31\x00\x00\x34\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x66\x00\x00\x00\x00\x55\x55\xff\xff\xff\xff\xac\x0d\x00\x04\x00\x00
Of course building packets by hand is time consuming and annoying, best to use a computer.