Skip to content

Commit

Permalink
refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
gfwilliams committed Nov 14, 2016
1 parent 298f3f4 commit a987311
Show file tree
Hide file tree
Showing 14 changed files with 939 additions and 247 deletions.
378 changes: 378 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

66 changes: 53 additions & 13 deletions README.md
@@ -1,34 +1,74 @@
sudo apt-get install nodered
sudo systemctl enable nodered.service
cd .node-red/
sudo npm install node-red-contrib-ui node-red-contrib-ble-uart node-red-contrib-graphs
EspruinoHub
===========

A BLE -> MQTT bridge for Raspberry Pi and other Embedded devices


Setting up
----------

sudo apt-get install node npm mosquitto mosquitto-clients
sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev
Assuming a blank Pi:

```
# Get node, npm, node-red, etc
sudo apt-get install node npm mosquitto mosquitto-clients nodered bluetooth bluez libbluetooth-dev libudev-dev
# Install node-red service
sudo systemctl enable nodered.service
# Install the node-red UI
cd .node-red && sudo npm install node-red-contrib-ui
# As it comes, NPM on the Pi is broken
# and doesn't like installing native libs
# and doesn't like installing native libs. Update NPM
sudo npm -g install npm node-gyp
npm install noble
# Now get this repository
git clone https://github.com/espruino/EspruinoHub
# Install its' requirements
cd EspruinoHub
npm install
# Give Node.js access to Bluetooth
sudo setcap cap_net_raw+eip $(eval readlink -f `which node`)
```

Usage
-----

Take a look at your mosquitto.conf, is there a "listener" line in there? Change
listener 1883 127.0.0.1
to
listener 1883

Run with `start.sh`


Testing MQTT
------------

```
# listen to all, verbose
mosquitto_sub -h localhost -t /# -v
# Test publish
mosquitto_pub -h localhost -t test/topic -m "Hello world"
```


Note
----

To allow Bluetooth to advertise services (for the HTTP proxy) you need:

```
# Stop the bluetooth service
sudo service bluetooth stop
# Start Bluetooth but without bluetoothd
sudo hciconfig hci0 up
```

See https://github.com/sandeepmistry/bleno



Run with `start.sh`
TODO
----

* Keep connection open in `connect.js` for 30 secs after first write
* Re-use the connection for queues requests
* Handle over-size writes
12 changes: 12 additions & 0 deletions config.json
@@ -0,0 +1,12 @@
{
"// If a device's address is here, it'll be given a human-readable name":0,
"known_devices" : {
"c0:52:3f:50:42:c9" : "office",
"fc:a6:c6:04:db:79" : "hall_down",
"cf:71:de:4d:f8:48" : "hall_up"
},
"// If there are any addresses here, they are given access to the HTTP proxy":0,
"http_whitelist" : [

]
}
237 changes: 18 additions & 219 deletions index.js
@@ -1,220 +1,19 @@
/*
TODO:
Keep connection open for 30 secs after first write
Queue subsequent requests (to avoid double-connect/etc)
Respond to /ble/read with /ble/characteristic
Handle over-size writes
Display log messages alongside device status
Don't broadcast data if it hasn't changed?
*/

var mqtt = require('mqtt')
var noble = require('noble');

require("./blePeripheral.js"); // Enable HTTP Proxy

var ATTRIBUTE_NAMES = {
"1809" : "Temperature",
"180a" : "Device Information",
"180f" : "Battery Percentage",
"181c" : "User Data",
"fe9f" : "Eddystone",
"6e400001b5a3f393e0a9e50e24dcca9e" : "nus",
"6e400002b5a3f393e0a9e50e24dcca9e" : "nus_tx",
"6e400003b5a3f393e0a9e50e24dcca9e" : "nus_rx",
};

var ATTRIBUTE_HANDLER = {
"1809" : function(a) {
return { temp : (a.length==2) ? (((a[1]<<8)+a[0])/100) : a[0] }
}
};

