iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Modern Web

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

Day 20:建立連結:連接 GATT 伺服器與日誌記錄

  • 分享至 

  • xImage
  •  

昨天,我們取得了歷史性的突破。透過 navigator.bluetooth.requestDevice(),我們的網頁第一次「看見」了真實世界中的藍牙裝置,並成功捕獲了代表它的 device 物件。我們找到了傳說中的寶藏島,正站在船頭,滿懷期待地眺望著海岸。

但發現島嶼只是第一步。要尋寶,我們必須踏上陸地,建立一個穩固的營地,作為所有探索行動的基地。在 Web Bluetooth 的世界裡,這個「登陸」的動作,就是連接到裝置的 GATT 伺服器。GATT 伺服器是裝置上所有資料(服務與特徵)的管理者,建立與它的連接,是我們後續所有讀寫操作的堅實基礎。

同時,在我們探索未知島嶼的過程中,詳細記錄每一步的足跡至關重要。「我們在什麼時候發現了島嶼?」「什麼時候成功登陸?」「探索過程中發生了什麼錯誤?」為了讓我們的工具更加專業,今天,我們不僅要學會如何連接,還將親手打造一個專屬的「航海日誌」系統,將每一步操作都清晰地顯示在介面上。

準備好,讓我們正式登陸!


1. device.gatt.connect() - 登陸島嶼的指令

我們昨天獲取的 device 物件,本身還不能用來讀寫資料。它身上有一個非常重要的屬性叫做 gatt,這就是通往裝置內部資料世界的大門。而 device.gatt.connect() 就是打開這扇大門的鑰匙。

  • device.gattBluetoothDevice 物件的一個屬性,它回傳一個 BluetoothRemoteGATT 物件,提供了連接到 GATT 伺服器的功能。

  • .connect()device.gatt 物件的方法。

    • 非同步:它是一個非同步操作,會立即回傳一個 Promise

    • 回傳值:當 Promise 成功解析 (fulfilled) 時,它會回傳一個 BluetoothRemoteGATTServer 物件。這個物件就代表了我們與裝置 GATT 伺服器的有效連接,是我們後續探索服務的起點。

    • 失敗:如果連接失敗(例如裝置突然關機或超出範圍),Promise 將會被拒絕 (rejected),我們的 try...catch 區塊會捕捉到這個錯誤。


2. 打造我們的「航海日誌」- The Log System

在修改連接邏輯之前,我們先把日誌系統建立好。一個好的日誌,能讓開發者(也就是我們自己)清楚地看到每一步發生了什麼。

步驟一:在 HTML 中開闢日誌區域

打開 index.html,在 <div id="gattContainer"> 的下方,加入日誌的區塊。

index.html

    </div>

    <hr> <h3>日誌記錄 (Log)</h3>
    <div id="logContainer">
      </div>

    <script src="app.js"></script>
  </body>
</html>

步驟二:用 CSS 美化日誌外觀

打開 style.css,在檔案末尾加入新的樣式,讓它看起來像一個專業的日誌面板。

style.css

/* --- 日誌容器樣式 --- */
#logContainer {
  background-color: #2c3e50; /* 深色背景 */
  color: #ecf0f1;           /* 淺色文字 */
  font-family: 'Courier New', Courier, monospace; /* 等寬字體 */
  font-size: 14px;
  padding: 15px;
  height: 250px;             /* 固定高度 */
  overflow-y: scroll;        /* 當內容超出高度時,顯示垂直滾動條 */
  border-radius: 5px;
  border: 1px solid #34495e;
}

#logContainer p {
  margin: 0 0 5px 0;      /* 每條日誌之間的間距 */
  word-break: break-all;  /* 長字串自動換行 */
}

步驟三:在 JavaScript 中創建 log 函式

這是最核心的一步。我們將創建一個萬用的 log 函式,它會同時在開發者控制台和我們的 UI 上輸出訊息。

打開 app.js,在檔案的最上方,選取我們新的 DOM 元素,並定義 log 函式。

app.js

// --- [1] 選取核心 DOM 元素 ---
const scanButton = document.getElementById('scanButton');
const statusText = document.getElementById('statusText');
const gattContainer = document.getElementById('gattContainer');
const logContainer = document.getElementById('logContainer'); // 新增!

