IoT-Enabled Desktop Applications Using Electron

Each day Internet of Things connects more and more devices. Often IoT means the presence of a dashboard/Web UI. But it can also be useful in desktop applications.

Let’s imagine we want to perform some action on the desktop each time the user interacts with some device. It can be easily implemented using the AWS IoT and the Electron framework.

As you may know, AWS IoT provides an MQTT-over-websockets support that can be used to build IoT-enabled web applications. While it’s a great feature, sometimes we may want to provide an interface that will be more high-level. In this case, the client application doesn’t need to know anything about the MQTT protocol or AWS IoT. Unfortunately, at this moment AWS doesn’t provide any ready-to-use service for sending notifications via websockets.

Since it’s not always possible to use a third-party service for push notifications, and data streaming, we can create a custom one. It will have an architecture like this:

Here is how it supposed to work:

  1. User clicks the AWS IoT Button
  2. AWS IoT Rules engine executes a rule that sends an SNS notification
  3. SNS notification is delivered to an HTTP endpoint(socket.io-based web application)
  4. Web application proxies all received HTTP requests via websockets to all connected clients

Note it’s just a PoC(proof-of-concept), so it doesn’t include any authorization or other security measures. Please, don’t use it directly in production environments!

Alternatively, it’s possible to use the HTML5 Push API. Since it doesn’t require a persistent connection from the server side, it would eliminate the need of AWS BeanStalk(AWS EB) instance and would make the solution completely serverless. But for demonstration purposes, we will be using websockets-based approach.

Implementation

Let’s assume we will be notifying all clients about the click of any AWS IoT Button connected to the AWS IoT. The configuration of the button itself is not described in this post.

AWS EB Socket.io Server

First, let’s create a simple socket.io application that forwards HTTP requests to connected websockets clients:

'use strict';

const express = require('express')
const app = express()
const server = require('http').createServer(app)
const io = require('socket.io')(server)
const bodyParser = require('body-parser')
const request = require('request')

const overrideContentType = function(req, res, next){
  if (req.headers['x-amz-sns-message-type']) {
      req.headers['content-type'] = 'application/json;charset=UTF-8'
  }
  next()
}

app.use(overrideContentType)
app.use(bodyParser.json())

app.get('/', function(req, res,next) {
    res.send({'Health': 'OK'})
});

app.post('/forward-event', function(req, res){
  if (req.headers['x-amz-sns-message-type'] === 'SubscriptionConfirmation'){
    request.get(req.body.SubscribeURL)
  } else {
    io.sockets.emit('event', req.body)
  }
  res.send({'Success': true})
})

server.listen(80)

As you can see, the app is very simple. The only magic is a middleware needed for proper handling of SNS subscription confirmation. To make the process of the deployment as simple as it possible, let’s also place the application inside a Docker container(we can deploy it directly to the AWS EB!):

FROM node
MAINTAINER AgileVision

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY package.json /usr/src/app
RUN npm install

ADD . /usr/src/app

EXPOSE 80
CMD [ "npm", "start" ]

No rocket science here either. To help AWS EB deploy our application correctly, we need to add a small piece of AWS-specific configuration — a Dockerrun.aws.json file with the following contents:

{
  "AWSEBDockerrunVersion": "1",
  "Ports": [
    {
      "ContainerPort": "80"
    }
  ]
}

This is just a hint that AWS EB will be using to expose required ports correctly.

SNS Configuration

To create a reusable configuration for AWS IoT rules and SNS, we will be using a CloudFormation script. Our script accepts AWS EB URL as a parameter and creates corresponding SNS topics and endpoints for forwarding data from AWS IoT to AWS EB and then to a websocket.

Here is the script itself:

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  EventForwarderUrl:
    Type: String
    Description: "URL of the event-forwarder endpoint"
