JavaScript-物联网实战-全-

JavaScript 物联网实战(全)

原文:zh.annas-archive.org/md5/8F10460F1A267E7E0720699DAEDCAC44

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

我们处于一个人们已经开始适应物联网产品的时代。物联网引起了很大的热情。本书将专注于构建基于物联网的应用程序,这将帮助您在物联网方面达到更高的理解水平。它将采用基于项目的方法,教会您构建独立的令人兴奋的应用程序,并教会您将项目扩展到另一个水平。我们将使用 JavaScript 作为我们的编程语言,Raspberry Pi 3 作为我们的硬件来构建有趣的物联网解决方案。

本书涵盖的内容

第一章,物联网的世界,向您介绍了物联网的世界。我们将回顾物联网的历史,确定一些用例,并对本书将涵盖的技术进行技术概述。

第二章,IoTFW.js - I,向您介绍了如何使用 JavaScript 构建物联网解决方案的参考框架。在本章中,我们将介绍高级架构,并开始安装所需的软件。我们将从下载基本应用程序开始,并将树莓派与 MQTTS 代理和 API 引擎连接在一起。

第三章,IoTFW.js - II,从上一章结束的地方继续,并完成了 API 引擎、Web 应用程序、桌面应用程序和移动应用程序的实现。在本章末尾,我们将使用 LED 和温度传感器实现一个简单的示例,应用程序的指令将打开/关闭 LED,并且温度传感器的数值将实时更新。

第四章,智能农业,讨论了使用我们构建的参考架构来构建一个简单的气象站。气象站由四个传感器组成,使用这些传感器我们可以监测农场条件。我们将对 API 引擎、Web 应用程序、桌面应用程序和移动应用程序进行必要的更改。

第五章,智能农业和语音 AI,展示了我们如何利用语音 AI 技术来构建有趣的物联网解决方案。我们将与智能气象站一起工作,并向该设置添加一个单通道机械继电器。然后,使用语音命令和亚马逊 Alexa,我们将管理气象站。

第六章,智能可穿戴设备,讨论了医疗保健领域中一个有趣的用例,术后患者护理。使用配有简单加速度计的智能可穿戴设备,可以轻松检测患者是否摔倒。在本章中,我们构建了所需的设置来收集传感器的加速度计值。

第七章,智能可穿戴设备和 IFTTT,解释了从加速度计收集的数据如何用于检测摔倒,并同时通知 API 引擎。使用一个名为If This Then ThatIFTTT)的流行概念,我们将构建自己的规则引擎,根据预定义的规则采取行动。在我们的示例中,如果检测到摔倒,我们将向患者的照料者发送电子邮件。

第八章,树莓派图像流,展示了如何利用树莓派摄像头模块构建实时图像流(MJPEG 技术)解决方案,以监视您的周围环境。我们还将实现基于运动的视频捕获,以在检测到运动时捕获视频。

第九章,智能监控,将带您了解如何使用亚马逊的 Rekognition 平台进行图像识别。我们将在检测到运动时使用树莓派 3 相机模块捕获图像。然后,我们将把这张图片发送到亚马逊的 Rekognition 平台,以便检测我们拍摄的图片是入侵者还是我们认识的人。

本书所需内容

要开始使用 JavaScript 构建物联网解决方案,您需要具备以下知识:

  • JavaScript 的中级到高级知识-ES5 和 ES6

  • MEAN 堆栈应用程序开发的中级到高级知识

  • Angular 4 的中级到高级知识

  • Electron Framework 的中级到高级知识

  • Ionic Framework 3 的中级到高级知识

  • 数字电子电路的初级到中级知识

  • 树莓派的初级到中级知识

  • 传感器和执行器的初级到中级知识

本书适合对象

本书适合那些已经精通 JavaScript 并希望将其 JavaScript 知识扩展到物联网领域的读者。对于有兴趣创建令人兴奋的项目的物联网爱好者,本书也会很有用。本书还适合那些擅长使用树莓派开发独立解决方案的读者;本书将帮助他们利用世界上最被误解的编程语言为其现有项目添加物联网功能。

约定

在本书中,您会发现一些不同种类信息的文本样式。以下是一些这些样式的示例和它们的含义解释。文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名会以以下形式显示:“现在,在broker文件夹内,创建另一个名为certs的文件夹,并cd进入该文件夹。”代码块设置如下:

// MongoDB connection options
    mongo: {
        uri: 'mongodb://admin:admin123@ds241055.mlab.com:41055/iotfwjs'
    },

    mqtt: {
        host: process.env.EMQTT_HOST || '127.0.0.1',
        clientId: 'API_Server_Dev',
        port: 8883
    }
};

任何命令行输入或输出都以以下形式书写:

openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem 

新术语重要词汇以粗体显示。您在屏幕上看到的词语,例如菜单或对话框中的词语,会以这样的形式出现在文本中:“登录后,点击“创建新”按钮来创建一个新的数据库。”

警告或重要提示会出现在这样的形式。提示和技巧会以这种形式出现。

第一章:物联网的世界

欢迎来到使用 JavaScript 的高级物联网。在本书中,我们将探讨使用 JavaScript 作为编程语言构建物联网解决方案。在我们开始技术深入之前,我想谈谈物联网的世界,它提供的解决方案,以及我们作为开发人员所承担的责任。在本章中,我们将讨论以下主题:

  • 物联网的世界

  • 物联网的历史

  • 物联网使用案例

  • 技术概述

  • 产品工程

物联网的世界

想象一种情景,你的牛奶用完了;你注意到了并把它放在了购物清单上。但由于不可预见的原因,你忘了买牛奶;嗯,第二天你就没有牛奶了。

现在想象另一种情景:你有一个智能冰箱,它注意到你的牛奶快用完了,把牛奶放在你的购物清单上,然后更新你的 GPS 路线,让你经过超市回家,但你还是忘了。

现在你必须面对你的冰箱的愤怒。

现在事情变得真实起来,想象另一种情况,你的冰箱跳过了中间商,直接在亚马逊上下订单,亚马逊在你第二天早餐需要的时候送货。

第三种情景是我们追求的。让一台机器与另一台机器交流并做出相应决策;在购买之前自动验证牛奶的类型、数量和过期日期等事项。

我们人类现在正在利用连接设备和智能设备的世界来让我们的生活变得更好。

什么是物联网?

如果你至少呼吸了十年,你一定听过智能生活、智能空间和智能设备等术语。所有这些都指的是一个称为物联网IoT)的父概念。

简而言之,当我们的电子、电气或电机械设备连接到互联网并相互交流时,就是物联网。

智能设备主要围绕两件事展开:

  • 传感器

  • 执行器

物联网领域的任何解决方案都是要么感知某物,要么执行某事。

有了这项技术,我们为谢尔顿·库珀(来自 CBS 电视系列剧《生活大爆炸》)找到了解决方案,他想知道有人坐在他的位置上时立刻得知:

来源:http://bigbangtheory.wikia.com/wiki/Sheldon's_Spot

我们所做的就是在沙发下放一个重量传感器,如果重量增加,传感器将触发指向沙发的摄像头拍照并发送一张照片的推送通知给他。怎么样?

我知道我举的例子有点过分,但你明白我的意思,对吧?

一点历史

物联网以各种形式存在已有 35 年以上。我找到的最早的例子是 1982 年在卡内基梅隆大学的一台可乐机。由四名研究生迈克·卡扎尔、大卫·尼科尔斯、约翰·扎尔纳伊和艾沃尔·德勒姆开发,他们将可乐机连接到互联网,这样他们就可以从自己的办公桌上检查机器里是否装满了冷可乐。来源(www.cs.cmu.edu/~coke/)。

蒂莫西·约翰·伯纳斯-李爵士于 1991 年发明了第一个网页。

另一个例子是约翰·罗姆基的互联网烤面包机。他使用 TCP/IP 协议将他的烤面包机连接到互联网。他创建了一个控制来打开烤面包机,一个控制来关闭它。当然,有人必须把面包放进烤面包机:

来源:http://ieeexplore.ieee.org/document/7786805/

另一个有趣的物联网例子是特洛伊房间咖啡壶。这是由昆汀·斯塔福德-弗雷泽和保罗·贾德茨基于 1993 年创建的。一台摄像机位于剑桥大学计算机实验室的特洛伊房间,监视着咖啡壶的水平,每分钟更新一次图像并发送到建筑的服务器:

来源:https://en.wikipedia.org/wiki/Trojan_Room_coffee_pot

正如之前提到的,我们可以看到,甚至在我们能够想象到可能性之前,人们已经在互联网相关的解决方案上进行了工作。

在过去的两年里,有一件事情让我一直看到并开始坚信:

“懒惰是发明之母。”

不是必要性,也不是无聊,而是懒惰。在当今时代,没有人想做像购物、走到开关旁边打开灯或空调这样的日常事务。因此,我们正在寻找解决这些问题的新颖创新的方法。

物联网用例

现在您对物联网有了一定的了解,您可以想象使用这种技术可以构建的无限可能性。

根据我的观察,物联网用例可以粗略地分为三个部分:

  • 解决问题

  • 便利

  • 炫耀

问题解决部分涉及物联网用于解决现实世界问题,例如,一个农民的农场距离家有半公里,他们必须一直走到农场才能打开水泵/发动机。另一个场景是术后患者出院后可以定期将其生命体征发送到医院,以监测患者是否有任何异常。这正是物联网非常适合的地方。

便利是指您可以在到达家之前 30 分钟打开空调,这样您进入时就可以放松,或者在工作时解锁您的门,如果您认识的人敲门而您不在附近。

炫耀是指您去另一个国家只是为了打开或关闭门廊灯,只是为了展示物联网的工作。

所有这些都是对这种技术的消费形式。

在本书中,我们将探讨一些属于先前用例的解决方案。

技术概述

现在我们知道了什么是物联网,我们可以开始定义技术堆栈。在本书中,我们将使用 JavaScript 构建一个通用框架,用于开发物联网应用程序。

我们将遵循云计算的方法,其中有一堆连接到云的设备,与雾计算方法相比,后者有一个网关,几乎可以做云可以做的所有事情,但是在本地可用于现场。

我们的智能设备将由树莓派 3 供电,它具有通过 Wi-Fi 与云进行通信的能力,并且可以使用其 GPIO 引脚与传感器和执行器进行通信。使用这个简单的硬件,我们将在本书中连接传感器和执行器,并构建一些真实世界的解决方案。

另一个替代品是树莓派 Zero W,这是树莓派 3 的微型版本,如果您想构建一个紧凑的解决方案。

我们将在第二章 IoTFW.js - I和第三章 IoTFW.js - II中逐步介绍每一种技术,并从那里开始使用这些技术在各个领域构建物联网解决方案。

产品工程

与软件开发不同,硬件开发确实很难。所花费的时间、复杂性和执行都很昂贵。想象一下 JavaScript 控制台中的语法错误;我们只需要转到特定的行号,进行更改,然后刷新浏览器。

现在将这与硬件产品开发进行比较。从确定一件硬件产品到将其作为收缩包装产品放在超市货架上,至少需要 8 个月的时间,至少需要制作四个产品迭代,以验证并在现实世界中测试它。

举个例子,产品上的组件定位会使产品成功或失败。想象一下如果充电器插头上没有凸起或握把;您在从插座中拔出充电器时手会一直滑动。这就是价值工程。

组建一个概念验证POC)非常简单,正如您将在本书的其余部分中看到的那样。将这个 POC 转变成一个成品产品是完全不同的事情。这种区别就像在你的浴室里唱歌和在有数百万观众的舞台上唱歌一样。

请记住,我们在本书中构建的示例都是 POC,并且没有一个远远接近用于产品生产。您可以始终使用我们在本书中将要研究的解决方案来更好地理解实施,然后围绕它们设计自己的解决方案。

总结

在本章中,我们看了一下什么是物联网以及一些相关历史。接下来,我们看了一些用例,一个高层次的技术概述,以及一些关于产品工程的内容。

在第二章中,IoTFW.js - I,我们将开始构建物联网框架,我们将在其上构建我们的解决方案。

第二章:IoTFW.js - I

在本章和第三章,IoTFW.js - II中,我们将开发一个用于构建各种物联网解决方案的参考架构。该参考架构或物联网框架将作为我们未来在本书中要开发的物联网解决方案的基础。我们将称这个参考架构或框架为 IoTFW.js。我们将致力于以下主题,以使 IoTFW.js 生动起来:

  • 设计 IoTFW.js 架构

  • 开发基于 Node.js 的服务器端层

  • 开发一个基于 Angular 4 的 Web 应用程序

  • 开发一个基于 Ionic 3 的移动应用程序

  • 开发一个基于 Angular 4 和 Electron.js 的桌面应用程序

  • 在树莓派 3 上设置和安装所需的依赖关系

  • 整合所有的部分

我们将在本章中涵盖一些先前的主题,以及第三章,IoTFW.js - II中的一些主题。

设计一个参考架构

正如我们在第一章,物联网的世界中所看到的,我们将要处理的所有示例都有一个共同的设置。那就是硬件、固件(在硬件上运行的软件)、代理、API 引擎和用户应用程序。

我们将在遇到相关框架的部分时进行扩展。

在需要时,我们将扩展硬件、移动应用程序或 API 引擎。

通过这个参考架构,我们将在现实世界中的设备与虚拟世界中的云之间建立一个管道。换句话说,物联网是设备和互联网之间的最后一英里解决方案。

架构

使用树莓派、Wi-Fi 网关、云引擎和用户界面应用程序组合在一起的简单参考架构如下图所示:

在非常高的层面上,我们在左侧有智能设备,在右侧有用户设备。它们之间的所有通信都通过云端进行。

以下是先前架构中每个关键实体的描述。我们将从左侧开始,向右移动。

智能设备

智能设备是硬件实体,包括传感器、执行器或两者,任何微控制器或微处理器,在我们的案例中是树莓派 3。

传感器是一种可以感知或测量物理特性并将其传回微控制器或微处理器的电子元件。传回的数据可以是周期性的或事件驱动的;事件驱动的意思是只有在数据发生变化时才会传回。温度传感器,如 LM35 或 DHT11,就是传感器的一个例子。

执行器也是可以触发现实世界中动作的电机械组件。通常,执行器不会自行行动。微控制器、微处理器或电子逻辑发送信号给执行器。执行器的一个例子是机械继电器。

我们在本书中提到的微处理器将是树莓派 3。

树莓派 3 是由树莓派基金会设计和开发的单板计算机。树莓派 3 是第三代树莓派。

在本书中,我们将使用树莓派 3 型 B 来进行所有示例。树莓派 3 型 B 的一些规格如下:

特性 规格
3
发布日期 2016 年 2 月
架构 ARMv8-A(64/32 位)
系统芯片(SoC) Broadcom BCM2837
CPU 1.2 GHz 64 位四核 ARM Cortex-A53
内存(SDRAM) 1 GB(与 GPU 共享)
USB 2.0 端口 4(通过机载 5 端口 USB 集线器)
机载网络 10/100 兆比特/秒以太网,802.11n 无线,蓝牙 4.1
低级外围设备 17× GPIO 加上相同的特定功能,和 HAT ID 总线
功率评级 空闲时平均 300 毫安(1.5 瓦),在压力下最大 1.34 安(6.7 瓦)(监视器、键盘、鼠标和 Wi-Fi 连接)
电源 通过 MicroUSB 或 GPIO 引脚的 5V

有关规格的更多信息,请参考树莓派的规格:en.wikipedia.org/wiki/Raspberry_Pi#Specifications

网关

我们架构中的下一个部分是 Wi-Fi 路由器。一个普通的家用 Wi-Fi 路由器将作为我们的网关。正如我们在第一章中所看到的,在物联网的世界中,集群设备与独立设备部分,我们遵循独立设备的方法,其中每个设备都是自给自足的,并且有自己的无线电与外界通信。我们将要构建的所有项目都包括一个树莓派 3,它具有微处理器以及与传感器和执行器进行通信的无线电。

MQTTS 代理

我们参考框架中的下一个重要部分是设备和云之间的安全通信通道。我们将使用 MQTT 作为我们的通信通道。MQTT 在以下引用中描述:mqtt.org/faq

MQTT 代表 MQ 遥测传输。这是一种发布/订阅、非常简单和轻量级的消息传递协议,设计用于受限设备和低带宽、高延迟或不可靠网络。设计原则是尽量减少网络带宽和设备资源需求,同时也尝试确保可靠性和一定程度的传递保证。

我们将使用 MQTT over SSL 或 MQTTS。在我们的架构中,我们将使用 Mosca(www.mosca.io/)作为我们的 MQTTS 代理。Mosca 是一个 Node.js MQTT 代理。当我们开始使用它时,我们将更多地谈论 Mosca。

API 引擎

API 引擎是一个基于 Node.js、Express 编写的 Web 服务器应用,具有 MongoDB 作为持久化层。该引擎负责与 Mosca 通信作为 MQTT 客户端,将数据持久化到 MongoDB,并使用 Express 公开 API。然后应用程序使用这些 API 来显示数据。

我们还将实现基于套接字的 API,用于用户界面在设备和服务器之间实时获取通知。

MongoDB

我们将使用 MongoDB 作为我们的数据持久化层。MongoDB 是一个 NoSQL 文档数据库,允许我们在一个集合中保存具有不同模式的文档。这种数据库非常适合处理来自各种设备的传感器数据,因为数据结构或参数因解决方案而异。要了解有关 MongoDB 的更多信息,请参阅www.mongodb.com/

Web 应用

Web 应用是一个简单的 Web/移动 Web 界面,将实现 API 引擎公开的 API。这些 API 将包括身份验证,访问特定的智能设备,获取智能设备的最新数据,并通过 API 将数据发送回智能设备。我们将使用 Angular 4(angular.io/)和 Twitter Bootstrap 3(getbootstrap.com/)技术来构建 Web 应用。

移动应用

我们将采用混合移动方法来构建我们的移动应用。移动应用实现了 API 引擎公开的 API。这些 API 将包括身份验证,访问特定的智能设备,获取智能设备的最新数据,并通过 API 将数据发送回智能设备。我们将使用 Ionic 3(ionicframework.com/),它由 Angular 4 提供支持,来构建移动应用。

桌面应用

我们将采用桌面混合方法来构建我们的桌面应用程序。桌面应用程序将实现 API 引擎提供的 API。这些 API 将包括身份验证,访问特定的智能设备,从智能设备获取最新数据,并通过 API 将数据发送回智能设备。我们将使用 Electron (electron.atom.io/)作为构建桌面应用程序的外壳。我们将使用 Angular 4 和 Twitter Bootstrap 3 (getbootstrap.com/)技术来构建桌面应用程序。我们会尽量在 Web 和桌面应用程序之间重用尽可能多的代码。

数据流

现在我们了解了架构的各个部分,我们现在将看一下组件之间的数据流。我们将讨论从智能设备到应用程序以及反之的数据流。

智能设备到应用程序

从传感器到用户设备的简单数据流程如下:

从上图可以看出,数据源自传感器;树莓派 3 读取这些数据,并通过 Wi-Fi 路由器将数据发布到 MQTTS 代理(Mosca)。一旦代理接收到数据,它将把数据发送到 API 引擎,API 引擎将数据持久化到数据库中。数据成功保存后,API 引擎将把新数据发送到我们的应用程序,以实时显示数据。

这里需要注意的一点是,API 引擎将充当 MQTT 客户端,并订阅设备发布数据的主题。我们将在实施时查看这些主题。

一般来说,在这种流程中的数据将是典型的传感器传输数据。

应用程序到智能设备

以下图表显示了数据如何从应用程序流向智能设备:

从上图可以看出,如果应用程序希望向智能设备发送指令,它会将该消息发送到 API 引擎。然后 API 引擎将数据持久化到数据库,并将数据发布到 MQTTS 代理,以传递给设备。设备将根据这些数据做出反应。

请注意,在这两种流程中,MQTTS 代理管理设备,API 引擎管理应用程序。

构建参考架构

在本节中,我们将开始组合所有部件并组合所需的设置。我们将从 Node.js 安装开始,然后是数据库,之后,继续其他部件。

在服务器上安装 Node.js

在继续开发之前,我们需要在服务器上安装 Node.js。这里的服务器可以是您自己的台式机、笔记本电脑、AWS 机器,或者是一个可能具有或不具有公共 IP 的 digitalocean 实例(www.iplocation.net/public-vs-private-ip-address)。

安装 Node.js,转到nodejs.org/en/并下载适合您的机器的版本。安装完成后,您可以通过从命令提示符/终端运行以下命令来测试安装:

node -v
# v6.10.1

npm -v
# 3.10.10  

您可能拥有比之前显示的版本更新的版本。

现在我们有了所需的软件,我们将继续。

安装 nodemon

现在我们已经安装了 Node.js,我们将安装 nodemon。这将负责自动重新启动我们的节点应用程序。运行:

npm install -g nodemon

MongoDB

您可以按照以下列出的两种方式之一设置数据库。

本地安装

我们可以在服务器上设置 MongoDB 作为独立安装。这样,数据库就在服务器上运行,并且数据会持久保存在那里。

根据您的操作系统,您可以按照提供的说明在docs.mongodb.com/manual/installation/上设置数据库。

安装完数据库后,为了测试一切是否正常工作,您可以打开一个新的终端,并通过运行以下命令启动 Mongo 守护进程:

mongod  

您应该看到类似于以下内容:

我正在使用默认端口27017运行数据库。

现在我们将使用 mongo shell 与数据库进行交互。打开一个新的命令提示符/终端,并运行以下命令:

mongo  

这将带我们进入mongo shell,通过它我们可以与 MongoDB 进行交互。以下是一些方便的命令:

描述 命令
显示所有数据库 show dbs
使用特定数据库 use local
创建数据库 use testdb
检查正在使用的数据库 db
创建集合 db.createCollection("user");
显示数据库中的所有集合 show collections
(创建)在集合中插入文档 db.user.insert({"name":"arvind"});
(读取)查询集合 db.user.find({});
(更新)修改集合中的文档 db.user.update({"name": "arvind"}, {"name" : "arvind2"}, {"upsert":true});
(删除)删除文档 db.user.remove({"name": "arvind2"});

使用前面的命令,您可以熟悉 Mongo shell。在我们的 API 引擎中,我们将使用 Mongoose ODM(mongoosejs.com/)来管理 Node.js/Express--API 引擎。

使用 mLab

如果您不想费心在本地设置数据库,可以使用 mLab(mlab.com/)等 MongoDB 作为服务。在本书中,我将遵循这种方法。我将使用 mLab 的实例,而不是本地数据库。

要设置 mLab MongoDB 实例,首先转到mlab.com/login/并登录。如果您没有帐户,可以通过转到mlab.com/signup/来创建一个。

mLab 有一个免费的层,我们将利用它来构建我们的参考架构。免费层非常适合像我们这样的开发和原型项目。一旦我们完成了实际的开发,并且准备好生产级应用程序,我们可以考虑一些更可靠的计划。您可以在mlab.com/plans/pricing/上了解定价。

一旦您登录,单击“创建新”按钮以创建新的数据库。现在,在云提供商下选择亚马逊网络服务,然后选择计划类型为 FREE,如下图所示:

最后,将数据库命名为iotfwjs,然后单击“创建”。然后,几秒钟后,我们应该为我们创建一个新的 MongoDB 实例。

一旦数据库创建完成,打开iotfwjs数据库。我们应该看到一些警告:一个是指出这个沙箱数据库不应该用于生产,我们知道这一点,另一个是没有数据库用户存在。

所以,让我们继续创建一个。单击“用户”选项卡,然后单击“添加数据库用户”按钮,并使用用户名admin和密码admin123填写表单,如下所示:

您可以选择自己的用户名和密码,并相应地在本书的其余部分进行更新。

现在来测试连接到我们的数据库,使用页面顶部的部分使用mongo shell 进行连接。在我的情况下,如下所示:

打开一个新的命令提示符,并运行以下命令(在相应地更新 mLab URL 和凭据之后):

mongo ds241055.mlab.com:41055/iotfwjs -u admin -p admin123  

我们应该能够登录到 shell,并可以从这里运行查询,如下所示:

这完成了我们的 MongoDB 设置。

MQTTS 代理 - Mosca

在本节中,我们将组装 MQTTS 代理。我们将使用 Mosca (www.mosca.io/)作为独立服务 (github.com/mcollina/mosca/wiki/Mosca-as-a-standalone-service)。

创建一个名为chapter2的新文件夹。在chapter2文件夹内,创建一个名为broker的新文件夹,并在该文件夹内打开一个新的命令提示符/终端。然后运行以下命令:

npm install mosca pino -g  

这将全局安装 Mosca 和 Pino。Pino (github.com/pinojs/pino)是一个 Node.js 日志记录器,它记录 Mosca 抛出的所有消息到控制台。

现在,Mosca 的默认版本实现了 MQTT。但我们希望在智能设备和云之间保护我们的通信,以避免中间人攻击。

因此,为了设置 MQTTS,我们需要 SSL 密钥和 SSL 证书。为了在本地创建 SSL 密钥和证书,我们将使用openssl

要检查您的计算机上是否存在openssl,运行openssl version -a,您应该看到关于您的openssl本地安装的信息。

如果您没有openssl,您可以从www.openssl.org/source/下载。

现在,在broker文件夹内,创建一个名为certs的文件夹,并cd进入该文件夹。运行以下命令生成所需的密钥和证书文件:

openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem  

这将提示一些问题,您可以按照以下方式填写相同的内容:

这将在certs文件夹内创建两个名为key.pemcertificate.pem的新文件。我们将在我们的 Mosca 设置中使用这些文件。

接下来,在broker文件夹的根目录下,创建一个名为index.js的新文件,并按以下方式更新它:

let SSL_KEY = __dirname + '/certs/key.pem';
let SSL_CERT = __dirname + '/certs/certificate.pem';
let MONGOURL = 'mongodb://admin:admin123@ds241055.mlab.com:41055/iotfwjs';

module.exports = {
    id: 'broker',
    stats: false,
    port: 8443,
    logger: {
        name: 'iotfwjs',
        level: 'debug'
    },
    secure: {
        keyPath: SSL_KEY,
        certPath: SSL_CERT,
    },
    backend: {
        type: 'mongodb',
        url: MONGOURL
    },
    persistence: {
        factory: 'mongo',
        url: MONGOURL
    }
};

前面的代码是我们将用于启动 Mosca 的配置。此配置加载 SSL 证书和密钥,并将 Mongo 设置为我们的持久层。

保存index.js,然后返回到终端/提示符,并cd到我们有index.js文件的位置。接下来,运行以下命令:

mosca -c index.js -v | pino  

我们应该看到以下内容:

正如您从前面看到的,我们连接到iotfwjs数据库,代理将监听端口8883以进行连接。

这完成了我们使用 Mosca 设置 MQTTS 代理的设置。

在下一步中,我们将实现 API 引擎,此时我们将测试 MQTTS 代理与 API 引擎的集成。

API 引擎 - Node.js 和 Express

在本节中,我们将构建 API 引擎。该引擎与我们的应用程序进行交互,并将智能设备的信息从和到代理进行级联连接。

要开始,我们将克隆一个使用 Yeoman (yeoman.io/)生成器创建的存储库,名为generator-node-express-mongo (www.npmjs.com/package/generator-node-express-mongo)。我们已经使用generator-node-express-mongo生成的代码并根据我们的需求进行了修改。

在您的计算机上的某个位置,使用以下命令下载本书的完整代码库:

git clone https://github.com/PacktPublishing/Practical-Internet-of-Things-with-JavaScript.git

或者,您也可以从github.com/PacktPublishing/Practical-Internet-of-Things-with-JavaScript下载 zip 文件。

一旦存储库被下载,cd进入base文件夹,并将api-engine-base文件夹复制到chapter2文件夹中。

这将下载api-engine样板代码。一旦repo被克隆,cd进入文件夹并运行以下命令:

npm install  

这将安装所需的依赖项。

如果我们打开cloned文件夹,我们应该看到以下内容:

.
├── package.json
└── server
    ├── api
    │ └── user
    │ ├── index.js
    │ ├── user.controller.js
    │ ├── user.model.js
    ├── app.js
    ├── auth
    │ ├── auth.service.js
    │ ├── index.js
    │ └── local
    │ ├── index.js
    │ └── passport.js
    ├── config
    │ ├── environment
    │ │ ├── development.js
    │ │ ├── index.js
    │ │ ├── production.js
    │ │ └── test.js
    │ ├── express.js
    │ └── socketio.js
    ├── mqtt
    │ └── index.js
    └── routes.js

该文件夹包含我们启动 API 引擎所需的所有基本内容。

从前面的结构中可以看出,我们在文件夹的根目录中有一个package.json。这个文件包含了所有需要的依赖项。我们还在这里定义了我们的启动脚本。

我们的所有应用程序文件都位于server文件夹中。一切都始于api-engine/server/app.js。我们初始化mongooseexpresssocketioconfigroutesmqtt。最后,我们启动服务器,并在localhost9000端口上侦听,借助server.listen()

api-engine/server/config/express.js具有初始化 Express 中间件所需的设置。api-engine/server/config/socketio.js包含管理 Web 套接字所需的逻辑。

我们将使用api-engine/server/config/environment来配置环境变量。在大部分的书中,我们将使用开发环境。如果我们打开api-engine/server/config/environment/development.js,我们应该看到mongomqtt的配置。更新它们如下:

// MongoDB connection options
    mongo: {
        uri: 'mongodb://admin:admin123@ds241055.mlab.com:41055/iotfwjs'
    },

    mqtt: {
        host: process.env.EMQTT_HOST || '127.0.0.1',
        clientId: 'API_Server_Dev',
        port: 8883
    }
};

根据您的设置更新 mongo URL(mLab 或本地)。由于我们将连接到在本地计算机上运行的 Mosca 代理,我们使用127.0.0.1作为主机。

授权

接下来,我们将看看开箱即用的身份验证。我们将使用JSON Web TokensJWTs)来验证要与我们的 API 引擎通信的客户端。我们将使用 Passport(passportjs.org/)进行身份验证。

打开api-engine/server/auth/index.js,我们应该看到使用require('./local/passport').setup(User, config);设置护照,并且我们正在创建一个新的身份验证路由。

路由在api-engine/server/routes.js中配置。如果我们打开api-engine/server/routes.js,我们应该看到app.use('/auth', require('./auth'));。这将创建一个名为/auth的新端点,在api-engine/server/auth/index.js中,我们已经添加了router.use('/local', require('./local'));现在,如果我们想要访问api-engine/server/auth/local/index.js中的POST方法,我们将向/auth/local发出 HTTP POST请求。

api-engine中,我们使用护照本地认证策略(github.com/jaredhanson/passport-local)来使用 MongoDB 进行持久化验证用户。

要创建新用户,我们将使用用户 API。如果我们打开api-engine/server/routes.js,我们应该看到定义了一个路由来访问用户集合app.use('/api/v1/users', require('./api/user'));。我们已经添加了/api/v1/users前缀,以便稍后可以对我们的 API 层进行版本控制。

如果我们打开api-engine/server/api/user/index.js,我们应该看到定义了以下六个路由:

  • router.get('/', auth.hasRole('admin'), controller.index);

  • router.delete('/:id', auth.hasRole('admin'), controller.destroy);

  • router.get('/me', auth.isAuthenticated(), controller.me);

  • router.put('/:id/password', auth.isAuthenticated(), controller.changePassword);

  • router.get('/:id', auth.isAuthenticated(), controller.show);

  • router.post('/', controller.create);

第一个路由是用于获取数据库中所有用户的路由,并使用api-engine/server/auth/auth.service.js中定义的auth.hasRole中间件,我们将检查用户是否经过身份验证并具有管理员角色。

下一步是删除具有 ID 的用户的路由;之后,我们有一个根据令牌获取用户信息的路由。我们有一个PUT路由来更新用户的信息;一个GET路由根据用户 ID 获取用户的信息;最后,一个POST路由来创建用户。请注意,POST路由没有任何身份验证或授权中间件,因为访问此端点的用户将是第一次使用我们的应用程序(或者正在尝试注册)。

使用POST路由,我们将创建一个新用户;这就是我们注册用户的方式:api-engine/server/api/user/user.model.js包含了用户的 Mongoose 模式,api-engine/server/api/user/user.controller.js包含了我们定义的路由的逻辑。

MQTT 客户端

最后,我们将看一下 MQTT 客户端与我们的api-engine的集成。如果我们打开api-engine/server/mqtt/index.js,我们应该会看到 MQTTS 客户端的默认设置。