// --- [2] 日誌記錄函式 ---
/**
 * 在 UI 和 Console 中記錄訊息
 * @param {string} message - 要記錄的訊息
 */
function log(message) {
  const now = new Date();
  const timeString = now.toLocaleTimeString(); // 取得 HH:MM:SS 格式的時間

  // 1. 在開發者控制台印出
  console.log(message);

  // 2. 在 UI 上創建一個新的 p 元素來顯示日誌
  const logEntry = document.createElement('p');
  logEntry.innerHTML = `<strong>[${timeString}]</strong> ${message}`;

  // 3. 將新的日誌項目附加到日誌容器中
  logContainer.appendChild(logEntry);

  // 4. 自動將滾動條滾動到最底部,以便永遠顯示最新的日誌
  logContainer.scrollTop = logContainer.scrollHeight;
}


// --- [3] GATT Profile 資料結構 ---
// ... (gattProfile 物件) ...

// ... (UI 生成函式) ...
  • logContainer.scrollTop = logContainer.scrollHeight; 是一個非常實用的小技巧,它能確保每次新增日誌後,滾動條都會自動滾動到底部,讓我們總能看到最新的訊息。

3. 整合!記錄我們的掃描與連接流程

萬事俱備!現在我們回到 scanButton.onclick 函式,用我們全新的 log 函式來記錄流程,並加入 device.gatt.connect() 的呼叫。

修改 app.js 中的 scanButton.onclick 函式:

// app.js

scanButton.onclick = async () => {
  // 清空舊的日誌和 GATT 資訊
  gattContainer.innerHTML = '';
  logContainer.innerHTML = '';

  if (!navigator.bluetooth) {
    log('錯誤: 您的瀏覽器不支援 Web Bluetooth API。');
    return;
  }

  try {
    log('正在請求藍牙裝置...');
    const device = await navigator.bluetooth.requestDevice({
      acceptAllDevices: true
    });
    
    log(`> 已選擇裝置: ${device.name || `ID: ${device.id}`}`);
    gattProfile.device = device;

    // ---- ↓↓↓ 今天新增的核心程式碼 ↓↓↓ ----
    
    log('正在連接到 GATT 伺服器...');
    const server = await device.gatt.connect();
    
    log('> 成功連接到 GATT 伺服器!');
    gattProfile.server = server;

    statusText.textContent = `已連接至 ${device.name}`;

    // TODO: 明天的任務 -> 探索這個伺服器上的服務...

    // ---- ↑↑↑ 今天新增的核心程式碼 ↑↑↑ ----
    
  } catch(error) {
    log(`錯誤: ${error.message}`);
  }
};
  • 我們將 console.log 和一部分 statusText.textContent = ... 都替換成了我們的 log() 函式,讓日誌記錄更統一、更強大。

  • 我們在 await device.gatt.connect() 前後都加入了日誌,這樣使用者就能清楚地看到程式正在進行哪一步。

現在,重新整理頁面並點擊掃描按鈕。選擇一個裝置後,你將會在 UI 的日誌區看到類似這樣的輸出:

[17:05:10] 正在請求藍牙裝置...
[17:05:15] > 已選擇裝置: My ESP32
[17:05:15] 正在連接到 GATT 伺服器...
[17:05:16] > 成功連接到 GATT 伺服器!

總結與後續

今天,我們成功地從「發現」邁向了「連接」!
我們已經成功登陸了寶藏島,並在海灘上建立了一個堅固的營地(GATT Server 連接)。我們的航海日誌(Log System)也已準備就見證著我們的偉大航程。
營地已經紮好,下一步就是拿出藏寶圖,開始真正的探索了。明天,我們將從 server 物件出發,學習如何使用 server.getPrimaryService()server.getPrimaryServices() 來發現這個裝置到底提供了哪些「服務」(藏寶圖上的山洞)。我們將第一次窺見裝置內部的秘密。

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


上一篇
Day 19:萬物皆可連:acceptAllDevices 的力量與責任
下一篇
Day 21 專案核心 (3):`getPrimaryServices()` 動態探索所有服務
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言