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:
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.
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.
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.
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.
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.
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.
Click the AWS IoT Button. A message box like this should appear:
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.
Here are some links that you might find useful: