The prerequisites of this project include Node.js, npm and the mqtt package. Their installation is outlined here.
We also need to install the noble package which is done as follows;
npm install noble
Overview
For this project, the goal is to use Node.js on the Raspberry Pi to read data from an Arduino. The data will be structured and published as a JavaScript string to a Message Queue Telemetry Transport (MQTT) server. A subscriber will then read this data and convert it into a JavaScript Object Notation (JSON) string. A system overview can be seen below;
This shows how the Raspberry Pi is the central BLE module as well the device which publishes data to the CloudMQTT server (which is running the Mosquitto service).
BLE Device
Our BLE device contains three services, two of these are a generic part of the Generic Attributes (GATT) profile, however one of the services from a previous assignment and contains characteristic data about the peripheral. This is the data of interest. A breakdown of the services and characteristics within our peripheral can be seen in the UML diagram below.
JSON
The next step was to structure the above peripheral information in a more meaningful, human-readable way. This was done using JavaScript Object Notation (JSON), an example of a JSON structure showing the above information from GATT profile can be seen below;
{ "Peripheral": [ { "id": "Marian", "mac": "98:4f:ee:0f:48:71", "Service": [ { "serviceUUID0": "1800", "Characteristic": [ { charuuid0: "2a00", data0: "47454e55494e4f203130312d34383731" }, { charuuid1: "2a01", data1 : "" }, { charuuid2: "2a04", data2: "4000780000005802" } ] }, { "serviceUUID1": "1801" }, { "serviceUUID2": "3999", "Characteristic": [ { charuuid0: "33", data0: "00" } ] } ] } ] }
As can be seen in the table above, the nesting nature of JSON is an advantage. An array of services can be seen nesting within a peripheral, and an array of characteristics can be seen within each service. The first two services contain generic device information. From the above we can see that the first characteristic has a UUID of value 2A00, this is the Device Name characteristic. The value 47454e55494e4f203130312d34383731, when converted from hexadecimal to ASCII is “GENUINO 101-4871”. This is not information of interest for our Node.js module, and as such, it is removed from our custom JSON frame which is published.
Code
The code for the project can be found at the bottom of this page. The code is heavily commented however its core functionality is also outlined in detail below;
- The Raspberry Pi scans for BLE devices and stores peripheral information within an array (provided it is within the specified Received Signal Strength Indicator (RSSI) threshold).
- If the name of the peripheral matches that of our own peripheral, the Node.js module stops scanning, and structures the peripheral information in a JSON structured JavaScript string.
- At this point, a connection is established using the
peripheral.connect()
function - Services on the Genuino 101 are discovered using the
peripheral.discoverServices()
function, and the custom service,services[2]
, is stored as JavaScript variable as this is the service of interest, its information is stored in a JSON format. - The function call
customService.discoverCharacteristics()
discovers the characteristics on our selected service. We store the characteristic of interest,characteristics[0]
, as a variable, - The program then accesses characteristic data using the
item.read()
function, which are also stored in a JSON format. - As the characteristic is the lowest level of the JSON object. The Node.js module disconnects from the Genuino by invoking
peripheral.disconnect()
. After this, all strings for each level of the desired JSON object are concatenated into a JSON frame string, called - This string is then published to the required topic
client.publish('Room401/'+macString,bcast)
, where macString is the MAC address of the peripheral. This function is part of the mqtt package from npm. - The above strings are then cleared to allow multiple readings of this peripheral. Multiple readings can be performed because of the
setInterval()
which deletes the peripheral information from the array if its timestamp is over 10 seconds old. - A subscription to the topic is set up using the function
client.subscribe('Room401/#',)
, it is worth noting that this function did not work with the MAC address was passed as a sub-topic. The hash-sign (#) is used as a work-around of this issue. - The subscriptions are then stored in a variable This is used to construct a JSON object using the function
JSON.parse(message)
. To print this object to the console, it must be converted back to a string. This is done using the function callJSON.stringify(jsonObject,null,2)
, which outputs a string indented with 2 spaces, this string can be seen in the terminal screenshot below
Results
We can see the data being read in and displayed to the console. It is displayed as JSON structured JavaScript string value. This is then published to the CloudMQTT server with a specified user, password and topic.
The string which is published from the Raspberry Pi can also be seen from the remote client which is running mosquitto_sub
, the output is the MQTT subscription.
The Node.js source code.
//import required packages to Node.js file var noble = require('noble'); var mqtt = require('mqtt') //Declare MQTT server parameters var options = { port: 16095, clientId: 'mqttjs_' + Math.random().toString(16).substr(2, 8), username: "user1", password: "password", }; var client = mqtt.connect('mqtt://m21.cloudmqtt.com', options) //Declare global variables var EXIT_GRACE_PERIOD = 10000; var RSSI_THRESHOLD = -90; var sampleGap = 15000; var lastSampleTime = 0; var inRange = []; //Empty strings in which to store peripheral information var peripheralString =""; var serviceString =""; var charString =""; var macString =""; //Enter code when event 'discover' is emitted from Noble module noble.on('discover', function(peripheral) { if (peripheral.rssi < RSSI_THRESHOLD) { //If RSSI is out of range, exit function return; } //Store peripheral ID in memory var id = peripheral.id; var entered = !inRange[id]; //If peripheral has not been stored in inRange, store it. if (entered) { inRange[id] = {peripheral: peripheral}; //Stop scanning for peripherals noble.stopScanning(); //If name of peripheral matches our Arduino, get its data if (peripheral.advertisement.localName=="Marian"){ //store data as JSON structured JavaScript string peripheralString += '{"id":"'+peripheral.advertisement.localName+'","mac":"'+peripheral.address+'","rssi":"'+peripheral.rssi+'","time":"'+new Date()+'",'; //Store MAC Address as string macString = peripheral.address; //Connect to peripheral peripheral.connect( function(error) { //Restart BLE Scanning noble.startScanning([], true); //Discover all services in peripheral peripheral.discoverServices([], function(error, services) { //store custom service UUID in JSON structure string serviceString += '{"serviceUUID'+'":"'+services[2].uuid+'"'; //Declare 3rd discovered service as our own custom service. var customService = services[2]; //Discover the characteristics within the custom service customService.discoverCharacteristics(null, function(error, characteristics) { //Select the first characteristic as the item of interest (i.e. the desired data) var item = characteristics[0]; //Begin reading the data item.read(function(error, data) { //Store characteristic UUID and value in JSON structured string charString = ',"Charicteristics":[{"charUUID":"'+ characteristics[0].uuid +'","charData":"'+data.toString('hex')+'"}]},'; //Upoin getting required data, disconnect from device peripheral.disconnect(function(error){ //Timestamp peripheral disconnect inRange[id].lastSeen = Date.now(); lastSampleTime = Date.now(); //If characteristic data has been stored, append it to its respective service string if(charString){ serviceString += charString; //If service data has been stored, appened it to its respective peripheral string if(serviceString) { //Remove trailing comma (used when several services are detected) serviceString = serviceString.slice(0,-1); peripheralString += '"Service":[ '+serviceString+']}'; //If peripheral data is stored, wrap it with top level JSON object name. if (peripheralString){ var bcast = '{"Peripheral":[ '+peripheralString+']}'; } //Print full JSON structured string to console, this is what will be published to the topic. console.log('Publish to MQTT Broker at: Room401/'+macString+'\n'); console.log(bcast); //Concatenate MAC string to topic name client.publish('Room401/'+macString, bcast, function() {}); //Reset the global variables charString=''; serviceString=''; peripheralString=''; }//end of if(serviceString) }//end of if(charString) });//end of peripheral.disconnect });//end of item.read });//end of discover.Charicteristics });//end of discover.Services });//end of pheriperal.Connect }//end of if(Marian) }//end of if(!inRange[id]) });//end of noble.on('discover'...) //Function to check if the time a peripheral was last is within the specified grace period setInterval(function() { //Iterate through peripherals in inRange array for (var id in inRange) { //If last seen date is less than grace period if (inRange[id].lastSeen < (Date.now() - EXIT_GRACE_PERIOD)) { //store expired peripheral object in new variable var peripheral = inRange[id].peripheral; //display peripheral information to console with timestamp console.log('"' + peripheral.advertisement.localName + '" exited (RSSI ' + peripheral.rssi + ') ' + new Date()); //remove peripheral object from inRange array delete inRange[id]; } } //Pass interval value to function (half the exit-grace-period) }, EXIT_GRACE_PERIOD / 2); //When statechange event is emitted from Noble module noble.on('stateChange', function(state) { //if new state is poweredOn, start scanning for peripherals if (state === 'poweredOn') noble.startScanning([], true); //if new state is anything else, stop scanning else noble.stopScanning(); }); //When connected client.on('connect', function() { //Subscribe to the all topics published to "Room401", this allows for different MAC addresses client.subscribe('Room401/#', function() { //Extract message from received packet from specified topic client.on('message', function(topic, message, packet) { //Construct JSON object from JavaScript value. var jsonObject = JSON.parse(message); //Convert JSON object to indented JavaScript string var jsonPretty = JSON.stringify(jsonObject,null,2); //Print formatted JSON string to console console.log("\nParsed JSON from subscription:\n\n"+jsonPretty+"\n"); }); }); });