Message Board Programming

Post Image
Posted on Updated

So far we’ve gotten most of the grunt work out of the way: we’ve designed the LED message board and got the circuit up and running. I can happily say the next portion of the project doesn’t require any soldering! Instead, we’ll be entering the world of Python programming. If you’re not much of a programmer, don’t worry — I’ve supplied everything you’ll need for this project here. You will, however, need to know basic object-oriented programming principles and understand Python syntax. Review the Readme.md of the project to see how to get the message board working for your specific setup. For an explanation of the code and how I wrote all of it, continue reading the secitons below.


Objective

Now that we have a working circuit, we need to start thinking about how we want to design the program. We need to design it in such a way that it is easily applicable to any message board, any message, any coloring scheme, etc. A couple of things that come to mind are the following:

  • How can we write the board for any size? While it would be nice if we knew that everyone who uses this program will be using a 5×48 LED Message Board, this is not the case. We need to write the program to be as robust as possible and handle multiple sizes.
  • How are we going to animate the sign? While the most logical animation is to move the words from left to right for easy readability, we could just as easily drop the words from the top and then scroll the words to the left when they are in full view. We shouldn’t limit ourselves to a single type of animation.
  • How are we going to choose the colors of the words? It would be very simple to choose a single color to display all words on the board, but it wouldn’t be that interesting. It also wouldn’t be that interesting to choose between a select amount of colors. We have to design a system that allows us to dynamically change the colors of the board.

Keeping all of the above in mind, we will proceed to design a program that is easily adjustable. This list doesn’t cover all the bases, but it is a good starting point. The more robust the program is, the wider variety of animations, coloring, and board sizes we will have available to us.

Prerequisites

Note: The following assumes you are running Jessie Raspbian on a Raspberry Pi 3.

The following article assumes you have some programming experience, understand object-oriented programming principles, and can read Python. If you do not have some of these skills you may find it difficult to follow along. While I write this article, I do not explain syntax specific to Python and assume you have the ability to navigate a Python program. For the most part, the first section of this article teaches you how to use the following Python libraries, while the latter section teaches you common design patterns for object-oriented programming.

Before we start programming, we need to install two Python libraries and Python 3. The first of the libraries is necessary only if you are using an LED strip of the form WS281X, where X represents any digit (e.g. WS2812). You can download the rpi_ws281x library here. This will install drivers the raspberry pi needs to communicate with the strip. The second Python library to install is BiblioPixel, which wraps around the WS281X driver and makes it much easier to control the LEDs. It also packages it’s own drivers we will need if we want to communicate with an LPD8806 or an APA102 model strip.

1. Installing Python 3

A Raspberry Pi 3 running Jessie should already have Python 3 installed. You can check by typing the following on the command line:

tylermckay@rPi$ which python3
/usr/bin/python3

If you get a path as an output, such as above, then it is already installed. If you get something like this:

tylermckay@rPi$ which python3
/usr/bin/which: no python3 in (...)

Then you will need to install Python 3. To install python3, use the following command:

tylermckay@rPi$ sudo apt-get install python3

And follow the onscreen instructions to continue the install.

Note: Python 3 is most likely already installed on your pi. It goes under a different command python3 vs. the traditional python command. The traditional python command is actually Python 2 on the raspberry pi.

2. Installing the rpi_ws281x Library

Note: If you do not have a WS2811 or WS2812 strip, you can move onto Installing BiblioPixel.

Go ahead and download the rpi_ws281x library. This is actually a C library, which has a Python wrapper. I’ve downloaded it in my home directory as below:

tylermckay@rPi$ ls -l
drwxr-xr-x 2 tylermckay tylermckay 4096 Aug 18 19:32 rpi_ws281x

You will aslo need scons to install the library, so type the following to install everything:

tylermckay@rPi$ cd rpi_ws281x
tylermckay@rPi$ pwd
/home/tylermckay/rpi_ws281x

tylermckay@rPi$ sudo apt-get install python-dev swig scons
(follow onscreen instructions to install scons)

tylermckay@rPi$ sudo scons
(use scons command in current directory)

tylermckay@rPi$ cd python
tylermckay@rPi$ pwd
/home/tylermckay/rpi_ws281x/python

tylermckay@rPi$ sudo python3 setup.py build install
(install rpi_ws281x library)

Now the C module is installed, and we are able to utilize the library by importing neopixel. This is actually all we need to start controlling the LEDs for this model strip, but we’ll need to install BiblioPixel for its ease of use and it’s cross-platform capabilities.

Note: Since the rpi_ws281x library requires a PWM signal, it must use the PWM GPIO pin (GPIO 18). This means you will be unable to output audio from that specific PWM GPIO pin (you cannot use two different PWM signals at the same time from the same pin), unless you add a USB sound card or other hardware.

3. Installing BiblioPixel

You are able to install BiblioPixel with one line as below:

tylermckay@rPi$ sudo pip3 install BiblioPixel

If you later run into issues finding the bibliopixel library, then try the same command without sudo. If you don’t have the pip3 command, you can install it with the following command:

tylermckay@rPi$ sudo apt-get install pip3

Now that we have BiblioPixel installed, we can control multiple model LEDs! Plus this library is not only useful for designing a message board, but also anything from lighting up a simple strip to displaying an image to the board.

Note: You should already have pip3 installed on your Pi. The Pi should have two types of Python install commands, pip and pip3. The regular pip command installs Python 2 modules, while the pip3 command installs Python 3 modules.

Orienting the Board

Great! It looks like we’re ready to start programming now that everything is installed. This section will deal with testing out some of the libraries we’ve installed, making sure everything works as expected. Then in the next section, we’ll actually start writing the full program to write messages to the board.

Let’s create a file called animateMessageBoard.py and change the permissions to be executable:

tylermckay@rPi$ touch animateMessageBoard.py
tylermckay@rPi$ chmod 0755 animateMessageBoard.py

The Basics

Let’s see if we are able to use the BiblioPixel library correctly. Open up animateMessageBoard.py in your favorite text editor and enter the following:


from bibliopixel.layout import *
from bibliopixel.animation import MatrixCalibrationTest
from bibliopixel.drivers.PiWS281X import *

#create driver
height = 5 
width  = 48 
driver = PiWS281X(height*width)
led    = Matrix(driver,width,height)

#run animation
anim = MatrixCalibrationTest(led)
anim.run()
						

Make sure to change the height and width to the appropriate dimensions for your board. If you are using a different model LED, you’ll need to import the appropriate library for you model and change the driver. E.g. if you are using an LPD8806 LED strip, it would look like this:


from bibliopixel.layout import *
from bibliopixel.animation import MatrixCalibrationTest
from bibliopixel.drivers.LPD8806 import *

#create driver
height = 5 
width  = 48 
driver = LPD8806(height*width)
led    = Matrix(driver,width,height)

#run animation
anim = MatrixCalibrationTest(led)
anim.run()
						

The above code sample creates a Matrix object that takes a driver object as an argument. The driver is pretty straight forward — it’s initialized with the total amount of LEDs for that specific LED strip. The real work from BiblioPixel comes with the Matrix() function. Let’s take a deeper look into the Matrix initializer:

Matrix(driver, width = 0, height = 0, coordMap = None, rotation = MatrixRotation.ROTATE_0, vert_flip = False, serpentine = True, threadedUpdate = False, brightness = 255, pixelSize = (1,1))

The first 7 arguments are the most important, so I will go over them now. If you would like to know what the other arguments do, check out the documentation.

  • driver – This is the only required object and represents the model LED strip we are using. You can see the full list of drivers available here.
  • width – The number of columns in our message board. In my case, this is 48. If you leave this blank, the Matrix object will fill this in assuming the board is a square.
  • height – The number of rows in our message board. In my case, this is 5. If you leave this blank, the Matrix object will fill this in assuming the board is a square.
  • coordMap – This is a mapping that will fill in the index of each LED. We will leave this blank, and use the standard mapping. This would only be necessary if you were to create a board with 4 different sections and draw individual words/graphics to those objects. This would also be necessary if you set up the connection from one strip to another in an inconsistent fashion.
  • rotation – Indicates the rotation needed to orient the board properly.
  • vert_flip – Indicates if the orientation should be flipped upside down.
  • serpentine – This indicates if the message board snakes around the board: the end of the first row leads to the end of the second row, the beginning of the second row leads to the start of the third row, and so on. If you set the board up properly, this should be true. If you did what I did, however, and accidentally set the board up in reverse, then this should be false.

We need to set this function up so each LED gets the proper mapping. At the end of the day, we want a coordinate of (3,1) to map to the LED in the 4th column and the 2nd row (indexing starts at 0). For example, say we setup a 3×5 LED board with the order in a serpentine fashion as so:

Coordinates: (X,Y)

     0  1  2  3  4
0 |[[1, 2, 3, 4, 5],
1 |[10, 9, 8, 7, 6],
2 |[11,12,13,14,15]]

Where each number within the matrix represents the number LED in the strip from the start of the connection to the end of the connection. Coordinates (3,1) should map to LED #7; however, if you setup the Matrix() function with the serpentine parameter set to False, then the Matrix function would map the LEDs as so:

Coordinates: (X,Y)

     0  1  2  3  4
0 |[[1, 2, 3, 4, 5],
1 |[ 6, 7, 8, 9,10],
2 |[11,12,13,14,15]]

In this case coordinates (3,1) would map to LED #9, but our LED #9 is acutally coordinates (1,1)! You can see the problem here. If we wanted to light up coordinates (3,1) with a color, say green, then we would actually be lighting up coordinates (1,1) with green. If you could apply this example to a board with any number of rows, you’ll notice every even number row will start coloring from the oppisite direction.

Okay, let’s backtrack a bit to the file we just created, animateMessageBoard.py. Let’s try running the program by using the following command:

