node.js, socket.io, and real-time web HMI example

Using node.js, socket.io, and express I created a simple real-time HMI for controlling valves. This method can be used to control any device including a network connected arduino or raspberry pi, local USB connected IO boards, or any other device that can communicate with web sockets. In this simple web based HMI clients connect and enter their name. Clients are then allowed to send and receive chat messages or press the open and close buttons associated with each valve. Security and access restrictions were not used here, but could be added later.

Before running the example install the latest version of node.js, socket.io and express.

The node.js app is similar to the chat example, with the addition of two functions that listen for close and open commands.

node.js application

var app = require('express')()
  , server = require('http').createServer(app)
  , io = require('socket.io').listen(server);

server.listen(8080);

// routing
app.get('/', function (req, res) {
  res.sendfile(__dirname + '/index.html'); 
});

app.get('/app.css', function (req, res) {
  res.sendfile(__dirname + '/app.css'); 
});

// usernames which are currently connected to the chat
var usernames = {};

io.sockets.on('connection', function (socket) {

// when the client emits 'sendchat', this listens and executes
socket.on('sendchat', function (data) {
// we tell the client to execute 'updatechat' with 2 parameters
io.sockets.emit('updatechat', socket.username, data);
});

// when the client emits 'opencmd', this listens and executes
socket.on('opencmd', function (data) {
// Add calls to IO here
io.sockets.emit('opencmd', socket.username, data);
setTimeout(function () {
  io.sockets.emit('openvalve', socket.username, data);
}, 1000)
});

// when the client emits 'closecmd', this listens and executes
socket.on('closecmd', function (data) {
// Add calls to IO here
io.sockets.emit('closecmd', socket.username, data);
setTimeout(function () {
  io.sockets.emit('closevalve', socket.username, data);
}, 1000)
});

// when the client emits 'adduser', this listens and executes
socket.on('adduser', function(username){
// we store the username in the socket session for this client
socket.username = username;
// add the client's username to the global list
usernames[username] = username;
// echo to client they've connected
socket.emit('updatechat', 'SERVER', 'you have connected');
// echo globally (all clients) that a person has connected
socket.broadcast.emit('updatechat', 'SERVER', username + ' has connected');
// update the list of users in chat, client-side
io.sockets.emit('updateusers', usernames);
});

// when the user disconnects.. perform this
socket.on('disconnect', function(){
// remove the username from global usernames list
delete usernames[socket.username];
// update list of users in chat, client-side
io.sockets.emit('updateusers', usernames);
// echo globally that this client has left
socket.broadcast.emit('updatechat', 'SERVER', socket.username + ' has disconnected');
});

});

The client side web page and css file are also similar to the chat example and include the face plates for the valve control.

index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"  dir="ltr">
<meta http-equiv="X-UA-Compatible" content="IE=9" />
<head><title>SCADA Valve Control Example</title>
<meta name="viewport" content="initial-scale = 1.0,maximum-scale = 1.0" />
<link type="text/css" rel="stylesheet" href="/app.css" media="all">
</head>
<script src="/socket.io/socket.io.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script>
var socket = io.connect('127.0.0.1:8080'); //set this to the ip address of your node.js server

// on connection to server, ask for user's name with an anonymous callback
socket.on('connect', function(){
// call the server-side function 'adduser' and send one parameter (value of prompt)
socket.emit('adduser', prompt("What's your name?"));
});

// listener, whenever the server emits 'updatechat', this updates the chat body
socket.on('updatechat', function (username, data) {
$('#conversation').append('<b>'+username + ':</b> ' + data + '<br>');
});

// listener, whenever the server emits 'updateusers', this updates the username list
socket.on('updateusers', function(data) {
$('#users').empty();
$.each(data, function(key, value) {
$('#users').append('<div>' + key + '</div>');
});
});

// listener, whenever the server emits 'updateusers', this updates the username list
socket.on('updatevalve', function(data) {
$('#users').empty();
$.each(data, function(key, value) {
$('#users').append('<div>' + key + '</div>');
});
});

