Connect to Bluetooth Devices Using Chrome
Connect to your bluetooth devices using Google Chrome Navigator API
Suyash Singh
Posted by Suyash Singh
on March 16, 2024
Photo by Chaitanya on Unsplash

Introduction

In today’s interconnected world, integrating IoT devices seamlessly into web applications is becoming increasingly important. One powerful way to achieve this is through the Navigator API for Bluetooth connections in web browsers. This API enables web developers to interact with Bluetooth peripherals directly from their applications, opening up a realm of possibilities for building innovative IoT solutions.

ELI5: How to Connect and Interact with Bluetooth Devices Using JavaScript

To demonstrate how to use Bluetooth in web applications, let’s walk through a simple example of a reminder app. In the previous post, we dealt with the hardware aspects of it by using Raspberry PI PICO W to setup our app with its bluetooth. This example will showcase how you can connect to Raspberry PI PICO reminder app we built in the previous post. Bluetooth device using Google Chrome, exchange data using GATT characteristics, and leverage real-time updates—all using TS and the Navigator API.

Step 1: Requesting and Connecting to a Bluetooth Device

First, we initiate a connection to the Bluetooth device using the navigator.bluetooth.requestDevice() method. This opens a system dialog listing nearby Bluetooth devices based on specified filters, such as device name prefixes and service UUIDs.

bluetooth.ts
import { useRemindersStore } from '@/stores';
import {
  convertReminderToBytes,
  createReminderFromString,
  decoder,
  encoder,
} from '@/utils';
 
let readReminderCharacteristic: BluetoothRemoteGATTCharacteristic | null = null;
let writeReminderCharacteristic: BluetoothRemoteGATTCharacteristic | null =
  null;
let dateCharacteristic: BluetoothRemoteGATTCharacteristic | null = null;
 
const SERVICE_UUID = '0492fcec-7194-11eb-9439-0242ac130002';
const READ_REMINDERS_CHARACTERISTIC_UUID =
  'd4fe65e5-42e7-4616-9d13-06f24f72465f';
const NEW_REMINDER_CHARACTERISTIC_UUID = 'd4fe65e5-42e7-4616-9d13-06f24f7246ff';
const DATE_CHARACTERISTIC_UUID = 'd4fe65e5-42e7-4616-9d13-06f24f724fff';
const DEVICE_NAME = 'RPi Pico Remind Me';
 
let CONNECTED = false;
 
export const isConnected = () => {
  return CONNECTED;
};
 
export const initiateConnection = async () => {
  const { initializeStore } = useRemindersStore();
  return navigator.bluetooth
    .requestDevice({
      filters: [
        {
          namePrefix: DEVICE_NAME,
        },
      ],
      optionalServices: [SERVICE_UUID],
    })
    .then(device => {
      console.log('Connecting to GATT Server...');
      return device?.gatt?.connect();
    })
    .then(server => {
      console.log('Getting Service...');
      return server?.getPrimaryServices();
    })
    .then((services: BluetoothRemoteGATTService[] | undefined) => {
      console.log('Getting Characteristics...');
      if (!services || !services.length) {
        return;
      }
      return services[0].getCharacteristics();
    })
    .then(
      (characteristics: BluetoothRemoteGATTCharacteristic[] | undefined) => {
        if (!characteristics || !characteristics.length) {
          return;
        }
        characteristics.forEach(c => {
          switch (c.uuid) {
            case READ_REMINDERS_CHARACTERISTIC_UUID:
              readReminderCharacteristic = c;
              readReminderCharacteristic?.addEventListener(
                'characteristicvaluechanged',
                readReminderCharacteristicValChanged,
              );
              readReminderCharacteristic?.readValue();
              break;
            case NEW_REMINDER_CHARACTERISTIC_UUID:
              writeReminderCharacteristic = c;
              break;
            case DATE_CHARACTERISTIC_UUID:
              dateCharacteristic = c;
              setInterval(syncTime, 2000);
              break;
          }
        });
 
        CONNECTED = true;
        initializeStore();
        return Promise.resolve();
      },
    )
    .catch((error: Error) => {
      CONNECTED = false;
      console.error(error);
    });
};
 
const readReminderCharacteristicValChanged = (event: Event) => {
  if (!event) {
    return;
  }
  const reminderString = decoder.decode(event.target?.value);
  if (reminderString !== '__$$NoTasks') {
    const reminder = createReminderFromString(reminderString);
    const { notifyReminder } = useRemindersStore();
    if (reminder) notifyReminder(reminder);
  }
 
  setTimeout(() => {
    try {
      if (readReminderCharacteristic) readReminderCharacteristic.readValue();
    } catch (e) {
      CONNECTED = false;
    }
  }, 100);
};
 
const syncTime = () => {
  const now = new Date();
 
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0');
  const date = String(now.getDate()).padStart(2, '0');
  const hours = String(now.getHours()).padStart(2, '0');
  const mins = String(now.getMinutes() + now.getSeconds() / 60.0).padStart(
    2,
    '0',
  );
  const timeString = `${year}&${month}&${date}&${hours}&${mins}`;
 
  if (CONNECTED) {
    dateCharacteristic?.writeValue(encoder.encode(timeString));
  }
};
 
export const sendReminderToDevice = (reminder: Reminder) => {
  if (CONNECTED) {
    writeReminderCharacteristic?.writeValue(convertReminderToBytes(reminder));
  }
};
 
export const removeReminderFromDevice = (reminder: Reminder) => {
  if (CONNECTED) {
    reminder.toBeRemoved = true;
    writeReminderCharacteristic?.writeValue(convertReminderToBytes(reminder));
  }
};

Step 2: Interacting with GATT Characteristics

Once connected, the application interacts with the device’s services and their associated GATT characteristics. Characteristics represent data attributes or actions on the device.

server.getPrimaryService(SERVICE_UUID)
.then(service => service.getCharacteristics())
.then(characteristics => {
  characteristics.forEach(characteristic => {
    switch (characteristic.uuid) {
      case READ_REMINDERS_CHARACTERISTIC_UUID:
        readReminderCharacteristic = characteristic;
        readReminderCharacteristic.addEventListener(
          'characteristicvaluechanged',
          readReminderCharacteristicValChanged,
        );
        readReminderCharacteristic.readValue();
        break;
      case NEW_REMINDER_CHARACTERISTIC_UUID:
        writeReminderCharacteristic = characteristic;
        break;
      case DATE_CHARACTERISTIC_UUID:
        dateCharacteristic = characteristic;
        setInterval(syncTime, 2000);
        break;
    }
  });
 
  CONNECTED = true;
  initializeStore();
})
.catch(error => {
  CONNECTED = false;
  console.error('Service or characteristic retrieval error:', error);
});
  • Service: A collection of characteristics defining a specific feature (e.g., Heart Rate Monitor).
  • Characteristics: Attributes within a service, each with a unique UUID for identification and access permissions.

Step 3: Data Exchange and Event Handling

Interact with characteristics to read, write, or listen for changes in real-time.

readReminderCharacteristic.readValue()
.then(value => {
  const reminderString = decoder.decode(value);
  if (reminderString !== '__$$NoTasks') {
    const reminder = createReminderFromString(reminderString);
    const { notifyReminder } = useRemindersStore();
    if (reminder) notifyReminder(reminder);
  }
})
.catch(error => {
  console.error('Characteristic read error:', error);
});
 
dateCharacteristic.writeValue(encoder.encode(timeString))
.then(() => {
  // Data successfully written to characteristic
})
.catch(error => {
  console.error('Characteristic write error:', error);
});
 
readReminderCharacteristic.addEventListener('characteristicvaluechanged', event => {
  // Handle characteristic value change
});
  • Read/Write: Retrieve current values (readValue()) or update device states (writeValue(data)).
  • Event Listeners: Receive notifications (addEventListener) for characteristic value changes.

Understanding the Navigator API for Bluetooth Connections

The Navigator API simplifies Bluetooth integration into web applications by providing standardized methods to connect, discover services, and interact with characteristics. It ensures secure and efficient communication between web apps and Bluetooth peripherals, fostering the development of sophisticated IoT solutions.

Navigator API Nuances

  • Browser Support: Check browser compatibility for the Navigator API and ensure experimental features (if required) are enabled.
  • Service and Characteristic Access: Utilize UUIDs to access specific device features and functionalities securely.
  • Real-Time Updates: Leverage event listeners for characteristic changes to enable responsive and dynamic application behavior.

Conclusion

By mastering the Navigator API for Bluetooth connections, developers can unlock the full potential of IoT integration in web applications. Whether building health monitors, smart home devices, or industrial automation systems, this API empowers developers to create robust, real-time solutions with seamless Bluetooth connectivity.

Start exploring the possibilities today and transform your web applications with direct browser-to-device interactions using Bluetooth!