I built this project for TheNextWeb Hack Battle 2016. It was awarded 3 partner prizes and won the hack battle.
The jury:
- ComeEmpty.me, this year’s winner, addresses the issue where your home and your mail box may not be in close proximity. Having a complete user story and building a viable, fully-working app aided in Fokke Zandbergen‘s win.
- With three simple steps – link device, train it on an empty mailbox, train it for a crowded box – and even those who live in remote villages or don’t have time to check their PO Box daily no longer have to waste precious time and energy on such a menial task.
- Besides, who knows what’s lurking in your box… that save the date to the wedding of the century or your much awaited tax return check.
The projects has the following components
- The Things Uno with HC-SR04 sensor to drop in your mailbox.
- The Things Network to send the data to the Arrow app.
- Website and API powered by Arrow to receive the sensor data.
- App powered by Titanium to manage the sensor.
The available sensor that made most sense to use was the Ultrasonic Sensor HC-SR04. By following this Random Nerd Tutorial and The Things Uno Workshop I got it to send the distance in cm to TTN.
Here's the full script:
#include "TheThingsUno.h"
// Set your app Credentials
const byte appEui[8] = {}; // SET
const byte appKey[16] = {}; // SET
#define debugSerial Serial
#define loraSerial Serial1
TheThingsUno ttu;
int trigPin = 11; //Trig - green Jumper
int echoPin = 12; //Echo - yellow Jumper
long interval = 10000;
long duration, cm, inches;
void setup()
{
debugSerial.begin(115200);
loraSerial.begin(57600);
delay(1000);
ttu.init(loraSerial, debugSerial); //Initializing...
ttu.reset();
ttu.join(appEui, appKey);
delay(6000);
ttu.showStatus();
debugSerial.println("Setup for The Things Network complete");
delay(1000);
ttu.init(loraSerial, debugSerial); //Initializing...
//Define inputs and outputs
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
}
void loop()
{
// SOURCE: http://randomnerdtutorials.com/complete-guide-for-ultrasonic-sensor-hc-sr04/
// The sensor is triggered by a HIGH pulse of 10 or more microseconds.
// Give a short LOW pulse beforehand to ensure a clean HIGH pulse:
digitalWrite(trigPin, LOW);
delayMicroseconds(5);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// Read the signal from the sensor: a HIGH pulse whose
// duration is the time (in microseconds) from the sending
// of the ping to the reception of its echo off of an object.
pinMode(echoPin, INPUT);
duration = pulseIn(echoPin, HIGH);
// convert the time into a distance
cm = (duration/2) / 29.1;
inches = (duration/2) / 74;
debugSerial.print("Distance: ");
debugSerial.print(cm);
debugSerial.print("cm");
debugSerial.println();
int data = (int)(cm);
byte buf[2];
buf[0] = (data >> 8) & 0xff;
buf[1] = data & 0xff;
ttu.sendBytes(buf, 2);
delay(interval);
}
2. Pass data to the Arrow appThis was simple:
- Get the keys to use in the Arrow app
Since Amazon was also a sponsor, I followed The Things Network Example Integration with AWS IoT (which was updated during the Hack Battle) to pass the data on to AWS-IoT instead of directly to the Arrow app.
3. Build the Arrow appThen I created an Arrow Web app and used the AWS IoT package to receive the data which I then stored in ArrowDB, while also checking if the mailbox changed state - if the device was already linked and trained.
Here's the Arrow API that registers a device to a pushToken en phone number coming from the app:
var _ = require('lodash');
var Arrow = require('arrow');
module.exports = Arrow.API.extend({
group: 'register',
path: '/api/register/:devEUI',
method: 'GET',
description: 'Link a device, pushToken and Msisdn',
parameters: {
devEUI: {
description: 'Device EUI'
},
pushToken: {
description: "Push Notification Token"
},
Msisdn: {
description: 'Phone Number'
}
},
action: function(req, resp, next) {
var deviceModel = req.server.getModel('device');
deviceModel.findAndModify({
devEUI: req.params.devEUI
}, {
devEUI: req.params.devEUI,
pushToken: req.params.pushToken,
Msisdn: req.params.Msisdn
}, {
upsert: true
}, function(err, device) {
if (err) {
req.server.logger.error('register', 'deviceModel.findAndModify', err);
return resp.failure(500, 'Failed to register device', null, null, next);
}
resp.success({
id: device.id
}, next);
});
}
});
And here's the API that trains the device by simply calculating the average distance for a certain amount of time during which the device was in a particular state:
var _ = require('lodash');
var Arrow = require('arrow');
module.exports = Arrow.API.extend({
group: 'train',
path: '/api/train/:type/:devEUI',
method: 'GET',
description: 'Start a training session for an empty mailbox',
parameters: {
devEUI: {
description: 'Device EUI'
},
type: {
description: "Either 'empty' or 'full'"
},
time: {
description: 'Time span to calculate the avarage for (ms)'
}
},
action: function(req, resp, next) {
var deviceModel = req.server.getModel('device');
var messageModel = req.server.getModel('message');
var fromTime = Date.now() - parseInt(req.params.time);
var query = {
where: {
devEUI: req.params.devEUI,
time: {
'$gte': fromTime
}
}
};
messageModel.query(query, function(err, res) {
if (err) {
req.server.logger.error('train messageModel.query', err);
}
if (err || res.length === 0) {
return resp.failure(500, 'No data received from device', null, null, next);
}
var avg = _.reduce(res, function(memo, msg) {
return memo + msg.distance;
}, 0) / res.length;
var modify = {
status: req.params.type
};
modify[req.params.type] = avg;
deviceModel.findAndModify({
devEUI: req.params.devEUI
}, modify, {}, function(err, res) {
if (err) {
req.server.logger.error('train', 'deviceModel.findAndModify', err);
}
resp.success({
avg: avg
}, next);
});
});
}
});
Finally, this is the code that receives messages from TTN (potentially via AWS-IoT), determines the state and if a push notification must be send:
var _ = require('lodash');
var ttn = require('./ttn');
exports.init = init;
function init(server) {
var client = new ttn.Client(server.config.listener.mqttBroker, server.config.listener.appEUI, server.config.listener.appAccessKey);
client.on('uplink', function(data) {
server.logger.info('listener', 'data', JSON.stringify(data));
var messageModel = server.getModel('message');
var deviceModel = server.getModel('device');
var doc = {
devEUI: data.devEUI
};
deviceModel.findAndModify(doc, doc, {
upsert: true
}, function(err, device) {
if (err) {
server.logger.error('deviceModel.query', err);
}
var message = {
devEUI: data.devEUI,
time: Date.now(),
distance: data.fields.distance,
metadata: data.metadata
};
messageModel.create(message, function(err) {
if (err) {
server.logger.error('listener', 'messageModel.create', err);
}
if (!_.has(device, 'empty') || !_.has(device, 'full')) {
return;
}
var diffWithEmpty = Math.abs(message.distance - device.empty);
var diffWithFull = Math.abs(message.distance - device.full);
var status = (diffWithEmpty < diffWithFull) ? 'empty' : 'full';
if (device.status !== status) {
server.logger.debug('listener', 'status', status);
device.set({
status: status
});
device.save(function(err) {
if (err) {
server.logger.error('listener', 'deviceModel.save', err);
}
});
if (device.pushToken) {
var ArrowDB = require('arrowdb'),
arrowDBApp = new ArrowDB(server.config.arrowDB.key),
payload = {
"alert": 'Come Empty Me!',
"status": status
};
if (status === 'empty') {
payload.alert = 'Thanks for emptying me!';
}
arrowDBApp.pushNotificationsNotifyTokens({
channel: 'main',
to_tokens: device.pushToken,
payload: payload
}, function(err, result) {
if (err) {
console.error(err.message);
} else {
console.log('Notification sent!');
}
});
}
}
});
});
});
client.connect();
}
4. Build the Titanium appThen I used Titanium to build a native iOS app. With some minor changes this could build an Android app as well. It communicates with the Arrow app to register and train the device and receive notifications on change. The user registers with his/her phone number using CM Telecom.
There's not much exciting things happening in the app, but here's the code that interacts with the API:
var URL = 'https://82be41210848d90db102940ca15e885cac05428b.cloudapp-enterprise.appcelerator.com';
var key = Alloy.CFG.arrow.key;
exports.linkDevice = linkDevice;
exports.train = train;
exports.getStatus = getStatus;
function linkDevice(devEUI, Msisdn, deviceToken, cb) {
request({
method: 'GET',
path: '/api/register/' + devEUI + '?Msisdn=' + Msisdn + '&pushToken=' + deviceToken
}, cb);
}
function train(devEUI, type, time, cb) {
var path = '/api/train/' + type + '/' + devEUI + '?time=' + time;
request({
method: 'GET',
path: path
}, cb);
}
function getStatus(devEUI, cb) {
var path = '/api/device/query?where=' + JSON.stringify({
devEUI: devEUI
});
request({
method: 'GET',
path: path
}, function(err, res) {
if (!err && res && res.length > 0) {
res = res[0];
}
cb(err, res);
});
}
function request(opts, cb) {
var xhr = Ti.Network.createHTTPClient({
onload: function(e) {
var res = JSON.parse(this.responseText);
cb(null, res[res.key]);
},
onerror: function(e) {
var res = JSON.parse(this.responseText);
cb(res.message);
}
});
xhr.open(opts.method, URL + opts.path);
var Authorization = 'Basic ' + Ti.Utils.base64encode(key + ':');
xhr.setRequestHeader('Authorization', Authorization);
xhr.send(opts.body);
}
That was it!
Comments