Bluetooth Low Energy Support Now Available in Moddable SDK

We are pleased to announce Bluetooth Low Energy (BLE) protocol support in the Moddable SDK, enabling both BLE peripheral and central device development. BLE is available now on Espressif ESP32 and Silicon Labs Blue Gecko microcontrollers.

This article describes the BLE features supported. It includes BLE example code in JavaScript. If you are not familiar with the BLE protocol (a.k.a. Bluetooth Smart) we recommend this excellent overview by Kevin Townsend at Adafruit.

Overview

The Moddable SDK defines a set of platform independent BLE APIs. A suite of JavaScript modules implements the APIs using the host platform's native BLE APIs. The host platforms BLE frameworks are either a set of C++ classes or a collection of C functions organized by function. Here are some common examples:

  • BLE central and peripheral devices use GAP APIs to establish connections and advertise.
  • BLE servers use GATT APIs to access service attributes contained in the local database.
  • BLE clients use GATT APIs to discover and read/write characteristics.

Bindings to the native APIs are built with XS in C, the C interface to the runtime of our XS JavaScript engine. Moddable's BLE API is designed to support the most common use cases, while remaining lightweight and easy to use. This approach builds on the BLE implementation of the host platform by adding a higher level, more broadly accessible JavaScript interface.

The Blue Gecko and ESP32 BLE frameworks both provide C interfaces. Because BLE is asynchronous, native code callbacks are supplied by the application developer to receive results. These callbacks add complexity, because the BLE client or server must implement the overall flow by keeping track of states. Furthermore, some frameworks call the callback functions from different threads, requiring BLE client and server developers to ensure that results are delivered safely to the application thread. While native code frameworks work well, our goal is to use JavaScript to provide a simpler API.

Detailed API documentation is included in the Moddable SDK together with plenty of example apps to get you started.

Features

The BLE implementation supports the following:

  • BLE peripheral (server) role.
  • BLE central (client) role.
  • Full service and characteristic discovery.
  • Advertisements defined by simple JavaScript objects, eliminating the need to write binary advertising data serializers and parsers.
  • GATT services defined in JSON files and automatic characteristic data value type conversion from BLE binary buffers.

Note: Because a BLE device typically implements only one role, either a client or a server, the BLEClient and BLEServer classes are built separately. This minimizes Flash and RAM use.

BLE Client

A BLE client connects to one of more BLE peripheral devices. While a BLE client usually runs on a mobile phone or desktop computer, a microcontroller may also implement a BLE client to support real-time feedback and/or visualization of sensor data.

The BLEClient class implements the BLE client role. Applications override this class to implement BLE client devices. Because BLE procedures and requests are asynchronous, results are always delivered to BLEClient class callbacks. Each callback is responsible for a single BLE event. This approach eliminates the need to maintain state and allows developers to implement only the callbacks required to interact with the target peripherals.

Peripheral Scanning

Scanning for BLE peripherals involves parsing the received binary advertising and scan response packets to find the advertisement data type of interest. Native code frameworks sometimes provide helper functions for this purpose. The BLE client typically matches the desired device by name or by checking if the device includes the service UUIDs required.

The complete BLE application below performs active scanning for peripherals and traces each peripheral's complete local name. Scanning for and identifying BLE peripherals is straightforward in JavaScript, thanks to getter accessor functions.

import BLEClient from "bleclient";

class Scanner extends BLEClient {
    onReady() {
        this.startScanning();
    }
    onDiscovered(device) {
        const scanResponse = device.scanResponse;
        const completeName = scanResponse.completeName;
        if (completeName)
            trace(completeName + "n");
    }
}
let scanner = new Scanner;

The onReady and onDiscovered class instance functions are BLE callbacks. The onReady callback is called when the BLE stack is ready to use. The onDiscovered callback is called for each BLE peripheral device discovered. The device argument is the peripheral. Its scanResponse property is a JavaScript object with properties for the advertised data types in the received packet. The completeName property returns the device's complete local name as a String, if available in the advertisement packet.

Discovery

After a BLE client connects to a peripheral, services and characteristics, represented by UUID, are discovered to read/write/notify/indicate characteristic values. The discovery process involves finding the required service(s) and characteristic(s) by UUID before accessing the characteristic values.

The example below performs service and characteristic discovery to find the Manufacturer Name String characteristic in the Device Information service from a device named "UART".

import BLEClient from "bleclient";
import {uuid} from "btutils";

const DEVICE_NAME = "UART";
const DEVICE_INFORMATION_SERVICE_UUID = uuid`180A`;
const MANUFACTURER_NAME_UUID = uuid`2A29`;

