For the past few months, I’ve been playing around with the new Web Bluetooth API which is about to ship in Chrome 56 in February 2017. And let me tell you, this new feature just unlocked lots of new possibilities for the Web.

As a Web Advocate, I was so excited and couldn’t wait to build an application showing how easy it is to combine Angular and the Web Bluetooth API (even more, with any of the upcoming Web APIs, more on that soon, stay tuned).

Let’s meet The Missing Web Bluetooth Module for Angular Application

I started then working with my buddy François Beaufort (kudos to him!) to build a demo app, a kind of proof of concept that illustrates how to integrate Web Bluetooth with Angular.

After implementing a couple of use cases, I came up with an Angular module which abstracts away all the boilerplate needed to configure the Web Bluetooth API.

A Few Disclaimers

Web Bluetooth APIs

I am going to assume that you’re already familiar with the Web Bluetooth APIs: GATT server, Services, Characteristics…etc. Please make yourself comfortable with this topic before reading the next sections. Here are few resources:

  1. https://developers.google.com/web/updates/2015/07/interact-with-ble-devices-on-the-web
  2. https://medium.com/@urish/start-building-with-web-bluetooth-and-progressive-web-apps-6534835959a6

Observables

I am also assuming that you have some basic knowledge about ObservablesObservers and Subjects.

Finnish Notation

You will notice that some methods ends with a $ symbol. This is some sort of convention in the Observables world that we’ve been using for a while. We may drop this $ symbol in the future because of this blog post.

https://miro.medium.com/v2/resize:fit:1000/1*hTkqd86iz_wxMBNT74EGVA.gif

Installing the module

You can get this module either using Yarn or NPM:

$ yarn add @manekinekko/angular-web-bluetooth$ npm i -S @manekinekko/angular-web-bluetooth

Using the WebBluetoothModule

The module is simple to use. First, import the WebBluetoothModule module form @manekinekko/angular-web-bluetooth:

import { NgModule } from '@angular/core';
import { 
  WebBluetoothModule
} from '@manekinekko/angular-web-bluetooth';
@NgModule({
  imports: [
    //...,
    WebBluetoothModule.forRoot()
  ],
  //...
})
export class AppModule { }

Calling the WebBluetoothModule.forRoot() method will provide the BluetoothCore service which you will need to use inside your own services/components, like so in battery-level.component.ts :

import { Component, OnInit, NgZone } from '@angular/core';
import { BatteryLevelService } from '../battery-level.service';
import { BluetoothCore } from '@manekinekko/angular-web-bluetooth';
@Component({
  selector: 'ble-battery-level',
  //...
  providers: [ BatteryLevelService, BluetoothCore ]
})
export class BatteryLevelComponent implements OnInit { //...

The WebBluetoothModule.forRoot() also provides a BrowserWebBluetooth implementation which uses navigator.bluetooth under the hood. A ServerWebBluetooth implementation for Angular Universal will come later. Of course, using Angular’s DI, you are free to provide your custom implementation if you like.

The BatteryLevelService (battery-level.service.ts) service is where you will (should) implement the logic of your device/sensor. In the following example, we are implementing a battery level service which reads the level of the connected device’s battery:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
// import BluetoothCore and types 
import {
  BluetoothCore,
  BluetoothRemoteGATTServer,
  BluetoothRemoteGATTService,
  BluetoothRemoteGATTCharacteristic,
  DataView
} from '@manekinekko/angular-web-bluetooth';
@Injectable()
export class BatteryLevelService {
  // define your services and characteristics
  static GATT_CHARACTERISTIC_BATTERY_LEVEL = 'battery_level';
  static GATT_PRIMARY_SERVICE = 'battery_service';
  constructor(
    // inject the BluetoothCore
    public ble: BluetoothCore
  ) {}
  getDevice() {
    // you can get ask for the device observable in order to be notified when the device has (dis)connected
    return this.ble.getDevice$();
  }
  streamValues() {
    // for realtime values, you can call this method and subscribe in order to receive the stream of realtime values
    return this.ble.streamValues$()
      .map( (value: DataView) => value.getUint8(0));
  }
  getBatteryLevel(): Observable<number> {
    return this.ble 
        // 1) call this method to run the discovery process
        .discover$({
          optionalServices:[
            BatteryLevelService.GATT_PRIMARY_SERVICE
          ]
        })
        // 2) you'll get the GATT server
        .mergeMap( (gatt: BluetoothRemoteGATTServer)  => {
          // 3) get the primary service of that GATT server
          return this.ble.getPrimaryService$(
            gatt, 
            BatteryLevelService.GATT_PRIMARY_SERVICE
          );
        })
        .mergeMap( (primaryService: BluetoothRemoteGATTService) => { 
          // 4) get a specific characteristic 
          return this.ble.getCharacteristic$(
            primaryService,
            BatteryLevelService
               .GATT_CHARACTERISTIC_BATTERY_LEVEL
          ); 
        })
        .mergeMap( 
          (characteristic: BluetoothRemoteGATTCharacteristic) =>  {
            // 5) read the provided value (as DataView)
            return this.ble.readValue$(characteristic);
          }
        )
        // 6) get the right value from the DataView
        .map( (value: DataView) => value.getUint8(0) );
  }
}

Give me some explanations

Okay! Let’s explain what’s happening inside the getBatteryLevel() method…

Basically, in order to read a value from a device, you need to go through the same workflow (for the common use cases):

  1. Call the discover$() method to run the discovery process

  2. This will give you back the GATT server

  3. Then, you will get the primary service of that GATT server

  4. Next, get a specific characteristic

  5. Last read the extracted value from that characteristic (as DataView)

  6. The last step will give you the values as DataView types. You will have to read the right values that are specific to your device/sensor. For instance, for simple values such battery level, calling a value.getUint8(0) is enough:

.map( (value: DataView) => value.getUint8(0) );

But sometimes, things can be more complicated. Some manufacturers usually provide their own Bluetooth GATT characteristics implementation and not follow the standards. This is the case if you need to read values from a Luxometer, commonly called a light sensor (which measures in LUX). Here is a sample code related to the Texas Instrument SensorTag CC2650 sensor:

.map( (data: DataView) => { let value = data.getUint16(0, true /* little endian */); let mantissa = value & 0x0FFF; let exponent = value » 12; let magnitude = Math.pow(2, exponent); let output = (mantissa * magnitude); let lux = output / 100.0; return +lux.toFixed(2); });

This is usually can be found in the device/sensor documentation, or source code, if you’re lucky!