tylermckay@rPi$ sudo python3 ./animateMessageBoard.py

If all went well, you’re board is lighting up like a Christmas tree (not really)! You should see something that looks like:

If you saw the above pattern created from your board, you can move onto the next section. If you didn’t see the above pattern because it is oriented incorrectly, keep reading to correct your orientation.

Rotating the Orientation

If you have something that looks like this:

You’ll need to adjust the rotational orientation of the board. The bibliopixel.layout.Rotation class contains 4 constants we can use to rotate the orientation:

class Rotation:

ROTATE_0   = 0  # no rotation
ROTATE_90  = 3  # rotate 90 degrees
ROTATE_180 = 2  # rotate 180 degrees
ROTATE_270 = 1  # rotate 270 degrees

To fix the orientation in the above example, we would have to rotate the orientation 90° clockwise. That would result in the following code:

led = Matrix(driver, width, height, None, MatrixRotation.ROTATE_90)

Flipping the Orientation

The above section shows you how to correct an orientation that needs to be rotated, but what would you do if you had something that looks like this:

You’ll need more than a rotation! In this case, we’ll need to flip the orientation upside down, as well as rotate the orientation 180°. That would provide a solution:

led = Matrix(driver, width, height, None, MatrixRotation.ROTATE_180, True)

Writing the Program

If you’ve gotten to this point, you’re board should be properly outputting a default bibliopixel animation MatrixCalibrationTest(). This is a nice step forward, but we still have yet to draw a single letter to the screen. Maybe you want to just download the program already and be done with it. If so, go ahead. If you want to see how the program works though, let’s start by making an animation.

Writing an animation

Make a file called MessageBoardAnimation.py, which should have the following contents:

from bibliopixel.layout import *
from bibliopixel.animation import BaseMatrixAnim
import time

class MessageBoardAnimation(BaseMatrixAnim):

    def __init__(self, led):
        #The base class MUST be initialized by calling super like this
        super(BaseMatrixAnim, self).__init__(led)

    # override to write out word for each frame in the animation
    def step(self, amt = 1):

    	color = (255,0,255)
    	self._led.set(self._step,1,color)

        self._step += amt

        time.sleep(0.2)

    	if self._step == self._led.width:
    		exit(0)

Now we have an animation file. Before we start creating an unwieldy amount of files in one directory, let’s create some structure. Create a directory messageBoard, and, within this directory, create another directory animations. Put MessageBoardAnimation.py in this directory. Also create a another file __init__.py for our current working directory. The whole process looks something like this:

tylermckay@rPi$ ls -l
-rwxr-xr-x 1 tylermckay  tylermckay   769B Aug  22 21:19 animateMessageBoard.py
-rwxr-xr-x 1 tylermckay  tylermckay   130B Aug  22 21:19 MessageBoardAnimation.py

tylermckay@rPi$ mkdir messageBoard
tylermckay@rPi$ mkdir messageBoard/animations
tylermckay@rPi$ mv MessageBoardAnimation.py messageBoard/animations/
tylermckay@rPi$ touch __init__.py
tylermckay@rPi$ ls -l
-rwxr-xr-x 1 tylermckay  tylermckay   769B Aug  22 21:19 animateMessageBoard.py
-rw-r--r-- 1 tylermckay  tylermckay   0B   Aug  22 21:19 __init__.py
drwxr-xr-x 1 tylermckay  tylermckay   130B Aug  22 21:19 messageBoard/

The __init__.py file just signals to Python that we are going to import files from this directory and its subdirectories. Let’s quickly go over our MessageBoardAnimation.py file. It inherits from the BaseMatrixAnim class that bibliopixel provides. This class is initialized by the led object just like the MatrixCalibrationTest class is, and it has one function it needs to override to run the animation, step(). This function takes in one argument besides self, called amt, which is the amount to increment the _step member.

The step() function is called every step of the animation, and the _step member is used to keep track of what part of the animation we are in. It is up to us to increment this variable. As you can see, all our code does is set a pixel color of the board by calling led.set(), wait 0.2 seconds, then increment _step. We end up setting one pixel of the board every iteration, at coordinates (self._step,1) with color (255,0,255), which looks like a purple light moving to the right until it hits the end of the board. At this point, the exit() function is called to terminate the program. Why don’t we try it out? Go back and edit animateMessageBoard.py, so we are importing and using our new animation correctly:


from bibliopixel.layout import *
from messageBoard.animations.MessageBoardAnimation import MessageBoardAnimation # import the new animation
from bibliopixel.drivers.PiWS281X import *

#create driver
height = 5 
width  = 48 
driver = PiWS281X(height*width)
led    = Matrix(driver,width,height)

#run animation
anim = MessageBoardAnimation(led) # swap in our animation
anim.run()
						

Run the program, and boom, you made an animation! A purple light just made its way across your board! Not much of an animation, but you get the point. You can easily change the code within the step() function of MessageBoardAnimation and you will be able to draw any animation you want.