class Discovery extends BLEClient {
    onReady() {
        this.startScanning();
    }
    onDiscovered(device) {
        if (DEVICE_NAME == device.scanResponse.completeName) {
            this.stopScanning();
            this.connect(device);
        }
    }
    onConnected(device) {
        device.discoverPrimaryService(DEVICE_INFORMATION_SERVICE_UUID);
    }
    onServices(services) {
        if (services.length)
            services[0].discoverCharacteristic(MANUFACTURER_NAME_UUID);
    }
    onCharacteristics(characteristics) {
        if (characteristics.length)
            trace("Found characteristicn");
    }
}
let discovery = new Discovery;

Notifications

After finding the desired characteristic, a client usually requests notification when the characteristic value changes. BLE apps enable characteristic value change notifications by writing to the associated Client Configuration Configuration descriptor, which itself must be discovered. The BLEClient simplfies this common process by providing helper functions to enable and disable notifications.

In the example below, a BLE Heart Rate Monitor client receives Heart Rate Measurement notifications from the BLE heart rate monitor device:

const HEART_RATE_SERVICE_UUID = uuid`180D`;
const HEART_RATE_MEASUREMENT_UUID = uuid`2A37`;

class HeartRateMonitor extends BLEClient {
    ...
    onConnected(device) {
        device.discoverPrimaryService(HEART_RATE_SERVICE_UUID);
    }
    onServices(services) {
        if (services.length)
            services[0].discoverCharacteristic(HEART_RATE_MEASUREMENT_UUID);
    }
    onCharacteristics(characteristics) {
        if (characteristics.length)
            characteristics[0].enableNotifications();
    }
    onCharacteristicNotification(characteristic, buffer) {
        const response = new Uint8Array(buffer);
        const flags = response[0];
        let beatsPerMinute = response[1];
        if (flags & 1)
            beatsPerMinute |= response[2] << 8;
    }
}
let hrm = new HeartRateMonitor;

BLE Server

The BLEServer class implements the BLE server role. Examples of BLE servers include a BLE Beacon that broadcasts to nearby devices, and a BLE peripheral with GATT services that clients can connect to and receive notifications. A BLE server may connect to only one BLE client at a time.

Advertising

BLE peripherals broadcast advertisement data to identify the device. BLE version 4.x advertisement data packets are limited to 31 bytes and consist of a series of advertisement data types in binary format. The BLEServer represents advertising data as properties of JavaScript objects to make advertising data easier to work with. The complete BLE server app below advertises a Health Thermometer device.

class Advertiser extends BLEServer {
    onReady() {
        this.startAdvertising({
            advertisingData: {flags: 6, completeName: "Thermometer Example", completeUUID16List: [uuid`1809`]}
        });
    }
}
let advertiser = new Advertiser;

All the Bluetooth advertisement data types are supported for both advertising and scan response data.

GATT Services

BLE peripherals deploy one or more BLE services. Each service contains one or more characteristics and each characteristic contains zero or more descriptors. Services and characteristics are stored by the peripheral in a local GATT database. A GATT database is effectively a table of attributes, which include services, characteristics and descriptors. Each of these attributes has properties, e.g. UUID, maximum attribute data length, permissions, etc. Developers of BLE peripherals are responsible for building this table.

Each native BLE framework has a different way of building the attribute table. The Silicon Labs Simplicity Studio IDE provides a graphical GATT Editor for defining services and characteristics. The editor generates C source code corresponding to the attribute table that is included by the project. The source is compiled and object code linked to the app. ESP32 developers define the GATT table as an array of attribute records in C. The Moddable SDK uses JSON to define the attribute table in a way that is independent of the underlying native code framework.

BLE servers may deploy multiple GATT services where each service is defined in a separate JSON file provided in the BLE application project. The JSON below corresponds to the GATT Battery Service:

{
    "service": {
        "uuid": "180F",
        "characteristics": {
            "battery": {
                "uuid": "2A19",
                "maxBytes": 1,
                "type": "Uint8",
                "permissions": "read",
                "properties": "read",
                "value": 85
            },
        }
    }
}

Using JSON makes sense to define BLE services in the Moddable SDK. However, parsing the JSON at runtime into JavaScript objects requires RAM to contain the attribute properties. Furthermore, the underlying native code frameworks still require a native code attribute table. The new bles2gatt command line tool bridges the gap between platform independent JSON and native device dependent attribute representation. It runs as part of the Moddable build for BLE peripherals. The bles2gatt tool parses the JSON at build time and generates the native source code required by the target microcontroller. The source code is automatically compiled and linked to the host app. Since the object code is deployed to Flash memory, RAM is only required to store dynamic attribute values.

