Skip to content

Instantly share code, notes, and snippets.

@TotallyInformation
Created October 23, 2013 09:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save TotallyInformation/7115338 to your computer and use it in GitHub Desktop.
Save TotallyInformation/7115338 to your computer and use it in GitHub Desktop.
Example of using Node.js to monitor serial output from an Arduino
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<title>Node/Ardiuno Listener</title>
<meta name="description" content="Node/Ardiuno Listener">
<meta name="author" content="Julian Knight">
<link rel="stylesheet" href="css/styles.css?v=1.0">
<style type="text/css">
table {
border-width: 0 0 1px 1px;
border-spacing: 0;
border-collapse: collapse;
border-style: solid;
}
td, th {
margin: 0;
padding: 4px;
border-width: 1px 1px 0 0;
border-style: solid;
}
</style>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/2.3.0/knockout-min.js"></script>
<script src="libraries/RGraph.common.core.js"></script>
<script src="libraries/RGraph.common.dynamic.js"></script> <!-- Just needed for dynamic features (eg tooltips) -->
<script src="libraries/RGraph.common.annotate.js"></script> <!-- Just needed for annotating -->
<script src="libraries/RGraph.common.context.js"></script> <!-- Just needed for context menus -->
<script src="libraries/RGraph.common.effects.js"></script> <!-- Just needed for visual effects -->
<script src="libraries/RGraph.common.key.js"></script> <!-- Just needed for keys -->
<script src="libraries/RGraph.common.resizing.js"></script> <!-- Just needed for resizing -->
<script src="libraries/RGraph.common.tooltips.js"></script> <!-- Just needed for tooltips -->
<script src="libraries/RGraph.common.zoom.js"></script> <!-- Just needed for zoom -->
<script src="libraries/RGraph.bar.js"></script> <!-- Just needed for Bar charts -->
<script src="libraries/RGraph.bipolar.js"></script> <!-- Just needed for Bi-polar charts -->
<script src="libraries/RGraph.cornergauge.js"></script> <!-- Just needed for CornerGauge charts -->
<script src="libraries/RGraph.fuel.js"></script> <!-- Just needed for Fuel charts -->
<script src="libraries/RGraph.funnel.js"></script> <!-- Just needed for Funnel charts -->
<script src="libraries/RGraph.gantt.js"></script> <!-- Just needed for Gantt charts -->
<script src="libraries/RGraph.gauge.js"></script> <!-- Just needed for Gauge charts -->
<script src="libraries/RGraph.hbar.js"></script> <!-- Just needed for Horizontal Bar charts -->
<script src="libraries/RGraph.hprogress.js"></script> <!-- Just needed for Horizontal Progress bars -->
<script src="libraries/RGraph.led.js"></script> <!-- Just needed for LED charts -->
<script src="libraries/RGraph.line.js"></script> <!-- Just needed for Line charts -->
<script src="libraries/RGraph.meter.js"></script> <!-- Just needed for Meter charts -->
<script src="libraries/RGraph.odo.js"></script> <!-- Just needed for Odometers -->
<script src="libraries/RGraph.pie.js"></script> <!-- Just needed for Pie AND Donut charts -->
<script src="libraries/RGraph.radar.js"></script> <!-- Just needed for Radar charts -->
<script src="libraries/RGraph.rose.js"></script> <!-- Just needed for Rose charts -->
<script src="libraries/RGraph.rscatter.js"></script> <!-- Just needed for Rscatter charts -->
<script src="libraries/RGraph.scatter.js"></script> <!-- Just needed for Scatter charts -->
<script src="libraries/RGraph.thermometer.js"></script> <!-- Just needed for Thermometer charts -->
<script src="libraries/RGraph.vprogress.js"></script> <!-- Just needed for Vertical Progress bars -->
<script src="libraries/RGraph.waterfall.js"></script> <!-- Just needed for Waterfall charts -->
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head><body>
<h2>Data from Arduino</h2>
<canvas id="myCanvas" width="600" height="250">[No canvas support]</canvas>
<canvas id="cvsTemp" width="80" height="250">[No canvas support]</canvas>
<button data-bind="click: btnClick">Click me</button>
<ul id="msg"></ul>
<ul id="error"></ul>
<table>
<thead>
<tr>
<th colspan="2">Temperature</th>
<th colspan="2">Humidity</th>
<th colspan="2">Light</th>
</tr>
<tr>
<th>Max</th>
<th>Min</th>
<th>Max</th>
<th>Min</th>
<th>Max</th>
<th>Min</th>
</tr>
</thead>
<tbody>
<tr>
<td data-bind="text: tMax"></td>
<td data-bind="text: tMin"></td>
<td data-bind="text: hMax"></td>
<td data-bind="text: hMin"></td>
<td data-bind="text: lMax"></td>
<td data-bind="text: lMin"></td>
</tr>
</tbody>
</table>
<div data-bind='text: items().length'>Length: &nbsp;</div>
<table>
<thead>
<tr>
<th>Counter</th>
<th>Time</th>
<th>Elapsed Time</th>
<th>Diff</th>
<th>Temperature</th>
<th>Humidity</th>
<th>Light</th>
<th>Motion</th>
<th>Battery</th>
</tr>
</thead>
<tbody data-bind="foreach: items">
<tr>
<td data-bind="text: loopCount"></td>
<td data-bind="text: timeStr"></td>
<td data-bind="text: elapsed"></td>
<td data-bind="text: loopTime"></td>
<td data-bind="text: temperature"></td>
<td data-bind="text: humidity"></td>
<td data-bind="text: light"></td>
<td data-bind="text: motion"></td>
<td data-bind="text: batteryStatus"></td>
</tr>
</tbody>
</table>
<!--<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>-->
<!--<input data-bind="blah: console.log($data)" />-->
<script>
//var socket;
var prevTime = 0;
//var dataLog = ko.observableArray(); // Initially an empty array
//var dataAdapter;
var dataTemp = [];
var dataHum = [];
var dataLabel = [];
var bar;
var tempGauge;
var canvas = document.getElementById("myCanvas");
var canvasT = document.getElementById("cvsTemp");
function chartUpdate() {
RGraph.Clear(canvas);
if (!bar) {
bar = new RGraph.Line('myCanvas', dataTemp, dataHum)
.Set('title.yaxis', 'Celcius/%')
//.Set('title.yaxis.pos', 0.5)
.Set('title.xaxis', 'Time (s)')
.Set('title.xaxis.pos', 0.5)
.Set('title', 'Temperature & Humidity Log')
.Set('title.vpos', 0.5)
.Set('linewidth',1)
//.Set('xticks', 10)
.Set('shadow', 1)
.Set('chart.filled', true)
.Set('chart.filled.accumulative', false)
.Set('chart.fillstyle', ['rgba(255,0,0,0.3)', 'rgba(0,0,255,0.3)'])
.Set('chart.text.size', 7)
.Set('chart.gutter.left', 35)
//.Set('chart.tickmarks', 'circle')
//.Set('chart.tickmarks.linewidth', 0.2)
.Set('key', ['Temperature', 'Humidity'])
.Set('key.position', 'gutter')
.Set('key.position.gutter.boxed', false)
.Set('key.position.x', 275)
}
bar.Set('chart.labels', dataLabel);
bar.original_data[0] = dataTemp;
bar.original_data[1] = dataHum;
bar.Draw();
};
function tempUpdate(temp) {
RGraph.Clear(canvasT);
if (!tempGauge) {
tempGauge = new RGraph.VProgress('cvsTemp', 0,40,0)
.Set('chart.arrows', true)
.Set('chart.shadow', true)
.Set('chart.title.side', 'Temperature *C')
}
tempGauge.value = temp;
tempGauge.Draw();
};
var DataModel = function() {
var self = this; // make sure we always have access to the correct context
// Place to hold the dynamic data
self.items = ko.observableArray();
self.tMin = ko.observable(9999);
self.tMax = ko.observable(0);
self.hMin = ko.observable(9999);
self.hMax = ko.observable(0);
self.lMin = ko.observable(9999);
self.lMax = ko.observable(0);
self.btnClick = function(data,event) {
console.log('DATA', data);
console.log('EVENT', event);
console.log('ITEMS', self.items());
console.log('ITEMS[0]', self.items()[0]);
console.log('MINMAX', self.minMax());
}
var socket = io.connect(''); // connect locally
// The serial listener detects new data and sends it on
socket.on('arduino', function (recievedData) {
//console.log(recievedData.ardData);
//console.log( parseArdMsg(recievedData.ardData) );
self.addItem( parseArdMsg(recievedData.ardData) );
});
socket.on('disconnect', function () {
$("#error").html('DISCONNECTED');
//setTimeout('window.location.reload()', 3000);
});
socket.on('connect_failed', function () {
$("#error").html('CONNECT FAILED');
});
socket.on('error', function () {
$("#error").html('ERROR');
});
socket.on('reconnect_failed', function () {
$("#error").html('RECONNECT FAILED');
});
socket.on('reconnecting', function () {
$("#error").html('RECONNECTING');
});
socket.on('reconnect', function () {
$("#error").html('RECONNECTED');
});
self.addItem = function( myItem ) {
//console.log( myItem );
self.items.unshift( myItem ); // add to the start
// data for charts
dataTemp.push(myItem.temperature);
dataHum.push(myItem.humidity);
// max/min
if ( myItem.temperature > self.tMax() ) {
self.tMax(myItem.temperature);
console.log("tMax", self.tMax(), myItem.temperature);
}
if ( myItem.temperature < self.tMin() ) {
self.tMin(myItem.temperature);
console.log("tMin", self.tMin, myItem.temperature);
}
if ( myItem.humidity > self.hMax() ) {
self.hMax(myItem.humidity);
}
if ( myItem.humidity < self.hMin() ) {
self.hMin(myItem.humidity);
}
if ( myItem.light > self.lMax() ) {
self.lMax(myItem.light);
}
if ( myItem.light < self.lMin() ) {
self.lMin(myItem.light);
}
// restrict labels on line chart to start and end of chart
if (dataLabel.length == 2) {
dataLabel[1] = myItem.elapsed;
} else {
dataLabel.push(myItem.elapsed);
}
// Update the charts
chartUpdate();
tempUpdate(myItem.temperature);
};
} // ---- End of DataModel ----
function parseArdMsg(msg) {
msg = msg.trim();
if ( msg.substring(0,1) == '{' ) {
return parseArdMsgJSON(msg);
} else {
$("#msg").html('Invalid Arduino Message - unknown msg type - should be JSON - see browser log');
console.log("ERROR: Invalid Arduino Message - unknown msg type - should be JSON");
console.log(msg);
return {};
}
} // ------- End of Function parseArdMsg ------- //
function parseArdMsgJSON(msg) {
var obj = jQuery.parseJSON( msg );
var currTime = 0;
//console.log(obj);
if(typeof obj.time!='undefined'){ //Check if there is a time element - there always should be
var thisLog = {};
// Walk through each key in the parsed JSON
jQuery.each(obj, function (index, value) {
//console.log(index, value);
switch(index) {
case "time":
// The arduino millsec timer rolls over approx every 70d
if (value != prevTime) {
var nowTime = new Date();
// How long since the last loop?
thisLog["loopTime"] = ((value - prevTime) / 1000);
// Readable elapsed time since Arduino began executing
thisLog["elapsed"] = millisecsToString(value);
thisLog["time"] = nowTime.valueOf();
thisLog["timeStr"] = nowTime.toLocaleString('en-GB', {weekday: "short", year: "numeric", month: "short", day: "numeric"});
// Save current time - we get >1 msg with same timestamp & don't want to double process
prevTime = value;
}
break;
case "light":
// High is dark, low is light so lets invert
value = parseFloat((1000/value - 0.98).toFixed(2));
thisLog['light'] = value;
//$("#"+index).html(value);
break;
case "type":
// No action, we don't need this now
break;
default:
//$("#"+index).html(value);
thisLog[index] = value;
} // End of switch
}); // End of each
//dataLog.push(thisLog);
return thisLog;
} else {
$("#msg").html('Invalid Arduino Message - no "time" object found - see browser log');
console.log("ERROR: Invalid Arduino Message - no 'time' object found");
console.log(msg);
return {};
}
} // ------- End of Function parseArdMsgJSON ------- //
function millisecsToString(ms) {
// The arduino millsec timer rolls over approx every 70d
var remain;
//var years = Math.floor(seconds / 31536000000);
//var remain = ms - ( years * 31536000000 );
var days = Math.floor( ms / 86400000 );
remain = ms - ( days * 86400000 );
var hours = Math.floor( remain / 3600000 );
remain = remain - ( hours * 3600000 );
var min = Math.floor( remain / 60000 );
remain = remain - ( min * 60000 );
var sec = Math.floor( remain / 1000 );
var remain = remain - ( sec * 1000 );
return days + "d " + hours + ":" + min + ":" + sec; // + "." + remain;
}
window.onload = function() {
//initSocketIO();
ko.applyBindings( new DataModel() );
};
$(document).ready(function() {
});
</script>
</body></html>
{
"name": "NodeListener3",
"version": "0.0.5",
"description": "Get Node to listen to an Arduino over serial port and serve up results",
"dependencies": {
"supervisor": "latest",
"serialport": "latest",
"socket.io": "latest",
"express": "latest",
"jade": "latest",
"node-persist": "latest"
},
"author": "Julian Knight <jk.node@knightnet.org.uk> (http://it.knightnet.org.uk)",
"scripts": {"start": "node server.js"},
"files": ["server.js", "static"],
"engines" : { "node" : ">=0.10" }
}
/* Listen to Arduino on a COM (serial) port and publish data on a sockets interface
* @author Julian Knight <jk.node@knightnet.org.uk> (http://it.knightnet.org.uk)
*
*/
var express = require("express")
,storage = require('node-persist')
,app = express()
,port = 3700
,debug = false
,exec = require('child_process').exec;
var child = exec('cat /sys/class/thermal/thermal_zone0/temp',
function(err, stdout, stderr) {
console.log("Thermal", stdout, stderr, err);
}
);
child();
app.use(express.static(__dirname + '/static'));
var io = require('socket.io').listen(app.listen(port)); //app.listen(port);
console.log("Listening on port " + port);
// Set up the serial port
var serialport = require("serialport")
,SerialPort = serialport.SerialPort // localize object constructor
,portName = 'COM5';
var connectArd = function() {
var sp = new SerialPort(portName, {
parser: serialport.parsers.readline("\r\n"),
//parser: serialport.parsers.raw,
baudrate: 9600,
// defaults for Arduino serial communication
dataBits: 8,
parity: 'none',
stopBits: 1,
flowControl: false
});
storage.initSync();
// Create a serial port listener
sp.on('open', function() {
console.log('Serial Port ' + portName + ' Opened');
// When data is on the serial port...
sp.on('data', function (data) { // call back when data is received
var nowTime = new Date();
// Log it
console.log(data);
// Persist it
storage.setItem('name','yourname');
// Send data to Socket.IO
// NOTE: We are expecting data in JSON format
io.emit('arduino', data);
});
});
sp.on('close', function(){
console.log('ARDUINO PORT CLOSED, waiting 9 sec before retry');
setTimeout(
function() {
//process.exit();
reconnectArd();
}, 9000);
});
sp.on('error', function (err) {
console.error("sp error: ", err, "Waiting 9 sec before restarting");
setTimeout(
function() {
//process.exit();
reconnectArd();
}, 9000);
});
};
connectArd();
// check for connection errors or drops and reconnect
var reconnectArd = function () {
console.log('INITIATING RECONNECT');
connectArd();
/*
setTimeout(function(){
console.log('RECONNECTING TO ARDUINO [after 5sec wait]');
connectArd();
}, 5000);*/
};
// Start a socket connection to/from the browser
if(debug === false){
io.set('log level', 0); // socket IO debug off
}
io.sockets.on('connection', function (socket) {
// On start, emit some data
//socket.emit('news', { hello: 'world' });
// Listen for an event from the browser
socket.on('my other event', function (data) {
console.log(data);
});
// Listen for arduino data from the serial listener
io.on('arduino', function (data) {
exec('cat /sys/class/thermal/thermal_zone0/temp',function(err,stdout){
console.log("Thermal", stdout);
});
//console.log('Socket Data: ' + data);
// Send it to the browser
socket.emit('arduino', { ardData: data });
});
});
io.sockets.on('disconnect', function() {
console.log('Socket Disconnected');
});
/*
serialport.list(function (err, ports) {
ports.forEach(function(port) {
console.log(port.comName);
console.log(port.pnpId);
console.log(port.manufacturer);
});
});
*/
// Writing to the serial port:
// serialPort.write("OMG IT WORKS\r");
// https://github.com/voodootikigod/node-serialport
@TotallyInformation
Copy link
Author

For those wondering what format of data to send from the Arduino. I'm using standard serial output but I format the output text such that it can be parsed on the Pi as JSON. If you look at the HTML file, you will see the JSON headings. Example:

{"Temperature":21.3, "Humidity": 43}

To output that on the Arduino, you will need to wrap it in strings:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment