Getting started with Zephyr and Web Bluetooth
Tags: webbluetooth, zephyr, iot
Published: 2023-12-04 22:05:57

---
title: Getting started with Zephyr and Web Bluetooth
published: true
description: 
tags: WebBluetooth, Zephyr, IoT
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/69rv591qm5dx1xyznec0.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-12-04 23:00 +0000
---

For this post, I wrote the embedded firmware and accompanying test web page in an afternoon - and I want everyone to know that they can too.

I did cheat a bit, as I am standing on the shoulders of giants, using the [Zephyr RTOS](https://www.zephyrproject.org/) on the embedded side and using [APIs](https://developer.chrome.com/articles/bluetooth/) in the browser that has taken quite some energy to perfect (credits go to [@reillyeon@toot.cafe](https://mastodon.social/@reillyeon@toot.cafe), [@Vincent_Scheib]
(https://twitter.com/Vincent_Scheib), [@quicksave2k](https://twitter.com/quicksave2k) and [more](https://github.com/WebBluetoothCG/web-bluetooth/graphs/contributors) ).

Anyway, let's get to work!

## What this thing does
The firmware exposes a GATT service with a few characteristics:

 * Setting the color of the primary RGB LED
 * Setting an ID, which will be appended to the BT name (convenient when in a workshop with multiple devices)
 * Being notified of button presses

It also enables logging over USB, if the board supports it.

In the root of the [repository](https://github.com/larsgk/simple-web-zephyr), there is a very [simple self contained HTML file](https://github.com/larsgk/simple-web-zephyr/blob/main/single_page.html) (so it doesn't require a web server when loaded as a file locally) to interact with the hardware via [Web Bluetooth](https://developer.chrome.com/articles/bluetooth/).

## A simple GATT service
In order to enable easy control from a web application using [Web Bluetooth](https://developer.chrome.com/articles/bluetooth/), a simple [GATT](https://www.bluetooth.com/bluetooth-resources/intro-to-bluetooth-gap-gatt/) service must be created.

First, we need to create UUIDs to be used for the service and characteristics.

```c
static struct bt_uuid_128 simple_io_service_uuid = BT_UUID_INIT_128(
	BT_UUID_128_ENCODE(0x13e779a0, 0xbb72, 0x43a4, 0xa748, 0x9781b918258c));

static const struct bt_uuid_128 simple_io_rgb_uuid = BT_UUID_INIT_128(
	BT_UUID_128_ENCODE(0x4332aca6, 0x6d71, 0x4173, 0x9945, 0x6653b6c684a0));

static const struct bt_uuid_128 simple_io_id_uuid = BT_UUID_INIT_128(
	BT_UUID_128_ENCODE(0xb749d964, 0x4efb, 0x408a, 0x82ad, 0x7495e8af8d6d));

static const struct bt_uuid_128 simple_io_button_uuid = BT_UUID_INIT_128(
	BT_UUID_128_ENCODE(0x030de9cf, 0xce4b, 0x44d0, 0x8aa2, 0x1db9185dc069));
```

The UUIDs are the unique identifiers used by the central device (in this case, the web application) to find the correct services and characteristics in BLE devices.  Some are short, [officially assigned, 16bit values](https://www.bluetooth.com/wp-content/uploads/Files/Specification/Assigned_Numbers.html#bookmark102) - the **Battery service** (= 0x180F) or **Heart Rate service** (= 0x180D) - but for custom services and characteristics like these, a custom 128bit UUID must be provided.

For UUID generation, I'd recommend using one of the [many](https://www.uuidtools.com/v4) [online](https://uuidonline.com/) [tools](https://www.uuidgenerator.net/).


Defining the GATT service structure in Zephyr:

```c
/* Simple IO Service Declaration */
BT_GATT_SERVICE_DEFINE(simple_io_svc,
	BT_GATT_PRIMARY_SERVICE(&simple_io_service_uuid),
	BT_GATT_CHARACTERISTIC(&simple_io_button_uuid.uuid,
			       BT_GATT_CHRC_NOTIFY,
			       BT_GATT_PERM_NONE,
				   NULL, NULL, NULL),
	BT_GATT_CCC(button_ccc_cfg_changed,
			BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
    BT_GATT_CUD("Button(s)", BT_GATT_PERM_READ),
    BT_GATT_CHARACTERISTIC(&simple_io_rgb_uuid.uuid,
				BT_GATT_CHRC_WRITE,
                BT_GATT_PERM_WRITE,
                NULL, write_rgb, NULL),
    BT_GATT_CUD("RGB", BT_GATT_PERM_READ),
    BT_GATT_CHARACTERISTIC(&simple_io_id_uuid.uuid,
				BT_GATT_CHRC_WRITE,
                BT_GATT_PERM_WRITE,
                NULL, write_id, NULL),
    BT_GATT_CUD("Device ID", BT_GATT_PERM_READ),
);
```

This structure defines a GATT service containing the needed characteristics.  For the sake of keeping things simple, a characteristic can be seen like a 'property' (or variable) made available over the air with a specified combination of `write`, `read` and/or `notify` capabilities (Note: there are more advanced options but for now, just focus on these three).

We have the following defined:

 * A _button_ characteristic: Will _notify_ on button presses.
 * An _rgb_ characteristic: To allow _writing_ an RGB value.
 * A _device id_ characteristic: To allow _writing_ an ID.

When a connected device _writes_ an RGB color value (e.g. via Web Bluetooth), the `write_rgb` function is called:

```c
static ssize_t write_rgb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
			const void *buf, uint16_t len, uint16_t offset,	uint8_t flags)
{
	uint8_t val[3];

	printk("%s: len=%zu, offset=%u\n", __func__, len, offset);

	if (offset != 0) {
		return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
	} else if (len != sizeof(val)) {
		return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
	}

	(void)memcpy(&val, buf, len);

    if (simple_io_cbs->set_rgb) {
        simple_io_cbs->set_rgb(val[0], val[1], val[2]);
    }

    return len;
}
```

This basically checks that 3 bytes are sent and if a `set_rgb` callback is registered, this will be called using the 3 bytes as red, green and blue values.

The full source can be found [here](https://github.com/larsgk/simple-web-zephyr/blob/main/app/src/simple_io_service.c).

## Advertising
Correctly discovering Bluetooth Low Energy devices requires them to advertise information about who they are, what services they provide or something else to allow unique or filtered identification.  You have probably seen this when trying to pair with a new set of earbuds, keyboard or other type of device from e.g. a mobile device.

In our case, we are interested in providing two things:

 * The UUID of our custom service
 * The name of the device

This is primarily done in two steps.  First, we define the advertising payload containing the service UUID:

```c
static const struct bt_data ad[] = {
	BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
	BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_SIMPLE_IO_SERVICE),
};
```

After the bluetooth stack is enabled and ready, we start advertising, including the name (will be added automatically by the Zephyr stack):

```c
static void bt_ready(void)
{
	int err;

	printk("%s: Bluetooth initialized\n", __func__);

    if (IS_ENABLED(CONFIG_SETTINGS)) {
		settings_load();
	}

	bt_simple_io_register_cb(&io_cbs);

	err = bt_le_adv_start(BT_LE_ADV_CONN_NAME, ad, ARRAY_SIZE(ad), NULL, 0);
	if (err) {
		printk("%s: Advertising failed to start (err %d)\n", __func__, err);
		return;
	}

	printk("%s: Advertising successfully started\n", __func__);
}
```

The full source for the main application can be found [here](https://github.com/larsgk/simple-web-zephyr/blob/main/app/src/main.c).


## Cloning, building and flashing
Before cloning the application repository, go to [The Zephyr getting started guide](https://docs.zephyrproject.org/latest/develop/getting_started/index.html) and install all dependencies (I'd recommend following the path with the virtual python environment).

This repository contains a stand alone Zephyr application that can be fetched and initialized like this:

```shell
west init -m git@github.com:larsgk/simple-web-zephyr.git --mr main my-workspace
```

Then use west to fetch dependencies:

```shell
cd my-workspace
west update
```

Go to the app folder:

```shell
cd simple-web-zephyr
```

There are a few scripts for building and flashing the [Nordic Semiconductor](https://www.nordicsemi.com/) [nRF52840 Dongle](https://www.nordicsemi.com/Products/Development-hardware/nRF52840-Dongle) - run them in this order:

```shell
compile_app.sh
create_flash_package.sh
flash_dongle.sh
```

Note: You'll need to get the dongle in DFU mode by pressing the small side button with a nail. The dongle should then start fading a red light in and out.

## Web Bluetooth test page
In order to connect to a BLE device using Web Bluetooth, first scan for the device, using a filter.  In this case, we look for the advertising of our custom service UUID:

```javascript
const SIMPLE_IO_SERVICE = '13e779a0-bb72-43a4-a748-9781b918258c';

...

const scan = async () => {
    try {
        const device = await navigator.bluetooth.requestDevice({
            filters: [{ services: [SIMPLE_IO_SERVICE] }]
        });

        await openDevice(device);
    } catch (err) {
        // ignore if we didn't get a device
    }
}
```

After connecting to the device, find the GATT service and hook up the characteristics:

```javascript
const openDevice = async (device) => {
    const server = await device.gatt.connect();
    
    try {
        const service = await server.getPrimaryService(SIMPLE_IO_SERVICE);

        await startButtonNotifications(service);
        await fetchRGBCharacteristic(service);

        console.log('Connected to device', device);
        statusElement.innerHTML = `Connected to ${device.name}`

        device.ongattserverdisconnected = _ => {
            console.log(`Disconnected ${device.id}`);
            statusElement.innerHTML = "Not Connected";
        };
    } catch (err) {
        console.warn(err);
    }
}
```

Start listening for button change notifications...

```javascript
const startButtonNotifications = async (service) => {
    const characteristic = await service.getCharacteristic(SIMPLE_IO_BUTTON_CHAR);
    characteristic.addEventListener('characteristicvaluechanged', (evt) => {
        const value = evt.target.value.getUint8(0);
        console.log(`Button = ${value}`);
        statusElement.innerHTML = `Button ${value === 0 ? "released" : "pressed"}`;
    });
    return characteristic.startNotifications();
}
```

...and connect the RGB characteristic to allow the application to set a color:

```javascript
const fetchRGBCharacteristic = async (service) => {
    rgbCharacteristic = await service.getCharacteristic(SIMPLE_IO_RGB_CHAR);
}

...

const setRGB = (r, g, b) => {
    if (rgbCharacteristic) {
        rgbCharacteristic.writeValueWithoutResponse(new Uint8Array([r, g, b]));
    }
}
```

The full source for the web application is available [here](https://github.com/larsgk/simple-web-zephyr/blob/main/single_page.html)

## Working together
Let's try to connect the web application to an nRF52840 dongle flashed with the Zephyr application.

First, connect the dongle to a power source (e.g. a PC USB port):

![Spiderman connecting the dongle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jrf1wun3nbsag02z5xcw.jpg)

The dongle should start flashing blue.

_NOTE: Spiderman wanted to make sure the dongle was properly web enabled but he is not strictly needed (please don't tell him)_

Then open the web application in a [Web Bluetooth capable browser](https://caniuse.com/web-bluetooth).

You should see something like this:

![Initial view of web page loaded](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nvpk10xep0hb8kqsey5x.png)

Now, press the **CONNECT** button and a connect dialog should appear, listing the devices found that satisfies the filter given to the `requestDevice` function.  Select the **Simple Web Zephyr** device and click pair:

![Request device dialog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ljy1vhv823enq45m4gve.png)

After successfully connecting, the page should look like this:

![Web page showing device connected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j246jodr3yfe4c9zx9x7.png)

Now try to push the button on the dongle to see the status message change:

![Web page showing button pressed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/el4ctstg746ao3rdz7n2.png)

This should show the page reacting to button press and release notifications sent from the device.

Try to open the color picker and select e.g. red:

![Color picker open](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d1ioepk9gez3v0oefgvq.png)

When the light on the dongle magically changes to red, more friends come to watch the fun:

![Dongle with red LED](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jcw3rupgtqkti8067kw3.jpg)

## Final remarks
I hope you enjoyed this introduction to Zephyr and Web Bluetooth.

Try to expand the Zephyr application with more services or maybe start adding a bit more functionality to the simple web page (e.g. try to make the RGB light change color on button presses).

The full source code is here: https://github.com/larsgk/simple-web-zephyr

Direct link to the test page is here: https://larsgk.github.io/simple-web-zephyr/single_page.html

Remember: if you download the page file, you can also just load it directly in the browser, make changes and reload.

Enjoy :)