Erik's Engineering

something alliterative

AJAX temperature display

The existing temperature display (a graph of time vs temperature) doesn't really cover all my needs. It's great for analyzing how things relate, but it isn't very good at answering the questions like "how cold is it outside?". For that, I want something like this:

It'll show the temperatures from any sensors that have reported back in the last half hour and it will keep updating so I don't have to reload the page to see what the current status is. That way, I can just leave the page up on the TV in the living room and have the temperature displayed with no fuss whatsoever.

Rather than reload the entire page every 30 seconds or a minute, I'll do the updates with some nice javascript.

The first step is to make the latest temperature from each source available conveniently. To do that, I'll need a new action with a resource (URL) that makes sense for retrieving a temperature based on a source.

First, change the map.resources entry for sources to:

  map.resources :sources do |source|
    source.resources :fahrenheit_temps, :collection => [:latest]
  end

This lets us access the latest temperature for a source at http://hostname/sources/1/fahrenheit_temps/latest.json. The exact suffix on latest doesn't matter - it could be html or xml. fahrenheit_temps is there because a) I'm going to serve it out of the fahrenheit_temps controller and b) in the future I might have multiple sensors with the same source (e.g. humidity, voltage, light). I'm trying to future proof the URL here.

You can use rake routes to help make sure that new routes like this are being interpreted properly. Run the command and pipe the results through grep (there's probably a LOT of output) to make sure things look right. That's the best way I've found to make sure I'm getting the URL I want. In this case, it took me a couple tries to avoid having it expect http://hostname/sources/1/fahrenheit_temps/1/latest.json which wouldn't have worked at all.

slim:data_logger edebill$ rake routes | grep latest
latest_source_fahrenheit_temps        /sources/:source_id/fahrenheit_temps/latest(.:format)   {:controller=>"fahrenheit_temps", :action=>"latest"}
slim:data_logger edebill$ 

Next, add a new method to fahrenheit_temps_controller.rb:

  def latest
    @source_id = params[:source_id]
    @fahrenheit_temp = FahrenheitTemp.find(:first, 
                                           :conditions => { :source_id => @source_id},
                                           :order => 'created_at desc')

    respond_to do |format|
      format.html { render :action => 'show' }
      format.xml  { render :xml => @fahrenheit_temp.to_xml(
                                                     :include => [:source],
                                                     :methods => [:display_temp,
                                                                 :display_time]) }
      format.json  { render :json => @fahrenheit_temp.to_json(
                                                     :include => [:source ],
                                                     :methods => [:display_temp,
                                                                  :display_time] ) }
    end
  end

Man, that looks ugly, but all the heavy lifting is in the first two statements. They take the :source_id parameter and use it to find the single most recent FahrenheitTemp with that source_id. The rest of it is just formatting the output. Since this is just a temperature, we can reuse the HTML display from the "show" action. For XML and JSON, I'm going to include some other stuff. :include => [:source] tells it to include the Source object that this temperature belongs to. That will give me access to the name of this source, instead of just the number. :methods => [:display_temp, :display_time] pulls in two additional methods from my model that give pretty versions of the time and temperature.

All this customization of XML and JSON output is essentially putting view logic in the controller. That's generally bad, but in this case that's idiomatic Rails - and I'm not about to replace 3 or 4 lines of ruby with a template file. If it got any more complex I'd probably do that, though.

It's important to include the time in the display so that people are reassured that your data is current. That's always a good idea when you're displaying time sensitive data and will help keep people from needlessly refreshing the page.

We've got access to the data, but it's going to be really slow without one more step. We need an index on the created_at column on fahrenheit_temps. Without this, the DB will have to scan the whole table to find the most recent entry.

Use script/generate migration add_index_to_fahrenheit_temps_created_at to create a migration that looks like this:

class AddIndexToFahrenheitTempsCreatedAt < ActiveRecord::Migration
  def self.up
    add_index :fahrenheit_temps, :created_at
  end

  def self.down
    remove_index :fahrenheit_temps, :created_at
  end
end

At this point, the app is ready for AJAX code to display all of this. For that, I add jQuery to the standard layout and give myself a way to add javascript to the HEAD block of each page.

Here's the relevant code I added inside the HEAD of my application layout:

    <%= javascript_include_tag 'jquery-1.4.1' %>
    <% if @custom_script %>
    <script type="text/javascript">
      <%= @custom_script %>
    </script>
    <% end %>

All that does is add a tag to import the jquery library (which must be added to public/javascripts) and then insert a script block if a special @custom_script variable is set during an action.

This lets me write a simple action that finds the sources that have had temperatures recorded recently and sets that variable like so:

    @custom_script =  <<SCRIPT
var updateTemp = function updateTemp(source) {   $.getJSON('/sources/' + source + '/fahrenheit_temps/latest.json', function(data) {
         $('#temp_source_' + source).replaceWith('<div id="temp_source_' + source + '"  class="big_data_block">  <div class="big_data_source"&gt;' + data.fahrenheit_temp.source.name + '</div><div class="big_data_value">' + data.fahrenheit_temp.display_temp + '</div><div class="big_data_time">' + data.fahrenheit_temp.display_time + '</div></div>') } );
}   
 $(document).ready(function() {
 SCRIPT

    @sources.each do |s|
      @custom_script += <<SCRIPT
  $('#content').append('<div id="temp_source_#{ s }" class="big_data_block"></div>')
  updateTemp(#{ s });
  window.setInterval("updateTemp(#{ s })", 30000);

SCRIPT
    end

    @custom_script += "});"
  end

This is a mixture of three different languages embedding each other, so lets step through it. First, I create a function called updateTemp() that will hit that new JSON resource for the latest temperature from a source and use the results to replace the contents of a div with the new data.

Then I create a document ready function that a) puts a div into the page for each source, b) calls updateTemp() to load some initial data into that div, and c) sets up a recurring event to call it again every 30 seconds.

All that's left at this point is styling those divs - which have their classes declared nicely so it's easy enough to call them out via stylesheet.

With a little more work, I could have had all the processing done by javascript (notice I rely on the initial page load to figure out what sources to care about). Right now it's impossible to write an outside app that duplicates this functionality because you can't find that list of most recent sources.

I might also have gotten away with a single event that cycled through the different readings instead of doing them all in parallel. That would be nicer for my server, but probably isn't a big deal for the number of sensors we're talking about here.

If I'd gone for a slower refresh (5 minutes or more) I'd want to update my passenger configuration so that it kept application instances around that long between calls. As it is, the default is 5 minutes, so this 30 second refresh should get existing instances every time.

(As always, all the code for this is up on GitHub)

Published on 14/03/2010 at 14h32 under , . Tags , , ,

  • By errordeveloper@gmail.com 04/09/2010 at 22h43

    That’s quite interesting .. I’d like to start with a project on rails to control lighting via DMX, using a Plug Computer and perhaps OpenDMX USB ..

    i’m a bit puzzled, cause Rails is a new thing to me and i’m mostly puzzled about what use for the interface - SVG or jquery GUI ..

    please reply if you got any ideas!


  • By Erik 23/09/2010 at 00h10

    I have to vote for jQuery to do the javascript part of the UI. It’s very nice to work with. Great community, lots of functionality, and I haven’t run into cases where it breaks for specific browsers.

    If you’re new to Rails, I’d suggest starting out doing everything with normal page loads. That removes javascript as one of the things that can go wrong. Get it working without the javascript/AJAX and then go back to add the extra fancy bits :)

    Good luck, and drop me a line if you get stuck on something specific.


Comment AJAX temperature display

Trackbacks are disabled

Powered by Typo – Thème Frédéric de Villamil | Photo Glenn