DigiSpark Adventures: A Poor Man's Chumby

By @zachfeldman

The DigiSpark

I’ve been taking some time recently to explore technology outside of my usual domain of web application programming in the form of small electronics projects. My goal was to learn more about the world of hardware and understand what makes the devices I use every day tick! I got an Arduino kit for the holidays last year and I’d done the beginner’s LED project but then put it down after that for a few months. I think I wanted something a little more concrete, something I wouldn’t be afraid to solder into a project.

The Delightful DigiSpark

The answer was found in a Kickstarter project that I backed almost a year ago to produce a tiny USB Arduino-like development board called a DigiSpark. I ordered a soldering kit from AdaFruit and to begin, I attempted the beginner’s multicolored LED and the genius Reddit Karma Controller

Karma Controller http://karmacontroller.com

After these trials, I proceeded to order up a 16x2 LCD screen kit. I soldered together the breadboard and after connecting it to the headers on the DigiSpark, I was able to run the short sample script and see some output! Although I’d taken a class on Processing before and therefore had some familiarity with the Arduino language (both are based on Wiring), I wanted to be able to interact with my creation using Ruby to display some useful weather and stock information. I was on my way to building a poor man’s Chumby!

I found the wonderful DigiUSB Gem, which provided a wrapper for communicating with the DigiSpark.

1
2
spark = DigiUSB.sparks.last spark.puts "Hello World"

The above displayed my Hello World message on the LCD with ease!

Hello World on 16x2

But what if I wanted to encode more complicated messages? I needed to figure out a way to split up longer text blocks and break up my messages into “frames” to display text, then pipe them to the LCD. I cracked open Sublime and got to work writing the method.

There were a surprising number of cases to handle here, to begin with:

  1. What if the message was multiple frames?
  2. What if the message was only equal to one line length?
  3. What if the message had individual words that were more then 16 characters?

Many more cases came up as I was testing the code with various samples of text including stock and weather information. This was a little bit more difficult then I realized it would be. I’d like to eventually write a test suite for all of these different cases, but in this initial iteration I thought that might be a bit of overkill. Looking back, I should’ve just done this from the beginning! Here’s the encode_frames method as it stands:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def encode_frames(message, final_message=[]) if message == nil final_message.push ["Message was empty.", " "] else if message.length <= 16 final_message.push [message, " "] else line_1 = [] line_2 = [] final_encoded = false line_1_open = true line_2_open = true leftover_words = [] final_message_pushed = false split_words = split_words_sixteen(message) split_words.each_with_index do |word, n| line_1_open = false if (line_1.join(" ") + " " + word).length > 16 && line_1_open line_2_open = false if (line_2.join(" ") + " " + word).length > 16 && line_2_open if (line_1.join(" ") + " " + word).length <= 16 && line_1_open line_1.push word elsif (line_2.join(" ") + " " + word).length <= 16 && line_2_open line_2.push word end if !line_1_open && !line_2_open && !final_message_pushed final_message.push [line_1.join(" "), line_2.join(" ")] final_message_pushed = true elsif !line_1_open && !line_2.empty? && !final_message_pushed && split_words[n] != nil && (line_2.join(" ") + " " + split_words[n]).length > 16 final_message.push [line_1.join(" "), line_2.join(" ")] final_message_pushed = true elsif !line_1_open && line_1[-1] == word && !final_message_pushed final_message.push [line_1.join(" "), " "] final_message_pushed = true elsif !line_1_open && line_2[-1] == word && split_words[-1] == word && !final_message_pushed final_message.push [line_1.join(" "), line_2.join(" ")] final_message_pushed = true message_over = true end if final_message_pushed && !message_over leftover_words.push word end end if leftover_words.empty? final_message else encode_frames(leftover_words.join(" "), final_message) end end end end

Once this encode_frames recursive function was finally built and fairly well manually tested, I wrote a simple display_message function to call it and a display_frames function to take the generated frames and display them on the DigiSpark with a 1.5 second delay between each screen. I was able to display whatever message I pleased!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def display_message(message, spark, options={}) puts message frames = encode_frames(message) if options[:loop] options[:loop].to_i.times do display_frames(frames, spark) end else display_frames(frames, spark) end end def display_frames(frames, spark) puts frames.inspect frames.each do |frame| spark.puts "#{frame[0]}\n#{frame[1]}" sleep(1.5) end end

Now that the basics were taken care of, I thought I’d whip out some fancy web applications stuff and connect this guy up to a server. I began writing a basic Sinatra app with the concept of “apps”, or modules that could be added on to display many different things. I wanted to get weather information and display it on my little LCD. I’d heard that forecast.io had an awesome API to use so I went ahead and used that through their gem, forecast_io. I’d need a method to encode my location into latitude and logitude coordinates as well, so I used the standard Geocoder gem. Here are my methods to fetch the weather and encode the lat/lon coordinates:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def get_coordinates(params) if !params[:address].nil? coords = Geocoder.coordinates(params[:address]) elsif !params[:zip].nil? coords = Geocoder.coordinates(params[:zip]) elsif request.ip != "0.0.0.0" || request.ip != "127.0.0.1" coords = Geocoder.coordinates(request.ip) else raise "Unable to geolocate you." end end def get_forecast(params) coords = get_coordinates(params) forecast = ForecastIO.forecast(coords[0], coords[1]) final_forecast = "" icon = forecast["daily"]["data"][0]["icon"] final_forecast += capitalize(icon + " ") if RAINY.include?(icon) if forecast["daily"]["data"][0]["precipProbability"] != 0 final_forecast += forecast["daily"]["data"][0]["precipProbability"].to_s[2..-1] + "% " end end final_forecast += "#{forecast['daily']['data'][0]['summary']}" final_forecast end

Keep in mind that I have my ForecastIO config in a separate configuration file with the necessary API key. I decided an address would be the most accurate way to get lat/lon followed by zip code and then by IP. IP isn’t used for now since this is only running locally but it’s there if it ever gets deployed somehow. I also wanted to show the percentage of precipitation if it was the kind of day that warranted it. The RAINY constant is defined by one of the app’s configuration files and defines the kinds of “icon” data types that equal a rainy day, including ‘Partly-cloudy-day’ and others. Here’s the display showing weather info:

App Structure

I structured the Sinatra app based off of a great Stack Overflow post describing an easily extensible structure. This should make it easy for anyone to add plugins to display whatever content they’d like!

App Structure

To add a plugin to the app, one just needs to add a file under apps/ with their desired endpoint and any helpers under helpers/. The display_message method will display the method easily on the DigiSpark. For instance, here’s the apps/weather.rb file.

1
2
3
4
5
6
class DigiLcdServer < Sinatra::Application post '/apps/weather' do display_message(get_forecast(params), SPARK, {loop: params[:loop]}) redirect "/" end end

I also wrote a stocks app.

App Structure

Pull requests and additional “apps” for this project are incredibly welcome! Check out the repo at https://github.com/zachfeldman/digi-lcd-server

I’m excited to keep playing around with more DigiStump stuff like my Charlieplex kit and a few other goodies this weekend! I also have a bluetooth module waiting to be hooked up. The hardware bug has bit me…hard!





Sign up for our e-mail list to hear about new posts.