Automatic Data Download
If you've deployed sensors with Bluetooth Espruino boards (see Data Collection), you might like a way to retrieve data from those devices automatically.
There are a few ways of doing this, but the one we'd recommend is as follows:
- An application running on a PC that scans for Bluetooth advertisements (we'll use Node.js and Noble in the example here)
- Espruino devices that advertise when they have ready to send over Bluetooth
The PC app will then connect to any devices it sees that have data ready, and will download the data. When that's done the devices will then advertise that they don't have any more data.
We'll use the Bluetooth UART connection here - while it's possible to have your own characteristics, the UART connection provides buffering and potentially slightly higher data throughput than you might get from a custom characteristic.
For this example we'll just log the temperature, but it's pretty easy to modify this to store whatever information you are interested in.
Espruino code
Here we're using a slightly modified version of the Data Collection flash memory example example.
Temperature (and a timestamp) is stored once a second in one of two binary files. When the first file is full, writing swaps to the second file and the Bluetooth advertising is updated.
We're using Espruino's Manufacturer Data ID (0x0590) and a single byte which is 1 to show when there is one whole file of data ready, and 0 when there is no data file ready, but this could be extended to show when any data is ready, not just a complete file.
var storage = require("Storage");
var FILESIZE = 2048; // Note: set this according to the amount of free storage available
var file = {
name : "",
offset : FILESIZE, // force a new file to be generated at first
};
function getOtherFilename() {
return file.name=="log1"?"log2":"log1";
}
// Set up bluetooth advertising to show if we have a sample or not
function updateAdvertising() {
var samplesAvailable = 0;
if (storage.read(getOtherFilename())!==undefined)
samplesAvailable = 1;
NRF.setAdvertising({},{
manufacturer:0x0590,
manufacturerData:[samplesAvailable]
});
}
// Add new data to a log file or switch log files
function saveData(txt) {
var l = txt.length;
if (file.offset+l>FILESIZE) {
// need a new file...
file.name = getOtherFilename();
// write data to file - this will overwrite the last one
storage.write(file.name,txt,0,FILESIZE);
file.offset = l;
// now we have a new file, update advertising - it should show it has data now
updateAdvertising();
} else {
// just append
storage.write(file.name,txt,file.offset);
file.offset += l;
}
}
// Write some data
setInterval(function() {
var buf = new ArrayBuffer(5); // 5 = record size
var d = new DataView(buf);
d.setUint32(0, Math.round(getTime()));
d.setInt8(4, Math.round(E.getTemperature()));
saveData(buf);
}, 1000);
// Read the data
function getData() {
var buf = E.toArrayBuffer(storage.read(getOtherFilename()));
var d = new DataView(buf);
for (var i=0;i<buf.length;i+=5) { // 5 = record size
if (d.getUint32(i+0)==0xFFFFFFFF)
break; // time is all 0xFF, it's not been written yet
print({
time : d.getUint32(i+0),
temp : d.getInt8(i+4)
});
}
}
// Remove the last file of data that was written
function removeLastFile() {
storage.erase(getOtherFilename());
updateAdvertising();
}
// Now update advertising - we may still have data
updateAdvertising()
PC Code
We're going to use the Node.js code from https://www.espruino.com/Interfacing#node-js-javascript as a base for this. Please check the notes there about installing the correct version of noble
.
This code will scan for bluetooth devices that advertise Espruino's 0x0590 bluetooth ID, and if the data associated with that is nonzero then they will be connected to, and the data downloaded to a file called Device_[macaddresss].txt
/* Downloads data from Bluetooth devices advertising Espruino's 0x0590 bluetooth ID */
var noble = require('@abandonware/noble');
// information used when connecting
var btDevice;
var txCharacteristic;
var rxCharacteristic;
// Code to handle scanning
noble.on('stateChange', function(state) {
console.log("Noble: stateChange -> "+state);
if (state=="poweredOn")
noble.startScanning([], true);
});
noble.on('discover', function(dev) {
if (btDevice!=undefined) return;
if (!dev.advertisement) return; // no advertisement info
if (!dev.advertisement.manufacturerData) return; // no manufacturerData
var mData = dev.advertisement.manufacturerData.toString("binary");
if (!mData.startsWith("\x90\x05")) return; // not Espruino's 0x0590 manufacturer data
mData = mData.substr(2); // strip off the 0x0590
var deviceName = dev.advertisement.localName || dev.address;
console.log("Found device: ",deviceName, "data:", JSON.stringify(mData)); // strip off the 0x0590, mData);
if (mData=="" || mData=="\0") {
console.log("Device doesn't have data - ignoring");
return;
}
noble.stopScanning();
// noble doesn't stop right after stopScanning is called,
// so we have to check btDevice to ensure we only connect once
// Now connect!
console.log("Connecting...");
var receivedData = "";
connect(dev, function() {
// Connected!
write("\x10getData();print('==END==')\n", function() {
console.log("Receiving...");
});
}, function() {
noble.startScanning([], true);
}, function(data) {
receivedData += data;
if (receivedData.includes("==END==")) {
console.log("Saving...");
saveDeviceData(dev.address, receivedData.replace("==END==","").trim());
console.log("Wipe and disconnect...");
write("\x10removeLastFile()\n", function() {
btDevice.disconnect();
});
}
});
});
// connect to a device
function connect(dev, callback, disconnectCallback, dataCallback) {
btDevice = dev;
console.log("BT> Connecting");
btDevice.removeAllListeners('disconnect'); // remove any listeners from previous attempts
btDevice.on('disconnect', function() {
console.log("Disconnected");
btDevice = undefined;
if (disconnectCallback)
setTimeout(disconnectCallback, 1000);
});
btDevice.connect(function (error) {
if (error) {
console.log("BT> ERROR Connecting",error);
btDevice = undefined;
return;
}
console.log("BT> Connected");
btDevice.discoverAllServicesAndCharacteristics(function(error, services, characteristics) {
function findByUUID(list, uuid) {
for (var i=0;i<list.length;i++)
if (list[i].uuid==uuid) return list[i];
return undefined;
}
var btUARTService = findByUUID(services, "6e400001b5a3f393e0a9e50e24dcca9e");
txCharacteristic = findByUUID(characteristics, "6e400002b5a3f393e0a9e50e24dcca9e");
rxCharacteristic = findByUUID(characteristics, "6e400003b5a3f393e0a9e50e24dcca9e");
if (error || !btUARTService || !txCharacteristic || !rxCharacteristic) {
console.log("BT> ERROR getting services/characteristics");
console.log("Service "+btUARTService);
console.log("TX "+txCharacteristic);
console.log("RX "+rxCharacteristic);
btDevice.disconnect();
txCharacteristic = undefined;
rxCharacteristic = undefined;
btDevice = undefined;
return openCallback();
}
rxCharacteristic.removeAllListeners('data'); // remove any listeners from previous attempts
rxCharacteristic.on('data', function (data) {
var s = "";
for (var i=0;i<data.length;i++) s+=String.fromCharCode(data[i]);
console.log("Received", JSON.stringify(s));
dataCallback(s);
});
rxCharacteristic.subscribe(function() {
// we're finished
callback();
});
});
});
};
// write to the connected device
function write(data, callback) {
if (!btDevice) throw new Error("Not connected");
function writeAgain() {
if (!data.length) return callback();
var d = data.substr(0,20);
data = data.substr(20);
var buf = Buffer.alloc(d.length);
for (var i = 0; i < buf.length; i++)
buf.writeUInt8(d.charCodeAt(i), i);
console.log("BT> Write "+JSON.stringify(buf.toString("binary")));
txCharacteristic.write(buf, false, writeAgain);
}
writeAgain();
}
// Write the data to a file...
function saveDeviceData(deviceAddress, data) {
require("fs").writeFileSync("Device_"+deviceAddress.replace(/:/g,"").replace(/ \.\\\//g,"")+".txt", data);
}
Improvements
There are a few improvements that could be made to this system:
- We connect to the first found device, but if the advertised data was a value (not just 0/1) we could download from the device with the most data first
- Data is transferred from the device as text, but it could be sent as binary to speed up the transfer
- While transferring data, the Espruino device will be busy and may not record a sample (
setInterval
could be used to transfer sample by sample and so not block other tasks from executing) - The downloading application could store files with a timestamp (right now it will overwrite the last file it received from the device)
This page is auto-generated from GitHub. If you see any mistakes or have suggestions, please let us know.