Article
Building Our First Programmed Office Window Display
Here at Foster Made, we have been interested in bringing our software development skills to the physical world. Many of us are fascinated by the Internet of Things and follow many of the advancements in the field. For example, a few months ago Kelly, Adam, and I began developing a phone app to communicate with Bluetooth beacons we had laid out all across the building.
As we were brainstorming through our next store display, we decided now would be another great opportunity to bring our programming work to the physical world. Nina and I whipped up a slew of ideas, thinking about motors, projectors, capacitive touch sensors, and all sorts of electronic components.
We settled on LED lights — lots and lots of lights.
The installation used two Arduino microcontrollers, one to control each window. The two windows communicated wirelessly through an infrared LED. Each circle of the installation contained 10 RGB LEDs; with 24 circles, we individually controlled 240 lights.
To develop the software running on each Arduino, we used PlatformIO. PlatformIO allowed us to use some of the tools and best practices we are used to when working in higher level languages while we are getting deeper into C/C++ for microcontrollers. It handles the dependency management we are so used to with composer, npm, and pip. A simple platformio.ini file specifies the microcontroller we are programming along with the libraries we want automatically installed. It also lets us program our microcontroller from within the IDEs we are used to such as JetBrains’ CLion or Github’s Atom.
This window display only used two libraries, Adafruit’s Neopixel library and z3t0’s Arduino IRremote library. Neopixels are a type of RGB LED with a driver that allows us to individually address each LED in the strip. The installation has 24 circles, each containing strips of 10 Neopixel LEDs. This means the microcontrollers need to individually address all 240 LEDs in less than a millisecond or so — otherwise we would end up with slow, jittery movement.
The IRremote library allows each of the windows to communicate wirelessly with each other. Think of it this way: one Arduino acts as a remote control while the other acts as a cable box. The library handles the complex timings required for different protocols, so we only needed around five lines of code to handle communicating and reading the transmitted data.
# Left window runs this code IRsend irsend;int r = 9; // this number might be set somewhere else in the codeirsend.sendSony(r, 12);// start the animation# Right window runs this code IRrecv irrecv(IR_INPUT);decode_results results;while (!irrecv.decode(&results)) { // wait}results.value;irrecv.resume();// start the animation
What is happening here? The left window is the remote control; it makes the decisions and the right window follows its lead. The function sendSony()
is defined in the library as so:
void IRsend::sendSony (unsigned long data, int nbits)
So the first argument is the data we are passing, while the second is the number of bits to transmit. We pass in an integer for data that corresponds to the animation we want to display. Once the left window transmits this integer, it goes ahead and starts running the animation. The right window receives and decodes the signal quickly enough that it is able to begin the animation at the same time as the left window.
The right window runs in a loop while !irrecv.decode(&results)
evaluates to true. decode()
attempts to decode data passed through the IR sensor. If it is able to decode a signal, it returns true
and stores the decoded value at the pointer passed into the function (results
, in this instance). The variable results is a class of type decode_results
that comes from the IRRemote library. The only attribute we really worry about is results. value
, which contains the actual value decoded. Finally, it is important that we call the resume()
member function on irrecv
so that we can reset the ISR (Interrupt Service Routine) — this way the microcontroller can handle interrupts in the future and can continue to receive IR signals the next go around.
Outside of the libraries we are using, let’s look at our own code by taking a look at an animation definition file
// src/patterns/quick.h#include "base_pattern.h"uint8_t quick_pattern[] = { 0b11111111, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b10101010, 0b10101010, 0b10101010, 0b01010101, 0b01010101, 0b01010101,};struct pattern quick() { struct pattern p; p.length = 5; p.frames = quick_pattern; p.fade_delay = 3; p.frame_delay = 8; return p;}
In this file, we’ve defined two items: an array of uint8_t bytes labeled quick_pattern
and a function which returns a struct of type pattern. The variable pattern
is defined in base_pattern.h
. The function simply builds our struct, which is composed of four attributes: length
, frames
, fade_delay
and frame_delay
. length
refers to the number of frames in the animation, while frames
is a pointer to our array of bytes. You will notice the number of bytes in this array is 3 * p.length
— logically, this is because each frame is composed of three bytes.
fade_delay
is the number of milliseconds to delay our lights from fading in (or out). Our lights fade in from an initial light level of 0 until they reach the highest brightness level, 255. Each time the brightness level is increased by 1, we delay for the number of milliseconds specified in fade_delay
.frame_delay
on the other hand is the delay before moving on to the next frame. So once the lights reach their max brightness of 255 and fade back to 0, our microcontroller pauses for the number of milliseconds specified in frame_delay
. As you can see, this means with a fade_delay
of 3, the speed of fade in or fade out would be 256 * 3 = 768 milliseconds
, while our delay between frames is only 8 milliseconds. With 5 frames in our animation, it takes ((256 * 3) * 2) * 5 + (8 * 4) = 7712 milliseconds
. So this animation takes about eight seconds to complete.
Now that we have defined our data structure, we need an algorithm to work with the data structure.
First, we look at a function to check if an individual circle should be fading in or out for this frame of the animation.
Let’s look at the first frame of animation we defined above:
0b11111111,0b00000000,0b00000000,
This frame means that the top row should be lit, while the second and third row should be unlit.
Below is the basic algorithm for determining which circle should be lit.
#define NUM_ROWS 3bool shouldFade(uint8_t f, uint8_t row, uint8_t column) { uint8_t offset = window == LEFT_WINDOW ? 4 : 0; return bitAtK(pattern.frames[f * NUM_ROWS + (2 - row)], (3 - column) + offset);}
This function takes in three unsigned integers — f
, row
and column
. f
is the index of the current frame we are on. We have just started the animation, so we are on frame 0. This number increases until we reach our fifth frame (f=4
).First we look at the function bitAtK
, which, as the name implies, returns the value of the bit at some place k
in a byte.
bool bitAtK(uint16_t value, int k) { return (value & (1 << k)) != 0;}
The function takes in a byte (actually two bytes) value as well as a position k
. With a few bitwise operations, we receive the bit at k
, either 1
or 0
.pattern.frames
is the pointer to our array of bytes from earlier. We are calling it as an array with [f * NUM_ROWS + (2 - row)]
. What we are doing is getting the value pattern.frames[x]
at some index x
.Which byte should we get? What should x
be? f * NUM_ROWS
returns the index of the first row for whichever frame we selected for f
. For frame 0
we receive 0
, for frame 1
we receive 3
, for frame 2
we receive 6
, and so on. Simple. Now because the microcontrollers are located at the bottom of the installation, (row: 0, column: 0
) is the bottom left and (row: 2, column: 3
) is the top right pixel for each window. So if f * NUM_ROWS
is pointing to the byte at index 0
, we are pointing to the values of (row: 2
) in the context of the installation. Instead we want (row: 0
). We need to flip the frame over to accommodate this layout.
# byte layouts: __ Bit 7 / __ Bit 0 / / 0b11111111 <- Index 00b00000000 <- Index 10b00000000 <- Index 2 ____ Column 0 / ____ Column 3 / / ____ Column 0 / / / ____ Column 3/ / / / o-o-o-o o-o-o-o <- Row 2 | |o-o-o-o +---+ o-o-o-o <- Row 1| | | |o-o-o-o | -| o-o-o-o <- Row 0 | | | | <- Arduino & Power left door right
So we simply take the row we want in the installation row and subtract it from 2
to get the expected index.
Now that we have the correct byte, we simply get the bitAtK
where k
is the column we are looking for plus an offset which is also calculated in this function. Each of the windows for the installation is controlled by its own Arduino. The codebase is identical for both except that in one Arduino, window = LEFT_WINDOW
and in the other window = RIGHT_WINDOW
. The right window consists of the first four bits (0–3
) while the left window consists of the last four bits (4–7
). This corresponds to our byte structure:
_____ ______ left \ / right0b1111 0000
So now we can get the value for each circle.
Unfortunately, it is not as simple as that. The LEDs snake around an S-shaped path starting from the floor with the Arduino up to the ceiling.
o-o-o-o o-o-o-o | |o-o-o-o +---+ o-o-o-o| | | |o-o-o-o | -| o-o-o-o | | | | <- Arduino & Power left door right
If we look at the left window, we can see the bottom row moves from right to left; the middle row moves left to right; and finally, the top row moves right to left. Not only that, but our right window does the complete opposite.
o-<-<-o o->->-o | |o->->-o +---+ o-<-<-o| | | |o-<-<-o | -| o->->-o | | | | <- Arduino & Power left door right
Because of this, our shouldFade function is actually a lot more complex. Our algorithm needs to account for the direction of movement in one window and the opposite direction in the other window.
Our final shouldFade
function looks like this.
bool shouldFade(uint8_t f, uint8_t row, uint8_t column) { uint8_t offset = window == LEFT_WINDOW ? 4 : 0; if ((window == LEFT_WINDOW && row != 1) || (window == RIGHT_WINDOW && row == 1)) { // right to left return bitAtK(pattern.frames[f * NUM_ROWS + (2 - row)], column + offset); } else { // left to right return bitAtK(pattern.frames[f * NUM_ROWS + (2 - row)], (3 - column) + offset); }}
Let’s break apart the conditional (window == LEFT_WINDOW && row != 1) || (window == RIGHT_WINDOW && row == 1)
. We can see in the comments for the conditional that if our condition evaluates to true, we are calculating the value based on a right-to-left direction. If the condition evaluates to false, we calculate the value based on a left-to-right direction.
When are we expecting motion from right to left? If the window is the left window and we are not the middle row or if the window is the right window and we are looking at the middle row.
The rest of the code for the project can be found in the store display repository on Github.
Animation patterns were designed by the crew here at Foster Made using a quick tool I updated from one of my old projects: http://fm-window.shmah.com/. The tool gave us a quick way to easily play around with these bits and bytes without worrying about playing around with bits and bytes. The tool’s gui exports the byte arrays in the background.
Nina and I worked on building and designing each circle, iterating over different ideas and thinking through the best materials for each part of the display. While building out the wooden circles, Nina thought up the brilliant idea of writing “Let’s Make Something Awesome” within the circles. The phrase filled the 24 circles perfectly, and, to our great relief, all of the puzzle pieces fit together during installation without any hitches.
In the future, we plan to make even more awesome work bridging the gap between software and the real world. With our store display, we are excited to bring more interactivity to make our installations even more awesome! So be on the lookout!
Great things start with a conversation
Want to stay in the loop? Sign-up for our quarterly newsletter and we’ll send you updates with a mix of our latest content.
Industries
©Copyright 2024. All Rights Reserved.Made with ♡ in Richmond, VA.