iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Modern Web

Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具系列 第 27

Day 27 強健性工程 (1):處理斷線與解析標準服務名稱

  • 分享至 

  • xImage
  •  

前言

戰友們,我們的通用 BLE 工具已經具備了探索、讀、寫、聽的核心能力,就像一艘裝備精良的探險船。但是,再精良的船,也必須能應對真實航程中的風浪

在藍牙的世界裡,最大的「風浪」莫過於斷線。裝置可能因為超出範圍、電量耗盡或使用者手動關閉而隨時斷開連接。一個專業的工具,絕不能在斷線時假裝一切正常,甚至崩潰。它必須能夠感知到斷線的發生,並優雅地回應,告知使用者當前的狀態,讓工具隨時準備好下一次的連接。

此外,我們的探險日誌和 UI 上,充斥著像 0000180f-0000-1000-8000-00805f9b34fb 這樣的天書般的 UUID。這對於偵錯效率是巨大的阻礙。如果我們的工具能像一位博學的考古學家,自動將這些神秘的符號翻譯成「Battery Service」,那麼它的專業性和易用性將會得到質的提升。

今天,我們將暫停向更深層次(描述符)的探索,轉而加固我們的船體,提升工具的強健性 (Robustness)智慧性

學習目標:

  1. 監聽斷線事件:學會使用 gattserverdisconnected 事件來捕捉藍牙斷線。

  2. 實現狀態管理:在斷線時,優雅地重置 UI 和內部狀態。

  3. 建立 UUID 字典:學習如何將藍牙官方指派的標準服務 UUID 映射為人類可讀的名稱。

  4. 升級 UI 工廠:改造 createServiceCard 函式,讓它能自動顯示已知的服務名稱。


內文

1. 加固船體:監聽 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 初始化。這讓我們的應用程式能夠乾淨利落地回到初始狀態,準備好下一次掃描。

2. 智慧化升級:解析標準服務 UUID

為了讓工具更智慧,我們需要一本「字典」,能將標準的 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;
}

3. 升級 UI 工廠: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 瑞士刀」的最後一塊拼圖。

那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。


上一篇
Day 26:綁定事件 (3):為動態生成的「寫入」按鈕注入靈魂
下一篇
Day 28:深入探索與強健性:讀取描述符與手動斷線
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言