2.5. NodeJS Application, Part 1: Reading Device Sensor Data from a Gateway

2.5.1. Introduction

This is the first part of a series of tutorials based upon an example Node project, written in JavaScript. This part of the tutorial covers the following:

  • Connecting a sensor device, like the Rigado SensorBeacon, to the Vesta Gateway
  • Enabling the Gateway to read sensor data from the SensorBeacon

Note

The code for this tutorial is available on GitLab.

In code samples in this document, each command will be preceded by the command prompt, in this case:

root@080030717-00055:~/nodejs-sensor-cloud#

2.5.2. Components

  • Gateway
  • SensorBeacon or other edge device
  • power, internet
  • other computer with keyboard/monitor, on same network as Gateway

2.5.3. Setup

  1. Connect Ethernet to the Gateway.

  2. Connect Power to the Gateway.

  3. Log in to the Gateway.

  4. Create a directory for this project.

    root@080030717-00055:~# mkdir nodejs-sensor-cloud
    root@080030717-00055:~# cd nodejs-sensor-cloud/
    
  5. Initialize a Node project with npm.

    root@080030717-00055:~/nodejs-sensor-cloud# npm init -y
    Wrote to /home/root/nodejs-sensor-cloud/package.json:
    
    {
      "name": "nodejs-sensor-cloud",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }
    
  6. Create a package dependency file.

    This example requires noble v1.8.1 and noble-device v1.4.1. Newer versions may not function properly. Edit the package.json to match the full package.json file contents below:

    {
      "name": "nodejs-sensor-cloud",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
      "dependencies":{
        "noble": "1.8.1",
        "noble-device": "1.4.1"
    }
    
  7. Create the script entry point.

    root@080030717-00055:~/nodejs-sensor-cloud# touch index.js
    

2.5.4. Discovering Devices

The first step in this application will be to get the Gateway to discover the SensorBeacons. To do this, we are going to implement a Device class to store state variables for each SensorBeacon. We are also going to rely on the rigado-sensor-beacon.js library, which handles the BLE communication with each Device.

  1. Download or copy the file rigado-sensor-beacon.js from the code on GitLab.

  2. Install node dependencies:

    root@080030717-00055:~/nodejs-sensor-cloud# npm install
    

    Note

    The output from this command may show some errors. This is expected. You should see output from npm install indicating what was successfully installed, such as this:

    ...
    +-- noble@1.8.1
    | +-- bluetooth-hci-socket@0.5.1
    | | `-- nan@2.10.0
    | +-- bplist-parser@0.0.6
    | `-- debug@2.2.0
    |   `-- ms@0.7.1
    `-- noble-device@1.4.1
    ...
    
  3. Create the file device.js and give it the following contents:

    'use strict';
    
    /**
     * A Device object manages an edge device and its bluetooth communication
     */
    class Device {
        constructor(bleDevice) {
            this.id = bleDevice.address.replace(/:/g, '');
            this.bleDevice = bleDevice;
            this.fwVersion = null;
            this.timer = null;
        }
    
        resetState() {
            this.fwVersion = null;
            this.timer = null;
        }
    
        /**
         * Returns a Promise that will resolve with the ble device's firmware revision,
         * or reject with an error
         */
        readFirmware() {
            return new Promise((resolve, reject) => {
                console.log(`${this.id}: readFirmwareRevision`);
                this.bleDevice.readFirmwareRevision((error, firmwareRevision) => {
                    if (error) {
                        reject(error);
                        return;
                    }
    
                    console.log(`${this.id}: firmware revision = ${firmwareRevision}`);
                    this.fwVersion = firmwareRevision;
    
                    resolve();
                });
            });
        }
    
        /**
         * Returns a Promise that will connect and set up the ble device with noble.
         * Once set up, it will read the device's firmware and enable its luxometer.
         */
        setUp() {
            return new Promise((resolve, reject) => {
    
                this.timer = setTimeout(() => {
                    this.bleDevice.disconnect();
                    reject("connection timeout")
                }, 10000)
    
                this.bleDevice.connectAndSetUp((setupError) => {
                    if (setupError) {
                        reject(new Error(setupError));
                    }
    
                    if(this.timer == null) {
                        this.bleDevice.disconnect();
                        reject("connection timeout")
                    } else {
                        clearTimeout(this.timer)
                        this.timer = null
                    }
    
                    this.readFirmware()
                        .then(() => {
                            resolve(this);
                        })
                        .catch((err) => {
                            console.log(`DEVICE SETUP ERROR: ${err}`);
                            reject(err)
                        });
                });
            });
        }
    }
    
    module.exports = Device;
    

    Note

    The Gateway comes with vi pre-installed for editing files locally. If you prefer to edit files on another computer, you can copy them to the Gateway with a command like this:

    user@host:~ $ scp path/to/file root@080030717-00055.local:/path/to/destination/
    
  4. Update index.js file with the following code:

    'use strict';
    
    const noble = require('noble');
    
    const Device = require('./device');
    const rigadoSensorBeacon = require('./rigado-sensor-beacon');
    
    /**
     * Connected devices
     * @global
     */
    const devices = {};
    
    /**
     * Cloud Provider access
     * @global
     */
    
    function startScanning() {
        // Restart scanning
        // https://github.com/sandeepmistry/noble/issues/223
        if (noble.state === 'poweredOn') {
            noble.startScanning([], true);
        } else {
            throw new Error('BLE poweredOff');
        }
    }
    
    function stopScanning() {
        noble.stopScanning();
    }
    
    /**
     * Called when a new device is discovered
     */
    var connected = false
    
    function onDiscover(beaconInst) {
        const id = beaconInst.address.replace(/:/g, '');
    
        if (connected) {
            return
        }
    
        var device = null;
    
        // must reuse device objects!
        if (devices[id]) {
            device = devices[id]
        } else {
            device = new Device(beaconInst);
        }
        console.log(`Connecting: ${device.id}`);
    
        //must stop scanning to initiate a connection
        connected = true
        stopScanning()
    
        // Connect to device and setup
        device.setUp()
            .then(() => {
    
                // Store in list of known devices
                devices[device.id] = device;
    
                // Set disconnect handler
                device.bleDevice.on('disconnect', function disconnectHandler() {
                    console.log(`${device.id}: disconnected`);
                    device.bleDevice.removeListener('disconnect', disconnectHandler);
                    device.resetState();
                    connected = false
                    startScanning();
                });
            })
            .catch((error) => {
                console.log(error)
                device.bleDevice.disconnect();
                connected = false
                startScanning();
            });
    }
    
    noble.on('scanStop', () => {
        console.log('Scanning stopped');
    });
    
    noble.on('scanStart', () => {
        console.log('Scanning started');
    });
    
    rigadoSensorBeacon.discoverAll(onDiscover);
    

    This main script defines functions for starting the BLE scan, as well as onDiscover, which is triggered when a device is discovered. It will also output console messages when scanning starts and stops. The final line in the script kicks off the process of discovering the SensorBeacons.

    The function onDiscover has only a few tasks at this point:

    • Checking if the discovered device is already tracked.
    • Telling the device to set itself up (with respect to Noble).
    • Restarting the scan, which had stopped when the device was discovered.
    • Adding the device to the list of tracked devices.
    • Telling the device that if its bleDevice is disconnected, it should remove itself from the list of tracked devices.
  5. Make sure the SensorBeacon is powered off.

  6. Run the script.

    root@080030717-00055:~/nodejs-sensor-cloud# node index.js
    Scanning started
    
  7. Power-on the SensorBeacon. Within a few seconds you should see a discovery message:

    root@080030717-00055:~/nodejs-sensor-cloud# node index.js
    Scanning started
    Discovered: f8287ceb48b1
    Scanning stopped
    f8287ceb48b1: readFirmwareRevision
    f8287ceb48b1: firmware revision = 1.0.4 (5)
    Scanning started
    

    Note

    If you don’t see a discovery message, check to make sure the Gateway’s Bluetooth is set up. Press Control-C to end the Node application and enter the command hciconfig.

    If there is no output, follow the steps in Setting Up Vesta Gateway Bluetooth Discovery and try the Node application again.

    If hciconfig does produce output, the problem may be with Bluetooth connectivity. Since this application uses Promises and communicates over a potentially-faulty protocol, it is possible for the script to fail in a number of ways. If the scanning fails to restart after a device discovery, or if a device fails to set itself up, try re-running the script.

2.5.5. Reading a Sensor

The next goal is to get the Gateway to read values from the SensorBeacon. In this case we’ll be using the luxometer to read light levels, but the SensorBeacon has many other kinds of sensor readings available.

  1. Open device.js and add the following 3 methods to the class:

    /**
     * Returns a Promise that will enable the device's luxometer
     */
    enableLuxometer() {
        return new Promise((resolve) => {
            console.log(`${this.id}: enableLuxometer`);
            this.bleDevice.enableLuxometer(resolve);
        });
    }
    
    /**
     * Returns a Promise that will notify the device's luxometer
     */
    notifyLuxometer() {
        return new Promise((resolve) => {
            console.log(`${this.id}: notifyLuxometer`);
            this.bleDevice.notifyLuxometer(resolve);
        });
    }
    
    /**
     * This function will bind its given callback to run when the ble device
     * triggers a 'luxometerChange' event.
     */
    onLuxometerChange(callback) {
        console.log(`${this.id}: set up onluxchange`);
        this.bleDevice.on('luxometerChange', (lux) => {
            callback(this, lux);
        });
    
        return this.notifyLuxometer();
    }
    
  2. Still within device.js, add a line inside the setUp method to enable the luxometer:

    ...
    this.readFirmware()
       .then(this.enableLuxometer.bind(this))
       .then() => {
          ...
    
  3. Open index.js and update the onDiscover method to bind the device’s onLuxometerChange function:

    ...
    // Connect to device and setup
    device.setUp()
        .then(() => {
            device.onLuxometerChange((d, lux) => {
                const telemetry = {
                    deviceID: d.id,
                    light: lux.toFixed(1),
                };
                          console.log(`Device ${d.id} lux: ${lux.toFixed(1)}`);
            });
    
            // Store in list of known devices
            devices[device.id] = device;
    
            // Set disconnect handler
            device.bleDevice.on('disconnect', function disconnectHandler() {
                console.log(`${device.id}: disconnected`);
                device.bleDevice.removeListener('disconnect', disconnectHandler);
                device.resetState();
                connected = false
                startScanning();
            });
        })
        .catch((error) => {
            console.log(error)
            device.bleDevice.disconnect();
            connected = false
            startScanning();
        });
        ...
    
  4. Upload the application files to the Gateway (if applicable).

  5. Run the script again, and watch the luxometer readings come in!

    root@080030717-00055:~/nodejs-sensor-cloud# node index.js
    Scanning started
    Discovered: f8287ceb48b1
    Scanning stopped
    f8287ceb48b1: readFirmwareRevision
    f8287ceb48b1: firmware revision = 1.0.4 (5)
    f8287ceb48b1: enableLuxometer
    f8287ceb48b1: set up onluxchange
    f8287ceb48b1: notifyLuxometer
    Scanning started
    Device f8287ceb48b1 lux: 86.0
    Device f8287ceb48b1 lux: 86.0
    Device f8287ceb48b1 lux: 86.0
    Device f8287ceb48b1 lux: 86.0
    ...