昨天,我們透過 device.gatt.connect()
與裝置的 GATT 伺服器建立了穩固的連接。我們的「營地」已經紮好,「航海日誌」也忠實地記錄下了這歷史性的一刻。現在,我們站在一片未知的土地上,對眼前的一切充滿了好奇。
今天的任務,是開始真正的探索。我們要離開營地,深入這座島嶼的腹地,去發現它到底隱藏著多少個神秘的洞穴——也就是裝置所提供的各種「服務 (Services)」。一個裝置可能同時提供心率服務、電池服務和裝置資訊服務,而目前我們對此一無所知。
我們今天將要使用的關鍵工具,是 server.getPrimaryServices()
。這個指令就像一副「全景地圖」,能讓我們一次性地獲取裝置上所有可用的主要服務列表。這一步,是將裝置的黑盒子徹底打開,窺探其內部功能、解開其神秘面紗的第一步。
server.getPrimaryServices()
這是我們從已連接的 GATT 伺服器 (server
物件) 出發,進行探索的第一個指令。
隸屬:BluetoothRemoteGATTServer
物件的方法。
非同步:它是一個非同步操作,會回傳一個 Promise
。
回傳值:當 Promise 成功解析 (fulfilled) 時,它會回傳一個陣列 (Array)。這個陣列中,包含了該裝置上所有主要服務的 BluetoothGATTService
物件。
與 getPrimaryService(uuid)
的區別:
getPrimaryServices()
(有 s):獲取所有服務,回傳一個陣列。適合我們的通用偵錯工具。
getPrimaryService(uuid)
(沒有 s):只獲取某個特定 UUID 的服務,回傳單一一個物件。適合那些只關心特定服務的專用 App。
我們將在 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}`);
}
const services = await server.getPrimaryServices();
await
其結果。執行完畢後,services
變數將會是一個包含 BluetoothGATTService
物件的陣列。for (const service of services)
for...of
迴圈,這是遍歷陣列最優雅的方式。在迴圈的每一次迭代中,service
變數都代表著陣列中的一個 BluetoothGATTService
物件。gattProfile.services[service.uuid] = { ... }
這是數據驅動的核心體現!我們將從真實世界獲取到的 service.uuid
作為「鍵」,將服務的詳細資訊存入我們在 Day 7 設計好的 gattProfile.services
物件中。
我們不僅儲存了 uuid
,還將 service
物件的原始實例 (instance) 也存了起來,因為我們明天就需要用它來探索特徵。
光是把數據存在變數裡還不夠,我們要讓使用者看見我們的探索成果!現在,我們將呼叫 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()
來獲取其下所有的特徵。我們離讀取到真實的感測器數據,僅剩下最後一步之遙。
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。