iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
Modern Web

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

Day7:萬物皆物件:設計儲存 GATT Profile 的資料結構

  • 分享至 

  • xImage
  •  

昨天,我們學會了如何像建築師一樣,使用函式、作用域和閉包來建構程式碼的「功能模組」。我們現在擁有的,是有組織、有紀律的工具箱。

然而,光有工具還不夠。在建造一棟大樓之前,建築師最需要的是什麼?是一張精準、詳細的建築藍圖。這張藍圖定義了大樓的結構、有多少房間、每個房間的功能是什麼。如果沒有藍圖就隨意施工,最後蓋出來的肯定是一棟混亂的危樓。

在我們的專案中,從藍牙裝置讀取到的所有資訊——裝置名稱、提供的所有服務、每項服務下的所有特徵,以及特徵的屬性——就是我們建築的「鋼筋水泥」。今天,我們的任務就是設計一張清晰的「資料藍圖」,學習如何用 JavaScript 中最強大的結構——物件 (Object)——來規劃和儲存所有這些複雜的資訊。

今天過後,你將不再只是看到零散的資料,而是能建立一個有系統、有階層的「GATT Profile 資料庫」。這個資料庫將是我們整個應用程式的「大腦」和「心臟」,未來所有的介面顯示、資料讀寫,都將圍繞著它來運作!

1. 再次深入:JavaScript 物件 (Object) 的威力

我們之前提過,物件 {} 就像一個收納盒。今天,我們要把它玩得更透徹。

核心概念:物件,是由一對對的「鍵 (Key)」和「值 (Value)」所組成的集合。你可以把它想像成一本「字典」,每個「鍵」就是一個詞彙(必須是字串),每個「值」就是這個詞彙的解釋(可以是任何東西)。

// 建立一個描述「人」的物件
const person = {
  // 鍵 (key)   :  值 (value)
  "name"        : "小明",
  "age"         : 18,
  "isStudent"   : true,
  "hobbies"     : ["打籃球", "寫程式", "看電影"], // 值可以是陣列
  "address"     : {                           // 值可以是另一個物件!
    "city"    : "台北市",
    "district": "信義區"
  },
  "sayHello"    : function() {                  // 值也可以是函式!
    console.log("大家好,我是 " + this.name);
  }
};

**如何存取物件裡的資料?

有兩種方式可以從物件這個「字典」裡查資料:

A. 點記法 (Dot Notation):最常用、最直觀。

console.log( person.name );      // 輸出: "小明"
console.log( person.address.city ); // 可以連鎖查詢: 取得 address 物件裡的 city

B. 括號記法 (Bracket Notation):當「鍵」含有特殊字元或儲存在變數裡時使用。

console.log( person['age'] );    // 輸出: 18

// 假設你想查詢的鍵,儲存在一個變數裡
let myKey = 'isStudent';
console.log( person[myKey] );    // 輸出: true (這件事用點記法 person.myKey 是做不到的!)

什麼是「方法 (Method)」?

當物件裡的值是一個函式時,我們給它一個特殊的名字,叫做「方法」。它代表這個物件所擁有的「行為」。

// 呼叫 person 物件的 sayHello 方法
person.sayHello(); // 輸出: "大家好,我是 小明"
  • this 關鍵字:在 sayHello 函式中,this 代表呼叫這個方法的物件本身,也就是 person。所以 this.name 就等同於 person.name

2. 繪製藍圖:將 GATT 結構映射為 JS 物件

好了,理論講完了,來點實際的。我們要怎麼用物件來描述一個藍牙裝置的結構呢?

回憶一下 GATT 的階層: 一個裝置 (Device) -> 包含多個服務 (Service) -> 每個服務包含多個特徵 (Characteristic)

我們的藍圖就要完全仿照這個結構!

第一層:裝置 (Device) 物件

這是我們整個資料結構的最外層,代表連上的那個藍牙裝置。

const gattProfile = {
 // 存放裝置的基本資訊
 device: {
   name: null, // 裝置名稱,例如 "My ESP32" (初始為 null)
   id: null    // 裝置ID (初始為 null)
 },
 // 準備一個收納盒,專門用來放所有的「服務」
 services: {} // 目前是空的
};

第二層:服務 (Services) 物件

services 本身也是一個物件。我們將用服務的 UUID 當作「鍵」,對應的服務物件當作「值」。

為什麼不用陣列 [] 因為用 UUID 當鍵,未來查詢特定服務會超級快gattProfile.services['UUID'] 一下就能找到,而不用跑 for 迴圈在陣列裡慢慢找。