// listener, whenever the server emits 'openvalve', this updates the username list
socket.on('openvalve', function(username, data) {
$('#' + data + ' > div.feedback > div.circle.status').removeClass('red').addClass('green');
});
socket.on('opencmd', function(username, data) {
$('#' + data + ' > div.feedback > div.circle.status > div.circle.command').removeClass('red').addClass('green');
});

// listener, whenever the server emits 'openvalve', this updates the username list
socket.on('closevalve', function(username, data) {
$('#' + data + ' > div.feedback > div.circle.status').removeClass('green').addClass('red');
});
socket.on('closecmd', function(username, data) {
$('#' + data + ' > div.feedback > div.circle.status > div.circle.command').removeClass('green').addClass('red');
});
// on load of page
$(function(){
// when the client clicks SEND
$('#datasend').click( function() {
var message = $('#data').val();
$('#data').val('');
// tell server to execute 'sendchat' and send along one parameter
socket.emit('sendchat', message);
});

// when the client hits ENTER on their keyboard
$('#data').keypress(function(e) {
if(e.which == 13) {
$(this).blur();
$('#datasend').focus().click();
}
});

// when the client clicks OPEN
$('.open').click( function() {
var id = $(this).parent().attr("id");;
//console.log(id);
socket.emit('opencmd', id);
});

// when the client clicks CLOSE
$('.close').click( function() {
var id = $(this).parent().attr("id");;
//console.log(id);
socket.emit('closecmd', id);
});

});

</script>
<body>
<p>
<div id="userlist">
<b>USERS</b>
<div id="users"></div>
</div>
<div id="messages">
<input id="data"/>
<input type="button" id="datasend" class="send" value="send" />
<div id="conversation"></div>
</div>
<div class="clear"></div>
<div id="valve1001" class="valve">
<h3>VALVE 1001</h3>
<div class="open">OPEN</div>
<div class="close">CLOSE</div>
<div class="clear"></div>
<div class="feedback">
<div class="circle status green">
<div class="circle command red"></div>
</div>
</div>
<div class="clear"></div>
</div>
<div id="valve1002" class="valve">
<h3>VALVE 1002</h3>
<div class="open">OPEN</div>
<div class="close">CLOSE</div>
<div class="clear"></div>
<div class="feedback">
<div class="circle status green">
<div class="circle command red"></div>
</div>
</div>
<div class="clear"></div>
</div>
<div id="valve1003" class="valve">
<h3>VALVE 1003</h3>
<div class="open">OPEN</div>
<div class="close">CLOSE</div>
<div class="clear"></div>
<div class="feedback">
<div class="circle status green">
<div class="circle command red"></div>
</div>
</div>
<div class="clear"></div>
</div>
<div id="valve1004" class="valve">
<h3>VALVE 1004</h3>
<div class="open">OPEN</div>
<div class="close">CLOSE</div>
<div class="clear"></div>
<div class="feedback">
<div class="circle status green">
<div class="circle command red"></div>
</div>
</div>
<div class="clear"></div>
</div>
</body>

app.css

body {
  font-family: Verdana,Tahoma,"DejaVu Sans",sans-serif;
}

p {
  width:100%;
}

.clear {
  clear:both;
}

#userlist {
  float:left;
  width:290px;
  border-right:1px solid black;
  height:100px;
  padding:10px;
  overflow:scroll-y;
}

#messages {
  float:left;
  width:290px;
  padding:10px;
}

#data {
  width:65%;
}

#conversation {
  overflow:scroll;
  width:96%;
  height:150px;
}

.valve {
  float:left;
  overflow:scroll-y;
  padding:20px;
  border:1px solid black;
}

.feedback {
  width:103px;
  height:103px;
  margin-left:auto;
  margin-right:auto;
  margin-top:20px;
}

