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:46:54 2014 +0000
Revision:
3:2e437ac484c5
Parent:
2:b6cd6d969956
Child:
4:297b36ee022b
before removal of ISR

Who changed what in which revision?

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