前言
昨天,我們為工具安裝了「被動安全系統」——當意外(藍牙斷線)來襲時,我們的應用程式能夠自動感知並優雅地回到港口(初始狀態)。
今天,我們將為這艘船安裝「主動控制系統」,並對島嶼進行最後、最精細的勘探。
首先,我們將賦予使用者主動結束的權力。一個專業的工具,不能只有開始,沒有結束。我們將新增一個「中斷連線」按鈕,讓使用者可以隨時、主動、乾淨地切斷藍牙連接,這將使我們的工具在可用性和強健性上,再上一個台階。
接著,我們將拿出探險家的「顯微鏡」,深入 GATT 結構的最後一層、也是最精細的一層——「描述符 (Descriptors)」。如果說「特徵」是寶箱,那麼「描述符」就是貼在寶箱上的標籤和說明書,它記錄了關於這個寶箱的元數據,例如「寶箱內物品的單位」或「開啟寶箱通知功能的開關」。探索它,將讓我們對裝置的理解達到極致。
內文
一個完整的互動流程,必須有開始,也要有結束。
步驟一:在 HTML 中新增按鈕
打開 index.html
,在「掃描」按鈕旁邊,加入一個預設為隱藏的「中斷連線」按鈕。
index.html
//index.html
<div class="control-section">
<button id="scanButton">掃描並連接藍牙裝置</button>
<button id="disconnectButton" class="hidden">中斷連線</button>
</div>
步驟二:用 CSS 定義樣式
打開 style.css
,為新按鈕添加樣式,並定義 .hidden
class。
//style.css
#disconnectButton {
background-color: #dc3545; /* 紅色,表示危險或結束操作 */
/* 其他樣式可以參考 scanButton */
color: white;
border: none;
border-radius: 5px;
padding: 10px 15px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
#disconnectButton:hover {
background-color: #c82333;
}
.hidden {
display: none; /* 讓元素完全從頁面上消失 */
}
步驟三:在 JavaScript 中注入靈魂
這是最核心的部分。我們需要在連接成功後,顯示「中斷連線」按鈕;在斷線後,再把它隱藏起來。
打開 app.js
// app.js
// [1] 在最上方選取 DOM 元素
const disconnectButton = document.getElementById('disconnectButton');
// [2] 在 onDisconnected 函式中,加入重置按鈕狀態的邏輯
function onDisconnected(event) {
// ... 其他清理程式碼 ...
// 顯示掃描按鈕,隱藏中斷連線按鈕
scanButton.classList.remove('hidden');
disconnectButton.classList.add('hidden');
}
// [3] 在全域新增 disconnectButton 的點擊事件
disconnectButton.onclick = () => {
if (gattProfile.device && gattProfile.device.gatt.connected) {
log('正在手動中斷連線...');
gattProfile.device.gatt.disconnect();
} else {
log('沒有已連接的裝置。');
}
};
// [4] 在 scanButton.onclick 的 try 區塊中,連接成功後,更新按鈕狀態
// ...
log('> 成功連接到 GATT 伺服器!');
// ...
// 顯示中斷連線按鈕,隱藏掃描按鈕
scanButton.classList.add('hidden');
disconnectButton.classList.remove('hidden');
device.addEventListener('gattserverdisconnected', onDisconnected);
// ...
程式碼解析:
狀態切換:scanButton
和 disconnectButton
就像蹺蹺板,一個顯示時另一個就隱藏。我們在「連接成功」和「斷線時」這兩個關鍵節點,更新它們的 .hidden
class 來實現這個效果。
主動斷線:disconnectButton.onclick
呼叫了 gattProfile.device.gatt.disconnect()
。這會主動觸發藍牙斷線。
架構的優雅:我們不需要在 disconnectButton.onclick
裡手動清理 UI!因為 disconnect()
會觸發我們昨天寫好的 gattserverdisconnected
事件,從而自動呼叫 onDisconnected
函式,完成所有的清理工作。這證明了我們事件驅動的架構是多麼穩健!
描述符是 GATT 結構中最精細的單位,它為特徵提供上下文。
characteristic.getDescriptors()
:
Promise
,解析後得到一個包含所有 BluetoothGATTDescriptor
物件的陣列。descriptor.readValue()
:
characteristic.readValue()
類似,用於讀取描述符的值,同樣回傳一個包含 DataView
的 Promise
。程式碼實戰:
我們將在探索「特徵」的迴圈內部,再嵌套一層迴圈來探索「描述符」。
在 app.js
的 scanButton.onclick
函式中,找到遍歷特徵的 for...of
迴圈,並在其中加入以下程式碼:
// 在遍歷 characteristics 的 for...of 迴圈內部
for (const characteristic of characteristics) {
// ... 之前處理特徵和渲染特徵面板的程式碼 ...
renderCharacteristic(service.uuid, { /*...*/ }, serviceCard);
// ---- ↓↓↓ 今天新增的核心程式碼 (2) ↓↓↓ ----
try {
log(`------> 正在探索描述符...`);
const descriptors = await characteristic.getDescriptors();
for (const descriptor of descriptors) {
log(`---------> 描述符 UUID: ${descriptor.uuid}`);
// 讀取描述符的值
const valueDataView = await descriptor.readValue();
// 我們使用 Day 28 的 parseValue 函式來解析
const parsedValue = parseValue(valueDataView);
log(`---------> 描述符值: ${parsedValue}`);
// TODO: (可選) 創建一個 renderDescriptor 的 UI 函式並呼叫它
}
} catch (error) {
log(`------> 探索描述符失敗: ${error.message}`);
}
// ---- ↑↑↑ 今天新增的核心程式碼 (2) ↑↑↑ ----
}
程式碼解析:
我們在 characteristic
迴圈內,呼叫了 getDescriptors()
。
接著遍歷返回的 descriptors
陣列。
對於每一個 descriptor
,我們呼叫 readValue()
並用我們之前寫好的 parseValue
函式來解析和記錄它的值。
現在,當你連接到一個裝置(例如包含標準心率服務的虛擬裝置),你將會在日誌中看到,它不僅探索到了服務和特徵,還進一步探索到了 Heart Rate Measurement
特徵下的 Client Characteristic Configuration
描述符 (2902
)!
後續
今天,我們WEB工具的所有核心功能都已打造完畢。它已經是一個可靠、功能完備的實用工具。我們幾乎掌握了 Web Bluetooth API 中所有關於探索和互動的關鍵部分。
在我們為這趟精彩的旅程畫上句點之前,還有最後一個錦上添花的重要主題值得我們探討:效能與最佳化。一個好的工具不僅要功能完整,還應該運行流暢、反應迅速,尤其是在處理大量數據流時。
明天,我們將為這次鐵人賽衝刺畫上一個完美的句號。我們將回顧整個專案,並專注於程式碼的最佳化與重構。我們將探討如何讓日誌系統更有效率、如何避免不必要的 DOM 操作,以及如何讓我們的程式碼架構更清晰、更易於未來的擴展。這將是從「能用」到「好用」的最後一步昇華。
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。