Electron Web Bluetooth API requestDevice() 错误

Electron Web Bluetooth API requestDevice() Error

我正在尝试开发一个与低功耗蓝牙设备通信的应用程序。我使用 Web 蓝牙 API 建立了一个工作 "website"。一切正常,所以我使用 Electron 框架来构建应用程序。

问题已知 - 如果您启动 navigator.bluetooth.requestDevice(),您会收到此错误消息: User cancelled the requestDevice() chooser..

这是由于 Chromium 中缺少设备选择器造成的。我发现了几个关于解决方法的主题,但没有示例。这是我的第一个 Electron 项目。也许有人解决了这个问题并且可以给我提示 :-)

在你的main.js中添加这段代码

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

这将在您的 Electron 应用程序中启用蓝牙。 并以此作为参考

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

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

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

但我建议您考虑一下您的 Electron 版本。

这里是一个代码示例,它将 return 第一个设备,而不是必须实现设备选择器:

  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);
    }
  });

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

非常感谢您的支持。根据您的建议和一些研究,我开发了一个可行的解决方案并愿意与您分享。

这两个链接对我帮助很大:

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

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

尤其是来自 MarshallOfSound 的 post – 描述得很好:

  • 在主进程中挂钩事件
  • 使用设备列表向渲染器进程发送消息
  • 在渲染器进程中显示UI
  • 发送selected设备到主进程
  • 调用回调

要获取有关主进程和渲染器进程、事件及其 API 的更多信息,请阅读:

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(已被 Gerrit post编辑)

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

对于我的应用程序,我想要一个设备选择器,如 Chrome 中所示。我想实现的顺序是:

  • 开始申请
  • 搜索设备
  • Devicepicker 弹出
  • Select 一台设备
  • Devicepicker 关闭
  • 查看申请中的数据

参考教程流程和代码片段的代码:

electron应用程序:main.js(主进程)renderer.js(渲染进程)devicepicker GUI:devicepicker.js(渲染进程)devicepicker.html & layout.css (GUI)

1) 用一个 GUI(我用了两个)和一个脚本创建 devicepicker

2) 在您的 main.js 中,在应用程序对象的 'ready' 事件中创建一个 select-bluetooth-device 事件(上面链接中的文档)当您在 renderer.js 中启动 navigator.bluetooth.requestDevice() 时,事件被触发并且设备列表在主进程中。使用 console.log(deviceList) 它在 shell 中可见。要处理它,您需要将它发送到渲染器进程(您的应用程序 window)。

3) 为了实现这一点,我们在 webContents.on 事件中实现了 BrowserWindow 对象的 webContents.send。现在主进程每次通过通道 channelForBluetoothDeviceList

发现新设备时都会发送一个设备列表

4) 在 renderer.js startDevicePicker() 中创建。 devicePicker() 必须在与 navigator.bluetooth.requestDevice() 相同的函数中启动。 startDevicePicker() 实例化一个新的 BrowserWindow() 对象,它加载 devicepicker.html

5) 要从主进程获取列表,必须在 startDevicePicker() 中实现一个 ipcRenderer.on() 侦听器来侦听 channelForBluetoothDeviceList我们主要过程的通道。现在我们可以在我们的电子应用程序(渲染器进程)中获取列表。要将它发送到 devicepicker UI,我们需要将它从我们的电子应用程序(渲染器进程)转发到 devicepicker(也是一个渲染器进程)

6) 为了实现这一点,我们需要 devicePicker() 中的 ipcRenderer.sendTo() 发送器,它将消息从渲染器进程转发到特定的其他渲染器过程。除了通道 bluetoothDeviceDiscoverList,我们还需要设备选择器的 BrowserWindow.id。因为我们刚刚实例化了它,所以我们可以使用我们的 devicepicker 对象。我有一个只发送一次的设备,主要过程比 devicepicker 的构建更快,而且我的列表从未发送到 devicepicker。所以我用 Promise()ipcRenderer.sendTo() 等待,直到设备选择器准备好使用。

7) 要在我们的设备选择器 GUI 接收设备列表,我们需要使用 ipcRenderer.on() (devicepicker.js).我现在将设备列表插入到设备选择器的 <option> 中,您当然可以使用其他元素 (devicepicker.html)。请注意:实施一个查询,将发送的列表与当前列表进行比较。否则你会得到多个设备并且你的 selection 会变得很长。我还需要这样做,还没有完成:-)

8) 到 select navigator.bluetooth.requestDevice() (renderer.js) 得到解析的设备,我们需要发回 BluetoothDevice.deviceId 我们的 selected 设备到主进程,我们在主进程中使用 deviceId 作为回调参数 (main.js) 回调“回调”。

9) 现在我们可以使用 ipcRenderer.sendTo() 将 selected BluetoothDevice.deviceId 发送到主进程 (devicepicker.js)。

10) 在电子应用程序的主进程 (main.js) 中,我们使用 ipcMain.on() 和回调监听通道 channelForSelectingDevice收到 BluetoothDevice.deviceId。设备发现停止,navigator.bluetooth.requestDevice() 得到解决,我们在应用程序中从我们的设备接收数据 (renderer.js)。要取消发现设备,请在另一个频道中用 ipcMain.on() 收听 channelForTerminationSignal 只是一个信号到主进程 (main.js),例如在单击后 (devicepicker.js) 和使用空字符串调用回调(如文档中所写)

我承认如果没有设备选择器,它可以做得更简单。然后只需将设备列表从主进程 (main.js) 发送到您的应用程序(渲染器进程)。但这对我理解电子过程有很大帮​​助。我希望本教程对您有用:-)!

片段:

    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();
}}