IoT Saga - Part 3 - Websockets! Connecting LiteCable to Hanami

Hello, Today I’d like to talk about how I’m using Websockets in Hanami. Well, when I was starting I added the following line inside my application.rb but after that I was worried about it.

security.content_security_policy%{
connect-src: ws: 'self';
}

I was using PahoJS to connect to MQTT directly, but I think it isn’t a good option because credentials can be exposed. And, above all I have to control who is receiving and sending data through MQTT in future.

I started to search on the Internet a good option when suddenly I saw this page:

That sounds good, then why not?!

How it works

Vladimir Dementyev(@palkan_tula) recently wrote a post about it, From his point of view Ruby and Rails aren’t the best option for websockets based his experience and benchmarks, a decision has been made then they (Anycable.io) decided to extract the WebSocket responsability to another language, in this case, the language selected was Go.
Anycable-Go deals with the websocket management and many other things without know of any business logic.

To deal with this layer, we need to create our classes to manage our rules, however how does AnyCable WebSocket(Go) connect to a Ruby Application?

They solved this problem using a gRPC client connected to another ruby process like the following picture.

Well, They explain all pieces in this post, it’s very interesting, please check it out.

I chose Hanami as Framework, I was looking for anyone that already made the connection between Hanami and Anycable but I didn’t find anything. That’s is reason why I decided to do it by myself and I will share my experience along this post. Fortunately, Anycable already has a example using Sinatra, I basically followed these steps changing some pieces, let’s start!

Adding pieces to setup

Firstly, we need a script to start our RPC server. I used the following code to start the Anycable RPC server and load all Hanami dependencies.

This server is a rack application then rack is required to run it, and also the ‘config/boot.rb’, which will load all Hanami components using ‘Hanami.boot’. After that the line ‘LiteCable.anycable!’ will enable the anycable compatibility mode. We must configure the class responsible to handle the connections, in this case ‘Usgard::Ws::Connection’. In the end, the server is started, then ‘Anycable::Server.start’ do it.

In sinatra example they’ve shown how start anycable-go and the RPC server using hivemind to start all processes. I use docker-compose, then I added the following lines to my compose file.

Ps:. The variable ‘DATABASE_URL’ must contains the connection string when ‘Hanami.boot’ is executed!

Now, the basic infrastructure is prepared to handle all websocket connections. Finally we can start to add the business logic.

Creating Channels and Connections

LiteCable is a ActionCable implementation, I think Rails defines whole concepts behind it very well. The paragraph below has been extracted from Rails doc.

For every WebSocket accepted by the server, a connection object is instantiated. […] The connection itself does not deal with any specific application logic beyond authentication and authorization. - Rails ActionCable Overview

So, we need to create a connection class to deal with this layer.

moduleUsgardmoduleWsclassConnection<LiteCable::Connection::Baseidentified_by:user,:siddefconnect#Ps:. I don't have authentication in this project, yet.@user='usgard'#cookies["user"]@sid=request.params["sid"]reject_unauthorized_connectionunless@userHanami::Logger.new.info"#{@user} connected"enddefdisconnectHanami::Logger.new.info"#{@user} disconnected"endendendend

Rails defines channels as “a logical unit of work, similar to what a controller does in a regular MVC setup.” So, a Channel class is required, I used the following class for my actuators.

Consuming websockets

“AnyCable uses ActionCable protocol, so you can use ActionCable JavaScript client without any monkey-patching.” - Anycable

I used the same JS of Rails, this JS is available here, and it handles the communication and keeps the websocket connection alive:

We can create our JS abstraction, but not now. I would rather use the cable.js, then I downloaded the JS and added to my application.html.slim

==javascript'cable'==javascript'channel'

But, wait a moment, What’s that channel.js? This JS is responsible to create the channel, I’m using the Revealing Module pattern on my JS, from my point of view it’s a good JS pattern and I guess it can be changed easily later.

App.channel=(function(){functioninit(configuration){returnconfigureCable(configuration);}// configure and create cable using identifier and functionsfunctionconfigureCable(configuration){returncreateCable().subscriptions.create(configuration.identifiers,configuration.functions);}functioncreateCable(){returnActionCable.createConsumer('ws://localhost:8080/cable'+'?sid='+socketId());}// Unique identifier for a connectionfunctionsocketId(){returnDate.now+generateRandomNumber();}functiongenerateRandomNumber(){...}return{init:init}}());

After that we have to create the JS deal with the incomming messages and send them to the WebSocket. I used the following code. I wanna build something like a terminal, which one I have to send messages to actuators channel and receive it.

PS:. I omitted some javascript of examples to turn easier to understand, however this code is available here

App.sensor=(function(){varconfig={container:"display_box",channel:"actuator",user:"usgard",socket:null};functioninit(configuration){config=Object.assign({},config,configuration);config.socket=App.channel.init({identifiers:identifier(),functions:subscriptionFunctions()});addListeners();}functionidentifier(){return{channel:config.channel,id:config.identifier};}functionsubscriptionFunctions(){return{connected:onConnected,disconnected:onDisconnected,received:onReceive}}// These functions will be evaluated when cable trigger the subscriptionsfunctiononDisconnected(){appendMessageToBox({user:'system',message:"Connection Lost"});}functiononReceive(data){appendMessageToBox(data);}// Similar to onReceive functionfunctiononConnected(){// { ... }}// This function will handle the message when enter is typedfunctionaddListeners(){returngetConsoleInput().addEventListener("keydown",function(event){if(event.which==13||event.keyCode==13){onEnter();returnfalse;}returntrue;});}// Sends to ActuatorChannelfunctiononEnter(){config.socket.perform('speak',{message:getMessageFromConsoleInput()});}// Create HTML elementsfunctiongetMessageFromConsoleInput(){// { ... }}// Some other functions { ... }return{init:init}}());

In the end, we have the HTML - in this case I used slim. So, here it is.