// 假設我們發現了一個「電池服務」
gattProfile.services['0000180f-0000-1000-8000-00805f9b34fb'] = {
  // 這是服務物件的內容
  uuid: '0000180f-0000-1000-8000-00805f9b34fb',
  characteristics: {} // 準備一個收納盒,放這個服務底下的所有「特徵」
};

第三層:特徵 (Characteristics) 物件

跟服務一樣,characteristics 也用特徵的 UUID 當作「鍵」。

// 在「電池服務」底下,我們發現了「電量等級特徵」
const serviceUUID = '0000180f-0000-1000-8000-00805f9b34fb';
const charUUID = '00002a19-0000-1000-8000-00805f9b34fb';

gattProfile.services[serviceUUID].characteristics[charUUID] = {
  // 這是特徵物件的內容
  uuid: charUUID,
  properties: { // 記錄這個特徵的所有屬性 (能做什麼)
    read: true,
    write: false,
    notify: true,
    indicate: false
    // ...等等
  },
  value: null // 用來存放從裝置讀取到的最新值 (例如: 98,代表電量98%)
};

3. 藍圖成品:一個完整的心率裝置範例

現在,讓我們把上面零散的片段組合起來,看看一個模擬的「心率監測器」的完整 gattProfile 會長什麼樣子。

// 這就是我們專案未來的「核心資料庫」結構!
const gattProfile = {
  device: {
    name: "Heart Rate Sensor v1.2",
    id: "AB:CD:EF:12:34:56"
  },
  services: {
    // 鍵: 心率服務的 UUID
    '0000180d-0000-1000-8000-00805f9b34fb': {
      uuid: '0000180d-0000-1000-8000-00805f9b34fb',
      characteristics: {
        // 鍵: 心率測量特徵的 UUID
        '00002a37-0000-1000-8000-00805f9b34fb': {
          uuid: '00002a37-0000-1000-8000-00805f9b34fb',
          properties: { read: false, write: false, notify: true },
          value: null // 初始值是 null,等待訂閱後接收資料
        },
        // 鍵: 身體感測器位置特徵的 UUID
        '00002a38-0000-1000-8000-00805f9b34fb': {
          uuid: '00002a38-0000-1000-8000-00805f9b34fb',
          properties: { read: true, write: false, notify: false },
          value: null
        }
      }
    },
    // 鍵: 裝置資訊服務的 UUID
    '0000180a-0000-1000-8000-00805f9b34fb': {
      uuid: '0000180a-0000-1000-8000-00805f9b34fb',
      characteristics: {
        // 鍵: 製造商名稱特徵的 UUID
        '00002a29-0000-1000-8000-00805f9b34fb': {
          uuid: '00002a29-0000-1000-8000-00805f9b34fb',
          properties: { read: true, write: false, notify: false },
          value: null
        }
      }
    }
  }
};

// --- 有了這個結構,存取資料就變得非常語意化且簡單 ---
console.log("裝置名稱:", gattProfile.device.name);

const heartRateServiceUUID = '0000180d-0000-1000-8000-00805f9b34fb';
const heartRateCharUUID = '00002a37-0000-1000-8000-00805f9b34fb';

// 檢查心率測量特徵是否可以被訂閱 (notify)
const canNotify = gattProfile.services[heartRateServiceUUID].characteristics[heartRateCharUUID].properties.notify;

console.log("心率測量特徵是否支援訂閱?", canNotify); // 輸出: true

總結與後續

今天我們完成了整個專案中最重要的一項設計工作:定義了我們應用程式的「單一事實來源 (Single Source of Truth)」

我們學會了:

  • 用物件來描述複雜的世界:將藍牙裝置的階層結構,完美地用巢狀物件來表示。

  • 設計了一個高效的資料庫:透過 UUID 作為鍵,讓未來的資料查詢可以一步到位。

  • 完成了核心藍圖 gattProfile:這個物件將會是我們接下來所有程式碼互動的中心。
    明天開始,我們就要拿著它,踏入真實的世界了!我們將學習如何啟動瀏覽器的藍牙掃描功能,捕捉空中的藍牙訊號,並從中解析出裝置的名稱和 ID,填入我們 gattProfile.device 藍圖中的第一塊空白。
    今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。


上一篇
DAY6 :程式碼的模組化:函式、作用域與閉包
下一篇
DAY8:資料的集合:陣列與常用方法實戰
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言