The characteristic key defined in JSON is used by the BLE server to identify the characteristic by name when a client reads or writes the corresponding value. As a result, BLE servers do not need to match characteristic read/write requests by UUID. Furthermore, the type property defines the data type of the characteristic value. The data type is used by the BLE server class to automatically convert the JavaScript value into the characteristic byte buffer required by the native code BLE stacks. As an example, the wifi-connection-server example app deploys one service with three characteristics. The SSID and password characteristic values are strings and the control characteristic value is an unsigned byte:

{
    "service": {
        "uuid": "FF00",
        "characteristics": {
            "SSID": {
                "uuid": "FF01",
                "maxBytes": 32,
                "type": "String",
                "permissions": "write",
                "properties": "write",
            },
            "password": {
                "uuid": "FF02",
                "maxBytes": 32,
                "type": "String",
                "permissions": "write",
                "properties": "write",
            },
            "control": {
                "uuid": "FF03",
                "maxBytes": 1,
                "type": "Uint8",
                "permissions": "write",
                "properties": "write",
            }
        }
    }
}

The bles2gatt tool generates an indexed lookup table in the native source code to map a characteristic attribute to a readable name:

#define char_name_count 3
static const char_name_table char_names[3] = {
    {0, 2, "SSID", "String"},
    {0, 4, "password", "String"},
    {0, 6, "control", "Uint8"},
};

The lookup table is used by the code that binds the C implementation to JavaScript, reducing the code required to service characteristic read and write requests. The wifi-connection-server example simply matches each characteristics by name. Before reaching the app, the characteristic values are converted to the types specified in the JSON:

onCharacteristicWritten(params) {
    let value = params.value;
    switch(params.name) {
        case "SSID":
            this.ssid = value;
            break;
        case "password":
            this.password = value;
            break;
        case "control":
            if ((1 == value) && this.ssid) {
                this.close();
                this.connectToWiFiNetwork();
            }
            break;
    }
}

A BLE client that services read requests doesn't need to explicitly match the characteristic name, by using JavaScript bracket notation:

onReady() {
    this.battery = 75;
}
onCharacteristicRead(params) {
    return this[params.name];
}

Read requests for static characteristic values do not require server code or RAM to store the value. Static characteristic values have a read permission and value property. The BLEServer class automatically responds to these requests:

{
    "service": {
        "uuid": "1800",
        "characteristics": {
            "device_name": {
                "uuid": "2A00",
                "maxBytes": 12,
                "type": "String",
                "permissions": "read",
                "properties": "read",
                "value": "Moddable HTM"
            },
            "appearance": {
                "uuid": "2A01",
                "maxBytes": 2,
                "type": "Uint16",
                "permissions": "read",
                "properties": "read",
                "value": 768
            },
        }
    }
}

In the example above, read requests for characteristic UUIDs 0x2A00 and 0x2A01 are automatically responded to by the BLEServer base class.

Cutting Edge JavaScript

The BLE implementation uses several modern JavaScript language features. The destructuring object assignment is used to reduce the code needed to provide default object property values. As of this writing, destructuring object assignment is at Phase 3 of the JavaScript standardization process, which means it will likely be part of a future update to the language pending experience implementing it in JavaScript engines. Using destructuring object assignment reduces this code:

let fast = params.hasOwnProperty("fast") ? params.fast : true;
let connectable = params.hasOwnProperty("connectable") ? params.connectable : true;
let discoverable = params.hasOwnProperty("discoverable") ? params.discoverable : true;
let scanResponseData = params.hasOwnProperty("scanResponseData") ? params.scanResponseData : null;
let advertisingData = params.advertisingData;

To this:

let {fast = true, connectable = true, discoverable = true, scanResponseData = null, advertisingData} = params;

Tagged templates allow developers to express Bluetooth UUIDs and addresses as strings, while using a byte array as the runtime format. The uuid tagged template function converts the string into an ArrayBuffer that contains the UUID binary data format:

const DEVICE_INFORMATION_SERVICE_UUID = uuid`180A`;

Instead of:

const DEVICE_INFORMATION_SERVICE_UUID = Uint8Array.of(0x0A, 0x18);

Future Work

The BLE peripheral and client API class implementations support most common scenarios. That said, we always strive to improve our work and support more features. Towards that end we are considering the following enhancements:

  • Support for Bluetooth private and random addresses
  • Security Manager support, including pairing, bonding, and encrypted connections
  • Support for Bluetooth 5 extended advertising features