Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
298f3f4
commit a987311
Showing
14 changed files
with
939 additions
and
247 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" : [ | ||
|
||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; |
Oops, something went wrong.