利用 Intel Edison 打造支持 Web 的 IoT 设备

Kenneth Christiansen
Kenneth Christiansen

如今,物联网的呼声很高, 它让像我这样的修补者和程序员都非常高兴。没有什么比将自己的发明变为现实并能够与您交谈更酷了!

但是,安装您很少使用的应用的 IoT 设备可能会很烦人,因此我们利用即将推出的网络技术(如实物网和 Web 蓝牙)让 IoT 设备更直观、更具干扰性。

客户端应用

Web 和 IoT 相得益彰

但要克服诸多障碍,物联网可以取得巨大成功。其中一个障碍是一些公司和产品要求用户为购买的每台设备安装应用,导致用户的手机上摆满大量用户很少使用的应用。

因此,我们对实物网项目非常感兴趣;借助此项目,设备能够以不会干扰的方式广播在线网站的网址。通过与网络蓝牙网络 USB网络 NFC 等新兴的 Web 技术相结合,网站可以直接连接到设备,或者至少可以说明连接设备的正确方法。

虽然本文主要介绍网络蓝牙,但某些用例可能更适合使用网络 NFC 或网络 USB。例如,如果您出于安全考虑需要进行物理连接,则首选 Web USB。

网站还可以用作渐进式 Web 应用 (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 会公布附近的实物网信标。
系统会列出 Web 应用网址。

与传感器/执行器通信

我们使用 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) 引脚。

Edison 板

对着蓝牙说话

与蓝牙对话比使用 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 方法并对“read”的值进行编码。

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 模块,则以上所有方法都将不起作用。除非您使用随 bleno 分发的 noble 版本,否则 eddystone-beacon 将不能与 bleno 搭配使用。幸运的是,这样做非常简单:

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

创建客户端 Web 应用

我们不妨以 Polymer* 中的响应式界面为例,说明客户端应用非蓝牙部分如何工作的问题。生成的应用如下所示:

手机上的客户端应用。
错误消息。

右侧显示了较早版本,其中显示了我添加的简单错误日志,以简化开发工作。

借助 Web 蓝牙功能,您可以轻松地与低功耗蓝牙设备进行通信,下面我们来看一下我的连接代码的简化版本。如果您不知道 promise 的工作原理,请先参阅此资源,然后再阅读更多内容。

连接到蓝牙设备涉及 promise 链。首先,我们过滤设备(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);
},

摘要

朋友们就是这样!如您所见,在客户端使用 Web 蓝牙与 Edison 上的 Node.js 与蓝牙低功耗通信非常简单且功能强大。

借助实物网和网络蓝牙,Chrome 能够找到设备并让用户能够轻松连接到该设备,而无需安装用户可能不需要且可能会不时更新但很少使用的应用。

演示

您可以尝试使用客户端,了解如何创建自己的 Web 应用以连接到自定义物联网设备。

源代码

如需查看源代码,请点击此处。 欢迎随时报告问题或发送补丁。

素描

如果您非常喜欢尝试新事物,并且想要重现我所做的一切,请参阅下面的“爱迪生”和面包板草图:

素描