WashWatch, keep a pulse your laundry facility with a simple sonar based real-time alert system.
Dependencies: C12832_lcd EthernetInterface WebSocketClient mbed-rtos mbed
Homepage
WashWatch¶
A simple Ethernet enabled remote sensing project to detect motion with sonar¶
Project Overview¶
This project utilizes the LPC1768 device with an analog sonar device to sense motion and provides that data to a server over a websocket. The device will connect over Ethernet via DHCP. The use case for the project was to allow several users to see whether a laundry facility was being occupied and if not when it was last occupied. The project provides an end-to-end system to allow users to view data from a remote LPC1768 device.
The project also makes use of several other tools for the server and client side part of the application. Node.js running on Linux was used for the web server with MongoDB used for the database. HTML5 and websockets were used to provide the real-time updates to the clients. Clients can connect with most browsers (mobile & desktop) that support websockets to receive the data.
The sonar device is connected to the LPC1768 application board as follows:
Non-LPC1768 Code¶
JavaScript server side code:¶
/* Node.js webserver to connect mbed to web clients server.js */ var app = require('http').createServer(handler) , io = require('socket.io').listen(app) , fs = require('fs') var mongo = require('./db'); //custom model for the MongoDB database //set the start time var start; mongo.query.exec(function(err,event){ //get the last time entry in the database try{ start = event[0].start; }catch(e){ start = new Date(); } }); app.listen(80); //web server to listen on port 80 io.set('log level',1); //server up files function handler (req, res) { var file = ''; //check to see if looking for a file //console.log(req.url); if(req.url == '/'){ file = '/index.html'; }else{ file = req.url; } fs.readFile(__dirname + file, function (err, data) { if (err) { res.writeHead(500); return res.end('Error loading '+req.url); } res.writeHead(200); res.end(data); }); } var count = 0; //socket connections to webclients io.sockets.on('connection', function (socket) { var address = socket.handshake.address; console.log("New connection from " + address.address + ":" + address.port); count++; //let everyone know there has been another client that joined console.log('User connected! users: '+count); socket.emit('users', {'count':count}); socket.broadcast.emit('users', {'count':count}); socket.on('disconnect',function(){ count--; //let everyone know that a client disconnected console.log('User disconnected, users: '+count); socket.emit('users', {'count':count}); socket.broadcast.emit('users', {'count':count}); }); socket.on('error',function(err,code){ console.log('Socket.IO Error: '+err+' '+code); }); //send out the current table mongo.query.exec(function(err,event){ io.sockets.emit('table',event); }); }); //socket connection to mbed var WebSocketServer = require('ws').Server , wss = new WebSocketServer({port: 8080}); //create a websocket server and listen on port 8080 var low = true; //detect movement var cutoff = 1.0; //default setting for the detection 1ft var debounce_ms = 1000; //time to wait for debounce var evt; //mongo.Event model wss.on('connection', function(ws) { console.log('Have a new ws connection!'); ws.on('message', function(data) { var ctime = new Date(); try{ var d = JSON.parse(data); //mbed will send all the data via JSON if(low && d.range > cutoff){ //start the timer low = false; //end of event if(typeof evt != 'undefined'){ evt.end = ctime; evt.duration_ms = ctime.getTime() - evt.start.getTime(); console.log(evt.duration_ms); if(evt.duration_ms > debounce_ms){ //if the duration is not long enough don't count start = ctime; //reset the clock for elapsed time evt.save(function(err,res){ //simple way of debouncing mongo.query.exec(function(err,event){ //call a function to organize the data and put into table //console.log(event); io.sockets.emit('table',event); }); }); } evt = undefined; //reset the event } }else if(!low && d.range <= cutoff){ evt = new mongo.Event(); //will timestamp with time low = true; } d.elapsed = ctime.getTime() - start.getTime(); d.time = ctime.getTime(); io.sockets.emit('mbed',d); //tell all the web clients about the new data }catch(e){ console.log('Error ws.on(message): '+e); } }); ws.on('disconnect',function(){ console.log('ws disconnected!'); }); ws.on('error',function(err,code){ console.log('WS Error: '+err+' '+code); }); });
JavaScript server side database code:¶
/* MongoDB helper library that describes model data to save db.js */ (function(exports){ var mongoose = require('mongoose'); var dbURI = 'mongodb://localhost:27017/mbed'; mongoose.connect(dbURI); //database operations var db = mongoose.connection; db.on('error', console.error.bind(console, 'connection error:')); db.once('open', function(){ console.log('Connection to '+dbURI); }); process.on('SIGINT', function(){ //if we kill the node app db.close(function(){ console.log('Mongoose/MongoDB disconnected through app termination'); process.exit(0); }); }); db.on('connected',function(){ console.log('we are connected to MongoDB'); }); //build the data schemas and models var eventSchema = new mongoose.Schema({ start: {type: Date, 'default': Date.now}, end: Date, duration_ms: Number, notes: String }); //model of the data schema var Event = mongoose.model('Event', eventSchema); //custom query that can be reused var query = Event.find({},{duration_ms:1,start:1,_id:0}).sort({start:-1}).limit(10); //public functions...the API exports.db = db; exports.Event = Event; //make new entries out of this model exports.query = query; })(typeof exports === 'undefined'? this['db']={}: exports);
HTML/JavaScript client code:¶
<!-- Client side code index.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>WashWatch</title> </head> <body> <h1>WashWatch</h1> <h3>Keep a pulse your laundry facility with a simple sonar based real-time alert system.</h3> <p>Users Connected:<span id="users"></span></p> <p id="test"></p> <p id="time"></p> <link rel="stylesheet" type="text/css" href="style.css"> <canvas id="mycanvas" width="400" height="150"></canvas> <div id="datatable"></div> <script src="/socket.io/socket.io.js"></script> <script src="jquery-1.11.0.min.js"></script> <script type="text/javascript" src="smoothie.js"></script> <script> //plotting using smoothie.js var smoothie = new SmoothieChart({ millisPerPixel:30, interpolation:'step', grid:{ verticalSections:4, fillStyle:'#ffffff', strokeStyle: '#85A4CE', }, labels:{ precision:0, fillStyle:'#000000' }, maxValue:10, minValue:0, timestampFormatter:SmoothieChart.timeFormatter }); smoothie.streamTo(document.getElementById("mycanvas")); var line1 = new TimeSeries(); smoothie.addTimeSeries(line1,{ lineWidth:1, strokeStyle:'#0759A2', fillStyle:'rgba(7,89,162,0.60)'} ); var socket = io.connect(document.url); //set up the socket to the server socket.on('connection',function(data){ $('#users').text(data.count); //display how many users are connected }); socket.on('mbed',function(data){ //receive websocket data from server var d = new Date(data.elapsed); var f = new Date(data.time); var s = ''; for( i in data){ s+='</br>'+i+': '+data[i]; } $('#test').html('Message: '+data.msg+'</br>'+'Range: '+parseFloat(data.range).toFixed(2)+' ft'+'</br>'+'Current Time: '+f.toLocaleTimeString()); $('#time').text('Elapsed Time: '+pad0s(d.getUTCHours())+':'+pad0s(d.getUTCMinutes())+':'+pad0s(d.getUTCSeconds())+'.'+pad0s(d.getUTCMilliseconds())); line1.append(new Date().getTime(),data.range); }); socket.on('users',function(data){ //display the number of users connected $('#users').text(data.count); }); socket.on('table',function(data){ //received data from database makeTable(data, $('#datatable')); }); //helper function to build a table var makeTable = function(data, element){ try{ element.empty(); }catch(e){}; var table = $('<table></table>').addClass('table'); var o; var header = true; var rows = 0; for(o in data){ if(header){ var row = $('<tr></tr>').addClass('header'); for(r in data[o]){ var col = $('<td></td>').text(r); row.append(col); } table.append(row); header = false; } rows++; var row = $('<tr></tr>'); if(rows % 2 == 0) row.addClass('even'); for(r in data[o]){ var s = data[o][r]; if(r == 'start' || r == 'end') s = new Date(s).toLocaleTimeString() +' '+new Date(s).toLocaleDateString(); var col = $('<td></td>').text(s); row.append(col); } table.append(row); } element.append(table); } //helper function to pad 0s for time var pad0s = function(num){ s=''+num; if(s.length<2) s='0'+num; return s; } </script> </body> </html>
Testing¶
Test Condition | Passing Criteria | Result |
---|---|---|
LPC1768 Testing | ||
Read sonar data | Sonar data is being displayed on LCD with correct units | PASS |
Init connection | Ensure when device is connected via Ethernet it acquires an IP and connects to the router | PASS |
Reset connection | Ensure device will restablish a connection after unplugging Ethernet cable simulating a network dropout/failure/bad connection | PASS |
Init connection websocket | Ensure when device is connected via Ethernet it will connect to web server and send websocket packets | PASS |
Reset connection websocket | Ensure device will reestablish a websocket connection after disconnecting from web server via Ethernet cable unplugged | PASS |
Reset connection websocket | Ensure device will restablish a websocket connection after killing web server | PASS |
Web Server | ||
Init connection websocket device | Ensure web server can receive device sonar data | PASS |
Init connection websocket client | Ensure web server is able to allow clients to connect and send them sonar data after receiving data from device | PASS |
Reset connection websocket device | Ensure web server will not crash if device disconnects and reconnects | PASS |
Reset connection websocket client | Ensure web server will not crash disconnects and reconnects | PASS |
Data collection | Ensure data is being saved to the database when debounce timing requirements are met | PASS |
Remember states | Ensure web server uses last time from database as the start time for elapsed time so as to ensure data integrity | PASS |
Elapsed time | Ensure when dbounce is met and the signal goes from low to high the elapsed time restarts | PASS |
Elapsed time | Ensure when sonar signal goes high to low and the dbounce is met the time elapsed stops | FAIL |
Client/Browser | ||
Init connection | Ensure that browser can connect and receive and display data from web server | PASS |
Reset connection | Ensure that browser can reconnect and receive and display data from web server | PASS |
Known Issues¶
- The system was originally tested with WiFly and was working. However, with the addition of both libraries, WiFly and Ethernet, there were compilation issues which had not been sorted out.
- When clients connect with mobile browsers and disconnect the number of users can get corrupted.
- Recording of elapsed time does not reset until after the sonar range goes high and debounce is met. When going high to low the timer should stop and elapsed time goes to zero, didn't add this logic.
Improvements¶
The following list captures some areas in which this system could be improved upon:
- Ability to connect both over WiFly and Ethernet.
- Allow the users to interact with the device, maybe ask for different data or reset the device.
- Allow many source devices to connect with the web server so a client could view more than one device.
- Provide client IDs and be able to track the client usage and find a way for the device to know which client is present.
Hardware, Software and References¶
- Sonar device Ultrasonic Range Finder - XL-Maxsonar EZ4
- Node.js used for the web server
- MongoDB used for the database to store sonar data
- Smoothie.js library to plot the sonar data in the browser
- jQuery library to create and modify HTML structure
- Javascript to link everything together