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 05:33:45 2014 +0000
Revision:
1:dac74cbb552e
Parent:
0:dba71a0b1714
Child:
2:b6cd6d969956
working projec t tested with start/stop of both server and cable

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 1:dac74cbb552e 4 #include "rtos.h"
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 1:dac74cbb552e 22 DigitalOut led1(LED1);
joeroop 1:dac74cbb552e 23
joeroop 1:dac74cbb552e 24 typedef enum {CONNECT = 1, DISCONNECT} state_t;
joeroop 1:dac74cbb552e 25 //Threads
joeroop 1:dac74cbb552e 26 void thread_eth(void const *args);
joeroop 1:dac74cbb552e 27 void thread_ws(void const *args);
joeroop 1:dac74cbb552e 28 void thread_send(void const *args);
joeroop 1:dac74cbb552e 29
joeroop 1:dac74cbb552e 30 Thread *proxy_eth;
joeroop 1:dac74cbb552e 31 Thread *proxy_ws;
joeroop 1:dac74cbb552e 32 Thread *proxy_send;
joeroop 1:dac74cbb552e 33
joeroop 1:dac74cbb552e 34 //ISRs
joeroop 1:dac74cbb552e 35 InterruptIn irptReset(p13); //up on joystick
joeroop 1:dac74cbb552e 36 void isrReset(void);
joeroop 1:dac74cbb552e 37
joeroop 1:dac74cbb552e 38
joeroop 0:dba71a0b1714 39 #ifdef WIFI
joeroop 0:dba71a0b1714 40 #include "WiflyInterface.h"
joeroop 1:dac74cbb552e 41 WiflyInterface eth(p9, p10, p30, p29, "Castleton", "Gratitude279!", WPA); //
joeroop 0:dba71a0b1714 42 #else
joeroop 1:dac74cbb552e 43 EthernetInterface eth;
joeroop 0:dba71a0b1714 44 #endif
joeroop 0:dba71a0b1714 45 Websocket ws("ws://embedded.duckdns.org:8080/");
joeroop 0:dba71a0b1714 46
joeroop 0:dba71a0b1714 47
joeroop 0:dba71a0b1714 48 //lcd
joeroop 0:dba71a0b1714 49 C12832_LCD lcd;
joeroop 0:dba71a0b1714 50 AnalogIn sonar(p17); //hook up gnd, 3.3 red, p17 yellow
joeroop 0:dba71a0b1714 51
joeroop 0:dba71a0b1714 52 void prnt(char msg[40],bool clear,int line);
joeroop 0:dba71a0b1714 53 void connect(void);
joeroop 0:dba71a0b1714 54 int cnt;
joeroop 0:dba71a0b1714 55
joeroop 0:dba71a0b1714 56 int main()
joeroop 0:dba71a0b1714 57 {
joeroop 1:dac74cbb552e 58 irptReset.rise(&isrReset);
joeroop 0:dba71a0b1714 59
joeroop 1:dac74cbb552e 60 Thread t1(thread_eth);
joeroop 1:dac74cbb552e 61 Thread t2(thread_ws);
joeroop 1:dac74cbb552e 62 Thread t3(thread_send);
joeroop 0:dba71a0b1714 63
joeroop 1:dac74cbb552e 64 proxy_eth = &t1;
joeroop 1:dac74cbb552e 65 proxy_ws = &t2;
joeroop 1:dac74cbb552e 66 proxy_send = &t3;
joeroop 1:dac74cbb552e 67
joeroop 1:dac74cbb552e 68 proxy_eth->signal_set(CONNECT); //start the system
joeroop 1:dac74cbb552e 69 while(1){
joeroop 1:dac74cbb552e 70 Thread::wait(1000);
joeroop 0:dba71a0b1714 71 }
joeroop 0:dba71a0b1714 72 }
joeroop 0:dba71a0b1714 73
joeroop 1:dac74cbb552e 74 void isrReset(void){
joeroop 1:dac74cbb552e 75 proxy_eth->signal_set(CONNECT);
joeroop 0:dba71a0b1714 76 }
joeroop 0:dba71a0b1714 77
joeroop 1:dac74cbb552e 78 //Threads
joeroop 1:dac74cbb552e 79 //keep track of ethernet connection
joeroop 1:dac74cbb552e 80 void thread_eth(void const *args){
joeroop 1:dac74cbb552e 81 osEvent evt;
joeroop 1:dac74cbb552e 82 int32_t sig, cnt, ret;
joeroop 1:dac74cbb552e 83 eth.init();
joeroop 1:dac74cbb552e 84 while(1){
joeroop 1:dac74cbb552e 85 evt = Thread::signal_wait(0); //will time out then loop not needed
joeroop 1:dac74cbb552e 86 sig = evt.value.signals;
joeroop 1:dac74cbb552e 87 switch(sig){
joeroop 1:dac74cbb552e 88 case CONNECT:
joeroop 1:dac74cbb552e 89 led1 = !led1;
joeroop 1:dac74cbb552e 90 cnt = 0; //make a saw tooth
joeroop 1:dac74cbb552e 91 while((ret = eth.connect()) != 0){
joeroop 1:dac74cbb552e 92 lcd.locate(0,0);
joeroop 1:dac74cbb552e 93 lcd.printf("eth try...%d",++cnt);
joeroop 1:dac74cbb552e 94 cnt = cnt > 1000 ? 0 : cnt; //limit the cnt
joeroop 1:dac74cbb552e 95 Thread::wait(250);
joeroop 1:dac74cbb552e 96 } //loop forever trying to connect
joeroop 1:dac74cbb552e 97 proxy_ws->signal_set(CONNECT);
joeroop 1:dac74cbb552e 98 break;
joeroop 1:dac74cbb552e 99 }
joeroop 0:dba71a0b1714 100 }
joeroop 1:dac74cbb552e 101 }
joeroop 1:dac74cbb552e 102 //keep track of ws connection
joeroop 1:dac74cbb552e 103 void thread_ws(void const *args){
joeroop 1:dac74cbb552e 104 osEvent evt;
joeroop 1:dac74cbb552e 105 int32_t sig, ret, cnt;
joeroop 1:dac74cbb552e 106 while(1){
joeroop 1:dac74cbb552e 107 evt = Thread::signal_wait(0); //will time out then loop not needed
joeroop 1:dac74cbb552e 108 sig = evt.value.signals;
joeroop 1:dac74cbb552e 109 switch(sig){
joeroop 1:dac74cbb552e 110 case CONNECT:
joeroop 1:dac74cbb552e 111 cnt = 50;
joeroop 1:dac74cbb552e 112 ws.close(); //close the connection because send was not working
joeroop 1:dac74cbb552e 113 while((ret = ws.connect()) != 0 && cnt-- > 0){
joeroop 1:dac74cbb552e 114 lcd.locate(0,10);
joeroop 1:dac74cbb552e 115 lcd.printf("ws try connect...%d",cnt);
joeroop 1:dac74cbb552e 116 Thread::wait(250);
joeroop 1:dac74cbb552e 117 } //loop x times trying to connect
joeroop 1:dac74cbb552e 118 if(ret != 0) proxy_eth->signal_set(CONNECT); //couldn't connect
joeroop 1:dac74cbb552e 119 else proxy_send->signal_set(CONNECT); //got a ws connection
joeroop 1:dac74cbb552e 120 break;
joeroop 1:dac74cbb552e 121 }
joeroop 0:dba71a0b1714 122 }
joeroop 1:dac74cbb552e 123
joeroop 1:dac74cbb552e 124 }
joeroop 1:dac74cbb552e 125 //send data at intervals
joeroop 1:dac74cbb552e 126 void thread_send(void const *args){
joeroop 1:dac74cbb552e 127 osEvent evt;
joeroop 1:dac74cbb552e 128 int32_t sig, ret, cnt;
joeroop 1:dac74cbb552e 129 char msg[100];
joeroop 1:dac74cbb552e 130 float range;
joeroop 1:dac74cbb552e 131 while(1){
joeroop 1:dac74cbb552e 132 evt = Thread::signal_wait(0); //will time out then loop not needed
joeroop 1:dac74cbb552e 133 sig = evt.value.signals;
joeroop 1:dac74cbb552e 134 switch(sig){
joeroop 1:dac74cbb552e 135 case CONNECT:
joeroop 1:dac74cbb552e 136 cnt = 0; //make this a saw tooth
joeroop 1:dac74cbb552e 137 do{
joeroop 1:dac74cbb552e 138 range = sonar*3.3/0.0032*0.0328084;
joeroop 1:dac74cbb552e 139 sprintf(msg, "{\"msg\":\"sonar\",\"range\":%3.2f}", range);
joeroop 1:dac74cbb552e 140 ret = ws.send(msg);
joeroop 1:dac74cbb552e 141 lcd.locate(0,20);
joeroop 1:dac74cbb552e 142 lcd.printf("%d range: %-3.2f ft %d ",ret, range, ws.is_connected());
joeroop 1:dac74cbb552e 143 Thread::wait(250);
joeroop 1:dac74cbb552e 144 }while(ret != -1);
joeroop 1:dac74cbb552e 145 proxy_ws->signal_set(CONNECT); //lost connection to ws so try ws
joeroop 1:dac74cbb552e 146 break;
joeroop 1:dac74cbb552e 147 }
joeroop 1:dac74cbb552e 148 }
joeroop 0:dba71a0b1714 149 }