Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
matesh committed Mar 10, 2022
0 parents commit 5f4e00d
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 0 deletions.
59 changes: 59 additions & 0 deletions README.md
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!
62 changes: 62 additions & 0 deletions TerminalRenderer.py
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 added UbuntuMono-B.ttf
Binary file not shown.
116 changes: 116 additions & 0 deletions boilerplate.py
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")
140 changes: 140 additions & 0 deletions renderer_common.py
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
1 change: 1 addition & 0 deletions requirement.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pillow

0 comments on commit 5f4e00d

Please sign in to comment.