在網頁上與硬體對話: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)。您可以把它想像成一個檔案系統:
- Device (裝置):你的藍牙設備(如:心率帶)。
- Service (服務):功能的集合(如:心率測量服務
0x180d)。類似於資料夾。 - Characteristic (特徵):實際的資料點(如:心率數值
0x2a37)。類似於檔案,可以讀取、寫入或訂閱通知。
二、 API 連線流程
- Request Device:彈出瀏覽器視窗,讓使用者選擇裝置。
- Connect Server:連接到裝置的 GATT Server。
- Get Service:取得特定的服務。
- Get Characteristic:取得特定的特徵。
- 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 開啟了網頁應用的無限可能,從醫療器材、智慧家居到玩具控制都能在網頁上實現。儘管相容性仍有限制,但在可控的環境(如企業內部系統或特定硬體配套網頁)中,它提供了一個極具魅力的解決方案。