iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
Modern Web

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

Day 21 專案核心 (3):`getPrimaryServices()` 動態探索所有服務

  • 分享至 

  • xImage
  •  

昨天,我們透過 device.gatt.connect() 與裝置的 GATT 伺服器建立了穩固的連接。我們的「營地」已經紮好,「航海日誌」也忠實地記錄下了這歷史性的一刻。現在,我們站在一片未知的土地上,對眼前的一切充滿了好奇。

今天的任務,是開始真正的探索。我們要離開營地,深入這座島嶼的腹地,去發現它到底隱藏著多少個神秘的洞穴——也就是裝置所提供的各種「服務 (Services)」。一個裝置可能同時提供心率服務、電池服務和裝置資訊服務,而目前我們對此一無所知。

我們今天將要使用的關鍵工具,是 server.getPrimaryServices()。這個指令就像一副「全景地圖」,能讓我們一次性地獲取裝置上所有可用的主要服務列表。這一步,是將裝置的黑盒子徹底打開,窺探其內部功能、解開其神秘面紗的第一步。


1. 探索者的羅盤: server.getPrimaryServices()

這是我們從已連接的 GATT 伺服器 (server 物件) 出發,進行探索的第一個指令。

  • 隸屬BluetoothRemoteGATTServer 物件的方法。

  • 非同步:它是一個非同步操作,會回傳一個 Promise

  • 回傳值:當 Promise 成功解析 (fulfilled) 時,它會回傳一個陣列 (Array)。這個陣列中,包含了該裝置上所有主要服務的 BluetoothGATTService 物件。

  • getPrimaryService(uuid) 的區別

    • getPrimaryServices() (有 s):獲取所有服務,回傳一個陣列。適合我們的通用偵錯工具。

    • getPrimaryService(uuid) (沒有 s):只獲取某個特定 UUID 的服務,回傳單一一個物件。適合那些只關心特定服務的專用 App。


2. 程式碼實戰:揭示所有服務的面紗

我們將在 scanButton.onclick 函式中,緊接著成功連接 server 之後,加入這段新的探索邏輯。

打開 app.js,找到 scanButton.onclick 函式,並在 await device.gatt.connect() 成功之後,加入以下程式碼:

// app.js -> scanButton.onclick

// ... (省略前面的掃描和連接程式碼)
try {
  // ...
  log('> 成功連接到 GATT 伺服器!');
  gattProfile.server = server;
  statusText.textContent = `已連接至 ${device.name}`;

  // ---- ↓↓↓ 今天新增的核心程式碼 ↓↓↓ ----

  log('正在探索服務...');
  const services = await server.getPrimaryServices();
  log(`> 發現 ${services.length} 個主要服務!`);

  // 使用 for...of 迴圈來處理每一個發現的服務
  for (const service of services) {
    log(`>> 服務 UUID: ${service.uuid}`);

    // 步驟 1: 將服務資訊存入我們的資料模型 gattProfile
    gattProfile.services[service.uuid] = {
      uuid: service.uuid,
      instance: service,      // 保存原始的 service 物件實例,供後續使用
      characteristics: {}   // 準備一個空物件,用來存放之後探索到的特徵
    };
  }

  // TODO: 明天的任務 -> 探索每個服務下的特徵...

  // ---- ↑↑↑ 今天新增的核心程式碼 ↑↑↑ ----

} catch(error) {
  log(`錯誤: ${error.message}`);
}

程式碼解析

  1. const services = await server.getPrimaryServices();

    • 我們呼叫 API 並 await 其結果。執行完畢後,services 變數將會是一個包含 BluetoothGATTService 物件的陣列。
  2. for (const service of services)

    • 我們使用 Day 5 學過的 for...of 迴圈,這是遍歷陣列最優雅的方式。在迴圈的每一次迭代中,service 變數都代表著陣列中的一個 BluetoothGATTService 物件。
  3. gattProfile.services[service.uuid] = { ... }

    • 這是數據驅動的核心體現!我們將從真實世界獲取到的 service.uuid 作為「鍵」,將服務的詳細資訊存入我們在 Day 7 設計好的 gattProfile.services 物件中。

    • 我們不僅儲存了 uuid,還將 service 物件的原始實例 (instance) 也存了起來,因為我們明天就需要用它來探索特徵。


3. 連接真實數據與動態 UI

光是把數據存在變數裡還不夠,我們要讓使用者看見我們的探索成果!現在,我們將呼叫 Day 13 建立的 createServiceCard UI 工廠函式。

繼續修改 for...of 迴圈,加入 UI 生成的程式碼:

// app.js -> for...of 迴圈內部

for (const service of services) {
  log(`>> 服務 UUID: ${service.uuid}`);

  // 步驟 1: 更新資料模型 (同上)
  gattProfile.services[service.uuid] = {
    uuid: service.uuid,
    instance: service,
    characteristics: {}
  };

  // 步驟 2: 使用 UI 工廠函式,為這個服務動態生成一個 UI 卡片
  // 注意:由於標準服務 UUID 通常有關聯的名稱,但自定義 UUID 沒有,
  // 我們需要一個輔助函式 (或在 createServiceCard 中處理) 來顯示已知的服務名稱。
  // 為簡化起見,我們先直接使用 UUID。
  const serviceCard = createServiceCard({
    uuid: service.uuid,
    name: `服務 (Service)` // 暫時使用通用名稱
  });

  // 步驟 3: 將新創建的卡片附加到主容器中,讓使用者看到
  gattContainer.appendChild(serviceCard);

  // 步驟 4 (可選但推薦): 在資料模型中保存對 UI 元素的引用
  // 這樣未來我們可以輕易地找到這張卡片,並把特徵 UI 附加進去
  gattProfile.services[service.uuid].uiCard = serviceCard;
}

現在,當你連接到一個裝置(例如你用手機 App 創建的那個包含多個服務的虛擬裝置)時,你會看到網頁上動態地生成了對應數量的服務卡片!每一個卡片都顯示了它獨一無二的 UUID。

我們的應用程式,不再只是一個連接器,它現在是一個真正的探索者


總結與後續

今天我們已經發現了島上的所有洞穴 (Services),並在地圖上標記了它們的位置。下一步,就是深入每一個洞穴,去尋找裡面真正的寶藏 (Characteristics)。

明天,我們將學習如何針對每一個已發現的服務,使用 service.getCharacteristics() 來獲取其下所有的特徵。我們離讀取到真實的感測器數據,僅剩下最後一步之遙。

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


上一篇
Day 20:建立連結:連接 GATT 伺服器與日誌記錄
下一篇
Day 22:專案核心 (4):`getCharacteristics()` 動態探索所有特徵
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言