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;

  1. 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).
  2. 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.
  3. At this point, a connection is established using the peripheral.connect() function
  4. 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.
  5. The function call customService.discoverCharacteristics() discovers the characteristics on our selected service. We store the characteristic of interest, characteristics[0], as a variable,
  6. The program then accesses characteristic data using the item.read() function, which are also stored in a JSON format.
  7. 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
  8. 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.
  9. 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.
  10. 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.
  11. 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 call JSON.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");
    }); 
  });
});