/** If the device is listed here, we use the human readable name
when printing status and publishing on MQTT */
var KNOWN_DEVICES = {
"c0:52:3f:50:42:c9" : "office",
"fc:a6:c6:04:db:79" : "hall_down",
"cf:71:de:4d:f8:48" : "hall_up"
};
// TODO: add this to config


var inRange = [];


function lookupAttribute(attr) {
for (var i in ATTRIBUTE_NAMES)
if (ATTRIBUTE_NAMES[i]==attr) return i;
return attr;
}

noble.on('discover', function(peripheral) {
var id = peripheral.address;

if (id in KNOWN_DEVICES)
id = KNOWN_DEVICES[id];

var entered = !inRange[id];
// console.log(JSON.stringify(peripheral.advertisement,null,2));

if (entered) {
inRange[id] = {
id : id,
address : peripheral.address,
peripheral: peripheral,
name : "?",
data : {}
};
mqttSend("/presence/ble/"+id, "1");
}
var mqttData = {
rssi: peripheral.rssi,
};
if (peripheral.advertisement.localName) {
mqttData.name = peripheral.advertisement.localName;
inRange[id].name = peripheral.advertisement.localName;
}
inRange[id].lastSeen = Date.now();
inRange[id].rssi = peripheral.rssi;

mqttSend("/ble/advertise/"+id, JSON.stringify(mqttData));
mqttSend("/ble/advertise/"+id+"/rssi", JSON.stringify(peripheral.rssi));

peripheral.advertisement.serviceData.forEach(function(d) {
/* Don't keep sending the same old data on MQTT. Only send it if
it's changed or >1 minute old. */
if (inRange[id].data[d.uuid] &&
inRange[id].data[d.uuid].payload.toString() == d.data.toString() &&
inRange[id].data[d.uuid].time > Date.now()-60000)
return;

mqttSend("/ble/advertise/"+id+"/"+d.uuid, JSON.stringify(d.data));
inRange[id].data[d.uuid] = { payload : d.data, time : Date.now() };
if (d.uuid in ATTRIBUTE_HANDLER) {
var v = ATTRIBUTE_HANDLER[d.uuid](d.data);
for (var k in v) {
mqttSend("/ble/advertise/"+id+"/"+k, JSON.stringify(v[k]));
mqttSend("/ble/"+k+"/"+id, JSON.stringify(v[k]));
}
}
});
});

function str2buf(str) {
var buf = new Buffer(str.length);
for (var i = 0; i < buf.length; i++) {
buf.writeUInt8(str.charCodeAt(i), i);
}
return buf;
}

noble.on('stateChange', function(state) {
if (state!="poweredOn") return;
noble.startScanning([], true);
console.log("Started...");
});

var client = mqtt.connect('mqtt://localhost');
var mqttConnected = false;

client.on('connect', function () {
console.log("MQTT Connected");
mqttConnected = true;
// client.publish('presence', 'Hello mqtt')
client.subscribe("/ble/write/#");
});

client.on('message', function (topic, message) {
console.log("MQTT>"+topic+" => "+message.toString())
var path = topic.substr(1).split("/");
if (path[0]=="ble" && path[1]=="write") {
var id = path[2].toLowerCase();
if (inRange[id]) {
var device = inRange[id].peripheral;
var service = lookupAttribute(path[3].toLowerCase());
var charc = lookupAttribute(path[4].toLowerCase());
console.log("Service ",service);
console.log("Characteristic ",charc);
device.connect(function (error) {
if (error) {
console.log("BT> ERROR Connecting");
}
console.log("BT> Connected");
device.discoverAllServicesAndCharacteristics(function(error, services, characteristics) {
console.log("BT> Got characteristics");
var characteristic;
for (var i=0;i<characteristics.length;i++)
if (characteristics[i].uuid==charc)
characteristic = characteristics[i];
if (characteristic) {
console.log("BT> Found characteristic");
var data = str2buf(message.toString());
// TODO: writing long strings
characteristic.write(data, false, function() {
console.log("BT> Disconnecting...");
device.disconnect();
});
} else {
console.log("BT> No characteristic found");
console.log("BT> Disconnecting...");
device.disconnect();
}
});
});
} else {
console.log("Write to "+id+" but not in range");
}
}
});

function mqttSend(topic, message) {
if (mqttConnected) client.publish(topic, message);
}

function checkForPresence() {
var timeout = Date.now() - 60*1000; // 60 seconds
Object.keys(inRange).forEach(function(id) {
if (inRange[id].lastSeen < timeout) {
mqttSend("/presence/ble/"+id, "0");
delete inRange[id];
}
});
}

function dumpStatus() {
// clear screen
console.log('\033c');
//process.stdout.write('\x1B[2J\x1B[0f');
// ...
console.log("Scanning... "+(new Date()).toString());
console.log();
// sort by most recent
var arr = [];
for (var id in inRange)
arr.push(inRange[id]);
arr.sort(function(a,b) { return a.rssi - b.rssi; });
// output
var amt = 3;
var maxAmt = process.stdout.getWindowSize()[1];
for (var i in arr) {
var p = arr[i];
if (++amt > maxAmt) { console.log("..."); return; }
console.log(p.id+" - "+p.name+" (RSSI "+p.rssi+")");
for (var j in p.data) {
if (++amt > maxAmt) { console.log("..."); return; }
var n = ATTRIBUTE_NAMES[j] ? ATTRIBUTE_NAMES[j] : j;
var v = p.data[j].payload;
if (j in ATTRIBUTE_HANDLER)
v = ATTRIBUTE_HANDLER[j](v);

console.log(" "+n+" => "+JSON.stringify(v));
}
}
}

// -----------------------------------------
setInterval(checkForPresence, 1000);
setInterval(dumpStatus, 1000);

* This file is part of EspruinoHub, a Bluetooth-MQTT bridge for
* Puck.js/Espruino JavaScript Microcontrollers
*
* Copyright (C) 2016 Gordon Williams <gw@pur3.co.uk>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* ----------------------------------------------------------------------------
*
* ----------------------------------------------------------------------------
*/

require("./lib/config.js").init(); // Load configuration
require("./lib/status.js").init(); // Enable Status reporting to console
require("./lib/service.js").init(); // Enable HTTP Proxy Service
require("./lib/discovery.js").init(); // Enable HTTP Proxy Service
43 changes: 43 additions & 0 deletions lib/attributes.js
@@ -0,0 +1,43 @@
/*
* This file is part of EspruinoHub, a Bluetooth-MQTT bridge for
* Puck.js/Espruino JavaScript Microcontrollers
*
* Copyright (C) 2016 Gordon Williams <gw@pur3.co.uk>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* ----------------------------------------------------------------------------
* Known Attributes and conversions for them
* ----------------------------------------------------------------------------
*/

exports.names = {
"1809" : "Temperature",
"180a" : "Device Information",
"180f" : "Battery Percentage",
"181c" : "User Data",
"fe9f" : "Eddystone",
"6e400001b5a3f393e0a9e50e24dcca9e" : "nus",
"6e400002b5a3f393e0a9e50e24dcca9e" : "nus_tx",
"6e400003b5a3f393e0a9e50e24dcca9e" : "nus_rx",
};

exports.handlers = {
"1809" : function(a) {
return { temp : (a.length==2) ? (((a[1]<<8)+a[0])/100) : a[0] }
}
};

exports.getReadableAttributeName = function(attr) {
for (var i in exports.names)
if (exports.names[i]==attr) return i;
return attr;
};

exports.decodeAttribute = function(name, value) {
if (name in exports.handlers)
return exports.handlers[name](value);
return value;
};

0 comments on commit a987311

Please sign in to comment.