forked from matesh/oes_matrix_challenge
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5f4e00d
Showing
6 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# Odyssey LED martix challenge introduction | ||
Show us what you can throw together in an hour! You will write code to draw on my LED matrix. | ||
It is a 17x17 matrix built out of WS812b LED pixels, which can be individually addressed and are | ||
able to emit 16 million colours. Your work will be judged by (and only by) the non-coders in the | ||
company, so impress them, not get stuck on your code style! | ||
|
||
# Requirements | ||
The code is written in python 3 and the required library (Pillow) that you need to install is in | ||
the requirements.txt file. This is not necesary, but is used to render text in a helper method. | ||
If you want to avoid installing it, just comment out the relevant code in `renderer_common.py` | ||
|
||
# boilerplate.py | ||
Your starting point, contains the example code. The code will be ran on a raspberry pi that | ||
controls the LED matrix. As you can see, the renderer runs in a separate process, so you will | ||
have an entire raspberry pi 3 processor core to use for your image generation. | ||
|
||
Put your code between the try: except sturcture, everything currently there can be removed. | ||
|
||
## General recommendations: | ||
- It is recommended to use the sleep routine in the boilerplate code for a stable and persistent | ||
frame rate. | ||
- If the code runs poorly, the frame rate can be lowered | ||
- The LED panel is pretty bright, I recommend dimming colours/pixels using the provided, it will come | ||
though better on camera as well. A Brightness between 30 and 60 gives enough flexibility with colours | ||
without being too aggressive. For the full 16 million colours, don't dim, but bear the consequences :) | ||
- The thing runs on a £10 chinesium power supply, avoid turning on all LEDs at with all colours at | ||
full brightness | ||
- If you are animating, it is cheaper in terms of hardware resources, to turn of pixels that are not needed | ||
in the next frame, instead of rendering an empty frame (clear screen) between frames. Sending the LED strip | ||
data through i2c takes time and it may delay the rendering of the next frame causing frame drops. | ||
|
||
# renderer_common.py | ||
This contains constants and functions used by both the renderers and the code using the renderer. | ||
Here you can find the definition of the LED panel, some constants for easier management (clearing) | ||
|
||
`dim_individual_colour` allows the brightness to be applied to just a colour value (red or green for example) | ||
|
||
`dim` will dim a colour tuple or an individual colour (interchangeable) | ||
|
||
Pixels are custom named tuples as defined here Pxels are to be defined by the ID of the LED pixel on the strip | ||
and a colour, which is also a tuple. This provides flexibility for various effects, like the random | ||
pixels in the example. The ID of a led pixel can be resolved from it's coordinates using the `get_pixel_index` function. | ||
|
||
Colours are to be defined as a named tuple defined here. | ||
|
||
# TerminalRenderer.py | ||
This is a helper that allows the drawn picture to be rendered as ASCII characters in the terminal. | ||
It's features are identical to the other renderers that I use live, so if your code runs in the terminal, | ||
it should run on the matrix as well. I tested this renderer on multiple computers on macOS and Windows and | ||
worked just fine, though it may not render perfectly smoothly in the terminal depending on the speed | ||
of your computer. | ||
|
||
# Submitting your code | ||
- Any additional requirements/libraries your code needs, please include in the `requirement.txt` | ||
- Create a pull request with your code | ||
- Make the result pretty, not your code, this is not a coder dick weaving, you need to impress | ||
the non-coders! :) | ||
- Please submit your code an hour before the kickoff at latest, so I have time to deploy, test and | ||
optimise if needed be! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import multiprocessing | ||
from renderer_common import LED_COUNT, CLEAR_FRAME_INDEX, ROWS, COLUMNS | ||
from traceback import print_exc | ||
import os | ||
|
||
|
||
class TerminalRenderer(multiprocessing.Process): | ||
def __init__(self, render_pipe, stop_signal, stopped_signal, render_delay=None): | ||
super(TerminalRenderer, self).__init__() | ||
|
||
self.render_pipe = render_pipe | ||
self.stop_signal = stop_signal | ||
self.stopped_signal = stopped_signal | ||
self.brightness = 255 | ||
|
||
self.unsuccessful_renders = 0 | ||
self.led_strip = [0] * LED_COUNT | ||
|
||
def run(self): | ||
try: | ||
while not self.stop_signal.is_set(): | ||
if self.render_pipe.poll(0.05): | ||
frame = self.render_pipe.recv() | ||
for pixel in frame: | ||
if pixel.index == CLEAR_FRAME_INDEX: | ||
self.led_strip = [0] * LED_COUNT | ||
else: | ||
if pixel.colour.green == 0x0 and pixel.colour.red == 0x0 and pixel.colour.blue == 0x0: | ||
self.led_strip[pixel.index] = 0 | ||
else: | ||
self.led_strip[pixel.index] = 1 | ||
|
||
self.print_matrix() | ||
|
||
except KeyboardInterrupt: | ||
pass | ||
except Exception: | ||
print("Exception in renderer") | ||
print_exc() | ||
finally: | ||
print("Terminating renderer") | ||
self.stopped_signal.set() | ||
|
||
def print_matrix(self): | ||
os.system('cls' if os.name == 'nt' else 'clear') | ||
frame = "" | ||
for row in range(0, ROWS): | ||
line = "{:2}|".format(row) | ||
for pixel in range(row*COLUMNS, (row+1)*COLUMNS): | ||
line += '*|' if self.led_strip[pixel] == 1 else " |" | ||
frame += line + os.linesep | ||
print(frame) | ||
|
||
|
||
def initialise_renderer(render_delay=None): | ||
render_pipe_out, render_pipe_in = multiprocessing.Pipe(False) | ||
terminate_renderer = multiprocessing.Event() | ||
renderer_terminated = multiprocessing.Event() | ||
renderer = TerminalRenderer(render_pipe_out, terminate_renderer, renderer_terminated, render_delay=None) | ||
renderer.daemon = True | ||
renderer.start() | ||
return renderer, render_pipe_in, terminate_renderer, renderer_terminated |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import traceback | ||
import random | ||
import time | ||
import sys | ||
from renderer_common import Colour, Pixel, ROWS, COLUMNS, LED_COUNT, CLEAR_FRAME, dim, draw_rectangle, render_text, get_pixel_index | ||
|
||
# Renderer selection according to argument. By default, it's the terminal renderer, other renderers will be used live. | ||
if "pi" in sys.argv: | ||
from RaspberryPiRenderer import initialise_renderer | ||
elif "nightdriver" in sys.argv: | ||
from NightDriverRenderer import initialise_renderer | ||
else: | ||
from TerminalRenderer import initialise_renderer | ||
|
||
|
||
def get_random_pixel(): | ||
""" | ||
returns a random pixel ID | ||
:return: | ||
""" | ||
return random.randint(0, ROWS * COLUMNS - 1) | ||
|
||
|
||
# Brightness 1 so it doesn't burn my retina | ||
BRIGHTNESS = 1 | ||
|
||
# Framerate in FPS | ||
FRAMERATE = 20 | ||
FRAME_TIME = (60/FRAMERATE)/60 | ||
|
||
# White colour just for the demonstration | ||
COLOUR = Colour(0xFF, 0xFF, 0xFF) | ||
|
||
|
||
if __name__ == "__main__": | ||
|
||
shutdown = False | ||
|
||
# Renderer initialisation | ||
renderer, render_pipe, terminate_renderer, renderer_terminated = initialise_renderer(render_delay=2) | ||
|
||
try: | ||
|
||
# Putting a single red pixel on the LED matrix using x, y coordinates | ||
# Defining the red colour | ||
colour = Colour(0xFF, 0x00, 0x00) | ||
|
||
# Defining the coordinates of the pixel. The top left pixel of the matrix is 0,0 and | ||
# currently it has 17 rows and 17 columns (rows 0-16 and columns 0-16) | ||
x = 3 | ||
y = 13 | ||
|
||
# Defining the actual pixel | ||
pixel = Pixel(get_pixel_index(x, y), colour) | ||
|
||
# Defining a frame, which is a list of pixels. | ||
# A frame (list of pixels) is the unit that can be rendered. | ||
frame = [pixel] | ||
|
||
# Send the frame for rendering | ||
render_pipe.send(frame) | ||
|
||
# Draw two rectangles and wait 3s. Shows that putting a new frame in the render pipe doesn't | ||
# clear the empty pixels in the new frame | ||
render_pipe.send(draw_rectangle(3, 3, 5, 8, dim(COLOUR, BRIGHTNESS))) | ||
render_pipe.send(draw_rectangle(9, 9, 3, 6, dim(COLOUR, BRIGHTNESS), fill=True)) | ||
time.sleep(3) | ||
|
||
# Clearing matrix by putting the pre-defined CLEAR_FRAME into the render pipe | ||
render_pipe.send(CLEAR_FRAME) | ||
|
||
# Render the text "Hello world" on the board | ||
render_text("Hello world", render_pipe, dim(COLOUR, BRIGHTNESS)) | ||
time.sleep(1) | ||
|
||
# Clearing matrix by putting the pre-defined CLEAR_FRAME into the render pipe | ||
render_pipe.send(CLEAR_FRAME) | ||
|
||
# Continuously running random pixel pattern thing | ||
active_pixels = [] | ||
MAX_ACTIVE_PIXELS = 50 | ||
|
||
counter = 0 | ||
while not shutdown: | ||
start = time.time() | ||
pixels_to_render = [] | ||
pixel = get_random_pixel() | ||
while pixel in active_pixels: | ||
pixel = get_random_pixel() | ||
active_pixels.append(pixel) | ||
pixels_to_render.append(Pixel(pixel, dim(COLOUR, BRIGHTNESS))) | ||
if MAX_ACTIVE_PIXELS < len(active_pixels): | ||
turn_off_pixel = active_pixels.pop(0) | ||
pixels_to_render.append(Pixel(turn_off_pixel, Colour(0x00, 0x00, 0x00))) | ||
render_pipe.send(pixels_to_render) | ||
|
||
# Recommended wait between frame renders to maintain a stable FPS and allow overruns | ||
try: | ||
time.sleep(FRAME_TIME - (time.time() - start)) | ||
except Exception: | ||
pass | ||
|
||
print("Terminating") | ||
render_pipe.send(CLEAR_FRAME) | ||
|
||
except KeyboardInterrupt: | ||
pass | ||
except Exception: | ||
print("Exception in main loop") | ||
traceback.print_exc() | ||
finally: | ||
print("Terminating renderer") | ||
terminate_renderer.set() | ||
while not renderer_terminated.is_set(): | ||
time.sleep(0.1) | ||
print("All processes stopped") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
from collections import namedtuple | ||
from PIL import Image | ||
from PIL import ImageFont | ||
from PIL import ImageDraw | ||
import time | ||
|
||
# Use this special pixel index to trigger a fast and efficient clear (turn off) of all pixels on the board | ||
CLEAR_FRAME_INDEX = 0XBEEF | ||
|
||
# This is used to define a colour. Colours are integers between 0x00-0xFF (0-255), as per the hex | ||
# colour coding definition. Purple for instance is 0xff00ff -> Colour(0xFF, 0x00, 0xFF) | ||
Colour = namedtuple("Colour", ["red", "green", "blue"]) | ||
|
||
# This is used to define a pixel that is to be inserted into the render queue. | ||
# Index is its position on the matrix (more precisely it's index in the led strip) | ||
# Colour is the colour tuple that contains the hex of the 3 base colours | ||
Pixel = namedtuple("Pixel", ["index", "colour"]) | ||
|
||
# Render this pixel to trigger a fast and efficient clear (turn off) of all pixels on the board | ||
CLEAR_FRAME_PIXEL = Pixel(CLEAR_FRAME_INDEX, None) | ||
|
||
# Render this frame to trigger a fast and efficient clear (turn off) of all pixels on the board | ||
CLEAR_FRAME = [CLEAR_FRAME_PIXEL] | ||
|
||
# Turned off pixel colour | ||
DARK = Colour(0x00, 0x00, 0x00) | ||
|
||
# An example pixel that can be inserted into the render pipe for rendering | ||
EXAMPLE_PIXEL = Pixel(2, Colour(0x00, 0x01, 0x00)) | ||
|
||
# Current LED board size definition | ||
ROWS = 17 | ||
COLUMNS = 17 | ||
LED_COUNT = ROWS * COLUMNS | ||
|
||
|
||
def dim_individual_colour(colour, brightness): | ||
""" | ||
Dims an individual colour | ||
:param colour: int 0-255 | ||
:param brightness: brightness 0-255 | ||
:return: dimmed colour | ||
""" | ||
new_colour = (float(brightness) / 255) * float(colour) | ||
if new_colour < 0: | ||
return 1 | ||
if 255 < new_colour: | ||
return 255 | ||
return int(new_colour) | ||
|
||
|
||
def dim(colour, brightness): | ||
""" | ||
Dims a colour | ||
""" | ||
try: | ||
return Colour(dim_individual_colour(colour.red, brightness), | ||
dim_individual_colour(colour.green, brightness), | ||
dim_individual_colour(colour.blue, brightness)) | ||
except Exception: | ||
try: | ||
return dim_individual_colour(colour, brightness) | ||
except Exception: | ||
return colour | ||
|
||
|
||
def render_text(text, render_pipe_input, text_colour): | ||
""" | ||
Method to render text on the matrix | ||
:param text: The text to be rendered | ||
:param render_pipe_input: The render pipe in which the rendered frames are to be inserted | ||
:param text_colour: The colour to be used when rendering, Colour named tuple | ||
:return: | ||
""" | ||
font = ImageFont.truetype('UbuntuMono-B.ttf', 17) # load the font | ||
size = font.getsize(text) # calc the size of text in pixels | ||
image = Image.new('1', size, 1) # create a b/w image | ||
draw = ImageDraw.Draw(image) | ||
draw.text((0, -1), text, font=font) # render the text to the bitmap | ||
column = 0 | ||
while column + 17 < size[0]: | ||
start = time.time() | ||
render_pixels = [] | ||
for index, column_to_render in enumerate(range(column, column + 17)): | ||
for row_to_render in range(2, size[1]): | ||
if image.getpixel((column_to_render, row_to_render)): | ||
render_pixels.append(Pixel(get_pixel_index(index, row_to_render-1), DARK)) | ||
else: | ||
render_pixels.append(Pixel(get_pixel_index(index, row_to_render-1), text_colour)) | ||
render_pipe_input.send(render_pixels) | ||
if column == 0: | ||
time.sleep(.5) | ||
else: | ||
try: | ||
time.sleep(0.05 - (time.time() - start)) | ||
except Exception: | ||
pass | ||
column += 1 | ||
|
||
|
||
def draw_rectangle(x, y, a, b, colour, fill=False): | ||
""" | ||
Draws a rectangle | ||
:param x: top left corner x coordinate | ||
:param y: top left corner y coordinate | ||
:param a: horizontal side length | ||
:param b: vertical side length | ||
:param colour: the colour of the rectangle | ||
:param fill: fill, true or false | ||
:return: rectangle frame | ||
""" | ||
to_return = [] | ||
if fill: | ||
for i in range(0, a): | ||
for j in range(0, b): | ||
to_return.append(Pixel(get_pixel_index(x+i, y+j), colour)) | ||
else: | ||
for i in range(0, a): | ||
to_return.append(Pixel(get_pixel_index(x+i, y), colour)) | ||
to_return.append(Pixel(get_pixel_index(x+i, y+b-1), colour)) | ||
for j in range(1, b-1): | ||
to_return.append(Pixel(get_pixel_index(x, y+j), colour)) | ||
to_return.append(Pixel(get_pixel_index(x + a-1, y + j), colour)) | ||
return to_return | ||
|
||
|
||
def left_to_right_top_to_bottom(x, y): | ||
""" | ||
Transformation from x/y coordinate to an index of the pixel on the LED strip | ||
:param x: x coordinate | ||
:param y: y coordinate | ||
:return: the index on the LED strip | ||
""" | ||
if y < 0 or x < 0 or ROWS <= y or COLUMNS <= x: | ||
raise ValueError("Invalid coordinate)") | ||
return y * COLUMNS + x | ||
|
||
|
||
# Transformation appropriate for this LED setup | ||
get_pixel_index = left_to_right_top_to_bottom |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Pillow |