Utools插件实现Web Bluetooth

想在Utools上插件用weble,它采用的是chromium内核,但是Electron 默认不会弹出设备选择窗口,如果直接使用web-ble那就会报用户取消选择的错误,所以overflower一下,发现这个问题是一直有的,找到了方案:2022 年 Electron 应用中使用 Web Bluetooth,同时Electron官方也给出案例的使用,关键点就是没有弹出窗口,要在main程序中拦截"select-bluetooth-device"事件,然后手动处理,官方也不提供只能自己写。


一、Electron的Web Bluetooth

要实现不让它自动取消,就要手动拦截取消自动处理,然后通过preload为中间组件,发送出去,代码如下:

javascript 复制代码
  // 在主进程捕获 Web Bluetooth 请求
  mainWindow.webContents.on("select-bluetooth-device", (event, deviceList, callback) => {
      event.preventDefault();// 禁止默认系统选择弹窗

      // 保存回调函数以便稍后使用
      selectCallback = callback;
      // 把设备列表发给渲染进程
      win.webContents.send("ble-device-list", deviceList);
      console.log("Bluetooth device list dispatched.");
    });
    
  // 当用户在自定义窗口选择了连接设备
  ipcMain.on("ble-device-selected", (_evt, deviceId) => {
    console.log("Choose the Device ID: ${deviceId}.");
    bluetoothCallback(deviceId);
  });

  // 用户取消选择
  ipcMain.on("ble-device-cancel", () => {
    console.log("User cancelled the requestDevice().");
    bluetoothCallback('');
  });

preload.js为中间组件,代码如下:

javascript 复制代码
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  onBLEDeviceList: (callback) => ipcRenderer.on("ble-device-list", callback),
  selectBLEDevice: (deviceId) => ipcRenderer.send("ble-device-selected", deviceId),
  cancelBLEDevice: () => ipcRenderer.send("ble-device-cancel"),
});

在render.js调用中间件,代码如下:

javascript 复制代码
const listEl = document.getElementById("device-list");
const scenBtn = document.getElementById("scenBtn");
const selectBtn = document.getElementById("select");
const cancelBtn = document.getElementById("cancel");

let selectedDeviceId = null;

listEl.addEventListener("change", () => {
  selectedDeviceId = listEl.value;
  console.log("当前选中:", selectedDeviceId);
});

selectBtn.addEventListener("click", () => {
  if (!selectedDeviceId) {
    alert("请先选择一个设备");
    return;
  }
  console.log("用户选中:", selectedDeviceId);
  window.electronAPI?.selectBLEDevice(selectedDeviceId);
});

cancelBtn.addEventListener("click",  ()=>{
  if(!selectedDeviceId)return;
  window.electronAPI?.cancelBLEDevice();
  listEl.innerHTML = "";
  selectedDeviceId=null;
});

scenBtn.addEventListener("click", async () => {
  try {
    console.log("开始连接");
 
    // 1. 请求 BLE 设备
    const device = await navigator.bluetooth.requestDevice({
    acceptAllDevices: true,
    });
    console.log('设备:', device.name);
  } catch (err) {
    console.error("❌ 请求失败:", err);
  }
});

// 监听主进程发送的设备列表
window.electronAPI?.onBLEDeviceList((_evt, deviceList) => {
  // 清空旧选项(可选)
  listEl.innerHTML = "";

  deviceList.forEach(device => {
    const option = document.createElement("option");
    option.value = device.deviceId;     // 选项值
    option.textContent  = `${device.deviceName || "未知设备"} (${device.deviceId})`; // 显示的文字
    listEl.appendChild(option);

  });
  // 确保 UI 刷新
  listEl.value = selectedDeviceId || deviceList[0]?.deviceId || "";
});

二、Node的abandonware/noble库的使用

为什么解决了electron的webble,又要继续用node包,因为这utools是electron写的,主程序的main插件是接触不到的,要不是utools自己支持不然就实现不了,解决方案还是node库现实一点,这个nobel好像还行,但不维护了所以用abandonware/noble库,参考的是:electron nodejs 检测蓝牙适配器可行方案npm 包 @abandonware/noble 使用教程,这个感觉能用是能,但很奇怪第一次扫描扫不出名称,再次扫可以出来所以它是没有初始化,这就很坑,感觉有用也不好用,preload.js代码如下:

javascript 复制代码
const noble = require('@abandonware/noble');

noble.on('stateChange', async (state) => {
  if (state === 'poweredOn') {
    await noble.startScanningAsync([],false);//初始化,[] = 所有服务,true = 允许重复广播
  }
});

let targetDevice=null;

      

window.services = {
  startScanning: () => {
    noble.startScanningAsync(); 
  },

};



var _gatt;
var _service;
var _deviceName;
var _chrct_cube;
var UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb';
var SERVICE_UUID = '0000fff0' + UUID_SUFFIX;
var CHRCT_UUID_CUBE = '0000fff6' + UUID_SUFFIX;
const TARGET_NAME = 'QY-QYSC-S-D2D3';




noble.on('discover', async (peripheral)=> {
    const deviceName= `${peripheral.advertisement.localName || "未知设备"} (${peripheral.address})`;
    console.log('发现设备,名称:', deviceName);
    if (TARGET_NAME === peripheral.advertisement.localName) {
      await noble.stopScanningAsync();
      console.log('找到目标设备,停止扫描,准备连接...');


      try {
          console.log("开始连接");
          await peripheral.connectAsync();
  

          // // 发现指定服务和特征
          // const { services, characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
          //   [SERVICE_UUID],
          //   [CHRCT_UUID_CUBE]
          // );

          const services=await peripheral.discoverServicesAsync([SERVICE_UUID]);
          const characteristics=await services[0].discoverCharacteristicsAsync([CHRCT_UUID_CUBE]);


          console.log('找到服务:', services[0]);
          const cubeChar = characteristics[0];
          console.log('找到特征:', cubeChar);

          // ========== 订阅通知(noble 方式)==========
          if (cubeChar.properties.includes('notify') || cubeChar.properties.includes('indicate')) {
            cubeChar.on('data', (data, isNotification) => {
              console.log('收到数据:', data.toString('hex'));
              onCubeEvent(data); // 你的处理函数
            });

            await cubeChar.subscribeAsync();
            console.log('已订阅通知');
          } else {
            console.log('特征不支持通知');
          }


        console.log('连接成功!');


        


        } catch (err) {
          console.error("❌ 请求失败:", err);
        }

    }
});

而调用也很方便,代码如下:

javascript 复制代码
$(function(){
   const $listEl   = $('#device-list');
   const $scenBtn  = $('#scenBtn');
   const $selectBtn = $('#select');
   const $cancelBtn = $('#cancel');



   $scenBtn.click(function(){
       console.log(" 开始写 jQuery 代码");
       window.services.startScanning();
  });
 
   //...
  
 
});

三、Node的webbluetooth库的使用

这才是真正可行的解决方案,Node.js 实现的 Web Bluetooth 规范的Type Script库,先是utools导入npm库,它的github为Node Web Bluetooth,好用可以点start支持,还有npm 包官方文档的参考,这才是现代化的库,与原版本webble用法一样,只要将变量名换一下就可以了,这库确实好用,代码如下:

javascript 复制代码
const bluetooth = require('webbluetooth').bluetooth;


console.log("蓝牙ble测试");
 
 
var _gatt;
var _service;
var _deviceName;
var _chrct_cube;
var UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb';
var SERVICE_UUID = '0000fff0' + UUID_SUFFIX;
var CHRCT_UUID_CUBE = '0000fff6' + UUID_SUFFIX;
 
var decoder = null;
var deviceMac = 'CC:A3:00:00:D2:D3';
var KEYS = ['NoDg7ANAjGkEwBYCc0xQnADAVgkzGAzHNAGyRTanQi5QIFyHrjQMQgsC6QA'];
 

async function connect() {

 
 console.log("开始连接");
 
    // 1. 请求 BLE 设备
    const device = await bluetooth.requestDevice({
        filters: [{
            name: 'QY-QYSC-S-D2D3'
        }],
        optionalServices: [SERVICE_UUID] // 这里加上你要访问的所有 service UUID
    });
    console.log('设备:', device.name);
 
    // 2. 连接 GATT 服务
    const server = await device.gatt.connect();
    console.log('已连接 GATT Server');
 
    // 3. 获取 Service
    const service = await server.getPrimaryService(SERVICE_UUID);
    console.log('service:\n',service);
 
    // 4. 获取 Characteristic
    const characteristic  = await service.getCharacteristic(CHRCT_UUID_CUBE);
    console.log('Characteristic:\n', characteristic);
 
    // 5. 订阅数据通知
    _chrct_cube=await characteristic.startNotifications();
    _chrct_cube.addEventListener('characteristicvaluechanged', onCubeEvent);
    console.log('已订阅数据通知 ✅');



}




window.services = {
  startScanning:async  () => {
    connect();
   
  },

};

 
  //数据处理函数
  function onCubeEvent(event) {
    console.log("aaaaaaaaaaaaaaaa");
 
  }

在Utools上用这个库的实现,如下图所示:


最后,写个ble-Demo一个是utools的,另一个electron的拦截实现,都要npm install然后,一个npm start另一个用utools选择:electron_ble_demohttps://github.com/10555gff/electron_ble_demo

相关推荐
李剑一2 小时前
mitt和bus有什么区别
前端·javascript·vue.js
VisuperviReborn2 小时前
React Native 与 iOS 原生通信:从理论到实践
前端·react native·前端框架
hashiqimiya3 小时前
html的input的required
java·前端·html
soda_yo3 小时前
JavaScripe中你所不知道的"变量提升"
javascript
Mapmost3 小时前
WebGL三维模型标准(二)模型加载常见问题解决方案
前端
Mapmost3 小时前
Web端三维模型标准(一):单位与比例、多边形优化
前端
www_stdio3 小时前
JavaScript 执行机制详解:从 V8 引擎到执行上下文
前端·javascript
我命由我123453 小时前
HTML - 换行标签的 3 种写法(<br>、<br/>、<br />)
前端·javascript·css·html·css3·html5·js
暮冬十七3 小时前
[特殊字符] Vue3 项目最佳实践:组件命名、目录结构与类型规范指南
前端·前端架构·vue3项目搭建