DEV Community 👩‍💻👨‍💻

Cover image for Using Web Bluetooth in an Electron App in 2022
Maneet Goyal for Manufac Analytics Private Limited

Posted on • Updated on

Using Web Bluetooth in an Electron App in 2022

The Web Bluetooth API is quite a thoughtful browser feature that allows you to interact with your BLE peripherals. If you have such a device handy at the moment, consider testing its capabilities via this link. 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.

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 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 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) 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.
const device = await navigator.bluetooth.requestDevice({...});
Enter fullscreen mode Exit fullscreen mode
  • Main: Since I was listening to "select-bluetooth-device", I have received the device list. Sending it to you via the xyz channel.
  win.webContents.on("select-bluetooth-device", (event, devices, callback) => {
    event.preventDefault();
    win.webContents.send("xyz", devices);
    console.log("Bluetooth device list dispatched.");
  });
Enter fullscreen mode Exit fullscreen mode
  • Renderer: Since I was already listening to the xyz channel, I have received the device list. I'll use it to create my popup.
// preload.ts
handleBluetoothDevices: (callback: (evt: IpcRendererEvent, value: ElectronBluetoothDevice[]) => void) => {
    // Fire the callback when we receive something on the "xyz" channel
    ipcRenderer.on("xyz", callback);
},
Enter fullscreen mode Exit fullscreen mode
// renderer.ts
window.electronAPI?.handleBluetoothDevices((_evt: IpcRendererEvent, value: ElectronBluetoothDevice[]) => { 
  // Use the devices array to create the popup
 });
Enter fullscreen mode Exit fullscreen mode
  • 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.
// renderer.ts
window.electronAPI?.selectBluetoothDevice(deviceID);
Enter fullscreen mode Exit fullscreen mode
// preload.ts
selectBluetoothDevice: (value: string) => {
    // Send the device ID to "main"
    ipcRenderer.send("abc", value);
},
Enter fullscreen mode Exit fullscreen mode
  • Main: I was listening to the abc channel and have received the device ID. Connecting to the device now.
  ipcMain.on("abc", (_evt: IpcMainEvent, value: string) => {
    ...
    console.log(`Bluetooth device '(${value})' selected.`);
  });
Enter fullscreen mode Exit fullscreen mode

Now, how does main connects to a BLE?

Recall the following:

win.webContents.on("select-bluetooth-device", (event, devices, callback) => {
Enter fullscreen mode Exit fullscreen mode

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 ❤️ 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.

Oldest comments (0)

Visualizing Promises and Async/Await 🤯

async await

☝️ Check out this all-time classic DEV post