Mitchell Vitez, 2018
In which I create a custom programming language just to make lights turn pretty colors
Pixll is a tiny infinite-array generation and transformation language used for creating one-dimensional lightshows. The name came from "raspberry PI X-mas Light Language".
Take a look at test.pxl
. You can compile this file by running ./PixllCompiler test.pxl > test.pxl-compiled.py
There are only three kinds of declarations in Pixll (as well as comments which are just lines beginning with #
): arrays, procedures, and transformations.
These begin with the keyword array
followed by the name of the array. Then there are two array names separated by a "stars and bars" pattern. This pattern provides a way to combine the arrays on either side. Read the pattern from left to right. Every time you see a bar, fill in the next element of the left-hand array. Every time you see a star, fill in the next element of the right-hand array. This pattern cycles infinitely. In this way, very complicated arrays can be built up from interactions betwen simpler ones.
# An array where every third pixel is white
array myArray
black --* white
# An array with pattern B R B W R B B R W B R B W R...
array combinedArr
myArray -*- red
There are primitive arrays for all the colors in raspilights.py
(e.g. white, black, red, orange, etc.). There are also two special primitive arrays: colors
, which has the color wheel in rainbow order, and randomcolors
, which generates an infinite array of random color values.
Another kind of code block in Pixll denotes a procedure. These have a name, followed by an initial array with syntax >
in front, then a list of transformations that will be done to the array on each step. You can optionally denote how many steps will run and how long each step will be (in seconds) by adding <steps>@<seconds_per_step>
after the procedure name. The default is 20 steps at 0.2 seconds per step.
# A procedure which starts with the array above and at each step shifts the hue by 12 colors, and rotates the array by 1 pixel
myProcedure
> myArray
shifthue 12
rotate 1
# A white array that gets darker each step. Happens in 10 steps at 1/10 second per step
fadeToBlack [email protected]
> white
darken 20
Each transformation takes a single integer argument. Some of the included library transformations are:
rotate
- rotates array by that number of pixelsdarken
- takes an array and darkens each member of itbrighten
- takes an array and brightens each member of itshifthue
- cycles through colors (runs through hues on color wheel)invert
- flips every color to its opposite
Finally, you can write your own simple transformations directly in Pixll. These take a color (r, g, b) and an argument (n). Each one begins with the keyword transform
, and the name of the transformation. Following this are three lines, one each for red, green, and blue. When your transformation runs, it will run the operations on each of these lines and create a new color from the passed-in r, g, b, and n values. Each value is then automatically clamped to the range [0, 255].
For a full explanation of supported operations, please see Appendix A.
transform increaseRed
r + n
g
b
transform swapRedBlue
b
g
r
You can also write your own more complicated transformations in Python. You'll need to call array.transform()
with a function that maps colors to colors. In Python, you can do this with a nested function that takes and returns an (r, g, b)
tuple. Then the outer function can call array.transform(innerFunction)
. Don't forget to import your custom transformations after you've written them.
def increaseRed(array, arg):
def _increaseRed(c):
r, g, b = c
new_r = min(255, r + arg)
return (new_r, g, b)
array.transform(_increaseRed)
return array
Pixll is intended to run continuously (you wouldn't want your lights suddenly shutting off!). At runtime, it takes all the user-defined procedures, and selects a random one. It also chooses randomly whether to display the LEDs in forward or reverse order (transformations will also be reversed). It then runs that selected procedure for a set time, then loops back and chooses another procedure to run.
You now know about all there is to know about Pixll! (It was lots of fun designing a tiny special-purpose language like this)
The Pixll compiler is a small Haskell parser/printer that transpiles Pixll to Python.
You can build the compiler with ghc PixllCompiler.hs
. Compilation takes a filename as the first argument and prints the result to stdout.
This is a very simple compiler. It essentially parses a .pxl
file to a syntax tree, converts each relevant element of that tree to Python code, and generates a little extra Python code that acts to select random functions from the .pxl
file to keep the lights running.
raspilights.py
is a Python library designed to be used as either a standalone driver for driving LED strips in python (as in light_patterns.py
), or as an included library in compiled Pixll files (as in test.pxl-compiled.py
).
raspilights has both hardware and software modes, which can be set via the respective flags in raspilights.py
. In hardware mode, the library sends GPIO output from a Raspberry Pi to drive a WS2812B LED strip. In software mode, the library prints arrays of colored rectangles to stdout, simulating what would happen if your code were hooked up to a hardware LED strip.
The number of pixels you have to play with is settable via PIXEL_COUNT
. I've found that around 80 works well in software mode, and in hardware mode set this to the number of LEDs on the physical LED strip (in my case, 300).
The library supports the following functions, where i
is an integer from 0 up to PIXEL_COUNT
, and colors are handled internally as (red, green, blue) tuples of integers between 0 and 255.
get_color(i)
- the color of a pixel at index iall_pixels()
- a range of indices from [0,PIXEL_COUNT
)clear()
- resets all pixels to (0, 0, 0)all_colors()
- a list of 256 colors spanning the rainbowrandom_color()
- a color with independently random r, g, and b elementsset_pixel(i, color)
- set the pixel at index i to the given colorset_all_pixels(color)
- sets every pixel to the given colorshow(seconds)
- needs to be called to see pixels light up. waits for the given number of seconds before continuingreversed_show(seconds)
- like show, but puts out pixels in reverse order
The library also supports many named color constants (BLACK
, WHITE
, AUBERGINE
, etc.) which can be found by reading the source or looking at the default colors demo in light_patterns.py
. Each color has a corresponding infinite Array. (The array names are lowercase, whereas the constants are uppercase)
In raspilights.py
, an Array is an infinite generator that keeps the first PIXEL_COUNT
elements around for easy access. This lets us generate infinite lists of pixels and transform them in various ways. These are then used by the Pixll language to easily create and manipulate infinite Arrays.
This project also comes with a small web app used for switching between different light patterns on the fly. Once you have Flask installed, just run python app.py
on your Pi and go to localhost:8000
with a device on your LAN. You'll see a list of buttons, which when clicked will switch the current light pattern to the one selected. You can add more patterns to this list by editing the procedures
dictionary in app.py
.
Let x
stand for any valid Pixll transformation expression. As well as integer literals, the following operations are valid (listed in precedence order):
Pixll | Python | Name |
---|---|---|
(x) |
(x) |
parenthesization |
-x |
-x |
unary negation |
sqrt x |
sqrt(x) |
square root |
int x |
int(x) |
cast to integer |
x^x |
x**x |
exponentiation |
x*x |
x*x |
multiplication |
x/x |
x/x |
floating point division |
x//x |
x//x |
integer division |
x%x |
x%x |
modulus |
x+x |
x+x |
addition |
x-x |
x-x |
subtraction |
r |
lambda (r, g, b): r |
red component |
g |
lambda (r, g, b): g |
green component |
b |
lambda (r, g, b): b |
blue component |
n |
def transform(n): ... n |
transform argument |
Note that sqrt x
and int x
bind tightly. That is to say sqrt x^3
compiles to sqrt(x)**3
, not sqrt(x**3)
.