使用 Intel Edison 建立啟用網路的 IoT 裝置

Kenneth Christiansen
Kenneth Christiansen

時至今日,物聯網在人群中露出了人群 這讓像我這樣的行家和程式設計師都非常興奮把自己的發明融入生活中,並且能夠與人們對話,是再好不過的事!

但安裝不常使用應用程式的 IoT 裝置可能會造成困擾,因此我們會利用實體網路和網路藍牙等即將推出的網路技術,讓 IoT 裝置更直覺化,避免造成乾擾。

用戶端應用程式

網路與 IoT

想在物聯網大獲成功前 仍有諸多挑戰才能克服其中一個障礙是,需要使用者每購買一部裝置就安裝應用程式,導致使用者的手機含有「很少」使用的應用程式。

因此,我們非常高興能推出 Physical Web 專案,此專案可讓裝置以非干擾方式播送線上網站的網址。搭配網路藍牙Web USB網路 NFC 等新興網路技術,網站可以直接連線至裝置,或至少說明正確的操作方式。

雖然本文主要著重於網路藍牙,但部分用途可能更適合使用 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 支援實體化網路,可讓 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 可以找到裝置。

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 對話,用藍牙對話輕鬆又簡單。

在下方範例中,我們建立兩個藍牙低功耗特性:一個用於 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* 建立的回應式使用者介面做為例子。產生的應用程式如下所示:

手機上的用戶端應用程式。
錯誤訊息。

右側顯示的是較舊版本,其中展示了我為了簡化開發作業而新增的簡易錯誤記錄。

透過網路藍牙,您可以輕鬆與藍牙低功耗裝置進行通訊,因此要來看看我的連線代碼的簡化版本。如果您不知道承諾的運作方式,請先參閱這項資源,再繼續閱讀下文。

連線至藍牙裝置需要實現一項承諾。首先,我們會篩選裝置 (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 一樣簡單。只要使用 TextEncoderTextDecoder 即可:

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);
},

摘要

原來如此!如您所見,在用戶端使用網路藍牙功能,在 Edison 上使用 Node.js 與藍牙低功耗通訊,非常簡單且功能強大。

透過實體化網路與網路藍牙,Chrome 會尋找裝置,讓使用者不必安裝使用者可能不想使用的常用應用程式,甚至可能會不時更新,輕鬆連線至裝置。

操作示範

您可以嘗試使用用戶端來取得靈感,瞭解如何建立自己的網頁應用程式來連線到自訂的物聯網裝置。

原始碼

您可以在這裡取得原始碼。歡迎回報問題或傳送修補程式。

素描效果

如果您確實冒險,希望重現我做的事,請參閱下方的 Edison 和麵包板素描:

素描效果