Drawing a Letter

Let’s consider how we’re going to make a letter appear on the screen. How would we draw the letter “A”? Well, I have a 5×48 LED message board, so it has to be 5 pixels high. How wide should the letter be though? If my board is 48 pixels wide, I want to choose a width that fits a decent amount of characters on the screen at one time. If I wanted to write the sentence “Hello World!”, then that would require 12 characters, which seems like a reasonable amount of characters to display at one time on the screen. Since the board is 48 pixels wide and I want 12 characters displayed on the screen at one time, then characters should have a width of 4 pixels. Nice! But we’re not quite there.

We don’t want the characters squished next to each other. There should be some space separating the characters; otherwise, it would become hard to discern words. This leads us to put an empty column of unlit LEDs next to the character. Really, each character is 3 pixels wide, the 4th column of pixels being unlit LEDs. This is all for my board since it is 5×48 pixels wide. I will continue assuming your board is the same dimensions for now. Later I’ll show you how to customize it for your specific dimensions.

Let’s consider drawing the letter “A” again. Imagine the board is a 5×48 matrix, the coordinates of each entry corresponding to a single LED. The letter A can also be a matrix, except it’s not 5×48, it’s 5×4 (the last column being empty). Consider the nonempty part of letter “A”. This would result in a 5×3 matrix. If we drew the letter “A” using a 5×3 matrix, how would it look? Consider the matrix below, where a 1 corresponds to an LED that is ON and 0 corresponds to an LED that is OFF:

[[0,1,0],
 [1,0,1],
 [1,1,1],
 [1,0,1],
 [1,0,1]]

That’s a decent “A” if I do say so myself. Before we draw this letter to the board, let’s consider the animation. We want the letter “A” to start from the right side of the board and move to the left each step of the animation until it is off the left side of the board. It will only draw the first column of our “A” matrix after the first step, the first and second column after the second step, and the first, second, and third column after the third step to create the illusion the letter is moving to the left. This process will continue until the letter makes its way off the board.

The range of x and y values for the animation will be constant (the width and height of the board respectively), since we have to redraw the entire board each frame of the animation. We only need to create a mapping from the x value of the board to the x value of the letter “A” matrix.

This means to draw the letter “A”, we need to loop through the range of x values for the board. Within that loop, we need to loop through all the possible y values for the board. Finally, for each iteration in the inner loop, we will need to create a mapping from the x values of the LED board matrix to the letter “A” matrix, and set the LED for that particular mapping. Part of this looks like the following in the step() function:


    def step(self, amt = 1):

        # define variables
	word       = [[0,1,1,1,1],[1,0,1,0,0],[0,1,1,1,1]] # the matrix for letter "A"
        boardWidth = self._led.width
        maxStep    = boardWidth + len(word) # letter is off the board at this step
        xRange     = boardWidth
        yRange     = self._led.height

        # loop through range of x values in board
        for xBoard in range(xRange):
            # create mapping to x value of word
            xWord  = ...
            for y in range(yRange):
                # draw LED to the board

        self._step += amt

        time.sleep(0.2)

    	if self._step >= maxStep: # end animation when the letter is off the board
		exit(0)
           

We loop through all the possible values for the xRange and all the possible values for the yRange. After the loops, we increment the step function, set a delay, and then exit the program if we reach the end of the animation. All we have to do is fill in the blanks and create the mapping to the letter matrix and set the LED.

Let’s get one thing straight. The mapping only has a relationship between the x value of the board and the x value of the letter matrix word, since we are animating across the x-axis. Also, the relationship seems to be linear because the x coordinate of the letter matrix shifts a constant amount after each step, amt (in our case this is 1). This means the relationship looks like this: xWord = xBoard * scale + xWordOffset, where scale and xWordOffset are some values we need to figure out.

It’s easy to figure out scale is constant because if it weren’t, then you could imagine the letter would not shift to the left at a constant rate. As the steps increased, the letter would change speed as it shifted to the left. If you dig a little bit deeper, you’ll notice scale is actually 1. We know this because each column of the letter is next to one another, so the difference between a mapping from a position say k+1 of the board and a position k of the board should be 1. To illustrate:

xWordKP1 = scale * (k+1) + xWordOffset # mapping to the x coordinate of the word from the k+1 coordinate of the board
xWordK   = scale * (k)   + xWordOffset # mapping to the x coordinate of the word from the k coordinate of the board

xWordKP1 - xWordK = 1 # we know this because each column of the letter matrix is next to each other
=> scale * (k+1) + xWordOffset - (scale * (k) + xWordOffset) = 1 # substituting
=> scale * k + scale + xWordOffset - scale * k - xWordOffset = 1 
=> scale = 1

Okay, now we can reduce the mapping to xWord = xBoard + xWordOffset, which means we just need to figure out xWordOffset. From the start, we want the letter matrix to be just off the right side of the board. On the first step, we want to show a single column of the letter (x coordinate equal to 0), located at the last x position of the board (x coordinate equal to the width of the board – 1). If we calculate the xWordOffset, we will determine the starting xWordOffset is 1 – the width of the board:

# first step of the animation
xWord = xBoard + xWordOffset
=> 0 = (width of board - 1) + xWordOffset
=> (1 - width of board) = xWordOffset

After the second step, we want the start of the x position of the letter matrix to be at the x position of the board that is the second to last column (width of the board – 2). This shows the xWordOffset is 2 – the width of the board:

# second step of the animation
xWord = xBoard + xWordOffset
=> 0 = (width of board - 2) + xWordOffset
=> (2 - width of board) = xWordOffset

You can imagine this pattern will continue — for the third step xWordOffset will be 3 – the width of the board, for the fourth, 4 – the width of the board, and so on. This means xWordOffset = the current step - the width of the board. Let’s go back and modify our step() function to reflect this:


    def step(self, amt = 1):

        # define variables
	word         = [[0,1,1,1,1],[1,0,1,0,0],[0,1,1,1,1]] # the matrix for letter "A"
        boardWidth   = self._led.width
        maxStep      = boardWidth + len(word) # letter is off the board at this step
        xRange       = boardWidth
        yRange       = self._led.height
        xWordOffset  = self._step - boardWidth

        # loop through range of x values in board
        for xBoard in range(xRange):
            # create mapping to x value of word
            xWord  = xWordOffset + xBoard
            for y in range(yRange):
                # draw LED to the board

        self._step += amt

        time.sleep(0.2)

    	if self._step >= maxStep: # end animation when the letter is off the board
		exit(0)
           

We’re basically there, now all we need to do is draw the LED to the board. Before we do this I need to make a brief observation. There is a possibility that the mapping we get for the letter, xWord, does not exist in our letter matrix. This happens because we are not really mapping the coordinates to the correct letter matrix.

Since the letter starts off unseen to the right of the board, we should actually be mapping to a letter matrix that is padded with 0 entries the width of the board wide to correspond to unlit LEDs. Also, we would need to do the same thing to the end of the letter matrix to make the whole letter disappear. In our example that would mean our letter “A” matrix would be 5×99. The first 48 columns would be all 0 entries to correspond to the initial unlit LEDs, the next 3 columns would correspond to the actual letter “A” matrix we had before, and the final 48 columns would be all 0 entries to correspond to the unlit LEDs that trail the letter as it makes its way off the board.

All this means is that when we light the LED up for the xWord and y coordinates we were given, we need to check that the coordinates exist in our letter matrix. If they don’t, then we will draw an unlit LED. If they do, then we’ll check the entry in the matrix and determine if it should be lit or unlit. Our step() function now looks like this:


    def step(self, amt = 1):

        # define variables
	word         = [[0,1,1,1,1],[1,0,1,0,0],[0,1,1,1,1]] # the matrix for letter "A"
        boardWidth   = self._led.width
        maxStep      = boardWidth + len(word) # letter is off the board at this step
        xRange       = boardWidth
        yRange       = self._led.height
        xWordOffset  = self._step - boardWidth
        color        = (255,0,0) # color to fill the lit LED (red)
        emptyColor   = (0,0,0)   # color to fill the unlit LED (no color)

        # loop through range of x values in board
        for xBoard in range(xRange):
            # create mapping to x value of word
            xWord  = xWordOffset + xBoard
            for y in range(yRange):
                # only continue if the x coordinate of the word exists otherwise draw the empty color to the board
                if xWord >= 0 and xWord < len(word):
                    if word[xWord][y]:
                        self._led.set(xBoard,y,color)
                    else:
                        self._led.set(xBoard,y,emptyColor)
                else:
                    self._led.set(xBoard,y,emptyColor)

        self._step += amt

        time.sleep(0.2)

    	if self._step >= maxStep: # end animation when the letter is off the board
		exit(0)
           

Well that’s pretty much it, so go run the program again and test it out. You should see the letter A run across the screen to the left. Now we will proceed to draw a word, which shouldn’t be hard because we can draw any matrix, word, to the board. We just need to create the word.

Drawing a Word

Let me reiterate the last point I made in the previous section: we can draw any word matrix to the board that is the same height as the board matrix. This means all we need to do is create the correct word matrix to draw, and we already have the code to draw it; furthermore, this section will deal with creating the word matrix from a string.

As stated earlier, I’m going to assume you have the same dimension board as I do for now. If we want to create a word matrix from any string, we need to be able to have a matrix to draw for every character in the string. This means we need to construct individual matrices for each letter. For a character that is a 5×3 matrix, I have already done this. Create a file called MessageCharacters5x3.py that will be located in a new folder messageBoard/characters/ with the following contents:

class MessageCharacters5x3(object):

	cA = [[0,1,1,1,1],[1,0,1,0,0],[0,1,1,1,1]]
	cB = [[1,1,1,1,1],[1,0,1,0,1],[0,1,0,1,0]]
	cC = [[0,1,1,1,0],[1,0,0,0,1],[0,1,0,1,0]]
	cD = [[1,1,1,1,1],[1,0,0,0,1],[0,1,1,1,0]]
	cE = [[1,1,1,1,1],[1,0,1,0,1],[1,0,1,0,1]]
	cF = [[1,1,1,1,1],[1,0,1,0,0],[1,0,1,0,0]]
	cG = [[0,1,1,1,0],[1,0,1,0,1],[1,0,1,1,1]]
	cH = [[1,1,1,1,1],[0,0,1,0,0],[1,1,1,1,1]]
	cI = [[1,0,0,0,1],[1,1,1,1,1],[1,0,0,0,1]]
	cJ = [[1,0,0,0,1],[1,1,1,1,1],[1,0,0,0,0]]
	cK = [[1,1,1,1,1],[0,0,1,0,0],[1,1,0,1,1]]
	cL = [[1,1,1,1,1],[0,0,0,0,1],[0,0,0,0,1]]
	cM = [[1,1,1,1,1],[0,1,0,0,0],[1,1,1,1,1]]
	cN = [[1,1,1,1,1],[0,1,0,0,0],[0,1,1,1,1]]
	cO = [[0,1,1,1,0],[1,0,0,0,1],[0,1,1,1,0]]
	cP = [[1,1,1,1,1],[1,0,1,0,0],[0,1,1,0,0]]
	cQ = [[0,1,1,0,0],[1,0,1,0,0],[1,1,1,1,1]]
	cR = [[1,1,1,1,1],[1,0,1,0,0],[0,1,0,1,1]]
	cS = [[0,1,1,0,1],[1,0,1,0,1],[1,0,1,1,0]]
	cT = [[1,0,0,0,0],[1,1,1,1,1],[1,0,0,0,0]]
	cU = [[1,1,1,1,1],[0,0,0,0,1],[1,1,1,1,1]]
	cV = [[1,1,1,1,0],[0,0,0,0,1],[1,1,1,1,0]]
	cW = [[1,1,1,1,1],[0,0,0,1,0],[1,1,1,1,1]]
	cX = [[1,1,0,1,1],[0,0,1,0,0],[1,1,0,1,1]]
	cY = [[1,1,0,0,0],[0,0,1,1,1],[1,1,0,0,0]]
	cZ = [[1,0,0,1,1],[1,0,1,0,1],[1,1,0,0,1]]

	c0 = [[1,1,1,1,1],[1,0,0,0,1],[1,1,1,1,1]]
	c1 = [[0,0,0,0,0],[1,1,1,1,1],[0,0,0,0,0]]
	c2 = [[1,0,1,1,1],[1,0,1,0,1],[1,1,1,0,1]]
	c3 = [[1,0,1,0,1],[1,0,1,0,1],[1,1,1,1,1]]
	c4 = [[1,1,1,0,0],[0,0,1,0,0],[1,1,1,1,1]]
	c5 = [[1,1,1,0,1],[1,0,1,0,1],[1,0,1,1,1]]
	c6 = [[1,1,1,1,1],[1,0,1,0,1],[1,0,1,1,1]]
	c7 = [[1,0,0,0,0],[1,0,0,0,0],[1,1,1,1,1]]
	c8 = [[1,1,1,1,1],[1,0,1,0,1],[1,1,1,1,1]]
	c9 = [[1,1,1,0,0],[1,0,1,0,0],[1,1,1,1,1]]

	cNULL          = [[0,0,0,0,0]]
	cSPACE         = [[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]]
	cPERIOD        = [[0,0,0,0,1],[0,0,0,0,0]]
	cCOMMA         = [[0,0,0,1,1],[0,0,0,0,0]]
	cDOUBLEQUOTE   = [[1,1,0,0,0],[1,1,0,0,0],[0,0,0,0,0]]
	cSINGLEQUOTE   = [[1,1,0,0,0],[0,0,0,0,0]]
	cCOMMA         = [[0,0,0,1,1],[0,0,0,0,0]]
	cEXCLAIMATION  = [[1,1,1,0,1],[0,0,0,0,0]]
	cQUESTION      = [[0,1,0,0,0],[1,0,0,1,1],[0,1,1,0,0],[0,0,0,0,0]]
	cUNKNOWN       = [[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1]]

As you can see, I’ve made matrices for every letter in the english alphabet, for every digit, and for some additional special characters. All the matricies are preceded by “c” and are properties of the MessageCharacters5x3.py class. Now we need to build a class that can parse a string and create a word matrix.

Let’s create a file StandardMessageParser.py and put it in an new folder messageBoard/parsers/. It will contain a class StandardMessageParser, which parses a string and creates a word matrix we can use to draw to the board. Here’s what it looks like:


class StandardMessageParser(object):

	_message       = ""
	_messageMatrix = []
	_characters    = None

	def __init__(self, message, characters):
		super(StandardMessageParser, self).__init__()
		self._message    = message.upper()
		self._characters = characters
		self.setMessage(self._message)

	def setMessage(self,message):
		pass

	def getMessage(self):
		return self._message

	def getMessageMatrix(self):
		return self._messageMatrix
		