我们使用以下配置来连接到 MQTTS 上的 Mosca 经纪人:

var client = mqtt.connect({
    port: config.mqtt.port,
    protocol: 'mqtts',
    host: config.mqtt.host,
    clientId: config.mqtt.clientId,
    reconnectPeriod: 1000,
    username: config.mqtt.clientId,
    password: config.mqtt.clientId,
    keepalive: 300,
    rejectUnauthorized: false
});

我们正在订阅两个事件:一个是在连接建立时,另一个是在接收消息时。在connect事件上,我们订阅了一个名为greet的主题,并在下一行发布了一个简单的消息到该主题。在message事件上,我们正在监听来自经纪人的任何消息,并打印主题和消息。

通过这样,我们已经了解了与api-engine一起工作所需的大部分代码。要启动api-enginecd进入chapter2/api-engine文件夹,并运行以下命令:

npm start  

这将在端口9000上启动一个新的 Express 服务器应用程序。

API 引擎测试

为了快速检查我们创建的 API,我们将使用一个名为 Postman 的 Chrome 扩展。您可以从这里设置 Chrome 扩展:chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en

一旦 Postman 设置好,我们将测试两个 API 调用以验证注册和登录方法。

打开 Postman,并输入请求的 URL 为http://localhost:9000/api/v1/users。接下来,选择方法类型为POST。完成后,我们将设置标头。添加一个新的标头,键为content-type,值为application/json

现在我们将构建请求体/有效载荷。点击 Headers 旁边的 Body 选项卡,选择 Raw request。并更新为以下内容:

{ 
   "email" : "arvind@myapp.com", 
   "password" : "123456", 
   "name" : "Arvind" 
} 

您可以根据需要更新数据。然后点击发送。这将向 API 引擎发出请求,API 引擎将将数据保存到数据库,并以新用户对象和授权令牌作出响应。

我们的输出应该如下:

现在,如果我们再次点击发送按钮并使用相同的数据,我们应该会看到一个验证错误,类似于以下内容:

现在,为了验证新注册的用户,我们将向http://localhost:9000/auth/local发出请求,只包含电子邮件和密码。我们应该会看到类似以下内容的东西:

这验证了我们创建的 API。

通过这样,我们完成了 API 引擎的演练。在下一节中,我们将集成api-engine与经纪人,并测试它们之间的连接。

经纪人和 API 引擎之间的通信

现在我们在云上完成了这两个软件,我们将对它们进行接口化。在api-engine/server/config/environment/development.js中,我们已经定义了api-engine需要连接到的经纪人 IP 和端口。

稍后,如果我们将这两个部分部署到不同的机器上,这就是我们更新 IP 和端口的地方,以便api-engine引用经纪人。

现在,为了测试通信,cd进入chapter2/broker文件夹,并运行以下命令:

mosca -c index.js -v | pino  

我们应该会看到以下内容:

接下来,打开一个新的命令提示符/终端,cd进入chapter2/api-engine文件夹,并运行以下命令:

npm start 

我们应该会看到以下内容:

API 引擎连接到 mLab MongoDB 实例,然后启动了一个新的 Express 服务器,最后连接到了 Mosca 经纪人,并发布了一条消息到 greet 主题。

现在,如果我们查看 Mosca 终端,我们应该会看到以下内容:

经纪人记录了迄今为止发生的活动。一个客户端连接了用户名API_Server_Dev并订阅了一个名为 greet 的主题,服务质量QoS)为0

有了这个,我们的经纪人和 API 引擎之间的集成就完成了。

接下来,我们将转向 Raspberry Pi 3,并开始使用 MQTTS 客户端。

如果您对 MQTT 协议不熟悉,可以参考MQTT Essentials: Part 1 - Introducing MQTT (www.hivemq.com/blog/mqtt-essentials-part-1-introducing-mqtt)以及后续部分。要了解更多关于 QoS 的信息,请参考MQTT Essentials Part 6: Quality of Service 0, 1 & 2 (www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels)。

树莓派软件

在这一部分,我们将构建所需的软件,使树莓派通过 Wi-Fi 路由器成为我们 Mosca 经纪人的客户端。

我们已经在数据流程图中看到了树莓派是如何站在传感器和 Mosca 经纪人之间的。现在我们将设置所需的代码和软件。

设置树莓派

在这一部分,我们将看看如何在树莓派上安装所需的软件。

树莓派安装了 Raspbian OS (www.raspberrypi.org/downloads/raspbian/)是必需的。在继续之前,Wi-Fi 应该已经设置并连接好。

如果您是第一次设置树莓派 3,请参考在树莓派上安装 Node.js 的初学者指南 (thisdavej.com/beginners-guide-to-installing-node-js-on-a-raspberry-pi/)。但是,我们将涵盖 Node.js 部分,您可以在启动 Pi 并配置 Wi-Fi 之前参考。

操作系统安装完成后,启动树莓派并登录。此时,它将通过您自己的访问点连接到互联网,您应该能够正常浏览互联网。

我正在使用 VNC Viewer 从我的 Apple MacBook Pro 访问我的树莓派 3。这样,我不会总是连接到树莓派 3。

我们将从下载 Node.js 开始。打开一个新的终端并运行以下命令:

$ sudo apt update
$ sudo apt full-upgrade 

这将升级所有需要升级的软件包。接下来,我们将安装最新版本的 Node.js。在撰写本文时,Node 7.x 是最新版本:

$ curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash -
$ sudo apt install nodejs  

这将需要一些时间来安装,一旦安装完成,您应该能够运行以下命令来查看 Node.js 和npm的版本:

node -v
npm -v  

有了这个,我们已经设置好了在 Raspberry Pi 3 上运行 MQTTS 客户端所需的软件。

树莓派 MQTTS 客户端

现在我们将使用 Node.js 的 MQTTS 客户端进行工作。

在树莓派 3 的桌面上,创建一个名为pi-client的文件夹。打开一个终端并cdpi-client文件夹。

我们要做的第一件事是创建一个package.json文件。从pi-client文件夹内部运行以下命令:

$ npm init

然后根据情况回答问题。完成后,我们将在 Raspberry Pi 3 上安装 MQTT.js (www.npmjs.com/package/mqtt)。运行以下命令:

$ npm install mqtt -save  

一旦这个安装也完成了,最终的package.json将与这个相同:

{
    "name": "pi-client",
    "version": "0.1.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "start": "node index.js"
    },
    "keywords": ["pi", "mqtts"],
    "author": "Arvind Ravulavaru",
    "private": true,
    "license": "ISC",
    "dependencies": {
        "mqtt": "².7.1"
    }
}

请注意,我们已经添加了一个启动脚本来启动我们的index.js文件。我们将在片刻之后创建index.js文件。

接下来,在pi-client文件夹的根目录下,创建一个名为config.js的文件。更新config.js如下:

module.exports = { 
    mqtt: { 
        host: '10.2.192.141', 
        clientId: 'rPI_3', 
        port: 8883 
    } 
}; 

请注意主机属性。这是设置为我的 MacBook 的 IP 地址,我的 MacBook 是我将运行 Mosca 经纪人 API 引擎的地方。确保它们三个(Mosca 经纪人、API 引擎和树莓派 3)都在同一个 Wi-Fi 网络上。

接下来,我们将编写所需的 MQTT 客户端代码。在pi-client文件夹的根目录下创建一个名为index.js的文件,并更新如下:

var config = require('./config.js'); 
var mqtt = require('mqtt') 
var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('greet') 
    client.publish('greet', 'Hello, IoTjs!') 
}); 

client.on('message', function(topic, message) { 
    // message is Buffer 
    console.log('Topic >> ', topic); 
    console.log('Message >> ', message.toString()) 
}); 

这是我们在 API 引擎上编写的相同测试代码,用于测试连接。保存所有文件并转向您的 Mosca 经纪人。

经纪人和树莓派之间的通信

在本节中,我们将通过 MQTTS 在经纪人和树莓派之间进行通信。

转到broker文件夹并运行以下命令:

mosca -c index.js -v | pino  

接下来,转到树莓派,cd进入pi-client文件夹,并运行以下命令:

$ npm start  

我们应该在树莓派上看到以下消息:

当我们查看 Mosca 的控制台时,我们应该看到以下内容:

这结束了我们在树莓派 3 和 Mosca 经纪人之间的连接测试。

故障排除

如果您无法看到以前的消息,请检查以下内容:

  • 检查树莓派和运行经纪人的机器是否在同一个 Wi-Fi 网络上

  • 检查运行经纪人的机器的 IP 地址

树莓派、经纪人和 API 引擎之间的通信

现在我们将集成树莓派、经纪人和 API 引擎,并将数据从树莓派传递到 API 引擎。

我们将实现这一点的方式是创建一个名为api-engine的主题和另一个名为rpi的主题。

要将数据从树莓派发送到 API 引擎,我们将使用api-engine主题,当我们需要将数据从 API 引擎发送到树莓派时,我们将使用rpi主题。

在这个例子中,我们将获取树莓派的 MAC 地址并将其发送到 API 引擎。API 引擎将通过将相同的 MAC 地址发送回树莓派来确认相同。API 引擎和树莓派之间的通信将在前面提到的两个主题上进行。

因此,首先,我们将按照以下方式更新api-engine/server/mqtt/index.js

var mqtt = require('mqtt'); 
var config = require('../config/environment'); 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('api-engine'); 
}); 

client.on('message', function(topic, message) { 
    // message is Buffer 
    // console.log('Topic >> ', topic); 
    // console.log('Message >> ', message.toString()); 
    if (topic === 'api-engine') { 
        var macAddress = message.toString(); 
        console.log('Mac Address >> ', macAddress); 
        client.publish('rpi', 'Got Mac Address: ' + macAddress); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

在这里,一旦建立了 MQTT 连接,我们就会订阅api-engine主题。当我们从api-engine主题接收到任何数据时,我们将把相同的数据发送回rpi主题。

broker文件夹中运行以下命令:

mosca -c index.js -v | pino  

接下来,在api-engine文件夹中运行以下命令:

npm start  

接下来,回到树莓派。我们将安装getmac模块(www.npmjs.com/package/getmac),这将帮助我们获取设备的 MAC 地址。

pi-client文件夹中运行以下命令:

$ npm install getmac --save  

完成后,更新/home/pi/Desktop/pi-client/index.js如下:

var config = require('./config.js'); 
var mqtt = require('mqtt'); 
var GetMac = require('getmac'); 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('rpi'); 
    GetMac.getMac(function(err, macAddress) { 
        if (err) throw err; 
        client.publish('api-engine', macAddress); 
    }); 
}); 

client.on('message', function(topic, message) { 
    // message is Buffer 
    // console.log('Topic >> ', topic); 
    // console.log('Message >> ', message.toString()); 
    if (topic === 'rpi') { 
        console.log('API Engine Response >> ', message.toString()); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

在以前的代码中,我们等待树莓派和经纪人之间的连接建立。一旦完成,我们就订阅了rpi主题。接下来,我们使用GetMac.getMac()获取了树莓派的 MAC 地址,并将其发布到api-engine主题。

message事件回调中,我们正在监听rpi主题。如果我们从服务器收到任何数据,它将在这里打印出来。

保存文件,并在pi-client文件夹中运行以下命令:

$ npm start  

现在,如果我们查看经纪人终端/提示,我们应该看到以下内容:

这两个设备都连接并订阅了感兴趣的主题。

接下来,如果我们查看api-engine终端/提示,我们应该看到以下内容:

最后,树莓派终端应该看起来与这个一样:

有了这个,我们完成了树莓派与经纪人和 API 引擎的集成。

在下一节中,我们将实现一个 Web 应用程序,该应用程序可以通过经纪人和 API 引擎与树莓派发送和接收数据。

Web 应用程序

在本节中,我们将构建一个与我们的 API 引擎进行交互的 Web 应用程序。Web 应用程序是我们将与智能设备进行交互的主要界面。

我们将使用 Angular (4)和 Twitter Bootstrap (3)构建 Web 应用程序。界面不一定要使用 Angular 和 Bootstrap 构建;也可以使用 jQuery 或 React.js。我们将做的只是使用浏览器中的 JavaScript 与 API 引擎的 API 进行接口。我们之所以使用 Angular,只是为了保持所有应用程序的框架一致。由于我们将使用 Ionic 框架,它也遵循 Angular 的方法,因此对我们来说管理和重用都会很容易。

要开始使用 Web 应用程序,我们将安装 Angular CLI (github.com/angular/angular-cli)。

在运行我们的代理和 API 引擎的机器上,我们也将设置 Web 应用程序。

设置应用程序

chapter2文件夹内,打开一个新的命令提示符/终端,并运行以下命令:

npm install -g @angular/cli  

这将安装 Angular CLI 生成器。安装完成后运行ng -v,您应该看到一个大于或等于 1.0.2 的版本号。

如果在设置和运行 IoTFW.js 时遇到任何问题,请随时在此处留下您的评论:github.com/PacktPublishing/Practical-Internet-of-Things-with-JavaScript/issues/1

对于 Web 应用程序,我们已经使用 Angular CLI 创建了一个基本项目,并添加了必要的部分来与 API 引擎集成。我们将克隆项目并在此基础上开始工作。

要开始,我们需要 Web 应用程序的基础。如果您还没有克隆该书的代码存储库,可以在您的任何位置使用以下命令行进行克隆:

git clone git@github.com:PacktPublishing/Practical-Internet-of-Things-with-JavaScript.git

或者您也可以从github.com/PacktPublishing/Practical-Internet-of-Things-with-JavaScript下载 zip 文件。

一旦存储库被下载,cd进入base文件夹,并将web-app-base文件夹复制到chapter2文件夹中。

基础已经复制完成后,cd进入web-app文件夹,并运行以下命令:

npm install  

这将安装所需的依赖项。

项目结构

如果打开cloned文件夹,我们应该看到以下内容:

.
├── README.md
├── e2e
│ ├── app.e2e-spec.ts
│ ├── app.po.ts
│ └── tsconfig.e2e.json
├── karma.conf.js
├── package.json
├── protractor.conf.js
├── src
│ ├── app
│ │ ├── add-device
│ │ │ ├── add-device.component.css
│ │ │ ├── add-device.component.html
│ │ │ ├── add-device.component.spec.ts
│ │ │ └── add-device.component.ts
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ ├── app.global.ts
│ │ ├── app.module.ts
│ │ ├── device
│ │ │ ├── device.component.css
│ │ │ ├── device.component.html
│ │ │ ├── device.component.spec.ts
│ │ │ └── device.component.ts
│ │ ├── device-template
│ │ │ ├── device-template.component.css
│ │ │ ├── device-template.component.html
│ │ │ ├── device-template.component.spec.ts
│ │ │ └── device-template.component.ts
│ │ ├── guard
│ │ │ ├── auth.guard.spec.ts
│ │ │ └── auth.guard.ts
│ │ ├── home
│ │ │ ├── home.component.css
│ │ │ ├── home.component.html
│ │ │ ├── home.component.spec.ts
│ │ │ └── home.component.ts
│ │ ├── login
│ │ │ ├── login.component.css
│ │ │ ├── login.component.html
│ │ │ ├── login.component.spec.ts
│ │ │ └── login.component.ts
│ │ ├── nav-bar
│ │ │ ├── nav-bar.component.css
│ │ │ ├── nav-bar.component.html
│ │ │ ├── nav-bar.component.spec.ts
│ │ │ └── nav-bar.component.ts
│ │ ├── register
│ │ │ ├── register.component.css
│ │ │ ├── register.component.html
│ │ │ ├── register.component.spec.ts
│ │ │ └── register.component.ts
│ │ └── services
│ │ ├── auth.service.spec.ts
│ │ ├── auth.service.ts
│ │ ├── data.service.spec.ts
│ │ ├── data.service.ts
│ │ ├── devices.service.spec.ts
│ │ ├── devices.service.ts
│ │ ├── http-interceptor.service.spec.ts
│ │ ├── http-interceptor.service.ts
│ │ ├── loader.service.spec.ts
│ │ ├── loader.service.ts
│ │ ├── socket.service.spec.ts
│ │ └── socket.service.ts
│ ├── assets
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.css
│ ├── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ └── typings.d.ts
├── tsconfig.json
└── tslint.json

现在,让我们来看一下项目结构和代码设置的步骤。

在高层次上,我们有一个src文件夹,其中包含所有的源代码和单元测试代码,还有一个e2e文件夹,其中包含端到端测试。

我们将大部分时间花在src/app文件夹内。在进入这个文件夹之前,打开web-app/src/main.ts,这是一切的开始。接下来,我们在这里添加了 Twitter Bootstrap Cosmos 主题(bootswatch.com/cosmo/),并定义了一些布局样式。

现在,app/src文件夹:在这里,我们定义了根组件、根模块和所需的组件和服务。

应用模块

打开web-app/src/app/app.module.ts。这个文件包括@NgModule声明,定义了我们将要使用的所有组件和服务。

我们已经创建了以下组件:

  • AppComponent:应用程序根组件,包含路由出口

  • NavBarComponent:这是出现在所有页面上的导航栏组件。该组件会自动检测认证状态,并相应地显示菜单栏

  • LoginComponent:处理登录功能

  • RegisterComponent:用于与 API 引擎进行注册

  • HomeComponent:这个组件显示当前登录用户附加的所有设备

  • DeviceComponent:这个组件显示有关一个设备的信息

  • AddDeviceComponent:这个组件让我们向设备列表中添加新的组件

  • DeviceTemplateComponent:用于表示应用程序中设备的通用模板

除了上述内容,我们还添加了所需的模块到导入中:

  • 路由模块:用于管理路由

  • LocalStorageModule:为了在浏览器中管理用户数据,我们将使用LocalStorgae

  • SimpleNotificationsModule:使用 Angular 2 通知显示通知(github.com/flauc/angular2-notifications

对于服务,我们有以下内容:

  • AuthService:管理 API 引擎提供的身份验证 API

  • DevicesService:管理 API 引擎提供的设备 API

  • DataService:管理 API 引擎提供的数据 API

  • SocketService:管理从 API 引擎实时发送数据的 Web 套接字

  • AuthGuard:一个 Angular 守卫,用于保护需要身份验证的路由。阅读使用 Angular 中的守卫保护路由blog.thoughtram.io/angular/2016/07/18/guards-in-angular-2.html)获取有关守卫的更多信息

  • LoaderService:在进行活动时显示和隐藏加载器栏

  • Http:我们用来发出 HTTP 请求的 HTTP 服务。在这里,我们没有直接使用 HTTP 服务,而是扩展了该类,并在其中添加了我们的逻辑,以更好地使用加载器服务来管理 HTTP 请求体验

请注意,此时 API 引擎没有设备和数据的 API,并且数据的套接字未设置。一旦我们完成 Web 应用程序,我们将在 API 引擎中实现它。

在这个 Web 应用程序中,我们将有以下路由:

  • login:让用户登录应用程序

  • register:注册我们的应用程序

  • home:显示用户帐户中所有设备的页面

  • add-device:向用户的设备列表添加新设备的页面

  • view-device/:id:查看由 URL 中的 id 参数标识的一个设备的页面

  • **:默认路由设置为登录

  • '':如果没有匹配的路由,我们将用户重定向到登录页面

Web 应用程序服务

现在我们在高层次上了解了这个 Web 应用程序中的所有内容,我们将逐步介绍服务和组件。

打开web-app/src/app/services/http-interceptor.service.ts;在这个类中,我们扩展了Http类并实现了类方法。我们添加了两个自己的方法,名为requestInterceptor()responseInterceptor(),分别拦截请求和响应。

当请求即将发送时,我们调用requestInterceptor()来显示加载器,指示 HTTP 活动,我们使用responseInterceptor()一旦响应到达就隐藏加载器。这样,用户清楚地知道是否有任何后台活动正在进行。

接下来是LoaderService类;打开web-app/src/app/services/loader.service.ts,从这里我们可以看到,我们添加了一个名为status的类属性,类型为BehaviorSubject<boolean>(要了解更多关于Behavior主题的信息,请参阅github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/behaviorsubject.md)。我们还有一个方法,如果 HTTP 服务或任何其他组件希望显示或隐藏加载器栏,它们将调用该方法,然后将值设置为 true 或 false。

加载器服务所需的 HTML 位于web-app/src/app/app.component.html,所需的样式位于web-app/src/app/app.component.css

我们将使用 Web 套接字在 Web 应用程序和 API 引擎之间实时流式传输数据。打开web-app/src/app/services/socket.service.ts,我们应该看到构造函数和getData()方法。我们在我们的 Web 应用程序中使用socket.io-clientgithub.com/socketio/socket.io-client)来管理 Web 套接字。

在构造函数中,我们已经创建了一个新的套接字连接到我们的 API 引擎,并将身份验证令牌作为查询参数传递。我们也将通过 Web 套接字验证传入的连接。只有在令牌有效的情况下,我们才允许连接,否则我们关闭 Web 套接字。

getData()内,我们订阅了设备的data:save主题。这是我们从 API 引擎得到通知的方式,当设备有新数据可用时。

现在我们将查看三个 API 服务,用于验证用户,获取用户设备和获取设备数据:

  • AuthService:打开web-app/src/app/services/auth.service.ts。在这里,我们已经定义了register()login()logout(),它们负责管理身份验证状态,我们还有isAuthenticated(),它返回当前的身份验证状态,即用户是已登录还是已注销。

  • DevicesService:打开web-app/src/app/services/devices.service.ts。在这里,我们实现了三种方法:创建一个,读取一个,删除一个。通过这样,我们为用户管理我们的设备。

  • DataService:打开web-app/src/app/services/data.service.ts,它管理设备的数据。我们这里只有两种方法:创建一个新的数据记录和获取设备的最后 30 条记录。

请注意,我们正在使用web-app/src/app/app.global.ts来保存所有我们的常量全局变量。

现在我们已经完成了所需的服务,我们将浏览组件。

Web 应用程序组件

我们将从应用程序组件开始。应用程序组件是根组件,它包含路由器出口,加载器服务 HTML 和通知服务 HTML。您可以在这里找到相同的内容:web-app/src/app/app.component.html。在web-app/src/app/app.component.ts中,我们已经定义了showLoader,它决定是否应该显示加载器。我们还定义了通知选项,它存储通知服务的配置。

在构造函数内,我们正在监听路由器上的路由更改事件,以便在页面更改时显示加载栏。我们还在监听加载器服务状态变量。如果这个变化,我们就会显示或隐藏加载器。

用户登陆的第一个页面是登录页面。登录页面/组件web-app/src/app/login/login.component.ts只有一个方法,从web-app/src/app/login/login.component.html获取用户的电子邮件和密码,并对用户进行身份验证。

使用主页上的注册按钮,用户注册自己。在RegisterComponent类内,web-app/src/app/register/register.component.ts,我们已经定义了register(),它获取用户的信息,并使用AuthService注册用户。

一旦用户成功验证,我们将用户重定向到LoginComponent。在HomeComponentweb-app/src/app/home/home.component.ts中,我们获取与用户关联的所有设备并在加载时显示它们。此页面还有一个按钮,用于使用AddDeviceComponent添加新设备。

要查看一个设备,我们使用DeviceComponent来查看一个设备。

目前,我们还没有任何可用于处理设备和数据的 API。在下一节中完成 API 引擎更新后,我们将重新访问此页面。

启动应用程序

要运行应用程序,请在web-app文件夹内打开终端/提示符,并运行以下命令:

ng serve

在运行上一个命令之前,请确保 API 引擎和 Mosca 正在运行。

一旦 webpack 编译成功,导航到http://localhost:4200/login,我们应该看到登录页面,这是第一个页面。

我们可以使用在测试 API 引擎时创建的帐户,使用 Postman,或者我们可以通过点击“使用 Web 应用程序注册”来创建一个新帐户,如下所示:

如果注册成功,我们应该被重定向到主页,如下所示:

如果我们打开开发者工具,应该会看到先前的消息。API 引擎没有实现设备的 API,因此出现了先前的“404”。我们将在第三章中修复这个问题,IoTFW.js - II

我们还将在第三章中逐步完成 Web 应用程序的剩余部分,一旦 API 引擎更新完成。

总结

在本章中,我们已经了解了建立物联网解决方案的过程。我们使用 JavaScript 作为编程语言构建了大部分框架。

我们首先了解了从树莓派到 Web 应用程序、桌面应用程序或移动应用程序等最终用户设备的架构和数据流。然后我们开始使用 Mosca 工作代理,设置了 MongoDB。接下来,我们设计并开发了 API 引擎,并完成了基本的树莓派设置。

我们已经在 Web 应用程序上工作,并设置了必要的模板,以便与应用程序的剩余部分配合使用。在第三章中,我们将完成整个框架,并集成 DHT11(温湿度)传感器和 LED,以验证端到端的双向数据流。

第三章:IoTFW.js - II

在上一章中,我们已经看到了树莓派、代理、API 引擎和 Web 应用程序之间的基本设置。在本章中,我们将继续处理框架的其余部分。我们还将构建一个涉及传感和执行的简单示例。我们将使用温湿度传感器读取温度和湿度,并使用 Web、桌面或移动应用程序打开/关闭连接到我们的树莓派的 LED。

在本章中,我们将涵盖以下主题:

  • 更新 API 引擎

  • 将 API 引擎与 Web 应用程序集成

  • 使用 DHT11 和 LED 构建端到端示例

  • 构建桌面应用程序

  • 构建移动应用程序

更新 API 引擎

现在我们已经完成了 Web 应用程序的开发,我们将更新 API 引擎以添加设备的 API 和数据服务,以及 Web 套接字。

打开api-engine/server/routes.js;我们将在这里添加两个路由。更新api-engine/server/routes.js如下:

'use strict'; 

var path = require('path'); 

module.exports = function(app) { 
  // Insert routes below 
  app.use('/api/v1/users', require('./api/user')); 
  app.use('/api/v1/devices', require('./api/device')); 
  app.use('/api/v1/data', require('./api/data')); 

  app.use('/auth', require('./auth')); 
}; 

现在,我们将为这些路由添加定义。在api-engine/server/api文件夹内,创建一个名为device的新文件夹。在device文件夹内,创建一个名为index.js的新文件。更新api-engine/server/api/device/index.js如下:

'use strict'; 

var express = require('express'); 
var controller = require('./device.controller'); 
var config = require('../../config/environment'); 
var auth = require('../../auth/auth.service'); 

var router = express.Router(); 

router.get('/', auth.isAuthenticated(), controller.index); 
router.delete('/:id', auth.isAuthenticated(), controller.destroy); 
router.put('/:id', auth.isAuthenticated(), controller.update); 
router.get('/:id', auth.isAuthenticated(), controller.show); 
router.post('/', auth.isAuthenticated(), controller.create); 

module.exports = router; 

在这里,我们添加了五个路由,如下:

  • 获取所有设备

  • 删除设备

  • 更新设备

  • 获取一个设备

  • 创建一个设备

接下来,在api-engine/server/api/device/文件夹内创建另一个文件,命名为device.model.js。这个文件将包含设备集合的 mongoose 模式。更新api-engine/server/api/device/device.model.js如下:

'use strict'; 

var mongoose = require('mongoose'); 
var Schema = mongoose.Schema; 

var DeviceSchema = new Schema({ 
    name: String, 
    macAddress: String, 
    createdBy: { 
        type: String, 
        default: 'user' 
    }, 
    createdAt: { 
        type: Date 
    }, 
    updatedAt: { 
        type: Date 
    } 
}); 

DeviceSchema.pre('save', function(next) { 
    var now = new Date(); 
    this.updatedAt = now; 
    if (!this.createdAt) { 
        this.createdAt = now; 
    } 
    next(); 
}); 

module.exports = mongoose.model('Device', DeviceSchema); 

最后,控制器逻辑。在api-engine/server/api/device文件夹内创建一个名为device.controller.js的文件,并更新api-engine/server/api/device/device.controller.js如下:

'use strict'; 

var Device = require('./device.model'); 

/** 
 * Get list of all devices for a user 
 */ 
exports.index = function(req, res) { 
    var currentUser = req.user._id; 
    // get only devices related to the current user 
    Device.find({ 
        createdBy: currentUser 
    }, function(err, devices) { 
        if (err) return res.status(500).send(err); 
        res.status(200).json(devices); 
    }); 
}; 

/** 
 * Add a new device 
 */ 
exports.create = function(req, res, next) { 
    var device = req.body; 
    // this device is created by the current user 
    device.createdBy = req.user._id; 
    Device.create(device, function(err, device) { 
        if (err) return res.status(500).send(err); 
        res.json(device); 
    }); 
}; 

/** 
 * Get a single device 
 */ 
exports.show = function(req, res, next) { 
    var deviceId = req.params.id; 
    // the current user should have created this device 
    Device.findOne({ 
        _id: deviceId, 
        createdBy: req.user._id 
    }, function(err, device) { 
        if (err) return res.status(500).send(err); 
        if (!device) return res.status(404).end(); 
        res.json(device); 
    }); 
}; 

/** 
 * Update a device 
 */ 
exports.update = function(req, res, next) { 
    var device = req.body; 
    device.createdBy = req.user._id; 

    Device.findOne({ 
        _id: deviceId, 
        createdBy: req.user._id 
    }, function(err, device) { 
        if (err) return res.status(500).send(err); 
        if (!device) return res.status(404).end(); 

        device.save(function(err, updatedDevice) { 
            if (err) return res.status(500).send(err); 
            return res.status(200).json(updatedDevice); 
        }); 
    }); 
}; 

/** 
 * Delete a device 
 */ 
exports.destroy = function(req, res) { 
    Device.findOne({ 
        _id: req.params.id, 
        createdBy: req.user._id 
    }, function(err, device) { 
        if (err) return res.status(500).send(err); 

        device.remove(function(err) { 
            if (err) return res.status(500).send(err); 
            return res.status(204).end(); 
        }); 
    }); 
}; 

在这里,我们已经定义了路由的逻辑。

设备 API 为我们管理设备。为了管理每个设备的数据,我们将使用这个集合。

现在,我们将定义数据 API。在api-engine/server/api文件夹内创建一个名为data的新文件夹。在api-engine/server/api/data文件夹内,创建一个名为index.js的新文件,并更新api-engine/server/api/data/index.js如下:

'use strict'; 

var express = require('express'); 
var controller = require('./data.controller'); 
var auth = require('../../auth/auth.service'); 

var router = express.Router(); 

router.get('/:deviceId/:limit', auth.isAuthenticated(), controller.index); 
router.post('/', auth.isAuthenticated(), controller.create); 

module.exports = router; 

我们在这里定义了两个路由:一个用于基于设备 ID 查看数据,另一个用于创建数据。查看数据路由返回作为请求的一部分传递的数量限制的设备数据。如果您记得,在web-app/src/app/services/data.service.ts中,我们已经将dataLimit类变量定义为30。这是我们从 API 中一次获取的记录数。

接下来,对于 mongoose 模式,在api-engine/server/api/data文件夹内创建一个名为data.model.js的新文件,并更新api-engine/server/api/data/data.model.js如下:

'use strict'; 

var mongoose = require('mongoose'); 
var Schema = mongoose.Schema; 

var DataSchema = new Schema({ 
    macAddress: { 
        type: String 
    }, 
    data: { 
        type: Schema.Types.Mixed 
    }, 
    createdBy: { 
        type: String, 
        default: 'raspberrypi3' 
    }, 
    createdAt: { 
        type: Date 
    }, 
    updatedAt: { 
        type: Date 
    } 
}); 

DataSchema.pre('save', function(next) { 
    var now = new Date(); 
    this.updatedAt = now; 
    if (!this.createdAt) { 
        this.createdAt = now; 
    } 
    next(); 
});
DataSchema.post('save', function(doc) { 
    //console.log('Post Save Called', doc); 
    require('./data.socket.js').onSave(doc) 
}); 

module.exports = mongoose.model('Data', DataSchema); 

现在,数据 API 的控制器逻辑。在api-engine/server/api/data文件夹内创建一个名为data.controller.js的文件,并更新api-engine/server/api/data/data.controller.js如下:

'use strict'; 

var Data = require('./data.model'); 

/** 
 * Get Data for a device 
 */ 
exports.index = function(req, res) { 
    var macAddress = req.params.deviceId; 
    var limit = parseInt(req.params.limit) || 30; 
    Data.find({ 
        macAddress: macAddress 
    }).limit(limit).exec(function(err, devices) { 
        if (err) return res.status(500).send(err); 
        res.status(200).json(devices); 
    }); 
}; 

/** 
 * Create a new data record 
 */ 
exports.create = function(req, res, next) { 
    var data = req.body; 
    data.createdBy = req.user._id; 
    Data.create(data, function(err, _data) { 
        if (err) return res.status(500).send(err); 
        res.json(_data); 
        if(data.topic === 'led'){ 
            require('../../mqtt/index.js').sendLEDData(data.data.l);// send led value 
        } 
    }); 
}; 

在这里,我们定义了两种方法:一种是为设备获取数据,另一种是为设备创建新的数据记录。

对于数据 API,我们也将实现套接字,因此当来自树莓派的新记录时,我们立即通知 Web 应用程序、桌面应用程序或移动应用程序,以便数据可以实时显示。

从前面的代码中可以看到,如果传入的主题是LED,我们将调用sendLEDData(),它会将数据发布到设备。

api-engine/server/api/data文件夹内创建一个名为data.socket.js的文件,并更新api-engine/server/api/data/data.socket.js如下:

/** 
 * Broadcast updates to client when the model changes 
 */ 

'use strict'; 

var data = require('./data.model'); 
var socket = undefined; 

exports.register = function(_socket) { 
   socket = _socket; 
} 

function onSave(doc) { 
    // send data to only the intended device 
    socket.emit('data:save:' + doc.macAddress, doc); 
} 

module.exports.onSave = onSave; 

这将负责在成功保存到数据库后发送新的数据记录。

接下来,我们需要将 socket 添加到 socket 配置中。打开api-engine/server/config/socketio.js并进行更新如下:

'use strict'; 

var config = require('./environment'); 

// When the user disconnects.. perform this 
function onDisconnect(socket) {} 

// When the user connects.. perform this 
function onConnect(socket) { 
    // Insert sockets below 
    require('../api/data/data.socket').register(socket); 
} 
module.exports = function(socketio) { 
    socketio.use(require('socketio-jwt').authorize({ 
        secret: config.secrets.session, 
        handshake: true 
    })); 

    socketio.on('connection', function(socket) { 
        var socketId = socket.id; 
        var clientIp = socket.request.connection.remoteAddress; 

        socket.address = socket.handshake.address !== null ? 
            socket.handshake.address.address + ':' + socket.handshake.address.port : 
            process.env.DOMAIN; 

        socket.connectedAt = new Date(); 

        // Call onDisconnect. 
        socket.on('disconnect', function() { 
            onDisconnect(socket); 
            // console.info('[%s] DISCONNECTED', socket.address); 
        }); 

        // Call onConnect. 
        onConnect(socket); 
        console.info('[%s] Connected on %s', socketId, clientIp); 
    }); 
}; 

请注意,我们使用socketio-jwt来验证套接字连接,以查看它是否具有 JWT。如果没有提供有效的 JWT,我们不允许客户端连接。

通过这样,我们完成了对 API 引擎的所需更改。保存所有文件并通过运行以下命令启动 API 引擎:

npm start  

这将启动 API 引擎。在下一节中,我们将测试 Web 应用程序和 API 引擎之间的集成,并继续从上一节开始的步骤。

集成 Web 应用程序和 API 引擎

启动代理商、API 引擎和 Web 应用程序。一旦它们都成功启动,导航到http://localhost:4200/。使用我们创建的凭据登录。一旦成功登录,我们应该看到以下屏幕:

这是真的,因为我们的账户中没有任何设备。点击添加设备,我们应该看到如下内容:

通过给设备命名来添加一个新设备。我给我的设备命名为Pi 1并添加了 mac 地址。我们将使用设备的 mac 地址作为识别设备的唯一方式。

点击创建,我们应该看到一个新设备被创建,它将重定向我们到主页并显示新创建的设备,可以在以下截图中看到:

现在,当我们点击查看按钮时,我们应该看到以下页面:

在本书的示例中,我们将不断更新此模板,并根据需要进行修改。目前,这是一个由web-app/src/app/device/device.component.html表示的虚拟模板。

如果我们打开开发者工具并查看网络选项卡 WS 部分,如下截图所示,我们应该能够看到一个带有 JWT 令牌的 Web 套接字请求被发送到我们的服务器:

通过这样,我们完成了将树莓派与代理商、代理商与 API 引擎以及 API 引擎与 Web 应用程序连接起来。为了完成从设备到 Web 应用程序的整个数据往返,我们将在下一节实现一个简单的用例。

使用 DHT11 和 LED 测试端到端流程

在开始处理桌面和移动应用程序之前,我们将为树莓派到 Web 应用程序的端到端数据流实现一个流程。

我们将要处理的示例实现了执行器和传感器用例。我们将把 LED 连接到树莓派,并从 Web 应用程序中打开/关闭 LED,我们还将把 DHT11 温度传感器连接到树莓派,并在 Web 应用程序中实时查看其值。

我们将开始使用树莓派,在那里实现所需的逻辑;接下来,与 API 引擎一起工作,进行所需的更改,最后是 Web 应用程序来表示数据。

设置和更新树莓派

首先,我们将按照以下方式设置电路:

现在,我们将进行以下连接:

源引脚 组件引脚
树莓派引脚 1 - 3.3V 面包板+栏杆
树莓派引脚 6 - 地面 面包板-栏杆
树莓派引脚 3 - GPIO 2 温度传感器信号引脚
树莓派引脚 12 - GPIO 18 LED 阳极引脚
LED 阴极引脚 面包板-栏杆
温度传感器+引脚 面包板+栏杆
温度传感器-引脚 面包板-栏杆

我们在引脚 12/GPIO 18 和 LED 引脚的阳极之间使用了一个 220 欧姆的限流电阻。

一旦建立了这种连接,我们将编写所需的逻辑。在树莓派上,打开pi-client/index.js文件并更新如下:

var config = require('./config.js'); 
var mqtt = require('mqtt'); 
var GetMac = require('getmac'); 
var rpiDhtSensor = require('rpi-dht-sensor'); 
var rpio = require('rpio'); 
var dht11 = new rpiDhtSensor.DHT11(2); 
var temp = 0, 
    prevTemp = 0; 
var humd = 0, 
    prevHumd = 0; 
var macAddress; 
var state = 0; 

// Set pin 12 as output pin and to low 
rpio.open(12, rpio.OUTPUT, rpio.LOW); 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('rpi'); 
    client.subscribe('led'); 
    GetMac.getMac(function(err, mac) { 
        if (err) throw err; 
        macAddress = mac; 
        client.publish('api-engine', mac); 
    }); 
}); 

client.on('message', function(topic, message) { 
    message = message.toString(); 
    if (topic === 'rpi') { 
        console.log('API Engine Response >> ', message); 
    } else if (topic === 'led') { 
        state = parseInt(message) 
        console.log('Turning LED', state ? 'On' : 'Off'); 
        // If we get a 1 we turn on the led, else off 
        rpio.write(12, state ? rpio.HIGH : rpio.LOW); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

// infinite loop, with 3 seconds delay 
setInterval(function() { 
    getDHT11Values(); 
    console.log('Temperature: ' + temp + 'C, ' + 'humidity: ' + humd + '%'); 
    // if the temperature and humidity values change 
    // then only publish the values 
    if (temp !== prevTemp || humd !== prevHumd) { 
        var data2Send = { 
            data: { 
                t: temp, 
                h: humd, 
                l: state 
            }, 
            macAddress: macAddress 
        }; 
        console.log('Data Published'); 
        client.publish('dht11', JSON.stringify(data2Send)); 
        // reset prev values to current 
        // for next loop 
        prevTemp = temp; 
        prevHumd = humd; 
    } // else chill! 

}, 3000); // every three second 

function getDHT11Values() { 
    var readout = dht11.read(); 
    // update global variable 
    temp = readout.temperature.toFixed(2); 
    humd = readout.humidity.toFixed(2); 
} 

在上述代码中,我们添加了一些节点模块,如下所示:

我们编写了一个setInterval(),它会每 3 秒运行一次。在callback中,我们调用getDHT11Values()来从传感器读取温度和湿度。如果温度和湿度值发生变化,我们就会将这些数据发布到代理。

还要注意client.on('message');在这里,我们添加了另一个if条件,并监听LED主题。如果当前消息来自LED主题,我们知道我们将收到一个10,表示打开或关闭 LED。

最后,我们将安装这两个模块,运行:

npm install rpi-dht-sensor -save
npm install rpio -save  

保存所有文件并运行npm start;这应该将树莓派连接到代理并订阅LED主题,如下所示:

此外,如果我们从树莓派的控制台输出中看到,应该会看到以下内容:

每当数据发生变化时,数据就会发布到代理。我们还没有实现对 API 引擎上的数据做出反应的逻辑,这将在下一节中完成。

更新 API 引擎

现在,我们将向在 API 引擎上运行的 MQTT 客户端添加所需的代码来处理来自设备的数据。更新api-engine/server/mqtt/index.js,如下所示:

var Data = require('../api/data/data.model'); 
var mqtt = require('mqtt'); 
var config = require('../config/environment'); 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    console.log('Connected to Mosca at ' + config.mqtt.host + ' on port ' + config.mqtt.port); 
    client.subscribe('api-engine'); 
    client.subscribe('dht11'); 
}); 

client.on('message', function(topic, message) { 
    // message is Buffer 
    // console.log('Topic >> ', topic); 
    // console.log('Message >> ', message.toString()); 
    if (topic === 'api-engine') { 
        var macAddress = message.toString(); 
        console.log('Mac Address >> ', macAddress); 
        client.publish('rpi', 'Got Mac Address: ' + macAddress); 
    } else if (topic === 'dht11') { 
        var data = JSON.parse(message.toString()); 
        // create a new data record for the device 
        Data.create(data, function(err, data) { 
            if (err) return console.error(err); 
            // if the record has been saved successfully,  
            // websockets will trigger a message to the web-app 
            console.log('Data Saved :', data.data); 
        }); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

exports.sendLEDData = function(data) { 
    console.log('Sending Data', data); 
    client.publish('led', data); 
} 

在这里,我们订阅了一个名为dht11的主题,以监听树莓派发布的关于温度和湿度值的消息。我们还公开了另一个名为sendLEDData的方法,用于接受需要发送到设备的数据。

如果我们保存所有文件并重新启动 API 引擎,应该会看到以下内容:

从上面的截图中,我们可以看到数据来自树莓派并保存到 MongoDB。要验证数据是否已保存,我们可以转到mlab数据库并查找名为datas的集合,应该如下所示:

每当数据成功保存时,相同的副本也将发送到 Web 应用程序。在下一节中,我们将在 Web 仪表板上实时显示这些数据。

更新 Web 应用程序

在本节中,我们将开发在 Web 应用程序中实时显示数据所需的代码,以及提供一个界面,通过该界面我们可以打开/关闭 LED。

我们将首先添加一个切换开关组件。我们将使用ngx-ui-switch (github.com/webcat12345/ngx-ui-switch)。

web-app-base文件夹内运行以下命令:

npm install ngx-ui-switch -save  

我们将使用ng2-charts valor-software.com/ng2-charts/来绘制温度和湿度值的图表。我们也将通过运行以下命令安装这个模块:

npm install ng2-charts --save
npm install chart.js --save  

这将安装切换开关和ng2-charts模块。接下来,我们需要将其添加到@NgModule中。打开web-app/src/app/app.module.ts并将以下命令添加到 imports 中:

import { UiSwitchModule } from 'ngx-ui-switch'; 
import { ChartsModule } from 'ng2-charts'; 

然后,将UiSwitchModuleChartsModule添加到 imports 数组中:

// snipp snipp 
imports: [ 
    RouterModule.forRoot(appRoutes), 
    BrowserModule, 
    BrowserAnimationsModule, 
    FormsModule, 
    HttpModule, 
    LocalStorageModule.withConfig({ 
      prefix: 'web-app', 
      storageType: 'localStorage' 
    }), 
    SimpleNotificationsModule.forRoot(), 
    UiSwitchModule, 
    ChartsModule 
  ], 
// snipp snipp 

完成后,我们需要将chart.js导入到我们的应用程序中。打开web-app/.angular-cli.json并更新scripts部分,如下所示:

// snipp snipp  
"scripts": [ 
        "../node_modules/chart.js/dist/Chart.js" 
      ], 
// snipp snipp  

保存所有文件并重新启动 Web 应用程序,如果它已经在运行。

现在,我们可以在设备组件中使用这个指令。

在我们当前的用例中,我们需要显示温度和湿度值,并提供一个切换开关来打开/关闭 LED。为此,我们在web-app/src/app/device/device.component.html中的模板将如下所示:

<div class="container"> 
    <br> 
    <div *ngIf="!device"> 
        <h3 class="text-center">Loading!</h3> 
    </div> 
    <div class="row" *ngIf="lastRecord"> 
        <div class="col-md-12"> 
            <div class="panel panel-info"> 
                <div class="panel-heading"> 
                    <h3 class="panel-title"> 
                        {{device.name}} 
                    </h3> 
                    <span class="pull-right btn-click"> 
                        <i class="fa fa-chevron-circle-up"></i> 
                    </span> 
                </div> 
                <div class="clearfix"></div> 
                <div class="table-responsive"> 
                    <table class="table table-striped"> 
                        <tr> 
                            <td>Toggle LED</td> 
                            <td> 
                                <ui-switch [(ngModel)]="toggleState" (change)="toggleChange($event)"></ui-switch> 
                            </td> 
                        </tr> 
                        <tr *ngIf="lastRecord"> 
                            <td>Temperature</td> 
                            <td>{{lastRecord.data.t}}</td> 
                        </tr> 
                        <tr *ngIf="lastRecord"> 
                            <td>Humidity</td> 
                            <td>{{lastRecord.data.h}}</td> 
                        </tr> 
                        <tr *ngIf="lastRecord"> 
                            <td>Received At</td> 
                            <td>{{lastRecord.createdAt | date: 'medium'}}</td> 
                        </tr> 
                    </table> 
                    <div class="col-md-10 col-md-offset-1" *ngIf="lineChartData.length > 0"> 
                        <canvas baseChart [datasets]="lineChartData" [labels]="lineChartLabels" [options]="lineChartOptions" [legend]="lineChartLegend" [chartType]="lineChartType"></canvas> 
                    </div> 
                </div> 
            </div> 
        </div> 
    </div> 
</div> 

DeviceComponent类的所需代码:web-app/src/app/device/device.component.ts将如下所示:

import { Component, OnInit, OnDestroy } from '@angular/core'; 
import { DevicesService } from '../services/devices.service'; 
import { Params, ActivatedRoute } from '@angular/router'; 
import { SocketService } from '../services/socket.service'; 
import { DataService } from '../services/data.service'; 
import { NotificationsService } from 'angular2-notifications'; 

@Component({ 
   selector: 'app-device', 
   templateUrl: './device.component.html', 
   styleUrls: ['./device.component.css'] 
}) 
export class DeviceComponent implements OnInit, OnDestroy { 
   device: any; 
   data: Array<any>; 
   toggleState: boolean = false; 
   private subDevice: any; 
   private subData: any; 
   lastRecord: any; 

   // line chart config 
   public lineChartOptions: any = { 
         responsive: true, 
         legend: { 
               position: 'bottom', 
         }, hover: { 
               mode: 'label' 
         }, scales: { 
               xAxes: [{ 
                     display: true, 
                     scaleLabel: { 
                           display: true, 
                           labelString: 'Time' 
                     } 
               }], 
               yAxes: [{ 
                     display: true, 
                     ticks: { 
                           beginAtZero: true, 
                           steps: 10, 
                           stepValue: 5, 
                           max: 70 
                     } 
               }] 
         }, 
         title: { 
               display: true, 
               text: 'Temperature & Humidity vs. Time' 
         } 
   }; 
   public lineChartLegend: boolean = true; 
   public lineChartType: string = 'line'; 
   public lineChartData: Array<any> = []; 
   public lineChartLabels: Array<any> = []; 

   constructor(private deviceService: DevicesService, 
         private socketService: SocketService, 
         private dataService: DataService, 
         private route: ActivatedRoute, 
         private notificationsService: NotificationsService) { } 

   ngOnInit() { 
         this.subDevice = this.route.params.subscribe((params) => { 
               this.deviceService.getOne(params['id']).subscribe((response) => { 
                     this.device = response.json(); 
                     this.getData(); 
                     this.socketInit(); 
               }); 
         }); 
   } 

   getData() { 
         this.dataService.get(this.device.macAddress).subscribe((response) => { 
               this.data = response.json(); 
               this.genChart(); 
               this.lastRecord = this.data[0]; // descending order data 
               if (this.lastRecord) { 
                     this.toggleState = this.lastRecord.data.l; 
               } 
         }); 
   } 

   toggleChange(state) { 
         let data = { 
               macAddress: this.device.macAddress, 
               data: { 
                     t: this.lastRecord.data.t, 
                     h: this.lastRecord.data.h, 
                     l: state ? 1 : 0 
               }, 
               topic: 'led' 
         } 

         this.dataService.create(data).subscribe((resp) => { 
               if (resp.json()._id) { 
                     this.notificationsService.success('Device Notified!'); 
               } 
         }, (err) => { 
               console.log(err); 
               this.notificationsService.error('Device Notification Failed. Check console for the error!'); 
         }) 
   } 

   socketInit() { 
         this.subData = this.socketService.getData(this.device.macAddress).subscribe((data) => { 
               if(this.data.length <= 0) return; 
               this.data.splice(this.data.length - 1, 1); // remove the last record 
               this.data.push(data); // add the new one 
               this.lastRecord = data; 
               this.genChart(); 
         }); 
   } 

   ngOnDestroy() { 
         this.subDevice.unsubscribe(); 
         this.subData ? this.subData.unsubscribe() : ''; 
   } 

   genChart() { 

         let data = this.data; 
         let _dtArr: Array<any> = []; 
         let _lblArr: Array<any> = []; 

         let tmpArr: Array<any> = []; 
         let humArr: Array<any> = []; 

         for (var i = 0; i < data.length; i++) { 
               let _d = data[i]; 
               tmpArr.push(_d.data.t); 
               humArr.push(_d.data.h); 
               _lblArr.push(this.formatDate(_d.createdAt)); 
         } 

         // reverse data to show the latest on the right side 
         tmpArr.reverse(); 
         humArr.reverse(); 
         _lblArr.reverse(); 

         _dtArr = [ 
               { 
                     data: tmpArr, 
                     label: 'Temperature' 
               }, 
               { 
                     data: humArr, 
                     label: 'Humidity %' 
               }, 
         ] 

         this.lineChartData = _dtArr; 
         this.lineChartLabels = _lblArr; 
   } 

   private formatDate(originalTime) { 
         var d = new Date(originalTime); 
         var datestring = d.getDate() + "-" + (d.getMonth() + 1) + "-" + d.getFullYear() + " " + 
               d.getHours() + ":" + d.getMinutes(); 
         return datestring;
   } 
} 

需要注意的关键方法如下:

  • getData(): 此方法用于在页面加载时获取最近的 30 条记录。我们从 API 引擎中以降序发送数据;因此我们提取最后一条记录并将其保存为最后一条记录。如果需要,我们可以使用剩余的记录来绘制图表

  • toggleChange(): 当切换开关被点击时,将触发此方法。此方法将发送数据到 API 引擎以保存

  • socketInit(): 此方法一直监听设备上的数据保存事件。使用此方法,我们将lastRecord变量更新为设备上的最新数据

  • genChart(): 此方法获取数据集合,然后绘制图表。当新数据通过套接字到达时,我们会从数据数组中删除最后一条记录并推送新记录,始终保持 30 条记录的总数

有了这个,我们就完成了处理此设置所需的模板开发。

保存所有文件,启动代理程序、API 引擎和 Web 应用程序,然后登录应用程序,然后导航到设备页面。

如果一切设置正确,我们应该看到以下屏幕:

现在,每当数据通过套接字传输时,图表会自动更新!

现在,为了测试 LED,切换 LED 按钮到开启状态,您应该看到我们在树莓派上设置的 LED 会亮起,同样,如果我们关闭它,LED 也会关闭。

构建桌面应用程序并实现端到端流程

现在我们已经完成了与 Web 应用程序的端到端流程,我们将扩展到桌面和移动应用程序。我们将首先构建相同 API 引擎的桌面客户端。因此,如果用户更喜欢使用桌面应用程序而不是 Web 或移动应用程序,他/她可以使用这个。

这个桌面应用程序将具有与 Web 应用程序相同的所有功能。

为了构建桌面应用程序,我们将使用 electron (electron.atom.io/) 框架。使用名为generator-electron (github.com/sindresorhus/generator-electron) 的 Yeoman (yeoman.io/) 生成器,我们将创建基本应用程序。然后,我们将构建我们的 Web 应用程序,并使用该构建的dist文件夹作为桌面应用程序的输入。一旦我们开始工作,所有这些将更加清晰。

要开始,请运行以下命令:

npm install yo generator-electron -g  

这将安装 yeoman 生成器和 electron 生成器。接下来,在chapter2文件夹内,创建一个名为desktop-app的文件夹,然后,在新的命令提示符/终端中运行以下命令:

yo electron

这个向导将询问一些问题,您可以相应地回答:

这将安装所需的依赖项。安装完成后,我们应该看到以下文件夹结构:

.

├── index.css

├── index.html

├── index.js

├── license

├── package.json

└── readme.md

有了根目录下的node_modules文件夹。

一切都始于desktop-app/package.json的启动脚本,它启动desktop-app/index.jsdesktop-app/index.js创建一个新的浏览器窗口,并启动desktop-app/index.html页面。

要从desktop-app文件夹内快速测试驱动,请运行以下命令:

npm start   

因此,我们应该看到以下屏幕:

现在,我们将添加所需的代码。在desktop-app文件夹的根目录下,创建一个名为freeport.js的文件,并更新desktop-app/freeport.js如下:

var net = require('net') 
module.exports = function(cb) { 
    var server = net.createServer(), 
        port = 0; 
    server.on('listening', function() { 
        port = server.address().port 
        server.close() 
    }); 
    server.on('close', function() { 
        cb(null, port) 
    }) 
    server.on('error', function(err) { 
        cb(err, null) 
    }) 
    server.listen(0, '127.0.0.1') 
} 

使用上述代码,我们将在最终用户的计算机上找到一个空闲端口,并在 electron 外壳中启动我们的 Web 应用程序。

接下来,在desktop-app文件夹的根目录下创建一个名为app的文件夹。我们将在这里倾倒文件。接下来,在desktop-app文件夹的根目录下,创建一个名为server.js的文件。更新server.js如下:

var FreePort = require('./freeport.js'); 
var http = require('http'), 
    fs = require('fs'), 
    html = ''; 

module.exports = function(cb) { 
    FreePort(function(err, port) { 
        console.log(port); 
        http.createServer(function(request, response) { 
            if (request.url === '/') { 
                html = fs.readFileSync('./app/index.html'); 
            } else { 
                html = fs.readFileSync('./app' + request.url); 
            } 
            response.writeHeader(200, { "Content-Type": "text/html" }); 
            response.write(html); 
            response.end(); 
        }).listen(port); 
        cb(port); 
    }); 
} 

在这里,我们监听一个空闲端口并启动index.html。现在,我们需要做的就是更新desktop-app/index.js中的createMainWindow()如下:

// snipp snipp 
function createMainWindow() { 
    const { width, height } = electron.screen.getPrimaryDisplay().workAreaSize; 
    const win = new electron.BrowserWindow({ width, height }) 
    const server = require("./server")(function(port) { 
        win.loadURL('http://localhost:' + port); 
        win.on('closed', onClosed); 
        console.log('Desktop app started on port :', port); 
    }); 

    return win; 
} 
// snipp snipp 

这就是我们需要的所有设置。

现在,返回到web-app文件夹的终端/提示符(是的web-app,而不是desktop-app),并运行以下命令:

ng build --env=prod

这将在web app文件夹内创建一个名为dist的新文件夹。dist文件夹的内容应如下所示:

.

├── favicon.ico

├── index.html

├── inline.bundle.js

├── inline.bundle.js.map

├── main.bundle.js

├── main.bundle.js.map

├── polyfills.bundle.js

├── polyfills.bundle.js.map

├── scripts.bundle.js

├── scripts.bundle.js.map

├── styles.bundle.js

├── styles.bundle.js.map

├── vendor.bundle.js

└── vendor.bundle.js.map

我们在 web 应用程序中编写的所有代码最终都打包到了前述文件中。我们将获取dist文件夹内的所有文件(而不是dist文件夹),然后将其粘贴到desktop-app/app文件夹中。在进行前述更改后,桌面应用程序的最终结构将如下所示:

.

├── app

│ ├── favicon.ico

│ ├── index.html

│ ├── inline.bundle.js

│ ├── inline.bundle.js.map

│ ├── main.bundle.js

│ ├── main.bundle.js.map

│ ├── polyfills.bundle.js

│ ├── polyfills.bundle.js.map

│ ├── scripts.bundle.js

│ ├── scripts.bundle.js.map

│ ├── styles.bundle.js

│ ├── styles.bundle.js.map

│ ├── vendor.bundle.js

│ └── vendor.bundle.js.map

├── freeport.js

├── index.css

├── index.html

├── index.js

├── license

├── package.json

├── readme.md

└── server.js

从现在开始,我们只需将web-app/dist文件夹的内容粘贴到desktop-appapp文件夹中。

要进行测试,请运行以下命令:

npm start 

这将带来登录屏幕,如下所示:

如果您看到之前显示的弹出窗口,请允许。成功登录后,您应该能够看到您帐户中的所有设备,如下所示:

最后,设备信息屏幕:

现在我们可以打开/关闭 LED,它应该有相应的反应。

现在,我们已经完成了桌面应用程序。

在下一节中,我们将使用 Ionic 框架构建一个移动应用程序。

构建移动应用程序并实现端到端流程

在本节中,我们将使用 Ionic 框架(ionicframework.com/)构建我们的移动伴侣应用程序。输出或示例与我们为 web 和桌面应用程序所做的相同。

开始时,我们将通过运行以下命令安装最新版本的ioniccordova

npm install -g ionic cordova  

现在,我们需要移动应用程序基础。如果您还没有克隆该书的代码存储库,可以使用以下命令(在您的任何位置)进行克隆:

git clone git@github.com:PacktPublishing/Practical-Internet-of-Things-with-JavaScript.git

或者您也可以从github.com/PacktPublishing/Practical-Internet-of-Things-with-JavaScript下载 zip 文件。

一旦存储库被下载,cd进入base文件夹,并将mobile-app-base文件夹复制到chapter2文件夹中。

复制完成后,cd进入mobile-app文件夹并运行以下命令:

npm install

然后

ionic cordova platform add android 

或者

 ionic cordova platform add ios 

这将负责安装所需的依赖项并添加 Android 或 iOS 平台。

如果我们查看mobile-app文件夹,应该会看到以下内容:

.

├── README.md

├── config.xml

├── hooks

│ └── README.md

├── ionic.config.json

├── package.json

├── platforms

│ ├── android

│ └── platforms.json

├── plugins

│ ├── android.json

│ ├── cordova-plugin-console

│ ├── cordova-plugin-device

│ ├── cordova-plugin-splashscreen

│ ├── cordova-plugin-statusbar

│ ├── cordova-plugin-whitelist

│ ├── fetch.json

│ └── ionic-plugin-keyboard

├── resources

│ ├── android

│ ├── icon.png

│ ├── ios

│ └── splash.png

├── src

│ ├── app

│ ├── assets

│ ├── declarations.d.ts

│ ├── index.html

│ ├── manifest.json

│ ├── pages

│ ├── service-worker.js

│ ├── services

│ └── theme

├── tsconfig.json

├── tslint.json

└── www

├── assets

├── build

├── index.html

├── manifest.json

└── service-worker.js

在我们的mobile-app文件夹中,最重要的文件是mobile-app/config.xml。该文件包含了 cordova 需要将 HTML/CSS/JS 应用程序转换为混合移动应用程序所需的定义。

接下来,我们有mobile-app/resourcesmobile-app/pluginsmobile-app/platforms文件夹,其中包含我们正在开发的应用程序的 cordova 封装代码。

最后,mobile-app/src文件夹,这个文件夹是我们所有源代码的所在地。移动端的设置与我们为 web 应用程序和桌面应用程序所做的设置类似。我们有一个服务文件夹,其中包括mobile-app/src/services/auth.service.ts用于身份验证,mobile-app/src/services/device.service.ts用于与设备 API 进行交互,mobile-app/src/services/data.service.ts用于从设备获取最新数据,mobile-app/src/services/socket.service.ts用于在我们的移动应用程序中设置 Web 套接字,最后,mobile-app/src/services/toast.service.ts用于显示适用于移动设备的通知。mobile-app/src/services/toast.service.ts类似于我们在 web 和桌面应用程序中使用的通知服务。

接下来,我们有所需的页面。移动应用程序只实现了登录页面。我们强制用户使用 Web 或桌面应用程序来创建新帐户。mobile-app/src/pages/login/login.ts包括身份验证逻辑。mobile-app/src/pages/home/home.ts包括用户注册的所有设备列表。mobile-app/src/pages/add-device/add-device.ts具有添加新设备所需的逻辑,mobile-app/src/pages/view-device/view-device.ts用于查看设备信息。

现在,在mobile-app文件夹中,运行以下命令:

ionic serve  

这将在浏览器中启动应用程序。如果您想在实际应用程序上进行测试,可以运行以下命令:

ionic cordova run android   

或者,您可以运行以下命令:

ionic cordova run ios  

这将在设备上启动应用程序。在任何情况下,应用程序的行为都将相同。

应用程序启动后,我们将看到登录页面:

一旦我们成功登录,我们应该看到如下的主页。我们可以使用标题栏中的+图标添加新设备:

新创建的设备应该在我们的主屏幕上反映出来,如下所示:

如果我们点击“查看设备”,我们应该看到设备信息,如下所示:

当我们切换按钮开/关时,树莓派上的 LED 应该打开/关闭:

同一设置的另一个视图如下所示:

上述是使用 DHT11 传感器和 LED 设置的树莓派 3 的设置。

通过这样做,我们已经成功建立了一个端到端的架构,用于执行物联网示例。从现在开始,我们将与 Web 应用程序、移动应用程序、桌面应用程序、树莓派以及一些 API 引擎一起工作,用于我们接下来的示例。我们将进行最小的更改。我们将专注于用例,而不是一遍又一遍地构建设置。

故障排除

如果您没有看到预期的输出,请检查以下内容:

  • 检查经纪人、API 引擎、Web 应用程序和树莓派应用程序是否正在运行

  • 检查提供给树莓派的经纪人的 IP 地址

  • 检查提供给移动应用程序的 API 引擎的 IP 地址

摘要

在第二章,IoTFW.js - I和在本章中,我们经历了设置整个框架以与物联网解决方案一起工作的整个过程。我们只使用 JavaScript 作为编程语言构建了整个框架。

我们从理解架构和数据流开始,从树莓派到最终用户设备,如 Web 应用程序、桌面应用程序或移动应用程序。然后我们开始使用 Mosca 设置经纪人,设置 MongoDB 后。接下来,我们设计并开发了 API 引擎,并完成了基本的树莓派设置。

我们在 Web 应用程序和桌面应用程序上工作,并将简单的 LED 和 DHT11 温湿度传感器与树莓派集成,并看到了从一端到另一端的简单流程。我们将温度和湿度实时传输到 Web 应用程序和桌面应用程序,并使用切换按钮打开了 LED。

最后,我们建立了一个移动应用程序,并实现/验证了 LED 和 DHT11 的设置。

在第四章,智能农业,使用当前设置作为基础,我们将构建智能农业解决方案。

第四章:智能农业

在本章中,我们将使用我们在第二章中构建的框架,IoTFW.js - I和第三章,IoTFW.js - II,并开始处理各种用例。我们将从农业领域开始,在本章中建立一个智能气象站。

任何农民的一个简单要求是能够了解他们农场附近和周围的环境因素。因此,我们将建立一个便携式气象站的原型。在本章中,我们将研究以下主题:

  • 农业和物联网

  • 设计智能气象站

  • 为 Raspberry Pi 3 开发代码

  • 在 API 引擎中更新 MQTT 代码

  • 修改 Web 应用程序、桌面应用程序和移动应用程序的模板

农业和物联网

Beecham Research 的一份报告预测,到 2025 年,全球人口将达到 80 亿,到 2050 年将达到 96 亿,为了跟上步伐,到 2050 年,粮食产量必须增加 70%。这是报告:www.beechamresearch.com/files/BRL%20Smart%20Farming%20Executive%20Summary.pdf

这意味着我们需要找到更快更有效的耕作方式。随着我们不断朝着 2050 年迈进,土地和资源将变得更加稀缺。这是因为,如果有资源的话,我们需要喂饱比以往任何时候都更多的人,除非有僵尸启示录发生,我们所有人都被其他僵尸吃掉!

这是技术提供解决方案的一个很好的机会。物联网几乎一直是关于智能家居、智能办公室和便利性,但它可以做得更多。这就是我们将在本章中涵盖的内容。我们将建立一个智能气象站,农民可以在他们的农场部署,以获取实时的温度、湿度、土壤湿度和雨水检测等指标。

其他传感器可以根据可用性和需求进行添加。

设计智能气象站

既然我们知道我们要构建什么以及为什么,让我们开始设计。我们要做的第一件事是确定所需的传感器。在这个智能气象站中,我们将使用以下传感器:

  • 温度传感器

  • 湿度传感器

  • 土壤湿度传感器

  • 雨水检测传感器

我选择了现成的传感器,以展示概念的证明。它们中的大多数对于测试和验证想法或作为爱好来说都很好,但不适合生产。

我们将把这些传感器连接到我们的 Raspberry Pi 3 上。我们将使用以下型号的传感器:

您也可以在其他地方购买这些传感器。

正如我们在第三章中看到的,温度和湿度传感器是数字传感器,我们将使用node-dht-sensor模块来读取温度和湿度值。土壤湿度传感器是模拟传感器,而 Raspberry Pi 3 没有任何模拟引脚。因此,我们将使用 Microchip 的 MCP3208 12 位 A/D IC,将传感器的模拟输出转换为数字,并通过 SPI 协议传输到 Raspberry Pi。

维基百科对 SPI 协议的定义如下:

串行外围接口SPI)总线是一种用于短距离通信的同步串行通信接口规范,主要用于嵌入式系统。该接口是由 Motorola 在 20 世纪 80 年代后期开发的,并已成为事实上的标准。有关 SPI 的更多信息,请参阅:en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus

雨水检测传感器可以被读取为模拟和数字信号。我们将使用模拟输出来检测雨水的级别,而不仅仅是是否下雨。

回到 MCP3208,它是一个 16 引脚封装,可以同时读取八个模拟输入,并可以将它们转换并通过 SPI 协议馈送到树莓派。您可以在这里阅读更多关于 MCP3208 IC 的信息:http://ww1.microchip.com/downloads/en/DeviceDoc/21298c.pdf。您可以从这里购买:www.amazon.com/Adafruit-MCP3008-8-Channel-Interface-Raspberry/dp/B00NAY3RB2/ref=sr_1_1

我们将直接将温度和湿度传感器连接到树莓派 3,将湿度传感器和雨水传感器连接到 MCP3208,然后将 MCP3208 通过 SPI 连接到树莓派 3。

在代理上,我们不打算改变任何东西。在 API 引擎中,我们将向 MQTT 客户端添加一个名为weather-status的新主题,然后将来自树莓派 3 的数据发送到这个主题。

在 Web 应用程序中,我们将更新用于查看天气指标的模板。桌面应用程序和移动应用程序也是如此。

设置树莓派 3

让我们开始制作原理图。

设置树莓派 3 和传感器如下所示:

这里是一个显示这些连接的表格:

树莓派和 MCP3208

参考以下表格:

树莓派引脚编号 - 引脚名称 MCP3208 引脚编号 - 引脚名称
1 - 3.3V 16 - VDD
1 - 3.3V 15 - AREF
6 - GND 14 - AGND
23 - GPIO11, SPI0_SCLK 13 - CLK
21 - GPIO09, SPI0_MISO 12 - DOUT
19 -GPIO10, SPI0_MOSI 11 - DIN
24 - GPIO08, CEO 10 - CS
6 - GND 9 - DGND

湿度传感器和 MCP3208

参考以下表格:

MCP3208 引脚编号 - 引脚名称 传感器名称 - 引脚编号
1 - A0 雨水传感器 - A0
2 - A1 湿度传感器 - A0

树莓派和 DHT11

参考以下表格:

树莓派编号 - 引脚名称 传感器名称 - 引脚编号
3 - GPIO2 DHT11 - Data

所有接地和所有 3.3V 都连接到一个公共点。

一旦我们按照之前显示的方式连接了传感器,我们将编写所需的代码来与传感器进行接口。

在我们继续之前,我们将把整个第二章,IoTFW.js - I,和第三章,IoTFW.js - II,的代码复制到另一个名为chapter4的文件夹中。

chapter4文件夹应该如下所示:

.

├── api-engine

│ ├── package.json

│ └── server

├── broker

│ ├── certs

│ └── index.js

├── desktop-app

│ ├── app

│ ├── freeport.js

│ ├── index.css

│ ├── index.html

│ ├── index.js

│ ├── license

│ ├── package.json

│ ├── readme.md

│ └── server.js

├── mobile-app

│ ├── config.xml

│ ├── hooks

│ ├── ionic.config.json

│ ├── package.json

│ ├── platforms

│ ├── plugins

│ ├── resources

│ ├── src

│ ├── tsconfig.json

│ ├── tslint.json

│ └── www

└── web-app

├── README.md

├── e2e

├── karma.conf.js

├── package.json

├── protractor.conf.js

├── src

├── tsconfig.json

└── tslint.json

我们将返回到树莓派,并在pi-client文件夹中更新index.js文件。更新pi-client/index.js,如下所示:

var config = require('./config.js');

var mqtt = require('mqtt');

var GetMac = require('getmac');

var async = require('async');

var rpiDhtSensor = require('rpi-dht-sensor');

var McpAdc = require('mcp-adc');

var adc = new McpAdc.Mcp3208();

var dht11 = new rpiDhtSensor.DHT11(2);

var temp = 0,

prevTemp = 0;

var humd = 0,

prevHumd = 0;

var macAddress;

var state = 0;

var moistureVal = 0,

prevMoistureVal = 0;

var rainVal = 0,

prevRainVal = 0;

var client = mqtt.connect({

port: config.mqtt.port,

protocol: 'mqtts',

host: config.mqtt.host,

clientId: config.mqtt.clientId,

reconnectPeriod: 1000,

username: config.mqtt.clientId,

password: config.mqtt.clientId,

keepalive: 300,

rejectUnauthorized: false

});

client.on('connect', function() {

client.subscribe('rpi');

GetMac.getMac(function(err, mac) {

if (err) throw err;

macAddress = mac;

client.publish('api-engine', mac);

});

});

client.on('message', function(topic, message) {

message = message.toString();

if (topic === 'rpi') {

console.log('API Engine Response >> ', message);

} else {

console.log('Unknown topic', topic);

}

});

// infinite loop, with 3 seconds delay

setInterval(function() {

readSensorValues(function(results) {

console.log('Temperature: ' + temp + 'C, ' + 'humidity: ' + humd + '%, ' + ' Rain level (%):' + rainVal + ', ' + 'moistureVal (%): ' + moistureVal);

// if the temperature and humidity values change

// then only publish the values

if (temp !== prevTemp || humd !== prevHumd || moistureVal !== prevMoistureVal || rainVal != prevRainVal) {

var data2Send = {

data: {

t: temp,

h: humd,

r: rainVal,

m: moistureVal

},

macAddress: macAddress

};

// console.log('Data Published');

client.publish('weather-status', JSON.stringify(data2Send));

// reset prev values to current

// for next loop

prevTemp = temp;

prevHumd = humd;

prevMoistureVal = moistureVal;

prevRainVal = rainVal;

}

});

}, 3000); // every three second

// `CB` expects {

// dht11Values: val,

// rainLevel: val,

// moistureLevel: val

// }

function readSensorValues(CB) {

async.parallel({

dht11Values: function(callback) {

var readout = dht11.read();

// update global variable

temp = readout.temperature.toFixed(2);

humd = readout.humidity.toFixed(2);

callback(null, { temp: temp, humidity: humd });

},

rainLevel: function(callback) {

// we are going to connect rain sensor

// on channel 0, hence 0 is the first arg below

adc.readRawValue(0, function(value) {

// update global variable

rainVal = value;

rainVal = (100 - parseFloat((rainVal / 4096) * 100)).toFixed(2);

callback(null, { rain: rainVal });

});

},

moistureLevel: function(callback) {

// we are going to connect moisture sensor

// on channel 1, hence 1 is the first arg below

adc.readRawValue(1, function(value) {

// update global variable

moistureVal = value;

moistureVal = (100 - parseFloat((moistureVal / 4096) * 100)).toFixed(2);

callback(null, { moisture: moistureVal });

});

}

}, function done(err, results) {

if (err) {

throw err;

}

// console.log(results);

if (CB) CB(results);

});

}

在前面的代码中,我们保留了 MQTT 设置。我们添加了mcp-adcgithub.com/anha1/mcp-adc)和asyncgithub.com/caolan/async)模块。mcp-adc管理 MCP3208 暴露的 SPI 协议接口,我们使用async模块并行读取所有传感器的数据。

我们首先通过 MQTTS 与代理建立连接。然后,我们使用setInterval()设置了一个间隔为 3 秒的无限循环。在setInterval()callback中,我们调用readSensorValues()来获取最新的传感器数值。

readSensorValues()使用async.parallel()并行读取三个传感器的数据,并更新我们定义的全局变量中的数据。一旦收集到所有传感器数据,我们将结果作为参数传递给callback函数。

一旦我们收到传感器数据,我们将检查温度、湿度、雨量和湿度数值之间是否有变化。如果没有变化,我们就放松;否则,我们将把这些数据发布到天气状态主题的代理上。

保存所有文件。现在,我们将从我们的桌面机器上启动 Mosca 代理:

mosca -c index.js -v | pino

一旦您启动了 Mosca 服务器,请检查 Mosca 运行的服务器的 IP 地址。在树莓派的config.js文件中更新相同的 IP。否则,树莓派无法将数据发布到代理。

一旦 Mosca 成功启动并且我们已经验证了 IP,就在树莓派上运行这个:

sudo node index.js

这将启动服务器,我们应该看到以下内容:

当我启动树莓派时,雨传感器是干的,湿度传感器被放置在干燥的土壤中。最初,雨传感器的值为1.86%,湿度传感器的值为4.57%

当我向植物/湿度传感器添加水时,百分比增加到98.83%;同样,当我在雨传感器上模拟降雨时,数值上升到89.48%

这是我智能气象站的原型设置:

蓝色芯片是 DHT11,湿度传感器被放置在我的桌边植物中,雨传感器被放置在一个塑料托盘中,用于收集雨水。面包板上有 MCP3208 IC 和所需的连接。

很多电线!

通过这样,我们完成了树莓派 3 所需的代码。在下一节中,我们将设置 API 引擎所需的代码。

设置 API 引擎

在最后一节中,我们已经看到了如何设置组件和代码,以便使用树莓派 3 建立智能气象站。现在,我们将致力于管理从树莓派 3 接收的数据的 API 引擎。

打开api-engine/server/mqtt/index.js并更新,如下所示:

var Data = require('../api/data/data.model'); 
var mqtt = require('mqtt'); 
var config = require('../config/environment'); 

var client = mqtt.connect({ 
port: config.mqtt.port, 
protocol: 'mqtts', 
host: config.mqtt.host, 
clientId: config.mqtt.clientId, 
reconnectPeriod: 1000, 
username: config.mqtt.clientId, 
password: config.mqtt.clientId, 
keepalive: 300, 
rejectUnauthorized: false 
}); 

client.on('connect', function() { 
console.log('Connected to Mosca at ' + config.mqtt.host + ' on port ' + config.mqtt.port); 
client.subscribe('api-engine'); 
client.subscribe('weather-status'); 
}); 

client.on('message', function(topic, message) { 
    // message is Buffer 
    // console.log('Topic >> ', topic); 
    // console.log('Message >> ', message.toString()); 
if (topic === 'api-engine') { 
varmacAddress = message.toString(); 
console.log('Mac Address >> ', macAddress); 
client.publish('rpi', 'Got Mac Address: ' + macAddress); 
    } else if (topic === 'weather-status') { 
var data = JSON.parse(message.toString()); 
        // create a new data record for the device 
Data.create(data, function(err, data) { 
if (err) return console.error(err); 
            // if the record has been saved successfully,  
            // websockets will trigger a message to the web-app 
console.log('Data Saved :', data.data); 
        }); 
    } else { 
console.log('Unknown topic', topic); 
    } 
}); 

在这里,我们正在等待weather-status主题上的消息,当我们从树莓派接收到数据时,我们将其保存到我们的数据库,并将数据推送到 Web 应用程序、移动应用程序和桌面应用程序。

这些是我们需要做的所有更改,以吸收来自树莓派 3 的数据并传递给 Web、桌面和移动应用程序。

保存所有文件并运行以下代码:

npm start  

这将启动 API 引擎并连接到 Mosca,以及树莓派:

如果我们让 API 引擎运行一段时间,我们应该会看到以下内容:

设备的数据在这里记录。

在下一节中,我们将更新 Web 应用程序,以便表示来自 API 引擎的数据。

设置 Web 应用程序

现在我们已经完成了 API 引擎,我们将开发所需的界面,以显示来自树莓派 3 的天气输出。

打开web-app/src/app/device/device.component.html并更新,如下所示:

<div class="container">
    <br>
    <div *ngIf="!device">
        <h3 class="text-center">Loading!</h3>
    </div>
    <div class="row" *ngIf="lastRecord">
        <div class="col-md-12">
            <div class="panel panel-info">
                <div class="panel-heading">
                    <h3 class="panel-title">
                        {{device.name}}
                    </h3>
                    <span class="pull-right btn-click">
                        <i class="fa fa-chevron-circle-up"></i>
                    </span>
                </div>
                <div class="clearfix"></div>
                <div class="table-responsive">
                    <table class="table table-striped">
                        <tr *ngIf="lastRecord">
                            <td>Temperature</td>
                            <td>{{lastRecord.data.t}}</td>
                        </tr>
                        <tr *ngIf="lastRecord">
                            <td>Humidity</td>
                            <td>{{lastRecord.data.h}} %</td>
                        </tr>
                        <tr *ngIf="lastRecord">
                            <td>Rain Level</td>
                            <td>{{lastRecord.data.r}} %</td>
                        </tr>
                        <tr *ngIf="lastRecord">
                            <td>Mositure Level</td>
                            <td>{{lastRecord.data.m}} %</td>
                        </tr>
                        <tr *ngIf="lastRecord">
                            <td>Received At</td>
                            <td>{{lastRecord.createdAt | date: 'medium'}}</td>
                        </tr>
                    </table>
                    <div class="col-md-6" *ngIf="tempHumdData.length > 0">
                        <canvas baseChart [datasets]="tempHumdData" [labels]="lineChartLabels" [options]="lineChartOptions" [legend]="lineChartLegend" [chartType]="lineChartType"></canvas>
                    </div>

                    <div class="col-md-6" *ngIf="rainMoisData.length > 0">
                        <canvas baseChart [datasets]="rainMoisData" [labels]="lineChartLabels" [options]="lineChartOptions" [legend]="lineChartLegend" [chartType]="lineChartType"></canvas>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

在上述代码中,我们在表中添加了四行,显示温度、湿度、雨量和湿度等级。我们还设置了画布来显示图表中的数值。

接下来是DeviceComponent的类定义,位于web-app/src/app/device/device.component.ts中。按照下面的示例更新web-app/src/app/device/device.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core'; 
import { DevicesService } from '../services/devices.service'; 
import { Params, ActivatedRoute } from '@angular/router'; 
import { SocketService } from '../services/socket.service'; 
import { DataService } from '../services/data.service'; 
import { NotificationsService } from 'angular2-notifications'; 

@Component({ 
   selector: 'app-device', 
   templateUrl: './device.component.html', 
   styleUrls: ['./device.component.css'] 
}) 
export class DeviceComponent implements OnInit, OnDestroy { 
   device: any; 
   data: Array<any>; 
   toggleState: boolean = false; 
   privatesubDevice: any; 
   privatesubData: any; 
   lastRecord: any; 

   // line chart config 
   publiclineChartOptions: any = { 
         responsive: true, 
         legend: { 
               position: 'bottom', 
         }, hover: { 
               mode: 'label' 
         }, scales: { 
               xAxes: [{ 
                     display: true, 
                     scaleLabel: { 
                           display: true, 
                           labelString: 'Time' 
                     } 
               }], 
               yAxes: [{ 
                     display: true, 
                     ticks: { 
                           beginAtZero: true, 
                           // steps: 10, 
                           // stepValue: 5, 
                           // max: 70 
                     } 
               }] 
         }, 
         title: { 
               display: true, 
               text: 'Sensor Data vs. Time' 
         } 
   }; 
   publiclineChartLegend: boolean = true; 
   publiclineChartType: string = 'line'; 
   publictempHumdData: Array<any> = []; 
   publicrainMoisData: Array<any> = []; 
   publiclineChartLabels: Array<any> = []; 

   constructor(private deviceService: DevicesService, 
         privatesocketService: SocketService, 
         privatedataService: DataService, 
         private route: ActivatedRoute, 
         privatenotificationsService: NotificationsService) { } 

   ngOnInit() { 
         this.subDevice = this.route.params.subscribe((params) => { 
               this.deviceService.getOne(params['id']).subscribe((response) => { 
                     this.device = response.json(); 
                     this.getData(); 
                     this.socketInit(); 
               }); 
         }); 
   } 

   getData() { 
         this.dataService.get(this.device.macAddress).subscribe((response) => { 
               this.data = response.json(); 
               this.lastRecord = this.data[0]; // descending order data 
               this.genChart(); 
         }); 
   } 

   socketInit() { 
         this.subData = this.socketService.getData(this.device.macAddress).subscribe((data) => { 
               if (this.data.length<= 0) return; 
               this.data.splice(this.data.length - 1, 1); // remove the last record 
               this.data.push(data); // add the new one 
               this.lastRecord = data; 
               this.genChart(); 
         }); 
   } 

   ngOnDestroy() { 
         this.subDevice.unsubscribe(); 
         this.subData ? this.subData.unsubscribe() : ''; 
   } 

   genChart() { 
         let data = this.data; 
         let _thArr: Array<any> = []; 
         let _rmArr: Array<any> = []; 
         let _lblArr: Array<any> = []; 

         lettmpArr: Array<any> = []; 
         lethumArr: Array<any> = []; 
         letraiArr: Array<any> = []; 
         letmoiArr: Array<any> = []; 

         for (vari = 0; i<data.length; i++) { 
               let _d = data[i]; 
               tmpArr.push(_d.data.t); 
               humArr.push(_d.data.h); 
               raiArr.push(_d.data.r); 
               moiArr.push(_d.data.m); 
               _lblArr.push(this.formatDate(_d.createdAt)); 
         } 

         // reverse data to show the latest on the right side 
         tmpArr.reverse(); 
         humArr.reverse(); 
         raiArr.reverse(); 
         moiArr.reverse(); 
         _lblArr.reverse(); 

         _thArr = [ 
               { 
                     data: tmpArr, 
                     label: 'Temperature' 
               }, 
               { 
                     data: humArr, 
                     label: 'Humidity %' 
               } 
         ] 

         _rmArr = [ 
               { 
                     data: raiArr, 
                     label: 'Rain Levels' 
               }, 
               { 
                     data: moiArr, 
                     label: 'Moisture Levels' 
               } 
         ] 

         this.tempHumdData = _thArr; 
         this.rainMoisData = _rmArr; 

         this.lineChartLabels = _lblArr; 
   } 

   privateformatDate(originalTime) { 
         var d = new Date(originalTime); 
         vardatestring = d.getDate() + "-" + (d.getMonth() + 1) + "-" + d.getFullYear() + " " + 
               d.getHours() + ":" + d.getMinutes(); 
         returndatestring; 
   } 

} 

在上述代码中,我们使用了ngOnInit钩子,并发出请求以获取设备数据。使用socketInit(),连同数据,我们将为当前设备注册套接字数据事件。

getData()中,我们从服务器获取数据,提取最新记录,并将其设置为lastRecord属性。最后,我们调用genChart()来绘制图表。

现在,我们已经完成了所需的更改。保存所有文件并运行以下命令:

ng server

如果我们导航到http://localhost:4200,登录,并点击DEVICE,我们应该看到以下内容:

每当数据发生变化时,我们应该看到 UI 会自动更新。

在下一节中,我们将构建相同的应用程序并在电子外壳中显示它。

设置桌面应用程序

在上一节中,我们为 Web 应用程序开发了模板和界面。在本节中,我们将构建相同的内容并将其放入桌面应用程序中。

要开始,请返回到web-app文件夹的终端/提示符,并运行以下命令:

ng build --env=prod

这将在web-app文件夹内创建一个名为dist的新文件夹。dist文件夹的内容应包括:

.

├── favicon.ico

├── index.html

├── inline.bundle.js

├── inline.bundle.js.map

├── main.bundle.js

├── main.bundle.js.map

├── polyfills.bundle.js

├── polyfills.bundle.js.map

├── scripts.bundle.js

├── scripts.bundle.js.map

├── styles.bundle.js

├── styles.bundle.js.map

├── vendor.bundle.js

└── vendor.bundle.js.map

我们编写的所有代码最终都打包到了上述文件中。我们将获取dist文件夹内的所有文件(而不是dist文件夹),然后将它们粘贴到desktop-app/app文件夹内。在进行上述更改后,桌面应用程序的最终结构将如下所示:

.

├── app

│ ├── favicon.ico

│ ├── index.html

│ ├── inline.bundle.js

│ ├── inline.bundle.js.map

│ ├── main.bundle.js

│ ├── main.bundle.js.map

│ ├── polyfills.bundle.js

│ ├── polyfills.bundle.js.map

│ ├── scripts.bundle.js

│ ├── scripts.bundle.js.map

│ ├── styles.bundle.js

│ ├── styles.bundle.js.map

│ ├── vendor.bundle.js

│ └── vendor.bundle.js.map

├── freeport.js

├── index.css

├── index.html

├── index.js

├── license

├── package.json

├── readme.md

└── server.js

为了测试该过程,请运行以下命令:

npm start

导航到DEVICE页面,我们应该看到以下内容:

每当数据发生变化时,我们应该看到 UI 会自动更新。

通过这样,我们已经完成了桌面应用程序的开发。在下一节中,我们将更新移动应用程序。

设置移动应用程序

在上一节中,我们看到了如何为智能气象站构建和运行桌面应用程序。在本节中,我们将更新移动应用程序的模板,以显示气象站数据。

打开mobile-app/src/pages/view-device/view-device.html并更新它,如下所示:

<ion-header>
    <ion-navbar>
        <ion-title>Mobile App</ion-title>
    </ion-navbar>
</ion-header>
<ion-content padding>
    <div *ngIf="!lastRecord">
        <h3 class="text-center">Loading!</h3>
    </div>
    <div *ngIf="lastRecord">
        <ion-list>
            <ion-item>
                <ion-label>Name</ion-label>
                <ion-label>{{device.name}}</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Temperature</ion-label>
                <ion-label>{{lastRecord.data.t}}</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Humidity</ion-label>
                <ion-label>{{lastRecord.data.h}} %</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Rain Level</ion-label>
                <ion-label>{{lastRecord.data.r}} %</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Moisture Level</ion-label>
                <ion-label>{{lastRecord.data.m}} %</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Received At</ion-label>
                <ion-label>{{lastRecord.createdAt | date: 'medium'}}</ion-label>
            </ion-item>
        </ion-list>
    </div>
</ion-content>

在上述代码中,我们在列表视图中创建了四个项目,用于显示温度、湿度、降雨量和湿度水平。ViewDevicePage类的所需逻辑将存在于mobile-app/src/pages/view-device/view-device.ts中。按照下面所示更新mobile-app/src/pages/view-device/view-device.ts

import { Component } from '@angular/core'; 
import { IonicPage, NavController, NavParams } from 'ionic-angular'; 

import { DevicesService } from '../../services/device.service'; 
import { DataService } from '../../services/data.service'; 
import { ToastService } from '../../services/toast.service'; 
import { SocketService } from '../../services/socket.service'; 

@IonicPage() 
@Component({ 
   selector: 'page-view-device', 
   templateUrl: 'view-device.html', 
}) 
export class ViewDevicePage { 
   device: any; 
   data: Array<any>; 
   toggleState: boolean = false; 
   privatesubData: any; 
   lastRecord: any; 

   constructor(private navCtrl: NavController, 
         privatenavParams: NavParams, 
         privatesocketService: SocketService, 
         privatedeviceService: DevicesService, 
         privatedataService: DataService, 
         privatetoastService: ToastService) { 
         this.device = navParams.get("device"); 
         console.log(this.device); 
   } 

   ionViewDidLoad() { 
         this.deviceService.getOne(this.device._id).subscribe((response) => { 
               this.device = response.json(); 
               this.getData(); 
               this.socketInit(); 
         }); 
   } 

   getData() { 
         this.dataService.get(this.device.macAddress).subscribe((response) => { 
               this.data = response.json(); 
               this.lastRecord = this.data[0]; // descending order data 
         }); 
   } 

   socketInit() { 
         this.subData = this.socketService.getData(this.device.macAddress).subscribe((data) => { 
               if(this.data.length<= 0) return; 
               this.data.splice(this.data.length - 1, 1); // remove the last record 
               this.data.push(data); // add the new one 
               this.lastRecord = data; 
         }); 
   } 

   ionViewDidUnload() { 
         this.subData&&this.subData.unsubscribe&&this.subData.unsubscribe(); //unsubscribe if subData is defined 
   } 
} 

在上述代码中,我们使用getData()从 API 引擎获取最新数据。然后,使用socketInit(),我们订阅了数据的最新更改。

检查 API 引擎运行的服务器的 IP 地址。在移动应用程序的mobile-app/src/app/app.globals.ts文件中更新相同的 IP。否则,移动应用程序无法与 API 引擎通信。

现在,保存所有文件并运行以下命令:

ionic serve

或者,您也可以通过运行以下命令将其部署到您的设备上:

ionic run android 

 ionic run ios

应用程序启动后,当我们导航到DEVICE页面时,我们应该在屏幕上看到以下内容:

从图像中可以看出,我们能够实时查看数据更新。

摘要

在本章中,我们利用了第二章和第三章中所学到的知识,构建了智能气象站的原型。我们首先确定了构建气象站所需的传感器。接下来,我们在树莓派 3 上设置了它们。我们编写了与传感器进行接口的所需代码。完成这些工作后,我们更新了 API 引擎,以从树莓派 3 上的新主题读取数据。一旦 API 引擎接收到数据,我们就将其保存在数据库中,然后通过 Web 套接字将其发送到 Web、桌面和移动应用程序。最后,我们更新了 Web、桌面和移动应用程序上的演示模板;然后,我们在 Web、桌面和移动应用程序上显示了来自树莓派的数据。

在第五章中,智能农业和语音人工智能,我们将使用亚马逊的 Alexa 和我们建立的智能气象站来进行语音人工智能的工作。

第五章:智能农业和语音人工智能

在第四章中,智能农业,我们已经看到了物联网可以产生影响的主流领域之一;农业部门。在本章中,我们将把这一点提升到一个新的水平。使用亚马逊 Alexa 等语音人工智能引擎,我们将与我们建立的智能气象站交谈。

例如,一个农民可以问 Alexa “Alexa,请问 smarty app 我的农场的湿度水平是多少”,然后 Alexa 会回答 “你的农场湿度水平是 20%。考虑现在浇水”。然后,农民会说 “Alexa,请问 smarty app 打开我的发动机”,然后 Alexa 会打开它。很迷人,不是吗?

一般来说,基于语音人工智能的物联网在智能家居和智能办公的概念中更为常见。我想在智能农业中实现它。

在本章中,我们将致力于以下工作:

  • 了解亚马逊 Alexa

  • 构建一个由 IoT.js 控制的水泵

  • 了解 AWS lambda

  • 为亚马逊 Alexa 开发技能

  • 测试气象站以及水泵

语音人工智能

曾经有一段时间,用智能手机打开/关闭某物是令人兴奋的。时代已经改变,语音人工智能的发展已经有了很大的进步。很多人用他们的声音做很多事情,从做笔记、建立购物清单到搜索互联网。我们不再用手做琐碎的活动。

“看吧,不用手!”

接下来呢?想到了就会发生吗?我很想活着看到那一天,因为我可以以思维的速度做事情。

如果你是语音人工智能的新手,你可以开始查找亚马逊 Alexa、Google Now/Google Assistant、Apple Siri 或 Windows Cortana,看看我在说什么。由于我们将在本章中使用亚马逊 Alexa,我们只会探索它。

亚马逊最近推出了两款名为亚马逊 Echo 和亚马逊 Echo Dot(最近也在印度上市),它们是由 Alexa,亚马逊的语音人工智能软件驱动的智能音箱。如果你想亲自体验 Alexa,而又不想购买 Echo 产品,可以在 Android 上下载 reverb 应用:play.google.com/store/apps/details?id=agency.rain.android.alexa&hl=en 或者在 iOS 上下载:itunes.apple.com/us/app/reverb-for-amazon-alexa/id1144695621?mt=8,然后启动该应用。

你应该看到一个带有麦克风图标的界面。按住麦克风,你应该在顶部看到“正在听...”的文字,就像下面的截图所示:

现在说,“Alexa,给我讲个笑话”,然后被 Alexa 娱乐吧!

试驾

为了测试我们将要构建的东西,在 reverb 应用中按下麦克风图标,然后说 “Alexa,请问 smarty app 天气报告”,你应该听到保存在智能气象站数据库中的最新数据。然后你可以说 “Alexa,请问 smarty app 打开发动机”,或者 “Alexa,请问 smarty app 关闭发动机”;如果我的设备在线,它会关闭它。

除了智能气象站,我们还将建立一个智能插座,可以连接到农场中的发动机。然后使用 Alexa,我们将打开/关闭发动机。

现在,如果你有亚马逊 Echo 或 Echo Dot,你可以测试我们将要构建的技能。或者,你也可以使用 reverb 应用来做同样的事情。你也可以使用 reverb.ai/echosim.io/ 来做同样的事情。

在你的 Alexa 技能发布之前,它只能在与你的亚马逊账户关联的设备上访问。如果你启用了测试版,那么你可以允许多人在他们的亚马逊账户关联的 Alexa 设备上访问这个技能。

如果你在探索演示时遇到问题,请查看这个视频录制:/videos/chapter5/alexa_smarty_app_demo.mov

那么,让我们开始吧!

构建智能插座

在本节中,我们将构建一个智能插座。设置将与第四章中的设置非常相似。创建一个名为chapter5的新文件夹,并将chapter4文件夹的内容复制到其中。chapter4文件夹中包含智能气象站的代码,现在,我们将添加智能插座所需的代码。

智能插座是一个可以通过互联网控制的简单电源插座。也就是说,打开插座和关闭插座。我们将使用机械继电器来实现这一点。

我们将从在树莓派上设置继电器开始。我将使用一个树莓派来演示智能气象站以及智能插座。您也可以使用两个树莓派来进行演示。

我们将向 API 引擎添加适当的 MQTT 客户端代码;接下来,更新 Web、桌面和移动应用程序,以添加一个切换开关来打开/关闭继电器。

我们将在socket上创建一个名为socket的新主题,我们将发送10来打开/关闭继电器,从而打开/关闭继电器另一端的负载。

请记住,我们正在探索可以使用物联网构建的各种解决方案,而不是构建最终产品本身。

使用树莓派设置继电器

目前,树莓派已连接智能气象站传感器。现在,我们将向设置添加一个继电器。

继电器是由电子信号驱动的电气开关。也就是说,用逻辑高1触发继电器会打开继电器,逻辑低0会关闭继电器。

一些继电器的工作方式相反,这取决于组件。要了解更多关于继电器类型和工作原理的信息,请参考www.phidgets.com/docs/Mechanical_Relay_Primer

您可以从亚马逊购买一个简单的 5V 继电器:(www.amazon.com/DAOKI%C2%AE-Arduino-Indicator-Channel-Official/dp/B00XT0OSUQ/ref=sr_1_3)。

继电器处理交流电流,在我们的示例中,我们不会将任何交流电源连接到继电器。我们将使用来自树莓派的 5V 直流电源来供电,并使用继电器上的 LED 指示灯来识别继电器是否已打开或关闭。如果您想将其连接到实际电源,请在这样做之前采取适当的预防措施。如果不注意,结果可能会令人震惊。

除了气象站,我们还将把继电器连接到树莓派 3. 将继电器连接如下图所示。

树莓派与智能气象站的连接:

树莓派与继电器(模块)的连接:

如果您购买了独立的继电器,您需要按照之前显示的电路进行设置。如果您购买了继电器模块,您需要在给继电器供电后,将引脚 18/GPIO24 连接到触发引脚。

为了重申之前的连接,请参见下表所示的表格:

  • 树莓派和 MCP3208:
树莓派编号 - 引脚名称 MCP 3208 引脚编号 - 引脚名称
1 - 3.3V 16 - VDD
1 - 3.3V 15 - AREF
6 - GND 14 - AGND
23 - GPIO11, SPI0_SCLK 13 - CLK
21 - GPIO09, SPI0_MISO 12 - DOUT
19 - GPIO10, SPI0_MOSI 11 - DIN
24 - GPIO08, CEO 10 - CS
6 - GND 9 - DGND
  • 湿度传感器和 MCP3208:
MCP 3208 引脚编号 - 引脚名称 传感器引脚
1 - A0 雨传感器 - A0
1 - A1 湿度传感器 - A0
  • 树莓派和 DHT11:
树莓派编号 - 引脚名称 传感器引脚
3 - GPIO2 DHT11 - 数据
  • 树莓派和继电器:
树莓派编号 - 引脚名称 传感器引脚
12 - GPIO18 继电器 - 触发引脚

所有地线和所有 3.3V 引脚都连接到一个公共点。继电器所需的只是来自树莓派的 5V 电源,即引脚 2。

一旦我们按照之前所示连接了传感器,我们将编写所需的代码来与传感器进行接口。

前往Raspberry Pi 3内的pi-client文件夹,打开pi-client/index.js,并进行如下更新:

var config = require('./config.js'); 
var mqtt = require('mqtt'); 
var GetMac = require('getmac'); 
var async = require('async'); 
var rpiDhtSensor = require('rpi-dht-sensor'); 
var McpAdc = require('mcp-adc'); 
var adc = new McpAdc.Mcp3208(); 
var rpio = require('rpio'); 

// Set pin 12 as output pin and to low 
rpio.open(12, rpio.OUTPUT, rpio.LOW); 

var dht11 = new rpiDhtSensor.DHT11(2); 
var temp = 0, 
    prevTemp = 0; 
var humd = 0, 
    prevHumd = 0; 
var macAddress; 
var state = 0; 

var mositureVal = 0, 
    prevMositureVal = 0; 
var rainVal = 0, 
    prevRainVal = 0; 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('rpi'); 
    client.subscribe('socket'); 
    GetMac.getMac(function(err, mac) { 
        if (err) throw err; 
        macAddress = mac; 
        client.publish('api-engine', mac); 
    }); 
}); 

client.on('message', function(topic, message) { 
    message = message.toString(); 
    if (topic === 'rpi') { 
        console.log('API Engine Response >> ', message); 
    } else if (topic === 'socket') { 
        state = parseInt(message) 
        console.log('Turning Relay', !state ? 'On' : 'Off'); 
        // Relays are almost always active low 
        //console.log(!state ? rpio.HIGH : rpio.LOW); 
        // If we get a 1 we turn on the relay, else off 
        rpio.write(12, !state ? rpio.HIGH : rpio.LOW); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

// infinite loop, with 3 seconds delay 
setInterval(function() { 
    readSensorValues(function(results) { 
        console.log('Temperature: ' + temp + 'C, ' + 'humidity: ' + humd + '%, ' + ' Rain level (%):' + rainVal + ', ' + 'mositureVal (%): ' + mositureVal); 
        // if the temperature and humidity values change 
        // then only publish the values 
        if (temp !== prevTemp || humd !== prevHumd || mositureVal !== prevMositureVal || rainVal != prevRainVal) { 
            var data2Send = { 
                data: { 
                    t: temp, 
                    h: humd, 
                    r: rainVal, 
                    m: mositureVal, 
                    s: state 
                }, 
                macAddress: macAddress 
            }; 
            // console.log('Data Published'); 
            client.publish('weather-status', JSON.stringify(data2Send)); 
            // reset prev values to current 
            // for next loop 
            prevTemp = temp; 
            prevHumd = humd; 
            prevMositureVal = mositureVal; 
            prevRainVal = rainVal; 
        } 
    }); 
}, 3000); // every three second 

function readSensorValues(CB) { 
    async.parallel({ 
        dht11Values: function(callback) { 
            var readout = dht11.read(); 
            // update global variable 
            temp = readout.temperature.toFixed(2); 
            humd = readout.humidity.toFixed(2); 
            callback(null, { temp: temp, humidity: humd }); 
        }, 
        rainLevel: function(callback) { 
            // we are going to connect rain sensor 
            // on channel 0, hence 0 is the first arg below 
            adc.readRawValue(0, function(value) { 
                // update global variable 
                rainVal = value; 
                rainVal = (100 - parseFloat((rainVal / 4096) * 100)).toFixed(2); 
                callback(null, { rain: rainVal }); 
            }); 
        }, 
        moistureLevel: function(callback) { 
            // we are going to connect mositure sensor 
            // on channel 1, hence 1 is the first arg below 
            adc.readRawValue(1, function(value) { 
                // update global variable 
                mositureVal = value; 
                mositureVal = (100 - parseFloat((mositureVal / 4096) * 100)).toFixed(2); 
                callback(null, { moisture: mositureVal }); 
            }); 
        } 
    }, function done(err, results) { 
        if (err) { 
            throw err; 
        } 
        // console.log(results); 
        if (CB) CB(results); 
    }); 
} 

对于Weather Station代码,我们已经添加了rpio模块,并使用rpio.open(),我们已经将引脚 12 设置为输出引脚。我们还在名为 socket 的主题上进行监听。当我们从代理在此主题上收到响应时,我们根据数据将引脚 12 设置为高电平或低电平。

现在,我们将在树莓派pi-client文件夹内安装rpio模块,并运行以下命令:

npm install rpio -save  

保存所有文件。现在,我们将从我们的桌面/机器上启动 Mosca 代理:

mosca -c index.js -v | pino  

一旦您启动了 Mosca 服务器,请检查 Mosca 正在运行的服务器的 IP 地址。在树莓派config.js文件中更新相同的 IP,否则树莓派无法将数据发布到代理。

一旦 Mosca 成功启动并且我们已经验证了树莓派上的 IP 地址,请运行:

sudo node index.js 

这将启动服务器并继续向代理发送天气信息。

在下一节中,我们将编写 API 引擎处理继电器所需的逻辑。

在 API 引擎中管理继电器

现在继电器已连接到树莓派,我们将编写逻辑,将打开/关闭命令发送到 socket 主题。打开api-engine/server/mqtt/index.js并进行如下更新:

var Data = require('../api/data/data.model'); 
var mqtt = require('mqtt'); 
var config = require('../config/environment'); 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    console.log('Connected to Mosca at ' + config.mqtt.host + ' on port ' + config.mqtt.port); 
    client.subscribe('api-engine'); 
    client.subscribe('weather-status'); 
}); 

client.on('message', function(topic, message) { 
    // message is Buffer 
    // console.log('Topic >> ', topic); 
    // console.log('Message >> ', message.toString()); 
    if (topic === 'api-engine') { 
        var macAddress = message.toString(); 
        console.log('Mac Address >> ', macAddress); 
        client.publish('rpi', 'Got Mac Address: ' + macAddress); 
    } else if (topic === 'weather-status') { 
        var data = JSON.parse(message.toString()); 
        // create a new data record for the device 
        Data.create(data, function(err, data) { 
            if (err) return console.error(err); 
            // if the record has been saved successfully,  
            // websockets will trigger a message to the web-app 
            console.log('Data Saved :', data.data); 
        }); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

exports.sendSocketData = function(data) { 
    console.log('Sending Data', data); 
    client.publish('socket', JSON.stringify(data)); 
} 

我们添加了一个名为sendSocketData的方法并导出它。我们将在api-engine/server/api/data/data.controller.jscreate方法中调用此方法,如下所示:

exports.create = function(req, res, next) { 
    var data = req.body; 
    data.createdBy = req.user._id; 
    Data.create(data, function(err, _data) { 
        if (err) return res.status(500).send(err); 
        if (data.topic === 'socket') { 
            require('../../mqtt/index.js').sendSocketData(_data.data.s); // send relay value 
        } 
        return res.json(_data); 
    }); 
}; 

保存所有文件并运行:

npm start  

您应该在屏幕上看到以下内容:

请注意,控制台中打印的数据字符串中的最后一个值; s,我们还发送继电器的状态以在 UI 中显示,如果继电器打开/关闭。

有了这个,我们就完成了开发 API 引擎所需的代码。在下一节中,我们将继续处理 Web 应用程序。

更新 Web 应用程序模板

在本节中,我们将更新 Web 应用程序模板,以便拥有一个切换按钮,与我们在第二章中所拥有的非常相似,IoTFW.js - I,以及第三章,IoTFW.js - II。使用切换按钮,我们将手动打开/关闭继电器。在后面的部分中,我们将对其进行自动化。

打开web-app/src/app/device/device.component.html并进行如下更新:

<div class="container">
    <br>
    <div *ngIf="!device">
        <h3 class="text-center">Loading!</h3>
    </div>
    <div class="row" *ngIf="lastRecord">
        <div class="col-md-12">
            <div class="panel panel-info">
                <div class="panel-heading">
                    <h3 class="panel-title">
                        {{device.name}}
                    </h3>
                    <span class="pull-right btn-click">
                        <i class="fa fa-chevron-circle-up"></i>
                    </span>
                </div>
                <div class="clearfix"></div>
                <div class="table-responsive">
                    <table class="table table-striped">
                        <tr>
                            <td>Toggle Socket</td>
                            <td>
                                <ui-switch [(ngModel)]="toggleState" (change)="toggleChange($event)"></ui-switch>
                            </td>
                        </tr>
                        <tr *ngIf="lastRecord">
                            <td>Temperature</td>
                            <td>{{lastRecord.data.t}}</td>
                        </tr>
                        <tr *ngIf="lastRecord">
                            <td>Humidity</td>
                            <td>{{lastRecord.data.h}} %</td>
                        </tr>
                        <tr *ngIf="lastRecord">
                            <td>Rain Level</td>
                            <td>{{lastRecord.data.r}} %</td>
                        </tr>
                        <tr *ngIf="lastRecord">
                            <td>Mositure Level</td>
                            <td>{{lastRecord.data.m}} %</td>
                        </tr>
                        <tr *ngIf="lastRecord">
                            <td>Received At</td>
                            <td>{{lastRecord.createdAt | date: 'medium'}}</td>
                        </tr>
                    </table>
                    <div class="col-md-6" *ngIf="tempHumdData.length > 0">
                        <canvas baseChart [datasets]="tempHumdData" [labels]="lineChartLabels" [options]="lineChartOptions" [legend]="lineChartLegend" [chartType]="lineChartType"></canvas>
                    </div>
                    <div class="col-md-6" *ngIf="rainMoisData.length > 0">
                        <canvas baseChart [datasets]="rainMoisData" [labels]="lineChartLabels" [options]="lineChartOptions" [legend]="lineChartLegend" [chartType]="lineChartType"></canvas>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

我们所做的只是添加了一个显示切换按钮的新行,通过使用它,我们可以打开/关闭插座。接下来,打开web-app/src/app/device/device.component.ts并进行如下更新,以管理切换按钮所需的逻辑:

import { Component, OnInit, OnDestroy } from '@angular/core'; 
import { DevicesService } from '../services/devices.service'; 
import { Params, ActivatedRoute } from '@angular/router'; 
import { SocketService } from '../services/socket.service'; 
import { DataService } from '../services/data.service'; 
import { NotificationsService } from 'angular2-notifications'; 

@Component({ 
   selector: 'app-device', 
   templateUrl: './device.component.html', 
   styleUrls: ['./device.component.css'] 
}) 
export class DeviceComponent implements OnInit, OnDestroy { 
   device: any; 
   data: Array<any>; 
   toggleState: boolean = false; 
   private subDevice: any; 
   private subData: any; 
   lastRecord: any; 

   // line chart config 
   public lineChartOptions: any = { 
         responsive: true, 
         legend: { 
               position: 'bottom', 
         }, hover: { 
               mode: 'label' 
         }, scales: { 
               xAxes: [{ 
                     display: true, 
                     scaleLabel: { 
                           display: true, 
                           labelString: 'Time' 
                     } 
               }], 
               yAxes: [{ 
                     display: true, 
                     ticks: { 
                           beginAtZero: true, 
                           // steps: 10, 
                           // stepValue: 5, 
                           // max: 70 
                     } 
               }] 
         }, 
         title: { 
               display: true, 
               text: 'Sensor Data vs. Time' 
         } 
   }; 
   public lineChartLegend: boolean = true; 
   public lineChartType: string = 'line'; 
   public tempHumdData: Array<any> = []; 
   public rainMoisData: Array<any> = []; 
   public lineChartLabels: Array<any> = []; 

   constructor(private deviceService: DevicesService, 
         private socketService: SocketService, 
         private dataService: DataService, 
         private route: ActivatedRoute, 
         private notificationsService: NotificationsService) { } 

   ngOnInit() { 
         this.subDevice = this.route.params.subscribe((params) => { 
               this.deviceService.getOne(params['id']).subscribe((response) => { 
                     this.device = response.json(); 
                     this.getData(); 
                     this.socketInit(); 
               }); 
         }); 
   } 

   getData() { 
         this.dataService.get(this.device.macAddress).subscribe((response) => { 
               this.data = response.json(); 
               this.lastRecord = this.data[0]; // descending order data 
               this.toggleState = this.lastRecord.data.s; 
               this.genChart(); 
         }); 
   } 

   socketInit() { 
         this.subData = this.socketService.getData(this.device.macAddress).subscribe((data) => { 
               if (this.data.length <= 0) return; 
               this.data.splice(this.data.length - 1, 1); // remove the last record 
               this.data.push(data); // add the new one 
               this.lastRecord = data; 
               this.toggleState = this.lastRecord.data.s; 
               this.genChart(); 
         }); 
   } 

   toggleChange(state) { 
         let data = { 
               macAddress: this.device.macAddress, 
               data: { 
                     t: this.lastRecord.data.t, 
                     h: this.lastRecord.data.h, 
                     m: this.lastRecord.data.m, 
                     r: this.lastRecord.data.r, 
                     s: state ? 1 : 0 
               }, 
               topic: 'socket' 
         } 

         this.dataService.create(data).subscribe((resp) => { 
               if (resp.json()._id) { 
                     this.notificationsService.success('Device Notified!'); 
               } 
         }, (err) => { 
               console.log(err); 
               this.notificationsService.error('Device Notification Failed. Check console for the error!'); 
         }) 
   } 

   ngOnDestroy() { 
         this.subDevice.unsubscribe(); 
         this.subData ? this.subData.unsubscribe() : ''; 
   } 

   genChart() { 
         let data = this.data; 
         let _thArr: Array<any> = []; 
         let _rmArr: Array<any> = []; 
         let _lblArr: Array<any> = []; 

         let tmpArr: Array<any> = []; 
         let humArr: Array<any> = []; 
         let raiArr: Array<any> = []; 
         let moiArr: Array<any> = []; 

         for (var i = 0; i < data.length; i++) { 
               let _d = data[i]; 
               tmpArr.push(_d.data.t); 
               humArr.push(_d.data.h); 
               raiArr.push(_d.data.r); 
               moiArr.push(_d.data.m); 
               _lblArr.push(this.formatDate(_d.createdAt)); 
         } 

         // reverse data to show the latest on the right side 
         tmpArr.reverse(); 
         humArr.reverse(); 
         raiArr.reverse(); 
         moiArr.reverse(); 
         _lblArr.reverse(); 

         _thArr = [ 
               { 
                     data: tmpArr, 
                     label: 'Temperature' 
               }, 
               { 
                     data: humArr, 
                     label: 'Humidity %' 
               } 
         ] 

         _rmArr = [ 
               { 
                     data: raiArr, 
                     label: 'Rain Levels' 
               }, 
               { 
                     data: moiArr, 
                     label: 'Moisture Levels' 
               } 
         ] 

         this.tempHumdData = _thArr; 
         this.rainMoisData = _rmArr; 

         this.lineChartLabels = _lblArr; 
   } 

   private formatDate(originalTime) { 
         var d = new Date(originalTime); 
         var datestring = d.getDate() + "-" + (d.getMonth() + 1) + "-" + d.getFullYear() + " " + 
               d.getHours() + ":" + d.getMinutes(); 
         return datestring; 
   } 

} 

我们在这里所做的一切就是管理切换按钮的状态。保存所有文件并运行以下命令:

ng serve

导航到http://localhost:4200,然后导航到设备页面。现在,通过页面上的切换按钮,我们可以打开/关闭继电器,如下面的截图所示:

如果一切设置正确,您应该看到继电器上的 LED 在继电器上打开/关闭,如下照片所示:

电线!嘿!

有了这个,我们就完成了 Web 应用程序。在下一节中,我们将构建相同的 Web 应用程序并将其部署到我们的桌面应用程序中。

更新桌面应用程序

现在 Web 应用程序已完成,我们将构建相同的 Web 应用程序并将其部署到我们的桌面应用程序中。

要开始,请返回到web-app文件夹的终端/提示符,并运行:

ng build --env=prod  

这将在web-app文件夹内创建一个名为dist的新文件夹。dist文件夹的内容应该如下所示:

.

├── favicon.ico

├── index.html

├── inline.bundle.js

├── inline.bundle.js.map

├── main.bundle.js

├── main.bundle.js.map

├── polyfills.bundle.js

├── polyfills.bundle.js.map

├── scripts.bundle.js

├── scripts.bundle.js.map

├── styles.bundle.js

├── styles.bundle.js.map

├── vendor.bundle.js

└── vendor.bundle.js.map

我们编写的所有代码最终都打包到了前面的文件中。我们将获取dist文件夹中的所有文件(而不是dist文件夹),然后将其粘贴到desktop-app/app文件夹中。在之前的更改后,desktop-app的最终结构将如下所示:

.

├── app

│ ├── favicon.ico

│ ├── index.html

│ ├── inline.bundle.js

│ ├── inline.bundle.js.map

│ ├── main.bundle.js

│ ├── main.bundle.js.map

│ ├── polyfills.bundle.js

│ ├── polyfills.bundle.js.map

│ ├── scripts.bundle.js

│ ├── scripts.bundle.js.map

│ ├── styles.bundle.js

│ ├── styles.bundle.js.map

│ ├── vendor.bundle.js

│ └── vendor.bundle.js.map

├── freeport.js

├── index.css

├── index.html

├── index.js

├── license

├── package.json

├── readme.md

└── server.js

要进行测试,请运行以下命令:

npm start  

然后,当我们导航到“查看设备”页面时,我们应该看到以下内容:

使用切换按钮,我们应该能够打开/关闭继电器。

通过这样,我们已经完成了桌面应用的开发。在下一节中,我们将更新移动应用。

更新移动应用模板

在上一节中,我们已经更新了桌面应用。在本节中,我们将使用切换开关组件更新移动应用模板。因此,使用此切换开关,我们可以打开/关闭智能插座。

首先,我们要更新“查看设备”模板。更新mobile-app/src/pages/view-device/view-device.html,如下所示:

<ion-header>
    <ion-navbar>
        <ion-title>Mobile App</ion-title>
    </ion-navbar>
</ion-header>
<ion-content padding>
    <div *ngIf="!lastRecord">
        <h3 class="text-center">Loading!</h3>
    </div>
    <div *ngIf="lastRecord">
        <ion-list>
            <ion-item>
                <ion-label>Name</ion-label>
                <ion-label>{{device.name}}</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Toggle LED</ion-label>
                <ion-toggle [(ngModel)]="toggleState" (click)="toggleChange($event)"></ion-toggle>
            </ion-item>
            <ion-item>
                <ion-label>Temperature</ion-label>
                <ion-label>{{lastRecord.data.t}}</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Humidity</ion-label>
                <ion-label>{{lastRecord.data.h}} %</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Rain Level</ion-label>
                <ion-label>{{lastRecord.data.r}} %</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Moisture Level</ion-label>
                <ion-label>{{lastRecord.data.m}} %</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Received At</ion-label>
                <ion-label>{{lastRecord.createdAt | date: 'medium'}}</ion-label>
            </ion-item>
        </ion-list>
    </div>
</ion-content>

接下来,我们将添加所需的逻辑来管理切换按钮。更新mobile-app/src/pages/view-device/view-device.ts,如下所示:

import { Component } from '@angular/core'; 
import { IonicPage, NavController, NavParams } from 'ionic-angular'; 

import { DevicesService } from '../../services/device.service'; 
import { DataService } from '../../services/data.service'; 
import { ToastService } from '../../services/toast.service'; 
import { SocketService } from '../../services/socket.service'; 

@IonicPage() 
@Component({ 
   selector: 'page-view-device', 
   templateUrl: 'view-device.html', 
}) 
export class ViewDevicePage { 
   device: any; 
   data: Array<any>; 
   toggleState: boolean = false; 
   private subData: any; 
   lastRecord: any; 

   constructor(private navCtrl: NavController, 
         private navParams: NavParams, 
         private socketService: SocketService, 
         private deviceService: DevicesService, 
         private dataService: DataService, 
         private toastService: ToastService) { 
         this.device = navParams.get("device"); 
         console.log(this.device); 
   } 

   ionViewDidLoad() { 
         this.deviceService.getOne(this.device._id).subscribe((response) => { 
               this.device = response.json(); 
               this.getData(); 
               this.socketInit(); 
         }); 
   } 

   getData() { 
         this.dataService.get(this.device.macAddress).subscribe((response) => { 
               this.data = response.json(); 
               this.lastRecord = this.data[0]; // descending order data 
               if (this.lastRecord) { 
                     this.toggleState = this.lastRecord.data.s; 
               } 
         }); 
   } 
   socketInit() { 
         this.subData = this.socketService.getData(this.device.macAddress).subscribe((data) => { 
               if (this.data.length <= 0) return; 
               this.data.splice(this.data.length - 1, 1); // remove the last record 
               this.data.push(data); // add the new one 
               this.lastRecord = data; 
         }); 
   } 

   toggleChange(state) { 
         let data = { 
               macAddress: this.device.macAddress, 
               data: { 
                     t: this.lastRecord.data.t, 
                     h: this.lastRecord.data.h, 
                     m: this.lastRecord.data.m, 
                     r: this.lastRecord.data.r, 
                     s: !state 
               }, 
               topic: 'socket' 
         } 

         console.log(data); 

         this.dataService.create(data).subscribe((resp) => { 
               if (resp.json()._id) { 
                     this.toastService.toggleToast('Device Notified!'); 
               } 
         }, (err) => { 
               console.log(err); 
               this.toastService.toggleToast('Device Notification Failed. Check console for the error!'); 
         }) 
   } 

   ionViewDidUnload() { 
         this.subData && this.subData.unsubscribe && this.subData.unsubscribe(); //unsubscribe if subData is defined 
   } 
} 

在这里,我们已经添加了所需的逻辑来管理切换按钮。保存所有文件并运行:

ionic serve 

或者,您也可以将其部署到您的设备上,方法是运行:

ionic run android  

或者:

ionic run ios  

应用程序启动后,当我们导航到“查看设备”页面时,我们应该看到以下内容:

我们应该能够使用移动应用上的切换按钮来控制插座。

通过这样,我们已经完成了智能电机的设置。

在下一节中,我们将为亚马逊 Alexa 构建一个新的技能。

开发 Alexa 技能

在上一节中,我们已经看到了如何构建智能插座并将其与我们现有的智能气象站集成。在本节中,我们将为与亚马逊 Alexa 接口的智能设备构建一个新的技能。

我们将创建一个名为 smarty app 的新技能,然后向其添加两个语音模型:

  • 获取最新的天气状态

  • 打开/关闭插座

如果您是 Alexa 及其技能开发的新手,我建议您在继续之前观看以下系列视频:开发 Alexa 技能:www.youtube.com/playlist?list=PL2KJmkHeYQTO6ci5KF08mvHYdAZu2jgkJ

为了快速概述我们的技能创建,我们将按照以下步骤进行:

  1. 登录到亚马逊开发者门户并创建和设置一个新技能

  2. 训练语音模型

  3. 在 AWS Lambda 服务中编写所需的业务逻辑

  4. 部署和测试设置

那么,让我们开始吧。

创建技能

我们要做的第一件事是登录到developer.amazon.com。一旦登录,点击页面顶部的 Alexa。您应该会登陆到一个页面,应该如下所示:

点击“开始”>下面的 Alexa 技能套件,您应该被重定向到一个页面,您可以查看您现有的技能集或创建一个新的。点击右上角的金色按钮,名为“添加新技能”。

您应该被重定向到一个页面,如下所示:

我已经提供了前面的信息。您可以根据需要进行配置。点击保存,然后点击左侧菜单上的“交互模型”,您应该被重定向到交互模型设置,如下所示:

我们将使用技能构建器,这在撰写时仍处于测试阶段。技能构建器是一个简单的界面,用于训练我们的语音模型。

点击“启动技能构建器”按钮。

训练语音模型

一旦我们进入技能构建器,我们将开始训练模型。在我们的应用程序中,我们将有两个意图:

  • WeatherStatusIntent:获取所有四个传感器的值

  • ControlMotorIntent:打开/关闭电机

除此之外,你还可以根据自己的需求添加其他意图。你可以添加一个仅湿度传感器意图,以仅获取湿度传感器的值,或者添加一个仅雨传感器意图,以仅获取雨传感器的值。

现在,我们将继续设置这些意图并创建槽。

一旦你进入了技能构建器,你应该看到类似以下的东西:

现在,在左侧的意图旁边使用 Add +,创建一个新的自定义意图并命名为WeatherStatusIntent,如下所示:

现在,我们要训练语音模型。一旦意图被创建,点击左侧菜单上的意图名。现在,我们应该看到一个名为示例话语的部分。我们要提供用户如何调用我们服务的示例话语。

为了保持简单,我只添加了三个示例:

Alexa,问 smarty app:

  • 天气报告

  • 天气状况

  • 字段条件

你可以在以下截图中看到这一点:

接下来,我们将使用相同的过程创建另一个名为ControlMotorIntent的意图。点击左侧菜单上的 ControlMotorIntent,我们应该看到示例话语部分。

对于这个意图,我们要做一些不同的事情;我们要创建一些叫做的东西。我们要取用户可能说出的示例话语,并提取其中的一部分作为变量。

例如,如果用户说Alexa,问 smarty app 打开电机Alexa,问 smarty app 关闭电机,除了打开或关闭之外,一切都是相同的,所以我们想将这些转换为变量,并分别处理每个指令。

如果槽被打开,我们就打开电机,如果槽被关闭,我们就关闭电机。

因此,一旦你输入了示例话语,比如打开电机,选择文本打开,如下截图所示:

一旦你选择了文本,输入一个自定义意图槽名 motorAction 并点击加号图标。

对于这个意图,我们只会有一个话语。接下来,我们需要配置 motorAction 意图槽。

在页面的右侧,你应该看到新创建的意图槽。勾选 REQ 列下的复选框。这意味着这个值是意图调用所必需的。接下来,点击槽名下面的选择槽类型。

在这里,我们需要定义一个自定义意图槽类型。添加motorActionIntentSlot,如下所示:

接下来,我们需要设置值。从左侧菜单中点击motorActionIntentSlot,然后添加两个值;turn on 和 turn off,如下所示:

完成后,我们需要设置当用户没有说出我们定义的两个槽值时将会说的提示。点击 ControlMotorIntent 下的{motorAction}和对话模型下方,输入提示,比如你想让我打开还是关闭电机?,如下所示:

通过这样,我们已经定义了我们的语音模型。

现在,我们需要要求 Alexa 技能引擎构建我们的语音模型,并将其添加到其技能引擎中。使用页面顶部的保存模型按钮保存模型,然后构建模型:

通常构建过程只需要五分钟或更短的时间。

ngrok API 引擎

在继续进行 lambda 服务之前,我们需要首先将 API 引擎暴露出来,以便通过公共 URL 可用,比如iotfwjs.com/api,这样当用户询问 Alexa 技能服务问题或发布命令时,Alexa 技能服务可以通过 lambda 服务联系我们。

到目前为止,我们一直在使用基于本地 IP 的配置来与 API 引擎、代理、Web 应用程序或树莓派进行交互。但是,当我们希望 Alexa 技能服务找到我们时,这种方法就行不通了。

因此,我们将使用一个名为ngrok的服务(ngrok.com/)来临时托管我们的本地代码,并提供一个公共 URL,Amazon Alexa 服务可以通过 lambda 服务找到我们。

要设置ngrok,请按照以下步骤进行:

  1. 从这里下载ngrok安装程序:ngrok.com/download适用于运行 API 引擎的操作系统

  2. 解压并复制ngrok下载的 ZIP 文件的内容到api-engine文件夹的根目录

  3. 通过运行以下命令从broker文件夹的根目录启动 Mosca:

mosca -c index.js -v | pino  
  1. 通过运行以下命令从api-engine文件夹的根目录启动 API 引擎:
npm start  
  1. 现在开始使用ngrok进行隧道。从我们已经复制了ngrok可执行文件的api-engine文件夹的根目录运行:
./ngrok http 9000  

运行./ngrok http 9000将在本地主机和ngrok服务器的公共实例之间启动一个新的隧道,我们应该看到以下内容:

转发 URL 每次杀死和重新启动ngrok时都会更改。在前面的情况下,ngrok 的公共 URL:http://add7231d.ngrok.io映射到我的本地服务器:http://localhost:9000。这不是很容易吗?

要快速测试公共 URL,请打开web-app/src/app/app.global.ts并更新如下:

export const Globals = Object.freeze({ 
   // BASE_API_URL: 'http://localhost:9000/', 
   BASE_API_URL: 'https://add7231d.ngrok.io/', 
   API_AUTH_TOKEN: 'AUTH_TOKEN', 
   AUTH_USER: 'AUTH_USER' 
}); 

现在,您可以从任何地方启动您的 web 应用程序,并且它将使用公共 URL 与 API 引擎进行通信。

在继续之前,请阅读ngrok的服务条款(ngrok.com/tos)和隐私政策(ngrok.com/privacy)。

定义 lambda 函数

现在语音模型已经训练好,并且我们有一个可以访问 API 引擎的公共 URL,我们将编写所需的服务来响应用户的交互。

当用户说“Alexa,请问 smarty app 天气报告”时,Alexa 将向 AWS lambda 函数发出请求,lambda 函数将调用 API 引擎进行适当的活动。

引用 AWS:aws.amazon.com/lambda/details/

AWS Lambda 是一种无服务器计算服务,它根据事件运行您的代码,并自动管理底层计算资源。您可以使用 AWS Lambda 来扩展其他 AWS 服务的自定义逻辑,或者创建自己的后端服务,以在 AWS 规模、性能和安全性上运行。

要了解有关 AWS lambda 的更多信息,请参阅:aws.amazon.com/lambda/details/

要开始,请转到 AWS 控制台:console.aws.amazon.com/并选择北弗吉尼亚地区。截至今天,只允许在北美和欧洲托管的 AWS lambda 服务与 Alexa 技能进行关联。

接下来,从顶部的服务菜单中,在计算部分下选择 Lambda。这将带我们到 lambda 服务的函数屏幕。单击创建 Lambda 函数,然后我们将被要求选择一个蓝图。选择空白函数。接下来,您将被要求选择一个触发器;选择 Alexa 技能集,如下:

点击下一步。现在,我们需要配置函数。更新如下:

对于 Lambda 函数代码,请输入以下代码:

'use strict'; 

// Route the incoming request based on type (LaunchRequest, IntentRequest, 
// etc.) The JSON body of the request is provided in the event parameter. 
exports.handler = function(event, context) { 
    try { 
        console.log("event.session.application.applicationId=" + event.session.application.applicationId); 

        if (event.session.new) { 
            onSessionStarted({ requestId: event.request.requestId }, event.session); 
        } 

        if (event.request.type === "LaunchRequest") { 
            onLaunch(event.request, 
                event.session, 
                function callback(sessionAttributes, speechletResponse) { 
                    context.succeed(buildResponse(sessionAttributes, speechletResponse)); 
                }); 
        } else if (event.request.type === "IntentRequest") { 
            onIntent(event.request, 
                event.session, 
                function callback(sessionAttributes, speechletResponse) { 
                    context.succeed(buildResponse(sessionAttributes, speechletResponse)); 
                }); 
        } else if (event.request.type === "SessionEndedRequest") { 
            onSessionEnded(event.request, event.session); 
            context.succeed(); 
        } 
    } catch (e) { 
        context.fail("Exception: " + e); 
    } 
}; 

/** 
 * Called when the session starts. 
 */ 
function onSessionStarted(sessionStartedRequest, session) { 
    console.log("onSessionStarted requestId=" + sessionStartedRequest.requestId + ", sessionId=" + session.sessionId); 

    // add any session init logic here 
} 

/** 
 * Called when the user invokes the skill without specifying what they want. 
 */ 
function onLaunch(launchRequest, session, callback) { 
    console.log("onLaunch requestId=" + launchRequest.requestId + ", sessionId=" + session.sessionId); 

    var cardTitle = "Smarty App" 
    var speechOutput = "Hello, What would you like to know about your farm today?" 
    callback(session.attributes, 
        buildSpeechletResponse(cardTitle, speechOutput, "", true)); 
} 

/** 
 * Called when the user specifies an intent for this skill. 
 */ 
function onIntent(intentRequest, session, callback) { 
    console.log("onIntent requestId=" + intentRequest.requestId + ", sessionId=" + session.sessionId); 

    var intent = intentRequest.intent, 
        intentName = intentRequest.intent.name; 

    // dispatch custom intents to handlers here 
    if (intentName == 'WeatherStatusIntent') { 
        handleWSIRequest(intent, session, callback); 
    } else if (intentName == 'ControlMotorIntent') { 
        handleCMIRequest(intent, session, callback); 
    } else { 
        throw "Invalid intent"; 
    } 
} 

/** 
 * Called when the user ends the session. 
 * Is not called when the skill returns shouldEndSession=true. 
 */ 
function onSessionEnded(sessionEndedRequest, session) { 
    console.log("onSessionEnded requestId=" + sessionEndedRequest.requestId + ", sessionId=" + session.sessionId); 

    // Add any cleanup logic here 
} 

function handleWSIRequest(intent, session, callback) { 
    getData(function(speechOutput) { 
        callback(session.attributes, 
            buildSpeechletResponseWithoutCard(speechOutput, "", "true")); 
    }); 
} 

function handleCMIRequest(intent, session, callback) { 
    var speechOutput = 'Got '; 
    var status; 
    var motorAction = intent.slots.motorAction.value; 
    speechOutput += motorAction; 
    if (motorAction === 'turn on') { 
        status = 1; 
    } 

    if (motorAction === 'turn off') { 
        status = 0; 
    } 
    setData(status, function(speechOutput) { 
        callback(session.attributes, 
            buildSpeechletResponseWithoutCard(speechOutput, "", "true")); 
    }); 

} 

function getData(cb) { 
    var http = require('http'); 
    var chunk = ''; 
    var options = { 
        host: '31d664cf.ngrok.io', 
        port: 80, 
        path: '/api/v1/data/b8:27:eb:39:92:0d/30', 
        agent: false, 
        timeout: 10000, 
        method: 'GET', 
        headers: { 
            'AlexSkillRequest': true, 
            'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OTFmZGI5ZGNlYjBiODM2YjIzMmI3MjMiLCJpYXQiOjE0OTcxNjE4MTUsImV4cCI6MTQ5NzI0ODIxNX0.ua-SXAqLb-XUEtbgY55TX_pKdD2Xj5OSM7b9Iox_Rd8' 
        } 
    }; 

    var req = http.request(options, function(res) { 
        res.on('data', function(_chunk) { 
            chunk += _chunk; 
        }); 

        res.on('end', function() { 
            var resp = chunk; 
            if (typeof chunk === 'string') { 
                resp = JSON.parse(chunk); 
            } 

            if (resp.length === 0) { 
                cb('Looks like we have not gathered any data yet! Please try again later!'); 
            } 

            var d = resp[0].data; 

            if (!d) { 
                cb('Looks like there is something wrong with the data we got! Please try again later!'); 
            } 

            var temp = d.t || 'invalid'; 
            var humd = d.h || 'invalid'; 
            var mois = d.m || 'invalid'; 
            var rain = d.r || 'invalid'; 

            cb('The temperature is ' + temp + ' degrees celsius, the humidity is ' + humd + ' percent, The moisture level is ' + mois + ' percent and the rain level is ' + rain + ' percent!'); 

        }); 

        res.on('error', function() { 
            console.log(arguments); 
            cb('Looks like something went wrong.'); 
        }); 
    }); 
    req.end(); 
} 

function setData(status, cb) { 
    var http = require('http'); 
    var chunk = ''; 
    var data = { 
        'status': status, 
        'macAddress': 'b8:27:eb:39:92:0d' 
    }; 

    data = JSON.stringify(data); 

    var options = { 
        host: '31d664cf.ngrok.io', 
        port: 80, 
        path: '/api/v1/data', 
        agent: false, 
        timeout: 10000, 
        method: 'POST', 
        headers: { 
            'AlexSkillRequest': true, 
            'Content-Type': 'application/json', 
            'Content-Length': Buffer.byteLength(data), 
            'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1OTFmZGI5ZGNlYjBiODM2YjIzMmI3MjMiLCJpYXQiOjE0OTcxNjE4MTUsImV4cCI6MTQ5NzI0ODIxNX0.ua-SXAqLb-XUEtbgY55TX_pKdD2Xj5OSM7b9Iox_Rd8' 
        } 
    }; 

    var req = http.request(options, function(res) { 
        res.on('data', function(_chunk) { 
            chunk += _chunk; 
        }); 

        res.on('end', function() { 
            var resp = chunk; 
            if (typeof chunk === 'string') { 
                resp = JSON.parse(chunk); 
            } 

            cb('Motor has been successfully ' + (status ? 'turned on' : 'turned off')); 

        }); 

        res.on('error', function() { 
            console.log(arguments); 
            cb('Looks like something went wrong.'); 
        }); 
    }); 

    // post the data 
    req.write(data); 
    req.end(); 
} 

// ------- Helper functions to build responses ------- 

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) { 
    return { 
        outputSpeech: { 
            type: "PlainText", 
            text: output 
        }, 
        card: { 
            type: "Simple", 
            title: title, 
            content: output 
        }, 
        reprompt: { 
            outputSpeech: { 
                type: "PlainText", 
                text: repromptText 
            } 
        }, 
        shouldEndSession: shouldEndSession 
    }; 
} 

function buildSpeechletResponseWithoutCard(output, repromptText, shouldEndSession) { 
    return { 
        outputSpeech: { 
            type: "PlainText", 
            text: output 
        }, 
        reprompt: { 
            outputSpeech: { 
                type: "PlainText", 
                text: repromptText 
            } 
        }, 
        shouldEndSession: shouldEndSession 
    }; 
} 

function buildResponse(sessionAttributes, speechletResponse) { 
    return { 
        version: "1.0", 
        sessionAttributes: sessionAttributes, 
        response: speechletResponse 
    }; 
} 

代码中有很多内容。exports.handler()是我们需要为 lambda 设置的默认函数。在其中,我们定义了传入请求的类型。如果传入的是IntentRequest,我们调用onIntent()。在onIntent()中,我们获取intentName并调用适当的逻辑。

如果intentNameWeatherStatusIntent,我们调用handleWSIRequest(),否则如果 intentName 是ControlMotorIntent,我们调用handleCMIRequest()

handleWSIRequest()内,我们调用getData(),它将向我们的ngrok URL 发出 HTTP GET请求。一旦数据到达,我们构造一个响应并将其返回给技能服务。

而且,handleCMIRequest()也是一样,只是它首先获取motorAction槽值,然后调用setData(),这将调用或者打开/关闭电机。

一旦代码被复制,您应该在底部找到额外的配置。我们将保持处理程序不变。对于角色,点击创建自定义角色,并进行如下设置:

然后点击允许。这将创建一个新的角色,该角色将在现有角色中填充,如下所示:

完成后,点击下一步。验证摘要,然后点击页面底部的创建函数。

如果一切顺利,您应该看到以下屏幕:

请注意右上角的 ARN。这是我们 lambda 函数的Amazon 资源名称ARN)。我们需要将其作为输入提供给 Alexa Skills Kit。

部署和测试

现在我们拥有了所有的部件,我们将在我们创建的 Alexa 技能中配置 ARN。返回 Alexa 技能,点击配置,然后按照以下方式更新配置:

点击下一步。如果一切设置正确,我们可以测试设置。

在测试页面的底部,我们应该看到一个名为服务仿真器的部分。您可以按照以下方式进行测试:

以下截图显示了 lambda 从 Alexa 接收到的请求:

通过这样,我们已经完成了将 Alexa 与我们的 IoT.js 框架集成。

总结

在本章中,我们探讨了如何将 Alexa 等语音 AI 服务与我们开发的 IoTFW.js 框架集成。我们继续使用第四章,智能农业中的相同示例,并通过设置可以打开/关闭电机的继电器开始了本章。接下来,我们了解了 Alexa 的工作原理。我们创建了一个新的自定义技能,然后设置了所需的语音模型。之后,我们在 AWS lambda 中编写了所需的业务逻辑,该逻辑将获取最新的天气状况并控制电机。

我们最终使用 reverb 应用程序测试了一切,并且也验证了一切。

在第六章,智能可穿戴中,我们将研究物联网和医疗保健。

第六章:智能可穿戴设备

在本章中,我们将介绍如何使用树莓派 3 创建一个简单的医疗保健应用程序。我们将构建一个带有 16x2 液晶显示屏的智能可穿戴设备,显示用户的位置,并在 Web/桌面/移动界面上显示加速度计的数值。这个产品的目标用户主要是年长者,主要用例是跌倒检测,我们将在第七章中进行讨论,智能可穿戴设备和 IFTTT

在本章中,我们将讨论以下内容:

  • 物联网和医疗保健

  • 设置所需的硬件

  • 整合加速度计并查看实时数据

物联网和医疗保健

想象一位成功接受心脏移植手术并在医院术后护理后被送回家的患者。对这位患者的关注程度将显著降低,因为家庭设施与医院相比将是最低的。这就是物联网以其实时能力介入的地方。

物联网和医疗保健是天作之合。风险和回报同样巨大。能够实时监测患者的健康状况,并获取他们的脉搏、体温和其他重要统计数据的信息,并对其进行诊断和处理是非常宝贵的。与此同时,如果连接中断两分钟,就会有人的生命受到威胁。

在我看来,要实现物联网在医疗保健领域的全部潜力,我们可能需要再等待 5-10 年,那时的连接将是绝对无缝的,数据包丢失将成为历史。

智能可穿戴设备

如前一节所述,我们将使用物联网在医疗保健领域做一些关键的事情。我们要构建的智能可穿戴设备的主要目的是识别跌倒。一旦识别到跌倒,我们就会通知云端。当我们周围有年长或患病的人因意外原因而倒下时,及时识别跌倒并采取行动有时可以挽救生命。

为了检测跌倒,我们将使用加速度计。引用维基百科的话:

"加速度计是一种测量真实加速度的设备。真实加速度是指物体在其瞬时静止参考系中的加速度(或速度变化率),并不同于坐标加速度,即在固定坐标系中的加速度。例如,静止在地球表面的加速度计将测量由于地球重力而产生的加速度,垂直向上(根据定义)为 g ≈ 9.81 m/s2。相比之下,自由下落的加速度计(以约 9.81 m/s2 的速率朝向地球中心下落)将测量为零。"

要了解更多关于加速度计及其工作原理的信息,请参阅加速度计的工作原理www.youtube.com/watch?v=i2U49usFo10

在本章中,我们将实现基本系统,收集 X、Y 和 Z 轴加速度原始值,并在 Web、桌面和移动应用程序上显示。在第七章中,智能可穿戴设备和 IFTTT,我们将使用这些数值来实现跌倒检测。

除了实时收集加速度计数值外,我们还将使用 16x2 液晶显示屏显示当前时间和用户的地理位置。如果需要,我们也可以在显示屏上添加其他文本。16x2 是一个简单的界面来显示内容。这可以通过诺基亚 5110 液晶屏(www.amazon.in/inch-Nokia-5110-KG075-KitsGuru/dp/B01CXNSJOA)进行扩展,以获得具有图形的更高级显示。

在接下来的部分,我们将组装所需的硬件,然后更新树莓派代码。之后,我们将开始处理 API 引擎和 UI 模板。

设置智能可穿戴设备

关于硬件设置的第一件事是它又大又笨重。这只是一个 POC,甚至不是一个接近生产设置的远程。硬件设置将包括连接到树莓派 3 和 16X2 LCD 的加速度计。

加速度计 ADXL345 通过 I2C 协议提供 X、Y 和 Z 轴的加速度。

按照以下方式连接硬件:

正如您在上面的原理图中所看到的,我们已经建立了以下连接:

  • 树莓派和 LCD:
树莓派编号 - 引脚名称 16x2 LCD Pi 名称
6 - GND - 面包板导轨 1 1 - GND
2 - 5V - 面包板导轨 2 2 - VCC
1 k Ohm 电位计 3 - VEE
32 - GPIO 12 4 - RS
6 - GND - 面包板导轨 1 5 -R/W
40 - GPIO 21 6 - EN
NC 7 - DB0
NC 8 - DB1
NC 9 - DB2
NC 10 - DB3
29 - GPIO 5 11 - DB4
31 - GPIO 6 12 - DB5
11 - GPIO 17 13 - DB6
12 - GPIO 18 14 - DB7
2 - 5V - 面包板导轨 2 15 - LED+
6 - GND - 面包板导轨 1 16 - LED-
  • 树莓派和 ADXL345:
树莓派编号 - 引脚名称 ADXL345 引脚编号 - 引脚名称
1 - 3.3V VCC
6 - GND - 面包板导轨 1 GND
5 - GPIO3/SCL1 SCL
3 - GPIO2/SDA1 SDA
6 - GND - 面包板导轨 1 SDO

我们将添加所需的代码:

  1. 首先创建一个名为chapter6的文件夹,然后将chapter4的内容复制到其中。我们将随着进展更新此代码

  2. 现在,我们将开始使用pi-client。在树莓派上,打开pi-client/index.js并按照以下方式更新它:

var config = require('./config.js'); 
var mqtt = require('mqtt'); 
var GetMac = require('getmac'); 
var request = require('request'); 
var ADXL345 = require('adxl345-sensor'); 
require('events').EventEmitter.prototype._maxListeners = 100; 

var adxl345 = new ADXL345(); // defaults to i2cBusNo 1, i2cAddress 0x53 

var Lcd = require('lcd'), 
    lcd = new Lcd({ 
        rs: 12, 
        e: 21, 
        data: [5, 6, 17, 18], 
        cols: 8, 
        rows: 2 
    }); 

var aclCtr = 0, 
    locCtr = 0; 

var x, prevX, y, prevY, z, prevZ; 
var locationG; // global location variable 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('rpi'); 
    client.subscribe('socket'); 
    GetMac.getMac(function(err, mac) { 
        if (err) throw err; 
        macAddress = mac; 
        displayLocation(); 
        initADXL345(); 
        client.publish('api-engine', mac); 
    }); 
}); 

client.on('message', function(topic, message) { 
    message = message.toString(); 
    if (topic === 'rpi') { 
        console.log('API Engine Response >> ', message); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

function initADXL345() { 
    adxl345.init().then(function() { 
            console.log('ADXL345 initialization succeeded'); 
            // init loop after ADXL345 has been setup 
            loop(); 
        }) 
        .catch(function(err) { 
            console.error('ADXL345 initialization failed: ', err); 
        }); 
} 

function loop() { 
    // infinite loop, with 1 seconds delay 
    setInterval(function() { 
        // wait till we get the location 
        // then start processing 
        if (!locationG) return; 

        if (aclCtr === 3) { // every 3 seconds 
            aclCtr = 0; 
            readSensorValues(function(acclVals) { 
                var x = acclVals.x; 
                var y = acclVals.y; 
                var z = acclVals.z; 

                var data2Send = { 
                    data: { 
                        acclVals: acclVals, 
                        location: locationG 
                    }, 
                    macAddress: macAddress 
                }; 

                // no duplicate data 
                if (x !== prevX || y !== prevY || z !== prevZ) { 
                    console.log('data2Send', data2Send); 
                    client.publish('accelerometer', JSON.stringify(data2Send)); 
                    console.log('Data Published'); 
                    prevX = x; 
                    prevY = y; 
                    prevZ = z; 
                } 
            }); 
        } 

        if (locCtr === 300) { // every 300 seconds 
            locCtr = 0; 
            displayLocation(); 
        } 

        aclCtr++; 
        locCtr++; 
    }, 1000); // every one second 
} 

function readSensorValues(CB) { 
    adxl345.getAcceleration(true) // true for g-force units, else false for m/s² 
        .then(function(acceleration) { 
            if (CB) CB(acceleration); 
        }) 
        .catch((err) => { 
            console.log('ADXL345 read error: ', err); 
        }); 
} 

function displayLocation() { 
    request('http://ipinfo.io', function(error, res, body) { 
        var info = JSON.parse(body); 
        // console.log(info); 
        locationG = info; 
        var text2Print = ''; 
        text2Print += 'City: ' + info.city; 
        text2Print += ' Region: ' + info.region; 
        text2Print += ' Country: ' + info.country + ' '; 
        lcd.setCursor(16, 0); // 1st row     
        lcd.autoscroll(); 
        printScroll(text2Print); 
    }); 
} 

// a function to print scroll 
function printScroll(str, pos) { 
    pos = pos || 0; 

    if (pos === str.length) { 
        pos = 0; 
    } 

    lcd.print(str[pos]); 
    //console.log('printing', str[pos]); 

    setTimeout(function() { 
        return printScroll(str, pos + 1); 
    }, 300); 
} 

// If ctrl+c is hit, free resources and exit. 
process.on('SIGINT', function() { 
    lcd.clear(); 
    lcd.close(); 
    process.exit(); 
}); 

从上述代码中可以看出,我们使用displayLocation()每小时显示一次位置,因为我们假设位置不会经常改变。我们使用ipinfo.io/服务来获取用户的位置。

  1. 最后,使用readSensorValues()我们每3秒获取一次加速度计的值,并将这些数据发布到名为accelerometer的主题中。

  2. 现在,我们将安装所需的依赖项。从pi-client文件夹内部运行以下命令:

npm install async getmac adxl345-sensor mqtt request --save
  1. 保存所有文件并通过运行在服务器或我们的桌面机器上启动 mosca broker 来启动:
mosca -c index.js -v | pino  
  1. 接下来,在树莓派上运行代码:
npm start  

这将启动pi-client并开始收集加速度计数据,并在 LCD 显示器上显示位置如下:

我的设置如下所示:

接下来,我们将与 API 引擎一起工作。

更新 API 引擎

现在我们已经让智能可穿戴设备运行并发送了三轴数据,我们现在将实现 API 引擎中接受该数据所需的逻辑,并将数据发送到 Web/桌面/移动应用程序中:

打开api-engine/server/mqtt/index.js并按照以下方式更新它:

var Data = require('../api/data/data.model'); 
var mqtt = require('mqtt'); 
var config = require('../config/environment'); 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    console.log('Connected to Mosca at ' + config.mqtt.host + ' on port ' + config.mqtt.port); 
    client.subscribe('api-engine'); 
    client.subscribe('accelerometer'); 
}); 

client.on('message', function(topic, message) { 
    // message is Buffer 
    // console.log('Topic >> ', topic); 
    // console.log('Message >> ', message.toString()); 
    if (topic === 'api-engine') { 
        var macAddress = message.toString(); 
        console.log('Mac Address >> ', macAddress); 
        client.publish('rpi', 'Got Mac Address: ' + macAddress); 
    } else if (topic === 'accelerometer') { 
        var data = JSON.parse(message.toString()); 
        // create a new data record for the device   
        Data.create(data, function(err, data) { 
            if (err) return console.error(err); 
            // if the record has been saved successfully,  
            // websockets will trigger a message to the web-app 
            console.log('Data Saved :', data.data); 
        }); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

在这里,我们订阅名为accelerometer的主题,并监听其变化。接下来,我们将按照以下方式更新api-engine/server/api/data/data.controller.js

'use strict'; 

var Data = require('./data.model'); 

/** 
 * Get Data for a device 
 */ 
exports.index = function(req, res) { 
    var macAddress = req.params.deviceId; 
    var limit = parseInt(req.params.limit) || 30; 

    Data 
        .find({ 
            macAddress: macAddress 
        }) 
        .sort({ 'createdAt': -1 }) 
        .limit(limit) 
        .exec(function(err, data) { 
            if (err) return res.status(500).send(err); 
            res.status(200).json(data); 
        }); 
}; 

/** 
 * Create a new data record 
 */ 
exports.create = function(req, res, next) { 
    var data = req.body || {}; 
    data.createdBy = req.user._id; 

    Data.create(data, function(err, _data) { 
        if (err) return res.status(500).send(err); 
        return res.json(_data); 
    }); 
}; 

上述代码用于将数据保存到数据库,并在从 Web、桌面和移动应用程序请求时从数据库中获取数据。

保存所有文件并运行 API 引擎:

npm start

这将启动 API 引擎,如果需要,我们可以重新启动智能可穿戴设备,我们应该看到以下内容:

在下一节中,我们将在 Web 应用程序中显示数据。

更新 Web 应用程序

现在我们已经完成了 API 引擎,我们将更新 Web 应用程序中的模板以显示三轴数据。打开web-app/src/app/device/device.component.html并按照以下方式更新它:

<div class="container">
  <br>
  <div *ngIf="!device">
    <h3 class="text-center">Loading!</h3>
  </div>
  <div class="row" *ngIf="lastRecord">
    <div class="col-md-12">
      <div class="panel panel-info">
        <div class="panel-heading">
          <h3 class="panel-title">
                        {{device.name}}
                    </h3>
          <span class="pull-right btn-click">
                        <i class="fa fa-chevron-circle-up"></i>
                    </span>
        </div>
        <div class="clearfix"></div>
        <div class="table-responsive">
          <table class="table table-striped">
            <tr *ngIf="lastRecord">
              <td>X-Axis</td>
              <td>{{lastRecord.data.acclVals.x}} {{lastRecord.data.acclVals.units}}</td>
            </tr>
            <tr *ngIf="lastRecord">
              <td>Y-Axis</td>
              <td>{{lastRecord.data.acclVals.y}} {{lastRecord.data.acclVals.units}}</td>
            </tr>
            <tr *ngIf="lastRecord">
              <td>Z-Axis</td>
              <td>{{lastRecord.data.acclVals.z}} {{lastRecord.data.acclVals.units}}</td>
            </tr>
            <tr *ngIf="lastRecord">
              <td>Location</td>
              <td>{{lastRecord.data.location.city}}, {{lastRecord.data.location.region}}, {{lastRecord.data.location.country}}</td>
            </tr>
            <tr *ngIf="lastRecord">
              <td>Received At</td>
              <td>{{lastRecord.createdAt | date : 'medium'}}</td>
            </tr>
          </table>
          <hr>
          <div class="col-md-12" *ngIf="acclVals.length > 0">
            <canvas baseChart [datasets]="acclVals" [labels]="lineChartLabels" [options]="lineChartOptions" [legend]="lineChartLegend" [chartType]="lineChartType"></canvas>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

所需的逻辑将在device.component.ts中。打开web-app/src/app/device/device.component.ts并按照以下方式更新它:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { DevicesService } from '../services/devices.service';
import { Params, ActivatedRoute } from '@angular/router';
import { SocketService } from '../services/socket.service';
import { DataService } from '../services/data.service';
import { NotificationsService } from 'angular2-notifications';

@Component({
  selector: 'app-device',
  templateUrl: './device.component.html',
  styleUrls: ['./device.component.css']
})
export class DeviceComponent implements OnInit, OnDestroy {
  device: any;
  data: Array<any>;
  toggleState: boolean = false;
  private subDevice: any;
  private subData: any;
  lastRecord: any;

  // line chart config
  public lineChartOptions: any = {
    responsive: true,
    legend: {
      position: 'bottom',
    }, hover: {
      mode: 'label'
    }, scales: {
      xAxes: [{
        display: true,
        scaleLabel: {
          display: true,
          labelString: 'Time'
        }
      }],
      yAxes: [{
        display: true,
        ticks: {
          beginAtZero: true,
          // steps: 10,
          // stepValue: 5,
          // max: 70
        }
      }],
      zAxes: [{
        display: true,
        ticks: {
          beginAtZero: true,
          // steps: 10,
          // stepValue: 5,
          // max: 70
        }
      }]
    },
    title: {
      display: true,
      text: 'X,Y,Z vs. Time'
    }
  };

  public lineChartLegend: boolean = true;
  public lineChartType: string = 'line';
  public acclVals: Array<any> = [];
  public lineChartLabels: Array<any> = [];

  constructor(private deviceService: DevicesService,
    private socketService: SocketService,
    private dataService: DataService,
    private route: ActivatedRoute,
    private notificationsService: NotificationsService) { }

  ngOnInit() {
    this.subDevice = this.route.params.subscribe((params) => {
      this.deviceService.getOne(params['id']).subscribe((response) => {
        this.device = response.json();
        this.getData();
      });
    });
  }

  getData() {
    this.dataService.get(this.device.macAddress).subscribe((response) => {
      this.data = response.json();
      this.lastRecord = this.data[0]; // descending order data
      this.toggleState = this.lastRecord.data.s;
      this.genChart();
      this.socketInit();
    });
  }

  socketInit() {
    this.subData = this.socketService.getData(this.device.macAddress).subscribe((data) => {
      if (this.data.length <= 0) return;
      this.data.splice(this.data.length - 1, 1); // remove the last record
      this.data.push(data); // add the new one
      this.lastRecord = data;
      this.toggleState = this.lastRecord.data.s;
      this.genChart();
    });
  }

  ngOnDestroy() {
    this.subDevice.unsubscribe();
    this.subData ? this.subData.unsubscribe() : '';
  }

  genChart() {
    let data = this.data;
    let _acclVals: Array<any> = [];
    let _lblArr: Array<any> = [];

    let xArr: Array<any> = [];
    let yArr: Array<any> = [];
    let zArr: Array<any> = [];

    for (var i = 0; i < data.length; i++) {
      let _d = data[i];
      xArr.push(_d.data.acclVals.x);
      yArr.push(_d.data.acclVals.y);
      zArr.push(_d.data.acclVals.z);
      _lblArr.push(this.formatDate(_d.createdAt));
    }

    // reverse data to show the latest on the right side
    xArr.reverse();
    yArr.reverse();
    zArr.reverse();
    _lblArr.reverse();

    _acclVals = [
      {
        data: xArr,
        label: 'X-Axis'
      },
      {
        data: yArr,
        label: 'Y-Axis'
      },
      {
        data: zArr,
        label: 'Z-Axis'
      }
    ]

    this.acclVals = _acclVals;

    this.lineChartLabels = _lblArr;
  }

  private formatDate(originalTime) {
    var d = new Date(originalTime);
    var datestring = d.getDate() + "-" + (d.getMonth() + 1) + "-" + d.getFullYear() + " " +
      d.getHours() + ":" + d.getMinutes();
    return datestring;
  }

}

保存所有文件并运行以下命令:

npm start  

导航到http://localhost:4200并查看设备,我们应该看到以下内容:

通过这样,我们已经完成了 Web 应用程序。

更新桌面应用程序

现在 Web 应用程序已经完成,我们将构建相同的应用程序并将其部署到我们的桌面应用程序中。

要开始,请返回到web-app文件夹的终端/提示符,并运行:

ng build --env=prod

这将在web-app文件夹内创建一个名为dist的新文件夹。dist文件夹的内容应该类似于以下内容:

.

├── favicon.ico

├── index.html

├── inline.bundle.js

├── inline.bundle.js.map

├── main.bundle.js

├── main.bundle.js.map

├── polyfills.bundle.js

├── polyfills.bundle.js.map

├── scripts.bundle.js

├── scripts.bundle.js.map

├── styles.bundle.js

├── styles.bundle.js.map

├── vendor.bundle.js

└── vendor.bundle.js.map

我们编写的所有代码最终都打包到了前面的文件中。我们将获取dist文件夹中的所有文件(而不是dist文件夹),然后将其粘贴到desktop-app/app文件夹中。在进行前述更改后,桌面应用程序的最终结构将如下所示:

.

├── app

│ ├── favicon.ico

│ ├── index.html

│ ├── inline.bundle.js

│ ├── inline.bundle.js.map

│ ├── main.bundle.js

│ ├── main.bundle.js.map

│ ├── polyfills.bundle.js

│ ├── polyfills.bundle.js.map

│ ├── scripts.bundle.js

│ ├── scripts.bundle.js.map

│ ├── styles.bundle.js

│ ├── styles.bundle.js.map

│ ├── vendor.bundle.js

│ └── vendor.bundle.js.map

├── freeport.js

├── index.css

├── index.html

├── index.js

├── license

├── package.json

├── readme.md

└── server.js

要进行测试,请运行以下命令:

npm start

然后当我们导航到 VIEW DEVICE 页面时,我们应该看到以下屏幕:

通过这样,我们已经完成了桌面应用程序的开发。在下一节中,我们将更新移动应用程序。

更新移动应用程序模板

在上一节中,我们已经更新了桌面应用程序。在本节中,我们将更新移动应用程序模板以显示三轴数据。

首先,我们要更新 view-device 模板。按照以下步骤更新mobile-app/src/pages/view-device/view-device.html

<ion-header>
    <ion-navbar>
        <ion-title>Mobile App</ion-title>
    </ion-navbar>
</ion-header>
<ion-content padding>
    <div *ngIf="!lastRecord">
        <h3 class="text-center">Loading!</h3>
    </div>
    <div *ngIf="lastRecord">
        <ion-list>
            <ion-item>
                <ion-label>Name</ion-label>
                <ion-label>{{device.name}}</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>X-Axis</ion-label>
                <ion-label>{{lastRecord.data.acclVals.x}} {{lastRecord.data.acclVals.units}}</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Y-Axis</ion-label>
                <ion-label>{{lastRecord.data.acclVals.y}} {{lastRecord.data.acclVals.units}}</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Z-Axis</ion-label>
                <ion-label>{{lastRecord.data.acclVals.z}} {{lastRecord.data.acclVals.units}}</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Location</ion-label>
                <ion-label>{{lastRecord.data.location.city}}, {{lastRecord.data.location.region}}, {{lastRecord.data.location.country}}</ion-label>
            </ion-item>
            <ion-item>
                <ion-label>Received At</ion-label>
                <ion-label>{{lastRecord.createdAt | date: 'medium'}}</ion-label>
            </ion-item>
        </ion-list>
    </div>
</ion-content>

接下来,按照以下步骤更新mobile-app/src/pages/view-device/view-device.ts

import { Component } from '@angular/core'; 
import { IonicPage, NavController, NavParams } from 'ionic-angular'; 

import { DevicesService } from '../../services/device.service'; 
import { DataService } from '../../services/data.service'; 
import { ToastService } from '../../services/toast.service'; 
import { SocketService } from '../../services/socket.service'; 

@IonicPage() 
@Component({ 
   selector: 'page-view-device', 
   templateUrl: 'view-device.html', 
}) 
export class ViewDevicePage { 
   device: any; 
   data: Array<any>; 
   toggleState: boolean = false; 
   private subData: any; 
   lastRecord: any; 

   constructor(private navCtrl: NavController, 
         private navParams: NavParams, 
         private socketService: SocketService, 
         private deviceService: DevicesService, 
         private dataService: DataService, 
         private toastService: ToastService) { 
         this.device = navParams.get("device"); 
         console.log(this.device); 
   } 

   ionViewDidLoad() { 
         this.deviceService.getOne(this.device._id).subscribe((response) => { 
               this.device = response.json(); 
               this.getData(); 
               this.socketInit(); 
         }); 
   } 

   getData() { 
         this.dataService.get(this.device.macAddress).subscribe((response) => { 
               this.data = response.json(); 
               this.lastRecord = this.data[0]; // descending order data 
         }); 
   } 

   socketInit() { 
         this.subData = this.socketService.getData(this.device.macAddress).subscribe((data) => { 
               if (this.data.length <= 0) return; 
               this.data.splice(this.data.length - 1, 1); // remove the last record 
               this.data.push(data); // add the new one 
               this.lastRecord = data; 
         }); 
   } 

   ionViewDidUnload() { 
         this.subData && this.subData.unsubscribe && this.subData.unsubscribe(); //unsubscribe if subData is defined 
   } 
} 

保存所有文件,并通过ionic serveionic cordova run android来运行移动应用程序。

然后我们应该看到以下内容:

通过这样,我们已经完成了在移动应用程序上显示智能可穿戴设备数据的工作。

摘要

在本章中,我们已经看到如何使用 Raspberry Pi 3 构建一个简单的智能可穿戴设备。我们设置了一个液晶显示屏和一个三轴加速度计,并在显示屏上显示了位置信息。我们实时将加速度计数据发布到云端,并在 Web、桌面和移动应用程序上显示出来。

在第七章,智能可穿戴设备和 IFTTT中,我们将通过在其上实施 IFTTT 规则,将智能可穿戴设备提升到一个新的水平。我们将执行诸如打电话或向急救联系人发送短信等操作,以便及时提供护理。

第七章:智能可穿戴和 IFTTT

在第六章 智能可穿戴中,我们看到了如何构建一个简单的可穿戴设备,显示用户的位置并读取加速计值。在本章中,我们将通过在设备上实现跌倒检测逻辑,然后在数据上添加If This Then ThatIFTTT)规则,将该应用程序提升到下一个级别。我们将讨论以下主题:

  • 什么是 IFTTT

  • IFTTT 和物联网

  • 了解跌倒检测

  • 基于加速计的跌倒检测

  • 构建一个 IFTTT 规则引擎

IFTTT 和物联网

这种反应模式可以轻松应用于某些情况。例如,如果病人摔倒,就叫救护车,或者如果温度低于 15 度,就关闭空调,等等。这些都是我们定义的简单规则,可以帮助我们自动化许多流程。

在物联网中,规则引擎是自动化大部分单调任务的关键。在本章中,我们将构建一个简单的硬编码规则引擎,将持续监视传入的数据。如果传入的数据与我们的任何规则匹配,它将执行一个响应。

我们正在构建的东西类似于ifttt.comifttt.com/discover)的概念,但非常特定于我们框架内存在的物联网设备。IFTTT(ifttt.com/discover)与我们在书中构建的内容无关。

跌倒检测

在第六章 智能可穿戴中,我们从加速计中收集了三个轴的值。现在,我们将利用这些数据来检测跌倒。

我建议观看视频自由落体中的加速计www.youtube.com/watch?v=-om0eTXsgnY),它解释了加速计在静止和运动时的行为。

现在我们了解了跌倒检测的基本概念,让我们谈谈我们的具体用例。

跌倒检测中最大的挑战是区分跌倒和其他活动,比如跑步和跳跃。在本章中,我们将保持简单,处理非常基本的条件,即用户静止或持续运动时突然摔倒。

为了确定用户是否摔倒,我们使用信号幅度矢量或SMVSMV是三个轴的值的均方根。也就是说:

如果我们开始绘制用户在站立不动然后摔倒时的SMV时间的图表,我们将得到以下图表:

请注意图表末端的尖峰。这是用户实际摔倒的点。

现在,当我们从 ADXL345 收集加速计值时,我们将计算 SMV。通过使用我们构建的智能可穿戴进行多次迭代,我一直能够在 1 g SMV 值处稳定地检测到跌倒。对于小于 1 g SMV 的任何值,用户几乎总是被认为是静止的,而大于 1 g SMV 的任何值都被认为是跌倒。

请注意,我已经将加速计放置在 y 轴垂直于地面的位置。

一旦我们把设置放在一起,您就可以亲自看到 SMV 值随加速计位置的变化而变化。

请注意,如果您正在进行其他活动,比如跳跃或下蹲,可能会触发跌倒检测。您可以调整 1 g SMV 的阈值,以获得一致的跌倒检测。

你也可以参考使用 3 轴数字加速度计检测人类跌倒:(www.analog.com/en/analog-dialogue/articles/detecting-falls-3-axis-digital-accelerometer.html),或者基于加速度计的身体传感器定位用于健康和医疗监测应用 (www.ncbi.nlm.nih.gov/pmc/articles/PMC3279922/),以及开发用于检测日常活动中跌倒的算法,使用 2 个三轴加速度计 (waset.org/publications/2993/development-of-the-algorithm-for-detecting-falls-during-daily-activity-using-2-tri-axial-accelerometers),以便更好地理解这个主题并提高系统的效率。

更新树莓派

现在我们知道需要做什么,我们将开始编写代码。

在继续之前,创建一个名为chapter7的文件夹,并在其中复制chapter6代码。

接下来,打开pi/index.js文件。我们将更新 ADXL345 初始化设置,然后开始处理数值。更新pi/index.js如下:

var config = require('./config.js'); 
var mqtt = require('mqtt'); 
var GetMac = require('getmac'); 
var request = require('request'); 
var ADXL345 = require('adxl345-sensor'); 
require('events').EventEmitter.prototype._maxListeners = 100; 

var adxl345 = new ADXL345(); // defaults to i2cBusNo 1, i2cAddress 0x53 

var Lcd = require('lcd'), 
    lcd = new Lcd({ 
        rs: 12, 
        e: 21, 
        data: [5, 6, 17, 18], 
        cols: 8, 
        rows: 2 
    }); 

var aclCtr = 0, 
    locCtr = 0; 

var prevX, prevY, prevZ, prevSMV, prevFALL; 
var locationG; // global location variable 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('rpi'); 
    client.subscribe('socket'); 
    GetMac.getMac(function(err, mac) { 
        if (err) throw err; 
        macAddress = mac; 
        displayLocation(); 
        initADXL345(); 
        client.publish('api-engine', mac); 
    }); 
}); 

