1. 內容為何?

在這個程式碼實驗室中,您將瞭解如何使用 Web Bluetooth API,只透過 JavaScript 控制 PLAYBULB LED 無焰蠟燭。此外,您也會用到 JavaScript ES2015 的功能,例如類別、箭頭函式、Map 和 Promise。
課程內容
- 如何在 JavaScript 中與鄰近藍牙裝置互動
- 如何使用 ES2015 類別、箭頭函式、Map 和 Promise
軟硬體需求
- 對網頁開發有基本瞭解
- 具備藍牙低功耗 (BLE) 和一般屬性設定檔 (GATT) 的基本知識
- 您選擇的文字編輯器
- Mac、Chromebook 或搭載 Chrome 瀏覽器應用程式的 Android M 裝置,以及 USB micro 轉 USB 傳輸線。
2. 搶先體驗
在實際開始進行這個程式碼實驗室之前,建議您先前往 https://googlecodelabs.github.io/candle-bluetooth 查看即將建立的應用程式最終版本,並試用您手邊的 PLAYBULB Candle 藍牙裝置。
你也可以前往 https://www.youtube.com/watch?v=fBCPA9gIxlU,觀看我變換顏色的影片
3. 做好準備
下載程式碼範例
如要取得這個程式碼研究室的程式碼範例,請在這裡下載 ZIP 檔案:
或複製這個 Git 存放區:
git clone https://github.com/googlecodelabs/candle-bluetooth.git
如果您下載的是 ZIP 格式的原始碼,解壓縮後應該會看到根資料夾 candle-bluetooth-master。
安裝及驗證網路伺服器
雖然您可以使用自己的網路伺服器,但本程式碼實驗室是專為與 Chrome 網路伺服器搭配使用而設計。如果尚未安裝,可以前往 Chrome 線上應用程式商店下載。
安裝 Web Server for Chrome 應用程式後,按一下書籤列上的「應用程式」捷徑:

在隨即顯示的視窗中,按一下「Web Server」圖示:

接著,您會看到這個對話方塊,可用來設定本機網路伺服器:

按一下「choose folder」按鈕,然後選取複製 (或取消封存) 存放區的根目錄。這樣一來,您就能透過網路伺服器對話方塊 (「網路伺服器網址」部分) 中醒目顯示的網址,提供正在進行的工作。
在「選項」下方,勾選「Automatically show index.html」旁的方塊,如下所示:

現在,請在網路瀏覽器中造訪您的網站 (按一下醒目顯示的網路伺服器網址),您應該會看到類似下方的頁面:

如要查看這個應用程式在 Android 手機上的外觀,請啟用 Android 遠端偵錯,並設定通訊埠轉送 (預設通訊埠編號為 8887)。接著,只要在 Android 手機上開啟新的 Chrome 分頁,前往 http://localhost:8887 即可。
下一步
目前這個網頁應用程式的功能不多。現在就開始新增藍牙支援功能!
4. 探索蠟燭
首先,我們會編寫一個程式庫,為 PLAYBULB Candle 藍牙裝置使用 JavaScript ES2015 類別。
保持冷靜。類別語法「並非」為 JavaScript 導入新的物件導向繼承模型。它只是提供更清晰的語法來建立物件和處理繼承,如下所示。
首先,我們在 playbulbCandle.js 中定義 PlaybulbCandle 類別,並建立 playbulbCandle 執行個體,稍後可在 app.js 檔案中使用。
playbulbCandle.js
(function() {
'use strict';
class PlaybulbCandle {
constructor() {
this.device = null;
}
}
window.playbulbCandle = new PlaybulbCandle();
})();
如要要求存取鄰近藍牙裝置,我們需要呼叫 navigator.bluetooth.requestDevice。由於 PLAYBULB Candle 裝置會持續宣傳 (如果尚未配對) 固定的藍牙 GATT 服務 UUID (簡稱為 0xFF02),因此我們只要定義常數,並將其新增至 PlaybulbCandle 類別的新公開 connect 方法中的篩選器服務參數即可。
我們也會在內部追蹤 BluetoothDevice 物件,以便日後視需要存取。由於 navigator.bluetooth.requestDevice 會傳回 JavaScript ES2015 Promise,因此我們會在 then 方法中執行這項操作。
playbulbCandle.js
(function() {
'use strict';
const CANDLE_SERVICE_UUID = 0xFF02;
class PlaybulbCandle {
constructor() {
this.device = null;
}
connect() {
let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
return navigator.bluetooth.requestDevice(options)
.then(function(device) {
this.device = device;
}.bind(this));
}
}
window.playbulbCandle = new PlaybulbCandle();
})();
基於安全防護功能考量,使用 navigator.bluetooth.requestDevice 探索鄰近藍牙裝置時,必須透過使用者手勢 (例如輕觸或點按滑鼠) 呼叫。因此,當使用者點選 app.js 檔案中的「Connect」按鈕時,我們會呼叫 connect 方法:
app.js
document.querySelector('#connect').addEventListener('click', function(event) {
document.querySelector('#state').classList.add('connecting');
playbulbCandle.connect()
.then(function() {
console.log(playbulbCandle.device);
document.querySelector('#state').classList.remove('connecting');
document.querySelector('#state').classList.add('connected');
})
.catch(function(error) {
console.error('Argh!', error);
});
});
執行應用程式
此時,請在網路瀏覽器中造訪您的網站 (按一下網路伺服器應用程式中醒目顯示的網路伺服器網址),或直接重新整理現有頁面。按一下綠色的「Connect」按鈕,在選擇器中挑選裝置,然後使用 Ctrl + Shift + J 鍵盤快速鍵開啟偏好的開發人員工具控制台,並留意記錄的 BluetoothDevice 物件。

如果藍牙和/或 PLAYBULB Candle 藍牙裝置關閉,可能會發生錯誤。在這種情況下,請開啟藍牙並再次繼續。
強制獎金
不知道您怎麼想,但我已經在這段程式碼中看到太多 function() {}。請改用 () => {} JavaScript ES2015 箭頭函式。這類函式是絕對的救星:具備匿名函式的所有優點,卻沒有繫結的缺點。
playbulbCandle.js
(function() {
'use strict';
const CANDLE_SERVICE_UUID = 0xFF02;
class PlaybulbCandle {
constructor() {
this.device = null;
}
connect() {
let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
return navigator.bluetooth.requestDevice(options)
.then(device => {
this.device = device;
});
}
}
window.playbulbCandle = new PlaybulbCandle();
})();
app.js
document.querySelector('#connect').addEventListener('click', event => {
playbulbCandle.connect()
.then(() => {
console.log(playbulbCandle.device);
document.querySelector('#state').classList.remove('connecting');
document.querySelector('#state').classList.add('connected');
})
.catch(error => {
console.error('Argh!', error);
});
});
下一步
- 好吧... 我真的能跟這根蠟燭說話嗎?
- 好的,跳到下一個步驟
常見問題
5. 朗讀內容
現在您已從 navigator.bluetooth.requestDevice 的 Promise 傳回 BluetoothDevice,接下來該怎麼做?接著,呼叫 device.gatt.connect(),連線至含有藍牙服務和特徵定義的藍牙遠端 GATT 伺服器:
playbulbCandle.js
class PlaybulbCandle {
constructor() {
this.device = null;
}
connect() {
let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}]};
return navigator.bluetooth.requestDevice(options)
.then(device => {
this.device = device;
return device.gatt.connect();
});
}
}
讀取裝置名稱
我們已連上 PLAYBULB Candle 藍牙裝置的 GATT 伺服器。現在,我們要取得主要 GATT 服務 (先前廣告為 0xFF02),並讀取屬於這項服務的裝置名稱特徵 (0xFFFF)。只要在 PlaybulbCandle 類別中新增 getDeviceName 方法,並使用 device.gatt.getPrimaryService 和 service.getCharacteristic,即可輕鬆達成這個目標。characteristic.readValue 方法實際上會傳回 DataView,我們只需使用 TextDecoder 解碼。
playbulbCandle.js
const CANDLE_DEVICE_NAME_UUID = 0xFFFF;
...
getDeviceName() {
return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
.then(service => service.getCharacteristic(CANDLE_DEVICE_NAME_UUID))
.then(characteristic => characteristic.readValue())
.then(data => {
let decoder = new TextDecoder('utf-8');
return decoder.decode(data);
});
}
讓我們在連線後呼叫 playbulbCandle.getDeviceName,將其新增至 app.js,並顯示裝置名稱。
app.js
document.querySelector('#connect').addEventListener('click', event => {
playbulbCandle.connect()
.then(() => {
console.log(playbulbCandle.device);
document.querySelector('#state').classList.remove('connecting');
document.querySelector('#state').classList.add('connected');
return playbulbCandle.getDeviceName().then(handleDeviceName);
})
.catch(error => {
console.error('Argh!', error);
});
});
function handleDeviceName(deviceName) {
document.querySelector('#deviceName').value = deviceName;
}
此時,請在網路瀏覽器中造訪您的網站 (按一下網路伺服器應用程式中醒目顯示的網路伺服器網址),或直接重新整理現有頁面。確認 PLAYBULB Candle 已開啟,然後點選頁面上的「連線」按鈕,顏色挑選器下方應該會顯示裝置名稱。

朗讀電池電量
PLAYBULB Candle 藍牙裝置中也有標準電池電量藍牙特性,其中包含裝置的電池電量。這表示我們可以使用標準名稱,例如 battery_service 代表藍牙 GATT 服務 UUID,battery_level 代表藍牙 GATT 特徵 UUID。
我們在 PlaybulbCandle 類別中新增 getBatteryLevel 方法,並讀取電池電量百分比。
playbulbCandle.js
getBatteryLevel() {
return this.device.gatt.getPrimaryService('battery_service')
.then(service => service.getCharacteristic('battery_level'))
.then(characteristic => characteristic.readValue())
.then(data => data.getUint8(0));
}
我們也需要更新 options JavaScript 物件,將電池服務加入 optionalServices 鍵,因為 PLAYBULB Candle 藍牙裝置不會 advertise 這項服務,但仍必須存取。
playbulbCandle.js
let options = {filters:[{services:[ CANDLE_SERVICE_UUID ]}],
optionalServices: ['battery_service']};
return navigator.bluetooth.requestDevice(options)
和之前一樣,取得裝置名稱並顯示電池電量後,請呼叫 playbulbCandle.getBatteryLevel,將其插入 app.js。
app.js
document.querySelector('#connect').addEventListener('click', event => {
playbulbCandle.connect()
.then(() => {
console.log(playbulbCandle.device);
document.querySelector('#state').classList.remove('connecting');
document.querySelector('#state').classList.add('connected');
return playbulbCandle.getDeviceName().then(handleDeviceName)
.then(() => playbulbCandle.getBatteryLevel().then(handleBatteryLevel));
})
.catch(error => {
console.error('Argh!', error);
});
});
function handleDeviceName(deviceName) {
document.querySelector('#deviceName').value = deviceName;
}
function handleBatteryLevel(batteryLevel) {
document.querySelector('#batteryLevel').textContent = batteryLevel + '%';
}
此時,請在網路瀏覽器中造訪網站 (按一下網路伺服器應用程式中醒目顯示的網路伺服器網址),或直接重新整理現有頁面。按一下頁面上的「連線」按鈕,即可看到裝置名稱和電池電量。
下一步
- 如何變更這個燈泡的顏色?這就是我來這裡的原因!
- 真的快到了,我保證...
常見問題
6. 變更顏色
如要變更顏色,只要將一組特定指令寫入主要 GATT 服務 (公告為 0xFF02) 的藍牙特性 (0xFFFC) 即可。舉例來說,如要將 PLAYBULB Candle 設為紅色,則會寫入一組 8 位元不帶正負號的整數,其值等於 [0x00, 255, 0, 0],其中 0x00 是白色飽和度,255, 0, 0 則分別是紅色、綠色和藍色值。
我們將使用 characteristic.writeValue,在 PlaybulbCandle 類別的新 setColor 公開方法中,將一些資料實際寫入藍牙特性。我們也會在 Promise 履行時傳回實際的紅色、綠色和藍色值,以便稍後在 app.js 中使用:
playbulbCandle.js
const CANDLE_COLOR_UUID = 0xFFFC;
...
setColor(r, g, b) {
let data = new Uint8Array([0x00, r, g, b]);
return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
.then(service => service.getCharacteristic(CANDLE_COLOR_UUID))
.then(characteristic => characteristic.writeValue(data))
.then(() => [r,g,b]);
}
請更新 app.js 中的 changeColor 函式,在勾選「No Effect」圓形按鈕時呼叫 playbulbCandle.setColor。使用者點選顏色挑選器畫布時,系統已設定全域 r, g, b 顏色變數。
app.js
function changeColor() {
var effect = document.querySelector('[name="effectSwitch"]:checked').id;
if (effect === 'noEffect') {
playbulbCandle.setColor(r, g, b).then(onColorChanged);
}
}
此時,請在網路瀏覽器中造訪您的網站 (按一下網路伺服器應用程式中醒目顯示的網路伺服器網址),或直接重新整理現有頁面。按一下頁面上的「Connect」按鈕,然後點選顏色挑選器,即可不限次數變更 PLAYBULB Candle 的顏色。
更多蠟燭效果
如果你曾點過蠟燭,就會知道燭光並非靜止不動。幸好,主要 GATT 服務中還有另一個藍牙特性 (0xFFFB),其廣告名稱為 0xFF02,可讓使用者設定一些蠟燭效果。
舉例來說,只要編寫 [0x00, r, g, b, 0x04, 0x00, 0x01, 0x00],就能設定「燭光效果」。你也可以使用 [0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00] 設定「閃爍效果」。
現在,我們將 setCandleEffectColor 和 setFlashingColor 方法新增至 PlaybulbCandle 類別。
playbulbCandle.js
const CANDLE_EFFECT_UUID = 0xFFFB;
...
setCandleEffectColor(r, g, b) {
let data = new Uint8Array([0x00, r, g, b, 0x04, 0x00, 0x01, 0x00]);
return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
.then(service => service.getCharacteristic(CANDLE_EFFECT_UUID))
.then(characteristic => characteristic.writeValue(data))
.then(() => [r,g,b]);
}
setFlashingColor(r, g, b) {
let data = new Uint8Array([0x00, r, g, b, 0x00, 0x00, 0x1F, 0x00]);
return this.device.gatt.getPrimaryService(CANDLE_SERVICE_UUID)
.then(service => service.getCharacteristic(CANDLE_EFFECT_UUID))
.then(characteristic => characteristic.writeValue(data))
.then(() => [r,g,b]);
}
接著更新 app.js 中的 changeColor 函式,在勾選「Candle Effect」圓形按鈕時呼叫 playbulbCandle.setCandleEffectColor,在勾選「Flashing」圓形按鈕時呼叫 playbulbCandle.setFlashingColor。這次我們會使用 switch,希望你不會介意。
app.js
function changeColor() {
var effect = document.querySelector('[name="effectSwitch"]:checked').id;
switch(effect) {
case 'noEffect':
playbulbCandle.setColor(r, g, b).then(onColorChanged);
break;
case 'candleEffect':
playbulbCandle.setCandleEffectColor(r, g, b).then(onColorChanged);
break;
case 'flashing':
playbulbCandle.setFlashingColor(r, g, b).then(onColorChanged);
break;
}
}
此時,請在網路瀏覽器中造訪您的網站 (按一下網路伺服器應用程式中醒目顯示的網路伺服器網址),或直接重新整理現有頁面。按一下頁面上的「Connect」按鈕,然後試用蠟燭和閃爍效果。
下一步
- 就這樣?3 個蠟燭效果不佳?這就是我來這裡的原因嗎?
- 還有更多,但這次你得自己找。
7. 更多應用方式
終於,您可能以為快要完成了,但應用程式還沒完成。我們來看看您是否真的瞭解在本程式碼實驗室中複製貼上的內容。接下來,請自行完成以下步驟,讓這個應用程式更臻完美。
新增缺少的特效
以下是遺漏效果的資料:
- Pulse:
[0x00, r, g, b, 0x01, 0x00, 0x09, 0x00](建議調整r, g, b值) - 彩虹:
[0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00](癲癇患者可能不適合使用) - 彩虹漸層:
[0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x26, 0x00]
這基本上代表將新的 setPulseColor、setRainbow 和 setRainbowFade 方法新增至 PlaybulbCandle 類別,並在 changeColor 中呼叫這些方法。
修正「無效」問題
你可能已經注意到,「無效果」選項不會重設任何進行中的效果,雖然這只是小問題,但還是要留意。接著就來解決這個問題。在 setColor 方法中,您必須先透過新的類別變數 _isEffectSet 檢查效果是否正在進行,如果是 true,請先關閉效果,再使用以下資料設定新顏色:[0x00, r, g, b, 0x05, 0x00, 0x01, 0x00]。
輸入裝置名稱
這題很簡單!寫入自訂裝置名稱,就像寫入先前的藍牙裝置名稱特徵一樣簡單。建議使用 TextEncoder encode 方法,取得包含裝置名稱的 Uint8Array。
接著,我會將「輸入」eventListener 新增至 document.querySelector('#deviceName'),並呼叫 playbulbCandle.setDeviceName,讓程式碼保持簡單。
我個人將其命名為 PLAY💡 CANDLE!
8. 這樣就大功告成了!
您已經瞭解的內容
- 如何在 JavaScript 中與鄰近藍牙裝置互動
- 如何使用 ES2015 類別、箭頭函式、Map 和 Promise
後續步驟
- 進一步瞭解 Web Bluetooth API
- 瀏覽官方 Web Bluetooth 範例和示範
- 看看飛天臭臉貓

