如何在 Vue 应用中,实现一个基于 `WebUSB` 或 `WebBluetooth` 的硬件设备交互功能?

各位观众老爷,欢迎来到今天的 Vue.js 硬件交互特别节目!今天咱们不搞花里胡哨的 UI 动画,来点硬核的——让你的 Vue 应用跟真实世界的硬件设备眉来眼去,哦不,是进行数据交互。

我们今天的主角是 WebUSB 和 Web Bluetooth,这两个浏览器 API 允许你直接从 JavaScript 代码控制连接到电脑的 USB 设备和蓝牙设备。想想就刺激,对不对?

为什么选择 WebUSB 和 Web Bluetooth?

在过去,想让 Web 应用跟硬件打交道,那可是个麻烦事。你得安装各种浏览器插件、ActiveX 控件,或者干脆整个 Native App。这不仅用户体验差,安全性也让人捏把汗。

WebUSB 和 Web Bluetooth 的出现,彻底改变了游戏规则。它们提供了标准化的 API,允许在浏览器中安全地访问硬件设备,而且无需安装任何插件!

特性 WebUSB Web Bluetooth
连接方式 USB Bluetooth
适用场景 打印机、3D 打印机、开发板等需要高速数据传输的设备 智能手表、心率带、智能家居设备等低功耗设备
安全性 基于 HTTPS 协议,用户授权访问 基于 HTTPS 协议,用户授权访问
兼容性 Chrome, Edge, Opera 等 Chrome, Edge, Opera, Safari (部分支持)等

准备工作

  1. HTTPS 环境: 必须使用 HTTPS 协议,否则浏览器会拒绝访问 WebUSB 和 Web Bluetooth。本地开发可以使用 localhost
  2. 现代浏览器: 确保你使用的是支持 WebUSB 和 Web Bluetooth 的浏览器,比如 Chrome, Edge, Opera。Safari 对 Web Bluetooth 的支持还在完善中。
  3. 硬件设备: 废话,当然得有硬件设备才能玩啊!最好是那种已经支持 WebUSB 或 Web Bluetooth 的设备,或者你能自己写固件控制的设备。
  4. Vue.js 项目: 我们今天的主战场是 Vue.js,所以你需要一个 Vue 项目。如果你还没有,可以用 Vue CLI 快速搭建一个。
vue create my-hardware-app

WebUSB 实战:点亮你的 LED 灯

咱们先来个简单的 WebUSB 示例,假设你有一个 USB 控制的 LED 灯(或者一个 Arduino 开发板,可以模拟 LED 灯)。我们的目标是:点击 Vue 应用中的按钮,点亮或熄灭 LED 灯。

1. HTML 结构

在你的 App.vue 组件中,添加两个按钮和一个显示设备连接状态的区域:

<template>
  <div id="app">
    <h1>WebUSB LED 控制器</h1>
    <p>设备状态: {{ deviceStatus }}</p>
    <button @click="connectDevice" :disabled="isConnecting || isConnected">
      {{ isConnecting ? '连接中...' : (isConnected ? '已连接' : '连接设备') }}
    </button>
    <button @click="toggleLed" :disabled="!isConnected">
      {{ ledStatus ? '熄灭 LED' : '点亮 LED' }}
    </button>
  </div>
</template>

2. JavaScript 逻辑

接下来,在 script 标签中编写 JavaScript 代码:

<script>
export default {
  data() {
    return {
      device: null,
      deviceStatus: '未连接',
      isConnecting: false,
      isConnected: false,
      ledStatus: false, // LED 的状态 (true: 亮, false: 灭)
      endpointIn: null, // 输入端点
      endpointOut: null, // 输出端点
    };
  },
  methods: {
    async connectDevice() {
      this.isConnecting = true;
      try {
        // 1. 请求设备
        this.device = await navigator.usb.requestDevice({
          filters: [], // 可以根据 vendorId 和 productId 过滤设备
        });

        this.deviceStatus = '已选择设备';

        // 2. 打开设备
        await this.device.open();
        this.deviceStatus = '设备已打开';

        // 3. 选择配置 (通常是第一个配置)
        await this.device.selectConfiguration(1);
        this.deviceStatus = '配置已选择';

        // 4. 声明接口
        await this.device.claimInterface(0); // 接口编号通常是 0
        this.deviceStatus = '接口已声明';

        // 5. 找到输入和输出端点
        let endpointInfo = this.findEndpoints(this.device.configuration.interfaces[0].alternates[0].endpoints);
        if(endpointInfo){
          this.endpointIn = endpointInfo.endpointIn;
          this.endpointOut = endpointInfo.endpointOut;
        } else {
          this.deviceStatus = '未找到输入或输出端点';
          this.disconnectDevice();
          return;
        }

        this.isConnected = true;
        this.isConnecting = false;
        this.deviceStatus = '连接成功';

      } catch (error) {
        this.deviceStatus = '连接失败: ' + error;
        this.isConnecting = false;
        this.isConnected = false;
        console.error('WebUSB 连接错误:', error);
      }
    },

    async toggleLed() {
      // 假设你的 USB 设备使用 0x01 控制 LED 的开关
      const value = this.ledStatus ? 0x00 : 0x01;
      try {
        // 发送控制命令
        await this.device.transferOut(this.endpointOut.endpointNumber, new Uint8Array([value]));
        this.ledStatus = !this.ledStatus; // 切换 LED 状态
        this.deviceStatus = `LED 已${this.ledStatus ? '点亮' : '熄灭'}`;
      } catch (error) {
        this.deviceStatus = '控制 LED 失败: ' + error;
        console.error('WebUSB 控制 LED 错误:', error);
      }
    },

    disconnectDevice() {
      if (this.device) {
        try{
          this.device.close();
          this.deviceStatus = '设备已断开';
        } catch (error){
          this.deviceStatus = '断开设备失败: ' + error;
        }
        this.device = null;
        this.isConnected = false;
        this.isConnecting = false;
        this.ledStatus = false;
        this.endpointIn = null;
        this.endpointOut = null;

      }
    },

    findEndpoints(endpoints){
      let endpointIn = null;
      let endpointOut = null;
      for(let endpoint of endpoints){
        if(endpoint.direction === 'in'){
          endpointIn = endpoint;
        } else if (endpoint.direction === 'out'){
          endpointOut = endpoint;
        }
      }
      if(endpointIn && endpointOut){
        return {endpointIn, endpointOut};
      } else {
        return null;
      }
    },
  },
  beforeUnmount() {
    this.disconnectDevice();
  }
};
</script>

3. 代码解释

  • navigator.usb.requestDevice() 弹出设备选择框,让用户选择 USB 设备。filters 数组可以用来过滤特定厂商和产品的设备。
  • device.open() 打开 USB 设备。
  • device.selectConfiguration() 选择设备的配置。通常情况下,第一个配置 (配置编号为 1) 就够用了。
  • device.claimInterface() 声明要使用的接口。接口编号通常是 0。
  • device.transferOut() 向 USB 设备发送数据。第一个参数是输出端点的编号,第二个参数是要发送的数据(必须是 Uint8Array 类型)。
  • 错误处理: 别忘了用 try...catch 捕获错误,并显示给用户。

4. 注意事项

  • USB 设备描述符: 你的 USB 设备需要提供正确的设备描述符,包括 vendorId(厂商 ID)和 productId(产品 ID)。这些信息可以在设备的驱动程序或文档中找到。
  • 权限问题: 某些操作系统可能需要额外的权限才能访问 USB 设备。
  • 数据格式: 你需要了解你的 USB 设备使用的数据格式,才能正确地发送和接收数据。

Web Bluetooth 实战:读取心率数据

接下来,咱们来挑战一下 Web Bluetooth,读取一个蓝牙心率带的数据。

1. HTML 结构

跟 WebUSB 类似,我们需要一些按钮和显示区域:

<template>
  <div id="app">
    <h1>Web Bluetooth 心率监测器</h1>
    <p>设备状态: {{ deviceStatus }}</p>
    <button @click="connectBluetoothDevice" :disabled="isConnecting || isConnected">
      {{ isConnecting ? '连接中...' : (isConnected ? '已连接' : '连接设备') }}
    </button>
    <p v-if="heartRate">心率: {{ heartRate }} BPM</p>
  </div>
</template>

2. JavaScript 逻辑

<script>
export default {
  data() {
    return {
      device: null,
      server: null,
      heartRateService: null,
      heartRateCharacteristic: null,
      deviceStatus: '未连接',
      isConnecting: false,
      isConnected: false,
      heartRate: null,
    };
  },
  methods: {
    async connectBluetoothDevice() {
      this.isConnecting = true;
      try {
        // 1. 请求设备
        this.device = await navigator.bluetooth.requestDevice({
          filters: [{ services: ['heart_rate'] }], // 只显示支持心率服务的设备
          optionalServices: ['heart_rate', 'battery_service'], // 请求访问其他服务
        });

        this.deviceStatus = '已选择设备: ' + this.device.name;

        // 2. 连接 GATT 服务器
        this.server = await this.device.gatt.connect();
        this.deviceStatus = '已连接 GATT 服务器';

        // 3. 获取心率服务
        this.heartRateService = await this.server.getPrimaryService('heart_rate');
        this.deviceStatus = '已获取心率服务';

        // 4. 获取心率测量特征
        this.heartRateCharacteristic = await this.heartRateService.getCharacteristic('heart_rate_measurement');
        this.deviceStatus = '已获取心率测量特征';

        // 5. 启动心率通知
        await this.heartRateCharacteristic.startNotifications();
        this.deviceStatus = '已启动心率通知';

        // 6. 监听心率数据
        this.heartRateCharacteristic.addEventListener('characteristicvaluechanged', this.handleHeartRate);

        this.isConnected = true;
        this.isConnecting = false;
      } catch (error) {
        this.deviceStatus = '连接失败: ' + error;
        this.isConnecting = false;
        this.isConnected = false;
        console.error('Web Bluetooth 连接错误:', error);
      }
    },

    handleHeartRate(event) {
      // 解析心率数据
      const value = event.target.value;
      const flags = value.getUint8(0);
      const rate16Bits = flags & 0x1;
      let heartRate;

      if (rate16Bits) {
        heartRate = value.getUint16(1, /* littleEndian= */ true);
      } else {
        heartRate = value.getUint8(1);
      }

      this.heartRate = heartRate;
    },

    async disconnectBluetoothDevice() {
      if (this.device && this.device.gatt.connected) {
        try{
          await this.heartRateCharacteristic.stopNotifications();
          this.heartRateCharacteristic.removeEventListener('characteristicvaluechanged', this.handleHeartRate);
          this.device.gatt.disconnect();
          this.deviceStatus = '设备已断开';
        } catch (error){
          this.deviceStatus = '断开设备失败: ' + error;
        }

        this.device = null;
        this.server = null;
        this.heartRateService = null;
        this.heartRateCharacteristic = null;
        this.isConnected = false;
        this.isConnecting = false;
        this.heartRate = null;
      }
    },
  },
  beforeUnmount() {
    this.disconnectBluetoothDevice();
  }
};
</script>

3. 代码解释

  • navigator.bluetooth.requestDevice() 弹出设备选择框,filters 数组指定要搜索的服务 UUID。optionalServices 数组允许你请求访问其他服务。
  • device.gatt.connect() 连接到设备的 GATT 服务器。GATT (Generic Attribute Profile) 是 Bluetooth LE 的核心概念,它定义了设备提供的服务和特征。
  • server.getPrimaryService() 获取指定 UUID 的服务。
  • service.getCharacteristic() 获取指定 UUID 的特征。特征是 GATT 中最小的数据单元。
  • characteristic.startNotifications() 启动特征值的通知。当特征值发生变化时,设备会向你的应用发送通知。
  • characteristic.addEventListener('characteristicvaluechanged', this.handleHeartRate) 监听特征值变化的事件,并在 handleHeartRate 方法中解析心率数据。
  • 心率数据解析: 心率数据的格式比较复杂,需要根据 Bluetooth SIG 规范进行解析。这段代码只是一个简单的示例,可能需要根据你的心率带的实际情况进行调整。

4. 注意事项

  • Bluetooth UUID: 你需要知道你想要访问的服务和特征的 UUID。这些信息通常可以在设备的文档或 Bluetooth SIG 数据库中找到。
  • 数据格式: 不同的 Bluetooth 设备可能使用不同的数据格式。你需要了解你的设备的具体格式,才能正确地解析数据。
  • 安全性: 某些 Bluetooth 设备可能需要配对或授权才能访问。

进一步探索

  • Web Serial API: 如果你的硬件设备使用串口通信,可以考虑使用 Web Serial API。
  • HID API: 如果你的硬件设备是 HID (Human Interface Device) 设备,比如键盘、鼠标、游戏手柄等,可以使用 HID API。
  • 封装成 Vue 组件: 为了方便复用和管理,你可以把 WebUSB 和 Web Bluetooth 的代码封装成 Vue 组件。
  • 状态管理: 使用 Vuex 或 Pinia 等状态管理库来管理设备连接状态和数据。
  • 错误处理和重连机制: 增加更完善的错误处理和自动重连机制,提高应用的健壮性。

总结

WebUSB 和 Web Bluetooth 为 Web 应用打开了通往硬件世界的大门。虽然学习曲线稍微陡峭,但只要你掌握了基本概念和 API,就能创造出令人惊艳的应用。

记住,实践是检验真理的唯一标准。赶紧拿起你的硬件设备,开始你的 Web 硬件之旅吧!

好了,今天的讲座就到这里。希望大家有所收获,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注