15th
warning: nerd stuff
At work, we have a website for monitoring our web app, Email Center Pro. It shows a log of recent major activity- new accounts, account upgrades, downgrades, if someone (gasp!) cancels an account, etc. The concept is similar to Queen Bee at 37signals, but I’m not sure if we got the idea from them. We also have key server information available for diagnostics and so forth. I though it would be cool to display this information in a glaceable format.
I like glanceable interfaces. A clock is an excellent example- if you want to know what time it is, you don’t have to visit a website or call up a specific application. All of the information that you need is readily available in a format that is quickly digestible with very little processing. Glanceable devices are fairly niche in the computing world, but their popularity is growing. Apple introduced Dashboard in OS X 10.4- press a button and a number of colorful web-enabled Widgets appear, showing you information ranging from world time and weather to ski reports and even controls for system daemons. Microsoft introduced SideShow a few years ago, a little LCD for displaying Vista’s version of Widgets- effectively one-upping Apple. Unfortunately Sideshow has thus far failed to gain widespread adoption. One of the coolest recent forays into the realm of glanceable interfaces is Chumby, a $180, softball-sized, squishy device with an LCD touchscreen and a wireless network connection that runs Linux and can display widgets and videos from the community (A Flash SDK is available) and from “Partners” such as CBS, The New York Times, and Shoutcast.
Initially we discussed several monitoring techniques. The first idea I had was to mount a large plasma screen on the support pillar near my cubicle and feed video to it from a concealed netbook running a custom Processing application showing a live representation of email sent and received. Envelopes flying from left to right would represent sent mail and envelopes flying right to left would represent received mail. Mail sent from our Email Center Pro account would trail a subject line to make things more visually interesting. Charts and visualizations along the bottom of the screen would indicate the number of concurrent online users, server loads, health metrics, and other useful information.
Although this would be incredibly cool, I didn’t really have a budget for this project. I briefly considered using a small text-only LCD screen, but tiny letters wouldn’t be quite glanceable. A quick visit to All Electronics and I found a solution: 
Liteon #LTM-Y2K19JF-03 at All Electronics - now sold out
At only $1, this display has basically everything I wanted. It includes seven 14 segment indicators, across the top, four 7 segments across the bottom, and four miscellaneous indicators. Best of all, there’s a built-in driver chip- so I could drive it with essentially two wires. Plus- it’s really cheap. I have no idea what it was originally built for- a voicemail system maybe? I bought 3.
I decided to power the whole thing with an Arduino, a simple prototyping board with an integrated microprocessor. I bought one a while back to play with and it was basically just collecting dust, so it was nice to finally break it out and use it for something.
I wired the LED displays up according to a pinout I found here. To update each display, I send it 180 bits of data. The displays didn’t come with a datasheet to explain how to communicate with them, but I found information about the protocol at the same site. I wired each data pin to an output pin on the Arduino, and to keep the wiring simple, I connected all of the clock pins to a single output pin. This complicated the code somewhat, but it also allowed the Arduino to address each display at the same time rather than one-at-a-time. This decreased the flicker on each update to an imperceptible level.
At this point I was able to light up segments at will, but I was still unable to make it actually spell anything. I wanted to get to a point where I could send it a string of text to display. To do this, I had to tell the code how to display each character I might send. Each character on the display has 14 segments, and each segment is assigned a binary place value. To light up all 14 segments, you send 11111111111111. The upper-left diagonal piece is associated with the zero-th place, and the top flat bar is the 13th place. If I wanted to light up the segments that make an upper-case E, I would need to send it 10011100000010. The lower 7 segment displays work the same way, except of course they can really only display numbers and certain letters.
The site I found earlier suggested making an Excel spreadsheet to help you design and figure out the bits for each letter- I don’t have Excel handy, and I don’t really know it very well anyway, so I wrote a quick little javascript to expedite the process. Just to make it easier to represent in my code I’m representing each letter in my code as a base-10 number rather than binary number- E is represented as 9986.
The following is the code I wrote for the Arduino. It’s in a language called Wiring, which should be pretty easy to read if you’re familiar with any C-like languages. If you’re a Wiring master this will probably be laughably poor. The first hundred or so lines are my custom font, it includes most of the lower printable ASCII character set.
// seriously this is incredibly poor code-
// and probably useless unless you have the exact same LED displays.
// Basically send a serial message with one char as a mode selector
// and the rest of the message is just what gets displayed...
// so if you send it "SHellow World" it will display "Hello World"
// The lower meters expect a 4 digit number, so you can
// put several in a single serial communication.
// if you say A1234B5678C2468 it will update the lower 3 displays with numbers
// Z is the analog meter and R is a red LED in the meter
#define ResetPin 5 // reset pin for all displays
#define ClockPin 6 // clock pin for all displays
#define Data1 4
#define Data2 3
#define Data3 2
// bitmaps converted to ints for convenience
const unsigned int letterMap[] = {
0, // SPACE
136, // !
65, // "
1962, // #
11690, // $
2542, // %
170, // &
64, // '
80, // (
5, // )
221, // *
170, // +
4, // ,
34, // -
8, // .S
68, // /
16128, // 0
6208, // 1
13858, // 2
15392, // 3
6434, // 4
11554, // 5
12066, // 6
8264, // 7
16162, // 8
14626, // 9
136, // :
132, // ;
80, // <
1058, // =
5, // >
8520, // ?
12072, // @
15138, // A
15528, // B
9984, // C
15496, // D
9986, // E
8994, // F
12064, // G
6946, // H
9352, // I
7168, // J
850, // K
1792, // L
6977, // M
6929, // N
16128, // O
13090, // P
16144, // Q
13106, // R
11554, // S
8328, // T
7936, // U
836, // V
6932, // W
85, // X
73, // Y
9284, // Z
9984, // [
17, // \
16255, //
15360, // ]
12544, // ^
1024, // _
1, // `
1546, // a
3874, // b
1570, // c
7714, // d
1542, // e
8962,
3120, // g
778, // h
8, // i
6160, // j
216, // k
768, // l
2602, // m
522, // n
3618, // o
9026, // p
14369, // q
40, // r
1072, // s
170, // t
3584, // u
516, // v
3592, // w
85, // x
3088, // y
1030, // z
9223, // {
136, // |
9328, // }
12643 // ENVELOPE
};
const int numberMap[10] = {
B01111110,
B00110000,
B01101101,
B01111001,
B00110011,
B01011011,
B00011111,
B01110000,
B01111111,
B01111011
};
int loopCnt = 0; // Loop counter
int loopRst = 25; // How many loops before we increment charStart
int loopFrame = 0; // Current frame counter (DEP)
int strPos = -21;
int charStart = 0; // the leftmost character to display
char incomingByte; // icoming byte from serial connection
int incomingByteCount = 0; // counter for how many bytes received in this buffer
int o;
int icon1 = 0;
int icon2 = 0;
int icon3 = 0;
char scrollString[21] = " Email Center Pro ";
char longString[127] = " Email Center Pro ";
char longStringBuffer[127] = "";
int longStringLength = 0;
int longStringBufferLength = 0;
int newStringWaiting = 0;
int a[4];
int b[4];
int c[4];
char z = 0;
//char* numStrings[3];
void setup()
{
pinMode(ResetPin, OUTPUT);
pinMode(ClockPin, OUTPUT);
pinMode(Data1, OUTPUT);
pinMode(Data2, OUTPUT);
pinMode(Data3, OUTPUT);
pinMode(13, OUTPUT);
pinMode(7, OUTPUT);
Serial.begin(9600);
// RESET the displays
digitalWrite(ResetPin,HIGH);
delay(10);
digitalWrite(ResetPin,LOW);
}
void loop() {
// look at Serial.available() to see if there is a Serial message
if (Serial.available()) {
unsigned int x = 0;
char option = 'E';
char buffer = 'E';
option = Serial.read();
delay(50);
switch(option) {
case 's':
case 'S':
while(x < 127 && Serial.available() > 0 && buffer != B00000000) {
buffer = Serial.read();
longStringBuffer[x] = buffer;
x++;
}
if (longStringBuffer[x] != B00000000) {
longStringBuffer[x] = B00000000; // cap that string
}
longStringBufferLength = x;
newStringWaiting = 1;
break;
case 'a':
case 'A':
while(x < 4) {
a[x] = ' ';
a[x] = Serial.read();
x++;
}
break;
case 'b':
case 'B':
while(x < 4) {
b[x] = ' ';
b[x] = Serial.read();
x++;
}
break;
case 'c':
case 'C':
while(x < 4) {
c[x] = ' ';
c[x] = Serial.read();
x++;
}
break;
case 'r':
case 'R':
int r = Serial.read();
if (r == '1') {
digitalWrite(7, HIGH);
} else {
digitalWrite(7, LOW);
}
break;
case 'z':
case 'Z':
analogWrite(11, map(Serial.read(), 'A', 'Z', 1, 255));
break;
}
}
// loopRst loop iterations have happened, increment frame count (or back to frame 0)
if (loopCnt >= loopRst) {
loopCnt = 0;
if (longStringBufferLength > 21) {
strPos++;
} else {
strPos = 0;
}
if (strPos == (longStringLength) || longStringBufferLength <= 21) {
if (longStringBufferLength > 21) {
strPos = -21;
}
// check for a new string in the buffer
if (newStringWaiting == 1) {
for (int i = 0; i <= longStringBufferLength; i++) {
longString[i] = longStringBuffer[i];
}
longStringLength = longStringBufferLength;
}
}
// build the string out to scrollString
for (int i = 0; i < 21; i++) {
if (strPos + i < 0 || (strPos + i) > (longStringLength - 1)) {
scrollString[i] = ' ';
} else {
scrollString[i] = longString[strPos + i];
}
}
}
loopCnt++;
// Each display takes 5 iterations of 36 bits to update
// 0) 1 start + 14 alpha + 2 colon + 1 ignore + 8 icon + 2 colon + 2 ignore + 5 transistor + 1 zero = 36
writeAll(1); // 1 start;
//writeBits(letterMap[o], 14); // 14 alpha
writeChars(scrollString[0], scrollString[7], scrollString[14]);
writeBits(B00000000, 2); // 2 colon
writeAll(0); // 1 ignore
//writeBits(B00000000, 8); // 8 icon
writeBytes(icon1, icon2, icon3);
writeBits(B00000000, 2); // 2 colon
writeBits(B00000000, 2); // 2 ignore
writeBits(B00010000, 5); // 5 transistor
writeAll(0); // 1 zero
// 1) 1 start + 14 alpha + 7 numeric + 7 numeric + 1 ignore + 5 transistor + 1 zero = 36
writeAll(1); // 1 start;
//writeBits(letterMap[o], 14); // 14 alpha
writeChars(scrollString[1], scrollString[8], scrollString[15]);
writeNums(a[0], b[0], c[0]); // 7 numeric
writeNums(a[2], b[2], c[2]); // 7 numeric
writeAll(0); // 1 ignore
writeBits(B00001000, 5); // 5 transistor
writeAll(0); // 1 zero
// 2) 1 start + 14 alpha + 7 numeric + 7 numeric + 1 ignore + 5 transistor + 1 zero = 36
writeAll(1); // 1 start;
//writeBits(letterMap[o], 14); // 14 alpha
writeChars(scrollString[2], scrollString[9], scrollString[16]);
writeNums(a[1], b[1], c[1]); // 7 numeric
writeNums(a[3], b[3], c[3]); // 7 numeric
writeAll(0); // 1 ignore
writeBits(B00000100, 5); // 5 transistor
writeAll(0); // 1 zero
// 3) 1 start + 14 alpha + 14 alpha + 1 ignore + 5 transistor + 1 zero = 36
writeAll(1); // 1 start;
//writeBits(letterMap[o], 14); // 14 alpha
//writeBits(letterMap[o], 14); // 14 alpha
writeChars(scrollString[3], scrollString[10], scrollString[17]);
writeChars(scrollString[5], scrollString[12], scrollString[19]);
writeAll(0); // 1 ignore
writeBits(B00000010, 5); // 5 transistor
writeAll(0); // 1 zero
// 4) 1 start + 14 alpha + 14 alpha + 1 ignore + 5 transistor + 1 zero = 36
writeAll(1); // 1 start;
//writeBits(letterMap[o], 14); // 14 alpha
writeChars(scrollString[4], scrollString[11], scrollString[18]);
//writeBits(letterMap[o], 14); // 14 alpha
writeChars(scrollString[6], scrollString[13], scrollString[20]);
writeAll(0); // 1 ignore
writeBits(B00000001, 5); // 5 transistor
writeAll(0); // 1 zero
}
int getCharMap(char c) {
int r = 0;
r = letterMap[c-' '];
//Serial.println(r);
return r;
}
int getNumMap(int c) {
int r = 0;
if (c == ' ') {
r = B00000000;
} else {
r = numberMap[c-'0'];
}
//Serial.println(r);
return r;
}
void writeBytes(int int1, int int2, int int3) {
for (int i = 0; i < 8; i++) {
writeBit((int1 << i) & (1 << 8), Data1); // writeAll((bits << i) & (1 << (length - 1)));
writeBit((int2 << i) & (1 << 8), Data2);
writeBit((int3 << i) & (1 << 8), Data3);
wiggleClock();
}
}
void writeChars(char c1, char c2, char c3) {
int int1, int2, int3;
int1 = getCharMap(c1);
int2 = getCharMap(c2);
int3 = getCharMap(c3);
for (int i = 0; i < 14; i++) {
writeBit((int1 << i) & (1 << 13), Data1); // writeAll((bits << i) & (1 << (length - 1)));
writeBit((int2 << i) & (1 << 13), Data2);
writeBit((int3 << i) & (1 << 13), Data3);
wiggleClock();
}
}
void writeNums(int c1, int c2, int c3) {
int int1, int2, int3;
int1 = getNumMap(c1);
int2 = getNumMap(c2);
int3 = getNumMap(c3);
for (int i = 0; i < 7; i++) {
writeBit((int1 << i) & (1 << 6), Data1);
writeBit((int2 << i) & (1 << 6), Data2);
writeBit((int3 << i) & (1 << 6), Data3);
wiggleClock();
}
}
void writeAll(int bit) {
writeBit(bit, Data1);
writeBit(bit, Data2);
writeBit(bit, Data3);
wiggleClock();
};
void writeBit(int value, int pin) {
//Serial.print(value);
//if (value == 1 || value == '1'){
if (value != 0){
digitalWrite(pin, HIGH);
} else {
digitalWrite(pin, LOW);
}
}
void writeBits(int bits, int length) {
for (int i = 0; i < length; i++) {
writeAll((bits << i) & (1 << (length - 1)));
}
}
// wiggle clock to signal next bit coming
int wiggleClock(){
// seems like there should be delays here and the tech sheet for the displays I guess mentions delays,
// yet it works fine with no delays. (less shakes on updates with no delay, so leaving it)
//delayMicroseconds(3); // pause
digitalWrite(ClockPin, HIGH); // Clock set to high
digitalWrite(ClockPin, LOW); // clock ends low
}
I wrote a quick Python script to access the system monitor webpage and get the most recent activity as well as some vital statistics, and send it down the wire- that code I won’t share because it’s pretty application specific. I used pyserial to communicate with the Arduino and Beautiful Soup to scrape the page. Future updates will probably render the screen scraping obsolete- I’d like to get the activity monitor connected to its own page on the server with only the information it needs. A cron job runs the script every 60 seconds, ensuring that the data is always up to date. Since I had some extra pins on the arduino, I added an analog VU meter to display relative email levels and a little red light that goes on if it can’t reach the status webpage for whatever reason.
It’s plugged into Smerger, our dev server, and sits in an ugly custom enclosure I made out of foamcore and an Ikea NON lamp box. It photographed pretty poorly but it’s easy to read in real life. It sits on top of my cube wall and brings cheer to all. Not quite a 56” plasma screen but it is still pretty cool.


