跳至主要內容
Skip to content

在網頁上與硬體對話:Web Bluetooth API 實戰指南

長久以來,網頁與硬體的互動總是被視為一條難以跨越的鴻溝。但隨著 Web Bluetooth API 的出現,我們現在可以直接從瀏覽器(無需安裝額外 App 或外掛)連接藍牙低功耗(BLE)設備,讀取心率感測器、控制燈光,甚至更新韌體。

本篇將帶你一步步封裝一個 Vue 3 的 useBluetooth,讓你輕鬆駕馭這個強大的功能。

WARNING

相容性警告: 目前 Web Bluetooth API 僅在 Chromium 核心 瀏覽器(Chrome, Edge, Opera)上完整支援,且必須在 HTTPS 環境下執行。Safari 與 Firefox 目前尚未支援。


一、 BLE 核心概念 (GATT)

在寫程式之前,我們必須先理解 BLE 的資料結構:GATT (Generic Attribute Profile)。您可以把它想像成一個檔案系統:

  1. Device (裝置):你的藍牙設備(如:心率帶)。
  2. Service (服務):功能的集合(如:心率測量服務 0x180d)。類似於資料夾。
  3. Characteristic (特徵):實際的資料點(如:心率數值 0x2a37)。類似於檔案,可以讀取、寫入或訂閱通知。

二、 API 連線流程

  1. Request Device:彈出瀏覽器視窗,讓使用者選擇裝置。
  2. Connect Server:連接到裝置的 GATT Server。
  3. Get Service:取得特定的服務。
  4. Get Characteristic:取得特定的特徵。
  5. Read / Write / Notify:進行資料讀寫。
javascript
// 範例:掃描支援電池服務的裝置
const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: ['battery_service'] }]
});

三、 封裝 useBluetooth Composable

這個 Composable 將處理繁瑣的連線邏輯,並提供響應式的狀態管理。

3.1 完整實作

typescript
// composables/useBluetooth.ts
import { ref, onUnmounted } from 'vue';

export function useBluetooth() {
  const isSupported = typeof navigator !== 'undefined' && 'bluetooth' in navigator;
  
  const device = ref<BluetoothDevice | null>(null);
  const server = ref<BluetoothRemoteGATTServer | null>(null);
  const isConnected = ref(false);
  const error = ref<string | null>(null);

  // 1. 請求並連接裝置
  async function requestDevice(options: RequestDeviceOptions) {
    error.value = null;
    try {
      if (!isSupported) throw new Error('您的瀏覽器不支援 Web Bluetooth');

      const selectedDevice = await navigator.bluetooth.requestDevice(options);
      device.value = selectedDevice;
      
      // 監聽斷線事件
      selectedDevice.addEventListener('gattserverdisconnected', onDisconnected);
      
      await connect();
    } catch (err: any) {
      error.value = err.message;
      console.error('Bluetooth Error:', err);
    }
  }

  // 2. 建立 GATT 連線
  async function connect() {
    if (!device.value?.gatt) return;
    
    try {
      server.value = await device.value.gatt.connect();
      isConnected.value = true;
    } catch (err: any) {
      error.value = `連線失敗: ${err.message}`;
    }
  }

  // 3. 斷開連線
  function disconnect() {
    if (device.value?.gatt?.connected) {
      device.value.gatt.disconnect();
    }
  }

  function onDisconnected() {
    isConnected.value = false;
    server.value = null;
    console.log('藍牙裝置已斷開連線');
  }

  // 4. 讀取數值 (Helper Function)
  async function readValue(serviceUUID: BluetoothServiceUUID, charUUID: BluetoothCharacteristicUUID) {
    if (!server.value) return null;
    try {
      const service = await server.value.getPrimaryService(serviceUUID);
      const characteristic = await service.getCharacteristic(charUUID);
      const value = await characteristic.readValue();
      return value;
    } catch (err: any) {
      error.value = `讀取失敗: ${err.message}`;
      return null;
    }
  }

  onUnmounted(() => {
    if (device.value) {
      device.value.removeEventListener('gattserverdisconnected', onDisconnected);
      disconnect();
    }
  });

  return {
    isSupported,
    device,
    isConnected,
    error,
    requestDevice,
    disconnect,
    readValue
  };
}

四、 實戰範例:讀取裝置電量

為了測試,我們可以使用標準的「電池服務 (Battery Service)」。其 Service UUID 為 battery_service (或 0x180f),Characteristic UUID 為 battery_level (或 0x2a19)。

vue
<script setup lang="ts">
import { ref } from 'vue';
import { useBluetooth } from '@/composables/useBluetooth';

const { 
  isSupported, 
  isConnected, 
  device, 
  error, 
  requestDevice, 
  disconnect, 
  readValue 
} = useBluetooth();

const batteryLevel = ref<number | null>(null);

async function connectBatteryDevice() {
  await requestDevice({
    filters: [{ services: ['battery_service'] }]
  });
}

async function getBatteryLevel() {
  if (!isConnected.value) return;
  
  // 讀取電池數據 (回傳的是 DataView)
  const value = await readValue('battery_service', 'battery_level');
  if (value) {
    // 電池電量通常是第一個 byte (0-100)
    batteryLevel.value = value.getUint8(0);
  }
}
</script>

<template>
  <div class="bluetooth-demo">
    <div v-if="!isSupported" class="alert error">
      您的瀏覽器不支援 Web Bluetooth API,請使用 Chrome 或 Edge。
    </div>

    <div v-else>
      <div v-if="error" class="alert error">{{ error }}</div>

      <div class="controls">
        <button v-if="!isConnected" @click="connectBatteryDevice">
          掃描並連接藍牙裝置
        </button>
        <button v-else @click="disconnect" class="danger">
          斷開連線 ({{ device?.name }})
        </button>
      </div>

      <div v-if="isConnected" class="status-panel">
        <div class="battery-display">
          <span>{{ batteryLevel !== null ? batteryLevel + '%' : '--' }}</span>
          <label>電量</label>
        </div>
        <button @click="getBatteryLevel">讀取電量</button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.alert {
  padding: 10px;
  border-radius: 4px;
  margin-bottom: 15px;
}
.error {
  background: #ffebee;
  color: #c62828;
}
.battery-display {
  font-size: 2em;
  font-weight: bold;
  text-align: center;
  margin: 20px 0;
}
.controls button {
  width: 100%;
  margin-bottom: 10px;
}
</style>

五、 進階技巧:訂閱通知 (Notifications)

對於像心率這種即時變化的數據,我們不會一直手動讀取,而是使用 startNotifications() 來訂閱變化。

typescript
// 假設已取得 characteristic
await characteristic.startNotifications();

characteristic.addEventListener('characteristicvaluechanged', (event) => {
  const value = (event.target as BluetoothRemoteGATTCharacteristic).value;
  // 解析數據... (心率資料格式較複雜,需參照 GATT 規格)
  console.log('收到新數據:', value);
});

總結

Web Bluetooth API 開啟了網頁應用的無限可能,從醫療器材、智慧家居到玩具控制都能在網頁上實現。儘管相容性仍有限制,但在可控的環境(如企業內部系統或特定硬體配套網頁)中,它提供了一個極具魅力的解決方案。


延伸閱讀