Resources:
  IoTButtonsClickTopic:
    Type: "AWS::SNS::Topic"
    Properties:
      DisplayName: IoT Button Clicks Topic
      TopicName: IoTButtonsClick
  IoTRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "iot.amazonaws.com"
            Action:
              - "sts:AssumeRole"
  IoTRolePolicy:
    Type: "AWS::IAM::Policy"
    DependsOn:
      - IoTRole
    Properties:
      PolicyName: IoTRolePolicy
      Roles:
        - !Ref IoTRole
      PolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: "Allow"
          Action:
            - "sns:*"
          Resource: "*"
  IoTButtonClickRule:
    Type: "AWS::IoT::TopicRule"
    DependsOn:
      - IoTButtonsClickTopic
      - IoTRole
      - IoTRolePolicy
    Properties:
      RuleName: IoTButtonClickRule
      TopicRulePayload:
        RuleDisabled: "true"
        Sql: >-
          SELECT * FROM 'iotbutton/+'
        Actions:
          - Sns:
              RoleArn: !GetAtt IoTRole.Arn
              TargetArn: !Ref IoTButtonsClickTopic
              MessageFormat: JSON

  EventForwarderSubscription:
    Type: "AWS::SNS::Subscription"
    DependsOn:
      - IoTButtonsClickTopic
    Properties:
      Endpoint: !Ref EventForwarderUrl
      Protocol: http
      TopicArn: !Ref IoTButtonsClickTopic

While it’s rather verbose, still it’s easy to read(at least once you get used to CloudFormation).As you can see, the script accepts one parameter — EventForwarderUrl. It’s an URL of the endpoint to which the SNS should post data that’s coming from the AWS IoT.

Desktop Application

Electron allows creating cross-platform applications using HTML and JavaScript. It’s built on a top of Chromium and NodeJS runtime so we can use goodies from both client-side and server-side JavaScript worlds!

The application itself is rather simple:

const {app, Menu, Tray, shell, dialog} = require('electron')
const path = require('path')
const socketClient = require('socket.io-client')
const serverUrl = 'http://<REPLACE-WITH-YOUR-AWS-EB-CNAME>/';

const iconPath = path.join(__dirname, 'Icon.png')
let tray = null

app.on('ready', () => {
  tray = new Tray(iconPath)
  const contextMenu = Menu.buildFromTemplate(
    [
        {
          label: 'Exit',
          selector: 'terminate:',
        }
    ]
  )

  tray.setToolTip('AWS IoT Demo')
  tray.setContextMenu(contextMenu)

  socket = socketClient(serverUrl)

  socket.on('connect', function(){
    console.log('Connected to the server', serverUrl);
  });

  socket.on('disconnect', function(){
    console.log('Disconnected from the server');
  });

  socket.on('event', function(event){
    dialog.showMessageBox(
      {
        type: 'info',
        title: 'Received an event',
        detail: 'Received an event: ' + event.Message
      }
    )
  })
})

Note that AWS EB CNAME must be assigned to the “serverUrl” variable.

Deployment

Since SNS references AWS EB URL, we need to deploy the socket.io application first. Use the following command:

eb init .

Follow the instructions on the screen. Once finished, create a new environment:

eb create

Specify the environment name and load balancer type(Classic). Copy the CNAME value — it’s required for the SNS configuration.

Now it’s time to create AWS IoT rules and SNS configuration by running the CloudFormation script:

aws cloudformation create-stack --stack-name="IoTElectron" --region=<region-of-the-button> --template-body=file://cloudformation.yml --capabilities=CAPABILITY_IAM --parameters=ParameterKey=EventForwarderUrl,ParameterValue=http://<CNAME-VALUE>/forward-event

Ensure AWS IoT rules and the SNS configuration are in the same region as your AWS IoT Button configuration or notifications won’t work!

Go to the Electron application directory and launch the application:

npm start

The application icon will appear in the system tray:

Tray Icon

Click the AWS IoT Button. A message box like this should appear:

Message Box

Conclusion

AWS IoT Rules Engine is a powerful tool that allows forwarding events utilizing various AWS services - Lambda, SNS, SES and others. Combining it with software like socket.io allows us to create applications with real-time notifications about IoT events.

Useful Links

Here are some links that you might find useful:

Comments

©2016-2018 — AgileVision sp. z o.o. All rights reserved.