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 04:06:57 2014 +0000
Revision:
5:4844e1f2fc0f
Parent:
4:297b36ee022b
Fixed header

Who changed what in which revision?

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