January 1st, 2012 — Foothill Ranch, CA
Markee is a proof-of-concept project that allows friends and family to use the web to send messages across the "digital divide" to reach loved ones that have physical accessibility limitations that prevent them from using modern media like telephone calls, text messaging or email. The idea was presented to me by my friend Leo Kempf, who was in need of a way to send messages to his grandmother that cannot answer the phone. His design challenge for me was to build an LED marquee capable of receiving message updates over the Internet.
- 1 Arduino Uno
- 3 Red/Green LED Matrix with Backpacks from SparkFun
- 1 Arduino Ethernet Shield
- Jumper wires
- 3 x 2 female to female SPI cable (or other connector)
- Web server running PHP, MySQL
- SparkFun RG Backback Firmware Update (Arduino)
- LED_matrix_chain_test.ino (Arduino)
- LED_matrix_scroll_test.ino (Arduino)
- LED_matrix_scroll_test.pde (Processing)
1. Assembling the Electronic Components
Though the final product has 3 daisy-chained LED matrix backpacks attached to an Arduino and Arduino Ethernet Shield, this configuration will not work right away. The best way to start is to connect one LED matrix backpack to the Arduino board. That sounds simple enough, but this task was surprisingly difficult.
The first bit of trouble came from the Red/Green LED Matrix (with Backpack) that I bought from SparkFun. Unfortunately, these products have the "Input" and "Output" labels misprinted on them. I don't know if this error was isolated to a small batch of products or if they are all that way, so proceed with caution. On my incorrectly labeled boards, the true input pins are the female pins.
The second issue I ran in to was with my choice of Arduino board. I bought an Arduino Uno to use with this project, however I started by testing the LED matrix with an older Arduino Duemilanove, which has a slightly different power rail than the Arduino Uno. I still don't know why, but the Duemilanove board will not drive the matrix properly.
Once those two issues are resolved you should be able to upload the XXXXX sample code to test a single LED Matrix.
After you have one LED matrix working successfully you can daisy-chain up to 7 more displays for a total display size of 8 boards or 8 x 64 pixels. Unfortunately, it's not exactly plug-and-play. In my experience, I had to upload new firmware to each matrix before they would work properly without exhibiting the "ghosting" problem seen in the sample video. I was able to find some references to this issue on some forums, but I would not have been able to resolve this without some dedicated help from an anonymous hacker named Jim D (thanks so much Jim!).
With his help (and his code) I was able to follow SparkFun's tutorial for using the Arduino as an AVR programmer to upload new firmware to each LED matrix. This required soldering some new pins onto the LED matrix backpack, which was trivial for two boards, but one of the boards was a hassle, probably because of my poor soldering job. Jim's Arduino code for the firmware update is available for download in the Source Code section. It also includes the .hex file for convenience.
After the LED matrices have been properly updated and tested, the Arduino Ethernet Shield can be added on top of the Arduino Uno to install the Ethernet functionality. Simply replace the connections for the LED matrix to their corresponding pins on the Ethernet Shield
2. Writing/Understanding the Arduino Code
The Arduino source code for this project includes detailed comments throughout, so rather than covering the code here, this section will describe some of the design decisions that led to the code.
I started on font design for this project right away because it was fun and something I could do while I was waiting for my parts to arrive. Each matrix is 8 pixels tall by 8 pixels wide so I started by designing a letterform for each of the 95 printable ASCII characters. Once I had sketched out the characters I translated them into a basic font definition file, where each line contained 64 binary values to indicate the on/off state of each pixel.
The downside of this basic approach of using all 8 columns to define each character is that not all characters require the same number of columns. For instance an "I" may only be 3 pixels wide, but an "A" may be 5 pixels wide. The good news is that character definitions can be trimmed to only include the number of columns needed for each character. The other good news is that since each column is 8 pixels tall each character definition will always be a multiple of 8, and therefore able to be stored as a single byte. So a character like "I" that is 3 columns wide will only require 3 bytes (24 bits) of memory to store.
If you are familiar with Arduino microcontrollers, then you know that they have limited memory in RAM (1 or 2 Kb depending on the model). This is typically plenty of memory for storing sensor data, however text strings can begin to require significant amounts of memory.
My font file contains 95 characters, with the average character requiring about 32 pixels to render. That equals 32 bits or 4 bytes, for a total of 380 bytes for the entire font description. In order for characters to contain a variable amount of rows an array of pointers must be used, which requires 95 integers for a total use of 180 bytes. Additionally, another array of integers is used to store the length of each character for another 180 bytes. So far that means the font requires 740 bytes for storage alone.
In order to display messages on par with a text message or a tweet, I'd like for the scroller to be able to display messages up to 140 characters in length. Since an ASCII character requires 1 byte per character that requires 140 bytes alone. The framebuffer for the screen will require 1 bit per pixel, so 3 screens with 64 pixels each requires 24 bytes.
In my implementation, I'm translating the message text into a full message rendering and then "sliding" the display window across this rendering to achieve the scrolling effect. In order to do this I must pre-compose the entire message, which requires a lot of memory. For example, a 140-character message with an average of 4 columns per character plus one column per character for a space in between each character adds up to 700 columns or 700 bytes.
As you can see for the alphabet (740 bytes), the message as text (140) and the message as pixels (740 bytes) we are up to 1580 bytes and quickly running out of the 2048 bytes of available RAM on the Arduino Uno. This doesn't even account for other program data. So what can be done about this?
Enter Program Memory (PROGMEM)
Fortunately, the Arduino has a lot more memory available besides the 2kb of RAM. The Arduino Uno has 32kb available for storing the actual program and using the PROGMEM library we can store much of our data in program memory and access it using this library.
I was glad I found this library, but I had a heck of a time implementing it. The library documentation has some good examples, but it lacks some of the finer details, such as working with pointer arrays and how to access them. Specifically, in earlier revisions of my code I was using a referenced array defined as "prog_char PROGMEM *alpahbytes" to access my font characters. However, I discovered the hard and confusing way that the pgm_read_byte_near() function could be used to read a specific value when it was accessed with a specific index value, but it would not work properly when using a variable as the index. I'm sure there are good reasons for this, but with my level of C++ knowledge I can't tell you why. Luckily, I found a post on a message board that used the "PGM_P" data type (and no reference) and that worked! (Link to resource).
3. Hardware Presentation
One of my favorite parts of making a project is the final presentation. I love putting on the final touches. Without access to a shop I really had to finish this using simple craft materials. Looking around the house I saw a 5" x 7" frame and realized my display would fit in that quite nicely and that it would be an inexpensive way to put a finished presentation on the front of the project. The back required more space than a frame could handle, so I figured I would need to craft my own case for the back to hold the Arduino and Ethernet shield.
At my local craft store I found a great 5 x 7 frame with some depth to it. I also found a 5" x 7" shadow box and bought that, but decided against using it since it sits 100% perpindicular on a shelf, versus a frame with a little bit of tilt to it that can be seen more easily from above. I also bought a little frame stand and a heavy 16" x 20" Canson art board. I've had good success in the past making my own cardboard boxes so I thought I'd try the same for this project.
I took measurements and carefully sketched out my vision for how this box could fold up and close, but still be accessible (to show of the insides and check on from time to time). Then I used some cheap cardboard (from my stash of leftover boxes from wedding presents) and made some templates to custom fit everything and get some measurements. I've made several of these cardboard boxes now and I've learned a few things, such as:
- Expect to make at least 1 "cheap" prototype before using your final materials
- Trite but true, "Measure twice and cut once"
- Don't rush
- Plan for "glue tabs" if necessary and don't cut off the extra material they require
- Factor in the thickness of the material when calculating measurements
- For thicker materials, cut several shallow cuts slowly rather than trying to do it in as few cuts as possible.
Once I was confident in my template I transferred my sketch to measurements and then penciled it on the art board and started cutting. It must have been luck because my design required all 20 inches of the artboard's length. I was extremely careful with my measurements and cuts and was rewarded with a great outcome on my first attempt, something I've learned to not expect. While the execution was what I hoped for, there is still room for improvement on the design as it doesn't close up quite as nicely as I envisioned.
4. The Web Interface
In order to receive messages this ticker obviously needed a web site to check for incoming messages and an interface for setting new messages. My proficiency in web development was a welcome skill to fall back on so I could continue working on the project when stumped by all of my Arduino challenges.
I've been designing and interfacing with a handful of web API's recently so I knew I wanted to design a REST-ful web API to get new messages and post new messages, with various options available as query string parameters. I've also been doing work with jQuery and have recently made some other simple sites that can be saved and loaded as full-screen web apps on iOS, so I knew I wanted to do that as well.
The iOS web app aspects...
Behind the web interface are two MySQL tables, one to store the messages and one to log device access.The 'message server' table stores a unique id, the time of submission, the time the message should begin to display, the time the message should cease to display, the message content, the IP address of the web client that created the message, the Device ID authorized to access the message and a User ID belong to the person who posted the message. Not all of that information is required for this implementation, however I went ahead and built it for multiple users and multiple devices.
The 'message_server_log' table stores a unique log identifier, the device ID, the IP address of the device, the mac address of the device, and the access time for each connection the device makes to the message server API. Again, this level of logging isn't really necessary, but it's interesting and not much extra trouble.
CREATE TABLE `message_server` ( `id` mediumint(4) unsigned NOT NULL AUTO_INCREMENT, `submission_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `start_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `end_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `message` varchar(255) NOT NULL DEFAULT '', `ip_address` varchar(15) NOT NULL DEFAULT '', `device_id` tinyint(3) unsigned NOT NULL DEFAULT '0', `user_id` tinyint(3) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=77 DEFAULT CHARSET=utf8;
CREATE TABLE `message_server_log` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `device_id` tinyint(3) unsigned NOT NULL DEFAULT '0', `ip_address` varchar(15) NOT NULL DEFAULT '0', `mac_address` varchar(12) NOT NULL DEFAULT '0', `access_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=146 DEFAULT CHARSET=utf8;
- GET api.php?method=current-message&device_id=1
- GET api.php?method=previous-messages&device_id=1&limit=10
- GET api.php?method=scheduled-messages&device_id=1&limit=10
- GET api.php?method=device-log&device_id=1&limit=10
- POST api.php
Response Format can be JSON or plain text (used by Arduino)