前言
戰友們,我們的通用 BLE 工具已經具備了探索、讀、寫、聽的核心能力,就像一艘裝備精良的探險船。但是,再精良的船,也必須能應對真實航程中的風浪。
在藍牙的世界裡,最大的「風浪」莫過於斷線。裝置可能因為超出範圍、電量耗盡或使用者手動關閉而隨時斷開連接。一個專業的工具,絕不能在斷線時假裝一切正常,甚至崩潰。它必須能夠感知到斷線的發生,並優雅地回應,告知使用者當前的狀態,讓工具隨時準備好下一次的連接。
此外,我們的探險日誌和 UI 上,充斥著像 0000180f-0000-1000-8000-00805f9b34fb
這樣的天書般的 UUID。這對於偵錯效率是巨大的阻礙。如果我們的工具能像一位博學的考古學家,自動將這些神秘的符號翻譯成「Battery Service」,那麼它的專業性和易用性將會得到質的提升。
今天,我們將暫停向更深層次(描述符)的探索,轉而加固我們的船體,提升工具的強健性 (Robustness) 與智慧性。
學習目標:
監聽斷線事件:學會使用 gattserverdisconnected
事件來捕捉藍牙斷線。
實現狀態管理:在斷線時,優雅地重置 UI 和內部狀態。
建立 UUID 字典:學習如何將藍牙官方指派的標準服務 UUID 映射為人類可讀的名稱。
升級 UI 工廠:改造 createServiceCard
函式,讓它能自動顯示已知的服務名稱。
內文
gattserverdisconnected
事件Web Bluetooth API 提供了一個專門的事件,讓我們可以監聽 GATT 伺服器的斷線。
事件名稱:'gattserverdisconnected'
監聽對象:BluetoothDevice
物件,也就是我們在 requestDevice
後得到的那個 device
物件。
程式碼實戰:
我們需要在成功連接到裝置之後,立刻為 device
物件綁定這個事件監聽器。
打開 app.js
,找到 scanButton.onclick
函式中 await device.gatt.connect()
的位置,在它後面加入以下程式碼:
// 在 scanButton.onclick 的 try 區塊中
// ...
log('> 成功連接到 GATT 伺服器!');
gattProfile.server = server;
statusText.textContent = `已連接至 ${device.name}`;
// ---- ↓↓↓ 今天新增的核心程式碼 (1) ↓↓↓ ----
// 為裝置綁定斷線事件監聽器
device.addEventListener('gattserverdisconnected', onDisconnected);
// ---- ↑↑↑ 今天新增的核心程式碼 (1) ↑↑↑ ----
// ... 後續的探索服務程式碼 ...
接著,在 app.js
的全域範圍(例如 log
函式下面),定義 onDisconnected
這個處理函式:
// app.js
/**
* 處理藍牙斷線事件
* @param {Event} event
*/
function onDisconnected(event) {
const device = event.target;
log(`> 裝置 "${device.name}" 已斷線。`);
// 清空 UI
gattContainer.innerHTML = '';
// 更新狀態文字
statusText.textContent = '未連線';
// (可選但推薦) 重置我們的資料模型
initGattProfile();
}
程式碼解析:
我們在連接成功後,立刻掛載了監聽器。
onDisconnected
函式負責在事件觸發時,執行所有清理工作:在日誌中通知使用者、清空舊的 GATT UI、重置狀態文字,並將我們的中央資料庫 gattProfile
初始化。這讓我們的應用程式能夠乾淨利落地回到初始狀態,準備好下一次掃描。
為了讓工具更智慧,我們需要一本「字典」,能將標準的 UUID 翻譯成名字。藍牙官方組織 (SIG) 為所有標準的服務和特徵指派了固定的 16 位元或 32 位元短 UUID。
我們可以建立一個簡單的 JavaScript 物件來充當這本字典。
在 app.js
中,新增一個 standardServices
物件:
// app.js
// --- UUID 名稱字典 ---
const standardServices = {
// 將官方 UUID(小寫)映射到名稱
"00001800-0000-1000-8000-00805f9b34fb": "Generic Access",
"00001801-0000-1000-8000-00805f9b34fb": "Generic Attribute",
"0000180f-0000-1000-8000-00805f9b34fb": "Battery Service",
"0000180d-0000-1000-8000-00805f9b34fb": "Heart Rate",
"0000180a-0000-1000-8000-00805f9b34fb": "Device Information"
// ... 你可以從藍牙官方網站找到更完整的列表並添加進來
};
/**
* 根據 UUID 獲取已知的服務名稱
* @param {string} uuid - 服務的 UUID
* @returns {string} - 已知的名稱或 null
*/
function getServiceName(uuid) {
// Web Bluetooth API 返回的 UUID 都是小寫的
return standardServices[uuid] || null;
}
createServiceCard
最後,我們來改造我們的 UI 工廠,讓它使用這本新字典。
修改 app.js
中的 createServiceCard
函式:
// app.js
// 修改 createServiceCard 函式
function createServiceCard(serviceInfo) {
const card = document.createElement('div');
card.className = 'service-card';
const serviceName = document.createElement('h3');
const serviceUuid = document.createElement('p');
// ---- ↓↓↓ 今天修改的核心邏輯 ↓↓↓ ----
const knownServiceName = getServiceName(serviceInfo.uuid);
serviceName.textContent = knownServiceName || '自訂服務 (Custom Service)';
// ---- ↑↑↑ 今天修改的核心邏輯 ↑↑↑ ----
serviceUuid.textContent = `UUID: ${serviceInfo.uuid}`;
card.appendChild(serviceName);
card.appendChild(serviceUuid);
return card;
}
同時,別忘了在 scanButton.onclick
的服務迴圈中,呼叫這個函式時傳入正確的參數:
// 在 scanButton.onclick 的 for...of 迴圈中
// const serviceCard = createServiceCard({ uuid: service.uuid, name: `服務` }); // 舊的寫法
const serviceCard = createServiceCard({ uuid: service.uuid }); // 新的、更簡潔的寫法
程式碼解析:
createServiceCard
現在會先呼叫 getServiceName
去查字典。
如果找到了對應的名稱(例如 "Battery Service"),就將其設為卡片的標題。
如果回傳 null
(代表這是一個廠商自訂的服務),就顯示一個通用的「自訂服務」標題。
這樣,我們的 UI 就同時兼顧了標準服務的可讀性和自訂服務的兼容性。
後續
今天,我們進行了一次至關重要的「強健性工程」。
我們學會了監聽 gattserverdisconnected
事件,並優雅地處理藍牙斷線,讓我們的工具在真實世界的不穩定環境中更加可靠。
我們透過建立 UUID 字典並升級 UI 工廠,極大地提升了工具的可讀性和專業性,告別了滿螢幕的天書 UUID。
明天 (Day 29),我們將執行原計畫,開始我們探險的最終章。我們將學習如何讀取比「特徵」更深一層的元數據——「描述符 (Descriptors)」,來徹底揭開裝置的每一個細節,完成我們這把「通用 BLE 瑞士刀」的最後一塊拼圖。
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。