The StandardMessageParser class is constructed with 2 parameters: the character set we are using (the one we just designed MessageCharacters5x3), and the message we need to parse. During initialization, it sets the two parameters privately and calls the setMessage() function — we still need to implement this. We also defined two getters: one for the message string, and one for the constructed message matrix.

Once we implement setMessage() it becomes pretty straight forward to draw a letter to the board. All we need to do is create an instance of StandardMessageParser, pass it the character set we are using and the message string, retrieve the messageMatrix for that message, and, finally, draw that matrix to the board through the step() function of our MessageBoardAnimation class.

The setMessage() function is pretty straightforward. We will loop through each character in the string and append the matrix for that character to the string. The whole implementation looks like this:


class StandardMessageParser(object):

	_message       = ""
	_messageMatrix = []
	_characters    = None

	def __init__(self, message, characters):
		super(StandardMessageParser, self).__init__()
		self._message    = message.upper()
		self._characters = characters
		self.setMessage(self._message)

	def setMessage(self,message):
		#reset message list
		self._messageMatrix = []

		#calculate message list
		for letter in message:
			letterMatrix = self.getLetterMatrixForLetter(letter)
			self._messageMatrix.extend(letterMatrix)
			self._messageMatrix.extend(self._characters.cNULL)

		#set our new message
		self._message = message

	def getMessage(self):
		return self._message

	def getMessageMatrix(self):
		return self._messageMatrix

	def getLetterMatrixForLetter(self,letter):

		letterMatrix = self._characters.cUNKNOWN

		#if we have the letter defined add it to the list, otherwise check for special character
		if hasattr(self._characters,"c"+letter):
			letterMatrix = self._characters.__getattribute__("c"+letter)
		elif letter == " ":
			letterMatrix = self._characters.cSPACE
		elif letter == ".":
			letterMatrix = self._characters.cPERIOD
		elif letter == ",":
			letterMatrix = self._characters.cCOMMA
		elif letter == "!":
			letterMatrix = self._characters.cEXCLAIMATION
		elif letter == "?":
			letterMatrix = self._characters.cQUESTION
		elif letter == "\"":
			letterMatrix = self._characters.cDOUBLEQUOTE
		elif letter == "'":
			letterMatrix = self._characters.cSINGLEQUOTE

		return letterMatrix
		

You can see the setMessage() first clears the _messageMatrix matrix just in case we already have one. It then loops through all the characters in the message string and gets the matrix representation of that character by calling the getLetterMatrixForLetter() function. All this function does is see if the character set we were given has this letter as an attribute.

We capitilized the entire message string when we initialized this class, so each character retrieved from the string will already be capitalized. This ensures we will get a match to the character matrix of the character set if it exists. The getLetterMatrixForLetter() function also checks for the special character individually and sets letterMatrix appropriately.

The letterMatrix is then appended to our _messageMatrix. Remember when I said characters were going to actually be 5×4 matrices? Well, I construct them here by appending an empty column matrix, cNULL, to our _messageMatrix after the letterMatrix. This will ensure the message the board draws is readable. Test out we can write a working message by modifying the step() function as so:


    def step(self, amt = 1):

        # get word
        characterSet  = MessageCharacters5x3()
        message       = "Hello World!"
        messageParser = StandardMessageParser(message,characterSet)
        word          = messageParser.getMessageMatrix()

        # define variables
        boardWidth   = self._led.width
        maxStep      = boardWidth + len(word) # letter is off the board at this step
        xRange       = boardWidth
        yRange       = self._led.height
        xWordOffset  =  self._step - boardWidth
        color        = (255,0,0) # color to fill the lit LED (red)
        emptyColor   = (0,0,0)   # color to fill the unlit LED (no color)

        # loop through range of x values in board
        for xBoard in range(xRange):
            # create mapping to x value of word
            xWord  = xWordOffset + xBoard
            for y in range(yRange):
                # only continue if the x coordinate of the word exists otherwise draw the empty color to the board
                if xWord >= 0 and xWord < len(word):
                    if word[xWord][y]:
                        self._led.set(xBoard,y,color)
                    else:
                        self._led.set(xBoard,y,emptyColor)
                else:
                    self._led.set(xBoard,y,emptyColor)

        self._step += amt

        time.sleep(0.2)

    	if self._step >= maxStep: # end animation when the letter is off the board
		exit(0)
           

You will also need to import the StandardMessageParser class and MessageCharacters5x3 class at the top of the file. Once you’ve done that, run the AnimateMessageBoard.py file like last time and that’s it! You can now simply change “Hello World!” to whatever string you want, and it will scroll across the screen! Now that we’ve done displayed actual words, we’ll need to clean up the code a little bit to make it more robust.

Cleaning Up the Code

