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({...});
-
Main: Since I was listening to
"select-bluetooth-device"
, I have received the device list. Sending it to you via thexyz
channel.
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.
// preload.ts
handleBluetoothDevices: (callback: (evt: IpcRendererEvent, value: ElectronBluetoothDevice[]) => void) => {
// Fire the callback when we receive something on the "xyz" channel
ipcRenderer.on("xyz", callback);
},
// 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.
// renderer.ts
window.electronAPI?.selectBluetoothDevice(deviceID);
// 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.
ipcMain.on("abc", (_evt: IpcMainEvent, value: string) => {
...
console.log(`Bluetooth device '(${value})' selected.`);
});
Now, how does main
connects to a BLE?
Recall the following:
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 ❤️ 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.
Top comments (5)
Can you share a complite Fiddle gist with the example?
Will try to find some time but the schedule looks packed in the near future, so hard to promise an ETA.
i want to make sample project. but i cannot typescript electron. can you sample source. provide me>?
You can try this: electronforge.io/templates/typescr...
well put, really helpful. thanks