RubDub0
RubDub0

Reputation: 83

Electron Web Bluetooth API requestDevice() Error

I'm trying to develop an application which communicates with Bluetooth Low Energy Devices. I established a working "website" with the Web Bluetooth API. Everything works fine, so I used the Electron framework, to build an application.

The issue is known - if you start navigator.bluetooth.requestDevice(), you get this error message: User cancelled the requestDevice() chooser..

This causes due to the missing device chooser in Chromium. There are several topics about workarounds I found, but no examples. This is my first Electron project. Maybe somebody solved this problem and can give me a hint :-)

Upvotes: 2

Views: 6904

Answers (3)

RubDub0
RubDub0

Reputation: 83

thank you very much for your support. According to your suggestions and some research, I developed a working solution and like to share it with you.

These two links helped me a lot:

https://github.com/electron/electron/issues/11865

https://github.com/electron/electron/issues/10764

Especially this post from MarshallOfSound – well described:

  • Hook event in main process
  • Send message to renderer process with device list
  • Show UI in renderer process
  • Send selected device to main process
  • Call callback

To get more Information about the main process and renderer process, events and their API, read this:

https://www.electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processes

https://www.electronjs.org/docs/api/ipc-main

https://www.electronjs.org/docs/api/web-contents#contentssendchannel-args

https://www.electronjs.org/docs/api/ipc-renderer

https://electronjs.org/docs/api/web-contents#event-select-bluetooth-device (already posted by Gerrit)

https://www.electronjs.org/docs/api/structures/bluetooth-device

For my application I wanted a device picker, as seen in Chrome. The sequence I wanted to implement is:

  • Start application
  • Search for a device
  • Devicepicker pops up
  • Select a device
  • Devicepicker closes
  • See data in application

A reference to the code for the processes of the tutorial and the code snippet:

electron application: main.js (main process) renderer.js (render process) devicepicker GUI: devicepicker.js (renderer process) devicepicker.html & layout.css (GUI)

1) Create devicepicker with a GUI (I used and two ) and a script

2) In your main.js create a select-bluetooth-device event inside the 'ready' event of your application object (docs in links above) When you start navigator.bluetooth.requestDevice() in your renderer.js, the event get’s fired and the devicelist is in the main process. With console.log(deviceList) it's visible in the shell. To handle it you need to send it to the renderer process (your application window).

3) To achieve this, we implement webContents.send of our BrowserWindow object inside the webContents.on event. Now the main process sends a devicelist every time he found new devices through the channel channelForBluetoothDeviceList

4) Create in renderer.js startDevicePicker(). devicePicker() must be started in the same function as navigator.bluetooth.requestDevice(). startDevicePicker() instantiates a new BrowserWindow() object which loads devicepicker.html

5) To get the list from the main process a ipcRenderer.on() listener must be implemented in startDevicePicker() that listens to the channelForBluetoothDeviceList channel of our main process. Now we can get the list in our electron application (renderer prcoess). To send it to the devicepicker UI, we need to forward it from our electron application (renderer process) to the devicepicker (also a renderer process)

6) To achieve this, we need the ipcRenderer.sendTo() sender in devicePicker(), which forwards messages between from a renderer process to a specific other renderer process. Additional to the channel bluetoothDeviceDiscoverList we need the BrowserWindow.id of the devicepicker. Since we just instantiated it, we can use our devicepicker object. I had a device which sended only once, the main process was faster than the build of the devicepicker and my list was never sent to the devicepicker. So I used a Promise() to wait with ipcRenderer.sendTo() until the devicepicker was ready to use.

7) To receive the devicelist at our devicepicker GUI, we need to listen to the bluetoothDeviceDiscoverList with ipcRenderer.on() (devicepicker.js). I inserted the devicelist now to the <option> of the devicepicker, you can use of course other elements (devicepicker.html). Please note: implement a query that compares the sended list to the current. Otherwise you get multiple devices and your selection gets loooong. I still need to do that, it's not finished yet :-)

8) To select a device that navigator.bluetooth.requestDevice() (renderer.js) gets resolved, we need to send back the BluetoothDevice.deviceId of our selected device to the main process where we callback ‚callback‘ with deviceId as callback parameter (main.js).

9) Now we can use ipcRenderer.sendTo() send the selected BluetoothDevice.deviceId to the mainprocess (devicepicker.js).

10) In the main process (main.js) of our electron application we listen to the channel channelForSelectingDevice with ipcMain.on() and callback with the received BluetoothDevice.deviceId. The device discovery gets stopped, navigator.bluetooth.requestDevice() gets resolved and we receive data from our device in our application (renderer.js). To cancel the discovery of devices, listen with ipcMain.on() in another channel channelForTerminationSignal just a signal to the main process (main.js), for example after a click (devicepicker.js) and call the callback with an empty string (as written in the docs)

