Skip to content

Bias Lighting

What better accessory to a new PC build than bias lighting? The ambient glow not only welcomes you to waste your time gaming into the early hours of the morning, but it’s also better for your eyes as it softens the edges of your screen.

Just take me to…

The parts you’ll need:

Neopixel Strip

First is the Neopixel assembly. For my project, I had a dual monitor setup, but you can easily do this with a single monitor as well. Layout the strip on the monitor and make sure the arrows are all facing the same direction. I accidentally soldered one segment backwards and blew a wall adapter that way. The LEDs still worked, so props to their robustness. Next is to cut small lengths of 22 gauge wire to connect each segment of Neopixel at their corners.

To mount the strip to the monitor, I used 3M adhesive strips. But they’re total shit so I recommend silicone adhesive instead.

Optional Step for Dual Monitor

If you’re only using a single monitor, skip this step. To connect the monitor, solder a 3-pin pigtail connector to each strip on each monitor. Connecting these pigtails will connect the strip. As before, make sure the arrows on the strips both run the same direction.

Circuitry

Power

Note that if you are using a more recent Raspberry Pi, the pinout may be different. The Pi will receive power from the USB, but the Neopixel strip will receive power separately from the wall adapter. This means the 5V rail powering the Neopixel is separate from the 5V powering the pi. The wall adapter should be able to supply 2A, as the LEDs get really power hungry in large numbers, though my code takes steps to alleviate this by capping the brightness. If you want to blow a fuse, set the MAX_BRIGHTNESS variable in bias_lighting.py to 255 and let your light shine.

The end of the strip should have 5 wires coming out of it.

The 3-pin pigtail connector gets connected to the 5V half of the level shifter. The separate red and yellow wires are used as separate power lines which powers both the Neopixel and the level shifter. They should connect to a female barrel connector that in turn connects to the wall adapter.

Level Shifter

The raspberry pi outputs 3.3V, but the power strip requires 5V. A level shifter is required to make up the difference. Place a capacitor between 5V and ground on the 5V side of the level shifter. I used 1mF because I ordered the wrong one. A 100uF will do just fine. Also place a resistor between the level shifter and the Neopixel’s data line to prevent damage from the LED.

On the 3.3V side, connect to the Pi’s 3.3V line, ground, and pin 18. Pin 18 is normally configured as output on bootup by default, but if there is a problem, feel free to use whatever pin works. If you do, go to bias_lighting.py and change the NEOPIXEL_PIN line to your new pin.

USB Serial

The client software communicates with the server on the pi over a serial connection. Unless you have an ungodly old PC with an serial port, you’re going to need a USB to TTL adapter. Connect the 4 pins to 5V, ground, and the Pi’s UART pins.

It is easiest to plug in the USB side into your monitor, assuming it has USB ports, and then run an extender cable to your computer. The pi can be powered through its 5V pin, meaning that when USB TTL cable is unplugged, the pi will power off. Normally, this means you can power cycle your pi by turning your monitor on and off.

Final Design

Software

The entire software repository can be found here. At the time of writing, it is only a proof of concept, but more features will be added in the future.

To install the server on the raspberry pi, place bias_lighting.py into /etc/init.d. This way, the script will run immediately on startup.

The server will continuously wait for commands via the serial connection. When a command (ie solid) is received, it is accompanied by other data to specify the details for the command (ie set these pixels red, set those pixels blue). Sometimes, a command requires animations, which requires redrawing the pixels periodically. This is done with the animator object and an animation function that corresponds to the specified command function. For example ember_fn, a command function, is called once, but its animation function, ember_ani is called 30 times a second until a new command is sent. The Animator class subclasses Thread to execute a provided function periodically while storing data returned by that function, so the next time the animation draws the pixels, it can remember its current state. When there’s no animation to be done (like the solid command), the thread is paused. When a new command that requires animation is requested, simply call the animator class’s change_ani function with a new function pointer, and the thread will automatically resume. The data the animator keeps track of is args and persist_data. The former, args, is a tuple that is passed to the the animator function and never changes. It usually consists of some constraints for the animation. The latter, persist_data, stores what is returned every time the animation function executes. This is typically used to store the current state of the animation. For example, args may say that you want red, white, and blue rotating around your monitor, while persist_data stores what pixels are currently red, white, or blue.

The communication used by the client consists of a command byte, two bytes indicating how many bytes in the subsequent data packet, and then the data packet with all the information about that command. Typically (but not strictly), commands are defined in swaths. As in one swath of LEDs will be this color or flicker in this way, but the adjacent swath of LEDs will be different colors.

Swath 1
Swath 2
Command Byte
Size of data packet
RGB data
Size of swath
More RGB data
Size of
swath

One example of breaking the swath convention is the config command. This is used mainly to specify the default animation, that is, the one that launches on startup. It has the same first 3 bytes, but it’s data packet consists of 2 bytes numbering the total length of the Neopixel strip in LEDs, followed by the list of bytes to interpret as a command

Default command
Command Byte
Size of data packet
Number of total LEDs
Command byte
Size of data packet
RGB data
Size of swath
More RGB data
Size of
swath

Future Plans

One of the downsides of python is that scripted languages are typically slow, especially on the Pi model 1 I used. This becomes more apparent in animation functions. The only one implemented at the time of writing, ember_ani, will run approximately 10 times per second. The plan is to code a python module called Animations in C++ that would handle these time sensitive operations more efficiently.

It almost goes without saying that I plan on adding new animations, particularly wipe and thunder animations. I’m also looking into one that can synchronize with music or the video on your monitor, but this would require a more powerful pi, and the video one in particular would need a DVI decoder, which can be quite expensive.

An more official release would come with setup scripts. This would be most useful for the client application, which would be well served with a proper desktop entry, complete with a fitting icon. Email me if you have an idea for that. I pay commission for graphic art. On the server side, future versions would implement log files to debug those rare server crashes.

Finally, there would be hardware upgrades, such as a “blank” button. Because if you’re like me, you have your workstation in your bedroom and want to turn off those lights at night to sleep. Press it again in the morning and the animation resumes right where you left off.

Troubleshooting

Q: Why are all the colors wrong?

A: Locate the following line:

t.strip = neopixel.Adafruit_NeoPixel(NUM_LEDS, NEOPIXEL_PIN, NEOPIXEL_HZ, 5, False, strip_type=neopixel.ws.WS2811_STRIP_GRB)

Change GRB to RGB, like so:

t.strip = neopixel.Adafruit_NeoPixel(NUM_LEDS, NEOPIXEL_PIN, NEOPIXEL_HZ, 5, False, strip_type=neopixel.ws.WS2811_STRIP_RGB)

Not all Neopixel strips are created equal, and sometimes when you order an RGB strip you get a GRB strip, like I did. Fortunately, Adafruit’s library can easily accommodate for this. If you apply this fix and get different wrong colors, you can see all the possible flags in the defines of jgarff’s Neopixel source code here

Q: Why did all the LEDs flash red and then go black?

A: The server on the pi crashed. Power cycle it to restore normal functionality. If you want to do me a solid, try and reproduce the error and open a new issue at this project’s github repository