昨天,我們學會了如何像建築師一樣,使用函式、作用域和閉包來建構程式碼的「功能模組」。我們現在擁有的,是有組織、有紀律的工具箱。
然而,光有工具還不夠。在建造一棟大樓之前,建築師最需要的是什麼?是一張精準、詳細的建築藍圖。這張藍圖定義了大樓的結構、有多少房間、每個房間的功能是什麼。如果沒有藍圖就隨意施工,最後蓋出來的肯定是一棟混亂的危樓。
在我們的專案中,從藍牙裝置讀取到的所有資訊——裝置名稱、提供的所有服務、每項服務下的所有特徵,以及特徵的屬性——就是我們建築的「鋼筋水泥」。今天,我們的任務就是設計一張清晰的「資料藍圖」,學習如何用 JavaScript 中最強大的結構——物件 (Object)——來規劃和儲存所有這些複雜的資訊。
今天過後,你將不再只是看到零散的資料,而是能建立一個有系統、有階層的「GATT Profile 資料庫」。這個資料庫將是我們整個應用程式的「大腦」和「心臟」,未來所有的介面顯示、資料讀寫,都將圍繞著它來運作!
我們之前提過,物件 {}
就像一個收納盒。今天,我們要把它玩得更透徹。
核心概念:物件,是由一對對的「鍵 (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
。好了,理論講完了,來點實際的。我們要怎麼用物件來描述一個藍牙裝置的結構呢?
回憶一下 GATT 的階層: 一個裝置 (Device) -> 包含多個服務 (Service) -> 每個服務包含多個特徵 (Characteristic)
我們的藍圖就要完全仿照這個結構!
這是我們整個資料結構的最外層,代表連上的那個藍牙裝置。
const gattProfile = {
// 存放裝置的基本資訊
device: {
name: null, // 裝置名稱,例如 "My ESP32" (初始為 null)
id: null // 裝置ID (初始為 null)
},
// 準備一個收納盒,專門用來放所有的「服務」
services: {} // 目前是空的
};
services
本身也是一個物件。我們將用服務的 UUID 當作「鍵」,對應的服務物件當作「值」。
為什麼不用陣列 []
? 因為用 UUID 當鍵,未來查詢特定服務會超級快!gattProfile.services['UUID']
一下就能找到,而不用跑 for
迴圈在陣列裡慢慢找。
// 假設我們發現了一個「電池服務」
gattProfile.services['0000180f-0000-1000-8000-00805f9b34fb'] = {
// 這是服務物件的內容
uuid: '0000180f-0000-1000-8000-00805f9b34fb',
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%)
};
現在,讓我們把上面零散的片段組合起來,看看一個模擬的「心率監測器」的完整 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
藍圖中的第一塊空白。
今天的內容就到這邊,感謝你能看到這裡,在這邊祝你早安、午安、晚安,我們明天見。