前言
在過去兩天,我們成功地掌握了從藍牙裝置接收數據的兩種模式:一次性「拉取」的 Read
和持續性「推送」的 Subscribe
。我們的應用程式已經成為一個合格的「傾聽者」,能夠理解並展示來自物理世界的訊號。
但是,一個真正強大的工具不僅要能「聽」,更要能「說」。如果我們的偵錯工具只能被動接收數據,那它就失去了一半的價值。我們需要能夠主動地向裝置發送指令、設定參數、控制其行為。
今天,我們將補完與裝置核心互動的最後一塊、也是最主動的一塊拼圖——寫入 (Write)。我們將學習如何從我們的網頁,向藍牙裝置主動發送指令和數據。
我們的目標是為「Write」按鈕和它旁邊的輸入框注入靈魂。使用者將能在輸入框中打字,點擊按鈕,然後這些數據將被編碼、發送,並真正地改變藍牙裝置上的狀態。這一步,將讓我們的工具從一個「觀察者」,徹底進化為一個「控制者」。
characteristic.writeValue()
這是我們工具箱裡的第三把、也是最主動的一把鑰匙,專門用來打開那些標有 write
或 writeWithoutResponse
屬性的寶箱,並把新的東西放進去。
隸屬:BluetoothGATTCharacteristic
物件的方法。
非同步:它是一個非同步操作,會回傳一個 Promise
。這個 Promise 會在裝置確認收到數據後完成 (fulfilled)。
關鍵參數 value
:它只接收一個參數 value
,也就是我們要寫入的數據。但這裡有一個巨大的陷阱:這個 value
不能是普通的 JavaScript 字串或數字!
TextEncoder
與數據編碼藍牙裝置的溝通,使用的是最低階、最高效的位元組 (Bytes)。JavaScript 中我們常用的字串 Hello
,對於藍牙裝置來說是無法理解的「外星語」。它只認得像 [72, 101, 108, 108, 111]
這樣由數字 0-255 組成的位元組序列(這串數字其實是 "H-e-l-l-o" 的 ASCII/UTF-8 編碼)。
writeValue()
方法要求我們傳入的 value
必須是 ArrayBuffer
或其「視圖」(如 Uint8Array
)。這就需要一個「外交翻譯官」,將我們人類易讀的字串,翻譯成機器能懂的位元組。
這個翻譯官,就是現代瀏覽器內建的 TextEncoder
API。
功能:它可以將 JavaScript 字串,按照 UTF-8 編碼規則,轉換成 Uint8Array
(8 位元無符號整數陣列),這正是 writeValue()
完美接受的格式。
使用方法:
// 1. 建立一個編碼器實例
const encoder = new TextEncoder();
// 2. 使用 .encode() 方法進行編碼
const dataToSend = encoder.encode('Hello, BLE!');
console.log(dataToSend); // 輸出: Uint8Array(11) [72, 101, 108, 108, 111, 44, 32, 66, 76, 69, 33]
Write
互動面板現在,讓我們回到 renderCharacteristic
函式,找到生成「Write」輸入框和按鈕的區塊,為它注入真正的寫入能力。
在 app.js
的 renderCharacteristic
函式中,找到 write
的 if
區塊並替換為以下內容:
// 在 renderCharacteristic 函式內部 (記得要傳入 serviceUuid 參數)
if (charInfo.properties.write || charInfo.properties.writeWithoutResponse) {
const writeInput = document.createElement('input');
writeInput.type = 'text';
writeInput.placeholder = '輸入要寫入的字串';
const writeButton = document.createElement('button');
writeButton.textContent = 'Write';
// 將 onclick 事件處理器升級為 async 函式
writeButton.onclick = async () => {
// 步驟 1: 從輸入框獲取使用者輸入的字串
const valueString = writeInput.value;
if (!valueString && valueString !== "") { // 允許寫入空字串
alert('請輸入要寫入的數值。');
return;
}
log(`準備向特徵 ${charInfo.uuid} 寫入資料...`);
try {
// 步驟 2: 從 gattProfile 找到特徵的實例
const characteristic = gattProfile.services[serviceUuid].characteristics[charInfo.uuid].instance;
// 步驟 3: 使用 TextEncoder 將字串編碼成 Uint8Array
const encoder = new TextEncoder();
const valueBuffer = encoder.encode(valueString);
log(`> 編碼 "${valueString}" 為 ${valueBuffer.byteLength} bytes: [${valueBuffer.join(',')}]`);
// 步驟 4: 呼叫 writeValue() API,傳入編碼後的 buffer
await characteristic.writeValue(valueBuffer);
log('>> 寫入操作成功!');
writeInput.style.backgroundColor = '#d4edda'; // 成功時,輸入框變綠
} catch (error) {
log(`[錯誤] 寫入失敗: ${error.message}`);
writeInput.style.backgroundColor = '#f8d7da'; // 失敗時,輸入框變紅
}
};
actionContainer.appendChild(writeInput);
actionContainer.appendChild(writeButton);
}
程式碼深度解析
獲取輸入:我們從 writeInput.value
獲取使用者輸入的字串。
數據編碼:在呼叫 API 之前,const valueBuffer = new TextEncoder().encode(valueString);
這一行是整個流程的關鍵,它完成了「翻譯」工作。
API 呼叫:我們 await characteristic.writeValue(valueBuffer)
,將翻譯好的位元組數據發送出去。
UI 回饋:無論成功或失敗,我們都透過改變輸入框的背景顏色,給予使用者最直接的視覺回饋。這是一個很好的使用者體驗設計。
writeWithoutResponse
:我們的 if
條件同時檢查了 write
和 writeWithoutResponse
。從 JavaScript 的角度來看,呼叫的函式都是 writeValue()
。瀏覽器會自動處理底層的藍牙協定差異。
後續
今天,我們完成了與 BLE 裝置核心互動的最後一環,也是最主動的一環。
至此!它具備了三大核心能力:Read (讀取)、Subscribe (傾聽) 和 Write (說話)。我們的應用程式,已經從一個觀察者和傾聽者,進化為一個可以主動下達指令的「控制者」。
我們已經掌握了與「特徵」互動的所有主要方法。但在 GATT 協議中,其實還有一個更深層次、更細微的結構——「描述符 (Descriptors)」,它通常用來提供關於特徵的元數據(例如,「這個溫度的單位是攝氏度」)。
那麼今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。