Created
October 23, 2013 09:19
-
-
Save TotallyInformation/7115338 to your computer and use it in GitHub Desktop.
Example of using Node.js to monitor serial output from an Arduino
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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: </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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 <[email protected]> (http://it.knightnet.org.uk)", | |
"scripts": {"start": "node server.js"}, | |
"files": ["server.js", "static"], | |
"engines" : { "node" : ">=0.10" } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Listen to Arduino on a COM (serial) port and publish data on a sockets interface | |
* @author Julian Knight <[email protected]> (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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
To output that on the Arduino, you will need to wrap it in strings: