iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Modern Web

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

Day 24:綁定事件 (1):為動態生成的「讀取」按鈕注入靈魂

  • 分享至 

  • xImage
  •  

在過去幾天,我們完成了宏偉的探索任務。我們的應用程式已經能夠繪製出任何藍牙裝置的完整「藏寶圖」,UI 上動態生成的服務與特徵面板,就是我們辛勤工作的成果。所有的寶箱(特徵)都已陳列在我們面前,並且我們清楚地知道每一個寶箱上掛著的是「讀取鎖」、「寫入鎖」還是「通知鎖」。

但是,寶箱本身不是寶藏,箱子裡的內容才是。

今天,我們將學習如何打開這些寶箱。我們將從最直接、最常見的操作——讀取 (Read)——開始。我們要為 UI 上那些 Read 按鈕注入真正的靈魂,讓它不再只是一個在控制台自言自語的 console.log,而是一個能真正從裝置中取回數據的「開鎖匠」。

今天的目標是:點擊「Read」按鈕,透過 characteristic.readValue() 從裝置獲取原始數據,解析它,並將結果光榮地顯示在我們的網頁上。這將是我們第一次看到真實的感測器數據,從物理世界流淌進我們的應用程式!


1. 開鎖的鑰匙: characteristic.readValue()

這是我們工具箱裡的第一把鑰匙,專門用來打開那些標有 read 屬性的寶箱。

  • 隸屬BluetoothGATTCharacteristic 物件的方法。也就是我們之前探索到並儲存在 gattProfile 中的那個 characteristic 實例。

  • 非同步:它是一個非同步操作,會回傳一個 Promise。因為從裝置讀取數據需要時間。

  • 回傳值:當 Promise 成功解析時,它回傳的不是一個簡單的數字或字串,而是一個 DataView 物件。


2. 寶箱裡的原始貨物: 理解 DataView

這是新手遇到的第一個「坎」,但理解它非常重要。

  • 為什麼不是直接的數字?

    藍牙裝置之間為了效率,溝通時傳送的不是我們熟悉的文字 '100',而是最原始、最底層的二進位數據(Bytes,位元組)。一個數字 100 在記憶體中可能只佔用 1 個位元組 01100100。

  • DataView 是什麼?

    你可以把 readValue() 返回的原始數據想像成一塊「未經切割的生肉」(在 JS 中稱為 ArrayBuffer)。DataView 物件則像一把多功能的瑞士刀,它提供了一組工具(方法),讓我們可以從這塊生肉上,按照我們想要的方式,精準地切下需要的部分。

  • 常用的「刀片」 (方法)

    • dataView.getUint8(byteOffset): 將指定位置(byteOffset)的 1 個位元組,解讀為一個 8 位元無符號整數(範圍 0-255)。這是最常用的方法之一,像電池電量就常用這種格式。

    • dataView.getInt16(byteOffset, littleEndian): 將指定位置的 2 個位元組,解讀為一個 16 位元整數。

    • 還有 getFloat32getUint32 等等,用來解讀更複雜的數據。

今天,我們將專注於最簡單的 getUint8(0),即讀取第一個位元組的數值。


3. 程式碼實戰:升級 Read 按鈕

我們的戰場,依然是 Day 14 建立的 renderCharacteristic 函式。我們需要找到生成 Read 按鈕的那段 if 邏輯,並將它的 onclick 事件處理器徹底改造。

打開 app.js,修改 renderCharacteristic 函式:

首先,為了讓 onclick 函式能夠方便地找到它對應的 service UUID,我們需要稍微修改一下 renderCharacteristic 的參數。

// 修改函式簽名,增加 serviceUuid 參數
function renderCharacteristic(serviceUuid, charInfo, serviceCard) {
  // ... 函式其他部分不變 ...
}

// 同時,在昨天呼叫它的地方,也要把 service.uuid 傳進去
// for (const service of services) {
//   ...
//   for (const characteristic of characteristics) {
//     renderCharacteristic(service.uuid, { ... }, serviceCard); // 像這樣
//   }
// }

接著,找到 if (charInfo.properties.read) 區塊,用下面的全新版本替換它:

// 在 renderCharacteristic 函式內部
if (charInfo.properties.read) {
    const readButton = document.createElement('button');
    readButton.textContent = 'Read';
    
    // 將 onclick 事件處理器升級為 async 函式!
    readButton.onclick = async () => {
      log(`從特徵 ${charInfo.uuid} 讀取資料...`);
      try {
        // 步驟 1: 從我們的 gattProfile 中獲取特徵的原始實例
        const characteristic = gattProfile.services[serviceUuid].characteristics[charInfo.uuid].instance;
        if (!characteristic) {
          log('錯誤: 找不到特徵實例。');
          return;
        }

        // 步驟 2: 呼叫 readValue() API
        const valueDataView = await characteristic.readValue();
        log(`> 收到原始 DataView: ${valueDataView.byteLength} bytes`);

        // 步驟 3: 解析數據 (此處以最簡單的 uint8 為例)
        // getUint8(0) 表示讀取第 0 個 byte (也就是第一個 byte)
        const value = valueDataView.getUint8(0);
        log(`>> 解析後的數值: ${value}`);

        // 步驟 4: 更新 UI 介面
        valueSpan.textContent = value;
        valueSpan.parentElement.style.backgroundColor = '#d4edda'; // 給個成功提示的背景色

        // 步驟 5 (可選但推薦): 更新我們的中央資料庫
        gattProfile.services[serviceUuid].characteristics[charInfo.uuid].value = value;

      } catch (error) {
        log(`[錯誤] 讀取特徵失敗: ${error.message}`);
        valueSpan.parentElement.style.backgroundColor = '#f8d7da'; // 給個失敗提示的背景色
      }
    };
    actionContainer.appendChild(readButton);
  }

程式碼深度解析

  1. readButton.onclick = async () => { ... }: 因為 readValue() 是非同步的,我們的事件處理器必須async 函式,這樣才能在裡面使用 await

  2. gattProfile.services[...].characteristics[...].instance: 這是閉包強大威力的完美體現!onclick 函式被觸發時,它可以從它被創建時的「記憶」中,拿到 serviceUuidcharInfo.uuid,然後像查字典一樣,從我們的中央資料庫 gattProfile 中,精準地找到我們當初儲存的那個 characteristic 原始物件實例。只有這個實例,才有 .readValue() 方法。

  3. await characteristic.readValue(): 程式執行到這裡會暫停,等待從藍牙裝置接收到數據。接收成功後,valueDataView 就會是我們得到的 DataView 物件。

  4. UI 更新:我們不僅更新了 valueSpan 的文字,還順便改變了它父層容器的背景顏色,給使用者一個清晰的視覺回饋。


總結與後續

今天,我們為「Read」按鈕注入了真正的靈魂!
讀取操作是一次性的「拉取 (Pull)」——我們主動去要資料。但很多藍牙應用,比如心率監測或溫度計,更需要裝置在數據變化時,能持續地「推送 (Push)」給我們。這就是「訂閱通知 (Subscription)」的用武之地。

明天,我們將為「Subscribe」按鈕注入靈魂,學習如何使用 startNotifications() 來監聽來自裝置的即時數據流,讓我們的應用程式從一個「詢問者」,進化為一個「傾聽者」。

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


上一篇
Day 23: 整合與渲染:將探索結果動態生成 UI
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言