I admit it could be done much simpler without a devicepicker. Then just send the devicelist from the main process (main.js) to your application (renderer process). But this helped me a lot to understand the processes in electron. I hope this tutorial is useful for you :-) !

        main.js
        
    const { ipcMain, app, BrowserWindow } = require('electron')
    
    let win = null;
    
    var callbackForBluetoothEvent = null;
    
    // Create the browser window.
    function createWindow () {
      
      win = new BrowserWindow({
        webPreferences: {
          nodeIntegration: true //to activate require()
        }
      })
    
      win.maximize()
      win.show()
    
      //This sender sends the devicelist from the main process to all renderer processes
      win.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
        event.preventDefault(); //important, otherwise first available device will be selected
        console.log(deviceList); //if you want to see the devices in the shell
        let bluetoothDeviceList = deviceList;
        callbackForBluetoothEvent = callback; //to make it accessible outside createWindow()
    
        win.webContents.send('channelForBluetoothDeviceList', bluetoothDeviceList);
        });
    
        // This method will be called when Electron has finished
        // initialization and is ready to create browser windows.
        // Some APIs can only be used after this event occurs.
        app.on('ready', createWindow)
    
        //cancels Discovery
        ipcMain.on('channelForTerminationSignal', _ => {
            callbackForBluetoothEvent(''); //reference to callback of win.webContents.on('select-bluetooth-device'...)
            console.log("Discovery cancelled");
        });
    
        //resolves navigator.bluetooth.requestDevice() and stops device discovery
        ipcMain.on('channelForSelectingDevice', (event, DeviceId) => {
            callbackForBluetoothEvent(sentDeviceId); //reference to callback of win.webContents.on('select-bluetooth-device'...)
            console.log("Device selected, discovery finished");
        })
        renderer.js
    
    function discoverDevice() {
        navigator.bluetooth.requestDevice()
        startDevicepicker()
    }
    
    function startDevicepicker(){
        
        let devicepicker = null;
        let mainProcessDeviceList = null;
        
        devicepicker = new BrowserWindow({
            width: 350,
            height: 270,
            show: false, //needed to resolve promise devicepickerStarted()            
            webPreferences: {
                nodeIntegration: true
              }
        })
    
        devicepicker.loadFile('devicePicker.html');
    
        //electron application listens for the devicelist from main process
        ipcRenderer.on('channelForBluetoothDeviceList', (event, list) => {
            mainProcessDeviceList = list;
            devicepickerStarted.then(_=> {
                console.log("Promise resolved!");
                ipcRenderer.sendTo(devicepicker.webContents.id, 'bluetoothDeviceDiscoverList', mainProcessDeviceList);     
            })   
        })
    
        //Promise that ensures that devicepicker GUI gets the list if the device only sends once
        var devicepickerStarted = new Promise(
            function (resolve, reject) {
                console.log("Promise started");
                devicepicker.once('ready-to-show', () => {
                    devicepicker.show();
                    resolve();
                    console.log("Devicepicker is ready!")
                })
            }
        )
    
        //remove listeners after closing devicepicker
        devicepicker.on('closed', _ => {            
            devicepicker = null;
            ipcRenderer.removeAllListeners('channelForBluetoothDeviceList');
            ipcRenderer.removeAllListeners('currentWindowId');
            ipcRenderer.removeAllListeners('receivedDeviceList');            
        })
    }
    
        devicepicker.js
        
    //save received list here
    var myDeviceList = new Array();
    
    //Html elements
    const devicePickerSelection = document.getElementById("devicePickerSelection");
    const buttonSelect = document.getElementById("Select");
    const buttonCancel = document.getElementById("Cancel");
    
    //eventListeners for buttons
    buttonSelect.addEventListener('click', selectFromDevicePicker);
    buttonCancel.addEventListener('click', cancelDevicePicker);
    
    //listens for deviceList
    ipcRenderer.on('receivedDeviceList', (event, bluetoothDeviceDiscoverList) => {
        console.log("list arrived!")
        //code: add list to html element
        });
    
    function selectFromDevicePicker() {
        let selectedDevice = devicePickerSelection.value;
        let deviceId = //depends on where you save the BluetoothDevice.deviceId values
    
        //sends deviceId to main process for callback to resolve navigator.bluetooth.requestDevice()
        ipcRenderer.send('channelForSelectingDevice', deviceId);
    
        ipcRenderer.removeAllListeners('receivedDeviceList');    
        closeDevicePicker();
    }
    
    function cancelDevicePicker() {    
        ipcRenderer.send('channelForTerminationSignal');
        closeDevicePicker();
    }
    
    function closeDevicePicker() {    
        myDevicePicker.close();
    }}

Upvotes: 6

tpikachu
tpikachu

Reputation: 4844

In your main.js Add this code snippet

if (process.platform === "linux"){
  app.commandLine.appendSwitch("enable-experimental-web-platform-features", true);
} else {
  app.commandLine.appendSwitch("enable-web-bluetooth", true);
}

This will enable the bluetooth at your Electron app. And use this as reference

https://github.com/electron/electron/issues/11865

https://github.com/electron/electron/issues/7367

https://github.com/aalhaimi/electron-web-bluetooth

But I'd suggest you to consider your Electron version.

Upvotes: 3

Gerrit
Gerrit

Reputation: 852

Here is a code sample that will just return the first device instead of having to implement a device chooser:

  mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
    event.preventDefault();
    console.log('Device list:', deviceList);
    let result = deviceList[0];
    if (!result) {
      callback('');
    } else {
      callback(result.deviceId);
    }
  });

Source: https://electronjs.org/docs/api/web-contents#event-select-bluetooth-device

Upvotes: 0

Related Questions