We can draw any phrase we want to the board now, but it’s not a very robust process. Everything would be very hard to maintain if we tried to apply this program to another board, or another type of animation for that matter. Here’s some problems I notice right off the bat:

  1. If we wanted to change the character set to different dimensions, we would have to write an entirely new parser.
  2. If we wanted to perform another type of animation, then we would have to make a completely different file.
  3. If we wanted to change the color of a certain animation but keep the mechanics of the same animation, then we would have to create a completely different file.

As you can see, there are a lot of issues that arise from our original design. We’ll need to start cleaning up the files we use, so we can make a more robust program.

Let’s address the first issue. The first thing we need to do to the step() function is separate the word from the animation algorithm. We should really be getting the phrase in the animateMessageBoard.py file, and initializing the MessageBoardAnimation class with the phrase. This will remove the dependency for the animation to have the phrase hard coded; instead, we can change the phrase dynamically during runtime (if we desire). Let’s change our animateMessageBoard.py and MessageBoardAnimation.py as so:

As you can see we’ve separated the creation of the word matrix from the step() function and initialized the MessageBoardAnimation class with it. This allows us to retrieve the word matrix in any way we want. For now we are hard coding our message, “Hello World!”, and creating the word matrix by initializing a parser.

We still have the issue of changing character sets and having to rewrite the StandardMessageParser to accept that character set. Solving this problem is fairly simple, as we just need to pass a neutral character set to the parser. The neutral character set will take the form of any character set we choose. We need to create another file MessageCharacters.py in the same directory as MessageCharacters5x3.py, and we’ll need to modify the MessageCharacters5x3.py file as so:

Here we’ve just made MessageCharacters5x3 a subclass of MessageCharacters. This allows us to pass any character set under the MessageCharacters type. This type simply stores the matrix representation of a character.

If you were wondering how to draw a message to a board with different dimensions, then wonder no further. All you need to do is create a new file in the messageBoard/characters directory and subclass it under the MessageCharacters type. After that you’ll need to create the matrix representation for each character in the character set.

For example, if you’re board is 8×48 and you want characters 5 pixels wide, you’ll need to create a file MessageCharacters8x5.py, create the class MessageCharacters8x5 that subclasses MessageCharacters.py within that file, and create the matrix representation for each character.

This should have pretty much solved our first issue, so let’s move onto the second issue. Here’s the real problem: if we wanted to animate the movement of the words differently, then we would have to create an entirely new animation. The new animation would have most of the code we already using in MessageBoardAnimation.py. To solve this, we need to encapsulate the animation algorithm inside of an object. This will allow us to separate the movement algorithm from the rest of the animation. Go ahead and create a new directory messageBoard/algorithms/ and within this folder create files called MessageAlgorithm.py and ScrollLeftMessageAlgorithm.py. The MessageBoardAnimation.py and animateMessageBoard files should also be modified as so:

The first thing we did was create a class MessageAlgorithm that is meant to be subclassed. The function animateLED() is the one that needs to implement the algorithm. The ScrollLeftMessageAlgorithm overrides this method to perform the scroll left animation. This is called in our step() function every iteration. The MessageBoardAnimation class is now initialized with a MessageAlgorithm object, so the algorithm can change at runtime by setting a new algorithm object.

Now if we wanted to make another animation, then we can still keep the MessageBoardAnimation class as is. We just need to create another message algorithm class that inherits from the MessageAlgorithm class and overrides the animateLED() function.

The final issue is very similar to issue #2, except this issue deals with color animations versus moving animations. If we want the animation that changes color to be dynamic, we have to do the same approach as the moving animation. The only exception to this is since the color is changed within the MessageAlgorithm class heiarchy, we need to modify this class, not the MessageBoardAnimation class.

Why don’t you create files ColorAlgorithm.py and RainbowColorAlgorithm.py within the messageBoard/algorithms/ directory. The MessageAlgorithm.py and animateMessageBoard files should also be modified like this:

Now we have a more manageable program! A few classes here or there to encapsulate logic and resources, and now we have a plug and play style message board. If you want to create your own message algoritm, then subclass the MessageAlgorithm class; if you want to create your own color algorithm class, then sublcass the ColorAlgorithm class. The bigger picture should be a lot more customizeable.

I should mention that this really doesn’t solve all of our problems. This really solves the practical issues we can think of at face value. Some things to consider with our current design: the color algorithm isn’t completely independent of the message algorithm, we may want to pass different parameters to the algorithms in the future, and we only have 2 methods to subclass for the color algorithm and 1 for the message algorithm. With a little toying around we could improve some of these weaknesses, but this design will suffice for our purposes.

Conclusion

With all that out of the way, we are left with the final task of creating a small web service to read data into the board. The bulk of the work is out of the way, so we can start finalizing the project. The source code in the previous sections will be available on my GitHub page. Feel free to download the files and modify them as you would like. The structure of the program, as explained above, is meant to be customized to fit the needs of your design.

Comment Below

Your email address will not be published.

*This field is required.
*This field is required.
*This field is required.