Integrating Websocket-Rails with AngularJS (or other JS Frameworks)
As a huge fan of anything and everything related to the Internet of Things, I recently began working on a side-project to make a web-based dashboard that will allow me to control a number of objects around my apartment. My goal was to build a web app that would enable me to:
- Turn on, turn off, dim, and change colors of a LIFX lightbulb and a regular lightbulb connected to an Arduino
- Turn on, turn off, and play certain playlists on my Sonos
- Make a cup of coffee using a coffee maker connected to an Arduino
- Turn on a motion detector (built using an Arduino) and trigger events (e.g., making me a cup of coffee in the mornings when I get up out of bed) and alerts
Initially, my plan was to build a Rails API running on a Raspberry Pi (serving as the control hub for my home) that I could ping over HTTP with various commands and then build an AngularJS front end that would enable me to seamlessly interact with the API.
However, I soon came to realize that a regular HTTP API just wouldn’t work for this sort of application
Because each request to a Rails application creates a new instance of the controller class, I could not open a connection with one of the physical objects (e.g., the LIFX bulb, the multiple Arduinos I’m using, the Sonos) and then maintain a reference to that same connection across requests when modifying something on with a subsequent request.
For example, if I wanted to turn on my LIFX light bulb, I could send a POST request to my LightsController#create which would 1) initialize the connection to my lightbulb and then 2) turn on the lightbulb.
def create
@light = LIFX::Client.lan.discover!.lights.first
@light.turn_on!
end
However, if I wanted to change something about the light, such as the color, brightness, or even turn it off, I would have to 1) send another request to my server to 2) create a new connection to the lightbulb and 3) modify the light as desired.
def set_rgb
@light = LIFX::Client.lan.discover!.lights.first
@light.set_color(LIFX::Color.rgb(r, g, b), duration: 0)
end
Because each request to the server creates a new instance of the controller, I have to continuously rediscover and reconnect to the LIFX light every time I wanted to make a change to the light.
This method creates a huge burden on the server (which, in my case is a poor little Raspberry Pi) and on the object itself because you are constantly pinging it with requests. To give you one example, I have a slider on my dashboard that sends a new value for the light brightness every time you move it. In just a few seconds of moving the slider up and down, a user can generate thousands of API requests to the endpoint (each notch moved on the slider is a request) — each time you have to open a new connection to the LIFX object!
HTTP requests are great for dealing with objects from the database; however, they are not optimal for the Internet of Things — that’s where websockets come in
Now, with a regular database-backed web application, the solution to this is to simply fetch the record in question from the database and then modify it as necessary — such a solution is not possible when working with the Internet of Things as one cannot just fetch a connection to a physical object from a database.
Websockets are the the solution for an Internet of Things applications. They allow you to create a persistent connection between the server and the client. What this means in practical terms, for the application that I’m describing, is that you can easily share objects (i.e., a connection to a physical object) across different methods in your controller and can send data back and forth (yes, websockets allow for easy, 2-way communication between the client and the server!) across this connection without constantly making hundreds of requests.
Since this was my first foray into websockets, I decided to look around and found the amazing websocket-rails gem. It allows you to easily set up an endpoint (ws://youdomain/websocket) that maps very easily to standard controllers and actions using an event mapping that is very similar to standard Rails routes. It allows you to bypass all things Rack-related.
Here are the events that I set up for my lights:
WebsocketRails::EventMap.describe do
subscribe :turn_light_on, ‘api/v1/lights#create’
subscribe :turn_light_off, ‘api/v1/lights#destroy’
subscribe :change_color, ‘api/v1/lights#change_color’
subscribe :change_brightness, ‘api/v1/lights#change_brightness’
end
The idea here was that lights#create would create a connection and turn on my light, light#destroy would turn it off and close the connection, lights#change_color would receive a message (similar to params in the HTTP world) with an RGB value to change the color of the light, and lights#change_brightness would receive a brightness value as a message. Here is my controller for these events:
class Api::V1::LightsController < WebsocketRails::BaseControllerdef initialize_session
controller_store[:light] = LifxInterface.new
enddef create
controller_store[:light].turn_on
enddef destroy
controller_store[:light].turn_off
enddef change_color
red = message[:red]
green = message[:green]
blue = message[:blue] controller_store[:light].set_rgb(red, green, blue)
end def change_brightness
brightness = message[:brightness]
controller_store[:light].change_brightness(brightness)
endend
Notice that I use the controller_store here — this is what allows me to pass objects across the methods of my controller. I only have to instantiate the connection to the light one time and then pass that connection to each of my methods as I need them.
These events are triggered on the client side using a dispatcher object in my Angular controller:
‘use strict’angular
.module(‘app.controllers’)
.controller(‘lightsController’, [‘$scope’, function ($scope) {
var dispatcher = new WebSocketRails(‘localhost:3000/websocket’);$scope.turnLightOn = function(){
dispatcher.trigger(‘turn_light_on’);
};$scope.turnLightOff = function(){
dispatcher.trigger(‘turn_light_off’);
};$scope.changeBrightness = function(lightBrightness){
lightBrightness = lightBrightness/100
dispatcher.trigger(‘change_brightness’, {data: {“brightness”: lightBrightness}});
};$scope.changeColor = function(colorValue){
var splitColors = colorValue.match(/\(.+\)/)[0].replace(“(“,””).replace(“)”,””).split(“,”)
var red = parseInt(splitColors[0])
var green = parseInt(splitColors[1])
var blue = parseInt(splitColors[2])
dispatcher.trigger(‘change_color’, {data: {“red”: red, “green”: green, “blue”: blue}});
}
}])
As you can see, I trigger certain events based on changes to the $scope in Angular. When a user clicks the “turn on” button on the page, the dispatcher sends a “turn_light_on” event that routes to lights#create in my Rails controller.
The problem is that creating this new dispatcher object on the client side isn’t easy if you’re not building your front end in Rails.
The websocket-rails gem only provides the CoffeeScript version of the code required to create the WebSocket object on the client side and this code is packaged in with the gem. If you’re trying to modularize your application, as I am (and as things seem to be trending towards), by having a separate front-end from Rails, then you can’t use the CoffeeScript files packaged with the gem.
I ended up going to the source of the websocket-rails gem, pulling out the CoffeeScript files, converting the CoffeeScript to regular Javascript and then including them in my Angular JS app. You can find them here on my github.
I added the files into my index.html so that the Javascript would load prior to my Angular application itself:
<! — Websocket Rails files →
<script src=”shared/websockets/websocket_rails.js”></script>
<script src=”shared/websockets/event.js”></script>
<script src=”shared/websockets/abstract_connection.js”></script>
<script src=”shared/websockets/http_connection.js”></script>
<script src=”shared/websockets/websocket_connection.js”></script>
<script src=”shared/websockets/channel.js”></script>
Please note that this is the order in which you have to load the files otherwise you may have some issues!
With these files in place you can now easily create the dispatcher in your Angular (or any front end client) application and interact with your Rails websocket back end.
If you have any questions at all or are interested in chatting about Internet of Things applications, please don’t hesitate to reach out to me at all — I love this stuff! Additionally, if you’re going to be at RailsConf in April this year (2015), I’ll be demoing the application I mentioned in a talk I’m giving on the Internet of Things — come say hi!