透過網路藍牙控制 PLAYBULB 蠟燭

1. 內容為何?

IMG_19700101_023537~2~2.jpg

在這個程式碼實驗室中,您將瞭解如何使用 Web Bluetooth API,只透過 JavaScript 控制 PLAYBULB LED 無焰蠟燭。此外,您也會用到 JavaScript ES2015 的功能,例如類別、箭頭函式、Map 和 Promise。

課程內容

  • 如何在 JavaScript 中與鄰近藍牙裝置互動
  • 如何使用 ES2015 類別、箭頭函式、Map 和 Promise

軟硬體需求

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 應用程式後,按一下書籤列上的「應用程式」捷徑:

Screen Shot 2016-11-16 at 4.10.42 PM.png

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

9f3c21b2cf6cbfb5.png

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

Screen Shot 2016-11-16 at 3.40.47 PM.png

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

在「選項」下方,勾選「Automatically show index.html」旁的方塊,如下所示:

Screen Shot 2016-11-16 at 3.40.56 PM.png

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

Screen Shot 2016-11-16 at 3.20.22 PM.png

如要查看這個應用程式在 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 物件。

Screen Shot 2016-11-16 at 3.27.12 PM.png

如果藍牙和/或 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 已開啟,然後點選頁面上的「連線」按鈕,顏色挑選器下方應該會顯示裝置名稱。

Screen Shot 2016-11-16 at 3.29.21 PM.png

朗讀電池電量

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 + '%';
}

此時,請在網路瀏覽器中造訪網站 (按一下網路伺服器應用程式中醒目顯示的網路伺服器網址),或直接重新整理現有頁面。按一下頁面上的「連線」按鈕,即可看到裝置名稱和電池電量。

Screen Shot 2016-11-16 at 3.29.21 PM.png

下一步

- 如何變更這個燈泡的顏色?這就是我來這裡的原因!

- 真的快到了,我保證...

常見問題

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 的顏色。

Screen Shot 2016-11-16 at 3.31.37 PM.png

更多蠟燭效果

如果你曾點過蠟燭,就會知道燭光並非靜止不動。幸好,主要 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」按鈕,然後試用蠟燭和閃爍效果。

Screen Shot 2016-11-16 at 3.33.23 PM.png

下一步

- 就這樣?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

後續步驟