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:
Tue Mar 25 03:57:43 2014 +0000
Revision:
4:297b36ee022b
Parent:
3:2e437ac484c5
Child:
5:4844e1f2fc0f
First publish for project

Who changed what in which revision?

UserRevisionLine numberNew contents of line
joeroop 4:297b36ee022b 1 /*
joeroop 4:297b36ee022b 2 Author: Joe Roop
joeroop 4:297b36ee022b 3 Assignment: RTOS Project
joeroop 4:297b36ee022b 4 Due: 4/25/14
joeroop 4:297b36ee022b 5 Objective:
joeroop 4:297b36ee022b 6 Utilize the mbed as a remote sensing device. The device
joeroop 4:297b36ee022b 7 will communicate over Ehternet to a server and that
joeroop 4:297b36ee022b 8 server will display the data in real-time and allow
joeroop 4:297b36ee022b 9 sever clients to connect. The use case for the project is
joeroop 4:297b36ee022b 10 to provide a community the ability to use the sensor to
joeroop 4:297b36ee022b 11 check the traffic of their local laundry facility. The device
joeroop 4:297b36ee022b 12 can be connected to any router with DHCP and will send its
joeroop 4:297b36ee022b 13 data out over a web socket to be collected.
joeroop 4:297b36ee022b 14 */
joeroop 0:dba71a0b1714 15
joeroop 2:b6cd6d969956 16 #include "mbed.h"
joeroop 2:b6cd6d969956 17 #include "rtos.h"
joeroop 2:b6cd6d969956 18 #include "EthernetInterface.h"
joeroop 2:b6cd6d969956 19 #include "Websocket.h"
joeroop 2:b6cd6d969956 20 #include "C12832_lcd.h"
joeroop 2:b6cd6d969956 21
joeroop 0:dba71a0b1714 22
joeroop 1:dac74cbb552e 23 typedef enum {CONNECT = 1, DISCONNECT} state_t;
joeroop 1:dac74cbb552e 24 //Threads
joeroop 1:dac74cbb552e 25 void thread_eth(void const *args);
joeroop 1:dac74cbb552e 26 void thread_ws(void const *args);
joeroop 1:dac74cbb552e 27 void thread_send(void const *args);
joeroop 1:dac74cbb552e 28
joeroop 1:dac74cbb552e 29 Thread *proxy_eth;
joeroop 1:dac74cbb552e 30 Thread *proxy_ws;
joeroop 1:dac74cbb552e 31 Thread *proxy_send;
joeroop 1:dac74cbb552e 32
joeroop 3:2e437ac484c5 33 EthernetInterface eth;
joeroop 4:297b36ee022b 34 Websocket ws("ws://washwatch.duckdns.org:8080/");
joeroop 0:dba71a0b1714 35
joeroop 0:dba71a0b1714 36
joeroop 0:dba71a0b1714 37 //lcd
joeroop 0:dba71a0b1714 38 C12832_LCD lcd;
joeroop 0:dba71a0b1714 39 AnalogIn sonar(p17); //hook up gnd, 3.3 red, p17 yellow
joeroop 3:2e437ac484c5 40 Mutex lcd_mutex;
joeroop 0:dba71a0b1714 41
joeroop 0:dba71a0b1714 42 void prnt(char msg[40],bool clear,int line);
joeroop 0:dba71a0b1714 43 void connect(void);
joeroop 0:dba71a0b1714 44 int cnt;
joeroop 0:dba71a0b1714 45
joeroop 0:dba71a0b1714 46 int main()
joeroop 4:297b36ee022b 47 {
joeroop 1:dac74cbb552e 48 Thread t1(thread_eth);
joeroop 1:dac74cbb552e 49 Thread t2(thread_ws);
joeroop 1:dac74cbb552e 50 Thread t3(thread_send);
joeroop 0:dba71a0b1714 51
joeroop 1:dac74cbb552e 52 proxy_eth = &t1;
joeroop 1:dac74cbb552e 53 proxy_ws = &t2;
joeroop 1:dac74cbb552e 54 proxy_send = &t3;
joeroop 1:dac74cbb552e 55
joeroop 1:dac74cbb552e 56 proxy_eth->signal_set(CONNECT); //start the system
joeroop 1:dac74cbb552e 57 while(1){
joeroop 1:dac74cbb552e 58 Thread::wait(1000);
joeroop 0:dba71a0b1714 59 }
joeroop 0:dba71a0b1714 60 }
joeroop 0:dba71a0b1714 61
joeroop 1:dac74cbb552e 62 //Threads
joeroop 1:dac74cbb552e 63 //keep track of ethernet connection
joeroop 1:dac74cbb552e 64 void thread_eth(void const *args){
joeroop 1:dac74cbb552e 65 osEvent evt;
joeroop 1:dac74cbb552e 66 int32_t sig, cnt, ret;
joeroop 1:dac74cbb552e 67 eth.init();
joeroop 1:dac74cbb552e 68 while(1){
joeroop 1:dac74cbb552e 69 evt = Thread::signal_wait(0); //will time out then loop not needed
joeroop 1:dac74cbb552e 70 sig = evt.value.signals;
joeroop 1:dac74cbb552e 71 switch(sig){
joeroop 1:dac74cbb552e 72 case CONNECT:
joeroop 1:dac74cbb552e 73 cnt = 0; //make a saw tooth
joeroop 1:dac74cbb552e 74 while((ret = eth.connect()) != 0){
joeroop 3:2e437ac484c5 75 lcd_mutex.lock();
joeroop 1:dac74cbb552e 76 lcd.locate(0,0);
joeroop 3:2e437ac484c5 77 lcd.printf("eth try...%d, %d",++cnt, ret);
joeroop 3:2e437ac484c5 78 lcd_mutex.unlock();
joeroop 1:dac74cbb552e 79 cnt = cnt > 1000 ? 0 : cnt; //limit the cnt
joeroop 1:dac74cbb552e 80 Thread::wait(250);
joeroop 1:dac74cbb552e 81 } //loop forever trying to connect
joeroop 1:dac74cbb552e 82 proxy_ws->signal_set(CONNECT);
joeroop 1:dac74cbb552e 83 break;
joeroop 1:dac74cbb552e 84 }
joeroop 0:dba71a0b1714 85 }
joeroop 1:dac74cbb552e 86 }
joeroop 1:dac74cbb552e 87 //keep track of ws connection
joeroop 1:dac74cbb552e 88 void thread_ws(void const *args){
joeroop 1:dac74cbb552e 89 osEvent evt;
joeroop 1:dac74cbb552e 90 int32_t sig, ret, cnt;
joeroop 1:dac74cbb552e 91 while(1){
joeroop 1:dac74cbb552e 92 evt = Thread::signal_wait(0); //will time out then loop not needed
joeroop 1:dac74cbb552e 93 sig = evt.value.signals;
joeroop 1:dac74cbb552e 94 switch(sig){
joeroop 1:dac74cbb552e 95 case CONNECT:
joeroop 1:dac74cbb552e 96 cnt = 50;
joeroop 1:dac74cbb552e 97 ws.close(); //close the connection because send was not working
joeroop 1:dac74cbb552e 98 while((ret = ws.connect()) != 0 && cnt-- > 0){
joeroop 3:2e437ac484c5 99 lcd_mutex.lock();
joeroop 1:dac74cbb552e 100 lcd.locate(0,10);
joeroop 1:dac74cbb552e 101 lcd.printf("ws try connect...%d",cnt);
joeroop 3:2e437ac484c5 102 lcd_mutex.unlock();
joeroop 1:dac74cbb552e 103 Thread::wait(250);
joeroop 1:dac74cbb552e 104 } //loop x times trying to connect
joeroop 1:dac74cbb552e 105 if(ret != 0) proxy_eth->signal_set(CONNECT); //couldn't connect
joeroop 1:dac74cbb552e 106 else proxy_send->signal_set(CONNECT); //got a ws connection
joeroop 1:dac74cbb552e 107 break;
joeroop 1:dac74cbb552e 108 }
joeroop 0:dba71a0b1714 109 }
joeroop 1:dac74cbb552e 110
joeroop 1:dac74cbb552e 111 }
joeroop 1:dac74cbb552e 112 //send data at intervals
joeroop 1:dac74cbb552e 113 void thread_send(void const *args){
joeroop 1:dac74cbb552e 114 osEvent evt;
joeroop 1:dac74cbb552e 115 int32_t sig, ret, cnt;
joeroop 1:dac74cbb552e 116 char msg[100];
joeroop 1:dac74cbb552e 117 float range;
joeroop 1:dac74cbb552e 118 while(1){
joeroop 1:dac74cbb552e 119 evt = Thread::signal_wait(0); //will time out then loop not needed
joeroop 1:dac74cbb552e 120 sig = evt.value.signals;
joeroop 1:dac74cbb552e 121 switch(sig){
joeroop 1:dac74cbb552e 122 case CONNECT:
joeroop 1:dac74cbb552e 123 cnt = 0; //make this a saw tooth
joeroop 1:dac74cbb552e 124 do{
joeroop 1:dac74cbb552e 125 range = sonar*3.3/0.0032*0.0328084;
joeroop 1:dac74cbb552e 126 sprintf(msg, "{\"msg\":\"sonar\",\"range\":%3.2f}", range);
joeroop 1:dac74cbb552e 127 ret = ws.send(msg);
joeroop 3:2e437ac484c5 128 lcd_mutex.lock();
joeroop 1:dac74cbb552e 129 lcd.locate(0,20);
joeroop 1:dac74cbb552e 130 lcd.printf("%d range: %-3.2f ft %d ",ret, range, ws.is_connected());
joeroop 3:2e437ac484c5 131 lcd_mutex.unlock();
joeroop 3:2e437ac484c5 132 Thread::wait(100); //10Hz
joeroop 1:dac74cbb552e 133 }while(ret != -1);
joeroop 1:dac74cbb552e 134 proxy_ws->signal_set(CONNECT); //lost connection to ws so try ws
joeroop 1:dac74cbb552e 135 break;
joeroop 1:dac74cbb552e 136 }
joeroop 1:dac74cbb552e 137 }
joeroop 0:dba71a0b1714 138 }