client.on('message', function(topic, message) { 
    message = message.toString(); 
    if (topic === 'rpi') { 
        console.log('API Engine Response >> ', message); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

function initADXL345() { 
    adxl345.init() 
        .then(() => adxl345.setMeasurementRange(ADXL345.RANGE_2_G())) 
        .then(() => adxl345.setDataRate(ADXL345.DATARATE_100_HZ())) 
        .then(() => adxl345.setOffsetX(0)) // measure for your particular device 
        .then(() => adxl345.setOffsetY(0)) // measure for your particular device 
        .then(() => adxl345.setOffsetZ(0)) // measure for your particular device 
        .then(() => adxl345.getMeasurementRange()) 
        .then((range) => { 
            console.log('Measurement range:', ADXL345.stringifyMeasurementRange(range)); 
            return adxl345.getDataRate(); 
        }) 
        .then((rate) => { 
            console.log('Data rate: ', ADXL345.stringifyDataRate(rate)); 
            return adxl345.getOffsets(); 
        }) 
        .then((offsets) => { 
            console.log('Offsets: ', JSON.stringify(offsets, null, 2)); 
            console.log('ADXL345 initialization succeeded'); 
            loop(); 
        }) 
        .catch((err) => console.error('ADXL345 initialization failed:', err)); 
} 

function loop() { 
    // infinite loop, with 3 seconds delay 
    setInterval(function() { 
        // wait till we get the location 
        // then start processing 
        if (!locationG) return; 

        readSensorValues(function(acclVals) { 
            var x = acclVals.x; 
            var y = acclVals.y; 
            var z = acclVals.z; 
            var fall = 0; 
            var smv = Math.sqrt(x * x, y * y, z * z); 

            if (smv > 1) { 
                fall = 1; 
            } 

            acclVals.smv = smv; 
            acclVals.fall = fall; 

            var data2Send = { 
                data: { 
                    acclVals: acclVals, 
                    location: locationG 
                }, 
                macAddress: macAddress 
            }; 

            // no duplicate data 
            if (fall === 1 && (x !== prevX || y !== prevY || z !== prevZ || smv !== prevSMV || fall !== prevFALL)) { 
                console.log('Fall Detected >> ', acclVals); 
                client.publish('accelerometer', JSON.stringify(data2Send)); 
                console.log('Data Published'); 
                prevX = x; 
                prevY = y; 
                prevZ = z; 
            } 
        }); 

        if (locCtr === 600) { // every 5 mins 
            locCtr = 0; 
            displayLocation(); 
        } 

        aclCtr++; 
        locCtr++; 
    }, 500); // every one second 
} 

function readSensorValues(CB) { 
    adxl345.getAcceleration(true) // true for g-force units, else false for m/s² 
        .then(function(acceleration) { 
            if (CB) CB(acceleration); 
        }) 
        .catch((err) => { 
            console.log('ADXL345 read error: ', err); 
        }); 
} 

function displayLocation() { 
    request('http://ipinfo.io', function(error, res, body) { 
        var info = JSON.parse(body); 
        // console.log(info); 
        locationG = info; 
        var text2Print = ''; 
        text2Print += 'City: ' + info.city; 
        text2Print += ' Region: ' + info.region; 
        text2Print += ' Country: ' + info.country + ' '; 
        lcd.setCursor(16, 0); // 1st row     
        lcd.autoscroll(); 
        printScroll(text2Print); 
    }); 
} 

// a function to print scroll 
function printScroll(str, pos) { 
    pos = pos || 0; 

    if (pos === str.length) { 
        pos = 0; 
    } 

    lcd.print(str[pos]); 
    //console.log('printing', str[pos]); 

    setTimeout(function() { 
        return printScroll(str, pos + 1); 
    }, 300); 
} 

// If ctrl+c is hit, free resources and exit. 
process.on('SIGINT', function() { 
    lcd.clear(); 
    lcd.close(); 
    process.exit(); 
});  

注意initADXL345()。我们将测量范围定义为2_G,清除偏移量,然后调用无限循环函数。在这种情况下,我们将setInterval()500毫秒运行一次,而不是每1秒。readSensorValues()500毫秒调用一次,而不是每3秒。这是为了确保我们能够及时捕捉到跌倒。

readSensorValues()中,一旦xyz值可用,我们就计算 SMV。然后,我们检查 SMV 值是否大于1:如果是,那么我们就检测到了跌倒。

除了xyz值之外,我们还发送 SMV 值以及跌倒值到 API 引擎。还要注意,在这个例子中,我们并不是在收集所有值后立即发送数据。我们只有在检测到跌倒时才发送数据。

保存所有文件。通过从chapter7/broker文件夹运行以下命令来启动代理:

mosca -c index.js -v | pino  

接下来,通过从chapter7/api-engine文件夹运行以下命令来启动 API 引擎:

npm start  

我们还没有将 IFTTT 逻辑添加到 API 引擎中,这将在下一节中完成。目前,为了验证我们的设置,让我们通过执行在树莓派上的index.js文件来开始:

npm start  

如果一切顺利,加速度计应该成功初始化,并且数据应该开始传入。

如果我们模拟自由落体,我们应该看到我们的第一条数据发送到 API 引擎,并且它应该看起来像以下截图:

正如你所看到的,模拟的自由落体给出了2.048 g 的 SMV。

我的硬件设置如下所示:

我将整个设置粘贴到了聚苯乙烯板上,这样我就可以舒适地测试跌倒检测逻辑。

在我确定自由落体的 SMV 时,我从设置中移除了 16 x 2 LCD。

在下一节中,我们将读取从设备接收到的数据,然后根据数据执行规则。

构建 IFTTT 规则引擎

现在我们正在将所需的数据发送到 API 引擎,我们将做两件事:

  1. 在网页、桌面和移动应用程序上显示我们从智能可穿戴设备得到的数据

  2. 在数据之上执行规则

我们将首先开始第二个目标。我们将构建一个规则引擎来根据我们收到的数据执行规则。

让我们从在api-engine/server文件夹的根目录下创建一个名为ifttt的文件夹开始。在ifttt文件夹中,创建一个名为rules.json的文件。更新api-engine/server/ifttt/rules.json如下:

[{ 
    "device": "b8:27:eb:39:92:0d", 
    "rules": [ 
    { 
        "if": 
        { 
            "prop": "fall", 
            "cond": "eq", 
            "valu": 1 
        }, 
        "then": 
        { 
            "action": "EMAIL", 
            "to": "arvind.ravulavaru@gmail.com" 
        } 
    }] 
}] 

从前面的代码中可以看出,我们正在维护一个包含所有规则的 JSON 文件。在我们的情况下,每个设备只有一个规则,规则有两部分:if部分和then部分。if指的是需要针对传入数据进行检查的属性,检查条件以及需要进行检查的值。then部分指的是如果if匹配,则需要执行的操作。在前面的情况下,此操作涉及发送电子邮件。

接下来,我们将构建规则引擎本身。在api-engine/server/ifttt文件夹内创建一个名为ifttt.js的文件,并更新api-engine/server/ifttt/ifttt.js,如下所示:

var Rules = require('./rules.json'); 

exports.processData = function(data) { 

    for (var i = 0; i < Rules.length; i++) { 
        if (Rules[i].device === data.macAddress) { 
            // the rule belows to the incoming device's data 
            for (var j = 0; j < Rules[i].rules.length; j++) { 
                // process one rule at a time 
                var rule = Rules[i].rules[j]; 
                var data = data.data.acclVals; 
                if (checkRuleAndData(rule, data)) { 
                    console.log('Rule Matched', 'Processing Then.'); 
                    if (rule.then.action === 'EMAIL') { 
                        console.log('Sending email to', rule.then.to); 
                        EMAIL(rule.then.to); 
                    } else { 
                        console.log('Unknown Then! Please re-check the rules'); 
                    } 
                } else { 
                    console.log('Rule Did Not Matched', rule, data); 
                } 
            } 
        } 
    } 
} 

/*   Rule process Helper  */ 
function checkRuleAndData(rule, data) { 
    var rule = rule.if; 
    if (rule.cond === 'lt') { 
        return rule.valu < data[rule['prop']]; 
    } else if (rule.cond === 'lte') { 
        return rule.valu <= data[rule['prop']]; 
    } else if (rule.cond === 'eq') { 
        return rule.valu === data[rule['prop']]; 
    } else if (rule.cond === 'gte') { 
        return rule.valu >= data[rule['prop']]; 
    } else if (rule.cond === 'gt') { 
        return rule.valu > data[rule['prop']]; 
    } else if (rule.cond === 'ne') { 
        return rule.valu !== data[rule['prop']]; 
    } else { 
        return false; 
    } 
} 

/*Then Helpers*/ 
function SMS() { 
    /// AN EXAMPLE TO SHOW OTHER THENs 
} 

function CALL() { 
    /// AN EXAMPLE TO SHOW OTHER THENs 
} 

function PUSHNOTIFICATION() { 
    /// AN EXAMPLE TO SHOW OTHER THENs 
} 

function EMAIL(to) { 
    /// AN EXAMPLE TO SHOW OTHER THENs 
    var email = require('emailjs'); 
    var server = email.server.connect({ 
        user: 'arvind.ravulavaru@gmail.com', 
        password: 'XXXXXXXXXX', 
        host: 'smtp.gmail.com', 
        ssl: true 
    }); 

    server.send({ 
        text: 'Fall has been detected. Please attend to the patient', 
        from: 'Patient Bot <arvind.ravulavaru@gmail.com>', 
        to: to, 
        subject: 'Fall Alert!!' 
    }, function(err, message) { 
        if (err) { 
            console.log('Message sending failed!', err); 
        } 
    }); 
} 

逻辑非常简单。当新的数据记录到达 API 引擎时,将调用processData()。然后,我们从rules.json文件中加载所有规则,并对它们进行迭代,以检查当前规则是否适用于传入设备。

如果是,则通过传递规则和传入数据来调用checkRuleAndData(),以检查当前数据集是否与预定义规则匹配。如果是,我们将检查动作,我们的情况是发送电子邮件。您可以在代码中更新相应的电子邮件凭据。

完成后,我们需要在api-engine/server/mqtt/index.js client.on('message')中使用topic等于accelerometer来调用processData()

更新client.on('message'),如下所示:

client.on('message', function(topic, message) { 
    // message is Buffer 
    // console.log('Topic >> ', topic); 
    // console.log('Message >> ', message.toString()); 
    if (topic === 'api-engine') { 
        var macAddress = message.toString(); 
        console.log('Mac Address >> ', macAddress); 
        client.publish('rpi', 'Got Mac Address: ' + macAddress); 
    } else if (topic === 'accelerometer') { 
        var data = JSON.parse(message.toString()); 
        console.log('data >> ', data); 
        // create a new data record for the device 
        Data.create(data, function(err, data) { 
            if (err) return console.error(err); 
            // if the record has been saved successfully,  
            // websockets will trigger a message to the web-app 
            // console.log('Data Saved :', data.data); 
            // Invoke IFTTT Rules Engine 
            RulesEngine.processData(data); 
        }); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

就是这样。我们已经准备好了 IFTTT 引擎运行所需的所有部件。

保存所有文件并重新启动 API 引擎。现在,模拟一次跌倒,我们应该看到一封电子邮件,内容应该类似于这样:

现在我们已经完成了 IFTTT 引擎,我们将更新界面以反映我们收集到的新数据。

更新 Web 应用程序

要更新 Web 应用程序,请打开web-app/src/app/device/device.component.html并进行如下更新:

<div class="container"> 
  <br> 
  <div *ngIf="!device"> 
    <h3 class="text-center">Loading!</h3> 
  </div> 
  <div class="row" *ngIf="lastRecord"> 
    <div class="col-md-12"> 
      <div class="panel panel-info"> 
        <div class="panel-heading"> 
          <h3 class="panel-title"> 
                        {{device.name}} 
                    </h3> 
          <span class="pull-right btn-click"> 
                        <i class="fa fa-chevron-circle-up"></i> 
                    </span> 
        </div> 
        <div class="clearfix"></div> 
        <div class="table-responsive"> 
          <table class="table table-striped"> 
            <tr *ngIf="lastRecord"> 
              <td>X-Axis</td> 
              <td>{{lastRecord.data.acclVals.x}} {{lastRecord.data.acclVals.units}}</td> 
            </tr> 
            <tr *ngIf="lastRecord"> 
              <td>Y-Axis</td> 
              <td>{{lastRecord.data.acclVals.y}} {{lastRecord.data.acclVals.units}}</td> 
            </tr> 
            <tr *ngIf="lastRecord"> 
              <td>Z-Axis</td> 
              <td>{{lastRecord.data.acclVals.z}} {{lastRecord.data.acclVals.units}}</td> 
            </tr> 
            <tr *ngIf="lastRecord"> 
              <td>Signal Magnitude Vector</td> 
              <td>{{lastRecord.data.acclVals.smv}}</td> 
            </tr> 
            <tr *ngIf="lastRecord"> 
              <td>Fall State</td> 
              <td>{{lastRecord.data.acclVals.fall ? 'Patient Down' : 'All is well!'}}</td> 
            </tr> 
            <tr *ngIf="lastRecord"> 
              <td>Location</td> 
              <td>{{lastRecord.data.location.city}}, {{lastRecord.data.location.region}}, {{lastRecord.data.location.country}}</td> 
            </tr> 
            <tr *ngIf="lastRecord"> 
              <td>Received At</td> 
              <td>{{lastRecord.createdAt | date : 'medium'}}</td> 
            </tr> 
          </table> 
          <hr> 
          <div class="col-md-12" *ngIf="acclVals.length > 0"> 
            <canvas baseChart [datasets]="acclVals" [labels]="lineChartLabels" [options]="lineChartOptions" [legend]="lineChartLegend" [chartType]="lineChartType"></canvas> 
          </div> 
        </div> 
      </div> 
    </div> 
  </div> 
</div> 

保存文件并运行:

npm start

一旦我们导航到设备页面,我们应该看到以下内容:

在下一节中,我们将更新桌面应用程序。

更新桌面应用程序

现在 Web 应用程序已经完成,我们将构建相同的内容并将其部署到我们的桌面应用程序中。

要开始,请返回到web-app文件夹的终端/提示符并运行:

ng build --env=prod

这将在web-app文件夹内创建一个名为dist的新文件夹。dist文件夹的内容应该如下所示:

.

├── favicon.ico

├── index.html

├── inline.bundle.js

├── inline.bundle.js.map

├── main.bundle.js

├── main.bundle.js.map

├── polyfills.bundle.js

├── polyfills.bundle.js.map

├── scripts.bundle.js

├── scripts.bundle.js.map

├── styles.bundle.js

├── styles.bundle.js.map

├── vendor.bundle.js

└── vendor.bundle.js.map

我们编写的所有代码最终都被捆绑到了前述文件中。我们将获取dist文件夹内的所有文件(而不是dist文件夹),然后将它们粘贴到desktop-app/app文件夹内。这些更改后桌面应用程序的最终结构将如下所示:

.

├── app

│ ├── favicon.ico

│ ├── index.html

│ ├── inline.bundle.js

│ ├── inline.bundle.js.map

│ ├── main.bundle.js

│ ├── main.bundle.js.map

│ ├── polyfills.bundle.js

│ ├── polyfills.bundle.js.map

│ ├── scripts.bundle.js

│ ├── scripts.bundle.js.map

│ ├── styles.bundle.js

│ ├── styles.bundle.js.map

│ ├── vendor.bundle.js

│ └── vendor.bundle.js.map

├── freeport.js

├── index.css

├── index.html

├── index.js

├── license

├── package.json

├── readme.md

└── server.js

进行测试,运行:

npm start  

然后,当我们导航到 VIEW DEVICE 页面时,我们应该看到以下内容:

现在桌面应用程序已经完成,我们将开始处理移动应用程序。

更新移动应用程序

为了在移动应用程序中反映新的模板,我们将更新mobile-app/src/pages/view-device/view-device.html,如下所示:

<ion-header> 
  <ion-navbar> 
    <ion-title>Mobile App</ion-title> 
  </ion-navbar> 
</ion-header> 
<ion-content padding> 
  <div *ngIf="!lastRecord"> 
    <h3 class="text-center">Loading!</h3> 
  </div> 
  <div *ngIf="lastRecord"> 
    <ion-list> 
      <ion-item> 
        <ion-label>Name</ion-label> 
        <ion-label>{{device.name}}</ion-label> 
      </ion-item> 
      <ion-item> 
        <ion-label>X-Axis</ion-label> 
        <ion-label>{{lastRecord.data.acclVals.x}} {{lastRecord.data.acclVals.units}}</ion-label> 
      </ion-item> 
      <ion-item> 
        <ion-label>Y-Axis</ion-label> 
        <ion-label>{{lastRecord.data.acclVals.y}} {{lastRecord.data.acclVals.units}}</ion-label> 
      </ion-item> 
      <ion-item> 
        <ion-label>Z-Axis</ion-label> 
        <ion-label>{{lastRecord.data.acclVals.z}} {{lastRecord.data.acclVals.units}}</ion-label> 
      </ion-item> 
      <ion-item> 
        <ion-label>Signal Magnitude Vector</ion-label> 
        <ion-label>{{lastRecord.data.acclVals.smv}}</ion-label> 
      </ion-item> 
      <ion-item> 
        <ion-label>Fall State</ion-label> 
        <ion-label>{{lastRecord.data.acclVals.fall ? 'Patient Down' : 'All is well!'}}</ion-label> 
      </ion-item> 
      <ion-item> 
        <ion-label>Location</ion-label> 
        <ion-label>{{lastRecord.data.location.city}}, {{lastRecord.data.location.region}}, {{lastRecord.data.location.country}}</ion-label> 
      </ion-item> 
      <ion-item> 
        <ion-label>Received At</ion-label> 
        <ion-label>{{lastRecord.createdAt | date: 'medium'}}</ion-label> 
      </ion-item> 
    </ion-list> 
  </div> 
</ion-content> 

保存所有文件并通过以下方式运行移动应用程序:

ionic serve  

您也可以使用:

ionic cordova run android 

我们应该看到以下内容:

总结

在本章中,我们使用了跌倒检测和 IFTTT 的概念。使用我们在第六章中构建的智能可穿戴设备,我们添加了跌倒检测逻辑。然后,我们将相同的数据发送到 API 引擎,并在 API 引擎中构建了自己的 IFTTT 规则引擎。我们定义了一个规则,用于在检测到跌倒时发送电子邮件。

除此之外,我们还更新了 Web、桌面和移动应用程序,以反映我们收集到的新数据。

在第八章中,树莓派图像流,我们将使用树莓派进行视频监控。

第八章:树莓派图像流式传输

在本章中,我们将学习使用树莓派 3 和树莓派摄像头进行实时视频流。我们将从树莓派 3 实时流式传输视频到我们的网络浏览器,并可以在世界各地访问此视频。作为下一步,我们将向当前设置添加运动检测器,如果检测到运动,我们将开始流式传输视频。在本章中,我们将介绍以下主题:

  • 理解 MJPEG

  • 使用树莓派和树莓派摄像头进行设置

  • 实时将摄像头图像流式传输到仪表板

  • 捕捉运动中的视频

MJPEG

引用维基百科,en.wikipedia.org/wiki/Motion_JPEG

在多媒体中,动态 JPEG(M-JPEG 或 MJPEG)是一种视频压缩格式,其中数字视频序列的每个视频帧或隔行场都单独压缩为 JPEG 图像。最初为多媒体 PC 应用程序开发,M-JPEG 现在被视频捕获设备(如数码相机、IP 摄像机和网络摄像头)以及非线性视频编辑系统所使用。它受 QuickTime Player、PlayStation 游戏机和 Safari、Google Chrome、Mozilla Firefox 和 Microsoft Edge 等网络浏览器的本地支持。

如前所述,我们将捕获一系列图像,每隔100ms并在一个主题上流式传输图像二进制数据到 API 引擎,我们将用最新的图像覆盖现有图像。

这个流媒体系统非常简单和老式。在流媒体过程中没有倒带或暂停。我们总是能看到最后一帧。

现在我们对我们要实现的目标有了很高的理解水平,让我们开始吧。

设置树莓派

使用树莓派 3 设置树莓派摄像头非常简单。您可以从任何知名在线供应商购买树莓派 3 摄像头(www.raspberrypi.org/products/camera-module-v2/)。然后您可以按照此视频进行设置:摄像头板设置:www.youtube.com/watch?v=GImeVqHQzsE

我的摄像头设置如下:

我使用了一个支架,将我的摄像头吊在上面。

设置摄像头

现在我们已经连接了摄像头并由树莓派 3 供电,我们将按照以下步骤设置摄像头:

  1. 从树莓派内部,启动一个新的终端并运行:
    sudo raspi-config
  1. 这将启动树莓派配置屏幕。选择接口选项:

  1. 在下一个屏幕上,选择 P1 摄像头并启用它:

  1. 这将触发重新启动,完成重新启动并重新登录到树莓派。

一旦您的摄像头设置好了,我们将对其进行测试。

测试摄像头

现在摄像头已经设置并通电,让我们来测试一下。打开一个新的终端并在桌面上cd。然后运行以下命令:

raspistill -o test.jpg

这将在当前工作目录Desktop中拍摄屏幕截图。屏幕看起来会像下面这样:

正如您所看到的,test.jpg被创建在Desktop上,当我双击它时,显示的是我办公室玻璃墙的照片。

开发逻辑

现在我们能够测试摄像头,我们将把这个设置与我们的物联网平台集成。我们将不断地以100ms的间隔流式传输这些图像到我们的 API 引擎,然后通过网络套接字更新网络上的用户界面。

要开始,我们将复制chapter4并将其转储到名为chapter8的文件夹中。在chapter8文件夹中,打开pi/index.js并进行以下更新:

var config = require('./config.js'); 
var mqtt = require('mqtt'); 
var GetMac = require('getmac'); 
var Raspistill = require('node-raspistill').Raspistill; 
var raspistill = new Raspistill({ 
    noFileSave: true, 
    encoding: 'jpg', 
    width: 640, 
    height: 480 
}); 

var crypto = require("crypto"); 
var fs = require('fs'); 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('rpi'); 
    GetMac.getMac(function(err, mac) { 
        if (err) throw err; 
        macAddress = mac; 
        client.publish('api-engine', mac); 
        startStreaming(); 
    }); 

}); 

client.on('message', function(topic, message) { 
    message = message.toString(); 
    if (topic === 'rpi') { 
        console.log('API Engine Response >> ', message); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

function startStreaming() { 
    raspistill 
        .timelapse(100, 0, function(image) { // every 100ms ~~FOREVER~~ 
            var data2Send = { 
                data: { 
                    image: image, 
                    id: crypto.randomBytes(8).toString("hex") 
                }, 
                macAddress: macAddress 
            }; 

            client.publish('image', JSON.stringify(data2Send)); 
            console.log('[image]', 'published'); 
        }) 
        .then(function() { 
            console.log('Timelapse Ended') 
        }) 
        .catch(function(err) { 
            console.log('Error', err); 
        }); 
} 

正如我们从前面的代码中所看到的,我们正在等待 MQTT 连接完成,一旦连接完成,我们调用startStreaming()开始流式传输。在startStreaming()内部,我们调用raspistill.timelapse()传入100ms,作为每次点击之间的时间差,0表示捕获应该持续不断地进行。

一旦图像被捕获,我们就用一个随机 ID、图像缓冲区和设备的macAddress构造data2Send对象。在发布到图像主题之前,我们将data2Send对象转换为字符串。

现在,将这个文件移动到树莓派的pi-client文件夹中,位于桌面上。然后从树莓派的pi-client文件夹内运行:

npm install && npm install node-raspistill --save  

这两个命令将安装node-raspistillpackage.json文件内的其他节点模块。

有了这个,我们完成了树莓派和相机的设置。在下一节中,我们将更新 API 引擎以接受图像的实时流。

更新 API 引擎

现在我们完成了树莓派的设置,我们将更新 API 引擎以接受传入的数据。

我们要做的第一件事是按照以下方式更新api-engine/server/mqtt/index.js

var Data = require('../api/data/data.model'); 
var mqtt = require('mqtt'); 
var config = require('../config/environment'); 
var fs = require('fs'); 
var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    console.log('Connected to Mosca at ' + config.mqtt.host + ' on port ' + config.mqtt.port); 
    client.subscribe('api-engine'); 
    client.subscribe('image'); 
}); 

client.on('message', function(topic, message) { 
    // message is Buffer 
    // console.log('Topic >> ', topic); 
    // console.log('Message >> ', message.toString()); 
    if (topic === 'api-engine') { 
        var macAddress = message.toString(); 
        console.log('Mac Address >> ', macAddress); 
        client.publish('rpi', 'Got Mac Address: ' + macAddress); 
    } else if (topic === 'image') { 
        message = JSON.parse(message.toString()); 
        // convert string to buffer 
        var image = Buffer.from(message.data.image, 'utf8'); 
        var fname = 'stream_' + ((message.macAddress).replace(/:/g, '_')) + '.jpg'; 
        fs.writeFile(__dirname + '/stream/' + fname, image, { encoding: 'binary' }, function(err) { 
            if (err) { 
                console.log('[image]', 'save failed', err); 
            } else { 
                console.log('[image]', 'saved'); 
            } 
        }); 

        // as of now we are not going to 
        // store the image buffer in DB.  
        // Gridfs would be a good way 
        // instead of storing a stringified text 
        delete message.data.image; 
        message.data.fname = fname; 

        // create a new data record for the device 
        Data.create(message, function(err, data) { 
            if (err) return console.error(err); 
            // if the record has been saved successfully,  
            // websockets will trigger a message to the web-app 
            // console.log('Data Saved :', data); 
        }); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

正如我们从前面的代码中所看到的,在 MQTT 的消息事件中,我们添加了一个名为image的新主题。在这个主题内,我们提取了图像缓冲区的字符串格式,并将其转换回图像二进制数据。然后使用fs模块,我们一遍又一遍地覆盖相同的图像。

我们同时将数据保存到 MongoDB,并触发一个 socket emit。

作为下一步,我们需要在mqtt文件夹内创建一个名为stream的文件夹。在这个文件夹内,放入一个图片,链接在这里:http://www.iconarchive.com/show/small-n-flat-icons-by-paomedia/sign-ban-icon.html. 如果相机没有可用的视频流,将显示这张图片。

所有的图像都将保存在stream文件夹内,对于相同的设备将更新相同的图像,正如前面提到的,不会有任何倒带或重播。

现在,图片被保存在stream文件夹内,我们需要暴露一个端点来将这张图片发送给请求的客户端。为此,打开api-engine/server/routes.js并将以下内容添加到module.exports函数中:

app.get('/stream/:fname', function(req, res, next) { 
        var fname = req.params.fname; 
        var streamDir = __dirname + '/mqtt/stream/'; 
        var img = streamDir + fname; 
        console.log(img); 
        fs.exists(img, function(exists) { 
         if (exists) { 
                return res.sendFile(img); 
            } else { 
                // http://www.iconarchive.com/show/small-n-flat-icons-by-paomedia/sign-ban-icon.html 
                return res.sendFile(streamDir + '/no-image.png'); 
            } 
        }); 
    });  

这将负责将图像分发给客户端(Web、桌面和移动端)。

有了这个,我们就完成了 API 引擎的设置。

保存所有文件并启动代理、API 引擎和pi-client。如果一切顺利设置,我们应该能看到来自树莓派的数据被发布。

以及在 API 引擎中出现的相同数据:

此时,图像正在被捕获并通过 MQTT 发送到 API 引擎。下一步是实时查看这些图像。

更新 Web 应用程序

现在数据正在流向 API 引擎,我们将在 Web 应用程序上显示它。打开web-app/src/app/device/device.component.html并按照以下方式更新它:

<div class="container"> 
    <br> 
    <div *ngIf="!device"> 
        <h3 class="text-center">Loading!</h3> 
    </div> 
    <div class="row" *ngIf="!lastRecord"> 
        <h3 class="text-center">No Data!</h3> 
    </div> 
    <div class="row" *ngIf="lastRecord"> 
        <div class="col-md-12"> 
            <div class="panel panel-info"> 
                <div class="panel-heading"> 
                    <h3 class="panel-title"> 
                        {{device.name}} 
                    </h3> 
                    <span class="pull-right btn-click"> 
                        <i class="fa fa-chevron-circle-up"></i> 
                    </span> 
                </div> 
                <div class="clearfix"></div> 
                <div class="table-responsive" *ngIf="lastRecord"> 
                    <table class="table table-striped"> 
                        <tr> 
                            <td colspan="2" class="text-center"><img  [src]="lastRecord.data.fname"></td> 
                        </tr> 
                        <tr class="text-center" > 
                            <td>Received At</td> 
                            <td>{{lastRecord.createdAt | date: 'medium'}}</td> 
                        </tr> 
                    </table> 
                </div> 
            </div> 
        </div> 
    </div> 
</div> 

在这里,我们实时显示了我们创建的图像。接下来,按照以下方式更新web-app/src/app/device/device.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core'; 
import { DevicesService } from '../services/devices.service'; 
import { Params, ActivatedRoute } from '@angular/router'; 
import { SocketService } from '../services/socket.service'; 
import { DataService } from '../services/data.service'; 
import { NotificationsService } from 'angular2-notifications'; 
import { Globals } from '../app.global'; 

@Component({ 
   selector: 'app-device', 
   templateUrl: './device.component.html', 
   styleUrls: ['./device.component.css'] 
}) 
export class DeviceComponent implements OnInit, OnDestroy { 
   device: any; 
   data: Array<any>; 
   toggleState: boolean = false; 
   private subDevice: any; 
   private subData: any; 
   lastRecord: any; 

   // line chart config 

   constructor(private deviceService: DevicesService, 
         private socketService: SocketService, 
         private dataService: DataService, 
         private route: ActivatedRoute, 
         private notificationsService: NotificationsService) { } 

   ngOnInit() { 
         this.subDevice = this.route.params.subscribe((params) => { 
               this.deviceService.getOne(params['id']).subscribe((response) => { 
                     this.device = response.json(); 
                     this.getData(); 
               }); 
         }); 
   } 

   getData() { 
         this.dataService.get(this.device.macAddress).subscribe((response) => { 
               this.data = response.json(); 
               let d = this.data[0]; 
               d.data.fname = Globals.BASE_API_URL + 'stream/' + d.data.fname; 
               this.lastRecord = d; // descending order data 
               this.socketInit(); 
         }); 
   } 

   socketInit() { 
         this.subData = this.socketService.getData(this.device.macAddress).subscribe((data: any) => { 
               if (this.data.length <= 0) return; 
               this.data.splice(this.data.length - 1, 1); // remove the last record 
               data.data.fname = Globals.BASE_API_URL + 'stream/' + data.data.fname + '?t=' + (Math.random() * 100000); // cache busting 
               this.data.push(data); // add the new one 
               this.lastRecord = data; 
         }); 
   }
   ngOnDestroy() { 
         this.subDevice.unsubscribe(); 
         this.subData ? this.subData.unsubscribe() : ''; 
   } 
} 

在这里,我们正在构建图像 URL 并将其指向 API 引擎。保存所有文件,并通过在web-app文件夹内运行以下命令来启动 Web 应用程序:

npm start  

如果一切按预期工作,当导航到“查看设备”页面时,我们应该会看到视频流非常缓慢地显示。我正在监视放在椅子前面的杯子,如下所示:

更新桌面应用程序

现在 Web 应用程序已经完成,我们将构建相同的应用程序并将其部署到我们的桌面应用程序内。

要开始,请返回到web-app文件夹的终端/提示符,并运行以下命令:

ng build --env=prod  

这将在web-app文件夹内创建一个名为dist的新文件夹。dist文件夹的内容应该如下所示:

.

├── favicon.ico

├── index.html

├── inline.bundle.js

├── inline.bundle.js.map

├── main.bundle.js

├── main.bundle.js.map

├── polyfills.bundle.js

├── polyfills.bundle.js.map

├── scripts.bundle.js

├── scripts.bundle.js.map

├── styles.bundle.js

├── styles.bundle.js.map

├── vendor.bundle.js

└── vendor.bundle.js.map

我们编写的所有代码最终都打包到了上述文件中。我们将获取dist文件夹中的所有文件(不包括dist文件夹),然后将其粘贴到desktop-app/app文件夹中。在进行上述更改后,desktop-app的最终结构将如下所示:

.

├── app

│ ├── favicon.ico

│ ├── index.html

│ ├── inline.bundle.js

│ ├── inline.bundle.js.map

│ ├── main.bundle.js

│ ├── main.bundle.js.map

│ ├── polyfills.bundle.js

│ ├── polyfills.bundle.js.map

│ ├── scripts.bundle.js

│ ├── scripts.bundle.js.map

│ ├── styles.bundle.js

│ ├── styles.bundle.js.map

│ ├── vendor.bundle.js

│ └── vendor.bundle.js.map

├── freeport.js

├── index.css

├── index.html

├── index.js

├── license

├── package.json

├── readme.md

└── server.js

进行测试,运行以下命令:

npm start 

然后当我们导航到 VIEW DEVICE 页面时,我们应该看到:

这样我们就完成了桌面应用程序的开发。在下一节中,我们将更新移动应用程序。

更新移动应用程序

在上一节中,我们已经更新了桌面应用程序。在本节中,我们将更新移动应用程序模板以流式传输图像。

首先,我们将更新 view-device 模板。按照以下方式更新mobile-app/src/pages/view-device/view-device.html

<ion-header> 
    <ion-navbar> 
        <ion-title>Mobile App</ion-title> 
    </ion-navbar> 
</ion-header> 
<ion-content padding> 
    <div *ngIf="!lastRecord"> 
        <h3 class="text-center">Loading!</h3> 
    </div> 
    <div *ngIf="lastRecord"> 
        <ion-list> 
            <ion-item> 
                <img [src]="lastRecord.data.fname"> 
            </ion-item> 
            <ion-item> 
                <ion-label>Received At</ion-label> 
                <ion-label>{{lastRecord.createdAt | date: 'medium'}}</ion-label> 
            </ion-item> 
        </ion-list> 
    </div> 
</ion-content> 

在移动端显示图像流的逻辑与 Web/桌面端相同。接下来,按照以下方式更新mobile-app/src/pages/view-device/view-device.ts

import { Component } from '@angular/core'; 
import { IonicPage, NavController, NavParams } from 'ionic-angular'; 
import { Globals } from '../../app/app.globals'; 
import { DevicesService } from '../../services/device.service'; 
import { DataService } from '../../services/data.service'; 
import { ToastService } from '../../services/toast.service'; 
import { SocketService } from '../../services/socket.service'; 

@IonicPage() 
@Component({ 
   selector: 'page-view-device', 
   templateUrl: 'view-device.html', 
}) 
export class ViewDevicePage { 
   device: any; 
   data: Array<any>; 
   toggleState: boolean = false; 
   private subData: any; 
   lastRecord: any; 

   constructor(private navCtrl: NavController, 
         private navParams: NavParams, 
         private socketService: SocketService, 
         private deviceService: DevicesService, 
         private dataService: DataService, 
         private toastService: ToastService) { 
         this.device = navParams.get("device"); 
         console.log(this.device); 
   } 

   ionViewDidLoad() { 
         this.deviceService.getOne(this.device._id).subscribe((response) => { 
               this.device = response.json(); 
               this.getData(); 
         }); 
   } 

   getData() { 
         this.dataService.get(this.device.macAddress).subscribe((response) => { 
               this.data = response.json(); 
               let d = this.data[0]; 
               d.data.fname = Globals.BASE_API_URL + 'stream/' + d.data.fname; 
               this.lastRecord = d; // descending order data 
               this.socketInit(); 
         }); 
   } 

   socketInit() { 
         this.subData = this.socketService.getData(this.device.macAddress).subscribe((data: any) => { 
               if(this.data.length <= 0) return; 
               this.data.splice(this.data.length - 1, 1); // remove the last record 
               data.data.fname = Globals.BASE_API_URL + 'stream/' + data.data.fname + '?t=' + (Math.random() * 100000); 
               this.data.push(data); // add the new one 
               this.lastRecord = data; 
         }); 
   } 

   ionViewDidUnload() { 
         this.subData && this.subData.unsubscribe && this.subData.unsubscribe(); //unsubscribe if subData is defined 
   } 
} 

保存所有文件并通过以下方式运行移动应用程序:

ionic serve  

或者使用以下代码:

ionic cordova run android  

然后我们应该看到以下内容:

这样我们就完成了在移动应用程序上显示摄像头数据。

基于运动的视频捕获

正如我们所看到的,流式传输有些不连贯,缓慢,并非实时,另一个可能的解决方案是在树莓派和摄像头上放置一个运动检测器。然后当检测到运动时,我们开始录制 10 秒的视频。然后将此视频作为附件通过电子邮件发送给用户。

现在,我们将开始更新我们现有的代码。

更新树莓派

首先,我们将更新我们的树莓派设置以适应 HC-SR501 PIR 传感器。您可以在此处找到 PIR 传感器:www.amazon.com/Motion-HC-SR501-Infrared-Arduino-Raspberry/dp/B00M1H7KBW/ref=sr_1_4_a_it

我们将把 PIR 传感器连接到树莓派的 17 号引脚,将摄像头连接到摄像头插槽,就像我们之前看到的那样。

一旦连接如前所述,按照以下方式更新pi/index.js

var config = require('./config.js'); 
var mqtt = require('mqtt'); 
var GetMac = require('getmac'); 
var Raspistill = require('node-raspistill').Raspistill; 
var crypto = require("crypto"); 
var fs = require('fs'); 
var Gpio = require('onoff').Gpio; 
var exec = require('child_process').exec; 

var pir = new Gpio(17, 'in', 'both'); 
var raspistill = new Raspistill({ 
    noFileSave: true, 
    encoding: 'jpg', 
    width: 640, 
    height: 480 
}); 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('rpi'); 
    GetMac.getMac(function(err, mac) { 
        if (err) throw err; 
        macAddress = mac; 
        client.publish('api-engine', mac); 
        // startStreaming(); 
    }); 

}); 

client.on('message', function(topic, message) { 
    message = message.toString(); 
    if (topic === 'rpi') { 
        console.log('API Engine Response >> ', message); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

function startStreaming() { 
    raspistill 
        .timelapse(100, 0, function(image) { // every 100ms ~~FOREVER~~ 
            var data2Send = { 
                data: { 
                    image: image, 
                    id: crypto.randomBytes(8).toString("hex") 
                }, 
                macAddress: macAddress 
            }; 

            client.publish('image', JSON.stringify(data2Send)); 
            console.log('[image]', 'published'); 
        }) 
        .then(function() { 
            console.log('Timelapse Ended') 
        }) 
        .catch(function(err) { 
            console.log('Error', err); 
        }); 
} 

var isRec = false; 

// keep watching for motion 
pir.watch(function(err, value) { 
    if (err) exit(); 
    if (value == 1 && !isRec) { 
        console.log('Intruder detected'); 
        console.log('capturing video.. '); 
        isRec = true; 
        var videoPath = __dirname + '/video.h264'; 
        var file = fs.createWriteStream(videoPath); 
        var video_path = './video/video' + Date.now() + '.h264'; 
        var cmd = 'raspivid -o ' + video_path + ' -t 5000'; 

        exec(cmd, function(error, stdout, stderr) { 
            // output is in stdout 
            console.log('Video Saved @ : ', video_path); 
            require('./mailer').sendEmail(video_path, true, function(err, info) { 
                setTimeout(function() { 
                    // isRec = false; 
                }, 3000); // don't allow recording for 3 sec after 
            }); 
        }); 
    } 
}); 

function exit() { 
    pir.unexport(); 
    process.exit(); 
} 

从上述代码中可以看出,我们已将 GPIO 17 标记为输入引脚,并将其分配给名为pir的变量。接下来,使用pir.watch(),我们不断查看运动检测器的值是否发生变化。如果运动检测器检测到某种变化,我们将检查值是否为1,这表示触发了运动。然后使用raspivid我们录制一个 5 秒的视频并通过电子邮件发送。

为了从树莓派 3 发送电子邮件所需的逻辑,创建一个名为mailer.js的新文件,放在pi-client文件夹的根目录,并按以下方式更新它:

var fs = require('fs'); 
var nodemailer = require('nodemailer'); 

var transporter = nodemailer.createTransport({ 
    service: 'Gmail', 
    auth: { 
        user: 'arvind.ravulavaru@gmail.com', 
        pass: '**********' 
    } 
}); 

var timerId; 

module.exports.sendEmail = function(file, deleteAfterUpload, cb) { 
    if (timerId) return; 

    timerId = setTimeout(function() { 
        clearTimeout(timerId); 
        timerId = null; 
    }, 10000); 

    console.log('Sendig an Email..'); 

    var mailOptions = { 
        from: 'Pi Bot <pi.intruder.alert@gmail.com>', 
        to: 'user@email.com', 
        subject: '[Pi Bot] Intruder Detected', 
        html: 'Intruder Detected. Please check the video attached. <br/><br/> Intruder Detected At : ' + Date(), 
        attachments: [{ 
            path: file 
        }] 
    }; 

    transporter.sendMail(mailOptions, function(err, info) { 
        if (err) { 
            console.log(err); 
        } else { 
            console.log('Message sent: ' + info.response); 
            if (deleteAfterUpload) { 
                fs.unlink(path); 
            } 
        } 

        if (cb) { 
            cb(err, info); 
        } 
    }); 
} 

我们使用 nodemailer 发送电子邮件。根据需要更新凭据。

接下来,运行以下命令:

npm install onoff -save  

在下一节中,我们将测试这个设置。

测试代码

现在我们已经完成设置,让我们来测试一下。给树莓派供电,如果尚未上传代码,则上传代码,并运行以下命令:

npm start

代码运行后,触发一次运动。这将启动摄像头录制并保存 5 秒的视频。然后将此视频通过电子邮件发送给用户。以下是输出的列表:

收到的电子邮件将如下所示:

这是使用树莓派 3 进行监视的另一种方法。

总结

在本章中,我们已经看到了使用树莓派进行监视的两种方法。第一种方法是我们将图像流式传输到 API 引擎,然后在移动 Web 和桌面应用程序上使用 MJPEG 进行可视化。第二种方法是检测运动,然后开始录制视频。然后将此视频作为附件通过电子邮件发送。这两种方法也可以结合在一起,如果在第一种方法中检测到运动,则可以开始 MJPEG 流式传输。

在第九章中,智能监控,我们将把这个提升到下一个级别,我们将在我们的捕获图像上添加人脸识别,并使用 AWS Rekognition 平台进行人脸识别(而不是人脸检测)。

第九章:智能监视

在第八章 树莓派图像流中,我们学习了如何将树莓派相机模块连接到树莓派 3,拍摄照片或视频,然后实时上传/流式传输。在本章中,我们将把这种逻辑提升到下一个级别。当检测到入侵时,我们将拍照,然后将该图像发送到亚马逊 Rekognition 平台并与一组图像进行比较。

在本章中,我们将涵盖以下内容:

  • 理解 AWS Rekognition

  • 使用授权人脸填充 AWS Rekognition 集合

  • 在入侵时从树莓派 3 拍照并将其与种子人脸进行比较

AWS Rekognition

以下引用来自 Amazon Rekognition (aws.amazon.com/rekognition/):

“Amazon Rekognition 是一项使您的应用程序轻松添加图像分析的服务。使用 Rekognition,您可以检测对象、场景、人脸;识别名人;并识别图像中的不当内容。您还可以搜索和比较人脸。Rekognition 的 API 使您能够快速将基于深度学习的复杂视觉搜索和图像分类添加到您的应用程序中。”

在本章中,我们将利用 AWS Rekognition 功能,帮助我们基于人脸识别而不是人脸检测来设置有条件的监视。

假设您已经在家门口使用树莓派设置了摄像头,并对其进行了编程,以持续拍摄入侵者的照片并将其发送给您。在这种设置中,您将收到每个来到您家门口的人的照片,例如您的家人、邻居等。但是,如果只有在入侵者是陌生人时才通知您呢?现在,这就是我所说的智能监视。

在第八章 树莓派图像流中,我们建立了一个设置,当检测到入侵时捕获图像,然后实时发送电子邮件并更新应用程序。

在本章中,我们将使用一组受信任的人脸填充 AWS Rekognition。然后,当摄像头捕获图像时,在检测到入侵时,我们将其发送到 AWS Rekognition 进行人脸识别。如果图像与受信任的图像之一匹配,则不会发生任何事情;否则,将发送电子邮件通知。

要了解更多关于 AWS Rekognition 及其工作原理的信息,请查看宣布亚马逊 Rekognition-基于深度学习的图像分析(www.youtube.com/watch?v=b6gN9jCmq3w)。

设置智能监视

现在我们已经了解了我们要做什么,我们将开始设置树莓派。

我们将设置摄像头和运动检测器,就像我们在第八章 树莓派图像流中所做的那样。接下来,我们将添加所需的逻辑,以在检测到运动时捕获图像,然后将其发送进行处理。

在执行此操作之前,我们需要使用授权人脸填充 Rekognition 集合。

此脚本可以作为 API 的一部分,作为 API 引擎,并且使用 Web 仪表板,我们可以上传和填充图像。但为了保持简单,我们将从计算机上运行这个独立的脚本。

设置 AWS 凭据

在开始开发之前,我们需要在本地机器上安装 AWS CLI 和 AWS 凭据。

首先,我们需要安装 AWS CLI。转到aws.amazon.com/cli并按照页面上的说明进行操作。要从命令提示符中测试安装,请运行:

aws --version

您应该看到类似以下的内容:

aws-cli/1.7.38 Python/2.7.9 Darwin/16.1.0

设置完成后,我们需要配置 AWS 凭据,这样只要我们使用这台机器,就不需要在代码中输入任何凭据。

运行以下内容:

aws configure

您将看到四个问题;用适当的信息填写它们:

如果在配置 AWS 凭据时遇到问题,请参考docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-quick-configuration

另一个选择是在代码本身中添加accessKeyIdsecretAccessKey。但我们仍然需要accessKeyIdsecretAccessKey来继续。

配置完成后,我们将开始与 AWS Rekognition 进行接口。

播种授权面孔

创建一个名为chapter9的文件夹,在这个文件夹内创建一个名为rekogniton_seed的文件夹。在这个文件夹内,创建一个名为seed.js的文件。

按照以下方式更新seed.js

var config = { 
    collectionName: 'AIOWJS-FACES', 
    region: 'eu-west-1', 
// If the credentials are set using `aws configure`, below two properties are not needed.    
    accessKeyId: 'YOUR-ACCESSKEYID',  
    secretAccessKey: YOUR-SECRETACCESSKEY' 
}; 

var AWS = require('aws-sdk'); 
var fs = require('fs-extra'); 
var path = require('path'); 
var klawSync = require('klaw-sync') 

AWS.config.region = config.region; 

var rekognition = new AWS.Rekognition({ 
    region: config.region, 
 // accessKeyId: config.accessKeyId, // uncomment as applicable 
 // secretAccessKey: config.secretAccessKey // uncomment as applicable 
}); 

function createCollection() { 
    rekognition.createCollection({ 
        'CollectionId': config.collectionName 
    }, (err, data) => { 
        if (err) { 
            console.log(err, err.stack); // an error occurred 
        } else { 
            console.log(data); // successful response 
        } 
    }); 
} 

function indexFaces() { 
    var paths = klawSync('./faces', { 
        nodir: true, 
        ignore: ['*.json'] 
    }); 

    paths.forEach((file) => { 
        var p = path.parse(file.path); 
        var name = p.name.replace(/\W/g, ''); 
        var bitmap = fs.readFileSync(file.path); 

        rekognition.indexFaces({ 
            'CollectionId': config.collectionName, 
            'DetectionAttributes': ['ALL'], 
            'ExternalImageId': name, 
            'Image': { 
                'Bytes': bitmap 
            } 
        }, (err, data) => { 
            if (err) { 
                console.log(err, err.stack); // an error occurred 
            } else { 
                console.log(data.FaceRecords); // successful response 
                fs.writeJson(file.path + '.json', data, (err) => { 
                    if (err) return console.error(err) 
                }); 
            } 
        }); 
    }); 
} 

createCollection(); 
indexFaces(); 

有关其他评论,请参阅源代码:github.com/PacktPublishing/Practical-Internet-of-Things-with-JavaScript

从前面的代码片段中,我们看到我们正在在eu-west-1地区创建一个名为AIOWJS-FACES的新集合。您可以在代码中使用accessKeyIdsecretAccessKey,也可以使用 AWS CLI 配置中的这两个。如果您正在使用 AWS CLI 配置中的密钥和密钥,您可以在初始化rekognition的新实例时将这两行注释掉。

我们调用createCollection()来创建一个新的集合,这只需要运行一次。

您可以随时播种数据,但集合创建应该只发生一次。

创建集合后,我们将从名为faces的文件夹中索引一些图像,现在我们将创建。在rekogniton_seed文件夹的根目录下创建一个名为faces的文件夹。在此文件夹中,上传带有面孔的清晰图像。图像的质量和清晰度越好,被识别的机会就越大。

我在faces文件夹中放了几张我的照片。在我们开始播种之前,我们需要安装所需的依赖项:

  1. rekogniton_seed文件夹内打开命令提示符/终端并运行:
npm init --yes
  1. 接下来,运行:
npm install aws-sdk fs-extra klaw-sync --save
  1. 安装完成后,通过运行创建集合并通过运行种子面孔:
node seed.js
  1. 对于每个上传的图像,我们应该看到类似以下的输出:
[ { Face:  
     { FaceId: '2d7ac2b3-fa84-5a16-ad8c-7fa670b8ec8c', 
       BoundingBox: [Object], 
       ImageId: '61a299b6-3004-576d-b966-31fb6780f1c7', 
       ExternalImageId: 'photo', 
       Confidence: 99.96211242675781 }, 
    FaceDetail:  
     { BoundingBox: [Object], 
       AgeRange: [Object], 
       Smile: [Object], 
       Eyeglasses: [Object], 
       Sunglasses: [Object], 
       Gender: [Object], 
       Beard: [Object], 
       Mustache: [Object], 
       EyesOpen: [Object], 
       MouthOpen: [Object], 
       Emotions: [Object], 
       Landmarks: [Object], 
       Pose: [Object], 
       Quality: [Object], 
       Confidence: 99.96211242675781 } } ] 

这个对象将包含 Rekognition 分析的图像的信息。

一旦播种完成,您可以查看faces文件夹中的*.json文件。这些 JSON 文件将包含有关图像的更多信息。

测试播种

现在播种完成了,让我们验证播种。这一步是完全可选的;如果您愿意,可以跳过这一步。

chapter9文件夹的根目录下创建一个名为rekogniton_seed_test的新文件夹。然后在rekogniton_seed_test的根目录下创建一个名为faces的文件夹,并将要测试的图像放入该文件夹。在我的情况下,图片是我在不同地点的照片。

接下来,创建一个名为seed_test.js的文件并更新它,如下所示:

var config = { 
    collectionName: 'AIOWJS-FACES', 
    region: 'eu-west-1', 
    accessKeyId: 'ACCESSKEYID',  
    secretAccessKey: SECRETACCESSKEY' 
}; 

var AWS = require('aws-sdk'); 
var fs = require('fs-extra'); 
var path = require('path'); 
var klawSync = require('klaw-sync') 

AWS.config.region = config.region; 

var rekognition = new AWS.Rekognition({ 
    region: config.region, 
    // accessKeyId: config.accessKeyId, // uncomment as applicable 
    // secretAccessKey: config.secretAccessKey // uncomment as applicable 
}); 

// Once you've created your collection you can run this to test it out. 
function FaceSearchTest(imagePath) { 
    var bitmap = fs.readFileSync(imagePath); 

    rekognition.searchFacesByImage({ 
        "CollectionId": config.collectionName, 
        "FaceMatchThreshold": 80, 
        "Image": { 
            "Bytes": bitmap, 
        }, 
        "MaxFaces": 1 
    }, (err, data) => { 
        if (err) { 
            console.error(err, err.stack); // an error occurred 
        } else { 
            // console.log(data); // successful response 
            console.log(data.FaceMatches.length > 0 ? data.FaceMatches[0].Face : data); 
        } 
    }); 
} 

FaceSearchTest(__dirname + '/faces/arvind_2.jpg'); 

在前面的代码中,我们从faces文件夹中提取图像并提交给识别,然后打印适当的响应。

完成后,我们将安装所需的依赖项:

  1. rekogniton_seed_test文件夹内打开命令提示符/终端并运行:
npm init --yes
  1. 然后运行:
npm install aws-sdk fs-extra path --save
  1. 现在,我们已经准备好运行这个例子了。从rekogniton_seed_test文件夹内运行:
node seed_test.js
  1. 我们应该看到类似以下的东西:
{ FaceId: '2d7ac2b3-fa84-5a16-ad8c-7fa670b8ec8c', 
  BoundingBox:  
   { Width: 0.4594019949436188, 
     Height: 0.4594019949436188, 
     Left: 0.3076919913291931, 
     Top: 0.2820509970188141 }, 
  ImageId: '61a299b6-3004-576d-b966-31fb6780f1c7', 
  ExternalImageId: 'photo', 
  Confidence: 99.96209716796875 } 

从前面的响应中有几件事情需要注意:

  • FaceId:这是当前面部匹配的 ID

  • ImageId:这是当前面部匹配的图像

有了这个,我们甚至可以从我们已经索引/播种的图像中标记用户。

您可以通过放置一个与我们的种子数据不匹配的图像并更新上述代码的最后一行来进行负面测试:

FaceSearchTest(__dirname + '/faces/no_arvind.jpg');

We should see something like the following:

{ SearchedFaceBoundingBox:

{ Width: 0.5322222113609314,

Height: 0.5333333611488342,

Left: 0.2777777910232544,

Top: 0.12444444745779037 },

SearchedFaceConfidence: 99.76634979248047,

FaceMatches: [] }

如你所见,没有找到匹配项。

一旦我们捕获了图像,我们将在树莓派中使用上述方法。

部署到树莓派

现在我们已经创建了一个 Rekognition 集合,并对其进行了测试(可选步骤),我们现在将开始设置树莓派代码。

我们将使用chapter8文件夹中的所有其他代码片段,并且只修改chapter9文件夹中的树莓派客户端。

将整个代码从chapter8文件夹复制到chapter9文件夹中。然后,打开pi-client文件夹,无论是在您的桌面上还是在树莓派本身上,并进行以下更新:

var config = require('./config.js'); 
var mqtt = require('mqtt'); 
var GetMac = require('getmac'); 
var Raspistill = require('node-raspistill').Raspistill; 
var crypto = require("crypto"); 
var Gpio = require('onoff').Gpio; 
var exec = require('child_process').exec; 

var AWS = require('aws-sdk'); 

var pir = new Gpio(17, 'in', 'both'); 
var raspistill = new Raspistill({ 
    noFileSave: true, 
    encoding: 'bmp', 
    width: 640, 
    height: 480 
}); 

// Rekognition config 
var config = { 
    collectionName: 'AIOWJS-FACES', 
    region: 'eu-west-1', 
    accessKeyId: 'ACCESSKEYID',  
    secretAccessKey: 'SECRETACCESSKEY' 
}; 

AWS.config.region = config.region; 

var rekognition = new AWS.Rekognition({ 
    region: config.region, 
    accessKeyId: config.accessKeyId, 
    secretAccessKey: config.secretAccessKey 
}); 

var client = mqtt.connect({ 
    port: config.mqtt.port, 
    protocol: 'mqtts', 
    host: config.mqtt.host, 
    clientId: config.mqtt.clientId, 
    reconnectPeriod: 1000, 
    username: config.mqtt.clientId, 
    password: config.mqtt.clientId, 
    keepalive: 300, 
    rejectUnauthorized: false 
}); 

client.on('connect', function() { 
    client.subscribe('rpi'); 
    GetMac.getMac(function(err, mac) { 
        if (err) throw err; 
        macAddress = mac; 
        client.publish('api-engine', mac); 
        // startStreaming(); 
    }); 

}); 

client.on('message', function(topic, message) { 
    message = message.toString(); 
    if (topic === 'rpi') { 
        console.log('API Engine Response >> ', message); 
    } else { 
        console.log('Unknown topic', topic); 
    } 
}); 

var processing = false; 

// keep watching for motion 
pir.watch(function(err, value) { 
    if (err) exit(); 
    if (value == 1 && !processing) { 
        raspistill.takePhoto() 
            .then((photo) => { 
                console.log('took photo'); 
                checkForMatch(photo, function(err, authorizedFace) { 
                    if (err) { 
                        console.error(err); 
                    } else { 
                        if (authorizedFace) { 
                            console.log('User Authorized'); 
                        } else { 
                            // unauthorized user,  
                            // send an email! 
                            require('./mailer').sendEmail(photo, function(err, info) { 
                                if (err) { 
                                    console.error(err); 
                                } else { 
                                    console.log('Email Send Success', info); 
                                } 
                            }); 
                        } 
                    } 
                }); 
            }) 
            .catch((error) => { 
                console.error('something bad happened', error); 
            }); 
    } 
}); 

function checkForMatch(image, cb) { 
    rekognition.searchFacesByImage({ 
        'CollectionId': config.collectionName, 
        'FaceMatchThreshold': 80, 
        'Image': { 
            'Bytes': image, 
        }, 
        'MaxFaces': 1 
    }, (err, data) => { 
        if (err) { 
            console.error(err, err.stack); // an error occurred 
            cb(err, null); 
        } else { 
            // console.log(data); // successful response 
            console.log(data.FaceMatches.length > 0 ? data.FaceMatches[0].Face : data); 
            cb(null, data.FaceMatches.length >= 1); 
        } 
    }); 
} 

function exit() { 
    pir.unexport(); 
    process.exit(); 
} 

在上述代码中,我们已经配置了向 AWS Rekognition 发出请求所需的配置,然后运行checkForMatch(),它将获取原始照片并检查匹配项。如果找到任何匹配项,我们将不会收到电子邮件;如果没有找到匹配项,我们将收到电子邮件。

接下来,我们将安装所需的依赖项。

运行以下命令:

npm install getmac mqtt node-raspistill aws-sdk --save

安装完成后,启动代理、api-engine和 Web 仪表板。然后运行以下命令:

node index.js

触发运动以捕获图像。如果捕获的图像与我们索引的人脸之一匹配,我们将不会收到电子邮件;如果匹配,我们将收到电子邮件。

简单吧?这是一个非常强大的设置,我们已经建立了一个可以在家庭或办公室提供监控的系统,可以轻松识别简单的误报。

这个例子可以进一步扩展,以便使用基于云的呼叫服务(如 Twilio)发送推送通知或呼叫邻居。

总结

在本章中,我们看到了如何使用树莓派和 AWS Rekognition 平台建立智能监控系统。

我们首先了解了 AWS Rekognition 平台,然后使用我们的图像对集合进行了索引/种子化。接下来,我们更新了树莓派代码,以便在检测到运动时拍摄照片,然后将该图像发送到 AWS Rekognition 以确定当前照片中的人脸是否与任何索引图像匹配。如果匹配,我们忽略该图像;如果不匹配,我们将发送一封带有该图像的电子邮件。

通过这样,我们完成了使用 JavaScript 进行实际物联网。我希望您已经学会了几种利用 JavaScript 和树莓派构建简单而强大的物联网解决方案的方法。

posted @ 2024-05-22 12:07  绝不原创的飞龙  阅读(14)  评论(0编辑  收藏  举报