iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Modern Web

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

Day 26:綁定事件 (3):為動態生成的「寫入」按鈕注入靈魂

  • 分享至 

  • xImage
  •  

前言

在過去兩天,我們成功地掌握了從藍牙裝置接收數據的兩種模式:一次性「拉取」的 Read 和持續性「推送」的 Subscribe。我們的應用程式已經成為一個合格的「傾聽者」,能夠理解並展示來自物理世界的訊號。

但是,一個真正強大的工具不僅要能「聽」,更要能「」。如果我們的偵錯工具只能被動接收數據,那它就失去了一半的價值。我們需要能夠主動地向裝置發送指令、設定參數、控制其行為。

今天,我們將補完與裝置核心互動的最後一塊、也是最主動的一塊拼圖——寫入 (Write)。我們將學習如何從我們的網頁,向藍牙裝置主動發送指令和數據

我們的目標是為「Write」按鈕和它旁邊的輸入框注入靈魂。使用者將能在輸入框中打字,點擊按鈕,然後這些數據將被編碼、發送,並真正地改變藍牙裝置上的狀態。這一步,將讓我們的工具從一個「觀察者」,徹底進化為一個「控制者」。

1. 發號施令的權杖: characteristic.writeValue()

這是我們工具箱裡的第三把、也是最主動的一把鑰匙,專門用來打開那些標有 writewriteWithoutResponse 屬性的寶箱,並把新的東西放進去。

  • 隸屬BluetoothGATTCharacteristic 物件的方法。

  • 非同步:它是一個非同步操作,會回傳一個 Promise。這個 Promise 會在裝置確認收到數據後完成 (fulfilled)。

  • 關鍵參數 value:它只接收一個參數 value,也就是我們要寫入的數據。但這裡有一個巨大的陷阱:這個 value 不能是普通的 JavaScript 字串或數字!

2. 外交翻譯官: 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]
    

3. 程式碼實戰:升級 Write 互動面板

現在,讓我們回到 renderCharacteristic 函式,找到生成「Write」輸入框和按鈕的區塊,為它注入真正的寫入能力。

app.jsrenderCharacteristic 函式中,找到 writeif 區塊並替換為以下內容:

// 在 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);
}

程式碼深度解析

  1. 獲取輸入:我們從 writeInput.value 獲取使用者輸入的字串。

  2. 數據編碼:在呼叫 API 之前,const valueBuffer = new TextEncoder().encode(valueString); 這一行是整個流程的關鍵,它完成了「翻譯」工作。

  3. API 呼叫:我們 await characteristic.writeValue(valueBuffer),將翻譯好的位元組數據發送出去。

  4. UI 回饋:無論成功或失敗,我們都透過改變輸入框的背景顏色,給予使用者最直接的視覺回饋。這是一個很好的使用者體驗設計。

  5. writeWithoutResponse:我們的 if 條件同時檢查了 writewriteWithoutResponse。從 JavaScript 的角度來看,呼叫的函式都是 writeValue()。瀏覽器會自動處理底層的藍牙協定差異。


後續

今天,我們完成了與 BLE 裝置核心互動的最後一環,也是最主動的一環。

至此!它具備了三大核心能力:Read (讀取)Subscribe (傾聽)Write (說話)。我們的應用程式,已經從一個觀察者和傾聽者,進化為一個可以主動下達指令的「控制者」。

我們已經掌握了與「特徵」互動的所有主要方法。但在 GATT 協議中,其實還有一個更深層次、更細微的結構——「描述符 (Descriptors)」,它通常用來提供關於特徵的元數據(例如,「這個溫度的單位是攝氏度」)。

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


上一篇
Day 25:綁定事件 (2):為「Subscribe」按鈕注入靈魂
下一篇
Day 27 強健性工程 (1):處理斷線與解析標準服務名稱
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言