iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Modern Web

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

Day 25:綁定事件 (2):為「Subscribe」按鈕注入靈魂

  • 分享至 

  • xImage
  •  

前言

戰友們,大家好!在昨天的文章結尾,我們成功地從藍牙裝置中「讀取」到了第一筆真實數據。

原本的計畫是接著介紹「寫入 (Write)」操作。但在反覆思考後,我認為將兩種接收數據的方式放在一起對比學習,能幫助我們更深刻地理解數據的流動方向。昨天我們學習的「讀取 (Read)」,是一種由我們主動發起的「拉取 (Pull)」模式,就像我們主動去信箱裡拿信。

而今天我們要學習的「訂閱通知 (Subscribe)」,則是一種被動接收的「推送 (Push)」模式。這就像快遞員在包裹送達時,主動發送一則簡訊到你手機上。我們不需要反覆檢查,而是在事件發生時,即時地收到通知。

因此,我決定稍微調整一下順序。今天,我們先來為「Subscribe」按鈕注入靈魂,學習如何傾聽來自裝置的即時數據流。這將讓我們的應用程式,從一個「詢問者」,進化為一個「傾聽者」。


1. 從「拉取」到「推送」:Notifications 的工作模式

readValue() 的一問一答不同,訂閱通知的流程分為兩大步:

  1. 開啟訂閱 (Subscribe):我們首先要告訴裝置:「嘿,我對這個特徵的數據很感興趣,從現在開始,只要它的值有變化,就請立刻通知我。」這個動作由 characteristic.startNotifications() 完成。

  2. 被動接收 (Listen):開啟訂閱後,我們不再使用 await 等待單一的回傳值。相反,我們需要在特徵上安裝一個「耳朵」(事件監聽器),靜靜地等待數據的到來。每當裝置推送新數據,這個「耳朵」就會被觸發,我們預先定義好的函式就會被執行。

2. 核心 API:startNotifications() 與事件監聽

  • characteristic.startNotifications():

    • 一個非同步方法,回傳 Promise。它的作用是告訴裝置我們準備好接收通知了。成功執行代表訂閱流程已開啟。
  • characteristic.addEventListener('characteristicvaluechanged', (event) => { ... }):

    • 今天最關鍵的 API。它為特徵新增一個事件監聽器。

    • 事件名稱是固定的字串:'characteristicvaluechanged'

    • 當裝置推送新數據時,這個事件就會被觸發,我們的回呼函式就會被執行。

  • event.target.value:

    • 在事件的回呼函式中,event 物件包含了所有相關資訊。其中 event.target 就是觸發事件的那個特徵本身,而 event.target.value 就是裝置推送過來的、包含了原始數據的 DataView 物件!
  • characteristic.stopNotifications()characteristic.removeEventListener(...):

    • 這兩個是取消訂閱的配套操作,用於關閉數據流並移除「耳朵」,節省資源。

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

我們將再次回到 renderCharacteristic 函式,找到 if (charInfo.properties.notify) 的區塊,為它打造一個具備狀態管理的完整訂閱功能。

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

// 在 renderCharacteristic 函式內部 (記得要傳入 serviceUuid 參數)
if (charInfo.properties.notify) {
    const notifyButton = document.createElement('button');
    notifyButton.textContent = 'Subscribe';

    // 用一個閉包內的變數來追蹤訂閱狀態
    let isSubscribed = false;

    // 將事件處理邏輯單獨定義成一個函式
    // 這樣我們才能在 removeEventListener 時精準地移除它
    const handleNotifications = (event) => {
      // 從事件物件中取得 DataView
      const valueDataView = event.target.value;
      // 解析數據 (此處仍以最簡單的 uint8 為例)
      const value = valueDataView.getUint8(0);
      const message = `[Notification] ${charInfo.uuid}: ${value}`;
      
      log(message); // 在我們的日誌中顯示
      valueSpan.textContent = value; // 更新特徵面板上的值
      
      // (可選) 更新中央資料庫
      gattProfile.services[serviceUuid].characteristics[charInfo.uuid].value = value;
    };

    // 為按鈕設定點擊事件
    notifyButton.onclick = async () => {
      try {
        // 從 gattProfile 中找到特徵的實例
        const characteristic = gattProfile.services[serviceUuid].characteristics[charInfo.uuid].instance;
        
        if (isSubscribed) {
          // --- 如果已經是訂閱狀態,就執行取消訂閱 ---
          await characteristic.stopNotifications();
          characteristic.removeEventListener('characteristicvaluechanged', handleNotifications);
          
          log(`> 已取消訂閱 ${charInfo.uuid}`);
          notifyButton.textContent = 'Subscribe'; // 按鈕文字變回 Subscribe
          isSubscribed = false;
        } else {
          // --- 如果尚未訂閱,就執行開始訂閱 ---
          await characteristic.startNotifications();
          characteristic.addEventListener('characteristicvaluechanged', handleNotifications);

          log(`> 已成功訂閱 ${charInfo.uuid}. 等待通知...`);
          notifyButton.textContent = 'Unsubscribe'; // 按鈕文字變為 Unsubscribe
          isSubscribed = true;
        }
      } catch (error) {
        log(`[錯誤] 操作通知失敗: ${error.message}`);
      }
    };
    actionContainer.appendChild(notifyButton);
}

程式碼深度解析

  1. isSubscribed 狀態變數:我們利用閉包的特性,讓每個 notify 按鈕都擁有自己獨立的 isSubscribed 狀態記錄。

  2. handleNotifications 獨立函式:我們將處理數據的邏輯,單獨封裝成一個函式。這樣做的好處是,addEventListenerremoveEventListener 可以準確地操作同一個函式引用,這是正確移除監聽器的標準做法。

  3. onclick 的狀態判斷:點擊按鈕時,函式會先檢查 isSubscribed 的狀態來決定是執行訂閱還是取消訂閱的流程,實現了按鈕功能的切換。

  4. UI/狀態同步:在每次操作成功後,我們都會同步更新按鈕的文字 (textContent) 和 isSubscribed 變數的值,確保 UI 顯示與程式內部狀態永遠保持一致。


總結與後續

今天,我們成功為「Subscribe」按鈕注入了生命。我們學會了如何開啟 (startNotifications) 和關閉 (stopNotifications) 通知,並透過 addEventListener 來接收即時數據流,讓我們的應用進化成了一個強大的「傾聽者」。

至此,我們已經掌握了從裝置接收數據的兩種主要方式:一次性的「Read」和持續性的「Subscribe」。

明天,我們將回歸原定計畫綁定事件 (3): 為『寫入』按鈕注入靈魂。我們將學習 characteristic.writeValue(),並真正地從我們的網頁,向藍牙裝置發送指令和數據,完成從「聽」到「說」的關鍵跨越。

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


上一篇
Day 24:綁定事件 (1):為動態生成的「讀取」按鈕注入靈魂
下一篇
Day 26:綁定事件 (3):為動態生成的「寫入」按鈕注入靈魂
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言