物聯網目前是大家熱議的話題,這讓我這類愛動手和編寫程式的人感到非常興奮。沒有什麼比讓自己的發明變得栩栩如生,並能與之對話更酷炫了!
不過,如果 IoT 裝置安裝您很少使用的應用程式,可能會造成困擾,因此我們採用即將推出的網頁技術 (例如 Physical Web 和 Web Bluetooth),讓 IoT 裝置更直覺且不具侵擾性。

網頁和 IoT,相得益彰
物聯網要想大放異彩,仍有許多難關要克服。其中一個障礙是,有些公司和產品會要求使用者為購買的每部裝置安裝應用程式,導致使用者手機上充斥著許多「很少使用」的應用程式。
因此,我們非常期待Physical Web 專案,讓裝置以不干擾的方式,將網址廣播到線上網站。搭配新興的網路技術,例如 Web Bluetooth、Web USB 和 Web NFC,網站就能直接連線至裝置,或至少說明正確的連線方式。
雖然本文主要著重於 Web Bluetooth,但某些用途可能更適合使用 Web NFC 或 Web USB。舉例來說,如果您基於安全性考量而需要實體連線,建議使用 Web USB。
網站也可以做為漸進式網頁應用程式 (PWA)。我們建議讀者參閱 Google 對 PWA 的說明。PWA 是具備回應式應用程式使用者體驗的網站,可在離線狀態下運作,並可新增至裝置主畫面。
為了證明概念,我一直使用 Intel® Edison Arduino 擴充板來建構小型裝置。裝置包含溫度感應器 (TMP36) 和致動器 (彩色 LED 陰極)。這部裝置的電路圖可參見本文結尾。

Intel Edison 是一款有趣的產品,因為它可以執行完整的 Linux* 發行版本。因此,我可以輕鬆使用 Node.js 編寫程式碼。安裝程式可讓您安裝 Intel* XDK,讓您輕鬆上手,但您也可以手動編寫程式並上傳至裝置。
針對我的 Node.js 應用程式,我需要三個節點模組及其依附元件:
eddystone-beacon
parse-color
johnny-five
前者會自動安裝 noble
,這是我用來透過藍牙低功耗進行通訊的節點模組。
專案的 package.json
檔案如下所示:
{
"name": "edison-webbluetooth-demo-server",
"version": "1.0.0",
"main": "main.js",
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"eddystone-beacon": "^1.0.5",
"johnny-five": "^0.9.30",
"parse-color": "^1.0.0"
}
}
宣布推出網站
自 49 版起,Android 版 Chrome 支援 Physical Web,可讓 Chrome 查看周遭裝置發布的網址。開發人員必須瞭解一些必要條件,例如網站必須可供大眾存取,且使用 HTTPS。
Eddystone 通訊協定對網址有 18 個位元組大小限制。因此,為了讓我的示範應用程式網址 (https://webbt-sensor-hub.appspot.com/) 正常運作,我必須使用網址縮短工具。
廣播網址的做法相當簡單。您只需匯入必要的程式庫,並呼叫幾個函式即可。其中一種方法是在 BLE 晶片開啟時呼叫 advertiseUrl
:
var beacon = require("eddystone-beacon");
var bleno = require('eddystone-beacon/node_modules/bleno');
bleno.on('stateChange', function(state) {
if (state === 'poweredOn') {
beacon.advertiseUrl("https://goo.gl/9FomQC", {name: 'Edison'});
}
}
這麼做真的很簡單。下圖顯示 Chrome 順利找到裝置。


與感應器/執行器通訊
我們使用 Johnny-Five* 與擴充板互動。Johnny-Five 提供了一個不錯的抽象層,可用來與 TMP36 感應器通訊。
您可以參考下方的簡單程式碼,瞭解如何接收溫度變更通知,以及設定初始 LED 顏色。
var five = require("johnny-five");
var Edison = require("edison-io");
var board = new five.Board({
io: new Edison()
});
board.on("ready", function() {
// Johnny-Five's Led.RGB class can be initialized with
// an array of pin numbers in R, G, B order.
// Reference: http://johnny-five.io/api/led.rgb/#parameters
var led = new five.Led.RGB([ 3, 5, 6 ]);
// Johnny-Five's Thermometer class provides a built-in
// controller definition for the TMP36 sensor. The controller
// handles computing a Celsius (also Fahrenheit & Kelvin) from
// a raw analog input value.
// Reference: http://johnny-five.io/api/thermometer/
var temp = new five.Thermometer({
controller: "TMP36",
pin: "A0",
});
temp.on("change", function() {
temperatureCharacteristic.valueChange(this.celsius);
});
colorCharacteristic._led = led;
led.color(colorCharacteristic._value);
led.intensity(30);
});
您可以先忽略上述 *Characteristic
變數,這些變數將在後續的藍牙介面相關章節中定義。
如您在溫度計物件例項化時所見,我會透過類比 A0
連接埠與 TMP36 通訊。彩色 LED 陰極上的電壓支架連接至數位接腳 3、5 和 6,而這恰好是 Edison Arduino 擴充板上的脈衝寬調變 (PWM) 接腳。

與藍牙對話
與藍牙通訊比使用 noble
更容易。
在以下範例中,我們建立了兩個 Bluetooth Low Energy 特性:一個用於 LED,另一個用於溫度感應器。前者可讓我們讀取目前的 LED 顏色,並設定新顏色。後者可讓我們訂閱溫度變更事件。
使用 noble
建立特徵相當簡單。您只需定義特性的通訊方式和 UUID 即可。通訊選項包括讀取、寫入、通知或任何組合。最簡單的做法是建立新物件,並從 bleno.Characteristic
繼承。
產生的特性物件如下所示:
var TemperatureCharacteristic = function() {
bleno.Characteristic.call(this, {
uuid: 'fc0a',
properties: ['read', 'notify'],
value: null
});
this._lastValue = 0;
this._total = 0;
this._samples = 0;
this._onChange = null;
};
util.inherits(TemperatureCharacteristic, bleno.Characteristic);
我們會將目前的溫度值儲存在 this._lastValue
變數中。我們需要新增 onReadRequest
方法,並為「讀取」編碼值。
TemperatureCharacteristic.prototype.onReadRequest = function(offset, callback) {
var data = new Buffer(8);
data.writeDoubleLE(this._lastValue, 0);
callback(this.RESULT_SUCCESS, data);
};
針對「通知」,我們需要新增方法來處理訂閱和取消訂閱。基本上,我們只會儲存回呼。當我們要傳送新的溫度原因時,就會使用新值 (以上述方式編碼) 呼叫該回呼。
TemperatureCharacteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) {
console.log("Subscribed to temperature change.");
this._onChange = updateValueCallback;
this._lastValue = undefined;
};
TemperatureCharacteristic.prototype.onUnsubscribe = function() {
console.log("Unsubscribed to temperature change.");
this._onChange = null;
};
由於值可能會稍微波動,因此我們需要平滑化從 TMP36 感應器取得的值。我選擇直接取 100 個樣本的平均值,並只在溫度變動至少 1 度時傳送更新。
TemperatureCharacteristic.prototype.valueChange = function(value) {
this._total += value;
this._samples++;
if (this._samples < NO_SAMPLES) {
return;
}
var newValue = Math.round(this._total / NO_SAMPLES);
this._total = 0;
this._samples = 0;
if (this._lastValue && Math.abs(this._lastValue - newValue) < 1) {
return;
}
this._lastValue = newValue;
console.log(newValue);
var data = new Buffer(8);
data.writeDoubleLE(newValue, 0);
if (this._onChange) {
this._onChange(data);
}
};
那是溫度感應器。彩色 LED 燈更簡單。物件和「read」方法如下所示。該特性已設定為允許「讀取」和「寫入」作業,且 UUID 與溫度特性不同。
var ColorCharacteristic = function() {
bleno.Characteristic.call(this, {
uuid: 'fc0b',
properties: ['read', 'write'],
value: null
});
this._value = 'ffffff';
this._led = null;
};
util.inherits(ColorCharacteristic, bleno.Characteristic);
ColorCharacteristic.prototype.onReadRequest = function(offset, callback) {
var data = new Buffer(this._value);
callback(this.RESULT_SUCCESS, data);
};
為了從物件控制 LED,我新增了 this._led
成員,用於儲存 Johnny-Five LED 物件。我也會將 LED 的顏色設為預設值 (白色,也就是 #ffffff
)。
board.on("ready", function() {
...
colorCharacteristic._led = led;
led.color(colorCharacteristic._value);
led.intensity(30);
...
}
「write」方法會接收字串 (就像「read」會傳送字串一樣),其中可以包含 CSS 顏色代碼 (例如:CSS 名稱,例如 rebeccapurple
或十六進位代碼,例如 #ff00bb
)。我使用名為 parse-color 的節點模組,一律取得 Johnny-Five 預期的十六進位值。
ColorCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) {
var value = parse(data.toString('utf8')).hex;
if (!value) {
callback(this.RESULT_SUCCESS);
return;
}
this._value = value;
console.log(value);
if (this._led) {
this._led.color(this._value);
}
callback(this.RESULT_SUCCESS);
};
如果未加入 bleno 模組,上述所有操作都無法運作。eddystone-beacon
無法與 bleno 搭配運作,除非您使用與 noble
一起發布的版本。所幸,這麼做很簡單:
var bleno = require('eddystone-beacon/node_modules/bleno');
var util = require('util');
我們現在只需要讓它宣傳我們的裝置 (UUID) 及其特性 (其他 UUID)
bleno.on('advertisingStart', function(error) {
...
bleno.setServices([
new bleno.PrimaryService({
uuid: 'fc00',
characteristics: [
temperatureCharacteristic, colorCharacteristic
]
})
]);
});
建立用戶端網頁應用程式
為了避免過度深入說明用戶端應用程式非藍牙部分的運作方式,我們可以以在 Polymer* 中建立的回應式使用者介面為例。最終的應用程式如下所示:


右側顯示較舊的版本,其中顯示我為了簡化開發作業而新增的簡單錯誤記錄。
Web Bluetooth 可讓您輕鬆與藍牙低功耗裝置通訊,因此讓我們看看簡化的連線程式碼。如果您不瞭解承諾的運作方式,請先參閱這份資源,再繼續閱讀本文。
連線至藍牙裝置時,會涉及一連串的承諾。首先,我們會篩選裝置 (UUID:FC00
,名稱:Edison
)。這會顯示對話方塊,讓使用者根據篩選條件選取裝置。接著,我們會連線至 GATT 服務,並取得主要服務和相關特性,然後讀取值並設定通知回呼。
下方程式碼的簡化版本僅適用於最新的 Web Bluetooth API,因此需要在 Android 上使用 Chrome 開發人員版本 (M49)。
navigator.bluetooth.requestDevice({
filters: [{ name: 'Edison' }],
optionalServices: [0xFC00]
})
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService(0xFC00))
.then(service => {
let p1 = () => service.getCharacteristic(0xFC0B)
.then(characteristic => {
this.colorLedCharacteristic = characteristic;
return this.readLedColor();
});
let p2 = () => service.getCharacteristic(0xFC0A)
.then(characteristic => {
characteristic.addEventListener(
'characteristicvaluechanged', this.onTemperatureChange);
return characteristic.startNotifications();
});
return p1().then(p2);
})
.catch(err => {
// Catch any error.
})
.then(() => {
// Connection fully established, unless there was an error above.
});
從 DataView
/ ArrayBuffer
(WebBluetooth API 使用的內容) 讀取及寫入字串,就像在 Node.js 端使用 Buffer
一樣簡單。我們只需要使用 TextEncoder
和 TextDecoder
:
readLedColor: function() {
return this.colorLedCharacteristic.readValue()
.then(data => {
// In Chrome 50+, a DataView is returned instead of an ArrayBuffer.
data = data.buffer ? data : new DataView(data);
let decoder = new TextDecoder("utf-8");
let decodedString = decoder.decode(data);
document.querySelector('#color').value = decodedString;
});
},
writeLedColor: function() {
let encoder = new TextEncoder("utf-8");
let value = document.querySelector('#color').value;
let encodedString = encoder.encode(value.toLowerCase());
return this.colorLedCharacteristic.writeValue(encodedString);
},
處理溫度感應器的 characteristicvaluechanged
事件也相當簡單:
onTemperatureChange: function(event) {
let data = event.target.value;
// In Chrome 50+, a DataView is returned instead of an ArrayBuffer.
data = data.buffer ? data : new DataView(data);
let temperature = data.getFloat64(0, /*littleEndian=*/ true);
document.querySelector('#temp').innerHTML = temperature.toFixed(0);
},
摘要
就這樣,各位!如您所見,使用用戶端的 Web Bluetooth 和 Edison 上的 Node.js 與低功耗藍牙通訊,既簡單又強大。
Chrome 會使用 Physical Web 和 Web Bluetooth 尋找裝置,讓使用者輕鬆連線,不必安裝使用者不想安裝的應用程式 (這類應用程式可能會不時更新)。
示範
您可以試試用戶端,瞭解如何建立自己的網路應用程式,連線至自訂物聯網裝置。
原始碼
原始碼可在這裡取得。歡迎回報問題或傳送修補程式。
素描
如果您想嘗試重現我所做的事,請參閱下方的 Edison 和麵包板草圖: