My Android phone's stock alarm clock wakes me up into a cold summer San Francisco morning. I wallow around a bit, but eventually get up to enjoy yet another day. An hour later, I'm running in my dress shoes, tie waving in the wind like that of an anime superhero schoolboy protagonist, towards a light rail train stop. Twenty seconds before I get there, the train departs, right before an unanticipated 20-minute break in the service. I eat a bagel I don't want in a cafe nearby just to sit there and work while waiting; my day, having barely started, is already ruined.
"SF Muni Sign at Home" seriesHow I built a San Francisco Muni sign that shows train arrival times at home.
Could I have made it? Of course, I could. I could have not re-done my tie three times striving to get perfect length; I could have not procrastinated on reddit for five minutes while sipping tea, I could have postponed paying my utilities bill. On the other hand, I don't want to finish too early and wait at home instead of in a cafe. I need something to let me know, at all times, when the next train would be.
San Francisco "Muni" public transportation vehicles, like those of many other modern big cities, are equipped with real-time GPS trackers that publish location data on the internet. My smartphone has QuickMuni app installed, so I could easily be checking it. I would pick up my phone, unlock it, wait for all the Adnroid lags, wait while the app downloads the upcoming train times, and repeat this every five minutes. This would be implausible. If only I had something like a wall clock so that it constantly displays upcoming train time predictions...
One morning I located the item I wanted in the wild. Actually, I've been seeing it every day, but had no idea that's what I needed all along.
That is it! I can mount a similar sign on my wall, drive it with something like Raspberry Pi or even with my desktop PC. I'd write some software to download, process, and display the train arrival times! I can even teach it to show weather forecast to quickly see if I need to carry an umbrella.
Well, let's go for it. I bought a Raspberry Pi from Amazon, and purchased this sign from BrightLEDSigns.com. I was looking for at least 16 rows of LEDs (to squeeze two lines of text in) and for a simple interface to draw pictures on it, as I didn't want to spend time to figure out opcodes or some other low-level, loosely documented coding. I got the sign for the $89 sale price.
After several nights of hacking, I finally had it: my own, always-on customized dashboard with relevant morning information.
This sign shows that the next 38-Geary buses will come to a specific, pre-configured stop in 2, 7, 14, 27, and 40 minutes; ellipsis means a long wait. The next line shows fewer arrivals for the "backup" 5-Fulton line. The sign also shows that it's 55 degrees Farenheit outside right now, and it will be 56 at 8pm (that's not a bug; that's San Francisco weather for you).
I also implemented a rendition of a real Muni sign just for the fun of it. It has some bugs, and it's slightly different from the real thing, but I didn't want to spend much time on that since I was not going to use it anyway (pull requests welcome). It looks good for demonstration purposes, though. It's animated, here is a video!
The rest of the post describes what components I assembled this thing from, how I configured the Raspberry Pi, what software I used to program the dashboard and to utilize external dependencies, and what problems and bugs I encountered on the way.
Here's the complete list of electronic components I used:
- Programmable LED sign - programmable with a simple API, monochrome, at least 16 pixels high, bought from this vendor the 16x96 one, with Perl API and USB interface.
- Raspberry PI - The famous $25 energy-efficient computer I managed to buy on Amazon for $40 at the time;
- USB Wi-fi dongle - found an unused one, origially bought on Amazon (newer Pi boards feature a built-in Wi-Fi)
- SD-card 8 Gb - Raspberry Pi uses SD-cards as "hard" disks to boot from. I found one in my closet.
- Raspberry Pi case - bought on Amazon too; not required, but you are less afraid of moving a working piece around;
- Micro-USB power cord - found unused in my closet
- Mouse, Keyboard, HDMI cable (or a less digital RCA cable), and USB hub - found in my closet; used for initial set-up only and not required to run, update, or write software
Total cost: around $200.
How to Run
There's basic documentation in the README file, and sample command lines are in the daemon startup script for this spec. Basically, you select a stop on nextmuni.com to find out your stop ID, and run client/client.rb --stopId 12345. Alternatively, you can run a morning dashboard by specifying up to two routes and stops in text, and add an URL to retrieve weather from weather.gov:
PI configuration notes
During the first run, connect the mouse, the keyboard, and the Wi-Fi dongle (if you need one) through the USB-hub. Follow up the Getting Started guide to download the initial Pi OS image onto the SD card. Boot it up; install the default Raspbian Debian-based distro.
Do select "automatically boot desktop environment on startup" when asked. While you won't need it, it's the easiest way to configure Wi-Fi. Once the desktop appears, select "Wi-Fi configuration" under one of the menus accessed via the Start button, and configure the wi-fi card you inserted to connect to the router. Make sure you also configure it to start up wi-fi automatically at boot.
Configure SSH server, and make sure it starts up at boot. Either configure hostname, or write down IP address. Reboot and try to SSH from another machine. If this works, disconnect mouse, keyboard, and HDMI: you don't need them anymore.
Make sure git is installed via sudo apt-get install git, download the sources, and follow the README instructions to install language runtimes. Play with the script's command line options to figure out what you want to run, and configure a daemon by installing the initscript. Once confident enough in resilience of the setup, mount this onto your wall.
So the solution helped: I stopped running like a Japanese schoolboy, and started to control my morning procrastination, too. As a bonus, I had a ballpark estimate of how much it should cost taxpayers to program this sign, and, of course, I had a lot of fun too. Finally, I now have a geeky item in my living room... although it looks to others as a stream of stock prices rather than of Muni arrival times.
The rest are less "hot" sections, detailing my experience with the tools I used for the job.
Raspberry Pi's default Linux, as a Debian derivative, contain decent support for Ruby, including 1.9. which is a bit old already, though. It takes Pi several seconds to load the interpreter and a dozen of gems I needed for this sign, but it's OK since the program only starts up once when the system is powered up.
So the sign I used provided a Perl API, so it would be natural to write the client program in Perl, too... not really. While I have a lot of experience programming in Perl, I would much rather prefer writing in a more interesting language, such as Python or Ruby. That was one of the reasons why I wrote a small wrapper over the Perl API, so that I could exec this simple program from any other language.
Another reason why I wrote this API was that the sign does not support two-line text in its built-in text renderer. The API allows you to supply a line of text, and the sign will happily render it, and even add some effects, but I wanted two lines in a smaller font instead. API doesn't have a "render image" function, but the sign allows you to create your own font glyphs, and insert them into text. The obvious idea of printing one 16x96 "symbol" worked. The wrapper does exactly this.
The sign can also loop through several images--just like the real Muni sign. API makes this very easy to program: this happens automatically when you send several messages. Integration with Pi went smoothly: just use $sign->send(device => "/dev/ttyUSB0"); in your program.
There were several glitches, though. First, each glyph is automatically centered, unless it's greater than about 80 pixels wide, in which case it's aligned to the left. Looping through differently aligned messages looks badly, so I force all images I send to the sign as 16x96, padding them with black pixels.
Second, there is no way to power off the sign programmatically. To power it off, I need to physically remove the USB connection, and turn it off because it starts using the battery otherwise. The sign manufacturer's intent probably was that you don't re-program the sign often, so you would program it once and then turn on and off manually. But we're programmers too; isn't working around things our bread and butter? Displaying an all-black picture did the trick of turning the sign "off".
Third, I refresh the sign each 30 seconds to ensure timely prediction, but it takes 1-2 seconds to update the sign. I suppose this is because the sign contains its own CPU and programming, or because I didn't try to play with send function arguments.
Although its internal plumbing probably affects the update speed, and I saw examples of offloading all the synchronization and flashing of the lamps to Raspberry Pi completely with different, more low-level signs, that would be too time-consuming for me to bother with. I also don't know how long the sign will last when it's re-programmed twice a minute. So far, I'm quite happy with the Bright LED sign I used and its API.
I expected to spend zero time on font rendering, but it took much, much longer than I expected. Well, I was expecting that writing a custom font rendering engine would be simple, but boy was I proven wrong.
First, I couldn't find a "raster" font that is just a set of bitmaps. I ended up downloading "pixelated" TTF fonts and rendering them with FontConfig library. I didn't want the sign to have a lot of dependencies, so I pre-rendeded the glyphs on my Pi in a ready-to-use manner with this script, and wrote a small library to operate them. The most fun part is that I could easily add new glyphs, such as rainfall indicators.
We also take a lot of font rendering for granted, such as aligning to center, word wrapping, kerning. I implemented some of them in a very simple manner (in Ruby!) but I feel that a couple more features would make me ragequit in favor of a real font rendering library instead.
Accessing Muni Predictions
Another reason why I chose Ruby was the Muni library in Ruby, ready-to-use for getting predictions from the NextBus system. Or so I thought: the library was missing some functionality, such as refining routes or getting predictions for all routes of a stop (this is what a Muni sign displays). It also pulled in the whole Rail stack only to display nicely formatted time string! (like, convert interval in seconds to something fancy as "over 1 hour").
I fixed some of these things, and bundled the gem with the original source
for your convenience because it was easier than forking it and adding it into an official repository.
Apparently, official Muni monitors operate in the same terms: routeConfig is one of the API commands of NextBus service.
The only thing I didn't find was the actual names of the
Weather Forecast Retrieval
Oh, well, here comes the easy part," I thought when I finished with everything else, and decided to throw in weather forecasts. I was wrong in a very interesting way.
Turns out, weather prediction APIs are usually not free. The websites I found at the top of Google search all required signups and including your personal key to all API calls. Many had free limited versions but required payment for hourly forecast--and I was mainly interested in conditions at a specific place at a specific hour of the day (San Francisco weather isn't very stable). Not that I'm opposed to paid services per se, or to parsing web pages with regexps, but I don't need that much from them to pay, do I?
I proceeded with a free government weather.gov service, which is actually kind of cool despite the web design of the last century. The service is limited to the U.S., but San Francisco Muni predictions are not very useful outside of the country either. To use it, you need to provide a link to XML table for your city/area, which would look like this: http://forecast.weather.gov/MapClick.php?lat=37.77493&lon=-122.41942&FcstType=digitalDWML.
I didn't use their API, because it's a full-blown SOAP, and ain't nobody got time for that.
Surprisingly, I don't have much to say about this. It just works! I didn't have to use it, and I could have used a desktop PC. The best part about Pi is that it works backwards, too: I successfully replaced desktop PC with it without doing anything differently. The device's operating system is simply a variation of Debian Linux, as found on desktops. I didn't have to debug through COM-port nor use specialized software to upload programs: I downloaded compilers and interpreters directly from the internet onto the device!
And now it's neatly powered from a single micro-usb port, and doesn't hike my electric bill despite being always on.
That's all I have to say for now. I need to add a couple of things, such as Muni Messages that remind users about non-working elevators and fare increases, to make the Muni experience complete. I also need automatic sign power-off when unused. I heard, there are motion detectors for Raspberry Pi, but a simple schedule would be nice, too.
Comments imported from the old website
Author Paul Shved
Modified August 8, 2013
License CC BY-SA 3.0