物联网是时下热门话题,这让像我这样的爱好者和程序员感到非常兴奋。没有什么比让自己的发明变得生动起来,并能够与它们对话更酷了!
但是,物联网设备安装您很少使用的应用可能会令人烦恼,因此我们利用即将推出的 Web 技术(例如 Physical Web 和 Web Bluetooth)来让物联网设备更直观、更不具侵扰性。
Web 和 IoT,天作之合
在物联网取得巨大成功之前,仍有许多障碍需要克服。其中一个障碍是,有些公司和产品要求用户为购买的每部设备安装应用,这会在用户的手机上堆满他们很少使用的应用。
因此,我们非常期待物联网项目,该项目允许设备以非侵扰性方式向在线网站广播网址。通过结合使用 Web Bluetooth、Web USB 和 Web NFC 等新兴 Web 技术,这些网站可以直接连接到设备,或者至少说明正确的连接方式。
虽然本文主要介绍 Web Bluetooth,但某些用例可能更适合使用 Web NFC 或 Web 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 可以轻松找到设备。
与传感器/执行器通信
我们使用 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
变量;这些变量将在稍后介绍与蓝牙交互的部分中定义。
如您在 Thermometer 对象的实例化中可能注意到,我通过模拟 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);
};
对于“notify”,我们需要添加一个方法来处理订阅和取消订阅。基本上,我们只存储一个回调。当有新的温度原因要发送时,我们会使用新值(如上文所述)调用该回调。
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 颜色代码(例如:rebeccapurple
等 CSS 名称或 #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 Bluetooth 可让您轻松与蓝牙低功耗设备通信,因此我们来看看简化版的连接代码。如果您不了解 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
一样简单。我们只需使用 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 可以找到设备并允许用户轻松连接到该设备,而无需安装用户可能不想使用且可能会不时更新的应用。
演示
您可以尝试使用客户端获取灵感,了解如何创建自己的 Web 应用以连接到自定义物联网设备。
源代码
素描
如果您非常喜欢冒险,并希望重现我的作品,请参阅下面的 Edison 和电路试验板草图: