Reverse Engineering an LTech M4 RGBW Controller


A while ago I purchased a RGBW LED strip and a controller for my desk, I mounted the strips on the back of my monitors and on the bottom of my desk. It was a bit of an arduous process as I wanted the strip to flex with my monitor arms, which involved cutting the strips into sections and then soldering a loop of wire to each to give some slack.

As for the actual controller I purchased an RF controller from a site called LED Lighthouse which was an LTECH M4 controller.

The controller had a variety of remotes but I opted for a one with a colour wheel and allowed me to set the white channel and gave a few pointless options like a party mode and some colour cycling modes.

I really always wanted to do more with it, and a few times dabbled with the idea of purchasing one of their WiFI controllers with an aim to say cycle the colours corresponding to my wallpaper or turn the LED strip off when I locked my machine.

The WiFI controller would have been the easy option however for £100 I really felt this was overkill, I'd already spent around £35 on the controller and the LED strip was around £55 so I really didn't want to invest more.

Since I'd recently purchased my HackRF I thought this would be a good time to look into how I could do some of this automation, my goal was to reverse engineer the protocol and try to replicate this with a Raspberry Pi and a cheap transmitter, just like the doorbell project.

TL;DR?

If you just want to skip to the protocol go to: Protocol Summary.

Determining Frequency and Modulation

The first step was to work out what frequency it was using, I had a suspicion this was going to be using 433Mhz so without even looking up any information about the product I connected up my HackRF and started up osmocom_fft tuned to around 433Mhz.

Pressing the buttons on the controller affirmed my suspicion, I could see the remote was using 433Mhz and I could also tell in the same way from the previous doorbell project that it was using ASK-OOK. This was going to be a relatively simple process of capturing the codes and then trying to work out what the protocol looks like.

Gathering Timing and Initial Data

The first step was to capture the timing and the content of the messages, the easiest thing to cover off would be how the device toggles on/off.

At this point I wanted to just simply test that we could replay the codes, that it wasn't doing any two way communication with the receiver. So just like the previous doorbell project I captured the code with hackrf_transfer and attempted to replay it.

1
2
sudo hackrf_transfer -t led_on.dat -f 433e6
sudo hackrf_transfer -r led_on.dat -f 433e6 -x 20

I made sure the lights were set off and I replayed the code, bingo the LEDs turned on. I made another assumption here that the remote probably isn't that intelligent and is probably sending the code to toggle on/off and the controller itself is keeping track of the state.

So I replayed the same code and discovered my assumption was wrong, playing the same code to turn the lights off did not work. So we can take one thing from this simple test, the remote keeps track of the state of the controller, this is more sophisticated than I initially thought.

I wanted to take this theory further and see if when you took the battery out of the remote it would reset the controller to a default colour, and I found that it didn't, so we can assume that the remote is actually storing these values in some form of NVRAM.

Anyway, getting back to the task at hand we can now be sure we can replay these codes ourselves without much sophistication. So lets start to delve into the actual protocol.

I captured the data much like in my previous blog post:

1
sudo osmocom_fft -f 433e6 -W

For a start I just wanted to work out what the off and on codes are for the receiver so I went through the process of starting a capture, then pressing the on button and stopping the capture. I then repeated the same for turning the LED strip off. I now had two files which represented an on and off message.

The next thing was to load these up in inspectrum and start looking at the actual data.

The first thing we can see is that it's a single message that is repeated 6 times, each of the messages is entirely the same.

Looking at the start of the message it's very easy to see this is a preamble, a set of highs and lows in an attempt to synchronise the clock of the transmitter and receiver.

Following on from this we can see some data represented which we will decode in a moment, then right at the end of the frame you can see a termination symbol represented by a longer high pulse.

Measuring the symbol periods we can see in this case the symbol rate is uniform, each represented 1 or 0 has the same symbol period composed of a short pulse followed by a long delay, or a long delay followed by a short pulse. We assume here that a long pulse represents a binary 1 and a short pulse represents a binary 0.

In terms of decoding this data this makes it a lot simpler as we can start to use Inspectrum's symbol cursor features to measure and then export the average amplitude for each of our symbols.

So lets take all our timing measurements by using the cursor:

Type Period
Preamble Delay 300us
Preamble Post Delay 1180us
Short Pulse 300us
Short Delay 590us
Long Pulse 590us
Long Delay 300us
Termination Pulse 1180us
Repeat Delay 32ms

There are a few different timings here, but as you can see our overall symbol period discounting preamble/termination is 300us+590us = 890us.

Reverse Engineering the Protocol

Now we can focus on decoding some of the data in our actual message looking at on and off messages we have the following decoded data:

As I said before since the symbol periods are uniform unlike the doorbell project we can start to use the cursor features available in Inspectrum to do a lot of the leg work for us.

I'll give a brief overview of how you can do this now and elaborate on it more in a later article. Open up your capture file via a terminal, you'll want to do this so we can get access to stdout.

The first thing we need to do is add an amplitude plot our data, you can do this by simply right clicking on the view window and adding a derived amplitude plot. You may at this stage want to mess around with the "Power max" and "Power min" settings until you have a clean binary output.

Next, you want to set your cursor covering the first symbol, in this case this should cover 890us. The zoom is your friend here, use control and the mouse wheel to zoom in without resetting the view.

From that point you need to find out how many symbols are in the message, in the case of this message it's 104 bytes (I just kept adjusting the number of symbols until it matched the message length), so we set our symbols to 104.

Now we have each symbol highlighted we can right click on the amplitude plot and "Export Symbols to stdout". It's really important at this point that the view only contains the information you care about, having other messages on the screen affects the output data.

1
-0.999863, -0.999964, 2.28756, -0.999452, -0.999348, -0.99976, -0.999682, -0.999887, 2.38464, 2.28868, -0.999698, -0.99984, -0.999847, -0.998347, -0.999863, 2.26799, -0.999339, 2.34046, -0.999669, -0.999591, -0.999256, -0.999864, 2.29974, 2.22888, 2.25899, 2.285, 2.33437, 2.22504, 2.30623, 2.32162, 2.32258, 2.3031, 2.27271, 2.22482, -0.998305, -0.999829, -0.999648, -0.9999, -0.999981, -0.999039, 2.27191, 2.25272, 2.22638, 2.2959, 2.20158, 2.26438, 2.16157, 2.25925, 2.2641, 2.2515, 2.21912, 2.22035, 2.2073, 2.2365, 2.19703, 2.28472, 2.22549, 2.20053, 2.24786, 2.2263, 2.27474, 2.24734, 2.29381, 2.2321, 2.29682, -0.999693, -0.999871, -0.999776, -0.99997, -0.999617, -0.999889, -0.999474, 2.21028, 2.24194, 2.23085, 2.23787, 2.19578, 2.19513, 2.22287, 2.17365, -0.999528, 2.26489, -0.999949, -0.999895, -0.99979, -0.999983, -0.999873, -0.999291, 2.18995, 2.20254, 2.23447, 2.2164, 2.2602, 2.20859, -0.999947, -0.999981, 2.31218, 2.24444, -0.999474, -0.999825, -0.998964, -0.99908, 2.23933, -0.999994

You should get something similar to the above, which for each symbol gives us an average amplitude, we then then to convert this into binary. We can see that each 0 has a lower average amplitude in that symbol, so we basically want everything >1 to represent a binary 1 and everything <1 to represent a binary 0.

Using Python is probably the easiest way to do this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import sys

data = [3, 2.26799, -0.999339, ...etc]

def convert(data):
  for i in data:
    if i>1:
      sys.stdout.write("1")
    else:
      sys.stdout.write("0")

convert(data)

I tend to just spin this up in an interactive Python shell rather than editing a file every time. From this we get the decoded data that we can then interpret directly.

1
2
3
4
On  | 00100000 11000001 01000011 11111111 01111111 11111111 00000001 00000000 10000000 11111111 00000000 01111011 11111110
Off | 00100000 11000001 01000011 11111111 01111111 11111111 00000001 00000000 00000000 11111111 00000000 10010111 11110010
                                                                              ^
                                                                              z

We can see the total length of the data is 104 bits, what's quite interesting is we can see actually a few bits of the message has changed even though all we've done is say turn on or turn off. I've highlighted a bit however that looks like it could be our on or off bit (z), it will become clear soon after decoding a few more messages why this bit looks significant.

The next step would be to try to force some modes where we can be relatively sure we haven't adjusted any other states. The ideal would be that we set the strip to Red, Green, and Blue. Capturing the data each time to try to work out which bits have changed. This should hopefully isolate just those bits that affect those channels.

Fortunately one of the modes of the controller is to force the strip to just display red, green, or blue so we can isolate these messages.

1
2
3
4
5
R 00100000 11000001 01000011 11111111 01111111 11111111 00000000 00000000 10000000 11111111 00000000 11010011 11111001
G 00100000 11000001 01000011 11111111 01111111 00000000 11111111 00000000 10000000 11111111 00000000 10100011 11110001
B 00100000 11000001 01000011 11111111 01111111 00000000 00000000 11111111 10000000 11111111 00000000 00101011 11000001
                                               ^------^ ^------^ ^------^ ^
                                                 Red      Green    Blue   z

Above we can see that we have a distinct pattern when we've changed those colours, it's highlighted to us that we have a byte representing each colour, so from this we've successfully isolated 3 bytes that represent those colours.

We actually have enough data now to turn the light on and off and change the colour to whatever we want. But we want to do a little more than just that, we still have to work out the following:

  • How to set the brightness of the light strip
  • How to turn the white channel on and off

So lets start with the brightness channel and work out how this is represented. Since we already have a list of known states, we can take one of these states and simply adjust the brightness. This gives us a basis of comparison between the states.

In this case we're going to take the blue channel and dim it to work out how the values change.

1
2
3
4
B 00100000 11000001 01000011 11111111 01111111 00000000 00000000 11111111 10000000 11111111 00000000 00101011 11000001
  00100000 11000001 01000011 11111111 01111111 00000000 00000000 11111111 10000000 01000010 00000000 10000101 11111110
                                               ^------^ ^------^ ^------^ ^        ^------^
                                                 Red      Green    Blue   z         Alpha

In the above we can see a byte has changed in the message which we can assume adjusts our brightness, in this case I've labelled it alpha.

Lets now isolate something else, we're going to take our blue channel and then turn on the white LED's to see how the data changes.

1
2
3
4
B 00100000 11000001 01000011 11111111 01111111 00000000 00000000 11111111 10000000 11111111 00000000 00101011 11000001
  00100000 11000001 01000011 11111111 00000001 00000000 00000000 00000000 10011000 11111111 00000000 11000011 11010000
                                      ^------^ ^------^ ^------^ ^------^ ^  ^^    ^------^
                                        Sel      Red     Green    Blue    z  ww     Alpha

In this comparison we can see a few things have changed again, we've toggled on two bits in our section that also controls on and off. Currently I'm not aware directly of what these two bits control, however it seems to be a requirement of turning the white channel on that these bits are set. We can also see that the section now labelled Sel has been changed. It's assumed that this is a kind of channel/mode selection for the rest of the data. i.e. Only affect this channel. From the remote I have it only sets two states 0x80 meaning RGB and 0x01 meaning White.

What's most interesting about this message is that when we turn white on, the RGB channels are set to 0x00 which is an indication that with this controller messages can only act on either the white channel or the RGB channel at a single time. It is not possible to change the colour and change the white channel at the same time from my experiments. This is perhaps due to the way the product was developed, perhaps an RGB version existed and the RGBW version was brought in and the original software developed upon to add the white channel functionality at a lesser development cost. Or perhaps it was done this way to allow backwards compatibility with RGB remotes. Though all of this is pure speculation.

Lets try to rule out a few more bytes, this remote has a few options for flashing LEDS and cycling through the colours. So lets put it through a few of those modes to see how the data changes, again using blue as a basis for comparison.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
B 00100000 11000001 01000011 11111111 01111111 00000000 00000000 11111111 10000000 11111111 00000000 00101011 11000001
Lights Flashing (RGB):
  00100000 11000001 01000011 11111111 00000000 11111111 11111111 11111111 10000000 11111111 10100000 11101001 01100100
Lights Flashing (RGBCMY):
  00100000 11000001 01000011 11111111 10000000 11111111 11111111 11111111 10000000 11111111 11100000 01010010 01101101
Lights Fading (RGB):
  00100000 11000001 01000011 11111111 01000000 11111111 11111111 11111111 10000000 11111111 11000000 11101001 11000000
Lights Fading (RGBCMY):
  00100000 11000001 01000011 11111111 11000000 11111111 11111111 11111111 10000000 11111111 01000000 11111100 11000010
                                      ^------^ ^------^ ^------^ ^------^ ^  ^^    ^------^ ^------^
                                        Sel      Red     Green    Blue    z  ww     Alpha     Mode

These two modes highlight another byte that has changed which I've now labelled Mode, we can see two modes within this 0xA0 - RGB Flashing, and 0xE0 which represents all colours flashing. Since we've identified the byte that represents this I'll list all modes that my remote supports:

We can also see that the selection is intrinsically tied to this:

Mode Sel Hex Mode Hex
Lights Flashing (RGB) 0x00 0xA0
Lights Flashing (RGBCMY) 0x80 0xE0
Lights Fading (RGB) 0x40 0xC0
Lights Fading (RGBCMY) 0xC0 0x40

It looks like in Sel these modes use the first 2 bits, and in the mode section they use the first 3 bits. I haven't seen any other bits used the mode block with my remote, but that's not to say there are other modes that I'm not able to see.

The RGB channels in this case always seem to be set to 0xFF, however this doesn't seem to have an impact on the setting, setting these to 0x00 seems to have no impact.

The other thing to note is that when setting the mode, the channel must be set to zero for the mode to take effect.

Ok, so we've run through most modes of the remote now, we now need to make some assumptions about the rest of the data.

1
2
3
B 00100000 11000001 01000011 11111111 01111111 00000000 00000000 11111111 10000000 11111111 00000000 00101011 11000001
  ^---------------------------------^ ^------^ ^------^ ^------^ ^------^ ^  ^^    ^------^ ^------^ ^---------------^
                   ID                   Sel      Red     Green    Blue    z  ww     Alpha     Mode       Checksum

Since the first 4 bytes have never changed during any message we can assume that this is an identifier for the remote. The terminology on the website seems to hint towards this since you can pair a remote to the receiver. I'm assuming that this is 4 bytes so that each remote that they manufacture can have a unqiue ID making it less likely for remotes to interfere with each other. The manual also suggests that the receiver learns the ID of a remote rather than the pairing occurring in the opposite direction. This would also make sense since we have only seen unidirectional data, the remote can only transmit and the controller can only receive.

The last two bytes change on every message when the data changes, prior messages sent with the same data have the same last two bytes. It's fairly obvious at this point then that this is a checksum designed to validate the contents of the message. Now we need to work out what checksum this is.

Mainly what we're looking for is a form of CRC check that is 16 bits in length and is probably fairly common for RF communications. After doing a bit of research about common CRC checks for 433Mhz it seems that CRC-CCIT is fairly common for this type of application however there are a few variations. At this point it was actually a case of trying some common CRC-CCIT implementations until we can find one that matches one of our known messages.

https://www.lammertbies.nl/comm/info/crc-calculation.html

The above website had a few implementations and it was simply a case of converting the message to hex and then trying to match the checksum at the end of the message.

After a few tries I found that it was CRC-CCIT (Kermit).

Once I had this information we actually have all of the data required to build some code that will then control this light strip.

Before we go onto that I'm going to summarise the protocol for those that want a single reference to go back to for their own implementation.

Protocol Summary

1
2
3
B 00100000 11000001 01000011 11111111 01111111 00000000 00000000 11111111 10000000 11111111 00000000 00101011 11000001
  ^---------------------------------^ ^------^ ^------^ ^------^ ^------^ ^------^ ^------^ ^------^ ^---------------^
                   ID                   Sel      Red     Green     Blue      Sw     Alpha     Mode       Checksum

Message

Name Description States Notes
ID A static identifier for the remote    
Sel Channel/Mode selection
  • 0x80 - RGB
  • 0x01 - White
  • 0x00 - RGB Flashing
  • 0x80 - RGBCMY Flashing
  • 0x40 - RGB Fading
  • 0xC0 - RGBCMY Fading
 
Red Red Channel level 0x00-0xFF  
Green Green Channel level 0x00-0xFF  
Blue Blue Channel level 0x00-0xFF  
Sw Switches (On/Off, White On/Off)
  • 0x80 - On/Off Switch
  • 0x98 - White Channel
 
Alpha Global brightness level 0x00-0xFF  
Mode Modes (Flashing, Cycling, etc.)
  • 0xA0 - RGB flashing
  • 0xE0 - RGBCMY flashing
  • 0xC0 - RGB Fading
  • 0x40 - RGBCMY Fading
  • Sel must be 0x00
  • Sel must be 0x80
  • Sel must be 0x40
  • Sel must be 0xC0
Checksum CRC-CCIT (Kermit)    

Building the Code

Now we have something to work to we can create something in C that will allow us to control the light strip.

For information about how to wire up a Raspberry Pi to a 433Mhz transmitter please refer to my previous doorbell article.

For this we're going to use exactly the same principles as the previous project of course changing the timings and switching our static code implementation with some controls.

The main focus of this isn't to implement everything, but to allow us to turn the light on/off and change the colours of the light strip.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#include <wiringPi.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <string.h>
#include <stdint.h>
#include "checksum.h"

#define OUTPUT_PIN 0

#define PREAMBLE_DELAY 300
#define SHORT_PULSE 300
#define LONG_PULSE 590
#define END_PULSE 1176

#define SHORT_PULSE_DELAY 590
#define LONG_PULSE_DELAY 300
#define REPEAT_DELAY 32000
#define PREAMBLE_POST_DELAY 1180

#define REPEAT 10

// Set this to your own remote ID
uint32_t device_id = 0x20C143FF;

// "Packet" holding all our values for the transmission
typedef struct packet {
    uint32_t id;
    uint8_t chan;
    uint8_t red;
    uint8_t green;
    uint8_t blue;
    uint8_t x;
    uint8_t alpha;
    uint8_t mode;
} packet;

void u8from32 (uint8_t b[4], uint32_t u32) {
    uint8_t *vp = (uint8_t *)&u32;

    b[0] = vp[3];
    b[1] = vp[2];
    b[2] = vp[1];
    b[3] = vp[0];
}

void send_preamble(void);
void send_close(void);
void send_byte(uint8_t b);
void send(packet data);
void setup(void);

int main (int argc, char **argv)
{

    setup();

    int oc;
    int colour = 0xFF0000;
    int alpha = 0xFF;
    int state = 1;

    while ((oc = getopt(argc, argv, "x:c:a:")) != -1) {

        switch (oc) {
        case 'c':
            colour = (int)strtol(optarg, NULL, 16);
            break;
        case 'a':
            alpha = atoi(optarg);
            break;
        case 'x':
            state = strcmp("on", optarg) == 0;
            break;
        case ':':
            fprintf(stderr, "%s: option '-%c' requires an argument\n",
                argv[0], optopt);
            abort();
            break;
        case '?':
        default:
            abort();
        }
    }

    //Convert colour hex to our colour values
    uint8_t r,g,b;
    b = colour;
    g = colour>>8;
    r = colour>>16;

    uint8_t x = 0x00;
    if(state == 1) {
        x = 0x80;
    }

    // Set up our data packet
    packet data = {
        id: device_id,
        chan: 0x7F,
        red: r,
        green: g,
        blue: b,
        x: x,
        alpha: alpha,
        mode: 0x00
    };

    // Repeat our message six times
    for(int i=0; i < REPEAT; i++) {
        send(data);
        delayMicroseconds(REPEAT_DELAY);
    }

}

void setup(void) {
    wiringPiSetup ();

    //Set GPIO 0 to output
    pinMode (OUTPUT_PIN, OUTPUT);
}

void send(packet data) {

    uint8_t bytes[13];

    uint8_t id[4];
    u8from32(id, data.id);

    // Set our ID bytes
    for(uint8_t i=0; i < 4; i++) {
        bytes[i] = id[i];
    }

    bytes[4] = data.chan;
    bytes[5] = data.red;
    bytes[6] = data.green;
    bytes[7] = data.blue;
    bytes[8] = data.x;
    bytes[9] = data.alpha;
    bytes[10] = data.mode;

    // Generate a CRC-CCIT (Kermit)
    uint16_t crc = crc_kermit(bytes, 11);

    bytes[11] = (uint8_t)(crc >> 8);
    bytes[12] = (uint8_t)crc;

    //Actually send the data
    send_preamble();
    for(i = 0; i < 13; i++) {
        send_byte(bytes[i]);
    }
    send_close();
}

void send_byte(uint8_t b) {
    for (int j=7; j>=0; j--)
    {
        if((b>>j)&1) {
            digitalWrite(OUTPUT_PIN, 1);
            delayMicroseconds(LONG_PULSE);
            digitalWrite(OUTPUT_PIN, 0);
            delayMicroseconds(LONG_PULSE_DELAY);
        } else {
            digitalWrite(OUTPUT_PIN, 1);
            delayMicroseconds(SHORT_PULSE);
            digitalWrite(OUTPUT_PIN, 0);
            delayMicroseconds(SHORT_PULSE_DELAY);
        }
    }
}

void send_preamble(void) {
    int state = 1;

    for(int i = 0; i < 22; i++) {
        digitalWrite(OUTPUT_PIN, state);
        delayMicroseconds(PREAMBLE_DELAY);
        state = state == 1 ? 0 : 1;
    }

    digitalWrite(OUTPUT_PIN, 0);
    delayMicroseconds(PREAMBLE_POST_DELAY);
}

void send_close(void) {
    digitalWrite(OUTPUT_PIN, 1);
    delayMicroseconds(END_PULSE);
    digitalWrite(OUTPUT_PIN, 0);
}

If you want to go ahead and compile this yourself on your Raspberry Pi note that you'll need wiringPi installed and in this instance I've also compiled libcrc that gives me a crc_kermit function to generate the checksum. You'll need the libcrc headers and the library in the same directory.

1
gcc -Wall -std=c99 -l wiringPi main.c libcrc.a

From this we get a binary that allows us to turn on/off the led strip, set the colour, and set the brightness.

1
ledcontrol -x on -c FF0000 -b 180

REST API

Currently this binary sits on our RPi, so we need some way of interacting with it remotely, in this case I decided to just set up a Flask project that would accept a JSON request and then pass these parameters on to the binary, this sits on the Pi accepting requests.

Ensure that you have python-pip installed and then go ahead and install Flask.

1
pip install Flask

Now we can create the endpoint that will allow us to call this binary remotely:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import subprocess
from flask import Flask, jsonify, request
app = Flask(__name__)

@app.route("/lights", methods=['POST'])
def lightcontrol():
    data = request.get_json()

    if data == None:
        return jsonify({'error': True}), 400

    x = 'off'
    if 'state' in data:
        if data['state'] == 'on':
            x = 'on'

    if 'colour' in data:
        colour = data['colour']
    else:
        colour = "FFFFFF"

    if 'alpha' in data:
        alpha = data['alpha']
    else:
        alpha = 255

    params = ['ledcontrol', '-x', x, '-c', colour, '-a', str(alpha)]

    subprocess.call(params)
    return jsonify({'success': True})

This is not the cleanest code ever but simply accepts some JSON parameters and calls the binary:

1
2
3
4
5
{
  "state": "on",
  "colour": "FF0000",
  "alpha": 180
}

We can now use this endpoint to add some functionality, since this light strip sits behind my desktop I want to start by turning the LED strip off when I lock my computer, then when I unlock it I want to turn it back on.

I use i3 as my desktop, and i3lock to lock. I also have xautolock sitting in the background that will lock the screen after 30 seconds of inactivity. So this is a relatively simple one to accomplish, I create a script that does my API call to the RPi and then locks the screen.

~/bin/lockscreen

1
2
3
4
#!/bin/bash

/usr/local/bin/http POST 192.168.1.164/lights state=off
i3lock -c 000000 -n && /usr/local/bin/http POST 192.168.1.164/lights state=on

Here all I'm doing is using httpie to make a call to the lights endpoint, then calling i3lock, I force i3 lock to not fork with -n so that I can make a call to httpie again once i3lock has died to turn the lights back on again.

Then I just ensure xautolock and my lock shortcut calls this script instead of calling i3lock directly. Adding this to my i3 config does just that:

~/.config/i3/config

1
2
bindsym Ctrl+$mod+l exec ~/bin/lockscreen
exec xautolock -detectsleep -time 3 -locker "~/bin/lockscreen" -notify 30 -notifier "notify-send -u critical -t 100 -- 'LOCKING screen in 30 seconds'"

Something else I wanted to do was to have some "themes" for my desktop, where if I change the wallpaper it also changes the LED strip behind the monitors. Again that's just a simple case of changing the wallpaper and then making a call to the API to change the colour.

~/bin/theme

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/bin/bash

PATH=/usr/local/bin/:$PATH

case "$1" in

green)
feh --bg-fill /home/paul/Pictures/wallpaper/green.jpg
/usr/local/bin/http POST 192.168.1.164/lights state=on colour=7FFF00 brightness=255
;;

orange)
feh --bg-fill /home/paul/Pictures/wallpaper/orange.jpg
/usr/local/bin/http POST 192.168.1.164/lights state=on colour=FFAA00 brightness=255
;;

white)
feh --bg-fill /home/paul/Pictures/wallpaper/white.jpg
/usr/local/bin/http POST 192.168.1.164/lights state=on colour=ffffff brightness=255
;;

red)
feh --bg-fill /home/paul/Pictures/wallpaper/red.jpg
/usr/local/bin/http POST 192.168.1.164/lights state=on colour=ff3000 brightness=255
;;

blue)
feh --bg-fill /home/paul/Pictures/wallpaper/blue.jpg
/usr/local/bin/http POST 192.168.1.164/lights state=on colour=3299ff brightness=255
;;

purple)
feh --bg-fill /home/paul/Pictures/wallpaper/purple.jpg
/usr/local/bin/http POST 192.168.1.164/lights state=on colour=551a8b brightness=255
;;

esac

Simply typing "theme red" in the terminal results in the wallpaper changing and then the lights changing behind the monitor.

The Result

So lets see this all in action!

As with all tech demos we can see a bug where I attempted to change the colour and for some reason it wasn't received, I've identified this as a timing issue. I plan to make an improvement to the setup where I offload the signal generation to an MCU, from that point the Pi is just responsible for sending SPI messages to the MCU.

From there I could even look at removing the Pi entirely by interfacing this directly to a computer if I were to introduce serial or USB to talk directly to the MCU.

What's Next?

We have the basics developed but there's a little more to do with it:

  • Since we have to send the full state each time, there's a problem if we change the theme then lock, it will attempt to set the color to 0xFFFFFF. Really we need to store the last colour like the remote does, then just change the applicable bit to turn it on or off.
  • We currently don't have data for changing the brightness of the white channel, only a way to turn it on or off, from a very brief look at it it isn't completely stateful. When you hold down the brightness button it will send a message to say "start dimming", then when you let go it will send another message to say "stop dimming". That's not great if we want to automate it, but it is possible. It's unfortunate this isn't available just as another field like the other colours.
  • We also don't have any data for changing the speed of the modes, each can flash the LED's, this has the same functionality as described above where it isn't stateful.

If anyone has any other data to go along with this or has corrections to the protocol please get in touch below.

Comments