.open {
-moz-box-shadow:inset 0px 1px 0px 0px #c1ed9c;
-webkit-box-shadow:inset 0px 1px 0px 0px #c1ed9c;
box-shadow:inset 0px 1px 0px 0px #c1ed9c;
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #9dce2c), color-stop(1, #8cb82b) );
background:-moz-linear-gradient( center top, #9dce2c 5%, #8cb82b 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9dce2c', endColorstr='#8cb82b');
background-color:#9dce2c;
-moz-border-radius:6px;
-webkit-border-radius:6px;
border-radius:6px;
border:1px solid #83c41a;
display:inline-block;
color:#ffffff;
font-family:arial;
font-size:15px;
font-weight:bold;
padding:6px 24px;
text-decoration:none;
text-shadow:1px 1px 0px #689324;
}.open:hover {
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #8cb82b), color-stop(1, #9dce2c) );
background:-moz-linear-gradient( center top, #8cb82b 5%, #9dce2c 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#8cb82b', endColorstr='#9dce2c');
background-color:#8cb82b;
}.open:active {
position:relative;
top:1px;
}

.close {
-moz-box-shadow:inset 0px 1px 0px 0px #f5978e;
-webkit-box-shadow:inset 0px 1px 0px 0px #f5978e;
box-shadow:inset 0px 1px 0px 0px #f5978e;
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #f24537), color-stop(1, #c62d1f) );
backgrouclear:both;nd:-moz-linear-gradient( center top, #f24537 5%, #c62d1f 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f24537', endColorstr='#c62d1f');
background-color:#f24537;
-moz-border-radius:6px;
-webkit-border-radius:6px;
border-radius:6px;
border:1px solid #d02718;
display:inline-block;
color:#ffffff;
font-family:arial;
font-size:15px;
font-weight:bold;
padding:6px 24px;
text-decoration:none;
text-shadow:1px 1px 0px #810e05;
}.close:hover {
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #c62d1f), color-stop(1, #f24537) );
background:-moz-linear-gradient( center top, #c62d1f 5%, #f24537 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#c62d1f', endColorstr='#f24537');
background-color:#c62d1f;
}.close:active {
position:relative;
top:1px;
}

.send {
-moz-box-shadow:inset 0px 1px 0px 0px #ffffff;
-webkit-box-shadow:inset 0px 1px 0px 0px #ffffff;
box-shadow:inset 0px 1px 0px 0px #ffffff;
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #dfdfdf) );
background:-moz-linear-gradient( center top, #ededed 5%, #dfdfdf 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed', endColorstr='#dfdfdf');
background-color:#ededed;
-moz-border-radius:6px;
-webkit-border-radius:6px;
border-radius:6px;
border:1px solid #dcdcdc;
display:inline-block;
color:#777777;
font-family:arial;
font-size:15px;
font-weight:bold;
padding:6px 24px;
text-decoration:none;
text-shadow:1px 1px 0px #ffffff;
}.send:hover {
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #dfdfdf), color-stop(1, #ededed) );
background:-moz-linear-gradient( center top, #dfdfdf 5%, #ededed 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#dfdfdf', endColorstr='#ededed');
background-color:#dfdfdf;
}.send:active {
position:relative;
top:1px;
}

.green {
  background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #9dce2c), color-stop(1, #8cb82b) );
  background:-moz-linear-gradient( center top, #9dce2c 5%, #8cb82b 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9dce2c', endColorstr='#8cb82b');
  background-color:#9dce2c;
}

.red {
  background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #f24537), color-stop(1, #c62d1f) );
backgrouclear:both;nd:-moz-linear-gradient( center top, #f24537 5%, #c62d1f 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f24537', endColorstr='#c62d1f');
  background-color:#f24537;
}

.circle {
  -moz-border-radius: 50%;
  border-radius: 50%; 
  display: inline-block;
  margin-right: 20px;
}

.status {
  width: 103px;
  height: 103px;
  position: relative;
}

.command {
  width: 50px;
  height: 50px;
  position: absolute;
  top: 23%;
  left: 23%;
  display: block;
  border: 3px solid white;
  overflow:hidden;
}

Read More