本文是“从物联到万联,Node.js与树莓派万维物联网构建实战”一书的读书笔记,该书翻译自“Building the Web of Things with examples in Node.js and Raspberry Pi (by Guinard &Trifa)”。
书中相关源码可以通过@github访问,读者可以通过@webofthings来访问作者搭建好的web物联网应用。
本文重点关注书中相关实例的实现及其依赖的一些关键概念,对一些基础知识和简单代码逻辑不做过多解释。主要总结实际操作碰到的问题和解决问题的思路。
1) 无法加载jquery.min.js
出现错误:”Failed to load resource: net::ERR_CONNECTION_RESET jquery.min.js:1“
考虑国内无法访问ajax.googleapis.com,我们可以将jquery下载到本地再修改src指向本地路径,也可以搜索网络上可用的jquery。以下我们替换这一行代码为:
重新打开html文件即可,注意进入浏览器可以在设置里面打开“开发人员模式”,切换到console选项卡查看打印的log。以下是该物联网聚合应用运行成功后的截图:
2)源码分析
//#A First, get the temperature in the user location from Yahoo
//#B Then get the temperature from the WoT Pi in London
//#C Prepare the text to publish and use it to update the content of the LCD screen
//#D POST the message to the LCD actuator
//#E Set a timer that will call the takePicture() function in N seconds (after the LCD content has been updated)
//#F Generates the text to display with the user name, location and Pi temperature
//#G Retrieve the current image from the Webcam in our office
//#H Update the HTML img tag with the image URL
以上英文还是比较好翻译的,如果不太确定,可以借助有道词典翻译下:
//#A首先,从雅虎获取用户位置的温度
//#B然后从伦敦的WoT Pi得到温度
//#C准备好要发布的文本并使用它来更新LCD屏幕的内容
//#D将信息发送到LCD驱动器
//#设置一个计时器,在N秒内调用takePicture()函数(LCD内容更新后)
//#F生成显示用户名、位置和Pi温度的文本
//#G从我们办公室的摄像头中获取当前的图像
//#H用图像URL更新HTML标签
这样我们就大概理清了这个web程序“ex-5-mashup.html”的大致逻辑就是:分别从雅虎和WOT Pi得到温度数据,经过处理之后把要发送的数据通过Web API发送给树莓派,等待一段时间,通过摄像头查看LCD数据的响应结果。
关于IO模型的相关知识,可以参考“UNIX高级编程”的相关议题。
1)使用request库发起http请求
npm init
npm install request --save
node *.js
代码清单 3.7
var request = require('request');
request('http://webofthings.org', function (error, response, body) { //#A
if (!error && response.statusCode === 200) {
console.log(body); //#B
}
});
//#A This is an anonymous callback that will be invoked when the request library did fetch the webofthings homepage from the Web
//#B This will display the HTML code of the page
以上看出,request库的第二个参数是一个匿名回调函数,当request库从第一个参数指定的URL获取到web网络数据后,该匿名函数被调用;这里的function有三个参数,当这个异步操作过程出现任何错误时,会将错误内容返回给error,response返回响应头,body返回响应正文。匿名函数体我们直接通过console.log将body内容打印处理。
2018.09.22更新
本章涵盖的内容如下:
探索Web智能产品的三种可能模式
通过Web协议访问传感器和执行器
使用Node.js和Express 在树莓派上面构建REST和WebSocket API
构建COPA设备并连接到Web
在树莓派上面使用MQTT来连接EVERYTHNG API
本章围绕物联网设备接入Web的三种集成模式展开,它们分别是:
直接连接模式——在设备上面实现REST
网关集成模式——COAP
云端集成模式——EVRYTHNG的MQTT
设计智能web产品的过程:
集成策略——选择一个模型将智能产品集成到Web。本章将讲解这些模型。
资源设计——确定智能产品的功能或服务,然后组织它们的层次结构。
表述设计——决定每个资源服务采用哪一种表述。
接口设计——决定每个服务可能接收哪些命令,以及处理哪些异常。
资源链接设计——决定不同资源之间如何相互链接。
要想实现一套成熟的REST API,有许多的Node.js框架可以选择。这里列举一些最流行的Node.js的Web/REST框架:<1>
EXPRESS: A NODE.JS WEB FRAMEWORK
虽然Express 可以在树莓派和大部分linux设备上面平稳运行,但是我们也要明白,Express并不是实现IOT设备Web API的最轻量方式。<2>
7.2.2 资源设计
我们要将硬件资源映射为REST接口,那么一个最基本的问题就是,如何来描述这些资源呢?答案就是通过JSON来保存我们的资源树。
步骤1:创建资源模型
新建resources/resources.json,代码如下:
{
"pi": {
"name": "WoT Pi",
"description": "A simple WoT-connected Raspberry PI for the WoT book.",
"port": 8484,
"sensors": {
"temperature": {
"name": "Temperature Sensor",
"description": "An ambient temperature sensor.",
"unit": "celsius",
"value": 0,
"gpio": 12
},
"humidity": {
"name": "Humidity Sensor",
"description": "An ambient humidity sensor.",
"unit": "%",
"value": 0,
"gpio": 12
},
"pir": {
"name": "Passive Infrared",
"description": "A passive infrared sensor. When 'true' someone is present.",
"value": true,
"gpio": 17
}
},
"actuators": {
"leds": {
"1": {
"name": "LED 1",
"value": false,
"gpio": 4
},
"2": {
"name": "LED 2",
"value": false,
"gpio": 9
}
}
}
}
}
新建resources/model.js,代码如下:
var resources = require('./resources.json');
module.exports = resources;
这个文件加载resources.json文件,然后exports将object转变为node模块,这样就可以在应用里面使用它了。
步骤2:创建Express路由
在Express和其他许多Web框架中,资源的URL通过路由来定义
var express = require('express'),
router = express.Router(),
resources = require('./../resources/model');
router.route('/').get(function (req, res, next) {
req.result = resources.pi.actuators;
next();
});
....
module.exports = router;
创建完路由后,export router,这样其他模块就可以require它了。
步骤3:创建Express应用
通过Express framwork封装一个HTTP server,在servers/http.js文件实现,在这个文件中加载在我们之前创建的路由。
// Final version
var express = require('express'),
actuatorsRoutes = require('./../routes/actuators'),
sensorRoutes = require('./../routes/sensors'),
thingsRoutes = require('./../routes/things'),
resources = require('./../resources/model'),
converter = require('./../middleware/converter'),
cors = require('cors'),
bodyParser = require('body-parser');
...
var app = express();
...
//绑定路由到Express应用
app.use('/pi/actuators', actuatorsRoutes);
app.use('/pi/sensors', sensorRoutes);
app.use('/things', thingsRoutes);
app.get('/pi', function (req, res) {
res.send('This is the WoT-Pi!')
});
...
module.exports = app;
在可以测试功能之前,还需要wot-server.js文件,这是WOT服务器的入口,负责以正确的配置启动服务。
// Final version
var httpServer = require('./servers/http'),
wsServer = require('./servers/websockets'),
resources = require('./resources/model');
...
// HTTP Server
var server = httpServer.listen(resources.pi.port, function () {
console.log('HTTP server started...');
// Websockets server
wsServer.listen(server);
console.info('Your WoT Pi is up and running on port %s', resources.pi.port);
});
步骤4:将传感器绑定到服务器上
来到这里就比较有趣了,因为涉及到和硬件的交互,直接看代码:
var model = resources.pi.sensors.pir;
var resources = require('./../../resources/model');
var model = resources.pi.sensors.pir;
...
function connectHardware() { //#B
var Gpio = require('onoff').Gpio;
sensor = new Gpio(model.gpio, 'in', 'both'); //#C
sensor.watch(function (err, value) { //#D
if (err) exit(err);
model.value = !!value;
showValue();
});
console.info('Hardware %s sensor started!', pluginName);
};
model.gpio获取硬件的GPIO配置信息,这个信息保存在我们前面提到的resources.json文件中。最终通过watch来监听GPIO事件,触发事件时回调函数会被执行。
有了插件后,在wot-server.js当然要把它用起来啦:
// Internal Plugins
var ledsPlugin = require('./plugins/internal/ledsPlugin'), //#A
pirPlugin = require('./plugins/internal/pirPlugin'), //#A
dhtPlugin = require('./plugins/internal/DHT22SensorPlugin'); //#A
// Internal Plugins for sensors/actuators connected to the PI GPIOs
// If you test this with real sensors do not forget to set simulate to 'false'
pirPlugin.start({'simulate': true, 'frequency': 2000}); //#B
ledsPlugin.start({'simulate': true, 'frequency': 10000}); //#B
dhtPlugin.start({'simulate': true, 'frequency': 10000}); //#B
在树莓派上用真实硬件测试
在树莓派上面运行我们前面创建好的项目
npm install –save 添加依赖
修改启动服务的参数为{‘simulate’:false}
node wot-server.js
7.2.3 表述设计
实现一个表述转换中间件
/middleware/converter.js : 实现html、json、msgpack 表述转换
7.2.4 接口设计
添加一个BODY PARSER
body-parser模块:用来接收客户端的JSON数据,在http.js中require它,在中间链开头添加app.use(bodyParser.json()),因为必须在其他中间件处理之前先处理HTTP消息的body部分。
支持其他的HTTP动作
为routes/actuators.js中的LED添加PUT支持
router.route('/leds/:id').get(function (req, res, next) { //#A
req.result = resources.pi.actuators.leds[req.params.id];
next();
}).put(function(req, res, next) { //#B
var selectedLed = resources.pi.actuators.leds[req.params.id];
selectedLed.value = req.body.value; //#C
req.result = selectedLed;
next();
});
将执行器绑定到服务器
/pligins/internal/ledsPlugin.js : 用于执行用户修改更新硬件状态。
7.2.5 通过WebSocket实现pub/sub接口
启动WebSocket服务用于监听HTTP服务器上面升级为WebSocket协议的请求
// HTTP Server
var server = httpServer.listen(resources.pi.port, function () {
console.log('HTTP server started...');
// Websockets server
wsServer.listen(server);
console.info('Your WoT Pi is up and running on port %s', resources.pi.port);
});
7.2.6 小结——直接集成模式
这小节我们直接在真实的设备上面构建WOT服务器,并且实现一些高级的功能如内容协商和WebSocket推送功能,也有大部分设备无法运行Node原生环境,后面会介绍另一种模式来支持非HTTP/WebSocket设备,即网关集成模式。
7.3.1 运行一个CoAP服务器
npm install coap
var coap = require('coap')
coap.createServer
github 上面的一个tiny coap server <3>
7.3.2 通过网关代理CoAP
我们知道,CoAP基于REST,但是由于它不是使用HTTP而是使用UDP代替TCP的,因此需要一个网关将CoAP消息转换成HTTP,它是理想的设备到设备低功耗无线电通信方式。我们需要为CoAP设备创建WOT网关,才能通过浏览器运行javascript和CoAP设备通信。
COAP技术门户网站<4>
为CoAP设备构建通用的HTTP代理<5>
7.3.3 小结——网关集成模式
对于一些设备来说,直接支持HTTP或者WebSocket是不太现实的,需要依靠更强大的网关来连接到万维物联网。除了Express外,还有其他开源的可选网关,比如OpenHab或The Thing System。
OpenHab<5>
The Thing System<6>
云服务不仅能将智能产品的API通过HTTP和WebSocket暴露出来,还能提供很多额外的特性,如无限制的数据存储、用户管理界面、数据可视化、流处理、支持多种并发请求等。
云服务提供商
Xively<1>
ThingWorx<2>
ThingSpeak<3>
Carriots<4>
thethings.io<5>
7.4.1设置EVRYTHNG账号
步骤1——创建项目和应用程序
步骤2——创建第一个产品和智能物件
步骤3——创建设备API KEY
步骤4——改变属性
7.4.2 创建MQTT客户端应用程序
代码路径:chapter7/part3-clound
cp config-sample.json config.json
change "thngId":"U5ngAdV6HfQQmpwaw2ywcfWt"
"thngApiKey":"6iVK8UUzwOlvdtQYfqwLjYmAVWkEbhhusbGEFIPwFN2fzfzDjASkpBbUHF9q1iG6ulNsqkdM7Wn4ENUN"
npm install
node simple-plug.js
成功运行MQTT客户端打印如下,该客户端与云引擎建立一个持久的连接,这个连接基于MQTT,每隔5秒更新一次属性值。
同时登陆EVEYTHNG平台,以下Thngs相关的Properties会同步刷新。
思考:MQTT推送数据改变云引擎模型中的数据,那么这个云引擎是如何告知浏览器javascript的呢?websocket吗?回想前面我们学习的,WebSocket:用来推送消息的实质的Web协议。
7.4.3 使用action来控制智能插座
上面成功运行MQTT客户端打印其实在一个回调函数中打印的,这个客户端的回调函数将会在EVRYTHNG云端属性发生变化时被调用。代码逻辑如下:
client.on('message', function (topic, message) { // #G
console.log(message.toString());
});
我们可以修改这里的逻辑,在云端属性变化的时候去触发动作,但这并不是最好的选择,因为这样必须分清那些属性是设备设置的,那些属性是应用程序里面改变的。这里我们使用action来发送更复杂的命令,可以带多个输入的参数。
首先我们要创建一个叫做_setStatus的action类型,猜测应该是管理平台的如下:
同样我们也可以使用curl来创建
curl -X POST "$SERVER/actions?project=$PROJECT_ID" \
> -H "Authorization: $EVRYTHNG_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{"name":"_setStatus","description":"Changes the status of the Thng","tags":["Wot","device"]}'
果然,以上命令执行成功后,多出如下一项,正是我们在body里面填的信息:
7.4.4 创建一个简单的Web控制应用
使用云服务创建简单的Web应用
需求:创建一个简单的web应用,使用户能够通过云端智能物件与设备交互。
使用WebSocket订阅它的属性,并在设备属性改变的时候及时显示出来;
这个应用也能通过云平台支持的REST API将命令推送到设备;
使用二维码来标识智能产品
GitHub pages 可以用来部署自己的网页或者搭建自己的网站
这里我们使用EVRYTHNG为我们提供的二维码访问地址:
https://webofthings.github.io/wot-book/plug.html?key=(your API key)&thngId=(your Thng ID)
这是我的设备二维码:
https://webofthings.github.io/wot-book/plug.html?key=6iVK8UUzwOlvdtQYfqwLjYmAVWkEbhhusbGEFIPwFN2fzfzDjASkpBbUHF9q1iG6ulNsqkdM7Wn4ENUN&thngId=U5ngAdV6HfQQmpwaw2ywcfWt
注意:前面章节是自己手敲curl命令来实现的,这些命令统一包含在part3-cloud下面的setup.sh脚本中
7.4.5 小结——云端集成模式
实时WOT产品最佳的方式是提供直接访问和云端访问两种模式。
本章讲解了物联网设备连接到万维物联网的三种模式,它们分别是:直连模式,网关模式和云服务模式。并详细介绍这三种模式的应用场景和使用的技术:Express 路由,COAP服务,MQTT客户端,Websocket订阅,action post等。在第八章我们将探索资源-链接设计步骤,实现资源的可探索与可发现。
8.2.1 网络发现(Network discovery)
常见的网络发现协议有:组播DNS(mDNS)、DLNA以及UPnP等。例如,大部分网络电视和多媒体播放器能够使用DLNA来发现局域网中的网络连接存储设备(NAS),并且从中读取媒体文件。
8.2.2 Web上的资源发现
爬取Web智能产品的API
HATEOAS和Web链接
8.3.1 Web智能产品模型简介
8.3.2 元数据
8.3.3 属性
8.3.4 行为
8.3.5 智能产品
8.3.6 在树莓派上面实现Web智能产品模型
8.3.7 小结——Web智能产品模型