Using Web Bluetooth in an Electron App in 2022
Tags: javascript, electron, bluetooth, webdev
Published: 2022-06-12 14:17:00

The [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) is quite a thoughtful browser feature that allows you to interact with your [BLE](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) peripherals. If you have such a device handy at the moment, consider testing its capabilities via this [link](https://googlechrome.github.io/samples/web-bluetooth/). Say, you could conveniently find out the device information "characteristics" of your peripheral which include details like manufacturer's name, firmware, hardware, and software details, model number, etc. via Bluetooth. I wouldn't dive deeper into the capabilities of the API and shall be assuming that you know [how it works](https://web.dev/bluetooth/).

One thing to note is that this API is currently **experimental** and isn't too widely supported. Fortunately, Chromium and hence, Chrome and Edge support it to a [decent extent](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility) which is good enough for a lot of common use cases.

Its availability in Chromium indicates that Electron Apps can utilize it as well. However, the developer experience in doing so isn't too ideal. At the time of this writing, when we "request" a connection to a BLE peripheral, the Electron app doesn't show you any popup window to allow you to select the device you want to connect with. Instead, it silently connects with the first available device which doesn't sound so great. **This article is dedicated to getting past this issue.**

---

_Chrome and Edge don't have this issue. You get a popup containing a list of devices to choose from._

--- 

Okay, so let's quickly reiterate the problem: we don't have access to any popup window or UI to select the preferred BLE device from an Electron App. 

Now, what could be the solution? Any frontend developers caught any hint? 👩‍💻🤔

Well, if there is no popup, then let's simply create a popup. 😅

---

_Yes, the solution is that straightforward only if we know what Inter-Process Communication in Electron means. The Electron team has already published a great [primer](https://www.electronjs.org/docs/latest/tutorial/ipc) on it. IMO, it should be enough to learn all what we need regarding IPC._

--- 

Electron App developers would know that it runs on 2 processes: `main` and `renderer`. The `main` process handles the NodeJS side of things and is responsible for spawning the `renderer` process which in turn is responsible for rendering the web page. 

Now what happens is that once we fire a Bluetooth connection request ([`requestDevice`](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice)) in the `renderer` process, it interestingly triggers a `"select-bluetooth-device"` event in the `main` process. The payload of this event contains an array of all the available BLE devices. Upon receiving that list, we need to send it to the `renderer` process to create the required popup UI. This is where IPC is used: to help the `main` and the `renderer` processes communicate with each other. 

Now, let's zoom in and look at the code:

- _Renderer_: I need to connected to a BLE so I have triggered `requestDevice`.

```ts
const device = await navigator.bluetooth.requestDevice({...});
```

- _Main_: Since I was listening to `"select-bluetooth-device"`, I have received the device list. Sending it to you via the `xyz` channel.

```ts
  win.webContents.on("select-bluetooth-device", (event, devices, callback) => {
    event.preventDefault();
    win.webContents.send("xyz", devices);
    console.log("Bluetooth device list dispatched.");
  });
```
- _Renderer_: Since I was already listening to the `xyz` channel, I have received the device list. I'll use it to create my popup.

```ts
// preload.ts
handleBluetoothDevices: (callback: (evt: IpcRendererEvent, value: ElectronBluetoothDevice[]) => void) => {
    // Fire the callback when we receive something on the "xyz" channel
    ipcRenderer.on("xyz", callback);
},
```

```ts
// renderer.ts
window.electronAPI?.handleBluetoothDevices((_evt: IpcRendererEvent, value: ElectronBluetoothDevice[]) => { 
  // Use the devices array to create the popup
 });
```

- _Renderer_: I created my popup and the user has selected a device. I am sending the device ID to you (via the `abc` channel) because you have the _"means"_ to connect to a BLE.

```ts
// renderer.ts
window.electronAPI?.selectBluetoothDevice(deviceID);
```

```ts
// preload.ts
selectBluetoothDevice: (value: string) => {
    // Send the device ID to "main"
    ipcRenderer.send("abc", value);
},
```

- _Main_: I was listening to the `abc` channel and have received the device ID. Connecting to the device now.

```ts
  ipcMain.on("abc", (_evt: IpcMainEvent, value: string) => {
    ...
    console.log(`Bluetooth device '(${value})' selected.`);
  });
```

---

### Now, how does `main` connects to a BLE?

Recall the following:

```ts
win.webContents.on("select-bluetooth-device", (event, devices, callback) => {
```

Here, apart from getting the device list, we also get a "one-time" `callback`. When this `callback` is fired with `""`, the BLE connection terminates and when it is fired with a device ID, a connection to the corresponding BLE peripheral gets established. Finally, you can start interacting with your device. 🚀

---

Parting Notes:

- Since Web Bluetooth API is supported in Chromium, we may be naively thinking of it as a straightforward process. The intent behind this article was to uncover some intricate details which a first time implementer may not know.

- This [SO entry](https://stackoverflow.com/a/59940816/6163934) ❤️ can be quite helpful in explaining the workflow in more depth. Recommend anyone in the process of utilizing the Web Bluetooth API in an Electron App to go through it.

- macOS users remember to allow Chromium and your IDE (say, VS Code) access to Bluetooth via System Preferences if you don't want to waste a few hours scratching your head. Speaking from [experience](https://github.com/electron/electron/issues/34443).