WashWatch, keep a pulse your laundry facility with a simple sonar based real-time alert system.

Dependencies:   C12832_lcd EthernetInterface WebSocketClient mbed-rtos mbed

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.

/media/uploads/joeroop/washwatch3.png

The sonar device is connected to the LPC1768 application board as follows: /media/uploads/joeroop/mbed_sonar.png

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 ConditionPassing CriteriaResult
LPC1768 Testing
Read sonar dataSonar data is being displayed on LCD with correct unitsPASS
Init connectionEnsure when device is connected via Ethernet it acquires an IP and connects to the routerPASS
Reset connectionEnsure device will restablish a connection after unplugging Ethernet cable simulating a network dropout/failure/bad connectionPASS
Init connection websocketEnsure when device is connected via Ethernet it will connect to web server and send websocket packetsPASS
Reset connection websocketEnsure device will reestablish a websocket connection after disconnecting from web server via Ethernet cable unpluggedPASS
Reset connection websocketEnsure device will restablish a websocket connection after killing web serverPASS
Web Server
Init connection websocket deviceEnsure web server can receive device sonar dataPASS
Init connection websocket clientEnsure web server is able to allow clients to connect and send them sonar data after receiving data from devicePASS
Reset connection websocket deviceEnsure web server will not crash if device disconnects and reconnectsPASS
Reset connection websocket clientEnsure web server will not crash disconnects and reconnectsPASS
Data collectionEnsure data is being saved to the database when debounce timing requirements are metPASS
Remember statesEnsure web server uses last time from database as the start time for elapsed time so as to ensure data integrityPASS
Elapsed timeEnsure when dbounce is met and the signal goes from low to high the elapsed time restartsPASS
Elapsed timeEnsure when sonar signal goes high to low and the dbounce is met the time elapsed stopsFAIL
Client/Browser
Init connectionEnsure that browser can connect and receive and display data from web serverPASS
Reset connectionEnsure that browser can reconnect and receive and display data from web serverPASS

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

Sonar Device

  • 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
Committer:
joeroop
Date:
Mon Mar 17 02:54:09 2014 +0000
Revision:
0:dba71a0b1714
Child:
1:dac74cbb552e
Works but no error trapping

Who changed what in which revision?

UserRevisionLine numberNew contents of line
joeroop 0:dba71a0b1714 1
joeroop 0:dba71a0b1714 2
joeroop 0:dba71a0b1714 3 #include "mbed.h"
joeroop 0:dba71a0b1714 4
joeroop 0:dba71a0b1714 5 #include "EthernetInterface.h"
joeroop 0:dba71a0b1714 6 #include "Websocket.h"
joeroop 0:dba71a0b1714 7 #include "C12832_lcd.h"
joeroop 0:dba71a0b1714 8
joeroop 0:dba71a0b1714 9 #ifndef WIFI
joeroop 0:dba71a0b1714 10 //#define WIFI
joeroop 0:dba71a0b1714 11 #endif
joeroop 0:dba71a0b1714 12
joeroop 0:dba71a0b1714 13 /* internet interface:
joeroop 0:dba71a0b1714 14 * - p9 and p10 are for the serial communication
joeroop 0:dba71a0b1714 15 * - p30 is for the reset pin
joeroop 0:dba71a0b1714 16 * - p29 is for the connection status
joeroop 0:dba71a0b1714 17 * - "mbed" is the ssid of the network
joeroop 0:dba71a0b1714 18 * - "password" is the password
joeroop 0:dba71a0b1714 19 * - WPA is the security
joeroop 0:dba71a0b1714 20 */
joeroop 0:dba71a0b1714 21
joeroop 0:dba71a0b1714 22 #ifdef WIFI
joeroop 0:dba71a0b1714 23 #include "WiflyInterface.h"
joeroop 0:dba71a0b1714 24 WiflyInterface internet(p9, p10, p30, p29, "Castleton", "Gratitude279!", WPA); //
joeroop 0:dba71a0b1714 25 #else
joeroop 0:dba71a0b1714 26 EthernetInterface internet;
joeroop 0:dba71a0b1714 27 #endif
joeroop 0:dba71a0b1714 28 Websocket ws("ws://embedded.duckdns.org:8080/");
joeroop 0:dba71a0b1714 29
joeroop 0:dba71a0b1714 30
joeroop 0:dba71a0b1714 31 //lcd
joeroop 0:dba71a0b1714 32 C12832_LCD lcd;
joeroop 0:dba71a0b1714 33 AnalogIn sonar(p17); //hook up gnd, 3.3 red, p17 yellow
joeroop 0:dba71a0b1714 34
joeroop 0:dba71a0b1714 35 void prnt(char msg[40],bool clear,int line);
joeroop 0:dba71a0b1714 36 void connect(void);
joeroop 0:dba71a0b1714 37 int cnt;
joeroop 0:dba71a0b1714 38
joeroop 0:dba71a0b1714 39 int main()
joeroop 0:dba71a0b1714 40 {
joeroop 0:dba71a0b1714 41 int ret;
joeroop 0:dba71a0b1714 42 char json_str[100];
joeroop 0:dba71a0b1714 43
joeroop 0:dba71a0b1714 44 ret = internet.init();
joeroop 0:dba71a0b1714 45 lcd.printf("ret: %d",ret);
joeroop 0:dba71a0b1714 46
joeroop 0:dba71a0b1714 47 connect();
joeroop 0:dba71a0b1714 48 float V;
joeroop 0:dba71a0b1714 49
joeroop 0:dba71a0b1714 50 while (1) {
joeroop 0:dba71a0b1714 51
joeroop 0:dba71a0b1714 52 V = sonar;
joeroop 0:dba71a0b1714 53 lcd.locate(0,20);
joeroop 0:dba71a0b1714 54 lcd.printf("range: %-3.2f ft",V*3.3/0.0032*0.0328084); //0 to 1
joeroop 0:dba71a0b1714 55
joeroop 0:dba71a0b1714 56 sprintf(json_str, "{\"msg\":\"sonar\",\"range\":%3.2f}", V*3.3/0.0032*0.0328084); //maybe bloking
joeroop 0:dba71a0b1714 57 ret = ws.send(json_str); //need to restart the server if disconnected ret = -1
joeroop 0:dba71a0b1714 58 if(ret == -1) connect();
joeroop 0:dba71a0b1714 59 sprintf(json_str,"Bytes sent: %d cnt: %d",ret,++cnt);
joeroop 0:dba71a0b1714 60 prnt(json_str,0,1);
joeroop 0:dba71a0b1714 61 wait(0.01);
joeroop 0:dba71a0b1714 62 //lcd.cls();
joeroop 0:dba71a0b1714 63 }
joeroop 0:dba71a0b1714 64 }
joeroop 0:dba71a0b1714 65
joeroop 0:dba71a0b1714 66 void prnt(char msg[40],bool clear,int line){
joeroop 0:dba71a0b1714 67 if(clear)
joeroop 0:dba71a0b1714 68 lcd.cls();
joeroop 0:dba71a0b1714 69 lcd.locate(0,line*10);
joeroop 0:dba71a0b1714 70 lcd.printf(msg);
joeroop 0:dba71a0b1714 71 }
joeroop 0:dba71a0b1714 72
joeroop 0:dba71a0b1714 73 //set up function if get disconnected ret = -1
joeroop 0:dba71a0b1714 74 void connect(void){
joeroop 0:dba71a0b1714 75 cnt = 0;
joeroop 0:dba71a0b1714 76 char message[100];
joeroop 0:dba71a0b1714 77 /*
joeroop 0:dba71a0b1714 78 while(!internet.disconnect()){
joeroop 0:dba71a0b1714 79 sprintf(message,"internet disconnect tries.. %3d",++cnt);
joeroop 0:dba71a0b1714 80 prnt(message,1);
joeroop 0:dba71a0b1714 81 wait(0.025);
joeroop 0:dba71a0b1714 82 }*/
joeroop 0:dba71a0b1714 83 cnt = 0;
joeroop 0:dba71a0b1714 84 while(!internet.connect()){
joeroop 0:dba71a0b1714 85 sprintf(message,"internet connect tries.. %3d",++cnt);
joeroop 0:dba71a0b1714 86 prnt(message,0,1);
joeroop 0:dba71a0b1714 87 wait(0.025);
joeroop 0:dba71a0b1714 88 }
joeroop 0:dba71a0b1714 89 sprintf(message,"IP: %s", internet.getIPAddress());
joeroop 0:dba71a0b1714 90 wait(5.0);
joeroop 0:dba71a0b1714 91 prnt(message,0,0);
joeroop 0:dba71a0b1714 92 cnt = 0;
joeroop 0:dba71a0b1714 93 while (!ws.connect()){
joeroop 0:dba71a0b1714 94 sprintf(message,"ws connect tries.. %3d",++cnt);
joeroop 0:dba71a0b1714 95 prnt(message,0,1);
joeroop 0:dba71a0b1714 96 wait(0.025);
joeroop 0:dba71a0b1714 97 }
joeroop 0:dba71a0b1714 98 //lcd.cls();
joeroop 0:dba71a0b1714 99 cnt = 0;
joeroop 0:dba71a0b1714 100 }